@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
|
@@ -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
|
}
|
|
@@ -11,18 +11,21 @@ export const renderDatepicker = (component: DeesInputDatepicker): TemplateResult
|
|
|
11
11
|
aria-label=${component.label}
|
|
12
12
|
aria-haspopup="dialog"
|
|
13
13
|
aria-expanded=${component.isOpened ? 'true' : 'false'}
|
|
14
|
+
aria-invalid=${component.validationText ? 'true' : 'false'}
|
|
15
|
+
aria-describedby="date-description date-validation"
|
|
14
16
|
class="date-input field-control ${component.isOpened ? 'open' : ''}"
|
|
15
|
-
.value=${component.formatDate(component.value)}
|
|
17
|
+
.value=${component.draftText ?? component.formatDate(component.value)}
|
|
16
18
|
.placeholder=${component.placeholder}
|
|
17
19
|
?disabled=${component.disabled}
|
|
20
|
+
?required=${component.required}
|
|
18
21
|
@click=${component.toggleCalendar}
|
|
19
22
|
@keydown=${component.handleKeydown}
|
|
20
23
|
@input=${component.handleManualInput}
|
|
21
24
|
@blur=${component.handleInputBlur}
|
|
22
|
-
style="padding-right: ${component.value ? 'calc(var(--dees-input-action-size) + 40px)' : '40px'}"
|
|
25
|
+
style="padding-right: ${component.value || component.draftText ? 'calc(var(--dees-input-action-size) + 40px)' : '40px'}"
|
|
23
26
|
/>
|
|
24
27
|
<div class="icon-container">
|
|
25
|
-
${component.value && !component.disabled ? html`
|
|
28
|
+
${(component.value || component.draftText) && !component.disabled ? html`
|
|
26
29
|
<button type="button" class="clear-button input-action" @click=${component.clearValue} aria-label="Clear date">
|
|
27
30
|
<dees-icon icon="lucide:x" iconSize="14"></dees-icon>
|
|
28
31
|
</button>
|
|
@@ -30,7 +33,10 @@ export const renderDatepicker = (component: DeesInputDatepicker): TemplateResult
|
|
|
30
33
|
<dees-icon class="calendar-icon" icon="lucide:calendar" iconSize="16"></dees-icon>
|
|
31
34
|
</div>
|
|
32
35
|
</div>
|
|
33
|
-
|
|
36
|
+
<div class="field-messages">
|
|
37
|
+
<div id="date-description">${component.renderDescription()}</div>
|
|
38
|
+
<div id="date-validation" class="validationText" role="status">${component.validationText}</div>
|
|
39
|
+
</div>
|
|
34
40
|
</div>
|
|
35
41
|
`;
|
|
36
42
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as colors from '../../00colors.js';
|
|
2
2
|
import { DeesInputBase } from '../dees-input-base/dees-input-base.js';
|
|
3
|
+
import { textInputContextMenuItems } from '../text-input-contextmenu.js';
|
|
3
4
|
import { demoFunc } from './dees-input-text.demo.js';
|
|
4
5
|
|
|
5
6
|
import {
|
|
@@ -408,58 +409,10 @@ export class DeesInputText extends DeesInputBase {
|
|
|
408
409
|
}
|
|
409
410
|
|
|
410
411
|
public getContextMenuItems() {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
name: 'Cut',
|
|
416
|
-
iconName: 'lucide:Scissors',
|
|
417
|
-
shortcut: 'Cmd+X',
|
|
418
|
-
disabled: !hasSelection,
|
|
419
|
-
action: async () => {
|
|
420
|
-
const selected = this.value.substring(input.selectionStart!, input.selectionEnd!);
|
|
421
|
-
await navigator.clipboard.writeText(selected);
|
|
422
|
-
const start = input.selectionStart!;
|
|
423
|
-
this.value = this.value.substring(0, start) + this.value.substring(input.selectionEnd!);
|
|
424
|
-
input.value = this.value;
|
|
425
|
-
input.setSelectionRange(start, start);
|
|
426
|
-
this.changeSubject.next(this);
|
|
427
|
-
},
|
|
428
|
-
},
|
|
429
|
-
{
|
|
430
|
-
name: 'Copy',
|
|
431
|
-
iconName: 'lucide:Copy',
|
|
432
|
-
shortcut: 'Cmd+C',
|
|
433
|
-
disabled: !hasSelection,
|
|
434
|
-
action: async () => {
|
|
435
|
-
const selected = this.value.substring(input.selectionStart!, input.selectionEnd!);
|
|
436
|
-
await navigator.clipboard.writeText(selected);
|
|
437
|
-
},
|
|
438
|
-
},
|
|
439
|
-
{
|
|
440
|
-
name: 'Paste',
|
|
441
|
-
iconName: 'lucide:ClipboardPaste',
|
|
442
|
-
shortcut: 'Cmd+V',
|
|
443
|
-
action: async () => {
|
|
444
|
-
const text = await navigator.clipboard.readText();
|
|
445
|
-
const start = input.selectionStart!;
|
|
446
|
-
const end = input.selectionEnd!;
|
|
447
|
-
this.value = this.value.substring(0, start) + text + this.value.substring(end);
|
|
448
|
-
input.value = this.value;
|
|
449
|
-
input.setSelectionRange(start + text.length, start + text.length);
|
|
450
|
-
this.changeSubject.next(this);
|
|
451
|
-
},
|
|
452
|
-
},
|
|
453
|
-
{ divider: true },
|
|
454
|
-
{
|
|
455
|
-
name: 'Select All',
|
|
456
|
-
iconName: 'lucide:TextCursorInput',
|
|
457
|
-
shortcut: 'Cmd+A',
|
|
458
|
-
action: async () => {
|
|
459
|
-
input.select();
|
|
460
|
-
},
|
|
461
|
-
},
|
|
462
|
-
];
|
|
412
|
+
return textInputContextMenuItems({
|
|
413
|
+
input: this.shadowRoot!.querySelector('input')!, disabled: this.disabled,
|
|
414
|
+
commit: value => { this.value = value; this.changeSubject.next(this); },
|
|
415
|
+
});
|
|
463
416
|
}
|
|
464
417
|
|
|
465
418
|
public getValue(): string {
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { customElement, property, html, css, type TemplateResult } from '@design.estate/dees-element';
|
|
2
|
+
import { DeesInputBase } from '../dees-input-base/dees-input-base.js';
|
|
3
|
+
import { textInputContextMenuItems } from '../text-input-contextmenu.js';
|
|
4
|
+
import { demoFunc } from './dees-input-textarea.demo.js';
|
|
5
|
+
import '../../00group-overlay/dees-contextmenu/dees-contextmenu.js';
|
|
6
|
+
|
|
7
|
+
declare global { interface HTMLElementTagNameMap { 'dees-input-textarea': DeesInputTextarea; } }
|
|
8
|
+
|
|
9
|
+
@customElement('dees-input-textarea')
|
|
10
|
+
export class DeesInputTextarea extends DeesInputBase<DeesInputTextarea> {
|
|
11
|
+
public static demo = demoFunc;
|
|
12
|
+
public static demoGroups = ['Input'];
|
|
13
|
+
public static inputLayout = 'compound' as const;
|
|
14
|
+
|
|
15
|
+
@property({ type: String }) accessor value = '';
|
|
16
|
+
@property({ type: String }) accessor placeholder = '';
|
|
17
|
+
@property({ type: Number }) accessor rows = 3;
|
|
18
|
+
@property({ type: Number }) accessor maxLength = -1;
|
|
19
|
+
@property({ type: String, reflect: true }) accessor validationState: 'valid' | 'warn' | 'invalid' | undefined;
|
|
20
|
+
@property({ type: String }) accessor validationText = '';
|
|
21
|
+
@property({ attribute: false }) accessor validationFunction: ((value: string) => { valid: boolean; message?: string }) | undefined;
|
|
22
|
+
private touched = false;
|
|
23
|
+
private lastValidity: boolean | undefined;
|
|
24
|
+
|
|
25
|
+
public static styles = [...DeesInputBase.baseStyles, css`
|
|
26
|
+
textarea { display: block; width: 100%; height: auto; padding: 10px 12px; resize: vertical; text-align: left; }
|
|
27
|
+
.field-messages { grid-column: 1 / -1; }
|
|
28
|
+
.validationText { font-size: var(--dees-font-footnote-size); line-height: var(--dees-font-footnote-line); color: var(--dees-color-text-error); }
|
|
29
|
+
.validationText:not(:empty) { margin-top: var(--dees-spacing-xs); }
|
|
30
|
+
:host([validationstate="invalid"]) textarea { border-color: var(--dees-color-accent-error); }
|
|
31
|
+
:host([validationstate="warn"]) .validationText { color: var(--dees-color-text-warning); }
|
|
32
|
+
:host([validationstate="valid"]) .validationText { color: var(--dees-color-text-success); }
|
|
33
|
+
`];
|
|
34
|
+
|
|
35
|
+
public render(): TemplateResult {
|
|
36
|
+
return html`<div class="input-wrapper">
|
|
37
|
+
<dees-label .label=${this.label} .infoText=${this.infoText} .required=${this.required} @click=${() => this.focus()}></dees-label>
|
|
38
|
+
<div class="input-container">
|
|
39
|
+
<textarea class="field-control" .value=${this.value} .rows=${Math.max(1, this.rows)}
|
|
40
|
+
placeholder=${this.placeholder} ?disabled=${this.disabled} ?required=${this.required}
|
|
41
|
+
aria-label=${this.label ?? ''} aria-invalid=${this.validationState === 'invalid' ? 'true' : 'false'}
|
|
42
|
+
aria-describedby="field-description field-validation"
|
|
43
|
+
@input=${this.handleInput} @blur=${() => this.validate()}></textarea>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="field-messages">
|
|
46
|
+
<div id="field-description">${this.renderDescription()}</div>
|
|
47
|
+
<div id="field-validation" class="validationText" role="status">${this.validationText}</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public willUpdate(changed: Map<PropertyKey, unknown>): void {
|
|
53
|
+
super.willUpdate(changed);
|
|
54
|
+
if (changed.has('value') && this.value == null) this.value = '';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public updated(changed: Map<string, unknown>): void {
|
|
58
|
+
super.updated(changed);
|
|
59
|
+
const input = this.shadowRoot?.querySelector('textarea');
|
|
60
|
+
if (this.maxLength >= 0) input?.setAttribute('maxlength', String(this.maxLength));
|
|
61
|
+
else input?.removeAttribute('maxlength');
|
|
62
|
+
if (['value', 'required', 'maxLength', 'validationFunction', 'disabled'].some(key => changed.has(key))) {
|
|
63
|
+
this.validate(this.touched || Boolean(this.value));
|
|
64
|
+
} else if (changed.has('validationState') || changed.has('validationText')) {
|
|
65
|
+
this.syncValidity();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private ruleMessage(): string {
|
|
70
|
+
if (this.disabled) return '';
|
|
71
|
+
if (this.required && !this.value.length) return 'Enter a value.';
|
|
72
|
+
if (this.maxLength >= 0 && this.value.length > this.maxLength) return `Use ${this.maxLength} characters or fewer.`;
|
|
73
|
+
const result = this.validationFunction?.(this.value);
|
|
74
|
+
return result && !result.valid ? result.message || 'Check this value.' : '';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private syncValidity(): void {
|
|
78
|
+
const message = this.disabled ? '' : this.ruleMessage() || (this.validationState === 'invalid' ? this.validationText || 'Check this value.' : '');
|
|
79
|
+
this.shadowRoot?.querySelector('textarea')?.setCustomValidity(message);
|
|
80
|
+
const valid = !message;
|
|
81
|
+
if (valid !== this.lastValidity) {
|
|
82
|
+
this.lastValidity = valid;
|
|
83
|
+
this.dispatchEvent(new CustomEvent('validation-change', { detail: { valid, message }, bubbles: true, composed: true }));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public validate(showMessage = true): boolean {
|
|
88
|
+
if (showMessage) this.touched = true;
|
|
89
|
+
const message = this.ruleMessage();
|
|
90
|
+
this.validationText = showMessage ? message : '';
|
|
91
|
+
this.validationState = showMessage ? message ? 'invalid' : 'valid' : undefined;
|
|
92
|
+
this.syncValidity();
|
|
93
|
+
return !message;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public checkValidity(): boolean {
|
|
97
|
+
return this.disabled || (!this.ruleMessage() && this.validationState !== 'invalid');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
public reportValidity(): boolean {
|
|
101
|
+
// Preserve a host-supplied validation message until the value changes.
|
|
102
|
+
const valid = this.validationState === 'invalid' ? this.checkValidity() : this.validate();
|
|
103
|
+
if (!valid) this.focus();
|
|
104
|
+
return valid;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private handleInput = (): void => {
|
|
108
|
+
if (this.disabled) return;
|
|
109
|
+
this.value = this.shadowRoot!.querySelector('textarea')!.value;
|
|
110
|
+
this.validate();
|
|
111
|
+
this.changeSubject.next(this);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
public getContextMenuItems() {
|
|
115
|
+
return textInputContextMenuItems({ input: this.shadowRoot!.querySelector('textarea')!, disabled: this.disabled,
|
|
116
|
+
commit: value => { this.value = value; this.validate(); this.changeSubject.next(this); } });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
public getValue(): string { return this.value; }
|
|
120
|
+
public setValue(value: string | null): void {
|
|
121
|
+
this.value = value ?? '';
|
|
122
|
+
this.touched = false;
|
|
123
|
+
this.validate(false);
|
|
124
|
+
}
|
|
125
|
+
public reset(): void { this.setValue(''); }
|
|
126
|
+
public focus(options?: FocusOptions): void { this.shadowRoot?.querySelector('textarea')?.focus(options); }
|
|
127
|
+
public blur(): void { this.shadowRoot?.querySelector('textarea')?.blur(); }
|
|
128
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './dees-input-textarea.js';
|
|
@@ -15,6 +15,7 @@ export * from './dees-input-richtext/index.js';
|
|
|
15
15
|
export * from './dees-input-searchselect/index.js';
|
|
16
16
|
export * from './dees-input-tags/index.js';
|
|
17
17
|
export * from './dees-input-text/index.js';
|
|
18
|
+
export * from './dees-input-textarea/index.js';
|
|
18
19
|
export * from './dees-input-toggle/index.js';
|
|
19
20
|
export * from './dees-input-typelist/index.js';
|
|
20
21
|
export * from './dees-input-wysiwyg.js';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
interface ITextInputContext {
|
|
2
|
+
input: HTMLInputElement | HTMLTextAreaElement;
|
|
3
|
+
disabled: boolean;
|
|
4
|
+
commit: (value: string) => void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Shared plain-text editing commands for single- and multiline fields. */
|
|
8
|
+
export const textInputContextMenuItems = ({ input, disabled, commit }: ITextInputContext) => {
|
|
9
|
+
const selectedText = () => input.value.substring(input.selectionStart ?? 0, input.selectionEnd ?? 0);
|
|
10
|
+
const replaceSelection = (text: string) => {
|
|
11
|
+
if (disabled || input.disabled) return;
|
|
12
|
+
input.setRangeText(text, input.selectionStart ?? 0, input.selectionEnd ?? 0, 'end');
|
|
13
|
+
commit(input.value);
|
|
14
|
+
};
|
|
15
|
+
return [
|
|
16
|
+
{ name: 'Cut', iconName: 'lucide:Scissors', shortcut: 'Cmd+X', disabled: disabled || !selectedText(), action: async () => {
|
|
17
|
+
if (disabled || input.disabled) return;
|
|
18
|
+
await navigator.clipboard.writeText(selectedText());
|
|
19
|
+
replaceSelection('');
|
|
20
|
+
} },
|
|
21
|
+
{ name: 'Copy', iconName: 'lucide:Copy', shortcut: 'Cmd+C', disabled: !selectedText(), action: async () => {
|
|
22
|
+
await navigator.clipboard.writeText(selectedText());
|
|
23
|
+
} },
|
|
24
|
+
{ name: 'Paste', iconName: 'lucide:ClipboardPaste', shortcut: 'Cmd+V', disabled, action: async () => {
|
|
25
|
+
if (disabled || input.disabled) return;
|
|
26
|
+
replaceSelection(await navigator.clipboard.readText());
|
|
27
|
+
} },
|
|
28
|
+
{ divider: true as const },
|
|
29
|
+
{ name: 'Select All', iconName: 'lucide:TextCursorInput', shortcut: 'Cmd+A', action: () => input.select() },
|
|
30
|
+
];
|
|
31
|
+
};
|