240 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			240 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
<script setup lang="ts">
 | 
						|
	import { ref, defineProps, watch, onMounted, toRefs, getCurrentInstance, PropType } from 'vue'
 | 
						|
	import { generateUniqueId, applyColor, getL10Weekday, getCalendarDates } from '../utils'
 | 
						|
 | 
						|
	interface SingleDatePickerPropsColorScheme {
 | 
						|
		mainColor: string
 | 
						|
		accentColor: string
 | 
						|
		borderColor: string
 | 
						|
		hoverColor: string
 | 
						|
		reversedColor: string
 | 
						|
	}
 | 
						|
 | 
						|
	type SingleDatePickerPropsAvailableDates = [Date | null, Date | null]
 | 
						|
 | 
						|
	const props = defineProps({
 | 
						|
		colorScheme: {
 | 
						|
			type: Object as () => SingleDatePickerPropsColorScheme,
 | 
						|
			default: () => ({
 | 
						|
				mainColor: '#000000',
 | 
						|
				accentColor: '#000000',
 | 
						|
				borderColor: '#e0e0e0',
 | 
						|
				hoverColor: '#00000017',
 | 
						|
				reversedColor: '#ffffff',
 | 
						|
			}),
 | 
						|
			required: false,
 | 
						|
		},
 | 
						|
		localization: {
 | 
						|
			type: String,
 | 
						|
			required: false,
 | 
						|
		},
 | 
						|
		modelValue: {
 | 
						|
			type: Date,
 | 
						|
			required: false,
 | 
						|
		},
 | 
						|
		availableRange: {
 | 
						|
			type: Array as unknown as PropType<SingleDatePickerPropsAvailableDates>,
 | 
						|
			required: false,
 | 
						|
		}
 | 
						|
	})
 | 
						|
 | 
						|
	const emit = defineEmits(['update:modelValue', 'close'])
 | 
						|
	const selectMonth = ref(false)
 | 
						|
	const uniqueId = generateUniqueId()
 | 
						|
	const currentMonth = ref(new Date().getMonth())
 | 
						|
	const currentYear = ref(new Date().getFullYear())
 | 
						|
	const l10nDays = ref<string[]>([])
 | 
						|
	const dates = ref<Date[]>([])
 | 
						|
	const hasCloseListener = getCurrentInstance()?.vnode?.props?.onClose !== undefined
 | 
						|
	const availableRangeStart = ref<Date | null>(null)
 | 
						|
	const availableRangeEnd = ref<Date | null>(null)
 | 
						|
 | 
						|
	const { colorScheme, localization } = toRefs(props)
 | 
						|
 | 
						|
	watch(colorScheme, newVal => {
 | 
						|
		applyColor(uniqueId, newVal)
 | 
						|
	})
 | 
						|
 | 
						|
	watch([currentMonth, currentYear], () => {
 | 
						|
		dates.value = getCalendarDates(currentMonth.value, currentYear.value)
 | 
						|
	})
 | 
						|
 | 
						|
	watch([props.availableRange], () => {
 | 
						|
		calculateAvailableRange()
 | 
						|
	})
 | 
						|
 | 
						|
	onMounted(() => {
 | 
						|
		applyColor(uniqueId, colorScheme.value)
 | 
						|
		l10nDays.value = getL10Weekday(localization?.value || navigator.languages[0])
 | 
						|
		
 | 
						|
		if (props.modelValue) {
 | 
						|
			currentMonth.value = props.modelValue.getMonth()
 | 
						|
			currentYear.value = props.modelValue.getFullYear()
 | 
						|
		} else {
 | 
						|
			currentMonth.value = new Date().getMonth()
 | 
						|
			currentYear.value = new Date().getFullYear()
 | 
						|
		}
 | 
						|
		dates.value = getCalendarDates(currentMonth.value, currentYear.value)
 | 
						|
 | 
						|
		calculateAvailableRange()
 | 
						|
	})
 | 
						|
 | 
						|
	function goToLastMonth() {
 | 
						|
		if (currentMonth.value === 0) {
 | 
						|
			currentMonth.value = 11
 | 
						|
			currentYear.value -= 1
 | 
						|
		} else {
 | 
						|
			currentMonth.value -= 1
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	function goToNextMonth() {
 | 
						|
		if (currentMonth.value === 11) {
 | 
						|
			currentMonth.value = 0
 | 
						|
			currentYear.value += 1
 | 
						|
		} else {
 | 
						|
			currentMonth.value += 1
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	function notAvailable(date: Date): boolean {
 | 
						|
		return currentMonth.value !== date.getMonth() || (availableRangeStart.value !== null && date < availableRangeStart.value) || (availableRangeEnd.value !== null && date > availableRangeEnd.value)
 | 
						|
	}
 | 
						|
 | 
						|
	function selectDate(date: Date) {
 | 
						|
		emit('update:modelValue', date)
 | 
						|
	}
 | 
						|
 | 
						|
	function changeYear(event: Event) {
 | 
						|
		const target = event.target as HTMLInputElement
 | 
						|
		const year = parseInt(target.value)
 | 
						|
		if (year) currentYear.value = year
 | 
						|
	}
 | 
						|
 | 
						|
	function adjustYear() {
 | 
						|
		if (currentYear.value < 100) currentYear.value = parseInt(`20${currentYear.value}`)
 | 
						|
	}
 | 
						|
 | 
						|
	function monthNotAvailable() {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
 | 
						|
	function closePanel() {
 | 
						|
		emit('close')
 | 
						|
	}
 | 
						|
 | 
						|
	function calculateAvailableRange() {
 | 
						|
		if (!props.availableRange) {
 | 
						|
			availableRangeStart.value = null
 | 
						|
			availableRangeEnd.value = null
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		if (props.availableRange.length !== 2) {
 | 
						|
			console.warn('Invalid availableRange: The length of the array should be 2. The parameter will be ignored.')
 | 
						|
			availableRangeStart.value = null
 | 
						|
			availableRangeEnd.value = null
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		const [start, end] = props.availableRange
 | 
						|
 | 
						|
		if (start && end) {
 | 
						|
			if (start > end) {
 | 
						|
				availableRangeStart.value = end
 | 
						|
				availableRangeEnd.value = start
 | 
						|
			} else {
 | 
						|
				availableRangeStart.value = start
 | 
						|
				availableRangeEnd.value = end
 | 
						|
			}
 | 
						|
		} else if (start && !end) {
 | 
						|
			availableRangeStart.value = start
 | 
						|
			availableRangeEnd.value = null
 | 
						|
		} else if (!start && end) {
 | 
						|
			availableRangeStart.value = null
 | 
						|
			availableRangeEnd.value = end
 | 
						|
		} else {
 | 
						|
			availableRangeStart.value = null
 | 
						|
			availableRangeEnd.value = null
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
</script>
 | 
						|
 | 
						|
<template>
 | 
						|
	<div :id="`__datenel-${uniqueId}`" class='datenel-component'>
 | 
						|
		<div role="dialog" aria-label="Date selection panel, you are now at month and year quick-select" v-if="selectMonth">
 | 
						|
			<div class='__datenel_header'>
 | 
						|
				<button class='__datenel_stepper' @click="() => {
 | 
						|
					if (currentYear <= 100) return
 | 
						|
					currentYear -= 1
 | 
						|
				}" :aria-label="`Go to last year, ${currentYear - 1}, you are now at year ${currentYear}`"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M10.8284 12.0007L15.7782 16.9504L14.364 18.3646L8 12.0007L14.364 5.63672L15.7782 7.05093L10.8284 12.0007Z"></path></svg></button>
 | 
						|
				<input class='__datenel_indicator'
 | 
						|
					v-model="currentYear"
 | 
						|
					@change="changeYear"
 | 
						|
					@blur="adjustYear"
 | 
						|
					aria-label="Year input, type a year to go to that year"
 | 
						|
				/>
 | 
						|
				<button class='__datenel_stepper' @click="() => {
 | 
						|
					if (currentYear >= 9999) return
 | 
						|
					currentYear += 1
 | 
						|
				}" :aria-label="`Go to next year, ${currentYear + 1}, you are now at year ${currentYear}`"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M13.1717 12.0007L8.22192 7.05093L9.63614 5.63672L16.0001 12.0007L9.63614 18.3646L8.22192 16.9504L13.1717 12.0007Z"></path></svg></button>
 | 
						|
			</div>
 | 
						|
 | 
						|
			<div class='__datenel_body'>
 | 
						|
				<div class='__datenel_month-selector-body'>
 | 
						|
					<button
 | 
						|
						v-for="(_, index) in 12"
 | 
						|
						:class="`__datenel_item ${monthNotAvailable() && '__datenel_not-available'}`"
 | 
						|
						key={index}
 | 
						|
						@click="() => {
 | 
						|
							currentMonth = index
 | 
						|
							selectMonth = false
 | 
						|
						}"
 | 
						|
						:aria-label="`Go to ${new Date(currentYear, index).toLocaleString(localization, { month: 'long' })} of the year ${currentYear}`"
 | 
						|
						:disabled="monthNotAvailable()"
 | 
						|
						:aria-hidden="monthNotAvailable()"
 | 
						|
					>
 | 
						|
						{{new Date(currentYear, index).toLocaleString(localization, { month: 'long' })}}
 | 
						|
					</button>
 | 
						|
				</div>
 | 
						|
			</div>
 | 
						|
		</div>
 | 
						|
 | 
						|
		<div role="dialog" aria-label="Date selection panel" v-if="!selectMonth">
 | 
						|
			<div class='__datenel_header'>
 | 
						|
				<button class='__datenel_stepper' @click="goToLastMonth()" :aria-label="`Go to last month, ${new Date(currentYear, currentMonth - 1).toLocaleString('default', { month: 'long', year: 'numeric' })}`"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M10.8284 12.0007L15.7782 16.9504L14.364 18.3646L8 12.0007L14.364 5.63672L15.7782 7.05093L10.8284 12.0007Z"></path></svg></button>
 | 
						|
				<button class='__datenel_indicator' @click="selectMonth = true" :aria-label="`You are now at ${new Date(currentYear, currentMonth).toLocaleString('default', { month: 'long', year: 'numeric' })}. Click here to quick-select month or year.`">
 | 
						|
					{{  new Date(currentYear, currentMonth).toLocaleString('default', { month: 'long', year: 'numeric' }) }}
 | 
						|
				</button>
 | 
						|
				<button class='__datenel_stepper' @click="goToNextMonth()" :aria-label="`Go to next month, ${new Date(currentYear, currentMonth + 1).toLocaleString('default', { month: 'long', year: 'numeric' })}`"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M13.1717 12.0007L8.22192 7.05093L9.63614 5.63672L16.0001 12.0007L9.63614 18.3646L8.22192 16.9504L13.1717 12.0007Z"></path></svg></button>
 | 
						|
			</div>
 | 
						|
 | 
						|
			<div class='__datenel_body'>
 | 
						|
				<div class='__datenel_calendar-view-body __datenel_grid' aria-live="polite">
 | 
						|
					<div class='__datenel_item __datenel_day-indicator' v-for="(day, index) in l10nDays" :key="index">{{ day }}</div>
 | 
						|
					<!--  -->
 | 
						|
					<button
 | 
						|
						v-for="date in dates"
 | 
						|
						:class="`__datenel_item __datenel_date ${notAvailable(date) && '__datenel_not-available'} ${modelValue?.toDateString() === date.toDateString() && '__datenel_active'}`"
 | 
						|
						:key="date.toISOString()"
 | 
						|
						@click="selectDate(date)"
 | 
						|
						:aria-label="`${date.toLocaleString(localization, { dateStyle: 'full' })}${date.toDateString() === new Date().toDateString() ? ', this is today' : ''}, click to select this date`"
 | 
						|
						:tabIndex="currentMonth !== date.getMonth() ? -1 : 0"
 | 
						|
						:aria-hidden="notAvailable(date)"
 | 
						|
						:disabled="notAvailable(date)"
 | 
						|
					>
 | 
						|
							{{date.getDate()}}
 | 
						|
							<svg v-if="date.toDateString() === new Date().toDateString()" xmlns="http://www.w3.org/2000/svg" class='__datenel_today-indicator' viewBox="0 0 24 24" fill="currentColor"><path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z"></path></svg>
 | 
						|
						</button>
 | 
						|
				</div>
 | 
						|
			</div>
 | 
						|
		</div>
 | 
						|
 | 
						|
		<button class='__datenel_sr-only' @click="closePanel" v-if="hasCloseListener">Close the panel</button>
 | 
						|
	</div>
 | 
						|
</template>
 | 
						|
 | 
						|
<style lang="scss" scoped>
 | 
						|
@use '../style.scss' as *;
 | 
						|
</style> |