@cahyo-dimas/freeday 3.8.0 → 3.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +126 -0
- package/COMPONENTS.md +112 -29
- package/README.id.md +4 -4
- package/README.md +4 -3
- package/adapters/blazor/FdyDatepicker.razor.cs +15 -1
- package/adapters/blazor/FdyMonthpicker.razor +29 -0
- package/adapters/blazor/FdyMonthpicker.razor.cs +102 -0
- package/adapters/blazor/FdyTable.razor +6 -1
- package/adapters/blazor/FdyTable.razor.cs +33 -0
- package/adapters/core/month-grid.d.ts +116 -0
- package/adapters/core/month-grid.js +234 -0
- package/adapters/react/components/FdyDatepicker.tsx +78 -160
- package/adapters/react/components/FdyMonthpicker.tsx +400 -0
- package/adapters/react/components/FdyTable.tsx +38 -2
- package/adapters/react/components/FdyTableFilter.tsx +13 -1
- package/adapters/react/index.d.ts +5 -1
- package/adapters/react/index.js +1 -0
- package/adapters/react/usePopover.ts +54 -9
- package/adapters/vue/components/FdyDatepicker.vue +72 -159
- package/adapters/vue/components/FdyMonthpicker.vue +387 -0
- package/adapters/vue/components/FdyTable.vue +33 -1
- package/adapters/vue/components/FdyTableFilter.vue +13 -1
- package/adapters/vue/index.d.ts +16 -0
- package/adapters/vue/index.js +1 -0
- package/adapters/vue/usePopover.ts +53 -10
- package/dist/freeday-table.js +64 -5
- package/dist/freeday.bundle.css +31 -6
- package/dist/freeday.css +31 -6
- package/dist/freeday.js +64 -5
- package/docs/agent-onboarding.md +3 -3
- package/docs/getting-started.md +6 -6
- package/docs/integrations.md +22 -4
- package/package.json +2 -2
- package/src/components/app-shell.css +12 -4
- package/src/components/table.css +19 -2
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, useId, type ComputedRef, type Ref } from 'vue';
|
|
3
3
|
import { usePopover } from '../usePopover';
|
|
4
|
+
/* The month and year grids, which `FdyMonthpicker` reads from the same module. They were written
|
|
5
|
+
here and moved out when the month picker arrived: a second copy of the bounds rule is a second
|
|
6
|
+
place to fix it, and this release spent three items on exactly that failure in the panel-position
|
|
7
|
+
logic (#079). What the two components do differently with the grid stays in each of them, which
|
|
8
|
+
is one branch: whether choosing a month commits or walks down into its days. */
|
|
9
|
+
import {
|
|
10
|
+
YEARS_PER_PAGE,
|
|
11
|
+
addMonths,
|
|
12
|
+
monthCells as buildMonthCells,
|
|
13
|
+
monthDisabled,
|
|
14
|
+
monthFormatters,
|
|
15
|
+
monthKeyIntent,
|
|
16
|
+
pageLocale,
|
|
17
|
+
parseISOMonth,
|
|
18
|
+
startOfDay,
|
|
19
|
+
startOfMonth,
|
|
20
|
+
toISOMonth,
|
|
21
|
+
yearCells as buildYearCells,
|
|
22
|
+
yearKeyIntent,
|
|
23
|
+
yearPageStart,
|
|
24
|
+
yearRangeLabel,
|
|
25
|
+
type FdyMonthCell,
|
|
26
|
+
type FdyMonthFormatters,
|
|
27
|
+
type FdyYearCell,
|
|
28
|
+
} from '../../core/month-grid.js';
|
|
4
29
|
|
|
5
30
|
// A Vue-native binding over freeday's `.fdy-datepicker` / `.fdy-cal__*` classes
|
|
6
31
|
// (see src/components/datepicker.css). freeday ships the styled trigger+popover
|
|
@@ -33,7 +58,15 @@ interface DayCell {
|
|
|
33
58
|
const props = defineProps<{
|
|
34
59
|
/** ISO `YYYY-MM-DD`, or `YYYY-MM` when `mode` is `'month'`. `''`/`null` for empty. */
|
|
35
60
|
modelValue: string | null;
|
|
36
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* `'day'` (default) answers with a date. `'month'` opens on the month grid, stops there, and
|
|
63
|
+
* answers with `YYYY-MM`.
|
|
64
|
+
*
|
|
65
|
+
* @deprecated Use `FdyMonthpicker` for a month. This prop makes `modelValue`, `min` and `max`
|
|
66
|
+
* carry two different formats while a *different* prop decides which, so the type cannot state
|
|
67
|
+
* what to store in them. `FdyMonthpicker` has one format per prop and no mode to read. Still
|
|
68
|
+
* works, unchanged, and removed in 4.0.
|
|
69
|
+
*/
|
|
37
70
|
mode?: FdyDatepickerMode;
|
|
38
71
|
min?: string;
|
|
39
72
|
max?: string;
|
|
@@ -77,9 +110,6 @@ function pad(n: number): string {
|
|
|
77
110
|
function toISO(d: Date): string {
|
|
78
111
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
79
112
|
}
|
|
80
|
-
function toISOMonth(d: Date): string {
|
|
81
|
-
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}`;
|
|
82
|
-
}
|
|
83
113
|
function parseISO(s: string | null | undefined): Date | null {
|
|
84
114
|
if (s === null || s === undefined || s === '') return null;
|
|
85
115
|
const parts: string[] = s.split('-');
|
|
@@ -90,35 +120,6 @@ function parseISO(s: string | null | undefined): Date | null {
|
|
|
90
120
|
const d: Date = new Date(y, m - 1, day);
|
|
91
121
|
return Number.isNaN(d.getTime()) ? null : d;
|
|
92
122
|
}
|
|
93
|
-
/** Month mode reads BOTH shapes, and the dropped day is the point: a bound of `2026-03` lands on
|
|
94
|
-
* March 1st, which `isMonthDisabled` then compares a whole month against correctly at either end,
|
|
95
|
-
* so an app can pass the strings it already stores. A full date still works. */
|
|
96
|
-
function parseISOMonth(s: string | null | undefined): Date | null {
|
|
97
|
-
if (s === null || s === undefined || s === '') return null;
|
|
98
|
-
const parts: string[] = s.split('-');
|
|
99
|
-
if (parts.length < 2) return null;
|
|
100
|
-
const y: number = Number(parts[0]);
|
|
101
|
-
const m: number = Number(parts[1]);
|
|
102
|
-
const day: number = parts.length > 2 ? Number(parts[2]) : 1;
|
|
103
|
-
const d: Date = new Date(y, m - 1, day);
|
|
104
|
-
return Number.isNaN(d.getTime()) ? null : d;
|
|
105
|
-
}
|
|
106
|
-
/* The page's own `lang`, which is what COMPONENTS.md has promised all along and what the vanilla
|
|
107
|
-
enhancer has always done. Handing `undefined` to Intl instead falls back to the BROWSER's
|
|
108
|
-
language, so an Indonesian reader saw Indonesian month names inside a kit that is English on
|
|
109
|
-
every other path, and an Indonesian PAGE read by an English browser got English ones. That is
|
|
110
|
-
the symptom this month mode was asked for, so the fallback has to be the page. */
|
|
111
|
-
function pageLocale(): string {
|
|
112
|
-
if (typeof document === 'undefined') return 'en';
|
|
113
|
-
// `||`, not `??`: an empty `lang=""` is not null and Intl throws RangeError on it.
|
|
114
|
-
return document.documentElement.getAttribute('lang') || 'en';
|
|
115
|
-
}
|
|
116
|
-
function startOfDay(d: Date): Date {
|
|
117
|
-
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
118
|
-
}
|
|
119
|
-
function startOfMonth(d: Date): Date {
|
|
120
|
-
return new Date(d.getFullYear(), d.getMonth(), 1);
|
|
121
|
-
}
|
|
122
123
|
function sameDay(a: Date | null, b: Date | null): boolean {
|
|
123
124
|
return a !== null && b !== null && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
124
125
|
}
|
|
@@ -127,11 +128,6 @@ function addDays(d: Date, n: number): Date {
|
|
|
127
128
|
x.setDate(x.getDate() + n);
|
|
128
129
|
return x;
|
|
129
130
|
}
|
|
130
|
-
function addMonths(d: Date, n: number): Date {
|
|
131
|
-
const x: Date = new Date(d);
|
|
132
|
-
x.setMonth(x.getMonth() + n);
|
|
133
|
-
return x;
|
|
134
|
-
}
|
|
135
131
|
|
|
136
132
|
const baseId: string = useId();
|
|
137
133
|
const triggerId: ComputedRef<string> = computed((): string => props.id ?? `${baseId}-trigger`);
|
|
@@ -204,95 +200,30 @@ const level: Ref<CalendarLevel> = ref(startLevel());
|
|
|
204
200
|
const focusMonth: Ref<number> = ref(0);
|
|
205
201
|
const focusYear: Ref<number> = ref(0);
|
|
206
202
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
different spot every time you stepped. */
|
|
210
|
-
const YEARS_PER_PAGE = 12;
|
|
211
|
-
const yearPageStart = (year: number): number => Math.floor(year / YEARS_PER_PAGE) * YEARS_PER_PAGE;
|
|
212
|
-
|
|
213
|
-
const monthCellFmt: ComputedRef<Intl.DateTimeFormat> = computed(
|
|
214
|
-
(): Intl.DateTimeFormat => new Intl.DateTimeFormat(activeLocale.value, { month: 'short' }),
|
|
203
|
+
const formatters: ComputedRef<FdyMonthFormatters> = computed(
|
|
204
|
+
(): FdyMonthFormatters => monthFormatters(activeLocale.value),
|
|
215
205
|
);
|
|
216
|
-
const monthNameFmt: ComputedRef<Intl.DateTimeFormat> = computed(
|
|
217
|
-
(): Intl.DateTimeFormat => new Intl.DateTimeFormat(activeLocale.value, { month: 'long', year: 'numeric' }),
|
|
218
|
-
);
|
|
219
|
-
|
|
220
|
-
interface MonthCell {
|
|
221
|
-
index: number;
|
|
222
|
-
label: string;
|
|
223
|
-
ariaLabel: string;
|
|
224
|
-
selected: boolean;
|
|
225
|
-
today: boolean;
|
|
226
|
-
disabled: boolean;
|
|
227
|
-
focusable: boolean;
|
|
228
|
-
}
|
|
229
206
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
return false;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const monthCells: ComputedRef<MonthCell[]> = computed((): MonthCell[] => {
|
|
242
|
-
const year: number = view.value.getFullYear();
|
|
243
|
-
const today: Date = new Date();
|
|
244
|
-
const sel: Date | null = selectedDate.value;
|
|
245
|
-
return Array.from({ length: 12 }, (_unused: unknown, index: number): MonthCell => {
|
|
246
|
-
const cellDate: Date = new Date(year, index, 1);
|
|
247
|
-
return {
|
|
248
|
-
index,
|
|
249
|
-
label: monthCellFmt.value.format(cellDate),
|
|
250
|
-
ariaLabel: monthNameFmt.value.format(cellDate),
|
|
251
|
-
selected: sel !== null && sel.getFullYear() === year && sel.getMonth() === index,
|
|
252
|
-
today: today.getFullYear() === year && today.getMonth() === index,
|
|
253
|
-
disabled: isMonthDisabled(year, index),
|
|
254
|
-
focusable: index === focusMonth.value,
|
|
255
|
-
};
|
|
256
|
-
});
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
interface YearCell {
|
|
260
|
-
year: number;
|
|
261
|
-
selected: boolean;
|
|
262
|
-
today: boolean;
|
|
263
|
-
disabled: boolean;
|
|
264
|
-
focusable: boolean;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/** A year is unreachable only when EVERY month in it falls outside min/max, the same rule
|
|
268
|
-
* isMonthDisabled applies one level down, and for the same reason. */
|
|
269
|
-
function isYearDisabled(year: number): boolean {
|
|
270
|
-
if (minDate.value !== null && startOfDay(new Date(year, 11, 31)).getTime() < startOfDay(minDate.value).getTime()) return true;
|
|
271
|
-
if (maxDate.value !== null && startOfDay(new Date(year, 0, 1)).getTime() > startOfDay(maxDate.value).getTime()) return true;
|
|
272
|
-
return false;
|
|
273
|
-
}
|
|
207
|
+
const monthCells: ComputedRef<FdyMonthCell[]> = computed((): FdyMonthCell[] => buildMonthCells({
|
|
208
|
+
year: view.value.getFullYear(),
|
|
209
|
+
selected: selectedDate.value,
|
|
210
|
+
min: minDate.value,
|
|
211
|
+
max: maxDate.value,
|
|
212
|
+
focusMonth: focusMonth.value,
|
|
213
|
+
formatters: formatters.value,
|
|
214
|
+
}));
|
|
274
215
|
|
|
275
216
|
const yearPage: ComputedRef<number> = computed((): number => yearPageStart(focusYear.value));
|
|
276
217
|
|
|
277
|
-
const yearCells: ComputedRef<
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
year,
|
|
285
|
-
selected: sel !== null && sel.getFullYear() === year,
|
|
286
|
-
today: thisYear === year,
|
|
287
|
-
disabled: isYearDisabled(year),
|
|
288
|
-
focusable: year === focusYear.value,
|
|
289
|
-
};
|
|
290
|
-
});
|
|
291
|
-
});
|
|
218
|
+
const yearCells: ComputedRef<FdyYearCell[]> = computed((): FdyYearCell[] => buildYearCells({
|
|
219
|
+
pageStart: yearPage.value,
|
|
220
|
+
selected: selectedDate.value,
|
|
221
|
+
min: minDate.value,
|
|
222
|
+
max: maxDate.value,
|
|
223
|
+
focusYear: focusYear.value,
|
|
224
|
+
}));
|
|
292
225
|
|
|
293
|
-
const
|
|
294
|
-
(): string => `${yearPage.value} – ${yearPage.value + YEARS_PER_PAGE - 1}`,
|
|
295
|
-
);
|
|
226
|
+
const yearRangeText: ComputedRef<string> = computed((): string => yearRangeLabel(yearPage.value));
|
|
296
227
|
|
|
297
228
|
const monthFmt: ComputedRef<Intl.DateTimeFormat> = computed(
|
|
298
229
|
(): Intl.DateTimeFormat => new Intl.DateTimeFormat(activeLocale.value, { month: 'long', year: 'numeric' }),
|
|
@@ -312,7 +243,7 @@ const dowFmt: ComputedRef<Intl.DateTimeFormat> = computed(
|
|
|
312
243
|
browser's language, this names it in the page's. */
|
|
313
244
|
const displayValue: ComputedRef<string> = computed((): string => {
|
|
314
245
|
if (selectedDate.value === null) return displayPlaceholder.value;
|
|
315
|
-
const fmt: Intl.DateTimeFormat = isMonthMode.value ?
|
|
246
|
+
const fmt: Intl.DateTimeFormat = isMonthMode.value ? formatters.value.name : valueFmt.value;
|
|
316
247
|
return fmt.format(selectedDate.value);
|
|
317
248
|
});
|
|
318
249
|
|
|
@@ -385,7 +316,7 @@ function chooseMonth(index: number): void {
|
|
|
385
316
|
|
|
386
317
|
function pickMonth(index: number): void {
|
|
387
318
|
const year: number = view.value.getFullYear();
|
|
388
|
-
if (
|
|
319
|
+
if (monthDisabled(year, index, minDate.value, maxDate.value)) return;
|
|
389
320
|
const first: Date = new Date(year, index, 1);
|
|
390
321
|
focusMonth.value = index;
|
|
391
322
|
focusDate.value = first;
|
|
@@ -434,47 +365,29 @@ function moveYearFocus(year: number): void {
|
|
|
434
365
|
}
|
|
435
366
|
|
|
436
367
|
function onYearKeydown(e: KeyboardEvent): void {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
case 'End': moveYearFocus(yearPageStart(focusYear.value) + YEARS_PER_PAGE - 1); break;
|
|
445
|
-
case 'PageUp': moveYearFocus(focusYear.value - YEARS_PER_PAGE); break;
|
|
446
|
-
case 'PageDown': moveYearFocus(focusYear.value + YEARS_PER_PAGE); break;
|
|
447
|
-
case 'Enter':
|
|
448
|
-
case ' ': openYear(focusYear.value); break;
|
|
449
|
-
case 'Escape': closePanel(true); break;
|
|
450
|
-
default: handled = false;
|
|
451
|
-
}
|
|
452
|
-
if (handled) { e.preventDefault(); e.stopPropagation(); }
|
|
368
|
+
const intent = yearKeyIntent(e.key, focusYear.value);
|
|
369
|
+
if (intent.action === 'none') return;
|
|
370
|
+
e.preventDefault();
|
|
371
|
+
e.stopPropagation();
|
|
372
|
+
if (intent.action === 'focus') moveYearFocus(intent.year);
|
|
373
|
+
else if (intent.action === 'choose') openYear(intent.year);
|
|
374
|
+
else closePanel(true);
|
|
453
375
|
}
|
|
454
376
|
|
|
377
|
+
/** `index` is already 0-11: the wrap lives in `monthKeyIntent`, so it is applied in one place. */
|
|
455
378
|
function moveMonthFocus(index: number, yearDelta: number): void {
|
|
456
379
|
if (yearDelta !== 0) view.value = addMonths(view.value, yearDelta * 12);
|
|
457
|
-
focusMonth.value =
|
|
380
|
+
focusMonth.value = index;
|
|
458
381
|
void nextTick((): void => focusMonthCell());
|
|
459
382
|
}
|
|
460
383
|
|
|
461
384
|
function onMonthKeydown(e: KeyboardEvent): void {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
case 'Home': moveMonthFocus(0, 0); break;
|
|
469
|
-
case 'End': moveMonthFocus(11, 0); break;
|
|
470
|
-
case 'PageUp': moveMonthFocus(focusMonth.value, -1); break;
|
|
471
|
-
case 'PageDown': moveMonthFocus(focusMonth.value, 1); break;
|
|
472
|
-
case 'Enter':
|
|
473
|
-
case ' ': chooseMonth(focusMonth.value); break;
|
|
474
|
-
case 'Escape': closePanel(true); break;
|
|
475
|
-
default: handled = false;
|
|
476
|
-
}
|
|
477
|
-
if (handled) e.preventDefault();
|
|
385
|
+
const intent = monthKeyIntent(e.key, focusMonth.value);
|
|
386
|
+
if (intent.action === 'none') return;
|
|
387
|
+
e.preventDefault();
|
|
388
|
+
if (intent.action === 'focus') moveMonthFocus(intent.index, intent.yearDelta);
|
|
389
|
+
else if (intent.action === 'choose') chooseMonth(intent.index);
|
|
390
|
+
else closePanel(true);
|
|
478
391
|
}
|
|
479
392
|
|
|
480
393
|
function pick(d: Date): void {
|
|
@@ -651,7 +564,7 @@ onBeforeUnmount((): void => {
|
|
|
651
564
|
:aria-label="level === 'years' ? prevYearsLabelText : level === 'months' ? prevYearLabelText : prevMonthLabelText"
|
|
652
565
|
@click="
|
|
653
566
|
level === 'years'
|
|
654
|
-
? moveYearFocus(focusYear -
|
|
567
|
+
? moveYearFocus(focusYear - YEARS_PER_PAGE)
|
|
655
568
|
: level === 'months'
|
|
656
569
|
? moveMonthFocus(focusMonth, -1)
|
|
657
570
|
: (view = addMonths(view, -1))
|
|
@@ -663,14 +576,14 @@ onBeforeUnmount((): void => {
|
|
|
663
576
|
class="fdy-cal__title"
|
|
664
577
|
:aria-label="level === 'years' ? undefined : level === 'months' ? chooseYearLabelText : chooseMonthLabelText"
|
|
665
578
|
@click="level === 'months' ? openYearGrid() : openMonthGrid()"
|
|
666
|
-
>{{ level === 'years' ?
|
|
579
|
+
>{{ level === 'years' ? yearRangeText : level === 'months' ? String(view.getFullYear()) : monthFmt.format(view) }}</button>
|
|
667
580
|
<button
|
|
668
581
|
type="button"
|
|
669
582
|
class="fdy-cal__nav"
|
|
670
583
|
:aria-label="level === 'years' ? nextYearsLabelText : level === 'months' ? nextYearLabelText : nextMonthLabelText"
|
|
671
584
|
@click="
|
|
672
585
|
level === 'years'
|
|
673
|
-
? moveYearFocus(focusYear +
|
|
586
|
+
? moveYearFocus(focusYear + YEARS_PER_PAGE)
|
|
674
587
|
: level === 'months'
|
|
675
588
|
? moveMonthFocus(focusMonth, 1)
|
|
676
589
|
: (view = addMonths(view, 1))
|