@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.
- package/fesm2022/ngneers-controls-accordion.mjs +32 -32
- package/fesm2022/ngneers-controls-accordion.mjs.map +1 -1
- package/fesm2022/ngneers-controls-api-ng.mjs +19 -19
- package/fesm2022/ngneers-controls-api-ng.mjs.map +1 -1
- package/fesm2022/ngneers-controls-api-resize.mjs +706 -0
- package/fesm2022/ngneers-controls-api-resize.mjs.map +1 -0
- package/fesm2022/ngneers-controls-avatar.mjs +13 -13
- package/fesm2022/ngneers-controls-avatar.mjs.map +1 -1
- package/fesm2022/ngneers-controls-base.mjs +89 -64
- package/fesm2022/ngneers-controls-base.mjs.map +1 -1
- package/fesm2022/ngneers-controls-breadcrumb.mjs +18 -18
- package/fesm2022/ngneers-controls-breadcrumb.mjs.map +1 -1
- package/fesm2022/ngneers-controls-button-group.mjs +8 -8
- package/fesm2022/ngneers-controls-button-group.mjs.map +1 -1
- package/fesm2022/ngneers-controls-button.mjs +16 -10
- package/fesm2022/ngneers-controls-button.mjs.map +1 -1
- package/fesm2022/{ngneers-controls-calendar-time-C7UnIFzU.mjs → ngneers-controls-calendar-time-Bzlv9Vkn.mjs} +9 -9
- package/fesm2022/{ngneers-controls-calendar-time-C7UnIFzU.mjs.map → ngneers-controls-calendar-time-Bzlv9Vkn.mjs.map} +1 -1
- package/fesm2022/ngneers-controls-calendar.mjs +54 -54
- package/fesm2022/ngneers-controls-calendar.mjs.map +1 -1
- package/fesm2022/ngneers-controls-checkbox.mjs +9 -9
- package/fesm2022/ngneers-controls-checkbox.mjs.map +1 -1
- package/fesm2022/ngneers-controls-chip.mjs +6 -6
- package/fesm2022/ngneers-controls-chip.mjs.map +1 -1
- package/fesm2022/ngneers-controls-defer.mjs +10 -10
- package/fesm2022/ngneers-controls-defer.mjs.map +1 -1
- package/fesm2022/ngneers-controls-dialog.mjs +37 -39
- package/fesm2022/ngneers-controls-dialog.mjs.map +1 -1
- package/fesm2022/ngneers-controls-directives.mjs +45 -39
- package/fesm2022/ngneers-controls-directives.mjs.map +1 -1
- package/fesm2022/ngneers-controls-drawer.mjs +28 -28
- package/fesm2022/ngneers-controls-drawer.mjs.map +1 -1
- package/fesm2022/ngneers-controls-edit-inplace.mjs +15 -15
- package/fesm2022/ngneers-controls-edit-inplace.mjs.map +1 -1
- package/fesm2022/ngneers-controls-filter.mjs +192 -44
- package/fesm2022/ngneers-controls-filter.mjs.map +1 -1
- package/fesm2022/{ngneers-controls-i18n-de-CdTPeBiR.mjs → ngneers-controls-i18n-de-CF0xTWEX.mjs} +9 -5
- package/fesm2022/ngneers-controls-i18n-de-CF0xTWEX.mjs.map +1 -0
- package/fesm2022/{ngneers-controls-i18n-en-Cfp2pOLY.mjs → ngneers-controls-i18n-en-8B7xAjH2.mjs} +9 -5
- package/fesm2022/ngneers-controls-i18n-en-8B7xAjH2.mjs.map +1 -0
- package/fesm2022/ngneers-controls-i18n.mjs +5 -5
- package/fesm2022/ngneers-controls-i18n.mjs.map +1 -1
- package/fesm2022/ngneers-controls-icon.mjs +10 -10
- package/fesm2022/ngneers-controls-icon.mjs.map +1 -1
- package/fesm2022/ngneers-controls-inplace.mjs +18 -18
- package/fesm2022/ngneers-controls-inplace.mjs.map +1 -1
- package/fesm2022/ngneers-controls-input-field.mjs +15 -15
- package/fesm2022/ngneers-controls-input-field.mjs.map +1 -1
- package/fesm2022/ngneers-controls-input-mask.mjs +8 -8
- package/fesm2022/ngneers-controls-input-mask.mjs.map +1 -1
- package/fesm2022/ngneers-controls-input.mjs +7 -6
- package/fesm2022/ngneers-controls-input.mjs.map +1 -1
- package/fesm2022/ngneers-controls-item-view.mjs +33 -33
- package/fesm2022/ngneers-controls-item-view.mjs.map +1 -1
- package/fesm2022/ngneers-controls-list-box.mjs +31 -31
- package/fesm2022/ngneers-controls-list-box.mjs.map +1 -1
- package/fesm2022/ngneers-controls-menu.mjs +32 -32
- package/fesm2022/ngneers-controls-menu.mjs.map +1 -1
- package/fesm2022/ngneers-controls-message.mjs +4 -4
- package/fesm2022/ngneers-controls-message.mjs.map +1 -1
- package/fesm2022/ngneers-controls-paginator.mjs +12 -12
- package/fesm2022/ngneers-controls-paginator.mjs.map +1 -1
- package/fesm2022/ngneers-controls-popover.mjs +13 -13
- package/fesm2022/ngneers-controls-popover.mjs.map +1 -1
- package/fesm2022/ngneers-controls-progress.mjs +16 -16
- package/fesm2022/ngneers-controls-progress.mjs.map +1 -1
- package/fesm2022/ngneers-controls-scroller.mjs +74 -39
- package/fesm2022/ngneers-controls-scroller.mjs.map +1 -1
- package/fesm2022/ngneers-controls-select-button.mjs +6 -6
- package/fesm2022/ngneers-controls-select-button.mjs.map +1 -1
- package/fesm2022/ngneers-controls-select.mjs +46 -46
- package/fesm2022/ngneers-controls-select.mjs.map +1 -1
- package/fesm2022/ngneers-controls-slider.mjs +12 -12
- package/fesm2022/ngneers-controls-slider.mjs.map +1 -1
- package/fesm2022/ngneers-controls-spinner.mjs +7 -7
- package/fesm2022/ngneers-controls-spinner.mjs.map +1 -1
- package/fesm2022/ngneers-controls-splitter.mjs +141 -328
- package/fesm2022/ngneers-controls-splitter.mjs.map +1 -1
- package/fesm2022/ngneers-controls-switch.mjs +3 -3
- package/fesm2022/ngneers-controls-table.mjs +1520 -80
- package/fesm2022/ngneers-controls-table.mjs.map +1 -1
- package/fesm2022/ngneers-controls-tabs.mjs +40 -39
- package/fesm2022/ngneers-controls-tabs.mjs.map +1 -1
- package/fesm2022/ngneers-controls-tag.mjs +4 -4
- package/fesm2022/ngneers-controls-tag.mjs.map +1 -1
- package/fesm2022/ngneers-controls-toast.mjs +18 -18
- package/fesm2022/ngneers-controls-toast.mjs.map +1 -1
- package/fesm2022/ngneers-controls-toggle-button.mjs +9 -9
- package/fesm2022/ngneers-controls-toggle-button.mjs.map +1 -1
- package/fesm2022/ngneers-controls-tooltip.mjs +37 -45
- package/fesm2022/ngneers-controls-tooltip.mjs.map +1 -1
- package/fesm2022/ngneers-controls-utils-ng.mjs +9 -9
- package/fesm2022/ngneers-controls-utils-ng.mjs.map +1 -1
- package/package.json +14 -10
- package/types/ngneers-controls-api-ng.d.ts +2 -2
- package/types/ngneers-controls-api-resize.d.ts +318 -0
- package/types/ngneers-controls-base.d.ts +8 -0
- package/types/ngneers-controls-breadcrumb.d.ts +1 -1
- package/types/ngneers-controls-button.d.ts +13 -8
- package/types/ngneers-controls-calendar.d.ts +5 -1
- package/types/ngneers-controls-checkbox.d.ts +1 -1
- package/types/ngneers-controls-directives.d.ts +7 -1
- package/types/ngneers-controls-drawer.d.ts +4 -0
- package/types/ngneers-controls-edit-inplace.d.ts +4 -0
- package/types/ngneers-controls-filter.d.ts +53 -4
- package/types/ngneers-controls-i18n.d.ts +4 -0
- package/types/ngneers-controls-icon.d.ts +1 -1
- package/types/ngneers-controls-input-field.d.ts +4 -0
- package/types/ngneers-controls-input.d.ts +1 -1
- package/types/ngneers-controls-list-box.d.ts +5 -1
- package/types/ngneers-controls-menu.d.ts +1 -1
- package/types/ngneers-controls-paginator.d.ts +4 -0
- package/types/ngneers-controls-popover.d.ts +1 -1
- package/types/ngneers-controls-scroller.d.ts +12 -2
- package/types/ngneers-controls-select.d.ts +6 -2
- package/types/ngneers-controls-spinner.d.ts +1 -1
- package/types/ngneers-controls-splitter.d.ts +29 -59
- package/types/ngneers-controls-table.d.ts +392 -24
- package/types/ngneers-controls-toast.d.ts +1 -1
- package/types/ngneers-controls-tooltip.d.ts +1 -7
- package/fesm2022/ngneers-controls-i18n-de-CdTPeBiR.mjs.map +0 -1
- 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,
|
|
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,
|
|
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 {
|
|
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
|
-
|
|
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
|
|
57
|
+
* Types for the table templates.
|
|
23
58
|
*/
|
|
24
59
|
templateTypes = templateTypesFn();
|
|
25
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.
|
|
26
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.
|
|
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.
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
124
|
-
|
|
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.
|
|
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: [
|
|
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
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
149
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.
|
|
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.
|
|
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]': '
|
|
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
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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.
|
|
189
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.
|
|
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.
|
|
1040
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: NgnTableTh, decorators: [{
|
|
192
1041
|
type: Directive,
|
|
193
|
-
args: [{
|
|
194
|
-
|
|
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.
|
|
1058
|
+
toggleClass(this._element.nativeElement, this.theme.class('row'), true);
|
|
204
1059
|
}
|
|
205
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.
|
|
206
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.
|
|
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.
|
|
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
|
-
|
|
230
|
-
|
|
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.
|
|
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({
|
|
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.
|
|
246
|
-
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.
|
|
247
|
-
|
|
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.
|
|
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: [
|
|
253
|
-
|
|
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.
|
|
317
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.
|
|
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.
|
|
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.
|
|
365
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.
|
|
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.
|
|
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)': `
|
|
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
|