@zengrid/core 1.2.2 → 1.3.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/dist/index.cjs.js +1873 -1095
- package/dist/index.esm.js +1936 -1161
- package/dist/package.json +2 -2
- package/dist/src/datetime-core/formatting/date-formatter.d.ts +2 -0
- package/dist/src/datetime-core/formatting/date-formatter.d.ts.map +1 -1
- package/dist/src/datetime-core/theming/datetime.css +463 -407
- package/dist/src/editing/checkbox/checkbox-dom.d.ts.map +1 -1
- package/dist/src/editing/datetime/date-editor/date-editor.d.ts.map +1 -1
- package/dist/src/editing/datetime/date-range-editor/date-range-calendar.d.ts.map +1 -1
- package/dist/src/editing/editor-manager/editor-manager.d.ts +23 -1
- package/dist/src/editing/editor-manager/editor-manager.d.ts.map +1 -1
- package/dist/src/editing/number/number-editor.d.ts.map +1 -1
- package/dist/src/editing/select/select-editor.d.ts.map +1 -1
- package/dist/src/features/column-drag/drag-mouse-handler.d.ts +1 -0
- package/dist/src/features/column-drag/drag-mouse-handler.d.ts.map +1 -1
- package/dist/src/features/copy-paste/clipboard-manager.d.ts +24 -2
- package/dist/src/features/copy-paste/clipboard-manager.d.ts.map +1 -1
- package/dist/src/features/loading/loading-indicator.d.ts.map +1 -1
- package/dist/src/features/loading/loading.styles.css +346 -342
- package/dist/src/features/selection/selection-manager.d.ts +14 -0
- package/dist/src/features/selection/selection-manager.d.ts.map +1 -1
- package/dist/src/grid/grid-core.d.ts.map +1 -1
- package/dist/src/grid/grid-setup.d.ts.map +1 -1
- package/dist/src/index.d.ts +6 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/plugins/clipboard/clipboard-plugin.d.ts +21 -0
- package/dist/src/plugins/clipboard/clipboard-plugin.d.ts.map +1 -0
- package/dist/src/plugins/clipboard/index.d.ts +3 -0
- package/dist/src/plugins/clipboard/index.d.ts.map +1 -0
- package/dist/src/plugins/column/column-plugin.d.ts.map +1 -1
- package/dist/src/plugins/dom/dom-plugin.d.ts.map +1 -1
- package/dist/src/plugins/editing/editing-plugin.d.ts.map +1 -1
- package/dist/src/plugins/filter-ui/field-state-utils.d.ts +6 -0
- package/dist/src/plugins/filter-ui/field-state-utils.d.ts.map +1 -0
- package/dist/src/plugins/filter-ui/filter-chip-bar.d.ts +16 -0
- package/dist/src/plugins/filter-ui/filter-chip-bar.d.ts.map +1 -0
- package/dist/src/plugins/filter-ui/filter-chip-descriptors.d.ts +4 -0
- package/dist/src/plugins/filter-ui/filter-chip-descriptors.d.ts.map +1 -0
- package/dist/src/plugins/filter-ui/filter-popup-dom.d.ts +32 -0
- package/dist/src/plugins/filter-ui/filter-popup-dom.d.ts.map +1 -0
- package/dist/src/plugins/filter-ui/filter-popup.d.ts +3 -0
- package/dist/src/plugins/filter-ui/filter-popup.d.ts.map +1 -0
- package/dist/src/plugins/filter-ui/filter-ui-operators.d.ts +9 -0
- package/dist/src/plugins/filter-ui/filter-ui-operators.d.ts.map +1 -0
- package/dist/src/plugins/filter-ui/filter-ui-types.d.ts +56 -0
- package/dist/src/plugins/filter-ui/filter-ui-types.d.ts.map +1 -0
- package/dist/src/plugins/filter-ui/grid-filter-ui.d.ts +20 -0
- package/dist/src/plugins/filter-ui/grid-filter-ui.d.ts.map +1 -0
- package/dist/src/plugins/filter-ui/index.d.ts +1 -0
- package/dist/src/plugins/filter-ui/index.d.ts.map +1 -1
- package/dist/src/plugins/header/header-dom-operations.d.ts +6 -0
- package/dist/src/plugins/header/header-dom-operations.d.ts.map +1 -1
- package/dist/src/plugins/header/header-plugin.d.ts.map +1 -1
- package/dist/src/plugins/header/header-registry-setup.d.ts.map +1 -1
- package/dist/src/plugins/header/header-rendering.d.ts.map +1 -1
- package/dist/src/plugins/header/header-types.d.ts +8 -1
- package/dist/src/plugins/header/header-types.d.ts.map +1 -1
- package/dist/src/plugins/index.d.ts +4 -0
- package/dist/src/plugins/index.d.ts.map +1 -1
- package/dist/src/plugins/keyboard/index.d.ts +3 -0
- package/dist/src/plugins/keyboard/index.d.ts.map +1 -0
- package/dist/src/plugins/keyboard/keyboard-nav-utils.d.ts +13 -0
- package/dist/src/plugins/keyboard/keyboard-nav-utils.d.ts.map +1 -0
- package/dist/src/plugins/keyboard/keyboard-plugin.d.ts +17 -0
- package/dist/src/plugins/keyboard/keyboard-plugin.d.ts.map +1 -0
- package/dist/src/plugins/lifecycle/lifecycle-plugin.d.ts +1 -0
- package/dist/src/plugins/lifecycle/lifecycle-plugin.d.ts.map +1 -1
- package/dist/src/plugins/pagination/pagination-plugin.d.ts.map +1 -1
- package/dist/src/plugins/rendering/rendering-plugin.d.ts.map +1 -1
- package/dist/src/plugins/selection/selection-plugin.d.ts.map +1 -1
- package/dist/src/reactive/store.d.ts +1 -0
- package/dist/src/reactive/store.d.ts.map +1 -1
- package/dist/src/reactive/types.d.ts +1 -0
- package/dist/src/reactive/types.d.ts.map +1 -1
- package/dist/src/rendering/cache/renderer-cache.interface.d.ts +6 -0
- package/dist/src/rendering/cache/renderer-cache.interface.d.ts.map +1 -1
- package/dist/src/rendering/cell-positioner/cell-positioner.d.ts +21 -0
- package/dist/src/rendering/cell-positioner/cell-positioner.d.ts.map +1 -1
- package/dist/src/rendering/headers/header-renderer.interface.d.ts +6 -0
- package/dist/src/rendering/headers/header-renderer.interface.d.ts.map +1 -1
- package/dist/src/rendering/headers/renderers/sortable-header-renderer.d.ts +4 -0
- package/dist/src/rendering/headers/renderers/sortable-header-renderer.d.ts.map +1 -1
- package/dist/src/rendering/renderers/dropdown/dropdown-dom.d.ts.map +1 -1
- package/dist/src/rendering/renderers/dropdown/dropdown-renderer.d.ts +2 -0
- package/dist/src/rendering/renderers/dropdown/dropdown-renderer.d.ts.map +1 -1
- package/dist/src/rendering/renderers/dropdown/dropdown-state.d.ts.map +1 -1
- package/dist/src/rendering/renderers/renderer.interface.d.ts +7 -0
- package/dist/src/rendering/renderers/renderer.interface.d.ts.map +1 -1
- package/dist/src/styles/a11y.css +20 -0
- package/dist/src/styles/calendar.css +291 -0
- package/dist/src/styles/cell-overflow.css +129 -0
- package/dist/src/styles/cell-renderers.css +90 -0
- package/dist/src/styles/date-picker.css +125 -0
- package/dist/src/styles/dropdown.css +156 -0
- package/dist/src/styles/filter.css +166 -0
- package/dist/src/styles/grid.css +91 -287
- package/dist/src/styles/header.css +60 -0
- package/dist/src/styles/overlays.css +44 -0
- package/dist/src/styles/tokens.css +86 -0
- package/dist/src/styles.css +24 -1112
- package/dist/src/types/grid.d.ts +32 -2
- package/dist/src/types/grid.d.ts.map +1 -1
- package/package.json +3 -3
- package/dist/src/grid/filter-ui.d.ts +0 -71
- package/dist/src/grid/filter-ui.d.ts.map +0 -1
package/dist/index.esm.js
CHANGED
|
@@ -1064,6 +1064,9 @@ function activatePhantom(key, realValue) {
|
|
|
1064
1064
|
phantomSubscribers.delete(key);
|
|
1065
1065
|
}
|
|
1066
1066
|
}
|
|
1067
|
+
function hasPlaceholder(key) {
|
|
1068
|
+
return placeholders.has(key);
|
|
1069
|
+
}
|
|
1067
1070
|
// --- Ownership helpers ---
|
|
1068
1071
|
function registerOwnership(owner, name) {
|
|
1069
1072
|
let set = ownershipMap.get(owner);
|
|
@@ -1204,12 +1207,19 @@ class CellPositioner {
|
|
|
1204
1207
|
constructor(options) {
|
|
1205
1208
|
this.lastRange = null;
|
|
1206
1209
|
this.renderedCells = new Map(); // key -> renderer name
|
|
1210
|
+
// The renderer instance actually applied to each pooled element. Keyed by the
|
|
1211
|
+
// element (not the cell key) so destroy() dispatches to the correct renderer
|
|
1212
|
+
// even for instance renderers, whose constructor.name is not in the registry.
|
|
1213
|
+
this.appliedRenderer = new WeakMap();
|
|
1207
1214
|
// Height measurement state (single RAF batch)
|
|
1208
1215
|
this.rafPending = false;
|
|
1209
1216
|
this.rowsToMeasure = new Map(); // row -> auto-height cells
|
|
1210
1217
|
this.heightCache = new Map(); // row -> measured height
|
|
1211
1218
|
// Reactive subscription
|
|
1212
1219
|
this.viewportSubscription = null;
|
|
1220
|
+
// Device-pixel-ratio (browser zoom) change tracking - re-snap edges on zoom
|
|
1221
|
+
this.dprQuery = null;
|
|
1222
|
+
this.dprListener = null;
|
|
1213
1223
|
this.scroller = options.scroller;
|
|
1214
1224
|
this.pool = options.pool;
|
|
1215
1225
|
this.registry = options.registry;
|
|
@@ -1221,6 +1231,31 @@ class CellPositioner {
|
|
|
1221
1231
|
this.isActive = options.isActive ?? (() => false);
|
|
1222
1232
|
this.isEditing = options.isEditing ?? (() => false);
|
|
1223
1233
|
this.rowHeightManager = options.rowHeightManager;
|
|
1234
|
+
this.setupDprListener();
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1237
|
+
* Re-render on device-pixel-ratio change (browser zoom) so cell edges re-snap to
|
|
1238
|
+
* whole device pixels. A `resolution` media query fires only when the DPR actually
|
|
1239
|
+
* changes - not on ordinary resize - and is bound to the current DPR, so it is
|
|
1240
|
+
* re-armed after every change.
|
|
1241
|
+
*/
|
|
1242
|
+
setupDprListener() {
|
|
1243
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
const arm = () => {
|
|
1247
|
+
if (this.dprQuery && this.dprListener) {
|
|
1248
|
+
this.dprQuery.removeEventListener('change', this.dprListener);
|
|
1249
|
+
}
|
|
1250
|
+
const dpr = window.devicePixelRatio || 1;
|
|
1251
|
+
this.dprQuery = window.matchMedia(`(resolution: ${dpr}dppx)`);
|
|
1252
|
+
this.dprListener = () => {
|
|
1253
|
+
arm();
|
|
1254
|
+
this.refresh();
|
|
1255
|
+
};
|
|
1256
|
+
this.dprQuery.addEventListener('change', this.dprListener);
|
|
1257
|
+
};
|
|
1258
|
+
arm();
|
|
1224
1259
|
}
|
|
1225
1260
|
/**
|
|
1226
1261
|
* Subscribe to viewport changes (reactive mode)
|
|
@@ -1293,12 +1328,29 @@ class CellPositioner {
|
|
|
1293
1328
|
}
|
|
1294
1329
|
return;
|
|
1295
1330
|
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1331
|
+
// The row/col count can shrink after lastRange was recorded (e.g. a backend
|
|
1332
|
+
// response with fewer rows). Clamp to current bounds so a stale out-of-range
|
|
1333
|
+
// index is never positioned (the height/width provider would throw), and
|
|
1334
|
+
// release the cells that fell outside so no ghost rows linger.
|
|
1335
|
+
const endRow = Math.min(this.lastRange.endRow, this.scroller.rowCount);
|
|
1336
|
+
const endCol = Math.min(this.lastRange.endCol, this.scroller.colCount);
|
|
1337
|
+
const startRow = Math.min(this.lastRange.startRow, endRow);
|
|
1338
|
+
const startCol = Math.min(this.lastRange.startCol, endCol);
|
|
1339
|
+
const activeKeys = new Set();
|
|
1340
|
+
for (let row = startRow; row < endRow; row++) {
|
|
1341
|
+
for (let col = startCol; col < endCol; col++) {
|
|
1298
1342
|
const key = this.getCellKey(row, col);
|
|
1343
|
+
activeKeys.add(key);
|
|
1299
1344
|
this.renderCell(row, col, key);
|
|
1300
1345
|
}
|
|
1301
1346
|
}
|
|
1347
|
+
this.pool.releaseExcept(activeKeys);
|
|
1348
|
+
for (const key of this.renderedCells.keys()) {
|
|
1349
|
+
if (!activeKeys.has(key)) {
|
|
1350
|
+
this.renderedCells.delete(key);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
this.lastRange = { startRow, endRow, startCol, endCol };
|
|
1302
1354
|
}
|
|
1303
1355
|
/**
|
|
1304
1356
|
* Clear rendered cells tracking so next renderVisibleCells re-renders all cells
|
|
@@ -1331,6 +1383,12 @@ class CellPositioner {
|
|
|
1331
1383
|
this.viewportSubscription();
|
|
1332
1384
|
this.viewportSubscription = null;
|
|
1333
1385
|
}
|
|
1386
|
+
// Remove device-pixel-ratio (zoom) listener
|
|
1387
|
+
if (this.dprQuery && this.dprListener) {
|
|
1388
|
+
this.dprQuery.removeEventListener('change', this.dprListener);
|
|
1389
|
+
this.dprQuery = null;
|
|
1390
|
+
this.dprListener = null;
|
|
1391
|
+
}
|
|
1334
1392
|
this.pool.clear();
|
|
1335
1393
|
this.renderedCells.clear();
|
|
1336
1394
|
this.lastRange = null;
|
|
@@ -1344,10 +1402,22 @@ class CellPositioner {
|
|
|
1344
1402
|
const value = this.getData(row, col);
|
|
1345
1403
|
const column = this.getColumn?.(col);
|
|
1346
1404
|
const rowData = this.getRowData?.(row);
|
|
1405
|
+
// Snap each cell EDGE (not its width) to a whole device pixel. The 1px inset
|
|
1406
|
+
// box-shadow that draws a cell border collapses to nothing when the edge lands on
|
|
1407
|
+
// a fractional device row - which is what happens at 100% zoom on fractional-DPR
|
|
1408
|
+
// displays (Windows 125%/150% scaling), causing borders to vanish "randomly".
|
|
1409
|
+
// Because offsets are prefix sums (offset(c) + width(c) === offset(c+1)), snapping
|
|
1410
|
+
// the left/right edges independently keeps neighbours exactly flush: no gaps or
|
|
1411
|
+
// overlaps, and it stays correct through column resizing and row-height changes.
|
|
1412
|
+
const dpr = (typeof window !== 'undefined' && window.devicePixelRatio) || 1;
|
|
1413
|
+
const left = Math.round(position.x * dpr) / dpr;
|
|
1414
|
+
const top = Math.round(position.y * dpr) / dpr;
|
|
1415
|
+
const width = Math.round((position.x + position.width) * dpr) / dpr - left;
|
|
1416
|
+
const height = Math.round((position.y + position.height) * dpr) / dpr - top;
|
|
1347
1417
|
// Position the cell using batched style updates (cssText) to prevent layout thrashing
|
|
1348
1418
|
// This is critical for scroll performance - individual style.property assignments
|
|
1349
1419
|
// can trigger multiple style recalculations, while cssText batches them
|
|
1350
|
-
element.style.cssText = `position: absolute; left: ${
|
|
1420
|
+
element.style.cssText = `position: absolute; left: ${left}px; top: ${top}px; width: ${width}px; height: ${height}px;`;
|
|
1351
1421
|
// For auto-height columns being measured, allow the cell to expand beyond the set height
|
|
1352
1422
|
// This is only for the MEASUREMENT phase - after measurement, all cells use the measured height
|
|
1353
1423
|
const needsMeasurement = this.rowHeightManager?.needsMeasurement(row);
|
|
@@ -1355,10 +1425,11 @@ class CellPositioner {
|
|
|
1355
1425
|
// Override height constraint for measurement - let content expand
|
|
1356
1426
|
// The browser will naturally reflow, and we'll measure in the next frame via RAF
|
|
1357
1427
|
element.style.height = 'auto';
|
|
1358
|
-
element.style.minHeight = `${
|
|
1428
|
+
element.style.minHeight = `${height}px`;
|
|
1359
1429
|
}
|
|
1360
1430
|
element.setAttribute('data-row', row.toString());
|
|
1361
1431
|
element.setAttribute('data-col', col.toString());
|
|
1432
|
+
element.classList.toggle('zg-cell-last-col', col === this.scroller.colCount - 1);
|
|
1362
1433
|
// Get renderer - handle both string names and renderer instances
|
|
1363
1434
|
let renderer;
|
|
1364
1435
|
let rendererName;
|
|
@@ -1390,8 +1461,9 @@ class CellPositioner {
|
|
|
1390
1461
|
isActive: this.isActive(row, col),
|
|
1391
1462
|
isEditing: this.isEditing(row, col),
|
|
1392
1463
|
};
|
|
1393
|
-
// Generate cache key
|
|
1394
|
-
|
|
1464
|
+
// Generate cache key (skip HTML-string caching for interactive renderers,
|
|
1465
|
+
// which lose event listeners / body-portaled DOM when restored via innerHTML)
|
|
1466
|
+
const cacheKey = this.cache && renderer.cacheable !== false
|
|
1395
1467
|
? RendererCache.generateKey(row, col, value, rendererName, {
|
|
1396
1468
|
isSelected: params.isSelected,
|
|
1397
1469
|
isActive: params.isActive,
|
|
@@ -1402,13 +1474,27 @@ class CellPositioner {
|
|
|
1402
1474
|
if (cacheKey && this.cache) {
|
|
1403
1475
|
const cached = this.cache.get(cacheKey);
|
|
1404
1476
|
if (cached) {
|
|
1477
|
+
// Renderer changed for this recycled element: destroy the previous one so
|
|
1478
|
+
// its classes (alignment via zg-cell-text/zg-cell-number, or padding-0
|
|
1479
|
+
// containers like zg-cell-checkbox-container) are stripped before we apply
|
|
1480
|
+
// the cached content's classes additively.
|
|
1481
|
+
const prevRenderer = this.appliedRenderer.get(element);
|
|
1482
|
+
if (prevRenderer && prevRenderer !== renderer) {
|
|
1483
|
+
prevRenderer.destroy(element);
|
|
1484
|
+
}
|
|
1405
1485
|
// Use cached content
|
|
1406
1486
|
element.innerHTML = cached.html;
|
|
1407
1487
|
// Apply cached classes
|
|
1408
1488
|
if (cached.classes) {
|
|
1409
1489
|
cached.classes.forEach((cls) => element.classList.add(cls));
|
|
1410
1490
|
}
|
|
1491
|
+
// Re-apply inline styles the renderer set on the cell (the cssText
|
|
1492
|
+
// reset above wiped them, and innerHTML does not carry them).
|
|
1493
|
+
if (cached.styles) {
|
|
1494
|
+
cached.styles.forEach(([prop, val]) => element.style.setProperty(prop, val));
|
|
1495
|
+
}
|
|
1411
1496
|
this.renderedCells.set(key, rendererName);
|
|
1497
|
+
this.appliedRenderer.set(element, renderer);
|
|
1412
1498
|
// Apply state classes
|
|
1413
1499
|
element.classList.toggle('zg-cell-selected', params.isSelected);
|
|
1414
1500
|
element.classList.toggle('zg-cell-active', params.isActive);
|
|
@@ -1421,23 +1507,31 @@ class CellPositioner {
|
|
|
1421
1507
|
return;
|
|
1422
1508
|
}
|
|
1423
1509
|
}
|
|
1424
|
-
// Render or update (cache miss or no cache)
|
|
1425
|
-
|
|
1426
|
-
//
|
|
1510
|
+
// Render or update (cache miss or no cache). The render/update decision is
|
|
1511
|
+
// keyed per cell: the pool wipes an element's innerHTML on release, so a
|
|
1512
|
+
// recycled element must be re-rendered even when the renderer is unchanged.
|
|
1513
|
+
const lastRendererName = this.renderedCells.get(key);
|
|
1514
|
+
// Capture classes/styles before render for delta calculation.
|
|
1515
|
+
// Style snapshot is only needed when the result will be cached.
|
|
1427
1516
|
const classesBefore = new Set(Array.from(element.classList));
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1517
|
+
const stylesBefore = cacheKey ? this.snapshotStyles(element) : null;
|
|
1518
|
+
const didRender = lastRendererName !== rendererName || !lastRendererName;
|
|
1519
|
+
if (didRender) {
|
|
1520
|
+
// Renderer changed or first render - destroy the renderer actually applied
|
|
1521
|
+
// to this element (tracked by instance, so instance renderers dispatch to
|
|
1522
|
+
// the correct destroy) before rendering the new one.
|
|
1523
|
+
const prevRenderer = this.appliedRenderer.get(element);
|
|
1524
|
+
if (prevRenderer && prevRenderer !== renderer) {
|
|
1525
|
+
prevRenderer.destroy(element);
|
|
1433
1526
|
}
|
|
1434
1527
|
renderer.render(element, params);
|
|
1435
|
-
this.renderedCells.set(key, rendererName);
|
|
1436
1528
|
}
|
|
1437
1529
|
else {
|
|
1438
1530
|
// Same renderer - just update
|
|
1439
1531
|
renderer.update(element, params);
|
|
1440
1532
|
}
|
|
1533
|
+
this.renderedCells.set(key, rendererName);
|
|
1534
|
+
this.appliedRenderer.set(element, renderer);
|
|
1441
1535
|
// Apply optional renderer class (before caching so it's included)
|
|
1442
1536
|
if (renderer.getCellClass) {
|
|
1443
1537
|
const rendererClass = renderer.getCellClass(params);
|
|
@@ -1445,8 +1539,14 @@ class CellPositioner {
|
|
|
1445
1539
|
element.classList.add(rendererClass);
|
|
1446
1540
|
}
|
|
1447
1541
|
}
|
|
1448
|
-
// Cache the rendered content with all classes added by renderer
|
|
1449
|
-
|
|
1542
|
+
// Cache the rendered content with all classes added by renderer.
|
|
1543
|
+
// Only cache from the render branch: there classesBefore/stylesBefore are a
|
|
1544
|
+
// clean baseline (pool reset + structural classes), so the delta captures
|
|
1545
|
+
// every renderer-owned class/style. In the update branch the element still
|
|
1546
|
+
// holds the renderer's classes from its prior render, so the delta would
|
|
1547
|
+
// omit them (e.g. zg-cell-number) and a later cache-hit on a freshly-pooled
|
|
1548
|
+
// element would drop that styling.
|
|
1549
|
+
if (cacheKey && this.cache && didRender) {
|
|
1450
1550
|
// Calculate delta: classes added during render
|
|
1451
1551
|
const addedClasses = [];
|
|
1452
1552
|
element.classList.forEach((cls) => {
|
|
@@ -1454,9 +1554,11 @@ class CellPositioner {
|
|
|
1454
1554
|
addedClasses.push(cls);
|
|
1455
1555
|
}
|
|
1456
1556
|
});
|
|
1557
|
+
const addedStyles = stylesBefore ? this.styleDelta(stylesBefore, element) : undefined;
|
|
1457
1558
|
this.cache.set(cacheKey, {
|
|
1458
1559
|
html: element.innerHTML,
|
|
1459
1560
|
classes: addedClasses.length > 0 ? addedClasses : undefined,
|
|
1561
|
+
styles: addedStyles,
|
|
1460
1562
|
});
|
|
1461
1563
|
}
|
|
1462
1564
|
// Apply state classes
|
|
@@ -1584,6 +1686,36 @@ class CellPositioner {
|
|
|
1584
1686
|
// Add the configured overflow class
|
|
1585
1687
|
element.classList.add(`zg-cell-overflow-${column.overflow.mode}`);
|
|
1586
1688
|
}
|
|
1689
|
+
/**
|
|
1690
|
+
* Snapshot the inline style properties currently set on the cell element.
|
|
1691
|
+
* Used to diff against the post-render state so renderer-applied styles can
|
|
1692
|
+
* be cached and restored on cache hits.
|
|
1693
|
+
*/
|
|
1694
|
+
snapshotStyles(element) {
|
|
1695
|
+
const style = element.style;
|
|
1696
|
+
const snapshot = new Map();
|
|
1697
|
+
for (let i = 0; i < style.length; i++) {
|
|
1698
|
+
const prop = style[i];
|
|
1699
|
+
snapshot.set(prop, style.getPropertyValue(prop));
|
|
1700
|
+
}
|
|
1701
|
+
return snapshot;
|
|
1702
|
+
}
|
|
1703
|
+
/**
|
|
1704
|
+
* Compute the inline styles added or changed since {@link snapshotStyles},
|
|
1705
|
+
* i.e. the styles the renderer set on the cell element.
|
|
1706
|
+
*/
|
|
1707
|
+
styleDelta(before, element) {
|
|
1708
|
+
const style = element.style;
|
|
1709
|
+
const delta = [];
|
|
1710
|
+
for (let i = 0; i < style.length; i++) {
|
|
1711
|
+
const prop = style[i];
|
|
1712
|
+
const val = style.getPropertyValue(prop);
|
|
1713
|
+
if (before.get(prop) !== val) {
|
|
1714
|
+
delta.push([prop, val]);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
return delta.length > 0 ? delta : undefined;
|
|
1718
|
+
}
|
|
1587
1719
|
getCellKey(row, col) {
|
|
1588
1720
|
return `${row}-${col}`;
|
|
1589
1721
|
}
|
|
@@ -3006,6 +3138,8 @@ function isValidDate(date) {
|
|
|
3006
3138
|
* Supported tokens:
|
|
3007
3139
|
* - YYYY: 4-digit year
|
|
3008
3140
|
* - YY: 2-digit year
|
|
3141
|
+
* - MMMM: Full month name (January-December)
|
|
3142
|
+
* - MMM: Short month name (Jan-Dec)
|
|
3009
3143
|
* - MM: 2-digit month (01-12)
|
|
3010
3144
|
* - M: Month without leading zero (1-12)
|
|
3011
3145
|
* - DD: 2-digit day (01-31)
|
|
@@ -3034,24 +3168,39 @@ function formatDate(date, format) {
|
|
|
3034
3168
|
const hours12 = hours % 12 || 12;
|
|
3035
3169
|
const ampm = hours >= 12 ? 'PM' : 'AM';
|
|
3036
3170
|
const pad = (n) => String(n).padStart(2, '0');
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3171
|
+
// Single-pass tokenizer: alternation is ordered longest-token-first so
|
|
3172
|
+
// multi-char tokens (YYYY/MMMM/MMM/MM) match before their shorter prefixes,
|
|
3173
|
+
// and each token is substituted exactly once — literal text is left intact.
|
|
3174
|
+
const tokens = {
|
|
3175
|
+
YYYY: String(year),
|
|
3176
|
+
YY: String(year).slice(-2),
|
|
3177
|
+
MMMM: MONTH_NAMES[month - 1],
|
|
3178
|
+
MMM: MONTH_NAMES_SHORT[month - 1],
|
|
3179
|
+
MM: pad(month),
|
|
3180
|
+
M: String(month),
|
|
3181
|
+
DD: pad(day),
|
|
3182
|
+
D: String(day),
|
|
3183
|
+
HH: pad(hours),
|
|
3184
|
+
H: String(hours),
|
|
3185
|
+
hh: pad(hours12),
|
|
3186
|
+
h: String(hours12),
|
|
3187
|
+
mm: pad(minutes),
|
|
3188
|
+
m: String(minutes),
|
|
3189
|
+
ss: pad(seconds),
|
|
3190
|
+
s: String(seconds),
|
|
3191
|
+
A: ampm,
|
|
3192
|
+
a: ampm.toLowerCase(),
|
|
3193
|
+
};
|
|
3194
|
+
return format.replace(/YYYY|YY|MMMM|MMM|MM|M|DD|D|HH|H|hh|h|mm|m|ss|s|A|a/g, (token) => tokens[token]);
|
|
3054
3195
|
}
|
|
3196
|
+
const MONTH_NAMES = [
|
|
3197
|
+
'January', 'February', 'March', 'April', 'May', 'June',
|
|
3198
|
+
'July', 'August', 'September', 'October', 'November', 'December',
|
|
3199
|
+
];
|
|
3200
|
+
const MONTH_NAMES_SHORT = [
|
|
3201
|
+
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
|
3202
|
+
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec',
|
|
3203
|
+
];
|
|
3055
3204
|
/**
|
|
3056
3205
|
* Format a date for display (common formats)
|
|
3057
3206
|
*/
|
|
@@ -4779,6 +4928,13 @@ class TextHeaderRenderer {
|
|
|
4779
4928
|
* ```
|
|
4780
4929
|
*/
|
|
4781
4930
|
class SortableHeaderRenderer extends TextHeaderRenderer {
|
|
4931
|
+
constructor() {
|
|
4932
|
+
super(...arguments);
|
|
4933
|
+
/**
|
|
4934
|
+
* This renderer wires its own sort-click handler; skip the grid's fallback.
|
|
4935
|
+
*/
|
|
4936
|
+
this.handlesSortInteraction = true;
|
|
4937
|
+
}
|
|
4782
4938
|
/**
|
|
4783
4939
|
* Initial render of sortable header
|
|
4784
4940
|
*/
|
|
@@ -4826,19 +4982,19 @@ class SortableHeaderRenderer extends TextHeaderRenderer {
|
|
|
4826
4982
|
// Create sort indicator container
|
|
4827
4983
|
const sortEl = document.createElement('span');
|
|
4828
4984
|
sortEl.className = 'zg-sort-indicator';
|
|
4829
|
-
sortEl.style.cssText = `
|
|
4830
|
-
display: inline-flex;
|
|
4831
|
-
flex-direction: column;
|
|
4832
|
-
align-items: center;
|
|
4833
|
-
justify-content: center;
|
|
4834
|
-
margin-left: 2px;
|
|
4835
|
-
font-size: 9px;
|
|
4836
|
-
line-height: 1;
|
|
4837
|
-
flex-shrink: 0;
|
|
4838
|
-
width: 14px;
|
|
4839
|
-
min-width: 14px;
|
|
4840
|
-
max-width: 14px;
|
|
4841
|
-
overflow: visible;
|
|
4985
|
+
sortEl.style.cssText = `
|
|
4986
|
+
display: inline-flex;
|
|
4987
|
+
flex-direction: column;
|
|
4988
|
+
align-items: center;
|
|
4989
|
+
justify-content: center;
|
|
4990
|
+
margin-left: 2px;
|
|
4991
|
+
font-size: 9px;
|
|
4992
|
+
line-height: 1;
|
|
4993
|
+
flex-shrink: 0;
|
|
4994
|
+
width: 14px;
|
|
4995
|
+
min-width: 14px;
|
|
4996
|
+
max-width: 14px;
|
|
4997
|
+
overflow: visible;
|
|
4842
4998
|
`;
|
|
4843
4999
|
// Update sort icon based on current state
|
|
4844
5000
|
this.updateSortIcon(sortEl, params);
|
|
@@ -4868,9 +5024,9 @@ class SortableHeaderRenderer extends TextHeaderRenderer {
|
|
|
4868
5024
|
const { sortDirection, sortPriority } = params;
|
|
4869
5025
|
if (!sortDirection) {
|
|
4870
5026
|
// Not sorted - show both arrows in muted state
|
|
4871
|
-
sortEl.innerHTML = `
|
|
4872
|
-
<span style="opacity: 0.3;">${sortIndicator?.ascIcon || SortableHeaderRenderer.DEFAULT_ASC_ICON}</span>
|
|
4873
|
-
<span style="opacity: 0.3; margin-top: -2px;">${sortIndicator?.descIcon || SortableHeaderRenderer.DEFAULT_DESC_ICON}</span>
|
|
5027
|
+
sortEl.innerHTML = `
|
|
5028
|
+
<span style="opacity: 0.3;">${sortIndicator?.ascIcon || SortableHeaderRenderer.DEFAULT_ASC_ICON}</span>
|
|
5029
|
+
<span style="opacity: 0.3; margin-top: -2px;">${sortIndicator?.descIcon || SortableHeaderRenderer.DEFAULT_DESC_ICON}</span>
|
|
4874
5030
|
`;
|
|
4875
5031
|
sortEl.style.opacity = '0.5';
|
|
4876
5032
|
}
|
|
@@ -4891,19 +5047,19 @@ class SortableHeaderRenderer extends TextHeaderRenderer {
|
|
|
4891
5047
|
const priorityBadge = document.createElement('span');
|
|
4892
5048
|
priorityBadge.className = 'zg-sort-priority';
|
|
4893
5049
|
priorityBadge.textContent = String(sortPriority + 1);
|
|
4894
|
-
priorityBadge.style.cssText = `
|
|
4895
|
-
position: absolute;
|
|
4896
|
-
top: 2px;
|
|
4897
|
-
right: 2px;
|
|
4898
|
-
font-size: 8px;
|
|
4899
|
-
background: var(--zg-primary-color, #0066cc);
|
|
4900
|
-
color: white;
|
|
4901
|
-
border-radius: 50%;
|
|
4902
|
-
width: 12px;
|
|
4903
|
-
height: 12px;
|
|
4904
|
-
display: flex;
|
|
4905
|
-
align-items: center;
|
|
4906
|
-
justify-content: center;
|
|
5050
|
+
priorityBadge.style.cssText = `
|
|
5051
|
+
position: absolute;
|
|
5052
|
+
top: 2px;
|
|
5053
|
+
right: 2px;
|
|
5054
|
+
font-size: 8px;
|
|
5055
|
+
background: var(--zg-primary-color, #0066cc);
|
|
5056
|
+
color: white;
|
|
5057
|
+
border-radius: 50%;
|
|
5058
|
+
width: 12px;
|
|
5059
|
+
height: 12px;
|
|
5060
|
+
display: flex;
|
|
5061
|
+
align-items: center;
|
|
5062
|
+
justify-content: center;
|
|
4907
5063
|
`;
|
|
4908
5064
|
sortEl.style.position = 'relative';
|
|
4909
5065
|
sortEl.appendChild(priorityBadge);
|
|
@@ -8999,6 +9155,10 @@ class DragMouseHandler {
|
|
|
8999
9155
|
constructor(options, callbacks, throttledDrag) {
|
|
9000
9156
|
// Container reference
|
|
9001
9157
|
this.container = null;
|
|
9158
|
+
// Source header cell captured at mousedown. Used to build the drag ghost so it
|
|
9159
|
+
// never depends on the transient mousemove target, which may have moved off the
|
|
9160
|
+
// header (onto a data cell or gap) by the time the drag threshold is crossed.
|
|
9161
|
+
this.sourceHeaderCell = null;
|
|
9002
9162
|
this.enabled = options.enabled;
|
|
9003
9163
|
this.dragThreshold = options.dragThreshold;
|
|
9004
9164
|
this.onBeforeDrag = options.onBeforeDrag;
|
|
@@ -9027,6 +9187,7 @@ class DragMouseHandler {
|
|
|
9027
9187
|
this.container = null;
|
|
9028
9188
|
}
|
|
9029
9189
|
this.removeGlobalListeners();
|
|
9190
|
+
this.sourceHeaderCell = null;
|
|
9030
9191
|
}
|
|
9031
9192
|
/**
|
|
9032
9193
|
* Handle mousedown event on header
|
|
@@ -9041,6 +9202,7 @@ class DragMouseHandler {
|
|
|
9041
9202
|
const columnId = headerCell.dataset['columnId'];
|
|
9042
9203
|
if (!columnId)
|
|
9043
9204
|
return;
|
|
9205
|
+
this.sourceHeaderCell = headerCell;
|
|
9044
9206
|
// Check if column can be dragged
|
|
9045
9207
|
if (!this.callbacks.canDragColumn(columnId))
|
|
9046
9208
|
return;
|
|
@@ -9084,7 +9246,7 @@ class DragMouseHandler {
|
|
|
9084
9246
|
if (state === 'pending') {
|
|
9085
9247
|
const distance = this.callbacks.getDragDistance();
|
|
9086
9248
|
if (distance >= this.dragThreshold) {
|
|
9087
|
-
this.callbacks.onDragStart(e, e.target);
|
|
9249
|
+
this.callbacks.onDragStart(e, this.sourceHeaderCell ?? e.target);
|
|
9088
9250
|
}
|
|
9089
9251
|
}
|
|
9090
9252
|
// Perform drag if in dragging state
|
|
@@ -9103,6 +9265,7 @@ class DragMouseHandler {
|
|
|
9103
9265
|
this.callbacks.onMouseUp();
|
|
9104
9266
|
// Remove global listeners
|
|
9105
9267
|
this.removeGlobalListeners();
|
|
9268
|
+
this.sourceHeaderCell = null;
|
|
9106
9269
|
}
|
|
9107
9270
|
/**
|
|
9108
9271
|
* Remove global mouse listeners
|
|
@@ -11342,6 +11505,14 @@ class GridStoreImpl {
|
|
|
11342
11505
|
}
|
|
11343
11506
|
const wrapped = createComputed(key, fn, owner, phase);
|
|
11344
11507
|
this.computeds.set(key, wrapped);
|
|
11508
|
+
// If something read this key before it existed, it subscribed to a phantom
|
|
11509
|
+
// placeholder. Activate it so those dependents re-run and re-subscribe to
|
|
11510
|
+
// the real computed — otherwise effects that read a computed key early
|
|
11511
|
+
// (e.g. the rendering sync effect reading rows.viewIndices before
|
|
11512
|
+
// setupCoreComputeds) stay bound to a dead placeholder and never fire.
|
|
11513
|
+
if (hasPlaceholder(key)) {
|
|
11514
|
+
activatePhantom(key, wrapped.value);
|
|
11515
|
+
}
|
|
11345
11516
|
}
|
|
11346
11517
|
effect(name, fn, owner, phase = 0) {
|
|
11347
11518
|
if (this.effects.has(name)) {
|
|
@@ -11407,6 +11578,9 @@ class GridStoreImpl {
|
|
|
11407
11578
|
this.actions.set(name, { handler, owner, invalidates: meta?.invalidates });
|
|
11408
11579
|
registerOwnership(owner, name);
|
|
11409
11580
|
}
|
|
11581
|
+
hasAction(name) {
|
|
11582
|
+
return this.actions.has(name);
|
|
11583
|
+
}
|
|
11410
11584
|
exec(name, ...args) {
|
|
11411
11585
|
const reg = this.actions.get(name);
|
|
11412
11586
|
if (!reg) {
|
|
@@ -12314,7 +12488,7 @@ function projectFilterAstToFieldState(ast) {
|
|
|
12314
12488
|
const root = astGroupToFieldGroup(ast.root, warnings);
|
|
12315
12489
|
const activeFields = new Set();
|
|
12316
12490
|
if (root) {
|
|
12317
|
-
collectActiveFields(root, activeFields);
|
|
12491
|
+
collectActiveFields$1(root, activeFields);
|
|
12318
12492
|
}
|
|
12319
12493
|
return {
|
|
12320
12494
|
state: {
|
|
@@ -12546,12 +12720,12 @@ function astGroupToFieldGroup(group, warnings) {
|
|
|
12546
12720
|
groups: groups.length > 0 ? groups : undefined,
|
|
12547
12721
|
};
|
|
12548
12722
|
}
|
|
12549
|
-
function collectActiveFields(group, activeFields) {
|
|
12723
|
+
function collectActiveFields$1(group, activeFields) {
|
|
12550
12724
|
for (const condition of group.conditions) {
|
|
12551
12725
|
activeFields.add(condition.field);
|
|
12552
12726
|
}
|
|
12553
12727
|
for (const nested of group.groups ?? []) {
|
|
12554
|
-
collectActiveFields(nested, activeFields);
|
|
12728
|
+
collectActiveFields$1(nested, activeFields);
|
|
12555
12729
|
}
|
|
12556
12730
|
}
|
|
12557
12731
|
function normalizeConditionValue(operator, value) {
|
|
@@ -16576,6 +16750,30 @@ class SelectionManager {
|
|
|
16576
16750
|
this.addSelection(selection);
|
|
16577
16751
|
this.emitSelectionChange();
|
|
16578
16752
|
}
|
|
16753
|
+
/**
|
|
16754
|
+
* Toggle a single cell in a multi-selection: remove it if that exact 1×1
|
|
16755
|
+
* cell is already selected, otherwise add it. Used for additive Ctrl/Cmd+click.
|
|
16756
|
+
* Returns the resulting selected state of the cell.
|
|
16757
|
+
*/
|
|
16758
|
+
toggleCell(row, col) {
|
|
16759
|
+
const existing = this.findCellSelection(row, col);
|
|
16760
|
+
if (existing) {
|
|
16761
|
+
this.selections.delete(existing.id);
|
|
16762
|
+
this.rebuildIntervals();
|
|
16763
|
+
this.emitSelectionChange();
|
|
16764
|
+
return false;
|
|
16765
|
+
}
|
|
16766
|
+
this.addSelection({
|
|
16767
|
+
id: this.generateId(),
|
|
16768
|
+
type: 'cell',
|
|
16769
|
+
startRow: row,
|
|
16770
|
+
endRow: row,
|
|
16771
|
+
startCol: col,
|
|
16772
|
+
endCol: col,
|
|
16773
|
+
});
|
|
16774
|
+
this.emitSelectionChange();
|
|
16775
|
+
return true;
|
|
16776
|
+
}
|
|
16579
16777
|
/**
|
|
16580
16778
|
* Select a row range
|
|
16581
16779
|
*/
|
|
@@ -16761,6 +16959,32 @@ class SelectionManager {
|
|
|
16761
16959
|
// Add to column interval tree
|
|
16762
16960
|
this.colIntervals.insert({ start: selection.startCol, end: selection.endCol }, selection);
|
|
16763
16961
|
}
|
|
16962
|
+
/**
|
|
16963
|
+
* Find an exact 1×1 cell selection at the given coordinates, if one exists.
|
|
16964
|
+
*/
|
|
16965
|
+
findCellSelection(row, col) {
|
|
16966
|
+
for (const sel of this.selections.values()) {
|
|
16967
|
+
if (sel.type === 'cell' &&
|
|
16968
|
+
sel.startRow === row &&
|
|
16969
|
+
sel.endRow === row &&
|
|
16970
|
+
sel.startCol === col &&
|
|
16971
|
+
sel.endCol === col) {
|
|
16972
|
+
return sel;
|
|
16973
|
+
}
|
|
16974
|
+
}
|
|
16975
|
+
return undefined;
|
|
16976
|
+
}
|
|
16977
|
+
/**
|
|
16978
|
+
* Rebuild the interval trees from the current selection map (after a removal).
|
|
16979
|
+
*/
|
|
16980
|
+
rebuildIntervals() {
|
|
16981
|
+
this.rowIntervals = new IntervalTree({ balanced: true });
|
|
16982
|
+
this.colIntervals = new IntervalTree({ balanced: true });
|
|
16983
|
+
for (const sel of this.selections.values()) {
|
|
16984
|
+
this.rowIntervals.insert({ start: sel.startRow, end: sel.endRow }, sel);
|
|
16985
|
+
this.colIntervals.insert({ start: sel.startCol, end: sel.endCol }, sel);
|
|
16986
|
+
}
|
|
16987
|
+
}
|
|
16764
16988
|
/**
|
|
16765
16989
|
* Generate unique ID for selections
|
|
16766
16990
|
*/
|
|
@@ -16849,6 +17073,11 @@ function createSelectionPlugin(options) {
|
|
|
16849
17073
|
store.set('selection.active', { row, col });
|
|
16850
17074
|
syncToStore();
|
|
16851
17075
|
}, 'selection');
|
|
17076
|
+
store.action('selection:toggleCell', (row, col) => {
|
|
17077
|
+
const selected = mgr.toggleCell(row, col);
|
|
17078
|
+
store.set('selection.active', selected ? { row, col } : null);
|
|
17079
|
+
syncToStore();
|
|
17080
|
+
}, 'selection');
|
|
16852
17081
|
store.action('selection:selectRange', (startRow, startCol, endRow, endCol, additive = false) => {
|
|
16853
17082
|
mgr.selectRange(startRow, startCol, endRow, endCol, additive);
|
|
16854
17083
|
syncToStore();
|
|
@@ -16902,6 +17131,13 @@ function createSelectionPlugin(options) {
|
|
|
16902
17131
|
if (opts.onCellClick)
|
|
16903
17132
|
opts.onCellClick(row, col);
|
|
16904
17133
|
store.exec('selection:setActive', { row, col });
|
|
17134
|
+
// Triple-click the first column to select the entire row.
|
|
17135
|
+
if (event.detail >= 3 && col === 0) {
|
|
17136
|
+
const additiveRow = (event.ctrlKey || event.metaKey) && !!multiSel;
|
|
17137
|
+
store.exec('selection:selectRows', row, row, additiveRow);
|
|
17138
|
+
anchorCell = { row, col };
|
|
17139
|
+
return;
|
|
17140
|
+
}
|
|
16905
17141
|
const additive = (event.ctrlKey || event.metaKey) && !!multiSel;
|
|
16906
17142
|
if (event.shiftKey && anchorCell) {
|
|
16907
17143
|
if (selType === 'row') {
|
|
@@ -16932,7 +17168,17 @@ function createSelectionPlugin(options) {
|
|
|
16932
17168
|
store.exec('selection:selectRange', row, col, row, col, additive);
|
|
16933
17169
|
break;
|
|
16934
17170
|
default:
|
|
16935
|
-
|
|
17171
|
+
if (additive) {
|
|
17172
|
+
// Ctrl/Cmd+click toggles the individual cell in a multi-selection
|
|
17173
|
+
store.exec('selection:toggleCell', row, col);
|
|
17174
|
+
}
|
|
17175
|
+
else if (mgr.isSelected(row, col)) {
|
|
17176
|
+
// Plain click on the already-selected cell clears it
|
|
17177
|
+
store.exec('selection:clear');
|
|
17178
|
+
}
|
|
17179
|
+
else {
|
|
17180
|
+
store.exec('selection:selectCell', row, col);
|
|
17181
|
+
}
|
|
16936
17182
|
}
|
|
16937
17183
|
anchorCell = { row, col };
|
|
16938
17184
|
}
|
|
@@ -16970,6 +17216,7 @@ function createSelectionPlugin(options) {
|
|
|
16970
17216
|
// --- API ---
|
|
16971
17217
|
api.register('selection', {
|
|
16972
17218
|
selectCell: (row, col, additive) => store.exec('selection:selectCell', row, col, additive),
|
|
17219
|
+
toggleCell: (row, col) => store.exec('selection:toggleCell', row, col),
|
|
16973
17220
|
selectRange: (startRow, startCol, endRow, endCol, additive) => store.exec('selection:selectRange', startRow, startCol, endRow, endCol, additive),
|
|
16974
17221
|
selectRows: (startRow, endRow, additive) => store.exec('selection:selectRows', startRow, endRow, additive),
|
|
16975
17222
|
selectColumns: (startCol, endCol, additive) => store.exec('selection:selectColumns', startCol, endCol, additive),
|
|
@@ -17143,8 +17390,8 @@ class TextEditor {
|
|
|
17143
17390
|
// Inline styles for better UX
|
|
17144
17391
|
input.style.width = '100%';
|
|
17145
17392
|
input.style.height = '100%';
|
|
17146
|
-
input.style.border = '
|
|
17147
|
-
input.style.outline = '
|
|
17393
|
+
input.style.border = '2px solid #4CAF50';
|
|
17394
|
+
input.style.outline = 'none';
|
|
17148
17395
|
input.style.padding = '4px 8px';
|
|
17149
17396
|
input.style.fontSize = '13px';
|
|
17150
17397
|
input.style.fontFamily = 'inherit';
|
|
@@ -17423,16 +17670,16 @@ class NumberEditor {
|
|
|
17423
17670
|
this.input.placeholder = this.options.placeholder;
|
|
17424
17671
|
}
|
|
17425
17672
|
// Styling
|
|
17426
|
-
this.input.style.cssText = `
|
|
17427
|
-
width: 100%;
|
|
17428
|
-
height: 100%;
|
|
17429
|
-
border: 2px solid #4caf50;
|
|
17430
|
-
padding: 0 8px;
|
|
17431
|
-
font-family: inherit;
|
|
17432
|
-
font-size: inherit;
|
|
17433
|
-
outline: none;
|
|
17434
|
-
box-sizing: border-box;
|
|
17435
|
-
text-align: right;
|
|
17673
|
+
this.input.style.cssText = `
|
|
17674
|
+
width: 100%;
|
|
17675
|
+
height: 100%;
|
|
17676
|
+
border: 2px solid #4caf50;
|
|
17677
|
+
padding: 0 8px;
|
|
17678
|
+
font-family: inherit;
|
|
17679
|
+
font-size: inherit;
|
|
17680
|
+
outline: none;
|
|
17681
|
+
box-sizing: border-box;
|
|
17682
|
+
text-align: right;
|
|
17436
17683
|
`;
|
|
17437
17684
|
// Event handlers
|
|
17438
17685
|
this.input.addEventListener('keydown', this.boundHandleKeyDown);
|
|
@@ -17506,13 +17753,14 @@ class NumberEditor {
|
|
|
17506
17753
|
}
|
|
17507
17754
|
}
|
|
17508
17755
|
handleKeyDown(event) {
|
|
17509
|
-
//
|
|
17510
|
-
|
|
17511
|
-
|
|
17512
|
-
|
|
17513
|
-
event.stopPropagation();
|
|
17514
|
-
// Let default behavior handle increment/decrement
|
|
17756
|
+
// Let Enter/Escape/Tab bubble to the EditorManager so it can
|
|
17757
|
+
// commit/cancel/navigate. Stopping propagation here would swallow them.
|
|
17758
|
+
if (event.key === 'Enter' || event.key === 'Escape' || event.key === 'Tab') {
|
|
17759
|
+
return;
|
|
17515
17760
|
}
|
|
17761
|
+
// Stop other keys (digits, arrows) from reaching grid navigation.
|
|
17762
|
+
// ArrowUp/Down keep their default increment/decrement behavior.
|
|
17763
|
+
event.stopPropagation();
|
|
17516
17764
|
}
|
|
17517
17765
|
}
|
|
17518
17766
|
|
|
@@ -17549,16 +17797,16 @@ class SelectEditor {
|
|
|
17549
17797
|
this.select = document.createElement('select');
|
|
17550
17798
|
this.select.className = 'zg-editor zg-select-editor';
|
|
17551
17799
|
// Styling
|
|
17552
|
-
this.select.style.cssText = `
|
|
17553
|
-
width: 100%;
|
|
17554
|
-
height: 100%;
|
|
17555
|
-
border: 2px solid #4caf50;
|
|
17556
|
-
padding: 0 8px;
|
|
17557
|
-
font-family: inherit;
|
|
17558
|
-
font-size: inherit;
|
|
17559
|
-
outline: none;
|
|
17560
|
-
box-sizing: border-box;
|
|
17561
|
-
background: white;
|
|
17800
|
+
this.select.style.cssText = `
|
|
17801
|
+
width: 100%;
|
|
17802
|
+
height: 100%;
|
|
17803
|
+
border: 2px solid #4caf50;
|
|
17804
|
+
padding: 0 8px;
|
|
17805
|
+
font-family: inherit;
|
|
17806
|
+
font-size: inherit;
|
|
17807
|
+
outline: none;
|
|
17808
|
+
box-sizing: border-box;
|
|
17809
|
+
background: white;
|
|
17562
17810
|
`;
|
|
17563
17811
|
// Add empty option if allowed
|
|
17564
17812
|
if (this.options.allowEmpty) {
|
|
@@ -17628,7 +17876,12 @@ class SelectEditor {
|
|
|
17628
17876
|
}
|
|
17629
17877
|
}
|
|
17630
17878
|
handleKeyDown(event) {
|
|
17631
|
-
//
|
|
17879
|
+
// Let Enter/Escape/Tab bubble to the EditorManager so it can
|
|
17880
|
+
// commit/cancel/navigate. Stopping propagation here would swallow them.
|
|
17881
|
+
if (event.key === 'Enter' || event.key === 'Escape' || event.key === 'Tab') {
|
|
17882
|
+
return;
|
|
17883
|
+
}
|
|
17884
|
+
// Stop other keys from reaching grid navigation.
|
|
17632
17885
|
event.stopPropagation();
|
|
17633
17886
|
}
|
|
17634
17887
|
}
|
|
@@ -17786,6 +18039,12 @@ class DateEditor {
|
|
|
17786
18039
|
type: 'default',
|
|
17787
18040
|
selectionDatesMode: 'single',
|
|
17788
18041
|
enableJumpToSelectedDate: true,
|
|
18042
|
+
// Follow the editor's theme (default 'light') instead of the library's
|
|
18043
|
+
// 'system' default, which would render dark on a dark OS even when the
|
|
18044
|
+
// grid is on a light theme. themeAttrDetect:false stops the library from
|
|
18045
|
+
// auto-detecting a page attribute and overriding selectedTheme.
|
|
18046
|
+
selectedTheme: this.options.theme === 'auto' ? 'system' : this.options.theme || 'light',
|
|
18047
|
+
themeAttrDetect: false,
|
|
17789
18048
|
dateMin: minDate ? formatDateForCalendar(minDate) : '1900-01-01',
|
|
17790
18049
|
dateMax: maxDate ? formatDateForCalendar(maxDate) : '2099-12-31',
|
|
17791
18050
|
onClickDate: (self, event) => {
|
|
@@ -19065,12 +19324,19 @@ async function initializeCalendars(startDiv, endDiv, options, currentValue, hand
|
|
|
19065
19324
|
const { minDate, maxDate } = options;
|
|
19066
19325
|
const dateMinStr = minDate ? formatDateForCalendar(minDate) : '1900-01-01';
|
|
19067
19326
|
const dateMaxStr = maxDate ? formatDateForCalendar(maxDate) : '2099-12-31';
|
|
19327
|
+
// Follow the editor's theme (default 'light') instead of the library's
|
|
19328
|
+
// 'system' default, which would render dark on a dark OS even when the grid
|
|
19329
|
+
// is on a light theme. themeAttrDetect:false stops the library from
|
|
19330
|
+
// auto-detecting a page attribute and overriding selectedTheme.
|
|
19331
|
+
const selectedTheme = options.theme === 'auto' ? 'system' : options.theme || 'light';
|
|
19068
19332
|
try {
|
|
19069
19333
|
const { Calendar } = await import('vanilla-calendar-pro');
|
|
19070
19334
|
const startOptions = {
|
|
19071
19335
|
type: 'default',
|
|
19072
19336
|
selectionDatesMode: 'single',
|
|
19073
19337
|
enableJumpToSelectedDate: true,
|
|
19338
|
+
selectedTheme,
|
|
19339
|
+
themeAttrDetect: false,
|
|
19074
19340
|
dateMin: dateMinStr,
|
|
19075
19341
|
dateMax: dateMaxStr,
|
|
19076
19342
|
selectedDates: currentValue.start
|
|
@@ -19102,6 +19368,8 @@ async function initializeCalendars(startDiv, endDiv, options, currentValue, hand
|
|
|
19102
19368
|
type: 'default',
|
|
19103
19369
|
selectionDatesMode: 'single',
|
|
19104
19370
|
enableJumpToSelectedDate: true,
|
|
19371
|
+
selectedTheme,
|
|
19372
|
+
themeAttrDetect: false,
|
|
19105
19373
|
dateMin: endDateMin,
|
|
19106
19374
|
dateMax: dateMaxStr,
|
|
19107
19375
|
selectedDates: currentValue.end
|
|
@@ -19357,9 +19625,17 @@ function createContainer$1(params, options) {
|
|
|
19357
19625
|
label.className = options.className;
|
|
19358
19626
|
label.style.display = 'flex';
|
|
19359
19627
|
label.style.alignItems = 'center';
|
|
19628
|
+
label.style.justifyContent = 'center';
|
|
19360
19629
|
label.style.cursor = options.disabled ? 'not-allowed' : 'pointer';
|
|
19361
19630
|
label.style.padding = '8px';
|
|
19362
19631
|
label.style.userSelect = 'none';
|
|
19632
|
+
// Fill the cell with an opaque, outlined surface so the underlying rendered
|
|
19633
|
+
// checkbox (e.g. CheckboxRenderer's centered box) does not show through.
|
|
19634
|
+
label.style.width = '100%';
|
|
19635
|
+
label.style.height = '100%';
|
|
19636
|
+
label.style.boxSizing = 'border-box';
|
|
19637
|
+
label.style.background = '#fff';
|
|
19638
|
+
label.style.border = '2px solid #4CAF50';
|
|
19363
19639
|
// Set data attributes
|
|
19364
19640
|
label.dataset['row'] = String(params.cell.row);
|
|
19365
19641
|
label.dataset['col'] = String(params.cell.col);
|
|
@@ -19840,6 +20116,8 @@ class EditorManager {
|
|
|
19840
20116
|
this.getCellElement = options.getCellElement;
|
|
19841
20117
|
this.onEditStart = options.onEditStart;
|
|
19842
20118
|
this.onEditEnd = options.onEditEnd;
|
|
20119
|
+
this.onCommit = options.onCommit;
|
|
20120
|
+
this.invalidEditMode = options.invalidEditMode ?? 'block';
|
|
19843
20121
|
this.boundHandleEditorKeyDown = this.handleEditorKeyDown.bind(this);
|
|
19844
20122
|
this.boundHandleClickOutside = this.handleClickOutside.bind(this);
|
|
19845
20123
|
// Register default editors
|
|
@@ -19892,10 +20170,10 @@ class EditorManager {
|
|
|
19892
20170
|
// Create editor container with correct positioning
|
|
19893
20171
|
this.editorContainer = document.createElement('div');
|
|
19894
20172
|
this.editorContainer.className = 'zg-editor-container';
|
|
19895
|
-
this.editorContainer.style.cssText = `
|
|
19896
|
-
position: absolute;
|
|
19897
|
-
z-index: 10002;
|
|
19898
|
-
overflow: visible;
|
|
20173
|
+
this.editorContainer.style.cssText = `
|
|
20174
|
+
position: absolute;
|
|
20175
|
+
z-index: 10002;
|
|
20176
|
+
overflow: visible;
|
|
19899
20177
|
`;
|
|
19900
20178
|
this.positionEditorContainer(cellElement);
|
|
19901
20179
|
// Determine editor - supports both instance and string type
|
|
@@ -19972,16 +20250,37 @@ class EditorManager {
|
|
|
19972
20250
|
const isValid = validation === undefined ||
|
|
19973
20251
|
validation === true ||
|
|
19974
20252
|
(typeof validation === 'object' && validation.valid);
|
|
19975
|
-
if (!isValid) {
|
|
19976
|
-
const message = typeof validation === 'object'
|
|
19977
|
-
|
|
20253
|
+
if (!isValid && this.invalidEditMode !== 'commit') {
|
|
20254
|
+
const message = (typeof validation === 'object' && validation.message) || 'Invalid value';
|
|
20255
|
+
if (this.invalidEditMode === 'revert') {
|
|
20256
|
+
// Discard the invalid input and restore the original value so focus can
|
|
20257
|
+
// move on. cancelEdit() emits cancel/end and cleans up.
|
|
20258
|
+
this.cancelEdit();
|
|
20259
|
+
}
|
|
20260
|
+
else {
|
|
20261
|
+
// 'block': keep the editor open and flag it invalid (never blocks with a
|
|
20262
|
+
// native alert(), which spammed on every blur and trapped the user).
|
|
20263
|
+
// Escape cancels; fixing the value lets the commit through.
|
|
20264
|
+
this.markInvalid(message);
|
|
20265
|
+
}
|
|
19978
20266
|
return;
|
|
19979
20267
|
}
|
|
19980
20268
|
// Get new value
|
|
19981
20269
|
const newValue = this.currentEditor.getValue();
|
|
19982
20270
|
const cell = this.editingCell;
|
|
20271
|
+
const oldValue = this.originalValue;
|
|
19983
20272
|
// Set value
|
|
19984
20273
|
this.setValue(cell.row, cell.col, newValue);
|
|
20274
|
+
// 'commit' mode: the value failed validation but the caller opted to accept
|
|
20275
|
+
// it. Tag the cell (after the re-render from setValue) so the error stays
|
|
20276
|
+
// visible after the editor closes.
|
|
20277
|
+
if (!isValid) {
|
|
20278
|
+
this.flagInvalidCell(cell);
|
|
20279
|
+
}
|
|
20280
|
+
// Record the change for undo/redo (only when the value actually changed)
|
|
20281
|
+
if (this.onCommit && oldValue !== newValue) {
|
|
20282
|
+
this.onCommit(cell, oldValue, newValue);
|
|
20283
|
+
}
|
|
19985
20284
|
// Emit events
|
|
19986
20285
|
if (this.events) {
|
|
19987
20286
|
this.events.emit('edit:commit', {
|
|
@@ -20052,6 +20351,36 @@ class EditorManager {
|
|
|
20052
20351
|
this.originalValue = null;
|
|
20053
20352
|
this.editorPopups.clear();
|
|
20054
20353
|
}
|
|
20354
|
+
/**
|
|
20355
|
+
* Flag the current editor as invalid without blocking the UI.
|
|
20356
|
+
* Highlights the input in red and shows a visible inline message below it
|
|
20357
|
+
* (plus a tooltip); the editor stays open so the user can fix the value or
|
|
20358
|
+
* press Escape.
|
|
20359
|
+
*/
|
|
20360
|
+
markInvalid(message) {
|
|
20361
|
+
if (!this.editorContainer)
|
|
20362
|
+
return;
|
|
20363
|
+
const field = this.editorContainer.querySelector('input, select, textarea');
|
|
20364
|
+
if (field) {
|
|
20365
|
+
field.style.borderColor = '#d32f2f';
|
|
20366
|
+
field.title = message;
|
|
20367
|
+
}
|
|
20368
|
+
let hint = this.editorContainer.querySelector('.zg-editor-error');
|
|
20369
|
+
if (!hint) {
|
|
20370
|
+
hint = document.createElement('div');
|
|
20371
|
+
hint.className = 'zg-editor-error';
|
|
20372
|
+
this.editorContainer.appendChild(hint);
|
|
20373
|
+
}
|
|
20374
|
+
hint.textContent = message;
|
|
20375
|
+
}
|
|
20376
|
+
/**
|
|
20377
|
+
* Mark a committed cell as holding an invalid value (used by 'commit' mode).
|
|
20378
|
+
* Adds `.zg-cell-invalid` so the error stays visible after the editor closes.
|
|
20379
|
+
*/
|
|
20380
|
+
flagInvalidCell(cell) {
|
|
20381
|
+
const cellElement = this.getCellElement(cell.row, cell.col);
|
|
20382
|
+
cellElement?.classList.add('zg-cell-invalid');
|
|
20383
|
+
}
|
|
20055
20384
|
/**
|
|
20056
20385
|
* Get currently editing cell
|
|
20057
20386
|
*/
|
|
@@ -20228,6 +20557,20 @@ class EditorManager {
|
|
|
20228
20557
|
}
|
|
20229
20558
|
}
|
|
20230
20559
|
|
|
20560
|
+
/**
|
|
20561
|
+
* HeaderManager types and interfaces
|
|
20562
|
+
*/
|
|
20563
|
+
/** Default header row height in pixels when `headerHeight` is not configured. */
|
|
20564
|
+
const DEFAULT_HEADER_HEIGHT = 40;
|
|
20565
|
+
/**
|
|
20566
|
+
* Resolve the header row height from grid options, falling back to the default.
|
|
20567
|
+
* A non-positive value falls back to the default to avoid a collapsed header.
|
|
20568
|
+
*/
|
|
20569
|
+
function resolveHeaderHeight(options) {
|
|
20570
|
+
const h = options.headerHeight;
|
|
20571
|
+
return typeof h === 'number' && h > 0 ? h : DEFAULT_HEADER_HEIGHT;
|
|
20572
|
+
}
|
|
20573
|
+
|
|
20231
20574
|
function createEditingPlugin(pluginOpts) {
|
|
20232
20575
|
return {
|
|
20233
20576
|
name: 'editing',
|
|
@@ -20261,24 +20604,28 @@ function createEditingPlugin(pluginOpts) {
|
|
|
20261
20604
|
const orderedColumns = columnModel.getVisibleColumnsInOrder();
|
|
20262
20605
|
return orderedColumns?.[viewCol]?.dataIndex ?? viewCol;
|
|
20263
20606
|
};
|
|
20607
|
+
const setCellValue = (row, col, value) => {
|
|
20608
|
+
const dataRow = mapViewRowToDataRow(row);
|
|
20609
|
+
const dataCol = mapViewColToDataCol(col);
|
|
20610
|
+
const rowData = state.data[dataRow];
|
|
20611
|
+
if (Array.isArray(rowData)) {
|
|
20612
|
+
rowData[dataCol] = value;
|
|
20613
|
+
}
|
|
20614
|
+
store.exec('rendering:updateCells', [{ row, col }]);
|
|
20615
|
+
};
|
|
20264
20616
|
if (!editorManager) {
|
|
20265
20617
|
editorManager = new EditorManager({
|
|
20266
20618
|
container,
|
|
20267
20619
|
scrollContainer,
|
|
20620
|
+
events,
|
|
20621
|
+
headerHeight: resolveHeaderHeight(options),
|
|
20622
|
+
invalidEditMode: options.editing?.invalidEditMode,
|
|
20268
20623
|
getValue: (row, col) => {
|
|
20269
20624
|
const dataRow = mapViewRowToDataRow(row);
|
|
20270
20625
|
const dataCol = mapViewColToDataCol(col);
|
|
20271
20626
|
return getDataAccessor?.()?.getValue(dataRow, dataCol);
|
|
20272
20627
|
},
|
|
20273
|
-
setValue:
|
|
20274
|
-
const dataRow = mapViewRowToDataRow(row);
|
|
20275
|
-
const dataCol = mapViewColToDataCol(col);
|
|
20276
|
-
const rowData = state.data[dataRow];
|
|
20277
|
-
if (Array.isArray(rowData)) {
|
|
20278
|
-
rowData[dataCol] = value;
|
|
20279
|
-
}
|
|
20280
|
-
store.exec('rendering:updateCells', [{ row, col }]);
|
|
20281
|
-
},
|
|
20628
|
+
setValue: setCellValue,
|
|
20282
20629
|
getColumn: (col) => {
|
|
20283
20630
|
const dataCol = mapViewColToDataCol(col);
|
|
20284
20631
|
return options.columns?.[dataCol];
|
|
@@ -20297,6 +20644,11 @@ function createEditingPlugin(pluginOpts) {
|
|
|
20297
20644
|
store.set('editing.active', null);
|
|
20298
20645
|
store.exec('rendering:refreshSelectionClasses');
|
|
20299
20646
|
},
|
|
20647
|
+
onCommit: (cell, oldValue, newValue) => {
|
|
20648
|
+
if (!store.hasAction('undoRedo:recordCellEdit'))
|
|
20649
|
+
return;
|
|
20650
|
+
store.exec('undoRedo:recordCellEdit', cell.row, cell.col, oldValue, newValue, setCellValue);
|
|
20651
|
+
},
|
|
20300
20652
|
});
|
|
20301
20653
|
store.exec('editing:bind', editorManager);
|
|
20302
20654
|
eventUnsubs.push(events.on('scroll', () => {
|
|
@@ -20342,6 +20694,10 @@ function createEditingPlugin(pluginOpts) {
|
|
|
20342
20694
|
container.addEventListener('keydown', keydownHandler);
|
|
20343
20695
|
domCleanups.push(() => container.removeEventListener('keydown', keydownHandler));
|
|
20344
20696
|
}, 'editing');
|
|
20697
|
+
// Edit lifecycle events (edit:start/commit/cancel/end) are emitted by the
|
|
20698
|
+
// EditorManager itself via the shared `events` emitter, so both interactive
|
|
20699
|
+
// (Enter/Tab/blur) and programmatic commits fire exactly once. These actions
|
|
20700
|
+
// only drive the manager and keep editing.active in sync.
|
|
20345
20701
|
store.action('editing:startEdit', (cell) => {
|
|
20346
20702
|
if (!editorManager)
|
|
20347
20703
|
return;
|
|
@@ -20349,30 +20705,20 @@ function createEditingPlugin(pluginOpts) {
|
|
|
20349
20705
|
return;
|
|
20350
20706
|
editorManager.startEdit(cell);
|
|
20351
20707
|
store.set('editing.active', cell);
|
|
20352
|
-
api.fireEvent('edit:start', { cell, value: editorManager.getCurrentValue?.() });
|
|
20353
20708
|
}, 'editing');
|
|
20354
|
-
store.action('editing:commitEdit', (
|
|
20709
|
+
store.action('editing:commitEdit', () => {
|
|
20355
20710
|
if (!editorManager)
|
|
20356
20711
|
return;
|
|
20357
|
-
|
|
20358
|
-
if (!cell)
|
|
20712
|
+
if (!store.get('editing.active'))
|
|
20359
20713
|
return;
|
|
20360
|
-
const oldValue = editorManager.getOriginalValue?.();
|
|
20361
20714
|
editorManager.commitEdit?.();
|
|
20362
20715
|
store.set('editing.active', null);
|
|
20363
|
-
api.fireEvent('edit:commit', { cell, oldValue, newValue: value });
|
|
20364
|
-
api.fireEvent('edit:end', { cell, value, cancelled: false });
|
|
20365
20716
|
}, 'editing');
|
|
20366
20717
|
store.action('editing:cancelEdit', () => {
|
|
20367
20718
|
if (!editorManager)
|
|
20368
20719
|
return;
|
|
20369
|
-
const cell = store.get('editing.active');
|
|
20370
20720
|
editorManager.cancelEdit?.();
|
|
20371
20721
|
store.set('editing.active', null);
|
|
20372
|
-
if (cell) {
|
|
20373
|
-
api.fireEvent('edit:cancel', { cell, value: undefined });
|
|
20374
|
-
api.fireEvent('edit:end', { cell, value: undefined, cancelled: true });
|
|
20375
|
-
}
|
|
20376
20722
|
}, 'editing');
|
|
20377
20723
|
api.register('editing', {
|
|
20378
20724
|
startEdit: (cell) => store.exec('editing:startEdit', cell),
|
|
@@ -20401,74 +20747,731 @@ function createEditingPlugin(pluginOpts) {
|
|
|
20401
20747
|
};
|
|
20402
20748
|
}
|
|
20403
20749
|
|
|
20750
|
+
const NAV_KEYS = new Set([
|
|
20751
|
+
'ArrowUp',
|
|
20752
|
+
'ArrowDown',
|
|
20753
|
+
'ArrowLeft',
|
|
20754
|
+
'ArrowRight',
|
|
20755
|
+
'PageUp',
|
|
20756
|
+
'PageDown',
|
|
20757
|
+
'Home',
|
|
20758
|
+
'End',
|
|
20759
|
+
]);
|
|
20404
20760
|
/**
|
|
20405
|
-
*
|
|
20406
|
-
*
|
|
20407
|
-
* Uses CommandStack from shared package for managing command history.
|
|
20408
|
-
* Supports:
|
|
20409
|
-
* - Cell value changes
|
|
20410
|
-
* - Filter changes
|
|
20411
|
-
* - Sort changes
|
|
20412
|
-
* - Selection changes
|
|
20413
|
-
* - Custom commands
|
|
20761
|
+
* Whether keydown originated in a form control / editable element, in which case
|
|
20762
|
+
* the grid should let the element handle the key rather than navigate cells.
|
|
20414
20763
|
*/
|
|
20764
|
+
function isEditableTarget(target) {
|
|
20765
|
+
const el = target;
|
|
20766
|
+
if (!el || !el.tagName)
|
|
20767
|
+
return false;
|
|
20768
|
+
const tag = el.tagName;
|
|
20769
|
+
return (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || el.isContentEditable === true);
|
|
20770
|
+
}
|
|
20415
20771
|
/**
|
|
20416
|
-
*
|
|
20772
|
+
* Compute the destination cell for a navigation key, clamped to grid bounds.
|
|
20773
|
+
* Returns the same reference when the key does not move (already at an edge).
|
|
20417
20774
|
*/
|
|
20418
|
-
|
|
20419
|
-
|
|
20420
|
-
|
|
20421
|
-
|
|
20422
|
-
|
|
20423
|
-
|
|
20424
|
-
|
|
20425
|
-
|
|
20426
|
-
|
|
20427
|
-
|
|
20428
|
-
|
|
20429
|
-
|
|
20430
|
-
|
|
20431
|
-
|
|
20432
|
-
|
|
20433
|
-
|
|
20434
|
-
|
|
20775
|
+
function nextCell(key, active, step, rows, cols) {
|
|
20776
|
+
let { row, col } = active;
|
|
20777
|
+
switch (key) {
|
|
20778
|
+
case 'ArrowUp':
|
|
20779
|
+
row = Math.max(0, row - 1);
|
|
20780
|
+
break;
|
|
20781
|
+
case 'ArrowDown':
|
|
20782
|
+
row = Math.min(rows - 1, row + 1);
|
|
20783
|
+
break;
|
|
20784
|
+
case 'ArrowLeft':
|
|
20785
|
+
col = Math.max(0, col - 1);
|
|
20786
|
+
break;
|
|
20787
|
+
case 'ArrowRight':
|
|
20788
|
+
col = Math.min(cols - 1, col + 1);
|
|
20789
|
+
break;
|
|
20790
|
+
case 'PageUp':
|
|
20791
|
+
row = Math.max(0, row - step);
|
|
20792
|
+
break;
|
|
20793
|
+
case 'PageDown':
|
|
20794
|
+
row = Math.min(rows - 1, row + step);
|
|
20795
|
+
break;
|
|
20796
|
+
case 'Home':
|
|
20797
|
+
col = 0;
|
|
20798
|
+
break;
|
|
20799
|
+
case 'End':
|
|
20800
|
+
col = cols - 1;
|
|
20801
|
+
break;
|
|
20802
|
+
default:
|
|
20803
|
+
return null;
|
|
20435
20804
|
}
|
|
20805
|
+
if (row === active.row && col === active.col)
|
|
20806
|
+
return active;
|
|
20807
|
+
return { row, col };
|
|
20436
20808
|
}
|
|
20809
|
+
|
|
20437
20810
|
/**
|
|
20438
|
-
*
|
|
20811
|
+
* KeyboardPlugin - Store-driven keyboard cell navigation.
|
|
20812
|
+
*
|
|
20813
|
+
* Maintains `state.activeCell` (the single source read by the rendering active-cell
|
|
20814
|
+
* highlight and the editing Enter-to-edit guard) and drives selection + scroll-into-view
|
|
20815
|
+
* as the active cell moves. Arrow/Page/Home/End move the cursor; Shift extends a range;
|
|
20816
|
+
* Ctrl/Cmd+A selects all. Enter/F2 editing is handled by the editing plugin's own keydown.
|
|
20439
20817
|
*/
|
|
20440
|
-
|
|
20441
|
-
|
|
20442
|
-
|
|
20443
|
-
|
|
20444
|
-
|
|
20445
|
-
|
|
20446
|
-
|
|
20447
|
-
|
|
20448
|
-
|
|
20449
|
-
|
|
20450
|
-
|
|
20451
|
-
|
|
20452
|
-
|
|
20453
|
-
|
|
20454
|
-
|
|
20455
|
-
|
|
20456
|
-
|
|
20457
|
-
|
|
20458
|
-
|
|
20459
|
-
|
|
20460
|
-
|
|
20818
|
+
function createKeyboardPlugin(pluginOpts) {
|
|
20819
|
+
return {
|
|
20820
|
+
name: 'keyboard',
|
|
20821
|
+
phase: 46,
|
|
20822
|
+
dependencies: ['core'],
|
|
20823
|
+
setup(store, api) {
|
|
20824
|
+
const domCleanups = [];
|
|
20825
|
+
function colCount() {
|
|
20826
|
+
return pluginOpts?.options?.colCount ?? 0;
|
|
20827
|
+
}
|
|
20828
|
+
function rowCount() {
|
|
20829
|
+
return pluginOpts?.options?.rowCount ?? 0;
|
|
20830
|
+
}
|
|
20831
|
+
function getScroller() {
|
|
20832
|
+
const fn = api.getMethod('rendering', 'getScroller');
|
|
20833
|
+
return fn ? fn() : null;
|
|
20834
|
+
}
|
|
20835
|
+
function pageStep(scroller, scrollContainer) {
|
|
20836
|
+
if (scroller && scrollContainer) {
|
|
20837
|
+
const top = scrollContainer.scrollTop;
|
|
20838
|
+
const rows = scroller.getRowAtOffset(top + scrollContainer.clientHeight) -
|
|
20839
|
+
scroller.getRowAtOffset(top);
|
|
20840
|
+
if (rows > 0)
|
|
20841
|
+
return rows;
|
|
20842
|
+
}
|
|
20843
|
+
return 10;
|
|
20844
|
+
}
|
|
20845
|
+
function scrollIntoView(cell) {
|
|
20846
|
+
const scroller = getScroller();
|
|
20847
|
+
const scrollContainer = store.get('dom.scrollContainer');
|
|
20848
|
+
if (!scroller || !scrollContainer)
|
|
20849
|
+
return;
|
|
20850
|
+
const pos = scroller.getCellPosition(cell.row, cell.col);
|
|
20851
|
+
const st = scrollContainer.scrollTop;
|
|
20852
|
+
const sl = scrollContainer.scrollLeft;
|
|
20853
|
+
const ch = scrollContainer.clientHeight;
|
|
20854
|
+
const cw = scrollContainer.clientWidth;
|
|
20855
|
+
let newTop = st;
|
|
20856
|
+
let newLeft = sl;
|
|
20857
|
+
if (pos.y < st)
|
|
20858
|
+
newTop = pos.y;
|
|
20859
|
+
else if (pos.y + pos.height > st + ch)
|
|
20860
|
+
newTop = pos.y + pos.height - ch;
|
|
20861
|
+
if (pos.x < sl)
|
|
20862
|
+
newLeft = pos.x;
|
|
20863
|
+
else if (pos.x + pos.width > sl + cw)
|
|
20864
|
+
newLeft = pos.x + pos.width - cw;
|
|
20865
|
+
if (newTop !== st || newLeft !== sl) {
|
|
20866
|
+
scrollContainer.scrollTop = newTop;
|
|
20867
|
+
scrollContainer.scrollLeft = newLeft;
|
|
20868
|
+
if (store.hasAction('rendering:handleScroll')) {
|
|
20869
|
+
store.exec('rendering:handleScroll', newTop, newLeft);
|
|
20870
|
+
}
|
|
20871
|
+
}
|
|
20872
|
+
}
|
|
20873
|
+
let anchor = null;
|
|
20874
|
+
function setActive(cell, extend) {
|
|
20875
|
+
const state = pluginOpts?.state;
|
|
20876
|
+
if (state)
|
|
20877
|
+
state.activeCell = cell;
|
|
20878
|
+
if (extend && anchor && store.hasAction('selection:selectRange')) {
|
|
20879
|
+
store.exec('selection:selectRange', anchor.row, anchor.col, cell.row, cell.col);
|
|
20880
|
+
}
|
|
20881
|
+
else if (store.hasAction('selection:selectCell')) {
|
|
20882
|
+
store.exec('selection:selectCell', cell.row, cell.col);
|
|
20883
|
+
anchor = cell;
|
|
20884
|
+
}
|
|
20885
|
+
else {
|
|
20886
|
+
anchor = cell;
|
|
20887
|
+
}
|
|
20888
|
+
// selection:* already refreshes classes via lifecycle; refresh directly when
|
|
20889
|
+
// there is no selection plugin so the active-cell highlight still repaints.
|
|
20890
|
+
if (!store.hasAction('selection:selectCell') && store.hasAction('rendering:refreshSelectionClasses')) {
|
|
20891
|
+
store.exec('rendering:refreshSelectionClasses');
|
|
20892
|
+
}
|
|
20893
|
+
scrollIntoView(cell);
|
|
20894
|
+
}
|
|
20895
|
+
const handleKeyDown = (event) => {
|
|
20896
|
+
const state = pluginOpts?.state;
|
|
20897
|
+
if (!state || state.editingCell)
|
|
20898
|
+
return;
|
|
20899
|
+
if (isEditableTarget(event.target))
|
|
20900
|
+
return;
|
|
20901
|
+
const rows = rowCount();
|
|
20902
|
+
const cols = colCount();
|
|
20903
|
+
if (rows <= 0 || cols <= 0)
|
|
20904
|
+
return;
|
|
20905
|
+
const cmd = event.ctrlKey || event.metaKey;
|
|
20906
|
+
const key = event.key;
|
|
20907
|
+
if (cmd && (key === 'a' || key === 'A')) {
|
|
20908
|
+
if (store.hasAction('selection:selectAll')) {
|
|
20909
|
+
store.exec('selection:selectAll', rows);
|
|
20910
|
+
event.preventDefault();
|
|
20911
|
+
}
|
|
20912
|
+
return;
|
|
20913
|
+
}
|
|
20914
|
+
if (!NAV_KEYS.has(key))
|
|
20915
|
+
return;
|
|
20916
|
+
const active = state.activeCell;
|
|
20917
|
+
if (!active) {
|
|
20918
|
+
setActive({ row: 0, col: 0 }, false);
|
|
20919
|
+
event.preventDefault();
|
|
20920
|
+
return;
|
|
20921
|
+
}
|
|
20922
|
+
let target;
|
|
20923
|
+
if (cmd && key === 'Home') {
|
|
20924
|
+
target = { row: 0, col: 0 };
|
|
20925
|
+
}
|
|
20926
|
+
else if (cmd && key === 'End') {
|
|
20927
|
+
target = { row: rows - 1, col: cols - 1 };
|
|
20928
|
+
}
|
|
20929
|
+
else {
|
|
20930
|
+
const scrollContainer = store.get('dom.scrollContainer');
|
|
20931
|
+
const step = key === 'PageUp' || key === 'PageDown'
|
|
20932
|
+
? pageStep(getScroller(), scrollContainer)
|
|
20933
|
+
: 1;
|
|
20934
|
+
target = nextCell(key, active, step, rows, cols);
|
|
20935
|
+
}
|
|
20936
|
+
if (!target)
|
|
20937
|
+
return;
|
|
20938
|
+
event.preventDefault();
|
|
20939
|
+
if (target === active)
|
|
20940
|
+
return;
|
|
20941
|
+
setActive(target, event.shiftKey);
|
|
20942
|
+
};
|
|
20943
|
+
const focusOnCellClick = (event) => {
|
|
20944
|
+
const container = pluginOpts?.container;
|
|
20945
|
+
const state = pluginOpts?.state;
|
|
20946
|
+
if (!container || (state && state.editingCell))
|
|
20947
|
+
return;
|
|
20948
|
+
const cell = event.target.closest('.zg-cell');
|
|
20949
|
+
if (cell)
|
|
20950
|
+
container.focus();
|
|
20951
|
+
};
|
|
20952
|
+
store.action('keyboard:setup', () => {
|
|
20953
|
+
const container = pluginOpts?.container;
|
|
20954
|
+
if (!container)
|
|
20955
|
+
return;
|
|
20956
|
+
if (!container.hasAttribute('tabindex')) {
|
|
20957
|
+
container.setAttribute('tabindex', '0');
|
|
20958
|
+
}
|
|
20959
|
+
container.addEventListener('keydown', handleKeyDown);
|
|
20960
|
+
container.addEventListener('mousedown', focusOnCellClick);
|
|
20961
|
+
domCleanups.push(() => container.removeEventListener('keydown', handleKeyDown));
|
|
20962
|
+
domCleanups.push(() => container.removeEventListener('mousedown', focusOnCellClick));
|
|
20963
|
+
}, 'keyboard');
|
|
20964
|
+
api.register('keyboard', {
|
|
20965
|
+
setActiveCell: (cell) => setActive(cell, false),
|
|
20966
|
+
getActiveCell: () => pluginOpts?.state?.activeCell ?? null,
|
|
20967
|
+
});
|
|
20968
|
+
return { teardown: [() => domCleanups.forEach((fn) => fn())] };
|
|
20969
|
+
},
|
|
20970
|
+
};
|
|
20461
20971
|
}
|
|
20972
|
+
|
|
20462
20973
|
/**
|
|
20463
|
-
*
|
|
20464
|
-
|
|
20465
|
-
|
|
20466
|
-
|
|
20467
|
-
|
|
20468
|
-
|
|
20469
|
-
|
|
20470
|
-
|
|
20471
|
-
|
|
20974
|
+
* ClipboardManager - Handles copy/cut/paste operations
|
|
20975
|
+
*
|
|
20976
|
+
* Supports multiple formats:
|
|
20977
|
+
* - text/plain: TSV (tab-separated values)
|
|
20978
|
+
* - text/html: HTML table format
|
|
20979
|
+
*
|
|
20980
|
+
* Compatible with Excel and Google Sheets.
|
|
20981
|
+
*
|
|
20982
|
+
* @example
|
|
20983
|
+
* ```typescript
|
|
20984
|
+
* const clipboard = new ClipboardManager({
|
|
20985
|
+
* getValue: (row, col) => data[row][col],
|
|
20986
|
+
* setValue: (row, col, value) => { data[row][col] = value; },
|
|
20987
|
+
* });
|
|
20988
|
+
*
|
|
20989
|
+
* // Copy selection
|
|
20990
|
+
* await clipboard.copy([
|
|
20991
|
+
* { startRow: 0, startCol: 0, endRow: 2, endCol: 2 },
|
|
20992
|
+
* ]);
|
|
20993
|
+
*
|
|
20994
|
+
* // Paste at location
|
|
20995
|
+
* await clipboard.paste(5, 0);
|
|
20996
|
+
* ```
|
|
20997
|
+
*/
|
|
20998
|
+
class ClipboardManager {
|
|
20999
|
+
constructor(options) {
|
|
21000
|
+
this.events = options.events;
|
|
21001
|
+
this.getValue = options.getValue;
|
|
21002
|
+
this.setValue = options.setValue;
|
|
21003
|
+
this.deleteValue = options.deleteValue;
|
|
21004
|
+
this.includeHeaders = options.includeHeaders ?? false;
|
|
21005
|
+
this.getColumnHeader = options.getColumnHeader;
|
|
21006
|
+
this.getRowCount = options.getRowCount;
|
|
21007
|
+
this.getColumnCount = options.getColumnCount;
|
|
21008
|
+
}
|
|
21009
|
+
/**
|
|
21010
|
+
* Clamp a range's end bounds to the real grid dimensions. Whole-row and
|
|
21011
|
+
* whole-column selections carry MAX_SAFE_INTEGER sentinels; iterating those
|
|
21012
|
+
* literally would freeze the UI and overflow array lengths.
|
|
21013
|
+
*/
|
|
21014
|
+
clampRange(range) {
|
|
21015
|
+
const startRow = Math.max(0, range.startRow);
|
|
21016
|
+
const startCol = Math.max(0, range.startCol);
|
|
21017
|
+
let endRow = range.endRow;
|
|
21018
|
+
let endCol = range.endCol;
|
|
21019
|
+
if (this.getRowCount)
|
|
21020
|
+
endRow = Math.min(endRow, this.getRowCount() - 1);
|
|
21021
|
+
if (this.getColumnCount)
|
|
21022
|
+
endCol = Math.min(endCol, this.getColumnCount() - 1);
|
|
21023
|
+
return { startRow, startCol, endRow, endCol };
|
|
21024
|
+
}
|
|
21025
|
+
/**
|
|
21026
|
+
* Copy selected ranges to clipboard
|
|
21027
|
+
* @param ranges - Ranges to copy
|
|
21028
|
+
*/
|
|
21029
|
+
async copy(ranges) {
|
|
21030
|
+
if (ranges.length === 0)
|
|
21031
|
+
return;
|
|
21032
|
+
const data = this.extractData(ranges);
|
|
21033
|
+
const tsvText = this.toTSV(data);
|
|
21034
|
+
const htmlText = this.toHTML(data);
|
|
21035
|
+
try {
|
|
21036
|
+
// Write to clipboard with multiple formats
|
|
21037
|
+
await navigator.clipboard.write([
|
|
21038
|
+
new ClipboardItem({
|
|
21039
|
+
'text/plain': new Blob([tsvText], { type: 'text/plain' }),
|
|
21040
|
+
'text/html': new Blob([htmlText], { type: 'text/html' }),
|
|
21041
|
+
}),
|
|
21042
|
+
]);
|
|
21043
|
+
// Emit event
|
|
21044
|
+
if (this.events) {
|
|
21045
|
+
this.events.emit('copy', {
|
|
21046
|
+
ranges,
|
|
21047
|
+
data: tsvText,
|
|
21048
|
+
});
|
|
21049
|
+
}
|
|
21050
|
+
}
|
|
21051
|
+
catch (error) {
|
|
21052
|
+
console.error('Failed to copy to clipboard:', error);
|
|
21053
|
+
// Fallback to older API
|
|
21054
|
+
try {
|
|
21055
|
+
await navigator.clipboard.writeText(tsvText);
|
|
21056
|
+
}
|
|
21057
|
+
catch (fallbackError) {
|
|
21058
|
+
console.error('Fallback copy also failed:', fallbackError);
|
|
21059
|
+
}
|
|
21060
|
+
}
|
|
21061
|
+
}
|
|
21062
|
+
/**
|
|
21063
|
+
* Cut selected ranges to clipboard (copy + delete)
|
|
21064
|
+
* @param ranges - Ranges to cut
|
|
21065
|
+
*/
|
|
21066
|
+
async cut(ranges) {
|
|
21067
|
+
if (ranges.length === 0)
|
|
21068
|
+
return;
|
|
21069
|
+
// Copy first
|
|
21070
|
+
await this.copy(ranges);
|
|
21071
|
+
// Then delete values
|
|
21072
|
+
for (const rawRange of ranges) {
|
|
21073
|
+
const range = this.clampRange(rawRange);
|
|
21074
|
+
for (let row = range.startRow; row <= range.endRow; row++) {
|
|
21075
|
+
for (let col = range.startCol; col <= range.endCol; col++) {
|
|
21076
|
+
if (this.deleteValue) {
|
|
21077
|
+
this.deleteValue(row, col);
|
|
21078
|
+
}
|
|
21079
|
+
else {
|
|
21080
|
+
this.setValue(row, col, null);
|
|
21081
|
+
}
|
|
21082
|
+
}
|
|
21083
|
+
}
|
|
21084
|
+
}
|
|
21085
|
+
// Emit event
|
|
21086
|
+
if (this.events) {
|
|
21087
|
+
this.events.emit('cut', {
|
|
21088
|
+
ranges,
|
|
21089
|
+
data: '', // Already emitted in copy
|
|
21090
|
+
});
|
|
21091
|
+
}
|
|
21092
|
+
}
|
|
21093
|
+
/**
|
|
21094
|
+
* Paste from clipboard at target location
|
|
21095
|
+
* @param targetRow - Target row
|
|
21096
|
+
* @param targetCol - Target column
|
|
21097
|
+
*/
|
|
21098
|
+
async paste(targetRow, targetCol) {
|
|
21099
|
+
try {
|
|
21100
|
+
// Try to read multiple formats
|
|
21101
|
+
const items = await navigator.clipboard.read();
|
|
21102
|
+
for (const item of items) {
|
|
21103
|
+
// Prefer HTML format (preserves formatting)
|
|
21104
|
+
if (item.types.includes('text/html')) {
|
|
21105
|
+
const blob = await item.getType('text/html');
|
|
21106
|
+
const html = await blob.text();
|
|
21107
|
+
const data = this.parseHTML(html);
|
|
21108
|
+
this.pasteData(data, targetRow, targetCol);
|
|
21109
|
+
return;
|
|
21110
|
+
}
|
|
21111
|
+
// Fallback to plain text
|
|
21112
|
+
if (item.types.includes('text/plain')) {
|
|
21113
|
+
const blob = await item.getType('text/plain');
|
|
21114
|
+
const text = await blob.text();
|
|
21115
|
+
const data = this.parseTSV(text);
|
|
21116
|
+
this.pasteData(data, targetRow, targetCol);
|
|
21117
|
+
return;
|
|
21118
|
+
}
|
|
21119
|
+
}
|
|
21120
|
+
}
|
|
21121
|
+
catch (error) {
|
|
21122
|
+
console.error('Failed to paste from clipboard:', error);
|
|
21123
|
+
// Fallback to text API
|
|
21124
|
+
try {
|
|
21125
|
+
const text = await navigator.clipboard.readText();
|
|
21126
|
+
const data = this.parseTSV(text);
|
|
21127
|
+
this.pasteData(data, targetRow, targetCol);
|
|
21128
|
+
}
|
|
21129
|
+
catch (fallbackError) {
|
|
21130
|
+
console.error('Fallback paste also failed:', fallbackError);
|
|
21131
|
+
}
|
|
21132
|
+
}
|
|
21133
|
+
}
|
|
21134
|
+
/**
|
|
21135
|
+
* Extract a copyable matrix from one or more ranges. All ranges are projected
|
|
21136
|
+
* onto their combined bounding box; a cell that falls outside every selected
|
|
21137
|
+
* range is emitted empty. A single dense range reduces to that rectangle, so
|
|
21138
|
+
* discontiguous multi-selections copy with their relative layout preserved
|
|
21139
|
+
* (Excel/Sheets behaviour).
|
|
21140
|
+
*/
|
|
21141
|
+
extractData(rawRanges) {
|
|
21142
|
+
const ranges = rawRanges.map((r) => this.clampRange(r));
|
|
21143
|
+
let minRow = Infinity;
|
|
21144
|
+
let maxRow = -Infinity;
|
|
21145
|
+
let minCol = Infinity;
|
|
21146
|
+
let maxCol = -Infinity;
|
|
21147
|
+
for (const r of ranges) {
|
|
21148
|
+
if (r.startRow < minRow)
|
|
21149
|
+
minRow = r.startRow;
|
|
21150
|
+
if (r.endRow > maxRow)
|
|
21151
|
+
maxRow = r.endRow;
|
|
21152
|
+
if (r.startCol < minCol)
|
|
21153
|
+
minCol = r.startCol;
|
|
21154
|
+
if (r.endCol > maxCol)
|
|
21155
|
+
maxCol = r.endCol;
|
|
21156
|
+
}
|
|
21157
|
+
const inSelection = (row, col) => ranges.some((r) => row >= r.startRow && row <= r.endRow && col >= r.startCol && col <= r.endCol);
|
|
21158
|
+
const data = [];
|
|
21159
|
+
// Include headers if requested
|
|
21160
|
+
if (this.includeHeaders && this.getColumnHeader) {
|
|
21161
|
+
const headers = [];
|
|
21162
|
+
for (let col = minCol; col <= maxCol; col++) {
|
|
21163
|
+
headers.push(this.getColumnHeader(col));
|
|
21164
|
+
}
|
|
21165
|
+
data.push(headers);
|
|
21166
|
+
}
|
|
21167
|
+
// Extract cell values across the bounding box
|
|
21168
|
+
for (let row = minRow; row <= maxRow; row++) {
|
|
21169
|
+
const rowData = [];
|
|
21170
|
+
for (let col = minCol; col <= maxCol; col++) {
|
|
21171
|
+
rowData.push(inSelection(row, col) ? this.getValue(row, col) : null);
|
|
21172
|
+
}
|
|
21173
|
+
data.push(rowData);
|
|
21174
|
+
}
|
|
21175
|
+
return data;
|
|
21176
|
+
}
|
|
21177
|
+
/**
|
|
21178
|
+
* Convert data to TSV format
|
|
21179
|
+
*/
|
|
21180
|
+
toTSV(data) {
|
|
21181
|
+
return data.map((row) => row.map((cell) => this.formatCellForTSV(cell)).join('\t')).join('\n');
|
|
21182
|
+
}
|
|
21183
|
+
/**
|
|
21184
|
+
* Format cell value for TSV
|
|
21185
|
+
*/
|
|
21186
|
+
formatCellForTSV(value) {
|
|
21187
|
+
if (value == null)
|
|
21188
|
+
return '';
|
|
21189
|
+
const str = String(value);
|
|
21190
|
+
// Escape tabs and newlines
|
|
21191
|
+
return str.replace(/\t/g, ' ').replace(/\n/g, ' ');
|
|
21192
|
+
}
|
|
21193
|
+
/**
|
|
21194
|
+
* Convert data to HTML table format
|
|
21195
|
+
*/
|
|
21196
|
+
toHTML(data) {
|
|
21197
|
+
let html = '<table>';
|
|
21198
|
+
for (let i = 0; i < data.length; i++) {
|
|
21199
|
+
const row = data[i];
|
|
21200
|
+
const tag = i === 0 && this.includeHeaders ? 'th' : 'td';
|
|
21201
|
+
html += '<tr>';
|
|
21202
|
+
for (const cell of row) {
|
|
21203
|
+
html += `<${tag}>${this.escapeHTML(String(cell ?? ''))}</${tag}>`;
|
|
21204
|
+
}
|
|
21205
|
+
html += '</tr>';
|
|
21206
|
+
}
|
|
21207
|
+
html += '</table>';
|
|
21208
|
+
return html;
|
|
21209
|
+
}
|
|
21210
|
+
/**
|
|
21211
|
+
* Escape HTML special characters
|
|
21212
|
+
*/
|
|
21213
|
+
escapeHTML(str) {
|
|
21214
|
+
return str
|
|
21215
|
+
.replace(/&/g, '&')
|
|
21216
|
+
.replace(/</g, '<')
|
|
21217
|
+
.replace(/>/g, '>')
|
|
21218
|
+
.replace(/"/g, '"')
|
|
21219
|
+
.replace(/'/g, ''');
|
|
21220
|
+
}
|
|
21221
|
+
/**
|
|
21222
|
+
* Parse TSV text to 2D array
|
|
21223
|
+
*/
|
|
21224
|
+
parseTSV(text) {
|
|
21225
|
+
const lines = text.split(/\r?\n/);
|
|
21226
|
+
return lines.map((line) => line.split('\t'));
|
|
21227
|
+
}
|
|
21228
|
+
/**
|
|
21229
|
+
* Parse HTML table to 2D array
|
|
21230
|
+
*/
|
|
21231
|
+
parseHTML(html) {
|
|
21232
|
+
const parser = new DOMParser();
|
|
21233
|
+
const doc = parser.parseFromString(html, 'text/html');
|
|
21234
|
+
const table = doc.querySelector('table');
|
|
21235
|
+
if (!table) {
|
|
21236
|
+
// No table found, treat as plain text
|
|
21237
|
+
return this.parseTSV(doc.body.textContent || '');
|
|
21238
|
+
}
|
|
21239
|
+
const data = [];
|
|
21240
|
+
const rows = Array.from(table.querySelectorAll('tr'));
|
|
21241
|
+
for (const row of rows) {
|
|
21242
|
+
const cells = Array.from(row.querySelectorAll('td, th'));
|
|
21243
|
+
const rowData = [];
|
|
21244
|
+
for (const cell of cells) {
|
|
21245
|
+
rowData.push(cell.textContent || '');
|
|
21246
|
+
}
|
|
21247
|
+
if (rowData.length > 0) {
|
|
21248
|
+
data.push(rowData);
|
|
21249
|
+
}
|
|
21250
|
+
}
|
|
21251
|
+
return data;
|
|
21252
|
+
}
|
|
21253
|
+
/**
|
|
21254
|
+
* Paste data at target location
|
|
21255
|
+
*/
|
|
21256
|
+
pasteData(data, targetRow, targetCol) {
|
|
21257
|
+
for (let i = 0; i < data.length; i++) {
|
|
21258
|
+
const row = data[i];
|
|
21259
|
+
for (let j = 0; j < row.length; j++) {
|
|
21260
|
+
const value = row[j];
|
|
21261
|
+
this.setValue(targetRow + i, targetCol + j, value);
|
|
21262
|
+
}
|
|
21263
|
+
}
|
|
21264
|
+
// Emit event
|
|
21265
|
+
if (this.events) {
|
|
21266
|
+
this.events.emit('paste', {
|
|
21267
|
+
cell: { row: targetRow, col: targetCol },
|
|
21268
|
+
data: this.toTSV(data),
|
|
21269
|
+
});
|
|
21270
|
+
}
|
|
21271
|
+
}
|
|
21272
|
+
}
|
|
21273
|
+
|
|
21274
|
+
/**
|
|
21275
|
+
* ClipboardPlugin - Wires copy/cut/paste (Ctrl/Cmd+C/X/V) to the ClipboardManager.
|
|
21276
|
+
*
|
|
21277
|
+
* Operates in view coordinates: ranges come from `selection.ranges` (falling back to a
|
|
21278
|
+
* single-cell range at `state.activeCell`), and get/set map view row/col to data row/col
|
|
21279
|
+
* the same way the editing plugin does, so TSV/HTML round-trips with Excel and Sheets.
|
|
21280
|
+
*/
|
|
21281
|
+
function createClipboardPlugin(pluginOpts) {
|
|
21282
|
+
return {
|
|
21283
|
+
name: 'clipboard',
|
|
21284
|
+
phase: 47,
|
|
21285
|
+
dependencies: ['core'],
|
|
21286
|
+
setup(store, api) {
|
|
21287
|
+
const domCleanups = [];
|
|
21288
|
+
let manager = null;
|
|
21289
|
+
const mapViewRowToDataRow = (viewRow) => {
|
|
21290
|
+
const viewIndices = store.get('rows.viewIndices');
|
|
21291
|
+
return viewIndices?.[viewRow] ?? viewRow;
|
|
21292
|
+
};
|
|
21293
|
+
const mapViewColToDataCol = (viewCol) => {
|
|
21294
|
+
const columnModel = pluginOpts?.getColumnModel?.();
|
|
21295
|
+
if (!columnModel)
|
|
21296
|
+
return viewCol;
|
|
21297
|
+
const ordered = columnModel.getVisibleColumnsInOrder();
|
|
21298
|
+
return ordered?.[viewCol]?.dataIndex ?? viewCol;
|
|
21299
|
+
};
|
|
21300
|
+
const setCellValue = (row, col, value) => {
|
|
21301
|
+
const state = pluginOpts?.state;
|
|
21302
|
+
if (!state)
|
|
21303
|
+
return;
|
|
21304
|
+
const dataRow = mapViewRowToDataRow(row);
|
|
21305
|
+
const dataCol = mapViewColToDataCol(col);
|
|
21306
|
+
const rowData = state.data[dataRow];
|
|
21307
|
+
if (Array.isArray(rowData))
|
|
21308
|
+
rowData[dataCol] = value;
|
|
21309
|
+
store.exec('rendering:updateCells', [{ row, col }]);
|
|
21310
|
+
};
|
|
21311
|
+
function getManager() {
|
|
21312
|
+
if (manager)
|
|
21313
|
+
return manager;
|
|
21314
|
+
manager = new ClipboardManager({
|
|
21315
|
+
events: pluginOpts?.events,
|
|
21316
|
+
getValue: (row, col) => {
|
|
21317
|
+
const dataRow = mapViewRowToDataRow(row);
|
|
21318
|
+
const dataCol = mapViewColToDataCol(col);
|
|
21319
|
+
return pluginOpts?.getDataAccessor?.()?.getValue(dataRow, dataCol);
|
|
21320
|
+
},
|
|
21321
|
+
setValue: setCellValue,
|
|
21322
|
+
getRowCount: () => {
|
|
21323
|
+
const viewIndices = store.get('rows.viewIndices');
|
|
21324
|
+
if (viewIndices)
|
|
21325
|
+
return viewIndices.length;
|
|
21326
|
+
return pluginOpts?.state?.data?.length ?? 0;
|
|
21327
|
+
},
|
|
21328
|
+
getColumnCount: () => {
|
|
21329
|
+
const columnModel = pluginOpts?.getColumnModel?.();
|
|
21330
|
+
const ordered = columnModel?.getVisibleColumnsInOrder?.();
|
|
21331
|
+
return ordered?.length ?? pluginOpts?.options?.columns?.length ?? 0;
|
|
21332
|
+
},
|
|
21333
|
+
});
|
|
21334
|
+
return manager;
|
|
21335
|
+
}
|
|
21336
|
+
function currentRanges() {
|
|
21337
|
+
const ranges = store.get('selection.ranges');
|
|
21338
|
+
if (ranges && ranges.length > 0)
|
|
21339
|
+
return ranges;
|
|
21340
|
+
const active = pluginOpts?.state?.activeCell;
|
|
21341
|
+
if (active) {
|
|
21342
|
+
return [{ startRow: active.row, startCol: active.col, endRow: active.row, endCol: active.col }];
|
|
21343
|
+
}
|
|
21344
|
+
return [];
|
|
21345
|
+
}
|
|
21346
|
+
function pasteTarget() {
|
|
21347
|
+
const active = pluginOpts?.state?.activeCell;
|
|
21348
|
+
if (active)
|
|
21349
|
+
return active;
|
|
21350
|
+
const ranges = store.get('selection.ranges');
|
|
21351
|
+
if (ranges && ranges.length > 0) {
|
|
21352
|
+
return { row: ranges[0].startRow, col: ranges[0].startCol };
|
|
21353
|
+
}
|
|
21354
|
+
return null;
|
|
21355
|
+
}
|
|
21356
|
+
const handleKeyDown = (event) => {
|
|
21357
|
+
const state = pluginOpts?.state;
|
|
21358
|
+
if (!state || state.editingCell)
|
|
21359
|
+
return;
|
|
21360
|
+
if (!(event.ctrlKey || event.metaKey))
|
|
21361
|
+
return;
|
|
21362
|
+
if (isEditableTarget(event.target))
|
|
21363
|
+
return;
|
|
21364
|
+
const key = event.key.toLowerCase();
|
|
21365
|
+
if (key === 'c') {
|
|
21366
|
+
const ranges = currentRanges();
|
|
21367
|
+
if (ranges.length === 0)
|
|
21368
|
+
return;
|
|
21369
|
+
event.preventDefault();
|
|
21370
|
+
void getManager().copy(ranges);
|
|
21371
|
+
}
|
|
21372
|
+
else if (key === 'x') {
|
|
21373
|
+
const ranges = currentRanges();
|
|
21374
|
+
if (ranges.length === 0)
|
|
21375
|
+
return;
|
|
21376
|
+
event.preventDefault();
|
|
21377
|
+
void getManager().cut(ranges);
|
|
21378
|
+
}
|
|
21379
|
+
else if (key === 'v') {
|
|
21380
|
+
const target = pasteTarget();
|
|
21381
|
+
if (!target)
|
|
21382
|
+
return;
|
|
21383
|
+
event.preventDefault();
|
|
21384
|
+
void getManager().paste(target.row, target.col);
|
|
21385
|
+
}
|
|
21386
|
+
};
|
|
21387
|
+
store.action('clipboard:setup', () => {
|
|
21388
|
+
const container = pluginOpts?.container;
|
|
21389
|
+
if (!container)
|
|
21390
|
+
return;
|
|
21391
|
+
container.addEventListener('keydown', handleKeyDown);
|
|
21392
|
+
domCleanups.push(() => container.removeEventListener('keydown', handleKeyDown));
|
|
21393
|
+
}, 'clipboard');
|
|
21394
|
+
api.register('clipboard', {
|
|
21395
|
+
copy: () => getManager().copy(currentRanges()),
|
|
21396
|
+
cut: () => getManager().cut(currentRanges()),
|
|
21397
|
+
paste: () => {
|
|
21398
|
+
const target = pasteTarget();
|
|
21399
|
+
return target ? getManager().paste(target.row, target.col) : Promise.resolve();
|
|
21400
|
+
},
|
|
21401
|
+
});
|
|
21402
|
+
return { teardown: [() => domCleanups.forEach((fn) => fn())] };
|
|
21403
|
+
},
|
|
21404
|
+
};
|
|
21405
|
+
}
|
|
21406
|
+
|
|
21407
|
+
/**
|
|
21408
|
+
* UndoRedoManager - Undo/Redo functionality for grid operations
|
|
21409
|
+
*
|
|
21410
|
+
* Uses CommandStack from shared package for managing command history.
|
|
21411
|
+
* Supports:
|
|
21412
|
+
* - Cell value changes
|
|
21413
|
+
* - Filter changes
|
|
21414
|
+
* - Sort changes
|
|
21415
|
+
* - Selection changes
|
|
21416
|
+
* - Custom commands
|
|
21417
|
+
*/
|
|
21418
|
+
/**
|
|
21419
|
+
* Cell edit command
|
|
21420
|
+
*/
|
|
21421
|
+
class CellEditCommand {
|
|
21422
|
+
constructor(row, col, oldValue, newValue, setValue) {
|
|
21423
|
+
this.row = row;
|
|
21424
|
+
this.col = col;
|
|
21425
|
+
this.oldValue = oldValue;
|
|
21426
|
+
this.newValue = newValue;
|
|
21427
|
+
this.setValue = setValue;
|
|
21428
|
+
this.description = `Edit cell (${row}, ${col})`;
|
|
21429
|
+
}
|
|
21430
|
+
execute() {
|
|
21431
|
+
this.setValue(this.row, this.col, this.newValue);
|
|
21432
|
+
}
|
|
21433
|
+
undo() {
|
|
21434
|
+
this.setValue(this.row, this.col, this.oldValue);
|
|
21435
|
+
}
|
|
21436
|
+
redo() {
|
|
21437
|
+
this.execute();
|
|
21438
|
+
}
|
|
21439
|
+
}
|
|
21440
|
+
/**
|
|
21441
|
+
* Batch cell edit command (for multiple cells)
|
|
21442
|
+
*/
|
|
21443
|
+
class BatchCellEditCommand {
|
|
21444
|
+
constructor(edits, setValue) {
|
|
21445
|
+
this.edits = edits;
|
|
21446
|
+
this.setValue = setValue;
|
|
21447
|
+
this.description = `Edit ${edits.length} cells`;
|
|
21448
|
+
}
|
|
21449
|
+
execute() {
|
|
21450
|
+
for (const edit of this.edits) {
|
|
21451
|
+
this.setValue(edit.row, edit.col, edit.newValue);
|
|
21452
|
+
}
|
|
21453
|
+
}
|
|
21454
|
+
undo() {
|
|
21455
|
+
// Undo in reverse order
|
|
21456
|
+
for (let i = this.edits.length - 1; i >= 0; i--) {
|
|
21457
|
+
const edit = this.edits[i];
|
|
21458
|
+
this.setValue(edit.row, edit.col, edit.oldValue);
|
|
21459
|
+
}
|
|
21460
|
+
}
|
|
21461
|
+
redo() {
|
|
21462
|
+
this.execute();
|
|
21463
|
+
}
|
|
21464
|
+
}
|
|
21465
|
+
/**
|
|
21466
|
+
* Filter change command
|
|
21467
|
+
*/
|
|
21468
|
+
class FilterChangeCommand {
|
|
21469
|
+
constructor(oldFilters, newFilters, setFilters) {
|
|
21470
|
+
this.oldFilters = oldFilters;
|
|
21471
|
+
this.newFilters = newFilters;
|
|
21472
|
+
this.setFilters = setFilters;
|
|
21473
|
+
this.description = 'Change filters';
|
|
21474
|
+
}
|
|
20472
21475
|
execute() {
|
|
20473
21476
|
this.setFilters(this.newFilters);
|
|
20474
21477
|
}
|
|
@@ -20832,42 +21835,45 @@ function createDomPlugin(opts) {
|
|
|
20832
21835
|
store.extend('dom.canvas', null, 'dom', 5);
|
|
20833
21836
|
store.extend('dom.headerContainer', null, 'dom', 5);
|
|
20834
21837
|
store.action('dom:setup', () => {
|
|
21838
|
+
if (options.hideLastColumnBorder) {
|
|
21839
|
+
container.classList.add('zg-hide-last-col-border');
|
|
21840
|
+
}
|
|
20835
21841
|
// Create viewport
|
|
20836
21842
|
viewport = document.createElement('div');
|
|
20837
21843
|
viewport.className = 'zg-viewport';
|
|
20838
|
-
viewport.style.cssText = `
|
|
20839
|
-
position: relative;
|
|
20840
|
-
width: 100%;
|
|
20841
|
-
height: 100%;
|
|
20842
|
-
overflow: hidden;
|
|
21844
|
+
viewport.style.cssText = `
|
|
21845
|
+
position: relative;
|
|
21846
|
+
width: 100%;
|
|
21847
|
+
height: 100%;
|
|
21848
|
+
overflow: hidden;
|
|
20843
21849
|
`;
|
|
20844
21850
|
// Create scroll container
|
|
20845
21851
|
scrollContainer = document.createElement('div');
|
|
20846
21852
|
scrollContainer.className = 'zg-scroll-container';
|
|
20847
|
-
const headerHeight = options.columns && options.columns.length > 0 ?
|
|
20848
|
-
scrollContainer.style.cssText = `
|
|
20849
|
-
position: absolute;
|
|
20850
|
-
top: ${headerHeight}px;
|
|
20851
|
-
left: 0;
|
|
20852
|
-
right: 0;
|
|
20853
|
-
bottom: 0;
|
|
20854
|
-
overflow: auto;
|
|
21853
|
+
const headerHeight = options.columns && options.columns.length > 0 ? resolveHeaderHeight(options) : 0;
|
|
21854
|
+
scrollContainer.style.cssText = `
|
|
21855
|
+
position: absolute;
|
|
21856
|
+
top: ${headerHeight}px;
|
|
21857
|
+
left: 0;
|
|
21858
|
+
right: 0;
|
|
21859
|
+
bottom: 0;
|
|
21860
|
+
overflow: auto;
|
|
20855
21861
|
`;
|
|
20856
21862
|
// Create canvas
|
|
20857
21863
|
canvas = document.createElement('div');
|
|
20858
21864
|
canvas.className = 'zg-canvas';
|
|
20859
|
-
canvas.style.cssText = `
|
|
20860
|
-
position: relative;
|
|
20861
|
-
pointer-events: none;
|
|
21865
|
+
canvas.style.cssText = `
|
|
21866
|
+
position: relative;
|
|
21867
|
+
pointer-events: none;
|
|
20862
21868
|
`;
|
|
20863
21869
|
// Create cells container
|
|
20864
21870
|
const cellsContainer = document.createElement('div');
|
|
20865
21871
|
cellsContainer.className = 'zg-cells';
|
|
20866
|
-
cellsContainer.style.cssText = `
|
|
20867
|
-
position: absolute;
|
|
20868
|
-
top: 0;
|
|
20869
|
-
left: 0;
|
|
20870
|
-
pointer-events: auto;
|
|
21872
|
+
cellsContainer.style.cssText = `
|
|
21873
|
+
position: absolute;
|
|
21874
|
+
top: 0;
|
|
21875
|
+
left: 0;
|
|
21876
|
+
pointer-events: auto;
|
|
20871
21877
|
`;
|
|
20872
21878
|
// Create header container if columns defined
|
|
20873
21879
|
if (options.columns && options.columns.length > 0) {
|
|
@@ -22700,16 +23706,6 @@ function createDropdownTrigger(displayText) {
|
|
|
22700
23706
|
arrow.innerHTML = '▼';
|
|
22701
23707
|
arrow.setAttribute('aria-hidden', 'true');
|
|
22702
23708
|
trigger.appendChild(arrow);
|
|
22703
|
-
trigger.style.width = '100%';
|
|
22704
|
-
trigger.style.padding = '6px 8px';
|
|
22705
|
-
trigger.style.border = '1px solid #ccc';
|
|
22706
|
-
trigger.style.borderRadius = '4px';
|
|
22707
|
-
trigger.style.backgroundColor = '#fff';
|
|
22708
|
-
trigger.style.cursor = 'pointer';
|
|
22709
|
-
trigger.style.textAlign = 'left';
|
|
22710
|
-
trigger.style.display = 'flex';
|
|
22711
|
-
trigger.style.justifyContent = 'space-between';
|
|
22712
|
-
trigger.style.alignItems = 'center';
|
|
22713
23709
|
return trigger;
|
|
22714
23710
|
}
|
|
22715
23711
|
/**
|
|
@@ -22721,13 +23717,7 @@ function createDropdownMenu$1(maxHeight) {
|
|
|
22721
23717
|
menu.style.display = 'none';
|
|
22722
23718
|
menu.style.position = 'absolute';
|
|
22723
23719
|
menu.style.zIndex = '1000';
|
|
22724
|
-
menu.style.backgroundColor = '#fff';
|
|
22725
|
-
menu.style.border = '1px solid #ccc';
|
|
22726
|
-
menu.style.borderRadius = '4px';
|
|
22727
|
-
menu.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)';
|
|
22728
23720
|
menu.style.maxHeight = `${maxHeight}px`;
|
|
22729
|
-
menu.style.overflowY = 'auto';
|
|
22730
|
-
menu.style.minWidth = '200px';
|
|
22731
23721
|
return menu;
|
|
22732
23722
|
}
|
|
22733
23723
|
/**
|
|
@@ -22736,17 +23726,11 @@ function createDropdownMenu$1(maxHeight) {
|
|
|
22736
23726
|
function createSearchInput$1() {
|
|
22737
23727
|
const searchContainer = document.createElement('div');
|
|
22738
23728
|
searchContainer.className = 'zg-dropdown-search';
|
|
22739
|
-
searchContainer.style.padding = '8px';
|
|
22740
|
-
searchContainer.style.borderBottom = '1px solid #eee';
|
|
22741
23729
|
const searchInput = document.createElement('input');
|
|
22742
23730
|
searchInput.type = 'text';
|
|
22743
23731
|
searchInput.className = 'zg-dropdown-search-input';
|
|
22744
23732
|
searchInput.placeholder = 'Search...';
|
|
22745
23733
|
searchInput.setAttribute('aria-label', 'Search options');
|
|
22746
|
-
searchInput.style.width = '100%';
|
|
22747
|
-
searchInput.style.padding = '4px 8px';
|
|
22748
|
-
searchInput.style.border = '1px solid #ccc';
|
|
22749
|
-
searchInput.style.borderRadius = '3px';
|
|
22750
23734
|
searchContainer.appendChild(searchInput);
|
|
22751
23735
|
return searchContainer;
|
|
22752
23736
|
}
|
|
@@ -22785,11 +23769,6 @@ function createGroupHeader(groupName) {
|
|
|
22785
23769
|
groupHeader.className = 'zg-dropdown-group-header';
|
|
22786
23770
|
groupHeader.textContent = groupName;
|
|
22787
23771
|
groupHeader.setAttribute('role', 'presentation');
|
|
22788
|
-
groupHeader.style.padding = '8px 12px';
|
|
22789
|
-
groupHeader.style.fontSize = '12px';
|
|
22790
|
-
groupHeader.style.fontWeight = 'bold';
|
|
22791
|
-
groupHeader.style.color = '#666';
|
|
22792
|
-
groupHeader.style.backgroundColor = '#f5f5f5';
|
|
22793
23772
|
return groupHeader;
|
|
22794
23773
|
}
|
|
22795
23774
|
/**
|
|
@@ -22809,10 +23788,6 @@ function createOptionElement$1(option, isSelected, optionRenderer) {
|
|
|
22809
23788
|
if (option.disabled) {
|
|
22810
23789
|
optionEl.setAttribute('aria-disabled', 'true');
|
|
22811
23790
|
}
|
|
22812
|
-
optionEl.style.padding = '8px 12px';
|
|
22813
|
-
optionEl.style.cursor = option.disabled ? 'not-allowed' : 'pointer';
|
|
22814
|
-
optionEl.style.backgroundColor = isSelected ? '#e3f2fd' : '#fff';
|
|
22815
|
-
optionEl.style.color = option.disabled ? '#999' : '#333';
|
|
22816
23791
|
return optionEl;
|
|
22817
23792
|
}
|
|
22818
23793
|
/**
|
|
@@ -22822,9 +23797,6 @@ function createNoOptionsElement() {
|
|
|
22822
23797
|
const noOptions = document.createElement('div');
|
|
22823
23798
|
noOptions.className = 'zg-dropdown-no-options';
|
|
22824
23799
|
noOptions.textContent = 'No options available';
|
|
22825
|
-
noOptions.style.padding = '12px';
|
|
22826
|
-
noOptions.style.color = '#999';
|
|
22827
|
-
noOptions.style.textAlign = 'center';
|
|
22828
23800
|
return noOptions;
|
|
22829
23801
|
}
|
|
22830
23802
|
/**
|
|
@@ -23041,8 +24013,13 @@ class DropdownState {
|
|
|
23041
24013
|
viewportPadding: 8,
|
|
23042
24014
|
},
|
|
23043
24015
|
scrollOptions: {
|
|
24016
|
+
// Trigger lives in a virtualized cell. While the cell is on-screen, keep
|
|
24017
|
+
// the menu glued to it (reposition); the moment it scrolls out of the
|
|
24018
|
+
// viewport, close. closeOnOutOfView fires while the element is still in
|
|
24019
|
+
// the viewport, i.e. before the cell is recycled to another row, so the
|
|
24020
|
+
// menu never anchors to the wrong row.
|
|
23044
24021
|
closeOnOutOfView: true,
|
|
23045
|
-
repositionOnScroll:
|
|
24022
|
+
repositionOnScroll: true,
|
|
23046
24023
|
},
|
|
23047
24024
|
restoreFocusOnClose: true,
|
|
23048
24025
|
restoreFocusTo: trigger,
|
|
@@ -23413,6 +24390,8 @@ class DropdownEventManager {
|
|
|
23413
24390
|
*/
|
|
23414
24391
|
class DropdownRenderer {
|
|
23415
24392
|
constructor(options = {}) {
|
|
24393
|
+
/** Interactive: attaches listeners and portals its menu to <body>; must not be HTML-cached. */
|
|
24394
|
+
this.cacheable = false;
|
|
23416
24395
|
// Throw if options array is empty or missing
|
|
23417
24396
|
if (!options.options || options.options.length === 0) {
|
|
23418
24397
|
throw new Error('DropdownRenderer requires at least one option');
|
|
@@ -23619,8 +24598,16 @@ class LoadingIndicator {
|
|
|
23619
24598
|
clearTimeout(this.minDisplayTimeout);
|
|
23620
24599
|
this.minDisplayTimeout = null;
|
|
23621
24600
|
}
|
|
23622
|
-
// Remove existing indicator
|
|
23623
|
-
|
|
24601
|
+
// Remove the existing indicator immediately. A new load is starting, so we
|
|
24602
|
+
// replace the node now rather than letting hide()'s minDisplayTime defer
|
|
24603
|
+
// removal — deferral would orphan the old node in the DOM (a stuck overlay)
|
|
24604
|
+
// once this.container is reassigned below. Matters when rapid loads fire
|
|
24605
|
+
// interleaved start/end events (e.g. a backend filter firing several
|
|
24606
|
+
// requests in one tick).
|
|
24607
|
+
if (this.container?.parentElement) {
|
|
24608
|
+
this.container.parentElement.removeChild(this.container);
|
|
24609
|
+
}
|
|
24610
|
+
this.container = null;
|
|
23624
24611
|
// Create new indicator
|
|
23625
24612
|
this.container = this.render(state);
|
|
23626
24613
|
this.container.classList.add('zg-loading-active');
|
|
@@ -24012,7 +24999,10 @@ function createRenderingPlugin(opts) {
|
|
|
24012
24999
|
loadingIndicator = new LoadingIndicator(options.loading);
|
|
24013
25000
|
}
|
|
24014
25001
|
function mapRowToDataIndex(row) {
|
|
24015
|
-
|
|
25002
|
+
// getUnphased: rows.viewIndices is a phase-Infinity computed, but rendering
|
|
25003
|
+
// runs at phase 30 (and can be driven synchronously from the phase-30 sync
|
|
25004
|
+
// effect). A phased read here would trip checkPhase and abort the render.
|
|
25005
|
+
const viewIndices = store.getUnphased('rows.viewIndices');
|
|
24016
25006
|
if (viewIndices)
|
|
24017
25007
|
return viewIndices[row];
|
|
24018
25008
|
return row;
|
|
@@ -24243,15 +25233,18 @@ function createRenderingPlugin(opts) {
|
|
|
24243
25233
|
store.action('rendering:registerRenderer', (name, renderer) => {
|
|
24244
25234
|
registry.register(name, renderer);
|
|
24245
25235
|
}, 'rendering');
|
|
24246
|
-
//
|
|
25236
|
+
// Single-responsibility viewport sync: resize the virtual scroller +
|
|
25237
|
+
// canvas to the current pipeline view, drop stale cache, and re-render.
|
|
25238
|
+
// This is the one shared implementation — explicit callers (sort,
|
|
25239
|
+
// pagination, init) run it synchronously via the action below; the
|
|
25240
|
+
// effect is a reactive safety net for pipeline changes that don't route
|
|
25241
|
+
// through an explicit call (e.g. filter, programmatic sort/filter).
|
|
24247
25242
|
let lastViewIndicesRef;
|
|
24248
|
-
|
|
24249
|
-
const viewIndices = store.get('rows.viewIndices');
|
|
24250
|
-
if (viewIndices === lastViewIndicesRef)
|
|
24251
|
-
return;
|
|
24252
|
-
lastViewIndicesRef = viewIndices;
|
|
25243
|
+
function syncViewport() {
|
|
24253
25244
|
if (!scroller)
|
|
24254
25245
|
return;
|
|
25246
|
+
const viewIndices = store.getUnphased('rows.viewIndices');
|
|
25247
|
+
lastViewIndicesRef = viewIndices;
|
|
24255
25248
|
const visibleCount = viewIndices ? viewIndices.length : options.rowCount;
|
|
24256
25249
|
scroller.setRowCount(visibleCount);
|
|
24257
25250
|
const canvas = store.get('dom.canvas');
|
|
@@ -24261,8 +25254,23 @@ function createRenderingPlugin(opts) {
|
|
|
24261
25254
|
}
|
|
24262
25255
|
if (cache)
|
|
24263
25256
|
cache.clear();
|
|
24264
|
-
|
|
24265
|
-
|
|
25257
|
+
// Re-render against the freshly-clamped visible range with fresh data.
|
|
25258
|
+
// refresh() can't be used here: it reuses the previous range (out of
|
|
25259
|
+
// bounds when the page shrinks) and skips cells whose row/col key is
|
|
25260
|
+
// unchanged even though the underlying data changed (page switch).
|
|
25261
|
+
if (positioner) {
|
|
25262
|
+
positioner.clearRenderedCells();
|
|
25263
|
+
positioner.renderVisibleCells(state.scrollPosition.top, state.scrollPosition.left);
|
|
25264
|
+
}
|
|
25265
|
+
}
|
|
25266
|
+
store.action('rendering:syncAfterPipeline', syncViewport, 'rendering');
|
|
25267
|
+
store.effect('rendering:syncAfterPipeline:auto', () => {
|
|
25268
|
+
const viewIndices = store.getUnphased('rows.viewIndices');
|
|
25269
|
+
// Explicit callers advance lastViewIndicesRef, so this coalesces to a
|
|
25270
|
+
// no-op on the next frame and avoids a duplicate render.
|
|
25271
|
+
if (viewIndices === lastViewIndicesRef)
|
|
25272
|
+
return;
|
|
25273
|
+
syncViewport();
|
|
24266
25274
|
}, 'rendering', 30);
|
|
24267
25275
|
// Setup loading listeners (store unsubs for teardown)
|
|
24268
25276
|
const eventUnsubs = [];
|
|
@@ -24289,6 +25297,7 @@ function createRenderingPlugin(opts) {
|
|
|
24289
25297
|
clearCache: () => store.exec('rendering:clearCache'),
|
|
24290
25298
|
handleScroll: (top, left) => store.exec('rendering:handleScroll', top, left),
|
|
24291
25299
|
updateCanvasSize: () => store.exec('rendering:updateCanvasSize'),
|
|
25300
|
+
syncAfterPipeline: () => store.exec('rendering:syncAfterPipeline'),
|
|
24292
25301
|
setRowCount: (count) => store.exec('rendering:setRowCount', count),
|
|
24293
25302
|
setColCount: (count) => store.exec('rendering:setColCount', count),
|
|
24294
25303
|
registerRenderer: (name, renderer) => store.exec('rendering:registerRenderer', name, renderer),
|
|
@@ -25533,6 +26542,7 @@ class HeaderRendererRegistry {
|
|
|
25533
26542
|
* Register default header renderers
|
|
25534
26543
|
*/
|
|
25535
26544
|
function registerDefaultRenderers(registry) {
|
|
26545
|
+
registry.setDefaultRenderer(new TextHeaderRenderer());
|
|
25536
26546
|
registry.register('text', () => new TextHeaderRenderer());
|
|
25537
26547
|
registry.register('sortable', () => new SortableHeaderRenderer());
|
|
25538
26548
|
registry.register('filterable', () => new FilterableHeaderRenderer());
|
|
@@ -25706,8 +26716,17 @@ function renderHeaderCell(column, columnId, columnIndex, headerCellsContainer, r
|
|
|
25706
26716
|
const element = document.createElement('div');
|
|
25707
26717
|
element.dataset['columnId'] = columnId;
|
|
25708
26718
|
element.dataset['columnIndex'] = String(columnIndex);
|
|
26719
|
+
// Reserve the column width before rendering so renderers that don't size the
|
|
26720
|
+
// cell themselves (e.g. custom renderers) don't collapse under the flex row.
|
|
26721
|
+
applyHeaderCellSizing(element, params);
|
|
25709
26722
|
// Render
|
|
25710
26723
|
renderer.render(element, params);
|
|
26724
|
+
// Custom renderers only own render/update/destroy. For sortable columns whose
|
|
26725
|
+
// renderer does not wire sort itself, delegate the sort click so header clicks
|
|
26726
|
+
// still toggle sort (and sort-driven header state keeps updating).
|
|
26727
|
+
if (column.sortable && !renderer.handlesSortInteraction && params.emit) {
|
|
26728
|
+
attachSortClickFallback(element, params);
|
|
26729
|
+
}
|
|
25711
26730
|
// Add to container
|
|
25712
26731
|
headerCellsContainer.appendChild(element);
|
|
25713
26732
|
// Store metadata by column ID (for reactive updates)
|
|
@@ -25719,6 +26738,36 @@ function renderHeaderCell(column, columnId, columnIndex, headerCellsContainer, r
|
|
|
25719
26738
|
lastParams: params,
|
|
25720
26739
|
});
|
|
25721
26740
|
}
|
|
26741
|
+
/**
|
|
26742
|
+
* Apply the structural sizing every header cell needs (column width, header
|
|
26743
|
+
* height, no flex shrink, border-box). Renderers may still override these, but
|
|
26744
|
+
* this baseline keeps custom renderers from collapsing to zero width.
|
|
26745
|
+
*/
|
|
26746
|
+
function applyHeaderCellSizing(element, params) {
|
|
26747
|
+
element.style.width = `${params.width}px`;
|
|
26748
|
+
element.style.height = `${params.height}px`;
|
|
26749
|
+
element.style.flexShrink = '0';
|
|
26750
|
+
element.style.boxSizing = 'border-box';
|
|
26751
|
+
}
|
|
26752
|
+
/**
|
|
26753
|
+
* Attach a default sort-click handler for sortable columns whose renderer does
|
|
26754
|
+
* not handle the interaction itself. The element is freshly created per render
|
|
26755
|
+
* and discarded on refresh/destroy, so the listener needs no explicit teardown.
|
|
26756
|
+
*/
|
|
26757
|
+
function attachSortClickFallback(element, params) {
|
|
26758
|
+
element.style.cursor = 'pointer';
|
|
26759
|
+
element.addEventListener('click', (e) => {
|
|
26760
|
+
const native = e;
|
|
26761
|
+
params.emit?.('header:sort:click', {
|
|
26762
|
+
columnIndex: params.columnIndex,
|
|
26763
|
+
column: params.column,
|
|
26764
|
+
nextDirection: params.sortDirection === 'asc' ? 'desc' : params.sortDirection === 'desc' ? null : 'asc',
|
|
26765
|
+
additive: !!(native.shiftKey || native.ctrlKey || native.metaKey),
|
|
26766
|
+
trigger: 'click',
|
|
26767
|
+
nativeEvent: native,
|
|
26768
|
+
});
|
|
26769
|
+
});
|
|
26770
|
+
}
|
|
25722
26771
|
/**
|
|
25723
26772
|
* Destroy all headers
|
|
25724
26773
|
*/
|
|
@@ -25814,6 +26863,9 @@ function updateHeaderByColumnId(columnId, headerCells, columnModel, config, head
|
|
|
25814
26863
|
const filterState = config.getFilterState?.() ?? [];
|
|
25815
26864
|
// Update params
|
|
25816
26865
|
const params = buildRenderParams(metadata.lastParams.column, metadata.columnIndex, columnState.dataIndex, columnState.actualWidth, headerHeight, sortState, filterState, config.eventEmitter);
|
|
26866
|
+
// Keep the reserved width in sync (e.g. column resize) before the renderer
|
|
26867
|
+
// updates, so custom renderers that don't resize themselves stay correct.
|
|
26868
|
+
applyHeaderCellSizing(metadata.element, params);
|
|
25817
26869
|
// Update the renderer
|
|
25818
26870
|
metadata.renderer.update(metadata.element, params);
|
|
25819
26871
|
metadata.lastParams = params;
|
|
@@ -25896,13 +26948,13 @@ function createHeaderPlugin(opts) {
|
|
|
25896
26948
|
phase: 70,
|
|
25897
26949
|
dependencies: ['core', 'dom', 'column'],
|
|
25898
26950
|
setup(store, api) {
|
|
25899
|
-
const { events, getColumnModel } = opts;
|
|
26951
|
+
const { events, getColumnModel, options } = opts;
|
|
25900
26952
|
const registry = new HeaderRendererRegistry();
|
|
25901
26953
|
const headerCells = new Map();
|
|
25902
26954
|
let headerCellsContainer = null;
|
|
25903
26955
|
let columnSubscription = null;
|
|
25904
26956
|
const eventUnsubs = [];
|
|
25905
|
-
const headerHeight =
|
|
26957
|
+
const headerHeight = resolveHeaderHeight(options);
|
|
25906
26958
|
// Register default renderers
|
|
25907
26959
|
registerDefaultRenderers(registry);
|
|
25908
26960
|
function getSortState() {
|
|
@@ -26480,6 +27532,25 @@ function createPaginationPlugin(opts) {
|
|
|
26480
27532
|
store.extend('pagination.currentPage', 0, 'pagination', 80);
|
|
26481
27533
|
store.extend('pagination.pageSize', options.pagination.pageSize ?? 100, 'pagination', 80);
|
|
26482
27534
|
store.extend('pagination.totalPages', 0, 'pagination', 80);
|
|
27535
|
+
// Frontend pagination: slice the post-sort/filter view down to the
|
|
27536
|
+
// current page so virtual scrolling only renders that page. Backend
|
|
27537
|
+
// pagination is sliced by the data plugin, so no pipeline phase there.
|
|
27538
|
+
const isFrontend = resolveQueryExecutionMode(options.dataMode) !== 'backend';
|
|
27539
|
+
if (isFrontend) {
|
|
27540
|
+
store.computed('pipeline.pagination', () => {
|
|
27541
|
+
const upstream = store.getUnphased('pipeline.filter') ??
|
|
27542
|
+
store.getUnphased('pipeline.sort') ??
|
|
27543
|
+
store.getUnphased('rows.indices');
|
|
27544
|
+
if (!upstream)
|
|
27545
|
+
return undefined;
|
|
27546
|
+
const pageSize = store.getUnphased('pagination.pageSize');
|
|
27547
|
+
if (!Number.isFinite(pageSize) || pageSize <= 0)
|
|
27548
|
+
return upstream;
|
|
27549
|
+
const currentPage = store.getUnphased('pagination.currentPage');
|
|
27550
|
+
const start = Math.max(0, currentPage) * pageSize;
|
|
27551
|
+
return upstream.slice(start, start + pageSize);
|
|
27552
|
+
}, 'pagination', 30);
|
|
27553
|
+
}
|
|
26483
27554
|
let paginationTop = null;
|
|
26484
27555
|
let paginationBottom = null;
|
|
26485
27556
|
function getTotalPages() {
|
|
@@ -26546,6 +27617,7 @@ function createPaginationPlugin(opts) {
|
|
|
26546
27617
|
return;
|
|
26547
27618
|
store.set('pagination.currentPage', page);
|
|
26548
27619
|
updatePaginationUI();
|
|
27620
|
+
store.exec('rendering:syncAfterPipeline');
|
|
26549
27621
|
if (options.onPageChange) {
|
|
26550
27622
|
options.onPageChange(page, store.get('pagination.pageSize'));
|
|
26551
27623
|
}
|
|
@@ -26578,6 +27650,7 @@ function createPaginationPlugin(opts) {
|
|
|
26578
27650
|
store.set('pagination.pageSize', pageSize);
|
|
26579
27651
|
store.set('pagination.currentPage', newPage);
|
|
26580
27652
|
updatePaginationUI();
|
|
27653
|
+
store.exec('rendering:syncAfterPipeline');
|
|
26581
27654
|
if (options.onPageChange) {
|
|
26582
27655
|
options.onPageChange(store.get('pagination.currentPage'), pageSize);
|
|
26583
27656
|
}
|
|
@@ -26753,7 +27826,7 @@ function createColumnPlugin(opts) {
|
|
|
26753
27826
|
showPreview: options.columnResize?.showPreview,
|
|
26754
27827
|
onColumnWidthsChange: options.onColumnWidthsChange,
|
|
26755
27828
|
getScrollLeft: () => scrollContainer?.scrollLeft ?? 0,
|
|
26756
|
-
getHeaderHeight: () =>
|
|
27829
|
+
getHeaderHeight: () => resolveHeaderHeight(options),
|
|
26757
27830
|
getViewportHeight: () => scrollContainer?.clientHeight ?? 0,
|
|
26758
27831
|
});
|
|
26759
27832
|
}, 'column');
|
|
@@ -26931,13 +28004,13 @@ function createColumnPlugin(opts) {
|
|
|
26931
28004
|
function measureHeaderTextWidth(text) {
|
|
26932
28005
|
if (!headerMeasureElement) {
|
|
26933
28006
|
headerMeasureElement = document.createElement('div');
|
|
26934
|
-
headerMeasureElement.style.cssText = `
|
|
26935
|
-
position: absolute;
|
|
26936
|
-
visibility: hidden;
|
|
26937
|
-
white-space: nowrap;
|
|
26938
|
-
font-family: inherit;
|
|
26939
|
-
font-size: inherit;
|
|
26940
|
-
font-weight: 600;
|
|
28007
|
+
headerMeasureElement.style.cssText = `
|
|
28008
|
+
position: absolute;
|
|
28009
|
+
visibility: hidden;
|
|
28010
|
+
white-space: nowrap;
|
|
28011
|
+
font-family: inherit;
|
|
28012
|
+
font-size: inherit;
|
|
28013
|
+
font-weight: 600;
|
|
26941
28014
|
`;
|
|
26942
28015
|
document.body.appendChild(headerMeasureElement);
|
|
26943
28016
|
}
|
|
@@ -27019,391 +28092,192 @@ function createColumnPlugin(opts) {
|
|
|
27019
28092
|
};
|
|
27020
28093
|
}
|
|
27021
28094
|
|
|
27022
|
-
|
|
27023
|
-
{
|
|
27024
|
-
|
|
27025
|
-
|
|
27026
|
-
|
|
27027
|
-
|
|
27028
|
-
|
|
27029
|
-
|
|
27030
|
-
|
|
27031
|
-
];
|
|
27032
|
-
const
|
|
27033
|
-
|
|
27034
|
-
|
|
27035
|
-
|
|
27036
|
-
{ value: 'greaterThanOrEqual', label: 'Greater than or equal', valueCount: 1 },
|
|
27037
|
-
{ value: 'lessThan', label: 'Less than', valueCount: 1 },
|
|
27038
|
-
{ value: 'lessThanOrEqual', label: 'Less than or equal', valueCount: 1 },
|
|
27039
|
-
{ value: 'between', label: 'Between', valueCount: 2 },
|
|
27040
|
-
{ value: 'blank', label: 'Is blank', valueCount: 0 },
|
|
27041
|
-
{ value: 'notBlank', label: 'Is not blank', valueCount: 0 },
|
|
27042
|
-
];
|
|
27043
|
-
const DATE_OPERATORS = [
|
|
27044
|
-
{ value: 'equals', label: 'Equals', valueCount: 1 },
|
|
27045
|
-
{ value: 'greaterThan', label: 'After', valueCount: 1 },
|
|
27046
|
-
{ value: 'lessThan', label: 'Before', valueCount: 1 },
|
|
27047
|
-
{ value: 'between', label: 'Between', valueCount: 2 },
|
|
27048
|
-
{ value: 'blank', label: 'Is blank', valueCount: 0 },
|
|
27049
|
-
{ value: 'notBlank', label: 'Is not blank', valueCount: 0 },
|
|
27050
|
-
];
|
|
27051
|
-
class GridFilterUI {
|
|
27052
|
-
constructor(options) {
|
|
27053
|
-
this.popup = null;
|
|
27054
|
-
this.chipBar = null;
|
|
27055
|
-
this.popupKeyHandler = null;
|
|
27056
|
-
this.filterClickUnsub = null;
|
|
27057
|
-
this.filterChangeUnsub = null;
|
|
27058
|
-
this.closePopup = () => {
|
|
27059
|
-
const popup = this.popup;
|
|
27060
|
-
if (!popup)
|
|
27061
|
-
return;
|
|
27062
|
-
if (this.overlayManager.isOpen(popup)) {
|
|
27063
|
-
this.overlayManager.close(popup);
|
|
27064
|
-
return;
|
|
27065
|
-
}
|
|
27066
|
-
this.teardownPopup();
|
|
27067
|
-
};
|
|
27068
|
-
this.container = options.container;
|
|
27069
|
-
this.events = options.events;
|
|
27070
|
-
this.columns = options.columns ?? [];
|
|
27071
|
-
this.getFilterState = options.getFilterState;
|
|
27072
|
-
this.getFilterExpression = options.getFilterExpression;
|
|
27073
|
-
this.setColumnFilter = options.setColumnFilter;
|
|
27074
|
-
this.setFieldState = options.setFieldState;
|
|
27075
|
-
this.clearColumnFilter = options.clearColumnFilter;
|
|
27076
|
-
this.clearQuickFilter = options.clearQuickFilter;
|
|
27077
|
-
this.clearAllFilters = options.clearAllFilters;
|
|
27078
|
-
this.getColumnDef = options.getColumnDef;
|
|
27079
|
-
this.mapVisualToDataCol = options.mapVisualToDataCol;
|
|
27080
|
-
this.overlayManager = OverlayManager.getInstance();
|
|
27081
|
-
this.filterClickUnsub = this.events.on('header:filter:click', (payload) => {
|
|
27082
|
-
this.openFilterPopup(payload);
|
|
27083
|
-
});
|
|
27084
|
-
this.filterChangeUnsub = this.events.on('filter:change', () => {
|
|
27085
|
-
this.renderFilterChips();
|
|
27086
|
-
});
|
|
27087
|
-
this.renderFilterChips();
|
|
27088
|
-
}
|
|
27089
|
-
setColumns(columns) {
|
|
27090
|
-
this.columns = [...columns];
|
|
27091
|
-
this.closePopup();
|
|
27092
|
-
this.renderFilterChips();
|
|
27093
|
-
}
|
|
27094
|
-
openFilterPopup(payload) {
|
|
27095
|
-
const dataCol = this.mapVisualToDataCol(payload.columnIndex);
|
|
27096
|
-
const columnDef = this.getColumnDef(dataCol);
|
|
27097
|
-
if (!columnDef)
|
|
27098
|
-
return;
|
|
27099
|
-
this.closePopup();
|
|
27100
|
-
const dropdownType = payload.dropdownType ?? 'text';
|
|
27101
|
-
const operators = this.getOperatorsForType(dropdownType);
|
|
27102
|
-
const existing = this.getFilterState().find((f) => f.column === dataCol);
|
|
27103
|
-
const popup = document.createElement('div');
|
|
27104
|
-
popup.className = 'zg-filter-popup';
|
|
27105
|
-
const title = document.createElement('div');
|
|
27106
|
-
title.className = 'zg-filter-popup__title';
|
|
27107
|
-
title.textContent = `Filter: ${this.getColumnLabel(columnDef, dataCol)}`;
|
|
27108
|
-
popup.appendChild(title);
|
|
27109
|
-
const logicRow = document.createElement('div');
|
|
27110
|
-
logicRow.className = 'zg-filter-popup__row';
|
|
27111
|
-
logicRow.innerHTML = `<label>Match</label>`;
|
|
27112
|
-
const logicSelect = document.createElement('select');
|
|
27113
|
-
logicSelect.className = 'zg-filter-popup__select';
|
|
27114
|
-
logicSelect.innerHTML = `
|
|
27115
|
-
<option value="AND">AND</option>
|
|
27116
|
-
<option value="OR">OR</option>
|
|
27117
|
-
`;
|
|
27118
|
-
logicSelect.value = existing?.logic ?? 'AND';
|
|
27119
|
-
logicRow.appendChild(logicSelect);
|
|
27120
|
-
popup.appendChild(logicRow);
|
|
27121
|
-
const conditionsContainer = document.createElement('div');
|
|
27122
|
-
conditionsContainer.className = 'zg-filter-popup__conditions';
|
|
27123
|
-
popup.appendChild(conditionsContainer);
|
|
27124
|
-
const conditionRows = [];
|
|
27125
|
-
const createConditionRow = (condition) => {
|
|
27126
|
-
const conditionEl = document.createElement('div');
|
|
27127
|
-
conditionEl.className = 'zg-filter-popup__condition';
|
|
27128
|
-
const operatorRow = document.createElement('div');
|
|
27129
|
-
operatorRow.className = 'zg-filter-popup__row';
|
|
27130
|
-
operatorRow.innerHTML = `<label>Operator</label>`;
|
|
27131
|
-
const operatorSelect = document.createElement('select');
|
|
27132
|
-
operatorSelect.className = 'zg-filter-popup__select';
|
|
27133
|
-
operators.forEach((op) => {
|
|
27134
|
-
const option = document.createElement('option');
|
|
27135
|
-
option.value = op.value;
|
|
27136
|
-
option.textContent = op.label;
|
|
27137
|
-
operatorSelect.appendChild(option);
|
|
27138
|
-
});
|
|
27139
|
-
operatorRow.appendChild(operatorSelect);
|
|
27140
|
-
conditionEl.appendChild(operatorRow);
|
|
27141
|
-
const valueRow = document.createElement('div');
|
|
27142
|
-
valueRow.className = 'zg-filter-popup__row';
|
|
27143
|
-
valueRow.innerHTML = `<label>Value</label>`;
|
|
27144
|
-
const valueInput = document.createElement('input');
|
|
27145
|
-
valueInput.className = 'zg-filter-popup__input';
|
|
27146
|
-
valueInput.type =
|
|
27147
|
-
dropdownType === 'number' ? 'number' : dropdownType === 'date' ? 'date' : 'text';
|
|
27148
|
-
const valueInputTo = document.createElement('input');
|
|
27149
|
-
valueInputTo.className = 'zg-filter-popup__input';
|
|
27150
|
-
valueInputTo.type = valueInput.type;
|
|
27151
|
-
valueInputTo.style.display = 'none';
|
|
27152
|
-
valueRow.appendChild(valueInput);
|
|
27153
|
-
valueRow.appendChild(valueInputTo);
|
|
27154
|
-
conditionEl.appendChild(valueRow);
|
|
27155
|
-
const actionsRow = document.createElement('div');
|
|
27156
|
-
actionsRow.className = 'zg-filter-popup__condition-actions';
|
|
27157
|
-
const removeBtn = document.createElement('button');
|
|
27158
|
-
removeBtn.type = 'button';
|
|
27159
|
-
removeBtn.className = 'zg-filter-popup__btn zg-filter-popup__btn--remove';
|
|
27160
|
-
removeBtn.textContent = 'Remove';
|
|
27161
|
-
actionsRow.appendChild(removeBtn);
|
|
27162
|
-
conditionEl.appendChild(actionsRow);
|
|
27163
|
-
const updateValueInputs = () => {
|
|
27164
|
-
const selected = operators.find((op) => op.value === operatorSelect.value);
|
|
27165
|
-
const valueCount = selected?.valueCount ?? 1;
|
|
27166
|
-
if (valueCount === 0) {
|
|
27167
|
-
valueInput.style.display = 'none';
|
|
27168
|
-
valueInputTo.style.display = 'none';
|
|
27169
|
-
}
|
|
27170
|
-
else if (valueCount === 2) {
|
|
27171
|
-
valueInput.style.display = '';
|
|
27172
|
-
valueInputTo.style.display = '';
|
|
27173
|
-
}
|
|
27174
|
-
else {
|
|
27175
|
-
valueInput.style.display = '';
|
|
27176
|
-
valueInputTo.style.display = 'none';
|
|
27177
|
-
}
|
|
27178
|
-
};
|
|
27179
|
-
operatorSelect.addEventListener('change', updateValueInputs);
|
|
27180
|
-
if (condition) {
|
|
27181
|
-
operatorSelect.value = condition.operator;
|
|
27182
|
-
if (Array.isArray(condition.value)) {
|
|
27183
|
-
valueInput.value = String(condition.value[0] ?? '');
|
|
27184
|
-
valueInputTo.value = String(condition.value[1] ?? '');
|
|
27185
|
-
}
|
|
27186
|
-
else if (condition.value != null) {
|
|
27187
|
-
valueInput.value = String(condition.value);
|
|
27188
|
-
}
|
|
27189
|
-
}
|
|
27190
|
-
updateValueInputs();
|
|
27191
|
-
removeBtn.addEventListener('click', () => {
|
|
27192
|
-
const index = conditionRows.findIndex((row) => row.element === conditionEl);
|
|
27193
|
-
if (index >= 0) {
|
|
27194
|
-
conditionRows.splice(index, 1);
|
|
27195
|
-
}
|
|
27196
|
-
conditionEl.remove();
|
|
27197
|
-
updateConditionControls();
|
|
27198
|
-
});
|
|
27199
|
-
return {
|
|
27200
|
-
element: conditionEl,
|
|
27201
|
-
operatorSelect,
|
|
27202
|
-
valueInput,
|
|
27203
|
-
valueInputTo,
|
|
27204
|
-
removeBtn,
|
|
27205
|
-
};
|
|
27206
|
-
};
|
|
27207
|
-
const updateConditionControls = () => {
|
|
27208
|
-
const showLogic = conditionRows.length > 1;
|
|
27209
|
-
logicRow.style.display = showLogic ? '' : 'none';
|
|
27210
|
-
conditionRows.forEach((row) => {
|
|
27211
|
-
row.removeBtn.style.display = showLogic ? '' : 'none';
|
|
27212
|
-
});
|
|
27213
|
-
};
|
|
27214
|
-
const addCondition = (condition) => {
|
|
27215
|
-
const row = createConditionRow(condition);
|
|
27216
|
-
conditionRows.push(row);
|
|
27217
|
-
conditionsContainer.appendChild(row.element);
|
|
27218
|
-
updateConditionControls();
|
|
27219
|
-
};
|
|
27220
|
-
const existingConditions = existing?.conditions && existing.conditions.length > 0 ? existing.conditions : null;
|
|
27221
|
-
if (existingConditions) {
|
|
27222
|
-
existingConditions.forEach((condition) => addCondition(condition));
|
|
27223
|
-
}
|
|
27224
|
-
else {
|
|
27225
|
-
addCondition();
|
|
27226
|
-
}
|
|
27227
|
-
const addConditionBtn = document.createElement('button');
|
|
27228
|
-
addConditionBtn.type = 'button';
|
|
27229
|
-
addConditionBtn.className = 'zg-filter-popup__btn zg-filter-popup__btn--add';
|
|
27230
|
-
addConditionBtn.textContent = 'Add condition';
|
|
27231
|
-
addConditionBtn.addEventListener('click', () => addCondition());
|
|
27232
|
-
popup.appendChild(addConditionBtn);
|
|
27233
|
-
const actions = document.createElement('div');
|
|
27234
|
-
actions.className = 'zg-filter-popup__actions';
|
|
27235
|
-
const applyBtn = document.createElement('button');
|
|
27236
|
-
applyBtn.type = 'button';
|
|
27237
|
-
applyBtn.className = 'zg-filter-popup__btn primary';
|
|
27238
|
-
applyBtn.textContent = 'Apply';
|
|
27239
|
-
const clearBtn = document.createElement('button');
|
|
27240
|
-
clearBtn.type = 'button';
|
|
27241
|
-
clearBtn.className = 'zg-filter-popup__btn';
|
|
27242
|
-
clearBtn.textContent = 'Clear';
|
|
27243
|
-
actions.appendChild(applyBtn);
|
|
27244
|
-
actions.appendChild(clearBtn);
|
|
27245
|
-
popup.appendChild(actions);
|
|
27246
|
-
const applyFilter = () => {
|
|
27247
|
-
const conditions = [];
|
|
27248
|
-
for (const row of conditionRows) {
|
|
27249
|
-
const operator = row.operatorSelect.value;
|
|
27250
|
-
const condition = this.buildCondition(operator, dropdownType, row.valueInput.value, row.valueInputTo.value);
|
|
27251
|
-
if (condition) {
|
|
27252
|
-
conditions.push(condition);
|
|
27253
|
-
}
|
|
27254
|
-
}
|
|
27255
|
-
if (conditions.length === 0) {
|
|
27256
|
-
return;
|
|
27257
|
-
}
|
|
27258
|
-
const logic = logicSelect.value ?? 'AND';
|
|
27259
|
-
this.setColumnFilter(dataCol, conditions, logic);
|
|
27260
|
-
this.closePopup();
|
|
27261
|
-
};
|
|
27262
|
-
applyBtn.addEventListener('click', applyFilter);
|
|
27263
|
-
clearBtn.addEventListener('click', () => {
|
|
27264
|
-
this.clearColumnFilter(dataCol);
|
|
27265
|
-
this.closePopup();
|
|
27266
|
-
});
|
|
27267
|
-
this.popup = popup;
|
|
27268
|
-
document.body.appendChild(popup);
|
|
27269
|
-
this.popupKeyHandler = (e) => {
|
|
27270
|
-
if (e.key === 'Enter') {
|
|
27271
|
-
const active = document.activeElement;
|
|
27272
|
-
if (!active || !popup.contains(active)) {
|
|
27273
|
-
return;
|
|
27274
|
-
}
|
|
27275
|
-
const tag = active.tagName.toLowerCase();
|
|
27276
|
-
if (tag === 'input' || tag === 'select') {
|
|
27277
|
-
e.preventDefault();
|
|
27278
|
-
applyFilter();
|
|
27279
|
-
}
|
|
27280
|
-
}
|
|
27281
|
-
};
|
|
27282
|
-
popup.addEventListener('keydown', this.popupKeyHandler);
|
|
27283
|
-
if (!payload.anchorElement) {
|
|
27284
|
-
const containerRect = this.container.getBoundingClientRect();
|
|
27285
|
-
popup.style.position = 'fixed';
|
|
27286
|
-
popup.style.top = `${containerRect.top + 12}px`;
|
|
27287
|
-
popup.style.left = `${containerRect.left + 12}px`;
|
|
27288
|
-
}
|
|
27289
|
-
this.overlayManager.open({
|
|
27290
|
-
overlay: popup,
|
|
27291
|
-
anchor: payload.anchorElement,
|
|
27292
|
-
additionalElements: payload.anchorElement ? [payload.anchorElement] : [],
|
|
27293
|
-
restoreFocusOnClose: !!payload.anchorElement,
|
|
27294
|
-
restoreFocusTo: payload.anchorElement ?? null,
|
|
27295
|
-
closeOnEscape: true,
|
|
27296
|
-
positionOptions: {
|
|
27297
|
-
placement: 'bottom-start',
|
|
27298
|
-
offset: 4,
|
|
27299
|
-
viewportPadding: 8,
|
|
27300
|
-
},
|
|
27301
|
-
scrollOptions: {
|
|
27302
|
-
closeOnOutOfView: true,
|
|
27303
|
-
repositionOnScroll: false,
|
|
27304
|
-
},
|
|
27305
|
-
onClose: () => {
|
|
27306
|
-
this.teardownPopup();
|
|
27307
|
-
},
|
|
27308
|
-
});
|
|
27309
|
-
// Focus first input for usability
|
|
27310
|
-
setTimeout(() => {
|
|
27311
|
-
const first = conditionRows[0];
|
|
27312
|
-
if (!first)
|
|
27313
|
-
return;
|
|
27314
|
-
if (first.valueInput.style.display !== 'none') {
|
|
27315
|
-
first.valueInput.focus();
|
|
27316
|
-
}
|
|
27317
|
-
else {
|
|
27318
|
-
first.operatorSelect.focus();
|
|
27319
|
-
}
|
|
27320
|
-
}, 0);
|
|
27321
|
-
}
|
|
27322
|
-
buildCondition(operator, dropdownType, value, valueTo) {
|
|
27323
|
-
const valueCount = this.getOperatorValueCount(operator, dropdownType);
|
|
27324
|
-
if (valueCount === 0) {
|
|
27325
|
-
return { operator };
|
|
27326
|
-
}
|
|
27327
|
-
if (valueCount === 2) {
|
|
27328
|
-
const parsed = [
|
|
27329
|
-
this.parseInputValue(value, dropdownType),
|
|
27330
|
-
this.parseInputValue(valueTo, dropdownType),
|
|
27331
|
-
];
|
|
27332
|
-
return { operator, value: parsed };
|
|
27333
|
-
}
|
|
27334
|
-
return { operator, value: this.parseInputValue(value, dropdownType) };
|
|
27335
|
-
}
|
|
27336
|
-
parseInputValue(value, dropdownType) {
|
|
27337
|
-
if (dropdownType === 'number') {
|
|
27338
|
-
const num = Number(value);
|
|
27339
|
-
return Number.isNaN(num) ? value : num;
|
|
27340
|
-
}
|
|
27341
|
-
if (dropdownType === 'date') {
|
|
27342
|
-
const ts = Date.parse(value);
|
|
27343
|
-
return Number.isNaN(ts) ? value : ts;
|
|
28095
|
+
function createFieldState(root) {
|
|
28096
|
+
return {
|
|
28097
|
+
root,
|
|
28098
|
+
activeFields: collectActiveFields(root),
|
|
28099
|
+
timestamp: Date.now(),
|
|
28100
|
+
};
|
|
28101
|
+
}
|
|
28102
|
+
function collectActiveFields(root) {
|
|
28103
|
+
if (!root)
|
|
28104
|
+
return [];
|
|
28105
|
+
const fields = new Set();
|
|
28106
|
+
const visit = (group) => {
|
|
28107
|
+
for (const condition of group.conditions) {
|
|
28108
|
+
fields.add(condition.field);
|
|
27344
28109
|
}
|
|
27345
|
-
|
|
27346
|
-
|
|
27347
|
-
getOperatorsForType(type) {
|
|
27348
|
-
switch (type) {
|
|
27349
|
-
case 'number':
|
|
27350
|
-
return NUMBER_OPERATORS;
|
|
27351
|
-
case 'date':
|
|
27352
|
-
return DATE_OPERATORS;
|
|
27353
|
-
default:
|
|
27354
|
-
return TEXT_OPERATORS;
|
|
28110
|
+
for (const nested of group.groups ?? []) {
|
|
28111
|
+
visit(nested);
|
|
27355
28112
|
}
|
|
28113
|
+
};
|
|
28114
|
+
visit(root);
|
|
28115
|
+
return Array.from(fields);
|
|
28116
|
+
}
|
|
28117
|
+
function removeTopLevelCondition(root, conditionIndex) {
|
|
28118
|
+
if (!root || root.logic !== 'AND')
|
|
28119
|
+
return null;
|
|
28120
|
+
return normalizeFieldGroup({
|
|
28121
|
+
logic: root.logic,
|
|
28122
|
+
conditions: root.conditions.filter((_, index) => index !== conditionIndex),
|
|
28123
|
+
groups: root.groups ? [...root.groups] : undefined,
|
|
28124
|
+
});
|
|
28125
|
+
}
|
|
28126
|
+
function removeTopLevelGroup(root, groupIndex) {
|
|
28127
|
+
if (!root || root.logic !== 'AND')
|
|
28128
|
+
return null;
|
|
28129
|
+
return normalizeFieldGroup({
|
|
28130
|
+
logic: root.logic,
|
|
28131
|
+
conditions: [...root.conditions],
|
|
28132
|
+
groups: root.groups?.filter((_, index) => index !== groupIndex),
|
|
28133
|
+
});
|
|
28134
|
+
}
|
|
28135
|
+
function normalizeFieldGroup(group) {
|
|
28136
|
+
const normalizedGroups = group.groups?.filter((nested) => nested.conditions.length > 0 || (nested.groups?.length ?? 0) > 0);
|
|
28137
|
+
if (group.conditions.length === 0 && (normalizedGroups?.length ?? 0) === 0) {
|
|
28138
|
+
return null;
|
|
27356
28139
|
}
|
|
27357
|
-
|
|
27358
|
-
|
|
27359
|
-
|
|
27360
|
-
|
|
27361
|
-
|
|
27362
|
-
|
|
27363
|
-
|
|
27364
|
-
|
|
28140
|
+
return {
|
|
28141
|
+
logic: group.logic,
|
|
28142
|
+
conditions: [...group.conditions],
|
|
28143
|
+
groups: normalizedGroups && normalizedGroups.length > 0 ? normalizedGroups : undefined,
|
|
28144
|
+
};
|
|
28145
|
+
}
|
|
28146
|
+
|
|
28147
|
+
function buildChipDescriptors(expression, labels, actions) {
|
|
28148
|
+
const chips = [];
|
|
28149
|
+
const fieldRoot = expression?.fieldState?.root ?? null;
|
|
28150
|
+
const quickFilter = expression?.quickFilter ?? null;
|
|
28151
|
+
if (fieldRoot) {
|
|
28152
|
+
chips.push(...buildFieldStateChips(fieldRoot, expression, labels, actions));
|
|
28153
|
+
}
|
|
28154
|
+
if (quickFilter?.query?.trim()) {
|
|
28155
|
+
chips.push({
|
|
28156
|
+
label: describeQuickFilter(quickFilter, labels),
|
|
28157
|
+
remove: () => actions.clearQuickFilter(),
|
|
28158
|
+
});
|
|
27365
28159
|
}
|
|
27366
|
-
|
|
27367
|
-
|
|
27368
|
-
|
|
27369
|
-
|
|
27370
|
-
}
|
|
27371
|
-
return this.getColumnLabel(this.columns[index], index);
|
|
28160
|
+
return chips;
|
|
28161
|
+
}
|
|
28162
|
+
function buildFieldStateChips(root, expression, labels, actions) {
|
|
28163
|
+
if (root.logic !== 'AND') {
|
|
28164
|
+
return [{ label: describeFieldGroup(root, labels), remove: () => actions.applyStructuredRoot(null) }];
|
|
27372
28165
|
}
|
|
27373
|
-
|
|
27374
|
-
|
|
27375
|
-
|
|
27376
|
-
|
|
27377
|
-
|
|
27378
|
-
|
|
28166
|
+
const chips = [];
|
|
28167
|
+
root.conditions.forEach((condition, index) => {
|
|
28168
|
+
chips.push({
|
|
28169
|
+
label: describeFieldCondition(condition, labels),
|
|
28170
|
+
remove: () => actions.applyStructuredRoot(removeTopLevelCondition(expression.fieldState?.root, index)),
|
|
28171
|
+
});
|
|
28172
|
+
});
|
|
28173
|
+
(root.groups ?? []).forEach((group, index) => {
|
|
28174
|
+
chips.push({
|
|
28175
|
+
label: describeFieldGroup(group, labels),
|
|
28176
|
+
remove: () => actions.applyStructuredRoot(removeTopLevelGroup(expression.fieldState?.root, index)),
|
|
28177
|
+
});
|
|
28178
|
+
});
|
|
28179
|
+
return chips;
|
|
28180
|
+
}
|
|
28181
|
+
function describeQuickFilter(quickFilter, labels) {
|
|
28182
|
+
const query = formatValue(quickFilter.query);
|
|
28183
|
+
if (!quickFilter.columns || quickFilter.columns.length === 0) {
|
|
28184
|
+
return `Search ${query}`;
|
|
27379
28185
|
}
|
|
27380
|
-
|
|
27381
|
-
|
|
27382
|
-
|
|
27383
|
-
|
|
27384
|
-
|
|
27385
|
-
|
|
27386
|
-
|
|
27387
|
-
|
|
27388
|
-
|
|
27389
|
-
|
|
27390
|
-
|
|
28186
|
+
const columnLabels = quickFilter.columns.map((column) => labels.getColumnLabelByIndex(column));
|
|
28187
|
+
return `Search ${columnLabels.join(', ')} ${query}`;
|
|
28188
|
+
}
|
|
28189
|
+
function describeFieldGroup(group, labels, nested = false) {
|
|
28190
|
+
const parts = [
|
|
28191
|
+
...group.conditions.map((condition) => describeFieldCondition(condition, labels)),
|
|
28192
|
+
...(group.groups ?? []).map((child) => describeFieldGroup(child, labels, true)),
|
|
28193
|
+
];
|
|
28194
|
+
if (parts.length === 0)
|
|
28195
|
+
return 'Filter group';
|
|
28196
|
+
const joined = parts.join(` ${group.logic} `);
|
|
28197
|
+
return nested && parts.length > 1 ? `(${joined})` : joined;
|
|
28198
|
+
}
|
|
28199
|
+
function describeFieldCondition(condition, labels) {
|
|
28200
|
+
const label = labels.getColumnLabelByField(condition.field);
|
|
28201
|
+
switch (condition.operator) {
|
|
28202
|
+
case 'eq':
|
|
28203
|
+
case 'equals':
|
|
28204
|
+
return `${label} = ${formatValue(condition.value)}`;
|
|
28205
|
+
case 'neq':
|
|
28206
|
+
case 'notEquals':
|
|
28207
|
+
return `${label} != ${formatValue(condition.value)}`;
|
|
28208
|
+
case 'contains':
|
|
28209
|
+
return `${label} contains ${formatValue(condition.value)}`;
|
|
28210
|
+
case 'notContains':
|
|
28211
|
+
return `${label} does not contain ${formatValue(condition.value)}`;
|
|
28212
|
+
case 'startsWith':
|
|
28213
|
+
return `${label} starts with ${formatValue(condition.value)}`;
|
|
28214
|
+
case 'endsWith':
|
|
28215
|
+
return `${label} ends with ${formatValue(condition.value)}`;
|
|
28216
|
+
case 'gt':
|
|
28217
|
+
case 'greaterThan':
|
|
28218
|
+
return `${label} > ${formatValue(condition.value)}`;
|
|
28219
|
+
case 'gte':
|
|
28220
|
+
case 'greaterThanOrEqual':
|
|
28221
|
+
return `${label} >= ${formatValue(condition.value)}`;
|
|
28222
|
+
case 'lt':
|
|
28223
|
+
case 'lessThan':
|
|
28224
|
+
return `${label} < ${formatValue(condition.value)}`;
|
|
28225
|
+
case 'lte':
|
|
28226
|
+
case 'lessThanOrEqual':
|
|
28227
|
+
return `${label} <= ${formatValue(condition.value)}`;
|
|
28228
|
+
case 'between':
|
|
28229
|
+
return `${label} between ${formatValue(condition.value)} and ${formatValue(condition.valueTo)}`;
|
|
28230
|
+
case 'in':
|
|
28231
|
+
return `${label} in ${formatValue(condition.value)}`;
|
|
28232
|
+
case 'notIn':
|
|
28233
|
+
return `${label} not in ${formatValue(condition.value)}`;
|
|
28234
|
+
case 'isNull':
|
|
28235
|
+
case 'blank':
|
|
28236
|
+
return `${label} is blank`;
|
|
28237
|
+
case 'isNotNull':
|
|
28238
|
+
case 'notBlank':
|
|
28239
|
+
return `${label} is not blank`;
|
|
28240
|
+
case 'regex':
|
|
28241
|
+
return `${label} matches ${formatValue(condition.value)}`;
|
|
28242
|
+
default:
|
|
28243
|
+
return condition.value === undefined
|
|
28244
|
+
? `${label} ${condition.operator}`
|
|
28245
|
+
: `${label} ${condition.operator} ${formatValue(condition.value)}`;
|
|
28246
|
+
}
|
|
28247
|
+
}
|
|
28248
|
+
function formatValue(value) {
|
|
28249
|
+
if (Array.isArray(value))
|
|
28250
|
+
return `[${value.map((item) => formatValue(item)).join(', ')}]`;
|
|
28251
|
+
if (value instanceof Date)
|
|
28252
|
+
return value.toISOString();
|
|
28253
|
+
if (typeof value === 'string')
|
|
28254
|
+
return `"${value}"`;
|
|
28255
|
+
if (value === null)
|
|
28256
|
+
return 'null';
|
|
28257
|
+
if (value === undefined)
|
|
28258
|
+
return 'undefined';
|
|
28259
|
+
return String(value);
|
|
28260
|
+
}
|
|
28261
|
+
|
|
28262
|
+
class FilterChipBar {
|
|
28263
|
+
constructor(container, getFilterExpression, labels, actions) {
|
|
28264
|
+
this.container = container;
|
|
28265
|
+
this.getFilterExpression = getFilterExpression;
|
|
28266
|
+
this.labels = labels;
|
|
28267
|
+
this.actions = actions;
|
|
28268
|
+
this.chipBar = null;
|
|
27391
28269
|
}
|
|
27392
|
-
|
|
28270
|
+
render() {
|
|
27393
28271
|
const chipBar = this.ensureChipBar();
|
|
27394
|
-
const
|
|
27395
|
-
const chips = this.buildChipDescriptors(expression);
|
|
28272
|
+
const chips = buildChipDescriptors(this.getFilterExpression(), this.labels, this.actions);
|
|
27396
28273
|
chipBar.replaceChildren();
|
|
27397
28274
|
if (chips.length === 0) {
|
|
27398
28275
|
chipBar.style.display = 'none';
|
|
27399
|
-
this.
|
|
28276
|
+
this.updateLayout();
|
|
27400
28277
|
return;
|
|
27401
28278
|
}
|
|
27402
28279
|
chipBar.style.display = '';
|
|
27403
|
-
|
|
27404
|
-
title.className = 'zg-filter-chip-bar__title';
|
|
27405
|
-
title.textContent = 'Filters';
|
|
27406
|
-
chipBar.appendChild(title);
|
|
28280
|
+
chipBar.appendChild(createTextElement('span', 'zg-filter-chip-bar__title', 'Filters'));
|
|
27407
28281
|
const chipList = document.createElement('div');
|
|
27408
28282
|
chipList.className = 'zg-filter-chip-bar__list';
|
|
27409
28283
|
for (const chip of chips) {
|
|
@@ -27414,259 +28288,407 @@ class GridFilterUI {
|
|
|
27414
28288
|
clearAllButton.type = 'button';
|
|
27415
28289
|
clearAllButton.className = 'zg-filter-chip-bar__clear';
|
|
27416
28290
|
clearAllButton.textContent = 'Clear all';
|
|
27417
|
-
clearAllButton.addEventListener('click', () =>
|
|
27418
|
-
this.clearAllFilters();
|
|
27419
|
-
});
|
|
28291
|
+
clearAllButton.addEventListener('click', () => this.actions.clearAllFilters());
|
|
27420
28292
|
chipBar.appendChild(clearAllButton);
|
|
27421
|
-
this.
|
|
28293
|
+
this.updateLayout();
|
|
28294
|
+
}
|
|
28295
|
+
destroy() {
|
|
28296
|
+
this.chipBar?.remove();
|
|
28297
|
+
this.chipBar = null;
|
|
28298
|
+
this.updateLayout();
|
|
28299
|
+
}
|
|
28300
|
+
ensureChipBar() {
|
|
28301
|
+
if (this.chipBar && this.chipBar.isConnected)
|
|
28302
|
+
return this.chipBar;
|
|
28303
|
+
const host = this.container.querySelector('.zg-viewport') ?? this.container;
|
|
28304
|
+
const chipBar = document.createElement('div');
|
|
28305
|
+
chipBar.className = 'zg-filter-chip-bar';
|
|
28306
|
+
chipBar.style.display = 'none';
|
|
28307
|
+
host.appendChild(chipBar);
|
|
28308
|
+
this.chipBar = chipBar;
|
|
28309
|
+
return chipBar;
|
|
27422
28310
|
}
|
|
27423
28311
|
createChipElement(label, remove) {
|
|
27424
28312
|
const chip = document.createElement('div');
|
|
27425
28313
|
chip.className = 'zg-filter-chip';
|
|
27426
28314
|
chip.title = label;
|
|
27427
|
-
|
|
27428
|
-
text.className = 'zg-filter-chip__label';
|
|
27429
|
-
text.textContent = label;
|
|
27430
|
-
chip.appendChild(text);
|
|
28315
|
+
chip.appendChild(createTextElement('span', 'zg-filter-chip__label', label));
|
|
27431
28316
|
const removeButton = document.createElement('button');
|
|
27432
28317
|
removeButton.type = 'button';
|
|
27433
28318
|
removeButton.className = 'zg-filter-chip__remove';
|
|
27434
28319
|
removeButton.setAttribute('aria-label', `Remove filter ${label}`);
|
|
27435
|
-
removeButton.textContent = '
|
|
27436
|
-
removeButton.addEventListener('click',
|
|
27437
|
-
remove();
|
|
27438
|
-
});
|
|
28320
|
+
removeButton.textContent = 'x';
|
|
28321
|
+
removeButton.addEventListener('click', remove);
|
|
27439
28322
|
chip.appendChild(removeButton);
|
|
27440
28323
|
return chip;
|
|
27441
28324
|
}
|
|
27442
|
-
|
|
27443
|
-
const
|
|
27444
|
-
const
|
|
27445
|
-
const
|
|
27446
|
-
|
|
27447
|
-
|
|
27448
|
-
|
|
27449
|
-
if (
|
|
27450
|
-
|
|
27451
|
-
label: this.describeQuickFilter(quickFilter),
|
|
27452
|
-
remove: () => this.clearQuickFilter(),
|
|
27453
|
-
});
|
|
27454
|
-
}
|
|
27455
|
-
return chips;
|
|
28325
|
+
updateLayout() {
|
|
28326
|
+
const scrollContainer = this.container.querySelector('.zg-scroll-container');
|
|
28327
|
+
const header = this.container.querySelector('.zg-header');
|
|
28328
|
+
const headerHeight = header?.offsetHeight ?? 0;
|
|
28329
|
+
const chipHeight = this.chipBar && this.chipBar.style.display !== 'none' ? this.chipBar.offsetHeight : 0;
|
|
28330
|
+
if (this.chipBar)
|
|
28331
|
+
this.chipBar.style.top = `${headerHeight}px`;
|
|
28332
|
+
if (scrollContainer)
|
|
28333
|
+
scrollContainer.style.top = `${headerHeight + chipHeight}px`;
|
|
27456
28334
|
}
|
|
27457
|
-
|
|
27458
|
-
|
|
27459
|
-
|
|
27460
|
-
|
|
27461
|
-
|
|
27462
|
-
|
|
27463
|
-
|
|
27464
|
-
|
|
27465
|
-
|
|
27466
|
-
|
|
27467
|
-
|
|
27468
|
-
|
|
27469
|
-
|
|
27470
|
-
|
|
27471
|
-
|
|
28335
|
+
}
|
|
28336
|
+
function createTextElement(tag, className, text) {
|
|
28337
|
+
const element = document.createElement(tag);
|
|
28338
|
+
element.className = className;
|
|
28339
|
+
element.textContent = text;
|
|
28340
|
+
return element;
|
|
28341
|
+
}
|
|
28342
|
+
|
|
28343
|
+
const TEXT_OPERATORS = [
|
|
28344
|
+
{ value: 'contains', label: 'Contains', valueCount: 1 },
|
|
28345
|
+
{ value: 'equals', label: 'Equals', valueCount: 1 },
|
|
28346
|
+
{ value: 'startsWith', label: 'Starts with', valueCount: 1 },
|
|
28347
|
+
{ value: 'endsWith', label: 'Ends with', valueCount: 1 },
|
|
28348
|
+
{ value: 'notContains', label: 'Not contains', valueCount: 1 },
|
|
28349
|
+
{ value: 'notEquals', label: 'Not equals', valueCount: 1 },
|
|
28350
|
+
{ value: 'blank', label: 'Is blank', valueCount: 0 },
|
|
28351
|
+
{ value: 'notBlank', label: 'Is not blank', valueCount: 0 },
|
|
28352
|
+
];
|
|
28353
|
+
const NUMBER_OPERATORS = [
|
|
28354
|
+
{ value: 'equals', label: 'Equals', valueCount: 1 },
|
|
28355
|
+
{ value: 'notEquals', label: 'Not equals', valueCount: 1 },
|
|
28356
|
+
{ value: 'greaterThan', label: 'Greater than', valueCount: 1 },
|
|
28357
|
+
{ value: 'greaterThanOrEqual', label: 'Greater than or equal', valueCount: 1 },
|
|
28358
|
+
{ value: 'lessThan', label: 'Less than', valueCount: 1 },
|
|
28359
|
+
{ value: 'lessThanOrEqual', label: 'Less than or equal', valueCount: 1 },
|
|
28360
|
+
{ value: 'between', label: 'Between', valueCount: 2 },
|
|
28361
|
+
{ value: 'blank', label: 'Is blank', valueCount: 0 },
|
|
28362
|
+
{ value: 'notBlank', label: 'Is not blank', valueCount: 0 },
|
|
28363
|
+
];
|
|
28364
|
+
const DATE_OPERATORS = [
|
|
28365
|
+
{ value: 'equals', label: 'Equals', valueCount: 1 },
|
|
28366
|
+
{ value: 'greaterThan', label: 'After', valueCount: 1 },
|
|
28367
|
+
{ value: 'lessThan', label: 'Before', valueCount: 1 },
|
|
28368
|
+
{ value: 'between', label: 'Between', valueCount: 2 },
|
|
28369
|
+
{ value: 'blank', label: 'Is blank', valueCount: 0 },
|
|
28370
|
+
{ value: 'notBlank', label: 'Is not blank', valueCount: 0 },
|
|
28371
|
+
];
|
|
28372
|
+
function getOperatorsForType(type) {
|
|
28373
|
+
switch (type) {
|
|
28374
|
+
case 'number':
|
|
28375
|
+
return NUMBER_OPERATORS;
|
|
28376
|
+
case 'date':
|
|
28377
|
+
return DATE_OPERATORS;
|
|
28378
|
+
default:
|
|
28379
|
+
return TEXT_OPERATORS;
|
|
28380
|
+
}
|
|
28381
|
+
}
|
|
28382
|
+
function getOperatorValueCount(operator, type) {
|
|
28383
|
+
return getOperatorsForType(type).find((op) => op.value === operator)?.valueCount ?? 1;
|
|
28384
|
+
}
|
|
28385
|
+
function buildCondition(operator, dropdownType, value, valueTo) {
|
|
28386
|
+
const valueCount = getOperatorValueCount(operator, dropdownType);
|
|
28387
|
+
if (valueCount === 0)
|
|
28388
|
+
return { operator };
|
|
28389
|
+
if (valueCount === 2) {
|
|
28390
|
+
return {
|
|
28391
|
+
operator,
|
|
28392
|
+
value: [parseInputValue(value, dropdownType), parseInputValue(valueTo, dropdownType)],
|
|
28393
|
+
};
|
|
28394
|
+
}
|
|
28395
|
+
return { operator, value: parseInputValue(value, dropdownType) };
|
|
28396
|
+
}
|
|
28397
|
+
function parseInputValue(value, dropdownType) {
|
|
28398
|
+
if (dropdownType === 'number') {
|
|
28399
|
+
const num = Number(value);
|
|
28400
|
+
return Number.isNaN(num) ? value : num;
|
|
28401
|
+
}
|
|
28402
|
+
if (dropdownType === 'date') {
|
|
28403
|
+
const ts = Date.parse(value);
|
|
28404
|
+
return Number.isNaN(ts) ? value : ts;
|
|
28405
|
+
}
|
|
28406
|
+
return value;
|
|
28407
|
+
}
|
|
28408
|
+
|
|
28409
|
+
function createTitle(options) {
|
|
28410
|
+
const title = document.createElement('div');
|
|
28411
|
+
title.className = 'zg-filter-popup__title';
|
|
28412
|
+
title.textContent = `Filter: ${options.getColumnLabel(options.columnDef, options.dataCol)}`;
|
|
28413
|
+
return title;
|
|
28414
|
+
}
|
|
28415
|
+
function createLogicRow(initial) {
|
|
28416
|
+
const row = document.createElement('div');
|
|
28417
|
+
row.className = 'zg-filter-popup__row';
|
|
28418
|
+
row.innerHTML = '<label>Match</label>';
|
|
28419
|
+
const select = document.createElement('select');
|
|
28420
|
+
select.className = 'zg-filter-popup__select';
|
|
28421
|
+
select.innerHTML = '<option value="AND">AND</option><option value="OR">OR</option>';
|
|
28422
|
+
select.value = initial;
|
|
28423
|
+
row.appendChild(select);
|
|
28424
|
+
return { row, select };
|
|
28425
|
+
}
|
|
28426
|
+
function createConditionRow(condition, operators, dropdownType, onRemove) {
|
|
28427
|
+
const element = document.createElement('div');
|
|
28428
|
+
element.className = 'zg-filter-popup__condition';
|
|
28429
|
+
const operatorSelect = createOperatorSelect(operators);
|
|
28430
|
+
const valueInput = createValueInput(dropdownType);
|
|
28431
|
+
const valueInputTo = createValueInput(dropdownType);
|
|
28432
|
+
valueInputTo.style.display = 'none';
|
|
28433
|
+
const removeBtn = createButton('zg-filter-popup__btn zg-filter-popup__btn--remove', 'Remove');
|
|
28434
|
+
appendRow(element, 'Operator', operatorSelect);
|
|
28435
|
+
appendRow(element, 'Value', valueInput, valueInputTo);
|
|
28436
|
+
const actions = document.createElement('div');
|
|
28437
|
+
actions.className = 'zg-filter-popup__condition-actions';
|
|
28438
|
+
actions.appendChild(removeBtn);
|
|
28439
|
+
element.appendChild(actions);
|
|
28440
|
+
const updateInputs = () => {
|
|
28441
|
+
const valueCount = operators.find((op) => op.value === operatorSelect.value)?.valueCount ?? 1;
|
|
28442
|
+
valueInput.style.display = valueCount === 0 ? 'none' : '';
|
|
28443
|
+
valueInputTo.style.display = valueCount === 2 ? '' : 'none';
|
|
28444
|
+
};
|
|
28445
|
+
operatorSelect.addEventListener('change', updateInputs);
|
|
28446
|
+
removeBtn.addEventListener('click', onRemove);
|
|
28447
|
+
applyConditionValue(condition, operatorSelect, valueInput, valueInputTo);
|
|
28448
|
+
updateInputs();
|
|
28449
|
+
return { element, operatorSelect, valueInput, valueInputTo, removeBtn };
|
|
28450
|
+
}
|
|
28451
|
+
function createValueInput(dropdownType) {
|
|
28452
|
+
const input = document.createElement('input');
|
|
28453
|
+
input.className = 'zg-filter-popup__input';
|
|
28454
|
+
input.type = dropdownType === 'number' ? 'number' : dropdownType === 'date' ? 'date' : 'text';
|
|
28455
|
+
return input;
|
|
28456
|
+
}
|
|
28457
|
+
function createOperatorSelect(operators) {
|
|
28458
|
+
const select = document.createElement('select');
|
|
28459
|
+
select.className = 'zg-filter-popup__select';
|
|
28460
|
+
operators.forEach((op) => {
|
|
28461
|
+
const option = document.createElement('option');
|
|
28462
|
+
option.value = op.value;
|
|
28463
|
+
option.textContent = op.label;
|
|
28464
|
+
select.appendChild(option);
|
|
28465
|
+
});
|
|
28466
|
+
return select;
|
|
28467
|
+
}
|
|
28468
|
+
function createButton(className, text) {
|
|
28469
|
+
const button = document.createElement('button');
|
|
28470
|
+
button.type = 'button';
|
|
28471
|
+
button.className = className;
|
|
28472
|
+
button.textContent = text;
|
|
28473
|
+
return button;
|
|
28474
|
+
}
|
|
28475
|
+
function appendRow(parent, label, ...children) {
|
|
28476
|
+
const row = document.createElement('div');
|
|
28477
|
+
row.className = 'zg-filter-popup__row';
|
|
28478
|
+
row.innerHTML = `<label>${label}</label>`;
|
|
28479
|
+
children.forEach((child) => row.appendChild(child));
|
|
28480
|
+
parent.appendChild(row);
|
|
28481
|
+
}
|
|
28482
|
+
function createActions() {
|
|
28483
|
+
const actions = document.createElement('div');
|
|
28484
|
+
actions.className = 'zg-filter-popup__actions';
|
|
28485
|
+
const applyBtn = createButton('zg-filter-popup__btn primary', 'Apply');
|
|
28486
|
+
const clearBtn = createButton('zg-filter-popup__btn', 'Clear');
|
|
28487
|
+
actions.appendChild(applyBtn);
|
|
28488
|
+
actions.appendChild(clearBtn);
|
|
28489
|
+
return { actions, applyBtn, clearBtn };
|
|
28490
|
+
}
|
|
28491
|
+
function applyConditionValue(condition, operatorSelect, valueInput, valueInputTo) {
|
|
28492
|
+
if (!condition)
|
|
28493
|
+
return;
|
|
28494
|
+
operatorSelect.value = condition.operator;
|
|
28495
|
+
if (Array.isArray(condition.value)) {
|
|
28496
|
+
valueInput.value = String(condition.value[0] ?? '');
|
|
28497
|
+
valueInputTo.value = String(condition.value[1] ?? '');
|
|
28498
|
+
}
|
|
28499
|
+
else if (condition.value != null) {
|
|
28500
|
+
valueInput.value = String(condition.value);
|
|
28501
|
+
}
|
|
28502
|
+
}
|
|
28503
|
+
function positionUnanchoredPopup(popup, options) {
|
|
28504
|
+
if (options.payload.anchorElement)
|
|
28505
|
+
return;
|
|
28506
|
+
const containerRect = options.container.getBoundingClientRect();
|
|
28507
|
+
popup.style.position = 'fixed';
|
|
28508
|
+
popup.style.top = `${containerRect.top + 12}px`;
|
|
28509
|
+
popup.style.left = `${containerRect.left + 12}px`;
|
|
28510
|
+
}
|
|
28511
|
+
|
|
28512
|
+
function openFilterPopup(options) {
|
|
28513
|
+
const dropdownType = options.payload.dropdownType ?? 'text';
|
|
28514
|
+
const operators = getOperatorsForType(dropdownType);
|
|
28515
|
+
const popup = document.createElement('div');
|
|
28516
|
+
popup.className = 'zg-filter-popup';
|
|
28517
|
+
popup.appendChild(createTitle(options));
|
|
28518
|
+
const logicRow = createLogicRow(options.existing?.logic ?? 'AND');
|
|
28519
|
+
popup.appendChild(logicRow.row);
|
|
28520
|
+
const conditionsContainer = document.createElement('div');
|
|
28521
|
+
conditionsContainer.className = 'zg-filter-popup__conditions';
|
|
28522
|
+
popup.appendChild(conditionsContainer);
|
|
28523
|
+
const conditionRows = [];
|
|
28524
|
+
const updateConditionControls = () => {
|
|
28525
|
+
const showLogic = conditionRows.length > 1;
|
|
28526
|
+
logicRow.row.style.display = showLogic ? '' : 'none';
|
|
28527
|
+
conditionRows.forEach((row) => {
|
|
28528
|
+
row.removeBtn.style.display = showLogic ? '' : 'none';
|
|
27472
28529
|
});
|
|
27473
|
-
|
|
27474
|
-
|
|
27475
|
-
|
|
27476
|
-
|
|
27477
|
-
|
|
28530
|
+
};
|
|
28531
|
+
const addCondition = (condition) => {
|
|
28532
|
+
const row = createConditionRow(condition, operators, dropdownType, () => {
|
|
28533
|
+
const index = conditionRows.findIndex((item) => item.element === row.element);
|
|
28534
|
+
if (index >= 0)
|
|
28535
|
+
conditionRows.splice(index, 1);
|
|
28536
|
+
row.element.remove();
|
|
28537
|
+
updateConditionControls();
|
|
27478
28538
|
});
|
|
27479
|
-
|
|
27480
|
-
|
|
27481
|
-
|
|
27482
|
-
|
|
27483
|
-
|
|
28539
|
+
conditionRows.push(row);
|
|
28540
|
+
conditionsContainer.appendChild(row.element);
|
|
28541
|
+
updateConditionControls();
|
|
28542
|
+
};
|
|
28543
|
+
const existingConditions = options.existing?.conditions?.length
|
|
28544
|
+
? options.existing.conditions
|
|
28545
|
+
: null;
|
|
28546
|
+
existingConditions ? existingConditions.forEach(addCondition) : addCondition();
|
|
28547
|
+
const addConditionBtn = createButton('zg-filter-popup__btn zg-filter-popup__btn--add', 'Add condition');
|
|
28548
|
+
addConditionBtn.addEventListener('click', () => addCondition());
|
|
28549
|
+
popup.appendChild(addConditionBtn);
|
|
28550
|
+
const { actions, applyBtn, clearBtn } = createActions();
|
|
28551
|
+
popup.appendChild(actions);
|
|
28552
|
+
const applyFilter = () => {
|
|
28553
|
+
const conditions = conditionRows.map((row) => buildCondition(row.operatorSelect.value, dropdownType, row.valueInput.value, row.valueInputTo.value));
|
|
28554
|
+
if (conditions.length === 0)
|
|
27484
28555
|
return;
|
|
27485
|
-
|
|
27486
|
-
|
|
27487
|
-
|
|
28556
|
+
options.setColumnFilter(options.dataCol, conditions, logicRow.select.value);
|
|
28557
|
+
handle.close();
|
|
28558
|
+
};
|
|
28559
|
+
applyBtn.addEventListener('click', applyFilter);
|
|
28560
|
+
clearBtn.addEventListener('click', () => {
|
|
28561
|
+
options.clearColumnFilter(options.dataCol);
|
|
28562
|
+
handle.close();
|
|
28563
|
+
});
|
|
28564
|
+
let keyHandler = (e) => {
|
|
28565
|
+
if (e.key !== 'Enter')
|
|
27488
28566
|
return;
|
|
27489
|
-
|
|
27490
|
-
|
|
27491
|
-
logic: root.logic,
|
|
27492
|
-
conditions: root.conditions.filter((_, index) => index !== conditionIndex),
|
|
27493
|
-
groups: root.groups ? [...root.groups] : undefined,
|
|
27494
|
-
});
|
|
27495
|
-
this.applyStructuredRoot(nextRoot);
|
|
27496
|
-
}
|
|
27497
|
-
removeTopLevelGroup(groupIndex) {
|
|
27498
|
-
const root = this.getFilterExpression()?.fieldState?.root;
|
|
27499
|
-
if (!root) {
|
|
28567
|
+
const active = document.activeElement;
|
|
28568
|
+
if (!active || !popup.contains(active))
|
|
27500
28569
|
return;
|
|
28570
|
+
const tag = active.tagName.toLowerCase();
|
|
28571
|
+
if (tag === 'input' || tag === 'select') {
|
|
28572
|
+
e.preventDefault();
|
|
28573
|
+
applyFilter();
|
|
27501
28574
|
}
|
|
27502
|
-
|
|
27503
|
-
|
|
28575
|
+
};
|
|
28576
|
+
popup.addEventListener('keydown', keyHandler);
|
|
28577
|
+
document.body.appendChild(popup);
|
|
28578
|
+
positionUnanchoredPopup(popup, options);
|
|
28579
|
+
const teardown = () => {
|
|
28580
|
+
if (keyHandler) {
|
|
28581
|
+
popup.removeEventListener('keydown', keyHandler);
|
|
28582
|
+
keyHandler = null;
|
|
28583
|
+
}
|
|
28584
|
+
popup.remove();
|
|
28585
|
+
};
|
|
28586
|
+
const handle = {
|
|
28587
|
+
close: () => {
|
|
28588
|
+
if (options.overlayManager.isOpen(popup)) {
|
|
28589
|
+
options.overlayManager.close(popup);
|
|
28590
|
+
return;
|
|
28591
|
+
}
|
|
28592
|
+
teardown();
|
|
28593
|
+
},
|
|
28594
|
+
destroy: teardown,
|
|
28595
|
+
};
|
|
28596
|
+
options.overlayManager.open({
|
|
28597
|
+
overlay: popup,
|
|
28598
|
+
anchor: options.payload.anchorElement,
|
|
28599
|
+
additionalElements: options.payload.anchorElement ? [options.payload.anchorElement] : [],
|
|
28600
|
+
restoreFocusOnClose: !!options.payload.anchorElement,
|
|
28601
|
+
restoreFocusTo: options.payload.anchorElement ?? null,
|
|
28602
|
+
closeOnEscape: true,
|
|
28603
|
+
positionOptions: { placement: 'bottom-start', offset: 4, viewportPadding: 8 },
|
|
28604
|
+
scrollOptions: { closeOnOutOfView: true, repositionOnScroll: false },
|
|
28605
|
+
onClose: teardown,
|
|
28606
|
+
});
|
|
28607
|
+
setTimeout(() => {
|
|
28608
|
+
const first = conditionRows[0];
|
|
28609
|
+
if (!first)
|
|
27504
28610
|
return;
|
|
27505
|
-
|
|
27506
|
-
|
|
27507
|
-
|
|
27508
|
-
|
|
27509
|
-
|
|
28611
|
+
if (first.valueInput.style.display !== 'none')
|
|
28612
|
+
first.valueInput.focus();
|
|
28613
|
+
else
|
|
28614
|
+
first.operatorSelect.focus();
|
|
28615
|
+
}, 0);
|
|
28616
|
+
return handle;
|
|
28617
|
+
}
|
|
28618
|
+
|
|
28619
|
+
class GridFilterUI {
|
|
28620
|
+
constructor(options) {
|
|
28621
|
+
this.options = options;
|
|
28622
|
+
this.overlayManager = OverlayManager.getInstance();
|
|
28623
|
+
this.popup = null;
|
|
28624
|
+
this.filterClickUnsub = null;
|
|
28625
|
+
this.filterChangeUnsub = null;
|
|
28626
|
+
this.columns = options.columns ?? [];
|
|
28627
|
+
this.chipBar = new FilterChipBar(options.container, options.getFilterExpression, {
|
|
28628
|
+
getColumnLabelByField: (field) => this.getColumnLabelByField(field),
|
|
28629
|
+
getColumnLabelByIndex: (index) => this.getColumnLabelByIndex(index),
|
|
28630
|
+
}, {
|
|
28631
|
+
clearQuickFilter: options.clearQuickFilter,
|
|
28632
|
+
clearAllFilters: options.clearAllFilters,
|
|
28633
|
+
applyStructuredRoot: (root) => options.setFieldState(createFieldState(root)),
|
|
27510
28634
|
});
|
|
27511
|
-
this.
|
|
27512
|
-
|
|
27513
|
-
applyStructuredRoot(root) {
|
|
27514
|
-
this.setFieldState({
|
|
27515
|
-
root,
|
|
27516
|
-
activeFields: this.collectActiveFields(root),
|
|
27517
|
-
timestamp: Date.now(),
|
|
28635
|
+
this.filterClickUnsub = options.events.on('header:filter:click', (payload) => {
|
|
28636
|
+
this.openFilterPopup(payload);
|
|
27518
28637
|
});
|
|
28638
|
+
this.filterChangeUnsub = options.events.on('filter:change', () => {
|
|
28639
|
+
this.chipBar.render();
|
|
28640
|
+
});
|
|
28641
|
+
this.chipBar.render();
|
|
27519
28642
|
}
|
|
27520
|
-
|
|
27521
|
-
|
|
27522
|
-
|
|
27523
|
-
|
|
27524
|
-
const fields = new Set();
|
|
27525
|
-
const visit = (group) => {
|
|
27526
|
-
for (const condition of group.conditions) {
|
|
27527
|
-
fields.add(condition.field);
|
|
27528
|
-
}
|
|
27529
|
-
for (const nested of group.groups ?? []) {
|
|
27530
|
-
visit(nested);
|
|
27531
|
-
}
|
|
27532
|
-
};
|
|
27533
|
-
visit(root);
|
|
27534
|
-
return Array.from(fields);
|
|
27535
|
-
}
|
|
27536
|
-
normalizeFieldGroup(group) {
|
|
27537
|
-
const normalizedGroups = group.groups?.filter((nested) => nested.conditions.length > 0 || (nested.groups?.length ?? 0) > 0);
|
|
27538
|
-
if (group.conditions.length === 0 && (normalizedGroups?.length ?? 0) === 0) {
|
|
27539
|
-
return null;
|
|
27540
|
-
}
|
|
27541
|
-
return {
|
|
27542
|
-
logic: group.logic,
|
|
27543
|
-
conditions: [...group.conditions],
|
|
27544
|
-
groups: normalizedGroups && normalizedGroups.length > 0 ? normalizedGroups : undefined,
|
|
27545
|
-
};
|
|
27546
|
-
}
|
|
27547
|
-
describeQuickFilter(quickFilter) {
|
|
27548
|
-
const query = this.formatValue(quickFilter.query);
|
|
27549
|
-
if (!quickFilter.columns || quickFilter.columns.length === 0) {
|
|
27550
|
-
return `Search ${query}`;
|
|
27551
|
-
}
|
|
27552
|
-
const labels = quickFilter.columns.map((column) => this.getColumnLabelByIndex(column));
|
|
27553
|
-
return `Search ${labels.join(', ')} ${query}`;
|
|
28643
|
+
setColumns(columns) {
|
|
28644
|
+
this.columns = [...columns];
|
|
28645
|
+
this.closePopup();
|
|
28646
|
+
this.chipBar.render();
|
|
27554
28647
|
}
|
|
27555
|
-
|
|
27556
|
-
|
|
27557
|
-
|
|
27558
|
-
|
|
27559
|
-
|
|
27560
|
-
|
|
27561
|
-
|
|
27562
|
-
}
|
|
27563
|
-
const joined = parts.join(` ${group.logic} `);
|
|
27564
|
-
return nested && parts.length > 1 ? `(${joined})` : joined;
|
|
28648
|
+
destroy() {
|
|
28649
|
+
this.closePopup();
|
|
28650
|
+
this.filterClickUnsub?.();
|
|
28651
|
+
this.filterChangeUnsub?.();
|
|
28652
|
+
this.filterClickUnsub = null;
|
|
28653
|
+
this.filterChangeUnsub = null;
|
|
28654
|
+
this.chipBar.destroy();
|
|
27565
28655
|
}
|
|
27566
|
-
|
|
27567
|
-
const
|
|
27568
|
-
|
|
27569
|
-
|
|
27570
|
-
|
|
27571
|
-
|
|
27572
|
-
|
|
27573
|
-
|
|
27574
|
-
|
|
27575
|
-
|
|
27576
|
-
|
|
27577
|
-
|
|
27578
|
-
|
|
27579
|
-
|
|
27580
|
-
|
|
27581
|
-
|
|
27582
|
-
|
|
27583
|
-
case 'gt':
|
|
27584
|
-
case 'greaterThan':
|
|
27585
|
-
return `${label} > ${this.formatValue(condition.value)}`;
|
|
27586
|
-
case 'gte':
|
|
27587
|
-
case 'greaterThanOrEqual':
|
|
27588
|
-
return `${label} >= ${this.formatValue(condition.value)}`;
|
|
27589
|
-
case 'lt':
|
|
27590
|
-
case 'lessThan':
|
|
27591
|
-
return `${label} < ${this.formatValue(condition.value)}`;
|
|
27592
|
-
case 'lte':
|
|
27593
|
-
case 'lessThanOrEqual':
|
|
27594
|
-
return `${label} <= ${this.formatValue(condition.value)}`;
|
|
27595
|
-
case 'between':
|
|
27596
|
-
return `${label} between ${this.formatValue(condition.value)} and ${this.formatValue(condition.valueTo)}`;
|
|
27597
|
-
case 'in':
|
|
27598
|
-
return `${label} in ${this.formatValue(condition.value)}`;
|
|
27599
|
-
case 'notIn':
|
|
27600
|
-
return `${label} not in ${this.formatValue(condition.value)}`;
|
|
27601
|
-
case 'isNull':
|
|
27602
|
-
case 'blank':
|
|
27603
|
-
return `${label} is blank`;
|
|
27604
|
-
case 'isNotNull':
|
|
27605
|
-
case 'notBlank':
|
|
27606
|
-
return `${label} is not blank`;
|
|
27607
|
-
case 'regex':
|
|
27608
|
-
return `${label} matches ${this.formatValue(condition.value)}`;
|
|
27609
|
-
default:
|
|
27610
|
-
return condition.value === undefined
|
|
27611
|
-
? `${label} ${condition.operator}`
|
|
27612
|
-
: `${label} ${condition.operator} ${this.formatValue(condition.value)}`;
|
|
27613
|
-
}
|
|
28656
|
+
openFilterPopup(payload) {
|
|
28657
|
+
const dataCol = this.options.mapVisualToDataCol(payload.columnIndex);
|
|
28658
|
+
const columnDef = this.options.getColumnDef(dataCol);
|
|
28659
|
+
if (!columnDef)
|
|
28660
|
+
return;
|
|
28661
|
+
this.closePopup();
|
|
28662
|
+
this.popup = openFilterPopup({
|
|
28663
|
+
container: this.options.container,
|
|
28664
|
+
overlayManager: this.overlayManager,
|
|
28665
|
+
payload,
|
|
28666
|
+
dataCol,
|
|
28667
|
+
columnDef,
|
|
28668
|
+
existing: this.options.getFilterState().find((filter) => filter.column === dataCol),
|
|
28669
|
+
setColumnFilter: this.options.setColumnFilter,
|
|
28670
|
+
clearColumnFilter: this.options.clearColumnFilter,
|
|
28671
|
+
getColumnLabel: (column, index) => this.getColumnLabel(column, index),
|
|
28672
|
+
});
|
|
27614
28673
|
}
|
|
27615
|
-
|
|
27616
|
-
|
|
27617
|
-
|
|
27618
|
-
}
|
|
27619
|
-
if (value instanceof Date) {
|
|
27620
|
-
return value.toISOString();
|
|
27621
|
-
}
|
|
27622
|
-
if (typeof value === 'string') {
|
|
27623
|
-
return `"${value}"`;
|
|
27624
|
-
}
|
|
27625
|
-
if (value === null) {
|
|
27626
|
-
return 'null';
|
|
27627
|
-
}
|
|
27628
|
-
if (value === undefined) {
|
|
27629
|
-
return 'undefined';
|
|
27630
|
-
}
|
|
27631
|
-
return String(value);
|
|
28674
|
+
closePopup() {
|
|
28675
|
+
this.popup?.close();
|
|
28676
|
+
this.popup = null;
|
|
27632
28677
|
}
|
|
27633
|
-
|
|
27634
|
-
|
|
27635
|
-
|
|
27636
|
-
|
|
27637
|
-
const chipHeight = this.chipBar && this.chipBar.style.display !== 'none' ? this.chipBar.offsetHeight : 0;
|
|
27638
|
-
if (this.chipBar) {
|
|
27639
|
-
this.chipBar.style.top = `${headerHeight}px`;
|
|
27640
|
-
}
|
|
27641
|
-
if (scrollContainer) {
|
|
27642
|
-
scrollContainer.style.top = `${headerHeight + chipHeight}px`;
|
|
27643
|
-
}
|
|
28678
|
+
getColumnLabel(column, index) {
|
|
28679
|
+
if (typeof column.header === 'string')
|
|
28680
|
+
return column.header;
|
|
28681
|
+
return column.header?.text ?? column.field ?? `Column ${index}`;
|
|
27644
28682
|
}
|
|
27645
|
-
|
|
27646
|
-
|
|
27647
|
-
|
|
27648
|
-
|
|
27649
|
-
|
|
27650
|
-
if (this.popup) {
|
|
27651
|
-
this.popup.remove();
|
|
27652
|
-
this.popup = null;
|
|
27653
|
-
}
|
|
28683
|
+
getColumnLabelByField(field) {
|
|
28684
|
+
const index = this.columns.findIndex((column) => column.field === field);
|
|
28685
|
+
if (index < 0)
|
|
28686
|
+
return field;
|
|
28687
|
+
return this.getColumnLabel(this.columns[index], index);
|
|
27654
28688
|
}
|
|
27655
|
-
|
|
27656
|
-
this.
|
|
27657
|
-
|
|
27658
|
-
this.filterClickUnsub();
|
|
27659
|
-
this.filterClickUnsub = null;
|
|
27660
|
-
}
|
|
27661
|
-
if (this.filterChangeUnsub) {
|
|
27662
|
-
this.filterChangeUnsub();
|
|
27663
|
-
this.filterChangeUnsub = null;
|
|
27664
|
-
}
|
|
27665
|
-
if (this.chipBar) {
|
|
27666
|
-
this.chipBar.remove();
|
|
27667
|
-
this.chipBar = null;
|
|
27668
|
-
}
|
|
27669
|
-
this.updateChipBarLayout();
|
|
28689
|
+
getColumnLabelByIndex(index) {
|
|
28690
|
+
const column = this.columns[index] ?? this.options.getColumnDef(index);
|
|
28691
|
+
return column ? this.getColumnLabel(column, index) : `Column ${index}`;
|
|
27670
28692
|
}
|
|
27671
28693
|
}
|
|
27672
28694
|
|
|
@@ -27900,16 +28922,18 @@ function createLifecyclePlugin(opts) {
|
|
|
27900
28922
|
setColumnModel(columnModel);
|
|
27901
28923
|
// Initialize rendering
|
|
27902
28924
|
store.exec('rendering:initialize', containerWidth, containerHeight);
|
|
28925
|
+
// Size the viewport to the initial pipeline view (e.g. first page)
|
|
28926
|
+
syncAfterPipeline();
|
|
27903
28927
|
// Attach scroll + resize
|
|
27904
28928
|
const scrollContainer = store.get('dom.scrollContainer');
|
|
27905
28929
|
if (scrollContainer) {
|
|
27906
|
-
if (
|
|
28930
|
+
if (store.hasAction('scroll:attach'))
|
|
27907
28931
|
store.exec('scroll:attach', scrollContainer);
|
|
27908
|
-
if (
|
|
28932
|
+
if (store.hasAction('resize:attach'))
|
|
27909
28933
|
store.exec('resize:attach', scrollContainer, containerWidth, containerHeight);
|
|
27910
28934
|
}
|
|
27911
28935
|
// Column resize
|
|
27912
|
-
if (options.
|
|
28936
|
+
if (options.enableColumnResize !== false && store.hasAction('column:initResize')) {
|
|
27913
28937
|
store.exec('column:initResize');
|
|
27914
28938
|
}
|
|
27915
28939
|
// Column drag
|
|
@@ -27923,14 +28947,14 @@ function createLifecyclePlugin(opts) {
|
|
|
27923
28947
|
const headerApi = api.getMethod('header', 'getHeaderCellsContainer');
|
|
27924
28948
|
const hc = headerApi ? headerApi() : null;
|
|
27925
28949
|
if (hc) {
|
|
27926
|
-
if (options.
|
|
28950
|
+
if (options.enableColumnResize !== false && store.hasAction('column:attachResize'))
|
|
27927
28951
|
store.exec('column:attachResize', hc);
|
|
27928
28952
|
if (options.enableColumnDrag !== false)
|
|
27929
28953
|
store.exec('column:attachDrag', hc);
|
|
27930
28954
|
}
|
|
27931
28955
|
}
|
|
27932
28956
|
// Selection
|
|
27933
|
-
if (
|
|
28957
|
+
if (store.hasAction('selection:attach') && scrollContainer) {
|
|
27934
28958
|
store.exec('selection:attach', {
|
|
27935
28959
|
container: scrollContainer,
|
|
27936
28960
|
selectionType: options.selectionType ?? 'cell',
|
|
@@ -27945,17 +28969,14 @@ function createLifecyclePlugin(opts) {
|
|
|
27945
28969
|
// Sync selection state
|
|
27946
28970
|
eventUnsubs.push(events.on('selection:change', (payload) => {
|
|
27947
28971
|
state.selection = payload.ranges ?? [];
|
|
27948
|
-
|
|
27949
|
-
// Expose selection checker for rendering
|
|
27950
|
-
api.fireEvent('selection:checkerUpdate', payload.isSelected);
|
|
27951
|
-
}
|
|
28972
|
+
opts.setSelectionChecker?.(payload.isSelected ?? null);
|
|
27952
28973
|
if (options.onSelectionChange)
|
|
27953
28974
|
options.onSelectionChange(state.selection);
|
|
27954
28975
|
store.exec('rendering:refreshSelectionClasses');
|
|
27955
28976
|
store.exec('lifecycle:updateHeaderCheckbox');
|
|
27956
28977
|
}));
|
|
27957
28978
|
}
|
|
27958
|
-
// Sort header click
|
|
28979
|
+
// Sort header click handler
|
|
27959
28980
|
eventUnsubs.push(events.on('header:sort:click', (event) => {
|
|
27960
28981
|
let dataCol = event.columnIndex;
|
|
27961
28982
|
if (columnModel) {
|
|
@@ -27966,36 +28987,44 @@ function createLifecyclePlugin(opts) {
|
|
|
27966
28987
|
store.exec('sort:toggle', dataCol, isAdditiveHeaderSortInteraction(event));
|
|
27967
28988
|
syncAfterPipeline();
|
|
27968
28989
|
}));
|
|
27969
|
-
// Checkbox header
|
|
28990
|
+
// Checkbox header handler
|
|
27970
28991
|
eventUnsubs.push(events.on('header:checkbox:change', (event) => {
|
|
27971
|
-
if (!
|
|
28992
|
+
if (!store.hasAction('selection:clear'))
|
|
27972
28993
|
return;
|
|
27973
28994
|
if (event.action === 'select-all')
|
|
27974
28995
|
store.exec('selection:selectAll', options.rowCount);
|
|
27975
28996
|
else
|
|
27976
28997
|
store.exec('selection:clear');
|
|
27977
28998
|
}));
|
|
27978
|
-
// Viewport resize
|
|
28999
|
+
// Viewport resize handler
|
|
27979
29000
|
eventUnsubs.push(events.on('viewport:resized', () => {
|
|
27980
29001
|
store.exec('rendering:updateScroller');
|
|
27981
29002
|
store.exec('rendering:refresh');
|
|
27982
29003
|
}));
|
|
27983
|
-
// Scroll
|
|
29004
|
+
// Scroll viewport handler
|
|
27984
29005
|
eventUnsubs.push(events.on('scroll', (payload) => {
|
|
27985
|
-
if (
|
|
29006
|
+
if (store.hasAction('viewport:updateRange') && payload.visibleRange) {
|
|
27986
29007
|
store.exec('viewport:updateRange', payload.visibleRange);
|
|
27987
29008
|
}
|
|
27988
29009
|
}));
|
|
27989
29010
|
// Filter UI
|
|
27990
|
-
if (
|
|
29011
|
+
if (store.hasAction('filterUI:attach')) {
|
|
27991
29012
|
store.exec('filterUI:attach', container, events);
|
|
27992
29013
|
}
|
|
27993
29014
|
// Infinite scroll
|
|
27994
|
-
if (
|
|
29015
|
+
if (store.hasAction('infiniteScroll:setup')) {
|
|
27995
29016
|
store.exec('infiniteScroll:setup');
|
|
27996
29017
|
}
|
|
27997
29018
|
// Editing
|
|
27998
29019
|
store.exec('editing:setup');
|
|
29020
|
+
// Keyboard navigation
|
|
29021
|
+
if (store.hasAction('keyboard:setup')) {
|
|
29022
|
+
store.exec('keyboard:setup');
|
|
29023
|
+
}
|
|
29024
|
+
// Clipboard (copy/cut/paste)
|
|
29025
|
+
if (store.hasAction('clipboard:setup')) {
|
|
29026
|
+
store.exec('clipboard:setup');
|
|
29027
|
+
}
|
|
27999
29028
|
rendered = true;
|
|
28000
29029
|
}
|
|
28001
29030
|
// Render visible cells
|
|
@@ -28007,13 +29036,13 @@ function createLifecyclePlugin(opts) {
|
|
|
28007
29036
|
}
|
|
28008
29037
|
// Update viewport range
|
|
28009
29038
|
const renderingApi = api.getMethod('rendering', 'getVisibleRange');
|
|
28010
|
-
if (
|
|
29039
|
+
if (store.hasAction('viewport:updateRange') && renderingApi) {
|
|
28011
29040
|
const initialRange = renderingApi();
|
|
28012
29041
|
if (initialRange)
|
|
28013
29042
|
store.exec('viewport:updateRange', initialRange);
|
|
28014
29043
|
}
|
|
28015
29044
|
// Update pagination
|
|
28016
|
-
if (
|
|
29045
|
+
if (store.hasAction('pagination:update')) {
|
|
28017
29046
|
store.exec('pagination:update');
|
|
28018
29047
|
}
|
|
28019
29048
|
const ensureDataRange = api.getMethod('data', 'ensureVisibleRange');
|
|
@@ -28021,13 +29050,10 @@ function createLifecyclePlugin(opts) {
|
|
|
28021
29050
|
void ensureDataRange('initial');
|
|
28022
29051
|
}
|
|
28023
29052
|
}, 'lifecycle');
|
|
29053
|
+
// Delegate to the single shared viewport-sync implementation in the
|
|
29054
|
+
// rendering plugin so sort, pagination, and init stay in lockstep.
|
|
28024
29055
|
function syncAfterPipeline() {
|
|
28025
|
-
|
|
28026
|
-
const visibleCount = viewIndices ? viewIndices.length : options.rowCount;
|
|
28027
|
-
store.exec('rendering:setRowCount', visibleCount);
|
|
28028
|
-
store.exec('rendering:updateCanvasSize');
|
|
28029
|
-
store.exec('rendering:clearCache');
|
|
28030
|
-
store.exec('rendering:refresh');
|
|
29056
|
+
store.exec('rendering:syncAfterPipeline');
|
|
28031
29057
|
}
|
|
28032
29058
|
store.action('lifecycle:syncAfterPipeline', () => syncAfterPipeline(), 'lifecycle');
|
|
28033
29059
|
store.action('lifecycle:updateHeaderCheckbox', () => {
|
|
@@ -28137,8 +29163,6 @@ function createSlimContext(container, options) {
|
|
|
28137
29163
|
data: [],
|
|
28138
29164
|
selection: [],
|
|
28139
29165
|
activeCell: null,
|
|
28140
|
-
sortState: [],
|
|
28141
|
-
filterState: [],
|
|
28142
29166
|
scrollPosition: { top: 0, left: 0 },
|
|
28143
29167
|
editingCell: null,
|
|
28144
29168
|
};
|
|
@@ -28149,6 +29173,12 @@ function createSlimContext(container, options) {
|
|
|
28149
29173
|
const pipelineRegistry = new PipelineRegistry();
|
|
28150
29174
|
pipelineRegistry.registerPhase('sort', 10, 'pipeline.sort');
|
|
28151
29175
|
pipelineRegistry.registerPhase('filter', 20, 'pipeline.filter');
|
|
29176
|
+
// Frontend pagination slices the post-sort/filter view; backend pagination
|
|
29177
|
+
// is handled by the data plugin, so no pipeline phase there.
|
|
29178
|
+
if (normalizedOptions.pagination?.enabled &&
|
|
29179
|
+
resolveQueryExecutionMode(normalizedOptions.dataMode) !== 'backend') {
|
|
29180
|
+
pipelineRegistry.registerPhase('pagination', 30, 'pipeline.pagination');
|
|
29181
|
+
}
|
|
28152
29182
|
gridApi.register('options', {
|
|
28153
29183
|
get: () => optionsManager.getOptions(),
|
|
28154
29184
|
getSnapshot: () => optionsManager.getOptionsSnapshot(),
|
|
@@ -28223,6 +29253,21 @@ function installAllPlugins(ctx) {
|
|
|
28223
29253
|
getColumnModel: () => columnModel,
|
|
28224
29254
|
getDataAccessor: () => dataAccessor,
|
|
28225
29255
|
}));
|
|
29256
|
+
// Phase 46: Keyboard navigation
|
|
29257
|
+
pluginHost.use(createKeyboardPlugin({
|
|
29258
|
+
options,
|
|
29259
|
+
state: ctx.state,
|
|
29260
|
+
container: ctx.container,
|
|
29261
|
+
}));
|
|
29262
|
+
// Phase 47: Clipboard (copy/cut/paste)
|
|
29263
|
+
pluginHost.use(createClipboardPlugin({
|
|
29264
|
+
options,
|
|
29265
|
+
state: ctx.state,
|
|
29266
|
+
container: ctx.container,
|
|
29267
|
+
events,
|
|
29268
|
+
getColumnModel: () => columnModel,
|
|
29269
|
+
getDataAccessor: () => dataAccessor,
|
|
29270
|
+
}));
|
|
28226
29271
|
// Phase 50: Scroll, Viewport
|
|
28227
29272
|
pluginHost.use(createScrollPlugin());
|
|
28228
29273
|
pluginHost.use(createViewportPlugin());
|
|
@@ -28234,6 +29279,7 @@ function installAllPlugins(ctx) {
|
|
|
28234
29279
|
}));
|
|
28235
29280
|
// Phase 70: Header
|
|
28236
29281
|
pluginHost.use(createHeaderPlugin({
|
|
29282
|
+
options,
|
|
28237
29283
|
events,
|
|
28238
29284
|
getColumnModel: () => columnModel,
|
|
28239
29285
|
}));
|
|
@@ -28251,10 +29297,14 @@ function installAllPlugins(ctx) {
|
|
|
28251
29297
|
state: ctx.state,
|
|
28252
29298
|
container: ctx.container,
|
|
28253
29299
|
events,
|
|
29300
|
+
getColumnModel: () => columnModel,
|
|
28254
29301
|
getDataAccessor: () => dataAccessor,
|
|
28255
29302
|
setColumnModel: (cm) => {
|
|
28256
29303
|
columnModel = cm;
|
|
28257
29304
|
},
|
|
29305
|
+
setSelectionChecker: (checker) => {
|
|
29306
|
+
selectionChecker = checker;
|
|
29307
|
+
},
|
|
28258
29308
|
}));
|
|
28259
29309
|
// Setup pipeline computeds
|
|
28260
29310
|
ctx.pipelineRegistry.setupCoreComputeds(store);
|
|
@@ -28265,15 +29315,8 @@ function installAllPlugins(ctx) {
|
|
|
28265
29315
|
ctx._setDataAccessor = (da) => {
|
|
28266
29316
|
dataAccessor = da;
|
|
28267
29317
|
};
|
|
28268
|
-
ctx._setSelectionChecker = (sc) => {
|
|
28269
|
-
selectionChecker = sc;
|
|
28270
|
-
};
|
|
28271
29318
|
ctx._getColumnModel = () => columnModel;
|
|
28272
29319
|
ctx._getDataAccessor = () => dataAccessor;
|
|
28273
|
-
// Wire selection checker updates from lifecycle plugin
|
|
28274
|
-
events.on('selection:checkerUpdate', (checker) => {
|
|
28275
|
-
selectionChecker = checker;
|
|
28276
|
-
});
|
|
28277
29320
|
// Install filter UI if columns + filter are present
|
|
28278
29321
|
if (options.columns &&
|
|
28279
29322
|
options.columns.length > 0 &&
|
|
@@ -29710,10 +30753,7 @@ class Grid {
|
|
|
29710
30753
|
this._theme.destroyAutoTheme();
|
|
29711
30754
|
this.ctx.pluginHost?.destroy?.();
|
|
29712
30755
|
this.ctx.optionsManager?.destroy?.();
|
|
29713
|
-
this.ctx.container.innerHTML = '';
|
|
29714
30756
|
this.ctx.container.classList.remove('zg-grid');
|
|
29715
|
-
this.ctx.state.data = [];
|
|
29716
|
-
this.ctx.state.selection = [];
|
|
29717
30757
|
this.ctx.isDestroyed = true;
|
|
29718
30758
|
}
|
|
29719
30759
|
// --- Options / Stats ---
|
|
@@ -30364,8 +31404,8 @@ function createSearchInput(options, placeholder) {
|
|
|
30364
31404
|
input.setAttribute('aria-controls', 'dropdown-menu');
|
|
30365
31405
|
input.style.width = '100%';
|
|
30366
31406
|
input.style.height = '100%';
|
|
30367
|
-
input.style.border = '
|
|
30368
|
-
input.style.outline = '
|
|
31407
|
+
input.style.border = '2px solid #4CAF50';
|
|
31408
|
+
input.style.outline = 'none';
|
|
30369
31409
|
input.style.padding = '4px 8px';
|
|
30370
31410
|
input.style.fontSize = '13px';
|
|
30371
31411
|
input.style.fontFamily = 'inherit';
|
|
@@ -30384,8 +31424,8 @@ function createDisplayElement(options, displayText) {
|
|
|
30384
31424
|
display.setAttribute('role', 'button');
|
|
30385
31425
|
display.style.width = '100%';
|
|
30386
31426
|
display.style.height = '100%';
|
|
30387
|
-
display.style.border = '
|
|
30388
|
-
display.style.outline = '
|
|
31427
|
+
display.style.border = '2px solid #4CAF50';
|
|
31428
|
+
display.style.outline = 'none';
|
|
30389
31429
|
display.style.padding = '4px 8px';
|
|
30390
31430
|
display.style.fontSize = '13px';
|
|
30391
31431
|
display.style.fontFamily = 'inherit';
|
|
@@ -33571,271 +34611,6 @@ class FocusManager {
|
|
|
33571
34611
|
}
|
|
33572
34612
|
}
|
|
33573
34613
|
|
|
33574
|
-
/**
|
|
33575
|
-
* ClipboardManager - Handles copy/cut/paste operations
|
|
33576
|
-
*
|
|
33577
|
-
* Supports multiple formats:
|
|
33578
|
-
* - text/plain: TSV (tab-separated values)
|
|
33579
|
-
* - text/html: HTML table format
|
|
33580
|
-
*
|
|
33581
|
-
* Compatible with Excel and Google Sheets.
|
|
33582
|
-
*
|
|
33583
|
-
* @example
|
|
33584
|
-
* ```typescript
|
|
33585
|
-
* const clipboard = new ClipboardManager({
|
|
33586
|
-
* getValue: (row, col) => data[row][col],
|
|
33587
|
-
* setValue: (row, col, value) => { data[row][col] = value; },
|
|
33588
|
-
* });
|
|
33589
|
-
*
|
|
33590
|
-
* // Copy selection
|
|
33591
|
-
* await clipboard.copy([
|
|
33592
|
-
* { startRow: 0, startCol: 0, endRow: 2, endCol: 2 },
|
|
33593
|
-
* ]);
|
|
33594
|
-
*
|
|
33595
|
-
* // Paste at location
|
|
33596
|
-
* await clipboard.paste(5, 0);
|
|
33597
|
-
* ```
|
|
33598
|
-
*/
|
|
33599
|
-
class ClipboardManager {
|
|
33600
|
-
constructor(options) {
|
|
33601
|
-
this.events = options.events;
|
|
33602
|
-
this.getValue = options.getValue;
|
|
33603
|
-
this.setValue = options.setValue;
|
|
33604
|
-
this.deleteValue = options.deleteValue;
|
|
33605
|
-
this.includeHeaders = options.includeHeaders ?? false;
|
|
33606
|
-
this.getColumnHeader = options.getColumnHeader;
|
|
33607
|
-
}
|
|
33608
|
-
/**
|
|
33609
|
-
* Copy selected ranges to clipboard
|
|
33610
|
-
* @param ranges - Ranges to copy
|
|
33611
|
-
*/
|
|
33612
|
-
async copy(ranges) {
|
|
33613
|
-
if (ranges.length === 0)
|
|
33614
|
-
return;
|
|
33615
|
-
// For simplicity, copy only the first range
|
|
33616
|
-
// TODO: Support multiple ranges
|
|
33617
|
-
const range = ranges[0];
|
|
33618
|
-
const data = this.extractRangeData(range);
|
|
33619
|
-
const tsvText = this.toTSV(data);
|
|
33620
|
-
const htmlText = this.toHTML(data);
|
|
33621
|
-
try {
|
|
33622
|
-
// Write to clipboard with multiple formats
|
|
33623
|
-
await navigator.clipboard.write([
|
|
33624
|
-
new ClipboardItem({
|
|
33625
|
-
'text/plain': new Blob([tsvText], { type: 'text/plain' }),
|
|
33626
|
-
'text/html': new Blob([htmlText], { type: 'text/html' }),
|
|
33627
|
-
}),
|
|
33628
|
-
]);
|
|
33629
|
-
// Emit event
|
|
33630
|
-
if (this.events) {
|
|
33631
|
-
this.events.emit('copy', {
|
|
33632
|
-
ranges,
|
|
33633
|
-
data: tsvText,
|
|
33634
|
-
});
|
|
33635
|
-
}
|
|
33636
|
-
}
|
|
33637
|
-
catch (error) {
|
|
33638
|
-
console.error('Failed to copy to clipboard:', error);
|
|
33639
|
-
// Fallback to older API
|
|
33640
|
-
try {
|
|
33641
|
-
await navigator.clipboard.writeText(tsvText);
|
|
33642
|
-
}
|
|
33643
|
-
catch (fallbackError) {
|
|
33644
|
-
console.error('Fallback copy also failed:', fallbackError);
|
|
33645
|
-
}
|
|
33646
|
-
}
|
|
33647
|
-
}
|
|
33648
|
-
/**
|
|
33649
|
-
* Cut selected ranges to clipboard (copy + delete)
|
|
33650
|
-
* @param ranges - Ranges to cut
|
|
33651
|
-
*/
|
|
33652
|
-
async cut(ranges) {
|
|
33653
|
-
if (ranges.length === 0)
|
|
33654
|
-
return;
|
|
33655
|
-
// Copy first
|
|
33656
|
-
await this.copy(ranges);
|
|
33657
|
-
// Then delete values
|
|
33658
|
-
for (const range of ranges) {
|
|
33659
|
-
for (let row = range.startRow; row <= range.endRow; row++) {
|
|
33660
|
-
for (let col = range.startCol; col <= range.endCol; col++) {
|
|
33661
|
-
if (this.deleteValue) {
|
|
33662
|
-
this.deleteValue(row, col);
|
|
33663
|
-
}
|
|
33664
|
-
else {
|
|
33665
|
-
this.setValue(row, col, null);
|
|
33666
|
-
}
|
|
33667
|
-
}
|
|
33668
|
-
}
|
|
33669
|
-
}
|
|
33670
|
-
// Emit event
|
|
33671
|
-
if (this.events) {
|
|
33672
|
-
this.events.emit('cut', {
|
|
33673
|
-
ranges,
|
|
33674
|
-
data: '', // Already emitted in copy
|
|
33675
|
-
});
|
|
33676
|
-
}
|
|
33677
|
-
}
|
|
33678
|
-
/**
|
|
33679
|
-
* Paste from clipboard at target location
|
|
33680
|
-
* @param targetRow - Target row
|
|
33681
|
-
* @param targetCol - Target column
|
|
33682
|
-
*/
|
|
33683
|
-
async paste(targetRow, targetCol) {
|
|
33684
|
-
try {
|
|
33685
|
-
// Try to read multiple formats
|
|
33686
|
-
const items = await navigator.clipboard.read();
|
|
33687
|
-
for (const item of items) {
|
|
33688
|
-
// Prefer HTML format (preserves formatting)
|
|
33689
|
-
if (item.types.includes('text/html')) {
|
|
33690
|
-
const blob = await item.getType('text/html');
|
|
33691
|
-
const html = await blob.text();
|
|
33692
|
-
const data = this.parseHTML(html);
|
|
33693
|
-
this.pasteData(data, targetRow, targetCol);
|
|
33694
|
-
return;
|
|
33695
|
-
}
|
|
33696
|
-
// Fallback to plain text
|
|
33697
|
-
if (item.types.includes('text/plain')) {
|
|
33698
|
-
const blob = await item.getType('text/plain');
|
|
33699
|
-
const text = await blob.text();
|
|
33700
|
-
const data = this.parseTSV(text);
|
|
33701
|
-
this.pasteData(data, targetRow, targetCol);
|
|
33702
|
-
return;
|
|
33703
|
-
}
|
|
33704
|
-
}
|
|
33705
|
-
}
|
|
33706
|
-
catch (error) {
|
|
33707
|
-
console.error('Failed to paste from clipboard:', error);
|
|
33708
|
-
// Fallback to text API
|
|
33709
|
-
try {
|
|
33710
|
-
const text = await navigator.clipboard.readText();
|
|
33711
|
-
const data = this.parseTSV(text);
|
|
33712
|
-
this.pasteData(data, targetRow, targetCol);
|
|
33713
|
-
}
|
|
33714
|
-
catch (fallbackError) {
|
|
33715
|
-
console.error('Fallback paste also failed:', fallbackError);
|
|
33716
|
-
}
|
|
33717
|
-
}
|
|
33718
|
-
}
|
|
33719
|
-
/**
|
|
33720
|
-
* Extract data from a range
|
|
33721
|
-
*/
|
|
33722
|
-
extractRangeData(range) {
|
|
33723
|
-
const data = [];
|
|
33724
|
-
// Include headers if requested
|
|
33725
|
-
if (this.includeHeaders && this.getColumnHeader) {
|
|
33726
|
-
const headers = [];
|
|
33727
|
-
for (let col = range.startCol; col <= range.endCol; col++) {
|
|
33728
|
-
headers.push(this.getColumnHeader(col));
|
|
33729
|
-
}
|
|
33730
|
-
data.push(headers);
|
|
33731
|
-
}
|
|
33732
|
-
// Extract cell values
|
|
33733
|
-
for (let row = range.startRow; row <= range.endRow; row++) {
|
|
33734
|
-
const rowData = [];
|
|
33735
|
-
for (let col = range.startCol; col <= range.endCol; col++) {
|
|
33736
|
-
rowData.push(this.getValue(row, col));
|
|
33737
|
-
}
|
|
33738
|
-
data.push(rowData);
|
|
33739
|
-
}
|
|
33740
|
-
return data;
|
|
33741
|
-
}
|
|
33742
|
-
/**
|
|
33743
|
-
* Convert data to TSV format
|
|
33744
|
-
*/
|
|
33745
|
-
toTSV(data) {
|
|
33746
|
-
return data.map((row) => row.map((cell) => this.formatCellForTSV(cell)).join('\t')).join('\n');
|
|
33747
|
-
}
|
|
33748
|
-
/**
|
|
33749
|
-
* Format cell value for TSV
|
|
33750
|
-
*/
|
|
33751
|
-
formatCellForTSV(value) {
|
|
33752
|
-
if (value == null)
|
|
33753
|
-
return '';
|
|
33754
|
-
const str = String(value);
|
|
33755
|
-
// Escape tabs and newlines
|
|
33756
|
-
return str.replace(/\t/g, ' ').replace(/\n/g, ' ');
|
|
33757
|
-
}
|
|
33758
|
-
/**
|
|
33759
|
-
* Convert data to HTML table format
|
|
33760
|
-
*/
|
|
33761
|
-
toHTML(data) {
|
|
33762
|
-
let html = '<table>';
|
|
33763
|
-
for (let i = 0; i < data.length; i++) {
|
|
33764
|
-
const row = data[i];
|
|
33765
|
-
const tag = i === 0 && this.includeHeaders ? 'th' : 'td';
|
|
33766
|
-
html += '<tr>';
|
|
33767
|
-
for (const cell of row) {
|
|
33768
|
-
html += `<${tag}>${this.escapeHTML(String(cell ?? ''))}</${tag}>`;
|
|
33769
|
-
}
|
|
33770
|
-
html += '</tr>';
|
|
33771
|
-
}
|
|
33772
|
-
html += '</table>';
|
|
33773
|
-
return html;
|
|
33774
|
-
}
|
|
33775
|
-
/**
|
|
33776
|
-
* Escape HTML special characters
|
|
33777
|
-
*/
|
|
33778
|
-
escapeHTML(str) {
|
|
33779
|
-
return str
|
|
33780
|
-
.replace(/&/g, '&')
|
|
33781
|
-
.replace(/</g, '<')
|
|
33782
|
-
.replace(/>/g, '>')
|
|
33783
|
-
.replace(/"/g, '"')
|
|
33784
|
-
.replace(/'/g, ''');
|
|
33785
|
-
}
|
|
33786
|
-
/**
|
|
33787
|
-
* Parse TSV text to 2D array
|
|
33788
|
-
*/
|
|
33789
|
-
parseTSV(text) {
|
|
33790
|
-
const lines = text.split(/\r?\n/);
|
|
33791
|
-
return lines.map((line) => line.split('\t'));
|
|
33792
|
-
}
|
|
33793
|
-
/**
|
|
33794
|
-
* Parse HTML table to 2D array
|
|
33795
|
-
*/
|
|
33796
|
-
parseHTML(html) {
|
|
33797
|
-
const parser = new DOMParser();
|
|
33798
|
-
const doc = parser.parseFromString(html, 'text/html');
|
|
33799
|
-
const table = doc.querySelector('table');
|
|
33800
|
-
if (!table) {
|
|
33801
|
-
// No table found, treat as plain text
|
|
33802
|
-
return this.parseTSV(doc.body.textContent || '');
|
|
33803
|
-
}
|
|
33804
|
-
const data = [];
|
|
33805
|
-
const rows = Array.from(table.querySelectorAll('tr'));
|
|
33806
|
-
for (const row of rows) {
|
|
33807
|
-
const cells = Array.from(row.querySelectorAll('td, th'));
|
|
33808
|
-
const rowData = [];
|
|
33809
|
-
for (const cell of cells) {
|
|
33810
|
-
rowData.push(cell.textContent || '');
|
|
33811
|
-
}
|
|
33812
|
-
if (rowData.length > 0) {
|
|
33813
|
-
data.push(rowData);
|
|
33814
|
-
}
|
|
33815
|
-
}
|
|
33816
|
-
return data;
|
|
33817
|
-
}
|
|
33818
|
-
/**
|
|
33819
|
-
* Paste data at target location
|
|
33820
|
-
*/
|
|
33821
|
-
pasteData(data, targetRow, targetCol) {
|
|
33822
|
-
for (let i = 0; i < data.length; i++) {
|
|
33823
|
-
const row = data[i];
|
|
33824
|
-
for (let j = 0; j < row.length; j++) {
|
|
33825
|
-
const value = row[j];
|
|
33826
|
-
this.setValue(targetRow + i, targetCol + j, value);
|
|
33827
|
-
}
|
|
33828
|
-
}
|
|
33829
|
-
// Emit event
|
|
33830
|
-
if (this.events) {
|
|
33831
|
-
this.events.emit('paste', {
|
|
33832
|
-
cell: { row: targetRow, col: targetCol },
|
|
33833
|
-
data: this.toTSV(data),
|
|
33834
|
-
});
|
|
33835
|
-
}
|
|
33836
|
-
}
|
|
33837
|
-
}
|
|
33838
|
-
|
|
33839
34614
|
/**
|
|
33840
34615
|
* Yield to the main thread via setTimeout(0).
|
|
33841
34616
|
* Allows the browser to process pending events/rendering between chunks.
|
|
@@ -34292,4 +35067,4 @@ function alpha(hex, a) {
|
|
|
34292
35067
|
// Version
|
|
34293
35068
|
const VERSION = '0.1.0';
|
|
34294
35069
|
|
|
34295
|
-
export { ARIAManager, AdvancedCellRenderer, AutoFitCalculator, ButtonRenderer, CSVExporter, CellPool, CellPositioner, CheckboxEditor, CheckboxHeaderRenderer, CheckboxRenderer, ChipRenderer, ClipboardManager, ColumnDragCommand, ColumnDragManager, ColumnGroupManager, ColumnGroupModel, ColumnGroupRenderer, RendererRegistry as ColumnGroupRendererRegistry, ColumnModel, ColumnModelWidthProvider, ColumnPinPlugin, ColumnReorderPlugin, ColumnResizeManager, ColumnVisibilityPlugin, DateEditor, DatePickerRenderer, DateRangeEditor, DateRangeRenderer, DateRenderer, DateTimeEditor, DateTimePickerRenderer, DateTimeRenderer, DateRenderer as DatetimeDateRenderer, PopupManager as DatetimePopupManager, ThemeManager as DatetimeThemeManager, DependencyGraph, DragKeyboardHandler, DragTouchHandler, DragVisualFeedback, DropdownEditor, DropdownRenderer, EditorManager, EventEmitter$1 as EventEmitter, FilterableHeaderRenderer, FocusManager, Grid, GridApiImpl, GridStoreImpl as GridStore, IconHeaderRenderer, ImageRenderer, KeyboardNavigator, LinkRenderer, MultiColumnSorter, NumberRenderer, PipelineRegistry, PluginHost, ProgressBarRenderer, RendererCache, RendererRegistry$1 as RendererRegistry, ResizeHandleRenderer, ResizePreview, ScrollModel, SelectRenderer, SingleColumnSorter, SortStateManager, SortableHeaderRenderer, TextEditor, TextHeaderRenderer, TextRenderer, ThemeBuilder, TimeEditor, TimePickerRenderer, TimeRenderer, UniformHeightProvider, UniformWidthProvider, VERSION, VariableHeightProvider, VariableWidthProvider, ViewportModel, VirtualScroller, alpha, applyTheme, containsCell, createAsyncFilterPlugin, createAsyncSortPlugin, createColumnGroupRenderer, createRendererRegistry as createColumnGroupRendererRegistry, createCorePlugin, createDateEditor, createDatePickerRenderer, createDateRangeEditor, createDateRenderer, createDateTimeEditor, createDateTimePickerRenderer, createDateTimeRenderer, createDateRenderer as createDatetimeDateRenderer, createDevToolsConnector, createDropdownRenderer, createEditingPlugin, createFilterPlugin, createHitTester, createIdentityIndexMap, createIndexMap, createLifecyclePlugin, createSelectionPlugin, createSortPlugin, createTheme, createTimeEditor, createTimePickerRenderer, createTimeRenderer, createUndoRedoPlugin, darkTheme, darken, draculaTheme, formatDate, formatDateForDisplay, formatDateRange, formatDateTime$2 as formatDateTime, formatTime, getAllThemes, getRenderer as getColumnGroupRenderer, getTheme, githubTheme, globalRendererRegistry as globalColumnGroupRendererRegistry, hasRenderer as hasColumnGroupRenderer, hasTheme, isDateInRange, isValidDate, lightTheme, lighten, listThemes, materialTheme, mergeRanges, nordTheme, normalizeRange, oneDarkTheme, parseDate, parseDateTime, parseTime, processInChunks, rangesIntersect, registerRenderer as registerColumnGroupRenderer, registerTheme, removeTheme, setDatetimeTheme, setDatetimeThemeConfig, solarizedTheme, themeToCSSVariables, unregisterTheme, yieldToMain };
|
|
35070
|
+
export { ARIAManager, AdvancedCellRenderer, AutoFitCalculator, ButtonRenderer, CSVExporter, CellPool, CellPositioner, CheckboxEditor, CheckboxHeaderRenderer, CheckboxRenderer, ChipRenderer, ClipboardManager, ColumnDragCommand, ColumnDragManager, ColumnGroupManager, ColumnGroupModel, ColumnGroupRenderer, RendererRegistry as ColumnGroupRendererRegistry, ColumnModel, ColumnModelWidthProvider, ColumnPinPlugin, ColumnReorderPlugin, ColumnResizeManager, ColumnVisibilityPlugin, DateEditor, DatePickerRenderer, DateRangeEditor, DateRangeRenderer, DateRenderer, DateTimeEditor, DateTimePickerRenderer, DateTimeRenderer, DateRenderer as DatetimeDateRenderer, PopupManager as DatetimePopupManager, ThemeManager as DatetimeThemeManager, DependencyGraph, DragKeyboardHandler, DragTouchHandler, DragVisualFeedback, DropdownEditor, DropdownRenderer, EditorManager, EventEmitter$1 as EventEmitter, FilterableHeaderRenderer, FocusManager, Grid, GridApiImpl, GridStoreImpl as GridStore, IconHeaderRenderer, ImageRenderer, KeyboardNavigator, LinkRenderer, MultiColumnSorter, NumberRenderer, PipelineRegistry, PluginHost, ProgressBarRenderer, RendererCache, RendererRegistry$1 as RendererRegistry, ResizeHandleRenderer, ResizePreview, ScrollModel, SelectRenderer, SingleColumnSorter, SortStateManager, SortableHeaderRenderer, TextEditor, TextHeaderRenderer, TextRenderer, ThemeBuilder, TimeEditor, TimePickerRenderer, TimeRenderer, UniformHeightProvider, UniformWidthProvider, VERSION, VariableHeightProvider, VariableWidthProvider, ViewportModel, VirtualScroller, alpha, applyTheme, containsCell, createAsyncFilterPlugin, createAsyncSortPlugin, createClipboardPlugin, createColumnGroupRenderer, createRendererRegistry as createColumnGroupRendererRegistry, createCorePlugin, createDateEditor, createDatePickerRenderer, createDateRangeEditor, createDateRenderer, createDateTimeEditor, createDateTimePickerRenderer, createDateTimeRenderer, createDateRenderer as createDatetimeDateRenderer, createDevToolsConnector, createDropdownRenderer, createEditingPlugin, createFilterPlugin, createFilterUIPlugin, createHitTester, createIdentityIndexMap, createIndexMap, createKeyboardPlugin, createLifecyclePlugin, createSelectionPlugin, createSortPlugin, createTheme, createTimeEditor, createTimePickerRenderer, createTimeRenderer, createUndoRedoPlugin, darkTheme, darken, draculaTheme, formatDate, formatDateForDisplay, formatDateRange, formatDateTime$2 as formatDateTime, formatTime, getAllThemes, getRenderer as getColumnGroupRenderer, getTheme, githubTheme, globalRendererRegistry as globalColumnGroupRendererRegistry, hasRenderer as hasColumnGroupRenderer, hasTheme, isDateInRange, isValidDate, lightTheme, lighten, listThemes, materialTheme, mergeRanges, nordTheme, normalizeRange, oneDarkTheme, parseDate, parseDateTime, parseTime, processInChunks, rangesIntersect, registerRenderer as registerColumnGroupRenderer, registerTheme, removeTheme, setDatetimeTheme, setDatetimeThemeConfig, solarizedTheme, themeToCSSVariables, unregisterTheme, yieldToMain };
|