@design.estate/dees-catalog 9.6.0 → 9.7.0
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/dist_bundle/bundle.js +2957 -2685
- package/dist_bundle/bundle.js.map +1 -1
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/demos/inputs.d.ts +1 -1
- package/dist_ts_web/demos/inputs.js +5 -4
- package/dist_ts_web/elements/00group-dataview/dees-table/dees-table.js +5 -4
- package/dist_ts_web/elements/00group-form/dees-form/dees-form.d.ts +5 -1
- package/dist_ts_web/elements/00group-form/dees-form/dees-form.js +27 -3
- package/dist_ts_web/elements/00group-harness/dees-harness-chat/dees-harness-chat.d.ts +1 -0
- package/dist_ts_web/elements/00group-harness/dees-harness-chat/dees-harness-chat.js +11 -2
- package/dist_ts_web/elements/00group-harness/dees-harness-composer/dees-harness-composer.d.ts +3 -0
- package/dist_ts_web/elements/00group-harness/dees-harness-composer/dees-harness-composer.demo.js +4 -1
- package/dist_ts_web/elements/00group-harness/dees-harness-composer/dees-harness-composer.js +51 -13
- package/dist_ts_web/elements/00group-input/dees-input-base/dees-input-base.d.ts +1 -8
- package/dist_ts_web/elements/00group-input/dees-input-base/dees-input-base.js +7 -2
- package/dist_ts_web/elements/00group-input/dees-input-datepicker/component.d.ts +18 -3
- package/dist_ts_web/elements/00group-input/dees-input-datepicker/component.js +181 -144
- package/dist_ts_web/elements/00group-input/dees-input-datepicker/date-validation.d.ts +13 -0
- package/dist_ts_web/elements/00group-input/dees-input-datepicker/date-validation.js +65 -0
- package/dist_ts_web/elements/00group-input/dees-input-datepicker/datepicker-popup.js +44 -56
- package/dist_ts_web/elements/00group-input/dees-input-datepicker/styles.js +6 -1
- package/dist_ts_web/elements/00group-input/dees-input-datepicker/template.js +11 -5
- package/dist_ts_web/elements/00group-input/dees-input-text/dees-input-text.d.ts +8 -8
- package/dist_ts_web/elements/00group-input/dees-input-text/dees-input-text.js +6 -53
- package/dist_ts_web/elements/00group-input/dees-input-textarea/dees-input-textarea.d.ts +62 -0
- package/dist_ts_web/elements/00group-input/dees-input-textarea/dees-input-textarea.demo.d.ts +1 -0
- package/dist_ts_web/elements/00group-input/dees-input-textarea/dees-input-textarea.demo.js +3 -0
- package/dist_ts_web/elements/00group-input/dees-input-textarea/dees-input-textarea.js +224 -0
- package/dist_ts_web/elements/00group-input/dees-input-textarea/index.d.ts +1 -0
- package/dist_ts_web/elements/00group-input/dees-input-textarea/index.js +2 -0
- package/dist_ts_web/elements/00group-input/index.d.ts +1 -0
- package/dist_ts_web/elements/00group-input/index.js +2 -1
- package/dist_ts_web/elements/00group-input/text-input-contextmenu.d.ts +29 -0
- package/dist_ts_web/elements/00group-input/text-input-contextmenu.js +29 -0
- package/dist_ts_web/elements/00group-overlay/dees-contextmenu/dees-contextmenu.d.ts +1 -1
- package/dist_ts_web/elements/00group-overlay/dees-contextmenu/dees-contextmenu.js +5 -4
- package/package.json +2 -2
- package/readme.md +78 -4
- package/scripts/check-bdtheme-ratchet.cjs +2 -2
- package/scripts/check-packed-consumer.mjs +48 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/demos/inputs.ts +5 -4
- package/ts_web/elements/00group-dataview/dees-table/dees-table.ts +3 -2
- package/ts_web/elements/00group-form/dees-form/dees-form.ts +25 -2
- package/ts_web/elements/00group-harness/dees-harness-chat/dees-harness-chat.ts +4 -0
- package/ts_web/elements/00group-harness/dees-harness-composer/dees-harness-composer.demo.ts +3 -0
- package/ts_web/elements/00group-harness/dees-harness-composer/dees-harness-composer.ts +37 -11
- package/ts_web/elements/00group-input/dees-input-base/dees-input-base.ts +6 -1
- package/ts_web/elements/00group-input/dees-input-datepicker/component.ts +149 -148
- package/ts_web/elements/00group-input/dees-input-datepicker/date-validation.ts +64 -0
- package/ts_web/elements/00group-input/dees-input-datepicker/datepicker-popup.ts +40 -40
- package/ts_web/elements/00group-input/dees-input-datepicker/styles.ts +5 -0
- package/ts_web/elements/00group-input/dees-input-datepicker/template.ts +10 -4
- package/ts_web/elements/00group-input/dees-input-text/dees-input-text.ts +5 -52
- package/ts_web/elements/00group-input/dees-input-textarea/dees-input-textarea.demo.ts +2 -0
- package/ts_web/elements/00group-input/dees-input-textarea/dees-input-textarea.ts +128 -0
- package/ts_web/elements/00group-input/dees-input-textarea/index.ts +1 -0
- package/ts_web/elements/00group-input/index.ts +1 -0
- package/ts_web/elements/00group-input/text-input-contextmenu.ts +31 -0
- package/ts_web/elements/00group-overlay/dees-contextmenu/dees-contextmenu.ts +3 -3
- package/readme.hints.md +0 -1233
- package/readme.icons.md +0 -1849
- package/readme.info.md +0 -80
- package/readme.plan.md +0 -671
- package/readme.playbook.md +0 -820
- package/readme.theme-migration.md +0 -168
|
@@ -9,6 +9,7 @@ import { demoFunc } from './demo.js';
|
|
|
9
9
|
import { datepickerStyles } from './styles.js';
|
|
10
10
|
import { renderDatepicker } from './template.js';
|
|
11
11
|
import { DeesInputDatepickerPopup } from './datepicker-popup.js';
|
|
12
|
+
import { createCalendarDate, dateValidationMessage, parseDateFields, parseDateValue } from './date-validation.js';
|
|
12
13
|
import '../../00group-utility/dees-icon/dees-icon.js';
|
|
13
14
|
import '../../00group-layout/dees-label/dees-label.js';
|
|
14
15
|
|
|
@@ -78,6 +79,17 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
|
|
78
79
|
@state()
|
|
79
80
|
accessor selectedMinute: number = 0;
|
|
80
81
|
|
|
82
|
+
/** Uncommitted text stays visible even when it cannot be represented by value. */
|
|
83
|
+
@state()
|
|
84
|
+
accessor draftText: string | null = null;
|
|
85
|
+
|
|
86
|
+
@state()
|
|
87
|
+
accessor validationText = '';
|
|
88
|
+
|
|
89
|
+
public get validationState(): 'invalid' | 'valid' { return this.validationText ? 'invalid' : 'valid'; }
|
|
90
|
+
|
|
91
|
+
private lastCommittedValue: string | undefined;
|
|
92
|
+
|
|
81
93
|
private popupInstance: DeesInputDatepickerPopup | null = null;
|
|
82
94
|
|
|
83
95
|
public static styles = datepickerStyles;
|
|
@@ -86,42 +98,59 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
|
|
86
98
|
return renderDatepicker(this);
|
|
87
99
|
}
|
|
88
100
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
101
|
+
public willUpdate(changed: Map<PropertyKey, unknown>): void {
|
|
102
|
+
super.willUpdate(changed);
|
|
103
|
+
if (changed.has('value') && this.value !== this.lastCommittedValue) {
|
|
104
|
+
this.syncValue(this.value);
|
|
92
105
|
}
|
|
106
|
+
}
|
|
93
107
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
108
|
+
public updated(changed: Map<string, unknown>): void {
|
|
109
|
+
super.updated(changed);
|
|
110
|
+
if (changed.has('disabled') && this.disabled) this.closePopup();
|
|
111
|
+
if (['value', 'minDate', 'maxDate', 'disabledDates', 'required', 'disabled'].some(key => changed.has(key))) {
|
|
112
|
+
this.updateValidation(this.currentValidationMessage());
|
|
113
|
+
}
|
|
114
|
+
if (this.popupInstance) {
|
|
115
|
+
this.popupInstance.minDate = this.minDate;
|
|
116
|
+
this.popupInstance.maxDate = this.maxDate;
|
|
117
|
+
this.popupInstance.disabledDates = this.disabledDates;
|
|
118
|
+
if (changed.has('selectedDate')) {
|
|
119
|
+
this.popupInstance.selectedDate = this.selectedDate;
|
|
120
|
+
this.popupInstance.viewDate = new Date(this.viewDate);
|
|
121
|
+
this.popupInstance.selectedHour = this.selectedHour;
|
|
122
|
+
this.popupInstance.selectedMinute = this.selectedMinute;
|
|
105
123
|
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
this.
|
|
124
|
+
}
|
|
125
|
+
const input = this.shadowRoot?.querySelector('input');
|
|
126
|
+
input?.setCustomValidity(this.disabled ? '' : this.validationText);
|
|
127
|
+
if (input && (changed.has('draftText') || changed.has('value'))) {
|
|
128
|
+
const display = this.draftText ?? this.formatDate(this.value);
|
|
129
|
+
if (input.value !== display) input.value = display;
|
|
111
130
|
}
|
|
112
131
|
}
|
|
113
132
|
|
|
133
|
+
private syncValue(value: string | null): void {
|
|
134
|
+
this.value = value ?? '';
|
|
135
|
+
this.lastCommittedValue = this.value;
|
|
136
|
+
this.draftText = null;
|
|
137
|
+
this.selectedDate = parseDateValue(this.value);
|
|
138
|
+
this.viewDate = new Date(this.selectedDate ?? new Date());
|
|
139
|
+
this.selectedHour = this.selectedDate?.getHours() ?? 0;
|
|
140
|
+
this.selectedMinute = this.selectedDate?.getMinutes() ?? 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
114
143
|
public formatDate(isoString: string): string {
|
|
115
144
|
if (!isoString) return '';
|
|
116
145
|
|
|
117
146
|
try {
|
|
118
|
-
const date =
|
|
119
|
-
if (
|
|
147
|
+
const date = parseDateValue(isoString);
|
|
148
|
+
if (!date) return isoString;
|
|
120
149
|
|
|
121
150
|
let formatted = this.dateFormat;
|
|
122
151
|
const day = date.getDate().toString().padStart(2, '0');
|
|
123
152
|
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
|
124
|
-
const year = date.getFullYear().toString();
|
|
153
|
+
const year = date.getFullYear().toString().padStart(4, '0');
|
|
125
154
|
|
|
126
155
|
formatted = formatted.replace('YYYY', year);
|
|
127
156
|
formatted = formatted.replace('YY', year.slice(-2));
|
|
@@ -171,7 +200,7 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
|
|
171
200
|
const opensToTop = spaceBelow < 400 && spaceAbove > spaceBelow;
|
|
172
201
|
|
|
173
202
|
if (!this.popupInstance) {
|
|
174
|
-
this.popupInstance =
|
|
203
|
+
this.popupInstance = document.createElement('dees-input-datepicker-popup');
|
|
175
204
|
}
|
|
176
205
|
|
|
177
206
|
// Configure popup
|
|
@@ -204,31 +233,50 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
|
|
204
233
|
|
|
205
234
|
private closePopup(): void {
|
|
206
235
|
this.isOpened = false;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
this.
|
|
236
|
+
const popup = this.popupInstance;
|
|
237
|
+
this.popupInstance = null;
|
|
238
|
+
if (popup) {
|
|
239
|
+
popup.removeEventListener('date-selected', this.handleDateSelected);
|
|
240
|
+
popup.removeEventListener('date-cleared', this.handleDateCleared);
|
|
241
|
+
popup.removeEventListener('close-request', this.handleCloseRequest);
|
|
242
|
+
popup.removeEventListener('reposition-request', this.handleRepositionRequest);
|
|
243
|
+
void popup.hide();
|
|
213
244
|
}
|
|
214
245
|
}
|
|
215
246
|
|
|
216
247
|
private handleDateSelected = (event: Event): void => {
|
|
217
|
-
|
|
218
|
-
this.
|
|
219
|
-
this.selectedHour = date.getHours();
|
|
220
|
-
this.selectedMinute = date.getMinutes();
|
|
221
|
-
this.viewDate = new Date(date);
|
|
222
|
-
this.value = this.formatValueWithTimezone(date);
|
|
223
|
-
this.changeSubject.next(this);
|
|
248
|
+
if (this.disabled) return;
|
|
249
|
+
this.commitDate((event as CustomEvent<Date>).detail, true);
|
|
224
250
|
};
|
|
225
251
|
|
|
226
252
|
private handleDateCleared = (): void => {
|
|
227
|
-
this.
|
|
228
|
-
this.selectedDate = null;
|
|
229
|
-
this.changeSubject.next(this);
|
|
253
|
+
if (!this.disabled) this.commitClear();
|
|
230
254
|
};
|
|
231
255
|
|
|
256
|
+
private commitDate(date: Date, normalizeDraft: boolean): boolean {
|
|
257
|
+
const message = dateValidationMessage(date, this);
|
|
258
|
+
if (message) { this.updateValidation(message); return false; }
|
|
259
|
+
const value = this.formatValueWithTimezone(date);
|
|
260
|
+
const changed = value !== this.value;
|
|
261
|
+
this.lastCommittedValue = value;
|
|
262
|
+
this.value = value;
|
|
263
|
+
this.selectedDate = new Date(date);
|
|
264
|
+
this.selectedHour = date.getHours();
|
|
265
|
+
this.selectedMinute = date.getMinutes();
|
|
266
|
+
this.viewDate = new Date(date);
|
|
267
|
+
if (normalizeDraft) this.draftText = null;
|
|
268
|
+
this.updateValidation('');
|
|
269
|
+
if (changed) this.changeSubject.next(this);
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private commitClear(): void {
|
|
274
|
+
const changed = Boolean(this.value);
|
|
275
|
+
this.syncValue('');
|
|
276
|
+
this.updateValidation(this.currentValidationMessage());
|
|
277
|
+
if (changed) this.changeSubject.next(this);
|
|
278
|
+
}
|
|
279
|
+
|
|
232
280
|
private handleCloseRequest = (): void => {
|
|
233
281
|
this.closePopup();
|
|
234
282
|
};
|
|
@@ -251,11 +299,17 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
|
|
251
299
|
};
|
|
252
300
|
|
|
253
301
|
public handleKeydown(e: KeyboardEvent): void {
|
|
254
|
-
if (
|
|
302
|
+
if (this.disabled || e.isComposing) return;
|
|
303
|
+
if (e.key === 'Enter') {
|
|
255
304
|
e.preventDefault();
|
|
256
|
-
this.
|
|
305
|
+
if (this.draftText !== null) this.commitManualInput(true);
|
|
306
|
+
else void this.toggleCalendar();
|
|
307
|
+
} else if (e.key === 'ArrowDown' && e.altKey) {
|
|
308
|
+
e.preventDefault();
|
|
309
|
+
if (!this.isOpened) void this.toggleCalendar();
|
|
257
310
|
} else if (e.key === 'Escape' && this.isOpened) {
|
|
258
311
|
e.preventDefault();
|
|
312
|
+
e.stopPropagation();
|
|
259
313
|
this.closePopup();
|
|
260
314
|
}
|
|
261
315
|
}
|
|
@@ -270,107 +324,73 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
|
|
270
324
|
|
|
271
325
|
public clearValue(e: Event): void {
|
|
272
326
|
e.stopPropagation();
|
|
273
|
-
this.
|
|
274
|
-
this.selectedDate = null;
|
|
275
|
-
this.changeSubject.next(this);
|
|
327
|
+
if (!this.disabled) this.commitClear();
|
|
276
328
|
}
|
|
277
329
|
|
|
278
330
|
public handleManualInput(e: InputEvent): void {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if (!inputValue) {
|
|
283
|
-
this.value = '';
|
|
284
|
-
this.selectedDate = null;
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const parsedDate = this.parseManualDate(inputValue);
|
|
289
|
-
if (parsedDate && !isNaN(parsedDate.getTime())) {
|
|
290
|
-
this.value = parsedDate.toISOString();
|
|
291
|
-
this.selectedDate = parsedDate;
|
|
292
|
-
this.viewDate = new Date(parsedDate);
|
|
293
|
-
this.selectedHour = parsedDate.getHours();
|
|
294
|
-
this.selectedMinute = parsedDate.getMinutes();
|
|
295
|
-
this.changeSubject.next(this);
|
|
296
|
-
}
|
|
331
|
+
if (this.disabled) return;
|
|
332
|
+
this.draftText = (e.target as HTMLInputElement).value;
|
|
333
|
+
if (!e.isComposing) this.updateValidation(this.currentValidationMessage());
|
|
297
334
|
}
|
|
298
335
|
|
|
299
|
-
public handleInputBlur(
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if (!inputValue) {
|
|
304
|
-
this.value = '';
|
|
305
|
-
this.selectedDate = null;
|
|
306
|
-
this.changeSubject.next(this);
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
336
|
+
public handleInputBlur(_event: FocusEvent): void {
|
|
337
|
+
if (!this.disabled && this.draftText !== null) this.commitManualInput(true);
|
|
338
|
+
}
|
|
309
339
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
this.changeSubject.next(this);
|
|
318
|
-
input.value = this.formatDate(this.value);
|
|
319
|
-
} else {
|
|
320
|
-
input.value = this.formatDate(this.value);
|
|
321
|
-
}
|
|
340
|
+
private commitManualInput(normalizeDraft: boolean): void {
|
|
341
|
+
const text = this.draftText?.trim() ?? '';
|
|
342
|
+
if (!text) { this.commitClear(); return; }
|
|
343
|
+
const date = this.parseManualDate(text);
|
|
344
|
+
const message = dateValidationMessage(date, this);
|
|
345
|
+
if (message) { this.updateValidation(message); return; }
|
|
346
|
+
this.commitDate(date!, normalizeDraft);
|
|
322
347
|
}
|
|
323
348
|
|
|
324
349
|
private parseManualDate(input: string): Date | null {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
let
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const [_, year, month, day] = isoMatch;
|
|
335
|
-
parsedDate = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
if (!parsedDate) {
|
|
339
|
-
const euMatch = datePart.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/);
|
|
340
|
-
if (euMatch) {
|
|
341
|
-
const [_, day, month, year] = euMatch;
|
|
342
|
-
parsedDate = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
|
|
343
|
-
}
|
|
350
|
+
const match = /^(\S+)(?:\s+(\d{1,2}):(\d{2})(?:\s+(AM|PM))?)?$/i.exec(input.trim());
|
|
351
|
+
if (!match) return null;
|
|
352
|
+
const fields = parseDateFields(match[1]);
|
|
353
|
+
if (!fields) return null;
|
|
354
|
+
let hour = Number(match[2] ?? 0);
|
|
355
|
+
const minute = Number(match[3] ?? 0);
|
|
356
|
+
if (match[4]) {
|
|
357
|
+
if (hour < 1 || hour > 12) return null;
|
|
358
|
+
hour = hour % 12 + (match[4].toUpperCase() === 'PM' ? 12 : 0);
|
|
344
359
|
}
|
|
360
|
+
return createCalendarDate(...fields, match[2] === undefined ? undefined : hour, minute);
|
|
361
|
+
}
|
|
345
362
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
363
|
+
private currentValidationMessage(): string {
|
|
364
|
+
if (this.disabled) return '';
|
|
365
|
+
const text = this.draftText?.trim();
|
|
366
|
+
if (this.draftText !== null) {
|
|
367
|
+
if (!text) return this.required ? 'Choose a date.' : '';
|
|
368
|
+
return dateValidationMessage(this.parseManualDate(text), this);
|
|
352
369
|
}
|
|
370
|
+
if (!this.value) return this.required ? 'Choose a date.' : '';
|
|
371
|
+
return dateValidationMessage(parseDateValue(this.value), this);
|
|
372
|
+
}
|
|
353
373
|
|
|
354
|
-
|
|
374
|
+
private updateValidation(message: string): void {
|
|
375
|
+
const changed = message !== this.validationText;
|
|
376
|
+
this.validationText = message;
|
|
377
|
+
this.shadowRoot?.querySelector('input')?.setCustomValidity(message);
|
|
378
|
+
if (changed) this.dispatchEvent(new CustomEvent('validation-change', {
|
|
379
|
+
detail: { valid: !message, message }, bubbles: true, composed: true,
|
|
380
|
+
}));
|
|
381
|
+
}
|
|
355
382
|
|
|
356
|
-
|
|
357
|
-
const timeMatch = timePart.match(/^(\d{1,2}):(\d{2})$/);
|
|
358
|
-
if (timeMatch) {
|
|
359
|
-
const [_, hours, minutes] = timeMatch;
|
|
360
|
-
parsedDate.setHours(parseInt(hours));
|
|
361
|
-
parsedDate.setMinutes(parseInt(minutes));
|
|
362
|
-
}
|
|
363
|
-
} else if (!this.enableTime) {
|
|
364
|
-
const now = new Date();
|
|
365
|
-
parsedDate.setHours(now.getHours());
|
|
366
|
-
parsedDate.setMinutes(now.getMinutes());
|
|
367
|
-
parsedDate.setSeconds(0);
|
|
368
|
-
parsedDate.setMilliseconds(0);
|
|
369
|
-
}
|
|
383
|
+
public checkValidity(): boolean { return !this.currentValidationMessage(); }
|
|
370
384
|
|
|
371
|
-
|
|
385
|
+
public reportValidity(): boolean {
|
|
386
|
+
const message = this.currentValidationMessage();
|
|
387
|
+
this.updateValidation(message);
|
|
388
|
+
if (message) this.focus();
|
|
389
|
+
return !message;
|
|
372
390
|
}
|
|
373
391
|
|
|
392
|
+
public reset(): void { this.setValue(''); }
|
|
393
|
+
|
|
374
394
|
private formatValueWithTimezone(date: Date): string {
|
|
375
395
|
if (!this.enableTimezone) return date.toISOString();
|
|
376
396
|
|
|
@@ -403,32 +423,13 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
|
|
403
423
|
return this.value;
|
|
404
424
|
}
|
|
405
425
|
|
|
406
|
-
public setValue(value: string): void {
|
|
407
|
-
this.value
|
|
408
|
-
|
|
409
|
-
try {
|
|
410
|
-
const date = new Date(value);
|
|
411
|
-
if (!isNaN(date.getTime())) {
|
|
412
|
-
this.selectedDate = date;
|
|
413
|
-
this.viewDate = new Date(date);
|
|
414
|
-
this.selectedHour = date.getHours();
|
|
415
|
-
this.selectedMinute = date.getMinutes();
|
|
416
|
-
}
|
|
417
|
-
} catch {
|
|
418
|
-
// Invalid date
|
|
419
|
-
}
|
|
420
|
-
}
|
|
426
|
+
public setValue(value: string | null): void {
|
|
427
|
+
this.syncValue(value);
|
|
428
|
+
this.updateValidation(this.currentValidationMessage());
|
|
421
429
|
}
|
|
422
430
|
|
|
423
431
|
async disconnectedCallback() {
|
|
432
|
+
this.closePopup();
|
|
424
433
|
await super.disconnectedCallback();
|
|
425
|
-
if (this.popupInstance) {
|
|
426
|
-
this.popupInstance.removeEventListener('date-selected', this.handleDateSelected);
|
|
427
|
-
this.popupInstance.removeEventListener('date-cleared', this.handleDateCleared);
|
|
428
|
-
this.popupInstance.removeEventListener('close-request', this.handleCloseRequest);
|
|
429
|
-
this.popupInstance.removeEventListener('reposition-request', this.handleRepositionRequest);
|
|
430
|
-
this.popupInstance.hide();
|
|
431
|
-
this.popupInstance = null;
|
|
432
|
-
}
|
|
433
434
|
}
|
|
434
435
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export interface IDateConstraints {
|
|
2
|
+
minDate: string;
|
|
3
|
+
maxDate: string;
|
|
4
|
+
disabledDates: string[];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Construct a local calendar date without accepting Date's overflow normalization. */
|
|
8
|
+
export const createCalendarDate = (year: number, month: number, day: number, hour?: number, minute = 0): Date | null => {
|
|
9
|
+
const requestedHour = hour ?? 0;
|
|
10
|
+
if (![year, month, day, requestedHour, minute].every(Number.isInteger)
|
|
11
|
+
|| year < 1 || year > 9999 || month < 1 || month > 12 || day < 1 || day > 31
|
|
12
|
+
|| requestedHour < 0 || requestedHour > 23 || minute < 0 || minute > 59) return null;
|
|
13
|
+
const date = new Date(0);
|
|
14
|
+
// setFullYear avoids the constructor's special mapping of years 0–99 to 1900–1999.
|
|
15
|
+
date.setFullYear(year, month - 1, day);
|
|
16
|
+
date.setHours(requestedHour, minute, 0, 0);
|
|
17
|
+
// Some zones enter daylight saving at midnight. A date-only entry still names
|
|
18
|
+
// that valid calendar day; an explicitly typed nonexistent time is rejected.
|
|
19
|
+
return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day
|
|
20
|
+
&& (hour === undefined || date.getHours() === hour && date.getMinutes() === minute) ? date : null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const parseDateFields = (text: string): [number, number, number] | null => {
|
|
24
|
+
let match = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(text);
|
|
25
|
+
if (match) return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
26
|
+
match = /^(\d{1,2})\.(\d{1,2})\.(\d{4})$/.exec(text);
|
|
27
|
+
if (match) return [Number(match[3]), Number(match[2]), Number(match[1])];
|
|
28
|
+
match = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/.exec(text);
|
|
29
|
+
return match ? [Number(match[3]), Number(match[1]), Number(match[2])] : null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Date-only constraints are local calendar days, never UTC-midnight instants. */
|
|
33
|
+
export const parseDateValue = (value: string): Date | null => {
|
|
34
|
+
if (!value) return null;
|
|
35
|
+
const fields = parseDateFields(value);
|
|
36
|
+
if (fields) return createCalendarDate(...fields);
|
|
37
|
+
const iso = /^(\d{4})-(\d{2})-(\d{2})T/.exec(value);
|
|
38
|
+
if (!iso || !createCalendarDate(Number(iso[1]), Number(iso[2]), Number(iso[3]))) return null;
|
|
39
|
+
const date = new Date(value);
|
|
40
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const calendarDayKey = (date: Date): number =>
|
|
44
|
+
date.getFullYear() * 10000 + (date.getMonth() + 1) * 100 + date.getDate();
|
|
45
|
+
|
|
46
|
+
/** Shared by manual entry, calendar buttons and the final commit boundary. */
|
|
47
|
+
export const dateValidationMessage = (date: Date | null, constraints: IDateConstraints): string => {
|
|
48
|
+
if (!date || Number.isNaN(date.getTime()) || date.getFullYear() < 1 || date.getFullYear() > 9999) return 'Enter a valid date (YYYY-MM-DD, DD.MM.YYYY or MM/DD/YYYY).';
|
|
49
|
+
const key = calendarDayKey(date);
|
|
50
|
+
for (const bound of ['minDate', 'maxDate'] as const) {
|
|
51
|
+
if (!constraints[bound]) continue;
|
|
52
|
+
const limit = parseDateValue(constraints[bound]);
|
|
53
|
+
if (!limit) return `${bound === 'minDate' ? 'Minimum' : 'Maximum'} date is invalid.`;
|
|
54
|
+
if (bound === 'minDate' ? key < calendarDayKey(limit) : key > calendarDayKey(limit)) {
|
|
55
|
+
return `Choose a date on or ${bound === 'minDate' ? 'after' : 'before'} ${constraints[bound]}.`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (const value of constraints.disabledDates ?? []) {
|
|
59
|
+
const disabled = parseDateValue(value);
|
|
60
|
+
if (!disabled) return 'A disabled date is invalid.';
|
|
61
|
+
if (key === calendarDayKey(disabled)) return 'This date is unavailable.';
|
|
62
|
+
}
|
|
63
|
+
return '';
|
|
64
|
+
};
|
|
@@ -14,6 +14,7 @@ import { themeDefaultStyles } from '../../00theme.js';
|
|
|
14
14
|
import { DeesWindowLayer } from '../../00group-overlay/dees-windowlayer/dees-windowlayer.js';
|
|
15
15
|
import '../../00group-utility/dees-icon/dees-icon.js';
|
|
16
16
|
import type { IDateEvent } from './types.js';
|
|
17
|
+
import { createCalendarDate, dateValidationMessage } from './date-validation.js';
|
|
17
18
|
|
|
18
19
|
declare global {
|
|
19
20
|
interface HTMLElementTagNameMap {
|
|
@@ -534,29 +535,32 @@ export class DeesInputDatepickerPopup extends DeesElement {
|
|
|
534
535
|
const totalEventCount = dayEvents.reduce((sum, event) => sum + (event.count || 1), 0);
|
|
535
536
|
|
|
536
537
|
return html`
|
|
537
|
-
<
|
|
538
|
+
<button type="button"
|
|
538
539
|
class="day ${isOtherMonth ? 'other-month' : ''} ${isToday ? 'today' : ''} ${isSelected ? 'selected' : ''} ${isDayDisabled ? 'disabled' : ''} ${hasEvents ? 'has-event' : ''}"
|
|
540
|
+
?disabled=${isDayDisabled}
|
|
541
|
+
aria-label=${`${String(day.getFullYear()).padStart(4, '0')}-${String(day.getMonth() + 1).padStart(2, '0')}-${String(day.getDate()).padStart(2, '0')}`}
|
|
542
|
+
aria-pressed=${isSelected ? 'true' : 'false'}
|
|
539
543
|
@click=${() => !isDayDisabled && this.handleSelectDate(day)}
|
|
540
544
|
>
|
|
541
545
|
${day.getDate()}
|
|
542
546
|
${hasEvents ? html`
|
|
543
547
|
${totalEventCount > 3 ? html`
|
|
544
|
-
<
|
|
548
|
+
<span class="event-count">${totalEventCount}</span>
|
|
545
549
|
` : html`
|
|
546
|
-
<
|
|
550
|
+
<span class="event-indicator">
|
|
547
551
|
${dayEvents.slice(0, 3).map(event => html`
|
|
548
|
-
<
|
|
552
|
+
<span class="event-dot ${event.type || 'info'}"></span>
|
|
549
553
|
`)}
|
|
550
|
-
</
|
|
554
|
+
</span>
|
|
551
555
|
`}
|
|
552
556
|
${dayEvents[0].title ? html`
|
|
553
|
-
<
|
|
557
|
+
<span class="event-tooltip">
|
|
554
558
|
${dayEvents[0].title}
|
|
555
559
|
${totalEventCount > 1 ? html` (+${totalEventCount - 1} more)` : ''}
|
|
556
|
-
</
|
|
560
|
+
</span>
|
|
557
561
|
` : ''}
|
|
558
562
|
` : ''}
|
|
559
|
-
</
|
|
563
|
+
</button>
|
|
560
564
|
`;
|
|
561
565
|
})}
|
|
562
566
|
</div>
|
|
@@ -601,8 +605,8 @@ export class DeesInputDatepickerPopup extends DeesElement {
|
|
|
601
605
|
` : ''}
|
|
602
606
|
|
|
603
607
|
<div class="calendar-actions">
|
|
604
|
-
<button class="action-button today-button" @click=${this.handleSelectToday}>Today</button>
|
|
605
|
-
<button class="action-button clear-action-button" @click=${this.handleClear}>Clear</button>
|
|
608
|
+
<button type="button" class="action-button today-button" ?disabled=${this.isDayDisabled(new Date())} @click=${this.handleSelectToday}>Today</button>
|
|
609
|
+
<button type="button" class="action-button clear-action-button" @click=${this.handleClear}>Clear</button>
|
|
606
610
|
</div>
|
|
607
611
|
</div>
|
|
608
612
|
`;
|
|
@@ -624,22 +628,19 @@ export class DeesInputDatepickerPopup extends DeesElement {
|
|
|
624
628
|
|
|
625
629
|
// Calendar logic
|
|
626
630
|
private getDaysInMonth(): Date[] {
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
const lastDay = new Date(year, month + 1, 0);
|
|
631
|
-
const days: Date[] = [];
|
|
631
|
+
const firstDay = new Date(this.viewDate);
|
|
632
|
+
firstDay.setDate(1);
|
|
633
|
+
firstDay.setHours(12, 0, 0, 0);
|
|
632
634
|
|
|
633
635
|
const startOffset = this.weekStartsOn === 1
|
|
634
636
|
? (firstDay.getDay() === 0 ? 6 : firstDay.getDay() - 1)
|
|
635
637
|
: firstDay.getDay();
|
|
636
638
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
return days;
|
|
639
|
+
return Array.from({ length: 42 }, (_, index) => {
|
|
640
|
+
const day = new Date(firstDay);
|
|
641
|
+
day.setDate(1 - startOffset + index);
|
|
642
|
+
return day;
|
|
643
|
+
});
|
|
643
644
|
}
|
|
644
645
|
|
|
645
646
|
private isToday(date: Date): boolean {
|
|
@@ -653,15 +654,7 @@ export class DeesInputDatepickerPopup extends DeesElement {
|
|
|
653
654
|
}
|
|
654
655
|
|
|
655
656
|
private isDayDisabled(date: Date): boolean {
|
|
656
|
-
|
|
657
|
-
if (this.maxDate) { const max = new Date(this.maxDate); if (date > max) return true; }
|
|
658
|
-
if (this.disabledDates?.length) {
|
|
659
|
-
return this.disabledDates.some(ds => {
|
|
660
|
-
try { const d = new Date(ds); return date.getDate() === d.getDate() && date.getMonth() === d.getMonth() && date.getFullYear() === d.getFullYear(); }
|
|
661
|
-
catch { return false; }
|
|
662
|
-
});
|
|
663
|
-
}
|
|
664
|
-
return false;
|
|
657
|
+
return Boolean(dateValidationMessage(date, this));
|
|
665
658
|
}
|
|
666
659
|
|
|
667
660
|
private getEventsForDate(date: Date): IDateEvent[] {
|
|
@@ -671,16 +664,24 @@ export class DeesInputDatepickerPopup extends DeesElement {
|
|
|
671
664
|
}
|
|
672
665
|
|
|
673
666
|
private previousMonth(): void {
|
|
674
|
-
|
|
667
|
+
const date = new Date(this.viewDate);
|
|
668
|
+
date.setMonth(date.getMonth() - 1, 1);
|
|
669
|
+
this.viewDate = date;
|
|
675
670
|
}
|
|
676
671
|
|
|
677
672
|
private nextMonth(): void {
|
|
678
|
-
|
|
673
|
+
const date = new Date(this.viewDate);
|
|
674
|
+
date.setMonth(date.getMonth() + 1, 1);
|
|
675
|
+
this.viewDate = date;
|
|
679
676
|
}
|
|
680
677
|
|
|
681
678
|
// Event dispatching
|
|
682
679
|
private handleSelectDate(day: Date): void {
|
|
683
|
-
|
|
680
|
+
if (this.isDayDisabled(day)) return;
|
|
681
|
+
const selected = createCalendarDate(day.getFullYear(), day.getMonth() + 1, day.getDate(),
|
|
682
|
+
this.enableTime ? this.selectedHour : undefined, this.enableTime ? this.selectedMinute : 0);
|
|
683
|
+
if (!selected || this.isDayDisabled(selected)) return;
|
|
684
|
+
this.selectedDate = selected;
|
|
684
685
|
this.dispatchEvent(new CustomEvent('date-selected', { detail: this.selectedDate }));
|
|
685
686
|
if (!this.enableTime) {
|
|
686
687
|
this.dispatchEvent(new CustomEvent('close-request'));
|
|
@@ -689,14 +690,11 @@ export class DeesInputDatepickerPopup extends DeesElement {
|
|
|
689
690
|
|
|
690
691
|
private handleSelectToday(): void {
|
|
691
692
|
const today = new Date();
|
|
692
|
-
this.
|
|
693
|
+
if (this.isDayDisabled(today)) return;
|
|
693
694
|
this.viewDate = new Date(today);
|
|
694
695
|
this.selectedHour = today.getHours();
|
|
695
696
|
this.selectedMinute = today.getMinutes();
|
|
696
|
-
this.
|
|
697
|
-
if (!this.enableTime) {
|
|
698
|
-
this.dispatchEvent(new CustomEvent('close-request'));
|
|
699
|
-
}
|
|
697
|
+
this.handleSelectDate(today);
|
|
700
698
|
}
|
|
701
699
|
|
|
702
700
|
private handleClear(): void {
|
|
@@ -722,7 +720,7 @@ export class DeesInputDatepickerPopup extends DeesElement {
|
|
|
722
720
|
const input = e.target as HTMLInputElement;
|
|
723
721
|
let value = parseInt(input.value) || 0;
|
|
724
722
|
value = Math.max(0, Math.min(59, value));
|
|
725
|
-
if (this.minuteIncrement > 1) value = Math.round(value / this.minuteIncrement) * this.minuteIncrement;
|
|
723
|
+
if (this.minuteIncrement > 1) value = Math.min(59, Math.round(value / this.minuteIncrement) * this.minuteIncrement);
|
|
726
724
|
this.selectedMinute = value;
|
|
727
725
|
this.emitTimeUpdate();
|
|
728
726
|
};
|
|
@@ -740,7 +738,9 @@ export class DeesInputDatepickerPopup extends DeesElement {
|
|
|
740
738
|
|
|
741
739
|
private emitTimeUpdate(): void {
|
|
742
740
|
if (this.selectedDate) {
|
|
743
|
-
|
|
741
|
+
const selected = createCalendarDate(this.selectedDate.getFullYear(), this.selectedDate.getMonth() + 1, this.selectedDate.getDate(), this.selectedHour, this.selectedMinute);
|
|
742
|
+
if (!selected || this.isDayDisabled(selected)) return;
|
|
743
|
+
this.selectedDate = selected;
|
|
744
744
|
this.dispatchEvent(new CustomEvent('date-selected', { detail: this.selectedDate }));
|
|
745
745
|
}
|
|
746
746
|
}
|
|
@@ -38,6 +38,11 @@ export const datepickerStyles = [
|
|
|
38
38
|
color: var(--dees-color-text-muted);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
.date-input[aria-invalid="true"] { border-color: var(--dees-color-accent-error); }
|
|
42
|
+
.field-messages { grid-column: 1 / -1; }
|
|
43
|
+
.validationText { color: var(--dees-color-text-error); font-size: var(--dees-font-footnote-size); }
|
|
44
|
+
.validationText:not(:empty) { margin-top: var(--dees-spacing-xs); }
|
|
45
|
+
|
|
41
46
|
.date-input:hover:not(:disabled) {
|
|
42
47
|
border-color: var(--dees-color-border-strong);
|
|
43
48
|
}
|