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