@ngneers/controls 0.0.1-pre5 → 0.0.1-pre7

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 (168) hide show
  1. package/fesm2022/ngneers-controls-accordion.mjs +33 -33
  2. package/fesm2022/ngneers-controls-accordion.mjs.map +1 -1
  3. package/fesm2022/ngneers-controls-api-ng.mjs +22 -19
  4. package/fesm2022/ngneers-controls-api-ng.mjs.map +1 -1
  5. package/fesm2022/ngneers-controls-api-resize.mjs +706 -0
  6. package/fesm2022/ngneers-controls-api-resize.mjs.map +1 -0
  7. package/fesm2022/ngneers-controls-avatar.mjs +13 -13
  8. package/fesm2022/ngneers-controls-avatar.mjs.map +1 -1
  9. package/fesm2022/ngneers-controls-base.mjs +89 -64
  10. package/fesm2022/ngneers-controls-base.mjs.map +1 -1
  11. package/fesm2022/ngneers-controls-breadcrumb.mjs +19 -19
  12. package/fesm2022/ngneers-controls-breadcrumb.mjs.map +1 -1
  13. package/fesm2022/ngneers-controls-button-group.mjs +8 -8
  14. package/fesm2022/ngneers-controls-button-group.mjs.map +1 -1
  15. package/fesm2022/ngneers-controls-button.mjs +15 -10
  16. package/fesm2022/ngneers-controls-button.mjs.map +1 -1
  17. package/fesm2022/{ngneers-controls-calendar-time-BEeJIqIX.mjs → ngneers-controls-calendar-time-CKAqRgOP.mjs} +9 -9
  18. package/fesm2022/{ngneers-controls-calendar-time-BEeJIqIX.mjs.map → ngneers-controls-calendar-time-CKAqRgOP.mjs.map} +1 -1
  19. package/fesm2022/ngneers-controls-calendar.mjs +56 -56
  20. package/fesm2022/ngneers-controls-calendar.mjs.map +1 -1
  21. package/fesm2022/ngneers-controls-checkbox.mjs +9 -9
  22. package/fesm2022/ngneers-controls-checkbox.mjs.map +1 -1
  23. package/fesm2022/ngneers-controls-chip.mjs +7 -7
  24. package/fesm2022/ngneers-controls-chip.mjs.map +1 -1
  25. package/fesm2022/ngneers-controls-default-icons.mjs +61 -0
  26. package/fesm2022/ngneers-controls-default-icons.mjs.map +1 -0
  27. package/fesm2022/ngneers-controls-defer.mjs +10 -10
  28. package/fesm2022/ngneers-controls-defer.mjs.map +1 -1
  29. package/fesm2022/ngneers-controls-dialog.mjs +38 -40
  30. package/fesm2022/ngneers-controls-dialog.mjs.map +1 -1
  31. package/fesm2022/ngneers-controls-directives.mjs +45 -39
  32. package/fesm2022/ngneers-controls-directives.mjs.map +1 -1
  33. package/fesm2022/ngneers-controls-drawer.mjs +29 -29
  34. package/fesm2022/ngneers-controls-drawer.mjs.map +1 -1
  35. package/fesm2022/ngneers-controls-edit-inplace.mjs +16 -16
  36. package/fesm2022/ngneers-controls-edit-inplace.mjs.map +1 -1
  37. package/fesm2022/ngneers-controls-filter.mjs +192 -44
  38. package/fesm2022/ngneers-controls-filter.mjs.map +1 -1
  39. package/fesm2022/{ngneers-controls-i18n-de-CdTPeBiR.mjs → ngneers-controls-i18n-de-CF0xTWEX.mjs} +9 -5
  40. package/fesm2022/ngneers-controls-i18n-de-CF0xTWEX.mjs.map +1 -0
  41. package/fesm2022/{ngneers-controls-i18n-en-Cfp2pOLY.mjs → ngneers-controls-i18n-en-8B7xAjH2.mjs} +9 -5
  42. package/fesm2022/ngneers-controls-i18n-en-8B7xAjH2.mjs.map +1 -0
  43. package/fesm2022/ngneers-controls-i18n.mjs +5 -5
  44. package/fesm2022/ngneers-controls-i18n.mjs.map +1 -1
  45. package/fesm2022/ngneers-controls-icon.mjs +111 -47
  46. package/fesm2022/ngneers-controls-icon.mjs.map +1 -1
  47. package/fesm2022/ngneers-controls-inplace.mjs +18 -18
  48. package/fesm2022/ngneers-controls-inplace.mjs.map +1 -1
  49. package/fesm2022/ngneers-controls-input-field.mjs +16 -16
  50. package/fesm2022/ngneers-controls-input-field.mjs.map +1 -1
  51. package/fesm2022/ngneers-controls-input-mask.mjs +8 -8
  52. package/fesm2022/ngneers-controls-input-mask.mjs.map +1 -1
  53. package/fesm2022/ngneers-controls-input.mjs +6 -6
  54. package/fesm2022/ngneers-controls-input.mjs.map +1 -1
  55. package/fesm2022/ngneers-controls-item-view.mjs +33 -33
  56. package/fesm2022/ngneers-controls-item-view.mjs.map +1 -1
  57. package/fesm2022/ngneers-controls-list-box.mjs +31 -31
  58. package/fesm2022/ngneers-controls-list-box.mjs.map +1 -1
  59. package/fesm2022/ngneers-controls-menu.mjs +33 -33
  60. package/fesm2022/ngneers-controls-menu.mjs.map +1 -1
  61. package/fesm2022/ngneers-controls-message.mjs +4 -4
  62. package/fesm2022/ngneers-controls-message.mjs.map +1 -1
  63. package/fesm2022/ngneers-controls-paginator.mjs +13 -13
  64. package/fesm2022/ngneers-controls-paginator.mjs.map +1 -1
  65. package/fesm2022/ngneers-controls-popover.mjs +21 -16
  66. package/fesm2022/ngneers-controls-popover.mjs.map +1 -1
  67. package/fesm2022/ngneers-controls-progress.mjs +16 -16
  68. package/fesm2022/ngneers-controls-progress.mjs.map +1 -1
  69. package/fesm2022/ngneers-controls-scroll-shadow.mjs +54 -0
  70. package/fesm2022/ngneers-controls-scroll-shadow.mjs.map +1 -0
  71. package/fesm2022/ngneers-controls-scroller.mjs +74 -39
  72. package/fesm2022/ngneers-controls-scroller.mjs.map +1 -1
  73. package/fesm2022/ngneers-controls-select-button.mjs +7 -7
  74. package/fesm2022/ngneers-controls-select-button.mjs.map +1 -1
  75. package/fesm2022/ngneers-controls-select.mjs +48 -47
  76. package/fesm2022/ngneers-controls-select.mjs.map +1 -1
  77. package/fesm2022/ngneers-controls-slider.mjs +11 -11
  78. package/fesm2022/ngneers-controls-slider.mjs.map +1 -1
  79. package/fesm2022/ngneers-controls-spinner.mjs +7 -7
  80. package/fesm2022/ngneers-controls-spinner.mjs.map +1 -1
  81. package/fesm2022/ngneers-controls-splitter.mjs +141 -328
  82. package/fesm2022/ngneers-controls-splitter.mjs.map +1 -1
  83. package/fesm2022/ngneers-controls-switch.mjs +3 -3
  84. package/fesm2022/ngneers-controls-switch.mjs.map +1 -1
  85. package/fesm2022/ngneers-controls-table.mjs +1456 -82
  86. package/fesm2022/ngneers-controls-table.mjs.map +1 -1
  87. package/fesm2022/ngneers-controls-tabs.mjs +41 -40
  88. package/fesm2022/ngneers-controls-tabs.mjs.map +1 -1
  89. package/fesm2022/ngneers-controls-tag.mjs +4 -4
  90. package/fesm2022/ngneers-controls-tag.mjs.map +1 -1
  91. package/fesm2022/ngneers-controls-toast.mjs +19 -19
  92. package/fesm2022/ngneers-controls-toast.mjs.map +1 -1
  93. package/fesm2022/ngneers-controls-toggle-button.mjs +10 -10
  94. package/fesm2022/ngneers-controls-toggle-button.mjs.map +1 -1
  95. package/fesm2022/ngneers-controls-tooltip.mjs +74 -38
  96. package/fesm2022/ngneers-controls-tooltip.mjs.map +1 -1
  97. package/fesm2022/ngneers-controls-utils-ng.mjs +9 -9
  98. package/fesm2022/ngneers-controls-utils-ng.mjs.map +1 -1
  99. package/fesm2022/ngneers-controls-utils.mjs +1 -1
  100. package/fesm2022/ngneers-controls-utils.mjs.map +1 -1
  101. package/package.json +34 -12
  102. package/types/ngneers-controls-accordion.d.ts +6 -4
  103. package/types/ngneers-controls-api-ng.d.ts +7 -2
  104. package/types/ngneers-controls-api-resize.d.ts +318 -0
  105. package/types/ngneers-controls-api.d.ts +2 -2
  106. package/types/ngneers-controls-base.d.ts +8 -0
  107. package/types/ngneers-controls-breadcrumb.d.ts +6 -4
  108. package/types/ngneers-controls-button.d.ts +13 -8
  109. package/types/ngneers-controls-calendar.d.ts +5 -1
  110. package/types/ngneers-controls-checkbox.d.ts +6 -4
  111. package/types/ngneers-controls-chip.d.ts +3 -1
  112. package/types/ngneers-controls-default-icons.d.ts +5 -0
  113. package/types/ngneers-controls-directives.d.ts +7 -1
  114. package/types/ngneers-controls-drawer.d.ts +7 -1
  115. package/types/ngneers-controls-edit-inplace.d.ts +4 -0
  116. package/types/ngneers-controls-filter.d.ts +52 -3
  117. package/types/ngneers-controls-i18n.d.ts +4 -0
  118. package/types/ngneers-controls-icon.d.ts +23 -40
  119. package/types/ngneers-controls-input-field.d.ts +7 -1
  120. package/types/ngneers-controls-item-view.d.ts +4 -2
  121. package/types/ngneers-controls-list-box.d.ts +5 -1
  122. package/types/ngneers-controls-menu.d.ts +4 -2
  123. package/types/ngneers-controls-message.d.ts +3 -1
  124. package/types/ngneers-controls-paginator.d.ts +4 -0
  125. package/types/ngneers-controls-popover.d.ts +2 -1
  126. package/types/ngneers-controls-scroll-shadow.d.ts +16 -0
  127. package/types/ngneers-controls-scroller.d.ts +12 -2
  128. package/types/ngneers-controls-select.d.ts +11 -12
  129. package/types/ngneers-controls-spinner.d.ts +1 -1
  130. package/types/ngneers-controls-splitter.d.ts +29 -59
  131. package/types/ngneers-controls-table.d.ts +380 -24
  132. package/types/ngneers-controls-tabs.d.ts +4 -2
  133. package/types/ngneers-controls-tag.d.ts +3 -1
  134. package/types/ngneers-controls-toast.d.ts +5 -3
  135. package/types/ngneers-controls-toggle-button.d.ts +5 -3
  136. package/types/ngneers-controls-tooltip.d.ts +7 -1
  137. package/fesm2022/ngneers-controls-i18n-de-CdTPeBiR.mjs.map +0 -1
  138. package/fesm2022/ngneers-controls-i18n-en-Cfp2pOLY.mjs.map +0 -1
  139. package/fesm2022/ngneers-controls-icon-calendar-z5xA9wTT.mjs +0 -4
  140. package/fesm2022/ngneers-controls-icon-calendar-z5xA9wTT.mjs.map +0 -1
  141. package/fesm2022/ngneers-controls-icon-check-DdAQ9k-T.mjs +0 -4
  142. package/fesm2022/ngneers-controls-icon-check-DdAQ9k-T.mjs.map +0 -1
  143. package/fesm2022/ngneers-controls-icon-chevron_down-HO-z-xOr.mjs +0 -4
  144. package/fesm2022/ngneers-controls-icon-chevron_down-HO-z-xOr.mjs.map +0 -1
  145. package/fesm2022/ngneers-controls-icon-chevron_left-CC5psi16.mjs +0 -4
  146. package/fesm2022/ngneers-controls-icon-chevron_left-CC5psi16.mjs.map +0 -1
  147. package/fesm2022/ngneers-controls-icon-chevron_right-BRxlzx4G.mjs +0 -4
  148. package/fesm2022/ngneers-controls-icon-chevron_right-BRxlzx4G.mjs.map +0 -1
  149. package/fesm2022/ngneers-controls-icon-chevron_up-cqhPO7g2.mjs +0 -4
  150. package/fesm2022/ngneers-controls-icon-chevron_up-cqhPO7g2.mjs.map +0 -1
  151. package/fesm2022/ngneers-controls-icon-ellipsis-DXIGA1sk.mjs +0 -4
  152. package/fesm2022/ngneers-controls-icon-ellipsis-DXIGA1sk.mjs.map +0 -1
  153. package/fesm2022/ngneers-controls-icon-filter-oU7E6Xgr.mjs +0 -4
  154. package/fesm2022/ngneers-controls-icon-filter-oU7E6Xgr.mjs.map +0 -1
  155. package/fesm2022/ngneers-controls-icon-filter_solid-CqIKuR4N.mjs +0 -4
  156. package/fesm2022/ngneers-controls-icon-filter_solid-CqIKuR4N.mjs.map +0 -1
  157. package/fesm2022/ngneers-controls-icon-magnifying_glass-T4Hzbkig.mjs +0 -4
  158. package/fesm2022/ngneers-controls-icon-magnifying_glass-T4Hzbkig.mjs.map +0 -1
  159. package/fesm2022/ngneers-controls-icon-minus-oGDEFO4E.mjs +0 -4
  160. package/fesm2022/ngneers-controls-icon-minus-oGDEFO4E.mjs.map +0 -1
  161. package/fesm2022/ngneers-controls-icon-sort-CRD4tafz.mjs +0 -4
  162. package/fesm2022/ngneers-controls-icon-sort-CRD4tafz.mjs.map +0 -1
  163. package/fesm2022/ngneers-controls-icon-sort_down-DiFB1P6R.mjs +0 -4
  164. package/fesm2022/ngneers-controls-icon-sort_down-DiFB1P6R.mjs.map +0 -1
  165. package/fesm2022/ngneers-controls-icon-sort_up-Bq_q3hIi.mjs +0 -4
  166. package/fesm2022/ngneers-controls-icon-sort_up-Bq_q3hIi.mjs.map +0 -1
  167. package/fesm2022/ngneers-controls-icon-xmark-BZKZORiu.mjs +0 -4
  168. package/fesm2022/ngneers-controls-icon-xmark-BZKZORiu.mjs.map +0 -1
@@ -1,56 +1,215 @@
1
1
  import * as i0 from '@angular/core';
