@nexterajs/ui 0.0.1 → 0.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.
Files changed (63) hide show
  1. package/README.md +279 -34
  2. package/fesm2022/nexterajs-ui-charts.mjs +1096 -0
  3. package/fesm2022/nexterajs-ui-charts.mjs.map +1 -0
  4. package/fesm2022/nexterajs-ui-core.mjs +835 -11
  5. package/fesm2022/nexterajs-ui-core.mjs.map +1 -1
  6. package/fesm2022/nexterajs-ui-display.mjs +1142 -0
  7. package/fesm2022/nexterajs-ui-display.mjs.map +1 -0
  8. package/fesm2022/nexterajs-ui-dropdowns.mjs +621 -69
  9. package/fesm2022/nexterajs-ui-dropdowns.mjs.map +1 -1
  10. package/fesm2022/nexterajs-ui-file-uploader.mjs +576 -11
  11. package/fesm2022/nexterajs-ui-file-uploader.mjs.map +1 -1
  12. package/fesm2022/nexterajs-ui-forms.mjs +2702 -452
  13. package/fesm2022/nexterajs-ui-forms.mjs.map +1 -1
  14. package/fesm2022/nexterajs-ui-grid.mjs +2350 -336
  15. package/fesm2022/nexterajs-ui-grid.mjs.map +1 -1
  16. package/fesm2022/nexterajs-ui-intl.mjs +117 -15
  17. package/fesm2022/nexterajs-ui-intl.mjs.map +1 -1
  18. package/fesm2022/nexterajs-ui-navigation.mjs +1054 -0
  19. package/fesm2022/nexterajs-ui-navigation.mjs.map +1 -0
  20. package/fesm2022/nexterajs-ui-notify.mjs +353 -0
  21. package/fesm2022/nexterajs-ui-notify.mjs.map +1 -0
  22. package/fesm2022/nexterajs-ui-overlay.mjs +1671 -0
  23. package/fesm2022/nexterajs-ui-overlay.mjs.map +1 -0
  24. package/fesm2022/nexterajs-ui-pivot.mjs +191 -136
  25. package/fesm2022/nexterajs-ui-pivot.mjs.map +1 -1
  26. package/fesm2022/nexterajs-ui-shell.mjs +542 -0
  27. package/fesm2022/nexterajs-ui-shell.mjs.map +1 -0
  28. package/fesm2022/nexterajs-ui-styling.mjs +568 -3
  29. package/fesm2022/nexterajs-ui-styling.mjs.map +1 -1
  30. package/fesm2022/nexterajs-ui-tree-view.mjs +150 -18
  31. package/fesm2022/nexterajs-ui-tree-view.mjs.map +1 -1
  32. package/fesm2022/nexterajs-ui-validation.mjs +283 -97
  33. package/fesm2022/nexterajs-ui-validation.mjs.map +1 -1
  34. package/fesm2022/nexterajs-ui.mjs +6 -0
  35. package/fesm2022/nexterajs-ui.mjs.map +1 -1
  36. package/package.json +37 -2
  37. package/styles/_dropdown.scss +150 -1
  38. package/styles/_editor.scss +75 -21
  39. package/styles/_icons.scss +92 -0
  40. package/styles/_mixins.scss +104 -1
  41. package/styles/_scrollbar.scss +134 -0
  42. package/styles/_status.scss +21 -0
  43. package/styles/_toast.scss +243 -0
  44. package/styles/_tokens.scss +467 -2
  45. package/styles/_widget.scss +14 -3
  46. package/styles/index.scss +10 -4
  47. package/types/nexterajs-ui-charts.d.ts +446 -0
  48. package/types/nexterajs-ui-core.d.ts +515 -30
  49. package/types/nexterajs-ui-display.d.ts +720 -0
  50. package/types/nexterajs-ui-dropdowns.d.ts +318 -57
  51. package/types/nexterajs-ui-file-uploader.d.ts +240 -8
  52. package/types/nexterajs-ui-forms.d.ts +772 -43
  53. package/types/nexterajs-ui-grid.d.ts +878 -135
  54. package/types/nexterajs-ui-intl.d.ts +39 -4
  55. package/types/nexterajs-ui-navigation.d.ts +531 -0
  56. package/types/nexterajs-ui-notify.d.ts +149 -0
  57. package/types/nexterajs-ui-overlay.d.ts +703 -0
  58. package/types/nexterajs-ui-pivot.d.ts +91 -70
  59. package/types/nexterajs-ui-shell.d.ts +294 -0
  60. package/types/nexterajs-ui-styling.d.ts +163 -3
  61. package/types/nexterajs-ui-tree-view.d.ts +56 -4
  62. package/types/nexterajs-ui-validation.d.ts +110 -22
  63. package/types/nexterajs-ui.d.ts +6 -0
@@ -2,22 +2,167 @@ import { Overlay } from '@angular/cdk/overlay';
2
2
  import { TemplatePortal } from '@angular/cdk/portal';
3
3
  import { NgTemplateOutlet } from '@angular/common';
4
4
  import * as i0 from '@angular/core';
5
- import { inject, ViewContainerRef, viewChild, contentChildren, input, computed, booleanAttribute, output, signal, forwardRef, ViewEncapsulation, ChangeDetectionStrategy, Component, contentChild, model, Injector, InjectionToken, ElementRef, TemplateRef, Directive } from '@angular/core';
5
+ import { signal, inject, DestroyRef, ViewContainerRef, viewChild, contentChildren, input, computed, numberAttribute, booleanAttribute, output, forwardRef, ViewEncapsulation, ChangeDetectionStrategy, Component, contentChild, model, Injector, InjectionToken, ElementRef, TemplateRef, Directive } from '@angular/core';
6
6
  import { NG_VALUE_ACCESSOR } from '@angular/forms';
7
- import { merge, filter, distinctUntilChanged, Subject } from 'rxjs';
8
- import { NxeFormControlBase, NxeTemplateDirective, nxeOptionalBoolean, NXE_VALIDATION_TARGET } from '@nexterajs/ui/core';
7
+ import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
8
+ import { Subject, switchMap, timer, map, firstValueFrom, from, filter, catchError, EMPTY, merge, mergeMap, distinctUntilChanged } from 'rxjs';
9
+ import { NxeFormControlBase, NxeTemplateDirective, nxeOptionalBoolean, nxeResolveText, nxeResolveExpr, NXE_VALIDATION_TARGET } from '@nexterajs/ui/core';
9
10
  import { nxePane, trackPaneFlip } from '@nexterajs/ui/styling';
