@vaadin/date-picker 25.3.0-dev.1fa5a51482 → 25.3.0-rc1
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 +391 -79
- package/package.json +13 -13
- package/src/styles/vaadin-date-picker-overlay-content-base-styles.js +0 -3
- package/src/styles/vaadin-month-calendar-base-styles.js +3 -0
- package/src/vaadin-date-metadata-controller.d.ts +54 -35
- package/src/vaadin-date-metadata-controller.js +170 -156
- package/src/vaadin-date-picker-helper.d.ts +144 -13
- package/src/vaadin-date-picker-helper.js +119 -15
- package/src/vaadin-date-picker-mixin.d.ts +71 -31
- package/src/vaadin-date-picker-mixin.js +116 -136
- package/src/vaadin-date-picker-overlay-content-mixin.js +143 -163
- package/src/vaadin-date-picker-year.js +3 -1
- package/src/vaadin-date-picker.d.ts +67 -11
- package/src/vaadin-date-picker.js +66 -13
- package/src/vaadin-infinite-scroller.js +19 -6
- package/src/vaadin-month-calendar-mixin.js +41 -64
- package/src/vaadin-month-calendar.js +4 -0
- package/web-types.json +29 -11
- package/web-types.lit.json +14 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vaadin/date-picker",
|
|
3
|
-
"version": "25.3.0-
|
|
3
|
+
"version": "25.3.0-rc1",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -35,21 +35,21 @@
|
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@open-wc/dedupe-mixin": "^1.3.0",
|
|
38
|
-
"@vaadin/a11y-base": "25.3.0-
|
|
39
|
-
"@vaadin/button": "25.3.0-
|
|
40
|
-
"@vaadin/component-base": "25.3.0-
|
|
41
|
-
"@vaadin/field-base": "25.3.0-
|
|
42
|
-
"@vaadin/input-container": "25.3.0-
|
|
43
|
-
"@vaadin/overlay": "25.3.0-
|
|
44
|
-
"@vaadin/vaadin-themable-mixin": "25.3.0-
|
|
38
|
+
"@vaadin/a11y-base": "25.3.0-rc1",
|
|
39
|
+
"@vaadin/button": "25.3.0-rc1",
|
|
40
|
+
"@vaadin/component-base": "25.3.0-rc1",
|
|
41
|
+
"@vaadin/field-base": "25.3.0-rc1",
|
|
42
|
+
"@vaadin/input-container": "25.3.0-rc1",
|
|
43
|
+
"@vaadin/overlay": "25.3.0-rc1",
|
|
44
|
+
"@vaadin/vaadin-themable-mixin": "25.3.0-rc1",
|
|
45
45
|
"lit": "^3.0.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@vaadin/aura": "25.3.0-
|
|
49
|
-
"@vaadin/chai-plugins": "25.3.0-
|
|
50
|
-
"@vaadin/test-runner-commands": "25.3.0-
|
|
48
|
+
"@vaadin/aura": "25.3.0-rc1",
|
|
49
|
+
"@vaadin/chai-plugins": "25.3.0-rc1",
|
|
50
|
+
"@vaadin/test-runner-commands": "25.3.0-rc1",
|
|
51
51
|
"@vaadin/testing-helpers": "^2.0.0",
|
|
52
|
-
"@vaadin/vaadin-lumo-styles": "25.3.0-
|
|
52
|
+
"@vaadin/vaadin-lumo-styles": "25.3.0-rc1",
|
|
53
53
|
"sinon": "^22.0.0"
|
|
54
54
|
},
|
|
55
55
|
"customElements": "custom-elements.json",
|
|
@@ -57,5 +57,5 @@
|
|
|
57
57
|
"web-types.json",
|
|
58
58
|
"web-types.lit.json"
|
|
59
59
|
],
|
|
60
|
-
"gitHead": "
|
|
60
|
+
"gitHead": "d4809590b2f61bd607e376ab3dec84f4bfb059a7"
|
|
61
61
|
}
|
|
@@ -50,9 +50,6 @@ export const overlayContentStyles = css`
|
|
|
50
50
|
display: none !important;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
/* Loading spinner shown while data is being loaded (currently while the disabled dates provider
|
|
54
|
-
resolves). Positioned over the month scroller, which paints its own transformed layers, so
|
|
55
|
-
grid stacking is not enough. */
|
|
56
53
|
[part='loader'] {
|
|
57
54
|
position: absolute;
|
|
58
55
|
z-index: 1;
|
|
@@ -10,6 +10,9 @@ export const monthCalendarStyles = css`
|
|
|
10
10
|
:host {
|
|
11
11
|
display: block;
|
|
12
12
|
padding: var(--vaadin-date-picker-month-padding, var(--vaadin-padding-s));
|
|
13
|
+
-webkit-tap-highlight-color: transparent;
|
|
14
|
+
-webkit-user-select: none;
|
|
15
|
+
user-select: none;
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
[part='month-header'] {
|
|
@@ -3,73 +3,92 @@
|
|
|
3
3
|
* Copyright (c) 2016 - 2026 Vaadin Ltd.
|
|
4
4
|
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
5
|
*/
|
|
6
|
-
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
|
7
|
-
import type { DatePickerDateMetadata,
|
|
8
|
-
|
|
9
|
-
export type DateMetadataProvider = (
|
|
10
|
-
range: DatePickerDateRange,
|
|
11
|
-
) => DatePickerDateMetadata[] | Promise<DatePickerDateMetadata[]>;
|
|
6
|
+
import type { ReactiveController, ReactiveControllerHost, ReactiveElement } from 'lit';
|
|
7
|
+
import type { DatePickerDateMetadata, DatePickerDateMetadataProvider } from './vaadin-date-picker-mixin.js';
|
|
12
8
|
|
|
13
9
|
/**
|
|
14
|
-
* A reactive controller that resolves the metadata (disabled state
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
10
|
+
* A reactive controller that resolves the metadata (currently the disabled state)
|
|
11
|
+
* for the dates shown by the date-picker's `dateMetadataProvider`.
|
|
12
|
+
*
|
|
13
|
+
* The provider is called for a range of months and may return an array
|
|
14
|
+
* synchronously or a `Promise`, so results from a server (Flow) or a remote
|
|
15
|
+
* availability service can be awaited. The range and each returned entry identify
|
|
16
|
+
* a date by an ISO 8601 string, e.g. `{ date: '2026-01-01', disabled: true }`.
|
|
17
|
+
*
|
|
18
|
+
* `ARCHITECTURE.md` in this package records the reasoning behind the request,
|
|
19
|
+
* caching, notification and failure behavior.
|
|
19
20
|
*/
|
|
20
|
-
export
|
|
21
|
-
|
|
21
|
+
export class DateMetadataController implements ReactiveController {
|
|
22
|
+
/**
|
|
23
|
+
* The controller host element.
|
|
24
|
+
*/
|
|
25
|
+
host: HTMLElement & ReactiveControllerHost;
|
|
22
26
|
|
|
23
27
|
/**
|
|
24
28
|
* The provider function, or `null` when none is set.
|
|
25
29
|
*/
|
|
26
|
-
provider:
|
|
30
|
+
provider: DatePickerDateMetadataProvider | null;
|
|
31
|
+
|
|
32
|
+
constructor(host: HTMLElement & ReactiveControllerHost, onChange?: () => void);
|
|
33
|
+
|
|
34
|
+
hostConnected(): void;
|
|
27
35
|
|
|
28
36
|
/**
|
|
29
|
-
*
|
|
37
|
+
* Registers an element to be re-rendered whenever the resolved metadata or the
|
|
38
|
+
* loading state changes. The element must render from its bindings, and must not be
|
|
39
|
+
* the one whose own observer triggers a load. It stays registered for the
|
|
40
|
+
* controller's lifetime.
|
|
30
41
|
*/
|
|
31
|
-
|
|
42
|
+
subscribe(element: ReactiveElement): void;
|
|
32
43
|
|
|
33
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Whether any month range is currently being fetched.
|
|
46
|
+
*/
|
|
47
|
+
isLoading(): boolean;
|
|
34
48
|
|
|
35
49
|
/**
|
|
36
|
-
* Sets the provider function and clears the cache. Passing the same provider
|
|
37
|
-
*
|
|
50
|
+
* Sets the provider function and clears the cache. Passing the same provider again
|
|
51
|
+
* is a no-op, so callers should keep a stable reference.
|
|
38
52
|
*/
|
|
39
|
-
setProvider(provider:
|
|
53
|
+
setProvider(provider: DatePickerDateMetadataProvider | null | undefined): void;
|
|
40
54
|
|
|
41
55
|
/**
|
|
42
56
|
* Clears the cache and invalidates any in-flight requests.
|
|
43
57
|
*/
|
|
44
|
-
|
|
58
|
+
clearCache(): void;
|
|
45
59
|
|
|
46
60
|
/**
|
|
47
|
-
* Whether the month containing the given date
|
|
61
|
+
* Whether the provider has answered for the month containing the given date.
|
|
62
|
+
* A month whose request failed is not loaded and will be requested again.
|
|
48
63
|
*/
|
|
49
|
-
isMonthLoaded(date: Date): boolean;
|
|
64
|
+
isMonthLoaded(date: Date | null | undefined): boolean;
|
|
50
65
|
|
|
51
66
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
67
|
+
* Whether the month containing the given date is currently being fetched. A month
|
|
68
|
+
* that has not been asked about is not pending, so this reports the same state as
|
|
69
|
+
* `isLoading()` does for the whole cache.
|
|
54
70
|
*/
|
|
55
|
-
|
|
71
|
+
isMonthPending(date: Date | null | undefined): boolean;
|
|
56
72
|
|
|
57
73
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
74
|
+
* The metadata resolved for the given date, or `undefined` when the date has
|
|
75
|
+
* no metadata or its month has not been resolved yet. Returns the entry the
|
|
76
|
+
* provider supplied, which the caller must not modify.
|
|
60
77
|
*/
|
|
61
|
-
|
|
78
|
+
getMetadata(date: Date | null | undefined): DatePickerDateMetadata | undefined;
|
|
62
79
|
|
|
63
80
|
/**
|
|
64
|
-
* Whether the given date
|
|
65
|
-
* provider, or its month has not been resolved. Returns `false` when no
|
|
66
|
-
* provider is set.
|
|
81
|
+
* Whether the given date is disabled by its metadata.
|
|
67
82
|
*/
|
|
68
|
-
|
|
83
|
+
isDateDisabled(date: Date | null | undefined): boolean;
|
|
69
84
|
|
|
70
85
|
/**
|
|
71
86
|
* Ensures the provider has been consulted for the inclusive range between the
|
|
72
|
-
* given dates,
|
|
87
|
+
* given dates, rounded out to whole blocks of months. Months already loaded or in
|
|
88
|
+
* flight are skipped, and the ones left over are requested with a single call.
|
|
89
|
+
*
|
|
90
|
+
* Each call that finds a missing month issues its own request, so a caller that
|
|
91
|
+
* loads on scroll should debounce.
|
|
73
92
|
*/
|
|
74
|
-
ensureRangeLoaded(startDate: Date, endDate: Date): void;
|
|
93
|
+
ensureRangeLoaded(startDate: Date | null | undefined, endDate: Date | null | undefined): void;
|
|
75
94
|
}
|
|
@@ -3,252 +3,266 @@
|
|
|
3
3
|
* Copyright (c) 2016 - 2026 Vaadin Ltd.
|
|
4
4
|
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { microTask } from '@vaadin/component-base/src/async.js';
|
|
7
|
+
import { Debouncer } from '@vaadin/component-base/src/debounce.js';
|
|
8
|
+
import { issueWarning } from '@vaadin/component-base/src/warnings.js';
|
|
9
|
+
import { formatISODate, lastOfMonth, monthDate, monthIndex, parseDate } from './vaadin-date-picker-helper.js';
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
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;
|
|
11
|
+
// Counted from January of year 0, so a block is one calendar year.
|
|
12
|
+
const BLOCK_MONTHS = 12;
|
|
14
13
|
|
|
15
|
-
function
|
|
16
|
-
return
|
|
14
|
+
function blockStart(month) {
|
|
15
|
+
return Math.floor(month / BLOCK_MONTHS) * BLOCK_MONTHS;
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
|
|
21
|
-
}
|
|
18
|
+
const PENDING_MONTH = Object.freeze({ pending: true });
|
|
22
19
|
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
// The date of an entry, or `undefined` when its `date` is not one. Checked for being a string first,
|
|
21
|
+
// since coercing another type to one can throw, and an entry comes from outside.
|
|
22
|
+
function entryDate(entry) {
|
|
23
|
+
return typeof entry?.date === 'string' ? parseDate(entry.date) : undefined;
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
function groupEntriesByMonth(months, entries) {
|
|
27
|
+
const result = new Map(months.map((month) => [month, new Map()]));
|
|
28
|
+
|
|
29
|
+
if (Array.isArray(entries)) {
|
|
30
|
+
entries.forEach((entry) => {
|
|
31
|
+
const date = entryDate(entry);
|
|
32
|
+
if (date) {
|
|
33
|
+
result.get(monthIndex(date))?.set(date.getDate(), entry);
|
|
34
|
+
} else {
|
|
35
|
+
issueWarning('Ignored `dateMetadataProvider` entries whose `date` is not an ISO 8601 date.');
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
} else if (entries != null) {
|
|
39
|
+
issueWarning('Expected `dateMetadataProvider` to return an array of date metadata objects.');
|
|
40
|
+
}
|
|
30
41
|
|
|
31
|
-
|
|
32
|
-
return new Date(date.getFullYear(), date.getMonth() + months, 1);
|
|
42
|
+
return result;
|
|
33
43
|
}
|
|
34
44
|
|
|
35
45
|
/**
|
|
36
|
-
* A reactive controller that resolves the metadata (disabled state
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
46
|
+
* A reactive controller that resolves the metadata (currently the disabled state)
|
|
47
|
+
* for the dates shown by the date-picker's `dateMetadataProvider`.
|
|
48
|
+
*
|
|
49
|
+
* The provider is called for a range of months and may return an array
|
|
50
|
+
* synchronously or a `Promise`, so results from a server (Flow) or a remote
|
|
51
|
+
* availability service can be awaited. The range and each returned entry identify
|
|
52
|
+
* a date by an ISO 8601 string, e.g. `{ date: '2026-01-01', disabled: true }`.
|
|
43
53
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* returned entry is a `DatePickerDate` extended with metadata fields, e.g.
|
|
47
|
-
* `{ year, month, day, disabled: true, part: 'busy' }`.
|
|
54
|
+
* `ARCHITECTURE.md` in this package records the reasoning behind the request,
|
|
55
|
+
* caching, notification and failure behavior.
|
|
48
56
|
*/
|
|
49
57
|
export class DateMetadataController {
|
|
58
|
+
/**
|
|
59
|
+
* The controller host element.
|
|
60
|
+
* @type {import('lit').ReactiveControllerHost & HTMLElement}
|
|
61
|
+
*/
|
|
62
|
+
host;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The provider function, or `null` when none is set.
|
|
66
|
+
* @type {Function | null}
|
|
67
|
+
*/
|
|
68
|
+
provider = null;
|
|
69
|
+
|
|
70
|
+
/** @type {(() => void) | undefined} */
|
|
71
|
+
#onChange;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* What is known about each month, keyed by month index: a shared marker while its request is in
|
|
75
|
+
* flight, or a record holding the resolved entries by day. A month is absent until it is loaded,
|
|
76
|
+
* and absent again if its request failed.
|
|
77
|
+
* @type {Map<number, { pending: boolean, entries?: Map<number, object> }>}
|
|
78
|
+
*/
|
|
79
|
+
#months = new Map();
|
|
80
|
+
|
|
81
|
+
/** @type {Set<import('lit').ReactiveElement>} */
|
|
82
|
+
#subscribers = new Set();
|
|
83
|
+
|
|
84
|
+
#requestId = 0;
|
|
85
|
+
|
|
86
|
+
/** @type {import('@vaadin/component-base/src/debounce.js').Debouncer} */
|
|
87
|
+
#notifyDebouncer;
|
|
88
|
+
|
|
50
89
|
constructor(host, onChange) {
|
|
51
90
|
this.host = host;
|
|
52
|
-
this
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
this
|
|
57
|
-
this.__requestId = 0;
|
|
91
|
+
this.#onChange = onChange;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
hostConnected() {
|
|
95
|
+
this.#notify();
|
|
58
96
|
}
|
|
59
97
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
98
|
+
/**
|
|
99
|
+
* Registers an element to be re-rendered whenever the resolved metadata or the
|
|
100
|
+
* loading state changes. The element must render from its bindings, and must not be
|
|
101
|
+
* the one whose own observer triggers a load. It stays registered for the
|
|
102
|
+
* controller's lifetime.
|
|
103
|
+
*
|
|
104
|
+
* @param {import('lit').ReactiveElement} element
|
|
105
|
+
*/
|
|
106
|
+
subscribe(element) {
|
|
107
|
+
this.#subscribers.add(element);
|
|
66
108
|
}
|
|
67
109
|
|
|
68
110
|
/**
|
|
69
111
|
* Whether any month range is currently being fetched.
|
|
70
112
|
* @return {boolean}
|
|
71
113
|
*/
|
|
72
|
-
|
|
73
|
-
|
|
114
|
+
isLoading() {
|
|
115
|
+
for (const { pending } of this.#months.values()) {
|
|
116
|
+
if (pending) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
74
121
|
}
|
|
75
122
|
|
|
76
123
|
/**
|
|
77
|
-
* Sets the provider function and clears the cache. Passing the same provider
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* re-fetch every visible range.
|
|
124
|
+
* Sets the provider function and clears the cache. Passing the same provider again
|
|
125
|
+
* is a no-op, so callers should keep a stable reference.
|
|
126
|
+
*
|
|
127
|
+
* @param {Function | null | undefined} provider
|
|
82
128
|
*/
|
|
83
129
|
setProvider(provider) {
|
|
84
|
-
|
|
130
|
+
const next = provider ?? null;
|
|
131
|
+
if (this.provider === next) {
|
|
85
132
|
return;
|
|
86
133
|
}
|
|
87
|
-
this.provider =
|
|
88
|
-
this.
|
|
134
|
+
this.provider = next;
|
|
135
|
+
this.clearCache();
|
|
89
136
|
}
|
|
90
137
|
|
|
91
|
-
/**
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
this.
|
|
96
|
-
this
|
|
97
|
-
this
|
|
138
|
+
/**
|
|
139
|
+
* Clears the cache and invalidates any in-flight requests.
|
|
140
|
+
*/
|
|
141
|
+
clearCache() {
|
|
142
|
+
this.#months.clear();
|
|
143
|
+
this.#requestId += 1;
|
|
144
|
+
this.#notify();
|
|
98
145
|
}
|
|
99
146
|
|
|
100
147
|
/**
|
|
101
|
-
* Whether the month containing the given date
|
|
102
|
-
*
|
|
148
|
+
* Whether the provider has answered for the month containing the given date.
|
|
149
|
+
* A month whose request failed is not loaded and will be requested again.
|
|
150
|
+
* @param {Date | null | undefined} date
|
|
103
151
|
* @return {boolean}
|
|
104
152
|
*/
|
|
105
153
|
isMonthLoaded(date) {
|
|
106
|
-
return this
|
|
154
|
+
return !!this.#resolvedMonth(date);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Whether the month containing the given date is currently being fetched. A month
|
|
159
|
+
* that has not been asked about is not pending, so this reports the same state as
|
|
160
|
+
* `isLoading()` does for the whole cache.
|
|
161
|
+
* @param {Date | null | undefined} date
|
|
162
|
+
* @return {boolean}
|
|
163
|
+
*/
|
|
164
|
+
isMonthPending(date) {
|
|
165
|
+
return !!date && !!this.#months.get(monthIndex(date))?.pending;
|
|
107
166
|
}
|
|
108
167
|
|
|
109
168
|
/**
|
|
110
169
|
* The metadata resolved for the given date, or `undefined` when the date has
|
|
111
|
-
* no metadata or its month has not been resolved yet.
|
|
112
|
-
*
|
|
170
|
+
* no metadata or its month has not been resolved yet. Returns the entry the
|
|
171
|
+
* provider supplied, which the caller must not modify.
|
|
172
|
+
* @param {Date | null | undefined} date
|
|
113
173
|
* @return {object | undefined}
|
|
114
174
|
*/
|
|
115
175
|
getMetadata(date) {
|
|
116
|
-
return date
|
|
176
|
+
return this.#resolvedMonth(date)?.entries.get(date.getDate());
|
|
117
177
|
}
|
|
118
178
|
|
|
119
179
|
/**
|
|
120
|
-
* Whether the given date is disabled by
|
|
121
|
-
*
|
|
122
|
-
* @param {Date} date
|
|
180
|
+
* Whether the given date is disabled by its metadata.
|
|
181
|
+
* @param {Date | null | undefined} date
|
|
123
182
|
* @return {boolean}
|
|
124
183
|
*/
|
|
125
184
|
isDateDisabled(date) {
|
|
126
185
|
return !!this.getMetadata(date)?.disabled;
|
|
127
186
|
}
|
|
128
187
|
|
|
129
|
-
/**
|
|
130
|
-
* Whether the given date cannot be selected yet: it is disabled by the
|
|
131
|
-
* provider, or its month has not been resolved so its state is still unknown.
|
|
132
|
-
* Returns `false` when no provider is set. Used to block selection and to
|
|
133
|
-
* render dates as non-selectable while their month is loading.
|
|
134
|
-
* @param {Date} date
|
|
135
|
-
* @return {boolean}
|
|
136
|
-
*/
|
|
137
|
-
isDateBlocked(date) {
|
|
138
|
-
return !!this.provider && !!date && (this.isDateDisabled(date) || !this.isMonthLoaded(date));
|
|
139
|
-
}
|
|
140
|
-
|
|
141
188
|
/**
|
|
142
189
|
* Ensures the provider has been consulted for the inclusive range between the
|
|
143
|
-
* given dates,
|
|
144
|
-
*
|
|
145
|
-
*
|
|
190
|
+
* given dates, rounded out to whole blocks of months. Months already loaded or in
|
|
191
|
+
* flight are skipped, and the ones left over are requested with a single call.
|
|
192
|
+
*
|
|
193
|
+
* Each call that finds a missing month issues its own request, so a caller that
|
|
194
|
+
* loads on scroll should debounce.
|
|
146
195
|
*
|
|
147
|
-
* @param {Date} startDate
|
|
148
|
-
* @param {Date} endDate
|
|
196
|
+
* @param {Date | null | undefined} startDate
|
|
197
|
+
* @param {Date | null | undefined} endDate
|
|
149
198
|
*/
|
|
150
199
|
ensureRangeLoaded(startDate, endDate) {
|
|
151
200
|
if (!this.provider || !startDate || !endDate) {
|
|
152
201
|
return;
|
|
153
202
|
}
|
|
154
203
|
|
|
155
|
-
const first =
|
|
156
|
-
const last =
|
|
204
|
+
const first = blockStart(monthIndex(startDate));
|
|
205
|
+
const last = blockStart(monthIndex(endDate)) + BLOCK_MONTHS - 1;
|
|
157
206
|
|
|
158
|
-
const
|
|
159
|
-
for (let month = first; month <= last; month
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
monthsToLoad.push(new Date(month));
|
|
207
|
+
const months = [];
|
|
208
|
+
for (let month = first; month <= last; month++) {
|
|
209
|
+
if (!this.#months.has(month)) {
|
|
210
|
+
months.push(month);
|
|
163
211
|
}
|
|
164
212
|
}
|
|
165
213
|
|
|
166
|
-
if (
|
|
167
|
-
|
|
214
|
+
if (months.length > 0) {
|
|
215
|
+
this.#loadMonths(months);
|
|
168
216
|
}
|
|
169
|
-
|
|
170
|
-
this.__groupConsecutiveMonths(monthsToLoad).forEach((group) => this.__loadGroup(group));
|
|
171
217
|
}
|
|
172
218
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
months.forEach((month) => {
|
|
177
|
-
const group = groups.at(-1);
|
|
178
|
-
const previous = group?.at(-1);
|
|
179
|
-
if (previous && monthKey(addMonths(previous, 1)) === monthKey(month)) {
|
|
180
|
-
group.push(month);
|
|
181
|
-
} else {
|
|
182
|
-
groups.push([month]);
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
return groups;
|
|
219
|
+
#resolvedMonth(date) {
|
|
220
|
+
const month = date && this.#months.get(monthIndex(date));
|
|
221
|
+
return month && !month.pending ? month : undefined;
|
|
186
222
|
}
|
|
187
223
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
this.__notify();
|
|
224
|
+
async #loadMonths(months) {
|
|
225
|
+
const requestId = this.#requestId;
|
|
226
|
+
months.forEach((month) => this.#months.set(month, PENDING_MONTH));
|
|
227
|
+
this.#notify();
|
|
193
228
|
|
|
194
229
|
const range = {
|
|
195
|
-
start:
|
|
196
|
-
end:
|
|
230
|
+
start: formatISODate(monthDate(months[0])),
|
|
231
|
+
end: formatISODate(lastOfMonth(monthDate(months.at(-1)))),
|
|
197
232
|
};
|
|
198
233
|
|
|
199
|
-
let
|
|
234
|
+
let entries;
|
|
200
235
|
try {
|
|
201
|
-
|
|
236
|
+
const data = await this.provider(range);
|
|
237
|
+
entries = groupEntriesByMonth(months, data);
|
|
202
238
|
} catch (error) {
|
|
203
|
-
// Treat a throwing provider the same as a rejected promise: clear the
|
|
204
|
-
// pending state and disable nothing, rather than letting the error
|
|
205
|
-
// propagate out of the scroll/render path that triggered the load.
|
|
206
239
|
console.error(error);
|
|
207
|
-
this.__resolveGroup(months, [], requestId);
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (result && typeof result.then === 'function') {
|
|
212
|
-
result.then(
|
|
213
|
-
(dates) => this.__resolveGroup(months, dates, requestId),
|
|
214
|
-
(error) => {
|
|
215
|
-
console.error(error);
|
|
216
|
-
this.__resolveGroup(months, [], requestId);
|
|
217
|
-
},
|
|
218
|
-
);
|
|
219
|
-
} else {
|
|
220
|
-
this.__resolveGroup(months, result, requestId);
|
|
221
240
|
}
|
|
222
|
-
}
|
|
223
241
|
|
|
224
|
-
|
|
225
|
-
__resolveGroup(months, entries, requestId) {
|
|
226
|
-
// Ignore results from before the last reset (e.g. provider changed).
|
|
227
|
-
if (requestId !== this.__requestId) {
|
|
242
|
+
if (requestId !== this.#requestId) {
|
|
228
243
|
return;
|
|
229
244
|
}
|
|
230
245
|
|
|
231
|
-
if (Array.isArray(entries)) {
|
|
232
|
-
entries.forEach((entry) => {
|
|
233
|
-
if (entry) {
|
|
234
|
-
this.__metadata.set(`${entry.year}-${entry.month}-${entry.day}`, entry);
|
|
235
|
-
}
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
|
|
239
246
|
months.forEach((month) => {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
247
|
+
if (entries) {
|
|
248
|
+
this.#months.set(month, { pending: false, entries: entries.get(month) });
|
|
249
|
+
} else {
|
|
250
|
+
this.#months.delete(month);
|
|
251
|
+
}
|
|
243
252
|
});
|
|
244
253
|
|
|
245
|
-
this
|
|
254
|
+
this.#notify();
|
|
246
255
|
}
|
|
247
256
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
257
|
+
#notify() {
|
|
258
|
+
this.#subscribers.forEach((element) => element.requestUpdate());
|
|
259
|
+
|
|
260
|
+
if (this.#onChange) {
|
|
261
|
+
this.#notifyDebouncer = Debouncer.debounce(this.#notifyDebouncer, microTask, () => {
|
|
262
|
+
if (this.host.isConnected) {
|
|
263
|
+
this.#onChange();
|
|
264
|
+
}
|
|
265
|
+
});
|
|
252
266
|
}
|
|
253
267
|
}
|
|
254
268
|
}
|