2
- import { contentChild, ChangeDetectionStrategy, Component, signal, input, model, computed, Directive, inject, Renderer2, ElementRef, afterNextRender, effect, NgModule, ViewContainerRef } from '@angular/core';
2
+ import { input, inject, ElementRef, effect, Directive, contentChild, ChangeDetectionStrategy, Component, signal, viewChild, model, booleanAttribute, computed, afterRenderEffect, afterNextRender, Renderer2, EnvironmentInjector, ApplicationRef, createComponent, NgModule, ViewContainerRef } from '@angular/core';
3
3
  import { NgTemplateOutlet } from '@angular/common';
4
4
  import { executeMultiFilter } from '@ngneers/controls/api';
5
- import { templateTypesFn, NgnTemplate, injectThemeTemplate, setComponentInput } from '@ngneers/controls/api/ng';
5
+ import { injectThemeTemplate, templateTypesFn, elementSizeSignal, NgnTemplate, setComponentInput } from '@ngneers/controls/api/ng';
6
+ import { ResizeEngine, getResizeLimitInPx } from '@ngneers/controls/api/resize';
6
7
  import { NgnBase, NgnPt, provideSelf, getNearestNgnInstance, getNearestNgnInstanceSig } from '@ngneers/controls/base';
8
+ import { NgnIcon } from '@ngneers/controls/icon';
7
9
  import { NgnPaginator } from '@ngneers/controls/paginator';
8
- import { NgnScroller, NgnScrollerItem } from '@ngneers/controls/scroller';
10
+ import { NgnScrollShadow } from '@ngneers/controls/scroll-shadow';
11
+ import { NgnScrollerItem, NgnScroller } from '@ngneers/controls/scroller';
9
12
  import { tableControlTemplate } from '@ngneers/controls-themes/templates/table';
10
13
  import { toggleClass, NgnError } from '@ngneers/controls/utils';
11
14
  import { setInputSignalValue } from '@ngneers/controls/utils-ng';
15
+ import { NgnCheckbox } from '@ngneers/controls/checkbox';
12
16
  import { NgnActionButton } from '@ngneers/controls/button';
13
17
  import { NgnFilter } from '@ngneers/controls/filter';
14
- import { NgnIcon } from '@ngneers/controls/icon';
18
+
19
+ class NgnTableGroupHeaderTr extends NgnScrollerItem {
20
+ ngnTableGroupHeaderTr = input.required(...(ngDevMode ? [{ debugName: "ngnTableGroupHeaderTr" }] : /* istanbul ignore next */ []));
21
+ ngnScrollerItem = input({}, ...(ngDevMode ? [{ debugName: "ngnScrollerItem" }] : /* istanbul ignore next */ []));
22
+ _element = inject((ElementRef));
23
+ theme = injectThemeTemplate(tableControlTemplate);
24
+ constructor() {
25
+ super();
26
+ effect(() => {
27
+ const row = this.ngnTableGroupHeaderTr();
28
+ setInputSignalValue(this.ngnScrollerItem, row);
29
+ });
30
+ this.prepareDom();
31
+ }
32
+ prepareDom() {
33
+ toggleClass(this._element.nativeElement, this.theme.class('row'), true);
34
+ toggleClass(this._element.nativeElement, this.theme.class('group-header-row'), true);
35
+ }
36
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: NgnTableGroupHeaderTr, deps: [], target: i0.ɵɵFactoryTarget.Directive });
37
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.10", 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 });
38
+ }
39
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: NgnTableGroupHeaderTr, decorators: [{
40
+ type: Directive,
41
+ args: [{
42
+ selector: '[ngnTableGroupHeaderTr]',
43
+ host: {
44
+ '[attr.aria-rowindex]': 'ngnTableGroupHeaderTr().index + 2',
45
+ '[style.--ngn-table-row-index]': 'ngnTableGroupHeaderTr().index + 2',
46
+ },
47
+ }]
48
+ }], ctorParameters: () => [], propDecorators: { ngnTableGroupHeaderTr: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngnTableGroupHeaderTr", required: true }] }], ngnScrollerItem: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngnScrollerItem", required: false }] }] } });
15
49
 
16
50
  class NgnTableTemplates extends NgnBase {
17
- _headerTemplate = contentChild('header', ...(ngDevMode ? [{ debugName: "_headerTemplate" }] : []));
18
- _bodyTemplate = contentChild('body', ...(ngDevMode ? [{ debugName: "_bodyTemplate" }] : []));
51
+ _headerTemplate = contentChild('header', ...(ngDevMode ? [{ debugName: "_headerTemplate" }] : /* istanbul ignore next */ []));
52
+ _bodyTemplate = contentChild('body', ...(ngDevMode ? [{ debugName: "_bodyTemplate" }] : /* istanbul ignore next */ []));
53
+ _groupHeaderTemplate = contentChild('groupHeader', ...(ngDevMode ? [{ debugName: "_groupHeaderTemplate" }] : /* istanbul ignore next */ []));
19
54
  headerTemplate = this._headerTemplate;
20
55
  bodyTemplate = this._bodyTemplate;
56
+ groupHeaderTemplate = this._groupHeaderTemplate;
21
57
  /**
22
- * Types for the calendar templates.
58
+ * Types for the table templates.
23
59
  */
24
60
  templateTypes = templateTypesFn();
25
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: NgnTableTemplates, deps: null, target: i0.ɵɵFactoryTarget.Component });
26
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.1.0", 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 }], usesInheritance: true, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
61
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: NgnTableTemplates, deps: null, target: i0.ɵɵFactoryTarget.Component });
62
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.10", 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, changeDetection: i0.ChangeDetectionStrategy.OnPush });
27
63
  }
28
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: NgnTableTemplates, decorators: [{
64
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: NgnTableTemplates, decorators: [{
29
65
  type: Component,
30
66
  args: [{
31
67
  template: '',
32
68
  changeDetection: ChangeDetectionStrategy.OnPush,
33
69
  }]
34
- }], propDecorators: { _headerTemplate: [{ type: i0.ContentChild, args: ['header', { isSignal: true }] }], _bodyTemplate: [{ type: i0.ContentChild, args: ['body', { isSignal: true }] }] } });
70
+ }], 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 }] }] } });
35
71
 
