@design.estate/dees-catalog 9.6.2 → 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 +2945 -2681
- 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-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-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 +1 -1
- package/readme.md +59 -4
- 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-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-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
|
@@ -203,6 +203,9 @@ export class DeesHarnessChat extends DeesElement {
|
|
|
203
203
|
@property({ attribute: false })
|
|
204
204
|
accessor attachments: IHarnessAttachment[] = [];
|
|
205
205
|
|
|
206
|
+
@property({ attribute: false })
|
|
207
|
+
accessor attachmentsEnabled: boolean = true;
|
|
208
|
+
|
|
206
209
|
@property({ attribute: false })
|
|
207
210
|
accessor suggestions: IHarnessComposerSuggestion[] = [];
|
|
208
211
|
|
|
@@ -763,6 +766,7 @@ export class DeesHarnessChat extends DeesElement {
|
|
|
763
766
|
.contextDescription=${this.heading ? `Replying to ${this.heading}` : ''}
|
|
764
767
|
.value=${this.composerValue}
|
|
765
768
|
.attachments=${this.attachments}
|
|
769
|
+
.attachmentsEnabled=${this.attachmentsEnabled}
|
|
766
770
|
.suggestions=${this.suggestions}
|
|
767
771
|
.maxAttachmentCount=${this.maxAttachmentCount}
|
|
768
772
|
.maxAttachmentBytes=${this.maxAttachmentBytes}
|
|
@@ -44,6 +44,9 @@ export const demoFunc = () => demoPage('Composer', 'Write, attach files, choose
|
|
|
44
44
|
<dees-harness-composer busy queuedCount="2"></dees-harness-composer>
|
|
45
45
|
<div class="dees-demo-label">Waiting for a connection</div>
|
|
46
46
|
<dees-harness-composer disabled placeholder="Connect to a project to chat."></dees-harness-composer>
|
|
47
|
+
<div class="dees-demo-label">Text-only conversation</div>
|
|
48
|
+
<dees-harness-composer .attachmentsEnabled=${false} .showModelControls=${false}
|
|
49
|
+
value="Summarize the outstanding invoices."></dees-harness-composer>
|
|
47
50
|
<div class="dees-demo-label">Files ready to send</div>
|
|
48
51
|
<dees-harness-composer
|
|
49
52
|
.showModelControls=${false}
|
|
@@ -112,6 +112,10 @@ export class DeesHarnessComposer extends DeesElement {
|
|
|
112
112
|
@property({ attribute: false })
|
|
113
113
|
accessor attachments: IHarnessAttachment[] = [];
|
|
114
114
|
|
|
115
|
+
/** Host capability: false disables every attachment entry point and sends text only. */
|
|
116
|
+
@property({ attribute: false })
|
|
117
|
+
accessor attachmentsEnabled: boolean = true;
|
|
118
|
+
|
|
115
119
|
@property({ attribute: false })
|
|
116
120
|
accessor suggestions: IHarnessComposerSuggestion[] = [];
|
|
117
121
|
|
|
@@ -473,6 +477,10 @@ export class DeesHarnessComposer extends DeesElement {
|
|
|
473
477
|
|
|
474
478
|
public updated(changedProperties: Map<PropertyKey, unknown>): void {
|
|
475
479
|
super.updated(changedProperties);
|
|
480
|
+
if (['attachmentsEnabled', 'disabled'].some(key => changedProperties.has(key) && changedProperties.get(key) !== undefined)) {
|
|
481
|
+
this.attachmentGeneration++;
|
|
482
|
+
if (!this.attachmentsEnabled || this.disabled) this.handleDragLeave();
|
|
483
|
+
}
|
|
476
484
|
this.syncTextareaSuggestionAria();
|
|
477
485
|
}
|
|
478
486
|
|
|
@@ -486,13 +494,13 @@ export class DeesHarnessComposer extends DeesElement {
|
|
|
486
494
|
@dragleave=${this.handleDragLeave}
|
|
487
495
|
@drop=${this.handleDrop}
|
|
488
496
|
>
|
|
489
|
-
|
|
497
|
+
${this.attachmentsEnabled ? html`<input
|
|
490
498
|
class="fileInput"
|
|
491
499
|
type="file"
|
|
492
500
|
multiple
|
|
493
501
|
@change=${this.handleFileInputChange}
|
|
494
|
-
|
|
495
|
-
${this.attachments.length
|
|
502
|
+
/>` : ''}
|
|
503
|
+
${this.attachmentsEnabled && this.attachments.length
|
|
496
504
|
? html`
|
|
497
505
|
<div class="attachmentRow">
|
|
498
506
|
${this.attachments.map((attachment) => this.renderChip(attachment))}
|
|
@@ -505,7 +513,7 @@ export class DeesHarnessComposer extends DeesElement {
|
|
|
505
513
|
.value=${this.value}
|
|
506
514
|
aria-label="Message"
|
|
507
515
|
aria-description=${this.contextDescription}
|
|
508
|
-
placeholder=${this.placeholder}
|
|
516
|
+
placeholder=${!this.attachmentsEnabled && this.placeholder === 'Message the agent — drop or paste files to attach…' ? 'Message the agent…' : this.placeholder}
|
|
509
517
|
?disabled=${this.disabled}
|
|
510
518
|
rows="1"
|
|
511
519
|
@input=${this.handleTextInput}
|
|
@@ -518,14 +526,16 @@ export class DeesHarnessComposer extends DeesElement {
|
|
|
518
526
|
></textarea>
|
|
519
527
|
</div>
|
|
520
528
|
<div class="controlsRow">
|
|
521
|
-
|
|
529
|
+
${this.attachmentsEnabled ? html`<dees-button
|
|
530
|
+
class="attachButton"
|
|
522
531
|
size="sm"
|
|
523
532
|
type="ghost"
|
|
533
|
+
title="Attach files"
|
|
524
534
|
?disabled=${this.disabled}
|
|
525
535
|
@clicked=${() => this.shadowRoot?.querySelector<HTMLInputElement>('.fileInput')?.click()}
|
|
526
536
|
>
|
|
527
537
|
<dees-icon icon="lucide:Paperclip" iconSize="14"></dees-icon>
|
|
528
|
-
</dees-button
|
|
538
|
+
</dees-button>` : ''}
|
|
529
539
|
${this.modeOptions.length
|
|
530
540
|
? html`
|
|
531
541
|
<dees-input-multitoggle class="modeSwitch" label="Agent mode" .labelPosition=${'none'}
|
|
@@ -869,6 +879,7 @@ export class DeesHarnessComposer extends DeesElement {
|
|
|
869
879
|
};
|
|
870
880
|
|
|
871
881
|
private handlePaste = (event: ClipboardEvent): void => {
|
|
882
|
+
if (!this.attachmentsEnabled || this.disabled) return;
|
|
872
883
|
const files = Array.from(event.clipboardData?.files ?? []);
|
|
873
884
|
if (!files.length) return;
|
|
874
885
|
event.preventDefault();
|
|
@@ -878,6 +889,10 @@ export class DeesHarnessComposer extends DeesElement {
|
|
|
878
889
|
private handleDragOver = (event: DragEvent): void => {
|
|
879
890
|
if (!event.dataTransfer?.types.includes('Files')) return;
|
|
880
891
|
event.preventDefault();
|
|
892
|
+
if (!this.attachmentsEnabled || this.disabled) {
|
|
893
|
+
event.dataTransfer.dropEffect = 'none';
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
881
896
|
this.shadowRoot?.querySelector('.composer')?.classList.add('dragOver');
|
|
882
897
|
};
|
|
883
898
|
|
|
@@ -890,23 +905,32 @@ export class DeesHarnessComposer extends DeesElement {
|
|
|
890
905
|
const files = Array.from(event.dataTransfer?.files ?? []);
|
|
891
906
|
if (!files.length) return;
|
|
892
907
|
event.preventDefault();
|
|
908
|
+
if (!this.attachmentsEnabled || this.disabled) {
|
|
909
|
+
if (event.dataTransfer) event.dataTransfer.dropEffect = 'none';
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
893
912
|
void this.addFiles(files);
|
|
894
913
|
};
|
|
895
914
|
|
|
896
915
|
private attachmentAdditionTail = Promise.resolve();
|
|
916
|
+
private attachmentGeneration = 0;
|
|
897
917
|
|
|
898
918
|
public addFiles(fileList: FileList | File[] | null): Promise<void> {
|
|
899
919
|
const files = Array.from(fileList ?? []);
|
|
900
|
-
if (!files.length) return Promise.resolve();
|
|
901
|
-
const
|
|
920
|
+
if (!files.length || !this.attachmentsEnabled || this.disabled) return Promise.resolve();
|
|
921
|
+
const generation = this.attachmentGeneration;
|
|
922
|
+
const addition = this.attachmentAdditionTail.then(() => this.addFilesSequential(files, generation));
|
|
902
923
|
this.attachmentAdditionTail = addition.catch(() => undefined);
|
|
903
924
|
return addition;
|
|
904
925
|
}
|
|
905
926
|
|
|
906
|
-
private async addFilesSequential(files: File[]): Promise<void> {
|
|
927
|
+
private async addFilesSequential(files: File[], generation: number): Promise<void> {
|
|
928
|
+
const allowed = () => this.attachmentsEnabled && !this.disabled && generation === this.attachmentGeneration;
|
|
929
|
+
if (!allowed()) return;
|
|
907
930
|
const added: IHarnessAttachment[] = [];
|
|
908
931
|
let totalBytes = this.attachments.reduce((sum, attachment) => sum + attachment.size, 0);
|
|
909
932
|
for (const file of files) {
|
|
933
|
+
if (!allowed()) return;
|
|
910
934
|
if (this.attachments.length + added.length >= this.maxAttachmentCount) {
|
|
911
935
|
this.emit('harness-attachment-error', {
|
|
912
936
|
message: `A maximum of ${this.maxAttachmentCount} attachments is allowed.`,
|
|
@@ -928,6 +952,7 @@ export class DeesHarnessComposer extends DeesElement {
|
|
|
928
952
|
}
|
|
929
953
|
try {
|
|
930
954
|
const dataBase64 = await harnessFileToBase64(file);
|
|
955
|
+
if (!allowed()) return;
|
|
931
956
|
const kind = harnessAttachmentKindFor(mediaType);
|
|
932
957
|
added.push({
|
|
933
958
|
id: globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
@@ -939,12 +964,13 @@ export class DeesHarnessComposer extends DeesElement {
|
|
|
939
964
|
});
|
|
940
965
|
totalBytes += file.size;
|
|
941
966
|
} catch (error) {
|
|
967
|
+
if (!allowed()) return;
|
|
942
968
|
this.emit('harness-attachment-error', {
|
|
943
969
|
message: error instanceof Error ? error.message : String(error),
|
|
944
970
|
});
|
|
945
971
|
}
|
|
946
972
|
}
|
|
947
|
-
if (added.length) {
|
|
973
|
+
if (added.length && allowed()) {
|
|
948
974
|
this.emitAttachments([...this.attachments, ...added]);
|
|
949
975
|
}
|
|
950
976
|
}
|
|
@@ -957,7 +983,7 @@ export class DeesHarnessComposer extends DeesElement {
|
|
|
957
983
|
&& (!this.busy || !this.inputSteeringEnabled || !this.value.trim())) return;
|
|
958
984
|
const detail: IHarnessComposerSendDetail = {
|
|
959
985
|
text: this.value,
|
|
960
|
-
attachments: this.attachments,
|
|
986
|
+
attachments: this.attachmentsEnabled ? this.attachments : [],
|
|
961
987
|
account: this.account || undefined,
|
|
962
988
|
model: this.model || undefined,
|
|
963
989
|
reasoningEffort: this.reasoningEffort || undefined,
|
|
@@ -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
|
+
};
|