tailjng 0.1.6 → 0.1.7
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/README.md +12 -4
- package/cli/execute/init-app.js +5 -2
- package/cli/execute/sync-app.js +14 -2
- package/cli/settings/colors-config-utils.js +43 -8
- package/cli/settings/icons-config-utils.js +62 -0
- package/cli/settings/path-utils.js +32 -2
- package/cli/settings/project-utils.js +7 -1
- package/cli/templates/app.generator.js +2 -2
- package/fesm2022/tailjng.mjs +247 -80
- package/fesm2022/tailjng.mjs.map +1 -1
- package/lib/services/static/theme.service.d.ts +39 -1
- package/lib/utils/theme/theme-variables.util.d.ts +31 -0
- package/package.json +1 -1
- package/public-api.d.ts +2 -1
- package/registry/components.json +41 -18
- package/src/colors.safelist.css +2 -2
- package/src/lib/components/.config/README.md +11 -0
- package/src/lib/components/.config/colors/README.md +38 -0
- package/src/lib/components/{colors-config → .config/colors}/colors.config.ts +5 -5
- package/src/lib/components/{colors-config → .config/colors}/colors.safelist.css +2 -2
- package/src/lib/components/.config/icons/README.md +26 -0
- package/src/lib/components/.config/icons/icons.lucide.ts +134 -0
- package/src/lib/components/.config/input/README.md +24 -0
- package/src/lib/components/.config/input/input.classes.ts +119 -0
- package/src/lib/components/alert/alert-dialog/dialog-alert.component.css +244 -2
- package/src/lib/components/alert/alert-dialog/dialog-alert.component.html +25 -38
- package/src/lib/components/alert/alert-dialog/dialog-alert.component.ts +66 -56
- package/src/lib/components/alert/alert-dialog/dialog-alert.types.ts +19 -0
- package/src/lib/components/alert/alert-toast/toast-alert.component.css +630 -12
- package/src/lib/components/alert/alert-toast/toast-alert.component.html +103 -102
- package/src/lib/components/alert/alert-toast/toast-alert.component.ts +485 -128
- package/src/lib/components/alert/alert-toast/toast-alert.types.ts +25 -0
- package/src/lib/components/badge/badge.component.html +34 -21
- package/src/lib/components/badge/badge.component.ts +140 -31
- package/src/lib/components/button/button.component.html +16 -10
- package/src/lib/components/button/button.component.ts +162 -22
- package/src/lib/components/card/card-complete/complete-card.component.html +2 -2
- package/src/lib/components/card/card-complete/complete-card.component.ts +26 -16
- package/src/lib/components/card/card-crud-complete/complete-crud-card.component.html +2 -2
- package/src/lib/components/card/card-crud-complete/complete-crud-card.component.ts +26 -16
- package/src/lib/components/checkbox/checkbox-input/input-checkbox.component.css +97 -0
- package/src/lib/components/checkbox/checkbox-input/input-checkbox.component.html +54 -46
- package/src/lib/components/checkbox/checkbox-input/input-checkbox.component.ts +135 -64
- package/src/lib/components/checkbox/checkbox-input/input-checkbox.types.ts +3 -0
- package/src/lib/components/checkbox/checkbox-switch/switch-checkbox.component.css +112 -0
- package/src/lib/components/checkbox/checkbox-switch/switch-checkbox.component.html +28 -25
- package/src/lib/components/checkbox/checkbox-switch/switch-checkbox.component.ts +67 -15
- package/src/lib/components/checkbox/checkbox-switch/switch-checkbox.types.ts +1 -0
- package/src/lib/components/coach-mark/coach-mark.component.html +4 -22
- package/src/lib/components/coach-mark/coach-mark.component.scss +1 -1
- package/src/lib/components/coach-mark/coach-mark.component.ts +51 -18
- package/src/lib/components/coach-mark/coach-mark.directive.ts +133 -78
- package/src/lib/components/coach-mark/coach-mark.types.ts +12 -0
- package/src/lib/components/dialog/dialog.component.css +103 -1
- package/src/lib/components/dialog/dialog.component.html +46 -66
- package/src/lib/components/dialog/dialog.component.ts +136 -110
- package/src/lib/components/dialog/dialog.types.ts +19 -0
- package/src/lib/components/filter/filter-complete/complete-filter.component.html +16 -19
- package/src/lib/components/filter/filter-complete/complete-filter.component.scss +35 -0
- package/src/lib/components/filter/filter-complete/complete-filter.component.ts +58 -34
- package/src/lib/components/filter/filter-complete/complete-filter.types.ts +7 -0
- package/src/lib/components/filter/filter-complete/complete-filter.util.ts +16 -0
- package/src/lib/components/form/form-container/container-form.component.css +4 -0
- package/src/lib/components/form/form-container/container-form.component.html +2 -2
- package/src/lib/components/form/form-container/container-form.component.ts +72 -16
- package/src/lib/components/form/form-container/container-form.types.ts +42 -0
- package/src/lib/components/form/form-container/form-col-span.directive.ts +25 -0
- package/src/lib/components/form/form-sidebar/sidebar-form.component.css +276 -0
- package/src/lib/components/form/form-sidebar/sidebar-form.component.html +117 -125
- package/src/lib/components/form/form-sidebar/sidebar-form.component.ts +109 -34
- package/src/lib/components/form/form-sidebar/sidebar-form.types.ts +3 -0
- package/src/lib/components/{toggle-radio/toggle-radio.component.css → form/form-validation/validation-form.component.css} +0 -1
- package/src/lib/components/form/form-validation/validation-form.component.html +10 -6
- package/src/lib/components/form/form-validation/validation-form.component.ts +99 -12
- package/src/lib/components/form/form-validation/validation-form.types.ts +33 -0
- package/src/lib/components/icon/icon.component.html +8 -5
- package/src/lib/components/icon/icon.component.ts +111 -9
- package/src/lib/components/input/input/input.component.html +19 -16
- package/src/lib/components/input/input/input.component.ts +130 -53
- package/src/lib/components/input/input/input.types.ts +8 -0
- package/src/lib/components/input/input-file/file-input.component.html +65 -56
- package/src/lib/components/input/input-file/file-input.component.ts +276 -173
- package/src/lib/components/input/input-file/file-input.types.ts +2 -0
- package/src/lib/components/input/input-range/range-input.component.css +67 -0
- package/src/lib/components/input/input-range/range-input.component.html +50 -58
- package/src/lib/components/input/input-range/range-input.component.ts +148 -60
- package/src/lib/components/input/input-range/range-input.types.ts +7 -0
- package/src/lib/components/input/input-textarea/textarea-input.component.html +16 -7
- package/src/lib/components/input/input-textarea/textarea-input.component.ts +140 -50
- package/src/lib/components/input/input-textarea/textarea-input.types.ts +2 -0
- package/src/lib/components/label/label.component.html +17 -16
- package/src/lib/components/label/label.component.ts +70 -16
- package/src/lib/components/label/label.types.ts +2 -0
- package/src/lib/components/menu/menu-options-table/menu-options-defaults.ts +34 -0
- package/src/lib/components/menu/menu-options-table/options-table-menu.component.html +34 -20
- package/src/lib/components/menu/menu-options-table/options-table-menu.component.ts +211 -58
- package/src/lib/components/menu/menu-options-table/options-table-menu.types.ts +38 -0
- package/src/lib/components/menu/options-coach-menu/options-coach-menu.component.html +49 -52
- package/src/lib/components/menu/options-coach-menu/options-coach-menu.component.ts +112 -24
- package/src/lib/components/menu/options-coach-menu/options-coach-menu.types.ts +9 -0
- package/src/lib/components/mode-toggle/mode-toggle.component.html +11 -16
- package/src/lib/components/mode-toggle/mode-toggle.component.ts +69 -33
- package/src/lib/components/paginator/paginator-complete/complete-paginator.component.html +4 -4
- package/src/lib/components/paginator/paginator-complete/complete-paginator.component.ts +31 -7
- package/src/lib/components/paginator/paginator-complete/complete-paginator.types.ts +12 -0
- package/src/lib/components/paginator/paginator-complete/complete-paginator.util.ts +36 -0
- package/src/lib/components/progress-bar/progress-bar.component.css +11 -0
- package/src/lib/components/progress-bar/progress-bar.component.html +41 -40
- package/src/lib/components/progress-bar/progress-bar.component.ts +95 -11
- package/src/lib/components/progress-bar/progress-bar.types.ts +2 -0
- package/src/lib/components/select/select-dropdown/dropdown-select.component.css +6 -0
- package/src/lib/components/select/select-dropdown/dropdown-select.component.html +54 -44
- package/src/lib/components/select/select-dropdown/dropdown-select.component.ts +450 -509
- package/src/lib/components/select/select-dropdown/dropdown-select.types.ts +43 -0
- package/src/lib/components/select/select-dropdown/dropdown-select.util.ts +179 -0
- package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.component.css +6 -0
- package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.component.html +131 -42
- package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.component.ts +491 -475
- package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.types.ts +22 -0
- package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.util.ts +20 -0
- package/src/lib/components/select/select-multi-table/multi-table-select.component.css +10 -0
- package/src/lib/components/select/select-multi-table/multi-table-select.component.html +76 -60
- package/src/lib/components/select/select-multi-table/multi-table-select.component.ts +250 -313
- package/src/lib/components/select/select-multi-table/multi-table-select.types.ts +10 -0
- package/src/lib/components/select/select-multi-table/multi-table-select.util.ts +5 -0
- package/src/lib/components/sidebar/sidebar-static/static-sidebar.component.css +212 -0
- package/src/lib/components/sidebar/sidebar-static/static-sidebar.component.html +62 -53
- package/src/lib/components/sidebar/sidebar-static/static-sidebar.component.ts +84 -27
- package/src/lib/components/sidebar/sidebar-static/static-sidebar.types.ts +2 -0
- package/src/lib/components/table/table-complete/complete-table.component.html +15 -17
- package/src/lib/components/table/table-complete/complete-table.component.ts +190 -338
- package/src/lib/components/table/table-complete/complete-table.types.ts +28 -0
- package/src/lib/components/table/table-complete/complete-table.util.ts +236 -0
- package/src/lib/components/table/table-complete/index.ts +2 -0
- package/src/lib/components/table/table-crud-complete/complete-crud-table.animations.ts +34 -0
- package/src/lib/components/table/table-crud-complete/complete-crud-table.component.html +73 -128
- package/src/lib/components/table/table-crud-complete/complete-crud-table.component.ts +542 -829
- package/src/lib/components/table/table-crud-complete/complete-crud-table.types.ts +57 -0
- package/src/lib/components/table/table-crud-complete/complete-crud-table.util.ts +723 -0
- package/src/lib/components/table/table-crud-complete/index.ts +3 -0
- package/src/lib/components/theme-generator/theme-generator.component.css +21 -0
- package/src/lib/components/theme-generator/theme-generator.component.html +146 -116
- package/src/lib/components/theme-generator/theme-generator.component.ts +44 -24
- package/src/lib/components/toggle-radio/shared/toggle-options.types.ts +8 -0
- package/src/lib/components/toggle-radio/shared/toggle-options.util.ts +44 -0
- package/src/lib/components/toggle-radio/toggle-radio/toggle-radio.component.css +135 -0
- package/src/lib/components/toggle-radio/toggle-radio/toggle-radio.component.html +52 -0
- package/src/lib/components/toggle-radio/toggle-radio/toggle-radio.component.ts +198 -0
- package/src/lib/components/toggle-radio/toggle-radio/toggle-radio.types.ts +1 -0
- package/src/lib/components/toggle-radio/toggle-segment/segment-toggle.component.css +108 -0
- package/src/lib/components/toggle-radio/toggle-segment/segment-toggle.component.html +37 -0
- package/src/lib/components/toggle-radio/toggle-segment/segment-toggle.component.ts +193 -0
- package/src/lib/components/toggle-radio/toggle-segment/segment-toggle.types.ts +1 -0
- package/src/lib/components/tooltip/tooltip.directive.ts +12 -9
- package/src/lib/components/tooltip/tooltip.service.ts +331 -133
- package/src/lib/components/tooltip/tooltip.types.ts +9 -0
- package/src/lib/components/viewer/viewer-image/image-viewer.component.css +90 -4
- package/src/lib/components/viewer/viewer-image/image-viewer.component.html +52 -103
- package/src/lib/components/viewer/viewer-image/image-viewer.component.ts +182 -177
- package/src/lib/components/viewer/viewer-image/image-viewer.types.ts +3 -0
- package/src/lib/components/viewer/viewer-pdf/pdf-viewer.component.css +177 -0
- package/src/lib/components/viewer/viewer-pdf/pdf-viewer.component.html +74 -24
- package/src/lib/components/viewer/viewer-pdf/pdf-viewer.component.ts +168 -15
- package/src/lib/components/viewer/viewer-pdf/pdf-viewer.types.ts +1 -0
- package/src/styles.css +2 -2
- package/lib/services/static/icons.service.d.ts +0 -65
- package/src/lib/components/colors-config/README.md +0 -38
- package/src/lib/components/form/form-sidebar/sidebar-form.component.scss +0 -0
- package/src/lib/components/form/form-validation/validation-form.component.scss +0 -0
- package/src/lib/components/menu/menu-options-table/options-table-menu.component.scss +0 -0
- package/src/lib/components/menu/options-coach-menu/options-coach-menu.component.scss +0 -12
- package/src/lib/components/sidebar/sidebar-static/static-sidebar.component.scss +0 -0
- package/src/lib/components/toggle-radio/toggle-radio.component.html +0 -51
- package/src/lib/components/toggle-radio/toggle-radio.component.ts +0 -222
- package/src/lib/components/viewer/viewer-pdf/pdf-viewer.component.scss +0 -0
- package/tailjng-0.1.6.tgz +0 -0
|
@@ -1,677 +1,618 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import { animate, style, transition, trigger } from '@angular/animations';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
import {
|
|
4
|
+
AfterViewInit,
|
|
5
|
+
ChangeDetectorRef,
|
|
6
|
+
Component,
|
|
7
|
+
ElementRef,
|
|
8
|
+
EventEmitter,
|
|
9
|
+
Input,
|
|
10
|
+
OnChanges,
|
|
11
|
+
OnDestroy,
|
|
12
|
+
OnInit,
|
|
13
|
+
Optional,
|
|
14
|
+
Output,
|
|
15
|
+
SimpleChanges,
|
|
16
|
+
ViewChild,
|
|
17
|
+
forwardRef,
|
|
18
|
+
} from '@angular/core';
|
|
19
|
+
import {
|
|
20
|
+
ControlValueAccessor,
|
|
21
|
+
FormsModule,
|
|
22
|
+
NG_VALUE_ACCESSOR,
|
|
23
|
+
ReactiveFormsModule,
|
|
24
|
+
} from '@angular/forms';
|
|
25
|
+
import { debounceTime, distinctUntilChanged, Subject, Subscription } from 'rxjs';
|
|
26
|
+
import { JGenericCrudService } from 'tailjng';
|
|
27
|
+
import { Icons } from '../../.config/icons/icons.lucide';
|
|
28
|
+
import { JIconComponent } from '../../icon/icon.component';
|
|
29
|
+
import type {
|
|
30
|
+
DropdownOption,
|
|
31
|
+
DropdownSelectDefaultFilters,
|
|
32
|
+
DropdownSelectDynamicParams,
|
|
33
|
+
DropdownSelectType,
|
|
34
|
+
DropdownSortOrder,
|
|
35
|
+
} from './dropdown-select.types';
|
|
36
|
+
import {
|
|
37
|
+
DROPDOWN_PANEL_DEFAULTS,
|
|
38
|
+
filterOptionsBySearch,
|
|
39
|
+
mapObjectOptions,
|
|
40
|
+
mapPrimitiveOptions,
|
|
41
|
+
positionDropdownPanel,
|
|
42
|
+
rebuildEndpointUrl,
|
|
43
|
+
resetDropdownPanelStyles,
|
|
44
|
+
resolveOptionLabel,
|
|
45
|
+
} from './dropdown-select.util';
|
|
46
|
+
|
|
47
|
+
export type {
|
|
48
|
+
DropdownOption,
|
|
49
|
+
DropdownPanelPosition,
|
|
50
|
+
DropdownPanelPositionConfig,
|
|
51
|
+
DropdownSelectDefaultFilters,
|
|
52
|
+
DropdownSelectDynamicParams,
|
|
53
|
+
DropdownSelectType,
|
|
54
|
+
DropdownSortOrder,
|
|
55
|
+
} from './dropdown-select.types';
|
|
56
|
+
export {
|
|
57
|
+
DROPDOWN_PANEL_DEFAULTS,
|
|
58
|
+
filterOptionsBySearch,
|
|
59
|
+
getNestedValue,
|
|
60
|
+
mapObjectOptions,
|
|
61
|
+
mapPrimitiveOptions,
|
|
62
|
+
positionDropdownPanel,
|
|
63
|
+
rebuildEndpointUrl,
|
|
64
|
+
resetDropdownPanelStyles,
|
|
65
|
+
resolveOptionLabel,
|
|
66
|
+
} from './dropdown-select.util';
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Dropdown select with static options or searchable CRUD endpoint.
|
|
70
|
+
* Panel uses fixed viewport positioning (dialogs, tables, filter popovers).
|
|
71
|
+
*
|
|
72
|
+
* Install: `npx tailjng add select-dropdown`
|
|
73
|
+
*
|
|
74
|
+
* ```html
|
|
75
|
+
* <JDropdownSelect
|
|
76
|
+
* [(ngModel)]="demoSelect"
|
|
77
|
+
* [options]="selectOptions"
|
|
78
|
+
* optionLabel="text"
|
|
79
|
+
* optionValue="value"
|
|
80
|
+
* [showClear]="true"
|
|
81
|
+
* />
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* Icons in template: `[icon]="Icons.ChevronDown"` (see `readonly Icons = Icons`).
|
|
85
|
+
*/
|
|
10
86
|
@Component({
|
|
11
|
-
selector:
|
|
12
|
-
imports: [
|
|
13
|
-
templateUrl:
|
|
14
|
-
styleUrl:
|
|
87
|
+
selector: 'JDropdownSelect',
|
|
88
|
+
imports: [JIconComponent, CommonModule, FormsModule, ReactiveFormsModule],
|
|
89
|
+
templateUrl: './dropdown-select.component.html',
|
|
90
|
+
styleUrl: './dropdown-select.component.css',
|
|
15
91
|
animations: [
|
|
16
|
-
trigger(
|
|
17
|
-
transition(
|
|
18
|
-
style({ transform:
|
|
19
|
-
animate(
|
|
92
|
+
trigger('modalTransition', [
|
|
93
|
+
transition(':enter', [
|
|
94
|
+
style({ transform: 'translateX(1rem)', opacity: 0 }),
|
|
95
|
+
animate('300ms ease-out', style({ transform: 'translateY(0)', opacity: 1 })),
|
|
96
|
+
]),
|
|
97
|
+
transition(':leave', [
|
|
98
|
+
animate('150ms ease-in', style({ transform: 'translateX(1rem)', opacity: 0 })),
|
|
20
99
|
]),
|
|
21
|
-
transition(":leave", [animate("150ms ease-in", style({ transform: "translateX(1rem)", opacity: 0 }))]),
|
|
22
100
|
]),
|
|
23
101
|
],
|
|
24
102
|
providers: [
|
|
25
103
|
{
|
|
26
104
|
provide: NG_VALUE_ACCESSOR,
|
|
27
|
-
useExisting: JDropdownSelectComponent,
|
|
105
|
+
useExisting: forwardRef(() => JDropdownSelectComponent),
|
|
28
106
|
multi: true,
|
|
29
107
|
},
|
|
30
108
|
],
|
|
31
109
|
})
|
|
32
|
-
export class JDropdownSelectComponent
|
|
110
|
+
export class JDropdownSelectComponent
|
|
111
|
+
implements ControlValueAccessor, OnInit, AfterViewInit, OnChanges, OnDestroy
|
|
112
|
+
{
|
|
113
|
+
// Project Icons registry — template usage: Icons.ChevronDown, Icons.Loader2, …
|
|
114
|
+
readonly Icons = Icons;
|
|
115
|
+
|
|
116
|
+
// =====================================================
|
|
117
|
+
// Inputs / outputs
|
|
118
|
+
// =====================================================
|
|
119
|
+
|
|
120
|
+
// `dropdown` = static options · `searchable` = CRUD endpoint + debounced search
|
|
121
|
+
@Input() type: DropdownSelectType = 'dropdown';
|
|
122
|
+
|
|
123
|
+
// Panel heading above the option list
|
|
124
|
+
@Input() title = 'Seleccionar';
|
|
125
|
+
|
|
126
|
+
// Trigger label when nothing is selected
|
|
127
|
+
@Input() placeholder = 'Seleccione una opción';
|
|
128
|
+
|
|
129
|
+
// Static options (`string[]` or object array with optionLabel / optionValue)
|
|
130
|
+
@Input() options: unknown[] = [];
|
|
131
|
+
|
|
132
|
+
// Property name(s) used to build option text (supports dot paths)
|
|
133
|
+
@Input() optionLabel: string | string[] = 'text';
|
|
33
134
|
|
|
34
|
-
|
|
135
|
+
// Property name used as the option value
|
|
136
|
+
@Input() optionValue = 'value';
|
|
35
137
|
|
|
36
|
-
|
|
37
|
-
@Input()
|
|
138
|
+
// Separator when `optionLabel` is an array of keys
|
|
139
|
+
@Input() labelSeparator = ' ';
|
|
38
140
|
|
|
39
|
-
|
|
40
|
-
@Input() optionLabel: string | string[] = "text";
|
|
41
|
-
@Input() optionValue = "value";
|
|
42
|
-
@Input() labelSeparator = " ";
|
|
141
|
+
// Prepends a synthetic “TODOS” option with `value: null` (searchable mode)
|
|
43
142
|
@Input() showAllOption = false;
|
|
44
143
|
|
|
45
|
-
@Input() isDisabled = false
|
|
144
|
+
@Input() isDisabled = false;
|
|
145
|
+
|
|
146
|
+
// Shows clear (X) on the trigger when a value is selected
|
|
46
147
|
@Input() showClear = false;
|
|
148
|
+
|
|
149
|
+
// Loading state on trigger or panel (API / parent control)
|
|
47
150
|
@Input() isLoading = false;
|
|
48
151
|
|
|
152
|
+
// Searchable: fetch on init
|
|
49
153
|
@Input() loadOnInit = false;
|
|
154
|
+
|
|
155
|
+
// Searchable: fetch when the panel opens
|
|
50
156
|
@Input() loadOpen = false;
|
|
51
157
|
|
|
52
|
-
|
|
53
|
-
@Input()
|
|
54
|
-
@Input() sort: "ASC" | "DESC" = "ASC";
|
|
55
|
-
@Input() limit: number = 1000;
|
|
158
|
+
// Searchable: default `filter[key]` query params
|
|
159
|
+
@Input() defaultFilters: DropdownSelectDefaultFilters = {};
|
|
56
160
|
|
|
57
|
-
|
|
58
|
-
@Input()
|
|
161
|
+
// Searchable: extra fields included in API search
|
|
162
|
+
@Input() searchFields: string[] = [];
|
|
59
163
|
|
|
60
|
-
|
|
61
|
-
@Input()
|
|
164
|
+
// Searchable: sort order sent to the API
|
|
165
|
+
@Input() sort: DropdownSortOrder = 'ASC';
|
|
62
166
|
|
|
63
|
-
|
|
64
|
-
@
|
|
167
|
+
// Searchable: max records per request
|
|
168
|
+
@Input() limit = 1000;
|
|
65
169
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
// Selectors
|
|
69
|
-
isColumnSelectorOpen = false
|
|
70
|
-
selectedValue: any = null
|
|
71
|
-
selectedLabel = ""
|
|
72
|
-
internalOptions: Array<{ value: any; text: string }> = [];
|
|
170
|
+
// Searchable panel: show inline search input
|
|
171
|
+
@Input() isSearch = true;
|
|
73
172
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
private readonly searchSubject = new Subject<string>();
|
|
77
|
-
private searchSubscription?: Subscription;
|
|
78
|
-
filteredOptions: Array<{ value: any; text: string; original?: any }> = [];
|
|
173
|
+
/** @deprecated Kept for backward compatibility; panel positioning no longer uses this flag. */
|
|
174
|
+
@Input() isFilterSelect = false;
|
|
79
175
|
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
dropdownLeft = 0;
|
|
83
|
-
dropdownWidth = 0;
|
|
176
|
+
// Extra Tailwind classes on trigger and panel shell
|
|
177
|
+
@Input() classes = '';
|
|
84
178
|
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
private onTouched: any = () => { };
|
|
179
|
+
// Extra conditional classes on the trigger button
|
|
180
|
+
@Input() ngClasses: Record<string, boolean> = {};
|
|
88
181
|
|
|
89
|
-
|
|
90
|
-
|
|
182
|
+
/** Emits the selected raw value or `null` when cleared. */
|
|
183
|
+
@Output() selectionChange = new EventEmitter<unknown>();
|
|
91
184
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
_finalEndpoint = "";
|
|
185
|
+
/** Searchable: emits the full API result array after each load. */
|
|
186
|
+
@Output() fullData = new EventEmitter<unknown[]>();
|
|
95
187
|
|
|
188
|
+
/** CRUD endpoint; supports `{param}` placeholders resolved via `dynamicParams`. */
|
|
96
189
|
@Input() set endpoint(value: string) {
|
|
97
|
-
this.
|
|
98
|
-
this.rebuildEndpoint()
|
|
190
|
+
this.rawEndpoint = value;
|
|
191
|
+
this.rebuildEndpoint();
|
|
99
192
|
}
|
|
100
193
|
|
|
101
194
|
get endpoint(): string {
|
|
102
|
-
return this.
|
|
195
|
+
return this.rawEndpoint;
|
|
103
196
|
}
|
|
104
197
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
this.
|
|
109
|
-
this.rebuildEndpoint()
|
|
110
|
-
}
|
|
111
|
-
get dynamicParams(): { [key: string]: any } {
|
|
112
|
-
return this._dynamicParams
|
|
198
|
+
/** Values substituted into `{param}` segments of `endpoint`. */
|
|
199
|
+
@Input() set dynamicParams(value: DropdownSelectDynamicParams) {
|
|
200
|
+
this.endpointDynamicParams = value;
|
|
201
|
+
this.rebuildEndpoint();
|
|
113
202
|
}
|
|
114
203
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
this._finalEndpoint = this._rawEndpoint.replace(/\{([^}]+)\}/g, (_, key) => {
|
|
118
|
-
return this._dynamicParams?.[key] ?? ""
|
|
119
|
-
})
|
|
204
|
+
get dynamicParams(): DropdownSelectDynamicParams {
|
|
205
|
+
return this.endpointDynamicParams;
|
|
120
206
|
}
|
|
121
207
|
|
|
208
|
+
// =====================================================
|
|
209
|
+
// View / state
|
|
210
|
+
// =====================================================
|
|
211
|
+
|
|
212
|
+
@ViewChild('selectButton') selectButton!: ElementRef<HTMLElement>;
|
|
213
|
+
@ViewChild('dropdownPanel') dropdownPanel?: ElementRef<HTMLElement>;
|
|
214
|
+
|
|
215
|
+
isColumnSelectorOpen = false;
|
|
216
|
+
selectedValue: unknown = null;
|
|
217
|
+
selectedLabel = '';
|
|
218
|
+
internalOptions: DropdownOption[] = [];
|
|
219
|
+
filteredOptions: DropdownOption[] = [];
|
|
220
|
+
|
|
221
|
+
searchTerm = '';
|
|
222
|
+
dropdownWidth = 0;
|
|
223
|
+
|
|
224
|
+
mainEndpoint = '';
|
|
225
|
+
|
|
226
|
+
/** Resolved endpoint after `{param}` substitution (used by searchable mode). */
|
|
227
|
+
_finalEndpoint = '';
|
|
228
|
+
|
|
229
|
+
private rawEndpoint = '';
|
|
230
|
+
private endpointDynamicParams: DropdownSelectDynamicParams = {};
|
|
231
|
+
|
|
232
|
+
private readonly searchSubject = new Subject<string>();
|
|
233
|
+
private searchSubscription?: Subscription;
|
|
234
|
+
private clickOutsideListener?: (event: MouseEvent) => void;
|
|
235
|
+
|
|
236
|
+
private onChange: (value: unknown) => void = () => {};
|
|
237
|
+
private onTouched: () => void = () => {};
|
|
238
|
+
|
|
239
|
+
private readonly panelPositionConfig = DROPDOWN_PANEL_DEFAULTS;
|
|
240
|
+
private readonly repositionDropdown = () => {
|
|
241
|
+
if (this.isColumnSelectorOpen) {
|
|
242
|
+
this.updateDropdownPosition();
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
122
246
|
constructor(
|
|
123
|
-
public readonly iconsService: JIconsService,
|
|
124
247
|
private readonly cdr: ChangeDetectorRef,
|
|
125
|
-
private readonly elementRef: ElementRef
|
|
248
|
+
private readonly elementRef: ElementRef<HTMLElement>,
|
|
126
249
|
@Optional() private readonly genericService: JGenericCrudService | null,
|
|
127
|
-
) {
|
|
250
|
+
) {}
|
|
128
251
|
|
|
129
|
-
|
|
130
|
-
|
|
252
|
+
// =====================================================
|
|
253
|
+
// Lifecycle
|
|
254
|
+
// =====================================================
|
|
131
255
|
|
|
132
|
-
|
|
133
|
-
this.
|
|
134
|
-
if (this.type === "searchable") {
|
|
135
|
-
this.loadData()
|
|
136
|
-
}
|
|
137
|
-
})
|
|
256
|
+
ngOnInit(): void {
|
|
257
|
+
this.mainEndpoint = this.endpoint.split('/')[0] ?? this.endpoint;
|
|
138
258
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
259
|
+
this.searchSubscription = this.searchSubject
|
|
260
|
+
.pipe(debounceTime(1000), distinctUntilChanged())
|
|
261
|
+
.subscribe(() => {
|
|
262
|
+
if (this.type === 'searchable') {
|
|
263
|
+
this.loadData();
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
if (this.loadOnInit && this.type === 'searchable') {
|
|
268
|
+
this.loadData();
|
|
142
269
|
}
|
|
143
270
|
|
|
144
|
-
|
|
145
|
-
this.updateSelectedLabel()
|
|
271
|
+
this.updateSelectedLabel();
|
|
146
272
|
}
|
|
147
273
|
|
|
148
|
-
ngAfterViewInit() {
|
|
149
|
-
this.setupClickOutsideListener()
|
|
150
|
-
setTimeout(() =>
|
|
151
|
-
this.processOptions()
|
|
152
|
-
})
|
|
274
|
+
ngAfterViewInit(): void {
|
|
275
|
+
this.setupClickOutsideListener();
|
|
276
|
+
setTimeout(() => this.processOptions());
|
|
153
277
|
}
|
|
154
278
|
|
|
155
279
|
ngOnChanges(changes: SimpleChanges): void {
|
|
156
|
-
if (changes[
|
|
157
|
-
this.processOptions()
|
|
280
|
+
if (changes['options']) {
|
|
281
|
+
this.processOptions();
|
|
158
282
|
}
|
|
159
283
|
}
|
|
160
284
|
|
|
161
|
-
ngOnDestroy() {
|
|
285
|
+
ngOnDestroy(): void {
|
|
286
|
+
this.removeRepositionListeners();
|
|
287
|
+
|
|
162
288
|
if (this.clickOutsideListener) {
|
|
163
|
-
document.removeEventListener(
|
|
164
|
-
}
|
|
165
|
-
if (this.searchSubscription) {
|
|
166
|
-
this.searchSubscription.unsubscribe()
|
|
289
|
+
document.removeEventListener('click', this.clickOutsideListener);
|
|
167
290
|
}
|
|
291
|
+
|
|
292
|
+
this.searchSubscription?.unsubscribe();
|
|
168
293
|
}
|
|
169
294
|
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
295
|
+
// =====================================================
|
|
296
|
+
// ControlValueAccessor
|
|
297
|
+
// =====================================================
|
|
173
298
|
|
|
174
299
|
/**
|
|
175
|
-
*
|
|
300
|
+
* Writes the value from Angular forms into the component.
|
|
301
|
+
* @param value Selected option value or `null`.
|
|
176
302
|
*/
|
|
177
|
-
|
|
178
|
-
this.
|
|
179
|
-
|
|
180
|
-
if (this.
|
|
181
|
-
this.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const text = Array.isArray(this.optionLabel)
|
|
188
|
-
? this.optionLabel.map((k) => this.getNestedValue(option, k)).join(" ")
|
|
189
|
-
: this.getNestedValue(option, this.optionLabel)
|
|
190
|
-
return {
|
|
191
|
-
value: option[this.optionValue],
|
|
192
|
-
text,
|
|
193
|
-
original: option,
|
|
194
|
-
}
|
|
195
|
-
})
|
|
303
|
+
writeValue(value: unknown): void {
|
|
304
|
+
this.selectedValue = value;
|
|
305
|
+
|
|
306
|
+
if (this.internalOptions.length > 0) {
|
|
307
|
+
this.updateSelectedLabel();
|
|
308
|
+
} else {
|
|
309
|
+
setTimeout(() => {
|
|
310
|
+
this.updateSelectedLabel();
|
|
311
|
+
this.cdr.detectChanges();
|
|
312
|
+
});
|
|
196
313
|
}
|
|
197
314
|
|
|
198
|
-
this.
|
|
199
|
-
this.updateSelectedLabel()
|
|
200
|
-
this.cdr.detectChanges()
|
|
315
|
+
this.cdr.markForCheck();
|
|
201
316
|
}
|
|
202
317
|
|
|
203
|
-
|
|
204
|
-
|
|
205
318
|
/**
|
|
206
|
-
*
|
|
207
|
-
* @param
|
|
319
|
+
* Registers the change callback for Angular forms.
|
|
320
|
+
* @param fn Callback invoked when the selection changes.
|
|
208
321
|
*/
|
|
209
|
-
|
|
210
|
-
this.
|
|
211
|
-
this.selectedLabel = option.text
|
|
212
|
-
this.onChange(this.selectedValue)
|
|
213
|
-
this.selectionChange.emit(option.original ?? option.value)
|
|
214
|
-
this.isColumnSelectorOpen = false
|
|
322
|
+
registerOnChange(fn: (value: unknown) => void): void {
|
|
323
|
+
this.onChange = fn;
|
|
215
324
|
}
|
|
216
325
|
|
|
326
|
+
/**
|
|
327
|
+
* Registers the touched callback for Angular forms.
|
|
328
|
+
* @param fn Callback invoked when the control is touched.
|
|
329
|
+
*/
|
|
330
|
+
registerOnTouched(fn: () => void): void {
|
|
331
|
+
this.onTouched = fn;
|
|
332
|
+
}
|
|
217
333
|
|
|
334
|
+
// =====================================================
|
|
335
|
+
// Options
|
|
336
|
+
// =====================================================
|
|
218
337
|
|
|
219
338
|
/**
|
|
220
|
-
*
|
|
221
|
-
* @param event
|
|
339
|
+
* Normalizes `options` into internal `{ value, text }` entries.
|
|
222
340
|
*/
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
this.
|
|
227
|
-
|
|
228
|
-
|
|
341
|
+
processOptions(): void {
|
|
342
|
+
this.internalOptions = [];
|
|
343
|
+
|
|
344
|
+
if (this.options?.length > 0 && typeof this.options[0] !== 'object') {
|
|
345
|
+
this.internalOptions = mapPrimitiveOptions(this.options);
|
|
346
|
+
} else if (this.options?.length > 0) {
|
|
347
|
+
this.internalOptions = mapObjectOptions(
|
|
348
|
+
this.options as Record<string, unknown>[],
|
|
349
|
+
this.optionLabel,
|
|
350
|
+
this.optionValue,
|
|
351
|
+
this.labelSeparator,
|
|
352
|
+
);
|
|
353
|
+
}
|
|
229
354
|
|
|
355
|
+
this.filteredOptions = [...this.internalOptions];
|
|
356
|
+
this.updateSelectedLabel();
|
|
357
|
+
this.cdr.detectChanges();
|
|
358
|
+
}
|
|
230
359
|
|
|
360
|
+
/**
|
|
361
|
+
* Selects an option, updates the form model and closes the panel.
|
|
362
|
+
* @param option Normalized option from the panel list.
|
|
363
|
+
*/
|
|
364
|
+
selectOption(option: DropdownOption): void {
|
|
365
|
+
this.selectedValue = option.value;
|
|
366
|
+
this.selectedLabel = option.text;
|
|
367
|
+
this.onChange(this.selectedValue);
|
|
368
|
+
this.selectionChange.emit(option.original ?? option.value);
|
|
369
|
+
this.closePanel();
|
|
370
|
+
}
|
|
231
371
|
|
|
232
372
|
/**
|
|
233
|
-
*
|
|
373
|
+
* Clears the current selection without opening the panel.
|
|
374
|
+
* @param event Click event (stopped to avoid toggling the panel).
|
|
234
375
|
*/
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
this.
|
|
376
|
+
clearSelection(event: Event): void {
|
|
377
|
+
event.stopPropagation();
|
|
378
|
+
this.writeValue(null);
|
|
379
|
+
this.onChange(null);
|
|
380
|
+
this.selectionChange.emit(null);
|
|
238
381
|
}
|
|
239
382
|
|
|
240
383
|
/**
|
|
241
|
-
*
|
|
242
|
-
* @returns
|
|
384
|
+
* Syncs `selectedLabel` with `selectedValue` and `internalOptions`.
|
|
243
385
|
*/
|
|
244
|
-
updateSelectedLabel() {
|
|
386
|
+
updateSelectedLabel(): void {
|
|
245
387
|
if (this.selectedValue === null) {
|
|
246
|
-
this.selectedLabel = this.placeholder
|
|
247
|
-
return
|
|
388
|
+
this.selectedLabel = this.placeholder;
|
|
389
|
+
return;
|
|
248
390
|
}
|
|
249
391
|
|
|
250
|
-
const selectedOption = this.internalOptions.find((
|
|
251
|
-
this.selectedLabel = selectedOption ? selectedOption.text : this.placeholder
|
|
392
|
+
const selectedOption = this.internalOptions.find((option) => option.value === this.selectedValue);
|
|
393
|
+
this.selectedLabel = selectedOption ? selectedOption.text : this.placeholder;
|
|
252
394
|
}
|
|
253
395
|
|
|
254
396
|
/**
|
|
255
|
-
*
|
|
256
|
-
* @
|
|
257
|
-
* @param path
|
|
258
|
-
* @returns
|
|
397
|
+
* Class map for the trigger button (disabled / loading + `ngClasses`).
|
|
398
|
+
* @returns Conditional classes merged with consumer `ngClasses`.
|
|
259
399
|
*/
|
|
260
|
-
|
|
261
|
-
return
|
|
400
|
+
getButtonNgClass(): Record<string, boolean> {
|
|
401
|
+
return {
|
|
402
|
+
'opacity-50 cursor-not-allowed pointer-events-none': this.isDisabled || this.isLoading,
|
|
403
|
+
...this.ngClasses,
|
|
404
|
+
};
|
|
262
405
|
}
|
|
263
406
|
|
|
264
|
-
//
|
|
265
|
-
// Search
|
|
266
|
-
//
|
|
407
|
+
// =====================================================
|
|
408
|
+
// Search / API
|
|
409
|
+
// =====================================================
|
|
267
410
|
|
|
268
411
|
/**
|
|
269
|
-
*
|
|
270
|
-
* @returns
|
|
412
|
+
* Loads options from the CRUD API (searchable mode).
|
|
271
413
|
*/
|
|
272
|
-
loadData() {
|
|
273
|
-
if (!this.endpoint || !this.genericService)
|
|
414
|
+
loadData(): void {
|
|
415
|
+
if (!this.endpoint || !this.genericService) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
this.isLoading = true;
|
|
274
420
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
421
|
+
const params: Record<string, unknown> = {
|
|
422
|
+
sortOrder: this.sort,
|
|
423
|
+
limit: this.limit,
|
|
424
|
+
};
|
|
279
425
|
|
|
280
|
-
// Apply default filters sent from the parent
|
|
281
426
|
Object.keys(this.defaultFilters).forEach((key) => {
|
|
282
|
-
params[`filter[${key}]`] = this.defaultFilters[key]
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const optionLabelsArray = Array.isArray(this.optionLabel) ? this.optionLabel : [this.optionLabel]
|
|
290
|
-
const allSearchFields = [...optionLabelsArray, ...this.searchFields]
|
|
291
|
-
params["searchFields"] = allSearchFields
|
|
427
|
+
params[`filter[${key}]`] = this.defaultFilters[key];
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
if (this.searchTerm.trim()) {
|
|
431
|
+
params['search'] = this.searchTerm;
|
|
432
|
+
const optionLabels = Array.isArray(this.optionLabel) ? this.optionLabel : [this.optionLabel];
|
|
433
|
+
params['searchFields'] = [...optionLabels, ...this.searchFields];
|
|
292
434
|
}
|
|
293
435
|
|
|
294
|
-
this.genericService.findAll<
|
|
436
|
+
this.genericService.findAll<Record<string, unknown>>({
|
|
437
|
+
endpoint: this._finalEndpoint,
|
|
438
|
+
params,
|
|
439
|
+
}).subscribe({
|
|
295
440
|
next: (response) => {
|
|
296
|
-
|
|
297
|
-
const data = response.data[this.mainEndpoint] || []
|
|
441
|
+
const data = (response.data[this.mainEndpoint] as Record<string, unknown>[]) || [];
|
|
298
442
|
this.options = data;
|
|
299
443
|
|
|
300
|
-
// Add the "ALL" option if required
|
|
301
444
|
if (this.showAllOption) {
|
|
302
|
-
|
|
303
|
-
this.internalOptions = [{
|
|
304
|
-
value: null, // Null value for the "ALL" option
|
|
305
|
-
text: 'TODOS',
|
|
306
|
-
}];
|
|
307
|
-
|
|
308
|
-
// Process the options for the dropdown
|
|
445
|
+
this.internalOptions = [{ value: null, text: 'TODOS' }];
|
|
309
446
|
this.internalOptions.push(
|
|
310
|
-
...this.
|
|
311
|
-
value: option[this.optionValue],
|
|
312
|
-
text: this.resolveLabel(option),
|
|
313
|
-
original: option,
|
|
314
|
-
}))
|
|
447
|
+
...mapObjectOptions(data, this.optionLabel, this.optionValue, this.labelSeparator),
|
|
315
448
|
);
|
|
316
|
-
|
|
317
449
|
} else {
|
|
450
|
+
this.internalOptions = mapObjectOptions(
|
|
451
|
+
data,
|
|
452
|
+
this.optionLabel,
|
|
453
|
+
this.optionValue,
|
|
454
|
+
this.labelSeparator,
|
|
455
|
+
);
|
|
456
|
+
}
|
|
318
457
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
original: option,
|
|
324
|
-
}))
|
|
458
|
+
this.filteredOptions = [...this.internalOptions];
|
|
459
|
+
this.fullData.emit(this.options);
|
|
460
|
+
this.isLoading = false;
|
|
461
|
+
this.updateSelectedLabel();
|
|
325
462
|
|
|
463
|
+
if (this.isColumnSelectorOpen) {
|
|
464
|
+
this.updateDropdownPosition();
|
|
326
465
|
}
|
|
327
466
|
|
|
328
|
-
this.
|
|
329
|
-
this.fullData.emit(this.options)
|
|
330
|
-
this.isLoading = false
|
|
331
|
-
this.updateSelectedLabel()
|
|
332
|
-
this.cdr.detectChanges()
|
|
467
|
+
this.cdr.detectChanges();
|
|
333
468
|
},
|
|
334
469
|
error: (error) => {
|
|
335
|
-
console.error(
|
|
336
|
-
this.isLoading = false
|
|
337
|
-
this.cdr.detectChanges()
|
|
470
|
+
console.error('Error fetching data:', error);
|
|
471
|
+
this.isLoading = false;
|
|
472
|
+
this.cdr.detectChanges();
|
|
338
473
|
},
|
|
339
|
-
})
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Search input handler
|
|
346
|
-
*/
|
|
347
|
-
onSearchInput() {
|
|
348
|
-
if (this.type === "searchable") {
|
|
349
|
-
// For searchable type, send the search term to the service
|
|
350
|
-
this.searchSubject.next(this.searchTerm)
|
|
351
|
-
} else {
|
|
352
|
-
// For other types, filter locally
|
|
353
|
-
this.filterOptions()
|
|
354
|
-
}
|
|
474
|
+
});
|
|
355
475
|
}
|
|
356
476
|
|
|
357
|
-
|
|
358
|
-
|
|
359
477
|
/**
|
|
360
|
-
*
|
|
361
|
-
* @returns
|
|
478
|
+
* Handles panel search input — debounced API call or local filter.
|
|
362
479
|
*/
|
|
363
|
-
|
|
364
|
-
if (
|
|
365
|
-
this.
|
|
366
|
-
return
|
|
480
|
+
onSearchInput(): void {
|
|
481
|
+
if (this.type === 'searchable') {
|
|
482
|
+
this.searchSubject.next(this.searchTerm);
|
|
483
|
+
return;
|
|
367
484
|
}
|
|
368
485
|
|
|
369
|
-
|
|
370
|
-
this.filteredOptions = this.internalOptions.filter((option) => option.text.toLowerCase().includes(searchTermLower))
|
|
486
|
+
this.filteredOptions = filterOptionsBySearch(this.internalOptions, this.searchTerm);
|
|
371
487
|
}
|
|
372
488
|
|
|
489
|
+
/** Resets the panel search field and re-runs {@link onSearchInput}. */
|
|
490
|
+
clearSearchTerm(): void {
|
|
491
|
+
this.searchTerm = '';
|
|
492
|
+
this.onSearchInput();
|
|
493
|
+
}
|
|
373
494
|
|
|
495
|
+
// =====================================================
|
|
496
|
+
// Panel open / position
|
|
497
|
+
// =====================================================
|
|
374
498
|
|
|
375
499
|
/**
|
|
376
|
-
*
|
|
377
|
-
* @
|
|
378
|
-
* @returns
|
|
500
|
+
* Opens or closes the option panel.
|
|
501
|
+
* Searchable mode may trigger {@link loadData} on first open.
|
|
379
502
|
*/
|
|
380
|
-
|
|
381
|
-
if (
|
|
382
|
-
return
|
|
383
|
-
.map((key) => this.getNestedValue(option, key))
|
|
384
|
-
.filter(Boolean)
|
|
385
|
-
.join(this.labelSeparator)
|
|
503
|
+
toggleColumnSelector(): void {
|
|
504
|
+
if (this.isDisabled) {
|
|
505
|
+
return;
|
|
386
506
|
}
|
|
387
|
-
return this.getNestedValue(option, this.optionLabel)
|
|
388
|
-
}
|
|
389
507
|
|
|
390
|
-
|
|
391
|
-
// Elements
|
|
392
|
-
// ======================================================
|
|
508
|
+
this.isColumnSelectorOpen = !this.isColumnSelectorOpen;
|
|
393
509
|
|
|
394
|
-
/**
|
|
395
|
-
* Open or close the dropdown
|
|
396
|
-
* @returns
|
|
397
|
-
*/
|
|
398
|
-
toggleColumnSelector() {
|
|
399
|
-
if (this.isDisabled) return
|
|
400
|
-
|
|
401
|
-
this.isColumnSelectorOpen = !this.isColumnSelectorOpen
|
|
402
510
|
if (this.isColumnSelectorOpen) {
|
|
403
|
-
this.onTouched()
|
|
404
|
-
this.
|
|
511
|
+
this.onTouched();
|
|
512
|
+
this.addRepositionListeners();
|
|
513
|
+
this.updateDropdownPosition();
|
|
405
514
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
this.loadData()
|
|
515
|
+
if (this.type === 'searchable' && this.loadOpen) {
|
|
516
|
+
this.loadData();
|
|
409
517
|
}
|
|
410
518
|
|
|
411
|
-
if (this.type ===
|
|
412
|
-
this.loadData()
|
|
519
|
+
if (this.type === 'searchable' && !this.loadOnInit && this.shouldTriggerLoad()) {
|
|
520
|
+
this.loadData();
|
|
413
521
|
}
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
|
|
418
522
|
|
|
419
|
-
|
|
420
|
-
* Detect clicks outside the dropdown to close it
|
|
421
|
-
*/
|
|
422
|
-
setupClickOutsideListener() {
|
|
423
|
-
this.clickOutsideListener = (event: MouseEvent) => {
|
|
424
|
-
const clickedElement = event.target as HTMLElement
|
|
425
|
-
const isOutsideDropdown = !this.elementRef.nativeElement.contains(clickedElement)
|
|
426
|
-
if (this.isColumnSelectorOpen && isOutsideDropdown) {
|
|
427
|
-
this.isColumnSelectorOpen = false
|
|
428
|
-
this.cdr.detectChanges()
|
|
429
|
-
}
|
|
523
|
+
return;
|
|
430
524
|
}
|
|
431
|
-
document.addEventListener("click", this.clickOutsideListener)
|
|
432
|
-
}
|
|
433
|
-
|
|
434
525
|
|
|
526
|
+
this.closePanel(false);
|
|
527
|
+
}
|
|
435
528
|
|
|
436
529
|
/**
|
|
437
|
-
*
|
|
530
|
+
* Positions the floating panel with fixed viewport coordinates
|
|
531
|
+
* (flip above/below, clamp to window — see {@link positionDropdownPanel}).
|
|
438
532
|
*/
|
|
439
|
-
updateDropdownPosition() {
|
|
533
|
+
updateDropdownPosition(): void {
|
|
440
534
|
setTimeout(() => {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
const button = this.selectButton.nativeElement
|
|
445
|
-
const buttonRect = button.getBoundingClientRect()
|
|
446
|
-
|
|
447
|
-
// Find the closest form container or dialog
|
|
448
|
-
let offsetParent: HTMLElement | null = this.selectButton.nativeElement
|
|
449
|
-
let isInSidebar = false
|
|
450
|
-
|
|
451
|
-
// Check if we're inside a sidebar form
|
|
452
|
-
while (
|
|
453
|
-
offsetParent &&
|
|
454
|
-
!offsetParent.classList.contains("content_form") &&
|
|
455
|
-
!offsetParent.classList.contains("p-dialog")
|
|
456
|
-
) {
|
|
457
|
-
if (offsetParent.classList.contains("fixed") && offsetParent.classList.contains("right-0")) {
|
|
458
|
-
isInSidebar = true
|
|
459
|
-
break
|
|
460
|
-
}
|
|
461
|
-
offsetParent = offsetParent.parentElement
|
|
535
|
+
const button = this.selectButton?.nativeElement;
|
|
536
|
+
if (!button) {
|
|
537
|
+
return;
|
|
462
538
|
}
|
|
463
539
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
if (
|
|
468
|
-
isInSidebar ||
|
|
469
|
-
(offsetParent &&
|
|
470
|
-
(offsetParent.classList.contains("content_form") || offsetParent.classList.contains("p-dialog")))
|
|
471
|
-
) {
|
|
472
|
-
offsetTop = offsetParent ? offsetParent.getBoundingClientRect().top : 0
|
|
473
|
-
offsetLeft = offsetParent ? offsetParent.getBoundingClientRect().left : 0
|
|
474
|
-
}
|
|
540
|
+
const buttonRect = button.getBoundingClientRect();
|
|
541
|
+
this.dropdownWidth = buttonRect.width;
|
|
542
|
+
this.cdr.detectChanges();
|
|
475
543
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
544
|
+
requestAnimationFrame(() => {
|
|
545
|
+
const panel = this.dropdownPanel?.nativeElement;
|
|
546
|
+
if (!panel) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
481
549
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
if (!dropdown) return
|
|
487
|
-
|
|
488
|
-
// First use fixed positioning to handle scroll position correctly
|
|
489
|
-
dropdown.style.position = "fixed"
|
|
490
|
-
dropdown.style.top = buttonRect.bottom + "px"
|
|
491
|
-
dropdown.style.left = buttonRect.left + "px"
|
|
492
|
-
dropdown.style.width = buttonRect.width + "px"
|
|
493
|
-
dropdown.style.zIndex = "600"
|
|
494
|
-
|
|
495
|
-
// Wait for dropdown to render with fixed positioning
|
|
496
|
-
setTimeout(() => {
|
|
497
|
-
// Get dropdown dimensions
|
|
498
|
-
const dropdownRect = dropdown.getBoundingClientRect()
|
|
499
|
-
const viewportHeight = window.innerHeight
|
|
500
|
-
const documentWidth = document.documentElement.clientWidth
|
|
501
|
-
|
|
502
|
-
// Determine if we need to flip or adjust position
|
|
503
|
-
let newFixedTop = buttonRect.bottom
|
|
504
|
-
let newFixedLeft = buttonRect.left
|
|
505
|
-
|
|
506
|
-
// Check if dropdown goes below viewport and flip it if needed
|
|
507
|
-
if (buttonRect.bottom + dropdownRect.height > viewportHeight) {
|
|
508
|
-
newFixedTop = buttonRect.top - dropdownRect.height
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// Check if dropdown goes beyond right edge
|
|
512
|
-
if (buttonRect.left + dropdownRect.width > documentWidth) {
|
|
513
|
-
newFixedLeft = buttonRect.left - dropdownRect.width + buttonRect.width
|
|
514
|
-
if (newFixedLeft < 0) {
|
|
515
|
-
newFixedLeft = 5
|
|
516
|
-
if (dropdownRect.width > documentWidth) {
|
|
517
|
-
dropdown.classList.add("constrain-width")
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
// Apply adjusted fixed position
|
|
523
|
-
dropdown.style.top = newFixedTop - 5 + "px"
|
|
524
|
-
dropdown.style.left = newFixedLeft - 15 + "px"
|
|
525
|
-
|
|
526
|
-
// Button filter
|
|
527
|
-
if (this.isFilterSelect) {
|
|
528
|
-
setTimeout(() => {
|
|
529
|
-
// Calculate absolute position based on the current fixed position
|
|
530
|
-
let newAbsoluteTop
|
|
531
|
-
let newAbsoluteLeft
|
|
532
|
-
|
|
533
|
-
if (newFixedTop === buttonRect.bottom) {
|
|
534
|
-
// Dropdown is below the button
|
|
535
|
-
newAbsoluteTop = this.dropdownTop
|
|
536
|
-
} else {
|
|
537
|
-
// Dropdown is above the button (flipped)
|
|
538
|
-
newAbsoluteTop = buttonRect.top - dropdownRect.height - offsetTop
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
if (newFixedLeft === buttonRect.left) {
|
|
542
|
-
// Dropdown is aligned with left edge of button
|
|
543
|
-
newAbsoluteLeft = this.dropdownLeft
|
|
544
|
-
} else if (newFixedLeft === buttonRect.left - dropdownRect.width + buttonRect.width) {
|
|
545
|
-
// Dropdown is aligned with right edge of button
|
|
546
|
-
newAbsoluteLeft = buttonRect.left - dropdownRect.width + buttonRect.width - offsetLeft
|
|
547
|
-
} else {
|
|
548
|
-
// Dropdown is at a fixed position from left edge
|
|
549
|
-
newAbsoluteLeft = newFixedLeft
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// Switch to absolute positioning
|
|
553
|
-
dropdown.style.position = "absolute"
|
|
554
|
-
dropdown.style.top = newAbsoluteTop + "px"
|
|
555
|
-
dropdown.style.left = newAbsoluteLeft - 2 + "px"
|
|
556
|
-
|
|
557
|
-
// Update internal state
|
|
558
|
-
this.dropdownTop = newAbsoluteTop
|
|
559
|
-
this.dropdownLeft = newAbsoluteLeft
|
|
560
|
-
this.cdr.detectChanges()
|
|
561
|
-
}, 0)
|
|
562
|
-
} else {
|
|
563
|
-
// After positioning is done, switch to absolute positioning
|
|
564
|
-
setTimeout(() => {
|
|
565
|
-
// Get the current visual position of the dropdown (relative to viewport)
|
|
566
|
-
const dropdownRect = dropdown.getBoundingClientRect()
|
|
567
|
-
|
|
568
|
-
// Find the dropdown's offset parent for absolute positioning
|
|
569
|
-
const dropdownOffsetParent = this.findPositionedParent(dropdown) || document.body
|
|
570
|
-
const parentRect = dropdownOffsetParent.getBoundingClientRect()
|
|
571
|
-
|
|
572
|
-
// Calculate absolute position that will maintain the same visual position
|
|
573
|
-
// Absolute positioning is relative to the offset parent
|
|
574
|
-
const absoluteTop = dropdownRect.top - parentRect.top + dropdownOffsetParent.scrollTop
|
|
575
|
-
const absoluteLeft = dropdownRect.left - parentRect.left + dropdownOffsetParent.scrollLeft
|
|
576
|
-
|
|
577
|
-
// Switch to absolute positioning with calculated coordinates
|
|
578
|
-
dropdown.style.position = "absolute"
|
|
579
|
-
dropdown.style.top = absoluteTop + "px"
|
|
580
|
-
dropdown.style.left = absoluteLeft + "px"
|
|
581
|
-
|
|
582
|
-
// Update internal state
|
|
583
|
-
this.dropdownTop = absoluteTop
|
|
584
|
-
this.dropdownLeft = absoluteLeft
|
|
585
|
-
this.cdr.detectChanges()
|
|
586
|
-
}, 0)
|
|
587
|
-
}
|
|
588
|
-
}, 0)
|
|
589
|
-
}, 0)
|
|
590
|
-
})
|
|
550
|
+
positionDropdownPanel(panel, buttonRect, this.panelPositionConfig);
|
|
551
|
+
this.cdr.detectChanges();
|
|
552
|
+
});
|
|
553
|
+
});
|
|
591
554
|
}
|
|
592
555
|
|
|
593
|
-
/**
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
private findPositionedParent(element: HTMLElement): HTMLElement | null {
|
|
599
|
-
if (!element) return null
|
|
600
|
-
let parent = element.parentElement
|
|
601
|
-
while (parent) {
|
|
602
|
-
const position = window.getComputedStyle(parent).position
|
|
603
|
-
if (position === "relative" || position === "absolute" || position === "fixed") {
|
|
604
|
-
return parent
|
|
605
|
-
}
|
|
606
|
-
parent = parent.parentElement
|
|
607
|
-
}
|
|
608
|
-
return document.body // Default to body if no positioned parent found
|
|
609
|
-
}
|
|
556
|
+
/** Document click listener — closes the panel when clicking outside. */
|
|
557
|
+
private setupClickOutsideListener(): void {
|
|
558
|
+
this.clickOutsideListener = (event: MouseEvent) => {
|
|
559
|
+
const clickedElement = event.target as HTMLElement;
|
|
560
|
+
const isOutsideDropdown = !this.elementRef.nativeElement.contains(clickedElement);
|
|
610
561
|
|
|
562
|
+
if (this.isColumnSelectorOpen && isOutsideDropdown) {
|
|
563
|
+
this.closePanel();
|
|
564
|
+
}
|
|
565
|
+
};
|
|
611
566
|
|
|
567
|
+
document.addEventListener('click', this.clickOutsideListener);
|
|
568
|
+
}
|
|
612
569
|
|
|
613
|
-
/**
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
this.selectedValue = value
|
|
570
|
+
/** Closes the panel and clears reposition listeners / dynamic panel styles. */
|
|
571
|
+
private closePanel(detectChanges = true): void {
|
|
572
|
+
this.isColumnSelectorOpen = false;
|
|
573
|
+
this.removeRepositionListeners();
|
|
574
|
+
this.resetDropdownPanelStyles();
|
|
619
575
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
this.updateSelectedLabel()
|
|
623
|
-
} else {
|
|
624
|
-
// Wait for options to load and then update the label
|
|
625
|
-
setTimeout(() => {
|
|
626
|
-
this.updateSelectedLabel()
|
|
627
|
-
this.cdr.detectChanges()
|
|
628
|
-
})
|
|
576
|
+
if (detectChanges) {
|
|
577
|
+
this.cdr.detectChanges();
|
|
629
578
|
}
|
|
630
|
-
this.cdr.markForCheck()
|
|
631
579
|
}
|
|
632
580
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
/**
|
|
638
|
-
* Register the change of the selected value
|
|
639
|
-
* @param fn
|
|
640
|
-
*/
|
|
641
|
-
registerOnChange(fn: any): void {
|
|
642
|
-
this.onChange = fn
|
|
581
|
+
/** Re-applies panel position on scroll (capture) and window resize. */
|
|
582
|
+
private addRepositionListeners(): void {
|
|
583
|
+
window.addEventListener('scroll', this.repositionDropdown, true);
|
|
584
|
+
window.addEventListener('resize', this.repositionDropdown);
|
|
643
585
|
}
|
|
644
586
|
|
|
587
|
+
/** Removes scroll/resize listeners registered while the panel is open. */
|
|
588
|
+
private removeRepositionListeners(): void {
|
|
589
|
+
window.removeEventListener('scroll', this.repositionDropdown, true);
|
|
590
|
+
window.removeEventListener('resize', this.repositionDropdown);
|
|
591
|
+
}
|
|
645
592
|
|
|
593
|
+
/** Resets dynamic max-height on the scrollable options list. */
|
|
594
|
+
private resetDropdownPanelStyles(): void {
|
|
595
|
+
const panel = this.dropdownPanel?.nativeElement;
|
|
596
|
+
if (!panel) {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
646
599
|
|
|
647
|
-
|
|
648
|
-
* Register the touch event
|
|
649
|
-
* @param fn
|
|
650
|
-
*/
|
|
651
|
-
registerOnTouched(fn: any): void {
|
|
652
|
-
this.onTouched = fn
|
|
600
|
+
resetDropdownPanelStyles(panel, this.panelPositionConfig.optionsListSelector);
|
|
653
601
|
}
|
|
654
602
|
|
|
603
|
+
/** Builds `_finalEndpoint` from `endpoint` + `dynamicParams`. */
|
|
604
|
+
private rebuildEndpoint(): void {
|
|
605
|
+
if (!this.rawEndpoint) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
655
608
|
|
|
656
|
-
|
|
657
|
-
/**
|
|
658
|
-
* Load searchable elements when needed
|
|
659
|
-
* @returns
|
|
660
|
-
*/
|
|
661
|
-
private shouldTriggerLoad(): boolean {
|
|
662
|
-
const isSearchActive = this.searchTerm && this.searchTerm.trim() !== ""
|
|
663
|
-
const isInitialState = this.selectedValue === null
|
|
664
|
-
return isSearchActive || isInitialState
|
|
609
|
+
this._finalEndpoint = rebuildEndpointUrl(this.rawEndpoint, this.endpointDynamicParams);
|
|
665
610
|
}
|
|
666
611
|
|
|
667
|
-
/**
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
return {
|
|
673
|
-
'opacity-50 cursor-not-allowed pointer-events-none': this.isDisabled || this.isLoading,
|
|
674
|
-
...this.ngClasses,
|
|
675
|
-
};
|
|
612
|
+
/** Whether searchable mode should fetch on first panel open. */
|
|
613
|
+
private shouldTriggerLoad(): boolean {
|
|
614
|
+
const isSearchActive = this.searchTerm.trim().length > 0;
|
|
615
|
+
const isInitialState = this.selectedValue === null;
|
|
616
|
+
return isSearchActive || isInitialState;
|
|
676
617
|
}
|
|
677
|
-
}
|
|
618
|
+
}
|