72
+ /**
73
+ * Default minimum column width in pixels for table columns.
74
+ * Enforced as an absolute floor even when a relative min-size (e.g. `5%`) resolves smaller.
75
+ */
76
+ const TABLE_MIN_COLUMN_WIDTH_PX = 50;
77
+ /** Splits a CSS grid-template-columns string into individual track values, handling minmax(). */
78
+ const GRID_TRACK_RE = /(?:minmax\([^)]+\)|[^\s]+)/g;
79
+ /**
80
+ * @category control
81
+ */
36
82
  class NgnTable extends NgnTableTemplates {
37
83
  theme = this.injectThemeTemplate(tableControlTemplate, {
38
84
  root: true,
39
85
  virtual: () => this.virtual(),
86
+ resizing: () => this._resizeEngine?.isDragging() ?? false,
87
+ selectable: () => !!this.selectionMode(),
88
+ reorderable: () => this.reorderable(),
89
+ reordering: () => this._isReordering(),
40
90
  });
41
- _registeredHeaderCells = signal([], ...(ngDevMode ? [{ debugName: "_registeredHeaderCells" }] : []));
42
- rows = input.required(...(ngDevMode ? [{ debugName: "rows" }] : []));
43
- rowHeight = input(...(ngDevMode ? [undefined, { debugName: "rowHeight" }] : []));
44
- fieldId = input.required(...(ngDevMode ? [{ debugName: "fieldId" }] : []));
45
- virtual = input(false, ...(ngDevMode ? [{ debugName: "virtual" }] : []));
46
- virtualPadding = input(2, ...(ngDevMode ? [{ debugName: "virtualPadding" }] : []));
47
- striped = input(false, ...(ngDevMode ? [{ debugName: "striped" }] : []));
48
- paginator = input(false, ...(ngDevMode ? [{ debugName: "paginator" }] : []));
91
+ _registeredHeaderCells = signal([], ...(ngDevMode ? [{ debugName: "_registeredHeaderCells" }] : /* istanbul ignore next */ []));
92
+ _stickyColumns = signal(new Map(), ...(ngDevMode ? [{ debugName: "_stickyColumns" }] : /* istanbul ignore next */ []));
93
+ _scroller = viewChild.required(NgnScroller);
94
+ rows = input.required(...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
95
+ rowHeight = input(...(ngDevMode ? [undefined, { debugName: "rowHeight" }] : /* istanbul ignore next */ []));
96
+ fieldId = input.required(...(ngDevMode ? [{ debugName: "fieldId" }] : /* istanbul ignore next */ []));
97
+ virtual = input(false, ...(ngDevMode ? [{ debugName: "virtual" }] : /* istanbul ignore next */ []));
98
+ virtualPadding = input(2, ...(ngDevMode ? [{ debugName: "virtualPadding" }] : /* istanbul ignore next */ []));
99
+ striped = input(false, ...(ngDevMode ? [{ debugName: "striped" }] : /* istanbul ignore next */ []));
100
+ paginator = input(false, ...(ngDevMode ? [{ debugName: "paginator" }] : /* istanbul ignore next */ []));
101
+ /**
102
+ * The selection mode for the table.
103
+ * - `'single'`: Only one row can be selected at a time.
104
+ * - `'multi'`: Multiple rows can be selected (with Ctrl/Shift click or checkboxes).
105
+ * - `null`: Selection is disabled.
106
+ * @default null
107
+ */
108
+ selectionMode = input(null, ...(ngDevMode ? [{ debugName: "selectionMode" }] : /* istanbul ignore next */ []));
109
+ /**
110
+ * The selected row IDs. Two-way bindable via `[(selection)]`.
111
+ * @default []
112
+ */
113
+ selection = model([], ...(ngDevMode ? [{ debugName: "selection" }] : /* istanbul ignore next */ []));
114
+ /**
115
+ * Whether a selection column directive is present in the template.
116
+ * Set automatically by `NgnTableSelectionColumn` — do not set manually.
117
+ */
118
+ _hasSelectionColumn = signal(false, ...(ngDevMode ? [{ debugName: "_hasSelectionColumn" }] : /* istanbul ignore next */ []));
119
+ /**
120
+ * Column key to group rows by. Rows with the same value in this column
121
+ * are collected under a collapsible group header.
122
+ * @default null (no grouping)
123
+ */
124
+ groupBy = input(null, ...(ngDevMode ? [{ debugName: "groupBy" }] : /* istanbul ignore next */ []));
125
+ /**
126
+ * Two-way model tracking which groups are currently expanded, identified
127
+ * by their column value. Defaults to `[]` (all collapsed).
128
+ * Pass all group values to start with all groups expanded.
129
+ *
130
+ * Type-safe: when `groupBy` is set, this is typed as `T[G][]` where G
131
+ * is the groupBy column key (inferred by Angular's template type checker).
132
+ */
133
+ expandedGroups = model([], ...(ngDevMode ? [{ debugName: "expandedGroups" }] : /* istanbul ignore next */ []));
134
+ /**
135
+ * Whether column resizing is enabled.
136
+ * @default false
137
+ */
138
+ resizable = input(false, { ...(ngDevMode ? { debugName: "resizable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
139
+ /**
140
+ * The resize distribution mode.
141
+ * - `'adjacent'`: Only columns adjacent to the resize handle change. Total width stays the same.
142
+ * - `'push'`: Column grows/shrinks independently, total width changes (enables horizontal scrolling).
143
+ * @default 'adjacent'
144
+ */
145
+ resizeMode = input('adjacent', ...(ngDevMode ? [{ debugName: "resizeMode" }] : /* istanbul ignore next */ []));
146
+ /**
147
+ * Whether to lock affected columns to fixed `px` values after a resize completes.
148
+ *
149
+ * - **`proportional` mode**: when `true`, the resized column stays as `px` (locked);
150
+ * when `false`, it is converted back to `fr` and continues to flex with the container.
151
+ * - **`adjacent` / `push` modes**: when `true`, columns whose size changed are converted
152
+ * to `px`; when `false`, columns keep their original units.
153
+ *
154
+ * @default false
155
+ */
156
+ lockSizes = input(false, ...(ngDevMode ? [{ debugName: "lockSizes" }] : /* istanbul ignore next */ []));
157
+ /**
158
+ * Whether column reordering is enabled.
159
+ * @default false
160
+ */
161
+ reorderable = input(false, { ...(ngDevMode ? { debugName: "reorderable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
162
+ /**
163
+ * The current column order as an array of column identifiers.
164
+ * When empty, the natural registration order (DOM order) is used.
165
+ * Supports two-way binding for persistence.
166
+ */
167
+ columnOrder = model([], ...(ngDevMode ? [{ debugName: "columnOrder" }] : /* istanbul ignore next */ []));
49
168
  trackById = (item) => item[this.fieldId()];
50
- sort = model(null, ...(ngDevMode ? [{ debugName: "sort" }] : []));
51
- filters = model(null, ...(ngDevMode ? [{ debugName: "filters" }] : []));
52
- pageState = signal(null, ...(ngDevMode ? [{ debugName: "pageState" }] : []));
169
+ sort = model(null, ...(ngDevMode ? [{ debugName: "sort" }] : /* istanbul ignore next */ []));
170
+ filters = model(null, ...(ngDevMode ? [{ debugName: "filters" }] : /* istanbul ignore next */ []));
171
+ pageState = signal(null, ...(ngDevMode ? [{ debugName: "pageState" }] : /* istanbul ignore next */ []));
172
+ // --- Selection state ---
173
+ showCheckboxes = this._hasSelectionColumn.asReadonly();
174
+ /**
175
+ * Set of selected row IDs for O(1) lookup.
176
+ * Uses a content-aware equality check to avoid invalidating dependents
177
+ * (50+ per-row computeds) when the selection hasn't actually changed.
178
+ */
179
+ selectionSet = computed(() => new Set(this.selection()), { ...(ngDevMode ? { debugName: "selectionSet" } : /* istanbul ignore next */ {}), equal: (a, b) => a.size === b.size && [...a].every(v => b.has(v)) });
180
+ /**
181
+ * Anchor index for Shift+click range selection (index in formattedRows).
182
+ */
183
+ _selectionAnchor = signal(null, ...(ngDevMode ? [{ debugName: "_selectionAnchor" }] : /* istanbul ignore next */ []));
184
+ /**
185
+ * Currently focused row index for keyboard navigation (index in formattedRows).
186
+ */
187
+ focusedRowIndex = signal(null, ...(ngDevMode ? [{ debugName: "focusedRowIndex" }] : /* istanbul ignore next */ []));
188
+ isAllSelected = computed(() => {
189
+ const rows = this.formattedRows();
190
+ const selSet = this.selectionSet();
191
+ return rows.length > 0 && rows.every(row => selSet.has(row.id));
192
+ }, ...(ngDevMode ? [{ debugName: "isAllSelected" }] : /* istanbul ignore next */ []));
193
+ isIndeterminate = computed(() => {
194
+ const selSet = this.selectionSet();
195
+ return selSet.size > 0 && !this.isAllSelected();
196
+ }, ...(ngDevMode ? [{ debugName: "isIndeterminate" }] : /* istanbul ignore next */ []));
197
+ headerCheckboxValue = computed(() => {
198
+ if (this.isAllSelected())
199
+ return true;
200
+ if (this.isIndeterminate())
201
+ return null;
202
+ return false;
203
+ }, ...(ngDevMode ? [{ debugName: "headerCheckboxValue" }] : /* istanbul ignore next */ []));
204
+ /**
205
+ * Total column count including the selection checkbox column.
206
+ */
207
+ totalColumnCount = computed(() => this.columnCount() + (this.showCheckboxes() ? 1 : 0), ...(ngDevMode ? [{ debugName: "totalColumnCount" }] : /* istanbul ignore next */ []));
53
208
  formattedRows = computed(() => {
209
+ const groupBy = this.groupBy();
210
+ if (groupBy) {
211
+ return this._groupedRows();
212
+ }
54
213
  const rows = this._sortedRows();
55
214
  return rows.map((data, index) => ({
56
215
  kind: 'data',
@@ -58,7 +217,51 @@ class NgnTable extends NgnTableTemplates {
58
217
  data,
59
218
  index,
60
219
  }));
61
- }, ...(ngDevMode ? [{ debugName: "formattedRows" }] : []));
220
+ }, ...(ngDevMode ? [{ debugName: "formattedRows" }] : /* istanbul ignore next */ []));
221
+ _groupedRows = computed(() => {
222
+ const groupBy = this.groupBy();
223
+ if (!groupBy)
224
+ return [];
225
+ const rows = this._sortedRows();
226
+ const expandedSet = new Set(this.expandedGroups());
227
+ const fieldId = this.fieldId();
228
+ const groupMap = new Map();
229
+ for (const row of rows) {
230
+ const key = row[groupBy];
231
+ let group = groupMap.get(key);
232
+ if (!group) {
233
+ group = [];
234
+ groupMap.set(key, group);
235
+ }
236
+ group.push(row);
237
+ }
238
+ // Build flat list with group headers interleaved
239
+ const result = [];
240
+ let index = 0;
241
+ for (const [groupKey, groupRows] of groupMap) {
242
+ const expanded = expandedSet.has(groupKey);
243
+ result.push({
244
+ kind: 'group-header',
245
+ id: `group-${String(groupKey)}`,
246
+ groupKey,
247
+ groupValue: groupKey,
248
+ count: groupRows.length,
249
+ expanded,
250
+ index: index++,
251
+ });
252
+ if (expanded) {
253
+ for (const data of groupRows) {
254
+ result.push({
255
+ kind: 'data',
256
+ id: data[fieldId],
257
+ data,
258
+ index: index++,
259
+ });
260
+ }
261
+ }
262
+ }
263
+ return result;
264
+ }, ...(ngDevMode ? [{ debugName: "_groupedRows" }] : /* istanbul ignore next */ []));
62
265
  pagedRows = computed(() => {
63
266
  const paginator = this.paginator();
64
267
  if (!paginator) {
@@ -69,7 +272,7 @@ class NgnTable extends NgnTableTemplates {
69
272
  return [];
70
273
  }
71
274
  return this.formattedRows().slice(pageState.slice.skip, pageState.slice.take + pageState.slice.skip);
72
- }, ...(ngDevMode ? [{ debugName: "pagedRows" }] : []));
275
+ }, ...(ngDevMode ? [{ debugName: "pagedRows" }] : /* istanbul ignore next */ []));
73
276
  _filteredRows = computed(() => {
74
277
  const filters = this.filters();
75
278
  const rows = this.rows();
@@ -77,7 +280,7 @@ class NgnTable extends NgnTableTemplates {
77
280
  return rows;
78
281
  }
79
282
  return executeMultiFilter(rows, filters);
80
- }, ...(ngDevMode ? [{ debugName: "_filteredRows" }] : []));
283
+ }, ...(ngDevMode ? [{ debugName: "_filteredRows" }] : /* istanbul ignore next */ []));
81
284
  _sortedRows = computed(() => {
82
285
  const rows = this._filteredRows();
83
286
  const sort = this.sort();
@@ -106,69 +309,798 @@ class NgnTable extends NgnTableTemplates {
106
309
  ? String(aValue).localeCompare(String(bValue))
107
310
  : String(bValue).localeCompare(String(aValue));
108
311
  });
109
- }, ...(ngDevMode ? [{ debugName: "_sortedRows" }] : []));
110
- columnCount = computed(() => this._registeredHeaderCells().length, ...(ngDevMode ? [{ debugName: "columnCount" }] : []));
312
+ }, ...(ngDevMode ? [{ debugName: "_sortedRows" }] : /* istanbul ignore next */ []));
313
+ columnCount = computed(() => this._registeredHeaderCells().length, ...(ngDevMode ? [{ debugName: "columnCount" }] : /* istanbul ignore next */ []));
314
+ // --- Column reorder state ---
315
+ _isReordering = signal(false, ...(ngDevMode ? [{ debugName: "_isReordering" }] : /* istanbul ignore next */ []));
316
+ _reorderSourceColumnId = signal(null, ...(ngDevMode ? [{ debugName: "_reorderSourceColumnId" }] : /* istanbul ignore next */ []));
317
+ _dropIndicatorState = signal(null, ...(ngDevMode ? [{ debugName: "_dropIndicatorState" }] : /* istanbul ignore next */ []));
318
+ _reorderTargetIndex = -1;
319
+ /**
320
+ * Effective column order: merges user-provided `columnOrder` with registered header cells.
321
+ * If `columnOrder` is empty, falls back to registration order.
322
+ * Unknown keys are filtered; new columns not in the order are appended.
323
+ */
324
+ _effectiveColumnOrder = computed(() => {
325
+ const cells = this._registeredHeaderCells();
326
+ const cellIds = cells.map(c => c.ngnTableTh());
327
+ const userOrder = this.columnOrder();
328
+ if (!userOrder.length) {
329
+ return cellIds;
330
+ }
331
+ // Filter to valid keys and append any new columns not in the order
332
+ const validOrder = userOrder.filter(id => cellIds.includes(id));
333
+ const missing = cellIds.filter(id => !validOrder.includes(id));
334
+ return [...validOrder, ...missing];
335
+ }, ...(ngDevMode ? [{ debugName: "_effectiveColumnOrder" }] : /* istanbul ignore next */ []));
336
+ _stickyStartColumns = computed(() => {
337
+ const stickyMap = this._stickyColumns();
338
+ const order = this._effectiveColumnOrder();
339
+ const contiguous = [];
340
+ for (const id of order) {
341
+ if (stickyMap.get(id) === 'start') {
342
+ contiguous.push(id);
343
+ }
344
+ else {
345
+ break;
346
+ }
347
+ }
348
+ return contiguous;
349
+ }, ...(ngDevMode ? [{ debugName: "_stickyStartColumns" }] : /* istanbul ignore next */ []));
350
+ _stickyEndColumns = computed(() => {
351
+ const stickyMap = this._stickyColumns();
352
+ const order = this._effectiveColumnOrder();
353
+ const contiguous = [];
354
+ for (let i = order.length - 1; i >= 0; i--) {
355
+ if (stickyMap.get(order[i]) === 'end') {
356
+ contiguous.unshift(order[i]);
357
+ }
358
+ else {
359
+ break;
360
+ }
361
+ }
362
+ return contiguous;
363
+ }, ...(ngDevMode ? [{ debugName: "_stickyEndColumns" }] : /* istanbul ignore next */ []));
364
+ /**
365
+ * Maps column ID → 1-based visual position.
366
+ */
367
+ columnOrderMap = computed(() => {
368
+ const order = this._effectiveColumnOrder();
369
+ const map = new Map();
370
+ for (let i = 0; i < order.length; i++) {
371
+ map.set(order[i], i + 1);
372
+ }
373
+ return map;
374
+ }, ...(ngDevMode ? [{ debugName: "columnOrderMap" }] : /* istanbul ignore next */ []));
375
+ /**
376
+ * Returns the 1-based visual column index for a given 0-based logical (DOM) index.
377
+ */
378
+ getVisualColumnIndex(logicalIndex) {
379
+ const cells = this._registeredHeaderCells();
380
+ const cell = cells[logicalIndex];
381
+ if (!cell)
382
+ return logicalIndex + 1;
383
+ return this.columnOrderMap().get(cell.ngnTableTh()) ?? logicalIndex + 1;
384
+ }
385
+ // --- Resize engine ---
386
+ _tableElementSize = elementSizeSignal(this.element);
387
+ _resizeEngine;
388
+ /**
389
+ * Grid template columns computed from the resize engine when resizable,
390
+ * otherwise falls back to the standard equal-width repeat.
391
+ */
392
+ gridTemplateColumns;
393
+ constructor() {
394
+ super();
395
+ // Create engine eagerly in constructor (outside reactive context) to avoid NG0602
396
+ this._resizeEngine = new ResizeEngine({
397
+ items: computed(() => this._registeredHeaderCells()),
398
+ containerSize: computed(() => {
399
+ // Subscribe to the size signal for reactivity, but use clientWidth
400
+ // to exclude vertical scrollbar width and avoid horizontal overflow.
401
+ this._tableElementSize();
402
+ const tableEl = this.element.nativeElement.querySelector('table');
403
+ const total = tableEl?.clientWidth ?? 0;
404
+ if (!this._hasSelectionColumn())
405
+ return total;
406
+ // Subtract the selection column width so the resize engine only distributes
407
+ // the remaining space among data columns.
408
+ const selCol = this.element.nativeElement.querySelector(`.${this.theme.class('selection-column')}`);
409
+ return total - (selCol?.getBoundingClientRect().width ?? 0);
410
+ }),
411
+ gapSizes: signal([]),
412
+ distributionMode: computed(() => this.resizeMode()),
413
+ containerConstrained: computed(() => this.resizeMode() !== 'push'),
414
+ lockSizes: computed(() => this.lockSizes()),
415
+ minItemSizePx: TABLE_MIN_COLUMN_WIDTH_PX,
416
+ resolveItemSizes: () => this._registeredHeaderCells().map(cell => cell.element.nativeElement.getBoundingClientRect().width),
417
+ });
418
+ this.gridTemplateColumns = computed(() => {
419
+ const checkboxCol = this._hasSelectionColumn() ? 'min-content ' : '';
420
+ if (this.resizable()) {
421
+ const rawSizes = this._resizeEngine.gridTemplateSizes();
422
+ // When reorderable, permute grid track sizes to match visual column order
423
+ if (this.reorderable()) {
424
+ return `${checkboxCol}${this._permuteGridSizes(rawSizes)}`;
425
+ }
426
+ return `${checkboxCol}${rawSizes}`;
427
+ }
428
+ return `${checkboxCol}repeat(${this.columnCount()}, 1fr)`;
429
+ }, ...(ngDevMode ? [{ debugName: "gridTemplateColumns" }] : /* istanbul ignore next */ []));
430
+ afterRenderEffect(() => {
431
+ const startCols = this._stickyStartColumns();
432
+ const endCols = this._stickyEndColumns();
433
+ if (startCols.length === 0 && endCols.length === 0)
434
+ return;
435
+ if (this._resizeEngine.isDragging())
436
+ return;
437
+ this._tableElementSize();
438
+ const tableEl = this.element.nativeElement.querySelector(':scope > table');
439
+ if (!tableEl)
440
+ return;
441
+ const cells = this._registeredHeaderCells();
442
+ let selectionWidth = 0;
443
+ if (this._hasSelectionColumn()) {
444
+ const selCol = tableEl.querySelector(`.${this.theme.class('selection-column')}`);
445
+ selectionWidth = selCol?.getBoundingClientRect().width ?? 0;
446
+ }
447
+ let cumulativeLeft = selectionWidth;
448
+ for (let i = 0; i < startCols.length; i++) {
449
+ tableEl.style.setProperty(`--ngn-sticky-start-offset-${i}`, `${cumulativeLeft}px`);
450
+ const cell = cells.find(c => c.ngnTableTh() === startCols[i]);
451
+ cumulativeLeft += cell?.element.nativeElement.getBoundingClientRect().width ?? 0;
452
+ }
453
+ let cumulativeRight = 0;
454
+ for (let i = endCols.length - 1; i >= 0; i--) {
455
+ tableEl.style.setProperty(`--ngn-sticky-end-offset-${i}`, `${cumulativeRight}px`);
456
+ const cell = cells.find(c => c.ngnTableTh() === endCols[i]);
457
+ cumulativeRight += cell?.element.nativeElement.getBoundingClientRect().width ?? 0;
458
+ }
459
+ });
460
+ }
111
461
  pageChanged(event) {
112
462
  this.pageState.set(event);
113
463
  }
114
464
  column(column) {
115
465
  return column;
116
466
  }
467
+ registerSelectionColumn() {
468
+ this._hasSelectionColumn.set(true);
469
+ }
470
+ unregisterSelectionColumn() {
471
+ this._hasSelectionColumn.set(false);
472
+ }
117
473
  registerHeaderCell(cell) {
118
474
  this._registeredHeaderCells.update(cells => [...cells, cell]);
119
475
  }
120
476
  unregisterHeaderCell(cell) {
121
477
  this._registeredHeaderCells.update(cells => cells.filter(c => c !== cell));
122
478
  }
123
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: NgnTable, deps: null, target: i0.ɵɵFactoryTarget.Component });
124
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", 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 }, sort: { classPropertyName: "sort", publicName: "sort", isSignal: true, isRequired: false, transformFunction: null }, filters: { classPropertyName: "filters", publicName: "filters", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sort: "sortChange", filters: "filtersChange" }, host: { attributes: { "tabindex": "0" } }, providers: [provideSelf(NgnTable)], usesInheritance: true, ngImport: i0, template: "<table\r\n [ptInt]=\"this\"\r\n [ptClass]=\"{ table: true, striped: striped() }\"\r\n [attr.aria-rowcount]=\"rows().length\"\r\n [style.--ngn-table-column-count]=\"columnCount()\"\r\n [style.--ngn-table-row-height.px]=\"rowHeight()\"\r\n>\r\n <thead [ptInt]=\"this\" [ptClass]=\"'head'\">\r\n <ng-container [ngTemplateOutlet]=\"headerTemplate()\"></ng-container>\r\n </thead>\r\n\r\n <tbody\r\n [ptInt]=\"this\"\r\n [ptClass]=\"'body'\"\r\n ngn-scroller\r\n #scroller\r\n [virtual]=\"virtual()\"\r\n [items]=\"pagedRows()\"\r\n [fieldId]=\"'id'\"\r\n [itemHeight]=\"rowHeight() || 0\"\r\n [virtualPadding]=\"virtualPadding()\"\r\n [style.--ngn-table-item-start-index]=\"scroller.itemStartIndex()\"\r\n >\r\n <ng-template #item [ngnTemplate]=\"scroller.templateTypes.item\" let-item let-index=\"index\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"bodyTemplate()\"\r\n [ngTemplateOutletContext]=\"{ $implicit: item }\"\r\n ></ng-container>\r\n </ng-template>\r\n </tbody>\r\n</table>\r\n@if (paginator()) {\r\n <ngn-paginator [totalItems]=\"rows().length\" (value)=\"pageChanged($event)\" />\r\n}\r\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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
479
+ getRegisteredHeaderCells() {
480
+ return this._registeredHeaderCells();
481
+ }
482
+ registerStickyColumn(columnId, side) {
483
+ this._stickyColumns.update(map => {
484
+ const next = new Map(map);
485
+ next.set(columnId, side);
486
+ return next;
487
+ });
488
+ }
489
+ unregisterStickyColumn(columnId) {
490
+ this._stickyColumns.update(map => {
491
+ const next = new Map(map);
492
+ next.delete(columnId);
493
+ return next;
494
+ });
495
+ }
496
+ getStickyInfo(columnId) {
497
+ const startCols = this._stickyStartColumns();
498
+ const endCols = this._stickyEndColumns();
499
+ const startIndex = startCols.indexOf(columnId);
500
+ if (startIndex >= 0) {
501
+ return { side: 'start', index: startIndex, isEdge: startIndex === startCols.length - 1 };
502
+ }
503
+ const endIndex = endCols.indexOf(columnId);
504
+ if (endIndex >= 0) {
505
+ return { side: 'end', index: endIndex, isEdge: endIndex === 0 };
506
+ }
507
+ return null;
508
+ }
509
+ // --- Resize operations (called by NgnTableTh) ---
510
+ startColumnResize(columnIndex, event) {
511
+ if (!this.resizable())
512
+ return;
513
+ this._resizeEngine.startDrag(columnIndex, event.clientX);
514
+ }
515
+ dragColumnResize(columnIndex, event) {
516
+ if (!this.resizable())
517
+ return;
518
+ this._resizeEngine.drag(columnIndex, event.clientX);
519
+ }
520
+ endColumnResize(columnIndex, cancel) {
521
+ if (!this.resizable())
522
+ return;
523
+ this._resizeEngine.endDrag(columnIndex, cancel);
524
+ }
525
+ // --- Selection operations ---
526
+ isRowSelected(id) {
527
+ return this.selectionSet().has(id);
528
+ }
529
+ handleRowClick(row, event) {
530
+ const mode = this.selectionMode();
531
+ if (!mode)
532
+ return;
533
+ const id = row.id;
534
+ const rowIndex = this.formattedRows().findIndex(r => r.id === row.id);
535
+ if (mode === 'single') {
536
+ this.selection.set([id]);
537
+ this._selectionAnchor.set(rowIndex);
538
+ this.focusedRowIndex.set(null);
539
+ return;
540
+ }
541
+ // Multi mode
542
+ if (event.shiftKey && this._selectionAnchor() !== null) {
543
+ this._selectRange(this._selectionAnchor(), rowIndex);
544
+ }
545
+ else if (event.ctrlKey || event.metaKey) {
546
+ this._toggleRowInSelection(id);
547
+ }
548
+ else {
549
+ this._toggleRowInSelection(id);
550
+ }
551
+ this._selectionAnchor.set(rowIndex);
552
+ this.focusedRowIndex.set(null);
553
+ }
554
+ handleCheckboxChange(row) {
555
+ const id = row.id;
556
+ this._toggleRowInSelection(id);
557
+ const rowIndex = this.formattedRows().findIndex(r => r.id === row.id);
558
+ this._selectionAnchor.set(rowIndex);
559
+ this.focusedRowIndex.set(null);
560
+ }
561
+ toggleSelectAll() {
562
+ if (this.isAllSelected()) {
563
+ this.selection.set([]);
564
+ }
565
+ else {
566
+ const dataRows = this.formattedRows().filter((r) => r.kind === 'data');
567
+ this.selection.set(dataRows.map(r => r.id));
568
+ }
569
+ }
570
+ _toggleRowInSelection(id) {
571
+ const current = this.selection();
572
+ if (this.selectionSet().has(id)) {
573
+ this.selection.set(current.filter(v => v !== id));
574
+ }
575
+ else {
576
+ this.selection.set([...current, id]);
577
+ }
578
+ }
579
+ _selectRange(fromIndex, toIndex) {
580
+ const rows = this.formattedRows();
581
+ const start = Math.min(fromIndex, toIndex);
582
+ const end = Math.max(fromIndex, toIndex);
583
+ const rangeIds = rows
584
+ .slice(start, end + 1)
585
+ .filter((r) => r.kind === 'data')
586
+ .map(r => r.id);
587
+ // Merge with existing selection (union)
588
+ const currentSet = new Set(this.selection());
589
+ for (const id of rangeIds) {
590
+ currentSet.add(id);
591
+ }
592
+ this.selection.set([...currentSet]);
593
+ }
594
+ // --- Keyboard navigation ---
595
+ onKeyDown(event) {
596
+ const mode = this.selectionMode();
597
+ if (!mode)
598
+ return;
599
+ const rows = this.formattedRows();
600
+ if (rows.length === 0)
601
+ return;
602
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
603
+ event.preventDefault();
604
+ event.stopPropagation();
605
+ // Start from current focus, or current selection in single mode, or -1
606
+ let currentIndex = this.focusedRowIndex();
607
+ if (currentIndex === null && mode === 'single') {
608
+ const sel = this.selection();
609
+ if (sel.length > 0) {
610
+ currentIndex = rows.findIndex(r => r.id === sel[0]);
611
+ }
612
+ }
613
+ currentIndex ??= -1;
614
+ const nextIndex = event.key === 'ArrowDown'
615
+ ? Math.min(currentIndex + 1, rows.length - 1)
616
+ : Math.max(currentIndex - 1, 0);
617
+ this.focusedRowIndex.set(nextIndex);
618
+ this._scroller().scrollToIndex(nextIndex);
619
+ if (mode === 'single') {
620
+ const id = rows[nextIndex].id;
621
+ this.selection.set([id]);
622
+ this._selectionAnchor.set(nextIndex);
623
+ }
624
+ else if (event.shiftKey) {
625
+ // Extend range selection with Shift+Arrow
626
+ const anchor = this._selectionAnchor() ?? nextIndex;
627
+ this._selectRange(anchor, nextIndex);
628
+ }
629
+ }
630
+ else if (event.key === ' ' || event.key === 'Enter') {
631
+ if (mode === 'multi') {
632
+ const focusIdx = this.focusedRowIndex();
633
+ if (focusIdx !== null && rows[focusIdx]) {
634
+ event.preventDefault();
635
+ event.stopPropagation();
636
+ const id = rows[focusIdx].id;
637
+ this._toggleRowInSelection(id);
638
+ this._selectionAnchor.set(focusIdx);
639
+ }
640
+ }
641
+ }
642
+ }
643
+ // --- Row grouping ---
644
+ /**
645
+ * Toggle the expanded state of a group identified by its column value.
646
+ */
647
+ toggleGroup(groupKey) {
648
+ const current = this.expandedGroups();
649
+ if (current.includes(groupKey)) {
650
+ this.expandedGroups.set(current.filter(k => k !== groupKey));
651
+ }
652
+ else {
653
+ this.expandedGroups.set([...current, groupKey]);
654
+ }
655
+ }
656
+ /**
657
+ * Toggle a group from a group-header row. Used in the template where
658
+ * the scroller item type cannot carry the `G` generic.
659
+ * @internal
660
+ */
661
+ toggleGroupFromRow(row) {
662
+ this.toggleGroup(row.groupKey);
663
+ }
664
+ // --- Reorder operations (called by NgnTableReorderableColumn) ---
665
+ getReorderBounds(columnId) {
666
+ const order = this._effectiveColumnOrder();
667
+ const startCols = this._stickyStartColumns();
668
+ const endCols = this._stickyEndColumns();
669
+ const side = this._stickyColumns().get(columnId);
670
+ if (side === 'start') {
671
+ return { min: 0, max: startCols.length };
672
+ }
673
+ else if (side === 'end') {
674
+ return { min: order.length - endCols.length, max: order.length };
675
+ }
676
+ else {
677
+ return { min: startCols.length, max: order.length - endCols.length };
678
+ }
679
+ }
680
+ startColumnReorder(columnId) {
681
+ if (!this.reorderable())
682
+ return;
683
+ this._isReordering.set(true);
684
+ this._reorderSourceColumnId.set(columnId);
685
+ this._reorderTargetIndex = -1;
686
+ }
687
+ dragColumnReorder(event) {
688
+ if (!this.reorderable() || !this._isReordering())
689
+ return;
690
+ const cells = this._registeredHeaderCells();
691
+ const effectiveOrder = this._effectiveColumnOrder();
692
+ const sourceId = this._reorderSourceColumnId();
693
+ if (!sourceId)
694
+ return;
695
+ // Find drop target by comparing cursor X to header cell midpoints (in visual order)
696
+ const cursorX = event.clientX;
697
+ let targetIndex = effectiveOrder.length;
698
+ // Build visual-order cells with their bounding boxes
699
+ const visualCells = effectiveOrder
700
+ .map(id => cells.find(c => c.ngnTableTh() === id))
701
+ .filter((c) => !!c);
702
+ for (let i = 0; i < visualCells.length; i++) {
703
+ const rect = visualCells[i].element.nativeElement.getBoundingClientRect();
704
+ const midX = rect.left + rect.width / 2;
705
+ if (cursorX < midX) {
706
+ targetIndex = i;
707
+ break;
708
+ }
709
+ }
710
+ const bounds = this.getReorderBounds(sourceId);
711
+ targetIndex = Math.max(bounds.min, Math.min(bounds.max, targetIndex));
712
+ this._reorderTargetIndex = targetIndex;
713
+ // Compute indicator position relative to the table element
714
+ const tableEl = this.element.nativeElement.querySelector(':scope > table');
715
+ if (!tableEl)
716
+ return;
717
+ const tableRect = tableEl.getBoundingClientRect();
718
+ // Use the first header cell's height for the indicator
719
+ const firstCellRect = visualCells[0]?.element.nativeElement.getBoundingClientRect();
720
+ const indicatorHeight = firstCellRect?.height ?? 40;
721
+ let indicatorLeftPx;
722
+ if (targetIndex === 0) {
723
+ const firstCell = visualCells[0];
724
+ if (!firstCell)
725
+ return;
726
+ const rect = firstCell.element.nativeElement.getBoundingClientRect();
727
+ indicatorLeftPx = rect.left - tableRect.left;
728
+ }
729
+ else if (targetIndex >= visualCells.length) {
730
+ const lastCell = visualCells[visualCells.length - 1];
731
+ if (!lastCell)
732
+ return;
733
+ const rect = lastCell.element.nativeElement.getBoundingClientRect();
734
+ indicatorLeftPx = rect.right - tableRect.left;
735
+ }
736
+ else {
737
+ const prevCell = visualCells[targetIndex - 1];
738
+ const nextCell = visualCells[targetIndex];
739
+ const prevRect = prevCell.element.nativeElement.getBoundingClientRect();
740
+ const nextRect = nextCell.element.nativeElement.getBoundingClientRect();
741
+ indicatorLeftPx = (prevRect.right + nextRect.left) / 2 - tableRect.left;
742
+ }
743
+ // Clamp so the 3px-wide indicator stays within the visible content area
744
+ // (use clientWidth to exclude scrollbar width from the boundary)
745
+ const maxLeft = tableEl.clientWidth - 3;
746
+ indicatorLeftPx = Math.max(0, Math.min(indicatorLeftPx, maxLeft));
747
+ this._dropIndicatorState.set({
748
+ leftPx: indicatorLeftPx,
749
+ topPx: tableEl.scrollTop,
750
+ heightPx: indicatorHeight,
751
+ });
752
+ }
753
+ endColumnReorder(cancel) {
754
+ if (!this.reorderable())
755
+ return;
756
+ if (!cancel && this._reorderTargetIndex >= 0) {
757
+ const sourceId = this._reorderSourceColumnId();
758
+ if (sourceId) {
759
+ const effectiveOrder = [...this._effectiveColumnOrder()];
760
+ const sourceIndex = effectiveOrder.indexOf(sourceId);
761
+ if (sourceIndex >= 0) {
762
+ // Remove source from its current position
763
+ effectiveOrder.splice(sourceIndex, 1);
764
+ // Adjust target index if source was before target
765
+ let insertAt = this._reorderTargetIndex;
766
+ if (sourceIndex < this._reorderTargetIndex) {
767
+ insertAt--;
768
+ }
769
+ // Insert at new position
770
+ effectiveOrder.splice(insertAt, 0, sourceId);
771
+ this.columnOrder.set(effectiveOrder);
772
+ }
773
+ }
774
+ }
775
+ this._isReordering.set(false);
776
+ this._reorderSourceColumnId.set(null);
777
+ this._dropIndicatorState.set(null);
778
+ this._reorderTargetIndex = -1;
779
+ }
780
+ getReorderSourceColumnId() {
781
+ return this._reorderSourceColumnId();
782
+ }
783
+ /**
784
+ * Permutes grid template sizes from logical (registration) order to visual (column order) order.
785
+ */
786
+ _permuteGridSizes(rawSizes) {
787
+ const tracks = rawSizes.match(GRID_TRACK_RE) ?? [];
788
+ const cells = this._registeredHeaderCells();
789
+ const effectiveOrder = this._effectiveColumnOrder();
790
+ if (tracks.length !== cells.length) {
791
+ return rawSizes; // Mismatch — don't permute
792
+ }
793
+ // Build map: column ID → track size (logical order)
794
+ const sizeByColumnId = new Map();
795
+ for (let i = 0; i < cells.length; i++) {
796
+ sizeByColumnId.set(cells[i].ngnTableTh(), tracks[i]);
797
+ }
798
+ // Emit sizes in visual order
799
+ return effectiveOrder.map(id => sizeByColumnId.get(id) ?? '1fr').join(' ');
800
+ }
801
+ /**
802
+ * Auto-sizes the column at `columnIndex` to fit its visible content.
803
+ * Uses `Canvas.measureText()` for fast, reflow-free text measurement since
804
+ * grid-constrained cells (`min-width: 0`) don't report natural content width
805
+ * via `scrollWidth`.
806
+ */
807
+ autoSizeColumn(columnIndex) {
808
+ if (!this.resizable())
809
+ return;
810
+ const cells = this._registeredHeaderCells();
811
+ const headerCell = cells[columnIndex];
812
+ if (!headerCell)
813
+ return;
814
+ const tableEl = this.element.nativeElement.querySelector('table');
815
+ if (!tableEl)
816
+ return;
817
+ const headerEl = headerCell.element.nativeElement;
818
+ const ctx = autoSizeCanvasCtx();
819
+ // Measure header: text width + non-text siblings (icons, controls) + flex gap.
820
+ // The header cell is a flex container with: cell-text, spacer, filter/sort icons, resize handle.
821
+ const cellTextClass = this.theme.class('cell-text');
822
+ const headerTextEl = headerEl.querySelector(`.${cellTextClass}`);
823
+ let headerRequiredWidth = 0;
824
+ if (headerTextEl) {
825
+ ctx.font = getCssFont(headerEl);
826
+ const text = headerTextEl.textContent ?? '';
827
+ headerRequiredWidth = Math.ceil(ctx.measureText(text).width);
828
+ }
829
+ // Add width of visible non-text siblings (filter icon, sort icon, etc.)
830
+ const headerStyle = getComputedStyle(headerEl);
831
+ const headerGap = parseFloat(headerStyle.gap) || 0;
832
+ let visibleSiblingCount = 0;
833
+ for (let i = 0; i < headerEl.children.length; i++) {
834
+ const child = headerEl.children[i];
835
+ if (child === headerTextEl)
836
+ continue;
837
+ // Skip spacer (zero intrinsic width) and hidden resize handle
838
+ if (child.offsetWidth > 0 && getComputedStyle(child).flexGrow === '0') {
839
+ headerRequiredWidth += child.offsetWidth;
840
+ visibleSiblingCount++;
841
+ }
842
+ }
843
+ // Add flex gaps between visible items
844
+ if (visibleSiblingCount > 0) {
845
+ headerRequiredWidth += headerGap * visibleSiblingCount;
846
+ }
847
+ let maxContentWidth = headerRequiredWidth;
848
+ // Measure visible body cells (nth-child is 1-indexed, offset by selection column)
849
+ const selectionOffset = this._hasSelectionColumn() ? 1 : 0;
850
+ const bodyCells = Array.from(tableEl.querySelectorAll(`tbody tr td:nth-child(${columnIndex + selectionOffset + 1})`));
851
+ if (bodyCells.length > 0) {
852
+ ctx.font = getCssFont(bodyCells[0]);
853
+ for (let i = 0; i < bodyCells.length; i++) {
854
+ const text = bodyCells[i].textContent ?? '';
855
+ maxContentWidth = Math.max(maxContentWidth, Math.ceil(ctx.measureText(text).width));
856
+ }
857
+ }
858
+ // Add cell horizontal padding
859
+ const paddingLeft = parseFloat(headerStyle.paddingLeft) || 0;
860
+ const paddingRight = parseFloat(headerStyle.paddingRight) || 0;
861
+ const totalWidth = maxContentWidth + paddingLeft + paddingRight;
862
+ // Clamp to min/max constraints
863
+ const containerSize = this._tableElementSize().width;
864
+ const minPx = Math.max(TABLE_MIN_COLUMN_WIDTH_PX, getResizeLimitInPx(headerCell.minSize(), containerSize));
865
+ const maxPx = getResizeLimitInPx(headerCell.maxSize(), containerSize);
866
+ const clampedWidth = Math.max(minPx, Math.min(maxPx, totalWidth));
867
+ this._resizeEngine.setItemSize(headerCell, `${clampedWidth}px`);
868
+ this._resizeEngine.hasBeenResized.set(true);
869
+ }
870
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: NgnTable, deps: [], target: i0.ɵɵFactoryTarget.Component });
871
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.10", 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 }, 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)" }, properties: { "attr.aria-multiselectable": "selectionMode() === \"multi\" ? true : null" } }, providers: [provideSelf(NgnTable)], viewQueries: [{ propertyName: "_scroller", first: true, predicate: NgnScroller, descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<table\r\n #scrollContainer\r\n ngnScrollShadow=\"horizontal\"\r\n [ptInt]=\"this\"\r\n [ptClass]=\"{ table: true, striped: striped() }\"\r\n [attr.aria-rowcount]=\"formattedRows().length\"\r\n [style.--ngn-table-column-count]=\"totalColumnCount()\"\r\n [style.--ngn-table-selection-offset]=\"showCheckboxes() ? 1 : 0\"\r\n [style.grid-template-columns]=\"gridTemplateColumns()\"\r\n [style.--ngn-table-row-height.px]=\"rowHeight()\"\r\n>\r\n <thead [ptInt]=\"this\" [ptClass]=\"'head'\">\r\n <ng-container [ngTemplateOutlet]=\"headerTemplate()\"></ng-container>\r\n </thead>\r\n @if (_dropIndicatorState()) {\r\n <div\r\n [ptInt]=\"this\"\r\n [ptClass]=\"'drop-indicator'\"\r\n [style.left.px]=\"_dropIndicatorState()!.leftPx\"\r\n [style.top.px]=\"_dropIndicatorState()!.topPx\"\r\n [style.height.px]=\"_dropIndicatorState()!.heightPx\"\r\n ></div>\r\n }\r\n\r\n <tbody\r\n [ptInt]=\"this\"\r\n [ptClass]=\"'body'\"\r\n ngn-scroller\r\n #scroller\r\n [scrollContainer]=\"scrollContainer\"\r\n [virtual]=\"virtual()\"\r\n [items]=\"pagedRows()\"\r\n [fieldId]=\"'id'\"\r\n [itemHeight]=\"rowHeight() || 0\"\r\n [virtualPadding]=\"virtualPadding()\"\r\n [style.--ngn-table-item-start-index]=\"scroller.itemStartIndex()\"\r\n >\r\n <ng-template #item [ngnTemplate]=\"scroller.templateTypes.item\" let-item let-index=\"index\">\r\n @if (item.kind === 'group-header') {\r\n @if (groupHeaderTemplate(); as customGroupHeader) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"customGroupHeader\"\r\n [ngTemplateOutletContext]=\"{\r\n $implicit: {\r\n groupKey: item.groupKey,\r\n groupValue: item.groupValue,\r\n count: item.count,\r\n expanded: item.expanded,\r\n },\r\n }\"\r\n ></ng-container>\r\n } @else {\r\n <tr\r\n [ngnTableGroupHeaderTr]=\"item\"\r\n [class]=\"theme.classes({ 'group-expanded': item.expanded })\"\r\n [attr.aria-expanded]=\"item.expanded\"\r\n role=\"row\"\r\n (click)=\"toggleGroupFromRow(item)\"\r\n >\r\n <td [class]=\"theme.class('group-header-cell')\" role=\"gridcell\">\r\n <ngn-icon\r\n [defaultIcon]=\"'table-group-toggle'\"\r\n [class]=\"theme.class('group-toggle')\"\r\n />\r\n <span>{{ item.groupValue }}</span>\r\n <span>({{ item.count }})</span>\r\n </td>\r\n </tr>\r\n }\r\n } @else {\r\n <ng-container\r\n [ngTemplateOutlet]=\"bodyTemplate()\"\r\n [ngTemplateOutletContext]=\"{ $implicit: item }\"\r\n ></ng-container>\r\n }\r\n </ng-template>\r\n </tbody>\r\n</table>\r\n@if (paginator()) {\r\n <ngn-paginator [totalItems]=\"rows().length\" (value)=\"pageChanged($event)\" />\r\n}\r\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"] }, { 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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
125
872
  }
126
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: NgnTable, decorators: [{
873
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: NgnTable, decorators: [{
127
874
  type: Component,
128
- args: [{ selector: 'ngn-table', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgTemplateOutlet, NgnScroller, NgnPaginator, NgnTemplate, NgnPt], providers: [provideSelf(NgnTable)], host: {
875
+ args: [{ selector: 'ngn-table', changeDetection: ChangeDetectionStrategy.OnPush, imports: [
876
+ NgTemplateOutlet,
877
+ NgnScroller,
878
+ NgnPaginator,
879
+ NgnTemplate,
880
+ NgnPt,
881
+ NgnIcon,
882
+ NgnTableGroupHeaderTr,
883
+ NgnScrollShadow,
884
+ ], providers: [provideSelf(NgnTable)], host: {
129
885
  tabindex: '0',
130
- }, template: "<table\r\n [ptInt]=\"this\"\r\n [ptClass]=\"{ table: true, striped: striped() }\"\r\n [attr.aria-rowcount]=\"rows().length\"\r\n [style.--ngn-table-column-count]=\"columnCount()\"\r\n [style.--ngn-table-row-height.px]=\"rowHeight()\"\r\n>\r\n <thead [ptInt]=\"this\" [ptClass]=\"'head'\">\r\n <ng-container [ngTemplateOutlet]=\"headerTemplate()\"></ng-container>\r\n </thead>\r\n\r\n <tbody\r\n [ptInt]=\"this\"\r\n [ptClass]=\"'body'\"\r\n ngn-scroller\r\n #scroller\r\n [virtual]=\"virtual()\"\r\n [items]=\"pagedRows()\"\r\n [fieldId]=\"'id'\"\r\n [itemHeight]=\"rowHeight() || 0\"\r\n [virtualPadding]=\"virtualPadding()\"\r\n [style.--ngn-table-item-start-index]=\"scroller.itemStartIndex()\"\r\n >\r\n <ng-template #item [ngnTemplate]=\"scroller.templateTypes.item\" let-item let-index=\"index\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"bodyTemplate()\"\r\n [ngTemplateOutletContext]=\"{ $implicit: item }\"\r\n ></ng-container>\r\n </ng-template>\r\n </tbody>\r\n</table>\r\n@if (paginator()) {\r\n <ngn-paginator [totalItems]=\"rows().length\" (value)=\"pageChanged($event)\" />\r\n}\r\n" }]
131
- }], propDecorators: { 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 }] }], sort: [{ type: i0.Input, args: [{ isSignal: true, alias: "sort", required: false }] }, { type: i0.Output, args: ["sortChange"] }], filters: [{ type: i0.Input, args: [{ isSignal: true, alias: "filters", required: false }] }, { type: i0.Output, args: ["filtersChange"] }] } });
886
+ '[attr.aria-multiselectable]': 'selectionMode() === "multi" ? true : null',
887
+ '(keydown)': 'onKeyDown($event)',
888
+ }, template: "<table\r\n #scrollContainer\r\n ngnScrollShadow=\"horizontal\"\r\n [ptInt]=\"this\"\r\n [ptClass]=\"{ table: true, striped: striped() }\"\r\n [attr.aria-rowcount]=\"formattedRows().length\"\r\n [style.--ngn-table-column-count]=\"totalColumnCount()\"\r\n [style.--ngn-table-selection-offset]=\"showCheckboxes() ? 1 : 0\"\r\n [style.grid-template-columns]=\"gridTemplateColumns()\"\r\n [style.--ngn-table-row-height.px]=\"rowHeight()\"\r\n>\r\n <thead [ptInt]=\"this\" [ptClass]=\"'head'\">\r\n <ng-container [ngTemplateOutlet]=\"headerTemplate()\"></ng-container>\r\n </thead>\r\n @if (_dropIndicatorState()) {\r\n <div\r\n [ptInt]=\"this\"\r\n [ptClass]=\"'drop-indicator'\"\r\n [style.left.px]=\"_dropIndicatorState()!.leftPx\"\r\n [style.top.px]=\"_dropIndicatorState()!.topPx\"\r\n [style.height.px]=\"_dropIndicatorState()!.heightPx\"\r\n ></div>\r\n }\r\n\r\n <tbody\r\n [ptInt]=\"this\"\r\n [ptClass]=\"'body'\"\r\n ngn-scroller\r\n #scroller\r\n [scrollContainer]=\"scrollContainer\"\r\n [virtual]=\"virtual()\"\r\n [items]=\"pagedRows()\"\r\n [fieldId]=\"'id'\"\r\n [itemHeight]=\"rowHeight() || 0\"\r\n [virtualPadding]=\"virtualPadding()\"\r\n [style.--ngn-table-item-start-index]=\"scroller.itemStartIndex()\"\r\n >\r\n <ng-template #item [ngnTemplate]=\"scroller.templateTypes.item\" let-item let-index=\"index\">\r\n @if (item.kind === 'group-header') {\r\n @if (groupHeaderTemplate(); as customGroupHeader) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"customGroupHeader\"\r\n [ngTemplateOutletContext]=\"{\r\n $implicit: {\r\n groupKey: item.groupKey,\r\n groupValue: item.groupValue,\r\n count: item.count,\r\n expanded: item.expanded,\r\n },\r\n }\"\r\n ></ng-container>\r\n } @else {\r\n <tr\r\n [ngnTableGroupHeaderTr]=\"item\"\r\n [class]=\"theme.classes({ 'group-expanded': item.expanded })\"\r\n [attr.aria-expanded]=\"item.expanded\"\r\n role=\"row\"\r\n (click)=\"toggleGroupFromRow(item)\"\r\n >\r\n <td [class]=\"theme.class('group-header-cell')\" role=\"gridcell\">\r\n <ngn-icon\r\n [defaultIcon]=\"'table-group-toggle'\"\r\n [class]=\"theme.class('group-toggle')\"\r\n />\r\n <span>{{ item.groupValue }}</span>\r\n <span>({{ item.count }})</span>\r\n </td>\r\n </tr>\r\n }\r\n } @else {\r\n <ng-container\r\n [ngTemplateOutlet]=\"bodyTemplate()\"\r\n [ngTemplateOutletContext]=\"{ $implicit: item }\"\r\n ></ng-container>\r\n }\r\n </ng-template>\r\n </tbody>\r\n</table>\r\n@if (paginator()) {\r\n <ngn-paginator [totalItems]=\"rows().length\" (value)=\"pageChanged($event)\" />\r\n}\r\n" }]
889
+ }], 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"] }], filters: [{ type: i0.Input, args: [{ isSignal: true, alias: "filters", required: false }] }, { type: i0.Output, args: ["filtersChange"] }] } });
890
+ // --- Auto-size helpers ---
891
+ /** Lazily created canvas context for text measurement (no DOM reflows). */
892
+ let _autoSizeCtx = null;
893
+ function autoSizeCanvasCtx() {
894
+ if (!_autoSizeCtx) {
895
+ _autoSizeCtx = document.createElement('canvas').getContext('2d');
896
+ }
897
+ return _autoSizeCtx;
898
+ }
899
+ /** Builds the CSS font shorthand from an element's computed style. */
900
+ function getCssFont(el) {
901
+ const s = getComputedStyle(el);
902
+ return `${s.fontStyle} ${s.fontWeight} ${s.fontSize} ${s.fontFamily}`;
903
+ }
132
904
 