10
- import { toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
11
+
12
+ /** True for a store object (anything with a `load` function that is not an array). */
13
+ function nxeIsPickerStore(value) {
14
+ return (!!value &&
15
+ typeof value === 'object' &&
16
+ !Array.isArray(value) &&
17
+ typeof value.load === 'function');
18
+ }
19
+ /**
20
+ * The search plumbing shared by the pickers: debounces typing by `searchTimeout`, emits
21
+ * `searchChange` once the text reaches `minSearchLength` (or is cleared), and, when the
22
+ * `dataSource` is a {@link NxePickerStore}, queries it and keeps only the newest response.
23
+ * Must be created in an injection context. It uses no `effect()`: it reacts only to the explicit
24
+ * {@link typed}, {@link open} and {@link loadMore} calls.
25
+ */
26
+ class NxePickerSearch {
27
+ cfg;
28
+ /** Rows of the store loaded so far (all pages of the current query). */
29
+ items = signal([], /* @ts-ignore */
30
+ ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
31
+ loading = signal(false, /* @ts-ignore */
32
+ ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
33
+ error = signal(null, /* @ts-ignore */
34
+ ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
35
+ hasMore = signal(false, /* @ts-ignore */
36
+ ...(ngDevMode ? [{ debugName: "hasMore" }] : /* istanbul ignore next */ []));
37
+ typedText = new Subject();
38
+ loadedFor = null;
39
+ seq = 0;
40
+ controller = null;
41
+ constructor(cfg) {
42
+ this.cfg = cfg;
43
+ const destroyRef = inject(DestroyRef);
44
+ this.typedText
45
+ .pipe(switchMap((text) => timer(Math.max(0, cfg.searchTimeout())).pipe(map(() => text))), takeUntilDestroyed(destroyRef))
46
+ .subscribe((text) => this.settled(text));
47
+ destroyRef.onDestroy(() => this.controller?.abort());
48
+ }
49
+ get hasStore() {
50
+ return this.cfg.store() !== null;
51
+ }
52
+ /** True when `text` is long enough to be searched. */
53
+ reachesMinimum(text) {
54
+ return text.length >= this.cfg.minSearchLength();
55
+ }
56
+ /** The user typed; the search settles after `searchTimeout`. */
57
+ typed(text) {
58
+ this.typedText.next(text);
59
+ }
60
+ /** The panel opened with `text` in the field: load the first page unless that query is current. */
61
+ open(text) {
62
+ if (this.hasStore && this.loadedFor !== this.effective(text)) {
63
+ this.query(text);
64
+ }
65
+ }
66
+ /** Loads the next page of the current query (a scroll-to-bottom handler calls this). */
67
+ loadMore() {
68
+ if (this.hasStore && this.hasMore() && !this.loading()) {
69
+ void this.run(this.loadedFor ?? '', true);
70
+ }
71
+ }
72
+ /** Fetches one row by key through `byKey`; `null` when the store cannot or does not know it. */
73
+ async lookup(key) {
74
+ const store = this.cfg.store();
75
+ if (!store?.byKey) {
76
+ return null;
77
+ }
78
+ const controller = new AbortController();
79
+ try {
80
+ return (await firstValueFrom(from(store.byKey(key, { signal: controller.signal })))) ?? null;
81
+ }
82
+ catch {
83
+ return null;
84
+ }
85
+ }
86
+ settled(text) {
87
+ if (text === '' || this.reachesMinimum(text)) {
88
+ this.cfg.searchChange({ searchValue: text });
89
+ }
90
+ if (this.hasStore) {
91
+ this.query(text);
92
+ }
93
+ }
94
+ /** The text actually sent to the store: below the minimum nothing is searched. */
95
+ effective(text) {
96
+ if (this.reachesMinimum(text)) {
97
+ return text;
98
+ }
99
+ return this.cfg.showDataBeforeSearch() ? '' : null;
100
+ }
101
+ query(text) {
102
+ const effective = this.effective(text);
103
+ if (effective === null) {
104
+ this.cancel();
105
+ this.items.set([]);
106
+ this.hasMore.set(false);
107
+ this.loadedFor = null;
108
+ this.cfg.loaded?.();
109
+ return;
110
+ }
111
+ void this.run(effective, false);
112
+ }
113
+ cancel() {
114
+ this.seq++;
115
+ this.controller?.abort();
116
+ this.controller = null;
117
+ this.loading.set(false);
118
+ }
119
+ async run(text, append) {
120
+ const store = this.cfg.store();
121
+ if (!store) {
122
+ return;
123
+ }
124
+ this.cancel();
125
+ const seq = this.seq;
126
+ const controller = new AbortController();
127
+ this.controller = controller;
128
+ this.loading.set(true);
129
+ this.error.set(null);
130
+ const take = Math.max(1, this.cfg.pageSize());
131
+ const skip = append ? this.items().length : 0;
132
+ try {
133
+ const raw = await firstValueFrom(from(store.load({ searchValue: text, searchExpr: this.cfg.searchExpr(), searchOperation: 'contains', skip, take }, { signal: controller.signal })));
134
+ if (seq !== this.seq) {
135
+ return;
136
+ }
137
+ const data = Array.isArray(raw) ? raw : (raw.data ?? []);
138
+ const total = Array.isArray(raw) ? undefined : raw.totalCount;
139
+ const next = append ? [...this.items(), ...data] : data;
140
+ this.items.set(next);
141
+ this.hasMore.set(total !== undefined ? next.length < total : data.length >= take);
142
+ this.loadedFor = text;
143
+ this.loading.set(false);
144
+ this.cfg.loaded?.();
145
+ }
146
+ catch (error) {
147
+ if (seq !== this.seq) {
148
+ return;
149
+ }
150
+ this.error.set(error);
151
+ this.loading.set(false);
152
+ }
153
+ }
154
+ }
11
155
 
12
156
  /**
13
157
  * `nxe-select-box` — drop-in replacement for DevExtreme `dx-select-box`.
14
158
  *
15
159
  * A signal-CVA single-select dropdown over a plain array (`[dataSource]`). It bakes in the app's
16
160
  * global `dxSelectBox.defaultOptions` (`displayExpr='lovName'`, `valueExpr='lovCode'`,
17
- * `searchEnabled`, `showClearButton`), so the bound control value is the `valueExpr` field
18
- * (a string code) and the field shows the `displayExpr` field. Dropdown is a CDK overlay with
19
- * an in-field search filter. Emits `(valueChange)` (the code) and `(onSelectionChanged)`
20
- * (`{selectedItem}`). Hosts a projected `nxe-validator` via {@link NXE_VALIDATION_TARGET}.
161
+ * `searchEnabled`), so the bound control value is the `valueExpr` field (a string code) and the
162
+ * field shows the `displayExpr` field. The clear button follows dx and is off unless asked for.
163
+ * Dropdown is a CDK overlay with an in-field search filter. Emits `(valueChange)` (the code) and
164
+ * `(onSelectionChanged)` (`{selectedItem}`). Hosts a projected `nxe-validator` via
165
+ * {@link NXE_VALIDATION_TARGET}.
21
166
  */
22
167
  class NxeSelectBoxComponent extends NxeFormControlBase {
23
168
  overlay = inject(Overlay);
@@ -28,7 +173,10 @@ class NxeSelectBoxComponent extends NxeFormControlBase {
28
173
  ...(ngDevMode ? [{ debugName: "inputEl" }] : /* istanbul ignore next */ []));
29
174
  templates = contentChildren(NxeTemplateDirective, /* @ts-ignore */
30
175
  ...(ngDevMode ? [{ debugName: "templates" }] : /* istanbul ignore next */ []));
31
- /** The source array (plain JS array of `Lov`-shaped objects, or primitives). */
176
+ /**
177
+ * The source: a plain array of `Lov`-shaped objects (or primitives), or an {@link NxePickerStore}
178
+ * whose `load({searchValue, skip, take})` the box calls itself (server-side search).
179
+ */
32
180
  dataSource = input([], /* @ts-ignore */
33
181
  ...(ngDevMode ? [{ debugName: "dataSource" }] : /* istanbul ignore next */ []));
34
182
  /** Field shown in the box/list (DevExtreme default `lovName`). Empty => the item itself. */
@@ -41,13 +189,24 @@ class NxeSelectBoxComponent extends NxeFormControlBase {
41
189
  ...(ngDevMode ? [{ debugName: "valueExpr" }] : /* istanbul ignore next */ []));
42
190
  /** Field matched while searching (DevExtreme default `lovName`). */
43
191
  searchExprInput = input(undefined, { ...(ngDevMode ? { debugName: "searchExprInput" } : /* istanbul ignore next */ {}), alias: 'searchExpr' });
44
- searchExpr = computed(() => this.searchExprInput() ?? this.nxeDefaults.selectBox?.searchExpr ?? 'lovName', /* @ts-ignore */
192
+ /**
193
+ * dx searches by `displayExpr` when `searchExpr` is not given. Defaulting to the literal
194
+ * 'lovName' instead meant that any picker with a custom displayExpr searched a field its
195
+ * items do not have, and found nothing - silently, because an empty result looks like no
196
+ * match. Where displayExpr is the default this resolves to 'lovName' exactly as before.
197
+ */
198
+ searchExpr = computed(() => this.searchExprInput() ?? this.nxeDefaults.selectBox?.searchExpr ?? this.displayExpr(), /* @ts-ignore */
45
199
  ...(ngDevMode ? [{ debugName: "searchExpr" }] : /* istanbul ignore next */ []));
46
200
  searchEnabledInput = input(undefined, { ...(ngDevMode ? { debugName: "searchEnabledInput" } : /* istanbul ignore next */ {}), alias: 'searchEnabled', transform: nxeOptionalBoolean });
47
201
  searchEnabled = computed(() => this.searchEnabledInput() ?? this.nxeDefaults.selectBox?.searchEnabled ?? true, /* @ts-ignore */
48
202
  ...(ngDevMode ? [{ debugName: "searchEnabled" }] : /* istanbul ignore next */ []));
49
203
  showClearButtonInput = input(undefined, { ...(ngDevMode ? { debugName: "showClearButtonInput" } : /* istanbul ignore next */ {}), alias: 'showClearButton', transform: nxeOptionalBoolean });
50
- showClearButton = computed(() => this.showClearButtonInput() ?? this.nxeDefaults.selectBox?.showClearButton ?? true, /* @ts-ignore */
204
+ /**
205
+ * dx defaults `showClearButton` to false (m_text_editor.base.js). The library used to default it
206
+ * on, which gave every host that never asked for one a clear button dx would not show. An app
207
+ * that wants it everywhere says so once through `NXE_DEFAULT_OPTIONS.selectBox.showClearButton`.
208
+ */
209
+ showClearButton = computed(() => this.showClearButtonInput() ?? this.nxeDefaults.selectBox?.showClearButton ?? false, /* @ts-ignore */
51
210
  ...(ngDevMode ? [{ debugName: "showClearButton" }] : /* istanbul ignore next */ []));
52
211
  /** dx parity: dx-select-box shows its drop-down arrow unless showDropDownButton=false. */
53
212
  showDropDownButtonInput = input(undefined, { ...(ngDevMode ? { debugName: "showDropDownButtonInput" } : /* istanbul ignore next */ {}), alias: 'showDropDownButton', transform: nxeOptionalBoolean });
@@ -62,9 +221,29 @@ class NxeSelectBoxComponent extends NxeFormControlBase {
62
221
  /** Named template ('inputField') for the closed field; `$implicit` = the selected item. */
63
222
  fieldTemplate = input(/* @ts-ignore */
64
223
  ...(ngDevMode ? [undefined, { debugName: "fieldTemplate" }] : /* istanbul ignore next */ []));
224
+ /** dx `searchTimeout`: ms of typing pause before `(searchChange)` fires or a store is queried. */
225
+ searchTimeout = input(500, { ...(ngDevMode ? { debugName: "searchTimeout" } : /* istanbul ignore next */ {}), transform: numberAttribute });
226
+ /** dx `minSearchLength`: characters needed before a search is emitted, applied or sent. */
227
+ minSearchLength = input(0, { ...(ngDevMode ? { debugName: "minSearchLength" } : /* istanbul ignore next */ {}), transform: numberAttribute });
228
+ /** dx `showDataBeforeSearch`: list rows while the text is shorter than `minSearchLength`. */
229
+ showDataBeforeSearch = input(false, { ...(ngDevMode ? { debugName: "showDataBeforeSearch" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
230
+ /** Rows requested per page from a store `dataSource` (`take`); more load on scroll to the end. */
231
+ pageSize = input(20, { ...(ngDevMode ? { debugName: "pageSize" } : /* istanbul ignore next */ {}), transform: numberAttribute });
232
+ /**
233
+ * dx `displayValue`: the text shown for the current value while its row is not among the loaded
234
+ * ones (a store `dataSource` with the selection outside the current page, and no `byKey`).
235
+ */
236
+ displayValue = input(undefined, /* @ts-ignore */
237
+ ...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
65
238
  /** DevExtreme parity, cosmetic. */
66
239
  hoverStateEnabled = input(true, { ...(ngDevMode ? { debugName: "hoverStateEnabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
67
240
  onSelectionChanged = output();
241
+ /**
242
+ * The search text settled: fires `searchTimeout` ms after the last keystroke, once the text has
243
+ * `minSearchLength` characters (or was cleared). Use it to query a server and replace
244
+ * `[dataSource]`, or pass a {@link NxePickerStore} and let the box query for you.
245
+ */
246
+ searchChange = output();
68
247
  focused = signal(false, /* @ts-ignore */
69
248
  ...(ngDevMode ? [{ debugName: "focused" }] : /* istanbul ignore next */ []));
70
249
  open = signal(false, /* @ts-ignore */
@@ -83,25 +262,51 @@ class NxeSelectBoxComponent extends NxeFormControlBase {
83
262
  overlayRef;
84
263
  closeSub;
85
264
  flipSub;
86
- items = computed(() => this.dataSource() ?? [], /* @ts-ignore */
265
+ store = computed(() => {
266
+ const ds = this.dataSource();
267
+ return nxeIsPickerStore(ds) ? ds : null;
268
+ }, /* @ts-ignore */
269
+ ...(ngDevMode ? [{ debugName: "store" }] : /* istanbul ignore next */ []));
270
+ search = new NxePickerSearch({
271
+ store: this.store,
272
+ searchExpr: this.searchExpr,
273
+ searchTimeout: this.searchTimeout,
274
+ minSearchLength: this.minSearchLength,
275
+ showDataBeforeSearch: this.showDataBeforeSearch,
276
+ pageSize: this.pageSize,
277
+ searchChange: (event) => this.searchChange.emit(event),
278
+ loaded: () => this.activeIndex.set(this.filteredItems().length ? 0 : -1),
279
+ });
280
+ /** Rows picked or fetched by key from a store, so the label outlives the page that held them. */
281
+ resolved = signal(new Map(), /* @ts-ignore */
282
+ ...(ngDevMode ? [{ debugName: "resolved" }] : /* istanbul ignore next */ []));
283
+ items = computed(() => {
284
+ const ds = this.dataSource();
285
+ return nxeIsPickerStore(ds) ? this.search.items() : (ds ?? []);
286
+ }, /* @ts-ignore */
87
287
  ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
288
+ /** Fetches the row of an externally set value through `byKey` when no loaded row has it. */
289
+ keyLookup = toObservable(this.value)
290
+ .pipe(filter((v) => v != null && v !== '' && this.search.hasStore && this.findRow(v) === undefined), switchMap((v) => from(this.search.lookup(v)).pipe(catchError(() => EMPTY), filter((row) => row != null), map((row) => [v, row]))), takeUntilDestroyed())
291
+ .subscribe(([key, row]) => this.remember(key, row));
292
+ findRow(key) {
293
+ return this.items().find((it) => this.extractValue(it) === key) ?? this.resolved().get(key);
294
+ }
295
+ remember(key, row) {
296
+ this.resolved.update((m) => new Map(m).set(key, row));
297
+ }
88
298
  display(item) {
89
- const expr = this.displayExpr();
90
- if (!expr) {
91
- return item == null ? '' : String(item);
92
- }
93
- return item?.[expr] ?? '';
299
+ return nxeResolveText(this.displayExpr(), item);
94
300
  }
95
301
  extractValue(item) {
96
- const expr = this.valueExpr();
97
- return expr ? item?.[expr] : item;
302
+ return nxeResolveExpr(this.valueExpr(), item);
98
303
  }
99
304
  selectedItem = computed(() => {
100
305
  const v = this.value();
101
306
  if (v == null) {
102
307
  return null;
103
308
  }
104
- return this.items().find((it) => this.extractValue(it) === v) ?? null;
309
+ return this.findRow(v) ?? null;
105
310
  }, /* @ts-ignore */
106
311
  ...(ngDevMode ? [{ debugName: "selectedItem" }] : /* istanbul ignore next */ []));
107
312
  hasValue = computed(() => this.value() != null && this.value() !== '', /* @ts-ignore */
@@ -109,7 +314,10 @@ class NxeSelectBoxComponent extends NxeFormControlBase {
109
314
  /** Display text when the field is not being searched. */
110
315
  displayText = computed(() => {
111
316
  const sel = this.selectedItem();
112
- return sel ? this.display(sel) : '';
317
+ if (sel) {
318
+ return this.display(sel);
319
+ }
320
+ return this.hasValue() ? (this.displayValue() ?? '') : '';
113
321
  }, /* @ts-ignore */
114
322
  ...(ngDevMode ? [{ debugName: "displayText" }] : /* istanbul ignore next */ []));
115
323
  /** Value bound to the input element. */
@@ -121,15 +329,18 @@ class NxeSelectBoxComponent extends NxeFormControlBase {
121
329
  filteredItems = computed(() => {
122
330
  const items = this.items();
123
331
  const search = this.searchText();
124
- if (!this.searchEnabled() || !search) {
332
+ if (this.search.hasStore) {
125
333
  return items;
126
334
  }
127
- const expr = this.searchExpr() || this.displayExpr();
335
+ if (!this.searchEnabled() || !search) {
336
+ return this.minSearchLength() > 0 && !this.showDataBeforeSearch() ? [] : items;
337
+ }
338
+ if (!this.search.reachesMinimum(search)) {
339
+ return this.showDataBeforeSearch() ? items : [];
340
+ }
341
+ const expr = this.searchExpr();
128
342
  const needle = search.toLowerCase();
129
- return items.filter((it) => {
130
- const hay = (expr ? it?.[expr] : it) ?? '';
131
- return String(hay).toLowerCase().includes(needle);
132
- });
343
+ return items.filter((it) => nxeResolveText(expr, it).toLowerCase().includes(needle));
133
344
  }, /* @ts-ignore */
134
345
  ...(ngDevMode ? [{ debugName: "filteredItems" }] : /* istanbul ignore next */ []));
135
346
  itemTemplateRef = computed(() => this.templates().find((t) => t.name() === this.itemTemplate())?.templateRef ?? null, /* @ts-ignore */
@@ -159,12 +370,21 @@ class NxeSelectBoxComponent extends NxeFormControlBase {
159
370
  this.openPanel();
160
371
  }
161
372
  onSearchInput(event) {
162
- this.searchText.set(event.target.value);
373
+ const text = event.target.value;
374
+ this.searchText.set(text);
375
+ this.search.typed(text);
163
376
  this.activeIndex.set(0);
164
377
  if (!this.open()) {
165
378
  this.openPanel();
166
379
  }
167
380
  }
381
+ /** Loads the next page of a store `dataSource` when the list is scrolled to its end. */
382
+ onPanelScroll(event) {
383
+ const el = event.target;
384
+ if (el.scrollTop + el.clientHeight >= el.scrollHeight - 8) {
385
+ this.search.loadMore();
386
+ }
387
+ }
168
388
  onBlur() {
169
389
  this.focused.set(false);
170
390
  this.markAsTouched();
@@ -175,6 +395,9 @@ class NxeSelectBoxComponent extends NxeFormControlBase {
175
395
  event.preventDefault();
176
396
  const previousValue = this.value();
177
397
  const next = this.extractValue(item);
398
+ if (this.search.hasStore) {
399
+ this.remember(next, item);
400
+ }
178
401
  this.commitValue(next, event);
179
402
  this.onSelectionChanged.emit({ selectedItem: item, previousValue, value: next });
180
403
  this.searchText.set(null);
@@ -226,6 +449,7 @@ class NxeSelectBoxComponent extends NxeFormControlBase {
226
449
  this.panelWidth.set(this.elementRef.nativeElement.offsetWidth);
227
450
  const idx = this.items().findIndex((it) => this.extractValue(it) === this.value());
228
451
  this.activeIndex.set(idx >= 0 ? idx : 0);
452
+ this.search.open(this.searchText() ?? '');
229
453
  if (!this.overlayRef) {
230
454
  this.overlayRef = this.overlay.create({
231
455
  positionStrategy: this.overlay
@@ -264,31 +488,33 @@ class NxeSelectBoxComponent extends NxeFormControlBase {
264
488
  this.overlayRef?.dispose();
265
489
  }
266
490
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeSelectBoxComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
267
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeSelectBoxComponent, isStandalone: true, selector: "nxe-select-box", inputs: { dataSource: { classPropertyName: "dataSource", publicName: "dataSource", isSignal: true, isRequired: false, transformFunction: null }, displayExprInput: { classPropertyName: "displayExprInput", publicName: "displayExpr", isSignal: true, isRequired: false, transformFunction: null }, valueExprInput: { classPropertyName: "valueExprInput", publicName: "valueExpr", isSignal: true, isRequired: false, transformFunction: null }, searchExprInput: { classPropertyName: "searchExprInput", publicName: "searchExpr", isSignal: true, isRequired: false, transformFunction: null }, searchEnabledInput: { classPropertyName: "searchEnabledInput", publicName: "searchEnabled", isSignal: true, isRequired: false, transformFunction: null }, showClearButtonInput: { classPropertyName: "showClearButtonInput", publicName: "showClearButton", isSignal: true, isRequired: false, transformFunction: null }, showDropDownButtonInput: { classPropertyName: "showDropDownButtonInput", publicName: "showDropDownButton", isSignal: true, isRequired: false, transformFunction: null }, placeholderInput: { classPropertyName: "placeholderInput", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, itemTemplate: { classPropertyName: "itemTemplate", publicName: "itemTemplate", isSignal: true, isRequired: false, transformFunction: null }, fieldTemplate: { classPropertyName: "fieldTemplate", publicName: "fieldTemplate", isSignal: true, isRequired: false, transformFunction: null }, hoverStateEnabled: { classPropertyName: "hoverStateEnabled", publicName: "hoverStateEnabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSelectionChanged: "onSelectionChanged" }, host: { listeners: { "keydown": "onKeydown($event)" }, properties: { "class.nxe-focused": "focused()", "class.nxe-invalid": "!!validationError()", "class.nxe-open": "open()" }, classAttribute: "nxe-widget nxe-editor nxe-selectbox" }, providers: [
491
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeSelectBoxComponent, isStandalone: true, selector: "nxe-select-box", inputs: { dataSource: { classPropertyName: "dataSource", publicName: "dataSource", isSignal: true, isRequired: false, transformFunction: null }, displayExprInput: { classPropertyName: "displayExprInput", publicName: "displayExpr", isSignal: true, isRequired: false, transformFunction: null }, valueExprInput: { classPropertyName: "valueExprInput", publicName: "valueExpr", isSignal: true, isRequired: false, transformFunction: null }, searchExprInput: { classPropertyName: "searchExprInput", publicName: "searchExpr", isSignal: true, isRequired: false, transformFunction: null }, searchEnabledInput: { classPropertyName: "searchEnabledInput", publicName: "searchEnabled", isSignal: true, isRequired: false, transformFunction: null }, showClearButtonInput: { classPropertyName: "showClearButtonInput", publicName: "showClearButton", isSignal: true, isRequired: false, transformFunction: null }, showDropDownButtonInput: { classPropertyName: "showDropDownButtonInput", publicName: "showDropDownButton", isSignal: true, isRequired: false, transformFunction: null }, placeholderInput: { classPropertyName: "placeholderInput", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, itemTemplate: { classPropertyName: "itemTemplate", publicName: "itemTemplate", isSignal: true, isRequired: false, transformFunction: null }, fieldTemplate: { classPropertyName: "fieldTemplate", publicName: "fieldTemplate", isSignal: true, isRequired: false, transformFunction: null }, searchTimeout: { classPropertyName: "searchTimeout", publicName: "searchTimeout", isSignal: true, isRequired: false, transformFunction: null }, minSearchLength: { classPropertyName: "minSearchLength", publicName: "minSearchLength", isSignal: true, isRequired: false, transformFunction: null }, showDataBeforeSearch: { classPropertyName: "showDataBeforeSearch", publicName: "showDataBeforeSearch", isSignal: true, isRequired: false, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, displayValue: { classPropertyName: "displayValue", publicName: "displayValue", isSignal: true, isRequired: false, transformFunction: null }, hoverStateEnabled: { classPropertyName: "hoverStateEnabled", publicName: "hoverStateEnabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSelectionChanged: "onSelectionChanged", searchChange: "searchChange" }, host: { listeners: { "keydown": "onKeydown($event)" }, properties: { "class.nxe-focused": "focused()", "class.nxe-invalid": "invalidState()", "class.nxe-open": "open()" }, classAttribute: "nxe-widget nxe-editor nxe-selectbox" }, providers: [
268
492
  { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeSelectBoxComponent), multi: true },
269
493
  { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeSelectBoxComponent) },
270
- ], queries: [{ propertyName: "templates", predicate: NxeTemplateDirective, isSignal: true }], viewQueries: [{ propertyName: "dropdownTemplate", first: true, predicate: ["dropdown"], descendants: true, isSignal: true }, { propertyName: "inputEl", first: true, predicate: ["input"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div class=\"nxe-editor-container\" (mousedown)=\"onFieldMousedown($event)\">\r\n @if (fieldTemplateRef(); as tpl) {\r\n <div class=\"nxe-select-field\">\r\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: selectedItem() }\" />\r\n </div>\r\n } @else {\r\n <input\r\n #input\r\n class=\"nxe-editor-input nxe-select-input\"\r\n type=\"text\"\r\n [value]=\"inputDisplay()\"\r\n [readOnly]=\"readOnly() || !searchEnabled()\"\r\n [disabled]=\"disabled()\"\r\n [attr.placeholder]=\"placeholder() || null\"\r\n autocomplete=\"off\"\r\n (input)=\"onSearchInput($event)\"\r\n (focus)=\"focused.set(true)\"\r\n (blur)=\"onBlur()\" />\r\n }\r\n\r\n @if (showClearButton() && hasValue() && !readOnly() && !disabled()) {\r\n <span class=\"nxe-select-clear\" (mousedown)=\"onClear($event)\" aria-hidden=\"true\">\r\n <svg viewBox=\"0 0 24 24\"><path d=\"M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z\" /></svg>\r\n </span>\r\n }\r\n\r\n @if (showDropDownButton() && !readOnly()) {\r\n <span class=\"nxe-select-arrow\" aria-hidden=\"true\">\r\n <svg viewBox=\"0 0 24 24\"><path d=\"M7 10l5 5 5-5z\" /></svg>\r\n </span>\r\n }\r\n</div>\r\n\r\n<ng-content />\r\n\r\n@if (validationError(); as error) {\r\n @if (focused()) {\r\n <div class=\"nxe-invalid-message\">{{ error }}</div>\r\n }\r\n}\r\n\r\n<ng-template #dropdown>\r\n <div class=\"nxe-select-panel\" [style.width.px]=\"panelWidth()\">\r\n @for (item of filteredItems(); track $index; let i = $index) {\r\n <div\r\n class=\"nxe-list-item\"\r\n [class.nxe-list-item-selected]=\"isSelected(item)\"\r\n [class.nxe-list-item-active]=\"i === activeIndex()\"\r\n (mousedown)=\"selectItem(item, $event)\">\r\n @if (itemTemplateRef(); as tpl) {\r\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: item }\" />\r\n } @else {\r\n {{ display(item) }}\r\n }\r\n </div>\r\n } @empty {\r\n <div class=\"nxe-list-empty\">No data to display</div>\r\n }\r\n </div>\r\n</ng-template>\r\n", styles: ["nxe-select-box{cursor:pointer}nxe-select-box .nxe-select-input{cursor:pointer}nxe-select-box .nxe-select-clear,nxe-select-box .nxe-select-arrow{flex:0 0 auto;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;color:#333}nxe-select-box .nxe-select-clear svg,nxe-select-box .nxe-select-arrow svg{width:16px;height:16px;fill:currentColor}nxe-select-box .nxe-select-clear{width:22px;color:#999;cursor:pointer}nxe-select-box .nxe-select-clear:hover{color:#333}nxe-select-box .nxe-editor-container:has(.nxe-select-arrow) .nxe-select-input{padding-inline-end:0}nxe-select-box .nxe-select-arrow{position:relative;width:34px;min-width:34px;padding:1px;border-radius:4px}nxe-select-box .nxe-select-arrow svg{width:18px;height:18px}nxe-select-box .nxe-editor-container:has(>.nxe-select-arrow){--nxe-invalid-badge-inset: 38px}nxe-select-box .nxe-editor-container:has(>.nxe-select-clear){--nxe-invalid-badge-inset: 26px}nxe-select-box .nxe-editor-container:has(>.nxe-select-arrow):has(>.nxe-select-clear){--nxe-invalid-badge-inset: 60px}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
494
+ ], queries: [{ propertyName: "templates", predicate: NxeTemplateDirective, isSignal: true }], viewQueries: [{ propertyName: "dropdownTemplate", first: true, predicate: ["dropdown"], descendants: true, isSignal: true }, { propertyName: "inputEl", first: true, predicate: ["input"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div class=\"nxe-editor-container\" (mousedown)=\"onFieldMousedown($event)\">\n @if (fieldTemplateRef(); as tpl) {\n <div class=\"nxe-select-field\">\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: selectedItem() }\" />\n </div>\n } @else {\n <input\n #input\n class=\"nxe-editor-input nxe-select-input\"\n [attr.tabindex]=\"tabIndex()\"\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"open() ? panelId() : null\"\n [attr.aria-activedescendant]=\"open() && activeIndex() >= 0 ? optionId(activeIndex()) : null\"\n [id]=\"controlId()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-labelledby]=\"ariaLabelledBy() || null\"\n [attr.aria-invalid]=\"invalidState() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n type=\"text\"\n [value]=\"inputDisplay()\"\n [readOnly]=\"readOnly() || !searchEnabled()\"\n [disabled]=\"disabled()\"\n [attr.placeholder]=\"placeholder() || null\"\n autocomplete=\"off\"\n (input)=\"onSearchInput($event)\"\n (focus)=\"focused.set(true)\"\n (blur)=\"onBlur()\" />\n }\n\n @if (showClearButton() && hasValue() && !readOnly() && !disabled()) {\n <span class=\"nxe-select-clear\" (mousedown)=\"onClear($event)\" aria-hidden=\"true\">\n <!-- A \u00D816 disc with a 2px \u00D7 cut out: two 2px bars, each running corner to corner of the\n 8px square centred on the disc, drawn as a plus and turned 45\u00B0. -->\n <svg viewBox=\"0 0 18 18\" focusable=\"false\">\n <path fill-rule=\"evenodd\" transform=\"rotate(45 9 9)\"\n d=\"M9 1a8 8 0 1 1 0 16A8 8 0 1 1 9 1ZM8 3.343h2V8h4.657v2H10v4.657H8V10H3.343V8H8Z\" />\n </svg>\n </span>\n }\n\n @if (showDropDownButton() && !readOnly()) {\n <span class=\"nxe-select-arrow\" aria-hidden=\"true\">\n <!-- An 8x4 triangle centred in the 18px box. -->\n <svg viewBox=\"0 0 18 18\" focusable=\"false\"><path d=\"M5 7h8l-4 4z\" /></svg>\n </span>\n }\n</div>\n\n<ng-content />\n\n@if (validationError(); as error) {\n @if (focused()) {\n <div class=\"nxe-invalid-message\">{{ error }}</div>\n }\n}\n\n<ng-template #dropdown>\n <div class=\"nxe-select-panel\" [style.width.px]=\"panelWidth()\"\n [id]=\"panelId()\"\n role=\"listbox\"\n (scroll)=\"onPanelScroll($event)\">\n @for (item of filteredItems(); track $index; let i = $index) {\n <div\n class=\"nxe-list-item\"\n [id]=\"optionId(i)\"\n role=\"option\"\n [attr.aria-selected]=\"isSelected(item)\"\n [class.nxe-list-item-selected]=\"isSelected(item)\"\n [class.nxe-list-item-active]=\"i === activeIndex()\"\n (mousedown)=\"selectItem(item, $event)\">\n @if (itemTemplateRef(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: item }\" />\n } @else {\n {{ display(item) }}\n }\n </div>\n } @empty {\n <div class=\"nxe-list-empty\">{{ nxeMessages.t('dropdown.noData') }}</div>\n }\n </div>\n</ng-template>\n", styles: ["nxe-select-box{cursor:pointer}nxe-select-box .nxe-select-input{cursor:pointer}nxe-select-box .nxe-select-clear{box-sizing:border-box;flex:0 0 34px;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;width:34px;min-width:34px;padding:0;color:var(--nxe-ui-text-muted, #999);cursor:pointer}nxe-select-box .nxe-select-clear svg{display:block;width:18px;height:18px;fill:currentColor}nxe-select-box .nxe-select-arrow{box-sizing:border-box;position:relative;flex:0 0 34px;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;width:34px;min-width:34px;padding:1px;border-radius:4px;background-clip:content-box;color:var(--nxe-ui-text, #333);cursor:pointer}nxe-select-box .nxe-select-arrow svg{display:block;width:18px;height:18px;fill:currentColor}nxe-select-box:hover:not(.nxe-disabled) .nxe-select-arrow,nxe-select-box:active:not(.nxe-disabled) .nxe-select-arrow{background-color:var(--nxe-ui-neutral-150, #e6e6e6)}nxe-select-box.nxe-open .nxe-select-arrow{background-color:var(--nxe-ui-neutral-350, #b3b3b3)}nxe-select-box .nxe-editor-container:has(>.nxe-select-arrow) .nxe-editor-input{padding-inline-end:0}nxe-select-box .nxe-editor-container:has(>.nxe-select-clear),nxe-select-box .nxe-editor-container:has(>.nxe-select-arrow){--nxe-invalid-badge-inset: 38px}nxe-select-box .nxe-editor-container:has(>.nxe-select-clear):has(>.nxe-select-arrow){--nxe-invalid-badge-inset: 72px}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
271
495
  }
272
496
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeSelectBoxComponent, decorators: [{
273
497
  type: Component,
274
498
  args: [{ selector: 'nxe-select-box', standalone: true, imports: [NgTemplateOutlet], host: {
275
499
  class: 'nxe-widget nxe-editor nxe-selectbox',
276
500
  '[class.nxe-focused]': 'focused()',
277
- '[class.nxe-invalid]': '!!validationError()',
501
+ '[class.nxe-invalid]': 'invalidState()',
278
502
  '[class.nxe-open]': 'open()',
279
503
  '(keydown)': 'onKeydown($event)',
280
504
  }, providers: [
281
505
  { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeSelectBoxComponent), multi: true },
282
506
  { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeSelectBoxComponent) },
283
- ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<div class=\"nxe-editor-container\" (mousedown)=\"onFieldMousedown($event)\">\r\n @if (fieldTemplateRef(); as tpl) {\r\n <div class=\"nxe-select-field\">\r\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: selectedItem() }\" />\r\n </div>\r\n } @else {\r\n <input\r\n #input\r\n class=\"nxe-editor-input nxe-select-input\"\r\n type=\"text\"\r\n [value]=\"inputDisplay()\"\r\n [readOnly]=\"readOnly() || !searchEnabled()\"\r\n [disabled]=\"disabled()\"\r\n [attr.placeholder]=\"placeholder() || null\"\r\n autocomplete=\"off\"\r\n (input)=\"onSearchInput($event)\"\r\n (focus)=\"focused.set(true)\"\r\n (blur)=\"onBlur()\" />\r\n }\r\n\r\n @if (showClearButton() && hasValue() && !readOnly() && !disabled()) {\r\n <span class=\"nxe-select-clear\" (mousedown)=\"onClear($event)\" aria-hidden=\"true\">\r\n <svg viewBox=\"0 0 24 24\"><path d=\"M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z\" /></svg>\r\n </span>\r\n }\r\n\r\n @if (showDropDownButton() && !readOnly()) {\r\n <span class=\"nxe-select-arrow\" aria-hidden=\"true\">\r\n <svg viewBox=\"0 0 24 24\"><path d=\"M7 10l5 5 5-5z\" /></svg>\r\n </span>\r\n }\r\n</div>\r\n\r\n<ng-content />\r\n\r\n@if (validationError(); as error) {\r\n @if (focused()) {\r\n <div class=\"nxe-invalid-message\">{{ error }}</div>\r\n }\r\n}\r\n\r\n<ng-template #dropdown>\r\n <div class=\"nxe-select-panel\" [style.width.px]=\"panelWidth()\">\r\n @for (item of filteredItems(); track $index; let i = $index) {\r\n <div\r\n class=\"nxe-list-item\"\r\n [class.nxe-list-item-selected]=\"isSelected(item)\"\r\n [class.nxe-list-item-active]=\"i === activeIndex()\"\r\n (mousedown)=\"selectItem(item, $event)\">\r\n @if (itemTemplateRef(); as tpl) {\r\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: item }\" />\r\n } @else {\r\n {{ display(item) }}\r\n }\r\n </div>\r\n } @empty {\r\n <div class=\"nxe-list-empty\">No data to display</div>\r\n }\r\n </div>\r\n</ng-template>\r\n", styles: ["nxe-select-box{cursor:pointer}nxe-select-box .nxe-select-input{cursor:pointer}nxe-select-box .nxe-select-clear,nxe-select-box .nxe-select-arrow{flex:0 0 auto;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;color:#333}nxe-select-box .nxe-select-clear svg,nxe-select-box .nxe-select-arrow svg{width:16px;height:16px;fill:currentColor}nxe-select-box .nxe-select-clear{width:22px;color:#999;cursor:pointer}nxe-select-box .nxe-select-clear:hover{color:#333}nxe-select-box .nxe-editor-container:has(.nxe-select-arrow) .nxe-select-input{padding-inline-end:0}nxe-select-box .nxe-select-arrow{position:relative;width:34px;min-width:34px;padding:1px;border-radius:4px}nxe-select-box .nxe-select-arrow svg{width:18px;height:18px}nxe-select-box .nxe-editor-container:has(>.nxe-select-arrow){--nxe-invalid-badge-inset: 38px}nxe-select-box .nxe-editor-container:has(>.nxe-select-clear){--nxe-invalid-badge-inset: 26px}nxe-select-box .nxe-editor-container:has(>.nxe-select-arrow):has(>.nxe-select-clear){--nxe-invalid-badge-inset: 60px}\n"] }]
284
- }], propDecorators: { dropdownTemplate: [{ type: i0.ViewChild, args: ['dropdown', { isSignal: true }] }], inputEl: [{ type: i0.ViewChild, args: ['input', { isSignal: true }] }], templates: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => NxeTemplateDirective), { isSignal: true }] }], dataSource: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataSource", required: false }] }], displayExprInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayExpr", required: false }] }], valueExprInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueExpr", required: false }] }], searchExprInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchExpr", required: false }] }], searchEnabledInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchEnabled", required: false }] }], showClearButtonInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClearButton", required: false }] }], showDropDownButtonInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "showDropDownButton", required: false }] }], placeholderInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], itemTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemTemplate", required: false }] }], fieldTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "fieldTemplate", required: false }] }], hoverStateEnabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "hoverStateEnabled", required: false }] }], onSelectionChanged: [{ type: i0.Output, args: ["onSelectionChanged"] }] } });
507
+ ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<div class=\"nxe-editor-container\" (mousedown)=\"onFieldMousedown($event)\">\n @if (fieldTemplateRef(); as tpl) {\n <div class=\"nxe-select-field\">\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: selectedItem() }\" />\n </div>\n } @else {\n <input\n #input\n class=\"nxe-editor-input nxe-select-input\"\n [attr.tabindex]=\"tabIndex()\"\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"open() ? panelId() : null\"\n [attr.aria-activedescendant]=\"open() && activeIndex() >= 0 ? optionId(activeIndex()) : null\"\n [id]=\"controlId()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-labelledby]=\"ariaLabelledBy() || null\"\n [attr.aria-invalid]=\"invalidState() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n type=\"text\"\n [value]=\"inputDisplay()\"\n [readOnly]=\"readOnly() || !searchEnabled()\"\n [disabled]=\"disabled()\"\n [attr.placeholder]=\"placeholder() || null\"\n autocomplete=\"off\"\n (input)=\"onSearchInput($event)\"\n (focus)=\"focused.set(true)\"\n (blur)=\"onBlur()\" />\n }\n\n @if (showClearButton() && hasValue() && !readOnly() && !disabled()) {\n <span class=\"nxe-select-clear\" (mousedown)=\"onClear($event)\" aria-hidden=\"true\">\n <!-- A \u00D816 disc with a 2px \u00D7 cut out: two 2px bars, each running corner to corner of the\n 8px square centred on the disc, drawn as a plus and turned 45\u00B0. -->\n <svg viewBox=\"0 0 18 18\" focusable=\"false\">\n <path fill-rule=\"evenodd\" transform=\"rotate(45 9 9)\"\n d=\"M9 1a8 8 0 1 1 0 16A8 8 0 1 1 9 1ZM8 3.343h2V8h4.657v2H10v4.657H8V10H3.343V8H8Z\" />\n </svg>\n </span>\n }\n\n @if (showDropDownButton() && !readOnly()) {\n <span class=\"nxe-select-arrow\" aria-hidden=\"true\">\n <!-- An 8x4 triangle centred in the 18px box. -->\n <svg viewBox=\"0 0 18 18\" focusable=\"false\"><path d=\"M5 7h8l-4 4z\" /></svg>\n </span>\n }\n</div>\n\n<ng-content />\n\n@if (validationError(); as error) {\n @if (focused()) {\n <div class=\"nxe-invalid-message\">{{ error }}</div>\n }\n}\n\n<ng-template #dropdown>\n <div class=\"nxe-select-panel\" [style.width.px]=\"panelWidth()\"\n [id]=\"panelId()\"\n role=\"listbox\"\n (scroll)=\"onPanelScroll($event)\">\n @for (item of filteredItems(); track $index; let i = $index) {\n <div\n class=\"nxe-list-item\"\n [id]=\"optionId(i)\"\n role=\"option\"\n [attr.aria-selected]=\"isSelected(item)\"\n [class.nxe-list-item-selected]=\"isSelected(item)\"\n [class.nxe-list-item-active]=\"i === activeIndex()\"\n (mousedown)=\"selectItem(item, $event)\">\n @if (itemTemplateRef(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: item }\" />\n } @else {\n {{ display(item) }}\n }\n </div>\n } @empty {\n <div class=\"nxe-list-empty\">{{ nxeMessages.t('dropdown.noData') }}</div>\n }\n </div>\n</ng-template>\n", styles: ["nxe-select-box{cursor:pointer}nxe-select-box .nxe-select-input{cursor:pointer}nxe-select-box .nxe-select-clear{box-sizing:border-box;flex:0 0 34px;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;width:34px;min-width:34px;padding:0;color:var(--nxe-ui-text-muted, #999);cursor:pointer}nxe-select-box .nxe-select-clear svg{display:block;width:18px;height:18px;fill:currentColor}nxe-select-box .nxe-select-arrow{box-sizing:border-box;position:relative;flex:0 0 34px;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;width:34px;min-width:34px;padding:1px;border-radius:4px;background-clip:content-box;color:var(--nxe-ui-text, #333);cursor:pointer}nxe-select-box .nxe-select-arrow svg{display:block;width:18px;height:18px;fill:currentColor}nxe-select-box:hover:not(.nxe-disabled) .nxe-select-arrow,nxe-select-box:active:not(.nxe-disabled) .nxe-select-arrow{background-color:var(--nxe-ui-neutral-150, #e6e6e6)}nxe-select-box.nxe-open .nxe-select-arrow{background-color:var(--nxe-ui-neutral-350, #b3b3b3)}nxe-select-box .nxe-editor-container:has(>.nxe-select-arrow) .nxe-editor-input{padding-inline-end:0}nxe-select-box .nxe-editor-container:has(>.nxe-select-clear),nxe-select-box .nxe-editor-container:has(>.nxe-select-arrow){--nxe-invalid-badge-inset: 38px}nxe-select-box .nxe-editor-container:has(>.nxe-select-clear):has(>.nxe-select-arrow){--nxe-invalid-badge-inset: 72px}\n"] }]
508
+ }], propDecorators: { dropdownTemplate: [{ type: i0.ViewChild, args: ['dropdown', { isSignal: true }] }], inputEl: [{ type: i0.ViewChild, args: ['input', { isSignal: true }] }], templates: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => NxeTemplateDirective), { isSignal: true }] }], dataSource: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataSource", required: false }] }], displayExprInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayExpr", required: false }] }], valueExprInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueExpr", required: false }] }], searchExprInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchExpr", required: false }] }], searchEnabledInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchEnabled", required: false }] }], showClearButtonInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClearButton", required: false }] }], showDropDownButtonInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "showDropDownButton", required: false }] }], placeholderInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], itemTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemTemplate", required: false }] }], fieldTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "fieldTemplate", required: false }] }], searchTimeout: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchTimeout", required: false }] }], minSearchLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "minSearchLength", required: false }] }], showDataBeforeSearch: [{ type: i0.Input, args: [{ isSignal: true, alias: "showDataBeforeSearch", required: false }] }], pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }], displayValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayValue", required: false }] }], hoverStateEnabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "hoverStateEnabled", required: false }] }], onSelectionChanged: [{ type: i0.Output, args: ["onSelectionChanged"] }], searchChange: [{ type: i0.Output, args: ["searchChange"] }] } });
285
509
 
