@ornery/ui-grid-vanilla 0.1.8 → 0.1.10
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 +3 -3
- package/demo/main.ts +1 -0
- package/dist/index.d.mts +17 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +252 -22
- package/dist/index.mjs +252 -22
- package/package.json +2 -2
- package/src/grid-controller.ts +29 -2
- package/src/index.integration.test.ts +31 -0
- package/src/index.test.ts +1 -1
- package/src/ui-grid-standalone.element.ts +291 -27
|
@@ -5,8 +5,11 @@ import {
|
|
|
5
5
|
canGridMoveColumns,
|
|
6
6
|
downloadGridCsvFile,
|
|
7
7
|
exportCsvRows,
|
|
8
|
+
findNextGridCell,
|
|
8
9
|
formatGridHeaderDisplayValue,
|
|
9
10
|
getCellValue,
|
|
11
|
+
isGridNavigationKey,
|
|
12
|
+
isPrintableGridKey,
|
|
10
13
|
sanitizeDownloadFilename,
|
|
11
14
|
type DisplayItem,
|
|
12
15
|
type GridColumnDef,
|
|
@@ -112,6 +115,7 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
112
115
|
private draggedColumnName: string | null = null;
|
|
113
116
|
private dropTargetColumnName: string | null = null;
|
|
114
117
|
private scrollPosition = 0;
|
|
118
|
+
private horizontalScrollPosition = 0;
|
|
115
119
|
private scrollFrame: number | null = null;
|
|
116
120
|
private suppressScrollEvent = false;
|
|
117
121
|
private lastVirtualStartIndex = -1;
|
|
@@ -121,6 +125,9 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
121
125
|
private benchmarkAverage = '—';
|
|
122
126
|
private skipNextRender = false;
|
|
123
127
|
private dataFrame: number | null = null;
|
|
128
|
+
private pendingPatchedRowIds: Set<string> | null = null;
|
|
129
|
+
private pendingDataRefreshMode: 'patch' | 'virtual' | 'full' | null = null;
|
|
130
|
+
private lastScrollActivityAt = 0;
|
|
124
131
|
|
|
125
132
|
static get observedAttributes(): string[] {
|
|
126
133
|
return [
|
|
@@ -168,13 +175,16 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
168
175
|
}
|
|
169
176
|
|
|
170
177
|
get options(): GridOptions {
|
|
171
|
-
|
|
172
|
-
this.activeOptions
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
+
if (this.activeOptions !== null) {
|
|
179
|
+
return this.buildEffectiveOptions(this.activeOptions);
|
|
180
|
+
}
|
|
181
|
+
// No imperative options set yet: safe defaults with attributes taking priority.
|
|
182
|
+
return {
|
|
183
|
+
id: '__ui-grid-pending__',
|
|
184
|
+
data: [],
|
|
185
|
+
columnDefs: [],
|
|
186
|
+
...this.attributeOptions,
|
|
187
|
+
} as GridOptions;
|
|
178
188
|
}
|
|
179
189
|
|
|
180
190
|
set options(value: GridOptions) {
|
|
@@ -276,6 +286,10 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
276
286
|
if (enableColumnMoving !== undefined)
|
|
277
287
|
this.attributeOptions.enableColumnMoving = enableColumnMoving;
|
|
278
288
|
|
|
289
|
+
const enableColumnResizing = this.parseBooleanAttribute('enable-column-resizing');
|
|
290
|
+
if (enableColumnResizing !== undefined)
|
|
291
|
+
this.attributeOptions.enableColumnResizing = enableColumnResizing;
|
|
292
|
+
|
|
279
293
|
const enableCellEdit = this.parseBooleanAttribute('enable-cell-edit');
|
|
280
294
|
if (enableCellEdit !== undefined) this.attributeOptions.enableCellEdit = enableCellEdit;
|
|
281
295
|
|
|
@@ -347,8 +361,16 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
347
361
|
this.attributeOptions.paginationPageSizes = paginationPageSizes;
|
|
348
362
|
|
|
349
363
|
// Re-render with the merged options.
|
|
350
|
-
if (this.activeOptions) {
|
|
364
|
+
if (this.activeOptions !== null) {
|
|
351
365
|
this.ensureController(this.buildEffectiveOptions(this.activeOptions));
|
|
366
|
+
} else {
|
|
367
|
+
// Purely declarative path: attributes drive the grid without an imperative bridge.
|
|
368
|
+
this.ensureController({
|
|
369
|
+
id: '__ui-grid-pending__',
|
|
370
|
+
data: [],
|
|
371
|
+
columnDefs: [],
|
|
372
|
+
...this.attributeOptions,
|
|
373
|
+
} as VanillaGridOptions);
|
|
352
374
|
}
|
|
353
375
|
}
|
|
354
376
|
|
|
@@ -565,6 +587,14 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
565
587
|
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
566
588
|
}
|
|
567
589
|
|
|
590
|
+
get enableColumnResizing(): boolean {
|
|
591
|
+
return (this.options as any).enableColumnResizing ?? true;
|
|
592
|
+
}
|
|
593
|
+
set enableColumnResizing(value: boolean) {
|
|
594
|
+
this.activeOptions = { ...this.activeOptions, enableColumnResizing: value } as any;
|
|
595
|
+
this.ensureController(this.buildEffectiveOptions(this.activeOptions as VanillaGridOptions));
|
|
596
|
+
}
|
|
597
|
+
|
|
568
598
|
get enableCellEdit(): boolean {
|
|
569
599
|
return this.options.enableCellEdit ?? false;
|
|
570
600
|
}
|
|
@@ -707,22 +737,63 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
707
737
|
return;
|
|
708
738
|
}
|
|
709
739
|
|
|
740
|
+
const previousSnapshot = this.snapshot;
|
|
741
|
+
|
|
710
742
|
// Update the pipeline synchronously (cheap, no DOM work).
|
|
711
743
|
this.skipNextRender = true;
|
|
712
744
|
controller.refreshData(rows);
|
|
713
745
|
this.skipNextRender = false;
|
|
714
746
|
|
|
747
|
+
const { mode, changedRowIds } = this.classifyDataRefresh(previousSnapshot, this.snapshot);
|
|
748
|
+
if (mode === null) {
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
this.pendingDataRefreshMode = mode;
|
|
753
|
+
this.pendingPatchedRowIds = changedRowIds;
|
|
754
|
+
|
|
715
755
|
// Schedule DOM patching on the next paint — latest call wins.
|
|
716
756
|
if (this.dataFrame !== null) {
|
|
717
757
|
cancelAnimationFrame(this.dataFrame);
|
|
718
758
|
}
|
|
719
759
|
this.dataFrame = requestAnimationFrame(() => {
|
|
720
|
-
this.
|
|
721
|
-
this.patchCells();
|
|
760
|
+
this.flushPendingDataRefresh();
|
|
722
761
|
});
|
|
723
762
|
}
|
|
724
763
|
|
|
725
|
-
private
|
|
764
|
+
private flushPendingDataRefresh(): void {
|
|
765
|
+
this.dataFrame = null;
|
|
766
|
+
|
|
767
|
+
const mode = this.pendingDataRefreshMode;
|
|
768
|
+
if (mode === null) {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (Date.now() - this.lastScrollActivityAt < 80) {
|
|
773
|
+
this.dataFrame = requestAnimationFrame(() => {
|
|
774
|
+
this.flushPendingDataRefresh();
|
|
775
|
+
});
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const changedRowIds = this.pendingPatchedRowIds;
|
|
780
|
+
this.pendingDataRefreshMode = null;
|
|
781
|
+
this.pendingPatchedRowIds = null;
|
|
782
|
+
|
|
783
|
+
if (mode === 'patch') {
|
|
784
|
+
this.patchCells(changedRowIds ?? undefined);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (mode === 'virtual') {
|
|
789
|
+
this.renderVirtualBody();
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
this.render();
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
private patchCells(changedRowIds?: ReadonlySet<string>): void {
|
|
726
797
|
const snapshot = this.snapshot;
|
|
727
798
|
if (!snapshot) {
|
|
728
799
|
return;
|
|
@@ -735,12 +806,16 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
735
806
|
|
|
736
807
|
// Build O(1) lookup maps from the current pipeline snapshot.
|
|
737
808
|
const rowMap = new Map<string, GridRow>();
|
|
809
|
+
const rowIndexMap = new Map<string, number>();
|
|
738
810
|
for (const row of snapshot.pipeline.visibleRows) {
|
|
739
811
|
rowMap.set(row.id, row);
|
|
812
|
+
rowIndexMap.set(row.id, rowIndexMap.size);
|
|
740
813
|
}
|
|
741
814
|
const colMap = new Map<string, GridColumnDef>();
|
|
815
|
+
const templateMarkupMap = new Map<string, string | null>();
|
|
742
816
|
for (const col of snapshot.visibleColumns) {
|
|
743
817
|
colMap.set(col.name, col);
|
|
818
|
+
templateMarkupMap.set(col.name, this.getTemplateMarkup(this.cellSlotName(col)));
|
|
744
819
|
}
|
|
745
820
|
|
|
746
821
|
// Patch every rendered body cell in-place.
|
|
@@ -749,23 +824,24 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
749
824
|
const rowId = cell.dataset['row'];
|
|
750
825
|
const colName = cell.dataset['column'];
|
|
751
826
|
if (!rowId || !colName) continue;
|
|
827
|
+
if (changedRowIds && !changedRowIds.has(rowId)) continue;
|
|
752
828
|
|
|
753
829
|
const row = rowMap.get(rowId);
|
|
754
830
|
const column = colMap.get(colName);
|
|
755
831
|
if (!row || !column) continue;
|
|
832
|
+
if (cell.classList.contains('cell-editing')) continue;
|
|
756
833
|
|
|
757
|
-
const
|
|
758
|
-
if (!
|
|
834
|
+
const cellContent = cell.querySelector<HTMLElement>('.cell-content');
|
|
835
|
+
if (!cellContent) continue;
|
|
759
836
|
|
|
760
|
-
const newContent = this.
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
cellShell.innerHTML = toggleHtml + newContent;
|
|
837
|
+
const newContent = this.renderCellTemplateFromMarkup(
|
|
838
|
+
row,
|
|
839
|
+
column,
|
|
840
|
+
rowIndexMap.get(rowId) ?? 0,
|
|
841
|
+
templateMarkupMap.get(colName) ?? null,
|
|
842
|
+
);
|
|
843
|
+
if (cellContent.innerHTML !== newContent) {
|
|
844
|
+
cellContent.innerHTML = newContent;
|
|
769
845
|
}
|
|
770
846
|
}
|
|
771
847
|
}
|
|
@@ -1023,12 +1099,65 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1023
1099
|
}
|
|
1024
1100
|
});
|
|
1025
1101
|
|
|
1102
|
+
root.addEventListener('mousedown', (event) => {
|
|
1103
|
+
const target = event.target as HTMLElement | null;
|
|
1104
|
+
if (!target || !this.controller) return;
|
|
1105
|
+
|
|
1106
|
+
const resizer = target.closest<HTMLElement>('.column-resizer[data-column]');
|
|
1107
|
+
if (!resizer || !this.controller.canResizeColumns()) return;
|
|
1108
|
+
|
|
1109
|
+
const columnName = resizer.dataset['column'];
|
|
1110
|
+
if (!columnName) return;
|
|
1111
|
+
|
|
1112
|
+
const headerCell = resizer.closest<HTMLElement>('.header-cell');
|
|
1113
|
+
if (!headerCell) return;
|
|
1114
|
+
|
|
1115
|
+
event.preventDefault();
|
|
1116
|
+
event.stopPropagation();
|
|
1117
|
+
|
|
1118
|
+
const startX = (event as MouseEvent).clientX;
|
|
1119
|
+
const startWidth = headerCell.getBoundingClientRect().width;
|
|
1120
|
+
let lastWidth = startWidth;
|
|
1121
|
+
|
|
1122
|
+
const handleMove = (moveEvent: MouseEvent): void => {
|
|
1123
|
+
lastWidth = Math.max(88, startWidth + (moveEvent.clientX - startX));
|
|
1124
|
+
|
|
1125
|
+
// Write directly to DOM — skip full refresh while dragging.
|
|
1126
|
+
const newTemplate = this.controller!.buildTemplateColumnsWithOverride(columnName, lastWidth);
|
|
1127
|
+
const root = this.shadowRoot ?? this;
|
|
1128
|
+
(root as ShadowRoot | HTMLElement).querySelectorAll<HTMLElement>('.header-grid, .filter-grid, .body-grid').forEach((el) => {
|
|
1129
|
+
el.style.gridTemplateColumns = newTemplate;
|
|
1130
|
+
});
|
|
1131
|
+
};
|
|
1132
|
+
|
|
1133
|
+
const handleUp = (): void => {
|
|
1134
|
+
window.removeEventListener('mousemove', handleMove);
|
|
1135
|
+
window.removeEventListener('mouseup', handleUp);
|
|
1136
|
+
// Commit final width once — triggers one full refresh.
|
|
1137
|
+
this.controller!.setColumnWidthOverride(columnName, lastWidth);
|
|
1138
|
+
};
|
|
1139
|
+
|
|
1140
|
+
window.addEventListener('mousemove', handleMove);
|
|
1141
|
+
window.addEventListener('mouseup', handleUp);
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1026
1144
|
root.addEventListener('dblclick', (event) => {
|
|
1027
1145
|
const target = event.target as HTMLElement | null;
|
|
1028
1146
|
if (!target || !this.controller) {
|
|
1029
1147
|
return;
|
|
1030
1148
|
}
|
|
1031
1149
|
|
|
1150
|
+
const resizer = target.closest<HTMLElement>('.column-resizer[data-column]');
|
|
1151
|
+
if (resizer && this.controller.canResizeColumns()) {
|
|
1152
|
+
const columnName = resizer.dataset['column'];
|
|
1153
|
+
if (columnName) {
|
|
1154
|
+
event.preventDefault();
|
|
1155
|
+
event.stopPropagation();
|
|
1156
|
+
this.controller.setColumnWidthOverride(columnName, this.measureAutoColumnWidth(columnName));
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1032
1161
|
const cell = target.closest<HTMLElement>('.body-cell');
|
|
1033
1162
|
if (!cell) {
|
|
1034
1163
|
return;
|
|
@@ -1203,7 +1332,9 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1203
1332
|
return;
|
|
1204
1333
|
}
|
|
1205
1334
|
|
|
1335
|
+
this.lastScrollActivityAt = Date.now();
|
|
1206
1336
|
this.scrollPosition = target.scrollTop;
|
|
1337
|
+
this.horizontalScrollPosition = target.scrollLeft;
|
|
1207
1338
|
if (!this.snapshot?.pipeline.virtualizationEnabled) {
|
|
1208
1339
|
return;
|
|
1209
1340
|
}
|
|
@@ -1301,6 +1432,7 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1301
1432
|
const previousGridTable = root.querySelector<HTMLElement>('.grid-table');
|
|
1302
1433
|
if (previousGridTable && !this.suppressScrollEvent) {
|
|
1303
1434
|
this.scrollPosition = previousGridTable.scrollTop;
|
|
1435
|
+
this.horizontalScrollPosition = previousGridTable.scrollLeft;
|
|
1304
1436
|
}
|
|
1305
1437
|
|
|
1306
1438
|
if (!snapshot) {
|
|
@@ -1368,14 +1500,20 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1368
1500
|
: `<div class="empty-state ui-grid-no-row-overlay"><strong>${escapeHtml(options.emptyMessage ?? labels.emptyHeading)}</strong><p>${escapeHtml(labels.emptyDescription)}</p></div>`;
|
|
1369
1501
|
|
|
1370
1502
|
const pagination = paginationEnabled && showPagination ? this.renderPagination(snapshot) : '';
|
|
1371
|
-
const
|
|
1503
|
+
const hasViewportScroll = virtualizationEnabled || options.viewportHeight !== undefined;
|
|
1504
|
+
const gridTableStyle = `${hasViewportScroll ? `height:${viewportHeight}px;overflow-y:auto;` : ''}--ui-grid-header-sticky-top:${headerStickyTop}px;`;
|
|
1372
1505
|
|
|
1373
1506
|
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>`;
|
|
1374
1507
|
|
|
1375
1508
|
const gridTable = root.querySelector<HTMLElement>('.grid-table');
|
|
1376
|
-
if (gridTable && this.scrollPosition > 0
|
|
1509
|
+
if (gridTable && (this.scrollPosition > 0 || this.horizontalScrollPosition > 0)) {
|
|
1377
1510
|
this.suppressScrollEvent = true;
|
|
1378
|
-
|
|
1511
|
+
if (this.scrollPosition > 0) {
|
|
1512
|
+
gridTable.scrollTop = this.scrollPosition;
|
|
1513
|
+
}
|
|
1514
|
+
if (this.horizontalScrollPosition > 0) {
|
|
1515
|
+
gridTable.scrollLeft = this.horizontalScrollPosition;
|
|
1516
|
+
}
|
|
1379
1517
|
requestAnimationFrame(() => {
|
|
1380
1518
|
this.suppressScrollEvent = false;
|
|
1381
1519
|
});
|
|
@@ -1450,9 +1588,13 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1450
1588
|
const pinLabel = isPinned
|
|
1451
1589
|
? (this.snapshot?.labels.unpin ?? 'Unpin')
|
|
1452
1590
|
: (this.snapshot?.labels.pinColumn ?? 'Pin');
|
|
1591
|
+
const canResize = controller.canResizeColumns();
|
|
1453
1592
|
const headerValue = escapeHtml(formatGridHeaderDisplayValue(buildGridHeaderContext(column)));
|
|
1593
|
+
const resizerHtml = canResize
|
|
1594
|
+
? `<button type="button" class="column-resizer" data-action="resize" data-column="${escapeHtml(column.name)}" aria-label="Resize ${escapeHtml(headerValue)} column" title="Drag to resize, double-click to auto fit"></button>`
|
|
1595
|
+
: '';
|
|
1454
1596
|
|
|
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
|
|
1597
|
+
return `<div class="${classes}" data-column="${escapeHtml(column.name)}" ${draggable} style="${stickyStyle}"><span class="header-label">${headerValue}</span><span class="header-actions">${sortEnabled ? `<button type="button" class="header-action" data-action="sort" data-column="${escapeHtml(column.name)}" aria-label="${escapeHtml(sortLabel)}" ${canSort ? '' : 'disabled'}>${this.renderControlIcon(sortIconKey)}<span class="sr-only">${escapeHtml(sortLabel)}</span></button>` : ''}${groupingEnabled ? `<button type="button" class="chip-action${controller.isColumnGrouped(column) ? ' chip-action-active' : ''}" data-action="group" data-column="${escapeHtml(column.name)}" aria-label="${escapeHtml(groupingLabel)}" ${canGroup ? '' : 'disabled'}>${this.renderControlIcon('group')}<span class="sr-only">${escapeHtml(groupingLabel)}</span></button>` : ''}${canPin ? `<div class="pin-control${this.openPinMenuColumn === column.name ? ' pin-control-open' : ''}"><button type="button" class="chip-action pin-trigger${isPinned ? ' chip-action-active' : ''}" data-action="pin-trigger" data-column="${escapeHtml(column.name)}" aria-label="${escapeHtml(pinLabel)}">${this.renderControlIcon('pin')}<span class="sr-only">${escapeHtml(pinLabel)}</span></button><div class="pin-menu" role="menu" aria-label="${escapeHtml(pinLabel)}"><button type="button" class="pin-menu-action" data-action="pin-left" data-column="${escapeHtml(column.name)}" aria-label="${escapeHtml(this.snapshot?.labels.pinLeft ?? 'Pin left')}">${this.renderIconWithClass('control-icon', 'pinLeft')}<span class="sr-only">${escapeHtml(this.snapshot?.labels.pinLeft ?? 'Pin left')}</span></button><button type="button" class="pin-menu-action" data-action="pin-right" data-column="${escapeHtml(column.name)}" aria-label="${escapeHtml(this.snapshot?.labels.pinRight ?? 'Pin right')}">${this.renderIconWithClass('control-icon', 'pinRight')}<span class="sr-only">${escapeHtml(this.snapshot?.labels.pinRight ?? 'Pin right')}</span></button></div></div>` : ''}</span>${resizerHtml}</div>`;
|
|
1456
1598
|
}
|
|
1457
1599
|
|
|
1458
1600
|
private renderFilterCell(column: GridColumnDef): string {
|
|
@@ -1472,6 +1614,24 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1472
1614
|
return `<label class="${classes}" style="${stickyStyle}"><input class="ui-grid-filter-input" data-role="filter" data-column="${escapeHtml(column.name)}" placeholder="${escapeHtml(controller.filterPlaceholder(column))}" value="${escapeHtml(value)}" ${canFilter ? '' : 'disabled'}></label>`;
|
|
1473
1615
|
}
|
|
1474
1616
|
|
|
1617
|
+
private measureAutoColumnWidth(columnName: string): number {
|
|
1618
|
+
const root = this.shadowRoot;
|
|
1619
|
+
if (root == null) return 176;
|
|
1620
|
+
const escaped = CSS.escape ? CSS.escape(columnName) : columnName.replace(/([\\".#:[\](){}+~> ])/g, '\\$1');
|
|
1621
|
+
const selectors = [
|
|
1622
|
+
`.header-cell[data-column="${escaped}"]`,
|
|
1623
|
+
`.filter-cell[data-column="${escaped}"]`,
|
|
1624
|
+
`.body-cell[data-column="${escaped}"] .cell-shell`,
|
|
1625
|
+
];
|
|
1626
|
+
let maxWidth = 0;
|
|
1627
|
+
for (const selector of selectors) {
|
|
1628
|
+
for (const el of root.querySelectorAll<HTMLElement>(selector)) {
|
|
1629
|
+
maxWidth = Math.max(maxWidth, el.scrollWidth);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
return maxWidth + 12;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1475
1635
|
private renderDisplayItem(item: DisplayItem, displayIndex: number): string {
|
|
1476
1636
|
if (!this.snapshot || !this.controller) {
|
|
1477
1637
|
return '';
|
|
@@ -1537,7 +1697,7 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1537
1697
|
.join(' ');
|
|
1538
1698
|
|
|
1539
1699
|
const stickyStyle = pinOffset ? `${pinOffset.side}:${pinOffset.offset};` : '';
|
|
1540
|
-
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}
|
|
1700
|
+
return `<div class="${classes}" tabindex="0" data-row="${escapeHtml(rowId)}" data-column="${escapeHtml(columnName)}" style="${stickyStyle}"><div class="cell-shell" style="padding-inline-start:${escapeHtml(controller.cellIndent(row, column))}">${treeToggle}${expandToggle}<div class="cell-content">${content}</div></div></div>`;
|
|
1541
1701
|
}
|
|
1542
1702
|
|
|
1543
1703
|
private renderPagination(snapshot: GridControllerSnapshot): string {
|
|
@@ -1559,6 +1719,15 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1559
1719
|
|
|
1560
1720
|
private renderCellTemplate(row: GridRow, column: GridColumnDef, displayIndex: number): string {
|
|
1561
1721
|
const templateMarkup = this.getTemplateMarkup(this.cellSlotName(column));
|
|
1722
|
+
return this.renderCellTemplateFromMarkup(row, column, displayIndex, templateMarkup);
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
private renderCellTemplateFromMarkup(
|
|
1726
|
+
row: GridRow,
|
|
1727
|
+
column: GridColumnDef,
|
|
1728
|
+
displayIndex: number,
|
|
1729
|
+
templateMarkup: string | null,
|
|
1730
|
+
): string {
|
|
1562
1731
|
if (!templateMarkup) {
|
|
1563
1732
|
return `<span class="cell-value">${escapeHtml(this.controller?.displayValue(row, column) ?? '')}</span>`;
|
|
1564
1733
|
}
|
|
@@ -1624,6 +1793,101 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1624
1793
|
);
|
|
1625
1794
|
}
|
|
1626
1795
|
|
|
1796
|
+
private classifyDataRefresh(
|
|
1797
|
+
previousSnapshot: GridControllerSnapshot | null,
|
|
1798
|
+
nextSnapshot: GridControllerSnapshot | null,
|
|
1799
|
+
): { mode: 'patch' | 'virtual' | 'full' | null; changedRowIds: Set<string> | null } {
|
|
1800
|
+
if (!nextSnapshot) {
|
|
1801
|
+
return { mode: 'full', changedRowIds: null };
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
if (!previousSnapshot) {
|
|
1805
|
+
return {
|
|
1806
|
+
mode: nextSnapshot.pipeline.virtualizationEnabled ? 'virtual' : 'full',
|
|
1807
|
+
changedRowIds: null,
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
if (previousSnapshot.visibleColumns.length !== nextSnapshot.visibleColumns.length) {
|
|
1812
|
+
return { mode: 'full', changedRowIds: null };
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
for (let index = 0; index < previousSnapshot.visibleColumns.length; index += 1) {
|
|
1816
|
+
if (previousSnapshot.visibleColumns[index]?.name !== nextSnapshot.visibleColumns[index]?.name) {
|
|
1817
|
+
return { mode: 'full', changedRowIds: null };
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
const previousWindow = this.getRenderedDisplayWindow(previousSnapshot);
|
|
1822
|
+
const nextWindow = this.getRenderedDisplayWindow(nextSnapshot);
|
|
1823
|
+
if (
|
|
1824
|
+
previousWindow.startIndex !== nextWindow.startIndex ||
|
|
1825
|
+
previousWindow.items.length !== nextWindow.items.length
|
|
1826
|
+
) {
|
|
1827
|
+
return {
|
|
1828
|
+
mode: nextSnapshot.pipeline.virtualizationEnabled ? 'virtual' : 'full',
|
|
1829
|
+
changedRowIds: null,
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
const changedRowIds = new Set<string>();
|
|
1834
|
+
for (let index = 0; index < previousWindow.items.length; index += 1) {
|
|
1835
|
+
const previousItem = previousWindow.items[index];
|
|
1836
|
+
const nextItem = nextWindow.items[index];
|
|
1837
|
+
|
|
1838
|
+
if (!previousItem || !nextItem || previousItem.kind !== 'row' || nextItem.kind !== 'row') {
|
|
1839
|
+
return {
|
|
1840
|
+
mode: nextSnapshot.pipeline.virtualizationEnabled ? 'virtual' : 'full',
|
|
1841
|
+
changedRowIds: null,
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
if (previousItem.row.id !== nextItem.row.id) {
|
|
1846
|
+
return {
|
|
1847
|
+
mode: nextSnapshot.pipeline.virtualizationEnabled ? 'virtual' : 'full',
|
|
1848
|
+
changedRowIds: null,
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
if (previousItem.row.entity !== nextItem.row.entity) {
|
|
1853
|
+
changedRowIds.add(nextItem.row.id);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
return {
|
|
1858
|
+
mode: changedRowIds.size > 0 ? 'patch' : null,
|
|
1859
|
+
changedRowIds: changedRowIds.size > 0 ? changedRowIds : null,
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
private getRenderedDisplayWindow(snapshot: GridControllerSnapshot): {
|
|
1864
|
+
startIndex: number;
|
|
1865
|
+
items: readonly DisplayItem[];
|
|
1866
|
+
} {
|
|
1867
|
+
if (!snapshot.pipeline.virtualizationEnabled) {
|
|
1868
|
+
return {
|
|
1869
|
+
startIndex: 0,
|
|
1870
|
+
items: snapshot.pipeline.displayItems,
|
|
1871
|
+
};
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
const stickyChromeHeight = this.measuredHeaderStickyHeight + this.measuredFilterStickyHeight;
|
|
1875
|
+
const viewportHeight = snapshot.options.viewportHeight ?? 560;
|
|
1876
|
+
const bodyViewportHeight = Math.max(snapshot.rowSize, viewportHeight - stickyChromeHeight);
|
|
1877
|
+
const bodyScrollTop = Math.max(0, this.scrollPosition - stickyChromeHeight);
|
|
1878
|
+
const overscan = 4;
|
|
1879
|
+
const startIndex = Math.max(0, Math.floor(bodyScrollTop / snapshot.rowSize) - overscan);
|
|
1880
|
+
const visibleCount = Math.ceil(bodyViewportHeight / snapshot.rowSize) + overscan * 2;
|
|
1881
|
+
|
|
1882
|
+
return {
|
|
1883
|
+
startIndex,
|
|
1884
|
+
items: snapshot.pipeline.displayItems.slice(
|
|
1885
|
+
startIndex,
|
|
1886
|
+
Math.min(snapshot.pipeline.displayItems.length, startIndex + visibleCount),
|
|
1887
|
+
),
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1627
1891
|
private cellSlotName(column: GridColumnDef): string {
|
|
1628
1892
|
return `cell-${column.name}`;
|
|
1629
1893
|
}
|