133
905
  class NgnTableTd extends NgnBase {
134
906
  theme = this.injectThemeTemplate(tableControlTemplate);
135
- columnIndex = signal(0, ...(ngDevMode ? [{ debugName: "columnIndex" }] : []));
907
+ _table = signal(null, ...(ngDevMode ? [{ debugName: "_table" }] : /* istanbul ignore next */ []));
908
+ /** 0-based logical (DOM) index of this cell within its row. */
909
+ _logicalIndex = signal(0, ...(ngDevMode ? [{ debugName: "_logicalIndex" }] : /* istanbul ignore next */ []));
910
+ /** 1-based visual column index, remapped through the table's column order. */
911
+ _visualColumnIndex = computed(() => {
912
+ const logicalIndex = this._logicalIndex();
913
+ const table = this._table();
914
+ if (table) {
915
+ // Subtract the selection column offset so DOM index aligns with registered header cells.
916
+ const offset = table.showCheckboxes() ? 1 : 0;
917
+ return table.getVisualColumnIndex(logicalIndex - offset);
918
+ }
919
+ return logicalIndex + 1;
920
+ }, ...(ngDevMode ? [{ debugName: "_visualColumnIndex" }] : /* istanbul ignore next */ []));
921
+ _columnId = computed(() => {
922
+ const table = this._table();
923
+ if (!table)
924
+ return null;
925
+ const offset = table.showCheckboxes() ? 1 : 0;
926
+ const logicalIndex = this._logicalIndex() - offset;
927
+ const cells = table.getRegisteredHeaderCells();
928
+ return cells[logicalIndex]?.ngnTableTh() ?? null;
929
+ }, ...(ngDevMode ? [{ debugName: "_columnId" }] : /* istanbul ignore next */ []));
930
+ _stickyInfo = computed(() => {
931
+ const table = this._table();
932
+ const columnId = this._columnId();
933
+ if (!table || !columnId)
934
+ return null;
935
+ return table.getStickyInfo(columnId);
936
+ }, ...(ngDevMode ? [{ debugName: "_stickyInfo" }] : /* istanbul ignore next */ []));
136
937
  constructor() {
137
938
  super();
138
939
  this.prepareDom();
139
940
  const parent = this.element.nativeElement.parentElement?.children;
140
941
  if (parent) {
141
942
  const index = Array.from(parent).findIndex(child => child === this.element.nativeElement);
142
- this.columnIndex.set(index + 1);
943
+ this._logicalIndex.set(index);
143
944
  }
945
+ afterNextRender(() => {
946
+ const table = getNearestNgnInstance(this.element.nativeElement, NgnTable);
947
+ this._table.set(table);
948
+ });
949
+ afterRenderEffect(() => {
950
+ const info = this._stickyInfo();
951
+ const el = this.element.nativeElement;
952
+ toggleClass(el, this.theme.class('sticky-start'), info?.side === 'start');
953
+ toggleClass(el, this.theme.class('sticky-end'), info?.side === 'end');
954
+ toggleClass(el, this.theme.class('sticky-start-edge'), info?.side === 'start' && info.isEdge);
955
+ toggleClass(el, this.theme.class('sticky-end-edge'), info?.side === 'end' && info.isEdge);
956
+ if (info) {
957
+ el.style.position = 'sticky';
958
+ if (info.side === 'start') {
959
+ el.style.left = `var(--ngn-sticky-start-offset-${info.index})`;
960
+ el.style.removeProperty('right');
961
+ }
962
+ else {
963
+ el.style.right = `var(--ngn-sticky-end-offset-${info.index})`;
964
+ el.style.removeProperty('left');
965
+ }
966
+ }
967
+ else {
968
+ el.style.removeProperty('position');
969
+ el.style.removeProperty('left');
970
+ el.style.removeProperty('right');
971
+ }
972
+ });
144
973
  }
145
974
  prepareDom() {
146
975
  toggleClass(this.element.nativeElement, this.theme.class('cell'), true);
147
976
  }
148
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: NgnTableTd, deps: [], target: i0.ɵɵFactoryTarget.Directive });
149
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: NgnTableTd, isStandalone: true, selector: "[ngnTableTd]", host: { properties: { "style.--ngn-table-column-index": "columnIndex()" } }, usesInheritance: true, ngImport: i0 });
977
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: NgnTableTd, deps: [], target: i0.ɵɵFactoryTarget.Directive });
978
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.10", type: NgnTableTd, isStandalone: true, selector: "[ngnTableTd]", host: { properties: { "style.--ngn-table-column-index": "_visualColumnIndex()" } }, usesInheritance: true, ngImport: i0 });
150
979
  }
