@design.estate/dees-catalog 9.6.2 → 9.8.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 +3138 -2725
- 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-appui/dees-appui/dees-appui.js +6 -1
- 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 +64 -16
- package/dist_ts_web/elements/00group-harness/dees-harness-message-list/dees-harness-message-list.d.ts +4 -0
- package/dist_ts_web/elements/00group-harness/dees-harness-message-list/dees-harness-message-list.js +52 -1
- package/dist_ts_web/elements/00group-harness/dees-harness-tool-card/dees-harness-tool-card.d.ts +4 -1
- package/dist_ts_web/elements/00group-harness/dees-harness-tool-card/dees-harness-tool-card.js +93 -20
- package/dist_ts_web/elements/00group-harness/dees-harness-tool-card/renderers.js +7 -21
- package/dist_ts_web/elements/00group-harness/harness.helpers.d.ts +20 -0
- package/dist_ts_web/elements/00group-harness/harness.helpers.js +58 -1
- package/dist_ts_web/elements/00group-harness/interfaces.d.ts +6 -0
- package/dist_ts_web/elements/00group-harness/toolregistry.d.ts +8 -0
- package/dist_ts_web/elements/00group-harness/toolregistry.js +10 -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/package.json +2 -2
- package/readme.md +61 -6
- package/scripts/check-packed-consumer.mjs +8 -5
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/demos/inputs.ts +5 -4
- package/ts_web/elements/00group-appui/dees-appui/dees-appui.ts +4 -0
- 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 +49 -14
- package/ts_web/elements/00group-harness/dees-harness-message-list/dees-harness-message-list.ts +50 -0
- package/ts_web/elements/00group-harness/dees-harness-tool-card/dees-harness-tool-card.ts +95 -23
- package/ts_web/elements/00group-harness/dees-harness-tool-card/renderers.ts +7 -20
- package/ts_web/elements/00group-harness/harness.helpers.ts +63 -0
- package/ts_web/elements/00group-harness/interfaces.ts +6 -0
- package/ts_web/elements/00group-harness/toolregistry.ts +22 -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
|
@@ -313,3 +313,66 @@ export const harnessSummarizeValue = (value: unknown): string => {
|
|
|
313
313
|
export const harnessSummaryLine = (value: unknown): string => {
|
|
314
314
|
return harnessSummarizeValue(value).split('\n')[0] || '';
|
|
315
315
|
};
|
|
316
|
+
|
|
317
|
+
// eslint-disable-next-line no-control-regex
|
|
318
|
+
const ansiEscapePattern = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07]*(?:\x07|\x1b\\)|\x1b[=>]/g;
|
|
319
|
+
|
|
320
|
+
/** Terminal output carries raw escape sequences; they must never reach a header or a summary. */
|
|
321
|
+
export const harnessStripAnsi = (text: string): string => text.replace(ansiEscapePattern, '');
|
|
322
|
+
|
|
323
|
+
export interface IHarnessTerminalOutcome {
|
|
324
|
+
stdout: string;
|
|
325
|
+
stderr: string;
|
|
326
|
+
/** Top-level exitCode wins over a nested one, matching how producers report it. */
|
|
327
|
+
exitCode?: number;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Normalizes the three shapes producers use for shell output: a `{stdout, stderr, exitCode}`
|
|
332
|
+
* record, a combined string with a separate top-level `exitCode`, or an arbitrary value.
|
|
333
|
+
*/
|
|
334
|
+
export const harnessTerminalOutcome = (callArg: IHarnessToolCall): IHarnessTerminalOutcome => {
|
|
335
|
+
const output = callArg.output;
|
|
336
|
+
let stdout = '';
|
|
337
|
+
let stderr = '';
|
|
338
|
+
let nestedExitCode: number | undefined;
|
|
339
|
+
if (harnessIsRecord(output)) {
|
|
340
|
+
stdout = typeof output.stdout === 'string' ? output.stdout : harnessStringifyValue(output.stdout ?? '');
|
|
341
|
+
stderr = typeof output.stderr === 'string' ? output.stderr : '';
|
|
342
|
+
nestedExitCode = typeof output.exitCode === 'number' ? output.exitCode : undefined;
|
|
343
|
+
} else {
|
|
344
|
+
stdout = harnessStringifyValue(output);
|
|
345
|
+
}
|
|
346
|
+
const exitCode = typeof callArg.exitCode === 'number' ? callArg.exitCode : nestedExitCode;
|
|
347
|
+
return { stdout, stderr, ...(exitCode === undefined ? {} : { exitCode }) };
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const harnessLastMeaningfulLine = (text: string): string => {
|
|
351
|
+
const lines = harnessStripAnsi(text).split('\n');
|
|
352
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
353
|
+
const line = lines[index].trim();
|
|
354
|
+
if (line) return line;
|
|
355
|
+
}
|
|
356
|
+
return '';
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* One-line outcome for a finished shell call, for the collapsed card header. The command itself
|
|
361
|
+
* is already the card subtitle, so this describes the RESULT: exit code, how much came back, and
|
|
362
|
+
* the last meaningful line. Harness support for a richer summary is uneven — only OpenCode
|
|
363
|
+
* supplies a model-authored description, so this is derived from output that every harness has.
|
|
364
|
+
*/
|
|
365
|
+
export const harnessTerminalResultSummary = (callArg: IHarnessToolCall): string => {
|
|
366
|
+
const { stdout, stderr, exitCode } = harnessTerminalOutcome(callArg);
|
|
367
|
+
const parts: string[] = [];
|
|
368
|
+
if (exitCode !== undefined) parts.push(exitCode === 0 ? 'exit 0' : `exit ${exitCode}`);
|
|
369
|
+
const combined = `${stdout}${stdout && stderr ? '\n' : ''}${stderr}`;
|
|
370
|
+
const lineCount = combined ? harnessStripAnsi(combined).split('\n').filter((line) => line.trim()).length : 0;
|
|
371
|
+
if (lineCount > 0) parts.push(`${lineCount} ${lineCount === 1 ? 'line' : 'lines'}`);
|
|
372
|
+
else if (exitCode !== undefined) parts.push('no output');
|
|
373
|
+
const lastLine = harnessLastMeaningfulLine(stderr) || harnessLastMeaningfulLine(stdout);
|
|
374
|
+
const prefix = parts.join(' · ');
|
|
375
|
+
if (!lastLine) return prefix;
|
|
376
|
+
const trimmed = lastLine.length > 160 ? `${lastLine.slice(0, 160)}…` : lastLine;
|
|
377
|
+
return prefix ? `${prefix} · ${trimmed}` : trimmed;
|
|
378
|
+
};
|
|
@@ -230,6 +230,12 @@ export interface IHarnessToolCall {
|
|
|
230
230
|
subtask?: IHarnessSubtaskStream;
|
|
231
231
|
/** model the spawned session runs on, e.g. "anthropic/claude-opus-5" */
|
|
232
232
|
model?: string;
|
|
233
|
+
/**
|
|
234
|
+
* Producer-supplied one-line description of the call, e.g. OpenCode's tool title or a parsed
|
|
235
|
+
* shell action. Single line, already bounded by the producer. Preferred over any line the card
|
|
236
|
+
* derives for itself; absent whenever the producer offers nothing.
|
|
237
|
+
*/
|
|
238
|
+
title?: string;
|
|
233
239
|
/** epoch ms */
|
|
234
240
|
startedAt?: number;
|
|
235
241
|
finishedAt?: number;
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { TemplateResult } from '@design.estate/dees-element';
|
|
2
2
|
import type { TDiffViewRequest } from '../00group-dataview/dees-dataview-codebox/dees-dataview-codebox.js';
|
|
3
3
|
import type { IHarnessToolCall, THarnessContentBlock } from './interfaces.js';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
harnessIsRecord,
|
|
6
|
+
harnessSummaryLine,
|
|
7
|
+
harnessTerminalOutcome,
|
|
8
|
+
harnessTerminalResultSummary,
|
|
9
|
+
} from './harness.helpers.js';
|
|
5
10
|
|
|
6
11
|
/**
|
|
7
12
|
* Render helpers handed to tool body renderers. Implemented by
|
|
@@ -49,6 +54,14 @@ export interface IHarnessToolDescriptor {
|
|
|
49
54
|
*/
|
|
50
55
|
renderBody?: (call: IHarnessToolCall, helpers: IHarnessToolRenderHelpers) => TemplateResult;
|
|
51
56
|
defaultExpanded?: boolean | ((call: IHarnessToolCall) => boolean);
|
|
57
|
+
/**
|
|
58
|
+
* Collapses a card that opened while it was live once its work is done, so a finished run
|
|
59
|
+
* stops occupying the transcript. Evaluated only for a settled call, and always subordinate to
|
|
60
|
+
* an explicit user toggle.
|
|
61
|
+
*/
|
|
62
|
+
collapseWhenFinished?: boolean | ((call: IHarnessToolCall) => boolean);
|
|
63
|
+
/** One-line outcome shown in the header while the card is collapsed. */
|
|
64
|
+
summary?: (call: IHarnessToolCall) => string;
|
|
52
65
|
/** Transcript grid packing; omitted descriptors use the compact layout. */
|
|
53
66
|
layout?: THarnessToolLayout;
|
|
54
67
|
/** styles the card with a destructive accent regardless of status */
|
|
@@ -101,6 +114,14 @@ const defaultDescriptors: IHarnessToolDescriptor[] = [
|
|
|
101
114
|
},
|
|
102
115
|
defaultExpanded: true,
|
|
103
116
|
layout: 'full-row',
|
|
117
|
+
/*
|
|
118
|
+
* Only a command that can be PROVEN to have succeeded collapses. An absent exit code is not
|
|
119
|
+
* evidence of success: a producer that reports a failed command as completed and encodes the
|
|
120
|
+
* failure in its output would otherwise have that failure hidden behind a one-line summary.
|
|
121
|
+
* Such a card stays open, and the producer reporting an exit code is the real fix.
|
|
122
|
+
*/
|
|
123
|
+
collapseWhenFinished: (call) => harnessTerminalOutcome(call).exitCode === 0,
|
|
124
|
+
summary: harnessTerminalResultSummary,
|
|
104
125
|
},
|
|
105
126
|
{
|
|
106
127
|
kind: 'file-read',
|
|
@@ -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
|
+
};
|