@aquera/nile-elements 2.0.19 → 2.0.20

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.
Files changed (87) hide show
  1. package/README.md +5 -0
  2. package/demo/index.js +0 -1
  3. package/dist/index.cjs.js +1 -1
  4. package/dist/index.esm.js +1 -1
  5. package/dist/index.js +1254 -488
  6. package/dist/nile-badge/index.cjs.js +1 -1
  7. package/dist/nile-badge/index.esm.js +1 -1
  8. package/dist/nile-badge/nile-badge.cjs.js +1 -1
  9. package/dist/nile-badge/nile-badge.cjs.js.map +1 -1
  10. package/dist/nile-badge/nile-badge.css.cjs.js +1 -1
  11. package/dist/nile-badge/nile-badge.css.cjs.js.map +1 -1
  12. package/dist/nile-badge/nile-badge.css.esm.js +24 -3
  13. package/dist/nile-badge/nile-badge.esm.js +5 -3
  14. package/dist/nile-calendar/index.cjs.js +1 -1
  15. package/dist/nile-calendar/index.esm.js +1 -1
  16. package/dist/nile-calendar/nile-calendar.cjs.js +2 -1
  17. package/dist/nile-calendar/nile-calendar.cjs.js.map +1 -1
  18. package/dist/nile-calendar/nile-calendar.css.cjs.js +1 -1
  19. package/dist/nile-calendar/nile-calendar.css.cjs.js.map +1 -1
  20. package/dist/nile-calendar/nile-calendar.css.esm.js +357 -2
  21. package/dist/nile-calendar/nile-calendar.esm.js +365 -41
  22. package/dist/nile-calendar/timezones.cjs.js +1 -1
  23. package/dist/nile-calendar/timezones.cjs.js.map +1 -1
  24. package/dist/nile-calendar/timezones.esm.js +1 -1
  25. package/dist/nile-date-time-picker/index.cjs.js +2 -0
  26. package/dist/nile-date-time-picker/index.cjs.js.map +1 -0
  27. package/dist/nile-date-time-picker/index.esm.js +1 -0
  28. package/dist/nile-date-time-picker/nile-date-time-picker.cjs.js +2 -0
  29. package/dist/nile-date-time-picker/nile-date-time-picker.cjs.js.map +1 -0
  30. package/dist/nile-date-time-picker/nile-date-time-picker.css.cjs.js +2 -0
  31. package/dist/nile-date-time-picker/nile-date-time-picker.css.cjs.js.map +1 -0
  32. package/dist/nile-date-time-picker/nile-date-time-picker.css.esm.js +13 -0
  33. package/dist/nile-date-time-picker/nile-date-time-picker.esm.js +53 -0
  34. package/dist/src/index.d.ts +1 -0
  35. package/dist/src/index.js +1 -0
  36. package/dist/src/index.js.map +1 -1
  37. package/dist/src/nile-badge/nile-badge.css.js +24 -3
  38. package/dist/src/nile-badge/nile-badge.css.js.map +1 -1
  39. package/dist/src/nile-badge/nile-badge.d.ts +7 -4
  40. package/dist/src/nile-badge/nile-badge.js +22 -5
  41. package/dist/src/nile-badge/nile-badge.js.map +1 -1
  42. package/dist/src/nile-badge/nile-badge.test.js +88 -0
  43. package/dist/src/nile-badge/nile-badge.test.js.map +1 -1
  44. package/dist/src/nile-calendar/nile-calendar.css.js +355 -0
  45. package/dist/src/nile-calendar/nile-calendar.css.js.map +1 -1
  46. package/dist/src/nile-calendar/nile-calendar.d.ts +175 -0
  47. package/dist/src/nile-calendar/nile-calendar.js +1726 -26
  48. package/dist/src/nile-calendar/nile-calendar.js.map +1 -1
  49. package/dist/src/nile-calendar/nile-calendar.test.d.ts +1 -0
  50. package/dist/src/nile-calendar/nile-calendar.test.js +3580 -0
  51. package/dist/src/nile-calendar/nile-calendar.test.js.map +1 -1
  52. package/dist/src/nile-calendar/timezones.d.ts +21 -0
  53. package/dist/src/nile-calendar/timezones.js +49 -0
  54. package/dist/src/nile-calendar/timezones.js.map +1 -1
  55. package/dist/src/nile-date-time-picker/index.d.ts +1 -0
  56. package/dist/src/nile-date-time-picker/index.js +2 -0
  57. package/dist/src/nile-date-time-picker/index.js.map +1 -0
  58. package/dist/src/nile-date-time-picker/nile-date-time-picker.css.d.ts +11 -0
  59. package/dist/src/nile-date-time-picker/nile-date-time-picker.css.js +49 -0
  60. package/dist/src/nile-date-time-picker/nile-date-time-picker.css.js.map +1 -0
  61. package/dist/src/nile-date-time-picker/nile-date-time-picker.d.ts +173 -0
  62. package/dist/src/nile-date-time-picker/nile-date-time-picker.js +1270 -0
  63. package/dist/src/nile-date-time-picker/nile-date-time-picker.js.map +1 -0
  64. package/dist/src/nile-date-time-picker/nile-date-time-picker.test.d.ts +143 -0
  65. package/dist/src/nile-date-time-picker/nile-date-time-picker.test.js +7277 -0
  66. package/dist/src/nile-date-time-picker/nile-date-time-picker.test.js.map +1 -0
  67. package/dist/src/version.js +1 -1
  68. package/dist/src/version.js.map +1 -1
  69. package/dist/tsconfig.tsbuildinfo +1 -1
  70. package/dist/utilities/animation-registry.cjs.js +1 -1
  71. package/dist/utilities/animation-registry.cjs.js.map +1 -1
  72. package/dist/utilities/animation-registry.esm.js +1 -1
  73. package/package.json +3 -1
  74. package/scripts/coverage-report.mjs +96 -0
  75. package/src/index.ts +1 -0
  76. package/src/nile-badge/nile-badge.css.ts +24 -3
  77. package/src/nile-badge/nile-badge.test.ts +102 -0
  78. package/src/nile-badge/nile-badge.ts +28 -5
  79. package/src/nile-calendar/nile-calendar.css.ts +355 -0
  80. package/src/nile-calendar/nile-calendar.test.ts +3793 -0
  81. package/src/nile-calendar/nile-calendar.ts +1760 -28
  82. package/src/nile-calendar/timezones.ts +57 -0
  83. package/src/nile-date-time-picker/index.ts +1 -0
  84. package/src/nile-date-time-picker/nile-date-time-picker.css.ts +54 -0
  85. package/src/nile-date-time-picker/nile-date-time-picker.test.ts +7886 -0
  86. package/src/nile-date-time-picker/nile-date-time-picker.ts +1153 -0
  87. package/vscode-html-custom-data.json +161 -6