151
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: NgnTableTd, decorators: [{
980
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: NgnTableTd, decorators: [{
152
981
  type: Directive,
153
982
  args: [{
154
983
  selector: '[ngnTableTd]',
155
- host: { '[style.--ngn-table-column-index]': 'columnIndex()' },
984
+ host: { '[style.--ngn-table-column-index]': '_visualColumnIndex()' },
156
985
  }]
157
986
  }], ctorParameters: () => [] });
158
987
 
159
988
  class NgnTableTh extends NgnBase {
160
989
  theme = this.injectThemeTemplate(tableControlTemplate);
161
990
  _table;
162
- ngnTableTh = input.required(...(ngDevMode ? [{ debugName: "ngnTableTh" }] : []));
991
+ _tableSignal = signal(null, ...(ngDevMode ? [{ debugName: "_tableSignal" }] : /* istanbul ignore next */ []));
992
+ _resizeHandle;
993
+ ngnTableTh = input.required(...(ngDevMode ? [{ debugName: "ngnTableTh" }] : /* istanbul ignore next */ []));
994
+ /**
995
+ * The size of this column (e.g. '1fr', '200px', '25%').
996
+ * @default '1fr'
997
+ */
998
+ size = model('1fr', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
999
+ /**
1000
+ * The minimum size constraint for this column.
1001
+ * @default '50px'
1002
+ */
1003
+ minSize = input('50px', ...(ngDevMode ? [{ debugName: "minSize" }] : /* istanbul ignore next */ []));
1004
+ /**
1005
+ * The maximum size constraint for this column.
1006
+ * @default '100%'
1007
+ */
1008
+ maxSize = input('100%', ...(ngDevMode ? [{ debugName: "maxSize" }] : /* istanbul ignore next */ []));
1009
+ /**
1010
+ * Whether this column is resizable. Computed from the parent table's `resizable` input.
1011
+ */
1012
+ resizable = computed(() => this._table?.resizable() ?? false, ...(ngDevMode ? [{ debugName: "resizable" }] : /* istanbul ignore next */ []));
1013
+ /**
1014
+ * Whether the resize handle should be shown. Hidden for the last column in adjacent
1015
+ * and proportional modes since it is implicitly resized via the previous column's handle.
1016
+ * Push mode shows handles on all columns since each column grows independently.
1017
+ */
1018
+ _showHandle = computed(() => {
1019
+ if (!this._table?.resizable())
1020
+ return false;
1021
+ if (this._table.resizeMode() === 'push')
1022
+ return true;
1023
+ // Adjacent & proportional: hide handle on last column
1024
+ const cells = this._table.getRegisteredHeaderCells();
1025
+ return cells.indexOf(this) < cells.length - 1;
1026
+ }, ...(ngDevMode ? [{ debugName: "_showHandle" }] : /* istanbul ignore next */ []));
1027
+ /**
1028
+ * 1-based visual column index, derived from the table's column order map.
1029
+ * Falls back to registration index + 1 when no reordering is active.
1030
+ */
1031
+ _visualColumnIndex = computed(() => {
1032
+ if (!this._table)
1033
+ return undefined;
1034
+ const map = this._table.columnOrderMap();
1035
+ return map.get(this.ngnTableTh()) ?? this.getColumnIndex() + 1;
1036
+ }, ...(ngDevMode ? [{ debugName: "_visualColumnIndex" }] : /* istanbul ignore next */ []));
163
1037
  constructor() {
164
1038
  super();
165
1039
  this.prepareDom();
166
1040
  const renderer = inject(Renderer2);
167
1041
  const elRef = inject(ElementRef);
168
1042
  afterNextRender(() => {
169
- const element = renderer.createElement('div');
170
- element.classList.add(this.theme.class('spacer'));
171
- elRef.nativeElement.appendChild(element);
1043
+ // Wrap existing content in a text container for ellipsis support
1044
+ const textWrapper = renderer.createElement('span');
1045
+ textWrapper.classList.add(this.theme.class('cell-text'));
1046
+ const el = elRef.nativeElement;
1047
+ while (el.firstChild) {
1048
+ textWrapper.appendChild(el.firstChild);
1049
+ }
1050
+ el.appendChild(textWrapper);
1051
+ // Move dynamically-created controls back out of the text wrapper
1052
+ // so they participate in the header cell's flex layout (order: 2, 3)
1053
+ const filterControl = textWrapper.querySelector(`.${this.theme.class('filter-control')}`);
1054
+ if (filterControl)
1055
+ el.appendChild(filterControl);
1056
+ const sortControl = textWrapper.querySelector(`.${this.theme.class('sort-control')}`);
1057
+ if (sortControl)
1058
+ el.appendChild(sortControl);
1059
+ // Spacer div for flex layout
1060
+ const spacer = renderer.createElement('div');
1061
+ spacer.classList.add(this.theme.class('spacer'));
1062
+ elRef.nativeElement.appendChild(spacer);
1063
+ // Resize handle
1064
+ this._resizeHandle = renderer.createElement('div');
1065
+ this._resizeHandle.classList.add(this.theme.class('resize-handle'));
1066
+ this._resizeHandle.addEventListener('pointerdown', this.onResizePointerDown);
1067
+ this._resizeHandle.addEventListener('dblclick', this.onResizeDblClick);
1068
+ elRef.nativeElement.appendChild(this._resizeHandle);
1069
+ });
1070
+ // Toggle handle visibility based on mode and column position
1071
+ afterRenderEffect(() => {
1072
+ const show = this._showHandle();
1073
+ if (this._resizeHandle) {
1074
+ this._resizeHandle.style.display = show ? '' : 'none';
1075
+ }
1076
+ });
1077
+ // Sticky column classes and offsets
1078
+ afterRenderEffect(() => {
1079
+ const table = this._tableSignal();
1080
+ if (!table)
1081
+ return;
1082
+ const info = table.getStickyInfo(this.ngnTableTh());
1083
+ const el = this.element.nativeElement;
1084
+ toggleClass(el, this.theme.class('sticky-start'), info?.side === 'start');
1085
+ toggleClass(el, this.theme.class('sticky-end'), info?.side === 'end');
1086
+ toggleClass(el, this.theme.class('sticky-start-edge'), info?.side === 'start' && info.isEdge);
1087
+ toggleClass(el, this.theme.class('sticky-end-edge'), info?.side === 'end' && info.isEdge);
1088
+ if (info) {
1089
+ el.style.position = 'sticky';
1090
+ if (info.side === 'start') {
1091
+ el.style.left = `var(--ngn-sticky-start-offset-${info.index})`;
1092
+ el.style.removeProperty('right');
1093
+ }
1094
+ else {
1095
+ el.style.right = `var(--ngn-sticky-end-offset-${info.index})`;
1096
+ el.style.removeProperty('left');
1097
+ }
1098
+ }
1099
+ else {
1100
+ el.style.removeProperty('position');
1101
+ el.style.removeProperty('left');
1102
+ el.style.removeProperty('right');
1103
+ }
172
1104
  });
173
1105
  }
174
1106
  ngOnInit() {
@@ -177,44 +1109,248 @@ class NgnTableTh extends NgnBase {
177
1109
  throw new NgnError('ngnTableTh', 'ngnTableTh must be used within an NgnTable component');
178
1110
  }
179
1111
  this._table = table;
1112
+ this._tableSignal.set(table);
180
1113
  this._table.registerHeaderCell(this);
181
1114
  }
182
1115
  ngOnDestroy() {
183
1116
  this._table?.unregisterHeaderCell(this);
1117
+ this._resizeHandle?.removeEventListener('pointerdown', this.onResizePointerDown);
1118
+ this._resizeHandle?.removeEventListener('dblclick', this.onResizeDblClick);
184
1119
  }
185
1120
  prepareDom() {
186
1121
  toggleClass(this.element.nativeElement, this.theme.class('cell'), true);
1122
+ toggleClass(this.element.nativeElement, this.theme.class('resizable'), true);
1123
+ }
1124
+ onResizeDblClick = (event) => {
1125
+ if (!this._table?.resizable())
1126
+ return;
1127
+ event.preventDefault();
1128
+ event.stopPropagation();
1129
+ const columnIndex = this.getColumnIndex();
1130
+ if (columnIndex < 0)
1131
+ return;
1132
+ this._table.autoSizeColumn(columnIndex);
1133
+ };
1134
+ onResizePointerDown = (event) => {
1135
+ if (!this._table?.resizable())
1136
+ return;
1137
+ event.preventDefault();
1138
+ event.stopPropagation();
1139
+ const target = event.target;
1140
+ try {
1141
+ target.setPointerCapture(event.pointerId);
1142
+ }
1143
+ catch {
1144
+ // Ignore
1145
+ }
1146
+ const columnIndex = this.getColumnIndex();
1147
+ if (columnIndex < 0)
1148
+ return;
1149
+ this._table.startColumnResize(columnIndex, event);
1150
+ const onPointerMove = (e) => {
1151
+ this._table.dragColumnResize(columnIndex, e);
1152
+ };
1153
+ const cleanup = (cancel) => {
1154
+ this._table.endColumnResize(columnIndex, cancel);
1155
+ target.removeEventListener('pointermove', onPointerMove);
1156
+ target.removeEventListener('pointerup', onPointerUp);
1157
+ target.removeEventListener('pointercancel', onPointerCancel);
1158
+ };
1159
+ const onPointerUp = () => cleanup(false);
1160
+ const onPointerCancel = () => cleanup(true);
1161
+ target.addEventListener('pointermove', onPointerMove);
1162
+ target.addEventListener('pointerup', onPointerUp);
1163
+ target.addEventListener('pointercancel', onPointerCancel);
1164
+ };
1165
+ getColumnIndex() {
1166
+ if (!this._table)
1167
+ return -1;
1168
+ const cells = this._table.getRegisteredHeaderCells();
1169
+ return cells.indexOf(this);
187
1170
  }
188
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: NgnTableTh, deps: [], target: i0.ɵɵFactoryTarget.Directive });
189
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.0", type: NgnTableTh, isStandalone: true, selector: "[ngnTableTh]", inputs: { ngnTableTh: { classPropertyName: "ngnTableTh", publicName: "ngnTableTh", isSignal: true, isRequired: true, transformFunction: null } }, usesInheritance: true, ngImport: i0 });
1171
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: NgnTableTh, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1172
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.10", 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: { properties: { "style.--ngn-table-column-index": "_visualColumnIndex()" } }, usesInheritance: true, ngImport: i0 });
190
1173
  }
