@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.
- package/README.md +5 -0
- package/demo/index.js +0 -1
- package/dist/index.cjs.js +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.js +1254 -488
- package/dist/nile-badge/index.cjs.js +1 -1
- package/dist/nile-badge/index.esm.js +1 -1
- package/dist/nile-badge/nile-badge.cjs.js +1 -1
- package/dist/nile-badge/nile-badge.cjs.js.map +1 -1
- package/dist/nile-badge/nile-badge.css.cjs.js +1 -1
- package/dist/nile-badge/nile-badge.css.cjs.js.map +1 -1
- package/dist/nile-badge/nile-badge.css.esm.js +24 -3
- package/dist/nile-badge/nile-badge.esm.js +5 -3
- package/dist/nile-calendar/index.cjs.js +1 -1
- package/dist/nile-calendar/index.esm.js +1 -1
- package/dist/nile-calendar/nile-calendar.cjs.js +2 -1
- package/dist/nile-calendar/nile-calendar.cjs.js.map +1 -1
- package/dist/nile-calendar/nile-calendar.css.cjs.js +1 -1
- package/dist/nile-calendar/nile-calendar.css.cjs.js.map +1 -1
- package/dist/nile-calendar/nile-calendar.css.esm.js +357 -2
- package/dist/nile-calendar/nile-calendar.esm.js +365 -41
- package/dist/nile-calendar/timezones.cjs.js +1 -1
- package/dist/nile-calendar/timezones.cjs.js.map +1 -1
- package/dist/nile-calendar/timezones.esm.js +1 -1
- package/dist/nile-date-time-picker/index.cjs.js +2 -0
- package/dist/nile-date-time-picker/index.cjs.js.map +1 -0
- package/dist/nile-date-time-picker/index.esm.js +1 -0
- package/dist/nile-date-time-picker/nile-date-time-picker.cjs.js +2 -0
- package/dist/nile-date-time-picker/nile-date-time-picker.cjs.js.map +1 -0
- package/dist/nile-date-time-picker/nile-date-time-picker.css.cjs.js +2 -0
- package/dist/nile-date-time-picker/nile-date-time-picker.css.cjs.js.map +1 -0
- package/dist/nile-date-time-picker/nile-date-time-picker.css.esm.js +13 -0
- package/dist/nile-date-time-picker/nile-date-time-picker.esm.js +53 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/nile-badge/nile-badge.css.js +24 -3
- package/dist/src/nile-badge/nile-badge.css.js.map +1 -1
- package/dist/src/nile-badge/nile-badge.d.ts +7 -4
- package/dist/src/nile-badge/nile-badge.js +22 -5
- package/dist/src/nile-badge/nile-badge.js.map +1 -1
- package/dist/src/nile-badge/nile-badge.test.js +88 -0
- package/dist/src/nile-badge/nile-badge.test.js.map +1 -1
- package/dist/src/nile-calendar/nile-calendar.css.js +355 -0
- package/dist/src/nile-calendar/nile-calendar.css.js.map +1 -1
- package/dist/src/nile-calendar/nile-calendar.d.ts +175 -0
- package/dist/src/nile-calendar/nile-calendar.js +1726 -26
- package/dist/src/nile-calendar/nile-calendar.js.map +1 -1
- package/dist/src/nile-calendar/nile-calendar.test.d.ts +1 -0
- package/dist/src/nile-calendar/nile-calendar.test.js +3580 -0
- package/dist/src/nile-calendar/nile-calendar.test.js.map +1 -1
- package/dist/src/nile-calendar/timezones.d.ts +21 -0
- package/dist/src/nile-calendar/timezones.js +49 -0
- package/dist/src/nile-calendar/timezones.js.map +1 -1
- package/dist/src/nile-date-time-picker/index.d.ts +1 -0
- package/dist/src/nile-date-time-picker/index.js +2 -0
- package/dist/src/nile-date-time-picker/index.js.map +1 -0
- package/dist/src/nile-date-time-picker/nile-date-time-picker.css.d.ts +11 -0
- package/dist/src/nile-date-time-picker/nile-date-time-picker.css.js +49 -0
- package/dist/src/nile-date-time-picker/nile-date-time-picker.css.js.map +1 -0
- package/dist/src/nile-date-time-picker/nile-date-time-picker.d.ts +173 -0
- package/dist/src/nile-date-time-picker/nile-date-time-picker.js +1270 -0
- package/dist/src/nile-date-time-picker/nile-date-time-picker.js.map +1 -0
- package/dist/src/nile-date-time-picker/nile-date-time-picker.test.d.ts +143 -0
- package/dist/src/nile-date-time-picker/nile-date-time-picker.test.js +7277 -0
- package/dist/src/nile-date-time-picker/nile-date-time-picker.test.js.map +1 -0
- package/dist/src/version.js +1 -1
- package/dist/src/version.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utilities/animation-registry.cjs.js +1 -1
- package/dist/utilities/animation-registry.cjs.js.map +1 -1
- package/dist/utilities/animation-registry.esm.js +1 -1
- package/package.json +3 -1
- package/scripts/coverage-report.mjs +96 -0
- package/src/index.ts +1 -0
- package/src/nile-badge/nile-badge.css.ts +24 -3
- package/src/nile-badge/nile-badge.test.ts +102 -0
- package/src/nile-badge/nile-badge.ts +28 -5
- package/src/nile-calendar/nile-calendar.css.ts +355 -0
- package/src/nile-calendar/nile-calendar.test.ts +3793 -0
- package/src/nile-calendar/nile-calendar.ts +1760 -28
- package/src/nile-calendar/timezones.ts +57 -0
- package/src/nile-date-time-picker/index.ts +1 -0
- package/src/nile-date-time-picker/nile-date-time-picker.css.ts +54 -0
- package/src/nile-date-time-picker/nile-date-time-picker.test.ts +7886 -0
- package/src/nile-date-time-picker/nile-date-time-picker.ts +1153 -0
- package/vscode-html-custom-data.json +161 -6
|
@@ -12,11 +12,148 @@ import { classMap } from 'lit/directives/class-map.js';
|
|
|
12
12
|
import { watch } from '../internal/watch';
|
|
13
13
|
import NileElement from '../internal/nile-element';
|
|
14
14
|
import { ClearMode } from './nile-calendar.enum';
|
|
15
|
+
import { TIMEZONES, isValidTimezone, isClockTime, utcFromFields, localDateFromFields, formatYear4, DEFAULT_START_YEAR, DEFAULT_END_YEAR } from './timezones';
|
|
16
|
+
import { setAnimation } from '../utilities/animation-registry';
|
|
17
|
+
/* one exact moment on the timeline Eg: 2026-03-01T10:00:00Z, …+05:30 */
|
|
18
|
+
const INSTANT_VALUE_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2})?(?:\.\d{1,3})?(?:Z|[+-]\d{2}:?\d{2})$/;
|
|
19
|
+
/** an instant plus the zone it was picked in. Eg: 2026-03-01T10:00:00.000[GMT+05:30 Asia/Kolkata] */
|
|
20
|
+
const BRACKETED_VALUE_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{3}))?\[GMT([+-])(\d{2}):(\d{2})\s+([^\]]+)\]$/;
|
|
21
|
+
export function instantFromValueString(raw) {
|
|
22
|
+
if (typeof raw !== 'string')
|
|
23
|
+
return null;
|
|
24
|
+
const trimmed = raw.trim();
|
|
25
|
+
if (!INSTANT_VALUE_RE.test(trimmed))
|
|
26
|
+
return null;
|
|
27
|
+
const ms = new Date(trimmed).getTime();
|
|
28
|
+
return isNaN(ms) ? null : ms;
|
|
29
|
+
}
|
|
30
|
+
/** digits on a clock face, 2026-03-01T10:00:00 */
|
|
31
|
+
const OFFSETLESS_VALUE_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?(?:\.(\d{1,3}))?$/;
|
|
32
|
+
/** Digits as written, with no zone conversion. */
|
|
33
|
+
export function wallClockFromOffsetLess(raw) {
|
|
34
|
+
if (typeof raw !== 'string')
|
|
35
|
+
return null;
|
|
36
|
+
const m = OFFSETLESS_VALUE_RE.exec(raw.trim());
|
|
37
|
+
if (!m)
|
|
38
|
+
return null;
|
|
39
|
+
const year = Number(m[1]);
|
|
40
|
+
const month0 = Number(m[2]) - 1;
|
|
41
|
+
const day = Number(m[3]);
|
|
42
|
+
if (month0 < 0 || month0 > 11 || day < 1)
|
|
43
|
+
return null;
|
|
44
|
+
if (!isClockTime(Number(m[4]), Number(m[5]), Number(m[6] ?? 0), Number(m[7] ?? 0)))
|
|
45
|
+
return null;
|
|
46
|
+
const lastDay = new Date(utcFromFields(year, month0 + 1, 0)).getUTCDate();
|
|
47
|
+
const t = utcFromFields(year, month0, Math.min(day, lastDay), Number(m[4]), Number(m[5]), Number(m[6] ?? 0), Number(m[7] ?? 0));
|
|
48
|
+
if (isNaN(t))
|
|
49
|
+
return null;
|
|
50
|
+
const u = new Date(t);
|
|
51
|
+
return {
|
|
52
|
+
year: u.getUTCFullYear(),
|
|
53
|
+
month: u.getUTCMonth() + 1,
|
|
54
|
+
day: u.getUTCDate(),
|
|
55
|
+
hours: u.getUTCHours(),
|
|
56
|
+
minutes: u.getUTCMinutes(),
|
|
57
|
+
seconds: u.getUTCSeconds(),
|
|
58
|
+
ms: u.getUTCMilliseconds(),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export function wallClockInZone(epochMs, zone) {
|
|
62
|
+
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
63
|
+
timeZone: zone,
|
|
64
|
+
hourCycle: 'h23',
|
|
65
|
+
year: 'numeric', month: '2-digit', day: '2-digit',
|
|
66
|
+
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
|
67
|
+
});
|
|
68
|
+
const parts = {};
|
|
69
|
+
for (const part of formatter.formatToParts(new Date(epochMs))) {
|
|
70
|
+
if (part.type !== 'literal')
|
|
71
|
+
parts[part.type] = Number(part.value);
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
year: parts.year,
|
|
75
|
+
month: parts.month,
|
|
76
|
+
day: parts.day,
|
|
77
|
+
hours: parts.hour === 24 ? 0 : parts.hour,
|
|
78
|
+
minutes: parts.minute,
|
|
79
|
+
seconds: parts.second,
|
|
80
|
+
ms: ((epochMs % 1000) + 1000) % 1000,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export function instantFromBracketedValue(raw) {
|
|
84
|
+
if (typeof raw !== 'string')
|
|
85
|
+
return null;
|
|
86
|
+
const m = BRACKETED_VALUE_RE.exec(raw.trim());
|
|
87
|
+
if (!m)
|
|
88
|
+
return null;
|
|
89
|
+
const [, y, mo, d, h, mi, s, ms, sign, oh, om, rawZone] = m;
|
|
90
|
+
const zone = rawZone.trim();
|
|
91
|
+
if (!isValidTimezone(zone))
|
|
92
|
+
return null;
|
|
93
|
+
const wall = {
|
|
94
|
+
year: Number(y), month: Number(mo), day: Number(d),
|
|
95
|
+
hours: Number(h), minutes: Number(mi), seconds: Number(s), ms: Number(ms ?? 0),
|
|
96
|
+
};
|
|
97
|
+
// ISO end-of-day: exactly 24:00:00(.000) is the next day's 00:00:00; any other hour 24 or above is not a time
|
|
98
|
+
const endOfDay = wall.hours === 24;
|
|
99
|
+
if (wall.hours > 24 || (endOfDay && (wall.minutes !== 0 || wall.seconds !== 0 || wall.ms !== 0)))
|
|
100
|
+
return null;
|
|
101
|
+
const offsetMs = (sign === '-' ? -1 : 1) * (Number(oh) * 3600000 + Number(om) * 60000);
|
|
102
|
+
const wallMs = utcFromFields(wall.year, wall.month - 1, wall.day, wall.hours, wall.minutes, wall.seconds, wall.ms);
|
|
103
|
+
const candidate = wallMs - offsetMs;
|
|
104
|
+
const readBack = wallClockInZone(candidate, zone);
|
|
105
|
+
// the digits the zone must read back: the written ones, or the rolled-over next day for 24:00:00
|
|
106
|
+
const expected = endOfDay ? wallClockInZone(wallMs, 'UTC') : wall;
|
|
107
|
+
const digitsAgree = readBack.year === expected.year && readBack.month === expected.month &&
|
|
108
|
+
readBack.day === expected.day && readBack.hours === expected.hours &&
|
|
109
|
+
readBack.minutes === expected.minutes && readBack.seconds === expected.seconds &&
|
|
110
|
+
readBack.ms === expected.ms;
|
|
111
|
+
if (!digitsAgree)
|
|
112
|
+
return null;
|
|
113
|
+
return { epochMs: candidate, timezone: zone };
|
|
114
|
+
}
|
|
15
115
|
/**
|
|
16
116
|
* Nile icon component.
|
|
17
117
|
*
|
|
18
118
|
* @tag nile-calendar
|
|
19
119
|
*
|
|
120
|
+
* @csspart calendar-container - The outer container.
|
|
121
|
+
* @csspart calendar-switcher - The Absolute / Relative tab switcher.
|
|
122
|
+
* @csspart button-container - The default variant's button row.
|
|
123
|
+
* @csspart calendar-wrapper - Wraps the month grid.
|
|
124
|
+
* @csspart trigger - The month and year dropdown triggers.
|
|
125
|
+
* @csspart calendar-day-N - A day cell, where N is the day of the month.
|
|
126
|
+
* @csspart current-date-dot - The marker on today's date.
|
|
127
|
+
*
|
|
128
|
+
* Parts that exist only in `variant="date-time-picker"`:
|
|
129
|
+
* @csspart calendar-config - The row holding the type switcher.
|
|
130
|
+
* @csspart calendar - One month's header and grid.
|
|
131
|
+
* @csspart calendar-header - The month header row.
|
|
132
|
+
* @csspart calendar-header-prev - The previous-month icon.
|
|
133
|
+
* @csspart calendar-header-next - The next-month icon.
|
|
134
|
+
* @csspart calendar-header-center - The month and year area.
|
|
135
|
+
* @csspart days-container - The grid of day cells.
|
|
136
|
+
* @csspart day-name - A weekday label (Sun, Mon, ...).
|
|
137
|
+
* @csspart calendar-day-label - The number inside a day cell.
|
|
138
|
+
* @csspart tp-chevron - The expand / collapse control.
|
|
139
|
+
* @csspart tp-scroll-area - The scrolling area under the grid.
|
|
140
|
+
* @csspart tp-scroll-content - The content inside the scrolling area.
|
|
141
|
+
* @csspart tp-time-fields - The row of time inputs.
|
|
142
|
+
* @csspart tp-time-field - One time input (hours, minutes, seconds, milliseconds).
|
|
143
|
+
* @csspart tp-stepper - The up/down stepper inside a time input.
|
|
144
|
+
* @csspart tp-stepper-btn - Either stepper button.
|
|
145
|
+
* @csspart tp-stepper-btn-up - The increase button.
|
|
146
|
+
* @csspart tp-stepper-btn-down - The decrease button.
|
|
147
|
+
* @csspart tp-timezone - The timezone section.
|
|
148
|
+
* @csspart tp-timezone-header - The timezone label row.
|
|
149
|
+
* @csspart tp-timezone-label - The "Timezone" label.
|
|
150
|
+
* @csspart tp-timezone-local-link - The "Use local timezone" link.
|
|
151
|
+
* @csspart tp-timezone-select - The timezone select.
|
|
152
|
+
* @csspart tp-footer - The footer.
|
|
153
|
+
* @csspart tp-footer-actions - The Reset and Apply group.
|
|
154
|
+
* @csspart tp-set-to-now-button - The "Set to now" button.
|
|
155
|
+
* @csspart clear-button - The Reset button.
|
|
156
|
+
* @csspart apply-button - The Apply button.
|
|
20
157
|
*/
|
|
21
158
|
let NileCalendar = class NileCalendar extends NileElement {
|
|
22
159
|
constructor() {
|
|
@@ -25,6 +162,7 @@ let NileCalendar = class NileCalendar extends NileElement {
|
|
|
25
162
|
this.allowedDates = JSON.stringify({});
|
|
26
163
|
this.range = false;
|
|
27
164
|
this.hideTypes = false;
|
|
165
|
+
this.showTypes = false;
|
|
28
166
|
this.doubleClickUnselect = false;
|
|
29
167
|
this.allowClear = false;
|
|
30
168
|
this.type = 'absolute';
|
|
@@ -33,6 +171,7 @@ let NileCalendar = class NileCalendar extends NileElement {
|
|
|
33
171
|
this.showMonthDropdown = false;
|
|
34
172
|
this.showYearDropdown = false;
|
|
35
173
|
this.dateFormat = 'MM/DD/YYYY';
|
|
174
|
+
this.structuredInput = false;
|
|
36
175
|
this.startDate = null;
|
|
37
176
|
this.endDate = null;
|
|
38
177
|
this.isSelectingStart = true;
|
|
@@ -41,6 +180,145 @@ let NileCalendar = class NileCalendar extends NileElement {
|
|
|
41
180
|
this.currentYear = new Date().getFullYear();
|
|
42
181
|
this.allowedDatesLocal = null;
|
|
43
182
|
this.disabled = false;
|
|
183
|
+
this.variant = 'default';
|
|
184
|
+
this.timezone = 'UTC';
|
|
185
|
+
/** the timezone the consumer configured; Reset restores this (internal changes never overwrite it) */
|
|
186
|
+
this.configuredTimezone = 'UTC';
|
|
187
|
+
/** the zone the component itself is assigning, so updated() can tell it from a consumer change */
|
|
188
|
+
this.internalTimezone = null;
|
|
189
|
+
this.hideTimeFields = [];
|
|
190
|
+
this.expanded = false;
|
|
191
|
+
this.tpLockedHeight = null;
|
|
192
|
+
this.tpHours = null;
|
|
193
|
+
this.tpMinutes = null;
|
|
194
|
+
this.tpSeconds = null;
|
|
195
|
+
this.tpMilliseconds = null;
|
|
196
|
+
this.tpFocusedTimeField = null;
|
|
197
|
+
/** the day the keyboard last moved to; owns the grid's single tab stop (roving tabindex) */
|
|
198
|
+
this.tpFocusDate = null;
|
|
199
|
+
this.tpEditingText = '';
|
|
200
|
+
this.seedInstant = null;
|
|
201
|
+
this.seedFieldsKey = null;
|
|
202
|
+
this.seedZone = null;
|
|
203
|
+
/** the `.base--date-time-picker` element the observer currently watches */
|
|
204
|
+
this.tpObservedBase = null;
|
|
205
|
+
this.maskValuesMap = new WeakMap();
|
|
206
|
+
this.boundMaskInputs = [];
|
|
207
|
+
this.onMaskKeydown = (e) => {
|
|
208
|
+
if (this.disabled || e.ctrlKey || e.metaKey || e.altKey)
|
|
209
|
+
return;
|
|
210
|
+
const nav = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End',
|
|
211
|
+
'Tab', 'Enter', 'Shift', 'Control', 'Meta', 'Alt', 'CapsLock'];
|
|
212
|
+
if (nav.includes(e.key))
|
|
213
|
+
return;
|
|
214
|
+
const nileInput = e.currentTarget;
|
|
215
|
+
const native = nileInput?.input;
|
|
216
|
+
if (!nileInput || !native)
|
|
217
|
+
return;
|
|
218
|
+
const kind = nileInput.classList.contains('time-input') ? 'time' : 'date';
|
|
219
|
+
const layout = this.maskLayoutFor(kind);
|
|
220
|
+
const values = this.maskValuesFor(native, layout);
|
|
221
|
+
const { slotPos } = this.computeMaskDisplay(values, layout);
|
|
222
|
+
const selStart = native.selectionStart ?? 0;
|
|
223
|
+
const selEnd = native.selectionEnd ?? selStart;
|
|
224
|
+
const writeMask = (display, caret) => {
|
|
225
|
+
native.value = display;
|
|
226
|
+
nileInput.value = display;
|
|
227
|
+
try {
|
|
228
|
+
native.setSelectionRange(caret, caret);
|
|
229
|
+
}
|
|
230
|
+
catch { /* noop */ }
|
|
231
|
+
};
|
|
232
|
+
const caretAfter = (target) => {
|
|
233
|
+
const after = this.computeMaskDisplay(values, layout);
|
|
234
|
+
const np = after.slotPos[target + 1];
|
|
235
|
+
if (np !== undefined && np !== -1)
|
|
236
|
+
return np;
|
|
237
|
+
return after.display.length;
|
|
238
|
+
};
|
|
239
|
+
if (e.key >= '0' && e.key <= '9') {
|
|
240
|
+
e.preventDefault();
|
|
241
|
+
let target = slotPos.findIndex(p => p !== -1 && p >= selStart);
|
|
242
|
+
if (target === -1)
|
|
243
|
+
target = values.findIndex(v => v === ''); // append at the first gap
|
|
244
|
+
if (target === -1)
|
|
245
|
+
return; // every slot full and caret past the end
|
|
246
|
+
values[target] = e.key;
|
|
247
|
+
this.clampMaskSegment(values, layout, target);
|
|
248
|
+
writeMask(this.computeMaskDisplay(values, layout).display, caretAfter(target));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (e.key === 'Backspace') {
|
|
252
|
+
e.preventDefault();
|
|
253
|
+
if (selStart !== selEnd) {
|
|
254
|
+
for (let i = 0; i < values.length; i++) {
|
|
255
|
+
if (slotPos[i] !== -1 && slotPos[i] >= selStart && slotPos[i] < selEnd)
|
|
256
|
+
values[i] = '';
|
|
257
|
+
}
|
|
258
|
+
writeMask(this.computeMaskDisplay(values, layout).display, selStart);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
let target = -1;
|
|
262
|
+
for (let i = 0; i < values.length; i++) {
|
|
263
|
+
if (values[i] !== '' && slotPos[i] !== -1 && slotPos[i] < selStart)
|
|
264
|
+
target = i;
|
|
265
|
+
}
|
|
266
|
+
if (target === -1)
|
|
267
|
+
return;
|
|
268
|
+
values[target] = '';
|
|
269
|
+
const after = this.computeMaskDisplay(values, layout);
|
|
270
|
+
writeMask(after.display, after.slotPos[target] !== -1 ? after.slotPos[target] : Math.max(0, selStart - 1));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (e.key === 'Delete') {
|
|
274
|
+
e.preventDefault();
|
|
275
|
+
let target = -1;
|
|
276
|
+
for (let i = 0; i < values.length; i++) {
|
|
277
|
+
if (values[i] !== '' && slotPos[i] !== -1 && slotPos[i] >= selStart) {
|
|
278
|
+
target = i;
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (target === -1)
|
|
283
|
+
return;
|
|
284
|
+
values[target] = '';
|
|
285
|
+
const after = this.computeMaskDisplay(values, layout);
|
|
286
|
+
writeMask(after.display, after.slotPos[target] !== -1 ? after.slotPos[target] : selStart);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (e.key.length === 1)
|
|
290
|
+
e.preventDefault(); // block any other printable key
|
|
291
|
+
};
|
|
292
|
+
this.onMaskBlur = (e) => {
|
|
293
|
+
const nileInput = e.currentTarget;
|
|
294
|
+
const native = nileInput?.input;
|
|
295
|
+
if (!nileInput || !native)
|
|
296
|
+
return;
|
|
297
|
+
const kind = nileInput.classList.contains('time-input') ? 'time' : 'date';
|
|
298
|
+
const layout = this.maskLayoutFor(kind);
|
|
299
|
+
const values = this.maskValuesFor(native, layout);
|
|
300
|
+
if (!values.some(v => v !== '')) {
|
|
301
|
+
native.value = '';
|
|
302
|
+
nileInput.value = '';
|
|
303
|
+
nileInput.emit?.('nile-change', { value: '' });
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
// left-pad any partially-typed segment, then clamp each into range
|
|
307
|
+
const segStarts = [...new Set(layout.slots.map(s => s.segStart))];
|
|
308
|
+
for (const start of segStarts) {
|
|
309
|
+
const idxs = layout.slots.map((s, i) => (s.segStart === start ? i : -1)).filter(i => i !== -1);
|
|
310
|
+
const digits = idxs.map(i => values[i]).filter(c => c !== '');
|
|
311
|
+
if (digits.length > 0 && digits.length < idxs.length) {
|
|
312
|
+
const padded = digits.join('').padStart(idxs.length, '0');
|
|
313
|
+
idxs.forEach((i, k) => { values[i] = padded[k]; });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
segStarts.forEach(start => this.clampMaskSegment(values, layout, start));
|
|
317
|
+
const { display } = this.computeMaskDisplay(values, layout);
|
|
318
|
+
native.value = display;
|
|
319
|
+
nileInput.value = display;
|
|
320
|
+
nileInput.emit?.('nile-change', { value: display });
|
|
321
|
+
};
|
|
44
322
|
}
|
|
45
323
|
/**
|
|
46
324
|
* The styles for NileCalendar
|
|
@@ -49,42 +327,182 @@ let NileCalendar = class NileCalendar extends NileElement {
|
|
|
49
327
|
static get styles() {
|
|
50
328
|
return [styles];
|
|
51
329
|
}
|
|
330
|
+
setTimezoneInternally(tz) {
|
|
331
|
+
if (tz !== this.timezone)
|
|
332
|
+
this.internalTimezone = tz;
|
|
333
|
+
this.timezone = tz;
|
|
334
|
+
}
|
|
335
|
+
/** every field shown, minus the keys in hideTimeFields (a non-array value counts as an empty list) */
|
|
336
|
+
get resolvedTimeFields() {
|
|
337
|
+
const merged = {
|
|
338
|
+
hours: true,
|
|
339
|
+
minutes: true,
|
|
340
|
+
seconds: true,
|
|
341
|
+
milliseconds: true,
|
|
342
|
+
timezone: true,
|
|
343
|
+
};
|
|
344
|
+
const hidden = Array.isArray(this.hideTimeFields) ? this.hideTimeFields : [];
|
|
345
|
+
for (const key of hidden)
|
|
346
|
+
merged[key] = false;
|
|
347
|
+
return merged;
|
|
348
|
+
}
|
|
349
|
+
get tpFewFieldsNoTz() {
|
|
350
|
+
const f = this.resolvedTimeFields;
|
|
351
|
+
if (f.timezone !== false)
|
|
352
|
+
return false;
|
|
353
|
+
const timeCount = ['hours', 'minutes', 'seconds', 'milliseconds'].filter(k => f[k] !== false).length;
|
|
354
|
+
return timeCount <= 2;
|
|
355
|
+
}
|
|
52
356
|
connectedCallback() {
|
|
53
357
|
super.connectedCallback();
|
|
358
|
+
// before initializeValue: a zoned/bracketed value seeds the zone internally and must not become the configured one
|
|
359
|
+
if (!this.hasUpdated)
|
|
360
|
+
this.configuredTimezone = this.timezone;
|
|
54
361
|
this.initializeValue();
|
|
362
|
+
// firstUpdated runs once; a re-attach (DOM move) must restore the structuredInput listeners disconnectedCallback removed (no-op unless structuredInput)
|
|
363
|
+
if (this.hasUpdated)
|
|
364
|
+
this.bindStructuredInput();
|
|
365
|
+
// disconnectedCallback dropped the observer; an already-rendered picker panel needs it back (updated() won't run on a bare re-attach)
|
|
366
|
+
if (this.hasUpdated && this.variant === 'date-time-picker')
|
|
367
|
+
this.observeCollapsedBase();
|
|
55
368
|
this.emit('nile-init');
|
|
56
369
|
}
|
|
57
370
|
disconnectedCallback() {
|
|
58
371
|
super.disconnectedCallback();
|
|
372
|
+
this.releasePanelObserver();
|
|
373
|
+
this.unbindStructuredInput();
|
|
59
374
|
this.emit('nile-destroy');
|
|
60
375
|
}
|
|
376
|
+
firstUpdated() {
|
|
377
|
+
this.bindStructuredInput();
|
|
378
|
+
}
|
|
61
379
|
updated(changedProperties) {
|
|
62
380
|
super.updated(changedProperties);
|
|
381
|
+
if (changedProperties.has('timezone')) {
|
|
382
|
+
if (this.timezone !== this.internalTimezone)
|
|
383
|
+
this.configuredTimezone = this.timezone;
|
|
384
|
+
this.internalTimezone = null;
|
|
385
|
+
}
|
|
386
|
+
if (changedProperties.has('timezone') &&
|
|
387
|
+
!changedProperties.has('valueAttribute') &&
|
|
388
|
+
this.seedInstant !== null) {
|
|
389
|
+
if (this.seedZone !== null) {
|
|
390
|
+
if (this.timezone !== this.seedZone)
|
|
391
|
+
this.clearInstantSeed();
|
|
392
|
+
}
|
|
393
|
+
else if (this.seedFieldsKey === this.instantSeedFieldsKey()) {
|
|
394
|
+
if (isValidTimezone(this.timezone)) {
|
|
395
|
+
this.seedFromInstant(this.seedInstant, this.timezone);
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
this.clearInstantSeed();
|
|
399
|
+
this.initializeValue();
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
this.clearInstantSeed();
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (changedProperties.has('structuredInput') ||
|
|
407
|
+
changedProperties.has('variant') ||
|
|
408
|
+
changedProperties.has('range') ||
|
|
409
|
+
changedProperties.has('showManualInputs') ||
|
|
410
|
+
changedProperties.has('type') ||
|
|
411
|
+
changedProperties.has('dateFormat')) {
|
|
412
|
+
this.bindStructuredInput();
|
|
413
|
+
}
|
|
63
414
|
if (changedProperties.has('valueAttribute')) {
|
|
64
415
|
if (!this.valueAttribute) {
|
|
65
416
|
this.value = null;
|
|
66
417
|
this.startDate = null;
|
|
67
418
|
this.endDate = null;
|
|
68
419
|
this.isSelectingStart = true;
|
|
420
|
+
if (this.variant === 'date-time-picker')
|
|
421
|
+
this.resetTimeUnits();
|
|
69
422
|
}
|
|
70
|
-
const date =
|
|
423
|
+
const date = this.dateFromValueString(this.valueAttribute || '');
|
|
71
424
|
if (!isNaN(date.getTime())) {
|
|
72
|
-
|
|
73
|
-
this.value = new Date(date.getTime() - offset * 60 * 1000);
|
|
425
|
+
this.value = date;
|
|
74
426
|
this.currentMonth = this.value.getMonth();
|
|
75
427
|
this.currentYear = this.value.getFullYear();
|
|
76
428
|
}
|
|
77
429
|
this.initializeValue();
|
|
78
430
|
}
|
|
431
|
+
if (this.variant === 'date-time-picker') {
|
|
432
|
+
this.applyMonthYearDropdownAnimation();
|
|
433
|
+
this.captureCollapsedHeight();
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
this.releasePanelObserver(); // the default variant has no picker panel to watch (a variant switch removed it)
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
releasePanelObserver() {
|
|
440
|
+
this.tpResizeObserver?.disconnect();
|
|
441
|
+
this.tpResizeObserver = undefined;
|
|
442
|
+
this.tpObservedBase = null;
|
|
443
|
+
}
|
|
444
|
+
/** (re)points the observer at the rendered panel; a no-op while it already watches that element */
|
|
445
|
+
observeCollapsedBase() {
|
|
446
|
+
const base = this.shadowRoot?.querySelector('.base--date-time-picker');
|
|
447
|
+
if (!base) {
|
|
448
|
+
this.releasePanelObserver();
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
if (this.tpObservedBase !== base) {
|
|
452
|
+
this.tpResizeObserver?.disconnect();
|
|
453
|
+
this.tpResizeObserver = new ResizeObserver(() => this.measurePanel(base));
|
|
454
|
+
this.tpResizeObserver.observe(base);
|
|
455
|
+
this.tpObservedBase = base;
|
|
456
|
+
}
|
|
457
|
+
return base;
|
|
458
|
+
}
|
|
459
|
+
captureCollapsedHeight() {
|
|
460
|
+
const base = this.observeCollapsedBase();
|
|
461
|
+
if (base)
|
|
462
|
+
this.measurePanel(base);
|
|
463
|
+
}
|
|
464
|
+
measurePanel(base) {
|
|
465
|
+
if (this.expanded)
|
|
466
|
+
return;
|
|
467
|
+
const h = `${base.offsetHeight}px`;
|
|
468
|
+
if (h !== this.tpLockedHeight)
|
|
469
|
+
this.tpLockedHeight = h;
|
|
470
|
+
}
|
|
471
|
+
applyMonthYearDropdownAnimation() {
|
|
472
|
+
const apply = (root) => {
|
|
473
|
+
root?.querySelectorAll('.month-dropdown, .year-dropdown').forEach(dd => {
|
|
474
|
+
setAnimation(dd, 'dropdown.show', {
|
|
475
|
+
keyframes: [
|
|
476
|
+
{ opacity: 0, scale: 0.9, translate: '0 -14px' },
|
|
477
|
+
{ opacity: 1, scale: 1, translate: '0 0' },
|
|
478
|
+
],
|
|
479
|
+
options: { duration: 280, easing: 'cubic-bezier(0.16, 1, 0.3, 1)' },
|
|
480
|
+
});
|
|
481
|
+
setAnimation(dd, 'dropdown.hide', {
|
|
482
|
+
keyframes: [
|
|
483
|
+
{ opacity: 1, scale: 1, translate: '0 0' },
|
|
484
|
+
{ opacity: 0, scale: 0.9, translate: '0 -14px' },
|
|
485
|
+
],
|
|
486
|
+
options: { duration: 160, easing: 'ease' },
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
};
|
|
490
|
+
apply(this.shadowRoot);
|
|
491
|
+
const inner = this.shadowRoot?.querySelector('.tp-inner-calendar');
|
|
492
|
+
if (inner) {
|
|
493
|
+
const innerApply = () => apply(inner.shadowRoot);
|
|
494
|
+
if (inner.updateComplete)
|
|
495
|
+
inner.updateComplete.then(innerApply);
|
|
496
|
+
else
|
|
497
|
+
innerApply();
|
|
498
|
+
}
|
|
79
499
|
}
|
|
80
500
|
get monthNames() {
|
|
81
501
|
return Array.from({ length: 12 }, (_, i) => new Date(0, i).toLocaleString('default', { month: 'long' }));
|
|
82
502
|
}
|
|
83
503
|
get yearOptions() {
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
const start = this.startYear ?? fallbackStart;
|
|
87
|
-
const end = this.endYear ?? fallbackEnd;
|
|
504
|
+
const start = this.startYear ?? DEFAULT_START_YEAR;
|
|
505
|
+
const end = this.endYear ?? DEFAULT_END_YEAR;
|
|
88
506
|
if (start > end)
|
|
89
507
|
return [];
|
|
90
508
|
return Array.from({ length: end - start + 1 }, (_, i) => start + i);
|
|
@@ -97,6 +515,42 @@ let NileCalendar = class NileCalendar extends NileElement {
|
|
|
97
515
|
this.currentYear = year;
|
|
98
516
|
this.emit('nile-year-change', { year });
|
|
99
517
|
}
|
|
518
|
+
/** brings the currently-selected month/year option into view when a dropdown opens */
|
|
519
|
+
scrollSelectedIntoView(selector) {
|
|
520
|
+
const selected = this.shadowRoot?.querySelector(selector);
|
|
521
|
+
if (!selected)
|
|
522
|
+
return;
|
|
523
|
+
const container = selected.closest('.month-scroll-wrapper, .year-scroll-wrapper');
|
|
524
|
+
if (!container) {
|
|
525
|
+
selected.scrollIntoView({ block: 'center' });
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const target = container.scrollTop +
|
|
529
|
+
(selected.getBoundingClientRect().top -
|
|
530
|
+
container.getBoundingClientRect().top) -
|
|
531
|
+
(container.clientHeight - selected.clientHeight) / 2;
|
|
532
|
+
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
533
|
+
if (prefersReducedMotion) {
|
|
534
|
+
container.scrollTop = target;
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
this.smoothScrollTo(container, target, 450);
|
|
538
|
+
}
|
|
539
|
+
smoothScrollTo(el, to, duration) {
|
|
540
|
+
const start = el.scrollTop;
|
|
541
|
+
const change = to - start;
|
|
542
|
+
if (change === 0)
|
|
543
|
+
return;
|
|
544
|
+
const startTime = performance.now();
|
|
545
|
+
const ease = (t) => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
|
|
546
|
+
const step = (now) => {
|
|
547
|
+
const t = Math.min(1, (now - startTime) / duration);
|
|
548
|
+
el.scrollTop = start + change * ease(t);
|
|
549
|
+
if (t < 1)
|
|
550
|
+
requestAnimationFrame(step);
|
|
551
|
+
};
|
|
552
|
+
requestAnimationFrame(step);
|
|
553
|
+
}
|
|
100
554
|
checkValidAllowedDate() {
|
|
101
555
|
let newDateRange;
|
|
102
556
|
try {
|
|
@@ -119,15 +573,25 @@ let NileCalendar = class NileCalendar extends NileElement {
|
|
|
119
573
|
this.allowedDatesLocal = newDateRange;
|
|
120
574
|
}
|
|
121
575
|
}
|
|
576
|
+
/**
|
|
577
|
+
* The year range month navigation enforces. An explicit `startYear` / `endYear` always wins; the date-time-picker
|
|
578
|
+
* variant falls back to the dropdown's 2000-2050 for an unset side, while the default calendar leaves it open as published.
|
|
579
|
+
*/
|
|
580
|
+
get minYear() {
|
|
581
|
+
return this.variant === 'date-time-picker' ? this.startYear ?? DEFAULT_START_YEAR : this.startYear;
|
|
582
|
+
}
|
|
583
|
+
get maxYear() {
|
|
584
|
+
return this.variant === 'date-time-picker' ? this.endYear ?? DEFAULT_END_YEAR : this.endYear;
|
|
585
|
+
}
|
|
122
586
|
isPrevDisabled() {
|
|
123
587
|
return (this.currentMonth === 0 &&
|
|
124
|
-
this.
|
|
125
|
-
this.currentYear <= this.
|
|
588
|
+
this.minYear !== undefined &&
|
|
589
|
+
this.currentYear <= this.minYear);
|
|
126
590
|
}
|
|
127
591
|
isNextDisabled() {
|
|
128
592
|
return (this.currentMonth === 11 &&
|
|
129
|
-
this.
|
|
130
|
-
this.currentYear >= this.
|
|
593
|
+
this.maxYear !== undefined &&
|
|
594
|
+
this.currentYear >= this.maxYear);
|
|
131
595
|
}
|
|
132
596
|
formatDate(date) {
|
|
133
597
|
if (!date)
|
|
@@ -136,7 +600,8 @@ let NileCalendar = class NileCalendar extends NileElement {
|
|
|
136
600
|
const mm = String(date.getMonth() + 1).padStart(2, '0');
|
|
137
601
|
const dd = String(date.getDate()).padStart(2, '0');
|
|
138
602
|
let formatted = this.dateFormat;
|
|
139
|
-
|
|
603
|
+
// the date-time-picker variant pads years 0-99 to four digits; the default calendar prints the year as published
|
|
604
|
+
formatted = formatted.replace(/YYYY/g, this.variant === 'date-time-picker' ? formatYear4(yyyy) : String(yyyy));
|
|
140
605
|
formatted = formatted.replace(/MM/g, mm);
|
|
141
606
|
formatted = formatted.replace(/DD/g, dd);
|
|
142
607
|
formatted = formatted.replace(/YY/g, String(yyyy).slice(-2));
|
|
@@ -154,10 +619,17 @@ let NileCalendar = class NileCalendar extends NileElement {
|
|
|
154
619
|
const match = input.match(regex);
|
|
155
620
|
if (!match || !match.groups)
|
|
156
621
|
return null;
|
|
157
|
-
const
|
|
622
|
+
const dtp = this.variant === 'date-time-picker';
|
|
623
|
+
const typedYear = match.groups.year;
|
|
624
|
+
let year = typedYear.length === 2 ? Number(`20${typedYear}`) : Number(typedYear);
|
|
625
|
+
// date-time-picker only: an existing bound keeps its century while the visible two digits are unchanged; with no existing bound a typed YY is 2000-2099 (the default calendar reads it as published)
|
|
626
|
+
if (dtp && typedYear.length === 2 && existingDate && Math.abs(existingDate.getFullYear()) % 100 === Number(typedYear)) {
|
|
627
|
+
year = existingDate.getFullYear();
|
|
628
|
+
}
|
|
158
629
|
const month = Number(match.groups.month) - 1;
|
|
159
630
|
const day = Number(match.groups.day);
|
|
160
|
-
|
|
631
|
+
// new Date(y, ...) remaps years 0-99 to 1900-1999 as published; only the variant keeps the literal year
|
|
632
|
+
const date = dtp ? localDateFromFields(year, month, day) : new Date(year, month, day);
|
|
161
633
|
if (existingDate) {
|
|
162
634
|
date.setHours(existingDate.getHours(), existingDate.getMinutes(), existingDate.getSeconds());
|
|
163
635
|
}
|
|
@@ -188,10 +660,131 @@ let NileCalendar = class NileCalendar extends NileElement {
|
|
|
188
660
|
this.requestUpdate();
|
|
189
661
|
}
|
|
190
662
|
}
|
|
663
|
+
unbindStructuredInput() {
|
|
664
|
+
for (const inp of this.boundMaskInputs) {
|
|
665
|
+
inp.removeEventListener('keydown', this.onMaskKeydown, true);
|
|
666
|
+
inp.removeEventListener('focusout', this.onMaskBlur);
|
|
667
|
+
}
|
|
668
|
+
this.boundMaskInputs = [];
|
|
669
|
+
}
|
|
670
|
+
bindStructuredInput() {
|
|
671
|
+
this.unbindStructuredInput();
|
|
672
|
+
if (!this.structuredInput)
|
|
673
|
+
return;
|
|
674
|
+
const inputs = this.shadowRoot?.querySelectorAll('.calender-input nile-input');
|
|
675
|
+
inputs?.forEach(inp => {
|
|
676
|
+
inp.addEventListener('keydown', this.onMaskKeydown, true);
|
|
677
|
+
inp.addEventListener('focusout', this.onMaskBlur);
|
|
678
|
+
this.boundMaskInputs.push(inp);
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
dateMaskSegments() {
|
|
682
|
+
const spec = (token) => {
|
|
683
|
+
switch (token) {
|
|
684
|
+
case 'YYYY': return { kind: 'year', len: 4, min: 0, max: 9999 };
|
|
685
|
+
case 'YY': return { kind: 'year', len: 2, min: 0, max: 99 };
|
|
686
|
+
case 'MM': return { kind: 'month', len: 2, min: 1, max: 12 };
|
|
687
|
+
default: return { kind: 'day', len: 2, min: 1, max: 31 }; // DD
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
const fmt = this.dateFormat || 'MM/DD/YYYY';
|
|
691
|
+
const re = /YYYY|YY|MM|DD/g;
|
|
692
|
+
const found = [];
|
|
693
|
+
let m;
|
|
694
|
+
while ((m = re.exec(fmt)) !== null)
|
|
695
|
+
found.push({ token: m[0], index: m.index });
|
|
696
|
+
return found.map((f, i) => {
|
|
697
|
+
const sep = fmt.slice(f.index + f.token.length, i + 1 < found.length ? found[i + 1].index : fmt.length);
|
|
698
|
+
return { ...spec(f.token), after: i < found.length - 1 ? sep : '' };
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
timeMaskSegments() {
|
|
702
|
+
return [
|
|
703
|
+
{ kind: 'hour', len: 2, min: 0, max: 23, after: ':' },
|
|
704
|
+
{ kind: 'minute', len: 2, min: 0, max: 59, after: ':' },
|
|
705
|
+
{ kind: 'second', len: 2, min: 0, max: 59, after: '' },
|
|
706
|
+
];
|
|
707
|
+
}
|
|
708
|
+
buildMaskLayout(segs) {
|
|
709
|
+
const tokens = [];
|
|
710
|
+
const slots = [];
|
|
711
|
+
for (const seg of segs) {
|
|
712
|
+
const segStart = slots.length;
|
|
713
|
+
for (let k = 0; k < seg.len; k++) {
|
|
714
|
+
tokens.push({ slot: slots.length });
|
|
715
|
+
slots.push({ kind: seg.kind, min: seg.min, max: seg.max, len: seg.len, segStart });
|
|
716
|
+
}
|
|
717
|
+
for (const ch of seg.after)
|
|
718
|
+
tokens.push({ lit: ch });
|
|
719
|
+
}
|
|
720
|
+
return { tokens, slots, length: tokens.length };
|
|
721
|
+
}
|
|
722
|
+
maskLayoutFor(kind) {
|
|
723
|
+
return this.buildMaskLayout(kind === 'time' ? this.timeMaskSegments() : this.dateMaskSegments());
|
|
724
|
+
}
|
|
725
|
+
computeMaskDisplay(values, layout) {
|
|
726
|
+
let lastFilled = -1;
|
|
727
|
+
values.forEach((v, i) => { if (v !== '')
|
|
728
|
+
lastFilled = i; });
|
|
729
|
+
let s = '';
|
|
730
|
+
const slotPos = new Array(layout.slots.length).fill(-1);
|
|
731
|
+
for (const t of layout.tokens) {
|
|
732
|
+
if ('slot' in t) {
|
|
733
|
+
if (t.slot > lastFilled)
|
|
734
|
+
break; // stop at the trailing empties
|
|
735
|
+
slotPos[t.slot] = s.length;
|
|
736
|
+
if (values[t.slot])
|
|
737
|
+
s += values[t.slot];
|
|
738
|
+
}
|
|
739
|
+
else {
|
|
740
|
+
s += t.lit;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return { display: lastFilled === -1 ? '' : s, slotPos, lastFilled };
|
|
744
|
+
}
|
|
745
|
+
initMaskValues(text, layout) {
|
|
746
|
+
const digits = (text || '').replace(/\D/g, '').slice(0, layout.slots.length);
|
|
747
|
+
return layout.slots.map((_s, i) => digits[i] ?? '');
|
|
748
|
+
}
|
|
749
|
+
/** clamp a filled segment into range (year is left unclamped) */
|
|
750
|
+
clampMaskSegment(values, layout, slotIndex) {
|
|
751
|
+
const seg = layout.slots[slotIndex];
|
|
752
|
+
if (seg.kind === 'year')
|
|
753
|
+
return;
|
|
754
|
+
const segSlots = [];
|
|
755
|
+
layout.slots.forEach((s, i) => { if (s.segStart === seg.segStart)
|
|
756
|
+
segSlots.push(i); });
|
|
757
|
+
const chars = segSlots.map(i => values[i]);
|
|
758
|
+
if (chars.some(c => c === ''))
|
|
759
|
+
return; // segment not full yet
|
|
760
|
+
const joined = chars.join('');
|
|
761
|
+
const n = Number(joined);
|
|
762
|
+
let corrected = null;
|
|
763
|
+
if (n > seg.max)
|
|
764
|
+
corrected = String(Number(joined[0])).padStart(seg.len, '0');
|
|
765
|
+
else if (n < seg.min)
|
|
766
|
+
corrected = String(seg.min).padStart(seg.len, '0');
|
|
767
|
+
if (corrected !== null)
|
|
768
|
+
segSlots.forEach((s, k) => { values[s] = corrected[k]; });
|
|
769
|
+
}
|
|
770
|
+
maskValuesFor(native, layout) {
|
|
771
|
+
let values = this.maskValuesMap.get(native);
|
|
772
|
+
if (!values || values.length !== layout.slots.length) {
|
|
773
|
+
values = this.initMaskValues(native.value, layout);
|
|
774
|
+
}
|
|
775
|
+
else if (this.computeMaskDisplay(values, layout).display !== native.value) {
|
|
776
|
+
values = this.initMaskValues(native.value, layout);
|
|
777
|
+
}
|
|
778
|
+
this.maskValuesMap.set(native, values);
|
|
779
|
+
return values;
|
|
780
|
+
}
|
|
191
781
|
/**
|
|
192
782
|
* Render method
|
|
193
783
|
*/
|
|
194
784
|
render() {
|
|
785
|
+
if (this.variant === 'date-time-picker') {
|
|
786
|
+
return this.renderDateTimePicker();
|
|
787
|
+
}
|
|
195
788
|
return html `
|
|
196
789
|
<div
|
|
197
790
|
class=${classMap({
|
|
@@ -357,7 +950,37 @@ let NileCalendar = class NileCalendar extends NileElement {
|
|
|
357
950
|
</div>
|
|
358
951
|
</div>
|
|
359
952
|
|
|
360
|
-
${this.
|
|
953
|
+
${this.variant === 'date-time-picker' ? html `
|
|
954
|
+
<div>
|
|
955
|
+
<div class="unit-input-container">
|
|
956
|
+
<nile-input
|
|
957
|
+
class="manual-input duration-input"
|
|
958
|
+
?disabled="${this.disabled}"
|
|
959
|
+
label="Duration"
|
|
960
|
+
inputmode="numeric"
|
|
961
|
+
type="text"
|
|
962
|
+
value="${this.selectedValue}"
|
|
963
|
+
@nile-input="${this.handleDurationChange}"
|
|
964
|
+
@nile-change="${this.handleDurationChange}"
|
|
965
|
+
placeholder="Enter Value"
|
|
966
|
+
></nile-input>
|
|
967
|
+
|
|
968
|
+
<nile-select
|
|
969
|
+
class="manual-input time-input"
|
|
970
|
+
?disabled="${this.disabled}"
|
|
971
|
+
label="Unit of time"
|
|
972
|
+
value="${this.selectedUnit}"
|
|
973
|
+
@nile-change="${this.handleUnitChange}"
|
|
974
|
+
>
|
|
975
|
+
${this.hideDurationFields?.includes('minutes') ? '' : html `<nile-option value="minutes">Minutes</nile-option>`}
|
|
976
|
+
${this.hideDurationFields?.includes('hours') ? '' : html `<nile-option value="hours">Hours</nile-option>`}
|
|
977
|
+
${this.hideDurationFields?.includes('days') ? '' : html `<nile-option value="days">Days</nile-option>`}
|
|
978
|
+
${this.hideDurationFields?.includes('weeks') ? '' : html `<nile-option value="weeks">Weeks</nile-option>`}
|
|
979
|
+
${this.hideDurationFields?.includes('months') ? '' : html `<nile-option value="months">Months</nile-option>`}
|
|
980
|
+
</nile-select>
|
|
981
|
+
</div>
|
|
982
|
+
</div>
|
|
983
|
+
` : this.range ? html `
|
|
361
984
|
<div>
|
|
362
985
|
<div class="unit-input-container">
|
|
363
986
|
<nile-input
|
|
@@ -602,13 +1225,19 @@ ${this.showMonthDropdown || this.showYearDropdown ? html `
|
|
|
602
1225
|
if (!this.allowedDatesLocal) {
|
|
603
1226
|
return true;
|
|
604
1227
|
}
|
|
605
|
-
|
|
1228
|
+
// Date.UTC remaps years 0-99 to 1900-1999 as published; only the date-time-picker variant keeps the literal year
|
|
1229
|
+
const dateToCheck = new Date(this.variant === 'date-time-picker' ? utcFromFields(year, month, day) : Date.UTC(year, month, day));
|
|
606
1230
|
const startDate = this.getUTCDate(this.allowedDatesLocal.startDate);
|
|
607
1231
|
const endDate = this.getUTCDate(this.allowedDatesLocal.endDate);
|
|
608
1232
|
endDate.setUTCHours(23, 59, 59, 999);
|
|
609
1233
|
const isWithinRange = dateToCheck >= startDate && dateToCheck <= endDate;
|
|
610
1234
|
return isWithinRange;
|
|
611
1235
|
}
|
|
1236
|
+
dayCellLabel(day, month, year) {
|
|
1237
|
+
return localDateFromFields(year, month, day, 12).toLocaleDateString('default', {
|
|
1238
|
+
weekday: 'long', day: 'numeric', month: 'long', year: 'numeric',
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
612
1241
|
clearDate(mode) {
|
|
613
1242
|
const clearType = mode ??
|
|
614
1243
|
(this.range
|
|
@@ -616,6 +1245,7 @@ ${this.showMonthDropdown || this.showYearDropdown ? html `
|
|
|
616
1245
|
? ClearMode.Relative
|
|
617
1246
|
: ClearMode.Range
|
|
618
1247
|
: ClearMode.Single);
|
|
1248
|
+
this.clearInstantSeed();
|
|
619
1249
|
if (clearType === ClearMode.Single) {
|
|
620
1250
|
this.value = null;
|
|
621
1251
|
}
|
|
@@ -638,7 +1268,8 @@ ${this.showMonthDropdown || this.showYearDropdown ? html `
|
|
|
638
1268
|
selectDate(day, month, year) {
|
|
639
1269
|
if (this.disabled)
|
|
640
1270
|
return;
|
|
641
|
-
|
|
1271
|
+
// new Date(y, ...) remaps years 0-99 to 1900-1999 as published; only the date-time-picker variant keeps the literal year
|
|
1272
|
+
const selectedDate = this.variant === 'date-time-picker' ? localDateFromFields(year, month, day) : new Date(year, month, day);
|
|
642
1273
|
if (this.range) {
|
|
643
1274
|
if (this.startDate && this.endDate) {
|
|
644
1275
|
this.startDate = null;
|
|
@@ -678,14 +1309,20 @@ ${this.showMonthDropdown || this.showYearDropdown ? html `
|
|
|
678
1309
|
* Function to be called on initialization to set all other properties
|
|
679
1310
|
*/
|
|
680
1311
|
initializeValue() {
|
|
1312
|
+
this.clearInstantSeed();
|
|
681
1313
|
if (this.range) {
|
|
682
1314
|
try {
|
|
683
1315
|
const rangeValue = JSON.parse(this.valueAttribute || '');
|
|
684
|
-
this.startDate =
|
|
685
|
-
this.endDate =
|
|
1316
|
+
this.startDate = this.rangeBoundFromValue(rangeValue.startDate, false);
|
|
1317
|
+
this.endDate = this.rangeBoundFromValue(rangeValue.endDate, true);
|
|
686
1318
|
// Convert to local time
|
|
687
1319
|
this.startDate = new Date(this.startDate.getTime());
|
|
688
1320
|
this.endDate = new Date(this.endDate.getTime());
|
|
1321
|
+
// date-time-picker variant only: open on the seeded range's start month (the default variant keeps its month)
|
|
1322
|
+
if (this.variant === 'date-time-picker' && this.startDate && !isNaN(this.startDate.getTime())) {
|
|
1323
|
+
this.currentMonth = this.startDate.getMonth();
|
|
1324
|
+
this.currentYear = this.startDate.getFullYear();
|
|
1325
|
+
}
|
|
689
1326
|
this.value = null;
|
|
690
1327
|
}
|
|
691
1328
|
catch (e) {
|
|
@@ -693,9 +1330,58 @@ ${this.showMonthDropdown || this.showYearDropdown ? html `
|
|
|
693
1330
|
}
|
|
694
1331
|
}
|
|
695
1332
|
else {
|
|
1333
|
+
if (this.variant === 'date-time-picker' && this.trySeedFromInstantValue()) {
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
const zoned = this.variant === 'date-time-picker'
|
|
1337
|
+
? this.parseZonedValue(this.valueAttribute)
|
|
1338
|
+
: null;
|
|
1339
|
+
if (zoned) {
|
|
1340
|
+
this.setTimezoneInternally(zoned.timezone);
|
|
1341
|
+
const w = zoned.wall;
|
|
1342
|
+
this.value = localDateFromFields(w.year, w.month - 1, w.day, 12, 0, 0, 0);
|
|
1343
|
+
this.currentMonth = w.month - 1;
|
|
1344
|
+
this.currentYear = w.year;
|
|
1345
|
+
this.tpHours = w.hours;
|
|
1346
|
+
this.tpMinutes = w.minutes;
|
|
1347
|
+
this.tpSeconds = w.seconds;
|
|
1348
|
+
this.tpMilliseconds = w.ms;
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
if (this.variant === 'date-time-picker') {
|
|
1352
|
+
const wall = wallClockFromOffsetLess(this.valueAttribute);
|
|
1353
|
+
if (wall) {
|
|
1354
|
+
this.value = localDateFromFields(wall.year, wall.month - 1, wall.day, 12, 0, 0, 0);
|
|
1355
|
+
this.currentMonth = wall.month - 1;
|
|
1356
|
+
this.currentYear = wall.year;
|
|
1357
|
+
this.tpHours = wall.hours;
|
|
1358
|
+
this.tpMinutes = wall.minutes;
|
|
1359
|
+
this.tpSeconds = wall.seconds;
|
|
1360
|
+
this.tpMilliseconds = wall.ms;
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
if (this.variant === 'date-time-picker') {
|
|
1365
|
+
const preset = this.parsePresetValue(this.valueAttribute);
|
|
1366
|
+
if (preset) {
|
|
1367
|
+
this.value = preset;
|
|
1368
|
+
this.currentMonth = preset.getMonth();
|
|
1369
|
+
this.currentYear = preset.getFullYear();
|
|
1370
|
+
// seed the time-picker spinners from the preset value's wall-clock time
|
|
1371
|
+
this.tpHours = preset.getHours();
|
|
1372
|
+
this.tpMinutes = preset.getMinutes();
|
|
1373
|
+
this.tpSeconds = preset.getSeconds();
|
|
1374
|
+
this.tpMilliseconds = preset.getMilliseconds();
|
|
1375
|
+
}
|
|
1376
|
+
else if (this.valueAttribute) {
|
|
1377
|
+
// an unparseable value must not leave the previous selection showing (the empty-value path above already clears it)
|
|
1378
|
+
this.value = null;
|
|
1379
|
+
this.resetTimeUnits();
|
|
1380
|
+
}
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
696
1383
|
if (this.valueAttribute) {
|
|
697
|
-
|
|
698
|
-
date = new Date(date.getTime() - date.getTimezoneOffset() * 60000);
|
|
1384
|
+
const date = this.dateFromValueString(this.valueAttribute);
|
|
699
1385
|
if (!isNaN(date.getTime())) {
|
|
700
1386
|
this.value = date;
|
|
701
1387
|
this.currentMonth = this.value.getMonth();
|
|
@@ -704,11 +1390,148 @@ ${this.showMonthDropdown || this.showYearDropdown ? html `
|
|
|
704
1390
|
}
|
|
705
1391
|
}
|
|
706
1392
|
}
|
|
1393
|
+
/** Forget any retained instant, in every category. */
|
|
1394
|
+
clearInstantSeed() {
|
|
1395
|
+
this.seedInstant = null;
|
|
1396
|
+
this.seedFieldsKey = null;
|
|
1397
|
+
this.seedZone = null;
|
|
1398
|
+
}
|
|
1399
|
+
instantSeedFieldsKey() {
|
|
1400
|
+
const v = this.value;
|
|
1401
|
+
if (!v)
|
|
1402
|
+
return null;
|
|
1403
|
+
return (`${v.getFullYear()}-${v.getMonth()}-${v.getDate()}|` +
|
|
1404
|
+
`${this.tpHours}:${this.tpMinutes}:${this.tpSeconds}.${this.tpMilliseconds}`);
|
|
1405
|
+
}
|
|
1406
|
+
seedFromInstant(epochMs, zone) {
|
|
1407
|
+
const w = wallClockInZone(epochMs, zone);
|
|
1408
|
+
this.value = localDateFromFields(w.year, w.month - 1, w.day, 12, 0, 0, 0);
|
|
1409
|
+
this.currentMonth = w.month - 1;
|
|
1410
|
+
this.currentYear = w.year;
|
|
1411
|
+
this.tpHours = w.hours;
|
|
1412
|
+
this.tpMinutes = w.minutes;
|
|
1413
|
+
this.tpSeconds = w.seconds;
|
|
1414
|
+
this.tpMilliseconds = w.ms;
|
|
1415
|
+
this.seedInstant = epochMs;
|
|
1416
|
+
this.seedFieldsKey = this.instantSeedFieldsKey();
|
|
1417
|
+
}
|
|
1418
|
+
trySeedFromInstantValue() {
|
|
1419
|
+
const epochMs = instantFromValueString(this.valueAttribute);
|
|
1420
|
+
if (epochMs !== null) {
|
|
1421
|
+
if (!isValidTimezone(this.timezone))
|
|
1422
|
+
return false;
|
|
1423
|
+
this.seedFromInstant(epochMs, this.timezone);
|
|
1424
|
+
this.seedZone = null;
|
|
1425
|
+
return true;
|
|
1426
|
+
}
|
|
1427
|
+
const bracketed = instantFromBracketedValue(this.valueAttribute);
|
|
1428
|
+
if (bracketed) {
|
|
1429
|
+
this.setTimezoneInternally(bracketed.timezone);
|
|
1430
|
+
this.seedFromInstant(bracketed.epochMs, bracketed.timezone);
|
|
1431
|
+
this.seedZone = bracketed.timezone;
|
|
1432
|
+
return true;
|
|
1433
|
+
}
|
|
1434
|
+
return false;
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Seeds `value` from a string with the published conversion: `new Date(raw)` shifted by the host offset. Every variant
|
|
1438
|
+
* keeps it, because the default calendar must behave exactly as the published component; the date-time-picker variant
|
|
1439
|
+
* re-derives its value from its own parsers in `initializeValue`.
|
|
1440
|
+
*/
|
|
1441
|
+
dateFromValueString(raw) {
|
|
1442
|
+
const parsed = new Date(raw);
|
|
1443
|
+
return new Date(parsed.getTime() - parsed.getTimezoneOffset() * 60000);
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* One bound of a `range` value. In the date-time-picker variant `YYYY-MM-DD` is a calendar day, not a UTC instant: it
|
|
1447
|
+
* lands on that local day, at 00:00:00.000 for the start and 23:59:59.999 for the end (what clicking the day produces).
|
|
1448
|
+
* Every other string, and every bound of the default calendar, goes through `new Date(raw)` exactly as published.
|
|
1449
|
+
*/
|
|
1450
|
+
rangeBoundFromValue(raw, isEnd) {
|
|
1451
|
+
if (this.variant === 'date-time-picker' && typeof raw === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(raw)) {
|
|
1452
|
+
const day = this.parsePresetValue(raw);
|
|
1453
|
+
if (day) {
|
|
1454
|
+
if (isEnd)
|
|
1455
|
+
day.setHours(23, 59, 59, 999);
|
|
1456
|
+
return day;
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
return new Date(raw);
|
|
1460
|
+
}
|
|
1461
|
+
parsePresetValue(raw) {
|
|
1462
|
+
if (!raw)
|
|
1463
|
+
return null;
|
|
1464
|
+
const dateOnly = /^\d{4}-\d{2}-\d{2}$/.exec(raw);
|
|
1465
|
+
if (dateOnly) {
|
|
1466
|
+
const year = Number(raw.slice(0, 4));
|
|
1467
|
+
const month = Number(raw.slice(5, 7)) - 1;
|
|
1468
|
+
const day = Number(raw.slice(8, 10));
|
|
1469
|
+
if (month < 0 || month > 11 || day < 1)
|
|
1470
|
+
return null;
|
|
1471
|
+
const lastDay = localDateFromFields(year, month + 1, 0).getDate();
|
|
1472
|
+
const date = localDateFromFields(year, month, Math.min(day, lastDay));
|
|
1473
|
+
return isNaN(date.getTime()) ? null : date;
|
|
1474
|
+
}
|
|
1475
|
+
// only an instant with an unusable zone or an undocumented string gets here; the documented formats are parsed before this
|
|
1476
|
+
const date = new Date(raw);
|
|
1477
|
+
return isNaN(date.getTime()) ? null : date;
|
|
1478
|
+
}
|
|
1479
|
+
parseZonedValue(raw) {
|
|
1480
|
+
if (!raw)
|
|
1481
|
+
return null;
|
|
1482
|
+
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());
|
|
1483
|
+
if (!m)
|
|
1484
|
+
return null;
|
|
1485
|
+
const [, y, mo, d, h, mi, s, ms, rawZone] = m;
|
|
1486
|
+
const month0 = Number(mo) - 1;
|
|
1487
|
+
const hours = Number(h);
|
|
1488
|
+
const minutes = Number(mi);
|
|
1489
|
+
const seconds = Number(s);
|
|
1490
|
+
if (month0 < 0 || month0 > 11 || hours > 23 || minutes > 59 || seconds > 59) {
|
|
1491
|
+
return null;
|
|
1492
|
+
}
|
|
1493
|
+
const day = Number(d);
|
|
1494
|
+
if (day < 1)
|
|
1495
|
+
return null;
|
|
1496
|
+
// clamp instead of letting an out-of-range day roll into the next month
|
|
1497
|
+
const lastDay = localDateFromFields(Number(y), month0 + 1, 0).getDate();
|
|
1498
|
+
const wall = {
|
|
1499
|
+
year: Number(y),
|
|
1500
|
+
month: month0 + 1,
|
|
1501
|
+
day: Math.min(day, lastDay),
|
|
1502
|
+
hours,
|
|
1503
|
+
minutes,
|
|
1504
|
+
seconds,
|
|
1505
|
+
ms: Number(ms ?? 0),
|
|
1506
|
+
};
|
|
1507
|
+
const zone = rawZone.trim();
|
|
1508
|
+
const timezone = isValidTimezone(zone) ? zone : 'UTC';
|
|
1509
|
+
return { wall, timezone };
|
|
1510
|
+
}
|
|
707
1511
|
/**
|
|
708
1512
|
* Function to handle relative date selection
|
|
709
1513
|
*/
|
|
710
1514
|
handleDurationChange(event) {
|
|
711
1515
|
event.stopPropagation();
|
|
1516
|
+
if (this.variant === 'date-time-picker') {
|
|
1517
|
+
const input = event.target;
|
|
1518
|
+
const cleaned = String(event.detail?.value ?? input?.value ?? '').replace(/\D/g, '');
|
|
1519
|
+
if (input && input.value !== cleaned)
|
|
1520
|
+
input.value = cleaned;
|
|
1521
|
+
const duration = Number(cleaned);
|
|
1522
|
+
if (!duration) {
|
|
1523
|
+
this.selectedValue = undefined;
|
|
1524
|
+
this.startDate = null;
|
|
1525
|
+
this.endDate = null;
|
|
1526
|
+
this.requestUpdate();
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
this.selectedValue = duration;
|
|
1530
|
+
if (this.selectedUnit && this.selectedValue) {
|
|
1531
|
+
this.handleTimeValueClick(this.selectedUnit, this.selectedValue, event);
|
|
1532
|
+
}
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
712
1535
|
event.detail.value = event.detail.value.replace(/e/gi, "");
|
|
713
1536
|
const duration = Number(event.detail.value);
|
|
714
1537
|
if (!duration) {
|
|
@@ -735,12 +1558,22 @@ ${this.showMonthDropdown || this.showYearDropdown ? html `
|
|
|
735
1558
|
createRelativePeriod(unit, value) {
|
|
736
1559
|
const endTime = new Date();
|
|
737
1560
|
const startTime = new Date();
|
|
1561
|
+
// only the date-time-picker variant uses exact elapsed time, month-end clamping and the too-large-duration guard; the
|
|
1562
|
+
// default calendar keeps the published wall-clock arithmetic (and its behaviour for an overflowing duration)
|
|
1563
|
+
const dtp = this.variant === 'date-time-picker';
|
|
738
1564
|
switch (unit) {
|
|
739
1565
|
case 'minutes':
|
|
740
|
-
|
|
1566
|
+
// elapsed duration: wall-clock arithmetic (setMinutes) is off by an hour across a host DST change
|
|
1567
|
+
if (dtp)
|
|
1568
|
+
startTime.setTime(endTime.getTime() - value * 60000);
|
|
1569
|
+
else
|
|
1570
|
+
startTime.setMinutes(startTime.getMinutes() - value);
|
|
741
1571
|
break;
|
|
742
1572
|
case 'hours':
|
|
743
|
-
|
|
1573
|
+
if (dtp)
|
|
1574
|
+
startTime.setTime(endTime.getTime() - value * 3600000);
|
|
1575
|
+
else
|
|
1576
|
+
startTime.setHours(startTime.getHours() - value);
|
|
744
1577
|
break;
|
|
745
1578
|
case 'days':
|
|
746
1579
|
startTime.setDate(startTime.getDate() - value);
|
|
@@ -749,9 +1582,25 @@ ${this.showMonthDropdown || this.showYearDropdown ? html `
|
|
|
749
1582
|
startTime.setDate(startTime.getDate() - 7 * value); // Subtract weeks as days
|
|
750
1583
|
break;
|
|
751
1584
|
case 'months':
|
|
752
|
-
|
|
1585
|
+
if (dtp) {
|
|
1586
|
+
// clamp to the target month's last day: setMonth alone rolls 31 Mar - 1 month into 3 Mar
|
|
1587
|
+
const day = startTime.getDate();
|
|
1588
|
+
startTime.setDate(1);
|
|
1589
|
+
startTime.setMonth(startTime.getMonth() - value);
|
|
1590
|
+
const lastDay = localDateFromFields(startTime.getFullYear(), startTime.getMonth() + 1, 0).getDate();
|
|
1591
|
+
startTime.setDate(Math.min(day, lastDay));
|
|
1592
|
+
}
|
|
1593
|
+
else {
|
|
1594
|
+
startTime.setMonth(startTime.getMonth() - value);
|
|
1595
|
+
}
|
|
753
1596
|
break;
|
|
754
1597
|
}
|
|
1598
|
+
// a duration too large for a Date leaves an Invalid Date; storing it would keep Apply enabled and make consumers throw (RangeError) when they format it, so in the variant it is no selection
|
|
1599
|
+
if (dtp && (isNaN(startTime.getTime()) || isNaN(endTime.getTime()))) {
|
|
1600
|
+
this.startDate = null;
|
|
1601
|
+
this.endDate = null;
|
|
1602
|
+
return { startDate: null, endDate: null };
|
|
1603
|
+
}
|
|
755
1604
|
this.startDate = new Date(startTime.getTime());
|
|
756
1605
|
this.endDate = new Date(endTime.getTime());
|
|
757
1606
|
return {
|
|
@@ -823,7 +1672,7 @@ ${this.showMonthDropdown || this.showYearDropdown ? html `
|
|
|
823
1672
|
return;
|
|
824
1673
|
if (this.currentMonth === 11) {
|
|
825
1674
|
const newYear = this.currentYear + 1;
|
|
826
|
-
if (this.
|
|
1675
|
+
if (this.maxYear !== undefined && newYear > this.maxYear)
|
|
827
1676
|
return;
|
|
828
1677
|
this.currentMonth = 0;
|
|
829
1678
|
this.currentYear = newYear;
|
|
@@ -837,7 +1686,7 @@ ${this.showMonthDropdown || this.showYearDropdown ? html `
|
|
|
837
1686
|
return;
|
|
838
1687
|
if (this.currentMonth === 0) {
|
|
839
1688
|
const newYear = this.currentYear - 1;
|
|
840
|
-
if (this.
|
|
1689
|
+
if (this.minYear !== undefined && newYear < this.minYear)
|
|
841
1690
|
return;
|
|
842
1691
|
this.currentMonth = 11;
|
|
843
1692
|
this.currentYear = newYear;
|
|
@@ -858,9 +1707,15 @@ ${this.showMonthDropdown || this.showYearDropdown ? html `
|
|
|
858
1707
|
if (this.disabled)
|
|
859
1708
|
return;
|
|
860
1709
|
this.type = event.detail.value;
|
|
1710
|
+
if (this.variant === 'date-time-picker' && this.type !== 'absolute')
|
|
1711
|
+
this.expanded = false;
|
|
861
1712
|
this.emit('nile-type-change', { value: this.type });
|
|
862
1713
|
}
|
|
863
1714
|
getUTCDate(dateStr) {
|
|
1715
|
+
// Date.UTC remaps years 0-99 to 1900-1999 as published; only the date-time-picker variant keeps the literal year
|
|
1716
|
+
if (this.variant === 'date-time-picker') {
|
|
1717
|
+
return new Date(utcFromFields(Number(dateStr.slice(0, 4)), Number(dateStr.slice(5, 7)) - 1, Number(dateStr.slice(8, 10))));
|
|
1718
|
+
}
|
|
864
1719
|
return new Date(Date.UTC(dateStr.slice(0, 4), dateStr.slice(5, 7) - 1, dateStr.slice(8, 10)));
|
|
865
1720
|
}
|
|
866
1721
|
emitChangedData(data) {
|
|
@@ -871,6 +1726,809 @@ ${this.showMonthDropdown || this.showYearDropdown ? html `
|
|
|
871
1726
|
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
872
1727
|
return Array.from({ length: daysInMonth }, (_, i) => i + 1);
|
|
873
1728
|
}
|
|
1729
|
+
renderDateTimePicker() {
|
|
1730
|
+
// the collapsed height is held while the month grid is expanded (the measured value reaches the stylesheet as a custom property)
|
|
1731
|
+
const heightLocked = this.type === 'absolute' && this.expanded && !!this.tpLockedHeight;
|
|
1732
|
+
return html `
|
|
1733
|
+
<div
|
|
1734
|
+
class=${classMap({
|
|
1735
|
+
base: true,
|
|
1736
|
+
'base--date-time-picker': true,
|
|
1737
|
+
'base--tp-expanded': this.expanded,
|
|
1738
|
+
'base--tp-fewfields': this.tpFewFieldsNoTz,
|
|
1739
|
+
'base--tp-height-locked': heightLocked,
|
|
1740
|
+
})}
|
|
1741
|
+
style=${heightLocked ? `--nile-calendar-tp-locked-height:${this.tpLockedHeight}` : nothing}
|
|
1742
|
+
part="calendar-container"
|
|
1743
|
+
>
|
|
1744
|
+
<div class=${classMap({
|
|
1745
|
+
'calendar-config': true,
|
|
1746
|
+
hidden: !this.showTypes,
|
|
1747
|
+
})} part="calendar-config">
|
|
1748
|
+
<div class="calendar-switcher" part="calendar-switcher">
|
|
1749
|
+
<nile-tab-group centered @nile-tab-show="${this.onTypeChange}" value="${this.type}">
|
|
1750
|
+
<nile-tab slot="nav" panel="absolute" tabindex="${this.disabled ? '-1' : '0'}">Absolute</nile-tab>
|
|
1751
|
+
<nile-tab slot="nav" panel="relative" tabindex="${this.disabled ? '-1' : '0'}">Relative</nile-tab>
|
|
1752
|
+
</nile-tab-group>
|
|
1753
|
+
</div>
|
|
1754
|
+
</div>
|
|
1755
|
+
|
|
1756
|
+
${this.type === 'relative' ? this.renderRelativeCalendar() : this.renderDateTimePickerAbsolute()}
|
|
1757
|
+
|
|
1758
|
+
<div
|
|
1759
|
+
class=${classMap({
|
|
1760
|
+
'tp-footer': true,
|
|
1761
|
+
'tp-footer--relative': this.type === 'relative',
|
|
1762
|
+
})}
|
|
1763
|
+
part="tp-footer"
|
|
1764
|
+
>
|
|
1765
|
+
${this.type === 'absolute' ? html `
|
|
1766
|
+
<nile-button
|
|
1767
|
+
id="tp-set-to-now"
|
|
1768
|
+
class="tp-set-to-now-button"
|
|
1769
|
+
part="tp-set-to-now-button"
|
|
1770
|
+
variant="secondary"
|
|
1771
|
+
?disabled="${this.disabled}"
|
|
1772
|
+
@click="${this.handleSetToNow}"
|
|
1773
|
+
@keydown="${(e) => { if (e.key === 'Enter' || e.key === ' ')
|
|
1774
|
+
this.handleSetToNow(); }}"
|
|
1775
|
+
>Set to now <nile-glyph name="ng-clock" size="16" method="stroke" color=${this.disabled ? 'var(--nile-colors-dark-500, var(--ng-colors-fg-disabled))' : nothing}></nile-glyph></nile-button>
|
|
1776
|
+
<nile-lite-tooltip
|
|
1777
|
+
for="tp-set-to-now"
|
|
1778
|
+
content="Sets the time to the current time"
|
|
1779
|
+
size="small"
|
|
1780
|
+
></nile-lite-tooltip>
|
|
1781
|
+
` : nothing}
|
|
1782
|
+
<div class="tp-footer-actions" part="tp-footer-actions">
|
|
1783
|
+
<nile-button
|
|
1784
|
+
class="clear-button"
|
|
1785
|
+
part="clear-button"
|
|
1786
|
+
variant="secondary"
|
|
1787
|
+
?disabled="${this.disabled || !this.hasSelection()}"
|
|
1788
|
+
@click="${this.handleReset}"
|
|
1789
|
+
@keydown="${(e) => { if (e.key === 'Enter' || e.key === ' ')
|
|
1790
|
+
this.handleReset(); }}"
|
|
1791
|
+
>Reset</nile-button>
|
|
1792
|
+
<nile-button
|
|
1793
|
+
class="apply-button"
|
|
1794
|
+
part="apply-button"
|
|
1795
|
+
?disabled="${this.disabled || !this.hasSelection()}"
|
|
1796
|
+
@click="${this.handleApply}"
|
|
1797
|
+
@keydown="${(e) => { if (e.key === 'Enter' || e.key === ' ')
|
|
1798
|
+
this.handleApply(); }}"
|
|
1799
|
+
>Apply</nile-button>
|
|
1800
|
+
</div>
|
|
1801
|
+
</div>
|
|
1802
|
+
</div>
|
|
1803
|
+
`;
|
|
1804
|
+
}
|
|
1805
|
+
renderDateTimePickerAbsolute() {
|
|
1806
|
+
return html `
|
|
1807
|
+
<div class="calendar-wrapper" part="calendar-wrapper">
|
|
1808
|
+
<div class="calendar-container">
|
|
1809
|
+
${this.expanded
|
|
1810
|
+
? this.renderExpandedCalendar()
|
|
1811
|
+
: this.renderCollapsedMonth(this.currentYear, this.currentMonth)}
|
|
1812
|
+
</div>
|
|
1813
|
+
</div>
|
|
1814
|
+
|
|
1815
|
+
<div
|
|
1816
|
+
class=${classMap({
|
|
1817
|
+
'tp-chevron': true,
|
|
1818
|
+
'tp-chevron--expanded': this.expanded,
|
|
1819
|
+
})}
|
|
1820
|
+
part="tp-chevron"
|
|
1821
|
+
role="button"
|
|
1822
|
+
tabindex="${this.disabled ? '-1' : '0'}"
|
|
1823
|
+
aria-expanded="${this.expanded}"
|
|
1824
|
+
aria-label="${this.expanded ? 'Collapse calendar' : 'Expand calendar'}"
|
|
1825
|
+
@click="${this.toggleExpanded}"
|
|
1826
|
+
@keydown="${(e) => { if (e.key === 'Enter' || e.key === ' ')
|
|
1827
|
+
this.toggleExpanded(); }}"
|
|
1828
|
+
>
|
|
1829
|
+
<nile-icon name="arrowdropdown" color=${this.disabled ? 'var(--nile-colors-dark-500, var(--ng-colors-fg-disabled))' : 'var(--nile-colors-dark-900, var(--ng-colors-text-primary-900))'}></nile-icon>
|
|
1830
|
+
</div>
|
|
1831
|
+
|
|
1832
|
+
<div class="tp-scroll-area" part="tp-scroll-area">
|
|
1833
|
+
<div class="tp-scroll-content" part="tp-scroll-content">
|
|
1834
|
+
${this.renderTimeFields()}
|
|
1835
|
+
${this.resolvedTimeFields.timezone ? this.renderTimezone() : ''}
|
|
1836
|
+
</div>
|
|
1837
|
+
</div>
|
|
1838
|
+
`;
|
|
1839
|
+
}
|
|
1840
|
+
renderExpandedCalendar() {
|
|
1841
|
+
return this.renderVariantMonth(this.currentYear, this.currentMonth, this.getFullMonthDays(this.currentYear, this.currentMonth));
|
|
1842
|
+
}
|
|
1843
|
+
renderCollapsedMonth(year, month) {
|
|
1844
|
+
return this.renderVariantMonth(year, month, this.getCollapsedWindowDays(year, month));
|
|
1845
|
+
}
|
|
1846
|
+
renderVariantMonth(year, month, cells) {
|
|
1847
|
+
const isSelectedDate = (day, month, year, isCurrentMonth) => {
|
|
1848
|
+
if (!isCurrentMonth)
|
|
1849
|
+
return '';
|
|
1850
|
+
if (this.value) {
|
|
1851
|
+
const isSelected = day === this.value.getDate() &&
|
|
1852
|
+
month === this.value.getMonth() &&
|
|
1853
|
+
year === this.value.getFullYear();
|
|
1854
|
+
if (isSelected)
|
|
1855
|
+
return 'selected-date';
|
|
1856
|
+
}
|
|
1857
|
+
return '';
|
|
1858
|
+
};
|
|
1859
|
+
const isCurrentDate = (day, month, year) => {
|
|
1860
|
+
const today = new Date();
|
|
1861
|
+
return (day === today.getDate() &&
|
|
1862
|
+
month + 1 === today.getMonth() + 1 &&
|
|
1863
|
+
year === today.getFullYear());
|
|
1864
|
+
};
|
|
1865
|
+
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
1866
|
+
// roving tabindex: one reachable day (the keyboard's last day, else the selection, else today, else the first allowed day)
|
|
1867
|
+
const key = (c) => `${c.year}-${c.month}-${c.day}`;
|
|
1868
|
+
const reachable = cells.filter(c => c.inMonth && this.isAllowedDate(c.day, c.month, c.year));
|
|
1869
|
+
const wanted = [
|
|
1870
|
+
this.tpFocusDate && this.tpFocusDate,
|
|
1871
|
+
this.value && { year: this.value.getFullYear(), month: this.value.getMonth(), day: this.value.getDate() },
|
|
1872
|
+
(() => { const t = new Date(); return { year: t.getFullYear(), month: t.getMonth(), day: t.getDate() }; })(),
|
|
1873
|
+
];
|
|
1874
|
+
let tabStop = null;
|
|
1875
|
+
for (const w of wanted) {
|
|
1876
|
+
if (w && reachable.some(c => key(c) === key(w))) {
|
|
1877
|
+
tabStop = key(w);
|
|
1878
|
+
break;
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
if (tabStop === null && reachable.length)
|
|
1882
|
+
tabStop = key(reachable[0]);
|
|
1883
|
+
return html `
|
|
1884
|
+
<div class="calendar" part="calendar">
|
|
1885
|
+
<div class="calendar-header" part="calendar-header">
|
|
1886
|
+
<nile-icon
|
|
1887
|
+
class="calendar-header__month-navigation"
|
|
1888
|
+
part="calendar-header-prev"
|
|
1889
|
+
name="var(--nile-icon-arrow-left, var(--ng-icon-chevron-left))"
|
|
1890
|
+
method="var(--nile-svg-method-fill, var(--ng-svg-method-stroke))"
|
|
1891
|
+
color=${this.disabled ? 'var(--nile-colors-dark-500, var(--ng-colors-fg-disabled))' : 'var(--nile-colors-dark-900, var(--ng-colors-text-primary-900))'}
|
|
1892
|
+
role="button"
|
|
1893
|
+
aria-label="Previous month"
|
|
1894
|
+
aria-disabled="${this.disabled || this.isPrevDisabled()}"
|
|
1895
|
+
tabindex="${this.disabled || this.isPrevDisabled() ? '-1' : '0'}"
|
|
1896
|
+
@keydown="${(e) => { if (e.key === 'Enter' || e.key === ' ')
|
|
1897
|
+
this.prevMonth(); }}"
|
|
1898
|
+
@click="${this.prevMonth}"
|
|
1899
|
+
>
|
|
1900
|
+
</nile-icon>
|
|
1901
|
+
<div class="calendar-header__center" part="calendar-header-center">
|
|
1902
|
+
|
|
1903
|
+
${this.showMonthDropdown || this.showYearDropdown ? html `
|
|
1904
|
+
${this.showMonthDropdown ? html `
|
|
1905
|
+
<nile-dropdown ?disabled="${this.disabled}" class="month-dropdown"
|
|
1906
|
+
@nile-after-show="${() => this.scrollSelectedIntoView('.month-menu-item[selected]')}">
|
|
1907
|
+
<span
|
|
1908
|
+
slot="trigger"
|
|
1909
|
+
class="calendar-header__trigger month-trigger"
|
|
1910
|
+
role="button"
|
|
1911
|
+
part="trigger"
|
|
1912
|
+
tabindex="${this.disabled ? '-1' : '0'}"
|
|
1913
|
+
@keydown="${(e) => {
|
|
1914
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
1915
|
+
e.preventDefault();
|
|
1916
|
+
e.currentTarget.click();
|
|
1917
|
+
}
|
|
1918
|
+
}}"
|
|
1919
|
+
>
|
|
1920
|
+
${new Date(year, month).toLocaleString('default', { month: 'long' })}
|
|
1921
|
+
<nile-icon name="arrowdropdown" color=${this.disabled ? 'var(--nile-colors-dark-500, var(--ng-colors-fg-disabled))' : 'var(--nile-colors-dark-900, var(--ng-colors-text-primary-900))'} class="calendar-header__caret"></nile-icon>
|
|
1922
|
+
</span>
|
|
1923
|
+
<nile-menu>
|
|
1924
|
+
<div class="month-scroll-wrapper">
|
|
1925
|
+
${this.monthNames.map((m, idx) => html `
|
|
1926
|
+
<nile-menu-item
|
|
1927
|
+
class="month-menu-item"
|
|
1928
|
+
?selected="${idx === month}"
|
|
1929
|
+
@click="${() => this.onMonthSelected(idx)}"
|
|
1930
|
+
>
|
|
1931
|
+
${m}
|
|
1932
|
+
</nile-menu-item>
|
|
1933
|
+
`)}
|
|
1934
|
+
</div>
|
|
1935
|
+
</nile-menu>
|
|
1936
|
+
</nile-dropdown>
|
|
1937
|
+
` : html `
|
|
1938
|
+
<span
|
|
1939
|
+
>${new Date(year, month).toLocaleString('default', { month: 'long' })}</span
|
|
1940
|
+
>
|
|
1941
|
+
`}
|
|
1942
|
+
${this.showYearDropdown ? html `
|
|
1943
|
+
<nile-dropdown ?disabled="${this.disabled}" class="year-dropdown"
|
|
1944
|
+
@nile-after-show="${() => this.scrollSelectedIntoView('.year-menu-item[selected]')}">
|
|
1945
|
+
<span
|
|
1946
|
+
slot="trigger"
|
|
1947
|
+
class="calendar-header__trigger"
|
|
1948
|
+
role="button"
|
|
1949
|
+
part="trigger"
|
|
1950
|
+
tabindex="${this.disabled ? '-1' : '0'}"
|
|
1951
|
+
@keydown="${(e) => {
|
|
1952
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
1953
|
+
e.preventDefault();
|
|
1954
|
+
e.currentTarget.click();
|
|
1955
|
+
}
|
|
1956
|
+
}}"
|
|
1957
|
+
>
|
|
1958
|
+
${year}
|
|
1959
|
+
<nile-icon name="arrowdropdown" color=${this.disabled ? 'var(--nile-colors-dark-500, var(--ng-colors-fg-disabled))' : 'var(--nile-colors-dark-900, var(--ng-colors-text-primary-900))'} class="calendar-header__caret"></nile-icon>
|
|
1960
|
+
</span>
|
|
1961
|
+
<nile-menu>
|
|
1962
|
+
<div class="year-scroll-wrapper">
|
|
1963
|
+
${this.yearOptions.map(y => html `
|
|
1964
|
+
<nile-menu-item
|
|
1965
|
+
class="year-menu-item"
|
|
1966
|
+
?selected="${y === year}"
|
|
1967
|
+
@click="${() => this.onYearSelected(y)}"
|
|
1968
|
+
>
|
|
1969
|
+
${y}
|
|
1970
|
+
</nile-menu-item>
|
|
1971
|
+
`)}
|
|
1972
|
+
</div>
|
|
1973
|
+
</nile-menu>
|
|
1974
|
+
</nile-dropdown>
|
|
1975
|
+
` : html `
|
|
1976
|
+
<span>${year}</span>
|
|
1977
|
+
`}
|
|
1978
|
+
` : html `
|
|
1979
|
+
<span
|
|
1980
|
+
>${new Date(year, month).toLocaleString('default', {
|
|
1981
|
+
month: 'long',
|
|
1982
|
+
})}
|
|
1983
|
+
${year}</span
|
|
1984
|
+
>
|
|
1985
|
+
`}
|
|
1986
|
+
</div>
|
|
1987
|
+
|
|
1988
|
+
<nile-icon
|
|
1989
|
+
class="calendar-header__month-navigation"
|
|
1990
|
+
part="calendar-header-next"
|
|
1991
|
+
name="var(--nile-icon-arrow-right, var(--ng-icon-chevron-right))"
|
|
1992
|
+
method="var(--nile-svg-method-fill, var(--ng-svg-method-stroke))"
|
|
1993
|
+
color=${this.disabled ? 'var(--nile-colors-dark-500, var(--ng-colors-fg-disabled))' : 'var(--nile-colors-dark-900, var(--ng-colors-text-primary-900))'}
|
|
1994
|
+
role="button"
|
|
1995
|
+
aria-label="Next month"
|
|
1996
|
+
aria-disabled="${this.disabled || this.isNextDisabled()}"
|
|
1997
|
+
tabindex="${this.disabled || this.isNextDisabled() ? '-1' : '0'}"
|
|
1998
|
+
@keydown="${(e) => { if (e.key === 'Enter' || e.key === ' ')
|
|
1999
|
+
this.nextMonth(); }}"
|
|
2000
|
+
@click="${this.nextMonth}"
|
|
2001
|
+
>
|
|
2002
|
+
</nile-icon>
|
|
2003
|
+
</div>
|
|
2004
|
+
|
|
2005
|
+
<div class="days_container" part="days-container">
|
|
2006
|
+
${dayNames.map(day => html `<div class="day_name" part="day-name">${day}</div>`)}
|
|
2007
|
+
${cells.map(cell => {
|
|
2008
|
+
const isCurrentMonth = cell.inMonth;
|
|
2009
|
+
const datePlacement = isSelectedDate(cell.day, cell.month, cell.year, isCurrentMonth);
|
|
2010
|
+
const classMapObj = {
|
|
2011
|
+
'day_date': true,
|
|
2012
|
+
'not-allowed': !this.isAllowedDate(cell.day, cell.month, cell.year),
|
|
2013
|
+
'filler': !isCurrentMonth,
|
|
2014
|
+
};
|
|
2015
|
+
if (datePlacement)
|
|
2016
|
+
classMapObj[datePlacement] = true;
|
|
2017
|
+
const cellAllowed = this.isAllowedDate(cell.day, cell.month, cell.year);
|
|
2018
|
+
const cellToday = isCurrentDate(cell.day, cell.month, cell.year) && isCurrentMonth;
|
|
2019
|
+
return html `
|
|
2020
|
+
<div
|
|
2021
|
+
part="calendar-day-${cell.day}"
|
|
2022
|
+
class=${classMap(classMapObj)}
|
|
2023
|
+
role=${isCurrentMonth ? 'button' : nothing}
|
|
2024
|
+
aria-hidden=${isCurrentMonth ? nothing : 'true'}
|
|
2025
|
+
aria-label=${isCurrentMonth ? this.dayCellLabel(cell.day, cell.month, cell.year) : nothing}
|
|
2026
|
+
aria-disabled=${isCurrentMonth && (!cellAllowed || this.disabled) ? 'true' : nothing}
|
|
2027
|
+
aria-pressed=${isCurrentMonth ? String(!!datePlacement) : nothing}
|
|
2028
|
+
aria-current=${cellToday ? 'date' : nothing}
|
|
2029
|
+
data-tp-date="${isCurrentMonth ? key(cell) : nothing}"
|
|
2030
|
+
tabindex="${isCurrentMonth && !this.disabled && key(cell) === tabStop ? '0' : '-1'}"
|
|
2031
|
+
@keydown="${(e) => {
|
|
2032
|
+
if (!isCurrentMonth)
|
|
2033
|
+
return;
|
|
2034
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
2035
|
+
e.preventDefault(); // Space on a role=button div would otherwise scroll the page
|
|
2036
|
+
this.tpFocusDate = { year: cell.year, month: cell.month, day: cell.day };
|
|
2037
|
+
this.tpSelectDate(cell.day, cell.month, cell.year);
|
|
2038
|
+
return;
|
|
2039
|
+
}
|
|
2040
|
+
this.handleDayNavigationKey(e, cell);
|
|
2041
|
+
}}"
|
|
2042
|
+
@click="${() => { if (isCurrentMonth) {
|
|
2043
|
+
this.tpFocusDate = { year: cell.year, month: cell.month, day: cell.day };
|
|
2044
|
+
this.tpSelectDate(cell.day, cell.month, cell.year);
|
|
2045
|
+
} }}"
|
|
2046
|
+
>
|
|
2047
|
+
<span class="day-label" part="calendar-day-label">
|
|
2048
|
+
${cell.day}
|
|
2049
|
+
${isCurrentDate(cell.day, cell.month, cell.year) && isCurrentMonth ? html `<div class="current__date__dot" part="current-date-dot"></div>` : nothing}
|
|
2050
|
+
</span>
|
|
2051
|
+
</div>`;
|
|
2052
|
+
})}
|
|
2053
|
+
</div>
|
|
2054
|
+
</div>
|
|
2055
|
+
`;
|
|
2056
|
+
}
|
|
2057
|
+
getCollapsedWindowDays(year, month) {
|
|
2058
|
+
const today = new Date();
|
|
2059
|
+
const isCurrentMonth = today.getFullYear() === year && today.getMonth() === month;
|
|
2060
|
+
const selectionInMonth = this.value instanceof Date &&
|
|
2061
|
+
this.value.getFullYear() === year &&
|
|
2062
|
+
this.value.getMonth() === month;
|
|
2063
|
+
const centerOn = (dayOfMonth) => {
|
|
2064
|
+
const anchor = localDateFromFields(year, month, dayOfMonth);
|
|
2065
|
+
const s = new Date(anchor);
|
|
2066
|
+
s.setDate(anchor.getDate() - anchor.getDay() - 7);
|
|
2067
|
+
return s;
|
|
2068
|
+
};
|
|
2069
|
+
const start = selectionInMonth
|
|
2070
|
+
? centerOn(this.value.getDate())
|
|
2071
|
+
: isCurrentMonth
|
|
2072
|
+
? centerOn(today.getDate())
|
|
2073
|
+
: (() => {
|
|
2074
|
+
const first = localDateFromFields(year, month, 1);
|
|
2075
|
+
const s = new Date(first);
|
|
2076
|
+
s.setDate(1 - first.getDay());
|
|
2077
|
+
return s;
|
|
2078
|
+
})();
|
|
2079
|
+
return Array.from({ length: 21 }, (_, i) => {
|
|
2080
|
+
const d = new Date(start);
|
|
2081
|
+
d.setDate(start.getDate() + i);
|
|
2082
|
+
return {
|
|
2083
|
+
day: d.getDate(),
|
|
2084
|
+
month: d.getMonth(),
|
|
2085
|
+
year: d.getFullYear(),
|
|
2086
|
+
inMonth: d.getMonth() === month && d.getFullYear() === year,
|
|
2087
|
+
};
|
|
2088
|
+
});
|
|
2089
|
+
}
|
|
2090
|
+
getFullMonthDays(year, month) {
|
|
2091
|
+
const first = localDateFromFields(year, month, 1);
|
|
2092
|
+
const leading = first.getDay();
|
|
2093
|
+
const daysInMonth = localDateFromFields(year, month + 1, 0).getDate();
|
|
2094
|
+
const totalCells = Math.ceil((leading + daysInMonth) / 7) * 7;
|
|
2095
|
+
const start = localDateFromFields(year, month, 1 - leading);
|
|
2096
|
+
return Array.from({ length: totalCells }, (_, i) => {
|
|
2097
|
+
const d = new Date(start);
|
|
2098
|
+
d.setDate(start.getDate() + i);
|
|
2099
|
+
return {
|
|
2100
|
+
day: d.getDate(),
|
|
2101
|
+
month: d.getMonth(),
|
|
2102
|
+
year: d.getFullYear(),
|
|
2103
|
+
inMonth: d.getMonth() === month && d.getFullYear() === year,
|
|
2104
|
+
};
|
|
2105
|
+
});
|
|
2106
|
+
}
|
|
2107
|
+
/**
|
|
2108
|
+
* Arrow keys move by a day / week, Home and End to the week's first and last day, PageUp and PageDown by a month
|
|
2109
|
+
* (Shift: a year). The target is clamped to the month's length and to the year range; a day that the collapsed
|
|
2110
|
+
* three-week window does not show expands the calendar to reveal it.
|
|
2111
|
+
*/
|
|
2112
|
+
handleDayNavigationKey(e, cell) {
|
|
2113
|
+
if (this.disabled || e.ctrlKey || e.metaKey || e.altKey)
|
|
2114
|
+
return;
|
|
2115
|
+
const from = localDateFromFields(cell.year, cell.month, cell.day);
|
|
2116
|
+
let target = null;
|
|
2117
|
+
switch (e.key) {
|
|
2118
|
+
case 'ArrowLeft':
|
|
2119
|
+
target = localDateFromFields(cell.year, cell.month, cell.day - 1);
|
|
2120
|
+
break;
|
|
2121
|
+
case 'ArrowRight':
|
|
2122
|
+
target = localDateFromFields(cell.year, cell.month, cell.day + 1);
|
|
2123
|
+
break;
|
|
2124
|
+
case 'ArrowUp':
|
|
2125
|
+
target = localDateFromFields(cell.year, cell.month, cell.day - 7);
|
|
2126
|
+
break;
|
|
2127
|
+
case 'ArrowDown':
|
|
2128
|
+
target = localDateFromFields(cell.year, cell.month, cell.day + 7);
|
|
2129
|
+
break;
|
|
2130
|
+
case 'Home':
|
|
2131
|
+
target = localDateFromFields(cell.year, cell.month, cell.day - from.getDay());
|
|
2132
|
+
break;
|
|
2133
|
+
case 'End':
|
|
2134
|
+
target = localDateFromFields(cell.year, cell.month, cell.day + (6 - from.getDay()));
|
|
2135
|
+
break;
|
|
2136
|
+
case 'PageUp':
|
|
2137
|
+
case 'PageDown': {
|
|
2138
|
+
const step = (e.key === 'PageUp' ? -1 : 1) * (e.shiftKey ? 12 : 1);
|
|
2139
|
+
const month = cell.month + step;
|
|
2140
|
+
const last = localDateFromFields(cell.year, month + 1, 0).getDate();
|
|
2141
|
+
target = localDateFromFields(cell.year, month, Math.min(cell.day, last));
|
|
2142
|
+
break;
|
|
2143
|
+
}
|
|
2144
|
+
default: return;
|
|
2145
|
+
}
|
|
2146
|
+
e.preventDefault();
|
|
2147
|
+
this.moveDayFocus(target);
|
|
2148
|
+
}
|
|
2149
|
+
moveDayFocus(target) {
|
|
2150
|
+
const year = target.getFullYear();
|
|
2151
|
+
const month = target.getMonth();
|
|
2152
|
+
const lo = this.minYear;
|
|
2153
|
+
const hi = this.maxYear;
|
|
2154
|
+
if ((lo !== undefined && year < lo) || (hi !== undefined && year > hi))
|
|
2155
|
+
return;
|
|
2156
|
+
this.tpFocusDate = { year, month, day: target.getDate() };
|
|
2157
|
+
this.currentYear = year;
|
|
2158
|
+
this.currentMonth = month;
|
|
2159
|
+
const sel = `[data-tp-date="${year}-${month}-${target.getDate()}"]`;
|
|
2160
|
+
const focusCell = () => this.shadowRoot?.querySelector(sel)?.focus();
|
|
2161
|
+
this.updateComplete.then(() => {
|
|
2162
|
+
if (this.shadowRoot?.querySelector(sel)) {
|
|
2163
|
+
focusCell();
|
|
2164
|
+
return;
|
|
2165
|
+
}
|
|
2166
|
+
// not in the collapsed window: reveal the whole month
|
|
2167
|
+
this.expanded = true;
|
|
2168
|
+
this.updateComplete.then(focusCell).catch(() => { });
|
|
2169
|
+
}).catch(() => { });
|
|
2170
|
+
}
|
|
2171
|
+
tpSelectDate(day, month, year) {
|
|
2172
|
+
if (this.disabled)
|
|
2173
|
+
return;
|
|
2174
|
+
if (!this.isAllowedDate(day, month, year))
|
|
2175
|
+
return;
|
|
2176
|
+
const selectedDate = localDateFromFields(year, month, day);
|
|
2177
|
+
// compare the calendar day only: seeded values are stored at noon, a clicked day at midnight
|
|
2178
|
+
const sameDay = !!this.value &&
|
|
2179
|
+
this.value.getFullYear() === year &&
|
|
2180
|
+
this.value.getMonth() === month &&
|
|
2181
|
+
this.value.getDate() === day;
|
|
2182
|
+
if (sameDay && this.doubleClickUnselect) {
|
|
2183
|
+
this.handleReset();
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
if (!sameDay) {
|
|
2187
|
+
this.resetTimeUnits();
|
|
2188
|
+
}
|
|
2189
|
+
this.value = selectedDate;
|
|
2190
|
+
}
|
|
2191
|
+
resetTimeUnits() {
|
|
2192
|
+
this.tpHours = null;
|
|
2193
|
+
this.tpMinutes = null;
|
|
2194
|
+
this.tpSeconds = null;
|
|
2195
|
+
this.tpMilliseconds = null;
|
|
2196
|
+
this.tpEditingText = '';
|
|
2197
|
+
}
|
|
2198
|
+
renderTimeFields() {
|
|
2199
|
+
const fields = this.resolvedTimeFields;
|
|
2200
|
+
return html `
|
|
2201
|
+
<div class=${classMap({
|
|
2202
|
+
'tp-time-fields': true,
|
|
2203
|
+
'tp-time-fields--stacked': this.tpFewFieldsNoTz,
|
|
2204
|
+
})} part="tp-time-fields">
|
|
2205
|
+
${fields.hours ? this.renderTimeField('Hours (24H)', 'HH', 23, this.tpHours, v => { this.tpHours = v; }) : ''}
|
|
2206
|
+
${fields.minutes ? this.renderTimeField('Minutes', 'MM', 59, this.tpMinutes, v => { this.tpMinutes = v; }) : ''}
|
|
2207
|
+
${fields.seconds ? this.renderTimeField('Seconds', 'SS', 59, this.tpSeconds, v => { this.tpSeconds = v; }) : ''}
|
|
2208
|
+
${fields.milliseconds ? this.renderTimeField('Milliseconds', 'MS', 999, this.tpMilliseconds, v => { this.tpMilliseconds = v; }) : ''}
|
|
2209
|
+
</div>
|
|
2210
|
+
`;
|
|
2211
|
+
}
|
|
2212
|
+
renderTimeField(label, placeholder, max, value, setter) {
|
|
2213
|
+
const maxDigits = String(max).length;
|
|
2214
|
+
const normalize = (raw) => {
|
|
2215
|
+
let digits = (raw || '').replace(/\D/g, '');
|
|
2216
|
+
while (digits.length > maxDigits && digits[0] === '0')
|
|
2217
|
+
digits = digits.slice(1);
|
|
2218
|
+
if (digits.length > maxDigits)
|
|
2219
|
+
digits = digits.slice(0, maxDigits);
|
|
2220
|
+
if (digits === '')
|
|
2221
|
+
return { text: '', value: null };
|
|
2222
|
+
const num = Number(digits);
|
|
2223
|
+
if (num > max) {
|
|
2224
|
+
const first = Number(digits[0]);
|
|
2225
|
+
return { text: String(first).padStart(maxDigits, '0'), value: first };
|
|
2226
|
+
}
|
|
2227
|
+
return { text: digits, value: num };
|
|
2228
|
+
};
|
|
2229
|
+
const apply = (e) => {
|
|
2230
|
+
e.stopPropagation();
|
|
2231
|
+
const input = e.target;
|
|
2232
|
+
const { text, value: next } = normalize(input.value);
|
|
2233
|
+
setter(next);
|
|
2234
|
+
this.tpEditingText = text;
|
|
2235
|
+
if (input.value !== text)
|
|
2236
|
+
input.value = text;
|
|
2237
|
+
};
|
|
2238
|
+
const step = (delta, focusEl) => {
|
|
2239
|
+
if (this.disabled)
|
|
2240
|
+
return;
|
|
2241
|
+
// A stepper click keeps DOM focus on whichever field was focused before, so move it here — the
|
|
2242
|
+
// shared editing text must be attributed to the right input.
|
|
2243
|
+
if (focusEl) {
|
|
2244
|
+
const input = focusEl.closest('nile-input');
|
|
2245
|
+
// `nile-input.focus()` dereferences its inner <input>, which does not exist until the input has rendered
|
|
2246
|
+
// (a calendar in a closed, not-yet-attached panel never renders it). Nothing to focus then; still step.
|
|
2247
|
+
if (input?.input)
|
|
2248
|
+
input.focus();
|
|
2249
|
+
}
|
|
2250
|
+
const span = max + 1;
|
|
2251
|
+
const next = value === null
|
|
2252
|
+
? (delta === 1 ? 0 : max)
|
|
2253
|
+
: ((value + delta) % span + span) % span;
|
|
2254
|
+
setter(next);
|
|
2255
|
+
this.tpFocusedTimeField = label;
|
|
2256
|
+
this.tpEditingText = String(next).padStart(maxDigits, '0');
|
|
2257
|
+
};
|
|
2258
|
+
const focused = this.tpFocusedTimeField === label;
|
|
2259
|
+
const displayValue = focused
|
|
2260
|
+
? this.tpEditingText
|
|
2261
|
+
: value !== null || this.value
|
|
2262
|
+
? String(value ?? 0).padStart(maxDigits, '0')
|
|
2263
|
+
: '';
|
|
2264
|
+
return html `
|
|
2265
|
+
<nile-input
|
|
2266
|
+
class="tp-time-field"
|
|
2267
|
+
part="tp-time-field"
|
|
2268
|
+
type="text"
|
|
2269
|
+
inputmode="numeric"
|
|
2270
|
+
label="${label}"
|
|
2271
|
+
placeholder="${placeholder}"
|
|
2272
|
+
?disabled="${this.disabled}"
|
|
2273
|
+
value="${displayValue}"
|
|
2274
|
+
@nile-change="${apply}"
|
|
2275
|
+
@nile-input="${apply}"
|
|
2276
|
+
@keydown="${(e) => {
|
|
2277
|
+
if (e.key === 'ArrowUp') {
|
|
2278
|
+
e.preventDefault();
|
|
2279
|
+
step(1);
|
|
2280
|
+
}
|
|
2281
|
+
else if (e.key === 'ArrowDown') {
|
|
2282
|
+
e.preventDefault();
|
|
2283
|
+
step(-1);
|
|
2284
|
+
}
|
|
2285
|
+
}}"
|
|
2286
|
+
@nile-focus="${(e) => {
|
|
2287
|
+
e.stopPropagation();
|
|
2288
|
+
this.tpFocusedTimeField = label;
|
|
2289
|
+
this.tpEditingText = '';
|
|
2290
|
+
}}"
|
|
2291
|
+
@nile-blur="${(e) => { e.stopPropagation(); if (this.tpFocusedTimeField === label)
|
|
2292
|
+
this.tpFocusedTimeField = null; }}"
|
|
2293
|
+
>
|
|
2294
|
+
<div
|
|
2295
|
+
slot="suffix"
|
|
2296
|
+
class="tp-stepper"
|
|
2297
|
+
part="tp-stepper"
|
|
2298
|
+
@mousedown="${(e) => e.preventDefault()}"
|
|
2299
|
+
>
|
|
2300
|
+
<div
|
|
2301
|
+
class="tp-stepper-btn tp-stepper-btn--up"
|
|
2302
|
+
part="tp-stepper-btn tp-stepper-btn-up"
|
|
2303
|
+
role="button"
|
|
2304
|
+
tabindex="${this.disabled ? '-1' : '0'}"
|
|
2305
|
+
aria-label="Increase ${label}"
|
|
2306
|
+
@click="${(e) => step(1, e.currentTarget)}"
|
|
2307
|
+
@keydown="${(e) => { if (e.key === 'Enter' || e.key === ' ')
|
|
2308
|
+
step(1, e.currentTarget); }}"
|
|
2309
|
+
>
|
|
2310
|
+
<nile-icon name="arrowdropdown" size="16" color=${this.disabled ? 'var(--nile-colors-dark-500, var(--ng-colors-fg-disabled))' : nothing}></nile-icon>
|
|
2311
|
+
</div>
|
|
2312
|
+
<div
|
|
2313
|
+
class="tp-stepper-btn"
|
|
2314
|
+
part="tp-stepper-btn tp-stepper-btn-down"
|
|
2315
|
+
role="button"
|
|
2316
|
+
tabindex="${this.disabled ? '-1' : '0'}"
|
|
2317
|
+
aria-label="Decrease ${label}"
|
|
2318
|
+
@click="${(e) => step(-1, e.currentTarget)}"
|
|
2319
|
+
@keydown="${(e) => { if (e.key === 'Enter' || e.key === ' ')
|
|
2320
|
+
step(-1, e.currentTarget); }}"
|
|
2321
|
+
>
|
|
2322
|
+
<nile-icon name="arrowdropdown" size="16" color=${this.disabled ? 'var(--nile-colors-dark-500, var(--ng-colors-fg-disabled))' : nothing}></nile-icon>
|
|
2323
|
+
</div>
|
|
2324
|
+
</div>
|
|
2325
|
+
</nile-input>
|
|
2326
|
+
`;
|
|
2327
|
+
}
|
|
2328
|
+
get timezoneOptions() {
|
|
2329
|
+
const zones = typeof Intl.supportedValuesOf === 'function'
|
|
2330
|
+
? Intl.supportedValuesOf('timeZone')
|
|
2331
|
+
: TIMEZONES;
|
|
2332
|
+
return zones.includes('UTC') ? zones : ['UTC', ...zones];
|
|
2333
|
+
}
|
|
2334
|
+
renderTimezone() {
|
|
2335
|
+
return html `
|
|
2336
|
+
<div class="tp-timezone" part="tp-timezone">
|
|
2337
|
+
<div class="tp-timezone-header" part="tp-timezone-header">
|
|
2338
|
+
<span class="tp-timezone-label" part="tp-timezone-label">Timezone</span>
|
|
2339
|
+
<span
|
|
2340
|
+
class="tp-timezone-local-link"
|
|
2341
|
+
part="tp-timezone-local-link"
|
|
2342
|
+
role="button"
|
|
2343
|
+
tabindex="${this.disabled ? '-1' : '0'}"
|
|
2344
|
+
@click="${this.handleUseLocalTimezone}"
|
|
2345
|
+
@keydown="${(e) => { if (e.key === 'Enter' || e.key === ' ') {
|
|
2346
|
+
e.preventDefault();
|
|
2347
|
+
this.handleUseLocalTimezone();
|
|
2348
|
+
} }}"
|
|
2349
|
+
>Use local timezone</span>
|
|
2350
|
+
</div>
|
|
2351
|
+
<nile-select
|
|
2352
|
+
class="tp-timezone-select"
|
|
2353
|
+
part="tp-timezone-select"
|
|
2354
|
+
hoist
|
|
2355
|
+
placement="top"
|
|
2356
|
+
?disabled="${this.disabled}"
|
|
2357
|
+
searchEnabled
|
|
2358
|
+
placeholder="Select timezone"
|
|
2359
|
+
.value="${this.timezone}"
|
|
2360
|
+
@nile-change="${this.handleTimezoneChange}"
|
|
2361
|
+
>
|
|
2362
|
+
${this.timezoneOptions.map(tz => html `<nile-option value="${tz}">${tz}</nile-option>`)}
|
|
2363
|
+
</nile-select>
|
|
2364
|
+
</div>
|
|
2365
|
+
`;
|
|
2366
|
+
}
|
|
2367
|
+
handleTimezoneChange(event) {
|
|
2368
|
+
event.stopPropagation();
|
|
2369
|
+
const selected = (event.detail && (event.detail.value ?? event.detail)) ||
|
|
2370
|
+
event.target?.value;
|
|
2371
|
+
if (typeof selected === 'string' && selected) {
|
|
2372
|
+
this.setTimezoneInternally(selected);
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
handleUseLocalTimezone() {
|
|
2376
|
+
if (this.disabled)
|
|
2377
|
+
return;
|
|
2378
|
+
const local = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
2379
|
+
if (local) {
|
|
2380
|
+
this.setTimezoneInternally(local);
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
toggleExpanded() {
|
|
2384
|
+
if (this.disabled)
|
|
2385
|
+
return;
|
|
2386
|
+
this.expanded = !this.expanded;
|
|
2387
|
+
}
|
|
2388
|
+
composeLocalTimezoneValue() {
|
|
2389
|
+
if (!this.value)
|
|
2390
|
+
return null;
|
|
2391
|
+
if (this.seedInstant !== null &&
|
|
2392
|
+
this.seedFieldsKey === this.instantSeedFieldsKey() &&
|
|
2393
|
+
(this.seedZone === null || this.seedZone === this.timezone)) {
|
|
2394
|
+
return new Date(this.seedInstant);
|
|
2395
|
+
}
|
|
2396
|
+
return this.wallClockToInstant(this.value.getFullYear(), this.value.getMonth(), this.value.getDate(), this.tpHours ?? 0, this.tpMinutes ?? 0, this.tpSeconds ?? 0, this.tpMilliseconds ?? 0);
|
|
2397
|
+
}
|
|
2398
|
+
composeValueISO(epochMs) {
|
|
2399
|
+
const zone = this.timezone || 'UTC';
|
|
2400
|
+
const p = (n, len = 2) => String(n).padStart(len, '0');
|
|
2401
|
+
const ms = ((epochMs % 1000) + 1000) % 1000;
|
|
2402
|
+
let y, mo, d, h, mi, s;
|
|
2403
|
+
let offMs;
|
|
2404
|
+
try {
|
|
2405
|
+
const dtf = new Intl.DateTimeFormat('en-US', {
|
|
2406
|
+
timeZone: zone,
|
|
2407
|
+
hourCycle: 'h23',
|
|
2408
|
+
year: 'numeric', month: '2-digit', day: '2-digit',
|
|
2409
|
+
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
|
2410
|
+
});
|
|
2411
|
+
const map = {};
|
|
2412
|
+
for (const part of dtf.formatToParts(new Date(epochMs))) {
|
|
2413
|
+
if (part.type !== 'literal')
|
|
2414
|
+
map[part.type] = Number(part.value);
|
|
2415
|
+
}
|
|
2416
|
+
y = map.year;
|
|
2417
|
+
mo = map.month;
|
|
2418
|
+
d = map.day;
|
|
2419
|
+
h = map.hour;
|
|
2420
|
+
mi = map.minute;
|
|
2421
|
+
s = map.second;
|
|
2422
|
+
offMs = this.zoneOffsetMs(epochMs - ms);
|
|
2423
|
+
}
|
|
2424
|
+
catch {
|
|
2425
|
+
const local = new Date(epochMs);
|
|
2426
|
+
y = local.getFullYear();
|
|
2427
|
+
mo = local.getMonth() + 1;
|
|
2428
|
+
d = local.getDate();
|
|
2429
|
+
h = local.getHours();
|
|
2430
|
+
mi = local.getMinutes();
|
|
2431
|
+
s = local.getSeconds();
|
|
2432
|
+
offMs = 0;
|
|
2433
|
+
}
|
|
2434
|
+
// the bracket only carries HH:mm; historical sub-minute offsets (LMT) round
|
|
2435
|
+
offMs = Math.round(offMs / 60000) * 60000;
|
|
2436
|
+
const sign = offMs >= 0 ? '+' : '-';
|
|
2437
|
+
const abs = Math.abs(offMs);
|
|
2438
|
+
const off = `${sign}${p(Math.floor(abs / 3600000))}:${p(Math.floor((abs % 3600000) / 60000))}`;
|
|
2439
|
+
return (`${p(y, 4)}-${p(mo)}-${p(d)}T${p(h)}:${p(mi)}:${p(s)}.${p(ms, 3)}` +
|
|
2440
|
+
`[GMT${off} ${zone}]`);
|
|
2441
|
+
}
|
|
2442
|
+
wallClockToInstant(y, monthIndex, day, h, m, s, ms) {
|
|
2443
|
+
const guess = utcFromFields(y, monthIndex, day, h, m, s, 0);
|
|
2444
|
+
try {
|
|
2445
|
+
const offset1 = this.zoneOffsetMs(guess);
|
|
2446
|
+
let instant = guess - offset1;
|
|
2447
|
+
const offset2 = this.zoneOffsetMs(instant);
|
|
2448
|
+
if (offset2 !== offset1)
|
|
2449
|
+
instant = guess - offset2;
|
|
2450
|
+
return new Date(instant + ms);
|
|
2451
|
+
}
|
|
2452
|
+
catch {
|
|
2453
|
+
const composed = new Date(this.value.getTime());
|
|
2454
|
+
composed.setHours(h, m, s, ms);
|
|
2455
|
+
return composed;
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
zoneOffsetMs(utcMs) {
|
|
2459
|
+
const dtf = new Intl.DateTimeFormat('en-US', {
|
|
2460
|
+
timeZone: this.timezone,
|
|
2461
|
+
hourCycle: 'h23',
|
|
2462
|
+
year: 'numeric', month: '2-digit', day: '2-digit',
|
|
2463
|
+
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
|
2464
|
+
});
|
|
2465
|
+
const map = {};
|
|
2466
|
+
for (const p of dtf.formatToParts(new Date(utcMs))) {
|
|
2467
|
+
if (p.type !== 'literal')
|
|
2468
|
+
map[p.type] = Number(p.value);
|
|
2469
|
+
}
|
|
2470
|
+
const asUTC = utcFromFields(map.year, map.month - 1, map.day, map.hour, map.minute, map.second);
|
|
2471
|
+
return asUTC - utcMs;
|
|
2472
|
+
}
|
|
2473
|
+
hasSelection() {
|
|
2474
|
+
if (this.type === 'relative') {
|
|
2475
|
+
return Boolean(this.startDate && this.endDate);
|
|
2476
|
+
}
|
|
2477
|
+
return Boolean(this.value);
|
|
2478
|
+
}
|
|
2479
|
+
handleSetToNow() {
|
|
2480
|
+
if (this.disabled)
|
|
2481
|
+
return;
|
|
2482
|
+
const now = new Date();
|
|
2483
|
+
this.tpHours = now.getHours();
|
|
2484
|
+
this.tpMinutes = now.getMinutes();
|
|
2485
|
+
this.tpSeconds = now.getSeconds();
|
|
2486
|
+
this.tpMilliseconds = now.getMilliseconds();
|
|
2487
|
+
this.value = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
2488
|
+
this.currentMonth = now.getMonth();
|
|
2489
|
+
this.currentYear = now.getFullYear();
|
|
2490
|
+
const local = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
2491
|
+
if (local) {
|
|
2492
|
+
this.setTimezoneInternally(local);
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
handleApply() {
|
|
2496
|
+
if (this.disabled || !this.hasSelection())
|
|
2497
|
+
return;
|
|
2498
|
+
if (this.type === 'relative') {
|
|
2499
|
+
this.emitDateTimePickerChange({
|
|
2500
|
+
startDate: this.startDate,
|
|
2501
|
+
endDate: this.endDate,
|
|
2502
|
+
timezone: 'UTC',
|
|
2503
|
+
});
|
|
2504
|
+
return;
|
|
2505
|
+
}
|
|
2506
|
+
const localTimezoneValue = this.composeLocalTimezoneValue();
|
|
2507
|
+
const unixEpoch = localTimezoneValue ? localTimezoneValue.getTime() : null;
|
|
2508
|
+
this.emitDateTimePickerChange({
|
|
2509
|
+
value: unixEpoch === null ? null : this.composeValueISO(unixEpoch),
|
|
2510
|
+
localTimezoneValue,
|
|
2511
|
+
unixEpoch,
|
|
2512
|
+
timezone: this.timezone,
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
handleReset() {
|
|
2516
|
+
if (this.disabled)
|
|
2517
|
+
return;
|
|
2518
|
+
this.clearInstantSeed();
|
|
2519
|
+
this.value = null;
|
|
2520
|
+
this.startDate = null;
|
|
2521
|
+
this.endDate = null;
|
|
2522
|
+
this.selectedUnit = undefined;
|
|
2523
|
+
this.selectedValue = undefined;
|
|
2524
|
+
this.resetTimeUnits();
|
|
2525
|
+
this.setTimezoneInternally(this.configuredTimezone);
|
|
2526
|
+
this.requestUpdate();
|
|
2527
|
+
this.emit('nile-clear');
|
|
2528
|
+
}
|
|
2529
|
+
emitDateTimePickerChange(data) {
|
|
2530
|
+
this.emit('nile-change', data);
|
|
2531
|
+
}
|
|
874
2532
|
};
|
|
875
2533
|
__decorate([
|
|
876
2534
|
property({ type: String, attribute: 'value' })
|
|
@@ -884,6 +2542,9 @@ __decorate([
|
|
|
884
2542
|
__decorate([
|
|
885
2543
|
property({ type: Boolean, attribute: true, reflect: true })
|
|
886
2544
|
], NileCalendar.prototype, "hideTypes", void 0);
|
|
2545
|
+
__decorate([
|
|
2546
|
+
property({ type: Boolean, attribute: true, reflect: true })
|
|
2547
|
+
], NileCalendar.prototype, "showTypes", void 0);
|
|
887
2548
|
__decorate([
|
|
888
2549
|
property({ type: Boolean, reflect: true })
|
|
889
2550
|
], NileCalendar.prototype, "doubleClickUnselect", void 0);
|
|
@@ -920,6 +2581,9 @@ __decorate([
|
|
|
920
2581
|
__decorate([
|
|
921
2582
|
property({ type: String, reflect: true, attribute: true })
|
|
922
2583
|
], NileCalendar.prototype, "dateFormat", void 0);
|
|
2584
|
+
__decorate([
|
|
2585
|
+
property({ type: Boolean, reflect: true })
|
|
2586
|
+
], NileCalendar.prototype, "structuredInput", void 0);
|
|
923
2587
|
__decorate([
|
|
924
2588
|
state()
|
|
925
2589
|
], NileCalendar.prototype, "startDate", void 0);
|
|
@@ -947,6 +2611,42 @@ __decorate([
|
|
|
947
2611
|
__decorate([
|
|
948
2612
|
property({ type: Boolean, attribute: true, reflect: true })
|
|
949
2613
|
], NileCalendar.prototype, "disabled", void 0);
|
|
2614
|
+
__decorate([
|
|
2615
|
+
property({ type: String, attribute: true })
|
|
2616
|
+
], NileCalendar.prototype, "variant", void 0);
|
|
2617
|
+
__decorate([
|
|
2618
|
+
property({ type: String, attribute: true })
|
|
2619
|
+
], NileCalendar.prototype, "timezone", void 0);
|
|
2620
|
+
__decorate([
|
|
2621
|
+
property({ type: Array, attribute: 'hide-time-fields' })
|
|
2622
|
+
], NileCalendar.prototype, "hideTimeFields", void 0);
|
|
2623
|
+
__decorate([
|
|
2624
|
+
state()
|
|
2625
|
+
], NileCalendar.prototype, "expanded", void 0);
|
|
2626
|
+
__decorate([
|
|
2627
|
+
state()
|
|
2628
|
+
], NileCalendar.prototype, "tpLockedHeight", void 0);
|
|
2629
|
+
__decorate([
|
|
2630
|
+
state()
|
|
2631
|
+
], NileCalendar.prototype, "tpHours", void 0);
|
|
2632
|
+
__decorate([
|
|
2633
|
+
state()
|
|
2634
|
+
], NileCalendar.prototype, "tpMinutes", void 0);
|
|
2635
|
+
__decorate([
|
|
2636
|
+
state()
|
|
2637
|
+
], NileCalendar.prototype, "tpSeconds", void 0);
|
|
2638
|
+
__decorate([
|
|
2639
|
+
state()
|
|
2640
|
+
], NileCalendar.prototype, "tpMilliseconds", void 0);
|
|
2641
|
+
__decorate([
|
|
2642
|
+
state()
|
|
2643
|
+
], NileCalendar.prototype, "tpFocusedTimeField", void 0);
|
|
2644
|
+
__decorate([
|
|
2645
|
+
state()
|
|
2646
|
+
], NileCalendar.prototype, "tpFocusDate", void 0);
|
|
2647
|
+
__decorate([
|
|
2648
|
+
state()
|
|
2649
|
+
], NileCalendar.prototype, "tpEditingText", void 0);
|
|
950
2650
|
__decorate([
|
|
951
2651
|
watch('allowedDates')
|
|
952
2652
|
], NileCalendar.prototype, "checkValidAllowedDate", null);
|