@one-grid-core/angular 0.1.0-beta.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +49 -4
  2. package/esm2022/lib/components/one-grid/one-grid.component.mjs +67 -16
  3. package/esm2022/lib/components/one-grid-body/one-grid-body.component.mjs +282 -30
  4. package/esm2022/lib/components/one-grid-cell-group-renderer/one-grid-cell-group-renderer.component.mjs +10 -7
  5. package/esm2022/lib/components/one-grid-cell-option-menu/one-grid-cell-option-menu.component.mjs +60 -4
  6. package/esm2022/lib/components/one-grid-checkbox-header/one-grid-checkbox-header.component.mjs +14 -5
  7. package/esm2022/lib/components/one-grid-header/one-grid-header.component.mjs +50 -13
  8. package/esm2022/lib/components/one-grid-pagination/one-grid-pagination.component.mjs +39 -15
  9. package/esm2022/lib/components/one-grid-row-action/one-grid-row-action.component.mjs +54 -8
  10. package/esm2022/lib/components/one-grid-row-drag-handle/one-grid-row-drag-handle.component.mjs +110 -0
  11. package/esm2022/lib/core/directives/one-cell-renderer-component.directive.mjs +9 -2
  12. package/esm2022/lib/core/directives/one-cell-text-expand.directive.mjs +33 -51
  13. package/esm2022/lib/core/instants/one-grid-api.class.mjs +429 -166
  14. package/esm2022/lib/core/instants/one-row-node.class.mjs +103 -37
  15. package/esm2022/lib/core/models/one-column-def-types.model.mjs +1 -1
  16. package/esm2022/lib/core/models/one-event-types.model.mjs +1 -1
  17. package/esm2022/lib/core/models/one-grid-types.model.mjs +1 -1
  18. package/esm2022/lib/core/models/one-row-drag.model.mjs +2 -0
  19. package/esm2022/lib/core/models/one-row.model.mjs +1 -1
  20. package/esm2022/lib/core/models/one-tree-data.model.mjs +1 -1
  21. package/esm2022/lib/core/models/one-validation.model.mjs +1 -1
  22. package/esm2022/lib/core/pipes/cell-field-value-empty.pipe.mjs +8 -3
  23. package/esm2022/lib/core/pipes/column-sort-state.pipe.mjs +3 -1
  24. package/esm2022/lib/core/services/og-column.service.mjs +120 -13
  25. package/esm2022/lib/core/services/one-grid-cell.service.mjs +123 -20
  26. package/esm2022/lib/core/services/one-grid-row-drag.service.mjs +351 -0
  27. package/esm2022/lib/core/services/one-grid-selection.service.mjs +19 -6
  28. package/esm2022/lib/helpers/one-column-def.helper.mjs +27 -2
  29. package/esm2022/lib/helpers/one-filter.helper.mjs +71 -12
  30. package/esm2022/lib/helpers/one-validation.helper.mjs +7 -2
  31. package/esm2022/lib/index.mjs +6 -1
  32. package/esm2022/lib/one-grid.module.mjs +7 -16
  33. package/fesm2022/one-grid-core-angular.mjs +1964 -421
  34. package/fesm2022/one-grid-core-angular.mjs.map +1 -1
  35. package/lib/components/one-grid/one-grid.component.d.ts +16 -1
  36. package/lib/components/one-grid-body/one-grid-body.component.d.ts +132 -5
  37. package/lib/components/one-grid-cell-group-renderer/one-grid-cell-group-renderer.component.d.ts +6 -1
  38. package/lib/components/one-grid-cell-option-menu/one-grid-cell-option-menu.component.d.ts +19 -2
  39. package/lib/components/one-grid-checkbox-header/one-grid-checkbox-header.component.d.ts +5 -2
  40. package/lib/components/one-grid-header/one-grid-header.component.d.ts +15 -7
  41. package/lib/components/one-grid-pagination/one-grid-pagination.component.d.ts +24 -5
  42. package/lib/components/one-grid-row-action/one-grid-row-action.component.d.ts +44 -0
  43. package/lib/components/one-grid-row-drag-handle/one-grid-row-drag-handle.component.d.ts +39 -0
  44. package/lib/core/directives/one-cell-text-expand.directive.d.ts +21 -9
  45. package/lib/core/instants/one-grid-api.class.d.ts +212 -26
  46. package/lib/core/instants/one-row-node.class.d.ts +67 -14
  47. package/lib/core/models/one-column-def-types.model.d.ts +89 -15
  48. package/lib/core/models/one-event-types.model.d.ts +24 -1
  49. package/lib/core/models/one-grid-types.model.d.ts +36 -1
  50. package/lib/core/models/one-row-drag.model.d.ts +44 -0
  51. package/lib/core/models/one-row.model.d.ts +0 -5
  52. package/lib/core/models/one-tree-data.model.d.ts +13 -0
  53. package/lib/core/models/one-validation.model.d.ts +12 -2
  54. package/lib/core/pipes/column-sort-state.pipe.d.ts +1 -1
  55. package/lib/core/services/og-column.service.d.ts +44 -3
  56. package/lib/core/services/one-grid-cell.service.d.ts +52 -4
  57. package/lib/core/services/one-grid-row-drag.service.d.ts +113 -0
  58. package/lib/core/services/one-grid-selection.service.d.ts +9 -0
  59. package/lib/helpers/one-column-def.helper.d.ts +19 -1
  60. package/lib/helpers/one-filter.helper.d.ts +29 -4
  61. package/lib/index.d.ts +5 -0
  62. package/lib/one-grid.module.d.ts +17 -19
  63. package/package.json +1 -2
  64. package/scss/_one-grid.scss +389 -17
  65. package/esm2022/lib/core/pipes/cell-disable-resolver.pipe.mjs +0 -26
  66. package/lib/core/pipes/cell-disable-resolver.pipe.d.ts +0 -9
@@ -1,20 +1,35 @@
1
1
  import { v4 } from 'uuid';
2
2
  import * as i0 from '@angular/core';
3
- import { Injectable, Pipe, Input, Component, EventEmitter, Output, HostListener, Directive, ViewChild, ChangeDetectionStrategy, NgModule } from '@angular/core';
4
- import { BehaviorSubject, Subject, Subscription, fromEvent } from 'rxjs';
3
+ import { Injectable, Pipe, inject, NgZone, Input, Component, ViewChild, EventEmitter, Output, HostListener, Directive, ViewChildren, ChangeDetectionStrategy, NgModule } from '@angular/core';
4
+ import { BehaviorSubject, Subject, Subscription } from 'rxjs';
5
5
  import _ from 'lodash';
6
6
  import { filter, map } from 'rxjs/operators';
7
7
  import * as i2 from '@angular/common';
8
8
  import { CommonModule } from '@angular/common';
9
- import * as i3 from 'lucide-angular';
9
+ import * as i2$1 from 'lucide-angular';
10
10
  import { LucideAngularModule, icons } from 'lucide-angular';
11
- import * as i5 from '@angular/cdk/scrolling';
11
+ import * as i6 from '@angular/cdk/scrolling';
12
12
  import { CdkVirtualScrollViewport, ScrollingModule } from '@angular/cdk/scrolling';
13
- import * as i3$1 from '@angular/forms';
13
+ import * as i3 from '@angular/forms';
14
14
  import { FormsModule, ReactiveFormsModule } from '@angular/forms';
15
- import { NgbTypeahead, NgbHighlight } from '@ng-bootstrap/ng-bootstrap';
16
- import { CdkDragPlaceholder } from '@angular/cdk/drag-drop';
17
15
 
16
+ /** Width used when a column defines neither `width` nor `minWidth`. */
17
+ const ONE_DEFAULT_COLUMN_WIDTH = 150;
18
+ /**
19
+ * The rendered width of a column, clamped to its bounds.
20
+ *
21
+ * `width`, `minWidth` and `maxWidth` are all optional, and the previous inline
22
+ * expression — repeated in two places — fell back to `minWidth` when `width`
23
+ * was absent. With neither set that produced `undefined`, and summing it gave
24
+ * a `NaN` row width, so the row collapsed. This is total: every combination of
25
+ * present and absent bounds resolves to a usable number.
26
+ */
27
+ function resolveColumnWidth(column) {
28
+ const min = column?.minWidth ?? 0;
29
+ const max = column?.maxWidth ?? Number.POSITIVE_INFINITY;
30
+ const width = column?.width ?? column?.minWidth ?? ONE_DEFAULT_COLUMN_WIDTH;
31
+ return Math.min(Math.max(width, min), max);
32
+ }
18
33
  /**
19
34
  * Apply Default Column Definitions
20
35
  */
@@ -27,7 +42,15 @@ function applyDefaultColumnDef(columnDefs, defaultColumnDef) {
27
42
  /**
28
43
  * One Column Params Resolver (Cell Renderer)
29
44
  */
30
- function resolveOneCellRendererParams(prop, params) {
45
+ function resolveOneCellRendererParams(prop, params,
46
+ /**
47
+ * What an unset property means: `false` for a capability you opt into,
48
+ * `true` for a restriction you opt out of. `!!prop` could not tell the two
49
+ * apart, which left `updatable` — a restriction — defaulting to "deny".
50
+ */
51
+ whenUnset = false) {
52
+ if (prop === undefined || prop === null)
53
+ return whenUnset;
31
54
  return typeof prop === 'function' ? prop(params) : !!prop;
32
55
  }
33
56
  /**
@@ -117,7 +140,12 @@ function getCellError(column, node, value, row) {
117
140
  if (!column?.validationRules || column?.validationRules?.length === 0)
118
141
  return null;
119
142
  if (column?.autoGroupField) {
120
- if (column.editableLevels.includes(node.level)) {
143
+ // `editableLevels` is optional, and this read was unguarded — a tree grid
144
+ // with validation rules on its group column threw on the first cell it
145
+ // rendered. Undeclared means no level is validated, matching the property's
146
+ // meaning elsewhere: it names the levels that are editable, and a level not
147
+ // named is not one of them.
148
+ if (column.editableLevels?.includes(node.level)) {
121
149
  for (const ruleFn of column?.validationRules) {
122
150
  const error = ruleFn(value, row);
123
151
  if (error)
@@ -138,35 +166,77 @@ function getCellError(column, node, value, row) {
138
166
  }
139
167
  }
140
168
 
169
+ /**
170
+ * The grid's wrapper around one row of your data.
171
+ *
172
+ * A node owns three kinds of state: its identity and position, the structural
173
+ * facts the grid derived while building the tree, and the per-cell UI state.
174
+ * `data` is your object, held by reference and mutated in place when a cell is
175
+ * committed — the node never copies it.
176
+ *
177
+ * @typeParam T - the row type
178
+ * @typeParam D - the detail row type, for master/detail grids
179
+ */
141
180
  class OneRowNode {
142
181
  constructor(rowId, data, rowIndex) {
143
- this.rowId = null;
144
- this.rowIndex = null;
145
- this.data = null;
146
- this.level = null;
182
+ /** Depth in the tree. 0 at the root; always 0 outside tree data. */
183
+ this.level = 0;
184
+ // ---------------------------------------------------------------------------
185
+ // Structure, derived by the grid while building rows
186
+ // ---------------------------------------------------------------------------
187
+ /** Has no children of its own. */
147
188
  this.leaf = false;
189
+ /** Is a group, but an empty one. */
148
190
  this.leafGroup = false;
191
+ /** Has children. */
192
+ this.group = false;
193
+ /** Can be opened — a non-empty group, or any row in a master/detail grid. */
194
+ this.expandable = false;
195
+ /** Survives the active filters. Recomputed on every filter pass. */
149
196
  this.visible = false;
150
197
  this.selected = false;
151
- this.expandable = false;
152
198
  this.expanded = false;
153
- this.group = false;
154
- this.submitted = false;
155
199
  this.parent = null;
156
200
  this.children = null;
201
+ // ---------------------------------------------------------------------------
202
+ // Per-cell UI state, keyed by column field
203
+ // ---------------------------------------------------------------------------
204
+ /** Which cells currently have an open editor. */
157
205
  this.editing = {};
206
+ /** Rendered value and kind for each cell, computed when the row is built. */
158
207
  this.cellStates = {};
159
208
  this.cellOptions = {};
209
+ // ---------------------------------------------------------------------------
210
+ // Master / detail
211
+ // ---------------------------------------------------------------------------
212
+ /** Detail rows for this master row. `null` until they have been supplied. */
160
213
  this.detailRowData = null;
214
+ /** Whether the custom detail renderer is showing. Toggled by the row action. */
161
215
  this.detailActive = false;
216
+ /** Height reserved for the detail area, computed from the detail row count. */
162
217
  this.detailRowHeight = null;
218
+ /**
219
+ * Listeners per event. Typed as a payload-agnostic callback because the map
220
+ * is heterogeneous; `on` and `emit` restore the per-event type at the edges.
221
+ */
163
222
  this.listeners = new Map();
164
223
  this.rowId = rowId;
165
224
  this.data = data;
166
225
  this.rowIndex = rowIndex;
167
226
  }
227
+ // ---------------------------------------------------------------------------
228
+ // Mutators
229
+ // ---------------------------------------------------------------------------
230
+ /**
231
+ * Write one field of the row and announce it.
232
+ *
233
+ * `null` is allowed because that is what the grid itself stores for a cleared
234
+ * cell; anything else must match the field's declared type.
235
+ */
168
236
  setDataValue(field, value) {
237
+ const previousData = { ...this.data };
169
238
  this.data[field] = value;
239
+ this.emit('rowDataChanged', { node: this, data: this.data, previousData });
170
240
  }
171
241
  setSelected(selected) {
172
242
  this.selected = selected;
@@ -177,54 +247,78 @@ class OneRowNode {
177
247
  setCellStates(cellStates) {
178
248
  this.cellStates = cellStates;
179
249
  }
250
+ /** @deprecated Read `node.selected`. */
180
251
  isSelected() {
181
252
  return this.selected;
182
253
  }
254
+ /** @deprecated Read `node.expanded`. */
183
255
  isExpanded() {
184
256
  return this.expanded;
185
257
  }
258
+ /** Supply or replace this master row's detail rows. */
259
+ setDetailRowData(detailRowData) {
260
+ const previousDetailRowData = this.detailRowData;
261
+ this.detailRowData = detailRowData;
262
+ this.emit('detailRowDataChanged', { node: this, detailRowData, previousDetailRowData });
263
+ }
264
+ setExpanded(expanded) {
265
+ if (this.expanded === expanded)
266
+ return; // no event for a no-op
267
+ this.expanded = expanded;
268
+ this.emit('expandedChanged', { node: this, expanded });
269
+ }
270
+ // ---------------------------------------------------------------------------
271
+ // Events
272
+ // ---------------------------------------------------------------------------
186
273
  /**
187
- * Event System
274
+ * Subscribe to one of this node's events.
275
+ *
276
+ * @returns an unsubscribe function. Call it, or `destroy()`, to release the
277
+ * listener — a node outlives any single render.
188
278
  */
189
279
  on(event, listener) {
190
- if (!this.listeners.has(event)) {
191
- this.listeners.set(event, new Set());
280
+ let listeners = this.listeners.get(event);
281
+ if (!listeners) {
282
+ listeners = new Set();
283
+ this.listeners.set(event, listeners);
192
284
  }
193
- this.listeners.get(event).add(listener);
194
- // Return unsubscribe fn
285
+ listeners.add(listener);
195
286
  return () => this.off(event, listener);
196
287
  }
197
288
  off(event, listener) {
198
289
  this.listeners.get(event)?.delete(listener);
199
290
  }
291
+ /**
292
+ * Notify listeners. Private on purpose: every event this class publishes is
293
+ * raised by the mutator that caused it, and a forged `expandedChanged` would
294
+ * leave the node's flag and the grid's view of it disagreeing.
295
+ */
200
296
  emit(event, payload) {
201
- this.listeners.get(event)?.forEach(cb => cb(payload));
297
+ const listeners = this.listeners.get(event);
298
+ if (!listeners?.size)
299
+ return;
300
+ // Snapshot: a listener is allowed to unsubscribe itself, or another, while
301
+ // the event is being delivered.
302
+ for (const listener of Array.from(listeners)) {
303
+ listener(payload);
304
+ }
202
305
  }
203
306
  /**
204
- * Mutators that fire events
205
- */
206
- setDetailRowData(detailRowData) {
207
- const previousDetailRowData = this.detailRowData;
208
- this.detailRowData = detailRowData;
209
- this.emit('detailRowDataChanged', {
210
- node: this,
211
- detailRowData,
212
- previousDetailRowData,
213
- });
214
- this.emit('childrenChanged', {
215
- node: this,
216
- children: this.children,
217
- source: 'masterDetail',
218
- });
219
- }
220
- setExpanded(expanded) {
221
- if (this.expanded === expanded)
222
- return; // skip if no change
223
- this.expanded = expanded;
224
- this.emit('expandedChanged', { node: this, expanded });
225
- }
307
+ * Release everything this node holds. Called by the grid when the row leaves
308
+ * the data or the grid is torn down.
309
+ *
310
+ * The tree links matter as much as the listeners: a parent holding children
311
+ * that hold the parent keeps the whole subtree — and its data — reachable.
312
+ */
226
313
  destroy() {
227
- this.listeners.clear(); // prevent memory leaks
314
+ this.listeners.clear();
315
+ this.parent = null;
316
+ this.children = null;
317
+ this.detailRowData = null;
318
+ this.detailRendererComponent = undefined;
319
+ this.editing = {};
320
+ this.cellStates = {};
321
+ this.cellOptions = {};
228
322
  }
229
323
  }
230
324
 
@@ -233,21 +327,40 @@ class OneRowNode {
233
327
  *
234
328
  * Rules:
235
329
  * - A node is visible if it matches the filters or has a matching descendant.
236
- * - A node is expanded if it has a matching descendant.
330
+ * - While a filter is active, a node is expanded iff it has a matching
331
+ * descendant, so the hit is revealed.
332
+ *
333
+ * Overwriting `expanded` is only correct *while filtering*. This runs from
334
+ * `applyFilters`, which every `setRowData` / `updateRowData` calls, so doing it
335
+ * unconditionally force-expanded every parent on each data update: it made
336
+ * `groupDefaultExpanded` inert for tree data and collapsed expanded
337
+ * master-detail rows (they have no children, so `childMatch` is false).
237
338
  *
238
339
  * @returns True if the current node or any descendant matches.
239
340
  */
240
- function filterTreeNode(node, filters, quick) {
341
+ function filterTreeNode(node, filters, quick, fields) {
342
+ const filtering = isFilterActive(filters, quick);
241
343
  // Check whether the current node matches all active filters.
242
- const selfMatch = matchesAdvancedFilters(node, filters) && matchesQuickFilter(node, quick);
344
+ const selfMatch = matchesAdvancedFilters(node, filters) && matchesQuickFilter(node, quick, fields);
243
345
  // Recursively determine whether any child matches.
244
- const childMatch = node.children?.some(child => filterTreeNode(child, filters, quick)) ?? false;
346
+ const childMatch = node.children?.some(child => filterTreeNode(child, filters, quick, fields)) ?? false;
245
347
  // Keep the node visible when it or any descendant matches.
246
348
  node.visible = selfMatch || childMatch;
247
- // Expand parents to reveal matching descendants.
248
- node.expanded = childMatch;
349
+ if (filtering) {
350
+ // Expand parents to reveal matching descendants.
351
+ node.expanded = childMatch;
352
+ }
249
353
  return node.visible;
250
354
  }
355
+ /**
356
+ * Whether anything is actually filtering right now. A model entry with no
357
+ * conditions matches everything, so it does not count as active.
358
+ */
359
+ function isFilterActive(filters, quick) {
360
+ if (quick)
361
+ return true;
362
+ return !!filters?.some(filter => filter?.conditions?.length > 0);
363
+ }
251
364
  /**
252
365
  * Apply advanced per-column filters
253
366
  */
@@ -263,8 +376,8 @@ function matchesAdvancedFilters(node, filters) {
263
376
  case 'contains': return val.includes(cmp);
264
377
  case 'startsWith': return val.startsWith(cmp);
265
378
  case 'endsWith': return val.endsWith(cmp);
266
- case 'greaterThan': return +val > +cmp;
267
- case 'lessThan': return +val < +cmp;
379
+ case 'greaterThan': return compare(val, cmp) > 0;
380
+ case 'lessThan': return compare(val, cmp) < 0;
268
381
  default: return true; // Unknown operator = ignore
269
382
  }
270
383
  });
@@ -272,12 +385,52 @@ function matchesAdvancedFilters(node, filters) {
272
385
  });
273
386
  }
274
387
  /**
275
- * Apply quick filter across all fields
388
+ * Order two already-lowercased cell values.
389
+ *
390
+ * Numeric when both sides parse as numbers, lexicographic otherwise. This used
391
+ * to be `+val > +cmp` unconditionally, so anything non-numeric compared NaN
392
+ * against NaN — always false. A date column filtered with `greaterThan` matched
393
+ * nothing at all rather than reporting a problem, and ISO dates happen to sort
394
+ * correctly as strings, which is what the fallback gives them.
395
+ */
396
+ function compare(val, cmp) {
397
+ const left = Number(val);
398
+ const right = Number(cmp);
399
+ const numeric = val !== '' && cmp !== '' && !Number.isNaN(left) && !Number.isNaN(right);
400
+ if (numeric)
401
+ return left === right ? 0 : left < right ? -1 : 1;
402
+ return val === cmp ? 0 : val < cmp ? -1 : 1;
403
+ }
404
+ /**
405
+ * Apply the quick filter to one row.
406
+ *
407
+ * Searches the fields the grid actually shows, when it is told what they are.
408
+ * It used to walk `Object.values(node.data)`, which had three consequences, all
409
+ * of them wrong:
410
+ *
411
+ * - a property with no column was searchable, so a user could match rows on
412
+ * data the grid never displayed;
413
+ * - a nested object stringified to `[object Object]`, so typing `object`
414
+ * matched every row holding one;
415
+ * - in a tree, `children` did the same, so most parents matched most queries.
416
+ *
417
+ * Non-primitive values are skipped regardless, so the `[object Object]` haystack
418
+ * cannot come back through a declared column either.
276
419
  */
277
- function matchesQuickFilter(node, quick) {
420
+ function matchesQuickFilter(node, quick, fields) {
278
421
  if (!quick)
279
422
  return true;
280
- return Object.values(node.data).some(value => (value ?? '').toString().toLowerCase().includes(quick));
423
+ const data = node.data;
424
+ const values = fields?.length ? fields.map(field => data?.[field]) : Object.values(data ?? {});
425
+ return values.some(value => {
426
+ if (value === null || value === undefined)
427
+ return false;
428
+ // Objects, arrays and functions have no useful text form. Skipping them is
429
+ // what keeps `[object Object]` out of the search.
430
+ if (typeof value === 'object' || typeof value === 'function')
431
+ return false;
432
+ return value.toString().toLowerCase().includes(quick);
433
+ });
281
434
  }
282
435
 
283
436
  /**
@@ -325,11 +478,17 @@ function resolveCellClass(column, row) {
325
478
  class OgColumnService {
326
479
  constructor() {
327
480
  this.defaultColumnDef = null;
481
+ /**
482
+ * The grid's `context`, so header callbacks receive the same one cell
483
+ * callbacks do. Set during init; null in a bare service.
484
+ */
485
+ this.context = null;
328
486
  this.columnDefs$ = new BehaviorSubject([]);
329
487
  this.groupColumnDefs$ = new BehaviorSubject([]);
330
488
  }
331
- initColumnDefs(defaultColumnDef, columnDefs, groupColumnDefs) {
489
+ initColumnDefs(defaultColumnDef, columnDefs, groupColumnDefs, context = null) {
332
490
  this.defaultColumnDef = defaultColumnDef;
491
+ this.context = context;
333
492
  this.setGroupColumnDefs(groupColumnDefs);
334
493
  this.setColumnDefs(columnDefs);
335
494
  }
@@ -339,7 +498,7 @@ class OgColumnService {
339
498
  return { columns, groups };
340
499
  }
341
500
  setGroupColumnDefs(groupColumnDefs) {
342
- const groups = groupColumnDefs.map((group) => this.resolveGroupPresentation(group));
501
+ const groups = groupColumnDefs.map((group) => this.resolveGroupPresentation(group, { context: this.context }));
343
502
  this.groupColumnDefs$.next(groups);
344
503
  }
345
504
  setColumnDefs(columnDefs) {
@@ -349,24 +508,86 @@ class OgColumnService {
349
508
  ...col,
350
509
  id: col?.field ? col.field : _.uniqueId('col_'),
351
510
  };
352
- return this.resolveCellPresentation(column);
511
+ return this.resolveHeaderPresentation(column);
353
512
  });
354
- this.recalculateGroups(columns);
355
- this.columnDefs$.next(columns);
513
+ const ordered = this.applyPinning(columns);
514
+ this.recalculateGroups(ordered);
515
+ this.columnDefs$.next(ordered);
516
+ }
517
+ /**
518
+ * Move pinned columns to their edge and measure how far in each one sits.
519
+ *
520
+ * Display order becomes left-pinned, unpinned, right-pinned, with declared
521
+ * order preserved inside each band. `_pinnedOffset` is the distance from that
522
+ * column's own edge — the sum of the widths of the pinned columns outside it
523
+ * — which is what the sticky `left` / `right` binding needs.
524
+ *
525
+ * With nothing pinned this returns the same array, in the same order, so a
526
+ * grid that does not use the feature is unaffected.
527
+ */
528
+ applyPinning(columns) {
529
+ const left = columns.filter(column => column.pinned === 'left');
530
+ const right = columns.filter(column => column.pinned === 'right');
531
+ if (!left.length && !right.length) {
532
+ for (const column of columns) {
533
+ column._pinnedOffset = null;
534
+ column._pinnedEdge = false;
535
+ }
536
+ return columns;
537
+ }
538
+ const center = columns.filter(column => column.pinned !== 'left' && column.pinned !== 'right');
539
+ let offset = 0;
540
+ for (const column of left) {
541
+ column._pinnedOffset = offset;
542
+ column._pinnedEdge = false;
543
+ offset += resolveColumnWidth(column);
544
+ }
545
+ // Right-pinned columns are measured from the right edge inwards, so the
546
+ // last declared one sits flush and the earlier ones stack outside it.
547
+ offset = 0;
548
+ for (let index = right.length - 1; index >= 0; index--) {
549
+ right[index]._pinnedOffset = offset;
550
+ right[index]._pinnedEdge = false;
551
+ offset += resolveColumnWidth(right[index]);
552
+ }
553
+ for (const column of center) {
554
+ column._pinnedOffset = null;
555
+ column._pinnedEdge = false;
556
+ }
557
+ // Only the innermost column of each band carries the divider, so stacked
558
+ // pinned columns do not each cast a shadow onto the next.
559
+ if (left.length)
560
+ left[left.length - 1]._pinnedEdge = true;
561
+ if (right.length)
562
+ right[0]._pinnedEdge = true;
563
+ return [...left, ...center, ...right];
564
+ }
565
+ /** Total width of the columns frozen against the given edge. */
566
+ getPinnedWidth(side) {
567
+ return this.columnDefs$.getValue()
568
+ .filter(column => column.pinned === side)
569
+ .reduce((total, column) => total + resolveColumnWidth(column), 0);
356
570
  }
357
571
  // Update single column
358
572
  updateColumnDef(id, changes) {
359
573
  const columns = this.getColumnState().columns;
360
574
  const updatedColumns = columns.map(col => col.id === id ? { ...col, ...changes } : col);
361
- this.recalculateGroups(updatedColumns);
362
- this.columnDefs$.next(updatedColumns);
575
+ // Re-pinned, not just re-grouped: resizing a pinned column changes where
576
+ // the ones inside it have to sit, and this is the path a column resize
577
+ // takes.
578
+ const ordered = this.applyPinning(updatedColumns);
579
+ this.recalculateGroups(ordered);
580
+ this.columnDefs$.next(ordered);
363
581
  }
364
582
  // Reorder columns
365
583
  reorderColumnDefs(fromIndex, toIndex) {
366
584
  const columns = _.clone(this.getColumnState().columns);
367
585
  const [moved] = columns.splice(fromIndex, 1);
368
586
  columns.splice(toIndex, 0, moved);
369
- this.columnDefs$.next(columns);
587
+ // Dragging can drop a column between two pinned ones; re-pinning puts the
588
+ // bands back in order rather than leaving them interleaved, which would
589
+ // make the computed offsets meaningless.
590
+ this.columnDefs$.next(this.applyPinning(columns));
370
591
  }
371
592
  /**
372
593
  * Group Header Columns
@@ -375,7 +596,7 @@ class OgColumnService {
375
596
  if (this.groupColumnDefs$.getValue().length > 0) {
376
597
  const groups = this.groupColumnDefs$.value.map(group => ({
377
598
  ...group,
378
- width: columnDefs.filter(c => c.groupId === group.id).reduce((sum, col) => sum + (col.width ? col.width > col.maxWidth ? col.maxWidth : col.width < col.minWidth ? col.minWidth : col.width : col.minWidth), 0)
599
+ width: columnDefs.filter(c => c.groupId === group.id).reduce((sum, col) => sum + resolveColumnWidth(col), 0)
379
600
  }));
380
601
  this.groupColumnDefs$.next(groups);
381
602
  }
@@ -385,13 +606,51 @@ class OgColumnService {
385
606
  col._resolvedGroupStyle = typeof col.groupStyle === 'function' ? col.groupStyle(params) : col.groupStyle;
386
607
  return col;
387
608
  }
388
- resolveCellPresentation(col, params) {
609
+ /**
610
+ * Resolve a column's header presentation. Once per column, when the columns
611
+ * are set — a header is one cell for the whole column, so there is no row to
612
+ * resolve it against and nothing to redo per render.
613
+ *
614
+ * This used to resolve the cell hooks too, from the same call, with no params
615
+ * at all. Two bugs came out of that: any callback touching `params` threw a
616
+ * TypeError at init, and the header's resolved value was then overwritten by
617
+ * every cell that rendered, since the body re-ran the same method per cell
618
+ * with row params. In dev mode the disagreement surfaced as NG0100.
619
+ */
620
+ resolveHeaderPresentation(col) {
621
+ const params = {
622
+ columnDef: col,
623
+ field: col.field,
624
+ context: this.context,
625
+ };
389
626
  col._resolvedHeaderClass = typeof col.headerClass === 'function' ? col.headerClass(params) : col.headerClass;
390
627
  col._resolvedHeaderStyle = typeof col.headerStyle === 'function' ? col.headerStyle(params) : col.headerStyle;
391
- col._resolvedCellClass = typeof col.cellClass === 'function' ? col.cellClass(params) : col.cellClass;
392
- col._resolvedCellStyle = typeof col.cellStyle === 'function' ? col.cellStyle(params) : col.cellStyle;
628
+ // The header's own predicates. Declared as `boolean | ((params) => boolean)`
629
+ // and read straight off the column in the template, where a function is
630
+ // simply truthy — so `sortable: () => false` rendered a sort button that a
631
+ // consumer had explicitly asked not to have. Resolved here to a real
632
+ // boolean, once, because none of them depends on a row.
633
+ col._resolvedSortable = resolveOneCellRendererParams(col.sortable, params);
634
+ col._resolvedResizable = resolveOneCellRendererParams(col.resizable, params);
635
+ col._resolvedDragable = resolveOneCellRendererParams(col.dragable, params);
636
+ col._resolvedSuppressHeaderMenuButton = resolveOneCellRendererParams(col.suppressHeaderMenuButton, params);
393
637
  return col;
394
638
  }
639
+ /**
640
+ * Resolve one cell's class, for one row.
641
+ *
642
+ * Pure: the result is returned rather than written back onto the column def,
643
+ * which every row of that column shares. Writing to it worked only because
644
+ * the template resolved and read in the same expression — one render order
645
+ * away from every row showing the last row's class.
646
+ */
647
+ resolveCellClass(col, params) {
648
+ return typeof col?.cellClass === 'function' ? col.cellClass(params) : col?.cellClass;
649
+ }
650
+ /** Resolve one cell's inline style, for one row. Pure, as above. */
651
+ resolveCellStyle(col, params) {
652
+ return typeof col?.cellStyle === 'function' ? col.cellStyle(params) : col?.cellStyle;
653
+ }
395
654
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OgColumnService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
396
655
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OgColumnService }); }
397
656
  }
@@ -427,10 +686,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
427
686
 
428
687
  class CellFieldValueEmptyPipe {
429
688
  transform(data, field) {
430
- if (!data) {
689
+ // `field` is optional on OneColumnDef — an action-only column legitimately
690
+ // has none. This used to call `field.split('.')` unguarded and throw a
691
+ // TypeError while the grid was building cell state, taking the whole grid
692
+ // down. No field means nothing to read, which is empty by definition.
693
+ if (!data || !field) {
431
694
  return true;
432
695
  }
433
- const value = field.split('.').reduce((obj, key) => obj?.[key], data);
696
+ const value = getFieldValue(data, field);
434
697
  return value === null || value === undefined || value === '';
435
698
  }
436
699
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CellFieldValueEmptyPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
@@ -492,19 +755,69 @@ class OneGridCellService {
492
755
  this._cellValueGetter = new CellValueGetterPipe();
493
756
  this._cellFieldValue = new CellFieldValuePipe();
494
757
  this._columnDefs = [];
758
+ this._api = null;
759
+ // ---------------------------------------------------------------------------
760
+ // Row-scoped column predicates
761
+ //
762
+ // Every one of these resolves the same way and receives the same params, via
763
+ // `cellParams`. They used to each build their own subset inline, which is how
764
+ // `value`, `field` and `api` came to be missing from all of them.
765
+ // ---------------------------------------------------------------------------
766
+ /** Whether this cell may be edited at all. A capability: unset means no. */
767
+ this.editableFn = (column, node) => resolveOneCellRendererParams(column?.editable, this.cellParams(column, node));
495
768
  /**
496
- * One Column Def Params Resolver
769
+ * The second edit gate, ANDed with `editable`.
770
+ *
771
+ * A restriction, so unset means "no restriction". It used to resolve through
772
+ * `!!undefined` to `false`, closing the gate on every column that did not name
773
+ * it — so `editable: true` on its own made nothing editable.
497
774
  */
498
- this.editableFn = (column, node) => resolveOneCellRendererParams(column.editable, { node, data: node.data, columnDef: column, rowIndex: node.rowIndex, context: this._context });
499
- this.updatableFn = (column, node) => resolveOneCellRendererParams(column.updatable, { node, data: node.data, columnDef: column, rowIndex: node.rowIndex, context: this._context });
500
- this.disableFn = (column, node) => resolveOneCellRendererParams(column.disable, { node, data: node.data, columnDef: column, rowIndex: node.rowIndex, context: this._context });
501
- this.dragableFn = (column, node) => resolveOneCellRendererParams(column.dragable, { node, data: node.data, columnDef: column, rowIndex: node.rowIndex, context: this._context });
502
- this.valueGetterFn = (column, node) => resolveOneValueGetterParams(column.valueGetter, { data: node.data, rowIndex: node.rowIndex, node: node, context: this._context });
775
+ this.updatableFn = (column, node) => resolveOneCellRendererParams(column?.updatable, this.cellParams(column, node), true);
776
+ /** Disables this column's row action buttons. Not an edit gate. */
777
+ this.disableFn = (column, node) => resolveOneCellRendererParams(column?.disable, this.cellParams(column, node));
778
+ /** Hides this column's row action buttons entirely. */
779
+ this.hideActionButtonFn = (column, node) => resolveOneCellRendererParams(column?.hideActionButton, this.cellParams(column, node));
780
+ /** Whether this cell carries a row drag handle. */
781
+ this.rowDragFn = (column, node) => resolveOneCellRendererParams(column?.rowDrag, this.cellParams(column, node));
782
+ this.valueGetterFn = (column, node) => resolveOneValueGetterParams(column?.valueGetter, { data: node.data, rowIndex: node.rowIndex, node, context: this._context });
503
783
  }
504
784
  applyColumnDefs(columnDefs, context) {
505
785
  this._columnDefs = columnDefs;
506
786
  this._context = context;
507
787
  }
788
+ /**
789
+ * Back-reference to the grid api, so predicate params can carry it.
790
+ *
791
+ * Pushed in rather than injected: `OneGridApi` already depends on this
792
+ * service, and injecting it back would close a DI cycle.
793
+ */
794
+ setGridApi(api) {
795
+ this._api = api;
796
+ }
797
+ /**
798
+ * The params every row-scoped column callback receives.
799
+ *
800
+ * One builder, deliberately: `editable`, `updatable`, `disable`, `rowDrag`,
801
+ * `hideActionButton`, `cellClass`, `cellStyle`, `cellRenderer` and
802
+ * `valueFormatter` are all "decide something about this cell", and a consumer
803
+ * should not have to remember which of them happens to be handed `value` or
804
+ * `field`. Each used to be called with a slightly different subset.
805
+ */
806
+ cellParams(column, node) {
807
+ return {
808
+ // Column config first, so the grid's own fields win a name collision: a
809
+ // stale `value` left in `cellRendererParams` must not shadow this cell's.
810
+ ...(column?.cellRendererParams ?? {}),
811
+ value: this.resolveValue(node, column),
812
+ data: node.data,
813
+ node,
814
+ rowIndex: node.rowIndex,
815
+ field: column?.field,
816
+ columnDef: column,
817
+ api: this._api ?? undefined,
818
+ context: this._context,
819
+ };
820
+ }
508
821
  /**
509
822
  * Generate New Node States
510
823
  */
@@ -514,48 +827,101 @@ class OneGridCellService {
514
827
  const newStates = {};
515
828
  for (const column of this._columnDefs) {
516
829
  const field = column.field;
830
+ // Cell state is keyed by field, so a column without one contributes
831
+ // nothing. Skipping it keeps `undefined` out of the state object; such a
832
+ // column still renders its action buttons, which do not read cell state.
833
+ if (!field)
834
+ continue;
517
835
  if (field !== '_oneRowNumber') {
518
836
  newStates[field] = this.generateCellState(node, column) ?? node.cellStates[field] ?? { type: 'none', value: '' };
519
837
  }
520
838
  else {
521
- newStates[field] = node.cellStates[column.field];
839
+ newStates[field] = node.cellStates[field];
522
840
  }
523
841
  }
524
842
  return newStates;
525
843
  }
844
+ /**
845
+ * The cell's value: the `valueGetter`'s output where the column declares one,
846
+ * the raw field otherwise. This is the value, not the display text — see
847
+ * {@link formatValue}.
848
+ */
849
+ resolveValue(node, column) {
850
+ return column?.valueGetter
851
+ ? this._cellValueGetter.transform(node.data, node, column, this._context)
852
+ : this._cellFieldValue.transform(node.data, column.field);
853
+ }
854
+ /**
855
+ * Apply the column's `valueFormatter`.
856
+ *
857
+ * `valueFormatter` was declared on the column definition and called from
858
+ * nowhere — a consumer could write one, get no error, and see nothing happen.
859
+ * It runs on displayed text only, after the `valueGetter`, and is deliberately
860
+ * skipped for placeholders: an empty cell already has its own mechanism in
861
+ * `cellRendererParams.placeholder`, and two features competing to fill the
862
+ * same gap is worse than one.
863
+ *
864
+ * Note that sorting and filtering read the underlying row data, not this —
865
+ * which is exactly why formatting belongs here rather than in the getter.
866
+ */
867
+ formatValue(node, column, value) {
868
+ if (typeof column?.valueFormatter !== 'function')
869
+ return value;
870
+ // `value` is passed explicitly rather than left to `cellParams`: the empty
871
+ // -cell branch resolves it through the getter before deciding whether there
872
+ // is anything to format, and the formatter must see that same value.
873
+ return column.valueFormatter({ ...this.cellParams(column, node), value });
874
+ }
526
875
  /**
527
876
  * Generate New Node State Based on Column Field
528
877
  */
529
878
  generateCellState(node, column) {
530
- const isField = column.field !== undefined || column.field !== '' || column.field !== null;
531
- const isEmpty = this._cellFieldValueEmpty.transform(node.data, column.field);
879
+ // This was `field !== undefined || field !== '' || field !== null`, which is
880
+ // true for every possible value including all three it meant to exclude —
881
+ // `||` where `&&` was intended. It gated the four branches below, so none
882
+ // of them could ever fall through to the default. Reduced to the check it
883
+ // was reaching for: a column with no field has no value to render.
884
+ const field = column?.field;
885
+ if (!field) {
886
+ return { type: 'none', value: '' };
887
+ }
888
+ const isEmpty = this._cellFieldValueEmpty.transform(node.data, field);
532
889
  const isGroup = node.group || node.leafGroup;
533
890
  const isAutoGroup = column?.autoGroupField;
534
891
  if (column && column.cellRenderer && !isGroup) {
535
- const value = column.cellRenderer({ node, columnDef: column });
536
- return { type: 'html', value: value };
892
+ // The renderer used to be handed `{ node, columnDef }` and nothing else,
893
+ // so the obvious way to write one — `params.value` — produced the string
894
+ // "undefined", and `params.data.x` threw. It now gets exactly what every
895
+ // other row-scoped hook gets.
896
+ return { type: 'html', value: column.cellRenderer(this.cellParams(column, node)) };
537
897
  }
538
898
  // 1. Cell Group Placeholder
539
- if (isField && isEmpty && isGroup && isAutoGroup) {
899
+ if (isEmpty && isGroup && isAutoGroup) {
540
900
  return { type: 'placeholder', value: column?.cellRendererParams?.placeholder ?? this._cellValueGetter.transform(node.data, node, column, this._context) ?? '' };
541
901
  }
542
902
  // 2. Cell Group Text Value
543
- if (isField && isGroup && isAutoGroup) {
544
- const value = column?.valueGetter ? this._cellValueGetter.transform(node.data, node, column, this._context) : this._cellFieldValue.transform(node.data, column.field);
903
+ if (isGroup && isAutoGroup) {
904
+ const value = this.formatValue(node, column, this.resolveValue(node, column));
545
905
  return { type: 'text', value: value ?? '' };
546
906
  }
547
907
  // 3. Cell Placeholder
548
- if (isField && isEmpty && !isGroup && !isAutoGroup) {
908
+ //
909
+ // The auto group column is deliberately NOT excluded here. Cases 1 and 2
910
+ // above already claimed the group rows, so what reaches this point is a
911
+ // leaf — and a leaf on the auto group column has to render its label too,
912
+ // indented by level. Excluding it dropped every leaf through to case 5 and
913
+ // rendered the group column blank for the whole bottom row of a tree.
914
+ if (isEmpty && !isGroup) {
549
915
  const valueGetter = this._cellValueGetter.transform(node.data, node, column, this._context);
550
916
  if (valueGetter) {
551
- return { type: 'text', value: valueGetter };
917
+ return { type: 'text', value: this.formatValue(node, column, valueGetter) };
552
918
  }
553
919
  const placeholder = column?.cellRendererParams?.placeholder ?? '';
554
920
  return { type: 'placeholder', value: placeholder };
555
921
  }
556
- // 4. Cell Text Value
557
- if (isField && !(isGroup || isAutoGroup)) {
558
- const value = column?.valueGetter ? this._cellValueGetter.transform(node.data, node, column, this._context) : this._cellFieldValue.transform(node.data, column.field);
922
+ // 4. Cell Text Value (leaves, including the auto group column — see above)
923
+ if (!isGroup) {
924
+ const value = this.formatValue(node, column, this.resolveValue(node, column));
559
925
  return { type: 'text', value: value ?? '' };
560
926
  }
561
927
  // 5. Default
@@ -637,13 +1003,26 @@ class OneGridSelectionService {
637
1003
  toggleAllSelection(allIds, selected) {
638
1004
  selected ? this._selectedIds = new Set(allIds) : this._selectedIds.clear();
639
1005
  }
640
- // Header Checkbox State
1006
+ /**
1007
+ * Tri-state for the header checkbox, over the rows currently present.
1008
+ *
1009
+ * This compared `_selectedIds.size` with `allIds.length` and never checked
1010
+ * that the selected ids were among the present ones. Selecting two rows and
1011
+ * then swapping in two different rows reported `checked` with nothing
1012
+ * actually selected, because the two counts happened to match. Counting the
1013
+ * intersection is the only thing that answers the question being asked.
1014
+ */
641
1015
  getSelectAllState(allIds) {
642
- if (this._selectedIds.size === 0)
1016
+ if (!allIds?.length)
1017
+ return 'unchecked';
1018
+ let selected = 0;
1019
+ for (const rowId of allIds) {
1020
+ if (this._selectedIds.has(rowId))
1021
+ selected++;
1022
+ }
1023
+ if (selected === 0)
643
1024
  return 'unchecked';
644
- if (this._selectedIds.size === allIds.length)
645
- return 'checked';
646
- return 'indeterminate';
1025
+ return selected === allIds.length ? 'checked' : 'indeterminate';
647
1026
  }
648
1027
  selectAll(rowIds) {
649
1028
  this._selectedIds = new Set(rowIds);
@@ -697,6 +1076,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
697
1076
  }] });
698
1077
 
699
1078
  class OneGridApi {
1079
+ /** Unique per grid instance, assigned during `init`. */
1080
+ get id() {
1081
+ return this._id;
1082
+ }
1083
+ /** The `[context]` the grid was given. */
1084
+ get context() {
1085
+ return this._context;
1086
+ }
1087
+ /** Selection mode, captured during `init`. Changing the input later has no effect. */
1088
+ get rowSelection() {
1089
+ return this._rowSelection;
1090
+ }
1091
+ /** Which role this instance plays: simple, master, detail or tree. */
1092
+ get apiType() {
1093
+ return this._apiType;
1094
+ }
1095
+ /** Every node the grid holds, in flattened order. */
1096
+ get rowNodes() {
1097
+ return this._rowNodes;
1098
+ }
1099
+ /** The sort model currently applied. Change it with `onSortChanged`. */
1100
+ get sortModel() {
1101
+ return this._sortModel;
1102
+ }
700
1103
  constructor(columnService, rowService, cellService, eventService, selectionService, expansionService) {
701
1104
  this.columnService = columnService;
702
1105
  this.rowService = rowService;
@@ -704,17 +1107,18 @@ class OneGridApi {
704
1107
  this.eventService = eventService;
705
1108
  this.selectionService = selectionService;
706
1109
  this.expansionService = expansionService;
707
- this.context = null;
708
- this.rowSelection = null;
709
- this.apiType = null;
710
- this.rowNodes = [];
711
- this.sortModel = [];
1110
+ this._context = null;
1111
+ this._rowSelection = null;
1112
+ this._apiType = null;
1113
+ this._rowNodes = [];
1114
+ this._sortModel = [];
712
1115
  this.rowHeight = 26;
713
1116
  this.headerRowHeight = 32;
714
1117
  this._currentIndex = 0;
715
1118
  this._quickFilterValue = null;
716
1119
  this._activeFilters = [];
717
1120
  this._visibleRowNodes = [];
1121
+ this._pinnedRowNodes = { top: [], bottom: [] };
718
1122
  this._originalRowNodes = new Map();
719
1123
  this._nodeCleanupMap = new Map(); // Cleanup registry (prevents memory leaks)
720
1124
  this._destroyed = false;
@@ -724,63 +1128,83 @@ class OneGridApi {
724
1128
  this.groupDefaultExpanded = 0;
725
1129
  this.masterDetail = false;
726
1130
  }
727
- init(gridApiType, rowSelection, columnDefs, defaultColumnDef, context, masterDetail, detailCellRendererParams, detailRendererComponent, treeData, groupColumnDefs) {
728
- this.id = v4();
729
- this.apiType = gridApiType;
730
- this.rowSelection = rowSelection;
731
- this.context = context;
1131
+ /**
1132
+ * Stand the grid up. Called by `OneGridComponent` from `ngOnInit`.
1133
+ *
1134
+ * Takes a single `OneGridInitOptions` rather than the ten positional
1135
+ * arguments it used to, so a call site reads as the configuration it is and
1136
+ * a new option no longer means editing every caller.
1137
+ *
1138
+ * @internal Applications obtain a ready-made API from `(onGridReady)`; they
1139
+ * should never call this themselves.
1140
+ */
1141
+ init(options) {
1142
+ const { gridType, columnDefs, defaultColumnDef = {}, groupColumnDefs = [], context = null, rowSelection = null, masterDetail = false, detailCellRendererParams = null, detailRendererComponent = null, treeData = false, } = options;
1143
+ this._id = v4();
1144
+ this._apiType = gridType;
1145
+ this._rowSelection = rowSelection;
1146
+ this._context = context;
732
1147
  this.masterDetail = masterDetail;
733
1148
  this.detailCellRendererParams = detailCellRendererParams;
734
1149
  this.detailRendererComponent = detailRendererComponent;
735
1150
  this.treeData = treeData;
736
- this.selectionService.setSelectionMode(this.rowSelection);
737
- this.rowService.setGetRowIdFn(this.context?.componentParent?.getRowId ?? null);
1151
+ this.selectionService.setSelectionMode(rowSelection);
1152
+ this.rowService.setGetRowIdFn(context?.componentParent?.getRowId ?? null);
738
1153
  this.cellService.applyColumnDefs(columnDefs, context);
739
- this.columnService.initColumnDefs(defaultColumnDef, columnDefs, groupColumnDefs);
1154
+ this.cellService.setGridApi(this);
1155
+ this.columnService.initColumnDefs(defaultColumnDef, columnDefs, groupColumnDefs, context);
740
1156
  const columnState = this.columnService.getColumnState();
741
1157
  setTimeout(() => this.eventService.dispatchEvent('columnChanged', columnState));
742
1158
  }
743
1159
  /**
744
- * Create Row Node & Clear All State IDS
1160
+ * Replace every row, discarding selection and expansion.
1161
+ *
1162
+ * Use this when the rows represent something new. To swap the data while
1163
+ * keeping what the user has selected and opened, use `updateRowData`.
745
1164
  */
746
1165
  setRowData(rowData) {
747
- const validRowIds = new Set();
748
- // Clear All State IDS
749
- this.selectionService.deselectAll();
750
- this.expansionService.clear();
751
- // Build New Row Nodes
752
- this.buildRowNodes(rowData, null, 0, validRowIds);
753
- // Apply Filter
754
- this.applyFilters();
755
- // Remove invalid nodes from map
756
- for (const rowId of this._originalRowNodes.keys()) {
757
- if (!validRowIds.has(rowId)) {
758
- this._originalRowNodes.delete(rowId);
759
- }
760
- }
1166
+ this.rebuildRows(rowData, true);
761
1167
  }
762
1168
  /**
763
- * Create Row Nodes & Not Clear State IDS
1169
+ * Replace every row, keeping selection and expansion.
1170
+ *
1171
+ * State is keyed by row id, so this only preserves anything if the grid has a
1172
+ * `context.componentParent.getRowId` — otherwise each rebuild mints new ids
1173
+ * and there is nothing left to match against.
764
1174
  */
765
1175
  updateRowData(rowData) {
1176
+ this.rebuildRows(rowData, false);
1177
+ }
1178
+ /**
1179
+ * The one row-loading path. `setRowData` and `updateRowData` differed only in
1180
+ * whether they cleared the state services first, so everything else — build,
1181
+ * filter, prune — lives here once.
1182
+ */
1183
+ rebuildRows(rowData, resetState) {
1184
+ if (this._destroyed)
1185
+ return;
1186
+ if (resetState) {
1187
+ this.selectionService.deselectAll();
1188
+ this.expansionService.clear();
1189
+ }
766
1190
  const validRowIds = new Set();
767
- // Build New Row Nodes
768
1191
  this.buildRowNodes(rowData, null, 0, validRowIds);
769
- // Apply Filter
1192
+ // Re-apply the active sort. `buildRowNodes` rebuilds the row list in the
1193
+ // order the data arrived, so without this a data update silently returned
1194
+ // the rows to that order while `sortModel` — and therefore the header's
1195
+ // sort arrow — still said they were sorted. Sorting before filtering
1196
+ // because filtering only sets visibility on whatever order this leaves.
1197
+ if (this._sortModel.length > 0)
1198
+ this.applySorting();
770
1199
  this.applyFilters();
771
- // Remove invalid nodes from map
772
- for (const rowId of this._originalRowNodes.keys()) {
773
- if (!validRowIds.has(rowId)) {
774
- this._originalRowNodes.delete(rowId);
775
- }
776
- }
1200
+ this.pruneRemovedNodes(validRowIds);
777
1201
  }
778
1202
  /**
779
1203
  * Recursive Build Row Nodes (Optimized)
780
1204
  */
781
1205
  buildRowNodes(rowData, parent = null, level = 0, validRowIds = new Set(), expandedIds = new Set(this.expansionService.getExpandedIds()), collapsedIds = new Set(this.expansionService.getCollapsedIds()), selectedIds = new Set(this.selectionService.getSelectedIds())) {
782
1206
  if (parent === null) {
783
- this.rowNodes = [];
1207
+ this._rowNodes = [];
784
1208
  this._currentIndex = 0;
785
1209
  }
786
1210
  if (!rowData?.length)
@@ -806,7 +1230,11 @@ class OneGridApi {
806
1230
  }
807
1231
  }
808
1232
  const childrenExist = children && children.length >= 0;
809
- node.group = this.masterDetail ? false : childrenExist;
1233
+ // Coerced, because `childrenExist` is `children && children.length >= 0`
1234
+ // — which is `null` for a row with no children field, not `false`. These
1235
+ // are declared `boolean`, and assigning null made every consumer compare
1236
+ // truthiness instead of the value.
1237
+ node.group = this.masterDetail ? false : !!childrenExist;
810
1238
  node.leaf = this.masterDetail ? true : !node.group;
811
1239
  node.leafGroup = this.masterDetail ? false : (node.group && !childrenExist);
812
1240
  node.expandable = this.masterDetail ? true : (node.group && !node.leafGroup);
@@ -814,11 +1242,11 @@ class OneGridApi {
814
1242
  node.setExpanded(expandedIds.has(rowId) ? true : collapsedIds.has(rowId) ? false : defaultExpanded);
815
1243
  node.setSelected(selectedIds.has(rowId));
816
1244
  node.setCellStates(this.cellService.generateCellStates(node));
817
- if (this.masterDetail && this.apiType === 'master') {
818
- this.getDetailRowData({ data, node: node, successCallback: (detailRowData) => node.setDetailRowData(detailRowData) });
1245
+ if (this.masterDetail && this._apiType === 'master') {
1246
+ this._getDetailRowData({ data, node: node, successCallback: (detailRowData) => node.setDetailRowData(detailRowData) });
819
1247
  }
820
1248
  this._currentIndex++;
821
- this.rowNodes.push(node);
1249
+ this._rowNodes.push(node);
822
1250
  nodes.push(node);
823
1251
  node.children = this.treeData && childrenExist ? this.buildRowNodes(children || [], node, level + 1, validRowIds, expandedIds, collapsedIds, selectedIds) : [];
824
1252
  }
@@ -830,19 +1258,37 @@ class OneGridApi {
830
1258
  applyFilters() {
831
1259
  const filters = this._activeFilters;
832
1260
  const quick = this._quickFilterValue?.toLowerCase() ?? '';
833
- this.rowNodes.forEach(node => filterTreeNode(node, filters, quick));
1261
+ // The fields the grid actually shows. Without them the quick filter falls
1262
+ // back to every property on the row, which lets a user match on data that
1263
+ // has no column and never appears on screen.
1264
+ const fields = this.quickFilterFields();
1265
+ this._rowNodes.forEach(node => filterTreeNode(node, filters, quick, fields));
834
1266
  this.refreshVisibleNodes();
835
1267
  }
1268
+ /**
1269
+ * Which fields the quick filter searches: every column that has one.
1270
+ *
1271
+ * Empty while the columns are still being registered, in which case the
1272
+ * helper falls back to searching the row — the same behaviour as before, and
1273
+ * the only sensible answer when the grid does not yet know its columns.
1274
+ */
1275
+ quickFilterFields() {
1276
+ return this.columnService
1277
+ .getColumnState()
1278
+ .columns
1279
+ .map(column => column.field)
1280
+ .filter((field) => !!field && field !== '_oneRowNumber');
1281
+ }
836
1282
  /**
837
1283
  * Update Row Nodes Display
838
1284
  */
839
- refreshVisibleNodes() {
1285
+ refreshVisibleNodes(dispatch = true) {
840
1286
  if (this._destroyed)
841
1287
  return;
842
1288
  const visibleNodes = [];
843
1289
  let rowNumber = 1;
844
1290
  const visibilityCache = new Map();
845
- for (const node of this.rowNodes) {
1291
+ for (const node of this._rowNodes) {
846
1292
  const parentVisible = node.parent ? visibilityCache.get(node.parent.rowId) : true;
847
1293
  const isVisible = node.visible && parentVisible && (!node.parent || node.parent.expanded);
848
1294
  visibilityCache.set(node.rowId, isVisible);
@@ -856,7 +1302,132 @@ class OneGridApi {
856
1302
  }
857
1303
  }
858
1304
  this._visibleRowNodes = [...visibleNodes];
859
- this.eventService.dispatchEvent('rowDataChanged', { nodes: visibleNodes });
1305
+ if (dispatch)
1306
+ this.eventService.dispatchEvent('rowDataChanged', { nodes: visibleNodes });
1307
+ }
1308
+ /**
1309
+ * Replace the rows anchored to one edge of the grid.
1310
+ *
1311
+ * Pinned rows are deliberately kept out of `rowNodes` and
1312
+ * `_originalRowNodes`: they are not data. Sorting, filtering, selection,
1313
+ * expansion, pagination and the visible row count all ignore them, which is
1314
+ * the whole point of a summary row. They render with the same column
1315
+ * definitions, so renderers and value getters work as they do everywhere
1316
+ * else.
1317
+ */
1318
+ setPinnedRowData(side, rowData) {
1319
+ if (this._destroyed)
1320
+ return;
1321
+ for (const node of this._pinnedRowNodes[side])
1322
+ node.destroy();
1323
+ this._pinnedRowNodes[side] = (rowData ?? []).map((data, index) => {
1324
+ // Ids are namespaced by side and position. They never collide with a data
1325
+ // row's id, and they are stable across rebuilds, which keeps the rendered
1326
+ // `data-cell-id` attributes stable too.
1327
+ const node = new OneRowNode(`one-pinned-${side}-${index}`, data, index);
1328
+ node.level = 0;
1329
+ node.leaf = true;
1330
+ node.visible = true;
1331
+ node.setCellStates(this.cellService.generateCellStates(node));
1332
+ return node;
1333
+ });
1334
+ this.eventService.dispatchEvent('pinnedRowDataChanged', {
1335
+ side,
1336
+ nodes: this._pinnedRowNodes[side],
1337
+ });
1338
+ }
1339
+ /** The rows anchored to one edge. Read-only; change them with `setPinnedRowData`. */
1340
+ getPinnedRowNodes(side) {
1341
+ return this._pinnedRowNodes[side];
1342
+ }
1343
+ /**
1344
+ * Row reordering
1345
+ * --------------------------------------------------------------------------
1346
+ *
1347
+ * `_originalRowNodes` is the source of truth for row order: its insertion
1348
+ * order is what `applySorting` falls back to when the sort model empties, and
1349
+ * what a rebuild walks. `_rowNodes` is only the current display order. A move
1350
+ * that touched one and not the other would either be undone by the next sort
1351
+ * change or invisible until one happened, so both are rewritten together.
1352
+ */
1353
+ /**
1354
+ * Whether the displayed order is the data order, and so whether a manual move
1355
+ * would survive.
1356
+ *
1357
+ * @returns the reason reordering is unavailable, or `null` when it is fine.
1358
+ */
1359
+ getRowDragSuppressedReason() {
1360
+ if (this.treeData)
1361
+ return 'treeData';
1362
+ if (this._sortModel.length > 0)
1363
+ return 'sorted';
1364
+ if (isFilterActive(this._activeFilters, this._quickFilterValue ?? ''))
1365
+ return 'filtered';
1366
+ return null;
1367
+ }
1368
+ /** Convenience inverse of {@link getRowDragSuppressedReason}. */
1369
+ canReorderRows() {
1370
+ return this.getRowDragSuppressedReason() === null;
1371
+ }
1372
+ /**
1373
+ * Move a row from one position to another.
1374
+ *
1375
+ * Indices are positions in the grid's row order, the same order
1376
+ * `getAllRowNodes()` returns. `targetIndex` is where the row ends up *after*
1377
+ * it has been lifted out of `sourceIndex` — dragging row 0 to the bottom of a
1378
+ * 5-row grid is `moveRow(0, 4)`, not `moveRow(0, 5)`.
1379
+ *
1380
+ * @returns `true` if the rows moved. `false` when the move was rejected —
1381
+ * out-of-range indices, a no-op, or an order the grid does not own
1382
+ * (see {@link getRowDragSuppressedReason}) — so a caller can tell a
1383
+ * refused move from an applied one.
1384
+ */
1385
+ moveRow(sourceIndex, targetIndex) {
1386
+ if (this._destroyed)
1387
+ return false;
1388
+ if (!this.canReorderRows())
1389
+ return false;
1390
+ const order = [...this._originalRowNodes.values()];
1391
+ if (!Number.isInteger(sourceIndex) || !Number.isInteger(targetIndex))
1392
+ return false;
1393
+ if (sourceIndex < 0 || sourceIndex >= order.length)
1394
+ return false;
1395
+ if (targetIndex < 0 || targetIndex >= order.length)
1396
+ return false;
1397
+ if (sourceIndex === targetIndex)
1398
+ return false;
1399
+ const [moved] = order.splice(sourceIndex, 1);
1400
+ order.splice(targetIndex, 0, moved);
1401
+ // Rebuilt rather than mutated: a Map preserves insertion order, and there
1402
+ // is no way to reposition a key within one.
1403
+ this._originalRowNodes = new Map(order.map(node => [node.rowId, node]));
1404
+ this._rowNodes = order;
1405
+ this.reindexRows();
1406
+ this.eventService.dispatchEvent('rowOrderChanged', { nodes: this._visibleRowNodes, node: moved, fromIndex: sourceIndex, toIndex: targetIndex });
1407
+ return true;
1408
+ }
1409
+ /**
1410
+ * Move a row identified by id. Convenient when the caller holds a node rather
1411
+ * than a position — the index it was built with goes stale after any move.
1412
+ */
1413
+ moveRowById(rowId, targetIndex) {
1414
+ const sourceIndex = [...this._originalRowNodes.values()].findIndex(node => node.rowId === rowId);
1415
+ return sourceIndex === -1 ? false : this.moveRow(sourceIndex, targetIndex);
1416
+ }
1417
+ /**
1418
+ * Re-stamp `rowIndex` from the current order.
1419
+ *
1420
+ * Row index is denormalised onto every node, and half the grid reads it —
1421
+ * cell params, the range selection, the detail lookup. Leaving it stale after
1422
+ * a move puts every row below the drop point one out.
1423
+ */
1424
+ reindexRows() {
1425
+ this._rowNodes.forEach((node, index) => node.setRowIndex(index));
1426
+ // Silent: row numbers and the visible list have to be rebuilt, but the
1427
+ // `rowDataChanged` that normally follows defers a frame and re-seeds detail
1428
+ // rows. A keyboard drag fires one of these per arrow key, and `moveRow`
1429
+ // announces itself through `rowOrderChanged` instead.
1430
+ this.refreshVisibleNodes(false);
860
1431
  }
861
1432
  getVisibleRowNodes() {
862
1433
  return this._visibleRowNodes;
@@ -865,17 +1436,19 @@ class OneGridApi {
865
1436
  * Sorting Model Feature
866
1437
  */
867
1438
  onSortChanged(sortModel) {
868
- this.sortModel = sortModel;
1439
+ if (this._destroyed)
1440
+ return;
1441
+ this._sortModel = sortModel;
869
1442
  this.applySorting();
870
- this.eventService.dispatchEvent('sortChanged', { nodes: this.rowNodes });
1443
+ this.eventService.dispatchEvent('sortChanged', { nodes: this._rowNodes });
871
1444
  }
872
1445
  applySorting() {
873
- if (this.sortModel.length === 0) {
874
- this.rowNodes = [...this._originalRowNodes.values()];
1446
+ if (this._sortModel.length === 0) {
1447
+ this._rowNodes = [...this._originalRowNodes.values()];
875
1448
  return;
876
1449
  }
877
- this.rowNodes = [...this._originalRowNodes.values()].sort((a, b) => {
878
- for (const sortItem of this.sortModel) {
1450
+ this._rowNodes = [...this._originalRowNodes.values()].sort((a, b) => {
1451
+ for (const sortItem of this._sortModel) {
879
1452
  const { colId, sort } = sortItem;
880
1453
  const valA = getFieldValue(a.data, colId);
881
1454
  const valB = getFieldValue(b.data, colId);
@@ -888,11 +1461,17 @@ class OneGridApi {
888
1461
  });
889
1462
  }
890
1463
  compareValues(valA, valB, sortDirection) {
891
- if (valA === null && valB === null)
1464
+ // `undefined` has to be treated as empty alongside `null`: getFieldValue
1465
+ // returns undefined for a missing field or an unresolved dotted path, and
1466
+ // comparing undefined with < / > yields false both ways, which made such
1467
+ // rows settle in whatever order the sort happened to encounter them.
1468
+ const aEmpty = valA === null || valA === undefined;
1469
+ const bEmpty = valB === null || valB === undefined;
1470
+ if (aEmpty && bEmpty)
892
1471
  return 0;
893
- if (valA === null)
1472
+ if (aEmpty)
894
1473
  return sortDirection === 'asc' ? 1 : -1;
895
- if (valB === null)
1474
+ if (bEmpty)
896
1475
  return sortDirection === 'asc' ? -1 : 1;
897
1476
  if (typeof valA === 'string' && typeof valB === 'string') {
898
1477
  const comparison = valA.localeCompare(valB, undefined, { numeric: true });
@@ -947,7 +1526,7 @@ class OneGridApi {
947
1526
  const node = this.getRowNodeById(rowId);
948
1527
  if (node) {
949
1528
  if (this.selectionService.getSelectionMode() === 'single') {
950
- this.rowNodes.forEach(rowNode => rowNode.setSelected(rowNode.rowId === node.rowId));
1529
+ this._rowNodes.forEach(rowNode => rowNode.setSelected(rowNode.rowId === node.rowId));
951
1530
  this._originalRowNodes.forEach(rowNode => rowNode.setSelected(rowNode.rowId === node.rowId));
952
1531
  }
953
1532
  else {
@@ -972,13 +1551,13 @@ class OneGridApi {
972
1551
  * Get & Set Functions
973
1552
  */
974
1553
  getAllRowNodes() {
975
- return this.rowNodes?.length > 0 ? [...this.rowNodes] : [];
1554
+ return this._rowNodes?.length > 0 ? [...this._rowNodes] : [];
976
1555
  }
977
1556
  getRowDataSelected() {
978
- return this.rowNodes.filter(node => node.selected).map(node => ({ ...node.data }));
1557
+ return this._rowNodes.filter(node => node.selected).map(node => ({ ...node.data }));
979
1558
  }
980
1559
  getSelectedRowNodes() {
981
- const nodes = this.rowNodes.filter(node => node.selected);
1560
+ const nodes = this._rowNodes.filter(node => node.selected);
982
1561
  return nodes && nodes.length > 0 ? [...nodes] : [];
983
1562
  }
984
1563
  /**
@@ -988,6 +1567,8 @@ class OneGridApi {
988
1567
  return this._activeFilters;
989
1568
  }
990
1569
  setFilterModel(model) {
1570
+ if (this._destroyed)
1571
+ return;
991
1572
  this._activeFilters = model;
992
1573
  this.applyFilters();
993
1574
  }
@@ -998,76 +1579,69 @@ class OneGridApi {
998
1579
  * Quick filter
999
1580
  */
1000
1581
  setQuickFilter(value) {
1582
+ if (this._destroyed)
1583
+ return;
1001
1584
  this._quickFilterValue = (value || '').toLowerCase();
1002
1585
  this.applyFilters();
1003
1586
  }
1004
1587
  /**
1005
- * Validation Feature
1588
+ * Run the column validators over one row.
1589
+ *
1590
+ * Rules run in order and stop at the first failure, so a field appears at
1591
+ * most once. Fields that passed are absent rather than mapped to `null`.
1592
+ *
1593
+ * The row type comes from the grid's own `T` — this used to declare its own
1594
+ * `<T>`, which shadowed the class parameter and let an `OneGridApi<Employee>`
1595
+ * happily validate an unrelated shape against employee columns.
1006
1596
  */
1007
1597
  validateRow(row, columns) {
1008
1598
  const errors = {};
1009
- columns.forEach(col => {
1010
- // Only check columns with validationRule
1011
- if (!col.validationRules || col.validationRules.length === 0)
1012
- return;
1013
- const rules = col.validationRules;
1014
- for (const rule of rules) {
1015
- const error = rule(row[col.field], row);
1599
+ for (const column of columns) {
1600
+ // A column can only contribute an error if it has both rules and a field
1601
+ // to key them under.
1602
+ if (!column.field || !column.validationRules?.length)
1603
+ continue;
1604
+ for (const rule of column.validationRules) {
1605
+ const error = rule(row[column.field], row);
1016
1606
  if (error) {
1017
- errors[col.field] = error;
1018
- break; // stop at first error for this column
1607
+ errors[column.field] = error;
1608
+ break; // first failure per column wins
1019
1609
  }
1020
1610
  }
1021
- });
1611
+ }
1022
1612
  return errors;
1023
1613
  }
1024
1614
  /**
1025
- * Validate all rows and return both errors and a flag indicating if there are any errors
1615
+ * Run the column validators over a whole dataset.
1616
+ *
1617
+ * Delegates to `validateRow` so the two can never disagree about what counts
1618
+ * as invalid — they previously carried separate copies of the same loop.
1026
1619
  */
1027
1620
  validateAllRows(rowData, columns) {
1028
- const allErrors = rowData.map(row => {
1029
- const errors = {};
1030
- columns.forEach(col => {
1031
- if (!col.validationRules || col.validationRules.length === 0)
1032
- return;
1033
- for (const rule of col.validationRules) {
1034
- const error = rule(row[col.field], row);
1035
- if (error) {
1036
- errors[col.field] = error;
1037
- break;
1038
- }
1039
- }
1040
- });
1041
- return errors;
1042
- });
1043
- const hasErrors = allErrors.some(err => Object.keys(err).length > 0);
1621
+ const allErrors = rowData.map(row => this.validateRow(row, columns));
1622
+ const hasErrors = allErrors.some(errors => Object.keys(errors).length > 0);
1044
1623
  return { errors: allErrors, valid: !hasErrors, invalid: hasErrors };
1045
1624
  }
1046
- /** Traverse all nodes */
1625
+ /**
1626
+ * Traverse every node, depth-first, once each.
1627
+ *
1628
+ * `rowNodes` is already the flattened tree — `buildRowNodes` pushes every
1629
+ * node into it as it descends — so walking it is the whole traversal. The
1630
+ * previous implementation walked that flat list *and* recursed into
1631
+ * `children`, visiting every nested node twice and every depth-3 node three
1632
+ * times. `index` is the node's position in the flattened order, matching
1633
+ * `node.rowIndex`, rather than its position among its siblings.
1634
+ */
1047
1635
  forEachNode(callback) {
1048
- const visit = (nodes) => {
1049
- nodes.forEach((node, index) => {
1050
- callback(node, index);
1051
- if (node?.children?.length)
1052
- visit(node.children);
1053
- });
1054
- };
1055
- visit(this.rowNodes);
1636
+ this._rowNodes.forEach((node, index) => callback(node, index));
1056
1637
  }
1057
- /** Traverse only leaf nodes */
1638
+ /** Traverse only leaf nodes, once each, in flattened order. */
1058
1639
  forEachLeafNode(callback) {
1059
1640
  let leafIndex = 0;
1060
- const visit = (nodes) => {
1061
- nodes.forEach(node => {
1062
- if (node.leaf) {
1063
- callback(node, leafIndex++);
1064
- }
1065
- else if (node.group && node?.children?.length) {
1066
- visit(node.children);
1067
- }
1068
- });
1069
- };
1070
- visit(this.rowNodes);
1641
+ for (const node of this._rowNodes) {
1642
+ if (node.leaf)
1643
+ callback(node, leafIndex++);
1644
+ }
1071
1645
  }
1072
1646
  getRowNodeById(rowId) {
1073
1647
  return this._originalRowNodes.get(rowId);
@@ -1081,8 +1655,25 @@ class OneGridApi {
1081
1655
  setGroupDefaultExpanded(defaultExpanded) {
1082
1656
  this.groupDefaultExpanded = defaultExpanded;
1083
1657
  }
1658
+ /**
1659
+ * Expansion state lives in two places, and they answer different questions.
1660
+ *
1661
+ * `OneGridExpansionService` records only *deliberate* changes — a click, or
1662
+ * one of the methods below. It is an override layer: `buildRowNodes` consults
1663
+ * it first and falls back to `groupDefaultExpanded` for every row the user
1664
+ * has not touched. That is what lets a later `groupDefaultExpanded` change
1665
+ * take effect, and what lets a collapse survive a rebuild.
1666
+ *
1667
+ * `node.expanded` is the resolved truth for a row, defaults included. Public
1668
+ * reads report that; the override sets stay internal.
1669
+ *
1670
+ * The invariant the setters below maintain: `expanded` is only ever true for
1671
+ * a node that can expand.
1672
+ */
1084
1673
  toggleExpandAll(expanded) {
1085
1674
  this.forEachNode(node => {
1675
+ if (!node.expandable)
1676
+ return;
1086
1677
  node.setExpanded(expanded);
1087
1678
  this.expansionService.toggleExpanded(node.rowId, expanded);
1088
1679
  });
@@ -1090,7 +1681,7 @@ class OneGridApi {
1090
1681
  }
1091
1682
  setExpandedById(rowId, expanded) {
1092
1683
  const node = this._originalRowNodes.get(rowId);
1093
- if (node) {
1684
+ if (node?.expandable) {
1094
1685
  node.setExpanded(expanded);
1095
1686
  this.expansionService.toggleExpanded(rowId, expanded);
1096
1687
  }
@@ -1100,7 +1691,7 @@ class OneGridApi {
1100
1691
  if (rowIds && rowIds.length > 0) {
1101
1692
  for (let rowId of rowIds) {
1102
1693
  const node = this._originalRowNodes.get(rowId);
1103
- if (node) {
1694
+ if (node?.expandable) {
1104
1695
  node.setExpanded(expanded);
1105
1696
  this.expansionService.toggleExpanded(rowId, expanded);
1106
1697
  }
@@ -1108,20 +1699,32 @@ class OneGridApi {
1108
1699
  this.refreshVisibleNodes();
1109
1700
  }
1110
1701
  }
1702
+ /**
1703
+ * Every currently expanded row, in tree order.
1704
+ *
1705
+ * Derived from the nodes rather than read off the override record, so rows
1706
+ * opened by `groupDefaultExpanded` are included — previously this returned
1707
+ * only rows someone had explicitly toggled, and came back empty on first load
1708
+ * while the first level was visibly open.
1709
+ */
1111
1710
  getExpandedByIds() {
1112
- return Array.from(this.expansionService.getExpandedIds());
1711
+ return this._rowNodes
1712
+ .filter(node => node.expandable && node.expanded)
1713
+ .map(node => node.rowId);
1113
1714
  }
1114
1715
  /**
1115
1716
  * Master Detail Feature
1116
1717
  */
1117
1718
  setDetailRowData(node, detailData) {
1719
+ if (this._destroyed)
1720
+ return;
1118
1721
  const rowNode = this.getRowNodeById(node.rowId);
1119
1722
  if (rowNode) {
1120
1723
  rowNode.setDetailRowData(detailData);
1121
1724
  }
1122
1725
  }
1123
1726
  setGetDetailRowData(getDetailRowData) {
1124
- this.getDetailRowData = getDetailRowData;
1727
+ this._getDetailRowData = getDetailRowData;
1125
1728
  }
1126
1729
  /**
1127
1730
  * Columns State
@@ -1130,15 +1733,17 @@ class OneGridApi {
1130
1733
  return this.columnService.getColumnState();
1131
1734
  }
1132
1735
  setColumnDefs(columnDefs) {
1736
+ if (this._destroyed)
1737
+ return;
1133
1738
  this.columnService.setColumnDefs(columnDefs);
1134
1739
  const columnState = this.columnService.getColumnState();
1135
- this.cellService.applyColumnDefs(columnState.columns, this.context);
1740
+ this.cellService.applyColumnDefs(columnState.columns, this._context);
1136
1741
  this.eventService.dispatchEvent('columnChanged', columnState);
1137
1742
  }
1138
1743
  updateColumn(id, changes) {
1139
1744
  this.columnService.updateColumnDef(id, changes);
1140
1745
  const columnState = this.columnService.getColumnState();
1141
- this.cellService.applyColumnDefs(columnState.columns, this.context);
1746
+ this.cellService.applyColumnDefs(columnState.columns, this._context);
1142
1747
  this.eventService.dispatchEvent('columnChanged', columnState);
1143
1748
  }
1144
1749
  reorderColumnDefs(fromIndex, toIndex) {
@@ -1160,11 +1765,12 @@ class OneGridApi {
1160
1765
  collect(node);
1161
1766
  this.setSelectedByIds(Array.from(rowIds), selected);
1162
1767
  }
1768
+ /**
1769
+ * @deprecated Identical to `updateCellStates`. Kept as an alias so existing
1770
+ * callers keep working; prefer `updateCellStates`, which says what it does.
1771
+ */
1163
1772
  updateNodeStates(node) {
1164
- const rowNode = this.getRowNodeById(node.rowId);
1165
- if (rowNode) {
1166
- rowNode.cellStates = this.cellService.generateCellStates(rowNode);
1167
- }
1773
+ this.updateCellStates(node);
1168
1774
  }
1169
1775
  updateCellState(node, column) {
1170
1776
  const rowNode = this.getRowNodeById(node.rowId);
@@ -1172,34 +1778,42 @@ class OneGridApi {
1172
1778
  rowNode.cellStates[column.field] = this.cellService.generateCellState(rowNode, column);
1173
1779
  }
1174
1780
  }
1781
+ /**
1782
+ * Recompute every cell of one row and repaint it.
1783
+ *
1784
+ * The repaint is the point: this is what a caller reaches for after mutating
1785
+ * a row in place, and it used to regenerate the state and stop there. The
1786
+ * body renders with OnPush, so nothing marked it dirty and the new state sat
1787
+ * unread until some unrelated event happened to trigger change detection —
1788
+ * which is why callers ended up nudging the grid with a synthetic DOM event.
1789
+ */
1175
1790
  updateCellStates(node) {
1791
+ if (this._destroyed)
1792
+ return;
1176
1793
  const rowNode = this.getRowNodeById(node.rowId);
1177
- if (rowNode) {
1178
- rowNode.cellStates = this.cellService.generateCellStates(rowNode);
1179
- }
1794
+ if (!rowNode)
1795
+ return;
1796
+ rowNode.cellStates = this.cellService.generateCellStates(rowNode);
1797
+ this.eventService.dispatchEvent('cellStateChanged', { data: rowNode.data, node: rowNode });
1180
1798
  }
1181
1799
  /**
1182
1800
  * Handler that Reacts to Detail Changes
1183
1801
  */
1802
+ /**
1803
+ * Keep a master row's reserved height in step with its detail rows.
1804
+ *
1805
+ * Two listeners used to be registered here and thrown away: an empty
1806
+ * `rowDataChanged` handler whose unsubscribe was never kept, and an
1807
+ * `expandedChanged` handler whose body was entirely commented out. Both are
1808
+ * gone — a listener with no behaviour is a leak with no upside.
1809
+ */
1184
1810
  subscribeToDetailChanges(node) {
1185
- const unsubRowData = node.on('rowDataChanged', ({ node, data, previousData }) => {
1186
- });
1187
- // When detail data arrives
1188
- const unsubDetail = node.on('detailRowDataChanged', ({ node, detailRowData, previousDetailRowData }) => {
1189
- if (this.apiType === 'master') {
1190
- this._resolvedDetailRowHeight(node);
1811
+ const unsubscribeDetail = node.on('detailRowDataChanged', ({ node }) => {
1812
+ if (this._apiType === 'master') {
1813
+ this.resolveDetailRowHeight(node);
1191
1814
  }
1192
1815
  });
1193
- // When row expands
1194
- const unsubExpand = node.on('expandedChanged', ({ node, expanded }) => {
1195
- // if (!expanded) return;
1196
- // // Only fetch if detail data not already loaded
1197
- // if (!node.detailRowData) {
1198
- // this.getDetailRowData({ data: node.data, node, successCallback: (detailRowData) => node.setDetailRowData(detailRowData) });
1199
- // }
1200
- });
1201
- // // Cleanup when node is removed from grid
1202
- this.registerNodeCleanup(node.rowId, [unsubDetail, unsubExpand]);
1816
+ this.registerNodeCleanup(node.rowId, [unsubscribeDetail]);
1203
1817
  }
1204
1818
  registerNodeCleanup(rowId, unsubs) {
1205
1819
  // Merge with any existing cleanups for this rowId
@@ -1211,7 +1825,14 @@ class OneGridApi {
1211
1825
  cleanups?.forEach(fn => fn());
1212
1826
  this._nodeCleanupMap.delete(rowId);
1213
1827
  }
1214
- // Call this when rows are removed
1828
+ /**
1829
+ * Drop nodes that are no longer in the data, releasing what they hold.
1830
+ *
1831
+ * This existed but was never called: `setRowData` and `updateRowData` each
1832
+ * deleted straight out of `_originalRowNodes`, so a removed node kept its
1833
+ * listeners and its entry in `_nodeCleanupMap` for the lifetime of the grid.
1834
+ * Both now come through here, which unsubscribes and clears the node first.
1835
+ */
1215
1836
  pruneRemovedNodes(validRowIds) {
1216
1837
  for (const [rowId, node] of this._originalRowNodes) {
1217
1838
  if (!validRowIds.has(rowId)) {
@@ -1222,58 +1843,427 @@ class OneGridApi {
1222
1843
  }
1223
1844
  }
1224
1845
  /**
1225
- * Resolve Detail Row Height
1846
+ * Height a master row must reserve for its detail grid:
1847
+ * rowHeight x clamp(rows, minRows, maxRows) + headerHeight.
1226
1848
  */
1227
- _resolvedDetailRowHeight(node) {
1228
- const minRows = this.detailCellRendererParams.detailGridOptions.detailRowHeightOptions.minRows;
1229
- const maxRows = this.detailCellRendererParams.detailGridOptions.detailRowHeightOptions.maxRows;
1849
+ resolveDetailRowHeight(node) {
1850
+ const heightOptions = this.detailCellRendererParams?.detailGridOptions?.detailRowHeightOptions;
1851
+ // Reachable whenever masterDetail is on but detailCellRendererParams is
1852
+ // incomplete; without the guard this threw from inside an event listener.
1853
+ if (!heightOptions)
1854
+ return;
1855
+ const minRows = heightOptions.minRows;
1856
+ const maxRows = heightOptions.maxRows;
1230
1857
  const minHeight = (this.rowHeight * minRows) + this.headerRowHeight;
1231
1858
  const maxHeight = (this.rowHeight * maxRows) + this.headerRowHeight;
1232
1859
  const rows = node.detailRowData && node.detailRowData.length ? node.detailRowData.length : 0;
1233
1860
  node.detailRowHeight = rows <= minRows ? minHeight : rows >= maxRows ? maxHeight : (this.rowHeight * rows) + this.headerRowHeight;
1234
1861
  }
1235
1862
  /**
1236
- * On Destroy
1863
+ * Release everything the grid holds. Called by `OneGridComponent.ngOnDestroy`.
1864
+ *
1865
+ * The services are deliberately *not* nulled out any more. They are injected,
1866
+ * Angular owns their lifetime, and nulling them only guaranteed a
1867
+ * `TypeError` for any callback that arrived after teardown — a detail-row
1868
+ * fetch resolving late, or the body's deferred update. `_destroyed` makes
1869
+ * those calls no-ops instead, which is what a torn-down object should do.
1237
1870
  */
1238
1871
  destroy() {
1239
1872
  if (this._destroyed)
1240
1873
  return;
1241
1874
  this._destroyed = true;
1242
- this.rowNodes.forEach(node => {
1243
- node.children = [];
1244
- node.parent = null;
1245
- });
1246
- this.rowNodes = [];
1875
+ // Unsubscribe every per-node listener before dropping the nodes.
1876
+ for (const rowId of Array.from(this._nodeCleanupMap.keys())) {
1877
+ this.cleanupNode(rowId);
1878
+ }
1879
+ // `destroy` drops the tree links and the listeners; the API no longer has
1880
+ // to reach in and unpick each node itself.
1881
+ this._originalRowNodes.forEach(node => node.destroy());
1882
+ this._rowNodes.forEach(node => node.destroy());
1883
+ for (const side of ['top', 'bottom']) {
1884
+ this._pinnedRowNodes[side].forEach(node => node.destroy());
1885
+ this._pinnedRowNodes[side] = [];
1886
+ }
1887
+ this._rowNodes = [];
1888
+ this._visibleRowNodes = [];
1247
1889
  this._originalRowNodes.clear();
1248
- this._currentIndex = null;
1249
- // Clear services
1250
- if (this.selectionService) {
1251
- this.selectionService.deselectAll();
1890
+ this._nodeCleanupMap.clear();
1891
+ this._currentIndex = 0;
1892
+ // Release the back-reference the cell service holds, so a torn-down api is
1893
+ // not kept alive by the service that outlives this call.
1894
+ this.cellService.setGridApi(null);
1895
+ this.selectionService.deselectAll();
1896
+ this.expansionService.clear();
1897
+ this._activeFilters = [];
1898
+ this._quickFilterValue = null;
1899
+ this._sortModel = [];
1900
+ this._context = null;
1901
+ this.masterDetail = false;
1902
+ this.treeData = false;
1903
+ this.treeDataChildrenField = null;
1904
+ this.groupDefaultExpanded = 0;
1905
+ this.detailCellRendererParams = null;
1906
+ this.detailRendererComponent = null;
1907
+ this._getDetailRowData = null;
1908
+ }
1909
+ /** Whether `destroy` has run. Late callbacks check this before doing work. */
1910
+ get destroyed() {
1911
+ return this._destroyed;
1912
+ }
1913
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OneGridApi, deps: [{ token: OgColumnService }, { token: OneGridRowService }, { token: OneGridCellService }, { token: OneGridEventService }, { token: OneGridSelectionService }, { token: OneGridExpansionService }], target: i0.ɵɵFactoryTarget.Injectable }); }
1914
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OneGridApi }); }
1915
+ }
1916
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OneGridApi, decorators: [{
1917
+ type: Injectable
1918
+ }], ctorParameters: () => [{ type: OgColumnService }, { type: OneGridRowService }, { type: OneGridCellService }, { type: OneGridEventService }, { type: OneGridSelectionService }, { type: OneGridExpansionService }] });
1919
+
1920
+ /** Pointer distance before a press becomes a drag. */
1921
+ const DRAG_THRESHOLD_PX = 4;
1922
+ /** How close to an edge auto-scroll kicks in, and how fast it goes. */
1923
+ const EDGE_ZONE_PX = 36;
1924
+ const EDGE_SPEED_PX = 12;
1925
+ /**
1926
+ * Owns one row-drag gesture at a time.
1927
+ *
1928
+ * Built on pointer events rather than HTML5 drag-and-drop or the CDK's
1929
+ * drag-drop:
1930
+ *
1931
+ * - HTML5 DnD does not fire for touch at all, and its drag image cannot be
1932
+ * styled to match the grid.
1933
+ * - `cdkDropList` assumes every item it manages is in the DOM. Inside a virtual
1934
+ * scroll viewport rows unmount as they leave the buffer, so the list it
1935
+ * thinks it is sorting is only ever the visible window.
1936
+ * - Pointer events cover mouse, touch and pen in one path, and pointer capture
1937
+ * keeps the gesture alive when the finger leaves the row it started on.
1938
+ *
1939
+ * Because rows are a fixed `itemSize`, the drop target is arithmetic on the
1940
+ * pointer's Y rather than a hit test against row elements — which is also why
1941
+ * it stays correct for rows that are scrolled out and not rendered.
1942
+ */
1943
+ class OneGridRowDragService {
1944
+ constructor() {
1945
+ this.zone = inject(NgZone);
1946
+ /** Live drag, for the body to render the handle state and the indicator. */
1947
+ this.state$ = new BehaviorSubject(null);
1948
+ /** Screen-reader narration for the keyboard path. */
1949
+ this.announcement$ = new BehaviorSubject('');
1950
+ this.context = null;
1951
+ this.pointerId = null;
1952
+ this.handle = null;
1953
+ this.startY = 0;
1954
+ this.armed = false;
1955
+ this.lastClientY = 0;
1956
+ this.scrollFrame = null;
1957
+ /** A move already applied during this drag, so a cancel can undo it. */
1958
+ this.appliedFrom = null;
1959
+ this.handlePointerMove = (event) => {
1960
+ if (this.pointerId === null || event.pointerId !== this.pointerId)
1961
+ return;
1962
+ if (!this.armed) {
1963
+ if (Math.abs(event.clientY - this.startY) < DRAG_THRESHOLD_PX)
1964
+ return;
1965
+ this.armed = true;
1966
+ this.publish({ rowId: this.nodeAt(this.appliedFrom)?.rowId ?? '', fromIndex: this.appliedFrom, insertAt: this.appliedFrom, source: 'pointer' });
1967
+ }
1968
+ // Held so the auto-scroll loop can recompute the target as the viewport
1969
+ // moves under a stationary finger.
1970
+ this.lastClientY = event.clientY;
1971
+ // Touch scrolling would otherwise pan the viewport out from under us.
1972
+ event.preventDefault();
1973
+ this.updateFromPointer();
1974
+ this.updateAutoScroll();
1975
+ };
1976
+ this.handlePointerUp = (event) => {
1977
+ if (this.pointerId === null || event.pointerId !== this.pointerId)
1978
+ return;
1979
+ const dragged = this.armed;
1980
+ this.teardownPointer();
1981
+ if (!dragged) {
1982
+ this.reset();
1983
+ return;
1984
+ }
1985
+ this.commit('pointer');
1986
+ };
1987
+ this.handlePointerCancel = () => {
1988
+ this.teardownPointer();
1989
+ this.cancel();
1990
+ };
1991
+ /** Escape aborts a pointer drag, the same as it does a keyboard one. */
1992
+ this.handlePointerKeydown = (event) => {
1993
+ if (event.key !== 'Escape')
1994
+ return;
1995
+ event.preventDefault();
1996
+ this.teardownPointer();
1997
+ this.cancel();
1998
+ };
1999
+ }
2000
+ attach(context) {
2001
+ this.context = context;
2002
+ }
2003
+ get state() {
2004
+ return this.state$.value;
2005
+ }
2006
+ get dragging() {
2007
+ return this.state$.value !== null;
2008
+ }
2009
+ // -------------------------------------------------------------------------
2010
+ // Pointer
2011
+ // -------------------------------------------------------------------------
2012
+ /**
2013
+ * Begin a pointer drag. Nothing visible happens until the pointer has moved
2014
+ * {@link DRAG_THRESHOLD_PX}, so a click on the handle — which is also the
2015
+ * keyboard affordance — is not swallowed by a zero-distance drag.
2016
+ */
2017
+ startPointerDrag(event, node, handlers = {}) {
2018
+ if (!this.context || this.dragging || this.pointerId !== null)
2019
+ return;
2020
+ // Secondary buttons open context menus; they are not a drag.
2021
+ if (event.button !== 0)
2022
+ return;
2023
+ const index = this.indexOf(node);
2024
+ if (index === -1)
2025
+ return;
2026
+ this.pointerId = event.pointerId;
2027
+ this.handle = event.target;
2028
+ this.startY = event.clientY;
2029
+ this.lastClientY = event.clientY;
2030
+ this.armed = false;
2031
+ this.appliedFrom = index;
2032
+ this.onEnd = handlers.onEnd;
2033
+ this.handle.setPointerCapture?.(event.pointerId);
2034
+ // Outside Angular: pointermove fires at the display's refresh rate and
2035
+ // every one of them would otherwise run change detection over the grid.
2036
+ // The few that change the drop target re-enter through `publish`.
2037
+ this.zone.runOutsideAngular(() => {
2038
+ document.addEventListener('pointermove', this.handlePointerMove, { passive: false });
2039
+ document.addEventListener('pointerup', this.handlePointerUp);
2040
+ document.addEventListener('pointercancel', this.handlePointerCancel);
2041
+ document.addEventListener('keydown', this.handlePointerKeydown);
2042
+ });
2043
+ }
2044
+ /**
2045
+ * Turn the pointer's Y into an insertion boundary.
2046
+ *
2047
+ * Boundaries, not rows: the value is where the row would be *inserted*, so
2048
+ * 0 is above the first row and `length` is below the last. Rounding rather
2049
+ * than flooring puts the split at the middle of each row, which is what
2050
+ * makes the indicator flip when you cross the halfway point.
2051
+ */
2052
+ updateFromPointer() {
2053
+ const current = this.state$.value;
2054
+ if (!this.context || !current)
2055
+ return;
2056
+ const rect = this.context.viewport.getBoundingClientRect();
2057
+ const offset = this.lastClientY - rect.top + this.context.viewport.scrollTop;
2058
+ const insertAt = this.clamp(Math.round(offset / this.context.itemSize), 0, this.context.nodes().length);
2059
+ if (insertAt === current.insertAt)
2060
+ return;
2061
+ this.publish({ ...current, insertAt });
2062
+ }
2063
+ /**
2064
+ * Scroll when the pointer sits near an edge, so rows outside the viewport
2065
+ * are reachable without letting go. Runs on rAF while the pointer is parked
2066
+ * in the zone, and re-derives the target each frame because the content
2067
+ * moves while the pointer does not.
2068
+ */
2069
+ updateAutoScroll() {
2070
+ if (!this.context)
2071
+ return;
2072
+ const rect = this.context.viewport.getBoundingClientRect();
2073
+ const distanceToTop = this.lastClientY - rect.top;
2074
+ const distanceToBottom = rect.bottom - this.lastClientY;
2075
+ let delta = 0;
2076
+ if (distanceToTop < EDGE_ZONE_PX)
2077
+ delta = -EDGE_SPEED_PX;
2078
+ else if (distanceToBottom < EDGE_ZONE_PX)
2079
+ delta = EDGE_SPEED_PX;
2080
+ if (delta === 0) {
2081
+ this.stopAutoScroll();
2082
+ return;
2083
+ }
2084
+ if (this.scrollFrame !== null)
2085
+ return;
2086
+ const step = () => {
2087
+ if (!this.context || !this.dragging) {
2088
+ this.stopAutoScroll();
2089
+ return;
2090
+ }
2091
+ const viewport = this.context.viewport;
2092
+ const before = viewport.scrollTop;
2093
+ viewport.scrollTop = before + delta;
2094
+ // Hit the end — nothing more to reveal, so stop burning frames.
2095
+ if (viewport.scrollTop === before) {
2096
+ this.stopAutoScroll();
2097
+ return;
2098
+ }
2099
+ this.updateFromPointer();
2100
+ this.scrollFrame = requestAnimationFrame(step);
2101
+ };
2102
+ this.scrollFrame = requestAnimationFrame(step);
2103
+ }
2104
+ stopAutoScroll() {
2105
+ if (this.scrollFrame === null)
2106
+ return;
2107
+ cancelAnimationFrame(this.scrollFrame);
2108
+ this.scrollFrame = null;
2109
+ }
2110
+ teardownPointer() {
2111
+ this.stopAutoScroll();
2112
+ if (this.handle && this.pointerId !== null) {
2113
+ this.handle.releasePointerCapture?.(this.pointerId);
2114
+ }
2115
+ document.removeEventListener('pointermove', this.handlePointerMove);
2116
+ document.removeEventListener('pointerup', this.handlePointerUp);
2117
+ document.removeEventListener('pointercancel', this.handlePointerCancel);
2118
+ document.removeEventListener('keydown', this.handlePointerKeydown);
2119
+ this.pointerId = null;
2120
+ this.handle = null;
2121
+ this.armed = false;
2122
+ }
2123
+ // -------------------------------------------------------------------------
2124
+ // Keyboard
2125
+ //
2126
+ // WCAG 2.5.7 Dragging Movements: anything achievable by dragging needs a
2127
+ // single-pointer, non-dragging alternative. Arrow-key moves are that, and
2128
+ // they are also simply how a keyboard user reorders a list.
2129
+ // -------------------------------------------------------------------------
2130
+ /** Space/Enter on the handle: pick the row up, or put it down. */
2131
+ toggleKeyboardDrag(node, handlers = {}) {
2132
+ if (!this.context)
2133
+ return;
2134
+ if (this.state$.value?.source === 'keyboard') {
2135
+ this.commit('keyboard');
2136
+ return;
2137
+ }
2138
+ if (this.dragging)
2139
+ return;
2140
+ const index = this.indexOf(node);
2141
+ if (index === -1)
2142
+ return;
2143
+ this.appliedFrom = index;
2144
+ this.onEnd = handlers.onEnd;
2145
+ this.publish({ rowId: node.rowId, fromIndex: index, insertAt: index, source: 'keyboard' });
2146
+ this.announce(`${this.describe(node, index)} grabbed. Use the arrow keys to move it, space to drop, escape to cancel.`);
2147
+ }
2148
+ /**
2149
+ * Arrow keys move the row a position at a time, applying as they go.
2150
+ *
2151
+ * Applied rather than previewed because the row has to be seen to move: a
2152
+ * keyboard user gets no drag image, and a preview line alone tells someone
2153
+ * relying on a screen reader nothing about where the row now sits. Escape
2154
+ * still restores the original position.
2155
+ */
2156
+ moveKeyboard(delta) {
2157
+ const current = this.state$.value;
2158
+ if (!this.context || current?.source !== 'keyboard')
2159
+ return false;
2160
+ const nodes = this.context.nodes();
2161
+ const from = current.insertAt;
2162
+ const to = this.clamp(from + delta, 0, nodes.length - 1);
2163
+ if (to === from) {
2164
+ this.announce(to === 0 ? 'Already at the top.' : 'Already at the bottom.');
2165
+ return true;
2166
+ }
2167
+ if (!this.context.move(from, to))
2168
+ return true;
2169
+ this.publish({ ...current, insertAt: to });
2170
+ const node = this.nodeAt(to);
2171
+ this.announce(node ? `Moved to position ${to + 1} of ${nodes.length}.` : '');
2172
+ return true;
2173
+ }
2174
+ // -------------------------------------------------------------------------
2175
+ // Commit / cancel
2176
+ // -------------------------------------------------------------------------
2177
+ commit(source) {
2178
+ const current = this.state$.value;
2179
+ if (!this.context || !current) {
2180
+ this.reset();
2181
+ return;
2182
+ }
2183
+ const nodes = this.context.nodes();
2184
+ // Pointer drags apply once, at the end. A boundary index becomes a row
2185
+ // index by dropping one when the row travelled downwards, since lifting
2186
+ // it out first shifts everything below it up.
2187
+ if (source === 'pointer') {
2188
+ const from = current.fromIndex;
2189
+ const to = this.clamp(current.insertAt > from ? current.insertAt - 1 : current.insertAt, 0, nodes.length - 1);
2190
+ const node = this.nodeAt(from);
2191
+ if (node && to !== from && this.context.move(from, to)) {
2192
+ this.finish({ node, fromIndex: from, toIndex: to, source });
2193
+ return;
2194
+ }
2195
+ this.reset();
2196
+ return;
1252
2197
  }
1253
- this.selectionService = null;
1254
- if (this.expansionService) {
1255
- this.expansionService.clear();
2198
+ // Keyboard moves were applied as they happened; this only confirms.
2199
+ const node = this.nodeAt(current.insertAt);
2200
+ const moved = node && current.insertAt !== current.fromIndex;
2201
+ this.announce(moved ? `Dropped at position ${current.insertAt + 1} of ${nodes.length}.` : 'Cancelled. Row returned to its original position.');
2202
+ if (moved) {
2203
+ this.finish({ node: node, fromIndex: current.fromIndex, toIndex: current.insertAt, source });
2204
+ return;
2205
+ }
2206
+ this.reset();
2207
+ }
2208
+ /** Abort, restoring anything a keyboard drag already applied. */
2209
+ cancel() {
2210
+ const current = this.state$.value;
2211
+ if (current?.source === 'keyboard' && this.context && current.insertAt !== current.fromIndex) {
2212
+ this.context.move(current.insertAt, current.fromIndex);
2213
+ this.announce('Cancelled. Row returned to its original position.');
2214
+ }
2215
+ else if (current) {
2216
+ this.announce('Cancelled.');
2217
+ }
2218
+ this.reset();
2219
+ }
2220
+ finish(event) {
2221
+ const done = this.onEnd;
2222
+ this.reset();
2223
+ this.zone.run(() => done?.(event));
2224
+ }
2225
+ reset() {
2226
+ this.stopAutoScroll();
2227
+ this.appliedFrom = null;
2228
+ this.onEnd = undefined;
2229
+ if (this.state$.value !== null) {
2230
+ this.zone.run(() => this.state$.next(null));
1256
2231
  }
1257
- this.expansionService = null;
1258
- this.eventService = null;
1259
- this.rowService = null;
1260
- this.cellService = null;
1261
- // Clear filters, sorting, context
1262
- this._activeFilters = [];
1263
- this._quickFilterValue = null;
1264
- this.sortModel = [];
1265
- // Clear master/detail/treeData
1266
- this.masterDetail = false;
1267
- this.treeData = false;
1268
- this.treeDataChildrenField = '';
1269
- this.groupDefaultExpanded = 0;
1270
2232
  }
1271
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OneGridApi, deps: [{ token: OgColumnService }, { token: OneGridRowService }, { token: OneGridCellService }, { token: OneGridEventService }, { token: OneGridSelectionService }, { token: OneGridExpansionService }], target: i0.ɵɵFactoryTarget.Injectable }); }
1272
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OneGridApi }); }
2233
+ // -------------------------------------------------------------------------
2234
+ // Helpers
2235
+ // -------------------------------------------------------------------------
2236
+ publish(state) {
2237
+ this.zone.run(() => this.state$.next(state));
2238
+ }
2239
+ announce(message) {
2240
+ this.zone.run(() => this.announcement$.next(message));
2241
+ }
2242
+ describe(node, index) {
2243
+ return `Row ${index + 1} of ${this.context?.nodes().length ?? 0},`;
2244
+ }
2245
+ indexOf(node) {
2246
+ return this.context?.nodes().findIndex(candidate => candidate.rowId === node.rowId) ?? -1;
2247
+ }
2248
+ nodeAt(index) {
2249
+ return this.context?.nodes()[index] ?? null;
2250
+ }
2251
+ clamp(value, min, max) {
2252
+ return Math.min(Math.max(value, min), max);
2253
+ }
2254
+ ngOnDestroy() {
2255
+ this.teardownPointer();
2256
+ this.state$.next(null);
2257
+ this.state$.complete();
2258
+ this.announcement$.complete();
2259
+ this.context = null;
2260
+ }
2261
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OneGridRowDragService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2262
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OneGridRowDragService }); }
1273
2263
  }
1274
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OneGridApi, decorators: [{
2264
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OneGridRowDragService, decorators: [{
1275
2265
  type: Injectable
1276
- }], ctorParameters: () => [{ type: OgColumnService }, { type: OneGridRowService }, { type: OneGridCellService }, { type: OneGridEventService }, { type: OneGridSelectionService }, { type: OneGridExpansionService }] });
2266
+ }] });
1277
2267
 
1278
2268
  class OneGridTreeDataService {
1279
2269
  create(tree, parentId, newNode) {
@@ -1334,6 +2324,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
1334
2324
 
1335
2325
  const OnePageSizeOption = [25, 50, 100, 250];
1336
2326
 
2327
+ function resolveRowHeight(params, getRowHeight, rowHeight = 26) {
2328
+ if (getRowHeight) {
2329
+ const result = getRowHeight(params);
2330
+ // fallback behavior like AG Grid
2331
+ if (result == null) {
2332
+ return rowHeight;
2333
+ }
2334
+ // guard against invalid values
2335
+ return Math.max(1, result);
2336
+ }
2337
+ return rowHeight;
2338
+ }
2339
+
1337
2340
  class HeaderCheckboxDisabledPipe {
1338
2341
  transform(rowNodes, selectionMode, allowedEdit) {
1339
2342
  return rowNodes?.length === 0 || selectionMode !== 'multiple' || !allowedEdit;
@@ -1384,6 +2387,8 @@ class OneGridCheckboxHeaderComponent {
1384
2387
  this.eventService = eventService;
1385
2388
  this.selectionService = selectionService;
1386
2389
  this.allowedEdit = false;
2390
+ /** Unsubscribe callbacks for the two grid listeners below. */
2391
+ this.teardown = [];
1387
2392
  this.state = 'unchecked';
1388
2393
  this.rowNodes = [];
1389
2394
  }
@@ -1392,19 +2397,26 @@ class OneGridCheckboxHeaderComponent {
1392
2397
  /**
1393
2398
  * Listen Row Node Selection Changed Event
1394
2399
  */
1395
- this.eventService.addEventListener('selectionChanged', (payload) => {
2400
+ this.teardown.push(this.eventService.addEventListener('selectionChanged', (payload) => {
1396
2401
  this.rowNodes = this.api.getAllRowNodes();
1397
2402
  const rowIds = this.rowNodes && this.rowNodes.length > 0 ? this.rowNodes.map((node) => node.rowId) : [];
1398
2403
  this.state = this.selectionService.getSelectAllState(rowIds);
1399
- });
2404
+ }));
1400
2405
  /**
1401
2406
  * Listen Row Data Changed Event
1402
2407
  */
1403
- this.eventService.addEventListener('rowDataChanged', (payload) => {
2408
+ this.teardown.push(this.eventService.addEventListener('rowDataChanged', (payload) => {
1404
2409
  this.rowNodes = this.api.getAllRowNodes();
1405
2410
  const rowIds = this.rowNodes && this.rowNodes.length > 0 ? this.rowNodes.map((node) => node.rowId) : [];
1406
2411
  this.state = this.selectionService.getSelectAllState(rowIds);
1407
- });
2412
+ }));
2413
+ }
2414
+ ngOnDestroy() {
2415
+ // Both listeners were registered and never released. The component-scoped
2416
+ // event service happened to clean up after them; relying on that left the
2417
+ // component's own lifecycle incomplete.
2418
+ this.teardown.forEach(unsubscribe => unsubscribe());
2419
+ this.teardown.length = 0;
1408
2420
  }
1409
2421
  /**
1410
2422
  * Toggle Selection Action
@@ -1435,6 +2447,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
1435
2447
  }] } });
1436
2448
 
1437
2449
  class ColumnSortStatePipe {
2450
+ // `readonly`, because this is fed straight from `api.sortModel`, which is
2451
+ // now a readonly view rather than the mutable array it used to hand out.
1438
2452
  transform(field, sortModel) {
1439
2453
  if (!sortModel || !field)
1440
2454
  return null;
@@ -1452,16 +2466,29 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
1452
2466
  }] });
1453
2467
 
1454
2468
  class OneGridHeaderComponent {
2469
+ /**
2470
+ * Horizontal offset of the body, mirrored here so the header tracks it.
2471
+ *
2472
+ * Applied to the viewport's `scrollLeft` rather than as a transform on the
2473
+ * row: sticky positioning needs a real scrollport above it. The group row,
2474
+ * which is a sibling of that viewport, still reads this for its transform.
2475
+ */
2476
+ set scrollLeft(value) {
2477
+ this._scrollLeft = value ?? 0;
2478
+ this.applyScrollLeft();
2479
+ }
2480
+ get scrollLeft() {
2481
+ return this._scrollLeft;
2482
+ }
1455
2483
  constructor(eventService, cdr) {
1456
2484
  this.eventService = eventService;
1457
2485
  this.cdr = cdr;
1458
- this.scrollLeft = 0;
2486
+ this._scrollLeft = 0;
1459
2487
  this.allowedEdit = false;
1460
2488
  this.enableGroupColumnDefs = false;
1461
2489
  this.groupColumnDefs = [];
1462
2490
  this.columnDefs = [];
1463
2491
  this.sortModel = [];
1464
- this.destroy$ = new Subject();
1465
2492
  this.subscriptions = new Subscription();
1466
2493
  this.draggedIndex = null;
1467
2494
  // Track resize state
@@ -1503,6 +2530,16 @@ class OneGridHeaderComponent {
1503
2530
  // this.cdr.markForCheck();
1504
2531
  }));
1505
2532
  }
2533
+ ngAfterViewInit() {
2534
+ // The input setter can run before the view exists, so whatever arrived
2535
+ // first is applied once here.
2536
+ this.applyScrollLeft();
2537
+ }
2538
+ applyScrollLeft() {
2539
+ if (this.headerViewport) {
2540
+ this.headerViewport.nativeElement.scrollLeft = this._scrollLeft;
2541
+ }
2542
+ }
1506
2543
  onHeaderMenuOptionClick(columnDef) {
1507
2544
  }
1508
2545
  onHeaderMenuSortClick(colId) {
@@ -1528,7 +2565,12 @@ class OneGridHeaderComponent {
1528
2565
  this.resizing = true;
1529
2566
  this.resizeColumnIndex = columnIndex;
1530
2567
  this.startX = event.clientX;
1531
- this.startWidth = this.columnDefs[columnIndex].width;
2568
+ // Measure what is on screen rather than trusting `col.width`, which is
2569
+ // optional: a column sized purely by minWidth had `undefined` here, and
2570
+ // `Math.max(50, undefined + deltaX)` is NaN — the column collapsed on the
2571
+ // first mouse move and could not be recovered.
2572
+ const columnElement = event.target?.closest('.one-grid-header-column');
2573
+ this.startWidth = columnElement?.offsetWidth || resolveColumnWidth(this.columnDefs[columnIndex]);
1532
2574
  // Add document-level listeners for drag and release
1533
2575
  document.addEventListener('mousemove', this.onResizeMove);
1534
2576
  document.addEventListener('mouseup', this.onResizeEnd);
@@ -1561,19 +2603,27 @@ class OneGridHeaderComponent {
1561
2603
  event.preventDefault();
1562
2604
  this.draggedIndex = null;
1563
2605
  }
1564
- /**
1565
- * Clear All Subscriptions
1566
- */
1567
2606
  ngOnDestroy() {
1568
- this.destroy$.complete();
2607
+ // This completed an unused Subject and left the real subscription running.
2608
+ // It survived only because OneGridEventService is component-scoped and
2609
+ // tears its own listeners down — the header was relying on that by accident.
2610
+ this.subscriptions.unsubscribe();
2611
+ // A resize in flight owns two listeners on `document`. Without this they
2612
+ // outlive the component, and their closures keep it alive with them.
2613
+ if (this.resizing) {
2614
+ this.onResizeEnd();
2615
+ }
1569
2616
  }
1570
2617
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OneGridHeaderComponent, deps: [{ token: OneGridEventService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); }
1571
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: OneGridHeaderComponent, selector: "one-grid-header", inputs: { scrollLeft: "scrollLeft", allowedEdit: "allowedEdit", enableGroupColumnDefs: "enableGroupColumnDefs", api: "api" }, ngImport: i0, template: "<div class=\"one-grid-header\">\n @if (enableGroupColumnDefs) {\n <div class=\"one-group-header-row\" [style.transform]=\"'translateX(-' + scrollLeft + 'px)'\">\n <ng-container *ngTemplateOutlet=\"groupColumns; context: { groups: groupColumnDefs }\"></ng-container>\n </div>\n }\n <div class=\"one-grid-header-viewport\">\n <div class=\"one-header-row\" [style.transform]=\"'translateX(-' + scrollLeft + 'px)'\">\n <ng-container *ngTemplateOutlet=\"headerColumns; context: { columns: columnDefs }\"></ng-container>\n </div>\n </div>\n</div>\n\n<!-- Group Header Cell Template -->\n<ng-template #groupColumns let-groups=\"groups\">\n @for(group of groups; track group.id) {\n <div class=\"one-group-header-cell\"\n [style.width.px]=\"group.width\"\n [ngClass]=\"group._resolvedGroupClass\"\n [ngStyle]=\"group._resolvedGroupStyle\">\n <div class=\"one-group-header-cell-text\">\n {{ group.headerName }}\n </div>\n </div>\n }\n</ng-template>\n\n<!-- Header Cell Template -->\n<ng-template #headerColumns let-columns=\"columns\">\n @for (col of columns; let colIndex = $index; track col.id) {\n <div class=\"one-grid-header-column\"\n [attr.data-index]=\"colIndex\"\n [style.width.px]=\"col.width\"\n [style.min-width.px]=\"col.minWidth\"\n [style.max-width.px]=\"col.maxWidth\"\n [draggable]=\"col?.dragable\"\n (dragstart)=\"onDragColumnStart($event, colIndex)\"\n (dragover)=\"onDragColumnOver($event, colIndex)\"\n (dragend)=\"onDragColumnEnd($event)\"\n (drop)=\"onDropColumn($event)\">\n <div class=\"one-grid-header-cell\"\n [ngClass]=\"col._resolvedHeaderClass\"\n [ngStyle]=\"col._resolvedHeaderStyle\"\n >\n <span class=\"one-grid-header-cell-group\">\n @if (col?.headerCheckboxSelection) {\n <one-grid-checkbox-header\n [api]=\"api\"\n [allowedEdit]=\"allowedEdit\"\n />\n }\n\n {{ col?.headerName ?? '' }}\n </span>\n\n <!-- Og Resize Handler -->\n @if (col?.resizable) {\n <div class=\"og-resize-handle\" (mousedown)=\"onResizeStart($event, colIndex)\">\n <i class=\"bi bi-grip-vertical\"></i>\n </div>\n }\n\n <div class=\"one-grid-header-cell-group\">\n\n <!-- Sort Menu Button -->\n @if (col?.sortable) {\n <div class=\"one-grid-header-menu-button\" (click)=\"onHeaderMenuSortClick(col.field)\">\n\n @if ((col.field | columnSortState:api.sortModel) === 'asc') {\n <lucide-icon name=\"arrow-up-a-z\" [size]=\"16\" [strokeWidth]=\"2\"></lucide-icon>\n }\n @else if ((col.field | columnSortState:api.sortModel) === 'desc') {\n <lucide-icon name=\"arrow-down-z-a\" [size]=\"16\" [strokeWidth]=\"2\"></lucide-icon>\n }\n @else {\n <lucide-icon name=\"arrow-down-up\" [size]=\"16\" [strokeWidth]=\"2\"></lucide-icon>\n }\n </div>\n }\n\n <!-- Og Option Menu Button -->\n @if (col && !col.suppressHeaderMenuButton) {\n <div class=\"one-grid-header-menu-button\" (click)=\"onHeaderMenuOptionClick(col)\">\n <i class=\"bi bi-three-dots-vertical\"></i>\n </div>\n }\n </div>\n </div>\n </div>\n }\n</ng-template>\n", styles: [""], dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i3.LucideAngularComponent, selector: "lucide-angular, lucide-icon, i-lucide, span-lucide", inputs: ["class", "name", "img", "color", "absoluteStrokeWidth", "size", "strokeWidth"] }, { kind: "component", type: OneGridCheckboxHeaderComponent, selector: "one-grid-checkbox-header", inputs: ["api", "allowedEdit"] }, { kind: "pipe", type: ColumnSortStatePipe, name: "columnSortState" }] }); }
2618
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: OneGridHeaderComponent, selector: "one-grid-header", inputs: { scrollLeft: "scrollLeft", allowedEdit: "allowedEdit", enableGroupColumnDefs: "enableGroupColumnDefs", api: "api" }, viewQueries: [{ propertyName: "headerViewport", first: true, predicate: ["headerViewport"], descendants: true, static: true }], ngImport: i0, template: "<div class=\"one-grid-header\">\n @if (enableGroupColumnDefs) {\n <div class=\"one-group-header-row\" [style.transform]=\"'translateX(-' + scrollLeft + 'px)'\">\n <ng-container *ngTemplateOutlet=\"groupColumns; context: { groups: groupColumnDefs }\"></ng-container>\n </div>\n }\n <!-- The column row is scrolled rather than translated. `position: sticky`\n resolves against the nearest scrollport, and a transformed ancestor is\n not one \u2014 pinned headers could never hold their place inside it. The\n group row above keeps its transform: groups span contiguous columns,\n which pinning reorders, so the two features do not combine. -->\n <div #headerViewport class=\"one-grid-header-viewport\">\n <div class=\"one-header-row\">\n <ng-container *ngTemplateOutlet=\"headerColumns; context: { columns: columnDefs }\"></ng-container>\n </div>\n </div>\n</div>\n\n<!-- Group Header Cell Template -->\n<ng-template #groupColumns let-groups=\"groups\">\n @for(group of groups; track group.id) {\n <div class=\"one-group-header-cell\"\n [style.width.px]=\"group.width\"\n [ngClass]=\"group._resolvedGroupClass\"\n [ngStyle]=\"group._resolvedGroupStyle\">\n <div class=\"one-group-header-cell-text\">\n {{ group.headerName }}\n </div>\n </div>\n }\n</ng-template>\n\n<!-- Header Cell Template -->\n<ng-template #headerColumns let-columns=\"columns\">\n @for (col of columns; let colIndex = $index; track col.id) {\n <div class=\"one-grid-header-column\"\n [attr.data-index]=\"colIndex\"\n [class.one-column-pinned-left]=\"col.pinned === 'left'\"\n [class.one-column-pinned-right]=\"col.pinned === 'right'\"\n [class.one-column-pinned-edge]=\"col._pinnedEdge\"\n [class.one-column-pinned-edge]=\"col._pinnedEdge\"\n [style.left.px]=\"col.pinned === 'left' ? col._pinnedOffset : null\"\n [style.right.px]=\"col.pinned === 'right' ? col._pinnedOffset : null\"\n [style.width.px]=\"col.width\"\n [style.min-width.px]=\"col.minWidth\"\n [style.max-width.px]=\"col.maxWidth\"\n [draggable]=\"col._resolvedDragable\"\n (dragstart)=\"onDragColumnStart($event, colIndex)\"\n (dragover)=\"onDragColumnOver($event, colIndex)\"\n (dragend)=\"onDragColumnEnd($event)\"\n (drop)=\"onDropColumn($event)\">\n <div class=\"one-grid-header-cell\"\n [ngClass]=\"col._resolvedHeaderClass\"\n [ngStyle]=\"col._resolvedHeaderStyle\"\n >\n <span class=\"one-grid-header-cell-group\">\n @if (col?.headerCheckboxSelection) {\n <one-grid-checkbox-header\n [api]=\"api\"\n [allowedEdit]=\"allowedEdit\"\n />\n }\n\n {{ col?.headerName ?? '' }}\n </span>\n\n <!-- Og Resize Handler -->\n @if (col._resolvedResizable) {\n <div class=\"og-resize-handle\" (mousedown)=\"onResizeStart($event, colIndex)\">\n <i class=\"bi bi-grip-vertical\"></i>\n </div>\n }\n\n <div class=\"one-grid-header-cell-group\">\n\n <!-- Sort Menu Button -->\n @if (col._resolvedSortable) {\n <div class=\"one-grid-header-menu-button\" (click)=\"onHeaderMenuSortClick(col.field)\">\n\n @if ((col.field | columnSortState:api.sortModel) === 'asc') {\n <lucide-icon name=\"arrow-up-a-z\" [size]=\"16\" [strokeWidth]=\"2\"></lucide-icon>\n }\n @else if ((col.field | columnSortState:api.sortModel) === 'desc') {\n <lucide-icon name=\"arrow-down-z-a\" [size]=\"16\" [strokeWidth]=\"2\"></lucide-icon>\n }\n @else {\n <lucide-icon name=\"arrow-down-up\" [size]=\"16\" [strokeWidth]=\"2\"></lucide-icon>\n }\n </div>\n }\n\n <!-- Og Option Menu Button -->\n @if (col && !col._resolvedSuppressHeaderMenuButton) {\n <div class=\"one-grid-header-menu-button\" (click)=\"onHeaderMenuOptionClick(col)\">\n <i class=\"bi bi-three-dots-vertical\"></i>\n </div>\n }\n </div>\n </div>\n </div>\n }\n</ng-template>\n", styles: [""], dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i2$1.LucideAngularComponent, selector: "lucide-angular, lucide-icon, i-lucide, span-lucide", inputs: ["class", "name", "img", "color", "absoluteStrokeWidth", "size", "strokeWidth"] }, { kind: "component", type: OneGridCheckboxHeaderComponent, selector: "one-grid-checkbox-header", inputs: ["api", "allowedEdit"] }, { kind: "pipe", type: ColumnSortStatePipe, name: "columnSortState" }] }); }
1572
2619
  }
1573
2620
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OneGridHeaderComponent, decorators: [{
1574
2621
  type: Component,
1575
- args: [{ standalone: false, selector: 'one-grid-header', template: "<div class=\"one-grid-header\">\n @if (enableGroupColumnDefs) {\n <div class=\"one-group-header-row\" [style.transform]=\"'translateX(-' + scrollLeft + 'px)'\">\n <ng-container *ngTemplateOutlet=\"groupColumns; context: { groups: groupColumnDefs }\"></ng-container>\n </div>\n }\n <div class=\"one-grid-header-viewport\">\n <div class=\"one-header-row\" [style.transform]=\"'translateX(-' + scrollLeft + 'px)'\">\n <ng-container *ngTemplateOutlet=\"headerColumns; context: { columns: columnDefs }\"></ng-container>\n </div>\n </div>\n</div>\n\n<!-- Group Header Cell Template -->\n<ng-template #groupColumns let-groups=\"groups\">\n @for(group of groups; track group.id) {\n <div class=\"one-group-header-cell\"\n [style.width.px]=\"group.width\"\n [ngClass]=\"group._resolvedGroupClass\"\n [ngStyle]=\"group._resolvedGroupStyle\">\n <div class=\"one-group-header-cell-text\">\n {{ group.headerName }}\n </div>\n </div>\n }\n</ng-template>\n\n<!-- Header Cell Template -->\n<ng-template #headerColumns let-columns=\"columns\">\n @for (col of columns; let colIndex = $index; track col.id) {\n <div class=\"one-grid-header-column\"\n [attr.data-index]=\"colIndex\"\n [style.width.px]=\"col.width\"\n [style.min-width.px]=\"col.minWidth\"\n [style.max-width.px]=\"col.maxWidth\"\n [draggable]=\"col?.dragable\"\n (dragstart)=\"onDragColumnStart($event, colIndex)\"\n (dragover)=\"onDragColumnOver($event, colIndex)\"\n (dragend)=\"onDragColumnEnd($event)\"\n (drop)=\"onDropColumn($event)\">\n <div class=\"one-grid-header-cell\"\n [ngClass]=\"col._resolvedHeaderClass\"\n [ngStyle]=\"col._resolvedHeaderStyle\"\n >\n <span class=\"one-grid-header-cell-group\">\n @if (col?.headerCheckboxSelection) {\n <one-grid-checkbox-header\n [api]=\"api\"\n [allowedEdit]=\"allowedEdit\"\n />\n }\n\n {{ col?.headerName ?? '' }}\n </span>\n\n <!-- Og Resize Handler -->\n @if (col?.resizable) {\n <div class=\"og-resize-handle\" (mousedown)=\"onResizeStart($event, colIndex)\">\n <i class=\"bi bi-grip-vertical\"></i>\n </div>\n }\n\n <div class=\"one-grid-header-cell-group\">\n\n <!-- Sort Menu Button -->\n @if (col?.sortable) {\n <div class=\"one-grid-header-menu-button\" (click)=\"onHeaderMenuSortClick(col.field)\">\n\n @if ((col.field | columnSortState:api.sortModel) === 'asc') {\n <lucide-icon name=\"arrow-up-a-z\" [size]=\"16\" [strokeWidth]=\"2\"></lucide-icon>\n }\n @else if ((col.field | columnSortState:api.sortModel) === 'desc') {\n <lucide-icon name=\"arrow-down-z-a\" [size]=\"16\" [strokeWidth]=\"2\"></lucide-icon>\n }\n @else {\n <lucide-icon name=\"arrow-down-up\" [size]=\"16\" [strokeWidth]=\"2\"></lucide-icon>\n }\n </div>\n }\n\n <!-- Og Option Menu Button -->\n @if (col && !col.suppressHeaderMenuButton) {\n <div class=\"one-grid-header-menu-button\" (click)=\"onHeaderMenuOptionClick(col)\">\n <i class=\"bi bi-three-dots-vertical\"></i>\n </div>\n }\n </div>\n </div>\n </div>\n }\n</ng-template>\n" }]
1576
- }], ctorParameters: () => [{ type: OneGridEventService }, { type: i0.ChangeDetectorRef }], propDecorators: { scrollLeft: [{
2622
+ args: [{ standalone: false, selector: 'one-grid-header', template: "<div class=\"one-grid-header\">\n @if (enableGroupColumnDefs) {\n <div class=\"one-group-header-row\" [style.transform]=\"'translateX(-' + scrollLeft + 'px)'\">\n <ng-container *ngTemplateOutlet=\"groupColumns; context: { groups: groupColumnDefs }\"></ng-container>\n </div>\n }\n <!-- The column row is scrolled rather than translated. `position: sticky`\n resolves against the nearest scrollport, and a transformed ancestor is\n not one \u2014 pinned headers could never hold their place inside it. The\n group row above keeps its transform: groups span contiguous columns,\n which pinning reorders, so the two features do not combine. -->\n <div #headerViewport class=\"one-grid-header-viewport\">\n <div class=\"one-header-row\">\n <ng-container *ngTemplateOutlet=\"headerColumns; context: { columns: columnDefs }\"></ng-container>\n </div>\n </div>\n</div>\n\n<!-- Group Header Cell Template -->\n<ng-template #groupColumns let-groups=\"groups\">\n @for(group of groups; track group.id) {\n <div class=\"one-group-header-cell\"\n [style.width.px]=\"group.width\"\n [ngClass]=\"group._resolvedGroupClass\"\n [ngStyle]=\"group._resolvedGroupStyle\">\n <div class=\"one-group-header-cell-text\">\n {{ group.headerName }}\n </div>\n </div>\n }\n</ng-template>\n\n<!-- Header Cell Template -->\n<ng-template #headerColumns let-columns=\"columns\">\n @for (col of columns; let colIndex = $index; track col.id) {\n <div class=\"one-grid-header-column\"\n [attr.data-index]=\"colIndex\"\n [class.one-column-pinned-left]=\"col.pinned === 'left'\"\n [class.one-column-pinned-right]=\"col.pinned === 'right'\"\n [class.one-column-pinned-edge]=\"col._pinnedEdge\"\n [class.one-column-pinned-edge]=\"col._pinnedEdge\"\n [style.left.px]=\"col.pinned === 'left' ? col._pinnedOffset : null\"\n [style.right.px]=\"col.pinned === 'right' ? col._pinnedOffset : null\"\n [style.width.px]=\"col.width\"\n [style.min-width.px]=\"col.minWidth\"\n [style.max-width.px]=\"col.maxWidth\"\n [draggable]=\"col._resolvedDragable\"\n (dragstart)=\"onDragColumnStart($event, colIndex)\"\n (dragover)=\"onDragColumnOver($event, colIndex)\"\n (dragend)=\"onDragColumnEnd($event)\"\n (drop)=\"onDropColumn($event)\">\n <div class=\"one-grid-header-cell\"\n [ngClass]=\"col._resolvedHeaderClass\"\n [ngStyle]=\"col._resolvedHeaderStyle\"\n >\n <span class=\"one-grid-header-cell-group\">\n @if (col?.headerCheckboxSelection) {\n <one-grid-checkbox-header\n [api]=\"api\"\n [allowedEdit]=\"allowedEdit\"\n />\n }\n\n {{ col?.headerName ?? '' }}\n </span>\n\n <!-- Og Resize Handler -->\n @if (col._resolvedResizable) {\n <div class=\"og-resize-handle\" (mousedown)=\"onResizeStart($event, colIndex)\">\n <i class=\"bi bi-grip-vertical\"></i>\n </div>\n }\n\n <div class=\"one-grid-header-cell-group\">\n\n <!-- Sort Menu Button -->\n @if (col._resolvedSortable) {\n <div class=\"one-grid-header-menu-button\" (click)=\"onHeaderMenuSortClick(col.field)\">\n\n @if ((col.field | columnSortState:api.sortModel) === 'asc') {\n <lucide-icon name=\"arrow-up-a-z\" [size]=\"16\" [strokeWidth]=\"2\"></lucide-icon>\n }\n @else if ((col.field | columnSortState:api.sortModel) === 'desc') {\n <lucide-icon name=\"arrow-down-z-a\" [size]=\"16\" [strokeWidth]=\"2\"></lucide-icon>\n }\n @else {\n <lucide-icon name=\"arrow-down-up\" [size]=\"16\" [strokeWidth]=\"2\"></lucide-icon>\n }\n </div>\n }\n\n <!-- Og Option Menu Button -->\n @if (col && !col._resolvedSuppressHeaderMenuButton) {\n <div class=\"one-grid-header-menu-button\" (click)=\"onHeaderMenuOptionClick(col)\">\n <i class=\"bi bi-three-dots-vertical\"></i>\n </div>\n }\n </div>\n </div>\n </div>\n }\n</ng-template>\n" }]
2623
+ }], ctorParameters: () => [{ type: OneGridEventService }, { type: i0.ChangeDetectorRef }], propDecorators: { headerViewport: [{
2624
+ type: ViewChild,
2625
+ args: ['headerViewport', { static: true }]
2626
+ }], scrollLeft: [{
1577
2627
  type: Input
1578
2628
  }], allowedEdit: [{
1579
2629
  type: Input
@@ -1654,31 +2704,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
1654
2704
  type: Output
1655
2705
  }] } });
1656
2706
 
1657
- class CellDisableResolverPipe {
1658
- transform(node, column, context, allowedEdit) {
1659
- if (!column || !node)
1660
- return false;
1661
- return resolveOneCellRendererParams(column.disable, {
1662
- node,
1663
- data: node.data,
1664
- columnDef: column,
1665
- rowIndex: node.rowIndex,
1666
- context
1667
- }) || !allowedEdit;
1668
- }
1669
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CellDisableResolverPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
1670
- static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "17.3.12", ngImport: i0, type: CellDisableResolverPipe, name: "cellDisableResolver" }); }
1671
- }
1672
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CellDisableResolverPipe, decorators: [{
1673
- type: Pipe,
1674
- args: [{
1675
- standalone: false,
1676
- name: 'cellDisableResolver'
1677
- }]
1678
- }] });
1679
-
1680
2707
  class OneGridRowActionComponent {
1681
- constructor() {
2708
+ constructor(cellService) {
2709
+ this.cellService = cellService;
1682
2710
  this.allowedEdit = false;
1683
2711
  this.masterDetail = false;
1684
2712
  this.masterNode = null;
@@ -1701,6 +2729,51 @@ class OneGridRowActionComponent {
1701
2729
  this.onRowOption = new EventEmitter();
1702
2730
  this.onRowLanguage = new EventEmitter();
1703
2731
  this.onRowCellOption = new EventEmitter();
2732
+ /**
2733
+ * Accessible names for the action buttons.
2734
+ *
2735
+ * Every one of them is an icon with no text, and none carried a label — a
2736
+ * screen reader announced "button" and nothing more, fourteen times per row.
2737
+ * These are also the `title`, so the icons are identifiable by pointer too.
2738
+ */
2739
+ this.labels = {
2740
+ gotoDetail: 'Open detail',
2741
+ addNew: 'Add new row',
2742
+ view: 'View row',
2743
+ location: 'Show location',
2744
+ document: 'Open document',
2745
+ copy: 'Copy row',
2746
+ delete: 'Delete row',
2747
+ gotoList: 'Open list',
2748
+ edit: 'Edit row',
2749
+ print: 'Print row',
2750
+ history: 'View history',
2751
+ option: 'Row options',
2752
+ language: 'Change language',
2753
+ cellOption: 'Cell options',
2754
+ };
2755
+ }
2756
+ /**
2757
+ * Whether this column's action buttons are hidden for this row.
2758
+ *
2759
+ * The templates read `column?.hideActionButton` directly, which is truthy for
2760
+ * any function — so a predicate asking to hide the buttons showed them, and
2761
+ * one asking to show them showed them too. Resolved here instead, with the
2762
+ * same params every other row-scoped predicate gets.
2763
+ */
2764
+ get hideActionButton() {
2765
+ return this.cellService.hideActionButtonFn(this.column, this.node);
2766
+ }
2767
+ /**
2768
+ * Whether this column's action buttons are disabled for this row.
2769
+ *
2770
+ * `disable` used to be resolved by `CellDisableResolverPipe`, which built its
2771
+ * own params — a different set from every other row-scoped predicate. It goes
2772
+ * through the shared builder now, so `disable` sees exactly what `editable`
2773
+ * and `hideActionButton` see.
2774
+ */
2775
+ get actionDisabled() {
2776
+ return this.cellService.disableFn(this.column, this.node) || !this.allowedEdit;
1704
2777
  }
1705
2778
  ngOnInit() {
1706
2779
  this.detailActiveExcludeLevels = new Set(this.column.cellRendererParams?.detailActiveExcludeLevels ?? []);
@@ -1811,13 +2884,13 @@ class OneGridRowActionComponent {
1811
2884
  }
1812
2885
  this.onRowCellOption.emit(data);
1813
2886
  }
1814
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OneGridRowActionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1815
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: OneGridRowActionComponent, selector: "one-grid-row-action", inputs: { column: "column", node: "node", allowedEdit: "allowedEdit", masterDetail: "masterDetail", masterNode: "masterNode", cellRendererParams: "cellRendererParams", gridType: "gridType", context: "context" }, outputs: { onRowGotoDetail: "onRowGotoDetail", onRowAddNew: "onRowAddNew", onRowView: "onRowView", onRowLocation: "onRowLocation", onRowDocument: "onRowDocument", onRowCopy: "onRowCopy", onRowDelete: "onRowDelete", onRowGotoList: "onRowGotoList", onRowEdit: "onRowEdit", onRowPrint: "onRowPrint", onRowHistory: "onRowHistory", onRowOption: "onRowOption", onRowLanguage: "onRowLanguage", onRowCellOption: "onRowCellOption" }, ngImport: i0, template: "@if ((cellRendererParams && node?.leaf && !column?.autoGroupField && !(node.group && node.leafGroup)) || (column?.rowActionLevels?.includes(node.level) && column?.autoGroupField)) {\n <div class=\"btn-action-container\">\n\n <!-- Expansion Button Status -->\n @if ((cellRendererParams?.detailActiveButtonStatus == 1 && !(detailActiveExcludeLevels?.has(node.level)))) {\n <a\n class=\"me-2\"\n [attr.aria-label]=\"node.detailActive ? 'Collapse row' : 'Expand row'\"\n [title]=\"node.detailActive ? 'Collapse' : 'Expand'\"\n (click)=\"invokeRowToggleDetailActiveExpand(node)\">\n\n <i class=\"bi\" [ngClass]=\"{ 'bi-chevron-down': node.detailActive, 'bi-chevron-right': !node.detailActive }\"></i>\n </a>\n }\n\n <!-- Goto Detail Button Status -->\n @if ((cellRendererParams?.gotoDetailButtonStatus == 1 && !(gotoDetailExcludeLevels?.has(node.level))) && (!column?.hideActionButton || allowedEdit)) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n (click)=\"invokeRowActionGotoDetail(node, column)\">\n\n <i class=\"bi bi-list-task\"></i>\n </button>\n }\n\n <!-- Add New Button Status -->\n @if ((cellRendererParams?.addNewButtonStatus == 1 && !(addNewExcludeLevels?.has(node.level))) && (!column?.hideActionButton || allowedEdit)) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n [class.disabled]=\"!allowedEdit\"\n (click)=\"invokeRowActionAddNew(node, column)\">\n\n <i class=\"bi bi-plus\"></i>\n </button>\n }\n\n <!-- View Button Status -->\n @if (cellRendererParams?.viewButtonStatus == 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n (click)=\"invokeRowActionView(node, column)\">\n\n <i class=\"bi bi-eye\"></i>\n </button>\n }\n @if (cellRendererParams?.locationButtonStatus == 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n (click)=\"invokeRowActionLocation(node, column)\">\n\n <i class=\"bi bi-geo-alt\"></i>\n </button>\n }\n\n <!-- Document Button Status -->\n @if (cellRendererParams?.documentButtonStatus == 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n (click)=\"invokeRowActionDocument(node, column)\">\n\n <i class=\"bi bi-file-earmark-text\"></i>\n </button>\n }\n\n <!-- Copy Button Status -->\n @if (cellRendererParams?.copyButtonStatus == 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n (click)=\"invokeRowActionCopy(node, column)\">\n\n <i class=\"bi bi-files\"></i>\n </button>\n }\n\n\n <!-- Delete Button Status -->\n @if (cellRendererParams?.deleteButtonStatus == 1 && (!column?.hideActionButton || allowedEdit)) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-danger\"\n [class.disabled]=\"!allowedEdit\"\n (click)=\"invokeRowActionDelete(node, column)\">\n\n <i class=\"bi bi-trash3\"></i>\n </button>\n }\n\n <!-- Goto List Button Status -->\n @if (cellRendererParams?.gotoListButtonStatus === 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-success\"\n (click)=\"invokeRowActionGotoList(node, column)\">\n\n <i class=\"bi bi-list-ul\"></i>\n </button>\n }\n\n <!-- Edit Button Status -->\n @if (cellRendererParams?.editButtonStatus === 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n [class.disabled]=\"node | cellDisableResolver:column:context:allowedEdit\"\n (click)=\"invokeRowActionEdit(node, column)\">\n\n <i class=\"bi bi-pencil\"></i>\n </button>\n }\n\n <!-- Print Button Status -->\n @if (cellRendererParams?.printButtonStatus === 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n [class.disabled]=\"node | cellDisableResolver:column:context:allowedEdit\"\n (click)=\"invokeRowActionPrint(node, column)\">\n\n <i class=\"bi bi-printer\"></i>\n </button>\n }\n\n <!-- History Button Status -->\n @if (cellRendererParams?.historyButtonStatus === 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n [class.disabled]=\"node | cellDisableResolver:column:context:allowedEdit\"\n (click)=\"invokeRowActionHistory(node, column)\">\n\n <i class=\"bi bi-clock-history\"></i>\n </button>\n }\n\n <!-- Option Button Status -->\n @if (cellRendererParams?.optionButtonStatus === 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n [class.disabled]=\"node | cellDisableResolver:column:context:allowedEdit\"\n (click)=\"invokeRowActionOption(node, column)\">\n\n <i class=\"bi bi-three-dots-vertical\"></i>\n </button>\n }\n\n <!-- Language Button Status -->\n @if (cellRendererParams?.languageButtonStatus === 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n [class.disabled]=\"node | cellDisableResolver:column:context:allowedEdit\"\n (click)=\"invokeRowActionLanguage(node, column)\">\n\n <i class=\"bi bi-translate\"></i>\n </button>\n }\n\n <!-- Cell Optoion Button Status (Context Menu) -->\n @if (cellRendererParams?.cellOptionButtonStatus === 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary border-0 text-body\"\n [style.background]=\"'transparent'\"\n [class.disabled]=\"node | cellDisableResolver:column:context:allowedEdit\"\n (click)=\"invokeRowActionCellOption(node, column)\">\n\n <i class=\"bi bi-three-dots-vertical\"></i>\n </button>\n }\n </div>\n}", styles: [""], dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "pipe", type: CellDisableResolverPipe, name: "cellDisableResolver" }] }); }
2887
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OneGridRowActionComponent, deps: [{ token: OneGridCellService }], target: i0.ɵɵFactoryTarget.Component }); }
2888
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: OneGridRowActionComponent, selector: "one-grid-row-action", inputs: { column: "column", node: "node", allowedEdit: "allowedEdit", masterDetail: "masterDetail", masterNode: "masterNode", cellRendererParams: "cellRendererParams", gridType: "gridType", context: "context" }, outputs: { onRowGotoDetail: "onRowGotoDetail", onRowAddNew: "onRowAddNew", onRowView: "onRowView", onRowLocation: "onRowLocation", onRowDocument: "onRowDocument", onRowCopy: "onRowCopy", onRowDelete: "onRowDelete", onRowGotoList: "onRowGotoList", onRowEdit: "onRowEdit", onRowPrint: "onRowPrint", onRowHistory: "onRowHistory", onRowOption: "onRowOption", onRowLanguage: "onRowLanguage", onRowCellOption: "onRowCellOption" }, ngImport: i0, template: "@if ((cellRendererParams && node?.leaf && !column?.autoGroupField && !(node.group && node.leafGroup)) || (column?.rowActionLevels?.includes(node.level) && column?.autoGroupField)) {\n <div class=\"btn-action-container\">\n\n <!-- Expansion Button Status -->\n @if ((cellRendererParams?.detailActiveButtonStatus == 1 && !(detailActiveExcludeLevels?.has(node.level)))) {\n <!-- A button, not an <a> with a click handler: it has no href, so\n it was neither focusable nor operable from the keyboard. -->\n <button\n type=\"button\"\n class=\"btn-action btn-action-plain me-2\"\n [attr.aria-expanded]=\"node.detailActive\"\n [attr.aria-label]=\"node.detailActive ? 'Collapse row' : 'Expand row'\"\n [title]=\"node.detailActive ? 'Collapse' : 'Expand'\"\n (click)=\"invokeRowToggleDetailActiveExpand(node)\">\n\n <i class=\"bi\" [ngClass]=\"{ 'bi-chevron-down': node.detailActive, 'bi-chevron-right': !node.detailActive }\"></i>\n </button>\n }\n\n <!-- Goto Detail Button Status -->\n @if ((cellRendererParams?.gotoDetailButtonStatus == 1 && !(gotoDetailExcludeLevels?.has(node.level))) && (!hideActionButton || allowedEdit)) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n [attr.aria-label]=\"labels.gotoDetail\"\n [title]=\"labels.gotoDetail\"\n (click)=\"invokeRowActionGotoDetail(node, column)\">\n\n <i class=\"bi bi-list-task\"></i>\n </button>\n }\n\n <!-- Add New Button Status -->\n @if ((cellRendererParams?.addNewButtonStatus == 1 && !(addNewExcludeLevels?.has(node.level))) && (!hideActionButton || allowedEdit)) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n [class.disabled]=\"actionDisabled\"\n [disabled]=\"actionDisabled\"\n [attr.aria-label]=\"labels.addNew\"\n [title]=\"labels.addNew\"\n (click)=\"invokeRowActionAddNew(node, column)\">\n\n <i class=\"bi bi-plus\"></i>\n </button>\n }\n\n <!-- View Button Status -->\n @if (cellRendererParams?.viewButtonStatus == 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n [attr.aria-label]=\"labels.view\"\n [title]=\"labels.view\"\n (click)=\"invokeRowActionView(node, column)\">\n\n <i class=\"bi bi-eye\"></i>\n </button>\n }\n @if (cellRendererParams?.locationButtonStatus == 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n [attr.aria-label]=\"labels.location\"\n [title]=\"labels.location\"\n (click)=\"invokeRowActionLocation(node, column)\">\n\n <i class=\"bi bi-geo-alt\"></i>\n </button>\n }\n\n <!-- Document Button Status -->\n @if (cellRendererParams?.documentButtonStatus == 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n [attr.aria-label]=\"labels.document\"\n [title]=\"labels.document\"\n (click)=\"invokeRowActionDocument(node, column)\">\n\n <i class=\"bi bi-file-earmark-text\"></i>\n </button>\n }\n\n <!-- Copy Button Status -->\n @if (cellRendererParams?.copyButtonStatus == 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n [attr.aria-label]=\"labels.copy\"\n [title]=\"labels.copy\"\n (click)=\"invokeRowActionCopy(node, column)\">\n\n <i class=\"bi bi-files\"></i>\n </button>\n }\n\n\n <!-- Delete Button Status -->\n @if (cellRendererParams?.deleteButtonStatus == 1 && (!hideActionButton || allowedEdit)) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-danger\"\n [class.disabled]=\"actionDisabled\"\n [disabled]=\"actionDisabled\"\n [attr.aria-label]=\"labels.delete\"\n [title]=\"labels.delete\"\n (click)=\"invokeRowActionDelete(node, column)\">\n\n <i class=\"bi bi-trash3\"></i>\n </button>\n }\n\n <!-- Goto List Button Status -->\n @if (cellRendererParams?.gotoListButtonStatus === 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-success\"\n [attr.aria-label]=\"labels.gotoList\"\n [title]=\"labels.gotoList\"\n (click)=\"invokeRowActionGotoList(node, column)\">\n\n <i class=\"bi bi-list-ul\"></i>\n </button>\n }\n\n <!-- Edit Button Status -->\n @if (cellRendererParams?.editButtonStatus === 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n [class.disabled]=\"actionDisabled\"\n [disabled]=\"actionDisabled\"\n [attr.aria-label]=\"labels.edit\"\n [title]=\"labels.edit\"\n (click)=\"invokeRowActionEdit(node, column)\">\n\n <i class=\"bi bi-pencil\"></i>\n </button>\n }\n\n <!-- Print Button Status -->\n @if (cellRendererParams?.printButtonStatus === 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n [class.disabled]=\"actionDisabled\"\n [disabled]=\"actionDisabled\"\n [attr.aria-label]=\"labels.print\"\n [title]=\"labels.print\"\n (click)=\"invokeRowActionPrint(node, column)\">\n\n <i class=\"bi bi-printer\"></i>\n </button>\n }\n\n <!-- History Button Status -->\n @if (cellRendererParams?.historyButtonStatus === 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n [class.disabled]=\"actionDisabled\"\n [disabled]=\"actionDisabled\"\n [attr.aria-label]=\"labels.history\"\n [title]=\"labels.history\"\n (click)=\"invokeRowActionHistory(node, column)\">\n\n <i class=\"bi bi-clock-history\"></i>\n </button>\n }\n\n <!-- Option Button Status -->\n @if (cellRendererParams?.optionButtonStatus === 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n [class.disabled]=\"actionDisabled\"\n [disabled]=\"actionDisabled\"\n [attr.aria-label]=\"labels.option\"\n [title]=\"labels.option\"\n (click)=\"invokeRowActionOption(node, column)\">\n\n <i class=\"bi bi-three-dots-vertical\"></i>\n </button>\n }\n\n <!-- Language Button Status -->\n @if (cellRendererParams?.languageButtonStatus === 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n [class.disabled]=\"actionDisabled\"\n [disabled]=\"actionDisabled\"\n [attr.aria-label]=\"labels.language\"\n [title]=\"labels.language\"\n (click)=\"invokeRowActionLanguage(node, column)\">\n\n <i class=\"bi bi-translate\"></i>\n </button>\n }\n\n <!-- Cell Optoion Button Status (Context Menu) -->\n @if (cellRendererParams?.cellOptionButtonStatus === 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary border-0 text-body\"\n [style.background]=\"'transparent'\"\n [class.disabled]=\"actionDisabled\"\n [disabled]=\"actionDisabled\"\n [attr.aria-label]=\"labels.cellOption\"\n [title]=\"labels.cellOption\"\n (click)=\"invokeRowActionCellOption(node, column)\">\n\n <i class=\"bi bi-three-dots-vertical\"></i>\n </button>\n }\n </div>\n}", styles: [""], dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }] }); }
1816
2889
  }
1817
2890
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OneGridRowActionComponent, decorators: [{
1818
2891
  type: Component,
1819
- args: [{ standalone: false, selector: 'one-grid-row-action', template: "@if ((cellRendererParams && node?.leaf && !column?.autoGroupField && !(node.group && node.leafGroup)) || (column?.rowActionLevels?.includes(node.level) && column?.autoGroupField)) {\n <div class=\"btn-action-container\">\n\n <!-- Expansion Button Status -->\n @if ((cellRendererParams?.detailActiveButtonStatus == 1 && !(detailActiveExcludeLevels?.has(node.level)))) {\n <a\n class=\"me-2\"\n [attr.aria-label]=\"node.detailActive ? 'Collapse row' : 'Expand row'\"\n [title]=\"node.detailActive ? 'Collapse' : 'Expand'\"\n (click)=\"invokeRowToggleDetailActiveExpand(node)\">\n\n <i class=\"bi\" [ngClass]=\"{ 'bi-chevron-down': node.detailActive, 'bi-chevron-right': !node.detailActive }\"></i>\n </a>\n }\n\n <!-- Goto Detail Button Status -->\n @if ((cellRendererParams?.gotoDetailButtonStatus == 1 && !(gotoDetailExcludeLevels?.has(node.level))) && (!column?.hideActionButton || allowedEdit)) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n (click)=\"invokeRowActionGotoDetail(node, column)\">\n\n <i class=\"bi bi-list-task\"></i>\n </button>\n }\n\n <!-- Add New Button Status -->\n @if ((cellRendererParams?.addNewButtonStatus == 1 && !(addNewExcludeLevels?.has(node.level))) && (!column?.hideActionButton || allowedEdit)) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n [class.disabled]=\"!allowedEdit\"\n (click)=\"invokeRowActionAddNew(node, column)\">\n\n <i class=\"bi bi-plus\"></i>\n </button>\n }\n\n <!-- View Button Status -->\n @if (cellRendererParams?.viewButtonStatus == 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n (click)=\"invokeRowActionView(node, column)\">\n\n <i class=\"bi bi-eye\"></i>\n </button>\n }\n @if (cellRendererParams?.locationButtonStatus == 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n (click)=\"invokeRowActionLocation(node, column)\">\n\n <i class=\"bi bi-geo-alt\"></i>\n </button>\n }\n\n <!-- Document Button Status -->\n @if (cellRendererParams?.documentButtonStatus == 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n (click)=\"invokeRowActionDocument(node, column)\">\n\n <i class=\"bi bi-file-earmark-text\"></i>\n </button>\n }\n\n <!-- Copy Button Status -->\n @if (cellRendererParams?.copyButtonStatus == 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n (click)=\"invokeRowActionCopy(node, column)\">\n\n <i class=\"bi bi-files\"></i>\n </button>\n }\n\n\n <!-- Delete Button Status -->\n @if (cellRendererParams?.deleteButtonStatus == 1 && (!column?.hideActionButton || allowedEdit)) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-danger\"\n [class.disabled]=\"!allowedEdit\"\n (click)=\"invokeRowActionDelete(node, column)\">\n\n <i class=\"bi bi-trash3\"></i>\n </button>\n }\n\n <!-- Goto List Button Status -->\n @if (cellRendererParams?.gotoListButtonStatus === 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-success\"\n (click)=\"invokeRowActionGotoList(node, column)\">\n\n <i class=\"bi bi-list-ul\"></i>\n </button>\n }\n\n <!-- Edit Button Status -->\n @if (cellRendererParams?.editButtonStatus === 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n [class.disabled]=\"node | cellDisableResolver:column:context:allowedEdit\"\n (click)=\"invokeRowActionEdit(node, column)\">\n\n <i class=\"bi bi-pencil\"></i>\n </button>\n }\n\n <!-- Print Button Status -->\n @if (cellRendererParams?.printButtonStatus === 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n [class.disabled]=\"node | cellDisableResolver:column:context:allowedEdit\"\n (click)=\"invokeRowActionPrint(node, column)\">\n\n <i class=\"bi bi-printer\"></i>\n </button>\n }\n\n <!-- History Button Status -->\n @if (cellRendererParams?.historyButtonStatus === 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n [class.disabled]=\"node | cellDisableResolver:column:context:allowedEdit\"\n (click)=\"invokeRowActionHistory(node, column)\">\n\n <i class=\"bi bi-clock-history\"></i>\n </button>\n }\n\n <!-- Option Button Status -->\n @if (cellRendererParams?.optionButtonStatus === 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n [class.disabled]=\"node | cellDisableResolver:column:context:allowedEdit\"\n (click)=\"invokeRowActionOption(node, column)\">\n\n <i class=\"bi bi-three-dots-vertical\"></i>\n </button>\n }\n\n <!-- Language Button Status -->\n @if (cellRendererParams?.languageButtonStatus === 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n [class.disabled]=\"node | cellDisableResolver:column:context:allowedEdit\"\n (click)=\"invokeRowActionLanguage(node, column)\">\n\n <i class=\"bi bi-translate\"></i>\n </button>\n }\n\n <!-- Cell Optoion Button Status (Context Menu) -->\n @if (cellRendererParams?.cellOptionButtonStatus === 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary border-0 text-body\"\n [style.background]=\"'transparent'\"\n [class.disabled]=\"node | cellDisableResolver:column:context:allowedEdit\"\n (click)=\"invokeRowActionCellOption(node, column)\">\n\n <i class=\"bi bi-three-dots-vertical\"></i>\n </button>\n }\n </div>\n}" }]
1820
- }], propDecorators: { column: [{
2892
+ args: [{ standalone: false, selector: 'one-grid-row-action', template: "@if ((cellRendererParams && node?.leaf && !column?.autoGroupField && !(node.group && node.leafGroup)) || (column?.rowActionLevels?.includes(node.level) && column?.autoGroupField)) {\n <div class=\"btn-action-container\">\n\n <!-- Expansion Button Status -->\n @if ((cellRendererParams?.detailActiveButtonStatus == 1 && !(detailActiveExcludeLevels?.has(node.level)))) {\n <!-- A button, not an <a> with a click handler: it has no href, so\n it was neither focusable nor operable from the keyboard. -->\n <button\n type=\"button\"\n class=\"btn-action btn-action-plain me-2\"\n [attr.aria-expanded]=\"node.detailActive\"\n [attr.aria-label]=\"node.detailActive ? 'Collapse row' : 'Expand row'\"\n [title]=\"node.detailActive ? 'Collapse' : 'Expand'\"\n (click)=\"invokeRowToggleDetailActiveExpand(node)\">\n\n <i class=\"bi\" [ngClass]=\"{ 'bi-chevron-down': node.detailActive, 'bi-chevron-right': !node.detailActive }\"></i>\n </button>\n }\n\n <!-- Goto Detail Button Status -->\n @if ((cellRendererParams?.gotoDetailButtonStatus == 1 && !(gotoDetailExcludeLevels?.has(node.level))) && (!hideActionButton || allowedEdit)) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n [attr.aria-label]=\"labels.gotoDetail\"\n [title]=\"labels.gotoDetail\"\n (click)=\"invokeRowActionGotoDetail(node, column)\">\n\n <i class=\"bi bi-list-task\"></i>\n </button>\n }\n\n <!-- Add New Button Status -->\n @if ((cellRendererParams?.addNewButtonStatus == 1 && !(addNewExcludeLevels?.has(node.level))) && (!hideActionButton || allowedEdit)) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n [class.disabled]=\"actionDisabled\"\n [disabled]=\"actionDisabled\"\n [attr.aria-label]=\"labels.addNew\"\n [title]=\"labels.addNew\"\n (click)=\"invokeRowActionAddNew(node, column)\">\n\n <i class=\"bi bi-plus\"></i>\n </button>\n }\n\n <!-- View Button Status -->\n @if (cellRendererParams?.viewButtonStatus == 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n [attr.aria-label]=\"labels.view\"\n [title]=\"labels.view\"\n (click)=\"invokeRowActionView(node, column)\">\n\n <i class=\"bi bi-eye\"></i>\n </button>\n }\n @if (cellRendererParams?.locationButtonStatus == 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n [attr.aria-label]=\"labels.location\"\n [title]=\"labels.location\"\n (click)=\"invokeRowActionLocation(node, column)\">\n\n <i class=\"bi bi-geo-alt\"></i>\n </button>\n }\n\n <!-- Document Button Status -->\n @if (cellRendererParams?.documentButtonStatus == 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n [attr.aria-label]=\"labels.document\"\n [title]=\"labels.document\"\n (click)=\"invokeRowActionDocument(node, column)\">\n\n <i class=\"bi bi-file-earmark-text\"></i>\n </button>\n }\n\n <!-- Copy Button Status -->\n @if (cellRendererParams?.copyButtonStatus == 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n [attr.aria-label]=\"labels.copy\"\n [title]=\"labels.copy\"\n (click)=\"invokeRowActionCopy(node, column)\">\n\n <i class=\"bi bi-files\"></i>\n </button>\n }\n\n\n <!-- Delete Button Status -->\n @if (cellRendererParams?.deleteButtonStatus == 1 && (!hideActionButton || allowedEdit)) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-danger\"\n [class.disabled]=\"actionDisabled\"\n [disabled]=\"actionDisabled\"\n [attr.aria-label]=\"labels.delete\"\n [title]=\"labels.delete\"\n (click)=\"invokeRowActionDelete(node, column)\">\n\n <i class=\"bi bi-trash3\"></i>\n </button>\n }\n\n <!-- Goto List Button Status -->\n @if (cellRendererParams?.gotoListButtonStatus === 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-success\"\n [attr.aria-label]=\"labels.gotoList\"\n [title]=\"labels.gotoList\"\n (click)=\"invokeRowActionGotoList(node, column)\">\n\n <i class=\"bi bi-list-ul\"></i>\n </button>\n }\n\n <!-- Edit Button Status -->\n @if (cellRendererParams?.editButtonStatus === 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n [class.disabled]=\"actionDisabled\"\n [disabled]=\"actionDisabled\"\n [attr.aria-label]=\"labels.edit\"\n [title]=\"labels.edit\"\n (click)=\"invokeRowActionEdit(node, column)\">\n\n <i class=\"bi bi-pencil\"></i>\n </button>\n }\n\n <!-- Print Button Status -->\n @if (cellRendererParams?.printButtonStatus === 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n [class.disabled]=\"actionDisabled\"\n [disabled]=\"actionDisabled\"\n [attr.aria-label]=\"labels.print\"\n [title]=\"labels.print\"\n (click)=\"invokeRowActionPrint(node, column)\">\n\n <i class=\"bi bi-printer\"></i>\n </button>\n }\n\n <!-- History Button Status -->\n @if (cellRendererParams?.historyButtonStatus === 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n [class.disabled]=\"actionDisabled\"\n [disabled]=\"actionDisabled\"\n [attr.aria-label]=\"labels.history\"\n [title]=\"labels.history\"\n (click)=\"invokeRowActionHistory(node, column)\">\n\n <i class=\"bi bi-clock-history\"></i>\n </button>\n }\n\n <!-- Option Button Status -->\n @if (cellRendererParams?.optionButtonStatus === 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n [class.disabled]=\"actionDisabled\"\n [disabled]=\"actionDisabled\"\n [attr.aria-label]=\"labels.option\"\n [title]=\"labels.option\"\n (click)=\"invokeRowActionOption(node, column)\">\n\n <i class=\"bi bi-three-dots-vertical\"></i>\n </button>\n }\n\n <!-- Language Button Status -->\n @if (cellRendererParams?.languageButtonStatus === 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary\"\n [class.disabled]=\"actionDisabled\"\n [disabled]=\"actionDisabled\"\n [attr.aria-label]=\"labels.language\"\n [title]=\"labels.language\"\n (click)=\"invokeRowActionLanguage(node, column)\">\n\n <i class=\"bi bi-translate\"></i>\n </button>\n }\n\n <!-- Cell Optoion Button Status (Context Menu) -->\n @if (cellRendererParams?.cellOptionButtonStatus === 1) {\n <button\n type=\"button\"\n class=\"btn-action btn-action-primary border-0 text-body\"\n [style.background]=\"'transparent'\"\n [class.disabled]=\"actionDisabled\"\n [disabled]=\"actionDisabled\"\n [attr.aria-label]=\"labels.cellOption\"\n [title]=\"labels.cellOption\"\n (click)=\"invokeRowActionCellOption(node, column)\">\n\n <i class=\"bi bi-three-dots-vertical\"></i>\n </button>\n }\n </div>\n}" }]
2893
+ }], ctorParameters: () => [{ type: OneGridCellService }], propDecorators: { column: [{
1821
2894
  type: Input,
1822
2895
  args: [{ required: true }]
1823
2896
  }], node: [{
@@ -1871,18 +2944,21 @@ class OneGridCellGroupRendererComponent {
1871
2944
  this.masterDetail = false;
1872
2945
  this.expansionChanged = new EventEmitter();
1873
2946
  }
1874
- invokeChangedClick() {
1875
- // if (this.masterDetail && !this.node.expanded) {
1876
- // this.detailCellRendererParams.getDetailRowData({ data: this.node.data, node: this.node, successCallback: (rows) => { this.node.setDetailRowData(rows) }});
1877
- // }
2947
+ /**
2948
+ * Presentational only: report the click and let the body, which holds the
2949
+ * API, perform the toggle. The click is stopped from bubbling so it is not
2950
+ * also read as a cell click by the cell underneath.
2951
+ */
2952
+ invokeChangedClick(event) {
2953
+ event.stopPropagation();
1878
2954
  this.expansionChanged.emit({ rowIndex: this.node.rowIndex, data: this.node.data, node: this.node });
1879
2955
  }
1880
2956
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OneGridCellGroupRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1881
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: OneGridCellGroupRendererComponent, selector: "one-grid-cell-group-renderer", inputs: { node: "node", masterDetail: "masterDetail", detailCellRendererParams: "detailCellRendererParams", detailRendererComponent: "detailRendererComponent" }, outputs: { expansionChanged: "expansionChanged" }, ngImport: i0, template: "@if (node.expandable) {\n <span class=\"cell-group-renderer\" [style.padding-left.px]=\"6\" (click)=\"invokeChangedClick()\">\n @if (node.children?.length > 0 || masterDetail) {\n <i [class.node-expanded]=\"node.expanded\" class=\"bi bi-chevron-right expansion-icon\"></i>\n }\n </span>\n}\n", styles: [""] }); }
2957
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: OneGridCellGroupRendererComponent, selector: "one-grid-cell-group-renderer", inputs: { node: "node", masterDetail: "masterDetail", detailCellRendererParams: "detailCellRendererParams", detailRendererComponent: "detailRendererComponent" }, outputs: { expansionChanged: "expansionChanged" }, ngImport: i0, template: "@if (node.expandable) {\n <span class=\"cell-group-renderer\" [style.padding-left.px]=\"6\">\n @if (node.children?.length > 0 || masterDetail) {\n <!-- A real button, not a div with a click handler: this is a\n disclosure control, so it needs to be focusable, operable with\n Enter and Space, and to expose its state via aria-expanded.\n mousedown is stopped so pressing the expander does not also\n begin a cell range selection on the cell underneath. -->\n <button\n type=\"button\"\n class=\"expansion-button\"\n [attr.aria-expanded]=\"node.expanded\"\n [attr.aria-label]=\"node.expanded ? 'Collapse row' : 'Expand row'\"\n (mousedown)=\"$event.stopPropagation()\"\n (dblclick)=\"$event.stopPropagation()\"\n (click)=\"invokeChangedClick($event)\">\n <i [class.node-expanded]=\"node.expanded\" class=\"bi bi-chevron-right expansion-icon\"></i>\n </button>\n }\n </span>\n}\n", styles: [""] }); }
1882
2958
  }
1883
2959
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OneGridCellGroupRendererComponent, decorators: [{
1884
2960
  type: Component,
1885
- args: [{ standalone: false, selector: 'one-grid-cell-group-renderer', template: "@if (node.expandable) {\n <span class=\"cell-group-renderer\" [style.padding-left.px]=\"6\" (click)=\"invokeChangedClick()\">\n @if (node.children?.length > 0 || masterDetail) {\n <i [class.node-expanded]=\"node.expanded\" class=\"bi bi-chevron-right expansion-icon\"></i>\n }\n </span>\n}\n" }]
2961
+ args: [{ standalone: false, selector: 'one-grid-cell-group-renderer', template: "@if (node.expandable) {\n <span class=\"cell-group-renderer\" [style.padding-left.px]=\"6\">\n @if (node.children?.length > 0 || masterDetail) {\n <!-- A real button, not a div with a click handler: this is a\n disclosure control, so it needs to be focusable, operable with\n Enter and Space, and to expose its state via aria-expanded.\n mousedown is stopped so pressing the expander does not also\n begin a cell range selection on the cell underneath. -->\n <button\n type=\"button\"\n class=\"expansion-button\"\n [attr.aria-expanded]=\"node.expanded\"\n [attr.aria-label]=\"node.expanded ? 'Collapse row' : 'Expand row'\"\n (mousedown)=\"$event.stopPropagation()\"\n (dblclick)=\"$event.stopPropagation()\"\n (click)=\"invokeChangedClick($event)\">\n <i [class.node-expanded]=\"node.expanded\" class=\"bi bi-chevron-right expansion-icon\"></i>\n </button>\n }\n </span>\n}\n" }]
1886
2962
  }], propDecorators: { node: [{
1887
2963
  type: Input
1888
2964
  }], masterDetail: [{
@@ -2000,8 +3076,15 @@ class OneCellRendererComponentDirective {
2000
3076
  return;
2001
3077
  const instance = this.compRef.instance;
2002
3078
  if (typeof instance.oneInit === 'function') {
3079
+ // The body supplies `value` already, and it honours the column's
3080
+ // valueGetter. Only derive it here when nothing did — this used to
3081
+ // overwrite unconditionally with the raw field, so a column with both a
3082
+ // valueGetter and a component renderer showed the underlying field
3083
+ // instead of the value every other part of the grid displays.
2003
3084
  const newParams = { ...this.params };
2004
- newParams.value = getFieldValue(this.params.data, this.params?.columnDef?.field) ?? null;
3085
+ if (newParams.value === undefined) {
3086
+ newParams.value = getFieldValue(this.params.data, this.params?.columnDef?.field) ?? null;
3087
+ }
2005
3088
  instance.oneInit(newParams);
2006
3089
  }
2007
3090
  else {
@@ -2038,68 +3121,53 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
2038
3121
  args: [{ required: true }]
2039
3122
  }] } });
2040
3123
 
3124
+ /**
3125
+ * Reveals a truncated cell in full while the pointer rests on it.
3126
+ *
3127
+ * Presentation lives in the stylesheet, under `.one-cell-text-expanded`. It
3128
+ * used to be applied as inline styles here, one of which was
3129
+ * `background: var(--ins-card-bg)` — a token from the application this library
3130
+ * was extracted from, which no consumer defines. The expanded cell therefore
3131
+ * had a transparent background and its text overlapped the next column. The
3132
+ * inline rules also set `z-index` on a statically positioned element, where it
3133
+ * does nothing.
3134
+ *
3135
+ * Overflow is measured on hover rather than after every view check. The old
3136
+ * version measured on init and on every text change, and reading `scrollWidth`
3137
+ * forces a synchronous layout — several hundred of them per render for a
3138
+ * normal viewport, to answer a question only the hovered cell ever asks.
3139
+ */
2041
3140
  class OneCellTextExpandDirective {
3141
+ static { this.EXPANDED_CLASS = 'one-cell-text-expanded'; }
3142
+ static { this.HOVER_DELAY_MS = 800; }
2042
3143
  constructor(cell, renderer) {
2043
3144
  this.cell = cell;
2044
3145
  this.renderer = renderer;
2045
- this.overflowing = false;
2046
- this.lastText = null;
2047
- }
2048
- ngAfterViewInit() {
2049
- this.checkOverflow();
2050
- this.lastText = this.cell.nativeElement.textContent;
2051
- }
2052
- ngAfterViewChecked() {
2053
- const currentText = this.cell.nativeElement.textContent;
2054
- if (this.lastText !== currentText) {
2055
- this.lastText = currentText;
2056
- this.checkOverflow();
2057
- this.collapse();
2058
- }
2059
- }
2060
- ngOnDestroy() {
2061
- clearTimeout(this.timer);
2062
- this.collapse();
2063
- }
2064
- checkOverflow() {
2065
- const cell = this.cell.nativeElement;
2066
- this.overflowing = cell.scrollWidth > cell.clientWidth;
2067
- }
2068
- onResize() {
2069
- this.checkOverflow();
2070
3146
  }
2071
3147
  onMouseEnter() {
2072
- if (!this.overflowing)
3148
+ const element = this.cell.nativeElement;
3149
+ // Measured here, so the layout read happens once for the cell being
3150
+ // hovered instead of for every cell on screen.
3151
+ if (element.scrollWidth <= element.clientWidth)
2073
3152
  return;
2074
- this.timer = setTimeout(() => this.expand(), 800);
3153
+ this.timer = setTimeout(() => this.expand(), OneCellTextExpandDirective.HOVER_DELAY_MS);
2075
3154
  }
2076
3155
  onMouseLeave() {
2077
- clearTimeout(this.timer);
3156
+ this.collapse();
3157
+ }
3158
+ ngOnDestroy() {
2078
3159
  this.collapse();
2079
3160
  }
2080
3161
  expand() {
2081
- const cell = this.cell.nativeElement;
2082
- this.renderer.setStyle(cell, 'overflow', 'visible');
2083
- this.renderer.setStyle(cell, 'text-overflow', 'unset');
2084
- this.renderer.setStyle(cell, 'z-index', '1000');
2085
- this.renderer.setStyle(cell, 'background', 'var(--ins-card-bg)');
2086
- this.renderer.setStyle(cell, 'white-space', 'nowrap');
2087
- this.renderer.setStyle(cell, 'transition', 'width 0.3s ease');
2088
- this.renderer.setStyle(cell, 'padding', '4px 8px');
2089
- this.renderer.setStyle(cell, 'border-radius', '4px');
3162
+ this.renderer.addClass(this.cell.nativeElement, OneCellTextExpandDirective.EXPANDED_CLASS);
2090
3163
  }
2091
3164
  collapse() {
2092
- const cell = this.cell.nativeElement;
2093
- this.renderer.setStyle(cell, 'white-space', 'nowrap');
2094
- this.renderer.setStyle(cell, 'overflow', 'hidden');
2095
- this.renderer.setStyle(cell, 'text-overflow', 'ellipsis');
2096
- this.renderer.removeStyle(cell, 'z-index');
2097
- this.renderer.removeStyle(cell, 'background');
2098
- this.renderer.removeStyle(cell, 'padding');
2099
- this.renderer.removeStyle(cell, 'border-radius');
3165
+ clearTimeout(this.timer);
3166
+ this.timer = undefined;
3167
+ this.renderer.removeClass(this.cell.nativeElement, OneCellTextExpandDirective.EXPANDED_CLASS);
2100
3168
  }
2101
3169
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OneCellTextExpandDirective, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Directive }); }
2102
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "17.3.12", type: OneCellTextExpandDirective, selector: "[oneCellTextExpand]", host: { listeners: { "window:resize": "onResize()", "mouseenter": "onMouseEnter()", "mouseleave": "onMouseLeave()" } }, ngImport: i0 }); }
3170
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "17.3.12", type: OneCellTextExpandDirective, selector: "[oneCellTextExpand]", host: { listeners: { "mouseenter": "onMouseEnter()", "mouseleave": "onMouseLeave()" } }, ngImport: i0 }); }
2103
3171
  }
2104
3172
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OneCellTextExpandDirective, decorators: [{
2105
3173
  type: Directive,
@@ -2107,10 +3175,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
2107
3175
  standalone: false,
2108
3176
  selector: '[oneCellTextExpand]'
2109
3177
  }]
2110
- }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }], propDecorators: { onResize: [{
2111
- type: HostListener,
2112
- args: ['window:resize']
2113
- }], onMouseEnter: [{
3178
+ }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }], propDecorators: { onMouseEnter: [{
2114
3179
  type: HostListener,
2115
3180
  args: ['mouseenter']
2116
3181
  }], onMouseLeave: [{
@@ -2119,6 +3184,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
2119
3184
  }] } });
2120
3185
 
2121
3186
  class OneGridCellOptionMenuComponent {
3187
+ get menuLabel() {
3188
+ return `Options for ${this.column?.headerName ?? this.column?.field ?? 'this cell'}`;
3189
+ }
2122
3190
  constructor(elementRef) {
2123
3191
  this.elementRef = elementRef;
2124
3192
  this.column = null;
@@ -2126,6 +3194,56 @@ class OneGridCellOptionMenuComponent {
2126
3194
  this.options = [];
2127
3195
  this.cellOptionMenuSelect = new EventEmitter();
2128
3196
  this.cellOptionMenuClose = new EventEmitter();
3197
+ /**
3198
+ * Roving tabindex: one item is tabbable and the arrows move which. Standard
3199
+ * for a menu — a list of separately tabbable buttons makes a keyboard user
3200
+ * tab through every option to get past the menu.
3201
+ */
3202
+ this.activeIndex = 0;
3203
+ }
3204
+ /**
3205
+ * Move focus into the menu once it exists.
3206
+ *
3207
+ * Opening a menu and leaving focus on the trigger strands a keyboard user:
3208
+ * the menu is on screen and nothing they press reaches it.
3209
+ */
3210
+ ngAfterViewInit() {
3211
+ this.focusItem(0);
3212
+ }
3213
+ onItemKeydown(event, index, option) {
3214
+ switch (event.key) {
3215
+ case 'ArrowDown':
3216
+ event.preventDefault();
3217
+ this.focusItem((index + 1) % this.options.length);
3218
+ return;
3219
+ case 'ArrowUp':
3220
+ event.preventDefault();
3221
+ this.focusItem((index - 1 + this.options.length) % this.options.length);
3222
+ return;
3223
+ case 'Home':
3224
+ event.preventDefault();
3225
+ this.focusItem(0);
3226
+ return;
3227
+ case 'End':
3228
+ event.preventDefault();
3229
+ this.focusItem(this.options.length - 1);
3230
+ return;
3231
+ case 'Enter':
3232
+ case ' ':
3233
+ // A <button> already activates on both, but only once the default runs.
3234
+ // Handling it here keeps the menu's behaviour independent of that.
3235
+ event.preventDefault();
3236
+ this.onOptionSelected(option);
3237
+ return;
3238
+ default:
3239
+ return;
3240
+ }
3241
+ }
3242
+ focusItem(index) {
3243
+ if (!this.options?.length)
3244
+ return;
3245
+ this.activeIndex = index;
3246
+ this.items?.get(index)?.nativeElement?.focus();
2129
3247
  }
2130
3248
  // Click outside
2131
3249
  onDocumentClick(event) {
@@ -2157,11 +3275,11 @@ class OneGridCellOptionMenuComponent {
2157
3275
  this.cellOptionMenuClose.emit();
2158
3276
  }
2159
3277
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OneGridCellOptionMenuComponent, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component }); }
2160
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: OneGridCellOptionMenuComponent, selector: "one-grid-cell-option-menu", inputs: { column: "column", node: "node", options: "options" }, outputs: { cellOptionMenuSelect: "cellOptionMenuSelect", cellOptionMenuClose: "cellOptionMenuClose" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape()", "document:scroll": "onScroll()" } }, ngImport: i0, template: "<div class=\"cell-option-menu\" #menuEl>\n @for (opt of options; track opt.value) {\n <div class=\"cell-option-menu-item\" (click)=\"onOptionSelected(opt)\">\n <!-- <span [style.padding-right.px]=\"8\"><lucide-icon name=\"check\" [size]=\"16\" [strokeWidth]=\"2\"></lucide-icon></span> -->\n <span>{{ opt.text }}</span> \n </div>\n }\n</div>", styles: [""] }); }
3278
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: OneGridCellOptionMenuComponent, selector: "one-grid-cell-option-menu", inputs: { column: "column", node: "node", options: "options" }, outputs: { cellOptionMenuSelect: "cellOptionMenuSelect", cellOptionMenuClose: "cellOptionMenuClose" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape()", "document:scroll": "onScroll()" } }, viewQueries: [{ propertyName: "items", predicate: ["item"], descendants: true }], ngImport: i0, template: "<!-- A menu, so it is announced as one and operable from the keyboard: the items\n were `<div (click)>` with no role, which meant the menu could be opened by\n mouse and then not used at all without one. -->\n<div class=\"cell-option-menu\" role=\"menu\" #menuEl [attr.aria-label]=\"menuLabel\">\n @for (opt of options; let index = $index; track opt.value) {\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"cell-option-menu-item\"\n #item\n [tabindex]=\"index === activeIndex ? 0 : -1\"\n (click)=\"onOptionSelected(opt)\"\n (keydown)=\"onItemKeydown($event, index, opt)\">\n <span>{{ opt.text }}</span>\n </button>\n }\n</div>\n", styles: [""] }); }
2161
3279
  }
2162
3280
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OneGridCellOptionMenuComponent, decorators: [{
2163
3281
  type: Component,
2164
- args: [{ selector: 'one-grid-cell-option-menu', standalone: false, template: "<div class=\"cell-option-menu\" #menuEl>\n @for (opt of options; track opt.value) {\n <div class=\"cell-option-menu-item\" (click)=\"onOptionSelected(opt)\">\n <!-- <span [style.padding-right.px]=\"8\"><lucide-icon name=\"check\" [size]=\"16\" [strokeWidth]=\"2\"></lucide-icon></span> -->\n <span>{{ opt.text }}</span> \n </div>\n }\n</div>" }]
3282
+ args: [{ selector: 'one-grid-cell-option-menu', standalone: false, template: "<!-- A menu, so it is announced as one and operable from the keyboard: the items\n were `<div (click)>` with no role, which meant the menu could be opened by\n mouse and then not used at all without one. -->\n<div class=\"cell-option-menu\" role=\"menu\" #menuEl [attr.aria-label]=\"menuLabel\">\n @for (opt of options; let index = $index; track opt.value) {\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"cell-option-menu-item\"\n #item\n [tabindex]=\"index === activeIndex ? 0 : -1\"\n (click)=\"onOptionSelected(opt)\"\n (keydown)=\"onItemKeydown($event, index, opt)\">\n <span>{{ opt.text }}</span>\n </button>\n }\n</div>\n" }]
2165
3283
  }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { column: [{
2166
3284
  type: Input
2167
3285
  }], node: [{
@@ -2172,6 +3290,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
2172
3290
  type: Output
2173
3291
  }], cellOptionMenuClose: [{
2174
3292
  type: Output
3293
+ }], items: [{
3294
+ type: ViewChildren,
3295
+ args: ['item']
2175
3296
  }], onDocumentClick: [{
2176
3297
  type: HostListener,
2177
3298
  args: ['document:click', ['$event']]
@@ -2183,6 +3304,112 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
2183
3304
  args: ['document:scroll']
2184
3305
  }] } });
2185
3306
 
3307
+ /** Why the handle is disabled, in words a user can act on. */
3308
+ const SUPPRESSED_TEXT = {
3309
+ sorted: 'Rows cannot be reordered while a sort is applied. Clear the sort to reorder.',
3310
+ filtered: 'Rows cannot be reordered while a filter is applied. Clear the filter to reorder.',
3311
+ treeData: 'Rows cannot be reordered in a tree grid.',
3312
+ };
3313
+ /**
3314
+ * The grab handle for one row.
3315
+ *
3316
+ * A real `<button>`, not a styled div: it has to be focusable and operable from
3317
+ * the keyboard, because WCAG 2.5.7 requires everything achievable by dragging
3318
+ * to have a non-dragging alternative. Space picks the row up, the arrows move
3319
+ * it, space drops it and escape puts it back.
3320
+ *
3321
+ * When reordering is unavailable the handle is disabled and says why, rather
3322
+ * than disappearing — a control that vanishes when the user sorts looks like a
3323
+ * bug, and one that silently does nothing is worse.
3324
+ */
3325
+ class OneGridRowDragHandleComponent {
3326
+ constructor(dragService) {
3327
+ this.dragService = dragService;
3328
+ /** Non-null when the grid's order is not its own; see the model docs. */
3329
+ this.suppressedReason = null;
3330
+ this.dragEnd = new EventEmitter();
3331
+ }
3332
+ get disabled() {
3333
+ return this.suppressedReason !== null;
3334
+ }
3335
+ get label() {
3336
+ if (this.suppressedReason)
3337
+ return SUPPRESSED_TEXT[this.suppressedReason];
3338
+ return this.grabbed
3339
+ ? 'Moving row. Arrow keys to move, space to drop, escape to cancel.'
3340
+ : 'Drag to reorder row, or press space to move it with the arrow keys.';
3341
+ }
3342
+ /** True while this specific row is the one being moved. */
3343
+ get grabbed() {
3344
+ return this.dragService.state?.rowId === this.node.rowId;
3345
+ }
3346
+ onPointerDown(event) {
3347
+ if (this.disabled)
3348
+ return;
3349
+ // The body starts a cell range selection on mousedown; the handle is not
3350
+ // part of that, and a press here must not also select.
3351
+ event.stopPropagation();
3352
+ this.dragService.startPointerDrag(event, this.node, { onEnd: (result) => this.emit(result) });
3353
+ }
3354
+ onKeyDown(event) {
3355
+ if (this.disabled)
3356
+ return;
3357
+ switch (event.key) {
3358
+ case ' ':
3359
+ case 'Enter':
3360
+ event.preventDefault();
3361
+ this.dragService.toggleKeyboardDrag(this.node, { onEnd: (result) => this.emit(result) });
3362
+ return;
3363
+ case 'ArrowUp':
3364
+ if (!this.grabbed)
3365
+ return;
3366
+ event.preventDefault();
3367
+ this.dragService.moveKeyboard(-1);
3368
+ return;
3369
+ case 'ArrowDown':
3370
+ if (!this.grabbed)
3371
+ return;
3372
+ event.preventDefault();
3373
+ this.dragService.moveKeyboard(1);
3374
+ return;
3375
+ case 'Escape':
3376
+ if (!this.grabbed)
3377
+ return;
3378
+ event.preventDefault();
3379
+ this.dragService.cancel();
3380
+ return;
3381
+ default:
3382
+ return;
3383
+ }
3384
+ }
3385
+ /**
3386
+ * A row that is picked up and dropped where it started produces no event.
3387
+ * Blur while grabbed would otherwise strand the row mid-move, so it cancels.
3388
+ */
3389
+ onBlur() {
3390
+ if (this.grabbed && this.dragService.state?.source === 'keyboard') {
3391
+ this.dragService.cancel();
3392
+ }
3393
+ }
3394
+ emit(result) {
3395
+ if (result)
3396
+ this.dragEnd.emit(result);
3397
+ }
3398
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OneGridRowDragHandleComponent, deps: [{ token: OneGridRowDragService }], target: i0.ɵɵFactoryTarget.Component }); }
3399
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: OneGridRowDragHandleComponent, selector: "one-grid-row-drag-handle", inputs: { node: "node", suppressedReason: "suppressedReason" }, outputs: { dragEnd: "dragEnd" }, ngImport: i0, template: "<button\n type=\"button\"\n class=\"one-row-drag-handle\"\n [class.one-row-drag-handle-grabbed]=\"grabbed\"\n [disabled]=\"disabled\"\n [attr.aria-label]=\"label\"\n [attr.aria-disabled]=\"disabled ? 'true' : null\"\n [attr.aria-pressed]=\"disabled ? null : grabbed\"\n [title]=\"label\"\n (pointerdown)=\"onPointerDown($event)\"\n (keydown)=\"onKeyDown($event)\"\n (blur)=\"onBlur()\"\n (click)=\"$event.stopPropagation()\"\n (dblclick)=\"$event.stopPropagation()\">\n <lucide-icon name=\"grip-vertical\" [size]=\"14\" [strokeWidth]=\"2\"></lucide-icon>\n</button>\n", styles: [":host{display:inline-flex;align-items:center}\n"], dependencies: [{ kind: "component", type: i2$1.LucideAngularComponent, selector: "lucide-angular, lucide-icon, i-lucide, span-lucide", inputs: ["class", "name", "img", "color", "absoluteStrokeWidth", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
3400
+ }
3401
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OneGridRowDragHandleComponent, decorators: [{
3402
+ type: Component,
3403
+ args: [{ standalone: false, selector: 'one-grid-row-drag-handle', changeDetection: ChangeDetectionStrategy.OnPush, template: "<button\n type=\"button\"\n class=\"one-row-drag-handle\"\n [class.one-row-drag-handle-grabbed]=\"grabbed\"\n [disabled]=\"disabled\"\n [attr.aria-label]=\"label\"\n [attr.aria-disabled]=\"disabled ? 'true' : null\"\n [attr.aria-pressed]=\"disabled ? null : grabbed\"\n [title]=\"label\"\n (pointerdown)=\"onPointerDown($event)\"\n (keydown)=\"onKeyDown($event)\"\n (blur)=\"onBlur()\"\n (click)=\"$event.stopPropagation()\"\n (dblclick)=\"$event.stopPropagation()\">\n <lucide-icon name=\"grip-vertical\" [size]=\"14\" [strokeWidth]=\"2\"></lucide-icon>\n</button>\n", styles: [":host{display:inline-flex;align-items:center}\n"] }]
3404
+ }], ctorParameters: () => [{ type: OneGridRowDragService }], propDecorators: { node: [{
3405
+ type: Input,
3406
+ args: [{ required: true }]
3407
+ }], suppressedReason: [{
3408
+ type: Input
3409
+ }], dragEnd: [{
3410
+ type: Output
3411
+ }] } });
3412
+
2186
3413
  class CellInvalidPipe {
2187
3414
  transform(cellStates, value, column, node, allowedEdit, submitted) {
2188
3415
  if (!column.validationRules?.length) {
@@ -2294,10 +3521,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
2294
3521
  }] });
2295
3522
 
2296
3523
  class OneGridBodyComponent {
2297
- constructor(colService, cellService, eventService, cdr) {
3524
+ constructor(colService, cellService, eventService, rowDragService, cdr) {
2298
3525
  this.colService = colService;
2299
3526
  this.cellService = cellService;
2300
3527
  this.eventService = eventService;
3528
+ this.rowDragService = rowDragService;
2301
3529
  this.cdr = cdr;
2302
3530
  this.rowData = [];
2303
3531
  this.pagination = false;
@@ -2305,6 +3533,14 @@ class OneGridBodyComponent {
2305
3533
  this.enableGroupColumnDefs = false;
2306
3534
  this.submitted = false;
2307
3535
  this.context = null;
3536
+ /**
3537
+ * Pinned rows are bound only so the component re-renders when they change;
3538
+ * the nodes themselves come from the API, which owns them.
3539
+ */
3540
+ this.pinnedTopRowData = [];
3541
+ this.pinnedBottomRowData = [];
3542
+ this.pinnedTopNodes = [];
3543
+ this.pinnedBottomNodes = [];
2308
3544
  this.columnDefs = [];
2309
3545
  this.cellRenderer = false;
2310
3546
  /**
@@ -2345,11 +3581,19 @@ class OneGridBodyComponent {
2345
3581
  this.expansionChanged = new EventEmitter();
2346
3582
  this.bodyScroll = new EventEmitter();
2347
3583
  this.cellOptionSelect = new EventEmitter();
3584
+ this.rowDragEnd = new EventEmitter();
2348
3585
  /**
2349
3586
  * Selection Range Variables
2350
3587
  */
2351
3588
  this.rowWidth = 0;
2352
3589
  this.nodes = [];
3590
+ /**
3591
+ * Row drag state, mirrored from the service so the template can read it
3592
+ * synchronously. `null` whenever no drag is in flight.
3593
+ */
3594
+ this.dragState = null;
3595
+ this.dragAnnouncement = '';
3596
+ this.rowDragSuppressed = null;
2353
3597
  this.selectedCells = new Set();
2354
3598
  this.isMouseDown = false;
2355
3599
  this.startCell = null;
@@ -2377,6 +3621,40 @@ class OneGridBodyComponent {
2377
3621
  this.itemSize = 30;
2378
3622
  this.minBufferPx = 200;
2379
3623
  this.maxBufferPx = 400;
3624
+ /**
3625
+ * Virtual Scroll Properties
3626
+ *
3627
+ * The buffer scales with the row count so small grids render in one pass, but
3628
+ * it is capped: `itemSize * items * 1.5` is 1.5x the *total* content height,
3629
+ * and CdkFixedSizeVirtualScroll expands its rendered range until the buffer is
3630
+ * satisfied. Uncapped, that makes the viewport materialise every row and
3631
+ * virtual scrolling stops doing anything at all (100k rows locked the tab).
3632
+ * MAX_BUFFER_PX keeps roughly twenty rows of overscan either side instead.
3633
+ */
3634
+ this.MAX_BUFFER_PX = 600;
3635
+ /**
3636
+ * Where to paint the drop indicator, in pixels from the top of the body.
3637
+ *
3638
+ * `insertAt` is a boundary, so the line sits at row-height times the number
3639
+ * of rows above it — in content space. The indicator is a sibling of the
3640
+ * viewport rather than a child, because anything inside it either scrolls
3641
+ * away with the transformed content wrapper or ignores the scroll entirely,
3642
+ * so the viewport's own scroll offset and its position within the body are
3643
+ * both subtracted back out here.
3644
+ *
3645
+ * A field rather than a getter: it is read from the template on every change
3646
+ * detection pass, and it only actually moves when the drag state or the
3647
+ * scroll position does.
3648
+ */
3649
+ this.dropIndicatorTop = null;
3650
+ /**
3651
+ * How wide the line is drawn. Rows are `width: fit-content`, so they are
3652
+ * often narrower than the viewport — a full-bleed line would hang past the
3653
+ * last column into empty space and read as part of the frame rather than the
3654
+ * rows. This is the visible extent of the row content, which also shrinks
3655
+ * correctly as the grid is scrolled sideways.
3656
+ */
3657
+ this.dropIndicatorWidth = 0;
2380
3658
  }
2381
3659
  ngOnInit() {
2382
3660
  /**
@@ -2385,13 +3663,30 @@ class OneGridBodyComponent {
2385
3663
  this.subscriptions.add(this.gridCopy$.subscribe((event) => {
2386
3664
  this.onGridCopy(event);
2387
3665
  }));
3666
+ /**
3667
+ * Row drag
3668
+ *
3669
+ * The service owns the gesture; the body only renders it and lends it the
3670
+ * viewport, the row height and a way to apply a move. The suppression
3671
+ * reason is re-read on every event that could change the order, so the
3672
+ * handle disables itself the moment a sort or filter is applied.
3673
+ */
3674
+ this.subscriptions.add(this.rowDragService.state$.subscribe((state) => {
3675
+ this.dragState = state;
3676
+ this.updateDropIndicator();
3677
+ this.cdr.markForCheck();
3678
+ }));
3679
+ this.subscriptions.add(this.rowDragService.announcement$.subscribe((message) => {
3680
+ this.dragAnnouncement = message;
3681
+ this.cdr.markForCheck();
3682
+ }));
2388
3683
  /**
2389
3684
  * Listen Column Changed Event
2390
3685
  */
2391
3686
  this.subscriptions.add(this.eventService.addEventListener('columnChanged', (payload) => {
2392
3687
  this.columnDefs = [...payload.columns];
2393
3688
  this.cellRenderer = this.columnDefs.some(col => col.cellRendererComponent);
2394
- this.rowWidth = this.columnDefs.reduce((sum, col) => sum + (col.width ? col.width > col.maxWidth ? col.maxWidth : col.width < col.minWidth ? col.minWidth : col.width : col.minWidth), 0);
3689
+ this.rowWidth = this.columnDefs.reduce((sum, col) => sum + resolveColumnWidth(col), 0);
2395
3690
  this.cdr.markForCheck();
2396
3691
  }));
2397
3692
  /**
@@ -2407,7 +3702,9 @@ class OneGridBodyComponent {
2407
3702
  if (node.leaf && this.detailRendererComponent) {
2408
3703
  node.detailRendererComponent = this.detailRendererComponent;
2409
3704
  }
2410
- if (node.expanded) {
3705
+ // Detail rows can still be in flight when a row is expanded, and
3706
+ // spreading null throws. Nothing to re-seed until they arrive.
3707
+ if (node.expanded && node.detailRowData) {
2411
3708
  this.api.setDetailRowData(node, [...node.detailRowData]);
2412
3709
  }
2413
3710
  return node;
@@ -2416,14 +3713,50 @@ class OneGridBodyComponent {
2416
3713
  else {
2417
3714
  this.nodes = nodes;
2418
3715
  }
3716
+ this.refreshRowDragSuppressed();
2419
3717
  this.cdr.markForCheck();
2420
3718
  });
2421
3719
  }));
3720
+ /**
3721
+ * Listen Pinned Row Data Changed Event
3722
+ */
3723
+ this.subscriptions.add(this.eventService.addEventListener('pinnedRowDataChanged', ({ side, nodes }) => {
3724
+ if (side === 'top') {
3725
+ this.pinnedTopNodes = nodes;
3726
+ }
3727
+ else {
3728
+ this.pinnedBottomNodes = nodes;
3729
+ }
3730
+ this.cdr.markForCheck();
3731
+ }));
2422
3732
  /**
2423
3733
  * Listen Sort Changed Event
2424
3734
  */
2425
3735
  this.subscriptions.add(this.eventService.addEventListener('sortChanged', ({ nodes }) => {
2426
3736
  this.nodes = nodes;
3737
+ // Sorting takes ownership of the order away from the user, so the handles
3738
+ // have to disable in the same frame the rows re-sort.
3739
+ this.refreshRowDragSuppressed();
3740
+ this.cdr.markForCheck();
3741
+ }));
3742
+ /**
3743
+ * Listen Row Order Changed Event
3744
+ *
3745
+ * Deliberately not folded into `rowDataChanged`: that path defers a frame
3746
+ * and re-seeds detail rows, which is right for a data load and far too much
3747
+ * for a move. A reorder is the same nodes in a different order.
3748
+ */
3749
+ this.subscriptions.add(this.eventService.addEventListener('rowOrderChanged', ({ nodes }) => {
3750
+ this.nodes = [...nodes];
3751
+ this.cdr.markForCheck();
3752
+ }));
3753
+ /**
3754
+ * Listen Cell State Changed Event
3755
+ *
3756
+ * The body renders OnPush, so regenerating a row's cell state is invisible
3757
+ * until something marks the body dirty. This is that something.
3758
+ */
3759
+ this.subscriptions.add(this.eventService.addEventListener('cellStateChanged', () => {
2427
3760
  this.cdr.markForCheck();
2428
3761
  }));
2429
3762
  /**
@@ -2442,9 +3775,33 @@ class OneGridBodyComponent {
2442
3775
  const scrollLeft = event.target.scrollLeft;
2443
3776
  if (scrollLeft !== this.lastScrollLeft) {
2444
3777
  this.lastScrollLeft = scrollLeft;
3778
+ this.syncPinnedRows(scrollLeft);
2445
3779
  this.bodyScroll.emit(scrollLeft);
3780
+ // The indicator is clipped to the row content, which moves sideways too.
3781
+ if (this.dragState) {
3782
+ this.updateDropIndicator();
3783
+ this.cdr.markForCheck();
3784
+ }
2446
3785
  return;
2447
3786
  }
3787
+ // Vertical: the drop indicator is anchored to the body, not the content, so
3788
+ // it has to be repositioned as the rows move beneath it — which they do on
3789
+ // their own during auto-scroll, with the pointer standing still.
3790
+ if (this.dragState) {
3791
+ this.updateDropIndicator();
3792
+ this.cdr.markForCheck();
3793
+ }
3794
+ }
3795
+ /**
3796
+ * Keep the pinned rows in step with the body's horizontal position.
3797
+ *
3798
+ * Assigned rather than transformed, for the same reason as the header: sticky
3799
+ * positioning for pinned *columns* has to resolve against a real scrollport,
3800
+ * and a transformed ancestor is not one. Pinned rows and pinned columns
3801
+ * therefore compose — a frozen corner cell stays frozen on both axes.
3802
+ */
3803
+ syncPinnedRows(scrollLeft) {
3804
+ this.pinnedViewports?.forEach(viewport => (viewport.nativeElement.scrollLeft = scrollLeft));
2448
3805
  }
2449
3806
  /**
2450
3807
  * Track Rendering Row Node By Row ID
@@ -2624,14 +3981,56 @@ class OneGridBodyComponent {
2624
3981
  invokeRowActionLanguage(event) {
2625
3982
  this.onRowLanguage.emit(event);
2626
3983
  }
3984
+ /**
3985
+ * Open the cell option menu, and tell the host it was asked for.
3986
+ *
3987
+ * The opening is the part that was missing. Everything else existed — the
3988
+ * trigger button behind `cellRendererParams.cellOptionButtonStatus`, the menu
3989
+ * component, the `cellOptionMenu` column property, the selection event wired
3990
+ * through to the grid's output — but nothing ever set the flag the template
3991
+ * renders on, so the button did nothing at all.
3992
+ *
3993
+ * Done here rather than in the row-action component because this is the
3994
+ * component whose template renders the menu, and so the one that has to be
3995
+ * marked for check.
3996
+ */
2627
3997
  invokeRowActionCellOption(event) {
3998
+ const field = event.column?.field;
3999
+ // Nothing to open when the column declared no options; the event still
4000
+ // goes to the host, which may be driving something of its own.
4001
+ if (field && event.node && event.column?.cellOptionMenu?.length) {
4002
+ event.node.cellOptions[field] = true;
4003
+ this.cdr.markForCheck();
4004
+ }
2628
4005
  this.onRowCellOption.emit(event);
2629
4006
  }
4007
+ /**
4008
+ * The menu closed itself — on select, on Escape, or on a click outside. It
4009
+ * has already cleared its own flag; this is what repaints without it.
4010
+ */
4011
+ cellOptionMenuClosed() {
4012
+ this.cdr.markForCheck();
4013
+ }
2630
4014
  invokeRowActionSelection(event) {
2631
4015
  this.onRowSelection.emit(event);
2632
4016
  }
4017
+ /**
4018
+ * Toggle a group / master row, then report the result.
4019
+ *
4020
+ * The group renderer is presentational: it says "the expander was clicked"
4021
+ * and nothing more. Performing the state change belongs here, where the API
4022
+ * is available. Previously this method only re-emitted, so the expander
4023
+ * rendered, animated on hover and did nothing at all — every consumer had to
4024
+ * discover for itself that `expansionChanged` was a request rather than the
4025
+ * notification its past-tense name promises.
4026
+ *
4027
+ * The event is re-emitted *after* the toggle, so `event.node.expanded` is the
4028
+ * new state, which is what a past-tense event should carry.
4029
+ */
2633
4030
  invokeExpansionChangedClick(event) {
2634
- this.expansionChanged.emit(event);
4031
+ const node = this.api.getRowNodeById(event.node.rowId) ?? event.node;
4032
+ this.api.setExpandedById(node.rowId, !node.expanded);
4033
+ this.expansionChanged.emit({ ...event, node, api: this.api });
2635
4034
  }
2636
4035
  cellOptionMenuSelected(event) {
2637
4036
  this.cellOptionSelect.emit(event);
@@ -2721,26 +4120,94 @@ class OneGridBodyComponent {
2721
4120
  }
2722
4121
  }
2723
4122
  }
2724
- /**
2725
- * Virtual Scroll Properties
2726
- */
2727
4123
  setVirtualScroll(items = 0) {
2728
- this.minBufferPx = this.itemSize * items * 1.5;
4124
+ this.minBufferPx = Math.min(this.itemSize * items * 1.5, this.MAX_BUFFER_PX);
2729
4125
  this.maxBufferPx = this.minBufferPx * 2;
2730
4126
  }
2731
4127
  /**
2732
4128
  * Unsubscribe All Subscriptions
2733
4129
  */
4130
+ /**
4131
+ * Hand the drag service what it needs to run a gesture.
4132
+ *
4133
+ * Deferred to here because the viewport element does not exist until the view
4134
+ * is up, and `nodes` is passed as a getter rather than a value — a drag
4135
+ * mutates the order underneath it, so a snapshot taken at attach time would
4136
+ * go stale on the first move.
4137
+ */
4138
+ ngAfterViewInit() {
4139
+ const element = this.viewport?.elementRef?.nativeElement;
4140
+ if (!element)
4141
+ return;
4142
+ this.rowDragService.attach({
4143
+ viewport: element,
4144
+ itemSize: this.itemSize,
4145
+ nodes: () => this.nodes,
4146
+ move: (fromIndex, toIndex) => {
4147
+ const moved = this.api.moveRow(fromIndex, toIndex);
4148
+ if (moved)
4149
+ this.cdr.markForCheck();
4150
+ return moved;
4151
+ },
4152
+ });
4153
+ this.refreshRowDragSuppressed();
4154
+ }
4155
+ /**
4156
+ * The params every per-cell callback receives.
4157
+ *
4158
+ * Built here rather than inline in the template so the whole documented
4159
+ * shape is passed — `value`, `field`, `rowIndex`, `api` and `context` were
4160
+ * all declared on `OneCellRendererParams` and none of them were being sent,
4161
+ * so a `cellClass` reading `params.value` silently got `undefined`.
4162
+ */
4163
+ cellParams(node, column) {
4164
+ return this.cellService.cellParams(column, node);
4165
+ }
4166
+ /**
4167
+ * Params for a component cell renderer.
4168
+ *
4169
+ * The cell params, plus whatever the column declared in
4170
+ * `cellRendererParams` — a field that exists precisely so a renderer can be
4171
+ * configured per column, and that nothing had ever forwarded to it. Column
4172
+ * config goes first so the grid's own fields win a name collision.
4173
+ */
4174
+ rendererParams(node, column) {
4175
+ return { ...(column?.cellRendererParams ?? {}), ...this.cellParams(node, column) };
4176
+ }
4177
+ /** Is this the row currently being moved? */
4178
+ isRowDragging(node) {
4179
+ return this.dragState?.rowId === node.rowId;
4180
+ }
4181
+ updateDropIndicator() {
4182
+ const state = this.dragState;
4183
+ const element = this.viewport?.elementRef?.nativeElement;
4184
+ if (state?.source !== 'pointer' || !element) {
4185
+ this.dropIndicatorTop = null;
4186
+ return;
4187
+ }
4188
+ this.dropIndicatorTop = (state.insertAt * this.itemSize) - element.scrollTop + element.offsetTop;
4189
+ this.dropIndicatorWidth = Math.max(Math.min(this.rowWidth - element.scrollLeft, element.clientWidth), 0);
4190
+ }
4191
+ onRowDragEnd(event) {
4192
+ this.rowDragEnd.emit(event);
4193
+ }
4194
+ refreshRowDragSuppressed() {
4195
+ const reason = this.api?.getRowDragSuppressedReason?.() ?? null;
4196
+ if (reason === this.rowDragSuppressed)
4197
+ return;
4198
+ this.rowDragSuppressed = reason;
4199
+ this.cdr.markForCheck();
4200
+ }
2734
4201
  ngOnDestroy() {
2735
4202
  this.subscriptions.unsubscribe();
2736
4203
  }
2737
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OneGridBodyComponent, deps: [{ token: OgColumnService }, { token: OneGridCellService }, { token: OneGridEventService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); }
2738
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: OneGridBodyComponent, selector: "one-grid-body", inputs: { rowData: "rowData", rowNumber: "rowNumber", api: "api", pagination: "pagination", allowedEdit: "allowedEdit", enableGroupColumnDefs: "enableGroupColumnDefs", submitted: "submitted", context: "context", getRowId: "getRowId", gridCopy$: "gridCopy$", gridType: "gridType", masterDetail: "masterDetail", masterNode: "masterNode", detailRowMaxHeight: "detailRowMaxHeight", detailCellRendererParams: "detailCellRendererParams", detailGridTemplate: "detailGridTemplate", detailRendererComponent: "detailRendererComponent", treeData: "treeData", treeDataChildrenField: "treeDataChildrenField", autoGroupColumnDef: "autoGroupColumnDef", groupDefaultExpanded: "groupDefaultExpanded" }, outputs: { onRowGotoDetail: "onRowGotoDetail", onRowAddNew: "onRowAddNew", onRowView: "onRowView", onRowCopy: "onRowCopy", onRowLocation: "onRowLocation", onRowDocument: "onRowDocument", onRowDelete: "onRowDelete", onRowGotoList: "onRowGotoList", onRowEdit: "onRowEdit", onRowPrint: "onRowPrint", onRowHistory: "onRowHistory", onRowOption: "onRowOption", onRowLanguage: "onRowLanguage", onRowCellOption: "onRowCellOption", onRowSelection: "onRowSelection", onShiftDoubleClick: "onShiftDoubleClick", rowCountChanged: "rowCountChanged", expansionChanged: "expansionChanged", bodyScroll: "bodyScroll", cellOptionSelect: "cellOptionSelect" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:mouseup": "onGlobalMouseUp()" } }, viewQueries: [{ propertyName: "bodyRef", first: true, predicate: ["bodyRef"], descendants: true, static: true }, { propertyName: "viewport", first: true, predicate: CdkVirtualScrollViewport, descendants: true }], ngImport: i0, template: "<div #bodyRef class=\"one-grid-body\">\n <cdk-virtual-scroll-viewport\n class=\"one-grid-body-viewport\"\n [itemSize]=\"itemSize\"\n [minBufferPx]=\"minBufferPx\"\n [maxBufferPx]=\"maxBufferPx\"\n (scroll)=\"onBodyScroll($event)\">\n\n <ng-container *cdkVirtualFor=\"let node of nodes; trackBy: trackByRowId;\">\n \n <div class=\"one-row\" [attr.data-row-id]=\"node.rowId\" [class.one-row-selected]=\"node.selected\">\n @for (col of columnDefs; track col.field;) {\n <ng-container [ngTemplateOutlet]=\"cellTemplate\" [ngTemplateOutletContext]=\"{ node, rowIndex: node.rowIndex, column: col }\"></ng-container>\n }\n </div>\n\n <!-- Custom Detail Component -->\n @if(node.leaf && node.detailActive && node?.detailRendererComponent) {\n <ng-container\n oneCellRendererComponent\n [cellRendererComponent]=\"node?.detailRendererComponent\"\n [params]=\"{ data: node.data, rowIndex: node.rowIndex, node: node, context: context }\">\n </ng-container>\n }\n\n <!-- Master Detail Grid (template provided by the parent one-grid to avoid a compile-time import cycle) -->\n @if (masterDetail && node.expanded && node?.detailRowData) {\n <div class=\"one-row-detail-container\" [attr.data-row-id]=\"node.rowId + '-detail'\">\n <ng-container\n [ngTemplateOutlet]=\"detailGridTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: node, node: node }\">\n </ng-container>\n </div>\n }\n </ng-container>\n @if (!nodes?.length) {\n <div style=\"width: max-content; border-top: 1px solid transparent;\">\n <div [style.width.px]=\"rowWidth\"></div>\n </div>\n }\n </cdk-virtual-scroll-viewport>\n @if (!nodes?.length) {\n <span class=\"one-grid-empty-message\" style=\"position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);\">No Rows to Show</span>\n }\n</div>\n\n<!-- Master Column Template -->\n<ng-template #cellTemplate let-node=\"node\" let-rowIndex=\"rowIndex\" let-column=\"column\">\n <div\n class=\"one-column\"\n [attr.data-cell-id]=\"node.rowId | cellKey:column.field\"\n [style.width.px]=\"column?.width\"\n [style.min-width.px]=\"column?.minWidth\"\n [style.max-width.px]=\"column?.maxWidth\"\n [class.one-column-border-top]=\"masterDetail && nodes?.length > 0 && nodes[node.rowIndex - 1]?.expanded\"\n [class.one-cell-selected]=\"selectedCells | cellSelected:node.rowId:column.field\"\n [class.one-cell-invalid]=\"node.cellStates | cellInvalid:node.data[column.field]:column:node:allowedEdit:submitted\"\n (dblclick)=\"startEdit($event, node, column)\"\n (mousedown)=\"startSelection(node.rowId, column.field)\"\n (mouseenter)=\"extendSelection(node.rowId, column.field)\"\n (mouseup)=\"endSelection()\">\n\n <!-- One Grid Cell Option -->\n @if (node.cellOptions[column.field]) {\n <one-grid-cell-option-menu\n #contextOptionMenuRef\n [node]=\"node\"\n [column]=\"column\"\n [options]=\"column.cellOptionMenu\"\n (cellOptionMenuSelect)=\"cellOptionMenuSelected($event)\"/>\n }\n\n <!-- Grid Cell Template -->\n @if(column?.cellRendererComponent) {\n <ng-container\n oneCellRendererComponent\n [cellRendererComponent]=\"column?.cellRendererComponent\"\n [params]=\"{ data: node.data, field: column.field, rowIndex: node.rowIndex, columnDef: column, node: node, context: context }\">\n </ng-container>\n }\n @else if (node.editing[column.field]) {\n <!-- Cell Editing Input -->\n <input class=\"one-cell-input\"\n name=\"{{ node.rowId + '' + column.field }}\"\n oneCellInputValidator\n [validationType]=\"column?.cellRendererParams?.inputType === 'number' ? 'number' : 'none'\"\n [placeholder]=\"column?.cellRendererParams?.placeholder\"\n [type]=\"column?.cellRendererParams?.inputType === 'number' ? 'text' : column?.cellRendererParams?.inputType\"\n [value]=\"node?.data[column?.field]\"\n [maxLength]=\"250\"\n (blur)=\"stopEdit(node, column, $event)\"\n (keydown.enter)=\"stopEdit(node, column, $event)\"\n (keydown.esc)=\"cancelEdit(node, column)\"\n />\n }\n @else if (column?.cellRenderer) {\n <div\n [ngStyle]=\"{ width: column.width + 'px' , position: 'relative' }\"\n [innerHTML]=\"node.cellStates | cellStateRenderer:column.field\"\n [class.editable-active]=\"node.cellStates | cellEditable:column:node:cellService.editableFn:cellService.updatableFn:allowedEdit\">\n </div>\n }\n @else {\n <div class=\"one-cell\"\n [class.align-content-left]=\"column?.alignContent === 'left'\"\n [class.align-content-center]=\"column?.alignContent === 'center'\"\n [class.align-content-right]=\"column?.alignContent === 'right'\"\n [ngClass]=\"colService.resolveCellPresentation(column, { node, data: node.data, columnDef: column })._resolvedCellClass\"\n [ngStyle]=\"colService.resolveCellPresentation(column, { node, data: node.data, columnDef: column })._resolvedCellStyle\"\n [style.padding-left.px]=\"column.autoGroupField && node.level | cellLevelPadding:24\"\n [class.one-cell-allowed-edit-active]=\"node.cellStates | cellEditable:column:node:cellService.editableFn:cellService.updatableFn:allowedEdit\">\n\n <!-- Cell Checkbox Selection -->\n @if (column?.checkboxSelection) {\n <one-grid-checkbox\n [node]=\"node\"\n [allowedEdit]=\"allowedEdit\"\n [api]=\"api\"\n (onRowSelection)=\"invokeRowActionSelection($event)\"\n />\n }\n\n <!-- Expand & Collapse Icon -->\n @if (((node?.group || detailRendererComponent) && column?.autoGroupField) || (masterDetail && node?.expandable && gridType === 'master' && column?.masterGroupField)) {\n <one-grid-cell-group-renderer\n [node]=\"node\"\n [masterDetail]=\"masterDetail\"\n [detailCellRendererParams]=\"detailCellRendererParams\"\n [detailRendererComponent]=\"detailRendererComponent\"\n (expansionChanged)=\"invokeExpansionChangedClick($event)\"\n />\n }\n\n @if(column?.cellRendererComponent) {\n <ng-container\n oneCellRendererComponent\n [cellRendererComponent]=\"column?.cellRendererComponent\"\n [params]=\"{ data: node.data, field: column.field, rowIndex: node.rowIndex, columnDef: column, node: node, context: context }\">\n </ng-container>\n }\n @else {\n <!-- Cell Value OR Placeholder -->\n <div oneCellTextExpand class=\"one-cell-text\" [class.one-cell-placeholder]=\"node.cellStates[column.field]?.type === 'placeholder'\">\n {{ (node.cellStates | cellStateRenderer:column.field) }}\n </div>\n }\n\n <!-- Cell Action Buttons -->\n @if (column.cellRendererParams | cellActionButtonEnable) {\n <one-grid-row-action\n [ngStyle]=\"{ marginLeft: 'auto' }\"\n [column]=\"column\"\n [node]=\"node\"\n [allowedEdit]=\"allowedEdit\"\n [masterNode]=\"masterNode\"\n [masterDetail]=\"masterDetail\"\n [gridType]=\"gridType\"\n [context]=\"context\"\n [cellRendererParams]=\"column?.cellRendererParams\"\n (onRowGotoDetail)=\"invokeRowActionGotoDetail($event)\"\n (onRowAddNew)=\"invokeRowActionAddNew($event)\"\n (onRowView)=\"invokeRowActionView($event)\"\n (onRowLocation)=\"invokeRowActionLocation($event)\"\n (onRowDocument)=\"invokeRowActionDocument($event)\"\n (onRowCopy)=\"invokeRowActionCopy($event)\"\n (onRowDelete)=\"invokeRowActionDelete($event)\"\n (onRowGotoList)=\"invokeRowActionGotoList($event)\"\n (onRowEdit)=\"invokeRowActionEdit($event)\"\n (onRowPrint)=\"invokeRowActionPrint($event)\"\n (onRowHistory)=\"invokeRowActionHistory($event)\"\n (onRowOption)=\"invokeRowActionOption($event)\"\n (onRowLanguage)=\"invokeRowActionLanguage($event)\"\n (onRowCellOption)=\"invokeRowActionCellOption($event)\"\n />\n }\n </div>\n }\n </div>\n</ng-template>\n", styles: [""], dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: i5.CdkFixedSizeVirtualScroll, selector: "cdk-virtual-scroll-viewport[itemSize]", inputs: ["itemSize", "minBufferPx", "maxBufferPx"] }, { kind: "directive", type: i5.CdkVirtualForOf, selector: "[cdkVirtualFor][cdkVirtualForOf]", inputs: ["cdkVirtualForOf", "cdkVirtualForTrackBy", "cdkVirtualForTemplate", "cdkVirtualForTemplateCacheSize"] }, { kind: "component", type: i5.CdkVirtualScrollViewport, selector: "cdk-virtual-scroll-viewport", inputs: ["orientation", "appendOnly"], outputs: ["scrolledIndexChange"] }, { kind: "component", type: OneGridCheckboxComponent, selector: "one-grid-checkbox", inputs: ["node", "allowedEdit", "api"], outputs: ["onRowSelection"] }, { kind: "component", type: OneGridRowActionComponent, selector: "one-grid-row-action", inputs: ["column", "node", "allowedEdit", "masterDetail", "masterNode", "cellRendererParams", "gridType", "context"], outputs: ["onRowGotoDetail", "onRowAddNew", "onRowView", "onRowLocation", "onRowDocument", "onRowCopy", "onRowDelete", "onRowGotoList", "onRowEdit", "onRowPrint", "onRowHistory", "onRowOption", "onRowLanguage", "onRowCellOption"] }, { kind: "component", type: OneGridCellGroupRendererComponent, selector: "one-grid-cell-group-renderer", inputs: ["node", "masterDetail", "detailCellRendererParams", "detailRendererComponent"], outputs: ["expansionChanged"] }, { kind: "directive", type: OneCellInputValidatorDirective, selector: "[oneCellInputValidator]", inputs: ["validationType", "regex"] }, { kind: "directive", type: OneCellRendererComponentDirective, selector: "[oneCellRendererComponent]", inputs: ["cellRendererComponent", "params"] }, { kind: "directive", type: OneCellTextExpandDirective, selector: "[oneCellTextExpand]" }, { kind: "component", type: OneGridCellOptionMenuComponent, selector: "one-grid-cell-option-menu", inputs: ["column", "node", "options"], outputs: ["cellOptionMenuSelect", "cellOptionMenuClose"] }, { kind: "pipe", type: CellEditablePipe, name: "cellEditable" }, { kind: "pipe", type: CellInvalidPipe, name: "cellInvalid" }, { kind: "pipe", type: CellSelectedPipe, name: "cellSelected" }, { kind: "pipe", type: CellActionButtonEnablePipe, name: "cellActionButtonEnable" }, { kind: "pipe", type: CellKeyPipe, name: "cellKey" }, { kind: "pipe", type: CellLevelPaddingPipe, name: "cellLevelPadding" }, { kind: "pipe", type: CellStateRendererPipe, name: "cellStateRenderer" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
4204
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OneGridBodyComponent, deps: [{ token: OgColumnService }, { token: OneGridCellService }, { token: OneGridEventService }, { token: OneGridRowDragService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); }
4205
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: OneGridBodyComponent, selector: "one-grid-body", inputs: { rowData: "rowData", rowNumber: "rowNumber", api: "api", pagination: "pagination", allowedEdit: "allowedEdit", enableGroupColumnDefs: "enableGroupColumnDefs", submitted: "submitted", context: "context", getRowId: "getRowId", gridCopy$: "gridCopy$", pinnedTopRowData: "pinnedTopRowData", pinnedBottomRowData: "pinnedBottomRowData", gridType: "gridType", masterDetail: "masterDetail", masterNode: "masterNode", detailRowMaxHeight: "detailRowMaxHeight", detailCellRendererParams: "detailCellRendererParams", detailGridTemplate: "detailGridTemplate", detailRendererComponent: "detailRendererComponent", treeData: "treeData", treeDataChildrenField: "treeDataChildrenField", autoGroupColumnDef: "autoGroupColumnDef", groupDefaultExpanded: "groupDefaultExpanded" }, outputs: { onRowGotoDetail: "onRowGotoDetail", onRowAddNew: "onRowAddNew", onRowView: "onRowView", onRowCopy: "onRowCopy", onRowLocation: "onRowLocation", onRowDocument: "onRowDocument", onRowDelete: "onRowDelete", onRowGotoList: "onRowGotoList", onRowEdit: "onRowEdit", onRowPrint: "onRowPrint", onRowHistory: "onRowHistory", onRowOption: "onRowOption", onRowLanguage: "onRowLanguage", onRowCellOption: "onRowCellOption", onRowSelection: "onRowSelection", onShiftDoubleClick: "onShiftDoubleClick", rowCountChanged: "rowCountChanged", expansionChanged: "expansionChanged", bodyScroll: "bodyScroll", cellOptionSelect: "cellOptionSelect", rowDragEnd: "rowDragEnd" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:mouseup": "onGlobalMouseUp()" } }, viewQueries: [{ propertyName: "bodyRef", first: true, predicate: ["bodyRef"], descendants: true, static: true }, { propertyName: "viewport", first: true, predicate: CdkVirtualScrollViewport, descendants: true }, { propertyName: "pinnedViewports", predicate: ["pinnedViewport"], descendants: true }], ngImport: i0, template: "<div #bodyRef class=\"one-grid-body\">\n\n <!-- Pinned rows live outside the scroll viewport so they never scroll\n vertically, and mirror its scrollLeft so the columns stay aligned. -->\n @if (pinnedTopNodes.length) {\n <div #pinnedViewport class=\"one-pinned-rows one-pinned-rows-top\">\n <ng-container [ngTemplateOutlet]=\"pinnedRows\" [ngTemplateOutletContext]=\"{ nodes: pinnedTopNodes }\"></ng-container>\n </div>\n }\n\n <!-- tabindex makes the scroll container reachable, so a keyboard user can\n scroll the rows with the arrow keys, Page Up/Down and Home/End. Without\n it the body is scrollable by pointer only (WCAG 2.1.1). -->\n <cdk-virtual-scroll-viewport\n class=\"one-grid-body-viewport\"\n tabindex=\"0\"\n [itemSize]=\"itemSize\"\n [minBufferPx]=\"minBufferPx\"\n [maxBufferPx]=\"maxBufferPx\"\n (scroll)=\"onBodyScroll($event)\">\n\n <ng-container *cdkVirtualFor=\"let node of nodes; trackBy: trackByRowId;\">\n \n <div class=\"one-row\"\n [attr.data-row-id]=\"node.rowId\"\n [class.one-row-selected]=\"node.selected\"\n [class.one-row-dragging]=\"isRowDragging(node)\">\n @for (col of columnDefs; track col.field;) {\n <ng-container [ngTemplateOutlet]=\"cellTemplate\" [ngTemplateOutletContext]=\"{ node, rowIndex: node.rowIndex, column: col }\"></ng-container>\n }\n </div>\n\n <!-- Custom Detail Component -->\n @if(node.leaf && node.detailActive && node?.detailRendererComponent) {\n <ng-container\n oneCellRendererComponent\n [cellRendererComponent]=\"node?.detailRendererComponent\"\n [params]=\"{ data: node.data, rowIndex: node.rowIndex, node: node, context: context }\">\n </ng-container>\n }\n\n <!-- Master Detail Grid (template provided by the parent one-grid to avoid a compile-time import cycle) -->\n @if (masterDetail && node.expanded && node?.detailRowData) {\n <div class=\"one-row-detail-container\" [attr.data-row-id]=\"node.rowId + '-detail'\">\n <ng-container\n [ngTemplateOutlet]=\"detailGridTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: node, node: node }\">\n </ng-container>\n </div>\n }\n </ng-container>\n @if (!nodes?.length) {\n <div style=\"width: max-content; border-top: 1px solid transparent;\">\n <div [style.width.px]=\"rowWidth\"></div>\n </div>\n }\n </cdk-virtual-scroll-viewport>\n\n <!-- Drop indicator.\n A line at the insertion point rather than a gap opened between rows:\n the viewport is virtualised at a fixed itemSize, so shifting rows apart\n would desynchronise the scroller from its own geometry. It is also the\n clearer signal \u2014 the row lands exactly where the line is.\n Positioned against the body, with the viewport's scroll subtracted, so\n it stays put while the content moves under it during auto-scroll. -->\n @if (dropIndicatorTop !== null) {\n <div class=\"one-row-drop-indicator\" aria-hidden=\"true\"\n [style.top.px]=\"dropIndicatorTop\"\n [style.width.px]=\"dropIndicatorWidth\"></div>\n }\n\n <!-- Keyboard reordering has no drag image to watch, so it narrates instead.\n Assertive because the message is the only feedback that the row moved. -->\n <div class=\"one-grid-visually-hidden\" role=\"status\" aria-live=\"assertive\" aria-atomic=\"true\">{{ dragAnnouncement }}</div>\n @if (!nodes?.length) {\n <span class=\"one-grid-empty-message\" style=\"position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);\">No Rows to Show</span>\n }\n\n @if (pinnedBottomNodes.length) {\n <div #pinnedViewport class=\"one-pinned-rows one-pinned-rows-bottom\">\n <ng-container [ngTemplateOutlet]=\"pinnedRows\" [ngTemplateOutletContext]=\"{ nodes: pinnedBottomNodes }\"></ng-container>\n </div>\n }\n</div>\n\n<!-- Pinned rows are display-only: they render exactly what a data cell renders,\n but carry none of its interactions. A totals row should not open an editor\n on double-click or start a range selection on drag. -->\n<ng-template #pinnedRows let-nodes=\"nodes\">\n @for (node of nodes; track node.rowId) {\n <div class=\"one-row one-row-pinned\" [attr.data-row-id]=\"node.rowId\">\n @for (column of columnDefs; track column.field) {\n <div\n class=\"one-column\"\n [attr.data-cell-id]=\"node.rowId | cellKey:column.field\"\n [class.one-column-pinned-left]=\"column.pinned === 'left'\"\n [class.one-column-pinned-right]=\"column.pinned === 'right'\"\n [class.one-column-pinned-edge]=\"column._pinnedEdge\"\n [style.left.px]=\"column.pinned === 'left' ? column._pinnedOffset : null\"\n [style.right.px]=\"column.pinned === 'right' ? column._pinnedOffset : null\"\n [style.width.px]=\"column?.width\"\n [style.min-width.px]=\"column?.minWidth\"\n [style.max-width.px]=\"column?.maxWidth\">\n\n @if (column?.cellRendererComponent) {\n <ng-container\n oneCellRendererComponent\n [cellRendererComponent]=\"column?.cellRendererComponent\"\n [params]=\"rendererParams(node, column)\">\n </ng-container>\n }\n @else if (column?.cellRenderer) {\n <div\n [ngStyle]=\"{ width: column.width + 'px', position: 'relative' }\"\n [innerHTML]=\"node.cellStates | cellStateRenderer:column.field\">\n </div>\n }\n @else {\n <div class=\"one-cell\"\n [class.align-content-left]=\"column?.alignContent === 'left'\"\n [class.align-content-center]=\"column?.alignContent === 'center'\"\n [class.align-content-right]=\"column?.alignContent === 'right'\"\n [ngClass]=\"colService.resolveCellClass(column, cellParams(node, column))\"\n [ngStyle]=\"colService.resolveCellStyle(column, cellParams(node, column))\">\n <div class=\"one-cell-text\">{{ (node.cellStates | cellStateRenderer:column.field) }}</div>\n </div>\n }\n </div>\n }\n </div>\n }\n</ng-template>\n\n<!-- Master Column Template -->\n<ng-template #cellTemplate let-node=\"node\" let-rowIndex=\"rowIndex\" let-column=\"column\">\n <div\n class=\"one-column\"\n [attr.data-cell-id]=\"node.rowId | cellKey:column.field\"\n [class.one-column-pinned-left]=\"column.pinned === 'left'\"\n [class.one-column-pinned-right]=\"column.pinned === 'right'\"\n [class.one-column-pinned-edge]=\"column._pinnedEdge\"\n [style.left.px]=\"column.pinned === 'left' ? column._pinnedOffset : null\"\n [style.right.px]=\"column.pinned === 'right' ? column._pinnedOffset : null\"\n [style.width.px]=\"column?.width\"\n [style.min-width.px]=\"column?.minWidth\"\n [style.max-width.px]=\"column?.maxWidth\"\n [class.one-column-border-top]=\"masterDetail && nodes?.length > 0 && nodes[node.rowIndex - 1]?.expanded\"\n [class.one-cell-selected]=\"selectedCells | cellSelected:node.rowId:column.field\"\n [class.one-cell-invalid]=\"node.cellStates | cellInvalid:node.data[column.field]:column:node:allowedEdit:submitted\"\n (dblclick)=\"startEdit($event, node, column)\"\n (mousedown)=\"startSelection(node.rowId, column.field)\"\n (mouseenter)=\"extendSelection(node.rowId, column.field)\"\n (mouseup)=\"endSelection()\">\n\n <!-- One Grid Cell Option -->\n @if (node.cellOptions[column.field]) {\n <one-grid-cell-option-menu\n #contextOptionMenuRef\n [node]=\"node\"\n [column]=\"column\"\n [options]=\"column.cellOptionMenu\"\n (cellOptionMenuSelect)=\"cellOptionMenuSelected($event)\"\n (cellOptionMenuClose)=\"cellOptionMenuClosed()\"/>\n }\n\n <!-- Grid Cell Template -->\n @if(column?.cellRendererComponent) {\n <ng-container\n oneCellRendererComponent\n [cellRendererComponent]=\"column?.cellRendererComponent\"\n [params]=\"rendererParams(node, column)\">\n </ng-container>\n }\n @else if (node.editing[column.field]) {\n <!-- Cell Editing Input -->\n <input class=\"one-cell-input\"\n name=\"{{ node.rowId + '' + column.field }}\"\n oneCellInputValidator\n [validationType]=\"column?.cellRendererParams?.inputType === 'number' ? 'number' : 'none'\"\n [placeholder]=\"column?.cellRendererParams?.placeholder\"\n [type]=\"column?.cellRendererParams?.inputType === 'number' ? 'text' : column?.cellRendererParams?.inputType\"\n [value]=\"node?.data[column?.field]\"\n [maxLength]=\"250\"\n (blur)=\"stopEdit(node, column, $event)\"\n (keydown.enter)=\"stopEdit(node, column, $event)\"\n (keydown.esc)=\"cancelEdit(node, column)\"\n />\n }\n @else if (column?.cellRenderer) {\n <div\n [ngStyle]=\"{ width: column.width + 'px' , position: 'relative' }\"\n [innerHTML]=\"node.cellStates | cellStateRenderer:column.field\"\n [class.editable-active]=\"node.cellStates | cellEditable:column:node:cellService.editableFn:cellService.updatableFn:allowedEdit\">\n </div>\n }\n @else {\n <div class=\"one-cell\"\n [class.align-content-left]=\"column?.alignContent === 'left'\"\n [class.align-content-center]=\"column?.alignContent === 'center'\"\n [class.align-content-right]=\"column?.alignContent === 'right'\"\n [ngClass]=\"colService.resolveCellClass(column, cellParams(node, column))\"\n [ngStyle]=\"colService.resolveCellStyle(column, cellParams(node, column))\"\n [style.padding-left.px]=\"column.autoGroupField && node.level | cellLevelPadding:24\"\n [class.one-cell-allowed-edit-active]=\"node.cellStates | cellEditable:column:node:cellService.editableFn:cellService.updatableFn:allowedEdit\">\n\n <!-- Row Drag Handle -->\n @if (cellService.rowDragFn(column, node)) {\n <one-grid-row-drag-handle\n [node]=\"node\"\n [suppressedReason]=\"rowDragSuppressed\"\n (dragEnd)=\"onRowDragEnd($event)\"\n />\n }\n\n <!-- Cell Checkbox Selection -->\n @if (column?.checkboxSelection) {\n <one-grid-checkbox\n [node]=\"node\"\n [allowedEdit]=\"allowedEdit\"\n [api]=\"api\"\n (onRowSelection)=\"invokeRowActionSelection($event)\"\n />\n }\n\n <!-- Expand & Collapse Icon -->\n @if (((node?.group || detailRendererComponent) && column?.autoGroupField) || (masterDetail && node?.expandable && gridType === 'master' && column?.masterGroupField)) {\n <one-grid-cell-group-renderer\n [node]=\"node\"\n [masterDetail]=\"masterDetail\"\n [detailCellRendererParams]=\"detailCellRendererParams\"\n [detailRendererComponent]=\"detailRendererComponent\"\n (expansionChanged)=\"invokeExpansionChangedClick($event)\"\n />\n }\n\n @if(column?.cellRendererComponent) {\n <ng-container\n oneCellRendererComponent\n [cellRendererComponent]=\"column?.cellRendererComponent\"\n [params]=\"rendererParams(node, column)\">\n </ng-container>\n }\n @else {\n <!-- Cell Value OR Placeholder -->\n <div oneCellTextExpand class=\"one-cell-text\" [class.one-cell-placeholder]=\"node.cellStates[column.field]?.type === 'placeholder'\">\n {{ (node.cellStates | cellStateRenderer:column.field) }}\n </div>\n }\n\n <!-- Cell Action Buttons -->\n @if (column.cellRendererParams | cellActionButtonEnable) {\n <one-grid-row-action\n [ngStyle]=\"{ marginLeft: 'auto' }\"\n [column]=\"column\"\n [node]=\"node\"\n [allowedEdit]=\"allowedEdit\"\n [masterNode]=\"masterNode\"\n [masterDetail]=\"masterDetail\"\n [gridType]=\"gridType\"\n [context]=\"context\"\n [cellRendererParams]=\"column?.cellRendererParams\"\n (onRowGotoDetail)=\"invokeRowActionGotoDetail($event)\"\n (onRowAddNew)=\"invokeRowActionAddNew($event)\"\n (onRowView)=\"invokeRowActionView($event)\"\n (onRowLocation)=\"invokeRowActionLocation($event)\"\n (onRowDocument)=\"invokeRowActionDocument($event)\"\n (onRowCopy)=\"invokeRowActionCopy($event)\"\n (onRowDelete)=\"invokeRowActionDelete($event)\"\n (onRowGotoList)=\"invokeRowActionGotoList($event)\"\n (onRowEdit)=\"invokeRowActionEdit($event)\"\n (onRowPrint)=\"invokeRowActionPrint($event)\"\n (onRowHistory)=\"invokeRowActionHistory($event)\"\n (onRowOption)=\"invokeRowActionOption($event)\"\n (onRowLanguage)=\"invokeRowActionLanguage($event)\"\n (onRowCellOption)=\"invokeRowActionCellOption($event)\"\n />\n }\n </div>\n }\n </div>\n</ng-template>\n", styles: [""], dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: i6.CdkFixedSizeVirtualScroll, selector: "cdk-virtual-scroll-viewport[itemSize]", inputs: ["itemSize", "minBufferPx", "maxBufferPx"] }, { kind: "directive", type: i6.CdkVirtualForOf, selector: "[cdkVirtualFor][cdkVirtualForOf]", inputs: ["cdkVirtualForOf", "cdkVirtualForTrackBy", "cdkVirtualForTemplate", "cdkVirtualForTemplateCacheSize"] }, { kind: "component", type: i6.CdkVirtualScrollViewport, selector: "cdk-virtual-scroll-viewport", inputs: ["orientation", "appendOnly"], outputs: ["scrolledIndexChange"] }, { kind: "component", type: OneGridCheckboxComponent, selector: "one-grid-checkbox", inputs: ["node", "allowedEdit", "api"], outputs: ["onRowSelection"] }, { kind: "component", type: OneGridRowActionComponent, selector: "one-grid-row-action", inputs: ["column", "node", "allowedEdit", "masterDetail", "masterNode", "cellRendererParams", "gridType", "context"], outputs: ["onRowGotoDetail", "onRowAddNew", "onRowView", "onRowLocation", "onRowDocument", "onRowCopy", "onRowDelete", "onRowGotoList", "onRowEdit", "onRowPrint", "onRowHistory", "onRowOption", "onRowLanguage", "onRowCellOption"] }, { kind: "component", type: OneGridCellGroupRendererComponent, selector: "one-grid-cell-group-renderer", inputs: ["node", "masterDetail", "detailCellRendererParams", "detailRendererComponent"], outputs: ["expansionChanged"] }, { kind: "directive", type: OneCellInputValidatorDirective, selector: "[oneCellInputValidator]", inputs: ["validationType", "regex"] }, { kind: "directive", type: OneCellRendererComponentDirective, selector: "[oneCellRendererComponent]", inputs: ["cellRendererComponent", "params"] }, { kind: "directive", type: OneCellTextExpandDirective, selector: "[oneCellTextExpand]" }, { kind: "component", type: OneGridCellOptionMenuComponent, selector: "one-grid-cell-option-menu", inputs: ["column", "node", "options"], outputs: ["cellOptionMenuSelect", "cellOptionMenuClose"] }, { kind: "component", type: OneGridRowDragHandleComponent, selector: "one-grid-row-drag-handle", inputs: ["node", "suppressedReason"], outputs: ["dragEnd"] }, { kind: "pipe", type: CellEditablePipe, name: "cellEditable" }, { kind: "pipe", type: CellInvalidPipe, name: "cellInvalid" }, { kind: "pipe", type: CellSelectedPipe, name: "cellSelected" }, { kind: "pipe", type: CellActionButtonEnablePipe, name: "cellActionButtonEnable" }, { kind: "pipe", type: CellKeyPipe, name: "cellKey" }, { kind: "pipe", type: CellLevelPaddingPipe, name: "cellLevelPadding" }, { kind: "pipe", type: CellStateRendererPipe, name: "cellStateRenderer" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2739
4206
  }
2740
4207
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OneGridBodyComponent, decorators: [{
2741
4208
  type: Component,
2742
- args: [{ standalone: false, selector: 'one-grid-body', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div #bodyRef class=\"one-grid-body\">\n <cdk-virtual-scroll-viewport\n class=\"one-grid-body-viewport\"\n [itemSize]=\"itemSize\"\n [minBufferPx]=\"minBufferPx\"\n [maxBufferPx]=\"maxBufferPx\"\n (scroll)=\"onBodyScroll($event)\">\n\n <ng-container *cdkVirtualFor=\"let node of nodes; trackBy: trackByRowId;\">\n \n <div class=\"one-row\" [attr.data-row-id]=\"node.rowId\" [class.one-row-selected]=\"node.selected\">\n @for (col of columnDefs; track col.field;) {\n <ng-container [ngTemplateOutlet]=\"cellTemplate\" [ngTemplateOutletContext]=\"{ node, rowIndex: node.rowIndex, column: col }\"></ng-container>\n }\n </div>\n\n <!-- Custom Detail Component -->\n @if(node.leaf && node.detailActive && node?.detailRendererComponent) {\n <ng-container\n oneCellRendererComponent\n [cellRendererComponent]=\"node?.detailRendererComponent\"\n [params]=\"{ data: node.data, rowIndex: node.rowIndex, node: node, context: context }\">\n </ng-container>\n }\n\n <!-- Master Detail Grid (template provided by the parent one-grid to avoid a compile-time import cycle) -->\n @if (masterDetail && node.expanded && node?.detailRowData) {\n <div class=\"one-row-detail-container\" [attr.data-row-id]=\"node.rowId + '-detail'\">\n <ng-container\n [ngTemplateOutlet]=\"detailGridTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: node, node: node }\">\n </ng-container>\n </div>\n }\n </ng-container>\n @if (!nodes?.length) {\n <div style=\"width: max-content; border-top: 1px solid transparent;\">\n <div [style.width.px]=\"rowWidth\"></div>\n </div>\n }\n </cdk-virtual-scroll-viewport>\n @if (!nodes?.length) {\n <span class=\"one-grid-empty-message\" style=\"position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);\">No Rows to Show</span>\n }\n</div>\n\n<!-- Master Column Template -->\n<ng-template #cellTemplate let-node=\"node\" let-rowIndex=\"rowIndex\" let-column=\"column\">\n <div\n class=\"one-column\"\n [attr.data-cell-id]=\"node.rowId | cellKey:column.field\"\n [style.width.px]=\"column?.width\"\n [style.min-width.px]=\"column?.minWidth\"\n [style.max-width.px]=\"column?.maxWidth\"\n [class.one-column-border-top]=\"masterDetail && nodes?.length > 0 && nodes[node.rowIndex - 1]?.expanded\"\n [class.one-cell-selected]=\"selectedCells | cellSelected:node.rowId:column.field\"\n [class.one-cell-invalid]=\"node.cellStates | cellInvalid:node.data[column.field]:column:node:allowedEdit:submitted\"\n (dblclick)=\"startEdit($event, node, column)\"\n (mousedown)=\"startSelection(node.rowId, column.field)\"\n (mouseenter)=\"extendSelection(node.rowId, column.field)\"\n (mouseup)=\"endSelection()\">\n\n <!-- One Grid Cell Option -->\n @if (node.cellOptions[column.field]) {\n <one-grid-cell-option-menu\n #contextOptionMenuRef\n [node]=\"node\"\n [column]=\"column\"\n [options]=\"column.cellOptionMenu\"\n (cellOptionMenuSelect)=\"cellOptionMenuSelected($event)\"/>\n }\n\n <!-- Grid Cell Template -->\n @if(column?.cellRendererComponent) {\n <ng-container\n oneCellRendererComponent\n [cellRendererComponent]=\"column?.cellRendererComponent\"\n [params]=\"{ data: node.data, field: column.field, rowIndex: node.rowIndex, columnDef: column, node: node, context: context }\">\n </ng-container>\n }\n @else if (node.editing[column.field]) {\n <!-- Cell Editing Input -->\n <input class=\"one-cell-input\"\n name=\"{{ node.rowId + '' + column.field }}\"\n oneCellInputValidator\n [validationType]=\"column?.cellRendererParams?.inputType === 'number' ? 'number' : 'none'\"\n [placeholder]=\"column?.cellRendererParams?.placeholder\"\n [type]=\"column?.cellRendererParams?.inputType === 'number' ? 'text' : column?.cellRendererParams?.inputType\"\n [value]=\"node?.data[column?.field]\"\n [maxLength]=\"250\"\n (blur)=\"stopEdit(node, column, $event)\"\n (keydown.enter)=\"stopEdit(node, column, $event)\"\n (keydown.esc)=\"cancelEdit(node, column)\"\n />\n }\n @else if (column?.cellRenderer) {\n <div\n [ngStyle]=\"{ width: column.width + 'px' , position: 'relative' }\"\n [innerHTML]=\"node.cellStates | cellStateRenderer:column.field\"\n [class.editable-active]=\"node.cellStates | cellEditable:column:node:cellService.editableFn:cellService.updatableFn:allowedEdit\">\n </div>\n }\n @else {\n <div class=\"one-cell\"\n [class.align-content-left]=\"column?.alignContent === 'left'\"\n [class.align-content-center]=\"column?.alignContent === 'center'\"\n [class.align-content-right]=\"column?.alignContent === 'right'\"\n [ngClass]=\"colService.resolveCellPresentation(column, { node, data: node.data, columnDef: column })._resolvedCellClass\"\n [ngStyle]=\"colService.resolveCellPresentation(column, { node, data: node.data, columnDef: column })._resolvedCellStyle\"\n [style.padding-left.px]=\"column.autoGroupField && node.level | cellLevelPadding:24\"\n [class.one-cell-allowed-edit-active]=\"node.cellStates | cellEditable:column:node:cellService.editableFn:cellService.updatableFn:allowedEdit\">\n\n <!-- Cell Checkbox Selection -->\n @if (column?.checkboxSelection) {\n <one-grid-checkbox\n [node]=\"node\"\n [allowedEdit]=\"allowedEdit\"\n [api]=\"api\"\n (onRowSelection)=\"invokeRowActionSelection($event)\"\n />\n }\n\n <!-- Expand & Collapse Icon -->\n @if (((node?.group || detailRendererComponent) && column?.autoGroupField) || (masterDetail && node?.expandable && gridType === 'master' && column?.masterGroupField)) {\n <one-grid-cell-group-renderer\n [node]=\"node\"\n [masterDetail]=\"masterDetail\"\n [detailCellRendererParams]=\"detailCellRendererParams\"\n [detailRendererComponent]=\"detailRendererComponent\"\n (expansionChanged)=\"invokeExpansionChangedClick($event)\"\n />\n }\n\n @if(column?.cellRendererComponent) {\n <ng-container\n oneCellRendererComponent\n [cellRendererComponent]=\"column?.cellRendererComponent\"\n [params]=\"{ data: node.data, field: column.field, rowIndex: node.rowIndex, columnDef: column, node: node, context: context }\">\n </ng-container>\n }\n @else {\n <!-- Cell Value OR Placeholder -->\n <div oneCellTextExpand class=\"one-cell-text\" [class.one-cell-placeholder]=\"node.cellStates[column.field]?.type === 'placeholder'\">\n {{ (node.cellStates | cellStateRenderer:column.field) }}\n </div>\n }\n\n <!-- Cell Action Buttons -->\n @if (column.cellRendererParams | cellActionButtonEnable) {\n <one-grid-row-action\n [ngStyle]=\"{ marginLeft: 'auto' }\"\n [column]=\"column\"\n [node]=\"node\"\n [allowedEdit]=\"allowedEdit\"\n [masterNode]=\"masterNode\"\n [masterDetail]=\"masterDetail\"\n [gridType]=\"gridType\"\n [context]=\"context\"\n [cellRendererParams]=\"column?.cellRendererParams\"\n (onRowGotoDetail)=\"invokeRowActionGotoDetail($event)\"\n (onRowAddNew)=\"invokeRowActionAddNew($event)\"\n (onRowView)=\"invokeRowActionView($event)\"\n (onRowLocation)=\"invokeRowActionLocation($event)\"\n (onRowDocument)=\"invokeRowActionDocument($event)\"\n (onRowCopy)=\"invokeRowActionCopy($event)\"\n (onRowDelete)=\"invokeRowActionDelete($event)\"\n (onRowGotoList)=\"invokeRowActionGotoList($event)\"\n (onRowEdit)=\"invokeRowActionEdit($event)\"\n (onRowPrint)=\"invokeRowActionPrint($event)\"\n (onRowHistory)=\"invokeRowActionHistory($event)\"\n (onRowOption)=\"invokeRowActionOption($event)\"\n (onRowLanguage)=\"invokeRowActionLanguage($event)\"\n (onRowCellOption)=\"invokeRowActionCellOption($event)\"\n />\n }\n </div>\n }\n </div>\n</ng-template>\n" }]
2743
- }], ctorParameters: () => [{ type: OgColumnService }, { type: OneGridCellService }, { type: OneGridEventService }, { type: i0.ChangeDetectorRef }], propDecorators: { bodyRef: [{
4209
+ args: [{ standalone: false, selector: 'one-grid-body', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div #bodyRef class=\"one-grid-body\">\n\n <!-- Pinned rows live outside the scroll viewport so they never scroll\n vertically, and mirror its scrollLeft so the columns stay aligned. -->\n @if (pinnedTopNodes.length) {\n <div #pinnedViewport class=\"one-pinned-rows one-pinned-rows-top\">\n <ng-container [ngTemplateOutlet]=\"pinnedRows\" [ngTemplateOutletContext]=\"{ nodes: pinnedTopNodes }\"></ng-container>\n </div>\n }\n\n <!-- tabindex makes the scroll container reachable, so a keyboard user can\n scroll the rows with the arrow keys, Page Up/Down and Home/End. Without\n it the body is scrollable by pointer only (WCAG 2.1.1). -->\n <cdk-virtual-scroll-viewport\n class=\"one-grid-body-viewport\"\n tabindex=\"0\"\n [itemSize]=\"itemSize\"\n [minBufferPx]=\"minBufferPx\"\n [maxBufferPx]=\"maxBufferPx\"\n (scroll)=\"onBodyScroll($event)\">\n\n <ng-container *cdkVirtualFor=\"let node of nodes; trackBy: trackByRowId;\">\n \n <div class=\"one-row\"\n [attr.data-row-id]=\"node.rowId\"\n [class.one-row-selected]=\"node.selected\"\n [class.one-row-dragging]=\"isRowDragging(node)\">\n @for (col of columnDefs; track col.field;) {\n <ng-container [ngTemplateOutlet]=\"cellTemplate\" [ngTemplateOutletContext]=\"{ node, rowIndex: node.rowIndex, column: col }\"></ng-container>\n }\n </div>\n\n <!-- Custom Detail Component -->\n @if(node.leaf && node.detailActive && node?.detailRendererComponent) {\n <ng-container\n oneCellRendererComponent\n [cellRendererComponent]=\"node?.detailRendererComponent\"\n [params]=\"{ data: node.data, rowIndex: node.rowIndex, node: node, context: context }\">\n </ng-container>\n }\n\n <!-- Master Detail Grid (template provided by the parent one-grid to avoid a compile-time import cycle) -->\n @if (masterDetail && node.expanded && node?.detailRowData) {\n <div class=\"one-row-detail-container\" [attr.data-row-id]=\"node.rowId + '-detail'\">\n <ng-container\n [ngTemplateOutlet]=\"detailGridTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: node, node: node }\">\n </ng-container>\n </div>\n }\n </ng-container>\n @if (!nodes?.length) {\n <div style=\"width: max-content; border-top: 1px solid transparent;\">\n <div [style.width.px]=\"rowWidth\"></div>\n </div>\n }\n </cdk-virtual-scroll-viewport>\n\n <!-- Drop indicator.\n A line at the insertion point rather than a gap opened between rows:\n the viewport is virtualised at a fixed itemSize, so shifting rows apart\n would desynchronise the scroller from its own geometry. It is also the\n clearer signal \u2014 the row lands exactly where the line is.\n Positioned against the body, with the viewport's scroll subtracted, so\n it stays put while the content moves under it during auto-scroll. -->\n @if (dropIndicatorTop !== null) {\n <div class=\"one-row-drop-indicator\" aria-hidden=\"true\"\n [style.top.px]=\"dropIndicatorTop\"\n [style.width.px]=\"dropIndicatorWidth\"></div>\n }\n\n <!-- Keyboard reordering has no drag image to watch, so it narrates instead.\n Assertive because the message is the only feedback that the row moved. -->\n <div class=\"one-grid-visually-hidden\" role=\"status\" aria-live=\"assertive\" aria-atomic=\"true\">{{ dragAnnouncement }}</div>\n @if (!nodes?.length) {\n <span class=\"one-grid-empty-message\" style=\"position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);\">No Rows to Show</span>\n }\n\n @if (pinnedBottomNodes.length) {\n <div #pinnedViewport class=\"one-pinned-rows one-pinned-rows-bottom\">\n <ng-container [ngTemplateOutlet]=\"pinnedRows\" [ngTemplateOutletContext]=\"{ nodes: pinnedBottomNodes }\"></ng-container>\n </div>\n }\n</div>\n\n<!-- Pinned rows are display-only: they render exactly what a data cell renders,\n but carry none of its interactions. A totals row should not open an editor\n on double-click or start a range selection on drag. -->\n<ng-template #pinnedRows let-nodes=\"nodes\">\n @for (node of nodes; track node.rowId) {\n <div class=\"one-row one-row-pinned\" [attr.data-row-id]=\"node.rowId\">\n @for (column of columnDefs; track column.field) {\n <div\n class=\"one-column\"\n [attr.data-cell-id]=\"node.rowId | cellKey:column.field\"\n [class.one-column-pinned-left]=\"column.pinned === 'left'\"\n [class.one-column-pinned-right]=\"column.pinned === 'right'\"\n [class.one-column-pinned-edge]=\"column._pinnedEdge\"\n [style.left.px]=\"column.pinned === 'left' ? column._pinnedOffset : null\"\n [style.right.px]=\"column.pinned === 'right' ? column._pinnedOffset : null\"\n [style.width.px]=\"column?.width\"\n [style.min-width.px]=\"column?.minWidth\"\n [style.max-width.px]=\"column?.maxWidth\">\n\n @if (column?.cellRendererComponent) {\n <ng-container\n oneCellRendererComponent\n [cellRendererComponent]=\"column?.cellRendererComponent\"\n [params]=\"rendererParams(node, column)\">\n </ng-container>\n }\n @else if (column?.cellRenderer) {\n <div\n [ngStyle]=\"{ width: column.width + 'px', position: 'relative' }\"\n [innerHTML]=\"node.cellStates | cellStateRenderer:column.field\">\n </div>\n }\n @else {\n <div class=\"one-cell\"\n [class.align-content-left]=\"column?.alignContent === 'left'\"\n [class.align-content-center]=\"column?.alignContent === 'center'\"\n [class.align-content-right]=\"column?.alignContent === 'right'\"\n [ngClass]=\"colService.resolveCellClass(column, cellParams(node, column))\"\n [ngStyle]=\"colService.resolveCellStyle(column, cellParams(node, column))\">\n <div class=\"one-cell-text\">{{ (node.cellStates | cellStateRenderer:column.field) }}</div>\n </div>\n }\n </div>\n }\n </div>\n }\n</ng-template>\n\n<!-- Master Column Template -->\n<ng-template #cellTemplate let-node=\"node\" let-rowIndex=\"rowIndex\" let-column=\"column\">\n <div\n class=\"one-column\"\n [attr.data-cell-id]=\"node.rowId | cellKey:column.field\"\n [class.one-column-pinned-left]=\"column.pinned === 'left'\"\n [class.one-column-pinned-right]=\"column.pinned === 'right'\"\n [class.one-column-pinned-edge]=\"column._pinnedEdge\"\n [style.left.px]=\"column.pinned === 'left' ? column._pinnedOffset : null\"\n [style.right.px]=\"column.pinned === 'right' ? column._pinnedOffset : null\"\n [style.width.px]=\"column?.width\"\n [style.min-width.px]=\"column?.minWidth\"\n [style.max-width.px]=\"column?.maxWidth\"\n [class.one-column-border-top]=\"masterDetail && nodes?.length > 0 && nodes[node.rowIndex - 1]?.expanded\"\n [class.one-cell-selected]=\"selectedCells | cellSelected:node.rowId:column.field\"\n [class.one-cell-invalid]=\"node.cellStates | cellInvalid:node.data[column.field]:column:node:allowedEdit:submitted\"\n (dblclick)=\"startEdit($event, node, column)\"\n (mousedown)=\"startSelection(node.rowId, column.field)\"\n (mouseenter)=\"extendSelection(node.rowId, column.field)\"\n (mouseup)=\"endSelection()\">\n\n <!-- One Grid Cell Option -->\n @if (node.cellOptions[column.field]) {\n <one-grid-cell-option-menu\n #contextOptionMenuRef\n [node]=\"node\"\n [column]=\"column\"\n [options]=\"column.cellOptionMenu\"\n (cellOptionMenuSelect)=\"cellOptionMenuSelected($event)\"\n (cellOptionMenuClose)=\"cellOptionMenuClosed()\"/>\n }\n\n <!-- Grid Cell Template -->\n @if(column?.cellRendererComponent) {\n <ng-container\n oneCellRendererComponent\n [cellRendererComponent]=\"column?.cellRendererComponent\"\n [params]=\"rendererParams(node, column)\">\n </ng-container>\n }\n @else if (node.editing[column.field]) {\n <!-- Cell Editing Input -->\n <input class=\"one-cell-input\"\n name=\"{{ node.rowId + '' + column.field }}\"\n oneCellInputValidator\n [validationType]=\"column?.cellRendererParams?.inputType === 'number' ? 'number' : 'none'\"\n [placeholder]=\"column?.cellRendererParams?.placeholder\"\n [type]=\"column?.cellRendererParams?.inputType === 'number' ? 'text' : column?.cellRendererParams?.inputType\"\n [value]=\"node?.data[column?.field]\"\n [maxLength]=\"250\"\n (blur)=\"stopEdit(node, column, $event)\"\n (keydown.enter)=\"stopEdit(node, column, $event)\"\n (keydown.esc)=\"cancelEdit(node, column)\"\n />\n }\n @else if (column?.cellRenderer) {\n <div\n [ngStyle]=\"{ width: column.width + 'px' , position: 'relative' }\"\n [innerHTML]=\"node.cellStates | cellStateRenderer:column.field\"\n [class.editable-active]=\"node.cellStates | cellEditable:column:node:cellService.editableFn:cellService.updatableFn:allowedEdit\">\n </div>\n }\n @else {\n <div class=\"one-cell\"\n [class.align-content-left]=\"column?.alignContent === 'left'\"\n [class.align-content-center]=\"column?.alignContent === 'center'\"\n [class.align-content-right]=\"column?.alignContent === 'right'\"\n [ngClass]=\"colService.resolveCellClass(column, cellParams(node, column))\"\n [ngStyle]=\"colService.resolveCellStyle(column, cellParams(node, column))\"\n [style.padding-left.px]=\"column.autoGroupField && node.level | cellLevelPadding:24\"\n [class.one-cell-allowed-edit-active]=\"node.cellStates | cellEditable:column:node:cellService.editableFn:cellService.updatableFn:allowedEdit\">\n\n <!-- Row Drag Handle -->\n @if (cellService.rowDragFn(column, node)) {\n <one-grid-row-drag-handle\n [node]=\"node\"\n [suppressedReason]=\"rowDragSuppressed\"\n (dragEnd)=\"onRowDragEnd($event)\"\n />\n }\n\n <!-- Cell Checkbox Selection -->\n @if (column?.checkboxSelection) {\n <one-grid-checkbox\n [node]=\"node\"\n [allowedEdit]=\"allowedEdit\"\n [api]=\"api\"\n (onRowSelection)=\"invokeRowActionSelection($event)\"\n />\n }\n\n <!-- Expand & Collapse Icon -->\n @if (((node?.group || detailRendererComponent) && column?.autoGroupField) || (masterDetail && node?.expandable && gridType === 'master' && column?.masterGroupField)) {\n <one-grid-cell-group-renderer\n [node]=\"node\"\n [masterDetail]=\"masterDetail\"\n [detailCellRendererParams]=\"detailCellRendererParams\"\n [detailRendererComponent]=\"detailRendererComponent\"\n (expansionChanged)=\"invokeExpansionChangedClick($event)\"\n />\n }\n\n @if(column?.cellRendererComponent) {\n <ng-container\n oneCellRendererComponent\n [cellRendererComponent]=\"column?.cellRendererComponent\"\n [params]=\"rendererParams(node, column)\">\n </ng-container>\n }\n @else {\n <!-- Cell Value OR Placeholder -->\n <div oneCellTextExpand class=\"one-cell-text\" [class.one-cell-placeholder]=\"node.cellStates[column.field]?.type === 'placeholder'\">\n {{ (node.cellStates | cellStateRenderer:column.field) }}\n </div>\n }\n\n <!-- Cell Action Buttons -->\n @if (column.cellRendererParams | cellActionButtonEnable) {\n <one-grid-row-action\n [ngStyle]=\"{ marginLeft: 'auto' }\"\n [column]=\"column\"\n [node]=\"node\"\n [allowedEdit]=\"allowedEdit\"\n [masterNode]=\"masterNode\"\n [masterDetail]=\"masterDetail\"\n [gridType]=\"gridType\"\n [context]=\"context\"\n [cellRendererParams]=\"column?.cellRendererParams\"\n (onRowGotoDetail)=\"invokeRowActionGotoDetail($event)\"\n (onRowAddNew)=\"invokeRowActionAddNew($event)\"\n (onRowView)=\"invokeRowActionView($event)\"\n (onRowLocation)=\"invokeRowActionLocation($event)\"\n (onRowDocument)=\"invokeRowActionDocument($event)\"\n (onRowCopy)=\"invokeRowActionCopy($event)\"\n (onRowDelete)=\"invokeRowActionDelete($event)\"\n (onRowGotoList)=\"invokeRowActionGotoList($event)\"\n (onRowEdit)=\"invokeRowActionEdit($event)\"\n (onRowPrint)=\"invokeRowActionPrint($event)\"\n (onRowHistory)=\"invokeRowActionHistory($event)\"\n (onRowOption)=\"invokeRowActionOption($event)\"\n (onRowLanguage)=\"invokeRowActionLanguage($event)\"\n (onRowCellOption)=\"invokeRowActionCellOption($event)\"\n />\n }\n </div>\n }\n </div>\n</ng-template>\n" }]
4210
+ }], ctorParameters: () => [{ type: OgColumnService }, { type: OneGridCellService }, { type: OneGridEventService }, { type: OneGridRowDragService }, { type: i0.ChangeDetectorRef }], propDecorators: { bodyRef: [{
2744
4211
  type: ViewChild,
2745
4212
  args: ['bodyRef', { static: true }]
2746
4213
  }], rowData: [{
@@ -2765,6 +4232,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
2765
4232
  }], gridCopy$: [{
2766
4233
  type: Input,
2767
4234
  args: [{ required: true }]
4235
+ }], pinnedTopRowData: [{
4236
+ type: Input
4237
+ }], pinnedBottomRowData: [{
4238
+ type: Input
4239
+ }], pinnedViewports: [{
4240
+ type: ViewChildren,
4241
+ args: ['pinnedViewport']
2768
4242
  }], viewport: [{
2769
4243
  type: ViewChild,
2770
4244
  args: [CdkVirtualScrollViewport]
@@ -2830,6 +4304,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
2830
4304
  type: Output
2831
4305
  }], cellOptionSelect: [{
2832
4306
  type: Output
4307
+ }], rowDragEnd: [{
4308
+ type: Output
2833
4309
  }], onDocumentClick: [{
2834
4310
  type: HostListener,
2835
4311
  args: ['document:click', ['$event']]
@@ -2853,31 +4329,56 @@ class OneGridPaginationComponent {
2853
4329
  this.goToNext = new EventEmitter();
2854
4330
  this.goToLast = new EventEmitter();
2855
4331
  this.pageSizeChange = new EventEmitter();
2856
- this.pageSize = OnePageSizeOption[0];
2857
- this.pageSizeOptions = OnePageSizeOption;
2858
4332
  }
2859
- ngAfterViewInit() {
2860
- if (this.paginate?.pageSizeOptions) {
2861
- this.pageSizeOptions = this.paginate.pageSizeOptions;
2862
- }
4333
+ /**
4334
+ * The page size the bar shows.
4335
+ *
4336
+ * Derived from the input rather than held locally. The bar is a display of
4337
+ * the host's paging state — it does not own it — and a local copy meant a
4338
+ * host that supplied `perPage: 50` still saw "25 rows" selected.
4339
+ */
4340
+ get pageSize() {
4341
+ return this.paginate?.perPage ?? OnePageSizeOption[0];
4342
+ }
4343
+ /**
4344
+ * The size options offered.
4345
+ *
4346
+ * Also derived. These used to be copied out of the input in
4347
+ * `ngAfterViewInit` — after the view had already been checked with the
4348
+ * defaults — so any grid supplying its own options threw `NG0100` in dev
4349
+ * mode, and later changes to the list were ignored entirely.
4350
+ */
4351
+ get pageSizeOptions() {
4352
+ return this.paginate?.pageSizeOptions?.length ? this.paginate.pageSizeOptions : OnePageSizeOption;
4353
+ }
4354
+ /** Whether there is a page before the current one. */
4355
+ get hasPrevious() {
4356
+ return this.pageCount > 1 && (this.paginate?.currentPage ?? 1) > 1;
4357
+ }
4358
+ /** Whether there is a page after the current one. */
4359
+ get hasNext() {
4360
+ return this.pageCount > 1 && (this.paginate?.currentPage ?? 1) < (this.paginate?.lastPage ?? 1);
4361
+ }
4362
+ get pageCount() {
4363
+ return this.paginate?.lastPage ?? 1;
2863
4364
  }
2864
4365
  goToFirstPage() {
2865
- if (!(this.paginate?.perPage < this.paginate?.total && this.paginate?.currentPage > 1))
4366
+ if (!this.hasPrevious)
2866
4367
  return;
2867
4368
  this.goToFirst.emit(this.paginate?.currentPage);
2868
4369
  }
2869
4370
  goToPreviousPage() {
2870
- if (!(this.paginate?.perPage < this.paginate?.total && this.paginate?.currentPage > 1))
4371
+ if (!this.hasPrevious)
2871
4372
  return;
2872
4373
  this.goToPrevious.emit(this.paginate?.currentPage);
2873
4374
  }
2874
4375
  goToNextPage() {
2875
- if (!(this.paginate?.perPage < this.paginate?.total && this.paginate?.currentPage < this.paginate?.lastPage))
4376
+ if (!this.hasNext)
2876
4377
  return;
2877
4378
  this.goToNext.emit(this.paginate?.currentPage);
2878
4379
  }
2879
4380
  goToLastPage() {
2880
- if (!(this.paginate?.perPage < this.paginate?.total && this.paginate?.currentPage < this.paginate?.lastPage))
4381
+ if (!this.hasNext)
2881
4382
  return;
2882
4383
  this.goToLast.emit(this.paginate?.currentPage);
2883
4384
  }
@@ -2885,16 +4386,15 @@ class OneGridPaginationComponent {
2885
4386
  const select = event.target;
2886
4387
  const newSize = Number(select.value);
2887
4388
  if (newSize !== this.pageSize) {
2888
- this.pageSize = newSize;
2889
- this.pageSizeChange.emit(this.pageSize);
4389
+ this.pageSizeChange.emit(newSize);
2890
4390
  }
2891
4391
  }
2892
4392
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OneGridPaginationComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2893
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: OneGridPaginationComponent, selector: "one-grid-pagination", inputs: { paginate: "paginate" }, outputs: { goToFirst: "goToFirst", goToPrevious: "goToPrevious", goToNext: "goToNext", goToLast: "goToLast", pageSizeChange: "pageSizeChange" }, ngImport: i0, template: "<div class=\"one-grid-pagination\">\n <div class=\"one-pagination-row\">\n @if (paginate?.firstRowIndex > 0 && paginate?.lastRowIndex > 0 && paginate?.total > 0) {\n <div class=\"one-pagination-columns-group\">\n <div class=\"one-pagination-column\">\n <span>{{ paginate?.firstRowIndex }}</span>\n <span>-</span>\n <span>{{ paginate?.lastRowIndex }}</span>\n &nbsp;<span>/</span>&nbsp;\n <span>{{ paginate?.total }}</span>\n </div>\n </div>\n }\n <div class=\"one-pagination-columns-group\">\n <div class=\"one-pagination-column\">\n <button \n class=\"one-pagination-btn\"\n (click)=\"goToFirstPage()\"\n [class.disabled]=\"!(paginate?.perPage < paginate?.total && paginate?.currentPage > 1)\">\n <lucide-icon name=\"chevron-first\" [size]=\"16\" [strokeWidth]=\"2\"></lucide-icon>\n </button>\n <button\n class=\"one-pagination-btn\"\n (click)=\"goToPreviousPage()\"\n [class.disabled]=\"!(paginate?.perPage < paginate?.total && paginate?.currentPage > 1)\">\n <lucide-icon name=\"chevron-left\" [size]=\"16\" [strokeWidth]=\"2\"></lucide-icon>\n </button>\n </div>\n <div class=\"one-pagination-column\">\n &nbsp;{{ paginate?.currentPage }}<span>/</span>{{ paginate?.lastPage }}&nbsp;\n </div>\n <div class=\"one-pagination-column\">\n <button\n class=\"one-pagination-btn\"\n (click)=\"goToNextPage()\"\n [class.disabled]=\"!(paginate?.perPage < paginate?.total && paginate?.currentPage < paginate?.lastPage)\">\n <lucide-icon name=\"chevron-right\" [size]=\"16\" [strokeWidth]=\"2\"></lucide-icon>\n </button>\n <button\n class=\"one-pagination-btn\"\n (click)=\"goToLastPage()\"\n [class.disabled]=\"!(paginate?.perPage < paginate?.total && paginate?.currentPage < paginate?.lastPage)\">\n <lucide-icon name=\"chevron-last\" [size]=\"16\" [strokeWidth]=\"2\"></lucide-icon>\n </button>\n </div>\n <div class=\"one-pagination-columns-group\">\n <div class=\"one-pagination-column\">\n <select name=\"row-per-page\" [value]=\"pageSize\" (change)=\"onPageSizeChange($event)\">\n @for (size of pageSizeOptions; track $index) {\n <option [value]=\"size\">{{ size }} rows</option>\n }\n </select>\n </div>\n </div>\n </div>\n </div>\n</div>", styles: [""], dependencies: [{ kind: "directive", type: i3$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i3$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "component", type: i3.LucideAngularComponent, selector: "lucide-angular, lucide-icon, i-lucide, span-lucide", inputs: ["class", "name", "img", "color", "absoluteStrokeWidth", "size", "strokeWidth"] }] }); }
4393
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: OneGridPaginationComponent, selector: "one-grid-pagination", inputs: { paginate: "paginate" }, outputs: { goToFirst: "goToFirst", goToPrevious: "goToPrevious", goToNext: "goToNext", goToLast: "goToLast", pageSizeChange: "pageSizeChange" }, ngImport: i0, template: "<!-- A navigation landmark, so the bar is reachable as one thing rather than as\n four unlabelled icon buttons floating under the grid. -->\n<div class=\"one-grid-pagination\" role=\"navigation\" aria-label=\"Pagination\">\n <div class=\"one-pagination-row\">\n @if (paginate?.firstRowIndex > 0 && paginate?.lastRowIndex > 0 && paginate?.total > 0) {\n <div class=\"one-pagination-columns-group\">\n <div class=\"one-pagination-column\">\n <span>{{ paginate?.firstRowIndex }}</span>\n <span>-</span>\n <span>{{ paginate?.lastRowIndex }}</span>\n &nbsp;<span>/</span>&nbsp;\n <span>{{ paginate?.total }}</span>\n </div>\n </div>\n }\n <div class=\"one-pagination-columns-group\">\n <div class=\"one-pagination-column\">\n <!-- `type=\"button\"` so the bar cannot submit a surrounding form,\n a real `disabled` rather than a class that only stops the\n mouse, and a name on every icon-only control. -->\n <button\n type=\"button\"\n class=\"one-pagination-btn\"\n aria-label=\"First page\"\n title=\"First page\"\n [disabled]=\"!hasPrevious\"\n [class.disabled]=\"!hasPrevious\"\n (click)=\"goToFirstPage()\">\n <lucide-icon name=\"chevron-first\" [size]=\"16\" [strokeWidth]=\"2\"></lucide-icon>\n </button>\n <button\n type=\"button\"\n class=\"one-pagination-btn\"\n aria-label=\"Previous page\"\n title=\"Previous page\"\n [disabled]=\"!hasPrevious\"\n [class.disabled]=\"!hasPrevious\"\n (click)=\"goToPreviousPage()\">\n <lucide-icon name=\"chevron-left\" [size]=\"16\" [strokeWidth]=\"2\"></lucide-icon>\n </button>\n </div>\n <!-- Announced as a live region: the numbers are the only feedback a\n screen reader gets that the page actually changed. -->\n <div class=\"one-pagination-column\" role=\"status\" aria-live=\"polite\">\n &nbsp;<span class=\"one-grid-visually-hidden\">Page </span>{{ paginate?.currentPage }}<span>/</span>{{ paginate?.lastPage }}&nbsp;\n </div>\n <div class=\"one-pagination-column\">\n <button\n type=\"button\"\n class=\"one-pagination-btn\"\n aria-label=\"Next page\"\n title=\"Next page\"\n [disabled]=\"!hasNext\"\n [class.disabled]=\"!hasNext\"\n (click)=\"goToNextPage()\">\n <lucide-icon name=\"chevron-right\" [size]=\"16\" [strokeWidth]=\"2\"></lucide-icon>\n </button>\n <button\n type=\"button\"\n class=\"one-pagination-btn\"\n aria-label=\"Last page\"\n title=\"Last page\"\n [disabled]=\"!hasNext\"\n [class.disabled]=\"!hasNext\"\n (click)=\"goToLastPage()\">\n <lucide-icon name=\"chevron-last\" [size]=\"16\" [strokeWidth]=\"2\"></lucide-icon>\n </button>\n </div>\n <div class=\"one-pagination-columns-group\">\n <div class=\"one-pagination-column\">\n <!-- `[selected]` per option, not `[value]` on the select:\n with options rendered by `@for`, the select's value is\n set before they exist and does not stick. -->\n <select name=\"row-per-page\" aria-label=\"Rows per page\" (change)=\"onPageSizeChange($event)\">\n @for (size of pageSizeOptions; track size) {\n <option [value]=\"size\" [selected]=\"size === pageSize\">{{ size }} rows</option>\n }\n </select>\n </div>\n </div>\n </div>\n </div>\n</div>\n", styles: [""], dependencies: [{ kind: "directive", type: i3.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i3.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "component", type: i2$1.LucideAngularComponent, selector: "lucide-angular, lucide-icon, i-lucide, span-lucide", inputs: ["class", "name", "img", "color", "absoluteStrokeWidth", "size", "strokeWidth"] }] }); }
2894
4394
  }
2895
4395
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OneGridPaginationComponent, decorators: [{
2896
4396
  type: Component,
2897
- args: [{ standalone: false, selector: 'one-grid-pagination', template: "<div class=\"one-grid-pagination\">\n <div class=\"one-pagination-row\">\n @if (paginate?.firstRowIndex > 0 && paginate?.lastRowIndex > 0 && paginate?.total > 0) {\n <div class=\"one-pagination-columns-group\">\n <div class=\"one-pagination-column\">\n <span>{{ paginate?.firstRowIndex }}</span>\n <span>-</span>\n <span>{{ paginate?.lastRowIndex }}</span>\n &nbsp;<span>/</span>&nbsp;\n <span>{{ paginate?.total }}</span>\n </div>\n </div>\n }\n <div class=\"one-pagination-columns-group\">\n <div class=\"one-pagination-column\">\n <button \n class=\"one-pagination-btn\"\n (click)=\"goToFirstPage()\"\n [class.disabled]=\"!(paginate?.perPage < paginate?.total && paginate?.currentPage > 1)\">\n <lucide-icon name=\"chevron-first\" [size]=\"16\" [strokeWidth]=\"2\"></lucide-icon>\n </button>\n <button\n class=\"one-pagination-btn\"\n (click)=\"goToPreviousPage()\"\n [class.disabled]=\"!(paginate?.perPage < paginate?.total && paginate?.currentPage > 1)\">\n <lucide-icon name=\"chevron-left\" [size]=\"16\" [strokeWidth]=\"2\"></lucide-icon>\n </button>\n </div>\n <div class=\"one-pagination-column\">\n &nbsp;{{ paginate?.currentPage }}<span>/</span>{{ paginate?.lastPage }}&nbsp;\n </div>\n <div class=\"one-pagination-column\">\n <button\n class=\"one-pagination-btn\"\n (click)=\"goToNextPage()\"\n [class.disabled]=\"!(paginate?.perPage < paginate?.total && paginate?.currentPage < paginate?.lastPage)\">\n <lucide-icon name=\"chevron-right\" [size]=\"16\" [strokeWidth]=\"2\"></lucide-icon>\n </button>\n <button\n class=\"one-pagination-btn\"\n (click)=\"goToLastPage()\"\n [class.disabled]=\"!(paginate?.perPage < paginate?.total && paginate?.currentPage < paginate?.lastPage)\">\n <lucide-icon name=\"chevron-last\" [size]=\"16\" [strokeWidth]=\"2\"></lucide-icon>\n </button>\n </div>\n <div class=\"one-pagination-columns-group\">\n <div class=\"one-pagination-column\">\n <select name=\"row-per-page\" [value]=\"pageSize\" (change)=\"onPageSizeChange($event)\">\n @for (size of pageSizeOptions; track $index) {\n <option [value]=\"size\">{{ size }} rows</option>\n }\n </select>\n </div>\n </div>\n </div>\n </div>\n</div>" }]
4397
+ args: [{ standalone: false, selector: 'one-grid-pagination', template: "<!-- A navigation landmark, so the bar is reachable as one thing rather than as\n four unlabelled icon buttons floating under the grid. -->\n<div class=\"one-grid-pagination\" role=\"navigation\" aria-label=\"Pagination\">\n <div class=\"one-pagination-row\">\n @if (paginate?.firstRowIndex > 0 && paginate?.lastRowIndex > 0 && paginate?.total > 0) {\n <div class=\"one-pagination-columns-group\">\n <div class=\"one-pagination-column\">\n <span>{{ paginate?.firstRowIndex }}</span>\n <span>-</span>\n <span>{{ paginate?.lastRowIndex }}</span>\n &nbsp;<span>/</span>&nbsp;\n <span>{{ paginate?.total }}</span>\n </div>\n </div>\n }\n <div class=\"one-pagination-columns-group\">\n <div class=\"one-pagination-column\">\n <!-- `type=\"button\"` so the bar cannot submit a surrounding form,\n a real `disabled` rather than a class that only stops the\n mouse, and a name on every icon-only control. -->\n <button\n type=\"button\"\n class=\"one-pagination-btn\"\n aria-label=\"First page\"\n title=\"First page\"\n [disabled]=\"!hasPrevious\"\n [class.disabled]=\"!hasPrevious\"\n (click)=\"goToFirstPage()\">\n <lucide-icon name=\"chevron-first\" [size]=\"16\" [strokeWidth]=\"2\"></lucide-icon>\n </button>\n <button\n type=\"button\"\n class=\"one-pagination-btn\"\n aria-label=\"Previous page\"\n title=\"Previous page\"\n [disabled]=\"!hasPrevious\"\n [class.disabled]=\"!hasPrevious\"\n (click)=\"goToPreviousPage()\">\n <lucide-icon name=\"chevron-left\" [size]=\"16\" [strokeWidth]=\"2\"></lucide-icon>\n </button>\n </div>\n <!-- Announced as a live region: the numbers are the only feedback a\n screen reader gets that the page actually changed. -->\n <div class=\"one-pagination-column\" role=\"status\" aria-live=\"polite\">\n &nbsp;<span class=\"one-grid-visually-hidden\">Page </span>{{ paginate?.currentPage }}<span>/</span>{{ paginate?.lastPage }}&nbsp;\n </div>\n <div class=\"one-pagination-column\">\n <button\n type=\"button\"\n class=\"one-pagination-btn\"\n aria-label=\"Next page\"\n title=\"Next page\"\n [disabled]=\"!hasNext\"\n [class.disabled]=\"!hasNext\"\n (click)=\"goToNextPage()\">\n <lucide-icon name=\"chevron-right\" [size]=\"16\" [strokeWidth]=\"2\"></lucide-icon>\n </button>\n <button\n type=\"button\"\n class=\"one-pagination-btn\"\n aria-label=\"Last page\"\n title=\"Last page\"\n [disabled]=\"!hasNext\"\n [class.disabled]=\"!hasNext\"\n (click)=\"goToLastPage()\">\n <lucide-icon name=\"chevron-last\" [size]=\"16\" [strokeWidth]=\"2\"></lucide-icon>\n </button>\n </div>\n <div class=\"one-pagination-columns-group\">\n <div class=\"one-pagination-column\">\n <!-- `[selected]` per option, not `[value]` on the select:\n with options rendered by `@for`, the select's value is\n set before they exist and does not stick. -->\n <select name=\"row-per-page\" aria-label=\"Rows per page\" (change)=\"onPageSizeChange($event)\">\n @for (size of pageSizeOptions; track size) {\n <option [value]=\"size\" [selected]=\"size === pageSize\">{{ size }} rows</option>\n }\n </select>\n </div>\n </div>\n </div>\n </div>\n</div>\n" }]
2898
4398
  }], propDecorators: { paginate: [{
2899
4399
  type: Input
2900
4400
  }], goToFirst: [{
@@ -2924,6 +4424,14 @@ class OneGridComponent {
2924
4424
  this.groupColumnDefs = [];
2925
4425
  this.defaultColumnDef = {};
2926
4426
  this.columnDefs = [];
4427
+ /**
4428
+ * Rows anchored above the scrolling area — a header summary, or an entry line.
4429
+ * They keep column alignment and scroll sideways with the body, but are not
4430
+ * data: sorting, filtering, selection and the row count all pass them by.
4431
+ */
4432
+ this.pinnedTopRowData = [];
4433
+ /** Rows anchored below the scrolling area, typically a totals row. */
4434
+ this.pinnedBottomRowData = [];
2927
4435
  // RXJS Properties
2928
4436
  this.gridCopy$ = new Subject();
2929
4437
  /**
@@ -2941,6 +4449,11 @@ class OneGridComponent {
2941
4449
  /**
2942
4450
  * Bind Row Action Events & Data to Parent
2943
4451
  */
4452
+ /**
4453
+ * A row finished moving. Fires once per completed reorder — not while the
4454
+ * pointer is in flight — and only when the position actually changed.
4455
+ */
4456
+ this.rowDragEnd = new EventEmitter();
2944
4457
  this.onGridReady = new EventEmitter();
2945
4458
  this.onRowGotoDetail = new EventEmitter();
2946
4459
  this.onRowAddNew = new EventEmitter();
@@ -2988,6 +4501,15 @@ class OneGridComponent {
2988
4501
  if (changes["rowData"] && !changes["rowData"]?.firstChange) {
2989
4502
  this.treeData || this.masterDetail ? this.api.updateRowData(this.rowData) : this.api.setRowData(this.rowData);
2990
4503
  }
4504
+ // Only later changes. The first one arrives before ngOnInit has run
4505
+ // api.init, so the column definitions are not registered yet and the body
4506
+ // has not subscribed — the initial load happens in ngAfterViewInit instead.
4507
+ if (changes["pinnedTopRowData"] && !changes["pinnedTopRowData"].firstChange) {
4508
+ this.api.setPinnedRowData('top', this.pinnedTopRowData);
4509
+ }
4510
+ if (changes["pinnedBottomRowData"] && !changes["pinnedBottomRowData"].firstChange) {
4511
+ this.api.setPinnedRowData('bottom', this.pinnedBottomRowData);
4512
+ }
2991
4513
  }
2992
4514
  ngOnInit() {
2993
4515
  /**
@@ -3015,7 +4537,18 @@ class OneGridComponent {
3015
4537
  /**
3016
4538
  * Initialize One Grid API
3017
4539
  */
3018
- this.api.init(this.gridType, this.rowSelection, this.columnDefs, this.defaultColumnDef, this.context, this.masterDetail, this.detailCellRendererParams, this.detailRendererComponent, this.treeData, this.groupColumnDefs);
4540
+ this.api.init({
4541
+ gridType: this.gridType,
4542
+ columnDefs: this.columnDefs,
4543
+ defaultColumnDef: this.defaultColumnDef,
4544
+ groupColumnDefs: this.groupColumnDefs,
4545
+ context: this.context,
4546
+ rowSelection: this.rowSelection,
4547
+ masterDetail: this.masterDetail,
4548
+ detailCellRendererParams: this.detailCellRendererParams,
4549
+ detailRendererComponent: this.detailRendererComponent,
4550
+ treeData: this.treeData,
4551
+ });
3019
4552
  /**
3020
4553
  * Setup Master Detail Configuration
3021
4554
  */
@@ -3043,14 +4576,20 @@ class OneGridComponent {
3043
4576
  }));
3044
4577
  }
3045
4578
  ngAfterViewInit() {
3046
- const headerViewport = this.elementRef.nativeElement.querySelector('.one-grid-header-column');
3047
- const bodyViewport = this.elementRef.nativeElement.querySelector('.one-grid-body-viewport');
3048
- // Listen Scroll Event Changed
3049
- this.subscriptions.add(fromEvent(bodyViewport, 'scroll').subscribe(() => {
3050
- if (headerViewport) {
3051
- headerViewport.scrollLeft = bodyViewport.scrollLeft;
3052
- }
3053
- }));
4579
+ // The header follows the body through (bodyScroll) -> [scrollLeft]. A second
4580
+ // subscription used to live here that queried '.one-grid-header-column' — a
4581
+ // header *cell*, not the viewport — and assigned scrollLeft to it, which
4582
+ // does nothing. It has been removed rather than repaired: the binding path
4583
+ // is the one that works, and it now drives the pinned columns too.
4584
+ // Pinned rows, now that the columns are registered and the body is
4585
+ // listening. Unlike rowData these are not loaded through (onGridReady) —
4586
+ // binding them is the whole contract — so the grid loads them itself.
4587
+ if (this.pinnedTopRowData?.length) {
4588
+ this.api.setPinnedRowData('top', this.pinnedTopRowData);
4589
+ }
4590
+ if (this.pinnedBottomRowData?.length) {
4591
+ this.api.setPinnedRowData('bottom', this.pinnedBottomRowData);
4592
+ }
3054
4593
  /**
3055
4594
  * Emit Grid Ready Event
3056
4595
  */
@@ -3149,6 +4688,9 @@ class OneGridComponent {
3149
4688
  invokePageSizeChange(pageSize) {
3150
4689
  this.pageSizeChange.emit(pageSize);
3151
4690
  }
4691
+ invokeRowDragEnd(event) {
4692
+ this.rowDragEnd.emit(event);
4693
+ }
3152
4694
  invokeExpansionChangedClick(event) {
3153
4695
  this.expansionChanged.emit(event);
3154
4696
  }
@@ -3161,15 +4703,16 @@ class OneGridComponent {
3161
4703
  this.subscriptions.unsubscribe();
3162
4704
  }
3163
4705
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OneGridComponent, deps: [{ token: OneGridApi }, { token: OneGridEventService }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component }); }
3164
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: OneGridComponent, selector: "one-grid", inputs: { rowNumber: "rowNumber", allowedEdit: "allowedEdit", submitted: "submitted", context: "context", rowData: "rowData", rowSelection: "rowSelection", enableGroupColumnDefs: "enableGroupColumnDefs", groupColumnDefs: "groupColumnDefs", defaultColumnDef: "defaultColumnDef", columnDefs: "columnDefs", gridType: "gridType", masterDetail: "masterDetail", masterNode: "masterNode", detailCellRendererParams: "detailCellRendererParams", detailRendererComponent: "detailRendererComponent", treeData: "treeData", treeDataChildrenField: "treeDataChildrenField", autoGroupColumnDef: "autoGroupColumnDef", groupDefaultExpanded: "groupDefaultExpanded", pagination: "pagination", paginate: "paginate" }, outputs: { onGridReady: "onGridReady", onRowGotoDetail: "onRowGotoDetail", onRowAddNew: "onRowAddNew", onRowView: "onRowView", onRowLocation: "onRowLocation", onRowDocument: "onRowDocument", onRowCopy: "onRowCopy", onRowDelete: "onRowDelete", onRowGotoList: "onRowGotoList", onRowEdit: "onRowEdit", onRowPrint: "onRowPrint", onRowHistory: "onRowHistory", onRowOption: "onRowOption", onRowLanguage: "onRowLanguage", onRowSelection: "onRowSelection", onShiftDoubleClick: "onShiftDoubleClick", rowCountChanged: "rowCountChanged", selectionChanged: "selectionChanged", expansionChanged: "expansionChanged", cellOptionSelect: "cellOptionSelect", onRowCellOption: "onRowCellOption", goToFirst: "goToFirst", goToPrevious: "goToPrevious", goToNext: "goToNext", goToLast: "goToLast", pageSizeChange: "pageSizeChange" }, providers: [
4706
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: OneGridComponent, selector: "one-grid", inputs: { rowNumber: "rowNumber", allowedEdit: "allowedEdit", submitted: "submitted", context: "context", rowData: "rowData", rowSelection: "rowSelection", enableGroupColumnDefs: "enableGroupColumnDefs", groupColumnDefs: "groupColumnDefs", defaultColumnDef: "defaultColumnDef", columnDefs: "columnDefs", pinnedTopRowData: "pinnedTopRowData", pinnedBottomRowData: "pinnedBottomRowData", gridType: "gridType", masterDetail: "masterDetail", masterNode: "masterNode", detailCellRendererParams: "detailCellRendererParams", detailRendererComponent: "detailRendererComponent", treeData: "treeData", treeDataChildrenField: "treeDataChildrenField", autoGroupColumnDef: "autoGroupColumnDef", groupDefaultExpanded: "groupDefaultExpanded", pagination: "pagination", paginate: "paginate" }, outputs: { rowDragEnd: "rowDragEnd", onGridReady: "onGridReady", onRowGotoDetail: "onRowGotoDetail", onRowAddNew: "onRowAddNew", onRowView: "onRowView", onRowLocation: "onRowLocation", onRowDocument: "onRowDocument", onRowCopy: "onRowCopy", onRowDelete: "onRowDelete", onRowGotoList: "onRowGotoList", onRowEdit: "onRowEdit", onRowPrint: "onRowPrint", onRowHistory: "onRowHistory", onRowOption: "onRowOption", onRowLanguage: "onRowLanguage", onRowSelection: "onRowSelection", onShiftDoubleClick: "onShiftDoubleClick", rowCountChanged: "rowCountChanged", selectionChanged: "selectionChanged", expansionChanged: "expansionChanged", cellOptionSelect: "cellOptionSelect", onRowCellOption: "onRowCellOption", goToFirst: "goToFirst", goToPrevious: "goToPrevious", goToNext: "goToNext", goToLast: "goToLast", pageSizeChange: "pageSizeChange" }, providers: [
3165
4707
  OneGridEventService,
3166
4708
  OneGridApi,
3167
4709
  OgColumnService,
3168
4710
  OneGridSelectionService,
3169
4711
  OneGridExpansionService,
3170
4712
  OneGridRowService,
3171
- OneGridCellService
3172
- ], viewQueries: [{ propertyName: "ogRef", first: true, predicate: ["ogRef"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: "<div #ogRef class=\"one-grid\" (copy)=\"onGridCopy($event)\" tabindex=\"0\">\n \n <!-- Headers -->\n <one-grid-header\n [api]=\"api\"\n [enableGroupColumnDefs]=\"enableGroupColumnDefs\"\n [allowedEdit]=\"allowedEdit\"\n [scrollLeft]=\"bodyScrollLeft\"\n />\n\n <!-- Body -->\n <one-grid-body\n [gridCopy$]=\"gridCopy$\"\n [rowData]=\"rowData\"\n [api]=\"api\"\n [rowNumber]=\"rowNumber\"\n [pagination]=\"pagination\"\n [allowedEdit]=\"allowedEdit\"\n [submitted]=\"submitted\"\n [context]=\"context\"\n [treeData]=\"treeData\"\n [gridType]=\"gridType\"\n [masterDetail]=\"masterDetail\"\n [masterNode]=\"masterNode\"\n [detailCellRendererParams]=\"detailCellRendererParams\"\n [detailRendererComponent]=\"detailRendererComponent\"\n [detailGridTemplate]=\"detailGridTpl\"\n [treeDataChildrenField]=\"treeDataChildrenField\"\n [autoGroupColumnDef]=\"autoGroupColumnDef\"\n [groupDefaultExpanded]=\"groupDefaultExpanded\"\n (expansionChanged)=\"invokeExpansionChangedClick($event)\"\n (onRowGotoDetail)=\"invokeRowActionGotoDetail($event)\"\n (onRowAddNew)=\"invokeRowActionAddNew($event)\"\n (onRowView)=\"invokeRowActionView($event)\"\n (onRowLocation)=\"invokeRowActionLocation($event)\"\n (onRowDocument)=\"invokeRowActionDocument($event)\"\n (onRowCopy)=\"invokeRowActionCopy($event)\"\n (onRowDelete)=\"invokeRowActionDelete($event)\"\n (onRowGotoList)=\"invokeRowActionGotoList($event)\"\n (onRowEdit)=\"invokeRowActionEdit($event)\"\n (onRowPrint)=\"invokeRowActionPrint($event)\"\n (onRowHistory)=\"invokeRowActionHistory($event)\"\n (onRowOption)=\"invokeRowActionOption($event)\"\n (onRowLanguage)=\"invokeRowActionLanguage($event)\"\n (onRowCellOption)=\"invokeRowActionCellOption($event)\"\n (onRowSelection)=\"invokeRowActionSelection($event)\"\n (onShiftDoubleClick)=\"invokeShiftDoubleClick($event)\"\n (rowCountChanged)=\"invokeRowCountChanged($event)\"\n (cellOptionSelect)=\"cellOptionMenuSelected($event)\"\n (bodyScroll)=\"onBodyScroll($event)\"\n />\n \n <!-- Pagination -->\n @if (pagination) {\n <one-grid-pagination\n [paginate]=\"paginate\"\n (goToFirst)=\"invokeGoToFirst()\"\n (goToLast)=\"invokeGoToLast()\"\n (goToPrevious)=\"invokeGoToPrevious()\"\n (goToNext)=\"invokeGoToNext()\"\n (pageSizeChange)=\"invokePageSizeChange($event)\"\n />\n }\n\n <!-- Master Detail Grid template, rendered inside one-grid-body via ngTemplateOutlet.\n Declared here (self-reference) so one-grid-body's template does not need to\n import one-grid, which would create an unsupported compile-time cycle. -->\n <ng-template #detailGridTpl let-node=\"node\">\n <one-grid\n [style.height.px]=\"node.detailRowHeight\"\n [gridType]=\"'detail'\"\n [masterNode]=\"node\"\n [masterDetail]=\"masterDetail\"\n [defaultColumnDef]=\"detailCellRendererParams.detailGridOptions.defaultColumnDef\"\n [columnDefs]=\"detailCellRendererParams.detailGridOptions.columnDefs\"\n [rowData]=\"node?.detailRowData\"\n [rowNumber]=\"detailCellRendererParams.detailGridOptions?.rowNumber\"\n [allowedEdit]=\"allowedEdit\"\n [submitted]=\"submitted\"\n [context]=\"context\"\n (expansionChanged)=\"invokeExpansionChangedClick($event)\"\n (onRowGotoDetail)=\"invokeRowActionGotoDetail($event)\"\n (onRowAddNew)=\"invokeRowActionAddNew($event)\"\n (onRowView)=\"invokeRowActionView($event)\"\n (onRowLocation)=\"invokeRowActionLocation($event)\"\n (onRowDocument)=\"invokeRowActionDocument($event)\"\n (onRowCopy)=\"invokeRowActionCopy($event)\"\n (onRowDelete)=\"invokeRowActionDelete($event)\"\n (onRowGotoList)=\"invokeRowActionGotoList($event)\"\n (onRowEdit)=\"invokeRowActionEdit($event)\"\n (onRowPrint)=\"invokeRowActionPrint($event)\"\n (onRowHistory)=\"invokeRowActionHistory($event)\"\n (onRowOption)=\"invokeRowActionOption($event)\"\n (onRowLanguage)=\"invokeRowActionLanguage($event)\"\n (onRowCellOption)=\"invokeRowActionCellOption($event)\"\n (onGridReady)=\"onDetailGridReady($event)\"\n />\n </ng-template>\n</div>", styles: [""], dependencies: [{ kind: "component", type: OneGridComponent, selector: "one-grid", inputs: ["rowNumber", "allowedEdit", "submitted", "context", "rowData", "rowSelection", "enableGroupColumnDefs", "groupColumnDefs", "defaultColumnDef", "columnDefs", "gridType", "masterDetail", "masterNode", "detailCellRendererParams", "detailRendererComponent", "treeData", "treeDataChildrenField", "autoGroupColumnDef", "groupDefaultExpanded", "pagination", "paginate"], outputs: ["onGridReady", "onRowGotoDetail", "onRowAddNew", "onRowView", "onRowLocation", "onRowDocument", "onRowCopy", "onRowDelete", "onRowGotoList", "onRowEdit", "onRowPrint", "onRowHistory", "onRowOption", "onRowLanguage", "onRowSelection", "onShiftDoubleClick", "rowCountChanged", "selectionChanged", "expansionChanged", "cellOptionSelect", "onRowCellOption", "goToFirst", "goToPrevious", "goToNext", "goToLast", "pageSizeChange"] }, { kind: "component", type: OneGridHeaderComponent, selector: "one-grid-header", inputs: ["scrollLeft", "allowedEdit", "enableGroupColumnDefs", "api"] }, { kind: "component", type: OneGridBodyComponent, selector: "one-grid-body", inputs: ["rowData", "rowNumber", "api", "pagination", "allowedEdit", "enableGroupColumnDefs", "submitted", "context", "getRowId", "gridCopy$", "gridType", "masterDetail", "masterNode", "detailRowMaxHeight", "detailCellRendererParams", "detailGridTemplate", "detailRendererComponent", "treeData", "treeDataChildrenField", "autoGroupColumnDef", "groupDefaultExpanded"], outputs: ["onRowGotoDetail", "onRowAddNew", "onRowView", "onRowCopy", "onRowLocation", "onRowDocument", "onRowDelete", "onRowGotoList", "onRowEdit", "onRowPrint", "onRowHistory", "onRowOption", "onRowLanguage", "onRowCellOption", "onRowSelection", "onShiftDoubleClick", "rowCountChanged", "expansionChanged", "bodyScroll", "cellOptionSelect"] }, { kind: "component", type: OneGridPaginationComponent, selector: "one-grid-pagination", inputs: ["paginate"], outputs: ["goToFirst", "goToPrevious", "goToNext", "goToLast", "pageSizeChange"] }] }); }
4713
+ OneGridCellService,
4714
+ OneGridRowDragService
4715
+ ], viewQueries: [{ propertyName: "ogRef", first: true, predicate: ["ogRef"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: "<!-- No tabindex here. The tab stop belongs on the body viewport, which is the\n element that actually scrolls; a focusable wrapper around a scrollable\n child is reachable but cannot be scrolled with the keyboard. The copy\n handler is unaffected: a copy event fired at the focused viewport bubbles\n up to here. -->\n<div #ogRef class=\"one-grid\" (copy)=\"onGridCopy($event)\">\n \n <!-- Headers -->\n <one-grid-header\n [api]=\"api\"\n [enableGroupColumnDefs]=\"enableGroupColumnDefs\"\n [allowedEdit]=\"allowedEdit\"\n [scrollLeft]=\"bodyScrollLeft\"\n />\n\n <!-- Body -->\n <one-grid-body\n [gridCopy$]=\"gridCopy$\"\n [rowData]=\"rowData\"\n [pinnedTopRowData]=\"pinnedTopRowData\"\n [pinnedBottomRowData]=\"pinnedBottomRowData\"\n [api]=\"api\"\n [rowNumber]=\"rowNumber\"\n [pagination]=\"pagination\"\n [allowedEdit]=\"allowedEdit\"\n [submitted]=\"submitted\"\n [context]=\"context\"\n [treeData]=\"treeData\"\n [gridType]=\"gridType\"\n [masterDetail]=\"masterDetail\"\n [masterNode]=\"masterNode\"\n [detailCellRendererParams]=\"detailCellRendererParams\"\n [detailRendererComponent]=\"detailRendererComponent\"\n [detailGridTemplate]=\"detailGridTpl\"\n [treeDataChildrenField]=\"treeDataChildrenField\"\n [autoGroupColumnDef]=\"autoGroupColumnDef\"\n [groupDefaultExpanded]=\"groupDefaultExpanded\"\n (rowDragEnd)=\"invokeRowDragEnd($event)\"\n (expansionChanged)=\"invokeExpansionChangedClick($event)\"\n (onRowGotoDetail)=\"invokeRowActionGotoDetail($event)\"\n (onRowAddNew)=\"invokeRowActionAddNew($event)\"\n (onRowView)=\"invokeRowActionView($event)\"\n (onRowLocation)=\"invokeRowActionLocation($event)\"\n (onRowDocument)=\"invokeRowActionDocument($event)\"\n (onRowCopy)=\"invokeRowActionCopy($event)\"\n (onRowDelete)=\"invokeRowActionDelete($event)\"\n (onRowGotoList)=\"invokeRowActionGotoList($event)\"\n (onRowEdit)=\"invokeRowActionEdit($event)\"\n (onRowPrint)=\"invokeRowActionPrint($event)\"\n (onRowHistory)=\"invokeRowActionHistory($event)\"\n (onRowOption)=\"invokeRowActionOption($event)\"\n (onRowLanguage)=\"invokeRowActionLanguage($event)\"\n (onRowCellOption)=\"invokeRowActionCellOption($event)\"\n (onRowSelection)=\"invokeRowActionSelection($event)\"\n (onShiftDoubleClick)=\"invokeShiftDoubleClick($event)\"\n (rowCountChanged)=\"invokeRowCountChanged($event)\"\n (cellOptionSelect)=\"cellOptionMenuSelected($event)\"\n (bodyScroll)=\"onBodyScroll($event)\"\n />\n \n <!-- Pagination -->\n @if (pagination) {\n <one-grid-pagination\n [paginate]=\"paginate\"\n (goToFirst)=\"invokeGoToFirst()\"\n (goToLast)=\"invokeGoToLast()\"\n (goToPrevious)=\"invokeGoToPrevious()\"\n (goToNext)=\"invokeGoToNext()\"\n (pageSizeChange)=\"invokePageSizeChange($event)\"\n />\n }\n\n <!-- Master Detail Grid template, rendered inside one-grid-body via ngTemplateOutlet.\n Declared here (self-reference) so one-grid-body's template does not need to\n import one-grid, which would create an unsupported compile-time cycle. -->\n <ng-template #detailGridTpl let-node=\"node\">\n <one-grid\n [style.height.px]=\"node.detailRowHeight\"\n [gridType]=\"'detail'\"\n [masterNode]=\"node\"\n [masterDetail]=\"masterDetail\"\n [defaultColumnDef]=\"detailCellRendererParams.detailGridOptions.defaultColumnDef\"\n [columnDefs]=\"detailCellRendererParams.detailGridOptions.columnDefs\"\n [rowData]=\"node?.detailRowData\"\n [rowNumber]=\"detailCellRendererParams.detailGridOptions?.rowNumber\"\n [allowedEdit]=\"allowedEdit\"\n [submitted]=\"submitted\"\n [context]=\"context\"\n (expansionChanged)=\"invokeExpansionChangedClick($event)\"\n (onRowGotoDetail)=\"invokeRowActionGotoDetail($event)\"\n (onRowAddNew)=\"invokeRowActionAddNew($event)\"\n (onRowView)=\"invokeRowActionView($event)\"\n (onRowLocation)=\"invokeRowActionLocation($event)\"\n (onRowDocument)=\"invokeRowActionDocument($event)\"\n (onRowCopy)=\"invokeRowActionCopy($event)\"\n (onRowDelete)=\"invokeRowActionDelete($event)\"\n (onRowGotoList)=\"invokeRowActionGotoList($event)\"\n (onRowEdit)=\"invokeRowActionEdit($event)\"\n (onRowPrint)=\"invokeRowActionPrint($event)\"\n (onRowHistory)=\"invokeRowActionHistory($event)\"\n (onRowOption)=\"invokeRowActionOption($event)\"\n (onRowLanguage)=\"invokeRowActionLanguage($event)\"\n (onRowCellOption)=\"invokeRowActionCellOption($event)\"\n (onGridReady)=\"onDetailGridReady($event)\"\n />\n </ng-template>\n</div>", styles: [""], dependencies: [{ kind: "component", type: OneGridComponent, selector: "one-grid", inputs: ["rowNumber", "allowedEdit", "submitted", "context", "rowData", "rowSelection", "enableGroupColumnDefs", "groupColumnDefs", "defaultColumnDef", "columnDefs", "pinnedTopRowData", "pinnedBottomRowData", "gridType", "masterDetail", "masterNode", "detailCellRendererParams", "detailRendererComponent", "treeData", "treeDataChildrenField", "autoGroupColumnDef", "groupDefaultExpanded", "pagination", "paginate"], outputs: ["rowDragEnd", "onGridReady", "onRowGotoDetail", "onRowAddNew", "onRowView", "onRowLocation", "onRowDocument", "onRowCopy", "onRowDelete", "onRowGotoList", "onRowEdit", "onRowPrint", "onRowHistory", "onRowOption", "onRowLanguage", "onRowSelection", "onShiftDoubleClick", "rowCountChanged", "selectionChanged", "expansionChanged", "cellOptionSelect", "onRowCellOption", "goToFirst", "goToPrevious", "goToNext", "goToLast", "pageSizeChange"] }, { kind: "component", type: OneGridHeaderComponent, selector: "one-grid-header", inputs: ["scrollLeft", "allowedEdit", "enableGroupColumnDefs", "api"] }, { kind: "component", type: OneGridBodyComponent, selector: "one-grid-body", inputs: ["rowData", "rowNumber", "api", "pagination", "allowedEdit", "enableGroupColumnDefs", "submitted", "context", "getRowId", "gridCopy$", "pinnedTopRowData", "pinnedBottomRowData", "gridType", "masterDetail", "masterNode", "detailRowMaxHeight", "detailCellRendererParams", "detailGridTemplate", "detailRendererComponent", "treeData", "treeDataChildrenField", "autoGroupColumnDef", "groupDefaultExpanded"], outputs: ["onRowGotoDetail", "onRowAddNew", "onRowView", "onRowCopy", "onRowLocation", "onRowDocument", "onRowDelete", "onRowGotoList", "onRowEdit", "onRowPrint", "onRowHistory", "onRowOption", "onRowLanguage", "onRowCellOption", "onRowSelection", "onShiftDoubleClick", "rowCountChanged", "expansionChanged", "bodyScroll", "cellOptionSelect", "rowDragEnd"] }, { kind: "component", type: OneGridPaginationComponent, selector: "one-grid-pagination", inputs: ["paginate"], outputs: ["goToFirst", "goToPrevious", "goToNext", "goToLast", "pageSizeChange"] }] }); }
3173
4716
  }
3174
4717
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OneGridComponent, decorators: [{
3175
4718
  type: Component,
@@ -3180,8 +4723,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
3180
4723
  OneGridSelectionService,
3181
4724
  OneGridExpansionService,
3182
4725
  OneGridRowService,
3183
- OneGridCellService
3184
- ], template: "<div #ogRef class=\"one-grid\" (copy)=\"onGridCopy($event)\" tabindex=\"0\">\n \n <!-- Headers -->\n <one-grid-header\n [api]=\"api\"\n [enableGroupColumnDefs]=\"enableGroupColumnDefs\"\n [allowedEdit]=\"allowedEdit\"\n [scrollLeft]=\"bodyScrollLeft\"\n />\n\n <!-- Body -->\n <one-grid-body\n [gridCopy$]=\"gridCopy$\"\n [rowData]=\"rowData\"\n [api]=\"api\"\n [rowNumber]=\"rowNumber\"\n [pagination]=\"pagination\"\n [allowedEdit]=\"allowedEdit\"\n [submitted]=\"submitted\"\n [context]=\"context\"\n [treeData]=\"treeData\"\n [gridType]=\"gridType\"\n [masterDetail]=\"masterDetail\"\n [masterNode]=\"masterNode\"\n [detailCellRendererParams]=\"detailCellRendererParams\"\n [detailRendererComponent]=\"detailRendererComponent\"\n [detailGridTemplate]=\"detailGridTpl\"\n [treeDataChildrenField]=\"treeDataChildrenField\"\n [autoGroupColumnDef]=\"autoGroupColumnDef\"\n [groupDefaultExpanded]=\"groupDefaultExpanded\"\n (expansionChanged)=\"invokeExpansionChangedClick($event)\"\n (onRowGotoDetail)=\"invokeRowActionGotoDetail($event)\"\n (onRowAddNew)=\"invokeRowActionAddNew($event)\"\n (onRowView)=\"invokeRowActionView($event)\"\n (onRowLocation)=\"invokeRowActionLocation($event)\"\n (onRowDocument)=\"invokeRowActionDocument($event)\"\n (onRowCopy)=\"invokeRowActionCopy($event)\"\n (onRowDelete)=\"invokeRowActionDelete($event)\"\n (onRowGotoList)=\"invokeRowActionGotoList($event)\"\n (onRowEdit)=\"invokeRowActionEdit($event)\"\n (onRowPrint)=\"invokeRowActionPrint($event)\"\n (onRowHistory)=\"invokeRowActionHistory($event)\"\n (onRowOption)=\"invokeRowActionOption($event)\"\n (onRowLanguage)=\"invokeRowActionLanguage($event)\"\n (onRowCellOption)=\"invokeRowActionCellOption($event)\"\n (onRowSelection)=\"invokeRowActionSelection($event)\"\n (onShiftDoubleClick)=\"invokeShiftDoubleClick($event)\"\n (rowCountChanged)=\"invokeRowCountChanged($event)\"\n (cellOptionSelect)=\"cellOptionMenuSelected($event)\"\n (bodyScroll)=\"onBodyScroll($event)\"\n />\n \n <!-- Pagination -->\n @if (pagination) {\n <one-grid-pagination\n [paginate]=\"paginate\"\n (goToFirst)=\"invokeGoToFirst()\"\n (goToLast)=\"invokeGoToLast()\"\n (goToPrevious)=\"invokeGoToPrevious()\"\n (goToNext)=\"invokeGoToNext()\"\n (pageSizeChange)=\"invokePageSizeChange($event)\"\n />\n }\n\n <!-- Master Detail Grid template, rendered inside one-grid-body via ngTemplateOutlet.\n Declared here (self-reference) so one-grid-body's template does not need to\n import one-grid, which would create an unsupported compile-time cycle. -->\n <ng-template #detailGridTpl let-node=\"node\">\n <one-grid\n [style.height.px]=\"node.detailRowHeight\"\n [gridType]=\"'detail'\"\n [masterNode]=\"node\"\n [masterDetail]=\"masterDetail\"\n [defaultColumnDef]=\"detailCellRendererParams.detailGridOptions.defaultColumnDef\"\n [columnDefs]=\"detailCellRendererParams.detailGridOptions.columnDefs\"\n [rowData]=\"node?.detailRowData\"\n [rowNumber]=\"detailCellRendererParams.detailGridOptions?.rowNumber\"\n [allowedEdit]=\"allowedEdit\"\n [submitted]=\"submitted\"\n [context]=\"context\"\n (expansionChanged)=\"invokeExpansionChangedClick($event)\"\n (onRowGotoDetail)=\"invokeRowActionGotoDetail($event)\"\n (onRowAddNew)=\"invokeRowActionAddNew($event)\"\n (onRowView)=\"invokeRowActionView($event)\"\n (onRowLocation)=\"invokeRowActionLocation($event)\"\n (onRowDocument)=\"invokeRowActionDocument($event)\"\n (onRowCopy)=\"invokeRowActionCopy($event)\"\n (onRowDelete)=\"invokeRowActionDelete($event)\"\n (onRowGotoList)=\"invokeRowActionGotoList($event)\"\n (onRowEdit)=\"invokeRowActionEdit($event)\"\n (onRowPrint)=\"invokeRowActionPrint($event)\"\n (onRowHistory)=\"invokeRowActionHistory($event)\"\n (onRowOption)=\"invokeRowActionOption($event)\"\n (onRowLanguage)=\"invokeRowActionLanguage($event)\"\n (onRowCellOption)=\"invokeRowActionCellOption($event)\"\n (onGridReady)=\"onDetailGridReady($event)\"\n />\n </ng-template>\n</div>" }]
4726
+ OneGridCellService,
4727
+ OneGridRowDragService
4728
+ ], template: "<!-- No tabindex here. The tab stop belongs on the body viewport, which is the\n element that actually scrolls; a focusable wrapper around a scrollable\n child is reachable but cannot be scrolled with the keyboard. The copy\n handler is unaffected: a copy event fired at the focused viewport bubbles\n up to here. -->\n<div #ogRef class=\"one-grid\" (copy)=\"onGridCopy($event)\">\n \n <!-- Headers -->\n <one-grid-header\n [api]=\"api\"\n [enableGroupColumnDefs]=\"enableGroupColumnDefs\"\n [allowedEdit]=\"allowedEdit\"\n [scrollLeft]=\"bodyScrollLeft\"\n />\n\n <!-- Body -->\n <one-grid-body\n [gridCopy$]=\"gridCopy$\"\n [rowData]=\"rowData\"\n [pinnedTopRowData]=\"pinnedTopRowData\"\n [pinnedBottomRowData]=\"pinnedBottomRowData\"\n [api]=\"api\"\n [rowNumber]=\"rowNumber\"\n [pagination]=\"pagination\"\n [allowedEdit]=\"allowedEdit\"\n [submitted]=\"submitted\"\n [context]=\"context\"\n [treeData]=\"treeData\"\n [gridType]=\"gridType\"\n [masterDetail]=\"masterDetail\"\n [masterNode]=\"masterNode\"\n [detailCellRendererParams]=\"detailCellRendererParams\"\n [detailRendererComponent]=\"detailRendererComponent\"\n [detailGridTemplate]=\"detailGridTpl\"\n [treeDataChildrenField]=\"treeDataChildrenField\"\n [autoGroupColumnDef]=\"autoGroupColumnDef\"\n [groupDefaultExpanded]=\"groupDefaultExpanded\"\n (rowDragEnd)=\"invokeRowDragEnd($event)\"\n (expansionChanged)=\"invokeExpansionChangedClick($event)\"\n (onRowGotoDetail)=\"invokeRowActionGotoDetail($event)\"\n (onRowAddNew)=\"invokeRowActionAddNew($event)\"\n (onRowView)=\"invokeRowActionView($event)\"\n (onRowLocation)=\"invokeRowActionLocation($event)\"\n (onRowDocument)=\"invokeRowActionDocument($event)\"\n (onRowCopy)=\"invokeRowActionCopy($event)\"\n (onRowDelete)=\"invokeRowActionDelete($event)\"\n (onRowGotoList)=\"invokeRowActionGotoList($event)\"\n (onRowEdit)=\"invokeRowActionEdit($event)\"\n (onRowPrint)=\"invokeRowActionPrint($event)\"\n (onRowHistory)=\"invokeRowActionHistory($event)\"\n (onRowOption)=\"invokeRowActionOption($event)\"\n (onRowLanguage)=\"invokeRowActionLanguage($event)\"\n (onRowCellOption)=\"invokeRowActionCellOption($event)\"\n (onRowSelection)=\"invokeRowActionSelection($event)\"\n (onShiftDoubleClick)=\"invokeShiftDoubleClick($event)\"\n (rowCountChanged)=\"invokeRowCountChanged($event)\"\n (cellOptionSelect)=\"cellOptionMenuSelected($event)\"\n (bodyScroll)=\"onBodyScroll($event)\"\n />\n \n <!-- Pagination -->\n @if (pagination) {\n <one-grid-pagination\n [paginate]=\"paginate\"\n (goToFirst)=\"invokeGoToFirst()\"\n (goToLast)=\"invokeGoToLast()\"\n (goToPrevious)=\"invokeGoToPrevious()\"\n (goToNext)=\"invokeGoToNext()\"\n (pageSizeChange)=\"invokePageSizeChange($event)\"\n />\n }\n\n <!-- Master Detail Grid template, rendered inside one-grid-body via ngTemplateOutlet.\n Declared here (self-reference) so one-grid-body's template does not need to\n import one-grid, which would create an unsupported compile-time cycle. -->\n <ng-template #detailGridTpl let-node=\"node\">\n <one-grid\n [style.height.px]=\"node.detailRowHeight\"\n [gridType]=\"'detail'\"\n [masterNode]=\"node\"\n [masterDetail]=\"masterDetail\"\n [defaultColumnDef]=\"detailCellRendererParams.detailGridOptions.defaultColumnDef\"\n [columnDefs]=\"detailCellRendererParams.detailGridOptions.columnDefs\"\n [rowData]=\"node?.detailRowData\"\n [rowNumber]=\"detailCellRendererParams.detailGridOptions?.rowNumber\"\n [allowedEdit]=\"allowedEdit\"\n [submitted]=\"submitted\"\n [context]=\"context\"\n (expansionChanged)=\"invokeExpansionChangedClick($event)\"\n (onRowGotoDetail)=\"invokeRowActionGotoDetail($event)\"\n (onRowAddNew)=\"invokeRowActionAddNew($event)\"\n (onRowView)=\"invokeRowActionView($event)\"\n (onRowLocation)=\"invokeRowActionLocation($event)\"\n (onRowDocument)=\"invokeRowActionDocument($event)\"\n (onRowCopy)=\"invokeRowActionCopy($event)\"\n (onRowDelete)=\"invokeRowActionDelete($event)\"\n (onRowGotoList)=\"invokeRowActionGotoList($event)\"\n (onRowEdit)=\"invokeRowActionEdit($event)\"\n (onRowPrint)=\"invokeRowActionPrint($event)\"\n (onRowHistory)=\"invokeRowActionHistory($event)\"\n (onRowOption)=\"invokeRowActionOption($event)\"\n (onRowLanguage)=\"invokeRowActionLanguage($event)\"\n (onRowCellOption)=\"invokeRowActionCellOption($event)\"\n (onGridReady)=\"onDetailGridReady($event)\"\n />\n </ng-template>\n</div>" }]
3185
4729
  }], ctorParameters: () => [{ type: OneGridApi }, { type: OneGridEventService }, { type: i0.ElementRef }], propDecorators: { ogRef: [{
3186
4730
  type: ViewChild,
3187
4731
  args: ['ogRef', { static: true }]
@@ -3205,6 +4749,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
3205
4749
  type: Input
3206
4750
  }], columnDefs: [{
3207
4751
  type: Input
4752
+ }], pinnedTopRowData: [{
4753
+ type: Input
4754
+ }], pinnedBottomRowData: [{
4755
+ type: Input
3208
4756
  }], gridType: [{
3209
4757
  type: Input
3210
4758
  }], masterDetail: [{
@@ -3223,6 +4771,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
3223
4771
  type: Input
3224
4772
  }], groupDefaultExpanded: [{
3225
4773
  type: Input
4774
+ }], rowDragEnd: [{
4775
+ type: Output
3226
4776
  }], onGridReady: [{
3227
4777
  type: Output
3228
4778
  }], onRowGotoDetail: [{
@@ -3385,7 +4935,7 @@ class OneGridCellCurrencyComponent {
3385
4935
  this.unsubscribe?.();
3386
4936
  }
3387
4937
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OneGridCellCurrencyComponent, deps: [{ token: OneGridCellService }, { token: OneGridEventService }], target: i0.ɵɵFactoryTarget.Component }); }
3388
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: OneGridCellCurrencyComponent, selector: "app-one-grid-cell-currency", inputs: { allowedEdit: "allowedEdit" }, ngImport: i0, template: "<div class=\"cell-wrapper\"\n [class.editable]=\"isEditable\"\n [attr.data-cell-id]=\"getCellField(params.node?.rowIndex, params.columnDef?.field)\"\n [style.min-width.px]=\"params.columnDef?.minWidth\"\n [style.max-width.px]=\"params.columnDef?.maxWidth\">\n\n @if (params.node?.editing?.[params.columnDef?.field]) {\n <input class=\"one-cell-input\"\n name=\"{{ params.node?.rowIndex }}_{{ params.columnDef?.field }}\"\n oneCellInputValidator\n validationType=\"number\"\n [placeholder]=\"params.columnDef?.cellRendererParams?.placeholder || ''\"\n type=\"text\"\n [(ngModel)]=\"value\"\n [maxLength]=\"250\"\n (blur)=\"stopEdit(params.node, params.columnDef, $event)\"\n (keydown.enter)=\"stopEdit(params.node, params.columnDef, $event)\"\n (keydown.esc)=\"cancelEdit(params.node, params.columnDef)\" />\n }\n @else {\n <div class=\"d-flex justify-content-between px-2 pt-1\"\n [class.one-cell-allowed-edit-active]=\"isEditable\"\n (dblclick)=\"startEdit(params.node, params.columnDef)\">\n <span class=\"currency-code\" style=\"font-weight: bold;\">{{ this.params?.data?.currency?.currencyCode || 'USD' }}</span>\n <span class=\"amount-value\">\n {{ result | number:(this.params?.field === 'amount' ? '1.3-3' : '1.2-2') }}\n </span>\n </div>\n }\n</div>\n", styles: [".one-cell-input{position:absolute;top:0;left:0;width:calc(100% - 1px);height:100%;padding:0 6px;background-color:var(--one-grid-input-bg, var(--bs-input-bg, #fff));border:1px solid var(--one-grid-success, var(--bs-success, #198754));border-radius:var(--one-grid-action-btn-radius, 4px);z-index:5}.one-cell-input:focus{background:var(--one-grid-input-bg, var(--bs-input-bg, #fff));border:1px solid var(--one-grid-success, var(--bs-success, #198754))}:host{display:block;width:100%;height:100%;position:relative}.cell-wrapper{position:relative;width:100%;height:100%}.cell-wrapper.editable:before{content:\"\";position:absolute;top:4px;right:4px;width:4px;height:4px;background-color:var(--one-grid-success, var(--bs-success, #198754));border-radius:50%;z-index:1;opacity:.5;transition:opacity .2s ease}.cell-wrapper.editable:hover:before{opacity:1}\n"], dependencies: [{ kind: "directive", type: i3$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: OneCellInputValidatorDirective, selector: "[oneCellInputValidator]", inputs: ["validationType", "regex"] }, { kind: "pipe", type: i2.DecimalPipe, name: "number" }] }); }
4938
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: OneGridCellCurrencyComponent, selector: "app-one-grid-cell-currency", inputs: { allowedEdit: "allowedEdit" }, ngImport: i0, template: "<div class=\"cell-wrapper\"\n [class.editable]=\"isEditable\"\n [attr.data-cell-id]=\"getCellField(params.node?.rowIndex, params.columnDef?.field)\"\n [style.min-width.px]=\"params.columnDef?.minWidth\"\n [style.max-width.px]=\"params.columnDef?.maxWidth\">\n\n @if (params.node?.editing?.[params.columnDef?.field]) {\n <input class=\"one-cell-input\"\n name=\"{{ params.node?.rowIndex }}_{{ params.columnDef?.field }}\"\n oneCellInputValidator\n validationType=\"number\"\n [placeholder]=\"params.columnDef?.cellRendererParams?.placeholder || ''\"\n type=\"text\"\n [(ngModel)]=\"value\"\n [maxLength]=\"250\"\n (blur)=\"stopEdit(params.node, params.columnDef, $event)\"\n (keydown.enter)=\"stopEdit(params.node, params.columnDef, $event)\"\n (keydown.esc)=\"cancelEdit(params.node, params.columnDef)\" />\n }\n @else {\n <div class=\"d-flex justify-content-between px-2 pt-1\"\n [class.one-cell-allowed-edit-active]=\"isEditable\"\n (dblclick)=\"startEdit(params.node, params.columnDef)\">\n <span class=\"currency-code\" style=\"font-weight: bold;\">{{ this.params?.data?.currency?.currencyCode || 'USD' }}</span>\n <span class=\"amount-value\">\n {{ result | number:(this.params?.field === 'amount' ? '1.3-3' : '1.2-2') }}\n </span>\n </div>\n }\n</div>\n", styles: [".one-cell-input{position:absolute;top:0;left:0;width:calc(100% - 1px);height:100%;padding:0 6px;background-color:var(--one-grid-input-bg, var(--bs-input-bg, #fff));border:1px solid var(--one-grid-success, var(--bs-success, #198754));border-radius:var(--one-grid-action-btn-radius, 4px);z-index:5}.one-cell-input:focus{background:var(--one-grid-input-bg, var(--bs-input-bg, #fff));border:1px solid var(--one-grid-success, var(--bs-success, #198754))}:host{display:block;width:100%;height:100%;position:relative}.cell-wrapper{position:relative;width:100%;height:100%}.cell-wrapper.editable:before{content:\"\";position:absolute;top:4px;right:4px;width:4px;height:4px;background-color:var(--one-grid-success, var(--bs-success, #198754));border-radius:50%;z-index:1;opacity:.5;transition:opacity .2s ease}.cell-wrapper.editable:hover:before{opacity:1}\n"], dependencies: [{ kind: "directive", type: i3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: OneCellInputValidatorDirective, selector: "[oneCellInputValidator]", inputs: ["validationType", "regex"] }, { kind: "pipe", type: i2.DecimalPipe, name: "number" }] }); }
3389
4939
  }
3390
4940
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OneGridCellCurrencyComponent, decorators: [{
3391
4941
  type: Component,
@@ -3481,7 +5031,6 @@ class OneGridModule {
3481
5031
  CellSelectedPipe,
3482
5032
  CellActionButtonEnablePipe,
3483
5033
  CellNumberValuePipe,
3484
- CellDisableResolverPipe,
3485
5034
  CellFieldPipe,
3486
5035
  CellKeyPipe,
3487
5036
  CellLevelPaddingPipe,
@@ -3495,17 +5044,14 @@ class OneGridModule {
3495
5044
  OneCellRendererComponentDirective,
3496
5045
  OneCellTextExpandDirective,
3497
5046
  OneGridCellCurrencyComponent,
3498
- OneGridCellOptionMenuComponent], imports: [CommonModule,
5047
+ OneGridCellOptionMenuComponent,
5048
+ OneGridRowDragHandleComponent], imports: [CommonModule,
3499
5049
  FormsModule,
3500
5050
  ScrollingModule,
3501
- NgbTypeahead,
3502
- NgbHighlight,
3503
- ReactiveFormsModule,
3504
- CdkDragPlaceholder, i3.LucideAngularModule], exports: [OneGridComponent] }); }
5051
+ ReactiveFormsModule, i2$1.LucideAngularModule], exports: [OneGridComponent] }); }
3505
5052
  static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OneGridModule, imports: [CommonModule,
3506
5053
  FormsModule,
3507
5054
  ScrollingModule,
3508
- NgbHighlight,
3509
5055
  ReactiveFormsModule,
3510
5056
  LucideAngularModule.pick(icons)] }); }
3511
5057
  }
@@ -3531,7 +5077,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
3531
5077
  CellSelectedPipe,
3532
5078
  CellActionButtonEnablePipe,
3533
5079
  CellNumberValuePipe,
3534
- CellDisableResolverPipe,
3535
5080
  CellFieldPipe,
3536
5081
  CellKeyPipe,
3537
5082
  CellLevelPaddingPipe,
@@ -3545,16 +5090,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
3545
5090
  OneCellRendererComponentDirective,
3546
5091
  OneCellTextExpandDirective,
3547
5092
  OneGridCellCurrencyComponent,
3548
- OneGridCellOptionMenuComponent
5093
+ OneGridCellOptionMenuComponent,
5094
+ OneGridRowDragHandleComponent
3549
5095
  ],
3550
5096
  imports: [
3551
5097
  CommonModule,
3552
5098
  FormsModule,
3553
5099
  ScrollingModule,
3554
- NgbTypeahead,
3555
- NgbHighlight,
3556
5100
  ReactiveFormsModule,
3557
- CdkDragPlaceholder,
3558
5101
  LucideAngularModule.pick(icons),
3559
5102
  ],
3560
5103
  exports: [OneGridComponent]
@@ -3569,5 +5112,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
3569
5112
  * Generated bundle index. Do not edit.
3570
5113
  */
3571
5114
 
3572
- export { OneGridApi, OneGridCellCurrencyComponent, OneGridCellService, OneGridComponent, OneGridEventService, OneGridExpansionService, OneGridModule, OneGridSelectionService, OneGridStatusComponent, OneGridTreeDataService, OnePageSizeOption, OneRowNode, ValidationRules, Validators, applyDefaultColumnDef, getCellError, resolveOneCellRendererParams, resolveOneColumDefParams, resolveOneValueGetterParams };
5115
+ export { ONE_DEFAULT_COLUMN_WIDTH, OneGridApi, OneGridCellCurrencyComponent, OneGridCellService, OneGridComponent, OneGridEventService, OneGridExpansionService, OneGridModule, OneGridRowDragService, OneGridSelectionService, OneGridStatusComponent, OneGridTreeDataService, OnePageSizeOption, OneRowNode, ValidationRules, Validators, applyDefaultColumnDef, getCellError, resolveColumnWidth, resolveOneCellRendererParams, resolveOneColumDefParams, resolveOneValueGetterParams, resolveRowHeight };
3573
5116
  //# sourceMappingURL=one-grid-core-angular.mjs.map