@pure-ds/core 0.7.24 → 0.7.26
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/.cursorrules +12 -1
- package/.github/copilot-instructions.md +12 -1
- package/custom-elements.json +1099 -74
- package/dist/types/public/assets/js/pds-ask.d.ts +2 -2
- package/dist/types/public/assets/js/pds-ask.d.ts.map +1 -1
- package/dist/types/public/assets/js/pds-manager.d.ts +4 -4
- package/dist/types/public/assets/js/pds-manager.d.ts.map +1 -1
- package/dist/types/public/assets/pds/components/pds-daterange.d.ts +2 -0
- package/dist/types/public/assets/pds/components/pds-daterange.d.ts.map +1 -0
- package/dist/types/public/assets/pds/components/pds-form.d.ts.map +1 -1
- package/dist/types/public/assets/pds/components/pds-rating.d.ts +120 -0
- package/dist/types/public/assets/pds/components/pds-rating.d.ts.map +1 -0
- package/dist/types/public/assets/pds/components/pds-tags.d.ts +2 -0
- package/dist/types/public/assets/pds/components/pds-tags.d.ts.map +1 -0
- package/dist/types/public/assets/pds/components/pds-toaster.d.ts +3 -0
- package/dist/types/public/assets/pds/components/pds-toaster.d.ts.map +1 -1
- package/dist/types/src/js/common/ask.d.ts.map +1 -1
- package/dist/types/src/js/pds-core/pds-generator.d.ts.map +1 -1
- package/dist/types/src/js/pds-core/pds-live.d.ts.map +1 -1
- package/package.json +2 -2
- package/packages/pds-cli/bin/templates/bootstrap/pds.config.js +14 -4
- package/public/assets/js/app.js +1 -1
- package/public/assets/js/pds-ask.js +6 -6
- package/public/assets/js/pds-manager.js +115 -40
- package/public/assets/pds/components/pds-calendar.js +91 -159
- package/public/assets/pds/components/pds-daterange.js +683 -0
- package/public/assets/pds/components/pds-form.js +123 -21
- package/public/assets/pds/components/pds-rating.js +648 -0
- package/public/assets/pds/components/pds-tags.js +802 -0
- package/public/assets/pds/components/pds-toaster.js +35 -1
- package/public/assets/pds/core/pds-ask.js +6 -6
- package/public/assets/pds/core/pds-manager.js +115 -40
- package/public/assets/pds/custom-elements.json +1099 -74
- package/public/assets/pds/pds-css-complete.json +7 -2
- package/public/assets/pds/pds.css-data.json +4 -4
- package/public/assets/pds/vscode-custom-data.json +97 -0
- package/src/js/pds-core/pds-generator.js +104 -29
- package/src/js/pds-core/pds-live.js +5 -0
- package/src/js/pds-core/pds-ontology.js +2 -2
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
import { PDS } from "#pds";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_DISPLAY_FORMAT = "d MMM yy";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @component pds-daterange
|
|
7
|
+
* @description Form-associated date range picker with a dropdown panel and two compact calendars.
|
|
8
|
+
*
|
|
9
|
+
* @attr {String} name - Form field name used during form submission.
|
|
10
|
+
* @attr {Boolean} required - Requires both start and end dates to be selected.
|
|
11
|
+
* @attr {Boolean} disabled - Disables interaction and closes the panel when active.
|
|
12
|
+
* @attr {String} value - Serialized range value as ISO interval: `YYYY-MM-DD/YYYY-MM-DD`.
|
|
13
|
+
* @attr {String} display-format - Button date text format. Default is `d MMM yy`.
|
|
14
|
+
*
|
|
15
|
+
* @prop {String} name - Form field name.
|
|
16
|
+
* @prop {Boolean} required - Whether the field is required.
|
|
17
|
+
* @prop {Boolean} disabled - Whether the control is disabled.
|
|
18
|
+
* @prop {Date|null} startDate - Start date value.
|
|
19
|
+
* @prop {Date|null} endDate - End date value.
|
|
20
|
+
* @prop {String} value - Serialized range value as ISO interval: `YYYY-MM-DD/YYYY-MM-DD`.
|
|
21
|
+
* @prop {String} displayFormat - Button date text format.
|
|
22
|
+
*
|
|
23
|
+
* @fires range-change - Fired whenever range selection changes.
|
|
24
|
+
* @fires change - Native-like change event fired alongside `range-change`.
|
|
25
|
+
*
|
|
26
|
+
* @csspart trigger - The button used to open/close the date range panel.
|
|
27
|
+
* @csspart panel - The dropdown panel containing calendars and actions.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* <pds-daterange
|
|
31
|
+
* name="travelPeriod"
|
|
32
|
+
* value="2026-03-10/2026-03-18"
|
|
33
|
+
* required
|
|
34
|
+
* ></pds-daterange>
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* <form>
|
|
38
|
+
* <pds-daterange name="bookingDates" value="2026-03-10/2026-03-18"></pds-daterange>
|
|
39
|
+
* <button type="submit">Submit</button>
|
|
40
|
+
* </form>
|
|
41
|
+
*/
|
|
42
|
+
class PdsDateRange extends HTMLElement {
|
|
43
|
+
#startDate;
|
|
44
|
+
#endDate;
|
|
45
|
+
#internals;
|
|
46
|
+
#initialValue;
|
|
47
|
+
#stylesApplied;
|
|
48
|
+
#eventsBound;
|
|
49
|
+
#syncingValueAttribute;
|
|
50
|
+
#outsideClickHandler;
|
|
51
|
+
|
|
52
|
+
static formAssociated = true;
|
|
53
|
+
|
|
54
|
+
static get observedAttributes() {
|
|
55
|
+
return ["name", "required", "disabled", "value", "display-format"];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
constructor() {
|
|
59
|
+
super();
|
|
60
|
+
this.attachShadow({ mode: "open" });
|
|
61
|
+
|
|
62
|
+
this.#startDate = null;
|
|
63
|
+
this.#endDate = null;
|
|
64
|
+
this.#initialValue = "";
|
|
65
|
+
this.#stylesApplied = false;
|
|
66
|
+
this.#eventsBound = false;
|
|
67
|
+
this.#syncingValueAttribute = false;
|
|
68
|
+
this.#outsideClickHandler = this.handleOutsideClick.bind(this);
|
|
69
|
+
|
|
70
|
+
this.#internals = this.attachInternals?.() ?? null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async connectedCallback() {
|
|
74
|
+
if (!this.#stylesApplied) {
|
|
75
|
+
const componentStyles = PDS.createStylesheet(
|
|
76
|
+
`
|
|
77
|
+
:host {
|
|
78
|
+
display: block;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.daterange {
|
|
82
|
+
position: relative;
|
|
83
|
+
display: inline-block;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.trigger {
|
|
87
|
+
min-width: 16rem;
|
|
88
|
+
justify-content: flex-start;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.panel {
|
|
92
|
+
position: absolute;
|
|
93
|
+
top: calc(100% + var(--spacing-2));
|
|
94
|
+
left: 0;
|
|
95
|
+
z-index: var(--z-dropdown, 1050);
|
|
96
|
+
min-width: max-content;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.panel[hidden] {
|
|
100
|
+
display: none;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.calendars {
|
|
104
|
+
display: grid;
|
|
105
|
+
grid-template-columns: repeat(2, max-content);
|
|
106
|
+
gap: var(--spacing-3);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.calendars pds-calendar {
|
|
110
|
+
--shadow: none;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.status {
|
|
114
|
+
margin-top: var(--spacing-2);
|
|
115
|
+
color: var(--surface-text-secondary);
|
|
116
|
+
font-size: var(--font-size-sm);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.actions {
|
|
120
|
+
margin-top: var(--spacing-2);
|
|
121
|
+
display: flex;
|
|
122
|
+
justify-content: flex-end;
|
|
123
|
+
}
|
|
124
|
+
`
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
await PDS.adoptLayers(
|
|
128
|
+
this.shadowRoot,
|
|
129
|
+
["primitives", "components", "utilities"],
|
|
130
|
+
[componentStyles]
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
this.#stylesApplied = true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this.render();
|
|
137
|
+
await this.ensureCalendarsReady();
|
|
138
|
+
|
|
139
|
+
this.#initialValue = this.getAttribute("value") || "";
|
|
140
|
+
if (this.#initialValue) this.value = this.#initialValue;
|
|
141
|
+
else this.positionCalendars();
|
|
142
|
+
|
|
143
|
+
this.bindEvents();
|
|
144
|
+
this.syncUI();
|
|
145
|
+
this.updateFormState();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async ensureCalendarsReady() {
|
|
149
|
+
if (typeof customElements !== "undefined") {
|
|
150
|
+
await customElements.whenDefined("pds-calendar");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
this.leftCalendar = this.shadowRoot?.querySelector("#left-calendar") || this.leftCalendar;
|
|
154
|
+
this.rightCalendar = this.shadowRoot?.querySelector("#right-calendar") || this.rightCalendar;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
setCalendarDate(calendar, value) {
|
|
158
|
+
if (!calendar || !(value instanceof Date) || Number.isNaN(value.getTime())) return;
|
|
159
|
+
|
|
160
|
+
const serialized = this.serializeDate(value);
|
|
161
|
+
if (serialized) {
|
|
162
|
+
calendar.setAttribute("date", serialized);
|
|
163
|
+
}
|
|
164
|
+
calendar.date = value;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
disconnectedCallback() {
|
|
168
|
+
document.removeEventListener("pointerdown", this.#outsideClickHandler, true);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
172
|
+
if (oldValue === newValue) return;
|
|
173
|
+
if (!this.shadowRoot) return;
|
|
174
|
+
|
|
175
|
+
if (name === "value") {
|
|
176
|
+
if (this.#syncingValueAttribute) return;
|
|
177
|
+
this.value = newValue || "";
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (name === "disabled") {
|
|
182
|
+
this.applyDisabledState();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (name === "display-format") {
|
|
186
|
+
this.syncUI();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
this.updateFormState();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
get form() {
|
|
193
|
+
return this.#internals?.form ?? null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
get name() {
|
|
197
|
+
return this.getAttribute("name") || "";
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
set name(value) {
|
|
201
|
+
if (value == null || value === "") {
|
|
202
|
+
this.removeAttribute("name");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
this.setAttribute("name", value);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
get required() {
|
|
209
|
+
return this.hasAttribute("required");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
set required(value) {
|
|
213
|
+
this.toggleAttribute("required", Boolean(value));
|
|
214
|
+
this.updateFormState();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
get disabled() {
|
|
218
|
+
return this.hasAttribute("disabled");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
set disabled(value) {
|
|
222
|
+
this.toggleAttribute("disabled", Boolean(value));
|
|
223
|
+
this.applyDisabledState();
|
|
224
|
+
this.updateFormState();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
get displayFormat() {
|
|
228
|
+
return this.getAttribute("display-format") || DEFAULT_DISPLAY_FORMAT;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
set displayFormat(value) {
|
|
232
|
+
if (value == null || String(value).trim() === "") {
|
|
233
|
+
this.removeAttribute("display-format");
|
|
234
|
+
this.syncUI();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
this.setAttribute("display-format", String(value));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
get startDate() {
|
|
241
|
+
return this.#startDate ? new Date(this.#startDate) : null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
set startDate(value) {
|
|
245
|
+
const parsed = this.parseDate(value);
|
|
246
|
+
this.#startDate = parsed;
|
|
247
|
+
|
|
248
|
+
if (this.#endDate && this.#startDate && this.#endDate < this.#startDate) {
|
|
249
|
+
this.#endDate = null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
this.positionCalendars();
|
|
253
|
+
this.syncUI();
|
|
254
|
+
this.updateFormState();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
get endDate() {
|
|
258
|
+
return this.#endDate ? new Date(this.#endDate) : null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
set endDate(value) {
|
|
262
|
+
const parsed = this.parseDate(value);
|
|
263
|
+
this.#endDate = parsed;
|
|
264
|
+
|
|
265
|
+
if (this.#startDate && this.#endDate && this.#endDate < this.#startDate) {
|
|
266
|
+
this.#endDate = null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
this.positionCalendars();
|
|
270
|
+
this.syncUI();
|
|
271
|
+
this.updateFormState();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
get value() {
|
|
275
|
+
if (!this.#startDate || !this.#endDate) return "";
|
|
276
|
+
return `${this.serializeDate(this.#startDate)}/${this.serializeDate(this.#endDate)}`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
set value(rawValue) {
|
|
280
|
+
const parsed = this.parseRangeValue(rawValue);
|
|
281
|
+
this.#startDate = parsed?.start ?? null;
|
|
282
|
+
this.#endDate = parsed?.end ?? null;
|
|
283
|
+
|
|
284
|
+
if (this.#endDate && this.#startDate && this.#endDate < this.#startDate) {
|
|
285
|
+
this.#endDate = null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
this.positionCalendars();
|
|
289
|
+
this.syncUI();
|
|
290
|
+
this.updateFormState();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
checkValidity() {
|
|
294
|
+
this.updateFormState();
|
|
295
|
+
return this.#internals?.checkValidity() ?? true;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
reportValidity() {
|
|
299
|
+
this.updateFormState();
|
|
300
|
+
return this.#internals?.reportValidity() ?? true;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
formAssociatedCallback() {
|
|
304
|
+
this.updateFormState();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
formDisabledCallback(disabled) {
|
|
308
|
+
this.disabled = disabled;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
formResetCallback() {
|
|
312
|
+
this.value = this.#initialValue || "";
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
formStateRestoreCallback(state) {
|
|
316
|
+
if (typeof state === "string") this.value = state;
|
|
317
|
+
else this.value = "";
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
render() {
|
|
321
|
+
this.shadowRoot.innerHTML = `
|
|
322
|
+
<div class="daterange">
|
|
323
|
+
<button type="button" class="trigger btn-outline" part="trigger" aria-haspopup="dialog" aria-expanded="false">When - Add dates</button>
|
|
324
|
+
<section class="panel card surface-overlay" part="panel" hidden>
|
|
325
|
+
<div class="calendars">
|
|
326
|
+
<pds-calendar compact id="left-calendar"></pds-calendar>
|
|
327
|
+
<pds-calendar compact id="right-calendar"></pds-calendar>
|
|
328
|
+
</div>
|
|
329
|
+
<p class="status text-muted" id="status-text"></p>
|
|
330
|
+
<div class="actions">
|
|
331
|
+
<button type="button" class="btn-secondary btn-sm" id="clear-btn">Clear</button>
|
|
332
|
+
</div>
|
|
333
|
+
</section>
|
|
334
|
+
</div>
|
|
335
|
+
`;
|
|
336
|
+
|
|
337
|
+
this.triggerButton = this.shadowRoot.querySelector(".trigger");
|
|
338
|
+
this.panel = this.shadowRoot.querySelector(".panel");
|
|
339
|
+
this.leftCalendar = this.shadowRoot.querySelector("#left-calendar");
|
|
340
|
+
this.rightCalendar = this.shadowRoot.querySelector("#right-calendar");
|
|
341
|
+
this.statusText = this.shadowRoot.querySelector("#status-text");
|
|
342
|
+
this.clearButton = this.shadowRoot.querySelector("#clear-btn");
|
|
343
|
+
|
|
344
|
+
this.applyDisabledState();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
bindEvents() {
|
|
348
|
+
if (this.#eventsBound) return;
|
|
349
|
+
this.#eventsBound = true;
|
|
350
|
+
|
|
351
|
+
this.triggerButton?.addEventListener("click", () => {
|
|
352
|
+
if (this.disabled) return;
|
|
353
|
+
this.togglePanel();
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
this.clearButton?.addEventListener("click", () => {
|
|
357
|
+
this.#startDate = null;
|
|
358
|
+
this.#endDate = null;
|
|
359
|
+
this.syncUI();
|
|
360
|
+
this.refreshHighlights();
|
|
361
|
+
this.updateFormState();
|
|
362
|
+
this.dispatchRangeChange();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
this.shadowRoot?.addEventListener("keydown", (event) => {
|
|
366
|
+
if (event.key !== "Escape") return;
|
|
367
|
+
this.closePanel();
|
|
368
|
+
this.triggerButton?.focus();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
document.addEventListener("pointerdown", this.#outsideClickHandler, true);
|
|
372
|
+
|
|
373
|
+
this.bindCalendar(this.leftCalendar, "left");
|
|
374
|
+
this.bindCalendar(this.rightCalendar, "right");
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
bindCalendar(calendar, side) {
|
|
378
|
+
if (!calendar || calendar.dataset.rangeBound) return;
|
|
379
|
+
calendar.dataset.rangeBound = "true";
|
|
380
|
+
|
|
381
|
+
calendar.addEventListener("month-change", () => this.enforceMinimumGap(side));
|
|
382
|
+
calendar.addEventListener("month-rendered", (event) => {
|
|
383
|
+
this.enforceMinimumGap(side);
|
|
384
|
+
const { year, month } = event.detail;
|
|
385
|
+
event.detail.fill(this.buildRangeEventsForMonth(year, month));
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
const bindSelection = () => {
|
|
389
|
+
calendar.shadowRoot?.addEventListener("change", (event) => {
|
|
390
|
+
const radio = event.target?.closest?.(".day-radio-input[data-day]");
|
|
391
|
+
if (!radio) return;
|
|
392
|
+
|
|
393
|
+
const day = Number.parseInt(radio.dataset.day || "", 10);
|
|
394
|
+
if (!Number.isInteger(day)) return;
|
|
395
|
+
|
|
396
|
+
const picked = this.toDayStart(new Date(calendar.year, calendar.month, day));
|
|
397
|
+
this.applyRangeSelection(picked);
|
|
398
|
+
});
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
if (calendar.shadowRoot) bindSelection();
|
|
402
|
+
else queueMicrotask(bindSelection);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
handleOutsideClick(event) {
|
|
406
|
+
if (!this.isConnected || !this.isOpen()) return;
|
|
407
|
+
if (event.composedPath().includes(this)) return;
|
|
408
|
+
this.closePanel();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
togglePanel() {
|
|
412
|
+
if (this.isOpen()) this.closePanel();
|
|
413
|
+
else this.openPanel();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
openPanel() {
|
|
417
|
+
if (!this.panel) return;
|
|
418
|
+
this.panel.hidden = false;
|
|
419
|
+
this.triggerButton?.setAttribute("aria-expanded", "true");
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
closePanel() {
|
|
423
|
+
if (!this.panel) return;
|
|
424
|
+
this.panel.hidden = true;
|
|
425
|
+
this.triggerButton?.setAttribute("aria-expanded", "false");
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
isOpen() {
|
|
429
|
+
return Boolean(this.panel && !this.panel.hidden);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
toDayStart(value) {
|
|
433
|
+
return new Date(value.getFullYear(), value.getMonth(), value.getDate());
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
toMonthStart(value) {
|
|
437
|
+
return new Date(value.getFullYear(), value.getMonth(), 1);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
addMonths(value, monthDelta) {
|
|
441
|
+
return new Date(value.getFullYear(), value.getMonth() + monthDelta, 1);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
parseDate(value) {
|
|
445
|
+
if (value == null || value === "") return null;
|
|
446
|
+
const parsed = value instanceof Date ? new Date(value) : new Date(value);
|
|
447
|
+
if (Number.isNaN(parsed.getTime())) return null;
|
|
448
|
+
return this.toDayStart(parsed);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
serializeDate(value) {
|
|
452
|
+
if (!(value instanceof Date) || Number.isNaN(value.getTime())) return "";
|
|
453
|
+
const year = value.getFullYear();
|
|
454
|
+
const month = String(value.getMonth() + 1).padStart(2, "0");
|
|
455
|
+
const day = String(value.getDate()).padStart(2, "0");
|
|
456
|
+
return `${year}-${month}-${day}`;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
parseRangeValue(rawValue) {
|
|
460
|
+
if (typeof rawValue !== "string") return null;
|
|
461
|
+
const match = rawValue.trim().match(/^(\d{4}-\d{2}-\d{2})\/(\d{4}-\d{2}-\d{2})$/);
|
|
462
|
+
if (!match) return null;
|
|
463
|
+
|
|
464
|
+
const start = this.parseDate(match[1]);
|
|
465
|
+
const end = this.parseDate(match[2]);
|
|
466
|
+
if (!start || !end) return null;
|
|
467
|
+
|
|
468
|
+
return { start, end };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
isRangeComplete() {
|
|
472
|
+
return (
|
|
473
|
+
this.#startDate instanceof Date
|
|
474
|
+
&& !Number.isNaN(this.#startDate.getTime())
|
|
475
|
+
&& this.#endDate instanceof Date
|
|
476
|
+
&& !Number.isNaN(this.#endDate.getTime())
|
|
477
|
+
&& this.#endDate >= this.#startDate
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
formatDisplayDate(value) {
|
|
482
|
+
if (!(value instanceof Date) || Number.isNaN(value.getTime())) return "";
|
|
483
|
+
|
|
484
|
+
const scopedLang =
|
|
485
|
+
this.getAttribute("lang")
|
|
486
|
+
|| this.closest("[lang]")?.getAttribute("lang")
|
|
487
|
+
|| document.documentElement?.getAttribute("lang")
|
|
488
|
+
|| navigator.language
|
|
489
|
+
|| "en";
|
|
490
|
+
|
|
491
|
+
const preset = (this.displayFormat || DEFAULT_DISPLAY_FORMAT).trim();
|
|
492
|
+
const presetOptions = {
|
|
493
|
+
"d MMM yy": { day: "numeric", month: "short", year: "2-digit" },
|
|
494
|
+
"d MMM yyyy": { day: "numeric", month: "short", year: "numeric" },
|
|
495
|
+
"dd MMM yy": { day: "2-digit", month: "short", year: "2-digit" },
|
|
496
|
+
"short": { dateStyle: "short" },
|
|
497
|
+
"medium": { dateStyle: "medium" },
|
|
498
|
+
"long": { dateStyle: "long" },
|
|
499
|
+
"full": { dateStyle: "full" },
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
const options = presetOptions[preset] || presetOptions[DEFAULT_DISPLAY_FORMAT];
|
|
503
|
+
return new Intl.DateTimeFormat(scopedLang, options).format(value);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
buildRangeEventsForMonth(year, month) {
|
|
507
|
+
if (!this.isRangeComplete()) return {};
|
|
508
|
+
|
|
509
|
+
const map = {};
|
|
510
|
+
const monthStart = new Date(year, month, 1);
|
|
511
|
+
const monthEnd = new Date(year, month + 1, 0);
|
|
512
|
+
const rangeStart = this.toDayStart(this.#startDate);
|
|
513
|
+
const rangeEnd = this.toDayStart(this.#endDate);
|
|
514
|
+
|
|
515
|
+
const visibleStart = rangeStart > monthStart ? rangeStart : monthStart;
|
|
516
|
+
const visibleEnd = rangeEnd < monthEnd ? rangeEnd : monthEnd;
|
|
517
|
+
if (visibleStart > visibleEnd) return map;
|
|
518
|
+
|
|
519
|
+
for (
|
|
520
|
+
let cursor = new Date(visibleStart.getFullYear(), visibleStart.getMonth(), visibleStart.getDate());
|
|
521
|
+
cursor <= visibleEnd;
|
|
522
|
+
cursor.setDate(cursor.getDate() + 1)
|
|
523
|
+
) {
|
|
524
|
+
const day = cursor.getDate();
|
|
525
|
+
const isStart = cursor.getTime() === rangeStart.getTime();
|
|
526
|
+
const isEnd = cursor.getTime() === rangeEnd.getTime();
|
|
527
|
+
map[day] = [{
|
|
528
|
+
title: isStart ? "Departure" : isEnd ? "Return" : "Travel day",
|
|
529
|
+
type: isStart || isEnd ? "primary" : "info"
|
|
530
|
+
}];
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return map;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
applyRangeSelection(pickedDate) {
|
|
537
|
+
if (this.#startDate && this.#endDate) {
|
|
538
|
+
this.#startDate = pickedDate;
|
|
539
|
+
this.#endDate = null;
|
|
540
|
+
} else if (!this.#startDate) {
|
|
541
|
+
this.#startDate = pickedDate;
|
|
542
|
+
} else if (pickedDate < this.#startDate) {
|
|
543
|
+
this.#startDate = pickedDate;
|
|
544
|
+
this.#endDate = null;
|
|
545
|
+
} else {
|
|
546
|
+
this.#endDate = pickedDate;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
this.syncUI();
|
|
550
|
+
this.refreshHighlights();
|
|
551
|
+
this.updateFormState();
|
|
552
|
+
this.dispatchRangeChange();
|
|
553
|
+
|
|
554
|
+
if (this.isRangeComplete()) {
|
|
555
|
+
this.closePanel();
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
enforceMinimumGap() {
|
|
560
|
+
if (!this.leftCalendar || !this.rightCalendar) return;
|
|
561
|
+
if (!Number.isInteger(this.leftCalendar.year) || !Number.isInteger(this.leftCalendar.month)) return;
|
|
562
|
+
if (!Number.isInteger(this.rightCalendar.year) || !Number.isInteger(this.rightCalendar.month)) return;
|
|
563
|
+
|
|
564
|
+
const leftMonth = this.toMonthStart(new Date(this.leftCalendar.year, this.leftCalendar.month, 1));
|
|
565
|
+
const rightMonth = this.toMonthStart(new Date(this.rightCalendar.year, this.rightCalendar.month, 1));
|
|
566
|
+
const minimumRight = this.addMonths(leftMonth, 1);
|
|
567
|
+
if (rightMonth < minimumRight) {
|
|
568
|
+
this.setCalendarDate(this.rightCalendar, minimumRight);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
positionCalendars() {
|
|
573
|
+
if (!this.leftCalendar || !this.rightCalendar) return;
|
|
574
|
+
|
|
575
|
+
const baseMonth = this.#startDate
|
|
576
|
+
? this.toMonthStart(this.#startDate)
|
|
577
|
+
: this.toMonthStart(new Date());
|
|
578
|
+
|
|
579
|
+
this.setCalendarDate(this.leftCalendar, baseMonth);
|
|
580
|
+
|
|
581
|
+
const minimumRight = this.addMonths(baseMonth, 1);
|
|
582
|
+
if (this.#endDate) {
|
|
583
|
+
const endMonth = this.toMonthStart(this.#endDate);
|
|
584
|
+
this.setCalendarDate(this.rightCalendar, endMonth < minimumRight ? minimumRight : endMonth);
|
|
585
|
+
} else {
|
|
586
|
+
this.setCalendarDate(this.rightCalendar, minimumRight);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
refreshHighlights() {
|
|
591
|
+
this.leftCalendar?.refresh();
|
|
592
|
+
this.rightCalendar?.refresh();
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
syncUI() {
|
|
596
|
+
if (!this.triggerButton || !this.statusText) {
|
|
597
|
+
this.triggerButton = this.triggerButton || this.shadowRoot?.querySelector(".trigger");
|
|
598
|
+
this.statusText = this.statusText || this.shadowRoot?.querySelector("#status-text");
|
|
599
|
+
if (!this.triggerButton || !this.statusText) {
|
|
600
|
+
this.reflectValueAttribute();
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const complete = this.isRangeComplete();
|
|
606
|
+
|
|
607
|
+
if (complete) {
|
|
608
|
+
this.triggerButton.textContent = `${this.formatDisplayDate(this.#startDate)} - ${this.formatDisplayDate(this.#endDate)}`;
|
|
609
|
+
const oneDayMs = 24 * 60 * 60 * 1000;
|
|
610
|
+
const nights = Math.round((this.toDayStart(this.#endDate) - this.toDayStart(this.#startDate)) / oneDayMs);
|
|
611
|
+
this.statusText.textContent = `${nights} night${nights === 1 ? "" : "s"} selected`;
|
|
612
|
+
} else if (this.#startDate) {
|
|
613
|
+
this.triggerButton.textContent = `${this.formatDisplayDate(this.#startDate)} - -`;
|
|
614
|
+
this.statusText.textContent = "Select checkout date";
|
|
615
|
+
} else {
|
|
616
|
+
this.triggerButton.textContent = "- - -";
|
|
617
|
+
this.statusText.textContent = "Select check-in and checkout dates";
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
this.reflectValueAttribute();
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
reflectValueAttribute() {
|
|
624
|
+
const nextValue = this.value;
|
|
625
|
+
this.#syncingValueAttribute = true;
|
|
626
|
+
if (nextValue) this.setAttribute("value", nextValue);
|
|
627
|
+
else this.removeAttribute("value");
|
|
628
|
+
this.#syncingValueAttribute = false;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
applyDisabledState() {
|
|
632
|
+
const disabled = this.disabled;
|
|
633
|
+
if (this.triggerButton) this.triggerButton.disabled = disabled;
|
|
634
|
+
if (this.clearButton) this.clearButton.disabled = disabled;
|
|
635
|
+
if (this.leftCalendar) this.leftCalendar.disabled = disabled;
|
|
636
|
+
if (this.rightCalendar) this.rightCalendar.disabled = disabled;
|
|
637
|
+
if (disabled) this.closePanel();
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
updateFormState() {
|
|
641
|
+
if (!this.#internals) return;
|
|
642
|
+
|
|
643
|
+
const fieldValue = this.value;
|
|
644
|
+
|
|
645
|
+
if (!this.name || this.disabled) {
|
|
646
|
+
this.#internals.setFormValue(null);
|
|
647
|
+
this.#internals.setValidity({});
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
this.#internals.setFormValue(fieldValue);
|
|
652
|
+
|
|
653
|
+
if (this.required && !fieldValue) {
|
|
654
|
+
this.#internals.setValidity(
|
|
655
|
+
{ valueMissing: true },
|
|
656
|
+
"Please select a start and end date.",
|
|
657
|
+
this.triggerButton
|
|
658
|
+
);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
this.#internals.setValidity({});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
dispatchRangeChange() {
|
|
666
|
+
this.dispatchEvent(
|
|
667
|
+
new CustomEvent("range-change", {
|
|
668
|
+
detail: {
|
|
669
|
+
startDate: this.startDate,
|
|
670
|
+
endDate: this.endDate,
|
|
671
|
+
value: this.value,
|
|
672
|
+
complete: this.isRangeComplete(),
|
|
673
|
+
},
|
|
674
|
+
bubbles: true,
|
|
675
|
+
composed: true,
|
|
676
|
+
})
|
|
677
|
+
);
|
|
678
|
+
|
|
679
|
+
this.dispatchEvent(new Event("change", { bubbles: true, composed: true }));
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
customElements.define("pds-daterange", PdsDateRange);
|