286
510
  /**
287
511
  * `nxe-tag-box` — drop-in replacement for DevExtreme `dx-tag-box` (multi-select sibling of
288
512
  * `nxe-select-box`). The bound value is an **array of `valueExpr` keys** (`lovCode` strings).
289
513
  * Bakes in the app's global `dxTagBox.defaultOptions` (display/search `lovName`, value `lovCode`,
290
- * search + clear + selection-controls on). Selected items render as removable chips; the dropdown
291
- * shows a checkbox per row. Provides {@link NXE_VALIDATION_TARGET} for a projected `nxe-validator`.
514
+ * search + selection-controls on); the clear button follows dx and is off unless asked for.
515
+ * Selected items render as removable chips; with selection controls the dropdown shows a checkbox
516
+ * per row under a tri-state "Select All". Provides {@link NXE_VALIDATION_TARGET} for a projected
517
+ * `nxe-validator`.
292
518
  */
293
519
  class NxeTagBoxComponent extends NxeFormControlBase {
294
520
  overlay = inject(Overlay);
@@ -297,6 +523,7 @@ class NxeTagBoxComponent extends NxeFormControlBase {
297
523
  ...(ngDevMode ? [{ debugName: "dropdownTemplate" }] : /* istanbul ignore next */ []));
298
524
  inputEl = viewChild('input', /* @ts-ignore */
299
525
  ...(ngDevMode ? [{ debugName: "inputEl" }] : /* istanbul ignore next */ []));
526
+ /** A plain array, or an {@link NxePickerStore} the box queries itself (server-side search). */
300
527
  dataSource = input([], /* @ts-ignore */
301
528
  ...(ngDevMode ? [{ debugName: "dataSource" }] : /* istanbul ignore next */ []));
302
529
  displayExprInput = input(undefined, { ...(ngDevMode ? { debugName: "displayExprInput" } : /* istanbul ignore next */ {}), alias: 'displayExpr' });
@@ -306,14 +533,37 @@ class NxeTagBoxComponent extends NxeFormControlBase {
306
533
  valueExpr = computed(() => this.valueExprInput() ?? this.nxeDefaults.tagBox?.valueExpr ?? 'lovCode', /* @ts-ignore */
307
534
  ...(ngDevMode ? [{ debugName: "valueExpr" }] : /* istanbul ignore next */ []));
308
535
  searchExprInput = input(undefined, { ...(ngDevMode ? { debugName: "searchExprInput" } : /* istanbul ignore next */ {}), alias: 'searchExpr' });
309
- searchExpr = computed(() => this.searchExprInput() ?? this.nxeDefaults.tagBox?.searchExpr ?? 'lovName', /* @ts-ignore */
536
+ /**
537
+ * dx searches by `displayExpr` when `searchExpr` is not given. Defaulting to the literal
538
+ * 'lovName' instead meant that any picker with a custom displayExpr searched a field its
539
+ * items do not have, and found nothing - silently, because an empty result looks like no
540
+ * match. Where displayExpr is the default this resolves to 'lovName' exactly as before.
541
+ */
542
+ searchExpr = computed(() => this.searchExprInput() ?? this.nxeDefaults.tagBox?.searchExpr ?? this.displayExpr(), /* @ts-ignore */
310
543
  ...(ngDevMode ? [{ debugName: "searchExpr" }] : /* istanbul ignore next */ []));
311
544
  searchEnabledInput = input(undefined, { ...(ngDevMode ? { debugName: "searchEnabledInput" } : /* istanbul ignore next */ {}), alias: 'searchEnabled', transform: nxeOptionalBoolean });
312
545
  searchEnabled = computed(() => this.searchEnabledInput() ?? this.nxeDefaults.tagBox?.searchEnabled ?? true, /* @ts-ignore */
313
546
  ...(ngDevMode ? [{ debugName: "searchEnabled" }] : /* istanbul ignore next */ []));
314
547
  showClearButtonInput = input(undefined, { ...(ngDevMode ? { debugName: "showClearButtonInput" } : /* istanbul ignore next */ {}), alias: 'showClearButton', transform: nxeOptionalBoolean });
315
- showClearButton = computed(() => this.showClearButtonInput() ?? this.nxeDefaults.tagBox?.showClearButton ?? true, /* @ts-ignore */
548
+ /**
549
+ * dx defaults `showClearButton` to false (m_text_editor.base.js). The library used to default it
550
+ * on, which gave every host that never asked for one a clear button dx would not show. An app
551
+ * that wants it everywhere says so once through `NXE_DEFAULT_OPTIONS.tagBox.showClearButton`.
552
+ */
553
+ showClearButton = computed(() => this.showClearButtonInput() ?? this.nxeDefaults.tagBox?.showClearButton ?? false, /* @ts-ignore */
316
554
  ...(ngDevMode ? [{ debugName: "showClearButton" }] : /* istanbul ignore next */ []));
555
+ showSelectionControlsInput = input(undefined, { ...(ngDevMode ? { debugName: "showSelectionControlsInput" } : /* istanbul ignore next */ {}), alias: 'showSelectionControls', transform: nxeOptionalBoolean });
556
+ /**
557
+ * dx `showSelectionControls`. On (the literal, and what every consumer site passes): a checkbox
558
+ * per row and a tri-state "Select All" row — dx switches its list to `selectionMode: 'all'`
559
+ * (m_tag_box.js `_listConfig`). Off: plain rows whose fill marks the selection
560
+ * (`selectionMode: 'multiple'`).
561
+ */
562
+ showSelectionControls = computed(() => this.showSelectionControlsInput() ?? this.nxeDefaults.tagBox?.showSelectionControls ?? true, /* @ts-ignore */
563
+ ...(ngDevMode ? [{ debugName: "showSelectionControls" }] : /* istanbul ignore next */ []));
564
+ /** dx `selectAllText` — the label of the "Select All" row (dx: `dxList-selectAll`). */
565
+ selectAllText = input(undefined, /* @ts-ignore */
566
+ ...(ngDevMode ? [{ debugName: "selectAllText" }] : /* istanbul ignore next */ []));
317
567
  /** dx parity: dx-tag-box does NOT show a drop-down arrow unless showDropDownButton is set. */
318
568
  showDropDownButtonInput = input(undefined, { ...(ngDevMode ? { debugName: "showDropDownButtonInput" } : /* istanbul ignore next */ {}), alias: 'showDropDownButton', transform: nxeOptionalBoolean });
319
569
  showDropDownButton = computed(() => this.showDropDownButtonInput() ?? this.nxeDefaults.tagBox?.showDropDownButton ?? false, /* @ts-ignore */
@@ -321,6 +571,16 @@ class NxeTagBoxComponent extends NxeFormControlBase {
321
571
  placeholderInput = input(undefined, { ...(ngDevMode ? { debugName: "placeholderInput" } : /* istanbul ignore next */ {}), alias: 'placeholder' });
322
572
  placeholder = computed(() => this.placeholderInput() ?? this.nxeDefaults.tagBox?.placeholder ?? '', /* @ts-ignore */
323
573
  ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
574
+ /** dx `searchTimeout`: ms of typing pause before `(searchChange)` fires or a store is queried. */
575
+ searchTimeout = input(500, { ...(ngDevMode ? { debugName: "searchTimeout" } : /* istanbul ignore next */ {}), transform: numberAttribute });
576
+ /** dx `minSearchLength`: characters needed before a search is emitted, applied or sent. */
577
+ minSearchLength = input(0, { ...(ngDevMode ? { debugName: "minSearchLength" } : /* istanbul ignore next */ {}), transform: numberAttribute });
578
+ /** dx `showDataBeforeSearch`: list rows while the text is shorter than `minSearchLength`. */
579
+ showDataBeforeSearch = input(false, { ...(ngDevMode ? { debugName: "showDataBeforeSearch" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
580
+ /** Rows requested per page from a store `dataSource` (`take`); more load on scroll to the end. */
581
+ pageSize = input(20, { ...(ngDevMode ? { debugName: "pageSize" } : /* istanbul ignore next */ {}), transform: numberAttribute });
582
+ /** The search text settled (after `searchTimeout`, once it has `minSearchLength` characters or was cleared). */
583
+ searchChange = output();
324
584
  focused = signal(false, /* @ts-ignore */
325
585
  ...(ngDevMode ? [{ debugName: "focused" }] : /* istanbul ignore next */ []));
326
586
  open = signal(false, /* @ts-ignore */
@@ -332,8 +592,38 @@ class NxeTagBoxComponent extends NxeFormControlBase {
332
592
  overlayRef;
333
593
  closeSub;
334
594
  flipSub;
335
- items = computed(() => this.dataSource() ?? [], /* @ts-ignore */
595
+ store = computed(() => {
596
+ const ds = this.dataSource();
597
+ return nxeIsPickerStore(ds) ? ds : null;
598
+ }, /* @ts-ignore */
599
+ ...(ngDevMode ? [{ debugName: "store" }] : /* istanbul ignore next */ []));
600
+ search = new NxePickerSearch({
601
+ store: this.store,
602
+ searchExpr: this.searchExpr,
603
+ searchTimeout: this.searchTimeout,
604
+ minSearchLength: this.minSearchLength,
605
+ showDataBeforeSearch: this.showDataBeforeSearch,
606
+ pageSize: this.pageSize,
607
+ searchChange: (event) => this.searchChange.emit(event),
608
+ });
609
+ /** Rows picked or fetched by key from a store, so a chip keeps its label off the loaded page. */
610
+ resolved = signal(new Map(), /* @ts-ignore */
611
+ ...(ngDevMode ? [{ debugName: "resolved" }] : /* istanbul ignore next */ []));
612
+ items = computed(() => {
613
+ const ds = this.dataSource();
614
+ return nxeIsPickerStore(ds) ? this.search.items() : (ds ?? []);
615
+ }, /* @ts-ignore */
336
616
  ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
617
+ /** Fetches, through `byKey`, the rows of externally set keys that no loaded row has. */
618
+ keyLookup = toObservable(this.value)
619
+ .pipe(mergeMap((v) => (Array.isArray(v) && this.search.hasStore ? v : [])), filter((key) => this.findRow(key) === undefined), mergeMap((key) => from(this.search.lookup(key)).pipe(catchError(() => EMPTY), filter((row) => row != null), map((row) => [key, row]))), takeUntilDestroyed())
620
+ .subscribe(([key, row]) => this.remember(key, row));
621
+ findRow(key) {
622
+ return this.items().find((it) => this.extractValue(it) === key) ?? this.resolved().get(key);
623
+ }
624
+ remember(key, row) {
625
+ this.resolved.update((m) => new Map(m).set(key, row));
626
+ }
337
627
  /** The selected keys (always an array). */
338
628
  selectedKeys = computed(() => {
339
629
  const v = this.value();
@@ -341,20 +631,15 @@ class NxeTagBoxComponent extends NxeFormControlBase {
341
631
  }, /* @ts-ignore */
342
632
  ...(ngDevMode ? [{ debugName: "selectedKeys" }] : /* istanbul ignore next */ []));
343
633
  display(item) {
344
- const expr = this.displayExpr();
345
- if (!expr) {
346
- return item == null ? '' : String(item);
347
- }
348
- return item?.[expr] ?? '';
634
+ return nxeResolveText(this.displayExpr(), item);
349
635
  }
350
636
  extractValue(item) {
351
- const expr = this.valueExpr();
352
- return expr ? item?.[expr] : item;
637
+ return nxeResolveExpr(this.valueExpr(), item);
353
638
  }
354
639
  /** Selected items resolved from the value keys (for chip rendering). */
355
640
  selectedItems = computed(() => {
356
641
  const keys = this.selectedKeys();
357
- return keys.map((k) => this.items().find((it) => this.extractValue(it) === k) ?? k);
642
+ return keys.map((k) => this.findRow(k) ?? k);
358
643
  }, /* @ts-ignore */
359
644
  ...(ngDevMode ? [{ debugName: "selectedItems" }] : /* istanbul ignore next */ []));
360
645
  hasValue = computed(() => this.selectedKeys().length > 0, /* @ts-ignore */
@@ -362,20 +647,38 @@ class NxeTagBoxComponent extends NxeFormControlBase {
362
647
  filteredItems = computed(() => {
363
648
  const items = this.items();
364
649
  const search = this.searchText();
365
- if (!this.searchEnabled() || !search) {
650
+ if (this.search.hasStore) {
366
651
  return items;
367
652
  }
368
- const expr = this.searchExpr() || this.displayExpr();
653
+ if (!this.searchEnabled() || !search) {
654
+ return this.minSearchLength() > 0 && !this.showDataBeforeSearch() ? [] : items;
655
+ }
656
+ if (!this.search.reachesMinimum(search)) {
657
+ return this.showDataBeforeSearch() ? items : [];
658
+ }
659
+ const expr = this.searchExpr();
369
660
  const needle = search.toLowerCase();
370
- return items.filter((it) => {
371
- const hay = (expr ? it?.[expr] : it) ?? '';
372
- return String(hay).toLowerCase().includes(needle);
373
- });
661
+ return items.filter((it) => nxeResolveText(expr, it).toLowerCase().includes(needle));
374
662
  }, /* @ts-ignore */
375
663
  ...(ngDevMode ? [{ debugName: "filteredItems" }] : /* istanbul ignore next */ []));
376
664
  isSelected(item) {
377
665
  return this.selectedKeys().includes(this.extractValue(item));
378
666
  }
667
+ /**
668
+ * The "Select All" checkbox value over the rows currently listed — dx `selectAllMode: 'page'`,
669
+ * so a search narrows what it counts: `true` when every listed row is selected, `false` when
670
+ * none is, and `undefined` (indeterminate) in between.
671
+ */
672
+ selectAllState = computed(() => {
673
+ const items = this.filteredItems();
674
+ const keys = this.selectedKeys();
675
+ const selected = items.filter((it) => keys.includes(this.extractValue(it))).length;
676
+ if (selected === 0) {
677
+ return false;
678
+ }
679
+ return selected === items.length ? true : undefined;
680
+ }, /* @ts-ignore */
681
+ ...(ngDevMode ? [{ debugName: "selectAllState" }] : /* istanbul ignore next */ []));
379
682
  onFieldMousedown(event) {
380
683
  if (this.readOnly() || this.disabled()) {
381
684
  return;
@@ -390,11 +693,20 @@ class NxeTagBoxComponent extends NxeFormControlBase {
390
693
  this.inputEl()?.nativeElement?.focus();
391
694
  }
392
695
  onSearchInput(event) {
393
- this.searchText.set(event.target.value);
696
+ const text = event.target.value;
697
+ this.searchText.set(text);
698
+ this.search.typed(text);
394
699
  if (!this.open()) {
395
700
  this.openPanel();
396
701
  }
397
702
  }
703
+ /** Loads the next page of a store `dataSource` when the list is scrolled to its end. */
704
+ onPanelScroll(event) {
705
+ const el = event.target;
706
+ if (el.scrollTop + el.clientHeight >= el.scrollHeight - 8) {
707
+ this.search.loadMore();
708
+ }
709
+ }
398
710
  onBlur() {
399
711
  this.focused.set(false);
400
712
  this.markAsTouched();
@@ -404,14 +716,42 @@ class NxeTagBoxComponent extends NxeFormControlBase {
404
716
  const key = this.extractValue(item);
405
717
  const keys = this.selectedKeys();
406
718
  const next = keys.includes(key) ? keys.filter((k) => k !== key) : [...keys, key];
719
+ if (this.search.hasStore) {
720
+ this.remember(key, item);
721
+ }
407
722
  this.commitValue(next, event);
723
+ if (this.searchText()) {
724
+ this.search.typed('');
725
+ }
408
726
  this.searchText.set('');
409
727
  const input = this.inputEl()?.nativeElement;
410
728
  if (input) {
411
729
  input.value = '';
412
730
  }
413
731
  }
732
+ /**
733
+ * dx `_selectAllHandler`: anything short of all-selected selects every listed row (keeping
734
+ * selections the search hides); all-selected deselects the listed rows only.
735
+ */
736
+ toggleAll(event) {
737
+ event.preventDefault();
738
+ const listed = this.filteredItems().map((it) => this.extractValue(it));
739
+ if (this.search.hasStore) {
740
+ for (const it of this.filteredItems()) {
741
+ this.remember(this.extractValue(it), it);
742
+ }
743
+ }
744
+ const keys = this.selectedKeys();
745
+ const next = this.selectAllState() === true
746
+ ? keys.filter((k) => !listed.includes(k))
747
+ : [...keys, ...listed.filter((k) => !keys.includes(k))];
748
+ this.commitValue(next, event);
749
+ }
414
750
  removeTag(item, event) {
751
+ // The × stays rendered in readOnly/disabled, as dx's does; its action is what is withheld.
752
+ if (this.readOnly() || this.disabled()) {
753
+ return;
754
+ }
415
755
  event.preventDefault();
416
756
  event.stopPropagation();
417
757
  const key = this.extractValue(item);
@@ -449,6 +789,7 @@ class NxeTagBoxComponent extends NxeFormControlBase {
449
789
  if (this.overlayRef.hasAttached()) {
450
790
  return;
451
791
  }
792
+ this.search.open(this.searchText());
452
793
  this.overlayRef.attach(new TemplatePortal(this.dropdownTemplate(), this.viewContainerRef));
453
794
  this.closeSub = merge(this.overlayRef.backdropClick(), this.overlayRef.keydownEvents().pipe(filter((e) => e.key === 'Escape'))).subscribe(() => this.close());
454
795
  this.open.set(true);
@@ -469,24 +810,24 @@ class NxeTagBoxComponent extends NxeFormControlBase {
469
810
  this.overlayRef?.dispose();
470
811
  }
471
812
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeTagBoxComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
472
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeTagBoxComponent, isStandalone: true, selector: "nxe-tag-box", inputs: { dataSource: { classPropertyName: "dataSource", publicName: "dataSource", isSignal: true, isRequired: false, transformFunction: null }, displayExprInput: { classPropertyName: "displayExprInput", publicName: "displayExpr", isSignal: true, isRequired: false, transformFunction: null }, valueExprInput: { classPropertyName: "valueExprInput", publicName: "valueExpr", isSignal: true, isRequired: false, transformFunction: null }, searchExprInput: { classPropertyName: "searchExprInput", publicName: "searchExpr", isSignal: true, isRequired: false, transformFunction: null }, searchEnabledInput: { classPropertyName: "searchEnabledInput", publicName: "searchEnabled", isSignal: true, isRequired: false, transformFunction: null }, showClearButtonInput: { classPropertyName: "showClearButtonInput", publicName: "showClearButton", isSignal: true, isRequired: false, transformFunction: null }, showDropDownButtonInput: { classPropertyName: "showDropDownButtonInput", publicName: "showDropDownButton", isSignal: true, isRequired: false, transformFunction: null }, placeholderInput: { classPropertyName: "placeholderInput", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "keydown": "onKeydown($event)" }, properties: { "class.nxe-focused": "focused()", "class.nxe-invalid": "!!validationError()", "class.nxe-open": "open()" }, classAttribute: "nxe-widget nxe-editor nxe-tagbox" }, providers: [
813
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeTagBoxComponent, isStandalone: true, selector: "nxe-tag-box", inputs: { dataSource: { classPropertyName: "dataSource", publicName: "dataSource", isSignal: true, isRequired: false, transformFunction: null }, displayExprInput: { classPropertyName: "displayExprInput", publicName: "displayExpr", isSignal: true, isRequired: false, transformFunction: null }, valueExprInput: { classPropertyName: "valueExprInput", publicName: "valueExpr", isSignal: true, isRequired: false, transformFunction: null }, searchExprInput: { classPropertyName: "searchExprInput", publicName: "searchExpr", isSignal: true, isRequired: false, transformFunction: null }, searchEnabledInput: { classPropertyName: "searchEnabledInput", publicName: "searchEnabled", isSignal: true, isRequired: false, transformFunction: null }, showClearButtonInput: { classPropertyName: "showClearButtonInput", publicName: "showClearButton", isSignal: true, isRequired: false, transformFunction: null }, showSelectionControlsInput: { classPropertyName: "showSelectionControlsInput", publicName: "showSelectionControls", isSignal: true, isRequired: false, transformFunction: null }, selectAllText: { classPropertyName: "selectAllText", publicName: "selectAllText", isSignal: true, isRequired: false, transformFunction: null }, showDropDownButtonInput: { classPropertyName: "showDropDownButtonInput", publicName: "showDropDownButton", isSignal: true, isRequired: false, transformFunction: null }, placeholderInput: { classPropertyName: "placeholderInput", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, searchTimeout: { classPropertyName: "searchTimeout", publicName: "searchTimeout", isSignal: true, isRequired: false, transformFunction: null }, minSearchLength: { classPropertyName: "minSearchLength", publicName: "minSearchLength", isSignal: true, isRequired: false, transformFunction: null }, showDataBeforeSearch: { classPropertyName: "showDataBeforeSearch", publicName: "showDataBeforeSearch", isSignal: true, isRequired: false, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { searchChange: "searchChange" }, host: { listeners: { "keydown": "onKeydown($event)" }, properties: { "class.nxe-focused": "focused()", "class.nxe-invalid": "invalidState()", "class.nxe-open": "open()" }, classAttribute: "nxe-widget nxe-editor nxe-tagbox" }, providers: [
473
814
  { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeTagBoxComponent), multi: true },
474
815
  { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeTagBoxComponent) },
475
- ], viewQueries: [{ propertyName: "dropdownTemplate", first: true, predicate: ["dropdown"], descendants: true, isSignal: true }, { propertyName: "inputEl", first: true, predicate: ["input"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div class=\"nxe-editor-container nxe-tag-container\" (mousedown)=\"onFieldMousedown($event)\">\r\n @for (item of selectedItems(); track $index) {\r\n <span class=\"nxe-tag\">\r\n <span class=\"nxe-tag-text\">{{ display(item) }}</span>\r\n @if (!readOnly() && !disabled()) {\r\n <span class=\"nxe-tag-remove\" (mousedown)=\"removeTag(item, $event)\" aria-hidden=\"true\"></span>\r\n }\r\n </span>\r\n }\r\n\r\n <input\r\n #input\r\n class=\"nxe-editor-input nxe-tag-input\"\r\n type=\"text\"\r\n [readOnly]=\"readOnly() || !searchEnabled()\"\r\n [disabled]=\"disabled()\"\r\n [attr.placeholder]=\"selectedItems().length ? null : (placeholder() || null)\"\r\n autocomplete=\"off\"\r\n (input)=\"onSearchInput($event)\"\r\n (focus)=\"focused.set(true)\"\r\n (blur)=\"onBlur()\" />\r\n\r\n @if (showClearButton() && hasValue() && !readOnly() && !disabled()) {\r\n <span class=\"nxe-select-clear\" (mousedown)=\"onClear($event)\" aria-hidden=\"true\">\r\n <svg viewBox=\"0 0 24 24\"><path d=\"M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z\" /></svg>\r\n </span>\r\n }\r\n @if (showDropDownButton() && !readOnly()) {\r\n <span class=\"nxe-select-arrow\" aria-hidden=\"true\">\r\n <svg viewBox=\"0 0 24 24\"><path d=\"M7 10l5 5 5-5z\" /></svg>\r\n </span>\r\n }\r\n</div>\r\n\r\n<ng-content />\r\n\r\n@if (validationError(); as error) {\r\n @if (focused()) {\r\n <div class=\"nxe-invalid-message\">{{ error }}</div>\r\n }\r\n}\r\n\r\n<ng-template #dropdown>\r\n <div class=\"nxe-select-panel\" [style.width.px]=\"panelWidth()\">\r\n @for (item of filteredItems(); track $index) {\r\n <div\r\n class=\"nxe-list-item nxe-tag-item\"\r\n [class.nxe-list-item-selected]=\"isSelected(item)\"\r\n (mousedown)=\"toggle(item, $event)\">\r\n <span class=\"nxe-tag-check\" [class.nxe-tag-checked]=\"isSelected(item)\" aria-hidden=\"true\"></span>\r\n <span class=\"nxe-tag-item-text\">{{ display(item) }}</span>\r\n </div>\r\n } @empty {\r\n <div class=\"nxe-list-empty\">No data to display</div>\r\n }\r\n </div>\r\n</ng-template>\r\n", styles: ["nxe-tag-box{cursor:pointer}nxe-tag-box .nxe-tag-container{flex-wrap:wrap;align-items:center;gap:4px;padding:3px 4px;min-height:36px}nxe-tag-box .nxe-tag-input{flex:1 1 60px;min-width:60px;width:auto;min-height:26px;padding:0 4px}nxe-tag-box .nxe-tag{position:relative;display:inline-flex;align-items:center;max-width:calc(100% - 1px);min-width:40px;padding:3px 25px 4px 6px;background-color:#dcdcdc;border-radius:2px;color:#333;line-height:1.2}nxe-tag-box .nxe-tag-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}nxe-tag-box .nxe-tag-remove{position:absolute;inset-inline-end:0;top:0;width:25px;height:100%;cursor:pointer}nxe-tag-box .nxe-tag-remove:before,nxe-tag-box .nxe-tag-remove:after{position:absolute;top:50%;content:\"\";transform:rotate(45deg);inset-inline-end:9px;margin-top:-5px;width:3px;height:11px;background:#a9a9aa}nxe-tag-box .nxe-tag-remove:after{inset-inline-end:5px;margin-top:-1px;width:11px;height:3px}nxe-tag-box .nxe-select-clear,nxe-tag-box .nxe-select-arrow{flex:0 0 auto;align-self:center;display:inline-flex;align-items:center;justify-content:center;color:#333}nxe-tag-box .nxe-select-clear svg,nxe-tag-box .nxe-select-arrow svg{width:16px;height:16px;fill:currentColor}nxe-tag-box .nxe-select-clear{width:22px;color:#999}nxe-tag-box .nxe-select-clear:hover{color:#333}nxe-tag-box .nxe-select-arrow{width:28px}nxe-tag-box .nxe-select-arrow svg{width:18px;height:18px}nxe-tag-box .nxe-editor-container:has(>.nxe-select-arrow){--nxe-invalid-badge-inset: 32px}nxe-tag-box .nxe-editor-container:has(>.nxe-select-clear){--nxe-invalid-badge-inset: 26px}nxe-tag-box .nxe-editor-container:has(>.nxe-select-arrow):has(>.nxe-select-clear){--nxe-invalid-badge-inset: 54px}.nxe-tag-item{display:flex;align-items:center;gap:8px}.nxe-tag-check{box-sizing:border-box;position:relative;flex-shrink:0;width:18px;height:18px;border:1px solid #ddd;border-radius:2px;background:#fff}.nxe-tag-check.nxe-tag-checked{border-color:#337ab7}.nxe-tag-check.nxe-tag-checked:after{content:\"\";position:absolute;top:1px;inset-inline-start:5px;width:5px;height:9px;border:solid #337ab7;border-width:0 2px 2px 0;transform:rotate(45deg)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
816
+ ], viewQueries: [{ propertyName: "dropdownTemplate", first: true, predicate: ["dropdown"], descendants: true, isSignal: true }, { propertyName: "inputEl", first: true, predicate: ["input"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!--\n Two columns, as dx-tag-box renders them: the wrapping tag container (dx turns\n `.dx-texteditor-input-container` into `.dx-tag-container`) and, beside it, the buttons \u2014 never\n inside it, so they cannot wrap onto a tag row and they centre on the whole editor height.\n-->\n<div class=\"nxe-editor-container\" (mousedown)=\"onFieldMousedown($event)\">\n <div class=\"nxe-tag-container\">\n @for (item of selectedItems(); track $index) {\n <span class=\"nxe-tag\">\n <span class=\"nxe-tag-text\">{{ display(item) }}</span>\n <!-- Rendered in every state, as dx does (m_tag_box.js appends it unconditionally);\n readOnly/disabled make it inert rather than removing it. -->\n <span class=\"nxe-tag-remove\" (mousedown)=\"removeTag(item, $event)\" aria-hidden=\"true\"></span>\n </span>\n }\n\n <input\n #input\n class=\"nxe-editor-input nxe-tag-input\"\n [attr.tabindex]=\"tabIndex()\"\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"open() ? panelId() : null\"\n\n [id]=\"controlId()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-labelledby]=\"ariaLabelledBy() || null\"\n [attr.aria-invalid]=\"invalidState() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n type=\"text\"\n [readOnly]=\"readOnly() || !searchEnabled()\"\n [disabled]=\"disabled()\"\n [attr.placeholder]=\"selectedItems().length ? null : (placeholder() || null)\"\n autocomplete=\"off\"\n (input)=\"onSearchInput($event)\"\n (focus)=\"focused.set(true)\"\n (blur)=\"onBlur()\" />\n </div>\n\n @if (showClearButton() && hasValue() && !readOnly() && !disabled()) {\n <span class=\"nxe-select-clear\" (mousedown)=\"onClear($event)\" aria-hidden=\"true\">\n <!-- A \u00D816 disc with a 2px \u00D7 cut out: two 2px bars, each running corner to corner of the\n 8px square centred on the disc, drawn as a plus and turned 45\u00B0. -->\n <svg viewBox=\"0 0 18 18\" focusable=\"false\">\n <path fill-rule=\"evenodd\" transform=\"rotate(45 9 9)\"\n d=\"M9 1a8 8 0 1 1 0 16A8 8 0 1 1 9 1ZM8 3.343h2V8h4.657v2H10v4.657H8V10H3.343V8H8Z\" />\n </svg>\n </span>\n }\n @if (showDropDownButton() && !readOnly()) {\n <span class=\"nxe-select-arrow\" aria-hidden=\"true\">\n <!-- An 8x4 triangle centred in the 18px box. -->\n <svg viewBox=\"0 0 18 18\" focusable=\"false\"><path d=\"M5 7h8l-4 4z\" /></svg>\n </span>\n }\n</div>\n\n<ng-content />\n\n@if (validationError(); as error) {\n @if (focused()) {\n <div class=\"nxe-invalid-message\">{{ error }}</div>\n }\n}\n\n<ng-template #dropdown>\n <div class=\"nxe-select-panel\" [style.width.px]=\"panelWidth()\"\n [id]=\"panelId()\"\n role=\"listbox\"\n aria-multiselectable=\"true\"\n (scroll)=\"onPanelScroll($event)\">\n @if (showSelectionControls()) {\n <!-- dx `selectionMode: 'all'`, which showSelectionControls switches on\n (m_tag_box.js _listConfig): a tri-state \"Select All\" over the rows shown. Hidden\n while the list is empty, as `.dx-empty-collection .dx-list-select-all` is. -->\n @if (filteredItems().length) {\n <div\n class=\"nxe-tag-select-all\"\n [id]=\"panelId() + '-select-all'\"\n role=\"option\"\n [attr.aria-selected]=\"selectAllState() === true\"\n [attr.aria-checked]=\"selectAllState() === undefined ? 'mixed' : selectAllState()\"\n (mousedown)=\"toggleAll($event)\">\n <span\n class=\"nxe-tag-check\"\n [class.nxe-tag-checked]=\"selectAllState() === true\"\n [class.nxe-tag-indeterminate]=\"selectAllState() === undefined\"\n aria-hidden=\"true\"></span>\n <span class=\"nxe-tag-select-all-label\">{{ selectAllText() ?? nxeMessages.t('dropdown.selectAll') }}</span>\n </div>\n }\n @for (item of filteredItems(); track $index; let i = $index) {\n <div\n class=\"nxe-list-item nxe-tag-item\"\n [id]=\"optionId(i)\"\n role=\"option\"\n [attr.aria-selected]=\"isSelected(item)\"\n [class.nxe-list-item-selected]=\"isSelected(item)\"\n (mousedown)=\"toggle(item, $event)\">\n <span class=\"nxe-tag-check\" [class.nxe-tag-checked]=\"isSelected(item)\" aria-hidden=\"true\"></span>\n <span class=\"nxe-tag-item-text\">{{ display(item) }}</span>\n </div>\n } @empty {\n <div class=\"nxe-list-empty\">{{ nxeMessages.t('dropdown.noData') }}</div>\n }\n } @else {\n <!-- dx `selectionMode: 'multiple'`: plain rows, selection shown by the row fill. -->\n @for (item of filteredItems(); track $index; let i = $index) {\n <div\n class=\"nxe-list-item\"\n [id]=\"optionId(i)\"\n role=\"option\"\n [attr.aria-selected]=\"isSelected(item)\"\n [class.nxe-list-item-selected]=\"isSelected(item)\"\n (mousedown)=\"toggle(item, $event)\">\n {{ display(item) }}\n </div>\n } @empty {\n <div class=\"nxe-list-empty\">{{ nxeMessages.t('dropdown.noData') }}</div>\n }\n }\n </div>\n</ng-template>\n", styles: ["nxe-tag-box{cursor:pointer}nxe-tag-box .nxe-tag-container{box-sizing:border-box;position:relative;display:flex;flex:1 1 auto;flex-wrap:wrap;align-items:stretch;align-self:stretch;min-width:0;padding-bottom:4px}nxe-tag-box .nxe-tag-input{flex:1 1 60px;min-width:60px;width:auto;height:auto;min-height:30px;padding-bottom:4px}nxe-tag-box .nxe-tag-container:has(>.nxe-tag)>.nxe-tag-input{padding-inline-start:0;margin-inline-start:5px}nxe-tag-box.nxe-invalid>.nxe-editor-container:not(:has(>.nxe-select-clear,>.nxe-select-arrow))>.nxe-tag-container:not(:has(>.nxe-tag))>.nxe-tag-input{padding-inline-end:34px}nxe-tag-box .nxe-tag{box-sizing:border-box;position:relative;display:inline-flex;align-self:flex-start;justify-content:center;max-width:calc(100% - 5px);min-width:40px;margin-top:4px;margin-inline-start:4px;padding-block:3px 4px;padding-inline:6px 25px;background-color:var(--nxe-ui-border, #ddd);border-radius:2px;color:var(--nxe-ui-text, #333)}nxe-tag-box .nxe-tag-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}nxe-tag-box .nxe-tag-remove{position:absolute;inset-inline-end:0;top:0;width:25px;height:100%;cursor:pointer}nxe-tag-box .nxe-tag-remove:before,nxe-tag-box .nxe-tag-remove:after{position:absolute;top:50%;content:\"\";transform:rotate(45deg);inset-inline-end:9px;margin-top:-5px;width:3px;height:11px;background:var(--nxe-ui-neutral-400, #aaa)}nxe-tag-box .nxe-tag-remove:after{inset-inline-end:5px;margin-top:-1px;width:11px;height:3px}nxe-tag-box:not(.nxe-disabled) .nxe-tag-remove:active:before,nxe-tag-box:not(.nxe-disabled) .nxe-tag-remove:active:after{background:var(--nxe-ui-border, #ddd)}nxe-tag-box.nxe-disabled .nxe-tag,nxe-tag-box.nxe-disabled .nxe-tag-remove{cursor:default}nxe-tag-box .nxe-select-clear{box-sizing:border-box;flex:0 0 34px;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;width:34px;min-width:34px;padding:0;color:var(--nxe-ui-text-muted, #999);cursor:pointer}nxe-tag-box .nxe-select-clear svg{display:block;width:18px;height:18px;fill:currentColor}nxe-tag-box .nxe-select-arrow{box-sizing:border-box;position:relative;flex:0 0 34px;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;width:34px;min-width:34px;padding:1px;border-radius:4px;background-clip:content-box;color:var(--nxe-ui-text, #333);cursor:pointer}nxe-tag-box .nxe-select-arrow svg{display:block;width:18px;height:18px;fill:currentColor}nxe-tag-box:hover:not(.nxe-disabled) .nxe-select-arrow,nxe-tag-box:active:not(.nxe-disabled) .nxe-select-arrow{background-color:var(--nxe-ui-neutral-150, #e6e6e6)}nxe-tag-box.nxe-open .nxe-select-arrow{background-color:var(--nxe-ui-neutral-350, #b3b3b3)}nxe-tag-box .nxe-editor-container:has(>.nxe-select-arrow) .nxe-editor-input{padding-inline-end:0}nxe-tag-box .nxe-editor-container:has(>.nxe-select-clear),nxe-tag-box .nxe-editor-container:has(>.nxe-select-arrow){--nxe-invalid-badge-inset: 38px}nxe-tag-box .nxe-editor-container:has(>.nxe-select-clear):has(>.nxe-select-arrow){--nxe-invalid-badge-inset: 72px}.nxe-select-panel .nxe-list-item.nxe-tag-item{display:block;position:relative;padding-block:10px;padding-inline:41px 10px}.nxe-select-panel .nxe-list-item.nxe-tag-item:hover,.nxe-select-panel .nxe-list-item.nxe-tag-item:active,.nxe-select-panel .nxe-list-item.nxe-tag-item.nxe-list-item-selected,.nxe-select-panel .nxe-list-item.nxe-tag-item.nxe-list-item-selected:hover:not(.nxe-list-item-active){background-color:transparent;color:var(--nxe-ui-text)}.nxe-tag-check{box-sizing:border-box;position:relative;display:block;width:22px;height:22px;border:1px solid var(--nxe-ui-border);border-radius:2px;background:var(--nxe-ui-surface)}.nxe-tag-check.nxe-tag-checked:after{content:\"\";position:absolute;box-sizing:border-box;top:5px;inset-inline-start:2.5px;width:15px;height:11px;background-color:var(--nxe-ui-accent);clip-path:polygon(0 5.586px,5.414px 11px,15px 1.414px,13.586px 0,5.414px 8.172px,1.414px 4.172px)}@media(forced-colors:active){.nxe-tag-check.nxe-tag-checked:after{forced-color-adjust:none;background-color:CanvasText}}.nxe-tag-check.nxe-tag-indeterminate:after{content:\"\";position:absolute;box-sizing:border-box;top:50%;inset-inline-start:50%;width:12px;height:12px;margin-top:-6px;margin-inline-start:-6px;border-radius:2px;background-color:var(--nxe-ui-accent)}@media(forced-colors:active){.nxe-tag-check.nxe-tag-indeterminate:after{forced-color-adjust:none;background-color:CanvasText}}.nxe-tag-item .nxe-tag-check{position:absolute;top:50%;inset-inline-start:10px;margin-top:-10px}.nxe-tag-item:hover .nxe-tag-check,.nxe-tag-select-all:hover .nxe-tag-check{border-color:var(--nxe-ui-checkbox-hover-border, #265a87)}.nxe-tag-select-all{box-sizing:border-box;padding-block:12px 11px;padding-inline:0;border-bottom:1px solid var(--nxe-ui-border);margin-bottom:3px;white-space:nowrap;cursor:pointer}.nxe-tag-select-all .nxe-tag-check{float:inline-start;margin-top:-1px;margin-bottom:-3px;margin-inline-start:10px}.nxe-tag-select-all-label{display:inline-block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:17px;padding:0 10px;margin-top:1px}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
476
817
  }
477
818
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeTagBoxComponent, decorators: [{
478
819
  type: Component,
479
820
  args: [{ selector: 'nxe-tag-box', standalone: true, host: {
480
821
  class: 'nxe-widget nxe-editor nxe-tagbox',
481
822
  '[class.nxe-focused]': 'focused()',
482
- '[class.nxe-invalid]': '!!validationError()',
823
+ '[class.nxe-invalid]': 'invalidState()',
483
824
  '[class.nxe-open]': 'open()',
484
825
  '(keydown)': 'onKeydown($event)',
485
826
  }, providers: [
486
827
  { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeTagBoxComponent), multi: true },
487
828
  { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeTagBoxComponent) },
488
- ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<div class=\"nxe-editor-container nxe-tag-container\" (mousedown)=\"onFieldMousedown($event)\">\r\n @for (item of selectedItems(); track $index) {\r\n <span class=\"nxe-tag\">\r\n <span class=\"nxe-tag-text\">{{ display(item) }}</span>\r\n @if (!readOnly() && !disabled()) {\r\n <span class=\"nxe-tag-remove\" (mousedown)=\"removeTag(item, $event)\" aria-hidden=\"true\"></span>\r\n }\r\n </span>\r\n }\r\n\r\n <input\r\n #input\r\n class=\"nxe-editor-input nxe-tag-input\"\r\n type=\"text\"\r\n [readOnly]=\"readOnly() || !searchEnabled()\"\r\n [disabled]=\"disabled()\"\r\n [attr.placeholder]=\"selectedItems().length ? null : (placeholder() || null)\"\r\n autocomplete=\"off\"\r\n (input)=\"onSearchInput($event)\"\r\n (focus)=\"focused.set(true)\"\r\n (blur)=\"onBlur()\" />\r\n\r\n @if (showClearButton() && hasValue() && !readOnly() && !disabled()) {\r\n <span class=\"nxe-select-clear\" (mousedown)=\"onClear($event)\" aria-hidden=\"true\">\r\n <svg viewBox=\"0 0 24 24\"><path d=\"M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z\" /></svg>\r\n </span>\r\n }\r\n @if (showDropDownButton() && !readOnly()) {\r\n <span class=\"nxe-select-arrow\" aria-hidden=\"true\">\r\n <svg viewBox=\"0 0 24 24\"><path d=\"M7 10l5 5 5-5z\" /></svg>\r\n </span>\r\n }\r\n</div>\r\n\r\n<ng-content />\r\n\r\n@if (validationError(); as error) {\r\n @if (focused()) {\r\n <div class=\"nxe-invalid-message\">{{ error }}</div>\r\n }\r\n}\r\n\r\n<ng-template #dropdown>\r\n <div class=\"nxe-select-panel\" [style.width.px]=\"panelWidth()\">\r\n @for (item of filteredItems(); track $index) {\r\n <div\r\n class=\"nxe-list-item nxe-tag-item\"\r\n [class.nxe-list-item-selected]=\"isSelected(item)\"\r\n (mousedown)=\"toggle(item, $event)\">\r\n <span class=\"nxe-tag-check\" [class.nxe-tag-checked]=\"isSelected(item)\" aria-hidden=\"true\"></span>\r\n <span class=\"nxe-tag-item-text\">{{ display(item) }}</span>\r\n </div>\r\n } @empty {\r\n <div class=\"nxe-list-empty\">No data to display</div>\r\n }\r\n </div>\r\n</ng-template>\r\n", styles: ["nxe-tag-box{cursor:pointer}nxe-tag-box .nxe-tag-container{flex-wrap:wrap;align-items:center;gap:4px;padding:3px 4px;min-height:36px}nxe-tag-box .nxe-tag-input{flex:1 1 60px;min-width:60px;width:auto;min-height:26px;padding:0 4px}nxe-tag-box .nxe-tag{position:relative;display:inline-flex;align-items:center;max-width:calc(100% - 1px);min-width:40px;padding:3px 25px 4px 6px;background-color:#dcdcdc;border-radius:2px;color:#333;line-height:1.2}nxe-tag-box .nxe-tag-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}nxe-tag-box .nxe-tag-remove{position:absolute;inset-inline-end:0;top:0;width:25px;height:100%;cursor:pointer}nxe-tag-box .nxe-tag-remove:before,nxe-tag-box .nxe-tag-remove:after{position:absolute;top:50%;content:\"\";transform:rotate(45deg);inset-inline-end:9px;margin-top:-5px;width:3px;height:11px;background:#a9a9aa}nxe-tag-box .nxe-tag-remove:after{inset-inline-end:5px;margin-top:-1px;width:11px;height:3px}nxe-tag-box .nxe-select-clear,nxe-tag-box .nxe-select-arrow{flex:0 0 auto;align-self:center;display:inline-flex;align-items:center;justify-content:center;color:#333}nxe-tag-box .nxe-select-clear svg,nxe-tag-box .nxe-select-arrow svg{width:16px;height:16px;fill:currentColor}nxe-tag-box .nxe-select-clear{width:22px;color:#999}nxe-tag-box .nxe-select-clear:hover{color:#333}nxe-tag-box .nxe-select-arrow{width:28px}nxe-tag-box .nxe-select-arrow svg{width:18px;height:18px}nxe-tag-box .nxe-editor-container:has(>.nxe-select-arrow){--nxe-invalid-badge-inset: 32px}nxe-tag-box .nxe-editor-container:has(>.nxe-select-clear){--nxe-invalid-badge-inset: 26px}nxe-tag-box .nxe-editor-container:has(>.nxe-select-arrow):has(>.nxe-select-clear){--nxe-invalid-badge-inset: 54px}.nxe-tag-item{display:flex;align-items:center;gap:8px}.nxe-tag-check{box-sizing:border-box;position:relative;flex-shrink:0;width:18px;height:18px;border:1px solid #ddd;border-radius:2px;background:#fff}.nxe-tag-check.nxe-tag-checked{border-color:#337ab7}.nxe-tag-check.nxe-tag-checked:after{content:\"\";position:absolute;top:1px;inset-inline-start:5px;width:5px;height:9px;border:solid #337ab7;border-width:0 2px 2px 0;transform:rotate(45deg)}\n"] }]
489
- }], propDecorators: { dropdownTemplate: [{ type: i0.ViewChild, args: ['dropdown', { isSignal: true }] }], inputEl: [{ type: i0.ViewChild, args: ['input', { isSignal: true }] }], dataSource: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataSource", required: false }] }], displayExprInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayExpr", required: false }] }], valueExprInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueExpr", required: false }] }], searchExprInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchExpr", required: false }] }], searchEnabledInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchEnabled", required: false }] }], showClearButtonInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClearButton", required: false }] }], showDropDownButtonInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "showDropDownButton", required: false }] }], placeholderInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }] } });
829
+ ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<!--\n Two columns, as dx-tag-box renders them: the wrapping tag container (dx turns\n `.dx-texteditor-input-container` into `.dx-tag-container`) and, beside it, the buttons \u2014 never\n inside it, so they cannot wrap onto a tag row and they centre on the whole editor height.\n-->\n<div class=\"nxe-editor-container\" (mousedown)=\"onFieldMousedown($event)\">\n <div class=\"nxe-tag-container\">\n @for (item of selectedItems(); track $index) {\n <span class=\"nxe-tag\">\n <span class=\"nxe-tag-text\">{{ display(item) }}</span>\n <!-- Rendered in every state, as dx does (m_tag_box.js appends it unconditionally);\n readOnly/disabled make it inert rather than removing it. -->\n <span class=\"nxe-tag-remove\" (mousedown)=\"removeTag(item, $event)\" aria-hidden=\"true\"></span>\n </span>\n }\n\n <input\n #input\n class=\"nxe-editor-input nxe-tag-input\"\n [attr.tabindex]=\"tabIndex()\"\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"open() ? panelId() : null\"\n\n [id]=\"controlId()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-labelledby]=\"ariaLabelledBy() || null\"\n [attr.aria-invalid]=\"invalidState() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n type=\"text\"\n [readOnly]=\"readOnly() || !searchEnabled()\"\n [disabled]=\"disabled()\"\n [attr.placeholder]=\"selectedItems().length ? null : (placeholder() || null)\"\n autocomplete=\"off\"\n (input)=\"onSearchInput($event)\"\n (focus)=\"focused.set(true)\"\n (blur)=\"onBlur()\" />\n </div>\n\n @if (showClearButton() && hasValue() && !readOnly() && !disabled()) {\n <span class=\"nxe-select-clear\" (mousedown)=\"onClear($event)\" aria-hidden=\"true\">\n <!-- A \u00D816 disc with a 2px \u00D7 cut out: two 2px bars, each running corner to corner of the\n 8px square centred on the disc, drawn as a plus and turned 45\u00B0. -->\n <svg viewBox=\"0 0 18 18\" focusable=\"false\">\n <path fill-rule=\"evenodd\" transform=\"rotate(45 9 9)\"\n d=\"M9 1a8 8 0 1 1 0 16A8 8 0 1 1 9 1ZM8 3.343h2V8h4.657v2H10v4.657H8V10H3.343V8H8Z\" />\n </svg>\n </span>\n }\n @if (showDropDownButton() && !readOnly()) {\n <span class=\"nxe-select-arrow\" aria-hidden=\"true\">\n <!-- An 8x4 triangle centred in the 18px box. -->\n <svg viewBox=\"0 0 18 18\" focusable=\"false\"><path d=\"M5 7h8l-4 4z\" /></svg>\n </span>\n }\n</div>\n\n<ng-content />\n\n@if (validationError(); as error) {\n @if (focused()) {\n <div class=\"nxe-invalid-message\">{{ error }}</div>\n }\n}\n\n<ng-template #dropdown>\n <div class=\"nxe-select-panel\" [style.width.px]=\"panelWidth()\"\n [id]=\"panelId()\"\n role=\"listbox\"\n aria-multiselectable=\"true\"\n (scroll)=\"onPanelScroll($event)\">\n @if (showSelectionControls()) {\n <!-- dx `selectionMode: 'all'`, which showSelectionControls switches on\n (m_tag_box.js _listConfig): a tri-state \"Select All\" over the rows shown. Hidden\n while the list is empty, as `.dx-empty-collection .dx-list-select-all` is. -->\n @if (filteredItems().length) {\n <div\n class=\"nxe-tag-select-all\"\n [id]=\"panelId() + '-select-all'\"\n role=\"option\"\n [attr.aria-selected]=\"selectAllState() === true\"\n [attr.aria-checked]=\"selectAllState() === undefined ? 'mixed' : selectAllState()\"\n (mousedown)=\"toggleAll($event)\">\n <span\n class=\"nxe-tag-check\"\n [class.nxe-tag-checked]=\"selectAllState() === true\"\n [class.nxe-tag-indeterminate]=\"selectAllState() === undefined\"\n aria-hidden=\"true\"></span>\n <span class=\"nxe-tag-select-all-label\">{{ selectAllText() ?? nxeMessages.t('dropdown.selectAll') }}</span>\n </div>\n }\n @for (item of filteredItems(); track $index; let i = $index) {\n <div\n class=\"nxe-list-item nxe-tag-item\"\n [id]=\"optionId(i)\"\n role=\"option\"\n [attr.aria-selected]=\"isSelected(item)\"\n [class.nxe-list-item-selected]=\"isSelected(item)\"\n (mousedown)=\"toggle(item, $event)\">\n <span class=\"nxe-tag-check\" [class.nxe-tag-checked]=\"isSelected(item)\" aria-hidden=\"true\"></span>\n <span class=\"nxe-tag-item-text\">{{ display(item) }}</span>\n </div>\n } @empty {\n <div class=\"nxe-list-empty\">{{ nxeMessages.t('dropdown.noData') }}</div>\n }\n } @else {\n <!-- dx `selectionMode: 'multiple'`: plain rows, selection shown by the row fill. -->\n @for (item of filteredItems(); track $index; let i = $index) {\n <div\n class=\"nxe-list-item\"\n [id]=\"optionId(i)\"\n role=\"option\"\n [attr.aria-selected]=\"isSelected(item)\"\n [class.nxe-list-item-selected]=\"isSelected(item)\"\n (mousedown)=\"toggle(item, $event)\">\n {{ display(item) }}\n </div>\n } @empty {\n <div class=\"nxe-list-empty\">{{ nxeMessages.t('dropdown.noData') }}</div>\n }\n }\n </div>\n</ng-template>\n", styles: ["nxe-tag-box{cursor:pointer}nxe-tag-box .nxe-tag-container{box-sizing:border-box;position:relative;display:flex;flex:1 1 auto;flex-wrap:wrap;align-items:stretch;align-self:stretch;min-width:0;padding-bottom:4px}nxe-tag-box .nxe-tag-input{flex:1 1 60px;min-width:60px;width:auto;height:auto;min-height:30px;padding-bottom:4px}nxe-tag-box .nxe-tag-container:has(>.nxe-tag)>.nxe-tag-input{padding-inline-start:0;margin-inline-start:5px}nxe-tag-box.nxe-invalid>.nxe-editor-container:not(:has(>.nxe-select-clear,>.nxe-select-arrow))>.nxe-tag-container:not(:has(>.nxe-tag))>.nxe-tag-input{padding-inline-end:34px}nxe-tag-box .nxe-tag{box-sizing:border-box;position:relative;display:inline-flex;align-self:flex-start;justify-content:center;max-width:calc(100% - 5px);min-width:40px;margin-top:4px;margin-inline-start:4px;padding-block:3px 4px;padding-inline:6px 25px;background-color:var(--nxe-ui-border, #ddd);border-radius:2px;color:var(--nxe-ui-text, #333)}nxe-tag-box .nxe-tag-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}nxe-tag-box .nxe-tag-remove{position:absolute;inset-inline-end:0;top:0;width:25px;height:100%;cursor:pointer}nxe-tag-box .nxe-tag-remove:before,nxe-tag-box .nxe-tag-remove:after{position:absolute;top:50%;content:\"\";transform:rotate(45deg);inset-inline-end:9px;margin-top:-5px;width:3px;height:11px;background:var(--nxe-ui-neutral-400, #aaa)}nxe-tag-box .nxe-tag-remove:after{inset-inline-end:5px;margin-top:-1px;width:11px;height:3px}nxe-tag-box:not(.nxe-disabled) .nxe-tag-remove:active:before,nxe-tag-box:not(.nxe-disabled) .nxe-tag-remove:active:after{background:var(--nxe-ui-border, #ddd)}nxe-tag-box.nxe-disabled .nxe-tag,nxe-tag-box.nxe-disabled .nxe-tag-remove{cursor:default}nxe-tag-box .nxe-select-clear{box-sizing:border-box;flex:0 0 34px;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;width:34px;min-width:34px;padding:0;color:var(--nxe-ui-text-muted, #999);cursor:pointer}nxe-tag-box .nxe-select-clear svg{display:block;width:18px;height:18px;fill:currentColor}nxe-tag-box .nxe-select-arrow{box-sizing:border-box;position:relative;flex:0 0 34px;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;width:34px;min-width:34px;padding:1px;border-radius:4px;background-clip:content-box;color:var(--nxe-ui-text, #333);cursor:pointer}nxe-tag-box .nxe-select-arrow svg{display:block;width:18px;height:18px;fill:currentColor}nxe-tag-box:hover:not(.nxe-disabled) .nxe-select-arrow,nxe-tag-box:active:not(.nxe-disabled) .nxe-select-arrow{background-color:var(--nxe-ui-neutral-150, #e6e6e6)}nxe-tag-box.nxe-open .nxe-select-arrow{background-color:var(--nxe-ui-neutral-350, #b3b3b3)}nxe-tag-box .nxe-editor-container:has(>.nxe-select-arrow) .nxe-editor-input{padding-inline-end:0}nxe-tag-box .nxe-editor-container:has(>.nxe-select-clear),nxe-tag-box .nxe-editor-container:has(>.nxe-select-arrow){--nxe-invalid-badge-inset: 38px}nxe-tag-box .nxe-editor-container:has(>.nxe-select-clear):has(>.nxe-select-arrow){--nxe-invalid-badge-inset: 72px}.nxe-select-panel .nxe-list-item.nxe-tag-item{display:block;position:relative;padding-block:10px;padding-inline:41px 10px}.nxe-select-panel .nxe-list-item.nxe-tag-item:hover,.nxe-select-panel .nxe-list-item.nxe-tag-item:active,.nxe-select-panel .nxe-list-item.nxe-tag-item.nxe-list-item-selected,.nxe-select-panel .nxe-list-item.nxe-tag-item.nxe-list-item-selected:hover:not(.nxe-list-item-active){background-color:transparent;color:var(--nxe-ui-text)}.nxe-tag-check{box-sizing:border-box;position:relative;display:block;width:22px;height:22px;border:1px solid var(--nxe-ui-border);border-radius:2px;background:var(--nxe-ui-surface)}.nxe-tag-check.nxe-tag-checked:after{content:\"\";position:absolute;box-sizing:border-box;top:5px;inset-inline-start:2.5px;width:15px;height:11px;background-color:var(--nxe-ui-accent);clip-path:polygon(0 5.586px,5.414px 11px,15px 1.414px,13.586px 0,5.414px 8.172px,1.414px 4.172px)}@media(forced-colors:active){.nxe-tag-check.nxe-tag-checked:after{forced-color-adjust:none;background-color:CanvasText}}.nxe-tag-check.nxe-tag-indeterminate:after{content:\"\";position:absolute;box-sizing:border-box;top:50%;inset-inline-start:50%;width:12px;height:12px;margin-top:-6px;margin-inline-start:-6px;border-radius:2px;background-color:var(--nxe-ui-accent)}@media(forced-colors:active){.nxe-tag-check.nxe-tag-indeterminate:after{forced-color-adjust:none;background-color:CanvasText}}.nxe-tag-item .nxe-tag-check{position:absolute;top:50%;inset-inline-start:10px;margin-top:-10px}.nxe-tag-item:hover .nxe-tag-check,.nxe-tag-select-all:hover .nxe-tag-check{border-color:var(--nxe-ui-checkbox-hover-border, #265a87)}.nxe-tag-select-all{box-sizing:border-box;padding-block:12px 11px;padding-inline:0;border-bottom:1px solid var(--nxe-ui-border);margin-bottom:3px;white-space:nowrap;cursor:pointer}.nxe-tag-select-all .nxe-tag-check{float:inline-start;margin-top:-1px;margin-bottom:-3px;margin-inline-start:10px}.nxe-tag-select-all-label{display:inline-block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:17px;padding:0 10px;margin-top:1px}\n"] }]
830
+ }], propDecorators: { dropdownTemplate: [{ type: i0.ViewChild, args: ['dropdown', { isSignal: true }] }], inputEl: [{ type: i0.ViewChild, args: ['input', { isSignal: true }] }], dataSource: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataSource", required: false }] }], displayExprInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayExpr", required: false }] }], valueExprInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueExpr", required: false }] }], searchExprInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchExpr", required: false }] }], searchEnabledInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchEnabled", required: false }] }], showClearButtonInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClearButton", required: false }] }], showSelectionControlsInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSelectionControls", required: false }] }], selectAllText: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectAllText", required: false }] }], showDropDownButtonInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "showDropDownButton", required: false }] }], placeholderInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], searchTimeout: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchTimeout", required: false }] }], minSearchLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "minSearchLength", required: false }] }], showDataBeforeSearch: [{ type: i0.Input, args: [{ isSignal: true, alias: "showDataBeforeSearch", required: false }] }], pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }], searchChange: [{ type: i0.Output, args: ["searchChange"] }] } });
490
831
 
