@ngneers/controls 0.0.1-next.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 (186) hide show
  1. package/fesm2022/ngneers-controls-accordion.mjs +234 -0
  2. package/fesm2022/ngneers-controls-accordion.mjs.map +1 -0
  3. package/fesm2022/ngneers-controls-api-ng.mjs +959 -0
  4. package/fesm2022/ngneers-controls-api-ng.mjs.map +1 -0
  5. package/fesm2022/ngneers-controls-api-resize.mjs +712 -0
  6. package/fesm2022/ngneers-controls-api-resize.mjs.map +1 -0
  7. package/fesm2022/ngneers-controls-api.mjs +347 -0
  8. package/fesm2022/ngneers-controls-api.mjs.map +1 -0
  9. package/fesm2022/ngneers-controls-avatar.mjs +77 -0
  10. package/fesm2022/ngneers-controls-avatar.mjs.map +1 -0
  11. package/fesm2022/ngneers-controls-base.mjs +620 -0
  12. package/fesm2022/ngneers-controls-base.mjs.map +1 -0
  13. package/fesm2022/ngneers-controls-breadcrumb.mjs +100 -0
  14. package/fesm2022/ngneers-controls-breadcrumb.mjs.map +1 -0
  15. package/fesm2022/ngneers-controls-button-group.mjs +110 -0
  16. package/fesm2022/ngneers-controls-button-group.mjs.map +1 -0
  17. package/fesm2022/ngneers-controls-button.mjs +83 -0
  18. package/fesm2022/ngneers-controls-button.mjs.map +1 -0
  19. package/fesm2022/ngneers-controls-calendar-time-CJT5AFlA.mjs +74 -0
  20. package/fesm2022/ngneers-controls-calendar-time-CJT5AFlA.mjs.map +1 -0
  21. package/fesm2022/ngneers-controls-calendar.mjs +801 -0
  22. package/fesm2022/ngneers-controls-calendar.mjs.map +1 -0
  23. package/fesm2022/ngneers-controls-checkbox.mjs +63 -0
  24. package/fesm2022/ngneers-controls-checkbox.mjs.map +1 -0
  25. package/fesm2022/ngneers-controls-chip.mjs +55 -0
  26. package/fesm2022/ngneers-controls-chip.mjs.map +1 -0
  27. package/fesm2022/ngneers-controls-default-icons.mjs +82 -0
  28. package/fesm2022/ngneers-controls-default-icons.mjs.map +1 -0
  29. package/fesm2022/ngneers-controls-defer.mjs +72 -0
  30. package/fesm2022/ngneers-controls-defer.mjs.map +1 -0
  31. package/fesm2022/ngneers-controls-dialog.mjs +407 -0
  32. package/fesm2022/ngneers-controls-dialog.mjs.map +1 -0
  33. package/fesm2022/ngneers-controls-directives.mjs +498 -0
  34. package/fesm2022/ngneers-controls-directives.mjs.map +1 -0
  35. package/fesm2022/ngneers-controls-drawer.mjs +271 -0
  36. package/fesm2022/ngneers-controls-drawer.mjs.map +1 -0
  37. package/fesm2022/ngneers-controls-edit-inplace.mjs +127 -0
  38. package/fesm2022/ngneers-controls-edit-inplace.mjs.map +1 -0
  39. package/fesm2022/ngneers-controls-errors.mjs +329 -0
  40. package/fesm2022/ngneers-controls-errors.mjs.map +1 -0
  41. package/fesm2022/ngneers-controls-filter.mjs +708 -0
  42. package/fesm2022/ngneers-controls-filter.mjs.map +1 -0
  43. package/fesm2022/ngneers-controls-hint.mjs +153 -0
  44. package/fesm2022/ngneers-controls-hint.mjs.map +1 -0
  45. package/fesm2022/ngneers-controls-i18n-translations-de.mjs +170 -0
  46. package/fesm2022/ngneers-controls-i18n-translations-de.mjs.map +1 -0
  47. package/fesm2022/ngneers-controls-i18n-translations-en.mjs +170 -0
  48. package/fesm2022/ngneers-controls-i18n-translations-en.mjs.map +1 -0
  49. package/fesm2022/ngneers-controls-i18n.mjs +57 -0
  50. package/fesm2022/ngneers-controls-i18n.mjs.map +1 -0
  51. package/fesm2022/ngneers-controls-icon.mjs +195 -0
  52. package/fesm2022/ngneers-controls-icon.mjs.map +1 -0
  53. package/fesm2022/ngneers-controls-inplace.mjs +150 -0
  54. package/fesm2022/ngneers-controls-inplace.mjs.map +1 -0
  55. package/fesm2022/ngneers-controls-input-field.mjs +165 -0
  56. package/fesm2022/ngneers-controls-input-field.mjs.map +1 -0
  57. package/fesm2022/ngneers-controls-input.mjs +72 -0
  58. package/fesm2022/ngneers-controls-input.mjs.map +1 -0
  59. package/fesm2022/ngneers-controls-item-view.mjs +473 -0
  60. package/fesm2022/ngneers-controls-item-view.mjs.map +1 -0
  61. package/fesm2022/ngneers-controls-list-box.mjs +281 -0
  62. package/fesm2022/ngneers-controls-list-box.mjs.map +1 -0
  63. package/fesm2022/ngneers-controls-mask-input.mjs +888 -0
  64. package/fesm2022/ngneers-controls-mask-input.mjs.map +1 -0
  65. package/fesm2022/ngneers-controls-menu.mjs +371 -0
  66. package/fesm2022/ngneers-controls-menu.mjs.map +1 -0
  67. package/fesm2022/ngneers-controls-message.mjs +30 -0
  68. package/fesm2022/ngneers-controls-message.mjs.map +1 -0
  69. package/fesm2022/ngneers-controls-number-input.mjs +366 -0
  70. package/fesm2022/ngneers-controls-number-input.mjs.map +1 -0
  71. package/fesm2022/ngneers-controls-paginator.mjs +129 -0
  72. package/fesm2022/ngneers-controls-paginator.mjs.map +1 -0
  73. package/fesm2022/ngneers-controls-popover.mjs +215 -0
  74. package/fesm2022/ngneers-controls-popover.mjs.map +1 -0
  75. package/fesm2022/ngneers-controls-progress.mjs +98 -0
  76. package/fesm2022/ngneers-controls-progress.mjs.map +1 -0
  77. package/fesm2022/ngneers-controls-radio.mjs +152 -0
  78. package/fesm2022/ngneers-controls-radio.mjs.map +1 -0
  79. package/fesm2022/ngneers-controls-roving-focus.mjs +323 -0
  80. package/fesm2022/ngneers-controls-roving-focus.mjs.map +1 -0
  81. package/fesm2022/ngneers-controls-scroll-shadow.mjs +65 -0
  82. package/fesm2022/ngneers-controls-scroll-shadow.mjs.map +1 -0
  83. package/fesm2022/ngneers-controls-scroller.mjs +308 -0
  84. package/fesm2022/ngneers-controls-scroller.mjs.map +1 -0
  85. package/fesm2022/ngneers-controls-select-button.mjs +58 -0
  86. package/fesm2022/ngneers-controls-select-button.mjs.map +1 -0
  87. package/fesm2022/ngneers-controls-select.mjs +417 -0
  88. package/fesm2022/ngneers-controls-select.mjs.map +1 -0
  89. package/fesm2022/ngneers-controls-slider.mjs +167 -0
  90. package/fesm2022/ngneers-controls-slider.mjs.map +1 -0
  91. package/fesm2022/ngneers-controls-snackbar.mjs +414 -0
  92. package/fesm2022/ngneers-controls-snackbar.mjs.map +1 -0
  93. package/fesm2022/ngneers-controls-spin-buttons.mjs +126 -0
  94. package/fesm2022/ngneers-controls-spin-buttons.mjs.map +1 -0
  95. package/fesm2022/ngneers-controls-spinner.mjs +184 -0
  96. package/fesm2022/ngneers-controls-spinner.mjs.map +1 -0
  97. package/fesm2022/ngneers-controls-splitter.mjs +604 -0
  98. package/fesm2022/ngneers-controls-splitter.mjs.map +1 -0
  99. package/fesm2022/ngneers-controls-state.mjs +185 -0
  100. package/fesm2022/ngneers-controls-state.mjs.map +1 -0
  101. package/fesm2022/ngneers-controls-switch.mjs +39 -0
  102. package/fesm2022/ngneers-controls-switch.mjs.map +1 -0
  103. package/fesm2022/ngneers-controls-table.mjs +2634 -0
  104. package/fesm2022/ngneers-controls-table.mjs.map +1 -0
  105. package/fesm2022/ngneers-controls-tabs.mjs +336 -0
  106. package/fesm2022/ngneers-controls-tabs.mjs.map +1 -0
  107. package/fesm2022/ngneers-controls-tag.mjs +30 -0
  108. package/fesm2022/ngneers-controls-tag.mjs.map +1 -0
  109. package/fesm2022/ngneers-controls-toast.mjs +350 -0
  110. package/fesm2022/ngneers-controls-toast.mjs.map +1 -0
  111. package/fesm2022/ngneers-controls-toggle-button.mjs +70 -0
  112. package/fesm2022/ngneers-controls-toggle-button.mjs.map +1 -0
  113. package/fesm2022/ngneers-controls-tooltip.mjs +506 -0
  114. package/fesm2022/ngneers-controls-tooltip.mjs.map +1 -0
  115. package/fesm2022/ngneers-controls-tree.mjs +599 -0
  116. package/fesm2022/ngneers-controls-tree.mjs.map +1 -0
  117. package/fesm2022/ngneers-controls-upload.mjs +377 -0
  118. package/fesm2022/ngneers-controls-upload.mjs.map +1 -0
  119. package/fesm2022/ngneers-controls-utils-ng.mjs +411 -0
  120. package/fesm2022/ngneers-controls-utils-ng.mjs.map +1 -0
  121. package/fesm2022/ngneers-controls-utils.mjs +261 -0
  122. package/fesm2022/ngneers-controls-utils.mjs.map +1 -0
  123. package/fesm2022/ngneers-controls.mjs +6 -0
  124. package/fesm2022/ngneers-controls.mjs.map +1 -0
  125. package/package.json +302 -0
  126. package/types/ngneers-controls-accordion.d.ts +139 -0
  127. package/types/ngneers-controls-api-ng.d.ts +364 -0
  128. package/types/ngneers-controls-api-resize.d.ts +318 -0
  129. package/types/ngneers-controls-api.d.ts +145 -0
  130. package/types/ngneers-controls-avatar.d.ts +54 -0
  131. package/types/ngneers-controls-base.d.ts +277 -0
  132. package/types/ngneers-controls-breadcrumb.d.ts +272 -0
  133. package/types/ngneers-controls-button-group.d.ts +46 -0
  134. package/types/ngneers-controls-button.d.ts +48 -0
  135. package/types/ngneers-controls-calendar.d.ts +466 -0
  136. package/types/ngneers-controls-checkbox.d.ts +40 -0
  137. package/types/ngneers-controls-chip.d.ts +202 -0
  138. package/types/ngneers-controls-default-icons.d.ts +5 -0
  139. package/types/ngneers-controls-defer.d.ts +48 -0
  140. package/types/ngneers-controls-dialog.d.ts +399 -0
  141. package/types/ngneers-controls-directives.d.ts +171 -0
  142. package/types/ngneers-controls-drawer.d.ts +308 -0
  143. package/types/ngneers-controls-edit-inplace.d.ts +263 -0
  144. package/types/ngneers-controls-errors.d.ts +155 -0
  145. package/types/ngneers-controls-filter.d.ts +609 -0
  146. package/types/ngneers-controls-hint.d.ts +88 -0
  147. package/types/ngneers-controls-i18n-translations-de.d.ts +164 -0
  148. package/types/ngneers-controls-i18n-translations-en.d.ts +164 -0
  149. package/types/ngneers-controls-i18n.d.ts +186 -0
  150. package/types/ngneers-controls-icon.d.ts +68 -0
  151. package/types/ngneers-controls-inplace.d.ts +98 -0
  152. package/types/ngneers-controls-input-field.d.ts +248 -0
  153. package/types/ngneers-controls-input.d.ts +30 -0
  154. package/types/ngneers-controls-item-view.d.ts +215 -0
  155. package/types/ngneers-controls-list-box.d.ts +334 -0
  156. package/types/ngneers-controls-mask-input.d.ts +434 -0
  157. package/types/ngneers-controls-menu.d.ts +192 -0
  158. package/types/ngneers-controls-message.d.ts +21 -0
  159. package/types/ngneers-controls-number-input.d.ts +146 -0
  160. package/types/ngneers-controls-paginator.d.ts +255 -0
  161. package/types/ngneers-controls-popover.d.ts +116 -0
  162. package/types/ngneers-controls-progress.d.ts +49 -0
  163. package/types/ngneers-controls-radio.d.ts +91 -0
  164. package/types/ngneers-controls-roving-focus.d.ts +107 -0
  165. package/types/ngneers-controls-scroll-shadow.d.ts +25 -0
  166. package/types/ngneers-controls-scroller.d.ts +151 -0
  167. package/types/ngneers-controls-select-button.d.ts +51 -0
  168. package/types/ngneers-controls-select.d.ts +409 -0
  169. package/types/ngneers-controls-slider.d.ts +57 -0
  170. package/types/ngneers-controls-snackbar.d.ts +330 -0
  171. package/types/ngneers-controls-spin-buttons.d.ts +69 -0
  172. package/types/ngneers-controls-spinner.d.ts +117 -0
  173. package/types/ngneers-controls-splitter.d.ts +465 -0
  174. package/types/ngneers-controls-state.d.ts +102 -0
  175. package/types/ngneers-controls-switch.d.ts +18 -0
  176. package/types/ngneers-controls-table.d.ts +3127 -0
  177. package/types/ngneers-controls-tabs.d.ts +118 -0
  178. package/types/ngneers-controls-tag.d.ts +21 -0
  179. package/types/ngneers-controls-toast.d.ts +298 -0
  180. package/types/ngneers-controls-toggle-button.d.ts +48 -0
  181. package/types/ngneers-controls-tooltip.d.ts +189 -0
  182. package/types/ngneers-controls-tree.d.ts +409 -0
  183. package/types/ngneers-controls-upload.d.ts +347 -0
  184. package/types/ngneers-controls-utils-ng.d.ts +129 -0
  185. package/types/ngneers-controls-utils.d.ts +130 -0
  186. package/types/ngneers-controls.d.ts +2 -0
