feat: let the Jalali date picker jump to any month or year from its title

This commit is contained in:
2026-10-20 14:47:25 +03:30
parent 29398a445b
commit 9a86f63f48
+134 -5
View File
@@ -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` }"
>
<header v-if="mode !== 'time'" class="nav">
<a-button type="text" class="nav-btn" aria-label="ماه بعد" @click="goMonth(1)">
<a-button type="text" class="nav-btn" :aria-label="navLabels.forward" @click="navStep(1)">
<template #icon><RightOutlined /></template>
</a-button>
<strong>{{ monthTitle }}</strong>
<a-button type="text" class="nav-btn next" aria-label="ماه قبل" @click="goMonth(-1)">
<a-button
type="text"
class="nav-title"
:class="{ static: calView === 'years' }"
:aria-label="calView === 'days' ? 'انتخاب ماه و سال' : calView === 'months' ? 'انتخاب سال' : undefined"
@click="onTitleClick"
>
{{ navTitle }}
</a-button>
<a-button type="text" class="nav-btn next" :aria-label="navLabels.back" @click="navStep(-1)">
<template #icon><LeftOutlined /></template>
</a-button>
</header>
<template v-if="mode !== 'time'">
<div v-if="mode !== 'time' && calView === 'months'" class="pick-grid">
<a-button
v-for="m in MONTHS"
:key="`pm-${m}`"
type="text"
class="pick-cell"
:class="{
on: selectedJ?.jy === cursor.jy && selectedJ?.jm === m,
today: todayJ.jy === cursor.jy && todayJ.jm === m,
}"
@click="pickMonth(m)"
>
{{ jalaliMonthName(m) }}
</a-button>
</div>
<div v-else-if="mode !== 'time' && calView === 'years'" class="pick-grid">
<a-button
v-for="y in yearOptions"
:key="`py-${y}`"
type="text"
class="pick-cell"
:class="{ on: selectedJ?.jy === y, today: todayJ.jy === y }"
@click="pickYear(y)"
>
{{ toFaDigits(y) }}
</a-button>
</div>
<template v-if="mode !== 'time' && calView === 'days'">
<div class="week">
<span v-for="day in WEEKDAYS" :key="day">{{ day }}</span>
</div>
@@ -781,8 +876,42 @@ onBeforeUnmount(() => {
margin-bottom: 0.55rem;
}
.nav strong {
.nav-title {
font-size: 0.92rem;
font-weight: 700;
color: #0f172a;
}
.nav-title:not(.static):hover {
color: #0f766e;
}
.nav-title.static {
cursor: default;
}
.pick-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.35rem;
padding: 0.25rem 0;
}
.pick-cell {
height: 44px;
border-radius: var(--radius);
color: #0f172a;
font-size: 0.85rem;
}
.pick-cell.today {
box-shadow: inset 0 0 0 1px #99f6e4;
}
.pick-cell.on,
.pick-cell:hover {
background: #0d9488;
color: #fff;
}
.nav-btn {