@vaadin/date-picker 25.3.0-alpha2 → 25.3.0-dev.3a3c2d7d2a
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/custom-elements.json +192 -1
- package/package.json +13 -13
- package/src/styles/vaadin-date-picker-overlay-content-base-styles.js +12 -0
- package/src/vaadin-date-picker-mixin.d.ts +28 -0
- package/src/vaadin-date-picker-mixin.js +142 -2
- package/src/vaadin-date-picker-overlay-content-mixin.js +130 -3
- package/src/vaadin-date-picker-overlay-content.js +4 -1
- package/src/vaadin-date-picker.d.ts +1 -1
- package/src/vaadin-date-picker.js +3 -0
- package/src/vaadin-disabled-dates-controller.d.ts +66 -0
- package/src/vaadin-disabled-dates-controller.js +241 -0
- package/src/vaadin-month-calendar-mixin.js +44 -2
- package/web-types.json +13 -2
- package/web-types.lit.json +9 -2
|
@@ -106,6 +106,37 @@ export const DatePickerOverlayContentMixin = (superClass) =>
|
|
|
106
106
|
type: Function,
|
|
107
107
|
},
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* A batch function that is consulted for a range of dates the calendar is about to render
|
|
111
|
+
* and returns, or resolves with, an array of `DatePickerDate` objects to disable.
|
|
112
|
+
*
|
|
113
|
+
* @type {function(DatePickerDateRange): Array<DatePickerDate> | Promise<Array<DatePickerDate>> | undefined}
|
|
114
|
+
*/
|
|
115
|
+
disabledDatesProvider: {
|
|
116
|
+
type: Function,
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Reflected while data is being loaded, so the overlay can show a loading spinner.
|
|
121
|
+
* Currently set while the disabled dates provider is resolving.
|
|
122
|
+
*/
|
|
123
|
+
loading: {
|
|
124
|
+
type: Boolean,
|
|
125
|
+
value: false,
|
|
126
|
+
reflectToAttribute: true,
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Bumped whenever the disabled dates controller's cache or loading state changes, to
|
|
131
|
+
* re-run `__updateCalendars` and push the new state to the rendered months.
|
|
132
|
+
* @private
|
|
133
|
+
*/
|
|
134
|
+
_disabledDatesVersion: {
|
|
135
|
+
type: Number,
|
|
136
|
+
value: 0,
|
|
137
|
+
attribute: false,
|
|
138
|
+
},
|
|
139
|
+
|
|
109
140
|
enteredDate: {
|
|
110
141
|
type: Date,
|
|
111
142
|
sync: true,
|
|
@@ -138,9 +169,10 @@ export const DatePickerOverlayContentMixin = (superClass) =>
|
|
|
138
169
|
|
|
139
170
|
static get observers() {
|
|
140
171
|
return [
|
|
141
|
-
'__updateCalendars(calendars, i18n, minDate, maxDate, selectedDate, focusedDate, showWeekNumbers, _ignoreTaps, _theme, isDateDisabled, enteredDate)',
|
|
172
|
+
'__updateCalendars(calendars, i18n, minDate, maxDate, selectedDate, focusedDate, showWeekNumbers, _ignoreTaps, _theme, isDateDisabled, _disabledDatesVersion, enteredDate)',
|
|
173
|
+
'__loadingChanged(loading)',
|
|
142
174
|
'__updateCancelButton(_cancelButton, i18n)',
|
|
143
|
-
'__updateTodayButton(_todayButton, i18n, minDate, maxDate, isDateDisabled)',
|
|
175
|
+
'__updateTodayButton(_todayButton, i18n, minDate, maxDate, isDateDisabled, _disabledDatesVersion)',
|
|
144
176
|
'__updateYears(years, selectedDate, _theme)',
|
|
145
177
|
];
|
|
146
178
|
}
|
|
@@ -299,6 +331,17 @@ export const DatePickerOverlayContentMixin = (superClass) =>
|
|
|
299
331
|
}
|
|
300
332
|
}
|
|
301
333
|
|
|
334
|
+
/** @private */
|
|
335
|
+
__loadingChanged(loading) {
|
|
336
|
+
// Mark the calendar region as busy while data is being loaded so assistive technology knows
|
|
337
|
+
// the content is still updating.
|
|
338
|
+
if (loading) {
|
|
339
|
+
this.setAttribute('aria-busy', 'true');
|
|
340
|
+
} else {
|
|
341
|
+
this.removeAttribute('aria-busy');
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
302
345
|
// eslint-disable-next-line @typescript-eslint/max-params
|
|
303
346
|
__updateCalendars(
|
|
304
347
|
calendars,
|
|
@@ -311,6 +354,7 @@ export const DatePickerOverlayContentMixin = (superClass) =>
|
|
|
311
354
|
ignoreTaps,
|
|
312
355
|
theme,
|
|
313
356
|
isDateDisabled,
|
|
357
|
+
disabledDatesVersion,
|
|
314
358
|
enteredDate,
|
|
315
359
|
) {
|
|
316
360
|
if (calendars?.length) {
|
|
@@ -319,6 +363,8 @@ export const DatePickerOverlayContentMixin = (superClass) =>
|
|
|
319
363
|
calendar.minDate = minDate;
|
|
320
364
|
calendar.maxDate = maxDate;
|
|
321
365
|
calendar.isDateDisabled = isDateDisabled;
|
|
366
|
+
calendar.disabledDatesController = this._disabledDatesController;
|
|
367
|
+
calendar.__disabledDatesVersion = disabledDatesVersion;
|
|
322
368
|
calendar.focusedDate = focusedDate;
|
|
323
369
|
calendar.selectedDate = selectedDate;
|
|
324
370
|
calendar.showWeekNumbers = showWeekNumbers;
|
|
@@ -331,7 +377,46 @@ export const DatePickerOverlayContentMixin = (superClass) =>
|
|
|
331
377
|
calendar.removeAttribute('theme');
|
|
332
378
|
}
|
|
333
379
|
});
|
|
380
|
+
|
|
381
|
+
this.__ensureVisibleDisabledDatesLoaded();
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Asks the disabled dates controller to load the range of months currently rendered by the
|
|
387
|
+
* scroller. The controller expands the range with a prefetch buffer and skips months that are
|
|
388
|
+
* already loaded or in flight, so one request covers the whole visible range.
|
|
389
|
+
* @private
|
|
390
|
+
*/
|
|
391
|
+
__ensureVisibleDisabledDatesLoaded() {
|
|
392
|
+
const controller = this._disabledDatesController;
|
|
393
|
+
if (!controller?.provider || !this.calendars || this.calendars.length === 0) {
|
|
394
|
+
return;
|
|
334
395
|
}
|
|
396
|
+
const indexes = this.calendars
|
|
397
|
+
.map((calendar) => calendar.month)
|
|
398
|
+
.filter(Boolean)
|
|
399
|
+
.map((month) => month.getFullYear() * 12 + month.getMonth());
|
|
400
|
+
if (indexes.length === 0) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const min = Math.min(...indexes);
|
|
404
|
+
const max = Math.max(...indexes);
|
|
405
|
+
controller.ensureRangeLoaded(
|
|
406
|
+
new Date(Math.floor(min / 12), min % 12, 1),
|
|
407
|
+
new Date(Math.floor(max / 12), max % 12, 1),
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Debounced variant used on scroll, so a continuous scroll only triggers one load once it
|
|
413
|
+
* settles instead of a request per intermediate position.
|
|
414
|
+
* @private
|
|
415
|
+
*/
|
|
416
|
+
__scheduleVisibleDisabledDatesLoad() {
|
|
417
|
+
this._loadDisabledDatesDebouncer = Debouncer.debounce(this._loadDisabledDatesDebouncer, timeOut.after(200), () =>
|
|
418
|
+
this.__ensureVisibleDisabledDatesLoaded(),
|
|
419
|
+
);
|
|
335
420
|
}
|
|
336
421
|
|
|
337
422
|
/** @private */
|
|
@@ -357,6 +442,11 @@ export const DatePickerOverlayContentMixin = (superClass) =>
|
|
|
357
442
|
if (!this._dateAllowed(dateToSelect)) {
|
|
358
443
|
return false;
|
|
359
444
|
}
|
|
445
|
+
// Block dates disabled by the provider, or in a month whose provider result has not loaded
|
|
446
|
+
// yet, so they cannot be selected via keyboard or the today button either.
|
|
447
|
+
if (this._disabledDatesController?.isDateBlocked(dateToSelect)) {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
360
450
|
this.selectedDate = dateToSelect;
|
|
361
451
|
this.dispatchEvent(
|
|
362
452
|
new CustomEvent('date-selected', { detail: { date: dateToSelect }, bubbles: true, composed: true }),
|
|
@@ -452,12 +542,17 @@ export const DatePickerOverlayContentMixin = (superClass) =>
|
|
|
452
542
|
const monthPosition = this._monthScroller.position;
|
|
453
543
|
this._visibleMonthIndex = Math.floor(monthPosition);
|
|
454
544
|
this._yearScroller.position = (monthPosition + this._originDate.getMonth()) / 12;
|
|
545
|
+
// Called from every navigation path (month scroll, year click, reveal), so this is the
|
|
546
|
+
// single place to keep the disabled dates loaded for the newly visible months. Debounced
|
|
547
|
+
// so a continuous scroll only triggers one load once it settles.
|
|
548
|
+
this.__scheduleVisibleDisabledDatesLoad();
|
|
455
549
|
}
|
|
456
550
|
|
|
457
551
|
/** @private */
|
|
458
552
|
_repositionMonthScroller() {
|
|
459
553
|
this._monthScroller.position = this._yearScroller.position * 12 - this._originDate.getMonth();
|
|
460
554
|
this._visibleMonthIndex = Math.floor(this._monthScroller.position);
|
|
555
|
+
this.__scheduleVisibleDisabledDatesLoad();
|
|
461
556
|
}
|
|
462
557
|
|
|
463
558
|
/** @private */
|
|
@@ -831,6 +926,33 @@ export const DatePickerOverlayContentMixin = (superClass) =>
|
|
|
831
926
|
this.focusDate(getClosestDate(focus, [this.minDate, this.maxDate]));
|
|
832
927
|
}
|
|
833
928
|
|
|
929
|
+
/**
|
|
930
|
+
* Returns the nearest date to the given one that can actually be selected: inside min/max, not
|
|
931
|
+
* disabled by `isDateDisabled`, and not disabled by a resolved `disabledDatesProvider`. Scans a
|
|
932
|
+
* year in both directions and returns `undefined` if none is found, so the caller can leave the
|
|
933
|
+
* focus untouched.
|
|
934
|
+
* @private
|
|
935
|
+
*/
|
|
936
|
+
__closestSelectableDate(date) {
|
|
937
|
+
const controller = this._disabledDatesController;
|
|
938
|
+
const isSelectable = (candidate) =>
|
|
939
|
+
this._dateAllowed(candidate) && !(controller?.provider && controller.isDateDisabled(candidate));
|
|
940
|
+
|
|
941
|
+
if (isSelectable(date)) {
|
|
942
|
+
return date;
|
|
943
|
+
}
|
|
944
|
+
for (let offset = 1; offset <= 366; offset++) {
|
|
945
|
+
for (const direction of [1, -1]) {
|
|
946
|
+
const candidate = new Date(date);
|
|
947
|
+
candidate.setDate(candidate.getDate() + offset * direction);
|
|
948
|
+
if (isSelectable(candidate)) {
|
|
949
|
+
return candidate;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return undefined;
|
|
954
|
+
}
|
|
955
|
+
|
|
834
956
|
/** @private */
|
|
835
957
|
_focusAllowedDate(dateToFocus, diff, keepMonth) {
|
|
836
958
|
// For this check we do consider the isDateDisabled function because disabled dates are allowed to be focused, just not outside min/max
|
|
@@ -918,7 +1040,12 @@ export const DatePickerOverlayContentMixin = (superClass) =>
|
|
|
918
1040
|
|
|
919
1041
|
/** @private */
|
|
920
1042
|
_isTodayAllowed(min, max, isDateDisabled) {
|
|
921
|
-
|
|
1043
|
+
const today = this._getTodayMidnight();
|
|
1044
|
+
if (!this._dateAllowed(today, min, max, isDateDisabled)) {
|
|
1045
|
+
return false;
|
|
1046
|
+
}
|
|
1047
|
+
// Today is not selectable while it is disabled by the provider or its month is still loading.
|
|
1048
|
+
return !this._disabledDatesController?.isDateBlocked(today);
|
|
922
1049
|
}
|
|
923
1050
|
|
|
924
1051
|
/** @private */
|
|
@@ -12,6 +12,7 @@ import { html, LitElement } from 'lit';
|
|
|
12
12
|
import { defineCustomElement } from '@vaadin/component-base/src/define.js';
|
|
13
13
|
import { DirMixin } from '@vaadin/component-base/src/dir-mixin.js';
|
|
14
14
|
import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
|
|
15
|
+
import { loaderStyles } from '@vaadin/component-base/src/styles/loader-styles.js';
|
|
15
16
|
import { LumoInjectionMixin } from '@vaadin/vaadin-themable-mixin/lumo-injection-mixin.js';
|
|
16
17
|
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
|
|
17
18
|
import { overlayContentStyles } from './styles/vaadin-date-picker-overlay-content-base-styles.js';
|
|
@@ -30,7 +31,7 @@ class DatePickerOverlayContent extends DatePickerOverlayContentMixin(
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
static get styles() {
|
|
33
|
-
return overlayContentStyles;
|
|
34
|
+
return [loaderStyles, overlayContentStyles];
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
static get lumoInjector() {
|
|
@@ -43,6 +44,8 @@ class DatePickerOverlayContent extends DatePickerOverlayContentMixin(
|
|
|
43
44
|
<slot name="months"></slot>
|
|
44
45
|
<slot name="years"></slot>
|
|
45
46
|
|
|
47
|
+
<div part="loader" aria-hidden="true"></div>
|
|
48
|
+
|
|
46
49
|
<div role="toolbar" part="toolbar">
|
|
47
50
|
<slot name="today-button"></slot>
|
|
48
51
|
<div
|
|
@@ -7,7 +7,7 @@ import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
|
|
|
7
7
|
import { InputControlMixin } from '@vaadin/field-base/src/input-control-mixin.js';
|
|
8
8
|
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
|
|
9
9
|
import { DatePickerMixin } from './vaadin-date-picker-mixin.js';
|
|
10
|
-
export { DatePickerDate, DatePickerI18n } from './vaadin-date-picker-mixin.js';
|
|
10
|
+
export { DatePickerDate, DatePickerDateRange, DatePickerI18n } from './vaadin-date-picker-mixin.js';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Fired when the user commits a value change.
|
|
@@ -92,6 +92,7 @@ import { DatePickerMixin } from './vaadin-date-picker-mixin.js';
|
|
|
92
92
|
* ----------------------|--------------------
|
|
93
93
|
* `years-toggle-button` | Fullscreen mode years scroller toggle
|
|
94
94
|
* `toolbar` | Toolbar with slotted buttons
|
|
95
|
+
* `loader` | Loading spinner shown while data is being loaded, for example while the disabled dates provider resolves
|
|
95
96
|
*
|
|
96
97
|
* The following state attributes are available on the `<vaadin-date-picker-overlay-content>` element:
|
|
97
98
|
*
|
|
@@ -100,6 +101,7 @@ import { DatePickerMixin } from './vaadin-date-picker-mixin.js';
|
|
|
100
101
|
* `desktop` | Set when the overlay content is in desktop mode
|
|
101
102
|
* `fullscreen` | Set when the overlay content is in fullscreen mode
|
|
102
103
|
* `years-visible` | Set when the year scroller is visible in fullscreen mode
|
|
104
|
+
* `loading` | Set while data is being loaded, for example while the disabled dates provider resolves
|
|
103
105
|
*
|
|
104
106
|
* In order to style the month calendar, use `<vaadin-month-calendar>` shadow DOM parts:
|
|
105
107
|
*
|
|
@@ -112,6 +114,7 @@ import { DatePickerMixin } from './vaadin-date-picker-mixin.js';
|
|
|
112
114
|
* `week-number` | Week number element
|
|
113
115
|
* `date` | Date element
|
|
114
116
|
* `disabled` | Disabled date element
|
|
117
|
+
* `pending` | Date element in a month whose disabled dates provider result is still loading
|
|
115
118
|
* `focused` | Focused date element
|
|
116
119
|
* `selected` | Selected date element
|
|
117
120
|
* `today` | Date element corresponding to the current day
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2016 - 2026 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
|
7
|
+
import type { DatePickerDate, DatePickerDateRange } from './vaadin-date-picker-mixin.js';
|
|
8
|
+
|
|
9
|
+
export type DisabledDatesProvider = (range: DatePickerDateRange) => DatePickerDate[] | Promise<DatePickerDate[]>;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A reactive controller that resolves the dates disabled by the date-picker's
|
|
13
|
+
* `disabledDatesProvider`. It calls the provider once for a range of months,
|
|
14
|
+
* caches the resolved months so scrolling back and forth does not re-fetch,
|
|
15
|
+
* and prefetches a buffer of months around the requested range.
|
|
16
|
+
*/
|
|
17
|
+
export declare class DisabledDatesController implements ReactiveController {
|
|
18
|
+
constructor(host: ReactiveControllerHost, onChange: () => void);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The provider function, or `null` when none is set.
|
|
22
|
+
*/
|
|
23
|
+
provider: DisabledDatesProvider | null;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Whether any month range is currently being fetched.
|
|
27
|
+
*/
|
|
28
|
+
readonly loading: boolean;
|
|
29
|
+
|
|
30
|
+
hostDisconnected(): void;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Sets the provider function and clears the cache. Passing the same provider
|
|
34
|
+
* again is a no-op. Callers should keep a stable provider reference.
|
|
35
|
+
*/
|
|
36
|
+
setProvider(provider: DisabledDatesProvider | null | undefined): void;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Clears the cache and invalidates any in-flight requests.
|
|
40
|
+
*/
|
|
41
|
+
reset(): void;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Whether the month containing the given date has been fully resolved.
|
|
45
|
+
*/
|
|
46
|
+
isMonthLoaded(date: Date): boolean;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Whether the given date is disabled by the provider. Only returns `true` for
|
|
50
|
+
* dates in an already-resolved month.
|
|
51
|
+
*/
|
|
52
|
+
isDateDisabled(date: Date): boolean;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Whether the given date cannot be selected yet: it is disabled by the
|
|
56
|
+
* provider, or its month has not been resolved. Returns `false` when no
|
|
57
|
+
* provider is set.
|
|
58
|
+
*/
|
|
59
|
+
isDateBlocked(date: Date): boolean;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Ensures the provider has been consulted for the inclusive range between the
|
|
63
|
+
* given dates, expanded by a prefetch buffer of months on each side.
|
|
64
|
+
*/
|
|
65
|
+
ensureRangeLoaded(startDate: Date, endDate: Date): void;
|
|
66
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2016 - 2026 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import { extractDateParts } from './vaadin-date-picker-helper.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Number of months fetched before and after the requested range, so that
|
|
10
|
+
* scrolling a few months in either direction does not trigger a new request.
|
|
11
|
+
* Mirrors how the grid prefetches rows around the viewport.
|
|
12
|
+
*/
|
|
13
|
+
const PREFETCH_MONTHS = 6;
|
|
14
|
+
|
|
15
|
+
function monthKey(date) {
|
|
16
|
+
return `${date.getFullYear()}-${date.getMonth()}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function dateKey(date) {
|
|
20
|
+
return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function firstOfMonth(date) {
|
|
24
|
+
return new Date(date.getFullYear(), date.getMonth(), 1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function lastOfMonth(date) {
|
|
28
|
+
return new Date(date.getFullYear(), date.getMonth() + 1, 0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function addMonths(date, months) {
|
|
32
|
+
return new Date(date.getFullYear(), date.getMonth() + months, 1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A reactive controller that resolves the dates disabled by the date-picker's
|
|
37
|
+
* `disabledDatesProvider`. It calls the provider once for a range of months
|
|
38
|
+
* (never one date at a time), caches the resolved months so scrolling back and
|
|
39
|
+
* forth does not re-fetch them, and prefetches a buffer of months around the
|
|
40
|
+
* requested range. While any request is in flight, {@link #loading} is `true`
|
|
41
|
+
* so the overlay can show a spinner.
|
|
42
|
+
*
|
|
43
|
+
* The provider may return an array synchronously or a `Promise`, so results
|
|
44
|
+
* from a server (Flow) or a remote availability service can be awaited.
|
|
45
|
+
*/
|
|
46
|
+
export class DisabledDatesController {
|
|
47
|
+
constructor(host, onChange) {
|
|
48
|
+
this.host = host;
|
|
49
|
+
this.__onChange = onChange;
|
|
50
|
+
this.provider = null;
|
|
51
|
+
this.__disabledDates = new Set();
|
|
52
|
+
this.__loadedMonths = new Set();
|
|
53
|
+
this.__pendingMonths = new Set();
|
|
54
|
+
this.__requestId = 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
hostDisconnected() {
|
|
58
|
+
// Invalidate in-flight requests so their late results don't touch a
|
|
59
|
+
// detached host, and drop the pending (loading) state. The resolved cache
|
|
60
|
+
// is kept so reopening the overlay does not re-fetch.
|
|
61
|
+
this.__requestId += 1;
|
|
62
|
+
this.__pendingMonths = new Set();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Whether any month range is currently being fetched.
|
|
67
|
+
* @return {boolean}
|
|
68
|
+
*/
|
|
69
|
+
get loading() {
|
|
70
|
+
return this.__pendingMonths.size > 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Sets the provider function and clears the cache. Passing the same provider
|
|
75
|
+
* again is a no-op, so the cache survives unrelated re-renders — callers
|
|
76
|
+
* should therefore keep a stable provider reference rather than passing a new
|
|
77
|
+
* function on every update, which would otherwise reset the cache and
|
|
78
|
+
* re-fetch every visible range.
|
|
79
|
+
*/
|
|
80
|
+
setProvider(provider) {
|
|
81
|
+
if (this.provider === provider) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
this.provider = provider;
|
|
85
|
+
this.reset();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Clears the cache and invalidates any in-flight requests. */
|
|
89
|
+
reset() {
|
|
90
|
+
this.__disabledDates = new Set();
|
|
91
|
+
this.__loadedMonths = new Set();
|
|
92
|
+
this.__pendingMonths = new Set();
|
|
93
|
+
this.__requestId += 1;
|
|
94
|
+
this.__notify();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Whether the month containing the given date has been fully resolved.
|
|
99
|
+
* @param {Date} date
|
|
100
|
+
* @return {boolean}
|
|
101
|
+
*/
|
|
102
|
+
isMonthLoaded(date) {
|
|
103
|
+
return this.__loadedMonths.has(monthKey(date));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Whether the given date is disabled by the provider. Only returns `true` for
|
|
108
|
+
* dates in an already-resolved month.
|
|
109
|
+
* @param {Date} date
|
|
110
|
+
* @return {boolean}
|
|
111
|
+
*/
|
|
112
|
+
isDateDisabled(date) {
|
|
113
|
+
return !!date && this.__disabledDates.has(dateKey(date));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Whether the given date cannot be selected yet: it is disabled by the
|
|
118
|
+
* provider, or its month has not been resolved so its state is still unknown.
|
|
119
|
+
* Returns `false` when no provider is set. Used to block selection and to
|
|
120
|
+
* render dates as non-selectable while their month is loading.
|
|
121
|
+
* @param {Date} date
|
|
122
|
+
* @return {boolean}
|
|
123
|
+
*/
|
|
124
|
+
isDateBlocked(date) {
|
|
125
|
+
return !!this.provider && !!date && (this.isDateDisabled(date) || !this.isMonthLoaded(date));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Ensures the provider has been consulted for the inclusive range between the
|
|
130
|
+
* given dates, expanded by a prefetch buffer of months on each side. Months
|
|
131
|
+
* that are already loaded or in flight are skipped, and consecutive missing
|
|
132
|
+
* months are grouped into a single provider call.
|
|
133
|
+
*
|
|
134
|
+
* @param {Date} startDate
|
|
135
|
+
* @param {Date} endDate
|
|
136
|
+
*/
|
|
137
|
+
ensureRangeLoaded(startDate, endDate) {
|
|
138
|
+
if (!this.provider || !startDate || !endDate) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const first = addMonths(firstOfMonth(startDate), -PREFETCH_MONTHS);
|
|
143
|
+
const last = addMonths(firstOfMonth(endDate), PREFETCH_MONTHS);
|
|
144
|
+
|
|
145
|
+
const monthsToLoad = [];
|
|
146
|
+
for (let month = first; month <= last; month = addMonths(month, 1)) {
|
|
147
|
+
const key = monthKey(month);
|
|
148
|
+
if (!this.__loadedMonths.has(key) && !this.__pendingMonths.has(key)) {
|
|
149
|
+
monthsToLoad.push(new Date(month));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (monthsToLoad.length === 0) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
this.__groupConsecutiveMonths(monthsToLoad).forEach((group) => this.__loadGroup(group));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** @private */
|
|
161
|
+
__groupConsecutiveMonths(months) {
|
|
162
|
+
const groups = [];
|
|
163
|
+
months.forEach((month) => {
|
|
164
|
+
const group = groups.at(-1);
|
|
165
|
+
const previous = group?.at(-1);
|
|
166
|
+
if (previous && monthKey(addMonths(previous, 1)) === monthKey(month)) {
|
|
167
|
+
group.push(month);
|
|
168
|
+
} else {
|
|
169
|
+
groups.push([month]);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
return groups;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** @private */
|
|
176
|
+
__loadGroup(months) {
|
|
177
|
+
const requestId = this.__requestId;
|
|
178
|
+
months.forEach((month) => this.__pendingMonths.add(monthKey(month)));
|
|
179
|
+
this.__notify();
|
|
180
|
+
|
|
181
|
+
const range = {
|
|
182
|
+
start: extractDateParts(firstOfMonth(months[0])),
|
|
183
|
+
end: extractDateParts(lastOfMonth(months[months.length - 1])),
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
let result;
|
|
187
|
+
try {
|
|
188
|
+
result = this.provider(range);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
// Treat a throwing provider the same as a rejected promise: clear the
|
|
191
|
+
// pending state and disable nothing, rather than letting the error
|
|
192
|
+
// propagate out of the scroll/render path that triggered the load.
|
|
193
|
+
console.error(error);
|
|
194
|
+
this.__resolveGroup(months, [], requestId);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (result && typeof result.then === 'function') {
|
|
199
|
+
result.then(
|
|
200
|
+
(dates) => this.__resolveGroup(months, dates, requestId),
|
|
201
|
+
(error) => {
|
|
202
|
+
console.error(error);
|
|
203
|
+
this.__resolveGroup(months, [], requestId);
|
|
204
|
+
},
|
|
205
|
+
);
|
|
206
|
+
} else {
|
|
207
|
+
this.__resolveGroup(months, result, requestId);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** @private */
|
|
212
|
+
__resolveGroup(months, dates, requestId) {
|
|
213
|
+
// Ignore results from before the last reset (e.g. provider changed).
|
|
214
|
+
if (requestId !== this.__requestId) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (Array.isArray(dates)) {
|
|
219
|
+
dates.forEach((date) => {
|
|
220
|
+
if (date) {
|
|
221
|
+
this.__disabledDates.add(`${date.year}-${date.month}-${date.day}`);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
months.forEach((month) => {
|
|
227
|
+
const key = monthKey(month);
|
|
228
|
+
this.__pendingMonths.delete(key);
|
|
229
|
+
this.__loadedMonths.add(key);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
this.__notify();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** @private */
|
|
236
|
+
__notify() {
|
|
237
|
+
if (this.__onChange) {
|
|
238
|
+
this.__onChange();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -87,6 +87,28 @@ export const MonthCalendarMixin = (superClass) =>
|
|
|
87
87
|
value: () => false,
|
|
88
88
|
},
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* The shared controller that resolves and caches the dates disabled by the date-picker's
|
|
92
|
+
* `disabledDatesProvider`. Set by the overlay content; used to look up the disabled state
|
|
93
|
+
* of this month's dates.
|
|
94
|
+
* @type {object | undefined}
|
|
95
|
+
* @private
|
|
96
|
+
*/
|
|
97
|
+
disabledDatesController: {
|
|
98
|
+
type: Object,
|
|
99
|
+
attribute: false,
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Bumped by the overlay content whenever the controller's cache or loading state changes,
|
|
104
|
+
* to trigger a re-render of this month.
|
|
105
|
+
* @private
|
|
106
|
+
*/
|
|
107
|
+
__disabledDatesVersion: {
|
|
108
|
+
type: Number,
|
|
109
|
+
attribute: false,
|
|
110
|
+
},
|
|
111
|
+
|
|
90
112
|
enteredDate: {
|
|
91
113
|
type: Date,
|
|
92
114
|
},
|
|
@@ -100,7 +122,8 @@ export const MonthCalendarMixin = (superClass) =>
|
|
|
100
122
|
/** @protected */
|
|
101
123
|
_days: {
|
|
102
124
|
type: Array,
|
|
103
|
-
computed:
|
|
125
|
+
computed:
|
|
126
|
+
'__computeDays(month, i18n, minDate, maxDate, isDateDisabled, disabledDatesController, __disabledDatesVersion)',
|
|
104
127
|
},
|
|
105
128
|
|
|
106
129
|
/** @protected */
|
|
@@ -345,6 +368,10 @@ export const MonthCalendarMixin = (superClass) =>
|
|
|
345
368
|
result.push('disabled');
|
|
346
369
|
}
|
|
347
370
|
|
|
371
|
+
if (date && this.__isMonthLoading()) {
|
|
372
|
+
result.push('pending');
|
|
373
|
+
}
|
|
374
|
+
|
|
348
375
|
if (dateEquals(date, focusedDate) && (hasFocus || dateEquals(date, enteredDate))) {
|
|
349
376
|
result.push('focused');
|
|
350
377
|
}
|
|
@@ -378,9 +405,24 @@ export const MonthCalendarMixin = (superClass) =>
|
|
|
378
405
|
return String(this.__isDaySelected(date, selectedDate));
|
|
379
406
|
}
|
|
380
407
|
|
|
408
|
+
/**
|
|
409
|
+
* Whether the provider result for the currently displayed month is still pending. The overlay
|
|
410
|
+
* drives loading for the whole visible range; here we only reflect the controller's state.
|
|
411
|
+
* @private
|
|
412
|
+
*/
|
|
413
|
+
__isMonthLoading() {
|
|
414
|
+
const controller = this.disabledDatesController;
|
|
415
|
+
return !!controller?.provider && this.month !== undefined && !controller.isMonthLoaded(this.month);
|
|
416
|
+
}
|
|
417
|
+
|
|
381
418
|
/** @private */
|
|
382
419
|
__isDayDisabled(date, minDate, maxDate, isDateDisabled) {
|
|
383
|
-
|
|
420
|
+
if (!dateAllowed(date, minDate, maxDate, isDateDisabled)) {
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
// A date is blocked while its month is pending (whole month non-selectable) or once the
|
|
424
|
+
// resolved provider result marks it disabled.
|
|
425
|
+
return !!this.disabledDatesController?.isDateBlocked(date);
|
|
384
426
|
}
|
|
385
427
|
|
|
386
428
|
/** @private */
|