491
832
  /**
492
833
  * `nxe-drop-down-options` — drop-in for DevExtreme `dxo-drop-down-options`, a no-template config
@@ -621,8 +962,8 @@ class NxeDropDownBoxComponent extends NxeFormControlBase {
621
962
  }
622
963
  // The bound value is the code; match it against valueExpr (default the implicit `lovCode`).
623
964
  const ve = this.valueExpr() || 'lovCode';
624
- const item = (this.dataSource() ?? []).find((it) => it?.[ve] === v);
625
- return item ? String(item[de] ?? '') : String(v);
965
+ const item = (this.dataSource() ?? []).find((it) => nxeResolveExpr(ve, it) === v);
966
+ return item ? nxeResolveText(de, item) : String(v);
626
967
  }, /* @ts-ignore */
627
968
  ...(ngDevMode ? [{ debugName: "displayText" }] : /* istanbul ignore next */ []));
628
969
  hasValue = computed(() => {
@@ -691,22 +1032,22 @@ class NxeDropDownBoxComponent extends NxeFormControlBase {
691
1032
  this.overlayRef?.dispose();
692
1033
  }
693
1034
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDropDownBoxComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
694
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeDropDownBoxComponent, isStandalone: true, selector: "nxe-drop-down-box", inputs: { dataSource: { classPropertyName: "dataSource", publicName: "dataSource", isSignal: true, isRequired: false, transformFunction: null }, displayExprInput: { classPropertyName: "displayExprInput", publicName: "displayExpr", isSignal: true, isRequired: false, transformFunction: null }, valueExprInput: { classPropertyName: "valueExprInput", publicName: "valueExpr", isSignal: true, isRequired: false, transformFunction: null }, placeholderInput: { classPropertyName: "placeholderInput", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, dropDownOptions: { classPropertyName: "dropDownOptions", publicName: "dropDownOptions", isSignal: true, isRequired: false, transformFunction: null }, showClearButtonInput: { classPropertyName: "showClearButtonInput", publicName: "showClearButton", isSignal: true, isRequired: false, transformFunction: null }, opened: { classPropertyName: "opened", publicName: "opened", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { opened: "openedChange", onClosed: "onClosed" }, host: { properties: { "class.nxe-focused": "focused()", "class.nxe-invalid": "!!validationError()", "class.nxe-open": "open()" }, classAttribute: "nxe-widget nxe-editor nxe-dropdownbox" }, providers: [
1035
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeDropDownBoxComponent, isStandalone: true, selector: "nxe-drop-down-box", inputs: { dataSource: { classPropertyName: "dataSource", publicName: "dataSource", isSignal: true, isRequired: false, transformFunction: null }, displayExprInput: { classPropertyName: "displayExprInput", publicName: "displayExpr", isSignal: true, isRequired: false, transformFunction: null }, valueExprInput: { classPropertyName: "valueExprInput", publicName: "valueExpr", isSignal: true, isRequired: false, transformFunction: null }, placeholderInput: { classPropertyName: "placeholderInput", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, dropDownOptions: { classPropertyName: "dropDownOptions", publicName: "dropDownOptions", isSignal: true, isRequired: false, transformFunction: null }, showClearButtonInput: { classPropertyName: "showClearButtonInput", publicName: "showClearButton", isSignal: true, isRequired: false, transformFunction: null }, opened: { classPropertyName: "opened", publicName: "opened", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { opened: "openedChange", onClosed: "onClosed" }, host: { properties: { "class.nxe-focused": "focused()", "class.nxe-invalid": "invalidState()", "class.nxe-open": "open()" }, classAttribute: "nxe-widget nxe-editor nxe-dropdownbox" }, providers: [
695
1036
  { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeDropDownBoxComponent), multi: true },
696
1037
  { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeDropDownBoxComponent) },
697
- ], queries: [{ propertyName: "templates", predicate: NxeTemplateDirective, isSignal: true }, { propertyName: "options", first: true, predicate: NxeDropDownOptionsComponent, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "panelTemplate", first: true, predicate: ["panel"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div class=\"nxe-editor-container\" (mousedown)=\"onFieldMousedown($event)\">\r\n <input\r\n class=\"nxe-editor-input nxe-dropdownbox-input\"\r\n type=\"text\"\r\n readonly\r\n [value]=\"displayText()\"\r\n [disabled]=\"disabled()\"\r\n [attr.placeholder]=\"placeholder() || null\"\r\n (focus)=\"focused.set(true)\"\r\n (blur)=\"onBlur()\" />\r\n @if (showClearButton() && hasValue() && !readOnly() && !disabled()) {\r\n <span class=\"nxe-select-clear\" (mousedown)=\"onClear($event)\" aria-hidden=\"true\">\r\n <svg viewBox=\"0 0 24 24\"><path d=\"M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z\" /></svg>\r\n </span>\r\n }\r\n\r\n @if (!readOnly()) {\r\n <span class=\"nxe-select-arrow\" aria-hidden=\"true\">\r\n <svg viewBox=\"0 0 24 24\"><path d=\"M7 10l5 5 5-5z\" /></svg>\r\n </span>\r\n }\r\n</div>\r\n\r\n<ng-content />\r\n\r\n@if (validationError(); as error) {\r\n @if (focused()) {\r\n <div class=\"nxe-invalid-message\">{{ error }}</div>\r\n }\r\n}\r\n\r\n<ng-template #panel>\r\n <div class=\"nxe-dropdownbox-panel\" [style.width]=\"panelStyleWidth()\">\r\n @if (contentTemplateRef(); as tpl) {\r\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: value() }\" />\r\n }\r\n </div>\r\n</ng-template>\r\n", styles: ["nxe-drop-down-box{cursor:pointer}nxe-drop-down-box .nxe-dropdownbox-input{cursor:pointer}nxe-drop-down-box .nxe-select-clear{flex:0 0 22px;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;width:22px;color:#999;cursor:pointer}nxe-drop-down-box .nxe-select-clear svg{width:16px;height:16px;fill:currentColor}nxe-drop-down-box .nxe-select-clear:hover{color:#333}nxe-drop-down-box .nxe-select-arrow{flex:0 0 34px;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;width:34px;color:#333;border-radius:4px}nxe-drop-down-box .nxe-select-arrow svg{width:18px;height:18px;fill:currentColor}nxe-drop-down-box .nxe-editor-container:has(>.nxe-select-arrow){--nxe-invalid-badge-inset: 38px}nxe-drop-down-box .nxe-editor-container:has(>.nxe-select-clear){--nxe-invalid-badge-inset: 26px}nxe-drop-down-box .nxe-editor-container:has(>.nxe-select-arrow):has(>.nxe-select-clear){--nxe-invalid-badge-inset: 60px}.nxe-dropdownbox-panel{box-sizing:border-box;background:#fff;border:1px solid #ddd;border-radius:6px;box-shadow:0 6px 12px #0000002d;overflow:auto;max-height:420px}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1038
+ ], queries: [{ propertyName: "templates", predicate: NxeTemplateDirective, isSignal: true }, { propertyName: "options", first: true, predicate: NxeDropDownOptionsComponent, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "panelTemplate", first: true, predicate: ["panel"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div class=\"nxe-editor-container\" (mousedown)=\"onFieldMousedown($event)\">\n <input\n class=\"nxe-editor-input nxe-dropdownbox-input\"\n [attr.tabindex]=\"tabIndex()\"\n role=\"combobox\"\n aria-haspopup=\"dialog\"\n [attr.aria-expanded]=\"opened()\"\n [attr.aria-controls]=\"opened() ? panelId() : null\"\n [id]=\"controlId()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-labelledby]=\"ariaLabelledBy() || null\"\n [attr.aria-invalid]=\"invalidState() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n type=\"text\"\n readonly\n [value]=\"displayText()\"\n [disabled]=\"disabled()\"\n [attr.placeholder]=\"placeholder() || null\"\n (focus)=\"focused.set(true)\"\n (blur)=\"onBlur()\" />\n @if (showClearButton() && hasValue() && !readOnly() && !disabled()) {\n <span class=\"nxe-select-clear\" (mousedown)=\"onClear($event)\" aria-hidden=\"true\">\n <!-- A \u00D816 disc with a 2px \u00D7 cut out: two 2px bars, each running corner to corner of the\n 8px square centred on the disc, drawn as a plus and turned 45\u00B0. -->\n <svg viewBox=\"0 0 18 18\" focusable=\"false\">\n <path fill-rule=\"evenodd\" transform=\"rotate(45 9 9)\"\n d=\"M9 1a8 8 0 1 1 0 16A8 8 0 1 1 9 1ZM8 3.343h2V8h4.657v2H10v4.657H8V10H3.343V8H8Z\" />\n </svg>\n </span>\n }\n\n @if (!readOnly()) {\n <span class=\"nxe-select-arrow\" aria-hidden=\"true\">\n <!-- An 8x4 triangle centred in the 18px box. -->\n <svg viewBox=\"0 0 18 18\" focusable=\"false\"><path d=\"M5 7h8l-4 4z\" /></svg>\n </span>\n }\n</div>\n\n<ng-content />\n\n@if (validationError(); as error) {\n @if (focused()) {\n <div class=\"nxe-invalid-message\">{{ error }}</div>\n }\n}\n\n<ng-template #panel>\n <div class=\"nxe-dropdownbox-panel\" [id]=\"panelId()\" role=\"dialog\" [style.width]=\"panelStyleWidth()\">\n @if (contentTemplateRef(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: value() }\" />\n }\n </div>\n</ng-template>\n", styles: ["nxe-drop-down-box{cursor:pointer}nxe-drop-down-box .nxe-dropdownbox-input{cursor:pointer}nxe-drop-down-box .nxe-select-clear{box-sizing:border-box;flex:0 0 34px;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;width:34px;min-width:34px;padding:0;color:var(--nxe-ui-text-muted, #999);cursor:pointer}nxe-drop-down-box .nxe-select-clear svg{display:block;width:18px;height:18px;fill:currentColor}nxe-drop-down-box .nxe-select-arrow{box-sizing:border-box;position:relative;flex:0 0 34px;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;width:34px;min-width:34px;padding:1px;border-radius:4px;background-clip:content-box;color:var(--nxe-ui-text, #333);cursor:pointer}nxe-drop-down-box .nxe-select-arrow svg{display:block;width:18px;height:18px;fill:currentColor}nxe-drop-down-box:hover:not(.nxe-disabled) .nxe-select-arrow,nxe-drop-down-box:active:not(.nxe-disabled) .nxe-select-arrow{background-color:var(--nxe-ui-neutral-150, #e6e6e6)}nxe-drop-down-box.nxe-open .nxe-select-arrow{background-color:var(--nxe-ui-neutral-350, #b3b3b3)}nxe-drop-down-box .nxe-editor-container:has(>.nxe-select-arrow) .nxe-editor-input{padding-inline-end:0}nxe-drop-down-box .nxe-editor-container:has(>.nxe-select-clear),nxe-drop-down-box .nxe-editor-container:has(>.nxe-select-arrow){--nxe-invalid-badge-inset: 38px}nxe-drop-down-box .nxe-editor-container:has(>.nxe-select-clear):has(>.nxe-select-arrow){--nxe-invalid-badge-inset: 72px}.nxe-dropdownbox-panel{box-sizing:border-box;background:var(--nxe-ui-surface, #fff);border:1px solid var(--nxe-ui-border, #ddd);border-radius:6px;box-shadow:var(--nxe-ui-popup-shadow, 0 6px 12px rgba(0, 0, 0, .175));overflow:auto;max-height:420px}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
698
1039
  }
699
1040
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDropDownBoxComponent, decorators: [{
700
1041
  type: Component,
701
1042
  args: [{ selector: 'nxe-drop-down-box', standalone: true, imports: [NgTemplateOutlet], host: {
702
1043
  class: 'nxe-widget nxe-editor nxe-dropdownbox',
703
1044
  '[class.nxe-focused]': 'focused()',
704
- '[class.nxe-invalid]': '!!validationError()',
1045
+ '[class.nxe-invalid]': 'invalidState()',
705
1046
  '[class.nxe-open]': 'open()',
706
1047
  }, providers: [
707
1048
  { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeDropDownBoxComponent), multi: true },
708
1049
  { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeDropDownBoxComponent) },
709
- ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<div class=\"nxe-editor-container\" (mousedown)=\"onFieldMousedown($event)\">\r\n <input\r\n class=\"nxe-editor-input nxe-dropdownbox-input\"\r\n type=\"text\"\r\n readonly\r\n [value]=\"displayText()\"\r\n [disabled]=\"disabled()\"\r\n [attr.placeholder]=\"placeholder() || null\"\r\n (focus)=\"focused.set(true)\"\r\n (blur)=\"onBlur()\" />\r\n @if (showClearButton() && hasValue() && !readOnly() && !disabled()) {\r\n <span class=\"nxe-select-clear\" (mousedown)=\"onClear($event)\" aria-hidden=\"true\">\r\n <svg viewBox=\"0 0 24 24\"><path d=\"M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z\" /></svg>\r\n </span>\r\n }\r\n\r\n @if (!readOnly()) {\r\n <span class=\"nxe-select-arrow\" aria-hidden=\"true\">\r\n <svg viewBox=\"0 0 24 24\"><path d=\"M7 10l5 5 5-5z\" /></svg>\r\n </span>\r\n }\r\n</div>\r\n\r\n<ng-content />\r\n\r\n@if (validationError(); as error) {\r\n @if (focused()) {\r\n <div class=\"nxe-invalid-message\">{{ error }}</div>\r\n }\r\n}\r\n\r\n<ng-template #panel>\r\n <div class=\"nxe-dropdownbox-panel\" [style.width]=\"panelStyleWidth()\">\r\n @if (contentTemplateRef(); as tpl) {\r\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: value() }\" />\r\n }\r\n </div>\r\n</ng-template>\r\n", styles: ["nxe-drop-down-box{cursor:pointer}nxe-drop-down-box .nxe-dropdownbox-input{cursor:pointer}nxe-drop-down-box .nxe-select-clear{flex:0 0 22px;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;width:22px;color:#999;cursor:pointer}nxe-drop-down-box .nxe-select-clear svg{width:16px;height:16px;fill:currentColor}nxe-drop-down-box .nxe-select-clear:hover{color:#333}nxe-drop-down-box .nxe-select-arrow{flex:0 0 34px;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;width:34px;color:#333;border-radius:4px}nxe-drop-down-box .nxe-select-arrow svg{width:18px;height:18px;fill:currentColor}nxe-drop-down-box .nxe-editor-container:has(>.nxe-select-arrow){--nxe-invalid-badge-inset: 38px}nxe-drop-down-box .nxe-editor-container:has(>.nxe-select-clear){--nxe-invalid-badge-inset: 26px}nxe-drop-down-box .nxe-editor-container:has(>.nxe-select-arrow):has(>.nxe-select-clear){--nxe-invalid-badge-inset: 60px}.nxe-dropdownbox-panel{box-sizing:border-box;background:#fff;border:1px solid #ddd;border-radius:6px;box-shadow:0 6px 12px #0000002d;overflow:auto;max-height:420px}\n"] }]
1050
+ ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<div class=\"nxe-editor-container\" (mousedown)=\"onFieldMousedown($event)\">\n <input\n class=\"nxe-editor-input nxe-dropdownbox-input\"\n [attr.tabindex]=\"tabIndex()\"\n role=\"combobox\"\n aria-haspopup=\"dialog\"\n [attr.aria-expanded]=\"opened()\"\n [attr.aria-controls]=\"opened() ? panelId() : null\"\n [id]=\"controlId()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-labelledby]=\"ariaLabelledBy() || null\"\n [attr.aria-invalid]=\"invalidState() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n type=\"text\"\n readonly\n [value]=\"displayText()\"\n [disabled]=\"disabled()\"\n [attr.placeholder]=\"placeholder() || null\"\n (focus)=\"focused.set(true)\"\n (blur)=\"onBlur()\" />\n @if (showClearButton() && hasValue() && !readOnly() && !disabled()) {\n <span class=\"nxe-select-clear\" (mousedown)=\"onClear($event)\" aria-hidden=\"true\">\n <!-- A \u00D816 disc with a 2px \u00D7 cut out: two 2px bars, each running corner to corner of the\n 8px square centred on the disc, drawn as a plus and turned 45\u00B0. -->\n <svg viewBox=\"0 0 18 18\" focusable=\"false\">\n <path fill-rule=\"evenodd\" transform=\"rotate(45 9 9)\"\n d=\"M9 1a8 8 0 1 1 0 16A8 8 0 1 1 9 1ZM8 3.343h2V8h4.657v2H10v4.657H8V10H3.343V8H8Z\" />\n </svg>\n </span>\n }\n\n @if (!readOnly()) {\n <span class=\"nxe-select-arrow\" aria-hidden=\"true\">\n <!-- An 8x4 triangle centred in the 18px box. -->\n <svg viewBox=\"0 0 18 18\" focusable=\"false\"><path d=\"M5 7h8l-4 4z\" /></svg>\n </span>\n }\n</div>\n\n<ng-content />\n\n@if (validationError(); as error) {\n @if (focused()) {\n <div class=\"nxe-invalid-message\">{{ error }}</div>\n }\n}\n\n<ng-template #panel>\n <div class=\"nxe-dropdownbox-panel\" [id]=\"panelId()\" role=\"dialog\" [style.width]=\"panelStyleWidth()\">\n @if (contentTemplateRef(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: value() }\" />\n }\n </div>\n</ng-template>\n", styles: ["nxe-drop-down-box{cursor:pointer}nxe-drop-down-box .nxe-dropdownbox-input{cursor:pointer}nxe-drop-down-box .nxe-select-clear{box-sizing:border-box;flex:0 0 34px;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;width:34px;min-width:34px;padding:0;color:var(--nxe-ui-text-muted, #999);cursor:pointer}nxe-drop-down-box .nxe-select-clear svg{display:block;width:18px;height:18px;fill:currentColor}nxe-drop-down-box .nxe-select-arrow{box-sizing:border-box;position:relative;flex:0 0 34px;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;width:34px;min-width:34px;padding:1px;border-radius:4px;background-clip:content-box;color:var(--nxe-ui-text, #333);cursor:pointer}nxe-drop-down-box .nxe-select-arrow svg{display:block;width:18px;height:18px;fill:currentColor}nxe-drop-down-box:hover:not(.nxe-disabled) .nxe-select-arrow,nxe-drop-down-box:active:not(.nxe-disabled) .nxe-select-arrow{background-color:var(--nxe-ui-neutral-150, #e6e6e6)}nxe-drop-down-box.nxe-open .nxe-select-arrow{background-color:var(--nxe-ui-neutral-350, #b3b3b3)}nxe-drop-down-box .nxe-editor-container:has(>.nxe-select-arrow) .nxe-editor-input{padding-inline-end:0}nxe-drop-down-box .nxe-editor-container:has(>.nxe-select-clear),nxe-drop-down-box .nxe-editor-container:has(>.nxe-select-arrow){--nxe-invalid-badge-inset: 38px}nxe-drop-down-box .nxe-editor-container:has(>.nxe-select-clear):has(>.nxe-select-arrow){--nxe-invalid-badge-inset: 72px}.nxe-dropdownbox-panel{box-sizing:border-box;background:var(--nxe-ui-surface, #fff);border:1px solid var(--nxe-ui-border, #ddd);border-radius:6px;box-shadow:var(--nxe-ui-popup-shadow, 0 6px 12px rgba(0, 0, 0, .175));overflow:auto;max-height:420px}\n"] }]
710
1051
  }], ctorParameters: () => [], propDecorators: { panelTemplate: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }], templates: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => NxeTemplateDirective), { isSignal: true }] }], options: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NxeDropDownOptionsComponent), { isSignal: true }] }], dataSource: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataSource", required: false }] }], displayExprInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayExpr", required: false }] }], valueExprInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueExpr", required: false }] }], placeholderInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], dropDownOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "dropDownOptions", required: false }] }], showClearButtonInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClearButton", required: false }] }], opened: [{ type: i0.Input, args: [{ isSignal: true, alias: "opened", required: false }] }, { type: i0.Output, args: ["openedChange"] }], onClosed: [{ type: i0.Output, args: ["onClosed"] }] } });
