@haloduck/ui 2.0.19 → 2.0.21
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/fesm2022/haloduck-ui.mjs +59 -12
- package/fesm2022/haloduck-ui.mjs.map +1 -1
- package/index.d.ts +2 -1
- package/package.json +1 -1
package/fesm2022/haloduck-ui.mjs
CHANGED
|
@@ -345,6 +345,7 @@ class SelectDropdownComponent {
|
|
|
345
345
|
manualInputValues = {};
|
|
346
346
|
activeManualKey = null;
|
|
347
347
|
selectedChange = new EventEmitter();
|
|
348
|
+
closeDropdown = new EventEmitter();
|
|
348
349
|
useFilter = false;
|
|
349
350
|
multiselect = true;
|
|
350
351
|
atLeastOne = false;
|
|
@@ -400,8 +401,13 @@ class SelectDropdownComponent {
|
|
|
400
401
|
this.activeManualKey = null;
|
|
401
402
|
}
|
|
402
403
|
else {
|
|
403
|
-
// open manual input
|
|
404
|
+
// open manual input and handle selection based on mode
|
|
404
405
|
this.activeManualKey = key;
|
|
406
|
+
// In single select mode, clear existing selections first
|
|
407
|
+
if (!this.multiselect) {
|
|
408
|
+
const sentinel = option.id || option.value;
|
|
409
|
+
this.setSelected([sentinel]);
|
|
410
|
+
}
|
|
405
411
|
// ensure there is an entry in manualInputValues
|
|
406
412
|
if (this.manualInputValues[key] === undefined) {
|
|
407
413
|
this.manualInputValues[key] = '';
|
|
@@ -457,15 +463,19 @@ class SelectDropdownComponent {
|
|
|
457
463
|
isOptionSelected(option) {
|
|
458
464
|
const id = option.id || option.value;
|
|
459
465
|
if (option.shouldManualInput) {
|
|
466
|
+
const key = this.getManualKey(option);
|
|
460
467
|
const prefix = option.manualPrefix;
|
|
468
|
+
// Check if this manual input option is currently active
|
|
469
|
+
const isActive = this.activeManualKey === key;
|
|
461
470
|
if (prefix) {
|
|
462
|
-
// selected
|
|
463
|
-
|
|
471
|
+
// selected if there exists a value with more than prefix, OR if it's currently active
|
|
472
|
+
const hasValueWithPrefix = this._selectedOptionIds.some((selectedId) => typeof selectedId === 'string' && selectedId.startsWith(prefix) && selectedId.length > prefix.length);
|
|
473
|
+
return hasValueWithPrefix || isActive;
|
|
464
474
|
}
|
|
465
|
-
// no prefix: consider selected if any selected id equals current typed value (non-empty)
|
|
466
|
-
const key = this.getManualKey(option);
|
|
475
|
+
// no prefix: consider selected if any selected id equals current typed value (non-empty) OR if it's active
|
|
467
476
|
const typed = (this.manualInputValues[key] || '').trim();
|
|
468
|
-
|
|
477
|
+
const hasTypedValue = typed !== '' && this._selectedOptionIds.includes(typed);
|
|
478
|
+
return hasTypedValue || isActive;
|
|
469
479
|
}
|
|
470
480
|
return this._selectedOptionIds.includes(id);
|
|
471
481
|
}
|
|
@@ -500,14 +510,15 @@ class SelectDropdownComponent {
|
|
|
500
510
|
onManualInputChange(option, value) {
|
|
501
511
|
const key = this.getManualKey(option);
|
|
502
512
|
this.manualInputValues[key] = value;
|
|
503
|
-
// Dynamically reflect selection as user types
|
|
513
|
+
// Dynamically reflect selection as user types
|
|
504
514
|
const prefix = option.manualPrefix || '';
|
|
505
515
|
const sentinel = option.id || option.value;
|
|
506
516
|
const trimmed = (value || '').trim();
|
|
507
517
|
if (this.multiselect) {
|
|
508
518
|
if (option.isExclusive) {
|
|
509
519
|
if (trimmed === '') {
|
|
510
|
-
|
|
520
|
+
// Keep the sentinel selected to maintain the manual input option as "active"
|
|
521
|
+
this.setSelected([sentinel]);
|
|
511
522
|
}
|
|
512
523
|
else {
|
|
513
524
|
this.setSelected([`${prefix}${trimmed}`]);
|
|
@@ -523,13 +534,20 @@ class SelectDropdownComponent {
|
|
|
523
534
|
next = [toAdd, ...next];
|
|
524
535
|
}
|
|
525
536
|
}
|
|
537
|
+
else {
|
|
538
|
+
// Keep the sentinel to maintain selection state when text is empty
|
|
539
|
+
if (!next.includes(sentinel)) {
|
|
540
|
+
next = [sentinel, ...next];
|
|
541
|
+
}
|
|
542
|
+
}
|
|
526
543
|
this.setSelected(next);
|
|
527
544
|
}
|
|
528
545
|
}
|
|
529
546
|
else {
|
|
530
547
|
// single select
|
|
531
548
|
if (trimmed === '') {
|
|
532
|
-
|
|
549
|
+
// Keep the sentinel selected to maintain the manual input option as "active"
|
|
550
|
+
this.setSelected([sentinel]);
|
|
533
551
|
}
|
|
534
552
|
else {
|
|
535
553
|
this.setSelected([`${prefix}${trimmed}`]);
|
|
@@ -541,6 +559,10 @@ class SelectDropdownComponent {
|
|
|
541
559
|
const raw = (this.manualInputValues[key] || '').trim();
|
|
542
560
|
// Use same logic as onManualInputChange
|
|
543
561
|
this.onManualInputChange(option, raw);
|
|
562
|
+
// Close dropdown in single select mode after confirming input
|
|
563
|
+
if (!this.multiselect) {
|
|
564
|
+
this.closeDropdown.emit();
|
|
565
|
+
}
|
|
544
566
|
}
|
|
545
567
|
onManualInputBlur(option) {
|
|
546
568
|
const key = this.getManualKey(option);
|
|
@@ -548,6 +570,25 @@ class SelectDropdownComponent {
|
|
|
548
570
|
if (raw === '') {
|
|
549
571
|
// hide input on blur when empty
|
|
550
572
|
this.activeManualKey = null;
|
|
573
|
+
// Also deselect the manual input option completely
|
|
574
|
+
const prefix = option.manualPrefix || '';
|
|
575
|
+
const sentinel = option.id || option.value;
|
|
576
|
+
if (this.multiselect) {
|
|
577
|
+
let next = [...this._selectedOptionIds];
|
|
578
|
+
// Remove sentinel and any values with the prefix
|
|
579
|
+
next = next.filter((id) => {
|
|
580
|
+
if (id === sentinel)
|
|
581
|
+
return false;
|
|
582
|
+
if (prefix && id.startsWith(prefix))
|
|
583
|
+
return false;
|
|
584
|
+
return true;
|
|
585
|
+
});
|
|
586
|
+
this.setSelected(next);
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
// Single select: deselect completely
|
|
590
|
+
this.setSelected([]);
|
|
591
|
+
}
|
|
551
592
|
}
|
|
552
593
|
}
|
|
553
594
|
emitSelectedChange() {
|
|
@@ -562,13 +603,15 @@ class SelectDropdownComponent {
|
|
|
562
603
|
});
|
|
563
604
|
}
|
|
564
605
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: SelectDropdownComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
565
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.4", type: SelectDropdownComponent, isStandalone: true, selector: "haloduck-select-dropdown", inputs: { useFilter: "useFilter", multiselect: "multiselect", atLeastOne: "atLeastOne", asButton: "asButton", options: "options", selectedOptionIds: "selectedOptionIds" }, outputs: { selectedChange: "selectedChange" }, providers: [provideTranslocoScope('haloduck')], ngImport: i0, template: "<div id=\"dropdown\"\n class=\"max-w-full mt-2 absolute z-40 bg-light-background dark:bg-dark-background text-light-on-background dark:text-dark-on-background border border-light-inactive dark:border-dark-inactive rounded max-h-60 flex flex-col gap-2\">\n @if (useFilter && _options.length >= 5) {\n <input #inputFilter\n id=\"inputFilter\"\n type=\"text\"\n [placeholder]=\"'haloduck.ui.select.Keyword...' | transloco\"\n (input)=\"onFilterInput($event)\"\n class=\"text-light-inactive dark:text-dark-inactive rounded-md outline -outline-offset-1 outline-light-inactive dark:outline-dark-inactive focus:outline-2 focus:outline-offset-2 focus:outline-light-primary dark:focus:outline-dark-primary px-3 py-1.5 text-sm/6 bg-light-control dark:bg-dark-control m-2\" />\n }\n <div class=\"overflow-y-auto\">\n @for ( option of _filteredOptions(); track (option.id) ? option.id : option.value) {\n <div class=\"px-3 py-2 text-sm/6 hover:bg-light-secondary/60 dark:hover:bg-dark-secondary/60 flex items-center justify-start whitespace-nowrap\"\n [class.cursor-pointer]=\"!option.shouldManualInput\"\n (click)=\"onToggleOption(option)\">\n @if(!asButton) {\n @if( isOptionSelected(option)) {\n <svg class=\"w-4 h-4 text-light-primary dark:text-dark-primary inline-block mr-2\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 20 20\"\n fill=\"currentColor\"\n aria-hidden=\"true\">\n <path fill-rule=\"evenodd\"\n d=\"M16.707 5.293a1 1 0 00-1.414 0L8 12.586 4.707 9.293a1 1 0 00-1.414 1.414l4 4a1 1 0 001.414 0l8-8a1 1 0 000-1.414z\"\n clip-rule=\"evenodd\" />\n </svg>\n } @else {\n <div class=\"w-4 h-4 inline-block mr-2\"></div>\n }\n }\n @if(option.shouldManualInput && (isOptionSelected(option) || (activeManualKey === (option.manualPrefix ? option.manualPrefix : (option.id || option.value))))) {\n <input type=\"text\"\n [(ngModel)]=\"manualInputValues[option.manualPrefix ? option.manualPrefix : (option.id || option.value)]\"\n [attr.data-manual-key]=\"option.manualPrefix ? option.manualPrefix : (option.id || option.value)\"\n (ngModelChange)=\"onManualInputChange(option, $event)\"\n (click)=\"$event.stopPropagation()\"\n (blur)=\"onManualInputBlur(option)\"\n (keydown.enter)=\"onManualInputConfirm(option)\"\n class=\"text-light-on-control dark:text-dark-on-control rounded-md outline -outline-offset-1 outline-light-inactive dark:outline-dark-inactive focus:outline-2 focus:outline-offset-2 focus:outline-light-primary dark:focus:outline-dark-primary px-2 py-1 text-sm/6 bg-light-control dark:bg-dark-control w-full\" />\n } @else {\n {{ option.value }}\n }\n </div>\n }\n </div>\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: TranslocoModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "pipe", type: i2$1.TranslocoPipe, name: "transloco" }] });
|
|
606
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.4", type: SelectDropdownComponent, isStandalone: true, selector: "haloduck-select-dropdown", inputs: { useFilter: "useFilter", multiselect: "multiselect", atLeastOne: "atLeastOne", asButton: "asButton", options: "options", selectedOptionIds: "selectedOptionIds" }, outputs: { selectedChange: "selectedChange", closeDropdown: "closeDropdown" }, providers: [provideTranslocoScope('haloduck')], ngImport: i0, template: "<div id=\"dropdown\"\n class=\"max-w-full mt-2 absolute z-40 bg-light-background dark:bg-dark-background text-light-on-background dark:text-dark-on-background border border-light-inactive dark:border-dark-inactive rounded max-h-60 flex flex-col gap-2\">\n @if (useFilter && _options.length >= 5) {\n <input #inputFilter\n id=\"inputFilter\"\n type=\"text\"\n [placeholder]=\"'haloduck.ui.select.Keyword...' | transloco\"\n (input)=\"onFilterInput($event)\"\n class=\"text-light-inactive dark:text-dark-inactive rounded-md outline -outline-offset-1 outline-light-inactive dark:outline-dark-inactive focus:outline-2 focus:outline-offset-2 focus:outline-light-primary dark:focus:outline-dark-primary px-3 py-1.5 text-sm/6 bg-light-control dark:bg-dark-control m-2\" />\n }\n <div class=\"overflow-y-auto\">\n @for ( option of _filteredOptions(); track (option.id) ? option.id : option.value) {\n <div class=\"px-3 py-2 text-sm/6 hover:bg-light-secondary/60 dark:hover:bg-dark-secondary/60 flex items-center justify-start whitespace-nowrap\"\n [class.cursor-pointer]=\"!option.shouldManualInput\"\n (click)=\"onToggleOption(option)\">\n @if(!asButton) {\n @if( isOptionSelected(option)) {\n <svg class=\"w-4 h-4 text-light-primary dark:text-dark-primary inline-block mr-2\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 20 20\"\n fill=\"currentColor\"\n aria-hidden=\"true\">\n <path fill-rule=\"evenodd\"\n d=\"M16.707 5.293a1 1 0 00-1.414 0L8 12.586 4.707 9.293a1 1 0 00-1.414 1.414l4 4a1 1 0 001.414 0l8-8a1 1 0 000-1.414z\"\n clip-rule=\"evenodd\" />\n </svg>\n } @else {\n <div class=\"w-4 h-4 inline-block mr-2\"></div>\n }\n }\n @if(option.shouldManualInput && (isOptionSelected(option) || (activeManualKey === (option.manualPrefix ? option.manualPrefix : (option.id || option.value))))) {\n <input type=\"text\"\n [(ngModel)]=\"manualInputValues[option.manualPrefix ? option.manualPrefix : (option.id || option.value)]\"\n [attr.data-manual-key]=\"option.manualPrefix ? option.manualPrefix : (option.id || option.value)\"\n (ngModelChange)=\"onManualInputChange(option, $event)\"\n (click)=\"$event.stopPropagation()\"\n (blur)=\"onManualInputBlur(option)\"\n (keydown.enter)=\"onManualInputConfirm(option)\"\n class=\"text-light-on-control dark:text-dark-on-control rounded-md outline -outline-offset-1 outline-light-inactive dark:outline-dark-inactive focus:outline-2 focus:outline-offset-2 focus:outline-light-primary dark:focus:outline-dark-primary px-2 py-1 text-sm/6 bg-light-control dark:bg-dark-control w-full\" />\n } @else {\n {{ option.value }}\n }\n </div>\n }\n </div>\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: TranslocoModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "pipe", type: i2$1.TranslocoPipe, name: "transloco" }] });
|
|
566
607
|
}
|
|
567
608
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: SelectDropdownComponent, decorators: [{
|
|
568
609
|
type: Component,
|
|
569
610
|
args: [{ selector: 'haloduck-select-dropdown', imports: [TranslocoModule, FormsModule], providers: [provideTranslocoScope('haloduck')], template: "<div id=\"dropdown\"\n class=\"max-w-full mt-2 absolute z-40 bg-light-background dark:bg-dark-background text-light-on-background dark:text-dark-on-background border border-light-inactive dark:border-dark-inactive rounded max-h-60 flex flex-col gap-2\">\n @if (useFilter && _options.length >= 5) {\n <input #inputFilter\n id=\"inputFilter\"\n type=\"text\"\n [placeholder]=\"'haloduck.ui.select.Keyword...' | transloco\"\n (input)=\"onFilterInput($event)\"\n class=\"text-light-inactive dark:text-dark-inactive rounded-md outline -outline-offset-1 outline-light-inactive dark:outline-dark-inactive focus:outline-2 focus:outline-offset-2 focus:outline-light-primary dark:focus:outline-dark-primary px-3 py-1.5 text-sm/6 bg-light-control dark:bg-dark-control m-2\" />\n }\n <div class=\"overflow-y-auto\">\n @for ( option of _filteredOptions(); track (option.id) ? option.id : option.value) {\n <div class=\"px-3 py-2 text-sm/6 hover:bg-light-secondary/60 dark:hover:bg-dark-secondary/60 flex items-center justify-start whitespace-nowrap\"\n [class.cursor-pointer]=\"!option.shouldManualInput\"\n (click)=\"onToggleOption(option)\">\n @if(!asButton) {\n @if( isOptionSelected(option)) {\n <svg class=\"w-4 h-4 text-light-primary dark:text-dark-primary inline-block mr-2\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 20 20\"\n fill=\"currentColor\"\n aria-hidden=\"true\">\n <path fill-rule=\"evenodd\"\n d=\"M16.707 5.293a1 1 0 00-1.414 0L8 12.586 4.707 9.293a1 1 0 00-1.414 1.414l4 4a1 1 0 001.414 0l8-8a1 1 0 000-1.414z\"\n clip-rule=\"evenodd\" />\n </svg>\n } @else {\n <div class=\"w-4 h-4 inline-block mr-2\"></div>\n }\n }\n @if(option.shouldManualInput && (isOptionSelected(option) || (activeManualKey === (option.manualPrefix ? option.manualPrefix : (option.id || option.value))))) {\n <input type=\"text\"\n [(ngModel)]=\"manualInputValues[option.manualPrefix ? option.manualPrefix : (option.id || option.value)]\"\n [attr.data-manual-key]=\"option.manualPrefix ? option.manualPrefix : (option.id || option.value)\"\n (ngModelChange)=\"onManualInputChange(option, $event)\"\n (click)=\"$event.stopPropagation()\"\n (blur)=\"onManualInputBlur(option)\"\n (keydown.enter)=\"onManualInputConfirm(option)\"\n class=\"text-light-on-control dark:text-dark-on-control rounded-md outline -outline-offset-1 outline-light-inactive dark:outline-dark-inactive focus:outline-2 focus:outline-offset-2 focus:outline-light-primary dark:focus:outline-dark-primary px-2 py-1 text-sm/6 bg-light-control dark:bg-dark-control w-full\" />\n } @else {\n {{ option.value }}\n }\n </div>\n }\n </div>\n</div>\n" }]
|
|
570
611
|
}], propDecorators: { selectedChange: [{
|
|
571
612
|
type: Output
|
|
613
|
+
}], closeDropdown: [{
|
|
614
|
+
type: Output
|
|
572
615
|
}], useFilter: [{
|
|
573
616
|
type: Input
|
|
574
617
|
}], multiselect: [{
|
|
@@ -685,6 +728,10 @@ class SelectComponent {
|
|
|
685
728
|
}
|
|
686
729
|
});
|
|
687
730
|
});
|
|
731
|
+
componentRef.instance.closeDropdown.subscribe(() => {
|
|
732
|
+
this.isDropdownOpen.set(false);
|
|
733
|
+
this.overlayRef?.detach();
|
|
734
|
+
});
|
|
688
735
|
this.isDropdownOpen.set(true);
|
|
689
736
|
}
|
|
690
737
|
else {
|
|
@@ -728,10 +775,10 @@ class SelectComponent {
|
|
|
728
775
|
onTouched = () => { };
|
|
729
776
|
writeValue(value) {
|
|
730
777
|
if (Array.isArray(value)) {
|
|
731
|
-
this.selectedOptionIds = value;
|
|
778
|
+
this.selectedOptionIds = value.filter(id => id !== null && id !== undefined && id !== '');
|
|
732
779
|
}
|
|
733
780
|
else {
|
|
734
|
-
this.selectedOptionIds = [value];
|
|
781
|
+
this.selectedOptionIds = (value !== null && value !== undefined && value !== '') ? [value] : [];
|
|
735
782
|
}
|
|
736
783
|
this.recomputeSelectedOptions();
|
|
737
784
|
}
|