@ornery/ui-grid-vanilla 0.1.9 → 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/index.html +3 -3
- package/demo/main.ts +1 -1
- package/dist/index.d.mts +584 -55
- package/dist/index.d.ts +584 -55
- package/dist/index.js +4887 -1404
- package/dist/index.mjs +5056 -1526
- 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 +1431 -20
- package/src/icons.ts +88 -0
- package/src/importer.test.ts +147 -0
- package/src/index.integration.test.ts +2 -2
- package/src/index.ts +9 -0
- package/src/infinite-scroll.test.ts +220 -0
- package/src/keyboard-navigation.test.ts +579 -0
- package/src/patch.ts +722 -0
- package/src/render.ts +465 -0
- package/src/row-edit.test.ts +191 -0
- package/src/row-selection.test.ts +550 -0
- package/src/save-state.test.ts +235 -0
- package/src/scss.d.ts +4 -0
- package/src/templates/index.ts +62 -0
- package/src/test-setup.ts +23 -0
- package/src/ui-grid-empty.template.ts +23 -0
- package/src/ui-grid-shell.scss +25 -0
- package/src/ui-grid-shell.template.ts +45 -0
- package/src/ui-grid-standalone.element.ts +751 -1032
- 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,44 +1,105 @@
|
|
|
1
1
|
import {
|
|
2
|
-
GRID_CORE_CSS,
|
|
3
|
-
SORT_DIRECTIONS,
|
|
4
|
-
buildGridHeaderContext,
|
|
5
2
|
canGridMoveColumns,
|
|
6
|
-
|
|
7
|
-
exportCsvRows,
|
|
8
|
-
formatGridHeaderDisplayValue,
|
|
9
|
-
getCellValue,
|
|
10
|
-
sanitizeDownloadFilename,
|
|
3
|
+
interpolateGridTemplate,
|
|
11
4
|
type DisplayItem,
|
|
12
5
|
type GridColumnDef,
|
|
13
6
|
type GridOptions,
|
|
14
7
|
type GridRecord,
|
|
15
|
-
type GroupItem,
|
|
16
8
|
type GridRow,
|
|
9
|
+
type GroupItem,
|
|
17
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';
|
|
18
82
|
import {
|
|
19
83
|
createVanillaGridController,
|
|
20
84
|
type GridControllerSnapshot,
|
|
21
85
|
type GridSaveState,
|
|
22
86
|
type VanillaGridController,
|
|
23
87
|
} from './grid-controller';
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function isRowItem(item: DisplayItem): item is DisplayItem & { kind: 'row'; row: GridRow } {
|
|
40
|
-
return item.kind === 'row';
|
|
41
|
-
}
|
|
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';
|
|
42
103
|
|
|
43
104
|
export type VanillaUiGridElement = HTMLElement & {
|
|
44
105
|
options: GridOptions;
|
|
@@ -52,123 +113,71 @@ export type VanillaUiGridElement = HTMLElement & {
|
|
|
52
113
|
setData(rows: GridRecord[]): void;
|
|
53
114
|
};
|
|
54
115
|
|
|
55
|
-
export type UiGridControlIconKey =
|
|
56
|
-
| 'sortNone'
|
|
57
|
-
| 'sortAsc'
|
|
58
|
-
| 'sortDesc'
|
|
59
|
-
| 'group'
|
|
60
|
-
| 'groupExpanded'
|
|
61
|
-
| 'groupCollapsed'
|
|
62
|
-
| 'treeExpanded'
|
|
63
|
-
| 'treeCollapsed'
|
|
64
|
-
| 'expandExpanded'
|
|
65
|
-
| 'expandCollapsed'
|
|
66
|
-
| 'pin'
|
|
67
|
-
| 'pinLeft'
|
|
68
|
-
| 'pinRight'
|
|
69
|
-
| 'paginationPrev'
|
|
70
|
-
| 'paginationNext';
|
|
71
|
-
|
|
72
|
-
export interface UiGridIconDefinition {
|
|
73
|
-
viewBox?: string;
|
|
74
|
-
path: string;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export type UiGridIconOverrides = Partial<Record<UiGridControlIconKey, UiGridIconDefinition>>;
|
|
78
|
-
|
|
79
116
|
type VanillaGridOptions = GridOptions & {
|
|
80
117
|
iconOverrides?: UiGridIconOverrides;
|
|
81
118
|
};
|
|
82
119
|
|
|
83
|
-
const DEFAULT_ICONS: Record<UiGridControlIconKey, UiGridIconDefinition> = {
|
|
84
|
-
sortNone: { path: 'M7 6h10v2H7V6Zm0 5h7v2H7v-2Zm0 5h4v2H7v-2Z' },
|
|
85
|
-
sortAsc: { path: 'M12 5l-6 6h4v8h4v-8h4z' },
|
|
86
|
-
sortDesc: { path: 'M12 19l6-6h-4V5h-4v8H6z' },
|
|
87
|
-
group: { path: 'M4 6h8v4H4V6Zm0 8h8v4H4v-4Zm10-8h6v4h-6V6Zm0 8h6v4h-6v-4Z' },
|
|
88
|
-
groupExpanded: { path: 'M7 10l5 5 5-5z' },
|
|
89
|
-
groupCollapsed: { path: 'M10 7l5 5-5 5z' },
|
|
90
|
-
treeExpanded: { path: 'M7 10l5 5 5-5z' },
|
|
91
|
-
treeCollapsed: { path: 'M10 7l5 5-5 5z' },
|
|
92
|
-
expandExpanded: { path: 'M7 10l5 5 5-5z' },
|
|
93
|
-
expandCollapsed: { path: 'M10 7l5 5-5 5z' },
|
|
94
|
-
pin: { path: 'M16 12V4h1V2H7v2h1v8l-2 2v2h5v6l1 1 1-1v-6h5v-2l-2-2z' },
|
|
95
|
-
pinLeft: { path: 'M10 6 4 12l6 6v-4h10v-4H10V6z' },
|
|
96
|
-
pinRight: { path: 'M14 6v4H4v4h10v4l6-6-6-6z' },
|
|
97
|
-
paginationPrev: { path: 'M15.41 7.41 14 6l-6 6 6 6 1.41-1.41L10.83 12z' },
|
|
98
|
-
paginationNext: { path: 'M8.59 16.59 10 18l6-6-6-6-1.41 1.41L13.17 12z' },
|
|
99
|
-
};
|
|
100
|
-
|
|
101
120
|
export class UiGridStandaloneElement extends HTMLElement {
|
|
102
|
-
|
|
103
|
-
private
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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);
|
|
128
178
|
|
|
129
179
|
static get observedAttributes(): string[] {
|
|
130
|
-
return [
|
|
131
|
-
// Scalar attributes
|
|
132
|
-
'grid-id',
|
|
133
|
-
'title',
|
|
134
|
-
'row-height',
|
|
135
|
-
'header-row-height',
|
|
136
|
-
'viewport-height',
|
|
137
|
-
'pagination-page-size',
|
|
138
|
-
'pagination-current-page',
|
|
139
|
-
'total-items',
|
|
140
|
-
'virtualization-threshold',
|
|
141
|
-
'tree-children-field',
|
|
142
|
-
'tree-indent',
|
|
143
|
-
'expandable-row-height',
|
|
144
|
-
'expandable-row-header-width',
|
|
145
|
-
'empty-message',
|
|
146
|
-
'infinite-scroll-rows-from-end',
|
|
147
|
-
// JSON attributes
|
|
148
|
-
'column-defs',
|
|
149
|
-
'data',
|
|
150
|
-
'grouping',
|
|
151
|
-
'pagination-page-sizes',
|
|
152
|
-
// Boolean flags
|
|
153
|
-
'enable-sorting',
|
|
154
|
-
'enable-filtering',
|
|
155
|
-
'enable-grouping',
|
|
156
|
-
'enable-pinning',
|
|
157
|
-
'enable-column-moving',
|
|
158
|
-
'enable-cell-edit',
|
|
159
|
-
'enable-cell-edit-on-focus',
|
|
160
|
-
'enable-pagination',
|
|
161
|
-
'enable-pagination-controls',
|
|
162
|
-
'use-external-pagination',
|
|
163
|
-
'enable-expandable',
|
|
164
|
-
'enable-tree-view',
|
|
165
|
-
'show-tree-expand-no-children',
|
|
166
|
-
'tree-row-header-always-visible',
|
|
167
|
-
'enable-auto-resize',
|
|
168
|
-
'enable-virtualization',
|
|
169
|
-
'infinite-scroll-up',
|
|
170
|
-
'infinite-scroll-down',
|
|
171
|
-
];
|
|
180
|
+
return [...OBSERVED_GRID_ATTRIBUTES];
|
|
172
181
|
}
|
|
173
182
|
|
|
174
183
|
get options(): GridOptions {
|
|
@@ -186,10 +195,36 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
186
195
|
|
|
187
196
|
set options(value: GridOptions) {
|
|
188
197
|
this.activeOptions = value as VanillaGridOptions;
|
|
189
|
-
this.
|
|
198
|
+
this.icons.setOverrides(this.activeOptions.iconOverrides ?? {});
|
|
190
199
|
this.ensureController(this.buildEffectiveOptions(this.activeOptions));
|
|
191
200
|
}
|
|
192
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
|
+
|
|
193
228
|
attributeChangedCallback(
|
|
194
229
|
_name: string,
|
|
195
230
|
_oldValue: string | null,
|
|
@@ -206,198 +241,47 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
206
241
|
}
|
|
207
242
|
|
|
208
243
|
private syncAttributesToOptions(): void {
|
|
209
|
-
this.attributeOptions =
|
|
210
|
-
|
|
211
|
-
// Scalar string attributes
|
|
212
|
-
const gridId = this.getAttribute('grid-id');
|
|
213
|
-
if (gridId !== null) this.attributeOptions.id = gridId;
|
|
214
|
-
|
|
215
|
-
const title = this.getAttribute('title');
|
|
216
|
-
if (title !== null) this.attributeOptions.title = title;
|
|
217
|
-
|
|
218
|
-
const emptyMessage = this.getAttribute('empty-message');
|
|
219
|
-
if (emptyMessage !== null) this.attributeOptions.emptyMessage = emptyMessage;
|
|
220
|
-
|
|
221
|
-
const treeChildrenField = this.getAttribute('tree-children-field');
|
|
222
|
-
if (treeChildrenField !== null) this.attributeOptions.treeChildrenField = treeChildrenField;
|
|
223
|
-
|
|
224
|
-
// Scalar number attributes
|
|
225
|
-
const rowHeight = this.parseNumberAttribute('row-height');
|
|
226
|
-
if (rowHeight !== undefined) this.attributeOptions.rowHeight = rowHeight;
|
|
227
|
-
|
|
228
|
-
const headerRowHeight = this.parseNumberAttribute('header-row-height');
|
|
229
|
-
if (headerRowHeight !== undefined)
|
|
230
|
-
this.attributeOptions.headerRowHeight = headerRowHeight;
|
|
231
|
-
|
|
232
|
-
const viewportHeight = this.parseNumberAttribute('viewport-height');
|
|
233
|
-
if (viewportHeight !== undefined) this.attributeOptions.viewportHeight = viewportHeight;
|
|
234
|
-
|
|
235
|
-
const paginationPageSize = this.parseNumberAttribute('pagination-page-size');
|
|
236
|
-
if (paginationPageSize !== undefined)
|
|
237
|
-
this.attributeOptions.paginationPageSize = paginationPageSize;
|
|
238
|
-
|
|
239
|
-
const paginationCurrentPage = this.parseNumberAttribute('pagination-current-page');
|
|
240
|
-
if (paginationCurrentPage !== undefined)
|
|
241
|
-
this.attributeOptions.paginationCurrentPage = paginationCurrentPage;
|
|
242
|
-
|
|
243
|
-
const totalItems = this.parseNumberAttribute('total-items');
|
|
244
|
-
if (totalItems !== undefined) this.attributeOptions.totalItems = totalItems;
|
|
245
|
-
|
|
246
|
-
const virtualizationThreshold = this.parseNumberAttribute('virtualization-threshold');
|
|
247
|
-
if (virtualizationThreshold !== undefined)
|
|
248
|
-
this.attributeOptions.virtualizationThreshold = virtualizationThreshold;
|
|
249
|
-
|
|
250
|
-
const treeIndent = this.parseNumberAttribute('tree-indent');
|
|
251
|
-
if (treeIndent !== undefined) this.attributeOptions.treeIndent = treeIndent;
|
|
252
|
-
|
|
253
|
-
const expandableRowHeight = this.parseNumberAttribute('expandable-row-height');
|
|
254
|
-
if (expandableRowHeight !== undefined)
|
|
255
|
-
this.attributeOptions.expandableRowHeight = expandableRowHeight;
|
|
256
|
-
|
|
257
|
-
const expandableRowHeaderWidth = this.parseNumberAttribute(
|
|
258
|
-
'expandable-row-header-width',
|
|
259
|
-
);
|
|
260
|
-
if (expandableRowHeaderWidth !== undefined)
|
|
261
|
-
this.attributeOptions.expandableRowHeaderWidth = expandableRowHeaderWidth;
|
|
262
|
-
|
|
263
|
-
const infiniteScrollRowsFromEnd = this.parseNumberAttribute(
|
|
264
|
-
'infinite-scroll-rows-from-end',
|
|
265
|
-
);
|
|
266
|
-
if (infiniteScrollRowsFromEnd !== undefined)
|
|
267
|
-
this.attributeOptions.infiniteScrollRowsFromEnd = infiniteScrollRowsFromEnd;
|
|
268
|
-
|
|
269
|
-
// Boolean attributes
|
|
270
|
-
const enableSorting = this.parseBooleanAttribute('enable-sorting');
|
|
271
|
-
if (enableSorting !== undefined) this.attributeOptions.enableSorting = enableSorting;
|
|
272
|
-
|
|
273
|
-
const enableFiltering = this.parseBooleanAttribute('enable-filtering');
|
|
274
|
-
if (enableFiltering !== undefined) this.attributeOptions.enableFiltering = enableFiltering;
|
|
275
|
-
|
|
276
|
-
const enableGrouping = this.parseBooleanAttribute('enable-grouping');
|
|
277
|
-
if (enableGrouping !== undefined) this.attributeOptions.enableGrouping = enableGrouping;
|
|
278
|
-
|
|
279
|
-
const enablePinning = this.parseBooleanAttribute('enable-pinning');
|
|
280
|
-
if (enablePinning !== undefined) this.attributeOptions.enablePinning = enablePinning;
|
|
281
|
-
|
|
282
|
-
const enableColumnMoving = this.parseBooleanAttribute('enable-column-moving');
|
|
283
|
-
if (enableColumnMoving !== undefined)
|
|
284
|
-
this.attributeOptions.enableColumnMoving = enableColumnMoving;
|
|
285
|
-
|
|
286
|
-
const enableCellEdit = this.parseBooleanAttribute('enable-cell-edit');
|
|
287
|
-
if (enableCellEdit !== undefined) this.attributeOptions.enableCellEdit = enableCellEdit;
|
|
288
|
-
|
|
289
|
-
const enableCellEditOnFocus = this.parseBooleanAttribute('enable-cell-edit-on-focus');
|
|
290
|
-
if (enableCellEditOnFocus !== undefined)
|
|
291
|
-
this.attributeOptions.enableCellEditOnFocus = enableCellEditOnFocus;
|
|
292
|
-
|
|
293
|
-
const enablePagination = this.parseBooleanAttribute('enable-pagination');
|
|
294
|
-
if (enablePagination !== undefined)
|
|
295
|
-
this.attributeOptions.enablePagination = enablePagination;
|
|
296
|
-
|
|
297
|
-
const enablePaginationControls = this.parseBooleanAttribute(
|
|
298
|
-
'enable-pagination-controls',
|
|
299
|
-
);
|
|
300
|
-
if (enablePaginationControls !== undefined)
|
|
301
|
-
this.attributeOptions.enablePaginationControls = enablePaginationControls;
|
|
244
|
+
this.attributeOptions = parseGridAttributeOptions(this);
|
|
302
245
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if (enableExpandable !== undefined) this.attributeOptions.enableExpandable = enableExpandable;
|
|
309
|
-
|
|
310
|
-
const enableTreeView = this.parseBooleanAttribute('enable-tree-view');
|
|
311
|
-
if (enableTreeView !== undefined) this.attributeOptions.enableTreeView = enableTreeView;
|
|
312
|
-
|
|
313
|
-
const showTreeExpandNoChildren = this.parseBooleanAttribute(
|
|
314
|
-
'show-tree-expand-no-children',
|
|
315
|
-
);
|
|
316
|
-
if (showTreeExpandNoChildren !== undefined)
|
|
317
|
-
this.attributeOptions.showTreeExpandNoChildren = showTreeExpandNoChildren;
|
|
318
|
-
|
|
319
|
-
const treeRowHeaderAlwaysVisible = this.parseBooleanAttribute(
|
|
320
|
-
'tree-row-header-always-visible',
|
|
321
|
-
);
|
|
322
|
-
if (treeRowHeaderAlwaysVisible !== undefined)
|
|
323
|
-
this.attributeOptions.treeRowHeaderAlwaysVisible = treeRowHeaderAlwaysVisible;
|
|
324
|
-
|
|
325
|
-
const enableAutoResize = this.parseBooleanAttribute('enable-auto-resize');
|
|
326
|
-
if (enableAutoResize !== undefined) this.attributeOptions.enableAutoResize = enableAutoResize;
|
|
327
|
-
|
|
328
|
-
const enableVirtualization = this.parseBooleanAttribute('enable-virtualization');
|
|
329
|
-
if (enableVirtualization !== undefined)
|
|
330
|
-
this.attributeOptions.enableVirtualization = enableVirtualization;
|
|
331
|
-
|
|
332
|
-
const infiniteScrollUp = this.parseBooleanAttribute('infinite-scroll-up');
|
|
333
|
-
if (infiniteScrollUp !== undefined) this.attributeOptions.infiniteScrollUp = infiniteScrollUp;
|
|
334
|
-
|
|
335
|
-
const infiniteScrollDown = this.parseBooleanAttribute('infinite-scroll-down');
|
|
336
|
-
if (infiniteScrollDown !== undefined)
|
|
337
|
-
this.attributeOptions.infiniteScrollDown = infiniteScrollDown;
|
|
338
|
-
|
|
339
|
-
// JSON attributes
|
|
340
|
-
const columnDefs = this.parseJsonAttribute<GridColumnDef[]>('column-defs');
|
|
341
|
-
if (columnDefs !== undefined) this.attributeOptions.columnDefs = columnDefs;
|
|
342
|
-
|
|
343
|
-
const data = this.parseJsonAttribute<GridRecord[]>('data');
|
|
344
|
-
if (data !== undefined) this.attributeOptions.data = data;
|
|
345
|
-
|
|
346
|
-
const grouping = this.parseJsonAttribute('grouping');
|
|
347
|
-
if (grouping !== undefined && grouping !== null)
|
|
348
|
-
this.attributeOptions.grouping = grouping;
|
|
349
|
-
|
|
350
|
-
const paginationPageSizes = this.parseJsonAttribute<number[] | null>(
|
|
351
|
-
'pagination-page-sizes',
|
|
352
|
-
);
|
|
353
|
-
if (paginationPageSizes !== undefined)
|
|
354
|
-
this.attributeOptions.paginationPageSizes = paginationPageSizes;
|
|
355
|
-
|
|
356
|
-
// 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.
|
|
357
251
|
if (this.activeOptions !== null) {
|
|
358
252
|
this.ensureController(this.buildEffectiveOptions(this.activeOptions));
|
|
359
253
|
} else {
|
|
360
|
-
// Purely declarative path:
|
|
361
|
-
this.
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
const raw = this.getAttribute(name);
|
|
376
|
-
if (raw === null) return undefined;
|
|
377
|
-
const parsed = Number(raw);
|
|
378
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
private parseJsonAttribute<T = unknown>(name: string): T | undefined {
|
|
382
|
-
const raw = this.getAttribute(name);
|
|
383
|
-
if (raw === null) return undefined;
|
|
384
|
-
try {
|
|
385
|
-
return JSON.parse(raw) as T;
|
|
386
|
-
} catch (e) {
|
|
387
|
-
console.warn(`<ui-grid-element>: invalid JSON in "${name}" attribute`, e);
|
|
388
|
-
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
|
+
);
|
|
389
269
|
}
|
|
390
270
|
}
|
|
391
271
|
|
|
392
272
|
get controlIcons(): UiGridIconOverrides {
|
|
393
|
-
return this.
|
|
273
|
+
return this.icons.getOverrides();
|
|
394
274
|
}
|
|
395
275
|
|
|
396
276
|
set controlIcons(value: UiGridIconOverrides) {
|
|
397
|
-
this.
|
|
277
|
+
this.icons.setOverrides(value);
|
|
398
278
|
this.render();
|
|
399
279
|
}
|
|
400
280
|
|
|
281
|
+
private resolveIcon(key: UiGridControlIconKey): UiGridIconDefinition {
|
|
282
|
+
return this.icons.resolve(key);
|
|
283
|
+
}
|
|
284
|
+
|
|
401
285
|
// ─────────────────────────────────────────────────────────────────
|
|
402
286
|
// Individual property accessors (mirrors of HTML attributes)
|
|
403
287
|
// ─────────────────────────────────────────────────────────────────
|
|
@@ -436,14 +320,6 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
436
320
|
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
437
321
|
}
|
|
438
322
|
|
|
439
|
-
get viewportHeight(): number {
|
|
440
|
-
return this.options.viewportHeight ?? 560;
|
|
441
|
-
}
|
|
442
|
-
set viewportHeight(value: number) {
|
|
443
|
-
this.activeOptions = { ...this.activeOptions, viewportHeight: value } as any;
|
|
444
|
-
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
445
|
-
}
|
|
446
|
-
|
|
447
323
|
get paginationPageSize(): number {
|
|
448
324
|
return this.options.paginationPageSize ?? 25;
|
|
449
325
|
}
|
|
@@ -509,7 +385,10 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
509
385
|
}
|
|
510
386
|
|
|
511
387
|
get emptyMessage(): string {
|
|
512
|
-
|
|
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 ?? '';
|
|
513
392
|
}
|
|
514
393
|
set emptyMessage(value: string) {
|
|
515
394
|
this.activeOptions = { ...this.activeOptions, emptyMessage: value } as any;
|
|
@@ -580,6 +459,14 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
580
459
|
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
581
460
|
}
|
|
582
461
|
|
|
462
|
+
get enableColumnResizing(): boolean {
|
|
463
|
+
return (this.options as any).enableColumnResizing ?? true;
|
|
464
|
+
}
|
|
465
|
+
set enableColumnResizing(value: boolean) {
|
|
466
|
+
this.activeOptions = { ...this.activeOptions, enableColumnResizing: value } as any;
|
|
467
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
468
|
+
}
|
|
469
|
+
|
|
583
470
|
get enableCellEdit(): boolean {
|
|
584
471
|
return this.options.enableCellEdit ?? false;
|
|
585
472
|
}
|
|
@@ -779,62 +666,14 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
779
666
|
}
|
|
780
667
|
|
|
781
668
|
private patchCells(changedRowIds?: ReadonlySet<string>): void {
|
|
782
|
-
|
|
783
|
-
if (!snapshot) {
|
|
784
|
-
return;
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
const root = this.shadowRoot;
|
|
788
|
-
if (!root) {
|
|
789
|
-
return;
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
// Build O(1) lookup maps from the current pipeline snapshot.
|
|
793
|
-
const rowMap = new Map<string, GridRow>();
|
|
794
|
-
const rowIndexMap = new Map<string, number>();
|
|
795
|
-
for (const row of snapshot.pipeline.visibleRows) {
|
|
796
|
-
rowMap.set(row.id, row);
|
|
797
|
-
rowIndexMap.set(row.id, rowIndexMap.size);
|
|
798
|
-
}
|
|
799
|
-
const colMap = new Map<string, GridColumnDef>();
|
|
800
|
-
const templateMarkupMap = new Map<string, string | null>();
|
|
801
|
-
for (const col of snapshot.visibleColumns) {
|
|
802
|
-
colMap.set(col.name, col);
|
|
803
|
-
templateMarkupMap.set(col.name, this.getTemplateMarkup(this.cellSlotName(col)));
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
// Patch every rendered body cell in-place.
|
|
807
|
-
const cells = root.querySelectorAll<HTMLElement>('[data-row][data-column]');
|
|
808
|
-
for (const cell of cells) {
|
|
809
|
-
const rowId = cell.dataset['row'];
|
|
810
|
-
const colName = cell.dataset['column'];
|
|
811
|
-
if (!rowId || !colName) continue;
|
|
812
|
-
if (changedRowIds && !changedRowIds.has(rowId)) continue;
|
|
813
|
-
|
|
814
|
-
const row = rowMap.get(rowId);
|
|
815
|
-
const column = colMap.get(colName);
|
|
816
|
-
if (!row || !column) continue;
|
|
817
|
-
if (cell.classList.contains('cell-editing')) continue;
|
|
818
|
-
|
|
819
|
-
const cellContent = cell.querySelector<HTMLElement>('.cell-content');
|
|
820
|
-
if (!cellContent) continue;
|
|
821
|
-
|
|
822
|
-
const newContent = this.renderCellTemplateFromMarkup(
|
|
823
|
-
row,
|
|
824
|
-
column,
|
|
825
|
-
rowIndexMap.get(rowId) ?? 0,
|
|
826
|
-
templateMarkupMap.get(colName) ?? null,
|
|
827
|
-
);
|
|
828
|
-
if (cellContent.innerHTML !== newContent) {
|
|
829
|
-
cellContent.innerHTML = newContent;
|
|
830
|
-
}
|
|
831
|
-
}
|
|
669
|
+
patchCells(this, changedRowIds);
|
|
832
670
|
}
|
|
833
671
|
|
|
834
672
|
connectedCallback(): void {
|
|
835
673
|
this.ensureShadowRoot();
|
|
836
674
|
this.bindEvents();
|
|
837
675
|
this.observeTemplateSlots();
|
|
676
|
+
this.setupAutoResize();
|
|
838
677
|
|
|
839
678
|
if (this.activeOptions) {
|
|
840
679
|
this.ensureController(this.buildEffectiveOptions(this.activeOptions));
|
|
@@ -848,13 +687,18 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
848
687
|
this.unsubscribe = null;
|
|
849
688
|
this.templateObserver?.disconnect();
|
|
850
689
|
this.templateObserver = null;
|
|
690
|
+
this.teardownAutoResize();
|
|
851
691
|
if (this.scrollFrame !== null) {
|
|
852
692
|
cancelAnimationFrame(this.scrollFrame);
|
|
853
693
|
this.scrollFrame = null;
|
|
854
694
|
}
|
|
695
|
+
// Emit `removed` events for every framework-rendered slot so wrappers
|
|
696
|
+
// can destroy their views.
|
|
697
|
+
this.frameworkSlots.flushRemovals();
|
|
855
698
|
}
|
|
856
699
|
|
|
857
|
-
|
|
700
|
+
/** @internal */
|
|
701
|
+
ensureShadowRoot(): ShadowRoot {
|
|
858
702
|
if (!this.shadowRoot) {
|
|
859
703
|
this.attachShadow({ mode: 'open' });
|
|
860
704
|
}
|
|
@@ -862,41 +706,103 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
862
706
|
return this.shadowRoot as ShadowRoot;
|
|
863
707
|
}
|
|
864
708
|
|
|
865
|
-
|
|
709
|
+
/** @internal */
|
|
710
|
+
ensureController(options: VanillaGridOptions): void {
|
|
866
711
|
if (this.controller) {
|
|
867
712
|
this.controller.setOptions(options);
|
|
868
713
|
return;
|
|
869
714
|
}
|
|
870
715
|
|
|
871
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
|
+
});
|
|
872
757
|
this.unsubscribe = this.controller.subscribe((snapshot) => {
|
|
873
758
|
this.snapshot = snapshot;
|
|
874
759
|
this.render();
|
|
875
760
|
});
|
|
876
761
|
}
|
|
877
762
|
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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;
|
|
881
784
|
}
|
|
785
|
+
this.importerFileInput.click();
|
|
786
|
+
}
|
|
882
787
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
} else {
|
|
887
|
-
this.render();
|
|
888
|
-
}
|
|
889
|
-
});
|
|
788
|
+
private observeTemplateSlots(): void {
|
|
789
|
+
observeTemplateSlots(this);
|
|
790
|
+
}
|
|
890
791
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
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);
|
|
897
802
|
}
|
|
898
803
|
|
|
899
|
-
|
|
804
|
+
/** @internal */
|
|
805
|
+
buildEffectiveOptions(options: VanillaGridOptions): VanillaGridOptions {
|
|
900
806
|
// Merge: attributes are overridden by explicit JS property values.
|
|
901
807
|
// JS property always wins: { ...attributeOptions, ...options }
|
|
902
808
|
const merged = {
|
|
@@ -904,8 +810,7 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
904
810
|
...options,
|
|
905
811
|
} as VanillaGridOptions;
|
|
906
812
|
|
|
907
|
-
|
|
908
|
-
if (merged.enableExpandable && hasExpandableSlot && !merged.expandableRowTemplate) {
|
|
813
|
+
if (merged.enableExpandable && !merged.expandableRowTemplate) {
|
|
909
814
|
merged.expandableRowTemplate = {
|
|
910
815
|
createEmbeddedView: () => undefined,
|
|
911
816
|
};
|
|
@@ -914,397 +819,28 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
914
819
|
return merged;
|
|
915
820
|
}
|
|
916
821
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
return;
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
const action = actionNode.dataset['action'];
|
|
935
|
-
if (!action) {
|
|
936
|
-
return;
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
if (this.openPinMenuColumn && !target.closest('.pin-control')) {
|
|
940
|
-
this.openPinMenuColumn = null;
|
|
941
|
-
this.render();
|
|
942
|
-
return;
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
if (action === 'sort') {
|
|
946
|
-
const columnName = actionNode.dataset['column'];
|
|
947
|
-
if (columnName) {
|
|
948
|
-
this.controller.toggleSort(columnName);
|
|
949
|
-
}
|
|
950
|
-
return;
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
if (action === 'group') {
|
|
954
|
-
const columnName = actionNode.dataset['column'];
|
|
955
|
-
if (columnName) {
|
|
956
|
-
this.controller.toggleGrouping(columnName);
|
|
957
|
-
}
|
|
958
|
-
return;
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
if (action === 'toggle-group') {
|
|
962
|
-
const groupId = actionNode.dataset['group'];
|
|
963
|
-
if (groupId) {
|
|
964
|
-
const collapsed = actionNode.dataset['collapsed'] !== 'true';
|
|
965
|
-
this.controller.setCollapsedGroup(groupId, collapsed);
|
|
966
|
-
}
|
|
967
|
-
return;
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
if (action === 'pin-trigger') {
|
|
971
|
-
const columnName = actionNode.dataset['column'];
|
|
972
|
-
const column = columnName
|
|
973
|
-
? this.snapshot.visibleColumns.find((candidate) => candidate.name === columnName)
|
|
974
|
-
: undefined;
|
|
975
|
-
|
|
976
|
-
if (columnName && column) {
|
|
977
|
-
if (this.controller.isPinned(column)) {
|
|
978
|
-
this.openPinMenuColumn = null;
|
|
979
|
-
this.controller.pinColumn(columnName, 'none');
|
|
980
|
-
return;
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
this.openPinMenuColumn = this.openPinMenuColumn === columnName ? null : columnName;
|
|
984
|
-
this.render();
|
|
985
|
-
}
|
|
986
|
-
return;
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
if (action === 'pin-left' || action === 'pin-right') {
|
|
990
|
-
const columnName = actionNode.dataset['column'];
|
|
991
|
-
if (columnName) {
|
|
992
|
-
this.openPinMenuColumn = null;
|
|
993
|
-
this.controller.pinColumn(columnName, action === 'pin-left' ? 'left' : 'right');
|
|
994
|
-
}
|
|
995
|
-
return;
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
if (action === 'toggle-tree') {
|
|
999
|
-
const rowId = actionNode.dataset['row'];
|
|
1000
|
-
if (rowId) {
|
|
1001
|
-
this.controller.toggleTreeRow(rowId);
|
|
1002
|
-
}
|
|
1003
|
-
return;
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
if (action === 'toggle-expand') {
|
|
1007
|
-
const rowId = actionNode.dataset['row'];
|
|
1008
|
-
if (rowId) {
|
|
1009
|
-
this.controller.toggleRowExpansion(rowId);
|
|
1010
|
-
}
|
|
1011
|
-
return;
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
if (action === 'page-prev') {
|
|
1015
|
-
this.controller.seekPage(this.snapshot.currentPage - 1);
|
|
1016
|
-
return;
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
if (action === 'page-next') {
|
|
1020
|
-
this.controller.seekPage(this.snapshot.currentPage + 1);
|
|
1021
|
-
return;
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
if (action === 'benchmark') {
|
|
1025
|
-
const result = this.controller.gridApi.core.benchmark();
|
|
1026
|
-
this.benchmarkAverage = result.averageMs.toFixed(2);
|
|
1027
|
-
this.render();
|
|
1028
|
-
return;
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
if (action === 'export-csv') {
|
|
1032
|
-
this.downloadCsv();
|
|
1033
|
-
}
|
|
1034
|
-
});
|
|
1035
|
-
|
|
1036
|
-
root.addEventListener('focusin', (event) => {
|
|
1037
|
-
const target = event.target as HTMLElement | null;
|
|
1038
|
-
if (!target) {
|
|
1039
|
-
return;
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
const cell = target.closest<HTMLElement>('.body-cell[data-row][data-column]');
|
|
1043
|
-
const rowId = cell?.dataset['row'];
|
|
1044
|
-
const columnName = cell?.dataset['column'];
|
|
1045
|
-
if (rowId && columnName) {
|
|
1046
|
-
this.focusedCell = { rowId, columnName };
|
|
1047
|
-
}
|
|
1048
|
-
});
|
|
1049
|
-
|
|
1050
|
-
root.addEventListener('focusout', (event) => {
|
|
1051
|
-
const relatedTarget = (event as FocusEvent).relatedTarget as HTMLElement | null;
|
|
1052
|
-
if (!relatedTarget?.closest('.body-cell[data-row][data-column]')) {
|
|
1053
|
-
this.focusedCell = null;
|
|
1054
|
-
}
|
|
1055
|
-
});
|
|
1056
|
-
|
|
1057
|
-
root.addEventListener('input', (event) => {
|
|
1058
|
-
const target = event.target as HTMLElement | null;
|
|
1059
|
-
if (!target || !this.controller) {
|
|
1060
|
-
return;
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
if (target instanceof HTMLInputElement && target.dataset['role'] === 'filter') {
|
|
1064
|
-
const columnName = target.dataset['column'];
|
|
1065
|
-
if (columnName) {
|
|
1066
|
-
this.controller.setFilter(columnName, target.value);
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
if (target instanceof HTMLInputElement && target.dataset['role'] === 'editor') {
|
|
1071
|
-
this.controller.updateEditingValue(target.value);
|
|
1072
|
-
}
|
|
1073
|
-
});
|
|
1074
|
-
|
|
1075
|
-
root.addEventListener('change', (event) => {
|
|
1076
|
-
const target = event.target as HTMLElement | null;
|
|
1077
|
-
if (!target || !this.controller) {
|
|
1078
|
-
return;
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
if (target instanceof HTMLSelectElement && target.dataset['role'] === 'page-size') {
|
|
1082
|
-
const value = Number.parseInt(target.value, 10);
|
|
1083
|
-
this.controller.setPageSize(value);
|
|
1084
|
-
}
|
|
1085
|
-
});
|
|
1086
|
-
|
|
1087
|
-
root.addEventListener('dblclick', (event) => {
|
|
1088
|
-
const target = event.target as HTMLElement | null;
|
|
1089
|
-
if (!target || !this.controller) {
|
|
1090
|
-
return;
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
const cell = target.closest<HTMLElement>('.body-cell');
|
|
1094
|
-
if (!cell) {
|
|
1095
|
-
return;
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
const rowId = cell.dataset['row'];
|
|
1099
|
-
const columnName = cell.dataset['column'];
|
|
1100
|
-
if (rowId && columnName) {
|
|
1101
|
-
this.controller.beginCellEdit(rowId, columnName, event);
|
|
1102
|
-
}
|
|
1103
|
-
});
|
|
1104
|
-
|
|
1105
|
-
root.addEventListener('keydown', (event) => {
|
|
1106
|
-
const keyboardEvent = event as KeyboardEvent;
|
|
1107
|
-
const target = event.target as HTMLElement | null;
|
|
1108
|
-
if (!target || !this.controller) {
|
|
1109
|
-
return;
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
if (target instanceof HTMLInputElement && target.dataset['role'] === 'editor') {
|
|
1113
|
-
if (keyboardEvent.key === 'Enter') {
|
|
1114
|
-
event.preventDefault();
|
|
1115
|
-
this.controller.commitCellEdit();
|
|
1116
|
-
return;
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
if (keyboardEvent.key === 'Escape') {
|
|
1120
|
-
event.preventDefault();
|
|
1121
|
-
this.controller.cancelCellEdit();
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
return;
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
const cell = target.closest<HTMLElement>('.body-cell');
|
|
1128
|
-
if (!cell) {
|
|
1129
|
-
return;
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
const rowId = cell.dataset['row'];
|
|
1133
|
-
const columnName = cell.dataset['column'];
|
|
1134
|
-
if (!rowId || !columnName) {
|
|
1135
|
-
return;
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
if (keyboardEvent.key === 'Enter' || keyboardEvent.key === 'F2') {
|
|
1139
|
-
event.preventDefault();
|
|
1140
|
-
this.controller.beginCellEdit(rowId, columnName, event);
|
|
1141
|
-
return;
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
if (
|
|
1145
|
-
keyboardEvent.key.length === 1 &&
|
|
1146
|
-
!keyboardEvent.ctrlKey &&
|
|
1147
|
-
!keyboardEvent.metaKey &&
|
|
1148
|
-
!keyboardEvent.altKey
|
|
1149
|
-
) {
|
|
1150
|
-
event.preventDefault();
|
|
1151
|
-
this.controller.beginCellEdit(rowId, columnName, event);
|
|
1152
|
-
this.controller.updateEditingValue(keyboardEvent.key);
|
|
1153
|
-
}
|
|
1154
|
-
});
|
|
1155
|
-
|
|
1156
|
-
root.addEventListener(
|
|
1157
|
-
'blur',
|
|
1158
|
-
(event) => {
|
|
1159
|
-
const target = event.target as HTMLElement | null;
|
|
1160
|
-
if (!target || !this.controller) {
|
|
1161
|
-
return;
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
if (target instanceof HTMLInputElement && target.dataset['role'] === 'editor') {
|
|
1165
|
-
this.controller.commitCellEdit();
|
|
1166
|
-
}
|
|
1167
|
-
},
|
|
1168
|
-
true,
|
|
1169
|
-
);
|
|
1170
|
-
|
|
1171
|
-
root.addEventListener('dragstart', (event) => {
|
|
1172
|
-
const dragEvent = event as DragEvent;
|
|
1173
|
-
const target = event.target as HTMLElement | null;
|
|
1174
|
-
const headerCell = target?.closest<HTMLElement>('.header-cell[data-column]');
|
|
1175
|
-
|
|
1176
|
-
if (!headerCell || !this.snapshot || !canGridMoveColumns(this.snapshot.options)) {
|
|
1177
|
-
event.preventDefault();
|
|
1178
|
-
return;
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
const columnName = headerCell.dataset['column'];
|
|
1182
|
-
if (!columnName || !dragEvent.dataTransfer) {
|
|
1183
|
-
event.preventDefault();
|
|
1184
|
-
return;
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
this.draggedColumnName = columnName;
|
|
1188
|
-
this.dropTargetColumnName = null;
|
|
1189
|
-
dragEvent.dataTransfer.effectAllowed = 'move';
|
|
1190
|
-
dragEvent.dataTransfer.setData('text/plain', columnName);
|
|
1191
|
-
headerCell.classList.add('is-dragging');
|
|
1192
|
-
});
|
|
1193
|
-
|
|
1194
|
-
root.addEventListener('dragover', (event) => {
|
|
1195
|
-
const dragEvent = event as DragEvent;
|
|
1196
|
-
const target = event.target as HTMLElement | null;
|
|
1197
|
-
const headerCell = target?.closest<HTMLElement>('.header-cell[data-column]');
|
|
1198
|
-
|
|
1199
|
-
if (!headerCell || !this.draggedColumnName || !dragEvent.dataTransfer) {
|
|
1200
|
-
return;
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
const columnName = headerCell.dataset['column'];
|
|
1204
|
-
if (!columnName || columnName === this.draggedColumnName) {
|
|
1205
|
-
return;
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
event.preventDefault();
|
|
1209
|
-
dragEvent.dataTransfer.dropEffect = 'move';
|
|
1210
|
-
|
|
1211
|
-
if (this.dropTargetColumnName !== columnName) {
|
|
1212
|
-
root
|
|
1213
|
-
.querySelectorAll('.header-cell.is-drag-target')
|
|
1214
|
-
.forEach((element) => element.classList.remove('is-drag-target'));
|
|
1215
|
-
this.dropTargetColumnName = columnName;
|
|
1216
|
-
headerCell.classList.add('is-drag-target');
|
|
1217
|
-
}
|
|
1218
|
-
});
|
|
1219
|
-
|
|
1220
|
-
root.addEventListener('dragleave', (event) => {
|
|
1221
|
-
const target = event.target as HTMLElement | null;
|
|
1222
|
-
const headerCell = target?.closest<HTMLElement>('.header-cell[data-column]');
|
|
1223
|
-
if (headerCell && headerCell.dataset['column'] === this.dropTargetColumnName) {
|
|
1224
|
-
headerCell.classList.remove('is-drag-target');
|
|
1225
|
-
}
|
|
1226
|
-
});
|
|
1227
|
-
|
|
1228
|
-
root.addEventListener('drop', (event) => {
|
|
1229
|
-
event.preventDefault();
|
|
1230
|
-
const dragEvent = event as DragEvent;
|
|
1231
|
-
const target = event.target as HTMLElement | null;
|
|
1232
|
-
const headerCell = target?.closest<HTMLElement>('.header-cell[data-column]');
|
|
1233
|
-
const targetColumn = headerCell?.dataset['column'];
|
|
1234
|
-
const sourceColumn =
|
|
1235
|
-
this.draggedColumnName ?? dragEvent.dataTransfer?.getData('text/plain') ?? null;
|
|
1236
|
-
|
|
1237
|
-
this.draggedColumnName = null;
|
|
1238
|
-
this.dropTargetColumnName = null;
|
|
1239
|
-
|
|
1240
|
-
root
|
|
1241
|
-
.querySelectorAll('.header-cell.is-dragging, .header-cell.is-drag-target')
|
|
1242
|
-
.forEach((element) => element.classList.remove('is-dragging', 'is-drag-target'));
|
|
1243
|
-
|
|
1244
|
-
if (!sourceColumn || !targetColumn || sourceColumn === targetColumn || !this.controller) {
|
|
1245
|
-
return;
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
this.controller.moveVisibleColumn(sourceColumn, targetColumn);
|
|
1249
|
-
});
|
|
1250
|
-
|
|
1251
|
-
root.addEventListener('dragend', () => {
|
|
1252
|
-
this.draggedColumnName = null;
|
|
1253
|
-
this.dropTargetColumnName = null;
|
|
1254
|
-
root
|
|
1255
|
-
.querySelectorAll('.header-cell.is-dragging, .header-cell.is-drag-target')
|
|
1256
|
-
.forEach((element) => element.classList.remove('is-dragging', 'is-drag-target'));
|
|
1257
|
-
});
|
|
1258
|
-
|
|
1259
|
-
root.addEventListener(
|
|
1260
|
-
'scroll',
|
|
1261
|
-
(event) => {
|
|
1262
|
-
const target = event.target as HTMLElement | null;
|
|
1263
|
-
if (!target?.classList.contains('grid-table') || this.suppressScrollEvent) {
|
|
1264
|
-
return;
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
this.lastScrollActivityAt = Date.now();
|
|
1268
|
-
this.scrollPosition = target.scrollTop;
|
|
1269
|
-
this.horizontalScrollPosition = target.scrollLeft;
|
|
1270
|
-
if (!this.snapshot?.pipeline.virtualizationEnabled) {
|
|
1271
|
-
return;
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
const stickyChromeHeight =
|
|
1275
|
-
this.measuredHeaderStickyHeight + this.measuredFilterStickyHeight;
|
|
1276
|
-
const bodyScrollTop = Math.max(0, this.scrollPosition - stickyChromeHeight);
|
|
1277
|
-
const overscan = 4;
|
|
1278
|
-
const nextStartIndex = Math.max(
|
|
1279
|
-
0,
|
|
1280
|
-
Math.floor(bodyScrollTop / this.snapshot.rowSize) - overscan,
|
|
1281
|
-
);
|
|
1282
|
-
if (nextStartIndex === this.lastVirtualStartIndex) {
|
|
1283
|
-
return;
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
if (this.scrollFrame !== null) {
|
|
1287
|
-
cancelAnimationFrame(this.scrollFrame);
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
this.scrollFrame = requestAnimationFrame(() => {
|
|
1291
|
-
this.scrollFrame = null;
|
|
1292
|
-
this.renderVirtualBody();
|
|
1293
|
-
});
|
|
1294
|
-
},
|
|
1295
|
-
true,
|
|
1296
|
-
);
|
|
1297
|
-
|
|
1298
|
-
(root as ShadowRoot & { __uiGridBound?: boolean }).__uiGridBound = true;
|
|
1299
|
-
}
|
|
1300
|
-
|
|
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
|
+
*/
|
|
1301
836
|
/**
|
|
1302
837
|
* Targeted update for virtual scroll: replaces only the rendered rows inside
|
|
1303
838
|
* the existing `.grid-virtual-body` without touching the scroll container.
|
|
1304
839
|
* This preserves momentum/inertia scrolling because the `.grid-table` element
|
|
1305
840
|
* is never destroyed.
|
|
1306
841
|
*/
|
|
1307
|
-
|
|
842
|
+
/** @internal */
|
|
843
|
+
renderVirtualBody(): void {
|
|
1308
844
|
const snapshot = this.snapshot;
|
|
1309
845
|
if (!snapshot?.pipeline.virtualizationEnabled) {
|
|
1310
846
|
this.render();
|
|
@@ -1325,12 +861,12 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1325
861
|
this.dataFrame = null;
|
|
1326
862
|
}
|
|
1327
863
|
|
|
1328
|
-
const
|
|
1329
|
-
const
|
|
1330
|
-
|
|
1331
|
-
|
|
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);
|
|
1332
868
|
const overscan = 4;
|
|
1333
|
-
const startIndex = Math.max(0, Math.floor(
|
|
869
|
+
const startIndex = Math.max(0, Math.floor(this.scrollPosition / snapshot.rowSize) - overscan);
|
|
1334
870
|
this.lastVirtualStartIndex = startIndex;
|
|
1335
871
|
const visibleCount = Math.ceil(bodyViewportHeight / snapshot.rowSize) + overscan * 2;
|
|
1336
872
|
const itemsToRender = snapshot.pipeline.displayItems.slice(
|
|
@@ -1343,9 +879,14 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1343
879
|
virtualBody.innerHTML = itemsToRender
|
|
1344
880
|
.map((item, index) => this.renderDisplayItem(item, startIndex + index))
|
|
1345
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;
|
|
1346
886
|
}
|
|
1347
887
|
|
|
1348
|
-
|
|
888
|
+
/** @internal */
|
|
889
|
+
render(): void {
|
|
1349
890
|
if (this.skipNextRender) {
|
|
1350
891
|
return;
|
|
1351
892
|
}
|
|
@@ -1361,17 +902,164 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1361
902
|
}
|
|
1362
903
|
const root = this.ensureShadowRoot();
|
|
1363
904
|
const snapshot = this.snapshot;
|
|
1364
|
-
const
|
|
1365
|
-
if (
|
|
1366
|
-
this.scrollPosition =
|
|
1367
|
-
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;
|
|
1368
909
|
}
|
|
1369
910
|
|
|
1370
911
|
if (!snapshot) {
|
|
1371
|
-
|
|
912
|
+
this.lastStructureKey = null;
|
|
913
|
+
this.lastItemsFingerprint = null;
|
|
914
|
+
emptyTemplate({ message: 'No grid options provided.' }).connect(root);
|
|
1372
915
|
return;
|
|
1373
916
|
}
|
|
1374
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
|
+
} {
|
|
1375
1063
|
const controller = this.controller!;
|
|
1376
1064
|
const options = snapshot.options;
|
|
1377
1065
|
const labels = snapshot.labels;
|
|
@@ -1383,19 +1071,22 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1383
1071
|
const paginationEnabled = controller.isPaginationEnabled();
|
|
1384
1072
|
const showPagination = controller.shouldShowPaginationControls();
|
|
1385
1073
|
const virtualizationEnabled = snapshot.pipeline.virtualizationEnabled;
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
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
|
+
|
|
1390
1082
|
let startIndex = 0;
|
|
1391
|
-
let itemsToRender = snapshot.pipeline.displayItems;
|
|
1083
|
+
let itemsToRender: readonly DisplayItem[] = snapshot.pipeline.displayItems;
|
|
1392
1084
|
let virtualOffset = 0;
|
|
1393
1085
|
const totalVirtualHeight = snapshot.pipeline.displayItems.length * snapshot.rowSize;
|
|
1394
1086
|
|
|
1395
1087
|
if (virtualizationEnabled) {
|
|
1396
|
-
const bodyScrollTop = Math.max(0, this.scrollPosition - stickyChromeHeight);
|
|
1397
1088
|
const overscan = 4;
|
|
1398
|
-
startIndex = Math.max(0, Math.floor(
|
|
1089
|
+
startIndex = Math.max(0, Math.floor(this.scrollPosition / snapshot.rowSize) - overscan);
|
|
1399
1090
|
this.lastVirtualStartIndex = startIndex;
|
|
1400
1091
|
const visibleCount = Math.ceil(bodyViewportHeight / snapshot.rowSize) + overscan * 2;
|
|
1401
1092
|
itemsToRender = snapshot.pipeline.displayItems.slice(
|
|
@@ -1407,6 +1098,65 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1407
1098
|
this.lastVirtualStartIndex = -1;
|
|
1408
1099
|
}
|
|
1409
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
|
+
|
|
1410
1160
|
const slotRegistry = this.renderSlotRegistry(snapshot.visibleColumns);
|
|
1411
1161
|
const header = snapshot.visibleColumns
|
|
1412
1162
|
.map((column) =>
|
|
@@ -1415,9 +1165,10 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1415
1165
|
.join('');
|
|
1416
1166
|
|
|
1417
1167
|
const filterRow = filterEnabled
|
|
1418
|
-
?
|
|
1419
|
-
|
|
1420
|
-
.join('')
|
|
1168
|
+
? filterRowMarkup(
|
|
1169
|
+
templateColumns,
|
|
1170
|
+
snapshot.visibleColumns.map((column) => this.renderFilterCell(column)).join(''),
|
|
1171
|
+
)
|
|
1421
1172
|
: '';
|
|
1422
1173
|
|
|
1423
1174
|
const bodyContent = itemsToRender
|
|
@@ -1427,51 +1178,103 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1427
1178
|
const body =
|
|
1428
1179
|
snapshot.pipeline.displayItems.length > 0
|
|
1429
1180
|
? virtualizationEnabled
|
|
1430
|
-
?
|
|
1431
|
-
:
|
|
1432
|
-
:
|
|
1181
|
+
? bodyVirtualMarkup(templateColumns, totalVirtualHeight, virtualOffset, bodyContent)
|
|
1182
|
+
: bodyStaticMarkup(templateColumns, bodyContent)
|
|
1183
|
+
: this.renderEmptyState(
|
|
1184
|
+
options.emptyMessage ?? labels.emptyHeading,
|
|
1185
|
+
labels.emptyDescription,
|
|
1186
|
+
);
|
|
1433
1187
|
|
|
1434
1188
|
const pagination = paginationEnabled && showPagination ? this.renderPagination(snapshot) : '';
|
|
1435
|
-
const hasViewportScroll = virtualizationEnabled || options.viewportHeight !== undefined;
|
|
1436
|
-
const gridTableStyle = `${hasViewportScroll ? `height:${viewportHeight}px;overflow-y:auto;` : ''}--ui-grid-header-sticky-top:${headerStickyTop}px;`;
|
|
1437
1189
|
|
|
1438
|
-
|
|
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
|
+
}
|
|
1439
1224
|
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
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
|
+
}
|
|
1453
1244
|
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
const nextFilterStickyHeight = filterGrid?.offsetHeight ?? 0;
|
|
1458
|
-
const stickyHeightsChanged =
|
|
1459
|
-
nextHeaderStickyHeight !== this.measuredHeaderStickyHeight ||
|
|
1460
|
-
nextFilterStickyHeight !== this.measuredFilterStickyHeight;
|
|
1245
|
+
private patchFilterCells(filterGrid: HTMLElement, snapshot: GridControllerSnapshot): void {
|
|
1246
|
+
patchFilterCells(this, filterGrid, snapshot);
|
|
1247
|
+
}
|
|
1461
1248
|
|
|
1462
|
-
|
|
1463
|
-
|
|
1249
|
+
private fingerprintItems(items: readonly DisplayItem[]): string {
|
|
1250
|
+
return fingerprintItems(items);
|
|
1251
|
+
}
|
|
1464
1252
|
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
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);
|
|
1475
1278
|
}
|
|
1476
1279
|
|
|
1477
1280
|
private renderHeaderCell(
|
|
@@ -1481,155 +1284,102 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1481
1284
|
pinningEnabled: boolean,
|
|
1482
1285
|
options: GridOptions,
|
|
1483
1286
|
): string {
|
|
1484
|
-
|
|
1485
|
-
const sortDirection = controller.getSortDirection(column);
|
|
1486
|
-
const sortLabel = controller.sortButtonLabel(column);
|
|
1487
|
-
const groupingLabel = controller.groupingButtonLabel(column);
|
|
1488
|
-
const canSort = sortEnabled && controller.isColumnSortable(column);
|
|
1489
|
-
const canGroup = groupingEnabled && column.enableGrouping !== false;
|
|
1490
|
-
const canPin = pinningEnabled && controller.isColumnPinnable(column);
|
|
1491
|
-
const isPinned = controller.isPinned(column);
|
|
1492
|
-
const pinOffset = isPinned ? controller.pinnedOffset(column) : null;
|
|
1493
|
-
const isPinnedLeftLast = controller.isPinnedLeftLast(column);
|
|
1494
|
-
const isPinnedRightFirst = controller.isPinnedRightFirst(column);
|
|
1495
|
-
const isDragTarget = this.dropTargetColumnName === column.name;
|
|
1496
|
-
const isDragging = this.draggedColumnName === column.name;
|
|
1497
|
-
const classes = [
|
|
1498
|
-
'header-cell',
|
|
1499
|
-
sortDirection !== SORT_DIRECTIONS.none ? 'is-active' : '',
|
|
1500
|
-
isPinned ? 'is-pinned' : '',
|
|
1501
|
-
isPinnedLeftLast ? 'is-pinned-left-last' : '',
|
|
1502
|
-
isPinnedRightFirst ? 'is-pinned-right-first' : '',
|
|
1503
|
-
this.openPinMenuColumn === column.name ? 'is-pin-menu-open' : '',
|
|
1504
|
-
isDragTarget ? 'is-drag-target' : '',
|
|
1505
|
-
isDragging ? 'is-dragging' : '',
|
|
1506
|
-
]
|
|
1507
|
-
.filter(Boolean)
|
|
1508
|
-
.join(' ');
|
|
1509
|
-
|
|
1510
|
-
const stickyStyle = pinOffset ? `${pinOffset.side}:${pinOffset.offset};` : '';
|
|
1511
|
-
|
|
1512
|
-
const sortIconKey =
|
|
1513
|
-
sortDirection === SORT_DIRECTIONS.asc
|
|
1514
|
-
? 'sortAsc'
|
|
1515
|
-
: sortDirection === SORT_DIRECTIONS.desc
|
|
1516
|
-
? 'sortDesc'
|
|
1517
|
-
: 'sortNone';
|
|
1518
|
-
|
|
1519
|
-
const draggable = canGridMoveColumns(options) ? 'draggable="true"' : '';
|
|
1520
|
-
const pinLabel = isPinned
|
|
1521
|
-
? (this.snapshot?.labels.unpin ?? 'Unpin')
|
|
1522
|
-
: (this.snapshot?.labels.pinColumn ?? 'Pin');
|
|
1523
|
-
const headerValue = escapeHtml(formatGridHeaderDisplayValue(buildGridHeaderContext(column)));
|
|
1524
|
-
|
|
1525
|
-
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></div>`;
|
|
1287
|
+
return renderHeaderCell(this, column, sortEnabled, groupingEnabled, pinningEnabled, options);
|
|
1526
1288
|
}
|
|
1527
1289
|
|
|
1528
1290
|
private renderFilterCell(column: GridColumnDef): string {
|
|
1529
|
-
|
|
1530
|
-
const controller = this.controller!;
|
|
1531
|
-
const canFilter = controller.isColumnFilterable(column);
|
|
1532
|
-
const pinOffset = controller.isPinned(column) ? controller.pinnedOffset(column) : null;
|
|
1533
|
-
const classes = [
|
|
1534
|
-
'filter-cell',
|
|
1535
|
-
controller.isPinned(column) ? 'is-pinned' : '',
|
|
1536
|
-
controller.isPinnedLeftLast(column) ? 'is-pinned-left-last' : '',
|
|
1537
|
-
controller.isPinnedRightFirst(column) ? 'is-pinned-right-first' : '',
|
|
1538
|
-
]
|
|
1539
|
-
.filter(Boolean)
|
|
1540
|
-
.join(' ');
|
|
1541
|
-
const stickyStyle = pinOffset ? `${pinOffset.side}:${pinOffset.offset};` : '';
|
|
1542
|
-
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);
|
|
1543
1292
|
}
|
|
1544
1293
|
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
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);
|
|
1313
|
+
}
|
|
1549
1314
|
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
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
|
+
}
|
|
1556
1323
|
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
}
|
|
1324
|
+
private scrollFocusedRowIntoView(rowId: string): void {
|
|
1325
|
+
scrollFocusedRowIntoView(this, rowId);
|
|
1326
|
+
}
|
|
1561
1327
|
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1328
|
+
private handleRowSelectionClick(rowId: string, columnName: string, event: Event): void {
|
|
1329
|
+
handleRowSelectionClick(this, rowId, columnName, event);
|
|
1330
|
+
}
|
|
1565
1331
|
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
.join('');
|
|
1332
|
+
private handleRowHeaderCheckboxClick(rowId: string, event: Event): void {
|
|
1333
|
+
handleRowHeaderCheckboxClick(this, rowId, event);
|
|
1569
1334
|
}
|
|
1570
1335
|
|
|
1571
|
-
private
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
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);
|
|
1348
|
+
}
|
|
1575
1349
|
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
: '';
|
|
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
|
+
}
|
|
1356
|
+
|
|
1357
|
+
private focusCellElement(rowId: string | null, columnName: string | null): void {
|
|
1358
|
+
focusCellElement(this, rowId, columnName);
|
|
1359
|
+
}
|
|
1587
1360
|
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
controller.isPinned(column) ? 'is-pinned' : '',
|
|
1599
|
-
controller.isPinnedLeftLast(column) ? 'is-pinned-left-last' : '',
|
|
1600
|
-
controller.isPinnedRightFirst(column) ? 'is-pinned-right-first' : '',
|
|
1601
|
-
this.focusedCell?.rowId === rowId && this.focusedCell.columnName === columnName
|
|
1602
|
-
? 'cell-focused'
|
|
1603
|
-
: '',
|
|
1604
|
-
editing ? 'cell-editing' : '',
|
|
1605
|
-
]
|
|
1606
|
-
.filter(Boolean)
|
|
1607
|
-
.join(' ');
|
|
1608
|
-
|
|
1609
|
-
const stickyStyle = pinOffset ? `${pinOffset.side}:${pinOffset.offset};` : '';
|
|
1610
|
-
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);
|
|
1611
1371
|
}
|
|
1612
1372
|
|
|
1613
1373
|
private renderPagination(snapshot: GridControllerSnapshot): string {
|
|
1614
|
-
|
|
1615
|
-
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
|
|
1616
|
-
.map(
|
|
1617
|
-
(size) =>
|
|
1618
|
-
`<option value="${size}" ${size === snapshot.pageSize ? 'selected' : ''}>${size}</option>`,
|
|
1619
|
-
)
|
|
1620
|
-
.join('')}</select></label></div></footer>`;
|
|
1374
|
+
return renderPagination(this, snapshot);
|
|
1621
1375
|
}
|
|
1622
1376
|
|
|
1623
1377
|
private renderSlotRegistry(columns: readonly GridColumnDef[]): string {
|
|
1624
|
-
|
|
1625
|
-
.map((column) => `<slot name="${escapeHtml(this.cellSlotName(column))}"></slot>`)
|
|
1626
|
-
.join('');
|
|
1627
|
-
return `<div hidden class="slot-registry">${cellSlots}<slot name="expandable-row"></slot></div>`;
|
|
1378
|
+
return renderSlotRegistry(columns);
|
|
1628
1379
|
}
|
|
1629
1380
|
|
|
1630
1381
|
private renderCellTemplate(row: GridRow, column: GridColumnDef, displayIndex: number): string {
|
|
1631
|
-
|
|
1632
|
-
return this.renderCellTemplateFromMarkup(row, column, displayIndex, templateMarkup);
|
|
1382
|
+
return renderCellTemplate(this, row, column, displayIndex);
|
|
1633
1383
|
}
|
|
1634
1384
|
|
|
1635
1385
|
private renderCellTemplateFromMarkup(
|
|
@@ -1638,62 +1388,20 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1638
1388
|
displayIndex: number,
|
|
1639
1389
|
templateMarkup: string | null,
|
|
1640
1390
|
): string {
|
|
1641
|
-
|
|
1642
|
-
return `<span class="cell-value">${escapeHtml(this.controller?.displayValue(row, column) ?? '')}</span>`;
|
|
1643
|
-
}
|
|
1644
|
-
|
|
1645
|
-
const rawRow = row.entity as GridRecord;
|
|
1646
|
-
const rawValue = getCellValue(rawRow, column);
|
|
1647
|
-
const valueText = rawValue == null ? '' : String(rawValue);
|
|
1648
|
-
return this.interpolateTemplate(templateMarkup, {
|
|
1649
|
-
$implicit: rawValue,
|
|
1650
|
-
value: rawValue,
|
|
1651
|
-
valueText,
|
|
1652
|
-
valueLower: valueText.toLowerCase(),
|
|
1653
|
-
row: rawRow,
|
|
1654
|
-
column,
|
|
1655
|
-
rowIndex: this.rowIndexFor(row, displayIndex),
|
|
1656
|
-
});
|
|
1391
|
+
return renderCellTemplateFromMarkup(this, row, column, displayIndex, templateMarkup);
|
|
1657
1392
|
}
|
|
1658
1393
|
|
|
1659
1394
|
private renderExpandableTemplate(row: GridRow): string {
|
|
1660
|
-
|
|
1661
|
-
if (!templateMarkup) {
|
|
1662
|
-
return `<p>${escapeHtml(String(row.entity['name'] ?? row.id))}</p>`;
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
return this.interpolateTemplate(templateMarkup, {
|
|
1666
|
-
$implicit: row.entity,
|
|
1667
|
-
row: row.entity,
|
|
1668
|
-
expanded: row.expanded,
|
|
1669
|
-
rowIndex: this.rowIndexFor(row),
|
|
1670
|
-
});
|
|
1395
|
+
return renderExpandableTemplate(this, row);
|
|
1671
1396
|
}
|
|
1672
1397
|
|
|
1673
1398
|
private getTemplateMarkup(slotName: string): string | null {
|
|
1674
|
-
|
|
1675
|
-
return template?.innerHTML ?? null;
|
|
1676
|
-
}
|
|
1677
|
-
|
|
1678
|
-
private interpolateTemplate(templateMarkup: string, context: Record<string, unknown>): string {
|
|
1679
|
-
return templateMarkup.replace(/{{\s*([^}]+?)\s*}}/g, (_match, expression) => {
|
|
1680
|
-
const value = this.resolveTemplateValue(context, String(expression).trim());
|
|
1681
|
-
return escapeHtml(value);
|
|
1682
|
-
});
|
|
1399
|
+
return getTemplateMarkupFromLightDom(this, slotName);
|
|
1683
1400
|
}
|
|
1684
1401
|
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
for (const segment of segments) {
|
|
1690
|
-
if (current == null || typeof current !== 'object') {
|
|
1691
|
-
return '';
|
|
1692
|
-
}
|
|
1693
|
-
current = (current as Record<string, unknown>)[segment];
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
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);
|
|
1697
1405
|
}
|
|
1698
1406
|
|
|
1699
1407
|
private rowIndexFor(row: GridRow, fallback = 0): number {
|
|
@@ -1723,7 +1431,9 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1723
1431
|
}
|
|
1724
1432
|
|
|
1725
1433
|
for (let index = 0; index < previousSnapshot.visibleColumns.length; index += 1) {
|
|
1726
|
-
if (
|
|
1434
|
+
if (
|
|
1435
|
+
previousSnapshot.visibleColumns[index]?.name !== nextSnapshot.visibleColumns[index]?.name
|
|
1436
|
+
) {
|
|
1727
1437
|
return { mode: 'full', changedRowIds: null };
|
|
1728
1438
|
}
|
|
1729
1439
|
}
|
|
@@ -1781,12 +1491,12 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1781
1491
|
};
|
|
1782
1492
|
}
|
|
1783
1493
|
|
|
1784
|
-
const
|
|
1785
|
-
const
|
|
1786
|
-
|
|
1787
|
-
|
|
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);
|
|
1788
1498
|
const overscan = 4;
|
|
1789
|
-
const startIndex = Math.max(0, Math.floor(
|
|
1499
|
+
const startIndex = Math.max(0, Math.floor(this.scrollPosition / snapshot.rowSize) - overscan);
|
|
1790
1500
|
const visibleCount = Math.ceil(bodyViewportHeight / snapshot.rowSize) + overscan * 2;
|
|
1791
1501
|
|
|
1792
1502
|
return {
|
|
@@ -1802,29 +1512,38 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1802
1512
|
return `cell-${column.name}`;
|
|
1803
1513
|
}
|
|
1804
1514
|
|
|
1805
|
-
private
|
|
1806
|
-
|
|
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) {
|
|
1807
1525
|
return;
|
|
1808
1526
|
}
|
|
1809
|
-
|
|
1810
|
-
const csv = exportCsvRows(this.snapshot.visibleColumns, this.snapshot.pipeline.visibleRows);
|
|
1811
|
-
const filename = sanitizeDownloadFilename(`${this.snapshot.options.id}.csv`);
|
|
1812
|
-
downloadGridCsvFile(csv, filename);
|
|
1527
|
+
this.controller.gridApi.exporter.csvExport(rowType, colType);
|
|
1813
1528
|
}
|
|
1814
1529
|
|
|
1815
1530
|
private renderControlIcon(key: UiGridControlIconKey): string {
|
|
1816
|
-
return this.
|
|
1531
|
+
return this.icons.renderControlIcon(key);
|
|
1817
1532
|
}
|
|
1818
1533
|
|
|
1819
1534
|
private renderIconWithClass(svgClass: string, key: UiGridControlIconKey): string {
|
|
1820
|
-
|
|
1821
|
-
const viewBox = escapeHtml(icon.viewBox ?? '0 0 24 24');
|
|
1822
|
-
const path = escapeHtml(icon.path);
|
|
1823
|
-
return `<svg class="${svgClass}" viewBox="${viewBox}" aria-hidden="true" focusable="false"><path d="${path}"></path></svg>`;
|
|
1535
|
+
return this.icons.renderIconWithClass(svgClass, key);
|
|
1824
1536
|
}
|
|
1825
1537
|
}
|
|
1826
1538
|
|
|
1827
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();
|
|
1828
1547
|
if (!customElements.get(tagName)) {
|
|
1829
1548
|
customElements.define(tagName, UiGridStandaloneElement);
|
|
1830
1549
|
}
|