191
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: NgnTableTh, decorators: [{
1174
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: NgnTableTh, decorators: [{
192
1175
  type: Directive,
193
- args: [{ selector: '[ngnTableTh]' }]
194
- }], ctorParameters: () => [], propDecorators: { ngnTableTh: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngnTableTh", required: true }] }] } });
1176
+ args: [{
1177
+ selector: '[ngnTableTh]',
1178
+ host: {
1179
+ '[style.--ngn-table-column-index]': '_visualColumnIndex()',
1180
+ },
1181
+ }]
1182
+ }], 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 }] }] } });
195
1183
 
196
1184
  class NgnTableHeadTr extends NgnBase {
197
1185
  theme = this.injectThemeTemplate(tableControlTemplate);
1186
+ _element = inject((ElementRef));
198
1187
  constructor() {
199
1188
  super();
200
1189
  this.prepareDom();
201
1190
  }
202
1191
  prepareDom() {
203
- toggleClass(this.element.nativeElement, this.theme.class('row'), true);
1192
+ toggleClass(this._element.nativeElement, this.theme.class('row'), true);
204
1193
  }
205
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: NgnTableHeadTr, deps: [], target: i0.ɵɵFactoryTarget.Directive });
206
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: NgnTableHeadTr, isStandalone: true, selector: "[ngnTableHeadTr]", host: { properties: { "attr.aria-rowindex": "1" } }, usesInheritance: true, ngImport: i0 });
1194
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: NgnTableHeadTr, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1195
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.10", type: NgnTableHeadTr, isStandalone: true, selector: "[ngnTableHeadTr]", host: { properties: { "attr.aria-rowindex": "1" } }, usesInheritance: true, ngImport: i0 });
207
1196
  }
