@ornery/ui-grid-vanilla 0.1.10 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +196 -0
- package/demo/main.ts +0 -1
- package/dist/index.d.mts +575 -55
- package/dist/index.d.ts +575 -55
- package/dist/index.js +4894 -1503
- package/dist/index.mjs +4994 -1556
- 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 +206 -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 +815 -0
- package/src/exporter.test.ts +186 -0
- package/src/focus.ts +353 -0
- package/src/framework-slots.ts +440 -0
- package/src/grid-controller.ts +1402 -18
- 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 +741 -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,103 @@ 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) {
|
|
882
712
|
this.controller.setOptions(options);
|
|
883
713
|
return;
|
|
884
714
|
}
|
|
885
715
|
|
|
886
716
|
this.controller = createVanillaGridController(options);
|
|
717
|
+
// Let the controller's cellNav.scrollToFocus delegate into our
|
|
718
|
+
// element's scroll-and-focus helpers.
|
|
719
|
+
this.controller.setCellNavScrollHandler((rowId, columnName) => {
|
|
720
|
+
if (!rowId || !columnName) return;
|
|
721
|
+
this.scrollFocusedRowIntoView(rowId);
|
|
722
|
+
this.focusCellElement(rowId, columnName);
|
|
723
|
+
});
|
|
724
|
+
// Infinite-scroll resetScroll() brings the viewport back to the top.
|
|
725
|
+
this.controller.setInfiniteScrollResetHandler(() => {
|
|
726
|
+
const bodyViewport = this.shadowRoot?.querySelector<HTMLElement>('.grid-body-viewport');
|
|
727
|
+
if (bodyViewport) {
|
|
728
|
+
bodyViewport.scrollTop = 0;
|
|
729
|
+
this.scrollPosition = 0;
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
// saveState scroll capture/restore — element owns the DOM element, so
|
|
733
|
+
// it provides the accessors the controller uses when save()/setState()
|
|
734
|
+
// handle scroll-position fields.
|
|
735
|
+
this.controller.setSaveStateScrollHandlers(
|
|
736
|
+
() => ({ scrollTop: this.scrollPosition, scrollLeft: this.horizontalScrollPosition }),
|
|
737
|
+
(scrollTop, scrollLeft) => {
|
|
738
|
+
const bodyViewport = this.shadowRoot?.querySelector<HTMLElement>('.grid-body-viewport');
|
|
739
|
+
if (!bodyViewport) return;
|
|
740
|
+
// Defer to a rAF so the body has finished laying out after refresh.
|
|
741
|
+
requestAnimationFrame(() => {
|
|
742
|
+
bodyViewport.scrollTop = scrollTop;
|
|
743
|
+
bodyViewport.scrollLeft = scrollLeft;
|
|
744
|
+
this.scrollPosition = scrollTop;
|
|
745
|
+
this.horizontalScrollPosition = scrollLeft;
|
|
746
|
+
// Keep header/filter strips aligned after a restore.
|
|
747
|
+
this.syncHeaderHorizontalScroll(scrollLeft);
|
|
748
|
+
});
|
|
749
|
+
},
|
|
750
|
+
);
|
|
751
|
+
// Importer file-picker: the element mounts a hidden <input type="file">
|
|
752
|
+
// the first time the controller asks for one, then keeps it around so
|
|
753
|
+
// subsequent imports reuse it. The controller stays DOM-free.
|
|
754
|
+
this.controller.setImporterFilePickerHandler(() => {
|
|
755
|
+
this.openImporterFilePicker();
|
|
756
|
+
});
|
|
887
757
|
this.unsubscribe = this.controller.subscribe((snapshot) => {
|
|
888
758
|
this.snapshot = snapshot;
|
|
889
759
|
this.render();
|
|
890
760
|
});
|
|
891
761
|
}
|
|
892
762
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
763
|
+
/** Hidden file input used by `gridApi.importer.importAFile()`. Lazily
|
|
764
|
+
* created on first use and reused thereafter. */
|
|
765
|
+
private importerFileInput: HTMLInputElement | null = null;
|
|
766
|
+
|
|
767
|
+
private openImporterFilePicker(): void {
|
|
768
|
+
if (!this.controller) return;
|
|
769
|
+
if (!this.importerFileInput) {
|
|
770
|
+
const input = document.createElement('input');
|
|
771
|
+
input.type = 'file';
|
|
772
|
+
input.accept = '.csv,.json,text/csv,application/json,text/plain';
|
|
773
|
+
input.style.display = 'none';
|
|
774
|
+
input.addEventListener('change', () => {
|
|
775
|
+
const file = input.files?.[0];
|
|
776
|
+
if (file && this.controller) {
|
|
777
|
+
void this.controller.gridApi.importer.importThisFile(file);
|
|
778
|
+
}
|
|
779
|
+
// Clear so the same file can be re-imported.
|
|
780
|
+
input.value = '';
|
|
781
|
+
});
|
|
782
|
+
this.appendChild(input);
|
|
783
|
+
this.importerFileInput = input;
|
|
896
784
|
}
|
|
785
|
+
this.importerFileInput.click();
|
|
786
|
+
}
|
|
897
787
|
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
} else {
|
|
902
|
-
this.render();
|
|
903
|
-
}
|
|
904
|
-
});
|
|
788
|
+
private observeTemplateSlots(): void {
|
|
789
|
+
observeTemplateSlots(this);
|
|
790
|
+
}
|
|
905
791
|
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
792
|
+
private bindEvents(): void {
|
|
793
|
+
bindEvents(this);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
private syncHeaderHorizontalScroll(scrollLeft: number): void {
|
|
797
|
+
syncHeaderHorizontalScroll(this, scrollLeft);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
private maybeTriggerInfiniteScroll(): void {
|
|
801
|
+
maybeTriggerInfiniteScroll(this);
|
|
912
802
|
}
|
|
913
803
|
|
|
914
|
-
|
|
804
|
+
/** @internal */
|
|
805
|
+
buildEffectiveOptions(options: VanillaGridOptions): VanillaGridOptions {
|
|
915
806
|
// Merge: attributes are overridden by explicit JS property values.
|
|
916
807
|
// JS property always wins: { ...attributeOptions, ...options }
|
|
917
808
|
const merged = {
|
|
@@ -919,8 +810,7 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
919
810
|
...options,
|
|
920
811
|
} as VanillaGridOptions;
|
|
921
812
|
|
|
922
|
-
|
|
923
|
-
if (merged.enableExpandable && hasExpandableSlot && !merged.expandableRowTemplate) {
|
|
813
|
+
if (merged.enableExpandable && !merged.expandableRowTemplate) {
|
|
924
814
|
merged.expandableRowTemplate = {
|
|
925
815
|
createEmbeddedView: () => undefined,
|
|
926
816
|
};
|
|
@@ -929,450 +819,28 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
929
819
|
return merged;
|
|
930
820
|
}
|
|
931
821
|
|
|
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
|
-
|
|
822
|
+
/**
|
|
823
|
+
* Ask the controller to evaluate whether the current scroll position
|
|
824
|
+
* should request more data at the top or bottom — ports the old grid's
|
|
825
|
+
* handleScroll → loadData check. The controller does the actual
|
|
826
|
+
* needLoadMoreData / needLoadMoreDataTop raise.
|
|
827
|
+
*/
|
|
828
|
+
/**
|
|
829
|
+
* Mirror horizontal scroll from the body viewport onto the header + filter
|
|
830
|
+
* strips by setting `scrollLeft` on them. Because those strips are real
|
|
831
|
+
* scroll containers (with their scrollbars hidden), `position: sticky` on
|
|
832
|
+
* pinned header / filter cells anchors to the strip's scroll position the
|
|
833
|
+
* same way body cells anchor to `.grid-body-viewport`. This is the
|
|
834
|
+
* imperative sync strategy the old ui-grid used in ui-grid-viewport.js.
|
|
835
|
+
*/
|
|
1369
836
|
/**
|
|
1370
837
|
* Targeted update for virtual scroll: replaces only the rendered rows inside
|
|
1371
838
|
* the existing `.grid-virtual-body` without touching the scroll container.
|
|
1372
839
|
* This preserves momentum/inertia scrolling because the `.grid-table` element
|
|
1373
840
|
* is never destroyed.
|
|
1374
841
|
*/
|
|
1375
|
-
|
|
842
|
+
/** @internal */
|
|
843
|
+
renderVirtualBody(): void {
|
|
1376
844
|
const snapshot = this.snapshot;
|
|
1377
845
|
if (!snapshot?.pipeline.virtualizationEnabled) {
|
|
1378
846
|
this.render();
|
|
@@ -1393,12 +861,12 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1393
861
|
this.dataFrame = null;
|
|
1394
862
|
}
|
|
1395
863
|
|
|
1396
|
-
const
|
|
1397
|
-
const
|
|
1398
|
-
|
|
1399
|
-
|
|
864
|
+
const bodyViewport = root?.querySelector<HTMLElement>('.grid-body-viewport');
|
|
865
|
+
const bodyViewportHeight = bodyViewport
|
|
866
|
+
? Math.max(snapshot.rowSize, bodyViewport.clientHeight)
|
|
867
|
+
: Math.max(snapshot.rowSize, (snapshot.options.minRowsToShow ?? 10) * snapshot.rowSize);
|
|
1400
868
|
const overscan = 4;
|
|
1401
|
-
const startIndex = Math.max(0, Math.floor(
|
|
869
|
+
const startIndex = Math.max(0, Math.floor(this.scrollPosition / snapshot.rowSize) - overscan);
|
|
1402
870
|
this.lastVirtualStartIndex = startIndex;
|
|
1403
871
|
const visibleCount = Math.ceil(bodyViewportHeight / snapshot.rowSize) + overscan * 2;
|
|
1404
872
|
const itemsToRender = snapshot.pipeline.displayItems.slice(
|
|
@@ -1411,9 +879,14 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1411
879
|
virtualBody.innerHTML = itemsToRender
|
|
1412
880
|
.map((item, index) => this.renderDisplayItem(item, startIndex + index))
|
|
1413
881
|
.join('');
|
|
882
|
+
// The body fingerprint now reflects the freshly-rendered slice so the
|
|
883
|
+
// next full render() can take the fast per-cell patch path.
|
|
884
|
+
this.lastItemsFingerprint = this.fingerprintItems(itemsToRender);
|
|
885
|
+
this.lastVirtualOffset = virtualOffset;
|
|
1414
886
|
}
|
|
1415
887
|
|
|
1416
|
-
|
|
888
|
+
/** @internal */
|
|
889
|
+
render(): void {
|
|
1417
890
|
if (this.skipNextRender) {
|
|
1418
891
|
return;
|
|
1419
892
|
}
|
|
@@ -1429,17 +902,164 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1429
902
|
}
|
|
1430
903
|
const root = this.ensureShadowRoot();
|
|
1431
904
|
const snapshot = this.snapshot;
|
|
1432
|
-
const
|
|
1433
|
-
if (
|
|
1434
|
-
this.scrollPosition =
|
|
1435
|
-
this.horizontalScrollPosition =
|
|
905
|
+
const previousViewport = root.querySelector<HTMLElement>('.grid-body-viewport');
|
|
906
|
+
if (previousViewport && !this.suppressScrollEvent) {
|
|
907
|
+
this.scrollPosition = previousViewport.scrollTop;
|
|
908
|
+
this.horizontalScrollPosition = previousViewport.scrollLeft;
|
|
1436
909
|
}
|
|
1437
910
|
|
|
1438
911
|
if (!snapshot) {
|
|
1439
|
-
|
|
912
|
+
this.lastStructureKey = null;
|
|
913
|
+
this.lastItemsFingerprint = null;
|
|
914
|
+
emptyTemplate({ message: 'No grid options provided.' }).connect(root);
|
|
1440
915
|
return;
|
|
1441
916
|
}
|
|
1442
917
|
|
|
918
|
+
const plan = this.buildRenderPlan(snapshot);
|
|
919
|
+
|
|
920
|
+
// The shadow-DOM structure is only safe to patch when the set of sub-regions,
|
|
921
|
+
// the visible column identity/order, and the pin/virt/filter/pagination
|
|
922
|
+
// toggles are unchanged. Otherwise the existing nodes don't line up with the
|
|
923
|
+
// new snapshot — fall back to a full re-mount.
|
|
924
|
+
const canPatch =
|
|
925
|
+
this.lastStructureKey !== null &&
|
|
926
|
+
this.lastStructureKey === plan.structureKey &&
|
|
927
|
+
root.querySelector('.grid-frame') !== null;
|
|
928
|
+
|
|
929
|
+
if (canPatch) {
|
|
930
|
+
this.renderPatch(plan, root);
|
|
931
|
+
} else {
|
|
932
|
+
this.renderFull(plan, root);
|
|
933
|
+
}
|
|
934
|
+
this.lastStructureKey = plan.structureKey;
|
|
935
|
+
|
|
936
|
+
const bodyViewport = root.querySelector<HTMLElement>('.grid-body-viewport');
|
|
937
|
+
if (bodyViewport && (this.scrollPosition > 0 || this.horizontalScrollPosition > 0)) {
|
|
938
|
+
this.suppressScrollEvent = true;
|
|
939
|
+
if (this.scrollPosition > 0) {
|
|
940
|
+
bodyViewport.scrollTop = this.scrollPosition;
|
|
941
|
+
}
|
|
942
|
+
if (this.horizontalScrollPosition > 0) {
|
|
943
|
+
bodyViewport.scrollLeft = this.horizontalScrollPosition;
|
|
944
|
+
}
|
|
945
|
+
// Keep the header/filter strips in sync on the first paint too.
|
|
946
|
+
this.syncHeaderHorizontalScroll(this.horizontalScrollPosition);
|
|
947
|
+
requestAnimationFrame(() => {
|
|
948
|
+
this.suppressScrollEvent = false;
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const headerGrid = root.querySelector<HTMLElement>('.header-grid');
|
|
953
|
+
const filterGrid = root.querySelector<HTMLElement>('.filter-grid');
|
|
954
|
+
const nextHeaderStickyHeight = headerGrid?.offsetHeight ?? 0;
|
|
955
|
+
const nextFilterStickyHeight = filterGrid?.offsetHeight ?? 0;
|
|
956
|
+
const stickyHeightsChanged =
|
|
957
|
+
nextHeaderStickyHeight !== this.measuredHeaderStickyHeight ||
|
|
958
|
+
nextFilterStickyHeight !== this.measuredFilterStickyHeight;
|
|
959
|
+
|
|
960
|
+
this.measuredHeaderStickyHeight = nextHeaderStickyHeight;
|
|
961
|
+
this.measuredFilterStickyHeight = nextFilterStickyHeight;
|
|
962
|
+
|
|
963
|
+
if (stickyHeightsChanged && !this.stickyHeightRelayoutQueued) {
|
|
964
|
+
this.stickyHeightRelayoutQueued = true;
|
|
965
|
+
requestAnimationFrame(() => {
|
|
966
|
+
this.stickyHeightRelayoutQueued = false;
|
|
967
|
+
if (!this.snapshot) {
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
this.render();
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
this.autoAdjustHeight(snapshot);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/** @internal */
|
|
978
|
+
private inAutoAdjust = false;
|
|
979
|
+
private lastAutoHeight = 0;
|
|
980
|
+
|
|
981
|
+
private autoAdjustHeight(snapshot: GridControllerSnapshot): void {
|
|
982
|
+
if (this.inAutoAdjust) return;
|
|
983
|
+
const options = snapshot.options;
|
|
984
|
+
if (options.enableMinHeightCheck === false) return;
|
|
985
|
+
|
|
986
|
+
const rowHeight = snapshot.rowSize;
|
|
987
|
+
const minRows = options.minRowsToShow ?? 10;
|
|
988
|
+
const headerHeight = this.measuredHeaderStickyHeight || options.headerRowHeight || 50;
|
|
989
|
+
const filterHeight = this.measuredFilterStickyHeight;
|
|
990
|
+
const paginationHeight = this.measuredPaginationHeight();
|
|
991
|
+
const minHeight = headerHeight + filterHeight + paginationHeight + (minRows * rowHeight);
|
|
992
|
+
|
|
993
|
+
if (this.clientHeight < minHeight || (paginationHeight > 0 && this.lastAutoHeight !== minHeight)) {
|
|
994
|
+
this.lastAutoHeight = minHeight;
|
|
995
|
+
this.style.height = `${minHeight}px`;
|
|
996
|
+
this.inAutoAdjust = true;
|
|
997
|
+
this.render();
|
|
998
|
+
this.inAutoAdjust = false;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
measuredPaginationHeight(): number {
|
|
1003
|
+
const paginationEl = this.shadowRoot?.querySelector<HTMLElement>('ui-grid-pagination');
|
|
1004
|
+
if (!paginationEl) return 0;
|
|
1005
|
+
return paginationEl.offsetHeight || 44;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/** @internal */
|
|
1009
|
+
setupAutoResize(): void {
|
|
1010
|
+
if (this.autoResizeObserver) return;
|
|
1011
|
+
if (typeof ResizeObserver === 'undefined') return;
|
|
1012
|
+
|
|
1013
|
+
this.autoResizeObserver = new ResizeObserver((entries) => {
|
|
1014
|
+
const options = this.snapshot?.options;
|
|
1015
|
+
if (options?.enableAutoResize === false) return;
|
|
1016
|
+
|
|
1017
|
+
const entry = entries[0];
|
|
1018
|
+
if (!entry) return;
|
|
1019
|
+
|
|
1020
|
+
if (this.autoResizeDebounceHandle !== null) {
|
|
1021
|
+
cancelAnimationFrame(this.autoResizeDebounceHandle);
|
|
1022
|
+
}
|
|
1023
|
+
this.autoResizeDebounceHandle = requestAnimationFrame(() => {
|
|
1024
|
+
this.autoResizeDebounceHandle = null;
|
|
1025
|
+
if (!this.snapshot) return;
|
|
1026
|
+
this.render();
|
|
1027
|
+
});
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
this.autoResizeObserver.observe(this);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/** @internal */
|
|
1034
|
+
teardownAutoResize(): void {
|
|
1035
|
+
if (this.autoResizeObserver) {
|
|
1036
|
+
this.autoResizeObserver.disconnect();
|
|
1037
|
+
this.autoResizeObserver = null;
|
|
1038
|
+
}
|
|
1039
|
+
if (this.autoResizeDebounceHandle !== null) {
|
|
1040
|
+
cancelAnimationFrame(this.autoResizeDebounceHandle);
|
|
1041
|
+
this.autoResizeDebounceHandle = null;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
private buildRenderPlan(snapshot: GridControllerSnapshot): {
|
|
1046
|
+
snapshot: GridControllerSnapshot;
|
|
1047
|
+
options: GridOptions;
|
|
1048
|
+
labels: GridControllerSnapshot['labels'];
|
|
1049
|
+
templateColumns: string;
|
|
1050
|
+
sortEnabled: boolean;
|
|
1051
|
+
filterEnabled: boolean;
|
|
1052
|
+
groupingEnabled: boolean;
|
|
1053
|
+
pinningEnabled: boolean;
|
|
1054
|
+
paginationEnabled: boolean;
|
|
1055
|
+
showPagination: boolean;
|
|
1056
|
+
virtualizationEnabled: boolean;
|
|
1057
|
+
itemsToRender: readonly DisplayItem[];
|
|
1058
|
+
startIndex: number;
|
|
1059
|
+
virtualOffset: number;
|
|
1060
|
+
totalVirtualHeight: number;
|
|
1061
|
+
structureKey: string;
|
|
1062
|
+
} {
|
|
1443
1063
|
const controller = this.controller!;
|
|
1444
1064
|
const options = snapshot.options;
|
|
1445
1065
|
const labels = snapshot.labels;
|
|
@@ -1451,19 +1071,22 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1451
1071
|
const paginationEnabled = controller.isPaginationEnabled();
|
|
1452
1072
|
const showPagination = controller.shouldShowPaginationControls();
|
|
1453
1073
|
const virtualizationEnabled = snapshot.pipeline.virtualizationEnabled;
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
const
|
|
1074
|
+
// Measure the body viewport from the DOM when available; on first paint
|
|
1075
|
+
// fall back to a reasonable default so virtualisation has something to
|
|
1076
|
+
// slice against until the element is measured.
|
|
1077
|
+
const bodyViewport = this.shadowRoot?.querySelector<HTMLElement>('.grid-body-viewport');
|
|
1078
|
+
const bodyViewportHeight = bodyViewport
|
|
1079
|
+
? Math.max(snapshot.rowSize, bodyViewport.clientHeight)
|
|
1080
|
+
: Math.max(snapshot.rowSize, (options.minRowsToShow ?? 10) * snapshot.rowSize);
|
|
1081
|
+
|
|
1458
1082
|
let startIndex = 0;
|
|
1459
|
-
let itemsToRender = snapshot.pipeline.displayItems;
|
|
1083
|
+
let itemsToRender: readonly DisplayItem[] = snapshot.pipeline.displayItems;
|
|
1460
1084
|
let virtualOffset = 0;
|
|
1461
1085
|
const totalVirtualHeight = snapshot.pipeline.displayItems.length * snapshot.rowSize;
|
|
1462
1086
|
|
|
1463
1087
|
if (virtualizationEnabled) {
|
|
1464
|
-
const bodyScrollTop = Math.max(0, this.scrollPosition - stickyChromeHeight);
|
|
1465
1088
|
const overscan = 4;
|
|
1466
|
-
startIndex = Math.max(0, Math.floor(
|
|
1089
|
+
startIndex = Math.max(0, Math.floor(this.scrollPosition / snapshot.rowSize) - overscan);
|
|
1467
1090
|
this.lastVirtualStartIndex = startIndex;
|
|
1468
1091
|
const visibleCount = Math.ceil(bodyViewportHeight / snapshot.rowSize) + overscan * 2;
|
|
1469
1092
|
itemsToRender = snapshot.pipeline.displayItems.slice(
|
|
@@ -1475,6 +1098,65 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1475
1098
|
this.lastVirtualStartIndex = -1;
|
|
1476
1099
|
}
|
|
1477
1100
|
|
|
1101
|
+
// Structure key: only the outer-shell layout. Things that swap the body
|
|
1102
|
+
// container (virtualization flip, empty state, viewport scroll) or the
|
|
1103
|
+
// pagination element appearance are handled *inside* the patch path so a
|
|
1104
|
+
// filter keystroke that crosses the virtualization threshold doesn't
|
|
1105
|
+
// tear down the focused filter input.
|
|
1106
|
+
const columnFingerprint = snapshot.visibleColumns
|
|
1107
|
+
.map(
|
|
1108
|
+
(c) =>
|
|
1109
|
+
`${c.name}:${controller.isPinned(c) ? 'p' : ''}${controller.isPinnedLeftLast(c) ? 'L' : ''}${controller.isPinnedRightFirst(c) ? 'R' : ''}`,
|
|
1110
|
+
)
|
|
1111
|
+
.join('|');
|
|
1112
|
+
const structureKey = [
|
|
1113
|
+
columnFingerprint,
|
|
1114
|
+
filterEnabled ? '1' : '0',
|
|
1115
|
+
paginationEnabled ? '1' : '0',
|
|
1116
|
+
].join('#');
|
|
1117
|
+
|
|
1118
|
+
return {
|
|
1119
|
+
snapshot,
|
|
1120
|
+
options,
|
|
1121
|
+
labels,
|
|
1122
|
+
templateColumns,
|
|
1123
|
+
sortEnabled,
|
|
1124
|
+
filterEnabled,
|
|
1125
|
+
groupingEnabled,
|
|
1126
|
+
pinningEnabled,
|
|
1127
|
+
paginationEnabled,
|
|
1128
|
+
showPagination,
|
|
1129
|
+
virtualizationEnabled,
|
|
1130
|
+
itemsToRender,
|
|
1131
|
+
startIndex,
|
|
1132
|
+
virtualOffset,
|
|
1133
|
+
totalVirtualHeight,
|
|
1134
|
+
structureKey,
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
private renderFull(
|
|
1139
|
+
plan: ReturnType<UiGridStandaloneElement['buildRenderPlan']>,
|
|
1140
|
+
_root: ShadowRoot,
|
|
1141
|
+
): void {
|
|
1142
|
+
const {
|
|
1143
|
+
snapshot,
|
|
1144
|
+
options,
|
|
1145
|
+
labels,
|
|
1146
|
+
templateColumns,
|
|
1147
|
+
sortEnabled,
|
|
1148
|
+
filterEnabled,
|
|
1149
|
+
groupingEnabled,
|
|
1150
|
+
pinningEnabled,
|
|
1151
|
+
paginationEnabled,
|
|
1152
|
+
showPagination,
|
|
1153
|
+
virtualizationEnabled,
|
|
1154
|
+
itemsToRender,
|
|
1155
|
+
startIndex,
|
|
1156
|
+
virtualOffset,
|
|
1157
|
+
totalVirtualHeight,
|
|
1158
|
+
} = plan;
|
|
1159
|
+
|
|
1478
1160
|
const slotRegistry = this.renderSlotRegistry(snapshot.visibleColumns);
|
|
1479
1161
|
const header = snapshot.visibleColumns
|
|
1480
1162
|
.map((column) =>
|
|
@@ -1483,9 +1165,10 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1483
1165
|
.join('');
|
|
1484
1166
|
|
|
1485
1167
|
const filterRow = filterEnabled
|
|
1486
|
-
?
|
|
1487
|
-
|
|
1488
|
-
.join('')
|
|
1168
|
+
? filterRowMarkup(
|
|
1169
|
+
templateColumns,
|
|
1170
|
+
snapshot.visibleColumns.map((column) => this.renderFilterCell(column)).join(''),
|
|
1171
|
+
)
|
|
1489
1172
|
: '';
|
|
1490
1173
|
|
|
1491
1174
|
const bodyContent = itemsToRender
|
|
@@ -1495,51 +1178,103 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1495
1178
|
const body =
|
|
1496
1179
|
snapshot.pipeline.displayItems.length > 0
|
|
1497
1180
|
? virtualizationEnabled
|
|
1498
|
-
?
|
|
1499
|
-
:
|
|
1500
|
-
:
|
|
1181
|
+
? bodyVirtualMarkup(templateColumns, totalVirtualHeight, virtualOffset, bodyContent)
|
|
1182
|
+
: bodyStaticMarkup(templateColumns, bodyContent)
|
|
1183
|
+
: this.renderEmptyState(
|
|
1184
|
+
options.emptyMessage ?? labels.emptyHeading,
|
|
1185
|
+
labels.emptyDescription,
|
|
1186
|
+
);
|
|
1501
1187
|
|
|
1502
1188
|
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
1189
|
|
|
1506
|
-
|
|
1190
|
+
this.gridTitle = escapeHtml(options.title ?? 'Data grid');
|
|
1191
|
+
const stickyTop = this.measuredHeaderStickyHeight || options.headerRowHeight || 50;
|
|
1192
|
+
const paginationHeight = this.measuredPaginationHeight();
|
|
1193
|
+
const tableHeight = this.clientHeight || ((options.minRowsToShow ?? 10) * snapshot.rowSize + (this.measuredHeaderStickyHeight || options.headerRowHeight || 50) + this.measuredFilterStickyHeight + paginationHeight);
|
|
1194
|
+
this.gridTableStyle = `--ui-grid-header-sticky-top:${stickyTop}px;height:${tableHeight}px;`;
|
|
1195
|
+
this.bodyViewportStyle = '';
|
|
1196
|
+
this.templateColumns = templateColumns;
|
|
1197
|
+
this.slotRegistry = slotRegistry;
|
|
1198
|
+
this.headerContent = header;
|
|
1199
|
+
this.filterRowContent = filterRow;
|
|
1200
|
+
this.bodyContent = body;
|
|
1201
|
+
this.paginationContent = pagination;
|
|
1202
|
+
|
|
1203
|
+
gridShellTemplate(this).connect();
|
|
1204
|
+
|
|
1205
|
+
// Flush framework-rendered slot deltas — dispatches cellSlotsChanged /
|
|
1206
|
+
// expandableRowSlotsChanged / groupRowSlotsChanged / headerSlotsChanged /
|
|
1207
|
+
// filterSlotsChanged / emptyStateSlotChanged with the adds + removes
|
|
1208
|
+
// relative to the previous render.
|
|
1209
|
+
this.frameworkSlots.flush();
|
|
1210
|
+
|
|
1211
|
+
// After a full mount, the new DOM exactly matches the current item set —
|
|
1212
|
+
// seed the fingerprint so the next render can take the fast patch path.
|
|
1213
|
+
this.lastItemsFingerprint = this.fingerprintItems(plan.itemsToRender);
|
|
1214
|
+
this.lastVirtualOffset = plan.virtualOffset;
|
|
1215
|
+
this.lastTotalVirtualHeight = plan.totalVirtualHeight;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
private renderPatch(
|
|
1219
|
+
plan: ReturnType<UiGridStandaloneElement['buildRenderPlan']>,
|
|
1220
|
+
root: ShadowRoot,
|
|
1221
|
+
): void {
|
|
1222
|
+
renderPatch(this, plan, root);
|
|
1223
|
+
}
|
|
1507
1224
|
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1225
|
+
private reconcileBodyRoot(
|
|
1226
|
+
root: ShadowRoot,
|
|
1227
|
+
kind: 'empty' | 'virtual' | 'static',
|
|
1228
|
+
options: GridOptions,
|
|
1229
|
+
labels: GridControllerSnapshot['labels'],
|
|
1230
|
+
templateColumns: string,
|
|
1231
|
+
virtualOffset: number,
|
|
1232
|
+
totalVirtualHeight: number,
|
|
1233
|
+
): HTMLElement | null {
|
|
1234
|
+
return reconcileBodyRoot(this, root, kind, options, labels, templateColumns, virtualOffset, totalVirtualHeight);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
private reconcilePagination(
|
|
1238
|
+
root: ShadowRoot,
|
|
1239
|
+
snapshot: GridControllerSnapshot,
|
|
1240
|
+
shouldShow: boolean,
|
|
1241
|
+
): void {
|
|
1242
|
+
reconcilePagination(this, root, snapshot, shouldShow);
|
|
1243
|
+
}
|
|
1521
1244
|
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
const nextFilterStickyHeight = filterGrid?.offsetHeight ?? 0;
|
|
1526
|
-
const stickyHeightsChanged =
|
|
1527
|
-
nextHeaderStickyHeight !== this.measuredHeaderStickyHeight ||
|
|
1528
|
-
nextFilterStickyHeight !== this.measuredFilterStickyHeight;
|
|
1245
|
+
private patchFilterCells(filterGrid: HTMLElement, snapshot: GridControllerSnapshot): void {
|
|
1246
|
+
patchFilterCells(this, filterGrid, snapshot);
|
|
1247
|
+
}
|
|
1529
1248
|
|
|
1530
|
-
|
|
1531
|
-
|
|
1249
|
+
private fingerprintItems(items: readonly DisplayItem[]): string {
|
|
1250
|
+
return fingerprintItems(items);
|
|
1251
|
+
}
|
|
1532
1252
|
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1253
|
+
private patchExistingRows(
|
|
1254
|
+
bodyContainer: HTMLElement,
|
|
1255
|
+
snapshot: GridControllerSnapshot,
|
|
1256
|
+
itemsToRender: readonly DisplayItem[],
|
|
1257
|
+
startIndex: number,
|
|
1258
|
+
): void {
|
|
1259
|
+
patchExistingRows(this, bodyContainer, snapshot, itemsToRender, startIndex);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
private patchGroupRow(el: HTMLElement, group: GroupItem): void {
|
|
1263
|
+
patchGroupRow(this, el, group);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
private patchBodyCell(
|
|
1267
|
+
cell: HTMLElement,
|
|
1268
|
+
row: GridRow,
|
|
1269
|
+
column: GridColumnDef,
|
|
1270
|
+
displayIndex: number,
|
|
1271
|
+
templateMarkupMap: Map<string, string | null>,
|
|
1272
|
+
): void {
|
|
1273
|
+
patchBodyCell(this, cell, row, column, displayIndex, templateMarkupMap);
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
private patchPagination(paginationEl: HTMLElement, snapshot: GridControllerSnapshot): void {
|
|
1277
|
+
patchPagination(this, paginationEl, snapshot);
|
|
1543
1278
|
}
|
|
1544
1279
|
|
|
1545
1280
|
private renderHeaderCell(
|
|
@@ -1549,177 +1284,102 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1549
1284
|
pinningEnabled: boolean,
|
|
1550
1285
|
options: GridOptions,
|
|
1551
1286
|
): 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>`;
|
|
1287
|
+
return renderHeaderCell(this, column, sortEnabled, groupingEnabled, pinningEnabled, options);
|
|
1598
1288
|
}
|
|
1599
1289
|
|
|
1600
1290
|
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>`;
|
|
1291
|
+
return renderFilterCell(this, column);
|
|
1615
1292
|
}
|
|
1616
1293
|
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1294
|
+
/**
|
|
1295
|
+
* Commit the in-flight edit, then move keyboard focus relative to the
|
|
1296
|
+
* committed cell. Splits commit + focus across two paint frames: commit
|
|
1297
|
+
* runs synchronously (editor unmounts, cell re-renders with the new value),
|
|
1298
|
+
* then focus moves to the adjacent cell. Scheduling the focus in a
|
|
1299
|
+
* microtask avoids racing the blur handler's re-entry guard and ensures
|
|
1300
|
+
* the destination cell is fully reattached in the DOM.
|
|
1301
|
+
*
|
|
1302
|
+
* If the source cell was being edited, the destination cell enters edit
|
|
1303
|
+
* mode too (provided the destination column is editable). This keeps
|
|
1304
|
+
* Tab/Enter nav inside an edit session continuous — you keep editing as
|
|
1305
|
+
* you move. Arrow nav outside an edit session does NOT auto-enter edit.
|
|
1306
|
+
*/
|
|
1307
|
+
private commitAndMove(
|
|
1308
|
+
fromRowId: string | null,
|
|
1309
|
+
fromColumnName: string | null,
|
|
1310
|
+
direction: 'left' | 'right' | 'up' | 'down',
|
|
1311
|
+
): void {
|
|
1312
|
+
commitAndMove(this, fromRowId, fromColumnName, direction);
|
|
1633
1313
|
}
|
|
1634
1314
|
|
|
1635
|
-
private
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1315
|
+
private moveGridFocus(
|
|
1316
|
+
direction: GridMoveDirection,
|
|
1317
|
+
rowId: string | null,
|
|
1318
|
+
columnName: string | null,
|
|
1319
|
+
opts: { resumeEdit?: boolean } = {},
|
|
1320
|
+
): void {
|
|
1321
|
+
moveGridFocus(this, direction, rowId, columnName, opts);
|
|
1322
|
+
}
|
|
1639
1323
|
|
|
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
|
-
}
|
|
1324
|
+
private scrollFocusedRowIntoView(rowId: string): void {
|
|
1325
|
+
scrollFocusedRowIntoView(this, rowId);
|
|
1326
|
+
}
|
|
1646
1327
|
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
}
|
|
1328
|
+
private handleRowSelectionClick(rowId: string, columnName: string, event: Event): void {
|
|
1329
|
+
handleRowSelectionClick(this, rowId, columnName, event);
|
|
1330
|
+
}
|
|
1651
1331
|
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1332
|
+
private handleRowHeaderCheckboxClick(rowId: string, event: Event): void {
|
|
1333
|
+
handleRowHeaderCheckboxClick(this, rowId, event);
|
|
1334
|
+
}
|
|
1655
1335
|
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1336
|
+
private matchesKeyOverride(
|
|
1337
|
+
override: {
|
|
1338
|
+
keyCode?: number;
|
|
1339
|
+
key?: string;
|
|
1340
|
+
shiftKey?: boolean;
|
|
1341
|
+
ctrlKey?: boolean;
|
|
1342
|
+
altKey?: boolean;
|
|
1343
|
+
metaKey?: boolean;
|
|
1344
|
+
},
|
|
1345
|
+
event: KeyboardEvent,
|
|
1346
|
+
): boolean {
|
|
1347
|
+
return matchesKeyOverride(override, event);
|
|
1659
1348
|
}
|
|
1660
1349
|
|
|
1661
|
-
private
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1350
|
+
private applyFocusedCellClass(
|
|
1351
|
+
previous: { rowId: string; columnName: string } | null,
|
|
1352
|
+
next: { rowId: string; columnName: string } | null,
|
|
1353
|
+
): void {
|
|
1354
|
+
applyFocusedCellClass(this, previous, next);
|
|
1355
|
+
}
|
|
1665
1356
|
|
|
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
|
-
: '';
|
|
1357
|
+
private focusCellElement(rowId: string | null, columnName: string | null): void {
|
|
1358
|
+
focusCellElement(this, rowId, columnName);
|
|
1359
|
+
}
|
|
1677
1360
|
|
|
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>`;
|
|
1361
|
+
private measureAutoColumnWidth(columnName: string): number {
|
|
1362
|
+
return measureAutoColumnWidth(this, columnName);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
private renderDisplayItem(item: DisplayItem, displayIndex: number): string {
|
|
1366
|
+
return renderDisplayItem(this, item, displayIndex);
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
private renderBodyCell(row: GridRow, column: GridColumnDef, displayIndex: number): string {
|
|
1370
|
+
return renderBodyCell(this, row, column, displayIndex);
|
|
1701
1371
|
}
|
|
1702
1372
|
|
|
1703
1373
|
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>`;
|
|
1374
|
+
return renderPagination(this, snapshot);
|
|
1711
1375
|
}
|
|
1712
1376
|
|
|
1713
1377
|
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>`;
|
|
1378
|
+
return renderSlotRegistry(columns);
|
|
1718
1379
|
}
|
|
1719
1380
|
|
|
1720
1381
|
private renderCellTemplate(row: GridRow, column: GridColumnDef, displayIndex: number): string {
|
|
1721
|
-
|
|
1722
|
-
return this.renderCellTemplateFromMarkup(row, column, displayIndex, templateMarkup);
|
|
1382
|
+
return renderCellTemplate(this, row, column, displayIndex);
|
|
1723
1383
|
}
|
|
1724
1384
|
|
|
1725
1385
|
private renderCellTemplateFromMarkup(
|
|
@@ -1728,62 +1388,20 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1728
1388
|
displayIndex: number,
|
|
1729
1389
|
templateMarkup: string | null,
|
|
1730
1390
|
): 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
|
-
});
|
|
1391
|
+
return renderCellTemplateFromMarkup(this, row, column, displayIndex, templateMarkup);
|
|
1747
1392
|
}
|
|
1748
1393
|
|
|
1749
1394
|
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
|
-
});
|
|
1395
|
+
return renderExpandableTemplate(this, row);
|
|
1761
1396
|
}
|
|
1762
1397
|
|
|
1763
1398
|
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
|
-
});
|
|
1399
|
+
return getTemplateMarkupFromLightDom(this, slotName);
|
|
1773
1400
|
}
|
|
1774
1401
|
|
|
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 ?? '';
|
|
1402
|
+
/** @internal Used by the render module to interpolate consumer templates. */
|
|
1403
|
+
interpolateTemplate(templateMarkup: string, context: Record<string, unknown>): string {
|
|
1404
|
+
return interpolateGridTemplate(templateMarkup, context, escapeHtml);
|
|
1787
1405
|
}
|
|
1788
1406
|
|
|
1789
1407
|
private rowIndexFor(row: GridRow, fallback = 0): number {
|
|
@@ -1813,7 +1431,9 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1813
1431
|
}
|
|
1814
1432
|
|
|
1815
1433
|
for (let index = 0; index < previousSnapshot.visibleColumns.length; index += 1) {
|
|
1816
|
-
if (
|
|
1434
|
+
if (
|
|
1435
|
+
previousSnapshot.visibleColumns[index]?.name !== nextSnapshot.visibleColumns[index]?.name
|
|
1436
|
+
) {
|
|
1817
1437
|
return { mode: 'full', changedRowIds: null };
|
|
1818
1438
|
}
|
|
1819
1439
|
}
|
|
@@ -1871,12 +1491,12 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1871
1491
|
};
|
|
1872
1492
|
}
|
|
1873
1493
|
|
|
1874
|
-
const
|
|
1875
|
-
const
|
|
1876
|
-
|
|
1877
|
-
|
|
1494
|
+
const bodyViewport = this.shadowRoot?.querySelector<HTMLElement>('.grid-body-viewport');
|
|
1495
|
+
const bodyViewportHeight = bodyViewport
|
|
1496
|
+
? Math.max(snapshot.rowSize, bodyViewport.clientHeight)
|
|
1497
|
+
: Math.max(snapshot.rowSize, (snapshot.options.minRowsToShow ?? 10) * snapshot.rowSize);
|
|
1878
1498
|
const overscan = 4;
|
|
1879
|
-
const startIndex = Math.max(0, Math.floor(
|
|
1499
|
+
const startIndex = Math.max(0, Math.floor(this.scrollPosition / snapshot.rowSize) - overscan);
|
|
1880
1500
|
const visibleCount = Math.ceil(bodyViewportHeight / snapshot.rowSize) + overscan * 2;
|
|
1881
1501
|
|
|
1882
1502
|
return {
|
|
@@ -1892,29 +1512,38 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1892
1512
|
return `cell-${column.name}`;
|
|
1893
1513
|
}
|
|
1894
1514
|
|
|
1895
|
-
private
|
|
1896
|
-
|
|
1515
|
+
private renderEmptyState(headingRaw: string, descriptionRaw: string): string {
|
|
1516
|
+
return renderEmptyState(this, headingRaw, descriptionRaw);
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
/** @internal */
|
|
1520
|
+
downloadCsv(
|
|
1521
|
+
rowType: 'all' | 'visible' | 'selected' = 'visible',
|
|
1522
|
+
colType: 'all' | 'visible' = 'visible',
|
|
1523
|
+
): void {
|
|
1524
|
+
if (!this.controller) {
|
|
1897
1525
|
return;
|
|
1898
1526
|
}
|
|
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);
|
|
1527
|
+
this.controller.gridApi.exporter.csvExport(rowType, colType);
|
|
1903
1528
|
}
|
|
1904
1529
|
|
|
1905
1530
|
private renderControlIcon(key: UiGridControlIconKey): string {
|
|
1906
|
-
return this.
|
|
1531
|
+
return this.icons.renderControlIcon(key);
|
|
1907
1532
|
}
|
|
1908
1533
|
|
|
1909
1534
|
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>`;
|
|
1535
|
+
return this.icons.renderIconWithClass(svgClass, key);
|
|
1914
1536
|
}
|
|
1915
1537
|
}
|
|
1916
1538
|
|
|
1917
1539
|
export async function defineStandaloneUiGridElement(tagName = 'ui-grid-element'): Promise<void> {
|
|
1540
|
+
UIGridFilterCell.define();
|
|
1541
|
+
UIGridGroupRow.define();
|
|
1542
|
+
UIGridPagination.define();
|
|
1543
|
+
UIGridBodyCell.define();
|
|
1544
|
+
UIGridHeaderCell.define();
|
|
1545
|
+
UIGridTemplate.define();
|
|
1546
|
+
UIGridCellEditor.define();
|
|
1918
1547
|
if (!customElements.get(tagName)) {
|
|
1919
1548
|
customElements.define(tagName, UiGridStandaloneElement);
|
|
1920
1549
|
}
|