711
1052
 
712
1053
  /** DI token the menu provides so projected `nxe-menu-item`s can close it on click. */
@@ -731,6 +1072,20 @@ class NxeListComponent {
731
1072
  /** DevExtreme `[elementAttr]` — `{ id, class }` applied to the host. */
732
1073
  elementAttr = input(/* @ts-ignore */
733
1074
  ...(ngDevMode ? [undefined, { debugName: "elementAttr" }] : /* istanbul ignore next */ []));
1075
+ /**
1076
+ * dx `keyExpr` — the field identifying an item. Used for `@for` tracking, so a list whose
1077
+ * items are replaced by equal-but-not-identical objects keeps its DOM instead of rebuilding
1078
+ * every row. Falls back to the index when unset, which is the previous behaviour.
1079
+ */
1080
+ keyExpr = input(undefined, /* @ts-ignore */
1081
+ ...(ngDevMode ? [{ debugName: "keyExpr" }] : /* istanbul ignore next */ []));
1082
+ trackItem(item, index) {
1083
+ const key = this.keyExpr();
1084
+ if (!key || item == null || typeof item !== 'object') {
1085
+ return index;
1086
+ }
1087
+ return item[key] ?? index;
1088
+ }
734
1089
  onItemClick = output();
735
1090
  items = computed(() => this.dataSource() ?? [], /* @ts-ignore */
736
1091
  ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
@@ -751,8 +1106,8 @@ class NxeListComponent {
751
1106
  this.menu?.closeMenu();
752
1107
  }
753
1108
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
754
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeListComponent, isStandalone: true, selector: "nxe-list", inputs: { dataSource: { classPropertyName: "dataSource", publicName: "dataSource", isSignal: true, isRequired: false, transformFunction: null }, itemTemplate: { classPropertyName: "itemTemplate", publicName: "itemTemplate", isSignal: true, isRequired: false, transformFunction: null }, elementAttr: { classPropertyName: "elementAttr", publicName: "elementAttr", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onItemClick: "onItemClick" }, host: { properties: { "class": "'nxe-widget nxe-list ' + (elementAttr()?.class ?? '')", "attr.id": "elementAttr()?.id ?? null" } }, queries: [{ propertyName: "templates", predicate: NxeTemplateDirective, isSignal: true }], ngImport: i0, template: `
755
- @for (item of items(); track $index; let last = $last) {
1109
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeListComponent, isStandalone: true, selector: "nxe-list", inputs: { dataSource: { classPropertyName: "dataSource", publicName: "dataSource", isSignal: true, isRequired: false, transformFunction: null }, itemTemplate: { classPropertyName: "itemTemplate", publicName: "itemTemplate", isSignal: true, isRequired: false, transformFunction: null }, elementAttr: { classPropertyName: "elementAttr", publicName: "elementAttr", isSignal: true, isRequired: false, transformFunction: null }, keyExpr: { classPropertyName: "keyExpr", publicName: "keyExpr", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onItemClick: "onItemClick" }, host: { properties: { "class": "'nxe-widget nxe-list ' + (elementAttr()?.class ?? '')", "attr.id": "elementAttr()?.id ?? null" } }, queries: [{ propertyName: "templates", predicate: NxeTemplateDirective, isSignal: true }], ngImport: i0, template: `
1110
+ @for (item of items(); track trackItem(item, $index); let last = $last) {
756
1111
  <div
757
1112
  class="nxe-list-item-row"
758
1113
  [class.nxe-state-disabled]="isDisabled(item)"
@@ -772,7 +1127,7 @@ class NxeListComponent {
772
1127
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeListComponent, decorators: [{
773
1128
  type: Component,
774
1129
  args: [{ selector: 'nxe-list', standalone: true, imports: [NgTemplateOutlet], template: `
775
- @for (item of items(); track $index; let last = $last) {
1130
+ @for (item of items(); track trackItem(item, $index); let last = $last) {
776
1131
  <div
777
1132
  class="nxe-list-item-row"
778
1133
  [class.nxe-state-disabled]="isDisabled(item)"
@@ -791,7 +1146,204 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
791
1146
  '[class]': "'nxe-widget nxe-list ' + (elementAttr()?.class ?? '')",
792
1147
  '[attr.id]': 'elementAttr()?.id ?? null',
793
1148
  }, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, styles: ["nxe-list{display:block}nxe-list .nxe-list-item-row{display:block;cursor:pointer}nxe-list .nxe-list-item-row.nxe-state-disabled{opacity:.5;cursor:default;-webkit-user-select:none;user-select:none}nxe-list .nxe-list-item-content{display:block}\n"] }]
794
- }], propDecorators: { templates: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => NxeTemplateDirective), { isSignal: true }] }], dataSource: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataSource", required: false }] }], itemTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemTemplate", required: false }] }], elementAttr: [{ type: i0.Input, args: [{ isSignal: true, alias: "elementAttr", required: false }] }], onItemClick: [{ type: i0.Output, args: ["onItemClick"] }] } });
1149
+ }], propDecorators: { templates: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => NxeTemplateDirective), { isSignal: true }] }], dataSource: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataSource", required: false }] }], itemTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemTemplate", required: false }] }], elementAttr: [{ type: i0.Input, args: [{ isSignal: true, alias: "elementAttr", required: false }] }], keyExpr: [{ type: i0.Input, args: [{ isSignal: true, alias: "keyExpr", required: false }] }], onItemClick: [{ type: i0.Output, args: ["onItemClick"] }] } });
1150
+
1151
+ /**
1152
+ * `nxe-auto-complete` — drop-in replacement for DevExtreme `dx-autocomplete`.
1153
+ *
1154
+ * The distinction from `nxe-select-box` is the value contract, and it is the whole reason this
1155
+ * is a separate component: here the control value is the TEXT the user typed. A suggestion is a
1156
+ * shortcut for typing, not a constraint on what may be entered, so free text survives and
1157
+ * `valueExpr` only says which field of a picked item becomes that text.
1158
+ *
1159
+ * Deliberately not merged into the existing Material-shaped `nxe-autocomplete`: that one takes
1160
+ * projected `nxe-option` children rather than a `[dataSource]`, so the two have incompatible
1161
+ * content models and merging them would make every future change to one a risk to the other.
1162
+ */
1163
+ class NxeAutoCompleteComponent extends NxeFormControlBase {
1164
+ overlay = inject(Overlay);
1165
+ viewContainerRef = inject(ViewContainerRef);
1166
+ dropdownTemplate = viewChild.required('dropdown', /* @ts-ignore */
1167
+ ...(ngDevMode ? [{ debugName: "dropdownTemplate" }] : /* istanbul ignore next */ []));
1168
+ inputEl = viewChild('input', /* @ts-ignore */
1169
+ ...(ngDevMode ? [{ debugName: "inputEl" }] : /* istanbul ignore next */ []));
1170
+ dataSource = input([], /* @ts-ignore */
1171
+ ...(ngDevMode ? [{ debugName: "dataSource" }] : /* istanbul ignore next */ []));
1172
+ /** Field of a picked item that becomes the value (dx default `lovName`). */
1173
+ valueExpr = input('lovName', /* @ts-ignore */
1174
+ ...(ngDevMode ? [{ debugName: "valueExpr" }] : /* istanbul ignore next */ []));
1175
+ /** Field matched against the query; defaults to {@link valueExpr}, as dx does. */
1176
+ searchExprInput = input(undefined, { ...(ngDevMode ? { debugName: "searchExprInput" } : /* istanbul ignore next */ {}), alias: 'searchExpr' });
1177
+ searchExpr = computed(() => this.searchExprInput() ?? this.valueExpr(), /* @ts-ignore */
1178
+ ...(ngDevMode ? [{ debugName: "searchExpr" }] : /* istanbul ignore next */ []));
1179
+ searchMode = input('contains', /* @ts-ignore */
1180
+ ...(ngDevMode ? [{ debugName: "searchMode" }] : /* istanbul ignore next */ []));
1181
+ /** Characters required before suggestions appear. dx defaults to 1; 0 shows all on focus. */
1182
+ minSearchLength = input(1, /* @ts-ignore */
1183
+ ...(ngDevMode ? [{ debugName: "minSearchLength" }] : /* istanbul ignore next */ []));
1184
+ /** dx caps the suggestion list; beyond this the user is expected to keep typing. */
1185
+ maxItemCount = input(10, /* @ts-ignore */
1186
+ ...(ngDevMode ? [{ debugName: "maxItemCount" }] : /* istanbul ignore next */ []));
1187
+ placeholder = input('', /* @ts-ignore */
1188
+ ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
1189
+ showClearButton = input(false, { ...(ngDevMode ? { debugName: "showClearButton" } : /* istanbul ignore next */ {}), transform: nxeOptionalBoolean });
1190
+ onSelectionChanged = output();
1191
+ focused = signal(false, /* @ts-ignore */
1192
+ ...(ngDevMode ? [{ debugName: "focused" }] : /* istanbul ignore next */ []));
1193
+ open = signal(false, /* @ts-ignore */
1194
+ ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
1195
+ activeIndex = signal(-1, /* @ts-ignore */
1196
+ ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
1197
+ panelWidth = signal(0, /* @ts-ignore */
1198
+ ...(ngDevMode ? [{ debugName: "panelWidth" }] : /* istanbul ignore next */ []));
1199
+ /** dx `.instance` facade — `focus()` is what the autofocus directive calls. */
1200
+ get instance() {
1201
+ return this;
1202
+ }
1203
+ overlayRef;
1204
+ closeSub;
1205
+ flipSub;
1206
+ text = computed(() => this.value() ?? '', /* @ts-ignore */
1207
+ ...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
1208
+ itemText(item) {
1209
+ return nxeResolveText(this.valueExpr(), item);
1210
+ }
1211
+ searchText(item) {
1212
+ return nxeResolveText(this.searchExpr(), item);
1213
+ }
1214
+ suggestions = computed(() => {
1215
+ const query = (this.value() ?? '').toLowerCase();
1216
+ if (query.length < this.minSearchLength()) {
1217
+ return [];
1218
+ }
1219
+ const mode = this.searchMode();
1220
+ const matches = (this.dataSource() ?? []).filter((item) => {
1221
+ const candidate = this.searchText(item).toLowerCase();
1222
+ if (mode === 'startswith') {
1223
+ return candidate.startsWith(query);
1224
+ }
1225
+ if (mode === 'equals') {
1226
+ return candidate === query;
1227
+ }
1228
+ return candidate.includes(query);
1229
+ });
1230
+ return matches.slice(0, this.maxItemCount());
1231
+ }, /* @ts-ignore */
1232
+ ...(ngDevMode ? [{ debugName: "suggestions" }] : /* istanbul ignore next */ []));
1233
+ onFieldInput(event) {
1234
+ // The typed text IS the value: an autocomplete constrains nothing.
1235
+ this.commitValue(event.target.value, event);
1236
+ this.activeIndex.set(-1);
1237
+ this.openPanel();
1238
+ }
1239
+ onFocus() {
1240
+ this.focused.set(true);
1241
+ this.openPanel();
1242
+ }
1243
+ onBlur() {
1244
+ this.focused.set(false);
1245
+ this.markAsTouched();
1246
+ this.closePanel();
1247
+ }
1248
+ pick(item, event) {
1249
+ event.preventDefault();
1250
+ this.commitValue(this.itemText(item), event);
1251
+ this.onSelectionChanged.emit({ selectedItem: item, value: this.value() });
1252
+ this.closePanel();
1253
+ }
1254
+ onKeydown(event) {
1255
+ if (event.key === 'Escape') {
1256
+ this.closePanel();
1257
+ return;
1258
+ }
1259
+ const items = this.suggestions();
1260
+ if (!items.length) {
1261
+ return;
1262
+ }
1263
+ if (event.key === 'ArrowDown') {
1264
+ event.preventDefault();
1265
+ this.openPanel();
1266
+ this.activeIndex.update((i) => Math.min(i + 1, items.length - 1));
1267
+ }
1268
+ else if (event.key === 'ArrowUp') {
1269
+ event.preventDefault();
1270
+ this.activeIndex.update((i) => Math.max(i - 1, 0));
1271
+ }
1272
+ else if (event.key === 'Enter' && this.activeIndex() >= 0) {
1273
+ this.pick(items[this.activeIndex()], event);
1274
+ }
1275
+ }
1276
+ onClear(event) {
1277
+ event.preventDefault();
1278
+ this.commitValue('', event);
1279
+ this.closePanel();
1280
+ }
1281
+ focus() {
1282
+ this.inputEl()?.nativeElement.focus();
1283
+ }
1284
+ openPanel() {
1285
+ if (this.readOnly() || this.disabled() || !this.suggestions().length) {
1286
+ this.closePanel();
1287
+ return;
1288
+ }
1289
+ if (this.overlayRef) {
1290
+ return;
1291
+ }
1292
+ const host = this.element;
1293
+ this.panelWidth.set(host.getBoundingClientRect().width);
1294
+ this.overlayRef = this.overlay.create({
1295
+ positionStrategy: this.overlay
1296
+ .position()
1297
+ .flexibleConnectedTo(host)
1298
+ .withPositions([
1299
+ { originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top' },
1300
+ { originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom' },
1301
+ ]),
1302
+ scrollStrategy: this.overlay.scrollStrategies.reposition(),
1303
+ panelClass: nxePane('nxe-autocomplete-box-pane'),
1304
+ });
1305
+ this.overlayRef.attach(new TemplatePortal(this.dropdownTemplate(), this.viewContainerRef));
1306
+ this.flipSub = trackPaneFlip(this.overlayRef);
1307
+ this.open.set(true);
1308
+ }
1309
+ closePanel() {
1310
+ this.overlayRef?.dispose();
1311
+ this.overlayRef = undefined;
1312
+ this.closeSub?.unsubscribe();
1313
+ this.flipSub?.unsubscribe();
1314
+ this.open.set(false);
1315
+ this.activeIndex.set(-1);
1316
+ }
1317
+ ngOnDestroy() {
1318
+ this.closePanel();
1319
+ }
1320
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeAutoCompleteComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
1321
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeAutoCompleteComponent, isStandalone: true, selector: "nxe-auto-complete", inputs: { dataSource: { classPropertyName: "dataSource", publicName: "dataSource", isSignal: true, isRequired: false, transformFunction: null }, valueExpr: { classPropertyName: "valueExpr", publicName: "valueExpr", isSignal: true, isRequired: false, transformFunction: null }, searchExprInput: { classPropertyName: "searchExprInput", publicName: "searchExpr", isSignal: true, isRequired: false, transformFunction: null }, searchMode: { classPropertyName: "searchMode", publicName: "searchMode", isSignal: true, isRequired: false, transformFunction: null }, minSearchLength: { classPropertyName: "minSearchLength", publicName: "minSearchLength", isSignal: true, isRequired: false, transformFunction: null }, maxItemCount: { classPropertyName: "maxItemCount", publicName: "maxItemCount", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, showClearButton: { classPropertyName: "showClearButton", publicName: "showClearButton", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSelectionChanged: "onSelectionChanged" }, host: { listeners: { "keydown": "onKeydown($event)" }, properties: { "class.nxe-focused": "focused()", "class.nxe-invalid": "invalidState()", "class.nxe-open": "open()" }, classAttribute: "nxe-widget nxe-editor nxe-autocomplete-box" }, providers: [
1322
+ {
1323
+ provide: NG_VALUE_ACCESSOR,
1324
+ useExisting: forwardRef(() => NxeAutoCompleteComponent),
1325
+ multi: true,
1326
+ },
1327
+ { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeAutoCompleteComponent) },
1328
+ ], viewQueries: [{ propertyName: "dropdownTemplate", first: true, predicate: ["dropdown"], descendants: true, isSignal: true }, { propertyName: "inputEl", first: true, predicate: ["input"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div class=\"nxe-editor-container\">\n <input\n #input\n class=\"nxe-editor-input nxe-autocomplete-input\"\n [attr.tabindex]=\"tabIndex()\"\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"open() ? panelId() : null\"\n [attr.aria-activedescendant]=\"open() && activeIndex() >= 0 ? optionId(activeIndex()) : null\"\n [id]=\"controlId()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-labelledby]=\"ariaLabelledBy() || null\"\n [attr.aria-invalid]=\"invalidState() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n type=\"text\"\n [value]=\"text()\"\n [readOnly]=\"readOnly()\"\n [disabled]=\"disabled()\"\n [attr.placeholder]=\"placeholder() || null\"\n autocomplete=\"off\"\n (input)=\"onFieldInput($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\" />\n\n @if (showClearButton() && text() && !readOnly() && !disabled()) {\n <span class=\"nxe-select-clear\" (mousedown)=\"onClear($event)\" aria-hidden=\"true\">\n <!-- A \u00D816 disc with a 2px \u00D7 cut out: two 2px bars, each running corner to corner of the\n 8px square centred on the disc, drawn as a plus and turned 45\u00B0. -->\n <svg viewBox=\"0 0 18 18\" focusable=\"false\">\n <path fill-rule=\"evenodd\" transform=\"rotate(45 9 9)\"\n d=\"M9 1a8 8 0 1 1 0 16A8 8 0 1 1 9 1ZM8 3.343h2V8h4.657v2H10v4.657H8V10H3.343V8H8Z\" />\n </svg>\n </span>\n }\n</div>\n\n<ng-content />\n\n@if (validationError(); as error) {\n @if (focused()) {\n <div class=\"nxe-invalid-message\">{{ error }}</div>\n }\n}\n\n<ng-template #dropdown>\n <div class=\"nxe-autocomplete-box-panel\" [style.width.px]=\"panelWidth()\"\n [id]=\"panelId()\"\n role=\"listbox\">\n @for (item of suggestions(); track $index; let i = $index) {\n <div\n class=\"nxe-list-item\"\n [id]=\"optionId(i)\"\n role=\"option\"\n [attr.aria-selected]=\"i === activeIndex()\"\n [class.nxe-list-item-active]=\"i === activeIndex()\"\n (mousedown)=\"pick(item, $event)\">\n {{ itemText(item) }}\n </div>\n }\n </div>\n</ng-template>\n", styles: ["nxe-auto-complete .nxe-select-clear{box-sizing:border-box;flex:0 0 34px;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;width:34px;min-width:34px;padding:0;color:var(--nxe-ui-text-muted, #999);cursor:pointer}nxe-auto-complete .nxe-select-clear svg{display:block;width:18px;height:18px;fill:currentColor}nxe-auto-complete .nxe-select-arrow{box-sizing:border-box;position:relative;flex:0 0 34px;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;width:34px;min-width:34px;padding:1px;border-radius:4px;background-clip:content-box;color:var(--nxe-ui-text, #333);cursor:pointer}nxe-auto-complete .nxe-select-arrow svg{display:block;width:18px;height:18px;fill:currentColor}nxe-auto-complete:hover:not(.nxe-disabled) .nxe-select-arrow,nxe-auto-complete:active:not(.nxe-disabled) .nxe-select-arrow{background-color:var(--nxe-ui-neutral-150, #e6e6e6)}nxe-auto-complete.nxe-open .nxe-select-arrow{background-color:var(--nxe-ui-neutral-350, #b3b3b3)}nxe-auto-complete .nxe-editor-container:has(>.nxe-select-arrow) .nxe-editor-input{padding-inline-end:0}nxe-auto-complete .nxe-editor-container:has(>.nxe-select-clear),nxe-auto-complete .nxe-editor-container:has(>.nxe-select-arrow){--nxe-invalid-badge-inset: 38px}nxe-auto-complete .nxe-editor-container:has(>.nxe-select-clear):has(>.nxe-select-arrow){--nxe-invalid-badge-inset: 72px}.nxe-autocomplete-box-panel{max-height:300px;overflow-y:auto;background:var(--nxe-ui-surface, #fff);border:1px solid var(--nxe-ui-border, #ddd);box-sizing:border-box}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1329
+ }
1330
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeAutoCompleteComponent, decorators: [{
1331
+ type: Component,
1332
+ args: [{ selector: 'nxe-auto-complete', standalone: true, host: {
1333
+ class: 'nxe-widget nxe-editor nxe-autocomplete-box',
1334
+ '[class.nxe-focused]': 'focused()',
1335
+ '[class.nxe-invalid]': 'invalidState()',
1336
+ '[class.nxe-open]': 'open()',
1337
+ '(keydown)': 'onKeydown($event)',
1338
+ }, providers: [
1339
+ {
1340
+ provide: NG_VALUE_ACCESSOR,
1341
+ useExisting: forwardRef(() => NxeAutoCompleteComponent),
1342
+ multi: true,
1343
+ },
1344
+ { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeAutoCompleteComponent) },
1345
+ ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<div class=\"nxe-editor-container\">\n <input\n #input\n class=\"nxe-editor-input nxe-autocomplete-input\"\n [attr.tabindex]=\"tabIndex()\"\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"open() ? panelId() : null\"\n [attr.aria-activedescendant]=\"open() && activeIndex() >= 0 ? optionId(activeIndex()) : null\"\n [id]=\"controlId()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-labelledby]=\"ariaLabelledBy() || null\"\n [attr.aria-invalid]=\"invalidState() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n type=\"text\"\n [value]=\"text()\"\n [readOnly]=\"readOnly()\"\n [disabled]=\"disabled()\"\n [attr.placeholder]=\"placeholder() || null\"\n autocomplete=\"off\"\n (input)=\"onFieldInput($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\" />\n\n @if (showClearButton() && text() && !readOnly() && !disabled()) {\n <span class=\"nxe-select-clear\" (mousedown)=\"onClear($event)\" aria-hidden=\"true\">\n <!-- A \u00D816 disc with a 2px \u00D7 cut out: two 2px bars, each running corner to corner of the\n 8px square centred on the disc, drawn as a plus and turned 45\u00B0. -->\n <svg viewBox=\"0 0 18 18\" focusable=\"false\">\n <path fill-rule=\"evenodd\" transform=\"rotate(45 9 9)\"\n d=\"M9 1a8 8 0 1 1 0 16A8 8 0 1 1 9 1ZM8 3.343h2V8h4.657v2H10v4.657H8V10H3.343V8H8Z\" />\n </svg>\n </span>\n }\n</div>\n\n<ng-content />\n\n@if (validationError(); as error) {\n @if (focused()) {\n <div class=\"nxe-invalid-message\">{{ error }}</div>\n }\n}\n\n<ng-template #dropdown>\n <div class=\"nxe-autocomplete-box-panel\" [style.width.px]=\"panelWidth()\"\n [id]=\"panelId()\"\n role=\"listbox\">\n @for (item of suggestions(); track $index; let i = $index) {\n <div\n class=\"nxe-list-item\"\n [id]=\"optionId(i)\"\n role=\"option\"\n [attr.aria-selected]=\"i === activeIndex()\"\n [class.nxe-list-item-active]=\"i === activeIndex()\"\n (mousedown)=\"pick(item, $event)\">\n {{ itemText(item) }}\n </div>\n }\n </div>\n</ng-template>\n", styles: ["nxe-auto-complete .nxe-select-clear{box-sizing:border-box;flex:0 0 34px;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;width:34px;min-width:34px;padding:0;color:var(--nxe-ui-text-muted, #999);cursor:pointer}nxe-auto-complete .nxe-select-clear svg{display:block;width:18px;height:18px;fill:currentColor}nxe-auto-complete .nxe-select-arrow{box-sizing:border-box;position:relative;flex:0 0 34px;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;width:34px;min-width:34px;padding:1px;border-radius:4px;background-clip:content-box;color:var(--nxe-ui-text, #333);cursor:pointer}nxe-auto-complete .nxe-select-arrow svg{display:block;width:18px;height:18px;fill:currentColor}nxe-auto-complete:hover:not(.nxe-disabled) .nxe-select-arrow,nxe-auto-complete:active:not(.nxe-disabled) .nxe-select-arrow{background-color:var(--nxe-ui-neutral-150, #e6e6e6)}nxe-auto-complete.nxe-open .nxe-select-arrow{background-color:var(--nxe-ui-neutral-350, #b3b3b3)}nxe-auto-complete .nxe-editor-container:has(>.nxe-select-arrow) .nxe-editor-input{padding-inline-end:0}nxe-auto-complete .nxe-editor-container:has(>.nxe-select-clear),nxe-auto-complete .nxe-editor-container:has(>.nxe-select-arrow){--nxe-invalid-badge-inset: 38px}nxe-auto-complete .nxe-editor-container:has(>.nxe-select-clear):has(>.nxe-select-arrow){--nxe-invalid-badge-inset: 72px}.nxe-autocomplete-box-panel{max-height:300px;overflow-y:auto;background:var(--nxe-ui-surface, #fff);border:1px solid var(--nxe-ui-border, #ddd);box-sizing:border-box}\n"] }]
1346
+ }], propDecorators: { dropdownTemplate: [{ type: i0.ViewChild, args: ['dropdown', { isSignal: true }] }], inputEl: [{ type: i0.ViewChild, args: ['input', { isSignal: true }] }], dataSource: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataSource", required: false }] }], valueExpr: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueExpr", required: false }] }], searchExprInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchExpr", required: false }] }], searchMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchMode", required: false }] }], minSearchLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "minSearchLength", required: false }] }], maxItemCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxItemCount", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], showClearButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClearButton", required: false }] }], onSelectionChanged: [{ type: i0.Output, args: ["onSelectionChanged"] }] } });
795
1347
 
796
1348
  /** DI token the autocomplete provides so projected `nxe-option`s can close it on click. */
797
1349
  const NXE_AUTOCOMPLETE = new InjectionToken('NXE_AUTOCOMPLETE');
@@ -949,5 +1501,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
949
1501
  * Generated bundle index. Do not edit.
950
1502
  */
951
1503
 
952
- export { NXE_AUTOCOMPLETE, NxeAutocompleteComponent, NxeAutocompleteTriggerDirective, NxeDropDownBoxComponent, NxeDropDownOptionsComponent, NxeListComponent, NxeOptionComponent, NxeSelectBoxComponent, NxeTagBoxComponent };
1504
+ export { NXE_AUTOCOMPLETE, NXE_MENU, NxeAutoCompleteComponent, NxeAutocompleteComponent, NxeAutocompleteTriggerDirective, NxeDropDownBoxComponent, NxeDropDownOptionsComponent, NxeListComponent, NxeOptionComponent, NxePickerSearch, NxeSelectBoxComponent, NxeTagBoxComponent, nxeIsPickerStore };
953
1505
  //# sourceMappingURL=nexterajs-ui-dropdowns.mjs.map