208
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: NgnTableHeadTr, decorators: [{
1197
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: NgnTableHeadTr, decorators: [{
209
1198
  type: Directive,
210
1199
  args: [{ selector: '[ngnTableHeadTr]', host: { '[attr.aria-rowindex]': '1' } }]
211
1200
  }], ctorParameters: () => [] });
212
1201
 
1202
+ /** Minimum distance in pixels before a drag is initiated. */
1203
+ const REORDER_DEAD_ZONE_PX = 5;
1204
+ /**
1205
+ * @category directive
1206
+ */
1207
+ class NgnTableReorderableColumn {
1208
+ theme = injectThemeTemplate(tableControlTemplate);
1209
+ _element = inject(ElementRef);
1210
+ _headerCell = inject(NgnTableTh);
1211
+ _columnId = this._headerCell.ngnTableTh;
1212
+ _table = getNearestNgnInstanceSig(this._element.nativeElement, NgnTable);
1213
+ _isDragging = false;
1214
+ _startX = 0;
1215
+ _startY = 0;
1216
+ _pointerId = null;
1217
+ _boundOnPointerMove = null;
1218
+ _boundOnPointerUp = null;
1219
+ _boundOnPointerCancel = null;
1220
+ _boundOnKeyDown = null;
1221
+ constructor() {
1222
+ afterNextRender(() => {
1223
+ const el = this._element.nativeElement;
1224
+ el.addEventListener('pointerdown', this.onPointerDown);
1225
+ });
1226
+ }
1227
+ ngOnDestroy() {
1228
+ this._element.nativeElement.removeEventListener('pointerdown', this.onPointerDown);
1229
+ this.cleanup();
1230
+ }
1231
+ onPointerDown = (event) => {
1232
+ const table = this._table();
1233
+ if (!table?.reorderable())
1234
+ return;
1235
+ // Don't intercept resize handle clicks
1236
+ const target = event.target;
1237
+ if (target.classList.contains(this.theme.class('resize-handle')))
1238
+ return;
1239
+ event.preventDefault();
1240
+ this._startX = event.clientX;
1241
+ this._startY = event.clientY;
1242
+ this._isDragging = false;
1243
+ this._pointerId = event.pointerId;
1244
+ try {
1245
+ this._element.nativeElement.setPointerCapture(event.pointerId);
1246
+ }
1247
+ catch {
1248
+ // Ignore
1249
+ }
1250
+ this._boundOnPointerMove = (e) => this.onPointerMove(e);
1251
+ this._boundOnPointerUp = () => this.onPointerUp();
1252
+ this._boundOnPointerCancel = () => this.onPointerCancel();
1253
+ this._boundOnKeyDown = (e) => this.onKeyDown(e);
1254
+ this._element.nativeElement.addEventListener('pointermove', this._boundOnPointerMove);
1255
+ this._element.nativeElement.addEventListener('pointerup', this._boundOnPointerUp);
1256
+ this._element.nativeElement.addEventListener('pointercancel', this._boundOnPointerCancel);
1257
+ document.addEventListener('keydown', this._boundOnKeyDown);
1258
+ };
1259
+ onPointerMove(event) {
1260
+ if (!this._isDragging) {
1261
+ const deltaX = event.clientX - this._startX;
1262
+ const deltaY = event.clientY - this._startY;
1263
+ const distance = Math.sqrt(deltaX ** 2 + deltaY ** 2);
1264
+ if (distance < REORDER_DEAD_ZONE_PX)
1265
+ return;
1266
+ // Start dragging
1267
+ this._isDragging = true;
1268
+ const table = this._table();
1269
+ table?.startColumnReorder(this._columnId());
1270
+ toggleClass(this._element.nativeElement, this.theme.class('drag-source'), true);
1271
+ }
1272
+ this._table()?.dragColumnReorder(event);
1273
+ }
1274
+ onPointerUp() {
1275
+ if (this._isDragging) {
1276
+ this._table()?.endColumnReorder(false);
1277
+ toggleClass(this._element.nativeElement, this.theme.class('drag-source'), false);
1278
+ }
1279
+ this.cleanup();
1280
+ }
1281
+ onPointerCancel() {
1282
+ if (this._isDragging) {
1283
+ this._table()?.endColumnReorder(true);
1284
+ toggleClass(this._element.nativeElement, this.theme.class('drag-source'), false);
1285
+ }
1286
+ this.cleanup();
1287
+ }
1288
+ onKeyDown(event) {
1289
+ if (event.key === 'Escape' && this._isDragging) {
1290
+ this._table()?.endColumnReorder(true);
1291
+ toggleClass(this._element.nativeElement, this.theme.class('drag-source'), false);
1292
+ this.cleanup();
1293
+ }
1294
+ }
1295
+ cleanup() {
1296
+ this._isDragging = false;
1297
+ if (this._pointerId != null) {
1298
+ try {
1299
+ this._element.nativeElement.releasePointerCapture(this._pointerId);
1300
+ }
1301
+ catch {
1302
+ // Ignore — capture may already be released
1303
+ }
1304
+ this._pointerId = null;
1305
+ }
1306
+ if (this._boundOnPointerMove) {
1307
+ this._element.nativeElement.removeEventListener('pointermove', this._boundOnPointerMove);
1308
+ this._boundOnPointerMove = null;
1309
+ }
1310
+ if (this._boundOnPointerUp) {
1311
+ this._element.nativeElement.removeEventListener('pointerup', this._boundOnPointerUp);
1312
+ this._boundOnPointerUp = null;
1313
+ }
1314
+ if (this._boundOnPointerCancel) {
1315
+ this._element.nativeElement.removeEventListener('pointercancel', this._boundOnPointerCancel);
1316
+ this._boundOnPointerCancel = null;
1317
+ }
1318
+ if (this._boundOnKeyDown) {
1319
+ document.removeEventListener('keydown', this._boundOnKeyDown);
1320
+ this._boundOnKeyDown = null;
1321
+ }
1322
+ }
1323
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: NgnTableReorderableColumn, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1324
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.10", type: NgnTableReorderableColumn, isStandalone: true, selector: "[ngnTableReorderableColumn]", ngImport: i0 });
1325
+ }
1326
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: NgnTableReorderableColumn, decorators: [{
1327
+ type: Directive,
1328
+ args: [{
1329
+ selector: '[ngnTableReorderableColumn]',
1330
+ }]
1331
+ }], ctorParameters: () => [] });
1332
+
213
1333
  class NgnTableBodyTr extends NgnScrollerItem {
214
- ngnTableBodyTr = input.required(...(ngDevMode ? [{ debugName: "ngnTableBodyTr" }] : []));
215
- ngnScrollerItem = input({}, ...(ngDevMode ? [{ debugName: "ngnScrollerItem" }] : []));
1334
+ ngnTableBodyTr = input.required(...(ngDevMode ? [{ debugName: "ngnTableBodyTr" }] : /* istanbul ignore next */ []));
1335
+ ngnScrollerItem = input({}, ...(ngDevMode ? [{ debugName: "ngnScrollerItem" }] : /* istanbul ignore next */ []));
216
1336
  _element = inject((ElementRef));
217
1337
  theme = injectThemeTemplate(tableControlTemplate);
1338
+ _table = getNearestNgnInstanceSig(this._element.nativeElement, NgnTable);
1339
+ selectable = computed(() => !!this._table()?.selectionMode(), ...(ngDevMode ? [{ debugName: "selectable" }] : /* istanbul ignore next */ []));
1340
+ selected = computed(() => {
1341
+ const table = this._table();
1342
+ if (!table || !table.selectionMode())
1343
+ return false;
1344
+ const row = this.ngnTableBodyTr();
1345
+ return table.isRowSelected(row.id);
1346
+ }, ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
1347
+ focused = computed(() => {
1348
+ const table = this._table();
1349
+ if (!table || !table.selectionMode())
1350
+ return false;
1351
+ const row = this.ngnTableBodyTr();
1352
+ return table.focusedRowIndex() === row.index;
1353
+ }, ...(ngDevMode ? [{ debugName: "focused" }] : /* istanbul ignore next */ []));
218
1354
  constructor() {
219
1355
  super();
220
1356
  effect(() => {
@@ -226,51 +1362,279 @@ class NgnTableBodyTr extends NgnScrollerItem {
226
1362
  prepareDom() {
227
1363
  toggleClass(this._element.nativeElement, this.theme.class('row'), true);
228
1364
  }
229
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: NgnTableBodyTr, deps: [], target: i0.ɵɵFactoryTarget.Directive });
230
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.0", 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: { properties: { "attr.aria-rowindex": "ngnTableBodyTr().index + 2", "style.--ngn-table-row-index": "ngnTableBodyTr().index + 2", "class": "theme.classes({'even': ngnTableBodyTr().index % 2 === 0})" } }, usesInheritance: true, ngImport: i0 });
1365
+ onRowClick(event) {
1366
+ const table = this._table();
1367
+ if (!table || !table.selectionMode())
1368
+ return;
1369
+ table.handleRowClick(this.ngnTableBodyTr(), event);
1370
+ }
1371
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: NgnTableBodyTr, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1372
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.10", 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 })", "attr.aria-selected": "selectable() ? selected() : null" } }, usesInheritance: true, ngImport: i0 });
231
1373
  }
232
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: NgnTableBodyTr, decorators: [{
1374
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: NgnTableBodyTr, decorators: [{
233
1375
  type: Directive,
234
1376
  args: [{
235
1377
  selector: '[ngnTableBodyTr]',
236
1378
  host: {
237
1379
  '[attr.aria-rowindex]': 'ngnTableBodyTr().index + 2',
238
1380
  '[style.--ngn-table-row-index]': 'ngnTableBodyTr().index + 2',
239
- '[class]': `theme.classes({'even': ngnTableBodyTr().index % 2 === 0})`,
1381
+ '[class]': `theme.classes({
1382
+ 'even': ngnTableBodyTr().index % 2 === 0,
1383
+ 'selected-row': selected(),
1384
+ 'focused-row': focused()
1385
+ })`,
1386
+ '[attr.aria-selected]': 'selectable() ? selected() : null',
1387
+ role: 'row',
1388
+ '(click)': 'onRowClick($event)',
240
1389
  },
241
1390
  }]
242
1391
  }], ctorParameters: () => [], propDecorators: { ngnTableBodyTr: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngnTableBodyTr", required: true }] }], ngnScrollerItem: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngnScrollerItem", required: false }] }] } });
243
1392
 
1393
+ /**
1394
+ * Directive that turns a `<th>` or `<td>` into a selection checkbox column.
1395
+ *
1396
+ * **Header usage** — renders a "select all / none" checkbox:
1397
+ * ```html
1398
+ * <th ngnTableSelectionColumn></th>
1399
+ * ```
1400
+ *
1401
+ * **Body usage** — renders a per-row checkbox (must be inside a `[ngnTableBodyTr]`):
1402
+ * ```html
1403
+ * <td ngnTableSelectionColumn></td>
1404
+ * ```
1405
+ *
1406
+ * @category directive
1407
+ */
1408
+ class NgnTableSelectionColumn {
1409
+ theme = injectThemeTemplate(tableControlTemplate);
1410
+ _element = inject(ElementRef);
1411
+ _envInjector = inject(EnvironmentInjector);
1412
+ _appRef = inject(ApplicationRef);
1413
+ _table = getNearestNgnInstanceSig(this._element.nativeElement, NgnTable);
1414
+ /** Non-null only when used inside a body row (`<td>`). */
1415
+ _bodyTr = inject(NgnTableBodyTr, { optional: true });
1416
+ _isHeader = this._element.nativeElement.tagName === 'TH';
1417
+ /**
1418
+ * NgnCheckbox created at the application level via `createComponent` + `EnvironmentInjector`.
1419
+ * This keeps it outside Angular's view tree so @for reconciliation cannot destroy it.
1420
+ */
1421
+ _checkboxRef;
1422
+ selected = computed(() => {
1423
+ if (this._isHeader)
1424
+ return false;
1425
+ const table = this._table();
1426
+ const row = this._bodyTr?.ngnTableBodyTr();
1427
+ if (!table || !row)
1428
+ return false;
1429
+ return table.isRowSelected(row.id);
1430
+ }, ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
1431
+ focused = computed(() => {
1432
+ if (this._isHeader)
1433
+ return false;
1434
+ const table = this._table();
1435
+ const row = this._bodyTr?.ngnTableBodyTr();
1436
+ if (!table || !row)
1437
+ return false;
1438
+ return table.focusedRowIndex() === row.index;
1439
+ }, ...(ngDevMode ? [{ debugName: "focused" }] : /* istanbul ignore next */ []));
1440
+ constructor() {
1441
+ this._prepareDom();
1442
+ this._createCheckbox();
1443
+ if (this._isHeader) {
1444
+ this._setupHeaderEffects();
1445
+ // Register the selection column once the table signal resolves
1446
+ effect(() => {
1447
+ const table = this._table();
1448
+ if (table) {
1449
+ table.registerSelectionColumn();
1450
+ }
1451
+ });
1452
+ }
1453
+ else {
1454
+ this._setupBodyEffects();
1455
+ }
1456
+ }
1457
+ ngOnDestroy() {
1458
+ if (this._isHeader) {
1459
+ this._table()?.unregisterSelectionColumn();
1460
+ }
1461
+ this._appRef.detachView(this._checkboxRef.hostView);
1462
+ this._checkboxRef.destroy();
1463
+ }
1464
+ _prepareDom() {
1465
+ const el = this._element.nativeElement;
1466
+ toggleClass(el, this.theme.class('cell'), true);
1467
+ toggleClass(el, this.theme.class('selection-column'), true);
1468
+ if (this._isHeader) {
1469
+ el.setAttribute('aria-label', 'Select all rows');
1470
+ }
1471
+ }
1472
+ /**
1473
+ * Creates an NgnCheckbox via `createComponent` + `EnvironmentInjector`.
1474
+ * The component is attached to `ApplicationRef` (not a ViewContainerRef),
1475
+ * so Angular's @for block reconciliation cannot destroy it.
1476
+ */
1477
+ _createCheckbox() {
1478
+ this._checkboxRef = createComponent(NgnCheckbox, {
1479
+ environmentInjector: this._envInjector,
1480
+ });
1481
+ this._appRef.attachView(this._checkboxRef.hostView);
1482
+ const cbEl = this._checkboxRef.location.nativeElement;
1483
+ cbEl.classList.add(this.theme.class('selection-checkbox'));
1484
+ if (this._isHeader) {
1485
+ setComponentInput(this._checkboxRef, 'allowIndeterminate', true);
1486
+ }
1487
+ this._element.nativeElement.appendChild(cbEl);
1488
+ }
1489
+ // --- Header (select-all) ---
1490
+ _setupHeaderEffects() {
1491
+ effect(() => {
1492
+ const table = this._table();
1493
+ if (!table)
1494
+ return;
1495
+ setComponentInput(this._checkboxRef, 'value', table.headerCheckboxValue());
1496
+ });
1497
+ }
1498
+ // --- Body (per-row) ---
1499
+ _setupBodyEffects() {
1500
+ effect(() => {
1501
+ const isSelected = this.selected();
1502
+ const isFocused = this.focused();
1503
+ const row = this._bodyTr?.ngnTableBodyTr();
1504
+ const el = this._element.nativeElement;
1505
+ setComponentInput(this._checkboxRef, 'value', isSelected);
1506
+ if (row) {
1507
+ el.style.setProperty('--ngn-table-row-index', String(row.index + 2));
1508
+ }
1509
+ toggleClass(el, this.theme.class('selected-row-cell'), isSelected);
1510
+ toggleClass(el, this.theme.class('focused-row-cell'), isFocused);
1511
+ });
1512
+ }
1513
+ // --- Click handling ---
1514
+ onClick(event) {
1515
+ event.stopPropagation();
1516
+ const table = this._table();
1517
+ if (!table)
1518
+ return;
1519
+ if (this._isHeader) {
1520
+ table.toggleSelectAll();
1521
+ }
1522
+ else {
1523
+ const row = this._bodyTr?.ngnTableBodyTr();
1524
+ if (row) {
1525
+ table.handleCheckboxChange(row);
1526
+ }
1527
+ }
1528
+ }
1529
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: NgnTableSelectionColumn, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1530
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.10", type: NgnTableSelectionColumn, isStandalone: true, selector: "[ngnTableSelectionColumn]", host: { listeners: { "click": "onClick($event)" } }, ngImport: i0 });
1531
+ }
1532
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: NgnTableSelectionColumn, decorators: [{
1533
+ type: Directive,
1534
+ args: [{
1535
+ selector: '[ngnTableSelectionColumn]',
1536
+ host: {
1537
+ '(click)': 'onClick($event)',
1538
+ },
1539
+ }]
1540
+ }], ctorParameters: () => [] });
1541
+
1542
+ class NgnTableStickyColumn {
1543
+ _element = inject(ElementRef);
1544
+ _headerCell = inject(NgnTableTh);
1545
+ _columnId = this._headerCell.ngnTableTh;
1546
+ ngnTableStickyColumn = input.required(...(ngDevMode ? [{ debugName: "ngnTableStickyColumn" }] : /* istanbul ignore next */ []));
1547
+ _table = getNearestNgnInstanceSig(this._element.nativeElement, NgnTable);
1548
+ constructor() {
1549
+ afterNextRender(() => {
1550
+ const table = this._table();
1551
+ if (table) {
1552
+ table.registerStickyColumn(this._columnId(), this.ngnTableStickyColumn());
1553
+ }
1554
+ });
1555
+ }
1556
+ ngOnDestroy() {
1557
+ this._table()?.unregisterStickyColumn(this._columnId());
1558
+ }
1559
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: NgnTableStickyColumn, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1560
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.10", type: NgnTableStickyColumn, isStandalone: true, selector: "[ngnTableStickyColumn]", inputs: { ngnTableStickyColumn: { classPropertyName: "ngnTableStickyColumn", publicName: "ngnTableStickyColumn", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
1561
+ }
1562
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: NgnTableStickyColumn, decorators: [{
1563
+ type: Directive,
1564
+ args: [{
1565
+ selector: '[ngnTableStickyColumn]',
1566
+ }]
1567
+ }], ctorParameters: () => [], propDecorators: { ngnTableStickyColumn: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngnTableStickyColumn", required: true }] }] } });
1568
+
244
1569
  class NgnTableModule {
245
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: NgnTableModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
246
- static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.1.0", ngImport: i0, type: NgnTableModule, imports: [NgnTable, NgnTableBodyTr, NgnTableTd, NgnTableTh, NgnTableHeadTr], exports: [NgnTable, NgnTableBodyTr, NgnTableTd, NgnTableTh, NgnTableHeadTr] });
247
- static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: NgnTableModule, imports: [NgnTable] });
1570
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: NgnTableModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
1571
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.10", ngImport: i0, type: NgnTableModule, imports: [NgnTable,
1572
+ NgnTableBodyTr,
1573
+ NgnTableTd,
1574
+ NgnTableTh,
1575
+ NgnTableHeadTr,
1576
+ NgnTableGroupHeaderTr,
1577
+ NgnTableReorderableColumn,
1578
+ NgnTableSelectionColumn,
1579
+ NgnTableStickyColumn], exports: [NgnTable,
1580
+ NgnTableBodyTr,
1581
+ NgnTableTd,
1582
+ NgnTableTh,
1583
+ NgnTableHeadTr,
1584
+ NgnTableGroupHeaderTr,
1585
+ NgnTableReorderableColumn,
1586
+ NgnTableSelectionColumn,
1587
+ NgnTableStickyColumn] });
1588
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: NgnTableModule, imports: [NgnTable] });
248
1589
  }
