@ornery/ui-grid-vanilla 0.1.7 → 0.1.8-hotfix
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/demo/index.html +145 -1
- package/demo/main.ts +53 -2
- package/dist/index.d.mts +114 -1
- package/dist/index.d.ts +114 -1
- package/dist/index.js +595 -16
- package/dist/index.mjs +597 -16
- package/package.json +1 -1
- package/src/grid-controller.ts +24 -0
- package/src/index.integration.test.ts +32 -0
- package/src/ui-grid-standalone.element.ts +702 -17
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
GRID_CORE_CSS,
|
|
3
3
|
SORT_DIRECTIONS,
|
|
4
|
+
buildGridHeaderContext,
|
|
4
5
|
canGridMoveColumns,
|
|
5
6
|
downloadGridCsvFile,
|
|
6
7
|
exportCsvRows,
|
|
8
|
+
formatGridHeaderDisplayValue,
|
|
7
9
|
getCellValue,
|
|
8
10
|
sanitizeDownloadFilename,
|
|
9
11
|
type DisplayItem,
|
|
@@ -42,6 +44,12 @@ export type VanillaUiGridElement = HTMLElement & {
|
|
|
42
44
|
options: GridOptions;
|
|
43
45
|
getState(): GridSaveState | null;
|
|
44
46
|
setState(state: Partial<GridSaveState>): void;
|
|
47
|
+
/**
|
|
48
|
+
* Fast-path data update: patches only visible cell content in the existing
|
|
49
|
+
* shadow DOM. Does NOT rebuild the scroll container or header, so scrolling
|
|
50
|
+
* remains completely smooth while data ticks in the background.
|
|
51
|
+
*/
|
|
52
|
+
setData(rows: GridRecord[]): void;
|
|
45
53
|
};
|
|
46
54
|
|
|
47
55
|
export type UiGridControlIconKey =
|
|
@@ -95,6 +103,8 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
95
103
|
private snapshot: GridControllerSnapshot | null = null;
|
|
96
104
|
private unsubscribe: (() => void) | null = null;
|
|
97
105
|
private activeOptions: VanillaGridOptions | null = null;
|
|
106
|
+
private attributeOptions: Partial<GridOptions> = {};
|
|
107
|
+
private attributeSyncScheduled = false;
|
|
98
108
|
private iconOverrides: UiGridIconOverrides = {};
|
|
99
109
|
private templateObserver: MutationObserver | null = null;
|
|
100
110
|
private openPinMenuColumn: string | null = null;
|
|
@@ -109,6 +119,53 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
109
119
|
private measuredFilterStickyHeight = 0;
|
|
110
120
|
private stickyHeightRelayoutQueued = false;
|
|
111
121
|
private benchmarkAverage = '—';
|
|
122
|
+
private skipNextRender = false;
|
|
123
|
+
private dataFrame: number | null = null;
|
|
124
|
+
|
|
125
|
+
static get observedAttributes(): string[] {
|
|
126
|
+
return [
|
|
127
|
+
// Scalar attributes
|
|
128
|
+
'grid-id',
|
|
129
|
+
'title',
|
|
130
|
+
'row-height',
|
|
131
|
+
'header-row-height',
|
|
132
|
+
'viewport-height',
|
|
133
|
+
'pagination-page-size',
|
|
134
|
+
'pagination-current-page',
|
|
135
|
+
'total-items',
|
|
136
|
+
'virtualization-threshold',
|
|
137
|
+
'tree-children-field',
|
|
138
|
+
'tree-indent',
|
|
139
|
+
'expandable-row-height',
|
|
140
|
+
'expandable-row-header-width',
|
|
141
|
+
'empty-message',
|
|
142
|
+
'infinite-scroll-rows-from-end',
|
|
143
|
+
// JSON attributes
|
|
144
|
+
'column-defs',
|
|
145
|
+
'data',
|
|
146
|
+
'grouping',
|
|
147
|
+
'pagination-page-sizes',
|
|
148
|
+
// Boolean flags
|
|
149
|
+
'enable-sorting',
|
|
150
|
+
'enable-filtering',
|
|
151
|
+
'enable-grouping',
|
|
152
|
+
'enable-pinning',
|
|
153
|
+
'enable-column-moving',
|
|
154
|
+
'enable-cell-edit',
|
|
155
|
+
'enable-cell-edit-on-focus',
|
|
156
|
+
'enable-pagination',
|
|
157
|
+
'enable-pagination-controls',
|
|
158
|
+
'use-external-pagination',
|
|
159
|
+
'enable-expandable',
|
|
160
|
+
'enable-tree-view',
|
|
161
|
+
'show-tree-expand-no-children',
|
|
162
|
+
'tree-row-header-always-visible',
|
|
163
|
+
'enable-auto-resize',
|
|
164
|
+
'enable-virtualization',
|
|
165
|
+
'infinite-scroll-up',
|
|
166
|
+
'infinite-scroll-down',
|
|
167
|
+
];
|
|
168
|
+
}
|
|
112
169
|
|
|
113
170
|
get options(): GridOptions {
|
|
114
171
|
return (
|
|
@@ -126,6 +183,197 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
126
183
|
this.ensureController(this.buildEffectiveOptions(this.activeOptions));
|
|
127
184
|
}
|
|
128
185
|
|
|
186
|
+
attributeChangedCallback(
|
|
187
|
+
_name: string,
|
|
188
|
+
_oldValue: string | null,
|
|
189
|
+
_newValue: string | null,
|
|
190
|
+
): void {
|
|
191
|
+
// Debounce multiple rapid attribute changes via microtask.
|
|
192
|
+
if (!this.attributeSyncScheduled) {
|
|
193
|
+
this.attributeSyncScheduled = true;
|
|
194
|
+
queueMicrotask(() => {
|
|
195
|
+
this.attributeSyncScheduled = false;
|
|
196
|
+
this.syncAttributesToOptions();
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private syncAttributesToOptions(): void {
|
|
202
|
+
this.attributeOptions = {};
|
|
203
|
+
|
|
204
|
+
// Scalar string attributes
|
|
205
|
+
const gridId = this.getAttribute('grid-id');
|
|
206
|
+
if (gridId !== null) this.attributeOptions.id = gridId;
|
|
207
|
+
|
|
208
|
+
const title = this.getAttribute('title');
|
|
209
|
+
if (title !== null) this.attributeOptions.title = title;
|
|
210
|
+
|
|
211
|
+
const emptyMessage = this.getAttribute('empty-message');
|
|
212
|
+
if (emptyMessage !== null) this.attributeOptions.emptyMessage = emptyMessage;
|
|
213
|
+
|
|
214
|
+
const treeChildrenField = this.getAttribute('tree-children-field');
|
|
215
|
+
if (treeChildrenField !== null) this.attributeOptions.treeChildrenField = treeChildrenField;
|
|
216
|
+
|
|
217
|
+
// Scalar number attributes
|
|
218
|
+
const rowHeight = this.parseNumberAttribute('row-height');
|
|
219
|
+
if (rowHeight !== undefined) this.attributeOptions.rowHeight = rowHeight;
|
|
220
|
+
|
|
221
|
+
const headerRowHeight = this.parseNumberAttribute('header-row-height');
|
|
222
|
+
if (headerRowHeight !== undefined)
|
|
223
|
+
this.attributeOptions.headerRowHeight = headerRowHeight;
|
|
224
|
+
|
|
225
|
+
const viewportHeight = this.parseNumberAttribute('viewport-height');
|
|
226
|
+
if (viewportHeight !== undefined) this.attributeOptions.viewportHeight = viewportHeight;
|
|
227
|
+
|
|
228
|
+
const paginationPageSize = this.parseNumberAttribute('pagination-page-size');
|
|
229
|
+
if (paginationPageSize !== undefined)
|
|
230
|
+
this.attributeOptions.paginationPageSize = paginationPageSize;
|
|
231
|
+
|
|
232
|
+
const paginationCurrentPage = this.parseNumberAttribute('pagination-current-page');
|
|
233
|
+
if (paginationCurrentPage !== undefined)
|
|
234
|
+
this.attributeOptions.paginationCurrentPage = paginationCurrentPage;
|
|
235
|
+
|
|
236
|
+
const totalItems = this.parseNumberAttribute('total-items');
|
|
237
|
+
if (totalItems !== undefined) this.attributeOptions.totalItems = totalItems;
|
|
238
|
+
|
|
239
|
+
const virtualizationThreshold = this.parseNumberAttribute('virtualization-threshold');
|
|
240
|
+
if (virtualizationThreshold !== undefined)
|
|
241
|
+
this.attributeOptions.virtualizationThreshold = virtualizationThreshold;
|
|
242
|
+
|
|
243
|
+
const treeIndent = this.parseNumberAttribute('tree-indent');
|
|
244
|
+
if (treeIndent !== undefined) this.attributeOptions.treeIndent = treeIndent;
|
|
245
|
+
|
|
246
|
+
const expandableRowHeight = this.parseNumberAttribute('expandable-row-height');
|
|
247
|
+
if (expandableRowHeight !== undefined)
|
|
248
|
+
this.attributeOptions.expandableRowHeight = expandableRowHeight;
|
|
249
|
+
|
|
250
|
+
const expandableRowHeaderWidth = this.parseNumberAttribute(
|
|
251
|
+
'expandable-row-header-width',
|
|
252
|
+
);
|
|
253
|
+
if (expandableRowHeaderWidth !== undefined)
|
|
254
|
+
this.attributeOptions.expandableRowHeaderWidth = expandableRowHeaderWidth;
|
|
255
|
+
|
|
256
|
+
const infiniteScrollRowsFromEnd = this.parseNumberAttribute(
|
|
257
|
+
'infinite-scroll-rows-from-end',
|
|
258
|
+
);
|
|
259
|
+
if (infiniteScrollRowsFromEnd !== undefined)
|
|
260
|
+
this.attributeOptions.infiniteScrollRowsFromEnd = infiniteScrollRowsFromEnd;
|
|
261
|
+
|
|
262
|
+
// Boolean attributes
|
|
263
|
+
const enableSorting = this.parseBooleanAttribute('enable-sorting');
|
|
264
|
+
if (enableSorting !== undefined) this.attributeOptions.enableSorting = enableSorting;
|
|
265
|
+
|
|
266
|
+
const enableFiltering = this.parseBooleanAttribute('enable-filtering');
|
|
267
|
+
if (enableFiltering !== undefined) this.attributeOptions.enableFiltering = enableFiltering;
|
|
268
|
+
|
|
269
|
+
const enableGrouping = this.parseBooleanAttribute('enable-grouping');
|
|
270
|
+
if (enableGrouping !== undefined) this.attributeOptions.enableGrouping = enableGrouping;
|
|
271
|
+
|
|
272
|
+
const enablePinning = this.parseBooleanAttribute('enable-pinning');
|
|
273
|
+
if (enablePinning !== undefined) this.attributeOptions.enablePinning = enablePinning;
|
|
274
|
+
|
|
275
|
+
const enableColumnMoving = this.parseBooleanAttribute('enable-column-moving');
|
|
276
|
+
if (enableColumnMoving !== undefined)
|
|
277
|
+
this.attributeOptions.enableColumnMoving = enableColumnMoving;
|
|
278
|
+
|
|
279
|
+
const enableCellEdit = this.parseBooleanAttribute('enable-cell-edit');
|
|
280
|
+
if (enableCellEdit !== undefined) this.attributeOptions.enableCellEdit = enableCellEdit;
|
|
281
|
+
|
|
282
|
+
const enableCellEditOnFocus = this.parseBooleanAttribute('enable-cell-edit-on-focus');
|
|
283
|
+
if (enableCellEditOnFocus !== undefined)
|
|
284
|
+
this.attributeOptions.enableCellEditOnFocus = enableCellEditOnFocus;
|
|
285
|
+
|
|
286
|
+
const enablePagination = this.parseBooleanAttribute('enable-pagination');
|
|
287
|
+
if (enablePagination !== undefined)
|
|
288
|
+
this.attributeOptions.enablePagination = enablePagination;
|
|
289
|
+
|
|
290
|
+
const enablePaginationControls = this.parseBooleanAttribute(
|
|
291
|
+
'enable-pagination-controls',
|
|
292
|
+
);
|
|
293
|
+
if (enablePaginationControls !== undefined)
|
|
294
|
+
this.attributeOptions.enablePaginationControls = enablePaginationControls;
|
|
295
|
+
|
|
296
|
+
const useExternalPagination = this.parseBooleanAttribute('use-external-pagination');
|
|
297
|
+
if (useExternalPagination !== undefined)
|
|
298
|
+
this.attributeOptions.useExternalPagination = useExternalPagination;
|
|
299
|
+
|
|
300
|
+
const enableExpandable = this.parseBooleanAttribute('enable-expandable');
|
|
301
|
+
if (enableExpandable !== undefined) this.attributeOptions.enableExpandable = enableExpandable;
|
|
302
|
+
|
|
303
|
+
const enableTreeView = this.parseBooleanAttribute('enable-tree-view');
|
|
304
|
+
if (enableTreeView !== undefined) this.attributeOptions.enableTreeView = enableTreeView;
|
|
305
|
+
|
|
306
|
+
const showTreeExpandNoChildren = this.parseBooleanAttribute(
|
|
307
|
+
'show-tree-expand-no-children',
|
|
308
|
+
);
|
|
309
|
+
if (showTreeExpandNoChildren !== undefined)
|
|
310
|
+
this.attributeOptions.showTreeExpandNoChildren = showTreeExpandNoChildren;
|
|
311
|
+
|
|
312
|
+
const treeRowHeaderAlwaysVisible = this.parseBooleanAttribute(
|
|
313
|
+
'tree-row-header-always-visible',
|
|
314
|
+
);
|
|
315
|
+
if (treeRowHeaderAlwaysVisible !== undefined)
|
|
316
|
+
this.attributeOptions.treeRowHeaderAlwaysVisible = treeRowHeaderAlwaysVisible;
|
|
317
|
+
|
|
318
|
+
const enableAutoResize = this.parseBooleanAttribute('enable-auto-resize');
|
|
319
|
+
if (enableAutoResize !== undefined) this.attributeOptions.enableAutoResize = enableAutoResize;
|
|
320
|
+
|
|
321
|
+
const enableVirtualization = this.parseBooleanAttribute('enable-virtualization');
|
|
322
|
+
if (enableVirtualization !== undefined)
|
|
323
|
+
this.attributeOptions.enableVirtualization = enableVirtualization;
|
|
324
|
+
|
|
325
|
+
const infiniteScrollUp = this.parseBooleanAttribute('infinite-scroll-up');
|
|
326
|
+
if (infiniteScrollUp !== undefined) this.attributeOptions.infiniteScrollUp = infiniteScrollUp;
|
|
327
|
+
|
|
328
|
+
const infiniteScrollDown = this.parseBooleanAttribute('infinite-scroll-down');
|
|
329
|
+
if (infiniteScrollDown !== undefined)
|
|
330
|
+
this.attributeOptions.infiniteScrollDown = infiniteScrollDown;
|
|
331
|
+
|
|
332
|
+
// JSON attributes
|
|
333
|
+
const columnDefs = this.parseJsonAttribute<GridColumnDef[]>('column-defs');
|
|
334
|
+
if (columnDefs !== undefined) this.attributeOptions.columnDefs = columnDefs;
|
|
335
|
+
|
|
336
|
+
const data = this.parseJsonAttribute<GridRecord[]>('data');
|
|
337
|
+
if (data !== undefined) this.attributeOptions.data = data;
|
|
338
|
+
|
|
339
|
+
const grouping = this.parseJsonAttribute('grouping');
|
|
340
|
+
if (grouping !== undefined && grouping !== null)
|
|
341
|
+
this.attributeOptions.grouping = grouping;
|
|
342
|
+
|
|
343
|
+
const paginationPageSizes = this.parseJsonAttribute<number[] | null>(
|
|
344
|
+
'pagination-page-sizes',
|
|
345
|
+
);
|
|
346
|
+
if (paginationPageSizes !== undefined)
|
|
347
|
+
this.attributeOptions.paginationPageSizes = paginationPageSizes;
|
|
348
|
+
|
|
349
|
+
// Re-render with the merged options.
|
|
350
|
+
if (this.activeOptions) {
|
|
351
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private parseBooleanAttribute(name: string): boolean | undefined {
|
|
356
|
+
return this.hasAttribute(name) ? true : undefined;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private parseNumberAttribute(name: string): number | undefined {
|
|
360
|
+
const raw = this.getAttribute(name);
|
|
361
|
+
if (raw === null) return undefined;
|
|
362
|
+
const parsed = Number(raw);
|
|
363
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private parseJsonAttribute<T = unknown>(name: string): T | undefined {
|
|
367
|
+
const raw = this.getAttribute(name);
|
|
368
|
+
if (raw === null) return undefined;
|
|
369
|
+
try {
|
|
370
|
+
return JSON.parse(raw) as T;
|
|
371
|
+
} catch (e) {
|
|
372
|
+
console.warn(`<ui-grid-element>: invalid JSON in "${name}" attribute`, e);
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
129
377
|
get controlIcons(): UiGridIconOverrides {
|
|
130
378
|
return this.iconOverrides;
|
|
131
379
|
}
|
|
@@ -135,6 +383,308 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
135
383
|
this.render();
|
|
136
384
|
}
|
|
137
385
|
|
|
386
|
+
// ─────────────────────────────────────────────────────────────────
|
|
387
|
+
// Individual property accessors (mirrors of HTML attributes)
|
|
388
|
+
// ─────────────────────────────────────────────────────────────────
|
|
389
|
+
|
|
390
|
+
get gridId(): string {
|
|
391
|
+
return this.options.id ?? '';
|
|
392
|
+
}
|
|
393
|
+
set gridId(value: string) {
|
|
394
|
+
this.activeOptions = { ...this.activeOptions, id: value } as any;
|
|
395
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// @ts-ignore: Intentional override of HTMLElement.title
|
|
399
|
+
get title(): string {
|
|
400
|
+
return this.options.title ?? '';
|
|
401
|
+
}
|
|
402
|
+
// @ts-ignore: Intentional override of HTMLElement.title
|
|
403
|
+
set title(value: string) {
|
|
404
|
+
this.activeOptions = { ...this.activeOptions, title: value } as any;
|
|
405
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
get rowHeight(): number {
|
|
409
|
+
return this.options.rowHeight ?? 40;
|
|
410
|
+
}
|
|
411
|
+
set rowHeight(value: number) {
|
|
412
|
+
this.activeOptions = { ...this.activeOptions, rowHeight: value } as any;
|
|
413
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
get headerRowHeight(): number {
|
|
417
|
+
return this.options.headerRowHeight ?? 50;
|
|
418
|
+
}
|
|
419
|
+
set headerRowHeight(value: number) {
|
|
420
|
+
this.activeOptions = { ...this.activeOptions, headerRowHeight: value } as any;
|
|
421
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
get viewportHeight(): number {
|
|
425
|
+
return this.options.viewportHeight ?? 560;
|
|
426
|
+
}
|
|
427
|
+
set viewportHeight(value: number) {
|
|
428
|
+
this.activeOptions = { ...this.activeOptions, viewportHeight: value } as any;
|
|
429
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
get paginationPageSize(): number {
|
|
433
|
+
return this.options.paginationPageSize ?? 25;
|
|
434
|
+
}
|
|
435
|
+
set paginationPageSize(value: number) {
|
|
436
|
+
this.activeOptions = { ...this.activeOptions, paginationPageSize: value } as any;
|
|
437
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
get paginationCurrentPage(): number {
|
|
441
|
+
return this.options.paginationCurrentPage ?? 1;
|
|
442
|
+
}
|
|
443
|
+
set paginationCurrentPage(value: number) {
|
|
444
|
+
this.activeOptions = { ...this.activeOptions, paginationCurrentPage: value } as any;
|
|
445
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
get totalItems(): number | undefined {
|
|
449
|
+
return this.options.totalItems;
|
|
450
|
+
}
|
|
451
|
+
set totalItems(value: number | undefined) {
|
|
452
|
+
this.activeOptions = { ...this.activeOptions, totalItems: value } as any;
|
|
453
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
get virtualizationThreshold(): number {
|
|
457
|
+
return this.options.virtualizationThreshold ?? 40;
|
|
458
|
+
}
|
|
459
|
+
set virtualizationThreshold(value: number) {
|
|
460
|
+
this.activeOptions = { ...this.activeOptions, virtualizationThreshold: value } as any;
|
|
461
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
get treeChildrenField(): string {
|
|
465
|
+
return this.options.treeChildrenField ?? 'children';
|
|
466
|
+
}
|
|
467
|
+
set treeChildrenField(value: string) {
|
|
468
|
+
this.activeOptions = { ...this.activeOptions, treeChildrenField: value } as any;
|
|
469
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
get treeIndent(): number {
|
|
473
|
+
return this.options.treeIndent ?? 20;
|
|
474
|
+
}
|
|
475
|
+
set treeIndent(value: number) {
|
|
476
|
+
this.activeOptions = { ...this.activeOptions, treeIndent: value } as any;
|
|
477
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
get expandableRowHeight(): number {
|
|
481
|
+
return this.options.expandableRowHeight ?? 150;
|
|
482
|
+
}
|
|
483
|
+
set expandableRowHeight(value: number) {
|
|
484
|
+
this.activeOptions = { ...this.activeOptions, expandableRowHeight: value } as any;
|
|
485
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
get expandableRowHeaderWidth(): number {
|
|
489
|
+
return this.options.expandableRowHeaderWidth ?? 40;
|
|
490
|
+
}
|
|
491
|
+
set expandableRowHeaderWidth(value: number) {
|
|
492
|
+
this.activeOptions = { ...this.activeOptions, expandableRowHeaderWidth: value } as any;
|
|
493
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
get emptyMessage(): string {
|
|
497
|
+
return this.options.emptyMessage ?? 'No data available.';
|
|
498
|
+
}
|
|
499
|
+
set emptyMessage(value: string) {
|
|
500
|
+
this.activeOptions = { ...this.activeOptions, emptyMessage: value } as any;
|
|
501
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
get infiniteScrollRowsFromEnd(): number {
|
|
505
|
+
return this.options.infiniteScrollRowsFromEnd ?? 10;
|
|
506
|
+
}
|
|
507
|
+
set infiniteScrollRowsFromEnd(value: number) {
|
|
508
|
+
this.activeOptions = { ...this.activeOptions, infiniteScrollRowsFromEnd: value } as any;
|
|
509
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
get columnDefs(): readonly GridColumnDef[] {
|
|
513
|
+
return this.options.columnDefs;
|
|
514
|
+
}
|
|
515
|
+
set columnDefs(value: readonly GridColumnDef[]) {
|
|
516
|
+
this.activeOptions = { ...this.activeOptions, columnDefs: value } as any;
|
|
517
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
get data(): readonly GridRecord[] {
|
|
521
|
+
return this.options.data;
|
|
522
|
+
}
|
|
523
|
+
set data(value: readonly GridRecord[]) {
|
|
524
|
+
this.activeOptions = { ...this.activeOptions, data: value } as any;
|
|
525
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
get enableSorting(): boolean {
|
|
529
|
+
return this.options.enableSorting ?? true;
|
|
530
|
+
}
|
|
531
|
+
set enableSorting(value: boolean) {
|
|
532
|
+
this.activeOptions = { ...this.activeOptions, enableSorting: value } as any;
|
|
533
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
get enableFiltering(): boolean {
|
|
537
|
+
return this.options.enableFiltering ?? true;
|
|
538
|
+
}
|
|
539
|
+
set enableFiltering(value: boolean) {
|
|
540
|
+
this.activeOptions = { ...this.activeOptions, enableFiltering: value } as any;
|
|
541
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
get enableGrouping(): boolean {
|
|
545
|
+
return this.options.enableGrouping ?? true;
|
|
546
|
+
}
|
|
547
|
+
set enableGrouping(value: boolean) {
|
|
548
|
+
this.activeOptions = { ...this.activeOptions, enableGrouping: value } as any;
|
|
549
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
get enablePinning(): boolean {
|
|
553
|
+
return this.options.enablePinning ?? true;
|
|
554
|
+
}
|
|
555
|
+
set enablePinning(value: boolean) {
|
|
556
|
+
this.activeOptions = { ...this.activeOptions, enablePinning: value } as any;
|
|
557
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
get enableColumnMoving(): boolean {
|
|
561
|
+
return this.options.enableColumnMoving ?? true;
|
|
562
|
+
}
|
|
563
|
+
set enableColumnMoving(value: boolean) {
|
|
564
|
+
this.activeOptions = { ...this.activeOptions, enableColumnMoving: value } as any;
|
|
565
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
get enableCellEdit(): boolean {
|
|
569
|
+
return this.options.enableCellEdit ?? false;
|
|
570
|
+
}
|
|
571
|
+
set enableCellEdit(value: boolean) {
|
|
572
|
+
this.activeOptions = { ...this.activeOptions, enableCellEdit: value } as any;
|
|
573
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
get enableCellEditOnFocus(): boolean {
|
|
577
|
+
return this.options.enableCellEditOnFocus ?? false;
|
|
578
|
+
}
|
|
579
|
+
set enableCellEditOnFocus(value: boolean) {
|
|
580
|
+
this.activeOptions = { ...this.activeOptions, enableCellEditOnFocus: value } as any;
|
|
581
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
get enablePagination(): boolean {
|
|
585
|
+
return this.options.enablePagination ?? false;
|
|
586
|
+
}
|
|
587
|
+
set enablePagination(value: boolean) {
|
|
588
|
+
this.activeOptions = { ...this.activeOptions, enablePagination: value } as any;
|
|
589
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
get enablePaginationControls(): boolean {
|
|
593
|
+
return this.options.enablePaginationControls ?? true;
|
|
594
|
+
}
|
|
595
|
+
set enablePaginationControls(value: boolean) {
|
|
596
|
+
this.activeOptions = { ...this.activeOptions, enablePaginationControls: value } as any;
|
|
597
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
get useExternalPagination(): boolean {
|
|
601
|
+
return this.options.useExternalPagination ?? false;
|
|
602
|
+
}
|
|
603
|
+
set useExternalPagination(value: boolean) {
|
|
604
|
+
this.activeOptions = { ...this.activeOptions, useExternalPagination: value } as any;
|
|
605
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
get enableExpandable(): boolean {
|
|
609
|
+
return this.options.enableExpandable ?? false;
|
|
610
|
+
}
|
|
611
|
+
set enableExpandable(value: boolean) {
|
|
612
|
+
this.activeOptions = { ...this.activeOptions, enableExpandable: value } as any;
|
|
613
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
get enableTreeView(): boolean {
|
|
617
|
+
return this.options.enableTreeView ?? false;
|
|
618
|
+
}
|
|
619
|
+
set enableTreeView(value: boolean) {
|
|
620
|
+
this.activeOptions = { ...this.activeOptions, enableTreeView: value } as any;
|
|
621
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
get showTreeExpandNoChildren(): boolean {
|
|
625
|
+
return this.options.showTreeExpandNoChildren ?? false;
|
|
626
|
+
}
|
|
627
|
+
set showTreeExpandNoChildren(value: boolean) {
|
|
628
|
+
this.activeOptions = { ...this.activeOptions, showTreeExpandNoChildren: value } as any;
|
|
629
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
get treeRowHeaderAlwaysVisible(): boolean {
|
|
633
|
+
return this.options.treeRowHeaderAlwaysVisible ?? false;
|
|
634
|
+
}
|
|
635
|
+
set treeRowHeaderAlwaysVisible(value: boolean) {
|
|
636
|
+
this.activeOptions = { ...this.activeOptions, treeRowHeaderAlwaysVisible: value } as any;
|
|
637
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
get enableAutoResize(): boolean {
|
|
641
|
+
return this.options.enableAutoResize ?? false;
|
|
642
|
+
}
|
|
643
|
+
set enableAutoResize(value: boolean) {
|
|
644
|
+
this.activeOptions = { ...this.activeOptions, enableAutoResize: value } as any;
|
|
645
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
get enableVirtualization(): boolean {
|
|
649
|
+
return this.options.enableVirtualization ?? true;
|
|
650
|
+
}
|
|
651
|
+
set enableVirtualization(value: boolean) {
|
|
652
|
+
this.activeOptions = { ...this.activeOptions, enableVirtualization: value } as any;
|
|
653
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
get infiniteScrollUp(): boolean {
|
|
657
|
+
return this.options.infiniteScrollUp ?? false;
|
|
658
|
+
}
|
|
659
|
+
set infiniteScrollUp(value: boolean) {
|
|
660
|
+
this.activeOptions = { ...this.activeOptions, infiniteScrollUp: value } as any;
|
|
661
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
get infiniteScrollDown(): boolean {
|
|
665
|
+
return this.options.infiniteScrollDown ?? false;
|
|
666
|
+
}
|
|
667
|
+
set infiniteScrollDown(value: boolean) {
|
|
668
|
+
this.activeOptions = { ...this.activeOptions, infiniteScrollDown: value } as any;
|
|
669
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
get grouping(): unknown {
|
|
673
|
+
return this.options.grouping;
|
|
674
|
+
}
|
|
675
|
+
set grouping(value: unknown) {
|
|
676
|
+
this.activeOptions = { ...this.activeOptions, grouping: value } as any;
|
|
677
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
get paginationPageSizes(): number[] | null | undefined {
|
|
681
|
+
return this.options.paginationPageSizes;
|
|
682
|
+
}
|
|
683
|
+
set paginationPageSizes(value: number[] | null | undefined) {
|
|
684
|
+
this.activeOptions = { ...this.activeOptions, paginationPageSizes: value } as any;
|
|
685
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
686
|
+
}
|
|
687
|
+
|
|
138
688
|
getState(): GridSaveState | null {
|
|
139
689
|
return this.controller?.getState() ?? null;
|
|
140
690
|
}
|
|
@@ -143,6 +693,83 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
143
693
|
this.controller?.setState(state);
|
|
144
694
|
}
|
|
145
695
|
|
|
696
|
+
/**
|
|
697
|
+
* Fast-path: update only row data without rebuilding the shadow DOM.
|
|
698
|
+
* The pipeline is updated synchronously; DOM patching is deferred to the
|
|
699
|
+
* next animation frame so it never interrupts an in-progress scroll paint.
|
|
700
|
+
* If multiple ticks arrive before the frame fires, only the latest wins.
|
|
701
|
+
* If a scroll-triggered full render fires first, the frame is cancelled
|
|
702
|
+
* (the fresh innerHTML already reflects the latest data).
|
|
703
|
+
*/
|
|
704
|
+
setData(rows: GridRecord[]): void {
|
|
705
|
+
const controller = this.controller;
|
|
706
|
+
if (!controller) {
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Update the pipeline synchronously (cheap, no DOM work).
|
|
711
|
+
this.skipNextRender = true;
|
|
712
|
+
controller.refreshData(rows);
|
|
713
|
+
this.skipNextRender = false;
|
|
714
|
+
|
|
715
|
+
// Schedule DOM patching on the next paint — latest call wins.
|
|
716
|
+
if (this.dataFrame !== null) {
|
|
717
|
+
cancelAnimationFrame(this.dataFrame);
|
|
718
|
+
}
|
|
719
|
+
this.dataFrame = requestAnimationFrame(() => {
|
|
720
|
+
this.dataFrame = null;
|
|
721
|
+
this.patchCells();
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
private patchCells(): void {
|
|
726
|
+
const snapshot = this.snapshot;
|
|
727
|
+
if (!snapshot) {
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const root = this.shadowRoot;
|
|
732
|
+
if (!root) {
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Build O(1) lookup maps from the current pipeline snapshot.
|
|
737
|
+
const rowMap = new Map<string, GridRow>();
|
|
738
|
+
for (const row of snapshot.pipeline.visibleRows) {
|
|
739
|
+
rowMap.set(row.id, row);
|
|
740
|
+
}
|
|
741
|
+
const colMap = new Map<string, GridColumnDef>();
|
|
742
|
+
for (const col of snapshot.visibleColumns) {
|
|
743
|
+
colMap.set(col.name, col);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Patch every rendered body cell in-place.
|
|
747
|
+
const cells = root.querySelectorAll<HTMLElement>('[data-row][data-column]');
|
|
748
|
+
for (const cell of cells) {
|
|
749
|
+
const rowId = cell.dataset['row'];
|
|
750
|
+
const colName = cell.dataset['column'];
|
|
751
|
+
if (!rowId || !colName) continue;
|
|
752
|
+
|
|
753
|
+
const row = rowMap.get(rowId);
|
|
754
|
+
const column = colMap.get(colName);
|
|
755
|
+
if (!row || !column) continue;
|
|
756
|
+
|
|
757
|
+
const cellShell = cell.querySelector<HTMLElement>('.cell-shell');
|
|
758
|
+
if (!cellShell) continue;
|
|
759
|
+
|
|
760
|
+
const newContent = this.renderCellTemplate(row, column, 0);
|
|
761
|
+
const toggles = cellShell.querySelectorAll<HTMLElement>('.row-toggle');
|
|
762
|
+
if (toggles.length === 0) {
|
|
763
|
+
cellShell.innerHTML = newContent;
|
|
764
|
+
} else {
|
|
765
|
+
const toggleHtml = Array.from(toggles)
|
|
766
|
+
.map((t) => t.outerHTML)
|
|
767
|
+
.join('');
|
|
768
|
+
cellShell.innerHTML = toggleHtml + newContent;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
146
773
|
connectedCallback(): void {
|
|
147
774
|
this.ensureShadowRoot();
|
|
148
775
|
this.bindEvents();
|
|
@@ -209,17 +836,21 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
209
836
|
}
|
|
210
837
|
|
|
211
838
|
private buildEffectiveOptions(options: VanillaGridOptions): VanillaGridOptions {
|
|
839
|
+
// Merge: attributes are overridden by explicit JS property values.
|
|
840
|
+
// JS property always wins: { ...attributeOptions, ...options }
|
|
841
|
+
const merged = {
|
|
842
|
+
...this.attributeOptions,
|
|
843
|
+
...options,
|
|
844
|
+
} as VanillaGridOptions;
|
|
845
|
+
|
|
212
846
|
const hasExpandableSlot = this.getTemplateMarkup('expandable-row') !== null;
|
|
213
|
-
if (
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
expandableRowTemplate: {
|
|
217
|
-
createEmbeddedView: () => undefined,
|
|
218
|
-
},
|
|
847
|
+
if (merged.enableExpandable && hasExpandableSlot && !merged.expandableRowTemplate) {
|
|
848
|
+
merged.expandableRowTemplate = {
|
|
849
|
+
createEmbeddedView: () => undefined,
|
|
219
850
|
};
|
|
220
851
|
}
|
|
221
852
|
|
|
222
|
-
return
|
|
853
|
+
return merged;
|
|
223
854
|
}
|
|
224
855
|
|
|
225
856
|
private bindEvents(): void {
|
|
@@ -595,7 +1226,7 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
595
1226
|
|
|
596
1227
|
this.scrollFrame = requestAnimationFrame(() => {
|
|
597
1228
|
this.scrollFrame = null;
|
|
598
|
-
this.
|
|
1229
|
+
this.renderVirtualBody();
|
|
599
1230
|
});
|
|
600
1231
|
},
|
|
601
1232
|
true,
|
|
@@ -604,7 +1235,67 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
604
1235
|
(root as ShadowRoot & { __uiGridBound?: boolean }).__uiGridBound = true;
|
|
605
1236
|
}
|
|
606
1237
|
|
|
1238
|
+
/**
|
|
1239
|
+
* Targeted update for virtual scroll: replaces only the rendered rows inside
|
|
1240
|
+
* the existing `.grid-virtual-body` without touching the scroll container.
|
|
1241
|
+
* This preserves momentum/inertia scrolling because the `.grid-table` element
|
|
1242
|
+
* is never destroyed.
|
|
1243
|
+
*/
|
|
1244
|
+
private renderVirtualBody(): void {
|
|
1245
|
+
const snapshot = this.snapshot;
|
|
1246
|
+
if (!snapshot?.pipeline.virtualizationEnabled) {
|
|
1247
|
+
this.render();
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
const root = this.shadowRoot;
|
|
1252
|
+
const virtualBody = root?.querySelector<HTMLElement>('.grid-virtual-body');
|
|
1253
|
+
if (!root || !virtualBody) {
|
|
1254
|
+
this.render();
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// Cancel any pending cell-only patch — the fresh rows we are about to
|
|
1259
|
+
// render already reflect the latest snapshot data.
|
|
1260
|
+
if (this.dataFrame !== null) {
|
|
1261
|
+
cancelAnimationFrame(this.dataFrame);
|
|
1262
|
+
this.dataFrame = null;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
const stickyChromeHeight = this.measuredHeaderStickyHeight + this.measuredFilterStickyHeight;
|
|
1266
|
+
const viewportHeight = snapshot.options.viewportHeight ?? 560;
|
|
1267
|
+
const bodyViewportHeight = Math.max(snapshot.rowSize, viewportHeight - stickyChromeHeight);
|
|
1268
|
+
const bodyScrollTop = Math.max(0, this.scrollPosition - stickyChromeHeight);
|
|
1269
|
+
const overscan = 4;
|
|
1270
|
+
const startIndex = Math.max(0, Math.floor(bodyScrollTop / snapshot.rowSize) - overscan);
|
|
1271
|
+
this.lastVirtualStartIndex = startIndex;
|
|
1272
|
+
const visibleCount = Math.ceil(bodyViewportHeight / snapshot.rowSize) + overscan * 2;
|
|
1273
|
+
const itemsToRender = snapshot.pipeline.displayItems.slice(
|
|
1274
|
+
startIndex,
|
|
1275
|
+
Math.min(snapshot.pipeline.displayItems.length, startIndex + visibleCount),
|
|
1276
|
+
);
|
|
1277
|
+
const virtualOffset = startIndex * snapshot.rowSize;
|
|
1278
|
+
|
|
1279
|
+
virtualBody.style.top = `${virtualOffset}px`;
|
|
1280
|
+
virtualBody.innerHTML = itemsToRender
|
|
1281
|
+
.map((item, index) => this.renderDisplayItem(item, startIndex + index))
|
|
1282
|
+
.join('');
|
|
1283
|
+
}
|
|
1284
|
+
|
|
607
1285
|
private render(): void {
|
|
1286
|
+
if (this.skipNextRender) {
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
// A full render already incorporates the latest data — cancel any pending
|
|
1290
|
+
// frame-based updates to avoid redundant DOM mutations immediately after.
|
|
1291
|
+
if (this.dataFrame !== null) {
|
|
1292
|
+
cancelAnimationFrame(this.dataFrame);
|
|
1293
|
+
this.dataFrame = null;
|
|
1294
|
+
}
|
|
1295
|
+
if (this.scrollFrame !== null) {
|
|
1296
|
+
cancelAnimationFrame(this.scrollFrame);
|
|
1297
|
+
this.scrollFrame = null;
|
|
1298
|
+
}
|
|
608
1299
|
const root = this.ensureShadowRoot();
|
|
609
1300
|
const snapshot = this.snapshot;
|
|
610
1301
|
const previousGridTable = root.querySelector<HTMLElement>('.grid-table');
|
|
@@ -632,10 +1323,6 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
632
1323
|
const headerStickyTop = this.measuredHeaderStickyHeight || options.headerRowHeight || 50;
|
|
633
1324
|
const stickyChromeHeight = this.measuredHeaderStickyHeight + this.measuredFilterStickyHeight;
|
|
634
1325
|
const bodyViewportHeight = Math.max(snapshot.rowSize, viewportHeight - stickyChromeHeight);
|
|
635
|
-
const visibleRowCount = snapshot.pipeline.visibleRows.length;
|
|
636
|
-
const totalRows = options.data.length;
|
|
637
|
-
const pinnedCount = Object.keys(snapshot.pinnedColumns).length;
|
|
638
|
-
|
|
639
1326
|
let startIndex = 0;
|
|
640
1327
|
let itemsToRender = snapshot.pipeline.displayItems;
|
|
641
1328
|
let virtualOffset = 0;
|
|
@@ -681,12 +1368,9 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
681
1368
|
: `<div class="empty-state ui-grid-no-row-overlay"><strong>${escapeHtml(options.emptyMessage ?? labels.emptyHeading)}</strong><p>${escapeHtml(labels.emptyDescription)}</p></div>`;
|
|
682
1369
|
|
|
683
1370
|
const pagination = paginationEnabled && showPagination ? this.renderPagination(snapshot) : '';
|
|
684
|
-
const hero = `<header class="grid-hero"><div><p class="eyebrow">Vanilla web component</p><h1>${escapeHtml(options.title ?? 'UI Grid')}</h1><p class="deck">Framework-neutral custom element with Shadow DOM styling, slot templates, pinning, virtualization, editing, tree view, grouping, and state restore.</p></div><div class="hero-actions"><button type="button" class="action action-secondary" data-action="benchmark">Benchmark</button><button type="button" class="action action-secondary" data-action="export-csv">Export CSV</button><div class="stats-card"><span>${visibleRowCount}</span><small>${escapeHtml(labels.statsVisibleRows)}</small></div></div></header>`;
|
|
685
|
-
const metrics = `<section class="metrics-strip" aria-label="Grid metrics"><article><strong>${snapshot.pipeline.pipelineMs.toFixed(2)} ms</strong><span>pipeline</span></article><article><strong>${virtualizationEnabled ? 'On' : 'Off'}</strong><span>virtualization</span></article><article><strong>${pinnedCount}</strong><span>pinned columns</span></article><article><strong>${escapeHtml(this.benchmarkAverage)}</strong><span>benchmark avg</span></article></section>`;
|
|
686
|
-
const toolbar = `<div class="grid-toolbar"><div><strong>${visibleRowCount}</strong><span>${escapeHtml(labels.toolbarOf)} ${totalRows} ${escapeHtml(labels.toolbarRows)}</span></div><p>${escapeHtml(options.id)}</p></div>`;
|
|
687
1371
|
const gridTableStyle = `${virtualizationEnabled ? `height:${viewportHeight}px;overflow-y:auto;` : ''}--ui-grid-header-sticky-top:${headerStickyTop}px;`;
|
|
688
1372
|
|
|
689
|
-
root.innerHTML = `<style>${GRID_CORE_CSS}</style
|
|
1373
|
+
root.innerHTML = `<style>${GRID_CORE_CSS}</style>${slotRegistry}<section class="grid-frame ui-grid" role="grid" aria-label="${escapeHtml(options.title ?? 'Data grid')}"><div class="grid-table ui-grid-contents-wrapper" style="${gridTableStyle}"><div class="header-grid ui-grid-header ui-grid-header-canvas" style="grid-template-columns:${templateColumns}">${header}</div>${filterRow}${body}</div>${pagination}</section>`;
|
|
690
1374
|
|
|
691
1375
|
const gridTable = root.querySelector<HTMLElement>('.grid-table');
|
|
692
1376
|
if (gridTable && this.scrollPosition > 0 && virtualizationEnabled) {
|
|
@@ -766,8 +1450,9 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
766
1450
|
const pinLabel = isPinned
|
|
767
1451
|
? (this.snapshot?.labels.unpin ?? 'Unpin')
|
|
768
1452
|
: (this.snapshot?.labels.pinColumn ?? 'Pin');
|
|
1453
|
+
const headerValue = escapeHtml(formatGridHeaderDisplayValue(buildGridHeaderContext(column)));
|
|
769
1454
|
|
|
770
|
-
return `<div class="${classes}" data-column="${escapeHtml(column.name)}" ${draggable} style="${stickyStyle}"><span class="header-label">${
|
|
1455
|
+
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>`;
|
|
771
1456
|
}
|
|
772
1457
|
|
|
773
1458
|
private renderFilterCell(column: GridColumnDef): string {
|