@lekoala/slot-picker 0.1.0 → 0.1.2
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/README.md +85 -39
- package/custom-elements.json +0 -5
- package/dist/slot-picker.css +22 -0
- package/dist/slot-picker.js +160 -134
- package/dist/slot-picker.min.css +1 -1
- package/dist/slot-picker.min.js +24 -24
- package/dist/types/date.d.ts +8 -1
- package/dist/types/date.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/locales/ar.d.ts +0 -1
- package/dist/types/locales/ar.d.ts.map +1 -1
- package/dist/types/locales/de.d.ts +0 -1
- package/dist/types/locales/de.d.ts.map +1 -1
- package/dist/types/locales/en.d.ts +0 -1
- package/dist/types/locales/en.d.ts.map +1 -1
- package/dist/types/locales/es.d.ts +0 -1
- package/dist/types/locales/es.d.ts.map +1 -1
- package/dist/types/locales/fr.d.ts +0 -1
- package/dist/types/locales/fr.d.ts.map +1 -1
- package/dist/types/locales/hi.d.ts +0 -1
- package/dist/types/locales/hi.d.ts.map +1 -1
- package/dist/types/locales/id.d.ts +0 -1
- package/dist/types/locales/id.d.ts.map +1 -1
- package/dist/types/locales/it.d.ts +0 -1
- package/dist/types/locales/it.d.ts.map +1 -1
- package/dist/types/locales/ja.d.ts +0 -1
- package/dist/types/locales/ja.d.ts.map +1 -1
- package/dist/types/locales/ko.d.ts +0 -1
- package/dist/types/locales/ko.d.ts.map +1 -1
- package/dist/types/locales/nl.d.ts +0 -1
- package/dist/types/locales/nl.d.ts.map +1 -1
- package/dist/types/locales/pl.d.ts +0 -1
- package/dist/types/locales/pl.d.ts.map +1 -1
- package/dist/types/locales/pt-BR.d.ts +0 -1
- package/dist/types/locales/pt-BR.d.ts.map +1 -1
- package/dist/types/locales/pt-PT.d.ts +0 -1
- package/dist/types/locales/pt-PT.d.ts.map +1 -1
- package/dist/types/locales/ru.d.ts +0 -1
- package/dist/types/locales/ru.d.ts.map +1 -1
- package/dist/types/locales/tr.d.ts +0 -1
- package/dist/types/locales/tr.d.ts.map +1 -1
- package/dist/types/locales/zh-CN.d.ts +0 -1
- package/dist/types/locales/zh-CN.d.ts.map +1 -1
- package/dist/types/messages.d.ts +0 -3
- package/dist/types/messages.d.ts.map +1 -1
- package/dist/types/model.d.ts +58 -23
- package/dist/types/model.d.ts.map +1 -1
- package/dist/types/slot-picker.d.ts +25 -15
- package/dist/types/slot-picker.d.ts.map +1 -1
- package/dist/types/views/shared.d.ts +5 -4
- package/dist/types/views/shared.d.ts.map +1 -1
- package/docs/USE_CASES.md +24 -18
- package/package.json +1 -1
- package/src/date.js +11 -1
- package/src/index.js +13 -2
- package/src/locales/ar.js +0 -1
- package/src/locales/de.js +0 -1
- package/src/locales/en.js +0 -1
- package/src/locales/es.js +0 -1
- package/src/locales/fr.js +0 -1
- package/src/locales/hi.js +0 -1
- package/src/locales/id.js +0 -1
- package/src/locales/it.js +0 -1
- package/src/locales/ja.js +0 -1
- package/src/locales/ko.js +0 -1
- package/src/locales/nl.js +0 -1
- package/src/locales/pl.js +0 -1
- package/src/locales/pt-BR.js +0 -1
- package/src/locales/pt-PT.js +0 -1
- package/src/locales/ru.js +0 -1
- package/src/locales/tr.js +0 -1
- package/src/locales/zh-CN.js +0 -1
- package/src/messages.js +0 -1
- package/src/model.js +149 -55
- package/src/slot-picker.css +22 -0
- package/src/slot-picker.js +123 -100
- package/src/views/shared.js +10 -7
package/src/slot-picker.js
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { compareDates, isDateValue, todayValue } from "./date.js";
|
|
2
2
|
import { resolveMessages } from "./messages.js";
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
clampDate,
|
|
5
5
|
clampStart,
|
|
6
6
|
collapsedDays,
|
|
7
7
|
ensureVisible,
|
|
8
8
|
hasDayContent,
|
|
9
9
|
isSlotValue,
|
|
10
|
-
isValidRange,
|
|
11
10
|
moveFocus,
|
|
12
11
|
normalizeBreakpoints,
|
|
13
12
|
normalizeDays,
|
|
13
|
+
normalizeHiddenDays,
|
|
14
|
+
projectDates,
|
|
14
15
|
RESPONSIVE_BREAKPOINTS,
|
|
15
16
|
rangeDetail,
|
|
16
17
|
resolveActiveDate,
|
|
17
18
|
resolveVisibleDayCount,
|
|
18
19
|
slotValue,
|
|
20
|
+
stepStart,
|
|
19
21
|
visibleDays,
|
|
20
22
|
} from "./model.js";
|
|
21
23
|
import { SlotSourceController } from "./source.js";
|
|
@@ -46,7 +48,7 @@ const RANGE_ATTRIBUTES = new Set(["start", "min", "max", "day-count", "responsiv
|
|
|
46
48
|
* responsive: boolean,
|
|
47
49
|
* homeDate: string|null,
|
|
48
50
|
* responsiveBreakpoints: ResponsiveBreakpoint[]|null,
|
|
49
|
-
*
|
|
51
|
+
* hiddenDays: number[]|null,
|
|
50
52
|
* }>} ConfigureOptions
|
|
51
53
|
*/
|
|
52
54
|
|
|
@@ -59,9 +61,11 @@ const RANGE_ATTRIBUTES = new Set(["start", "min", "max", "day-count", "responsiv
|
|
|
59
61
|
* - value: YYYY-MM-DDTHH:mm
|
|
60
62
|
*
|
|
61
63
|
* State:
|
|
62
|
-
* - dayCount: consumer's maximum intention
|
|
64
|
+
* - dayCount: consumer's maximum intention, counted in columns
|
|
65
|
+
* - hiddenDays: weekdays that are never a column (calendar structure)
|
|
63
66
|
* - visibleDayCount: capacity actually resolved from bounds + width
|
|
64
|
-
* - start/range:
|
|
67
|
+
* - start/range: first and last projected day; the civil envelope between
|
|
68
|
+
* them may be wider than `dayCount` when weekdays are hidden
|
|
65
69
|
* - activeDate: consulted day
|
|
66
70
|
* - focusedValue: keyboard target, never a selection
|
|
67
71
|
* - value: selected local slot value
|
|
@@ -89,13 +93,14 @@ export class SlotPickerElement extends HTMLElement {
|
|
|
89
93
|
"home-date",
|
|
90
94
|
"next-availability",
|
|
91
95
|
"notice-display",
|
|
92
|
-
"closed-days",
|
|
93
96
|
"locale",
|
|
94
97
|
"lang",
|
|
95
98
|
];
|
|
96
99
|
|
|
97
100
|
/** @type {SlotDay[]} */
|
|
98
101
|
#days = [];
|
|
102
|
+
/** @type {number[]} */
|
|
103
|
+
#hiddenDays = [];
|
|
99
104
|
#sourceController = new SlotSourceController();
|
|
100
105
|
/** @type {Partial<typeof import("./messages.js").DEFAULT_MESSAGES>|null} */
|
|
101
106
|
#messages = null;
|
|
@@ -176,7 +181,10 @@ export class SlotPickerElement extends HTMLElement {
|
|
|
176
181
|
set start(value) {
|
|
177
182
|
if (!isDateValue(value)) throw new TypeError("start must be YYYY-MM-DD");
|
|
178
183
|
const count = this.visibleDayCount;
|
|
179
|
-
this.setAttribute(
|
|
184
|
+
this.setAttribute(
|
|
185
|
+
"start",
|
|
186
|
+
count > 0 ? clampStart(value, this.min, this.max, count, this.#hiddenDays) : value,
|
|
187
|
+
);
|
|
180
188
|
}
|
|
181
189
|
|
|
182
190
|
get dayCount() {
|
|
@@ -191,14 +199,54 @@ export class SlotPickerElement extends HTMLElement {
|
|
|
191
199
|
}
|
|
192
200
|
|
|
193
201
|
/**
|
|
194
|
-
*
|
|
195
|
-
*
|
|
202
|
+
* Weekdays that are never rendered as a column, as `Date#getDay` indexes
|
|
203
|
+
* (`[0, 6]` hides the weekend). Calendar structure, resolved before any
|
|
204
|
+
* data is loaded, so `dayCount` keeps meaning "columns": hidden weekdays
|
|
205
|
+
* widen the civil envelope handed to the source instead of eating a column.
|
|
206
|
+
* Never confused with `closed`, which is the state of a real date.
|
|
207
|
+
* @returns {number[]}
|
|
208
|
+
*/
|
|
209
|
+
get hiddenDays() {
|
|
210
|
+
return [...this.#hiddenDays];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** @param {readonly number[]|null} value */
|
|
214
|
+
set hiddenDays(value) {
|
|
215
|
+
this.#hiddenDays = normalizeHiddenDays(value);
|
|
216
|
+
if (!this.isConnected || this.#batching) return;
|
|
217
|
+
this.#reconcile({ pinActive: true });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Capacity requested before the bounds: the consumer's intention narrowed
|
|
222
|
+
* by the responsive ladder. Step 1 of the pipeline.
|
|
223
|
+
*/
|
|
224
|
+
#requestedDayCount() {
|
|
225
|
+
if (!this.responsive || this.#measuredWidth === null) return this.dayCount;
|
|
226
|
+
return resolveVisibleDayCount(
|
|
227
|
+
this.dayCount,
|
|
228
|
+
this.#measuredWidth,
|
|
229
|
+
this.#breakpoints ?? RESPONSIVE_BREAKPOINTS,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Step 2, and the single source of truth for everything downstream: the
|
|
235
|
+
* civil dates actually projected as columns. `range`, `activeDate`,
|
|
236
|
+
* navigation, rendering and loading all read this, so they can never
|
|
237
|
+
* disagree on what a "day" is.
|
|
238
|
+
* @returns {string[]}
|
|
239
|
+
*/
|
|
240
|
+
#projection() {
|
|
241
|
+
return projectDates(this.start, this.#requestedDayCount(), this.#hiddenDays, this.min, this.max);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Capacity actually resolved from `dayCount`, the width and the bounds.
|
|
246
|
+
* @see dayCount for the consumer's intention.
|
|
196
247
|
*/
|
|
197
248
|
get visibleDayCount() {
|
|
198
|
-
|
|
199
|
-
const bounded = boundedDayCount(this.dayCount, this.min, this.max);
|
|
200
|
-
if (!this.responsive || this.#measuredWidth === null) return bounded;
|
|
201
|
-
return resolveVisibleDayCount(bounded, this.#measuredWidth, this.#breakpoints ?? RESPONSIVE_BREAKPOINTS);
|
|
249
|
+
return this.#projection().length;
|
|
202
250
|
}
|
|
203
251
|
|
|
204
252
|
get responsive() {
|
|
@@ -263,11 +311,9 @@ export class SlotPickerElement extends HTMLElement {
|
|
|
263
311
|
}
|
|
264
312
|
|
|
265
313
|
get activeDate() {
|
|
266
|
-
const
|
|
267
|
-
if (!
|
|
268
|
-
|
|
269
|
-
const base = isDateValue(raw) ? raw : this.start;
|
|
270
|
-
return resolveActiveDate(this.start, count, base);
|
|
314
|
+
const dates = this.#projection();
|
|
315
|
+
if (!dates.length) return "";
|
|
316
|
+
return resolveActiveDate(dates, this.getAttribute("active-date") || "");
|
|
271
317
|
}
|
|
272
318
|
|
|
273
319
|
set activeDate(value) {
|
|
@@ -319,23 +365,6 @@ export class SlotPickerElement extends HTMLElement {
|
|
|
319
365
|
this.setAttribute("notice-display", value);
|
|
320
366
|
}
|
|
321
367
|
|
|
322
|
-
/**
|
|
323
|
-
* Projection policy for `closed` days:
|
|
324
|
-
* - `show` (default) renders every civil day;
|
|
325
|
-
* - `hide` omits closed days from the projection without changing the civil
|
|
326
|
-
* range, `previous()`/`next()` or `source.load()`.
|
|
327
|
-
*/
|
|
328
|
-
get closedDays() {
|
|
329
|
-
return this.getAttribute("closed-days") === "hide" ? "hide" : "show";
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
set closedDays(value) {
|
|
333
|
-
if (value !== "show" && value !== "hide") {
|
|
334
|
-
throw new TypeError('closed-days must be "show" or "hide"');
|
|
335
|
-
}
|
|
336
|
-
this.setAttribute("closed-days", value);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
368
|
get expanded() {
|
|
340
369
|
return this.hasAttribute("expanded");
|
|
341
370
|
}
|
|
@@ -399,19 +428,22 @@ export class SlotPickerElement extends HTMLElement {
|
|
|
399
428
|
else this.setAttribute("locale", value);
|
|
400
429
|
}
|
|
401
430
|
|
|
402
|
-
/**
|
|
431
|
+
/**
|
|
432
|
+
* Resolved visible range: first projected day, last projected day, and the
|
|
433
|
+
* number of columns. Empty and invalid when the count is 0. The civil
|
|
434
|
+
* envelope `start`..`end` is what the source must load and may be wider
|
|
435
|
+
* than `dayCount`.
|
|
436
|
+
*/
|
|
403
437
|
get range() {
|
|
404
|
-
|
|
405
|
-
if (!count) return { start: "", end: "", dayCount: 0 };
|
|
406
|
-
return rangeDetail(this.start, count);
|
|
438
|
+
return rangeDetail(this.#projection());
|
|
407
439
|
}
|
|
408
440
|
|
|
409
441
|
previous() {
|
|
410
|
-
this.#navigate(-
|
|
442
|
+
this.#navigate(-1);
|
|
411
443
|
}
|
|
412
444
|
|
|
413
445
|
next() {
|
|
414
|
-
this.#navigate(
|
|
446
|
+
this.#navigate(1);
|
|
415
447
|
}
|
|
416
448
|
|
|
417
449
|
/**
|
|
@@ -421,10 +453,9 @@ export class SlotPickerElement extends HTMLElement {
|
|
|
421
453
|
* @returns {string} the resolved active date, or "" when the range is invalid
|
|
422
454
|
*/
|
|
423
455
|
goTo(date) {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
this.#mutate(() => this.#moveTo(target, count));
|
|
456
|
+
if (!this.visibleDayCount) return "";
|
|
457
|
+
const target = isDateValue(date) ? clampDate(date, this.min, this.max) : this.start;
|
|
458
|
+
this.#mutate(() => this.#moveTo(target));
|
|
428
459
|
this.#commit();
|
|
429
460
|
return this.activeDate;
|
|
430
461
|
}
|
|
@@ -476,7 +507,7 @@ export class SlotPickerElement extends HTMLElement {
|
|
|
476
507
|
if (options.homeDate !== undefined) this.homeDate = options.homeDate ?? "";
|
|
477
508
|
if (options.responsiveBreakpoints !== undefined)
|
|
478
509
|
this.responsiveBreakpoints = options.responsiveBreakpoints;
|
|
479
|
-
if (options.
|
|
510
|
+
if (options.hiddenDays !== undefined) this.hiddenDays = options.hiddenDays;
|
|
480
511
|
});
|
|
481
512
|
// An explicit start wins over the previous active-day anchor; only the
|
|
482
513
|
// other options preserve the consulted day.
|
|
@@ -507,21 +538,23 @@ export class SlotPickerElement extends HTMLElement {
|
|
|
507
538
|
if (options.responsiveBreakpoints != null && !Array.isArray(options.responsiveBreakpoints)) {
|
|
508
539
|
throw new TypeError("responsiveBreakpoints must be an array or null");
|
|
509
540
|
}
|
|
510
|
-
if (options.
|
|
511
|
-
throw new TypeError('closedDays must be "show" or "hide"');
|
|
512
|
-
}
|
|
541
|
+
if (options.hiddenDays !== undefined) normalizeHiddenDays(options.hiddenDays);
|
|
513
542
|
}
|
|
514
543
|
|
|
515
544
|
async reload() {
|
|
516
545
|
await this.#load();
|
|
517
546
|
}
|
|
518
547
|
|
|
519
|
-
/**
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
548
|
+
/**
|
|
549
|
+
* Move one window, counted in projected days: the adjacent window keeps the
|
|
550
|
+
* same number of columns.
|
|
551
|
+
* @param {1|-1} direction
|
|
552
|
+
*/
|
|
553
|
+
#navigate(direction) {
|
|
554
|
+
const dates = this.#projection();
|
|
555
|
+
if (!dates.length) return;
|
|
556
|
+
const next = stepStart(dates, direction, this.#hiddenDays, this.min, this.max);
|
|
557
|
+
if (!next || next === this.start) return;
|
|
525
558
|
this.#mutate(() => this.setAttribute("start", next));
|
|
526
559
|
this.#commit();
|
|
527
560
|
}
|
|
@@ -529,23 +562,16 @@ export class SlotPickerElement extends HTMLElement {
|
|
|
529
562
|
/**
|
|
530
563
|
* Move the window just enough to make `target` visible.
|
|
531
564
|
* @param {string} target
|
|
532
|
-
* @param {number} count
|
|
533
565
|
*/
|
|
534
|
-
#moveTo(target
|
|
535
|
-
const
|
|
566
|
+
#moveTo(target) {
|
|
567
|
+
const count = this.#requestedDayCount();
|
|
568
|
+
const nextStart = ensureVisible(this.start, count, target, this.min, this.max, this.#hiddenDays);
|
|
536
569
|
if (nextStart !== this.getAttribute("start")) this.setAttribute("start", nextStart);
|
|
537
|
-
const
|
|
570
|
+
const dates = projectDates(nextStart, count, this.#hiddenDays, this.min, this.max);
|
|
571
|
+
const nextActive = resolveActiveDate(dates, target);
|
|
538
572
|
if (this.getAttribute("active-date") !== nextActive) this.setAttribute("active-date", nextActive);
|
|
539
573
|
}
|
|
540
574
|
|
|
541
|
-
/** @param {string} date */
|
|
542
|
-
#clampToBounds(date) {
|
|
543
|
-
let next = date;
|
|
544
|
-
if (this.min && compareDates(next, this.min) < 0) next = this.min;
|
|
545
|
-
if (this.max && compareDates(next, this.max) > 0) next = this.max;
|
|
546
|
-
return next;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
575
|
/**
|
|
550
576
|
* @param {() => void} fn
|
|
551
577
|
*/
|
|
@@ -570,8 +596,8 @@ export class SlotPickerElement extends HTMLElement {
|
|
|
570
596
|
const wasBatching = this.#batching;
|
|
571
597
|
this.#batching = true;
|
|
572
598
|
try {
|
|
573
|
-
const count = this
|
|
574
|
-
const invalid =
|
|
599
|
+
const count = this.#requestedDayCount();
|
|
600
|
+
const invalid = this.visibleDayCount === 0;
|
|
575
601
|
this.toggleAttribute("data-invalid-range", invalid);
|
|
576
602
|
if (!invalid) {
|
|
577
603
|
const rawActive = this.getAttribute("active-date") || "";
|
|
@@ -581,17 +607,18 @@ export class SlotPickerElement extends HTMLElement {
|
|
|
581
607
|
let anchor = this.start;
|
|
582
608
|
if (pinActive) {
|
|
583
609
|
if (previous?.dayCount && isDateValue(rawActive)) {
|
|
584
|
-
anchor =
|
|
610
|
+
anchor = clampDate(rawActive, previous.start, previous.end);
|
|
585
611
|
} else if (isDateValue(rawActive)) {
|
|
586
612
|
anchor = rawActive;
|
|
587
613
|
} else if (previous?.start) {
|
|
588
614
|
anchor = previous.start;
|
|
589
615
|
}
|
|
590
616
|
}
|
|
591
|
-
const nextStart = ensureVisible(this.start, count, anchor, this.min, this.max);
|
|
617
|
+
const nextStart = ensureVisible(this.start, count, anchor, this.min, this.max, this.#hiddenDays);
|
|
592
618
|
if (nextStart !== this.getAttribute("start")) this.setAttribute("start", nextStart);
|
|
593
619
|
if (pinActive && isDateValue(anchor)) {
|
|
594
|
-
const
|
|
620
|
+
const dates = projectDates(nextStart, count, this.#hiddenDays, this.min, this.max);
|
|
621
|
+
const nextActive = resolveActiveDate(dates, anchor);
|
|
595
622
|
if (this.getAttribute("active-date") !== nextActive) this.setAttribute("active-date", nextActive);
|
|
596
623
|
}
|
|
597
624
|
}
|
|
@@ -665,9 +692,9 @@ export class SlotPickerElement extends HTMLElement {
|
|
|
665
692
|
|
|
666
693
|
/** @param {string} date @param {boolean} emit */
|
|
667
694
|
#setActiveDate(date, emit = true) {
|
|
668
|
-
const
|
|
669
|
-
if (!
|
|
670
|
-
const resolved = resolveActiveDate(
|
|
695
|
+
const dates = this.#projection();
|
|
696
|
+
if (!dates.length) return;
|
|
697
|
+
const resolved = resolveActiveDate(dates, date);
|
|
671
698
|
const before = this.activeDate;
|
|
672
699
|
if (resolved === before && this.getAttribute("active-date") === resolved) return;
|
|
673
700
|
this.setAttribute("active-date", resolved);
|
|
@@ -763,19 +790,13 @@ export class SlotPickerElement extends HTMLElement {
|
|
|
763
790
|
}
|
|
764
791
|
|
|
765
792
|
/**
|
|
766
|
-
* Days actually handed to a projection
|
|
767
|
-
*
|
|
768
|
-
*
|
|
769
|
-
* `activeDate` on its own.
|
|
793
|
+
* Days actually handed to a projection: one per projected date, filled in
|
|
794
|
+
* when the source knows nothing about that day. Loaded data never adds or
|
|
795
|
+
* removes a column, so `closed: true` cannot change the grid.
|
|
770
796
|
* @returns {SlotDay[]}
|
|
771
797
|
*/
|
|
772
798
|
#projectedDays() {
|
|
773
|
-
|
|
774
|
-
if (!count) return [];
|
|
775
|
-
const days = visibleDays(this.#days, this.start, count);
|
|
776
|
-
if (this.closedDays !== "hide") return days;
|
|
777
|
-
const active = this.activeDate;
|
|
778
|
-
return days.filter((day) => !day.closed || (this.layout === "day" && day.date === active));
|
|
799
|
+
return visibleDays(this.#days, this.#projection());
|
|
779
800
|
}
|
|
780
801
|
|
|
781
802
|
#render() {
|
|
@@ -785,18 +806,19 @@ export class SlotPickerElement extends HTMLElement {
|
|
|
785
806
|
const pendingSelector = this.#pendingFocus;
|
|
786
807
|
this.#pendingFocus = "";
|
|
787
808
|
|
|
788
|
-
const
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
const civilDays = count ? visibleDays(this.#days, this.start, count) : [];
|
|
793
|
-
const days = this.#projectedDays();
|
|
794
|
-
const allClosed = civilDays.length > 0 && civilDays.every((day) => day.closed);
|
|
809
|
+
const dates = this.#projection();
|
|
810
|
+
const range = rangeDetail(dates);
|
|
811
|
+
const invalid = range.dayCount === 0;
|
|
812
|
+
const days = visibleDays(this.#days, dates);
|
|
795
813
|
// Resolve at render time so `setDefaultMessages()` reaches live instances.
|
|
796
814
|
const messages = resolveMessages(this.#messages);
|
|
797
815
|
const locale = this.locale;
|
|
798
|
-
|
|
799
|
-
|
|
816
|
+
// Navigation is offered when stepping actually moves the window: with
|
|
817
|
+
// hidden weekdays a bound can sit on a day that is never projected, so a
|
|
818
|
+
// civil comparison would enable a button that does nothing.
|
|
819
|
+
const canPrevious =
|
|
820
|
+
!invalid && stepStart(dates, -1, this.#hiddenDays, this.min, this.max) !== range.start;
|
|
821
|
+
const canNext = !invalid && stepStart(dates, 1, this.#hiddenDays, this.min, this.max) !== range.start;
|
|
800
822
|
|
|
801
823
|
// A range is "empty" only when the projected days have no meaningful
|
|
802
824
|
// content. A notice or a closed day without slots keeps the projection.
|
|
@@ -806,12 +828,11 @@ export class SlotPickerElement extends HTMLElement {
|
|
|
806
828
|
let hasOverflow = false;
|
|
807
829
|
if (empty) {
|
|
808
830
|
content = rangeEmpty({
|
|
809
|
-
start:
|
|
810
|
-
end:
|
|
831
|
+
start: range.start,
|
|
832
|
+
end: range.end,
|
|
811
833
|
invalid,
|
|
812
834
|
canNext,
|
|
813
835
|
nextAvailability: this.hasAttribute("next-availability"),
|
|
814
|
-
closed: allClosed,
|
|
815
836
|
locale,
|
|
816
837
|
messages,
|
|
817
838
|
});
|
|
@@ -857,9 +878,11 @@ export class SlotPickerElement extends HTMLElement {
|
|
|
857
878
|
const homeAvailable = this.hasAttribute("home-date") && !invalid;
|
|
858
879
|
const homeInRange =
|
|
859
880
|
homeAvailable &&
|
|
860
|
-
compareDates(this.homeDate,
|
|
861
|
-
compareDates(this.homeDate,
|
|
881
|
+
compareDates(this.homeDate, range.start) >= 0 &&
|
|
882
|
+
compareDates(this.homeDate, range.end) <= 0;
|
|
862
883
|
|
|
884
|
+
// Column capacity is resolved before the data: at equal width and bounds
|
|
885
|
+
// it never changes, whatever the source says about those days.
|
|
863
886
|
this.style.setProperty("--_sp-day-count", String(Math.max(1, days.length)));
|
|
864
887
|
this.style.setProperty("--_sp-max-visible-rows", String(this.maxVisibleRows));
|
|
865
888
|
// `aria-busy` is the single source of loading state; the fade is pure CSS.
|
package/src/views/shared.js
CHANGED
|
@@ -3,7 +3,7 @@ import { slotValue } from "../model.js";
|
|
|
3
3
|
|
|
4
4
|
/** @typedef {import("../model.js").Slot} Slot */
|
|
5
5
|
/** @typedef {{oneSlot:string,manySlots:string}} SlotCountMessages */
|
|
6
|
-
/** @typedef {{rangeEmptyTitle:string,rangeEmptyDescription:string,rangeEmptyNext:string,nextAvailability:string
|
|
6
|
+
/** @typedef {{rangeEmptyTitle:string,rangeEmptyDescription:string,rangeEmptyNext:string,nextAvailability:string}} RangeEmptyMessages */
|
|
7
7
|
|
|
8
8
|
/** @type {Record<string,string>} */
|
|
9
9
|
const ENTITIES = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
@@ -60,7 +60,9 @@ export function slotButton({ date, slot, dayIndex, slotIndex, selected, tabbed }
|
|
|
60
60
|
const disabled = slot.disabled ? ' disabled aria-disabled="true"' : "";
|
|
61
61
|
// Neutral presentation hook: a theme maps tones to color, the core does not.
|
|
62
62
|
const tone = slot.tone ? ` data-tone="${escapeAttr(slot.tone)}"` : "";
|
|
63
|
-
|
|
63
|
+
// The time sits in its own inline-flex wrapper so a theme can append an icon
|
|
64
|
+
// that stays in flow next to it without leaking width into the day column.
|
|
65
|
+
return `<button type="button" role="option" class="sp-slot" data-day-index="${dayIndex}" data-slot-index="${slotIndex}" data-value="${escapeAttr(value)}"${tone} aria-selected="${selected ? "true" : "false"}" tabindex="${tabbed ? 0 : -1}"${disabled}${description}><span class="sp-slot-time">${escapeHtml(slot.start)}</span></button>`;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
/**
|
|
@@ -119,9 +121,12 @@ export function slotCountText(count, messages) {
|
|
|
119
121
|
* the immediately following window, while `next availability` (opt-in via
|
|
120
122
|
* `next-availability`) is the contextual business search that may skip
|
|
121
123
|
* several windows through the source. They are never merged.
|
|
122
|
-
*
|
|
124
|
+
*
|
|
125
|
+
* A closed day is day content, so a window holding one never reaches this
|
|
126
|
+
* state: it keeps its columns and says "Closed" in each of them.
|
|
127
|
+
* @param {{start:string,end:string,invalid:boolean,canNext:boolean,nextAvailability:boolean,locale:string,messages:RangeEmptyMessages}} options
|
|
123
128
|
*/
|
|
124
|
-
export function rangeEmpty({ start, end, invalid, canNext, nextAvailability,
|
|
129
|
+
export function rangeEmpty({ start, end, invalid, canNext, nextAvailability, locale, messages }) {
|
|
125
130
|
const showDescription = !invalid && Boolean(start);
|
|
126
131
|
let description = "";
|
|
127
132
|
if (showDescription) {
|
|
@@ -147,10 +152,8 @@ export function rangeEmpty({ start, end, invalid, canNext, nextAvailability, clo
|
|
|
147
152
|
);
|
|
148
153
|
}
|
|
149
154
|
const action = actions.length ? `<div class="sp-range-empty-actions">${actions.join("")}</div>` : "";
|
|
150
|
-
// A wholly closed civil range is not the same statement as "no availability".
|
|
151
|
-
const title = closed ? messages.closedRange : messages.rangeEmptyTitle;
|
|
152
155
|
return `<div class="sp-range-empty">
|
|
153
|
-
<strong class="sp-range-empty-title">${escapeHtml(
|
|
156
|
+
<strong class="sp-range-empty-title">${escapeHtml(messages.rangeEmptyTitle)}</strong>
|
|
154
157
|
${description}
|
|
155
158
|
${action}
|
|
156
159
|
</div>`;
|