@ngneers/controls 0.0.1-pre4 → 0.0.1-pre6

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