From 9a86f63f482b43402d1fb6ad5042fb9e8bad8dd4 Mon Sep 17 00:00:00 2001 From: Mahdi Date: Tue, 20 Oct 2026 14:47:25 +0330 Subject: [PATCH] feat: let the Jalali date picker jump to any month or year from its title --- apps/web/src/components/JalaliDateInput.vue | 139 +++++++++++++++++++- 1 file changed, 134 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/JalaliDateInput.vue b/apps/web/src/components/JalaliDateInput.vue index 7d51480..dab256f 100644 --- a/apps/web/src/components/JalaliDateInput.vue +++ b/apps/web/src/components/JalaliDateInput.vue @@ -59,6 +59,11 @@ const draftMinute = ref(0); const clockFace = ref<'hour' | 'minute'>('hour'); const isMobile = ref(false); const dragging = ref(false); +/** نمای تقویم: روزها ← کلیک عنوان → ماه‌ها ← کلیک عنوان → سال‌ها */ +const calView = ref<'days' | 'months' | 'years'>('days'); +const YEAR_PAGE = 12; +const yearPageStart = ref(todayJ.jy - 5); +const MONTHS = Array.from({ length: 12 }, (_, i) => i + 1); type Cell = { key: string; @@ -107,6 +112,26 @@ const monthTitle = computed( () => `${jalaliMonthName(cursor.value.jm)} ${toFaDigits(cursor.value.jy)}`, ); +const navTitle = computed(() => { + if (calView.value === 'months') return toFaDigits(cursor.value.jy); + if (calView.value === 'years') { + return `${toFaDigits(yearPageStart.value)} – ${toFaDigits(yearPageStart.value + YEAR_PAGE - 1)}`; + } + return monthTitle.value; +}); + +const navLabels = computed(() => { + if (calView.value === 'months') return { forward: 'سال بعد', back: 'سال قبل' }; + if (calView.value === 'years') return { forward: 'سال‌های بعد', back: 'سال‌های قبل' }; + return { forward: 'ماه بعد', back: 'ماه قبل' }; +}); + +const yearOptions = computed(() => + Array.from({ length: YEAR_PAGE }, (_, i) => yearPageStart.value + i), +); + +const selectedJ = computed(() => (parsed.value ? dateToJalali(parsed.value) : null)); + const hours = Array.from({ length: 24 }, (_, i) => i); const minuteMarks = Array.from({ length: 12 }, (_, i) => i * 5); const columnMinutes = Array.from({ length: 60 }, (_, i) => i); @@ -316,6 +341,7 @@ function onWindowChange() { async function show() { if (props.disabled) return; syncDraftFrom(parsed.value); + calView.value = 'days'; clockFace.value = 'hour'; syncViewport(); open.value = true; @@ -341,6 +367,38 @@ function goMonth(delta: number) { void nextTick(place); } +function navStep(delta: number) { + if (calView.value === 'months') { + cursor.value = { jy: cursor.value.jy + delta, jm: cursor.value.jm }; + } else if (calView.value === 'years') { + yearPageStart.value += delta * YEAR_PAGE; + } else { + goMonth(delta); + } +} + +function onTitleClick() { + if (calView.value === 'days') { + calView.value = 'months'; + } else if (calView.value === 'months') { + yearPageStart.value = cursor.value.jy - 5; + calView.value = 'years'; + } + void nextTick(place); +} + +function pickMonth(jm: number) { + cursor.value = { jy: cursor.value.jy, jm }; + calView.value = 'days'; + void nextTick(place); +} + +function pickYear(jy: number) { + cursor.value = { jy, jm: cursor.value.jm }; + calView.value = 'months'; + void nextTick(place); +} + function pickDay(cell: Cell) { if (!cell.inMonth) { cursor.value = { jy: cell.jy, jm: cell.jm }; @@ -483,16 +541,53 @@ onBeforeUnmount(() => { :style="{ top: `${top}px`, left: `${left}px` }" > -