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