@nexterajs/ui 0.0.1
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/LICENSE +202 -0
- package/NOTICE +16 -0
- package/README.md +64 -0
- package/fesm2022/nexterajs-ui-animations.mjs +184 -0
- package/fesm2022/nexterajs-ui-animations.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-core.mjs +326 -0
- package/fesm2022/nexterajs-ui-core.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-dropdowns.mjs +953 -0
- package/fesm2022/nexterajs-ui-dropdowns.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-excel.mjs +779 -0
- package/fesm2022/nexterajs-ui-excel.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-file-uploader.mjs +125 -0
- package/fesm2022/nexterajs-ui-file-uploader.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-forms.mjs +1113 -0
- package/fesm2022/nexterajs-ui-forms.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-grid.mjs +4145 -0
- package/fesm2022/nexterajs-ui-grid.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-intl.mjs +377 -0
- package/fesm2022/nexterajs-ui-intl.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-pivot.mjs +4739 -0
- package/fesm2022/nexterajs-ui-pivot.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-styling.mjs +59 -0
- package/fesm2022/nexterajs-ui-styling.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-tree-view.mjs +202 -0
- package/fesm2022/nexterajs-ui-tree-view.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-validation.mjs +269 -0
- package/fesm2022/nexterajs-ui-validation.mjs.map +1 -0
- package/fesm2022/nexterajs-ui.mjs +23 -0
- package/fesm2022/nexterajs-ui.mjs.map +1 -0
- package/package.json +100 -0
- package/styles/_button-directives.scss +39 -0
- package/styles/_dropdown.scss +113 -0
- package/styles/_editor.scss +146 -0
- package/styles/_mixins.scss +2 -0
- package/styles/_tokens.scss +77 -0
- package/styles/_widget.scss +58 -0
- package/styles/index.scss +83 -0
- package/types/nexterajs-ui-animations.d.ts +22 -0
- package/types/nexterajs-ui-core.d.ts +328 -0
- package/types/nexterajs-ui-dropdowns.d.ts +334 -0
- package/types/nexterajs-ui-excel.d.ts +159 -0
- package/types/nexterajs-ui-file-uploader.d.ts +47 -0
- package/types/nexterajs-ui-forms.d.ts +296 -0
- package/types/nexterajs-ui-grid.d.ts +1485 -0
- package/types/nexterajs-ui-intl.d.ts +125 -0
- package/types/nexterajs-ui-pivot.d.ts +1470 -0
- package/types/nexterajs-ui-styling.d.ts +40 -0
- package/types/nexterajs-ui-tree-view.d.ts +69 -0
- package/types/nexterajs-ui-validation.d.ts +102 -0
- package/types/nexterajs-ui.d.ts +12 -0
|
@@ -0,0 +1,4145 @@
|
|
|
1
|
+
import { formatNumber, parseNumberFormat, parseDateValue, formatDisplay } from '@nexterajs/ui/intl';
|
|
2
|
+
import * as i0 from '@angular/core';
|
|
3
|
+
import { input, contentChildren, computed, ViewEncapsulation, ChangeDetectionStrategy, Component, signal, output, Directive, model, inject, ElementRef, ViewContainerRef, NgZone, contentChild, EventEmitter, afterRenderEffect, untracked, afterNextRender, linkedSignal, viewChild, Output } from '@angular/core';
|
|
4
|
+
import { NgTemplateOutlet } from '@angular/common';
|
|
5
|
+
import { ComponentPortal, TemplatePortal } from '@angular/cdk/portal';
|
|
6
|
+
import { nxePane } from '@nexterajs/ui/styling';
|
|
7
|
+
import { Workbook, nxeSaveAs } from '@nexterajs/ui/excel';
|
|
8
|
+
import { Overlay } from '@angular/cdk/overlay';
|
|
9
|
+
import { NXE_DEFAULT_OPTIONS, NxeTemplateDirective } from '@nexterajs/ui/core';
|
|
10
|
+
|
|
11
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
12
|
+
// Shared value-formatting helpers for the nxe grid family — the single source of truth for how
|
|
13
|
+
// a dx-style column/field `format` renders. Extracted from nxe-data-grid's private
|
|
14
|
+
// `defaultFormat()`/`datePattern()` (the grid delegates here unchanged); the pivot resolves its
|
|
15
|
+
// data-field formats through the same functions so both widgets can never drift apart.
|
|
16
|
+
//
|
|
17
|
+
// dx `{ type }` format objects resolve to the library's own LDML masks:
|
|
18
|
+
// { type: 'fixedPoint', precision: N } → '#,##0' + N decimals
|
|
19
|
+
// { type: 'currency', precision: N } → the same mask — deliberately WITHOUT a currency
|
|
20
|
+
// symbol (recorded in DEFAULTS.md)
|
|
21
|
+
// { type: 'percent', precision: N } → value × 100 + '%' (Excel's own '%' code multiplies,
|
|
22
|
+
// so the exported mask carries a bare '%')
|
|
23
|
+
// { type: '<date pattern>' } → the pattern itself (the app's `Format.DATE`)
|
|
24
|
+
// { formatter } → the function's own text
|
|
25
|
+
/** `precision: 2` → `'.00'`; absent/zero precision → `''` (dx defaults fixedPoint to 0). */
|
|
26
|
+
function precisionDecimals(precision) {
|
|
27
|
+
return precision && precision > 0 ? '.' + '0'.repeat(precision) : '';
|
|
28
|
+
}
|
|
29
|
+
/** Resolve a `format` to a date display pattern — a `{ type }` object's `type` IS the pattern. */
|
|
30
|
+
function nxeDatePattern(format, fallback = 'dd MMM yyyy') {
|
|
31
|
+
if (!format) {
|
|
32
|
+
return fallback;
|
|
33
|
+
}
|
|
34
|
+
return typeof format === 'string' ? format : format.type ?? fallback;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Resolve a `format` to the LDML number mask the library's number engine understands: string
|
|
38
|
+
* masks pass through; `{ type: 'fixedPoint' | 'currency', precision }` build a grouped mask
|
|
39
|
+
* (`currency` deliberately carries NO symbol — DEFAULTS.md). Anything else → `undefined`.
|
|
40
|
+
*/
|
|
41
|
+
function nxeNumberMask(format) {
|
|
42
|
+
if (!format) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
if (typeof format === 'string') {
|
|
46
|
+
return format;
|
|
47
|
+
}
|
|
48
|
+
if (format.type === 'fixedPoint' || format.type === 'currency') {
|
|
49
|
+
return '#,##0' + precisionDecimals(format.precision);
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Format a numeric value per a dx-style `format` (string mask, `{ type }` object, or
|
|
55
|
+
* `{ formatter }`). No format / an unresolvable one → `String(value)` (the grid's untyped
|
|
56
|
+
* rendering).
|
|
57
|
+
*/
|
|
58
|
+
function nxeFormatNumberValue(value, format) {
|
|
59
|
+
if (format && typeof format === 'object') {
|
|
60
|
+
if (typeof format.formatter === 'function') {
|
|
61
|
+
return String(format.formatter(value));
|
|
62
|
+
}
|
|
63
|
+
if (format.type === 'percent') {
|
|
64
|
+
const mask = '#,##0' + precisionDecimals(format.precision) + '%';
|
|
65
|
+
return formatNumber(value * 100, parseNumberFormat(mask));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const mask = nxeNumberMask(format);
|
|
69
|
+
return mask ? formatNumber(value, parseNumberFormat(mask)) : String(value);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Derive the Excel `numFmt` mask for a `format`: string masks pass through, fixedPoint/currency
|
|
73
|
+
* come from {@link nxeNumberMask}, percent appends a bare `%` (Excel multiplies by 100 itself),
|
|
74
|
+
* and any other `{ type }` is a date pattern used as-is. `{ formatter }`-only objects →
|
|
75
|
+
* `undefined` (callers fall back to exporting the formatted text).
|
|
76
|
+
*/
|
|
77
|
+
function nxeExcelNumFmt(format) {
|
|
78
|
+
if (!format) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
if (typeof format === 'string') {
|
|
82
|
+
return format;
|
|
83
|
+
}
|
|
84
|
+
if (typeof format.formatter === 'function') {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
if (format.type === 'percent') {
|
|
88
|
+
return '#,##0' + precisionDecimals(format.precision) + '%';
|
|
89
|
+
}
|
|
90
|
+
return nxeNumberMask(format) ?? format.type ?? undefined;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The data grid's default cell rendering (pure extraction of nxe-data-grid's `defaultFormat`):
|
|
94
|
+
* `dataType`/`format` → display text. Number cells honour string masks only — exactly the
|
|
95
|
+
* grid's pre-extraction behaviour.
|
|
96
|
+
*/
|
|
97
|
+
function nxeDefaultFormat(dataType, format, raw) {
|
|
98
|
+
if (raw == null || raw === '') {
|
|
99
|
+
return '';
|
|
100
|
+
}
|
|
101
|
+
switch (dataType) {
|
|
102
|
+
case 'number': {
|
|
103
|
+
const pattern = typeof format === 'string' ? format : undefined;
|
|
104
|
+
const num = typeof raw === 'number' ? raw : Number(raw);
|
|
105
|
+
return isNaN(num) ? String(raw) : formatNumber(num, parseNumberFormat(pattern));
|
|
106
|
+
}
|
|
107
|
+
case 'date': {
|
|
108
|
+
const dt = parseDateValue(raw);
|
|
109
|
+
return dt ? formatDisplay(dt, nxeDatePattern(format)) : String(raw);
|
|
110
|
+
}
|
|
111
|
+
case 'boolean':
|
|
112
|
+
return raw ? '✓' : '';
|
|
113
|
+
default:
|
|
114
|
+
return String(raw);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* DOM lookups shared by the grid component and its drag/resize controllers.
|
|
120
|
+
*
|
|
121
|
+
* Both questions here used to be answered with `querySelector('.dx-header-row')`, which stopped
|
|
122
|
+
* being right once bands could add header rows above the leaves.
|
|
123
|
+
*/
|
|
124
|
+
/**
|
|
125
|
+
* The all-leaves header row of THIS grid.
|
|
126
|
+
*
|
|
127
|
+
* Two things have to be true at once. It must be the LAST `.dx-header-row` in the thead, because
|
|
128
|
+
* bands sit above the leaves and only the bottom row has one cell per data column. And it must
|
|
129
|
+
* belong to this grid rather than a nested master-detail grid — those live in the tbody, so the
|
|
130
|
+
* first `thead` in document order is always the outer grid's own.
|
|
131
|
+
*/
|
|
132
|
+
function nxeLeafHeaderRow(host) {
|
|
133
|
+
const thead = host.querySelector('.dx-datagrid-content > .dx-datagrid-table > thead');
|
|
134
|
+
const rows = thead?.querySelectorAll('.dx-header-row');
|
|
135
|
+
return rows && rows.length ? rows[rows.length - 1] : null;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The data cells of a header row, command columns removed.
|
|
139
|
+
*
|
|
140
|
+
* Indexing these by visible-column index is deliberate: command cells live in the FIRST header
|
|
141
|
+
* row and span downwards, so a banded grid's leaf row has none, and any fixed lead offset would
|
|
142
|
+
* be off by however many command columns the grid happens to show.
|
|
143
|
+
*/
|
|
144
|
+
function nxeHeaderDataCells(row) {
|
|
145
|
+
if (!row) {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
return Array.from(row.cells).filter((td) => !td.classList.contains('dx-command-expand') &&
|
|
149
|
+
!td.classList.contains('dx-command-select') &&
|
|
150
|
+
!td.classList.contains('dx-command-edit'));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Pointer-based drag engines (drag-to-group parity with DevExtreme, baselined against the
|
|
155
|
+
* bright deployment 2026-07-27; gesture core shared with the pivot field panel since P3-C2).
|
|
156
|
+
*
|
|
157
|
+
* Deliberately NOT CDK DragDrop: the drop topologies are "header → group panel", "chip →
|
|
158
|
+
* anywhere outside = ungroup/remove", "chip ↔ chip = reorder" — not list-to-list — and the
|
|
159
|
+
* ghosts must be dx's exact clones (`.dx-datagrid-drag-header` for the grid,
|
|
160
|
+
* `.dx-pivotgrid-fields-container.dx-drag` for the pivot panel). Document-level listeners run
|
|
161
|
+
* outside the zone; Angular re-enters only on drop. No `setPointerCapture` (throws for
|
|
162
|
+
* synthetic pointerIds under Karma).
|
|
163
|
+
*
|
|
164
|
+
* dx baseline facts encoded here:
|
|
165
|
+
* - grid ghost: `div.dx-datagrid-drag-header.dx-datagrid-text-content.dx-widget.dx-datagrid-drag-action`
|
|
166
|
+
* appended to <body>, caption text only, positioned near the cursor;
|
|
167
|
+
* - insertion indicator: `div.dx-block-separator` placed between panel chips;
|
|
168
|
+
* - chip drag highlights the header row with `dx-datagrid-drop-highlight`;
|
|
169
|
+
* - the source cell/chip stays in place, dimmed;
|
|
170
|
+
* - header→panel drop APPENDS as the last group level (indicator after the last chip).
|
|
171
|
+
*/
|
|
172
|
+
/** Movement (px) before a press becomes a drag — below this a release is a normal click. */
|
|
173
|
+
const DRAG_START_DELTA = 5;
|
|
174
|
+
/** Ghost offset from the pointer (px), tuned against the bright screenshots. */
|
|
175
|
+
const GHOST_OFFSET = 8;
|
|
176
|
+
/**
|
|
177
|
+
* One-shot suppression of the click a completed gesture is about to fire, shared by the drag and
|
|
178
|
+
* resize engines. Strictly gesture-scoped: clears once that trailing click has passed (even when
|
|
179
|
+
* it lands on an element without a consuming handler) and expires after 500ms, so a later genuine
|
|
180
|
+
* click is never eaten.
|
|
181
|
+
*/
|
|
182
|
+
class NxeGestureClickSuppression {
|
|
183
|
+
pending = false;
|
|
184
|
+
until = 0;
|
|
185
|
+
arm() {
|
|
186
|
+
this.pending = true;
|
|
187
|
+
this.until = performance.now() + 500;
|
|
188
|
+
// Capture runs before the bubble-phase consumers; the macrotask runs after dispatch.
|
|
189
|
+
document.addEventListener('click', () => setTimeout(() => (this.pending = false)), { capture: true, once: true });
|
|
190
|
+
}
|
|
191
|
+
consume() {
|
|
192
|
+
const suppress = this.pending && performance.now() < this.until;
|
|
193
|
+
this.pending = false;
|
|
194
|
+
return suppress;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* The shared pointer-gesture core (extracted from the grid controller in P3-C2 so the pivot
|
|
199
|
+
* field panel reuses it rather than copy-pasting). Behavior is the audited 2026-07-27 gesture:
|
|
200
|
+
* press arms, 5px starts, Escape cancels (suppressing the held pointer's release click),
|
|
201
|
+
* pointercancel cancels silently, drop arms one-shot click suppression.
|
|
202
|
+
*/
|
|
203
|
+
class NxeDragGesture {
|
|
204
|
+
zone;
|
|
205
|
+
delegate;
|
|
206
|
+
active = null;
|
|
207
|
+
suppression = new NxeGestureClickSuppression();
|
|
208
|
+
onMove = (e) => this.pointerMove(e);
|
|
209
|
+
onUp = (e) => {
|
|
210
|
+
if (this.active && e.pointerId !== this.active.pointerId) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
this.pointerUp();
|
|
214
|
+
};
|
|
215
|
+
onCancel = (e) => {
|
|
216
|
+
if (this.active && e.pointerId !== this.active.pointerId) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
// pointercancel fires no trailing click — cancel WITHOUT arming click suppression.
|
|
220
|
+
this.cancel(false);
|
|
221
|
+
};
|
|
222
|
+
onKey = (e) => {
|
|
223
|
+
if (e.key === 'Escape') {
|
|
224
|
+
this.cancel(true); // the still-held pointer's release click will follow
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
constructor(zone, delegate) {
|
|
228
|
+
this.zone = zone;
|
|
229
|
+
this.delegate = delegate;
|
|
230
|
+
}
|
|
231
|
+
/** Arm a gesture on pointerdown. Returns false (payload must NOT be set) when refused. */
|
|
232
|
+
begin(event) {
|
|
233
|
+
if (event.button !== 0 || this.active) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
this.active = {
|
|
237
|
+
pointerId: event.pointerId,
|
|
238
|
+
startX: event.clientX,
|
|
239
|
+
startY: event.clientY,
|
|
240
|
+
source: event.currentTarget,
|
|
241
|
+
started: false,
|
|
242
|
+
ghost: null,
|
|
243
|
+
};
|
|
244
|
+
this.zone.runOutsideAngular(() => {
|
|
245
|
+
document.addEventListener('pointermove', this.onMove);
|
|
246
|
+
document.addEventListener('pointerup', this.onUp);
|
|
247
|
+
document.addEventListener('pointercancel', this.onCancel);
|
|
248
|
+
document.addEventListener('keydown', this.onKey);
|
|
249
|
+
});
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* One-shot suppression of the click browsers fire right after a drag's pointerup — call at
|
|
254
|
+
* the top of header/chip/funnel click handlers so a drop never doubles as a click action.
|
|
255
|
+
*/
|
|
256
|
+
consumeClick() {
|
|
257
|
+
return this.suppression.consume();
|
|
258
|
+
}
|
|
259
|
+
destroy() {
|
|
260
|
+
if (this.active) {
|
|
261
|
+
this.cancel(false);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
pointerMove(e) {
|
|
265
|
+
const a = this.active;
|
|
266
|
+
if (!a || e.pointerId !== a.pointerId) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (!a.started) {
|
|
270
|
+
if (Math.hypot(e.clientX - a.startX, e.clientY - a.startY) < DRAG_START_DELTA) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
a.started = true;
|
|
274
|
+
a.ghost = this.mountGhost(a);
|
|
275
|
+
for (const cls of this.delegate.sourceDragClasses) {
|
|
276
|
+
a.source.classList.add(cls);
|
|
277
|
+
}
|
|
278
|
+
document.body.classList.add('nxe-grid-dragging');
|
|
279
|
+
this.delegate.dragStarted?.();
|
|
280
|
+
}
|
|
281
|
+
if (e.cancelable) {
|
|
282
|
+
e.preventDefault(); // no text selection / touch scroll mid-drag
|
|
283
|
+
}
|
|
284
|
+
if (a.ghost) {
|
|
285
|
+
a.ghost.style.left = `${e.clientX + GHOST_OFFSET}px`;
|
|
286
|
+
a.ghost.style.top = `${e.clientY + GHOST_OFFSET}px`;
|
|
287
|
+
}
|
|
288
|
+
this.delegate.dragMove(e.clientX, e.clientY);
|
|
289
|
+
}
|
|
290
|
+
mountGhost(a) {
|
|
291
|
+
const ghost = this.delegate.createGhost();
|
|
292
|
+
ghost.style.position = 'fixed';
|
|
293
|
+
ghost.style.left = `${a.startX + GHOST_OFFSET}px`;
|
|
294
|
+
ghost.style.top = `${a.startY + GHOST_OFFSET}px`;
|
|
295
|
+
ghost.style.pointerEvents = 'none';
|
|
296
|
+
ghost.style.zIndex = '10000';
|
|
297
|
+
document.body.appendChild(ghost);
|
|
298
|
+
return ghost;
|
|
299
|
+
}
|
|
300
|
+
pointerUp() {
|
|
301
|
+
const a = this.active;
|
|
302
|
+
this.detachListeners();
|
|
303
|
+
this.active = null;
|
|
304
|
+
if (!a) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (!a.started) {
|
|
308
|
+
this.delegate.reset?.();
|
|
309
|
+
return; // plain click — let the click event do its job
|
|
310
|
+
}
|
|
311
|
+
this.suppression.arm();
|
|
312
|
+
// Imperative DOM must be gone BEFORE any signal write re-runs a drop zone's @for.
|
|
313
|
+
this.teardownVisuals(a);
|
|
314
|
+
this.zone.run(() => this.delegate.drop());
|
|
315
|
+
this.delegate.reset?.();
|
|
316
|
+
}
|
|
317
|
+
cancel(willClickFollow) {
|
|
318
|
+
const a = this.active;
|
|
319
|
+
this.detachListeners();
|
|
320
|
+
this.active = null;
|
|
321
|
+
if (!a) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (a.started) {
|
|
325
|
+
if (willClickFollow) {
|
|
326
|
+
this.suppression.arm(); // Escape: the held pointer's release click still fires
|
|
327
|
+
}
|
|
328
|
+
this.teardownVisuals(a);
|
|
329
|
+
}
|
|
330
|
+
this.delegate.reset?.();
|
|
331
|
+
}
|
|
332
|
+
teardownVisuals(a) {
|
|
333
|
+
a.ghost?.remove();
|
|
334
|
+
a.ghost = null;
|
|
335
|
+
for (const cls of this.delegate.sourceDragClasses) {
|
|
336
|
+
a.source.classList.remove(cls);
|
|
337
|
+
}
|
|
338
|
+
document.body.classList.remove('nxe-grid-dragging');
|
|
339
|
+
this.delegate.clearDropVisuals();
|
|
340
|
+
}
|
|
341
|
+
detachListeners() {
|
|
342
|
+
document.removeEventListener('pointermove', this.onMove);
|
|
343
|
+
document.removeEventListener('pointerup', this.onUp);
|
|
344
|
+
document.removeEventListener('pointercancel', this.onCancel);
|
|
345
|
+
document.removeEventListener('keydown', this.onKey);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/** The data grid's drag controller — payload + drop topology over the shared gesture core. */
|
|
349
|
+
class NxeGridDragController {
|
|
350
|
+
hooks;
|
|
351
|
+
drag = null;
|
|
352
|
+
gesture;
|
|
353
|
+
constructor(hooks) {
|
|
354
|
+
this.hooks = hooks;
|
|
355
|
+
this.gesture = new NxeDragGesture(hooks.zone, {
|
|
356
|
+
// Exact class list observed on bright's dx ghost + an nxe hook for fallback styling.
|
|
357
|
+
createGhost: () => {
|
|
358
|
+
const ghost = document.createElement('div');
|
|
359
|
+
ghost.className =
|
|
360
|
+
'dx-datagrid-drag-header dx-datagrid-text-content dx-widget dx-datagrid-drag-action nxe-grid-drag-ghost';
|
|
361
|
+
const col = this.drag?.col;
|
|
362
|
+
ghost.textContent = col ? col.caption() || col.dataField() : '';
|
|
363
|
+
ghost.style.whiteSpace = 'nowrap';
|
|
364
|
+
return ghost;
|
|
365
|
+
},
|
|
366
|
+
sourceDragClasses: ['nxe-drag-source'],
|
|
367
|
+
dragStarted: () => {
|
|
368
|
+
if (this.drag?.kind === 'chip') {
|
|
369
|
+
// dx: while a chip is dragged, the header row is the highlighted return target.
|
|
370
|
+
this.headerRow()?.classList.add('dx-datagrid-drop-highlight');
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
dragMove: (x, y) => {
|
|
374
|
+
if (this.drag) {
|
|
375
|
+
this.updateDropTarget(this.drag, x, y);
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
drop: () => this.completeDrop(),
|
|
379
|
+
clearDropVisuals: () => {
|
|
380
|
+
if (this.drag) {
|
|
381
|
+
this.removeSeparator(this.drag);
|
|
382
|
+
this.removeColIndicator(this.drag);
|
|
383
|
+
}
|
|
384
|
+
this.headerRow()?.classList.remove('dx-datagrid-drop-highlight');
|
|
385
|
+
},
|
|
386
|
+
reset: () => {
|
|
387
|
+
this.drag = null;
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
startHeaderDrag(col, visibleIndex, event) {
|
|
392
|
+
this.begin('header', col, visibleIndex, event);
|
|
393
|
+
}
|
|
394
|
+
startChipDrag(col, level, event) {
|
|
395
|
+
this.begin('chip', col, level, event);
|
|
396
|
+
}
|
|
397
|
+
/** See {@link NxeDragGesture.consumeClick}. */
|
|
398
|
+
consumeClick() {
|
|
399
|
+
return this.gesture.consumeClick();
|
|
400
|
+
}
|
|
401
|
+
destroy() {
|
|
402
|
+
this.gesture.destroy();
|
|
403
|
+
}
|
|
404
|
+
begin(kind, col, level, event) {
|
|
405
|
+
if (!this.gesture.begin(event)) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
this.drag = {
|
|
409
|
+
kind,
|
|
410
|
+
col,
|
|
411
|
+
level,
|
|
412
|
+
separator: null,
|
|
413
|
+
colIndicator: null,
|
|
414
|
+
overPanel: false,
|
|
415
|
+
dropIndex: null,
|
|
416
|
+
headerSlot: null,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
updateDropTarget(a, x, y) {
|
|
420
|
+
const panel = this.panel();
|
|
421
|
+
const rect = panel?.getBoundingClientRect();
|
|
422
|
+
a.overPanel = !!rect && x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
|
|
423
|
+
if (!panel || !a.overPanel) {
|
|
424
|
+
a.dropIndex = null;
|
|
425
|
+
this.removeSeparator(a);
|
|
426
|
+
if (a.kind === 'header') {
|
|
427
|
+
this.updateHeaderReorderTarget(a, x, y);
|
|
428
|
+
}
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
this.removeColIndicator(a);
|
|
432
|
+
a.headerSlot = null;
|
|
433
|
+
const chips = Array.from(panel.querySelectorAll('.dx-group-panel-item'));
|
|
434
|
+
if (a.kind === 'header') {
|
|
435
|
+
// dx appends on header→panel drop; the indicator sits after the last chip.
|
|
436
|
+
a.dropIndex = chips.length;
|
|
437
|
+
this.placeSeparator(a, panel, chips, chips.length);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
let index = chips.length;
|
|
441
|
+
for (let i = 0; i < chips.length; i++) {
|
|
442
|
+
const r = chips[i].getBoundingClientRect();
|
|
443
|
+
if (x < r.left + r.width / 2) {
|
|
444
|
+
index = i;
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
a.dropIndex = index;
|
|
449
|
+
this.placeSeparator(a, panel, chips, index);
|
|
450
|
+
}
|
|
451
|
+
/** Header-row drop zone (only with `allowColumnReordering`): slot between data headers. */
|
|
452
|
+
updateHeaderReorderTarget(a, x, y) {
|
|
453
|
+
if (!this.hooks.reorderEnabled()) {
|
|
454
|
+
a.headerSlot = null;
|
|
455
|
+
this.removeColIndicator(a);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const rowRect = this.headerRow()?.getBoundingClientRect();
|
|
459
|
+
if (!rowRect || x < rowRect.left || x > rowRect.right || y < rowRect.top || y > rowRect.bottom) {
|
|
460
|
+
a.headerSlot = null;
|
|
461
|
+
this.removeColIndicator(a);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const cells = this.hooks.headerDataCells();
|
|
465
|
+
if (!cells.length) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
let slot = cells.length;
|
|
469
|
+
for (let i = 0; i < cells.length; i++) {
|
|
470
|
+
const r = cells[i].getBoundingClientRect();
|
|
471
|
+
if (x < r.left + r.width / 2) {
|
|
472
|
+
slot = i;
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
a.headerSlot = slot;
|
|
477
|
+
if (!a.colIndicator) {
|
|
478
|
+
a.colIndicator = document.createElement('div');
|
|
479
|
+
a.colIndicator.className = 'nxe-col-drop-indicator';
|
|
480
|
+
document.body.appendChild(a.colIndicator);
|
|
481
|
+
}
|
|
482
|
+
const edge = slot < cells.length
|
|
483
|
+
? cells[slot].getBoundingClientRect().left
|
|
484
|
+
: cells[cells.length - 1].getBoundingClientRect().right;
|
|
485
|
+
a.colIndicator.style.left = `${edge - 1}px`;
|
|
486
|
+
a.colIndicator.style.top = `${rowRect.top}px`;
|
|
487
|
+
a.colIndicator.style.height = `${rowRect.height}px`;
|
|
488
|
+
}
|
|
489
|
+
removeColIndicator(a) {
|
|
490
|
+
a.colIndicator?.remove();
|
|
491
|
+
a.colIndicator = null;
|
|
492
|
+
}
|
|
493
|
+
completeDrop() {
|
|
494
|
+
const a = this.drag;
|
|
495
|
+
if (!a) {
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (a.kind === 'header') {
|
|
499
|
+
if (a.overPanel) {
|
|
500
|
+
this.hooks.onGroupDrop(a.col);
|
|
501
|
+
}
|
|
502
|
+
else if (a.headerSlot != null) {
|
|
503
|
+
this.hooks.onColumnMove(a.level, a.headerSlot);
|
|
504
|
+
}
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (!a.overPanel) {
|
|
508
|
+
this.hooks.onChipRemove(a.col);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
if (a.dropIndex != null) {
|
|
512
|
+
const target = a.dropIndex > a.level ? a.dropIndex - 1 : a.dropIndex;
|
|
513
|
+
if (target !== a.level) {
|
|
514
|
+
this.hooks.onChipMove(a.level, target);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
placeSeparator(a, panel, chips, index) {
|
|
519
|
+
if (!a.separator) {
|
|
520
|
+
a.separator = document.createElement('div');
|
|
521
|
+
a.separator.className = 'dx-block-separator';
|
|
522
|
+
a.separator.textContent = ' '; // dx renders the separator as an block
|
|
523
|
+
}
|
|
524
|
+
const reference = chips[index] ?? null;
|
|
525
|
+
if (a.separator.parentElement !== panel || a.separator.nextElementSibling !== reference) {
|
|
526
|
+
panel.insertBefore(a.separator, reference);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
removeSeparator(a) {
|
|
530
|
+
a.separator?.remove();
|
|
531
|
+
a.separator = null;
|
|
532
|
+
}
|
|
533
|
+
panel() {
|
|
534
|
+
return this.hooks.host().querySelector('.dx-datagrid-group-panel');
|
|
535
|
+
}
|
|
536
|
+
headerRow() {
|
|
537
|
+
return nxeLeafHeaderRow(this.hooks.host());
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
542
|
+
// Shared types for the nxe-data-grid family. Row data is `any` to mirror DevExtreme's untyped
|
|
543
|
+
// grid (the app stores plain JS records and reads arbitrary fields off `key.data`).
|
|
544
|
+
|
|
545
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
546
|
+
/**
|
|
547
|
+
* `nxe-column` — drop-in replacement for DevExtreme `dxi-column`.
|
|
548
|
+
*
|
|
549
|
+
* A no-render config element collected by the parent `nxe-data-grid` via `contentChildren`.
|
|
550
|
+
* Carries the column's display/format/behaviour config as signals. Works inside `@for`/`@if`
|
|
551
|
+
* with full property binding (column sets are computed at render time; `dataField`/`caption`
|
|
552
|
+
* are sometimes expressions). There are no command-button columns or `calculate*` options here.
|
|
553
|
+
*/
|
|
554
|
+
class NxeColumnComponent {
|
|
555
|
+
caption = input('', /* @ts-ignore */
|
|
556
|
+
...(ngDevMode ? [{ debugName: "caption" }] : /* istanbul ignore next */ []));
|
|
557
|
+
/** Bound row field (supports dotted paths and bound/ternary expressions). */
|
|
558
|
+
dataField = input('', /* @ts-ignore */
|
|
559
|
+
...(ngDevMode ? [{ debugName: "dataField" }] : /* istanbul ignore next */ []));
|
|
560
|
+
dataType = input('string', /* @ts-ignore */
|
|
561
|
+
...(ngDevMode ? [{ debugName: "dataType" }] : /* istanbul ignore next */ []));
|
|
562
|
+
/** px width — number (`[width]="150"`) or numeric string (`width="200"`). */
|
|
563
|
+
width = input(undefined, /* @ts-ignore */
|
|
564
|
+
...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
|
|
565
|
+
minWidth = input(undefined, /* @ts-ignore */
|
|
566
|
+
...(ngDevMode ? [{ debugName: "minWidth" }] : /* istanbul ignore next */ []));
|
|
567
|
+
alignment = input(undefined, /* @ts-ignore */
|
|
568
|
+
...(ngDevMode ? [{ debugName: "alignment" }] : /* istanbul ignore next */ []));
|
|
569
|
+
/** Number-mask string (`#,##0.00`), date format string, or `{ type: 'dd MMM yyyy' }`. */
|
|
570
|
+
format = input(undefined, /* @ts-ignore */
|
|
571
|
+
...(ngDevMode ? [{ debugName: "format" }] : /* istanbul ignore next */ []));
|
|
572
|
+
/** `(info:{value}) => string` (e.g. `formatAccounting` for parenthesized negatives). */
|
|
573
|
+
customizeText = input(undefined, /* @ts-ignore */
|
|
574
|
+
...(ngDevMode ? [{ debugName: "customizeText" }] : /* istanbul ignore next */ []));
|
|
575
|
+
/** Name of a sibling `<ng-template nxeTemplate="...">` to render the cell. */
|
|
576
|
+
cellTemplate = input(undefined, /* @ts-ignore */
|
|
577
|
+
...(ngDevMode ? [{ debugName: "cellTemplate" }] : /* istanbul ignore next */ []));
|
|
578
|
+
/**
|
|
579
|
+
* DevExtreme column `lookup` (orbit addition): raw values resolve to display text through
|
|
580
|
+
* `dataSource` (`{ lookup: { dataSource, valueExpr: 'lovCode', displayExpr: 'lovName' } }`).
|
|
581
|
+
* Inline cell editing renders a `<select>` over the same list.
|
|
582
|
+
*/
|
|
583
|
+
lookup = input(undefined, /* @ts-ignore */
|
|
584
|
+
...(ngDevMode ? [{ debugName: "lookup" }] : /* istanbul ignore next */ []));
|
|
585
|
+
allowSorting = input(true, /* @ts-ignore */
|
|
586
|
+
...(ngDevMode ? [{ debugName: "allowSorting" }] : /* istanbul ignore next */ []));
|
|
587
|
+
/** dx parity: whether the header can be dragged into the group panel (default true). */
|
|
588
|
+
allowGrouping = input(true, /* @ts-ignore */
|
|
589
|
+
...(ngDevMode ? [{ debugName: "allowGrouping" }] : /* istanbul ignore next */ []));
|
|
590
|
+
/** dx parity: whether this column shows a resize handle when the grid allows resizing. */
|
|
591
|
+
allowResizing = input(true, /* @ts-ignore */
|
|
592
|
+
...(ngDevMode ? [{ debugName: "allowResizing" }] : /* istanbul ignore next */ []));
|
|
593
|
+
allowFiltering = input(true, /* @ts-ignore */
|
|
594
|
+
...(ngDevMode ? [{ debugName: "allowFiltering" }] : /* istanbul ignore next */ []));
|
|
595
|
+
allowEditing = input(true, /* @ts-ignore */
|
|
596
|
+
...(ngDevMode ? [{ debugName: "allowEditing" }] : /* istanbul ignore next */ []));
|
|
597
|
+
allowExporting = input(true, /* @ts-ignore */
|
|
598
|
+
...(ngDevMode ? [{ debugName: "allowExporting" }] : /* istanbul ignore next */ []));
|
|
599
|
+
allowSearch = input(true, /* @ts-ignore */
|
|
600
|
+
...(ngDevMode ? [{ debugName: "allowSearch" }] : /* istanbul ignore next */ []));
|
|
601
|
+
allowHeaderFiltering = input(true, /* @ts-ignore */
|
|
602
|
+
...(ngDevMode ? [{ debugName: "allowHeaderFiltering" }] : /* istanbul ignore next */ []));
|
|
603
|
+
visible = input(true, /* @ts-ignore */
|
|
604
|
+
...(ngDevMode ? [{ debugName: "visible" }] : /* istanbul ignore next */ []));
|
|
605
|
+
sortOrder = input(undefined, /* @ts-ignore */
|
|
606
|
+
...(ngDevMode ? [{ debugName: "sortOrder" }] : /* istanbul ignore next */ []));
|
|
607
|
+
sortIndex = input(undefined, /* @ts-ignore */
|
|
608
|
+
...(ngDevMode ? [{ debugName: "sortIndex" }] : /* istanbul ignore next */ []));
|
|
609
|
+
groupIndex = input(undefined, /* @ts-ignore */
|
|
610
|
+
...(ngDevMode ? [{ debugName: "groupIndex" }] : /* istanbul ignore next */ []));
|
|
611
|
+
fixed = input(false, /* @ts-ignore */
|
|
612
|
+
...(ngDevMode ? [{ debugName: "fixed" }] : /* istanbul ignore next */ []));
|
|
613
|
+
filterValues = input(undefined, /* @ts-ignore */
|
|
614
|
+
...(ngDevMode ? [{ debugName: "filterValues" }] : /* istanbul ignore next */ []));
|
|
615
|
+
/** Column `type` (only `type="date"` seen — a display hint, NOT a command column). */
|
|
616
|
+
type = input(undefined, /* @ts-ignore */
|
|
617
|
+
...(ngDevMode ? [{ debugName: "type" }] : /* istanbul ignore next */ []));
|
|
618
|
+
/**
|
|
619
|
+
* dx `cssClass` — extra classes on this column's header, filter and data cells. The escape
|
|
620
|
+
* hatch for one-off cell styling that does not warrant a `cellTemplate`.
|
|
621
|
+
*/
|
|
622
|
+
cssClass = input(undefined, /* @ts-ignore */
|
|
623
|
+
...(ngDevMode ? [{ debugName: "cssClass" }] : /* istanbul ignore next */ []));
|
|
624
|
+
/**
|
|
625
|
+
* dx `editCellTemplate` — the sibling `<ng-template nxeTemplate="NAME">` rendered while this
|
|
626
|
+
* cell is being edited, in place of the built-in editor. Its context is a
|
|
627
|
+
* {@link NxeEditCellInfo}, whose `data` is the live row: a template that assigns
|
|
628
|
+
* `ctx.data.field = $event` writes straight through, which is how dx's own edit templates
|
|
629
|
+
* were used.
|
|
630
|
+
*/
|
|
631
|
+
editCellTemplate = input(undefined, /* @ts-ignore */
|
|
632
|
+
...(ngDevMode ? [{ debugName: "editCellTemplate" }] : /* istanbul ignore next */ []));
|
|
633
|
+
/**
|
|
634
|
+
* dx `groupCellTemplate` — replaces the `"Caption: value"` text in a group row's caption
|
|
635
|
+
* cell when this column is the one grouped by.
|
|
636
|
+
*/
|
|
637
|
+
groupCellTemplate = input(undefined, /* @ts-ignore */
|
|
638
|
+
...(ngDevMode ? [{ debugName: "groupCellTemplate" }] : /* istanbul ignore next */ []));
|
|
639
|
+
/**
|
|
640
|
+
* Child columns declared inside this one — dx's banded header. Empty for an ordinary column.
|
|
641
|
+
*
|
|
642
|
+
* `descendants: false` is explicit rather than relied upon: a band's grandchildren belong to
|
|
643
|
+
* its children, and flattening them here would collapse the very nesting the band expresses.
|
|
644
|
+
*/
|
|
645
|
+
childColumns = contentChildren(NxeColumnComponent, { ...(ngDevMode ? { debugName: "childColumns" } : /* istanbul ignore next */ {}), descendants: false });
|
|
646
|
+
/** dx `isBand`: a column that groups other columns under a shared header instead of a field. */
|
|
647
|
+
isBand = computed(() => this.childColumns().length > 0, /* @ts-ignore */
|
|
648
|
+
...(ngDevMode ? [{ debugName: "isBand" }] : /* istanbul ignore next */ []));
|
|
649
|
+
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 });
|
|
651
|
+
}
|
|
652
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeColumnComponent, decorators: [{
|
|
653
|
+
type: Component,
|
|
654
|
+
args: [{
|
|
655
|
+
selector: 'nxe-column',
|
|
656
|
+
standalone: true,
|
|
657
|
+
template: '',
|
|
658
|
+
host: { style: 'display: none' },
|
|
659
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
660
|
+
encapsulation: ViewEncapsulation.None,
|
|
661
|
+
}]
|
|
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 }] }] } });
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* `nxe-selection` — drop-in replacement for DevExtreme `dxo-selection` (grid child option).
|
|
666
|
+
*
|
|
667
|
+
* A no-render config element collected by `nxe-data-grid` via `contentChild`. The app only uses
|
|
668
|
+
* `mode` ("single" ×17 / "multiple" ×14, plain string attrs), `selectAllMode="page"`,
|
|
669
|
+
* `showCheckBoxesMode` ("always" ×11 shows the checkbox column; "false"/"none" hide it →
|
|
670
|
+
* selection by row click). `recursive` is tree-list-only (ignored here).
|
|
671
|
+
*/
|
|
672
|
+
class NxeSelectionComponent {
|
|
673
|
+
mode = input('single', /* @ts-ignore */
|
|
674
|
+
...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
|
|
675
|
+
selectAllMode = input('page', /* @ts-ignore */
|
|
676
|
+
...(ngDevMode ? [{ debugName: "selectAllMode" }] : /* istanbul ignore next */ []));
|
|
677
|
+
/** "always" shows the checkbox column; anything else ("false"/"none"/"onClick") hides it. */
|
|
678
|
+
showCheckBoxesMode = input('onClick', /* @ts-ignore */
|
|
679
|
+
...(ngDevMode ? [{ debugName: "showCheckBoxesMode" }] : /* istanbul ignore next */ []));
|
|
680
|
+
recursive = input(false, /* @ts-ignore */
|
|
681
|
+
...(ngDevMode ? [{ debugName: "recursive" }] : /* istanbul ignore next */ []));
|
|
682
|
+
/** dx parity: row-body clicks toggle selection even when checkboxes are shown. */
|
|
683
|
+
selectByClick = input(false, /* @ts-ignore */
|
|
684
|
+
...(ngDevMode ? [{ debugName: "selectByClick" }] : /* istanbul ignore next */ []));
|
|
685
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeSelectionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
686
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.5", type: NxeSelectionComponent, isStandalone: true, selector: "nxe-selection", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, selectAllMode: { classPropertyName: "selectAllMode", publicName: "selectAllMode", isSignal: true, isRequired: false, transformFunction: null }, showCheckBoxesMode: { classPropertyName: "showCheckBoxesMode", publicName: "showCheckBoxesMode", isSignal: true, isRequired: false, transformFunction: null }, recursive: { classPropertyName: "recursive", publicName: "recursive", isSignal: true, isRequired: false, transformFunction: null }, selectByClick: { classPropertyName: "selectByClick", publicName: "selectByClick", isSignal: true, isRequired: false, transformFunction: null } }, host: { styleAttribute: "display: none" }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
687
|
+
}
|
|
688
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeSelectionComponent, decorators: [{
|
|
689
|
+
type: Component,
|
|
690
|
+
args: [{
|
|
691
|
+
selector: 'nxe-selection',
|
|
692
|
+
standalone: true,
|
|
693
|
+
template: '',
|
|
694
|
+
host: { style: 'display: none' },
|
|
695
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
696
|
+
encapsulation: ViewEncapsulation.None,
|
|
697
|
+
}]
|
|
698
|
+
}], 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
|
+
|
|
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
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
741
|
+
/**
|
|
742
|
+
* Header-filter state model — the draft/apply/search machinery extracted from
|
|
743
|
+
* `nxe-data-grid` (pivot parity Phase 3a) so the pivot's field panel can reuse it on pages
|
|
744
|
+
* that have no data grid. Pure signals/computeds, no Angular DI, ZERO behavior change:
|
|
745
|
+
* every computed/method body is a verbatim move from `nxe-data-grid.component.ts`.
|
|
746
|
+
*
|
|
747
|
+
* The model is generic over the column type `C` (the grid passes `NxeColumnComponent`; the
|
|
748
|
+
* pivot will pass its field descriptor) and reads rows/values through a
|
|
749
|
+
* {@link NxeHeaderFilterSource} adapter, so it owns only the frozen semantics:
|
|
750
|
+
* `(Blanks)` first, Yes/No boolean buckets, the date Year > Month > Day tree, dx search
|
|
751
|
+
* scoping of (Select All), check-on-open, and "everything checked = no filter" on apply.
|
|
752
|
+
*/
|
|
753
|
+
/**
|
|
754
|
+
* Header-filter key for blank-ish values (`null`/`undefined`/`''`) — dx lists them as one
|
|
755
|
+
* `(Blanks)` bucket, FIRST (bright audit). A symbol can never collide with data values.
|
|
756
|
+
*/
|
|
757
|
+
const NXE_HF_BLANKS = Symbol('nxe-hf-blanks');
|
|
758
|
+
/**
|
|
759
|
+
* Normalizes a raw cell value to its header-filter key: blank-ish → the `(Blanks)` sentinel,
|
|
760
|
+
* date columns → a `yyyy-MM-dd` day key (the tree's leaf unit), else the raw value.
|
|
761
|
+
*/
|
|
762
|
+
function nxeHfKey(raw, isDate) {
|
|
763
|
+
if (raw == null || raw === '') {
|
|
764
|
+
return NXE_HF_BLANKS;
|
|
765
|
+
}
|
|
766
|
+
if (isDate) {
|
|
767
|
+
const dt = parseDateValue(raw);
|
|
768
|
+
return dt ? dt.toFormat('yyyy-MM-dd') : String(raw);
|
|
769
|
+
}
|
|
770
|
+
return raw;
|
|
771
|
+
}
|
|
772
|
+
class NxeHeaderFilterModel {
|
|
773
|
+
source;
|
|
774
|
+
static MONTH_NAMES = [
|
|
775
|
+
'January', 'February', 'March', 'April', 'May', 'June',
|
|
776
|
+
'July', 'August', 'September', 'October', 'November', 'December',
|
|
777
|
+
];
|
|
778
|
+
/** The column whose popup is open (`null` = closed). */
|
|
779
|
+
column = signal(null, /* @ts-ignore */
|
|
780
|
+
...(ngDevMode ? [{ debugName: "column" }] : /* istanbul ignore next */ []));
|
|
781
|
+
/** The checked key set being edited in the popup (committed by the host on OK). */
|
|
782
|
+
draft = signal(new Set(), /* @ts-ignore */
|
|
783
|
+
...(ngDevMode ? [{ debugName: "draft" }] : /* istanbul ignore next */ []));
|
|
784
|
+
search = signal('', /* @ts-ignore */
|
|
785
|
+
...(ngDevMode ? [{ debugName: "search" }] : /* istanbul ignore next */ []));
|
|
786
|
+
expanded = signal(new Set(), /* @ts-ignore */
|
|
787
|
+
...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
|
|
788
|
+
constructor(source) {
|
|
789
|
+
this.source = source;
|
|
790
|
+
}
|
|
791
|
+
/** This column's header-filter key for a raw value. */
|
|
792
|
+
key(column, raw) {
|
|
793
|
+
return nxeHfKey(raw, this.source.isDate(column));
|
|
794
|
+
}
|
|
795
|
+
/** Distinct {key, text} for the open column — `(Blanks)` first, then Yes/No or values ascending. */
|
|
796
|
+
distinct = computed(() => {
|
|
797
|
+
const col = this.column();
|
|
798
|
+
if (!col) {
|
|
799
|
+
return [];
|
|
800
|
+
}
|
|
801
|
+
const seen = new Map();
|
|
802
|
+
for (const row of this.source.rows()) {
|
|
803
|
+
const raw = this.source.rawValue(col, row);
|
|
804
|
+
const key = this.key(col, raw);
|
|
805
|
+
if (!seen.has(key)) {
|
|
806
|
+
seen.set(key, {
|
|
807
|
+
key,
|
|
808
|
+
raw,
|
|
809
|
+
text: key === NXE_HF_BLANKS
|
|
810
|
+
? '(Blanks)'
|
|
811
|
+
: (this.source.boolLabel(col, raw) ?? this.source.displayText(col, row)),
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
// dx sorts lookup columns by display text, everything else by raw value.
|
|
816
|
+
const byDisplay = this.source.sortByDisplayText(col);
|
|
817
|
+
return [...seen.values()]
|
|
818
|
+
.sort((a, b) => {
|
|
819
|
+
if (a.key === NXE_HF_BLANKS) {
|
|
820
|
+
return -1;
|
|
821
|
+
}
|
|
822
|
+
if (b.key === NXE_HF_BLANKS) {
|
|
823
|
+
return 1;
|
|
824
|
+
}
|
|
825
|
+
const la = this.source.boolLabel(col, a.raw);
|
|
826
|
+
const lb = this.source.boolLabel(col, b.raw);
|
|
827
|
+
if (la && lb) {
|
|
828
|
+
return la === lb ? 0 : la === 'Yes' ? -1 : 1;
|
|
829
|
+
}
|
|
830
|
+
return byDisplay ? a.text.localeCompare(b.text) : this.source.compare(a.raw, b.raw);
|
|
831
|
+
})
|
|
832
|
+
.map(({ key, text }) => ({ key, text }));
|
|
833
|
+
}, /* @ts-ignore */
|
|
834
|
+
...(ngDevMode ? [{ debugName: "distinct" }] : /* istanbul ignore next */ []));
|
|
835
|
+
distinctFiltered = computed(() => {
|
|
836
|
+
const q = this.search().trim().toLowerCase();
|
|
837
|
+
return q ? this.distinct().filter((d) => d.text.toLowerCase().includes(q)) : this.distinct();
|
|
838
|
+
}, /* @ts-ignore */
|
|
839
|
+
...(ngDevMode ? [{ debugName: "distinctFiltered" }] : /* istanbul ignore next */ []));
|
|
840
|
+
// --- Date-column header filter: Year > Month > Day tree (dx groupInterval default) ----------
|
|
841
|
+
isDate = computed(() => {
|
|
842
|
+
const col = this.column();
|
|
843
|
+
return !!col && this.source.isDate(col);
|
|
844
|
+
}, /* @ts-ignore */
|
|
845
|
+
...(ngDevMode ? [{ debugName: "isDate" }] : /* istanbul ignore next */ []));
|
|
846
|
+
dateTree = computed(() => {
|
|
847
|
+
const col = this.column();
|
|
848
|
+
if (!col || !this.source.isDate(col)) {
|
|
849
|
+
return [];
|
|
850
|
+
}
|
|
851
|
+
const days = new Set();
|
|
852
|
+
const unparseable = new Map();
|
|
853
|
+
let hasBlanks = false;
|
|
854
|
+
for (const row of this.source.rows()) {
|
|
855
|
+
const key = this.key(col, this.source.rawValue(col, row));
|
|
856
|
+
if (key === NXE_HF_BLANKS) {
|
|
857
|
+
hasBlanks = true;
|
|
858
|
+
}
|
|
859
|
+
else if (typeof key === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(key)) {
|
|
860
|
+
days.add(key);
|
|
861
|
+
}
|
|
862
|
+
else {
|
|
863
|
+
// nxeHfKey's String(raw) fallback for unparseable dates — a flat leaf, not Y>M>D
|
|
864
|
+
unparseable.set(String(key), key);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
const out = [];
|
|
868
|
+
if (hasBlanks) {
|
|
869
|
+
out.push({ label: '(Blanks)', key: 'blanks', children: [], leafKeys: [NXE_HF_BLANKS] });
|
|
870
|
+
}
|
|
871
|
+
const byYear = new Map();
|
|
872
|
+
for (const day of [...days].sort()) {
|
|
873
|
+
const year = day.slice(0, 4);
|
|
874
|
+
const month = Number(day.slice(5, 7));
|
|
875
|
+
if (!byYear.has(year)) {
|
|
876
|
+
byYear.set(year, new Map());
|
|
877
|
+
}
|
|
878
|
+
const months = byYear.get(year);
|
|
879
|
+
if (!months.has(month)) {
|
|
880
|
+
months.set(month, []);
|
|
881
|
+
}
|
|
882
|
+
months.get(month).push(day);
|
|
883
|
+
}
|
|
884
|
+
for (const [year, months] of byYear) {
|
|
885
|
+
const monthNodes = [];
|
|
886
|
+
for (const [month, dayKeys] of months) {
|
|
887
|
+
monthNodes.push({
|
|
888
|
+
label: NxeHeaderFilterModel.MONTH_NAMES[month - 1],
|
|
889
|
+
key: `m:${year}-${month}`,
|
|
890
|
+
children: dayKeys.map((d) => ({
|
|
891
|
+
label: String(Number(d.slice(8, 10))),
|
|
892
|
+
key: `d:${d}`,
|
|
893
|
+
children: [],
|
|
894
|
+
leafKeys: [d],
|
|
895
|
+
})),
|
|
896
|
+
leafKeys: dayKeys,
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
out.push({
|
|
900
|
+
label: year,
|
|
901
|
+
key: `y:${year}`,
|
|
902
|
+
children: monthNodes,
|
|
903
|
+
leafKeys: monthNodes.flatMap((m) => m.leafKeys),
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
for (const [label, key] of [...unparseable].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
907
|
+
out.push({ label, key: `o:${label}`, children: [], leafKeys: [key] });
|
|
908
|
+
}
|
|
909
|
+
return out;
|
|
910
|
+
}, /* @ts-ignore */
|
|
911
|
+
...(ngDevMode ? [{ debugName: "dateTree" }] : /* istanbul ignore next */ []));
|
|
912
|
+
/**
|
|
913
|
+
* Search prunes the tree dx-style: a node stays if its label matches or a descendant does;
|
|
914
|
+
* a matched-label node with no matching descendants collapses to a leaf (keeps all its keys).
|
|
915
|
+
*/
|
|
916
|
+
dateTreeFiltered = computed(() => {
|
|
917
|
+
const q = this.search().trim().toLowerCase();
|
|
918
|
+
if (!q) {
|
|
919
|
+
return this.dateTree();
|
|
920
|
+
}
|
|
921
|
+
const prune = (nodes) => nodes
|
|
922
|
+
.map((n) => {
|
|
923
|
+
const kids = prune(n.children);
|
|
924
|
+
if (kids.length) {
|
|
925
|
+
return { ...n, children: kids, leafKeys: kids.flatMap((k) => k.leafKeys) };
|
|
926
|
+
}
|
|
927
|
+
if (n.label.toLowerCase().includes(q)) {
|
|
928
|
+
return { ...n, children: [] };
|
|
929
|
+
}
|
|
930
|
+
return null;
|
|
931
|
+
})
|
|
932
|
+
.filter((n) => !!n);
|
|
933
|
+
return prune(this.dateTree());
|
|
934
|
+
}, /* @ts-ignore */
|
|
935
|
+
...(ngDevMode ? [{ debugName: "dateTreeFiltered" }] : /* istanbul ignore next */ []));
|
|
936
|
+
toggleExpand(key) {
|
|
937
|
+
this.expanded.update((set) => {
|
|
938
|
+
const next = new Set(set);
|
|
939
|
+
next.has(key) ? next.delete(key) : next.add(key);
|
|
940
|
+
return next;
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
/** While searching every surviving branch shows expanded (dx auto-expands matches). */
|
|
944
|
+
nodeExpanded(key) {
|
|
945
|
+
return this.search().trim() ? true : this.expanded().has(key);
|
|
946
|
+
}
|
|
947
|
+
nodeChecked(n) {
|
|
948
|
+
const draft = this.draft();
|
|
949
|
+
return n.leafKeys.length > 0 && n.leafKeys.every((k) => draft.has(k));
|
|
950
|
+
}
|
|
951
|
+
nodeIndeterminate(n) {
|
|
952
|
+
const draft = this.draft();
|
|
953
|
+
return !this.nodeChecked(n) && n.leafKeys.some((k) => draft.has(k));
|
|
954
|
+
}
|
|
955
|
+
toggleNode(n) {
|
|
956
|
+
const removing = this.nodeChecked(n);
|
|
957
|
+
this.draft.update((set) => {
|
|
958
|
+
const next = new Set(set);
|
|
959
|
+
for (const k of n.leafKeys) {
|
|
960
|
+
removing ? next.delete(k) : next.add(k);
|
|
961
|
+
}
|
|
962
|
+
return next;
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
/** Every selectable key of the open column (tree leaves for dates, list keys otherwise). */
|
|
966
|
+
allKeys = computed(() => this.isDate()
|
|
967
|
+
? this.dateTree().flatMap((n) => n.leafKeys)
|
|
968
|
+
: this.distinct().map((d) => d.key), /* @ts-ignore */
|
|
969
|
+
...(ngDevMode ? [{ debugName: "allKeys" }] : /* istanbul ignore next */ []));
|
|
970
|
+
/** The keys currently shown in the popup — with a search active, only the matches. */
|
|
971
|
+
visibleKeys = computed(() => this.isDate()
|
|
972
|
+
? this.dateTreeFiltered().flatMap((n) => n.leafKeys)
|
|
973
|
+
: this.distinctFiltered().map((d) => d.key), /* @ts-ignore */
|
|
974
|
+
...(ngDevMode ? [{ debugName: "visibleKeys" }] : /* istanbul ignore next */ []));
|
|
975
|
+
/** (Select All) state/toggle operate on the VISIBLE keys (dx: search scopes Select All). */
|
|
976
|
+
allChecked = computed(() => {
|
|
977
|
+
const all = this.visibleKeys();
|
|
978
|
+
const draft = this.draft();
|
|
979
|
+
return all.length > 0 && all.every((k) => draft.has(k));
|
|
980
|
+
}, /* @ts-ignore */
|
|
981
|
+
...(ngDevMode ? [{ debugName: "allChecked" }] : /* istanbul ignore next */ []));
|
|
982
|
+
/** Whether every key of the whole column is checked — decides "no filter" on apply. */
|
|
983
|
+
fullChecked = computed(() => {
|
|
984
|
+
const all = this.allKeys();
|
|
985
|
+
const draft = this.draft();
|
|
986
|
+
return all.length > 0 && all.every((k) => draft.has(k));
|
|
987
|
+
}, /* @ts-ignore */
|
|
988
|
+
...(ngDevMode ? [{ debugName: "fullChecked" }] : /* istanbul ignore next */ []));
|
|
989
|
+
/**
|
|
990
|
+
* Reset the popup state for `column` (check-on-open — FROZEN, see DEFAULTS.md):
|
|
991
|
+
* no applied filter → everything checked; else start from the applied set.
|
|
992
|
+
*/
|
|
993
|
+
open(column, applied) {
|
|
994
|
+
this.column.set(column);
|
|
995
|
+
this.search.set('');
|
|
996
|
+
this.expanded.set(new Set());
|
|
997
|
+
this.draft.set(applied ? new Set(applied) : new Set(this.allKeys()));
|
|
998
|
+
}
|
|
999
|
+
close() {
|
|
1000
|
+
this.column.set(null);
|
|
1001
|
+
}
|
|
1002
|
+
toggleValue(value) {
|
|
1003
|
+
this.draft.update((set) => {
|
|
1004
|
+
const next = new Set(set);
|
|
1005
|
+
next.has(value) ? next.delete(value) : next.add(value);
|
|
1006
|
+
return next;
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
toggleAll() {
|
|
1010
|
+
const visible = this.visibleKeys();
|
|
1011
|
+
const removing = this.allChecked();
|
|
1012
|
+
this.draft.update((set) => {
|
|
1013
|
+
const next = new Set(set);
|
|
1014
|
+
for (const k of visible) {
|
|
1015
|
+
removing ? next.delete(k) : next.add(k);
|
|
1016
|
+
}
|
|
1017
|
+
return next;
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* The set the host stores on OK: everything checked (or nothing to choose from) =
|
|
1022
|
+
* explicit "no filter" (`null` so a declarative seed can't resurrect); else the
|
|
1023
|
+
* allowed-key set (empty = empty grid).
|
|
1024
|
+
*/
|
|
1025
|
+
applyResult() {
|
|
1026
|
+
const clear = this.fullChecked() || this.allKeys().length === 0;
|
|
1027
|
+
return clear ? null : new Set(this.draft());
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
1032
|
+
/**
|
|
1033
|
+
* The header-filter popup panel — the `.nxe-hf-*` chunk extracted from `nxe-data-grid`'s
|
|
1034
|
+
* template (pivot parity Phase 3a) so the pivot can open the same popup on pages with no
|
|
1035
|
+
* data grid. Same DOM, same FROZEN classes (`.nxe-hf-item`/`.nxe-hf-search`/… — DEFAULTS.md):
|
|
1036
|
+
* the host element carries `dx-header-filter-menu nxe-hf-panel`, exactly where the old
|
|
1037
|
+
* template's root `<div>` sat inside the overlay pane.
|
|
1038
|
+
*
|
|
1039
|
+
* Renders whatever {@link NxeHeaderFilterModel} it is given — checkbox list or the date
|
|
1040
|
+
* Year > Month > Day tree — and emits `applied`/`cancelled` for the host to commit/close;
|
|
1041
|
+
* the panel itself never touches the host's filter state.
|
|
1042
|
+
*/
|
|
1043
|
+
class NxeHeaderFilterPanelComponent {
|
|
1044
|
+
model = input.required(/* @ts-ignore */
|
|
1045
|
+
...(ngDevMode ? [{ debugName: "model" }] : /* istanbul ignore next */ []));
|
|
1046
|
+
/** Live predicate (re-read every CD, like the grid template did) — a mid-open config
|
|
1047
|
+
change to `search.enabled` still shows/hides the box. */
|
|
1048
|
+
searchEnabled = input(() => true, /* @ts-ignore */
|
|
1049
|
+
...(ngDevMode ? [{ debugName: "searchEnabled" }] : /* istanbul ignore next */ []));
|
|
1050
|
+
/** OK — the host commits the draft and closes. */
|
|
1051
|
+
applied = output();
|
|
1052
|
+
/** Cancel — the host closes without committing. */
|
|
1053
|
+
cancelled = output();
|
|
1054
|
+
showSearch() {
|
|
1055
|
+
return this.searchEnabled()();
|
|
1056
|
+
}
|
|
1057
|
+
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 });
|
|
1059
|
+
}
|
|
1060
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeHeaderFilterPanelComponent, decorators: [{
|
|
1061
|
+
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"] }] } });
|
|
1064
|
+
|
|
1065
|
+
/** Create + attach the header-filter popup; the caller keeps the ref to detach/dispose. */
|
|
1066
|
+
function openNxeHeaderFilterOverlay(options) {
|
|
1067
|
+
const overlayRef = options.overlay.create({
|
|
1068
|
+
positionStrategy: options.overlay
|
|
1069
|
+
.position()
|
|
1070
|
+
.flexibleConnectedTo(options.origin)
|
|
1071
|
+
.withPositions([
|
|
1072
|
+
{ originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top' },
|
|
1073
|
+
{ originX: 'end', originY: 'bottom', overlayX: 'end', overlayY: 'top' },
|
|
1074
|
+
]),
|
|
1075
|
+
scrollStrategy: options.overlay.scrollStrategies.reposition(),
|
|
1076
|
+
hasBackdrop: true,
|
|
1077
|
+
backdropClass: 'cdk-overlay-transparent-backdrop',
|
|
1078
|
+
panelClass: nxePane('nxe-hf-pane'),
|
|
1079
|
+
});
|
|
1080
|
+
overlayRef.backdropClick().subscribe(() => options.onClose());
|
|
1081
|
+
// Escape closes the filter menu (dx). This subscription is also what makes CDK deliver the
|
|
1082
|
+
// key here at all: OverlayKeyboardDispatcher dispatches to the topmost overlay that HAS a
|
|
1083
|
+
// keydown observer, so without it Escape fell through to whatever overlay sits underneath —
|
|
1084
|
+
// on the pivot that is the field chooser, which would close and strand this popup on a
|
|
1085
|
+
// detached anchor.
|
|
1086
|
+
overlayRef.keydownEvents().subscribe((event) => {
|
|
1087
|
+
if (event.key === 'Escape') {
|
|
1088
|
+
event.stopPropagation();
|
|
1089
|
+
options.onClose();
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
const panelRef = overlayRef.attach(new ComponentPortal(NxeHeaderFilterPanelComponent, options.viewContainerRef));
|
|
1093
|
+
panelRef.setInput('model', options.model);
|
|
1094
|
+
panelRef.setInput('searchEnabled', options.searchEnabled);
|
|
1095
|
+
panelRef.instance.applied.subscribe(() => options.onApply());
|
|
1096
|
+
panelRef.instance.cancelled.subscribe(() => options.onClose());
|
|
1097
|
+
return overlayRef;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/** `dxo-grouping` — only `[autoExpandAll]="true"` is used (24 grids). */
|
|
1101
|
+
class NxeGroupingComponent {
|
|
1102
|
+
autoExpandAll = input(true, /* @ts-ignore */
|
|
1103
|
+
...(ngDevMode ? [{ debugName: "autoExpandAll" }] : /* istanbul ignore next */ []));
|
|
1104
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeGroupingComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1105
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.5", type: NxeGroupingComponent, isStandalone: true, selector: "nxe-grouping", inputs: { autoExpandAll: { classPropertyName: "autoExpandAll", publicName: "autoExpandAll", isSignal: true, isRequired: false, transformFunction: null } }, host: { styleAttribute: "display: none" }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
1106
|
+
}
|
|
1107
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeGroupingComponent, decorators: [{
|
|
1108
|
+
type: Component,
|
|
1109
|
+
args: [{
|
|
1110
|
+
selector: 'nxe-grouping',
|
|
1111
|
+
standalone: true,
|
|
1112
|
+
template: '',
|
|
1113
|
+
host: { style: 'display: none' },
|
|
1114
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1115
|
+
encapsulation: ViewEncapsulation.None,
|
|
1116
|
+
}]
|
|
1117
|
+
}], propDecorators: { autoExpandAll: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoExpandAll", required: false }] }] } });
|
|
1118
|
+
/** `dxo-group-panel` — only `[visible]` is used (23 grids). */
|
|
1119
|
+
class NxeGroupPanelComponent {
|
|
1120
|
+
visible = input(false, /* @ts-ignore */
|
|
1121
|
+
...(ngDevMode ? [{ debugName: "visible" }] : /* istanbul ignore next */ []));
|
|
1122
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeGroupPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1123
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.5", type: NxeGroupPanelComponent, isStandalone: true, selector: "nxe-group-panel", inputs: { visible: { classPropertyName: "visible", publicName: "visible", isSignal: true, isRequired: false, transformFunction: null } }, host: { styleAttribute: "display: none" }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
1124
|
+
}
|
|
1125
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeGroupPanelComponent, decorators: [{
|
|
1126
|
+
type: Component,
|
|
1127
|
+
args: [{
|
|
1128
|
+
selector: 'nxe-group-panel',
|
|
1129
|
+
standalone: true,
|
|
1130
|
+
template: '',
|
|
1131
|
+
host: { style: 'display: none' },
|
|
1132
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1133
|
+
encapsulation: ViewEncapsulation.None,
|
|
1134
|
+
}]
|
|
1135
|
+
}], propDecorators: { visible: [{ type: i0.Input, args: [{ isSignal: true, alias: "visible", required: false }] }] } });
|
|
1136
|
+
|
|
1137
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
1138
|
+
/** Shared base for total/group summary items. The app only uses `summaryType="sum"`. */
|
|
1139
|
+
class NxeSummaryItemBase {
|
|
1140
|
+
column = input('', /* @ts-ignore */
|
|
1141
|
+
...(ngDevMode ? [{ debugName: "column" }] : /* istanbul ignore next */ []));
|
|
1142
|
+
summaryType = input('sum', /* @ts-ignore */
|
|
1143
|
+
...(ngDevMode ? [{ debugName: "summaryType" }] : /* istanbul ignore next */ []));
|
|
1144
|
+
/** Identifier passed to `calculateCustomSummary` for `summaryType='custom'` items. */
|
|
1145
|
+
name = input(undefined, /* @ts-ignore */
|
|
1146
|
+
...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
|
|
1147
|
+
/** Positional template; `{0}` is replaced by the customized/formatted value. */
|
|
1148
|
+
displayFormat = input('{0}', /* @ts-ignore */
|
|
1149
|
+
...(ngDevMode ? [{ debugName: "displayFormat" }] : /* istanbul ignore next */ []));
|
|
1150
|
+
customizeText = input(undefined, /* @ts-ignore */
|
|
1151
|
+
...(ngDevMode ? [{ debugName: "customizeText" }] : /* istanbul ignore next */ []));
|
|
1152
|
+
valueFormat = input(undefined, /* @ts-ignore */
|
|
1153
|
+
...(ngDevMode ? [{ debugName: "valueFormat" }] : /* istanbul ignore next */ []));
|
|
1154
|
+
alignByColumn = input(false, /* @ts-ignore */
|
|
1155
|
+
...(ngDevMode ? [{ debugName: "alignByColumn" }] : /* istanbul ignore next */ []));
|
|
1156
|
+
/** Render this group aggregate in a footer row under the group instead of the group row. */
|
|
1157
|
+
showInGroupFooter = input(false, /* @ts-ignore */
|
|
1158
|
+
...(ngDevMode ? [{ debugName: "showInGroupFooter" }] : /* istanbul ignore next */ []));
|
|
1159
|
+
showInColumn = input(undefined, /* @ts-ignore */
|
|
1160
|
+
...(ngDevMode ? [{ debugName: "showInColumn" }] : /* istanbul ignore next */ []));
|
|
1161
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeSummaryItemBase, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1162
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.5", type: NxeSummaryItemBase, isStandalone: true, inputs: { column: { classPropertyName: "column", publicName: "column", isSignal: true, isRequired: false, transformFunction: null }, summaryType: { classPropertyName: "summaryType", publicName: "summaryType", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, displayFormat: { classPropertyName: "displayFormat", publicName: "displayFormat", isSignal: true, isRequired: false, transformFunction: null }, customizeText: { classPropertyName: "customizeText", publicName: "customizeText", isSignal: true, isRequired: false, transformFunction: null }, valueFormat: { classPropertyName: "valueFormat", publicName: "valueFormat", isSignal: true, isRequired: false, transformFunction: null }, alignByColumn: { classPropertyName: "alignByColumn", publicName: "alignByColumn", isSignal: true, isRequired: false, transformFunction: null }, showInGroupFooter: { classPropertyName: "showInGroupFooter", publicName: "showInGroupFooter", isSignal: true, isRequired: false, transformFunction: null }, showInColumn: { classPropertyName: "showInColumn", publicName: "showInColumn", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
|
|
1163
|
+
}
|
|
1164
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeSummaryItemBase, decorators: [{
|
|
1165
|
+
type: Directive
|
|
1166
|
+
}], propDecorators: { column: [{ type: i0.Input, args: [{ isSignal: true, alias: "column", required: false }] }], summaryType: [{ type: i0.Input, args: [{ isSignal: true, alias: "summaryType", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], displayFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayFormat", required: false }] }], customizeText: [{ type: i0.Input, args: [{ isSignal: true, alias: "customizeText", required: false }] }], valueFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueFormat", required: false }] }], alignByColumn: [{ type: i0.Input, args: [{ isSignal: true, alias: "alignByColumn", required: false }] }], showInGroupFooter: [{ type: i0.Input, args: [{ isSignal: true, alias: "showInGroupFooter", required: false }] }], showInColumn: [{ type: i0.Input, args: [{ isSignal: true, alias: "showInColumn", required: false }] }] } });
|
|
1167
|
+
/** `dxi-total-item` — a grand-total footer aggregate. */
|
|
1168
|
+
class NxeTotalItemComponent extends NxeSummaryItemBase {
|
|
1169
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeTotalItemComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
1170
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.5", type: NxeTotalItemComponent, isStandalone: true, selector: "nxe-total-item", host: { styleAttribute: "display: none" }, usesInheritance: true, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
1171
|
+
}
|
|
1172
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeTotalItemComponent, decorators: [{
|
|
1173
|
+
type: Component,
|
|
1174
|
+
args: [{
|
|
1175
|
+
selector: 'nxe-total-item',
|
|
1176
|
+
standalone: true,
|
|
1177
|
+
template: '',
|
|
1178
|
+
host: { style: 'display: none' },
|
|
1179
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1180
|
+
encapsulation: ViewEncapsulation.None,
|
|
1181
|
+
}]
|
|
1182
|
+
}] });
|
|
1183
|
+
/** `dxi-group-item` — a per-group footer aggregate. */
|
|
1184
|
+
class NxeGroupItemComponent extends NxeSummaryItemBase {
|
|
1185
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeGroupItemComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
1186
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.5", type: NxeGroupItemComponent, isStandalone: true, selector: "nxe-group-item", host: { styleAttribute: "display: none" }, usesInheritance: true, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
1187
|
+
}
|
|
1188
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeGroupItemComponent, decorators: [{
|
|
1189
|
+
type: Component,
|
|
1190
|
+
args: [{
|
|
1191
|
+
selector: 'nxe-group-item',
|
|
1192
|
+
standalone: true,
|
|
1193
|
+
template: '',
|
|
1194
|
+
host: { style: 'display: none' },
|
|
1195
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1196
|
+
encapsulation: ViewEncapsulation.None,
|
|
1197
|
+
}]
|
|
1198
|
+
}] });
|
|
1199
|
+
/** `dxo-summary` — container collecting `nxe-total-item` / `nxe-group-item` children. */
|
|
1200
|
+
class NxeSummaryComponent {
|
|
1201
|
+
/**
|
|
1202
|
+
* DevExtreme `calculateCustomSummary` (orbit addition): called with the dx protocol —
|
|
1203
|
+
* `{name, summaryProcess:'start'}`, then `'calculate'` once per row (`options.value` = the
|
|
1204
|
+
* row object), then `'finalize'`; the handler mutates `options.totalValue`.
|
|
1205
|
+
*/
|
|
1206
|
+
calculateCustomSummary = input(undefined, /* @ts-ignore */
|
|
1207
|
+
...(ngDevMode ? [{ debugName: "calculateCustomSummary" }] : /* istanbul ignore next */ []));
|
|
1208
|
+
totalItems = contentChildren(NxeTotalItemComponent, /* @ts-ignore */
|
|
1209
|
+
...(ngDevMode ? [{ debugName: "totalItems" }] : /* istanbul ignore next */ []));
|
|
1210
|
+
groupItems = contentChildren(NxeGroupItemComponent, /* @ts-ignore */
|
|
1211
|
+
...(ngDevMode ? [{ debugName: "groupItems" }] : /* istanbul ignore next */ []));
|
|
1212
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeSummaryComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1213
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.1.5", type: NxeSummaryComponent, isStandalone: true, selector: "nxe-summary", inputs: { calculateCustomSummary: { classPropertyName: "calculateCustomSummary", publicName: "calculateCustomSummary", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "totalItems", predicate: NxeTotalItemComponent, isSignal: true }, { propertyName: "groupItems", predicate: NxeGroupItemComponent, isSignal: true }], ngImport: i0, template: '<ng-content />', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
1214
|
+
}
|
|
1215
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeSummaryComponent, decorators: [{
|
|
1216
|
+
type: Component,
|
|
1217
|
+
args: [{
|
|
1218
|
+
selector: 'nxe-summary',
|
|
1219
|
+
standalone: true,
|
|
1220
|
+
template: '<ng-content />',
|
|
1221
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1222
|
+
encapsulation: ViewEncapsulation.None,
|
|
1223
|
+
}]
|
|
1224
|
+
}], propDecorators: { calculateCustomSummary: [{ type: i0.Input, args: [{ isSignal: true, alias: "calculateCustomSummary", required: false }] }], totalItems: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => NxeTotalItemComponent), { isSignal: true }] }], groupItems: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => NxeGroupItemComponent), { isSignal: true }] }] } });
|
|
1225
|
+
|
|
1226
|
+
/** `dxo-paging` — internal client paging. Presence enables paging; `[pageSize]`/`[pageIndex]` used. */
|
|
1227
|
+
class NxePagingComponent {
|
|
1228
|
+
pageSize = input(20, /* @ts-ignore */
|
|
1229
|
+
...(ngDevMode ? [{ debugName: "pageSize" }] : /* istanbul ignore next */ []));
|
|
1230
|
+
pageIndex = input(0, /* @ts-ignore */
|
|
1231
|
+
...(ngDevMode ? [{ debugName: "pageIndex" }] : /* istanbul ignore next */ []));
|
|
1232
|
+
enabled = input(true, /* @ts-ignore */
|
|
1233
|
+
...(ngDevMode ? [{ debugName: "enabled" }] : /* istanbul ignore next */ []));
|
|
1234
|
+
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 });
|
|
1236
|
+
}
|
|
1237
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxePagingComponent, decorators: [{
|
|
1238
|
+
type: Component,
|
|
1239
|
+
args: [{
|
|
1240
|
+
selector: 'nxe-paging',
|
|
1241
|
+
standalone: true,
|
|
1242
|
+
template: '',
|
|
1243
|
+
host: { style: 'display: none' },
|
|
1244
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1245
|
+
encapsulation: ViewEncapsulation.None,
|
|
1246
|
+
}]
|
|
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 }] }] } });
|
|
1248
|
+
/** `dxo-pager` — the pager UI config. */
|
|
1249
|
+
class NxePagerComponent {
|
|
1250
|
+
visible = input('auto', /* @ts-ignore */
|
|
1251
|
+
...(ngDevMode ? [{ debugName: "visible" }] : /* istanbul ignore next */ []));
|
|
1252
|
+
allowedPageSizes = input('auto', /* @ts-ignore */
|
|
1253
|
+
...(ngDevMode ? [{ debugName: "allowedPageSizes" }] : /* istanbul ignore next */ []));
|
|
1254
|
+
showPageSizeSelector = input(false, /* @ts-ignore */
|
|
1255
|
+
...(ngDevMode ? [{ debugName: "showPageSizeSelector" }] : /* istanbul ignore next */ []));
|
|
1256
|
+
showNavigationButtons = input(false, /* @ts-ignore */
|
|
1257
|
+
...(ngDevMode ? [{ debugName: "showNavigationButtons" }] : /* istanbul ignore next */ []));
|
|
1258
|
+
showInfo = input(false, /* @ts-ignore */
|
|
1259
|
+
...(ngDevMode ? [{ debugName: "showInfo" }] : /* istanbul ignore next */ []));
|
|
1260
|
+
displayMode = input('adaptive', /* @ts-ignore */
|
|
1261
|
+
...(ngDevMode ? [{ debugName: "displayMode" }] : /* istanbul ignore next */ []));
|
|
1262
|
+
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 });
|
|
1264
|
+
}
|
|
1265
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxePagerComponent, decorators: [{
|
|
1266
|
+
type: Component,
|
|
1267
|
+
args: [{
|
|
1268
|
+
selector: 'nxe-pager',
|
|
1269
|
+
standalone: true,
|
|
1270
|
+
template: '',
|
|
1271
|
+
host: { style: 'display: none' },
|
|
1272
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1273
|
+
encapsulation: ViewEncapsulation.None,
|
|
1274
|
+
}]
|
|
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 }] }] } });
|
|
1276
|
+
|
|
1277
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
1278
|
+
/**
|
|
1279
|
+
* `dxo-editing` — inline editing config. In this app editing is almost always template-driven
|
|
1280
|
+
* (cell templates host `nxe-*` editors two-way-bound to `key.data.<field>`), so `mode="cell"` +
|
|
1281
|
+
* `allowUpdating` are effectively decorative there. `mode="row"` with `[(changes)]`/`[(editRowKey)]`
|
|
1282
|
+
* (autonumber-detail) and native cell editors (job-upsert) drive the grid's native inline editing.
|
|
1283
|
+
*/
|
|
1284
|
+
class NxeEditingComponent {
|
|
1285
|
+
mode = input('cell', /* @ts-ignore */
|
|
1286
|
+
...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
|
|
1287
|
+
allowUpdating = input(false, /* @ts-ignore */
|
|
1288
|
+
...(ngDevMode ? [{ debugName: "allowUpdating" }] : /* istanbul ignore next */ []));
|
|
1289
|
+
allowAdding = input(false, /* @ts-ignore */
|
|
1290
|
+
...(ngDevMode ? [{ debugName: "allowAdding" }] : /* istanbul ignore next */ []));
|
|
1291
|
+
allowDeleting = input(false, /* @ts-ignore */
|
|
1292
|
+
...(ngDevMode ? [{ debugName: "allowDeleting" }] : /* istanbul ignore next */ []));
|
|
1293
|
+
/** Two-way tracked changes (autonumber-detail's `Change<T>[]`). */
|
|
1294
|
+
changes = model([], /* @ts-ignore */
|
|
1295
|
+
...(ngDevMode ? [{ debugName: "changes" }] : /* istanbul ignore next */ []));
|
|
1296
|
+
/** Two-way key of the row currently in edit mode (row mode). */
|
|
1297
|
+
editRowKey = model(null, /* @ts-ignore */
|
|
1298
|
+
...(ngDevMode ? [{ debugName: "editRowKey" }] : /* istanbul ignore next */ []));
|
|
1299
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeEditingComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1300
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.5", type: NxeEditingComponent, isStandalone: true, selector: "nxe-editing", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, allowUpdating: { classPropertyName: "allowUpdating", publicName: "allowUpdating", isSignal: true, isRequired: false, transformFunction: null }, allowAdding: { classPropertyName: "allowAdding", publicName: "allowAdding", isSignal: true, isRequired: false, transformFunction: null }, allowDeleting: { classPropertyName: "allowDeleting", publicName: "allowDeleting", isSignal: true, isRequired: false, transformFunction: null }, changes: { classPropertyName: "changes", publicName: "changes", isSignal: true, isRequired: false, transformFunction: null }, editRowKey: { classPropertyName: "editRowKey", publicName: "editRowKey", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { changes: "changesChange", editRowKey: "editRowKeyChange" }, host: { styleAttribute: "display: none" }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
1301
|
+
}
|
|
1302
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeEditingComponent, decorators: [{
|
|
1303
|
+
type: Component,
|
|
1304
|
+
args: [{
|
|
1305
|
+
selector: 'nxe-editing',
|
|
1306
|
+
standalone: true,
|
|
1307
|
+
template: '',
|
|
1308
|
+
host: { style: 'display: none' },
|
|
1309
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1310
|
+
encapsulation: ViewEncapsulation.None,
|
|
1311
|
+
}]
|
|
1312
|
+
}], propDecorators: { mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], allowUpdating: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowUpdating", required: false }] }], allowAdding: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowAdding", required: false }] }], allowDeleting: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowDeleting", required: false }] }], changes: [{ type: i0.Input, args: [{ isSignal: true, alias: "changes", required: false }] }, { type: i0.Output, args: ["changesChange"] }], editRowKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "editRowKey", required: false }] }, { type: i0.Output, args: ["editRowKeyChange"] }] } });
|
|
1313
|
+
|
|
1314
|
+
/**
|
|
1315
|
+
* `nxe-filter-row` — drop-in for DevExtreme `dxo-filter-row` (orbit addition; psak expressed
|
|
1316
|
+
* the same config through the grid's `[filterRow]` object input, which remains supported).
|
|
1317
|
+
* A no-render config element read by the parent `nxe-data-grid`.
|
|
1318
|
+
*/
|
|
1319
|
+
class NxeFilterRowComponent {
|
|
1320
|
+
visible = input(false, /* @ts-ignore */
|
|
1321
|
+
...(ngDevMode ? [{ debugName: "visible" }] : /* istanbul ignore next */ []));
|
|
1322
|
+
/** DevExtreme parity (`auto`/`onClick`); the nxe filter row always applies on input. */
|
|
1323
|
+
applyFilter = input(undefined, /* @ts-ignore */
|
|
1324
|
+
...(ngDevMode ? [{ debugName: "applyFilter" }] : /* istanbul ignore next */ []));
|
|
1325
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeFilterRowComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1326
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.5", type: NxeFilterRowComponent, isStandalone: true, selector: "nxe-filter-row", inputs: { visible: { classPropertyName: "visible", publicName: "visible", isSignal: true, isRequired: false, transformFunction: null }, applyFilter: { classPropertyName: "applyFilter", publicName: "applyFilter", isSignal: true, isRequired: false, transformFunction: null } }, host: { styleAttribute: "display: none" }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
1327
|
+
}
|
|
1328
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeFilterRowComponent, decorators: [{
|
|
1329
|
+
type: Component,
|
|
1330
|
+
args: [{
|
|
1331
|
+
selector: 'nxe-filter-row',
|
|
1332
|
+
standalone: true,
|
|
1333
|
+
template: '',
|
|
1334
|
+
host: { style: 'display: none' },
|
|
1335
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1336
|
+
encapsulation: ViewEncapsulation.None,
|
|
1337
|
+
}]
|
|
1338
|
+
}], propDecorators: { visible: [{ type: i0.Input, args: [{ isSignal: true, alias: "visible", required: false }] }], applyFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "applyFilter", required: false }] }] } });
|
|
1339
|
+
|
|
1340
|
+
/**
|
|
1341
|
+
* `nxe-master-detail` — drop-in for DevExtreme `dxo-master-detail` (orbit addition).
|
|
1342
|
+
*
|
|
1343
|
+
* A no-render config element read by the parent `nxe-data-grid`. When `enabled`, the grid
|
|
1344
|
+
* prepends a `dx-command-expand` column; expanding a row renders a
|
|
1345
|
+
* `<tr class="dx-master-detail-row">` hosting the named sibling
|
|
1346
|
+
* `<ng-template nxeTemplate="...">` with `{ data, rowIndex, key }` — instantiated ONLY while
|
|
1347
|
+
* expanded, preserving the app's lazy nested-grid loading (`onRowExpanding` fires first).
|
|
1348
|
+
*/
|
|
1349
|
+
class NxeMasterDetailComponent {
|
|
1350
|
+
enabled = input(false, /* @ts-ignore */
|
|
1351
|
+
...(ngDevMode ? [{ debugName: "enabled" }] : /* istanbul ignore next */ []));
|
|
1352
|
+
/** Name of the sibling `nxeTemplate` rendered as the detail section (default `detail`). */
|
|
1353
|
+
template = input('detail', /* @ts-ignore */
|
|
1354
|
+
...(ngDevMode ? [{ debugName: "template" }] : /* istanbul ignore next */ []));
|
|
1355
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeMasterDetailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1356
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.5", type: NxeMasterDetailComponent, isStandalone: true, selector: "nxe-master-detail", inputs: { enabled: { classPropertyName: "enabled", publicName: "enabled", isSignal: true, isRequired: false, transformFunction: null }, template: { classPropertyName: "template", publicName: "template", isSignal: true, isRequired: false, transformFunction: null } }, host: { styleAttribute: "display: none" }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
1357
|
+
}
|
|
1358
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeMasterDetailComponent, decorators: [{
|
|
1359
|
+
type: Component,
|
|
1360
|
+
args: [{
|
|
1361
|
+
selector: 'nxe-master-detail',
|
|
1362
|
+
standalone: true,
|
|
1363
|
+
template: '',
|
|
1364
|
+
host: { style: 'display: none' },
|
|
1365
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1366
|
+
encapsulation: ViewEncapsulation.None,
|
|
1367
|
+
}]
|
|
1368
|
+
}], propDecorators: { enabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "enabled", required: false }] }], template: [{ type: i0.Input, args: [{ isSignal: true, alias: "template", required: false }] }] } });
|
|
1369
|
+
|
|
1370
|
+
/**
|
|
1371
|
+
* `dxo-scrolling` — accepted for parity.
|
|
1372
|
+
*
|
|
1373
|
+
* Grid: the only usage is `rowRenderingMode="virtual"` (ledger-list), which renders only
|
|
1374
|
+
* visible rows but loads all data; nxe-data-grid renders all rows (datasets are small
|
|
1375
|
+
* enough), so it stays a no-op config there.
|
|
1376
|
+
*
|
|
1377
|
+
* Pivot (LIVE since W4 Lane 3): `NxePivotGridComponent` contentChild's this and
|
|
1378
|
+
* `mode: 'virtual'` arms ROW WINDOWING — for `rowHeaderLayout: 'tree'` ONLY (the standard
|
|
1379
|
+
* layout's rowspan-merged parent labels conflict with row slicing; DEFAULTS.md). Render-only:
|
|
1380
|
+
* fixed 31px row pitch, spacer rows keep the scrollbar honest, and the model/export/chart
|
|
1381
|
+
* always see every row. The DA page projects `mode="virtual"` but renders the standard
|
|
1382
|
+
* layout → no-op there, dx-consistent with its pane-based virtual scrolling being a render
|
|
1383
|
+
* optimization, never a data change.
|
|
1384
|
+
*/
|
|
1385
|
+
class NxeScrollingComponent {
|
|
1386
|
+
mode = input('standard', /* @ts-ignore */
|
|
1387
|
+
...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
|
|
1388
|
+
rowRenderingMode = input('standard', /* @ts-ignore */
|
|
1389
|
+
...(ngDevMode ? [{ debugName: "rowRenderingMode" }] : /* istanbul ignore next */ []));
|
|
1390
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeScrollingComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1391
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.5", type: NxeScrollingComponent, isStandalone: true, selector: "nxe-scrolling", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, rowRenderingMode: { classPropertyName: "rowRenderingMode", publicName: "rowRenderingMode", isSignal: true, isRequired: false, transformFunction: null } }, host: { styleAttribute: "display: none" }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
1392
|
+
}
|
|
1393
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeScrollingComponent, decorators: [{
|
|
1394
|
+
type: Component,
|
|
1395
|
+
args: [{
|
|
1396
|
+
selector: 'nxe-scrolling',
|
|
1397
|
+
standalone: true,
|
|
1398
|
+
template: '',
|
|
1399
|
+
host: { style: 'display: none' },
|
|
1400
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1401
|
+
encapsulation: ViewEncapsulation.None,
|
|
1402
|
+
}]
|
|
1403
|
+
}], propDecorators: { mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], rowRenderingMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowRenderingMode", required: false }] }] } });
|
|
1404
|
+
|
|
1405
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
1406
|
+
function getRaw(row, field) {
|
|
1407
|
+
if (!field || row == null) {
|
|
1408
|
+
return undefined;
|
|
1409
|
+
}
|
|
1410
|
+
return field.includes('.') ? field.split('.').reduce((o, k) => o?.[k], row) : row[field];
|
|
1411
|
+
}
|
|
1412
|
+
/** A cell's exceljs value + optional number format. */
|
|
1413
|
+
function cellValue(row, col) {
|
|
1414
|
+
const raw = getRaw(row, col.dataField);
|
|
1415
|
+
if (raw == null || raw === '') {
|
|
1416
|
+
return { value: '' };
|
|
1417
|
+
}
|
|
1418
|
+
switch (col.dataType) {
|
|
1419
|
+
case 'number': {
|
|
1420
|
+
const num = typeof raw === 'number' ? raw : Number(raw);
|
|
1421
|
+
if (isNaN(num)) {
|
|
1422
|
+
return { value: String(raw) };
|
|
1423
|
+
}
|
|
1424
|
+
// dx number masks ≈ exceljs numFmt; convert single-quoted literals to double-quoted.
|
|
1425
|
+
const mask = typeof col.format === 'string' ? col.format.replace(/'/g, '"') : undefined;
|
|
1426
|
+
return { value: num, numFmt: mask };
|
|
1427
|
+
}
|
|
1428
|
+
case 'date': {
|
|
1429
|
+
const dt = parseDateValue(raw);
|
|
1430
|
+
if (!dt) {
|
|
1431
|
+
return { value: String(raw) };
|
|
1432
|
+
}
|
|
1433
|
+
// exceljs date codes are lowercase (dd mmm yyyy hh:mm).
|
|
1434
|
+
return { value: dt.toJSDate(), numFmt: nxeDatePattern(col.format).toLowerCase() };
|
|
1435
|
+
}
|
|
1436
|
+
case 'boolean':
|
|
1437
|
+
return { value: raw ? 'true' : 'false' };
|
|
1438
|
+
default:
|
|
1439
|
+
return { value: String(raw) };
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
/**
|
|
1443
|
+
* Replacement for DevExtreme's `exportDataGrid({component, worksheet, autoFilterEnabled})`.
|
|
1444
|
+
* Walks an {@link NxeGridInstance}'s currently-visible columns + filtered/sorted rows into an
|
|
1445
|
+
* exceljs sheet (header row + autofilter + per-column numFmt), defaults the font to 'Calibri
|
|
1446
|
+
* Light' (preserving the app's `formatExcel`), and saves via `nxeSaveAs` — matching the prior
|
|
1447
|
+
* `report.helper.exportExcel` pipeline.
|
|
1448
|
+
*/
|
|
1449
|
+
async function exportNxeGridToExcel(grid, opts) {
|
|
1450
|
+
const workbook = new Workbook();
|
|
1451
|
+
const ws = workbook.addWorksheet(opts.sheetName);
|
|
1452
|
+
const cols = grid.getVisibleColumns();
|
|
1453
|
+
const rows = grid.getVisibleRows();
|
|
1454
|
+
ws.addRow(cols.map((c) => c.caption));
|
|
1455
|
+
for (const row of rows) {
|
|
1456
|
+
const cells = cols.map((c) => cellValue(row, c));
|
|
1457
|
+
const r = ws.addRow(cells.map((c) => c.value));
|
|
1458
|
+
cells.forEach((c, i) => {
|
|
1459
|
+
if (c.numFmt) {
|
|
1460
|
+
r.getCell(i + 1).numFmt = c.numFmt;
|
|
1461
|
+
}
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
if (opts.autoFilter !== false && cols.length > 0) {
|
|
1465
|
+
ws.autoFilter = { from: { row: 1, column: 1 }, to: { row: 1, column: cols.length } };
|
|
1466
|
+
}
|
|
1467
|
+
if (opts.applyFont !== false) {
|
|
1468
|
+
ws.eachRow((row) => row.eachCell((cell) => {
|
|
1469
|
+
if (!cell.font?.name) {
|
|
1470
|
+
cell.font = Object.assign(cell.font || {}, { name: 'Calibri Light' });
|
|
1471
|
+
}
|
|
1472
|
+
}));
|
|
1473
|
+
}
|
|
1474
|
+
const buffer = await workbook.xlsx.writeBuffer();
|
|
1475
|
+
const fileName = opts.fileName.endsWith('.xlsx') ? opts.fileName : `${opts.fileName}.xlsx`;
|
|
1476
|
+
nxeSaveAs(new Blob([buffer], { type: 'application/octet-stream' }), fileName);
|
|
1477
|
+
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Drop-in for DevExtreme's `exportDataGrid({component, worksheet, autoFilterEnabled})`
|
|
1480
|
+
* (orbit addition): writes the grid's visible columns + rows into the CALLER's worksheet —
|
|
1481
|
+
* the caller keeps ownership of the workbook, post-formatting, and saving
|
|
1482
|
+
* (`report.helper.exportExcel` pipeline).
|
|
1483
|
+
*/
|
|
1484
|
+
async function exportDataGrid(opts) {
|
|
1485
|
+
const ws = opts.worksheet;
|
|
1486
|
+
const cols = opts.component.getVisibleColumns();
|
|
1487
|
+
const rows = opts.component.getVisibleRows();
|
|
1488
|
+
ws.addRow(cols.map((c) => c.caption));
|
|
1489
|
+
for (const row of rows) {
|
|
1490
|
+
const cells = cols.map((c) => cellValue(row, c));
|
|
1491
|
+
const r = ws.addRow(cells.map((c) => c.value));
|
|
1492
|
+
cells.forEach((c, i) => {
|
|
1493
|
+
if (c.numFmt) {
|
|
1494
|
+
r.getCell(i + 1).numFmt = c.numFmt;
|
|
1495
|
+
}
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
if (opts.autoFilterEnabled && cols.length > 0) {
|
|
1499
|
+
ws.autoFilter = { from: { row: 1, column: 1 }, to: { row: 1, column: cols.length } };
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
class NxeGridResizeController {
|
|
1504
|
+
hooks;
|
|
1505
|
+
active = null;
|
|
1506
|
+
suppression = new NxeGestureClickSuppression();
|
|
1507
|
+
onMove = (e) => this.pointerMove(e);
|
|
1508
|
+
onUp = (e) => {
|
|
1509
|
+
if (this.active && e.pointerId !== this.active.pointerId) {
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
this.pointerUp();
|
|
1513
|
+
};
|
|
1514
|
+
onCancelEv = (e) => {
|
|
1515
|
+
if (this.active && e.pointerId !== this.active.pointerId) {
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
this.cancel();
|
|
1519
|
+
};
|
|
1520
|
+
onKey = (e) => {
|
|
1521
|
+
if (e.key === 'Escape') {
|
|
1522
|
+
this.cancel();
|
|
1523
|
+
}
|
|
1524
|
+
};
|
|
1525
|
+
constructor(hooks) {
|
|
1526
|
+
this.hooks = hooks;
|
|
1527
|
+
}
|
|
1528
|
+
/** `visibleIndex`/`lead` locate the header cell + `<col>`; `nextField` only in nextColumn mode. */
|
|
1529
|
+
start(field, nextField, visibleIndex, lead, event) {
|
|
1530
|
+
if (event.button !== 0 || this.active) {
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
event.stopPropagation(); // the header td above would otherwise start a group drag
|
|
1534
|
+
event.preventDefault();
|
|
1535
|
+
const host = this.hooks.host();
|
|
1536
|
+
const table = host.querySelector('.dx-datagrid-content > .dx-datagrid-table');
|
|
1537
|
+
const headerRow = nxeLeafHeaderRow(host);
|
|
1538
|
+
if (!table || !headerRow) {
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
const cols = Array.from(table.querySelectorAll(':scope > colgroup > col'));
|
|
1542
|
+
// The colgroup still counts command columns, so it keeps the lead offset; the header row
|
|
1543
|
+
// does not, because with bands the command cells span down from the row above.
|
|
1544
|
+
const headerCells = nxeHeaderDataCells(headerRow);
|
|
1545
|
+
const colEl = cols[lead + visibleIndex];
|
|
1546
|
+
const cell = headerCells[visibleIndex];
|
|
1547
|
+
if (!colEl || !cell) {
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
const nextColEl = nextField != null ? cols[lead + visibleIndex + 1] ?? null : null;
|
|
1551
|
+
const nextCell = nextField != null ? headerCells[visibleIndex + 1] : null;
|
|
1552
|
+
this.active = {
|
|
1553
|
+
field,
|
|
1554
|
+
nextField,
|
|
1555
|
+
pointerId: event.pointerId,
|
|
1556
|
+
startX: event.clientX,
|
|
1557
|
+
startW: cell.getBoundingClientRect().width,
|
|
1558
|
+
startNextW: nextCell ? nextCell.getBoundingClientRect().width : 0,
|
|
1559
|
+
colEl,
|
|
1560
|
+
nextColEl,
|
|
1561
|
+
tableEl: table,
|
|
1562
|
+
startTableW: table.getBoundingClientRect().width,
|
|
1563
|
+
prevColStyle: colEl.style.width,
|
|
1564
|
+
prevNextStyle: nextColEl?.style.width ?? '',
|
|
1565
|
+
prevTableStyle: table.style.width,
|
|
1566
|
+
lastW: cell.getBoundingClientRect().width,
|
|
1567
|
+
lastNextW: nextCell ? nextCell.getBoundingClientRect().width : 0,
|
|
1568
|
+
};
|
|
1569
|
+
this.hooks.onStart();
|
|
1570
|
+
document.body.classList.add('nxe-grid-resizing');
|
|
1571
|
+
this.hooks.zone.runOutsideAngular(() => {
|
|
1572
|
+
document.addEventListener('pointermove', this.onMove);
|
|
1573
|
+
document.addEventListener('pointerup', this.onUp);
|
|
1574
|
+
document.addEventListener('pointercancel', this.onCancelEv);
|
|
1575
|
+
document.addEventListener('keydown', this.onKey);
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
consumeClick() {
|
|
1579
|
+
return this.suppression.consume();
|
|
1580
|
+
}
|
|
1581
|
+
destroy() {
|
|
1582
|
+
if (this.active) {
|
|
1583
|
+
this.cancel();
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
pointerMove(e) {
|
|
1587
|
+
const a = this.active;
|
|
1588
|
+
if (!a || e.pointerId !== a.pointerId) {
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
const min = this.hooks.minWidth();
|
|
1592
|
+
const delta = e.clientX - a.startX;
|
|
1593
|
+
if (this.hooks.mode() === 'widget' || !a.nextColEl) {
|
|
1594
|
+
const w = Math.max(min, a.startW + delta);
|
|
1595
|
+
a.colEl.style.width = `${w}px`;
|
|
1596
|
+
a.tableEl.style.width = `${a.startTableW + (w - a.startW)}px`;
|
|
1597
|
+
a.lastW = w;
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
// nextColumn: trade with the neighbor, both clamped, table width constant.
|
|
1601
|
+
let w = Math.max(min, a.startW + delta);
|
|
1602
|
+
let next = a.startNextW - (w - a.startW);
|
|
1603
|
+
if (next < min) {
|
|
1604
|
+
next = min;
|
|
1605
|
+
w = a.startW + a.startNextW - min;
|
|
1606
|
+
}
|
|
1607
|
+
a.colEl.style.width = `${w}px`;
|
|
1608
|
+
a.nextColEl.style.width = `${next}px`;
|
|
1609
|
+
a.lastW = w;
|
|
1610
|
+
a.lastNextW = next;
|
|
1611
|
+
}
|
|
1612
|
+
pointerUp() {
|
|
1613
|
+
const a = this.active;
|
|
1614
|
+
this.teardown();
|
|
1615
|
+
if (!a) {
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
this.suppression.arm();
|
|
1619
|
+
// A zero-movement release is a plain click on the handle — no freeze, no no-op event.
|
|
1620
|
+
if (a.lastW === a.startW && a.lastNextW === a.startNextW) {
|
|
1621
|
+
this.hooks.zone.run(() => this.hooks.onCancel());
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
const changes = [
|
|
1625
|
+
{ field: a.field, from: a.startW, to: a.lastW },
|
|
1626
|
+
];
|
|
1627
|
+
if (a.nextField && a.lastNextW !== a.startNextW) {
|
|
1628
|
+
changes.push({ field: a.nextField, from: a.startNextW, to: a.lastNextW });
|
|
1629
|
+
}
|
|
1630
|
+
this.hooks.zone.run(() => this.hooks.onCommit(changes));
|
|
1631
|
+
}
|
|
1632
|
+
cancel() {
|
|
1633
|
+
const a = this.active;
|
|
1634
|
+
this.teardown();
|
|
1635
|
+
if (!a) {
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
this.suppression.arm();
|
|
1639
|
+
a.colEl.style.width = a.prevColStyle;
|
|
1640
|
+
if (a.nextColEl) {
|
|
1641
|
+
a.nextColEl.style.width = a.prevNextStyle;
|
|
1642
|
+
}
|
|
1643
|
+
a.tableEl.style.width = a.prevTableStyle;
|
|
1644
|
+
this.hooks.zone.run(() => this.hooks.onCancel());
|
|
1645
|
+
}
|
|
1646
|
+
teardown() {
|
|
1647
|
+
this.active = null;
|
|
1648
|
+
document.body.classList.remove('nxe-grid-resizing');
|
|
1649
|
+
document.removeEventListener('pointermove', this.onMove);
|
|
1650
|
+
document.removeEventListener('pointerup', this.onUp);
|
|
1651
|
+
document.removeEventListener('pointercancel', this.onCancelEv);
|
|
1652
|
+
document.removeEventListener('keydown', this.onKey);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
1657
|
+
/** Classify a rendered `<tr>` for dx's `rowType`, or null for rows dx does not report. */
|
|
1658
|
+
function nxeRowTypeOf(tr) {
|
|
1659
|
+
const c = tr.classList;
|
|
1660
|
+
if (c.contains('dx-header-row')) {
|
|
1661
|
+
return 'header';
|
|
1662
|
+
}
|
|
1663
|
+
if (c.contains('dx-datagrid-filter-row')) {
|
|
1664
|
+
return 'filter';
|
|
1665
|
+
}
|
|
1666
|
+
if (c.contains('dx-data-row')) {
|
|
1667
|
+
return 'data';
|
|
1668
|
+
}
|
|
1669
|
+
if (c.contains('dx-group-footer-row')) {
|
|
1670
|
+
return 'groupFooter';
|
|
1671
|
+
}
|
|
1672
|
+
if (c.contains('dx-group-row')) {
|
|
1673
|
+
return 'group';
|
|
1674
|
+
}
|
|
1675
|
+
if (tr.parentElement?.classList.contains('dx-datagrid-total-footer')) {
|
|
1676
|
+
return 'totalFooter';
|
|
1677
|
+
}
|
|
1678
|
+
return null;
|
|
1679
|
+
}
|
|
1680
|
+
/** Depth-first leaves of a declared column tree: a band contributes its children, not itself. */
|
|
1681
|
+
function flattenLeafColumns(cols) {
|
|
1682
|
+
const out = [];
|
|
1683
|
+
for (const col of cols) {
|
|
1684
|
+
if (col.isBand()) {
|
|
1685
|
+
out.push(...flattenLeafColumns(col.childColumns()));
|
|
1686
|
+
}
|
|
1687
|
+
else {
|
|
1688
|
+
out.push(col);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
return out;
|
|
1692
|
+
}
|
|
1693
|
+
/**
|
|
1694
|
+
* `nxe-data-grid` — drop-in replacement for DevExtreme `dx-data-grid` (read-only core, slice 9a).
|
|
1695
|
+
*
|
|
1696
|
+
* Renders a single `<table>` emitting the dx DOM-class contract (`dx-widget dx-datagrid`,
|
|
1697
|
+
* `dx-row`/`dx-header-row`/`dx-data-row`/`dx-datagrid-headers`/`dx-datagrid-rowsview`, `td`s) so
|
|
1698
|
+
* the globally-loaded `dx.light.css` + repo `_grid.scss` overrides style it pixel-for-pixel
|
|
1699
|
+
* (4px cell padding, #f8fafc header, #525252 header text, row/hover/alt colours). Columns are
|
|
1700
|
+
* collected from `nxe-column` content children; named `cellTemplate`s resolve to sibling
|
|
1701
|
+
* `<ng-template nxeTemplate="name">` blocks rendered with `{ data, rowIndex, columnIndex, ... }`
|
|
1702
|
+
* (the live row object, so in-cell `nxe-*` editors can write through `key.data.<field>`).
|
|
1703
|
+
*
|
|
1704
|
+
* The `.instance` facade exposes only what the app calls: `getSelectedRowsData()` (12 template
|
|
1705
|
+
* sites), `refresh()` (13), `columnOption(field,'visible',bool)` (3). Selection UI, header-filter,
|
|
1706
|
+
* grouping and summary land in slice 9b.
|
|
1707
|
+
*/
|
|
1708
|
+
class NxeDataGridComponent {
|
|
1709
|
+
elementRef = inject((ElementRef));
|
|
1710
|
+
overlay = inject(Overlay);
|
|
1711
|
+
viewContainerRef = inject(ViewContainerRef);
|
|
1712
|
+
ngZone = inject(NgZone);
|
|
1713
|
+
defaults = inject(NXE_DEFAULT_OPTIONS);
|
|
1714
|
+
/** Pointer drag engine: drag-to-group, chip drag-out (ungroup), chip/column reorder. */
|
|
1715
|
+
drag = new NxeGridDragController({
|
|
1716
|
+
zone: this.ngZone,
|
|
1717
|
+
host: () => this.elementRef.nativeElement,
|
|
1718
|
+
onGroupDrop: (col) => this.groupByColumn(col),
|
|
1719
|
+
onChipRemove: (col) => this.ungroupColumn(col.dataField()),
|
|
1720
|
+
onChipMove: (from, to) => this.moveGroup(from, to),
|
|
1721
|
+
reorderEnabled: () => this.allowColumnReordering(),
|
|
1722
|
+
onColumnMove: (from, slot) => this.moveColumnByVisible(from, slot),
|
|
1723
|
+
headerDataCells: () => nxeHeaderDataCells(nxeLeafHeaderRow(this.elementRef.nativeElement)),
|
|
1724
|
+
});
|
|
1725
|
+
/** Pointer resize engine (dx allowColumnResizing). */
|
|
1726
|
+
resize = new NxeGridResizeController({
|
|
1727
|
+
zone: this.ngZone,
|
|
1728
|
+
host: () => this.elementRef.nativeElement,
|
|
1729
|
+
mode: () => this.columnResizingMode(),
|
|
1730
|
+
minWidth: () => this.columnMinWidth() ?? 50,
|
|
1731
|
+
onStart: () => (this.resizeActive = true),
|
|
1732
|
+
onCancel: () => (this.resizeActive = false),
|
|
1733
|
+
onCommit: (changes) => this.commitColumnResize(changes),
|
|
1734
|
+
});
|
|
1735
|
+
resizeActive = false;
|
|
1736
|
+
/**
|
|
1737
|
+
* Columns exactly as declared: top-level leaves and bands, bands still holding their children.
|
|
1738
|
+
* Only the header model reads this — everything else wants leaves.
|
|
1739
|
+
*/
|
|
1740
|
+
declaredColumns = contentChildren(NxeColumnComponent, { ...(ngDevMode ? { debugName: "declaredColumns" } : /* istanbul ignore next */ {}), descendants: false });
|
|
1741
|
+
/**
|
|
1742
|
+
* The data columns: bands flattened away, depth-first, in declaration order.
|
|
1743
|
+
*
|
|
1744
|
+
* Keeping `columns()` meaning "the columns that carry data" is what stops banded headers
|
|
1745
|
+
* being a rewrite. Sorting, grouping, filtering, summaries, resize, export and the whole
|
|
1746
|
+
* `<tbody>` are about leaves and continue to work untouched; bands exist only in `<thead>`.
|
|
1747
|
+
*/
|
|
1748
|
+
columns = computed(() => flattenLeafColumns(this.declaredColumns()), /* @ts-ignore */
|
|
1749
|
+
...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
|
|
1750
|
+
/** True when any declared column is a band, i.e. the header needs more than one row. */
|
|
1751
|
+
hasBands = computed(() => this.declaredColumns().some((c) => c.isBand()), /* @ts-ignore */
|
|
1752
|
+
...(ngDevMode ? [{ debugName: "hasBands" }] : /* istanbul ignore next */ []));
|
|
1753
|
+
templates = contentChildren(NxeTemplateDirective, /* @ts-ignore */
|
|
1754
|
+
...(ngDevMode ? [{ debugName: "templates" }] : /* istanbul ignore next */ []));
|
|
1755
|
+
selection = contentChild(NxeSelectionComponent, /* @ts-ignore */
|
|
1756
|
+
...(ngDevMode ? [{ debugName: "selection" }] : /* istanbul ignore next */ []));
|
|
1757
|
+
headerFilter = contentChild(NxeHeaderFilterComponent, /* @ts-ignore */
|
|
1758
|
+
...(ngDevMode ? [{ debugName: "headerFilter" }] : /* istanbul ignore next */ []));
|
|
1759
|
+
grouping = contentChild(NxeGroupingComponent, /* @ts-ignore */
|
|
1760
|
+
...(ngDevMode ? [{ debugName: "grouping" }] : /* istanbul ignore next */ []));
|
|
1761
|
+
groupPanel = contentChild(NxeGroupPanelComponent, /* @ts-ignore */
|
|
1762
|
+
...(ngDevMode ? [{ debugName: "groupPanel" }] : /* istanbul ignore next */ []));
|
|
1763
|
+
summaryCmp = contentChild(NxeSummaryComponent, /* @ts-ignore */
|
|
1764
|
+
...(ngDevMode ? [{ debugName: "summaryCmp" }] : /* istanbul ignore next */ []));
|
|
1765
|
+
pagingCmp = contentChild(NxePagingComponent, /* @ts-ignore */
|
|
1766
|
+
...(ngDevMode ? [{ debugName: "pagingCmp" }] : /* istanbul ignore next */ []));
|
|
1767
|
+
pagerCmp = contentChild(NxePagerComponent, /* @ts-ignore */
|
|
1768
|
+
...(ngDevMode ? [{ debugName: "pagerCmp" }] : /* istanbul ignore next */ []));
|
|
1769
|
+
editingCmp = contentChild(NxeEditingComponent, /* @ts-ignore */
|
|
1770
|
+
...(ngDevMode ? [{ debugName: "editingCmp" }] : /* istanbul ignore next */ []));
|
|
1771
|
+
filterRowCmp = contentChild(NxeFilterRowComponent, /* @ts-ignore */
|
|
1772
|
+
...(ngDevMode ? [{ debugName: "filterRowCmp" }] : /* istanbul ignore next */ []));
|
|
1773
|
+
masterDetail = contentChild(NxeMasterDetailComponent, /* @ts-ignore */
|
|
1774
|
+
...(ngDevMode ? [{ debugName: "masterDetail" }] : /* istanbul ignore next */ []));
|
|
1775
|
+
// --- Inputs (mirror dx-data-grid; object-literal inputs accepted as-is for drop-in parity) ---
|
|
1776
|
+
dataSource = input([], /* @ts-ignore */
|
|
1777
|
+
...(ngDevMode ? [{ debugName: "dataSource" }] : /* istanbul ignore next */ []));
|
|
1778
|
+
keyExpr = input(undefined, /* @ts-ignore */
|
|
1779
|
+
...(ngDevMode ? [{ debugName: "keyExpr" }] : /* istanbul ignore next */ []));
|
|
1780
|
+
// Presentation options resolve as `binding ?? NXE_DEFAULT_OPTIONS ?? literal`. The raw input
|
|
1781
|
+
// is aliased so the element keeps its dx name, and the public member stays the resolved
|
|
1782
|
+
// value — every read site, template included, goes through the cascade without knowing.
|
|
1783
|
+
//
|
|
1784
|
+
// The default has to be `undefined`, not `false`: "unset" is a distinct state from "set to
|
|
1785
|
+
// false", and only the former defers to the token. A `booleanAttribute` transform here would
|
|
1786
|
+
// fold one into the other and silently disable the middle rung.
|
|
1787
|
+
hoverStateEnabledInput = input(undefined, { ...(ngDevMode ? { debugName: "hoverStateEnabledInput" } : /* istanbul ignore next */ {}), alias: 'hoverStateEnabled' });
|
|
1788
|
+
showColumnLinesInput = input(undefined, { ...(ngDevMode ? { debugName: "showColumnLinesInput" } : /* istanbul ignore next */ {}), alias: 'showColumnLines' });
|
|
1789
|
+
showRowLinesInput = input(undefined, { ...(ngDevMode ? { debugName: "showRowLinesInput" } : /* istanbul ignore next */ {}), alias: 'showRowLines' });
|
|
1790
|
+
showBordersInput = input(undefined, { ...(ngDevMode ? { debugName: "showBordersInput" } : /* istanbul ignore next */ {}), alias: 'showBorders' });
|
|
1791
|
+
wordWrapEnabledInput = input(undefined, { ...(ngDevMode ? { debugName: "wordWrapEnabledInput" } : /* istanbul ignore next */ {}), alias: 'wordWrapEnabled' });
|
|
1792
|
+
columnAutoWidthInput = input(undefined, { ...(ngDevMode ? { debugName: "columnAutoWidthInput" } : /* istanbul ignore next */ {}), alias: 'columnAutoWidth' });
|
|
1793
|
+
rowAlternationEnabledInput = input(undefined, { ...(ngDevMode ? { debugName: "rowAlternationEnabledInput" } : /* istanbul ignore next */ {}), alias: 'rowAlternationEnabled' });
|
|
1794
|
+
/** dx resolves `nxe-tree-list` against the `treeList` slice, not `dataGrid`. */
|
|
1795
|
+
widgetDefaults = computed(() => (this.treeMode ? this.defaults.treeList : this.defaults.dataGrid) ?? {}, /* @ts-ignore */
|
|
1796
|
+
...(ngDevMode ? [{ debugName: "widgetDefaults" }] : /* istanbul ignore next */ []));
|
|
1797
|
+
hoverStateEnabled = computed(() => this.hoverStateEnabledInput() ?? this.widgetDefaults().hoverStateEnabled ?? false, /* @ts-ignore */
|
|
1798
|
+
...(ngDevMode ? [{ debugName: "hoverStateEnabled" }] : /* istanbul ignore next */ []));
|
|
1799
|
+
showColumnLines = computed(() => this.showColumnLinesInput() ?? this.widgetDefaults().showColumnLines ?? false, /* @ts-ignore */
|
|
1800
|
+
...(ngDevMode ? [{ debugName: "showColumnLines" }] : /* istanbul ignore next */ []));
|
|
1801
|
+
showRowLines = computed(() => this.showRowLinesInput() ?? this.widgetDefaults().showRowLines ?? true, /* @ts-ignore */
|
|
1802
|
+
...(ngDevMode ? [{ debugName: "showRowLines" }] : /* istanbul ignore next */ []));
|
|
1803
|
+
showBorders = computed(() => this.showBordersInput() ?? this.widgetDefaults().showBorders ?? false, /* @ts-ignore */
|
|
1804
|
+
...(ngDevMode ? [{ debugName: "showBorders" }] : /* istanbul ignore next */ []));
|
|
1805
|
+
wordWrapEnabled = computed(() => this.wordWrapEnabledInput() ?? this.widgetDefaults().wordWrapEnabled ?? true, /* @ts-ignore */
|
|
1806
|
+
...(ngDevMode ? [{ debugName: "wordWrapEnabled" }] : /* istanbul ignore next */ []));
|
|
1807
|
+
columnAutoWidth = computed(() => this.columnAutoWidthInput() ?? this.widgetDefaults().columnAutoWidth ?? false, /* @ts-ignore */
|
|
1808
|
+
...(ngDevMode ? [{ debugName: "columnAutoWidth" }] : /* istanbul ignore next */ []));
|
|
1809
|
+
rowAlternationEnabled = computed(() => this.rowAlternationEnabledInput() ?? this.widgetDefaults().rowAlternationEnabled ?? false, /* @ts-ignore */
|
|
1810
|
+
...(ngDevMode ? [{ debugName: "rowAlternationEnabled" }] : /* istanbul ignore next */ []));
|
|
1811
|
+
remoteOperations = input(false, /* @ts-ignore */
|
|
1812
|
+
...(ngDevMode ? [{ debugName: "remoteOperations" }] : /* istanbul ignore next */ []));
|
|
1813
|
+
/**
|
|
1814
|
+
* dx `syncLookupFilterValues` — accepted, and deliberately inert. It asks dx to narrow a
|
|
1815
|
+
* lookup column's header-filter list to the values still reachable under the other filters.
|
|
1816
|
+
* This grid never cascades those lists for client arrays (see `hfBaseRows`), so `false` is
|
|
1817
|
+
* already what it does; `true` is the option we do not honour.
|
|
1818
|
+
* @nxeInert grid — parity/data-grid.yaml "syncLookupFilterValues".
|
|
1819
|
+
*/
|
|
1820
|
+
syncLookupFilterValues = input(true, /* @ts-ignore */
|
|
1821
|
+
...(ngDevMode ? [{ debugName: "syncLookupFilterValues" }] : /* istanbul ignore next */ []));
|
|
1822
|
+
columnMinWidth = input(undefined, /* @ts-ignore */
|
|
1823
|
+
...(ngDevMode ? [{ debugName: "columnMinWidth" }] : /* istanbul ignore next */ []));
|
|
1824
|
+
noDataTextInput = input(undefined, { ...(ngDevMode ? { debugName: "noDataTextInput" } : /* istanbul ignore next */ {}), alias: 'noDataText' });
|
|
1825
|
+
noDataText = computed(() => this.noDataTextInput() ?? this.widgetDefaults().noDataText ?? 'No data', /* @ts-ignore */
|
|
1826
|
+
...(ngDevMode ? [{ debugName: "noDataText" }] : /* istanbul ignore next */ []));
|
|
1827
|
+
loadPanel = input(undefined, /* @ts-ignore */
|
|
1828
|
+
...(ngDevMode ? [{ debugName: "loadPanel" }] : /* istanbul ignore next */ []));
|
|
1829
|
+
paging = input(undefined, /* @ts-ignore */
|
|
1830
|
+
...(ngDevMode ? [{ debugName: "paging" }] : /* istanbul ignore next */ []));
|
|
1831
|
+
filterRow = input(undefined, /* @ts-ignore */
|
|
1832
|
+
...(ngDevMode ? [{ debugName: "filterRow" }] : /* istanbul ignore next */ []));
|
|
1833
|
+
/** Object-input forms of the config elements (dx accepts both; orbit uses both styles). */
|
|
1834
|
+
headerFilterOptions = input(undefined, { ...(ngDevMode ? { debugName: "headerFilterOptions" } : /* istanbul ignore next */ {}), alias: 'headerFilter' });
|
|
1835
|
+
groupingOptions = input(undefined, { ...(ngDevMode ? { debugName: "groupingOptions" } : /* istanbul ignore next */ {}), alias: 'grouping' });
|
|
1836
|
+
groupPanelOptions = input(undefined, { ...(ngDevMode ? { debugName: "groupPanelOptions" } : /* istanbul ignore next */ {}), alias: 'groupPanel' });
|
|
1837
|
+
scrolling = input(undefined, /* @ts-ignore */
|
|
1838
|
+
...(ngDevMode ? [{ debugName: "scrolling" }] : /* istanbul ignore next */ []));
|
|
1839
|
+
sorting = input(undefined, /* @ts-ignore */
|
|
1840
|
+
...(ngDevMode ? [{ debugName: "sorting" }] : /* istanbul ignore next */ []));
|
|
1841
|
+
allowColumnResizing = input(false, /* @ts-ignore */
|
|
1842
|
+
...(ngDevMode ? [{ debugName: "allowColumnResizing" }] : /* istanbul ignore next */ []));
|
|
1843
|
+
/** dx columnResizingMode: 'nextColumn' (default) trades width; 'widget' grows the table. */
|
|
1844
|
+
columnResizingMode = input('nextColumn', /* @ts-ignore */
|
|
1845
|
+
...(ngDevMode ? [{ debugName: "columnResizingMode" }] : /* istanbul ignore next */ []));
|
|
1846
|
+
/** dx allowColumnReordering (default false — matches every dx-era template in this app). */
|
|
1847
|
+
allowColumnReordering = input(false, /* @ts-ignore */
|
|
1848
|
+
...(ngDevMode ? [{ debugName: "allowColumnReordering" }] : /* istanbul ignore next */ []));
|
|
1849
|
+
repaintChangesOnly = input(false, /* @ts-ignore */
|
|
1850
|
+
...(ngDevMode ? [{ debugName: "repaintChangesOnly" }] : /* istanbul ignore next */ []));
|
|
1851
|
+
showColumnHeaders = input(true, /* @ts-ignore */
|
|
1852
|
+
...(ngDevMode ? [{ debugName: "showColumnHeaders" }] : /* istanbul ignore next */ []));
|
|
1853
|
+
// --- Tree-list inputs (dx-tree-list) -------------------------------------------------------
|
|
1854
|
+
parentIdExpr = input(undefined, /* @ts-ignore */
|
|
1855
|
+
...(ngDevMode ? [{ debugName: "parentIdExpr" }] : /* istanbul ignore next */ []));
|
|
1856
|
+
dataStructure = input('plain', /* @ts-ignore */
|
|
1857
|
+
...(ngDevMode ? [{ debugName: "dataStructure" }] : /* istanbul ignore next */ []));
|
|
1858
|
+
rootValue = input(undefined, /* @ts-ignore */
|
|
1859
|
+
...(ngDevMode ? [{ debugName: "rootValue" }] : /* istanbul ignore next */ []));
|
|
1860
|
+
autoExpandAll = input(false, /* @ts-ignore */
|
|
1861
|
+
...(ngDevMode ? [{ debugName: "autoExpandAll" }] : /* istanbul ignore next */ []));
|
|
1862
|
+
expandedRowKeys = model([], /* @ts-ignore */
|
|
1863
|
+
...(ngDevMode ? [{ debugName: "expandedRowKeys" }] : /* istanbul ignore next */ []));
|
|
1864
|
+
/** Tree-list multi-select keys (`[(selectedRowKeys)]`); recursive via dxo-selection. */
|
|
1865
|
+
selectedRowKeys = model([], /* @ts-ignore */
|
|
1866
|
+
...(ngDevMode ? [{ debugName: "selectedRowKeys" }] : /* istanbul ignore next */ []));
|
|
1867
|
+
/**
|
|
1868
|
+
* Declarative cell merge (orbit addition, replaces the legacy `onCellPrepared` DOM surgery):
|
|
1869
|
+
* dataFields merged hierarchically left→right — consecutive rows with equal values render one
|
|
1870
|
+
* cell with `rowspan`; a break in an earlier field breaks all later fields.
|
|
1871
|
+
*/
|
|
1872
|
+
mergeColumns = input([], /* @ts-ignore */
|
|
1873
|
+
...(ngDevMode ? [{ debugName: "mergeColumns" }] : /* istanbul ignore next */ []));
|
|
1874
|
+
onSelectionChanged = output();
|
|
1875
|
+
onRowClick = output();
|
|
1876
|
+
/** Fires after the rendered row set changes (dx parity; used for post-render DOM work). */
|
|
1877
|
+
onContentReady = output();
|
|
1878
|
+
/**
|
|
1879
|
+
* dx parity: `{ fullName: 'paging.pageIndex', value }` for internal paging and
|
|
1880
|
+
* `{ fullName: 'columns[<i>].groupIndex', previousValue, value }` for runtime grouping
|
|
1881
|
+
* changes (one event per gesture — the contract generic-list's persistence handler expects).
|
|
1882
|
+
*/
|
|
1883
|
+
onOptionChanged = output();
|
|
1884
|
+
/** Extra CSS classes for a data row (replaces dx `onRowPrepared` class tweaks). */
|
|
1885
|
+
rowClass = input(undefined, /* @ts-ignore */
|
|
1886
|
+
...(ngDevMode ? [{ debugName: "rowClass" }] : /* istanbul ignore next */ []));
|
|
1887
|
+
/**
|
|
1888
|
+
* dx `onCellPrepared` / `onRowPrepared`, fired once per rendered cell and row after the DOM
|
|
1889
|
+
* is written, with the real element for handlers that mutate it.
|
|
1890
|
+
*
|
|
1891
|
+
* Classic EventEmitters, NOT `output()`, for the same reason the pivot uses them: `.observed`
|
|
1892
|
+
* gates a per-cell walk, so a grid that binds neither pays nothing. That gate is the whole
|
|
1893
|
+
* cost model here — without it every grid would walk every cell on every render.
|
|
1894
|
+
*
|
|
1895
|
+
* `mergeColumns` and `rowClass` are the supported way to do what most handlers do; these
|
|
1896
|
+
* exist because applications carry handlers that cannot be rewritten.
|
|
1897
|
+
*/
|
|
1898
|
+
onCellPrepared = new EventEmitter();
|
|
1899
|
+
onRowPrepared = new EventEmitter();
|
|
1900
|
+
rowClassFor(row) {
|
|
1901
|
+
return this.rowClass()?.(row) ?? '';
|
|
1902
|
+
}
|
|
1903
|
+
/** Master-detail expand lifecycle (fires before the detail template instantiates). */
|
|
1904
|
+
onRowExpanding = output();
|
|
1905
|
+
onRowExpanded = output();
|
|
1906
|
+
/** Set imperatively by `app-data-pagination-wrapper` (a group-count summary). Stored for 9b. */
|
|
1907
|
+
summary = undefined;
|
|
1908
|
+
hoveredIndex = signal(null, /* @ts-ignore */
|
|
1909
|
+
...(ngDevMode ? [{ debugName: "hoveredIndex" }] : /* istanbul ignore next */ []));
|
|
1910
|
+
sort = signal(null, /* @ts-ignore */
|
|
1911
|
+
...(ngDevMode ? [{ debugName: "sort" }] : /* istanbul ignore next */ []));
|
|
1912
|
+
sortInit = signal(false, /* @ts-ignore */
|
|
1913
|
+
...(ngDevMode ? [{ debugName: "sortInit" }] : /* istanbul ignore next */ []));
|
|
1914
|
+
visibilityOverride = signal(new Map(), /* @ts-ignore */
|
|
1915
|
+
...(ngDevMode ? [{ debugName: "visibilityOverride" }] : /* istanbul ignore next */ []));
|
|
1916
|
+
/** Filter-row state per dataField (operation + typed text) — controlled editors read it back. */
|
|
1917
|
+
filterRowState = signal(new Map(), /* @ts-ignore */
|
|
1918
|
+
...(ngDevMode ? [{ debugName: "filterRowState" }] : /* istanbul ignore next */ []));
|
|
1919
|
+
/**
|
|
1920
|
+
* Header-filter runtime overrides: field → allowed-key set, or `null` for "user explicitly
|
|
1921
|
+
* cleared" (a declarative `filterValues` seed must NOT resurrect after Select All → OK).
|
|
1922
|
+
* Absent field = untouched → the column's `filterValues` input seeds the filter (dx parity).
|
|
1923
|
+
*/
|
|
1924
|
+
hfOverride = signal(new Map(), /* @ts-ignore */
|
|
1925
|
+
...(ngDevMode ? [{ debugName: "hfOverride" }] : /* istanbul ignore next */ []));
|
|
1926
|
+
selectedSet = signal(new Set(), /* @ts-ignore */
|
|
1927
|
+
...(ngDevMode ? [{ debugName: "selectedSet" }] : /* istanbul ignore next */ []));
|
|
1928
|
+
refreshTick = signal(0, /* @ts-ignore */
|
|
1929
|
+
...(ngDevMode ? [{ debugName: "refreshTick" }] : /* istanbul ignore next */ []));
|
|
1930
|
+
/**
|
|
1931
|
+
* Header-filter popup working state — the draft/search/tree machinery lives in
|
|
1932
|
+
* {@link NxeHeaderFilterModel} (extracted for pivot reuse, Phase 3a); the grid supplies
|
|
1933
|
+
* its rows/columns through the source adapter and keeps the aliases the specs poke.
|
|
1934
|
+
*/
|
|
1935
|
+
hf = new NxeHeaderFilterModel({
|
|
1936
|
+
rows: () => this.hfBaseRows(),
|
|
1937
|
+
rawValue: (col, row) => this.getRaw(row, col.dataField()),
|
|
1938
|
+
displayText: (col, row) => this.cellText(col, row),
|
|
1939
|
+
isDate: (col) => col.dataType() === 'date',
|
|
1940
|
+
sortByDisplayText: (col) => !!col.lookup(),
|
|
1941
|
+
boolLabel: (col, raw) => this.hfBoolLabel(col, raw),
|
|
1942
|
+
compare: (a, b) => this.compare(a, b),
|
|
1943
|
+
});
|
|
1944
|
+
hfColumn = this.hf.column;
|
|
1945
|
+
hfDraft = this.hf.draft;
|
|
1946
|
+
hfOverlayRef;
|
|
1947
|
+
// Internal paging (dxo-paging present) working state
|
|
1948
|
+
pageIndex = signal(0, /* @ts-ignore */
|
|
1949
|
+
...(ngDevMode ? [{ debugName: "pageIndex" }] : /* istanbul ignore next */ []));
|
|
1950
|
+
userPageSize = signal(null, /* @ts-ignore */
|
|
1951
|
+
...(ngDevMode ? [{ debugName: "userPageSize" }] : /* istanbul ignore next */ []));
|
|
1952
|
+
/** DevExtreme `.instance` facade — `#gridAll.instance.<method>()`. */
|
|
1953
|
+
facade = {
|
|
1954
|
+
getSelectedRowsData: () => this.selectedRowsData(),
|
|
1955
|
+
getSelectedRowKeys: () => this.rowKeys(this.selectedRowsData()),
|
|
1956
|
+
refresh: () => this.refreshTick.update((v) => v + 1),
|
|
1957
|
+
clearFilter: (filterName) => {
|
|
1958
|
+
if (!filterName || filterName === 'row') {
|
|
1959
|
+
this.filterRowState.set(new Map());
|
|
1960
|
+
}
|
|
1961
|
+
if (!filterName || filterName === 'header') {
|
|
1962
|
+
// Explicit nulls so declarative `filterValues` seeds don't resurrect (dx clears
|
|
1963
|
+
// column.filterValues even when configured declaratively).
|
|
1964
|
+
const cleared = new Map();
|
|
1965
|
+
for (const col of this.columns()) {
|
|
1966
|
+
if (col.filterValues()?.length) {
|
|
1967
|
+
cleared.set(col.dataField(), null);
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
this.hfOverride.set(cleared);
|
|
1971
|
+
}
|
|
1972
|
+
},
|
|
1973
|
+
columnOption: (field, optionName, value) => {
|
|
1974
|
+
if (optionName === 'visible') {
|
|
1975
|
+
this.setColumnVisible(field, value);
|
|
1976
|
+
}
|
|
1977
|
+
},
|
|
1978
|
+
getVisibleColumns: () => this.visibleColumns().map((c) => ({
|
|
1979
|
+
caption: c.caption(),
|
|
1980
|
+
dataField: c.dataField(),
|
|
1981
|
+
dataType: c.dataType(),
|
|
1982
|
+
format: c.format(),
|
|
1983
|
+
})),
|
|
1984
|
+
getVisibleRows: () => this.displayRows(),
|
|
1985
|
+
totalCount: () => this.displayRows().length,
|
|
1986
|
+
getRootNode: () => ({
|
|
1987
|
+
key: undefined,
|
|
1988
|
+
data: undefined,
|
|
1989
|
+
level: -1,
|
|
1990
|
+
hasChildren: this.treeRoots().length > 0,
|
|
1991
|
+
children: this.treeRoots(),
|
|
1992
|
+
}),
|
|
1993
|
+
updateDimensions: () => this.refreshTick.update((v) => v + 1),
|
|
1994
|
+
clearGrouping: () => this.clearGroupingInternal(),
|
|
1995
|
+
expandAll: () => {
|
|
1996
|
+
this.groupExpandDefault.set(true);
|
|
1997
|
+
this.groupExpandOverride.set(new Map());
|
|
1998
|
+
if (this.treeMode) {
|
|
1999
|
+
const keyF = this.keyExpr() ?? 'id';
|
|
2000
|
+
const all = new Map();
|
|
2001
|
+
for (const r of (this.dataSource() ?? [])) {
|
|
2002
|
+
all.set(r[keyF], true);
|
|
2003
|
+
}
|
|
2004
|
+
this.expandOverride.set(all);
|
|
2005
|
+
}
|
|
2006
|
+
},
|
|
2007
|
+
saveEditData: () => {
|
|
2008
|
+
const editing = this.editingCmp();
|
|
2009
|
+
const key = editing?.editRowKey();
|
|
2010
|
+
if (editing && key != null) {
|
|
2011
|
+
const row = (this.dataSource() ?? []).find((r) => this.rowKey(r) === key);
|
|
2012
|
+
if (row) {
|
|
2013
|
+
this.saveRowEdit(row, new Event('save'));
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
},
|
|
2017
|
+
element: () => this.elementRef.nativeElement,
|
|
2018
|
+
expandRow: (key) => {
|
|
2019
|
+
// Symmetric with collapseRow for the tree list: writes the same override map the
|
|
2020
|
+
// expander arrows use, so a programmatic reveal wins over an earlier manual collapse
|
|
2021
|
+
// (the [expandedRowKeys] seed alone cannot — overrides take precedence over it).
|
|
2022
|
+
if (this.treeMode) {
|
|
2023
|
+
this.expandOverride.update((m) => new Map(m).set(key, true));
|
|
2024
|
+
}
|
|
2025
|
+
},
|
|
2026
|
+
collapseRow: (key) => {
|
|
2027
|
+
// Master-detail collapse by key; tree-list node collapse for tree mode.
|
|
2028
|
+
this.expandedDetails.update((set) => {
|
|
2029
|
+
const next = new Set(set);
|
|
2030
|
+
next.delete(key);
|
|
2031
|
+
return next;
|
|
2032
|
+
});
|
|
2033
|
+
if (this.treeMode) {
|
|
2034
|
+
this.expandOverride.update((m) => new Map(m).set(key, false));
|
|
2035
|
+
}
|
|
2036
|
+
// dx grouped-grid form: collapseRow(['X','Y']) collapses that group path.
|
|
2037
|
+
if (!this.treeMode && Array.isArray(key) && this.groupColumns().length) {
|
|
2038
|
+
let groupKey = '';
|
|
2039
|
+
key.forEach((value, level) => {
|
|
2040
|
+
groupKey = `${groupKey}/${level}:${typeof value}:${String(value)}`;
|
|
2041
|
+
});
|
|
2042
|
+
this.groupExpandOverride.update((m) => new Map(m).set(groupKey, false));
|
|
2043
|
+
}
|
|
2044
|
+
},
|
|
2045
|
+
};
|
|
2046
|
+
get instance() {
|
|
2047
|
+
return this.facade;
|
|
2048
|
+
}
|
|
2049
|
+
/** Guards against a handler that schedules another render re-entering the walk. */
|
|
2050
|
+
preparedDepth = 0;
|
|
2051
|
+
constructor() {
|
|
2052
|
+
// Registered before onContentReady so cells are prepared by the time it fires, as in dx.
|
|
2053
|
+
afterRenderEffect(() => {
|
|
2054
|
+
this.renderRows();
|
|
2055
|
+
this.visibleColumns();
|
|
2056
|
+
untracked(() => this.emitPreparedEvents());
|
|
2057
|
+
});
|
|
2058
|
+
// dx onContentReady parity: fire after the rendered row set reaches the DOM.
|
|
2059
|
+
afterRenderEffect(() => {
|
|
2060
|
+
this.renderRows();
|
|
2061
|
+
this.onContentReady.emit({ component: this.facade, element: this.elementRef.nativeElement });
|
|
2062
|
+
});
|
|
2063
|
+
// dx columnAutoWidth parity: measure natural column widths after render, then apply
|
|
2064
|
+
// fixed-layout pixel widths distributed like dx (explicit widths kept; free columns share
|
|
2065
|
+
// the remaining container width proportionally to their max-content size).
|
|
2066
|
+
afterRenderEffect(() => {
|
|
2067
|
+
this.renderRows();
|
|
2068
|
+
this.visibleColumns();
|
|
2069
|
+
this.columnAutoWidth();
|
|
2070
|
+
this.wordWrapEnabled();
|
|
2071
|
+
this.containerW();
|
|
2072
|
+
untracked(() => this.scheduleAutoWidthMeasure());
|
|
2073
|
+
});
|
|
2074
|
+
afterNextRender(() => {
|
|
2075
|
+
const rv = this.elementRef.nativeElement.querySelector('.dx-datagrid-rowsview');
|
|
2076
|
+
if (rv && typeof ResizeObserver !== 'undefined') {
|
|
2077
|
+
this.resizeObserver = new ResizeObserver((entries) => {
|
|
2078
|
+
const w = Math.round(entries[0]?.contentRect?.width ?? 0);
|
|
2079
|
+
if (w > 0 && Math.abs(w - this.containerW()) > 1) {
|
|
2080
|
+
this.containerW.set(w);
|
|
2081
|
+
}
|
|
2082
|
+
});
|
|
2083
|
+
this.resizeObserver.observe(rv);
|
|
2084
|
+
}
|
|
2085
|
+
});
|
|
2086
|
+
}
|
|
2087
|
+
// --- Column auto-width engine (dx columnAutoWidth parity) ----------------------------------
|
|
2088
|
+
autoWidths = signal(null, /* @ts-ignore */
|
|
2089
|
+
...(ngDevMode ? [{ debugName: "autoWidths" }] : /* istanbul ignore next */ []));
|
|
2090
|
+
containerW = signal(0, /* @ts-ignore */
|
|
2091
|
+
...(ngDevMode ? [{ debugName: "containerW" }] : /* istanbul ignore next */ []));
|
|
2092
|
+
resizeObserver = null;
|
|
2093
|
+
autoWidthPending = false;
|
|
2094
|
+
lastAutoWidthKey = '';
|
|
2095
|
+
scheduleAutoWidthMeasure() {
|
|
2096
|
+
// Frozen after a manual resize (dx stops redistributing), paused while a resize drag
|
|
2097
|
+
// writes the colgroup directly.
|
|
2098
|
+
if (!this.columnAutoWidth() || this.resizeActive || this.widthOverride().size) {
|
|
2099
|
+
if (this.autoWidths() !== null) {
|
|
2100
|
+
this.lastAutoWidthKey = '';
|
|
2101
|
+
this.autoWidths.set(null);
|
|
2102
|
+
}
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
2105
|
+
if (this.autoWidthPending) {
|
|
2106
|
+
return;
|
|
2107
|
+
}
|
|
2108
|
+
this.autoWidthPending = true;
|
|
2109
|
+
requestAnimationFrame(() => {
|
|
2110
|
+
this.autoWidthPending = false;
|
|
2111
|
+
this.measureAutoWidths();
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
measureAutoWidths() {
|
|
2115
|
+
const host = this.elementRef.nativeElement;
|
|
2116
|
+
const table = host.querySelector('.dx-datagrid-content > .dx-datagrid-table');
|
|
2117
|
+
const rowsview = host.querySelector('.dx-datagrid-rowsview');
|
|
2118
|
+
const headerRow = nxeLeafHeaderRow(host);
|
|
2119
|
+
if (!table || !rowsview || !headerRow) {
|
|
2120
|
+
return;
|
|
2121
|
+
}
|
|
2122
|
+
const cols = this.visibleColumns();
|
|
2123
|
+
const lead = this.groupLevelCount() + (this.masterDetailEnabled() ? 1 : 0) + (this.showCheckboxes() ? 1 : 0);
|
|
2124
|
+
// Indexed among data cells, not by a lead offset: command cells live in the FIRST header
|
|
2125
|
+
// row and span down, so a banded grid's leaf row has none of them.
|
|
2126
|
+
const cells = nxeHeaderDataCells(headerRow);
|
|
2127
|
+
const container = rowsview.clientWidth;
|
|
2128
|
+
if (cells.length !== cols.length || container <= 0) {
|
|
2129
|
+
return;
|
|
2130
|
+
}
|
|
2131
|
+
// Measurement pass: natural max-content widths (no wrap), reverted before paint.
|
|
2132
|
+
const colEls = Array.from(table.querySelectorAll(':scope > colgroup > col'))
|
|
2133
|
+
.slice(lead, lead + cols.length);
|
|
2134
|
+
const prevLayout = table.style.tableLayout;
|
|
2135
|
+
const prevWidth = table.style.width;
|
|
2136
|
+
const prevColWidths = colEls.map((c) => c.style.width);
|
|
2137
|
+
table.style.tableLayout = 'auto';
|
|
2138
|
+
table.style.width = 'max-content';
|
|
2139
|
+
colEls.forEach((c) => (c.style.width = ''));
|
|
2140
|
+
const measured = cells.map((c) => c.getBoundingClientRect().width);
|
|
2141
|
+
table.style.tableLayout = prevLayout;
|
|
2142
|
+
table.style.width = prevWidth;
|
|
2143
|
+
colEls.forEach((c, i) => (c.style.width = prevColWidths[i]));
|
|
2144
|
+
// dx-style distribution: explicit px widths kept; free columns get the remaining space
|
|
2145
|
+
// proportionally to measured max-content (never below columnMinWidth; last col absorbs
|
|
2146
|
+
// rounding). Over-constrained -> keep max-content and let the rowsview scroll.
|
|
2147
|
+
const explicit = cols.map((c) => {
|
|
2148
|
+
const w = this.colWidth(c);
|
|
2149
|
+
if (w == null) {
|
|
2150
|
+
return null;
|
|
2151
|
+
}
|
|
2152
|
+
const n = parseFloat(w);
|
|
2153
|
+
return Number.isFinite(n) && (w.endsWith('px') || /^\d+(\.\d+)?$/.test(w)) ? n : null;
|
|
2154
|
+
});
|
|
2155
|
+
const specialSum = 30 * this.groupLevelCount() +
|
|
2156
|
+
(this.masterDetailEnabled() ? 30 : 0) +
|
|
2157
|
+
(this.showCheckboxes() ? 70 : 0) +
|
|
2158
|
+
(this.rowEditCommandVisible() ? 100 : 0);
|
|
2159
|
+
const fixedSum = explicit.reduce((s, w) => s + (w ?? 0), 0) + specialSum;
|
|
2160
|
+
const freeIdx = explicit.map((w, i) => (w == null ? i : -1)).filter((i) => i >= 0);
|
|
2161
|
+
const out = explicit.map((w) => (w == null ? null : `${w}px`));
|
|
2162
|
+
const minW = this.columnMinWidth() ?? 50;
|
|
2163
|
+
let total = fixedSum;
|
|
2164
|
+
if (freeIdx.length) {
|
|
2165
|
+
const freeSum = freeIdx.reduce((s, i) => s + measured[i], 0) || 1;
|
|
2166
|
+
const avail = container - fixedSum;
|
|
2167
|
+
if (avail >= freeSum) {
|
|
2168
|
+
const scale = avail / freeSum;
|
|
2169
|
+
let used = 0;
|
|
2170
|
+
freeIdx.forEach((i, k) => {
|
|
2171
|
+
const w = k === freeIdx.length - 1
|
|
2172
|
+
? Math.max(minW, avail - used)
|
|
2173
|
+
: Math.max(minW, Math.round(measured[i] * scale * 100) / 100);
|
|
2174
|
+
used += w;
|
|
2175
|
+
out[i] = `${w}px`;
|
|
2176
|
+
});
|
|
2177
|
+
total += used;
|
|
2178
|
+
}
|
|
2179
|
+
else {
|
|
2180
|
+
freeIdx.forEach((i) => {
|
|
2181
|
+
const w = Math.max(minW, Math.ceil(measured[i]));
|
|
2182
|
+
total += w;
|
|
2183
|
+
out[i] = `${w}px`;
|
|
2184
|
+
});
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
const tableWidth = total > container + 1 ? `${Math.ceil(total)}px` : '100%';
|
|
2188
|
+
const key = `${out.join('|')}@${tableWidth}@${container}`;
|
|
2189
|
+
if (key !== this.lastAutoWidthKey) {
|
|
2190
|
+
this.lastAutoWidthKey = key;
|
|
2191
|
+
this.autoWidths.set({ widths: out, tableWidth });
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
/**
|
|
2195
|
+
* A column with no frozen/declared width appearing AFTER a manual resize froze the layout
|
|
2196
|
+
* (ungrouped chip, re-shown via columnOption): give it dx's default column width instead of
|
|
2197
|
+
* letting the pinned table width squeeze it to ~0.
|
|
2198
|
+
*/
|
|
2199
|
+
frozenColWidth(col) {
|
|
2200
|
+
const manual = this.widthOverride().get(col.dataField());
|
|
2201
|
+
if (manual != null) {
|
|
2202
|
+
return manual;
|
|
2203
|
+
}
|
|
2204
|
+
const declared = parseFloat(this.colWidth(col) ?? '');
|
|
2205
|
+
return Number.isFinite(declared) ? declared : 100;
|
|
2206
|
+
}
|
|
2207
|
+
effectiveColWidth(col, idx) {
|
|
2208
|
+
const aw = this.autoWidths();
|
|
2209
|
+
if (aw && this.columnAutoWidth()) {
|
|
2210
|
+
return aw.widths[idx] ?? this.colWidth(col);
|
|
2211
|
+
}
|
|
2212
|
+
const w = this.colWidth(col);
|
|
2213
|
+
if (w == null && this.widthOverride().size) {
|
|
2214
|
+
return `${this.frozenColWidth(col)}px`;
|
|
2215
|
+
}
|
|
2216
|
+
return w;
|
|
2217
|
+
}
|
|
2218
|
+
tableWidthStyle() {
|
|
2219
|
+
// After a manual resize the layout is frozen: widget mode sizes the table to the sum of
|
|
2220
|
+
// its columns (dx grows/shrinks the table); nextColumn keeps filling the container.
|
|
2221
|
+
if (this.widthOverride().size && this.columnResizingMode() === 'widget') {
|
|
2222
|
+
let total = 30 * this.groupLevelCount() +
|
|
2223
|
+
(this.masterDetailEnabled() ? 30 : 0) +
|
|
2224
|
+
(this.showCheckboxes() ? 40 : 0) +
|
|
2225
|
+
(this.rowEditCommandVisible() ? 100 : 0);
|
|
2226
|
+
for (const col of this.visibleColumns()) {
|
|
2227
|
+
total += this.frozenColWidth(col);
|
|
2228
|
+
}
|
|
2229
|
+
return `${Math.round(total)}px`;
|
|
2230
|
+
}
|
|
2231
|
+
if (this.widthOverride().size) {
|
|
2232
|
+
return '100%';
|
|
2233
|
+
}
|
|
2234
|
+
const aw = this.autoWidths();
|
|
2235
|
+
return aw && this.columnAutoWidth() ? aw.tableWidth : '100%';
|
|
2236
|
+
}
|
|
2237
|
+
// --- Selection (dxo-selection) ---------------------------------------------------------------
|
|
2238
|
+
selectionEnabled = computed(() => {
|
|
2239
|
+
const s = this.selection();
|
|
2240
|
+
return !!s && s.mode() !== 'none';
|
|
2241
|
+
}, /* @ts-ignore */
|
|
2242
|
+
...(ngDevMode ? [{ debugName: "selectionEnabled" }] : /* istanbul ignore next */ []));
|
|
2243
|
+
selectionMode = computed(() => this.selection()?.mode() ?? 'single', /* @ts-ignore */
|
|
2244
|
+
...(ngDevMode ? [{ debugName: "selectionMode" }] : /* istanbul ignore next */ []));
|
|
2245
|
+
showCheckboxes = computed(() => {
|
|
2246
|
+
if (!this.selectionEnabled()) {
|
|
2247
|
+
return false;
|
|
2248
|
+
}
|
|
2249
|
+
// Tree-list multiple selection shows checkboxes by default (dx behaviour).
|
|
2250
|
+
return (this.selection()?.showCheckBoxesMode() === 'always' ||
|
|
2251
|
+
(this.treeMode && this.selectionMode() === 'multiple'));
|
|
2252
|
+
}, /* @ts-ignore */
|
|
2253
|
+
...(ngDevMode ? [{ debugName: "showCheckboxes" }] : /* istanbul ignore next */ []));
|
|
2254
|
+
/** Selected rows in dataSource order (stale refs from old data naturally filtered out). */
|
|
2255
|
+
selectedRowsData = computed(() => {
|
|
2256
|
+
if (this.treeMode) {
|
|
2257
|
+
const keySet = new Set(this.selectedRowKeys());
|
|
2258
|
+
return keySet.size === 0
|
|
2259
|
+
? []
|
|
2260
|
+
: (this.dataSource() ?? []).filter((r) => keySet.has(this.rowKey(r)));
|
|
2261
|
+
}
|
|
2262
|
+
const set = this.selectedSet();
|
|
2263
|
+
return set.size === 0 ? [] : (this.dataSource() ?? []).filter((r) => set.has(r));
|
|
2264
|
+
}, /* @ts-ignore */
|
|
2265
|
+
...(ngDevMode ? [{ debugName: "selectedRowsData" }] : /* istanbul ignore next */ []));
|
|
2266
|
+
allSelected = computed(() => {
|
|
2267
|
+
if (this.treeMode) {
|
|
2268
|
+
const rows = (this.dataSource() ?? []);
|
|
2269
|
+
const keys = this.selectedRowKeys();
|
|
2270
|
+
return rows.length > 0 && rows.every((r) => keys.includes(this.rowKey(r)));
|
|
2271
|
+
}
|
|
2272
|
+
const rows = this.displayRows();
|
|
2273
|
+
const set = this.selectedSet();
|
|
2274
|
+
return rows.length > 0 && rows.every((r) => set.has(r));
|
|
2275
|
+
}, /* @ts-ignore */
|
|
2276
|
+
...(ngDevMode ? [{ debugName: "allSelected" }] : /* istanbul ignore next */ []));
|
|
2277
|
+
someSelected = computed(() => this.treeMode
|
|
2278
|
+
? this.selectedRowKeys().length > 0
|
|
2279
|
+
: this.displayRows().some((r) => this.selectedSet().has(r)), /* @ts-ignore */
|
|
2280
|
+
...(ngDevMode ? [{ debugName: "someSelected" }] : /* istanbul ignore next */ []));
|
|
2281
|
+
rowKey(row) {
|
|
2282
|
+
const k = this.keyExpr();
|
|
2283
|
+
return k ? row?.[k] : row;
|
|
2284
|
+
}
|
|
2285
|
+
treeDescendantKeys(row) {
|
|
2286
|
+
const start = this.rowKey(row);
|
|
2287
|
+
const out = [];
|
|
2288
|
+
const find = (nodes) => {
|
|
2289
|
+
for (const n of nodes) {
|
|
2290
|
+
if (n.key === start)
|
|
2291
|
+
return n;
|
|
2292
|
+
const f = find(n.children);
|
|
2293
|
+
if (f)
|
|
2294
|
+
return f;
|
|
2295
|
+
}
|
|
2296
|
+
return null;
|
|
2297
|
+
};
|
|
2298
|
+
const node = find(this.treeRoots());
|
|
2299
|
+
const collect = (n) => {
|
|
2300
|
+
out.push(n.key);
|
|
2301
|
+
n.children.forEach(collect);
|
|
2302
|
+
};
|
|
2303
|
+
if (node)
|
|
2304
|
+
collect(node);
|
|
2305
|
+
return out.length ? out : [start];
|
|
2306
|
+
}
|
|
2307
|
+
toggleTreeSelection(row) {
|
|
2308
|
+
const key = this.rowKey(row);
|
|
2309
|
+
// dx parity: single mode REPLACES the selection, and re-clicking the selected row keeps
|
|
2310
|
+
// it selected. A toggle here would immediately undo a key the host page just wrote into
|
|
2311
|
+
// [(selectedRowKeys)] from its own onRowClick handler — the output fires before this
|
|
2312
|
+
// runs, so the toggle would find the key present and delete it, ending every click with
|
|
2313
|
+
// an empty selection.
|
|
2314
|
+
if (this.selectionMode() !== 'multiple') {
|
|
2315
|
+
const previous = this.selectedRowKeys();
|
|
2316
|
+
const added = previous.includes(key) ? [] : [key];
|
|
2317
|
+
const removed = previous.filter((k) => k !== key);
|
|
2318
|
+
this.selectedRowKeys.set([key]);
|
|
2319
|
+
this.emitTreeSelectionChanged([key], added, removed);
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2322
|
+
const recursive = this.selection()?.recursive() ?? false;
|
|
2323
|
+
const keys = new Set(this.selectedRowKeys());
|
|
2324
|
+
const selecting = !keys.has(key);
|
|
2325
|
+
const affected = recursive ? this.treeDescendantKeys(row) : [key];
|
|
2326
|
+
for (const k of affected) {
|
|
2327
|
+
selecting ? keys.add(k) : keys.delete(k);
|
|
2328
|
+
}
|
|
2329
|
+
const arr = [...keys];
|
|
2330
|
+
this.selectedRowKeys.set(arr);
|
|
2331
|
+
this.emitTreeSelectionChanged(arr, selecting ? affected : [], selecting ? [] : affected);
|
|
2332
|
+
}
|
|
2333
|
+
emitTreeSelectionChanged(allKeys, added, removed) {
|
|
2334
|
+
const keySet = new Set(allKeys);
|
|
2335
|
+
const data = (this.dataSource() ?? []).filter((r) => keySet.has(this.rowKey(r)));
|
|
2336
|
+
this.onSelectionChanged.emit({
|
|
2337
|
+
selectedRowsData: data,
|
|
2338
|
+
selectedRowKeys: allKeys,
|
|
2339
|
+
currentSelectedRowKeys: added,
|
|
2340
|
+
currentDeselectedRowKeys: removed,
|
|
2341
|
+
component: this.facade,
|
|
2342
|
+
element: this.elementRef.nativeElement,
|
|
2343
|
+
});
|
|
2344
|
+
}
|
|
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
|
+
// --- Internal paging (dxo-paging) -----------------------------------------------------------
|
|
2382
|
+
pagingEnabled = computed(() => !!this.pagingCmp() && this.pagingCmp().enabled(), /* @ts-ignore */
|
|
2383
|
+
...(ngDevMode ? [{ debugName: "pagingEnabled" }] : /* istanbul ignore next */ []));
|
|
2384
|
+
effectivePageSize = computed(() => this.userPageSize() ?? this.pagingCmp()?.pageSize() ?? 20, /* @ts-ignore */
|
|
2385
|
+
...(ngDevMode ? [{ debugName: "effectivePageSize" }] : /* istanbul ignore next */ []));
|
|
2386
|
+
pageCount = computed(() => this.pagingEnabled() ? Math.max(1, Math.ceil(this.displayRows().length / this.effectivePageSize())) : 1, /* @ts-ignore */
|
|
2387
|
+
...(ngDevMode ? [{ debugName: "pageCount" }] : /* istanbul ignore next */ []));
|
|
2388
|
+
/** The filtered/sorted rows visible on the current page (all rows when paging is off). */
|
|
2389
|
+
pagedRows = computed(() => {
|
|
2390
|
+
const rows = this.displayRows();
|
|
2391
|
+
if (!this.pagingEnabled()) {
|
|
2392
|
+
return rows;
|
|
2393
|
+
}
|
|
2394
|
+
const ps = this.effectivePageSize();
|
|
2395
|
+
const pi = Math.min(this.pageIndex(), this.pageCount() - 1);
|
|
2396
|
+
return rows.slice(pi * ps, pi * ps + ps);
|
|
2397
|
+
}, /* @ts-ignore */
|
|
2398
|
+
...(ngDevMode ? [{ debugName: "pagedRows" }] : /* istanbul ignore next */ []));
|
|
2399
|
+
pagerVisible = computed(() => {
|
|
2400
|
+
const p = this.pagerCmp();
|
|
2401
|
+
if (!p) {
|
|
2402
|
+
return false;
|
|
2403
|
+
}
|
|
2404
|
+
return p.visible() === true || (p.visible() === 'auto' && this.pagingEnabled());
|
|
2405
|
+
}, /* @ts-ignore */
|
|
2406
|
+
...(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 */
|
|
2411
|
+
...(ngDevMode ? [{ debugName: "pageSizes" }] : /* istanbul ignore next */ []));
|
|
2412
|
+
pageNumbers = computed(() => Array.from({ length: this.pageCount() }, (_, i) => i), /* @ts-ignore */
|
|
2413
|
+
...(ngDevMode ? [{ debugName: "pageNumbers" }] : /* istanbul ignore next */ []));
|
|
2414
|
+
// --- Tree-list hierarchy (dx-tree-list) -----------------------------------------------------
|
|
2415
|
+
/** True when the host element is `<nxe-tree-list>` (vs `<nxe-data-grid>`). */
|
|
2416
|
+
treeMode = this.elementRef.nativeElement.tagName.toLowerCase() === 'nxe-tree-list';
|
|
2417
|
+
expandOverride = signal(new Map(), /* @ts-ignore */
|
|
2418
|
+
...(ngDevMode ? [{ debugName: "expandOverride" }] : /* istanbul ignore next */ []));
|
|
2419
|
+
/** Tree assembled from the flat dataSource via keyExpr/parentIdExpr/rootValue. */
|
|
2420
|
+
treeRoots = computed(() => {
|
|
2421
|
+
if (!this.treeMode) {
|
|
2422
|
+
return [];
|
|
2423
|
+
}
|
|
2424
|
+
const rows = (this.dataSource() ?? []);
|
|
2425
|
+
const keyF = this.keyExpr() ?? 'id';
|
|
2426
|
+
const parentF = this.parentIdExpr() ?? 'parentId';
|
|
2427
|
+
const root = this.rootValue();
|
|
2428
|
+
const byKey = new Map();
|
|
2429
|
+
for (const r of rows) {
|
|
2430
|
+
byKey.set(r[keyF], { key: r[keyF], data: r, children: [], level: 0, hasChildren: false });
|
|
2431
|
+
}
|
|
2432
|
+
const roots = [];
|
|
2433
|
+
for (const r of rows) {
|
|
2434
|
+
const node = byKey.get(r[keyF]);
|
|
2435
|
+
const pk = r[parentF];
|
|
2436
|
+
const parent = root !== undefined && pk === root ? undefined : byKey.get(pk);
|
|
2437
|
+
if (parent && parent !== node) {
|
|
2438
|
+
parent.children.push(node);
|
|
2439
|
+
parent.hasChildren = true;
|
|
2440
|
+
}
|
|
2441
|
+
else {
|
|
2442
|
+
roots.push(node);
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
const assign = (nodes, lvl) => {
|
|
2446
|
+
for (const n of nodes) {
|
|
2447
|
+
n.level = lvl;
|
|
2448
|
+
assign(n.children, lvl + 1);
|
|
2449
|
+
}
|
|
2450
|
+
};
|
|
2451
|
+
assign(roots, 0);
|
|
2452
|
+
return roots;
|
|
2453
|
+
}, /* @ts-ignore */
|
|
2454
|
+
...(ngDevMode ? [{ debugName: "treeRoots" }] : /* istanbul ignore next */ []));
|
|
2455
|
+
treeExpanded = computed(() => {
|
|
2456
|
+
const override = this.expandOverride();
|
|
2457
|
+
const expandAll = this.autoExpandAll();
|
|
2458
|
+
const seeded = new Set(this.expandedRowKeys());
|
|
2459
|
+
const out = new Set();
|
|
2460
|
+
const walk = (nodes) => {
|
|
2461
|
+
for (const n of nodes) {
|
|
2462
|
+
if (n.hasChildren) {
|
|
2463
|
+
const def = expandAll || seeded.has(n.key);
|
|
2464
|
+
if (override.get(n.key) ?? def) {
|
|
2465
|
+
out.add(n.key);
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
walk(n.children);
|
|
2469
|
+
}
|
|
2470
|
+
};
|
|
2471
|
+
walk(this.treeRoots());
|
|
2472
|
+
return out;
|
|
2473
|
+
}, /* @ts-ignore */
|
|
2474
|
+
...(ngDevMode ? [{ debugName: "treeExpanded" }] : /* istanbul ignore next */ []));
|
|
2475
|
+
toggleTreeNode(key, event) {
|
|
2476
|
+
event.stopPropagation();
|
|
2477
|
+
const expanded = this.treeExpanded().has(key);
|
|
2478
|
+
this.expandOverride.update((m) => new Map(m).set(key, !expanded));
|
|
2479
|
+
}
|
|
2480
|
+
/** Flattened group + data render rows (tree hierarchy, then grouping, then flat). */
|
|
2481
|
+
renderRows = computed(() => {
|
|
2482
|
+
if (this.treeMode) {
|
|
2483
|
+
this.refreshTick(); // role-tab mutates selectedRowKeys then calls .instance.refresh()
|
|
2484
|
+
const expanded = this.treeExpanded();
|
|
2485
|
+
const out = [];
|
|
2486
|
+
let idx = 0;
|
|
2487
|
+
const walk = (nodes) => {
|
|
2488
|
+
for (const n of nodes) {
|
|
2489
|
+
out.push({
|
|
2490
|
+
kind: 'data',
|
|
2491
|
+
row: n.data,
|
|
2492
|
+
rowIndex: idx++,
|
|
2493
|
+
level: n.level,
|
|
2494
|
+
hasChildren: n.hasChildren,
|
|
2495
|
+
expanded: expanded.has(n.key),
|
|
2496
|
+
nodeKey: n.key,
|
|
2497
|
+
});
|
|
2498
|
+
if (n.hasChildren && expanded.has(n.key)) {
|
|
2499
|
+
walk(n.children);
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
};
|
|
2503
|
+
walk(this.treeRoots());
|
|
2504
|
+
return out;
|
|
2505
|
+
}
|
|
2506
|
+
const groupCols = this.groupColumns();
|
|
2507
|
+
const rows = this.pagedRows();
|
|
2508
|
+
if (groupCols.length === 0) {
|
|
2509
|
+
return rows.map((row, i) => ({ kind: 'data', row, rowIndex: i }));
|
|
2510
|
+
}
|
|
2511
|
+
const out = [];
|
|
2512
|
+
const items = this.groupItems();
|
|
2513
|
+
let dataIndex = 0;
|
|
2514
|
+
const walk = (subset, level, parentKey) => {
|
|
2515
|
+
const col = groupCols[level];
|
|
2516
|
+
const field = col.dataField();
|
|
2517
|
+
// Contiguous partition by group value — displayRows pre-sorts by the group chain, so
|
|
2518
|
+
// first-seen bucket order IS the dx group order.
|
|
2519
|
+
const buckets = new Map();
|
|
2520
|
+
for (const r of subset) {
|
|
2521
|
+
const v = this.getRaw(r, field);
|
|
2522
|
+
if (!buckets.has(v)) {
|
|
2523
|
+
buckets.set(v, []);
|
|
2524
|
+
}
|
|
2525
|
+
buckets.get(v).push(r);
|
|
2526
|
+
}
|
|
2527
|
+
for (const [value, bucket] of buckets) {
|
|
2528
|
+
const groupKey = `${parentKey}/${level}:${typeof value}:${String(value)}`;
|
|
2529
|
+
const isCollapsed = !this.isGroupExpanded(groupKey);
|
|
2530
|
+
// dx three-way split: footer items → group footer row; alignByColumn items →
|
|
2531
|
+
// summary cells aligned under their column; the rest → "(a, b)" caption suffix.
|
|
2532
|
+
const alignedSums = new Map();
|
|
2533
|
+
const captionParts = [];
|
|
2534
|
+
const footerSums = new Map();
|
|
2535
|
+
for (const item of items) {
|
|
2536
|
+
const text = this.summaryText(item, bucket);
|
|
2537
|
+
if (item.showInGroupFooter()) {
|
|
2538
|
+
footerSums.set(item.column(), text);
|
|
2539
|
+
}
|
|
2540
|
+
else if (item.alignByColumn()) {
|
|
2541
|
+
alignedSums.set(item.showInColumn() ?? item.column(), text);
|
|
2542
|
+
}
|
|
2543
|
+
else {
|
|
2544
|
+
captionParts.push(text);
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
out.push({
|
|
2548
|
+
kind: 'group',
|
|
2549
|
+
level,
|
|
2550
|
+
column: col,
|
|
2551
|
+
text: this.cellText(col, bucket[0]),
|
|
2552
|
+
value: this.getRaw(bucket[0], col.dataField()),
|
|
2553
|
+
rows: bucket,
|
|
2554
|
+
groupKey,
|
|
2555
|
+
collapsed: isCollapsed,
|
|
2556
|
+
alignedSums,
|
|
2557
|
+
captionSuffix: captionParts.length ? ` (${captionParts.join(', ')})` : '',
|
|
2558
|
+
});
|
|
2559
|
+
if (!isCollapsed) {
|
|
2560
|
+
if (level + 1 < groupCols.length) {
|
|
2561
|
+
walk(bucket, level + 1, groupKey);
|
|
2562
|
+
}
|
|
2563
|
+
else {
|
|
2564
|
+
for (const r of bucket) {
|
|
2565
|
+
out.push({ kind: 'data', row: r, rowIndex: dataIndex++ });
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
if (footerSums.size) {
|
|
2569
|
+
out.push({ kind: 'groupFooter', level, sums: footerSums });
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
};
|
|
2574
|
+
walk(rows, 0, '');
|
|
2575
|
+
return out;
|
|
2576
|
+
}, /* @ts-ignore */
|
|
2577
|
+
...(ngDevMode ? [{ debugName: "renderRows" }] : /* istanbul ignore next */ []));
|
|
2578
|
+
/** Computes + formats a summary item over a set of rows. */
|
|
2579
|
+
summaryText(item, rows) {
|
|
2580
|
+
const field = item.column();
|
|
2581
|
+
let value;
|
|
2582
|
+
switch (item.summaryType()) {
|
|
2583
|
+
case 'custom':
|
|
2584
|
+
value = this.customSummaryValue(item, rows);
|
|
2585
|
+
break;
|
|
2586
|
+
case 'count':
|
|
2587
|
+
value = rows.length;
|
|
2588
|
+
break;
|
|
2589
|
+
case 'min':
|
|
2590
|
+
case 'max':
|
|
2591
|
+
case 'avg':
|
|
2592
|
+
case 'sum':
|
|
2593
|
+
default: {
|
|
2594
|
+
const nums = rows
|
|
2595
|
+
.map((r) => Number(this.getRaw(r, field)))
|
|
2596
|
+
.filter((n) => !isNaN(n));
|
|
2597
|
+
if (item.summaryType() === 'min')
|
|
2598
|
+
value = Math.min(...nums);
|
|
2599
|
+
else if (item.summaryType() === 'max')
|
|
2600
|
+
value = Math.max(...nums);
|
|
2601
|
+
else if (item.summaryType() === 'avg')
|
|
2602
|
+
value = nums.reduce((a, b) => a + b, 0) / (nums.length || 1);
|
|
2603
|
+
else
|
|
2604
|
+
value = nums.reduce((a, b) => a + b, 0);
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
const fmt = item.valueFormat();
|
|
2608
|
+
const valueText = typeof fmt === 'string' ? formatNumber(value, parseNumberFormat(fmt)) : String(value);
|
|
2609
|
+
const ct = item.customizeText();
|
|
2610
|
+
const text = ct ? ct({ value, valueText }) : valueText;
|
|
2611
|
+
return item.displayFormat().replace('{0}', text);
|
|
2612
|
+
}
|
|
2613
|
+
/**
|
|
2614
|
+
* DevExtreme `calculateCustomSummary` protocol replay (orbit addition): `start`, then
|
|
2615
|
+
* `calculate` once per row (`options.value` = the row object, matching the app's
|
|
2616
|
+
* `generateSummaryFunction` which reads row properties), then `finalize`.
|
|
2617
|
+
*/
|
|
2618
|
+
customSummaryValue(item, rows) {
|
|
2619
|
+
const fn = this.summaryCmp()?.calculateCustomSummary();
|
|
2620
|
+
if (!fn) {
|
|
2621
|
+
return 0;
|
|
2622
|
+
}
|
|
2623
|
+
const options = { name: item.name(), summaryProcess: 'start', totalValue: 0 };
|
|
2624
|
+
fn(options);
|
|
2625
|
+
options.summaryProcess = 'calculate';
|
|
2626
|
+
for (const row of rows) {
|
|
2627
|
+
options.value = row;
|
|
2628
|
+
fn(options);
|
|
2629
|
+
}
|
|
2630
|
+
options.summaryProcess = 'finalize';
|
|
2631
|
+
delete options.value;
|
|
2632
|
+
fn(options);
|
|
2633
|
+
return Number(options.totalValue) || 0;
|
|
2634
|
+
}
|
|
2635
|
+
toggleGroup(groupKey) {
|
|
2636
|
+
const expanded = this.isGroupExpanded(groupKey);
|
|
2637
|
+
this.groupExpandOverride.update((m) => new Map(m).set(groupKey, !expanded));
|
|
2638
|
+
}
|
|
2639
|
+
goToPage(index) {
|
|
2640
|
+
const next = Math.max(0, Math.min(index, this.pageCount() - 1));
|
|
2641
|
+
if (next !== this.pageIndex()) {
|
|
2642
|
+
this.pageIndex.set(next);
|
|
2643
|
+
this.onOptionChanged.emit({ name: 'paging', fullName: 'paging.pageIndex', value: next });
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
prevPage() {
|
|
2647
|
+
this.goToPage(this.pageIndex() - 1);
|
|
2648
|
+
}
|
|
2649
|
+
nextPage() {
|
|
2650
|
+
this.goToPage(this.pageIndex() + 1);
|
|
2651
|
+
}
|
|
2652
|
+
changePageSize(size) {
|
|
2653
|
+
this.userPageSize.set(size);
|
|
2654
|
+
this.pageIndex.set(0);
|
|
2655
|
+
}
|
|
2656
|
+
onPageSizeSelect(event) {
|
|
2657
|
+
this.changePageSize(Number(event.target.value));
|
|
2658
|
+
}
|
|
2659
|
+
pageInfo = computed(() => {
|
|
2660
|
+
const total = this.displayRows().length;
|
|
2661
|
+
const ps = this.effectivePageSize();
|
|
2662
|
+
const start = total === 0 ? 0 : this.pageIndex() * ps + 1;
|
|
2663
|
+
const end = Math.min((this.pageIndex() + 1) * ps, total);
|
|
2664
|
+
return `${start}-${end} of ${total}`;
|
|
2665
|
+
}, /* @ts-ignore */
|
|
2666
|
+
...(ngDevMode ? [{ debugName: "pageInfo" }] : /* istanbul ignore next */ []));
|
|
2667
|
+
// --- Runtime group state (drag-to-group backbone) --------------------------------------------
|
|
2668
|
+
static fieldArrayEqual = (a, b) => a.length === b.length && a.every((v, i) => v === b[i]);
|
|
2669
|
+
/**
|
|
2670
|
+
* Declarative seed: ordered dataFields of columns declaring `groupIndex`. Custom equality so
|
|
2671
|
+
* unrelated column-input churn (captions, widths, lookups) can never reset runtime drag state.
|
|
2672
|
+
*/
|
|
2673
|
+
declaredGroupFields = computed(() => this.columns()
|
|
2674
|
+
.filter((c) => c.groupIndex() != null)
|
|
2675
|
+
.sort((a, b) => (a.groupIndex() ?? 0) - (b.groupIndex() ?? 0))
|
|
2676
|
+
.map((c) => c.dataField()), { ...(ngDevMode ? { debugName: "declaredGroupFields" } : /* istanbul ignore next */ {}), equal: NxeDataGridComponent.fieldArrayEqual });
|
|
2677
|
+
/**
|
|
2678
|
+
* THE group state — array order == group level. Runtime gestures (drag-to-group, chip
|
|
2679
|
+
* drag-out/reorder, clearGrouping) win until the declarative seed itself changes
|
|
2680
|
+
* (generic-list re-seeds `groupIndex` from localStorage through `filterResource`).
|
|
2681
|
+
*/
|
|
2682
|
+
groupedFields = linkedSignal({ ...(ngDevMode ? { debugName: "groupedFields" } : /* istanbul ignore next */ {}), source: () => this.declaredGroupFields(),
|
|
2683
|
+
computation: (seed) => seed });
|
|
2684
|
+
/**
|
|
2685
|
+
* Per-grouped-column sort direction (dx: grouping a column sorts it ascending; clicking its
|
|
2686
|
+
* group-panel chip cycles asc↔desc). Pruned to the currently grouped fields so a re-grouped
|
|
2687
|
+
* column starts fresh at `asc`.
|
|
2688
|
+
*/
|
|
2689
|
+
groupSortDir = linkedSignal({ ...(ngDevMode ? { debugName: "groupSortDir" } : /* istanbul ignore next */ {}), source: () => this.groupedFields(),
|
|
2690
|
+
computation: (fields, previous) => {
|
|
2691
|
+
const prev = previous?.value;
|
|
2692
|
+
const next = new Map();
|
|
2693
|
+
if (prev) {
|
|
2694
|
+
for (const field of fields) {
|
|
2695
|
+
const dir = prev.get(field);
|
|
2696
|
+
if (dir) {
|
|
2697
|
+
next.set(field, dir);
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
return next;
|
|
2702
|
+
} });
|
|
2703
|
+
/** Columns moved into group rows, in group-level order. */
|
|
2704
|
+
groupColumns = computed(() => this.groupedFields()
|
|
2705
|
+
.map((field) => this.columns().find((c) => c.dataField() === field))
|
|
2706
|
+
.filter((c) => !!c), /* @ts-ignore */
|
|
2707
|
+
...(ngDevMode ? [{ debugName: "groupColumns" }] : /* istanbul ignore next */ []));
|
|
2708
|
+
/** The active sort direction of a grouped column (chip indicator + group-order comparator). */
|
|
2709
|
+
groupDir(col) {
|
|
2710
|
+
return this.groupSortDir().get(col.dataField()) ?? col.sortOrder() ?? 'asc';
|
|
2711
|
+
}
|
|
2712
|
+
/** Chip click (dx): cycle the grouped column's sort asc↔desc — never back to none. */
|
|
2713
|
+
sortGroupColumn(col) {
|
|
2714
|
+
const field = col.dataField();
|
|
2715
|
+
if (!this.groupedFields().includes(field)) {
|
|
2716
|
+
return;
|
|
2717
|
+
}
|
|
2718
|
+
const next = this.groupDir(col) === 'asc' ? 'desc' : 'asc';
|
|
2719
|
+
this.groupSortDir.update((m) => new Map(m).set(field, next));
|
|
2720
|
+
}
|
|
2721
|
+
/** Gesture: drag a header into the group panel — append as the last group level. */
|
|
2722
|
+
groupByColumn(col) {
|
|
2723
|
+
const field = col.dataField();
|
|
2724
|
+
// allowGrouping guard here too: with allowColumnReordering on, headers are draggable
|
|
2725
|
+
// even when the column refuses grouping — a panel drop must then be a no-op.
|
|
2726
|
+
if (!field || !col.allowGrouping() || this.groupedFields().includes(field)) {
|
|
2727
|
+
return;
|
|
2728
|
+
}
|
|
2729
|
+
const level = this.groupedFields().length;
|
|
2730
|
+
// dx moves an active user sort on this column onto the group level (chip indicator).
|
|
2731
|
+
const current = this.sortInit() ? this.sort() : null;
|
|
2732
|
+
this.groupedFields.update((fields) => [...fields, field]);
|
|
2733
|
+
if (current?.field === field) {
|
|
2734
|
+
this.groupSortDir.update((m) => new Map(m).set(field, current.dir));
|
|
2735
|
+
this.sort.set(null);
|
|
2736
|
+
}
|
|
2737
|
+
this.emitGroupIndexChange(this.columns().indexOf(col), null, level);
|
|
2738
|
+
}
|
|
2739
|
+
/** Gesture: drag a chip out of the group panel — remove that group level. */
|
|
2740
|
+
ungroupColumn(field) {
|
|
2741
|
+
const fields = this.groupedFields();
|
|
2742
|
+
const level = fields.indexOf(field);
|
|
2743
|
+
if (level === -1) {
|
|
2744
|
+
return;
|
|
2745
|
+
}
|
|
2746
|
+
// dx keeps the column sorted when it leaves the group panel: restore its group direction
|
|
2747
|
+
// as the header sort — unless another column already carries the active user sort.
|
|
2748
|
+
const col = this.columns().find((c) => c.dataField() === field);
|
|
2749
|
+
const dir = col ? this.groupDir(col) : 'asc';
|
|
2750
|
+
this.groupedFields.set(fields.filter((f) => f !== field));
|
|
2751
|
+
if (!this.effectiveSort()) {
|
|
2752
|
+
this.sortInit.set(true);
|
|
2753
|
+
this.sort.set({ field, dir });
|
|
2754
|
+
}
|
|
2755
|
+
this.emitGroupIndexChange(this.columns().findIndex((c) => c.dataField() === field), level, null);
|
|
2756
|
+
}
|
|
2757
|
+
/**
|
|
2758
|
+
* Gesture: reorder chips within the group panel — move a group level.
|
|
2759
|
+
*
|
|
2760
|
+
* Emission contract: generic-list's frozen handler replays each event as a positional SWAP,
|
|
2761
|
+
* while a chip drag is a MOVE. The two only coincide for adjacent positions, so a distance-n
|
|
2762
|
+
* move emits n adjacent-swap events (each adjacent swap == the corresponding move step) —
|
|
2763
|
+
* the replayed array converges to exactly the grid's order.
|
|
2764
|
+
*/
|
|
2765
|
+
moveGroup(from, to) {
|
|
2766
|
+
const fields = this.groupedFields();
|
|
2767
|
+
if (from === to || from < 0 || to < 0 || from >= fields.length || to >= fields.length) {
|
|
2768
|
+
return;
|
|
2769
|
+
}
|
|
2770
|
+
const next = [...fields];
|
|
2771
|
+
const [moved] = next.splice(from, 1);
|
|
2772
|
+
next.splice(to, 0, moved);
|
|
2773
|
+
this.groupedFields.set(next);
|
|
2774
|
+
const movedColumnIndex = this.columns().findIndex((c) => c.dataField() === moved);
|
|
2775
|
+
const step = from < to ? 1 : -1;
|
|
2776
|
+
for (let level = from; level !== to; level += step) {
|
|
2777
|
+
this.emitGroupIndexChange(movedColumnIndex, level, level + step);
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
/**
|
|
2781
|
+
* Real dx `clearGrouping()`: ungroup everything. Removal events fire in DESCENDING level
|
|
2782
|
+
* order so generic-list's `splice(previousValue, 1)` replay converges to `[]`.
|
|
2783
|
+
*/
|
|
2784
|
+
clearGroupingInternal() {
|
|
2785
|
+
const fields = this.groupedFields();
|
|
2786
|
+
if (!fields.length) {
|
|
2787
|
+
return;
|
|
2788
|
+
}
|
|
2789
|
+
this.groupedFields.set([]);
|
|
2790
|
+
for (let level = fields.length - 1; level >= 0; level--) {
|
|
2791
|
+
this.emitGroupIndexChange(this.columns().findIndex((c) => c.dataField() === fields[level]), level, null);
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
emitGroupIndexChange(columnIndex, previousValue, value) {
|
|
2795
|
+
this.onOptionChanged.emit({
|
|
2796
|
+
name: 'columns',
|
|
2797
|
+
fullName: `columns[${columnIndex}].groupIndex`,
|
|
2798
|
+
previousValue,
|
|
2799
|
+
value,
|
|
2800
|
+
});
|
|
2801
|
+
}
|
|
2802
|
+
// --- Runtime column order (allowColumnReordering) + width (allowColumnResizing) overrides ----
|
|
2803
|
+
declaredFieldOrder = computed(() => this.columns().map((c) => c.dataField()), { ...(ngDevMode ? { debugName: "declaredFieldOrder" } : /* istanbul ignore next */ {}), equal: NxeDataGridComponent.fieldArrayEqual });
|
|
2804
|
+
/** Runtime column order; header drag-reorder wins until the declared column set changes. */
|
|
2805
|
+
fieldOrder = linkedSignal({ ...(ngDevMode ? { debugName: "fieldOrder" } : /* istanbul ignore next */ {}), source: () => this.declaredFieldOrder(),
|
|
2806
|
+
computation: (seed) => seed });
|
|
2807
|
+
declaredWidthsKey = computed(() => this.columns()
|
|
2808
|
+
.map((c) => `${c.dataField()}:${c.width() ?? ''}`)
|
|
2809
|
+
.join('|'), /* @ts-ignore */
|
|
2810
|
+
...(ngDevMode ? [{ debugName: "declaredWidthsKey" }] : /* istanbul ignore next */ []));
|
|
2811
|
+
/**
|
|
2812
|
+
* Manual column widths (px). Populated on the first resize with a snapshot of every rendered
|
|
2813
|
+
* width — freezing the layout like dx — and reset when declared widths change.
|
|
2814
|
+
*/
|
|
2815
|
+
widthOverride = linkedSignal({ ...(ngDevMode ? { debugName: "widthOverride" } : /* istanbul ignore next */ {}), source: () => this.declaredWidthsKey(),
|
|
2816
|
+
computation: () => new Map() });
|
|
2817
|
+
/** Visible data columns: `visible`/`columnOption` honoured, grouped columns excluded. */
|
|
2818
|
+
visibleColumns = computed(() => {
|
|
2819
|
+
const grouped = new Set(this.groupColumns());
|
|
2820
|
+
const filtered = this.columns().filter((c) => {
|
|
2821
|
+
if (grouped.has(c)) {
|
|
2822
|
+
return false;
|
|
2823
|
+
}
|
|
2824
|
+
const override = this.visibilityOverride().get(c.dataField());
|
|
2825
|
+
return override ?? c.visible();
|
|
2826
|
+
});
|
|
2827
|
+
const pos = new Map();
|
|
2828
|
+
this.fieldOrder().forEach((f, i) => pos.set(f, i));
|
|
2829
|
+
return [...filtered].sort((a, b) => (pos.get(a.dataField()) ?? 0) - (pos.get(b.dataField()) ?? 0));
|
|
2830
|
+
}, /* @ts-ignore */
|
|
2831
|
+
...(ngDevMode ? [{ debugName: "visibleColumns" }] : /* istanbul ignore next */ []));
|
|
2832
|
+
/**
|
|
2833
|
+
* How many header rows the band structure needs, counted over the VISIBLE tree.
|
|
2834
|
+
*
|
|
2835
|
+
* Pruning matters: a band whose every leaf is hidden or grouped away contributes no row, so
|
|
2836
|
+
* a grid that loses its last band collapses back to a single header row rather than keeping
|
|
2837
|
+
* an empty one.
|
|
2838
|
+
*/
|
|
2839
|
+
headerDepth = computed(() => {
|
|
2840
|
+
const visible = new Set(this.visibleColumns());
|
|
2841
|
+
const depthOf = (cols) => {
|
|
2842
|
+
let d = 0;
|
|
2843
|
+
for (const col of cols) {
|
|
2844
|
+
if (col.isBand()) {
|
|
2845
|
+
const inner = depthOf(col.childColumns());
|
|
2846
|
+
if (inner > 0) {
|
|
2847
|
+
d = Math.max(d, 1 + inner);
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
else if (visible.has(col)) {
|
|
2851
|
+
d = Math.max(d, 1);
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
return d;
|
|
2855
|
+
};
|
|
2856
|
+
return Math.max(1, depthOf(this.declaredColumns()));
|
|
2857
|
+
}, /* @ts-ignore */
|
|
2858
|
+
...(ngDevMode ? [{ debugName: "headerDepth" }] : /* istanbul ignore next */ []));
|
|
2859
|
+
/**
|
|
2860
|
+
* The header as rows of cells: bands carry `colspan`, leaves above the bottom row carry
|
|
2861
|
+
* `rowspan`, which is dx's own banded markup.
|
|
2862
|
+
*/
|
|
2863
|
+
headerRows = computed(() => {
|
|
2864
|
+
const visible = this.visibleColumns();
|
|
2865
|
+
const visibleSet = new Set(visible);
|
|
2866
|
+
const indexOfLeaf = new Map();
|
|
2867
|
+
visible.forEach((c, i) => indexOfLeaf.set(c, i));
|
|
2868
|
+
const visibleLeaves = (col) => col.isBand()
|
|
2869
|
+
? col.childColumns().reduce((n, c) => n + visibleLeaves(c), 0)
|
|
2870
|
+
: visibleSet.has(col)
|
|
2871
|
+
? 1
|
|
2872
|
+
: 0;
|
|
2873
|
+
// Siblings render in visible-leaf order, not declaration order: a drag-reorder rewrites
|
|
2874
|
+
// `visibleColumns()` and the header has to follow, or the body reorders under a header
|
|
2875
|
+
// that did not. A band takes the position of its leftmost leaf.
|
|
2876
|
+
const orderOf = (col) => {
|
|
2877
|
+
if (!col.isBand()) {
|
|
2878
|
+
return indexOfLeaf.get(col) ?? Number.MAX_SAFE_INTEGER;
|
|
2879
|
+
}
|
|
2880
|
+
return col.childColumns().reduce((min, c) => Math.min(min, orderOf(c)), Number.MAX_SAFE_INTEGER);
|
|
2881
|
+
};
|
|
2882
|
+
const depth = this.headerDepth();
|
|
2883
|
+
const rows = Array.from({ length: depth }, () => []);
|
|
2884
|
+
const walk = (cols, level) => {
|
|
2885
|
+
for (const col of [...cols].sort((a, b) => orderOf(a) - orderOf(b))) {
|
|
2886
|
+
const span = visibleLeaves(col);
|
|
2887
|
+
if (span === 0) {
|
|
2888
|
+
continue;
|
|
2889
|
+
}
|
|
2890
|
+
if (col.isBand()) {
|
|
2891
|
+
rows[level].push({
|
|
2892
|
+
column: col,
|
|
2893
|
+
colspan: span,
|
|
2894
|
+
rowspan: 1,
|
|
2895
|
+
isBand: true,
|
|
2896
|
+
leafIndex: null,
|
|
2897
|
+
});
|
|
2898
|
+
walk(col.childColumns(), level + 1);
|
|
2899
|
+
}
|
|
2900
|
+
else {
|
|
2901
|
+
rows[level].push({
|
|
2902
|
+
column: col,
|
|
2903
|
+
colspan: 1,
|
|
2904
|
+
rowspan: depth - level,
|
|
2905
|
+
isBand: false,
|
|
2906
|
+
leafIndex: indexOfLeaf.get(col) ?? null,
|
|
2907
|
+
});
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
};
|
|
2911
|
+
walk(this.declaredColumns(), 0);
|
|
2912
|
+
return rows;
|
|
2913
|
+
}, /* @ts-ignore */
|
|
2914
|
+
...(ngDevMode ? [{ debugName: "headerRows" }] : /* istanbul ignore next */ []));
|
|
2915
|
+
/** Drag-reorder drop: move visible column `fromVis` to insertion `slot` (dx visibleIndex). */
|
|
2916
|
+
moveColumnByVisible(fromVis, slot) {
|
|
2917
|
+
const vis = this.visibleColumns();
|
|
2918
|
+
const moved = vis[fromVis]?.dataField();
|
|
2919
|
+
if (!moved) {
|
|
2920
|
+
return;
|
|
2921
|
+
}
|
|
2922
|
+
const target = slot > fromVis ? slot - 1 : slot;
|
|
2923
|
+
if (target === fromVis) {
|
|
2924
|
+
return;
|
|
2925
|
+
}
|
|
2926
|
+
const without = vis.filter((_, i) => i !== fromVis);
|
|
2927
|
+
const anchor = without[target]?.dataField() ?? null;
|
|
2928
|
+
const order = this.fieldOrder().filter((f) => f !== moved);
|
|
2929
|
+
const idx = anchor ? order.indexOf(anchor) : order.length;
|
|
2930
|
+
const next = [...order];
|
|
2931
|
+
next.splice(idx, 0, moved);
|
|
2932
|
+
this.fieldOrder.set(next);
|
|
2933
|
+
this.onOptionChanged.emit({
|
|
2934
|
+
name: 'columns',
|
|
2935
|
+
fullName: `columns[${this.columns().findIndex((c) => c.dataField() === moved)}].visibleIndex`,
|
|
2936
|
+
previousValue: fromVis,
|
|
2937
|
+
value: target,
|
|
2938
|
+
});
|
|
2939
|
+
}
|
|
2940
|
+
/** True when this header renders a resize handle on its right edge. */
|
|
2941
|
+
resizeHandleVisible(col, ci) {
|
|
2942
|
+
if (!this.allowColumnResizing() || !col.allowResizing()) {
|
|
2943
|
+
return false;
|
|
2944
|
+
}
|
|
2945
|
+
return this.columnResizingMode() === 'widget' || ci < this.visibleColumns().length - 1;
|
|
2946
|
+
}
|
|
2947
|
+
startColumnResize(col, ci, event) {
|
|
2948
|
+
const next = this.columnResizingMode() === 'nextColumn' ? this.visibleColumns()[ci + 1] ?? null : null;
|
|
2949
|
+
this.resize.start(col.dataField(), next?.dataField() ?? null, ci, this.leadCellCount(), event);
|
|
2950
|
+
}
|
|
2951
|
+
/** Command columns preceding the data columns — the offset dx's 1-based aria-colindex counts from. */
|
|
2952
|
+
leadCellCount() {
|
|
2953
|
+
return (this.groupLevelCount() +
|
|
2954
|
+
(this.masterDetailEnabled() ? 1 : 0) +
|
|
2955
|
+
(this.showCheckboxes() ? 1 : 0));
|
|
2956
|
+
}
|
|
2957
|
+
/** Resize commit: freeze all rendered widths (dx post-resize behavior) + emit width events. */
|
|
2958
|
+
commitColumnResize(changes) {
|
|
2959
|
+
const map = new Map(this.widthOverride());
|
|
2960
|
+
const headerRow = nxeLeafHeaderRow(this.elementRef.nativeElement);
|
|
2961
|
+
if (headerRow) {
|
|
2962
|
+
const cells = nxeHeaderDataCells(headerRow);
|
|
2963
|
+
this.visibleColumns().forEach((c, i) => {
|
|
2964
|
+
const cell = cells[i];
|
|
2965
|
+
if (cell) {
|
|
2966
|
+
map.set(c.dataField(), Math.round(cell.getBoundingClientRect().width * 100) / 100);
|
|
2967
|
+
}
|
|
2968
|
+
});
|
|
2969
|
+
}
|
|
2970
|
+
for (const ch of changes) {
|
|
2971
|
+
map.set(ch.field, Math.round(ch.to * 100) / 100);
|
|
2972
|
+
}
|
|
2973
|
+
this.resizeActive = false;
|
|
2974
|
+
this.widthOverride.set(map);
|
|
2975
|
+
for (const ch of changes) {
|
|
2976
|
+
this.onOptionChanged.emit({
|
|
2977
|
+
name: 'columns',
|
|
2978
|
+
fullName: `columns[${this.columns().findIndex((c) => c.dataField() === ch.field)}].width`,
|
|
2979
|
+
previousValue: ch.from,
|
|
2980
|
+
value: ch.to,
|
|
2981
|
+
});
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
filterRowVisible = computed(() => this.filterRow()?.visible === true || this.filterRowCmp()?.visible() === true, /* @ts-ignore */
|
|
2985
|
+
...(ngDevMode ? [{ debugName: "filterRowVisible" }] : /* istanbul ignore next */ []));
|
|
2986
|
+
// --- Master-detail (dxo-master-detail, orbit addition) --------------------------------------
|
|
2987
|
+
masterDetailEnabled = computed(() => this.masterDetail()?.enabled() === true, /* @ts-ignore */
|
|
2988
|
+
...(ngDevMode ? [{ debugName: "masterDetailEnabled" }] : /* istanbul ignore next */ []));
|
|
2989
|
+
expandedDetails = signal(new Set(), /* @ts-ignore */
|
|
2990
|
+
...(ngDevMode ? [{ debugName: "expandedDetails" }] : /* istanbul ignore next */ []));
|
|
2991
|
+
isDetailExpanded(row) {
|
|
2992
|
+
return this.expandedDetails().has(this.rowKey(row));
|
|
2993
|
+
}
|
|
2994
|
+
toggleDetail(row, event) {
|
|
2995
|
+
event.stopPropagation();
|
|
2996
|
+
const key = this.rowKey(row);
|
|
2997
|
+
const expanding = !this.expandedDetails().has(key);
|
|
2998
|
+
if (expanding) {
|
|
2999
|
+
this.onRowExpanding.emit({ key, component: this.facade });
|
|
3000
|
+
}
|
|
3001
|
+
this.expandedDetails.update((set) => {
|
|
3002
|
+
const next = new Set(set);
|
|
3003
|
+
expanding ? next.add(key) : next.delete(key);
|
|
3004
|
+
return next;
|
|
3005
|
+
});
|
|
3006
|
+
if (expanding) {
|
|
3007
|
+
this.onRowExpanded.emit({ key, component: this.facade });
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
detailTemplate() {
|
|
3011
|
+
return this.templateFor(this.masterDetail()?.template());
|
|
3012
|
+
}
|
|
3013
|
+
detailContext(row, rowIndex) {
|
|
3014
|
+
return { $implicit: { data: row, rowIndex, key: this.rowKey(row) } };
|
|
3015
|
+
}
|
|
3016
|
+
/** Group indent levels (dx renders one 30px command column per group level; grid mode only). */
|
|
3017
|
+
groupLevelCount = computed(() => this.treeMode ? 0 : this.groupColumns().length, /* @ts-ignore */
|
|
3018
|
+
...(ngDevMode ? [{ debugName: "groupLevelCount" }] : /* istanbul ignore next */ []));
|
|
3019
|
+
indents(count) {
|
|
3020
|
+
return Array.from({ length: count }, (_, i) => i);
|
|
3021
|
+
}
|
|
3022
|
+
/** Visible columns from the first alignByColumn target onward — each gets its own summary td. */
|
|
3023
|
+
groupRowTrailing(item) {
|
|
3024
|
+
if (!item.alignedSums.size) {
|
|
3025
|
+
return [];
|
|
3026
|
+
}
|
|
3027
|
+
const cols = this.visibleColumns();
|
|
3028
|
+
const first = cols.findIndex((c) => item.alignedSums.has(c.dataField()));
|
|
3029
|
+
if (first === -1) {
|
|
3030
|
+
return [];
|
|
3031
|
+
}
|
|
3032
|
+
let trailing = cols.slice(first);
|
|
3033
|
+
// The caption cell needs at least one column of room — trim leading trailing cells
|
|
3034
|
+
// instead of letting the colspan clamp shift the summary alignment.
|
|
3035
|
+
const maxTrailing = this.allColCount() - item.level - 2 - (this.rowEditCommandVisible() ? 1 : 0);
|
|
3036
|
+
if (trailing.length > maxTrailing) {
|
|
3037
|
+
trailing = trailing.slice(trailing.length - Math.max(0, maxTrailing));
|
|
3038
|
+
}
|
|
3039
|
+
return trailing;
|
|
3040
|
+
}
|
|
3041
|
+
/** dx group-cell span: everything after the expand cell not taken by aligned summary cells. */
|
|
3042
|
+
groupCellColspan(item) {
|
|
3043
|
+
const trailing = this.groupRowTrailing(item).length;
|
|
3044
|
+
const editTd = trailing > 0 && this.rowEditCommandVisible() ? 1 : 0;
|
|
3045
|
+
return Math.max(1, this.allColCount() - item.level - 1 - trailing - editTd);
|
|
3046
|
+
}
|
|
3047
|
+
/** Total column count including command columns (indents/checkbox/expand/edit) for colspans. */
|
|
3048
|
+
allColCount = computed(() => (this.groupLevelCount() +
|
|
3049
|
+
this.visibleColumns().length +
|
|
3050
|
+
(this.showCheckboxes() ? 1 : 0) +
|
|
3051
|
+
(this.masterDetailEnabled() ? 1 : 0) +
|
|
3052
|
+
(this.rowEditCommandVisible() ? 1 : 0)) ||
|
|
3053
|
+
1, /* @ts-ignore */
|
|
3054
|
+
...(ngDevMode ? [{ debugName: "allColCount" }] : /* istanbul ignore next */ []));
|
|
3055
|
+
// --- Inline editing (dxo-editing, orbit addition) -------------------------------------------
|
|
3056
|
+
cellEditingEnabled = computed(() => {
|
|
3057
|
+
const e = this.editingCmp();
|
|
3058
|
+
return !!e && e.mode() === 'cell' && e.allowUpdating();
|
|
3059
|
+
}, /* @ts-ignore */
|
|
3060
|
+
...(ngDevMode ? [{ debugName: "cellEditingEnabled" }] : /* istanbul ignore next */ []));
|
|
3061
|
+
rowEditingEnabled = computed(() => {
|
|
3062
|
+
const e = this.editingCmp();
|
|
3063
|
+
return !!e && e.mode() === 'row' && e.allowUpdating();
|
|
3064
|
+
}, /* @ts-ignore */
|
|
3065
|
+
...(ngDevMode ? [{ debugName: "rowEditingEnabled" }] : /* istanbul ignore next */ []));
|
|
3066
|
+
rowEditCommandVisible = computed(() => this.rowEditingEnabled(), /* @ts-ignore */
|
|
3067
|
+
...(ngDevMode ? [{ debugName: "rowEditCommandVisible" }] : /* istanbul ignore next */ []));
|
|
3068
|
+
/** The single cell in edit mode (`cell` mode): row ref + dataField. */
|
|
3069
|
+
editingCell = signal(null, /* @ts-ignore */
|
|
3070
|
+
...(ngDevMode ? [{ debugName: "editingCell" }] : /* istanbul ignore next */ []));
|
|
3071
|
+
/** Buffered values for the row in edit mode (`row` mode), keyed by dataField. */
|
|
3072
|
+
rowEditBuffer = signal({}, /* @ts-ignore */
|
|
3073
|
+
...(ngDevMode ? [{ debugName: "rowEditBuffer" }] : /* istanbul ignore next */ []));
|
|
3074
|
+
isEditingCell(row, col) {
|
|
3075
|
+
const c = this.editingCell();
|
|
3076
|
+
return !!c && c.row === row && c.field === col.dataField();
|
|
3077
|
+
}
|
|
3078
|
+
isRowEditing(row) {
|
|
3079
|
+
return this.rowEditingEnabled() && this.editingCmp()?.editRowKey() === this.rowKey(row);
|
|
3080
|
+
}
|
|
3081
|
+
editableCell(col) {
|
|
3082
|
+
if (col.allowEditing() === false || !col.dataField()) {
|
|
3083
|
+
return false;
|
|
3084
|
+
}
|
|
3085
|
+
// A `cellTemplate` cell renders its own content, so the built-in editor stays out of its
|
|
3086
|
+
// way. Naming an `editCellTemplate` is an explicit request to edit it anyway — which is
|
|
3087
|
+
// broker's dominant shape, where `customizeText`/`format` render the cell at rest and a
|
|
3088
|
+
// projected editor takes over on click.
|
|
3089
|
+
return !!col.editCellTemplate() || !col.cellTemplate();
|
|
3090
|
+
}
|
|
3091
|
+
onCellClick(row, col, event) {
|
|
3092
|
+
if (!this.cellEditingEnabled() || !this.editableCell(col)) {
|
|
3093
|
+
return;
|
|
3094
|
+
}
|
|
3095
|
+
event.stopPropagation();
|
|
3096
|
+
this.editingCell.set({ row, field: col.dataField() });
|
|
3097
|
+
}
|
|
3098
|
+
/** dx cell-mode semantics: commit to the row on blur/Enter (client-side array write-through). */
|
|
3099
|
+
commitCellEdit(row, col, rawInput) {
|
|
3100
|
+
this.writeCell(row, col, rawInput);
|
|
3101
|
+
this.editingCell.set(null);
|
|
3102
|
+
this.refreshTick.update((v) => v + 1);
|
|
3103
|
+
}
|
|
3104
|
+
/** Write a cell value through the grid's own path, leaving edit state alone (dx `setValue`). */
|
|
3105
|
+
writeCellValue(row, col, value) {
|
|
3106
|
+
this.writeCell(row, col, value);
|
|
3107
|
+
this.refreshTick.update((v) => v + 1);
|
|
3108
|
+
}
|
|
3109
|
+
cancelCellEdit() {
|
|
3110
|
+
this.editingCell.set(null);
|
|
3111
|
+
}
|
|
3112
|
+
/**
|
|
3113
|
+
* Leave cell-edit state when focus goes somewhere outside the cell.
|
|
3114
|
+
*
|
|
3115
|
+
* The built-in editor commits on its own blur, but a projected `editCellTemplate` has no such
|
|
3116
|
+
* contract, so the cell needs to notice for itself. Nothing is committed here: those templates
|
|
3117
|
+
* write through the live row object, so leaving edit state is purely a view change.
|
|
3118
|
+
*
|
|
3119
|
+
* A projected picker opens its panel in a CDK overlay appended to `<body>`, which means focus
|
|
3120
|
+
* legitimately leaves the `<td>` while the edit is still going on — hence the second guard.
|
|
3121
|
+
*/
|
|
3122
|
+
onEditCellFocusOut(row, col, event) {
|
|
3123
|
+
if (!col.editCellTemplate() || !this.isEditingCell(row, col)) {
|
|
3124
|
+
return;
|
|
3125
|
+
}
|
|
3126
|
+
const td = event.currentTarget;
|
|
3127
|
+
const next = event.relatedTarget;
|
|
3128
|
+
if (!td || (next && td.contains(next))) {
|
|
3129
|
+
return;
|
|
3130
|
+
}
|
|
3131
|
+
if (document.querySelector('.cdk-overlay-container')?.hasChildNodes()) {
|
|
3132
|
+
return;
|
|
3133
|
+
}
|
|
3134
|
+
this.editingCell.set(null);
|
|
3135
|
+
}
|
|
3136
|
+
writeCell(row, col, rawInput) {
|
|
3137
|
+
const field = col.dataField();
|
|
3138
|
+
let value = rawInput;
|
|
3139
|
+
if (col.dataType() === 'number' && typeof rawInput === 'string') {
|
|
3140
|
+
const n = Number(rawInput.replace(/,/g, ''));
|
|
3141
|
+
value = rawInput.trim() === '' || isNaN(n) ? null : n;
|
|
3142
|
+
}
|
|
3143
|
+
if (field.includes('.')) {
|
|
3144
|
+
const parts = field.split('.');
|
|
3145
|
+
const last = parts.pop();
|
|
3146
|
+
const target = parts.reduce((o, k) => (o == null ? o : o[k]), row);
|
|
3147
|
+
if (target != null) {
|
|
3148
|
+
target[last] = value;
|
|
3149
|
+
}
|
|
3150
|
+
return;
|
|
3151
|
+
}
|
|
3152
|
+
row[field] = value;
|
|
3153
|
+
}
|
|
3154
|
+
startRowEdit(row, event) {
|
|
3155
|
+
event.stopPropagation();
|
|
3156
|
+
this.editingCmp()?.editRowKey.set(this.rowKey(row));
|
|
3157
|
+
this.rowEditBuffer.set({});
|
|
3158
|
+
}
|
|
3159
|
+
bufferRowEdit(col, rawInput) {
|
|
3160
|
+
this.rowEditBuffer.update((b) => ({ ...b, [col.dataField()]: rawInput }));
|
|
3161
|
+
}
|
|
3162
|
+
/** dx row-mode Save: apply the buffer to the row and record an `update` change. */
|
|
3163
|
+
saveRowEdit(row, event) {
|
|
3164
|
+
event.stopPropagation();
|
|
3165
|
+
const editing = this.editingCmp();
|
|
3166
|
+
if (!editing) {
|
|
3167
|
+
return;
|
|
3168
|
+
}
|
|
3169
|
+
const buffer = this.rowEditBuffer();
|
|
3170
|
+
const data = {};
|
|
3171
|
+
for (const [field, raw] of Object.entries(buffer)) {
|
|
3172
|
+
const col = this.columns().find((c) => c.dataField() === field);
|
|
3173
|
+
if (col) {
|
|
3174
|
+
this.writeCell(row, col, raw);
|
|
3175
|
+
data[field] = this.getRaw(row, field);
|
|
3176
|
+
}
|
|
3177
|
+
}
|
|
3178
|
+
if (Object.keys(data).length) {
|
|
3179
|
+
editing.changes.update((list) => [
|
|
3180
|
+
...list.filter((c) => !(c.type === 'update' && c.key === this.rowKey(row))),
|
|
3181
|
+
{ type: 'update', key: this.rowKey(row), data },
|
|
3182
|
+
]);
|
|
3183
|
+
}
|
|
3184
|
+
editing.editRowKey.set(null);
|
|
3185
|
+
this.rowEditBuffer.set({});
|
|
3186
|
+
this.refreshTick.update((v) => v + 1);
|
|
3187
|
+
}
|
|
3188
|
+
cancelRowEdit(event) {
|
|
3189
|
+
event.stopPropagation();
|
|
3190
|
+
this.editingCmp()?.editRowKey.set(null);
|
|
3191
|
+
this.rowEditBuffer.set({});
|
|
3192
|
+
}
|
|
3193
|
+
editorInputValue(row, col) {
|
|
3194
|
+
if (this.isRowEditing(row)) {
|
|
3195
|
+
const buffered = this.rowEditBuffer()[col.dataField()];
|
|
3196
|
+
if (buffered !== undefined) {
|
|
3197
|
+
return String(buffered);
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3200
|
+
const raw = this.getRaw(row, col.dataField());
|
|
3201
|
+
return raw == null ? '' : String(raw);
|
|
3202
|
+
}
|
|
3203
|
+
// --- Cell merge (`[mergeColumns]`, orbit addition) ------------------------------------------
|
|
3204
|
+
/**
|
|
3205
|
+
* Rowspan map over `renderRows`: `"<renderIndex>:<field>"` → span (>1 = merged head,
|
|
3206
|
+
* 0 = suppressed continuation, absent = normal cell). Hierarchical: a value break in an
|
|
3207
|
+
* earlier merge column breaks all later ones; group/detail rows break every run.
|
|
3208
|
+
*/
|
|
3209
|
+
mergeSpans = computed(() => {
|
|
3210
|
+
const fields = this.mergeColumns();
|
|
3211
|
+
const map = new Map();
|
|
3212
|
+
if (!fields.length) {
|
|
3213
|
+
return map;
|
|
3214
|
+
}
|
|
3215
|
+
const rows = this.renderRows();
|
|
3216
|
+
// runStart[f] = renderRows index where the current run for field f began.
|
|
3217
|
+
const runStart = new Map();
|
|
3218
|
+
const runLength = new Map();
|
|
3219
|
+
const flush = (field) => {
|
|
3220
|
+
const start = runStart.get(field);
|
|
3221
|
+
const len = runLength.get(field) ?? 0;
|
|
3222
|
+
if (start !== undefined && len > 1) {
|
|
3223
|
+
map.set(`${start}:${field}`, len);
|
|
3224
|
+
for (let i = start + 1; i < start + len; i++) {
|
|
3225
|
+
map.set(`${i}:${field}`, 0);
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
runStart.delete(field);
|
|
3229
|
+
runLength.delete(field);
|
|
3230
|
+
};
|
|
3231
|
+
let prevRow = null;
|
|
3232
|
+
rows.forEach((item, idx) => {
|
|
3233
|
+
if (item.kind !== 'data') {
|
|
3234
|
+
fields.forEach(flush);
|
|
3235
|
+
prevRow = null;
|
|
3236
|
+
return;
|
|
3237
|
+
}
|
|
3238
|
+
let broken = false;
|
|
3239
|
+
for (const field of fields) {
|
|
3240
|
+
const same = !broken &&
|
|
3241
|
+
prevRow != null &&
|
|
3242
|
+
this.getRaw(prevRow, field) === this.getRaw(item.row, field) &&
|
|
3243
|
+
this.getRaw(item.row, field) != null &&
|
|
3244
|
+
this.getRaw(item.row, field) !== '';
|
|
3245
|
+
if (same) {
|
|
3246
|
+
runLength.set(field, (runLength.get(field) ?? 1) + 1);
|
|
3247
|
+
}
|
|
3248
|
+
else {
|
|
3249
|
+
broken = true;
|
|
3250
|
+
flush(field);
|
|
3251
|
+
runStart.set(field, idx);
|
|
3252
|
+
runLength.set(field, 1);
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
prevRow = item.row;
|
|
3256
|
+
});
|
|
3257
|
+
fields.forEach(flush);
|
|
3258
|
+
return map;
|
|
3259
|
+
}, /* @ts-ignore */
|
|
3260
|
+
...(ngDevMode ? [{ debugName: "mergeSpans" }] : /* istanbul ignore next */ []));
|
|
3261
|
+
mergeSpan(renderIndex, col) {
|
|
3262
|
+
if (!this.mergeColumns().includes(col.dataField())) {
|
|
3263
|
+
return null;
|
|
3264
|
+
}
|
|
3265
|
+
return this.mergeSpans().get(`${renderIndex}:${col.dataField()}`) ?? null;
|
|
3266
|
+
}
|
|
3267
|
+
isMergeSuppressed(renderIndex, col) {
|
|
3268
|
+
return this.mergeSpan(renderIndex, col) === 0;
|
|
3269
|
+
}
|
|
3270
|
+
columnByField = computed(() => {
|
|
3271
|
+
const map = new Map();
|
|
3272
|
+
for (const c of this.columns()) {
|
|
3273
|
+
map.set(c.dataField(), c);
|
|
3274
|
+
}
|
|
3275
|
+
return map;
|
|
3276
|
+
}, /* @ts-ignore */
|
|
3277
|
+
...(ngDevMode ? [{ debugName: "columnByField" }] : /* istanbul ignore next */ []));
|
|
3278
|
+
filterRowPredicate = computed(() => {
|
|
3279
|
+
const state = this.filterRowState();
|
|
3280
|
+
if (!state.size) {
|
|
3281
|
+
return null;
|
|
3282
|
+
}
|
|
3283
|
+
const byField = this.columnByField();
|
|
3284
|
+
const matchers = [];
|
|
3285
|
+
for (const [field, entry] of state) {
|
|
3286
|
+
const col = byField.get(field);
|
|
3287
|
+
if (!col) {
|
|
3288
|
+
continue;
|
|
3289
|
+
}
|
|
3290
|
+
const matcher = this.buildFilterRowMatcher(col, entry);
|
|
3291
|
+
if (matcher) {
|
|
3292
|
+
matchers.push(matcher);
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
return matchers.length ? (row) => matchers.every((m) => m(row)) : null;
|
|
3296
|
+
}, /* @ts-ignore */
|
|
3297
|
+
...(ngDevMode ? [{ debugName: "filterRowPredicate" }] : /* istanbul ignore next */ []));
|
|
3298
|
+
/** Builds one column's filter-row matcher; null when the entry has no effective value yet. */
|
|
3299
|
+
buildFilterRowMatcher(col, entry) {
|
|
3300
|
+
const kind = this.frEditorKind(col);
|
|
3301
|
+
const t1 = entry.text.trim();
|
|
3302
|
+
const t2 = (entry.text2 ?? '').trim();
|
|
3303
|
+
if (!t1 && !t2) {
|
|
3304
|
+
return null;
|
|
3305
|
+
}
|
|
3306
|
+
if (kind === 'lookup' || kind === 'boolean') {
|
|
3307
|
+
// select editors: dx filters by '=' on the raw value; 'false' must NOT match blanks
|
|
3308
|
+
return (row) => {
|
|
3309
|
+
const raw = this.getRaw(row, col.dataField());
|
|
3310
|
+
if (kind === 'boolean') {
|
|
3311
|
+
return t1 === 'true' ? raw === true : raw === false;
|
|
3312
|
+
}
|
|
3313
|
+
return String(raw ?? '') === t1;
|
|
3314
|
+
};
|
|
3315
|
+
}
|
|
3316
|
+
if (kind === 'number') {
|
|
3317
|
+
const v1 = t1 ? Number(t1.replace(/,/g, '')) : null;
|
|
3318
|
+
const v2 = t2 ? Number(t2.replace(/,/g, '')) : null;
|
|
3319
|
+
if ((t1 && Number.isNaN(v1)) || (t2 && Number.isNaN(v2))) {
|
|
3320
|
+
return null; // dx applies nothing until the editor holds a valid number
|
|
3321
|
+
}
|
|
3322
|
+
return (row) => {
|
|
3323
|
+
const raw = this.getRaw(row, col.dataField());
|
|
3324
|
+
const n = raw == null || raw === '' ? null : Number(raw);
|
|
3325
|
+
if (n == null || Number.isNaN(n)) {
|
|
3326
|
+
return entry.op === '<>';
|
|
3327
|
+
}
|
|
3328
|
+
return this.matchScalar(entry.op, n, v1, v2);
|
|
3329
|
+
};
|
|
3330
|
+
}
|
|
3331
|
+
if (kind === 'date') {
|
|
3332
|
+
const d1 = t1 ? this.dayKey(t1) : null;
|
|
3333
|
+
const d2 = t2 ? this.dayKey(t2) : null;
|
|
3334
|
+
if ((t1 && !d1) || (t2 && !d2)) {
|
|
3335
|
+
return null;
|
|
3336
|
+
}
|
|
3337
|
+
return (row) => {
|
|
3338
|
+
const raw = this.getRaw(row, col.dataField());
|
|
3339
|
+
const day = raw == null || raw === '' ? null : this.dayKey(raw);
|
|
3340
|
+
if (!day) {
|
|
3341
|
+
return entry.op === '<>';
|
|
3342
|
+
}
|
|
3343
|
+
// dx expands date '=' to the whole day — day keys compare lexicographically.
|
|
3344
|
+
return this.matchScalar(entry.op, day, d1, d2);
|
|
3345
|
+
};
|
|
3346
|
+
}
|
|
3347
|
+
// string operations, case-insensitive over the displayed text
|
|
3348
|
+
const q = t1.toLowerCase();
|
|
3349
|
+
return (row) => {
|
|
3350
|
+
const text = this.cellText(col, row).toLowerCase();
|
|
3351
|
+
switch (entry.op) {
|
|
3352
|
+
case 'notcontains':
|
|
3353
|
+
return !text.includes(q);
|
|
3354
|
+
case 'startswith':
|
|
3355
|
+
return text.startsWith(q);
|
|
3356
|
+
case 'endswith':
|
|
3357
|
+
return text.endsWith(q);
|
|
3358
|
+
case '=':
|
|
3359
|
+
return text === q;
|
|
3360
|
+
case '<>':
|
|
3361
|
+
return text !== q;
|
|
3362
|
+
default:
|
|
3363
|
+
return text.includes(q);
|
|
3364
|
+
}
|
|
3365
|
+
};
|
|
3366
|
+
}
|
|
3367
|
+
matchScalar(op, value, v1, v2) {
|
|
3368
|
+
switch (op) {
|
|
3369
|
+
case '<>':
|
|
3370
|
+
return value !== v1;
|
|
3371
|
+
case '<':
|
|
3372
|
+
return v1 != null && value < v1;
|
|
3373
|
+
case '<=':
|
|
3374
|
+
return v1 != null && value <= v1;
|
|
3375
|
+
case '>':
|
|
3376
|
+
return v1 != null && value > v1;
|
|
3377
|
+
case '>=':
|
|
3378
|
+
return v1 != null && value >= v1;
|
|
3379
|
+
case 'between':
|
|
3380
|
+
return (v1 == null || value >= v1) && (v2 == null || value <= v2);
|
|
3381
|
+
default:
|
|
3382
|
+
return value === v1;
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
dayKey(raw) {
|
|
3386
|
+
const dt = parseDateValue(raw);
|
|
3387
|
+
return dt ? dt.toFormat('yyyy-MM-dd') : null;
|
|
3388
|
+
}
|
|
3389
|
+
/**
|
|
3390
|
+
* The applied header filters: runtime overrides win; untouched columns fall back to their
|
|
3391
|
+
* declarative `filterValues` seed. Values are normalized through `hfKey`.
|
|
3392
|
+
*/
|
|
3393
|
+
effectiveHeaderFilters = computed(() => {
|
|
3394
|
+
const out = new Map();
|
|
3395
|
+
const override = this.hfOverride();
|
|
3396
|
+
for (const col of this.columns()) {
|
|
3397
|
+
const field = col.dataField();
|
|
3398
|
+
if (!field || out.has(field)) {
|
|
3399
|
+
continue;
|
|
3400
|
+
}
|
|
3401
|
+
if (override.has(field)) {
|
|
3402
|
+
const set = override.get(field);
|
|
3403
|
+
if (set) {
|
|
3404
|
+
out.set(field, set);
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
else {
|
|
3408
|
+
const seed = col.filterValues();
|
|
3409
|
+
if (seed?.length) {
|
|
3410
|
+
out.set(field, new Set(seed.map((v) => this.hfKey(col, v))));
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
return out;
|
|
3415
|
+
}, /* @ts-ignore */
|
|
3416
|
+
...(ngDevMode ? [{ debugName: "effectiveHeaderFilters" }] : /* istanbul ignore next */ []));
|
|
3417
|
+
headerFilterPredicate = computed(() => {
|
|
3418
|
+
const hf = this.effectiveHeaderFilters();
|
|
3419
|
+
if (!hf.size) {
|
|
3420
|
+
return null;
|
|
3421
|
+
}
|
|
3422
|
+
const byField = this.columnByField();
|
|
3423
|
+
const entries = [...hf].map(([field, set]) => ({ field, set, col: byField.get(field) }));
|
|
3424
|
+
return (row) => entries.every(({ field, set, col }) => set.has(col ? this.hfKey(col, this.getRaw(row, field)) : this.getRaw(row, field)));
|
|
3425
|
+
}, /* @ts-ignore */
|
|
3426
|
+
...(ngDevMode ? [{ debugName: "headerFilterPredicate" }] : /* istanbul ignore next */ []));
|
|
3427
|
+
/** dataSource → filter-row filters → header filters (sorting happens in `displayRows`). */
|
|
3428
|
+
filteredRows = computed(() => {
|
|
3429
|
+
this.refreshTick();
|
|
3430
|
+
let rows = (this.dataSource() ?? []);
|
|
3431
|
+
const rowFilter = this.filterRowPredicate();
|
|
3432
|
+
if (rowFilter) {
|
|
3433
|
+
rows = rows.filter(rowFilter);
|
|
3434
|
+
}
|
|
3435
|
+
const hf = this.headerFilterPredicate();
|
|
3436
|
+
if (hf) {
|
|
3437
|
+
rows = rows.filter(hf);
|
|
3438
|
+
}
|
|
3439
|
+
return rows;
|
|
3440
|
+
}, /* @ts-ignore */
|
|
3441
|
+
...(ngDevMode ? [{ debugName: "filteredRows" }] : /* istanbul ignore next */ []));
|
|
3442
|
+
/**
|
|
3443
|
+
* dx sort semantics: grouped columns lead the sort expression in level order (each by its
|
|
3444
|
+
* group direction), then the user/declarative sort applies WITHIN the innermost groups.
|
|
3445
|
+
* Ungrouped grids keep the single-column behavior unchanged.
|
|
3446
|
+
*/
|
|
3447
|
+
displayRows = computed(() => {
|
|
3448
|
+
const rows = this.filteredRows();
|
|
3449
|
+
const chain = [];
|
|
3450
|
+
if (!this.treeMode) {
|
|
3451
|
+
for (const col of this.groupColumns()) {
|
|
3452
|
+
chain.push({ field: col.dataField(), dir: this.groupDir(col) });
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
const user = this.effectiveSort();
|
|
3456
|
+
if (user && !chain.some((s) => s.field === user.field)) {
|
|
3457
|
+
chain.push(user);
|
|
3458
|
+
}
|
|
3459
|
+
if (!chain.length) {
|
|
3460
|
+
return rows;
|
|
3461
|
+
}
|
|
3462
|
+
return [...rows].sort((a, b) => {
|
|
3463
|
+
for (const s of chain) {
|
|
3464
|
+
const r = this.compare(this.getRaw(a, s.field), this.getRaw(b, s.field));
|
|
3465
|
+
if (r !== 0) {
|
|
3466
|
+
return s.dir === 'asc' ? r : -r;
|
|
3467
|
+
}
|
|
3468
|
+
}
|
|
3469
|
+
return 0;
|
|
3470
|
+
});
|
|
3471
|
+
}, /* @ts-ignore */
|
|
3472
|
+
...(ngDevMode ? [{ debugName: "displayRows" }] : /* istanbul ignore next */ []));
|
|
3473
|
+
loading = computed(() => this.dataSource() == null && this.loadPanel()?.enabled !== false, /* @ts-ignore */
|
|
3474
|
+
...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
3475
|
+
/** The active sort: an explicit user click, else the first column declaring `sortOrder`. */
|
|
3476
|
+
effectiveSort() {
|
|
3477
|
+
if (this.sortInit()) {
|
|
3478
|
+
return this.sort();
|
|
3479
|
+
}
|
|
3480
|
+
const sorted = this.columns().find((c) => c.sortOrder());
|
|
3481
|
+
return sorted ? { field: sorted.dataField(), dir: sorted.sortOrder() } : null;
|
|
3482
|
+
}
|
|
3483
|
+
// --- Rendering helpers -----------------------------------------------------------------------
|
|
3484
|
+
align(col) {
|
|
3485
|
+
const a = col.alignment();
|
|
3486
|
+
if (a) {
|
|
3487
|
+
return a;
|
|
3488
|
+
}
|
|
3489
|
+
switch (col.dataType()) {
|
|
3490
|
+
case 'number':
|
|
3491
|
+
return 'right';
|
|
3492
|
+
case 'boolean':
|
|
3493
|
+
return 'center';
|
|
3494
|
+
default:
|
|
3495
|
+
return 'left';
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
colWidth(col) {
|
|
3499
|
+
const manual = this.widthOverride().get(col.dataField());
|
|
3500
|
+
if (manual != null) {
|
|
3501
|
+
return `${manual}px`;
|
|
3502
|
+
}
|
|
3503
|
+
const w = col.width();
|
|
3504
|
+
if (w == null) {
|
|
3505
|
+
return null;
|
|
3506
|
+
}
|
|
3507
|
+
return typeof w === 'number' ? `${w}px` : /^\d+$/.test(w) ? `${w}px` : w;
|
|
3508
|
+
}
|
|
3509
|
+
getRaw(row, dataField) {
|
|
3510
|
+
if (!dataField || row == null) {
|
|
3511
|
+
return undefined;
|
|
3512
|
+
}
|
|
3513
|
+
if (dataField.includes('.')) {
|
|
3514
|
+
return dataField.split('.').reduce((o, k) => (o == null ? o : o[k]), row);
|
|
3515
|
+
}
|
|
3516
|
+
return row[dataField];
|
|
3517
|
+
}
|
|
3518
|
+
/** Resolve the displayed cell text: lookup > customizeText > dataType/format > raw string. */
|
|
3519
|
+
/** dx parity: boolean values render as read-only checkboxes (dx dataType inference did this). */
|
|
3520
|
+
isBooleanCell(col, row) {
|
|
3521
|
+
if (col.dataType() === 'boolean') {
|
|
3522
|
+
return true;
|
|
3523
|
+
}
|
|
3524
|
+
const v = this.getRaw(row, col.dataField());
|
|
3525
|
+
return v === true || v === false;
|
|
3526
|
+
}
|
|
3527
|
+
cellText(col, row) {
|
|
3528
|
+
const raw = this.getRaw(row, col.dataField());
|
|
3529
|
+
const valueText = this.lookupText(col, raw) ?? this.defaultFormat(col, raw);
|
|
3530
|
+
const ct = col.customizeText();
|
|
3531
|
+
return ct ? ct({ value: raw, valueText }) : valueText;
|
|
3532
|
+
}
|
|
3533
|
+
/** Column `lookup` resolution (orbit addition): raw value → display text via the lookup list. */
|
|
3534
|
+
lookupText(col, raw) {
|
|
3535
|
+
const lookup = col.lookup();
|
|
3536
|
+
if (!lookup || raw == null) {
|
|
3537
|
+
return null;
|
|
3538
|
+
}
|
|
3539
|
+
const valueExpr = lookup.valueExpr ?? 'lovCode';
|
|
3540
|
+
const displayExpr = lookup.displayExpr ?? 'lovName';
|
|
3541
|
+
const match = (lookup.dataSource ?? []).find((item) => item?.[valueExpr] === raw);
|
|
3542
|
+
return match ? String(match[displayExpr] ?? raw) : String(raw);
|
|
3543
|
+
}
|
|
3544
|
+
defaultFormat(col, raw) {
|
|
3545
|
+
return nxeDefaultFormat(col.dataType(), col.format(), raw);
|
|
3546
|
+
}
|
|
3547
|
+
templateFor(name) {
|
|
3548
|
+
if (!name) {
|
|
3549
|
+
return null;
|
|
3550
|
+
}
|
|
3551
|
+
return this.templates().find((t) => t.name() === name)?.templateRef ?? null;
|
|
3552
|
+
}
|
|
3553
|
+
cellContext(row, rowIndex, columnIndex, col) {
|
|
3554
|
+
const value = this.getRaw(row, col.dataField());
|
|
3555
|
+
return {
|
|
3556
|
+
$implicit: {
|
|
3557
|
+
data: row,
|
|
3558
|
+
rowIndex,
|
|
3559
|
+
columnIndex,
|
|
3560
|
+
value,
|
|
3561
|
+
displayValue: value,
|
|
3562
|
+
text: this.cellText(col, row),
|
|
3563
|
+
key: row,
|
|
3564
|
+
column: { dataField: col.dataField(), caption: col.caption() },
|
|
3565
|
+
},
|
|
3566
|
+
};
|
|
3567
|
+
}
|
|
3568
|
+
editCellContext(row, rowIndex, columnIndex, col) {
|
|
3569
|
+
const value = this.getRaw(row, col.dataField());
|
|
3570
|
+
// Captured now rather than read inside setValue: which write path applies is decided by
|
|
3571
|
+
// the state the template was rendered in, and `cell` mode clears `editingCell` on commit.
|
|
3572
|
+
const cellMode = this.isEditingCell(row, col);
|
|
3573
|
+
return {
|
|
3574
|
+
$implicit: {
|
|
3575
|
+
data: row,
|
|
3576
|
+
key: this.rowKey(row),
|
|
3577
|
+
value,
|
|
3578
|
+
displayValue: value,
|
|
3579
|
+
text: this.cellText(col, row),
|
|
3580
|
+
rowIndex,
|
|
3581
|
+
columnIndex,
|
|
3582
|
+
column: {
|
|
3583
|
+
dataField: col.dataField(),
|
|
3584
|
+
caption: col.caption(),
|
|
3585
|
+
dataType: col.dataType(),
|
|
3586
|
+
format: col.format(),
|
|
3587
|
+
},
|
|
3588
|
+
// dx's setValue changes the cell value; it does not end the edit. The editor
|
|
3589
|
+
// closes when focus leaves, which `onEditCellFocusOut` handles. Committing here
|
|
3590
|
+
// instead would also fight the click that triggered it: clearing the edit state
|
|
3591
|
+
// mid-dispatch lets the same event bubble to the cell and reopen it.
|
|
3592
|
+
setValue: (newValue) => cellMode ? this.writeCellValue(row, col, newValue) : this.bufferRowEdit(col, newValue),
|
|
3593
|
+
component: this.facade,
|
|
3594
|
+
},
|
|
3595
|
+
};
|
|
3596
|
+
}
|
|
3597
|
+
groupCellContext(item) {
|
|
3598
|
+
return {
|
|
3599
|
+
$implicit: {
|
|
3600
|
+
data: [...item.rows],
|
|
3601
|
+
key: item.value,
|
|
3602
|
+
value: item.value,
|
|
3603
|
+
displayValue: item.text,
|
|
3604
|
+
// What the grid would have rendered, suffix included, so a template can decorate
|
|
3605
|
+
// the default rather than having to rebuild it.
|
|
3606
|
+
text: `${item.column.caption()}: ${item.text}${item.captionSuffix}`,
|
|
3607
|
+
groupIndex: item.level,
|
|
3608
|
+
columnIndex: this.columns().indexOf(item.column),
|
|
3609
|
+
column: { dataField: item.column.dataField(), caption: item.column.caption() },
|
|
3610
|
+
component: this.facade,
|
|
3611
|
+
},
|
|
3612
|
+
};
|
|
3613
|
+
}
|
|
3614
|
+
/**
|
|
3615
|
+
* dx `onCellPrepared` / `onRowPrepared`.
|
|
3616
|
+
*
|
|
3617
|
+
* Emitted from a render hook rather than an effect, because the payload carries the real
|
|
3618
|
+
* `<td>`: it cannot exist before the DOM does. `afterRenderEffect` is the sanctioned
|
|
3619
|
+
* primitive for that here, and the walk runs inside `untracked()` so a handler that happens
|
|
3620
|
+
* to read a signal cannot enrol it in this hook's dependency set — the invisible-dependency
|
|
3621
|
+
* failure the `effect()` ban exists to prevent.
|
|
3622
|
+
*/
|
|
3623
|
+
emitPreparedEvents() {
|
|
3624
|
+
if (!this.onCellPrepared.observed && !this.onRowPrepared.observed) {
|
|
3625
|
+
return;
|
|
3626
|
+
}
|
|
3627
|
+
// A handler that writes a signal schedules another render, and therefore another
|
|
3628
|
+
// emission. dx re-fires on repaint too and the merge helpers are idempotent, but a
|
|
3629
|
+
// pathological handler should fail safe rather than hang the tab.
|
|
3630
|
+
if (this.preparedDepth > 0) {
|
|
3631
|
+
return;
|
|
3632
|
+
}
|
|
3633
|
+
const host = this.elementRef.nativeElement;
|
|
3634
|
+
const table = host.querySelector('.dx-datagrid-content > .dx-datagrid-table');
|
|
3635
|
+
if (!table) {
|
|
3636
|
+
return;
|
|
3637
|
+
}
|
|
3638
|
+
// Own rows only: a nested master-detail grid renders its own rows deeper inside a cell,
|
|
3639
|
+
// and section.parentElement === table is what separates them.
|
|
3640
|
+
const rows = Array.from(table.querySelectorAll('tr')).filter((tr) => tr.parentElement?.parentElement === table);
|
|
3641
|
+
const cols = this.visibleColumns();
|
|
3642
|
+
const dataItems = this.renderRows().filter((r) => r.kind === 'data');
|
|
3643
|
+
const groupItems = this.renderRows().filter((r) => r.kind === 'group');
|
|
3644
|
+
this.preparedDepth++;
|
|
3645
|
+
try {
|
|
3646
|
+
let dataSeen = 0;
|
|
3647
|
+
let groupSeen = 0;
|
|
3648
|
+
for (const tr of rows) {
|
|
3649
|
+
const rowType = nxeRowTypeOf(tr);
|
|
3650
|
+
if (!rowType) {
|
|
3651
|
+
continue;
|
|
3652
|
+
}
|
|
3653
|
+
let info;
|
|
3654
|
+
if (rowType === 'data') {
|
|
3655
|
+
const item = dataItems[dataSeen++];
|
|
3656
|
+
info = {
|
|
3657
|
+
rowType,
|
|
3658
|
+
data: item?.row,
|
|
3659
|
+
key: item ? this.rowKey(item.row) : undefined,
|
|
3660
|
+
rowIndex: item?.rowIndex ?? -1,
|
|
3661
|
+
isSelected: item ? this.isSelected(item.row) : false,
|
|
3662
|
+
};
|
|
3663
|
+
}
|
|
3664
|
+
else if (rowType === 'group') {
|
|
3665
|
+
const item = groupItems[groupSeen++];
|
|
3666
|
+
info = {
|
|
3667
|
+
rowType,
|
|
3668
|
+
data: item?.rows,
|
|
3669
|
+
key: item?.value,
|
|
3670
|
+
rowIndex: -1,
|
|
3671
|
+
// dx handlers read one name or the other depending on vintage; supply both.
|
|
3672
|
+
groupIndex: item?.level,
|
|
3673
|
+
groupLevel: item?.level,
|
|
3674
|
+
isExpanded: item ? !item.collapsed : undefined,
|
|
3675
|
+
};
|
|
3676
|
+
}
|
|
3677
|
+
else {
|
|
3678
|
+
info = { rowType, data: undefined, key: undefined, rowIndex: -1 };
|
|
3679
|
+
}
|
|
3680
|
+
if (this.onRowPrepared.observed) {
|
|
3681
|
+
this.onRowPrepared.emit({
|
|
3682
|
+
...info,
|
|
3683
|
+
rowElement: tr,
|
|
3684
|
+
columnCount: tr.cells.length,
|
|
3685
|
+
component: this.facade,
|
|
3686
|
+
element: host,
|
|
3687
|
+
});
|
|
3688
|
+
}
|
|
3689
|
+
if (!this.onCellPrepared.observed) {
|
|
3690
|
+
continue;
|
|
3691
|
+
}
|
|
3692
|
+
let colSeen = 0;
|
|
3693
|
+
for (const td of Array.from(tr.cells)) {
|
|
3694
|
+
const isCommand = td.classList.contains('dx-command-expand') ||
|
|
3695
|
+
td.classList.contains('dx-command-select') ||
|
|
3696
|
+
td.classList.contains('dx-command-edit') ||
|
|
3697
|
+
td.classList.contains('dx-group-cell') ||
|
|
3698
|
+
td.classList.contains('nxe-band-cell');
|
|
3699
|
+
const col = isCommand ? undefined : cols[colSeen];
|
|
3700
|
+
const columnIndex = isCommand ? -1 : colSeen;
|
|
3701
|
+
if (!isCommand) {
|
|
3702
|
+
colSeen++;
|
|
3703
|
+
}
|
|
3704
|
+
const value = col && rowType === 'data' && info.data
|
|
3705
|
+
? this.getRaw(info.data, col.dataField())
|
|
3706
|
+
: undefined;
|
|
3707
|
+
this.onCellPrepared.emit({
|
|
3708
|
+
data: info.data,
|
|
3709
|
+
key: info.key,
|
|
3710
|
+
value,
|
|
3711
|
+
displayValue: value,
|
|
3712
|
+
text: td.textContent ?? '',
|
|
3713
|
+
columnIndex,
|
|
3714
|
+
rowIndex: info.rowIndex,
|
|
3715
|
+
column: col
|
|
3716
|
+
? {
|
|
3717
|
+
dataField: col.dataField(),
|
|
3718
|
+
caption: col.caption(),
|
|
3719
|
+
dataType: col.dataType(),
|
|
3720
|
+
index: columnIndex,
|
|
3721
|
+
}
|
|
3722
|
+
: null,
|
|
3723
|
+
row: info,
|
|
3724
|
+
rowType,
|
|
3725
|
+
cellElement: td,
|
|
3726
|
+
component: this.facade,
|
|
3727
|
+
element: host,
|
|
3728
|
+
});
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
finally {
|
|
3733
|
+
this.preparedDepth--;
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
3736
|
+
// --- Interaction -----------------------------------------------------------------------------
|
|
3737
|
+
/** True when this header can start a drag gesture (to the group panel and/or a reorder). */
|
|
3738
|
+
headerDraggable(col) {
|
|
3739
|
+
if (this.treeMode || !col.dataField()) {
|
|
3740
|
+
return false;
|
|
3741
|
+
}
|
|
3742
|
+
// Reordering is off for banded grids: dragging a leaf out of its band has no meaning in
|
|
3743
|
+
// dx, and letting it happen here would tear the band apart. Grouping still works, and a
|
|
3744
|
+
// band whose leaves are all grouped away simply disappears.
|
|
3745
|
+
return ((this.groupPanelVisible() && col.allowGrouping()) ||
|
|
3746
|
+
(this.allowColumnReordering() && !this.hasBands()));
|
|
3747
|
+
}
|
|
3748
|
+
onHeaderPointerDown(col, ci, event) {
|
|
3749
|
+
if (this.headerDraggable(col)) {
|
|
3750
|
+
this.drag.startHeaderDrag(col, ci, event);
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3753
|
+
onChipPointerDown(col, level, event) {
|
|
3754
|
+
this.drag.startChipDrag(col, level, event);
|
|
3755
|
+
}
|
|
3756
|
+
/** Chip click (dx): toggle the grouped column's sort — unless this click ends a drag. */
|
|
3757
|
+
onChipClick(col) {
|
|
3758
|
+
if (this.drag.consumeClick()) {
|
|
3759
|
+
return;
|
|
3760
|
+
}
|
|
3761
|
+
this.sortGroupColumn(col);
|
|
3762
|
+
}
|
|
3763
|
+
onHeaderClick(col) {
|
|
3764
|
+
if (this.drag.consumeClick() || this.resize.consumeClick()) {
|
|
3765
|
+
return; // this click is the tail of a drag/resize gesture, not a sort request
|
|
3766
|
+
}
|
|
3767
|
+
if (!col.allowSorting() || this.sorting()?.mode === 'none') {
|
|
3768
|
+
return;
|
|
3769
|
+
}
|
|
3770
|
+
const field = col.dataField();
|
|
3771
|
+
if (!field) {
|
|
3772
|
+
return;
|
|
3773
|
+
}
|
|
3774
|
+
this.sortInit.set(true);
|
|
3775
|
+
const current = this.effectiveSort();
|
|
3776
|
+
if (!current || current.field !== field) {
|
|
3777
|
+
this.sort.set({ field, dir: 'asc' });
|
|
3778
|
+
}
|
|
3779
|
+
else if (current.dir === 'asc') {
|
|
3780
|
+
this.sort.set({ field, dir: 'desc' });
|
|
3781
|
+
}
|
|
3782
|
+
else {
|
|
3783
|
+
this.sort.set(null);
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
sortDir(col) {
|
|
3787
|
+
const s = this.effectiveSort();
|
|
3788
|
+
return s && s.field === col.dataField() ? s.dir : null;
|
|
3789
|
+
}
|
|
3790
|
+
onRowEnter(index) {
|
|
3791
|
+
if (this.hoverStateEnabled()) {
|
|
3792
|
+
this.hoveredIndex.set(index);
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
onRowLeave() {
|
|
3796
|
+
this.hoveredIndex.set(null);
|
|
3797
|
+
}
|
|
3798
|
+
isHovered(index) {
|
|
3799
|
+
return this.hoveredIndex() === index;
|
|
3800
|
+
}
|
|
3801
|
+
isSelected(row) {
|
|
3802
|
+
return this.treeMode
|
|
3803
|
+
? this.selectedRowKeys().includes(this.rowKey(row))
|
|
3804
|
+
: this.selectedSet().has(row);
|
|
3805
|
+
}
|
|
3806
|
+
/** Row body click: single-select (LOV pickers) or toggle when checkboxes are hidden. */
|
|
3807
|
+
onDataRowClick(row, rowIndex = 0) {
|
|
3808
|
+
this.onRowClick.emit({ data: row, rowIndex, component: this.facade });
|
|
3809
|
+
if (!this.selectionEnabled()) {
|
|
3810
|
+
return;
|
|
3811
|
+
}
|
|
3812
|
+
if (this.selectionMode() === 'multiple' &&
|
|
3813
|
+
this.showCheckboxes() &&
|
|
3814
|
+
!this.selection()?.selectByClick()) {
|
|
3815
|
+
return; // checkbox-driven; row click is a no-op
|
|
3816
|
+
}
|
|
3817
|
+
this.toggleRow(row);
|
|
3818
|
+
}
|
|
3819
|
+
onCheckboxClick(row, event) {
|
|
3820
|
+
event.stopPropagation();
|
|
3821
|
+
this.toggleRow(row);
|
|
3822
|
+
}
|
|
3823
|
+
toggleRow(row) {
|
|
3824
|
+
if (this.treeMode) {
|
|
3825
|
+
this.toggleTreeSelection(row);
|
|
3826
|
+
return;
|
|
3827
|
+
}
|
|
3828
|
+
const set = new Set(this.selectedSet());
|
|
3829
|
+
let currentSelected = [];
|
|
3830
|
+
let currentDeselected = [];
|
|
3831
|
+
if (this.selectionMode() === 'single') {
|
|
3832
|
+
currentDeselected = [...set].filter((r) => r !== row);
|
|
3833
|
+
set.clear();
|
|
3834
|
+
set.add(row);
|
|
3835
|
+
currentSelected = [row];
|
|
3836
|
+
}
|
|
3837
|
+
else if (set.has(row)) {
|
|
3838
|
+
set.delete(row);
|
|
3839
|
+
currentDeselected = [row];
|
|
3840
|
+
}
|
|
3841
|
+
else {
|
|
3842
|
+
set.add(row);
|
|
3843
|
+
currentSelected = [row];
|
|
3844
|
+
}
|
|
3845
|
+
this.selectedSet.set(set);
|
|
3846
|
+
this.emitSelectionChanged(currentSelected, currentDeselected);
|
|
3847
|
+
}
|
|
3848
|
+
toggleSelectAll(event) {
|
|
3849
|
+
event.stopPropagation();
|
|
3850
|
+
if (this.treeMode) {
|
|
3851
|
+
const allKeys = (this.dataSource() ?? []).map((r) => this.rowKey(r));
|
|
3852
|
+
if (this.allSelected()) {
|
|
3853
|
+
this.selectedRowKeys.set([]);
|
|
3854
|
+
this.emitTreeSelectionChanged([], [], allKeys);
|
|
3855
|
+
}
|
|
3856
|
+
else {
|
|
3857
|
+
this.selectedRowKeys.set(allKeys);
|
|
3858
|
+
this.emitTreeSelectionChanged(allKeys, allKeys, []);
|
|
3859
|
+
}
|
|
3860
|
+
return;
|
|
3861
|
+
}
|
|
3862
|
+
const rows = this.displayRows();
|
|
3863
|
+
if (this.allSelected()) {
|
|
3864
|
+
this.selectedSet.set(new Set());
|
|
3865
|
+
this.emitSelectionChanged([], rows);
|
|
3866
|
+
}
|
|
3867
|
+
else {
|
|
3868
|
+
this.selectedSet.set(new Set(rows));
|
|
3869
|
+
this.emitSelectionChanged(rows, []);
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3872
|
+
emitSelectionChanged(currentSelected, currentDeselected) {
|
|
3873
|
+
const data = this.selectedRowsData();
|
|
3874
|
+
this.onSelectionChanged.emit({
|
|
3875
|
+
selectedRowsData: data,
|
|
3876
|
+
selectedRowKeys: this.rowKeys(data),
|
|
3877
|
+
currentSelectedRowKeys: this.rowKeys(currentSelected),
|
|
3878
|
+
currentDeselectedRowKeys: this.rowKeys(currentDeselected),
|
|
3879
|
+
component: this.facade,
|
|
3880
|
+
element: this.elementRef.nativeElement,
|
|
3881
|
+
});
|
|
3882
|
+
}
|
|
3883
|
+
rowKeys(rows) {
|
|
3884
|
+
const key = this.keyExpr();
|
|
3885
|
+
return key ? rows.map((r) => r?.[key]) : rows;
|
|
3886
|
+
}
|
|
3887
|
+
// --- Filter row (dxo-filter-row): typed editors + dx operation-chooser menu ------------------
|
|
3888
|
+
static STRING_OPS = [
|
|
3889
|
+
{ op: 'contains', label: 'Contains' },
|
|
3890
|
+
{ op: 'notcontains', label: 'Does not contain' },
|
|
3891
|
+
{ op: 'startswith', label: 'Starts with' },
|
|
3892
|
+
{ op: 'endswith', label: 'Ends with' },
|
|
3893
|
+
{ op: '=', label: 'Equals' },
|
|
3894
|
+
{ op: '<>', label: 'Does not equal' },
|
|
3895
|
+
];
|
|
3896
|
+
static SCALAR_OPS = [
|
|
3897
|
+
{ op: '=', label: 'Equals' },
|
|
3898
|
+
{ op: '<>', label: 'Does not equal' },
|
|
3899
|
+
{ op: '<', label: 'Less than' },
|
|
3900
|
+
{ op: '>', label: 'Greater than' },
|
|
3901
|
+
{ op: '<=', label: 'Less than or equal to' },
|
|
3902
|
+
{ op: '>=', label: 'Greater than or equal to' },
|
|
3903
|
+
{ op: 'between', label: 'Between' },
|
|
3904
|
+
];
|
|
3905
|
+
static OP_ICON_SLUGS = {
|
|
3906
|
+
contains: 'contains',
|
|
3907
|
+
notcontains: 'not-contains',
|
|
3908
|
+
startswith: 'starts-with',
|
|
3909
|
+
endswith: 'ends-with',
|
|
3910
|
+
'=': 'equals',
|
|
3911
|
+
'<>': 'not-equals',
|
|
3912
|
+
'<': 'less',
|
|
3913
|
+
'<=': 'less-equal',
|
|
3914
|
+
'>': 'greater',
|
|
3915
|
+
'>=': 'greater-equal',
|
|
3916
|
+
between: 'between',
|
|
3917
|
+
};
|
|
3918
|
+
frMenuColumn = signal(null, /* @ts-ignore */
|
|
3919
|
+
...(ngDevMode ? [{ debugName: "frMenuColumn" }] : /* istanbul ignore next */ []));
|
|
3920
|
+
frMenuOverlayRef;
|
|
3921
|
+
frMenuPanel = viewChild('frOpMenu', /* @ts-ignore */
|
|
3922
|
+
...(ngDevMode ? [{ debugName: "frMenuPanel" }] : /* istanbul ignore next */ []));
|
|
3923
|
+
/** Which editor a filter cell renders (mirrors dx: select for lookup/boolean, typed inputs). */
|
|
3924
|
+
frEditorKind(col) {
|
|
3925
|
+
if (col.lookup()) {
|
|
3926
|
+
return 'lookup';
|
|
3927
|
+
}
|
|
3928
|
+
const type = col.dataType();
|
|
3929
|
+
return type === 'boolean' || type === 'date' || type === 'number' ? type : 'text';
|
|
3930
|
+
}
|
|
3931
|
+
/** dx shows the operation-chooser magnifier only for typed (non-select) editors. */
|
|
3932
|
+
frHasOpMenu(col) {
|
|
3933
|
+
const kind = this.frEditorKind(col);
|
|
3934
|
+
return kind !== 'lookup' && kind !== 'boolean';
|
|
3935
|
+
}
|
|
3936
|
+
frDefaultOp(col) {
|
|
3937
|
+
return this.frEditorKind(col) === 'text' ? 'contains' : '=';
|
|
3938
|
+
}
|
|
3939
|
+
frOp(col) {
|
|
3940
|
+
return this.filterRowState().get(col.dataField())?.op ?? this.frDefaultOp(col);
|
|
3941
|
+
}
|
|
3942
|
+
frText(col) {
|
|
3943
|
+
return this.filterRowState().get(col.dataField())?.text ?? '';
|
|
3944
|
+
}
|
|
3945
|
+
frText2(col) {
|
|
3946
|
+
return this.filterRowState().get(col.dataField())?.text2 ?? '';
|
|
3947
|
+
}
|
|
3948
|
+
frOpIconClass(col) {
|
|
3949
|
+
return `dx-icon-filter-operation-${NxeDataGridComponent.OP_ICON_SLUGS[this.frOp(col)]}`;
|
|
3950
|
+
}
|
|
3951
|
+
opIconSlug(op) {
|
|
3952
|
+
return NxeDataGridComponent.OP_ICON_SLUGS[op];
|
|
3953
|
+
}
|
|
3954
|
+
setFilterRowText(col, text, second = false) {
|
|
3955
|
+
const field = col.dataField();
|
|
3956
|
+
const next = new Map(this.filterRowState());
|
|
3957
|
+
const prev = next.get(field) ?? { op: this.frDefaultOp(col), text: '' };
|
|
3958
|
+
next.set(field, second ? { ...prev, text2: text } : { ...prev, text });
|
|
3959
|
+
this.filterRowState.set(next);
|
|
3960
|
+
}
|
|
3961
|
+
openFilterOpMenu(col, event) {
|
|
3962
|
+
event.stopPropagation();
|
|
3963
|
+
this.frMenuColumn.set(col);
|
|
3964
|
+
this.frMenuOverlayRef?.dispose();
|
|
3965
|
+
const origin = event.currentTarget;
|
|
3966
|
+
this.frMenuOverlayRef = this.overlay.create({
|
|
3967
|
+
positionStrategy: this.overlay
|
|
3968
|
+
.position()
|
|
3969
|
+
.flexibleConnectedTo(origin)
|
|
3970
|
+
.withPositions([
|
|
3971
|
+
{ originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top' },
|
|
3972
|
+
{ originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom' },
|
|
3973
|
+
]),
|
|
3974
|
+
scrollStrategy: this.overlay.scrollStrategies.reposition(),
|
|
3975
|
+
hasBackdrop: true,
|
|
3976
|
+
backdropClass: 'cdk-overlay-transparent-backdrop',
|
|
3977
|
+
panelClass: nxePane('nxe-fr-op-pane'),
|
|
3978
|
+
});
|
|
3979
|
+
this.frMenuOverlayRef.backdropClick().subscribe(() => this.closeFilterOpMenu());
|
|
3980
|
+
const panel = this.frMenuPanel();
|
|
3981
|
+
if (panel) {
|
|
3982
|
+
this.frMenuOverlayRef.attach(new TemplatePortal(panel, this.viewContainerRef));
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
frMenuOps() {
|
|
3986
|
+
const col = this.frMenuColumn();
|
|
3987
|
+
if (!col) {
|
|
3988
|
+
return [];
|
|
3989
|
+
}
|
|
3990
|
+
return this.frEditorKind(col) === 'text'
|
|
3991
|
+
? NxeDataGridComponent.STRING_OPS
|
|
3992
|
+
: NxeDataGridComponent.SCALAR_OPS;
|
|
3993
|
+
}
|
|
3994
|
+
selectFilterOp(op) {
|
|
3995
|
+
const col = this.frMenuColumn();
|
|
3996
|
+
if (col) {
|
|
3997
|
+
const field = col.dataField();
|
|
3998
|
+
const next = new Map(this.filterRowState());
|
|
3999
|
+
const prev = next.get(field) ?? { op: this.frDefaultOp(col), text: '' };
|
|
4000
|
+
next.set(field, { ...prev, op, ...(op !== 'between' ? { text2: undefined } : {}) });
|
|
4001
|
+
this.filterRowState.set(next);
|
|
4002
|
+
}
|
|
4003
|
+
this.closeFilterOpMenu();
|
|
4004
|
+
}
|
|
4005
|
+
/** dx Reset menu item: restore the default operation and clear the editor. */
|
|
4006
|
+
resetFilterRow() {
|
|
4007
|
+
const col = this.frMenuColumn();
|
|
4008
|
+
if (col) {
|
|
4009
|
+
const next = new Map(this.filterRowState());
|
|
4010
|
+
next.delete(col.dataField());
|
|
4011
|
+
this.filterRowState.set(next);
|
|
4012
|
+
}
|
|
4013
|
+
this.closeFilterOpMenu();
|
|
4014
|
+
}
|
|
4015
|
+
closeFilterOpMenu() {
|
|
4016
|
+
this.frMenuOverlayRef?.detach();
|
|
4017
|
+
this.frMenuColumn.set(null);
|
|
4018
|
+
}
|
|
4019
|
+
// --- Header filter (dxo-header-filter) -------------------------------------------------------
|
|
4020
|
+
headerFilterEnabled = computed(() => this.headerFilter()?.visible() === true || this.headerFilterOptions()?.visible === true, /* @ts-ignore */
|
|
4021
|
+
...(ngDevMode ? [{ debugName: "headerFilterEnabled" }] : /* istanbul ignore next */ []));
|
|
4022
|
+
hasHeaderFilter(col) {
|
|
4023
|
+
return this.effectiveHeaderFilters().has(col.dataField());
|
|
4024
|
+
}
|
|
4025
|
+
/** Whether this column shows a funnel — the grid has header filtering on AND the column opts in.
|
|
4026
|
+
Named because the header cell asks three times (the caption's indicator class, the wrapper's
|
|
4027
|
+
existence, the funnel itself) and the group-panel chip asks again. */
|
|
4028
|
+
headerFilterVisible(col) {
|
|
4029
|
+
return this.headerFilterEnabled() && col.allowHeaderFiltering();
|
|
4030
|
+
}
|
|
4031
|
+
/**
|
|
4032
|
+
* Normalizes a raw cell value to its header-filter key: blank-ish → the `(Blanks)` sentinel,
|
|
4033
|
+
* date columns → a `yyyy-MM-dd` day key (the tree's leaf unit), else the raw value.
|
|
4034
|
+
*/
|
|
4035
|
+
hfKey(col, raw) {
|
|
4036
|
+
return nxeHfKey(raw, col.dataType() === 'date');
|
|
4037
|
+
}
|
|
4038
|
+
/** dx does NOT cascade header-filter lists for client arrays (bright audit V6). */
|
|
4039
|
+
hfBaseRows() {
|
|
4040
|
+
return (this.dataSource() ?? []);
|
|
4041
|
+
}
|
|
4042
|
+
hfBoolLabel(col, raw) {
|
|
4043
|
+
if (col.lookup()) {
|
|
4044
|
+
return null;
|
|
4045
|
+
}
|
|
4046
|
+
if (raw === true) {
|
|
4047
|
+
return 'Yes';
|
|
4048
|
+
}
|
|
4049
|
+
if (raw === false || col.dataType() === 'boolean') {
|
|
4050
|
+
return 'No';
|
|
4051
|
+
}
|
|
4052
|
+
return null;
|
|
4053
|
+
}
|
|
4054
|
+
/** (Select All) checkbox state — spec-pinned alias of the model's computed
|
|
4055
|
+
((Select All) operates on the VISIBLE keys: dx scopes it to the search). */
|
|
4056
|
+
hfAllChecked = this.hf.allChecked;
|
|
4057
|
+
/** Default true — see NxeHeaderFilterComponent.searchEnabled. The two branches have to agree,
|
|
4058
|
+
since a grid can be configured either by the content child or by the object input. */
|
|
4059
|
+
hfSearchEnabled() {
|
|
4060
|
+
return (this.headerFilter()?.searchEnabled() ??
|
|
4061
|
+
this.headerFilterOptions()?.search?.enabled ??
|
|
4062
|
+
true);
|
|
4063
|
+
}
|
|
4064
|
+
openHeaderFilter(col, event) {
|
|
4065
|
+
event.stopPropagation();
|
|
4066
|
+
if (this.drag.consumeClick()) {
|
|
4067
|
+
return; // the funnel was merely the landing spot of an aborted drag
|
|
4068
|
+
}
|
|
4069
|
+
// No applied filter → everything checked; else start from the applied set.
|
|
4070
|
+
this.hf.open(col, this.effectiveHeaderFilters().get(col.dataField()));
|
|
4071
|
+
this.hfOverlayRef?.dispose();
|
|
4072
|
+
this.hfOverlayRef = openNxeHeaderFilterOverlay({
|
|
4073
|
+
overlay: this.overlay,
|
|
4074
|
+
viewContainerRef: this.viewContainerRef,
|
|
4075
|
+
origin: event.currentTarget,
|
|
4076
|
+
model: this.hf,
|
|
4077
|
+
searchEnabled: () => this.hfSearchEnabled(),
|
|
4078
|
+
onApply: () => this.hfApply(),
|
|
4079
|
+
onClose: () => this.closeHeaderFilter(),
|
|
4080
|
+
});
|
|
4081
|
+
}
|
|
4082
|
+
hfApply() {
|
|
4083
|
+
const col = this.hf.column();
|
|
4084
|
+
if (col) {
|
|
4085
|
+
const next = new Map(this.hfOverride());
|
|
4086
|
+
// Everything checked (or nothing to choose from) = explicit "no filter" (`null` so a
|
|
4087
|
+
// declarative seed can't resurrect); else the allowed-key set (empty = empty grid).
|
|
4088
|
+
next.set(col.dataField(), this.hf.applyResult());
|
|
4089
|
+
this.hfOverride.set(next);
|
|
4090
|
+
}
|
|
4091
|
+
this.closeHeaderFilter();
|
|
4092
|
+
}
|
|
4093
|
+
closeHeaderFilter() {
|
|
4094
|
+
this.hfOverlayRef?.detach();
|
|
4095
|
+
this.hf.close();
|
|
4096
|
+
}
|
|
4097
|
+
setColumnVisible(field, visible) {
|
|
4098
|
+
this.visibilityOverride.update((m) => {
|
|
4099
|
+
const next = new Map(m);
|
|
4100
|
+
next.set(field, visible);
|
|
4101
|
+
return next;
|
|
4102
|
+
});
|
|
4103
|
+
}
|
|
4104
|
+
compare(a, b) {
|
|
4105
|
+
if (a == null && b == null)
|
|
4106
|
+
return 0;
|
|
4107
|
+
if (a == null)
|
|
4108
|
+
return -1;
|
|
4109
|
+
if (b == null)
|
|
4110
|
+
return 1;
|
|
4111
|
+
if (typeof a === 'number' && typeof b === 'number')
|
|
4112
|
+
return a - b;
|
|
4113
|
+
const da = parseDateValue(a);
|
|
4114
|
+
const db = parseDateValue(b);
|
|
4115
|
+
if (da && db)
|
|
4116
|
+
return da.toMillis() - db.toMillis();
|
|
4117
|
+
return String(a).localeCompare(String(b));
|
|
4118
|
+
}
|
|
4119
|
+
ngOnDestroy() {
|
|
4120
|
+
this.drag.destroy();
|
|
4121
|
+
this.resize.destroy();
|
|
4122
|
+
this.hfOverlayRef?.dispose();
|
|
4123
|
+
this.frMenuOverlayRef?.dispose();
|
|
4124
|
+
this.resizeObserver?.disconnect();
|
|
4125
|
+
}
|
|
4126
|
+
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 });
|
|
4128
|
+
}
|
|
4129
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDataGridComponent, decorators: [{
|
|
4130
|
+
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: [{
|
|
4133
|
+
type: Output
|
|
4134
|
+
}], onRowPrepared: [{
|
|
4135
|
+
type: Output
|
|
4136
|
+
}], onRowExpanding: [{ type: i0.Output, args: ["onRowExpanding"] }], onRowExpanded: [{ type: i0.Output, args: ["onRowExpanded"] }], frMenuPanel: [{ type: i0.ViewChild, args: ['frOpMenu', { isSignal: true }] }] } });
|
|
4137
|
+
|
|
4138
|
+
// @nexterajs/ui/grid — public API.
|
|
4139
|
+
|
|
4140
|
+
/**
|
|
4141
|
+
* Generated bundle index. Do not edit.
|
|
4142
|
+
*/
|
|
4143
|
+
|
|
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 };
|
|
4145
|
+
//# sourceMappingURL=nexterajs-ui-grid.mjs.map
|