@johly/bits-ui 2.18.10 → 2.18.12
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.
|
@@ -15,15 +15,30 @@ export const dateFieldAttrs = createBitsAttrs({
|
|
|
15
15
|
component: "date-field",
|
|
16
16
|
parts: ["input", "label", "segment"],
|
|
17
17
|
});
|
|
18
|
+
/**
|
|
19
|
+
* Whether the year segment holds a year that is still being typed. The segment
|
|
20
|
+
* is zero-padded, so "0202" is what typing "202" towards "2024" looks like.
|
|
21
|
+
*/
|
|
22
|
+
function isPartialYear(year) {
|
|
23
|
+
return year !== null && Number.parseInt(year) < 1000;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Days in the typed month. Until the year is fully typed it could still become
|
|
27
|
+
* a leap year, so a leap year is assumed.
|
|
28
|
+
*/
|
|
29
|
+
function getDaysInSegmentMonth(dateRef, year, month) {
|
|
30
|
+
const typedYear = year && !isPartialYear(year) ? Number.parseInt(year) : 2000;
|
|
31
|
+
return getDaysInMonth(dateRef.set({ year: typedYear, month: Number.parseInt(month) }));
|
|
32
|
+
}
|
|
18
33
|
const SEGMENT_CONFIGS = {
|
|
19
34
|
day: {
|
|
20
35
|
min: 1,
|
|
21
36
|
max: (root) => {
|
|
22
|
-
const
|
|
37
|
+
const { year, month } = root.segmentValues;
|
|
38
|
+
if (!month)
|
|
39
|
+
return 31;
|
|
23
40
|
const placeholder = root.value.current ?? root.placeholder.current;
|
|
24
|
-
return
|
|
25
|
-
? getDaysInMonth(placeholder.set({ month: Number.parseInt(segmentMonthValue) }))
|
|
26
|
-
: getDaysInMonth(placeholder);
|
|
41
|
+
return getDaysInSegmentMonth(placeholder, year, month);
|
|
27
42
|
},
|
|
28
43
|
cycle: 1,
|
|
29
44
|
padZero: true,
|
|
@@ -265,6 +280,19 @@ export class DateFieldRootState {
|
|
|
265
280
|
this.states.minute.updating = null;
|
|
266
281
|
this.states.dayPeriod.updating = null;
|
|
267
282
|
}
|
|
283
|
+
/** Once the year is fully typed, clamp a day it doesn't have (29 February). */
|
|
284
|
+
#clampDayToYear(segments, year, dateRef) {
|
|
285
|
+
if (year === null || isPartialYear(year))
|
|
286
|
+
return;
|
|
287
|
+
if (segments.month === null || segments.day === null)
|
|
288
|
+
return;
|
|
289
|
+
const daysInMonth = getDaysInSegmentMonth(dateRef, year, segments.month);
|
|
290
|
+
if (Number.parseInt(segments.day) <= daysInMonth)
|
|
291
|
+
return;
|
|
292
|
+
segments.day = `${daysInMonth}`;
|
|
293
|
+
if (this.states.day.updating)
|
|
294
|
+
this.states.day.updating = segments.day;
|
|
295
|
+
}
|
|
268
296
|
setValue(value) {
|
|
269
297
|
this.value.current = value;
|
|
270
298
|
}
|
|
@@ -439,8 +467,7 @@ export class DateFieldRootState {
|
|
|
439
467
|
const next = castCb(pVal);
|
|
440
468
|
this.states.month.updating = next;
|
|
441
469
|
if (next !== null && prev.day !== null) {
|
|
442
|
-
const
|
|
443
|
-
const daysInMonth = getDaysInMonth(toDate(date));
|
|
470
|
+
const daysInMonth = getDaysInSegmentMonth(dateRef, prev.year, next);
|
|
444
471
|
const prevDay = Number.parseInt(prev.day);
|
|
445
472
|
if (prevDay > daysInMonth) {
|
|
446
473
|
prev.day = `${daysInMonth}`;
|
|
@@ -491,6 +518,7 @@ export class DateFieldRootState {
|
|
|
491
518
|
else if (part === "year") {
|
|
492
519
|
const next = castCb(pVal);
|
|
493
520
|
this.states.year.updating = next;
|
|
521
|
+
this.#clampDayToYear(prev, next, dateRef);
|
|
494
522
|
newSegmentValues = { ...prev, [part]: next };
|
|
495
523
|
}
|
|
496
524
|
else if (part === "day") {
|
|
@@ -509,8 +537,7 @@ export class DateFieldRootState {
|
|
|
509
537
|
const next = castCb(pVal);
|
|
510
538
|
if (part === "month" && next !== null && prev.day !== null) {
|
|
511
539
|
this.states.month.updating = next;
|
|
512
|
-
const
|
|
513
|
-
const daysInMonth = getDaysInMonth(toDate(date));
|
|
540
|
+
const daysInMonth = getDaysInSegmentMonth(dateRef, prev.year, next);
|
|
514
541
|
if (Number.parseInt(prev.day) > daysInMonth) {
|
|
515
542
|
prev.day = `${daysInMonth}`;
|
|
516
543
|
}
|
|
@@ -519,6 +546,7 @@ export class DateFieldRootState {
|
|
|
519
546
|
else if (part === "year") {
|
|
520
547
|
const next = castCb(pVal);
|
|
521
548
|
this.states.year.updating = next;
|
|
549
|
+
this.#clampDayToYear(prev, next, dateRef);
|
|
522
550
|
newSegmentValues = { ...prev, [part]: next };
|
|
523
551
|
}
|
|
524
552
|
else if (part === "day") {
|
|
@@ -531,6 +559,12 @@ export class DateFieldRootState {
|
|
|
531
559
|
}
|
|
532
560
|
}
|
|
533
561
|
this.segmentValues = newSegmentValues;
|
|
562
|
+
// Committing a year mid-typing would clamp the day to it, turning
|
|
563
|
+
// 29 February into the 28th before "2024" is complete, so the value
|
|
564
|
+
// is left alone until the year is.
|
|
565
|
+
if (part === "year" && "year" in newSegmentValues && isPartialYear(newSegmentValues.year)) {
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
534
568
|
if (areAllSegmentsFilled(newSegmentValues, this.#fieldNode)) {
|
|
535
569
|
this.setValue(getValueFromSegments({
|
|
536
570
|
segmentObj: newSegmentValues,
|
|
@@ -91,12 +91,13 @@ function createSelectableMenuInternal({ column, actions, filter, }) {
|
|
|
91
91
|
label: option.label,
|
|
92
92
|
icon: option.icon,
|
|
93
93
|
count: counts?.get(option.value) ?? option.count ?? 0,
|
|
94
|
+
keywords: option.keywords,
|
|
94
95
|
};
|
|
95
96
|
return {
|
|
96
97
|
kind: "checkbox-item",
|
|
97
98
|
id: option.value,
|
|
98
99
|
label: option.label,
|
|
99
|
-
keywords: [option.value, option.label],
|
|
100
|
+
keywords: [option.value, option.label, ...(option.keywords ?? [])],
|
|
100
101
|
checked: selected,
|
|
101
102
|
onCheckedChange: (checked) => {
|
|
102
103
|
if (checked) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { styleToString } from "svelte-toolbelt";
|
|
2
2
|
import { getPlaceholder } from "../placeholders.js";
|
|
3
|
-
import { hasTime, isZonedDateTime } from "../utils.js";
|
|
3
|
+
import { getDaysInMonth, hasTime, isZonedDateTime } from "../utils.js";
|
|
4
4
|
import { ALL_SEGMENT_PARTS, DATE_SEGMENT_PARTS, EDITABLE_SEGMENT_PARTS, EDITABLE_TIME_SEGMENT_PARTS, } from "./parts.js";
|
|
5
5
|
import { getSegments } from "./segments.js";
|
|
6
6
|
import { isBrowser, isNull, isNumberString } from "../../is.js";
|
|
@@ -25,6 +25,25 @@ export function initializeSegmentValues(granularity) {
|
|
|
25
25
|
});
|
|
26
26
|
return Object.fromEntries(initialParts);
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* The date to format a segment value with. A day can be typed before its month,
|
|
30
|
+
* so it may not exist in `dateRef`'s month; setting it there would constrain it
|
|
31
|
+
* (31 would read as 30), so it is shown in the first month long enough instead.
|
|
32
|
+
*/
|
|
33
|
+
function getPartDate(dateRef, part, value) {
|
|
34
|
+
if (part !== "day")
|
|
35
|
+
return dateRef.set({ [part]: value });
|
|
36
|
+
const day = Number.parseInt(value);
|
|
37
|
+
if (day <= getDaysInMonth(dateRef))
|
|
38
|
+
return dateRef.set({ day });
|
|
39
|
+
const monthsInYear = dateRef.calendar.getMonthsInYear(dateRef);
|
|
40
|
+
for (let month = 1; month <= monthsInYear; month++) {
|
|
41
|
+
const candidate = dateRef.set({ month, day: 1 });
|
|
42
|
+
if (day <= getDaysInMonth(candidate))
|
|
43
|
+
return candidate.set({ day });
|
|
44
|
+
}
|
|
45
|
+
return dateRef.set({ day });
|
|
46
|
+
}
|
|
28
47
|
function createContentObj(props) {
|
|
29
48
|
const { segmentValues, formatter, locale, dateRef } = props;
|
|
30
49
|
const content = Object.keys(segmentValues).reduce((obj, part) => {
|
|
@@ -53,7 +72,7 @@ function createContentObj(props) {
|
|
|
53
72
|
return "0";
|
|
54
73
|
}
|
|
55
74
|
else if (!isNull(value) && !isNull(intValue)) {
|
|
56
|
-
const formatted = formatter.part(dateRef
|
|
75
|
+
const formatted = formatter.part(getPartDate(dateRef, part, value), part, {
|
|
57
76
|
hourCycle: props.hourCycle === 24 ? "h23" : undefined,
|
|
58
77
|
});
|
|
59
78
|
/**
|
|
@@ -116,7 +135,7 @@ function createContentObj(props) {
|
|
|
116
135
|
return "0";
|
|
117
136
|
}
|
|
118
137
|
else if (!isNull(value)) {
|
|
119
|
-
const formatted = formatter.part(dateRef
|
|
138
|
+
const formatted = formatter.part(getPartDate(dateRef, part, value), part);
|
|
120
139
|
if (part === "year") {
|
|
121
140
|
return `${value}`;
|
|
122
141
|
}
|
|
@@ -238,9 +257,23 @@ function getUsedSegments(fieldNode) {
|
|
|
238
257
|
});
|
|
239
258
|
return usedSegments;
|
|
240
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* The order the segments are applied in when building a date from them.
|
|
262
|
+
*
|
|
263
|
+
* Setting a part on a `DateValue` constrains the result immediately, so the day
|
|
264
|
+
* has to be applied after the year and month it belongs to. Applying it in the
|
|
265
|
+
* order the segments are displayed clamps the day to the number of days in the
|
|
266
|
+
* `dateRef`'s month in day-first locales (e.g. `31/08` entered in `en-GB` with a
|
|
267
|
+
* `dateRef` in November would resolve to the 30th).
|
|
268
|
+
*/
|
|
269
|
+
const SEGMENT_APPLY_ORDER = ["year", "month", "day", "hour", "minute", "second", "dayPeriod"];
|
|
270
|
+
function getSegmentApplyOrder(part) {
|
|
271
|
+
const index = SEGMENT_APPLY_ORDER.indexOf(part);
|
|
272
|
+
return index === -1 ? SEGMENT_APPLY_ORDER.length : index;
|
|
273
|
+
}
|
|
241
274
|
export function getValueFromSegments(props) {
|
|
242
275
|
const { segmentObj, fieldNode, dateRef } = props;
|
|
243
|
-
const usedSegments = getUsedSegments(fieldNode);
|
|
276
|
+
const usedSegments = getUsedSegments(fieldNode).sort((a, b) => getSegmentApplyOrder(a) - getSegmentApplyOrder(b));
|
|
244
277
|
let date = dateRef;
|
|
245
278
|
for (const part of usedSegments) {
|
|
246
279
|
if ("hour" in segmentObj) {
|