249
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: NgnTableModule, decorators: [{
1590
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: NgnTableModule, decorators: [{
250
1591
  type: NgModule,
251
1592
  args: [{
252
- imports: [NgnTable, NgnTableBodyTr, NgnTableTd, NgnTableTh, NgnTableHeadTr],
253
- exports: [NgnTable, NgnTableBodyTr, NgnTableTd, NgnTableTh, NgnTableHeadTr],
1593
+ imports: [
1594
+ NgnTable,
1595
+ NgnTableBodyTr,
1596
+ NgnTableTd,
1597
+ NgnTableTh,
1598
+ NgnTableHeadTr,
1599
+ NgnTableGroupHeaderTr,
1600
+ NgnTableReorderableColumn,
1601
+ NgnTableSelectionColumn,
1602
+ NgnTableStickyColumn,
1603
+ ],
1604
+ exports: [
1605
+ NgnTable,
1606
+ NgnTableBodyTr,
1607
+ NgnTableTd,
1608
+ NgnTableTh,
1609
+ NgnTableHeadTr,
1610
+ NgnTableGroupHeaderTr,
1611
+ NgnTableReorderableColumn,
1612
+ NgnTableSelectionColumn,
1613
+ NgnTableStickyColumn,
1614
+ ],
254
1615
  }]
255
1616
  }] });
256
1617
 
1618
+ /**
1619
+ * @category directive
1620
+ */
257
1621
  class NgnTableFilterableColumn {
258
1622
  theme = injectThemeTemplate(tableControlTemplate);
259
1623
  _element = inject(ElementRef);
260
1624
  _columnId = inject(NgnTableTh).ngnTableTh;
261
- ngnTableFilterableColumn = input(...(ngDevMode ? [undefined, { debugName: "ngnTableFilterableColumn" }] : []));
1625
+ ngnTableFilterableColumn = input(...(ngDevMode ? [undefined, { debugName: "ngnTableFilterableColumn" }] : /* istanbul ignore next */ []));
262
1626
  _ngnActionButton;
263
1627
  _ngnFilter;
264
1628
  _table = getNearestNgnInstanceSig(this._element.nativeElement, NgnTable);
265
1629
  _rows = computed(() => {
266
1630
  return this._table()?.rows() || [];
267
- }, ...(ngDevMode ? [{ debugName: "_rows" }] : []));
1631
+ }, ...(ngDevMode ? [{ debugName: "_rows" }] : /* istanbul ignore next */ []));
268
1632
  filter = computed(() => {
269
1633
  const tableFilter = this._table()?.filters()?.[this._columnId()];
270
1634
  return tableFilter;
271
- }, ...(ngDevMode ? [{ debugName: "filter" }] : []));
272
- ngnTableFilterableColumnType = input.required(...(ngDevMode ? [{ debugName: "ngnTableFilterableColumnType" }] : []));
273
- ngnTableFilterableColumnItems = input(...(ngDevMode ? [undefined, { debugName: "ngnTableFilterableColumnItems" }] : []));
1635
+ }, ...(ngDevMode ? [{ debugName: "filter" }] : /* istanbul ignore next */ []));
1636
+ ngnTableFilterableColumnType = input.required(...(ngDevMode ? [{ debugName: "ngnTableFilterableColumnType" }] : /* istanbul ignore next */ []));
1637
+ ngnTableFilterableColumnItems = input(...(ngDevMode ? [undefined, { debugName: "ngnTableFilterableColumnItems" }] : /* istanbul ignore next */ []));
274
1638
  constructor() {
275
1639
  this._ngnActionButton = inject(ViewContainerRef).createComponent((NgnActionButton));
276
1640
  this._ngnFilter = inject(ViewContainerRef).createComponent(NgnFilter);
@@ -278,6 +1642,7 @@ class NgnTableFilterableColumn {
278
1642
  this._element.nativeElement.appendChild(this._ngnFilter.location.nativeElement);
279
1643
  this._ngnActionButton.location.nativeElement.classList.add(this.theme.class('filter-control'));
280
1644
  setComponentInput(this._ngnActionButton, 'kind', 'icon');
1645
+ setComponentInput(this._ngnActionButton, 'inline', true);
281
1646
  setComponentInput(this._ngnFilter, 'anchor', this._ngnActionButton.location.nativeElement);
282
1647
  setComponentInput(this._ngnFilter, 'mode', 'headless');
283
1648
  setComponentInput(this._ngnFilter, 'allowMultiple', true);
@@ -285,12 +1650,12 @@ class NgnTableFilterableColumn {
285
1650
  label: 'Filter',
286
1651
  value: null,
287
1652
  kind: 'icon',
288
- defaultIcon: this.filter() ? 'filter_solid' : 'filter',
1653
+ defaultIcon: this.filter() ? 'filter-active' : 'filter-inactive',
289
1654
  action: event => {
290
1655
  event.stopPropagation();
291
1656
  this._ngnFilter.instance.show();
292
1657
  },
293
- }), ...(ngDevMode ? [{ debugName: "cfg" }] : []));
1658
+ }), ...(ngDevMode ? [{ debugName: "cfg" }] : /* istanbul ignore next */ []));
294
1659
  effect(() => {
295
1660
  setComponentInput(this._ngnActionButton, 'config', cfg());
296
1661
  });
@@ -313,10 +1678,10 @@ class NgnTableFilterableColumn {
313
1678
  this._ngnActionButton.destroy();
314
1679
  this._ngnFilter.destroy();
315
1680
  }
316
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: NgnTableFilterableColumn, deps: [], target: i0.ɵɵFactoryTarget.Directive });
317
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.0", 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 });
1681
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: NgnTableFilterableColumn, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1682
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.10", 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 });
318
1683
  }
319
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: NgnTableFilterableColumn, decorators: [{
1684
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: NgnTableFilterableColumn, decorators: [{
320
1685
  type: Directive,
321
1686
  args: [{
322
1687
  selector: '[ngnTableFilterableColumn]',
@@ -326,10 +1691,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
326
1691
  }]
327
1692
  }], 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 }] }] } });
328
1693
 
1694
+ /**
1695
+ * @category directive
1696
+ */
329
1697
  class NgnTableSortableColumn {
330
1698
  theme = injectThemeTemplate(tableControlTemplate);
331
1699
  _element = inject(ElementRef);
332
- ngnTableSortableColumn = input(...(ngDevMode ? [undefined, { debugName: "ngnTableSortableColumn" }] : []));
1700
+ ngnTableSortableColumn = input(...(ngDevMode ? [undefined, { debugName: "ngnTableSortableColumn" }] : /* istanbul ignore next */ []));
333
1701
  _table = getNearestNgnInstanceSig(this._element.nativeElement, NgnTable);
334
1702
  sort = computed(() => {
335
1703
  const tableSort = this._table()?.sort();
@@ -337,7 +1705,7 @@ class NgnTableSortableColumn {
337
1705
  return tableSort.direction;
338
1706
  }
339
1707
  return null;
340
- }, ...(ngDevMode ? [{ debugName: "sort" }] : []));
1708
+ }, ...(ngDevMode ? [{ debugName: "sort" }] : /* istanbul ignore next */ []));
341
1709
  columnId = inject(NgnTableTh).ngnTableTh;
342
1710
  _ngnIcon;
343
1711
  constructor() {
@@ -346,12 +1714,18 @@ class NgnTableSortableColumn {
346
1714
  this._ngnIcon.location.nativeElement.classList.add(this.theme.class('sort-control'));
347
1715
  effect(() => {
348
1716
  const sort = this.sort();
349
- setComponentInput(this._ngnIcon, 'defaultIcon', sort === 'asc' ? 'sort_down' : sort === 'desc' ? 'sort_up' : 'sort');
1717
+ setComponentInput(this._ngnIcon, 'defaultIcon', sort === 'asc' ? 'sort-ascending' : sort === 'desc' ? 'sort-descending' : 'sort-neutral');
350
1718
  });
351
1719
  }
352
1720
  ngOnDestroy() {
353
1721
  this._ngnIcon.destroy();
354
1722
  }
1723
+ onHostClick(event) {
1724
+ const target = event.target;
1725
+ if (target.classList.contains(this.theme.class('resize-handle')))
1726
+ return;
1727
+ this.toggleSort();
1728
+ }
355
1729
  toggleSort() {
356
1730
  const currentSort = this.sort();
357
1731
  const table = this._table();
@@ -361,16 +1735,16 @@ class NgnTableSortableColumn {
361
1735
  ? { column: this.columnId(), direction: 'desc' }
362
1736
  : null);
363
1737
  }
364
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: NgnTableSortableColumn, deps: [], target: i0.ɵɵFactoryTarget.Directive });
365
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.0", type: NgnTableSortableColumn, isStandalone: true, selector: "[ngnTableSortableColumn]", inputs: { ngnTableSortableColumn: { classPropertyName: "ngnTableSortableColumn", publicName: "ngnTableSortableColumn", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "click": "toggleSort()" }, properties: { "class": "theme.classes({'sortable-column': true, 'sorted-column': !!sort() })" } }, ngImport: i0 });
1738
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: NgnTableSortableColumn, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1739
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.10", 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() })" } }, ngImport: i0 });
366
1740
  }
367
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: NgnTableSortableColumn, decorators: [{
1741
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: NgnTableSortableColumn, decorators: [{
368
1742
  type: Directive,
369
1743
  args: [{
370
1744
  selector: '[ngnTableSortableColumn]',
371
1745
  host: {
372
1746
  '[class]': `theme.classes({'sortable-column': true, 'sorted-column': !!sort() })`,
373
- '(click)': `toggleSort()`,
1747
+ '(click)': `onHostClick($event)`,
374
1748
  },
375
1749
  }]
376
1750
  }], ctorParameters: () => [], propDecorators: { ngnTableSortableColumn: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngnTableSortableColumn", required: false }] }] } });
@@ -379,5 +1753,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
379
1753
  * Generated bundle index. Do not edit.
380
1754
  */
381
1755
 
382
- export { NgnTable, NgnTableBodyTr, NgnTableFilterableColumn, NgnTableHeadTr, NgnTableModule, NgnTableSortableColumn, NgnTableTd, NgnTableTh };
1756
+ export { NgnTable, NgnTableBodyTr, NgnTableFilterableColumn, NgnTableGroupHeaderTr, NgnTableHeadTr, NgnTableModule, NgnTableReorderableColumn, NgnTableSelectionColumn, NgnTableSortableColumn, NgnTableStickyColumn, NgnTableTd, NgnTableTemplates, NgnTableTh };
383
1757
  //# sourceMappingURL=ngneers-controls-table.mjs.map