@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.
- package/README.md +279 -34
- package/fesm2022/nexterajs-ui-charts.mjs +1096 -0
- package/fesm2022/nexterajs-ui-charts.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-core.mjs +835 -11
- package/fesm2022/nexterajs-ui-core.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-display.mjs +1142 -0
- package/fesm2022/nexterajs-ui-display.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-dropdowns.mjs +621 -69
- package/fesm2022/nexterajs-ui-dropdowns.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-file-uploader.mjs +576 -11
- package/fesm2022/nexterajs-ui-file-uploader.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-forms.mjs +2702 -452
- package/fesm2022/nexterajs-ui-forms.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-grid.mjs +2350 -336
- package/fesm2022/nexterajs-ui-grid.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-intl.mjs +117 -15
- package/fesm2022/nexterajs-ui-intl.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-navigation.mjs +1054 -0
- package/fesm2022/nexterajs-ui-navigation.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-notify.mjs +353 -0
- package/fesm2022/nexterajs-ui-notify.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-overlay.mjs +1671 -0
- package/fesm2022/nexterajs-ui-overlay.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-pivot.mjs +191 -136
- package/fesm2022/nexterajs-ui-pivot.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-shell.mjs +542 -0
- package/fesm2022/nexterajs-ui-shell.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-styling.mjs +568 -3
- package/fesm2022/nexterajs-ui-styling.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-tree-view.mjs +150 -18
- package/fesm2022/nexterajs-ui-tree-view.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-validation.mjs +283 -97
- package/fesm2022/nexterajs-ui-validation.mjs.map +1 -1
- package/fesm2022/nexterajs-ui.mjs +6 -0
- package/fesm2022/nexterajs-ui.mjs.map +1 -1
- package/package.json +37 -2
- package/styles/_dropdown.scss +150 -1
- package/styles/_editor.scss +75 -21
- package/styles/_icons.scss +92 -0
- package/styles/_mixins.scss +104 -1
- package/styles/_scrollbar.scss +134 -0
- package/styles/_status.scss +21 -0
- package/styles/_toast.scss +243 -0
- package/styles/_tokens.scss +467 -2
- package/styles/_widget.scss +14 -3
- package/styles/index.scss +10 -4
- package/types/nexterajs-ui-charts.d.ts +446 -0
- package/types/nexterajs-ui-core.d.ts +515 -30
- package/types/nexterajs-ui-display.d.ts +720 -0
- package/types/nexterajs-ui-dropdowns.d.ts +318 -57
- package/types/nexterajs-ui-file-uploader.d.ts +240 -8
- package/types/nexterajs-ui-forms.d.ts +772 -43
- package/types/nexterajs-ui-grid.d.ts +878 -135
- package/types/nexterajs-ui-intl.d.ts +39 -4
- package/types/nexterajs-ui-navigation.d.ts +531 -0
- package/types/nexterajs-ui-notify.d.ts +149 -0
- package/types/nexterajs-ui-overlay.d.ts +703 -0
- package/types/nexterajs-ui-pivot.d.ts +91 -70
- package/types/nexterajs-ui-shell.d.ts +294 -0
- package/types/nexterajs-ui-styling.d.ts +163 -3
- package/types/nexterajs-ui-tree-view.d.ts +56 -4
- package/types/nexterajs-ui-validation.d.ts +110 -22
- 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,
|
|
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 {
|
|
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
|
|
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.
|
|
95
|
-
*
|
|
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
|
|
104
|
-
|
|
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
|
-
|
|
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('.
|
|
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
|
|
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
|
-
*
|
|
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('.
|
|
134
|
-
const rows = thead?.querySelectorAll('.
|
|
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
|
|
244
|
+
* The header cell of each visible data column, in visible-column order.
|
|
139
245
|
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
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
|
|
145
|
-
|
|
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
|
|
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 (`.
|
|
160
|
-
* `.
|
|
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.
|
|
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.
|
|
168
|
-
* - chip drag highlights the header row with `
|
|
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
|
-
'
|
|
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('
|
|
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('
|
|
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('.
|
|
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 = '
|
|
635
|
+
a.separator.className = 'nxe-block-separator';
|
|
522
636
|
a.separator.textContent = ' '; // dx renders the separator as an 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('.
|
|
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
|
-
|
|
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
|
-
|
|
773
|
-
|
|
774
|
-
|
|
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
|
-
?
|
|
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:
|
|
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:
|
|
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 `
|
|
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
|
-
|
|
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 `
|
|
1345
|
-
* `<tr class="
|
|
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
|
-
|
|
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
|
|
1425
|
-
const mask = typeof col.format === 'string' ? col.format
|
|
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('.
|
|
1537
|
-
|
|
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
|
|
1543
|
-
//
|
|
1544
|
-
const headerCells =
|
|
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
|
-
|
|
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 = `${
|
|
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(() =>
|
|
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
|
-
|
|
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('
|
|
2387
|
+
if (c.contains('nxe-header-row')) {
|
|
1661
2388
|
return 'header';
|
|
1662
2389
|
}
|
|
1663
|
-
if (c.contains('
|
|
2390
|
+
if (c.contains('nxe-datagrid-filter-row')) {
|
|
1664
2391
|
return 'filter';
|
|
1665
2392
|
}
|
|
1666
|
-
if (c.contains('
|
|
2393
|
+
if (c.contains('nxe-data-row')) {
|
|
1667
2394
|
return 'data';
|
|
1668
2395
|
}
|
|
1669
|
-
if (c.contains('
|
|
2396
|
+
if (c.contains('nxe-group-footer-row')) {
|
|
1670
2397
|
return 'groupFooter';
|
|
1671
2398
|
}
|
|
1672
|
-
if (c.contains('
|
|
2399
|
+
if (c.contains('nxe-group-row')) {
|
|
1673
2400
|
return 'group';
|
|
1674
2401
|
}
|
|
1675
|
-
if (tr.parentElement?.classList.contains('
|
|
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>`
|
|
1697
|
-
* `
|
|
1698
|
-
*
|
|
1699
|
-
* (4px cell padding, #f8fafc header, #525252 header text, row/hover/alt colours)
|
|
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: () =>
|
|
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
|
-
|
|
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 ??
|
|
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 ??
|
|
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 ?? '
|
|
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) =>
|
|
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
|
-
|
|
1948
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
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
|
|
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 =
|
|
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('.
|
|
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('.
|
|
2117
|
-
const rowsview = host.querySelector('.
|
|
2118
|
-
|
|
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.
|
|
2124
|
-
//
|
|
2125
|
-
// row and
|
|
2126
|
-
const cells =
|
|
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.
|
|
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
|
-
|
|
2223
|
-
|
|
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
|
-
:
|
|
3225
|
+
: this.sourceRows().filter((r) => keySet.has(this.rowKey(r)));
|
|
2261
3226
|
}
|
|
2262
3227
|
const set = this.selectedSet();
|
|
2263
|
-
return set.size === 0 ? [] :
|
|
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 =
|
|
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.
|
|
2278
|
-
? this.
|
|
2279
|
-
: this.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
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
|
-
|
|
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:
|
|
2338
|
-
selectedRowKeys:
|
|
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() ??
|
|
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(() =>
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
2413
|
-
|
|
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 =
|
|
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(
|
|
3864
|
+
footerSums.set(this.summaryTarget(item), text);
|
|
2539
3865
|
}
|
|
2540
3866
|
else if (item.alignByColumn()) {
|
|
2541
|
-
alignedSums.set(
|
|
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'
|
|
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.
|
|
3992
|
+
this.goToPage(this.currentPage() - 1);
|
|
2648
3993
|
}
|
|
2649
3994
|
nextPage() {
|
|
2650
|
-
this.goToPage(this.
|
|
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
|
-
|
|
2660
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
2961
|
-
|
|
2962
|
-
const
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
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.
|
|
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 (
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
return
|
|
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) =>
|
|
3542
|
-
return match ?
|
|
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
|
-
|
|
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:
|
|
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('.
|
|
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
|
-
|
|
5426
|
+
const lead = this.leadCellCount();
|
|
3693
5427
|
for (const td of Array.from(tr.cells)) {
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
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:
|
|
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
|
-
/**
|
|
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 (!
|
|
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.
|
|
5695
|
+
this.setSort({ field, dir: 'asc' });
|
|
3778
5696
|
}
|
|
3779
5697
|
else if (current.dir === 'asc') {
|
|
3780
|
-
this.
|
|
5698
|
+
this.setSort({ field, dir: 'desc' });
|
|
3781
5699
|
}
|
|
3782
5700
|
else {
|
|
3783
|
-
this.
|
|
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 =
|
|
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.
|
|
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: '
|
|
3890
|
-
{ op: 'notcontains', label: '
|
|
3891
|
-
{ op: 'startswith', label: '
|
|
3892
|
-
{ op: 'endswith', label: '
|
|
3893
|
-
{ op: '=', label: '
|
|
3894
|
-
{ op: '<>', label: '
|
|
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: '
|
|
3898
|
-
{ op: '<>', label: '
|
|
3899
|
-
{ op: '<', label: '
|
|
3900
|
-
{ op: '>', label: '
|
|
3901
|
-
{ op: '<=', label: '
|
|
3902
|
-
{ op: '>=', label: '
|
|
3903
|
-
{ op: 'between', label: '
|
|
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 =
|
|
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
|
-
|
|
3949
|
-
|
|
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, {
|
|
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,
|
|
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
|
|
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 ||
|
|
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
|