@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
@@ -1,12 +1,15 @@
1
- import { formatNumber, parseNumberFormat, parseDateValue, formatDisplay } from '@nexterajs/ui/intl';
1
+ import { NXE_EN_US_SEPARATORS, formatNumber, parseNumberFormat, parseDateValue, formatDisplay, NXE_NUMBER_SEPARATORS } from '@nexterajs/ui/intl';
2
2
  import * as i0 from '@angular/core';
3
- import { input, contentChildren, computed, ViewEncapsulation, ChangeDetectionStrategy, Component, signal, output, Directive, model, inject, ElementRef, ViewContainerRef, NgZone, contentChild, EventEmitter, afterRenderEffect, untracked, afterNextRender, linkedSignal, viewChild, Output } from '@angular/core';
3
+ import { input, ViewEncapsulation, ChangeDetectionStrategy, Component, contentChildren, contentChild, computed, signal, inject, output, viewChild, Directive, model, ElementRef, Injector, numberAttribute, booleanAttribute, linkedSignal, afterNextRender, ViewContainerRef, NgZone, DestroyRef, EventEmitter, untracked, afterRenderEffect, Output } from '@angular/core';
4
4
  import { NgTemplateOutlet } from '@angular/common';
5
+ import { NxeMessageService, NXE_DEFAULT_OPTIONS, NxeTemplateDirective, nxeResolveExpr, nxeResolveText } from '@nexterajs/ui/core';
5
6
  import { ComponentPortal, TemplatePortal } from '@angular/cdk/portal';
6
7
  import { nxePane } from '@nexterajs/ui/styling';
7
8
  import { Workbook, nxeSaveAs } from '@nexterajs/ui/excel';
9
+ import { isObservable, debounce, of, timer } from 'rxjs';
8
10
  import { Overlay } from '@angular/cdk/overlay';
9
- import { NXE_DEFAULT_OPTIONS, NxeTemplateDirective } from '@nexterajs/ui/core';
11
+ import { toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
12
+ import { DateTime, Info } from 'luxon';
10
13
 
11
14
  /* eslint-disable @typescript-eslint/no-explicit-any */
12
15
  // Shared value-formatting helpers for the nxe grid family — the single source of truth for how
@@ -55,21 +58,22 @@ function nxeNumberMask(format) {
55
58
  * `{ formatter }`). No format / an unresolvable one → `String(value)` (the grid's untyped
56
59
  * rendering).
57
60
  */
58
- function nxeFormatNumberValue(value, format) {
61
+ function nxeFormatNumberValue(value, format, separators = NXE_EN_US_SEPARATORS) {
59
62
  if (format && typeof format === 'object') {
60
63
  if (typeof format.formatter === 'function') {
61
64
  return String(format.formatter(value));
62
65
  }
63
66
  if (format.type === 'percent') {
64
67
  const mask = '#,##0' + precisionDecimals(format.precision) + '%';
65
- return formatNumber(value * 100, parseNumberFormat(mask));
68
+ return formatNumber(value * 100, parseNumberFormat(mask), separators);
66
69
  }
67
70
  }
68
71
  const mask = nxeNumberMask(format);
69
- return mask ? formatNumber(value, parseNumberFormat(mask)) : String(value);
72
+ return mask ? formatNumber(value, parseNumberFormat(mask), separators) : String(value);
70
73
  }
71
74
  /**
72
- * Derive the Excel `numFmt` mask for a `format`: string masks pass through, fixedPoint/currency
75
+ * Derive the Excel `numFmt` mask for a `format`: string masks go through
76
+ * {@link nxeExcelNumberMask} (the same conversion the grid export uses), fixedPoint/currency
73
77
  * come from {@link nxeNumberMask}, percent appends a bare `%` (Excel multiplies by 100 itself),
74
78
  * and any other `{ type }` is a date pattern used as-is. `{ formatter }`-only objects →
75
79
  * `undefined` (callers fall back to exporting the formatted text).
@@ -79,7 +83,7 @@ function nxeExcelNumFmt(format) {
79
83
  return undefined;
80
84
  }
81
85
  if (typeof format === 'string') {
82
- return format;
86
+ return nxeExcelNumberMask(format);
83
87
  }
84
88
  if (typeof format.formatter === 'function') {
85
89
  return undefined;
@@ -89,65 +93,175 @@ function nxeExcelNumFmt(format) {
89
93
  }
90
94
  return nxeNumberMask(format) ?? format.type ?? undefined;
91
95
  }
96
+ /** Excel's digit placeholders. */
97
+ const NXE_DIGIT_PLACEHOLDERS = new Set(['0', '#', '?']);
98
+ /**
99
+ * Excel `numFmt` for a dx LDML string mask.
100
+ *
101
+ * The two languages disagree on one character. In Excel an unquoted comma after the last digit
102
+ * placeholder SCALES the value by 1/1000, while dx's LDML engine ignores it (see the golden
103
+ * transcript in `@nexterajs/ui/intl`). The consumer percent mask `#,##0.00,'%'` therefore
104
+ * exported 100 as "0.10%". Dropped here, per `;` section: every unquoted comma after the
105
+ * section's decimal point, and a comma that follows a digit placeholder with no placeholder
106
+ * after it before the section's decimal point or end (`#,##0,'%'`, `#,##0,.00`) — none of them
107
+ * ever meant anything to dx. Grouping commas between integer placeholders stay, and
108
+ * single-quoted literals become Excel's double-quoted ones.
109
+ */
110
+ function nxeExcelNumberMask(mask) {
111
+ const chars = Array.from(mask);
112
+ // Whether each character is part of a single-quoted literal (its quotes included).
113
+ const quoted = [];
114
+ let inQuote = false;
115
+ for (const ch of chars) {
116
+ if (ch === "'") {
117
+ inQuote = !inQuote;
118
+ quoted.push(true);
119
+ }
120
+ else {
121
+ quoted.push(inQuote);
122
+ }
123
+ }
124
+ /** A digit placeholder follows the comma at `i` in its section's integer part: it groups. */
125
+ const groups = (i) => {
126
+ for (let j = i + 1; j < chars.length; j++) {
127
+ if (quoted[j]) {
128
+ continue;
129
+ }
130
+ if (chars[j] === '.' || chars[j] === ';') {
131
+ return false;
132
+ }
133
+ if (NXE_DIGIT_PLACEHOLDERS.has(chars[j])) {
134
+ return true;
135
+ }
136
+ }
137
+ return false;
138
+ };
139
+ let out = '';
140
+ let afterPoint = false;
141
+ // The last unquoted character that was not a comma, within the current section.
142
+ let prev = '';
143
+ for (let i = 0; i < chars.length; i++) {
144
+ const ch = chars[i];
145
+ if (ch === "'") {
146
+ out += '"';
147
+ continue;
148
+ }
149
+ if (!quoted[i]) {
150
+ if (ch === ';') {
151
+ afterPoint = false;
152
+ prev = '';
153
+ out += ch;
154
+ continue;
155
+ }
156
+ if (ch === '.') {
157
+ afterPoint = true;
158
+ }
159
+ if (ch === ',') {
160
+ if (afterPoint || (NXE_DIGIT_PLACEHOLDERS.has(prev) && !groups(i))) {
161
+ continue;
162
+ }
163
+ }
164
+ else {
165
+ prev = ch;
166
+ }
167
+ }
168
+ out += ch;
169
+ }
170
+ return out;
171
+ }
172
+ /**
173
+ * dx `getValueDataType` (m_columns_controller_utils.js:230-236): the type a value contributes
174
+ * to column type inference. `null`, arrays and functions contribute nothing.
175
+ */
176
+ function nxeValueDataType(v) {
177
+ if (v == null || Array.isArray(v)) {
178
+ return undefined;
179
+ }
180
+ if (v instanceof Date) {
181
+ return 'date';
182
+ }
183
+ const t = typeof v;
184
+ return t === 'string' || t === 'number' || t === 'boolean' || t === 'object' ? t : undefined;
185
+ }
186
+ /**
187
+ * dx's number-column deserializer (m_columns_controller_utils.js:263 `updateSerializers`):
188
+ * `parseFloat`, so "20.5" is 20.5 and "1,234" is 1 exactly as dx shows it. `NaN` when the
189
+ * value is not numeric at all, which callers render raw.
190
+ */
191
+ function nxeNumberValue(raw) {
192
+ return typeof raw === 'number' ? raw : parseFloat(String(raw));
193
+ }
92
194
  /**
93
195
  * The data grid's default cell rendering (pure extraction of nxe-data-grid's `defaultFormat`):
94
- * `dataType`/`format` → display text. Number cells honour string masks only — exactly the
95
- * grid's pre-extraction behaviour.
196
+ * `dataType`/`format` → display text.
197
+ *
198
+ * dx's format_helper (format_helper.js:23-43) formats by the VALUE, not by the column type: any
199
+ * JS number meets any valid `format`, whatever the column declared. So a number in an untyped
200
+ * or string column still takes its mask, and a number column honours object formats as well.
96
201
  */
97
- function nxeDefaultFormat(dataType, format, raw) {
202
+ function nxeDefaultFormat(dataType, format, raw, separators = NXE_EN_US_SEPARATORS, locale) {
98
203
  if (raw == null || raw === '') {
99
204
  return '';
100
205
  }
101
206
  switch (dataType) {
102
207
  case 'number': {
103
- const pattern = typeof format === 'string' ? format : undefined;
104
- const num = typeof raw === 'number' ? raw : Number(raw);
105
- return isNaN(num) ? String(raw) : formatNumber(num, parseNumberFormat(pattern));
208
+ const num = nxeNumberValue(raw);
209
+ return isNaN(num) ? String(raw) : nxeFormatNumberValue(num, format, separators);
106
210
  }
107
211
  case 'date': {
108
212
  const dt = parseDateValue(raw);
109
- return dt ? formatDisplay(dt, nxeDatePattern(format)) : String(raw);
213
+ return dt ? formatDisplay(dt, nxeDatePattern(format), locale) : String(raw);
110
214
  }
111
215
  case 'boolean':
112
216
  return raw ? '✓' : '';
113
217
  default:
114
- return String(raw);
218
+ // No format (or an unresolvable one) leaves the number as String(value), as dx does.
219
+ return typeof raw === 'number' ? nxeFormatNumberValue(raw, format, separators) : String(raw);
115
220
  }
116
221
  }
117
222
 
118
223
  /**
119
224
  * DOM lookups shared by the grid component and its drag/resize controllers.
120
225
  *
121
- * Both questions here used to be answered with `querySelector('.dx-header-row')`, which stopped
226
+ * Both questions here used to be answered with `querySelector('.nxe-header-row')`, which stopped
122
227
  * being right once bands could add header rows above the leaves.
123
228
  */
124
229
  /**
125
- * The all-leaves header row of THIS grid.
230
+ * The bottom `.nxe-header-row` of THIS grid.
231
+ *
232
+ * Used for header-strip geometry — the drop highlight and the drag indicator's vertical extent.
233
+ * It must belong to this grid rather than a nested master-detail grid; those live in the tbody,
234
+ * so the first `thead` in document order is always the outer grid's own.
126
235
  *
127
- * Two things have to be true at once. It must be the LAST `.dx-header-row` in the thead, because
128
- * bands sit above the leaves and only the bottom row has one cell per data column. And it must
129
- * belong to this grid rather than a nested master-detail grid — those live in the tbody, so the
130
- * first `thead` in document order is always the outer grid's own.
236
+ * It is NOT one cell per column — see {@link nxeHeaderLeafCells}.
131
237
  */
132
238
  function nxeLeafHeaderRow(host) {
133
- const thead = host.querySelector('.dx-datagrid-content > .dx-datagrid-table > thead');
134
- const rows = thead?.querySelectorAll('.dx-header-row');
239
+ const thead = host.querySelector('.nxe-datagrid-content > .nxe-datagrid-table > thead');
240
+ const rows = thead?.querySelectorAll('.nxe-header-row');
135
241
  return rows && rows.length ? rows[rows.length - 1] : null;
136
242
  }
137
243
  /**
138
- * The data cells of a header row, command columns removed.
244
+ * The header cell of each visible data column, in visible-column order.
139
245
  *
140
- * Indexing these by visible-column index is deliberate: command cells live in the FIRST header
141
- * row and span downwards, so a banded grid's leaf row has none, and any fixed lead offset would
142
- * be off by however many command columns the grid happens to show.
246
+ * Deliberately not "the cells of the bottom header row". A top-level column in a banded grid is
247
+ * emitted in the FIRST header row with a rowspan reaching the bottom, so the bottom row holds
248
+ * only the leaves that sit under a band: `Name | Shared(Gross, Comm) | Total` puts Name and Total
249
+ * in row 0 and just Gross and Comm in row 1. Reading the bottom row loses half the columns, and
250
+ * everything that indexes these by visible position — auto-width, resize, reorder, keyboard
251
+ * focus — then addresses the wrong column.
252
+ *
253
+ * `role="columnheader"` plus `aria-colindex` is the discriminator: every leaf header cell carries
254
+ * both, a band is a column header without a number, and a command cell (group indent, detail
255
+ * expander, select-all, edit) is numbered, as dx numbers it, but is no column header
256
+ * (m_column_headers.js:193). The number already accounts for the lead command columns, so sorting
257
+ * on it recovers visible order across however many rows the header happens to have.
143
258
  */
144
- function nxeHeaderDataCells(row) {
145
- if (!row) {
259
+ function nxeHeaderLeafCells(host) {
260
+ const thead = host.querySelector('.nxe-datagrid-content > .nxe-datagrid-table > thead');
261
+ if (!thead) {
146
262
  return [];
147
263
  }
148
- return Array.from(row.cells).filter((td) => !td.classList.contains('dx-command-expand') &&
149
- !td.classList.contains('dx-command-select') &&
150
- !td.classList.contains('dx-command-edit'));
264
+ return Array.from(thead.querySelectorAll('.nxe-header-row > [role="columnheader"][aria-colindex]')).sort((a, b) => Number(a.getAttribute('aria-colindex')) - Number(b.getAttribute('aria-colindex')));
151
265
  }
152
266
 
153
267
  /**
@@ -156,16 +270,16 @@ function nxeHeaderDataCells(row) {
156
270
  *
157
271
  * Deliberately NOT CDK DragDrop: the drop topologies are "header → group panel", "chip →
158
272
  * anywhere outside = ungroup/remove", "chip ↔ chip = reorder" — not list-to-list — and the
159
- * ghosts must be dx's exact clones (`.dx-datagrid-drag-header` for the grid,
160
- * `.dx-pivotgrid-fields-container.dx-drag` for the pivot panel). Document-level listeners run
273
+ * ghosts must be dx's exact clones (`.nxe-datagrid-drag-header` for the grid,
274
+ * `.nxe-pivotgrid-fields-container.nxe-drag` for the pivot panel). Document-level listeners run
161
275
  * outside the zone; Angular re-enters only on drop. No `setPointerCapture` (throws for
162
276
  * synthetic pointerIds under Karma).
163
277
  *
164
278
  * dx baseline facts encoded here:
165
- * - grid ghost: `div.dx-datagrid-drag-header.dx-datagrid-text-content.dx-widget.dx-datagrid-drag-action`
279
+ * - grid ghost: `div.nxe-datagrid-drag-header.nxe-datagrid-text-content.nxe-widget.nxe-datagrid-drag-action`
166
280
  * appended to <body>, caption text only, positioned near the cursor;
167
- * - insertion indicator: `div.dx-block-separator` placed between panel chips;
168
- * - chip drag highlights the header row with `dx-datagrid-drop-highlight`;
281
+ * - insertion indicator: `div.nxe-block-separator` placed between panel chips;
282
+ * - chip drag highlights the header row with `nxe-datagrid-drop-highlight`;
169
283
  * - the source cell/chip stays in place, dimmed;
170
284
  * - header→panel drop APPENDS as the last group level (indicator after the last chip).
171
285
  */
@@ -357,7 +471,7 @@ class NxeGridDragController {
357
471
  createGhost: () => {
358
472
  const ghost = document.createElement('div');
359
473
  ghost.className =
360
- 'dx-datagrid-drag-header dx-datagrid-text-content dx-widget dx-datagrid-drag-action nxe-grid-drag-ghost';
474
+ 'nxe-datagrid-drag-header nxe-datagrid-text-content nxe-widget nxe-datagrid-drag-action nxe-grid-drag-ghost';
361
475
  const col = this.drag?.col;
362
476
  ghost.textContent = col ? col.caption() || col.dataField() : '';
363
477
  ghost.style.whiteSpace = 'nowrap';
@@ -367,7 +481,7 @@ class NxeGridDragController {
367
481
  dragStarted: () => {
368
482
  if (this.drag?.kind === 'chip') {
369
483
  // dx: while a chip is dragged, the header row is the highlighted return target.
370
- this.headerRow()?.classList.add('dx-datagrid-drop-highlight');
484
+ this.headerRow()?.classList.add('nxe-datagrid-drop-highlight');
371
485
  }
372
486
  },
373
487
  dragMove: (x, y) => {
@@ -381,7 +495,7 @@ class NxeGridDragController {
381
495
  this.removeSeparator(this.drag);
382
496
  this.removeColIndicator(this.drag);
383
497
  }
384
- this.headerRow()?.classList.remove('dx-datagrid-drop-highlight');
498
+ this.headerRow()?.classList.remove('nxe-datagrid-drop-highlight');
385
499
  },
386
500
  reset: () => {
387
501
  this.drag = null;
@@ -430,7 +544,7 @@ class NxeGridDragController {
430
544
  }
431
545
  this.removeColIndicator(a);
432
546
  a.headerSlot = null;
433
- const chips = Array.from(panel.querySelectorAll('.dx-group-panel-item'));
547
+ const chips = Array.from(panel.querySelectorAll('.nxe-group-panel-item'));
434
548
  if (a.kind === 'header') {
435
549
  // dx appends on header→panel drop; the indicator sits after the last chip.
436
550
  a.dropIndex = chips.length;
@@ -518,7 +632,7 @@ class NxeGridDragController {
518
632
  placeSeparator(a, panel, chips, index) {
519
633
  if (!a.separator) {
520
634
  a.separator = document.createElement('div');
521
- a.separator.className = 'dx-block-separator';
635
+ a.separator.className = 'nxe-block-separator';
522
636
  a.separator.textContent = ' '; // dx renders the separator as an &nbsp; block
523
637
  }
524
638
  const reference = chips[index] ?? null;
@@ -531,7 +645,7 @@ class NxeGridDragController {
531
645
  a.separator = null;
532
646
  }
533
647
  panel() {
534
- return this.hooks.host().querySelector('.dx-datagrid-group-panel');
648
+ return this.hooks.host().querySelector('.nxe-datagrid-group-panel');
535
649
  }
536
650
  headerRow() {
537
651
  return nxeLeafHeaderRow(this.hooks.host());
@@ -542,6 +656,52 @@ class NxeGridDragController {
542
656
  // Shared types for the nxe-data-grid family. Row data is `any` to mirror DevExtreme's untyped
543
657
  // grid (the app stores plain JS records and reads arbitrary fields off `key.data`).
544
658
 
659
+ /**
660
+ * `nxe-header-filter` — drop-in replacement for DevExtreme `dxo-header-filter` (grid child option).
661
+ *
662
+ * Adds a funnel icon to each filterable column header; clicking it opens a popup of the column's
663
+ * distinct values (checkbox list + optional search) to filter the grid. The app uses essentially
664
+ * one shape: `[search]="{ enabled: true }" [visible]="true"` (60 files). Legacy `[allowSearch]`
665
+ * (1×) folds into `search.enabled`.
666
+ */
667
+ class NxeHeaderFilterComponent {
668
+ visible = input(true, /* @ts-ignore */
669
+ ...(ngDevMode ? [{ debugName: "visible" }] : /* istanbul ignore next */ []));
670
+ search = input(undefined, /* @ts-ignore */
671
+ ...(ngDevMode ? [{ debugName: "search" }] : /* istanbul ignore next */ []));
672
+ /** Legacy alias for `search.enabled` (one holdout). */
673
+ allowSearch = input(undefined, /* @ts-ignore */
674
+ ...(ngDevMode ? [{ debugName: "allowSearch" }] : /* istanbul ignore next */ []));
675
+ /**
676
+ * dx `headerFilter.dataSource` — a fixed `{ text, value }` list shown instead of the values
677
+ * derived from the rows.
678
+ */
679
+ dataSource = input(undefined, /* @ts-ignore */
680
+ ...(ngDevMode ? [{ debugName: "dataSource" }] : /* istanbul ignore next */ []));
681
+ /** Whether the in-popup search box shows.
682
+ *
683
+ * Defaults to **true**, matching the legacy DevExtreme deployment: a popup listing every
684
+ * distinct value in a column is close to unusable without one. 60 of the app's templates
685
+ * already opt in explicitly, so this only changes the handful that never said either way —
686
+ * and an explicit `[search]="{ enabled: false }"` still turns it off. */
687
+ searchEnabled() {
688
+ return this.search()?.enabled ?? this.allowSearch() ?? true;
689
+ }
690
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeHeaderFilterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
691
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.5", type: NxeHeaderFilterComponent, isStandalone: true, selector: "nxe-header-filter", inputs: { visible: { classPropertyName: "visible", publicName: "visible", isSignal: true, isRequired: false, transformFunction: null }, search: { classPropertyName: "search", publicName: "search", isSignal: true, isRequired: false, transformFunction: null }, allowSearch: { classPropertyName: "allowSearch", publicName: "allowSearch", isSignal: true, isRequired: false, transformFunction: null }, dataSource: { classPropertyName: "dataSource", publicName: "dataSource", isSignal: true, isRequired: false, transformFunction: null } }, host: { styleAttribute: "display: none" }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
692
+ }
693
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeHeaderFilterComponent, decorators: [{
694
+ type: Component,
695
+ args: [{
696
+ selector: 'nxe-header-filter',
697
+ standalone: true,
698
+ template: '',
699
+ host: { style: 'display: none' },
700
+ changeDetection: ChangeDetectionStrategy.OnPush,
701
+ encapsulation: ViewEncapsulation.None,
702
+ }]
703
+ }], propDecorators: { visible: [{ type: i0.Input, args: [{ isSignal: true, alias: "visible", required: false }] }], search: [{ type: i0.Input, args: [{ isSignal: true, alias: "search", required: false }] }], allowSearch: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowSearch", required: false }] }], dataSource: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataSource", required: false }] }] } });
704
+
545
705
  /* eslint-disable @typescript-eslint/no-explicit-any */
546
706
  /**
547
707
  * `nxe-column` — drop-in replacement for DevExtreme `dxi-column`.
@@ -557,7 +717,11 @@ class NxeColumnComponent {
557
717
  /** Bound row field (supports dotted paths and bound/ternary expressions). */
558
718
  dataField = input('', /* @ts-ignore */
559
719
  ...(ngDevMode ? [{ debugName: "dataField" }] : /* istanbul ignore next */ []));
560
- dataType = input('string', /* @ts-ignore */
720
+ /**
721
+ * Unset = dx: inferred from the loaded data (m_columns_controller.js:914-975), so a numeric
722
+ * column nobody typed still right-aligns and honours its `format`. Declaring it wins.
723
+ */
724
+ dataType = input(undefined, /* @ts-ignore */
561
725
  ...(ngDevMode ? [{ debugName: "dataType" }] : /* istanbul ignore next */ []));
562
726
  /** px width — number (`[width]="150"`) or numeric string (`width="200"`). */
563
727
  width = input(undefined, /* @ts-ignore */
@@ -643,11 +807,17 @@ class NxeColumnComponent {
643
807
  * its children, and flattening them here would collapse the very nesting the band expresses.
644
808
  */
645
809
  childColumns = contentChildren(NxeColumnComponent, { ...(ngDevMode ? { debugName: "childColumns" } : /* istanbul ignore next */ {}), descendants: false });
810
+ /**
811
+ * A `<nxe-header-filter>` declared inside this column, dx's per-column `headerFilter`
812
+ * config. Only its `dataSource` is read here; visibility and search stay grid-level.
813
+ */
814
+ headerFilterConfig = contentChild(NxeHeaderFilterComponent, /* @ts-ignore */
815
+ ...(ngDevMode ? [{ debugName: "headerFilterConfig" }] : /* istanbul ignore next */ []));
646
816
  /** dx `isBand`: a column that groups other columns under a shared header instead of a field. */
647
817
  isBand = computed(() => this.childColumns().length > 0, /* @ts-ignore */
648
818
  ...(ngDevMode ? [{ debugName: "isBand" }] : /* istanbul ignore next */ []));
649
819
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeColumnComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
650
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.1.5", type: NxeColumnComponent, isStandalone: true, selector: "nxe-column", inputs: { caption: { classPropertyName: "caption", publicName: "caption", isSignal: true, isRequired: false, transformFunction: null }, dataField: { classPropertyName: "dataField", publicName: "dataField", isSignal: true, isRequired: false, transformFunction: null }, dataType: { classPropertyName: "dataType", publicName: "dataType", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, minWidth: { classPropertyName: "minWidth", publicName: "minWidth", isSignal: true, isRequired: false, transformFunction: null }, alignment: { classPropertyName: "alignment", publicName: "alignment", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, customizeText: { classPropertyName: "customizeText", publicName: "customizeText", isSignal: true, isRequired: false, transformFunction: null }, cellTemplate: { classPropertyName: "cellTemplate", publicName: "cellTemplate", isSignal: true, isRequired: false, transformFunction: null }, lookup: { classPropertyName: "lookup", publicName: "lookup", isSignal: true, isRequired: false, transformFunction: null }, allowSorting: { classPropertyName: "allowSorting", publicName: "allowSorting", isSignal: true, isRequired: false, transformFunction: null }, allowGrouping: { classPropertyName: "allowGrouping", publicName: "allowGrouping", isSignal: true, isRequired: false, transformFunction: null }, allowResizing: { classPropertyName: "allowResizing", publicName: "allowResizing", isSignal: true, isRequired: false, transformFunction: null }, allowFiltering: { classPropertyName: "allowFiltering", publicName: "allowFiltering", isSignal: true, isRequired: false, transformFunction: null }, allowEditing: { classPropertyName: "allowEditing", publicName: "allowEditing", isSignal: true, isRequired: false, transformFunction: null }, allowExporting: { classPropertyName: "allowExporting", publicName: "allowExporting", isSignal: true, isRequired: false, transformFunction: null }, allowSearch: { classPropertyName: "allowSearch", publicName: "allowSearch", isSignal: true, isRequired: false, transformFunction: null }, allowHeaderFiltering: { classPropertyName: "allowHeaderFiltering", publicName: "allowHeaderFiltering", isSignal: true, isRequired: false, transformFunction: null }, visible: { classPropertyName: "visible", publicName: "visible", isSignal: true, isRequired: false, transformFunction: null }, sortOrder: { classPropertyName: "sortOrder", publicName: "sortOrder", isSignal: true, isRequired: false, transformFunction: null }, sortIndex: { classPropertyName: "sortIndex", publicName: "sortIndex", isSignal: true, isRequired: false, transformFunction: null }, groupIndex: { classPropertyName: "groupIndex", publicName: "groupIndex", isSignal: true, isRequired: false, transformFunction: null }, fixed: { classPropertyName: "fixed", publicName: "fixed", isSignal: true, isRequired: false, transformFunction: null }, filterValues: { classPropertyName: "filterValues", publicName: "filterValues", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, cssClass: { classPropertyName: "cssClass", publicName: "cssClass", isSignal: true, isRequired: false, transformFunction: null }, editCellTemplate: { classPropertyName: "editCellTemplate", publicName: "editCellTemplate", isSignal: true, isRequired: false, transformFunction: null }, groupCellTemplate: { classPropertyName: "groupCellTemplate", publicName: "groupCellTemplate", isSignal: true, isRequired: false, transformFunction: null } }, host: { styleAttribute: "display: none" }, queries: [{ propertyName: "childColumns", predicate: NxeColumnComponent, isSignal: true }], ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
820
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.1.5", type: NxeColumnComponent, isStandalone: true, selector: "nxe-column", inputs: { caption: { classPropertyName: "caption", publicName: "caption", isSignal: true, isRequired: false, transformFunction: null }, dataField: { classPropertyName: "dataField", publicName: "dataField", isSignal: true, isRequired: false, transformFunction: null }, dataType: { classPropertyName: "dataType", publicName: "dataType", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, minWidth: { classPropertyName: "minWidth", publicName: "minWidth", isSignal: true, isRequired: false, transformFunction: null }, alignment: { classPropertyName: "alignment", publicName: "alignment", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, customizeText: { classPropertyName: "customizeText", publicName: "customizeText", isSignal: true, isRequired: false, transformFunction: null }, cellTemplate: { classPropertyName: "cellTemplate", publicName: "cellTemplate", isSignal: true, isRequired: false, transformFunction: null }, lookup: { classPropertyName: "lookup", publicName: "lookup", isSignal: true, isRequired: false, transformFunction: null }, allowSorting: { classPropertyName: "allowSorting", publicName: "allowSorting", isSignal: true, isRequired: false, transformFunction: null }, allowGrouping: { classPropertyName: "allowGrouping", publicName: "allowGrouping", isSignal: true, isRequired: false, transformFunction: null }, allowResizing: { classPropertyName: "allowResizing", publicName: "allowResizing", isSignal: true, isRequired: false, transformFunction: null }, allowFiltering: { classPropertyName: "allowFiltering", publicName: "allowFiltering", isSignal: true, isRequired: false, transformFunction: null }, allowEditing: { classPropertyName: "allowEditing", publicName: "allowEditing", isSignal: true, isRequired: false, transformFunction: null }, allowExporting: { classPropertyName: "allowExporting", publicName: "allowExporting", isSignal: true, isRequired: false, transformFunction: null }, allowSearch: { classPropertyName: "allowSearch", publicName: "allowSearch", isSignal: true, isRequired: false, transformFunction: null }, allowHeaderFiltering: { classPropertyName: "allowHeaderFiltering", publicName: "allowHeaderFiltering", isSignal: true, isRequired: false, transformFunction: null }, visible: { classPropertyName: "visible", publicName: "visible", isSignal: true, isRequired: false, transformFunction: null }, sortOrder: { classPropertyName: "sortOrder", publicName: "sortOrder", isSignal: true, isRequired: false, transformFunction: null }, sortIndex: { classPropertyName: "sortIndex", publicName: "sortIndex", isSignal: true, isRequired: false, transformFunction: null }, groupIndex: { classPropertyName: "groupIndex", publicName: "groupIndex", isSignal: true, isRequired: false, transformFunction: null }, fixed: { classPropertyName: "fixed", publicName: "fixed", isSignal: true, isRequired: false, transformFunction: null }, filterValues: { classPropertyName: "filterValues", publicName: "filterValues", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, cssClass: { classPropertyName: "cssClass", publicName: "cssClass", isSignal: true, isRequired: false, transformFunction: null }, editCellTemplate: { classPropertyName: "editCellTemplate", publicName: "editCellTemplate", isSignal: true, isRequired: false, transformFunction: null }, groupCellTemplate: { classPropertyName: "groupCellTemplate", publicName: "groupCellTemplate", isSignal: true, isRequired: false, transformFunction: null } }, host: { styleAttribute: "display: none" }, queries: [{ propertyName: "childColumns", predicate: NxeColumnComponent, isSignal: true }, { propertyName: "headerFilterConfig", first: true, predicate: NxeHeaderFilterComponent, descendants: true, isSignal: true }], ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
651
821
  }
652
822
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeColumnComponent, decorators: [{
653
823
  type: Component,
@@ -659,7 +829,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
659
829
  changeDetection: ChangeDetectionStrategy.OnPush,
660
830
  encapsulation: ViewEncapsulation.None,
661
831
  }]
662
- }], propDecorators: { caption: [{ type: i0.Input, args: [{ isSignal: true, alias: "caption", required: false }] }], dataField: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataField", required: false }] }], dataType: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataType", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], minWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "minWidth", required: false }] }], alignment: [{ type: i0.Input, args: [{ isSignal: true, alias: "alignment", required: false }] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], customizeText: [{ type: i0.Input, args: [{ isSignal: true, alias: "customizeText", required: false }] }], cellTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "cellTemplate", required: false }] }], lookup: [{ type: i0.Input, args: [{ isSignal: true, alias: "lookup", required: false }] }], allowSorting: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowSorting", required: false }] }], allowGrouping: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowGrouping", required: false }] }], allowResizing: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowResizing", required: false }] }], allowFiltering: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowFiltering", required: false }] }], allowEditing: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowEditing", required: false }] }], allowExporting: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowExporting", required: false }] }], allowSearch: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowSearch", required: false }] }], allowHeaderFiltering: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowHeaderFiltering", required: false }] }], visible: [{ type: i0.Input, args: [{ isSignal: true, alias: "visible", required: false }] }], sortOrder: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortOrder", required: false }] }], sortIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortIndex", required: false }] }], groupIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupIndex", required: false }] }], fixed: [{ type: i0.Input, args: [{ isSignal: true, alias: "fixed", required: false }] }], filterValues: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterValues", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], cssClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "cssClass", required: false }] }], editCellTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "editCellTemplate", required: false }] }], groupCellTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupCellTemplate", required: false }] }], childColumns: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => NxeColumnComponent), { ...{ descendants: false }, isSignal: true }] }] } });
832
+ }], propDecorators: { caption: [{ type: i0.Input, args: [{ isSignal: true, alias: "caption", required: false }] }], dataField: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataField", required: false }] }], dataType: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataType", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], minWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "minWidth", required: false }] }], alignment: [{ type: i0.Input, args: [{ isSignal: true, alias: "alignment", required: false }] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], customizeText: [{ type: i0.Input, args: [{ isSignal: true, alias: "customizeText", required: false }] }], cellTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "cellTemplate", required: false }] }], lookup: [{ type: i0.Input, args: [{ isSignal: true, alias: "lookup", required: false }] }], allowSorting: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowSorting", required: false }] }], allowGrouping: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowGrouping", required: false }] }], allowResizing: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowResizing", required: false }] }], allowFiltering: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowFiltering", required: false }] }], allowEditing: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowEditing", required: false }] }], allowExporting: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowExporting", required: false }] }], allowSearch: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowSearch", required: false }] }], allowHeaderFiltering: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowHeaderFiltering", required: false }] }], visible: [{ type: i0.Input, args: [{ isSignal: true, alias: "visible", required: false }] }], sortOrder: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortOrder", required: false }] }], sortIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortIndex", required: false }] }], groupIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupIndex", required: false }] }], fixed: [{ type: i0.Input, args: [{ isSignal: true, alias: "fixed", required: false }] }], filterValues: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterValues", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], cssClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "cssClass", required: false }] }], editCellTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "editCellTemplate", required: false }] }], groupCellTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupCellTemplate", required: false }] }], childColumns: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => NxeColumnComponent), { ...{ descendants: false }, isSignal: true }] }], headerFilterConfig: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NxeHeaderFilterComponent), { isSignal: true }] }] } });
663
833
 
664
834
  /**
665
835
  * `nxe-selection` — drop-in replacement for DevExtreme `dxo-selection` (grid child option).
@@ -697,46 +867,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
697
867
  }]
698
868
  }], propDecorators: { mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], selectAllMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectAllMode", required: false }] }], showCheckBoxesMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCheckBoxesMode", required: false }] }], recursive: [{ type: i0.Input, args: [{ isSignal: true, alias: "recursive", required: false }] }], selectByClick: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectByClick", required: false }] }] } });
699
869
 
700
- /**
701
- * `nxe-header-filter` — drop-in replacement for DevExtreme `dxo-header-filter` (grid child option).
702
- *
703
- * Adds a funnel icon to each filterable column header; clicking it opens a popup of the column's
704
- * distinct values (checkbox list + optional search) to filter the grid. The app uses essentially
705
- * one shape: `[search]="{ enabled: true }" [visible]="true"` (60 files). Legacy `[allowSearch]`
706
- * (1×) folds into `search.enabled`.
707
- */
708
- class NxeHeaderFilterComponent {
709
- visible = input(true, /* @ts-ignore */
710
- ...(ngDevMode ? [{ debugName: "visible" }] : /* istanbul ignore next */ []));
711
- search = input(undefined, /* @ts-ignore */
712
- ...(ngDevMode ? [{ debugName: "search" }] : /* istanbul ignore next */ []));
713
- /** Legacy alias for `search.enabled` (one holdout). */
714
- allowSearch = input(undefined, /* @ts-ignore */
715
- ...(ngDevMode ? [{ debugName: "allowSearch" }] : /* istanbul ignore next */ []));
716
- /** Whether the in-popup search box shows.
717
- *
718
- * Defaults to **true**, matching the legacy DevExtreme deployment: a popup listing every
719
- * distinct value in a column is close to unusable without one. 60 of the app's templates
720
- * already opt in explicitly, so this only changes the handful that never said either way —
721
- * and an explicit `[search]="{ enabled: false }"` still turns it off. */
722
- searchEnabled() {
723
- return this.search()?.enabled ?? this.allowSearch() ?? true;
724
- }
725
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeHeaderFilterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
726
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.5", type: NxeHeaderFilterComponent, isStandalone: true, selector: "nxe-header-filter", inputs: { visible: { classPropertyName: "visible", publicName: "visible", isSignal: true, isRequired: false, transformFunction: null }, search: { classPropertyName: "search", publicName: "search", isSignal: true, isRequired: false, transformFunction: null }, allowSearch: { classPropertyName: "allowSearch", publicName: "allowSearch", isSignal: true, isRequired: false, transformFunction: null } }, host: { styleAttribute: "display: none" }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
727
- }
728
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeHeaderFilterComponent, decorators: [{
729
- type: Component,
730
- args: [{
731
- selector: 'nxe-header-filter',
732
- standalone: true,
733
- template: '',
734
- host: { style: 'display: none' },
735
- changeDetection: ChangeDetectionStrategy.OnPush,
736
- encapsulation: ViewEncapsulation.None,
737
- }]
738
- }], propDecorators: { visible: [{ type: i0.Input, args: [{ isSignal: true, alias: "visible", required: false }] }], search: [{ type: i0.Input, args: [{ isSignal: true, alias: "search", required: false }] }], allowSearch: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowSearch", required: false }] }] } });
739
-
740
870
  /* eslint-disable @typescript-eslint/no-explicit-any */
741
871
  /**
742
872
  * Header-filter state model — the draft/apply/search machinery extracted from
@@ -749,6 +879,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
749
879
  * {@link NxeHeaderFilterSource} adapter, so it owns only the frozen semantics:
750
880
  * `(Blanks)` first, Yes/No boolean buckets, the date Year > Month > Day tree, dx search
751
881
  * scoping of (Select All), check-on-open, and "everything checked = no filter" on apply.
882
+ * (Select All)'s indeterminate state ({@link NxeHeaderFilterModel.someChecked}) is derived
883
+ * from the same draft and search scope, and changes none of these rules.
752
884
  */
753
885
  /**
754
886
  * Header-filter key for blank-ish values (`null`/`undefined`/`''`) — dx lists them as one
@@ -769,12 +901,18 @@ function nxeHfKey(raw, isDate) {
769
901
  }
770
902
  return raw;
771
903
  }
772
- class NxeHeaderFilterModel {
773
- source;
774
- static MONTH_NAMES = [
904
+ const NXE_HF_ENGLISH_LABELS = {
905
+ blanks: () => '(Blanks)',
906
+ yes: () => 'Yes',
907
+ no: () => 'No',
908
+ month: (month) => [
775
909
  'January', 'February', 'March', 'April', 'May', 'June',
776
910
  'July', 'August', 'September', 'October', 'November', 'December',
777
- ];
911
+ ][month - 1],
912
+ };
913
+ class NxeHeaderFilterModel {
914
+ source;
915
+ labels;
778
916
  /** The column whose popup is open (`null` = closed). */
779
917
  column = signal(null, /* @ts-ignore */
780
918
  ...(ngDevMode ? [{ debugName: "column" }] : /* istanbul ignore next */ []));
@@ -785,8 +923,13 @@ class NxeHeaderFilterModel {
785
923
  ...(ngDevMode ? [{ debugName: "search" }] : /* istanbul ignore next */ []));
786
924
  expanded = signal(new Set(), /* @ts-ignore */
787
925
  ...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
788
- constructor(source) {
926
+ constructor(source, labels = NXE_HF_ENGLISH_LABELS) {
789
927
  this.source = source;
928
+ this.labels = labels;
929
+ }
930
+ /** The popup's wording for a boolean bucket. */
931
+ boolText(label) {
932
+ return label === null ? null : label === 'Yes' ? this.labels.yes() : this.labels.no();
790
933
  }
791
934
  /** This column's header-filter key for a raw value. */
792
935
  key(column, raw) {
@@ -798,6 +941,10 @@ class NxeHeaderFilterModel {
798
941
  if (!col) {
799
942
  return [];
800
943
  }
944
+ const fixed = this.source.fixedItems?.(col);
945
+ if (fixed) {
946
+ return fixed;
947
+ }
801
948
  const seen = new Map();
802
949
  for (const row of this.source.rows()) {
803
950
  const raw = this.source.rawValue(col, row);
@@ -807,8 +954,8 @@ class NxeHeaderFilterModel {
807
954
  key,
808
955
  raw,
809
956
  text: key === NXE_HF_BLANKS
810
- ? '(Blanks)'
811
- : (this.source.boolLabel(col, raw) ?? this.source.displayText(col, row)),
957
+ ? this.labels.blanks()
958
+ : (this.boolText(this.source.boolLabel(col, raw)) ?? this.source.displayText(col, row)),
812
959
  });
813
960
  }
814
961
  }
@@ -866,7 +1013,7 @@ class NxeHeaderFilterModel {
866
1013
  }
867
1014
  const out = [];
868
1015
  if (hasBlanks) {
869
- out.push({ label: '(Blanks)', key: 'blanks', children: [], leafKeys: [NXE_HF_BLANKS] });
1016
+ out.push({ label: this.labels.blanks(), key: 'blanks', children: [], leafKeys: [NXE_HF_BLANKS] });
870
1017
  }
871
1018
  const byYear = new Map();
872
1019
  for (const day of [...days].sort()) {
@@ -885,7 +1032,7 @@ class NxeHeaderFilterModel {
885
1032
  const monthNodes = [];
886
1033
  for (const [month, dayKeys] of months) {
887
1034
  monthNodes.push({
888
- label: NxeHeaderFilterModel.MONTH_NAMES[month - 1],
1035
+ label: this.labels.month(month),
889
1036
  key: `m:${year}-${month}`,
890
1037
  children: dayKeys.map((d) => ({
891
1038
  label: String(Number(d.slice(8, 10))),
@@ -979,6 +1126,20 @@ class NxeHeaderFilterModel {
979
1126
  return all.length > 0 && all.every((k) => draft.has(k));
980
1127
  }, /* @ts-ignore */
981
1128
  ...(ngDevMode ? [{ debugName: "allChecked" }] : /* istanbul ignore next */ []));
1129
+ /**
1130
+ * (Select All)'s third state: some visible keys checked, but not all. dx shows it as the
1131
+ * indeterminate square (the select-all dxCheckBox's `value: undefined`). It is never true
1132
+ * together with {@link allChecked}, so check-on-open, which checks everything when no filter
1133
+ * is applied, always opens on a fully checked box and never on a half-checked one.
1134
+ */
1135
+ someChecked = computed(() => {
1136
+ if (this.allChecked()) {
1137
+ return false;
1138
+ }
1139
+ const draft = this.draft();
1140
+ return this.visibleKeys().some((k) => draft.has(k));
1141
+ }, /* @ts-ignore */
1142
+ ...(ngDevMode ? [{ debugName: "someChecked" }] : /* istanbul ignore next */ []));
982
1143
  /** Whether every key of the whole column is checked — decides "no filter" on apply. */
983
1144
  fullChecked = computed(() => {
984
1145
  const all = this.allKeys();
@@ -1033,14 +1194,18 @@ class NxeHeaderFilterModel {
1033
1194
  * The header-filter popup panel — the `.nxe-hf-*` chunk extracted from `nxe-data-grid`'s
1034
1195
  * template (pivot parity Phase 3a) so the pivot can open the same popup on pages with no
1035
1196
  * data grid. Same DOM, same FROZEN classes (`.nxe-hf-item`/`.nxe-hf-search`/… — DEFAULTS.md):
1036
- * the host element carries `dx-header-filter-menu nxe-hf-panel`, exactly where the old
1037
- * template's root `<div>` sat inside the overlay pane.
1197
+ * the host element carries `nxe-header-filter-menu nxe-hf-panel`, exactly where the old
1198
+ * template's root `<div>` sat inside the overlay pane. The dx-parity pass only added to that DOM:
1199
+ * the `input.nxe-hf-search` now sits in a `.nxe-hf-searchbox` with its magnifier and clear
1200
+ * button, and rows also carry `.nxe-hf-list-item` / `.nxe-hf-tree-item`.
1038
1201
  *
1039
1202
  * Renders whatever {@link NxeHeaderFilterModel} it is given — checkbox list or the date
1040
1203
  * Year > Month > Day tree — and emits `applied`/`cancelled` for the host to commit/close;
1041
1204
  * the panel itself never touches the host's filter state.
1042
1205
  */
1043
1206
  class NxeHeaderFilterPanelComponent {
1207
+ /** The library's runtime-switchable messages. */
1208
+ messages = inject(NxeMessageService);
1044
1209
  model = input.required(/* @ts-ignore */
1045
1210
  ...(ngDevMode ? [{ debugName: "model" }] : /* istanbul ignore next */ []));
1046
1211
  /** Live predicate (re-read every CD, like the grid template did) — a mid-open config
@@ -1051,16 +1216,24 @@ class NxeHeaderFilterPanelComponent {
1051
1216
  applied = output();
1052
1217
  /** Cancel — the host closes without committing. */
1053
1218
  cancelled = output();
1219
+ searchInput = viewChild('searchInput', /* @ts-ignore */
1220
+ ...(ngDevMode ? [{ debugName: "searchInput" }] : /* istanbul ignore next */ []));
1054
1221
  showSearch() {
1055
1222
  return this.searchEnabled()();
1056
1223
  }
1224
+ /** The search box's clear button (dx search mode forces `showClearButton`): empties the
1225
+ search and keeps the caret in the box, as dx's clear does. */
1226
+ clearSearch() {
1227
+ this.model().search.set('');
1228
+ this.searchInput()?.nativeElement.focus();
1229
+ }
1057
1230
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeHeaderFilterPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1058
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeHeaderFilterPanelComponent, isStandalone: true, selector: "nxe-header-filter-panel", inputs: { model: { classPropertyName: "model", publicName: "model", isSignal: true, isRequired: true, transformFunction: null }, searchEnabled: { classPropertyName: "searchEnabled", publicName: "searchEnabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { applied: "applied", cancelled: "cancelled" }, host: { classAttribute: "dx-header-filter-menu nxe-hf-panel" }, ngImport: i0, template: "@if (showSearch()) {\r\n <input\r\n class=\"nxe-hf-search\"\r\n type=\"text\"\r\n placeholder=\"Search\"\r\n [value]=\"model().search()\"\r\n (input)=\"model().search.set($any($event.target).value)\" />\r\n}\r\n<div class=\"nxe-hf-list\" [class.nxe-hf-tree]=\"model().isDate()\">\r\n <label class=\"nxe-hf-item nxe-hf-selectall\">\r\n <input type=\"checkbox\" [checked]=\"model().allChecked()\" (change)=\"model().toggleAll()\" />\r\n <span>Select All</span>\r\n </label>\r\n @if (model().isDate()) {\r\n <!-- dx date header filter: Year > Month > Day checkbox tree -->\r\n <ng-container\r\n [ngTemplateOutlet]=\"hfTreeNodes\"\r\n [ngTemplateOutletContext]=\"{ $implicit: model().dateTreeFiltered() }\" />\r\n } @else {\r\n @for (item of model().distinctFiltered(); track item.key) {\r\n <label class=\"nxe-hf-item\">\r\n <input\r\n type=\"checkbox\"\r\n [checked]=\"model().draft().has(item.key)\"\r\n (change)=\"model().toggleValue(item.key)\" />\r\n <span>{{ item.text }}</span>\r\n </label>\r\n }\r\n }\r\n</div>\r\n<div class=\"nxe-hf-actions\">\r\n <button type=\"button\" class=\"nxe-hf-btn nxe-hf-ok\" (click)=\"applied.emit()\">OK</button>\r\n <button type=\"button\" class=\"nxe-hf-btn\" (click)=\"cancelled.emit()\">Cancel</button>\r\n</div>\r\n\r\n<!-- Recursive date-tree nodes for the header-filter popup -->\r\n<ng-template #hfTreeNodes let-nodes>\r\n @for (n of nodes; track n.key) {\r\n <div class=\"nxe-hf-tree-node\">\r\n @if (n.children.length) {\r\n <span\r\n class=\"nxe-hf-tree-toggle\"\r\n [class.nxe-expanded]=\"model().nodeExpanded(n.key)\"\r\n (click)=\"model().toggleExpand(n.key)\"></span>\r\n } @else {\r\n <span class=\"nxe-hf-tree-toggle-stub\"></span>\r\n }\r\n <label class=\"nxe-hf-item\">\r\n <input\r\n type=\"checkbox\"\r\n [checked]=\"model().nodeChecked(n)\"\r\n [indeterminate]=\"model().nodeIndeterminate(n)\"\r\n (change)=\"model().toggleNode(n)\" />\r\n <span>{{ n.label }}</span>\r\n </label>\r\n @if (n.children.length && model().nodeExpanded(n.key)) {\r\n <div class=\"nxe-hf-tree-children\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"hfTreeNodes\"\r\n [ngTemplateOutletContext]=\"{ $implicit: n.children }\" />\r\n </div>\r\n }\r\n </div>\r\n }\r\n</ng-template>\r\n", styles: ["@charset \"UTF-8\";.nxe-hf-panel{display:flex;flex-direction:column;min-width:200px;max-width:320px;background:#fff;border:1px solid #ddd;border-radius:4px;box-shadow:0 6px 12px #0000002d;color:#333}.nxe-hf-panel .nxe-hf-search{margin:6px;padding:5px 8px;border:1px solid #ddd;border-radius:3px;font:inherit;outline:none}.nxe-hf-panel .nxe-hf-search:focus{border-color:#30374c}.nxe-hf-panel .nxe-hf-list{max-height:260px;overflow-y:auto;padding:2px 0}.nxe-hf-panel .nxe-hf-item{display:flex;align-items:center;gap:8px;padding:10px;cursor:pointer}.nxe-hf-panel .nxe-hf-item:hover{background:#f4f4f4}.nxe-hf-panel .nxe-hf-item span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.nxe-hf-panel .nxe-hf-selectall{padding:12px 10px 8px;border-bottom:1px solid #ddd}.nxe-hf-panel .nxe-hf-actions{display:flex;justify-content:flex-end;gap:6px;padding:6px;border-top:1px solid #eee}.nxe-hf-panel .nxe-hf-btn{min-width:85px;padding:8px 12px;border:1px solid #ddd;border-radius:4px;background:transparent;color:inherit;cursor:pointer;font:inherit}.nxe-hf-panel .nxe-hf-btn:hover{background:#0000000a}.nxe-hf-panel input[type=checkbox]{appearance:none;-webkit-appearance:none;flex:0 0 auto;position:relative;box-sizing:border-box;width:22px;height:22px;margin:0;border:1px solid #ddd;border-radius:2px;background:#fff;cursor:pointer}.nxe-hf-panel input[type=checkbox]:checked:after{content:\"\";position:absolute;left:7px;top:3px;width:6px;height:12px;border:solid #337ab7;border-width:0 2px 2px 0;transform:rotate(45deg)}.nxe-hf-panel input[type=checkbox]:indeterminate:after{content:\"\";position:absolute;left:4px;top:9px;width:12px;height:2px;background:#337ab7}.nxe-hf-panel input[type=checkbox]:hover{border-color:#265a87}.nxe-hf-panel input[type=checkbox]:focus-visible{outline:none;border-color:#337ab7}.nxe-hf-tree-node{display:block;white-space:nowrap}.nxe-hf-tree-children{padding-left:18px}.nxe-hf-tree-node .nxe-hf-item{display:inline-flex}.nxe-hf-tree-toggle{display:inline-block;position:relative;width:14px;height:14px;cursor:pointer;vertical-align:middle}.nxe-hf-tree-toggle:before{content:\"\";position:absolute;left:4px;top:4px;width:5px;height:5px;border-right:1.5px solid #555;border-bottom:1.5px solid #555;transform:rotate(-45deg)}.nxe-hf-tree-toggle.nxe-expanded:before{transform:rotate(45deg)}.nxe-hf-tree-toggle-stub{display:inline-block;width:14px}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1231
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeHeaderFilterPanelComponent, isStandalone: true, selector: "nxe-header-filter-panel", inputs: { model: { classPropertyName: "model", publicName: "model", isSignal: true, isRequired: true, transformFunction: null }, searchEnabled: { classPropertyName: "searchEnabled", publicName: "searchEnabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { applied: "applied", cancelled: "cancelled" }, host: { classAttribute: "nxe-header-filter-menu nxe-hf-panel" }, viewQueries: [{ propertyName: "searchInput", first: true, predicate: ["searchInput"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (showSearch()) {\r\n <!-- dx builds this as a dxTextBox in mode 'search' (the list/tree searchEnabled option), which\r\n forces showClearButton: a magnifier in a 34px slot inside the left border, and a clear\r\n button in a 34px area inside the right border that only exists while there is text to\r\n clear. Both glyphs are drawn here from primitives; the DXIcons font is not used. -->\r\n <div class=\"nxe-hf-searchbox\">\r\n <!-- The magnifier both search boxes share (dx draws them with one rule,\r\n .dx-searchbox .dx-icon-search): a r4.75 ring and a 45deg handle, 1.9px strokes. Keep\r\n it identical to the tree view's (nxe-tree-view.component.html). -->\r\n <svg class=\"nxe-hf-search-icon\" viewBox=\"0 0 34 34\" aria-hidden=\"true\" focusable=\"false\">\r\n <circle cx=\"15\" cy=\"15\" r=\"4.75\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.9\" />\r\n <path d=\"M18.5 18.5 23.5 23.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.9\" />\r\n </svg>\r\n <input\r\n #searchInput\r\n class=\"nxe-hf-search\"\r\n type=\"text\"\r\n [attr.placeholder]=\"messages.t('common.search')\"\r\n [attr.aria-label]=\"messages.t('common.search')\"\r\n [value]=\"model().search()\"\r\n (input)=\"model().search.set($any($event.target).value)\" />\r\n @if (model().search()) {\r\n <button\r\n class=\"nxe-hf-search-clear\"\r\n type=\"button\"\r\n tabindex=\"-1\"\r\n [attr.aria-label]=\"messages.t('common.clear')\"\r\n (mousedown)=\"$event.preventDefault()\"\r\n (click)=\"clearSearch()\">\r\n <!-- The editors' clear glyph: a 16px disc with a 2px x cut out, two 2px bars\r\n each running corner to corner of the 8px square centred on the disc, drawn as\r\n a plus and turned 45deg. -->\r\n <svg viewBox=\"0 0 18 18\" aria-hidden=\"true\" focusable=\"false\">\r\n <path\r\n fill=\"currentColor\"\r\n fill-rule=\"evenodd\"\r\n transform=\"rotate(45 9 9)\"\r\n d=\"M9 1a8 8 0 1 1 0 16A8 8 0 1 1 9 1ZM8 3.343h2V8h4.657v2H10v4.657H8V10H3.343V8H8Z\" />\r\n </svg>\r\n </button>\r\n }\r\n </div>\r\n}\r\n<div class=\"nxe-hf-list\" [class.nxe-hf-tree]=\"model().isDate()\">\r\n <label class=\"nxe-hf-item nxe-hf-selectall\">\r\n <input\r\n type=\"checkbox\"\r\n [checked]=\"model().allChecked()\"\r\n [indeterminate]=\"model().someChecked()\"\r\n (change)=\"model().toggleAll()\" />\r\n <span>{{ messages.t('common.selectAll') }}</span>\r\n </label>\r\n @if (model().isDate()) {\r\n <!-- dx date header filter: Year > Month > Day checkbox tree -->\r\n <ng-container\r\n [ngTemplateOutlet]=\"hfTreeNodes\"\r\n [ngTemplateOutletContext]=\"{ $implicit: model().dateTreeFiltered() }\" />\r\n } @else {\r\n @for (item of model().distinctFiltered(); track item.key) {\r\n <label class=\"nxe-hf-item nxe-hf-list-item\">\r\n <input\r\n type=\"checkbox\"\r\n [checked]=\"model().draft().has(item.key)\"\r\n (change)=\"model().toggleValue(item.key)\" />\r\n <span>{{ item.text }}</span>\r\n </label>\r\n }\r\n }\r\n</div>\r\n<div class=\"nxe-hf-actions\">\r\n <button type=\"button\" class=\"nxe-hf-btn nxe-hf-ok\" (click)=\"applied.emit()\">{{ messages.t('common.ok') }}</button>\r\n <button type=\"button\" class=\"nxe-hf-btn\" (click)=\"cancelled.emit()\">{{ messages.t('common.cancel') }}</button>\r\n</div>\r\n\r\n<!-- Recursive date-tree nodes for the header-filter popup. Each node's first grid row is dx's\r\n .dx-treeview-item: the toggle (or its stub) and the label share it, the label spanning the\r\n whole row so its hover paints the full row, as dx's does. -->\r\n<ng-template #hfTreeNodes let-nodes>\r\n @for (n of nodes; track n.key) {\r\n <div class=\"nxe-hf-tree-node\">\r\n @if (n.children.length) {\r\n <span\r\n class=\"nxe-hf-tree-toggle\"\r\n [class.nxe-expanded]=\"model().nodeExpanded(n.key)\"\r\n (click)=\"model().toggleExpand(n.key)\"></span>\r\n } @else {\r\n <span class=\"nxe-hf-tree-toggle-stub\"></span>\r\n }\r\n <label class=\"nxe-hf-item nxe-hf-tree-item\">\r\n <input\r\n type=\"checkbox\"\r\n [checked]=\"model().nodeChecked(n)\"\r\n [indeterminate]=\"model().nodeIndeterminate(n)\"\r\n (change)=\"model().toggleNode(n)\" />\r\n <span>{{ n.label }}</span>\r\n </label>\r\n @if (n.children.length && model().nodeExpanded(n.key)) {\r\n <div class=\"nxe-hf-tree-children\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"hfTreeNodes\"\r\n [ngTemplateOutletContext]=\"{ $implicit: n.children }\" />\r\n </div>\r\n }\r\n </div>\r\n }\r\n</ng-template>\r\n", styles: ["@charset \"UTF-8\";.nxe-hf-panel{display:flex;flex-direction:column;box-sizing:border-box;width:252px;height:325px;min-width:var(--nxe-ui-header-filter-min-width, 0px);padding:20px 0 0;background:var(--nxe-ui-surface, #fff);border:1px solid var(--nxe-ui-border, #ddd);border-radius:var(--nxe-ui-popup-radius, 6px);box-shadow:var(--nxe-ui-popup-shadow, 0 6px 12px rgba(0, 0, 0, .175));color:var(--nxe-ui-text, #333)}.nxe-hf-panel .nxe-hf-searchbox{position:relative;display:flex;flex:0 0 auto;box-sizing:border-box;margin:0 20px 5px;padding:0;border:1px solid var(--nxe-ui-border, #ddd);border-radius:var(--nxe-ui-radius, 4px);background:var(--nxe-ui-surface, #fff)}.nxe-hf-panel .nxe-hf-searchbox:hover{border-color:var(--nxe-ui-accent-faded, rgba(51, 122, 183, .4))}.nxe-hf-panel .nxe-hf-searchbox:focus-within{border-color:var(--nxe-ui-accent, #337ab7)}.nxe-hf-panel .nxe-hf-search{-webkit-appearance:none;appearance:none;flex:1 1 auto;min-width:0;width:100%;box-sizing:border-box;min-height:34px;margin:0;padding:7px 9px 8px;padding-inline-start:34px;border:0;border-radius:var(--nxe-ui-radius, 4px);outline:0;background:transparent;color:var(--nxe-ui-text, #333);font:inherit}.nxe-hf-panel .nxe-hf-search::placeholder{color:var(--nxe-ui-text-muted, #999);opacity:1}.nxe-hf-panel .nxe-hf-search-icon{position:absolute;top:50%;left:0;display:block;box-sizing:border-box;width:34px;height:34px;margin-top:-17px;color:var(--nxe-ui-text-muted, #999);pointer-events:none}.nxe-hf-panel .nxe-hf-search-clear{position:relative;display:flex;align-items:center;justify-content:center;flex:0 0 34px;box-sizing:border-box;width:34px;min-width:34px;margin:0;padding:0;border:0;background:transparent;color:var(--nxe-ui-text-muted, #999);cursor:pointer}.nxe-hf-panel .nxe-hf-search-clear svg{display:block;width:18px;height:18px}.nxe-hf-panel .nxe-hf-list{flex:1 1 auto;min-height:0;margin:0 20px 20px;padding:0;overflow-x:hidden;overflow-y:auto}.nxe-hf-panel .nxe-hf-item{box-sizing:border-box;margin:0;cursor:pointer}.nxe-hf-panel .nxe-hf-item>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.nxe-hf-panel .nxe-hf-list-item{display:flex;align-items:center;padding:0}.nxe-hf-panel .nxe-hf-list-item>input[type=checkbox]{margin:-1px 0 -3px;margin-inline:10px -1px}.nxe-hf-panel .nxe-hf-list-item>span{flex:1 1 auto;min-width:0;padding:10px}.nxe-hf-panel .nxe-hf-list-item+.nxe-hf-list-item{border-top:1px solid var(--nxe-ui-border, #ddd)}.nxe-hf-panel .nxe-hf-selectall{display:block;padding:9px 0 8px;border-bottom:1px solid var(--nxe-ui-border, #ddd);white-space:nowrap}.nxe-hf-panel .nxe-hf-selectall>input[type=checkbox]{float:inline-start;margin:-1px 0 -3px;margin-inline:10px 0}.nxe-hf-panel .nxe-hf-selectall>span{display:inline-block;box-sizing:border-box;max-width:calc(100% - 32px);margin-top:1px;padding:0 10px;line-height:17px;vertical-align:baseline}.nxe-hf-panel .nxe-hf-list-item:hover>input[type=checkbox]:not(:focus-visible),.nxe-hf-panel .nxe-hf-selectall:hover>input[type=checkbox]:not(:focus-visible){border-color:var(--nxe-ui-checkbox-hover-border, #265a87)}.nxe-hf-panel .nxe-hf-tree>.nxe-hf-selectall{display:flex;align-items:center;padding:9px 40px 11px}.nxe-hf-panel .nxe-hf-tree>.nxe-hf-selectall>input[type=checkbox]{float:none;margin:0}.nxe-hf-panel .nxe-hf-tree>.nxe-hf-selectall>span{display:block;flex:0 1 auto;min-width:0;max-width:none;margin:0;padding:0;padding-inline-start:8px;line-height:inherit}.nxe-hf-panel .nxe-hf-tree-node{display:grid;grid-template-columns:32px minmax(0,1fr);align-items:center;white-space:nowrap}.nxe-hf-panel .nxe-hf-tree-toggle,.nxe-hf-panel .nxe-hf-tree-toggle-stub{grid-area:1/1;justify-self:end;position:relative;display:block;box-sizing:border-box;width:22px;height:22px}.nxe-hf-panel .nxe-hf-tree-toggle{color:var(--nxe-ui-text, #333);cursor:pointer}.nxe-hf-panel .nxe-hf-tree-toggle:before{content:\"\";position:absolute;left:50%;top:50%;box-sizing:border-box;width:0;height:0;margin:-4px 0 0 -2px;border-style:solid;border-width:4px 0 4px 4px;border-color:transparent transparent transparent currentColor;transform:none}.nxe-hf-panel .nxe-hf-tree-toggle.nxe-expanded:before{margin:-2px 0 0 -4px;border-width:4px 4px 0;border-color:currentColor transparent transparent}.nxe-hf-panel .nxe-hf-tree-toggle-stub{pointer-events:none}.nxe-hf-panel .nxe-hf-tree-item{grid-area:1/1/2/3;display:flex;align-items:center;gap:8px;padding:5px 10px;padding-inline-start:40px;pointer-events:none}.nxe-hf-panel .nxe-hf-tree-item>input[type=checkbox],.nxe-hf-panel .nxe-hf-tree-item>span{pointer-events:auto}.nxe-hf-panel .nxe-hf-tree-item>span{flex:0 1 auto;min-width:0}.nxe-hf-panel .nxe-hf-tree-children{grid-area:2/1/3/3;padding-inline-start:15px}.nxe-hf-panel .nxe-hf-tree-node:hover:not(:has(>.nxe-hf-tree-children:hover))>.nxe-hf-tree-item{background-color:var(--nxe-ui-hover-bg, rgba(0, 0, 0, .04));color:var(--nxe-ui-text, #333)}.nxe-hf-panel .nxe-hf-actions{display:flex;flex:0 0 auto;align-items:center;justify-content:flex-end;gap:10px;box-sizing:content-box;height:36px;padding:20px}.nxe-hf-panel .nxe-hf-btn{box-sizing:border-box;min-width:100px;padding:8px 12px;border:1px solid var(--nxe-ui-border, #ddd);border-radius:var(--nxe-ui-radius, 4px);background:transparent;color:inherit;cursor:pointer;font:inherit;line-height:1.2858}.nxe-hf-panel .nxe-hf-btn:hover{background:var(--nxe-ui-hover-bg, rgba(0, 0, 0, .04))}.nxe-hf-panel input[type=checkbox]{appearance:none;-webkit-appearance:none;flex:0 0 auto;position:relative;box-sizing:border-box;width:22px;height:22px;margin:0;border:1px solid var(--nxe-ui-border, #ddd);border-radius:2px;background:var(--nxe-ui-surface, #fff);color:var(--nxe-ui-accent, #337ab7);cursor:pointer}.nxe-hf-panel input[type=checkbox]:checked:not(:indeterminate):after{content:\"\";position:absolute;left:50%;top:50%;box-sizing:border-box;width:7.5px;height:13.25px;margin:-8.1px 0 0 -3.75px;border:solid currentColor;border-width:0 2px 2px 0;border-radius:0;background:none;transform:rotate(45deg)}.nxe-hf-panel input[type=checkbox]:indeterminate:after{content:\"\";position:absolute;left:50%;top:50%;box-sizing:border-box;width:12px;height:12px;margin:-6px 0 0 -6px;border:0;border-radius:2px;background:currentColor;transform:none}@media(forced-colors:active){.nxe-hf-panel input[type=checkbox]:indeterminate:after{forced-color-adjust:none;background-color:CanvasText}}.nxe-hf-panel input[type=checkbox]:hover{border-color:var(--nxe-ui-checkbox-hover-border, #265a87)}.nxe-hf-panel input[type=checkbox]:focus-visible{outline:none;border-color:var(--nxe-ui-accent, #337ab7)}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1059
1232
  }
1060
1233
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeHeaderFilterPanelComponent, decorators: [{
1061
1234
  type: Component,
1062
- args: [{ selector: 'nxe-header-filter-panel', standalone: true, imports: [NgTemplateOutlet], host: { class: 'dx-header-filter-menu nxe-hf-panel' }, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "@if (showSearch()) {\r\n <input\r\n class=\"nxe-hf-search\"\r\n type=\"text\"\r\n placeholder=\"Search\"\r\n [value]=\"model().search()\"\r\n (input)=\"model().search.set($any($event.target).value)\" />\r\n}\r\n<div class=\"nxe-hf-list\" [class.nxe-hf-tree]=\"model().isDate()\">\r\n <label class=\"nxe-hf-item nxe-hf-selectall\">\r\n <input type=\"checkbox\" [checked]=\"model().allChecked()\" (change)=\"model().toggleAll()\" />\r\n <span>Select All</span>\r\n </label>\r\n @if (model().isDate()) {\r\n <!-- dx date header filter: Year > Month > Day checkbox tree -->\r\n <ng-container\r\n [ngTemplateOutlet]=\"hfTreeNodes\"\r\n [ngTemplateOutletContext]=\"{ $implicit: model().dateTreeFiltered() }\" />\r\n } @else {\r\n @for (item of model().distinctFiltered(); track item.key) {\r\n <label class=\"nxe-hf-item\">\r\n <input\r\n type=\"checkbox\"\r\n [checked]=\"model().draft().has(item.key)\"\r\n (change)=\"model().toggleValue(item.key)\" />\r\n <span>{{ item.text }}</span>\r\n </label>\r\n }\r\n }\r\n</div>\r\n<div class=\"nxe-hf-actions\">\r\n <button type=\"button\" class=\"nxe-hf-btn nxe-hf-ok\" (click)=\"applied.emit()\">OK</button>\r\n <button type=\"button\" class=\"nxe-hf-btn\" (click)=\"cancelled.emit()\">Cancel</button>\r\n</div>\r\n\r\n<!-- Recursive date-tree nodes for the header-filter popup -->\r\n<ng-template #hfTreeNodes let-nodes>\r\n @for (n of nodes; track n.key) {\r\n <div class=\"nxe-hf-tree-node\">\r\n @if (n.children.length) {\r\n <span\r\n class=\"nxe-hf-tree-toggle\"\r\n [class.nxe-expanded]=\"model().nodeExpanded(n.key)\"\r\n (click)=\"model().toggleExpand(n.key)\"></span>\r\n } @else {\r\n <span class=\"nxe-hf-tree-toggle-stub\"></span>\r\n }\r\n <label class=\"nxe-hf-item\">\r\n <input\r\n type=\"checkbox\"\r\n [checked]=\"model().nodeChecked(n)\"\r\n [indeterminate]=\"model().nodeIndeterminate(n)\"\r\n (change)=\"model().toggleNode(n)\" />\r\n <span>{{ n.label }}</span>\r\n </label>\r\n @if (n.children.length && model().nodeExpanded(n.key)) {\r\n <div class=\"nxe-hf-tree-children\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"hfTreeNodes\"\r\n [ngTemplateOutletContext]=\"{ $implicit: n.children }\" />\r\n </div>\r\n }\r\n </div>\r\n }\r\n</ng-template>\r\n", styles: ["@charset \"UTF-8\";.nxe-hf-panel{display:flex;flex-direction:column;min-width:200px;max-width:320px;background:#fff;border:1px solid #ddd;border-radius:4px;box-shadow:0 6px 12px #0000002d;color:#333}.nxe-hf-panel .nxe-hf-search{margin:6px;padding:5px 8px;border:1px solid #ddd;border-radius:3px;font:inherit;outline:none}.nxe-hf-panel .nxe-hf-search:focus{border-color:#30374c}.nxe-hf-panel .nxe-hf-list{max-height:260px;overflow-y:auto;padding:2px 0}.nxe-hf-panel .nxe-hf-item{display:flex;align-items:center;gap:8px;padding:10px;cursor:pointer}.nxe-hf-panel .nxe-hf-item:hover{background:#f4f4f4}.nxe-hf-panel .nxe-hf-item span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.nxe-hf-panel .nxe-hf-selectall{padding:12px 10px 8px;border-bottom:1px solid #ddd}.nxe-hf-panel .nxe-hf-actions{display:flex;justify-content:flex-end;gap:6px;padding:6px;border-top:1px solid #eee}.nxe-hf-panel .nxe-hf-btn{min-width:85px;padding:8px 12px;border:1px solid #ddd;border-radius:4px;background:transparent;color:inherit;cursor:pointer;font:inherit}.nxe-hf-panel .nxe-hf-btn:hover{background:#0000000a}.nxe-hf-panel input[type=checkbox]{appearance:none;-webkit-appearance:none;flex:0 0 auto;position:relative;box-sizing:border-box;width:22px;height:22px;margin:0;border:1px solid #ddd;border-radius:2px;background:#fff;cursor:pointer}.nxe-hf-panel input[type=checkbox]:checked:after{content:\"\";position:absolute;left:7px;top:3px;width:6px;height:12px;border:solid #337ab7;border-width:0 2px 2px 0;transform:rotate(45deg)}.nxe-hf-panel input[type=checkbox]:indeterminate:after{content:\"\";position:absolute;left:4px;top:9px;width:12px;height:2px;background:#337ab7}.nxe-hf-panel input[type=checkbox]:hover{border-color:#265a87}.nxe-hf-panel input[type=checkbox]:focus-visible{outline:none;border-color:#337ab7}.nxe-hf-tree-node{display:block;white-space:nowrap}.nxe-hf-tree-children{padding-left:18px}.nxe-hf-tree-node .nxe-hf-item{display:inline-flex}.nxe-hf-tree-toggle{display:inline-block;position:relative;width:14px;height:14px;cursor:pointer;vertical-align:middle}.nxe-hf-tree-toggle:before{content:\"\";position:absolute;left:4px;top:4px;width:5px;height:5px;border-right:1.5px solid #555;border-bottom:1.5px solid #555;transform:rotate(-45deg)}.nxe-hf-tree-toggle.nxe-expanded:before{transform:rotate(45deg)}.nxe-hf-tree-toggle-stub{display:inline-block;width:14px}\n"] }]
1063
- }], propDecorators: { model: [{ type: i0.Input, args: [{ isSignal: true, alias: "model", required: true }] }], searchEnabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchEnabled", required: false }] }], applied: [{ type: i0.Output, args: ["applied"] }], cancelled: [{ type: i0.Output, args: ["cancelled"] }] } });
1235
+ args: [{ selector: 'nxe-header-filter-panel', standalone: true, imports: [NgTemplateOutlet], host: { class: 'nxe-header-filter-menu nxe-hf-panel' }, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "@if (showSearch()) {\r\n <!-- dx builds this as a dxTextBox in mode 'search' (the list/tree searchEnabled option), which\r\n forces showClearButton: a magnifier in a 34px slot inside the left border, and a clear\r\n button in a 34px area inside the right border that only exists while there is text to\r\n clear. Both glyphs are drawn here from primitives; the DXIcons font is not used. -->\r\n <div class=\"nxe-hf-searchbox\">\r\n <!-- The magnifier both search boxes share (dx draws them with one rule,\r\n .dx-searchbox .dx-icon-search): a r4.75 ring and a 45deg handle, 1.9px strokes. Keep\r\n it identical to the tree view's (nxe-tree-view.component.html). -->\r\n <svg class=\"nxe-hf-search-icon\" viewBox=\"0 0 34 34\" aria-hidden=\"true\" focusable=\"false\">\r\n <circle cx=\"15\" cy=\"15\" r=\"4.75\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.9\" />\r\n <path d=\"M18.5 18.5 23.5 23.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.9\" />\r\n </svg>\r\n <input\r\n #searchInput\r\n class=\"nxe-hf-search\"\r\n type=\"text\"\r\n [attr.placeholder]=\"messages.t('common.search')\"\r\n [attr.aria-label]=\"messages.t('common.search')\"\r\n [value]=\"model().search()\"\r\n (input)=\"model().search.set($any($event.target).value)\" />\r\n @if (model().search()) {\r\n <button\r\n class=\"nxe-hf-search-clear\"\r\n type=\"button\"\r\n tabindex=\"-1\"\r\n [attr.aria-label]=\"messages.t('common.clear')\"\r\n (mousedown)=\"$event.preventDefault()\"\r\n (click)=\"clearSearch()\">\r\n <!-- The editors' clear glyph: a 16px disc with a 2px x cut out, two 2px bars\r\n each running corner to corner of the 8px square centred on the disc, drawn as\r\n a plus and turned 45deg. -->\r\n <svg viewBox=\"0 0 18 18\" aria-hidden=\"true\" focusable=\"false\">\r\n <path\r\n fill=\"currentColor\"\r\n fill-rule=\"evenodd\"\r\n transform=\"rotate(45 9 9)\"\r\n d=\"M9 1a8 8 0 1 1 0 16A8 8 0 1 1 9 1ZM8 3.343h2V8h4.657v2H10v4.657H8V10H3.343V8H8Z\" />\r\n </svg>\r\n </button>\r\n }\r\n </div>\r\n}\r\n<div class=\"nxe-hf-list\" [class.nxe-hf-tree]=\"model().isDate()\">\r\n <label class=\"nxe-hf-item nxe-hf-selectall\">\r\n <input\r\n type=\"checkbox\"\r\n [checked]=\"model().allChecked()\"\r\n [indeterminate]=\"model().someChecked()\"\r\n (change)=\"model().toggleAll()\" />\r\n <span>{{ messages.t('common.selectAll') }}</span>\r\n </label>\r\n @if (model().isDate()) {\r\n <!-- dx date header filter: Year > Month > Day checkbox tree -->\r\n <ng-container\r\n [ngTemplateOutlet]=\"hfTreeNodes\"\r\n [ngTemplateOutletContext]=\"{ $implicit: model().dateTreeFiltered() }\" />\r\n } @else {\r\n @for (item of model().distinctFiltered(); track item.key) {\r\n <label class=\"nxe-hf-item nxe-hf-list-item\">\r\n <input\r\n type=\"checkbox\"\r\n [checked]=\"model().draft().has(item.key)\"\r\n (change)=\"model().toggleValue(item.key)\" />\r\n <span>{{ item.text }}</span>\r\n </label>\r\n }\r\n }\r\n</div>\r\n<div class=\"nxe-hf-actions\">\r\n <button type=\"button\" class=\"nxe-hf-btn nxe-hf-ok\" (click)=\"applied.emit()\">{{ messages.t('common.ok') }}</button>\r\n <button type=\"button\" class=\"nxe-hf-btn\" (click)=\"cancelled.emit()\">{{ messages.t('common.cancel') }}</button>\r\n</div>\r\n\r\n<!-- Recursive date-tree nodes for the header-filter popup. Each node's first grid row is dx's\r\n .dx-treeview-item: the toggle (or its stub) and the label share it, the label spanning the\r\n whole row so its hover paints the full row, as dx's does. -->\r\n<ng-template #hfTreeNodes let-nodes>\r\n @for (n of nodes; track n.key) {\r\n <div class=\"nxe-hf-tree-node\">\r\n @if (n.children.length) {\r\n <span\r\n class=\"nxe-hf-tree-toggle\"\r\n [class.nxe-expanded]=\"model().nodeExpanded(n.key)\"\r\n (click)=\"model().toggleExpand(n.key)\"></span>\r\n } @else {\r\n <span class=\"nxe-hf-tree-toggle-stub\"></span>\r\n }\r\n <label class=\"nxe-hf-item nxe-hf-tree-item\">\r\n <input\r\n type=\"checkbox\"\r\n [checked]=\"model().nodeChecked(n)\"\r\n [indeterminate]=\"model().nodeIndeterminate(n)\"\r\n (change)=\"model().toggleNode(n)\" />\r\n <span>{{ n.label }}</span>\r\n </label>\r\n @if (n.children.length && model().nodeExpanded(n.key)) {\r\n <div class=\"nxe-hf-tree-children\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"hfTreeNodes\"\r\n [ngTemplateOutletContext]=\"{ $implicit: n.children }\" />\r\n </div>\r\n }\r\n </div>\r\n }\r\n</ng-template>\r\n", styles: ["@charset \"UTF-8\";.nxe-hf-panel{display:flex;flex-direction:column;box-sizing:border-box;width:252px;height:325px;min-width:var(--nxe-ui-header-filter-min-width, 0px);padding:20px 0 0;background:var(--nxe-ui-surface, #fff);border:1px solid var(--nxe-ui-border, #ddd);border-radius:var(--nxe-ui-popup-radius, 6px);box-shadow:var(--nxe-ui-popup-shadow, 0 6px 12px rgba(0, 0, 0, .175));color:var(--nxe-ui-text, #333)}.nxe-hf-panel .nxe-hf-searchbox{position:relative;display:flex;flex:0 0 auto;box-sizing:border-box;margin:0 20px 5px;padding:0;border:1px solid var(--nxe-ui-border, #ddd);border-radius:var(--nxe-ui-radius, 4px);background:var(--nxe-ui-surface, #fff)}.nxe-hf-panel .nxe-hf-searchbox:hover{border-color:var(--nxe-ui-accent-faded, rgba(51, 122, 183, .4))}.nxe-hf-panel .nxe-hf-searchbox:focus-within{border-color:var(--nxe-ui-accent, #337ab7)}.nxe-hf-panel .nxe-hf-search{-webkit-appearance:none;appearance:none;flex:1 1 auto;min-width:0;width:100%;box-sizing:border-box;min-height:34px;margin:0;padding:7px 9px 8px;padding-inline-start:34px;border:0;border-radius:var(--nxe-ui-radius, 4px);outline:0;background:transparent;color:var(--nxe-ui-text, #333);font:inherit}.nxe-hf-panel .nxe-hf-search::placeholder{color:var(--nxe-ui-text-muted, #999);opacity:1}.nxe-hf-panel .nxe-hf-search-icon{position:absolute;top:50%;left:0;display:block;box-sizing:border-box;width:34px;height:34px;margin-top:-17px;color:var(--nxe-ui-text-muted, #999);pointer-events:none}.nxe-hf-panel .nxe-hf-search-clear{position:relative;display:flex;align-items:center;justify-content:center;flex:0 0 34px;box-sizing:border-box;width:34px;min-width:34px;margin:0;padding:0;border:0;background:transparent;color:var(--nxe-ui-text-muted, #999);cursor:pointer}.nxe-hf-panel .nxe-hf-search-clear svg{display:block;width:18px;height:18px}.nxe-hf-panel .nxe-hf-list{flex:1 1 auto;min-height:0;margin:0 20px 20px;padding:0;overflow-x:hidden;overflow-y:auto}.nxe-hf-panel .nxe-hf-item{box-sizing:border-box;margin:0;cursor:pointer}.nxe-hf-panel .nxe-hf-item>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.nxe-hf-panel .nxe-hf-list-item{display:flex;align-items:center;padding:0}.nxe-hf-panel .nxe-hf-list-item>input[type=checkbox]{margin:-1px 0 -3px;margin-inline:10px -1px}.nxe-hf-panel .nxe-hf-list-item>span{flex:1 1 auto;min-width:0;padding:10px}.nxe-hf-panel .nxe-hf-list-item+.nxe-hf-list-item{border-top:1px solid var(--nxe-ui-border, #ddd)}.nxe-hf-panel .nxe-hf-selectall{display:block;padding:9px 0 8px;border-bottom:1px solid var(--nxe-ui-border, #ddd);white-space:nowrap}.nxe-hf-panel .nxe-hf-selectall>input[type=checkbox]{float:inline-start;margin:-1px 0 -3px;margin-inline:10px 0}.nxe-hf-panel .nxe-hf-selectall>span{display:inline-block;box-sizing:border-box;max-width:calc(100% - 32px);margin-top:1px;padding:0 10px;line-height:17px;vertical-align:baseline}.nxe-hf-panel .nxe-hf-list-item:hover>input[type=checkbox]:not(:focus-visible),.nxe-hf-panel .nxe-hf-selectall:hover>input[type=checkbox]:not(:focus-visible){border-color:var(--nxe-ui-checkbox-hover-border, #265a87)}.nxe-hf-panel .nxe-hf-tree>.nxe-hf-selectall{display:flex;align-items:center;padding:9px 40px 11px}.nxe-hf-panel .nxe-hf-tree>.nxe-hf-selectall>input[type=checkbox]{float:none;margin:0}.nxe-hf-panel .nxe-hf-tree>.nxe-hf-selectall>span{display:block;flex:0 1 auto;min-width:0;max-width:none;margin:0;padding:0;padding-inline-start:8px;line-height:inherit}.nxe-hf-panel .nxe-hf-tree-node{display:grid;grid-template-columns:32px minmax(0,1fr);align-items:center;white-space:nowrap}.nxe-hf-panel .nxe-hf-tree-toggle,.nxe-hf-panel .nxe-hf-tree-toggle-stub{grid-area:1/1;justify-self:end;position:relative;display:block;box-sizing:border-box;width:22px;height:22px}.nxe-hf-panel .nxe-hf-tree-toggle{color:var(--nxe-ui-text, #333);cursor:pointer}.nxe-hf-panel .nxe-hf-tree-toggle:before{content:\"\";position:absolute;left:50%;top:50%;box-sizing:border-box;width:0;height:0;margin:-4px 0 0 -2px;border-style:solid;border-width:4px 0 4px 4px;border-color:transparent transparent transparent currentColor;transform:none}.nxe-hf-panel .nxe-hf-tree-toggle.nxe-expanded:before{margin:-2px 0 0 -4px;border-width:4px 4px 0;border-color:currentColor transparent transparent}.nxe-hf-panel .nxe-hf-tree-toggle-stub{pointer-events:none}.nxe-hf-panel .nxe-hf-tree-item{grid-area:1/1/2/3;display:flex;align-items:center;gap:8px;padding:5px 10px;padding-inline-start:40px;pointer-events:none}.nxe-hf-panel .nxe-hf-tree-item>input[type=checkbox],.nxe-hf-panel .nxe-hf-tree-item>span{pointer-events:auto}.nxe-hf-panel .nxe-hf-tree-item>span{flex:0 1 auto;min-width:0}.nxe-hf-panel .nxe-hf-tree-children{grid-area:2/1/3/3;padding-inline-start:15px}.nxe-hf-panel .nxe-hf-tree-node:hover:not(:has(>.nxe-hf-tree-children:hover))>.nxe-hf-tree-item{background-color:var(--nxe-ui-hover-bg, rgba(0, 0, 0, .04));color:var(--nxe-ui-text, #333)}.nxe-hf-panel .nxe-hf-actions{display:flex;flex:0 0 auto;align-items:center;justify-content:flex-end;gap:10px;box-sizing:content-box;height:36px;padding:20px}.nxe-hf-panel .nxe-hf-btn{box-sizing:border-box;min-width:100px;padding:8px 12px;border:1px solid var(--nxe-ui-border, #ddd);border-radius:var(--nxe-ui-radius, 4px);background:transparent;color:inherit;cursor:pointer;font:inherit;line-height:1.2858}.nxe-hf-panel .nxe-hf-btn:hover{background:var(--nxe-ui-hover-bg, rgba(0, 0, 0, .04))}.nxe-hf-panel input[type=checkbox]{appearance:none;-webkit-appearance:none;flex:0 0 auto;position:relative;box-sizing:border-box;width:22px;height:22px;margin:0;border:1px solid var(--nxe-ui-border, #ddd);border-radius:2px;background:var(--nxe-ui-surface, #fff);color:var(--nxe-ui-accent, #337ab7);cursor:pointer}.nxe-hf-panel input[type=checkbox]:checked:not(:indeterminate):after{content:\"\";position:absolute;left:50%;top:50%;box-sizing:border-box;width:7.5px;height:13.25px;margin:-8.1px 0 0 -3.75px;border:solid currentColor;border-width:0 2px 2px 0;border-radius:0;background:none;transform:rotate(45deg)}.nxe-hf-panel input[type=checkbox]:indeterminate:after{content:\"\";position:absolute;left:50%;top:50%;box-sizing:border-box;width:12px;height:12px;margin:-6px 0 0 -6px;border:0;border-radius:2px;background:currentColor;transform:none}@media(forced-colors:active){.nxe-hf-panel input[type=checkbox]:indeterminate:after{forced-color-adjust:none;background-color:CanvasText}}.nxe-hf-panel input[type=checkbox]:hover{border-color:var(--nxe-ui-checkbox-hover-border, #265a87)}.nxe-hf-panel input[type=checkbox]:focus-visible{outline:none;border-color:var(--nxe-ui-accent, #337ab7)}\n"] }]
1236
+ }], propDecorators: { model: [{ type: i0.Input, args: [{ isSignal: true, alias: "model", required: true }] }], searchEnabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchEnabled", required: false }] }], applied: [{ type: i0.Output, args: ["applied"] }], cancelled: [{ type: i0.Output, args: ["cancelled"] }], searchInput: [{ type: i0.ViewChild, args: ['searchInput', { isSignal: true }] }] } });
1064
1237
 
1065
1238
  /** Create + attach the header-filter popup; the caller keeps the ref to detach/dispose. */
1066
1239
  function openNxeHeaderFilterOverlay(options) {
@@ -1227,12 +1400,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
1227
1400
  class NxePagingComponent {
1228
1401
  pageSize = input(20, /* @ts-ignore */
1229
1402
  ...(ngDevMode ? [{ debugName: "pageSize" }] : /* istanbul ignore next */ []));
1230
- pageIndex = input(0, /* @ts-ignore */
1403
+ /** Two-way: dx supports `[(pageIndex)]`, and a consumer drives the pager from outside with it. */
1404
+ pageIndex = model(0, /* @ts-ignore */
1231
1405
  ...(ngDevMode ? [{ debugName: "pageIndex" }] : /* istanbul ignore next */ []));
1232
1406
  enabled = input(true, /* @ts-ignore */
1233
1407
  ...(ngDevMode ? [{ debugName: "enabled" }] : /* istanbul ignore next */ []));
1234
1408
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxePagingComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1235
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.5", type: NxePagingComponent, isStandalone: true, selector: "nxe-paging", inputs: { pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, pageIndex: { classPropertyName: "pageIndex", publicName: "pageIndex", isSignal: true, isRequired: false, transformFunction: null }, enabled: { classPropertyName: "enabled", publicName: "enabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { styleAttribute: "display: none" }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1409
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.5", type: NxePagingComponent, isStandalone: true, selector: "nxe-paging", inputs: { pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, pageIndex: { classPropertyName: "pageIndex", publicName: "pageIndex", isSignal: true, isRequired: false, transformFunction: null }, enabled: { classPropertyName: "enabled", publicName: "enabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pageIndex: "pageIndexChange" }, host: { styleAttribute: "display: none" }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1236
1410
  }
1237
1411
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxePagingComponent, decorators: [{
1238
1412
  type: Component,
@@ -1244,11 +1418,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
1244
1418
  changeDetection: ChangeDetectionStrategy.OnPush,
1245
1419
  encapsulation: ViewEncapsulation.None,
1246
1420
  }]
1247
- }], propDecorators: { pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }], pageIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageIndex", required: false }] }], enabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "enabled", required: false }] }] } });
1421
+ }], propDecorators: { pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }], pageIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageIndex", required: false }] }, { type: i0.Output, args: ["pageIndexChange"] }], enabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "enabled", required: false }] }] } });
1248
1422
  /** `dxo-pager` — the pager UI config. */
1249
1423
  class NxePagerComponent {
1424
+ /** `'auto'` shows the pager only when there is more than one page (dx `isVisible`). */
1250
1425
  visible = input('auto', /* @ts-ignore */
1251
1426
  ...(ngDevMode ? [{ debugName: "visible" }] : /* istanbul ignore next */ []));
1427
+ /** `'auto'` offers half, the same and double the page size (dx `getPageSizes`). */
1252
1428
  allowedPageSizes = input('auto', /* @ts-ignore */
1253
1429
  ...(ngDevMode ? [{ debugName: "allowedPageSizes" }] : /* istanbul ignore next */ []));
1254
1430
  showPageSizeSelector = input(false, /* @ts-ignore */
@@ -1257,10 +1433,20 @@ class NxePagerComponent {
1257
1433
  ...(ngDevMode ? [{ debugName: "showNavigationButtons" }] : /* istanbul ignore next */ []));
1258
1434
  showInfo = input(false, /* @ts-ignore */
1259
1435
  ...(ngDevMode ? [{ debugName: "showInfo" }] : /* istanbul ignore next */ []));
1436
+ /**
1437
+ * `'full'` and `'adaptive'` list the page sizes as buttons; `'compact'` uses a drop-down and
1438
+ * always shows the navigation buttons, as dx's small mode does.
1439
+ */
1260
1440
  displayMode = input('adaptive', /* @ts-ignore */
1261
1441
  ...(ngDevMode ? [{ debugName: "displayMode" }] : /* istanbul ignore next */ []));
1442
+ /** dx `pager.infoText`: `{0}` the page, `{1}` the page count, `{2}` the item count. */
1443
+ infoText = input(undefined, /* @ts-ignore */
1444
+ ...(ngDevMode ? [{ debugName: "infoText" }] : /* istanbul ignore next */ []));
1445
+ /** dx `pager.label`: the pager's accessible name. */
1446
+ label = input(undefined, /* @ts-ignore */
1447
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1262
1448
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxePagerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1263
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.5", type: NxePagerComponent, isStandalone: true, selector: "nxe-pager", inputs: { visible: { classPropertyName: "visible", publicName: "visible", isSignal: true, isRequired: false, transformFunction: null }, allowedPageSizes: { classPropertyName: "allowedPageSizes", publicName: "allowedPageSizes", isSignal: true, isRequired: false, transformFunction: null }, showPageSizeSelector: { classPropertyName: "showPageSizeSelector", publicName: "showPageSizeSelector", isSignal: true, isRequired: false, transformFunction: null }, showNavigationButtons: { classPropertyName: "showNavigationButtons", publicName: "showNavigationButtons", isSignal: true, isRequired: false, transformFunction: null }, showInfo: { classPropertyName: "showInfo", publicName: "showInfo", isSignal: true, isRequired: false, transformFunction: null }, displayMode: { classPropertyName: "displayMode", publicName: "displayMode", isSignal: true, isRequired: false, transformFunction: null } }, host: { styleAttribute: "display: none" }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1449
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.5", type: NxePagerComponent, isStandalone: true, selector: "nxe-pager", inputs: { visible: { classPropertyName: "visible", publicName: "visible", isSignal: true, isRequired: false, transformFunction: null }, allowedPageSizes: { classPropertyName: "allowedPageSizes", publicName: "allowedPageSizes", isSignal: true, isRequired: false, transformFunction: null }, showPageSizeSelector: { classPropertyName: "showPageSizeSelector", publicName: "showPageSizeSelector", isSignal: true, isRequired: false, transformFunction: null }, showNavigationButtons: { classPropertyName: "showNavigationButtons", publicName: "showNavigationButtons", isSignal: true, isRequired: false, transformFunction: null }, showInfo: { classPropertyName: "showInfo", publicName: "showInfo", isSignal: true, isRequired: false, transformFunction: null }, displayMode: { classPropertyName: "displayMode", publicName: "displayMode", isSignal: true, isRequired: false, transformFunction: null }, infoText: { classPropertyName: "infoText", publicName: "infoText", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null } }, host: { styleAttribute: "display: none" }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1264
1450
  }
1265
1451
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxePagerComponent, decorators: [{
1266
1452
  type: Component,
@@ -1272,7 +1458,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
1272
1458
  changeDetection: ChangeDetectionStrategy.OnPush,
1273
1459
  encapsulation: ViewEncapsulation.None,
1274
1460
  }]
1275
- }], propDecorators: { visible: [{ type: i0.Input, args: [{ isSignal: true, alias: "visible", required: false }] }], allowedPageSizes: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowedPageSizes", required: false }] }], showPageSizeSelector: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPageSizeSelector", required: false }] }], showNavigationButtons: [{ type: i0.Input, args: [{ isSignal: true, alias: "showNavigationButtons", required: false }] }], showInfo: [{ type: i0.Input, args: [{ isSignal: true, alias: "showInfo", required: false }] }], displayMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayMode", required: false }] }] } });
1461
+ }], propDecorators: { visible: [{ type: i0.Input, args: [{ isSignal: true, alias: "visible", required: false }] }], allowedPageSizes: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowedPageSizes", required: false }] }], showPageSizeSelector: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPageSizeSelector", required: false }] }], showNavigationButtons: [{ type: i0.Input, args: [{ isSignal: true, alias: "showNavigationButtons", required: false }] }], showInfo: [{ type: i0.Input, args: [{ isSignal: true, alias: "showInfo", required: false }] }], displayMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayMode", required: false }] }], infoText: [{ type: i0.Input, args: [{ isSignal: true, alias: "infoText", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }] } });
1276
1462
 
1277
1463
  /* eslint-disable @typescript-eslint/no-explicit-any */
1278
1464
  /**
@@ -1341,8 +1527,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
1341
1527
  * `nxe-master-detail` — drop-in for DevExtreme `dxo-master-detail` (orbit addition).
1342
1528
  *
1343
1529
  * A no-render config element read by the parent `nxe-data-grid`. When `enabled`, the grid
1344
- * prepends a `dx-command-expand` column; expanding a row renders a
1345
- * `<tr class="dx-master-detail-row">` hosting the named sibling
1530
+ * prepends a `nxe-command-expand` column; expanding a row renders a
1531
+ * `<tr class="nxe-master-detail-row">` hosting the named sibling
1346
1532
  * `<ng-template nxeTemplate="...">` with `{ data, rowIndex, key }` — instantiated ONLY while
1347
1533
  * expanded, preserving the app's lazy nested-grid loading (`onRowExpanding` fires first).
1348
1534
  */
@@ -1417,12 +1603,14 @@ function cellValue(row, col) {
1417
1603
  }
1418
1604
  switch (col.dataType) {
1419
1605
  case 'number': {
1420
- const num = typeof raw === 'number' ? raw : Number(raw);
1606
+ // The same parseFloat deserializer the cell display uses, so a cell exports the number
1607
+ // it shows. dx exports that raw number too (exceljs/export.js:207-221).
1608
+ const num = nxeNumberValue(raw);
1421
1609
  if (isNaN(num)) {
1422
1610
  return { value: String(raw) };
1423
1611
  }
1424
- // dx number masks ≈ exceljs numFmt; convert single-quoted literals to double-quoted.
1425
- const mask = typeof col.format === 'string' ? col.format.replace(/'/g, '"') : undefined;
1612
+ // dx number masks ≈ exceljs numFmt, minus the scaling commas Excel reads differently.
1613
+ const mask = typeof col.format === 'string' ? nxeExcelNumberMask(col.format) : undefined;
1426
1614
  return { value: num, numFmt: mask };
1427
1615
  }
1428
1616
  case 'date': {
@@ -1500,6 +1688,476 @@ async function exportDataGrid(opts) {
1500
1688
  }
1501
1689
  }
1502
1690
 
1691
+ /* eslint-disable @typescript-eslint/no-explicit-any */
1692
+ /** The default page size of a source (and of a remote grid with no `nxe-paging`). */
1693
+ const NXE_REMOTE_DEFAULT_PAGE_SIZE = 20;
1694
+ /** Structural equality of two queries; what keeps a re-stated, unchanged query from reloading. */
1695
+ function nxeSameQuery(a, b) {
1696
+ return (a.pageIndex === b.pageIndex &&
1697
+ a.pageSize === b.pageSize &&
1698
+ JSON.stringify(a.sort) === JSON.stringify(b.sort) &&
1699
+ JSON.stringify(a.filter) === JSON.stringify(b.filter));
1700
+ }
1701
+ /** Builds the load options for a query. */
1702
+ function nxeLoadOptions(query) {
1703
+ const pageSize = Math.max(1, Math.floor(query.pageSize));
1704
+ const pageIndex = Math.max(0, Math.floor(query.pageIndex));
1705
+ return {
1706
+ pageIndex,
1707
+ pageSize,
1708
+ skip: pageIndex * pageSize,
1709
+ take: pageSize,
1710
+ sort: query.sort.map((s) => ({ selector: s.selector, desc: s.desc })),
1711
+ filter: query.filter,
1712
+ requireTotalCount: true,
1713
+ };
1714
+ }
1715
+ const FILTER_OPERATORS = new Set([
1716
+ '=',
1717
+ '<>',
1718
+ '<',
1719
+ '<=',
1720
+ '>',
1721
+ '>=',
1722
+ 'contains',
1723
+ 'notcontains',
1724
+ 'startswith',
1725
+ 'endswith',
1726
+ ]);
1727
+ function isCondition(expr) {
1728
+ return (Array.isArray(expr) &&
1729
+ expr.length === 3 &&
1730
+ typeof expr[0] === 'string' &&
1731
+ typeof expr[1] === 'string' &&
1732
+ FILTER_OPERATORS.has(expr[1]));
1733
+ }
1734
+ /**
1735
+ * Flattens the AND-chain a grid builds into simple conditions, for endpoints that take a flat
1736
+ * parameter list (`status=sent,confirmed&supplierId=123`). An OR of `=` on one field becomes one
1737
+ * `in` condition. `exact` is `false` when part of the expression could not be flattened (a `!`, or
1738
+ * an OR across fields); those parts are left out of `conditions`, so an adapter that cannot take
1739
+ * `exact: false` should send the expression as JSON instead.
1740
+ */
1741
+ function nxeFlattenFilter(filter) {
1742
+ const conditions = [];
1743
+ let exact = true;
1744
+ const walk = (expr) => {
1745
+ if (isCondition(expr)) {
1746
+ conditions.push({ field: expr[0], operator: expr[1], value: expr[2] });
1747
+ return;
1748
+ }
1749
+ if (Array.isArray(expr) && expr.length >= 3 && expr.length % 2 === 1) {
1750
+ const connectors = new Set(expr.filter((_, i) => i % 2 === 1));
1751
+ if (connectors.size === 1 && connectors.has('and')) {
1752
+ for (let i = 0; i < expr.length; i += 2) {
1753
+ walk(expr[i]);
1754
+ }
1755
+ return;
1756
+ }
1757
+ if (connectors.size === 1 && connectors.has('or')) {
1758
+ const parts = expr.filter((_, i) => i % 2 === 0);
1759
+ if (parts.every((p) => isCondition(p) && p[1] === '=' && p[0] === parts[0][0])) {
1760
+ conditions.push({
1761
+ field: parts[0][0],
1762
+ operator: 'in',
1763
+ value: parts.map((p) => p[2]),
1764
+ });
1765
+ return;
1766
+ }
1767
+ }
1768
+ }
1769
+ exact = false;
1770
+ };
1771
+ if (filter && filter.length) {
1772
+ walk(filter);
1773
+ }
1774
+ return { conditions, exact };
1775
+ }
1776
+ function nxeFilterJoin(connector, parts) {
1777
+ const present = parts.filter((p) => !!p && p.length > 0);
1778
+ if (present.length === 0) {
1779
+ return null;
1780
+ }
1781
+ if (present.length === 1) {
1782
+ return present[0];
1783
+ }
1784
+ return present.flatMap((p, i) => (i === 0 ? [p] : [connector, p]));
1785
+ }
1786
+ /** Combines expressions with AND; `null` and empty ones are dropped, a single one is returned as is. */
1787
+ function nxeFilterAnd(...parts) {
1788
+ return nxeFilterJoin('and', parts);
1789
+ }
1790
+ /** Combines expressions with OR, as {@link nxeFilterAnd} does with AND. */
1791
+ function nxeFilterOr(...parts) {
1792
+ return nxeFilterJoin('or', parts);
1793
+ }
1794
+ /**
1795
+ * A server-backed list for `nxe-data-grid` and `nxe-paginator`. It holds one page at a time:
1796
+ * the grid (or the app) states a query, the source asks the server for exactly that page, and the
1797
+ * result lands in signals.
1798
+ *
1799
+ * ```ts
1800
+ * readonly orders = new NxeRemoteDataSource<Order>(
1801
+ * (o, { signal }) => this.http.get<OrderList>('/api/orders', { params: toParams(o) }).pipe(map(toResult)),
1802
+ * { key: 'id' },
1803
+ * );
1804
+ * ```
1805
+ *
1806
+ * A request still in flight is aborted (its `AbortSignal` fires, or its observable is
1807
+ * unsubscribed) when a newer one starts, and a late response that slips past is ignored, so the
1808
+ * rows on screen always belong to the latest query.
1809
+ *
1810
+ * The source is lazy: nothing loads until {@link setOptions} or {@link reload} is called, which a
1811
+ * grid bound to it does on its own.
1812
+ */
1813
+ class NxeRemoteDataSource {
1814
+ loadFn;
1815
+ /** The row key field, when the source was given one. */
1816
+ key;
1817
+ /** Milliseconds a grid waits after the last filter-row keystroke before asking. */
1818
+ filterDebounce;
1819
+ itemsState = signal([], /* @ts-ignore */
1820
+ ...(ngDevMode ? [{ debugName: "itemsState" }] : /* istanbul ignore next */ []));
1821
+ totalCountState = signal(0, /* @ts-ignore */
1822
+ ...(ngDevMode ? [{ debugName: "totalCountState" }] : /* istanbul ignore next */ []));
1823
+ loadingState = signal(false, /* @ts-ignore */
1824
+ ...(ngDevMode ? [{ debugName: "loadingState" }] : /* istanbul ignore next */ []));
1825
+ errorState = signal(null, /* @ts-ignore */
1826
+ ...(ngDevMode ? [{ debugName: "errorState" }] : /* istanbul ignore next */ []));
1827
+ loadedState = signal(false, /* @ts-ignore */
1828
+ ...(ngDevMode ? [{ debugName: "loadedState" }] : /* istanbul ignore next */ []));
1829
+ queryState;
1830
+ optionsState;
1831
+ /** The rows of the current page. Kept (not cleared) while the next page loads. */
1832
+ items = this.itemsState.asReadonly();
1833
+ /** Rows matching the filter across every page, as the last successful response said. */
1834
+ totalCount = this.totalCountState.asReadonly();
1835
+ /** A request is in flight. */
1836
+ loading = this.loadingState.asReadonly();
1837
+ /** The last request's failure, or `null` after a success or while a retry runs. */
1838
+ error = this.errorState.asReadonly();
1839
+ /** A response has arrived at least once. */
1840
+ loaded = this.loadedState.asReadonly();
1841
+ /** The query and the `skip`/`take` of the page the next (or current) request uses. */
1842
+ options;
1843
+ started = false;
1844
+ seq = 0;
1845
+ controller = null;
1846
+ unsubscribe = null;
1847
+ constructor(loadFn, config = {}) {
1848
+ this.loadFn = loadFn;
1849
+ this.key = config.key;
1850
+ this.filterDebounce = config.filterDebounce ?? 300;
1851
+ const initial = {
1852
+ pageIndex: config.initial?.pageIndex ?? 0,
1853
+ pageSize: config.initial?.pageSize ?? NXE_REMOTE_DEFAULT_PAGE_SIZE,
1854
+ sort: config.initial?.sort ?? [],
1855
+ filter: config.initial?.filter ?? null,
1856
+ };
1857
+ this.queryState = signal(initial, /* @ts-ignore */
1858
+ ...(ngDevMode ? [{ debugName: "queryState" }] : /* istanbul ignore next */ []));
1859
+ this.optionsState = signal(nxeLoadOptions(initial), /* @ts-ignore */
1860
+ ...(ngDevMode ? [{ debugName: "optionsState" }] : /* istanbul ignore next */ []));
1861
+ this.options = this.optionsState.asReadonly();
1862
+ }
1863
+ /**
1864
+ * States a new query (any of its parts) and loads it. Nothing happens when it is the query
1865
+ * already stated, so a caller can re-state its inputs freely. The first call starts the source.
1866
+ */
1867
+ setOptions(patch) {
1868
+ const current = this.queryState();
1869
+ const next = {
1870
+ pageIndex: patch.pageIndex ?? current.pageIndex,
1871
+ pageSize: patch.pageSize ?? current.pageSize,
1872
+ sort: patch.sort ?? current.sort,
1873
+ filter: patch.filter === undefined ? current.filter : patch.filter,
1874
+ };
1875
+ if (this.started && nxeSameQuery(current, next)) {
1876
+ return;
1877
+ }
1878
+ this.queryState.set(next);
1879
+ this.optionsState.set(nxeLoadOptions(next));
1880
+ this.run();
1881
+ }
1882
+ /** Loads the current query again (after an edit, or as a retry). */
1883
+ reload() {
1884
+ this.run();
1885
+ }
1886
+ /** Aborts the request in flight and ignores its response. */
1887
+ cancel() {
1888
+ this.seq++;
1889
+ this.abort();
1890
+ this.loadingState.set(false);
1891
+ }
1892
+ abort() {
1893
+ this.controller?.abort();
1894
+ this.controller = null;
1895
+ this.unsubscribe?.();
1896
+ this.unsubscribe = null;
1897
+ }
1898
+ run() {
1899
+ this.started = true;
1900
+ this.abort();
1901
+ const seq = ++this.seq;
1902
+ const controller = new AbortController();
1903
+ this.controller = controller;
1904
+ this.loadingState.set(true);
1905
+ this.errorState.set(null);
1906
+ const options = nxeLoadOptions(this.queryState());
1907
+ let pending;
1908
+ try {
1909
+ const result = this.loadFn(options, { signal: controller.signal });
1910
+ pending = isObservable(result) ? this.fromObservable(result, controller.signal) : Promise.resolve(result);
1911
+ }
1912
+ catch (error) {
1913
+ pending = Promise.reject(error);
1914
+ }
1915
+ pending.then((result) => {
1916
+ if (seq !== this.seq) {
1917
+ return; // superseded: a newer query owns the signals now
1918
+ }
1919
+ const page = Array.isArray(result)
1920
+ ? { data: result, totalCount: result.length }
1921
+ : result;
1922
+ const data = page.data ?? [];
1923
+ this.itemsState.set(data);
1924
+ this.totalCountState.set(Number.isFinite(page.totalCount) ? page.totalCount : data.length);
1925
+ this.loadedState.set(true);
1926
+ this.loadingState.set(false);
1927
+ }, (error) => {
1928
+ if (seq !== this.seq) {
1929
+ return;
1930
+ }
1931
+ this.itemsState.set([]);
1932
+ this.errorState.set(error ?? new Error('Load failed'));
1933
+ this.loadingState.set(false);
1934
+ });
1935
+ }
1936
+ /** The last value an observable emits; rejects when it errors or completes empty. */
1937
+ fromObservable(source, abortSignal) {
1938
+ return new Promise((resolve, reject) => {
1939
+ let last = null;
1940
+ const subscription = source.subscribe({
1941
+ next: (value) => (last = { value }),
1942
+ error: reject,
1943
+ complete: () => (last ? resolve(last.value) : reject(new Error('Load completed without a value'))),
1944
+ });
1945
+ this.unsubscribe = () => subscription.unsubscribe();
1946
+ abortSignal.addEventListener('abort', () => subscription.unsubscribe(), { once: true });
1947
+ });
1948
+ }
1949
+ }
1950
+ /** True for an object with a `load` function that is not an array or a {@link NxeRemoteDataSource}. */
1951
+ function nxeIsStoreLike(value) {
1952
+ return (!!value &&
1953
+ typeof value === 'object' &&
1954
+ !Array.isArray(value) &&
1955
+ !(value instanceof NxeRemoteDataSource) &&
1956
+ typeof value.load === 'function');
1957
+ }
1958
+
1959
+ /**
1960
+ * The pager's page list and info text, as DevExtreme 24.2.3's pagination builds them.
1961
+ *
1962
+ * Pure functions, so the grid holds the only state (the sliding window, carried between renders
1963
+ * by a linkedSignal) and the rules can be specced without rendering a grid.
1964
+ */
1965
+ /** dx `maxPagesCount` (grid_core/pager/m_pager.js): up to this many pages are all listed. */
1966
+ const NXE_PAGER_MAX_PAGES = 10;
1967
+ /** dx `PAGES_LIMITER` (pagination/pages/large.js): the pages kept around the current one. */
1968
+ const WINDOW = 4;
1969
+ /** dx's `dxPagination-infoText` message. */
1970
+ const NXE_PAGER_INFO_TEXT = 'Page {0} of {1} ({2} items)';
1971
+ const pageItem = (page) => ({ key: String(page), page, separator: false });
1972
+ const separatorItem = (key) => ({ key, page: -1, separator: true });
1973
+ /**
1974
+ * The page list for `pageIndex` of `pageCount`, given the previous render's window.
1975
+ *
1976
+ * dx `PagesLarge` (pagination/pages/large.js). Ten pages or fewer are all listed. Beyond that the
1977
+ * list is the first page, a window of four, the last page, and a `. . .` separator wherever pages
1978
+ * are skipped. The window stays put while the new page is one of its inner pages (so clicking
1979
+ * through it does not scroll the numbers under the pointer), and otherwise moves so the page
1980
+ * lands one in from its edge.
1981
+ */
1982
+ function nxePagerWindow(pageCount, pageIndex, prev) {
1983
+ const slide = prev?.window ?? [];
1984
+ const reuse = prev?.reuse ?? [];
1985
+ if (pageCount <= NXE_PAGER_MAX_PAGES) {
1986
+ // dx leaves its window state untouched here, so it resumes if the count grows again.
1987
+ return { window: slide, reuse, items: Array.from({ length: pageCount }, (_, i) => pageItem(i)) };
1988
+ }
1989
+ const last = pageCount - 1;
1990
+ const reusable = reuse.length > 0 && reuse[reuse.length - 1] < last && reuse.includes(pageIndex);
1991
+ let next = slide;
1992
+ let nextReuse = reuse;
1993
+ if (!reusable) {
1994
+ let start;
1995
+ if (pageIndex === slide[0]) {
1996
+ start = pageIndex - 1;
1997
+ }
1998
+ else if (pageIndex === slide[slide.length - 1]) {
1999
+ start = pageIndex + 2 - WINDOW;
2000
+ }
2001
+ else if (pageIndex < WINDOW) {
2002
+ start = 1;
2003
+ }
2004
+ else if (pageIndex >= pageCount - WINDOW) {
2005
+ start = pageCount - WINDOW - 1;
2006
+ }
2007
+ else {
2008
+ start = pageIndex - 1;
2009
+ }
2010
+ // Only a window left over from a different page count can land outside 1..last-4; dx
2011
+ // would list pages that do not exist, so keep it inside the list.
2012
+ start = Math.min(Math.max(start, 1), pageCount - WINDOW - 1);
2013
+ next = Array.from({ length: WINDOW }, (_, i) => start + i);
2014
+ }
2015
+ const start = next[0];
2016
+ const kind = start === 1 ? 'high' : start + WINDOW === last ? 'low' : 'both';
2017
+ if (!reusable) {
2018
+ nextReuse = kind === 'both' ? next.slice(1, -1) : kind === 'high' ? next.slice(0, -1) : next.slice(1);
2019
+ }
2020
+ const items = [pageItem(0)];
2021
+ if (kind !== 'high') {
2022
+ items.push(separatorItem('low'));
2023
+ }
2024
+ items.push(...next.map(pageItem));
2025
+ if (kind !== 'low') {
2026
+ items.push(separatorItem('high'));
2027
+ }
2028
+ items.push(pageItem(last));
2029
+ return { window: next, reuse: nextReuse, items };
2030
+ }
2031
+ /**
2032
+ * dx's info text: `{0}` is the 1-based page, `{1}` the page count and `{2}` the item count
2033
+ * (pagination/info.js, formatted by core/utils/string `format`).
2034
+ */
2035
+ function nxePagerInfoText(template, pageIndex, pageCount, itemCount) {
2036
+ const values = [String(pageIndex + 1), String(pageCount), String(itemCount)];
2037
+ return template.replace(/\{(\d+)\}/g, (match, i) => values[Number(i)] ?? match);
2038
+ }
2039
+ /**
2040
+ * dx `getPageSizes` (grid_core/pager/m_pager.js): an explicit list as given; `'auto'` offers half,
2041
+ * the same and double the configured page size, and nothing when that size is 1 or less.
2042
+ */
2043
+ function nxePagerPageSizes(allowed, pageSize) {
2044
+ if (Array.isArray(allowed)) {
2045
+ return [...allowed];
2046
+ }
2047
+ return allowed === 'auto' && pageSize > 1 ? [Math.floor(pageSize / 2), pageSize, 2 * pageSize] : [];
2048
+ }
2049
+ /** A page or item count with the locale's group separator (`100,000`, or `100.000` under `id`). */
2050
+ function nxePagerCount(value, separators) {
2051
+ return formatNumber(value, parseNumberFormat('#,##0'), separators);
2052
+ }
2053
+
2054
+ /**
2055
+ * A page navigator that stands on its own: page buttons with the grid pager's sliding window,
2056
+ * previous/next, a page-size selector and the total. Use it beside any list, or bind it to the same
2057
+ * state a remote grid pages with.
2058
+ *
2059
+ * ```html
2060
+ * <nxe-paginator [(pageIndex)]="page" [(pageSize)]="size" [totalCount]="orders.totalCount()" />
2061
+ * ```
2062
+ *
2063
+ * Text comes from `NxeMessageService`, so it follows the language. The nav is a labelled
2064
+ * `navigation` landmark; the current page carries `aria-current="page"`. With focus on a page
2065
+ * button, Left/Right move to the previous/next page and Home/End to the first/last.
2066
+ */
2067
+ class NxePaginatorComponent {
2068
+ messages = inject(NxeMessageService);
2069
+ separators = inject(NXE_NUMBER_SEPARATORS);
2070
+ host = inject(ElementRef);
2071
+ injector = inject(Injector);
2072
+ /** The 0-based current page. Two-way. */
2073
+ pageIndex = model(0, /* @ts-ignore */
2074
+ ...(ngDevMode ? [{ debugName: "pageIndex" }] : /* istanbul ignore next */ []));
2075
+ /** Rows per page. Two-way; changing it returns to the first page. */
2076
+ pageSize = model(20, /* @ts-ignore */
2077
+ ...(ngDevMode ? [{ debugName: "pageSize" }] : /* istanbul ignore next */ []));
2078
+ /** Rows across every page. */
2079
+ totalCount = input(0, { ...(ngDevMode ? { debugName: "totalCount" } : /* istanbul ignore next */ {}), transform: numberAttribute });
2080
+ /** The sizes the selector offers. The current size is always offered too. */
2081
+ pageSizes = input([10, 20, 50, 100], /* @ts-ignore */
2082
+ ...(ngDevMode ? [{ debugName: "pageSizes" }] : /* istanbul ignore next */ []));
2083
+ showPageSizeSelector = input(true, { ...(ngDevMode ? { debugName: "showPageSizeSelector" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2084
+ showInfo = input(true, { ...(ngDevMode ? { debugName: "showInfo" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2085
+ /** Greys the controls out and ignores clicks, e.g. while a page loads. */
2086
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2087
+ /** The navigation landmark's accessible name; default is the `grid.pager.navigation` message. */
2088
+ label = input(undefined, /* @ts-ignore */
2089
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
2090
+ t(key, ...args) {
2091
+ return this.messages.t(key, ...args);
2092
+ }
2093
+ pageCount = computed(() => Math.max(1, Math.ceil(Math.max(0, this.totalCount()) / Math.max(1, this.pageSize()))), /* @ts-ignore */
2094
+ ...(ngDevMode ? [{ debugName: "pageCount" }] : /* istanbul ignore next */ []));
2095
+ /** The page shown: `pageIndex` held inside the page count. */
2096
+ currentPage = computed(() => Math.max(0, Math.min(this.pageIndex(), this.pageCount() - 1)), /* @ts-ignore */
2097
+ ...(ngDevMode ? [{ debugName: "currentPage" }] : /* istanbul ignore next */ []));
2098
+ window = linkedSignal({ ...(ngDevMode ? { debugName: "window" } : /* istanbul ignore next */ {}), source: () => ({ count: this.pageCount(), index: this.currentPage() }),
2099
+ computation: (src, prev) => nxePagerWindow(src.count, src.index, prev?.value) });
2100
+ items = computed(() => this.window().items, /* @ts-ignore */
2101
+ ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
2102
+ sizes = computed(() => {
2103
+ const sizes = new Set(this.pageSizes());
2104
+ sizes.add(this.pageSize());
2105
+ return [...sizes].sort((a, b) => a - b);
2106
+ }, /* @ts-ignore */
2107
+ ...(ngDevMode ? [{ debugName: "sizes" }] : /* istanbul ignore next */ []));
2108
+ info = computed(() => this.t('grid.pager.info', nxePagerCount(this.currentPage() + 1, this.separators), nxePagerCount(this.pageCount(), this.separators), nxePagerCount(this.totalCount(), this.separators)), /* @ts-ignore */
2109
+ ...(ngDevMode ? [{ debugName: "info" }] : /* istanbul ignore next */ []));
2110
+ total = computed(() => this.t('grid.pager.total', nxePagerCount(this.totalCount(), this.separators)), /* @ts-ignore */
2111
+ ...(ngDevMode ? [{ debugName: "total" }] : /* istanbul ignore next */ []));
2112
+ go(index) {
2113
+ if (this.disabled()) {
2114
+ return;
2115
+ }
2116
+ const next = Math.max(0, Math.min(index, this.pageCount() - 1));
2117
+ if (next !== this.pageIndex()) {
2118
+ this.pageIndex.set(next);
2119
+ }
2120
+ }
2121
+ onSizeChange(event) {
2122
+ const size = Number(event.target.value);
2123
+ if (size > 0 && size !== this.pageSize()) {
2124
+ this.pageSize.set(size);
2125
+ this.pageIndex.set(0);
2126
+ }
2127
+ }
2128
+ onKeydown(event) {
2129
+ if (!event.target.classList.contains('nxe-paginator-page')) {
2130
+ return;
2131
+ }
2132
+ const last = this.pageCount() - 1;
2133
+ const current = this.currentPage();
2134
+ const target = event.key === 'ArrowLeft'
2135
+ ? current - 1
2136
+ : event.key === 'ArrowRight'
2137
+ ? current + 1
2138
+ : event.key === 'Home'
2139
+ ? 0
2140
+ : event.key === 'End'
2141
+ ? last
2142
+ : null;
2143
+ if (target === null) {
2144
+ return;
2145
+ }
2146
+ event.preventDefault();
2147
+ this.go(target);
2148
+ // The page buttons are re-rendered by key, so the old one keeps focus; move it to the new page.
2149
+ afterNextRender(() => this.host.nativeElement
2150
+ .querySelector('.nxe-paginator-page[aria-current="page"]')
2151
+ ?.focus(), { injector: this.injector });
2152
+ }
2153
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxePaginatorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2154
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxePaginatorComponent, isStandalone: true, selector: "nxe-paginator", inputs: { pageIndex: { classPropertyName: "pageIndex", publicName: "pageIndex", isSignal: true, isRequired: false, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, totalCount: { classPropertyName: "totalCount", publicName: "totalCount", isSignal: true, isRequired: false, transformFunction: null }, pageSizes: { classPropertyName: "pageSizes", publicName: "pageSizes", isSignal: true, isRequired: false, transformFunction: null }, showPageSizeSelector: { classPropertyName: "showPageSizeSelector", publicName: "showPageSizeSelector", isSignal: true, isRequired: false, transformFunction: null }, showInfo: { classPropertyName: "showInfo", publicName: "showInfo", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pageIndex: "pageIndexChange", pageSize: "pageSizeChange" }, host: { listeners: { "keydown": "onKeydown($event)" }, classAttribute: "nxe-paginator" }, ngImport: i0, template: "<nav class=\"nxe-paginator-nav\" [attr.aria-label]=\"label() ?? t('grid.pager.navigation')\" [attr.aria-busy]=\"disabled() ? 'true' : null\">\r\n @if (showPageSizeSelector()) {\r\n <label class=\"nxe-paginator-sizes\">\r\n <span class=\"nxe-paginator-sizes-label\">{{ t('grid.pager.pageSize') }}</span>\r\n <select class=\"nxe-paginator-size\" [disabled]=\"disabled()\" (change)=\"onSizeChange($event)\">\r\n @for (size of sizes(); track size) {\r\n <option [value]=\"size\" [selected]=\"size === pageSize()\">{{ size }}</option>\r\n }\r\n </select>\r\n </label>\r\n }\r\n <span class=\"nxe-paginator-spacer\"></span>\r\n <span class=\"nxe-paginator-info\" role=\"status\">{{ showInfo() ? info() : total() }}</span>\r\n <div class=\"nxe-paginator-pages\">\r\n <button\r\n type=\"button\"\r\n class=\"nxe-paginator-nav-button nxe-paginator-prev\"\r\n [attr.aria-label]=\"t('grid.pager.previous')\"\r\n [attr.aria-disabled]=\"disabled() || currentPage() === 0 ? 'true' : null\"\r\n (click)=\"go(currentPage() - 1)\"></button>\r\n @for (item of items(); track item.key) {\r\n @if (item.separator) {\r\n <span class=\"nxe-paginator-separator\" aria-hidden=\"true\">. . .</span>\r\n } @else {\r\n <button\r\n type=\"button\"\r\n class=\"nxe-paginator-page\"\r\n [class.nxe-paginator-current]=\"item.page === currentPage()\"\r\n [attr.aria-label]=\"t('grid.pager.page', item.page + 1)\"\r\n [attr.aria-current]=\"item.page === currentPage() ? 'page' : null\"\r\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\r\n (click)=\"go(item.page)\">{{ item.page + 1 }}</button>\r\n }\r\n }\r\n <button\r\n type=\"button\"\r\n class=\"nxe-paginator-nav-button nxe-paginator-next\"\r\n [attr.aria-label]=\"t('grid.pager.next')\"\r\n [attr.aria-disabled]=\"disabled() || currentPage() >= pageCount() - 1 ? 'true' : null\"\r\n (click)=\"go(currentPage() + 1)\"></button>\r\n </div>\r\n</nav>\r\n", styles: ["nxe-paginator{display:block}nxe-paginator .nxe-paginator-nav{display:flex;flex-wrap:wrap;align-items:center;gap:var(--nxe-ui-space-2, 8px) var(--nxe-ui-space-3, 12px);color:var(--nxe-ui-text, #333);font-size:var(--nxe-ui-font-size-sm, 13px);line-height:var(--nxe-ui-line-height-sm, 18px)}nxe-paginator .nxe-paginator-spacer{flex:1 1 auto}nxe-paginator .nxe-paginator-sizes{display:inline-flex;align-items:center;gap:var(--nxe-ui-space-2, 8px)}nxe-paginator .nxe-paginator-size{height:32px;padding:0 var(--nxe-ui-space-2, 8px);border:1px solid var(--nxe-ui-border, #ddd);border-radius:var(--nxe-ui-radius, 4px);background:var(--nxe-ui-surface, #fff);color:inherit;font:inherit}nxe-paginator .nxe-paginator-info{color:var(--nxe-ui-text-muted, #999)}nxe-paginator .nxe-paginator-pages{display:inline-flex;align-items:center;gap:var(--nxe-ui-space-1, 4px)}nxe-paginator .nxe-paginator-page,nxe-paginator .nxe-paginator-nav-button{min-width:32px;height:32px;padding:0 var(--nxe-ui-space-2, 8px);border:1px solid transparent;border-radius:var(--nxe-ui-radius, 4px);background:transparent;color:inherit;font:inherit;cursor:pointer}nxe-paginator .nxe-paginator-page:hover,nxe-paginator .nxe-paginator-nav-button:hover{background:var(--nxe-ui-hover-bg, rgba(0, 0, 0, .04))}nxe-paginator .nxe-paginator-page:focus-visible,nxe-paginator .nxe-paginator-nav-button:focus-visible{outline:none;box-shadow:var(--nxe-ui-focus-ring, 0 0 0 2px rgba(0, 92, 170, .5))}nxe-paginator .nxe-paginator-page[aria-disabled=true],nxe-paginator .nxe-paginator-nav-button[aria-disabled=true]{opacity:var(--nxe-ui-disabled-opacity, .5);cursor:default;pointer-events:none}nxe-paginator .nxe-paginator-current{background:var(--nxe-ui-selected-bg, rgba(0, 0, 0, .1));cursor:default}nxe-paginator .nxe-paginator-nav-button{position:relative}nxe-paginator .nxe-paginator-nav-button:before{content:\"\";position:absolute;inset:0;margin:auto;width:8px;height:8px;border-style:solid;border-color:currentColor;border-width:0 0 2px 2px}nxe-paginator .nxe-paginator-prev:before{transform:translate(2px) rotate(45deg)}nxe-paginator .nxe-paginator-next:before{transform:translate(-2px) rotate(-135deg)}nxe-paginator .nxe-paginator-separator{padding:0 var(--nxe-ui-space-1, 4px);opacity:.7}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
2155
+ }
2156
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxePaginatorComponent, decorators: [{
2157
+ type: Component,
2158
+ args: [{ selector: 'nxe-paginator', host: { class: 'nxe-paginator', '(keydown)': 'onKeydown($event)' }, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<nav class=\"nxe-paginator-nav\" [attr.aria-label]=\"label() ?? t('grid.pager.navigation')\" [attr.aria-busy]=\"disabled() ? 'true' : null\">\r\n @if (showPageSizeSelector()) {\r\n <label class=\"nxe-paginator-sizes\">\r\n <span class=\"nxe-paginator-sizes-label\">{{ t('grid.pager.pageSize') }}</span>\r\n <select class=\"nxe-paginator-size\" [disabled]=\"disabled()\" (change)=\"onSizeChange($event)\">\r\n @for (size of sizes(); track size) {\r\n <option [value]=\"size\" [selected]=\"size === pageSize()\">{{ size }}</option>\r\n }\r\n </select>\r\n </label>\r\n }\r\n <span class=\"nxe-paginator-spacer\"></span>\r\n <span class=\"nxe-paginator-info\" role=\"status\">{{ showInfo() ? info() : total() }}</span>\r\n <div class=\"nxe-paginator-pages\">\r\n <button\r\n type=\"button\"\r\n class=\"nxe-paginator-nav-button nxe-paginator-prev\"\r\n [attr.aria-label]=\"t('grid.pager.previous')\"\r\n [attr.aria-disabled]=\"disabled() || currentPage() === 0 ? 'true' : null\"\r\n (click)=\"go(currentPage() - 1)\"></button>\r\n @for (item of items(); track item.key) {\r\n @if (item.separator) {\r\n <span class=\"nxe-paginator-separator\" aria-hidden=\"true\">. . .</span>\r\n } @else {\r\n <button\r\n type=\"button\"\r\n class=\"nxe-paginator-page\"\r\n [class.nxe-paginator-current]=\"item.page === currentPage()\"\r\n [attr.aria-label]=\"t('grid.pager.page', item.page + 1)\"\r\n [attr.aria-current]=\"item.page === currentPage() ? 'page' : null\"\r\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\r\n (click)=\"go(item.page)\">{{ item.page + 1 }}</button>\r\n }\r\n }\r\n <button\r\n type=\"button\"\r\n class=\"nxe-paginator-nav-button nxe-paginator-next\"\r\n [attr.aria-label]=\"t('grid.pager.next')\"\r\n [attr.aria-disabled]=\"disabled() || currentPage() >= pageCount() - 1 ? 'true' : null\"\r\n (click)=\"go(currentPage() + 1)\"></button>\r\n </div>\r\n</nav>\r\n", styles: ["nxe-paginator{display:block}nxe-paginator .nxe-paginator-nav{display:flex;flex-wrap:wrap;align-items:center;gap:var(--nxe-ui-space-2, 8px) var(--nxe-ui-space-3, 12px);color:var(--nxe-ui-text, #333);font-size:var(--nxe-ui-font-size-sm, 13px);line-height:var(--nxe-ui-line-height-sm, 18px)}nxe-paginator .nxe-paginator-spacer{flex:1 1 auto}nxe-paginator .nxe-paginator-sizes{display:inline-flex;align-items:center;gap:var(--nxe-ui-space-2, 8px)}nxe-paginator .nxe-paginator-size{height:32px;padding:0 var(--nxe-ui-space-2, 8px);border:1px solid var(--nxe-ui-border, #ddd);border-radius:var(--nxe-ui-radius, 4px);background:var(--nxe-ui-surface, #fff);color:inherit;font:inherit}nxe-paginator .nxe-paginator-info{color:var(--nxe-ui-text-muted, #999)}nxe-paginator .nxe-paginator-pages{display:inline-flex;align-items:center;gap:var(--nxe-ui-space-1, 4px)}nxe-paginator .nxe-paginator-page,nxe-paginator .nxe-paginator-nav-button{min-width:32px;height:32px;padding:0 var(--nxe-ui-space-2, 8px);border:1px solid transparent;border-radius:var(--nxe-ui-radius, 4px);background:transparent;color:inherit;font:inherit;cursor:pointer}nxe-paginator .nxe-paginator-page:hover,nxe-paginator .nxe-paginator-nav-button:hover{background:var(--nxe-ui-hover-bg, rgba(0, 0, 0, .04))}nxe-paginator .nxe-paginator-page:focus-visible,nxe-paginator .nxe-paginator-nav-button:focus-visible{outline:none;box-shadow:var(--nxe-ui-focus-ring, 0 0 0 2px rgba(0, 92, 170, .5))}nxe-paginator .nxe-paginator-page[aria-disabled=true],nxe-paginator .nxe-paginator-nav-button[aria-disabled=true]{opacity:var(--nxe-ui-disabled-opacity, .5);cursor:default;pointer-events:none}nxe-paginator .nxe-paginator-current{background:var(--nxe-ui-selected-bg, rgba(0, 0, 0, .1));cursor:default}nxe-paginator .nxe-paginator-nav-button{position:relative}nxe-paginator .nxe-paginator-nav-button:before{content:\"\";position:absolute;inset:0;margin:auto;width:8px;height:8px;border-style:solid;border-color:currentColor;border-width:0 0 2px 2px}nxe-paginator .nxe-paginator-prev:before{transform:translate(2px) rotate(45deg)}nxe-paginator .nxe-paginator-next:before{transform:translate(-2px) rotate(-135deg)}nxe-paginator .nxe-paginator-separator{padding:0 var(--nxe-ui-space-1, 4px);opacity:.7}\n"] }]
2159
+ }], propDecorators: { pageIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageIndex", required: false }] }, { type: i0.Output, args: ["pageIndexChange"] }], pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }, { type: i0.Output, args: ["pageSizeChange"] }], totalCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "totalCount", required: false }] }], pageSizes: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSizes", required: false }] }], showPageSizeSelector: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPageSizeSelector", required: false }] }], showInfo: [{ type: i0.Input, args: [{ isSignal: true, alias: "showInfo", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }] } });
2160
+
1503
2161
  class NxeGridResizeController {
1504
2162
  hooks;
1505
2163
  active = null;
@@ -1533,15 +2191,14 @@ class NxeGridResizeController {
1533
2191
  event.stopPropagation(); // the header td above would otherwise start a group drag
1534
2192
  event.preventDefault();
1535
2193
  const host = this.hooks.host();
1536
- const table = host.querySelector('.dx-datagrid-content > .dx-datagrid-table');
1537
- const headerRow = nxeLeafHeaderRow(host);
1538
- if (!table || !headerRow) {
2194
+ const table = host.querySelector('.nxe-datagrid-content > .nxe-datagrid-table');
2195
+ if (!table) {
1539
2196
  return;
1540
2197
  }
1541
2198
  const cols = Array.from(table.querySelectorAll(':scope > colgroup > col'));
1542
- // The colgroup still counts command columns, so it keeps the lead offset; the header row
1543
- // does not, because with bands the command cells span down from the row above.
1544
- const headerCells = nxeHeaderDataCells(headerRow);
2199
+ // The colgroup still counts command columns, so it keeps the lead offset; the header
2200
+ // cells do not, because they are gathered by column rather than by cell position.
2201
+ const headerCells = nxeHeaderLeafCells(host);
1545
2202
  const colEl = cols[lead + visibleIndex];
1546
2203
  const cell = headerCells[visibleIndex];
1547
2204
  if (!colEl || !cell) {
@@ -1549,6 +2206,17 @@ class NxeGridResizeController {
1549
2206
  }
1550
2207
  const nextColEl = nextField != null ? cols[lead + visibleIndex + 1] ?? null : null;
1551
2208
  const nextCell = nextField != null ? headerCells[visibleIndex + 1] : null;
2209
+ const trailingColEl = cols[lead + headerCells.length - 1];
2210
+ const trailingCell = headerCells[headerCells.length - 1];
2211
+ const rowsView = table.closest('.nxe-datagrid-rowsview');
2212
+ const fill = this.hooks.mode() === 'widget' && this.hooks.fillsGrid() && trailingColEl && trailingCell && rowsView
2213
+ ? {
2214
+ colEl: trailingColEl,
2215
+ startW: trailingCell.getBoundingClientRect().width,
2216
+ prevStyle: trailingColEl.style.width,
2217
+ roomW: rowsView.clientWidth,
2218
+ }
2219
+ : null;
1552
2220
  this.active = {
1553
2221
  field,
1554
2222
  nextField,
@@ -1565,6 +2233,7 @@ class NxeGridResizeController {
1565
2233
  prevTableStyle: table.style.width,
1566
2234
  lastW: cell.getBoundingClientRect().width,
1567
2235
  lastNextW: nextCell ? nextCell.getBoundingClientRect().width : 0,
2236
+ fill,
1568
2237
  };
1569
2238
  this.hooks.onStart();
1570
2239
  document.body.classList.add('nxe-grid-resizing');
@@ -1591,9 +2260,22 @@ class NxeGridResizeController {
1591
2260
  const min = this.hooks.minWidth();
1592
2261
  const delta = e.clientX - a.startX;
1593
2262
  if (this.hooks.mode() === 'widget' || !a.nextColEl) {
1594
- const w = Math.max(min, a.startW + delta);
2263
+ let w = Math.max(min, a.startW + delta);
2264
+ let tableW = a.startTableW + (w - a.startW);
2265
+ // fillsGrid: hand what the table comes out short of the rows view to the trailing
2266
+ // column now, as the commit will, so the release lands where the drag already is.
2267
+ if (a.fill) {
2268
+ const slack = Math.max(0, a.fill.roomW - tableW);
2269
+ if (a.fill.colEl === a.colEl) {
2270
+ w += slack;
2271
+ }
2272
+ else {
2273
+ a.fill.colEl.style.width = `${a.fill.startW + slack}px`;
2274
+ }
2275
+ tableW += slack;
2276
+ }
1595
2277
  a.colEl.style.width = `${w}px`;
1596
- a.tableEl.style.width = `${a.startTableW + (w - a.startW)}px`;
2278
+ a.tableEl.style.width = `${tableW}px`;
1597
2279
  a.lastW = w;
1598
2280
  return;
1599
2281
  }
@@ -1627,7 +2309,25 @@ class NxeGridResizeController {
1627
2309
  if (a.nextField && a.lastNextW !== a.startNextW) {
1628
2310
  changes.push({ field: a.nextField, from: a.startNextW, to: a.lastNextW });
1629
2311
  }
1630
- this.hooks.zone.run(() => this.hooks.onCommit(changes));
2312
+ this.hooks.zone.run(() => {
2313
+ // The commit measures the cells as the drag left them, so it goes first.
2314
+ this.hooks.onCommit(changes);
2315
+ // Then the DOM goes back to what the [style.width] bindings last wrote. Angular only
2316
+ // rewrites a binding whose value changed, and a second resize often commits the same
2317
+ // value (the trailing column's width, a table at 100%): the drag's leftovers would stay
2318
+ // on screen. Restored, the next render lands the committed layout either way.
2319
+ this.restore(a);
2320
+ });
2321
+ }
2322
+ restore(a) {
2323
+ a.colEl.style.width = a.prevColStyle;
2324
+ if (a.nextColEl) {
2325
+ a.nextColEl.style.width = a.prevNextStyle;
2326
+ }
2327
+ if (a.fill && a.fill.colEl !== a.colEl) {
2328
+ a.fill.colEl.style.width = a.fill.prevStyle;
2329
+ }
2330
+ a.tableEl.style.width = a.prevTableStyle;
1631
2331
  }
1632
2332
  cancel() {
1633
2333
  const a = this.active;
@@ -1636,11 +2336,7 @@ class NxeGridResizeController {
1636
2336
  return;
1637
2337
  }
1638
2338
  this.suppression.arm();
1639
- a.colEl.style.width = a.prevColStyle;
1640
- if (a.nextColEl) {
1641
- a.nextColEl.style.width = a.prevNextStyle;
1642
- }
1643
- a.tableEl.style.width = a.prevTableStyle;
2339
+ this.restore(a);
1644
2340
  this.hooks.zone.run(() => this.hooks.onCancel());
1645
2341
  }
1646
2342
  teardown() {
@@ -1654,25 +2350,56 @@ class NxeGridResizeController {
1654
2350
  }
1655
2351
 
1656
2352
  /* eslint-disable @typescript-eslint/no-explicit-any */
2353
+ /** The day after a `yyyy-MM-dd` day key. */
2354
+ function nxeNextDay(day) {
2355
+ return DateTime.fromISO(day).plus({ days: 1 }).toFormat('yyyy-MM-dd');
2356
+ }
2357
+ /**
2358
+ * Recursive tree selection state per node key: `all` when the node's key or an ancestor's is in
2359
+ * `keys`, or when every child is `all`; `some` when only part of its subtree is; absent otherwise.
2360
+ */
2361
+ function nxeTreeCheckState(roots, keys) {
2362
+ const out = new Map();
2363
+ const walk = (node, forced) => {
2364
+ const self = forced || keys.has(node.key);
2365
+ let state = self ? 'all' : 'none';
2366
+ if (node.children.length) {
2367
+ let every = true;
2368
+ let any = false;
2369
+ for (const child of node.children) {
2370
+ const s = walk(child, self);
2371
+ every &&= s === 'all';
2372
+ any ||= s !== 'none';
2373
+ }
2374
+ state = every ? 'all' : any ? 'some' : 'none';
2375
+ }
2376
+ if (state !== 'none') {
2377
+ out.set(node.key, state);
2378
+ }
2379
+ return state;
2380
+ };
2381
+ roots.forEach((r) => walk(r, false));
2382
+ return out;
2383
+ }
1657
2384
  /** Classify a rendered `<tr>` for dx's `rowType`, or null for rows dx does not report. */
1658
2385
  function nxeRowTypeOf(tr) {
1659
2386
  const c = tr.classList;
1660
- if (c.contains('dx-header-row')) {
2387
+ if (c.contains('nxe-header-row')) {
1661
2388
  return 'header';
1662
2389
  }
1663
- if (c.contains('dx-datagrid-filter-row')) {
2390
+ if (c.contains('nxe-datagrid-filter-row')) {
1664
2391
  return 'filter';
1665
2392
  }
1666
- if (c.contains('dx-data-row')) {
2393
+ if (c.contains('nxe-data-row')) {
1667
2394
  return 'data';
1668
2395
  }
1669
- if (c.contains('dx-group-footer-row')) {
2396
+ if (c.contains('nxe-group-footer-row')) {
1670
2397
  return 'groupFooter';
1671
2398
  }
1672
- if (c.contains('dx-group-row')) {
2399
+ if (c.contains('nxe-group-row')) {
1673
2400
  return 'group';
1674
2401
  }
1675
- if (tr.parentElement?.classList.contains('dx-datagrid-total-footer')) {
2402
+ if (tr.parentElement?.classList.contains('nxe-datagrid-total-footer')) {
1676
2403
  return 'totalFooter';
1677
2404
  }
1678
2405
  return null;
@@ -1690,13 +2417,30 @@ function flattenLeafColumns(cols) {
1690
2417
  }
1691
2418
  return out;
1692
2419
  }
2420
+ /**
2421
+ * A row's side of a collapsed border: the widest top or bottom border across the `<tr>` and its
2422
+ * cells, as computed (a `none` border computes to 0). Null, or anything but a row, has none.
2423
+ */
2424
+ function nxeRowEdge(row, side) {
2425
+ if (!(row instanceof HTMLTableRowElement)) {
2426
+ return 0;
2427
+ }
2428
+ const prop = `border-${side}-width`;
2429
+ let width = parseFloat(getComputedStyle(row).getPropertyValue(prop)) || 0;
2430
+ for (const cell of Array.from(row.cells)) {
2431
+ width = Math.max(width, parseFloat(getComputedStyle(cell).getPropertyValue(prop)) || 0);
2432
+ }
2433
+ return width;
2434
+ }
1693
2435
  /**
1694
2436
  * `nxe-data-grid` — drop-in replacement for DevExtreme `dx-data-grid` (read-only core, slice 9a).
1695
2437
  *
1696
- * Renders a single `<table>` emitting the dx DOM-class contract (`dx-widget dx-datagrid`,
1697
- * `dx-row`/`dx-header-row`/`dx-data-row`/`dx-datagrid-headers`/`dx-datagrid-rowsview`, `td`s) so
1698
- * the globally-loaded `dx.light.css` + repo `_grid.scss` overrides style it pixel-for-pixel
1699
- * (4px cell padding, #f8fafc header, #525252 header text, row/hover/alt colours). Columns are
2438
+ * Renders a single `<table>` under the library's own class namespace (`nxe-widget nxe-datagrid`,
2439
+ * `nxe-row`/`nxe-header-row`/`nxe-data-row`/`nxe-datagrid-headers`/`nxe-datagrid-rowsview`, `td`s).
2440
+ * The structure still mirrors dx's, name for name, so the visual contract carries over
2441
+ * (4px cell padding, #f8fafc header, #525252 header text, row/hover/alt colours) — but the
2442
+ * `dx-` prefix is gone; `provideNxeDxCompat()` re-emits it for stylesheets not yet migrated.
2443
+ * Columns are
1700
2444
  * collected from `nxe-column` content children; named `cellTemplate`s resolve to sibling
1701
2445
  * `<ng-template nxeTemplate="name">` blocks rendered with `{ data, rowIndex, columnIndex, ... }`
1702
2446
  * (the live row object, so in-cell `nxe-*` editors can write through `key.data.<field>`).
@@ -1710,7 +2454,15 @@ class NxeDataGridComponent {
1710
2454
  overlay = inject(Overlay);
1711
2455
  viewContainerRef = inject(ViewContainerRef);
1712
2456
  ngZone = inject(NgZone);
2457
+ destroyRef = inject(DestroyRef);
1713
2458
  defaults = inject(NXE_DEFAULT_OPTIONS);
2459
+ /** Locale separators for every number this grid renders — cells, summaries and group text. */
2460
+ separators = inject(NXE_NUMBER_SEPARATORS);
2461
+ messages = inject(NxeMessageService);
2462
+ /** One library message for the active language; reading it in a template re-renders on a switch. */
2463
+ t(key, ...args) {
2464
+ return this.messages.t(key, ...args);
2465
+ }
1714
2466
  /** Pointer drag engine: drag-to-group, chip drag-out (ungroup), chip/column reorder. */
1715
2467
  drag = new NxeGridDragController({
1716
2468
  zone: this.ngZone,
@@ -1720,13 +2472,14 @@ class NxeDataGridComponent {
1720
2472
  onChipMove: (from, to) => this.moveGroup(from, to),
1721
2473
  reorderEnabled: () => this.allowColumnReordering(),
1722
2474
  onColumnMove: (from, slot) => this.moveColumnByVisible(from, slot),
1723
- headerDataCells: () => nxeHeaderDataCells(nxeLeafHeaderRow(this.elementRef.nativeElement)),
2475
+ headerDataCells: () => nxeHeaderLeafCells(this.elementRef.nativeElement),
1724
2476
  });
1725
2477
  /** Pointer resize engine (dx allowColumnResizing). */
1726
2478
  resize = new NxeGridResizeController({
1727
2479
  zone: this.ngZone,
1728
2480
  host: () => this.elementRef.nativeElement,
1729
2481
  mode: () => this.columnResizingMode(),
2482
+ fillsGrid: () => this.resizeFillsGrid(),
1730
2483
  minWidth: () => this.columnMinWidth() ?? 50,
1731
2484
  onStart: () => (this.resizeActive = true),
1732
2485
  onCancel: () => (this.resizeActive = false),
@@ -1773,6 +2526,11 @@ class NxeDataGridComponent {
1773
2526
  masterDetail = contentChild(NxeMasterDetailComponent, /* @ts-ignore */
1774
2527
  ...(ngDevMode ? [{ debugName: "masterDetail" }] : /* istanbul ignore next */ []));
1775
2528
  // --- Inputs (mirror dx-data-grid; object-literal inputs accepted as-is for drop-in parity) ---
2529
+ /**
2530
+ * The rows: a plain array (paged, sorted and filtered here), a {@link NxeRemoteDataSource}
2531
+ * (paged, sorted and filtered by the server; the grid holds one page), or, with
2532
+ * `remoteOperations` on, a `CustomStore`-shaped `{ load, key? }`.
2533
+ */
1776
2534
  dataSource = input([], /* @ts-ignore */
1777
2535
  ...(ngDevMode ? [{ debugName: "dataSource" }] : /* istanbul ignore next */ []));
1778
2536
  keyExpr = input(undefined, /* @ts-ignore */
@@ -1791,25 +2549,68 @@ class NxeDataGridComponent {
1791
2549
  wordWrapEnabledInput = input(undefined, { ...(ngDevMode ? { debugName: "wordWrapEnabledInput" } : /* istanbul ignore next */ {}), alias: 'wordWrapEnabled' });
1792
2550
  columnAutoWidthInput = input(undefined, { ...(ngDevMode ? { debugName: "columnAutoWidthInput" } : /* istanbul ignore next */ {}), alias: 'columnAutoWidth' });
1793
2551
  rowAlternationEnabledInput = input(undefined, { ...(ngDevMode ? { debugName: "rowAlternationEnabledInput" } : /* istanbul ignore next */ {}), alias: 'rowAlternationEnabled' });
2552
+ resizeFillsGridInput = input(undefined, { ...(ngDevMode ? { debugName: "resizeFillsGridInput" } : /* istanbul ignore next */ {}), alias: 'resizeFillsGrid' });
1794
2553
  /** dx resolves `nxe-tree-list` against the `treeList` slice, not `dataGrid`. */
1795
2554
  widgetDefaults = computed(() => (this.treeMode ? this.defaults.treeList : this.defaults.dataGrid) ?? {}, /* @ts-ignore */
1796
2555
  ...(ngDevMode ? [{ debugName: "widgetDefaults" }] : /* istanbul ignore next */ []));
1797
2556
  hoverStateEnabled = computed(() => this.hoverStateEnabledInput() ?? this.widgetDefaults().hoverStateEnabled ?? false, /* @ts-ignore */
1798
2557
  ...(ngDevMode ? [{ debugName: "hoverStateEnabled" }] : /* istanbul ignore next */ []));
1799
- showColumnLines = computed(() => this.showColumnLinesInput() ?? this.widgetDefaults().showColumnLines ?? false, /* @ts-ignore */
2558
+ /**
2559
+ * Widget-mode resizing that never leaves a blank strip: the trailing column takes whatever a
2560
+ * narrowed layout leaves of the grid's width. Not dx - dx lets the table end short - so off
2561
+ * unless bound or set app-wide through `NXE_DEFAULT_OPTIONS`.
2562
+ */
2563
+ resizeFillsGrid = computed(() => this.resizeFillsGridInput() ?? this.widgetDefaults().resizeFillsGrid ?? false, /* @ts-ignore */
2564
+ ...(ngDevMode ? [{ debugName: "resizeFillsGrid" }] : /* istanbul ignore next */ []));
2565
+ // The literals are DevExtreme 24.2.3's generic defaults: showColumnLines true, showRowLines
2566
+ // false and wordWrapEnabled false (m_rows_view.js:1180-1182), showBorders false
2567
+ // (m_grid_view.js:743). dxTreeList shares them and changes them only for the Material themes
2568
+ // (tree_list/m_widget_base.js:39-52), so both hosts use the same literals. The library shipped
2569
+ // column lines off and row lines and wrapping on, the reverse of dx, so every grid that bound
2570
+ // neither option drew the wrong lines.
2571
+ showColumnLines = computed(() => this.showColumnLinesInput() ?? this.widgetDefaults().showColumnLines ?? true, /* @ts-ignore */
1800
2572
  ...(ngDevMode ? [{ debugName: "showColumnLines" }] : /* istanbul ignore next */ []));
1801
- showRowLines = computed(() => this.showRowLinesInput() ?? this.widgetDefaults().showRowLines ?? true, /* @ts-ignore */
2573
+ showRowLines = computed(() => this.showRowLinesInput() ?? this.widgetDefaults().showRowLines ?? false, /* @ts-ignore */
1802
2574
  ...(ngDevMode ? [{ debugName: "showRowLines" }] : /* istanbul ignore next */ []));
1803
2575
  showBorders = computed(() => this.showBordersInput() ?? this.widgetDefaults().showBorders ?? false, /* @ts-ignore */
1804
2576
  ...(ngDevMode ? [{ debugName: "showBorders" }] : /* istanbul ignore next */ []));
1805
- wordWrapEnabled = computed(() => this.wordWrapEnabledInput() ?? this.widgetDefaults().wordWrapEnabled ?? true, /* @ts-ignore */
2577
+ wordWrapEnabled = computed(() => this.wordWrapEnabledInput() ?? this.widgetDefaults().wordWrapEnabled ?? false, /* @ts-ignore */
1806
2578
  ...(ngDevMode ? [{ debugName: "wordWrapEnabled" }] : /* istanbul ignore next */ []));
1807
2579
  columnAutoWidth = computed(() => this.columnAutoWidthInput() ?? this.widgetDefaults().columnAutoWidth ?? false, /* @ts-ignore */
1808
2580
  ...(ngDevMode ? [{ debugName: "columnAutoWidth" }] : /* istanbul ignore next */ []));
1809
2581
  rowAlternationEnabled = computed(() => this.rowAlternationEnabledInput() ?? this.widgetDefaults().rowAlternationEnabled ?? false, /* @ts-ignore */
1810
2582
  ...(ngDevMode ? [{ debugName: "rowAlternationEnabled" }] : /* istanbul ignore next */ []));
2583
+ /**
2584
+ * dx `remoteOperations`. With a `{ load }` store as the `dataSource`, any truthy value makes the
2585
+ * grid page, sort and filter on the server (`{ paging, sorting, filtering }` is read as all or
2586
+ * nothing). A {@link NxeRemoteDataSource} is remote whatever this says.
2587
+ */
1811
2588
  remoteOperations = input(false, /* @ts-ignore */
1812
2589
  ...(ngDevMode ? [{ debugName: "remoteOperations" }] : /* istanbul ignore next */ []));
2590
+ // Controlled state, for binding to URL query params. `undefined` = not controlled: the grid
2591
+ // keeps its own state. A remote grid always writes them back, so `[(pageIndex)]` follows the
2592
+ // user; a client-side grid writes back only the ones that are bound.
2593
+ /** The 0-based page. Two-way. */
2594
+ pageIndexModel = model(undefined, { ...(ngDevMode ? { debugName: "pageIndexModel" } : /* istanbul ignore next */ {}), alias: 'pageIndex' });
2595
+ /** Rows per page. Two-way. */
2596
+ pageSizeModel = model(undefined, { ...(ngDevMode ? { debugName: "pageSizeModel" } : /* istanbul ignore next */ {}), alias: 'pageSize' });
2597
+ /** The sort, `[{ selector, desc }]`. Two-way; a header click writes its single level here. */
2598
+ sortModel = model(undefined, { ...(ngDevMode ? { debugName: "sortModel" } : /* istanbul ignore next */ {}), alias: 'sort' });
2599
+ /**
2600
+ * A filter the application owns (a filter bar, a status tab), in {@link NxeFilterExpression}
2601
+ * form. A remote grid ANDs it with the filter row and header filter and sends the lot to the
2602
+ * server. Changing it returns the grid to the first page.
2603
+ */
2604
+ // The annotation is explicit on purpose: the library compiles without strictNullChecks, which erases
2605
+ // `| null | undefined` from an inferred type, so the built .d.ts would otherwise say non-null.
2606
+ filter = input(undefined, /* @ts-ignore */
2607
+ ...(ngDevMode ? [{ debugName: "filter" }] : /* istanbul ignore next */ []));
2608
+ /**
2609
+ * The options of every request a remote grid hands its source: page, size, sort and the
2610
+ * combined filter. One emission per distinct request, after filter-row typing settles. This is
2611
+ * where an application mirrors the grid's state into the URL.
2612
+ */
2613
+ optionsChange = output();
1813
2614
  /**
1814
2615
  * dx `syncLookupFilterValues` — accepted, and deliberately inert. It asks dx to narrow a
1815
2616
  * lookup column's header-filter list to the values still reachable under the other filters.
@@ -1821,8 +2622,19 @@ class NxeDataGridComponent {
1821
2622
  ...(ngDevMode ? [{ debugName: "syncLookupFilterValues" }] : /* istanbul ignore next */ []));
1822
2623
  columnMinWidth = input(undefined, /* @ts-ignore */
1823
2624
  ...(ngDevMode ? [{ debugName: "columnMinWidth" }] : /* istanbul ignore next */ []));
2625
+ /** dx `height` — a bare number means px, anything else passes through. */
2626
+ height = input(undefined, /* @ts-ignore */
2627
+ ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
2628
+ heightStyle = computed(() => {
2629
+ const h = this.height();
2630
+ if (h === undefined || h === null || h === '') {
2631
+ return null;
2632
+ }
2633
+ return /^-?\d+(\.\d+)?$/.test(String(h)) ? `${h}px` : String(h);
2634
+ }, /* @ts-ignore */
2635
+ ...(ngDevMode ? [{ debugName: "heightStyle" }] : /* istanbul ignore next */ []));
1824
2636
  noDataTextInput = input(undefined, { ...(ngDevMode ? { debugName: "noDataTextInput" } : /* istanbul ignore next */ {}), alias: 'noDataText' });
1825
- noDataText = computed(() => this.noDataTextInput() ?? this.widgetDefaults().noDataText ?? 'No data', /* @ts-ignore */
2637
+ noDataText = computed(() => this.noDataTextInput() ?? this.widgetDefaults().noDataText ?? this.messages.t('grid.noData'), /* @ts-ignore */
1826
2638
  ...(ngDevMode ? [{ debugName: "noDataText" }] : /* istanbul ignore next */ []));
1827
2639
  loadPanel = input(undefined, /* @ts-ignore */
1828
2640
  ...(ngDevMode ? [{ debugName: "loadPanel" }] : /* istanbul ignore next */ []));
@@ -1836,6 +2648,10 @@ class NxeDataGridComponent {
1836
2648
  groupPanelOptions = input(undefined, { ...(ngDevMode ? { debugName: "groupPanelOptions" } : /* istanbul ignore next */ {}), alias: 'groupPanel' });
1837
2649
  scrolling = input(undefined, /* @ts-ignore */
1838
2650
  ...(ngDevMode ? [{ debugName: "scrolling" }] : /* istanbul ignore next */ []));
2651
+ /**
2652
+ * dx `sorting`. Unbound means dx's default mode 'single'. `false` and `null` are accepted as dx
2653
+ * reads them - no mode, so headers keep their sort slot but a click never sorts.
2654
+ */
1839
2655
  sorting = input(undefined, /* @ts-ignore */
1840
2656
  ...(ngDevMode ? [{ debugName: "sorting" }] : /* istanbul ignore next */ []));
1841
2657
  allowColumnResizing = input(false, /* @ts-ignore */
@@ -1934,26 +2750,56 @@ class NxeDataGridComponent {
1934
2750
  */
1935
2751
  hf = new NxeHeaderFilterModel({
1936
2752
  rows: () => this.hfBaseRows(),
2753
+ fixedItems: (col) => {
2754
+ const items = col.headerFilterConfig()?.dataSource();
2755
+ // dx uses a supplied list verbatim: declared order, no (Blanks), and values that do
2756
+ // not occur in the data still offered - which is the reason to supply one.
2757
+ return items
2758
+ ? items.map((i) => ({ key: i.value, text: String(i.text ?? i.value ?? '') }))
2759
+ : null;
2760
+ },
1937
2761
  rawValue: (col, row) => this.getRaw(row, col.dataField()),
1938
2762
  displayText: (col, row) => this.cellText(col, row),
1939
- isDate: (col) => col.dataType() === 'date',
2763
+ isDate: (col) => this.colType(col) === 'date',
1940
2764
  sortByDisplayText: (col) => !!col.lookup(),
1941
2765
  boolLabel: (col, raw) => this.hfBoolLabel(col, raw),
1942
2766
  compare: (a, b) => this.compare(a, b),
2767
+ }, {
2768
+ blanks: () => this.messages.t('common.blanks'),
2769
+ yes: () => this.messages.t('common.yes'),
2770
+ no: () => this.messages.t('common.no'),
2771
+ month: (month) => Info.months('long', { locale: this.messages.locale() })[month - 1],
1943
2772
  });
1944
2773
  hfColumn = this.hf.column;
1945
2774
  hfDraft = this.hf.draft;
1946
2775
  hfOverlayRef;
1947
- // Internal paging (dxo-paging present) working state
1948
- pageIndex = signal(0, /* @ts-ignore */
2776
+ /**
2777
+ * The current page.
2778
+ *
2779
+ * A `linkedSignal` over `nxe-paging`'s own `pageIndex` so the binding is genuinely two-way:
2780
+ * a host that sets `[(pageIndex)]` moves the grid, and `goToPage` writes back. Declaring the
2781
+ * input without this would satisfy an audit while doing nothing — the option would resolve
2782
+ * and be ignored, which is worse than not accepting it.
2783
+ */
2784
+ pageIndex = linkedSignal(() => {
2785
+ const remote = this.remote();
2786
+ return (this.pageIndexModel() ??
2787
+ this.pagingCmp()?.pageIndex() ??
2788
+ (remote ? untracked(() => remote.options().pageIndex) : 0));
2789
+ }, /* @ts-ignore */
1949
2790
  ...(ngDevMode ? [{ debugName: "pageIndex" }] : /* istanbul ignore next */ []));
1950
- userPageSize = signal(null, /* @ts-ignore */
2791
+ userPageSize = linkedSignal(() => this.pageSizeModel() ?? null, /* @ts-ignore */
1951
2792
  ...(ngDevMode ? [{ debugName: "userPageSize" }] : /* istanbul ignore next */ []));
1952
2793
  /** DevExtreme `.instance` facade — `#gridAll.instance.<method>()`. */
1953
2794
  facade = {
1954
2795
  getSelectedRowsData: () => this.selectedRowsData(),
1955
- getSelectedRowKeys: () => this.rowKeys(this.selectedRowsData()),
1956
- refresh: () => this.refreshTick.update((v) => v + 1),
2796
+ getSelectedRowKeys: () => (this.remote() ? this.selectedRowKeys() : this.rowKeys(this.selectedRowsData())),
2797
+ clearSelection: () => this.deselectRows(null),
2798
+ deselectAll: () => this.deselectRows(this.treeMode ? null : this.displayRows()),
2799
+ refresh: () => {
2800
+ this.refreshTick.update((v) => v + 1);
2801
+ this.remote()?.reload();
2802
+ },
1957
2803
  clearFilter: (filterName) => {
1958
2804
  if (!filterName || filterName === 'row') {
1959
2805
  this.filterRowState.set(new Map());
@@ -1975,14 +2821,18 @@ class NxeDataGridComponent {
1975
2821
  this.setColumnVisible(field, value);
1976
2822
  }
1977
2823
  },
1978
- getVisibleColumns: () => this.visibleColumns().map((c) => ({
1979
- caption: c.caption(),
1980
- dataField: c.dataField(),
1981
- dataType: c.dataType(),
1982
- format: c.format(),
1983
- })),
2824
+ getVisibleColumns: () => this.visibleColumns().map((c) => {
2825
+ const type = this.colType(c);
2826
+ return {
2827
+ caption: c.caption(),
2828
+ dataField: c.dataField(),
2829
+ // An inferred 'object' column exports as text, as an untyped one always did.
2830
+ dataType: type === 'object' ? 'string' : type,
2831
+ format: c.format(),
2832
+ };
2833
+ }),
1984
2834
  getVisibleRows: () => this.displayRows(),
1985
- totalCount: () => this.displayRows().length,
2835
+ totalCount: () => this.remote()?.totalCount() ?? this.displayRows().length,
1986
2836
  getRootNode: () => ({
1987
2837
  key: undefined,
1988
2838
  data: undefined,
@@ -1998,7 +2848,7 @@ class NxeDataGridComponent {
1998
2848
  if (this.treeMode) {
1999
2849
  const keyF = this.keyExpr() ?? 'id';
2000
2850
  const all = new Map();
2001
- for (const r of (this.dataSource() ?? [])) {
2851
+ for (const r of this.sourceRows()) {
2002
2852
  all.set(r[keyF], true);
2003
2853
  }
2004
2854
  this.expandOverride.set(all);
@@ -2008,7 +2858,7 @@ class NxeDataGridComponent {
2008
2858
  const editing = this.editingCmp();
2009
2859
  const key = editing?.editRowKey();
2010
2860
  if (editing && key != null) {
2011
- const row = (this.dataSource() ?? []).find((r) => this.rowKey(r) === key);
2861
+ const row = this.sourceRows().find((r) => this.rowKey(r) === key);
2012
2862
  if (row) {
2013
2863
  this.saveRowEdit(row, new Event('save'));
2014
2864
  }
@@ -2049,12 +2899,32 @@ class NxeDataGridComponent {
2049
2899
  /** Guards against a handler that schedules another render re-entering the walk. */
2050
2900
  preparedDepth = 0;
2051
2901
  constructor() {
2902
+ // Server-side data: the request is a computed over exactly the state it is made of, and
2903
+ // this stream (explicitly those signals, nothing a callee reads) hands each distinct one to
2904
+ // the source, waiting out filter-row typing first.
2905
+ toObservable(this.remoteRequest)
2906
+ .pipe(debounce(() => {
2907
+ if (!this.remoteTyping) {
2908
+ return of(0);
2909
+ }
2910
+ this.remoteTyping = false;
2911
+ return timer(this.remote()?.filterDebounce ?? 0);
2912
+ }), takeUntilDestroyed(this.destroyRef))
2913
+ .subscribe((request) => request && this.dispatchRemote(request));
2052
2914
  // Registered before onContentReady so cells are prepared by the time it fires, as in dx.
2053
2915
  afterRenderEffect(() => {
2054
2916
  this.renderRows();
2055
2917
  this.visibleColumns();
2056
2918
  untracked(() => this.emitPreparedEvents());
2057
2919
  });
2920
+ // A cellTemplate that draws a check box centres its column, header and cells, the way a
2921
+ // boolean column is centred. Only the rendered cells can say what a template draws, so
2922
+ // this reads them after render; the DOM walk is untracked, like emitPreparedEvents.
2923
+ afterRenderEffect(() => {
2924
+ this.renderRows();
2925
+ this.visibleColumns();
2926
+ untracked(() => this.detectTemplateCheckboxColumns());
2927
+ });
2058
2928
  // dx onContentReady parity: fire after the rendered row set reaches the DOM.
2059
2929
  afterRenderEffect(() => {
2060
2930
  this.renderRows();
@@ -2072,7 +2942,7 @@ class NxeDataGridComponent {
2072
2942
  untracked(() => this.scheduleAutoWidthMeasure());
2073
2943
  });
2074
2944
  afterNextRender(() => {
2075
- const rv = this.elementRef.nativeElement.querySelector('.dx-datagrid-rowsview');
2945
+ const rv = this.elementRef.nativeElement.querySelector('.nxe-datagrid-rowsview');
2076
2946
  if (rv && typeof ResizeObserver !== 'undefined') {
2077
2947
  this.resizeObserver = new ResizeObserver((entries) => {
2078
2948
  const w = Math.round(entries[0]?.contentRect?.width ?? 0);
@@ -2083,6 +2953,22 @@ class NxeDataGridComponent {
2083
2953
  this.resizeObserver.observe(rv);
2084
2954
  }
2085
2955
  });
2956
+ // The free-space row: re-read whenever the rows view (the grid's height) or its table (the
2957
+ // rows) changes size. The paged case needs no reading beyond the row height, so a page
2958
+ // change that keeps the table's height still resolves through `freeSpace` alone.
2959
+ afterNextRender(() => {
2960
+ const host = this.elementRef.nativeElement;
2961
+ // This grid's own elements: a nested master-detail grid's come later in the document.
2962
+ const rv = host.querySelector('.nxe-datagrid-rowsview');
2963
+ const table = rv?.querySelector(':scope > .nxe-datagrid-content > .nxe-datagrid-table');
2964
+ if (!rv || !table || typeof ResizeObserver === 'undefined') {
2965
+ return;
2966
+ }
2967
+ const observer = new ResizeObserver(() => this.measureFreeSpace(rv, table));
2968
+ observer.observe(rv);
2969
+ observer.observe(table);
2970
+ this.destroyRef.onDestroy(() => observer.disconnect());
2971
+ });
2086
2972
  }
2087
2973
  // --- Column auto-width engine (dx columnAutoWidth parity) ----------------------------------
2088
2974
  autoWidths = signal(null, /* @ts-ignore */
@@ -2113,17 +2999,16 @@ class NxeDataGridComponent {
2113
2999
  }
2114
3000
  measureAutoWidths() {
2115
3001
  const host = this.elementRef.nativeElement;
2116
- const table = host.querySelector('.dx-datagrid-content > .dx-datagrid-table');
2117
- const rowsview = host.querySelector('.dx-datagrid-rowsview');
2118
- const headerRow = nxeLeafHeaderRow(host);
2119
- if (!table || !rowsview || !headerRow) {
3002
+ const table = host.querySelector('.nxe-datagrid-content > .nxe-datagrid-table');
3003
+ const rowsview = host.querySelector('.nxe-datagrid-rowsview');
3004
+ if (!table || !rowsview) {
2120
3005
  return;
2121
3006
  }
2122
3007
  const cols = this.visibleColumns();
2123
- const lead = this.groupLevelCount() + (this.masterDetailEnabled() ? 1 : 0) + (this.showCheckboxes() ? 1 : 0);
2124
- // Indexed among data cells, not by a lead offset: command cells live in the FIRST header
2125
- // row and span down, so a banded grid's leaf row has none of them.
2126
- const cells = nxeHeaderDataCells(headerRow);
3008
+ const lead = this.groupLevelCount() + (this.masterDetailEnabled() ? 1 : 0) + (this.selectColumnVisible() ? 1 : 0);
3009
+ // One cell per visible column, gathered across the header rows: with bands a top-level
3010
+ // column sits in the first row and spans down, so no single row holds them all.
3011
+ const cells = nxeHeaderLeafCells(host);
2127
3012
  const container = rowsview.clientWidth;
2128
3013
  if (cells.length !== cols.length || container <= 0) {
2129
3014
  return;
@@ -2134,10 +3019,18 @@ class NxeDataGridComponent {
2134
3019
  const prevLayout = table.style.tableLayout;
2135
3020
  const prevWidth = table.style.width;
2136
3021
  const prevColWidths = colEls.map((c) => c.style.width);
3022
+ // dx's best-fit pass moves the header tbody into the rows table as `dx-header
3023
+ // dx-datagrid-best-fit` (m_grid_view.js:187-206). Its rules unfloat the indicator block and
3024
+ // show every `dx-sort-none` placeholder at 14px, so each sortable column measures 17px wider
3025
+ // (34px centred, with the hidden mirror) whether sorted or not - and sorting never changes a
3026
+ // width. The class goes on our thead, which is that tbody's counterpart.
3027
+ const thead = table.querySelector(':scope > thead');
3028
+ thead?.classList.add('nxe-datagrid-best-fit');
2137
3029
  table.style.tableLayout = 'auto';
2138
3030
  table.style.width = 'max-content';
2139
3031
  colEls.forEach((c) => (c.style.width = ''));
2140
3032
  const measured = cells.map((c) => c.getBoundingClientRect().width);
3033
+ thead?.classList.remove('nxe-datagrid-best-fit');
2141
3034
  table.style.tableLayout = prevLayout;
2142
3035
  table.style.width = prevWidth;
2143
3036
  colEls.forEach((c, i) => (c.style.width = prevColWidths[i]));
@@ -2154,7 +3047,7 @@ class NxeDataGridComponent {
2154
3047
  });
2155
3048
  const specialSum = 30 * this.groupLevelCount() +
2156
3049
  (this.masterDetailEnabled() ? 30 : 0) +
2157
- (this.showCheckboxes() ? 70 : 0) +
3050
+ (this.selectColumnVisible() ? 70 : 0) +
2158
3051
  (this.rowEditCommandVisible() ? 100 : 0);
2159
3052
  const fixedSum = explicit.reduce((s, w) => s + (w ?? 0), 0) + specialSum;
2160
3053
  const freeIdx = explicit.map((w, i) => (w == null ? i : -1)).filter((i) => i >= 0);
@@ -2209,24 +3102,51 @@ class NxeDataGridComponent {
2209
3102
  if (aw && this.columnAutoWidth()) {
2210
3103
  return aw.widths[idx] ?? this.colWidth(col);
2211
3104
  }
3105
+ const slack = this.frozenSlack();
3106
+ if (slack > 0 && idx === this.visibleColumns().length - 1) {
3107
+ return `${Math.round(this.frozenColWidth(col) + slack)}px`;
3108
+ }
3109
+ // A manual resize froze every visible column at a measured width, and that width - not the
3110
+ // declared one - is what the column now has. (A declared width used to win here, and only a
3111
+ // stale inline style left by the drag kept a resized column at its new width.)
3112
+ if (this.widthOverride().has(col.dataField())) {
3113
+ return `${this.frozenColWidth(col)}px`;
3114
+ }
2212
3115
  const w = this.colWidth(col);
2213
3116
  if (w == null && this.widthOverride().size) {
2214
3117
  return `${this.frozenColWidth(col)}px`;
2215
3118
  }
2216
3119
  return w;
2217
3120
  }
3121
+ /** What a frozen layout's columns add up to, the command columns included. */
3122
+ frozenTableWidth() {
3123
+ let total = 30 * this.groupLevelCount() +
3124
+ (this.masterDetailEnabled() ? 30 : 0) +
3125
+ (this.selectColumnVisible() ? 40 : 0) +
3126
+ (this.rowEditCommandVisible() ? 100 : 0);
3127
+ for (const col of this.visibleColumns()) {
3128
+ total += this.frozenColWidth(col);
3129
+ }
3130
+ return total;
3131
+ }
3132
+ /**
3133
+ * With `resizeFillsGrid`, the room a frozen widget-mode layout leaves on the right, which the
3134
+ * trailing column takes. 0 when the columns fill or overflow the grid - that table scrolls.
3135
+ */
3136
+ frozenSlack = computed(() => {
3137
+ if (!this.resizeFillsGrid() || !this.widthOverride().size || this.columnResizingMode() !== 'widget') {
3138
+ return 0;
3139
+ }
3140
+ const container = this.containerW();
3141
+ return container > 0 ? Math.max(0, Math.round(container - this.frozenTableWidth())) : 0;
3142
+ }, /* @ts-ignore */
3143
+ ...(ngDevMode ? [{ debugName: "frozenSlack" }] : /* istanbul ignore next */ []));
2218
3144
  tableWidthStyle() {
2219
3145
  // After a manual resize the layout is frozen: widget mode sizes the table to the sum of
2220
3146
  // its columns (dx grows/shrinks the table); nextColumn keeps filling the container.
2221
3147
  if (this.widthOverride().size && this.columnResizingMode() === 'widget') {
2222
- let total = 30 * this.groupLevelCount() +
2223
- (this.masterDetailEnabled() ? 30 : 0) +
2224
- (this.showCheckboxes() ? 40 : 0) +
2225
- (this.rowEditCommandVisible() ? 100 : 0);
2226
- for (const col of this.visibleColumns()) {
2227
- total += this.frozenColWidth(col);
2228
- }
2229
- return `${Math.round(total)}px`;
3148
+ // The trailing column already holds the slack, so the columns add up to the grid.
3149
+ return this.frozenSlack() > 0 ? '100%' : `${Math.round(this.frozenTableWidth())}px`;
2230
3150
  }
2231
3151
  if (this.widthOverride().size) {
2232
3152
  return '100%';
@@ -2251,21 +3171,76 @@ class NxeDataGridComponent {
2251
3171
  (this.treeMode && this.selectionMode() === 'multiple'));
2252
3172
  }, /* @ts-ignore */
2253
3173
  ...(ngDevMode ? [{ debugName: "showCheckboxes" }] : /* istanbul ignore next */ []));
3174
+ /**
3175
+ * The detached checkbox column. A tree list has none: dx draws its row checkbox in the first
3176
+ * cell, after the expander, and its select-all in the first column's header.
3177
+ */
3178
+ selectColumnVisible = computed(() => this.showCheckboxes() && !this.treeMode, /* @ts-ignore */
3179
+ ...(ngDevMode ? [{ debugName: "selectColumnVisible" }] : /* istanbul ignore next */ []));
3180
+ treeSelectAll = computed(() => this.treeMode && this.showCheckboxes() && this.selectionMode() === 'multiple', /* @ts-ignore */
3181
+ ...(ngDevMode ? [{ debugName: "treeSelectAll" }] : /* istanbul ignore next */ []));
3182
+ recursiveTreeSelection = computed(() => this.treeMode && this.selectionMode() === 'multiple' && (this.selection()?.recursive() ?? false), /* @ts-ignore */
3183
+ ...(ngDevMode ? [{ debugName: "recursiveTreeSelection" }] : /* istanbul ignore next */ []));
3184
+ /**
3185
+ * Each node's recursive selection state as dx draws it: `all` when its key is selected or an
3186
+ * ancestor's is, or when every child is; `some` when only some descendants are; absent for none.
3187
+ * Keys hold the selection, so the hierarchy - not the key list - decides what a parent shows.
3188
+ */
3189
+ treeCheckState = computed(() => this.recursiveTreeSelection()
3190
+ ? nxeTreeCheckState(this.treeRoots(), new Set(this.selectedRowKeys()))
3191
+ : new Map(), /* @ts-ignore */
3192
+ ...(ngDevMode ? [{ debugName: "treeCheckState" }] : /* istanbul ignore next */ []));
3193
+ /** Parent of each tree node, for settling ancestors after a recursive toggle. */
3194
+ treeParents = computed(() => {
3195
+ const parents = new Map();
3196
+ const walk = (nodes) => {
3197
+ for (const n of nodes) {
3198
+ n.children.forEach((c) => parents.set(c.key, n));
3199
+ walk(n.children);
3200
+ }
3201
+ };
3202
+ walk(this.treeRoots());
3203
+ return parents;
3204
+ }, /* @ts-ignore */
3205
+ ...(ngDevMode ? [{ debugName: "treeParents" }] : /* istanbul ignore next */ []));
2254
3206
  /** Selected rows in dataSource order (stale refs from old data naturally filtered out). */
2255
3207
  selectedRowsData = computed(() => {
3208
+ if (this.remote()) {
3209
+ // Keyed, and across pages: the row object is the page's when it is on screen, else the
3210
+ // one remembered from when it was selected.
3211
+ const page = new Map(this.sourceRows().map((r) => [this.rowKey(r), r]));
3212
+ return this.selectedRowKeys().flatMap((key) => {
3213
+ const row = page.get(key) ?? this.remoteRowCache.get(key);
3214
+ if (row === undefined) {
3215
+ return [];
3216
+ }
3217
+ this.remoteRowCache.set(key, row);
3218
+ return [row];
3219
+ });
3220
+ }
2256
3221
  if (this.treeMode) {
2257
3222
  const keySet = new Set(this.selectedRowKeys());
2258
3223
  return keySet.size === 0
2259
3224
  ? []
2260
- : (this.dataSource() ?? []).filter((r) => keySet.has(this.rowKey(r)));
3225
+ : this.sourceRows().filter((r) => keySet.has(this.rowKey(r)));
2261
3226
  }
2262
3227
  const set = this.selectedSet();
2263
- return set.size === 0 ? [] : (this.dataSource() ?? []).filter((r) => set.has(r));
3228
+ return set.size === 0 ? [] : this.sourceRows().filter((r) => set.has(r));
2264
3229
  }, /* @ts-ignore */
2265
3230
  ...(ngDevMode ? [{ debugName: "selectedRowsData" }] : /* istanbul ignore next */ []));
2266
3231
  allSelected = computed(() => {
3232
+ if (this.remote()) {
3233
+ const rows = this.displayRows();
3234
+ const keys = this.selectedKeySet();
3235
+ return rows.length > 0 && rows.every((r) => keys.has(this.rowKey(r)));
3236
+ }
3237
+ if (this.recursiveTreeSelection()) {
3238
+ const roots = this.treeRoots();
3239
+ const state = this.treeCheckState();
3240
+ return roots.length > 0 && roots.every((n) => state.get(n.key) === 'all');
3241
+ }
2267
3242
  if (this.treeMode) {
2268
- const rows = (this.dataSource() ?? []);
3243
+ const rows = this.sourceRows();
2269
3244
  const keys = this.selectedRowKeys();
2270
3245
  return rows.length > 0 && rows.every((r) => keys.includes(this.rowKey(r)));
2271
3246
  }
@@ -2274,12 +3249,19 @@ class NxeDataGridComponent {
2274
3249
  return rows.length > 0 && rows.every((r) => set.has(r));
2275
3250
  }, /* @ts-ignore */
2276
3251
  ...(ngDevMode ? [{ debugName: "allSelected" }] : /* istanbul ignore next */ []));
2277
- someSelected = computed(() => this.treeMode
2278
- ? this.selectedRowKeys().length > 0
2279
- : this.displayRows().some((r) => this.selectedSet().has(r)), /* @ts-ignore */
3252
+ someSelected = computed(() => this.remote()
3253
+ ? this.displayRows().some((r) => this.selectedKeySet().has(this.rowKey(r)))
3254
+ : this.recursiveTreeSelection()
3255
+ ? this.treeCheckState().size > 0
3256
+ : this.treeMode
3257
+ ? this.selectedRowKeys().length > 0
3258
+ : this.displayRows().some((r) => this.selectedSet().has(r)), /* @ts-ignore */
2280
3259
  ...(ngDevMode ? [{ debugName: "someSelected" }] : /* istanbul ignore next */ []));
3260
+ /** The key field: the bound `keyExpr`, else the remote source's own `key`. */
3261
+ keyField = computed(() => this.keyExpr() ?? this.remote()?.key, /* @ts-ignore */
3262
+ ...(ngDevMode ? [{ debugName: "keyField" }] : /* istanbul ignore next */ []));
2281
3263
  rowKey(row) {
2282
- const k = this.keyExpr();
3264
+ const k = this.keyField();
2283
3265
  return k ? row?.[k] : row;
2284
3266
  }
2285
3267
  treeDescendantKeys(row) {
@@ -2319,98 +3301,442 @@ class NxeDataGridComponent {
2319
3301
  this.emitTreeSelectionChanged([key], added, removed);
2320
3302
  return;
2321
3303
  }
2322
- const recursive = this.selection()?.recursive() ?? false;
3304
+ if (this.recursiveTreeSelection()) {
3305
+ this.toggleRecursiveTreeSelection(row, key);
3306
+ return;
3307
+ }
2323
3308
  const keys = new Set(this.selectedRowKeys());
2324
3309
  const selecting = !keys.has(key);
2325
- const affected = recursive ? this.treeDescendantKeys(row) : [key];
2326
- for (const k of affected) {
2327
- selecting ? keys.add(k) : keys.delete(k);
3310
+ selecting ? keys.add(key) : keys.delete(key);
3311
+ const arr = [...keys];
3312
+ this.selectedRowKeys.set(arr);
3313
+ this.emitTreeSelectionChanged(arr, selecting ? [key] : [], selecting ? [] : [key]);
3314
+ }
3315
+ /**
3316
+ * dx recursive selection. Clicking a node that is not fully checked (unchecked, or a parent
3317
+ * showing indeterminate) selects its whole subtree; clicking a checked one clears it. Then its
3318
+ * ancestors follow their children, checked once all of them are and cleared as soon as one is
3319
+ * not, so deselecting one child leaves its parent indeterminate rather than still ticked.
3320
+ *
3321
+ * The key list is rewritten as every checked node, which dx's `getSelectedRowKeys('all')`
3322
+ * also reports. The event's current keys are what changed on screen.
3323
+ */
3324
+ toggleRecursiveTreeSelection(row, key) {
3325
+ const before = this.treeCheckState();
3326
+ const checked = new Set();
3327
+ before.forEach((state, k) => state === 'all' && checked.add(k));
3328
+ const selecting = before.get(key) !== 'all';
3329
+ for (const k of this.treeDescendantKeys(row)) {
3330
+ selecting ? checked.add(k) : checked.delete(k);
3331
+ }
3332
+ const parents = this.treeParents();
3333
+ for (let parent = parents.get(key); parent; parent = parents.get(parent.key)) {
3334
+ parent.children.every((c) => checked.has(c.key)) ? checked.add(parent.key) : checked.delete(parent.key);
3335
+ }
3336
+ const previous = this.selectedRowKeys();
3337
+ const kept = previous.filter((k) => checked.has(k));
3338
+ const keptSet = new Set(kept);
3339
+ const next = [...kept, ...[...checked].filter((k) => !keptSet.has(k))];
3340
+ const wasChecked = (k) => before.get(k) === 'all';
3341
+ this.selectedRowKeys.set(next);
3342
+ this.emitTreeSelectionChanged(next, next.filter((k) => !wasChecked(k)), [...before.keys()].filter((k) => wasChecked(k) && !checked.has(k)));
3343
+ }
3344
+ /** A recursive tree list's parent with only some descendants selected (dx's indeterminate box). */
3345
+ isTreeIndeterminate(row) {
3346
+ return this.treeCheckState().get(this.rowKey(row)) === 'some';
3347
+ }
3348
+ emitTreeSelectionChanged(allKeys, added, removed) {
3349
+ const keySet = new Set(allKeys);
3350
+ const data = this.sourceRows().filter((r) => keySet.has(this.rowKey(r)));
3351
+ this.onSelectionChanged.emit({
3352
+ selectedRowsData: data,
3353
+ selectedRowKeys: allKeys,
3354
+ currentSelectedRowKeys: added,
3355
+ currentDeselectedRowKeys: removed,
3356
+ component: this.facade,
3357
+ element: this.elementRef.nativeElement,
3358
+ });
3359
+ }
3360
+ // --- Grouping + summary (dxo-grouping / dxo-group-panel / dxo-summary) ------------------------
3361
+ /** `grouping.autoExpandAll` from the child element or object input; dx default is true. */
3362
+ groupAutoExpand = computed(() => this.grouping()?.autoExpandAll() ?? this.groupingOptions()?.autoExpandAll ?? true, /* @ts-ignore */
3363
+ ...(ngDevMode ? [{ debugName: "groupAutoExpand" }] : /* istanbul ignore next */ []));
3364
+ /** The current expand default — `expandAll()` flips it to true; an input change resets it. */
3365
+ groupExpandDefault = linkedSignal({ ...(ngDevMode ? { debugName: "groupExpandDefault" } : /* istanbul ignore next */ {}), source: () => this.groupAutoExpand(),
3366
+ computation: (v) => v });
3367
+ /**
3368
+ * Per-groupKey manual toggles. Reset when the autoExpandAll option flips or the group
3369
+ * configuration changes (dx repaints and re-applies the default; drag-created groups inherit it).
3370
+ */
3371
+ groupExpandOverride = linkedSignal({ ...(ngDevMode ? { debugName: "groupExpandOverride" } : /* istanbul ignore next */ {}), source: () => `${this.groupAutoExpand()}|${this.groupedFields().join('\u0000')}`,
3372
+ computation: () => new Map() });
3373
+ isGroupExpanded(groupKey) {
3374
+ return this.groupExpandOverride().get(groupKey) ?? this.groupExpandDefault();
3375
+ }
3376
+ groupPanelVisible = computed(() => this.groupPanel()?.visible() === true || this.groupPanelOptions()?.visible === true, /* @ts-ignore */
3377
+ ...(ngDevMode ? [{ debugName: "groupPanelVisible" }] : /* istanbul ignore next */ []));
3378
+ isGrouped = computed(() => this.groupColumns().length > 0, /* @ts-ignore */
3379
+ ...(ngDevMode ? [{ debugName: "isGrouped" }] : /* istanbul ignore next */ []));
3380
+ totalItems = computed(() => this.summaryCmp()?.totalItems() ?? [], /* @ts-ignore */
3381
+ ...(ngDevMode ? [{ debugName: "totalItems" }] : /* istanbul ignore next */ []));
3382
+ groupItems = computed(() => this.summaryCmp()?.groupItems() ?? [], /* @ts-ignore */
3383
+ ...(ngDevMode ? [{ debugName: "groupItems" }] : /* istanbul ignore next */ []));
3384
+ hasTotalFooter = computed(() => this.totalItems().length > 0 && this.displayRows().length > 0, /* @ts-ignore */
3385
+ ...(ngDevMode ? [{ debugName: "hasTotalFooter" }] : /* istanbul ignore next */ []));
3386
+ /** Grand-total sums keyed by dataField → formatted text (alignByColumn placement). */
3387
+ totalSums = computed(() => {
3388
+ const map = new Map();
3389
+ const rows = this.displayRows();
3390
+ for (const item of this.totalItems()) {
3391
+ map.set(item.column(), this.summaryText(item, rows));
3392
+ }
3393
+ return map;
3394
+ }, /* @ts-ignore */
3395
+ ...(ngDevMode ? [{ debugName: "totalSums" }] : /* istanbul ignore next */ []));
3396
+ // --- Server-side data (NxeRemoteDataSource) -------------------------------------------------
3397
+ /** A `{ load }` store handed to `dataSource` with `remoteOperations`, wrapped once per store. */
3398
+ storeWrappers = new WeakMap();
3399
+ wrapped = null;
3400
+ remoteWarnings = new Set();
3401
+ /** Rows picked on earlier pages, by key, so `selectedRowsData` still has them. */
3402
+ remoteRowCache = new Map();
3403
+ /** Set by filter-row typing; the next request waits `filterDebounce` before it is sent. */
3404
+ remoteTyping = false;
3405
+ /** What the last request was for, to tell a changed filter, sort or size from a changed page. */
3406
+ remoteSignature = null;
3407
+ warnOnce(key, message) {
3408
+ if (!this.remoteWarnings.has(key)) {
3409
+ this.remoteWarnings.add(key);
3410
+ console.warn(`[nxe-data-grid] ${message}`);
3411
+ }
3412
+ }
3413
+ /**
3414
+ * The source the grid pages through, or `null` for a client-side array. An
3415
+ * {@link NxeRemoteDataSource} is used as it is; a `{ load }` store becomes one when
3416
+ * `remoteOperations` is on.
3417
+ */
3418
+ remote = computed(() => {
3419
+ const source = this.dataSource();
3420
+ if (this.treeMode) {
3421
+ if (source instanceof NxeRemoteDataSource || nxeIsStoreLike(source)) {
3422
+ this.warnOnce('tree', 'nxe-tree-list does not support server-side data; the source is ignored.');
3423
+ }
3424
+ return null;
3425
+ }
3426
+ if (source instanceof NxeRemoteDataSource) {
3427
+ return source;
3428
+ }
3429
+ if (!nxeIsStoreLike(source)) {
3430
+ return null;
3431
+ }
3432
+ if (!this.remoteOperations()) {
3433
+ this.warnOnce('store', 'A { load } dataSource needs [remoteOperations]="true"; without it the grid shows no rows.');
3434
+ return null;
3435
+ }
3436
+ let wrapper = this.storeWrappers.get(source);
3437
+ if (!wrapper) {
3438
+ wrapper = new NxeRemoteDataSource((options, context) => source.load(options, context), {
3439
+ key: source.key,
3440
+ });
3441
+ this.storeWrappers.set(source, wrapper);
3442
+ }
3443
+ this.wrapped = wrapper;
3444
+ return wrapper;
3445
+ }, /* @ts-ignore */
3446
+ ...(ngDevMode ? [{ debugName: "remote" }] : /* istanbul ignore next */ []));
3447
+ /** The rows every client-side computed reads: the array, or the remote source's current page. */
3448
+ sourceRows = computed(() => {
3449
+ const remote = this.remote();
3450
+ if (remote) {
3451
+ return remote.items();
3452
+ }
3453
+ const source = this.dataSource();
3454
+ return Array.isArray(source) ? source : [];
3455
+ }, /* @ts-ignore */
3456
+ ...(ngDevMode ? [{ debugName: "sourceRows" }] : /* istanbul ignore next */ []));
3457
+ selectedKeySet = computed(() => new Set(this.selectedRowKeys()), /* @ts-ignore */
3458
+ ...(ngDevMode ? [{ debugName: "selectedKeySet" }] : /* istanbul ignore next */ []));
3459
+ /** The sort a request carries: the controlled `sort`, else the header sort. */
3460
+ remoteSort = computed(() => {
3461
+ const bound = this.sortModel();
3462
+ if (bound !== undefined) {
3463
+ return bound.map((s) => ({ selector: s.selector, desc: s.desc }));
3464
+ }
3465
+ const sort = this.effectiveSort();
3466
+ return sort ? [{ selector: sort.field, desc: sort.dir === 'desc' }] : [];
3467
+ }, /* @ts-ignore */
3468
+ ...(ngDevMode ? [{ debugName: "remoteSort" }] : /* istanbul ignore next */ []));
3469
+ /** The filter a request carries: filter row AND header filters AND the application's `filter`. */
3470
+ remoteFilter = computed(() => {
3471
+ if (!this.remote()) {
3472
+ return null;
3473
+ }
3474
+ return nxeFilterAnd(this.remoteFilterRowExpression(), this.remoteHeaderFilterExpression(), this.filter());
3475
+ }, /* @ts-ignore */
3476
+ ...(ngDevMode ? [{ debugName: "remoteFilter" }] : /* istanbul ignore next */ []));
3477
+ /** The request the remote source should be showing, or `null` for a client-side grid. */
3478
+ remoteRequest = computed(() => {
3479
+ const source = this.remote();
3480
+ if (!source) {
3481
+ return null;
3482
+ }
3483
+ return {
3484
+ source,
3485
+ query: {
3486
+ pageIndex: this.currentPage(),
3487
+ pageSize: this.effectivePageSize(),
3488
+ sort: this.remoteSort(),
3489
+ filter: this.remoteFilter(),
3490
+ },
3491
+ };
3492
+ }, { ...(ngDevMode ? { debugName: "remoteRequest" } : /* istanbul ignore next */ {}),
3493
+ // The page index is clamped by the source's total, so every response re-derives the
3494
+ // request; an unchanged one must not be a new value, or it would reload forever.
3495
+ equal: (a, b) => a === b || (!!a && !!b && a.source === b.source && nxeSameQuery(a.query, b.query)) });
3496
+ dispatchRemote(request) {
3497
+ const { source, query } = request;
3498
+ const key = JSON.stringify([query.pageSize, query.sort, query.filter]);
3499
+ const last = this.remoteSignature;
3500
+ this.remoteSignature = { source, key };
3501
+ if (last && last.source === source && last.key !== key && query.pageIndex !== 0) {
3502
+ // A new filter, sort or page size means a new result set: back to its first page. The
3503
+ // move restates the request, which is what loads.
3504
+ this.goToPage(0);
3505
+ return;
3506
+ }
3507
+ if (this.groupColumns().length) {
3508
+ this.warnOnce('group', 'Grouping is not supported with server-side data: rows are grouped within the current page only.');
3509
+ }
3510
+ if (this.summaryCmp()) {
3511
+ this.warnOnce('summary', 'Summaries are not supported with server-side data: they are computed over the current page only.');
3512
+ }
3513
+ const before = source.options();
3514
+ source.setOptions(query);
3515
+ if (source.options() !== before) {
3516
+ this.optionsChange.emit(source.options());
3517
+ }
3518
+ }
3519
+ // Filter row -> expression. The matcher in `buildFilterRowMatcher` is the client-side reading
3520
+ // of the same state; keep the two in step.
3521
+ remoteFilterRowExpression() {
3522
+ const byField = this.columnByField();
3523
+ const parts = [];
3524
+ for (const [field, entry] of this.filterRowState()) {
3525
+ const col = byField.get(field);
3526
+ const expr = col ? this.filterRowCondition(col, entry) : null;
3527
+ if (expr) {
3528
+ parts.push(expr);
3529
+ }
3530
+ }
3531
+ return nxeFilterAnd(...parts);
3532
+ }
3533
+ filterRowCondition(col, entry) {
3534
+ const field = col.dataField();
3535
+ const kind = this.frEditorKind(col);
3536
+ const t1 = entry.text.trim();
3537
+ const t2 = (entry.text2 ?? '').trim();
3538
+ if (!t1 && !t2) {
3539
+ return null;
3540
+ }
3541
+ if (kind === 'boolean') {
3542
+ return [field, '=', t1 === 'true'];
3543
+ }
3544
+ if (kind === 'lookup') {
3545
+ const lookup = col.lookup();
3546
+ const valueOf = (i) => nxeResolveExpr(lookup.valueExpr ?? 'lovCode', i);
3547
+ const item = (lookup.dataSource ?? []).find((i) => String(valueOf(i) ?? '') === t1);
3548
+ return [field, '=', item ? valueOf(item) : t1];
3549
+ }
3550
+ const between = (lo, hi, hiOp) => nxeFilterAnd(lo == null ? null : [field, '>=', lo], hi == null ? null : [field, hiOp, hi]);
3551
+ if (kind === 'number') {
3552
+ const v1 = t1 ? Number(t1.replace(/,/g, '')) : null;
3553
+ const v2 = t2 ? Number(t2.replace(/,/g, '')) : null;
3554
+ if ((t1 && Number.isNaN(v1)) || (t2 && Number.isNaN(v2))) {
3555
+ return null; // nothing applies until the editor holds a valid number
3556
+ }
3557
+ if (entry.op === 'between') {
3558
+ return between(v1, v2, '<=');
3559
+ }
3560
+ const op = ['<>', '<', '<=', '>', '>='].find((o) => o === entry.op) ?? '=';
3561
+ return v1 == null ? null : [field, op, v1];
3562
+ }
3563
+ if (kind === 'date') {
3564
+ const d1 = t1 ? this.dayKey(t1) : null;
3565
+ const d2 = t2 ? this.dayKey(t2) : null;
3566
+ if ((t1 && !d1) || (t2 && !d2)) {
3567
+ return null;
3568
+ }
3569
+ // A date is a whole day, as in dx: '=' is [day, next day), '<=' ends before the next day.
3570
+ const next = (day) => nxeNextDay(day);
3571
+ switch (entry.op) {
3572
+ case 'between':
3573
+ return between(d1, d2 ? next(d2) : null, '<');
3574
+ case '<>':
3575
+ return d1 ? nxeFilterOr([field, '<', d1], [field, '>=', next(d1)]) : null;
3576
+ case '<':
3577
+ return d1 ? [field, '<', d1] : null;
3578
+ case '<=':
3579
+ return d1 ? [field, '<', next(d1)] : null;
3580
+ case '>':
3581
+ return d1 ? [field, '>=', next(d1)] : null;
3582
+ case '>=':
3583
+ return d1 ? [field, '>=', d1] : null;
3584
+ default:
3585
+ return d1 ? between(d1, next(d1), '<') : null;
3586
+ }
3587
+ }
3588
+ // Text: the operation as chosen. Case is the server's business; the client matcher ignores it.
3589
+ const textOps = ['notcontains', 'startswith', 'endswith', '=', '<>'];
3590
+ return [field, textOps.find((o) => o === entry.op) ?? 'contains', t1];
3591
+ }
3592
+ remoteHeaderFilterExpression() {
3593
+ const byField = this.columnByField();
3594
+ const parts = [];
3595
+ for (const [field, keys] of this.effectiveHeaderFilters()) {
3596
+ const isDate = (byField.get(field) ? this.colType(byField.get(field)) : undefined) === 'date';
3597
+ const values = [...keys].map((key) => {
3598
+ if (key === NXE_HF_BLANKS) {
3599
+ return [field, '=', null];
3600
+ }
3601
+ if (isDate && typeof key === 'string') {
3602
+ return [[field, '>=', key], 'and', [field, '<', nxeNextDay(key)]];
3603
+ }
3604
+ return [field, '=', key];
3605
+ });
3606
+ // An empty selection is an empty grid: a condition nothing satisfies.
3607
+ parts.push(nxeFilterOr(...values) ?? [[field, '=', null], 'and', [field, '<>', null]]);
3608
+ }
3609
+ return nxeFilterAnd(...parts);
3610
+ }
3611
+ toggleRemoteSelection(row) {
3612
+ const key = this.rowKey(row);
3613
+ const previous = this.selectedRowKeys();
3614
+ let next;
3615
+ let added = [];
3616
+ let removed = [];
3617
+ if (this.selectionMode() !== 'multiple') {
3618
+ added = previous.includes(key) ? [] : [key];
3619
+ removed = previous.filter((k) => k !== key);
3620
+ next = [key];
3621
+ }
3622
+ else if (previous.includes(key)) {
3623
+ next = previous.filter((k) => k !== key);
3624
+ removed = [key];
3625
+ }
3626
+ else {
3627
+ next = [...previous, key];
3628
+ added = [key];
3629
+ }
3630
+ this.remoteRowCache.set(key, row);
3631
+ this.selectedRowKeys.set(next);
3632
+ this.emitRemoteSelectionChanged(next, added, removed);
3633
+ }
3634
+ toggleRemoteSelectAll() {
3635
+ const pageKeys = this.displayRows().map((r) => this.rowKey(r));
3636
+ for (const row of this.displayRows()) {
3637
+ this.remoteRowCache.set(this.rowKey(row), row);
3638
+ }
3639
+ const previous = this.selectedRowKeys();
3640
+ if (this.allSelected()) {
3641
+ const drop = new Set(pageKeys);
3642
+ const next = previous.filter((k) => !drop.has(k));
3643
+ this.selectedRowKeys.set(next);
3644
+ this.emitRemoteSelectionChanged(next, [], pageKeys);
3645
+ }
3646
+ else {
3647
+ const have = new Set(previous);
3648
+ const added = pageKeys.filter((k) => !have.has(k));
3649
+ const next = [...previous, ...added];
3650
+ this.selectedRowKeys.set(next);
3651
+ this.emitRemoteSelectionChanged(next, added, []);
2328
3652
  }
2329
- const arr = [...keys];
2330
- this.selectedRowKeys.set(arr);
2331
- this.emitTreeSelectionChanged(arr, selecting ? affected : [], selecting ? [] : affected);
2332
3653
  }
2333
- emitTreeSelectionChanged(allKeys, added, removed) {
2334
- const keySet = new Set(allKeys);
2335
- const data = (this.dataSource() ?? []).filter((r) => keySet.has(this.rowKey(r)));
3654
+ emitRemoteSelectionChanged(keys, added, removed) {
2336
3655
  this.onSelectionChanged.emit({
2337
- selectedRowsData: data,
2338
- selectedRowKeys: allKeys,
3656
+ selectedRowsData: this.selectedRowsData(),
3657
+ selectedRowKeys: keys,
2339
3658
  currentSelectedRowKeys: added,
2340
3659
  currentDeselectedRowKeys: removed,
2341
3660
  component: this.facade,
2342
3661
  element: this.elementRef.nativeElement,
2343
3662
  });
2344
3663
  }
2345
- // --- Grouping + summary (dxo-grouping / dxo-group-panel / dxo-summary) ------------------------
2346
- /** `grouping.autoExpandAll` from the child element or object input; dx default is true. */
2347
- groupAutoExpand = computed(() => this.grouping()?.autoExpandAll() ?? this.groupingOptions()?.autoExpandAll ?? true, /* @ts-ignore */
2348
- ...(ngDevMode ? [{ debugName: "groupAutoExpand" }] : /* istanbul ignore next */ []));
2349
- /** The current expand default — `expandAll()` flips it to true; an input change resets it. */
2350
- groupExpandDefault = linkedSignal({ ...(ngDevMode ? { debugName: "groupExpandDefault" } : /* istanbul ignore next */ {}), source: () => this.groupAutoExpand(),
2351
- computation: (v) => v });
2352
- /**
2353
- * Per-groupKey manual toggles. Reset when the autoExpandAll option flips or the group
2354
- * configuration changes (dx repaints and re-applies the default; drag-created groups inherit it).
2355
- */
2356
- groupExpandOverride = linkedSignal({ ...(ngDevMode ? { debugName: "groupExpandOverride" } : /* istanbul ignore next */ {}), source: () => `${this.groupAutoExpand()}|${this.groupedFields().join('\u0000')}`,
2357
- computation: () => new Map() });
2358
- isGroupExpanded(groupKey) {
2359
- return this.groupExpandOverride().get(groupKey) ?? this.groupExpandDefault();
2360
- }
2361
- groupPanelVisible = computed(() => this.groupPanel()?.visible() === true || this.groupPanelOptions()?.visible === true, /* @ts-ignore */
2362
- ...(ngDevMode ? [{ debugName: "groupPanelVisible" }] : /* istanbul ignore next */ []));
2363
- isGrouped = computed(() => this.groupColumns().length > 0, /* @ts-ignore */
2364
- ...(ngDevMode ? [{ debugName: "isGrouped" }] : /* istanbul ignore next */ []));
2365
- totalItems = computed(() => this.summaryCmp()?.totalItems() ?? [], /* @ts-ignore */
2366
- ...(ngDevMode ? [{ debugName: "totalItems" }] : /* istanbul ignore next */ []));
2367
- groupItems = computed(() => this.summaryCmp()?.groupItems() ?? [], /* @ts-ignore */
2368
- ...(ngDevMode ? [{ debugName: "groupItems" }] : /* istanbul ignore next */ []));
2369
- hasTotalFooter = computed(() => this.totalItems().length > 0 && this.displayRows().length > 0, /* @ts-ignore */
2370
- ...(ngDevMode ? [{ debugName: "hasTotalFooter" }] : /* istanbul ignore next */ []));
2371
- /** Grand-total sums keyed by dataField → formatted text (alignByColumn placement). */
2372
- totalSums = computed(() => {
2373
- const map = new Map();
2374
- const rows = this.displayRows();
2375
- for (const item of this.totalItems()) {
2376
- map.set(item.column(), this.summaryText(item, rows));
2377
- }
2378
- return map;
2379
- }, /* @ts-ignore */
2380
- ...(ngDevMode ? [{ debugName: "totalSums" }] : /* istanbul ignore next */ []));
2381
3664
  // --- Internal paging (dxo-paging) -----------------------------------------------------------
2382
- pagingEnabled = computed(() => !!this.pagingCmp() && this.pagingCmp().enabled(), /* @ts-ignore */
3665
+ pagingEnabled = computed(() => !!this.remote() || (!!this.pagingCmp() && this.pagingCmp().enabled()), /* @ts-ignore */
2383
3666
  ...(ngDevMode ? [{ debugName: "pagingEnabled" }] : /* istanbul ignore next */ []));
2384
- effectivePageSize = computed(() => this.userPageSize() ?? this.pagingCmp()?.pageSize() ?? 20, /* @ts-ignore */
3667
+ effectivePageSize = computed(() => this.userPageSize() ??
3668
+ this.pagingCmp()?.pageSize() ??
3669
+ this.remote()?.options().pageSize ??
3670
+ NXE_REMOTE_DEFAULT_PAGE_SIZE, /* @ts-ignore */
2385
3671
  ...(ngDevMode ? [{ debugName: "effectivePageSize" }] : /* istanbul ignore next */ []));
2386
- pageCount = computed(() => this.pagingEnabled() ? Math.max(1, Math.ceil(this.displayRows().length / this.effectivePageSize())) : 1, /* @ts-ignore */
3672
+ pageCount = computed(() => {
3673
+ const remote = this.remote();
3674
+ if (remote) {
3675
+ return Math.max(1, Math.ceil(remote.totalCount() / this.effectivePageSize()));
3676
+ }
3677
+ return this.pagingEnabled() ? Math.max(1, Math.ceil(this.displayRows().length / this.effectivePageSize())) : 1;
3678
+ }, /* @ts-ignore */
2387
3679
  ...(ngDevMode ? [{ debugName: "pageCount" }] : /* istanbul ignore next */ []));
3680
+ /**
3681
+ * The page shown: `pageIndex` held inside the page count. A filter can leave the bound index
3682
+ * past the last page; dx moves it back (m_data_source_adapter.js:454), and the rows, the
3683
+ * pager's selected button, its info text and prev/next all read this one value.
3684
+ */
3685
+ currentPage = computed(() => {
3686
+ const remote = this.remote();
3687
+ // Until the first response there is no total to hold the index inside.
3688
+ if (remote && !remote.loaded()) {
3689
+ return Math.max(0, this.pageIndex());
3690
+ }
3691
+ return Math.max(0, Math.min(this.pageIndex(), this.pageCount() - 1));
3692
+ }, /* @ts-ignore */
3693
+ ...(ngDevMode ? [{ debugName: "currentPage" }] : /* istanbul ignore next */ []));
2388
3694
  /** The filtered/sorted rows visible on the current page (all rows when paging is off). */
2389
3695
  pagedRows = computed(() => {
2390
3696
  const rows = this.displayRows();
2391
- if (!this.pagingEnabled()) {
3697
+ if (!this.pagingEnabled() || this.remote()) {
2392
3698
  return rows;
2393
3699
  }
2394
3700
  const ps = this.effectivePageSize();
2395
- const pi = Math.min(this.pageIndex(), this.pageCount() - 1);
3701
+ const pi = this.currentPage();
2396
3702
  return rows.slice(pi * ps, pi * ps + ps);
2397
3703
  }, /* @ts-ignore */
2398
3704
  ...(ngDevMode ? [{ debugName: "pagedRows" }] : /* istanbul ignore next */ []));
3705
+ /**
3706
+ * dx `PagerView.isVisible` (m_pager.js): `true` always, `'auto'` only while there is more than
3707
+ * one page to move between, and never under virtual or infinite scrolling.
3708
+ */
2399
3709
  pagerVisible = computed(() => {
2400
3710
  const p = this.pagerCmp();
2401
3711
  if (!p) {
2402
3712
  return false;
2403
3713
  }
2404
- return p.visible() === true || (p.visible() === 'auto' && this.pagingEnabled());
3714
+ const mode = this.scrolling()?.mode;
3715
+ return (p.visible() === true ||
3716
+ (p.visible() === 'auto' &&
3717
+ this.pagingEnabled() &&
3718
+ this.pageCount() > 1 &&
3719
+ mode !== 'virtual' &&
3720
+ mode !== 'infinite'));
2405
3721
  }, /* @ts-ignore */
2406
3722
  ...(ngDevMode ? [{ debugName: "pagerVisible" }] : /* istanbul ignore next */ []));
2407
- pageSizes = computed(() => {
2408
- const a = this.pagerCmp()?.allowedPageSizes();
2409
- return Array.isArray(a) ? a : [this.effectivePageSize()];
2410
- }, /* @ts-ignore */
3723
+ pageSizes = computed(() => nxePagerPageSizes(this.pagerCmp()?.allowedPageSizes(), this.pagingCmp()?.pageSize() ?? 20), /* @ts-ignore */
2411
3724
  ...(ngDevMode ? [{ debugName: "pageSizes" }] : /* istanbul ignore next */ []));
2412
- pageNumbers = computed(() => Array.from({ length: this.pageCount() }, (_, i) => i), /* @ts-ignore */
2413
- ...(ngDevMode ? [{ debugName: "pageNumbers" }] : /* istanbul ignore next */ []));
3725
+ /** dx's small mode: a page-size drop-down, and the navigation buttons whatever the option says. */
3726
+ pagerCompact = computed(() => this.pagerCmp()?.displayMode() === 'compact', /* @ts-ignore */
3727
+ ...(ngDevMode ? [{ debugName: "pagerCompact" }] : /* istanbul ignore next */ []));
3728
+ /** dx `getRenderPrevButton` (pages/page_index_selector.js): `!isLargeDisplayMode || showNavigationButtons`. */
3729
+ pagerNavVisible = computed(() => this.pagerCompact() || !!this.pagerCmp()?.showNavigationButtons(), /* @ts-ignore */
3730
+ ...(ngDevMode ? [{ debugName: "pagerNavVisible" }] : /* istanbul ignore next */ []));
3731
+ /**
3732
+ * The page buttons and `. . .` separators. A linkedSignal because dx's list keeps a sliding
3733
+ * window between renders (pages/large.js): the previous window decides whether the next one
3734
+ * moves.
3735
+ */
3736
+ pagerWindow = linkedSignal({ ...(ngDevMode ? { debugName: "pagerWindow" } : /* istanbul ignore next */ {}), source: () => ({ count: this.pageCount(), index: this.currentPage() }),
3737
+ computation: (src, prev) => nxePagerWindow(src.count, src.index, prev?.value) });
3738
+ pageItems = computed(() => this.pagerWindow().items, /* @ts-ignore */
3739
+ ...(ngDevMode ? [{ debugName: "pageItems" }] : /* istanbul ignore next */ []));
2414
3740
  // --- Tree-list hierarchy (dx-tree-list) -----------------------------------------------------
2415
3741
  /** True when the host element is `<nxe-tree-list>` (vs `<nxe-data-grid>`). */
2416
3742
  treeMode = this.elementRef.nativeElement.tagName.toLowerCase() === 'nxe-tree-list';
@@ -2421,7 +3747,7 @@ class NxeDataGridComponent {
2421
3747
  if (!this.treeMode) {
2422
3748
  return [];
2423
3749
  }
2424
- const rows = (this.dataSource() ?? []);
3750
+ const rows = this.sourceRows();
2425
3751
  const keyF = this.keyExpr() ?? 'id';
2426
3752
  const parentF = this.parentIdExpr() ?? 'parentId';
2427
3753
  const root = this.rootValue();
@@ -2535,10 +3861,10 @@ class NxeDataGridComponent {
2535
3861
  for (const item of items) {
2536
3862
  const text = this.summaryText(item, bucket);
2537
3863
  if (item.showInGroupFooter()) {
2538
- footerSums.set(item.column(), text);
3864
+ footerSums.set(this.summaryTarget(item), text);
2539
3865
  }
2540
3866
  else if (item.alignByColumn()) {
2541
- alignedSums.set(item.showInColumn() ?? item.column(), text);
3867
+ alignedSums.set(this.summaryTarget(item), text);
2542
3868
  }
2543
3869
  else {
2544
3870
  captionParts.push(text);
@@ -2575,6 +3901,18 @@ class NxeDataGridComponent {
2575
3901
  return out;
2576
3902
  }, /* @ts-ignore */
2577
3903
  ...(ngDevMode ? [{ debugName: "renderRows" }] : /* istanbul ignore next */ []));
3904
+ /**
3905
+ * The dataField a group summary renders under: dx `_calculateSummaryCells` (m_summary.js:443)
3906
+ * takes `showInColumn` when it names a column, else the item's own `column`.
3907
+ *
3908
+ * Keying footers on `column()` alone put an item that names only `showInColumn` (broker's
3909
+ * `name="totalSumInsured" showInColumn="totalSumInsured" summaryType="custom"`) under its
3910
+ * empty `column` - i.e. in whichever column happens to have no dataField.
3911
+ */
3912
+ summaryTarget(item) {
3913
+ const target = item.showInColumn();
3914
+ return target && this.columnByField().has(target) ? target : item.column();
3915
+ }
2578
3916
  /** Computes + formats a summary item over a set of rows. */
2579
3917
  summaryText(item, rows) {
2580
3918
  const field = item.column();
@@ -2605,7 +3943,9 @@ class NxeDataGridComponent {
2605
3943
  }
2606
3944
  }
2607
3945
  const fmt = item.valueFormat();
2608
- const valueText = typeof fmt === 'string' ? formatNumber(value, parseNumberFormat(fmt)) : String(value);
3946
+ const valueText = typeof fmt === 'string'
3947
+ ? formatNumber(value, parseNumberFormat(fmt), this.separators)
3948
+ : String(value);
2609
3949
  const ct = item.customizeText();
2610
3950
  const text = ct ? ct({ value, valueText }) : valueText;
2611
3951
  return item.displayFormat().replace('{0}', text);
@@ -2640,29 +3980,33 @@ class NxeDataGridComponent {
2640
3980
  const next = Math.max(0, Math.min(index, this.pageCount() - 1));
2641
3981
  if (next !== this.pageIndex()) {
2642
3982
  this.pageIndex.set(next);
3983
+ // Write back so `[(pageIndex)]` reflects a page the grid moved to itself.
3984
+ this.pagingCmp()?.pageIndex.set(next);
3985
+ if (this.pageIndexModel() !== undefined || this.remote()) {
3986
+ this.pageIndexModel.set(next);
3987
+ }
2643
3988
  this.onOptionChanged.emit({ name: 'paging', fullName: 'paging.pageIndex', value: next });
2644
3989
  }
2645
3990
  }
2646
3991
  prevPage() {
2647
- this.goToPage(this.pageIndex() - 1);
3992
+ this.goToPage(this.currentPage() - 1);
2648
3993
  }
2649
3994
  nextPage() {
2650
- this.goToPage(this.pageIndex() + 1);
3995
+ this.goToPage(this.currentPage() + 1);
2651
3996
  }
2652
3997
  changePageSize(size) {
2653
3998
  this.userPageSize.set(size);
2654
3999
  this.pageIndex.set(0);
4000
+ if (this.pageSizeModel() !== undefined || this.remote()) {
4001
+ this.pageSizeModel.set(size);
4002
+ this.pageIndexModel.set(0);
4003
+ }
2655
4004
  }
2656
4005
  onPageSizeSelect(event) {
2657
4006
  this.changePageSize(Number(event.target.value));
2658
4007
  }
2659
- pageInfo = computed(() => {
2660
- const total = this.displayRows().length;
2661
- const ps = this.effectivePageSize();
2662
- const start = total === 0 ? 0 : this.pageIndex() * ps + 1;
2663
- const end = Math.min((this.pageIndex() + 1) * ps, total);
2664
- return `${start}-${end} of ${total}`;
2665
- }, /* @ts-ignore */
4008
+ /** dx's info text, `Page {0} of {1} ({2} items)` unless `infoText` says otherwise (pagination/info.js). */
4009
+ pageInfo = computed(() => nxePagerInfoText(this.pagerCmp()?.infoText() ?? this.messages.t('grid.pager.info'), this.currentPage(), this.remote() ? nxePagerCount(this.pageCount(), this.separators) : this.pageCount(), this.remote() ? nxePagerCount(this.remote().totalCount(), this.separators) : this.displayRows().length), /* @ts-ignore */
2666
4010
  ...(ngDevMode ? [{ debugName: "pageInfo" }] : /* istanbul ignore next */ []));
2667
4011
  // --- Runtime group state (drag-to-group backbone) --------------------------------------------
2668
4012
  static fieldArrayEqual = (a, b) => a.length === b.length && a.every((v, i) => v === b[i]);
@@ -2732,7 +4076,7 @@ class NxeDataGridComponent {
2732
4076
  this.groupedFields.update((fields) => [...fields, field]);
2733
4077
  if (current?.field === field) {
2734
4078
  this.groupSortDir.update((m) => new Map(m).set(field, current.dir));
2735
- this.sort.set(null);
4079
+ this.setSort(null);
2736
4080
  }
2737
4081
  this.emitGroupIndexChange(this.columns().indexOf(col), null, level);
2738
4082
  }
@@ -2749,8 +4093,7 @@ class NxeDataGridComponent {
2749
4093
  const dir = col ? this.groupDir(col) : 'asc';
2750
4094
  this.groupedFields.set(fields.filter((f) => f !== field));
2751
4095
  if (!this.effectiveSort()) {
2752
- this.sortInit.set(true);
2753
- this.sort.set({ field, dir });
4096
+ this.setSort({ field, dir });
2754
4097
  }
2755
4098
  this.emitGroupIndexChange(this.columns().findIndex((c) => c.dataField() === field), level, null);
2756
4099
  }
@@ -2948,30 +4291,50 @@ class NxeDataGridComponent {
2948
4291
  const next = this.columnResizingMode() === 'nextColumn' ? this.visibleColumns()[ci + 1] ?? null : null;
2949
4292
  this.resize.start(col.dataField(), next?.dataField() ?? null, ci, this.leadCellCount(), event);
2950
4293
  }
4294
+ /** A visible column's position, for cells rendered outside the main `@for` index. */
4295
+ visibleIndexOf(col) {
4296
+ return this.visibleColumns().indexOf(col);
4297
+ }
2951
4298
  /** Command columns preceding the data columns — the offset dx's 1-based aria-colindex counts from. */
2952
4299
  leadCellCount() {
2953
4300
  return (this.groupLevelCount() +
2954
4301
  (this.masterDetailEnabled() ? 1 : 0) +
2955
- (this.showCheckboxes() ? 1 : 0));
2956
- }
4302
+ (this.selectColumnVisible() ? 1 : 0));
4303
+ }
4304
+ // dx numbers every cell of a rows-view row, command cells included (`_setCellAriaAttributes`,
4305
+ // m_columns_view.js:681-693): the group indents are 1..n, then the master-detail expander, the
4306
+ // selection column, the data columns and the edit column. Apps address cells by it - bright's
4307
+ // merge helper lifts `td[aria-colindex="1"]` out of a merged row, which in a grouped grid is
4308
+ // the indent cell - so the command cells carry it too.
4309
+ /** The master-detail expander's aria-colindex: straight after the group indents. */
4310
+ expandColIndex = computed(() => this.groupLevelCount() + 1, /* @ts-ignore */
4311
+ ...(ngDevMode ? [{ debugName: "expandColIndex" }] : /* istanbul ignore next */ []));
4312
+ /** The selection column's aria-colindex: after the indents and the expander. */
4313
+ selectColIndex = computed(() => this.groupLevelCount() + (this.masterDetailEnabled() ? 1 : 0) + 1, /* @ts-ignore */
4314
+ ...(ngDevMode ? [{ debugName: "selectColIndex" }] : /* istanbul ignore next */ []));
4315
+ /** The row-edit command column's aria-colindex: after the last data column. */
4316
+ editColIndex = computed(() => this.leadCellCount() + this.visibleColumns().length + 1, /* @ts-ignore */
4317
+ ...(ngDevMode ? [{ debugName: "editColIndex" }] : /* istanbul ignore next */ []));
2957
4318
  /** Resize commit: freeze all rendered widths (dx post-resize behavior) + emit width events. */
2958
4319
  commitColumnResize(changes) {
2959
4320
  const map = new Map(this.widthOverride());
2960
- const headerRow = nxeLeafHeaderRow(this.elementRef.nativeElement);
2961
- if (headerRow) {
2962
- const cells = nxeHeaderDataCells(headerRow);
2963
- this.visibleColumns().forEach((c, i) => {
2964
- const cell = cells[i];
2965
- if (cell) {
2966
- map.set(c.dataField(), Math.round(cell.getBoundingClientRect().width * 100) / 100);
2967
- }
2968
- });
2969
- }
4321
+ const cells = nxeHeaderLeafCells(this.elementRef.nativeElement);
4322
+ this.visibleColumns().forEach((c, i) => {
4323
+ const cell = cells[i];
4324
+ if (cell) {
4325
+ map.set(c.dataField(), Math.round(cell.getBoundingClientRect().width * 100) / 100);
4326
+ }
4327
+ });
2970
4328
  for (const ch of changes) {
2971
4329
  map.set(ch.field, Math.round(ch.to * 100) / 100);
2972
4330
  }
2973
4331
  this.resizeActive = false;
2974
4332
  this.widthOverride.set(map);
4333
+ // dx stops redistributing after a manual resize. The auto-width pass would drop its result
4334
+ // on its next run, but nothing that run tracks has changed, so drop it now - left in place,
4335
+ // it outranks the frozen widths and the column snaps back to its auto width on release.
4336
+ this.autoWidths.set(null);
4337
+ this.lastAutoWidthKey = '';
2975
4338
  for (const ch of changes) {
2976
4339
  this.onOptionChanged.emit({
2977
4340
  name: 'columns',
@@ -3047,7 +4410,7 @@ class NxeDataGridComponent {
3047
4410
  /** Total column count including command columns (indents/checkbox/expand/edit) for colspans. */
3048
4411
  allColCount = computed(() => (this.groupLevelCount() +
3049
4412
  this.visibleColumns().length +
3050
- (this.showCheckboxes() ? 1 : 0) +
4413
+ (this.selectColumnVisible() ? 1 : 0) +
3051
4414
  (this.masterDetailEnabled() ? 1 : 0) +
3052
4415
  (this.rowEditCommandVisible() ? 1 : 0)) ||
3053
4416
  1, /* @ts-ignore */
@@ -3093,8 +4456,51 @@ class NxeDataGridComponent {
3093
4456
  return;
3094
4457
  }
3095
4458
  event.stopPropagation();
4459
+ // A boolean cell already shows its editor (dx showEditorAlways); only its checkbox
4460
+ // toggles, so a click beside the box does not swap anything in.
4461
+ if (this.booleanInPlace(col)) {
4462
+ return;
4463
+ }
3096
4464
  this.editingCell.set({ row, field: col.dataField() });
3097
4465
  }
4466
+ /**
4467
+ * dx `readOnly: !options.setValue` (m_editing.js:210): a grid checkbox is editable only where
4468
+ * dx gives the cell a setValue - cell mode on an editable column (m_editing.js:1832-1847). Row
4469
+ * mode's resting rows stay read-only; the row being edited renders the editor instead.
4470
+ */
4471
+ booleanReadOnly(col) {
4472
+ return !(this.cellEditingEnabled() && this.editableCell(col));
4473
+ }
4474
+ /**
4475
+ * A boolean cell that is its own editor: cell mode, editable, and no editCellTemplate asking
4476
+ * to take over. The resting checkbox toggles in place, as dx's always-shown dxCheckBox does.
4477
+ */
4478
+ booleanInPlace(col) {
4479
+ return this.isBooleanCell(col) && !this.booleanReadOnly(col) && !col.editCellTemplate();
4480
+ }
4481
+ /** A resting boolean checkbox was clicked: toggle it when it is editable, else let it be. */
4482
+ onBooleanCellClick(row, col, event) {
4483
+ if (!this.booleanInPlace(col)) {
4484
+ return;
4485
+ }
4486
+ event.stopPropagation();
4487
+ this.commitCellEdit(row, col, this.getRaw(row, col.dataField()) !== true);
4488
+ }
4489
+ /**
4490
+ * The checkbox editor's state: the buffered value in the row being edited, else the row's.
4491
+ * Read as the native editor it replaces read it - any value but '' and 'false' is checked.
4492
+ */
4493
+ editorChecked(row, col) {
4494
+ const text = this.editorInputValue(row, col);
4495
+ return !!text && text !== 'false';
4496
+ }
4497
+ /** A click or Space on the checkbox editor. Space must not scroll the page or reach the grid. */
4498
+ toggleEditorCheck(row, col, mode, event) {
4499
+ event.preventDefault();
4500
+ event.stopPropagation();
4501
+ const next = !this.editorChecked(row, col);
4502
+ mode === 'cell' ? this.commitCellEdit(row, col, next) : this.bufferRowEdit(col, next);
4503
+ }
3098
4504
  /** dx cell-mode semantics: commit to the row on blur/Enter (client-side array write-through). */
3099
4505
  commitCellEdit(row, col, rawInput) {
3100
4506
  this.writeCell(row, col, rawInput);
@@ -3136,7 +4542,7 @@ class NxeDataGridComponent {
3136
4542
  writeCell(row, col, rawInput) {
3137
4543
  const field = col.dataField();
3138
4544
  let value = rawInput;
3139
- if (col.dataType() === 'number' && typeof rawInput === 'string') {
4545
+ if (this.colType(col) === 'number' && typeof rawInput === 'string') {
3140
4546
  const n = Number(rawInput.replace(/,/g, ''));
3141
4547
  value = rawInput.trim() === '' || isNaN(n) ? null : n;
3142
4548
  }
@@ -3427,7 +4833,10 @@ class NxeDataGridComponent {
3427
4833
  /** dataSource → filter-row filters → header filters (sorting happens in `displayRows`). */
3428
4834
  filteredRows = computed(() => {
3429
4835
  this.refreshTick();
3430
- let rows = (this.dataSource() ?? []);
4836
+ let rows = this.sourceRows();
4837
+ if (this.remote()) {
4838
+ return rows; // the server has filtered
4839
+ }
3431
4840
  const rowFilter = this.filterRowPredicate();
3432
4841
  if (rowFilter) {
3433
4842
  rows = rows.filter(rowFilter);
@@ -3452,7 +4861,7 @@ class NxeDataGridComponent {
3452
4861
  chain.push({ field: col.dataField(), dir: this.groupDir(col) });
3453
4862
  }
3454
4863
  }
3455
- const user = this.effectiveSort();
4864
+ const user = this.remote() ? null : this.effectiveSort(); // a remote source arrives sorted
3456
4865
  if (user && !chain.some((s) => s.field === user.field)) {
3457
4866
  chain.push(user);
3458
4867
  }
@@ -3470,23 +4879,338 @@ class NxeDataGridComponent {
3470
4879
  });
3471
4880
  }, /* @ts-ignore */
3472
4881
  ...(ngDevMode ? [{ debugName: "displayRows" }] : /* istanbul ignore next */ []));
3473
- loading = computed(() => this.dataSource() == null && this.loadPanel()?.enabled !== false, /* @ts-ignore */
4882
+ loading = computed(() => (this.dataSource() == null || this.remote()?.loading() === true) &&
4883
+ this.loadPanel()?.enabled !== false, /* @ts-ignore */
3474
4884
  ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
4885
+ /** A remote request is in flight over rows already on screen: the overlay, not the empty-body spinner. */
4886
+ remoteBusy = computed(() => !!this.remote() && this.loading() && !this.isEmpty(), /* @ts-ignore */
4887
+ ...(ngDevMode ? [{ debugName: "remoteBusy" }] : /* istanbul ignore next */ []));
4888
+ remoteFailed = computed(() => this.remote()?.error() != null, /* @ts-ignore */
4889
+ ...(ngDevMode ? [{ debugName: "remoteFailed" }] : /* istanbul ignore next */ []));
4890
+ retryRemote() {
4891
+ this.remote()?.reload();
4892
+ }
4893
+ /**
4894
+ * dx `isEmpty()` = `!items().length` (m_data_controller.js:1044-1046), toggled as `dx-empty`
4895
+ * on the rows view in `_renderCore` (m_rows_view.js:710) whether loading or not.
4896
+ *
4897
+ * Keyed on `renderRows` - what the `<tbody>` iterates, dx's `items()` - not `displayRows`. The
4898
+ * two agree for flat and grouped grids (`pagedRows` clamps the page), but a tree list builds
4899
+ * `renderRows` from the unfiltered source while `displayRows` applies the filters, so a tree
4900
+ * whose filter matches nothing still renders rows and must not also show an empty body.
4901
+ */
4902
+ isEmpty = computed(() => this.renderRows().length === 0, /* @ts-ignore */
4903
+ ...(ngDevMode ? [{ debugName: "isEmpty" }] : /* istanbul ignore next */ []));
4904
+ // --- Free-space row (dx m_rows_view.js updateFreeSpaceRowHeight, :816-861) --------------------
4905
+ /**
4906
+ * What the last reading of this grid's own layout found.
4907
+ *
4908
+ * - `bounded` is dx's `hasHeight`: the rows view is sized by the grid, not by its rows.
4909
+ * - `room` is dx's `elementHeightWithoutScrollbar - contentHeight` (m_rows_view.js:840-842): the
4910
+ * rows view's inner height less the table as it stands with the free-space row empty and the
4911
+ * last row keeping its line. Neither the row's height nor the last-row-border state changes
4912
+ * it, so what it decides cannot change the next reading.
4913
+ * - `fill` is dx's `resultHeight` (:844-848): the height that leaves the table 1px short of
4914
+ * the rows view (`_getHeightCorrection`, 1 in Chrome).
4915
+ * - `rowHeight` is dx's `_rowHeight`, the average rendered row. It sizes the gap that a short
4916
+ * last page leaves.
4917
+ */
4918
+ freeSpaceMeasure = signal({ bounded: false, room: 0, fill: 0, rowHeight: 0 }, { ...(ngDevMode ? { debugName: "freeSpaceMeasure" } : /* istanbul ignore next */ {}), equal: (a, b) => a.bounded === b.bounded &&
4919
+ Math.abs(a.room - b.room) < 0.01 &&
4920
+ Math.abs(a.fill - b.fill) < 0.01 &&
4921
+ Math.abs(a.rowHeight - b.rowHeight) < 0.01 });
4922
+ /**
4923
+ * dx's three cases, in its order:
4924
+ * - no rows: the row shows at 0px, so the last row keeps no border to lose (:857-860);
4925
+ * - a bounded grid whose rows leave room: the row fills it, less the 1px dx subtracts in
4926
+ * Chrome (`_getHeightCorrection`), and column lines run to the bottom (:837-855);
4927
+ * - an unbounded grid on a short page of several: the row holds the missing rows' height, so
4928
+ * the grid keeps its height from page to page (:823-835).
4929
+ *
4930
+ * `visible` drives the last row's border in a bordered grid (`_updateLastRowBorder`, :931-937).
4931
+ */
4932
+ freeSpace = computed(() => {
4933
+ if (this.isEmpty()) {
4934
+ return { height: 0, visible: true };
4935
+ }
4936
+ const m = this.freeSpaceMeasure();
4937
+ if (m.bounded) {
4938
+ return m.room > 0
4939
+ ? { height: Math.max(0, m.fill), visible: true }
4940
+ : { height: 0, visible: false };
4941
+ }
4942
+ const missing = this.effectivePageSize() - this.pagedRows().length;
4943
+ const mode = this.scrolling()?.mode;
4944
+ if (!this.treeMode &&
4945
+ this.pagingEnabled() &&
4946
+ missing > 0 &&
4947
+ this.pageCount() > 1 &&
4948
+ mode !== 'virtual' &&
4949
+ mode !== 'infinite') {
4950
+ return { height: missing * m.rowHeight, visible: true };
4951
+ }
4952
+ return { height: 0, visible: false };
4953
+ }, /* @ts-ignore */
4954
+ ...(ngDevMode ? [{ debugName: "freeSpace" }] : /* istanbul ignore next */ []));
4955
+ freeSpaceHeight = computed(() => this.freeSpace().height, /* @ts-ignore */
4956
+ ...(ngDevMode ? [{ debugName: "freeSpaceHeight" }] : /* istanbul ignore next */ []));
4957
+ freeSpaceVisible = computed(() => this.freeSpace().visible, /* @ts-ignore */
4958
+ ...(ngDevMode ? [{ debugName: "freeSpaceVisible" }] : /* istanbul ignore next */ []));
4959
+ /**
4960
+ * One reading of the rows view, from the ResizeObserver that watches it and its table.
4961
+ *
4962
+ * dx hides the free-space row and then measures (m_rows_view.js:837-848). This reading cannot
4963
+ * hide it - that would shrink the document and could clamp its scroll position - so it takes
4964
+ * the row's part of the table back out arithmetically, from quantities that do not move with
4965
+ * what the reading decides:
4966
+ *
4967
+ * - The row's box comes out whole: the height it was given, or with none, its halves of the
4968
+ * collapsed borders above and below it (0.5px under a last row with a 1px frame).
4969
+ * - With no free space shown, a bordered grid's last row gives its line to the box
4970
+ * (`nxe-last-row-border`, `border-bottom-width:0`), which makes the table a line shorter.
4971
+ * That line is put back, at the grid's line width, so the answer is the same either way.
4972
+ *
4973
+ * Read naively off the table, either one feeds back: the half-line reads as room, which shows
4974
+ * the row, which gives the line back, which removes the room - a flip every frame - and a
4975
+ * filled row in a rows view that hugs its table reads as its own room, 1px less each time.
4976
+ *
4977
+ * The second case gets its own rule. A rows view that hugs its table while the row adds height
4978
+ * of its own is sized by that row, so it has no room, whatever `height` says: the row lets go
4979
+ * at once. (A bounded grid resized to exactly its table height less the 1px lands here too, and
4980
+ * fills again on the next reading.)
4981
+ *
4982
+ * Whether the grid is bounded (dx `hasHeight`): an explicit `height` other than 'auto' always
4983
+ * is (m_grid_view.js:597-604, `isHeightSpecified`), even a percentage that resolves to auto,
4984
+ * and then has no room while its rows view hugs. Otherwise a rows view that does not hug is
4985
+ * bounded by CSS, and one that hugs while the row holds free space is not, so a grid that
4986
+ * loses its height lets go of the space. When they coincide with no free space held (an exact
4987
+ * fit), the previous answer stands.
4988
+ */
4989
+ measureFreeSpace(rowsView, table) {
4990
+ const tbody = table.tBodies[0];
4991
+ const fsRow = tbody?.lastElementChild;
4992
+ if (!(fsRow instanceof HTMLTableRowElement) ||
4993
+ !fsRow.classList.contains('nxe-freespace-row') ||
4994
+ this.isEmpty()) {
4995
+ return;
4996
+ }
4997
+ const prev = this.freeSpaceMeasure();
4998
+ const last = fsRow.previousElementSibling;
4999
+ // The collapsed borders above and below the row: the last row's line, and the total
5000
+ // footer's top line when there is one (the table's own edge otherwise, which has none).
5001
+ const above = Math.max(nxeRowEdge(last, 'bottom'), nxeRowEdge(fsRow, 'top'));
5002
+ const below = Math.max(nxeRowEdge(fsRow, 'bottom'), nxeRowEdge(table.tFoot?.rows[0], 'top'));
5003
+ // The last row's line as it is with free space shown. The last-row-border rule strips it
5004
+ // to 0 only on a data row; it is drawn at the grid's line width.
5005
+ let line = above;
5006
+ if (rowsView.classList.contains('nxe-last-row-border') && last?.classList.contains('nxe-data-row')) {
5007
+ const token = parseFloat(getComputedStyle(rowsView).getPropertyValue('--nxe-ui-grid-line-width'));
5008
+ line = Number.isFinite(token) ? token : 1;
5009
+ }
5010
+ const fsHeight = fsRow.getBoundingClientRect().height;
5011
+ // The row adds height of its own, beyond its halves of the borders around it.
5012
+ const holding = fsHeight > (above + below) / 2 + 0.01;
5013
+ const tableHeight = table.getBoundingClientRect().height;
5014
+ // Everything but the free-space row, the last row keeping its line: dx's `rowsHeight`.
5015
+ const rowsHeight = tableHeight - fsHeight - above / 2 + line / 2;
5016
+ // And with the row empty: dx's `contentHeight`.
5017
+ const contentHeight = rowsHeight + (line + below) / 2;
5018
+ // Less borders and a horizontal scrollbar: dx's `elementHeightWithoutScrollbar`.
5019
+ const inner = rowsView.getBoundingClientRect().height - (rowsView.offsetHeight - rowsView.clientHeight);
5020
+ const h = this.heightStyle();
5021
+ const hugging = Math.abs(inner - tableHeight) < 0.5;
5022
+ const bounded = (h !== null && h !== 'auto') || (hugging ? prev.bounded && !holding : true);
5023
+ const rows = tbody.rows.length - 1;
5024
+ this.freeSpaceMeasure.set({
5025
+ bounded,
5026
+ room: hugging && holding ? 0 : inner - contentHeight,
5027
+ fill: inner - rowsHeight - 1,
5028
+ rowHeight: rows > 0 ? (tbody.getBoundingClientRect().height - fsHeight) / rows : prev.rowHeight,
5029
+ });
5030
+ }
5031
+ /**
5032
+ * dx `updateColumnDataTypes` (m_columns_controller.js:914-977): a column with no declared
5033
+ * `dataType` takes the type of its values, and mixed values make it 'string'.
5034
+ *
5035
+ * A lookup column is typed by its DISPLAY values - dx's `lookup.dataType`, which is what dx
5036
+ * aligns, formats and edits a lookup by (:901) - and a value with no lookup match contributes
5037
+ * nothing: dx's `lookup.calculateCellValue` returns `valueMap[value]`, undefined for a missing
5038
+ * key, so `getValueDataType` skips it (:946). A not-yet-loaded list must not type the column.
5039
+ *
5040
+ * Once inferred, a type sticks. dx infers only while `dataType` is unset (:935) and stops once
5041
+ * every column is typed (:289), so a later load of mixed values cannot flip a column; the
5042
+ * previous value of this linkedSignal carries that. The refresh tick is in the source because
5043
+ * rows mutated in place (inline editing, `refresh()`) can type a column that was all blanks.
5044
+ *
5045
+ * dx samples the loaded page (or the first group's leaves); this scans the client array,
5046
+ * which only differs when the types are mixed beyond that sample.
5047
+ */
5048
+ inferredTypes = linkedSignal({ ...(ngDevMode ? { debugName: "inferredTypes" } : /* istanbul ignore next */ {}), source: () => {
5049
+ this.refreshTick();
5050
+ return { rows: this.sourceRows(), cols: this.columns() };
5051
+ },
5052
+ computation: ({ rows, cols }, previous) => {
5053
+ const out = new Map();
5054
+ for (const col of cols) {
5055
+ const kept = previous?.value.get(col);
5056
+ // A rebound dataField is a different column as far as its values are concerned.
5057
+ if (kept && kept.field === col.dataField()) {
5058
+ out.set(col, kept);
5059
+ }
5060
+ }
5061
+ if (!rows.length) {
5062
+ return out;
5063
+ }
5064
+ for (const col of cols) {
5065
+ const field = col.dataField();
5066
+ if (out.has(col) || col.dataType() || !field) {
5067
+ continue;
5068
+ }
5069
+ const display = this.lookupDisplayMap(col);
5070
+ let type;
5071
+ for (const row of rows) {
5072
+ let value = this.getRaw(row, field);
5073
+ if (display) {
5074
+ if (!display.has(value)) {
5075
+ continue;
5076
+ }
5077
+ value = display.get(value);
5078
+ }
5079
+ const valueType = nxeValueDataType(value);
5080
+ type = type ?? valueType;
5081
+ if (type && valueType && type !== valueType) {
5082
+ type = 'string';
5083
+ break;
5084
+ }
5085
+ }
5086
+ if (type) {
5087
+ out.set(col, { field, type });
5088
+ }
5089
+ }
5090
+ return out;
5091
+ } });
5092
+ /** A lookup's value → raw display value, first match winning as in `lookupText`; null without one. */
5093
+ lookupDisplayMap(col) {
5094
+ const lookup = col.lookup();
5095
+ if (!lookup) {
5096
+ return null;
5097
+ }
5098
+ const valueExpr = lookup.valueExpr ?? 'lovCode';
5099
+ const displayExpr = lookup.displayExpr ?? 'lovName';
5100
+ const map = new Map();
5101
+ for (const item of lookup.dataSource ?? []) {
5102
+ const key = nxeResolveExpr(valueExpr, item);
5103
+ if (!map.has(key)) {
5104
+ map.set(key, nxeResolveExpr(displayExpr, item));
5105
+ }
5106
+ }
5107
+ return map;
5108
+ }
5109
+ /**
5110
+ * The column's effective type - its declared `dataType`, else the inferred one - which is what
5111
+ * dx's `column.dataType` holds after inference. Every type-driven decision reads this, never
5112
+ * `col.dataType()` directly, or an undeclared column would behave as text in one place and
5113
+ * as a number in the next.
5114
+ */
5115
+ colType(col) {
5116
+ return col.dataType() ?? this.inferredTypes().get(col)?.type;
5117
+ }
3475
5118
  /** The active sort: an explicit user click, else the first column declaring `sortOrder`. */
3476
5119
  effectiveSort() {
5120
+ const bound = this.sortModel();
5121
+ if (bound !== undefined) {
5122
+ const first = bound[0];
5123
+ return first ? { field: first.selector, dir: first.desc ? 'desc' : 'asc' } : null;
5124
+ }
3477
5125
  if (this.sortInit()) {
3478
5126
  return this.sort();
3479
5127
  }
3480
5128
  const sorted = this.columns().find((c) => c.sortOrder());
3481
- return sorted ? { field: sorted.dataField(), dir: sorted.sortOrder() } : null;
5129
+ if (sorted) {
5130
+ return { field: sorted.dataField(), dir: sorted.sortOrder() };
5131
+ }
5132
+ const remote = this.remote();
5133
+ const initial = remote ? untracked(() => remote.options().sort[0]) : undefined;
5134
+ return initial ? { field: initial.selector, dir: initial.desc ? 'desc' : 'asc' } : null;
5135
+ }
5136
+ /** A user sort change: the grid's own state, and the controlled `sort` when it is in play. */
5137
+ setSort(next) {
5138
+ this.sortInit.set(true);
5139
+ this.sort.set(next);
5140
+ if (this.sortModel() !== undefined || this.remote()) {
5141
+ this.sortModel.set(next ? [{ selector: next.field, desc: next.dir === 'desc' }] : []);
5142
+ }
3482
5143
  }
3483
5144
  // --- Rendering helpers -----------------------------------------------------------------------
5145
+ /**
5146
+ * dx `getAlignmentByDataType` (m_columns_controller_utils.js) over the effective type:
5147
+ * number right, boolean centre, everything else left. A lookup aligns by its display text
5148
+ * (dx `lookup.dataType`, :901), which is text - so left, even over numeric codes.
5149
+ */
5150
+ /**
5151
+ * Columns whose cellTemplate draws a check box (nxe-check-box, mat-checkbox, a native
5152
+ * checkbox input or anything with role=checkbox). align() centres them like a boolean column,
5153
+ * header and cells, unless the column asks for center or right. dx centres only by dataType,
5154
+ * so a template check box over non-boolean data (or no dataField) would sit at the left there;
5155
+ * a check box reads as a boolean whatever feeds it, so here it is centred too.
5156
+ *
5157
+ * Sticky, as an inferred type is: a column that drew a check box keeps its alignment when a
5158
+ * filter or an empty page leaves no row to look at. A column that goes away drops out.
5159
+ */
5160
+ templateCheckboxColumns = signal(new Set(), /* @ts-ignore */
5161
+ ...(ngDevMode ? [{ debugName: "templateCheckboxColumns" }] : /* istanbul ignore next */ []));
5162
+ detectTemplateCheckboxColumns() {
5163
+ const cols = this.visibleColumns();
5164
+ const current = this.templateCheckboxColumns();
5165
+ let next = null;
5166
+ for (const col of current) {
5167
+ if (!cols.includes(col)) {
5168
+ (next ??= new Set(current)).delete(col);
5169
+ }
5170
+ }
5171
+ const candidates = cols.filter((c) => !!c.cellTemplate() && !current.has(c));
5172
+ if (candidates.length) {
5173
+ const host = this.elementRef.nativeElement;
5174
+ // This grid's own table and rows: a nested master-detail grid's come later, and its
5175
+ // rows live in a master-detail row, never in one of these data rows' cells' place.
5176
+ const table = host.querySelector('.nxe-datagrid-content > .nxe-datagrid-table');
5177
+ // A few rows are enough to know what a template draws, and keep this cheap on long pages.
5178
+ const rows = table
5179
+ ? Array.from(table.querySelectorAll(':scope > tbody > tr.nxe-data-row')).slice(0, 20)
5180
+ : [];
5181
+ const lead = this.leadCellCount();
5182
+ for (const col of candidates) {
5183
+ const index = lead + cols.indexOf(col) + 1;
5184
+ const drawsCheckbox = rows.some((tr) => tr.querySelector(`:scope > td[aria-colindex="${index}"] :is(nxe-check-box, mat-checkbox, input[type="checkbox"], [role="checkbox"])`));
5185
+ if (drawsCheckbox) {
5186
+ (next ??= new Set(current)).add(col);
5187
+ }
5188
+ }
5189
+ }
5190
+ if (next) {
5191
+ this.templateCheckboxColumns.set(next);
5192
+ }
5193
+ }
5194
+ /** Boolean data (declared or inferred) or a cellTemplate seen drawing a check box. */
5195
+ isCheckboxColumn(col) {
5196
+ return (this.colType(col) === 'boolean' && !col.lookup()) || this.templateCheckboxColumns().has(col);
5197
+ }
3484
5198
  align(col) {
3485
5199
  const a = col.alignment();
5200
+ // A check box column - boolean data, or a cellTemplate that draws a check box - is centred,
5201
+ // header and cells, unless it asks for center or right. 'left' counts as the default here:
5202
+ // apps bind alignment="left" onto every column that has none of its own (Bright's generic
5203
+ // list does), and that must not pin a check box to the left.
5204
+ if (a !== 'center' && a !== 'right' && this.isCheckboxColumn(col)) {
5205
+ return 'center';
5206
+ }
3486
5207
  if (a) {
3487
5208
  return a;
3488
5209
  }
3489
- switch (col.dataType()) {
5210
+ if (col.lookup()) {
5211
+ return 'left';
5212
+ }
5213
+ switch (this.colType(col)) {
3490
5214
  case 'number':
3491
5215
  return 'right';
3492
5216
  case 'boolean':
@@ -3495,6 +5219,14 @@ class NxeDataGridComponent {
3495
5219
  return 'left';
3496
5220
  }
3497
5221
  }
5222
+ /**
5223
+ * The filter cell's text-align: dx sets the filter editor's input to the column alignment
5224
+ * unless it is centre (m_filter_row.js:390-395).
5225
+ */
5226
+ filterCellAlign(col) {
5227
+ const a = this.align(col);
5228
+ return a === 'center' ? null : a;
5229
+ }
3498
5230
  colWidth(col) {
3499
5231
  const manual = this.widthOverride().get(col.dataField());
3500
5232
  if (manual != null) {
@@ -3515,15 +5247,16 @@ class NxeDataGridComponent {
3515
5247
  }
3516
5248
  return row[dataField];
3517
5249
  }
3518
- /** Resolve the displayed cell text: lookup > customizeText > dataType/format > raw string. */
3519
- /** dx parity: boolean values render as read-only checkboxes (dx dataType inference did this). */
3520
- isBooleanCell(col, row) {
3521
- if (col.dataType() === 'boolean') {
3522
- return true;
3523
- }
3524
- const v = this.getRaw(row, col.dataField());
3525
- return v === true || v === false;
5250
+ /**
5251
+ * dx `showEditorAlways` (m_columns_controller.js:912): a boolean-typed column - declared or
5252
+ * inferred - renders a checkbox, unless it is a lookup or has its own cellTemplate. Decided
5253
+ * per column, not per value: a column whose values are mixed infers 'string' and shows text,
5254
+ * as in dx.
5255
+ */
5256
+ isBooleanCell(col) {
5257
+ return this.colType(col) === 'boolean' && !col.lookup() && !col.cellTemplate();
3526
5258
  }
5259
+ /** Resolve the displayed cell text: lookup > customizeText > dataType/format > raw string. */
3527
5260
  cellText(col, row) {
3528
5261
  const raw = this.getRaw(row, col.dataField());
3529
5262
  const valueText = this.lookupText(col, raw) ?? this.defaultFormat(col, raw);
@@ -3538,11 +5271,12 @@ class NxeDataGridComponent {
3538
5271
  }
3539
5272
  const valueExpr = lookup.valueExpr ?? 'lovCode';
3540
5273
  const displayExpr = lookup.displayExpr ?? 'lovName';
3541
- const match = (lookup.dataSource ?? []).find((item) => item?.[valueExpr] === raw);
3542
- return match ? String(match[displayExpr] ?? raw) : String(raw);
5274
+ const match = (lookup.dataSource ?? []).find((item) => nxeResolveExpr(valueExpr, item) === raw);
5275
+ return match ? nxeResolveText(displayExpr, match) || String(raw) : String(raw);
3543
5276
  }
3544
5277
  defaultFormat(col, raw) {
3545
- return nxeDefaultFormat(col.dataType(), col.format(), raw);
5278
+ // The locale is read here so a language switch re-renders date cells (month names).
5279
+ return nxeDefaultFormat(this.colType(col), col.format(), raw, this.separators, this.messages.locale());
3546
5280
  }
3547
5281
  templateFor(name) {
3548
5282
  if (!name) {
@@ -3582,7 +5316,7 @@ class NxeDataGridComponent {
3582
5316
  column: {
3583
5317
  dataField: col.dataField(),
3584
5318
  caption: col.caption(),
3585
- dataType: col.dataType(),
5319
+ dataType: this.colType(col),
3586
5320
  format: col.format(),
3587
5321
  },
3588
5322
  // dx's setValue changes the cell value; it does not end the edit. The editor
@@ -3631,7 +5365,7 @@ class NxeDataGridComponent {
3631
5365
  return;
3632
5366
  }
3633
5367
  const host = this.elementRef.nativeElement;
3634
- const table = host.querySelector('.dx-datagrid-content > .dx-datagrid-table');
5368
+ const table = host.querySelector('.nxe-datagrid-content > .nxe-datagrid-table');
3635
5369
  if (!table) {
3636
5370
  return;
3637
5371
  }
@@ -3689,18 +5423,30 @@ class NxeDataGridComponent {
3689
5423
  if (!this.onCellPrepared.observed) {
3690
5424
  continue;
3691
5425
  }
3692
- let colSeen = 0;
5426
+ const lead = this.leadCellCount();
3693
5427
  for (const td of Array.from(tr.cells)) {
3694
- const isCommand = td.classList.contains('dx-command-expand') ||
3695
- td.classList.contains('dx-command-select') ||
3696
- td.classList.contains('dx-command-edit') ||
3697
- td.classList.contains('dx-group-cell') ||
3698
- td.classList.contains('nxe-band-cell');
3699
- const col = isCommand ? undefined : cols[colSeen];
3700
- const columnIndex = isCommand ? -1 : colSeen;
3701
- if (!isCommand) {
3702
- colSeen++;
3703
- }
5428
+ // Read the column off `aria-colindex` rather than counting cells.
5429
+ //
5430
+ // Counting was wrong in four separate ways, all silent, and every one of them
5431
+ // the shape this event exists to serve. A data row's grouping indent cells
5432
+ // carry only `nxe-datagrid-group-space`, which no class test here listed, so
5433
+ // in a grouped grid every cell reported the column one place to its left per
5434
+ // group level and the last column was never reported at all. `mergeColumns`
5435
+ // deletes the covered `<td>`s, so a continuation row is short and shifts the
5436
+ // other way. A band adds a header row whose cells are not leaves. And the
5437
+ // counter restarted per `<tr>`, so a banded header row numbered from zero.
5438
+ //
5439
+ // `aria-colindex` numbers every data cell, leaf header cell, filter cell and
5440
+ // summary cell, and already includes the lead command columns, so it survives
5441
+ // all four. Command cells carry it too, as dx's do, but their numbers fall
5442
+ // outside the data columns' range (at or below the lead count, or past the last
5443
+ // column). Those, the cells without one (a band) and the group caption - dx
5444
+ // numbers it as the cell after its toggle, which can be a data column's number
5445
+ // - are not columns and are reported with `column: null` and index -1.
5446
+ const colAttr = td.classList.contains('nxe-group-cell') ? null : td.getAttribute('aria-colindex');
5447
+ const at = colAttr ? Number(colAttr) - 1 - lead : -1;
5448
+ const col = at >= 0 ? cols[at] : undefined;
5449
+ const columnIndex = col ? at : -1;
3704
5450
  const value = col && rowType === 'data' && info.data
3705
5451
  ? this.getRaw(info.data, col.dataField())
3706
5452
  : undefined;
@@ -3716,7 +5462,7 @@ class NxeDataGridComponent {
3716
5462
  ? {
3717
5463
  dataField: col.dataField(),
3718
5464
  caption: col.caption(),
3719
- dataType: col.dataType(),
5465
+ dataType: this.colType(col),
3720
5466
  index: columnIndex,
3721
5467
  }
3722
5468
  : null,
@@ -3733,6 +5479,176 @@ class NxeDataGridComponent {
3733
5479
  this.preparedDepth--;
3734
5480
  }
3735
5481
  }
5482
+ // --- Keyboard navigation (WAI-ARIA grid pattern) ---------------------------------------------
5483
+ /**
5484
+ * The single cell holding the grid's tab stop.
5485
+ *
5486
+ * A grid is one stop in the tab order, not one per cell — a 200-row table would otherwise be
5487
+ * 1,400 tab presses to get past. Row `-1` is the header, so ArrowUp from the first data row
5488
+ * reaches the column headers rather than stopping dead.
5489
+ */
5490
+ focusedCell = signal({ row: -1, col: 0 }, /* @ts-ignore */
5491
+ ...(ngDevMode ? [{ debugName: "focusedCell" }] : /* istanbul ignore next */ []));
5492
+ /**
5493
+ * The rows the keyboard and `aria-rowindex` address: the data rows actually RENDERED.
5494
+ *
5495
+ * Emphatically not `pagedRows()`. That is the page's data BEFORE grouping and tree collapse
5496
+ * take rows out of the DOM, while `renderRows` renumbers densely over what it emits - so with
5497
+ * one group collapsed `pagedRows()` counts three rows where two have a cell to focus. Every
5498
+ * consumer of the count is then wrong in a different way: `aria-rowcount` claims a row a
5499
+ * reader cannot reach, Ctrl+End targets an index with no DOM behind it and movement silently
5500
+ * does nothing, and Enter and Space read `pagedRows()[row]` - which is a DIFFERENT row than
5501
+ * the one under the cursor, so Space selects something the user is not looking at.
5502
+ */
5503
+ navigableRows = computed(() => this.renderRows()
5504
+ .filter((r) => r.kind === 'data')
5505
+ .map((r) => r.row), /* @ts-ignore */
5506
+ ...(ngDevMode ? [{ debugName: "navigableRows" }] : /* istanbul ignore next */ []));
5507
+ isCellTabStop(row, col) {
5508
+ const cell = this.focusedCell();
5509
+ return cell.row === row && cell.col === col;
5510
+ }
5511
+ focusCell(row, col) {
5512
+ this.focusedCell.set({ row, col });
5513
+ }
5514
+ /** dx marks the sorted column for assistive tech as well as with the arrow glyph. */
5515
+ ariaSort(col) {
5516
+ if (!col.allowSorting()) {
5517
+ return null;
5518
+ }
5519
+ const dir = this.sortDir(col);
5520
+ return dir === 'asc' ? 'ascending' : dir === 'desc' ? 'descending' : null;
5521
+ }
5522
+ /**
5523
+ * The DOM cell at a logical position, addressed by `aria-colindex` rather than by offset.
5524
+ *
5525
+ * Offsets break twice over. A band cell carries `role="columnheader"` too, so counting
5526
+ * header cells finds bands as well as leaves; and `mergeColumns` deletes the covered `<td>`s,
5527
+ * so a data row can be short. `aria-colindex` is emitted on every cell and already includes
5528
+ * the lead command columns, so it names the position exactly rather than by counting.
5529
+ *
5530
+ * Both lookups stay inside this grid's own table: a master-detail grid renders a whole second
5531
+ * grid into a `<td>`, and its rows would otherwise answer to the outer grid's arrow keys.
5532
+ */
5533
+ cellAt(row, col) {
5534
+ const host = this.elementRef.nativeElement;
5535
+ const colIndex = this.leadCellCount() + col + 1;
5536
+ if (row < 0) {
5537
+ return nxeHeaderLeafCells(host)[col] ?? null;
5538
+ }
5539
+ const tbody = host.querySelector('.nxe-datagrid-content > .nxe-datagrid-table > tbody');
5540
+ return (tbody?.querySelector(`:scope > .nxe-data-row[aria-rowindex="${row + 1}"] > [aria-colindex="${colIndex}"]`) ?? null);
5541
+ }
5542
+ /** Move the tab stop and put real DOM focus on the cell that now holds it. */
5543
+ moveFocus(row, col) {
5544
+ const maxRow = this.navigableRows().length - 1;
5545
+ const maxCol = this.visibleColumns().length - 1;
5546
+ const targetRow = Math.max(-1, Math.min(row, maxRow));
5547
+ const nextCol = Math.max(0, Math.min(col, maxCol));
5548
+ // A merged cell's rowspan removes the `<td>`s beneath it, so a position can have no cell
5549
+ // of its own. Keep going the way we were headed until one does — landing on the covering
5550
+ // cell instead would make ArrowDown into a merged block a dead end.
5551
+ const step = targetRow > this.focusedCell().row ? 1 : -1;
5552
+ let probe = targetRow;
5553
+ let cell = this.cellAt(probe, nextCol);
5554
+ while (!cell && probe + step >= -1 && probe + step <= maxRow) {
5555
+ probe += step;
5556
+ cell = this.cellAt(probe, nextCol);
5557
+ }
5558
+ if (!cell) {
5559
+ return;
5560
+ }
5561
+ this.focusedCell.set({ row: probe, col: nextCol });
5562
+ cell.focus();
5563
+ }
5564
+ /**
5565
+ * Arrow-key movement over cells.
5566
+ *
5567
+ * Deliberately not bound to the rowsview: a projected editor inside a cell needs its own
5568
+ * arrow keys, so anything originating inside an editor is left alone.
5569
+ */
5570
+ onGridKeydown(event) {
5571
+ const target = event.target;
5572
+ if (target &&
5573
+ target.closest('input, textarea, select, [contenteditable="true"], .nxe-grid-cell-editor-check')) {
5574
+ return;
5575
+ }
5576
+ const { row, col } = this.focusedCell();
5577
+ const rows = this.navigableRows();
5578
+ const lastRow = rows.length - 1;
5579
+ const lastCol = this.visibleColumns().length - 1;
5580
+ switch (event.key) {
5581
+ case 'ArrowRight':
5582
+ event.preventDefault();
5583
+ this.moveFocus(row, col + 1);
5584
+ break;
5585
+ case 'ArrowLeft':
5586
+ event.preventDefault();
5587
+ this.moveFocus(row, col - 1);
5588
+ break;
5589
+ case 'ArrowDown':
5590
+ event.preventDefault();
5591
+ this.moveFocus(row + 1, col);
5592
+ break;
5593
+ case 'ArrowUp':
5594
+ event.preventDefault();
5595
+ this.moveFocus(row - 1, col);
5596
+ break;
5597
+ case 'Home':
5598
+ event.preventDefault();
5599
+ this.moveFocus(event.ctrlKey ? -1 : row, 0);
5600
+ break;
5601
+ case 'End':
5602
+ event.preventDefault();
5603
+ this.moveFocus(event.ctrlKey ? lastRow : row, lastCol);
5604
+ break;
5605
+ case 'PageDown':
5606
+ event.preventDefault();
5607
+ this.pagingEnabled() ? this.nextPage() : this.moveFocus(lastRow, col);
5608
+ break;
5609
+ case 'PageUp':
5610
+ event.preventDefault();
5611
+ this.pagingEnabled() ? this.prevPage() : this.moveFocus(-1, col);
5612
+ break;
5613
+ case 'Enter':
5614
+ case 'F2': {
5615
+ if (row < 0) {
5616
+ // On a header, Enter sorts — the same thing a click does.
5617
+ event.preventDefault();
5618
+ const col0 = this.visibleColumns()[col];
5619
+ if (col0) {
5620
+ this.onHeaderClick(col0);
5621
+ }
5622
+ break;
5623
+ }
5624
+ const rowData = rows[row];
5625
+ const column = this.visibleColumns()[col];
5626
+ if (rowData && column && this.cellEditingEnabled() && this.editableCell(column)) {
5627
+ event.preventDefault();
5628
+ if (this.booleanInPlace(column)) {
5629
+ // The checkbox is already the editor, and it takes no focus of its own:
5630
+ // Enter does what a click on it does.
5631
+ this.commitCellEdit(rowData, column, this.getRaw(rowData, column.dataField()) !== true);
5632
+ break;
5633
+ }
5634
+ this.editingCell.set({ row: rowData, field: column.dataField() });
5635
+ }
5636
+ break;
5637
+ }
5638
+ case ' ': {
5639
+ if (row < 0 || this.selectionMode() === 'none') {
5640
+ break;
5641
+ }
5642
+ event.preventDefault();
5643
+ const rowData = rows[row];
5644
+ if (rowData) {
5645
+ this.toggleRow(rowData);
5646
+ }
5647
+ break;
5648
+ }
5649
+ default:
5650
+ }
5651
+ }
3736
5652
  // --- Interaction -----------------------------------------------------------------------------
3737
5653
  /** True when this header can start a drag gesture (to the group panel and/or a reorder). */
3738
5654
  headerDraggable(col) {
@@ -3753,9 +5669,12 @@ class NxeDataGridComponent {
3753
5669
  onChipPointerDown(col, level, event) {
3754
5670
  this.drag.startChipDrag(col, level, event);
3755
5671
  }
3756
- /** Chip click (dx): toggle the grouped column's sort — unless this click ends a drag. */
5672
+ /**
5673
+ * Chip click (dx): toggle the grouped column's sort — unless this click ends a drag. dx sends
5674
+ * it through the same changeSortOrder as a header click (m_sorting.js:134), so the same gate.
5675
+ */
3757
5676
  onChipClick(col) {
3758
- if (this.drag.consumeClick()) {
5677
+ if (this.drag.consumeClick() || !this.clickSorts(col)) {
3759
5678
  return;
3760
5679
  }
3761
5680
  this.sortGroupColumn(col);
@@ -3764,29 +5683,66 @@ class NxeDataGridComponent {
3764
5683
  if (this.drag.consumeClick() || this.resize.consumeClick()) {
3765
5684
  return; // this click is the tail of a drag/resize gesture, not a sort request
3766
5685
  }
3767
- if (!col.allowSorting() || this.sorting()?.mode === 'none') {
5686
+ if (!this.clickSorts(col)) {
3768
5687
  return;
3769
5688
  }
3770
5689
  const field = col.dataField();
3771
5690
  if (!field) {
3772
5691
  return;
3773
5692
  }
3774
- this.sortInit.set(true);
3775
5693
  const current = this.effectiveSort();
3776
5694
  if (!current || current.field !== field) {
3777
- this.sort.set({ field, dir: 'asc' });
5695
+ this.setSort({ field, dir: 'asc' });
3778
5696
  }
3779
5697
  else if (current.dir === 'asc') {
3780
- this.sort.set({ field, dir: 'desc' });
5698
+ this.setSort({ field, dir: 'desc' });
3781
5699
  }
3782
5700
  else {
3783
- this.sort.set(null);
5701
+ this.setSort(null);
3784
5702
  }
3785
5703
  }
3786
5704
  sortDir(col) {
3787
5705
  const s = this.effectiveSort();
3788
5706
  return s && s.field === col.dataField() ? s.dir : null;
3789
5707
  }
5708
+ /**
5709
+ * `sorting.mode` as dx's sorting code reads the option: unbound is dx's default 'single', an
5710
+ * object gives its `mode` (default 'single'), and anything else - `[sorting]="false"` or
5711
+ * `null` - has no mode at all.
5712
+ */
5713
+ sortingMode() {
5714
+ const s = this.sorting();
5715
+ if (s === undefined) {
5716
+ return 'single';
5717
+ }
5718
+ return s ? (s.mode ?? 'single') : undefined;
5719
+ }
5720
+ /**
5721
+ * dx changeSortOrder (m_columns_controller.js:758-766) acts only in mode 'single' or
5722
+ * 'multiple', and only on a column that allows sorting. `[sorting]="false"` still renders the
5723
+ * sort slot, but a header or chip click never sorts.
5724
+ */
5725
+ clickSorts(col) {
5726
+ const mode = this.sortingMode();
5727
+ return col.allowSorting() && (mode === 'single' || mode === 'multiple');
5728
+ }
5729
+ /**
5730
+ * dx `isSortingAllowed` (m_sorting_mixin.js:37): a sortable column, unless the mode is
5731
+ * literally 'none'. A column with no dataField is never sortable (m_columns_controller.js:
5732
+ * 1383-1388). Gates the `nxe-datagrid-action` class, and the sort slot of an unsorted column.
5733
+ */
5734
+ sortAllowed(col) {
5735
+ const s = this.sorting();
5736
+ return col.allowSorting() && !!col.dataField() && (s ? s.mode : undefined) !== 'none';
5737
+ }
5738
+ /**
5739
+ * Whether the header renders dx's sort span: always for the sorted column, and as an empty
5740
+ * `nxe-sort-none` placeholder for any other sortable one (m_sorting_mixin.js:38-46). The
5741
+ * placeholder is display:none in the fixed table and 14px wide in the best-fit pass.
5742
+ */
5743
+ sortSlot(col) {
5744
+ return this.sortDir(col) !== null || this.sortAllowed(col);
5745
+ }
3790
5746
  onRowEnter(index) {
3791
5747
  if (this.hoverStateEnabled()) {
3792
5748
  this.hoveredIndex.set(index);
@@ -3799,6 +5755,12 @@ class NxeDataGridComponent {
3799
5755
  return this.hoveredIndex() === index;
3800
5756
  }
3801
5757
  isSelected(row) {
5758
+ if (this.remote()) {
5759
+ return this.selectedKeySet().has(this.rowKey(row));
5760
+ }
5761
+ if (this.recursiveTreeSelection()) {
5762
+ return this.treeCheckState().get(this.rowKey(row)) === 'all';
5763
+ }
3802
5764
  return this.treeMode
3803
5765
  ? this.selectedRowKeys().includes(this.rowKey(row))
3804
5766
  : this.selectedSet().has(row);
@@ -3821,6 +5783,10 @@ class NxeDataGridComponent {
3821
5783
  this.toggleRow(row);
3822
5784
  }
3823
5785
  toggleRow(row) {
5786
+ if (this.remote()) {
5787
+ this.toggleRemoteSelection(row);
5788
+ return;
5789
+ }
3824
5790
  if (this.treeMode) {
3825
5791
  this.toggleTreeSelection(row);
3826
5792
  return;
@@ -3847,8 +5813,12 @@ class NxeDataGridComponent {
3847
5813
  }
3848
5814
  toggleSelectAll(event) {
3849
5815
  event.stopPropagation();
5816
+ if (this.remote()) {
5817
+ this.toggleRemoteSelectAll();
5818
+ return;
5819
+ }
3850
5820
  if (this.treeMode) {
3851
- const allKeys = (this.dataSource() ?? []).map((r) => this.rowKey(r));
5821
+ const allKeys = this.sourceRows().map((r) => this.rowKey(r));
3852
5822
  if (this.allSelected()) {
3853
5823
  this.selectedRowKeys.set([]);
3854
5824
  this.emitTreeSelectionChanged([], [], allKeys);
@@ -3869,6 +5839,39 @@ class NxeDataGridComponent {
3869
5839
  this.emitSelectionChanged(rows, []);
3870
5840
  }
3871
5841
  }
5842
+ /**
5843
+ * `clearSelection()` (`scope` null: every selected row) and `deselectAll()` (`scope`: the rows
5844
+ * on show). One `onSelectionChanged` with the dropped keys as deselected, none when nothing
5845
+ * was selected.
5846
+ */
5847
+ deselectRows(scope) {
5848
+ if (this.remote() || this.treeMode) {
5849
+ const previous = this.selectedRowKeys();
5850
+ const drop = scope ? new Set(scope.map((r) => this.rowKey(r))) : null;
5851
+ const removed = drop ? previous.filter((k) => drop.has(k)) : previous;
5852
+ if (!removed.length) {
5853
+ return;
5854
+ }
5855
+ const next = drop ? previous.filter((k) => !drop.has(k)) : [];
5856
+ this.selectedRowKeys.set(next);
5857
+ if (this.remote()) {
5858
+ this.emitRemoteSelectionChanged(next, [], removed);
5859
+ }
5860
+ else {
5861
+ this.emitTreeSelectionChanged(next, [], removed);
5862
+ }
5863
+ return;
5864
+ }
5865
+ const previous = this.selectedSet();
5866
+ const removed = scope ? scope.filter((r) => previous.has(r)) : [...previous];
5867
+ if (!removed.length) {
5868
+ return;
5869
+ }
5870
+ const next = new Set(previous);
5871
+ removed.forEach((r) => next.delete(r));
5872
+ this.selectedSet.set(next);
5873
+ this.emitSelectionChanged([], removed);
5874
+ }
3872
5875
  emitSelectionChanged(currentSelected, currentDeselected) {
3873
5876
  const data = this.selectedRowsData();
3874
5877
  this.onSelectionChanged.emit({
@@ -3881,26 +5884,26 @@ class NxeDataGridComponent {
3881
5884
  });
3882
5885
  }
3883
5886
  rowKeys(rows) {
3884
- const key = this.keyExpr();
5887
+ const key = this.keyField();
3885
5888
  return key ? rows.map((r) => r?.[key]) : rows;
3886
5889
  }
3887
5890
  // --- Filter row (dxo-filter-row): typed editors + dx operation-chooser menu ------------------
3888
5891
  static STRING_OPS = [
3889
- { op: 'contains', label: 'Contains' },
3890
- { op: 'notcontains', label: 'Does not contain' },
3891
- { op: 'startswith', label: 'Starts with' },
3892
- { op: 'endswith', label: 'Ends with' },
3893
- { op: '=', label: 'Equals' },
3894
- { op: '<>', label: 'Does not equal' },
5892
+ { op: 'contains', label: 'grid.op.contains' },
5893
+ { op: 'notcontains', label: 'grid.op.notContains' },
5894
+ { op: 'startswith', label: 'grid.op.startsWith' },
5895
+ { op: 'endswith', label: 'grid.op.endsWith' },
5896
+ { op: '=', label: 'grid.op.equals' },
5897
+ { op: '<>', label: 'grid.op.notEquals' },
3895
5898
  ];
3896
5899
  static SCALAR_OPS = [
3897
- { op: '=', label: 'Equals' },
3898
- { op: '<>', label: 'Does not equal' },
3899
- { op: '<', label: 'Less than' },
3900
- { op: '>', label: 'Greater than' },
3901
- { op: '<=', label: 'Less than or equal to' },
3902
- { op: '>=', label: 'Greater than or equal to' },
3903
- { op: 'between', label: 'Between' },
5900
+ { op: '=', label: 'grid.op.equals' },
5901
+ { op: '<>', label: 'grid.op.notEquals' },
5902
+ { op: '<', label: 'grid.op.lessThan' },
5903
+ { op: '>', label: 'grid.op.greaterThan' },
5904
+ { op: '<=', label: 'grid.op.lessThanOrEqual' },
5905
+ { op: '>=', label: 'grid.op.greaterThanOrEqual' },
5906
+ { op: 'between', label: 'grid.op.between' },
3904
5907
  ];
3905
5908
  static OP_ICON_SLUGS = {
3906
5909
  contains: 'contains',
@@ -3925,7 +5928,7 @@ class NxeDataGridComponent {
3925
5928
  if (col.lookup()) {
3926
5929
  return 'lookup';
3927
5930
  }
3928
- const type = col.dataType();
5931
+ const type = this.colType(col);
3929
5932
  return type === 'boolean' || type === 'date' || type === 'number' ? type : 'text';
3930
5933
  }
3931
5934
  /** dx shows the operation-chooser magnifier only for typed (non-select) editors. */
@@ -3945,13 +5948,20 @@ class NxeDataGridComponent {
3945
5948
  frText2(col) {
3946
5949
  return this.filterRowState().get(col.dataField())?.text2 ?? '';
3947
5950
  }
3948
- frOpIconClass(col) {
3949
- return `dx-icon-filter-operation-${NxeDataGridComponent.OP_ICON_SLUGS[this.frOp(col)]}`;
5951
+ /**
5952
+ * The chooser glyph: dx's magnifier ('default') until an operation is picked from the menu,
5953
+ * then that operation's own. Only the slug varies - the template emits `nxe-icon-filter-operation-*`
5954
+ * and nothing prefixed `dx-`, which the compat mirror would strip for want of an nxe twin.
5955
+ */
5956
+ frOpIconSlug(col) {
5957
+ const entry = this.filterRowState().get(col.dataField());
5958
+ return entry?.chosen ? NxeDataGridComponent.OP_ICON_SLUGS[entry.op] : 'default';
3950
5959
  }
3951
5960
  opIconSlug(op) {
3952
5961
  return NxeDataGridComponent.OP_ICON_SLUGS[op];
3953
5962
  }
3954
5963
  setFilterRowText(col, text, second = false) {
5964
+ this.remoteTyping = true;
3955
5965
  const field = col.dataField();
3956
5966
  const next = new Map(this.filterRowState());
3957
5967
  const prev = next.get(field) ?? { op: this.frDefaultOp(col), text: '' };
@@ -3987,9 +5997,7 @@ class NxeDataGridComponent {
3987
5997
  if (!col) {
3988
5998
  return [];
3989
5999
  }
3990
- return this.frEditorKind(col) === 'text'
3991
- ? NxeDataGridComponent.STRING_OPS
3992
- : NxeDataGridComponent.SCALAR_OPS;
6000
+ return (this.frEditorKind(col) === 'text' ? NxeDataGridComponent.STRING_OPS : NxeDataGridComponent.SCALAR_OPS).map(({ op, label }) => ({ op, label: this.messages.t(label) }));
3993
6001
  }
3994
6002
  selectFilterOp(op) {
3995
6003
  const col = this.frMenuColumn();
@@ -3997,7 +6005,12 @@ class NxeDataGridComponent {
3997
6005
  const field = col.dataField();
3998
6006
  const next = new Map(this.filterRowState());
3999
6007
  const prev = next.get(field) ?? { op: this.frDefaultOp(col), text: '' };
4000
- next.set(field, { ...prev, op, ...(op !== 'between' ? { text2: undefined } : {}) });
6008
+ next.set(field, {
6009
+ ...prev,
6010
+ op,
6011
+ chosen: true,
6012
+ ...(op !== 'between' ? { text2: undefined } : {}),
6013
+ });
4001
6014
  this.filterRowState.set(next);
4002
6015
  }
4003
6016
  this.closeFilterOpMenu();
@@ -4033,11 +6046,11 @@ class NxeDataGridComponent {
4033
6046
  * date columns → a `yyyy-MM-dd` day key (the tree's leaf unit), else the raw value.
4034
6047
  */
4035
6048
  hfKey(col, raw) {
4036
- return nxeHfKey(raw, col.dataType() === 'date');
6049
+ return nxeHfKey(raw, this.colType(col) === 'date');
4037
6050
  }
4038
6051
  /** dx does NOT cascade header-filter lists for client arrays (bright audit V6). */
4039
6052
  hfBaseRows() {
4040
- return (this.dataSource() ?? []);
6053
+ return this.sourceRows();
4041
6054
  }
4042
6055
  hfBoolLabel(col, raw) {
4043
6056
  if (col.lookup()) {
@@ -4046,7 +6059,7 @@ class NxeDataGridComponent {
4046
6059
  if (raw === true) {
4047
6060
  return 'Yes';
4048
6061
  }
4049
- if (raw === false || col.dataType() === 'boolean') {
6062
+ if (raw === false || this.colType(col) === 'boolean') {
4050
6063
  return 'No';
4051
6064
  }
4052
6065
  return null;
@@ -4122,14 +6135,15 @@ class NxeDataGridComponent {
4122
6135
  this.hfOverlayRef?.dispose();
4123
6136
  this.frMenuOverlayRef?.dispose();
4124
6137
  this.resizeObserver?.disconnect();
6138
+ this.wrapped?.cancel();
4125
6139
  }
4126
6140
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDataGridComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4127
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeDataGridComponent, isStandalone: true, selector: "nxe-data-grid, nxe-tree-list", inputs: { dataSource: { classPropertyName: "dataSource", publicName: "dataSource", isSignal: true, isRequired: false, transformFunction: null }, keyExpr: { classPropertyName: "keyExpr", publicName: "keyExpr", isSignal: true, isRequired: false, transformFunction: null }, hoverStateEnabledInput: { classPropertyName: "hoverStateEnabledInput", publicName: "hoverStateEnabled", isSignal: true, isRequired: false, transformFunction: null }, showColumnLinesInput: { classPropertyName: "showColumnLinesInput", publicName: "showColumnLines", isSignal: true, isRequired: false, transformFunction: null }, showRowLinesInput: { classPropertyName: "showRowLinesInput", publicName: "showRowLines", isSignal: true, isRequired: false, transformFunction: null }, showBordersInput: { classPropertyName: "showBordersInput", publicName: "showBorders", isSignal: true, isRequired: false, transformFunction: null }, wordWrapEnabledInput: { classPropertyName: "wordWrapEnabledInput", publicName: "wordWrapEnabled", isSignal: true, isRequired: false, transformFunction: null }, columnAutoWidthInput: { classPropertyName: "columnAutoWidthInput", publicName: "columnAutoWidth", isSignal: true, isRequired: false, transformFunction: null }, rowAlternationEnabledInput: { classPropertyName: "rowAlternationEnabledInput", publicName: "rowAlternationEnabled", isSignal: true, isRequired: false, transformFunction: null }, remoteOperations: { classPropertyName: "remoteOperations", publicName: "remoteOperations", isSignal: true, isRequired: false, transformFunction: null }, syncLookupFilterValues: { classPropertyName: "syncLookupFilterValues", publicName: "syncLookupFilterValues", isSignal: true, isRequired: false, transformFunction: null }, columnMinWidth: { classPropertyName: "columnMinWidth", publicName: "columnMinWidth", isSignal: true, isRequired: false, transformFunction: null }, noDataTextInput: { classPropertyName: "noDataTextInput", publicName: "noDataText", isSignal: true, isRequired: false, transformFunction: null }, loadPanel: { classPropertyName: "loadPanel", publicName: "loadPanel", isSignal: true, isRequired: false, transformFunction: null }, paging: { classPropertyName: "paging", publicName: "paging", isSignal: true, isRequired: false, transformFunction: null }, filterRow: { classPropertyName: "filterRow", publicName: "filterRow", isSignal: true, isRequired: false, transformFunction: null }, headerFilterOptions: { classPropertyName: "headerFilterOptions", publicName: "headerFilter", isSignal: true, isRequired: false, transformFunction: null }, groupingOptions: { classPropertyName: "groupingOptions", publicName: "grouping", isSignal: true, isRequired: false, transformFunction: null }, groupPanelOptions: { classPropertyName: "groupPanelOptions", publicName: "groupPanel", isSignal: true, isRequired: false, transformFunction: null }, scrolling: { classPropertyName: "scrolling", publicName: "scrolling", isSignal: true, isRequired: false, transformFunction: null }, sorting: { classPropertyName: "sorting", publicName: "sorting", isSignal: true, isRequired: false, transformFunction: null }, allowColumnResizing: { classPropertyName: "allowColumnResizing", publicName: "allowColumnResizing", isSignal: true, isRequired: false, transformFunction: null }, columnResizingMode: { classPropertyName: "columnResizingMode", publicName: "columnResizingMode", isSignal: true, isRequired: false, transformFunction: null }, allowColumnReordering: { classPropertyName: "allowColumnReordering", publicName: "allowColumnReordering", isSignal: true, isRequired: false, transformFunction: null }, repaintChangesOnly: { classPropertyName: "repaintChangesOnly", publicName: "repaintChangesOnly", isSignal: true, isRequired: false, transformFunction: null }, showColumnHeaders: { classPropertyName: "showColumnHeaders", publicName: "showColumnHeaders", isSignal: true, isRequired: false, transformFunction: null }, parentIdExpr: { classPropertyName: "parentIdExpr", publicName: "parentIdExpr", isSignal: true, isRequired: false, transformFunction: null }, dataStructure: { classPropertyName: "dataStructure", publicName: "dataStructure", isSignal: true, isRequired: false, transformFunction: null }, rootValue: { classPropertyName: "rootValue", publicName: "rootValue", isSignal: true, isRequired: false, transformFunction: null }, autoExpandAll: { classPropertyName: "autoExpandAll", publicName: "autoExpandAll", isSignal: true, isRequired: false, transformFunction: null }, expandedRowKeys: { classPropertyName: "expandedRowKeys", publicName: "expandedRowKeys", isSignal: true, isRequired: false, transformFunction: null }, selectedRowKeys: { classPropertyName: "selectedRowKeys", publicName: "selectedRowKeys", isSignal: true, isRequired: false, transformFunction: null }, mergeColumns: { classPropertyName: "mergeColumns", publicName: "mergeColumns", isSignal: true, isRequired: false, transformFunction: null }, rowClass: { classPropertyName: "rowClass", publicName: "rowClass", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { expandedRowKeys: "expandedRowKeysChange", selectedRowKeys: "selectedRowKeysChange", onSelectionChanged: "onSelectionChanged", onRowClick: "onRowClick", onContentReady: "onContentReady", onOptionChanged: "onOptionChanged", onCellPrepared: "onCellPrepared", onRowPrepared: "onRowPrepared", onRowExpanding: "onRowExpanding", onRowExpanded: "onRowExpanded" }, host: { classAttribute: "nxe-widget nxe-data-grid" }, queries: [{ propertyName: "declaredColumns", predicate: NxeColumnComponent, isSignal: true }, { propertyName: "templates", predicate: NxeTemplateDirective, isSignal: true }, { propertyName: "selection", first: true, predicate: NxeSelectionComponent, descendants: true, isSignal: true }, { propertyName: "headerFilter", first: true, predicate: NxeHeaderFilterComponent, descendants: true, isSignal: true }, { propertyName: "grouping", first: true, predicate: NxeGroupingComponent, descendants: true, isSignal: true }, { propertyName: "groupPanel", first: true, predicate: NxeGroupPanelComponent, descendants: true, isSignal: true }, { propertyName: "summaryCmp", first: true, predicate: NxeSummaryComponent, descendants: true, isSignal: true }, { propertyName: "pagingCmp", first: true, predicate: NxePagingComponent, descendants: true, isSignal: true }, { propertyName: "pagerCmp", first: true, predicate: NxePagerComponent, descendants: true, isSignal: true }, { propertyName: "editingCmp", first: true, predicate: NxeEditingComponent, descendants: true, isSignal: true }, { propertyName: "filterRowCmp", first: true, predicate: NxeFilterRowComponent, descendants: true, isSignal: true }, { propertyName: "masterDetail", first: true, predicate: NxeMasterDetailComponent, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "frMenuPanel", first: true, predicate: ["frOpMenu"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"dx-widget dx-datagrid\" [class.dx-datagrid-borders]=\"showBorders()\">\r\n <div class=\"dx-gridbase-container\">\r\n @if (groupPanelVisible()) {\r\n <div class=\"dx-datagrid-header-panel\">\r\n <div class=\"dx-datagrid-group-panel\">\r\n @if (groupColumns().length === 0) {\r\n <span class=\"dx-group-panel-message\">Drag a column header here to group by that column</span>\r\n } @else {\r\n @for (col of groupColumns(); track col.dataField(); let li = $index) {\r\n <!-- dx chip DOM: caption + sort indicator + header-filter button (bright audit) -->\r\n <div\r\n class=\"dx-group-panel-item dx-datagrid-action dx-datagrid-drag-action\"\r\n tabindex=\"0\"\r\n (pointerdown)=\"onChipPointerDown(col, li, $event)\"\r\n (click)=\"onChipClick(col)\">{{ col.caption() }}<div class=\"dx-column-indicators\" role=\"presentation\"><span\r\n class=\"dx-sort\"\r\n [class.dx-sort-up]=\"groupDir(col) === 'asc'\"\r\n [class.dx-sort-down]=\"groupDir(col) === 'desc'\"></span>@if (headerFilterVisible(col)) {<span\r\n class=\"nxe-grid-hf-icon\"\r\n [class.nxe-hf-active]=\"hasHeaderFilter(col)\"\r\n (pointerdown)=\"$event.stopPropagation()\"\r\n (click)=\"openHeaderFilter(col, $event)\"></span>}</div></div>\r\n }\r\n }\r\n </div>\r\n </div>\r\n }\r\n <div\r\n class=\"dx-datagrid-rowsview dx-datagrid-after-headers\"\r\n [class.dx-datagrid-nowrap]=\"!wordWrapEnabled()\">\r\n <div class=\"dx-datagrid-content\">\r\n <table class=\"dx-datagrid-table dx-datagrid-table-fixed\" [style.width]=\"tableWidthStyle()\">\r\n <colgroup>\r\n @for (i of indents(groupLevelCount()); track i) {\r\n <col style=\"width: 30px\" />\r\n }\r\n @if (masterDetailEnabled()) {\r\n <col style=\"width: 30px\" />\r\n }\r\n @if (showCheckboxes()) {\r\n <col style=\"width: 40px\" />\r\n }\r\n @for (col of visibleColumns(); track $index; let ci = $index) {\r\n <col [style.width]=\"effectiveColWidth(col, ci)\" />\r\n }\r\n @if (rowEditCommandVisible()) {\r\n <col style=\"width: 100px\" />\r\n }\r\n </colgroup>\r\n <thead class=\"dx-datagrid-headers\">\r\n <!-- Bands add header rows: a band cell spans its visible leaves with\r\n colspan, and a leaf above the bottom row spans down with rowspan.\r\n That is dx's own banded markup, and it keeps the LAST header row\r\n all-leaves - which the auto-width, resize and drag code relies on. -->\r\n @for (hrow of headerRows(); track $index; let hi = $index) {\r\n <tr class=\"dx-row dx-header-row\" [class.dx-column-lines]=\"showColumnLines()\">\r\n @if (hi === 0) {\r\n @for (i of indents(groupLevelCount()); track i) {\r\n <td\r\n class=\"dx-command-expand dx-datagrid-group-space dx-cell-focus-disabled\"\r\n [attr.rowspan]=\"headerDepth() > 1 ? headerDepth() : null\"></td>\r\n }\r\n @if (masterDetailEnabled()) {\r\n <td\r\n class=\"dx-command-expand\"\r\n [attr.rowspan]=\"headerDepth() > 1 ? headerDepth() : null\"></td>\r\n }\r\n @if (showCheckboxes()) {\r\n <td\r\n class=\"dx-command-select\"\r\n [attr.rowspan]=\"headerDepth() > 1 ? headerDepth() : null\">\r\n @if (selectionMode() === 'multiple') {\r\n <span\r\n class=\"dx-select-checkbox nxe-grid-checkbox\"\r\n [class.nxe-checked]=\"allSelected()\"\r\n [class.nxe-indeterminate]=\"someSelected() && !allSelected()\"\r\n (click)=\"toggleSelectAll($event)\">\r\n <span class=\"nxe-grid-checkbox-box\"></span>\r\n </span>\r\n }\r\n </td>\r\n }\r\n }\r\n @for (cell of hrow; track $index) {\r\n @if (cell.isBand) {\r\n <td\r\n class=\"nxe-band-cell\"\r\n [class]=\"cell.column.cssClass()\"\r\n [attr.colspan]=\"cell.colspan > 1 ? cell.colspan : null\"\r\n [style.text-align]=\"cell.column.alignment() ?? 'center'\">\r\n <div class=\"dx-datagrid-text-content\">{{ cell.column.caption() }}</div>\r\n </td>\r\n } @else {\r\n <td\r\n [class]=\"cell.column.cssClass()\"\r\n [attr.aria-colindex]=\"leadCellCount() + cell.leafIndex + 1\"\r\n [attr.rowspan]=\"cell.rowspan > 1 ? cell.rowspan : null\"\r\n [style.text-align]=\"align(cell.column)\"\r\n [class.dx-cell-focus-disabled]=\"!cell.column.allowSorting()\"\r\n [class.dx-datagrid-drag-action]=\"headerDraggable(cell.column)\"\r\n (pointerdown)=\"onHeaderPointerDown(cell.column, cell.leafIndex, $event)\"\r\n (click)=\"onHeaderClick(cell.column)\">\r\n <!-- Both indicator classes are load-bearing, not decorative: they are what\r\n gate dx's caption max-widths (`.dx-header-row .dx-sort-indicator\r\n {max-width:calc(100% - 17px)}` and the .dx-header-filter-indicator\r\n family), i.e. the 17px-per-indicator budget that stops a long\r\n caption from colliding with the arrow and the funnel. -->\r\n <div\r\n class=\"dx-datagrid-text-content\"\r\n [class.dx-sort-indicator]=\"!!sortDir(cell.column)\"\r\n [class.dx-header-filter-indicator]=\"headerFilterVisible(cell.column)\">\r\n {{ cell.column.caption() }}\r\n </div>\r\n <!-- dx wraps the arrow and the funnel in one indicator block, and the\r\n group-panel chip below already did. Matching it here means the two\r\n places share a single set of metrics instead of drifting. -->\r\n @if (sortDir(cell.column) || headerFilterVisible(cell.column)) {\r\n <div class=\"dx-column-indicators\" role=\"presentation\">\r\n @if (sortDir(cell.column); as dir) {\r\n <span\r\n class=\"dx-sort\"\r\n [class.dx-sort-up]=\"dir === 'asc'\"\r\n [class.dx-sort-down]=\"dir === 'desc'\"></span>\r\n }\r\n @if (headerFilterVisible(cell.column)) {\r\n <span\r\n class=\"nxe-grid-hf-icon\"\r\n [class.nxe-hf-active]=\"hasHeaderFilter(cell.column)\"\r\n (click)=\"openHeaderFilter(cell.column, $event)\"></span>\r\n }\r\n </div>\r\n }\r\n @if (resizeHandleVisible(cell.column, cell.leafIndex)) {\r\n <span\r\n class=\"nxe-col-resize-handle\"\r\n (pointerdown)=\"startColumnResize(cell.column, cell.leafIndex, $event)\"></span>\r\n }\r\n </td>\r\n }\r\n }\r\n @if (hi === 0 && rowEditCommandVisible()) {\r\n <td\r\n class=\"dx-command-edit\"\r\n [attr.rowspan]=\"headerDepth() > 1 ? headerDepth() : null\"></td>\r\n }\r\n </tr>\r\n }\r\n @if (filterRowVisible()) {\r\n <tr class=\"dx-row dx-datagrid-filter-row\">\r\n @for (i of indents(groupLevelCount()); track i) {\r\n <td class=\"dx-command-expand dx-datagrid-group-space\"></td>\r\n }\r\n @if (masterDetailEnabled()) {\r\n <td class=\"dx-command-expand\"></td>\r\n }\r\n @if (showCheckboxes()) {\r\n <td class=\"dx-command-select\"></td>\r\n }\r\n @for (col of visibleColumns(); track $index) {\r\n <!-- dx filter cell: td.dx-editor-cell > .dx-editor-with-menu >\r\n [.dx-menu op chooser][typed editor] (dx-compat.css contract) -->\r\n <td class=\"dx-editor-cell\" [class]=\"col.cssClass()\">\r\n @if (col.allowFiltering()) {\r\n @switch (frEditorKind(col)) {\r\n @case ('lookup') {\r\n <select\r\n class=\"dx-texteditor-input nxe-grid-filter-input\"\r\n [value]=\"frText(col)\"\r\n (click)=\"$event.stopPropagation()\"\r\n (change)=\"setFilterRowText(col, $any($event.target).value)\">\r\n <option value=\"\">(All)</option>\r\n @for (item of col.lookup()!.dataSource; track $index) {\r\n <option [value]=\"item[col.lookup()!.valueExpr ?? 'lovCode']\">\r\n {{ item[col.lookup()!.displayExpr ?? 'lovName'] }}\r\n </option>\r\n }\r\n </select>\r\n }\r\n @case ('boolean') {\r\n <select\r\n class=\"dx-texteditor-input nxe-grid-filter-input\"\r\n [value]=\"frText(col)\"\r\n (click)=\"$event.stopPropagation()\"\r\n (change)=\"setFilterRowText(col, $any($event.target).value)\">\r\n <option value=\"\">(All)</option>\r\n <option value=\"true\">true</option>\r\n <option value=\"false\">false</option>\r\n </select>\r\n }\r\n @default {\r\n <div class=\"dx-editor-with-menu\">\r\n <div class=\"dx-menu dx-menu-base\">\r\n <ul class=\"dx-menu-horizontal\">\r\n <li>\r\n <div\r\n class=\"dx-menu-item\"\r\n title=\"Filter operation\"\r\n (click)=\"openFilterOpMenu(col, $event)\">\r\n <div class=\"dx-menu-item-content\">\r\n <span class=\"dx-icon\" [class]=\"frOpIconClass(col)\"></span>\r\n </div>\r\n </div>\r\n </li>\r\n </ul>\r\n </div>\r\n <input\r\n class=\"dx-texteditor-input nxe-grid-filter-input\"\r\n [type]=\"frEditorKind(col) === 'date' ? 'date' : 'text'\"\r\n [attr.inputmode]=\"frEditorKind(col) === 'number' ? 'decimal' : null\"\r\n [value]=\"frText(col)\"\r\n (input)=\"setFilterRowText(col, $any($event.target).value)\"\r\n (click)=\"$event.stopPropagation()\" />\r\n @if (frOp(col) === 'between') {\r\n <input\r\n class=\"dx-texteditor-input nxe-grid-filter-input nxe-fr-second\"\r\n [type]=\"frEditorKind(col) === 'date' ? 'date' : 'text'\"\r\n [attr.inputmode]=\"frEditorKind(col) === 'number' ? 'decimal' : null\"\r\n [value]=\"frText2(col)\"\r\n (input)=\"setFilterRowText(col, $any($event.target).value, true)\"\r\n (click)=\"$event.stopPropagation()\" />\r\n }\r\n </div>\r\n }\r\n }\r\n }\r\n </td>\r\n }\r\n @if (rowEditCommandVisible()) {\r\n <td class=\"dx-command-edit\"></td>\r\n }\r\n </tr>\r\n }\r\n </thead>\r\n <tbody>\r\n @for (item of renderRows(); track $index; let ri = $index) {\r\n @if (item.kind === 'group') {\r\n <!-- dx group-row anatomy (bright audit): [indent cells][expand chevron\r\n cell][dx-group-cell colspan][alignByColumn summary cells] -->\r\n <tr\r\n class=\"dx-row dx-group-row\"\r\n [class.dx-column-lines]=\"showColumnLines()\"\r\n [attr.aria-roledescription]=\"item.collapsed ? 'Collapsed row' : 'Expanded row'\">\r\n @for (i of indents(item.level); track i) {\r\n <td class=\"dx-command-expand dx-datagrid-group-space\"></td>\r\n }\r\n <td\r\n class=\"dx-command-expand dx-datagrid-group-space dx-datagrid-expand dx-selection-disabled\"\r\n [attr.aria-label]=\"item.collapsed ? 'Expand' : 'Collapse'\"\r\n (click)=\"toggleGroup(item.groupKey)\">\r\n <div\r\n [class.dx-datagrid-group-opened]=\"!item.collapsed\"\r\n [class.dx-datagrid-group-closed]=\"item.collapsed\"></div>\r\n </td>\r\n <td class=\"dx-group-cell\" [attr.colspan]=\"groupCellColspan(item)\">\r\n @if (templateFor(item.column.groupCellTemplate()); as gtpl) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"gtpl\"\r\n [ngTemplateOutletContext]=\"groupCellContext(item)\" />\r\n } @else {\r\n {{ item.column.caption() }}: {{ item.text }}{{ item.captionSuffix }}\r\n }\r\n </td>\r\n @for (col of groupRowTrailing(item); track col.dataField()) {\r\n <td class=\"dx-datagrid-summary-item\" [style.text-align]=\"align(col)\">\r\n {{ item.alignedSums.get(col.dataField()) || '' }}\r\n </td>\r\n }\r\n @if (groupRowTrailing(item).length && rowEditCommandVisible()) {\r\n <td class=\"dx-command-edit\"></td>\r\n }\r\n </tr>\r\n } @else if (item.kind === 'groupFooter') {\r\n <tr\r\n class=\"dx-row dx-group-footer-row nxe-group-footer-row\"\r\n [class]=\"'nxe-group-footer-level-' + item.level\">\r\n @for (i of indents(groupLevelCount()); track i) {\r\n <td class=\"dx-datagrid-group-space\"></td>\r\n }\r\n @if (masterDetailEnabled()) {\r\n <td class=\"dx-command-expand\"></td>\r\n }\r\n @if (showCheckboxes()) {\r\n <td class=\"dx-command-select\"></td>\r\n }\r\n @for (col of visibleColumns(); track col.dataField()) {\r\n <td class=\"dx-datagrid-summary-item\" [style.text-align]=\"align(col)\">\r\n {{ item.sums.get(col.dataField()) || '' }}\r\n </td>\r\n }\r\n @if (rowEditCommandVisible()) {\r\n <td class=\"dx-command-edit\"></td>\r\n }\r\n </tr>\r\n } @else {\r\n <tr\r\n class=\"dx-row dx-data-row\"\r\n [class]=\"rowClassFor(item.row)\"\r\n [attr.aria-rowindex]=\"item.rowIndex + 1\"\r\n [class.dx-row-lines]=\"showRowLines()\"\r\n [class.dx-column-lines]=\"showColumnLines()\"\r\n [class.dx-row-alt]=\"rowAlternationEnabled() && item.rowIndex % 2 === 1\"\r\n [class.dx-selection]=\"isSelected(item.row)\"\r\n [class.dx-state-hover]=\"isHovered(item.rowIndex)\"\r\n [class.dx-edit-row]=\"isRowEditing(item.row)\"\r\n (mouseenter)=\"onRowEnter(item.rowIndex)\"\r\n (mouseleave)=\"onRowLeave()\"\r\n (click)=\"onDataRowClick(item.row, item.rowIndex)\">\r\n @for (i of indents(groupLevelCount()); track i) {\r\n <td class=\"dx-datagrid-group-space\"></td>\r\n }\r\n @if (masterDetailEnabled()) {\r\n <td class=\"dx-command-expand dx-datagrid-expand\">\r\n <span\r\n class=\"nxe-detail-expander\"\r\n [class.nxe-expanded]=\"isDetailExpanded(item.row)\"\r\n (click)=\"toggleDetail(item.row, $event)\"></span>\r\n </td>\r\n }\r\n @if (showCheckboxes()) {\r\n <td class=\"dx-command-select\">\r\n <span\r\n class=\"dx-select-checkbox nxe-grid-checkbox\"\r\n [class.nxe-checked]=\"isSelected(item.row)\"\r\n (click)=\"onCheckboxClick(item.row, $event)\">\r\n <span class=\"nxe-grid-checkbox-box\"></span>\r\n </span>\r\n </td>\r\n }\r\n @for (col of visibleColumns(); track $index; let ci = $index) {\r\n @if (!isMergeSuppressed(ri, col)) {\r\n <td\r\n [class]=\"col.cssClass()\"\r\n [attr.aria-colindex]=\"leadCellCount() + ci + 1\"\r\n [attr.aria-roledescription]=\"\r\n editableCell(col) && (cellEditingEnabled() || rowEditingEnabled())\r\n ? 'Editable'\r\n : null\r\n \"\r\n [style.text-align]=\"align(col)\"\r\n [style.padding-left.px]=\"treeMode && ci === 0 ? (item.level || 0) * 18 + 4 : null\"\r\n [attr.rowspan]=\"mergeSpan(ri, col)\"\r\n [class.dx-editor-cell]=\"isEditingCell(item.row, col) || (isRowEditing(item.row) && editableCell(col))\"\r\n (click)=\"onCellClick(item.row, col, $event)\"\r\n (focusout)=\"onEditCellFocusOut(item.row, col, $event)\">\r\n @if (treeMode && ci === 0) {\r\n @if (item.hasChildren) {\r\n <span\r\n class=\"nxe-tree-expander\"\r\n [class.nxe-collapsed]=\"!item.expanded\"\r\n (click)=\"toggleTreeNode(item.nodeKey, $event)\"></span>\r\n } @else {\r\n <span class=\"nxe-tree-expander-stub\"></span>\r\n }\r\n }\r\n @if (isEditingCell(item.row, col) || (isRowEditing(item.row) && editableCell(col))) {\r\n @if (templateFor(col.editCellTemplate()); as etpl) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"etpl\"\r\n [ngTemplateOutletContext]=\"editCellContext(item.row, item.rowIndex, ci, col)\" />\r\n } @else if (isEditingCell(item.row, col)) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"cellEditor\"\r\n [ngTemplateOutletContext]=\"{ row: item.row, col: col, mode: 'cell' }\" />\r\n } @else {\r\n <ng-container\r\n [ngTemplateOutlet]=\"cellEditor\"\r\n [ngTemplateOutletContext]=\"{ row: item.row, col: col, mode: 'row' }\" />\r\n }\r\n } @else if (templateFor(col.cellTemplate()); as tpl) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"tpl\"\r\n [ngTemplateOutletContext]=\"cellContext(item.row, item.rowIndex, ci, col)\" />\r\n } @else if (isBooleanCell(col, item.row)) {\r\n <!-- dx parity: boolean cells render a read-only dx-style checkbox, not \"true\"/\"false\" -->\r\n <span\r\n class=\"nxe-grid-checkbox nxe-grid-checkbox-readonly\"\r\n [class.nxe-checked]=\"getRaw(item.row, col.dataField()) === true\">\r\n <span class=\"nxe-grid-checkbox-box\"></span>\r\n </span>\r\n } @else {\r\n {{ cellText(col, item.row) }}\r\n }\r\n </td>\r\n }\r\n }\r\n @if (rowEditCommandVisible()) {\r\n <td class=\"dx-command-edit dx-datagrid-drag-action\">\r\n @if (isRowEditing(item.row)) {\r\n <a class=\"dx-link dx-link-save\" (click)=\"saveRowEdit(item.row, $event)\">Save</a>\r\n <a class=\"dx-link dx-link-cancel\" (click)=\"cancelRowEdit($event)\">Cancel</a>\r\n } @else {\r\n <a class=\"dx-link dx-link-edit\" (click)=\"startRowEdit(item.row, $event)\">Edit</a>\r\n }\r\n </td>\r\n }\r\n </tr>\r\n @if (masterDetailEnabled() && isDetailExpanded(item.row)) {\r\n <tr class=\"dx-row dx-master-detail-row\">\r\n <td [attr.colspan]=\"allColCount()\">\r\n @if (detailTemplate(); as tpl) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"tpl\"\r\n [ngTemplateOutletContext]=\"detailContext(item.row, item.rowIndex)\" />\r\n }\r\n </td>\r\n </tr>\r\n }\r\n }\r\n }\r\n <tr class=\"dx-row dx-freespace-row\">\r\n <td [attr.colspan]=\"allColCount()\"></td>\r\n </tr>\r\n </tbody>\r\n @if (hasTotalFooter()) {\r\n <tfoot class=\"dx-datagrid-total-footer\">\r\n <tr class=\"dx-row\">\r\n @for (i of indents(groupLevelCount()); track i) {\r\n <td class=\"dx-datagrid-group-space\"></td>\r\n }\r\n @if (masterDetailEnabled()) {\r\n <td class=\"dx-command-expand\"></td>\r\n }\r\n @if (showCheckboxes()) {\r\n <td class=\"dx-command-select\"></td>\r\n }\r\n @for (col of visibleColumns(); track col.dataField()) {\r\n <td class=\"dx-datagrid-summary-item\" [style.text-align]=\"align(col)\">\r\n {{ totalSums().get(col.dataField()) || '' }}\r\n </td>\r\n }\r\n @if (rowEditCommandVisible()) {\r\n <td class=\"dx-command-edit\"></td>\r\n }\r\n </tr>\r\n </tfoot>\r\n }\r\n </table>\r\n </div>\r\n\r\n @if (displayRows().length === 0 && !loading()) {\r\n <div class=\"dx-datagrid-nodata\">{{ noDataText() }}</div>\r\n }\r\n @if (loading()) {\r\n <div class=\"dx-datagrid-loadpanel\"><span class=\"nxe-grid-spinner\"></span></div>\r\n }\r\n </div>\r\n\r\n @if (pagerVisible()) {\r\n <div class=\"dx-datagrid-pager dx-pager nxe-grid-pager\">\r\n @if (pagerCmp()?.showPageSizeSelector() && pageSizes().length > 1) {\r\n <select class=\"nxe-pager-size\" (change)=\"onPageSizeSelect($event)\">\r\n @for (s of pageSizes(); track s) {\r\n <option [value]=\"s\" [selected]=\"s === effectivePageSize()\">{{ s }}</option>\r\n }\r\n </select>\r\n }\r\n <div class=\"nxe-pager-pages\">\r\n @if (pagerCmp()?.showNavigationButtons()) {\r\n <button type=\"button\" class=\"nxe-pager-nav\" [disabled]=\"pageIndex() === 0\" (click)=\"prevPage()\">\u2039</button>\r\n }\r\n @for (p of pageNumbers(); track p) {\r\n <button\r\n type=\"button\"\r\n class=\"nxe-pager-page\"\r\n [class.nxe-active]=\"p === pageIndex()\"\r\n (click)=\"goToPage(p)\">\r\n {{ p + 1 }}\r\n </button>\r\n }\r\n @if (pagerCmp()?.showNavigationButtons()) {\r\n <button type=\"button\" class=\"nxe-pager-nav\" [disabled]=\"pageIndex() >= pageCount() - 1\" (click)=\"nextPage()\">\u203A</button>\r\n }\r\n </div>\r\n @if (pagerCmp()?.showInfo()) {\r\n <span class=\"nxe-pager-info\">{{ pageInfo() }}</span>\r\n }\r\n </div>\r\n }\r\n </div>\r\n</div>\r\n\r\n<!-- Inline cell/row editor (dxo-editing, orbit addition). `mode: 'cell'` commits on blur/Enter;\r\n `mode: 'row'` buffers on input until the command column's Save. -->\r\n<ng-template #cellEditor let-row=\"row\" let-col=\"col\" let-mode=\"mode\">\r\n @if (col.lookup(); as lookup) {\r\n <select\r\n class=\"dx-texteditor-input nxe-grid-cell-editor\"\r\n [value]=\"editorInputValue(row, col)\"\r\n (click)=\"$event.stopPropagation()\"\r\n (change)=\"\r\n mode === 'cell'\r\n ? commitCellEdit(row, col, $any($event.target).value)\r\n : bufferRowEdit(col, $any($event.target).value)\r\n \">\r\n <option value=\"\"></option>\r\n @for (item of lookup.dataSource; track $index) {\r\n <option [value]=\"item[lookup.valueExpr ?? 'lovCode']\">\r\n {{ item[lookup.displayExpr ?? 'lovName'] }}\r\n </option>\r\n }\r\n </select>\r\n } @else if (col.dataType() === 'boolean') {\r\n <input\r\n type=\"checkbox\"\r\n class=\"nxe-grid-cell-editor-check\"\r\n [checked]=\"!!editorInputValue(row, col) && editorInputValue(row, col) !== 'false'\"\r\n (click)=\"$event.stopPropagation()\"\r\n (change)=\"\r\n mode === 'cell'\r\n ? commitCellEdit(row, col, $any($event.target).checked)\r\n : bufferRowEdit(col, $any($event.target).checked)\r\n \" />\r\n } @else {\r\n <input\r\n type=\"text\"\r\n class=\"dx-texteditor-input nxe-grid-cell-editor\"\r\n [value]=\"editorInputValue(row, col)\"\r\n (click)=\"$event.stopPropagation()\"\r\n (keydown.enter)=\"mode === 'cell' && commitCellEdit(row, col, $any($event.target).value)\"\r\n (keydown.escape)=\"mode === 'cell' && cancelCellEdit()\"\r\n (blur)=\"mode === 'cell' && commitCellEdit(row, col, $any($event.target).value)\"\r\n (input)=\"mode === 'row' && bufferRowEdit(col, $any($event.target).value)\" />\r\n }\r\n</ng-template>\r\n\r\n<!-- The header-filter popup (#hfPanel) moved to nxe-header-filter-panel.component.html \u2014\r\n openHeaderFilter attaches it through openNxeHeaderFilterOverlay (pivot parity Phase 3a). -->\r\n\r\n<!-- Filter-row operation-chooser dropdown (dx filter menu) -->\r\n<ng-template #frOpMenu>\r\n <div class=\"nxe-fr-op-menu dx-datagrid\">\r\n @for (item of frMenuOps(); track item.op) {\r\n <div\r\n class=\"dx-menu-item\"\r\n [class.dx-menu-item-selected]=\"frMenuColumn() && frOp(frMenuColumn()!) === item.op\"\r\n (click)=\"selectFilterOp(item.op)\">\r\n <span class=\"dx-icon dx-icon-filter-operation-{{ opIconSlug(item.op) }}\"></span>\r\n <span class=\"dx-menu-item-text\">{{ item.label }}</span>\r\n </div>\r\n }\r\n <div class=\"dx-menu-item nxe-fr-reset\" (click)=\"resetFilterRow()\">\r\n <span class=\"dx-icon\"></span>\r\n <span class=\"dx-menu-item-text\">Reset</span>\r\n </div>\r\n </div>\r\n</ng-template>\r\n\r\n", styles: ["@charset \"UTF-8\";.nxe-data-grid{display:block}.nxe-data-grid .dx-datagrid{height:100%;position:relative}.nxe-data-grid .dx-gridbase-container{display:flex;flex-direction:column;height:100%}.nxe-data-grid .dx-datagrid-rowsview{position:relative;overflow:auto;flex:1 1 auto;min-height:0}.nxe-data-grid .dx-datagrid-content{display:block;min-height:100%}.nxe-data-grid table.dx-datagrid-table{display:table;width:100%;table-layout:fixed;border-collapse:collapse;margin:0}.nxe-data-grid .dx-datagrid-content .dx-datagrid-table{max-width:none}.nxe-data-grid .dx-datagrid-headers{display:table-header-group;position:sticky;top:0;z-index:1;background:#fff;border-bottom:0}.nxe-data-grid tbody{display:table-row-group}.nxe-data-grid .dx-row{display:table-row}.nxe-data-grid .dx-datagrid-headers .dx-header-row>td{border-bottom:1px solid #e2e8f0}.nxe-data-grid .dx-datagrid-nodata{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#333;font-size:17px;opacity:.7;pointer-events:none}.nxe-data-grid .dx-datagrid-loadpanel{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:#ffffff80;z-index:2}.nxe-data-grid .nxe-grid-spinner{width:32px;height:32px;border:3px solid rgba(48,55,76,.2);border-top-color:#30374c;border-radius:50%;animation:nxe-grid-spin .8s linear infinite}.nxe-data-grid .dx-column-indicators{display:inline-block;vertical-align:top;white-space:nowrap}.nxe-data-grid .dx-sort{display:inline-block;position:relative;width:14px;height:14px;vertical-align:middle}.nxe-data-grid .dx-sort:after{content:\"\";position:absolute;left:50%;top:50%;width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent}.nxe-data-grid .dx-sort.dx-sort-up:after{border-bottom:5px solid #757575;transform:translate(-50%,-50%)}.nxe-data-grid .dx-sort.dx-sort-down:after{border-top:5px solid #757575;transform:translate(-50%,-50%)}.nxe-data-grid .dx-header-row>td{cursor:default;-webkit-user-select:none;user-select:none;height:27px}.nxe-data-grid .dx-header-row>td{position:relative}.nxe-data-grid .nxe-grid-hf-icon{display:inline-block;position:relative;width:14px;height:14px;cursor:pointer;vertical-align:middle;opacity:.5}.nxe-data-grid .nxe-grid-hf-icon:before{content:\"\";position:absolute;inset:0;background-color:#757575;clip-path:polygon(0 0,100% 0,58% 50%,58% 100%,42% 85%,42% 50%)}.nxe-data-grid .nxe-grid-hf-icon:after{content:\"\";position:absolute;inset:0;margin:-7px}.nxe-data-grid .nxe-grid-hf-icon:hover{opacity:.75}.nxe-data-grid .nxe-grid-hf-icon.nxe-hf-active{opacity:1}.nxe-data-grid .nxe-grid-pager{display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:8px 12px;border-top:1px solid #ddd}.nxe-data-grid .nxe-pager-size{padding:2px 6px;border:1px solid #ddd;border-radius:3px;font:inherit}.nxe-data-grid .nxe-pager-pages{display:flex;align-items:center;gap:4px;flex-wrap:wrap}.nxe-data-grid .nxe-pager-page,.nxe-data-grid .nxe-pager-nav{min-width:24px;padding:3px 7px 4px;border:1px solid transparent;border-radius:3px;background:transparent;color:#333;cursor:pointer;font:inherit;line-height:1}.nxe-data-grid .nxe-pager-page:hover:not(:disabled),.nxe-data-grid .nxe-pager-nav:hover:not(:disabled){background:#f4f4f4}.nxe-data-grid .nxe-pager-page:disabled,.nxe-data-grid .nxe-pager-nav:disabled{opacity:.4;cursor:default}.nxe-data-grid .nxe-pager-page.nxe-active,.nxe-data-grid .nxe-pager-nav.nxe-active{background:#d4d4d4;border-color:transparent}.nxe-data-grid .nxe-pager-info{color:#757575;margin-left:auto}.nxe-data-grid .dx-datagrid-header-panel{padding:.5rem .75rem}.nxe-data-grid .dx-datagrid-group-panel{align-items:center;min-height:36px}.nxe-data-grid .nxe-tree-expander,.nxe-data-grid .nxe-tree-expander-stub{display:inline-block;width:16px;height:16px;flex:0 0 16px;vertical-align:middle;margin-right:2px}.nxe-data-grid .nxe-tree-expander{position:relative;cursor:pointer}.nxe-data-grid .nxe-tree-expander:before{content:\"\";position:absolute;left:4px;top:4px;width:6px;height:6px;border-right:1.5px solid #555;border-bottom:1.5px solid #555;transform:rotate(45deg)}.nxe-data-grid .nxe-tree-expander.nxe-collapsed:before{transform:rotate(-45deg)}.nxe-data-grid .dx-group-row>td{padding:4px 8px;background-color:#f7f7f7}.nxe-data-grid .dx-group-row>td.dx-group-cell{font-weight:700}.nxe-data-grid .dx-group-row>td.dx-datagrid-expand{cursor:pointer}.nxe-data-grid .dx-group-row>td.dx-datagrid-summary-item{font-weight:400;color:#333}.nxe-data-grid .dx-datagrid-group-opened,.nxe-data-grid .dx-datagrid-group-closed{display:inline-block;position:relative;width:14px;height:14px;cursor:pointer}.nxe-data-grid .dx-datagrid-group-opened:before,.nxe-data-grid .dx-datagrid-group-closed:before{content:\"\"!important;position:absolute;left:3px;right:auto;top:2px;width:6px;height:6px;border-right:1.5px solid #555;border-bottom:1.5px solid #555;transform:rotate(45deg)}.nxe-data-grid .dx-datagrid-group-closed:before{transform:rotate(-45deg)}.nxe-data-grid .dx-datagrid-total-footer>tr>td,.nxe-data-grid tfoot.dx-datagrid-total-footer td{border-top:1px solid #ddd;padding:6px 4px;font-weight:700;color:#959595}.nxe-data-grid .dx-datagrid-filter-row>td{padding:2px 4px}.nxe-data-grid .nxe-grid-filter-input{width:100%;box-sizing:border-box;padding:4px 6px;border:1px solid #ddd;border-radius:2px;font:inherit;outline:none}.nxe-data-grid .nxe-grid-filter-input:focus{border-color:#30374c}.nxe-data-grid .dx-command-select{width:40px;min-width:40px;max-width:40px;text-align:center;vertical-align:middle}.nxe-data-grid .nxe-grid-checkbox{display:inline-block;cursor:pointer;line-height:0}.nxe-data-grid .nxe-grid-checkbox .nxe-grid-checkbox-box{display:inline-block;position:relative;width:16px;height:16px;box-sizing:border-box;border:1px solid #ddd;border-radius:2px;background:#fff;vertical-align:middle}.nxe-data-grid .nxe-grid-checkbox.nxe-checked .nxe-grid-checkbox-box{border-color:#ddd;background:#fff}.nxe-data-grid .nxe-grid-checkbox.nxe-checked .nxe-grid-checkbox-box:after{content:\"\";position:absolute;left:4px;top:1px;width:5px;height:9px;border:solid #337ab7;border-width:0 2px 2px 0;transform:rotate(45deg)}.nxe-data-grid .nxe-grid-checkbox.nxe-indeterminate .nxe-grid-checkbox-box{border-color:#ddd}.nxe-data-grid .nxe-grid-checkbox.nxe-indeterminate .nxe-grid-checkbox-box:after{content:\"\";position:absolute;left:3px;top:3px;width:8px;height:8px;background:#337ab7}.nxe-data-grid .nxe-grid-checkbox.nxe-grid-checkbox-readonly{display:block;text-align:center;cursor:default;height:18px;line-height:18px}@keyframes nxe-grid-spin{to{transform:rotate(360deg)}}.nxe-data-grid .dx-command-expand{width:30px;text-align:center;cursor:pointer}.nxe-detail-expander{display:inline-block;width:0;height:0;border-top:5px solid transparent;border-bottom:5px solid transparent;border-left:6px solid #999;cursor:pointer;transition:transform .15s ease}.nxe-detail-expander.nxe-expanded{transform:rotate(90deg)}.nxe-data-grid .dx-master-detail-row>td{padding:8px 12px;background:#fafafa}.nxe-grid-cell-editor{width:100%;box-sizing:border-box;border:1px solid #337ab7;padding:2px 4px;font:inherit;background:#fff;outline:none}.nxe-data-grid .dx-command-edit{text-align:center;white-space:nowrap}.nxe-data-grid .dx-command-edit .dx-link{color:#337ab7;cursor:pointer;margin:0 4px;text-decoration:none}.nxe-data-grid .dx-command-edit .dx-link:hover{text-decoration:underline}.nxe-data-grid .dx-header-row td.dx-datagrid-drag-action,.nxe-data-grid .dx-group-panel-item{touch-action:none}.nxe-data-grid .nxe-drag-source{opacity:.5}.nxe-data-grid .dx-datagrid-filter-row .dx-menu ul{list-style:none;margin:0;padding:0}.nxe-data-grid .dx-datagrid-filter-row .dx-editor-with-menu{display:flex}.nxe-data-grid .dx-datagrid-filter-row .dx-editor-with-menu .nxe-grid-filter-input{flex:1 1 0;min-width:0}.nxe-data-grid .dx-datagrid-filter-row .dx-editor-with-menu .nxe-fr-second{padding-left:4px!important;border-left:1px solid #ddd}.nxe-data-grid .dx-datagrid-filter-row .dx-icon[class*=dx-icon-filter-operation-],.nxe-fr-op-menu .dx-icon[class*=dx-icon-filter-operation-]{font:600 12px/18px inherit!important;font-family:inherit!important;display:inline-block;width:18px;height:18px;text-align:center;color:#757575}.nxe-data-grid .dx-icon-filter-operation-contains:before,.nxe-fr-op-menu .dx-icon-filter-operation-contains:before{content:\"*\"!important}.nxe-data-grid .dx-icon-filter-operation-not-contains:before,.nxe-fr-op-menu .dx-icon-filter-operation-not-contains:before{content:\"!*\"!important}.nxe-data-grid .dx-icon-filter-operation-starts-with:before,.nxe-fr-op-menu .dx-icon-filter-operation-starts-with:before{content:\"a*\"!important}.nxe-data-grid .dx-icon-filter-operation-ends-with:before,.nxe-fr-op-menu .dx-icon-filter-operation-ends-with:before{content:\"*a\"!important}.nxe-data-grid .dx-icon-filter-operation-equals:before,.nxe-fr-op-menu .dx-icon-filter-operation-equals:before{content:\"=\"!important}.nxe-data-grid .dx-icon-filter-operation-not-equals:before,.nxe-fr-op-menu .dx-icon-filter-operation-not-equals:before{content:\"\\2260\"!important}.nxe-data-grid .dx-icon-filter-operation-less:before,.nxe-fr-op-menu .dx-icon-filter-operation-less:before{content:\"<\"!important}.nxe-data-grid .dx-icon-filter-operation-greater:before,.nxe-fr-op-menu .dx-icon-filter-operation-greater:before{content:\">\"!important}.nxe-data-grid .dx-icon-filter-operation-less-equal:before,.nxe-fr-op-menu .dx-icon-filter-operation-less-equal:before{content:\"\\2264\"!important}.nxe-data-grid .dx-icon-filter-operation-greater-equal:before,.nxe-fr-op-menu .dx-icon-filter-operation-greater-equal:before{content:\"\\2265\"!important}.nxe-data-grid .dx-icon-filter-operation-between:before,.nxe-fr-op-menu .dx-icon-filter-operation-between:before{content:\"\\2194\"!important}.nxe-fr-op-menu{background:#fff;border:1px solid #ddd;box-shadow:0 2px 6px #00000026;padding:4px 0;min-width:180px}.nxe-fr-op-menu .dx-menu-item{display:flex;align-items:center;gap:8px;padding:6px 24px 6px 8px;cursor:pointer;color:#333}.nxe-fr-op-menu .dx-menu-item:hover{background:#f0f0f0}.nxe-fr-op-menu .dx-menu-item.dx-menu-item-selected{background:#e6e6e6}.nxe-fr-op-menu .dx-menu-item .dx-icon{width:18px;text-align:center}.nxe-data-grid .nxe-col-resize-handle{position:absolute;right:-4px;top:0;bottom:0;width:8px;cursor:col-resize;z-index:2;touch-action:none}body.nxe-grid-resizing{cursor:col-resize;user-select:none;-webkit-user-select:none}.nxe-col-drop-indicator{position:fixed;width:2px;background:#337ab7;z-index:10000;pointer-events:none}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
6141
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeDataGridComponent, isStandalone: true, selector: "nxe-data-grid, nxe-tree-list", inputs: { dataSource: { classPropertyName: "dataSource", publicName: "dataSource", isSignal: true, isRequired: false, transformFunction: null }, keyExpr: { classPropertyName: "keyExpr", publicName: "keyExpr", isSignal: true, isRequired: false, transformFunction: null }, hoverStateEnabledInput: { classPropertyName: "hoverStateEnabledInput", publicName: "hoverStateEnabled", isSignal: true, isRequired: false, transformFunction: null }, showColumnLinesInput: { classPropertyName: "showColumnLinesInput", publicName: "showColumnLines", isSignal: true, isRequired: false, transformFunction: null }, showRowLinesInput: { classPropertyName: "showRowLinesInput", publicName: "showRowLines", isSignal: true, isRequired: false, transformFunction: null }, showBordersInput: { classPropertyName: "showBordersInput", publicName: "showBorders", isSignal: true, isRequired: false, transformFunction: null }, wordWrapEnabledInput: { classPropertyName: "wordWrapEnabledInput", publicName: "wordWrapEnabled", isSignal: true, isRequired: false, transformFunction: null }, columnAutoWidthInput: { classPropertyName: "columnAutoWidthInput", publicName: "columnAutoWidth", isSignal: true, isRequired: false, transformFunction: null }, rowAlternationEnabledInput: { classPropertyName: "rowAlternationEnabledInput", publicName: "rowAlternationEnabled", isSignal: true, isRequired: false, transformFunction: null }, resizeFillsGridInput: { classPropertyName: "resizeFillsGridInput", publicName: "resizeFillsGrid", isSignal: true, isRequired: false, transformFunction: null }, remoteOperations: { classPropertyName: "remoteOperations", publicName: "remoteOperations", isSignal: true, isRequired: false, transformFunction: null }, pageIndexModel: { classPropertyName: "pageIndexModel", publicName: "pageIndex", isSignal: true, isRequired: false, transformFunction: null }, pageSizeModel: { classPropertyName: "pageSizeModel", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, sortModel: { classPropertyName: "sortModel", publicName: "sort", isSignal: true, isRequired: false, transformFunction: null }, filter: { classPropertyName: "filter", publicName: "filter", isSignal: true, isRequired: false, transformFunction: null }, syncLookupFilterValues: { classPropertyName: "syncLookupFilterValues", publicName: "syncLookupFilterValues", isSignal: true, isRequired: false, transformFunction: null }, columnMinWidth: { classPropertyName: "columnMinWidth", publicName: "columnMinWidth", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, noDataTextInput: { classPropertyName: "noDataTextInput", publicName: "noDataText", isSignal: true, isRequired: false, transformFunction: null }, loadPanel: { classPropertyName: "loadPanel", publicName: "loadPanel", isSignal: true, isRequired: false, transformFunction: null }, paging: { classPropertyName: "paging", publicName: "paging", isSignal: true, isRequired: false, transformFunction: null }, filterRow: { classPropertyName: "filterRow", publicName: "filterRow", isSignal: true, isRequired: false, transformFunction: null }, headerFilterOptions: { classPropertyName: "headerFilterOptions", publicName: "headerFilter", isSignal: true, isRequired: false, transformFunction: null }, groupingOptions: { classPropertyName: "groupingOptions", publicName: "grouping", isSignal: true, isRequired: false, transformFunction: null }, groupPanelOptions: { classPropertyName: "groupPanelOptions", publicName: "groupPanel", isSignal: true, isRequired: false, transformFunction: null }, scrolling: { classPropertyName: "scrolling", publicName: "scrolling", isSignal: true, isRequired: false, transformFunction: null }, sorting: { classPropertyName: "sorting", publicName: "sorting", isSignal: true, isRequired: false, transformFunction: null }, allowColumnResizing: { classPropertyName: "allowColumnResizing", publicName: "allowColumnResizing", isSignal: true, isRequired: false, transformFunction: null }, columnResizingMode: { classPropertyName: "columnResizingMode", publicName: "columnResizingMode", isSignal: true, isRequired: false, transformFunction: null }, allowColumnReordering: { classPropertyName: "allowColumnReordering", publicName: "allowColumnReordering", isSignal: true, isRequired: false, transformFunction: null }, repaintChangesOnly: { classPropertyName: "repaintChangesOnly", publicName: "repaintChangesOnly", isSignal: true, isRequired: false, transformFunction: null }, showColumnHeaders: { classPropertyName: "showColumnHeaders", publicName: "showColumnHeaders", isSignal: true, isRequired: false, transformFunction: null }, parentIdExpr: { classPropertyName: "parentIdExpr", publicName: "parentIdExpr", isSignal: true, isRequired: false, transformFunction: null }, dataStructure: { classPropertyName: "dataStructure", publicName: "dataStructure", isSignal: true, isRequired: false, transformFunction: null }, rootValue: { classPropertyName: "rootValue", publicName: "rootValue", isSignal: true, isRequired: false, transformFunction: null }, autoExpandAll: { classPropertyName: "autoExpandAll", publicName: "autoExpandAll", isSignal: true, isRequired: false, transformFunction: null }, expandedRowKeys: { classPropertyName: "expandedRowKeys", publicName: "expandedRowKeys", isSignal: true, isRequired: false, transformFunction: null }, selectedRowKeys: { classPropertyName: "selectedRowKeys", publicName: "selectedRowKeys", isSignal: true, isRequired: false, transformFunction: null }, mergeColumns: { classPropertyName: "mergeColumns", publicName: "mergeColumns", isSignal: true, isRequired: false, transformFunction: null }, rowClass: { classPropertyName: "rowClass", publicName: "rowClass", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pageIndexModel: "pageIndexChange", pageSizeModel: "pageSizeChange", sortModel: "sortChange", optionsChange: "optionsChange", expandedRowKeys: "expandedRowKeysChange", selectedRowKeys: "selectedRowKeysChange", onSelectionChanged: "onSelectionChanged", onRowClick: "onRowClick", onContentReady: "onContentReady", onOptionChanged: "onOptionChanged", onCellPrepared: "onCellPrepared", onRowPrepared: "onRowPrepared", onRowExpanding: "onRowExpanding", onRowExpanded: "onRowExpanded" }, host: { classAttribute: "nxe-widget nxe-data-grid" }, queries: [{ propertyName: "declaredColumns", predicate: NxeColumnComponent, isSignal: true }, { propertyName: "templates", predicate: NxeTemplateDirective, isSignal: true }, { propertyName: "selection", first: true, predicate: NxeSelectionComponent, descendants: true, isSignal: true }, { propertyName: "headerFilter", first: true, predicate: NxeHeaderFilterComponent, descendants: true, isSignal: true }, { propertyName: "grouping", first: true, predicate: NxeGroupingComponent, descendants: true, isSignal: true }, { propertyName: "groupPanel", first: true, predicate: NxeGroupPanelComponent, descendants: true, isSignal: true }, { propertyName: "summaryCmp", first: true, predicate: NxeSummaryComponent, descendants: true, isSignal: true }, { propertyName: "pagingCmp", first: true, predicate: NxePagingComponent, descendants: true, isSignal: true }, { propertyName: "pagerCmp", first: true, predicate: NxePagerComponent, descendants: true, isSignal: true }, { propertyName: "editingCmp", first: true, predicate: NxeEditingComponent, descendants: true, isSignal: true }, { propertyName: "filterRowCmp", first: true, predicate: NxeFilterRowComponent, descendants: true, isSignal: true }, { propertyName: "masterDetail", first: true, predicate: NxeMasterDetailComponent, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "frMenuPanel", first: true, predicate: ["frOpMenu"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\r\n class=\"nxe-widget nxe-datagrid\"\r\n [class.nxe-datagrid-borders]=\"showBorders()\"\r\n [style.height]=\"heightStyle()\">\r\n <div class=\"nxe-gridbase-container\">\r\n @if (groupPanelVisible()) {\r\n <div class=\"nxe-datagrid-header-panel\">\r\n <div class=\"nxe-datagrid-group-panel\">\r\n @if (groupColumns().length === 0) {\r\n <span class=\"nxe-group-panel-message\">{{ t('grid.groupPanel') }}</span>\r\n } @else {\r\n @for (col of groupColumns(); track col.dataField(); let li = $index) {\r\n <!-- dx chip DOM: caption + sort indicator + header-filter button (bright audit) -->\r\n <div\r\n class=\"nxe-group-panel-item nxe-datagrid-action nxe-datagrid-drag-action\"\r\n tabindex=\"0\"\r\n (pointerdown)=\"onChipPointerDown(col, li, $event)\"\r\n (click)=\"onChipClick(col)\">{{ col.caption() }}<div class=\"nxe-column-indicators\" role=\"presentation\"><span\r\n class=\"nxe-sort\"\r\n [class.nxe-sort-up]=\"groupDir(col) === 'asc'\"\r\n [class.nxe-sort-down]=\"groupDir(col) === 'desc'\"></span>@if (headerFilterVisible(col)) {<span\r\n class=\"nxe-grid-hf-icon\"\r\n [class.nxe-hf-active]=\"hasHeaderFilter(col)\"\r\n (pointerdown)=\"$event.stopPropagation()\"\r\n (click)=\"openHeaderFilter(col, $event)\"></span>}</div></div>\r\n }\r\n }\r\n </div>\r\n </div>\r\n }\r\n <!-- dx drops the last row's bottom line in a bordered grid when no free-space row follows\r\n it, so the rows view's own border is the only line there (m_rows_view.js:931-937). -->\r\n <div\r\n class=\"nxe-datagrid-rowsview nxe-datagrid-after-headers\"\r\n [class.nxe-datagrid-nowrap]=\"!wordWrapEnabled()\"\r\n [class.nxe-empty]=\"isEmpty()\"\r\n [class.nxe-remote-busy]=\"remoteBusy()\"\r\n [class.nxe-last-row-border]=\"showBorders() && !freeSpaceVisible()\">\r\n <div class=\"nxe-datagrid-content\">\r\n <!-- role=grid (treegrid when hierarchical) rather than the implicit table role:\r\n this is an interactive widget with a single tab stop and arrow-key movement,\r\n not a static table.\r\n\r\n aria-rowcount counts the PAGE, because aria-rowindex is page-relative and the\r\n two have to agree. Making the index absolute would be more literally correct\r\n for a paged grid, but consuming code addresses cells through it\r\n (`tr[aria-rowindex=N] > td[aria-colindex=M]`), so the index is a contract. -->\r\n <table\r\n class=\"nxe-datagrid-table nxe-datagrid-table-fixed\"\r\n [attr.role]=\"treeMode ? 'treegrid' : 'grid'\"\r\n [attr.aria-rowcount]=\"navigableRows().length\"\r\n [attr.aria-busy]=\"remoteBusy() ? 'true' : null\"\r\n [attr.aria-colcount]=\"visibleColumns().length + leadCellCount() + (rowEditCommandVisible() ? 1 : 0)\"\r\n [style.width]=\"tableWidthStyle()\"\r\n (keydown)=\"onGridKeydown($event)\">\r\n <colgroup>\r\n @for (i of indents(groupLevelCount()); track i) {\r\n <col style=\"width: 30px\" />\r\n }\r\n @if (masterDetailEnabled()) {\r\n <col style=\"width: 30px\" />\r\n }\r\n @if (selectColumnVisible()) {\r\n <col style=\"width: 40px\" />\r\n }\r\n @for (col of visibleColumns(); track $index; let ci = $index) {\r\n <col [style.width]=\"effectiveColWidth(col, ci)\" />\r\n }\r\n @if (rowEditCommandVisible()) {\r\n <col style=\"width: 100px\" />\r\n }\r\n </colgroup>\r\n <thead class=\"nxe-datagrid-headers\">\r\n <!-- Bands add header rows: a band cell spans its visible leaves with\r\n colspan, and a leaf above the bottom row spans down with rowspan.\r\n That is dx's own banded markup, and it keeps the LAST header row\r\n all-leaves - which the auto-width, resize and drag code relies on. -->\r\n @for (hrow of headerRows(); track $index; let hi = $index) {\r\n <tr\r\n class=\"nxe-row nxe-header-row\"\r\n role=\"row\"\r\n [class.nxe-column-lines]=\"showColumnLines()\">\r\n @if (hi === 0) {\r\n <!-- dx numbers the header's command cells like the body's\r\n (m_columns_view.js:681-693); they are not role=columnheader\r\n (m_column_headers.js:193), which keeps them out of the leaf\r\n header list (nxeHeaderLeafCells). -->\r\n @for (i of indents(groupLevelCount()); track i) {\r\n <td\r\n class=\"nxe-command-expand nxe-datagrid-group-space nxe-cell-focus-disabled\"\r\n [attr.aria-colindex]=\"i + 1\"\r\n [attr.rowspan]=\"headerDepth() > 1 ? headerDepth() : null\"></td>\r\n }\r\n @if (masterDetailEnabled()) {\r\n <!-- dx gives every cell of an 'expand' command column the\r\n group-space class (m_columns_view.js:290-293), which drops\r\n the column line between it and the next cell. -->\r\n <td\r\n class=\"nxe-command-expand nxe-datagrid-group-space\"\r\n [attr.aria-colindex]=\"expandColIndex()\"\r\n [attr.rowspan]=\"headerDepth() > 1 ? headerDepth() : null\"></td>\r\n }\r\n @if (selectColumnVisible()) {\r\n <td\r\n class=\"nxe-command-select\"\r\n [attr.aria-colindex]=\"selectColIndex()\"\r\n [attr.rowspan]=\"headerDepth() > 1 ? headerDepth() : null\">\r\n @if (selectionMode() === 'multiple') {\r\n <span\r\n class=\"nxe-select-checkbox nxe-grid-checkbox nxe-checkbox nxe-datagrid-checkbox-size\"\r\n role=\"checkbox\"\r\n [attr.aria-checked]=\"allSelected() ? 'true' : someSelected() ? 'mixed' : 'false'\"\r\n [class.nxe-checked]=\"allSelected()\"\r\n [class.nxe-indeterminate]=\"someSelected() && !allSelected()\"\r\n (click)=\"toggleSelectAll($event)\">\r\n <span class=\"nxe-grid-checkbox-box\"></span>\r\n </span>\r\n }\r\n </td>\r\n }\r\n }\r\n @for (cell of hrow; track $index) {\r\n @if (cell.isBand) {\r\n <!-- dx gives a band the alignment of any column: left unless set\r\n (a band has no dataType to right-align or centre it by). -->\r\n <td\r\n class=\"nxe-band-cell\"\r\n role=\"columnheader\"\r\n [class]=\"cell.column.cssClass()\"\r\n [attr.colspan]=\"cell.colspan > 1 ? cell.colspan : null\"\r\n [style.text-align]=\"align(cell.column)\">\r\n <div class=\"nxe-datagrid-text-content\">{{ cell.column.caption() }}</div>\r\n </td>\r\n } @else {\r\n @let hDir = sortDir(cell.column);\r\n @let hHf = headerFilterVisible(cell.column);\r\n @let hSortSlot = sortSlot(cell.column);\r\n @let hInd = hSortSlot || hHf;\r\n @let hAlign = align(cell.column);\r\n <!-- dx places the indicator block before the caption when column lines are\r\n on or the column is right-aligned (m_column_headers.js createCellContent,\r\n m_column_state_mixin.js _applyColumnState), floated to the cell edge\r\n only with column lines: right, or left for a right-aligned column. -->\r\n @let hLead = hInd && (showColumnLines() || hAlign === 'right');\r\n @let hFloat = showColumnLines() ? (hAlign === 'right' ? 'left' : 'right') : null;\r\n <td\r\n [class]=\"cell.column.cssClass()\"\r\n role=\"columnheader\"\r\n [attr.aria-colindex]=\"leadCellCount() + cell.leafIndex + 1\"\r\n [attr.aria-sort]=\"ariaSort(cell.column)\"\r\n [attr.tabindex]=\"isCellTabStop(-1, cell.leafIndex) ? 0 : -1\"\r\n (focus)=\"focusCell(-1, cell.leafIndex)\"\r\n [attr.rowspan]=\"cell.rowspan > 1 ? cell.rowspan : null\"\r\n [style.text-align]=\"hAlign\"\r\n [class.nxe-cell-focus-disabled]=\"!cell.column.allowSorting()\"\r\n [class.nxe-datagrid-action]=\"sortAllowed(cell.column)\"\r\n [class.nxe-datagrid-drag-action]=\"headerDraggable(cell.column)\"\r\n [class.nxe-tree-select-all]=\"treeSelectAll() && cell.leafIndex === 0\"\r\n (pointerdown)=\"onHeaderPointerDown(cell.column, cell.leafIndex, $event)\"\r\n (click)=\"onHeaderClick(cell.column)\">\r\n @if (treeSelectAll() && cell.leafIndex === 0) {\r\n <!-- dx's tree list puts select-all in the first column's header. -->\r\n <span\r\n class=\"nxe-select-checkbox nxe-grid-checkbox nxe-checkbox nxe-datagrid-checkbox-size\"\r\n role=\"checkbox\"\r\n [attr.aria-label]=\"t('common.selectAll')\"\r\n [attr.aria-checked]=\"allSelected() ? 'true' : someSelected() ? 'mixed' : 'false'\"\r\n [class.nxe-checked]=\"allSelected()\"\r\n [class.nxe-indeterminate]=\"someSelected() && !allSelected()\"\r\n (pointerdown)=\"$event.stopPropagation()\"\r\n (click)=\"toggleSelectAll($event)\">\r\n <span class=\"nxe-grid-checkbox-box\"></span>\r\n </span>\r\n }\r\n @if (hLead) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"headerIndicators\"\r\n [ngTemplateOutletContext]=\"{ $implicit: cell.column, dir: hDir, sortSlot: hSortSlot, hf: hHf, hfFirst: hAlign === 'right', float: hFloat }\" />\r\n }\r\n <!-- dx _alignCaptionByCenter: a centred caption gets an unfloated,\r\n invisible copy of the indicator block on its other side, so the\r\n caption centres on the cell rather than caption + arrow centring\r\n as a group. Spacers only: no sort or funnel classes (specs and\r\n apps pick funnels by index) and no handlers - a click there falls\r\n through to the cell and sorts, as dx's hidden clone lets it. -->\r\n @if (hInd && hAlign === 'center') {\r\n <div\r\n class=\"nxe-column-indicators nxe-visibility-hidden\"\r\n role=\"presentation\"\r\n aria-hidden=\"true\">\r\n @if (hSortSlot) {\r\n <span\r\n class=\"nxe-column-indicator-spacer\"\r\n [class.nxe-column-indicator-spacer-none]=\"!hDir\"></span>\r\n }\r\n @if (hHf) {\r\n <span class=\"nxe-column-indicator-spacer\"></span>\r\n }\r\n </div>\r\n }\r\n <!-- The indicator classes are load-bearing, not decorative: they gate dx's\r\n caption budget (`.dx-header-row .dx-sort-indicator{max-width:calc(100%\r\n - 17px)}` and family) and, with the alignment classes, the 3px gap to\r\n the block. dx sets alignment-left for left and centre columns and\r\n alignment-right for right and centre ones, whenever the cell has an\r\n indicator slot at all - even an unsorted, hidden one. -->\r\n <div\r\n class=\"nxe-datagrid-text-content\"\r\n [class.nxe-text-content-alignment-left]=\"hInd && hAlign !== 'right'\"\r\n [class.nxe-text-content-alignment-right]=\"hInd && hAlign !== 'left'\"\r\n [class.nxe-sort-indicator]=\"hDir !== null\"\r\n [class.nxe-header-filter-indicator]=\"hHf\">\r\n {{ cell.column.caption() }}\r\n </div>\r\n @if (hInd && !hLead) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"headerIndicators\"\r\n [ngTemplateOutletContext]=\"{ $implicit: cell.column, dir: hDir, sortSlot: hSortSlot, hf: hHf, hfFirst: hAlign === 'right', float: null }\" />\r\n }\r\n @if (resizeHandleVisible(cell.column, cell.leafIndex)) {\r\n <span\r\n class=\"nxe-col-resize-handle\"\r\n (pointerdown)=\"startColumnResize(cell.column, cell.leafIndex, $event)\"></span>\r\n }\r\n </td>\r\n }\r\n }\r\n @if (hi === 0 && rowEditCommandVisible()) {\r\n <td\r\n class=\"nxe-command-edit\"\r\n [attr.aria-colindex]=\"editColIndex()\"\r\n [attr.rowspan]=\"headerDepth() > 1 ? headerDepth() : null\"></td>\r\n }\r\n </tr>\r\n }\r\n @if (filterRowVisible()) {\r\n <!-- The headers view toggles column lines on every row it renders, the\r\n filter row included (m_column_headers.js:205-207). -->\r\n <!-- Every filter-row cell is numbered, as the headers view numbers it\r\n (m_columns_view.js:681-693). -->\r\n <tr class=\"nxe-row nxe-datagrid-filter-row\" [class.nxe-column-lines]=\"showColumnLines()\">\r\n @for (i of indents(groupLevelCount()); track i) {\r\n <td class=\"nxe-command-expand nxe-datagrid-group-space\" [attr.aria-colindex]=\"i + 1\"></td>\r\n }\r\n @if (masterDetailEnabled()) {\r\n <td class=\"nxe-command-expand nxe-datagrid-group-space\" [attr.aria-colindex]=\"expandColIndex()\"></td>\r\n }\r\n @if (selectColumnVisible()) {\r\n <td class=\"nxe-command-select\" [attr.aria-colindex]=\"selectColIndex()\"></td>\r\n }\r\n @for (col of visibleColumns(); track $index; let ci = $index) {\r\n <!-- dx filter cell: td.dx-editor-cell > .dx-editor-with-menu >\r\n [.dx-menu op chooser][typed editor] (dx-compat.css contract) -->\r\n <td\r\n class=\"nxe-editor-cell\"\r\n [class]=\"col.cssClass()\"\r\n [attr.aria-colindex]=\"leadCellCount() + ci + 1\">\r\n @if (col.allowFiltering()) {\r\n @switch (frEditorKind(col)) {\r\n @case ('lookup') {\r\n <select\r\n class=\"nxe-texteditor-input nxe-grid-filter-input\"\r\n [value]=\"frText(col)\"\r\n (click)=\"$event.stopPropagation()\"\r\n (change)=\"setFilterRowText(col, $any($event.target).value)\">\r\n <option value=\"\">{{ t('common.all') }}</option>\r\n @for (item of col.lookup()!.dataSource; track $index) {\r\n <option [value]=\"item[col.lookup()!.valueExpr ?? 'lovCode']\">\r\n {{ item[col.lookup()!.displayExpr ?? 'lovName'] }}\r\n </option>\r\n }\r\n </select>\r\n }\r\n @case ('boolean') {\r\n <select\r\n class=\"nxe-texteditor-input nxe-grid-filter-input\"\r\n [value]=\"frText(col)\"\r\n (click)=\"$event.stopPropagation()\"\r\n (change)=\"setFilterRowText(col, $any($event.target).value)\">\r\n <option value=\"\">{{ t('common.all') }}</option>\r\n <option value=\"true\">{{ t('grid.filterRow.true') }}</option>\r\n <option value=\"false\">{{ t('grid.filterRow.false') }}</option>\r\n </select>\r\n }\r\n @default {\r\n <div class=\"nxe-editor-with-menu\">\r\n <div class=\"nxe-menu nxe-menu-base\">\r\n <ul class=\"nxe-menu-horizontal\">\r\n <li>\r\n <div\r\n class=\"nxe-menu-item\"\r\n [attr.title]=\"t('grid.filterRow.operation')\"\r\n (click)=\"openFilterOpMenu(col, $event)\">\r\n <div class=\"nxe-menu-item-content\">\r\n <span\r\n class=\"nxe-icon nxe-icon-filter-operation-{{ frOpIconSlug(col) }}\"></span>\r\n </div>\r\n </div>\r\n </li>\r\n </ul>\r\n </div>\r\n <!-- dx sets the FIRST input's text-align to the column\r\n alignment inline, unless centre (m_filter_row.js:390-395). -->\r\n <input\r\n class=\"nxe-texteditor-input nxe-grid-filter-input\"\r\n [type]=\"frEditorKind(col) === 'date' ? 'date' : 'text'\"\r\n [attr.inputmode]=\"frEditorKind(col) === 'number' ? 'decimal' : null\"\r\n [style.text-align]=\"filterCellAlign(col)\"\r\n [value]=\"frText(col)\"\r\n (input)=\"setFilterRowText(col, $any($event.target).value)\"\r\n (click)=\"$event.stopPropagation()\" />\r\n @if (frOp(col) === 'between') {\r\n <input\r\n class=\"nxe-texteditor-input nxe-grid-filter-input nxe-fr-second\"\r\n [type]=\"frEditorKind(col) === 'date' ? 'date' : 'text'\"\r\n [attr.inputmode]=\"frEditorKind(col) === 'number' ? 'decimal' : null\"\r\n [value]=\"frText2(col)\"\r\n (input)=\"setFilterRowText(col, $any($event.target).value, true)\"\r\n (click)=\"$event.stopPropagation()\" />\r\n }\r\n </div>\r\n }\r\n }\r\n }\r\n </td>\r\n }\r\n @if (rowEditCommandVisible()) {\r\n <td class=\"nxe-command-edit\" [attr.aria-colindex]=\"editColIndex()\"></td>\r\n }\r\n </tr>\r\n }\r\n </thead>\r\n <tbody>\r\n @for (item of renderRows(); track $index; let ri = $index) {\r\n @if (item.kind === 'group') {\r\n <!-- dx group-row anatomy (bright audit): [indent cells][expand chevron\r\n cell][dx-group-cell colspan][alignByColumn summary cells]. dx numbers\r\n every cell of a rows-view row, command cells included\r\n (m_columns_view.js:681-693), and the caption as the cell after the\r\n toggle (m_rows_view.js:626-635, columnIndex groupIndex + 1). That\r\n number can be a data column's, so dx looks group rows up with\r\n `:not(.dx-group-cell)` (m_columns_view.js:942), and onCellPrepared\r\n reports the caption as no column. -->\r\n <tr\r\n class=\"nxe-row nxe-group-row\"\r\n [class.nxe-column-lines]=\"showColumnLines()\"\r\n [attr.aria-roledescription]=\"item.collapsed ? t('grid.collapsedRow') : t('grid.expandedRow')\">\r\n @for (i of indents(item.level); track i) {\r\n <td class=\"nxe-command-expand nxe-datagrid-group-space\" [attr.aria-colindex]=\"i + 1\"></td>\r\n }\r\n <td\r\n class=\"nxe-command-expand nxe-datagrid-group-space nxe-datagrid-expand nxe-selection-disabled\"\r\n [attr.aria-colindex]=\"item.level + 1\"\r\n [attr.aria-label]=\"item.collapsed ? t('grid.expand') : t('grid.collapse')\"\r\n (click)=\"toggleGroup(item.groupKey)\">\r\n <div\r\n [class.nxe-datagrid-group-opened]=\"!item.collapsed\"\r\n [class.nxe-datagrid-group-closed]=\"item.collapsed\"></div>\r\n </td>\r\n <td\r\n class=\"nxe-group-cell\"\r\n [attr.aria-colindex]=\"item.level + 2\"\r\n [attr.colspan]=\"groupCellColspan(item)\">\r\n @if (templateFor(item.column.groupCellTemplate()); as gtpl) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"gtpl\"\r\n [ngTemplateOutletContext]=\"groupCellContext(item)\" />\r\n } @else {\r\n {{ item.column.caption() }}: {{ item.text }}{{ item.captionSuffix }}\r\n }\r\n </td>\r\n @for (col of groupRowTrailing(item); track col.dataField()) {\r\n <td\r\n class=\"nxe-datagrid-summary-item\"\r\n [attr.aria-colindex]=\"leadCellCount() + visibleIndexOf(col) + 1\"\r\n [style.text-align]=\"align(col)\">\r\n {{ item.alignedSums.get(col.dataField()) || '' }}\r\n </td>\r\n }\r\n @if (groupRowTrailing(item).length && rowEditCommandVisible()) {\r\n <td class=\"nxe-command-edit\" [attr.aria-colindex]=\"editColIndex()\"></td>\r\n }\r\n </tr>\r\n } @else if (item.kind === 'groupFooter') {\r\n <!-- dx's group footer: dx-datagrid-group-footer, and column lines like\r\n any row the rows view renders (m_rows_view.js:222-229). Row lines are\r\n data rows' only (:228); the line under a footer is the column-lines\r\n rule's. -->\r\n <tr\r\n class=\"nxe-row nxe-group-footer-row nxe-datagrid-group-footer\"\r\n [class]=\"'nxe-group-footer-level-' + item.level\"\r\n [class.nxe-column-lines]=\"showColumnLines()\">\r\n @for (i of indents(groupLevelCount()); track i) {\r\n <td class=\"nxe-command-expand nxe-datagrid-group-space\" [attr.aria-colindex]=\"i + 1\"></td>\r\n }\r\n @if (masterDetailEnabled()) {\r\n <td class=\"nxe-command-expand nxe-datagrid-group-space\" [attr.aria-colindex]=\"expandColIndex()\"></td>\r\n }\r\n @if (selectColumnVisible()) {\r\n <td class=\"nxe-command-select\" [attr.aria-colindex]=\"selectColIndex()\"></td>\r\n }\r\n @for (col of visibleColumns(); track col.dataField()) {\r\n <td\r\n class=\"nxe-datagrid-summary-item\"\r\n [attr.aria-colindex]=\"leadCellCount() + visibleIndexOf(col) + 1\"\r\n [style.text-align]=\"align(col)\">\r\n {{ item.sums.get(col.dataField()) || '' }}\r\n </td>\r\n }\r\n @if (rowEditCommandVisible()) {\r\n <td class=\"nxe-command-edit\" [attr.aria-colindex]=\"editColIndex()\"></td>\r\n }\r\n </tr>\r\n } @else {\r\n <tr\r\n class=\"nxe-row nxe-data-row\"\r\n role=\"row\"\r\n [class]=\"rowClassFor(item.row)\"\r\n [attr.aria-rowindex]=\"item.rowIndex + 1\"\r\n [attr.aria-selected]=\"selectionMode() === 'none' ? null : isSelected(item.row)\"\r\n [class.nxe-row-lines]=\"showRowLines()\"\r\n [class.nxe-column-lines]=\"showColumnLines()\"\r\n [class.nxe-row-alt]=\"rowAlternationEnabled() && item.rowIndex % 2 === 1\"\r\n [class.nxe-selection]=\"isSelected(item.row)\"\r\n [class.nxe-state-hover]=\"isHovered(item.rowIndex)\"\r\n [class.nxe-edit-row]=\"isRowEditing(item.row)\"\r\n (mouseenter)=\"onRowEnter(item.rowIndex)\"\r\n (mouseleave)=\"onRowLeave()\"\r\n (click)=\"onDataRowClick(item.row, item.rowIndex)\">\r\n @for (i of indents(groupLevelCount()); track i) {\r\n <td class=\"nxe-command-expand nxe-datagrid-group-space\" [attr.aria-colindex]=\"i + 1\"></td>\r\n }\r\n @if (masterDetailEnabled()) {\r\n <!-- dx reuses the group row's toggle for the detail row\r\n (m_master_detail.js:33-43, m_utils.js:462-474), and any click\r\n in the cell toggles (m_grouping.js:523-532 `closest('.dx-datagrid-expand')`).\r\n toggleDetail stops the click there, as dx-selection-disabled does. -->\r\n <td\r\n class=\"nxe-command-expand nxe-datagrid-group-space nxe-datagrid-expand nxe-selection-disabled\"\r\n [attr.aria-colindex]=\"expandColIndex()\"\r\n [attr.aria-label]=\"isDetailExpanded(item.row) ? t('grid.collapse') : t('grid.expand')\"\r\n (click)=\"toggleDetail(item.row, $event)\">\r\n <div\r\n [class.nxe-datagrid-group-opened]=\"isDetailExpanded(item.row)\"\r\n [class.nxe-datagrid-group-closed]=\"!isDetailExpanded(item.row)\"></div>\r\n </td>\r\n }\r\n @if (selectColumnVisible()) {\r\n <td class=\"nxe-command-select\" [attr.aria-colindex]=\"selectColIndex()\">\r\n <span\r\n class=\"nxe-select-checkbox nxe-grid-checkbox nxe-checkbox nxe-datagrid-checkbox-size\"\r\n role=\"checkbox\"\r\n [attr.aria-checked]=\"isSelected(item.row)\"\r\n [class.nxe-checked]=\"isSelected(item.row)\"\r\n (click)=\"onCheckboxClick(item.row, $event)\">\r\n <span class=\"nxe-grid-checkbox-box\"></span>\r\n </span>\r\n </td>\r\n }\r\n @for (col of visibleColumns(); track $index; let ci = $index) {\r\n @if (!isMergeSuppressed(ri, col)) {\r\n <td\r\n [class]=\"col.cssClass()\"\r\n role=\"gridcell\"\r\n [attr.aria-colindex]=\"leadCellCount() + ci + 1\"\r\n [attr.tabindex]=\"isCellTabStop(item.rowIndex, ci) ? 0 : -1\"\r\n (focus)=\"focusCell(item.rowIndex, ci)\"\r\n [attr.aria-roledescription]=\"\r\n editableCell(col) && (cellEditingEnabled() || rowEditingEnabled())\r\n ? t('grid.editableCell')\r\n : null\r\n \"\r\n [style.text-align]=\"align(col)\"\r\n [attr.rowspan]=\"mergeSpan(ri, col)\"\r\n [class.nxe-editor-cell]=\"isEditingCell(item.row, col) || (isRowEditing(item.row) && editableCell(col))\"\r\n (click)=\"onCellClick(item.row, col, $event)\"\r\n (focusout)=\"onEditCellFocusOut(item.row, col, $event)\">\r\n @if (treeMode && ci === 0) {\r\n <!-- dx's tree icons (dx-treelist-icon-container): one 14px slot\r\n per level, then the 14x18 toggle or, on a leaf, one more slot.\r\n Floated as dx floats it in the fixed table, so the text\r\n wraps beside it and starts flush after it. Not named\r\n nxe-treelist-*: the dx mirror would hand it to an app's\r\n newer-dx tree layout (18px slots). -->\r\n <span class=\"nxe-tree-icons\" [class.nxe-tree-icons-select]=\"showCheckboxes()\">\r\n @for (i of indents(item.level || 0); track i) {\r\n <span class=\"nxe-tree-expander-stub\"></span>\r\n }\r\n @if (item.hasChildren) {\r\n <span\r\n class=\"nxe-tree-expander\"\r\n [class.nxe-collapsed]=\"!item.expanded\"\r\n (click)=\"toggleTreeNode(item.nodeKey, $event)\"></span>\r\n } @else {\r\n <span class=\"nxe-tree-expander-stub\"></span>\r\n }\r\n <!-- dx draws a tree list's row checkbox here, in the icon\r\n container after the toggle, not in a command column. -->\r\n @if (showCheckboxes()) {\r\n <span\r\n class=\"nxe-select-checkbox nxe-grid-checkbox nxe-checkbox nxe-datagrid-checkbox-size nxe-tree-checkbox\"\r\n role=\"checkbox\"\r\n [attr.aria-checked]=\"isTreeIndeterminate(item.row) ? 'mixed' : isSelected(item.row)\"\r\n [class.nxe-checked]=\"isSelected(item.row)\"\r\n [class.nxe-indeterminate]=\"isTreeIndeterminate(item.row)\"\r\n (click)=\"onCheckboxClick(item.row, $event)\">\r\n <span class=\"nxe-grid-checkbox-box\"></span>\r\n </span>\r\n }\r\n </span>\r\n }\r\n @if (isEditingCell(item.row, col) || (isRowEditing(item.row) && editableCell(col))) {\r\n @if (templateFor(col.editCellTemplate()); as etpl) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"etpl\"\r\n [ngTemplateOutletContext]=\"editCellContext(item.row, item.rowIndex, ci, col)\" />\r\n } @else if (isEditingCell(item.row, col)) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"cellEditor\"\r\n [ngTemplateOutletContext]=\"{ row: item.row, col: col, mode: 'cell' }\" />\r\n } @else {\r\n <ng-container\r\n [ngTemplateOutlet]=\"cellEditor\"\r\n [ngTemplateOutletContext]=\"{ row: item.row, col: col, mode: 'row' }\" />\r\n }\r\n } @else if (templateFor(col.cellTemplate()); as tpl) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"tpl\"\r\n [ngTemplateOutletContext]=\"cellContext(item.row, item.rowIndex, ci, col)\" />\r\n } @else if (isBooleanCell(col)) {\r\n <!-- dx showEditorAlways: a boolean cell is a dx checkbox, not\r\n \"true\"/\"false\". It is read-only unless the grid edits cells\r\n and the column is editable (m_editing.js:210, 1832-1847);\r\n then one click toggles the value in place. -->\r\n <span\r\n class=\"nxe-grid-checkbox nxe-grid-checkbox-readonly nxe-checkbox nxe-datagrid-checkbox-size\"\r\n role=\"checkbox\"\r\n [class.nxe-grid-checkbox-left]=\"align(col) === 'left'\"\r\n [class.nxe-grid-checkbox-right]=\"align(col) === 'right'\"\r\n [class.nxe-checked]=\"getRaw(item.row, col.dataField()) === true\"\r\n [class.nxe-state-readonly]=\"booleanReadOnly(col)\"\r\n [attr.aria-checked]=\"getRaw(item.row, col.dataField()) === true\"\r\n [attr.aria-readonly]=\"booleanReadOnly(col) ? 'true' : null\"\r\n (click)=\"onBooleanCellClick(item.row, col, $event)\">\r\n <span class=\"nxe-grid-checkbox-box\"></span>\r\n </span>\r\n } @else {\r\n <!-- In an ng-container so the text node carries no padding space:\r\n after the tree icons one would render as a 3px gap. -->\r\n <ng-container>{{ cellText(col, item.row) }}</ng-container>\r\n }\r\n </td>\r\n }\r\n }\r\n @if (rowEditCommandVisible()) {\r\n <td class=\"nxe-command-edit nxe-datagrid-drag-action\" [attr.aria-colindex]=\"editColIndex()\">\r\n @if (isRowEditing(item.row)) {\r\n <a class=\"nxe-link nxe-link-save\" (click)=\"saveRowEdit(item.row, $event)\">{{ t('grid.save') }}</a>\r\n <a class=\"nxe-link nxe-link-cancel\" (click)=\"cancelRowEdit($event)\">{{ t('grid.cancel') }}</a>\r\n } @else {\r\n <a class=\"nxe-link nxe-link-edit\" (click)=\"startRowEdit(item.row, $event)\">{{ t('grid.edit') }}</a>\r\n }\r\n </td>\r\n }\r\n </tr>\r\n @if (masterDetailEnabled() && isDetailExpanded(item.row)) {\r\n <!-- dx's detail row takes row lines as well as column lines, and its one\r\n cell is dx-master-detail-cell (m_master_detail.js:290-294, :325),\r\n rendered as column 0 - so aria-colindex 1. -->\r\n <tr\r\n class=\"nxe-row nxe-master-detail-row\"\r\n [class.nxe-row-lines]=\"showRowLines()\"\r\n [class.nxe-column-lines]=\"showColumnLines()\">\r\n <td class=\"nxe-master-detail-cell nxe-cell-focus-disabled\" aria-colindex=\"1\" [attr.colspan]=\"allColCount()\">\r\n @if (detailTemplate(); as tpl) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"tpl\"\r\n [ngTemplateOutletContext]=\"detailContext(item.row, item.rowIndex)\" />\r\n }\r\n </td>\r\n </tr>\r\n }\r\n }\r\n }\r\n <!-- dx's free-space row (m_rows_view.js:448-466): one cell per column, so the\r\n column lines run on through the space it fills. The row is\r\n role=\"presentation\", as dx's `_createEmptyRow` makes it, so assistive tech\r\n inside role=grid does not meet a row of empty gridcells; its cells take\r\n no role and no aria-colindex: nothing that addresses cells may count them. -->\r\n <tr\r\n role=\"presentation\"\r\n class=\"nxe-row nxe-freespace-row\"\r\n [class.nxe-column-lines]=\"showColumnLines()\"\r\n [style.height.px]=\"freeSpaceHeight() || null\">\r\n @for (i of indents(groupLevelCount()); track i) {\r\n <td class=\"nxe-command-expand nxe-datagrid-group-space\"></td>\r\n }\r\n @if (masterDetailEnabled()) {\r\n <td class=\"nxe-command-expand nxe-datagrid-group-space\"></td>\r\n }\r\n @if (selectColumnVisible()) {\r\n <td class=\"nxe-command-select\"></td>\r\n }\r\n @for (col of visibleColumns(); track $index) {\r\n <td [class]=\"col.cssClass()\"></td>\r\n }\r\n @if (rowEditCommandVisible()) {\r\n <td class=\"nxe-command-edit\"></td>\r\n }\r\n </tr>\r\n </tbody>\r\n @if (hasTotalFooter()) {\r\n <tfoot class=\"nxe-datagrid-total-footer\">\r\n <tr class=\"nxe-row\">\r\n @for (i of indents(groupLevelCount()); track i) {\r\n <td class=\"nxe-command-expand nxe-datagrid-group-space\" [attr.aria-colindex]=\"i + 1\"></td>\r\n }\r\n @if (masterDetailEnabled()) {\r\n <td class=\"nxe-command-expand nxe-datagrid-group-space\" [attr.aria-colindex]=\"expandColIndex()\"></td>\r\n }\r\n @if (selectColumnVisible()) {\r\n <td class=\"nxe-command-select\" [attr.aria-colindex]=\"selectColIndex()\"></td>\r\n }\r\n @for (col of visibleColumns(); track col.dataField()) {\r\n <td\r\n class=\"nxe-datagrid-summary-item\"\r\n [attr.aria-colindex]=\"leadCellCount() + visibleIndexOf(col) + 1\"\r\n [style.text-align]=\"align(col)\">\r\n {{ totalSums().get(col.dataField()) || '' }}\r\n </td>\r\n }\r\n @if (rowEditCommandVisible()) {\r\n <td class=\"nxe-command-edit\" [attr.aria-colindex]=\"editColIndex()\"></td>\r\n }\r\n </tr>\r\n </tfoot>\r\n }\r\n </table>\r\n </div>\r\n\r\n @if (remoteBusy()) {\r\n <!-- Server-side data: a request is in flight over the rows already on screen. -->\r\n <div class=\"nxe-datagrid-loadpanel nxe-datagrid-remote-busy\" role=\"status\" [attr.aria-label]=\"t('grid.remote.loading')\">\r\n <span class=\"nxe-grid-spinner\"></span>\r\n </div>\r\n }\r\n\r\n @if (isEmpty()) {\r\n <!-- dx `.dx-datagrid-rowsview.dx-empty{height:100px}`: our rows view also holds the\r\n <thead>, so the empty body is its own box after the table. dx appends the nodata\r\n span to the rows view outside its scrollable (m_utils.js:206-210) and positions the\r\n load panel `of:` the rows view, header excluded (m_rows_view.js:1023) - both live\r\n in this box. The load panel can: loading() implies a null source, hence no rows.\r\n If the total footer is ever shown for empty data (dx m_summary.js:487-503), this\r\n box has to move between the <tbody> and the <tfoot>. -->\r\n <div class=\"nxe-datagrid-empty-body\">\r\n @if (remoteFailed()) {\r\n <div class=\"nxe-datagrid-remote-error\" role=\"alert\">\r\n <span class=\"nxe-datagrid-remote-error-text\">{{ t('grid.remote.error') }}</span>\r\n <button type=\"button\" class=\"nxe-datagrid-remote-retry\" (click)=\"retryRemote()\">{{ t('grid.remote.retry') }}</button>\r\n </div>\r\n } @else if (!loading()) {\r\n <span class=\"nxe-datagrid-nodata\">{{ noDataText() }}</span>\r\n } @else {\r\n <div class=\"nxe-datagrid-loadpanel\"><span class=\"nxe-grid-spinner\"></span></div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n @if (pagerVisible()) {\r\n <!-- dx's pager (pagination/content.js) under dx's class names: the page sizes float left;\r\n the pages float right - the info text, then prev, the page buttons and next. dx\r\n renders div[role=button]s; these are real buttons with dx's labels. The prev/next\r\n buttons are empty: the chevron is drawn by CSS. At either end dx only takes the\r\n button out of the tab order (page_index_selector.js), so it keeps focus rather than\r\n dropping it as a disabled button would; a click there is a no-op (goToPage clamps). -->\r\n <div\r\n class=\"nxe-datagrid-pager nxe-pager nxe-grid-pager\"\r\n role=\"navigation\"\r\n [attr.aria-label]=\"pagerCmp()?.label() ?? t('grid.pager.navigation')\">\r\n @if (pagerCmp()?.showPageSizeSelector()) {\r\n <div class=\"nxe-page-sizes\">\r\n @if (pagerCompact()) {\r\n @if (pageSizes().length) {\r\n <select class=\"nxe-pager-size\" [attr.aria-label]=\"t('grid.pager.pageSize')\" (change)=\"onPageSizeSelect($event)\">\r\n @for (s of pageSizes(); track $index) {\r\n <option [value]=\"s\" [selected]=\"s === effectivePageSize()\">{{ s }}</option>\r\n }\r\n </select>\r\n }\r\n } @else {\r\n @for (s of pageSizes(); track $index; let first = $first) {\r\n <button\r\n type=\"button\"\r\n class=\"nxe-page-size\"\r\n [class.nxe-selection]=\"s === effectivePageSize()\"\r\n [class.nxe-first-child]=\"first\"\r\n [attr.aria-label]=\"t('grid.pager.itemsPerPage', s)\"\r\n (click)=\"changePageSize(s)\">{{ s }}</button>\r\n }\r\n }\r\n </div>\r\n }\r\n <div class=\"nxe-pages\">\r\n @if (pagerCmp()?.showInfo()) {\r\n <div class=\"nxe-info nxe-pager-info\">{{ pageInfo() }}</div>\r\n }\r\n <div class=\"nxe-page-indexes nxe-pager-pages\">\r\n @if (pagerNavVisible()) {\r\n <button\r\n type=\"button\"\r\n class=\"nxe-navigate-button nxe-prev-button nxe-pager-nav\"\r\n [class.nxe-button-disable]=\"currentPage() === 0\"\r\n [attr.aria-disabled]=\"currentPage() === 0 ? 'true' : null\"\r\n [attr.tabindex]=\"currentPage() === 0 ? -1 : null\"\r\n [attr.aria-label]=\"t('grid.pager.previous')\"\r\n (click)=\"prevPage()\"></button>\r\n }\r\n @for (item of pageItems(); track item.key) {\r\n @if (item.separator) {\r\n <div class=\"nxe-separator\">. . .</div>\r\n } @else {\r\n <button\r\n type=\"button\"\r\n class=\"nxe-page nxe-pager-page\"\r\n [class.nxe-selection]=\"item.page === currentPage()\"\r\n [class.nxe-active]=\"item.page === currentPage()\"\r\n [attr.aria-label]=\"t('grid.pager.page', item.page + 1)\"\r\n [attr.aria-current]=\"item.page === currentPage() ? 'page' : null\"\r\n (click)=\"goToPage(item.page)\">{{ item.page + 1 }}</button>\r\n }\r\n }\r\n @if (pagerNavVisible()) {\r\n <button\r\n type=\"button\"\r\n class=\"nxe-navigate-button nxe-next-button nxe-pager-nav\"\r\n [class.nxe-button-disable]=\"currentPage() >= pageCount() - 1\"\r\n [attr.aria-disabled]=\"currentPage() >= pageCount() - 1 ? 'true' : null\"\r\n [attr.tabindex]=\"currentPage() >= pageCount() - 1 ? -1 : null\"\r\n [attr.aria-label]=\"t('grid.pager.next')\"\r\n (click)=\"nextPage()\"></button>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n</div>\r\n\r\n<!-- Inline cell/row editor (dxo-editing, orbit addition). `mode: 'cell'` commits on blur/Enter;\r\n `mode: 'row'` buffers on input until the command column's Save. -->\r\n<ng-template #cellEditor let-row=\"row\" let-col=\"col\" let-mode=\"mode\">\r\n @if (col.lookup(); as lookup) {\r\n <select\r\n class=\"nxe-texteditor-input nxe-grid-cell-editor\"\r\n [value]=\"editorInputValue(row, col)\"\r\n (click)=\"$event.stopPropagation()\"\r\n (change)=\"\r\n mode === 'cell'\r\n ? commitCellEdit(row, col, $any($event.target).value)\r\n : bufferRowEdit(col, $any($event.target).value)\r\n \">\r\n <option value=\"\"></option>\r\n @for (item of lookup.dataSource; track $index) {\r\n <option [value]=\"item[lookup.valueExpr ?? 'lovCode']\">\r\n {{ item[lookup.displayExpr ?? 'lovName'] }}\r\n </option>\r\n }\r\n </select>\r\n } @else if (colType(col) === 'boolean') {\r\n <!-- dx's checkbox editor, the same box as the resting boolean cell (never the browser's\r\n native checkbox). Cell mode toggles resting cells in place and never gets here; this\r\n is the row being edited, and a click or Space only buffers until Save. It takes focus\r\n as dx's dxCheckBox does (tabIndex 0, Space toggles: check_box.js:125), so Tab reaches\r\n it with the row's other editors. -->\r\n <span\r\n class=\"nxe-grid-checkbox nxe-grid-checkbox-readonly nxe-checkbox nxe-datagrid-checkbox-size nxe-grid-cell-editor-check\"\r\n role=\"checkbox\"\r\n tabindex=\"0\"\r\n [class.nxe-grid-checkbox-left]=\"align(col) === 'left'\"\r\n [class.nxe-grid-checkbox-right]=\"align(col) === 'right'\"\r\n [class.nxe-checked]=\"editorChecked(row, col)\"\r\n [attr.aria-checked]=\"editorChecked(row, col)\"\r\n (click)=\"toggleEditorCheck(row, col, mode, $event)\"\r\n (keydown.space)=\"toggleEditorCheck(row, col, mode, $event)\">\r\n <span class=\"nxe-grid-checkbox-box\"></span>\r\n </span>\r\n } @else {\r\n <input\r\n type=\"text\"\r\n class=\"nxe-texteditor-input nxe-grid-cell-editor\"\r\n [value]=\"editorInputValue(row, col)\"\r\n (click)=\"$event.stopPropagation()\"\r\n (keydown.enter)=\"mode === 'cell' && commitCellEdit(row, col, $any($event.target).value)\"\r\n (keydown.escape)=\"mode === 'cell' && cancelCellEdit()\"\r\n (blur)=\"mode === 'cell' && commitCellEdit(row, col, $any($event.target).value)\"\r\n (input)=\"mode === 'row' && bufferRowEdit(col, $any($event.target).value)\" />\r\n }\r\n</ng-template>\r\n\r\n<!-- A header cell's indicator block (dx .dx-column-indicators). dx creates the sort span first and\r\n the funnel second; the funnel is prepended when the column is right-aligned\r\n (m_header_filter_core.js _renderIndicator), so a right-aligned column reads [funnel][sort] and\r\n every other [sort][funnel]. An unsorted sortable column keeps an empty `nxe-sort-none` span, as\r\n dx does: hidden in the fixed table, measured by the best-fit pass. -->\r\n<ng-template\r\n #headerIndicators\r\n let-col\r\n let-dir=\"dir\"\r\n let-sortSlot=\"sortSlot\"\r\n let-hf=\"hf\"\r\n let-hfFirst=\"hfFirst\"\r\n let-float=\"float\">\r\n <div class=\"nxe-column-indicators\" role=\"presentation\" [style.float]=\"float\">\r\n @for (kind of hfFirst ? ['funnel', 'sort'] : ['sort', 'funnel']; track kind) {\r\n @if (kind === 'funnel') {\r\n @if (hf) {\r\n <span\r\n class=\"nxe-grid-hf-icon\"\r\n [class.nxe-hf-active]=\"hasHeaderFilter(col)\"\r\n (click)=\"openHeaderFilter(col, $event)\"></span>\r\n }\r\n } @else if (sortSlot) {\r\n <span\r\n class=\"nxe-sort\"\r\n [class.nxe-sort-none]=\"!dir\"\r\n [class.nxe-sort-up]=\"dir === 'asc'\"\r\n [class.nxe-sort-down]=\"dir === 'desc'\"></span>\r\n }\r\n }\r\n </div>\r\n</ng-template>\r\n\r\n<!-- The header-filter popup (#hfPanel) moved to nxe-header-filter-panel.component.html \u2014\r\n openHeaderFilter attaches it through openNxeHeaderFilterOverlay (pivot parity Phase 3a). -->\r\n\r\n<!-- Filter-row operation-chooser dropdown (dx filter menu) -->\r\n<ng-template #frOpMenu>\r\n <div class=\"nxe-fr-op-menu nxe-datagrid\">\r\n @for (item of frMenuOps(); track item.op) {\r\n <div\r\n class=\"nxe-menu-item\"\r\n [class.nxe-menu-item-selected]=\"frMenuColumn() && frOp(frMenuColumn()!) === item.op\"\r\n (click)=\"selectFilterOp(item.op)\">\r\n <span class=\"nxe-icon nxe-icon-filter-operation-{{ opIconSlug(item.op) }}\"></span>\r\n <span class=\"nxe-menu-item-text\">{{ item.label }}</span>\r\n </div>\r\n }\r\n <!-- dx draws Reset with the chooser's own magnifier. -->\r\n <div class=\"nxe-menu-item nxe-fr-reset\" (click)=\"resetFilterRow()\">\r\n <span class=\"nxe-icon nxe-icon-filter-operation-default\"></span>\r\n <span class=\"nxe-menu-item-text\">{{ t('grid.filterRow.reset') }}</span>\r\n </div>\r\n </div>\r\n</ng-template>\r\n\r\n", styles: ["@charset \"UTF-8\";.nxe-data-grid{display:block}.nxe-data-grid .nxe-datagrid{height:100%;position:relative;white-space:normal;cursor:default;color:var(--nxe-ui-text, #333)}.nxe-data-grid .nxe-gridbase-container{display:flex;flex-direction:column;height:100%;line-height:inherit;min-height:inherit;max-height:inherit}.nxe-data-grid .nxe-gridbase-container>*{flex:0 0 auto}.nxe-data-grid .nxe-datagrid-rowsview{position:relative;overflow:auto;flex:1 1 auto;min-height:0;background-color:var(--nxe-ui-surface, #fff)}.nxe-data-grid .nxe-datagrid-content{display:block;position:relative;min-height:100%}.nxe-data-grid .nxe-datagrid-rowsview .nxe-datagrid-content{overflow-anchor:none}.nxe-data-grid .nxe-datagrid-rowsview.nxe-empty{display:flex;flex-direction:column;height:auto}.nxe-data-grid .nxe-datagrid-rowsview.nxe-empty>.nxe-datagrid-content{flex:none;min-height:0}.nxe-data-grid .nxe-datagrid-rowsview.nxe-empty>.nxe-datagrid-empty-body{position:sticky;inset-inline-start:0;flex:1 1 var(--nxe-ui-grid-empty-height, 100px);min-height:0}.nxe-data-grid table.nxe-datagrid-table{display:table;width:100%;table-layout:fixed;border-collapse:collapse;margin:0;overflow-x:clip}.nxe-data-grid .nxe-datagrid-content .nxe-datagrid-table{max-width:none}.nxe-data-grid .nxe-datagrid-headers{display:table-header-group;position:sticky;top:0;z-index:1;color:var(--nxe-ui-neutral-600, #757575);font-weight:400;-webkit-user-select:none;user-select:none;-webkit-touch-callout:none;touch-action:pinch-zoom;background:var(--nxe-ui-surface, #fff);border-bottom:0}.nxe-data-grid tbody{display:table-row-group}.nxe-data-grid .nxe-row{display:table-row}.nxe-data-grid .nxe-datagrid-rowsview tbody>.nxe-row{border-top:var(--nxe-ui-grid-line-width, 1px) solid transparent;border-bottom:var(--nxe-ui-grid-line-width, 1px) solid transparent}.nxe-data-grid .nxe-datagrid-headers>.nxe-row{border-top:0;border-bottom:0}.nxe-data-grid .nxe-datagrid-headers:before{content:none}.nxe-data-grid .nxe-datagrid-headers>.nxe-row>td{border-bottom:var(--nxe-ui-grid-line-width, 1px) solid var(--nxe-ui-grid-header-line-color, #ddd)}.nxe-data-grid .nxe-datagrid-rowsview.nxe-datagrid-after-headers{border-top:0}.nxe-data-grid .nxe-datagrid .nxe-row-lines>td{border-bottom:var(--nxe-ui-grid-line-width, 1px) solid var(--nxe-ui-grid-row-line-color, #ddd)}.nxe-data-grid .nxe-datagrid-rowsview .nxe-row.nxe-row-lines:first-child,.nxe-data-grid .nxe-datagrid-rowsview .nxe-row.nxe-row-lines:first-child>td{border-top:none}.nxe-data-grid .nxe-datagrid .nxe-column-lines>td{border-left:var(--nxe-ui-grid-line-width, 1px) solid var(--nxe-ui-grid-column-line-color, #ddd);border-right:var(--nxe-ui-grid-line-width, 1px) solid var(--nxe-ui-grid-column-line-color, #ddd)}.nxe-data-grid .nxe-datagrid .nxe-column-lines>td:first-child{border-left:none}.nxe-data-grid .nxe-datagrid .nxe-column-lines>td:last-child{border-right:none}.nxe-data-grid .nxe-datagrid-content .nxe-datagrid-table .nxe-row>td:first-child{border-left:0}.nxe-data-grid .nxe-datagrid-content .nxe-datagrid-table .nxe-row>td.nxe-datagrid-group-space{border-right:none}.nxe-data-grid .nxe-datagrid-content .nxe-datagrid-table .nxe-row>td.nxe-datagrid-group-space+td{border-left:none}.nxe-data-grid .nxe-datagrid-rowsview .nxe-row.nxe-group-row>td{border-top:var(--nxe-ui-grid-line-width, 1px) solid var(--nxe-ui-grid-row-line-color, #ddd);border-bottom:var(--nxe-ui-grid-line-width, 1px) solid var(--nxe-ui-grid-row-line-color, #ddd)}.nxe-data-grid .nxe-datagrid-rowsview .nxe-row.nxe-group-row:first-child,.nxe-data-grid .nxe-datagrid-rowsview .nxe-row.nxe-group-row:first-child>td{border-top:none}.nxe-data-grid .nxe-datagrid-rowsview .nxe-group-row.nxe-row>td{border-left-color:transparent;border-right-color:transparent}.nxe-data-grid .nxe-row.nxe-datagrid-group-footer>td{border-top:var(--nxe-ui-grid-line-width, 1px) solid var(--nxe-ui-grid-row-line-color, #ddd);background-color:var(--nxe-ui-surface, #fff);border-left-width:0;border-right-width:0}.nxe-data-grid .nxe-row.nxe-datagrid-group-footer.nxe-column-lines{border-bottom:var(--nxe-ui-grid-line-width, 1px) solid var(--nxe-ui-grid-row-line-color, #ddd)}.nxe-data-grid .nxe-datagrid-rowsview .nxe-row.nxe-edit-row>td{border-top:var(--nxe-ui-grid-line-width, 1px) solid var(--nxe-ui-grid-row-line-color, #ddd);border-bottom:var(--nxe-ui-grid-line-width, 1px) solid var(--nxe-ui-grid-row-line-color, #ddd)}.nxe-data-grid .nxe-datagrid-rowsview .nxe-row.nxe-edit-row:first-child>td{border-top-width:0;border-bottom:var(--nxe-ui-grid-line-width, 1px) solid var(--nxe-ui-grid-row-line-color, #ddd)}.nxe-data-grid .nxe-datagrid-rowsview .nxe-selection.nxe-row:not(.nxe-row-focused):not(.nxe-row-lines)>td,.nxe-data-grid .nxe-datagrid-rowsview .nxe-selection.nxe-row:not(.nxe-row-focused):hover:not(.nxe-row-lines)>td{border-top:var(--nxe-ui-grid-line-width, 1px) solid var(--nxe-ui-neutral-150, #e6e6e6);border-bottom:var(--nxe-ui-grid-line-width, 1px) solid var(--nxe-ui-neutral-150, #e6e6e6)}.nxe-data-grid .nxe-datagrid-rowsview .nxe-selection.nxe-row:not(.nxe-row-focused).nxe-column-lines>td,.nxe-data-grid .nxe-datagrid-rowsview .nxe-selection.nxe-row:not(.nxe-row-focused):hover.nxe-column-lines>td{border-left-color:var(--nxe-ui-grid-column-line-color, #ddd);border-right-color:var(--nxe-ui-grid-column-line-color, #ddd)}.nxe-data-grid .nxe-datagrid-rowsview .nxe-selection.nxe-row:not(.nxe-row-focused).nxe-row-lines>td,.nxe-data-grid .nxe-datagrid-rowsview .nxe-selection.nxe-row:not(.nxe-row-focused):hover.nxe-row-lines>td{border-bottom-color:var(--nxe-ui-grid-row-line-color, #ddd)}.nxe-data-grid .nxe-datagrid .nxe-row-alt>td{background-color:var(--nxe-ui-grid-alt-row-bg, #f5f5f5)}.nxe-data-grid .nxe-datagrid .nxe-row-alt.nxe-row:not(.nxe-row-removed){border-bottom-color:transparent}.nxe-data-grid .nxe-datagrid-borders>.nxe-gridbase-container>.nxe-datagrid-rowsview{border:var(--nxe-ui-grid-line-width, 1px) solid var(--nxe-ui-grid-border-color, #ddd)}.nxe-data-grid .nxe-datagrid-borders>.nxe-gridbase-container>.nxe-datagrid-header-panel{border-bottom:0}.nxe-data-grid .nxe-datagrid-borders>.nxe-gridbase-container>.nxe-grid-pager{margin-top:-1px}.nxe-data-grid .nxe-datagrid-borders>.nxe-gridbase-container>.nxe-datagrid-rowsview.nxe-last-row-border>.nxe-datagrid-content>.nxe-datagrid-table>tbody>.nxe-data-row:nth-last-child(2),.nxe-data-grid .nxe-datagrid-borders>.nxe-gridbase-container>.nxe-datagrid-rowsview.nxe-last-row-border>.nxe-datagrid-content>.nxe-datagrid-table>tbody>.nxe-data-row:nth-last-child(2)>td{border-bottom-width:0}.nxe-data-grid .nxe-datagrid-nodata{position:absolute;top:50%;left:50%;width:100%;text-align:center;transform:translate(-50%,-50%);color:var(--nxe-ui-text, #333);font-size:var(--nxe-ui-grid-nodata-font-size, 17px);opacity:.7;pointer-events:none}.nxe-data-grid .nxe-datagrid-loadpanel{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:var(--nxe-ui-scrim, rgba(255, 255, 255, .5));z-index:2}.nxe-data-grid .nxe-datagrid-remote-error{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--nxe-ui-space-2, 8px);color:var(--nxe-ui-invalid, #d9534f)}.nxe-data-grid .nxe-datagrid-remote-retry{padding:var(--nxe-ui-space-1, 4px) var(--nxe-ui-space-3, 12px);border:1px solid var(--nxe-ui-border, #ddd);border-radius:var(--nxe-ui-radius, 4px);background:var(--nxe-ui-surface, #fff);color:var(--nxe-ui-text, #333);font:inherit;cursor:pointer}.nxe-data-grid .nxe-grid-spinner{width:32px;height:32px;border:3px solid var(--nxe-ui-spinner-track, rgba(48, 55, 76, .2));border-top-color:var(--nxe-ui-ink-strong, #30374c);border-radius:50%;animation:nxe-grid-spin .8s linear infinite}.nxe-data-grid .nxe-header-row>td{position:relative;cursor:default;-webkit-user-select:none;user-select:none;white-space:nowrap;overflow:clip;overflow-clip-margin:4px}.nxe-data-grid .nxe-header-row>td.nxe-datagrid-action{cursor:pointer}.nxe-data-grid .nxe-header-row>td>.nxe-datagrid-text-content{display:inline-block;vertical-align:top;white-space:normal;overflow:hidden;text-overflow:ellipsis}.nxe-data-grid .nxe-datagrid-nowrap .nxe-header-row>td>.nxe-datagrid-text-content{white-space:nowrap}.nxe-data-grid .nxe-header-row>td>.nxe-datagrid-text-content:not(.nxe-sort-indicator):not(.nxe-header-filter-indicator){max-width:100%}.nxe-data-grid .nxe-header-row .nxe-sort-indicator,.nxe-data-grid .nxe-header-row .nxe-header-filter-indicator{max-width:calc(100% - 17px)}.nxe-data-grid .nxe-header-row .nxe-sort-indicator.nxe-text-content-alignment-left,.nxe-data-grid .nxe-header-row .nxe-header-filter-indicator.nxe-text-content-alignment-left{margin-right:3px}.nxe-data-grid .nxe-header-row .nxe-sort-indicator.nxe-text-content-alignment-right,.nxe-data-grid .nxe-header-row .nxe-header-filter-indicator.nxe-text-content-alignment-right{margin-left:3px}.nxe-data-grid .nxe-header-row .nxe-sort-indicator.nxe-text-content-alignment-left.nxe-text-content-alignment-right,.nxe-data-grid .nxe-header-row .nxe-header-filter-indicator.nxe-text-content-alignment-left.nxe-text-content-alignment-right{max-width:calc(100% - 34px)}.nxe-data-grid .nxe-header-row .nxe-sort-indicator.nxe-header-filter-indicator{max-width:calc(100% - 31px)}.nxe-data-grid .nxe-header-row .nxe-sort-indicator.nxe-header-filter-indicator.nxe-text-content-alignment-left.nxe-text-content-alignment-right{max-width:calc(100% - 62px)}.nxe-data-grid .nxe-column-indicators{display:inline-block;vertical-align:top;white-space:nowrap;scale:var(--nxe-ui-grid-indicator-scale, 1);padding-top:var(--nxe-ui-grid-indicator-offset, 0px)}.nxe-data-grid .nxe-column-indicators.nxe-visibility-hidden{visibility:hidden}.nxe-data-grid .nxe-column-indicator-spacer{display:inline-block;width:14px;height:14px;vertical-align:top}.nxe-data-grid .nxe-datagrid-table-fixed .nxe-column-indicators .nxe-sort.nxe-sort-none,.nxe-data-grid .nxe-datagrid-table-fixed .nxe-column-indicators .nxe-column-indicator-spacer-none{display:none}.nxe-data-grid .nxe-datagrid-best-fit .nxe-column-indicators{float:none!important}.nxe-data-grid .nxe-datagrid-best-fit .nxe-column-indicators>span{width:14px}.nxe-data-grid .nxe-datagrid-best-fit .nxe-column-indicators .nxe-sort.nxe-sort-none,.nxe-data-grid .nxe-datagrid-best-fit .nxe-column-indicators .nxe-column-indicator-spacer-none{display:inline-block!important}.nxe-data-grid .nxe-datagrid-best-fit .nxe-header-row .nxe-datagrid-text-content{max-width:none!important}.nxe-data-grid .nxe-datagrid-best-fit .nxe-text-content-alignment-left{margin-right:3px}.nxe-data-grid .nxe-datagrid-best-fit .nxe-text-content-alignment-right{margin-left:3px}.nxe-data-grid .nxe-sort{display:inline-block;position:relative;width:14px;height:14px;vertical-align:top}.nxe-data-grid .nxe-sort:after{content:\"\";position:absolute;left:0;top:0;width:14px;height:14px;background-color:currentColor}@media(forced-colors:active){.nxe-data-grid .nxe-sort:after{forced-color-adjust:none;background-color:CanvasText}}.nxe-data-grid .nxe-sort.nxe-sort-up:after{clip-path:polygon(6.5px 0,9px 3px,7px 3px,7px 14px,6px 14px,6px 3px,4px 3px)}.nxe-data-grid .nxe-sort.nxe-sort-down:after{clip-path:polygon(6px 0,7px 0,7px 11px,9px 11px,6.5px 14px,4px 11px,6px 11px)}.nxe-data-grid .nxe-sort.nxe-sort-none:after{content:none}.nxe-data-grid .nxe-datagrid-group-panel .nxe-sort{color:var(--nxe-ui-neutral-600, #757575)}.nxe-data-grid .nxe-datagrid-group-panel .nxe-group-panel-item .nxe-sort{margin-left:6px}.nxe-data-grid .nxe-grid-hf-icon{display:inline-block;position:relative;width:14px;height:14px;cursor:pointer;vertical-align:top;opacity:.5}.nxe-data-grid .nxe-grid-hf-icon:before{content:\"\";position:absolute;inset:0;background-color:var(--nxe-ui-neutral-600, #757575);clip-path:polygon(0 0,14px 0,14px 3px,9px 8px,9px 14px,5px 14px,5px 8px,0 3px)}@media(forced-colors:active){.nxe-data-grid .nxe-grid-hf-icon:before{forced-color-adjust:none;background-color:CanvasText}}.nxe-data-grid .nxe-grid-hf-icon:after{content:\"\";position:absolute;inset:0;margin:-7px}.nxe-data-grid .nxe-grid-hf-icon.nxe-hf-active{opacity:1}.nxe-data-grid .nxe-grid-pager{display:block;flex:none;overflow:hidden;width:100%;padding:9px 0;border-top:1px solid var(--nxe-ui-grid-border-color, #ddd);color:var(--nxe-ui-text, #333);line-height:normal;-webkit-user-select:none;user-select:none}.nxe-data-grid .nxe-grid-pager .nxe-page-sizes{float:left}.nxe-data-grid .nxe-grid-pager .nxe-pages{float:right;white-space:nowrap}.nxe-data-grid .nxe-grid-pager .nxe-pages .nxe-info{display:inline-block;margin-right:9px;opacity:.7}.nxe-data-grid .nxe-grid-pager .nxe-pages .nxe-page-indexes{display:inline-block}.nxe-data-grid .nxe-grid-pager .nxe-pages .nxe-navigate-button,.nxe-data-grid .nxe-grid-pager .nxe-pages .nxe-page,.nxe-data-grid .nxe-grid-pager .nxe-page-sizes .nxe-page-size{appearance:none;margin:0;background:transparent;color:inherit;font:inherit;text-align:center;outline:0;cursor:pointer}.nxe-data-grid .nxe-grid-pager .nxe-pages .nxe-navigate-button:focus-visible,.nxe-data-grid .nxe-grid-pager .nxe-pages .nxe-page:focus-visible,.nxe-data-grid .nxe-grid-pager .nxe-page-sizes .nxe-page-size:focus-visible{outline-width:2px;outline-style:solid;outline-color:var(--nxe-ui-focus-ring-color, #5c95c5);outline-offset:-2px}.nxe-data-grid .nxe-grid-pager .nxe-pages .nxe-page{display:inline-block;margin:0 1px 0 5px;padding:7px 9px 8px;border:1px solid transparent;border-radius:3px}.nxe-data-grid .nxe-grid-pager .nxe-page-sizes .nxe-page-size{display:inline-block;margin:0 1px 0 4px;padding:7px 9px 8px 10px;border:1px solid transparent;border-radius:3px}.nxe-data-grid .nxe-grid-pager .nxe-page-sizes .nxe-page-size.nxe-first-child,.nxe-data-grid .nxe-grid-pager .nxe-page-sizes .nxe-page-size:first-child{margin-left:1px}.nxe-data-grid .nxe-grid-pager .nxe-page-sizes .nxe-selection,.nxe-data-grid .nxe-grid-pager .nxe-pages .nxe-selection{cursor:inherit;text-shadow:none;color:var(--nxe-ui-text, #333);border-color:transparent;background-color:var(--nxe-ui-neutral-230, #d4d4d4)}.nxe-data-grid .nxe-grid-pager .nxe-pages .nxe-separator{display:inline-block;padding:0 8px}.nxe-data-grid .nxe-grid-pager .nxe-pages .nxe-navigate-button{position:relative;display:inline-block;box-sizing:content-box;width:9px;height:17px;padding:9px 13px;border:0;vertical-align:top}.nxe-data-grid .nxe-grid-pager .nxe-pages .nxe-navigate-button:before{content:\"\";position:absolute;display:block;box-sizing:border-box;top:50%;left:50%;width:21px;height:21px;margin:-10.5px 0 0 -10.5px;background-color:currentColor}@media(forced-colors:active){.nxe-data-grid .nxe-grid-pager .nxe-pages .nxe-navigate-button:before{forced-color-adjust:none;background-color:CanvasText}}.nxe-data-grid .nxe-grid-pager .nxe-pages .nxe-navigate-button.nxe-button-disable{opacity:.3;cursor:inherit}.nxe-data-grid .nxe-grid-pager .nxe-pages .nxe-prev-button:before{clip-path:polygon(5.75px 10.25px,13.75px 2.25px,15.25px 3.75px,8.75px 10.25px,15.25px 16.75px,13.75px 18.25px)}.nxe-data-grid .nxe-grid-pager .nxe-pages .nxe-next-button:before{clip-path:polygon(15.25px 10.25px,7.25px 2.25px,5.75px 3.75px,12.25px 10.25px,5.75px 16.75px,7.25px 18.25px)}.nxe-data-grid .nxe-grid-pager .nxe-pager-size{padding:2px 6px;border:1px solid var(--nxe-ui-border, #ddd);border-radius:3px;font:inherit}.nxe-data-grid .nxe-datagrid-header-panel{padding:.5rem .75rem;text-align:left;border-bottom:var(--nxe-ui-grid-line-width, 1px) solid var(--nxe-ui-border, #ddd)}.nxe-data-grid .nxe-datagrid-group-panel{display:flex;align-items:center;width:100%;min-height:36px;white-space:nowrap;font-size:var(--nxe-ui-font-size, 14px);touch-action:pinch-zoom}.nxe-data-grid .nxe-datagrid-group-panel .nxe-group-panel-message{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--nxe-ui-neutral-600, #757575);font-weight:400;padding:7px;border-top:1px solid transparent;border-bottom:1px solid transparent;line-height:1.2858}.nxe-data-grid .nxe-datagrid-group-panel .nxe-group-panel-item{display:inline-block;min-width:30px;white-space:nowrap;margin-right:10px;color:var(--nxe-ui-neutral-600, #757575);font-weight:400;border:1px solid var(--nxe-ui-border, #ddd);padding:7px}.nxe-data-grid .nxe-datagrid-group-panel .nxe-block-separator{position:relative;margin-right:10px;border-radius:3px;background-color:var(--nxe-ui-accent, #337ab7)}.nxe-data-grid .nxe-tree-icons{display:inline-block;float:left;white-space:nowrap;vertical-align:top;line-height:1}.nxe-data-grid .nxe-tree-expander,.nxe-data-grid .nxe-tree-expander-stub{display:inline-block;position:relative;width:14px;height:14px;vertical-align:top}.nxe-data-grid .nxe-tree-icons-select{position:relative;padding-right:23px}.nxe-data-grid .nxe-tree-icons .nxe-tree-checkbox{position:absolute;top:50%;right:7px;margin:-8px 0 0}.nxe-data-grid .nxe-header-row>.nxe-tree-select-all{position:relative;padding-left:44px}.nxe-data-grid .nxe-header-row>.nxe-tree-select-all>.nxe-grid-checkbox{position:absolute;left:21px;margin:1px 0 0}.nxe-data-grid .nxe-tree-expander{height:18px;margin-right:4px;color:var(--nxe-ui-neutral-450, #959595);cursor:pointer}.nxe-data-grid .nxe-tree-expander:before{content:\"\";position:absolute;left:0;top:8px;width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;border-top:4px solid currentColor}.nxe-data-grid .nxe-tree-expander.nxe-collapsed:before{left:1px;top:6px;border-top:4px solid transparent;border-right:0;border-bottom:4px solid transparent;border-left:4px solid currentColor}.nxe-data-grid .nxe-group-row{font-weight:700;color:var(--nxe-ui-grid-group-row-color, #222)}.nxe-data-grid .nxe-group-row>td{background-color:var(--nxe-ui-neutral-60, #f7f7f7)}.nxe-data-grid .nxe-group-row>td.nxe-group-cell{font-weight:700}.nxe-data-grid .nxe-group-row>td.nxe-datagrid-summary-item,.nxe-data-grid .nxe-group-footer-row>td.nxe-datagrid-summary-item{font-weight:700;color:var(--nxe-ui-grid-summary-color, rgba(51, 51, 51, .7))}.nxe-data-grid .nxe-datagrid-group-opened,.nxe-data-grid .nxe-datagrid-group-closed{display:block;position:relative;width:100%;height:18px;padding:0;color:var(--nxe-ui-neutral-450, #959595);cursor:pointer}.nxe-data-grid .nxe-datagrid-group-opened:before,.nxe-data-grid .nxe-datagrid-group-closed:before{content:\"\"!important;position:absolute;left:50%;right:auto;width:0;height:0}.nxe-data-grid .nxe-datagrid-group-opened:before{top:7px;margin-left:-4px;border-left:4px solid transparent;border-right:4px solid transparent;border-top:4px solid currentColor}.nxe-data-grid .nxe-datagrid-group-closed:before{top:5px;margin-left:-2px;border-top:4px solid transparent;border-bottom:4px solid transparent;border-left:4px solid currentColor}.nxe-data-grid .nxe-datagrid-total-footer{position:relative}.nxe-data-grid .nxe-datagrid-total-footer>tr>td,.nxe-data-grid tfoot.nxe-datagrid-total-footer td{border-top:1px solid var(--nxe-ui-border, #ddd);padding:6px 4px;font-weight:700;color:var(--nxe-ui-neutral-450, rgb(149, 149, 149))}.nxe-data-grid .nxe-datagrid-filter-row>td{padding:2px 4px}.nxe-data-grid .nxe-grid-filter-input{width:100%;box-sizing:border-box;padding:4px 6px;border:1px solid var(--nxe-ui-border, #ddd);border-radius:2px;font:inherit;color:var(--nxe-ui-text, #333);outline:none}.nxe-data-grid .nxe-grid-filter-input:focus{border-color:var(--nxe-ui-ink-strong, #30374c)}.nxe-data-grid .nxe-datagrid-filter-row select.nxe-grid-filter-input{appearance:none;background:var(--nxe-ui-surface, #fff) url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 4'%3E%3Cpath d='M0 0h8L4 4z' fill='%23333'/%3E%3C/svg%3E\") no-repeat right 12px center/8px 4px;padding-right:34px!important}.nxe-data-grid .nxe-command-select{width:40px;min-width:40px;max-width:40px;text-align:center;vertical-align:middle}.nxe-data-grid .nxe-grid-checkbox{display:block;width:16px;margin:0 auto;padding-bottom:2px;line-height:0;cursor:pointer}.nxe-data-grid .nxe-grid-checkbox.nxe-grid-checkbox-left{margin-left:0}.nxe-data-grid .nxe-grid-checkbox.nxe-grid-checkbox-right{margin-right:0}.nxe-data-grid .nxe-grid-checkbox:focus{outline:none}.nxe-data-grid .nxe-grid-checkbox:focus .nxe-grid-checkbox-box{border-color:var(--nxe-ui-accent, #337ab7)}.nxe-data-grid .nxe-grid-checkbox .nxe-grid-checkbox-box{display:block;position:relative;width:16px;height:16px;box-sizing:border-box;border:1px solid var(--nxe-ui-border, #ddd);border-radius:2px;background:var(--nxe-ui-surface, #fff)}.nxe-data-grid .nxe-grid-checkbox:not(.nxe-state-readonly):hover .nxe-grid-checkbox-box{border-color:var(--nxe-ui-checkbox-hover-border, #265a87)}.nxe-data-grid .nxe-grid-checkbox.nxe-checked .nxe-grid-checkbox-box:after{content:\"\";position:absolute;inset:-1px;background-color:var(--nxe-ui-accent, #337ab7);clip-path:polygon(6.75px 12px,2.5px 7.75px,3.5px 6.75px,6.75px 10px,12.75px 4px,13.75px 5px)}@media(forced-colors:active){.nxe-data-grid .nxe-grid-checkbox.nxe-checked .nxe-grid-checkbox-box:after{forced-color-adjust:none;background-color:CanvasText}}.nxe-data-grid .nxe-grid-checkbox.nxe-indeterminate .nxe-grid-checkbox-box:after{content:\"\";position:absolute;left:4px;top:4px;width:6px;height:6px;background-color:var(--nxe-ui-accent, #337ab7)}@media(forced-colors:active){.nxe-data-grid .nxe-grid-checkbox.nxe-indeterminate .nxe-grid-checkbox-box:after{forced-color-adjust:none;background-color:CanvasText}}.nxe-data-grid .nxe-grid-checkbox.nxe-state-readonly{cursor:default}.nxe-data-grid .nxe-grid-checkbox.nxe-state-readonly .nxe-grid-checkbox-box{border-color:var(--nxe-ui-neutral-80, #f4f4f4);background-color:var(--nxe-ui-surface, #fff)}@keyframes nxe-grid-spin{to{transform:rotate(360deg)}}:where(.nxe-data-grid .nxe-row)>td{padding:var(--nxe-ui-grid-cell-padding, 7px);vertical-align:var(--nxe-ui-grid-cell-vertical-align, middle)}:where(.nxe-data-grid .nxe-row)>td:first-child{padding-left:var(--nxe-ui-grid-edge-padding, var(--nxe-ui-grid-cell-padding, 7px))}:where(.nxe-data-grid .nxe-row)>td:last-child{padding-right:var(--nxe-ui-grid-edge-padding, var(--nxe-ui-grid-cell-padding, 7px))}:where(.nxe-data-grid) .nxe-header-row{background-color:var(--nxe-ui-grid-header-bg, transparent);color:var(--nxe-ui-grid-header-color, inherit)}.nxe-datagrid-rowsview .nxe-row{border-top:1px solid transparent;border-bottom:1px solid transparent}.nxe-datagrid-rowsview .nxe-datagrid-table .nxe-row.nxe-freespace-row,.nxe-datagrid-rowsview .nxe-datagrid-table .nxe-row.nxe-virtual-row{border-top:0;border-bottom:0}.nxe-datagrid-rowsview .nxe-datagrid-table .nxe-row.nxe-freespace-row>td,.nxe-datagrid-rowsview .nxe-datagrid-table .nxe-row.nxe-virtual-row>td{padding-top:0;padding-bottom:0}.nxe-datagrid-nowrap{white-space:nowrap}.nxe-datagrid-rowsview :where(tbody)>.nxe-row>td{overflow:hidden;text-overflow:ellipsis}.nxe-datagrid-rowsview .nxe-selection.nxe-row:not(.nxe-row-focused)>td,.nxe-datagrid-rowsview .nxe-selection.nxe-row:not(.nxe-row-focused):hover>td{background-color:var(--nxe-ui-neutral-150, #e6e6e6);color:var(--nxe-ui-text, #333)}.nxe-data-grid .nxe-data-row>td.nxe-editor-cell input:not([type=hidden]){text-align:inherit}.nxe-datagrid-content .nxe-datagrid-table .nxe-row .nxe-command-expand{padding:0;width:30px;min-width:30px;max-width:30px;-webkit-user-select:none;user-select:none}.nxe-datagrid-table .nxe-row .nxe-command-expand.nxe-datagrid-group-space{padding-top:var(--nxe-ui-grid-expand-offset, 7px)}.nxe-datagrid-content .nxe-datagrid-table .nxe-row>td.nxe-datagrid-group-space{vertical-align:top}.nxe-datagrid-content .nxe-datagrid-table .nxe-row .nxe-command-select{padding:0}.nxe-datagrid-rowsview .nxe-master-detail-row>.nxe-master-detail-cell{padding:20px}.nxe-datagrid-rowsview .nxe-master-detail-row .nxe-master-detail-cell:not(.nxe-row-lines){border-top:var(--nxe-ui-grid-line-width, 1px) solid var(--nxe-ui-grid-row-line-color, #ddd);border-bottom:var(--nxe-ui-grid-line-width, 1px) solid var(--nxe-ui-grid-row-line-color, #ddd)}.nxe-datagrid-rowsview .nxe-master-detail-row:not(.nxe-datagrid-edit-form) .nxe-master-detail-cell{background-color:var(--nxe-ui-neutral-50, #fafafa)}.nxe-grid-cell-editor{width:100%;box-sizing:border-box;border:1px solid var(--nxe-ui-accent, #337ab7);padding:2px 4px;font:inherit;background:var(--nxe-ui-surface, #fff);outline:none}.nxe-data-grid .nxe-command-edit{text-align:center;white-space:nowrap}.nxe-datagrid .nxe-link{text-decoration:underline;cursor:pointer;color:var(--nxe-ui-accent, #337ab7)}.nxe-datagrid-content .nxe-datagrid-table .nxe-row .nxe-command-edit .nxe-link{display:inline-block}.nxe-datagrid .nxe-datagrid-content .nxe-datagrid-table .nxe-row .nxe-command-edit .nxe-link:not(.nxe-link-icon){margin:0 2px;min-width:20px}.nxe-data-row.nxe-state-hover:not(.nxe-selection):not(.nxe-row-modified):not(.nxe-row-inserted):not(.nxe-row-removed):not(.nxe-edit-row)>td:not(.nxe-focused) .nxe-link,.nxe-datagrid-rowsview .nxe-selection.nxe-row:hover>td .nxe-link,.nxe-datagrid-rowsview .nxe-selection>td .nxe-link{color:inherit}.nxe-data-grid .nxe-header-row td.nxe-datagrid-drag-action,.nxe-data-grid .nxe-group-panel-item{touch-action:none}.nxe-data-grid .nxe-drag-source{opacity:.5}.nxe-data-grid .nxe-datagrid-filter-row .nxe-menu ul{list-style:none;margin:0;padding:0}.nxe-data-grid .nxe-datagrid-filter-row .nxe-editor-with-menu{display:flex;position:relative}.nxe-data-grid .nxe-datagrid-filter-row .nxe-editor-with-menu>.nxe-menu{position:absolute;top:0;left:0;bottom:0;height:auto;z-index:1;cursor:pointer}.nxe-data-grid .nxe-datagrid-filter-row .nxe-editor-with-menu>.nxe-menu ul,.nxe-data-grid .nxe-datagrid-filter-row .nxe-editor-with-menu>.nxe-menu li{height:100%}.nxe-data-grid .nxe-datagrid-filter-row .nxe-editor-with-menu>.nxe-menu .nxe-menu-item{display:flex;align-items:center;box-sizing:border-box;height:100%;padding:1px 8px 0}.nxe-data-grid .nxe-datagrid-filter-row .nxe-editor-with-menu>.nxe-menu .nxe-menu-item-content{display:flex;align-items:center;padding:0}.nxe-data-grid .nxe-datagrid-filter-row .nxe-editor-with-menu .nxe-grid-filter-input{flex:1 1 0;min-width:0}.nxe-data-grid .nxe-datagrid-filter-row .nxe-editor-with-menu>.nxe-grid-filter-input:not(.nxe-fr-second){padding-left:32px!important}.nxe-data-grid .nxe-datagrid-filter-row .nxe-editor-with-menu .nxe-fr-second{padding-left:4px!important;border-left:1px solid var(--nxe-ui-border, #ddd)}.nxe-data-grid .nxe-datagrid-filter-row .nxe-menu-item-content .nxe-icon,.nxe-fr-op-menu .nxe-menu-item .nxe-icon{display:block;flex:0 0 14px;width:14px;height:14px;margin:0;padding:0;font-size:0;line-height:0;color:var(--nxe-ui-neutral-500, #898989);background-color:currentColor;-webkit-mask:var(--nxe-ui-grid-fr-op-glyph, linear-gradient(transparent, transparent)) center/14px 14px no-repeat;mask:var(--nxe-ui-grid-fr-op-glyph, linear-gradient(transparent, transparent)) center/14px 14px no-repeat}@media(forced-colors:active){.nxe-data-grid .nxe-datagrid-filter-row .nxe-menu-item-content .nxe-icon,.nxe-fr-op-menu .nxe-menu-item .nxe-icon{forced-color-adjust:none;background-color:CanvasText}}.nxe-data-grid .nxe-datagrid-filter-row .nxe-menu-item-content .nxe-icon:not(.nxe-icon-filter-operation-default),.nxe-fr-op-menu .nxe-menu-item .nxe-icon:not(.nxe-icon-filter-operation-default){position:relative;top:-1px}.nxe-data-grid .nxe-datagrid-filter-row .nxe-menu-item-content .nxe-icon:before,.nxe-fr-op-menu .nxe-menu-item .nxe-icon:before{content:none}.nxe-icon-filter-operation-default{--nxe-ui-grid-fr-op-glyph: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cg fill='none' stroke='%23000' stroke-width='1.5'%3E%3Ccircle cx='5.5' cy='5.5' r='4'/%3E%3Cpath d='M8.5 8.5l4 4'/%3E%3C/g%3E%3C/svg%3E\")}.nxe-icon-filter-operation-contains{--nxe-ui-grid-fr-op-glyph: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cmask id='m'%3E%3Crect width='14' height='14' fill='%23fff'/%3E%3Cg fill='none' stroke='%23000'%3E%3Ccircle cx='7' cy='9.25' r='1.75'/%3E%3Cpath d='M5.25 3.5v8'/%3E%3C/g%3E%3C/mask%3E%3Crect x='4.5' y='2.5' width='5' height='9.5' mask='url(%23m)'/%3E%3Cg fill='none' stroke='%23000'%3E%3Ccircle cx='2.25' cy='9.25' r='1.75'/%3E%3Cpath d='M4 7v4.5'/%3E%3Cpath d='M13.4 7.9A1.75 1.75 0 1 0 13.4 10.6'/%3E%3C/g%3E%3C/svg%3E\")}.nxe-icon-filter-operation-not-contains{--nxe-ui-grid-fr-op-glyph: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cmask id='m'%3E%3Crect width='14' height='14' fill='%23fff'/%3E%3Cg fill='none' stroke='%23000'%3E%3Ccircle cx='2.25' cy='9.25' r='1.75'/%3E%3Cpath d='M4 7v4.5'/%3E%3Ccircle cx='7' cy='9.25' r='1.75'/%3E%3Cpath d='M5.25 3.5v8'/%3E%3Cpath d='M13.4 7.9A1.75 1.75 0 1 0 13.4 10.6'/%3E%3C/g%3E%3Crect y='8.75' width='14' height='1'/%3E%3C/mask%3E%3Crect x='0' y='2.5' width='14' height='9.5' mask='url(%23m)'/%3E%3C/svg%3E\")}.nxe-icon-filter-operation-starts-with{--nxe-ui-grid-fr-op-glyph: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cmask id='m'%3E%3Crect width='14' height='14' fill='%23fff'/%3E%3Cg fill='none' stroke='%23000'%3E%3Ccircle cx='2.25' cy='9.25' r='1.75'/%3E%3Cpath d='M4 7v4.5'/%3E%3Ccircle cx='7' cy='9.25' r='1.75'/%3E%3Cpath d='M5.25 3.5v8'/%3E%3C/g%3E%3C/mask%3E%3Crect x='0' y='2.5' width='9.5' height='9.5' mask='url(%23m)'/%3E%3Cg fill='none' stroke='%23000'%3E%3Cpath d='M13.4 7.9A1.75 1.75 0 1 0 13.4 10.6'/%3E%3C/g%3E%3C/svg%3E\")}.nxe-icon-filter-operation-ends-with{--nxe-ui-grid-fr-op-glyph: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cg fill='none' stroke='%23000'%3E%3Ccircle cx='2.25' cy='9.25' r='1.75'/%3E%3Cpath d='M4 7v4.5'/%3E%3C/g%3E%3Cmask id='m'%3E%3Crect width='14' height='14' fill='%23fff'/%3E%3Cg fill='none' stroke='%23000'%3E%3Ccircle cx='7' cy='9.25' r='1.75'/%3E%3Cpath d='M5.25 3.5v8'/%3E%3Cpath d='M13.4 7.9A1.75 1.75 0 1 0 13.4 10.6'/%3E%3C/g%3E%3C/mask%3E%3Crect x='4.5' y='2.5' width='9.5' height='9.5' mask='url(%23m)'/%3E%3C/svg%3E\")}.nxe-icon-filter-operation-equals{--nxe-ui-grid-fr-op-glyph: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath d='M2 5h10v1H2zM2 8h10v1H2z'/%3E%3C/svg%3E\")}.nxe-icon-filter-operation-not-equals{--nxe-ui-grid-fr-op-glyph: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath d='M2 5h10v1H2zM2 8h10v1H2zM9.3 2h1.4l-6 10H3.3z'/%3E%3C/svg%3E\")}.nxe-icon-filter-operation-less{--nxe-ui-grid-fr-op-glyph: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath d='M10.75 3.5 3.75 7l7 3.5' fill='none' stroke='%23000' stroke-width='1.2'/%3E%3C/svg%3E\")}.nxe-icon-filter-operation-greater{--nxe-ui-grid-fr-op-glyph: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath d='M3.25 3.5l7 3.5-7 3.5' fill='none' stroke='%23000' stroke-width='1.2'/%3E%3C/svg%3E\")}.nxe-icon-filter-operation-less-equal{--nxe-ui-grid-fr-op-glyph: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath d='M10.75 2 3.75 5.5l7 3.5M3.75 8.5l7 3.5' fill='none' stroke='%23000' stroke-width='1.2'/%3E%3C/svg%3E\")}.nxe-icon-filter-operation-greater-equal{--nxe-ui-grid-fr-op-glyph: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath d='M3.25 2l7 3.5-7 3.5M10.25 8.5l-7 3.5' fill='none' stroke='%23000' stroke-width='1.2'/%3E%3C/svg%3E\")}.nxe-icon-filter-operation-between{--nxe-ui-grid-fr-op-glyph: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath d='M2 5h1v5H2zM11 5h1v5h-1zM3 7h8v1H3z'/%3E%3C/svg%3E\")}.nxe-fr-op-menu{background:var(--nxe-ui-surface, #fff);border:1px solid var(--nxe-ui-border, #ddd);box-shadow:var(--nxe-ui-shadow-sm, 0 2px 6px rgba(0, 0, 0, .15));padding:4px 0;min-width:180px}.nxe-fr-op-menu .nxe-menu-item{display:flex;align-items:center;gap:5px;box-sizing:border-box;height:31px;padding:0 24px 0 5px;cursor:pointer;color:var(--nxe-ui-text, #333)}.nxe-fr-op-menu .nxe-menu-item:hover{background:var(--nxe-ui-neutral-100, #f0f0f0)}.nxe-fr-op-menu .nxe-menu-item.nxe-menu-item-selected{background:var(--nxe-ui-neutral-150, #e6e6e6)}.nxe-data-grid .nxe-col-resize-handle{position:absolute;right:-4px;top:0;bottom:0;width:8px;cursor:col-resize;z-index:2;touch-action:none}body.nxe-grid-resizing{cursor:col-resize;user-select:none;-webkit-user-select:none}.nxe-col-drop-indicator{position:fixed;width:2px;background:var(--nxe-ui-accent, #337ab7);z-index:10000;pointer-events:none}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
4128
6142
  }
4129
6143
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDataGridComponent, decorators: [{
4130
6144
  type: Component,
4131
- args: [{ selector: 'nxe-data-grid, nxe-tree-list', standalone: true, imports: [NgTemplateOutlet], host: { class: 'nxe-widget nxe-data-grid' }, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<div class=\"dx-widget dx-datagrid\" [class.dx-datagrid-borders]=\"showBorders()\">\r\n <div class=\"dx-gridbase-container\">\r\n @if (groupPanelVisible()) {\r\n <div class=\"dx-datagrid-header-panel\">\r\n <div class=\"dx-datagrid-group-panel\">\r\n @if (groupColumns().length === 0) {\r\n <span class=\"dx-group-panel-message\">Drag a column header here to group by that column</span>\r\n } @else {\r\n @for (col of groupColumns(); track col.dataField(); let li = $index) {\r\n <!-- dx chip DOM: caption + sort indicator + header-filter button (bright audit) -->\r\n <div\r\n class=\"dx-group-panel-item dx-datagrid-action dx-datagrid-drag-action\"\r\n tabindex=\"0\"\r\n (pointerdown)=\"onChipPointerDown(col, li, $event)\"\r\n (click)=\"onChipClick(col)\">{{ col.caption() }}<div class=\"dx-column-indicators\" role=\"presentation\"><span\r\n class=\"dx-sort\"\r\n [class.dx-sort-up]=\"groupDir(col) === 'asc'\"\r\n [class.dx-sort-down]=\"groupDir(col) === 'desc'\"></span>@if (headerFilterVisible(col)) {<span\r\n class=\"nxe-grid-hf-icon\"\r\n [class.nxe-hf-active]=\"hasHeaderFilter(col)\"\r\n (pointerdown)=\"$event.stopPropagation()\"\r\n (click)=\"openHeaderFilter(col, $event)\"></span>}</div></div>\r\n }\r\n }\r\n </div>\r\n </div>\r\n }\r\n <div\r\n class=\"dx-datagrid-rowsview dx-datagrid-after-headers\"\r\n [class.dx-datagrid-nowrap]=\"!wordWrapEnabled()\">\r\n <div class=\"dx-datagrid-content\">\r\n <table class=\"dx-datagrid-table dx-datagrid-table-fixed\" [style.width]=\"tableWidthStyle()\">\r\n <colgroup>\r\n @for (i of indents(groupLevelCount()); track i) {\r\n <col style=\"width: 30px\" />\r\n }\r\n @if (masterDetailEnabled()) {\r\n <col style=\"width: 30px\" />\r\n }\r\n @if (showCheckboxes()) {\r\n <col style=\"width: 40px\" />\r\n }\r\n @for (col of visibleColumns(); track $index; let ci = $index) {\r\n <col [style.width]=\"effectiveColWidth(col, ci)\" />\r\n }\r\n @if (rowEditCommandVisible()) {\r\n <col style=\"width: 100px\" />\r\n }\r\n </colgroup>\r\n <thead class=\"dx-datagrid-headers\">\r\n <!-- Bands add header rows: a band cell spans its visible leaves with\r\n colspan, and a leaf above the bottom row spans down with rowspan.\r\n That is dx's own banded markup, and it keeps the LAST header row\r\n all-leaves - which the auto-width, resize and drag code relies on. -->\r\n @for (hrow of headerRows(); track $index; let hi = $index) {\r\n <tr class=\"dx-row dx-header-row\" [class.dx-column-lines]=\"showColumnLines()\">\r\n @if (hi === 0) {\r\n @for (i of indents(groupLevelCount()); track i) {\r\n <td\r\n class=\"dx-command-expand dx-datagrid-group-space dx-cell-focus-disabled\"\r\n [attr.rowspan]=\"headerDepth() > 1 ? headerDepth() : null\"></td>\r\n }\r\n @if (masterDetailEnabled()) {\r\n <td\r\n class=\"dx-command-expand\"\r\n [attr.rowspan]=\"headerDepth() > 1 ? headerDepth() : null\"></td>\r\n }\r\n @if (showCheckboxes()) {\r\n <td\r\n class=\"dx-command-select\"\r\n [attr.rowspan]=\"headerDepth() > 1 ? headerDepth() : null\">\r\n @if (selectionMode() === 'multiple') {\r\n <span\r\n class=\"dx-select-checkbox nxe-grid-checkbox\"\r\n [class.nxe-checked]=\"allSelected()\"\r\n [class.nxe-indeterminate]=\"someSelected() && !allSelected()\"\r\n (click)=\"toggleSelectAll($event)\">\r\n <span class=\"nxe-grid-checkbox-box\"></span>\r\n </span>\r\n }\r\n </td>\r\n }\r\n }\r\n @for (cell of hrow; track $index) {\r\n @if (cell.isBand) {\r\n <td\r\n class=\"nxe-band-cell\"\r\n [class]=\"cell.column.cssClass()\"\r\n [attr.colspan]=\"cell.colspan > 1 ? cell.colspan : null\"\r\n [style.text-align]=\"cell.column.alignment() ?? 'center'\">\r\n <div class=\"dx-datagrid-text-content\">{{ cell.column.caption() }}</div>\r\n </td>\r\n } @else {\r\n <td\r\n [class]=\"cell.column.cssClass()\"\r\n [attr.aria-colindex]=\"leadCellCount() + cell.leafIndex + 1\"\r\n [attr.rowspan]=\"cell.rowspan > 1 ? cell.rowspan : null\"\r\n [style.text-align]=\"align(cell.column)\"\r\n [class.dx-cell-focus-disabled]=\"!cell.column.allowSorting()\"\r\n [class.dx-datagrid-drag-action]=\"headerDraggable(cell.column)\"\r\n (pointerdown)=\"onHeaderPointerDown(cell.column, cell.leafIndex, $event)\"\r\n (click)=\"onHeaderClick(cell.column)\">\r\n <!-- Both indicator classes are load-bearing, not decorative: they are what\r\n gate dx's caption max-widths (`.dx-header-row .dx-sort-indicator\r\n {max-width:calc(100% - 17px)}` and the .dx-header-filter-indicator\r\n family), i.e. the 17px-per-indicator budget that stops a long\r\n caption from colliding with the arrow and the funnel. -->\r\n <div\r\n class=\"dx-datagrid-text-content\"\r\n [class.dx-sort-indicator]=\"!!sortDir(cell.column)\"\r\n [class.dx-header-filter-indicator]=\"headerFilterVisible(cell.column)\">\r\n {{ cell.column.caption() }}\r\n </div>\r\n <!-- dx wraps the arrow and the funnel in one indicator block, and the\r\n group-panel chip below already did. Matching it here means the two\r\n places share a single set of metrics instead of drifting. -->\r\n @if (sortDir(cell.column) || headerFilterVisible(cell.column)) {\r\n <div class=\"dx-column-indicators\" role=\"presentation\">\r\n @if (sortDir(cell.column); as dir) {\r\n <span\r\n class=\"dx-sort\"\r\n [class.dx-sort-up]=\"dir === 'asc'\"\r\n [class.dx-sort-down]=\"dir === 'desc'\"></span>\r\n }\r\n @if (headerFilterVisible(cell.column)) {\r\n <span\r\n class=\"nxe-grid-hf-icon\"\r\n [class.nxe-hf-active]=\"hasHeaderFilter(cell.column)\"\r\n (click)=\"openHeaderFilter(cell.column, $event)\"></span>\r\n }\r\n </div>\r\n }\r\n @if (resizeHandleVisible(cell.column, cell.leafIndex)) {\r\n <span\r\n class=\"nxe-col-resize-handle\"\r\n (pointerdown)=\"startColumnResize(cell.column, cell.leafIndex, $event)\"></span>\r\n }\r\n </td>\r\n }\r\n }\r\n @if (hi === 0 && rowEditCommandVisible()) {\r\n <td\r\n class=\"dx-command-edit\"\r\n [attr.rowspan]=\"headerDepth() > 1 ? headerDepth() : null\"></td>\r\n }\r\n </tr>\r\n }\r\n @if (filterRowVisible()) {\r\n <tr class=\"dx-row dx-datagrid-filter-row\">\r\n @for (i of indents(groupLevelCount()); track i) {\r\n <td class=\"dx-command-expand dx-datagrid-group-space\"></td>\r\n }\r\n @if (masterDetailEnabled()) {\r\n <td class=\"dx-command-expand\"></td>\r\n }\r\n @if (showCheckboxes()) {\r\n <td class=\"dx-command-select\"></td>\r\n }\r\n @for (col of visibleColumns(); track $index) {\r\n <!-- dx filter cell: td.dx-editor-cell > .dx-editor-with-menu >\r\n [.dx-menu op chooser][typed editor] (dx-compat.css contract) -->\r\n <td class=\"dx-editor-cell\" [class]=\"col.cssClass()\">\r\n @if (col.allowFiltering()) {\r\n @switch (frEditorKind(col)) {\r\n @case ('lookup') {\r\n <select\r\n class=\"dx-texteditor-input nxe-grid-filter-input\"\r\n [value]=\"frText(col)\"\r\n (click)=\"$event.stopPropagation()\"\r\n (change)=\"setFilterRowText(col, $any($event.target).value)\">\r\n <option value=\"\">(All)</option>\r\n @for (item of col.lookup()!.dataSource; track $index) {\r\n <option [value]=\"item[col.lookup()!.valueExpr ?? 'lovCode']\">\r\n {{ item[col.lookup()!.displayExpr ?? 'lovName'] }}\r\n </option>\r\n }\r\n </select>\r\n }\r\n @case ('boolean') {\r\n <select\r\n class=\"dx-texteditor-input nxe-grid-filter-input\"\r\n [value]=\"frText(col)\"\r\n (click)=\"$event.stopPropagation()\"\r\n (change)=\"setFilterRowText(col, $any($event.target).value)\">\r\n <option value=\"\">(All)</option>\r\n <option value=\"true\">true</option>\r\n <option value=\"false\">false</option>\r\n </select>\r\n }\r\n @default {\r\n <div class=\"dx-editor-with-menu\">\r\n <div class=\"dx-menu dx-menu-base\">\r\n <ul class=\"dx-menu-horizontal\">\r\n <li>\r\n <div\r\n class=\"dx-menu-item\"\r\n title=\"Filter operation\"\r\n (click)=\"openFilterOpMenu(col, $event)\">\r\n <div class=\"dx-menu-item-content\">\r\n <span class=\"dx-icon\" [class]=\"frOpIconClass(col)\"></span>\r\n </div>\r\n </div>\r\n </li>\r\n </ul>\r\n </div>\r\n <input\r\n class=\"dx-texteditor-input nxe-grid-filter-input\"\r\n [type]=\"frEditorKind(col) === 'date' ? 'date' : 'text'\"\r\n [attr.inputmode]=\"frEditorKind(col) === 'number' ? 'decimal' : null\"\r\n [value]=\"frText(col)\"\r\n (input)=\"setFilterRowText(col, $any($event.target).value)\"\r\n (click)=\"$event.stopPropagation()\" />\r\n @if (frOp(col) === 'between') {\r\n <input\r\n class=\"dx-texteditor-input nxe-grid-filter-input nxe-fr-second\"\r\n [type]=\"frEditorKind(col) === 'date' ? 'date' : 'text'\"\r\n [attr.inputmode]=\"frEditorKind(col) === 'number' ? 'decimal' : null\"\r\n [value]=\"frText2(col)\"\r\n (input)=\"setFilterRowText(col, $any($event.target).value, true)\"\r\n (click)=\"$event.stopPropagation()\" />\r\n }\r\n </div>\r\n }\r\n }\r\n }\r\n </td>\r\n }\r\n @if (rowEditCommandVisible()) {\r\n <td class=\"dx-command-edit\"></td>\r\n }\r\n </tr>\r\n }\r\n </thead>\r\n <tbody>\r\n @for (item of renderRows(); track $index; let ri = $index) {\r\n @if (item.kind === 'group') {\r\n <!-- dx group-row anatomy (bright audit): [indent cells][expand chevron\r\n cell][dx-group-cell colspan][alignByColumn summary cells] -->\r\n <tr\r\n class=\"dx-row dx-group-row\"\r\n [class.dx-column-lines]=\"showColumnLines()\"\r\n [attr.aria-roledescription]=\"item.collapsed ? 'Collapsed row' : 'Expanded row'\">\r\n @for (i of indents(item.level); track i) {\r\n <td class=\"dx-command-expand dx-datagrid-group-space\"></td>\r\n }\r\n <td\r\n class=\"dx-command-expand dx-datagrid-group-space dx-datagrid-expand dx-selection-disabled\"\r\n [attr.aria-label]=\"item.collapsed ? 'Expand' : 'Collapse'\"\r\n (click)=\"toggleGroup(item.groupKey)\">\r\n <div\r\n [class.dx-datagrid-group-opened]=\"!item.collapsed\"\r\n [class.dx-datagrid-group-closed]=\"item.collapsed\"></div>\r\n </td>\r\n <td class=\"dx-group-cell\" [attr.colspan]=\"groupCellColspan(item)\">\r\n @if (templateFor(item.column.groupCellTemplate()); as gtpl) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"gtpl\"\r\n [ngTemplateOutletContext]=\"groupCellContext(item)\" />\r\n } @else {\r\n {{ item.column.caption() }}: {{ item.text }}{{ item.captionSuffix }}\r\n }\r\n </td>\r\n @for (col of groupRowTrailing(item); track col.dataField()) {\r\n <td class=\"dx-datagrid-summary-item\" [style.text-align]=\"align(col)\">\r\n {{ item.alignedSums.get(col.dataField()) || '' }}\r\n </td>\r\n }\r\n @if (groupRowTrailing(item).length && rowEditCommandVisible()) {\r\n <td class=\"dx-command-edit\"></td>\r\n }\r\n </tr>\r\n } @else if (item.kind === 'groupFooter') {\r\n <tr\r\n class=\"dx-row dx-group-footer-row nxe-group-footer-row\"\r\n [class]=\"'nxe-group-footer-level-' + item.level\">\r\n @for (i of indents(groupLevelCount()); track i) {\r\n <td class=\"dx-datagrid-group-space\"></td>\r\n }\r\n @if (masterDetailEnabled()) {\r\n <td class=\"dx-command-expand\"></td>\r\n }\r\n @if (showCheckboxes()) {\r\n <td class=\"dx-command-select\"></td>\r\n }\r\n @for (col of visibleColumns(); track col.dataField()) {\r\n <td class=\"dx-datagrid-summary-item\" [style.text-align]=\"align(col)\">\r\n {{ item.sums.get(col.dataField()) || '' }}\r\n </td>\r\n }\r\n @if (rowEditCommandVisible()) {\r\n <td class=\"dx-command-edit\"></td>\r\n }\r\n </tr>\r\n } @else {\r\n <tr\r\n class=\"dx-row dx-data-row\"\r\n [class]=\"rowClassFor(item.row)\"\r\n [attr.aria-rowindex]=\"item.rowIndex + 1\"\r\n [class.dx-row-lines]=\"showRowLines()\"\r\n [class.dx-column-lines]=\"showColumnLines()\"\r\n [class.dx-row-alt]=\"rowAlternationEnabled() && item.rowIndex % 2 === 1\"\r\n [class.dx-selection]=\"isSelected(item.row)\"\r\n [class.dx-state-hover]=\"isHovered(item.rowIndex)\"\r\n [class.dx-edit-row]=\"isRowEditing(item.row)\"\r\n (mouseenter)=\"onRowEnter(item.rowIndex)\"\r\n (mouseleave)=\"onRowLeave()\"\r\n (click)=\"onDataRowClick(item.row, item.rowIndex)\">\r\n @for (i of indents(groupLevelCount()); track i) {\r\n <td class=\"dx-datagrid-group-space\"></td>\r\n }\r\n @if (masterDetailEnabled()) {\r\n <td class=\"dx-command-expand dx-datagrid-expand\">\r\n <span\r\n class=\"nxe-detail-expander\"\r\n [class.nxe-expanded]=\"isDetailExpanded(item.row)\"\r\n (click)=\"toggleDetail(item.row, $event)\"></span>\r\n </td>\r\n }\r\n @if (showCheckboxes()) {\r\n <td class=\"dx-command-select\">\r\n <span\r\n class=\"dx-select-checkbox nxe-grid-checkbox\"\r\n [class.nxe-checked]=\"isSelected(item.row)\"\r\n (click)=\"onCheckboxClick(item.row, $event)\">\r\n <span class=\"nxe-grid-checkbox-box\"></span>\r\n </span>\r\n </td>\r\n }\r\n @for (col of visibleColumns(); track $index; let ci = $index) {\r\n @if (!isMergeSuppressed(ri, col)) {\r\n <td\r\n [class]=\"col.cssClass()\"\r\n [attr.aria-colindex]=\"leadCellCount() + ci + 1\"\r\n [attr.aria-roledescription]=\"\r\n editableCell(col) && (cellEditingEnabled() || rowEditingEnabled())\r\n ? 'Editable'\r\n : null\r\n \"\r\n [style.text-align]=\"align(col)\"\r\n [style.padding-left.px]=\"treeMode && ci === 0 ? (item.level || 0) * 18 + 4 : null\"\r\n [attr.rowspan]=\"mergeSpan(ri, col)\"\r\n [class.dx-editor-cell]=\"isEditingCell(item.row, col) || (isRowEditing(item.row) && editableCell(col))\"\r\n (click)=\"onCellClick(item.row, col, $event)\"\r\n (focusout)=\"onEditCellFocusOut(item.row, col, $event)\">\r\n @if (treeMode && ci === 0) {\r\n @if (item.hasChildren) {\r\n <span\r\n class=\"nxe-tree-expander\"\r\n [class.nxe-collapsed]=\"!item.expanded\"\r\n (click)=\"toggleTreeNode(item.nodeKey, $event)\"></span>\r\n } @else {\r\n <span class=\"nxe-tree-expander-stub\"></span>\r\n }\r\n }\r\n @if (isEditingCell(item.row, col) || (isRowEditing(item.row) && editableCell(col))) {\r\n @if (templateFor(col.editCellTemplate()); as etpl) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"etpl\"\r\n [ngTemplateOutletContext]=\"editCellContext(item.row, item.rowIndex, ci, col)\" />\r\n } @else if (isEditingCell(item.row, col)) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"cellEditor\"\r\n [ngTemplateOutletContext]=\"{ row: item.row, col: col, mode: 'cell' }\" />\r\n } @else {\r\n <ng-container\r\n [ngTemplateOutlet]=\"cellEditor\"\r\n [ngTemplateOutletContext]=\"{ row: item.row, col: col, mode: 'row' }\" />\r\n }\r\n } @else if (templateFor(col.cellTemplate()); as tpl) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"tpl\"\r\n [ngTemplateOutletContext]=\"cellContext(item.row, item.rowIndex, ci, col)\" />\r\n } @else if (isBooleanCell(col, item.row)) {\r\n <!-- dx parity: boolean cells render a read-only dx-style checkbox, not \"true\"/\"false\" -->\r\n <span\r\n class=\"nxe-grid-checkbox nxe-grid-checkbox-readonly\"\r\n [class.nxe-checked]=\"getRaw(item.row, col.dataField()) === true\">\r\n <span class=\"nxe-grid-checkbox-box\"></span>\r\n </span>\r\n } @else {\r\n {{ cellText(col, item.row) }}\r\n }\r\n </td>\r\n }\r\n }\r\n @if (rowEditCommandVisible()) {\r\n <td class=\"dx-command-edit dx-datagrid-drag-action\">\r\n @if (isRowEditing(item.row)) {\r\n <a class=\"dx-link dx-link-save\" (click)=\"saveRowEdit(item.row, $event)\">Save</a>\r\n <a class=\"dx-link dx-link-cancel\" (click)=\"cancelRowEdit($event)\">Cancel</a>\r\n } @else {\r\n <a class=\"dx-link dx-link-edit\" (click)=\"startRowEdit(item.row, $event)\">Edit</a>\r\n }\r\n </td>\r\n }\r\n </tr>\r\n @if (masterDetailEnabled() && isDetailExpanded(item.row)) {\r\n <tr class=\"dx-row dx-master-detail-row\">\r\n <td [attr.colspan]=\"allColCount()\">\r\n @if (detailTemplate(); as tpl) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"tpl\"\r\n [ngTemplateOutletContext]=\"detailContext(item.row, item.rowIndex)\" />\r\n }\r\n </td>\r\n </tr>\r\n }\r\n }\r\n }\r\n <tr class=\"dx-row dx-freespace-row\">\r\n <td [attr.colspan]=\"allColCount()\"></td>\r\n </tr>\r\n </tbody>\r\n @if (hasTotalFooter()) {\r\n <tfoot class=\"dx-datagrid-total-footer\">\r\n <tr class=\"dx-row\">\r\n @for (i of indents(groupLevelCount()); track i) {\r\n <td class=\"dx-datagrid-group-space\"></td>\r\n }\r\n @if (masterDetailEnabled()) {\r\n <td class=\"dx-command-expand\"></td>\r\n }\r\n @if (showCheckboxes()) {\r\n <td class=\"dx-command-select\"></td>\r\n }\r\n @for (col of visibleColumns(); track col.dataField()) {\r\n <td class=\"dx-datagrid-summary-item\" [style.text-align]=\"align(col)\">\r\n {{ totalSums().get(col.dataField()) || '' }}\r\n </td>\r\n }\r\n @if (rowEditCommandVisible()) {\r\n <td class=\"dx-command-edit\"></td>\r\n }\r\n </tr>\r\n </tfoot>\r\n }\r\n </table>\r\n </div>\r\n\r\n @if (displayRows().length === 0 && !loading()) {\r\n <div class=\"dx-datagrid-nodata\">{{ noDataText() }}</div>\r\n }\r\n @if (loading()) {\r\n <div class=\"dx-datagrid-loadpanel\"><span class=\"nxe-grid-spinner\"></span></div>\r\n }\r\n </div>\r\n\r\n @if (pagerVisible()) {\r\n <div class=\"dx-datagrid-pager dx-pager nxe-grid-pager\">\r\n @if (pagerCmp()?.showPageSizeSelector() && pageSizes().length > 1) {\r\n <select class=\"nxe-pager-size\" (change)=\"onPageSizeSelect($event)\">\r\n @for (s of pageSizes(); track s) {\r\n <option [value]=\"s\" [selected]=\"s === effectivePageSize()\">{{ s }}</option>\r\n }\r\n </select>\r\n }\r\n <div class=\"nxe-pager-pages\">\r\n @if (pagerCmp()?.showNavigationButtons()) {\r\n <button type=\"button\" class=\"nxe-pager-nav\" [disabled]=\"pageIndex() === 0\" (click)=\"prevPage()\">\u2039</button>\r\n }\r\n @for (p of pageNumbers(); track p) {\r\n <button\r\n type=\"button\"\r\n class=\"nxe-pager-page\"\r\n [class.nxe-active]=\"p === pageIndex()\"\r\n (click)=\"goToPage(p)\">\r\n {{ p + 1 }}\r\n </button>\r\n }\r\n @if (pagerCmp()?.showNavigationButtons()) {\r\n <button type=\"button\" class=\"nxe-pager-nav\" [disabled]=\"pageIndex() >= pageCount() - 1\" (click)=\"nextPage()\">\u203A</button>\r\n }\r\n </div>\r\n @if (pagerCmp()?.showInfo()) {\r\n <span class=\"nxe-pager-info\">{{ pageInfo() }}</span>\r\n }\r\n </div>\r\n }\r\n </div>\r\n</div>\r\n\r\n<!-- Inline cell/row editor (dxo-editing, orbit addition). `mode: 'cell'` commits on blur/Enter;\r\n `mode: 'row'` buffers on input until the command column's Save. -->\r\n<ng-template #cellEditor let-row=\"row\" let-col=\"col\" let-mode=\"mode\">\r\n @if (col.lookup(); as lookup) {\r\n <select\r\n class=\"dx-texteditor-input nxe-grid-cell-editor\"\r\n [value]=\"editorInputValue(row, col)\"\r\n (click)=\"$event.stopPropagation()\"\r\n (change)=\"\r\n mode === 'cell'\r\n ? commitCellEdit(row, col, $any($event.target).value)\r\n : bufferRowEdit(col, $any($event.target).value)\r\n \">\r\n <option value=\"\"></option>\r\n @for (item of lookup.dataSource; track $index) {\r\n <option [value]=\"item[lookup.valueExpr ?? 'lovCode']\">\r\n {{ item[lookup.displayExpr ?? 'lovName'] }}\r\n </option>\r\n }\r\n </select>\r\n } @else if (col.dataType() === 'boolean') {\r\n <input\r\n type=\"checkbox\"\r\n class=\"nxe-grid-cell-editor-check\"\r\n [checked]=\"!!editorInputValue(row, col) && editorInputValue(row, col) !== 'false'\"\r\n (click)=\"$event.stopPropagation()\"\r\n (change)=\"\r\n mode === 'cell'\r\n ? commitCellEdit(row, col, $any($event.target).checked)\r\n : bufferRowEdit(col, $any($event.target).checked)\r\n \" />\r\n } @else {\r\n <input\r\n type=\"text\"\r\n class=\"dx-texteditor-input nxe-grid-cell-editor\"\r\n [value]=\"editorInputValue(row, col)\"\r\n (click)=\"$event.stopPropagation()\"\r\n (keydown.enter)=\"mode === 'cell' && commitCellEdit(row, col, $any($event.target).value)\"\r\n (keydown.escape)=\"mode === 'cell' && cancelCellEdit()\"\r\n (blur)=\"mode === 'cell' && commitCellEdit(row, col, $any($event.target).value)\"\r\n (input)=\"mode === 'row' && bufferRowEdit(col, $any($event.target).value)\" />\r\n }\r\n</ng-template>\r\n\r\n<!-- The header-filter popup (#hfPanel) moved to nxe-header-filter-panel.component.html \u2014\r\n openHeaderFilter attaches it through openNxeHeaderFilterOverlay (pivot parity Phase 3a). -->\r\n\r\n<!-- Filter-row operation-chooser dropdown (dx filter menu) -->\r\n<ng-template #frOpMenu>\r\n <div class=\"nxe-fr-op-menu dx-datagrid\">\r\n @for (item of frMenuOps(); track item.op) {\r\n <div\r\n class=\"dx-menu-item\"\r\n [class.dx-menu-item-selected]=\"frMenuColumn() && frOp(frMenuColumn()!) === item.op\"\r\n (click)=\"selectFilterOp(item.op)\">\r\n <span class=\"dx-icon dx-icon-filter-operation-{{ opIconSlug(item.op) }}\"></span>\r\n <span class=\"dx-menu-item-text\">{{ item.label }}</span>\r\n </div>\r\n }\r\n <div class=\"dx-menu-item nxe-fr-reset\" (click)=\"resetFilterRow()\">\r\n <span class=\"dx-icon\"></span>\r\n <span class=\"dx-menu-item-text\">Reset</span>\r\n </div>\r\n </div>\r\n</ng-template>\r\n\r\n", styles: ["@charset \"UTF-8\";.nxe-data-grid{display:block}.nxe-data-grid .dx-datagrid{height:100%;position:relative}.nxe-data-grid .dx-gridbase-container{display:flex;flex-direction:column;height:100%}.nxe-data-grid .dx-datagrid-rowsview{position:relative;overflow:auto;flex:1 1 auto;min-height:0}.nxe-data-grid .dx-datagrid-content{display:block;min-height:100%}.nxe-data-grid table.dx-datagrid-table{display:table;width:100%;table-layout:fixed;border-collapse:collapse;margin:0}.nxe-data-grid .dx-datagrid-content .dx-datagrid-table{max-width:none}.nxe-data-grid .dx-datagrid-headers{display:table-header-group;position:sticky;top:0;z-index:1;background:#fff;border-bottom:0}.nxe-data-grid tbody{display:table-row-group}.nxe-data-grid .dx-row{display:table-row}.nxe-data-grid .dx-datagrid-headers .dx-header-row>td{border-bottom:1px solid #e2e8f0}.nxe-data-grid .dx-datagrid-nodata{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#333;font-size:17px;opacity:.7;pointer-events:none}.nxe-data-grid .dx-datagrid-loadpanel{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:#ffffff80;z-index:2}.nxe-data-grid .nxe-grid-spinner{width:32px;height:32px;border:3px solid rgba(48,55,76,.2);border-top-color:#30374c;border-radius:50%;animation:nxe-grid-spin .8s linear infinite}.nxe-data-grid .dx-column-indicators{display:inline-block;vertical-align:top;white-space:nowrap}.nxe-data-grid .dx-sort{display:inline-block;position:relative;width:14px;height:14px;vertical-align:middle}.nxe-data-grid .dx-sort:after{content:\"\";position:absolute;left:50%;top:50%;width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent}.nxe-data-grid .dx-sort.dx-sort-up:after{border-bottom:5px solid #757575;transform:translate(-50%,-50%)}.nxe-data-grid .dx-sort.dx-sort-down:after{border-top:5px solid #757575;transform:translate(-50%,-50%)}.nxe-data-grid .dx-header-row>td{cursor:default;-webkit-user-select:none;user-select:none;height:27px}.nxe-data-grid .dx-header-row>td{position:relative}.nxe-data-grid .nxe-grid-hf-icon{display:inline-block;position:relative;width:14px;height:14px;cursor:pointer;vertical-align:middle;opacity:.5}.nxe-data-grid .nxe-grid-hf-icon:before{content:\"\";position:absolute;inset:0;background-color:#757575;clip-path:polygon(0 0,100% 0,58% 50%,58% 100%,42% 85%,42% 50%)}.nxe-data-grid .nxe-grid-hf-icon:after{content:\"\";position:absolute;inset:0;margin:-7px}.nxe-data-grid .nxe-grid-hf-icon:hover{opacity:.75}.nxe-data-grid .nxe-grid-hf-icon.nxe-hf-active{opacity:1}.nxe-data-grid .nxe-grid-pager{display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:8px 12px;border-top:1px solid #ddd}.nxe-data-grid .nxe-pager-size{padding:2px 6px;border:1px solid #ddd;border-radius:3px;font:inherit}.nxe-data-grid .nxe-pager-pages{display:flex;align-items:center;gap:4px;flex-wrap:wrap}.nxe-data-grid .nxe-pager-page,.nxe-data-grid .nxe-pager-nav{min-width:24px;padding:3px 7px 4px;border:1px solid transparent;border-radius:3px;background:transparent;color:#333;cursor:pointer;font:inherit;line-height:1}.nxe-data-grid .nxe-pager-page:hover:not(:disabled),.nxe-data-grid .nxe-pager-nav:hover:not(:disabled){background:#f4f4f4}.nxe-data-grid .nxe-pager-page:disabled,.nxe-data-grid .nxe-pager-nav:disabled{opacity:.4;cursor:default}.nxe-data-grid .nxe-pager-page.nxe-active,.nxe-data-grid .nxe-pager-nav.nxe-active{background:#d4d4d4;border-color:transparent}.nxe-data-grid .nxe-pager-info{color:#757575;margin-left:auto}.nxe-data-grid .dx-datagrid-header-panel{padding:.5rem .75rem}.nxe-data-grid .dx-datagrid-group-panel{align-items:center;min-height:36px}.nxe-data-grid .nxe-tree-expander,.nxe-data-grid .nxe-tree-expander-stub{display:inline-block;width:16px;height:16px;flex:0 0 16px;vertical-align:middle;margin-right:2px}.nxe-data-grid .nxe-tree-expander{position:relative;cursor:pointer}.nxe-data-grid .nxe-tree-expander:before{content:\"\";position:absolute;left:4px;top:4px;width:6px;height:6px;border-right:1.5px solid #555;border-bottom:1.5px solid #555;transform:rotate(45deg)}.nxe-data-grid .nxe-tree-expander.nxe-collapsed:before{transform:rotate(-45deg)}.nxe-data-grid .dx-group-row>td{padding:4px 8px;background-color:#f7f7f7}.nxe-data-grid .dx-group-row>td.dx-group-cell{font-weight:700}.nxe-data-grid .dx-group-row>td.dx-datagrid-expand{cursor:pointer}.nxe-data-grid .dx-group-row>td.dx-datagrid-summary-item{font-weight:400;color:#333}.nxe-data-grid .dx-datagrid-group-opened,.nxe-data-grid .dx-datagrid-group-closed{display:inline-block;position:relative;width:14px;height:14px;cursor:pointer}.nxe-data-grid .dx-datagrid-group-opened:before,.nxe-data-grid .dx-datagrid-group-closed:before{content:\"\"!important;position:absolute;left:3px;right:auto;top:2px;width:6px;height:6px;border-right:1.5px solid #555;border-bottom:1.5px solid #555;transform:rotate(45deg)}.nxe-data-grid .dx-datagrid-group-closed:before{transform:rotate(-45deg)}.nxe-data-grid .dx-datagrid-total-footer>tr>td,.nxe-data-grid tfoot.dx-datagrid-total-footer td{border-top:1px solid #ddd;padding:6px 4px;font-weight:700;color:#959595}.nxe-data-grid .dx-datagrid-filter-row>td{padding:2px 4px}.nxe-data-grid .nxe-grid-filter-input{width:100%;box-sizing:border-box;padding:4px 6px;border:1px solid #ddd;border-radius:2px;font:inherit;outline:none}.nxe-data-grid .nxe-grid-filter-input:focus{border-color:#30374c}.nxe-data-grid .dx-command-select{width:40px;min-width:40px;max-width:40px;text-align:center;vertical-align:middle}.nxe-data-grid .nxe-grid-checkbox{display:inline-block;cursor:pointer;line-height:0}.nxe-data-grid .nxe-grid-checkbox .nxe-grid-checkbox-box{display:inline-block;position:relative;width:16px;height:16px;box-sizing:border-box;border:1px solid #ddd;border-radius:2px;background:#fff;vertical-align:middle}.nxe-data-grid .nxe-grid-checkbox.nxe-checked .nxe-grid-checkbox-box{border-color:#ddd;background:#fff}.nxe-data-grid .nxe-grid-checkbox.nxe-checked .nxe-grid-checkbox-box:after{content:\"\";position:absolute;left:4px;top:1px;width:5px;height:9px;border:solid #337ab7;border-width:0 2px 2px 0;transform:rotate(45deg)}.nxe-data-grid .nxe-grid-checkbox.nxe-indeterminate .nxe-grid-checkbox-box{border-color:#ddd}.nxe-data-grid .nxe-grid-checkbox.nxe-indeterminate .nxe-grid-checkbox-box:after{content:\"\";position:absolute;left:3px;top:3px;width:8px;height:8px;background:#337ab7}.nxe-data-grid .nxe-grid-checkbox.nxe-grid-checkbox-readonly{display:block;text-align:center;cursor:default;height:18px;line-height:18px}@keyframes nxe-grid-spin{to{transform:rotate(360deg)}}.nxe-data-grid .dx-command-expand{width:30px;text-align:center;cursor:pointer}.nxe-detail-expander{display:inline-block;width:0;height:0;border-top:5px solid transparent;border-bottom:5px solid transparent;border-left:6px solid #999;cursor:pointer;transition:transform .15s ease}.nxe-detail-expander.nxe-expanded{transform:rotate(90deg)}.nxe-data-grid .dx-master-detail-row>td{padding:8px 12px;background:#fafafa}.nxe-grid-cell-editor{width:100%;box-sizing:border-box;border:1px solid #337ab7;padding:2px 4px;font:inherit;background:#fff;outline:none}.nxe-data-grid .dx-command-edit{text-align:center;white-space:nowrap}.nxe-data-grid .dx-command-edit .dx-link{color:#337ab7;cursor:pointer;margin:0 4px;text-decoration:none}.nxe-data-grid .dx-command-edit .dx-link:hover{text-decoration:underline}.nxe-data-grid .dx-header-row td.dx-datagrid-drag-action,.nxe-data-grid .dx-group-panel-item{touch-action:none}.nxe-data-grid .nxe-drag-source{opacity:.5}.nxe-data-grid .dx-datagrid-filter-row .dx-menu ul{list-style:none;margin:0;padding:0}.nxe-data-grid .dx-datagrid-filter-row .dx-editor-with-menu{display:flex}.nxe-data-grid .dx-datagrid-filter-row .dx-editor-with-menu .nxe-grid-filter-input{flex:1 1 0;min-width:0}.nxe-data-grid .dx-datagrid-filter-row .dx-editor-with-menu .nxe-fr-second{padding-left:4px!important;border-left:1px solid #ddd}.nxe-data-grid .dx-datagrid-filter-row .dx-icon[class*=dx-icon-filter-operation-],.nxe-fr-op-menu .dx-icon[class*=dx-icon-filter-operation-]{font:600 12px/18px inherit!important;font-family:inherit!important;display:inline-block;width:18px;height:18px;text-align:center;color:#757575}.nxe-data-grid .dx-icon-filter-operation-contains:before,.nxe-fr-op-menu .dx-icon-filter-operation-contains:before{content:\"*\"!important}.nxe-data-grid .dx-icon-filter-operation-not-contains:before,.nxe-fr-op-menu .dx-icon-filter-operation-not-contains:before{content:\"!*\"!important}.nxe-data-grid .dx-icon-filter-operation-starts-with:before,.nxe-fr-op-menu .dx-icon-filter-operation-starts-with:before{content:\"a*\"!important}.nxe-data-grid .dx-icon-filter-operation-ends-with:before,.nxe-fr-op-menu .dx-icon-filter-operation-ends-with:before{content:\"*a\"!important}.nxe-data-grid .dx-icon-filter-operation-equals:before,.nxe-fr-op-menu .dx-icon-filter-operation-equals:before{content:\"=\"!important}.nxe-data-grid .dx-icon-filter-operation-not-equals:before,.nxe-fr-op-menu .dx-icon-filter-operation-not-equals:before{content:\"\\2260\"!important}.nxe-data-grid .dx-icon-filter-operation-less:before,.nxe-fr-op-menu .dx-icon-filter-operation-less:before{content:\"<\"!important}.nxe-data-grid .dx-icon-filter-operation-greater:before,.nxe-fr-op-menu .dx-icon-filter-operation-greater:before{content:\">\"!important}.nxe-data-grid .dx-icon-filter-operation-less-equal:before,.nxe-fr-op-menu .dx-icon-filter-operation-less-equal:before{content:\"\\2264\"!important}.nxe-data-grid .dx-icon-filter-operation-greater-equal:before,.nxe-fr-op-menu .dx-icon-filter-operation-greater-equal:before{content:\"\\2265\"!important}.nxe-data-grid .dx-icon-filter-operation-between:before,.nxe-fr-op-menu .dx-icon-filter-operation-between:before{content:\"\\2194\"!important}.nxe-fr-op-menu{background:#fff;border:1px solid #ddd;box-shadow:0 2px 6px #00000026;padding:4px 0;min-width:180px}.nxe-fr-op-menu .dx-menu-item{display:flex;align-items:center;gap:8px;padding:6px 24px 6px 8px;cursor:pointer;color:#333}.nxe-fr-op-menu .dx-menu-item:hover{background:#f0f0f0}.nxe-fr-op-menu .dx-menu-item.dx-menu-item-selected{background:#e6e6e6}.nxe-fr-op-menu .dx-menu-item .dx-icon{width:18px;text-align:center}.nxe-data-grid .nxe-col-resize-handle{position:absolute;right:-4px;top:0;bottom:0;width:8px;cursor:col-resize;z-index:2;touch-action:none}body.nxe-grid-resizing{cursor:col-resize;user-select:none;-webkit-user-select:none}.nxe-col-drop-indicator{position:fixed;width:2px;background:#337ab7;z-index:10000;pointer-events:none}\n"] }]
4132
- }], ctorParameters: () => [], propDecorators: { declaredColumns: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => NxeColumnComponent), { ...{ descendants: false }, isSignal: true }] }], templates: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => NxeTemplateDirective), { isSignal: true }] }], selection: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NxeSelectionComponent), { isSignal: true }] }], headerFilter: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NxeHeaderFilterComponent), { isSignal: true }] }], grouping: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NxeGroupingComponent), { isSignal: true }] }], groupPanel: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NxeGroupPanelComponent), { isSignal: true }] }], summaryCmp: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NxeSummaryComponent), { isSignal: true }] }], pagingCmp: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NxePagingComponent), { isSignal: true }] }], pagerCmp: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NxePagerComponent), { isSignal: true }] }], editingCmp: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NxeEditingComponent), { isSignal: true }] }], filterRowCmp: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NxeFilterRowComponent), { isSignal: true }] }], masterDetail: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NxeMasterDetailComponent), { isSignal: true }] }], dataSource: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataSource", required: false }] }], keyExpr: [{ type: i0.Input, args: [{ isSignal: true, alias: "keyExpr", required: false }] }], hoverStateEnabledInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "hoverStateEnabled", required: false }] }], showColumnLinesInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "showColumnLines", required: false }] }], showRowLinesInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "showRowLines", required: false }] }], showBordersInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "showBorders", required: false }] }], wordWrapEnabledInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "wordWrapEnabled", required: false }] }], columnAutoWidthInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnAutoWidth", required: false }] }], rowAlternationEnabledInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowAlternationEnabled", required: false }] }], remoteOperations: [{ type: i0.Input, args: [{ isSignal: true, alias: "remoteOperations", required: false }] }], syncLookupFilterValues: [{ type: i0.Input, args: [{ isSignal: true, alias: "syncLookupFilterValues", required: false }] }], columnMinWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnMinWidth", required: false }] }], noDataTextInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "noDataText", required: false }] }], loadPanel: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadPanel", required: false }] }], paging: [{ type: i0.Input, args: [{ isSignal: true, alias: "paging", required: false }] }], filterRow: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterRow", required: false }] }], headerFilterOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerFilter", required: false }] }], groupingOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "grouping", required: false }] }], groupPanelOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupPanel", required: false }] }], scrolling: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrolling", required: false }] }], sorting: [{ type: i0.Input, args: [{ isSignal: true, alias: "sorting", required: false }] }], allowColumnResizing: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowColumnResizing", required: false }] }], columnResizingMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnResizingMode", required: false }] }], allowColumnReordering: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowColumnReordering", required: false }] }], repaintChangesOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "repaintChangesOnly", required: false }] }], showColumnHeaders: [{ type: i0.Input, args: [{ isSignal: true, alias: "showColumnHeaders", required: false }] }], parentIdExpr: [{ type: i0.Input, args: [{ isSignal: true, alias: "parentIdExpr", required: false }] }], dataStructure: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataStructure", required: false }] }], rootValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "rootValue", required: false }] }], autoExpandAll: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoExpandAll", required: false }] }], expandedRowKeys: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandedRowKeys", required: false }] }, { type: i0.Output, args: ["expandedRowKeysChange"] }], selectedRowKeys: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedRowKeys", required: false }] }, { type: i0.Output, args: ["selectedRowKeysChange"] }], mergeColumns: [{ type: i0.Input, args: [{ isSignal: true, alias: "mergeColumns", required: false }] }], onSelectionChanged: [{ type: i0.Output, args: ["onSelectionChanged"] }], onRowClick: [{ type: i0.Output, args: ["onRowClick"] }], onContentReady: [{ type: i0.Output, args: ["onContentReady"] }], onOptionChanged: [{ type: i0.Output, args: ["onOptionChanged"] }], rowClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowClass", required: false }] }], onCellPrepared: [{
6145
+ args: [{ selector: 'nxe-data-grid, nxe-tree-list', standalone: true, imports: [NgTemplateOutlet], host: { class: 'nxe-widget nxe-data-grid' }, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<div\r\n class=\"nxe-widget nxe-datagrid\"\r\n [class.nxe-datagrid-borders]=\"showBorders()\"\r\n [style.height]=\"heightStyle()\">\r\n <div class=\"nxe-gridbase-container\">\r\n @if (groupPanelVisible()) {\r\n <div class=\"nxe-datagrid-header-panel\">\r\n <div class=\"nxe-datagrid-group-panel\">\r\n @if (groupColumns().length === 0) {\r\n <span class=\"nxe-group-panel-message\">{{ t('grid.groupPanel') }}</span>\r\n } @else {\r\n @for (col of groupColumns(); track col.dataField(); let li = $index) {\r\n <!-- dx chip DOM: caption + sort indicator + header-filter button (bright audit) -->\r\n <div\r\n class=\"nxe-group-panel-item nxe-datagrid-action nxe-datagrid-drag-action\"\r\n tabindex=\"0\"\r\n (pointerdown)=\"onChipPointerDown(col, li, $event)\"\r\n (click)=\"onChipClick(col)\">{{ col.caption() }}<div class=\"nxe-column-indicators\" role=\"presentation\"><span\r\n class=\"nxe-sort\"\r\n [class.nxe-sort-up]=\"groupDir(col) === 'asc'\"\r\n [class.nxe-sort-down]=\"groupDir(col) === 'desc'\"></span>@if (headerFilterVisible(col)) {<span\r\n class=\"nxe-grid-hf-icon\"\r\n [class.nxe-hf-active]=\"hasHeaderFilter(col)\"\r\n (pointerdown)=\"$event.stopPropagation()\"\r\n (click)=\"openHeaderFilter(col, $event)\"></span>}</div></div>\r\n }\r\n }\r\n </div>\r\n </div>\r\n }\r\n <!-- dx drops the last row's bottom line in a bordered grid when no free-space row follows\r\n it, so the rows view's own border is the only line there (m_rows_view.js:931-937). -->\r\n <div\r\n class=\"nxe-datagrid-rowsview nxe-datagrid-after-headers\"\r\n [class.nxe-datagrid-nowrap]=\"!wordWrapEnabled()\"\r\n [class.nxe-empty]=\"isEmpty()\"\r\n [class.nxe-remote-busy]=\"remoteBusy()\"\r\n [class.nxe-last-row-border]=\"showBorders() && !freeSpaceVisible()\">\r\n <div class=\"nxe-datagrid-content\">\r\n <!-- role=grid (treegrid when hierarchical) rather than the implicit table role:\r\n this is an interactive widget with a single tab stop and arrow-key movement,\r\n not a static table.\r\n\r\n aria-rowcount counts the PAGE, because aria-rowindex is page-relative and the\r\n two have to agree. Making the index absolute would be more literally correct\r\n for a paged grid, but consuming code addresses cells through it\r\n (`tr[aria-rowindex=N] > td[aria-colindex=M]`), so the index is a contract. -->\r\n <table\r\n class=\"nxe-datagrid-table nxe-datagrid-table-fixed\"\r\n [attr.role]=\"treeMode ? 'treegrid' : 'grid'\"\r\n [attr.aria-rowcount]=\"navigableRows().length\"\r\n [attr.aria-busy]=\"remoteBusy() ? 'true' : null\"\r\n [attr.aria-colcount]=\"visibleColumns().length + leadCellCount() + (rowEditCommandVisible() ? 1 : 0)\"\r\n [style.width]=\"tableWidthStyle()\"\r\n (keydown)=\"onGridKeydown($event)\">\r\n <colgroup>\r\n @for (i of indents(groupLevelCount()); track i) {\r\n <col style=\"width: 30px\" />\r\n }\r\n @if (masterDetailEnabled()) {\r\n <col style=\"width: 30px\" />\r\n }\r\n @if (selectColumnVisible()) {\r\n <col style=\"width: 40px\" />\r\n }\r\n @for (col of visibleColumns(); track $index; let ci = $index) {\r\n <col [style.width]=\"effectiveColWidth(col, ci)\" />\r\n }\r\n @if (rowEditCommandVisible()) {\r\n <col style=\"width: 100px\" />\r\n }\r\n </colgroup>\r\n <thead class=\"nxe-datagrid-headers\">\r\n <!-- Bands add header rows: a band cell spans its visible leaves with\r\n colspan, and a leaf above the bottom row spans down with rowspan.\r\n That is dx's own banded markup, and it keeps the LAST header row\r\n all-leaves - which the auto-width, resize and drag code relies on. -->\r\n @for (hrow of headerRows(); track $index; let hi = $index) {\r\n <tr\r\n class=\"nxe-row nxe-header-row\"\r\n role=\"row\"\r\n [class.nxe-column-lines]=\"showColumnLines()\">\r\n @if (hi === 0) {\r\n <!-- dx numbers the header's command cells like the body's\r\n (m_columns_view.js:681-693); they are not role=columnheader\r\n (m_column_headers.js:193), which keeps them out of the leaf\r\n header list (nxeHeaderLeafCells). -->\r\n @for (i of indents(groupLevelCount()); track i) {\r\n <td\r\n class=\"nxe-command-expand nxe-datagrid-group-space nxe-cell-focus-disabled\"\r\n [attr.aria-colindex]=\"i + 1\"\r\n [attr.rowspan]=\"headerDepth() > 1 ? headerDepth() : null\"></td>\r\n }\r\n @if (masterDetailEnabled()) {\r\n <!-- dx gives every cell of an 'expand' command column the\r\n group-space class (m_columns_view.js:290-293), which drops\r\n the column line between it and the next cell. -->\r\n <td\r\n class=\"nxe-command-expand nxe-datagrid-group-space\"\r\n [attr.aria-colindex]=\"expandColIndex()\"\r\n [attr.rowspan]=\"headerDepth() > 1 ? headerDepth() : null\"></td>\r\n }\r\n @if (selectColumnVisible()) {\r\n <td\r\n class=\"nxe-command-select\"\r\n [attr.aria-colindex]=\"selectColIndex()\"\r\n [attr.rowspan]=\"headerDepth() > 1 ? headerDepth() : null\">\r\n @if (selectionMode() === 'multiple') {\r\n <span\r\n class=\"nxe-select-checkbox nxe-grid-checkbox nxe-checkbox nxe-datagrid-checkbox-size\"\r\n role=\"checkbox\"\r\n [attr.aria-checked]=\"allSelected() ? 'true' : someSelected() ? 'mixed' : 'false'\"\r\n [class.nxe-checked]=\"allSelected()\"\r\n [class.nxe-indeterminate]=\"someSelected() && !allSelected()\"\r\n (click)=\"toggleSelectAll($event)\">\r\n <span class=\"nxe-grid-checkbox-box\"></span>\r\n </span>\r\n }\r\n </td>\r\n }\r\n }\r\n @for (cell of hrow; track $index) {\r\n @if (cell.isBand) {\r\n <!-- dx gives a band the alignment of any column: left unless set\r\n (a band has no dataType to right-align or centre it by). -->\r\n <td\r\n class=\"nxe-band-cell\"\r\n role=\"columnheader\"\r\n [class]=\"cell.column.cssClass()\"\r\n [attr.colspan]=\"cell.colspan > 1 ? cell.colspan : null\"\r\n [style.text-align]=\"align(cell.column)\">\r\n <div class=\"nxe-datagrid-text-content\">{{ cell.column.caption() }}</div>\r\n </td>\r\n } @else {\r\n @let hDir = sortDir(cell.column);\r\n @let hHf = headerFilterVisible(cell.column);\r\n @let hSortSlot = sortSlot(cell.column);\r\n @let hInd = hSortSlot || hHf;\r\n @let hAlign = align(cell.column);\r\n <!-- dx places the indicator block before the caption when column lines are\r\n on or the column is right-aligned (m_column_headers.js createCellContent,\r\n m_column_state_mixin.js _applyColumnState), floated to the cell edge\r\n only with column lines: right, or left for a right-aligned column. -->\r\n @let hLead = hInd && (showColumnLines() || hAlign === 'right');\r\n @let hFloat = showColumnLines() ? (hAlign === 'right' ? 'left' : 'right') : null;\r\n <td\r\n [class]=\"cell.column.cssClass()\"\r\n role=\"columnheader\"\r\n [attr.aria-colindex]=\"leadCellCount() + cell.leafIndex + 1\"\r\n [attr.aria-sort]=\"ariaSort(cell.column)\"\r\n [attr.tabindex]=\"isCellTabStop(-1, cell.leafIndex) ? 0 : -1\"\r\n (focus)=\"focusCell(-1, cell.leafIndex)\"\r\n [attr.rowspan]=\"cell.rowspan > 1 ? cell.rowspan : null\"\r\n [style.text-align]=\"hAlign\"\r\n [class.nxe-cell-focus-disabled]=\"!cell.column.allowSorting()\"\r\n [class.nxe-datagrid-action]=\"sortAllowed(cell.column)\"\r\n [class.nxe-datagrid-drag-action]=\"headerDraggable(cell.column)\"\r\n [class.nxe-tree-select-all]=\"treeSelectAll() && cell.leafIndex === 0\"\r\n (pointerdown)=\"onHeaderPointerDown(cell.column, cell.leafIndex, $event)\"\r\n (click)=\"onHeaderClick(cell.column)\">\r\n @if (treeSelectAll() && cell.leafIndex === 0) {\r\n <!-- dx's tree list puts select-all in the first column's header. -->\r\n <span\r\n class=\"nxe-select-checkbox nxe-grid-checkbox nxe-checkbox nxe-datagrid-checkbox-size\"\r\n role=\"checkbox\"\r\n [attr.aria-label]=\"t('common.selectAll')\"\r\n [attr.aria-checked]=\"allSelected() ? 'true' : someSelected() ? 'mixed' : 'false'\"\r\n [class.nxe-checked]=\"allSelected()\"\r\n [class.nxe-indeterminate]=\"someSelected() && !allSelected()\"\r\n (pointerdown)=\"$event.stopPropagation()\"\r\n (click)=\"toggleSelectAll($event)\">\r\n <span class=\"nxe-grid-checkbox-box\"></span>\r\n </span>\r\n }\r\n @if (hLead) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"headerIndicators\"\r\n [ngTemplateOutletContext]=\"{ $implicit: cell.column, dir: hDir, sortSlot: hSortSlot, hf: hHf, hfFirst: hAlign === 'right', float: hFloat }\" />\r\n }\r\n <!-- dx _alignCaptionByCenter: a centred caption gets an unfloated,\r\n invisible copy of the indicator block on its other side, so the\r\n caption centres on the cell rather than caption + arrow centring\r\n as a group. Spacers only: no sort or funnel classes (specs and\r\n apps pick funnels by index) and no handlers - a click there falls\r\n through to the cell and sorts, as dx's hidden clone lets it. -->\r\n @if (hInd && hAlign === 'center') {\r\n <div\r\n class=\"nxe-column-indicators nxe-visibility-hidden\"\r\n role=\"presentation\"\r\n aria-hidden=\"true\">\r\n @if (hSortSlot) {\r\n <span\r\n class=\"nxe-column-indicator-spacer\"\r\n [class.nxe-column-indicator-spacer-none]=\"!hDir\"></span>\r\n }\r\n @if (hHf) {\r\n <span class=\"nxe-column-indicator-spacer\"></span>\r\n }\r\n </div>\r\n }\r\n <!-- The indicator classes are load-bearing, not decorative: they gate dx's\r\n caption budget (`.dx-header-row .dx-sort-indicator{max-width:calc(100%\r\n - 17px)}` and family) and, with the alignment classes, the 3px gap to\r\n the block. dx sets alignment-left for left and centre columns and\r\n alignment-right for right and centre ones, whenever the cell has an\r\n indicator slot at all - even an unsorted, hidden one. -->\r\n <div\r\n class=\"nxe-datagrid-text-content\"\r\n [class.nxe-text-content-alignment-left]=\"hInd && hAlign !== 'right'\"\r\n [class.nxe-text-content-alignment-right]=\"hInd && hAlign !== 'left'\"\r\n [class.nxe-sort-indicator]=\"hDir !== null\"\r\n [class.nxe-header-filter-indicator]=\"hHf\">\r\n {{ cell.column.caption() }}\r\n </div>\r\n @if (hInd && !hLead) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"headerIndicators\"\r\n [ngTemplateOutletContext]=\"{ $implicit: cell.column, dir: hDir, sortSlot: hSortSlot, hf: hHf, hfFirst: hAlign === 'right', float: null }\" />\r\n }\r\n @if (resizeHandleVisible(cell.column, cell.leafIndex)) {\r\n <span\r\n class=\"nxe-col-resize-handle\"\r\n (pointerdown)=\"startColumnResize(cell.column, cell.leafIndex, $event)\"></span>\r\n }\r\n </td>\r\n }\r\n }\r\n @if (hi === 0 && rowEditCommandVisible()) {\r\n <td\r\n class=\"nxe-command-edit\"\r\n [attr.aria-colindex]=\"editColIndex()\"\r\n [attr.rowspan]=\"headerDepth() > 1 ? headerDepth() : null\"></td>\r\n }\r\n </tr>\r\n }\r\n @if (filterRowVisible()) {\r\n <!-- The headers view toggles column lines on every row it renders, the\r\n filter row included (m_column_headers.js:205-207). -->\r\n <!-- Every filter-row cell is numbered, as the headers view numbers it\r\n (m_columns_view.js:681-693). -->\r\n <tr class=\"nxe-row nxe-datagrid-filter-row\" [class.nxe-column-lines]=\"showColumnLines()\">\r\n @for (i of indents(groupLevelCount()); track i) {\r\n <td class=\"nxe-command-expand nxe-datagrid-group-space\" [attr.aria-colindex]=\"i + 1\"></td>\r\n }\r\n @if (masterDetailEnabled()) {\r\n <td class=\"nxe-command-expand nxe-datagrid-group-space\" [attr.aria-colindex]=\"expandColIndex()\"></td>\r\n }\r\n @if (selectColumnVisible()) {\r\n <td class=\"nxe-command-select\" [attr.aria-colindex]=\"selectColIndex()\"></td>\r\n }\r\n @for (col of visibleColumns(); track $index; let ci = $index) {\r\n <!-- dx filter cell: td.dx-editor-cell > .dx-editor-with-menu >\r\n [.dx-menu op chooser][typed editor] (dx-compat.css contract) -->\r\n <td\r\n class=\"nxe-editor-cell\"\r\n [class]=\"col.cssClass()\"\r\n [attr.aria-colindex]=\"leadCellCount() + ci + 1\">\r\n @if (col.allowFiltering()) {\r\n @switch (frEditorKind(col)) {\r\n @case ('lookup') {\r\n <select\r\n class=\"nxe-texteditor-input nxe-grid-filter-input\"\r\n [value]=\"frText(col)\"\r\n (click)=\"$event.stopPropagation()\"\r\n (change)=\"setFilterRowText(col, $any($event.target).value)\">\r\n <option value=\"\">{{ t('common.all') }}</option>\r\n @for (item of col.lookup()!.dataSource; track $index) {\r\n <option [value]=\"item[col.lookup()!.valueExpr ?? 'lovCode']\">\r\n {{ item[col.lookup()!.displayExpr ?? 'lovName'] }}\r\n </option>\r\n }\r\n </select>\r\n }\r\n @case ('boolean') {\r\n <select\r\n class=\"nxe-texteditor-input nxe-grid-filter-input\"\r\n [value]=\"frText(col)\"\r\n (click)=\"$event.stopPropagation()\"\r\n (change)=\"setFilterRowText(col, $any($event.target).value)\">\r\n <option value=\"\">{{ t('common.all') }}</option>\r\n <option value=\"true\">{{ t('grid.filterRow.true') }}</option>\r\n <option value=\"false\">{{ t('grid.filterRow.false') }}</option>\r\n </select>\r\n }\r\n @default {\r\n <div class=\"nxe-editor-with-menu\">\r\n <div class=\"nxe-menu nxe-menu-base\">\r\n <ul class=\"nxe-menu-horizontal\">\r\n <li>\r\n <div\r\n class=\"nxe-menu-item\"\r\n [attr.title]=\"t('grid.filterRow.operation')\"\r\n (click)=\"openFilterOpMenu(col, $event)\">\r\n <div class=\"nxe-menu-item-content\">\r\n <span\r\n class=\"nxe-icon nxe-icon-filter-operation-{{ frOpIconSlug(col) }}\"></span>\r\n </div>\r\n </div>\r\n </li>\r\n </ul>\r\n </div>\r\n <!-- dx sets the FIRST input's text-align to the column\r\n alignment inline, unless centre (m_filter_row.js:390-395). -->\r\n <input\r\n class=\"nxe-texteditor-input nxe-grid-filter-input\"\r\n [type]=\"frEditorKind(col) === 'date' ? 'date' : 'text'\"\r\n [attr.inputmode]=\"frEditorKind(col) === 'number' ? 'decimal' : null\"\r\n [style.text-align]=\"filterCellAlign(col)\"\r\n [value]=\"frText(col)\"\r\n (input)=\"setFilterRowText(col, $any($event.target).value)\"\r\n (click)=\"$event.stopPropagation()\" />\r\n @if (frOp(col) === 'between') {\r\n <input\r\n class=\"nxe-texteditor-input nxe-grid-filter-input nxe-fr-second\"\r\n [type]=\"frEditorKind(col) === 'date' ? 'date' : 'text'\"\r\n [attr.inputmode]=\"frEditorKind(col) === 'number' ? 'decimal' : null\"\r\n [value]=\"frText2(col)\"\r\n (input)=\"setFilterRowText(col, $any($event.target).value, true)\"\r\n (click)=\"$event.stopPropagation()\" />\r\n }\r\n </div>\r\n }\r\n }\r\n }\r\n </td>\r\n }\r\n @if (rowEditCommandVisible()) {\r\n <td class=\"nxe-command-edit\" [attr.aria-colindex]=\"editColIndex()\"></td>\r\n }\r\n </tr>\r\n }\r\n </thead>\r\n <tbody>\r\n @for (item of renderRows(); track $index; let ri = $index) {\r\n @if (item.kind === 'group') {\r\n <!-- dx group-row anatomy (bright audit): [indent cells][expand chevron\r\n cell][dx-group-cell colspan][alignByColumn summary cells]. dx numbers\r\n every cell of a rows-view row, command cells included\r\n (m_columns_view.js:681-693), and the caption as the cell after the\r\n toggle (m_rows_view.js:626-635, columnIndex groupIndex + 1). That\r\n number can be a data column's, so dx looks group rows up with\r\n `:not(.dx-group-cell)` (m_columns_view.js:942), and onCellPrepared\r\n reports the caption as no column. -->\r\n <tr\r\n class=\"nxe-row nxe-group-row\"\r\n [class.nxe-column-lines]=\"showColumnLines()\"\r\n [attr.aria-roledescription]=\"item.collapsed ? t('grid.collapsedRow') : t('grid.expandedRow')\">\r\n @for (i of indents(item.level); track i) {\r\n <td class=\"nxe-command-expand nxe-datagrid-group-space\" [attr.aria-colindex]=\"i + 1\"></td>\r\n }\r\n <td\r\n class=\"nxe-command-expand nxe-datagrid-group-space nxe-datagrid-expand nxe-selection-disabled\"\r\n [attr.aria-colindex]=\"item.level + 1\"\r\n [attr.aria-label]=\"item.collapsed ? t('grid.expand') : t('grid.collapse')\"\r\n (click)=\"toggleGroup(item.groupKey)\">\r\n <div\r\n [class.nxe-datagrid-group-opened]=\"!item.collapsed\"\r\n [class.nxe-datagrid-group-closed]=\"item.collapsed\"></div>\r\n </td>\r\n <td\r\n class=\"nxe-group-cell\"\r\n [attr.aria-colindex]=\"item.level + 2\"\r\n [attr.colspan]=\"groupCellColspan(item)\">\r\n @if (templateFor(item.column.groupCellTemplate()); as gtpl) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"gtpl\"\r\n [ngTemplateOutletContext]=\"groupCellContext(item)\" />\r\n } @else {\r\n {{ item.column.caption() }}: {{ item.text }}{{ item.captionSuffix }}\r\n }\r\n </td>\r\n @for (col of groupRowTrailing(item); track col.dataField()) {\r\n <td\r\n class=\"nxe-datagrid-summary-item\"\r\n [attr.aria-colindex]=\"leadCellCount() + visibleIndexOf(col) + 1\"\r\n [style.text-align]=\"align(col)\">\r\n {{ item.alignedSums.get(col.dataField()) || '' }}\r\n </td>\r\n }\r\n @if (groupRowTrailing(item).length && rowEditCommandVisible()) {\r\n <td class=\"nxe-command-edit\" [attr.aria-colindex]=\"editColIndex()\"></td>\r\n }\r\n </tr>\r\n } @else if (item.kind === 'groupFooter') {\r\n <!-- dx's group footer: dx-datagrid-group-footer, and column lines like\r\n any row the rows view renders (m_rows_view.js:222-229). Row lines are\r\n data rows' only (:228); the line under a footer is the column-lines\r\n rule's. -->\r\n <tr\r\n class=\"nxe-row nxe-group-footer-row nxe-datagrid-group-footer\"\r\n [class]=\"'nxe-group-footer-level-' + item.level\"\r\n [class.nxe-column-lines]=\"showColumnLines()\">\r\n @for (i of indents(groupLevelCount()); track i) {\r\n <td class=\"nxe-command-expand nxe-datagrid-group-space\" [attr.aria-colindex]=\"i + 1\"></td>\r\n }\r\n @if (masterDetailEnabled()) {\r\n <td class=\"nxe-command-expand nxe-datagrid-group-space\" [attr.aria-colindex]=\"expandColIndex()\"></td>\r\n }\r\n @if (selectColumnVisible()) {\r\n <td class=\"nxe-command-select\" [attr.aria-colindex]=\"selectColIndex()\"></td>\r\n }\r\n @for (col of visibleColumns(); track col.dataField()) {\r\n <td\r\n class=\"nxe-datagrid-summary-item\"\r\n [attr.aria-colindex]=\"leadCellCount() + visibleIndexOf(col) + 1\"\r\n [style.text-align]=\"align(col)\">\r\n {{ item.sums.get(col.dataField()) || '' }}\r\n </td>\r\n }\r\n @if (rowEditCommandVisible()) {\r\n <td class=\"nxe-command-edit\" [attr.aria-colindex]=\"editColIndex()\"></td>\r\n }\r\n </tr>\r\n } @else {\r\n <tr\r\n class=\"nxe-row nxe-data-row\"\r\n role=\"row\"\r\n [class]=\"rowClassFor(item.row)\"\r\n [attr.aria-rowindex]=\"item.rowIndex + 1\"\r\n [attr.aria-selected]=\"selectionMode() === 'none' ? null : isSelected(item.row)\"\r\n [class.nxe-row-lines]=\"showRowLines()\"\r\n [class.nxe-column-lines]=\"showColumnLines()\"\r\n [class.nxe-row-alt]=\"rowAlternationEnabled() && item.rowIndex % 2 === 1\"\r\n [class.nxe-selection]=\"isSelected(item.row)\"\r\n [class.nxe-state-hover]=\"isHovered(item.rowIndex)\"\r\n [class.nxe-edit-row]=\"isRowEditing(item.row)\"\r\n (mouseenter)=\"onRowEnter(item.rowIndex)\"\r\n (mouseleave)=\"onRowLeave()\"\r\n (click)=\"onDataRowClick(item.row, item.rowIndex)\">\r\n @for (i of indents(groupLevelCount()); track i) {\r\n <td class=\"nxe-command-expand nxe-datagrid-group-space\" [attr.aria-colindex]=\"i + 1\"></td>\r\n }\r\n @if (masterDetailEnabled()) {\r\n <!-- dx reuses the group row's toggle for the detail row\r\n (m_master_detail.js:33-43, m_utils.js:462-474), and any click\r\n in the cell toggles (m_grouping.js:523-532 `closest('.dx-datagrid-expand')`).\r\n toggleDetail stops the click there, as dx-selection-disabled does. -->\r\n <td\r\n class=\"nxe-command-expand nxe-datagrid-group-space nxe-datagrid-expand nxe-selection-disabled\"\r\n [attr.aria-colindex]=\"expandColIndex()\"\r\n [attr.aria-label]=\"isDetailExpanded(item.row) ? t('grid.collapse') : t('grid.expand')\"\r\n (click)=\"toggleDetail(item.row, $event)\">\r\n <div\r\n [class.nxe-datagrid-group-opened]=\"isDetailExpanded(item.row)\"\r\n [class.nxe-datagrid-group-closed]=\"!isDetailExpanded(item.row)\"></div>\r\n </td>\r\n }\r\n @if (selectColumnVisible()) {\r\n <td class=\"nxe-command-select\" [attr.aria-colindex]=\"selectColIndex()\">\r\n <span\r\n class=\"nxe-select-checkbox nxe-grid-checkbox nxe-checkbox nxe-datagrid-checkbox-size\"\r\n role=\"checkbox\"\r\n [attr.aria-checked]=\"isSelected(item.row)\"\r\n [class.nxe-checked]=\"isSelected(item.row)\"\r\n (click)=\"onCheckboxClick(item.row, $event)\">\r\n <span class=\"nxe-grid-checkbox-box\"></span>\r\n </span>\r\n </td>\r\n }\r\n @for (col of visibleColumns(); track $index; let ci = $index) {\r\n @if (!isMergeSuppressed(ri, col)) {\r\n <td\r\n [class]=\"col.cssClass()\"\r\n role=\"gridcell\"\r\n [attr.aria-colindex]=\"leadCellCount() + ci + 1\"\r\n [attr.tabindex]=\"isCellTabStop(item.rowIndex, ci) ? 0 : -1\"\r\n (focus)=\"focusCell(item.rowIndex, ci)\"\r\n [attr.aria-roledescription]=\"\r\n editableCell(col) && (cellEditingEnabled() || rowEditingEnabled())\r\n ? t('grid.editableCell')\r\n : null\r\n \"\r\n [style.text-align]=\"align(col)\"\r\n [attr.rowspan]=\"mergeSpan(ri, col)\"\r\n [class.nxe-editor-cell]=\"isEditingCell(item.row, col) || (isRowEditing(item.row) && editableCell(col))\"\r\n (click)=\"onCellClick(item.row, col, $event)\"\r\n (focusout)=\"onEditCellFocusOut(item.row, col, $event)\">\r\n @if (treeMode && ci === 0) {\r\n <!-- dx's tree icons (dx-treelist-icon-container): one 14px slot\r\n per level, then the 14x18 toggle or, on a leaf, one more slot.\r\n Floated as dx floats it in the fixed table, so the text\r\n wraps beside it and starts flush after it. Not named\r\n nxe-treelist-*: the dx mirror would hand it to an app's\r\n newer-dx tree layout (18px slots). -->\r\n <span class=\"nxe-tree-icons\" [class.nxe-tree-icons-select]=\"showCheckboxes()\">\r\n @for (i of indents(item.level || 0); track i) {\r\n <span class=\"nxe-tree-expander-stub\"></span>\r\n }\r\n @if (item.hasChildren) {\r\n <span\r\n class=\"nxe-tree-expander\"\r\n [class.nxe-collapsed]=\"!item.expanded\"\r\n (click)=\"toggleTreeNode(item.nodeKey, $event)\"></span>\r\n } @else {\r\n <span class=\"nxe-tree-expander-stub\"></span>\r\n }\r\n <!-- dx draws a tree list's row checkbox here, in the icon\r\n container after the toggle, not in a command column. -->\r\n @if (showCheckboxes()) {\r\n <span\r\n class=\"nxe-select-checkbox nxe-grid-checkbox nxe-checkbox nxe-datagrid-checkbox-size nxe-tree-checkbox\"\r\n role=\"checkbox\"\r\n [attr.aria-checked]=\"isTreeIndeterminate(item.row) ? 'mixed' : isSelected(item.row)\"\r\n [class.nxe-checked]=\"isSelected(item.row)\"\r\n [class.nxe-indeterminate]=\"isTreeIndeterminate(item.row)\"\r\n (click)=\"onCheckboxClick(item.row, $event)\">\r\n <span class=\"nxe-grid-checkbox-box\"></span>\r\n </span>\r\n }\r\n </span>\r\n }\r\n @if (isEditingCell(item.row, col) || (isRowEditing(item.row) && editableCell(col))) {\r\n @if (templateFor(col.editCellTemplate()); as etpl) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"etpl\"\r\n [ngTemplateOutletContext]=\"editCellContext(item.row, item.rowIndex, ci, col)\" />\r\n } @else if (isEditingCell(item.row, col)) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"cellEditor\"\r\n [ngTemplateOutletContext]=\"{ row: item.row, col: col, mode: 'cell' }\" />\r\n } @else {\r\n <ng-container\r\n [ngTemplateOutlet]=\"cellEditor\"\r\n [ngTemplateOutletContext]=\"{ row: item.row, col: col, mode: 'row' }\" />\r\n }\r\n } @else if (templateFor(col.cellTemplate()); as tpl) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"tpl\"\r\n [ngTemplateOutletContext]=\"cellContext(item.row, item.rowIndex, ci, col)\" />\r\n } @else if (isBooleanCell(col)) {\r\n <!-- dx showEditorAlways: a boolean cell is a dx checkbox, not\r\n \"true\"/\"false\". It is read-only unless the grid edits cells\r\n and the column is editable (m_editing.js:210, 1832-1847);\r\n then one click toggles the value in place. -->\r\n <span\r\n class=\"nxe-grid-checkbox nxe-grid-checkbox-readonly nxe-checkbox nxe-datagrid-checkbox-size\"\r\n role=\"checkbox\"\r\n [class.nxe-grid-checkbox-left]=\"align(col) === 'left'\"\r\n [class.nxe-grid-checkbox-right]=\"align(col) === 'right'\"\r\n [class.nxe-checked]=\"getRaw(item.row, col.dataField()) === true\"\r\n [class.nxe-state-readonly]=\"booleanReadOnly(col)\"\r\n [attr.aria-checked]=\"getRaw(item.row, col.dataField()) === true\"\r\n [attr.aria-readonly]=\"booleanReadOnly(col) ? 'true' : null\"\r\n (click)=\"onBooleanCellClick(item.row, col, $event)\">\r\n <span class=\"nxe-grid-checkbox-box\"></span>\r\n </span>\r\n } @else {\r\n <!-- In an ng-container so the text node carries no padding space:\r\n after the tree icons one would render as a 3px gap. -->\r\n <ng-container>{{ cellText(col, item.row) }}</ng-container>\r\n }\r\n </td>\r\n }\r\n }\r\n @if (rowEditCommandVisible()) {\r\n <td class=\"nxe-command-edit nxe-datagrid-drag-action\" [attr.aria-colindex]=\"editColIndex()\">\r\n @if (isRowEditing(item.row)) {\r\n <a class=\"nxe-link nxe-link-save\" (click)=\"saveRowEdit(item.row, $event)\">{{ t('grid.save') }}</a>\r\n <a class=\"nxe-link nxe-link-cancel\" (click)=\"cancelRowEdit($event)\">{{ t('grid.cancel') }}</a>\r\n } @else {\r\n <a class=\"nxe-link nxe-link-edit\" (click)=\"startRowEdit(item.row, $event)\">{{ t('grid.edit') }}</a>\r\n }\r\n </td>\r\n }\r\n </tr>\r\n @if (masterDetailEnabled() && isDetailExpanded(item.row)) {\r\n <!-- dx's detail row takes row lines as well as column lines, and its one\r\n cell is dx-master-detail-cell (m_master_detail.js:290-294, :325),\r\n rendered as column 0 - so aria-colindex 1. -->\r\n <tr\r\n class=\"nxe-row nxe-master-detail-row\"\r\n [class.nxe-row-lines]=\"showRowLines()\"\r\n [class.nxe-column-lines]=\"showColumnLines()\">\r\n <td class=\"nxe-master-detail-cell nxe-cell-focus-disabled\" aria-colindex=\"1\" [attr.colspan]=\"allColCount()\">\r\n @if (detailTemplate(); as tpl) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"tpl\"\r\n [ngTemplateOutletContext]=\"detailContext(item.row, item.rowIndex)\" />\r\n }\r\n </td>\r\n </tr>\r\n }\r\n }\r\n }\r\n <!-- dx's free-space row (m_rows_view.js:448-466): one cell per column, so the\r\n column lines run on through the space it fills. The row is\r\n role=\"presentation\", as dx's `_createEmptyRow` makes it, so assistive tech\r\n inside role=grid does not meet a row of empty gridcells; its cells take\r\n no role and no aria-colindex: nothing that addresses cells may count them. -->\r\n <tr\r\n role=\"presentation\"\r\n class=\"nxe-row nxe-freespace-row\"\r\n [class.nxe-column-lines]=\"showColumnLines()\"\r\n [style.height.px]=\"freeSpaceHeight() || null\">\r\n @for (i of indents(groupLevelCount()); track i) {\r\n <td class=\"nxe-command-expand nxe-datagrid-group-space\"></td>\r\n }\r\n @if (masterDetailEnabled()) {\r\n <td class=\"nxe-command-expand nxe-datagrid-group-space\"></td>\r\n }\r\n @if (selectColumnVisible()) {\r\n <td class=\"nxe-command-select\"></td>\r\n }\r\n @for (col of visibleColumns(); track $index) {\r\n <td [class]=\"col.cssClass()\"></td>\r\n }\r\n @if (rowEditCommandVisible()) {\r\n <td class=\"nxe-command-edit\"></td>\r\n }\r\n </tr>\r\n </tbody>\r\n @if (hasTotalFooter()) {\r\n <tfoot class=\"nxe-datagrid-total-footer\">\r\n <tr class=\"nxe-row\">\r\n @for (i of indents(groupLevelCount()); track i) {\r\n <td class=\"nxe-command-expand nxe-datagrid-group-space\" [attr.aria-colindex]=\"i + 1\"></td>\r\n }\r\n @if (masterDetailEnabled()) {\r\n <td class=\"nxe-command-expand nxe-datagrid-group-space\" [attr.aria-colindex]=\"expandColIndex()\"></td>\r\n }\r\n @if (selectColumnVisible()) {\r\n <td class=\"nxe-command-select\" [attr.aria-colindex]=\"selectColIndex()\"></td>\r\n }\r\n @for (col of visibleColumns(); track col.dataField()) {\r\n <td\r\n class=\"nxe-datagrid-summary-item\"\r\n [attr.aria-colindex]=\"leadCellCount() + visibleIndexOf(col) + 1\"\r\n [style.text-align]=\"align(col)\">\r\n {{ totalSums().get(col.dataField()) || '' }}\r\n </td>\r\n }\r\n @if (rowEditCommandVisible()) {\r\n <td class=\"nxe-command-edit\" [attr.aria-colindex]=\"editColIndex()\"></td>\r\n }\r\n </tr>\r\n </tfoot>\r\n }\r\n </table>\r\n </div>\r\n\r\n @if (remoteBusy()) {\r\n <!-- Server-side data: a request is in flight over the rows already on screen. -->\r\n <div class=\"nxe-datagrid-loadpanel nxe-datagrid-remote-busy\" role=\"status\" [attr.aria-label]=\"t('grid.remote.loading')\">\r\n <span class=\"nxe-grid-spinner\"></span>\r\n </div>\r\n }\r\n\r\n @if (isEmpty()) {\r\n <!-- dx `.dx-datagrid-rowsview.dx-empty{height:100px}`: our rows view also holds the\r\n <thead>, so the empty body is its own box after the table. dx appends the nodata\r\n span to the rows view outside its scrollable (m_utils.js:206-210) and positions the\r\n load panel `of:` the rows view, header excluded (m_rows_view.js:1023) - both live\r\n in this box. The load panel can: loading() implies a null source, hence no rows.\r\n If the total footer is ever shown for empty data (dx m_summary.js:487-503), this\r\n box has to move between the <tbody> and the <tfoot>. -->\r\n <div class=\"nxe-datagrid-empty-body\">\r\n @if (remoteFailed()) {\r\n <div class=\"nxe-datagrid-remote-error\" role=\"alert\">\r\n <span class=\"nxe-datagrid-remote-error-text\">{{ t('grid.remote.error') }}</span>\r\n <button type=\"button\" class=\"nxe-datagrid-remote-retry\" (click)=\"retryRemote()\">{{ t('grid.remote.retry') }}</button>\r\n </div>\r\n } @else if (!loading()) {\r\n <span class=\"nxe-datagrid-nodata\">{{ noDataText() }}</span>\r\n } @else {\r\n <div class=\"nxe-datagrid-loadpanel\"><span class=\"nxe-grid-spinner\"></span></div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n @if (pagerVisible()) {\r\n <!-- dx's pager (pagination/content.js) under dx's class names: the page sizes float left;\r\n the pages float right - the info text, then prev, the page buttons and next. dx\r\n renders div[role=button]s; these are real buttons with dx's labels. The prev/next\r\n buttons are empty: the chevron is drawn by CSS. At either end dx only takes the\r\n button out of the tab order (page_index_selector.js), so it keeps focus rather than\r\n dropping it as a disabled button would; a click there is a no-op (goToPage clamps). -->\r\n <div\r\n class=\"nxe-datagrid-pager nxe-pager nxe-grid-pager\"\r\n role=\"navigation\"\r\n [attr.aria-label]=\"pagerCmp()?.label() ?? t('grid.pager.navigation')\">\r\n @if (pagerCmp()?.showPageSizeSelector()) {\r\n <div class=\"nxe-page-sizes\">\r\n @if (pagerCompact()) {\r\n @if (pageSizes().length) {\r\n <select class=\"nxe-pager-size\" [attr.aria-label]=\"t('grid.pager.pageSize')\" (change)=\"onPageSizeSelect($event)\">\r\n @for (s of pageSizes(); track $index) {\r\n <option [value]=\"s\" [selected]=\"s === effectivePageSize()\">{{ s }}</option>\r\n }\r\n </select>\r\n }\r\n } @else {\r\n @for (s of pageSizes(); track $index; let first = $first) {\r\n <button\r\n type=\"button\"\r\n class=\"nxe-page-size\"\r\n [class.nxe-selection]=\"s === effectivePageSize()\"\r\n [class.nxe-first-child]=\"first\"\r\n [attr.aria-label]=\"t('grid.pager.itemsPerPage', s)\"\r\n (click)=\"changePageSize(s)\">{{ s }}</button>\r\n }\r\n }\r\n </div>\r\n }\r\n <div class=\"nxe-pages\">\r\n @if (pagerCmp()?.showInfo()) {\r\n <div class=\"nxe-info nxe-pager-info\">{{ pageInfo() }}</div>\r\n }\r\n <div class=\"nxe-page-indexes nxe-pager-pages\">\r\n @if (pagerNavVisible()) {\r\n <button\r\n type=\"button\"\r\n class=\"nxe-navigate-button nxe-prev-button nxe-pager-nav\"\r\n [class.nxe-button-disable]=\"currentPage() === 0\"\r\n [attr.aria-disabled]=\"currentPage() === 0 ? 'true' : null\"\r\n [attr.tabindex]=\"currentPage() === 0 ? -1 : null\"\r\n [attr.aria-label]=\"t('grid.pager.previous')\"\r\n (click)=\"prevPage()\"></button>\r\n }\r\n @for (item of pageItems(); track item.key) {\r\n @if (item.separator) {\r\n <div class=\"nxe-separator\">. . .</div>\r\n } @else {\r\n <button\r\n type=\"button\"\r\n class=\"nxe-page nxe-pager-page\"\r\n [class.nxe-selection]=\"item.page === currentPage()\"\r\n [class.nxe-active]=\"item.page === currentPage()\"\r\n [attr.aria-label]=\"t('grid.pager.page', item.page + 1)\"\r\n [attr.aria-current]=\"item.page === currentPage() ? 'page' : null\"\r\n (click)=\"goToPage(item.page)\">{{ item.page + 1 }}</button>\r\n }\r\n }\r\n @if (pagerNavVisible()) {\r\n <button\r\n type=\"button\"\r\n class=\"nxe-navigate-button nxe-next-button nxe-pager-nav\"\r\n [class.nxe-button-disable]=\"currentPage() >= pageCount() - 1\"\r\n [attr.aria-disabled]=\"currentPage() >= pageCount() - 1 ? 'true' : null\"\r\n [attr.tabindex]=\"currentPage() >= pageCount() - 1 ? -1 : null\"\r\n [attr.aria-label]=\"t('grid.pager.next')\"\r\n (click)=\"nextPage()\"></button>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n</div>\r\n\r\n<!-- Inline cell/row editor (dxo-editing, orbit addition). `mode: 'cell'` commits on blur/Enter;\r\n `mode: 'row'` buffers on input until the command column's Save. -->\r\n<ng-template #cellEditor let-row=\"row\" let-col=\"col\" let-mode=\"mode\">\r\n @if (col.lookup(); as lookup) {\r\n <select\r\n class=\"nxe-texteditor-input nxe-grid-cell-editor\"\r\n [value]=\"editorInputValue(row, col)\"\r\n (click)=\"$event.stopPropagation()\"\r\n (change)=\"\r\n mode === 'cell'\r\n ? commitCellEdit(row, col, $any($event.target).value)\r\n : bufferRowEdit(col, $any($event.target).value)\r\n \">\r\n <option value=\"\"></option>\r\n @for (item of lookup.dataSource; track $index) {\r\n <option [value]=\"item[lookup.valueExpr ?? 'lovCode']\">\r\n {{ item[lookup.displayExpr ?? 'lovName'] }}\r\n </option>\r\n }\r\n </select>\r\n } @else if (colType(col) === 'boolean') {\r\n <!-- dx's checkbox editor, the same box as the resting boolean cell (never the browser's\r\n native checkbox). Cell mode toggles resting cells in place and never gets here; this\r\n is the row being edited, and a click or Space only buffers until Save. It takes focus\r\n as dx's dxCheckBox does (tabIndex 0, Space toggles: check_box.js:125), so Tab reaches\r\n it with the row's other editors. -->\r\n <span\r\n class=\"nxe-grid-checkbox nxe-grid-checkbox-readonly nxe-checkbox nxe-datagrid-checkbox-size nxe-grid-cell-editor-check\"\r\n role=\"checkbox\"\r\n tabindex=\"0\"\r\n [class.nxe-grid-checkbox-left]=\"align(col) === 'left'\"\r\n [class.nxe-grid-checkbox-right]=\"align(col) === 'right'\"\r\n [class.nxe-checked]=\"editorChecked(row, col)\"\r\n [attr.aria-checked]=\"editorChecked(row, col)\"\r\n (click)=\"toggleEditorCheck(row, col, mode, $event)\"\r\n (keydown.space)=\"toggleEditorCheck(row, col, mode, $event)\">\r\n <span class=\"nxe-grid-checkbox-box\"></span>\r\n </span>\r\n } @else {\r\n <input\r\n type=\"text\"\r\n class=\"nxe-texteditor-input nxe-grid-cell-editor\"\r\n [value]=\"editorInputValue(row, col)\"\r\n (click)=\"$event.stopPropagation()\"\r\n (keydown.enter)=\"mode === 'cell' && commitCellEdit(row, col, $any($event.target).value)\"\r\n (keydown.escape)=\"mode === 'cell' && cancelCellEdit()\"\r\n (blur)=\"mode === 'cell' && commitCellEdit(row, col, $any($event.target).value)\"\r\n (input)=\"mode === 'row' && bufferRowEdit(col, $any($event.target).value)\" />\r\n }\r\n</ng-template>\r\n\r\n<!-- A header cell's indicator block (dx .dx-column-indicators). dx creates the sort span first and\r\n the funnel second; the funnel is prepended when the column is right-aligned\r\n (m_header_filter_core.js _renderIndicator), so a right-aligned column reads [funnel][sort] and\r\n every other [sort][funnel]. An unsorted sortable column keeps an empty `nxe-sort-none` span, as\r\n dx does: hidden in the fixed table, measured by the best-fit pass. -->\r\n<ng-template\r\n #headerIndicators\r\n let-col\r\n let-dir=\"dir\"\r\n let-sortSlot=\"sortSlot\"\r\n let-hf=\"hf\"\r\n let-hfFirst=\"hfFirst\"\r\n let-float=\"float\">\r\n <div class=\"nxe-column-indicators\" role=\"presentation\" [style.float]=\"float\">\r\n @for (kind of hfFirst ? ['funnel', 'sort'] : ['sort', 'funnel']; track kind) {\r\n @if (kind === 'funnel') {\r\n @if (hf) {\r\n <span\r\n class=\"nxe-grid-hf-icon\"\r\n [class.nxe-hf-active]=\"hasHeaderFilter(col)\"\r\n (click)=\"openHeaderFilter(col, $event)\"></span>\r\n }\r\n } @else if (sortSlot) {\r\n <span\r\n class=\"nxe-sort\"\r\n [class.nxe-sort-none]=\"!dir\"\r\n [class.nxe-sort-up]=\"dir === 'asc'\"\r\n [class.nxe-sort-down]=\"dir === 'desc'\"></span>\r\n }\r\n }\r\n </div>\r\n</ng-template>\r\n\r\n<!-- The header-filter popup (#hfPanel) moved to nxe-header-filter-panel.component.html \u2014\r\n openHeaderFilter attaches it through openNxeHeaderFilterOverlay (pivot parity Phase 3a). -->\r\n\r\n<!-- Filter-row operation-chooser dropdown (dx filter menu) -->\r\n<ng-template #frOpMenu>\r\n <div class=\"nxe-fr-op-menu nxe-datagrid\">\r\n @for (item of frMenuOps(); track item.op) {\r\n <div\r\n class=\"nxe-menu-item\"\r\n [class.nxe-menu-item-selected]=\"frMenuColumn() && frOp(frMenuColumn()!) === item.op\"\r\n (click)=\"selectFilterOp(item.op)\">\r\n <span class=\"nxe-icon nxe-icon-filter-operation-{{ opIconSlug(item.op) }}\"></span>\r\n <span class=\"nxe-menu-item-text\">{{ item.label }}</span>\r\n </div>\r\n }\r\n <!-- dx draws Reset with the chooser's own magnifier. -->\r\n <div class=\"nxe-menu-item nxe-fr-reset\" (click)=\"resetFilterRow()\">\r\n <span class=\"nxe-icon nxe-icon-filter-operation-default\"></span>\r\n <span class=\"nxe-menu-item-text\">{{ t('grid.filterRow.reset') }}</span>\r\n </div>\r\n </div>\r\n</ng-template>\r\n\r\n", styles: ["@charset \"UTF-8\";.nxe-data-grid{display:block}.nxe-data-grid .nxe-datagrid{height:100%;position:relative;white-space:normal;cursor:default;color:var(--nxe-ui-text, #333)}.nxe-data-grid .nxe-gridbase-container{display:flex;flex-direction:column;height:100%;line-height:inherit;min-height:inherit;max-height:inherit}.nxe-data-grid .nxe-gridbase-container>*{flex:0 0 auto}.nxe-data-grid .nxe-datagrid-rowsview{position:relative;overflow:auto;flex:1 1 auto;min-height:0;background-color:var(--nxe-ui-surface, #fff)}.nxe-data-grid .nxe-datagrid-content{display:block;position:relative;min-height:100%}.nxe-data-grid .nxe-datagrid-rowsview .nxe-datagrid-content{overflow-anchor:none}.nxe-data-grid .nxe-datagrid-rowsview.nxe-empty{display:flex;flex-direction:column;height:auto}.nxe-data-grid .nxe-datagrid-rowsview.nxe-empty>.nxe-datagrid-content{flex:none;min-height:0}.nxe-data-grid .nxe-datagrid-rowsview.nxe-empty>.nxe-datagrid-empty-body{position:sticky;inset-inline-start:0;flex:1 1 var(--nxe-ui-grid-empty-height, 100px);min-height:0}.nxe-data-grid table.nxe-datagrid-table{display:table;width:100%;table-layout:fixed;border-collapse:collapse;margin:0;overflow-x:clip}.nxe-data-grid .nxe-datagrid-content .nxe-datagrid-table{max-width:none}.nxe-data-grid .nxe-datagrid-headers{display:table-header-group;position:sticky;top:0;z-index:1;color:var(--nxe-ui-neutral-600, #757575);font-weight:400;-webkit-user-select:none;user-select:none;-webkit-touch-callout:none;touch-action:pinch-zoom;background:var(--nxe-ui-surface, #fff);border-bottom:0}.nxe-data-grid tbody{display:table-row-group}.nxe-data-grid .nxe-row{display:table-row}.nxe-data-grid .nxe-datagrid-rowsview tbody>.nxe-row{border-top:var(--nxe-ui-grid-line-width, 1px) solid transparent;border-bottom:var(--nxe-ui-grid-line-width, 1px) solid transparent}.nxe-data-grid .nxe-datagrid-headers>.nxe-row{border-top:0;border-bottom:0}.nxe-data-grid .nxe-datagrid-headers:before{content:none}.nxe-data-grid .nxe-datagrid-headers>.nxe-row>td{border-bottom:var(--nxe-ui-grid-line-width, 1px) solid var(--nxe-ui-grid-header-line-color, #ddd)}.nxe-data-grid .nxe-datagrid-rowsview.nxe-datagrid-after-headers{border-top:0}.nxe-data-grid .nxe-datagrid .nxe-row-lines>td{border-bottom:var(--nxe-ui-grid-line-width, 1px) solid var(--nxe-ui-grid-row-line-color, #ddd)}.nxe-data-grid .nxe-datagrid-rowsview .nxe-row.nxe-row-lines:first-child,.nxe-data-grid .nxe-datagrid-rowsview .nxe-row.nxe-row-lines:first-child>td{border-top:none}.nxe-data-grid .nxe-datagrid .nxe-column-lines>td{border-left:var(--nxe-ui-grid-line-width, 1px) solid var(--nxe-ui-grid-column-line-color, #ddd);border-right:var(--nxe-ui-grid-line-width, 1px) solid var(--nxe-ui-grid-column-line-color, #ddd)}.nxe-data-grid .nxe-datagrid .nxe-column-lines>td:first-child{border-left:none}.nxe-data-grid .nxe-datagrid .nxe-column-lines>td:last-child{border-right:none}.nxe-data-grid .nxe-datagrid-content .nxe-datagrid-table .nxe-row>td:first-child{border-left:0}.nxe-data-grid .nxe-datagrid-content .nxe-datagrid-table .nxe-row>td.nxe-datagrid-group-space{border-right:none}.nxe-data-grid .nxe-datagrid-content .nxe-datagrid-table .nxe-row>td.nxe-datagrid-group-space+td{border-left:none}.nxe-data-grid .nxe-datagrid-rowsview .nxe-row.nxe-group-row>td{border-top:var(--nxe-ui-grid-line-width, 1px) solid var(--nxe-ui-grid-row-line-color, #ddd);border-bottom:var(--nxe-ui-grid-line-width, 1px) solid var(--nxe-ui-grid-row-line-color, #ddd)}.nxe-data-grid .nxe-datagrid-rowsview .nxe-row.nxe-group-row:first-child,.nxe-data-grid .nxe-datagrid-rowsview .nxe-row.nxe-group-row:first-child>td{border-top:none}.nxe-data-grid .nxe-datagrid-rowsview .nxe-group-row.nxe-row>td{border-left-color:transparent;border-right-color:transparent}.nxe-data-grid .nxe-row.nxe-datagrid-group-footer>td{border-top:var(--nxe-ui-grid-line-width, 1px) solid var(--nxe-ui-grid-row-line-color, #ddd);background-color:var(--nxe-ui-surface, #fff);border-left-width:0;border-right-width:0}.nxe-data-grid .nxe-row.nxe-datagrid-group-footer.nxe-column-lines{border-bottom:var(--nxe-ui-grid-line-width, 1px) solid var(--nxe-ui-grid-row-line-color, #ddd)}.nxe-data-grid .nxe-datagrid-rowsview .nxe-row.nxe-edit-row>td{border-top:var(--nxe-ui-grid-line-width, 1px) solid var(--nxe-ui-grid-row-line-color, #ddd);border-bottom:var(--nxe-ui-grid-line-width, 1px) solid var(--nxe-ui-grid-row-line-color, #ddd)}.nxe-data-grid .nxe-datagrid-rowsview .nxe-row.nxe-edit-row:first-child>td{border-top-width:0;border-bottom:var(--nxe-ui-grid-line-width, 1px) solid var(--nxe-ui-grid-row-line-color, #ddd)}.nxe-data-grid .nxe-datagrid-rowsview .nxe-selection.nxe-row:not(.nxe-row-focused):not(.nxe-row-lines)>td,.nxe-data-grid .nxe-datagrid-rowsview .nxe-selection.nxe-row:not(.nxe-row-focused):hover:not(.nxe-row-lines)>td{border-top:var(--nxe-ui-grid-line-width, 1px) solid var(--nxe-ui-neutral-150, #e6e6e6);border-bottom:var(--nxe-ui-grid-line-width, 1px) solid var(--nxe-ui-neutral-150, #e6e6e6)}.nxe-data-grid .nxe-datagrid-rowsview .nxe-selection.nxe-row:not(.nxe-row-focused).nxe-column-lines>td,.nxe-data-grid .nxe-datagrid-rowsview .nxe-selection.nxe-row:not(.nxe-row-focused):hover.nxe-column-lines>td{border-left-color:var(--nxe-ui-grid-column-line-color, #ddd);border-right-color:var(--nxe-ui-grid-column-line-color, #ddd)}.nxe-data-grid .nxe-datagrid-rowsview .nxe-selection.nxe-row:not(.nxe-row-focused).nxe-row-lines>td,.nxe-data-grid .nxe-datagrid-rowsview .nxe-selection.nxe-row:not(.nxe-row-focused):hover.nxe-row-lines>td{border-bottom-color:var(--nxe-ui-grid-row-line-color, #ddd)}.nxe-data-grid .nxe-datagrid .nxe-row-alt>td{background-color:var(--nxe-ui-grid-alt-row-bg, #f5f5f5)}.nxe-data-grid .nxe-datagrid .nxe-row-alt.nxe-row:not(.nxe-row-removed){border-bottom-color:transparent}.nxe-data-grid .nxe-datagrid-borders>.nxe-gridbase-container>.nxe-datagrid-rowsview{border:var(--nxe-ui-grid-line-width, 1px) solid var(--nxe-ui-grid-border-color, #ddd)}.nxe-data-grid .nxe-datagrid-borders>.nxe-gridbase-container>.nxe-datagrid-header-panel{border-bottom:0}.nxe-data-grid .nxe-datagrid-borders>.nxe-gridbase-container>.nxe-grid-pager{margin-top:-1px}.nxe-data-grid .nxe-datagrid-borders>.nxe-gridbase-container>.nxe-datagrid-rowsview.nxe-last-row-border>.nxe-datagrid-content>.nxe-datagrid-table>tbody>.nxe-data-row:nth-last-child(2),.nxe-data-grid .nxe-datagrid-borders>.nxe-gridbase-container>.nxe-datagrid-rowsview.nxe-last-row-border>.nxe-datagrid-content>.nxe-datagrid-table>tbody>.nxe-data-row:nth-last-child(2)>td{border-bottom-width:0}.nxe-data-grid .nxe-datagrid-nodata{position:absolute;top:50%;left:50%;width:100%;text-align:center;transform:translate(-50%,-50%);color:var(--nxe-ui-text, #333);font-size:var(--nxe-ui-grid-nodata-font-size, 17px);opacity:.7;pointer-events:none}.nxe-data-grid .nxe-datagrid-loadpanel{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:var(--nxe-ui-scrim, rgba(255, 255, 255, .5));z-index:2}.nxe-data-grid .nxe-datagrid-remote-error{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--nxe-ui-space-2, 8px);color:var(--nxe-ui-invalid, #d9534f)}.nxe-data-grid .nxe-datagrid-remote-retry{padding:var(--nxe-ui-space-1, 4px) var(--nxe-ui-space-3, 12px);border:1px solid var(--nxe-ui-border, #ddd);border-radius:var(--nxe-ui-radius, 4px);background:var(--nxe-ui-surface, #fff);color:var(--nxe-ui-text, #333);font:inherit;cursor:pointer}.nxe-data-grid .nxe-grid-spinner{width:32px;height:32px;border:3px solid var(--nxe-ui-spinner-track, rgba(48, 55, 76, .2));border-top-color:var(--nxe-ui-ink-strong, #30374c);border-radius:50%;animation:nxe-grid-spin .8s linear infinite}.nxe-data-grid .nxe-header-row>td{position:relative;cursor:default;-webkit-user-select:none;user-select:none;white-space:nowrap;overflow:clip;overflow-clip-margin:4px}.nxe-data-grid .nxe-header-row>td.nxe-datagrid-action{cursor:pointer}.nxe-data-grid .nxe-header-row>td>.nxe-datagrid-text-content{display:inline-block;vertical-align:top;white-space:normal;overflow:hidden;text-overflow:ellipsis}.nxe-data-grid .nxe-datagrid-nowrap .nxe-header-row>td>.nxe-datagrid-text-content{white-space:nowrap}.nxe-data-grid .nxe-header-row>td>.nxe-datagrid-text-content:not(.nxe-sort-indicator):not(.nxe-header-filter-indicator){max-width:100%}.nxe-data-grid .nxe-header-row .nxe-sort-indicator,.nxe-data-grid .nxe-header-row .nxe-header-filter-indicator{max-width:calc(100% - 17px)}.nxe-data-grid .nxe-header-row .nxe-sort-indicator.nxe-text-content-alignment-left,.nxe-data-grid .nxe-header-row .nxe-header-filter-indicator.nxe-text-content-alignment-left{margin-right:3px}.nxe-data-grid .nxe-header-row .nxe-sort-indicator.nxe-text-content-alignment-right,.nxe-data-grid .nxe-header-row .nxe-header-filter-indicator.nxe-text-content-alignment-right{margin-left:3px}.nxe-data-grid .nxe-header-row .nxe-sort-indicator.nxe-text-content-alignment-left.nxe-text-content-alignment-right,.nxe-data-grid .nxe-header-row .nxe-header-filter-indicator.nxe-text-content-alignment-left.nxe-text-content-alignment-right{max-width:calc(100% - 34px)}.nxe-data-grid .nxe-header-row .nxe-sort-indicator.nxe-header-filter-indicator{max-width:calc(100% - 31px)}.nxe-data-grid .nxe-header-row .nxe-sort-indicator.nxe-header-filter-indicator.nxe-text-content-alignment-left.nxe-text-content-alignment-right{max-width:calc(100% - 62px)}.nxe-data-grid .nxe-column-indicators{display:inline-block;vertical-align:top;white-space:nowrap;scale:var(--nxe-ui-grid-indicator-scale, 1);padding-top:var(--nxe-ui-grid-indicator-offset, 0px)}.nxe-data-grid .nxe-column-indicators.nxe-visibility-hidden{visibility:hidden}.nxe-data-grid .nxe-column-indicator-spacer{display:inline-block;width:14px;height:14px;vertical-align:top}.nxe-data-grid .nxe-datagrid-table-fixed .nxe-column-indicators .nxe-sort.nxe-sort-none,.nxe-data-grid .nxe-datagrid-table-fixed .nxe-column-indicators .nxe-column-indicator-spacer-none{display:none}.nxe-data-grid .nxe-datagrid-best-fit .nxe-column-indicators{float:none!important}.nxe-data-grid .nxe-datagrid-best-fit .nxe-column-indicators>span{width:14px}.nxe-data-grid .nxe-datagrid-best-fit .nxe-column-indicators .nxe-sort.nxe-sort-none,.nxe-data-grid .nxe-datagrid-best-fit .nxe-column-indicators .nxe-column-indicator-spacer-none{display:inline-block!important}.nxe-data-grid .nxe-datagrid-best-fit .nxe-header-row .nxe-datagrid-text-content{max-width:none!important}.nxe-data-grid .nxe-datagrid-best-fit .nxe-text-content-alignment-left{margin-right:3px}.nxe-data-grid .nxe-datagrid-best-fit .nxe-text-content-alignment-right{margin-left:3px}.nxe-data-grid .nxe-sort{display:inline-block;position:relative;width:14px;height:14px;vertical-align:top}.nxe-data-grid .nxe-sort:after{content:\"\";position:absolute;left:0;top:0;width:14px;height:14px;background-color:currentColor}@media(forced-colors:active){.nxe-data-grid .nxe-sort:after{forced-color-adjust:none;background-color:CanvasText}}.nxe-data-grid .nxe-sort.nxe-sort-up:after{clip-path:polygon(6.5px 0,9px 3px,7px 3px,7px 14px,6px 14px,6px 3px,4px 3px)}.nxe-data-grid .nxe-sort.nxe-sort-down:after{clip-path:polygon(6px 0,7px 0,7px 11px,9px 11px,6.5px 14px,4px 11px,6px 11px)}.nxe-data-grid .nxe-sort.nxe-sort-none:after{content:none}.nxe-data-grid .nxe-datagrid-group-panel .nxe-sort{color:var(--nxe-ui-neutral-600, #757575)}.nxe-data-grid .nxe-datagrid-group-panel .nxe-group-panel-item .nxe-sort{margin-left:6px}.nxe-data-grid .nxe-grid-hf-icon{display:inline-block;position:relative;width:14px;height:14px;cursor:pointer;vertical-align:top;opacity:.5}.nxe-data-grid .nxe-grid-hf-icon:before{content:\"\";position:absolute;inset:0;background-color:var(--nxe-ui-neutral-600, #757575);clip-path:polygon(0 0,14px 0,14px 3px,9px 8px,9px 14px,5px 14px,5px 8px,0 3px)}@media(forced-colors:active){.nxe-data-grid .nxe-grid-hf-icon:before{forced-color-adjust:none;background-color:CanvasText}}.nxe-data-grid .nxe-grid-hf-icon:after{content:\"\";position:absolute;inset:0;margin:-7px}.nxe-data-grid .nxe-grid-hf-icon.nxe-hf-active{opacity:1}.nxe-data-grid .nxe-grid-pager{display:block;flex:none;overflow:hidden;width:100%;padding:9px 0;border-top:1px solid var(--nxe-ui-grid-border-color, #ddd);color:var(--nxe-ui-text, #333);line-height:normal;-webkit-user-select:none;user-select:none}.nxe-data-grid .nxe-grid-pager .nxe-page-sizes{float:left}.nxe-data-grid .nxe-grid-pager .nxe-pages{float:right;white-space:nowrap}.nxe-data-grid .nxe-grid-pager .nxe-pages .nxe-info{display:inline-block;margin-right:9px;opacity:.7}.nxe-data-grid .nxe-grid-pager .nxe-pages .nxe-page-indexes{display:inline-block}.nxe-data-grid .nxe-grid-pager .nxe-pages .nxe-navigate-button,.nxe-data-grid .nxe-grid-pager .nxe-pages .nxe-page,.nxe-data-grid .nxe-grid-pager .nxe-page-sizes .nxe-page-size{appearance:none;margin:0;background:transparent;color:inherit;font:inherit;text-align:center;outline:0;cursor:pointer}.nxe-data-grid .nxe-grid-pager .nxe-pages .nxe-navigate-button:focus-visible,.nxe-data-grid .nxe-grid-pager .nxe-pages .nxe-page:focus-visible,.nxe-data-grid .nxe-grid-pager .nxe-page-sizes .nxe-page-size:focus-visible{outline-width:2px;outline-style:solid;outline-color:var(--nxe-ui-focus-ring-color, #5c95c5);outline-offset:-2px}.nxe-data-grid .nxe-grid-pager .nxe-pages .nxe-page{display:inline-block;margin:0 1px 0 5px;padding:7px 9px 8px;border:1px solid transparent;border-radius:3px}.nxe-data-grid .nxe-grid-pager .nxe-page-sizes .nxe-page-size{display:inline-block;margin:0 1px 0 4px;padding:7px 9px 8px 10px;border:1px solid transparent;border-radius:3px}.nxe-data-grid .nxe-grid-pager .nxe-page-sizes .nxe-page-size.nxe-first-child,.nxe-data-grid .nxe-grid-pager .nxe-page-sizes .nxe-page-size:first-child{margin-left:1px}.nxe-data-grid .nxe-grid-pager .nxe-page-sizes .nxe-selection,.nxe-data-grid .nxe-grid-pager .nxe-pages .nxe-selection{cursor:inherit;text-shadow:none;color:var(--nxe-ui-text, #333);border-color:transparent;background-color:var(--nxe-ui-neutral-230, #d4d4d4)}.nxe-data-grid .nxe-grid-pager .nxe-pages .nxe-separator{display:inline-block;padding:0 8px}.nxe-data-grid .nxe-grid-pager .nxe-pages .nxe-navigate-button{position:relative;display:inline-block;box-sizing:content-box;width:9px;height:17px;padding:9px 13px;border:0;vertical-align:top}.nxe-data-grid .nxe-grid-pager .nxe-pages .nxe-navigate-button:before{content:\"\";position:absolute;display:block;box-sizing:border-box;top:50%;left:50%;width:21px;height:21px;margin:-10.5px 0 0 -10.5px;background-color:currentColor}@media(forced-colors:active){.nxe-data-grid .nxe-grid-pager .nxe-pages .nxe-navigate-button:before{forced-color-adjust:none;background-color:CanvasText}}.nxe-data-grid .nxe-grid-pager .nxe-pages .nxe-navigate-button.nxe-button-disable{opacity:.3;cursor:inherit}.nxe-data-grid .nxe-grid-pager .nxe-pages .nxe-prev-button:before{clip-path:polygon(5.75px 10.25px,13.75px 2.25px,15.25px 3.75px,8.75px 10.25px,15.25px 16.75px,13.75px 18.25px)}.nxe-data-grid .nxe-grid-pager .nxe-pages .nxe-next-button:before{clip-path:polygon(15.25px 10.25px,7.25px 2.25px,5.75px 3.75px,12.25px 10.25px,5.75px 16.75px,7.25px 18.25px)}.nxe-data-grid .nxe-grid-pager .nxe-pager-size{padding:2px 6px;border:1px solid var(--nxe-ui-border, #ddd);border-radius:3px;font:inherit}.nxe-data-grid .nxe-datagrid-header-panel{padding:.5rem .75rem;text-align:left;border-bottom:var(--nxe-ui-grid-line-width, 1px) solid var(--nxe-ui-border, #ddd)}.nxe-data-grid .nxe-datagrid-group-panel{display:flex;align-items:center;width:100%;min-height:36px;white-space:nowrap;font-size:var(--nxe-ui-font-size, 14px);touch-action:pinch-zoom}.nxe-data-grid .nxe-datagrid-group-panel .nxe-group-panel-message{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--nxe-ui-neutral-600, #757575);font-weight:400;padding:7px;border-top:1px solid transparent;border-bottom:1px solid transparent;line-height:1.2858}.nxe-data-grid .nxe-datagrid-group-panel .nxe-group-panel-item{display:inline-block;min-width:30px;white-space:nowrap;margin-right:10px;color:var(--nxe-ui-neutral-600, #757575);font-weight:400;border:1px solid var(--nxe-ui-border, #ddd);padding:7px}.nxe-data-grid .nxe-datagrid-group-panel .nxe-block-separator{position:relative;margin-right:10px;border-radius:3px;background-color:var(--nxe-ui-accent, #337ab7)}.nxe-data-grid .nxe-tree-icons{display:inline-block;float:left;white-space:nowrap;vertical-align:top;line-height:1}.nxe-data-grid .nxe-tree-expander,.nxe-data-grid .nxe-tree-expander-stub{display:inline-block;position:relative;width:14px;height:14px;vertical-align:top}.nxe-data-grid .nxe-tree-icons-select{position:relative;padding-right:23px}.nxe-data-grid .nxe-tree-icons .nxe-tree-checkbox{position:absolute;top:50%;right:7px;margin:-8px 0 0}.nxe-data-grid .nxe-header-row>.nxe-tree-select-all{position:relative;padding-left:44px}.nxe-data-grid .nxe-header-row>.nxe-tree-select-all>.nxe-grid-checkbox{position:absolute;left:21px;margin:1px 0 0}.nxe-data-grid .nxe-tree-expander{height:18px;margin-right:4px;color:var(--nxe-ui-neutral-450, #959595);cursor:pointer}.nxe-data-grid .nxe-tree-expander:before{content:\"\";position:absolute;left:0;top:8px;width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;border-top:4px solid currentColor}.nxe-data-grid .nxe-tree-expander.nxe-collapsed:before{left:1px;top:6px;border-top:4px solid transparent;border-right:0;border-bottom:4px solid transparent;border-left:4px solid currentColor}.nxe-data-grid .nxe-group-row{font-weight:700;color:var(--nxe-ui-grid-group-row-color, #222)}.nxe-data-grid .nxe-group-row>td{background-color:var(--nxe-ui-neutral-60, #f7f7f7)}.nxe-data-grid .nxe-group-row>td.nxe-group-cell{font-weight:700}.nxe-data-grid .nxe-group-row>td.nxe-datagrid-summary-item,.nxe-data-grid .nxe-group-footer-row>td.nxe-datagrid-summary-item{font-weight:700;color:var(--nxe-ui-grid-summary-color, rgba(51, 51, 51, .7))}.nxe-data-grid .nxe-datagrid-group-opened,.nxe-data-grid .nxe-datagrid-group-closed{display:block;position:relative;width:100%;height:18px;padding:0;color:var(--nxe-ui-neutral-450, #959595);cursor:pointer}.nxe-data-grid .nxe-datagrid-group-opened:before,.nxe-data-grid .nxe-datagrid-group-closed:before{content:\"\"!important;position:absolute;left:50%;right:auto;width:0;height:0}.nxe-data-grid .nxe-datagrid-group-opened:before{top:7px;margin-left:-4px;border-left:4px solid transparent;border-right:4px solid transparent;border-top:4px solid currentColor}.nxe-data-grid .nxe-datagrid-group-closed:before{top:5px;margin-left:-2px;border-top:4px solid transparent;border-bottom:4px solid transparent;border-left:4px solid currentColor}.nxe-data-grid .nxe-datagrid-total-footer{position:relative}.nxe-data-grid .nxe-datagrid-total-footer>tr>td,.nxe-data-grid tfoot.nxe-datagrid-total-footer td{border-top:1px solid var(--nxe-ui-border, #ddd);padding:6px 4px;font-weight:700;color:var(--nxe-ui-neutral-450, rgb(149, 149, 149))}.nxe-data-grid .nxe-datagrid-filter-row>td{padding:2px 4px}.nxe-data-grid .nxe-grid-filter-input{width:100%;box-sizing:border-box;padding:4px 6px;border:1px solid var(--nxe-ui-border, #ddd);border-radius:2px;font:inherit;color:var(--nxe-ui-text, #333);outline:none}.nxe-data-grid .nxe-grid-filter-input:focus{border-color:var(--nxe-ui-ink-strong, #30374c)}.nxe-data-grid .nxe-datagrid-filter-row select.nxe-grid-filter-input{appearance:none;background:var(--nxe-ui-surface, #fff) url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 4'%3E%3Cpath d='M0 0h8L4 4z' fill='%23333'/%3E%3C/svg%3E\") no-repeat right 12px center/8px 4px;padding-right:34px!important}.nxe-data-grid .nxe-command-select{width:40px;min-width:40px;max-width:40px;text-align:center;vertical-align:middle}.nxe-data-grid .nxe-grid-checkbox{display:block;width:16px;margin:0 auto;padding-bottom:2px;line-height:0;cursor:pointer}.nxe-data-grid .nxe-grid-checkbox.nxe-grid-checkbox-left{margin-left:0}.nxe-data-grid .nxe-grid-checkbox.nxe-grid-checkbox-right{margin-right:0}.nxe-data-grid .nxe-grid-checkbox:focus{outline:none}.nxe-data-grid .nxe-grid-checkbox:focus .nxe-grid-checkbox-box{border-color:var(--nxe-ui-accent, #337ab7)}.nxe-data-grid .nxe-grid-checkbox .nxe-grid-checkbox-box{display:block;position:relative;width:16px;height:16px;box-sizing:border-box;border:1px solid var(--nxe-ui-border, #ddd);border-radius:2px;background:var(--nxe-ui-surface, #fff)}.nxe-data-grid .nxe-grid-checkbox:not(.nxe-state-readonly):hover .nxe-grid-checkbox-box{border-color:var(--nxe-ui-checkbox-hover-border, #265a87)}.nxe-data-grid .nxe-grid-checkbox.nxe-checked .nxe-grid-checkbox-box:after{content:\"\";position:absolute;inset:-1px;background-color:var(--nxe-ui-accent, #337ab7);clip-path:polygon(6.75px 12px,2.5px 7.75px,3.5px 6.75px,6.75px 10px,12.75px 4px,13.75px 5px)}@media(forced-colors:active){.nxe-data-grid .nxe-grid-checkbox.nxe-checked .nxe-grid-checkbox-box:after{forced-color-adjust:none;background-color:CanvasText}}.nxe-data-grid .nxe-grid-checkbox.nxe-indeterminate .nxe-grid-checkbox-box:after{content:\"\";position:absolute;left:4px;top:4px;width:6px;height:6px;background-color:var(--nxe-ui-accent, #337ab7)}@media(forced-colors:active){.nxe-data-grid .nxe-grid-checkbox.nxe-indeterminate .nxe-grid-checkbox-box:after{forced-color-adjust:none;background-color:CanvasText}}.nxe-data-grid .nxe-grid-checkbox.nxe-state-readonly{cursor:default}.nxe-data-grid .nxe-grid-checkbox.nxe-state-readonly .nxe-grid-checkbox-box{border-color:var(--nxe-ui-neutral-80, #f4f4f4);background-color:var(--nxe-ui-surface, #fff)}@keyframes nxe-grid-spin{to{transform:rotate(360deg)}}:where(.nxe-data-grid .nxe-row)>td{padding:var(--nxe-ui-grid-cell-padding, 7px);vertical-align:var(--nxe-ui-grid-cell-vertical-align, middle)}:where(.nxe-data-grid .nxe-row)>td:first-child{padding-left:var(--nxe-ui-grid-edge-padding, var(--nxe-ui-grid-cell-padding, 7px))}:where(.nxe-data-grid .nxe-row)>td:last-child{padding-right:var(--nxe-ui-grid-edge-padding, var(--nxe-ui-grid-cell-padding, 7px))}:where(.nxe-data-grid) .nxe-header-row{background-color:var(--nxe-ui-grid-header-bg, transparent);color:var(--nxe-ui-grid-header-color, inherit)}.nxe-datagrid-rowsview .nxe-row{border-top:1px solid transparent;border-bottom:1px solid transparent}.nxe-datagrid-rowsview .nxe-datagrid-table .nxe-row.nxe-freespace-row,.nxe-datagrid-rowsview .nxe-datagrid-table .nxe-row.nxe-virtual-row{border-top:0;border-bottom:0}.nxe-datagrid-rowsview .nxe-datagrid-table .nxe-row.nxe-freespace-row>td,.nxe-datagrid-rowsview .nxe-datagrid-table .nxe-row.nxe-virtual-row>td{padding-top:0;padding-bottom:0}.nxe-datagrid-nowrap{white-space:nowrap}.nxe-datagrid-rowsview :where(tbody)>.nxe-row>td{overflow:hidden;text-overflow:ellipsis}.nxe-datagrid-rowsview .nxe-selection.nxe-row:not(.nxe-row-focused)>td,.nxe-datagrid-rowsview .nxe-selection.nxe-row:not(.nxe-row-focused):hover>td{background-color:var(--nxe-ui-neutral-150, #e6e6e6);color:var(--nxe-ui-text, #333)}.nxe-data-grid .nxe-data-row>td.nxe-editor-cell input:not([type=hidden]){text-align:inherit}.nxe-datagrid-content .nxe-datagrid-table .nxe-row .nxe-command-expand{padding:0;width:30px;min-width:30px;max-width:30px;-webkit-user-select:none;user-select:none}.nxe-datagrid-table .nxe-row .nxe-command-expand.nxe-datagrid-group-space{padding-top:var(--nxe-ui-grid-expand-offset, 7px)}.nxe-datagrid-content .nxe-datagrid-table .nxe-row>td.nxe-datagrid-group-space{vertical-align:top}.nxe-datagrid-content .nxe-datagrid-table .nxe-row .nxe-command-select{padding:0}.nxe-datagrid-rowsview .nxe-master-detail-row>.nxe-master-detail-cell{padding:20px}.nxe-datagrid-rowsview .nxe-master-detail-row .nxe-master-detail-cell:not(.nxe-row-lines){border-top:var(--nxe-ui-grid-line-width, 1px) solid var(--nxe-ui-grid-row-line-color, #ddd);border-bottom:var(--nxe-ui-grid-line-width, 1px) solid var(--nxe-ui-grid-row-line-color, #ddd)}.nxe-datagrid-rowsview .nxe-master-detail-row:not(.nxe-datagrid-edit-form) .nxe-master-detail-cell{background-color:var(--nxe-ui-neutral-50, #fafafa)}.nxe-grid-cell-editor{width:100%;box-sizing:border-box;border:1px solid var(--nxe-ui-accent, #337ab7);padding:2px 4px;font:inherit;background:var(--nxe-ui-surface, #fff);outline:none}.nxe-data-grid .nxe-command-edit{text-align:center;white-space:nowrap}.nxe-datagrid .nxe-link{text-decoration:underline;cursor:pointer;color:var(--nxe-ui-accent, #337ab7)}.nxe-datagrid-content .nxe-datagrid-table .nxe-row .nxe-command-edit .nxe-link{display:inline-block}.nxe-datagrid .nxe-datagrid-content .nxe-datagrid-table .nxe-row .nxe-command-edit .nxe-link:not(.nxe-link-icon){margin:0 2px;min-width:20px}.nxe-data-row.nxe-state-hover:not(.nxe-selection):not(.nxe-row-modified):not(.nxe-row-inserted):not(.nxe-row-removed):not(.nxe-edit-row)>td:not(.nxe-focused) .nxe-link,.nxe-datagrid-rowsview .nxe-selection.nxe-row:hover>td .nxe-link,.nxe-datagrid-rowsview .nxe-selection>td .nxe-link{color:inherit}.nxe-data-grid .nxe-header-row td.nxe-datagrid-drag-action,.nxe-data-grid .nxe-group-panel-item{touch-action:none}.nxe-data-grid .nxe-drag-source{opacity:.5}.nxe-data-grid .nxe-datagrid-filter-row .nxe-menu ul{list-style:none;margin:0;padding:0}.nxe-data-grid .nxe-datagrid-filter-row .nxe-editor-with-menu{display:flex;position:relative}.nxe-data-grid .nxe-datagrid-filter-row .nxe-editor-with-menu>.nxe-menu{position:absolute;top:0;left:0;bottom:0;height:auto;z-index:1;cursor:pointer}.nxe-data-grid .nxe-datagrid-filter-row .nxe-editor-with-menu>.nxe-menu ul,.nxe-data-grid .nxe-datagrid-filter-row .nxe-editor-with-menu>.nxe-menu li{height:100%}.nxe-data-grid .nxe-datagrid-filter-row .nxe-editor-with-menu>.nxe-menu .nxe-menu-item{display:flex;align-items:center;box-sizing:border-box;height:100%;padding:1px 8px 0}.nxe-data-grid .nxe-datagrid-filter-row .nxe-editor-with-menu>.nxe-menu .nxe-menu-item-content{display:flex;align-items:center;padding:0}.nxe-data-grid .nxe-datagrid-filter-row .nxe-editor-with-menu .nxe-grid-filter-input{flex:1 1 0;min-width:0}.nxe-data-grid .nxe-datagrid-filter-row .nxe-editor-with-menu>.nxe-grid-filter-input:not(.nxe-fr-second){padding-left:32px!important}.nxe-data-grid .nxe-datagrid-filter-row .nxe-editor-with-menu .nxe-fr-second{padding-left:4px!important;border-left:1px solid var(--nxe-ui-border, #ddd)}.nxe-data-grid .nxe-datagrid-filter-row .nxe-menu-item-content .nxe-icon,.nxe-fr-op-menu .nxe-menu-item .nxe-icon{display:block;flex:0 0 14px;width:14px;height:14px;margin:0;padding:0;font-size:0;line-height:0;color:var(--nxe-ui-neutral-500, #898989);background-color:currentColor;-webkit-mask:var(--nxe-ui-grid-fr-op-glyph, linear-gradient(transparent, transparent)) center/14px 14px no-repeat;mask:var(--nxe-ui-grid-fr-op-glyph, linear-gradient(transparent, transparent)) center/14px 14px no-repeat}@media(forced-colors:active){.nxe-data-grid .nxe-datagrid-filter-row .nxe-menu-item-content .nxe-icon,.nxe-fr-op-menu .nxe-menu-item .nxe-icon{forced-color-adjust:none;background-color:CanvasText}}.nxe-data-grid .nxe-datagrid-filter-row .nxe-menu-item-content .nxe-icon:not(.nxe-icon-filter-operation-default),.nxe-fr-op-menu .nxe-menu-item .nxe-icon:not(.nxe-icon-filter-operation-default){position:relative;top:-1px}.nxe-data-grid .nxe-datagrid-filter-row .nxe-menu-item-content .nxe-icon:before,.nxe-fr-op-menu .nxe-menu-item .nxe-icon:before{content:none}.nxe-icon-filter-operation-default{--nxe-ui-grid-fr-op-glyph: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cg fill='none' stroke='%23000' stroke-width='1.5'%3E%3Ccircle cx='5.5' cy='5.5' r='4'/%3E%3Cpath d='M8.5 8.5l4 4'/%3E%3C/g%3E%3C/svg%3E\")}.nxe-icon-filter-operation-contains{--nxe-ui-grid-fr-op-glyph: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cmask id='m'%3E%3Crect width='14' height='14' fill='%23fff'/%3E%3Cg fill='none' stroke='%23000'%3E%3Ccircle cx='7' cy='9.25' r='1.75'/%3E%3Cpath d='M5.25 3.5v8'/%3E%3C/g%3E%3C/mask%3E%3Crect x='4.5' y='2.5' width='5' height='9.5' mask='url(%23m)'/%3E%3Cg fill='none' stroke='%23000'%3E%3Ccircle cx='2.25' cy='9.25' r='1.75'/%3E%3Cpath d='M4 7v4.5'/%3E%3Cpath d='M13.4 7.9A1.75 1.75 0 1 0 13.4 10.6'/%3E%3C/g%3E%3C/svg%3E\")}.nxe-icon-filter-operation-not-contains{--nxe-ui-grid-fr-op-glyph: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cmask id='m'%3E%3Crect width='14' height='14' fill='%23fff'/%3E%3Cg fill='none' stroke='%23000'%3E%3Ccircle cx='2.25' cy='9.25' r='1.75'/%3E%3Cpath d='M4 7v4.5'/%3E%3Ccircle cx='7' cy='9.25' r='1.75'/%3E%3Cpath d='M5.25 3.5v8'/%3E%3Cpath d='M13.4 7.9A1.75 1.75 0 1 0 13.4 10.6'/%3E%3C/g%3E%3Crect y='8.75' width='14' height='1'/%3E%3C/mask%3E%3Crect x='0' y='2.5' width='14' height='9.5' mask='url(%23m)'/%3E%3C/svg%3E\")}.nxe-icon-filter-operation-starts-with{--nxe-ui-grid-fr-op-glyph: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cmask id='m'%3E%3Crect width='14' height='14' fill='%23fff'/%3E%3Cg fill='none' stroke='%23000'%3E%3Ccircle cx='2.25' cy='9.25' r='1.75'/%3E%3Cpath d='M4 7v4.5'/%3E%3Ccircle cx='7' cy='9.25' r='1.75'/%3E%3Cpath d='M5.25 3.5v8'/%3E%3C/g%3E%3C/mask%3E%3Crect x='0' y='2.5' width='9.5' height='9.5' mask='url(%23m)'/%3E%3Cg fill='none' stroke='%23000'%3E%3Cpath d='M13.4 7.9A1.75 1.75 0 1 0 13.4 10.6'/%3E%3C/g%3E%3C/svg%3E\")}.nxe-icon-filter-operation-ends-with{--nxe-ui-grid-fr-op-glyph: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cg fill='none' stroke='%23000'%3E%3Ccircle cx='2.25' cy='9.25' r='1.75'/%3E%3Cpath d='M4 7v4.5'/%3E%3C/g%3E%3Cmask id='m'%3E%3Crect width='14' height='14' fill='%23fff'/%3E%3Cg fill='none' stroke='%23000'%3E%3Ccircle cx='7' cy='9.25' r='1.75'/%3E%3Cpath d='M5.25 3.5v8'/%3E%3Cpath d='M13.4 7.9A1.75 1.75 0 1 0 13.4 10.6'/%3E%3C/g%3E%3C/mask%3E%3Crect x='4.5' y='2.5' width='9.5' height='9.5' mask='url(%23m)'/%3E%3C/svg%3E\")}.nxe-icon-filter-operation-equals{--nxe-ui-grid-fr-op-glyph: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath d='M2 5h10v1H2zM2 8h10v1H2z'/%3E%3C/svg%3E\")}.nxe-icon-filter-operation-not-equals{--nxe-ui-grid-fr-op-glyph: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath d='M2 5h10v1H2zM2 8h10v1H2zM9.3 2h1.4l-6 10H3.3z'/%3E%3C/svg%3E\")}.nxe-icon-filter-operation-less{--nxe-ui-grid-fr-op-glyph: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath d='M10.75 3.5 3.75 7l7 3.5' fill='none' stroke='%23000' stroke-width='1.2'/%3E%3C/svg%3E\")}.nxe-icon-filter-operation-greater{--nxe-ui-grid-fr-op-glyph: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath d='M3.25 3.5l7 3.5-7 3.5' fill='none' stroke='%23000' stroke-width='1.2'/%3E%3C/svg%3E\")}.nxe-icon-filter-operation-less-equal{--nxe-ui-grid-fr-op-glyph: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath d='M10.75 2 3.75 5.5l7 3.5M3.75 8.5l7 3.5' fill='none' stroke='%23000' stroke-width='1.2'/%3E%3C/svg%3E\")}.nxe-icon-filter-operation-greater-equal{--nxe-ui-grid-fr-op-glyph: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath d='M3.25 2l7 3.5-7 3.5M10.25 8.5l-7 3.5' fill='none' stroke='%23000' stroke-width='1.2'/%3E%3C/svg%3E\")}.nxe-icon-filter-operation-between{--nxe-ui-grid-fr-op-glyph: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath d='M2 5h1v5H2zM11 5h1v5h-1zM3 7h8v1H3z'/%3E%3C/svg%3E\")}.nxe-fr-op-menu{background:var(--nxe-ui-surface, #fff);border:1px solid var(--nxe-ui-border, #ddd);box-shadow:var(--nxe-ui-shadow-sm, 0 2px 6px rgba(0, 0, 0, .15));padding:4px 0;min-width:180px}.nxe-fr-op-menu .nxe-menu-item{display:flex;align-items:center;gap:5px;box-sizing:border-box;height:31px;padding:0 24px 0 5px;cursor:pointer;color:var(--nxe-ui-text, #333)}.nxe-fr-op-menu .nxe-menu-item:hover{background:var(--nxe-ui-neutral-100, #f0f0f0)}.nxe-fr-op-menu .nxe-menu-item.nxe-menu-item-selected{background:var(--nxe-ui-neutral-150, #e6e6e6)}.nxe-data-grid .nxe-col-resize-handle{position:absolute;right:-4px;top:0;bottom:0;width:8px;cursor:col-resize;z-index:2;touch-action:none}body.nxe-grid-resizing{cursor:col-resize;user-select:none;-webkit-user-select:none}.nxe-col-drop-indicator{position:fixed;width:2px;background:var(--nxe-ui-accent, #337ab7);z-index:10000;pointer-events:none}\n"] }]
6146
+ }], ctorParameters: () => [], propDecorators: { declaredColumns: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => NxeColumnComponent), { ...{ descendants: false }, isSignal: true }] }], templates: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => NxeTemplateDirective), { isSignal: true }] }], selection: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NxeSelectionComponent), { isSignal: true }] }], headerFilter: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NxeHeaderFilterComponent), { isSignal: true }] }], grouping: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NxeGroupingComponent), { isSignal: true }] }], groupPanel: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NxeGroupPanelComponent), { isSignal: true }] }], summaryCmp: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NxeSummaryComponent), { isSignal: true }] }], pagingCmp: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NxePagingComponent), { isSignal: true }] }], pagerCmp: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NxePagerComponent), { isSignal: true }] }], editingCmp: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NxeEditingComponent), { isSignal: true }] }], filterRowCmp: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NxeFilterRowComponent), { isSignal: true }] }], masterDetail: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NxeMasterDetailComponent), { isSignal: true }] }], dataSource: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataSource", required: false }] }], keyExpr: [{ type: i0.Input, args: [{ isSignal: true, alias: "keyExpr", required: false }] }], hoverStateEnabledInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "hoverStateEnabled", required: false }] }], showColumnLinesInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "showColumnLines", required: false }] }], showRowLinesInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "showRowLines", required: false }] }], showBordersInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "showBorders", required: false }] }], wordWrapEnabledInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "wordWrapEnabled", required: false }] }], columnAutoWidthInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnAutoWidth", required: false }] }], rowAlternationEnabledInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowAlternationEnabled", required: false }] }], resizeFillsGridInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "resizeFillsGrid", required: false }] }], remoteOperations: [{ type: i0.Input, args: [{ isSignal: true, alias: "remoteOperations", required: false }] }], pageIndexModel: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageIndex", required: false }] }, { type: i0.Output, args: ["pageIndexChange"] }], pageSizeModel: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }, { type: i0.Output, args: ["pageSizeChange"] }], sortModel: [{ type: i0.Input, args: [{ isSignal: true, alias: "sort", required: false }] }, { type: i0.Output, args: ["sortChange"] }], filter: [{ type: i0.Input, args: [{ isSignal: true, alias: "filter", required: false }] }], optionsChange: [{ type: i0.Output, args: ["optionsChange"] }], syncLookupFilterValues: [{ type: i0.Input, args: [{ isSignal: true, alias: "syncLookupFilterValues", required: false }] }], columnMinWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnMinWidth", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], noDataTextInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "noDataText", required: false }] }], loadPanel: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadPanel", required: false }] }], paging: [{ type: i0.Input, args: [{ isSignal: true, alias: "paging", required: false }] }], filterRow: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterRow", required: false }] }], headerFilterOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerFilter", required: false }] }], groupingOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "grouping", required: false }] }], groupPanelOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupPanel", required: false }] }], scrolling: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrolling", required: false }] }], sorting: [{ type: i0.Input, args: [{ isSignal: true, alias: "sorting", required: false }] }], allowColumnResizing: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowColumnResizing", required: false }] }], columnResizingMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnResizingMode", required: false }] }], allowColumnReordering: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowColumnReordering", required: false }] }], repaintChangesOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "repaintChangesOnly", required: false }] }], showColumnHeaders: [{ type: i0.Input, args: [{ isSignal: true, alias: "showColumnHeaders", required: false }] }], parentIdExpr: [{ type: i0.Input, args: [{ isSignal: true, alias: "parentIdExpr", required: false }] }], dataStructure: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataStructure", required: false }] }], rootValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "rootValue", required: false }] }], autoExpandAll: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoExpandAll", required: false }] }], expandedRowKeys: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandedRowKeys", required: false }] }, { type: i0.Output, args: ["expandedRowKeysChange"] }], selectedRowKeys: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedRowKeys", required: false }] }, { type: i0.Output, args: ["selectedRowKeysChange"] }], mergeColumns: [{ type: i0.Input, args: [{ isSignal: true, alias: "mergeColumns", required: false }] }], onSelectionChanged: [{ type: i0.Output, args: ["onSelectionChanged"] }], onRowClick: [{ type: i0.Output, args: ["onRowClick"] }], onContentReady: [{ type: i0.Output, args: ["onContentReady"] }], onOptionChanged: [{ type: i0.Output, args: ["onOptionChanged"] }], rowClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowClass", required: false }] }], onCellPrepared: [{
4133
6147
  type: Output
4134
6148
  }], onRowPrepared: [{
4135
6149
  type: Output
@@ -4141,5 +6155,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
4141
6155
  * Generated bundle index. Do not edit.
4142
6156
  */
4143
6157
 
4144
- export { NXE_HF_BLANKS, NxeColumnComponent, NxeDataGridComponent, NxeDragGesture, NxeEditingComponent, NxeFilterRowComponent, NxeGestureClickSuppression, NxeGridDragController, NxeGroupItemComponent, NxeGroupPanelComponent, NxeGroupingComponent, NxeHeaderFilterComponent, NxeHeaderFilterModel, NxeHeaderFilterPanelComponent, NxeMasterDetailComponent, NxePagerComponent, NxePagingComponent, NxeScrollingComponent, NxeSelectionComponent, NxeSummaryComponent, NxeTotalItemComponent, exportDataGrid, exportNxeGridToExcel, nxeDatePattern, nxeDefaultFormat, nxeExcelNumFmt, nxeFormatNumberValue, nxeHfKey, nxeNumberMask, openNxeHeaderFilterOverlay };
6158
+ export { NXE_HF_BLANKS, NXE_REMOTE_DEFAULT_PAGE_SIZE, NxeColumnComponent, NxeDataGridComponent, NxeDragGesture, NxeEditingComponent, NxeFilterRowComponent, NxeGestureClickSuppression, NxeGridDragController, NxeGroupItemComponent, NxeGroupPanelComponent, NxeGroupingComponent, NxeHeaderFilterComponent, NxeHeaderFilterModel, NxeHeaderFilterPanelComponent, NxeMasterDetailComponent, NxePagerComponent, NxePaginatorComponent, NxePagingComponent, NxeRemoteDataSource, NxeScrollingComponent, NxeSelectionComponent, NxeSummaryComponent, NxeTotalItemComponent, exportDataGrid, exportNxeGridToExcel, nxeDatePattern, nxeDefaultFormat, nxeExcelNumFmt, nxeExcelNumberMask, nxeFilterAnd, nxeFilterOr, nxeFlattenFilter, nxeFormatNumberValue, nxeHfKey, nxeIsStoreLike, nxeLoadOptions, nxeNumberMask, nxeNumberValue, nxeSameQuery, nxeValueDataType, openNxeHeaderFilterOverlay };
4145
6159
  //# sourceMappingURL=nexterajs-ui-grid.mjs.map