@memberjunction/ng-trees 0.0.1 → 3.1.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/lib/events/tree-events.d.ts +267 -0
- package/dist/lib/events/tree-events.d.ts.map +1 -0
- package/dist/lib/events/tree-events.js +383 -0
- package/dist/lib/events/tree-events.js.map +1 -0
- package/dist/lib/models/tree-types.d.ts +254 -0
- package/dist/lib/models/tree-types.d.ts.map +1 -0
- package/dist/lib/models/tree-types.js +57 -0
- package/dist/lib/models/tree-types.js.map +1 -0
- package/dist/lib/ng-trees.module.d.ts +16 -0
- package/dist/lib/ng-trees.module.d.ts.map +1 -0
- package/dist/lib/ng-trees.module.js +47 -0
- package/dist/lib/ng-trees.module.js.map +1 -0
- package/dist/lib/tree/tree.component.d.ts +353 -0
- package/dist/lib/tree/tree.component.d.ts.map +1 -0
- package/dist/lib/tree/tree.component.js +1695 -0
- package/dist/lib/tree/tree.component.js.map +1 -0
- package/dist/lib/tree-dropdown/tree-dropdown.component.d.ts +295 -0
- package/dist/lib/tree-dropdown/tree-dropdown.component.d.ts.map +1 -0
- package/dist/lib/tree-dropdown/tree-dropdown.component.js +1012 -0
- package/dist/lib/tree-dropdown/tree-dropdown.component.js.map +1 -0
- package/dist/public-api.d.ts +9 -0
- package/dist/public-api.d.ts.map +1 -0
- package/dist/public-api.js +16 -0
- package/dist/public-api.js.map +1 -0
- package/package.json +40 -6
- package/README.md +0 -45
|
@@ -0,0 +1,1012 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree Dropdown Component for @memberjunction/ng-trees
|
|
3
|
+
*
|
|
4
|
+
* A searchable dropdown with tree selection. Features:
|
|
5
|
+
* - Smart positioning (auto-flips above/below based on available space)
|
|
6
|
+
* - Portal rendering to avoid clipping
|
|
7
|
+
* - Type-ahead search with highlighting
|
|
8
|
+
* - Keyboard navigation
|
|
9
|
+
* - Single and multi-select modes
|
|
10
|
+
*/
|
|
11
|
+
import { Component, Input, Output, EventEmitter, ViewChild } from '@angular/core';
|
|
12
|
+
import { BeforeSearchEventArgs, AfterSearchEventArgs, BeforeDropdownOpenEventArgs, AfterDropdownOpenEventArgs, BeforeDropdownCloseEventArgs, AfterDropdownCloseEventArgs } from '../events/tree-events';
|
|
13
|
+
import { Subject } from 'rxjs';
|
|
14
|
+
import { debounceTime, takeUntil } from 'rxjs/operators';
|
|
15
|
+
import { Metadata, CompositeKey } from '@memberjunction/core';
|
|
16
|
+
import * as i0 from "@angular/core";
|
|
17
|
+
import * as i1 from "@angular/common";
|
|
18
|
+
import * as i2 from "../tree/tree.component";
|
|
19
|
+
const _c0 = ["triggerElement"];
|
|
20
|
+
const _c1 = ["searchInput"];
|
|
21
|
+
const _c2 = ["dropdownPanel"];
|
|
22
|
+
const _c3 = ["treeComponent"];
|
|
23
|
+
const _c4 = () => ({});
|
|
24
|
+
function TreeDropdownComponent_span_4_Template(rf, ctx) { if (rf & 1) {
|
|
25
|
+
i0.ɵɵelementStart(0, "span", 15);
|
|
26
|
+
i0.ɵɵelement(1, "i");
|
|
27
|
+
i0.ɵɵelementEnd();
|
|
28
|
+
} if (rf & 2) {
|
|
29
|
+
const ctx_r1 = i0.ɵɵnextContext();
|
|
30
|
+
i0.ɵɵstyleProp("color", ctx_r1.getDisplayColor());
|
|
31
|
+
i0.ɵɵadvance();
|
|
32
|
+
i0.ɵɵclassMap(ctx_r1.getDisplayIcon());
|
|
33
|
+
} }
|
|
34
|
+
function TreeDropdownComponent_span_7_Template(rf, ctx) { if (rf & 1) {
|
|
35
|
+
i0.ɵɵelementStart(0, "span", 16);
|
|
36
|
+
i0.ɵɵelement(1, "i", 17);
|
|
37
|
+
i0.ɵɵelementEnd();
|
|
38
|
+
} }
|
|
39
|
+
function TreeDropdownComponent_button_9_Template(rf, ctx) { if (rf & 1) {
|
|
40
|
+
const _r3 = i0.ɵɵgetCurrentView();
|
|
41
|
+
i0.ɵɵelementStart(0, "button", 18);
|
|
42
|
+
i0.ɵɵlistener("click", function TreeDropdownComponent_button_9_Template_button_click_0_listener($event) { i0.ɵɵrestoreView(_r3); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.Clear($event)); });
|
|
43
|
+
i0.ɵɵelement(1, "i", 19);
|
|
44
|
+
i0.ɵɵelementEnd();
|
|
45
|
+
} }
|
|
46
|
+
function TreeDropdownComponent_div_12_div_2_button_5_Template(rf, ctx) { if (rf & 1) {
|
|
47
|
+
const _r6 = i0.ɵɵgetCurrentView();
|
|
48
|
+
i0.ɵɵelementStart(0, "button", 29);
|
|
49
|
+
i0.ɵɵlistener("click", function TreeDropdownComponent_div_12_div_2_button_5_Template_button_click_0_listener() { i0.ɵɵrestoreView(_r6); const ctx_r1 = i0.ɵɵnextContext(3); return i0.ɵɵresetView(ctx_r1.onClearSearch()); });
|
|
50
|
+
i0.ɵɵelement(1, "i", 19);
|
|
51
|
+
i0.ɵɵelementEnd();
|
|
52
|
+
} }
|
|
53
|
+
function TreeDropdownComponent_div_12_div_2_Template(rf, ctx) { if (rf & 1) {
|
|
54
|
+
const _r5 = i0.ɵɵgetCurrentView();
|
|
55
|
+
i0.ɵɵelementStart(0, "div", 24)(1, "div", 25);
|
|
56
|
+
i0.ɵɵelement(2, "i", 26);
|
|
57
|
+
i0.ɵɵelementStart(3, "input", 27, 3);
|
|
58
|
+
i0.ɵɵlistener("input", function TreeDropdownComponent_div_12_div_2_Template_input_input_3_listener($event) { i0.ɵɵrestoreView(_r5); const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.onSearchInput($event)); })("keydown", function TreeDropdownComponent_div_12_div_2_Template_input_keydown_3_listener($event) { i0.ɵɵrestoreView(_r5); const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.onSearchKeyDown($event)); });
|
|
59
|
+
i0.ɵɵelementEnd();
|
|
60
|
+
i0.ɵɵtemplate(5, TreeDropdownComponent_div_12_div_2_button_5_Template, 2, 0, "button", 28);
|
|
61
|
+
i0.ɵɵelementEnd()();
|
|
62
|
+
} if (rf & 2) {
|
|
63
|
+
const ctx_r1 = i0.ɵɵnextContext(2);
|
|
64
|
+
i0.ɵɵadvance(3);
|
|
65
|
+
i0.ɵɵproperty("placeholder", ctx_r1.SearchConfig.Placeholder || "Type to search...")("value", ctx_r1.SearchText);
|
|
66
|
+
i0.ɵɵadvance(2);
|
|
67
|
+
i0.ɵɵproperty("ngIf", ctx_r1.SearchText);
|
|
68
|
+
} }
|
|
69
|
+
function TreeDropdownComponent_div_12_Template(rf, ctx) { if (rf & 1) {
|
|
70
|
+
const _r4 = i0.ɵɵgetCurrentView();
|
|
71
|
+
i0.ɵɵelementStart(0, "div", 20, 1);
|
|
72
|
+
i0.ɵɵtemplate(2, TreeDropdownComponent_div_12_div_2_Template, 6, 3, "div", 21);
|
|
73
|
+
i0.ɵɵelementStart(3, "div", 22)(4, "mj-tree", 23, 2);
|
|
74
|
+
i0.ɵɵlistener("SelectionChange", function TreeDropdownComponent_div_12_Template_mj_tree_SelectionChange_4_listener($event) { i0.ɵɵrestoreView(_r4); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onTreeSelectionChange($event)); })("BeforeNodeSelect", function TreeDropdownComponent_div_12_Template_mj_tree_BeforeNodeSelect_4_listener($event) { i0.ɵɵrestoreView(_r4); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onTreeBeforeNodeSelect($event)); })("AfterNodeSelect", function TreeDropdownComponent_div_12_Template_mj_tree_AfterNodeSelect_4_listener($event) { i0.ɵɵrestoreView(_r4); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onTreeAfterNodeSelect($event)); })("BeforeDataLoad", function TreeDropdownComponent_div_12_Template_mj_tree_BeforeDataLoad_4_listener($event) { i0.ɵɵrestoreView(_r4); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onTreeBeforeDataLoad($event)); })("AfterDataLoad", function TreeDropdownComponent_div_12_Template_mj_tree_AfterDataLoad_4_listener($event) { i0.ɵɵrestoreView(_r4); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.onTreeAfterDataLoad($event)); });
|
|
75
|
+
i0.ɵɵelementEnd()()();
|
|
76
|
+
} if (rf & 2) {
|
|
77
|
+
const ctx_r1 = i0.ɵɵnextContext();
|
|
78
|
+
i0.ɵɵclassMap(ctx_r1.StyleConfig.DropdownClass || "");
|
|
79
|
+
i0.ɵɵclassProp("tree-dropdown-panel--open", ctx_r1.IsOpen)("tree-dropdown-panel--above", ctx_r1.Position == null ? null : ctx_r1.Position.renderAbove)("tree-dropdown-panel--below", !(ctx_r1.Position == null ? null : ctx_r1.Position.renderAbove));
|
|
80
|
+
i0.ɵɵproperty("ngStyle", ctx_r1.IsOpen ? ctx_r1.getDropdownStyles() : i0.ɵɵpureFunction0(21, _c4));
|
|
81
|
+
i0.ɵɵadvance(2);
|
|
82
|
+
i0.ɵɵproperty("ngIf", ctx_r1.EnableSearch);
|
|
83
|
+
i0.ɵɵadvance(2);
|
|
84
|
+
i0.ɵɵproperty("BranchConfig", ctx_r1.BranchConfig)("LeafConfig", ctx_r1.LeafConfig)("SelectionMode", ctx_r1.SelectionMode)("SelectableTypes", ctx_r1.SelectableTypes)("SelectedIDs", ctx_r1.getSelectedIDsArray())("AutoLoad", ctx_r1.AutoLoad)("ShowIcons", true)("ShowExpandCollapseAll", false)("AnimateExpandCollapse", true)("EmptyMessage", ctx_r1.SearchText ? "No matches found" : "No items available")("EmptyIcon", ctx_r1.SearchText ? "fa-solid fa-search" : "fa-solid fa-folder-open");
|
|
85
|
+
} }
|
|
86
|
+
export class TreeDropdownComponent {
|
|
87
|
+
cdr;
|
|
88
|
+
renderer;
|
|
89
|
+
// ========================================
|
|
90
|
+
// Tree Configuration (passed to inner tree)
|
|
91
|
+
// ========================================
|
|
92
|
+
/** Branch (category) entity configuration - REQUIRED */
|
|
93
|
+
BranchConfig;
|
|
94
|
+
/** Optional leaf entity configuration */
|
|
95
|
+
LeafConfig;
|
|
96
|
+
/** Selection mode: 'single' or 'multiple' */
|
|
97
|
+
SelectionMode = 'single';
|
|
98
|
+
/** What types can be selected: 'branch', 'leaf', or 'both' */
|
|
99
|
+
SelectableTypes = 'both';
|
|
100
|
+
// ========================================
|
|
101
|
+
// Value Inputs
|
|
102
|
+
// ========================================
|
|
103
|
+
/** Current selected value (CompositeKey for single, CompositeKeys for multiple) */
|
|
104
|
+
_value = null;
|
|
105
|
+
/**
|
|
106
|
+
* The selected value as a CompositeKey (single select) or array of CompositeKeys (multi-select).
|
|
107
|
+
* CompositeKey supports both simple single-field primary keys and composite primary keys.
|
|
108
|
+
*
|
|
109
|
+
* @example Single select with simple ID:
|
|
110
|
+
* ```typescript
|
|
111
|
+
* dropdown.Value = CompositeKey.FromID('some-guid');
|
|
112
|
+
* ```
|
|
113
|
+
*
|
|
114
|
+
* @example Single select with composite key:
|
|
115
|
+
* ```typescript
|
|
116
|
+
* dropdown.Value = new CompositeKey([
|
|
117
|
+
* { FieldName: 'Field1', Value: 'value1' },
|
|
118
|
+
* { FieldName: 'Field2', Value: 'value2' }
|
|
119
|
+
* ]);
|
|
120
|
+
* ```
|
|
121
|
+
*
|
|
122
|
+
* @example Multi-select:
|
|
123
|
+
* ```typescript
|
|
124
|
+
* dropdown.Value = [
|
|
125
|
+
* CompositeKey.FromID('guid1'),
|
|
126
|
+
* CompositeKey.FromID('guid2')
|
|
127
|
+
* ];
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
set Value(val) {
|
|
131
|
+
if (!CompositeKey.EqualsEx(val, this._value)) {
|
|
132
|
+
this._value = val;
|
|
133
|
+
// If tree is loaded, sync selection immediately
|
|
134
|
+
if (this.IsLoaded && this.treeComponent) {
|
|
135
|
+
this.syncValueToSelection();
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
// Tree not loaded yet - fetch display text directly via Metadata
|
|
139
|
+
this.fetchDisplayTextForValue(val);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
get Value() {
|
|
144
|
+
return this._value;
|
|
145
|
+
}
|
|
146
|
+
/** Cached display text for showing in trigger before tree loads */
|
|
147
|
+
_pendingDisplayText = null;
|
|
148
|
+
// ========================================
|
|
149
|
+
// Dropdown-specific Inputs
|
|
150
|
+
// ========================================
|
|
151
|
+
/** Placeholder text when nothing selected */
|
|
152
|
+
Placeholder = 'Select...';
|
|
153
|
+
/** Enable search filtering */
|
|
154
|
+
EnableSearch = true;
|
|
155
|
+
/** Search configuration */
|
|
156
|
+
SearchConfig = {};
|
|
157
|
+
/** Dropdown configuration */
|
|
158
|
+
DropdownConfig = {};
|
|
159
|
+
/** Style configuration */
|
|
160
|
+
StyleConfig = {};
|
|
161
|
+
/** Show clear button */
|
|
162
|
+
Clearable = true;
|
|
163
|
+
/** Disabled state */
|
|
164
|
+
Disabled = false;
|
|
165
|
+
/** Show node icons in display */
|
|
166
|
+
ShowIconInDisplay = true;
|
|
167
|
+
/** Auto-load data on init */
|
|
168
|
+
AutoLoad = true;
|
|
169
|
+
/** Show loading in trigger */
|
|
170
|
+
ShowLoadingInTrigger = true;
|
|
171
|
+
// ========================================
|
|
172
|
+
// Event Outputs
|
|
173
|
+
// ========================================
|
|
174
|
+
/** Emitted when value changes */
|
|
175
|
+
ValueChange = new EventEmitter();
|
|
176
|
+
/** Emitted with full node(s) when selection changes */
|
|
177
|
+
SelectionChange = new EventEmitter();
|
|
178
|
+
// Bubble up tree events
|
|
179
|
+
BeforeNodeSelect = new EventEmitter();
|
|
180
|
+
AfterNodeSelect = new EventEmitter();
|
|
181
|
+
BeforeSearch = new EventEmitter();
|
|
182
|
+
AfterSearch = new EventEmitter();
|
|
183
|
+
BeforeDropdownOpen = new EventEmitter();
|
|
184
|
+
AfterDropdownOpen = new EventEmitter();
|
|
185
|
+
BeforeDropdownClose = new EventEmitter();
|
|
186
|
+
AfterDropdownClose = new EventEmitter();
|
|
187
|
+
BeforeDataLoad = new EventEmitter();
|
|
188
|
+
AfterDataLoad = new EventEmitter();
|
|
189
|
+
// ========================================
|
|
190
|
+
// ViewChild References
|
|
191
|
+
// ========================================
|
|
192
|
+
triggerElement;
|
|
193
|
+
searchInput;
|
|
194
|
+
dropdownPanel;
|
|
195
|
+
treeComponent;
|
|
196
|
+
// ========================================
|
|
197
|
+
// State
|
|
198
|
+
// ========================================
|
|
199
|
+
/** Is dropdown open */
|
|
200
|
+
IsOpen = false;
|
|
201
|
+
/** Dropdown position */
|
|
202
|
+
Position = null;
|
|
203
|
+
/** Search text */
|
|
204
|
+
SearchText = '';
|
|
205
|
+
/** Selected nodes (for display) */
|
|
206
|
+
SelectedNodes = [];
|
|
207
|
+
/** Is tree loading */
|
|
208
|
+
IsLoading = false;
|
|
209
|
+
/** Has data loaded */
|
|
210
|
+
IsLoaded = false;
|
|
211
|
+
/** Dropdown portal element */
|
|
212
|
+
dropdownPortal = null;
|
|
213
|
+
/** Search debounce subject */
|
|
214
|
+
searchSubject = new Subject();
|
|
215
|
+
/** Destroy subject */
|
|
216
|
+
destroy$ = new Subject();
|
|
217
|
+
/** Click outside listener */
|
|
218
|
+
clickOutsideListener = null;
|
|
219
|
+
/** Scroll listener */
|
|
220
|
+
scrollListener = null;
|
|
221
|
+
/** Resize listener */
|
|
222
|
+
resizeListener = null;
|
|
223
|
+
/** Pending load promise resolvers */
|
|
224
|
+
_loadResolvers = [];
|
|
225
|
+
// ========================================
|
|
226
|
+
// Constructor
|
|
227
|
+
// ========================================
|
|
228
|
+
constructor(cdr, renderer) {
|
|
229
|
+
this.cdr = cdr;
|
|
230
|
+
this.renderer = renderer;
|
|
231
|
+
}
|
|
232
|
+
// ========================================
|
|
233
|
+
// Lifecycle
|
|
234
|
+
// ========================================
|
|
235
|
+
ngOnInit() {
|
|
236
|
+
// Setup search debounce
|
|
237
|
+
const debounceMs = this.SearchConfig.DebounceMs ?? 200;
|
|
238
|
+
this.searchSubject.pipe(debounceTime(debounceMs), takeUntil(this.destroy$)).subscribe(text => {
|
|
239
|
+
this.performSearch(text);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
ngAfterViewInit() {
|
|
243
|
+
// Create portal element
|
|
244
|
+
this.createDropdownPortal();
|
|
245
|
+
}
|
|
246
|
+
// Note: Value changes are handled by the getter/setter pattern
|
|
247
|
+
// No ngOnChanges needed for that property
|
|
248
|
+
ngOnDestroy() {
|
|
249
|
+
this.destroy$.next();
|
|
250
|
+
this.destroy$.complete();
|
|
251
|
+
this.removeDropdownPortal();
|
|
252
|
+
this.removeEventListeners();
|
|
253
|
+
}
|
|
254
|
+
// ========================================
|
|
255
|
+
// Public Methods
|
|
256
|
+
// ========================================
|
|
257
|
+
/**
|
|
258
|
+
* Returns a promise that resolves when the tree data has finished loading.
|
|
259
|
+
* If data is already loaded, the promise resolves immediately.
|
|
260
|
+
*
|
|
261
|
+
* Use this method when you need to perform operations that depend on the tree
|
|
262
|
+
* being fully loaded, such as programmatically selecting nodes or accessing
|
|
263
|
+
* the tree structure.
|
|
264
|
+
*
|
|
265
|
+
* Note: For setting initial values, you typically don't need this method -
|
|
266
|
+
* just set the `Value` input and the component will automatically display
|
|
267
|
+
* the correct text by looking up the record name via Metadata.
|
|
268
|
+
*
|
|
269
|
+
* @returns A promise that resolves when the tree data is loaded
|
|
270
|
+
* @example
|
|
271
|
+
* ```typescript
|
|
272
|
+
* // Wait for tree to load before accessing tree structure
|
|
273
|
+
* await treeDropdown.WaitForDataLoad();
|
|
274
|
+
* const nodes = treeDropdown.treeComponent.Nodes;
|
|
275
|
+
* ```
|
|
276
|
+
*/
|
|
277
|
+
WaitForDataLoad() {
|
|
278
|
+
if (this.IsLoaded) {
|
|
279
|
+
return Promise.resolve();
|
|
280
|
+
}
|
|
281
|
+
return new Promise((resolve) => {
|
|
282
|
+
this._loadResolvers.push(resolve);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Open the dropdown
|
|
287
|
+
*/
|
|
288
|
+
Open() {
|
|
289
|
+
if (this.Disabled || this.IsOpen) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
// Fire before event
|
|
293
|
+
const beforeEvent = new BeforeDropdownOpenEventArgs(this);
|
|
294
|
+
this.BeforeDropdownOpen.emit(beforeEvent);
|
|
295
|
+
if (beforeEvent.Cancel) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
this.IsOpen = true;
|
|
299
|
+
this.calculatePosition();
|
|
300
|
+
this.attachEventListeners();
|
|
301
|
+
// Focus search input after opening
|
|
302
|
+
setTimeout(() => {
|
|
303
|
+
if (this.EnableSearch && this.searchInput) {
|
|
304
|
+
this.searchInput.nativeElement.focus();
|
|
305
|
+
}
|
|
306
|
+
}, 50);
|
|
307
|
+
// Fire after event
|
|
308
|
+
const afterEvent = new AfterDropdownOpenEventArgs(this, this.Position?.renderAbove ? 'above' : 'below');
|
|
309
|
+
this.AfterDropdownOpen.emit(afterEvent);
|
|
310
|
+
this.cdr.detectChanges();
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Close the dropdown
|
|
314
|
+
*/
|
|
315
|
+
Close(reason = 'programmatic') {
|
|
316
|
+
if (!this.IsOpen) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
// Fire before event
|
|
320
|
+
const beforeEvent = new BeforeDropdownCloseEventArgs(this, reason);
|
|
321
|
+
this.BeforeDropdownClose.emit(beforeEvent);
|
|
322
|
+
if (beforeEvent.Cancel) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
this.IsOpen = false;
|
|
326
|
+
this.SearchText = '';
|
|
327
|
+
this.clearSearch();
|
|
328
|
+
this.removeEventListeners();
|
|
329
|
+
// Fire after event
|
|
330
|
+
const afterEvent = new AfterDropdownCloseEventArgs(this, reason);
|
|
331
|
+
this.AfterDropdownClose.emit(afterEvent);
|
|
332
|
+
this.cdr.detectChanges();
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Toggle dropdown
|
|
336
|
+
*/
|
|
337
|
+
Toggle() {
|
|
338
|
+
if (this.IsOpen) {
|
|
339
|
+
this.Close('programmatic');
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
this.Open();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Clear selection
|
|
347
|
+
*/
|
|
348
|
+
Clear(event) {
|
|
349
|
+
if (event) {
|
|
350
|
+
event.stopPropagation();
|
|
351
|
+
}
|
|
352
|
+
this.SelectedNodes = [];
|
|
353
|
+
this._value = null;
|
|
354
|
+
this._pendingDisplayText = null;
|
|
355
|
+
if (this.treeComponent) {
|
|
356
|
+
this.treeComponent.ClearSelection();
|
|
357
|
+
}
|
|
358
|
+
this.ValueChange.emit(this._value);
|
|
359
|
+
this.SelectionChange.emit(null);
|
|
360
|
+
this.cdr.detectChanges();
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Refresh tree data
|
|
364
|
+
*/
|
|
365
|
+
async Refresh() {
|
|
366
|
+
if (this.treeComponent) {
|
|
367
|
+
await this.treeComponent.Refresh();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// ========================================
|
|
371
|
+
// Template Event Handlers
|
|
372
|
+
// ========================================
|
|
373
|
+
/**
|
|
374
|
+
* Handle trigger click
|
|
375
|
+
*/
|
|
376
|
+
onTriggerClick() {
|
|
377
|
+
if (!this.Disabled) {
|
|
378
|
+
this.Toggle();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Handle trigger keydown
|
|
383
|
+
*/
|
|
384
|
+
onTriggerKeyDown(event) {
|
|
385
|
+
if (this.Disabled) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
switch (event.key) {
|
|
389
|
+
case 'Enter':
|
|
390
|
+
case ' ':
|
|
391
|
+
case 'ArrowDown':
|
|
392
|
+
event.preventDefault();
|
|
393
|
+
this.Open();
|
|
394
|
+
break;
|
|
395
|
+
case 'Escape':
|
|
396
|
+
if (this.IsOpen) {
|
|
397
|
+
event.preventDefault();
|
|
398
|
+
this.Close('escape');
|
|
399
|
+
}
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Handle search input
|
|
405
|
+
*/
|
|
406
|
+
onSearchInput(event) {
|
|
407
|
+
const value = event.target.value;
|
|
408
|
+
this.SearchText = value;
|
|
409
|
+
this.searchSubject.next(value);
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Handle search keydown
|
|
413
|
+
*/
|
|
414
|
+
onSearchKeyDown(event) {
|
|
415
|
+
switch (event.key) {
|
|
416
|
+
case 'Escape':
|
|
417
|
+
event.preventDefault();
|
|
418
|
+
event.stopPropagation();
|
|
419
|
+
this.Close('escape');
|
|
420
|
+
break;
|
|
421
|
+
case 'ArrowDown':
|
|
422
|
+
event.preventDefault();
|
|
423
|
+
// Focus first tree node
|
|
424
|
+
if (this.treeComponent) {
|
|
425
|
+
const visibleNodes = this.getVisibleNodesInOrder(this.treeComponent.Nodes);
|
|
426
|
+
if (visibleNodes.length > 0) {
|
|
427
|
+
this.treeComponent.FocusedNode = visibleNodes[0];
|
|
428
|
+
this.cdr.detectChanges();
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Handle clear search
|
|
436
|
+
*/
|
|
437
|
+
onClearSearch() {
|
|
438
|
+
this.SearchText = '';
|
|
439
|
+
this.clearSearch();
|
|
440
|
+
if (this.searchInput) {
|
|
441
|
+
this.searchInput.nativeElement.focus();
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Handle tree selection change
|
|
446
|
+
*/
|
|
447
|
+
onTreeSelectionChange(nodes) {
|
|
448
|
+
this.SelectedNodes = nodes;
|
|
449
|
+
// Clear pending display text since we now have real nodes
|
|
450
|
+
this._pendingDisplayText = null;
|
|
451
|
+
// Update value - convert node IDs to CompositeKeys
|
|
452
|
+
if (this.SelectionMode === 'single') {
|
|
453
|
+
this._value = nodes.length > 0 ? CompositeKey.FromID(nodes[0].ID) : null;
|
|
454
|
+
this.ValueChange.emit(this._value);
|
|
455
|
+
this.SelectionChange.emit(nodes.length > 0 ? nodes[0] : null);
|
|
456
|
+
// Close on selection in single mode (unless disabled)
|
|
457
|
+
// Only close if user actually selected something (not on empty selection from sync)
|
|
458
|
+
if (this.DropdownConfig.CloseOnSelect !== false && nodes.length > 0) {
|
|
459
|
+
this.Close('selection');
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
this._value = nodes.map(n => CompositeKey.FromID(n.ID));
|
|
464
|
+
this.ValueChange.emit(this._value);
|
|
465
|
+
this.SelectionChange.emit(nodes.length > 0 ? nodes : null);
|
|
466
|
+
}
|
|
467
|
+
this.cdr.detectChanges();
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Handle tree data load events
|
|
471
|
+
*/
|
|
472
|
+
onTreeBeforeDataLoad(event) {
|
|
473
|
+
this.IsLoading = true;
|
|
474
|
+
this.BeforeDataLoad.emit(event);
|
|
475
|
+
this.cdr.detectChanges();
|
|
476
|
+
}
|
|
477
|
+
onTreeAfterDataLoad(event) {
|
|
478
|
+
this.IsLoading = false;
|
|
479
|
+
this.IsLoaded = true;
|
|
480
|
+
// Resolve all pending WaitForDataLoad() promises
|
|
481
|
+
const resolvers = this._loadResolvers;
|
|
482
|
+
this._loadResolvers = [];
|
|
483
|
+
for (const resolve of resolvers) {
|
|
484
|
+
resolve();
|
|
485
|
+
}
|
|
486
|
+
// Sync selection after load - defer to next microtask to ensure ViewChild is resolved
|
|
487
|
+
Promise.resolve().then(() => {
|
|
488
|
+
if (this.treeComponent) {
|
|
489
|
+
}
|
|
490
|
+
this.syncValueToSelection();
|
|
491
|
+
this.cdr.detectChanges();
|
|
492
|
+
});
|
|
493
|
+
this.AfterDataLoad.emit(event);
|
|
494
|
+
this.cdr.detectChanges();
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Bubble tree events
|
|
498
|
+
*/
|
|
499
|
+
onTreeBeforeNodeSelect(event) {
|
|
500
|
+
this.BeforeNodeSelect.emit(event);
|
|
501
|
+
}
|
|
502
|
+
onTreeAfterNodeSelect(event) {
|
|
503
|
+
this.AfterNodeSelect.emit(event);
|
|
504
|
+
}
|
|
505
|
+
// ========================================
|
|
506
|
+
// Private Methods
|
|
507
|
+
// ========================================
|
|
508
|
+
/**
|
|
509
|
+
* Create dropdown portal element (attached to body)
|
|
510
|
+
*/
|
|
511
|
+
createDropdownPortal() {
|
|
512
|
+
this.dropdownPortal = this.renderer.createElement('div');
|
|
513
|
+
this.renderer.addClass(this.dropdownPortal, 'mj-tree-dropdown-portal');
|
|
514
|
+
this.renderer.setStyle(this.dropdownPortal, 'position', 'fixed');
|
|
515
|
+
this.renderer.setStyle(this.dropdownPortal, 'z-index', '10000');
|
|
516
|
+
this.renderer.setStyle(this.dropdownPortal, 'display', 'none');
|
|
517
|
+
this.renderer.appendChild(document.body, this.dropdownPortal);
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Remove dropdown portal
|
|
521
|
+
*/
|
|
522
|
+
removeDropdownPortal() {
|
|
523
|
+
if (this.dropdownPortal && this.dropdownPortal.parentNode) {
|
|
524
|
+
this.dropdownPortal.parentNode.removeChild(this.dropdownPortal);
|
|
525
|
+
this.dropdownPortal = null;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Calculate dropdown position
|
|
530
|
+
*/
|
|
531
|
+
calculatePosition() {
|
|
532
|
+
if (!this.triggerElement) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const triggerRect = this.triggerElement.nativeElement.getBoundingClientRect();
|
|
536
|
+
const viewportHeight = window.innerHeight;
|
|
537
|
+
const viewportWidth = window.innerWidth;
|
|
538
|
+
// Parse max height from config
|
|
539
|
+
const maxHeightConfig = this.DropdownConfig.MaxHeight || '300px';
|
|
540
|
+
const maxHeightPx = parseInt(maxHeightConfig, 10) || 300;
|
|
541
|
+
// Calculate available space
|
|
542
|
+
const spaceBelow = viewportHeight - triggerRect.bottom - 8; // 8px margin
|
|
543
|
+
const spaceAbove = triggerRect.top - 8;
|
|
544
|
+
// Determine if we should render above or below
|
|
545
|
+
let renderAbove = false;
|
|
546
|
+
let maxHeight = maxHeightPx;
|
|
547
|
+
if (this.DropdownConfig.Position === 'above') {
|
|
548
|
+
renderAbove = true;
|
|
549
|
+
maxHeight = Math.min(maxHeightPx, spaceAbove);
|
|
550
|
+
}
|
|
551
|
+
else if (this.DropdownConfig.Position === 'below') {
|
|
552
|
+
renderAbove = false;
|
|
553
|
+
maxHeight = Math.min(maxHeightPx, spaceBelow);
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
// Auto position
|
|
557
|
+
if (spaceBelow < maxHeightPx && spaceAbove > spaceBelow) {
|
|
558
|
+
renderAbove = true;
|
|
559
|
+
maxHeight = Math.min(maxHeightPx, spaceAbove);
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
renderAbove = false;
|
|
563
|
+
maxHeight = Math.min(maxHeightPx, spaceBelow);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
// Calculate width
|
|
567
|
+
const minWidth = this.DropdownConfig.MinWidth
|
|
568
|
+
? parseInt(this.DropdownConfig.MinWidth, 10)
|
|
569
|
+
: triggerRect.width;
|
|
570
|
+
const width = Math.max(minWidth, triggerRect.width);
|
|
571
|
+
// Ensure dropdown doesn't go off screen horizontally
|
|
572
|
+
let left = triggerRect.left;
|
|
573
|
+
if (left + width > viewportWidth - 8) {
|
|
574
|
+
left = viewportWidth - width - 8;
|
|
575
|
+
}
|
|
576
|
+
if (left < 8) {
|
|
577
|
+
left = 8;
|
|
578
|
+
}
|
|
579
|
+
// Calculate top position
|
|
580
|
+
let top;
|
|
581
|
+
if (renderAbove) {
|
|
582
|
+
top = triggerRect.top - maxHeight - 4;
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
585
|
+
top = triggerRect.bottom + 4;
|
|
586
|
+
}
|
|
587
|
+
this.Position = {
|
|
588
|
+
top,
|
|
589
|
+
left,
|
|
590
|
+
width,
|
|
591
|
+
maxHeight,
|
|
592
|
+
renderAbove
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Attach event listeners for click outside, scroll, resize
|
|
597
|
+
*/
|
|
598
|
+
attachEventListeners() {
|
|
599
|
+
// Click outside - defer with a small timeout to:
|
|
600
|
+
// 1. Allow the opening click event to complete
|
|
601
|
+
// 2. Ensure the dropdown panel DOM element is fully rendered
|
|
602
|
+
// 3. Allow Angular change detection to complete
|
|
603
|
+
if (this.DropdownConfig.CloseOnOutsideClick !== false) {
|
|
604
|
+
setTimeout(() => {
|
|
605
|
+
// Only attach if still open (could have been closed in the meantime)
|
|
606
|
+
if (!this.IsOpen) {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
this.clickOutsideListener = this.renderer.listen('document', 'click', (event) => {
|
|
610
|
+
const target = event.target;
|
|
611
|
+
// Double check we're still open
|
|
612
|
+
if (!this.IsOpen) {
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
const isInsideTrigger = this.triggerElement?.nativeElement?.contains(target);
|
|
616
|
+
// Check if click is inside the dropdown panel (rendered inline, not in portal)
|
|
617
|
+
const isInsideDropdown = this.dropdownPanel?.nativeElement?.contains(target);
|
|
618
|
+
if (!isInsideTrigger && !isInsideDropdown) {
|
|
619
|
+
this.Close('outsideClick');
|
|
620
|
+
}
|
|
621
|
+
else {
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
}, 100); // 100ms delay to ensure DOM is stable
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
}
|
|
628
|
+
// Escape key
|
|
629
|
+
if (this.DropdownConfig.CloseOnEscape !== false) {
|
|
630
|
+
document.addEventListener('keydown', this.handleEscapeKey);
|
|
631
|
+
}
|
|
632
|
+
// Scroll - reposition dropdown
|
|
633
|
+
this.scrollListener = this.renderer.listen('window', 'scroll', () => {
|
|
634
|
+
if (this.IsOpen) {
|
|
635
|
+
this.calculatePosition();
|
|
636
|
+
this.updateDropdownPortalPosition();
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
// Resize - reposition dropdown
|
|
640
|
+
this.resizeListener = this.renderer.listen('window', 'resize', () => {
|
|
641
|
+
if (this.IsOpen) {
|
|
642
|
+
this.calculatePosition();
|
|
643
|
+
this.updateDropdownPortalPosition();
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Remove event listeners
|
|
649
|
+
*/
|
|
650
|
+
removeEventListeners() {
|
|
651
|
+
if (this.clickOutsideListener) {
|
|
652
|
+
this.clickOutsideListener();
|
|
653
|
+
this.clickOutsideListener = null;
|
|
654
|
+
}
|
|
655
|
+
document.removeEventListener('keydown', this.handleEscapeKey);
|
|
656
|
+
if (this.scrollListener) {
|
|
657
|
+
this.scrollListener();
|
|
658
|
+
this.scrollListener = null;
|
|
659
|
+
}
|
|
660
|
+
if (this.resizeListener) {
|
|
661
|
+
this.resizeListener();
|
|
662
|
+
this.resizeListener = null;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Handle escape key
|
|
667
|
+
*/
|
|
668
|
+
handleEscapeKey = (event) => {
|
|
669
|
+
if (event.key === 'Escape' && this.IsOpen) {
|
|
670
|
+
this.Close('escape');
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
/**
|
|
674
|
+
* Update dropdown portal position
|
|
675
|
+
*/
|
|
676
|
+
updateDropdownPortalPosition() {
|
|
677
|
+
if (!this.dropdownPortal || !this.Position) {
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
this.renderer.setStyle(this.dropdownPortal, 'top', `${this.Position.top}px`);
|
|
681
|
+
this.renderer.setStyle(this.dropdownPortal, 'left', `${this.Position.left}px`);
|
|
682
|
+
this.renderer.setStyle(this.dropdownPortal, 'width', `${this.Position.width}px`);
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Perform search on tree
|
|
686
|
+
*/
|
|
687
|
+
performSearch(text) {
|
|
688
|
+
// Fire before event
|
|
689
|
+
const beforeEvent = new BeforeSearchEventArgs(this, text);
|
|
690
|
+
this.BeforeSearch.emit(beforeEvent);
|
|
691
|
+
if (beforeEvent.Cancel) {
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
const searchText = beforeEvent.ModifiedSearchText ?? text;
|
|
695
|
+
if (!this.treeComponent) {
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
const matchedNodes = this.treeComponent.FilterNodes(searchText, {
|
|
699
|
+
caseSensitive: this.SearchConfig.CaseSensitive,
|
|
700
|
+
searchBranches: this.SearchConfig.SearchBranches ?? true,
|
|
701
|
+
searchLeaves: this.SearchConfig.SearchLeaves ?? true,
|
|
702
|
+
searchDescription: this.SearchConfig.SearchDescription
|
|
703
|
+
});
|
|
704
|
+
// Auto-expand to show matches
|
|
705
|
+
if (this.SearchConfig.AutoExpandMatches !== false && searchText.trim()) {
|
|
706
|
+
for (const node of matchedNodes) {
|
|
707
|
+
this.treeComponent.ExpandToNode(node.ID);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
// Fire after event
|
|
711
|
+
const afterEvent = new AfterSearchEventArgs(this, searchText, matchedNodes);
|
|
712
|
+
this.AfterSearch.emit(afterEvent);
|
|
713
|
+
this.cdr.detectChanges();
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Clear search filter
|
|
717
|
+
*/
|
|
718
|
+
clearSearch() {
|
|
719
|
+
if (this.treeComponent) {
|
|
720
|
+
this.treeComponent.FilterNodes('', {});
|
|
721
|
+
this.cdr.detectChanges();
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Get all visible nodes in tree order (for keyboard navigation)
|
|
726
|
+
*/
|
|
727
|
+
getVisibleNodesInOrder(nodes) {
|
|
728
|
+
const result = [];
|
|
729
|
+
this.collectVisibleNodesRecursive(nodes, result);
|
|
730
|
+
return result;
|
|
731
|
+
}
|
|
732
|
+
collectVisibleNodesRecursive(nodes, result) {
|
|
733
|
+
for (const node of nodes) {
|
|
734
|
+
if (node.Visible) {
|
|
735
|
+
result.push(node);
|
|
736
|
+
if (node.Expanded && node.Type === 'branch') {
|
|
737
|
+
this.collectVisibleNodesRecursive(node.Children, result);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Sync value to tree selection
|
|
744
|
+
*/
|
|
745
|
+
syncValueToSelection() {
|
|
746
|
+
if (!this.treeComponent || !this.IsLoaded) {
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
// Convert CompositeKey(s) to string IDs for tree selection
|
|
750
|
+
const ids = this.getSelectedIDsArray();
|
|
751
|
+
// Pass emitChange=false to avoid emitting SelectionChange during sync
|
|
752
|
+
// This prevents unnecessary events and parent component confusion
|
|
753
|
+
this.treeComponent.SelectNodes(ids, false);
|
|
754
|
+
// Use try-catch as defensive measure since tree component may not be fully ready
|
|
755
|
+
try {
|
|
756
|
+
this.SelectedNodes = this.treeComponent.GetSelectedNodes() || [];
|
|
757
|
+
}
|
|
758
|
+
catch {
|
|
759
|
+
this.SelectedNodes = [];
|
|
760
|
+
}
|
|
761
|
+
// Clear pending display text since we now have real nodes
|
|
762
|
+
this._pendingDisplayText = null;
|
|
763
|
+
this.cdr.detectChanges();
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Fetch display text for value before tree loads using Metadata.GetEntityRecordName
|
|
767
|
+
*/
|
|
768
|
+
async fetchDisplayTextForValue(val) {
|
|
769
|
+
if (!val || !this.LeafConfig) {
|
|
770
|
+
this._pendingDisplayText = null;
|
|
771
|
+
this.cdr.detectChanges();
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
try {
|
|
775
|
+
const md = new Metadata();
|
|
776
|
+
const entityName = this.LeafConfig.EntityName;
|
|
777
|
+
if (Array.isArray(val)) {
|
|
778
|
+
// Multiple selection
|
|
779
|
+
if (val.length === 0) {
|
|
780
|
+
this._pendingDisplayText = null;
|
|
781
|
+
}
|
|
782
|
+
else if (val.length === 1) {
|
|
783
|
+
this._pendingDisplayText = await md.GetEntityRecordName(entityName, val[0]);
|
|
784
|
+
}
|
|
785
|
+
else {
|
|
786
|
+
this._pendingDisplayText = `${val.length} items selected`;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
else {
|
|
790
|
+
// Single selection
|
|
791
|
+
this._pendingDisplayText = await md.GetEntityRecordName(entityName, val);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
catch (error) {
|
|
795
|
+
console.warn('[TreeDropdown] Failed to fetch display text:', error);
|
|
796
|
+
this._pendingDisplayText = null;
|
|
797
|
+
}
|
|
798
|
+
this.cdr.detectChanges();
|
|
799
|
+
}
|
|
800
|
+
// ========================================
|
|
801
|
+
// Template Helpers
|
|
802
|
+
// ========================================
|
|
803
|
+
/**
|
|
804
|
+
* Get display text for selected value(s)
|
|
805
|
+
*/
|
|
806
|
+
getDisplayText() {
|
|
807
|
+
// If we have selected nodes from the tree, use those
|
|
808
|
+
if (this.SelectedNodes.length > 0) {
|
|
809
|
+
if (this.SelectionMode === 'single') {
|
|
810
|
+
return this.SelectedNodes[0].Label;
|
|
811
|
+
}
|
|
812
|
+
// Multiple selection
|
|
813
|
+
if (this.SelectedNodes.length === 1) {
|
|
814
|
+
return this.SelectedNodes[0].Label;
|
|
815
|
+
}
|
|
816
|
+
return `${this.SelectedNodes.length} items selected`;
|
|
817
|
+
}
|
|
818
|
+
// If tree not loaded but we have pending display text from Metadata lookup
|
|
819
|
+
if (this._pendingDisplayText) {
|
|
820
|
+
return this._pendingDisplayText;
|
|
821
|
+
}
|
|
822
|
+
return '';
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Get display icon for single selection
|
|
826
|
+
*/
|
|
827
|
+
getDisplayIcon() {
|
|
828
|
+
if (!this.ShowIconInDisplay || this.SelectedNodes.length !== 1) {
|
|
829
|
+
return null;
|
|
830
|
+
}
|
|
831
|
+
return this.SelectedNodes[0].Icon;
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Get display color for single selection
|
|
835
|
+
*/
|
|
836
|
+
getDisplayColor() {
|
|
837
|
+
if (this.SelectedNodes.length !== 1) {
|
|
838
|
+
return null;
|
|
839
|
+
}
|
|
840
|
+
return this.SelectedNodes[0].Color || null;
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Check if has selection
|
|
844
|
+
*/
|
|
845
|
+
hasSelection() {
|
|
846
|
+
return this.SelectedNodes.length > 0 || this._pendingDisplayText != null;
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Get dropdown panel styles
|
|
850
|
+
*/
|
|
851
|
+
getDropdownStyles() {
|
|
852
|
+
if (!this.Position) {
|
|
853
|
+
return {};
|
|
854
|
+
}
|
|
855
|
+
return {
|
|
856
|
+
top: `${this.Position.top}px`,
|
|
857
|
+
left: `${this.Position.left}px`,
|
|
858
|
+
width: `${this.Position.width}px`,
|
|
859
|
+
maxHeight: `${this.Position.maxHeight}px`
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Get trigger classes
|
|
864
|
+
*/
|
|
865
|
+
getTriggerClasses() {
|
|
866
|
+
return {
|
|
867
|
+
'tree-dropdown-trigger': true,
|
|
868
|
+
'tree-dropdown-trigger--open': this.IsOpen,
|
|
869
|
+
'tree-dropdown-trigger--disabled': this.Disabled,
|
|
870
|
+
'tree-dropdown-trigger--has-value': this.hasSelection(),
|
|
871
|
+
'tree-dropdown-trigger--loading': this.IsLoading && this.ShowLoadingInTrigger
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Get selected IDs as a string array for passing to tree component.
|
|
876
|
+
* Extracts the first key value from each CompositeKey (typically the ID field).
|
|
877
|
+
*/
|
|
878
|
+
getSelectedIDsArray() {
|
|
879
|
+
if (!this._value) {
|
|
880
|
+
return [];
|
|
881
|
+
}
|
|
882
|
+
const keys = Array.isArray(this._value) ? this._value : [this._value];
|
|
883
|
+
return keys.map(key => {
|
|
884
|
+
// Get the first value from the composite key (usually the ID)
|
|
885
|
+
const firstValue = key.GetValueByIndex(0);
|
|
886
|
+
return firstValue != null ? String(firstValue) : '';
|
|
887
|
+
}).filter(id => id !== '');
|
|
888
|
+
}
|
|
889
|
+
static ɵfac = function TreeDropdownComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || TreeDropdownComponent)(i0.ɵɵdirectiveInject(i0.ChangeDetectorRef), i0.ɵɵdirectiveInject(i0.Renderer2)); };
|
|
890
|
+
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: TreeDropdownComponent, selectors: [["mj-tree-dropdown"]], viewQuery: function TreeDropdownComponent_Query(rf, ctx) { if (rf & 1) {
|
|
891
|
+
i0.ɵɵviewQuery(_c0, 5);
|
|
892
|
+
i0.ɵɵviewQuery(_c1, 5);
|
|
893
|
+
i0.ɵɵviewQuery(_c2, 5);
|
|
894
|
+
i0.ɵɵviewQuery(_c3, 5);
|
|
895
|
+
} if (rf & 2) {
|
|
896
|
+
let _t;
|
|
897
|
+
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.triggerElement = _t.first);
|
|
898
|
+
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.searchInput = _t.first);
|
|
899
|
+
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.dropdownPanel = _t.first);
|
|
900
|
+
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.treeComponent = _t.first);
|
|
901
|
+
} }, inputs: { BranchConfig: "BranchConfig", LeafConfig: "LeafConfig", SelectionMode: "SelectionMode", SelectableTypes: "SelectableTypes", Value: "Value", Placeholder: "Placeholder", EnableSearch: "EnableSearch", SearchConfig: "SearchConfig", DropdownConfig: "DropdownConfig", StyleConfig: "StyleConfig", Clearable: "Clearable", Disabled: "Disabled", ShowIconInDisplay: "ShowIconInDisplay", AutoLoad: "AutoLoad", ShowLoadingInTrigger: "ShowLoadingInTrigger" }, outputs: { ValueChange: "ValueChange", SelectionChange: "SelectionChange", BeforeNodeSelect: "BeforeNodeSelect", AfterNodeSelect: "AfterNodeSelect", BeforeSearch: "BeforeSearch", AfterSearch: "AfterSearch", BeforeDropdownOpen: "BeforeDropdownOpen", AfterDropdownOpen: "AfterDropdownOpen", BeforeDropdownClose: "BeforeDropdownClose", AfterDropdownClose: "AfterDropdownClose", BeforeDataLoad: "BeforeDataLoad", AfterDataLoad: "AfterDataLoad" }, decls: 13, vars: 16, consts: [["triggerElement", ""], ["dropdownPanel", ""], ["treeComponent", ""], ["searchInput", ""], [1, "tree-dropdown"], ["tabindex", "0", "role", "combobox", 3, "click", "keydown", "ngClass"], [1, "tree-dropdown-trigger__content"], ["class", "tree-dropdown-trigger__icon", 3, "color", 4, "ngIf"], [1, "tree-dropdown-trigger__text"], ["class", "tree-dropdown-trigger__loading", 4, "ngIf"], [1, "tree-dropdown-trigger__actions"], ["type", "button", "class", "tree-dropdown-trigger__clear", "title", "Clear selection", "tabindex", "-1", 3, "click", 4, "ngIf"], [1, "tree-dropdown-trigger__chevron"], [1, "fa-solid", 3, "ngClass"], ["class", "tree-dropdown-panel", "role", "tree", 3, "tree-dropdown-panel--open", "tree-dropdown-panel--above", "tree-dropdown-panel--below", "class", "ngStyle", 4, "ngIf"], [1, "tree-dropdown-trigger__icon"], [1, "tree-dropdown-trigger__loading"], [1, "fa-solid", "fa-spinner", "fa-spin"], ["type", "button", "title", "Clear selection", "tabindex", "-1", 1, "tree-dropdown-trigger__clear", 3, "click"], [1, "fa-solid", "fa-times"], ["role", "tree", 1, "tree-dropdown-panel", 3, "ngStyle"], ["class", "tree-dropdown-search", 4, "ngIf"], [1, "tree-dropdown-tree"], [3, "SelectionChange", "BeforeNodeSelect", "AfterNodeSelect", "BeforeDataLoad", "AfterDataLoad", "BranchConfig", "LeafConfig", "SelectionMode", "SelectableTypes", "SelectedIDs", "AutoLoad", "ShowIcons", "ShowExpandCollapseAll", "AnimateExpandCollapse", "EmptyMessage", "EmptyIcon"], [1, "tree-dropdown-search"], [1, "tree-dropdown-search__input-wrapper"], [1, "fa-solid", "fa-search", "tree-dropdown-search__icon"], ["type", "text", "autocomplete", "off", "spellcheck", "false", 1, "tree-dropdown-search__input", 3, "input", "keydown", "placeholder", "value"], ["type", "button", "class", "tree-dropdown-search__clear", "tabindex", "-1", 3, "click", 4, "ngIf"], ["type", "button", "tabindex", "-1", 1, "tree-dropdown-search__clear", 3, "click"]], template: function TreeDropdownComponent_Template(rf, ctx) { if (rf & 1) {
|
|
902
|
+
const _r1 = i0.ɵɵgetCurrentView();
|
|
903
|
+
i0.ɵɵelementStart(0, "div", 4)(1, "div", 5, 0);
|
|
904
|
+
i0.ɵɵlistener("click", function TreeDropdownComponent_Template_div_click_1_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onTriggerClick()); })("keydown", function TreeDropdownComponent_Template_div_keydown_1_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onTriggerKeyDown($event)); });
|
|
905
|
+
i0.ɵɵelementStart(3, "div", 6);
|
|
906
|
+
i0.ɵɵtemplate(4, TreeDropdownComponent_span_4_Template, 2, 4, "span", 7);
|
|
907
|
+
i0.ɵɵelementStart(5, "span", 8);
|
|
908
|
+
i0.ɵɵtext(6);
|
|
909
|
+
i0.ɵɵelementEnd();
|
|
910
|
+
i0.ɵɵtemplate(7, TreeDropdownComponent_span_7_Template, 2, 0, "span", 9);
|
|
911
|
+
i0.ɵɵelementEnd();
|
|
912
|
+
i0.ɵɵelementStart(8, "div", 10);
|
|
913
|
+
i0.ɵɵtemplate(9, TreeDropdownComponent_button_9_Template, 2, 0, "button", 11);
|
|
914
|
+
i0.ɵɵelementStart(10, "span", 12);
|
|
915
|
+
i0.ɵɵelement(11, "i", 13);
|
|
916
|
+
i0.ɵɵelementEnd()()();
|
|
917
|
+
i0.ɵɵtemplate(12, TreeDropdownComponent_div_12_Template, 6, 22, "div", 14);
|
|
918
|
+
i0.ɵɵelementEnd();
|
|
919
|
+
} if (rf & 2) {
|
|
920
|
+
i0.ɵɵclassProp("tree-dropdown--disabled", ctx.Disabled);
|
|
921
|
+
i0.ɵɵadvance();
|
|
922
|
+
i0.ɵɵclassMap(ctx.StyleConfig.SearchInputClass || "");
|
|
923
|
+
i0.ɵɵproperty("ngClass", ctx.getTriggerClasses());
|
|
924
|
+
i0.ɵɵattribute("aria-expanded", ctx.IsOpen)("aria-haspopup", "tree")("aria-disabled", ctx.Disabled);
|
|
925
|
+
i0.ɵɵadvance(3);
|
|
926
|
+
i0.ɵɵproperty("ngIf", ctx.getDisplayIcon());
|
|
927
|
+
i0.ɵɵadvance();
|
|
928
|
+
i0.ɵɵclassProp("tree-dropdown-trigger__text--placeholder", !ctx.hasSelection());
|
|
929
|
+
i0.ɵɵadvance();
|
|
930
|
+
i0.ɵɵtextInterpolate1(" ", ctx.hasSelection() ? ctx.getDisplayText() : ctx.Placeholder, " ");
|
|
931
|
+
i0.ɵɵadvance();
|
|
932
|
+
i0.ɵɵproperty("ngIf", ctx.IsLoading && ctx.ShowLoadingInTrigger);
|
|
933
|
+
i0.ɵɵadvance(2);
|
|
934
|
+
i0.ɵɵproperty("ngIf", ctx.Clearable && ctx.hasSelection() && !ctx.Disabled);
|
|
935
|
+
i0.ɵɵadvance(2);
|
|
936
|
+
i0.ɵɵproperty("ngClass", ctx.IsOpen ? "fa-chevron-up" : "fa-chevron-down");
|
|
937
|
+
i0.ɵɵadvance();
|
|
938
|
+
i0.ɵɵproperty("ngIf", ctx.IsOpen);
|
|
939
|
+
} }, dependencies: [i1.NgClass, i1.NgIf, i1.NgStyle, i2.TreeComponent], styles: ["\n\n\n\n\n\n\n\n\n\n\n\n\n[_nghost-%COMP%] {\n \n\n --dropdown-bg: #ffffff;\n --dropdown-border-color: #d0d0d0;\n --dropdown-border-hover: #999999;\n --dropdown-border-focus: #2196f3;\n --dropdown-text-color: #333333;\n --dropdown-text-placeholder: #999999;\n --dropdown-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);\n\n \n\n --dropdown-trigger-bg: #ffffff;\n --dropdown-trigger-height: 40px;\n --dropdown-trigger-padding: 8px 12px;\n --dropdown-trigger-radius: 8px;\n\n \n\n --dropdown-search-bg: #f5f5f5;\n --dropdown-search-height: 36px;\n\n \n\n --dropdown-animation-duration: 150ms;\n --dropdown-animation-easing: ease-out;\n\n display: block;\n position: relative;\n width: 100%;\n}\n\n\n\n\n\n\n.tree-dropdown[_ngcontent-%COMP%] {\n position: relative;\n width: 100%;\n}\n\n.tree-dropdown--disabled[_ngcontent-%COMP%] {\n opacity: 0.6;\n pointer-events: none;\n}\n\n\n\n\n\n\n.tree-dropdown-trigger[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n min-height: var(--dropdown-trigger-height);\n padding: var(--dropdown-trigger-padding);\n background: var(--dropdown-trigger-bg);\n border: 1px solid var(--dropdown-border-color);\n border-radius: var(--dropdown-trigger-radius);\n cursor: pointer;\n outline: none;\n transition: all var(--dropdown-animation-duration) var(--dropdown-animation-easing);\n}\n\n.tree-dropdown-trigger[_ngcontent-%COMP%]:hover {\n border-color: var(--dropdown-border-hover);\n}\n\n.tree-dropdown-trigger[_ngcontent-%COMP%]:focus, \n.tree-dropdown-trigger--open[_ngcontent-%COMP%] {\n border-color: var(--dropdown-border-focus);\n box-shadow: 0 0 0 3px rgba(33, 150, 243, 0.15);\n}\n\n.tree-dropdown-trigger--disabled[_ngcontent-%COMP%] {\n cursor: not-allowed;\n background: #f5f5f5;\n}\n\n.tree-dropdown-trigger__content[_ngcontent-%COMP%] {\n flex: 1;\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n overflow: hidden;\n}\n\n.tree-dropdown-trigger__icon[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 14px;\n flex-shrink: 0;\n}\n\n.tree-dropdown-trigger__text[_ngcontent-%COMP%] {\n flex: 1;\n font-size: 14px;\n color: var(--dropdown-text-color);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.tree-dropdown-trigger__text--placeholder[_ngcontent-%COMP%] {\n color: var(--dropdown-text-placeholder);\n}\n\n.tree-dropdown-trigger__loading[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n color: var(--dropdown-border-focus);\n font-size: 12px;\n}\n\n.tree-dropdown-trigger__actions[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 4px;\n flex-shrink: 0;\n}\n\n.tree-dropdown-trigger__clear[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 20px;\n height: 20px;\n padding: 0;\n border: none;\n border-radius: 50%;\n background: #e0e0e0;\n color: #666;\n cursor: pointer;\n font-size: 10px;\n transition: all var(--dropdown-animation-duration) var(--dropdown-animation-easing);\n}\n\n.tree-dropdown-trigger__clear[_ngcontent-%COMP%]:hover {\n background: #d0d0d0;\n color: #333;\n}\n\n.tree-dropdown-trigger__chevron[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 20px;\n color: var(--dropdown-text-placeholder);\n font-size: 12px;\n transition: transform var(--dropdown-animation-duration) var(--dropdown-animation-easing);\n}\n\n.tree-dropdown-trigger--open[_ngcontent-%COMP%] .tree-dropdown-trigger__chevron[_ngcontent-%COMP%] {\n transform: rotate(180deg);\n}\n\n\n\n\n\n\n.tree-dropdown-panel[_ngcontent-%COMP%] {\n position: fixed;\n z-index: 10000;\n display: flex;\n flex-direction: column;\n background: var(--dropdown-bg);\n border: 1px solid var(--dropdown-border-color);\n border-radius: 8px;\n box-shadow: var(--dropdown-shadow);\n overflow: hidden;\n animation: _ngcontent-%COMP%_dropdown-open var(--dropdown-animation-duration) var(--dropdown-animation-easing);\n \n\n box-sizing: border-box;\n}\n\n.tree-dropdown-panel--above[_ngcontent-%COMP%] {\n transform-origin: bottom center;\n}\n\n.tree-dropdown-panel--below[_ngcontent-%COMP%] {\n transform-origin: top center;\n}\n\n@keyframes _ngcontent-%COMP%_dropdown-open {\n from {\n opacity: 0;\n transform: scaleY(0.95);\n }\n to {\n opacity: 1;\n transform: scaleY(1);\n }\n}\n\n\n\n\n\n\n.tree-dropdown-search[_ngcontent-%COMP%] {\n padding: 8px;\n border-bottom: 1px solid #e8e8e8;\n background: var(--dropdown-search-bg);\n}\n\n.tree-dropdown-search__input-wrapper[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 8px;\n height: var(--dropdown-search-height);\n padding: 0 10px;\n background: var(--dropdown-bg);\n border: 1px solid #e0e0e0;\n border-radius: 6px;\n transition: all var(--dropdown-animation-duration) var(--dropdown-animation-easing);\n}\n\n.tree-dropdown-search__input-wrapper[_ngcontent-%COMP%]:focus-within {\n border-color: var(--dropdown-border-focus);\n box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.1);\n}\n\n.tree-dropdown-search__icon[_ngcontent-%COMP%] {\n color: #999;\n font-size: 12px;\n flex-shrink: 0;\n}\n\n.tree-dropdown-search__input[_ngcontent-%COMP%] {\n flex: 1;\n border: none;\n background: transparent;\n font-size: 14px;\n color: var(--dropdown-text-color);\n outline: none;\n min-width: 0;\n}\n\n.tree-dropdown-search__input[_ngcontent-%COMP%]::placeholder {\n color: var(--dropdown-text-placeholder);\n}\n\n.tree-dropdown-search__clear[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 18px;\n height: 18px;\n padding: 0;\n border: none;\n border-radius: 50%;\n background: #e0e0e0;\n color: #666;\n cursor: pointer;\n font-size: 9px;\n flex-shrink: 0;\n transition: all var(--dropdown-animation-duration) var(--dropdown-animation-easing);\n}\n\n.tree-dropdown-search__clear[_ngcontent-%COMP%]:hover {\n background: #d0d0d0;\n color: #333;\n}\n\n\n\n\n\n\n.tree-dropdown-tree[_ngcontent-%COMP%] {\n flex: 1;\n overflow: auto;\n min-height: 0;\n max-height: 100%;\n}\n\n.tree-dropdown-tree[_ngcontent-%COMP%] mj-tree[_ngcontent-%COMP%] {\n display: block;\n height: auto;\n max-height: 100%;\n}\n\n\n\n.tree-dropdown-tree[_ngcontent-%COMP%] .tree-container {\n border-radius: 0;\n height: auto;\n max-height: none;\n}\n\n.tree-dropdown-tree[_ngcontent-%COMP%] .tree-content {\n padding: 4px 0;\n}\n\n.tree-dropdown-tree[_ngcontent-%COMP%] .tree-node {\n padding: 6px 12px;\n min-height: 32px;\n}\n\n.tree-dropdown-tree[_ngcontent-%COMP%] .tree-empty {\n padding: 24px 16px;\n}\n\n.tree-dropdown-tree[_ngcontent-%COMP%] .tree-loading {\n padding: 24px 16px;\n}\n\n\n\n\n\n\n[_nghost-%COMP%] .mj-tree-dropdown-portal {\n pointer-events: none;\n}\n\n[_nghost-%COMP%] .mj-tree-dropdown-portal > * {\n pointer-events: auto;\n}\n\n\n\n\n\n\n.tree-dropdown-pills[_ngcontent-%COMP%] {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n padding: 4px;\n}\n\n.tree-dropdown-pill[_ngcontent-%COMP%] {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 2px 8px;\n background: #e3f2fd;\n border-radius: 12px;\n font-size: 12px;\n color: #1976d2;\n}\n\n.tree-dropdown-pill__remove[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 14px;\n height: 14px;\n padding: 0;\n border: none;\n border-radius: 50%;\n background: transparent;\n color: #1976d2;\n cursor: pointer;\n font-size: 8px;\n transition: background var(--dropdown-animation-duration) var(--dropdown-animation-easing);\n}\n\n.tree-dropdown-pill__remove[_ngcontent-%COMP%]:hover {\n background: rgba(25, 118, 210, 0.2);\n}\n\n\n\n\n\n\n@media (max-width: 480px) {\n [_nghost-%COMP%] {\n --dropdown-trigger-height: 44px;\n }\n\n .tree-dropdown-trigger__text[_ngcontent-%COMP%] {\n font-size: 15px;\n }\n\n .tree-dropdown-search__input[_ngcontent-%COMP%] {\n font-size: 16px; \n\n }\n}"] });
|
|
940
|
+
}
|
|
941
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(TreeDropdownComponent, [{
|
|
942
|
+
type: Component,
|
|
943
|
+
args: [{ selector: 'mj-tree-dropdown', template: "<!-- Tree Dropdown Component Template -->\n<div class=\"tree-dropdown\" [class.tree-dropdown--disabled]=\"Disabled\">\n\n <!-- Trigger Button -->\n <div\n #triggerElement\n [ngClass]=\"getTriggerClasses()\"\n [class]=\"StyleConfig.SearchInputClass || ''\"\n tabindex=\"0\"\n role=\"combobox\"\n [attr.aria-expanded]=\"IsOpen\"\n [attr.aria-haspopup]=\"'tree'\"\n [attr.aria-disabled]=\"Disabled\"\n (click)=\"onTriggerClick()\"\n (keydown)=\"onTriggerKeyDown($event)\">\n\n <!-- Selected Value Display -->\n <div class=\"tree-dropdown-trigger__content\">\n <!-- Icon -->\n <span\n class=\"tree-dropdown-trigger__icon\"\n *ngIf=\"getDisplayIcon()\"\n [style.color]=\"getDisplayColor()\">\n <i [class]=\"getDisplayIcon()\"></i>\n </span>\n\n <!-- Text -->\n <span\n class=\"tree-dropdown-trigger__text\"\n [class.tree-dropdown-trigger__text--placeholder]=\"!hasSelection()\">\n {{ hasSelection() ? getDisplayText() : Placeholder }}\n </span>\n\n <!-- Loading Spinner -->\n <span class=\"tree-dropdown-trigger__loading\" *ngIf=\"IsLoading && ShowLoadingInTrigger\">\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n </span>\n </div>\n\n <!-- Actions -->\n <div class=\"tree-dropdown-trigger__actions\">\n <!-- Clear Button -->\n <button\n type=\"button\"\n class=\"tree-dropdown-trigger__clear\"\n *ngIf=\"Clearable && hasSelection() && !Disabled\"\n (click)=\"Clear($event)\"\n title=\"Clear selection\"\n tabindex=\"-1\">\n <i class=\"fa-solid fa-times\"></i>\n </button>\n\n <!-- Chevron -->\n <span class=\"tree-dropdown-trigger__chevron\">\n <i\n class=\"fa-solid\"\n [ngClass]=\"IsOpen ? 'fa-chevron-up' : 'fa-chevron-down'\">\n </i>\n </span>\n </div>\n </div>\n\n <!-- Dropdown Panel (rendered in place, will be moved to portal when open) -->\n <div\n #dropdownPanel\n class=\"tree-dropdown-panel\"\n [class.tree-dropdown-panel--open]=\"IsOpen\"\n [class.tree-dropdown-panel--above]=\"Position?.renderAbove\"\n [class.tree-dropdown-panel--below]=\"!Position?.renderAbove\"\n [class]=\"StyleConfig.DropdownClass || ''\"\n [ngStyle]=\"IsOpen ? getDropdownStyles() : {}\"\n *ngIf=\"IsOpen\"\n role=\"tree\">\n\n <!-- Search Input -->\n <div class=\"tree-dropdown-search\" *ngIf=\"EnableSearch\">\n <div class=\"tree-dropdown-search__input-wrapper\">\n <i class=\"fa-solid fa-search tree-dropdown-search__icon\"></i>\n <input\n #searchInput\n type=\"text\"\n class=\"tree-dropdown-search__input\"\n [placeholder]=\"SearchConfig.Placeholder || 'Type to search...'\"\n [value]=\"SearchText\"\n (input)=\"onSearchInput($event)\"\n (keydown)=\"onSearchKeyDown($event)\"\n autocomplete=\"off\"\n spellcheck=\"false\">\n <button\n type=\"button\"\n class=\"tree-dropdown-search__clear\"\n *ngIf=\"SearchText\"\n (click)=\"onClearSearch()\"\n tabindex=\"-1\">\n <i class=\"fa-solid fa-times\"></i>\n </button>\n </div>\n </div>\n\n <!-- Tree -->\n <div class=\"tree-dropdown-tree\">\n <mj-tree\n #treeComponent\n [BranchConfig]=\"BranchConfig\"\n [LeafConfig]=\"LeafConfig\"\n [SelectionMode]=\"SelectionMode\"\n [SelectableTypes]=\"SelectableTypes\"\n [SelectedIDs]=\"getSelectedIDsArray()\"\n [AutoLoad]=\"AutoLoad\"\n [ShowIcons]=\"true\"\n [ShowExpandCollapseAll]=\"false\"\n [AnimateExpandCollapse]=\"true\"\n [EmptyMessage]=\"SearchText ? 'No matches found' : 'No items available'\"\n [EmptyIcon]=\"SearchText ? 'fa-solid fa-search' : 'fa-solid fa-folder-open'\"\n (SelectionChange)=\"onTreeSelectionChange($event)\"\n (BeforeNodeSelect)=\"onTreeBeforeNodeSelect($event)\"\n (AfterNodeSelect)=\"onTreeAfterNodeSelect($event)\"\n (BeforeDataLoad)=\"onTreeBeforeDataLoad($event)\"\n (AfterDataLoad)=\"onTreeAfterDataLoad($event)\">\n </mj-tree>\n </div>\n </div>\n</div>\n", styles: ["/**\n * Tree Dropdown Component Styles\n *\n * Uses CSS custom properties for easy theming.\n * Containers can override these variables to customize appearance.\n */\n\n/* ========================================\n CSS Variables (Theming)\n ======================================== */\n\n:host {\n /* Colors */\n --dropdown-bg: #ffffff;\n --dropdown-border-color: #d0d0d0;\n --dropdown-border-hover: #999999;\n --dropdown-border-focus: #2196f3;\n --dropdown-text-color: #333333;\n --dropdown-text-placeholder: #999999;\n --dropdown-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);\n\n /* Trigger */\n --dropdown-trigger-bg: #ffffff;\n --dropdown-trigger-height: 40px;\n --dropdown-trigger-padding: 8px 12px;\n --dropdown-trigger-radius: 8px;\n\n /* Search */\n --dropdown-search-bg: #f5f5f5;\n --dropdown-search-height: 36px;\n\n /* Animation */\n --dropdown-animation-duration: 150ms;\n --dropdown-animation-easing: ease-out;\n\n display: block;\n position: relative;\n width: 100%;\n}\n\n/* ========================================\n Container\n ======================================== */\n\n.tree-dropdown {\n position: relative;\n width: 100%;\n}\n\n.tree-dropdown--disabled {\n opacity: 0.6;\n pointer-events: none;\n}\n\n/* ========================================\n Trigger\n ======================================== */\n\n.tree-dropdown-trigger {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n min-height: var(--dropdown-trigger-height);\n padding: var(--dropdown-trigger-padding);\n background: var(--dropdown-trigger-bg);\n border: 1px solid var(--dropdown-border-color);\n border-radius: var(--dropdown-trigger-radius);\n cursor: pointer;\n outline: none;\n transition: all var(--dropdown-animation-duration) var(--dropdown-animation-easing);\n}\n\n.tree-dropdown-trigger:hover {\n border-color: var(--dropdown-border-hover);\n}\n\n.tree-dropdown-trigger:focus,\n.tree-dropdown-trigger--open {\n border-color: var(--dropdown-border-focus);\n box-shadow: 0 0 0 3px rgba(33, 150, 243, 0.15);\n}\n\n.tree-dropdown-trigger--disabled {\n cursor: not-allowed;\n background: #f5f5f5;\n}\n\n.tree-dropdown-trigger__content {\n flex: 1;\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n overflow: hidden;\n}\n\n.tree-dropdown-trigger__icon {\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 14px;\n flex-shrink: 0;\n}\n\n.tree-dropdown-trigger__text {\n flex: 1;\n font-size: 14px;\n color: var(--dropdown-text-color);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.tree-dropdown-trigger__text--placeholder {\n color: var(--dropdown-text-placeholder);\n}\n\n.tree-dropdown-trigger__loading {\n display: flex;\n align-items: center;\n color: var(--dropdown-border-focus);\n font-size: 12px;\n}\n\n.tree-dropdown-trigger__actions {\n display: flex;\n align-items: center;\n gap: 4px;\n flex-shrink: 0;\n}\n\n.tree-dropdown-trigger__clear {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 20px;\n height: 20px;\n padding: 0;\n border: none;\n border-radius: 50%;\n background: #e0e0e0;\n color: #666;\n cursor: pointer;\n font-size: 10px;\n transition: all var(--dropdown-animation-duration) var(--dropdown-animation-easing);\n}\n\n.tree-dropdown-trigger__clear:hover {\n background: #d0d0d0;\n color: #333;\n}\n\n.tree-dropdown-trigger__chevron {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 20px;\n color: var(--dropdown-text-placeholder);\n font-size: 12px;\n transition: transform var(--dropdown-animation-duration) var(--dropdown-animation-easing);\n}\n\n.tree-dropdown-trigger--open .tree-dropdown-trigger__chevron {\n transform: rotate(180deg);\n}\n\n/* ========================================\n Dropdown Panel\n ======================================== */\n\n.tree-dropdown-panel {\n position: fixed;\n z-index: 10000;\n display: flex;\n flex-direction: column;\n background: var(--dropdown-bg);\n border: 1px solid var(--dropdown-border-color);\n border-radius: 8px;\n box-shadow: var(--dropdown-shadow);\n overflow: hidden;\n animation: dropdown-open var(--dropdown-animation-duration) var(--dropdown-animation-easing);\n /* Ensure the panel respects its maxHeight and allows child scrolling */\n box-sizing: border-box;\n}\n\n.tree-dropdown-panel--above {\n transform-origin: bottom center;\n}\n\n.tree-dropdown-panel--below {\n transform-origin: top center;\n}\n\n@keyframes dropdown-open {\n from {\n opacity: 0;\n transform: scaleY(0.95);\n }\n to {\n opacity: 1;\n transform: scaleY(1);\n }\n}\n\n/* ========================================\n Search\n ======================================== */\n\n.tree-dropdown-search {\n padding: 8px;\n border-bottom: 1px solid #e8e8e8;\n background: var(--dropdown-search-bg);\n}\n\n.tree-dropdown-search__input-wrapper {\n display: flex;\n align-items: center;\n gap: 8px;\n height: var(--dropdown-search-height);\n padding: 0 10px;\n background: var(--dropdown-bg);\n border: 1px solid #e0e0e0;\n border-radius: 6px;\n transition: all var(--dropdown-animation-duration) var(--dropdown-animation-easing);\n}\n\n.tree-dropdown-search__input-wrapper:focus-within {\n border-color: var(--dropdown-border-focus);\n box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.1);\n}\n\n.tree-dropdown-search__icon {\n color: #999;\n font-size: 12px;\n flex-shrink: 0;\n}\n\n.tree-dropdown-search__input {\n flex: 1;\n border: none;\n background: transparent;\n font-size: 14px;\n color: var(--dropdown-text-color);\n outline: none;\n min-width: 0;\n}\n\n.tree-dropdown-search__input::placeholder {\n color: var(--dropdown-text-placeholder);\n}\n\n.tree-dropdown-search__clear {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 18px;\n height: 18px;\n padding: 0;\n border: none;\n border-radius: 50%;\n background: #e0e0e0;\n color: #666;\n cursor: pointer;\n font-size: 9px;\n flex-shrink: 0;\n transition: all var(--dropdown-animation-duration) var(--dropdown-animation-easing);\n}\n\n.tree-dropdown-search__clear:hover {\n background: #d0d0d0;\n color: #333;\n}\n\n/* ========================================\n Tree Container\n ======================================== */\n\n.tree-dropdown-tree {\n flex: 1;\n overflow: auto;\n min-height: 0;\n max-height: 100%;\n}\n\n.tree-dropdown-tree mj-tree {\n display: block;\n height: auto;\n max-height: 100%;\n}\n\n/* Override tree styles for dropdown context */\n.tree-dropdown-tree ::ng-deep .tree-container {\n border-radius: 0;\n height: auto;\n max-height: none;\n}\n\n.tree-dropdown-tree ::ng-deep .tree-content {\n padding: 4px 0;\n}\n\n.tree-dropdown-tree ::ng-deep .tree-node {\n padding: 6px 12px;\n min-height: 32px;\n}\n\n.tree-dropdown-tree ::ng-deep .tree-empty {\n padding: 24px 16px;\n}\n\n.tree-dropdown-tree ::ng-deep .tree-loading {\n padding: 24px 16px;\n}\n\n/* ========================================\n Portal Container (in body)\n ======================================== */\n\n:host ::ng-deep .mj-tree-dropdown-portal {\n pointer-events: none;\n}\n\n:host ::ng-deep .mj-tree-dropdown-portal > * {\n pointer-events: auto;\n}\n\n/* ========================================\n Multiple Selection Pills\n ======================================== */\n\n.tree-dropdown-pills {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n padding: 4px;\n}\n\n.tree-dropdown-pill {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 2px 8px;\n background: #e3f2fd;\n border-radius: 12px;\n font-size: 12px;\n color: #1976d2;\n}\n\n.tree-dropdown-pill__remove {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 14px;\n height: 14px;\n padding: 0;\n border: none;\n border-radius: 50%;\n background: transparent;\n color: #1976d2;\n cursor: pointer;\n font-size: 8px;\n transition: background var(--dropdown-animation-duration) var(--dropdown-animation-easing);\n}\n\n.tree-dropdown-pill__remove:hover {\n background: rgba(25, 118, 210, 0.2);\n}\n\n/* ========================================\n Responsive\n ======================================== */\n\n@media (max-width: 480px) {\n :host {\n --dropdown-trigger-height: 44px;\n }\n\n .tree-dropdown-trigger__text {\n font-size: 15px;\n }\n\n .tree-dropdown-search__input {\n font-size: 16px; /* Prevents zoom on iOS */\n }\n}\n"] }]
|
|
944
|
+
}], () => [{ type: i0.ChangeDetectorRef }, { type: i0.Renderer2 }], { BranchConfig: [{
|
|
945
|
+
type: Input
|
|
946
|
+
}], LeafConfig: [{
|
|
947
|
+
type: Input
|
|
948
|
+
}], SelectionMode: [{
|
|
949
|
+
type: Input
|
|
950
|
+
}], SelectableTypes: [{
|
|
951
|
+
type: Input
|
|
952
|
+
}], Value: [{
|
|
953
|
+
type: Input
|
|
954
|
+
}], Placeholder: [{
|
|
955
|
+
type: Input
|
|
956
|
+
}], EnableSearch: [{
|
|
957
|
+
type: Input
|
|
958
|
+
}], SearchConfig: [{
|
|
959
|
+
type: Input
|
|
960
|
+
}], DropdownConfig: [{
|
|
961
|
+
type: Input
|
|
962
|
+
}], StyleConfig: [{
|
|
963
|
+
type: Input
|
|
964
|
+
}], Clearable: [{
|
|
965
|
+
type: Input
|
|
966
|
+
}], Disabled: [{
|
|
967
|
+
type: Input
|
|
968
|
+
}], ShowIconInDisplay: [{
|
|
969
|
+
type: Input
|
|
970
|
+
}], AutoLoad: [{
|
|
971
|
+
type: Input
|
|
972
|
+
}], ShowLoadingInTrigger: [{
|
|
973
|
+
type: Input
|
|
974
|
+
}], ValueChange: [{
|
|
975
|
+
type: Output
|
|
976
|
+
}], SelectionChange: [{
|
|
977
|
+
type: Output
|
|
978
|
+
}], BeforeNodeSelect: [{
|
|
979
|
+
type: Output
|
|
980
|
+
}], AfterNodeSelect: [{
|
|
981
|
+
type: Output
|
|
982
|
+
}], BeforeSearch: [{
|
|
983
|
+
type: Output
|
|
984
|
+
}], AfterSearch: [{
|
|
985
|
+
type: Output
|
|
986
|
+
}], BeforeDropdownOpen: [{
|
|
987
|
+
type: Output
|
|
988
|
+
}], AfterDropdownOpen: [{
|
|
989
|
+
type: Output
|
|
990
|
+
}], BeforeDropdownClose: [{
|
|
991
|
+
type: Output
|
|
992
|
+
}], AfterDropdownClose: [{
|
|
993
|
+
type: Output
|
|
994
|
+
}], BeforeDataLoad: [{
|
|
995
|
+
type: Output
|
|
996
|
+
}], AfterDataLoad: [{
|
|
997
|
+
type: Output
|
|
998
|
+
}], triggerElement: [{
|
|
999
|
+
type: ViewChild,
|
|
1000
|
+
args: ['triggerElement']
|
|
1001
|
+
}], searchInput: [{
|
|
1002
|
+
type: ViewChild,
|
|
1003
|
+
args: ['searchInput']
|
|
1004
|
+
}], dropdownPanel: [{
|
|
1005
|
+
type: ViewChild,
|
|
1006
|
+
args: ['dropdownPanel']
|
|
1007
|
+
}], treeComponent: [{
|
|
1008
|
+
type: ViewChild,
|
|
1009
|
+
args: ['treeComponent']
|
|
1010
|
+
}] }); })();
|
|
1011
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(TreeDropdownComponent, { className: "TreeDropdownComponent", filePath: "src/lib/tree-dropdown/tree-dropdown.component.ts", lineNumber: 68 }); })();
|
|
1012
|
+
//# sourceMappingURL=tree-dropdown.component.js.map
|