@@ -0,0 +1,2634 @@
1
+ import * as i0 from '@angular/core';
2
+ import { signal, computed, afterRenderEffect, input, inject, ElementRef, effect, Directive, contentChild, Component, viewChild, model, booleanAttribute, afterNextRender, Renderer2, ViewContainerRef, viewChildren, EnvironmentInjector, ApplicationRef, createComponent, NgModule } from '@angular/core';
3
+ import { NgTemplateOutlet } from '@angular/common';
4
+ import { elementSizeSignal, injectThemeTemplate, templateTypesFn, NgnTemplate, domEventHandler, setComponentInput } from '@ngneers/controls/api/ng';
5
+ import { NgnBase, NgnPt, provideSelf, getNearestNgnInstance, getNearestNgnInstanceSig } from '@ngneers/controls/base';
6
+ import { I18n } from '@ngneers/controls/i18n';
7
+ import { NgnIcon } from '@ngneers/controls/icon';
8
+ import { NgnPaginator } from '@ngneers/controls/paginator';
9
+ import { NgnScrollShadow } from '@ngneers/controls/scroll-shadow';
10
+ import { NgnScrollerItem, NgnScroller } from '@ngneers/controls/scroller';
11
+ import { tableControlTemplate } from '@ngneers/controls-themes/templates/table';
12
+ import { ResizeEngine, getResizeLimitInPx } from '@ngneers/controls/api/resize';
13
+ import { toggleClass, NgnError } from '@ngneers/controls/utils';
14
+ import { setInputSignalValue } from '@ngneers/controls/utils-ng';
15
+ import { executeMultiFilter } from '@ngneers/controls/api';
16
+ import { openMenuAt } from '@ngneers/controls/menu';
17
+ import { NgnActionButton } from '@ngneers/controls/button';
18
+ import { NgnCheckbox } from '@ngneers/controls/checkbox';
19
+ import { NgnFilter } from '@ngneers/controls/filter';
20
+
21
+ /**
22
+ * Default minimum column width in pixels for table columns.
23
+ * Enforced as an absolute floor even when a relative min-size (e.g. `5%`) resolves smaller.
24
+ */
25
+ const TABLE_MIN_COLUMN_WIDTH_PX = 50;
26
+ /** Splits a CSS grid-template-columns string into individual track values, handling minmax(). */
27
+ const GRID_TRACK_RE = /(?:minmax\([^)]+\)|[^\s]+)/g;
28
+ /**
29
+ * Owns all column geometry for {@link NgnTable}: effective order, widths (via the wrapped
30
+ * {@link ResizeEngine}), sticky columns, reorder gestures, auto-sizing, and the composed
31
+ * `grid-template-columns`. Reads table flags through injected signals; writes only its own
32
+ * state and the injected `columnOrder` model.
33
+ */
34
+ class TableColumnLayoutModel {
35
+ _deps;
36
+ _registeredHeaderCells = signal([], /* @ts-ignore */
37
+ ...(ngDevMode ? [{ debugName: "_registeredHeaderCells" }] : /* istanbul ignore next */ []));
38
+ _stickyColumns = signal(new Map(), /* @ts-ignore */
39
+ ...(ngDevMode ? [{ debugName: "_stickyColumns" }] : /* istanbul ignore next */ []));
40
+ _hasSelectionColumn = signal(false, /* @ts-ignore */
41
+ ...(ngDevMode ? [{ debugName: "_hasSelectionColumn" }] : /* istanbul ignore next */ []));
42
+ _isReordering = signal(false, /* @ts-ignore */
43
+ ...(ngDevMode ? [{ debugName: "_isReordering" }] : /* istanbul ignore next */ []));
44
+ _reorderSourceColumnId = signal(null, /* @ts-ignore */
45
+ ...(ngDevMode ? [{ debugName: "_reorderSourceColumnId" }] : /* istanbul ignore next */ []));
46
+ _dropIndicatorState = signal(null, /* @ts-ignore */
47
+ ...(ngDevMode ? [{ debugName: "_dropIndicatorState" }] : /* istanbul ignore next */ []));
48
+ _reorderTargetIndex = -1;
49
+ _tableElementSize;
50
+ _resizeEngine;
51
+ hasSelectionColumn = this._hasSelectionColumn.asReadonly();
52
+ isReordering = this._isReordering.asReadonly();
53
+ dropIndicatorState = this._dropIndicatorState.asReadonly();
54
+ isDragging = () => this._resizeEngine.isDragging();
55
+ columnCount = computed(() => this._registeredHeaderCells().length, /* @ts-ignore */
56
+ ...(ngDevMode ? [{ debugName: "columnCount" }] : /* istanbul ignore next */ []));
57
+ /**
58
+ * Effective column order: merges user-provided `columnOrder` with registered header cells.
59
+ * If `columnOrder` is empty, falls back to registration order.
60
+ * Unknown keys are filtered; new columns not in the order are appended.
61
+ */
62
+ _effectiveColumnOrder = computed(() => {
63
+ const cells = this._registeredHeaderCells();
64
+ const cellIds = cells.map(c => c.ngnTableTh());
65
+ const userOrder = this._deps.columnOrder();
66
+ if (!userOrder.length) {
67
+ return cellIds;
68
+ }
69
+ // Filter to valid keys and append any new columns not in the order
70
+ const validOrder = userOrder.filter(id => cellIds.includes(id));
71
+ const missing = cellIds.filter(id => !validOrder.includes(id));
72
+ return [...validOrder, ...missing];
73
+ }, /* @ts-ignore */
74
+ ...(ngDevMode ? [{ debugName: "_effectiveColumnOrder" }] : /* istanbul ignore next */ []));
75
+ stickyStartColumns = computed(() => {
76
+ const stickyMap = this._stickyColumns();
77
+ const order = this._effectiveColumnOrder();
78
+ const contiguous = [];
79
+ for (const id of order) {
80
+ if (stickyMap.get(id) === 'start') {
81
+ contiguous.push(id);
82
+ }
83
+ else {
84
+ break;
85
+ }
86
+ }
87
+ return contiguous;
88
+ }, /* @ts-ignore */
89
+ ...(ngDevMode ? [{ debugName: "stickyStartColumns" }] : /* istanbul ignore next */ []));
90
+ stickyEndColumns = computed(() => {
91
+ const stickyMap = this._stickyColumns();
92
+ const order = this._effectiveColumnOrder();
93
+ const contiguous = [];
94
+ for (let i = order.length - 1; i >= 0; i--) {
95
+ if (stickyMap.get(order[i]) === 'end') {
96
+ contiguous.unshift(order[i]);
97
+ }
98
+ else {
99
+ break;
100
+ }
101
+ }
102
+ return contiguous;
103
+ }, /* @ts-ignore */
104
+ ...(ngDevMode ? [{ debugName: "stickyEndColumns" }] : /* istanbul ignore next */ []));
105
+ /**
106
+ * Maps column ID → 1-based visual position.
107
+ */
108
+ columnOrderMap = computed(() => {
109
+ const order = this._effectiveColumnOrder();
110
+ const map = new Map();
111
+ for (let i = 0; i < order.length; i++) {
112
+ map.set(order[i], i + 1);
113
+ }
114
+ return map;
115
+ }, /* @ts-ignore */
116
+ ...(ngDevMode ? [{ debugName: "columnOrderMap" }] : /* istanbul ignore next */ []));
117
+ /**
118
+ * Grid template columns computed from the resize engine when resizable,
119
+ * otherwise falls back to the standard equal-width repeat.
120
+ */
121
+ gridTemplateColumns;
122
+ constructor(_deps) {
123
+ this._deps = _deps;
124
+ this._tableElementSize = elementSizeSignal(this._deps.element);
125
+ // Create engine eagerly in constructor (outside reactive context) to avoid NG0602
126
+ this._resizeEngine = new ResizeEngine({
127
+ items: computed(() => this._registeredHeaderCells()),
128
+ containerSize: computed(() => {
129
+ // Subscribe to the size signal for reactivity, but use clientWidth
130
+ // to exclude vertical scrollbar width and avoid horizontal overflow.
131
+ this._tableElementSize();
132
+ const tableEl = this._deps.element.nativeElement.querySelector('table');
133
+ const total = tableEl?.clientWidth ?? 0;
134
+ if (!this._hasSelectionColumn())
135
+ return total;
136
+ // Subtract the selection column width so the resize engine only distributes
137
+ // the remaining space among data columns.
138
+ const selCol = this._deps.element.nativeElement.querySelector(`.${this._deps.themeClass('selection-column')}`);
139
+ return total - (selCol?.getBoundingClientRect().width ?? 0);
140
+ }),
141
+ gapSizes: signal([]),
142
+ distributionMode: computed(() => this._deps.resizeMode()),
143
+ containerConstrained: computed(() => this._deps.resizeMode() !== 'push'),
144
+ lockSizes: computed(() => this._deps.lockSizes()),
145
+ minItemSizePx: TABLE_MIN_COLUMN_WIDTH_PX,
146
+ resolveItemSizes: () => this._registeredHeaderCells().map(cell => cell.element.nativeElement.getBoundingClientRect().width),
147
+ });
148
+ this.gridTemplateColumns = computed(() => {
149
+ const checkboxCol = this._hasSelectionColumn() ? 'min-content ' : '';
150
+ if (this._deps.resizable()) {
151
+ const rawSizes = this._resizeEngine.gridTemplateSizes();
152
+ // When reorderable, permute grid track sizes to match visual column order
153
+ if (this._deps.reorderable()) {
154
+ return `${checkboxCol}${this._permuteGridSizes(rawSizes)}`;
155
+ }
156
+ return `${checkboxCol}${rawSizes}`;
157
+ }
158
+ return `${checkboxCol}repeat(${this.columnCount()}, 1fr)`;
159
+ }, /* @ts-ignore */
160
+ ...(ngDevMode ? [{ debugName: "gridTemplateColumns" }] : /* istanbul ignore next */ []));
161
+ afterRenderEffect(() => {
162
+ const startCols = this.stickyStartColumns();
163
+ const endCols = this.stickyEndColumns();
164
+ if (startCols.length === 0 && endCols.length === 0)
165
+ return;
166
+ if (this._resizeEngine.isDragging())
167
+ return;
168
+ this._tableElementSize();
169
+ const tableEl = this._deps.element.nativeElement.querySelector(':scope > table');
170
+ if (!tableEl)
171
+ return;
172
+ const cells = this._registeredHeaderCells();
173
+ let selectionWidth = 0;
174
+ if (this._hasSelectionColumn()) {
175
+ const selCol = tableEl.querySelector(`.${this._deps.themeClass('selection-column')}`);
176
+ selectionWidth = selCol?.getBoundingClientRect().width ?? 0;
177
+ }
178
+ let cumulativeLeft = selectionWidth;
179
+ for (let i = 0; i < startCols.length; i++) {
180
+ tableEl.style.setProperty(`--ngn-sticky-start-offset-${i}`, `${cumulativeLeft}px`);
181
+ const cell = cells.find(c => c.ngnTableTh() === startCols[i]);
182
+ cumulativeLeft += cell?.element.nativeElement.getBoundingClientRect().width ?? 0;
183
+ }
184
+ let cumulativeRight = 0;
185
+ for (let i = endCols.length - 1; i >= 0; i--) {
186
+ tableEl.style.setProperty(`--ngn-sticky-end-offset-${i}`, `${cumulativeRight}px`);
187
+ const cell = cells.find(c => c.ngnTableTh() === endCols[i]);
188
+ cumulativeRight += cell?.element.nativeElement.getBoundingClientRect().width ?? 0;
189
+ }
190
+ });
191
+ }
192
+ /**
193
+ * Returns the 1-based visual column index for a given 0-based logical (DOM) index.
194
+ */
195
+ getVisualColumnIndex(logicalIndex) {
196
+ const cells = this._registeredHeaderCells();
197
+ const cell = cells[logicalIndex];
198
+ if (!cell)
199
+ return logicalIndex + 1;
200
+ return this.columnOrderMap().get(cell.ngnTableTh()) ?? logicalIndex + 1;
201
+ }
202
+ registerSelectionColumn() {
203
+ this._hasSelectionColumn.set(true);
204
+ }
205
+ unregisterSelectionColumn() {
206
+ this._hasSelectionColumn.set(false);
207
+ }
208
+ registerHeaderCell(cell) {
209
+ this._registeredHeaderCells.update(cells => [...cells, cell]);
210
+ }
211
+ unregisterHeaderCell(cell) {
212
+ this._registeredHeaderCells.update(cells => cells.filter(c => c !== cell));
213
+ }
214
+ getRegisteredHeaderCells() {
215
+ return this._registeredHeaderCells();
216
+ }
217
+ registerStickyColumn(columnId, side) {
218
+ this._stickyColumns.update(map => {
219
+ const next = new Map(map);
220
+ next.set(columnId, side);
221
+ return next;
222
+ });
223
+ }
224
+ unregisterStickyColumn(columnId) {
225
+ this._stickyColumns.update(map => {
226
+ const next = new Map(map);
227
+ next.delete(columnId);
228
+ return next;
229
+ });
230
+ }
231
+ getStickyInfo(columnId) {
232
+ const startCols = this.stickyStartColumns();
233
+ const endCols = this.stickyEndColumns();
234
+ const startIndex = startCols.indexOf(columnId);
235
+ if (startIndex >= 0) {
236
+ return { side: 'start', index: startIndex, isEdge: startIndex === startCols.length - 1 };
237
+ }
238
+ const endIndex = endCols.indexOf(columnId);
239
+ if (endIndex >= 0) {
240
+ return { side: 'end', index: endIndex, isEdge: endIndex === 0 };
241
+ }
242
+ return null;
243
+ }
244
+ // --- Resize operations (called by NgnTableTh) ---
245
+ startColumnResize(columnIndex, event) {
246
+ if (!this._deps.resizable())
247
+ return;
248
+ this._resizeEngine.startDrag(columnIndex, event.clientX);
249
+ }
250
+ dragColumnResize(columnIndex, event) {
251
+ if (!this._deps.resizable())
252
+ return;
253
+ this._resizeEngine.drag(columnIndex, event.clientX);
254
+ }
255
+ endColumnResize(columnIndex, cancel) {
256
+ if (!this._deps.resizable())
257
+ return;
258
+ this._resizeEngine.endDrag(columnIndex, cancel);
259
+ }
260
+ // --- Reorder operations (called by NgnTableReorderableColumn) ---
261
+ getReorderBounds(columnId) {
262
+ const order = this._effectiveColumnOrder();
263
+ const startCols = this.stickyStartColumns();
264
+ const endCols = this.stickyEndColumns();
265
+ const side = this._stickyColumns().get(columnId);
266
+ if (side === 'start') {
267
+ return { min: 0, max: startCols.length };
268
+ }
269
+ else if (side === 'end') {
270
+ return { min: order.length - endCols.length, max: order.length };
271
+ }
272
+ else {
273
+ return { min: startCols.length, max: order.length - endCols.length };
274
+ }
275
+ }
276
+ startColumnReorder(columnId) {
277
+ if (!this._deps.reorderable())
278
+ return;
279
+ this._isReordering.set(true);
280
+ this._reorderSourceColumnId.set(columnId);
281
+ this._reorderTargetIndex = -1;
282
+ }
283
+ dragColumnReorder(event) {
284
+ if (!this._deps.reorderable() || !this._isReordering())
285
+ return;
286
+ const cells = this._registeredHeaderCells();
287
+ const effectiveOrder = this._effectiveColumnOrder();
288
+ const sourceId = this._reorderSourceColumnId();
289
+ if (!sourceId)
290
+ return;
291
+ // Find drop target by comparing cursor X to header cell midpoints (in visual order)
292
+ const cursorX = event.clientX;
293
+ let targetIndex = effectiveOrder.length;
294
+ // Build visual-order cells with their bounding boxes
295
+ const visualCells = effectiveOrder
296
+ .map(id => cells.find(c => c.ngnTableTh() === id))
297
+ .filter((c) => !!c);
298
+ for (let i = 0; i < visualCells.length; i++) {
299
+ const rect = visualCells[i].element.nativeElement.getBoundingClientRect();
300
+ const midX = rect.left + rect.width / 2;
301
+ if (cursorX < midX) {
302
+ targetIndex = i;
303
+ break;
304
+ }
305
+ }
306
+ const bounds = this.getReorderBounds(sourceId);
307
+ targetIndex = Math.max(bounds.min, Math.min(bounds.max, targetIndex));
308
+ this._reorderTargetIndex = targetIndex;
309
+ // Compute indicator position relative to the table element
310
+ const tableEl = this._deps.element.nativeElement.querySelector(':scope > table');
311
+ if (!tableEl)
312
+ return;
313
+ const tableRect = tableEl.getBoundingClientRect();
314
+ // Use the first header cell's height for the indicator
315
+ const firstCellRect = visualCells[0]?.element.nativeElement.getBoundingClientRect();
316
+ const indicatorHeight = firstCellRect?.height ?? 40;
317
+ let indicatorLeftPx;
318
+ if (targetIndex === 0) {
319
+ const firstCell = visualCells[0];
320
+ if (!firstCell)
321
+ return;
322
+ const rect = firstCell.element.nativeElement.getBoundingClientRect();
323
+ indicatorLeftPx = rect.left - tableRect.left;
324
+ }
325
+ else if (targetIndex >= visualCells.length) {
326
+ const lastCell = visualCells[visualCells.length - 1];
327
+ if (!lastCell)
328
+ return;
329
+ const rect = lastCell.element.nativeElement.getBoundingClientRect();
330
+ indicatorLeftPx = rect.right - tableRect.left;
331
+ }
332
+ else {
333
+ const prevCell = visualCells[targetIndex - 1];
334
+ const nextCell = visualCells[targetIndex];
335
+ const prevRect = prevCell.element.nativeElement.getBoundingClientRect();
336
+ const nextRect = nextCell.element.nativeElement.getBoundingClientRect();
337
+ indicatorLeftPx = (prevRect.right + nextRect.left) / 2 - tableRect.left;
338
+ }
339
+ // Clamp so the 3px-wide indicator stays within the visible content area
340
+ // (use clientWidth to exclude scrollbar width from the boundary)
341
+ const maxLeft = tableEl.clientWidth - 3;
342
+ indicatorLeftPx = Math.max(0, Math.min(indicatorLeftPx, maxLeft));
343
+ this._dropIndicatorState.set({
344
+ leftPx: indicatorLeftPx,
345
+ topPx: tableEl.scrollTop,
346
+ heightPx: indicatorHeight,
347
+ });
348
+ }
349
+ endColumnReorder(cancel) {
350
+ if (!this._deps.reorderable())
351
+ return;
352
+ if (!cancel && this._reorderTargetIndex >= 0) {
353
+ const sourceId = this._reorderSourceColumnId();
354
+ if (sourceId) {
355
+ const effectiveOrder = [...this._effectiveColumnOrder()];
356
+ const sourceIndex = effectiveOrder.indexOf(sourceId);
357
+ if (sourceIndex >= 0) {
358
+ // Remove source from its current position
359
+ effectiveOrder.splice(sourceIndex, 1);
360
+ // Adjust target index if source was before target
361
+ let insertAt = this._reorderTargetIndex;
362
+ if (sourceIndex < this._reorderTargetIndex) {
363
+ insertAt--;
364
+ }
365
+ // Insert at new position
366
+ effectiveOrder.splice(insertAt, 0, sourceId);
367
+ this._deps.columnOrder.set(effectiveOrder);
368
+ }
369
+ }
370
+ }
371
+ this._isReordering.set(false);
372
+ this._reorderSourceColumnId.set(null);
373
+ this._dropIndicatorState.set(null);
374
+ this._reorderTargetIndex = -1;
375
+ }
376
+ getReorderSourceColumnId() {
377
+ return this._reorderSourceColumnId();
378
+ }
379
+ /**
380
+ * Permutes grid template sizes from logical (registration) order to visual (column order) order.
381
+ */
382
+ _permuteGridSizes(rawSizes) {
383
+ const tracks = rawSizes.match(GRID_TRACK_RE) ?? [];
384
+ const cells = this._registeredHeaderCells();
385
+ const effectiveOrder = this._effectiveColumnOrder();
386
+ if (tracks.length !== cells.length) {
387
+ return rawSizes; // Mismatch — don't permute
388
+ }
389
+ // Build map: column ID → track size (logical order)
390
+ const sizeByColumnId = new Map();
391
+ for (let i = 0; i < cells.length; i++) {
392
+ sizeByColumnId.set(cells[i].ngnTableTh(), tracks[i]);
393
+ }
394
+ // Emit sizes in visual order
395
+ return effectiveOrder.map(id => sizeByColumnId.get(id) ?? '1fr').join(' ');
396
+ }
397
+ /**
398
+ * Auto-sizes the column at `columnIndex` to fit its visible content.
399
+ * Uses `Canvas.measureText()` for fast, reflow-free text measurement since
400
+ * grid-constrained cells (`min-width: 0`) don't report natural content width
401
+ * via `scrollWidth`.
402
+ */
403
+ autoSizeColumn(columnIndex) {
404
+ if (!this._deps.resizable())
405
+ return;
406
+ const cells = this._registeredHeaderCells();
407
+ const headerCell = cells[columnIndex];
408
+ if (!headerCell)
409
+ return;
410
+ const tableEl = this._deps.element.nativeElement.querySelector('table');
411
+ if (!tableEl)
412
+ return;
413
+ const headerEl = headerCell.element.nativeElement;
414
+ const ctx = autoSizeCanvasCtx();
415
+ // Measure header: text width + non-text siblings (icons, controls) + flex gap.
416
+ // The header cell is a flex container with: cell-text, spacer, filter/sort icons, resize handle.
417
+ const cellTextClass = this._deps.themeClass('cell-text');
418
+ const headerTextEl = headerEl.querySelector(`.${cellTextClass}`);
419
+ let headerRequiredWidth = 0;
420
+ if (headerTextEl) {
421
+ ctx.font = getCssFont(headerEl);
422
+ const text = headerTextEl.textContent ?? '';
423
+ headerRequiredWidth = Math.ceil(ctx.measureText(text).width);
424
+ }
425
+ // Add width of visible non-text siblings (filter icon, sort icon, etc.)
426
+ const headerStyle = getComputedStyle(headerEl);
427
+ const headerGap = parseFloat(headerStyle.gap) || 0;
428
+ let visibleSiblingCount = 0;
429
+ for (let i = 0; i < headerEl.children.length; i++) {
430
+ const child = headerEl.children[i];
431
+ if (child === headerTextEl)
432
+ continue;
433
+ // Skip spacer (zero intrinsic width) and hidden resize handle
434
+ if (child.offsetWidth > 0 && getComputedStyle(child).flexGrow === '0') {
435
+ headerRequiredWidth += child.offsetWidth;
436
+ visibleSiblingCount++;
437
+ }
438
+ }
439
+ // Add flex gaps between visible items
440
+ if (visibleSiblingCount > 0) {
441
+ headerRequiredWidth += headerGap * visibleSiblingCount;
442
+ }
443
+ let maxContentWidth = headerRequiredWidth;
444
+ // Measure visible body cells (nth-child is 1-indexed, offset by selection column)
445
+ const selectionOffset = this._hasSelectionColumn() ? 1 : 0;
446
+ const bodyCells = Array.from(tableEl.querySelectorAll(`tbody tr td:nth-child(${columnIndex + selectionOffset + 1})`));
447
+ if (bodyCells.length > 0) {
448
+ ctx.font = getCssFont(bodyCells[0]);
449
+ for (let i = 0; i < bodyCells.length; i++) {
450
+ const text = bodyCells[i].textContent ?? '';
451
+ maxContentWidth = Math.max(maxContentWidth, Math.ceil(ctx.measureText(text).width));
452
+ }
453
+ }
454
+ // Add cell horizontal padding
455
+ const paddingLeft = parseFloat(headerStyle.paddingLeft) || 0;
456
+ const paddingRight = parseFloat(headerStyle.paddingRight) || 0;
457
+ const totalWidth = maxContentWidth + paddingLeft + paddingRight;
458
+ // Clamp to min/max constraints
459
+ const containerSize = this._tableElementSize().width;
460
+ const minPx = Math.max(TABLE_MIN_COLUMN_WIDTH_PX, getResizeLimitInPx(headerCell.minSize(), containerSize));
461
+ const maxPx = getResizeLimitInPx(headerCell.maxSize(), containerSize);
462
+ const clampedWidth = Math.max(minPx, Math.min(maxPx, totalWidth));
463
+ this._resizeEngine.setItemSize(headerCell, `${clampedWidth}px`);
464
+ this._resizeEngine.hasBeenResized.set(true);
465
+ }
466
+ }
467
+ // --- Auto-size helpers ---
468
+ /** Lazily created canvas context for text measurement (no DOM reflows). */
469
+ let _autoSizeCtx = null;
470
+ function autoSizeCanvasCtx() {
471
+ if (!_autoSizeCtx) {
472
+ _autoSizeCtx = document.createElement('canvas').getContext('2d');
473
+ }
474
+ return _autoSizeCtx;
475
+ }
476
+ /** Builds the CSS font shorthand from an element's computed style. */
477
+ function getCssFont(el) {
478
+ const s = getComputedStyle(el);
479
+ return `${s.fontStyle} ${s.fontWeight} ${s.fontSize} ${s.fontFamily}`;
480
+ }
481
+
482
+ class NgnTableGroupHeaderTr extends NgnScrollerItem {
483
+ /** The group-header row this `<tr>` renders. */
484
+ ngnTableGroupHeaderTr = input.required(/* @ts-ignore */
485
+ ...(ngDevMode ? [{ debugName: "ngnTableGroupHeaderTr" }] : /* istanbul ignore next */ []));
486
+ /** The item bound to the underlying scroller entry; kept in sync with {@link ngnTableGroupHeaderTr}. */
487
+ ngnScrollerItem = input({}, /* @ts-ignore */
488
+ ...(ngDevMode ? [{ debugName: "ngnScrollerItem" }] : /* istanbul ignore next */ []));
489
+ _element = inject((ElementRef));
490
+ theme = injectThemeTemplate(tableControlTemplate);
491
+ constructor() {
492
+ super();
493
+ effect(() => {
494
+ const row = this.ngnTableGroupHeaderTr();
495
+ setInputSignalValue(this.ngnScrollerItem, row);
496
+ });
497
+ this.prepareDom();
498
+ }
499
+ prepareDom() {
500
+ toggleClass(this._element.nativeElement, this.theme.class('row'), true);
501
+ toggleClass(this._element.nativeElement, this.theme.class('group-header-row'), true);
502
+ }
503
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnTableGroupHeaderTr, deps: [], target: i0.ɵɵFactoryTarget.Directive });
504
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: NgnTableGroupHeaderTr, isStandalone: true, selector: "[ngnTableGroupHeaderTr]", inputs: { ngnTableGroupHeaderTr: { classPropertyName: "ngnTableGroupHeaderTr", publicName: "ngnTableGroupHeaderTr", isSignal: true, isRequired: true, transformFunction: null }, ngnScrollerItem: { classPropertyName: "ngnScrollerItem", publicName: "ngnScrollerItem", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.aria-rowindex": "ngnTableGroupHeaderTr().index + 2", "style.--ngn-table-row-index": "ngnTableGroupHeaderTr().index + 2" } }, usesInheritance: true, ngImport: i0 });
505
+ }
506
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnTableGroupHeaderTr, decorators: [{
507
+ type: Directive,
508
+ args: [{
509
+ selector: '[ngnTableGroupHeaderTr]',
510
+ host: {
511
+ '[attr.aria-rowindex]': 'ngnTableGroupHeaderTr().index + 2',
512
+ '[style.--ngn-table-row-index]': 'ngnTableGroupHeaderTr().index + 2',
513
+ },
514
+ }]
515
+ }], ctorParameters: () => [], propDecorators: { ngnTableGroupHeaderTr: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngnTableGroupHeaderTr", required: true }] }], ngnScrollerItem: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngnScrollerItem", required: false }] }] } });
516
+
517
+ /** Applies the active multi-column filter set. Returns the input untouched when no filters are set. */
518
+ function filterRows(rows, filters) {
519
+ if (!filters) {
520
+ return rows;
521
+ }
522
+ return executeMultiFilter(rows, filters);
523
+ }
524
+ /**
525
+ * Default comparator: nulls sort first on `asc` / last on `desc`; numbers compare
526
+ * numerically; everything else compares via `localeCompare` on the stringified value.
527
+ */
528
+ function defaultSortComparator(a, b, sort) {
529
+ const { column, direction } = sort;
530
+ // Mirrors the existing index access in groupRows / the original inline comparator.
531
+ const aValue = a[column];
532
+ const bValue = b[column];
533
+ if (aValue == null && bValue != null) {
534
+ return direction === 'asc' ? -1 : 1;
535
+ }
536
+ if (aValue != null && bValue == null) {
537
+ return direction === 'asc' ? 1 : -1;
538
+ }
539
+ if (aValue == null && bValue == null) {
540
+ return 0;
541
+ }
542
+ return typeof aValue === 'number' && typeof bValue === 'number'
543
+ ? direction === 'asc'
544
+ ? aValue - bValue
545
+ : bValue - aValue
546
+ : direction === 'asc'
547
+ ? String(aValue).localeCompare(String(bValue))
548
+ : String(bValue).localeCompare(String(aValue));
549
+ }
550
+ /** Sorts rows without mutating the input. Returns the input untouched when no sort is set. */
551
+ function sortRows(rows, sort, comparator = defaultSortComparator) {
552
+ if (!sort) {
553
+ return rows;
554
+ }
555
+ return rows.toSorted((a, b) => comparator(a, b, sort));
556
+ }
557
+ /**
558
+ * Builds a flat row list with group headers interleaved, preserving the incoming
559
+ * (already filtered + sorted) order. Data rows appear only under expanded groups.
560
+ */
561
+ function groupRows(rows, groupBy, fieldId, expanded) {
562
+ const groupMap = new Map();
563
+ for (const row of rows) {
564
+ const key = row[groupBy];
565
+ let group = groupMap.get(key);
566
+ if (!group) {
567
+ group = [];
568
+ groupMap.set(key, group);
569
+ }
570
+ group.push(row);
571
+ }
572
+ const result = [];
573
+ let index = 0;
574
+ for (const [groupKey, members] of groupMap) {
575
+ const isExpanded = expanded.has(groupKey);
576
+ result.push({
577
+ kind: 'group-header',
578
+ id: `group-${String(groupKey)}`,
579
+ groupKey,
580
+ groupValue: groupKey,
581
+ count: members.length,
582
+ expanded: isExpanded,
583
+ index: index++,
584
+ });
585
+ if (isExpanded) {
586
+ for (const data of members) {
587
+ result.push({
588
+ kind: 'data',
589
+ id: data[fieldId],
590
+ data,
591
+ index: index++,
592
+ });
593
+ }
594
+ }
595
+ }
596
+ return result;
597
+ }
598
+ /** Slices a formatted row list to the current page. Returns `[]` when there is no page state. */
599
+ function paginateRows(rows, pageState) {
600
+ if (!pageState) {
601
+ return [];
602
+ }
603
+ return rows.slice(pageState.slice.skip, pageState.slice.take + pageState.slice.skip);
604
+ }
605
+
606
+ /**
607
+ * Roving keyboard model for row actions, reconciled onto the table's single
608
+ * current-row index ({@link RowNavigationDeps.focusedRowIndex}). The single
609
+ * tab stop lives on the table host; ArrowRight enters the action bar and
610
+ * ArrowLeft/Right move between actions, ArrowLeft off the first action
611
+ * returns to row navigation. ContextMenu and Shift+F10 always open the
612
+ * context menu on the current row, regardless of selection mode.
613
+ *
614
+ * When {@link RowNavigationDeps.selectionMode} is set and focus is not in
615
+ * the action bar, this model deliberately does **not** handle
616
+ * ArrowUp/Down/Enter/Space itself — it returns `false` so the table's own
617
+ * `onKeyDown` falls through to
618
+ * {@link import('./table-selection-model').TableSelectionModel#onKeyDown},
619
+ * which already resolves the start row correctly (including the
620
+ * null-`focusedRowIndex` → currently-selected-row fallback after a mouse
621
+ * click). Recomputing that start index here would silently diverge from the
622
+ * selection model's resolution — that's exactly the bug this delegation
623
+ * avoids. When `selectionMode` is `null`, this model owns ArrowUp/Down
624
+ * itself (moves `focusedRowIndex`) and Enter opens the context menu. Only
625
+ * claims keys when at least one row has registered actions (see
626
+ * {@link RowNavigationDeps.hasActions}), so tables without row actions keep
627
+ * selection's keyboard behavior untouched.
628
+ */
629
+ class TableRowNavigationModel {
630
+ _deps;
631
+ /** Whether DOM focus is currently inside the current row's action bar (as opposed to on the row itself). */
632
+ inActions = signal(false, /* @ts-ignore */
633
+ ...(ngDevMode ? [{ debugName: "inActions" }] : /* istanbul ignore next */ []));
634
+ constructor(_deps) {
635
+ this._deps = _deps;
636
+ }
637
+ /** Returns true when the event was handled (caller should stop propagation to other handlers). */
638
+ onKeyDown(event) {
639
+ if (!this._deps.hasActions())
640
+ return false;
641
+ const rows = this._deps.viewRows();
642
+ if (rows.length === 0)
643
+ return false;
644
+ const current = this._deps.focusedRowIndex();
645
+ const mode = this._deps.selectionMode();
646
+ switch (event.key) {
647
+ case 'ArrowDown':
648
+ case 'ArrowUp':
649
+ return this._moveRow(event, rows.length, current, mode);
650
+ case 'ArrowRight':
651
+ return this._moveRight(event, current);
652
+ case 'ArrowLeft':
653
+ return this._moveLeft(event, current);
654
+ case 'Enter':
655
+ case ' ':
656
+ return this._activate(event, current, mode);
657
+ case 'ContextMenu':
658
+ return this._openMenu(event, current);
659
+ case 'F10':
660
+ return event.shiftKey ? this._openMenu(event, current) : false;
661
+ case 'Escape':
662
+ // Back out of the action bar to the row (standard roving-toolbar exit),
663
+ // mirroring ArrowLeft off the first action.
664
+ if (!this.inActions())
665
+ return false;
666
+ this.inActions.set(false);
667
+ this._deps.focusHost();
668
+ event.preventDefault();
669
+ return true;
670
+ default:
671
+ return false;
672
+ }
673
+ }
674
+ _moveRow(event, rowCount, current, mode) {
675
+ // While focus is inside the action bar, ArrowUp/Down must not move the
676
+ // current row (and must not fall through to selection) — swallow them.
677
+ if (this.inActions()) {
678
+ event.preventDefault();
679
+ return true;
680
+ }
681
+ if (mode) {
682
+ // Delegate entirely to TableSelectionModel.onKeyDown — it already
683
+ // resolves the start index (including the null-focus → selected-row
684
+ // fallback) and applies the move/select/scroll side-effects. Returning
685
+ // `false` here lets the table's onKeyDown fall through to it, so the
686
+ // combined single/multi-select + actions behavior stays byte-identical
687
+ // to the no-actions case.
688
+ return false;
689
+ }
690
+ const start = current ?? -1;
691
+ const next = event.key === 'ArrowDown' ? Math.min(start + 1, rowCount - 1) : Math.max(start - 1, 0);
692
+ this._deps.focusedRowIndex.set(next);
693
+ this._deps.scrollToIndex(next);
694
+ event.preventDefault();
695
+ return true;
696
+ }
697
+ _moveRight(event, current) {
698
+ if (current === null)
699
+ return false;
700
+ if (this.inActions()) {
701
+ this._deps.moveAction(current, 1);
702
+ event.preventDefault();
703
+ return true;
704
+ }
705
+ if (!this._deps.enterActions(current))
706
+ return false;
707
+ this.inActions.set(true);
708
+ event.preventDefault();
709
+ return true;
710
+ }
711
+ _moveLeft(event, current) {
712
+ if (current === null || !this.inActions())
713
+ return false;
714
+ const stayed = this._deps.moveAction(current, -1);
715
+ if (!stayed) {
716
+ this.inActions.set(false); // fell off the first action → back to row navigation
717
+ this._deps.focusHost();
718
+ }
719
+ event.preventDefault();
720
+ return true;
721
+ }
722
+ _activate(event, current, mode) {
723
+ // Let the focused native <button> handle Enter/Space itself — do not
724
+ // preventDefault, do not open the menu.
725
+ if (this.inActions())
726
+ return false;
727
+ // Delegate entirely to TableSelectionModel.onKeyDown when selection is
728
+ // active — same rationale as the ArrowUp/Down case in `_moveRow`: it
729
+ // reads the same `focusedRowIndex`, but keeping a single handler avoids
730
+ // this model silently drifting from the selection model's semantics.
731
+ if (mode)
732
+ return false;
733
+ if (current === null)
734
+ return false;
735
+ if (event.key !== 'Enter')
736
+ return false; // Space is a no-op without selection
737
+ return this._openMenu(event, current);
738
+ }
739
+ _openMenu(event, current) {
740
+ if (current === null || this.inActions())
741
+ return false;
742
+ if (!this._deps.openMenu(current))
743
+ return false;
744
+ event.preventDefault();
745
+ return true;
746
+ }
747
+ }
748
+
749
+ /**
750
+ * Owns row selection, the selection anchor, keyboard navigation, and the derived
751
+ * select-all / indeterminate state for {@link NgnTable}. Reads the view rows through an
752
+ * injected signal and writes only the selection + focus state passed in.
753
+ */
754
+ class TableSelectionModel {
755
+ _deps;
756
+ constructor(_deps) {
757
+ this._deps = _deps;
758
+ }
759
+ /** Anchor index for Shift+click range selection (index in viewRows). */
760
+ _selectionAnchor = signal(null, /* @ts-ignore */
761
+ ...(ngDevMode ? [{ debugName: "_selectionAnchor" }] : /* istanbul ignore next */ []));
762
+ selectionSet = computed(() => new Set(this._deps.selection()), { ...(ngDevMode ? { debugName: "selectionSet" } : /* istanbul ignore next */ {}), equal: (a, b) => a.size === b.size && [...a].every(v => b.has(v)) });
763
+ isAllSelected = computed(() => {
764
+ const rows = this._deps.viewRows();
765
+ const selSet = this.selectionSet();
766
+ return rows.length > 0 && rows.every(row => selSet.has(row.id));
767
+ }, /* @ts-ignore */
768
+ ...(ngDevMode ? [{ debugName: "isAllSelected" }] : /* istanbul ignore next */ []));
769
+ isIndeterminate = computed(() => {
770
+ const selSet = this.selectionSet();
771
+ return selSet.size > 0 && !this.isAllSelected();
772
+ }, /* @ts-ignore */
773
+ ...(ngDevMode ? [{ debugName: "isIndeterminate" }] : /* istanbul ignore next */ []));
774
+ headerCheckboxValue = computed(() => {
775
+ if (this.isAllSelected())
776
+ return true;
777
+ if (this.isIndeterminate())
778
+ return null;
779
+ return false;
780
+ }, /* @ts-ignore */
781
+ ...(ngDevMode ? [{ debugName: "headerCheckboxValue" }] : /* istanbul ignore next */ []));
782
+ isRowSelected(id) {
783
+ return this.selectionSet().has(id);
784
+ }
785
+ handleRowClick(row, event) {
786
+ const mode = this._deps.selectionMode();
787
+ if (!mode)
788
+ return;
789
+ const id = row.id;
790
+ const rowIndex = this._deps.viewRows().findIndex(r => r.id === row.id);
791
+ if (mode === 'single') {
792
+ this._deps.selection.set([id]);
793
+ this._selectionAnchor.set(rowIndex);
794
+ this._deps.focusedRowIndex.set(null);
795
+ return;
796
+ }
797
+ if (event.shiftKey && this._selectionAnchor() !== null) {
798
+ this._selectRange(this._selectionAnchor(), rowIndex);
799
+ }
800
+ else if (event.ctrlKey || event.metaKey) {
801
+ this._toggleRowInSelection(id);
802
+ }
803
+ else {
804
+ this._toggleRowInSelection(id);
805
+ }
806
+ this._selectionAnchor.set(rowIndex);
807
+ this._deps.focusedRowIndex.set(null);
808
+ }
809
+ handleCheckboxChange(row) {
810
+ const id = row.id;
811
+ this._toggleRowInSelection(id);
812
+ const rowIndex = this._deps.viewRows().findIndex(r => r.id === row.id);
813
+ this._selectionAnchor.set(rowIndex);
814
+ this._deps.focusedRowIndex.set(null);
815
+ }
816
+ toggleSelectAll() {
817
+ if (this.isAllSelected()) {
818
+ this._deps.selection.set([]);
819
+ }
820
+ else {
821
+ const dataRows = this._deps
822
+ .viewRows()
823
+ .filter((r) => r.kind === 'data');
824
+ this._deps.selection.set(dataRows.map(r => r.id));
825
+ }
826
+ }
827
+ _toggleRowInSelection(id) {
828
+ const current = this._deps.selection();
829
+ if (this.selectionSet().has(id)) {
830
+ this._deps.selection.set(current.filter(v => v !== id));
831
+ }
832
+ else {
833
+ this._deps.selection.set([...current, id]);
834
+ }
835
+ }
836
+ _selectRange(fromIndex, toIndex) {
837
+ const rows = this._deps.viewRows();
838
+ const start = Math.min(fromIndex, toIndex);
839
+ const end = Math.max(fromIndex, toIndex);
840
+ const rangeIds = rows
841
+ .slice(start, end + 1)
842
+ .filter((r) => r.kind === 'data')
843
+ .map(r => r.id);
844
+ const currentSet = new Set(this._deps.selection());
845
+ for (const id of rangeIds) {
846
+ currentSet.add(id);
847
+ }
848
+ this._deps.selection.set([...currentSet]);
849
+ }
850
+ onKeyDown(event) {
851
+ const mode = this._deps.selectionMode();
852
+ if (!mode)
853
+ return;
854
+ const rows = this._deps.viewRows();
855
+ if (rows.length === 0)
856
+ return;
857
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
858
+ event.preventDefault();
859
+ event.stopPropagation();
860
+ let currentIndex = this._deps.focusedRowIndex();
861
+ if (currentIndex === null && mode === 'single') {
862
+ const sel = this._deps.selection();
863
+ if (sel.length > 0) {
864
+ currentIndex = rows.findIndex(r => r.id === sel[0]);
865
+ }
866
+ }
867
+ currentIndex ??= -1;
868
+ const nextIndex = event.key === 'ArrowDown'
869
+ ? Math.min(currentIndex + 1, rows.length - 1)
870
+ : Math.max(currentIndex - 1, 0);
871
+ this._deps.focusedRowIndex.set(nextIndex);
872
+ this._deps.scrollToIndex(nextIndex);
873
+ this.applyArrowMove(nextIndex, event.shiftKey);
874
+ }
875
+ else if (event.key === ' ' || event.key === 'Enter') {
876
+ if (mode === 'multi') {
877
+ const focusIdx = this._deps.focusedRowIndex();
878
+ if (focusIdx !== null && rows[focusIdx]) {
879
+ event.preventDefault();
880
+ event.stopPropagation();
881
+ this.toggleCurrentRow(focusIdx);
882
+ }
883
+ }
884
+ }
885
+ }
886
+ /**
887
+ * Applies the selection side-effects of an arrow-key move that already set
888
+ * `focusedRowIndex` to `nextIndex` — single mode selects the row and resets
889
+ * the anchor, multi mode with `shiftKey` extends the range from the anchor,
890
+ * multi mode alone only moves focus (a no-op here). No-ops when
891
+ * {@link TableSelectionModelDeps.selectionMode} is `null`. Only called from
892
+ * {@link onKeyDown} — {@link TableRowNavigationModel} deliberately does not
893
+ * recompute or replicate this (it would risk diverging from this model's
894
+ * null-`focusedRowIndex` start-index resolution), and instead defers
895
+ * ArrowUp/Down entirely to {@link onKeyDown} when a row-actions table also
896
+ * has a `selectionMode`.
897
+ */
898
+ applyArrowMove(nextIndex, shiftKey) {
899
+ const mode = this._deps.selectionMode();
900
+ if (!mode)
901
+ return;
902
+ const rows = this._deps.viewRows();
903
+ const row = rows[nextIndex];
904
+ if (!row)
905
+ return;
906
+ if (mode === 'single') {
907
+ this._deps.selection.set([row.id]);
908
+ this._selectionAnchor.set(nextIndex);
909
+ }
910
+ else if (shiftKey) {
911
+ const anchor = this._selectionAnchor() ?? nextIndex;
912
+ this._selectRange(anchor, nextIndex);
913
+ }
914
+ }
915
+ /**
916
+ * Toggles selection of the row at `index` and sets it as the new anchor —
917
+ * the multi-mode Enter/Space behavior. Only called from {@link onKeyDown};
918
+ * {@link TableRowNavigationModel} defers Enter/Space to it for the same
919
+ * reason as {@link applyArrowMove}.
920
+ */
921
+ toggleCurrentRow(index) {
922
+ const rows = this._deps.viewRows();
923
+ const row = rows[index];
924
+ if (!row)
925
+ return;
926
+ this._toggleRowInSelection(row.id);
927
+ this._selectionAnchor.set(index);
928
+ }
929
+ }
930
+
931
+ class NgnTableTemplates extends NgnBase {
932
+ _headerTemplate = contentChild('header', /* @ts-ignore */
933
+ ...(ngDevMode ? [{ debugName: "_headerTemplate" }] : /* istanbul ignore next */ []));
934
+ _bodyTemplate = contentChild('body', /* @ts-ignore */
935
+ ...(ngDevMode ? [{ debugName: "_bodyTemplate" }] : /* istanbul ignore next */ []));
936
+ _groupHeaderTemplate = contentChild('groupHeader', /* @ts-ignore */
937
+ ...(ngDevMode ? [{ debugName: "_groupHeaderTemplate" }] : /* istanbul ignore next */ []));
938
+ headerTemplate = this._headerTemplate;
939
+ bodyTemplate = this._bodyTemplate;
940
+ groupHeaderTemplate = this._groupHeaderTemplate;
941
+ /**
942
+ * Types for the table templates.
943
+ */
944
+ templateTypes = templateTypesFn();
945
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnTableTemplates, deps: null, target: i0.ɵɵFactoryTarget.Component });
946
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.0.5", type: NgnTableTemplates, isStandalone: true, selector: "ng-component", queries: [{ propertyName: "_headerTemplate", first: true, predicate: ["header"], descendants: true, isSignal: true }, { propertyName: "_bodyTemplate", first: true, predicate: ["body"], descendants: true, isSignal: true }, { propertyName: "_groupHeaderTemplate", first: true, predicate: ["groupHeader"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: '', isInline: true });
947
+ }
948
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnTableTemplates, decorators: [{
949
+ type: Component,
950
+ args: [{
951
+ template: '',
952
+ }]
953
+ }], propDecorators: { _headerTemplate: [{ type: i0.ContentChild, args: ['header', { isSignal: true }] }], _bodyTemplate: [{ type: i0.ContentChild, args: ['body', { isSignal: true }] }], _groupHeaderTemplate: [{ type: i0.ContentChild, args: ['groupHeader', { isSignal: true }] }] } });
954
+
955
+ /**
956
+ * @category control
957
+ */
958
+ class NgnTable extends NgnTableTemplates {
959
+ theme = this.injectThemeTemplate(tableControlTemplate, {
960
+ root: true,
961
+ virtual: () => this.virtual(),
962
+ resizing: () => this._columns?.isDragging() ?? false,
963
+ selectable: () => !!this.selectionMode(),
964
+ reorderable: () => this.reorderable(),
965
+ reordering: () => this._columns?.isReordering() ?? false,
966
+ });
967
+ _scroller = viewChild.required(NgnScroller);
968
+ /** The data rows to render. */
969
+ rows = input.required(/* @ts-ignore */
970
+ ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
971
+ /**
972
+ * When {@link virtual} is enabled, this defines the height of each row in pixels.
973
+ */
974
+ rowHeight = input(/* @ts-ignore */
975
+ ...(ngDevMode ? [undefined, { debugName: "rowHeight" }] : /* istanbul ignore next */ []));
976
+ /** The key of the property that uniquely identifies each row. */
977
+ fieldId = input.required(/* @ts-ignore */
978
+ ...(ngDevMode ? [{ debugName: "fieldId" }] : /* istanbul ignore next */ []));
979
+ /**
980
+ * Whether the table is virtualized. When enabled, provide {@link rowHeight}.
981
+ * @default false
982
+ */
983
+ virtual = input(false, /* @ts-ignore */
984
+ ...(ngDevMode ? [{ debugName: "virtual" }] : /* istanbul ignore next */ []));
985
+ /**
986
+ * When {@link virtual} is enabled, the number of extra rows to render above and below the viewport.
987
+ * @default 2
988
+ */
989
+ virtualPadding = input(2, /* @ts-ignore */
990
+ ...(ngDevMode ? [{ debugName: "virtualPadding" }] : /* istanbul ignore next */ []));
991
+ /**
992
+ * Whether to apply alternating background colors to rows.
993
+ * @default false
994
+ */
995
+ striped = input(false, /* @ts-ignore */
996
+ ...(ngDevMode ? [{ debugName: "striped" }] : /* istanbul ignore next */ []));
997
+ /**
998
+ * Whether to render a paginator and page the rows.
999
+ * @default false
1000
+ */
1001
+ paginator = input(false, /* @ts-ignore */
1002
+ ...(ngDevMode ? [{ debugName: "paginator" }] : /* istanbul ignore next */ []));
1003
+ /**
1004
+ * The selection mode for the table.
1005
+ * - `'single'`: Only one row can be selected at a time.
1006
+ * - `'multi'`: Multiple rows can be selected (with Ctrl/Shift click or checkboxes).
1007
+ * - `null`: Selection is disabled.
1008
+ * @default null
1009
+ */
1010
+ selectionMode = input(null, /* @ts-ignore */
1011
+ ...(ngDevMode ? [{ debugName: "selectionMode" }] : /* istanbul ignore next */ []));
1012
+ /**
1013
+ * The selected row IDs. Two-way bindable via `[(selection)]`.
1014
+ * @default []
1015
+ */
1016
+ selection = model([], /* @ts-ignore */
1017
+ ...(ngDevMode ? [{ debugName: "selection" }] : /* istanbul ignore next */ []));
1018
+ /**
1019
+ * Column key to group rows by. Rows with the same value in this column
1020
+ * are collected under a collapsible group header.
1021
+ * @default null (no grouping)
1022
+ */
1023
+ groupBy = input(null, /* @ts-ignore */
1024
+ ...(ngDevMode ? [{ debugName: "groupBy" }] : /* istanbul ignore next */ []));
1025
+ /**
1026
+ * Two-way model tracking which groups are currently expanded, identified
1027
+ * by their column value. Defaults to `[]` (all collapsed).
1028
+ * Pass all group values to start with all groups expanded.
1029
+ *
1030
+ * Type-safe: when `groupBy` is set, this is typed as `T[G][]` where G
1031
+ * is the groupBy column key (inferred by Angular's template type checker).
1032
+ */
1033
+ expandedGroups = model([], /* @ts-ignore */
1034
+ ...(ngDevMode ? [{ debugName: "expandedGroups" }] : /* istanbul ignore next */ []));
1035
+ /**
1036
+ * Whether column resizing is enabled.
1037
+ * @default false
1038
+ */
1039
+ resizable = input(false, { ...(ngDevMode ? { debugName: "resizable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1040
+ /**
1041
+ * The resize distribution mode.
1042
+ * - `'adjacent'`: Only columns adjacent to the resize handle change. Total width stays the same.
1043
+ * - `'push'`: Column grows/shrinks independently, total width changes (enables horizontal scrolling).
1044
+ * @default 'adjacent'
1045
+ */
1046
+ resizeMode = input('adjacent', /* @ts-ignore */
1047
+ ...(ngDevMode ? [{ debugName: "resizeMode" }] : /* istanbul ignore next */ []));
1048
+ /**
1049
+ * Whether to lock affected columns to fixed `px` values after a resize completes.
1050
+ *
1051
+ * - **`proportional` mode**: when `true`, the resized column stays as `px` (locked);
1052
+ * when `false`, it is converted back to `fr` and continues to flex with the container.
1053
+ * - **`adjacent` / `push` modes**: when `true`, columns whose size changed are converted
1054
+ * to `px`; when `false`, columns keep their original units.
1055
+ *
1056
+ * @default false
1057
+ */
1058
+ lockSizes = input(false, /* @ts-ignore */
1059
+ ...(ngDevMode ? [{ debugName: "lockSizes" }] : /* istanbul ignore next */ []));
1060
+ /**
1061
+ * Whether column reordering is enabled.
1062
+ * @default false
1063
+ */
1064
+ reorderable = input(false, { ...(ngDevMode ? { debugName: "reorderable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1065
+ /**
1066
+ * The current column order as an array of column identifiers.
1067
+ * When empty, the natural registration order (DOM order) is used.
1068
+ * Supports two-way binding for persistence.
1069
+ */
1070
+ columnOrder = model([], /* @ts-ignore */
1071
+ ...(ngDevMode ? [{ debugName: "columnOrder" }] : /* istanbul ignore next */ []));
1072
+ trackById = (item) => item[this.fieldId()];
1073
+ /**
1074
+ * The active sort descriptor (column + direction), or `null` when unsorted. Two-way bindable.
1075
+ * @default null
1076
+ */
1077
+ sort = model(null, /* @ts-ignore */
1078
+ ...(ngDevMode ? [{ debugName: "sort" }] : /* istanbul ignore next */ []));
1079
+ /**
1080
+ * Optional custom comparator used when `sort` is active. Receives the two rows and the
1081
+ * active sort descriptor. When omitted, a default comparator is used (nulls-first on asc,
1082
+ * numeric for numbers, `localeCompare` otherwise).
1083
+ */
1084
+ sortComparator = input(undefined, /* @ts-ignore */
1085
+ ...(ngDevMode ? [{ debugName: "sortComparator" }] : /* istanbul ignore next */ []));
1086
+ /**
1087
+ * The active per-column filter configuration, keyed by column, or `null` when unfiltered. Two-way bindable.
1088
+ * @default null
1089
+ */
1090
+ filters = model(null, /* @ts-ignore */
1091
+ ...(ngDevMode ? [{ debugName: "filters" }] : /* istanbul ignore next */ []));
1092
+ pageState = signal(null, /* @ts-ignore */
1093
+ ...(ngDevMode ? [{ debugName: "pageState" }] : /* istanbul ignore next */ []));
1094
+ i18n = inject(I18n).translations;
1095
+ /**
1096
+ * Polite live-region text announcing sort / filter / pagination / selection
1097
+ * changes to assistive tech. Populated by an effect in the constructor.
1098
+ */
1099
+ liveAnnouncement = signal('', /* @ts-ignore */
1100
+ ...(ngDevMode ? [{ debugName: "liveAnnouncement" }] : /* istanbul ignore next */ []));
1101
+ // --- Column layout (geometry, sticky, resize, reorder, auto-size) ---
1102
+ _columns = new TableColumnLayoutModel({
1103
+ element: this.element,
1104
+ resizable: this.resizable,
1105
+ reorderable: this.reorderable,
1106
+ resizeMode: this.resizeMode,
1107
+ lockSizes: this.lockSizes,
1108
+ columnOrder: this.columnOrder,
1109
+ themeClass: name => this.theme.class(name),
1110
+ });
1111
+ /**
1112
+ * Whether a selection column directive is present in the template.
1113
+ * Set automatically by `NgnTableSelectionColumn` — do not set manually.
1114
+ */
1115
+ showCheckboxes = this._columns.hasSelectionColumn;
1116
+ dropIndicatorState = this._columns.dropIndicatorState;
1117
+ columnOrderMap = computed(() => this._columns.columnOrderMap(), /* @ts-ignore */
1118
+ ...(ngDevMode ? [{ debugName: "columnOrderMap" }] : /* istanbul ignore next */ []));
1119
+ /**
1120
+ * Grid template columns computed from the resize engine when resizable,
1121
+ * otherwise falls back to the standard equal-width repeat.
1122
+ */
1123
+ get gridTemplateColumns() {
1124
+ return this._columns.gridTemplateColumns;
1125
+ }
1126
+ // --- Selection state ---
1127
+ /**
1128
+ * The single current-row index (index in {@link formattedRows}) for
1129
+ * keyboard navigation. Shared by {@link NgnTable}'s selection keyboard
1130
+ * handling and row-actions keyboard navigation — arrows move it, and (when
1131
+ * {@link selectionMode} is set) selection follows it. Cleared whenever
1132
+ * {@link formattedRows} changes identity (sort/filter/rows replaced) so it
1133
+ * never points at a stale row.
1134
+ */
1135
+ focusedRowIndex = signal(null, /* @ts-ignore */
1136
+ ...(ngDevMode ? [{ debugName: "focusedRowIndex" }] : /* istanbul ignore next */ []));
1137
+ _selection;
1138
+ headerCheckboxValue;
1139
+ // --- Row actions registry + keyboard navigation ---
1140
+ _rowActions = new Map();
1141
+ _rowNav;
1142
+ /**
1143
+ * Total column count including the selection checkbox column.
1144
+ */
1145
+ totalColumnCount = computed(() => this._columns.columnCount() + (this._columns.hasSelectionColumn() ? 1 : 0), /* @ts-ignore */
1146
+ ...(ngDevMode ? [{ debugName: "totalColumnCount" }] : /* istanbul ignore next */ []));
1147
+ formattedRows = computed(() => {
1148
+ const groupBy = this.groupBy();
1149
+ if (groupBy) {
1150
+ return this._groupedRows();
1151
+ }
1152
+ const rows = this._sortedRows();
1153
+ return rows.map((data, index) => ({
1154
+ kind: 'data',
1155
+ id: data[this.fieldId()],
1156
+ data,
1157
+ index,
1158
+ }));
1159
+ }, /* @ts-ignore */
1160
+ ...(ngDevMode ? [{ debugName: "formattedRows" }] : /* istanbul ignore next */ []));
1161
+ _groupedRows = computed(() => {
1162
+ const groupBy = this.groupBy();
1163
+ if (!groupBy)
1164
+ return [];
1165
+ return groupRows(this._sortedRows(), groupBy, this.fieldId(), new Set(this.expandedGroups()));
1166
+ }, /* @ts-ignore */
1167
+ ...(ngDevMode ? [{ debugName: "_groupedRows" }] : /* istanbul ignore next */ []));
1168
+ pagedRows = computed(() => {
1169
+ if (!this.paginator()) {
1170
+ return this.formattedRows();
1171
+ }
1172
+ return [...paginateRows(this.formattedRows(), this.pageState())];
1173
+ }, /* @ts-ignore */
1174
+ ...(ngDevMode ? [{ debugName: "pagedRows" }] : /* istanbul ignore next */ []));
1175
+ _filteredRows = computed(() => filterRows(this.rows(), this.filters()), /* @ts-ignore */
1176
+ ...(ngDevMode ? [{ debugName: "_filteredRows" }] : /* istanbul ignore next */ []));
1177
+ _sortedRows = computed(() => sortRows(this._filteredRows(), this.sort(), this.sortComparator()), /* @ts-ignore */
1178
+ ...(ngDevMode ? [{ debugName: "_sortedRows" }] : /* istanbul ignore next */ []));
1179
+ constructor() {
1180
+ super();
1181
+ this._selection = new TableSelectionModel({
1182
+ viewRows: this.formattedRows,
1183
+ selectionMode: this.selectionMode,
1184
+ fieldId: this.fieldId,
1185
+ selection: this.selection,
1186
+ focusedRowIndex: this.focusedRowIndex,
1187
+ scrollToIndex: index => this._scroller().scrollToIndex(index),
1188
+ });
1189
+ this._rowNav = new TableRowNavigationModel({
1190
+ viewRows: this.formattedRows,
1191
+ hasActions: () => this._rowActions.size > 0,
1192
+ focusedRowIndex: this.focusedRowIndex,
1193
+ selectionMode: this.selectionMode,
1194
+ scrollToIndex: index => this._scroller().scrollToIndex(index),
1195
+ enterActions: index => this.getRowActions(index)?.focusFirstAction() ?? false,
1196
+ moveAction: (index, delta) => this.getRowActions(index)?.moveAction(delta) ?? false,
1197
+ openMenu: index => this.getRowActions(index)?.openMenuFromKeyboard() ?? false,
1198
+ focusHost: () => this.element.nativeElement.focus(),
1199
+ });
1200
+ this.headerCheckboxValue = computed(() => this._selection.headerCheckboxValue(), /* @ts-ignore */
1201
+ ...(ngDevMode ? [{ debugName: "headerCheckboxValue" }] : /* istanbul ignore next */ []));
1202
+ // Minor #3: clear the current-row highlight whenever the underlying row
1203
+ // set changes identity (sort/filter/rows replaced), so it never lingers
1204
+ // on a stale index. Only reads `formattedRows` as a dependency — the
1205
+ // writes below must not be read here, or this effect would re-run on its
1206
+ // own writes and clobber every keyboard row move.
1207
+ effect(() => {
1208
+ this.formattedRows();
1209
+ this.focusedRowIndex.set(null);
1210
+ this._rowNav.inActions.set(false);
1211
+ });
1212
+ // A11y: announce sort / filter / pagination / selection changes through a
1213
+ // polite live region — none of these are otherwise conveyed to assistive
1214
+ // tech. Only the aspect that actually changed is announced (checked in
1215
+ // priority order), and the first run only seeds the baselines. Writes only
1216
+ // `liveAnnouncement`, which it never reads, so it cannot re-trigger itself.
1217
+ let seeded = false;
1218
+ let prevSort = '';
1219
+ let prevFilterCount = 0;
1220
+ let prevPageKey = '';
1221
+ let prevSelCount = 0;
1222
+ effect(() => {
1223
+ const sort = this.sort();
1224
+ const filterCount = this._filteredRows().length;
1225
+ const page = this.pageState();
1226
+ const selCount = this.selection().length;
1227
+ const sortKey = sort ? `${sort.column}:${sort.direction}` : '';
1228
+ const pageKey = page ? `${page.page.current}/${page.page.size}` : '';
1229
+ if (!seeded) {
1230
+ seeded = true;
1231
+ [prevSort, prevFilterCount, prevPageKey, prevSelCount] = [
1232
+ sortKey,
1233
+ filterCount,
1234
+ pageKey,
1235
+ selCount,
1236
+ ];
1237
+ return;
1238
+ }
1239
+ let message = '';
1240
+ if (filterCount !== prevFilterCount) {
1241
+ message = this.i18n['table_resultCount']({ count: filterCount });
1242
+ }
1243
+ else if (sortKey !== prevSort) {
1244
+ message = sort
1245
+ ? this.i18n['table_sortedBy']({
1246
+ column: sort.column,
1247
+ direction: this.i18n[sort.direction === 'asc' ? 'table_sortAscending' : 'table_sortDescending'](),
1248
+ })
1249
+ : this.i18n['table_sortCleared']();
1250
+ }
1251
+ else if (pageKey !== prevPageKey) {
1252
+ const size = page?.page.size ?? 0;
1253
+ const pages = size > 0 ? Math.ceil(filterCount / size) : 1;
1254
+ message = this.i18n['table_page']({ page: (page?.page.current ?? 0) + 1, pages });
1255
+ }
1256
+ else if (selCount !== prevSelCount) {
1257
+ message = this.i18n['table_selectedCount']({ count: selCount, total: this.rows().length });
1258
+ }
1259
+ [prevSort, prevFilterCount, prevPageKey, prevSelCount] = [
1260
+ sortKey,
1261
+ filterCount,
1262
+ pageKey,
1263
+ selCount,
1264
+ ];
1265
+ if (message) {
1266
+ this.liveAnnouncement.set(message);
1267
+ }
1268
+ });
1269
+ }
1270
+ pageChanged(event) {
1271
+ this.pageState.set(event);
1272
+ }
1273
+ column(column) {
1274
+ return column;
1275
+ }
1276
+ // --- Column registration / geometry (delegated to TableColumnLayoutModel) ---
1277
+ getVisualColumnIndex(logicalIndex) {
1278
+ return this._columns.getVisualColumnIndex(logicalIndex);
1279
+ }
1280
+ getRegisteredHeaderCells() {
1281
+ return this._columns.getRegisteredHeaderCells();
1282
+ }
1283
+ getStickyInfo(columnId) {
1284
+ return this._columns.getStickyInfo(columnId);
1285
+ }
1286
+ registerHeaderCell(cell) {
1287
+ this._columns.registerHeaderCell(cell);
1288
+ }
1289
+ unregisterHeaderCell(cell) {
1290
+ this._columns.unregisterHeaderCell(cell);
1291
+ }
1292
+ registerSelectionColumn() {
1293
+ this._columns.registerSelectionColumn();
1294
+ }
1295
+ unregisterSelectionColumn() {
1296
+ this._columns.unregisterSelectionColumn();
1297
+ }
1298
+ registerStickyColumn(columnId, side) {
1299
+ this._columns.registerStickyColumn(columnId, side);
1300
+ }
1301
+ unregisterStickyColumn(columnId) {
1302
+ this._columns.unregisterStickyColumn(columnId);
1303
+ }
1304
+ // --- Resize operations (called by NgnTableTh) ---
1305
+ startColumnResize(columnIndex, event) {
1306
+ this._columns.startColumnResize(columnIndex, event);
1307
+ }
1308
+ dragColumnResize(columnIndex, event) {
1309
+ this._columns.dragColumnResize(columnIndex, event);
1310
+ }
1311
+ endColumnResize(columnIndex, cancel) {
1312
+ this._columns.endColumnResize(columnIndex, cancel);
1313
+ }
1314
+ autoSizeColumn(columnIndex) {
1315
+ this._columns.autoSizeColumn(columnIndex);
1316
+ }
1317
+ // --- Reorder operations (called by NgnTableReorderableColumn) ---
1318
+ getReorderBounds(columnId) {
1319
+ return this._columns.getReorderBounds(columnId);
1320
+ }
1321
+ startColumnReorder(columnId) {
1322
+ this._columns.startColumnReorder(columnId);
1323
+ }
1324
+ dragColumnReorder(event) {
1325
+ this._columns.dragColumnReorder(event);
1326
+ }
1327
+ endColumnReorder(cancel) {
1328
+ this._columns.endColumnReorder(cancel);
1329
+ }
1330
+ getReorderSourceColumnId() {
1331
+ return this._columns.getReorderSourceColumnId();
1332
+ }
1333
+ // --- Selection operations ---
1334
+ isRowSelected(id) {
1335
+ return this._selection.isRowSelected(id);
1336
+ }
1337
+ handleRowClick(row, event) {
1338
+ this._selection.handleRowClick(row, event);
1339
+ }
1340
+ handleCheckboxChange(row) {
1341
+ this._selection.handleCheckboxChange(row);
1342
+ }
1343
+ toggleSelectAll() {
1344
+ this._selection.toggleSelectAll();
1345
+ }
1346
+ // --- Row actions registry ---
1347
+ /**
1348
+ * Registers a row's {@link NgnTableRowActions} directive keyed by its row
1349
+ * index, so keyboard navigation can look up the active row's actions.
1350
+ * Called by `NgnTableRowActions`; not intended for manual use.
1351
+ */
1352
+ registerRowActions(index, dir) {
1353
+ this._rowActions.set(index, dir);
1354
+ }
1355
+ /**
1356
+ * Unregisters a row's actions directive. Called by `NgnTableRowActions`.
1357
+ * Only removes the entry if `dir` is still the registered owner for
1358
+ * `index`, so a stale unregister (e.g. after row recycling re-registered
1359
+ * a different instance at the same index) cannot clobber it.
1360
+ */
1361
+ unregisterRowActions(index, dir) {
1362
+ if (this._rowActions.get(index) === dir)
1363
+ this._rowActions.delete(index);
1364
+ }
1365
+ /** Looks up the registered actions directive for a row index, if any. */
1366
+ getRowActions(index) {
1367
+ return this._rowActions.get(index);
1368
+ }
1369
+ /**
1370
+ * Whether keyboard focus is currently inside the action bar of the row at
1371
+ * `index` — i.e. this row is both the current row and
1372
+ * {@link TableRowNavigationModel.inActions}. Drives the `active-row`
1373
+ * highlight in {@link NgnTableBodyTr}.
1374
+ */
1375
+ isRowInActions(index) {
1376
+ return this._rowNav.inActions() && this.focusedRowIndex() === index;
1377
+ }
1378
+ // --- Keyboard navigation ---
1379
+ onKeyDown(event) {
1380
+ if (this._rowNav.onKeyDown(event))
1381
+ return;
1382
+ if (this._rowNav.inActions())
1383
+ return; // focus is in a row's action bar — selection must not handle keys
1384
+ this._selection.onKeyDown(event);
1385
+ }
1386
+ // --- Row grouping ---
1387
+ /**
1388
+ * Toggle the expanded state of a group identified by its column value.
1389
+ */
1390
+ toggleGroup(groupKey) {
1391
+ const current = this.expandedGroups();
1392
+ if (current.includes(groupKey)) {
1393
+ this.expandedGroups.set(current.filter(k => k !== groupKey));
1394
+ }
1395
+ else {
1396
+ this.expandedGroups.set([...current, groupKey]);
1397
+ }
1398
+ }
1399
+ /**
1400
+ * Toggle a group from a group-header row. Used in the template where
1401
+ * the scroller item type cannot carry the `G` generic.
1402
+ * @internal
1403
+ */
1404
+ toggleGroupFromRow(row) {
1405
+ this.toggleGroup(row.groupKey);
1406
+ }
1407
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnTable, deps: [], target: i0.ɵɵFactoryTarget.Component });
1408
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: NgnTable, isStandalone: true, selector: "ngn-table", inputs: { rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: true, transformFunction: null }, rowHeight: { classPropertyName: "rowHeight", publicName: "rowHeight", isSignal: true, isRequired: false, transformFunction: null }, fieldId: { classPropertyName: "fieldId", publicName: "fieldId", isSignal: true, isRequired: true, transformFunction: null }, virtual: { classPropertyName: "virtual", publicName: "virtual", isSignal: true, isRequired: false, transformFunction: null }, virtualPadding: { classPropertyName: "virtualPadding", publicName: "virtualPadding", isSignal: true, isRequired: false, transformFunction: null }, striped: { classPropertyName: "striped", publicName: "striped", isSignal: true, isRequired: false, transformFunction: null }, paginator: { classPropertyName: "paginator", publicName: "paginator", isSignal: true, isRequired: false, transformFunction: null }, selectionMode: { classPropertyName: "selectionMode", publicName: "selectionMode", isSignal: true, isRequired: false, transformFunction: null }, selection: { classPropertyName: "selection", publicName: "selection", isSignal: true, isRequired: false, transformFunction: null }, groupBy: { classPropertyName: "groupBy", publicName: "groupBy", isSignal: true, isRequired: false, transformFunction: null }, expandedGroups: { classPropertyName: "expandedGroups", publicName: "expandedGroups", isSignal: true, isRequired: false, transformFunction: null }, resizable: { classPropertyName: "resizable", publicName: "resizable", isSignal: true, isRequired: false, transformFunction: null }, resizeMode: { classPropertyName: "resizeMode", publicName: "resizeMode", isSignal: true, isRequired: false, transformFunction: null }, lockSizes: { classPropertyName: "lockSizes", publicName: "lockSizes", isSignal: true, isRequired: false, transformFunction: null }, reorderable: { classPropertyName: "reorderable", publicName: "reorderable", isSignal: true, isRequired: false, transformFunction: null }, columnOrder: { classPropertyName: "columnOrder", publicName: "columnOrder", isSignal: true, isRequired: false, transformFunction: null }, sort: { classPropertyName: "sort", publicName: "sort", isSignal: true, isRequired: false, transformFunction: null }, sortComparator: { classPropertyName: "sortComparator", publicName: "sortComparator", isSignal: true, isRequired: false, transformFunction: null }, filters: { classPropertyName: "filters", publicName: "filters", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selection: "selectionChange", expandedGroups: "expandedGroupsChange", columnOrder: "columnOrderChange", sort: "sortChange", filters: "filtersChange" }, host: { attributes: { "tabindex": "0" }, listeners: { "keydown": "onKeyDown($event)" } }, providers: [provideSelf(NgnTable)], viewQueries: [{ propertyName: "_scroller", first: true, predicate: NgnScroller, descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<span\n aria-live=\"polite\"\n aria-atomic=\"true\"\n style=\"\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n clip: rect(0 0 0 0);\n white-space: nowrap;\n border: 0;\n \"\n >{{ liveAnnouncement() }}</span\n>\n<table\n #scrollContainer\n role=\"grid\"\n ngnScrollShadow=\"horizontal\"\n [ptInt]=\"this\"\n [ptClass]=\"{ table: true, striped: striped() }\"\n [attr.aria-multiselectable]=\"selectionMode() === 'multi' ? true : null\"\n [attr.aria-rowcount]=\"formattedRows().length\"\n [style.--ngn-table-column-count]=\"totalColumnCount()\"\n [style.--ngn-table-selection-offset]=\"showCheckboxes() ? 1 : 0\"\n [style.grid-template-columns]=\"gridTemplateColumns()\"\n [style.--ngn-table-row-height.px]=\"rowHeight()\"\n>\n <thead role=\"rowgroup\" [ptInt]=\"this\" [ptClass]=\"'head'\">\n <ng-container [ngTemplateOutlet]=\"headerTemplate()\"></ng-container>\n </thead>\n @if (dropIndicatorState()) {\n <div\n [ptInt]=\"this\"\n [ptClass]=\"'drop-indicator'\"\n [style.left.px]=\"dropIndicatorState()!.leftPx\"\n [style.top.px]=\"dropIndicatorState()!.topPx\"\n [style.height.px]=\"dropIndicatorState()!.heightPx\"\n ></div>\n }\n\n <tbody\n role=\"rowgroup\"\n [ptInt]=\"this\"\n [ptClass]=\"'body'\"\n [ptDep]=\"'scroller'\"\n ngn-scroller\n #scroller\n [scrollContainer]=\"scrollContainer\"\n [virtual]=\"virtual()\"\n [items]=\"pagedRows()\"\n [fieldId]=\"'id'\"\n [itemHeight]=\"rowHeight() || 0\"\n [virtualPadding]=\"virtualPadding()\"\n [style.--ngn-table-item-start-index]=\"scroller.itemStartIndex()\"\n >\n <ng-template #item [ngnTemplate]=\"scroller.templateTypes.item\" let-item let-index=\"index\">\n @if (item.kind === 'group-header') {\n @if (groupHeaderTemplate(); as customGroupHeader) {\n <ng-container\n [ngTemplateOutlet]=\"customGroupHeader\"\n [ngTemplateOutletContext]=\"{\n $implicit: {\n groupKey: item.groupKey,\n groupValue: item.groupValue,\n count: item.count,\n expanded: item.expanded,\n },\n }\"\n ></ng-container>\n } @else {\n <tr\n [ngnTableGroupHeaderTr]=\"item\"\n [class]=\"theme.classes({ 'group-expanded': item.expanded })\"\n [attr.aria-expanded]=\"item.expanded\"\n role=\"row\"\n (click)=\"toggleGroupFromRow(item)\"\n >\n <td [class]=\"theme.class('group-header-cell')\" role=\"gridcell\">\n <ngn-icon\n [ptInt]=\"this\"\n [ptClass]=\"'group-toggle'\"\n [ptDep]=\"'group-toggle-icon'\"\n [defaultIcon]=\"'table-group-toggle'\"\n />\n <span>{{ item.groupValue }}</span>\n <span>({{ item.count }})</span>\n </td>\n </tr>\n }\n } @else {\n <ng-container\n [ngTemplateOutlet]=\"bodyTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"\n ></ng-container>\n }\n </ng-template>\n </tbody>\n</table>\n@if (paginator()) {\n <ngn-paginator\n [ptInt]=\"this\"\n [ptDep]=\"'paginator'\"\n [totalItems]=\"rows().length\"\n (value)=\"pageChanged($event)\"\n />\n}\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: NgnScroller, selector: "ngn-scroller, [ngn-scroller]", inputs: ["items", "fieldId", "virtual", "itemHeight", "virtualPadding", "focusable", "fieldSticky"] }, { kind: "component", type: NgnPaginator, selector: "ngn-paginator", inputs: ["totalItems", "pageSize", "possiblePageSizes", "fixedPageSize", "page"], outputs: ["pageSizeChange", "value", "pageChange"] }, { kind: "directive", type: NgnTemplate, selector: "ng-template[ngnTemplate]", inputs: ["ngnTemplate"] }, { kind: "directive", type: NgnPt, selector: "[ptInt]", inputs: ["ptInt", "ptClass", "ptDep"] }, { kind: "component", type: NgnIcon, selector: "ngn-icon", inputs: ["defaultIcon", "icon"] }, { kind: "directive", type: NgnTableGroupHeaderTr, selector: "[ngnTableGroupHeaderTr]", inputs: ["ngnTableGroupHeaderTr", "ngnScrollerItem"] }, { kind: "directive", type: NgnScrollShadow, selector: "[ngnScrollShadow]", inputs: ["ngnScrollShadow", "scrollShadowTarget"] }] });
1409
+ }
1410
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnTable, decorators: [{
1411
+ type: Component,
1412
+ args: [{ selector: 'ngn-table', imports: [
1413
+ NgTemplateOutlet,
1414
+ NgnScroller,
1415
+ NgnPaginator,
1416
+ NgnTemplate,
1417
+ NgnPt,
1418
+ NgnIcon,
1419
+ NgnTableGroupHeaderTr,
1420
+ NgnScrollShadow,
1421
+ ], providers: [provideSelf(NgnTable)], host: {
1422
+ tabindex: '0',
1423
+ '(keydown)': 'onKeyDown($event)',
1424
+ }, template: "<span\n aria-live=\"polite\"\n aria-atomic=\"true\"\n style=\"\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n clip: rect(0 0 0 0);\n white-space: nowrap;\n border: 0;\n \"\n >{{ liveAnnouncement() }}</span\n>\n<table\n #scrollContainer\n role=\"grid\"\n ngnScrollShadow=\"horizontal\"\n [ptInt]=\"this\"\n [ptClass]=\"{ table: true, striped: striped() }\"\n [attr.aria-multiselectable]=\"selectionMode() === 'multi' ? true : null\"\n [attr.aria-rowcount]=\"formattedRows().length\"\n [style.--ngn-table-column-count]=\"totalColumnCount()\"\n [style.--ngn-table-selection-offset]=\"showCheckboxes() ? 1 : 0\"\n [style.grid-template-columns]=\"gridTemplateColumns()\"\n [style.--ngn-table-row-height.px]=\"rowHeight()\"\n>\n <thead role=\"rowgroup\" [ptInt]=\"this\" [ptClass]=\"'head'\">\n <ng-container [ngTemplateOutlet]=\"headerTemplate()\"></ng-container>\n </thead>\n @if (dropIndicatorState()) {\n <div\n [ptInt]=\"this\"\n [ptClass]=\"'drop-indicator'\"\n [style.left.px]=\"dropIndicatorState()!.leftPx\"\n [style.top.px]=\"dropIndicatorState()!.topPx\"\n [style.height.px]=\"dropIndicatorState()!.heightPx\"\n ></div>\n }\n\n <tbody\n role=\"rowgroup\"\n [ptInt]=\"this\"\n [ptClass]=\"'body'\"\n [ptDep]=\"'scroller'\"\n ngn-scroller\n #scroller\n [scrollContainer]=\"scrollContainer\"\n [virtual]=\"virtual()\"\n [items]=\"pagedRows()\"\n [fieldId]=\"'id'\"\n [itemHeight]=\"rowHeight() || 0\"\n [virtualPadding]=\"virtualPadding()\"\n [style.--ngn-table-item-start-index]=\"scroller.itemStartIndex()\"\n >\n <ng-template #item [ngnTemplate]=\"scroller.templateTypes.item\" let-item let-index=\"index\">\n @if (item.kind === 'group-header') {\n @if (groupHeaderTemplate(); as customGroupHeader) {\n <ng-container\n [ngTemplateOutlet]=\"customGroupHeader\"\n [ngTemplateOutletContext]=\"{\n $implicit: {\n groupKey: item.groupKey,\n groupValue: item.groupValue,\n count: item.count,\n expanded: item.expanded,\n },\n }\"\n ></ng-container>\n } @else {\n <tr\n [ngnTableGroupHeaderTr]=\"item\"\n [class]=\"theme.classes({ 'group-expanded': item.expanded })\"\n [attr.aria-expanded]=\"item.expanded\"\n role=\"row\"\n (click)=\"toggleGroupFromRow(item)\"\n >\n <td [class]=\"theme.class('group-header-cell')\" role=\"gridcell\">\n <ngn-icon\n [ptInt]=\"this\"\n [ptClass]=\"'group-toggle'\"\n [ptDep]=\"'group-toggle-icon'\"\n [defaultIcon]=\"'table-group-toggle'\"\n />\n <span>{{ item.groupValue }}</span>\n <span>({{ item.count }})</span>\n </td>\n </tr>\n }\n } @else {\n <ng-container\n [ngTemplateOutlet]=\"bodyTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"\n ></ng-container>\n }\n </ng-template>\n </tbody>\n</table>\n@if (paginator()) {\n <ngn-paginator\n [ptInt]=\"this\"\n [ptDep]=\"'paginator'\"\n [totalItems]=\"rows().length\"\n (value)=\"pageChanged($event)\"\n />\n}\n" }]
1425
+ }], ctorParameters: () => [], propDecorators: { _scroller: [{ type: i0.ViewChild, args: [i0.forwardRef(() => NgnScroller), { isSignal: true }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: true }] }], rowHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowHeight", required: false }] }], fieldId: [{ type: i0.Input, args: [{ isSignal: true, alias: "fieldId", required: true }] }], virtual: [{ type: i0.Input, args: [{ isSignal: true, alias: "virtual", required: false }] }], virtualPadding: [{ type: i0.Input, args: [{ isSignal: true, alias: "virtualPadding", required: false }] }], striped: [{ type: i0.Input, args: [{ isSignal: true, alias: "striped", required: false }] }], paginator: [{ type: i0.Input, args: [{ isSignal: true, alias: "paginator", required: false }] }], selectionMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionMode", required: false }] }], selection: [{ type: i0.Input, args: [{ isSignal: true, alias: "selection", required: false }] }, { type: i0.Output, args: ["selectionChange"] }], groupBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupBy", required: false }] }], expandedGroups: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandedGroups", required: false }] }, { type: i0.Output, args: ["expandedGroupsChange"] }], resizable: [{ type: i0.Input, args: [{ isSignal: true, alias: "resizable", required: false }] }], resizeMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "resizeMode", required: false }] }], lockSizes: [{ type: i0.Input, args: [{ isSignal: true, alias: "lockSizes", required: false }] }], reorderable: [{ type: i0.Input, args: [{ isSignal: true, alias: "reorderable", required: false }] }], columnOrder: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnOrder", required: false }] }, { type: i0.Output, args: ["columnOrderChange"] }], sort: [{ type: i0.Input, args: [{ isSignal: true, alias: "sort", required: false }] }, { type: i0.Output, args: ["sortChange"] }], sortComparator: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortComparator", required: false }] }], filters: [{ type: i0.Input, args: [{ isSignal: true, alias: "filters", required: false }] }, { type: i0.Output, args: ["filtersChange"] }] } });
1426
+
1427
+ /**
1428
+ * Lightweight body-cell directive. Deliberately does **not** extend `NgnBase`:
1429
+ * a `<td>` is instantiated once per cell (thousands per table), so it avoids the
1430
+ * per-control overhead (kind/color effects, `viewChildren` query, theme kind/color
1431
+ * injection, leave-animation hooks). It only does what a cell needs — apply the
1432
+ * `cell` class, expose its visual column index, and mirror sticky positioning.
1433
+ */
1434
+ class NgnTableTd {
1435
+ _element = inject(ElementRef);
1436
+ theme = injectThemeTemplate(tableControlTemplate);
1437
+ _table = signal(null, /* @ts-ignore */
1438
+ ...(ngDevMode ? [{ debugName: "_table" }] : /* istanbul ignore next */ []));
1439
+ /** 0-based logical (DOM) index of this cell within its row. */
1440
+ _logicalIndex = signal(0, /* @ts-ignore */
1441
+ ...(ngDevMode ? [{ debugName: "_logicalIndex" }] : /* istanbul ignore next */ []));
1442
+ /** 1-based visual column index, remapped through the table's column order. */
1443
+ _visualColumnIndex = computed(() => {
1444
+ const logicalIndex = this._logicalIndex();
1445
+ const table = this._table();
1446
+ if (table) {
1447
+ // Subtract the selection column offset so DOM index aligns with registered header cells.
1448
+ const offset = table.showCheckboxes() ? 1 : 0;
1449
+ return table.getVisualColumnIndex(logicalIndex - offset);
1450
+ }
1451
+ return logicalIndex + 1;
1452
+ }, /* @ts-ignore */
1453
+ ...(ngDevMode ? [{ debugName: "_visualColumnIndex" }] : /* istanbul ignore next */ []));
1454
+ _columnId = computed(() => {
1455
+ const table = this._table();
1456
+ if (!table)
1457
+ return null;
1458
+ const offset = table.showCheckboxes() ? 1 : 0;
1459
+ const logicalIndex = this._logicalIndex() - offset;
1460
+ const cells = table.getRegisteredHeaderCells();
1461
+ return cells[logicalIndex]?.ngnTableTh() ?? null;
1462
+ }, /* @ts-ignore */
1463
+ ...(ngDevMode ? [{ debugName: "_columnId" }] : /* istanbul ignore next */ []));
1464
+ _stickyInfo = computed(() => {
1465
+ const table = this._table();
1466
+ const columnId = this._columnId();
1467
+ if (!table || !columnId)
1468
+ return null;
1469
+ return table.getStickyInfo(columnId);
1470
+ }, /* @ts-ignore */
1471
+ ...(ngDevMode ? [{ debugName: "_stickyInfo" }] : /* istanbul ignore next */ []));
1472
+ constructor() {
1473
+ this.prepareDom();
1474
+ const el = this._element.nativeElement;
1475
+ const parent = el.parentElement?.children;
1476
+ if (parent) {
1477
+ const index = Array.from(parent).findIndex(child => child === el);
1478
+ this._logicalIndex.set(index);
1479
+ }
1480
+ afterNextRender(() => {
1481
+ const table = getNearestNgnInstance(el, NgnTable);
1482
+ this._table.set(table);
1483
+ });
1484
+ afterRenderEffect(() => {
1485
+ const info = this._stickyInfo();
1486
+ const el = this._element.nativeElement;
1487
+ toggleClass(el, this.theme.class('sticky-start'), info?.side === 'start');
1488
+ toggleClass(el, this.theme.class('sticky-end'), info?.side === 'end');
1489
+ toggleClass(el, this.theme.class('sticky-start-edge'), info?.side === 'start' && info.isEdge);
1490
+ toggleClass(el, this.theme.class('sticky-end-edge'), info?.side === 'end' && info.isEdge);
1491
+ if (info) {
1492
+ el.style.position = 'sticky';
1493
+ if (info.side === 'start') {
1494
+ el.style.left = `var(--ngn-sticky-start-offset-${info.index})`;
1495
+ el.style.removeProperty('right');
1496
+ }
1497
+ else {
1498
+ el.style.right = `var(--ngn-sticky-end-offset-${info.index})`;
1499
+ el.style.removeProperty('left');
1500
+ }
1501
+ }
1502
+ else {
1503
+ el.style.removeProperty('position');
1504
+ el.style.removeProperty('left');
1505
+ el.style.removeProperty('right');
1506
+ }
1507
+ });
1508
+ }
1509
+ prepareDom() {
1510
+ toggleClass(this._element.nativeElement, this.theme.class('cell'), true);
1511
+ }
1512
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnTableTd, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1513
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.5", type: NgnTableTd, isStandalone: true, selector: "[ngnTableTd]", host: { attributes: { "role": "gridcell" }, properties: { "style.--ngn-table-column-index": "_visualColumnIndex()" } }, ngImport: i0 });
1514
+ }
1515
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnTableTd, decorators: [{
1516
+ type: Directive,
1517
+ args: [{
1518
+ selector: '[ngnTableTd]',
1519
+ host: { role: 'gridcell', '[style.--ngn-table-column-index]': '_visualColumnIndex()' },
1520
+ }]
1521
+ }], ctorParameters: () => [] });
1522
+
1523
+ class NgnTableTh extends NgnBase {
1524
+ theme = this.injectThemeTemplate(tableControlTemplate);
1525
+ _table;
1526
+ _tableSignal = signal(null, /* @ts-ignore */
1527
+ ...(ngDevMode ? [{ debugName: "_tableSignal" }] : /* istanbul ignore next */ []));
1528
+ _resizeHandle;
1529
+ /** The unique column identifier for this header cell, used for sorting, filtering, sticky, and reorder. */
1530
+ ngnTableTh = input.required(/* @ts-ignore */
1531
+ ...(ngDevMode ? [{ debugName: "ngnTableTh" }] : /* istanbul ignore next */ []));
1532
+ /**
1533
+ * The size of this column (e.g. '1fr', '200px', '25%').
1534
+ * @default '1fr'
1535
+ */
1536
+ size = model('1fr', /* @ts-ignore */
1537
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
1538
+ /**
1539
+ * The minimum size constraint for this column.
1540
+ * @default '50px'
1541
+ */
1542
+ minSize = input('50px', /* @ts-ignore */
1543
+ ...(ngDevMode ? [{ debugName: "minSize" }] : /* istanbul ignore next */ []));
1544
+ /**
1545
+ * The maximum size constraint for this column.
1546
+ * @default '100%'
1547
+ */
1548
+ maxSize = input('100%', /* @ts-ignore */
1549
+ ...(ngDevMode ? [{ debugName: "maxSize" }] : /* istanbul ignore next */ []));
1550
+ /**
1551
+ * Whether this column is resizable. Computed from the parent table's `resizable` input.
1552
+ */
1553
+ resizable = computed(() => this._table?.resizable() ?? false, /* @ts-ignore */
1554
+ ...(ngDevMode ? [{ debugName: "resizable" }] : /* istanbul ignore next */ []));
1555
+ /**
1556
+ * Whether the resize handle should be shown. Hidden for the last column in adjacent
1557
+ * and proportional modes since it is implicitly resized via the previous column's handle.
1558
+ * Push mode shows handles on all columns since each column grows independently.
1559
+ */
1560
+ _showHandle = computed(() => {
1561
+ if (!this._table?.resizable())
1562
+ return false;
1563
+ if (this._table.resizeMode() === 'push')
1564
+ return true;
1565
+ // Adjacent & proportional: hide handle on last column
1566
+ const cells = this._table.getRegisteredHeaderCells();
1567
+ return cells.indexOf(this) < cells.length - 1;
1568
+ }, /* @ts-ignore */
1569
+ ...(ngDevMode ? [{ debugName: "_showHandle" }] : /* istanbul ignore next */ []));
1570
+ /**
1571
+ * 1-based visual column index, derived from the table's column order map.
1572
+ * Falls back to registration index + 1 when no reordering is active.
1573
+ */
1574
+ _visualColumnIndex = computed(() => {
1575
+ if (!this._table)
1576
+ return undefined;
1577
+ const map = this._table.columnOrderMap();
1578
+ return map.get(this.ngnTableTh()) ?? this.getColumnIndex() + 1;
1579
+ }, /* @ts-ignore */
1580
+ ...(ngDevMode ? [{ debugName: "_visualColumnIndex" }] : /* istanbul ignore next */ []));
1581
+ constructor() {
1582
+ super();
1583
+ this.prepareDom();
1584
+ const renderer = inject(Renderer2);
1585
+ const elRef = inject(ElementRef);
1586
+ afterNextRender(() => {
1587
+ // Wrap existing content in a text container for ellipsis support
1588
+ const textWrapper = renderer.createElement('span');
1589
+ textWrapper.classList.add(this.theme.class('cell-text'));
1590
+ const el = elRef.nativeElement;
1591
+ while (el.firstChild) {
1592
+ textWrapper.appendChild(el.firstChild);
1593
+ }
1594
+ el.appendChild(textWrapper);
1595
+ // Move dynamically-created controls back out of the text wrapper
1596
+ // so they participate in the header cell's flex layout (order: 2, 3)
1597
+ const filterControl = textWrapper.querySelector(`.${this.theme.class('filter-control')}`);
1598
+ if (filterControl)
1599
+ el.appendChild(filterControl);
1600
+ const sortControl = textWrapper.querySelector(`.${this.theme.class('sort-control')}`);
1601
+ if (sortControl)
1602
+ el.appendChild(sortControl);
1603
+ // Spacer div for flex layout
1604
+ const spacer = renderer.createElement('div');
1605
+ spacer.classList.add(this.theme.class('spacer'));
1606
+ elRef.nativeElement.appendChild(spacer);
1607
+ // Resize handle
1608
+ this._resizeHandle = renderer.createElement('div');
1609
+ this._resizeHandle.classList.add(this.theme.class('resize-handle'));
1610
+ this._resizeHandle.addEventListener('pointerdown', this.onResizePointerDown);
1611
+ this._resizeHandle.addEventListener('dblclick', this.onResizeDblClick);
1612
+ elRef.nativeElement.appendChild(this._resizeHandle);
1613
+ });
1614
+ // Toggle handle visibility based on mode and column position
1615
+ afterRenderEffect(() => {
1616
+ const show = this._showHandle();
1617
+ if (this._resizeHandle) {
1618
+ this._resizeHandle.style.display = show ? '' : 'none';
1619
+ }
1620
+ });
1621
+ // Sticky column classes and offsets
1622
+ afterRenderEffect(() => {
1623
+ const table = this._tableSignal();
1624
+ if (!table)
1625
+ return;
1626
+ const info = table.getStickyInfo(this.ngnTableTh());
1627
+ const el = this.element.nativeElement;
1628
+ toggleClass(el, this.theme.class('sticky-start'), info?.side === 'start');
1629
+ toggleClass(el, this.theme.class('sticky-end'), info?.side === 'end');
1630
+ toggleClass(el, this.theme.class('sticky-start-edge'), info?.side === 'start' && info.isEdge);
1631
+ toggleClass(el, this.theme.class('sticky-end-edge'), info?.side === 'end' && info.isEdge);
1632
+ if (info) {
1633
+ el.style.position = 'sticky';
1634
+ if (info.side === 'start') {
1635
+ el.style.left = `var(--ngn-sticky-start-offset-${info.index})`;
1636
+ el.style.removeProperty('right');
1637
+ }
1638
+ else {
1639
+ el.style.right = `var(--ngn-sticky-end-offset-${info.index})`;
1640
+ el.style.removeProperty('left');
1641
+ }
1642
+ }
1643
+ else {
1644
+ el.style.removeProperty('position');
1645
+ el.style.removeProperty('left');
1646
+ el.style.removeProperty('right');
1647
+ }
1648
+ });
1649
+ }
1650
+ ngOnInit() {
1651
+ const table = getNearestNgnInstance(this.element.nativeElement, (NgnTable));
1652
+ if (!table) {
1653
+ throw new NgnError('ngnTableTh', 'ngnTableTh must be used within an NgnTable component');
1654
+ }
1655
+ this._table = table;
1656
+ this._tableSignal.set(table);
1657
+ this._table.registerHeaderCell(this);
1658
+ }
1659
+ ngOnDestroy() {
1660
+ this._table?.unregisterHeaderCell(this);
1661
+ this._resizeHandle?.removeEventListener('pointerdown', this.onResizePointerDown);
1662
+ this._resizeHandle?.removeEventListener('dblclick', this.onResizeDblClick);
1663
+ }
1664
+ prepareDom() {
1665
+ toggleClass(this.element.nativeElement, this.theme.class('cell'), true);
1666
+ toggleClass(this.element.nativeElement, this.theme.class('resizable'), true);
1667
+ }
1668
+ onResizeDblClick = (event) => {
1669
+ if (!this._table?.resizable())
1670
+ return;
1671
+ event.preventDefault();
1672
+ event.stopPropagation();
1673
+ const columnIndex = this.getColumnIndex();
1674
+ if (columnIndex < 0)
1675
+ return;
1676
+ this._table.autoSizeColumn(columnIndex);
1677
+ };
1678
+ onResizePointerDown = (event) => {
1679
+ if (!this._table?.resizable())
1680
+ return;
1681
+ event.preventDefault();
1682
+ event.stopPropagation();
1683
+ const target = event.target;
1684
+ try {
1685
+ target.setPointerCapture(event.pointerId);
1686
+ }
1687
+ catch {
1688
+ // Ignore
1689
+ }
1690
+ const columnIndex = this.getColumnIndex();
1691
+ if (columnIndex < 0)
1692
+ return;
1693
+ this._table.startColumnResize(columnIndex, event);
1694
+ const onPointerMove = (e) => {
1695
+ this._table.dragColumnResize(columnIndex, e);
1696
+ };
1697
+ const cleanup = (cancel) => {
1698
+ this._table.endColumnResize(columnIndex, cancel);
1699
+ target.removeEventListener('pointermove', onPointerMove);
1700
+ target.removeEventListener('pointerup', onPointerUp);
1701
+ target.removeEventListener('pointercancel', onPointerCancel);
1702
+ };
1703
+ const onPointerUp = () => cleanup(false);
1704
+ const onPointerCancel = () => cleanup(true);
1705
+ target.addEventListener('pointermove', onPointerMove);
1706
+ target.addEventListener('pointerup', onPointerUp);
1707
+ target.addEventListener('pointercancel', onPointerCancel);
1708
+ };
1709
+ getColumnIndex() {
1710
+ if (!this._table)
1711
+ return -1;
1712
+ const cells = this._table.getRegisteredHeaderCells();
1713
+ return cells.indexOf(this);
1714
+ }
1715
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnTableTh, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1716
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: NgnTableTh, isStandalone: true, selector: "[ngnTableTh]", inputs: { ngnTableTh: { classPropertyName: "ngnTableTh", publicName: "ngnTableTh", isSignal: true, isRequired: true, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, minSize: { classPropertyName: "minSize", publicName: "minSize", isSignal: true, isRequired: false, transformFunction: null }, maxSize: { classPropertyName: "maxSize", publicName: "maxSize", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { size: "sizeChange" }, host: { attributes: { "role": "columnheader" }, properties: { "style.--ngn-table-column-index": "_visualColumnIndex()" } }, usesInheritance: true, ngImport: i0 });
1717
+ }
1718
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnTableTh, decorators: [{
1719
+ type: Directive,
1720
+ args: [{
1721
+ selector: '[ngnTableTh]',
1722
+ host: {
1723
+ role: 'columnheader',
1724
+ '[style.--ngn-table-column-index]': '_visualColumnIndex()',
1725
+ },
1726
+ }]
1727
+ }], ctorParameters: () => [], propDecorators: { ngnTableTh: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngnTableTh", required: true }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }, { type: i0.Output, args: ["sizeChange"] }], minSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "minSize", required: false }] }], maxSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSize", required: false }] }] } });
1728
+
1729
+ class NgnTableHeadTr extends NgnBase {
1730
+ theme = this.injectThemeTemplate(tableControlTemplate);
1731
+ _element = inject((ElementRef));
1732
+ constructor() {
1733
+ super();
1734
+ this.prepareDom();
1735
+ }
1736
+ prepareDom() {
1737
+ toggleClass(this._element.nativeElement, this.theme.class('row'), true);
1738
+ }
1739
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnTableHeadTr, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1740
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.5", type: NgnTableHeadTr, isStandalone: true, selector: "[ngnTableHeadTr]", host: { attributes: { "role": "row" }, properties: { "attr.aria-rowindex": "1" } }, usesInheritance: true, ngImport: i0 });
1741
+ }
1742
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnTableHeadTr, decorators: [{
1743
+ type: Directive,
1744
+ args: [{ selector: '[ngnTableHeadTr]', host: { role: 'row', '[attr.aria-rowindex]': '1' } }]
1745
+ }], ctorParameters: () => [] });
1746
+
1747
+ /** Minimum distance in pixels before a drag is initiated. */
1748
+ const REORDER_DEAD_ZONE_PX = 5;
1749
+ /**
1750
+ * @category directive
1751
+ */
1752
+ class NgnTableReorderableColumn {
1753
+ theme = injectThemeTemplate(tableControlTemplate);
1754
+ _element = inject(ElementRef);
1755
+ _headerCell = inject(NgnTableTh);
1756
+ _columnId = this._headerCell.ngnTableTh;
1757
+ _table = getNearestNgnInstanceSig(this._element.nativeElement, NgnTable);
1758
+ _isDragging = false;
1759
+ _startX = 0;
1760
+ _startY = 0;
1761
+ _pointerId = null;
1762
+ _boundOnPointerMove = null;
1763
+ _boundOnPointerUp = null;
1764
+ _boundOnPointerCancel = null;
1765
+ _boundOnKeyDown = null;
1766
+ constructor() {
1767
+ afterNextRender(() => {
1768
+ const el = this._element.nativeElement;
1769
+ el.addEventListener('pointerdown', this.onPointerDown);
1770
+ });
1771
+ }
1772
+ ngOnDestroy() {
1773
+ this._element.nativeElement.removeEventListener('pointerdown', this.onPointerDown);
1774
+ this.cleanup();
1775
+ }
1776
+ onPointerDown = (event) => {
1777
+ const table = this._table();
1778
+ if (!table?.reorderable())
1779
+ return;
1780
+ // Don't intercept resize handle clicks
1781
+ const target = event.target;
1782
+ if (target.classList.contains(this.theme.class('resize-handle')))
1783
+ return;
1784
+ event.preventDefault();
1785
+ this._startX = event.clientX;
1786
+ this._startY = event.clientY;
1787
+ this._isDragging = false;
1788
+ this._pointerId = event.pointerId;
1789
+ try {
1790
+ this._element.nativeElement.setPointerCapture(event.pointerId);
1791
+ }
1792
+ catch {
1793
+ // Ignore
1794
+ }
1795
+ this._boundOnPointerMove = (e) => this.onPointerMove(e);
1796
+ this._boundOnPointerUp = () => this.onPointerUp();
1797
+ this._boundOnPointerCancel = () => this.onPointerCancel();
1798
+ this._boundOnKeyDown = (e) => this.onKeyDown(e);
1799
+ this._element.nativeElement.addEventListener('pointermove', this._boundOnPointerMove);
1800
+ this._element.nativeElement.addEventListener('pointerup', this._boundOnPointerUp);
1801
+ this._element.nativeElement.addEventListener('pointercancel', this._boundOnPointerCancel);
1802
+ document.addEventListener('keydown', this._boundOnKeyDown);
1803
+ };
1804
+ onPointerMove(event) {
1805
+ if (!this._isDragging) {
1806
+ const deltaX = event.clientX - this._startX;
1807
+ const deltaY = event.clientY - this._startY;
1808
+ const distance = Math.sqrt(deltaX ** 2 + deltaY ** 2);
1809
+ if (distance < REORDER_DEAD_ZONE_PX)
1810
+ return;
1811
+ // Start dragging
1812
+ this._isDragging = true;
1813
+ const table = this._table();
1814
+ table?.startColumnReorder(this._columnId());
1815
+ toggleClass(this._element.nativeElement, this.theme.class('drag-source'), true);
1816
+ }
1817
+ this._table()?.dragColumnReorder(event);
1818
+ }
1819
+ onPointerUp() {
1820
+ if (this._isDragging) {
1821
+ this._table()?.endColumnReorder(false);
1822
+ toggleClass(this._element.nativeElement, this.theme.class('drag-source'), false);
1823
+ }
1824
+ this.cleanup();
1825
+ }
1826
+ onPointerCancel() {
1827
+ if (this._isDragging) {
1828
+ this._table()?.endColumnReorder(true);
1829
+ toggleClass(this._element.nativeElement, this.theme.class('drag-source'), false);
1830
+ }
1831
+ this.cleanup();
1832
+ }
1833
+ onKeyDown(event) {
1834
+ if (event.key === 'Escape' && this._isDragging) {
1835
+ this._table()?.endColumnReorder(true);
1836
+ toggleClass(this._element.nativeElement, this.theme.class('drag-source'), false);
1837
+ this.cleanup();
1838
+ }
1839
+ }
1840
+ cleanup() {
1841
+ this._isDragging = false;
1842
+ if (this._pointerId != null) {
1843
+ try {
1844
+ this._element.nativeElement.releasePointerCapture(this._pointerId);
1845
+ }
1846
+ catch {
1847
+ // Ignore — capture may already be released
1848
+ }
1849
+ this._pointerId = null;
1850
+ }
1851
+ if (this._boundOnPointerMove) {
1852
+ this._element.nativeElement.removeEventListener('pointermove', this._boundOnPointerMove);
1853
+ this._boundOnPointerMove = null;
1854
+ }
1855
+ if (this._boundOnPointerUp) {
1856
+ this._element.nativeElement.removeEventListener('pointerup', this._boundOnPointerUp);
1857
+ this._boundOnPointerUp = null;
1858
+ }
1859
+ if (this._boundOnPointerCancel) {
1860
+ this._element.nativeElement.removeEventListener('pointercancel', this._boundOnPointerCancel);
1861
+ this._boundOnPointerCancel = null;
1862
+ }
1863
+ if (this._boundOnKeyDown) {
1864
+ document.removeEventListener('keydown', this._boundOnKeyDown);
1865
+ this._boundOnKeyDown = null;
1866
+ }
1867
+ }
1868
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnTableReorderableColumn, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1869
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.5", type: NgnTableReorderableColumn, isStandalone: true, selector: "[ngnTableReorderableColumn]", ngImport: i0 });
1870
+ }
1871
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnTableReorderableColumn, decorators: [{
1872
+ type: Directive,
1873
+ args: [{
1874
+ selector: '[ngnTableReorderableColumn]',
1875
+ }]
1876
+ }], ctorParameters: () => [] });
1877
+
1878
+ class NgnTableBodyTr extends NgnScrollerItem {
1879
+ /** The formatted data row this `<tr>` renders. */
1880
+ ngnTableBodyTr = input.required(/* @ts-ignore */
1881
+ ...(ngDevMode ? [{ debugName: "ngnTableBodyTr" }] : /* istanbul ignore next */ []));
1882
+ /** The item bound to the underlying scroller entry; kept in sync with {@link ngnTableBodyTr}. */
1883
+ ngnScrollerItem = input({}, /* @ts-ignore */
1884
+ ...(ngDevMode ? [{ debugName: "ngnScrollerItem" }] : /* istanbul ignore next */ []));
1885
+ _element = inject((ElementRef));
1886
+ theme = injectThemeTemplate(tableControlTemplate);
1887
+ _table = getNearestNgnInstanceSig(this._element.nativeElement, NgnTable);
1888
+ selectable = computed(() => !!this._table()?.selectionMode(), /* @ts-ignore */
1889
+ ...(ngDevMode ? [{ debugName: "selectable" }] : /* istanbul ignore next */ []));
1890
+ selected = computed(() => {
1891
+ const table = this._table();
1892
+ if (!table || !table.selectionMode())
1893
+ return false;
1894
+ const row = this.ngnTableBodyTr();
1895
+ return table.isRowSelected(row.id);
1896
+ }, /* @ts-ignore */
1897
+ ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
1898
+ /**
1899
+ * Whether this row is the table's single current-row for keyboard
1900
+ * navigation ({@link NgnTable.focusedRowIndex}). Tracks selection keyboard
1901
+ * nav when {@link NgnTable.selectionMode} is set, and also tracks
1902
+ * row-actions keyboard nav on non-selectable tables that have row actions,
1903
+ * so a keyboard user always sees which row is current.
1904
+ */
1905
+ focused = computed(() => {
1906
+ const table = this._table();
1907
+ if (!table)
1908
+ return false;
1909
+ return table.focusedRowIndex() === this.ngnTableBodyTr().index;
1910
+ }, /* @ts-ignore */
1911
+ ...(ngDevMode ? [{ debugName: "focused" }] : /* istanbul ignore next */ []));
1912
+ /** Whether this row's action bar currently has DOM focus. */
1913
+ active = computed(() => {
1914
+ const table = this._table();
1915
+ if (!table)
1916
+ return false;
1917
+ return table.isRowInActions(this.ngnTableBodyTr().index);
1918
+ }, /* @ts-ignore */
1919
+ ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
1920
+ constructor() {
1921
+ super();
1922
+ effect(() => {
1923
+ const row = this.ngnTableBodyTr();
1924
+ setInputSignalValue(this.ngnScrollerItem, row);
1925
+ });
1926
+ this.prepareDom();
1927
+ }
1928
+ prepareDom() {
1929
+ toggleClass(this._element.nativeElement, this.theme.class('row'), true);
1930
+ }
1931
+ onRowClick(event) {
1932
+ const table = this._table();
1933
+ if (!table || !table.selectionMode())
1934
+ return;
1935
+ table.handleRowClick(this.ngnTableBodyTr(), event);
1936
+ }
1937
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnTableBodyTr, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1938
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: NgnTableBodyTr, isStandalone: true, selector: "[ngnTableBodyTr]", inputs: { ngnTableBodyTr: { classPropertyName: "ngnTableBodyTr", publicName: "ngnTableBodyTr", isSignal: true, isRequired: true, transformFunction: null }, ngnScrollerItem: { classPropertyName: "ngnScrollerItem", publicName: "ngnScrollerItem", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "row" }, listeners: { "click": "onRowClick($event)" }, properties: { "attr.aria-rowindex": "ngnTableBodyTr().index + 2", "style.--ngn-table-row-index": "ngnTableBodyTr().index + 2", "class": "theme.classes({\n 'even': ngnTableBodyTr().index % 2 === 0,\n 'selected-row': selected(),\n 'focused-row': focused(),\n 'active-row': active()\n })", "attr.aria-selected": "selectable() ? selected() : null" } }, usesInheritance: true, ngImport: i0 });
1939
+ }
1940
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnTableBodyTr, decorators: [{
1941
+ type: Directive,
1942
+ args: [{
1943
+ selector: '[ngnTableBodyTr]',
1944
+ host: {
1945
+ '[attr.aria-rowindex]': 'ngnTableBodyTr().index + 2',
1946
+ '[style.--ngn-table-row-index]': 'ngnTableBodyTr().index + 2',
1947
+ '[class]': `theme.classes({
1948
+ 'even': ngnTableBodyTr().index % 2 === 0,
1949
+ 'selected-row': selected(),
1950
+ 'focused-row': focused(),
1951
+ 'active-row': active()
1952
+ })`,
1953
+ '[attr.aria-selected]': 'selectable() ? selected() : null',
1954
+ role: 'row',
1955
+ '(click)': 'onRowClick($event)',
1956
+ },
1957
+ }]
1958
+ }], ctorParameters: () => [], propDecorators: { ngnTableBodyTr: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngnTableBodyTr", required: true }] }], ngnScrollerItem: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngnScrollerItem", required: false }] }] } });
1959
+
1960
+ /** Whether an action opens a submenu rather than firing a leaf callback. */
1961
+ function hasChildren(item) {
1962
+ return !!item.children && item.children.length > 0;
1963
+ }
1964
+ /**
1965
+ * Adapts an {@link NgnActionItem} (the context-menu shape) to the
1966
+ * {@link NgnActionButtonConfig} that `ngn-action-button` consumes. Icon-only
1967
+ * rendering (`kind: 'icon'`) is chosen when the item has an icon, so the label
1968
+ * becomes the button's tooltip / accessible name.
1969
+ */
1970
+ function actionItemToButtonConfig(item) {
1971
+ return {
1972
+ label: item.label,
1973
+ value: item.id,
1974
+ icon: item.icon,
1975
+ kind: item.icon ? 'icon' : undefined,
1976
+ disabled: item.disabled,
1977
+ testId: item.testId,
1978
+ action: () => item.callback?.(),
1979
+ };
1980
+ }
1981
+
1982
+ /**
1983
+ * Inline row-actions bar: renders each {@link NgnActionItem} as an
1984
+ * `ngn-action-button`. An action with `children` opens a submenu popover
1985
+ * anchored to its own button — the anchor is resolved from the rendered
1986
+ * button element itself, never from a `testId` lookup, so it keeps working
1987
+ * even when an action has no `testId`. Positioning/visibility is applied by
1988
+ * the table theme via the `row-actions` class; this component only renders
1989
+ * the buttons.
1990
+ */
1991
+ class NgnTableRowActionsBar {
1992
+ theme = injectThemeTemplate(tableControlTemplate);
1993
+ _vcr = inject(ViewContainerRef);
1994
+ _element = inject(ElementRef);
1995
+ _menu;
1996
+ // One host `ElementRef` per rendered `ngn-action-button`, in the same order
1997
+ // as `barActions()`. Reading the native `<button>` off these refs is the
1998
+ // robust anchor for the submenu popover — it doesn't depend on the action
1999
+ // having a `testId`. The accessible name itself is owned by `NgnTooltip`
2000
+ // (via `ngnTooltipAutoAriaMode="label"` on `action-button.html`), not this
2001
+ // component.
2002
+ _buttonHosts = viewChildren(NgnActionButton, { ...(ngDevMode ? { debugName: "_buttonHosts" } : /* istanbul ignore next */ {}), read: ElementRef });
2003
+ /**
2004
+ * The actions to render as buttons. Used for standalone consumers that bind
2005
+ * an array directly. When {@link actionsSource} is provided (the directive
2006
+ * mounts the bar imperatively), that signal takes precedence.
2007
+ */
2008
+ actions = input([], /* @ts-ignore */
2009
+ ...(ngDevMode ? [{ debugName: "actions" }] : /* istanbul ignore next */ []));
2010
+ /**
2011
+ * A reactive source of actions. Preferred over {@link actions} when set:
2012
+ * the host directive passes its own `actions` input signal here so the bar
2013
+ * reads it lazily during its own change detection. This avoids the directive
2014
+ * imperatively re-pushing the array every change-detection pass — which, when
2015
+ * a consumer binds a fresh array each pass (e.g. `actionsFor(row)`), would
2016
+ * otherwise loop into infinite change detection (NG0103).
2017
+ * @internal
2018
+ */
2019
+ actionsSource = input(null, /* @ts-ignore */
2020
+ ...(ngDevMode ? [{ debugName: "actionsSource" }] : /* istanbul ignore next */ []));
2021
+ barActions = computed(() => {
2022
+ const source = this.actionsSource();
2023
+ const items = source ? source() : this.actions();
2024
+ return items.map(item => ({
2025
+ item,
2026
+ config: actionItemToButtonConfig(item),
2027
+ hasChildren: hasChildren(item),
2028
+ }));
2029
+ }, /* @ts-ignore */
2030
+ ...(ngDevMode ? [{ debugName: "barActions" }] : /* istanbul ignore next */ []));
2031
+ ngOnDestroy() {
2032
+ this._menu?.destroy();
2033
+ }
2034
+ // Single source of truth for the rendered `<button>` elements, in bar order:
2035
+ // derived from the `_buttonHosts()` view query (same as the submenu anchor
2036
+ // uses), rather than a parallel `querySelectorAll` — so focus navigation and
2037
+ // anchoring can never drift apart.
2038
+ _buttons = () => this._buttonHosts()
2039
+ .map(host => this._buttonElement(host))
2040
+ .filter((b) => b !== null);
2041
+ /** Focuses the first enabled action button. Returns whether one was focused. */
2042
+ focusFirst() {
2043
+ const btn = this._buttons().find(b => !b.disabled);
2044
+ btn?.focus();
2045
+ return !!btn;
2046
+ }
2047
+ /**
2048
+ * Moves focus by `delta` among the rendered action buttons. Returns whether
2049
+ * focus stayed within the bar — `false` when the move would go past the
2050
+ * first/last button, in which case focus is left unchanged.
2051
+ */
2052
+ move(delta) {
2053
+ const buttons = this._buttons();
2054
+ const active = document.activeElement;
2055
+ const idx = buttons.findIndex(b => b === active);
2056
+ const next = idx + delta;
2057
+ if (next < 0 || next >= buttons.length)
2058
+ return false;
2059
+ buttons[next]?.focus();
2060
+ return true;
2061
+ }
2062
+ /**
2063
+ * Stops a native button click (mouse or Space/Enter key activation) from
2064
+ * bubbling to the row's own click handler — otherwise, on a table with
2065
+ * `selectionMode` set, activating an action would also select/toggle the
2066
+ * row it lives in. Mirrors {@link import('./table-selection-column').NgnTableSelectionColumn}'s
2067
+ * click guard for the same reason.
2068
+ */
2069
+ onBarClick(event) {
2070
+ event.stopPropagation();
2071
+ }
2072
+ onClicked(action) {
2073
+ if (!action.hasChildren)
2074
+ return;
2075
+ const index = this.barActions().indexOf(action);
2076
+ const host = index >= 0 ? this._buttonHosts()[index] : undefined;
2077
+ const anchor = (host && this._buttonElement(host)) ?? this._element.nativeElement;
2078
+ this._menu = openMenuAt(this._vcr, this._menu, action.item.children ?? [], anchor, 'bottom-end');
2079
+ }
2080
+ _buttonElement(host) {
2081
+ return host.nativeElement.querySelector('button');
2082
+ }
2083
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnTableRowActionsBar, deps: [], target: i0.ɵɵFactoryTarget.Component });
2084
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: NgnTableRowActionsBar, isStandalone: true, selector: "ngn-table-row-actions-bar", inputs: { actions: { classPropertyName: "actions", publicName: "actions", isSignal: true, isRequired: false, transformFunction: null }, actionsSource: { classPropertyName: "actionsSource", publicName: "actionsSource", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "presentation" }, listeners: { "click": "onBarClick($event)" }, properties: { "class": "theme.class('row-actions')" } }, viewQueries: [{ propertyName: "_buttonHosts", predicate: NgnActionButton, descendants: true, read: ElementRef, isSignal: true }], ngImport: i0, template: `
2085
+ @for (action of barActions(); track action.item.id) {
2086
+ <ngn-action-button
2087
+ [config]="action.config"
2088
+ [inline]="true"
2089
+ [attr.aria-haspopup]="action.hasChildren ? 'menu' : null"
2090
+ (clicked)="onClicked(action)"
2091
+ />
2092
+ }
2093
+ `, isInline: true, dependencies: [{ kind: "component", type: NgnActionButton, selector: "ngn-action-button", inputs: ["config", "inline"], outputs: ["clicked"] }] });
2094
+ }
2095
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnTableRowActionsBar, decorators: [{
2096
+ type: Component,
2097
+ args: [{
2098
+ selector: 'ngn-table-row-actions-bar',
2099
+ imports: [NgnActionButton],
2100
+ template: `
2101
+ @for (action of barActions(); track action.item.id) {
2102
+ <ngn-action-button
2103
+ [config]="action.config"
2104
+ [inline]="true"
2105
+ [attr.aria-haspopup]="action.hasChildren ? 'menu' : null"
2106
+ (clicked)="onClicked(action)"
2107
+ />
2108
+ }
2109
+ `,
2110
+ host: {
2111
+ '[class]': `theme.class('row-actions')`,
2112
+ role: 'presentation',
2113
+ '(click)': 'onBarClick($event)',
2114
+ },
2115
+ }]
2116
+ }], propDecorators: { _buttonHosts: [{ type: i0.ViewChildren, args: [i0.forwardRef(() => NgnActionButton), { ...{ read: ElementRef }, isSignal: true }] }], actions: [{ type: i0.Input, args: [{ isSignal: true, alias: "actions", required: false }] }], actionsSource: [{ type: i0.Input, args: [{ isSignal: true, alias: "actionsSource", required: false }] }] } });
2117
+
2118
+ /**
2119
+ * Per-row actions for the table body `<tr>`. Exposes the same `NgnActionItem[]`
2120
+ * as a right-click context menu ({@link context}) and/or an inline hover
2121
+ * button-bar ({@link inline}). Both modes are on by default and independent.
2122
+ */
2123
+ class NgnTableRowActions {
2124
+ _element = inject(ElementRef);
2125
+ _vcr = inject(ViewContainerRef);
2126
+ _renderer = inject(Renderer2);
2127
+ _row = inject(NgnTableBodyTr, { optional: true });
2128
+ _table = getNearestNgnInstanceSig(this._element.nativeElement, NgnTable);
2129
+ _menu;
2130
+ _bar;
2131
+ /** The actions available on this row. */
2132
+ actions = input.required({ ...(ngDevMode ? { debugName: "actions" } : /* istanbul ignore next */ {}), alias: 'ngnTableRowActions' });
2133
+ /**
2134
+ * Whether right-clicking the row opens a context menu of the actions.
2135
+ * @default true
2136
+ */
2137
+ context = input(true, { ...(ngDevMode ? { debugName: "context" } : /* istanbul ignore next */ {}), transform: booleanAttribute,
2138
+ alias: 'ngnTableRowActionsContext' });
2139
+ /**
2140
+ * Whether an inline button-bar is rendered at the row's right edge, revealed
2141
+ * on hover or keyboard focus.
2142
+ * @default true
2143
+ */
2144
+ inline = input(true, { ...(ngDevMode ? { debugName: "inline" } : /* istanbul ignore next */ {}), transform: booleanAttribute,
2145
+ alias: 'ngnTableRowActionsInline' });
2146
+ constructor() {
2147
+ domEventHandler(this._element, 'contextmenu', e => this._onContextMenu(e));
2148
+ effect(() => {
2149
+ if (this.inline()) {
2150
+ this._mountBar();
2151
+ }
2152
+ else {
2153
+ this._unmountBar();
2154
+ }
2155
+ });
2156
+ // Register with the nearest table keyed by this row's index, so the
2157
+ // table's keyboard navigation model can reach this directive.
2158
+ effect(onCleanup => {
2159
+ const table = this._table();
2160
+ const row = this._row;
2161
+ if (!table || !row)
2162
+ return;
2163
+ const index = row.ngnTableBodyTr().index;
2164
+ table.registerRowActions(index, this);
2165
+ onCleanup(() => table.unregisterRowActions(index, this));
2166
+ });
2167
+ }
2168
+ ngOnDestroy() {
2169
+ this._menu?.destroy();
2170
+ this._unmountBar();
2171
+ }
2172
+ /**
2173
+ * Opens the context menu anchored to the row element (keyboard trigger:
2174
+ * Enter, ContextMenu, or Shift+F10). Deliberately independent of
2175
+ * {@link context} — that input only toggles the right-click affordance,
2176
+ * not the keyboard one, so keyboard users can always reach a row's menu.
2177
+ */
2178
+ openMenuFromKeyboard() {
2179
+ this._menu = openMenuAt(this._vcr, this._menu, this.actions(), this._element.nativeElement, 'bottom-start');
2180
+ return true;
2181
+ }
2182
+ /** Focuses the first enabled action button in the inline bar. Returns whether one was focused. */
2183
+ focusFirstAction() {
2184
+ return this._bar?.instance.focusFirst() ?? false;
2185
+ }
2186
+ /** Moves focus by `delta` among the inline bar's action buttons. Returns whether focus stayed within the bar. */
2187
+ moveAction(delta) {
2188
+ return this._bar?.instance.move(delta) ?? false;
2189
+ }
2190
+ _onContextMenu(event) {
2191
+ // A `contextmenu` event is by definition a context-menu request (right-click
2192
+ // or the keyboard menu key), so no mouse-button check is needed — and
2193
+ // `event.button` is not reliably `2` across browsers (e.g. WebKit/Firefox),
2194
+ // which would otherwise suppress the menu there.
2195
+ if (!this.context())
2196
+ return;
2197
+ if (this._menu?.instance.open())
2198
+ return;
2199
+ event.preventDefault();
2200
+ event.stopPropagation();
2201
+ this._menu = openMenuAt(this._vcr, this._menu, this.actions(), {
2202
+ x: event.clientX,
2203
+ y: event.clientY,
2204
+ });
2205
+ }
2206
+ _mountBar() {
2207
+ if (this._bar)
2208
+ return;
2209
+ this._bar = this._vcr.createComponent(NgnTableRowActionsBar);
2210
+ // Move the bar's host element inside the <tr> so it joins the row's
2211
+ // subgrid and the table's horizontal scroll container. Use Renderer2 so the
2212
+ // move stays platform-agnostic (SSR / non-DOM renderers).
2213
+ this._renderer.appendChild(this._element.nativeElement, this._bar.location.nativeElement);
2214
+ // Pass the actions SIGNAL (not its current value) once. The bar reads it
2215
+ // reactively in its own change detection, so we never re-push the array
2216
+ // here. Reading `this.actions()` inside this effect-driven method would
2217
+ // make the effect re-run on every actions change — and when a consumer
2218
+ // binds a fresh array each change-detection pass, re-pushing it would loop
2219
+ // into infinite change detection (NG0103).
2220
+ setComponentInput(this._bar, 'actionsSource', this.actions);
2221
+ }
2222
+ _unmountBar() {
2223
+ this._bar?.destroy();
2224
+ this._bar = undefined;
2225
+ }
2226
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnTableRowActions, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2227
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: NgnTableRowActions, isStandalone: true, selector: "[ngnTableRowActions]", inputs: { actions: { classPropertyName: "actions", publicName: "ngnTableRowActions", isSignal: true, isRequired: true, transformFunction: null }, context: { classPropertyName: "context", publicName: "ngnTableRowActionsContext", isSignal: true, isRequired: false, transformFunction: null }, inline: { classPropertyName: "inline", publicName: "ngnTableRowActionsInline", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
2228
+ }
2229
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnTableRowActions, decorators: [{
2230
+ type: Directive,
2231
+ args: [{ selector: '[ngnTableRowActions]' }]
2232
+ }], ctorParameters: () => [], propDecorators: { actions: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngnTableRowActions", required: true }] }], context: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngnTableRowActionsContext", required: false }] }], inline: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngnTableRowActionsInline", required: false }] }] } });
2233
+
2234
+ /**
2235
+ * Directive that turns a `<th>` or `<td>` into a selection checkbox column.
2236
+ *
2237
+ * **Header usage** — renders a "select all / none" checkbox:
2238
+ * ```html
2239
+ * <th ngnTableSelectionColumn></th>
2240
+ * ```
2241
+ *
2242
+ * **Body usage** — renders a per-row checkbox (must be inside a `[ngnTableBodyTr]`):
2243
+ * ```html
2244
+ * <td ngnTableSelectionColumn></td>
2245
+ * ```
2246
+ *
2247
+ * @category directive
2248
+ */
2249
+ class NgnTableSelectionColumn {
2250
+ theme = injectThemeTemplate(tableControlTemplate);
2251
+ _element = inject(ElementRef);
2252
+ _envInjector = inject(EnvironmentInjector);
2253
+ _appRef = inject(ApplicationRef);
2254
+ _i18n = inject(I18n).translations;
2255
+ _table = getNearestNgnInstanceSig(this._element.nativeElement, NgnTable);
2256
+ /** Non-null only when used inside a body row (`<td>`). */
2257
+ _bodyTr = inject(NgnTableBodyTr, { optional: true });
2258
+ _isHeader = this._element.nativeElement.tagName === 'TH';
2259
+ /**
2260
+ * NgnCheckbox created at the application level via `createComponent` + `EnvironmentInjector`.
2261
+ * This keeps it outside Angular's view tree so `@for` reconciliation cannot destroy it.
2262
+ */
2263
+ _checkboxRef;
2264
+ selected = computed(() => {
2265
+ if (this._isHeader)
2266
+ return false;
2267
+ const table = this._table();
2268
+ const row = this._bodyTr?.ngnTableBodyTr();
2269
+ if (!table || !row)
2270
+ return false;
2271
+ return table.isRowSelected(row.id);
2272
+ }, /* @ts-ignore */
2273
+ ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
2274
+ focused = computed(() => {
2275
+ if (this._isHeader)
2276
+ return false;
2277
+ const table = this._table();
2278
+ const row = this._bodyTr?.ngnTableBodyTr();
2279
+ if (!table || !row)
2280
+ return false;
2281
+ return table.focusedRowIndex() === row.index;
2282
+ }, /* @ts-ignore */
2283
+ ...(ngDevMode ? [{ debugName: "focused" }] : /* istanbul ignore next */ []));
2284
+ constructor() {
2285
+ this._prepareDom();
2286
+ this._createCheckbox();
2287
+ if (this._isHeader) {
2288
+ this._setupHeaderEffects();
2289
+ // Name the select-all checkbox, kept in sync with the active language
2290
+ // (translations load asynchronously, so this cannot be set once at init).
2291
+ effect(() => {
2292
+ setComponentInput(this._checkboxRef, 'label', this._i18n['table_selectAllRows']());
2293
+ });
2294
+ // Register the selection column once the table signal resolves
2295
+ effect(() => {
2296
+ const table = this._table();
2297
+ if (table) {
2298
+ table.registerSelectionColumn();
2299
+ }
2300
+ });
2301
+ }
2302
+ else {
2303
+ this._setupBodyEffects();
2304
+ // Mirror the header: name the per-row checkbox, kept in sync with language.
2305
+ effect(() => {
2306
+ setComponentInput(this._checkboxRef, 'label', this._i18n['table_selectRow']());
2307
+ });
2308
+ }
2309
+ }
2310
+ ngOnDestroy() {
2311
+ if (this._isHeader) {
2312
+ this._table()?.unregisterSelectionColumn();
2313
+ }
2314
+ this._appRef.detachView(this._checkboxRef.hostView);
2315
+ this._checkboxRef.destroy();
2316
+ }
2317
+ _prepareDom() {
2318
+ const el = this._element.nativeElement;
2319
+ // Mirror the grid roles that ngnTableTh/ngnTableTd set, so the selection column
2320
+ // stays inside the table's `role="grid"` semantics instead of being an orphan cell.
2321
+ el.setAttribute('role', this._isHeader ? 'columnheader' : 'gridcell');
2322
+ toggleClass(el, this.theme.class('cell'), true);
2323
+ toggleClass(el, this.theme.class('selection-column'), true);
2324
+ }
2325
+ /**
2326
+ * Creates an NgnCheckbox via `createComponent` + `EnvironmentInjector`.
2327
+ * The component is attached to `ApplicationRef` (not a ViewContainerRef),
2328
+ * so Angular's `@for` block reconciliation cannot destroy it.
2329
+ */
2330
+ _createCheckbox() {
2331
+ this._checkboxRef = createComponent(NgnCheckbox, {
2332
+ environmentInjector: this._envInjector,
2333
+ });
2334
+ this._appRef.attachView(this._checkboxRef.hostView);
2335
+ const cbEl = this._checkboxRef.location.nativeElement;
2336
+ cbEl.classList.add(this.theme.class('selection-checkbox'));
2337
+ if (this._isHeader) {
2338
+ setComponentInput(this._checkboxRef, 'allowIndeterminate', true);
2339
+ }
2340
+ this._element.nativeElement.appendChild(cbEl);
2341
+ }
2342
+ // --- Header (select-all) ---
2343
+ _setupHeaderEffects() {
2344
+ effect(() => {
2345
+ const table = this._table();
2346
+ if (!table)
2347
+ return;
2348
+ setComponentInput(this._checkboxRef, 'value', table.headerCheckboxValue());
2349
+ });
2350
+ }
2351
+ // --- Body (per-row) ---
2352
+ _setupBodyEffects() {
2353
+ effect(() => {
2354
+ const isSelected = this.selected();
2355
+ const isFocused = this.focused();
2356
+ const row = this._bodyTr?.ngnTableBodyTr();
2357
+ const el = this._element.nativeElement;
2358
+ setComponentInput(this._checkboxRef, 'value', isSelected);
2359
+ if (row) {
2360
+ el.style.setProperty('--ngn-table-row-index', String(row.index + 2));
2361
+ }
2362
+ toggleClass(el, this.theme.class('selected-row-cell'), isSelected);
2363
+ toggleClass(el, this.theme.class('focused-row-cell'), isFocused);
2364
+ });
2365
+ }
2366
+ // --- Click handling ---
2367
+ onClick(event) {
2368
+ event.stopPropagation();
2369
+ const table = this._table();
2370
+ if (!table)
2371
+ return;
2372
+ if (this._isHeader) {
2373
+ table.toggleSelectAll();
2374
+ }
2375
+ else {
2376
+ const row = this._bodyTr?.ngnTableBodyTr();
2377
+ if (row) {
2378
+ table.handleCheckboxChange(row);
2379
+ }
2380
+ }
2381
+ }
2382
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnTableSelectionColumn, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2383
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.5", type: NgnTableSelectionColumn, isStandalone: true, selector: "[ngnTableSelectionColumn]", host: { listeners: { "click": "onClick($event)" } }, ngImport: i0 });
2384
+ }
2385
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnTableSelectionColumn, decorators: [{
2386
+ type: Directive,
2387
+ args: [{
2388
+ selector: '[ngnTableSelectionColumn]',
2389
+ host: {
2390
+ '(click)': 'onClick($event)',
2391
+ },
2392
+ }]
2393
+ }], ctorParameters: () => [] });
2394
+
2395
+ class NgnTableStickyColumn {
2396
+ _element = inject(ElementRef);
2397
+ _headerCell = inject(NgnTableTh);
2398
+ _columnId = this._headerCell.ngnTableTh;
2399
+ /** Which edge the column sticks to: `'start'` (left) or `'end'` (right). */
2400
+ ngnTableStickyColumn = input.required(/* @ts-ignore */
2401
+ ...(ngDevMode ? [{ debugName: "ngnTableStickyColumn" }] : /* istanbul ignore next */ []));
2402
+ _table = getNearestNgnInstanceSig(this._element.nativeElement, NgnTable);
2403
+ constructor() {
2404
+ afterNextRender(() => {
2405
+ const table = this._table();
2406
+ if (table) {
2407
+ table.registerStickyColumn(this._columnId(), this.ngnTableStickyColumn());
2408
+ }
2409
+ });
2410
+ }
2411
+ ngOnDestroy() {
2412
+ this._table()?.unregisterStickyColumn(this._columnId());
2413
+ }
2414
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnTableStickyColumn, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2415
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: NgnTableStickyColumn, isStandalone: true, selector: "[ngnTableStickyColumn]", inputs: { ngnTableStickyColumn: { classPropertyName: "ngnTableStickyColumn", publicName: "ngnTableStickyColumn", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
2416
+ }
2417
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnTableStickyColumn, decorators: [{
2418
+ type: Directive,
2419
+ args: [{
2420
+ selector: '[ngnTableStickyColumn]',
2421
+ }]
2422
+ }], ctorParameters: () => [], propDecorators: { ngnTableStickyColumn: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngnTableStickyColumn", required: true }] }] } });
2423
+
2424
+ class NgnTableModule {
2425
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnTableModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
2426
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "22.0.5", ngImport: i0, type: NgnTableModule, imports: [NgnTable,
2427
+ NgnTableBodyTr,
2428
+ NgnTableTd,
2429
+ NgnTableTh,
2430
+ NgnTableHeadTr,
2431
+ NgnTableGroupHeaderTr,
2432
+ NgnTableReorderableColumn,
2433
+ NgnTableRowActions,
2434
+ NgnTableRowActionsBar,
2435
+ NgnTableSelectionColumn,
2436
+ NgnTableStickyColumn], exports: [NgnTable,
2437
+ NgnTableBodyTr,
2438
+ NgnTableTd,
2439
+ NgnTableTh,
2440
+ NgnTableHeadTr,
2441
+ NgnTableGroupHeaderTr,
2442
+ NgnTableReorderableColumn,
2443
+ NgnTableRowActions,
2444
+ NgnTableRowActionsBar,
2445
+ NgnTableSelectionColumn,
2446
+ NgnTableStickyColumn] });
2447
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnTableModule, imports: [NgnTable,
2448
+ NgnTableRowActionsBar] });
2449
+ }
2450
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnTableModule, decorators: [{
2451
+ type: NgModule,
2452
+ args: [{
2453
+ imports: [
2454
+ NgnTable,
2455
+ NgnTableBodyTr,
2456
+ NgnTableTd,
2457
+ NgnTableTh,
2458
+ NgnTableHeadTr,
2459
+ NgnTableGroupHeaderTr,
2460
+ NgnTableReorderableColumn,
2461
+ NgnTableRowActions,
2462
+ NgnTableRowActionsBar,
2463
+ NgnTableSelectionColumn,
2464
+ NgnTableStickyColumn,
2465
+ ],
2466
+ exports: [
2467
+ NgnTable,
2468
+ NgnTableBodyTr,
2469
+ NgnTableTd,
2470
+ NgnTableTh,
2471
+ NgnTableHeadTr,
2472
+ NgnTableGroupHeaderTr,
2473
+ NgnTableReorderableColumn,
2474
+ NgnTableRowActions,
2475
+ NgnTableRowActionsBar,
2476
+ NgnTableSelectionColumn,
2477
+ NgnTableStickyColumn,
2478
+ ],
2479
+ }]
2480
+ }] });
2481
+
2482
+ /**
2483
+ * @category directive
2484
+ */
2485
+ class NgnTableFilterableColumn {
2486
+ theme = injectThemeTemplate(tableControlTemplate);
2487
+ _element = inject(ElementRef);
2488
+ _columnId = inject(NgnTableTh).ngnTableTh;
2489
+ /** Enables filtering on this column. The directive selector; its value is unused. */
2490
+ ngnTableFilterableColumn = input(/* @ts-ignore */
2491
+ ...(ngDevMode ? [undefined, { debugName: "ngnTableFilterableColumn" }] : /* istanbul ignore next */ []));
2492
+ _ngnActionButton;
2493
+ _ngnFilter;
2494
+ _table = getNearestNgnInstanceSig(this._element.nativeElement, NgnTable);
2495
+ _rows = computed(() => {
2496
+ return this._table()?.rows() || [];
2497
+ }, /* @ts-ignore */
2498
+ ...(ngDevMode ? [{ debugName: "_rows" }] : /* istanbul ignore next */ []));
2499
+ filter = computed(() => {
2500
+ const tableFilter = this._table()?.filters()?.[this._columnId()];
2501
+ return tableFilter;
2502
+ }, /* @ts-ignore */
2503
+ ...(ngDevMode ? [{ debugName: "filter" }] : /* istanbul ignore next */ []));
2504
+ /** The data type of the column, which determines the available filter operators and UI. */
2505
+ ngnTableFilterableColumnType = input.required(/* @ts-ignore */
2506
+ ...(ngDevMode ? [{ debugName: "ngnTableFilterableColumnType" }] : /* istanbul ignore next */ []));
2507
+ /** For list-based filters, the set of selectable option values to offer. */
2508
+ ngnTableFilterableColumnItems = input(/* @ts-ignore */
2509
+ ...(ngDevMode ? [undefined, { debugName: "ngnTableFilterableColumnItems" }] : /* istanbul ignore next */ []));
2510
+ constructor() {
2511
+ this._ngnActionButton = inject(ViewContainerRef).createComponent((NgnActionButton));
2512
+ this._ngnFilter = inject(ViewContainerRef).createComponent(NgnFilter);
2513
+ this._element.nativeElement.appendChild(this._ngnActionButton.location.nativeElement);
2514
+ this._element.nativeElement.appendChild(this._ngnFilter.location.nativeElement);
2515
+ this._ngnActionButton.location.nativeElement.classList.add(this.theme.class('filter-control'));
2516
+ setComponentInput(this._ngnActionButton, 'kind', 'icon');
2517
+ setComponentInput(this._ngnActionButton, 'inline', true);
2518
+ setComponentInput(this._ngnFilter, 'anchor', this._ngnActionButton.location.nativeElement);
2519
+ setComponentInput(this._ngnFilter, 'mode', 'headless');
2520
+ setComponentInput(this._ngnFilter, 'allowMultiple', true);
2521
+ const cfg = computed(() => ({
2522
+ label: 'Filter',
2523
+ value: null,
2524
+ kind: 'icon',
2525
+ defaultIcon: this.filter() ? 'filter-active' : 'filter-inactive',
2526
+ action: event => {
2527
+ event.stopPropagation();
2528
+ this._ngnFilter.instance.show();
2529
+ },
2530
+ }), /* @ts-ignore */
2531
+ ...(ngDevMode ? [{ debugName: "cfg" }] : /* istanbul ignore next */ []));
2532
+ effect(() => {
2533
+ setComponentInput(this._ngnActionButton, 'config', cfg());
2534
+ });
2535
+ effect(() => {
2536
+ setComponentInput(this._ngnFilter, 'data', this._rows().map(row => row[this._columnId()]));
2537
+ });
2538
+ effect(() => {
2539
+ setComponentInput(this._ngnFilter, 'dataType', this.ngnTableFilterableColumnType());
2540
+ });
2541
+ effect(() => {
2542
+ setComponentInput(this._ngnFilter, 'listOptions', this.ngnTableFilterableColumnItems());
2543
+ });
2544
+ this._ngnFilter.instance.filterChange.subscribe(cfg => {
2545
+ const table = this._table();
2546
+ const currentFilters = table?.filters() || {};
2547
+ table?.filters.set({ ...currentFilters, [this._columnId()]: cfg ?? undefined });
2548
+ });
2549
+ }
2550
+ ngOnDestroy() {
2551
+ this._ngnActionButton.destroy();
2552
+ this._ngnFilter.destroy();
2553
+ }
2554
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnTableFilterableColumn, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2555
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: NgnTableFilterableColumn, isStandalone: true, selector: "[ngnTableFilterableColumn]", inputs: { ngnTableFilterableColumn: { classPropertyName: "ngnTableFilterableColumn", publicName: "ngnTableFilterableColumn", isSignal: true, isRequired: false, transformFunction: null }, ngnTableFilterableColumnType: { classPropertyName: "ngnTableFilterableColumnType", publicName: "ngnTableFilterableColumnType", isSignal: true, isRequired: true, transformFunction: null }, ngnTableFilterableColumnItems: { classPropertyName: "ngnTableFilterableColumnItems", publicName: "ngnTableFilterableColumnItems", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "theme.classes({'filterable-column': true, 'filtered-column': !!filter() })" } }, ngImport: i0 });
2556
+ }
2557
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnTableFilterableColumn, decorators: [{
2558
+ type: Directive,
2559
+ args: [{
2560
+ selector: '[ngnTableFilterableColumn]',
2561
+ host: {
2562
+ '[class]': `theme.classes({'filterable-column': true, 'filtered-column': !!filter() })`,
2563
+ },
2564
+ }]
2565
+ }], ctorParameters: () => [], propDecorators: { ngnTableFilterableColumn: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngnTableFilterableColumn", required: false }] }], ngnTableFilterableColumnType: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngnTableFilterableColumnType", required: true }] }], ngnTableFilterableColumnItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngnTableFilterableColumnItems", required: false }] }] } });
2566
+
2567
+ /**
2568
+ * @category directive
2569
+ */
2570
+ class NgnTableSortableColumn {
2571
+ theme = injectThemeTemplate(tableControlTemplate);
2572
+ _element = inject(ElementRef);
2573
+ /** Enables sorting on this column. The directive selector; its value is unused. */
2574
+ ngnTableSortableColumn = input(/* @ts-ignore */
2575
+ ...(ngDevMode ? [undefined, { debugName: "ngnTableSortableColumn" }] : /* istanbul ignore next */ []));
2576
+ _table = getNearestNgnInstanceSig(this._element.nativeElement, NgnTable);
2577
+ sort = computed(() => {
2578
+ const tableSort = this._table()?.sort();
2579
+ if (tableSort?.column === this.columnId()) {
2580
+ return tableSort.direction;
2581
+ }
2582
+ return null;
2583
+ }, /* @ts-ignore */
2584
+ ...(ngDevMode ? [{ debugName: "sort" }] : /* istanbul ignore next */ []));
2585
+ columnId = inject(NgnTableTh).ngnTableTh;
2586
+ _ngnIcon;
2587
+ constructor() {
2588
+ this._ngnIcon = inject(ViewContainerRef).createComponent(NgnIcon);
2589
+ this._element.nativeElement.appendChild(this._ngnIcon.location.nativeElement);
2590
+ this._ngnIcon.location.nativeElement.classList.add(this.theme.class('sort-control'));
2591
+ effect(() => {
2592
+ const sort = this.sort();
2593
+ setComponentInput(this._ngnIcon, 'defaultIcon', sort === 'asc' ? 'sort-ascending' : sort === 'desc' ? 'sort-descending' : 'sort-neutral');
2594
+ });
2595
+ }
2596
+ ngOnDestroy() {
2597
+ this._ngnIcon.destroy();
2598
+ }
2599
+ onHostClick(event) {
2600
+ const target = event.target;
2601
+ if (target.classList.contains(this.theme.class('resize-handle')))
2602
+ return;
2603
+ this.toggleSort();
2604
+ }
2605
+ toggleSort() {
2606
+ const currentSort = this.sort();
2607
+ const table = this._table();
2608
+ table?.sort.set(currentSort === null
2609
+ ? { column: this.columnId(), direction: 'asc' }
2610
+ : currentSort === 'asc'
2611
+ ? { column: this.columnId(), direction: 'desc' }
2612
+ : null);
2613
+ }
2614
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnTableSortableColumn, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2615
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: NgnTableSortableColumn, isStandalone: true, selector: "[ngnTableSortableColumn]", inputs: { ngnTableSortableColumn: { classPropertyName: "ngnTableSortableColumn", publicName: "ngnTableSortableColumn", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "click": "onHostClick($event)" }, properties: { "class": "theme.classes({'sortable-column': true, 'sorted-column': !!sort() })", "attr.aria-sort": "sort() === 'asc' ? 'ascending' : sort() === 'desc' ? 'descending' : 'none'" } }, ngImport: i0 });
2616
+ }
2617
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnTableSortableColumn, decorators: [{
2618
+ type: Directive,
2619
+ args: [{
2620
+ selector: '[ngnTableSortableColumn]',
2621
+ host: {
2622
+ '[class]': `theme.classes({'sortable-column': true, 'sorted-column': !!sort() })`,
2623
+ '[attr.aria-sort]': `sort() === 'asc' ? 'ascending' : sort() === 'desc' ? 'descending' : 'none'`,
2624
+ '(click)': `onHostClick($event)`,
2625
+ },
2626
+ }]
2627
+ }], ctorParameters: () => [], propDecorators: { ngnTableSortableColumn: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngnTableSortableColumn", required: false }] }] } });
2628
+
2629
+ /**
2630
+ * Generated bundle index. Do not edit.
2631
+ */
2632
+
2633
+ export { NgnTable, NgnTableBodyTr, NgnTableFilterableColumn, NgnTableGroupHeaderTr, NgnTableHeadTr, NgnTableModule, NgnTableReorderableColumn, NgnTableRowActions, NgnTableRowActionsBar, NgnTableSelectionColumn, NgnTableSortableColumn, NgnTableStickyColumn, NgnTableTd, NgnTableTemplates, NgnTableTh, defaultSortComparator, filterRows, groupRows, paginateRows, sortRows };
2634
+ //# sourceMappingURL=ngneers-controls-table.mjs.map