@ornery/ui-grid-vanilla 0.1.10 → 1.0.1
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/AGENTS.md +70 -0
- package/README.md +196 -0
- package/demo/main.ts +0 -1
- package/dist/index.d.mts +582 -55
- package/dist/index.d.ts +582 -55
- package/dist/index.js +4959 -1429
- package/dist/index.mjs +5137 -1560
- package/package.json +11 -8
- package/scss-plugin.ts +65 -0
- package/src/adopt-styles.ts +39 -0
- package/src/attribute-bridge.ts +291 -0
- package/src/auto-height.test.ts +142 -0
- package/src/auto-resize.test.ts +207 -0
- package/src/cell-nav.test.ts +262 -0
- package/src/components/grid-body-cell/grid-body-cell.ts +23 -0
- package/src/components/grid-body-cell/index.ts +1 -0
- package/src/components/grid-cell-editor/grid-cell-editor.ts +104 -0
- package/src/components/grid-cell-editor/index.ts +1 -0
- package/src/components/grid-filter-cell/grid-filter-cell.scss +14 -0
- package/src/components/grid-filter-cell/grid-filter-cell.template.ts +20 -0
- package/src/components/grid-filter-cell/grid-filter-cell.ts +121 -0
- package/src/components/grid-filter-cell/index.ts +1 -0
- package/src/components/grid-group-row/grid-group-row.scss +24 -0
- package/src/components/grid-group-row/grid-group-row.template.ts +23 -0
- package/src/components/grid-group-row/grid-group-row.ts +94 -0
- package/src/components/grid-group-row/index.ts +1 -0
- package/src/components/grid-header-cell/grid-header-cell.ts +17 -0
- package/src/components/grid-header-cell/index.ts +1 -0
- package/src/components/grid-pagination/grid-pagination.scss +39 -0
- package/src/components/grid-pagination/grid-pagination.template.ts +40 -0
- package/src/components/grid-pagination/grid-pagination.ts +204 -0
- package/src/components/grid-pagination/index.ts +1 -0
- package/src/components/grid-template/grid-template.ts +166 -0
- package/src/components/grid-template/index.ts +1 -0
- package/src/events.ts +824 -0
- package/src/exporter.test.ts +186 -0
- package/src/focus.ts +379 -0
- package/src/framework-slots.ts +440 -0
- package/src/grid-controller.ts +1532 -21
- package/src/icons.ts +88 -0
- package/src/importer.test.ts +147 -0
- package/src/index.integration.test.ts +2 -2
- package/src/index.ts +9 -0
- package/src/infinite-scroll.test.ts +220 -0
- package/src/keyboard-navigation.test.ts +579 -0
- package/src/patch.ts +722 -0
- package/src/render.ts +465 -0
- package/src/row-edit.test.ts +191 -0
- package/src/row-selection.test.ts +550 -0
- package/src/save-state.test.ts +235 -0
- package/src/scss.d.ts +4 -0
- package/src/templates/index.ts +62 -0
- package/src/test-setup.ts +23 -0
- package/src/ui-grid-empty.template.ts +23 -0
- package/src/ui-grid-shell.scss +25 -0
- package/src/ui-grid-shell.template.ts +45 -0
- package/src/ui-grid-standalone.element.ts +751 -1112
- package/src/utils/cell-class.ts +70 -0
- package/src/utils/display-items.ts +15 -0
- package/src/utils/dom.ts +64 -0
- package/src/validate.test.ts +170 -0
- package/tsup.config.ts +10 -0
- package/vite.config.ts +10 -1
|
@@ -1,47 +1,105 @@
|
|
|
1
1
|
import {
|
|
2
|
-
GRID_CORE_CSS,
|
|
3
|
-
SORT_DIRECTIONS,
|
|
4
|
-
buildGridHeaderContext,
|
|
5
2
|
canGridMoveColumns,
|
|
6
|
-
|
|
7
|
-
exportCsvRows,
|
|
8
|
-
findNextGridCell,
|
|
9
|
-
formatGridHeaderDisplayValue,
|
|
10
|
-
getCellValue,
|
|
11
|
-
isGridNavigationKey,
|
|
12
|
-
isPrintableGridKey,
|
|
13
|
-
sanitizeDownloadFilename,
|
|
3
|
+
interpolateGridTemplate,
|
|
14
4
|
type DisplayItem,
|
|
15
5
|
type GridColumnDef,
|
|
16
6
|
type GridOptions,
|
|
17
7
|
type GridRecord,
|
|
18
|
-
type GroupItem,
|
|
19
8
|
type GridRow,
|
|
9
|
+
type GroupItem,
|
|
20
10
|
} from '@ornery/ui-grid-core';
|
|
11
|
+
import {
|
|
12
|
+
FrameworkSlotBridge,
|
|
13
|
+
type FrameworkRenderedSlotsConfig,
|
|
14
|
+
} from './framework-slots';
|
|
15
|
+
export type {
|
|
16
|
+
FrameworkRenderedSlotsConfig,
|
|
17
|
+
FrameworkCellSlot,
|
|
18
|
+
FrameworkHeaderSlot,
|
|
19
|
+
FrameworkFilterSlot,
|
|
20
|
+
FrameworkGroupRowSlot,
|
|
21
|
+
FrameworkExpandableRowSlot,
|
|
22
|
+
FrameworkEmptyStateSlot,
|
|
23
|
+
FrameworkSlotDelta,
|
|
24
|
+
} from './framework-slots';
|
|
25
|
+
import {
|
|
26
|
+
OBSERVED_GRID_ATTRIBUTES,
|
|
27
|
+
parseGridAttributeOptions,
|
|
28
|
+
} from './attribute-bridge';
|
|
29
|
+
import {
|
|
30
|
+
getTemplateMarkup as getTemplateMarkupFromLightDom,
|
|
31
|
+
renderBodyCell,
|
|
32
|
+
renderCellTemplate,
|
|
33
|
+
renderCellTemplateFromMarkup,
|
|
34
|
+
renderDisplayItem,
|
|
35
|
+
renderEmptyState,
|
|
36
|
+
renderExpandableTemplate,
|
|
37
|
+
renderFilterCell,
|
|
38
|
+
renderHeaderCell,
|
|
39
|
+
renderPagination,
|
|
40
|
+
renderSlotRegistry,
|
|
41
|
+
} from './render';
|
|
42
|
+
import {
|
|
43
|
+
fingerprintItems,
|
|
44
|
+
patchBodyCell,
|
|
45
|
+
patchCells,
|
|
46
|
+
patchExistingRows,
|
|
47
|
+
patchFilterCells,
|
|
48
|
+
patchGroupRow,
|
|
49
|
+
patchPagination,
|
|
50
|
+
reconcileBodyRoot,
|
|
51
|
+
reconcilePagination,
|
|
52
|
+
renderPatch,
|
|
53
|
+
} from './patch';
|
|
54
|
+
import {
|
|
55
|
+
applyFocusedCellClass,
|
|
56
|
+
commitAndMove,
|
|
57
|
+
focusCellElement,
|
|
58
|
+
handleRowHeaderCheckboxClick,
|
|
59
|
+
handleRowSelectionClick,
|
|
60
|
+
matchesKeyOverride,
|
|
61
|
+
measureAutoColumnWidth,
|
|
62
|
+
moveGridFocus,
|
|
63
|
+
scrollFocusedRowIntoView,
|
|
64
|
+
type GridMoveDirection,
|
|
65
|
+
} from './focus';
|
|
66
|
+
import {
|
|
67
|
+
bindEvents,
|
|
68
|
+
maybeTriggerInfiniteScroll,
|
|
69
|
+
observeTemplateSlots,
|
|
70
|
+
syncHeaderHorizontalScroll,
|
|
71
|
+
} from './events';
|
|
72
|
+
import { bodyCellClass } from './utils/cell-class';
|
|
73
|
+
import { asGroupItem, isRowItem } from './utils/display-items';
|
|
74
|
+
import { escapeHtml } from './utils/dom';
|
|
75
|
+
import {
|
|
76
|
+
IconRegistry,
|
|
77
|
+
type UiGridControlIconKey,
|
|
78
|
+
type UiGridIconDefinition,
|
|
79
|
+
type UiGridIconOverrides,
|
|
80
|
+
} from './icons';
|
|
81
|
+
export type { UiGridControlIconKey, UiGridIconDefinition, UiGridIconOverrides } from './icons';
|
|
21
82
|
import {
|
|
22
83
|
createVanillaGridController,
|
|
23
84
|
type GridControllerSnapshot,
|
|
24
85
|
type GridSaveState,
|
|
25
86
|
type VanillaGridController,
|
|
26
87
|
} from './grid-controller';
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function isRowItem(item: DisplayItem): item is DisplayItem & { kind: 'row'; row: GridRow } {
|
|
43
|
-
return item.kind === 'row';
|
|
44
|
-
}
|
|
88
|
+
import emptyTemplate from './ui-grid-empty.template';
|
|
89
|
+
import gridShellTemplate from './ui-grid-shell.template';
|
|
90
|
+
import {
|
|
91
|
+
bodyStaticMarkup,
|
|
92
|
+
bodyVirtualMarkup,
|
|
93
|
+
emptyDataMarkup,
|
|
94
|
+
filterRowMarkup,
|
|
95
|
+
} from './templates';
|
|
96
|
+
import { UIGridFilterCell } from './components/grid-filter-cell';
|
|
97
|
+
import { UIGridGroupRow } from './components/grid-group-row';
|
|
98
|
+
import { UIGridPagination } from './components/grid-pagination';
|
|
99
|
+
import { UIGridBodyCell } from './components/grid-body-cell';
|
|
100
|
+
import { UIGridHeaderCell } from './components/grid-header-cell';
|
|
101
|
+
import { UIGridTemplate } from './components/grid-template';
|
|
102
|
+
import { UIGridCellEditor } from './components/grid-cell-editor';
|
|
45
103
|
|
|
46
104
|
export type VanillaUiGridElement = HTMLElement & {
|
|
47
105
|
options: GridOptions;
|
|
@@ -55,123 +113,71 @@ export type VanillaUiGridElement = HTMLElement & {
|
|
|
55
113
|
setData(rows: GridRecord[]): void;
|
|
56
114
|
};
|
|
57
115
|
|
|
58
|
-
export type UiGridControlIconKey =
|
|
59
|
-
| 'sortNone'
|
|
60
|
-
| 'sortAsc'
|
|
61
|
-
| 'sortDesc'
|
|
62
|
-
| 'group'
|
|
63
|
-
| 'groupExpanded'
|
|
64
|
-
| 'groupCollapsed'
|
|
65
|
-
| 'treeExpanded'
|
|
66
|
-
| 'treeCollapsed'
|
|
67
|
-
| 'expandExpanded'
|
|
68
|
-
| 'expandCollapsed'
|
|
69
|
-
| 'pin'
|
|
70
|
-
| 'pinLeft'
|
|
71
|
-
| 'pinRight'
|
|
72
|
-
| 'paginationPrev'
|
|
73
|
-
| 'paginationNext';
|
|
74
|
-
|
|
75
|
-
export interface UiGridIconDefinition {
|
|
76
|
-
viewBox?: string;
|
|
77
|
-
path: string;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export type UiGridIconOverrides = Partial<Record<UiGridControlIconKey, UiGridIconDefinition>>;
|
|
81
|
-
|
|
82
116
|
type VanillaGridOptions = GridOptions & {
|
|
83
117
|
iconOverrides?: UiGridIconOverrides;
|
|
84
118
|
};
|
|
85
119
|
|
|
86
|
-
const DEFAULT_ICONS: Record<UiGridControlIconKey, UiGridIconDefinition> = {
|
|
87
|
-
sortNone: { path: 'M7 6h10v2H7V6Zm0 5h7v2H7v-2Zm0 5h4v2H7v-2Z' },
|
|
88
|
-
sortAsc: { path: 'M12 5l-6 6h4v8h4v-8h4z' },
|
|
89
|
-
sortDesc: { path: 'M12 19l6-6h-4V5h-4v8H6z' },
|
|
90
|
-
group: { path: 'M4 6h8v4H4V6Zm0 8h8v4H4v-4Zm10-8h6v4h-6V6Zm0 8h6v4h-6v-4Z' },
|
|
91
|
-
groupExpanded: { path: 'M7 10l5 5 5-5z' },
|
|
92
|
-
groupCollapsed: { path: 'M10 7l5 5-5 5z' },
|
|
93
|
-
treeExpanded: { path: 'M7 10l5 5 5-5z' },
|
|
94
|
-
treeCollapsed: { path: 'M10 7l5 5-5 5z' },
|
|
95
|
-
expandExpanded: { path: 'M7 10l5 5 5-5z' },
|
|
96
|
-
expandCollapsed: { path: 'M10 7l5 5-5 5z' },
|
|
97
|
-
pin: { path: 'M16 12V4h1V2H7v2h1v8l-2 2v2h5v6l1 1 1-1v-6h5v-2l-2-2z' },
|
|
98
|
-
pinLeft: { path: 'M10 6 4 12l6 6v-4h10v-4H10V6z' },
|
|
99
|
-
pinRight: { path: 'M14 6v4H4v4h10v4l6-6-6-6z' },
|
|
100
|
-
paginationPrev: { path: 'M15.41 7.41 14 6l-6 6 6 6 1.41-1.41L10.83 12z' },
|
|
101
|
-
paginationNext: { path: 'M8.59 16.59 10 18l6-6-6-6-1.41 1.41L13.17 12z' },
|
|
102
|
-
};
|
|
103
|
-
|
|
104
120
|
export class UiGridStandaloneElement extends HTMLElement {
|
|
105
|
-
|
|
106
|
-
private
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
121
|
+
// Internal fields. Marked `public` (no access modifier) rather than
|
|
122
|
+
// `private` because the render / patch / events / focus modules live in
|
|
123
|
+
// their own files and read this state directly. TypeScript has no real
|
|
124
|
+
// "module-internal" visibility, so we use `@internal` JSDoc to flag
|
|
125
|
+
// fields that aren't part of the public API. Consumers of the web
|
|
126
|
+
// component see only `options`, `controlIcons`, `setFrameworkRenderedSlots`,
|
|
127
|
+
// and the template-facing strings (`gridTitle`, etc.).
|
|
128
|
+
/** @internal */ controller: VanillaGridController | null = null;
|
|
129
|
+
/** @internal */ snapshot: GridControllerSnapshot | null = null;
|
|
130
|
+
/** @internal */ unsubscribe: (() => void) | null = null;
|
|
131
|
+
/** @internal */ activeOptions: VanillaGridOptions | null = null;
|
|
132
|
+
/** @internal */ attributeOptions: Partial<GridOptions> = {};
|
|
133
|
+
/** @internal */ attributeSyncScheduled = false;
|
|
134
|
+
/** @internal Owns the SVG-icon registry: defaults merged with consumer
|
|
135
|
+
* overrides plus rendering helpers. See `./icons.ts`. */
|
|
136
|
+
icons = new IconRegistry();
|
|
137
|
+
/** @internal */ templateObserver: MutationObserver | null = null;
|
|
138
|
+
/** @internal */ openPinMenuColumn: string | null = null;
|
|
139
|
+
/** @internal */ focusedCell: { rowId: string; columnName: string } | null = null;
|
|
140
|
+
/** @internal */ draggedColumnName: string | null = null;
|
|
141
|
+
/** @internal */ dropTargetColumnName: string | null = null;
|
|
142
|
+
/** @internal */ scrollPosition = 0;
|
|
143
|
+
/** @internal */ horizontalScrollPosition = 0;
|
|
144
|
+
/** @internal */ scrollFrame: number | null = null;
|
|
145
|
+
/** @internal */ suppressScrollEvent = false;
|
|
146
|
+
/** @internal */ lastVirtualStartIndex = -1;
|
|
147
|
+
/** @internal */ measuredHeaderStickyHeight = 0;
|
|
148
|
+
/** @internal */ measuredFilterStickyHeight = 0;
|
|
149
|
+
/** @internal */ stickyHeightRelayoutQueued = false;
|
|
150
|
+
/** @internal */ benchmarkAverage = '—';
|
|
151
|
+
/** @internal */ skipNextRender = false;
|
|
152
|
+
/** @internal */ autoResizeObserver: ResizeObserver | null = null;
|
|
153
|
+
/** @internal */ autoResizeDebounceHandle: number | null = null;
|
|
154
|
+
|
|
155
|
+
// Template-facing properties for the grid shell template
|
|
156
|
+
gridTitle = 'Data grid';
|
|
157
|
+
gridTableStyle = '';
|
|
158
|
+
bodyViewportStyle = '';
|
|
159
|
+
templateColumns = '';
|
|
160
|
+
slotRegistry = '';
|
|
161
|
+
headerContent = '';
|
|
162
|
+
filterRowContent = '';
|
|
163
|
+
bodyContent = '';
|
|
164
|
+
paginationContent = '';
|
|
165
|
+
/** @internal */ dataFrame: number | null = null;
|
|
166
|
+
/** @internal */ pendingPatchedRowIds: Set<string> | null = null;
|
|
167
|
+
/** @internal */ pendingDataRefreshMode: 'patch' | 'virtual' | 'full' | null = null;
|
|
168
|
+
/** @internal */ lastScrollActivityAt = 0;
|
|
169
|
+
/** @internal */ lastStructureKey: string | null = null;
|
|
170
|
+
/** @internal */ lastItemsFingerprint: string | null = null;
|
|
171
|
+
/** @internal */ lastVirtualOffset = 0;
|
|
172
|
+
/** @internal */ lastTotalVirtualHeight = 0;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @internal Framework-rendered slot bridge. See `./framework-slots.ts`.
|
|
176
|
+
*/
|
|
177
|
+
frameworkSlots = new FrameworkSlotBridge(this);
|
|
131
178
|
|
|
132
179
|
static get observedAttributes(): string[] {
|
|
133
|
-
return [
|
|
134
|
-
// Scalar attributes
|
|
135
|
-
'grid-id',
|
|
136
|
-
'title',
|
|
137
|
-
'row-height',
|
|
138
|
-
'header-row-height',
|
|
139
|
-
'viewport-height',
|
|
140
|
-
'pagination-page-size',
|
|
141
|
-
'pagination-current-page',
|
|
142
|
-
'total-items',
|
|
143
|
-
'virtualization-threshold',
|
|
144
|
-
'tree-children-field',
|
|
145
|
-
'tree-indent',
|
|
146
|
-
'expandable-row-height',
|
|
147
|
-
'expandable-row-header-width',
|
|
148
|
-
'empty-message',
|
|
149
|
-
'infinite-scroll-rows-from-end',
|
|
150
|
-
// JSON attributes
|
|
151
|
-
'column-defs',
|
|
152
|
-
'data',
|
|
153
|
-
'grouping',
|
|
154
|
-
'pagination-page-sizes',
|
|
155
|
-
// Boolean flags
|
|
156
|
-
'enable-sorting',
|
|
157
|
-
'enable-filtering',
|
|
158
|
-
'enable-grouping',
|
|
159
|
-
'enable-pinning',
|
|
160
|
-
'enable-column-moving',
|
|
161
|
-
'enable-cell-edit',
|
|
162
|
-
'enable-cell-edit-on-focus',
|
|
163
|
-
'enable-pagination',
|
|
164
|
-
'enable-pagination-controls',
|
|
165
|
-
'use-external-pagination',
|
|
166
|
-
'enable-expandable',
|
|
167
|
-
'enable-tree-view',
|
|
168
|
-
'show-tree-expand-no-children',
|
|
169
|
-
'tree-row-header-always-visible',
|
|
170
|
-
'enable-auto-resize',
|
|
171
|
-
'enable-virtualization',
|
|
172
|
-
'infinite-scroll-up',
|
|
173
|
-
'infinite-scroll-down',
|
|
174
|
-
];
|
|
180
|
+
return [...OBSERVED_GRID_ATTRIBUTES];
|
|
175
181
|
}
|
|
176
182
|
|
|
177
183
|
get options(): GridOptions {
|
|
@@ -189,10 +195,36 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
189
195
|
|
|
190
196
|
set options(value: GridOptions) {
|
|
191
197
|
this.activeOptions = value as VanillaGridOptions;
|
|
192
|
-
this.
|
|
198
|
+
this.icons.setOverrides(this.activeOptions.iconOverrides ?? {});
|
|
193
199
|
this.ensureController(this.buildEffectiveOptions(this.activeOptions));
|
|
194
200
|
}
|
|
195
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Declare which slots are framework-rendered. When a slot is flagged, the
|
|
204
|
+
* element renders a `<slot name="…">` placeholder at the usual site instead
|
|
205
|
+
* of running its string-interpolation template, and dispatches
|
|
206
|
+
* `*SlotsChanged` events so a wrapper can insert light-DOM content with a
|
|
207
|
+
* matching `slot` attribute.
|
|
208
|
+
*
|
|
209
|
+
* Pass an empty object (or `{}`) to revert every slot kind back to the
|
|
210
|
+
* default interpolation path. Individual fields behave the same way:
|
|
211
|
+
* omitting `cells` means the cells list is unchanged; passing
|
|
212
|
+
* `cells: []` clears it.
|
|
213
|
+
*
|
|
214
|
+
* This is the bridge the Angular / React wrappers use to project native
|
|
215
|
+
* templates (ng-template, JSX render props) into the grid. Consumers of
|
|
216
|
+
* the web component directly can ignore this method — the default
|
|
217
|
+
* `<template slot="cell-columnName">` workflow still works.
|
|
218
|
+
*/
|
|
219
|
+
setFrameworkRenderedSlots(config: FrameworkRenderedSlotsConfig): void {
|
|
220
|
+
if (this.frameworkSlots.configure(config)) {
|
|
221
|
+
// The set of slot placeholders changed — force a structural re-render
|
|
222
|
+
// so framework-rendered slots transition to `<slot>` nodes (or the
|
|
223
|
+
// reverse, when a slot is revoked).
|
|
224
|
+
this.render();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
196
228
|
attributeChangedCallback(
|
|
197
229
|
_name: string,
|
|
198
230
|
_oldValue: string | null,
|
|
@@ -209,202 +241,47 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
209
241
|
}
|
|
210
242
|
|
|
211
243
|
private syncAttributesToOptions(): void {
|
|
212
|
-
this.attributeOptions =
|
|
213
|
-
|
|
214
|
-
// Scalar string attributes
|
|
215
|
-
const gridId = this.getAttribute('grid-id');
|
|
216
|
-
if (gridId !== null) this.attributeOptions.id = gridId;
|
|
217
|
-
|
|
218
|
-
const title = this.getAttribute('title');
|
|
219
|
-
if (title !== null) this.attributeOptions.title = title;
|
|
220
|
-
|
|
221
|
-
const emptyMessage = this.getAttribute('empty-message');
|
|
222
|
-
if (emptyMessage !== null) this.attributeOptions.emptyMessage = emptyMessage;
|
|
223
|
-
|
|
224
|
-
const treeChildrenField = this.getAttribute('tree-children-field');
|
|
225
|
-
if (treeChildrenField !== null) this.attributeOptions.treeChildrenField = treeChildrenField;
|
|
226
|
-
|
|
227
|
-
// Scalar number attributes
|
|
228
|
-
const rowHeight = this.parseNumberAttribute('row-height');
|
|
229
|
-
if (rowHeight !== undefined) this.attributeOptions.rowHeight = rowHeight;
|
|
230
|
-
|
|
231
|
-
const headerRowHeight = this.parseNumberAttribute('header-row-height');
|
|
232
|
-
if (headerRowHeight !== undefined)
|
|
233
|
-
this.attributeOptions.headerRowHeight = headerRowHeight;
|
|
234
|
-
|
|
235
|
-
const viewportHeight = this.parseNumberAttribute('viewport-height');
|
|
236
|
-
if (viewportHeight !== undefined) this.attributeOptions.viewportHeight = viewportHeight;
|
|
237
|
-
|
|
238
|
-
const paginationPageSize = this.parseNumberAttribute('pagination-page-size');
|
|
239
|
-
if (paginationPageSize !== undefined)
|
|
240
|
-
this.attributeOptions.paginationPageSize = paginationPageSize;
|
|
241
|
-
|
|
242
|
-
const paginationCurrentPage = this.parseNumberAttribute('pagination-current-page');
|
|
243
|
-
if (paginationCurrentPage !== undefined)
|
|
244
|
-
this.attributeOptions.paginationCurrentPage = paginationCurrentPage;
|
|
245
|
-
|
|
246
|
-
const totalItems = this.parseNumberAttribute('total-items');
|
|
247
|
-
if (totalItems !== undefined) this.attributeOptions.totalItems = totalItems;
|
|
248
|
-
|
|
249
|
-
const virtualizationThreshold = this.parseNumberAttribute('virtualization-threshold');
|
|
250
|
-
if (virtualizationThreshold !== undefined)
|
|
251
|
-
this.attributeOptions.virtualizationThreshold = virtualizationThreshold;
|
|
252
|
-
|
|
253
|
-
const treeIndent = this.parseNumberAttribute('tree-indent');
|
|
254
|
-
if (treeIndent !== undefined) this.attributeOptions.treeIndent = treeIndent;
|
|
255
|
-
|
|
256
|
-
const expandableRowHeight = this.parseNumberAttribute('expandable-row-height');
|
|
257
|
-
if (expandableRowHeight !== undefined)
|
|
258
|
-
this.attributeOptions.expandableRowHeight = expandableRowHeight;
|
|
259
|
-
|
|
260
|
-
const expandableRowHeaderWidth = this.parseNumberAttribute(
|
|
261
|
-
'expandable-row-header-width',
|
|
262
|
-
);
|
|
263
|
-
if (expandableRowHeaderWidth !== undefined)
|
|
264
|
-
this.attributeOptions.expandableRowHeaderWidth = expandableRowHeaderWidth;
|
|
265
|
-
|
|
266
|
-
const infiniteScrollRowsFromEnd = this.parseNumberAttribute(
|
|
267
|
-
'infinite-scroll-rows-from-end',
|
|
268
|
-
);
|
|
269
|
-
if (infiniteScrollRowsFromEnd !== undefined)
|
|
270
|
-
this.attributeOptions.infiniteScrollRowsFromEnd = infiniteScrollRowsFromEnd;
|
|
271
|
-
|
|
272
|
-
// Boolean attributes
|
|
273
|
-
const enableSorting = this.parseBooleanAttribute('enable-sorting');
|
|
274
|
-
if (enableSorting !== undefined) this.attributeOptions.enableSorting = enableSorting;
|
|
275
|
-
|
|
276
|
-
const enableFiltering = this.parseBooleanAttribute('enable-filtering');
|
|
277
|
-
if (enableFiltering !== undefined) this.attributeOptions.enableFiltering = enableFiltering;
|
|
278
|
-
|
|
279
|
-
const enableGrouping = this.parseBooleanAttribute('enable-grouping');
|
|
280
|
-
if (enableGrouping !== undefined) this.attributeOptions.enableGrouping = enableGrouping;
|
|
281
|
-
|
|
282
|
-
const enablePinning = this.parseBooleanAttribute('enable-pinning');
|
|
283
|
-
if (enablePinning !== undefined) this.attributeOptions.enablePinning = enablePinning;
|
|
284
|
-
|
|
285
|
-
const enableColumnMoving = this.parseBooleanAttribute('enable-column-moving');
|
|
286
|
-
if (enableColumnMoving !== undefined)
|
|
287
|
-
this.attributeOptions.enableColumnMoving = enableColumnMoving;
|
|
288
|
-
|
|
289
|
-
const enableColumnResizing = this.parseBooleanAttribute('enable-column-resizing');
|
|
290
|
-
if (enableColumnResizing !== undefined)
|
|
291
|
-
this.attributeOptions.enableColumnResizing = enableColumnResizing;
|
|
292
|
-
|
|
293
|
-
const enableCellEdit = this.parseBooleanAttribute('enable-cell-edit');
|
|
294
|
-
if (enableCellEdit !== undefined) this.attributeOptions.enableCellEdit = enableCellEdit;
|
|
295
|
-
|
|
296
|
-
const enableCellEditOnFocus = this.parseBooleanAttribute('enable-cell-edit-on-focus');
|
|
297
|
-
if (enableCellEditOnFocus !== undefined)
|
|
298
|
-
this.attributeOptions.enableCellEditOnFocus = enableCellEditOnFocus;
|
|
299
|
-
|
|
300
|
-
const enablePagination = this.parseBooleanAttribute('enable-pagination');
|
|
301
|
-
if (enablePagination !== undefined)
|
|
302
|
-
this.attributeOptions.enablePagination = enablePagination;
|
|
303
|
-
|
|
304
|
-
const enablePaginationControls = this.parseBooleanAttribute(
|
|
305
|
-
'enable-pagination-controls',
|
|
306
|
-
);
|
|
307
|
-
if (enablePaginationControls !== undefined)
|
|
308
|
-
this.attributeOptions.enablePaginationControls = enablePaginationControls;
|
|
309
|
-
|
|
310
|
-
const useExternalPagination = this.parseBooleanAttribute('use-external-pagination');
|
|
311
|
-
if (useExternalPagination !== undefined)
|
|
312
|
-
this.attributeOptions.useExternalPagination = useExternalPagination;
|
|
313
|
-
|
|
314
|
-
const enableExpandable = this.parseBooleanAttribute('enable-expandable');
|
|
315
|
-
if (enableExpandable !== undefined) this.attributeOptions.enableExpandable = enableExpandable;
|
|
244
|
+
this.attributeOptions = parseGridAttributeOptions(this);
|
|
316
245
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
);
|
|
323
|
-
if (showTreeExpandNoChildren !== undefined)
|
|
324
|
-
this.attributeOptions.showTreeExpandNoChildren = showTreeExpandNoChildren;
|
|
325
|
-
|
|
326
|
-
const treeRowHeaderAlwaysVisible = this.parseBooleanAttribute(
|
|
327
|
-
'tree-row-header-always-visible',
|
|
328
|
-
);
|
|
329
|
-
if (treeRowHeaderAlwaysVisible !== undefined)
|
|
330
|
-
this.attributeOptions.treeRowHeaderAlwaysVisible = treeRowHeaderAlwaysVisible;
|
|
331
|
-
|
|
332
|
-
const enableAutoResize = this.parseBooleanAttribute('enable-auto-resize');
|
|
333
|
-
if (enableAutoResize !== undefined) this.attributeOptions.enableAutoResize = enableAutoResize;
|
|
334
|
-
|
|
335
|
-
const enableVirtualization = this.parseBooleanAttribute('enable-virtualization');
|
|
336
|
-
if (enableVirtualization !== undefined)
|
|
337
|
-
this.attributeOptions.enableVirtualization = enableVirtualization;
|
|
338
|
-
|
|
339
|
-
const infiniteScrollUp = this.parseBooleanAttribute('infinite-scroll-up');
|
|
340
|
-
if (infiniteScrollUp !== undefined) this.attributeOptions.infiniteScrollUp = infiniteScrollUp;
|
|
341
|
-
|
|
342
|
-
const infiniteScrollDown = this.parseBooleanAttribute('infinite-scroll-down');
|
|
343
|
-
if (infiniteScrollDown !== undefined)
|
|
344
|
-
this.attributeOptions.infiniteScrollDown = infiniteScrollDown;
|
|
345
|
-
|
|
346
|
-
// JSON attributes
|
|
347
|
-
const columnDefs = this.parseJsonAttribute<GridColumnDef[]>('column-defs');
|
|
348
|
-
if (columnDefs !== undefined) this.attributeOptions.columnDefs = columnDefs;
|
|
349
|
-
|
|
350
|
-
const data = this.parseJsonAttribute<GridRecord[]>('data');
|
|
351
|
-
if (data !== undefined) this.attributeOptions.data = data;
|
|
352
|
-
|
|
353
|
-
const grouping = this.parseJsonAttribute('grouping');
|
|
354
|
-
if (grouping !== undefined && grouping !== null)
|
|
355
|
-
this.attributeOptions.grouping = grouping;
|
|
356
|
-
|
|
357
|
-
const paginationPageSizes = this.parseJsonAttribute<number[] | null>(
|
|
358
|
-
'pagination-page-sizes',
|
|
359
|
-
);
|
|
360
|
-
if (paginationPageSizes !== undefined)
|
|
361
|
-
this.attributeOptions.paginationPageSizes = paginationPageSizes;
|
|
362
|
-
|
|
363
|
-
// Re-render with the merged options.
|
|
246
|
+
// Re-render with the merged options. Both paths funnel through
|
|
247
|
+
// buildEffectiveOptions so derived fields (e.g. the default
|
|
248
|
+
// `expandableRowTemplate` when `enableExpandable` is true) are applied
|
|
249
|
+
// regardless of whether options came from an imperative `grid.options =`
|
|
250
|
+
// assignment or purely from HTML attributes.
|
|
364
251
|
if (this.activeOptions !== null) {
|
|
365
252
|
this.ensureController(this.buildEffectiveOptions(this.activeOptions));
|
|
366
253
|
} else {
|
|
367
|
-
// Purely declarative path:
|
|
368
|
-
this.
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const raw = this.getAttribute(name);
|
|
383
|
-
if (raw === null) return undefined;
|
|
384
|
-
const parsed = Number(raw);
|
|
385
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
private parseJsonAttribute<T = unknown>(name: string): T | undefined {
|
|
389
|
-
const raw = this.getAttribute(name);
|
|
390
|
-
if (raw === null) return undefined;
|
|
391
|
-
try {
|
|
392
|
-
return JSON.parse(raw) as T;
|
|
393
|
-
} catch (e) {
|
|
394
|
-
console.warn(`<ui-grid-element>: invalid JSON in "${name}" attribute`, e);
|
|
395
|
-
return undefined;
|
|
254
|
+
// Purely declarative path: the attribute-only options are already
|
|
255
|
+
// populated on `this.attributeOptions` (including `data` / `columnDefs`
|
|
256
|
+
// when the consumer set `[attr.data]` / `[attr.column-defs]`). Pass
|
|
257
|
+
// the minimum identity so `buildEffectiveOptions`'s merge doesn't
|
|
258
|
+
// clobber attribute-provided arrays with empty fallbacks — the
|
|
259
|
+
// attribute values always win because the spread order is
|
|
260
|
+
// `{ ...attributeOptions, ...options }`.
|
|
261
|
+
const fallbackId = this.attributeOptions.id ?? '__ui-grid-pending__';
|
|
262
|
+
this.ensureController(
|
|
263
|
+
this.buildEffectiveOptions({
|
|
264
|
+
id: fallbackId,
|
|
265
|
+
data: this.attributeOptions.data ?? [],
|
|
266
|
+
columnDefs: this.attributeOptions.columnDefs ?? [],
|
|
267
|
+
} as VanillaGridOptions),
|
|
268
|
+
);
|
|
396
269
|
}
|
|
397
270
|
}
|
|
398
271
|
|
|
399
272
|
get controlIcons(): UiGridIconOverrides {
|
|
400
|
-
return this.
|
|
273
|
+
return this.icons.getOverrides();
|
|
401
274
|
}
|
|
402
275
|
|
|
403
276
|
set controlIcons(value: UiGridIconOverrides) {
|
|
404
|
-
this.
|
|
277
|
+
this.icons.setOverrides(value);
|
|
405
278
|
this.render();
|
|
406
279
|
}
|
|
407
280
|
|
|
281
|
+
private resolveIcon(key: UiGridControlIconKey): UiGridIconDefinition {
|
|
282
|
+
return this.icons.resolve(key);
|
|
283
|
+
}
|
|
284
|
+
|
|
408
285
|
// ─────────────────────────────────────────────────────────────────
|
|
409
286
|
// Individual property accessors (mirrors of HTML attributes)
|
|
410
287
|
// ─────────────────────────────────────────────────────────────────
|
|
@@ -443,14 +320,6 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
443
320
|
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
444
321
|
}
|
|
445
322
|
|
|
446
|
-
get viewportHeight(): number {
|
|
447
|
-
return this.options.viewportHeight ?? 560;
|
|
448
|
-
}
|
|
449
|
-
set viewportHeight(value: number) {
|
|
450
|
-
this.activeOptions = { ...this.activeOptions, viewportHeight: value } as any;
|
|
451
|
-
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
452
|
-
}
|
|
453
|
-
|
|
454
323
|
get paginationPageSize(): number {
|
|
455
324
|
return this.options.paginationPageSize ?? 25;
|
|
456
325
|
}
|
|
@@ -516,7 +385,10 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
516
385
|
}
|
|
517
386
|
|
|
518
387
|
get emptyMessage(): string {
|
|
519
|
-
|
|
388
|
+
// Fall back to the grid's label pack so consumers can re-localize the
|
|
389
|
+
// empty heading through `options.labels.emptyHeading` rather than
|
|
390
|
+
// hardcoding an English string here.
|
|
391
|
+
return this.options.emptyMessage ?? this.snapshot?.labels.emptyHeading ?? '';
|
|
520
392
|
}
|
|
521
393
|
set emptyMessage(value: string) {
|
|
522
394
|
this.activeOptions = { ...this.activeOptions, emptyMessage: value } as any;
|
|
@@ -794,62 +666,14 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
794
666
|
}
|
|
795
667
|
|
|
796
668
|
private patchCells(changedRowIds?: ReadonlySet<string>): void {
|
|
797
|
-
|
|
798
|
-
if (!snapshot) {
|
|
799
|
-
return;
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
const root = this.shadowRoot;
|
|
803
|
-
if (!root) {
|
|
804
|
-
return;
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
// Build O(1) lookup maps from the current pipeline snapshot.
|
|
808
|
-
const rowMap = new Map<string, GridRow>();
|
|
809
|
-
const rowIndexMap = new Map<string, number>();
|
|
810
|
-
for (const row of snapshot.pipeline.visibleRows) {
|
|
811
|
-
rowMap.set(row.id, row);
|
|
812
|
-
rowIndexMap.set(row.id, rowIndexMap.size);
|
|
813
|
-
}
|
|
814
|
-
const colMap = new Map<string, GridColumnDef>();
|
|
815
|
-
const templateMarkupMap = new Map<string, string | null>();
|
|
816
|
-
for (const col of snapshot.visibleColumns) {
|
|
817
|
-
colMap.set(col.name, col);
|
|
818
|
-
templateMarkupMap.set(col.name, this.getTemplateMarkup(this.cellSlotName(col)));
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
// Patch every rendered body cell in-place.
|
|
822
|
-
const cells = root.querySelectorAll<HTMLElement>('[data-row][data-column]');
|
|
823
|
-
for (const cell of cells) {
|
|
824
|
-
const rowId = cell.dataset['row'];
|
|
825
|
-
const colName = cell.dataset['column'];
|
|
826
|
-
if (!rowId || !colName) continue;
|
|
827
|
-
if (changedRowIds && !changedRowIds.has(rowId)) continue;
|
|
828
|
-
|
|
829
|
-
const row = rowMap.get(rowId);
|
|
830
|
-
const column = colMap.get(colName);
|
|
831
|
-
if (!row || !column) continue;
|
|
832
|
-
if (cell.classList.contains('cell-editing')) continue;
|
|
833
|
-
|
|
834
|
-
const cellContent = cell.querySelector<HTMLElement>('.cell-content');
|
|
835
|
-
if (!cellContent) continue;
|
|
836
|
-
|
|
837
|
-
const newContent = this.renderCellTemplateFromMarkup(
|
|
838
|
-
row,
|
|
839
|
-
column,
|
|
840
|
-
rowIndexMap.get(rowId) ?? 0,
|
|
841
|
-
templateMarkupMap.get(colName) ?? null,
|
|
842
|
-
);
|
|
843
|
-
if (cellContent.innerHTML !== newContent) {
|
|
844
|
-
cellContent.innerHTML = newContent;
|
|
845
|
-
}
|
|
846
|
-
}
|
|
669
|
+
patchCells(this, changedRowIds);
|
|
847
670
|
}
|
|
848
671
|
|
|
849
672
|
connectedCallback(): void {
|
|
850
673
|
this.ensureShadowRoot();
|
|
851
674
|
this.bindEvents();
|
|
852
675
|
this.observeTemplateSlots();
|
|
676
|
+
this.setupAutoResize();
|
|
853
677
|
|
|
854
678
|
if (this.activeOptions) {
|
|
855
679
|
this.ensureController(this.buildEffectiveOptions(this.activeOptions));
|
|
@@ -863,13 +687,18 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
863
687
|
this.unsubscribe = null;
|
|
864
688
|
this.templateObserver?.disconnect();
|
|
865
689
|
this.templateObserver = null;
|
|
690
|
+
this.teardownAutoResize();
|
|
866
691
|
if (this.scrollFrame !== null) {
|
|
867
692
|
cancelAnimationFrame(this.scrollFrame);
|
|
868
693
|
this.scrollFrame = null;
|
|
869
694
|
}
|
|
695
|
+
// Emit `removed` events for every framework-rendered slot so wrappers
|
|
696
|
+
// can destroy their views.
|
|
697
|
+
this.frameworkSlots.flushRemovals();
|
|
870
698
|
}
|
|
871
699
|
|
|
872
|
-
|
|
700
|
+
/** @internal */
|
|
701
|
+
ensureShadowRoot(): ShadowRoot {
|
|
873
702
|
if (!this.shadowRoot) {
|
|
874
703
|
this.attachShadow({ mode: 'open' });
|
|
875
704
|
}
|
|
@@ -877,41 +706,109 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
877
706
|
return this.shadowRoot as ShadowRoot;
|
|
878
707
|
}
|
|
879
708
|
|
|
880
|
-
|
|
709
|
+
/** @internal */
|
|
710
|
+
ensureController(options: VanillaGridOptions): void {
|
|
881
711
|
if (this.controller) {
|
|
712
|
+
if (this.clientWidth > 0) {
|
|
713
|
+
this.controller.setViewportWidth(this.clientWidth);
|
|
714
|
+
}
|
|
882
715
|
this.controller.setOptions(options);
|
|
883
716
|
return;
|
|
884
717
|
}
|
|
885
718
|
|
|
886
719
|
this.controller = createVanillaGridController(options);
|
|
720
|
+
if (this.clientWidth > 0) {
|
|
721
|
+
this.controller.setViewportWidth(this.clientWidth);
|
|
722
|
+
}
|
|
723
|
+
// Let the controller's cellNav.scrollToFocus delegate into our
|
|
724
|
+
// element's scroll-and-focus helpers.
|
|
725
|
+
this.controller.setCellNavScrollHandler((rowId, columnName) => {
|
|
726
|
+
if (!rowId || !columnName) return;
|
|
727
|
+
this.scrollFocusedRowIntoView(rowId);
|
|
728
|
+
this.focusCellElement(rowId, columnName);
|
|
729
|
+
});
|
|
730
|
+
// Infinite-scroll resetScroll() brings the viewport back to the top.
|
|
731
|
+
this.controller.setInfiniteScrollResetHandler(() => {
|
|
732
|
+
const bodyViewport = this.shadowRoot?.querySelector<HTMLElement>('.grid-body-viewport');
|
|
733
|
+
if (bodyViewport) {
|
|
734
|
+
bodyViewport.scrollTop = 0;
|
|
735
|
+
this.scrollPosition = 0;
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
// saveState scroll capture/restore — element owns the DOM element, so
|
|
739
|
+
// it provides the accessors the controller uses when save()/setState()
|
|
740
|
+
// handle scroll-position fields.
|
|
741
|
+
this.controller.setSaveStateScrollHandlers(
|
|
742
|
+
() => ({ scrollTop: this.scrollPosition, scrollLeft: this.horizontalScrollPosition }),
|
|
743
|
+
(scrollTop, scrollLeft) => {
|
|
744
|
+
const bodyViewport = this.shadowRoot?.querySelector<HTMLElement>('.grid-body-viewport');
|
|
745
|
+
if (!bodyViewport) return;
|
|
746
|
+
// Defer to a rAF so the body has finished laying out after refresh.
|
|
747
|
+
requestAnimationFrame(() => {
|
|
748
|
+
bodyViewport.scrollTop = scrollTop;
|
|
749
|
+
bodyViewport.scrollLeft = scrollLeft;
|
|
750
|
+
this.scrollPosition = scrollTop;
|
|
751
|
+
this.horizontalScrollPosition = scrollLeft;
|
|
752
|
+
// Keep header/filter strips aligned after a restore.
|
|
753
|
+
this.syncHeaderHorizontalScroll(scrollLeft);
|
|
754
|
+
});
|
|
755
|
+
},
|
|
756
|
+
);
|
|
757
|
+
// Importer file-picker: the element mounts a hidden <input type="file">
|
|
758
|
+
// the first time the controller asks for one, then keeps it around so
|
|
759
|
+
// subsequent imports reuse it. The controller stays DOM-free.
|
|
760
|
+
this.controller.setImporterFilePickerHandler(() => {
|
|
761
|
+
this.openImporterFilePicker();
|
|
762
|
+
});
|
|
887
763
|
this.unsubscribe = this.controller.subscribe((snapshot) => {
|
|
888
764
|
this.snapshot = snapshot;
|
|
889
765
|
this.render();
|
|
890
766
|
});
|
|
891
767
|
}
|
|
892
768
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
769
|
+
/** Hidden file input used by `gridApi.importer.importAFile()`. Lazily
|
|
770
|
+
* created on first use and reused thereafter. */
|
|
771
|
+
private importerFileInput: HTMLInputElement | null = null;
|
|
772
|
+
|
|
773
|
+
private openImporterFilePicker(): void {
|
|
774
|
+
if (!this.controller) return;
|
|
775
|
+
if (!this.importerFileInput) {
|
|
776
|
+
const input = document.createElement('input');
|
|
777
|
+
input.type = 'file';
|
|
778
|
+
input.accept = '.csv,.json,text/csv,application/json,text/plain';
|
|
779
|
+
input.style.display = 'none';
|
|
780
|
+
input.addEventListener('change', () => {
|
|
781
|
+
const file = input.files?.[0];
|
|
782
|
+
if (file && this.controller) {
|
|
783
|
+
void this.controller.gridApi.importer.importThisFile(file);
|
|
784
|
+
}
|
|
785
|
+
// Clear so the same file can be re-imported.
|
|
786
|
+
input.value = '';
|
|
787
|
+
});
|
|
788
|
+
this.appendChild(input);
|
|
789
|
+
this.importerFileInput = input;
|
|
896
790
|
}
|
|
791
|
+
this.importerFileInput.click();
|
|
792
|
+
}
|
|
897
793
|
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
} else {
|
|
902
|
-
this.render();
|
|
903
|
-
}
|
|
904
|
-
});
|
|
794
|
+
private observeTemplateSlots(): void {
|
|
795
|
+
observeTemplateSlots(this);
|
|
796
|
+
}
|
|
905
797
|
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
798
|
+
private bindEvents(): void {
|
|
799
|
+
bindEvents(this);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
private syncHeaderHorizontalScroll(scrollLeft: number): void {
|
|
803
|
+
syncHeaderHorizontalScroll(this, scrollLeft);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
private maybeTriggerInfiniteScroll(): void {
|
|
807
|
+
maybeTriggerInfiniteScroll(this);
|
|
912
808
|
}
|
|
913
809
|
|
|
914
|
-
|
|
810
|
+
/** @internal */
|
|
811
|
+
buildEffectiveOptions(options: VanillaGridOptions): VanillaGridOptions {
|
|
915
812
|
// Merge: attributes are overridden by explicit JS property values.
|
|
916
813
|
// JS property always wins: { ...attributeOptions, ...options }
|
|
917
814
|
const merged = {
|
|
@@ -919,8 +816,7 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
919
816
|
...options,
|
|
920
817
|
} as VanillaGridOptions;
|
|
921
818
|
|
|
922
|
-
|
|
923
|
-
if (merged.enableExpandable && hasExpandableSlot && !merged.expandableRowTemplate) {
|
|
819
|
+
if (merged.enableExpandable && !merged.expandableRowTemplate) {
|
|
924
820
|
merged.expandableRowTemplate = {
|
|
925
821
|
createEmbeddedView: () => undefined,
|
|
926
822
|
};
|
|
@@ -929,450 +825,28 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
929
825
|
return merged;
|
|
930
826
|
}
|
|
931
827
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
return;
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
const action = actionNode.dataset['action'];
|
|
950
|
-
if (!action) {
|
|
951
|
-
return;
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
if (this.openPinMenuColumn && !target.closest('.pin-control')) {
|
|
955
|
-
this.openPinMenuColumn = null;
|
|
956
|
-
this.render();
|
|
957
|
-
return;
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
if (action === 'sort') {
|
|
961
|
-
const columnName = actionNode.dataset['column'];
|
|
962
|
-
if (columnName) {
|
|
963
|
-
this.controller.toggleSort(columnName);
|
|
964
|
-
}
|
|
965
|
-
return;
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
if (action === 'group') {
|
|
969
|
-
const columnName = actionNode.dataset['column'];
|
|
970
|
-
if (columnName) {
|
|
971
|
-
this.controller.toggleGrouping(columnName);
|
|
972
|
-
}
|
|
973
|
-
return;
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
if (action === 'toggle-group') {
|
|
977
|
-
const groupId = actionNode.dataset['group'];
|
|
978
|
-
if (groupId) {
|
|
979
|
-
const collapsed = actionNode.dataset['collapsed'] !== 'true';
|
|
980
|
-
this.controller.setCollapsedGroup(groupId, collapsed);
|
|
981
|
-
}
|
|
982
|
-
return;
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
if (action === 'pin-trigger') {
|
|
986
|
-
const columnName = actionNode.dataset['column'];
|
|
987
|
-
const column = columnName
|
|
988
|
-
? this.snapshot.visibleColumns.find((candidate) => candidate.name === columnName)
|
|
989
|
-
: undefined;
|
|
990
|
-
|
|
991
|
-
if (columnName && column) {
|
|
992
|
-
if (this.controller.isPinned(column)) {
|
|
993
|
-
this.openPinMenuColumn = null;
|
|
994
|
-
this.controller.pinColumn(columnName, 'none');
|
|
995
|
-
return;
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
this.openPinMenuColumn = this.openPinMenuColumn === columnName ? null : columnName;
|
|
999
|
-
this.render();
|
|
1000
|
-
}
|
|
1001
|
-
return;
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
if (action === 'pin-left' || action === 'pin-right') {
|
|
1005
|
-
const columnName = actionNode.dataset['column'];
|
|
1006
|
-
if (columnName) {
|
|
1007
|
-
this.openPinMenuColumn = null;
|
|
1008
|
-
this.controller.pinColumn(columnName, action === 'pin-left' ? 'left' : 'right');
|
|
1009
|
-
}
|
|
1010
|
-
return;
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
if (action === 'toggle-tree') {
|
|
1014
|
-
const rowId = actionNode.dataset['row'];
|
|
1015
|
-
if (rowId) {
|
|
1016
|
-
this.controller.toggleTreeRow(rowId);
|
|
1017
|
-
}
|
|
1018
|
-
return;
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
if (action === 'toggle-expand') {
|
|
1022
|
-
const rowId = actionNode.dataset['row'];
|
|
1023
|
-
if (rowId) {
|
|
1024
|
-
this.controller.toggleRowExpansion(rowId);
|
|
1025
|
-
}
|
|
1026
|
-
return;
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
if (action === 'page-prev') {
|
|
1030
|
-
this.controller.seekPage(this.snapshot.currentPage - 1);
|
|
1031
|
-
return;
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
if (action === 'page-next') {
|
|
1035
|
-
this.controller.seekPage(this.snapshot.currentPage + 1);
|
|
1036
|
-
return;
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
if (action === 'benchmark') {
|
|
1040
|
-
const result = this.controller.gridApi.core.benchmark();
|
|
1041
|
-
this.benchmarkAverage = result.averageMs.toFixed(2);
|
|
1042
|
-
this.render();
|
|
1043
|
-
return;
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
if (action === 'export-csv') {
|
|
1047
|
-
this.downloadCsv();
|
|
1048
|
-
}
|
|
1049
|
-
});
|
|
1050
|
-
|
|
1051
|
-
root.addEventListener('focusin', (event) => {
|
|
1052
|
-
const target = event.target as HTMLElement | null;
|
|
1053
|
-
if (!target) {
|
|
1054
|
-
return;
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
const cell = target.closest<HTMLElement>('.body-cell[data-row][data-column]');
|
|
1058
|
-
const rowId = cell?.dataset['row'];
|
|
1059
|
-
const columnName = cell?.dataset['column'];
|
|
1060
|
-
if (rowId && columnName) {
|
|
1061
|
-
this.focusedCell = { rowId, columnName };
|
|
1062
|
-
}
|
|
1063
|
-
});
|
|
1064
|
-
|
|
1065
|
-
root.addEventListener('focusout', (event) => {
|
|
1066
|
-
const relatedTarget = (event as FocusEvent).relatedTarget as HTMLElement | null;
|
|
1067
|
-
if (!relatedTarget?.closest('.body-cell[data-row][data-column]')) {
|
|
1068
|
-
this.focusedCell = null;
|
|
1069
|
-
}
|
|
1070
|
-
});
|
|
1071
|
-
|
|
1072
|
-
root.addEventListener('input', (event) => {
|
|
1073
|
-
const target = event.target as HTMLElement | null;
|
|
1074
|
-
if (!target || !this.controller) {
|
|
1075
|
-
return;
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
if (target instanceof HTMLInputElement && target.dataset['role'] === 'filter') {
|
|
1079
|
-
const columnName = target.dataset['column'];
|
|
1080
|
-
if (columnName) {
|
|
1081
|
-
this.controller.setFilter(columnName, target.value);
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
if (target instanceof HTMLInputElement && target.dataset['role'] === 'editor') {
|
|
1086
|
-
this.controller.updateEditingValue(target.value);
|
|
1087
|
-
}
|
|
1088
|
-
});
|
|
1089
|
-
|
|
1090
|
-
root.addEventListener('change', (event) => {
|
|
1091
|
-
const target = event.target as HTMLElement | null;
|
|
1092
|
-
if (!target || !this.controller) {
|
|
1093
|
-
return;
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
if (target instanceof HTMLSelectElement && target.dataset['role'] === 'page-size') {
|
|
1097
|
-
const value = Number.parseInt(target.value, 10);
|
|
1098
|
-
this.controller.setPageSize(value);
|
|
1099
|
-
}
|
|
1100
|
-
});
|
|
1101
|
-
|
|
1102
|
-
root.addEventListener('mousedown', (event) => {
|
|
1103
|
-
const target = event.target as HTMLElement | null;
|
|
1104
|
-
if (!target || !this.controller) return;
|
|
1105
|
-
|
|
1106
|
-
const resizer = target.closest<HTMLElement>('.column-resizer[data-column]');
|
|
1107
|
-
if (!resizer || !this.controller.canResizeColumns()) return;
|
|
1108
|
-
|
|
1109
|
-
const columnName = resizer.dataset['column'];
|
|
1110
|
-
if (!columnName) return;
|
|
1111
|
-
|
|
1112
|
-
const headerCell = resizer.closest<HTMLElement>('.header-cell');
|
|
1113
|
-
if (!headerCell) return;
|
|
1114
|
-
|
|
1115
|
-
event.preventDefault();
|
|
1116
|
-
event.stopPropagation();
|
|
1117
|
-
|
|
1118
|
-
const startX = (event as MouseEvent).clientX;
|
|
1119
|
-
const startWidth = headerCell.getBoundingClientRect().width;
|
|
1120
|
-
let lastWidth = startWidth;
|
|
1121
|
-
|
|
1122
|
-
const handleMove = (moveEvent: MouseEvent): void => {
|
|
1123
|
-
lastWidth = Math.max(88, startWidth + (moveEvent.clientX - startX));
|
|
1124
|
-
|
|
1125
|
-
// Write directly to DOM — skip full refresh while dragging.
|
|
1126
|
-
const newTemplate = this.controller!.buildTemplateColumnsWithOverride(columnName, lastWidth);
|
|
1127
|
-
const root = this.shadowRoot ?? this;
|
|
1128
|
-
(root as ShadowRoot | HTMLElement).querySelectorAll<HTMLElement>('.header-grid, .filter-grid, .body-grid').forEach((el) => {
|
|
1129
|
-
el.style.gridTemplateColumns = newTemplate;
|
|
1130
|
-
});
|
|
1131
|
-
};
|
|
1132
|
-
|
|
1133
|
-
const handleUp = (): void => {
|
|
1134
|
-
window.removeEventListener('mousemove', handleMove);
|
|
1135
|
-
window.removeEventListener('mouseup', handleUp);
|
|
1136
|
-
// Commit final width once — triggers one full refresh.
|
|
1137
|
-
this.controller!.setColumnWidthOverride(columnName, lastWidth);
|
|
1138
|
-
};
|
|
1139
|
-
|
|
1140
|
-
window.addEventListener('mousemove', handleMove);
|
|
1141
|
-
window.addEventListener('mouseup', handleUp);
|
|
1142
|
-
});
|
|
1143
|
-
|
|
1144
|
-
root.addEventListener('dblclick', (event) => {
|
|
1145
|
-
const target = event.target as HTMLElement | null;
|
|
1146
|
-
if (!target || !this.controller) {
|
|
1147
|
-
return;
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
const resizer = target.closest<HTMLElement>('.column-resizer[data-column]');
|
|
1151
|
-
if (resizer && this.controller.canResizeColumns()) {
|
|
1152
|
-
const columnName = resizer.dataset['column'];
|
|
1153
|
-
if (columnName) {
|
|
1154
|
-
event.preventDefault();
|
|
1155
|
-
event.stopPropagation();
|
|
1156
|
-
this.controller.setColumnWidthOverride(columnName, this.measureAutoColumnWidth(columnName));
|
|
1157
|
-
return;
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
const cell = target.closest<HTMLElement>('.body-cell');
|
|
1162
|
-
if (!cell) {
|
|
1163
|
-
return;
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
const rowId = cell.dataset['row'];
|
|
1167
|
-
const columnName = cell.dataset['column'];
|
|
1168
|
-
if (rowId && columnName) {
|
|
1169
|
-
this.controller.beginCellEdit(rowId, columnName, event);
|
|
1170
|
-
}
|
|
1171
|
-
});
|
|
1172
|
-
|
|
1173
|
-
root.addEventListener('keydown', (event) => {
|
|
1174
|
-
const keyboardEvent = event as KeyboardEvent;
|
|
1175
|
-
const target = event.target as HTMLElement | null;
|
|
1176
|
-
if (!target || !this.controller) {
|
|
1177
|
-
return;
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
if (target instanceof HTMLInputElement && target.dataset['role'] === 'editor') {
|
|
1181
|
-
if (keyboardEvent.key === 'Enter') {
|
|
1182
|
-
event.preventDefault();
|
|
1183
|
-
this.controller.commitCellEdit();
|
|
1184
|
-
return;
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
if (keyboardEvent.key === 'Escape') {
|
|
1188
|
-
event.preventDefault();
|
|
1189
|
-
this.controller.cancelCellEdit();
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
return;
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
const cell = target.closest<HTMLElement>('.body-cell');
|
|
1196
|
-
if (!cell) {
|
|
1197
|
-
return;
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
const rowId = cell.dataset['row'];
|
|
1201
|
-
const columnName = cell.dataset['column'];
|
|
1202
|
-
if (!rowId || !columnName) {
|
|
1203
|
-
return;
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
if (keyboardEvent.key === 'Enter' || keyboardEvent.key === 'F2') {
|
|
1207
|
-
event.preventDefault();
|
|
1208
|
-
this.controller.beginCellEdit(rowId, columnName, event);
|
|
1209
|
-
return;
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
if (
|
|
1213
|
-
keyboardEvent.key.length === 1 &&
|
|
1214
|
-
!keyboardEvent.ctrlKey &&
|
|
1215
|
-
!keyboardEvent.metaKey &&
|
|
1216
|
-
!keyboardEvent.altKey
|
|
1217
|
-
) {
|
|
1218
|
-
event.preventDefault();
|
|
1219
|
-
this.controller.beginCellEdit(rowId, columnName, event);
|
|
1220
|
-
this.controller.updateEditingValue(keyboardEvent.key);
|
|
1221
|
-
}
|
|
1222
|
-
});
|
|
1223
|
-
|
|
1224
|
-
root.addEventListener(
|
|
1225
|
-
'blur',
|
|
1226
|
-
(event) => {
|
|
1227
|
-
const target = event.target as HTMLElement | null;
|
|
1228
|
-
if (!target || !this.controller) {
|
|
1229
|
-
return;
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
if (target instanceof HTMLInputElement && target.dataset['role'] === 'editor') {
|
|
1233
|
-
this.controller.commitCellEdit();
|
|
1234
|
-
}
|
|
1235
|
-
},
|
|
1236
|
-
true,
|
|
1237
|
-
);
|
|
1238
|
-
|
|
1239
|
-
root.addEventListener('dragstart', (event) => {
|
|
1240
|
-
const dragEvent = event as DragEvent;
|
|
1241
|
-
const target = event.target as HTMLElement | null;
|
|
1242
|
-
const headerCell = target?.closest<HTMLElement>('.header-cell[data-column]');
|
|
1243
|
-
|
|
1244
|
-
if (!headerCell || !this.snapshot || !canGridMoveColumns(this.snapshot.options)) {
|
|
1245
|
-
event.preventDefault();
|
|
1246
|
-
return;
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
const columnName = headerCell.dataset['column'];
|
|
1250
|
-
if (!columnName || !dragEvent.dataTransfer) {
|
|
1251
|
-
event.preventDefault();
|
|
1252
|
-
return;
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
this.draggedColumnName = columnName;
|
|
1256
|
-
this.dropTargetColumnName = null;
|
|
1257
|
-
dragEvent.dataTransfer.effectAllowed = 'move';
|
|
1258
|
-
dragEvent.dataTransfer.setData('text/plain', columnName);
|
|
1259
|
-
headerCell.classList.add('is-dragging');
|
|
1260
|
-
});
|
|
1261
|
-
|
|
1262
|
-
root.addEventListener('dragover', (event) => {
|
|
1263
|
-
const dragEvent = event as DragEvent;
|
|
1264
|
-
const target = event.target as HTMLElement | null;
|
|
1265
|
-
const headerCell = target?.closest<HTMLElement>('.header-cell[data-column]');
|
|
1266
|
-
|
|
1267
|
-
if (!headerCell || !this.draggedColumnName || !dragEvent.dataTransfer) {
|
|
1268
|
-
return;
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
const columnName = headerCell.dataset['column'];
|
|
1272
|
-
if (!columnName || columnName === this.draggedColumnName) {
|
|
1273
|
-
return;
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
event.preventDefault();
|
|
1277
|
-
dragEvent.dataTransfer.dropEffect = 'move';
|
|
1278
|
-
|
|
1279
|
-
if (this.dropTargetColumnName !== columnName) {
|
|
1280
|
-
root
|
|
1281
|
-
.querySelectorAll('.header-cell.is-drag-target')
|
|
1282
|
-
.forEach((element) => element.classList.remove('is-drag-target'));
|
|
1283
|
-
this.dropTargetColumnName = columnName;
|
|
1284
|
-
headerCell.classList.add('is-drag-target');
|
|
1285
|
-
}
|
|
1286
|
-
});
|
|
1287
|
-
|
|
1288
|
-
root.addEventListener('dragleave', (event) => {
|
|
1289
|
-
const target = event.target as HTMLElement | null;
|
|
1290
|
-
const headerCell = target?.closest<HTMLElement>('.header-cell[data-column]');
|
|
1291
|
-
if (headerCell && headerCell.dataset['column'] === this.dropTargetColumnName) {
|
|
1292
|
-
headerCell.classList.remove('is-drag-target');
|
|
1293
|
-
}
|
|
1294
|
-
});
|
|
1295
|
-
|
|
1296
|
-
root.addEventListener('drop', (event) => {
|
|
1297
|
-
event.preventDefault();
|
|
1298
|
-
const dragEvent = event as DragEvent;
|
|
1299
|
-
const target = event.target as HTMLElement | null;
|
|
1300
|
-
const headerCell = target?.closest<HTMLElement>('.header-cell[data-column]');
|
|
1301
|
-
const targetColumn = headerCell?.dataset['column'];
|
|
1302
|
-
const sourceColumn =
|
|
1303
|
-
this.draggedColumnName ?? dragEvent.dataTransfer?.getData('text/plain') ?? null;
|
|
1304
|
-
|
|
1305
|
-
this.draggedColumnName = null;
|
|
1306
|
-
this.dropTargetColumnName = null;
|
|
1307
|
-
|
|
1308
|
-
root
|
|
1309
|
-
.querySelectorAll('.header-cell.is-dragging, .header-cell.is-drag-target')
|
|
1310
|
-
.forEach((element) => element.classList.remove('is-dragging', 'is-drag-target'));
|
|
1311
|
-
|
|
1312
|
-
if (!sourceColumn || !targetColumn || sourceColumn === targetColumn || !this.controller) {
|
|
1313
|
-
return;
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
this.controller.moveVisibleColumn(sourceColumn, targetColumn);
|
|
1317
|
-
});
|
|
1318
|
-
|
|
1319
|
-
root.addEventListener('dragend', () => {
|
|
1320
|
-
this.draggedColumnName = null;
|
|
1321
|
-
this.dropTargetColumnName = null;
|
|
1322
|
-
root
|
|
1323
|
-
.querySelectorAll('.header-cell.is-dragging, .header-cell.is-drag-target')
|
|
1324
|
-
.forEach((element) => element.classList.remove('is-dragging', 'is-drag-target'));
|
|
1325
|
-
});
|
|
1326
|
-
|
|
1327
|
-
root.addEventListener(
|
|
1328
|
-
'scroll',
|
|
1329
|
-
(event) => {
|
|
1330
|
-
const target = event.target as HTMLElement | null;
|
|
1331
|
-
if (!target?.classList.contains('grid-table') || this.suppressScrollEvent) {
|
|
1332
|
-
return;
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
this.lastScrollActivityAt = Date.now();
|
|
1336
|
-
this.scrollPosition = target.scrollTop;
|
|
1337
|
-
this.horizontalScrollPosition = target.scrollLeft;
|
|
1338
|
-
if (!this.snapshot?.pipeline.virtualizationEnabled) {
|
|
1339
|
-
return;
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
const stickyChromeHeight =
|
|
1343
|
-
this.measuredHeaderStickyHeight + this.measuredFilterStickyHeight;
|
|
1344
|
-
const bodyScrollTop = Math.max(0, this.scrollPosition - stickyChromeHeight);
|
|
1345
|
-
const overscan = 4;
|
|
1346
|
-
const nextStartIndex = Math.max(
|
|
1347
|
-
0,
|
|
1348
|
-
Math.floor(bodyScrollTop / this.snapshot.rowSize) - overscan,
|
|
1349
|
-
);
|
|
1350
|
-
if (nextStartIndex === this.lastVirtualStartIndex) {
|
|
1351
|
-
return;
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
if (this.scrollFrame !== null) {
|
|
1355
|
-
cancelAnimationFrame(this.scrollFrame);
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
this.scrollFrame = requestAnimationFrame(() => {
|
|
1359
|
-
this.scrollFrame = null;
|
|
1360
|
-
this.renderVirtualBody();
|
|
1361
|
-
});
|
|
1362
|
-
},
|
|
1363
|
-
true,
|
|
1364
|
-
);
|
|
1365
|
-
|
|
1366
|
-
(root as ShadowRoot & { __uiGridBound?: boolean }).__uiGridBound = true;
|
|
1367
|
-
}
|
|
1368
|
-
|
|
828
|
+
/**
|
|
829
|
+
* Ask the controller to evaluate whether the current scroll position
|
|
830
|
+
* should request more data at the top or bottom — ports the old grid's
|
|
831
|
+
* handleScroll → loadData check. The controller does the actual
|
|
832
|
+
* needLoadMoreData / needLoadMoreDataTop raise.
|
|
833
|
+
*/
|
|
834
|
+
/**
|
|
835
|
+
* Mirror horizontal scroll from the body viewport onto the header + filter
|
|
836
|
+
* strips by setting `scrollLeft` on them. Because those strips are real
|
|
837
|
+
* scroll containers (with their scrollbars hidden), `position: sticky` on
|
|
838
|
+
* pinned header / filter cells anchors to the strip's scroll position the
|
|
839
|
+
* same way body cells anchor to `.grid-body-viewport`. This is the
|
|
840
|
+
* imperative sync strategy the old ui-grid used in ui-grid-viewport.js.
|
|
841
|
+
*/
|
|
1369
842
|
/**
|
|
1370
843
|
* Targeted update for virtual scroll: replaces only the rendered rows inside
|
|
1371
844
|
* the existing `.grid-virtual-body` without touching the scroll container.
|
|
1372
845
|
* This preserves momentum/inertia scrolling because the `.grid-table` element
|
|
1373
846
|
* is never destroyed.
|
|
1374
847
|
*/
|
|
1375
|
-
|
|
848
|
+
/** @internal */
|
|
849
|
+
renderVirtualBody(): void {
|
|
1376
850
|
const snapshot = this.snapshot;
|
|
1377
851
|
if (!snapshot?.pipeline.virtualizationEnabled) {
|
|
1378
852
|
this.render();
|
|
@@ -1393,12 +867,12 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1393
867
|
this.dataFrame = null;
|
|
1394
868
|
}
|
|
1395
869
|
|
|
1396
|
-
const
|
|
1397
|
-
const
|
|
1398
|
-
|
|
1399
|
-
|
|
870
|
+
const bodyViewport = root?.querySelector<HTMLElement>('.grid-body-viewport');
|
|
871
|
+
const bodyViewportHeight = bodyViewport
|
|
872
|
+
? Math.max(snapshot.rowSize, bodyViewport.clientHeight)
|
|
873
|
+
: Math.max(snapshot.rowSize, (snapshot.options.minRowsToShow ?? 10) * snapshot.rowSize);
|
|
1400
874
|
const overscan = 4;
|
|
1401
|
-
const startIndex = Math.max(0, Math.floor(
|
|
875
|
+
const startIndex = Math.max(0, Math.floor(this.scrollPosition / snapshot.rowSize) - overscan);
|
|
1402
876
|
this.lastVirtualStartIndex = startIndex;
|
|
1403
877
|
const visibleCount = Math.ceil(bodyViewportHeight / snapshot.rowSize) + overscan * 2;
|
|
1404
878
|
const itemsToRender = snapshot.pipeline.displayItems.slice(
|
|
@@ -1411,9 +885,16 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1411
885
|
virtualBody.innerHTML = itemsToRender
|
|
1412
886
|
.map((item, index) => this.renderDisplayItem(item, startIndex + index))
|
|
1413
887
|
.join('');
|
|
888
|
+
// The body fingerprint now reflects the freshly-rendered slice so the
|
|
889
|
+
// next full render() can take the fast per-cell patch path.
|
|
890
|
+
this.lastItemsFingerprint = this.fingerprintItems(itemsToRender);
|
|
891
|
+
this.lastVirtualOffset = virtualOffset;
|
|
892
|
+
|
|
893
|
+
this.frameworkSlots.flush();
|
|
1414
894
|
}
|
|
1415
895
|
|
|
1416
|
-
|
|
896
|
+
/** @internal */
|
|
897
|
+
render(): void {
|
|
1417
898
|
if (this.skipNextRender) {
|
|
1418
899
|
return;
|
|
1419
900
|
}
|
|
@@ -1429,17 +910,166 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1429
910
|
}
|
|
1430
911
|
const root = this.ensureShadowRoot();
|
|
1431
912
|
const snapshot = this.snapshot;
|
|
1432
|
-
const
|
|
1433
|
-
if (
|
|
1434
|
-
this.scrollPosition =
|
|
1435
|
-
this.horizontalScrollPosition =
|
|
913
|
+
const previousViewport = root.querySelector<HTMLElement>('.grid-body-viewport');
|
|
914
|
+
if (previousViewport && !this.suppressScrollEvent) {
|
|
915
|
+
this.scrollPosition = previousViewport.scrollTop;
|
|
916
|
+
this.horizontalScrollPosition = previousViewport.scrollLeft;
|
|
1436
917
|
}
|
|
1437
918
|
|
|
1438
919
|
if (!snapshot) {
|
|
1439
|
-
|
|
920
|
+
this.lastStructureKey = null;
|
|
921
|
+
this.lastItemsFingerprint = null;
|
|
922
|
+
emptyTemplate({ message: 'No grid options provided.' }).connect(root);
|
|
1440
923
|
return;
|
|
1441
924
|
}
|
|
1442
925
|
|
|
926
|
+
const plan = this.buildRenderPlan(snapshot);
|
|
927
|
+
|
|
928
|
+
// The shadow-DOM structure is only safe to patch when the set of sub-regions,
|
|
929
|
+
// the visible column identity/order, and the pin/virt/filter/pagination
|
|
930
|
+
// toggles are unchanged. Otherwise the existing nodes don't line up with the
|
|
931
|
+
// new snapshot — fall back to a full re-mount.
|
|
932
|
+
const canPatch =
|
|
933
|
+
this.lastStructureKey !== null &&
|
|
934
|
+
this.lastStructureKey === plan.structureKey &&
|
|
935
|
+
root.querySelector('.grid-frame') !== null;
|
|
936
|
+
|
|
937
|
+
if (canPatch) {
|
|
938
|
+
this.renderPatch(plan, root);
|
|
939
|
+
} else {
|
|
940
|
+
this.renderFull(plan, root);
|
|
941
|
+
}
|
|
942
|
+
this.lastStructureKey = plan.structureKey;
|
|
943
|
+
|
|
944
|
+
const bodyViewport = root.querySelector<HTMLElement>('.grid-body-viewport');
|
|
945
|
+
if (bodyViewport && (this.scrollPosition > 0 || this.horizontalScrollPosition > 0)) {
|
|
946
|
+
this.suppressScrollEvent = true;
|
|
947
|
+
if (this.scrollPosition > 0) {
|
|
948
|
+
bodyViewport.scrollTop = this.scrollPosition;
|
|
949
|
+
}
|
|
950
|
+
if (this.horizontalScrollPosition > 0) {
|
|
951
|
+
bodyViewport.scrollLeft = this.horizontalScrollPosition;
|
|
952
|
+
}
|
|
953
|
+
// Keep the header/filter strips in sync on the first paint too.
|
|
954
|
+
this.syncHeaderHorizontalScroll(this.horizontalScrollPosition);
|
|
955
|
+
requestAnimationFrame(() => {
|
|
956
|
+
this.suppressScrollEvent = false;
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
const headerGrid = root.querySelector<HTMLElement>('.header-grid');
|
|
961
|
+
const filterGrid = root.querySelector<HTMLElement>('.filter-grid');
|
|
962
|
+
const nextHeaderStickyHeight = headerGrid?.offsetHeight ?? 0;
|
|
963
|
+
const nextFilterStickyHeight = filterGrid?.offsetHeight ?? 0;
|
|
964
|
+
const stickyHeightsChanged =
|
|
965
|
+
nextHeaderStickyHeight !== this.measuredHeaderStickyHeight ||
|
|
966
|
+
nextFilterStickyHeight !== this.measuredFilterStickyHeight;
|
|
967
|
+
|
|
968
|
+
this.measuredHeaderStickyHeight = nextHeaderStickyHeight;
|
|
969
|
+
this.measuredFilterStickyHeight = nextFilterStickyHeight;
|
|
970
|
+
|
|
971
|
+
if (stickyHeightsChanged && !this.stickyHeightRelayoutQueued) {
|
|
972
|
+
this.stickyHeightRelayoutQueued = true;
|
|
973
|
+
requestAnimationFrame(() => {
|
|
974
|
+
this.stickyHeightRelayoutQueued = false;
|
|
975
|
+
if (!this.snapshot) {
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
this.render();
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
this.autoAdjustHeight(snapshot);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/** @internal */
|
|
986
|
+
private inAutoAdjust = false;
|
|
987
|
+
private lastAutoHeight = 0;
|
|
988
|
+
|
|
989
|
+
private autoAdjustHeight(snapshot: GridControllerSnapshot): void {
|
|
990
|
+
if (this.inAutoAdjust) return;
|
|
991
|
+
const options = snapshot.options;
|
|
992
|
+
if (options.enableMinHeightCheck === false) return;
|
|
993
|
+
|
|
994
|
+
const rowHeight = snapshot.rowSize;
|
|
995
|
+
const minRows = options.minRowsToShow ?? 10;
|
|
996
|
+
const headerHeight = this.measuredHeaderStickyHeight || options.headerRowHeight || 50;
|
|
997
|
+
const filterHeight = this.measuredFilterStickyHeight;
|
|
998
|
+
const paginationHeight = this.measuredPaginationHeight();
|
|
999
|
+
const minHeight = headerHeight + filterHeight + paginationHeight + (minRows * rowHeight);
|
|
1000
|
+
|
|
1001
|
+
if (this.clientHeight < minHeight || (paginationHeight > 0 && this.lastAutoHeight !== minHeight)) {
|
|
1002
|
+
this.lastAutoHeight = minHeight;
|
|
1003
|
+
this.style.height = `${minHeight}px`;
|
|
1004
|
+
this.inAutoAdjust = true;
|
|
1005
|
+
this.render();
|
|
1006
|
+
this.inAutoAdjust = false;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
measuredPaginationHeight(): number {
|
|
1011
|
+
const paginationEl = this.shadowRoot?.querySelector<HTMLElement>('ui-grid-pagination');
|
|
1012
|
+
if (!paginationEl) return 0;
|
|
1013
|
+
return paginationEl.offsetHeight || 44;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/** @internal */
|
|
1017
|
+
setupAutoResize(): void {
|
|
1018
|
+
if (this.autoResizeObserver) return;
|
|
1019
|
+
if (typeof ResizeObserver === 'undefined') return;
|
|
1020
|
+
|
|
1021
|
+
this.autoResizeObserver = new ResizeObserver((entries) => {
|
|
1022
|
+
const entry = entries[0];
|
|
1023
|
+
if (!entry) return;
|
|
1024
|
+
|
|
1025
|
+
const newWidth = entry.contentRect.width;
|
|
1026
|
+
if (this.controller && newWidth > 0) {
|
|
1027
|
+
this.controller.setViewportWidth(newWidth);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
if (this.autoResizeDebounceHandle !== null) {
|
|
1031
|
+
cancelAnimationFrame(this.autoResizeDebounceHandle);
|
|
1032
|
+
}
|
|
1033
|
+
this.autoResizeDebounceHandle = requestAnimationFrame(() => {
|
|
1034
|
+
this.autoResizeDebounceHandle = null;
|
|
1035
|
+
if (!this.snapshot) return;
|
|
1036
|
+
this.render();
|
|
1037
|
+
});
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
this.autoResizeObserver.observe(this);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/** @internal */
|
|
1044
|
+
teardownAutoResize(): void {
|
|
1045
|
+
if (this.autoResizeObserver) {
|
|
1046
|
+
this.autoResizeObserver.disconnect();
|
|
1047
|
+
this.autoResizeObserver = null;
|
|
1048
|
+
}
|
|
1049
|
+
if (this.autoResizeDebounceHandle !== null) {
|
|
1050
|
+
cancelAnimationFrame(this.autoResizeDebounceHandle);
|
|
1051
|
+
this.autoResizeDebounceHandle = null;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
private buildRenderPlan(snapshot: GridControllerSnapshot): {
|
|
1056
|
+
snapshot: GridControllerSnapshot;
|
|
1057
|
+
options: GridOptions;
|
|
1058
|
+
labels: GridControllerSnapshot['labels'];
|
|
1059
|
+
templateColumns: string;
|
|
1060
|
+
sortEnabled: boolean;
|
|
1061
|
+
filterEnabled: boolean;
|
|
1062
|
+
groupingEnabled: boolean;
|
|
1063
|
+
pinningEnabled: boolean;
|
|
1064
|
+
paginationEnabled: boolean;
|
|
1065
|
+
showPagination: boolean;
|
|
1066
|
+
virtualizationEnabled: boolean;
|
|
1067
|
+
itemsToRender: readonly DisplayItem[];
|
|
1068
|
+
startIndex: number;
|
|
1069
|
+
virtualOffset: number;
|
|
1070
|
+
totalVirtualHeight: number;
|
|
1071
|
+
structureKey: string;
|
|
1072
|
+
} {
|
|
1443
1073
|
const controller = this.controller!;
|
|
1444
1074
|
const options = snapshot.options;
|
|
1445
1075
|
const labels = snapshot.labels;
|
|
@@ -1451,19 +1081,22 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1451
1081
|
const paginationEnabled = controller.isPaginationEnabled();
|
|
1452
1082
|
const showPagination = controller.shouldShowPaginationControls();
|
|
1453
1083
|
const virtualizationEnabled = snapshot.pipeline.virtualizationEnabled;
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
const
|
|
1084
|
+
// Measure the body viewport from the DOM when available; on first paint
|
|
1085
|
+
// fall back to a reasonable default so virtualisation has something to
|
|
1086
|
+
// slice against until the element is measured.
|
|
1087
|
+
const bodyViewport = this.shadowRoot?.querySelector<HTMLElement>('.grid-body-viewport');
|
|
1088
|
+
const bodyViewportHeight = bodyViewport
|
|
1089
|
+
? Math.max(snapshot.rowSize, bodyViewport.clientHeight)
|
|
1090
|
+
: Math.max(snapshot.rowSize, (options.minRowsToShow ?? 10) * snapshot.rowSize);
|
|
1091
|
+
|
|
1458
1092
|
let startIndex = 0;
|
|
1459
|
-
let itemsToRender = snapshot.pipeline.displayItems;
|
|
1093
|
+
let itemsToRender: readonly DisplayItem[] = snapshot.pipeline.displayItems;
|
|
1460
1094
|
let virtualOffset = 0;
|
|
1461
1095
|
const totalVirtualHeight = snapshot.pipeline.displayItems.length * snapshot.rowSize;
|
|
1462
1096
|
|
|
1463
1097
|
if (virtualizationEnabled) {
|
|
1464
|
-
const bodyScrollTop = Math.max(0, this.scrollPosition - stickyChromeHeight);
|
|
1465
1098
|
const overscan = 4;
|
|
1466
|
-
startIndex = Math.max(0, Math.floor(
|
|
1099
|
+
startIndex = Math.max(0, Math.floor(this.scrollPosition / snapshot.rowSize) - overscan);
|
|
1467
1100
|
this.lastVirtualStartIndex = startIndex;
|
|
1468
1101
|
const visibleCount = Math.ceil(bodyViewportHeight / snapshot.rowSize) + overscan * 2;
|
|
1469
1102
|
itemsToRender = snapshot.pipeline.displayItems.slice(
|
|
@@ -1475,6 +1108,65 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1475
1108
|
this.lastVirtualStartIndex = -1;
|
|
1476
1109
|
}
|
|
1477
1110
|
|
|
1111
|
+
// Structure key: only the outer-shell layout. Things that swap the body
|
|
1112
|
+
// container (virtualization flip, empty state, viewport scroll) or the
|
|
1113
|
+
// pagination element appearance are handled *inside* the patch path so a
|
|
1114
|
+
// filter keystroke that crosses the virtualization threshold doesn't
|
|
1115
|
+
// tear down the focused filter input.
|
|
1116
|
+
const columnFingerprint = snapshot.visibleColumns
|
|
1117
|
+
.map(
|
|
1118
|
+
(c) =>
|
|
1119
|
+
`${c.name}:${controller.isPinned(c) ? 'p' : ''}${controller.isPinnedLeftLast(c) ? 'L' : ''}${controller.isPinnedRightFirst(c) ? 'R' : ''}`,
|
|
1120
|
+
)
|
|
1121
|
+
.join('|');
|
|
1122
|
+
const structureKey = [
|
|
1123
|
+
columnFingerprint,
|
|
1124
|
+
filterEnabled ? '1' : '0',
|
|
1125
|
+
paginationEnabled ? '1' : '0',
|
|
1126
|
+
].join('#');
|
|
1127
|
+
|
|
1128
|
+
return {
|
|
1129
|
+
snapshot,
|
|
1130
|
+
options,
|
|
1131
|
+
labels,
|
|
1132
|
+
templateColumns,
|
|
1133
|
+
sortEnabled,
|
|
1134
|
+
filterEnabled,
|
|
1135
|
+
groupingEnabled,
|
|
1136
|
+
pinningEnabled,
|
|
1137
|
+
paginationEnabled,
|
|
1138
|
+
showPagination,
|
|
1139
|
+
virtualizationEnabled,
|
|
1140
|
+
itemsToRender,
|
|
1141
|
+
startIndex,
|
|
1142
|
+
virtualOffset,
|
|
1143
|
+
totalVirtualHeight,
|
|
1144
|
+
structureKey,
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
private renderFull(
|
|
1149
|
+
plan: ReturnType<UiGridStandaloneElement['buildRenderPlan']>,
|
|
1150
|
+
_root: ShadowRoot,
|
|
1151
|
+
): void {
|
|
1152
|
+
const {
|
|
1153
|
+
snapshot,
|
|
1154
|
+
options,
|
|
1155
|
+
labels,
|
|
1156
|
+
templateColumns,
|
|
1157
|
+
sortEnabled,
|
|
1158
|
+
filterEnabled,
|
|
1159
|
+
groupingEnabled,
|
|
1160
|
+
pinningEnabled,
|
|
1161
|
+
paginationEnabled,
|
|
1162
|
+
showPagination,
|
|
1163
|
+
virtualizationEnabled,
|
|
1164
|
+
itemsToRender,
|
|
1165
|
+
startIndex,
|
|
1166
|
+
virtualOffset,
|
|
1167
|
+
totalVirtualHeight,
|
|
1168
|
+
} = plan;
|
|
1169
|
+
|
|
1478
1170
|
const slotRegistry = this.renderSlotRegistry(snapshot.visibleColumns);
|
|
1479
1171
|
const header = snapshot.visibleColumns
|
|
1480
1172
|
.map((column) =>
|
|
@@ -1483,9 +1175,10 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1483
1175
|
.join('');
|
|
1484
1176
|
|
|
1485
1177
|
const filterRow = filterEnabled
|
|
1486
|
-
?
|
|
1487
|
-
|
|
1488
|
-
.join('')
|
|
1178
|
+
? filterRowMarkup(
|
|
1179
|
+
templateColumns,
|
|
1180
|
+
snapshot.visibleColumns.map((column) => this.renderFilterCell(column)).join(''),
|
|
1181
|
+
)
|
|
1489
1182
|
: '';
|
|
1490
1183
|
|
|
1491
1184
|
const bodyContent = itemsToRender
|
|
@@ -1495,51 +1188,103 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1495
1188
|
const body =
|
|
1496
1189
|
snapshot.pipeline.displayItems.length > 0
|
|
1497
1190
|
? virtualizationEnabled
|
|
1498
|
-
?
|
|
1499
|
-
:
|
|
1500
|
-
:
|
|
1191
|
+
? bodyVirtualMarkup(templateColumns, totalVirtualHeight, virtualOffset, bodyContent)
|
|
1192
|
+
: bodyStaticMarkup(templateColumns, bodyContent)
|
|
1193
|
+
: this.renderEmptyState(
|
|
1194
|
+
options.emptyMessage ?? labels.emptyHeading,
|
|
1195
|
+
labels.emptyDescription,
|
|
1196
|
+
);
|
|
1501
1197
|
|
|
1502
1198
|
const pagination = paginationEnabled && showPagination ? this.renderPagination(snapshot) : '';
|
|
1503
|
-
const hasViewportScroll = virtualizationEnabled || options.viewportHeight !== undefined;
|
|
1504
|
-
const gridTableStyle = `${hasViewportScroll ? `height:${viewportHeight}px;overflow-y:auto;` : ''}--ui-grid-header-sticky-top:${headerStickyTop}px;`;
|
|
1505
1199
|
|
|
1506
|
-
|
|
1200
|
+
this.gridTitle = escapeHtml(options.title ?? 'Data grid');
|
|
1201
|
+
const stickyTop = this.measuredHeaderStickyHeight || options.headerRowHeight || 50;
|
|
1202
|
+
const paginationHeight = this.measuredPaginationHeight();
|
|
1203
|
+
const tableHeight = this.clientHeight || ((options.minRowsToShow ?? 10) * snapshot.rowSize + (this.measuredHeaderStickyHeight || options.headerRowHeight || 50) + this.measuredFilterStickyHeight + paginationHeight);
|
|
1204
|
+
this.gridTableStyle = `--ui-grid-header-sticky-top:${stickyTop}px;height:${tableHeight}px;`;
|
|
1205
|
+
this.bodyViewportStyle = '';
|
|
1206
|
+
this.templateColumns = templateColumns;
|
|
1207
|
+
this.slotRegistry = slotRegistry;
|
|
1208
|
+
this.headerContent = header;
|
|
1209
|
+
this.filterRowContent = filterRow;
|
|
1210
|
+
this.bodyContent = body;
|
|
1211
|
+
this.paginationContent = pagination;
|
|
1212
|
+
|
|
1213
|
+
gridShellTemplate(this).connect();
|
|
1214
|
+
|
|
1215
|
+
// Flush framework-rendered slot deltas — dispatches cellSlotsChanged /
|
|
1216
|
+
// expandableRowSlotsChanged / groupRowSlotsChanged / headerSlotsChanged /
|
|
1217
|
+
// filterSlotsChanged / emptyStateSlotChanged with the adds + removes
|
|
1218
|
+
// relative to the previous render.
|
|
1219
|
+
this.frameworkSlots.flush();
|
|
1220
|
+
|
|
1221
|
+
// After a full mount, the new DOM exactly matches the current item set —
|
|
1222
|
+
// seed the fingerprint so the next render can take the fast patch path.
|
|
1223
|
+
this.lastItemsFingerprint = this.fingerprintItems(plan.itemsToRender);
|
|
1224
|
+
this.lastVirtualOffset = plan.virtualOffset;
|
|
1225
|
+
this.lastTotalVirtualHeight = plan.totalVirtualHeight;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
private renderPatch(
|
|
1229
|
+
plan: ReturnType<UiGridStandaloneElement['buildRenderPlan']>,
|
|
1230
|
+
root: ShadowRoot,
|
|
1231
|
+
): void {
|
|
1232
|
+
renderPatch(this, plan, root);
|
|
1233
|
+
}
|
|
1507
1234
|
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1235
|
+
private reconcileBodyRoot(
|
|
1236
|
+
root: ShadowRoot,
|
|
1237
|
+
kind: 'empty' | 'virtual' | 'static',
|
|
1238
|
+
options: GridOptions,
|
|
1239
|
+
labels: GridControllerSnapshot['labels'],
|
|
1240
|
+
templateColumns: string,
|
|
1241
|
+
virtualOffset: number,
|
|
1242
|
+
totalVirtualHeight: number,
|
|
1243
|
+
): HTMLElement | null {
|
|
1244
|
+
return reconcileBodyRoot(this, root, kind, options, labels, templateColumns, virtualOffset, totalVirtualHeight);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
private reconcilePagination(
|
|
1248
|
+
root: ShadowRoot,
|
|
1249
|
+
snapshot: GridControllerSnapshot,
|
|
1250
|
+
shouldShow: boolean,
|
|
1251
|
+
): void {
|
|
1252
|
+
reconcilePagination(this, root, snapshot, shouldShow);
|
|
1253
|
+
}
|
|
1521
1254
|
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
const nextFilterStickyHeight = filterGrid?.offsetHeight ?? 0;
|
|
1526
|
-
const stickyHeightsChanged =
|
|
1527
|
-
nextHeaderStickyHeight !== this.measuredHeaderStickyHeight ||
|
|
1528
|
-
nextFilterStickyHeight !== this.measuredFilterStickyHeight;
|
|
1255
|
+
private patchFilterCells(filterGrid: HTMLElement, snapshot: GridControllerSnapshot): void {
|
|
1256
|
+
patchFilterCells(this, filterGrid, snapshot);
|
|
1257
|
+
}
|
|
1529
1258
|
|
|
1530
|
-
|
|
1531
|
-
|
|
1259
|
+
private fingerprintItems(items: readonly DisplayItem[]): string {
|
|
1260
|
+
return fingerprintItems(items);
|
|
1261
|
+
}
|
|
1532
1262
|
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1263
|
+
private patchExistingRows(
|
|
1264
|
+
bodyContainer: HTMLElement,
|
|
1265
|
+
snapshot: GridControllerSnapshot,
|
|
1266
|
+
itemsToRender: readonly DisplayItem[],
|
|
1267
|
+
startIndex: number,
|
|
1268
|
+
): void {
|
|
1269
|
+
patchExistingRows(this, bodyContainer, snapshot, itemsToRender, startIndex);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
private patchGroupRow(el: HTMLElement, group: GroupItem): void {
|
|
1273
|
+
patchGroupRow(this, el, group);
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
private patchBodyCell(
|
|
1277
|
+
cell: HTMLElement,
|
|
1278
|
+
row: GridRow,
|
|
1279
|
+
column: GridColumnDef,
|
|
1280
|
+
displayIndex: number,
|
|
1281
|
+
templateMarkupMap: Map<string, string | null>,
|
|
1282
|
+
): void {
|
|
1283
|
+
patchBodyCell(this, cell, row, column, displayIndex, templateMarkupMap);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
private patchPagination(paginationEl: HTMLElement, snapshot: GridControllerSnapshot): void {
|
|
1287
|
+
patchPagination(this, paginationEl, snapshot);
|
|
1543
1288
|
}
|
|
1544
1289
|
|
|
1545
1290
|
private renderHeaderCell(
|
|
@@ -1549,177 +1294,102 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1549
1294
|
pinningEnabled: boolean,
|
|
1550
1295
|
options: GridOptions,
|
|
1551
1296
|
): string {
|
|
1552
|
-
|
|
1553
|
-
const sortDirection = controller.getSortDirection(column);
|
|
1554
|
-
const sortLabel = controller.sortButtonLabel(column);
|
|
1555
|
-
const groupingLabel = controller.groupingButtonLabel(column);
|
|
1556
|
-
const canSort = sortEnabled && controller.isColumnSortable(column);
|
|
1557
|
-
const canGroup = groupingEnabled && column.enableGrouping !== false;
|
|
1558
|
-
const canPin = pinningEnabled && controller.isColumnPinnable(column);
|
|
1559
|
-
const isPinned = controller.isPinned(column);
|
|
1560
|
-
const pinOffset = isPinned ? controller.pinnedOffset(column) : null;
|
|
1561
|
-
const isPinnedLeftLast = controller.isPinnedLeftLast(column);
|
|
1562
|
-
const isPinnedRightFirst = controller.isPinnedRightFirst(column);
|
|
1563
|
-
const isDragTarget = this.dropTargetColumnName === column.name;
|
|
1564
|
-
const isDragging = this.draggedColumnName === column.name;
|
|
1565
|
-
const classes = [
|
|
1566
|
-
'header-cell',
|
|
1567
|
-
sortDirection !== SORT_DIRECTIONS.none ? 'is-active' : '',
|
|
1568
|
-
isPinned ? 'is-pinned' : '',
|
|
1569
|
-
isPinnedLeftLast ? 'is-pinned-left-last' : '',
|
|
1570
|
-
isPinnedRightFirst ? 'is-pinned-right-first' : '',
|
|
1571
|
-
this.openPinMenuColumn === column.name ? 'is-pin-menu-open' : '',
|
|
1572
|
-
isDragTarget ? 'is-drag-target' : '',
|
|
1573
|
-
isDragging ? 'is-dragging' : '',
|
|
1574
|
-
]
|
|
1575
|
-
.filter(Boolean)
|
|
1576
|
-
.join(' ');
|
|
1577
|
-
|
|
1578
|
-
const stickyStyle = pinOffset ? `${pinOffset.side}:${pinOffset.offset};` : '';
|
|
1579
|
-
|
|
1580
|
-
const sortIconKey =
|
|
1581
|
-
sortDirection === SORT_DIRECTIONS.asc
|
|
1582
|
-
? 'sortAsc'
|
|
1583
|
-
: sortDirection === SORT_DIRECTIONS.desc
|
|
1584
|
-
? 'sortDesc'
|
|
1585
|
-
: 'sortNone';
|
|
1586
|
-
|
|
1587
|
-
const draggable = canGridMoveColumns(options) ? 'draggable="true"' : '';
|
|
1588
|
-
const pinLabel = isPinned
|
|
1589
|
-
? (this.snapshot?.labels.unpin ?? 'Unpin')
|
|
1590
|
-
: (this.snapshot?.labels.pinColumn ?? 'Pin');
|
|
1591
|
-
const canResize = controller.canResizeColumns();
|
|
1592
|
-
const headerValue = escapeHtml(formatGridHeaderDisplayValue(buildGridHeaderContext(column)));
|
|
1593
|
-
const resizerHtml = canResize
|
|
1594
|
-
? `<button type="button" class="column-resizer" data-action="resize" data-column="${escapeHtml(column.name)}" aria-label="Resize ${escapeHtml(headerValue)} column" title="Drag to resize, double-click to auto fit"></button>`
|
|
1595
|
-
: '';
|
|
1596
|
-
|
|
1597
|
-
return `<div class="${classes}" data-column="${escapeHtml(column.name)}" ${draggable} style="${stickyStyle}"><span class="header-label">${headerValue}</span><span class="header-actions">${sortEnabled ? `<button type="button" class="header-action" data-action="sort" data-column="${escapeHtml(column.name)}" aria-label="${escapeHtml(sortLabel)}" ${canSort ? '' : 'disabled'}>${this.renderControlIcon(sortIconKey)}<span class="sr-only">${escapeHtml(sortLabel)}</span></button>` : ''}${groupingEnabled ? `<button type="button" class="chip-action${controller.isColumnGrouped(column) ? ' chip-action-active' : ''}" data-action="group" data-column="${escapeHtml(column.name)}" aria-label="${escapeHtml(groupingLabel)}" ${canGroup ? '' : 'disabled'}>${this.renderControlIcon('group')}<span class="sr-only">${escapeHtml(groupingLabel)}</span></button>` : ''}${canPin ? `<div class="pin-control${this.openPinMenuColumn === column.name ? ' pin-control-open' : ''}"><button type="button" class="chip-action pin-trigger${isPinned ? ' chip-action-active' : ''}" data-action="pin-trigger" data-column="${escapeHtml(column.name)}" aria-label="${escapeHtml(pinLabel)}">${this.renderControlIcon('pin')}<span class="sr-only">${escapeHtml(pinLabel)}</span></button><div class="pin-menu" role="menu" aria-label="${escapeHtml(pinLabel)}"><button type="button" class="pin-menu-action" data-action="pin-left" data-column="${escapeHtml(column.name)}" aria-label="${escapeHtml(this.snapshot?.labels.pinLeft ?? 'Pin left')}">${this.renderIconWithClass('control-icon', 'pinLeft')}<span class="sr-only">${escapeHtml(this.snapshot?.labels.pinLeft ?? 'Pin left')}</span></button><button type="button" class="pin-menu-action" data-action="pin-right" data-column="${escapeHtml(column.name)}" aria-label="${escapeHtml(this.snapshot?.labels.pinRight ?? 'Pin right')}">${this.renderIconWithClass('control-icon', 'pinRight')}<span class="sr-only">${escapeHtml(this.snapshot?.labels.pinRight ?? 'Pin right')}</span></button></div></div>` : ''}</span>${resizerHtml}</div>`;
|
|
1297
|
+
return renderHeaderCell(this, column, sortEnabled, groupingEnabled, pinningEnabled, options);
|
|
1598
1298
|
}
|
|
1599
1299
|
|
|
1600
1300
|
private renderFilterCell(column: GridColumnDef): string {
|
|
1601
|
-
|
|
1602
|
-
const controller = this.controller!;
|
|
1603
|
-
const canFilter = controller.isColumnFilterable(column);
|
|
1604
|
-
const pinOffset = controller.isPinned(column) ? controller.pinnedOffset(column) : null;
|
|
1605
|
-
const classes = [
|
|
1606
|
-
'filter-cell',
|
|
1607
|
-
controller.isPinned(column) ? 'is-pinned' : '',
|
|
1608
|
-
controller.isPinnedLeftLast(column) ? 'is-pinned-left-last' : '',
|
|
1609
|
-
controller.isPinnedRightFirst(column) ? 'is-pinned-right-first' : '',
|
|
1610
|
-
]
|
|
1611
|
-
.filter(Boolean)
|
|
1612
|
-
.join(' ');
|
|
1613
|
-
const stickyStyle = pinOffset ? `${pinOffset.side}:${pinOffset.offset};` : '';
|
|
1614
|
-
return `<label class="${classes}" style="${stickyStyle}"><input class="ui-grid-filter-input" data-role="filter" data-column="${escapeHtml(column.name)}" placeholder="${escapeHtml(controller.filterPlaceholder(column))}" value="${escapeHtml(value)}" ${canFilter ? '' : 'disabled'}></label>`;
|
|
1301
|
+
return renderFilterCell(this, column);
|
|
1615
1302
|
}
|
|
1616
1303
|
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1304
|
+
/**
|
|
1305
|
+
* Commit the in-flight edit, then move keyboard focus relative to the
|
|
1306
|
+
* committed cell. Splits commit + focus across two paint frames: commit
|
|
1307
|
+
* runs synchronously (editor unmounts, cell re-renders with the new value),
|
|
1308
|
+
* then focus moves to the adjacent cell. Scheduling the focus in a
|
|
1309
|
+
* microtask avoids racing the blur handler's re-entry guard and ensures
|
|
1310
|
+
* the destination cell is fully reattached in the DOM.
|
|
1311
|
+
*
|
|
1312
|
+
* If the source cell was being edited, the destination cell enters edit
|
|
1313
|
+
* mode too (provided the destination column is editable). This keeps
|
|
1314
|
+
* Tab/Enter nav inside an edit session continuous — you keep editing as
|
|
1315
|
+
* you move. Arrow nav outside an edit session does NOT auto-enter edit.
|
|
1316
|
+
*/
|
|
1317
|
+
private commitAndMove(
|
|
1318
|
+
fromRowId: string | null,
|
|
1319
|
+
fromColumnName: string | null,
|
|
1320
|
+
direction: 'left' | 'right' | 'up' | 'down',
|
|
1321
|
+
): void {
|
|
1322
|
+
commitAndMove(this, fromRowId, fromColumnName, direction);
|
|
1633
1323
|
}
|
|
1634
1324
|
|
|
1635
|
-
private
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1325
|
+
private moveGridFocus(
|
|
1326
|
+
direction: GridMoveDirection,
|
|
1327
|
+
rowId: string | null,
|
|
1328
|
+
columnName: string | null,
|
|
1329
|
+
opts: { resumeEdit?: boolean } = {},
|
|
1330
|
+
): void {
|
|
1331
|
+
moveGridFocus(this, direction, rowId, columnName, opts);
|
|
1332
|
+
}
|
|
1639
1333
|
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
const disclosureLabel = this.controller.groupDisclosureLabel(group);
|
|
1644
|
-
return `<button type="button" class="group-row ui-grid-row ui-grid-group-row" data-action="toggle-group" data-group="${escapeHtml(group.id)}" data-collapsed="${group.collapsed ? 'true' : 'false'}" style="grid-column: 1 / -1; padding-inline-start:${group.depth * 20 + 10}px">${this.renderIconWithClass('toggle-icon group-disclosure-icon', iconKey)}<span class="sr-only">${escapeHtml(disclosureLabel)}</span><strong>${escapeHtml(group.field)}: ${escapeHtml(group.label)}</strong><span>${group.count} ${escapeHtml(this.snapshot.labels.groupRowsSuffix)}</span></button>`;
|
|
1645
|
-
}
|
|
1334
|
+
private scrollFocusedRowIntoView(rowId: string): void {
|
|
1335
|
+
scrollFocusedRowIntoView(this, rowId);
|
|
1336
|
+
}
|
|
1646
1337
|
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
}
|
|
1338
|
+
private handleRowSelectionClick(rowId: string, columnName: string, event: Event): void {
|
|
1339
|
+
handleRowSelectionClick(this, rowId, columnName, event);
|
|
1340
|
+
}
|
|
1651
1341
|
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1342
|
+
private handleRowHeaderCheckboxClick(rowId: string, event: Event): void {
|
|
1343
|
+
handleRowHeaderCheckboxClick(this, rowId, event);
|
|
1344
|
+
}
|
|
1655
1345
|
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1346
|
+
private matchesKeyOverride(
|
|
1347
|
+
override: {
|
|
1348
|
+
keyCode?: number;
|
|
1349
|
+
key?: string;
|
|
1350
|
+
shiftKey?: boolean;
|
|
1351
|
+
ctrlKey?: boolean;
|
|
1352
|
+
altKey?: boolean;
|
|
1353
|
+
metaKey?: boolean;
|
|
1354
|
+
},
|
|
1355
|
+
event: KeyboardEvent,
|
|
1356
|
+
): boolean {
|
|
1357
|
+
return matchesKeyOverride(override, event);
|
|
1659
1358
|
}
|
|
1660
1359
|
|
|
1661
|
-
private
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1360
|
+
private applyFocusedCellClass(
|
|
1361
|
+
previous: { rowId: string; columnName: string } | null,
|
|
1362
|
+
next: { rowId: string; columnName: string } | null,
|
|
1363
|
+
): void {
|
|
1364
|
+
applyFocusedCellClass(this, previous, next);
|
|
1365
|
+
}
|
|
1665
1366
|
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
const editing = controller.isEditingCell(rowId, columnName);
|
|
1670
|
-
const pinOffset = controller.isPinned(column) ? controller.pinnedOffset(column) : null;
|
|
1671
|
-
const treeToggle = controller.showTreeToggle(row, column)
|
|
1672
|
-
? `<button type="button" class="row-toggle" data-action="toggle-tree" data-row="${escapeHtml(rowId)}" aria-label="${escapeHtml(controller.treeToggleLabel(row))}">${this.renderIconWithClass('toggle-icon', controller.isTreeRowExpanded(row) ? 'treeExpanded' : 'treeCollapsed')}<span class="sr-only">${escapeHtml(controller.treeToggleLabel(row))}</span></button>`
|
|
1673
|
-
: '';
|
|
1674
|
-
const expandToggle = controller.showExpandToggle(row, column)
|
|
1675
|
-
? `<button type="button" class="row-toggle row-toggle-expand" data-action="toggle-expand" data-row="${escapeHtml(rowId)}" aria-label="${escapeHtml(controller.expandToggleLabel(row))}">${this.renderIconWithClass('toggle-icon', row.expanded ? 'expandExpanded' : 'expandCollapsed')}<span class="sr-only">${escapeHtml(controller.expandToggleLabel(row))}</span></button>`
|
|
1676
|
-
: '';
|
|
1367
|
+
private focusCellElement(rowId: string | null, columnName: string | null): void {
|
|
1368
|
+
focusCellElement(this, rowId, columnName);
|
|
1369
|
+
}
|
|
1677
1370
|
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
controller.isPinned(column) ? 'is-pinned' : '',
|
|
1689
|
-
controller.isPinnedLeftLast(column) ? 'is-pinned-left-last' : '',
|
|
1690
|
-
controller.isPinnedRightFirst(column) ? 'is-pinned-right-first' : '',
|
|
1691
|
-
this.focusedCell?.rowId === rowId && this.focusedCell.columnName === columnName
|
|
1692
|
-
? 'cell-focused'
|
|
1693
|
-
: '',
|
|
1694
|
-
editing ? 'cell-editing' : '',
|
|
1695
|
-
]
|
|
1696
|
-
.filter(Boolean)
|
|
1697
|
-
.join(' ');
|
|
1698
|
-
|
|
1699
|
-
const stickyStyle = pinOffset ? `${pinOffset.side}:${pinOffset.offset};` : '';
|
|
1700
|
-
return `<div class="${classes}" tabindex="0" data-row="${escapeHtml(rowId)}" data-column="${escapeHtml(columnName)}" style="${stickyStyle}"><div class="cell-shell" style="padding-inline-start:${escapeHtml(controller.cellIndent(row, column))}">${treeToggle}${expandToggle}<div class="cell-content">${content}</div></div></div>`;
|
|
1371
|
+
private measureAutoColumnWidth(columnName: string): number {
|
|
1372
|
+
return measureAutoColumnWidth(this, columnName);
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
private renderDisplayItem(item: DisplayItem, displayIndex: number): string {
|
|
1376
|
+
return renderDisplayItem(this, item, displayIndex);
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
private renderBodyCell(row: GridRow, column: GridColumnDef, displayIndex: number): string {
|
|
1380
|
+
return renderBodyCell(this, row, column, displayIndex);
|
|
1701
1381
|
}
|
|
1702
1382
|
|
|
1703
1383
|
private renderPagination(snapshot: GridControllerSnapshot): string {
|
|
1704
|
-
|
|
1705
|
-
return `<footer class="pagination-bar ui-grid-pagination"><p>${snapshot.firstRowIndex + 1}-${snapshot.lastRowIndex + 1} of ${snapshot.pipeline.totalItems}</p><div class="pagination-controls"><button type="button" class="action action-secondary pagination-button" data-action="page-prev" aria-label="${escapeHtml(snapshot.labels.paginationPrevious)}" ${snapshot.currentPage <= 1 ? 'disabled' : ''}>${this.renderIconWithClass('pagination-icon', 'paginationPrev')}<span class="sr-only">${escapeHtml(snapshot.labels.paginationPrevious)}</span></button><span>${escapeHtml(snapshot.labels.paginationPage)} ${snapshot.currentPage} ${escapeHtml(snapshot.labels.paginationOf)} ${snapshot.totalPages}</span><button type="button" class="action action-secondary pagination-button" data-action="page-next" aria-label="${escapeHtml(snapshot.labels.paginationNext)}" ${snapshot.currentPage >= snapshot.totalPages ? 'disabled' : ''}>${this.renderIconWithClass('pagination-icon', 'paginationNext')}<span class="sr-only">${escapeHtml(snapshot.labels.paginationNext)}</span></button><label class="pagination-size"><span class="sr-only">${escapeHtml(snapshot.labels.paginationRows)}</span><select class="page-size" data-role="page-size">${pageSizes
|
|
1706
|
-
.map(
|
|
1707
|
-
(size) =>
|
|
1708
|
-
`<option value="${size}" ${size === snapshot.pageSize ? 'selected' : ''}>${size}</option>`,
|
|
1709
|
-
)
|
|
1710
|
-
.join('')}</select></label></div></footer>`;
|
|
1384
|
+
return renderPagination(this, snapshot);
|
|
1711
1385
|
}
|
|
1712
1386
|
|
|
1713
1387
|
private renderSlotRegistry(columns: readonly GridColumnDef[]): string {
|
|
1714
|
-
|
|
1715
|
-
.map((column) => `<slot name="${escapeHtml(this.cellSlotName(column))}"></slot>`)
|
|
1716
|
-
.join('');
|
|
1717
|
-
return `<div hidden class="slot-registry">${cellSlots}<slot name="expandable-row"></slot></div>`;
|
|
1388
|
+
return renderSlotRegistry(columns);
|
|
1718
1389
|
}
|
|
1719
1390
|
|
|
1720
1391
|
private renderCellTemplate(row: GridRow, column: GridColumnDef, displayIndex: number): string {
|
|
1721
|
-
|
|
1722
|
-
return this.renderCellTemplateFromMarkup(row, column, displayIndex, templateMarkup);
|
|
1392
|
+
return renderCellTemplate(this, row, column, displayIndex);
|
|
1723
1393
|
}
|
|
1724
1394
|
|
|
1725
1395
|
private renderCellTemplateFromMarkup(
|
|
@@ -1728,62 +1398,20 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1728
1398
|
displayIndex: number,
|
|
1729
1399
|
templateMarkup: string | null,
|
|
1730
1400
|
): string {
|
|
1731
|
-
|
|
1732
|
-
return `<span class="cell-value">${escapeHtml(this.controller?.displayValue(row, column) ?? '')}</span>`;
|
|
1733
|
-
}
|
|
1734
|
-
|
|
1735
|
-
const rawRow = row.entity as GridRecord;
|
|
1736
|
-
const rawValue = getCellValue(rawRow, column);
|
|
1737
|
-
const valueText = rawValue == null ? '' : String(rawValue);
|
|
1738
|
-
return this.interpolateTemplate(templateMarkup, {
|
|
1739
|
-
$implicit: rawValue,
|
|
1740
|
-
value: rawValue,
|
|
1741
|
-
valueText,
|
|
1742
|
-
valueLower: valueText.toLowerCase(),
|
|
1743
|
-
row: rawRow,
|
|
1744
|
-
column,
|
|
1745
|
-
rowIndex: this.rowIndexFor(row, displayIndex),
|
|
1746
|
-
});
|
|
1401
|
+
return renderCellTemplateFromMarkup(this, row, column, displayIndex, templateMarkup);
|
|
1747
1402
|
}
|
|
1748
1403
|
|
|
1749
1404
|
private renderExpandableTemplate(row: GridRow): string {
|
|
1750
|
-
|
|
1751
|
-
if (!templateMarkup) {
|
|
1752
|
-
return `<p>${escapeHtml(String(row.entity['name'] ?? row.id))}</p>`;
|
|
1753
|
-
}
|
|
1754
|
-
|
|
1755
|
-
return this.interpolateTemplate(templateMarkup, {
|
|
1756
|
-
$implicit: row.entity,
|
|
1757
|
-
row: row.entity,
|
|
1758
|
-
expanded: row.expanded,
|
|
1759
|
-
rowIndex: this.rowIndexFor(row),
|
|
1760
|
-
});
|
|
1405
|
+
return renderExpandableTemplate(this, row);
|
|
1761
1406
|
}
|
|
1762
1407
|
|
|
1763
1408
|
private getTemplateMarkup(slotName: string): string | null {
|
|
1764
|
-
|
|
1765
|
-
return template?.innerHTML ?? null;
|
|
1766
|
-
}
|
|
1767
|
-
|
|
1768
|
-
private interpolateTemplate(templateMarkup: string, context: Record<string, unknown>): string {
|
|
1769
|
-
return templateMarkup.replace(/{{\s*([^}]+?)\s*}}/g, (_match, expression) => {
|
|
1770
|
-
const value = this.resolveTemplateValue(context, String(expression).trim());
|
|
1771
|
-
return escapeHtml(value);
|
|
1772
|
-
});
|
|
1409
|
+
return getTemplateMarkupFromLightDom(this, slotName);
|
|
1773
1410
|
}
|
|
1774
1411
|
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
for (const segment of segments) {
|
|
1780
|
-
if (current == null || typeof current !== 'object') {
|
|
1781
|
-
return '';
|
|
1782
|
-
}
|
|
1783
|
-
current = (current as Record<string, unknown>)[segment];
|
|
1784
|
-
}
|
|
1785
|
-
|
|
1786
|
-
return current ?? '';
|
|
1412
|
+
/** @internal Used by the render module to interpolate consumer templates. */
|
|
1413
|
+
interpolateTemplate(templateMarkup: string, context: Record<string, unknown>): string {
|
|
1414
|
+
return interpolateGridTemplate(templateMarkup, context, escapeHtml);
|
|
1787
1415
|
}
|
|
1788
1416
|
|
|
1789
1417
|
private rowIndexFor(row: GridRow, fallback = 0): number {
|
|
@@ -1813,7 +1441,9 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1813
1441
|
}
|
|
1814
1442
|
|
|
1815
1443
|
for (let index = 0; index < previousSnapshot.visibleColumns.length; index += 1) {
|
|
1816
|
-
if (
|
|
1444
|
+
if (
|
|
1445
|
+
previousSnapshot.visibleColumns[index]?.name !== nextSnapshot.visibleColumns[index]?.name
|
|
1446
|
+
) {
|
|
1817
1447
|
return { mode: 'full', changedRowIds: null };
|
|
1818
1448
|
}
|
|
1819
1449
|
}
|
|
@@ -1871,12 +1501,12 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1871
1501
|
};
|
|
1872
1502
|
}
|
|
1873
1503
|
|
|
1874
|
-
const
|
|
1875
|
-
const
|
|
1876
|
-
|
|
1877
|
-
|
|
1504
|
+
const bodyViewport = this.shadowRoot?.querySelector<HTMLElement>('.grid-body-viewport');
|
|
1505
|
+
const bodyViewportHeight = bodyViewport
|
|
1506
|
+
? Math.max(snapshot.rowSize, bodyViewport.clientHeight)
|
|
1507
|
+
: Math.max(snapshot.rowSize, (snapshot.options.minRowsToShow ?? 10) * snapshot.rowSize);
|
|
1878
1508
|
const overscan = 4;
|
|
1879
|
-
const startIndex = Math.max(0, Math.floor(
|
|
1509
|
+
const startIndex = Math.max(0, Math.floor(this.scrollPosition / snapshot.rowSize) - overscan);
|
|
1880
1510
|
const visibleCount = Math.ceil(bodyViewportHeight / snapshot.rowSize) + overscan * 2;
|
|
1881
1511
|
|
|
1882
1512
|
return {
|
|
@@ -1892,29 +1522,38 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1892
1522
|
return `cell-${column.name}`;
|
|
1893
1523
|
}
|
|
1894
1524
|
|
|
1895
|
-
private
|
|
1896
|
-
|
|
1525
|
+
private renderEmptyState(headingRaw: string, descriptionRaw: string): string {
|
|
1526
|
+
return renderEmptyState(this, headingRaw, descriptionRaw);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
/** @internal */
|
|
1530
|
+
downloadCsv(
|
|
1531
|
+
rowType: 'all' | 'visible' | 'selected' = 'visible',
|
|
1532
|
+
colType: 'all' | 'visible' = 'visible',
|
|
1533
|
+
): void {
|
|
1534
|
+
if (!this.controller) {
|
|
1897
1535
|
return;
|
|
1898
1536
|
}
|
|
1899
|
-
|
|
1900
|
-
const csv = exportCsvRows(this.snapshot.visibleColumns, this.snapshot.pipeline.visibleRows);
|
|
1901
|
-
const filename = sanitizeDownloadFilename(`${this.snapshot.options.id}.csv`);
|
|
1902
|
-
downloadGridCsvFile(csv, filename);
|
|
1537
|
+
this.controller.gridApi.exporter.csvExport(rowType, colType);
|
|
1903
1538
|
}
|
|
1904
1539
|
|
|
1905
1540
|
private renderControlIcon(key: UiGridControlIconKey): string {
|
|
1906
|
-
return this.
|
|
1541
|
+
return this.icons.renderControlIcon(key);
|
|
1907
1542
|
}
|
|
1908
1543
|
|
|
1909
1544
|
private renderIconWithClass(svgClass: string, key: UiGridControlIconKey): string {
|
|
1910
|
-
|
|
1911
|
-
const viewBox = escapeHtml(icon.viewBox ?? '0 0 24 24');
|
|
1912
|
-
const path = escapeHtml(icon.path);
|
|
1913
|
-
return `<svg class="${svgClass}" viewBox="${viewBox}" aria-hidden="true" focusable="false"><path d="${path}"></path></svg>`;
|
|
1545
|
+
return this.icons.renderIconWithClass(svgClass, key);
|
|
1914
1546
|
}
|
|
1915
1547
|
}
|
|
1916
1548
|
|
|
1917
1549
|
export async function defineStandaloneUiGridElement(tagName = 'ui-grid-element'): Promise<void> {
|
|
1550
|
+
UIGridFilterCell.define();
|
|
1551
|
+
UIGridGroupRow.define();
|
|
1552
|
+
UIGridPagination.define();
|
|
1553
|
+
UIGridBodyCell.define();
|
|
1554
|
+
UIGridHeaderCell.define();
|
|
1555
|
+
UIGridTemplate.define();
|
|
1556
|
+
UIGridCellEditor.define();
|
|
1918
1557
|
if (!customElements.get(tagName)) {
|
|
1919
1558
|
customElements.define(tagName, UiGridStandaloneElement);
|
|
1920
1559
|
}
|