Set min/max year based on platform limitations 1901...2100.
This commit is contained in:
		| @@ -31,6 +31,8 @@ import { | |||||||
|   toLocalString, |   toLocalString, | ||||||
|   mondayIndex, |   mondayIndex, | ||||||
|   DEFAULT_TZ, |   DEFAULT_TZ, | ||||||
|  |   MIN_YEAR, | ||||||
|  |   MAX_YEAR, | ||||||
| } from '@/utils/date' | } from '@/utils/date' | ||||||
| import { addDays } from 'date-fns' | import { addDays } from 'date-fns' | ||||||
| import WeekRow from './WeekRow.vue' | import WeekRow from './WeekRow.vue' | ||||||
| @@ -43,8 +45,6 @@ const minVirtualWeek = ref(0) | |||||||
| const visibleWeeks = ref([]) | const visibleWeeks = ref([]) | ||||||
|  |  | ||||||
| const config = { | const config = { | ||||||
|   min_year: 1900, |  | ||||||
|   max_year: 2100, |  | ||||||
|   weekend: getLocaleWeekendDays(), |   weekend: getLocaleWeekendDays(), | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -116,7 +116,7 @@ const handleWheel = (e) => { | |||||||
|   const currentYear = calendarStore.viewYear |   const currentYear = calendarStore.viewYear | ||||||
|   const delta = Math.round(e.deltaY * (1 / 3)) |   const delta = Math.round(e.deltaY * (1 / 3)) | ||||||
|   if (!delta) return |   if (!delta) return | ||||||
|   const newYear = Math.max(config.min_year, Math.min(config.max_year, currentYear + delta)) |   const newYear = Math.max(MIN_YEAR, Math.min(MAX_YEAR, currentYear + delta)) | ||||||
|   if (newYear === currentYear) return |   if (newYear === currentYear) return | ||||||
|  |  | ||||||
|   const topDisplayIndex = Math.floor(viewportEl.value.scrollTop / rowHeight.value) |   const topDisplayIndex = Math.floor(viewportEl.value.scrollTop / rowHeight.value) | ||||||
| @@ -156,8 +156,8 @@ const goToTodayHandler = () => { | |||||||
| onMounted(() => { | onMounted(() => { | ||||||
|   rowHeight.value = computeRowHeight() |   rowHeight.value = computeRowHeight() | ||||||
|  |  | ||||||
|   const minYearDate = new Date(config.min_year, 0, 1) |   const minYearDate = new Date(MIN_YEAR, 0, 1) | ||||||
|   const maxYearLastDay = new Date(config.max_year, 11, 31) |   const maxYearLastDay = new Date(MAX_YEAR, 11, 31) | ||||||
|   const lastWeekMonday = addDays(maxYearLastDay, -mondayIndex(maxYearLastDay)) |   const lastWeekMonday = addDays(maxYearLastDay, -mondayIndex(maxYearLastDay)) | ||||||
|  |  | ||||||
|   minVirtualWeek.value = getWeekIndex(minYearDate) |   minVirtualWeek.value = getWeekIndex(minYearDate) | ||||||
|   | |||||||
| @@ -6,6 +6,8 @@ import { | |||||||
|   reorderByFirstDay, |   reorderByFirstDay, | ||||||
|   getISOWeek, |   getISOWeek, | ||||||
|   getISOWeekYear, |   getISOWeekYear, | ||||||
|  |   MIN_YEAR, | ||||||
|  |   MAX_YEAR, | ||||||
| } from '@/utils/date' | } from '@/utils/date' | ||||||
| import Numeric from '@/components/Numeric.vue' | import Numeric from '@/components/Numeric.vue' | ||||||
| import { addDays } from 'date-fns' | import { addDays } from 'date-fns' | ||||||
| @@ -49,7 +51,7 @@ function isoWeekMonday(isoYear, isoWeek) { | |||||||
|  |  | ||||||
| function changeYear(y) { | function changeYear(y) { | ||||||
|   if (y == null) return |   if (y == null) return | ||||||
|   y = Math.round(Math.max(calendarStore.minYear, Math.min(calendarStore.maxYear, y))) |   y = Math.round(Math.max(MIN_YEAR, Math.min(MAX_YEAR, y))) | ||||||
|   if (y === currentYear.value) return |   if (y === currentYear.value) return | ||||||
|   const vw = topVirtualWeek.value |   const vw = topVirtualWeek.value | ||||||
|   // Fraction within current row |   // Fraction within current row | ||||||
| @@ -94,8 +96,8 @@ const weekdayNames = computed(() => { | |||||||
|       <Numeric |       <Numeric | ||||||
|         :model-value="currentYear" |         :model-value="currentYear" | ||||||
|         @update:modelValue="changeYear" |         @update:modelValue="changeYear" | ||||||
|         :min="calendarStore.minYear" |   :min="MIN_YEAR" | ||||||
|         :max="calendarStore.maxYear" |   :max="MAX_YEAR" | ||||||
|         :step="1" |         :step="1" | ||||||
|         aria-label="Year" |         aria-label="Year" | ||||||
|         number-prefix="" |         number-prefix="" | ||||||
|   | |||||||
| @@ -17,6 +17,8 @@ import { | |||||||
|   getOccurrenceIndex, |   getOccurrenceIndex, | ||||||
|   getVirtualOccurrenceEndDate, |   getVirtualOccurrenceEndDate, | ||||||
|   getISOWeek, |   getISOWeek, | ||||||
|  |   MIN_YEAR, | ||||||
|  |   MAX_YEAR, | ||||||
| } from '@/utils/date' | } from '@/utils/date' | ||||||
| import { toLocalString, fromLocalString, DEFAULT_TZ } from '@/utils/date' | import { toLocalString, fromLocalString, DEFAULT_TZ } from '@/utils/date' | ||||||
| import { addDays, differenceInCalendarDays, differenceInWeeks } from 'date-fns' | import { addDays, differenceInCalendarDays, differenceInWeeks } from 'date-fns' | ||||||
| @@ -76,14 +78,14 @@ function registerTap(rawDate, type) { | |||||||
| } | } | ||||||
|  |  | ||||||
| const minVirtualWeek = computed(() => { | const minVirtualWeek = computed(() => { | ||||||
|   const date = new Date(calendarStore.minYear, 0, 1) |   const date = new Date(MIN_YEAR, 0, 1) | ||||||
|   const dayOffset = (date.getDay() - calendarStore.config.first_day + 7) % 7 |   const dayOffset = (date.getDay() - calendarStore.config.first_day + 7) % 7 | ||||||
|   const firstDayOfWeek = addDays(date, -dayOffset) |   const firstDayOfWeek = addDays(date, -dayOffset) | ||||||
|   return differenceInWeeks(firstDayOfWeek, baseDate.value) |   return differenceInWeeks(firstDayOfWeek, baseDate.value) | ||||||
| }) | }) | ||||||
|  |  | ||||||
| const maxVirtualWeek = computed(() => { | const maxVirtualWeek = computed(() => { | ||||||
|   const date = new Date(calendarStore.maxYear, 11, 31) |   const date = new Date(MAX_YEAR, 11, 31) | ||||||
|   const dayOffset = (date.getDay() - calendarStore.config.first_day + 7) % 7 |   const dayOffset = (date.getDay() - calendarStore.config.first_day + 7) % 7 | ||||||
|   const firstDayOfWeek = addDays(date, -dayOffset) |   const firstDayOfWeek = addDays(date, -dayOffset) | ||||||
|   return differenceInWeeks(firstDayOfWeek, baseDate.value) |   return differenceInWeeks(firstDayOfWeek, baseDate.value) | ||||||
| @@ -276,7 +278,7 @@ function createWeek(virtualWeek) { | |||||||
|  |  | ||||||
|   let monthLabel = null |   let monthLabel = null | ||||||
|   if (hasFirst && monthToLabel !== null) { |   if (hasFirst && monthToLabel !== null) { | ||||||
|     if (labelYear && labelYear <= calendarStore.config.max_year) { |     if (labelYear && labelYear <= MAX_YEAR) { | ||||||
|       let weeksSpan = 0 |       let weeksSpan = 0 | ||||||
|       const d = addDays(cur, -1) |       const d = addDays(cur, -1) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ | |||||||
|   <div |   <div | ||||||
|     ref="rootEl" |     ref="rootEl" | ||||||
|     class="mini-stepper drag-mode" |     class="mini-stepper drag-mode" | ||||||
|     :class="[extraClass, { dragging }]" |     :class="[extraClass, { dragging, 'pointer-locked': pointerLocked }]" | ||||||
|     :aria-label="ariaLabel" |     :aria-label="ariaLabel" | ||||||
|     role="spinbutton" |     role="spinbutton" | ||||||
|     :aria-valuemin="minValue" |     :aria-valuemin="minValue" | ||||||
| @@ -19,7 +19,7 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script setup> | <script setup> | ||||||
| import { computed, ref } from 'vue' | import { computed, ref, onBeforeUnmount } from 'vue' | ||||||
|  |  | ||||||
| const model = defineModel({ type: Number, default: 0 }) | const model = defineModel({ type: Number, default: 0 }) | ||||||
|  |  | ||||||
| @@ -102,46 +102,91 @@ const dragging = ref(false) | |||||||
| const rootEl = ref(null) | const rootEl = ref(null) | ||||||
| let startX = 0 | let startX = 0 | ||||||
| let startY = 0 | let startY = 0 | ||||||
| let startVal = 0 | let accumX = 0 // accumulated horizontal movement since last applied step (works for both locked & unlocked) | ||||||
|  | let lastClientX = 0 // previous clientX when not pointer locked | ||||||
|  | const pointerLocked = ref(false) | ||||||
|  |  | ||||||
|  | function updatePointerLocked() { | ||||||
|  |   pointerLocked.value = | ||||||
|  |     typeof document !== 'undefined' && document.pointerLockElement === rootEl.value | ||||||
|  |   // Reset baseline if lock just engaged | ||||||
|  |   if (pointerLocked.value) { | ||||||
|  |     accumX = 0 | ||||||
|  |     startX = 0 // not used while locked | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function addPointerLockListeners() { | ||||||
|  |   if (typeof document === 'undefined') return | ||||||
|  |   document.addEventListener('pointerlockchange', updatePointerLocked) | ||||||
|  |   document.addEventListener('pointerlockerror', updatePointerLocked) | ||||||
|  | } | ||||||
|  | function removePointerLockListeners() { | ||||||
|  |   if (typeof document === 'undefined') return | ||||||
|  |   document.removeEventListener('pointerlockchange', updatePointerLocked) | ||||||
|  |   document.removeEventListener('pointerlockerror', updatePointerLocked) | ||||||
|  | } | ||||||
|  |  | ||||||
| function onPointerDown(e) { | function onPointerDown(e) { | ||||||
|   e.preventDefault() |   e.preventDefault() | ||||||
|   startX = e.clientX |   startX = e.clientX | ||||||
|   startY = e.clientY |   startY = e.clientY | ||||||
|   startVal = current.value |   lastClientX = e.clientX | ||||||
|  |   accumX = 0 | ||||||
|   dragging.value = true |   dragging.value = true | ||||||
|   try { |   try { | ||||||
|     e.currentTarget.setPointerCapture(e.pointerId) |     e.currentTarget.setPointerCapture?.(e.pointerId) | ||||||
|   } catch {} |   } catch {} | ||||||
|   rootEl.value?.addEventListener('pointermove', onPointerMove) |   if (e.pointerType === 'mouse' && rootEl.value?.requestPointerLock) { | ||||||
|   rootEl.value?.addEventListener('pointerup', onPointerUp, { once: true }) |     addPointerLockListeners() | ||||||
|   rootEl.value?.addEventListener('pointercancel', onPointerCancel, { once: true }) |     try { | ||||||
|  |       rootEl.value.requestPointerLock() | ||||||
|  |     } catch {} | ||||||
|  |   } | ||||||
|  |   document.addEventListener('pointermove', onPointerMove) | ||||||
|  |   document.addEventListener('pointerup', onPointerUp, { once: true }) | ||||||
|  |   document.addEventListener('pointercancel', onPointerCancel, { once: true }) | ||||||
| } | } | ||||||
| function onPointerMove(e) { | function onPointerMove(e) { | ||||||
|   if (!dragging.value) return |   if (!dragging.value) return | ||||||
|   // Prevent page scroll on touch while dragging |  | ||||||
|   if (e.pointerType === 'touch') e.preventDefault() |   if (e.pointerType === 'touch') e.preventDefault() | ||||||
|   const primary = e.clientX - startX // horizontal only |   let dx | ||||||
|   const steps = Math.trunc(primary / props.pixelsPerStep) |   if (pointerLocked.value) { | ||||||
|  |     dx = e.movementX || 0 | ||||||
|   // Find current value index in all valid values |  | ||||||
|   const currentIndex = allValidValues.value.indexOf(startVal) |  | ||||||
|   if (currentIndex === -1) return // shouldn't happen |  | ||||||
|  |  | ||||||
|   const newIndex = currentIndex + steps |  | ||||||
|   if (props.clamp) { |  | ||||||
|     const clampedIndex = Math.max(0, Math.min(newIndex, allValidValues.value.length - 1)) |  | ||||||
|     const next = allValidValues.value[clampedIndex] |  | ||||||
|     if (next !== current.value) current.value = next |  | ||||||
|   } else { |   } else { | ||||||
|     if (newIndex >= 0 && newIndex < allValidValues.value.length) { |     dx = e.clientX - lastClientX | ||||||
|       const next = allValidValues.value[newIndex] |     lastClientX = e.clientX | ||||||
|  |   } | ||||||
|  |   if (!dx) return | ||||||
|  |   accumX += dx | ||||||
|  |   const stepSize = props.pixelsPerStep || 1 | ||||||
|  |   let steps = Math.trunc(accumX / stepSize) | ||||||
|  |   if (steps === 0) return | ||||||
|  |   // Apply steps relative to current value index each time (dynamic baseline) and keep leftover pixels | ||||||
|  |   const applySteps = (count) => { | ||||||
|  |     const currentIndex = allValidValues.value.indexOf(current.value) | ||||||
|  |     if (currentIndex === -1) return | ||||||
|  |     let targetIndex = currentIndex + count | ||||||
|  |     if (props.clamp) { | ||||||
|  |       targetIndex = Math.max(0, Math.min(targetIndex, allValidValues.value.length - 1)) | ||||||
|  |     } | ||||||
|  |     if (targetIndex >= 0 && targetIndex < allValidValues.value.length) { | ||||||
|  |       const next = allValidValues.value[targetIndex] | ||||||
|       if (next !== current.value) current.value = next |       if (next !== current.value) current.value = next | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |   applySteps(steps) | ||||||
|  |   // Remove consumed distance (works even if clamped; leftover ensures responsiveness keeps consistent feel) | ||||||
|  |   accumX -= steps * stepSize | ||||||
| } | } | ||||||
| function endDragListeners() { | function endDragListeners() { | ||||||
|   rootEl.value?.removeEventListener('pointermove', onPointerMove) |   document.removeEventListener('pointermove', onPointerMove) | ||||||
|  |   if (pointerLocked.value && document.exitPointerLock) { | ||||||
|  |     try { | ||||||
|  |       document.exitPointerLock() | ||||||
|  |     } catch {} | ||||||
|  |   } | ||||||
|  |   removePointerLockListeners() | ||||||
| } | } | ||||||
| function onPointerUp() { | function onPointerUp() { | ||||||
|   dragging.value = false |   dragging.value = false | ||||||
| @@ -267,4 +312,7 @@ function onWheel(e) { | |||||||
| .mini-stepper.drag-mode.dragging { | .mini-stepper.drag-mode.dragging { | ||||||
|   cursor: grabbing; |   cursor: grabbing; | ||||||
| } | } | ||||||
|  | .mini-stepper.drag-mode.pointer-locked.dragging { | ||||||
|  |   cursor: none; /* hide cursor for infinite drag */ | ||||||
|  | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -10,9 +10,6 @@ import { | |||||||
| import { differenceInCalendarDays, addDays } from 'date-fns' | import { differenceInCalendarDays, addDays } from 'date-fns' | ||||||
| import { initializeHolidays, getAvailableCountries, getAvailableStates } from '@/utils/holidays' | import { initializeHolidays, getAvailableCountries, getAvailableStates } from '@/utils/holidays' | ||||||
|  |  | ||||||
| const MIN_YEAR = 1900 |  | ||||||
| const MAX_YEAR = 2100 |  | ||||||
|  |  | ||||||
| export const useCalendarStore = defineStore('calendar', { | export const useCalendarStore = defineStore('calendar', { | ||||||
|   state: () => ({ |   state: () => ({ | ||||||
|     today: toLocalString(new Date(), DEFAULT_TZ), |     today: toLocalString(new Date(), DEFAULT_TZ), | ||||||
| @@ -23,8 +20,6 @@ export const useCalendarStore = defineStore('calendar', { | |||||||
|     _holidaysInitialized: false, |     _holidaysInitialized: false, | ||||||
|     config: { |     config: { | ||||||
|       select_days: 14, |       select_days: 14, | ||||||
|       min_year: MIN_YEAR, |  | ||||||
|       max_year: MAX_YEAR, |  | ||||||
|       first_day: 1, |       first_day: 1, | ||||||
|       holidays: { |       holidays: { | ||||||
|         enabled: true, |         enabled: true, | ||||||
| @@ -34,12 +29,6 @@ export const useCalendarStore = defineStore('calendar', { | |||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|   }), |   }), | ||||||
|  |  | ||||||
|   getters: { |  | ||||||
|     minYear: () => MIN_YEAR, |  | ||||||
|     maxYear: () => MAX_YEAR, |  | ||||||
|   }, |  | ||||||
|  |  | ||||||
|   actions: { |   actions: { | ||||||
|     _resolveCountry(code) { |     _resolveCountry(code) { | ||||||
|       if (!code || code !== 'auto') return code |       if (!code || code !== 'auto') return code | ||||||
|   | |||||||
| @@ -23,6 +23,9 @@ const monthAbbr = [ | |||||||
|   'nov', |   'nov', | ||||||
|   'dec', |   'dec', | ||||||
| ] | ] | ||||||
|  | // Calendar year bounds (used instead of config.min_year / config.max_year) | ||||||
|  | const MIN_YEAR = 1901 | ||||||
|  | const MAX_YEAR = 2100 | ||||||
|  |  | ||||||
| // Core helpers ------------------------------------------------------------ | // Core helpers ------------------------------------------------------------ | ||||||
| /** | /** | ||||||
| @@ -320,6 +323,8 @@ function formatTodayString(date) { | |||||||
| export { | export { | ||||||
|   // constants |   // constants | ||||||
|   monthAbbr, |   monthAbbr, | ||||||
|  |   MIN_YEAR, | ||||||
|  |   MAX_YEAR, | ||||||
|   DEFAULT_TZ, |   DEFAULT_TZ, | ||||||
|   // core tz helpers |   // core tz helpers | ||||||
|   makeTZDate, |   makeTZDate, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Leo Vasanko
					Leo Vasanko