@@ -0,0 +1,1270 @@
1
+ /**
2
+ * Copyright Aquera Inc 2026
3
+ *
4
+ * This source code is licensed under the BSD-3-Clause license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+ var NileDateTimePicker_1;
8
+ import { __decorate } from "tslib";
9
+ import { html } from 'lit';
10
+ import { customElement, property, query } from 'lit/decorators.js';
11
+ import { ifDefined } from 'lit/directives/if-defined.js';
12
+ import { createRef, ref } from 'lit/directives/ref.js';
13
+ import { styles } from './nile-date-time-picker.css';
14
+ import NileElement from '../internal/nile-element';
15
+ import '../nile-floating-panel';
16
+ import '../nile-calendar';
17
+ import '../nile-input';
18
+ import { isValidTimezone, utcFromFields, localDateFromFields, DEFAULT_START_YEAR, DEFAULT_END_YEAR } from '../nile-calendar/timezones';
19
+ import { instantFromValueString, wallClockFromOffsetLess, wallClockInZone } from '../nile-calendar/nile-calendar';
20
+ /** Accessible name for the trigger input when a consumer sets neither `label` nor `aria-label`. */
21
+ const DEFAULT_TRIGGER_LABEL = 'Date and time';
22
+ /**
23
+ * @tag nile-date-time-picker
24
+ * @event nile-change - Absolute `{ value, localTimezoneValue, unixEpoch, timezone }` (`unixEpoch` = ms since 1970, null when empty); relative `{ startDate, endDate, timezone }`.
25
+ * @event nile-clear
26
+ */
27
+ let NileDateTimePicker = NileDateTimePicker_1 = class NileDateTimePicker extends NileElement {
28
+ constructor() {
29
+ super(...arguments);
30
+ this.showMonthDropdown = false;
31
+ this.showYearDropdown = false;
32
+ this.type = 'absolute';
33
+ /** Show the Absolute / Relative type switcher. Hidden by default; set to reveal it. */
34
+ this.showTypes = false;
35
+ this.hideDurationFields = [];
36
+ this.hideTimeFields = [];
37
+ /** Selected date/time for the inner `nile-calendar`: an instant, an offset-less wall clock read in `timezone`, the bracketed form, or date-only — all different. `unixEpoch` is the authoritative instant. */
38
+ this.value = null;
39
+ this.timezone = 'UTC';
40
+ this.allowedDates = JSON.stringify({});
41
+ this.dateFormat = 'DD/MM/YY';
42
+ this.placeholder = '';
43
+ this.label = '';
44
+ this.accessibleLabel = '';
45
+ this.doubleClickUnselect = false;
46
+ this.disabled = false;
47
+ this.open = false;
48
+ this.calendarRef = createRef();
49
+ this._openSyncFromPanel = false;
50
+ this._skipNextSeed = false;
51
+ /** true once the user has typed/pasted/deleted in the mask since the last seed, so only user edits are clamped to allowedDates */
52
+ this._maskEdited = false;
53
+ /** the zone the consumer configured (first render, or any later external change), restored on Clear instead of forcing UTC */
54
+ this._defaultTimezone = 'UTC';
55
+ /** the last zone this component assigned itself, so `updated` can tell it apart from a consumer change */
56
+ this._internalTimezone = null;
57
+ /** keep `open` in sync with the panel's own show/hide (click, outside-click, Escape) */
58
+ /** `performance.now()` of an Escape pressed while focus was inside the panel; 0 when there is none to honour */
59
+ this._escapeFromPanelAt = 0;
60
+ /**
61
+ * The panel hides on a document-level Escape and drops focus to <body> when the focused control disappears with it.
62
+ * Runs in the capture phase, before the panel's own handler, to note whether focus was inside the panel; onPanelHide
63
+ * then hands focus back to the trigger. Escape with focus anywhere else leaves focus where it is.
64
+ */
65
+ this.onEscapeKeydown = (e) => {
66
+ if (e.key === 'Tab') {
67
+ this.onPanelTab(e);
68
+ return;
69
+ }
70
+ if (e.key !== 'Escape' || !this.open)
71
+ return;
72
+ this._escapeFromPanelAt = this.focusIsInPanel() ? performance.now() : 0;
73
+ };
74
+ /** ArrowDown / Alt+ArrowDown on the field opens the panel and moves focus into it. Enter, Space and typing are untouched. */
75
+ this.onTriggerKeydown = (e) => {
76
+ if (e.key !== 'ArrowDown' || e.defaultPrevented || e.ctrlKey || e.metaKey || e.shiftKey)
77
+ return;
78
+ if (this.disabled || this.open)
79
+ return;
80
+ e.preventDefault();
81
+ this.panel?.show?.()?.then(() => this.focusIntoPanel()).catch(() => { });
82
+ };
83
+ this.onPanelShow = (e) => {
84
+ e.stopPropagation();
85
+ this.applyDialogName();
86
+ document.addEventListener('keydown', this.onEscapeKeydown, true);
87
+ this.syncOpenFromPanel(true);
88
+ if (this._showCommittedOnOpen)
89
+ this.showSelectedMonth();
90
+ this._showCommittedOnOpen = false;
91
+ };
92
+ /** set by Apply: the next open lands on the committed date, not on whatever year/month was browsed before it */
93
+ this._showCommittedOnOpen = false;
94
+ this.onPanelHide = (e) => {
95
+ e.stopPropagation();
96
+ document.removeEventListener('keydown', this.onEscapeKeydown, true);
97
+ this.syncOpenFromPanel(false);
98
+ const fromPanel = this._escapeFromPanelAt && performance.now() - this._escapeFromPanelAt < 1000;
99
+ this._escapeFromPanelAt = 0;
100
+ if (fromPanel)
101
+ this.triggerInput?.focus?.();
102
+ };
103
+ this.maskValues = [];
104
+ this.maskSuffix = '';
105
+ /** the slot values as last seeded/committed from `value`; blur clamps only segments that differ from it (the user's edits) */
106
+ this.seedMask = null;
107
+ /** the seeded value's full year, set wherever `seedMask` is: a `YY` mask holds only its last two digits */
108
+ this.seedYear = null;
109
+ /** set when a YY year with no valid two-digit form was blanked; until a year is typed (or the mask is reseeded) the calendar keeps no selection built from day and month alone */
110
+ this._yearRejected = false;
111
+ this.onMaskBlur = () => {
112
+ if (this.type !== 'absolute')
113
+ return;
114
+ const nileInput = this.triggerInput;
115
+ const native = nileInput?.input;
116
+ if (!native)
117
+ return;
118
+ const layout = this.maskLayout();
119
+ if (this.maskValues.length !== layout.slots.length)
120
+ this.initMaskState(native.value);
121
+ if (!this.maskValues.some(v => v !== '')) {
122
+ native.value = '';
123
+ nileInput.value = '';
124
+ return;
125
+ }
126
+ const segStarts = [...new Set(layout.slots.map(s => s.segStart))];
127
+ for (const start of segStarts) {
128
+ const idxs = layout.slots.map((s, i) => (s.segStart === start ? i : -1)).filter(i => i !== -1);
129
+ const digits = idxs.map(i => this.maskValues[i]).filter(c => c !== '');
130
+ if (digits.length > 0 && digits.length < idxs.length) {
131
+ const padded = digits.join('').padStart(idxs.length, '0');
132
+ idxs.forEach((i, k) => { this.maskValues[i] = padded[k]; });
133
+ }
134
+ }
135
+ // a seeded value is not a user edit: clamping it to startYear/endYear, or re-reading a YY year as 20xx into the calendar, would diverge from `value`
136
+ const edited = this._maskEdited;
137
+ if (edited) {
138
+ // only segments the user changed since the seed: an untouched out-of-range year must survive an edit to another segment
139
+ const seed = this.seedMask?.length === this.maskValues.length ? this.seedMask : null;
140
+ segStarts.forEach(start => {
141
+ const touched = !seed
142
+ || layout.slots.some((sl, i) => sl.segStart === start && this.maskValues[i] !== seed[i]);
143
+ if (touched)
144
+ this.clampSegment(this.maskValues, layout, start);
145
+ });
146
+ this.clampDay(this.maskValues, layout);
147
+ }
148
+ const { display } = this.computeDisplay(this.maskValues, this.maskSuffix, layout);
149
+ native.value = display;
150
+ nileInput.value = display;
151
+ if (edited)
152
+ this.syncCalendarFromMask();
153
+ this.seedMask = [...this.maskValues];
154
+ this._maskEdited = false;
155
+ };
156
+ this.onMaskPaste = (e) => {
157
+ if (this.type !== 'absolute' || this.disabled)
158
+ return;
159
+ const nileInput = this.triggerInput;
160
+ const native = nileInput?.input;
161
+ if (!native)
162
+ return;
163
+ const text = e.clipboardData?.getData('text') ?? '';
164
+ e.preventDefault();
165
+ if (!text)
166
+ return;
167
+ const selStart = native.selectionStart ?? native.value.length;
168
+ const selEnd = native.selectionEnd ?? selStart;
169
+ const layout = this.maskLayout();
170
+ if (this.maskValues.length !== layout.slots.length)
171
+ this.initMaskState(native.value);
172
+ const { slotPos } = this.computeDisplay(this.maskValues, this.maskSuffix, layout);
173
+ // slot index the caret/selection starts at
174
+ let first = slotPos.findIndex(pos => pos !== -1 && pos >= selStart);
175
+ if (first === -1)
176
+ first = this.maskValues.findIndex(v => v === '');
177
+ if (first === -1)
178
+ first = 0;
179
+ let last = first;
180
+ slotPos.forEach((pos, i) => { if (pos !== -1 && pos < selEnd)
181
+ last = Math.max(last, i + 1); });
182
+ const digits = text.replace(/\D/g, '');
183
+ if (!digits)
184
+ return;
185
+ this._maskEdited = true;
186
+ const values = [...this.maskValues];
187
+ if (selEnd > selStart)
188
+ for (let i = first; i < Math.min(last, values.length); i++)
189
+ values[i] = '';
190
+ for (let k = 0; k < digits.length && first + k < values.length; k++)
191
+ values[first + k] = digits[k];
192
+ this.maskValues = values;
193
+ const segStarts = [...new Set(layout.slots.map(s => s.segStart))];
194
+ segStarts.forEach(start => this.clampSegment(this.maskValues, layout, start));
195
+ this.clampDay(this.maskValues, layout);
196
+ const out = this.computeDisplay(this.maskValues, this.maskSuffix, layout).display;
197
+ this.writeMask(nileInput, native, out, out.length);
198
+ this.syncCalendarFromMask();
199
+ };
200
+ /** Covers cut, undo/redo, drag-drop, IME and soft-keyboard input, none of which arrive as a usable keydown. */
201
+ this.onMaskBeforeInput = (e) => {
202
+ if (this.type !== 'absolute' || this.disabled)
203
+ return;
204
+ // digits typed on a desktop keyboard are handled by onMaskKeydown (which preventDefaults them)
205
+ const t = e.inputType;
206
+ if (t === 'insertFromPaste')
207
+ return; // handled by onMaskPaste
208
+ const nileInput = this.triggerInput;
209
+ const native = nileInput?.input;
210
+ if (!native)
211
+ return;
212
+ const handled = ['insertText', 'insertCompositionText', 'insertReplacementText', 'insertFromDrop',
213
+ 'deleteByCut', 'deleteByDrag', 'deleteContentBackward', 'deleteContentForward', 'historyUndo', 'historyRedo'];
214
+ if (!handled.includes(t))
215
+ return;
216
+ // let the browser apply the edit, then rebuild the slots from the resulting text
217
+ setTimeout(() => this.reflowMaskFromNative(nileInput, native), 0);
218
+ };
219
+ this.onMaskKeydown = (e) => {
220
+ if (this.type !== 'absolute')
221
+ return;
222
+ if (this.disabled || e.ctrlKey || e.metaKey || e.altKey)
223
+ return;
224
+ const nav = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End',
225
+ 'Tab', 'Enter', 'Shift', 'Control', 'Meta', 'Alt', 'CapsLock'];
226
+ if (nav.includes(e.key))
227
+ return;
228
+ const nileInput = this.triggerInput;
229
+ const native = nileInput?.input;
230
+ if (!native)
231
+ return;
232
+ const layout = this.maskLayout();
233
+ if (this.maskValues.length !== layout.slots.length)
234
+ this.initMaskState(native.value);
235
+ const values = this.maskValues;
236
+ const { slotPos } = this.computeDisplay(values, this.maskSuffix, layout);
237
+ const selStart = native.selectionStart ?? 0;
238
+ const selEnd = native.selectionEnd ?? selStart;
239
+ // caret position to place after editing slot `target`
240
+ const caretAfter = (target) => {
241
+ const after = this.computeDisplay(values, this.maskSuffix, layout);
242
+ const np = after.slotPos[target + 1];
243
+ if (np !== undefined && np !== -1)
244
+ return np;
245
+ const suffixLen = after.lastFilled === layout.slots.length - 1 ? this.maskSuffix.length : 0;
246
+ return after.display.length - suffixLen;
247
+ };
248
+ const commit = (target, sync = true) => {
249
+ this.writeMask(nileInput, native, this.computeDisplay(values, this.maskSuffix, layout).display, caretAfter(target));
250
+ if (sync)
251
+ this.syncCalendarFromMask(); // reflect a complete manual edit back into the panel
252
+ };
253
+ const full = this.computeDisplay(values, this.maskSuffix, layout).display;
254
+ const timeEndPos = full.length - this.maskSuffix.length;
255
+ if ((e.key === 'Backspace' || e.key === 'Delete') && this.maskSuffix && selStart > timeEndPos) {
256
+ e.preventDefault();
257
+ this.maskSuffix = '';
258
+ const after = this.computeDisplay(values, this.maskSuffix, layout);
259
+ this.writeMask(nileInput, native, after.display, after.display.length);
260
+ this.syncCalendarFromMask();
261
+ this._skipNextSeed = true;
262
+ this.setTimezoneInternally('UTC');
263
+ if (this.value) {
264
+ this.value = this.value.replace(/\[GMT[+-]\d{2}:\d{2}\s+[^\]]+\]$/, '[GMT+00:00 UTC]');
265
+ }
266
+ this.updateComplete
267
+ .then(() => { this._skipNextSeed = false; })
268
+ .catch(() => { this._skipNextSeed = false; });
269
+ const cal = this.calendarRef.value;
270
+ if (cal) {
271
+ cal.timezone = 'UTC';
272
+ cal.updateComplete
273
+ ?.then(() => cal.syncInnerCalendar?.())
274
+ .catch(() => { });
275
+ }
276
+ return;
277
+ }
278
+ if ((e.key >= '0' && e.key <= '9') || e.key === 'Backspace' || e.key === 'Delete')
279
+ this._maskEdited = true;
280
+ if (e.key >= '0' && e.key <= '9') {
281
+ e.preventDefault();
282
+ let target = slotPos.findIndex(p => p !== -1 && p >= selStart);
283
+ if (target === -1)
284
+ target = values.findIndex(v => v === ''); // append at the first gap
285
+ if (target === -1)
286
+ return; // every slot full and caret past the end
287
+ values[target] = e.key;
288
+ const typed = layout.slots[target];
289
+ if (typed.kind === 'year') {
290
+ // overtyping a seeded year leaves every slot filled while it is still half old, half new: the year is only
291
+ // complete once its last digit lands, so range-clamping it (or the day against it) earlier would act on a number nobody typed
292
+ const last = layout.slots.reduce((m, s, i) => (s.segStart === typed.segStart ? i : m), target);
293
+ if (target !== last) {
294
+ commit(target, false);
295
+ return;
296
+ }
297
+ }
298
+ else {
299
+ this.settleYear(values, layout);
300
+ }
301
+ this.clampSegment(values, layout, target);
302
+ this.clampDay(values, layout);
303
+ commit(target);
304
+ return;
305
+ }
306
+ if (e.key === 'Backspace') {
307
+ e.preventDefault();
308
+ if (selStart !== selEnd) {
309
+ for (let i = 0; i < values.length; i++) {
310
+ if (slotPos[i] !== -1 && slotPos[i] >= selStart && slotPos[i] < selEnd)
311
+ values[i] = '';
312
+ }
313
+ this.writeMask(nileInput, native, this.computeDisplay(values, this.maskSuffix, layout).display, selStart);
314
+ return;
315
+ }
316
+ let target = -1;
317
+ for (let i = 0; i < values.length; i++) {
318
+ if (values[i] !== '' && slotPos[i] !== -1 && slotPos[i] < selStart)
319
+ target = i;
320
+ }
321
+ if (target === -1)
322
+ return;
323
+ values[target] = '';
324
+ const after = this.computeDisplay(values, this.maskSuffix, layout);
325
+ this.writeMask(nileInput, native, after.display, after.slotPos[target] !== -1 ? after.slotPos[target] : Math.max(0, selStart - 1));
326
+ return;
327
+ }
328
+ if (e.key === 'Delete') {
329
+ e.preventDefault();
330
+ let target = -1;
331
+ for (let i = 0; i < values.length; i++) {
332
+ if (values[i] !== '' && slotPos[i] !== -1 && slotPos[i] >= selStart) {
333
+ target = i;
334
+ break;
335
+ }
336
+ }
337
+ if (target === -1)
338
+ return;
339
+ values[target] = '';
340
+ const after = this.computeDisplay(values, this.maskSuffix, layout);
341
+ this.writeMask(nileInput, native, after.display, after.slotPos[target] !== -1 ? after.slotPos[target] : selStart);
342
+ return;
343
+ }
344
+ if (e.key.length === 1)
345
+ e.preventDefault();
346
+ };
347
+ }
348
+ static get styles() {
349
+ return [styles];
350
+ }
351
+ get structuredInput() { return true; }
352
+ setTimezoneInternally(tz) {
353
+ this._internalTimezone = tz;
354
+ this.timezone = tz;
355
+ }
356
+ get resolvedTimeFields() {
357
+ const hidden = Array.isArray(this.hideTimeFields) ? this.hideTimeFields : [];
358
+ const keys = ['hours', 'minutes', 'seconds', 'milliseconds', 'timezone'];
359
+ const out = {};
360
+ for (const k of keys)
361
+ out[k] = !hidden.includes(k);
362
+ return out;
363
+ }
364
+ get shownTimeFields() {
365
+ const f = this.resolvedTimeFields;
366
+ return NileDateTimePicker_1.TIME_SPEC.filter(t => f[t.key] !== false);
367
+ }
368
+ timeSep(kind) {
369
+ return kind === 'SSS' ? '.' : ':';
370
+ }
371
+ get showTimezone() {
372
+ return this.resolvedTimeFields.timezone !== false;
373
+ }
374
+ get defaultPlaceholder() {
375
+ const time = this.shownTimeFields;
376
+ let s = this.dateFormat || 'DD/MM/YY';
377
+ if (time.length) {
378
+ const timeStr = time
379
+ .map((t, i) => (i > 0 ? this.timeSep(t.kind) : '') + t.ph)
380
+ .join('');
381
+ s += ` - ${timeStr}`;
382
+ }
383
+ if (this.showTimezone)
384
+ s += ' - timezone';
385
+ return s;
386
+ }
387
+ get triggerInput() {
388
+ return this.shadowRoot?.querySelector('nile-input') ?? null;
389
+ }
390
+ get calendarElement() {
391
+ return this.calendarRef.value ?? null;
392
+ }
393
+ connectedCallback() {
394
+ super.connectedCallback();
395
+ // firstUpdated runs once; a re-attach (DOM move) must restore the mask listeners disconnectedCallback removed
396
+ if (this.hasUpdated)
397
+ this.bindStructuredInput();
398
+ this.emit('nile-init');
399
+ }
400
+ disconnectedCallback() {
401
+ super.disconnectedCallback();
402
+ document.removeEventListener('keydown', this.onEscapeKeydown, true);
403
+ const nileInput = this.triggerInput;
404
+ nileInput?.removeEventListener('keydown', this.onMaskKeydown, true);
405
+ nileInput?.removeEventListener('focusout', this.onMaskBlur);
406
+ nileInput?.removeEventListener('paste', this.onMaskPaste, true);
407
+ nileInput?.removeEventListener('beforeinput', this.onMaskBeforeInput, true);
408
+ nileInput?.removeEventListener('keydown', this.onTriggerKeydown);
409
+ this.emit('nile-destroy');
410
+ }
411
+ syncOpenFromPanel(open) {
412
+ if (this.open === open)
413
+ return;
414
+ this._openSyncFromPanel = true;
415
+ this.open = open;
416
+ this.updateComplete
417
+ .then(() => { this._openSyncFromPanel = false; })
418
+ .catch(() => { this._openSyncFromPanel = false; });
419
+ }
420
+ /** the element that really holds focus, however deep in shadow roots */
421
+ deepActiveElement() {
422
+ let active = document.activeElement;
423
+ while (active?.shadowRoot?.activeElement)
424
+ active = active.shadowRoot.activeElement;
425
+ return active;
426
+ }
427
+ /** tab stops inside the panel in DOM order, looking through shadow roots (the panel is portaled to <body>) */
428
+ panelTabbables() {
429
+ const cal = this.calendarRef.value;
430
+ if (!cal)
431
+ return [];
432
+ const out = [];
433
+ const walk = (node) => {
434
+ for (const child of Array.from(node.children)) {
435
+ if (child.hidden || child.hasAttribute('inert'))
436
+ continue;
437
+ if (child.tabIndex >= 0 && !child.matches(':disabled') && child.getClientRects().length > 0
438
+ && getComputedStyle(child).visibility !== 'hidden')
439
+ out.push(child);
440
+ if (child.shadowRoot)
441
+ walk(child.shadowRoot);
442
+ walk(child);
443
+ }
444
+ };
445
+ if (cal.shadowRoot)
446
+ walk(cal.shadowRoot);
447
+ return out;
448
+ }
449
+ /** the panel's way in: the day that owns the grid's tab stop, else the first control */
450
+ focusIntoPanel() {
451
+ const tabbables = this.panelTabbables();
452
+ (tabbables.find(el => el.classList.contains('day_date')) ?? tabbables[0])?.focus();
453
+ }
454
+ /**
455
+ * The panel is portaled to the end of <body>, so the browser's own Tab order skips it (Chrome, Edge, Firefox) or
456
+ * lands in it from the wrong side. While it is open Tab from the field enters it at its first control, Shift+Tab from its first control
457
+ * returns to the field, and Tab from its last control hands focus back to the field so the browser's default move
458
+ * continues to the next page element (the panel then closes: it has been left). No trap: Shift+Tab from the field and
459
+ * every Tab outside the panel behave as usual.
460
+ */
461
+ onPanelTab(e) {
462
+ if (!this.open || e.ctrlKey || e.metaKey || e.altKey)
463
+ return;
464
+ const native = this.triggerInput?.input;
465
+ const active = this.deepActiveElement();
466
+ if (!native || !active)
467
+ return;
468
+ if (active === native) {
469
+ if (e.shiftKey)
470
+ return;
471
+ const tabbables = this.panelTabbables();
472
+ if (!tabbables.length)
473
+ return;
474
+ e.preventDefault();
475
+ tabbables[0].focus();
476
+ return;
477
+ }
478
+ const tabbables = this.panelTabbables();
479
+ const at = tabbables.indexOf(active);
480
+ if (at === -1)
481
+ return;
482
+ if (e.shiftKey && at === 0) {
483
+ e.preventDefault();
484
+ native.focus();
485
+ }
486
+ else if (!e.shiftKey && at === tabbables.length - 1) {
487
+ native.focus(); // no preventDefault: the default Tab now moves on from the field
488
+ this.panel?.hide?.();
489
+ }
490
+ }
491
+ focusIsInPanel() {
492
+ const cal = this.calendarRef.value;
493
+ const active = document.activeElement;
494
+ if (!cal || !active)
495
+ return false;
496
+ const popper = cal.closest('.nile-dtp-panel');
497
+ return active === cal || cal.contains(active) || (!!popper && (popper === active || popper.contains(active)));
498
+ }
499
+ /**
500
+ * nile-floating-panel creates the `role="dialog"` container itself, portals it out of this shadow root and gives it no
501
+ * name. Name it here after the field it belongs to, so the shared panel (and nile-context-menu) stays untouched.
502
+ */
503
+ applyDialogName() {
504
+ const dialog = this.calendarRef.value?.closest('[role="dialog"]');
505
+ if (!dialog)
506
+ return;
507
+ const field = this.label.trim() || this.accessibleLabel.trim() || DEFAULT_TRIGGER_LABEL;
508
+ dialog.setAttribute('aria-label', `${field} picker`);
509
+ }
510
+ /**
511
+ * The month/year dropdowns only navigate, so after Apply the view can be left on a browsed month. Reopening then shows the
512
+ * committed date's month/year. Dismissing without Apply (Escape, outside click) never takes this path: the view and any
513
+ * pending selection stay as the user left them. With no selection the view is left alone.
514
+ */
515
+ showSelectedMonth() {
516
+ const cal = this.calendarRef.value;
517
+ const selected = this.type === 'absolute' ? cal?.value : null;
518
+ if (!(selected instanceof Date) || isNaN(selected.getTime()))
519
+ return;
520
+ if (cal.currentYear === selected.getFullYear() && cal.currentMonth === selected.getMonth())
521
+ return;
522
+ cal.currentYear = selected.getFullYear();
523
+ cal.currentMonth = selected.getMonth();
524
+ cal.updateComplete
525
+ ?.then(() => cal.syncInnerCalendar?.())
526
+ .catch(() => { });
527
+ }
528
+ willUpdate(changedProperties) {
529
+ super.willUpdate?.(changedProperties);
530
+ if (!this.hasUpdated)
531
+ this._defaultTimezone = this.timezone;
532
+ }
533
+ firstUpdated(changedProperties) {
534
+ super.firstUpdated?.(changedProperties);
535
+ this.bindStructuredInput();
536
+ this.seedTriggerFromValue();
537
+ this.applyTriggerAriaLabel();
538
+ }
539
+ updated(changedProperties) {
540
+ super.updated(changedProperties);
541
+ if (changedProperties.has('timezone')) {
542
+ if (this.timezone !== this._internalTimezone)
543
+ this._defaultTimezone = this.timezone;
544
+ this._internalTimezone = null;
545
+ }
546
+ // a live change to the display layout (not the first render) leaves the trigger text in the old layout; drop any draft and redraw from the committed value
547
+ const layoutChanged = ['dateFormat', 'hideTimeFields']
548
+ .some(p => changedProperties.has(p) && changedProperties.get(p) !== undefined);
549
+ // switching back to Absolute must not leave Relative text in the trigger; same redraw, from the retained committed absolute `value` (or empty)
550
+ const typeChanged = changedProperties.has('type') && changedProperties.get('type') !== undefined;
551
+ if ((layoutChanged || typeChanged) && this.type === 'absolute') {
552
+ this._skipNextSeed = false;
553
+ this._maskEdited = false;
554
+ if (this.value)
555
+ this.seedTriggerFromValue();
556
+ else
557
+ this.clearTriggerText(true);
558
+ }
559
+ else if (changedProperties.has('value') && !this.value && changedProperties.get('value')) {
560
+ // seedTriggerFromValue ignores an empty value, so a programmatic reset must clear the text itself
561
+ this.clearTriggerText();
562
+ }
563
+ else if (changedProperties.has('value') || changedProperties.has('timezone')) {
564
+ this.seedTriggerFromValue();
565
+ }
566
+ if (changedProperties.has('open') && !this._openSyncFromPanel) {
567
+ if (this.open)
568
+ this.panel?.show?.();
569
+ else
570
+ this.panel?.hide?.();
571
+ }
572
+ if (changedProperties.has('accessibleLabel') || changedProperties.has('label')) {
573
+ this.applyTriggerAriaLabel();
574
+ this.applyDialogName();
575
+ }
576
+ }
577
+ applyTriggerAriaLabel() {
578
+ const nileInput = this.triggerInput;
579
+ if (!nileInput) {
580
+ this.updateComplete
581
+ .then(() => { if (this.triggerInput)
582
+ this.applyTriggerAriaLabel(); })
583
+ .catch(() => { });
584
+ return;
585
+ }
586
+ // A visible `label` names the field on its own, so `aria-label` stays off; with neither set, fall back to a built-in name (the placeholder describes the FORMAT, not the purpose).
587
+ const name = this.label
588
+ ? ''
589
+ : this.accessibleLabel.trim() || DEFAULT_TRIGGER_LABEL;
590
+ const apply = () => {
591
+ const native = nileInput.input;
592
+ if (!native)
593
+ return;
594
+ if (name)
595
+ native.setAttribute('aria-label', name);
596
+ else
597
+ native.removeAttribute('aria-label');
598
+ };
599
+ apply();
600
+ nileInput.updateComplete
601
+ ?.then(apply)
602
+ .catch(() => { });
603
+ }
604
+ seedTriggerFromValue() {
605
+ // a manual mask edit owns the trigger text; re-seeding would overwrite it
606
+ if (this._skipNextSeed)
607
+ return;
608
+ const triggerInput = this.triggerInput;
609
+ if (!triggerInput || this.type !== 'absolute')
610
+ return;
611
+ const raw = this.value;
612
+ if (!raw)
613
+ return;
614
+ const zoned = this.parseZonedValue(raw);
615
+ if (zoned && zoned.timezone !== this.timezone) {
616
+ this.setTimezoneInternally(zoned.timezone);
617
+ return;
618
+ }
619
+ const wall = this.wallClockFromRaw(raw, zoned?.wall);
620
+ if (!wall) {
621
+ // unparseable: show nothing, as for an invalid initial value, instead of the previous selection
622
+ this.clearTriggerText();
623
+ return;
624
+ }
625
+ const text = this.formatValue({ wall, timezone: this.timezone });
626
+ if (!text)
627
+ return;
628
+ this.initMaskState(text); // keep the editable slot values in sync
629
+ this.seedMask = [...this.maskValues];
630
+ this.seedYear = wall.year;
631
+ this._maskEdited = false;
632
+ triggerInput.value = text;
633
+ }
634
+ /** `silent` is for display-only refreshes (a dateFormat/hideTimeFields change): no `nile-input`, since the user did not type or clear */
635
+ clearTriggerText(silent = false) {
636
+ const triggerInput = this.triggerInput;
637
+ this.maskValues = [];
638
+ this.maskSuffix = '';
639
+ this.seedMask = null;
640
+ this.seedYear = null;
641
+ this._yearRejected = false;
642
+ if (!triggerInput || !triggerInput.value)
643
+ return;
644
+ triggerInput.value = '';
645
+ if (!silent)
646
+ triggerInput.emit('nile-input', { value: '' });
647
+ }
648
+ wallClockFromRaw(raw, zoned) {
649
+ if (zoned)
650
+ return zoned;
651
+ const epochMs = instantFromValueString(raw);
652
+ if (epochMs !== null) {
653
+ // an instant reads in this component's zone, never the browser's; an unusable zone falls back to the browser's, as in the calendar
654
+ return isValidTimezone(this.timezone)
655
+ ? wallClockInZone(epochMs, this.timezone)
656
+ : this.wallClockFromDate(new Date(epochMs));
657
+ }
658
+ const trimmed = raw.trim();
659
+ // date-only is a calendar day: midnight of that day; anything outside the documented formats is null
660
+ return wallClockFromOffsetLess(/^\d{4}-\d{2}-\d{2}$/.test(trimmed) ? `${trimmed}T00:00` : trimmed);
661
+ }
662
+ bindStructuredInput() {
663
+ const nileInput = this.triggerInput;
664
+ if (!nileInput)
665
+ return;
666
+ nileInput.removeEventListener('keydown', this.onMaskKeydown, true);
667
+ nileInput.removeEventListener('focusout', this.onMaskBlur);
668
+ nileInput.removeEventListener('paste', this.onMaskPaste, true);
669
+ nileInput.removeEventListener('beforeinput', this.onMaskBeforeInput, true);
670
+ nileInput.removeEventListener('keydown', this.onTriggerKeydown);
671
+ nileInput.addEventListener('keydown', this.onTriggerKeydown);
672
+ if (this.structuredInput) {
673
+ nileInput.addEventListener('keydown', this.onMaskKeydown, true);
674
+ nileInput.addEventListener('focusout', this.onMaskBlur);
675
+ nileInput.addEventListener('paste', this.onMaskPaste, true);
676
+ nileInput.addEventListener('beforeinput', this.onMaskBeforeInput, true);
677
+ }
678
+ }
679
+ /**
680
+ * The full year for a completed year segment. A `YY` year whose two digits equal the seeded value's keeps its century
681
+ * (editing the day of 1999-06-15, or retyping or restoring `99`, stays 1999); any other two digits read as 2000-2099.
682
+ */
683
+ fullYearOf(yearText) {
684
+ const n = Number(yearText);
685
+ if (yearText.length !== 2)
686
+ return n;
687
+ if (this.seedYear !== null && Math.abs(this.seedYear) % 100 === n)
688
+ return this.seedYear;
689
+ return 2000 + n;
690
+ }
691
+ maskLayout() {
692
+ const segs = this.maskSegments();
693
+ const tokens = [];
694
+ const slots = [];
695
+ for (const seg of segs) {
696
+ const segStart = slots.length;
697
+ for (let k = 0; k < seg.len; k++) {
698
+ tokens.push({ slot: slots.length });
699
+ slots.push({ kind: seg.kind, max: seg.max, len: seg.len, segStart });
700
+ }
701
+ for (const ch of seg.after)
702
+ tokens.push({ lit: ch });
703
+ }
704
+ return { tokens, slots, length: tokens.length };
705
+ }
706
+ computeDisplay(values, suffix, layout) {
707
+ let lastFilled = -1;
708
+ values.forEach((v, i) => { if (v !== '')
709
+ lastFilled = i; });
710
+ let s = '';
711
+ const slotPos = new Array(layout.slots.length).fill(-1);
712
+ for (const t of layout.tokens) {
713
+ if ('slot' in t) {
714
+ if (t.slot > lastFilled)
715
+ break; // stop at the trailing empties
716
+ slotPos[t.slot] = s.length;
717
+ if (values[t.slot])
718
+ s += values[t.slot];
719
+ }
720
+ else {
721
+ s += t.lit;
722
+ }
723
+ }
724
+ return { display: lastFilled === -1 ? '' : s + suffix, slotPos, lastFilled };
725
+ }
726
+ initMaskState(text) {
727
+ const layout = this.maskLayout();
728
+ const portion = (text || '').slice(0, layout.length);
729
+ this.maskSuffix = (text || '').slice(layout.length);
730
+ const digits = portion.replace(/\D/g, '');
731
+ this.maskValues = layout.slots.map((_s, i) => digits[i] ?? '');
732
+ this.seedMask = null; // text of unknown origin; callers that seed from `value` re-mark it
733
+ this.seedYear = null;
734
+ this._yearRejected = false;
735
+ }
736
+ clampSegment(values, layout, slotIndex) {
737
+ const seg = layout.slots[slotIndex];
738
+ const segSlots = [];
739
+ layout.slots.forEach((s, i) => { if (s.segStart === seg.segStart)
740
+ segSlots.push(i); });
741
+ const chars = segSlots.map(i => values[i]);
742
+ if (chars.some(c => c === ''))
743
+ return; // segment not full yet
744
+ const joined = chars.join('');
745
+ const n = Number(joined);
746
+ let corrected = null;
747
+ if (seg.kind === 'year') {
748
+ const fullYear = seg.len === 2 ? this.fullYearOf(joined) : n;
749
+ // an unset side falls back to 2000 / 2050, the same range the calendar's arrows and year dropdown use
750
+ const clamped = Math.min(this.endYear ?? DEFAULT_END_YEAR, Math.max(this.startYear ?? DEFAULT_START_YEAR, fullYear));
751
+ if (clamped !== fullYear) {
752
+ const back = seg.len === 2 ? clamped - 2000 : clamped;
753
+ if (seg.len === 2 && (back < 0 || back > 99)) {
754
+ // the range lies outside what a YY mask can write (2000-2099), so no typed YY year can be in it: put back the
755
+ // seeded digits, or blank the year when there are none, rather than commit a year the range rules out
756
+ const seed = this.seedMask?.length === values.length ? this.seedMask : null;
757
+ segSlots.forEach(s => { values[s] = seed ? seed[s] : ''; });
758
+ if (!seed)
759
+ this._yearRejected = true;
760
+ return;
761
+ }
762
+ corrected = String(back).padStart(seg.len, '0').slice(-seg.len);
763
+ }
764
+ }
765
+ else {
766
+ const min = seg.kind === 'month' || seg.kind === 'day' ? 1 : 0;
767
+ if (n > seg.max)
768
+ corrected = String(Number(joined[0])).padStart(seg.len, '0');
769
+ else if (n < min)
770
+ corrected = String(min).padStart(seg.len, '0');
771
+ }
772
+ if (corrected !== null)
773
+ segSlots.forEach((s, k) => { values[s] = corrected[k]; });
774
+ }
775
+ /** An abandoned year edit (digits typed over the old year, then the caret moved on) is normalised before another segment's edit builds on it. An untouched year, however far out of range, is left alone. */
776
+ settleYear(values, layout) {
777
+ const yearSlot = layout.slots.findIndex(s => s.kind === 'year');
778
+ if (yearSlot === -1)
779
+ return;
780
+ const seed = this.seedMask?.length === values.length ? this.seedMask : null;
781
+ const changed = !!seed && layout.slots.some((s, i) => s.kind === 'year' && values[i] !== seed[i]);
782
+ if (changed)
783
+ this.clampSegment(values, layout, yearSlot);
784
+ }
785
+ clampDay(values, layout) {
786
+ const collect = (kind) => layout.slots
787
+ .map((s, i) => (s.kind === kind ? i : -1)).filter(i => i !== -1);
788
+ const daySlots = collect('day');
789
+ if (!daySlots.length || daySlots.some(i => values[i] === ''))
790
+ return;
791
+ const monthSlots = collect('month');
792
+ const yearSlots = collect('year');
793
+ const monthStr = monthSlots.map(i => values[i]).join('');
794
+ const yearStr = yearSlots.map(i => values[i]).join('');
795
+ if (monthStr.length !== monthSlots.length)
796
+ return; // month not known yet
797
+ const month = Number(monthStr) > 12 ? String(Number(monthStr[0])).padStart(2, '0') : monthStr;
798
+ const fullYear = yearSlots.length && yearStr.length === yearSlots.length
799
+ ? (yearSlots.length === 2 ? String(this.fullYearOf(yearStr)) : yearStr)
800
+ : '';
801
+ const dayMax = this.maxDayForMonth(month, fullYear);
802
+ const dayStr = daySlots.map(i => values[i]).join('');
803
+ if (Number(dayStr) > dayMax) {
804
+ const corrected = String(dayMax).padStart(daySlots.length, '0');
805
+ daySlots.forEach((s, k) => { values[s] = corrected[k]; });
806
+ }
807
+ }
808
+ writeMask(nileInput, native, display, caret) {
809
+ native.value = display;
810
+ nileInput.value = display;
811
+ try {
812
+ native.setSelectionRange(caret, caret);
813
+ }
814
+ catch { /* noop */ }
815
+ nileInput.updateComplete
816
+ ?.then(() => { try {
817
+ native.setSelectionRange(caret, caret);
818
+ }
819
+ catch { /* noop */ } })
820
+ .catch(() => { });
821
+ }
822
+ completedSegment(layout, kind) {
823
+ const idxs = layout.slots
824
+ .map((s, i) => (s.kind === kind ? i : -1))
825
+ .filter(i => i !== -1);
826
+ if (!idxs.length || idxs.some(i => this.maskValues[i] === ''))
827
+ return null;
828
+ return idxs.map(i => this.maskValues[i]).join('');
829
+ }
830
+ /**
831
+ * A typed date outside the calendar's allowedDates range snaps to the nearest boundary (the calendar disables
832
+ * those day cells but typing bypasses it). Only the date slots change; time and timezone are untouched.
833
+ */
834
+ clampMaskToAllowedDates(cal, layout) {
835
+ const range = cal.allowedDatesLocal;
836
+ if (!range)
837
+ return;
838
+ const dayStr = this.completedSegment(layout, 'day');
839
+ const monthStr = this.completedSegment(layout, 'month');
840
+ const yearStr = this.completedSegment(layout, 'year');
841
+ if (dayStr === null || monthStr === null || yearStr === null)
842
+ return;
843
+ const yearLen = layout.slots.filter(s => s.kind === 'year').length;
844
+ const year = yearLen === 2 ? this.fullYearOf(yearStr) : Number(yearStr);
845
+ const month0 = Number(monthStr) - 1;
846
+ const day = Number(dayStr);
847
+ if (month0 < 0 || month0 > 11 || day < 1 || cal.isAllowedDate(day, month0, year))
848
+ return;
849
+ const typed = utcFromFields(year, month0, day);
850
+ const bound = typed < cal.getUTCDate(range.startDate).getTime()
851
+ ? cal.getUTCDate(range.startDate)
852
+ : cal.getUTCDate(range.endDate);
853
+ const boundYear = bound.getUTCFullYear();
854
+ if (yearLen === 2 && (boundYear < 2000 || boundYear > 2099))
855
+ return; // not representable in a YY mask
856
+ const fill = (kind, text) => {
857
+ let k = 0;
858
+ layout.slots.forEach((s, i) => { if (s.kind === kind)
859
+ this.maskValues[i] = text[k++]; });
860
+ };
861
+ fill('year', yearLen === 2 ? String(boundYear - 2000).padStart(2, '0') : String(boundYear).padStart(4, '0'));
862
+ if (yearLen === 2)
863
+ this.seedYear = boundYear; // the year text now holds the boundary's year
864
+ fill('month', String(bound.getUTCMonth() + 1).padStart(2, '0'));
865
+ fill('day', String(bound.getUTCDate()).padStart(2, '0'));
866
+ const nileInput = this.triggerInput;
867
+ const native = nileInput?.input;
868
+ if (!native)
869
+ return;
870
+ const { display } = this.computeDisplay(this.maskValues, this.maskSuffix, layout);
871
+ this.writeMask(nileInput, native, display, native.selectionStart ?? display.length);
872
+ }
873
+ syncCalendarFromMask() {
874
+ const layout = this.maskLayout();
875
+ if (this.maskValues.length !== layout.slots.length)
876
+ return;
877
+ const cal = this.calendarRef.value;
878
+ if (!cal)
879
+ return;
880
+ if (this._yearRejected) {
881
+ if (this.completedSegment(layout, 'year') !== null) {
882
+ this._yearRejected = false;
883
+ }
884
+ else {
885
+ // day and month alone must not leave a committable date (carrying the calendar's own year) behind
886
+ cal.value = null;
887
+ cal.updateComplete?.then(() => cal.syncInnerCalendar?.()).catch(() => { });
888
+ return;
889
+ }
890
+ }
891
+ if (this._maskEdited)
892
+ this.clampMaskToAllowedDates(cal, layout);
893
+ const monthStr = this.completedSegment(layout, 'month');
894
+ const dayStr = this.completedSegment(layout, 'day');
895
+ const yearStr = this.completedSegment(layout, 'year');
896
+ const yearSlots = layout.slots.filter(s => s.kind === 'year').length;
897
+ const typedMonth = monthStr === null ? null : Number(monthStr) - 1;
898
+ const typedYear = yearStr === null ? null : (yearSlots === 2 ? this.fullYearOf(yearStr) : Number(yearStr));
899
+ let changed = false;
900
+ if (typedMonth !== null && typedMonth >= 0 && typedMonth <= 11) {
901
+ cal.currentMonth = typedMonth;
902
+ changed = true;
903
+ }
904
+ if (typedYear !== null) {
905
+ cal.currentYear = typedYear;
906
+ changed = true;
907
+ }
908
+ if (dayStr !== null) {
909
+ const month = typedMonth ?? cal.currentMonth;
910
+ const year = typedYear ?? cal.currentYear;
911
+ const lastDay = localDateFromFields(year, month + 1, 0).getDate();
912
+ const date = localDateFromFields(year, month, Math.min(Number(dayStr), lastDay));
913
+ if (!isNaN(date.getTime())) {
914
+ cal.value = date;
915
+ cal.currentMonth = date.getMonth();
916
+ cal.currentYear = date.getFullYear();
917
+ changed = true;
918
+ }
919
+ }
920
+ const setTime = (kind, prop) => {
921
+ const v = this.completedSegment(layout, kind);
922
+ if (v !== null) {
923
+ cal[prop] = Number(v);
924
+ changed = true;
925
+ }
926
+ };
927
+ setTime('HH', 'tpHours');
928
+ setTime('mm', 'tpMinutes');
929
+ setTime('ss', 'tpSeconds');
930
+ setTime('SSS', 'tpMilliseconds');
931
+ const tz = this.maskSuffix.replace(/^\s*-\s*/, '').trim();
932
+ if (tz) {
933
+ cal.timezone = tz;
934
+ changed = true;
935
+ }
936
+ if (changed) {
937
+ cal.updateComplete
938
+ ?.then(() => cal.syncInnerCalendar?.())
939
+ .catch(() => { });
940
+ }
941
+ }
942
+ /** Re-derive slot state from whatever the native input now holds, then redisplay it masked. */
943
+ reflowMaskFromNative(nileInput, native) {
944
+ const layout = this.maskLayout();
945
+ // initMaskState forgets the seed's century; the year digits themselves decide whether it still applies
946
+ const { seedYear } = this;
947
+ this.initMaskState(native.value);
948
+ this._maskEdited = true;
949
+ this.seedYear = seedYear;
950
+ const segStarts = [...new Set(layout.slots.map(s => s.segStart))];
951
+ segStarts.forEach(start => this.clampSegment(this.maskValues, layout, start));
952
+ this.clampDay(this.maskValues, layout);
953
+ const { display } = this.computeDisplay(this.maskValues, this.maskSuffix, layout);
954
+ this.writeMask(nileInput, native, display, display.length - (this.maskSuffix && this.maskValues.every(v => v !== '') ? this.maskSuffix.length : 0));
955
+ this.syncCalendarFromMask();
956
+ }
957
+ maskSegments() {
958
+ const spec = (token) => {
959
+ switch (token) {
960
+ case 'YYYY': return { kind: 'year', len: 4, max: 9999 };
961
+ case 'YY': return { kind: 'year', len: 2, max: 99 };
962
+ case 'MM': return { kind: 'month', len: 2, max: 12 };
963
+ default: return { kind: 'day', len: 2, max: 31 }; // DD
964
+ }
965
+ };
966
+ const fmt = this.dateFormat || 'DD/MM/YY';
967
+ const re = /YYYY|YY|MM|DD/g;
968
+ const found = [];
969
+ let m;
970
+ while ((m = re.exec(fmt)) !== null)
971
+ found.push({ token: m[0], index: m.index });
972
+ const time = this.shownTimeFields;
973
+ const segments = found.map((f, i) => {
974
+ const sep = fmt.slice(f.index + f.token.length, i + 1 < found.length ? found[i + 1].index : fmt.length);
975
+ return { ...spec(f.token), after: i < found.length - 1 ? sep : (time.length ? ' - ' : '') };
976
+ });
977
+ time.forEach((t, i) => {
978
+ const next = time[i + 1];
979
+ segments.push({ kind: t.kind, len: t.len, max: t.max, after: next ? this.timeSep(next.kind) : '' });
980
+ });
981
+ return segments;
982
+ }
983
+ maxDayForMonth(month, fullYear) {
984
+ if (month.length < 2)
985
+ return 31;
986
+ const m = Number(month);
987
+ if (m < 1 || m > 12)
988
+ return 31;
989
+ const days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
990
+ if (m !== 2)
991
+ return days[m - 1];
992
+ if (!fullYear)
993
+ return 29;
994
+ const y = Number(fullYear);
995
+ const leap = (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0;
996
+ return leap ? 29 : 28;
997
+ }
998
+ render() {
999
+ return html `
1000
+ <nile-floating-panel
1001
+ part="panel"
1002
+ exportparts="base"
1003
+ panelClass="nile-dtp-panel"
1004
+ trigger="click"
1005
+ placement="bottom-start"
1006
+ arrow="none"
1007
+ distance="6"
1008
+ ?disabled="${this.disabled}"
1009
+ @nile-init="${(e) => e.stopPropagation()}"
1010
+ @nile-destroy="${(e) => e.stopPropagation()}"
1011
+ @nile-visibility-change="${(e) => e.stopPropagation()}"
1012
+ @nile-show="${this.onPanelShow}"
1013
+ @nile-hide="${this.onPanelHide}"
1014
+ >
1015
+ <nile-input
1016
+ part="trigger"
1017
+ exportparts="base"
1018
+ label=${ifDefined(this.label || undefined)}
1019
+ placeholder="${this.placeholder || this.defaultPlaceholder}"
1020
+ ?disabled="${this.disabled}"
1021
+ >
1022
+ <nile-glyph slot="suffix" name="ng-calendar-clock" size="16" method="stroke" color=${ifDefined(this.disabled ? 'var(--nile-colors-dark-500, var(--ng-colors-fg-disabled))' : undefined)}></nile-glyph>
1023
+ </nile-input>
1024
+
1025
+ <nile-calendar
1026
+ ${ref(this.calendarRef)}
1027
+ variant="date-time-picker"
1028
+ value="${ifDefined(this.value ?? undefined)}"
1029
+ allowedDates="${this.allowedDates}"
1030
+ ?showTypes="${this.showTypes}"
1031
+ ?doubleClickUnselect="${this.doubleClickUnselect}"
1032
+ type="${this.type}"
1033
+ selectedUnit="${ifDefined(this.selectedUnit)}"
1034
+ selectedValue="${ifDefined(this.selectedValue)}"
1035
+ .hideDurationFields="${this.hideDurationFields}"
1036
+ startYear="${ifDefined(this.startYear)}"
1037
+ endYear="${ifDefined(this.endYear)}"
1038
+ ?showMonthDropdown="${this.showMonthDropdown}"
1039
+ ?showYearDropdown="${this.showYearDropdown}"
1040
+ ?disabled="${this.disabled}"
1041
+ timezone="${this.timezone}"
1042
+ dateFormat="${this.dateFormat}"
1043
+ .hideTimeFields="${this.hideTimeFields}"
1044
+ @nile-init="${(e) => e.stopPropagation()}"
1045
+ @nile-destroy="${(e) => e.stopPropagation()}"
1046
+ @nile-change="${this.handleChanged}"
1047
+ @nile-clear="${(e) => { e.stopPropagation(); this.handleClear(); }}"
1048
+ @nile-type-change="${(e) => { e.stopPropagation(); this.handleTypeChange(e); }}"
1049
+ ></nile-calendar>
1050
+ </nile-floating-panel>
1051
+ `;
1052
+ }
1053
+ handleTypeChange(e) {
1054
+ const next = e.detail?.value;
1055
+ if (next === 'absolute' || next === 'relative')
1056
+ this.type = next;
1057
+ // slot state belongs to the absolute layout; drop it so a stale mask can't rewrite relative text
1058
+ this.maskValues = [];
1059
+ this.maskSuffix = '';
1060
+ this._yearRejected = false;
1061
+ this.emit('nile-type-change', e.detail);
1062
+ }
1063
+ handleChanged(event) {
1064
+ event.stopPropagation();
1065
+ if (this.disabled)
1066
+ return;
1067
+ const detail = event.detail;
1068
+ const triggerInput = this.triggerInput;
1069
+ if (triggerInput && detail) {
1070
+ const text = this.formatValue(detail);
1071
+ if (this.type === 'absolute') {
1072
+ this.initMaskState(text); // keep the editable slot values in sync
1073
+ this.seedMask = [...this.maskValues];
1074
+ // the applied selection's own year, so a YY mask keeps its century on the next edit
1075
+ this.seedYear = detail.wall?.year
1076
+ ?? (detail.value instanceof Date ? detail.value.getFullYear()
1077
+ : typeof detail.value === 'string' ? this.parseWallClock(detail.value)?.year ?? null : null);
1078
+ this._maskEdited = false;
1079
+ this._showCommittedOnOpen = true;
1080
+ }
1081
+ else {
1082
+ // relative range text isn't in the absolute mask layout; drop stale slot state
1083
+ this.maskValues = [];
1084
+ this.maskSuffix = '';
1085
+ this._yearRejected = false;
1086
+ }
1087
+ triggerInput.value = text;
1088
+ triggerInput.emit('nile-input', { value: text });
1089
+ }
1090
+ // keep the public value in step with the applied selection, so a later re-seed (e.g. a timezone change) can't restore the old one
1091
+ if (this.type === 'absolute' && typeof detail?.value === 'string' && detail.value) {
1092
+ this.value = detail.value;
1093
+ }
1094
+ this.emit('nile-change', detail);
1095
+ triggerInput?.input?.focus?.();
1096
+ this.panel?.hide();
1097
+ }
1098
+ handleClear() {
1099
+ const triggerInput = this.triggerInput;
1100
+ if (triggerInput) {
1101
+ triggerInput.value = '';
1102
+ triggerInput.emit('nile-input', { value: '' });
1103
+ }
1104
+ this.maskValues = [];
1105
+ this.maskSuffix = '';
1106
+ this._yearRejected = false;
1107
+ this.value = null;
1108
+ this.setTimezoneInternally(this._defaultTimezone);
1109
+ // the calendar may have drifted (zone typed in the mask, picked in its dropdown) while our own `timezone` did not change, so the template binding won't re-apply it
1110
+ const cal = this.calendarRef.value;
1111
+ if (cal)
1112
+ cal.timezone = this._defaultTimezone;
1113
+ this.emit('nile-clear');
1114
+ }
1115
+ pad(value, length = 2) {
1116
+ return String(value).padStart(length, '0');
1117
+ }
1118
+ /** Read an instant's fields in the host zone. Lossless in this direction. */
1119
+ wallClockFromDate(date) {
1120
+ return {
1121
+ year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate(),
1122
+ hours: date.getHours(), minutes: date.getMinutes(),
1123
+ seconds: date.getSeconds(), ms: date.getMilliseconds(),
1124
+ };
1125
+ }
1126
+ formatDatePartFields(wall) {
1127
+ const yyyy = String(wall.year);
1128
+ // the structured mask has exactly four year slots, so years 0-999 must print four digits (a bare `100` would be re-read as `1000` on blur)
1129
+ const year4 = wall.year >= 0 && wall.year < 1000 ? yyyy.padStart(4, '0') : yyyy;
1130
+ return (this.dateFormat || 'DD/MM/YY')
1131
+ .replace(/YYYY/g, year4)
1132
+ .replace(/YY/g, yyyy.padStart(2, '0').slice(-2))
1133
+ .replace(/MM/g, this.pad(wall.month))
1134
+ .replace(/DD/g, this.pad(wall.day));
1135
+ }
1136
+ formatWallClock(wall) {
1137
+ const valueFor = {
1138
+ HH: this.pad(wall.hours),
1139
+ mm: this.pad(wall.minutes),
1140
+ ss: this.pad(wall.seconds),
1141
+ SSS: this.pad(wall.ms, 3),
1142
+ };
1143
+ const time = this.shownTimeFields;
1144
+ if (!time.length)
1145
+ return this.formatDatePartFields(wall);
1146
+ const timeStr = time
1147
+ .map((t, i) => (i > 0 ? this.timeSep(t.kind) : '') + valueFor[t.kind])
1148
+ .join('');
1149
+ return `${this.formatDatePartFields(wall)} - ${timeStr}`;
1150
+ }
1151
+ formatDateTime(date) {
1152
+ return this.formatWallClock(this.wallClockFromDate(date));
1153
+ }
1154
+ /** A value's digits as written: bracketed `…[GMT+05:30 Zone]` or offset-less `YYYY-MM-DDTHH:mm[:ss[.SSS]]`; anything else is null. */
1155
+ parseWallClock(iso) {
1156
+ return this.parseZonedValue(iso)?.wall ?? wallClockFromOffsetLess(iso);
1157
+ }
1158
+ parseZonedValue(raw) {
1159
+ const m = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?)\[GMT[+-]\d{2}:\d{2}\s+([^\]]+)\]$/.exec(raw.trim());
1160
+ if (!m)
1161
+ return null;
1162
+ const wall = wallClockFromOffsetLess(m[1]);
1163
+ if (!wall)
1164
+ return null;
1165
+ const zone = m[2].trim();
1166
+ const timezone = isValidTimezone(zone) ? zone : 'UTC';
1167
+ return { wall, timezone };
1168
+ }
1169
+ formatValue(detail) {
1170
+ // `detail` is an untrusted event payload: optional chaining alone would render a literal " - undefined" when `timezone` is omitted, so fall back to the component's own zone.
1171
+ const zone = detail?.timezone ?? this.timezone;
1172
+ const tz = this.showTimezone ? ` - ${zone}` : '';
1173
+ if (detail?.wall) {
1174
+ return `${this.formatWallClock(detail.wall)}${tz}`;
1175
+ }
1176
+ if (detail?.value instanceof Date) {
1177
+ return `${this.formatDateTime(detail.value)}${tz}`;
1178
+ }
1179
+ if (typeof detail?.value === 'string' && detail.value) {
1180
+ const wall = this.parseWallClock(detail.value);
1181
+ if (wall)
1182
+ return `${this.formatWallClock(wall)}${tz}`;
1183
+ }
1184
+ if (detail?.startDate && detail?.endDate) {
1185
+ // a labelled range must show its instants in the labelled zone, not the host's
1186
+ const inZone = (d) => this.showTimezone && isValidTimezone(zone)
1187
+ ? this.formatWallClock(wallClockInZone(d.getTime(), zone))
1188
+ : this.formatDateTime(d);
1189
+ const start = inZone(new Date(detail.startDate));
1190
+ const end = inZone(new Date(detail.endDate));
1191
+ return `${start} - ${end}${tz}`;
1192
+ }
1193
+ return '';
1194
+ }
1195
+ };
1196
+ NileDateTimePicker.TIME_SPEC = [
1197
+ { key: 'hours', kind: 'HH', len: 2, max: 23, ph: 'hh' },
1198
+ { key: 'minutes', kind: 'mm', len: 2, max: 59, ph: 'mm' },
1199
+ { key: 'seconds', kind: 'ss', len: 2, max: 59, ph: 'ss' },
1200
+ { key: 'milliseconds', kind: 'SSS', len: 3, max: 999, ph: 'SSS' },
1201
+ ];
1202
+ __decorate([
1203
+ property({ type: Boolean, attribute: true, reflect: true })
1204
+ ], NileDateTimePicker.prototype, "showMonthDropdown", void 0);
1205
+ __decorate([
1206
+ property({ type: Boolean, attribute: true, reflect: true })
1207
+ ], NileDateTimePicker.prototype, "showYearDropdown", void 0);
1208
+ __decorate([
1209
+ property({ type: String, reflect: true })
1210
+ ], NileDateTimePicker.prototype, "type", void 0);
1211
+ __decorate([
1212
+ property({ type: Boolean, attribute: true, reflect: true })
1213
+ ], NileDateTimePicker.prototype, "showTypes", void 0);
1214
+ __decorate([
1215
+ property({ type: Array, attribute: true })
1216
+ ], NileDateTimePicker.prototype, "hideDurationFields", void 0);
1217
+ __decorate([
1218
+ property({ type: Array, attribute: 'hide-time-fields' })
1219
+ ], NileDateTimePicker.prototype, "hideTimeFields", void 0);
1220
+ __decorate([
1221
+ property({ type: String })
1222
+ ], NileDateTimePicker.prototype, "value", void 0);
1223
+ __decorate([
1224
+ property({ type: String, reflect: true })
1225
+ ], NileDateTimePicker.prototype, "timezone", void 0);
1226
+ __decorate([
1227
+ property({ type: String })
1228
+ ], NileDateTimePicker.prototype, "selectedUnit", void 0);
1229
+ __decorate([
1230
+ property({ type: Number })
1231
+ ], NileDateTimePicker.prototype, "selectedValue", void 0);
1232
+ __decorate([
1233
+ property({ type: Number, attribute: true, reflect: true })
1234
+ ], NileDateTimePicker.prototype, "startYear", void 0);
1235
+ __decorate([
1236
+ property({ type: Number, attribute: true, reflect: true })
1237
+ ], NileDateTimePicker.prototype, "endYear", void 0);
1238
+ __decorate([
1239
+ property({ type: String, attribute: true, reflect: true })
1240
+ ], NileDateTimePicker.prototype, "allowedDates", void 0);
1241
+ __decorate([
1242
+ property({ type: String, reflect: true })
1243
+ ], NileDateTimePicker.prototype, "dateFormat", void 0);
1244
+ __decorate([
1245
+ property({ type: String })
1246
+ ], NileDateTimePicker.prototype, "placeholder", void 0);
1247
+ __decorate([
1248
+ property({ type: String })
1249
+ ], NileDateTimePicker.prototype, "label", void 0);
1250
+ __decorate([
1251
+ property({ type: String, attribute: 'aria-label' })
1252
+ ], NileDateTimePicker.prototype, "accessibleLabel", void 0);
1253
+ __decorate([
1254
+ property({ type: Boolean, reflect: true })
1255
+ ], NileDateTimePicker.prototype, "doubleClickUnselect", void 0);
1256
+ __decorate([
1257
+ property({ type: Boolean, attribute: true, reflect: true })
1258
+ ], NileDateTimePicker.prototype, "disabled", void 0);
1259
+ __decorate([
1260
+ property({ type: Boolean, reflect: true })
1261
+ ], NileDateTimePicker.prototype, "open", void 0);
1262
+ __decorate([
1263
+ query('nile-floating-panel')
1264
+ ], NileDateTimePicker.prototype, "panel", void 0);
1265
+ NileDateTimePicker = NileDateTimePicker_1 = __decorate([
1266
+ customElement('nile-date-time-picker')
1267
+ ], NileDateTimePicker);
1268
+ export { NileDateTimePicker };
1269
+ export default NileDateTimePicker;
1270
+ //# sourceMappingURL=nile-date-time-picker.js.map