@aquera/nile-elements 2.0.1 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/demo/nxtgen-dark.css +2 -2
- package/dist/index.cjs.js +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.js +812 -465
- package/dist/nile-timestamp-builder/index.cjs.js +2 -0
- package/dist/nile-timestamp-builder/index.cjs.js.map +1 -0
- package/dist/nile-timestamp-builder/index.esm.js +1 -0
- package/dist/nile-timestamp-builder/nile-timestamp-builder-utils.cjs.js +2 -0
- package/dist/nile-timestamp-builder/nile-timestamp-builder-utils.cjs.js.map +1 -0
- package/dist/nile-timestamp-builder/nile-timestamp-builder-utils.esm.js +1 -0
- package/dist/nile-timestamp-builder/nile-timestamp-builder.cjs.js +2 -0
- package/dist/nile-timestamp-builder/nile-timestamp-builder.cjs.js.map +1 -0
- package/dist/nile-timestamp-builder/nile-timestamp-builder.css.cjs.js +2 -0
- package/dist/nile-timestamp-builder/nile-timestamp-builder.css.cjs.js.map +1 -0
- package/dist/nile-timestamp-builder/nile-timestamp-builder.css.esm.js +195 -0
- package/dist/nile-timestamp-builder/nile-timestamp-builder.esm.js +154 -0
- package/dist/nile-timestamp-builder/timezones.cjs.js +2 -0
- package/dist/nile-timestamp-builder/timezones.cjs.js.map +1 -0
- package/dist/nile-timestamp-builder/timezones.esm.js +1 -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-timestamp-builder/index.d.ts +1 -0
- package/dist/src/nile-timestamp-builder/index.js +2 -0
- package/dist/src/nile-timestamp-builder/index.js.map +1 -0
- package/dist/src/nile-timestamp-builder/nile-timestamp-builder-utils.d.ts +54 -0
- package/dist/src/nile-timestamp-builder/nile-timestamp-builder-utils.js +254 -0
- package/dist/src/nile-timestamp-builder/nile-timestamp-builder-utils.js.map +1 -0
- package/dist/src/nile-timestamp-builder/nile-timestamp-builder.css.d.ts +7 -0
- package/dist/src/nile-timestamp-builder/nile-timestamp-builder.css.js +203 -0
- package/dist/src/nile-timestamp-builder/nile-timestamp-builder.css.js.map +1 -0
- package/dist/src/nile-timestamp-builder/nile-timestamp-builder.d.ts +94 -0
- package/dist/src/nile-timestamp-builder/nile-timestamp-builder.js +578 -0
- package/dist/src/nile-timestamp-builder/nile-timestamp-builder.js.map +1 -0
- package/dist/src/nile-timestamp-builder/nile-timestamp-builder.test.d.ts +1 -0
- package/dist/src/nile-timestamp-builder/nile-timestamp-builder.test.js +890 -0
- package/dist/src/nile-timestamp-builder/nile-timestamp-builder.test.js.map +1 -0
- package/dist/src/nile-timestamp-builder/timezones.d.ts +8 -0
- package/dist/src/nile-timestamp-builder/timezones.js +357 -0
- package/dist/src/nile-timestamp-builder/timezones.js.map +1 -0
- package/dist/src/version.js +2 -2
- package/dist/src/version.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/nile-timestamp-builder/index.ts +1 -0
- package/src/nile-timestamp-builder/nile-timestamp-builder-utils.ts +321 -0
- package/src/nile-timestamp-builder/nile-timestamp-builder.css.ts +204 -0
- package/src/nile-timestamp-builder/nile-timestamp-builder.test.ts +1089 -0
- package/src/nile-timestamp-builder/nile-timestamp-builder.ts +625 -0
- package/src/nile-timestamp-builder/timezones.ts +356 -0
- package/vscode-html-custom-data.json +92 -0
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Aquera Inc 2026
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the BSD-3-Clause license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { html, nothing } from 'lit';
|
|
9
|
+
import { customElement, property, query, state } from 'lit/decorators.js';
|
|
10
|
+
import { classMap } from 'lit/directives/class-map.js';
|
|
11
|
+
import { styles } from './nile-timestamp-builder.css';
|
|
12
|
+
import NileElement from '../internal/nile-element';
|
|
13
|
+
import type { CSSResultGroup, PropertyValues } from 'lit';
|
|
14
|
+
import type { NileFormControl } from '../internal/nile-element';
|
|
15
|
+
import { FormControlController } from '../internal/form';
|
|
16
|
+
import { defaultValue } from '../internal/default-value';
|
|
17
|
+
import {
|
|
18
|
+
EPOCH_WALLCLOCK,
|
|
19
|
+
TimestampField,
|
|
20
|
+
WallClock,
|
|
21
|
+
clampWallClock,
|
|
22
|
+
daysInMonth,
|
|
23
|
+
epochToFields,
|
|
24
|
+
fieldsToEpoch,
|
|
25
|
+
formatWallClock,
|
|
26
|
+
formatZonedValue,
|
|
27
|
+
getLocalTimeZone,
|
|
28
|
+
getTimezoneList,
|
|
29
|
+
isValidTimeZone,
|
|
30
|
+
normalizeValueInput,
|
|
31
|
+
parseZonedValue,
|
|
32
|
+
} from './nile-timestamp-builder-utils';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @event nile-init - Emitted when the component is added to the DOM.
|
|
36
|
+
* @event nile-destroy - Emitted when the component is removed from the DOM.
|
|
37
|
+
* @event nile-change - Emitted when the value is committed.
|
|
38
|
+
* @event nile-input - Emitted as fields change, before the value is committed.
|
|
39
|
+
* @event nile-expand - Emitted when the editor opens.
|
|
40
|
+
* @event nile-collapse - Emitted when the editor closes.
|
|
41
|
+
* @event nile-copy - Emitted after the value is written to the clipboard.
|
|
42
|
+
* @event nile-copy-error - Emitted when the clipboard write is refused.
|
|
43
|
+
*/
|
|
44
|
+
@customElement('nile-timestamp-builder')
|
|
45
|
+
export class NileTimestampBuilder
|
|
46
|
+
extends NileElement
|
|
47
|
+
implements NileFormControl
|
|
48
|
+
{
|
|
49
|
+
static get styles(): CSSResultGroup {
|
|
50
|
+
return [styles];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private formControlController: FormControlController | null = null;
|
|
54
|
+
private internalValueUpdate = false;
|
|
55
|
+
private internalTimezoneUpdate = false;
|
|
56
|
+
@query('.value-input') valueInput!: HTMLInputElement;private _value: Date | null = null;
|
|
57
|
+
@property({
|
|
58
|
+
attribute: 'value',
|
|
59
|
+
converter: {
|
|
60
|
+
fromAttribute: (value: string | null) => value,
|
|
61
|
+
toAttribute: (value: Date | null) =>
|
|
62
|
+
value === null ? null : value.toISOString(),
|
|
63
|
+
},
|
|
64
|
+
})
|
|
65
|
+
get value(): Date | null {
|
|
66
|
+
return this._value;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
set value(input: Date | number | string | null) {
|
|
70
|
+
if (typeof input === 'string') {
|
|
71
|
+
const zoned = parseZonedValue(input);
|
|
72
|
+
if (zoned && isValidTimeZone(zoned.timezone)) {
|
|
73
|
+
this.timezone = zoned.timezone;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const oldValue = this._value;
|
|
77
|
+
const next = normalizeValueInput(input);
|
|
78
|
+
if (next?.getTime() === oldValue?.getTime()) {
|
|
79
|
+
this.internalValueUpdate = false;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
this._value = next;
|
|
83
|
+
this.requestUpdate('value', oldValue);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@defaultValue() defaultValue: Date | null = null;
|
|
87
|
+
@property({ type: String, reflect: true }) timezone: string =
|
|
88
|
+
getLocalTimeZone();
|
|
89
|
+
@property({ type: String, reflect: true }) name = '';
|
|
90
|
+
@property({ type: Boolean, reflect: true }) disabled = false;
|
|
91
|
+
@property({ type: Boolean, reflect: true }) required = false;
|
|
92
|
+
@property({ type: Boolean, reflect: true }) readonly = false;
|
|
93
|
+
@property({ type: Boolean, reflect: true }) copyable = false;
|
|
94
|
+
@property({ type: String, reflect: true }) form = '';
|
|
95
|
+
@property({ type: Boolean, reflect: true }) open = false;
|
|
96
|
+
@property({ type: String, reflect: true }) label = '';
|
|
97
|
+
@property({ attribute: true, type: Number, reflect: true }) minYear = 1;
|
|
98
|
+
@property({ attribute: true, type: Number, reflect: true }) maxYear = 9999;
|
|
99
|
+
@state() wallClock: WallClock = EPOCH_WALLCLOCK;
|
|
100
|
+
@state() timezoneList: string[] = [];
|
|
101
|
+
@state() hasValue = false;
|
|
102
|
+
@state() justCopied = false;
|
|
103
|
+
|
|
104
|
+
get validity(): ValidityState {
|
|
105
|
+
return this.valueInput?.validity;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
get validationMessage(): string {
|
|
109
|
+
return this.valueInput?.validationMessage ?? '';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
connectedCallback() {
|
|
113
|
+
super.connectedCallback();
|
|
114
|
+
|
|
115
|
+
this.timezoneList = getTimezoneList();
|
|
116
|
+
|
|
117
|
+
if (!isValidTimeZone(this.timezone)) {
|
|
118
|
+
this.timezone = getLocalTimeZone();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (this.value !== null) {
|
|
122
|
+
this.wallClock = epochToFields(this.value.getTime(), this.timezone);
|
|
123
|
+
this.hasValue = true;
|
|
124
|
+
} else {
|
|
125
|
+
this.wallClock = epochToFields(Date.now(), this.timezone);
|
|
126
|
+
this.hasValue = false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.formControlController = new FormControlController(this, {
|
|
130
|
+
assumeInteractionOn: ['nile-change'],
|
|
131
|
+
value: input => {
|
|
132
|
+
const el = input as NileTimestampBuilder;
|
|
133
|
+
return el.value === null ? undefined : (el.valueString() ?? undefined);
|
|
134
|
+
},
|
|
135
|
+
defaultValue: input => (input as NileTimestampBuilder).defaultValue,
|
|
136
|
+
setValue: (input, val) => {
|
|
137
|
+
(input as NileTimestampBuilder).value = val as string | null;
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
this.emit('nile-init');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
disconnectedCallback() {
|
|
145
|
+
super.disconnectedCallback();
|
|
146
|
+
window.clearTimeout(this.copiedTimer);
|
|
147
|
+
this.emit('nile-destroy');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
protected willUpdate(changedProperties: PropertyValues): void {
|
|
151
|
+
if (changedProperties.has('value')) {
|
|
152
|
+
if (this.internalValueUpdate) {
|
|
153
|
+
this.internalValueUpdate = false;
|
|
154
|
+
} else {
|
|
155
|
+
if (this.value === null) {
|
|
156
|
+
this.hasValue = false;
|
|
157
|
+
} else {
|
|
158
|
+
this.wallClock = epochToFields(this.value.getTime(), this.timezone);
|
|
159
|
+
this.hasValue = true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} else if (this.isTimezoneReassigned(changedProperties) && !this.open) {
|
|
163
|
+
this.syncValueToTimezone();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
protected updated(changedProperties: PropertyValues): void {
|
|
168
|
+
if (this.isTimezoneReassigned(changedProperties)) {
|
|
169
|
+
if (this.internalTimezoneUpdate) {
|
|
170
|
+
this.internalTimezoneUpdate = false;
|
|
171
|
+
} else {
|
|
172
|
+
this.emit('nile-input', this.liveDetail());
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private isTimezoneReassigned(changedProperties: PropertyValues): boolean {
|
|
178
|
+
return (
|
|
179
|
+
changedProperties.has('timezone') &&
|
|
180
|
+
changedProperties.get('timezone') !== undefined
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private syncValueToTimezone(): void {
|
|
185
|
+
if (!this.hasValue || this.value === null) return;
|
|
186
|
+
const epoch = fieldsToEpoch(this.wallClock, this.timezone);
|
|
187
|
+
if (Number.isNaN(epoch) || epoch === this.value.getTime()) return;
|
|
188
|
+
this.internalValueUpdate = true;
|
|
189
|
+
this.value = new Date(epoch);
|
|
190
|
+
this.syncHiddenInput();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private valueString(): string | null {
|
|
194
|
+
return this.value === null ? null : this.value.toISOString();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private syncHiddenInput(): void {
|
|
198
|
+
if (this.valueInput) {
|
|
199
|
+
this.valueInput.value = this.valueString() ?? '';
|
|
200
|
+
}
|
|
201
|
+
this.formControlController?.updateValidity();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private changeDetail(): {
|
|
205
|
+
value: string | null;
|
|
206
|
+
localTimezoneValue: Date | null;
|
|
207
|
+
unix: number | null;
|
|
208
|
+
timezone: string;
|
|
209
|
+
} {
|
|
210
|
+
if (this.value === null) {
|
|
211
|
+
return {
|
|
212
|
+
value: null,
|
|
213
|
+
localTimezoneValue: null,
|
|
214
|
+
unix: null,
|
|
215
|
+
timezone: this.timezone,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const epoch = this.value.getTime();
|
|
219
|
+
return {
|
|
220
|
+
value: formatZonedValue(this.wallClock, this.timezone),
|
|
221
|
+
localTimezoneValue: new Date(epoch),
|
|
222
|
+
// Unix time (epoch milliseconds, matching Date.getTime()).
|
|
223
|
+
unix: epoch,
|
|
224
|
+
timezone: this.timezone,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
private liveDetail(): {
|
|
230
|
+
value: string | null;
|
|
231
|
+
localTimezoneValue: Date | null;
|
|
232
|
+
unix: number | null;
|
|
233
|
+
timezone: string;
|
|
234
|
+
} {
|
|
235
|
+
const epoch = fieldsToEpoch(this.wallClock, this.timezone);
|
|
236
|
+
if (Number.isNaN(epoch)) {
|
|
237
|
+
return {
|
|
238
|
+
value: null,
|
|
239
|
+
localTimezoneValue: null,
|
|
240
|
+
unix: null,
|
|
241
|
+
timezone: this.timezone,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
value: formatZonedValue(this.wallClock, this.timezone),
|
|
246
|
+
localTimezoneValue: new Date(epoch),
|
|
247
|
+
// Unix time (epoch milliseconds, matching Date.getTime()).
|
|
248
|
+
unix: epoch,
|
|
249
|
+
timezone: this.timezone,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private commit(): void {
|
|
254
|
+
const epoch = fieldsToEpoch(this.wallClock, this.timezone);
|
|
255
|
+
this.internalValueUpdate = true;
|
|
256
|
+
this.value = new Date(epoch);
|
|
257
|
+
this.hasValue = true;
|
|
258
|
+
this.syncHiddenInput();
|
|
259
|
+
this.emit('nile-change', this.changeDetail());
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
private onFieldChange(field: TimestampField) {
|
|
264
|
+
return (event: CustomEvent) => {
|
|
265
|
+
event.stopPropagation();
|
|
266
|
+
const raw = Number(event.detail?.value);
|
|
267
|
+
const next = clampWallClock(
|
|
268
|
+
{ ...this.wallClock, [field]: raw },
|
|
269
|
+
this.minYear,
|
|
270
|
+
this.maxYear
|
|
271
|
+
);
|
|
272
|
+
this.wallClock = next;
|
|
273
|
+
this.emit('nile-input', this.liveDetail());
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private onTimezoneChange(event: CustomEvent) {
|
|
278
|
+
event.stopPropagation();
|
|
279
|
+
const next = event.detail?.value;
|
|
280
|
+
if (typeof next === 'string' && isValidTimeZone(next)) {
|
|
281
|
+
this.internalTimezoneUpdate = true;
|
|
282
|
+
this.timezone = next;
|
|
283
|
+
this.emit('nile-input', this.liveDetail());
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private handleSetNow = () => {
|
|
288
|
+
if (this.disabled || this.readonly) return;
|
|
289
|
+
const now = Date.now();
|
|
290
|
+
this.wallClock = epochToFields(now, this.timezone);
|
|
291
|
+
this.commit();
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
/** Copies the canonical zoned value */
|
|
295
|
+
private handleCopy = async () => {
|
|
296
|
+
if (this.disabled || !this.hasValue) return;
|
|
297
|
+
const text = formatZonedValue(this.wallClock, this.timezone);
|
|
298
|
+
try {
|
|
299
|
+
await navigator.clipboard.writeText(text);
|
|
300
|
+
} catch {
|
|
301
|
+
this.emit('nile-copy-error', { value: text });
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
this.justCopied = true;
|
|
305
|
+
window.clearTimeout(this.copiedTimer);
|
|
306
|
+
this.copiedTimer = window.setTimeout(() => {
|
|
307
|
+
this.justCopied = false;
|
|
308
|
+
}, NileTimestampBuilder.COPIED_FEEDBACK_MS);
|
|
309
|
+
this.emit('nile-copy', { value: text });
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
private handleStageNow = () => {
|
|
313
|
+
if (this.disabled || this.readonly) return;
|
|
314
|
+
const now = Date.now();
|
|
315
|
+
this.wallClock = epochToFields(now, this.timezone);
|
|
316
|
+
this.emit('nile-input', this.liveDetail());
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
private handleUseLocalTz = () => {
|
|
320
|
+
if (this.disabled || this.readonly) return;
|
|
321
|
+
this.internalTimezoneUpdate = true;
|
|
322
|
+
this.timezone = getLocalTimeZone();
|
|
323
|
+
this.emit('nile-input', this.liveDetail());
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
private handleUseUtc = () => {
|
|
327
|
+
if (this.disabled || this.readonly) return;
|
|
328
|
+
this.internalTimezoneUpdate = true;
|
|
329
|
+
this.timezone = 'UTC';
|
|
330
|
+
this.emit('nile-input', this.liveDetail());
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
/** Touch input has no reliable `dblclick`, so double-taps are timed by hand. */
|
|
334
|
+
private static readonly DOUBLE_TAP_MS = 300;
|
|
335
|
+
private static readonly COPIED_FEEDBACK_MS = 1600;
|
|
336
|
+
private lastTapAt = 0;
|
|
337
|
+
private copiedTimer = 0;
|
|
338
|
+
|
|
339
|
+
private handleDisplayDoubleClick = () => {
|
|
340
|
+
this.handleEdit();
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
private handleDisplayPointerUp = (event: PointerEvent) => {
|
|
344
|
+
if (event.pointerType === 'mouse') return;
|
|
345
|
+
const now = Date.now();
|
|
346
|
+
if (now - this.lastTapAt <= NileTimestampBuilder.DOUBLE_TAP_MS) {
|
|
347
|
+
this.lastTapAt = 0;
|
|
348
|
+
this.handleEdit();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
this.lastTapAt = now;
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
/** Keyboard equivalent of the double-click/double-tap edit affordance. */
|
|
355
|
+
private handleDisplayKeyDown = (event: KeyboardEvent) => {
|
|
356
|
+
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
357
|
+
event.preventDefault();
|
|
358
|
+
this.handleEdit();
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
private handleEdit = () => {
|
|
362
|
+
if (this.disabled || this.readonly || this.open) return;
|
|
363
|
+
if (this.value === null) {
|
|
364
|
+
this.wallClock = epochToFields(Date.now(), this.timezone);
|
|
365
|
+
}
|
|
366
|
+
this.open = true;
|
|
367
|
+
this.emit('nile-expand', { open: true });
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
private handleDone = () => {
|
|
371
|
+
this.open = false;
|
|
372
|
+
this.commit();
|
|
373
|
+
this.emit('nile-collapse', { open: false });
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
checkValidity(): boolean {
|
|
377
|
+
return this.valueInput?.checkValidity() ?? true;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
reportValidity(): boolean {
|
|
381
|
+
return this.valueInput?.reportValidity() ?? true;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
setCustomValidity(message: string): void {
|
|
385
|
+
this.valueInput?.setCustomValidity(message);
|
|
386
|
+
this.formControlController?.updateValidity();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
getForm(): HTMLFormElement | null {
|
|
390
|
+
return this.formControlController?.getForm() ?? null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
private renderField(
|
|
394
|
+
field: TimestampField,
|
|
395
|
+
fieldLabel: string,
|
|
396
|
+
val: number,
|
|
397
|
+
min: number,
|
|
398
|
+
max: number
|
|
399
|
+
) {
|
|
400
|
+
return html`
|
|
401
|
+
<div class="tsb__field" part="field">
|
|
402
|
+
<nile-input
|
|
403
|
+
type="number"
|
|
404
|
+
exportparts="base:field__base, input:field__input"
|
|
405
|
+
size="medium"
|
|
406
|
+
label=${fieldLabel}
|
|
407
|
+
?disabled=${this.disabled}
|
|
408
|
+
?readonly=${this.readonly}
|
|
409
|
+
min=${min}
|
|
410
|
+
max=${max}
|
|
411
|
+
step="1"
|
|
412
|
+
.value=${String(val)}
|
|
413
|
+
@nile-change=${this.onFieldChange(field)}
|
|
414
|
+
></nile-input>
|
|
415
|
+
</div>
|
|
416
|
+
`;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private renderCollapsed() {
|
|
420
|
+
const wc = this.wallClock;
|
|
421
|
+
const editable = !this.disabled && !this.readonly;
|
|
422
|
+
return html`
|
|
423
|
+
<div class="tsb__collapsed" part="display">
|
|
424
|
+
<div
|
|
425
|
+
class=${classMap({
|
|
426
|
+
tsb__display: true,
|
|
427
|
+
'tsb__display--editable': editable,
|
|
428
|
+
})}
|
|
429
|
+
part="display-content"
|
|
430
|
+
role=${editable ? 'button' : nothing}
|
|
431
|
+
tabindex=${this.disabled ? nothing : '0'}
|
|
432
|
+
aria-labelledby=${this.label ? 'tsb-label tsb-display-value' : nothing}
|
|
433
|
+
aria-readonly=${this.readonly ? 'true' : nothing}
|
|
434
|
+
@dblclick=${this.handleDisplayDoubleClick}
|
|
435
|
+
@pointerup=${this.handleDisplayPointerUp}
|
|
436
|
+
@keydown=${this.handleDisplayKeyDown}
|
|
437
|
+
>
|
|
438
|
+
<span
|
|
439
|
+
id="tsb-display-value"
|
|
440
|
+
class=${classMap({
|
|
441
|
+
tsb__value: true,
|
|
442
|
+
'tsb__value--empty': !this.hasValue,
|
|
443
|
+
})}
|
|
444
|
+
part="value"
|
|
445
|
+
>
|
|
446
|
+
${this.hasValue ? formatWallClock(wc) : 'No timestamp set'}
|
|
447
|
+
</span>
|
|
448
|
+
<span class="tsb__tz" part="timezone-label">${this.timezone}</span>
|
|
449
|
+
</div>
|
|
450
|
+
${editable ? this.renderEditActions() : nothing}
|
|
451
|
+
${this.copyable && this.readonly && !this.disabled
|
|
452
|
+
? this.renderCopyAction()
|
|
453
|
+
: nothing}
|
|
454
|
+
</div>
|
|
455
|
+
`;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private renderEditActions() {
|
|
459
|
+
return html`
|
|
460
|
+
<div class="tsb__actions" part="actions">
|
|
461
|
+
<nile-button
|
|
462
|
+
variant="secondary-grey"
|
|
463
|
+
?disabled=${this.disabled}
|
|
464
|
+
@click=${this.handleSetNow}
|
|
465
|
+
>Now</nile-button
|
|
466
|
+
>
|
|
467
|
+
<nile-button
|
|
468
|
+
variant="primary"
|
|
469
|
+
title="Edit"
|
|
470
|
+
?disabled=${this.disabled}
|
|
471
|
+
@click=${this.handleEdit}
|
|
472
|
+
><nile-glyph
|
|
473
|
+
name="ng-pencil"
|
|
474
|
+
size="16"
|
|
475
|
+
method="stroke"
|
|
476
|
+
color="currentColor"
|
|
477
|
+
></nile-glyph
|
|
478
|
+
></nile-button
|
|
479
|
+
>
|
|
480
|
+
</div>
|
|
481
|
+
`;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private renderCopyAction() {
|
|
485
|
+
return html`
|
|
486
|
+
<div class="tsb__actions" part="actions">
|
|
487
|
+
<nile-button
|
|
488
|
+
variant="ghost"
|
|
489
|
+
title=${this.justCopied ? 'Copied' : 'Copy timestamp'}
|
|
490
|
+
aria-label=${this.justCopied ? 'Copied' : 'Copy timestamp'}
|
|
491
|
+
?disabled=${!this.hasValue}
|
|
492
|
+
@click=${this.handleCopy}
|
|
493
|
+
><nile-glyph
|
|
494
|
+
name=${this.justCopied ? 'ng-copy-check' : 'ng-copy'}
|
|
495
|
+
size="16"
|
|
496
|
+
method="stroke"
|
|
497
|
+
color="currentColor"
|
|
498
|
+
></nile-glyph
|
|
499
|
+
></nile-button>
|
|
500
|
+
</div>
|
|
501
|
+
`;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private renderExpanded() {
|
|
505
|
+
const wc = this.wallClock;
|
|
506
|
+
const maxDay = daysInMonth(wc.year, wc.month);
|
|
507
|
+
return html`
|
|
508
|
+
<div class="tsb__expanded" part="editor">
|
|
509
|
+
<div class="tsb__fields" part="fields">
|
|
510
|
+
${this.renderField(
|
|
511
|
+
TimestampField.Year,
|
|
512
|
+
'Year',
|
|
513
|
+
wc.year,
|
|
514
|
+
this.minYear,
|
|
515
|
+
this.maxYear
|
|
516
|
+
)}
|
|
517
|
+
${this.renderField(TimestampField.Month, 'Month', wc.month, 1, 12)}
|
|
518
|
+
${this.renderField(TimestampField.Day, 'Day', wc.day, 1, maxDay)}
|
|
519
|
+
${this.renderField(TimestampField.Hour, 'Hour', wc.hour, 0, 23)}
|
|
520
|
+
${this.renderField(
|
|
521
|
+
TimestampField.Minute,
|
|
522
|
+
'Minute',
|
|
523
|
+
wc.minute,
|
|
524
|
+
0,
|
|
525
|
+
59
|
|
526
|
+
)}
|
|
527
|
+
${this.renderField(
|
|
528
|
+
TimestampField.Second,
|
|
529
|
+
'Second',
|
|
530
|
+
wc.second,
|
|
531
|
+
0,
|
|
532
|
+
59
|
|
533
|
+
)}
|
|
534
|
+
${this.renderField(
|
|
535
|
+
TimestampField.Millisecond,
|
|
536
|
+
'Milliseconds',
|
|
537
|
+
wc.millisecond,
|
|
538
|
+
0,
|
|
539
|
+
999
|
|
540
|
+
)}
|
|
541
|
+
</div>
|
|
542
|
+
|
|
543
|
+
<div class="tsb__timezone" part="timezone-select">
|
|
544
|
+
<nile-select
|
|
545
|
+
searchEnabled
|
|
546
|
+
label="Timezone"
|
|
547
|
+
size="medium"
|
|
548
|
+
?disabled=${this.disabled}
|
|
549
|
+
.value=${this.timezone}
|
|
550
|
+
@nile-change=${this.onTimezoneChange}
|
|
551
|
+
>
|
|
552
|
+
${this.timezoneList.map(
|
|
553
|
+
tz => html`<nile-option value=${tz}>${tz}</nile-option>`
|
|
554
|
+
)}
|
|
555
|
+
</nile-select>
|
|
556
|
+
</div>
|
|
557
|
+
|
|
558
|
+
<div class="tsb__actions" part="editor-actions">
|
|
559
|
+
<nile-button
|
|
560
|
+
variant="secondary-grey"
|
|
561
|
+
?disabled=${this.disabled}
|
|
562
|
+
@click=${this.handleStageNow}
|
|
563
|
+
>Set to Now</nile-button
|
|
564
|
+
>
|
|
565
|
+
<nile-button
|
|
566
|
+
variant="secondary-grey"
|
|
567
|
+
?disabled=${this.disabled}
|
|
568
|
+
@click=${this.handleUseLocalTz}
|
|
569
|
+
>Use Local Timezone</nile-button
|
|
570
|
+
>
|
|
571
|
+
<nile-button
|
|
572
|
+
variant="secondary-grey"
|
|
573
|
+
?disabled=${this.disabled}
|
|
574
|
+
@click=${this.handleUseUtc}
|
|
575
|
+
>Use UTC</nile-button
|
|
576
|
+
>
|
|
577
|
+
<nile-button
|
|
578
|
+
variant="primary"
|
|
579
|
+
?disabled=${this.disabled}
|
|
580
|
+
@click=${this.handleDone}
|
|
581
|
+
>Done</nile-button
|
|
582
|
+
>
|
|
583
|
+
</div>
|
|
584
|
+
</div>
|
|
585
|
+
`;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
render() {
|
|
589
|
+
return html`
|
|
590
|
+
<div
|
|
591
|
+
class="tsb"
|
|
592
|
+
part="base"
|
|
593
|
+
role="group"
|
|
594
|
+
aria-labelledby=${this.label ? 'tsb-label' : nothing}
|
|
595
|
+
>
|
|
596
|
+
${this.label
|
|
597
|
+
? html`<label id="tsb-label" class="tsb__label" part="label"
|
|
598
|
+
>${this.label}</label
|
|
599
|
+
>`
|
|
600
|
+
: ''}
|
|
601
|
+
${this.open && !this.readonly
|
|
602
|
+
? this.renderExpanded()
|
|
603
|
+
: this.renderCollapsed()}
|
|
604
|
+
|
|
605
|
+
<input
|
|
606
|
+
class="value-input"
|
|
607
|
+
type="text"
|
|
608
|
+
?required=${this.required}
|
|
609
|
+
?disabled=${this.disabled}
|
|
610
|
+
.value=${this.valueString() ?? ''}
|
|
611
|
+
tabindex="-1"
|
|
612
|
+
aria-hidden="true"
|
|
613
|
+
/>
|
|
614
|
+
</div>
|
|
615
|
+
`;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
export default NileTimestampBuilder;
|
|
620
|
+
|
|
621
|
+
declare global {
|
|
622
|
+
interface HTMLElementTagNameMap {
|
|
623
|
+
'nile-timestamp-builder': NileTimestampBuilder;
|
|
624
|
+
}
|
|
625
|
+
}
|