@freegridjs/core 1.9.5 → 1.11.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.js CHANGED
@@ -432,9 +432,10 @@ var ColumnModel = class {
432
432
  setContainerWidth(width) {
433
433
  if (width === this.containerWidth) return false;
434
434
  this.containerWidth = width;
435
- const before = this.displayed().map((c) => c.width).join();
435
+ const sig = () => this.displayed().map((c) => `${c.width}${c.region}`).join();
436
+ const before = sig();
436
437
  this.relayout();
437
- return before !== this.displayed().map((c) => c.width).join();
438
+ return before !== sig();
438
439
  }
439
440
  roots() {
440
441
  return this.roots_;
@@ -608,7 +609,19 @@ var ColumnModel = class {
608
609
  return excess > 0 ? d > 0 : excess < 0 ? d < 0 : false;
609
610
  });
610
611
  if (!clamped.length) {
611
- for (const n of pending) flexWidth.set(n, clampWidth(n.def, share(n)));
612
+ let rest = free;
613
+ for (const n of pending) {
614
+ const w = clampWidth(n.def, share(n));
615
+ flexWidth.set(n, w);
616
+ rest -= w;
617
+ }
618
+ for (const n of pending) {
619
+ if (rest < 1) break;
620
+ const w = flexWidth.get(n);
621
+ if (clampWidth(n.def, w + 1) !== w + 1) continue;
622
+ flexWidth.set(n, w + 1);
623
+ rest--;
624
+ }
612
625
  break;
613
626
  }
614
627
  let used = 0;
@@ -624,13 +637,16 @@ var ColumnModel = class {
624
637
  const next = { left: [], center: [], right: [] };
625
638
  this.leafById = /* @__PURE__ */ new Map();
626
639
  let index = 0;
640
+ const pinnedWidth = visible.reduce((s, n) => s + (n.pinned ? widthOf(n) : 0), 0);
641
+ const unpin = this.containerWidth > 0 && pinnedWidth > this.containerWidth * 0.75;
642
+ const x = { left: 0, center: 0, right: 0 };
627
643
  for (const r of REGIONS) {
628
- let x = 0;
644
+ const to = unpin ? "center" : r;
629
645
  for (const n of visible) {
630
646
  if ((n.pinned ?? "center") !== r) continue;
631
- const col = { id: n.id, def: n.def, node: n, width: widthOf(n), region: r, left: x, index: index++ };
632
- x += col.width;
633
- next[r].push(col);
647
+ const col = { id: n.id, def: n.def, node: n, width: widthOf(n), region: to, left: x[to], index: index++ };
648
+ x[to] += col.width;
649
+ next[to].push(col);
634
650
  this.leafById.set(n.id, col);
635
651
  }
636
652
  }
@@ -638,6 +654,39 @@ var ColumnModel = class {
638
654
  }
639
655
  };
640
656
 
657
+ // src/keyboard.ts
658
+ var imeKey = (e) => e.isComposing || e.keyCode === 229;
659
+ var NAV_KEYS = /* @__PURE__ */ new Set(["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Tab", "Home", "End", "PageUp", "PageDown"]);
660
+ function nextFocus(key, mods, p, s) {
661
+ const r = (i) => Math.max(0, Math.min(s.rows - 1, i));
662
+ const c = (i) => Math.max(0, Math.min(s.cols - 1, i));
663
+ switch (key) {
664
+ case "ArrowUp":
665
+ return { rowIndex: r(p.rowIndex - 1), colIndex: p.colIndex };
666
+ case "ArrowDown":
667
+ return { rowIndex: r(p.rowIndex + 1), colIndex: p.colIndex };
668
+ case "ArrowLeft":
669
+ return { rowIndex: p.rowIndex, colIndex: c(p.colIndex - 1) };
670
+ case "ArrowRight":
671
+ return { rowIndex: p.rowIndex, colIndex: c(p.colIndex + 1) };
672
+ case "PageUp":
673
+ return { rowIndex: r(p.rowIndex - s.pageRows), colIndex: p.colIndex };
674
+ case "PageDown":
675
+ return { rowIndex: r(p.rowIndex + s.pageRows), colIndex: p.colIndex };
676
+ case "Home":
677
+ return { rowIndex: mods.ctrl ? 0 : p.rowIndex, colIndex: 0 };
678
+ case "End":
679
+ return { rowIndex: mods.ctrl ? s.rows - 1 : p.rowIndex, colIndex: s.cols - 1 };
680
+ case "Tab": {
681
+ const flat2 = p.rowIndex * s.cols + p.colIndex + (mods.shift ? -1 : 1);
682
+ if (flat2 < 0 || flat2 >= s.rows * s.cols) return null;
683
+ return { rowIndex: Math.floor(flat2 / s.cols), colIndex: flat2 % s.cols };
684
+ }
685
+ default:
686
+ return null;
687
+ }
688
+ }
689
+
641
690
  // src/formula.ts
642
691
  var FormulaError = class extends Error {
643
692
  constructor(code) {
@@ -1320,25 +1369,6 @@ function columnFormulaValue(def, row) {
1320
1369
  }
1321
1370
  var formulasOff = (def) => !!scopes.get(def)?.off?.();
1322
1371
  var isFormula = (v) => typeof v === "string" && v.length > 1 && v.startsWith("=");
1323
- function formulaRefs(formula) {
1324
- let n;
1325
- try {
1326
- n = compile(formula);
1327
- } catch {
1328
- return null;
1329
- }
1330
- const out = /* @__PURE__ */ new Set();
1331
- const walk = (x) => {
1332
- if (x.k === "ref") out.add(x.name);
1333
- else if (x.k === "un") walk(x.a);
1334
- else if (x.k === "bin") {
1335
- walk(x.a);
1336
- walk(x.b);
1337
- } else if (x.k === "call") x.args.forEach(walk);
1338
- };
1339
- walk(n);
1340
- return [...out];
1341
- }
1342
1372
 
1343
1373
  // src/rows/values.ts
1344
1374
  function getCellValue(def, row) {
@@ -1361,6 +1391,7 @@ var REGIONS2 = ["left", "center", "right"];
1361
1391
  var NUM_OPS = [[">=", "gte"], ["<=", "lte"], ["<>", "ne"], ["!=", "ne"], [">", "gt"], ["<", "lt"], ["=", "eq"]];
1362
1392
  var DATE_OPS = [[">", "after"], ["<", "before"], ["=", "equals"]];
1363
1393
  var RANGE = /^(.+?)\s*(?:~|\.\.)\s*(.+)$/;
1394
+ var MULTI = "~many";
1364
1395
  var toNumber = (s) => {
1365
1396
  const n = Number(s.replace(/[,\s₩$€¥]/g, ""));
1366
1397
  return s.trim() !== "" && Number.isFinite(n) ? n : null;
@@ -1460,8 +1491,21 @@ var FilterRow = class {
1460
1491
  box.closest(".fg-filter-cell").classList.toggle("fg-filter-active", !!model);
1461
1492
  if (box === box.ownerDocument.activeElement || box.getRootNode().activeElement === box) continue;
1462
1493
  if (box instanceof HTMLSelectElement) {
1463
- const v = model?.type === "set" && model.values.length === 1 ? JSON.stringify(model.values[0]) : "";
1464
- if ([...box.options].some((o) => o.value === v)) box.value = v;
1494
+ const many = model?.type === "set" && model.values.length > 1 ? model.values.length : 0;
1495
+ let multi = box.querySelector(`option[value="${MULTI}"]`);
1496
+ if (many) {
1497
+ if (!multi) {
1498
+ multi = el("option", "", { value: MULTI });
1499
+ multi.disabled = true;
1500
+ box.options[0].after(multi);
1501
+ }
1502
+ multi.textContent = this.grid.store.state.locale.setSelected.replace("{n}", String(many));
1503
+ box.value = MULTI;
1504
+ } else {
1505
+ multi?.remove();
1506
+ const v = model?.type === "set" && model.values.length === 1 ? JSON.stringify(model.values[0]) : "";
1507
+ if ([...box.options].some((o) => o.value === v)) box.value = v;
1508
+ }
1465
1509
  } else box.value = filterText(model);
1466
1510
  box.classList.remove("fg-filter-bad");
1467
1511
  }
@@ -1475,6 +1519,10 @@ var FilterRow = class {
1475
1519
  if (!def) continue;
1476
1520
  const v = select.value;
1477
1521
  this.fillOptions(select, def);
1522
+ if (v === MULTI) {
1523
+ this.rebuilt = true;
1524
+ continue;
1525
+ }
1478
1526
  if (v && ![...select.options].some((o) => o.value === v)) {
1479
1527
  const o = el("option", "", { value: v });
1480
1528
  o.textContent = String(JSON.parse(v));
@@ -1495,7 +1543,7 @@ var FilterRow = class {
1495
1543
  select.replaceChildren(all2, ...opts);
1496
1544
  }
1497
1545
  cell(c, h) {
1498
- const cell = el("div", "fg-filter-cell");
1546
+ const cell = el("div", "fg-filter-cell", { role: "gridcell" });
1499
1547
  cell.style.left = `${c.left}px`;
1500
1548
  cell.style.width = `${c.width}px`;
1501
1549
  cell.style.height = `${h}px`;
@@ -1529,6 +1577,7 @@ var FilterRow = class {
1529
1577
  });
1530
1578
  input.addEventListener("keydown", (e) => {
1531
1579
  e.stopPropagation();
1580
+ if (imeKey(e)) return;
1532
1581
  if (e.key === "Enter") {
1533
1582
  clearTimeout(this.timers.get(c.id));
1534
1583
  apply();
@@ -1818,8 +1867,10 @@ function extreme(values, pickMax) {
1818
1867
  function aggregate(agg, values, rows) {
1819
1868
  if (typeof agg === "function") return agg(values, rows);
1820
1869
  switch (agg) {
1821
- case "sum":
1822
- return numbers(values).reduce((s, n) => s + n, 0);
1870
+ case "sum": {
1871
+ const nums = numbers(values);
1872
+ return nums.length || !values.length ? nums.reduce((s, n) => s + n, 0) : null;
1873
+ }
1823
1874
  case "avg": {
1824
1875
  const nums = numbers(values);
1825
1876
  return nums.length ? nums.reduce((s, n) => s + n, 0) / nums.length : null;
@@ -2055,6 +2106,7 @@ var Renderer = class {
2055
2106
  };
2056
2107
  this.overlayKey = "";
2057
2108
  this.heightFn = null;
2109
+ this.freeBadge = null;
2058
2110
  this.onCardClick = (e) => {
2059
2111
  const target = e.target;
2060
2112
  const card = target.closest(".fg-card");
@@ -2203,7 +2255,8 @@ var Renderer = class {
2203
2255
  }
2204
2256
  if (options.autoHeight && !this.useCards()) {
2205
2257
  this.root.classList.add("fg-auto-height");
2206
- this.bodyEl.style.height = `${this.rowsHeight(count) + this.topOffset + bottomExtra + Math.max(0, this.scroller.offsetHeight - this.scroller.clientHeight)}px`;
2258
+ const rowsH = count > 0 ? this.rowsHeight(count) : Math.max(this.rowsHeight(count), 3 * rh);
2259
+ this.bodyEl.style.height = `${rowsH + this.topOffset + bottomExtra + Math.max(0, this.scroller.offsetHeight - this.scroller.clientHeight)}px`;
2207
2260
  }
2208
2261
  const sig = `${headerHeight}|${REGIONS3.map((r) => columns.regionWidth(r)).join(",")}|${this.rowsHeight(count) + this.topOffset + bottomExtra}|${summary && sumPinned ? sumPos : ""}|${sumRows}|${options.autoHeight ? 1 : 0}`;
2209
2262
  if (sig !== this.layoutSig) {
@@ -2262,6 +2315,7 @@ var Renderer = class {
2262
2315
  if (ref.nextElementSibling !== b) ref.after(b);
2263
2316
  ref = b;
2264
2317
  }
2318
+ this.placeFreeBadge();
2265
2319
  }
2266
2320
  /** "No rows" / "Loading" over the body, or the noRowsOverlay / loadingOverlay given in the options. */
2267
2321
  paintOverlay(count) {
@@ -2352,7 +2406,7 @@ var Renderer = class {
2352
2406
  const { grid } = this;
2353
2407
  const state = grid.store.state;
2354
2408
  const total = grid.rowModel.server ? count : grid.viewRows().length;
2355
- const key = `${this.version}.${grid.valuesVersion}|${count}|${total}|${state.selected.size}|${JSON.stringify(state.range)}|${state.locale.code}`;
2409
+ const key = `${this.version}.${grid.valuesVersion}|${count}|${total}|${state.selected.size}|${JSON.stringify(state.range)}|${JSON.stringify(state.extraRanges)}|${state.locale.code}`;
2356
2410
  if (this.statusEl && key === this.statusKey) return;
2357
2411
  this.statusKey = key;
2358
2412
  this.statusEl ?? (this.statusEl = el("div", "fg-statusbar", { role: "status", "aria-live": "polite" }));
@@ -3224,12 +3278,21 @@ var Renderer = class {
3224
3278
  /** Small "free version" mark in the bottom-right corner (unlicensed company domains flagged on freegrid.app). */
3225
3279
  showFreeBadge() {
3226
3280
  if (this.root.querySelector(".fg-free-badge")) return;
3227
- const a = el("a", "fg-free-badge", { href: "https://freegrid.app/pricing/", target: "_blank", rel: "noopener" });
3281
+ const a = el("div", "fg-free-badge", { "aria-hidden": "true" });
3228
3282
  a.innerHTML = '<svg viewBox="0 0 32 32" width="14" height="14" aria-hidden="true"><rect width="32" height="32" rx="7" fill="#0E1726"/><path d="M7 11.5h18M7 16.5h18M7 21.5h18M13 7v18" stroke="#fff" stroke-width="2" stroke-linecap="round"/><rect x="14" y="12.5" width="11" height="3" rx="1" fill="#F2A900"/></svg>';
3229
3283
  const text2 = el("span");
3230
3284
  text2.textContent = `FreeGrid \xB7 ${this.grid.store.state.locale.freeBadge}`;
3231
3285
  a.append(text2);
3232
3286
  this.root.append(a);
3287
+ this.freeBadge = a;
3288
+ this.placeFreeBadge();
3289
+ }
3290
+ /** The free badge sits above the status bar and pager, not on them. */
3291
+ placeFreeBadge() {
3292
+ if (!this.freeBadge) return;
3293
+ const below = (this.statusEl?.isConnected ? this.statusEl.offsetHeight : 0) + (this.pagerEl?.isConnected ? this.pagerEl.offsetHeight : 0);
3294
+ const bottom = below ? `${below + 22}px` : "";
3295
+ if (this.freeBadge.style.bottom !== bottom) this.freeBadge.style.bottom = bottom;
3233
3296
  }
3234
3297
  /** Card view applies when enabled and the grid is narrower than the breakpoint. */
3235
3298
  useCards() {
@@ -3549,6 +3612,8 @@ var en = {
3549
3612
  apply: "Apply",
3550
3613
  reset: "Reset",
3551
3614
  selectAll: "(Select all)",
3615
+ setSelected: "({n} selected)",
3616
+ noAggregate: "No aggregate",
3552
3617
  search: "Search\u2026",
3553
3618
  blanks: "(Blanks)",
3554
3619
  value: "Value",
@@ -3635,7 +3700,6 @@ var en = {
3635
3700
  freeBadge: "Free version",
3636
3701
  exportPdf: "Export PDF",
3637
3702
  exportJson: "Export JSON",
3638
- ai: { title: "AI assistant", placeholder: "Ask about the data or tell the grid what to do", send: "Send", thinking: "Working\u2026", undo: "Undo", undone: "Undone", failed: "The assistant did not answer", skipped: "Skipped", done: "Done" },
3639
3703
  cf: {
3640
3704
  title: "Conditional formatting",
3641
3705
  add: "Add rule",
@@ -3732,38 +3796,6 @@ function eventInside(e, ...els) {
3732
3796
  return els.some((el2) => !!el2 && el2.contains(e.target));
3733
3797
  }
3734
3798
 
3735
- // src/keyboard.ts
3736
- var NAV_KEYS = /* @__PURE__ */ new Set(["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Tab", "Home", "End", "PageUp", "PageDown"]);
3737
- function nextFocus(key, mods, p, s) {
3738
- const r = (i) => Math.max(0, Math.min(s.rows - 1, i));
3739
- const c = (i) => Math.max(0, Math.min(s.cols - 1, i));
3740
- switch (key) {
3741
- case "ArrowUp":
3742
- return { rowIndex: r(p.rowIndex - 1), colIndex: p.colIndex };
3743
- case "ArrowDown":
3744
- return { rowIndex: r(p.rowIndex + 1), colIndex: p.colIndex };
3745
- case "ArrowLeft":
3746
- return { rowIndex: p.rowIndex, colIndex: c(p.colIndex - 1) };
3747
- case "ArrowRight":
3748
- return { rowIndex: p.rowIndex, colIndex: c(p.colIndex + 1) };
3749
- case "PageUp":
3750
- return { rowIndex: r(p.rowIndex - s.pageRows), colIndex: p.colIndex };
3751
- case "PageDown":
3752
- return { rowIndex: r(p.rowIndex + s.pageRows), colIndex: p.colIndex };
3753
- case "Home":
3754
- return { rowIndex: mods.ctrl ? 0 : p.rowIndex, colIndex: 0 };
3755
- case "End":
3756
- return { rowIndex: mods.ctrl ? s.rows - 1 : p.rowIndex, colIndex: s.cols - 1 };
3757
- case "Tab": {
3758
- const flat2 = p.rowIndex * s.cols + p.colIndex + (mods.shift ? -1 : 1);
3759
- if (flat2 < 0 || flat2 >= s.rows * s.cols) return null;
3760
- return { rowIndex: Math.floor(flat2 / s.cols), colIndex: flat2 % s.cols };
3761
- }
3762
- default:
3763
- return null;
3764
- }
3765
- }
3766
-
3767
3799
  // src/registry.ts
3768
3800
  var liveGrids = /* @__PURE__ */ new Map();
3769
3801
  var apiGrids = /* @__PURE__ */ new WeakMap();
@@ -3779,7 +3811,8 @@ var DEFAULT_KEYMAP = {
3779
3811
  "Ctrl+Z": "undo",
3780
3812
  "Ctrl+Y": "redo",
3781
3813
  "Ctrl+Shift+Z": "redo",
3782
- "Ctrl+F": "find"
3814
+ "Ctrl+F": "find",
3815
+ "Alt+ArrowDown": "columnMenu"
3783
3816
  };
3784
3817
  function comboOf(e) {
3785
3818
  const parts = [];
@@ -3928,6 +3961,8 @@ function attachBody(grid) {
3928
3961
  case "paste":
3929
3962
  navigator.clipboard?.readText?.().then((t) => grid.pasteText(t), () => void 0);
3930
3963
  return true;
3964
+ case "columnMenu":
3965
+ return grid.menu.openAtFocus();
3931
3966
  }
3932
3967
  if (!f) return false;
3933
3968
  if (!grid.editor.canEdit(f.rowIndex, f.colId)) return false;
@@ -3943,6 +3978,13 @@ function attachBody(grid) {
3943
3978
  }
3944
3979
  return false;
3945
3980
  };
3981
+ const onImeKey = (e) => {
3982
+ const f = grid.store.state.focus;
3983
+ if (e.isComposing || e.key !== "Process" || e.ctrlKey || e.metaKey || e.altKey) return;
3984
+ if (!f || !grid.editor.canEdit(f.rowIndex, f.colId)) return;
3985
+ if (grid.columns.getColumn(f.colId).def.type === "boolean") return;
3986
+ grid.editor.start(f.rowIndex, f.colId, "");
3987
+ };
3946
3988
  const onExpandKey = (e) => {
3947
3989
  const f = grid.store.state.focus;
3948
3990
  if (!f || e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) return false;
@@ -4092,6 +4134,10 @@ function attachBody(grid) {
4092
4134
  const cols = grid.columns.displayed();
4093
4135
  const count = grid.rowModel.getRowCount();
4094
4136
  if (!cols.length || !count) return;
4137
+ if (imeKey(e)) {
4138
+ onImeKey(e);
4139
+ return;
4140
+ }
4095
4141
  if (onExpandKey(e)) {
4096
4142
  e.preventDefault();
4097
4143
  return;
@@ -4156,14 +4202,14 @@ function attachBody(grid) {
4156
4202
  const nested = (e) => e.target?.closest?.(".fg-root") !== root;
4157
4203
  const onCopy = (e) => {
4158
4204
  if (fromField(e) || nested(e)) return;
4159
- const text2 = grid.selectionTSV(false);
4205
+ const text2 = grid.copyText(false);
4160
4206
  if (!text2 || !e.clipboardData) return;
4161
4207
  e.clipboardData.setData("text/plain", text2);
4162
4208
  e.preventDefault();
4163
4209
  };
4164
4210
  const onCut = (e) => {
4165
4211
  if (fromField(e) || nested(e)) return;
4166
- const text2 = grid.selectionTSV(false);
4212
+ const text2 = grid.copyText(false);
4167
4213
  if (!text2 || !e.clipboardData) return;
4168
4214
  e.clipboardData.setData("text/plain", text2);
4169
4215
  e.preventDefault();
@@ -4530,7 +4576,9 @@ var ColumnChooserDialog = class {
4530
4576
  if (!eventInside(e, popup)) this.close();
4531
4577
  };
4532
4578
  const onKey = (e) => {
4533
- if (e.key === "Escape") this.close();
4579
+ if (e.key !== "Escape") return;
4580
+ this.close();
4581
+ this.grid.renderer.gridEl.focus();
4534
4582
  };
4535
4583
  document.addEventListener("pointerdown", onDown, true);
4536
4584
  document.addEventListener("keydown", onKey, true);
@@ -4669,12 +4717,13 @@ var ContextMenu = class {
4669
4717
  const node = grid.columns.getNode(colId);
4670
4718
  const def = node?.def;
4671
4719
  const dataCol = !!def && !isSystemCol(colId);
4720
+ const checked = grid.store.state.selected.size ? grid.shownSelectedRows() : [];
4672
4721
  const items = [
4673
4722
  { label: loc.copy, shortcut: shortcut("C"), action: () => grid.copyToClipboard(false) },
4674
4723
  { label: loc.copyWithHeaders, action: () => grid.copyToClipboard(true) },
4675
4724
  // after the copy items, so defaultItems indexes from earlier versions still point at the same entries
4676
4725
  { label: loc.cut, shortcut: shortcut("X"), action: () => grid.cutToClipboard() },
4677
- ...grid.store.state.selected.size ? [{ label: loc.copyRows, action: () => grid.copyToClipboard(false, true) }] : [],
4726
+ ...checked.length ? [{ label: loc.copyRows, action: () => grid.copyToClipboard(false, true) }] : [],
4678
4727
  { label: loc.paste, shortcut: shortcut("V"), action: () => this.pasteFromClipboard() }
4679
4728
  ];
4680
4729
  if (grid.history.canUndo() || grid.history.canRedo()) {
@@ -4684,8 +4733,8 @@ var ContextMenu = class {
4684
4733
  { label: loc.redo, shortcut: shortcut("Y"), action: () => grid.redo(), disabled: !grid.history.canRedo() }
4685
4734
  );
4686
4735
  }
4687
- if (grid.options.rowSelection && grid.store.state.selected.size) {
4688
- items.push({ label: loc.deleteRows, action: () => grid.api.removeSelectedRows() });
4736
+ if (grid.options.rowSelection && checked.length) {
4737
+ items.push({ label: loc.deleteRows, action: () => grid.removeRows(checked) });
4689
4738
  }
4690
4739
  if (dataCol) {
4691
4740
  items.push("separator", { label: loc.autosizeThis, action: () => grid.autoSize([colId]) });
@@ -4713,7 +4762,6 @@ var ContextMenu = class {
4713
4762
  { label: loc.exportCsv, action: () => grid.api.exportCsv() },
4714
4763
  ...grid.paid ? [{ label: loc.exportExcel, action: () => grid.api.exportXlsx() }] : [],
4715
4764
  { label: loc.exportJson, action: () => grid.api.exportJson() },
4716
- ...grid.options.assistant?.url && grid.paid ? [{ label: `${loc.ai.title}\u2026`, action: () => grid.api.openAssistant() }] : [],
4717
4765
  ...grid.paid ? [{ label: loc.exportPdf, action: () => grid.api.exportPdf() }] : []
4718
4766
  );
4719
4767
  if (grid.options.advancedFilter !== false && !grid.options.pivot && grid.paid) items.push("separator", { label: `${loc.advancedFilter}\u2026`, action: () => grid.filterBuilder.open() });
@@ -4780,7 +4828,9 @@ var ContextMenu = class {
4780
4828
  popupParent(this.grid.renderer.root).append(popup);
4781
4829
  const w = popup.offsetWidth || 220, h = popup.offsetHeight || 300;
4782
4830
  popup.style.left = `${Math.max(4, Math.min(x, window.innerWidth - w - 4))}px`;
4783
- popup.style.top = `${Math.max(4, Math.min(y, window.innerHeight - h - 4))}px`;
4831
+ const top = Math.max(4, Math.min(y, window.innerHeight - h - 4));
4832
+ popup.style.top = `${top}px`;
4833
+ popup.style.maxHeight = `${Math.max(120, window.innerHeight - top - 8)}px`;
4784
4834
  this.popup = popup;
4785
4835
  const onDown = (e) => {
4786
4836
  if (!eventInside(e, popup)) this.close();
@@ -5061,6 +5111,7 @@ var Finder = class {
5061
5111
  input.addEventListener("input", () => this.find(input.value));
5062
5112
  bar.addEventListener("keydown", (e) => {
5063
5113
  e.stopPropagation();
5114
+ if (imeKey(e)) return;
5064
5115
  if (e.key === "Escape") {
5065
5116
  e.preventDefault();
5066
5117
  this.closeBar();
@@ -5070,10 +5121,28 @@ var Finder = class {
5070
5121
  else this.next();
5071
5122
  }
5072
5123
  });
5073
- this.grid.renderer.root.append(bar);
5124
+ const { root, headerEl, main } = this.grid.renderer;
5125
+ root.append(bar);
5126
+ const box = root.getBoundingClientRect(), head = headerEl.getBoundingClientRect(), area = main.getBoundingClientRect();
5127
+ if (head.height) bar.style.top = `${Math.round(head.bottom - box.top) + 6}px`;
5128
+ if (area.width) bar.style.right = `${Math.max(0, Math.round(box.right - area.right)) + 18}px`;
5074
5129
  this.bar = bar;
5075
5130
  input.focus();
5076
5131
  }
5132
+ /** setLocale: the open bar's placeholder, button labels and "no matches" in the new language. */
5133
+ relabel() {
5134
+ if (!this.bar) return;
5135
+ const loc = this.grid.store.state.locale;
5136
+ const input = this.bar.querySelector("input");
5137
+ input.placeholder = loc.findPlaceholder;
5138
+ input.setAttribute("aria-label", loc.findPlaceholder);
5139
+ const labels = [loc.findPrevious, loc.findNext, loc.close];
5140
+ this.bar.querySelectorAll(".fg-findbar-btn").forEach((b, i) => {
5141
+ b.setAttribute("aria-label", labels[i]);
5142
+ b.title = labels[i];
5143
+ });
5144
+ this.updateCount();
5145
+ }
5077
5146
  closeBar() {
5078
5147
  this.bar?.remove();
5079
5148
  this.bar = null;
@@ -5326,7 +5395,7 @@ var CellEditor = class {
5326
5395
  const col = this.grid.columns.getColumn(colId);
5327
5396
  if (!col?.def.field || rowIndex < 0 || rowIndex >= this.grid.rowModel.getRowCount()) return false;
5328
5397
  const node = this.grid.rowModel.getRow(rowIndex);
5329
- return !node.kind && cellEditable(col.def, node.data, rowIndex);
5398
+ return !node.kind && this.grid.cellEditable(col.def, node.data, rowIndex);
5330
5399
  }
5331
5400
  start(rowIndex, colId, initialText) {
5332
5401
  if (this.cur && !this.commit(null)) return false;
@@ -5399,6 +5468,7 @@ var CellEditor = class {
5399
5468
  wrap2.append(msg);
5400
5469
  const onKey = (e) => {
5401
5470
  e.stopPropagation();
5471
+ if (imeKey(e)) return;
5402
5472
  if (e.key === "Escape") {
5403
5473
  e.preventDefault();
5404
5474
  this.cancel();
@@ -5552,6 +5622,7 @@ var CellEditor = class {
5552
5622
  i = cols.length - 1;
5553
5623
  if (!this.session) r--;
5554
5624
  }
5625
+ while (r !== rowIndex && r >= 0 && r < count && grid.rowModel.getRow(r)?.kind) r += step;
5555
5626
  if (r < 0 || r >= count) return;
5556
5627
  this.start(r, cols[i].id);
5557
5628
  }
@@ -6661,9 +6732,9 @@ function exportColumns(grid, ids) {
6661
6732
  return grid.columns.displayed().filter((c) => !isSystemCol(c.id)).map((c) => ({ id: c.id, def: c.def, node: c.node, width: c.width }));
6662
6733
  }
6663
6734
  function exportRows(grid, mode) {
6664
- if (mode === "selected") return grid.selectedRows();
6735
+ if (mode === "selected") return grid.shownSelectedRows();
6665
6736
  if (mode === "all") return grid.rowModel.server ? grid.rowModel.dataRows().map((n) => n.data) : grid.viewRows();
6666
- return grid.rowModel.dataRows().map((n) => n.data);
6737
+ return grid.orderedDataRows().map((n) => n.data);
6667
6738
  }
6668
6739
  var header = (def, id) => def.header ?? def.field ?? id;
6669
6740
  var ALIGNS = ["left", "center", "right"];
@@ -7118,8 +7189,10 @@ function buildXlsxSheet(grid, opts = {}) {
7118
7189
  return d ? { v: d, fmt } : String(v);
7119
7190
  }
7120
7191
  case "boolean":
7192
+ if (typeof v === "number" && Number.isFinite(v)) return v;
7121
7193
  return typeof v === "boolean" ? v : String(v);
7122
7194
  default:
7195
+ if (typeof v === "number" && Number.isFinite(v)) return v;
7123
7196
  return formatters[i](v);
7124
7197
  }
7125
7198
  };
@@ -7754,6 +7827,44 @@ function buildPivot(source, config, defOf, labels, locale) {
7754
7827
  });
7755
7828
  return { columns: [...rowColumns, ...valueColumns], rows, groupBy: cfg.rows.slice(0, -1) };
7756
7829
  }
7830
+ var ROLLUP = /* @__PURE__ */ new WeakMap();
7831
+ function rollupSource(row) {
7832
+ return ROLLUP.get(row);
7833
+ }
7834
+ function buildRollup(source, cols, by) {
7835
+ const isKey = (c) => by ? by.includes(c.id) : !c.def.aggFunc;
7836
+ const keys = cols.filter(isKey).map((c) => c.def);
7837
+ const rest = cols.filter((c) => !isKey(c) && c.def.field && !c.def.valueGetter).map((c) => c.def);
7838
+ const values = rest.filter((d) => d.aggFunc);
7839
+ const others = rest.filter((d) => !d.aggFunc);
7840
+ const buckets = /* @__PURE__ */ new Map();
7841
+ for (const r of source) {
7842
+ const k = keys.map((d) => setKey(getCellValue(d, r))).join("");
7843
+ let list = buckets.get(k);
7844
+ if (!list) buckets.set(k, list = []);
7845
+ list.push(r);
7846
+ }
7847
+ const rows = [];
7848
+ for (const src of buckets.values()) {
7849
+ const row = { ...src[0] };
7850
+ for (const d of values) {
7851
+ try {
7852
+ row[d.field] = aggregate(d.aggFunc, src.map((r) => getCellValue(d, r)), src);
7853
+ } catch {
7854
+ row[d.field] = null;
7855
+ }
7856
+ }
7857
+ if (src.length > 1) {
7858
+ for (const d of others) {
7859
+ const first = setKey(getCellValue(d, src[0]));
7860
+ if (src.some((r) => setKey(getCellValue(d, r)) !== first)) row[d.field] = null;
7861
+ }
7862
+ }
7863
+ ROLLUP.set(row, src);
7864
+ rows.push(row);
7865
+ }
7866
+ return rows;
7867
+ }
7757
7868
 
7758
7869
  // src/features.ts
7759
7870
  var PAID_FEATURES = {
@@ -7773,13 +7884,12 @@ var PAID_FEATURES = {
7773
7884
  formulas: { en: "Formulas", ko: "\uC218\uC2DD" },
7774
7885
  find: { en: "Find", ko: "\uCC3E\uAE30" },
7775
7886
  conditional: { en: "Conditional formatting", ko: "\uC870\uAC74\uBD80 \uC11C\uC2DD" },
7776
- assistant: { en: "AI assistant", ko: "AI \uC5B4\uC2DC\uC2A4\uD134\uD2B8" },
7777
7887
  merge: { en: "Merged cells", ko: "\uC140 \uBCD1\uD569" },
7778
7888
  workbook: { en: "Workbook", ko: "\uC6CC\uD06C\uBD81" }
7779
7889
  };
7780
7890
 
7781
7891
  // src/version.ts
7782
- var version = "1.9.5";
7892
+ var version = "1.11.0";
7783
7893
 
7784
7894
  // src/license.ts
7785
7895
  var PUBLIC_KEY = { kty: "EC", crv: "P-256", x: "sUTKkglZmk-HMKPNub8p7AeLBKesHCU8irIkw9rJlbE", y: "j0U_aKRDR7iApY9UpaD3hUemhBLZJwDv9-QiT_hUyw8" };
@@ -8078,6 +8188,12 @@ var ColumnMenu = class {
8078
8188
  if (grouped.length) g.push([loc.expandAll, () => this.grid.expandAll(true)], [loc.collapseAll, () => this.grid.expandAll(false)]);
8079
8189
  popup.append(this.section(g));
8080
8190
  }
8191
+ if (this.grid.getRollup() && !this.grid.isRollupKey(colId)) {
8192
+ const cur = def.aggFunc ?? null;
8193
+ const mark = (on, label) => `${on ? "\u2713 " : ""}${label}`;
8194
+ const aggs = ["sum", "avg", "min", "max", "count"].map((a) => [mark(cur === a, loc[a]), () => this.grid.setColumnAggFunc(colId, a)]);
8195
+ popup.append(this.section([[mark(!cur, loc.noAggregate), () => this.grid.setColumnAggFunc(colId, null)], ...aggs]));
8196
+ }
8081
8197
  const items = [
8082
8198
  [loc.autosizeThis, () => this.grid.autoSize([colId])],
8083
8199
  [loc.autosizeAll, () => this.grid.autoSize()]
@@ -8105,14 +8221,30 @@ var ColumnMenu = class {
8105
8221
  this.close();
8106
8222
  this.grid.renderer.gridEl.focus();
8107
8223
  };
8224
+ const onScroll = () => this.close();
8225
+ const scroller = this.grid.renderer.scroller;
8108
8226
  document.addEventListener("pointerdown", onDown, true);
8109
8227
  document.addEventListener("keydown", onKey, true);
8228
+ scroller.addEventListener("scroll", onScroll, { passive: true });
8110
8229
  this.off.push(
8111
8230
  () => document.removeEventListener("pointerdown", onDown, true),
8112
- () => document.removeEventListener("keydown", onKey, true)
8231
+ () => document.removeEventListener("keydown", onKey, true),
8232
+ () => scroller.removeEventListener("scroll", onScroll)
8113
8233
  );
8114
8234
  popup.querySelector(".fg-filter-value:not([hidden]), .fg-set-search, .fg-menu-item")?.focus();
8115
8235
  }
8236
+ /** Keyboard (Alt+ArrowDown): the menu of the focused cell's column (else the first column), under its header. */
8237
+ openAtFocus() {
8238
+ const { grid } = this;
8239
+ const f = grid.store.state.focus;
8240
+ const colId = f && !isSystemCol(f.colId) ? f.colId : grid.columns.displayed().find((c) => !isSystemCol(c.id))?.id;
8241
+ if (!colId) return false;
8242
+ const root = grid.renderer.root;
8243
+ const id = CSS.escape(colId);
8244
+ const anchor = root.querySelector(`.fg-header-cell:not(.fg-header-group)[data-col="${id}"]`) ?? (f ? root.querySelector(`.fg-row[data-row="${f.rowIndex}"] .fg-cell[data-col="${id}"]`) : null) ?? grid.renderer.gridEl;
8245
+ this.open(colId, anchor);
8246
+ return true;
8247
+ }
8116
8248
  /** The filter UI of a column (used by the menu and the filters panel); `done` runs after apply/reset. */
8117
8249
  filterSection(colId, done) {
8118
8250
  const node = this.grid.columns.getNode(colId);
@@ -8137,6 +8269,8 @@ var ColumnMenu = class {
8137
8269
  b.addEventListener("click", () => {
8138
8270
  run2();
8139
8271
  this.close();
8272
+ const a = activeElement(this.grid.renderer.root);
8273
+ if (!a || a === document.body || !a.isConnected) this.grid.renderer.gridEl.focus({ preventScroll: true });
8140
8274
  });
8141
8275
  s.append(b);
8142
8276
  }
@@ -8191,7 +8325,7 @@ var ColumnMenu = class {
8191
8325
  };
8192
8326
  select.addEventListener("change", sync);
8193
8327
  sync();
8194
- for (const input of [v1, v2]) input.addEventListener("keydown", (e) => e.key === "Enter" && apply());
8328
+ for (const input of [v1, v2]) input.addEventListener("keydown", (e) => e.key === "Enter" && !imeKey(e) && apply());
8195
8329
  row.append(select, v1, v2);
8196
8330
  const read = () => {
8197
8331
  const op = select.value;
@@ -8303,7 +8437,7 @@ var ColumnMenu = class {
8303
8437
  grid.setFilter(colId, checked.size === distinct.size ? null : { type: "set", values: entries.filter(([k]) => checked.has(k)).map(([, v]) => v) });
8304
8438
  done();
8305
8439
  };
8306
- search.addEventListener("keydown", (e) => e.key === "Enter" && apply());
8440
+ search.addEventListener("keydown", (e) => e.key === "Enter" && !imeKey(e) && apply());
8307
8441
  draw();
8308
8442
  box.append(this.title(loc), search, allLabel, list, this.actions(colId, loc, apply, done));
8309
8443
  return box;
@@ -8333,6 +8467,7 @@ var RowModel = class {
8333
8467
  this.sorted = [];
8334
8468
  this.display = [];
8335
8469
  this.indexById = null;
8470
+ this.orderedCache = null;
8336
8471
  this.layoutCache = null;
8337
8472
  /** Searchable text of each row, kept between quick filter keystrokes. */
8338
8473
  this.quickCache = null;
@@ -8391,6 +8526,13 @@ var RowModel = class {
8391
8526
  fullCount() {
8392
8527
  return this.full.length;
8393
8528
  }
8529
+ dataRowsBefore(index) {
8530
+ const end = Math.min(index, this.full.length);
8531
+ if (this.full === this.sorted) return end;
8532
+ let n = 0;
8533
+ for (let k = 0; k < end; k++) if (!this.full[k].kind) n++;
8534
+ return n;
8535
+ }
8394
8536
  dataCount() {
8395
8537
  return this.sorted.length;
8396
8538
  }
@@ -8411,6 +8553,19 @@ var RowModel = class {
8411
8553
  dataRows() {
8412
8554
  return this.sorted;
8413
8555
  }
8556
+ /** Filtered data rows in display order, as if every group and tree row were expanded. */
8557
+ orderedDataRows() {
8558
+ const c = this.orderedCache;
8559
+ if (c && c.full === this.full) return c.rows;
8560
+ const d = this.last.display;
8561
+ let rows = this.sorted;
8562
+ if (d && (d.getParentId || d.groupBy.length)) {
8563
+ const open = { ...d, expanded: /* @__PURE__ */ new Map(), defaultExpanded: -1, detailOpen: /* @__PURE__ */ new Set(), lazyChildren: void 0 };
8564
+ rows = this.buildDisplay(open, this.last.sort ?? [], this.last.collator ?? this.deps.collator()).filter((n) => !n.kind);
8565
+ }
8566
+ this.orderedCache = { full: this.full, rows };
8567
+ return rows;
8568
+ }
8414
8569
  allNodes() {
8415
8570
  return this.nodes;
8416
8571
  }
@@ -8813,6 +8968,7 @@ var ServerRowModel = class {
8813
8968
  /** Server-side grouping, while groupBy has columns. */
8814
8969
  this.groups = null;
8815
8970
  this.displayIn = null;
8971
+ this.quiet = false;
8816
8972
  }
8817
8973
  get bs() {
8818
8974
  return this.page ? this.page.size : this.deps.blockSize;
@@ -8911,9 +9067,19 @@ var ServerRowModel = class {
8911
9067
  const abs = this.base + index;
8912
9068
  const hit = this.peek(abs);
8913
9069
  if (hit) return hit;
8914
- if (index >= 0 && index < this.getRowCount()) this.ensure(Math.floor(abs / this.bs));
9070
+ if (!this.quiet && index >= 0 && index < this.getRowCount()) this.ensure(Math.floor(abs / this.bs));
8915
9071
  return this.placeholder(abs);
8916
9072
  }
9073
+ /** Run `fn` with getRow answering from loaded blocks only (rows not loaded stay placeholders, nothing is requested). */
9074
+ withoutLoading(fn) {
9075
+ const was = this.quiet;
9076
+ this.quiet = true;
9077
+ try {
9078
+ return fn();
9079
+ } finally {
9080
+ this.quiet = was;
9081
+ }
9082
+ }
8917
9083
  ensure(k) {
8918
9084
  const cur = this.blocks.get(k);
8919
9085
  if (cur && (cur.state !== "failed" || Date.now() < (cur.retryAt ?? 0))) return;
@@ -9335,17 +9501,23 @@ var FilterBuilder = class {
9335
9501
  actions.append(reset, ok);
9336
9502
  popup.append(head, join, list, addBtn, actions);
9337
9503
  popupParent(grid.renderer.root).append(popup);
9504
+ if (!rows.length) addBtn.click();
9338
9505
  const r = grid.renderer.root.getBoundingClientRect();
9339
9506
  const w = popup.offsetWidth || 460;
9340
9507
  popup.style.left = `${Math.max(4, Math.min(r.left + 24, (window.innerWidth || 1024) - w - 4))}px`;
9341
- popup.style.top = `${Math.max(4, r.top + 24)}px`;
9508
+ const header2 = grid.renderer.headerEl.getBoundingClientRect();
9509
+ const vh = window.innerHeight || 768;
9510
+ const top = Math.max(4, Math.min((header2.height ? header2.bottom : r.top) + 8, vh - (popup.offsetHeight || 0) - 4));
9511
+ popup.style.top = `${top}px`;
9512
+ popup.style.maxHeight = `${Math.max(160, vh - top - 8)}px`;
9342
9513
  this.popup = popup;
9343
- if (!rows.length) addBtn.click();
9344
9514
  const onDown = (e) => {
9345
9515
  if (!eventInside(e, popup)) this.close();
9346
9516
  };
9347
9517
  const onKey = (e) => {
9348
- if (e.key === "Escape") this.close();
9518
+ if (e.key !== "Escape") return;
9519
+ this.close();
9520
+ grid.renderer.gridEl.focus();
9349
9521
  };
9350
9522
  document.addEventListener("pointerdown", onDown, true);
9351
9523
  document.addEventListener("keydown", onKey, true);
@@ -9483,8 +9655,8 @@ var FormatDialog = class {
9483
9655
  CF_PRESETS.forEach((p, k) => {
9484
9656
  const b = el("button", "fg-cf-swatch", { type: "button", "aria-label": cf.styles[p.key], title: cf.styles[p.key], "aria-pressed": String(k === preset) });
9485
9657
  b.textContent = p.key === "bold" ? "B" : "Aa";
9486
- if (p.style.background) b.style.background = p.style.background;
9487
- if (p.style.color) b.style.color = p.style.color;
9658
+ b.style.background = p.style.background ?? "#ffffff";
9659
+ b.style.color = p.style.color ?? "#1f2430";
9488
9660
  b.addEventListener("click", () => {
9489
9661
  preset = k;
9490
9662
  for (const s of swatches.children) s.setAttribute("aria-pressed", String(s === b));
@@ -9583,348 +9755,9 @@ var FormatDialog = class {
9583
9755
  if (!eventInside(e, popup)) this.close();
9584
9756
  };
9585
9757
  const onKey = (e) => {
9586
- if (e.key === "Escape") this.close();
9587
- };
9588
- document.addEventListener("pointerdown", onDown, true);
9589
- document.addEventListener("keydown", onKey, true);
9590
- this.off.push(
9591
- () => document.removeEventListener("pointerdown", onDown, true),
9592
- () => document.removeEventListener("keydown", onKey, true)
9593
- );
9594
- }
9595
- close() {
9596
- for (const f of this.off) f();
9597
- this.off = [];
9598
- this.popup?.remove();
9599
- this.popup = null;
9600
- }
9601
- };
9602
-
9603
- // src/assistant.ts
9604
- var FILTER_TYPES = ["text", "number", "date", "set", "combined"];
9605
- var CHART_TYPES = ["bar", "stackedBar", "line", "area", "pie", "donut", "combo", "scatter"];
9606
- var MAX_ACTIONS = 30;
9607
- function describeColumn(grid, id, def, rows) {
9608
- const col = { id, header: def.header ?? def.field ?? id };
9609
- if (def.type) col.type = def.type;
9610
- if (def.formula) col.formula = def.formula;
9611
- if (def.aggFunc && typeof def.aggFunc === "string") col.aggFunc = def.aggFunc;
9612
- let blanks = 0;
9613
- const counts = /* @__PURE__ */ new Map();
9614
- let min = Infinity, max = -Infinity, sum = 0, nums = 0;
9615
- let minText = "", maxText = "";
9616
- for (const r of rows) {
9617
- const v = getCellValue(def, r);
9618
- if (isBlank(v)) {
9619
- blanks++;
9620
- continue;
9621
- }
9622
- if (typeof v === "number" && Number.isFinite(v)) {
9623
- nums++;
9624
- sum += v;
9625
- if (v < min) min = v;
9626
- if (v > max) max = v;
9627
- } else if (def.type === "date") {
9628
- const t = v instanceof Date ? v.toISOString().slice(0, 10) : String(v);
9629
- if (!minText || t < minText) minText = t;
9630
- if (!maxText || t > maxText) maxText = t;
9631
- }
9632
- if (counts.size < 5e3) {
9633
- const k = setKey(v);
9634
- const c = counts.get(k);
9635
- if (c) c.n++;
9636
- else counts.set(k, { value: v, n: 1 });
9637
- }
9638
- }
9639
- col.blanks = blanks;
9640
- col.distinct = counts.size;
9641
- if (nums) Object.assign(col, { min, max, avg: Math.round(sum / nums * 1e3) / 1e3 });
9642
- if (minText) Object.assign(col, { min: minText, max: maxText });
9643
- if (!nums && counts.size) {
9644
- col.top = [...counts.values()].sort((a, b) => b.n - a.n).slice(0, 12).map((c) => ({ value: String(c.value).slice(0, 80), count: c.n }));
9645
- }
9646
- return col;
9647
- }
9648
- function assistantRequest(grid, prompt, options) {
9649
- const rows = grid.rowModel.dataRows().map((n) => n.data);
9650
- const all2 = grid.columns.getState().map((s2) => grid.columns.getNode(s2.colId)).filter((n) => n && !n.children && !isSystemCol(n.id));
9651
- const columns = all2.map((n) => ({ ...describeColumn(grid, n.id, n.def, rows), hidden: grid.columns.getState().find((s2) => s2.colId === n.id)?.hide || void 0 }));
9652
- const sampleN = typeof options.sampleRows === "number" && options.sampleRows >= 0 ? Math.min(50, options.sampleRows) : 5;
9653
- const sample = rows.slice(0, sampleN).map((r) => Object.fromEntries(all2.map((n) => [n.id, getCellValue(n.def, r)])));
9654
- const s = grid.store.state;
9655
- return {
9656
- prompt: String(prompt).slice(0, 2e3),
9657
- locale: s.locale.code,
9658
- rowCount: rows.length,
9659
- totalRows: grid.rowModel.server ? rows.length : grid.store.state.data.length,
9660
- columns,
9661
- sample,
9662
- state: {
9663
- sort: s.sort,
9664
- filters: s.filters,
9665
- quickFilter: s.quickFilter,
9666
- advancedFilter: s.advancedFilter,
9667
- groupBy: grid.getGroupBy(),
9668
- pivot: grid.getPivot(),
9669
- conditionalFormats: grid.getConditionalFormats()
9670
- },
9671
- ...options.context ? { context: options.context() } : {}
9672
- };
9673
- }
9674
- function applyAssistantActions(grid, actions) {
9675
- const before = { state: grid.api.getState(), defs: grid.columnDefs(), summary: grid.summaryOptions() };
9676
- const applied = [];
9677
- const skipped = [];
9678
- const charts = [];
9679
- const list = Array.isArray(actions) ? actions.slice(0, MAX_ACTIONS) : [];
9680
- const known = (id) => typeof id === "string" && !!grid.columns.getNode(id) && !grid.columns.getNode(id).children && !isSystemCol(id);
9681
- const ok = (a) => applied.push(a);
9682
- const skip = (a, reason) => skipped.push({ action: a, reason });
9683
- for (const raw of list) {
9684
- const a = raw;
9685
- if (!a || typeof a !== "object" || typeof a.type !== "string") {
9686
- skip(raw, "not an action");
9687
- continue;
9688
- }
9689
- try {
9690
- switch (a.type) {
9691
- case "sort": {
9692
- const sort = (Array.isArray(a.sort) ? a.sort : []).filter((x) => !!x && known(x.colId) && (x.dir === "asc" || x.dir === "desc"));
9693
- if (Array.isArray(a.sort) && sort.length !== a.sort.length) skip(a, "unknown column in sort");
9694
- grid.setSort(sort.map((x) => ({ colId: x.colId, dir: x.dir })));
9695
- ok(a);
9696
- break;
9697
- }
9698
- case "filter": {
9699
- if (!known(a.colId)) {
9700
- skip(a, "unknown column");
9701
- break;
9702
- }
9703
- const m = a.model;
9704
- if (m !== null && (!m || typeof m !== "object" || !FILTER_TYPES.includes(m.type))) {
9705
- skip(a, "bad filter model");
9706
- break;
9707
- }
9708
- grid.setFilter(a.colId, m);
9709
- ok(a);
9710
- break;
9711
- }
9712
- case "clearFilters":
9713
- for (const id of Object.keys(grid.store.state.filters)) grid.setFilter(id, null);
9714
- grid.setAdvancedFilter(null);
9715
- grid.api.setQuickFilter("");
9716
- ok(a);
9717
- break;
9718
- case "quickFilter":
9719
- grid.api.setQuickFilter(typeof a.text === "string" ? a.text.slice(0, 200) : "");
9720
- ok(a);
9721
- break;
9722
- case "advancedFilter": {
9723
- const f = a.filter;
9724
- const ids = [];
9725
- const walk = (x) => {
9726
- const o = x;
9727
- if (Array.isArray(o?.conditions)) o.conditions.forEach(walk);
9728
- else ids.push(o?.colId);
9729
- };
9730
- if (f) walk(f);
9731
- if (f && (!Array.isArray(f.conditions) || !ids.every(known))) {
9732
- skip(a, "unknown column in advanced filter");
9733
- break;
9734
- }
9735
- grid.setAdvancedFilter(f ?? null);
9736
- ok(a);
9737
- break;
9738
- }
9739
- case "groupBy": {
9740
- const ids = Array.isArray(a.colIds) ? a.colIds.filter(known) : [];
9741
- grid.setGroupBy(ids);
9742
- if (a.expand) grid.expandAll(true);
9743
- ok(a);
9744
- break;
9745
- }
9746
- case "pivot": {
9747
- const p = a.pivot;
9748
- if (p && (!Array.isArray(p.rows) || !Array.isArray(p.values) || !p.rows.every(known) || !(p.columns ?? []).every(known) || !p.values.every((v) => v && known(v.colId)))) {
9749
- skip(a, "unknown column in pivot");
9750
- break;
9751
- }
9752
- grid.setPivot(p ?? null);
9753
- ok(a);
9754
- break;
9755
- }
9756
- case "showColumns":
9757
- case "hideColumns": {
9758
- const ids = Array.isArray(a.colIds) ? a.colIds.filter(known) : [];
9759
- if (ids.length) grid.setColumnsVisible(ids, a.type === "showColumns");
9760
- ok(a);
9761
- break;
9762
- }
9763
- case "conditionalFormat": {
9764
- const f = a.format;
9765
- if (!f || !Array.isArray(f.columns) || !f.columns.length || !f.columns.every(known)) {
9766
- skip(a, "unknown column in format");
9767
- break;
9768
- }
9769
- grid.setConditionalFormats([...grid.getConditionalFormats(), f]);
9770
- ok(a);
9771
- break;
9772
- }
9773
- case "clearConditionalFormats":
9774
- grid.setConditionalFormats([]);
9775
- ok(a);
9776
- break;
9777
- case "addColumn": {
9778
- const c = a.column;
9779
- if (!c || typeof c.id !== "string" || !/^[\w.-]{1,40}$/.test(c.id) || grid.columns.getNode(c.id)) {
9780
- skip(a, "bad or taken column id");
9781
- break;
9782
- }
9783
- const f = typeof c.formula === "string" ? c.formula.startsWith("=") ? c.formula : `=${c.formula}` : "";
9784
- const refs = f ? formulaRefs(f) : null;
9785
- if (!refs) {
9786
- skip(a, "formula does not parse");
9787
- break;
9788
- }
9789
- const def = { id: c.id, header: typeof c.header === "string" ? c.header.slice(0, 80) : c.id, formula: f };
9790
- if (c.type === "number" || c.type === "date" || c.type === "text") def.type = c.type;
9791
- grid.api.setColumns([...grid.columnDefs(), def]);
9792
- ok(a);
9793
- break;
9794
- }
9795
- case "summary": {
9796
- const s = a.summary;
9797
- if (s && (!Array.isArray(s.rows) || !s.rows.every((r) => r && Object.keys(r.columns ?? {}).every(known)))) {
9798
- skip(a, "unknown column in summary");
9799
- break;
9800
- }
9801
- grid.setSummary(s ?? null);
9802
- ok(a);
9803
- break;
9804
- }
9805
- case "chart": {
9806
- const ids = Array.isArray(a.colIds) ? a.colIds.filter(known) : [];
9807
- const displayed = grid.columns.displayed().map((c) => c.id);
9808
- const idx = ids.map((id) => displayed.indexOf(id)).filter((i) => i >= 0);
9809
- const count = grid.rowModel.getRowCount();
9810
- if (!idx.length || !count) {
9811
- skip(a, "nothing to chart");
9812
- break;
9813
- }
9814
- const range = { start: { rowIndex: 0, colId: displayed[Math.min(...idx)] }, end: { rowIndex: Math.min(count, 200) - 1, colId: displayed[Math.max(...idx)] } };
9815
- charts.push(grid.api.createChart({ type: CHART_TYPES.includes(a.chartType) ? a.chartType : "bar", range, title: typeof a.title === "string" ? a.title.slice(0, 80) : void 0 }));
9816
- ok(a);
9817
- break;
9818
- }
9819
- case "message":
9820
- ok(a);
9821
- break;
9822
- default:
9823
- skip(a, "unknown action");
9824
- }
9825
- } catch {
9826
- skip(a, "failed");
9827
- }
9828
- }
9829
- const undo = () => {
9830
- for (const c of charts) c.destroy();
9831
- if (grid.columnDefs().length !== before.defs.length || grid.columnDefs().some((d, i) => d !== before.defs[i])) grid.api.setColumns(before.defs);
9832
- grid.setSummary(before.summary);
9833
- grid.api.setState(before.state);
9834
- if (!before.state.conditionalFormats) grid.setConditionalFormats([]);
9835
- };
9836
- return { applied, skipped, undo };
9837
- }
9838
- async function askAssistant(grid, prompt, options, signal) {
9839
- if (!options?.url) throw new Error("assistant.url is not set");
9840
- const headers = typeof options.headers === "function" ? options.headers() : options.headers ?? {};
9841
- const res = await fetch(options.url, {
9842
- method: "POST",
9843
- credentials: options.credentials ?? "same-origin",
9844
- headers: { "Content-Type": "application/json", Accept: "application/json", ...headers },
9845
- body: JSON.stringify(assistantRequest(grid, prompt, options)),
9846
- signal
9847
- });
9848
- if (!res.ok) throw new Error(`HTTP ${res.status} from the assistant`);
9849
- const json = await res.json();
9850
- const result = applyAssistantActions(grid, Array.isArray(json?.actions) ? json.actions : []);
9851
- const say = typeof json?.message === "string" ? json.message : result.applied.filter((a) => a.type === "message").map((a) => a.text ?? "").join("\n");
9852
- return { ...result, message: say.slice(0, 4e3) };
9853
- }
9854
-
9855
- // src/interactions/assistantPanel.ts
9856
- var AssistantPanel = class {
9857
- constructor(grid) {
9858
- this.grid = grid;
9859
- this.popup = null;
9860
- this.log = null;
9861
- this.off = [];
9862
- this.busy = null;
9863
- }
9864
- get isOpen() {
9865
- return !!this.popup;
9866
- }
9867
- open() {
9868
- if (this.popup) {
9869
- this.popup.querySelector(".fg-ai-input")?.focus();
9870
- return;
9871
- }
9872
- const { grid } = this;
9873
- const loc = grid.store.state.locale;
9874
- const opts = grid.options.assistant;
9875
- const popup = el("div", `fg-menu fg-ai-panel fg-theme-${grid.store.state.theme}`, { role: "dialog", "aria-label": loc.ai.title });
9876
- grid.applyFontTo(popup);
9877
- const head = el("div", "fg-chooser-head");
9878
- const title = el("span", "fg-menu-title");
9879
- title.textContent = loc.ai.title;
9880
- const x = el("button", "fg-chooser-close", { type: "button", "aria-label": loc.close });
9881
- x.textContent = "\xD7";
9882
- x.addEventListener("click", () => this.close());
9883
- head.append(title, x);
9884
- const log = el("div", "fg-ai-log", { role: "log", "aria-live": "polite" });
9885
- this.log = log;
9886
- const form = el("form", "fg-ai-form");
9887
- const input = el("textarea", "fg-input fg-ai-input", { rows: "2", placeholder: loc.ai.placeholder, "aria-label": loc.ai.placeholder, maxlength: "2000" });
9888
- const send2 = el("button", "fg-btn fg-btn-primary fg-ai-send", { type: "submit" });
9889
- send2.textContent = loc.ai.send;
9890
- form.append(input, send2);
9891
- const tips = el("div", "fg-ai-tips");
9892
- for (const s of (opts?.suggestions ?? []).slice(0, 6)) {
9893
- const b = el("button", "fg-ai-tip", { type: "button" });
9894
- b.textContent = s;
9895
- b.addEventListener("click", () => {
9896
- input.value = s;
9897
- input.focus();
9898
- });
9899
- tips.append(b);
9900
- }
9901
- input.addEventListener("keydown", (e) => {
9902
- if (e.key === "Enter" && !e.shiftKey && !e.isComposing) {
9903
- e.preventDefault();
9904
- form.requestSubmit?.() ?? form.dispatchEvent(new Event("submit", { cancelable: true }));
9905
- }
9906
- });
9907
- form.addEventListener("submit", (e) => {
9908
- e.preventDefault();
9909
- const prompt = input.value.trim();
9910
- if (!prompt || this.busy) return;
9911
- input.value = "";
9912
- void this.ask(prompt, send2);
9913
- });
9914
- popup.append(head, log, form);
9915
- if (tips.childElementCount) popup.append(tips);
9916
- popupParent(grid.renderer.root).append(popup);
9917
- const r = grid.renderer.root.getBoundingClientRect();
9918
- const w = popup.offsetWidth || 360;
9919
- popup.style.left = `${Math.max(4, Math.min(r.right - w - 16, (window.innerWidth || 1024) - w - 4))}px`;
9920
- popup.style.top = `${Math.max(4, r.top + 16)}px`;
9921
- this.popup = popup;
9922
- input.focus();
9923
- const onDown = (e) => {
9924
- if (!eventInside(e, popup) && !this.busy) this.close();
9925
- };
9926
- const onKey = (e) => {
9927
- if (e.key === "Escape") this.close();
9758
+ if (e.key !== "Escape") return;
9759
+ this.close();
9760
+ grid.renderer.gridEl.focus();
9928
9761
  };
9929
9762
  document.addEventListener("pointerdown", onDown, true);
9930
9763
  document.addEventListener("keydown", onKey, true);
@@ -9933,58 +9766,11 @@ var AssistantPanel = class {
9933
9766
  () => document.removeEventListener("keydown", onKey, true)
9934
9767
  );
9935
9768
  }
9936
- say(who, text2) {
9937
- const msg = el("div", `fg-ai-msg fg-ai-${who}`);
9938
- msg.textContent = text2;
9939
- this.log?.append(msg);
9940
- if (this.log) this.log.scrollTop = this.log.scrollHeight;
9941
- return msg;
9942
- }
9943
- async ask(prompt, send2) {
9944
- const loc = this.grid.store.state.locale;
9945
- this.say("user", prompt);
9946
- const wait = this.say("ai", loc.ai.thinking);
9947
- wait.classList.add("fg-ai-wait");
9948
- this.busy = new AbortController();
9949
- send2.disabled = true;
9950
- try {
9951
- const res = await askAssistant(this.grid, prompt, this.grid.options.assistant, this.busy.signal);
9952
- wait.classList.remove("fg-ai-wait");
9953
- const changed = res.applied.filter((a) => a.type !== "message").length;
9954
- wait.textContent = res.message || (changed ? loc.ai.done : "");
9955
- if (res.skipped.length) {
9956
- const note = el("div", "fg-ai-note");
9957
- note.textContent = `${loc.ai.skipped}: ${res.skipped.map((s) => s.reason).join(", ")}`;
9958
- wait.append(note);
9959
- }
9960
- if (changed) {
9961
- const undo = el("button", "fg-btn fg-ai-undo", { type: "button" });
9962
- undo.textContent = loc.ai.undo;
9963
- undo.addEventListener("click", () => {
9964
- res.undo();
9965
- undo.disabled = true;
9966
- undo.textContent = loc.ai.undone;
9967
- });
9968
- wait.append(undo);
9969
- }
9970
- this.grid.events.emit("assistantAnswer", { prompt, message: res.message, applied: res.applied, skipped: res.skipped });
9971
- } catch (e) {
9972
- if (e?.name === "AbortError") return;
9973
- wait.remove();
9974
- this.say("error", loc.ai.failed);
9975
- } finally {
9976
- this.busy = null;
9977
- send2.disabled = false;
9978
- }
9979
- }
9980
9769
  close() {
9981
- this.busy?.abort();
9982
- this.busy = null;
9983
9770
  for (const f of this.off) f();
9984
9771
  this.off = [];
9985
9772
  this.popup?.remove();
9986
9773
  this.popup = null;
9987
- this.log = null;
9988
9774
  }
9989
9775
  };
9990
9776
 
@@ -10027,6 +9813,12 @@ var Grid = class {
10027
9813
  this.sourceDefs = [];
10028
9814
  this.pivotCfg = null;
10029
9815
  this.pivotData = null;
9816
+ /** rollup option / setRollup: rows merged by the shown columns without an aggFunc. */
9817
+ this.rollupOn = false;
9818
+ /** rollup { by }: the columns the rows merge by (only the shown ones count); null: every shown column without an aggFunc. */
9819
+ this.rollupBy = null;
9820
+ /** The merged rows while rollup applies (not while pivoting), else null. */
9821
+ this.rollupData = null;
10030
9822
  /** Grouping state from before pivot mode, restored when it ends. */
10031
9823
  this.savedGroup = null;
10032
9824
  /** 0-based page and rows per page (pagination only). */
@@ -10067,7 +9859,8 @@ var Grid = class {
10067
9859
  formula: (f, row) => evaluateFormula(f, row, /* @__PURE__ */ new Set(), 0, /* @__PURE__ */ new Map(), this.formulaScope)
10068
9860
  });
10069
9861
  this.formatDialog = new FormatDialog(this);
10070
- this.assistantPanel = new AssistantPanel(this);
9862
+ /** Selection waiting for the row model to hold the newly merged rows (set by updateRows). */
9863
+ this.rollupSelection = null;
10071
9864
  // ── pagination and server rows ──────────────────────────────
10072
9865
  /** Cells flashing after a transaction: `${rowId}\u0000${colId}` → time the flash ends. */
10073
9866
  /** showOverlay: an overlay the app asked for, shown whatever the row count. */
@@ -10093,6 +9886,8 @@ var Grid = class {
10093
9886
  }
10094
9887
  };
10095
9888
  this.density = "normal";
9889
+ /** Set by the cellPadding option or setCellPadding; null: the theme / density value. */
9890
+ this.cellPadding = null;
10096
9891
  // ── merge, aggregates, fill, pinned rows ────────────────────
10097
9892
  /** `sortWithinMerges` is on. */
10098
9893
  this.sortWithin = false;
@@ -10173,6 +9968,7 @@ var Grid = class {
10173
9968
  this.renderer.applyTheme();
10174
9969
  if (options.flashDuration) this.renderer.root.style.setProperty("--fg-flash-duration", `${this.flashMs()}ms`);
10175
9970
  this.renderer.applyFont(this.font.size, this.font.family);
9971
+ if (options.cellPadding != null) this.applyCellPadding(options.cellPadding);
10176
9972
  this.menu = new ColumnMenu(this);
10177
9973
  this.chooser = new ColumnChooserDialog(this);
10178
9974
  this.contextMenu = new ContextMenu(this);
@@ -10199,7 +9995,6 @@ var Grid = class {
10199
9995
  () => this.charts.destroy(),
10200
9996
  () => this.filterBuilder.close(),
10201
9997
  () => this.formatDialog.close(),
10202
- () => this.assistantPanel.close(),
10203
9998
  attachTooltips(this)
10204
9999
  );
10205
10000
  if (typeof ResizeObserver !== "undefined") {
@@ -10208,6 +10003,7 @@ var Grid = class {
10208
10003
  this.disposers.push(() => ro.disconnect());
10209
10004
  }
10210
10005
  if (options.pivot) this.setPivot(options.pivot);
10006
+ if (options.rollup) this.setRollup(options.rollup);
10211
10007
  void needsFreeBadge(options.licenseKey).then((show) => {
10212
10008
  if (show && !this.destroyed) this.renderer.showFreeBadge();
10213
10009
  });
@@ -10286,6 +10082,7 @@ var Grid = class {
10286
10082
  }
10287
10083
  /** Column definitions or visibility changed; filters/quick filter may depend on them. */
10288
10084
  columnsChanged() {
10085
+ this.rebuildRollup();
10289
10086
  this.rowModel.invalidate();
10290
10087
  this.updateRows();
10291
10088
  this.refreshSummary();
@@ -10308,17 +10105,102 @@ var Grid = class {
10308
10105
  }
10309
10106
  updateRows() {
10310
10107
  const paged = !!this.options.pagination;
10311
- const data = this.pivotData ?? this.store.state.data;
10108
+ const data = this.viewRows();
10312
10109
  this.rowModel.update({ ...this.store.state, data, display: this.display(), page: paged ? { index: this.page, size: this.pageSize } : null });
10110
+ const remapped = this.rollupSelection;
10111
+ if (remapped) {
10112
+ this.rollupSelection = null;
10113
+ this.setSelected(remapped);
10114
+ }
10313
10115
  if (paged && this.page >= this.getPageCount()) {
10314
10116
  this.page = this.getPageCount() - 1;
10315
10117
  this.rowModel.update({ ...this.store.state, data, display: this.display(), page: { index: this.page, size: this.pageSize } });
10316
10118
  }
10317
10119
  }
10318
10120
  // ── pivot ───────────────────────────────────────────────────
10319
- /** Rows behind the view: the pivot table while pivoting, else the source data. */
10121
+ /** Rows behind the view: the pivot table while pivoting, the merged rows with rollup, else the source data. */
10320
10122
  viewRows() {
10321
- return this.pivotData ?? this.store.state.data;
10123
+ return this.pivotData ?? this.rollupData ?? this.store.state.data;
10124
+ }
10125
+ // ── rollup ──────────────────────────────────────────────────
10126
+ getRollup() {
10127
+ return this.rollupBy ? { by: [...this.rollupBy] } : this.rollupOn;
10128
+ }
10129
+ /** true: merge by every shown column without an aggFunc; { by }: merge by the shown `by` columns; false: off. */
10130
+ setRollup(cfg) {
10131
+ const by = cfg && typeof cfg === "object" ? Array.isArray(cfg.by) ? cfg.by.filter((id) => typeof id === "string") : null : null;
10132
+ if (cfg && typeof cfg === "object" && !by) {
10133
+ warn("setRollup: expected true, false or { by: string[] }");
10134
+ return;
10135
+ }
10136
+ const on = !!cfg;
10137
+ if (on && !this.allow("pivot")) return;
10138
+ if (on && this.clientOnly("rollup")) return;
10139
+ if (on && this.options.treeData) {
10140
+ warn("rollup is not available with treeData");
10141
+ return;
10142
+ }
10143
+ if (on === this.rollupOn && (by ?? []).join("") === (this.rollupBy ?? []).join("") && !by === !this.rollupBy) return;
10144
+ this.editor.cancel();
10145
+ this.rollupOn = on;
10146
+ this.rollupBy = on ? by : null;
10147
+ this.store.set({ range: null, focus: null });
10148
+ this.columnsChanged();
10149
+ }
10150
+ /** Merge the source rows again (rollup on, not pivoting); the view is updated by the caller. */
10151
+ rebuildRollup() {
10152
+ const old = this.rollupData;
10153
+ this.rollupData = this.rollupOn && !this.pivotCfg ? buildRollup(this.store.state.data, this.dataColumns().map((c) => ({ id: c.id, def: c.def })), this.rollupBy) : null;
10154
+ if (old || this.rollupData) this.remapRollupSelection(old);
10155
+ }
10156
+ /**
10157
+ * Keep the selection across a re-merge: a selected merged row stands for its source rows, and a newly merged
10158
+ * row is selected when every one of its source rows was (so a delete never reaches a row nobody checked).
10159
+ */
10160
+ remapRollupSelection(old) {
10161
+ const sel = this.store.state.selected;
10162
+ if (!sel.size) return;
10163
+ const idOf = (r) => this.rowModel.idOf(r);
10164
+ const picked = old ? new Set(old.filter((r) => sel.has(idOf(r))).flatMap((r) => rollupSource(r) ?? [r]).map(idOf)) : sel;
10165
+ const now = this.rollupData;
10166
+ const next = now ? now.filter((r) => (rollupSource(r) ?? [r]).every((s) => picked.has(idOf(s)))).map(idOf) : this.store.state.data.map(idOf).filter((id) => picked.has(id));
10167
+ if (next.length !== sel.size || next.some((id) => !sel.has(id))) this.rollupSelection = new Set(next);
10168
+ }
10169
+ /** With rollup { by }: whether the rows merge by this column (it is one of `by`). */
10170
+ isRollupKey(colId) {
10171
+ return !!this.rollupBy?.includes(colId);
10172
+ }
10173
+ /** The source rows behind `rows`: merged rollup rows stand for the rows they merge (for group and summary values). */
10174
+ sourceRowsOf(rows) {
10175
+ return this.rollupData ? rows.flatMap((r) => rollupSource(r) ?? [r]) : rows;
10176
+ }
10177
+ /** Whether a cell can be changed: never on merged rollup rows. */
10178
+ cellEditable(def, row, rowIndex) {
10179
+ return !this.rollupData && cellEditable(def, row, rowIndex);
10180
+ }
10181
+ /** Set or clear a column's aggFunc (null: no aggregate, so with rollup it becomes a column the rows merge by). */
10182
+ setColumnAggFunc(colId, aggFunc) {
10183
+ if (aggFunc !== null && typeof aggFunc !== "function" && !["sum", "avg", "min", "max", "count"].includes(aggFunc)) {
10184
+ warn(`setColumnAggFunc: unknown aggFunc "${String(aggFunc)}"`);
10185
+ return;
10186
+ }
10187
+ let found = false;
10188
+ const map = (defs2) => defs2.map((d) => {
10189
+ if (!d || typeof d !== "object") return d;
10190
+ if (Array.isArray(d.children)) return { ...d, children: map(d.children) };
10191
+ if ((d.id ?? d.field) !== colId) return d;
10192
+ found = true;
10193
+ const next = { ...d };
10194
+ if (aggFunc) next.aggFunc = aggFunc;
10195
+ else delete next.aggFunc;
10196
+ return next;
10197
+ });
10198
+ const defs = map(this.sourceDefs);
10199
+ if (!found) {
10200
+ warn(`setColumnAggFunc: no column "${colId}"`);
10201
+ return;
10202
+ }
10203
+ this.setColumns(defs);
10322
10204
  }
10323
10205
  /** Leaf columns of the user's definitions (not the pivot columns). */
10324
10206
  sourceLeaves() {
@@ -10376,6 +10258,7 @@ var Grid = class {
10376
10258
  afterPivotSwitch() {
10377
10259
  this.page = 0;
10378
10260
  this.store.set({ sort: [], filters: {}, range: null, focus: null });
10261
+ this.rebuildRollup();
10379
10262
  this.rowModel.invalidate();
10380
10263
  this.updateRows();
10381
10264
  this.displayVersion++;
@@ -10484,12 +10367,31 @@ var Grid = class {
10484
10367
  } finally {
10485
10368
  this.keepRange = false;
10486
10369
  }
10370
+ this.clampRanges();
10487
10371
  }
10488
10372
  this.editor.sync();
10489
10373
  if (flashKeys.size && this.options.flashCells !== false) this.flash(flashKeys);
10490
10374
  this.events.emit("transactionApplied", result);
10491
10375
  return result;
10492
10376
  }
10377
+ /** Kept ranges and focus after the row count shrank: cut to the last row, dropped when wholly past it. */
10378
+ clampRanges() {
10379
+ const count = this.rowModel.getRowCount();
10380
+ const { range, extraRanges, focus } = this.store.state;
10381
+ const clamp = (r) => {
10382
+ if (!r || r.start.rowIndex < count && r.end.rowIndex < count) return r;
10383
+ if (Math.min(r.start.rowIndex, r.end.rowIndex) >= count) return null;
10384
+ return { start: { ...r.start, rowIndex: Math.min(r.start.rowIndex, count - 1) }, end: { ...r.end, rowIndex: Math.min(r.end.rowIndex, count - 1) } };
10385
+ };
10386
+ const extras = extraRanges.map(clamp).filter((r) => !!r);
10387
+ if (extras.length !== extraRanges.length || extras.some((r, i) => r !== extraRanges[i])) this.store.set({ extraRanges: extras });
10388
+ if (focus && focus.rowIndex >= count) this.store.set({ focus: count ? { ...focus, rowIndex: count - 1 } : null });
10389
+ const next = clamp(range);
10390
+ if (next !== range) {
10391
+ this.store.set({ range: next });
10392
+ this.events.emit("rangeChange", { range: next && { start: { ...next.start }, end: { ...next.end } } });
10393
+ }
10394
+ }
10493
10395
  /** One transaction against `data` (remove → update → add); returns the new array. */
10494
10396
  /** Whether rows changed in `fields` could filter, sort, group or move differently. */
10495
10397
  viewReads(fields) {
@@ -10644,14 +10546,24 @@ var Grid = class {
10644
10546
  }
10645
10547
  if (want("groupBy") && Array.isArray(state.groupBy)) this.setGroupBy(state.groupBy.filter(known));
10646
10548
  if (want("pivot")) this.setPivot(state.pivot ?? null);
10647
- if (want("sort") && Array.isArray(state.sort)) this.setSort(state.sort.filter((x) => x && known(x.colId)));
10648
- if (want("filters") && state.filters && typeof state.filters === "object") {
10649
- const filters = Object.fromEntries(Object.entries(state.filters).filter(([id]) => known(id)));
10650
- this.store.set({ filters });
10651
- this.events.emit("filterChange", { filters: { ...filters } });
10549
+ const query = {};
10550
+ if (want("sort") && Array.isArray(state.sort)) {
10551
+ const sort = this.checkSort(state.sort.filter((x) => x && known(x.colId)));
10552
+ if (sort) query.sort = sort;
10652
10553
  }
10653
- if (want("advancedFilter")) this.setAdvancedFilter(state.advancedFilter ?? null);
10654
- if (want("quickFilter") && typeof state.quickFilter === "string") this.store.set({ quickFilter: state.quickFilter });
10554
+ if (want("filters") && state.filters && typeof state.filters === "object") {
10555
+ query.filters = Object.fromEntries(Object.entries(state.filters).filter(([id]) => known(id)));
10556
+ }
10557
+ if (want("advancedFilter")) {
10558
+ const f = state.advancedFilter;
10559
+ const next = f && typeof f === "object" && Array.isArray(f.conditions) && f.conditions.length ? f : null;
10560
+ if (next !== this.store.state.advancedFilter && (!next || this.allow("advancedFilter"))) query.advancedFilter = next;
10561
+ }
10562
+ if (want("quickFilter") && typeof state.quickFilter === "string") query.quickFilter = state.quickFilter;
10563
+ this.store.set(query);
10564
+ if (query.sort) this.events.emit("sortChange", { sort: [...query.sort] });
10565
+ if (query.filters) this.events.emit("filterChange", { filters: { ...query.filters } });
10566
+ if ("advancedFilter" in query) this.events.emit("advancedFilterChange", { advancedFilter: query.advancedFilter ?? null });
10655
10567
  if (want("expanded") && state.expanded && typeof state.expanded === "object") {
10656
10568
  const expanded = new Map(Object.entries(state.expanded).filter(([, v]) => typeof v === "boolean"));
10657
10569
  if (this.savedGroup) this.savedGroup.expanded = expanded;
@@ -10677,11 +10589,13 @@ var Grid = class {
10677
10589
  rows.forEach((r, k) => {
10678
10590
  nums[k] = r.kind ? 0 : ++n;
10679
10591
  });
10680
- this.rowNumberCache = { rows, nums };
10592
+ const first = this.page * this.pageSize;
10593
+ const offset = this.options.pagination && !this.rowModel.server ? this.rowModel.dataRowsBefore?.(first) ?? first : 0;
10594
+ this.rowNumberCache = { rows, nums, offset };
10681
10595
  }
10682
10596
  const v = this.rowNumberCache.nums[i];
10683
10597
  if (!v) return null;
10684
- return v + (this.options.pagination && !this.rowModel.server ? this.page * this.pageSize : 0);
10598
+ return v + this.rowNumberCache.offset;
10685
10599
  }
10686
10600
  getPage() {
10687
10601
  return this.options.pagination ? this.page : 0;
@@ -10733,6 +10647,7 @@ var Grid = class {
10733
10647
  /** Server rows: drop the cache and load again. */
10734
10648
  refreshRows() {
10735
10649
  if (this.rowModel instanceof ServerRowModel) {
10650
+ this.saveEditBeforeReload();
10736
10651
  this.rowModel.purge();
10737
10652
  this.renderer.invalidate({ rows: true });
10738
10653
  }
@@ -10742,6 +10657,12 @@ var Grid = class {
10742
10657
  this.renderer.invalidate({ header: true, rows: true });
10743
10658
  this.events.emit("rowsLoaded", total === null ? { start, end } : { start, end, total });
10744
10659
  }
10660
+ /** Server rows are about to be dropped: save an open editor to its row first, else it is left over another row. */
10661
+ saveEditBeforeReload() {
10662
+ if (!this.rowModel.server) return;
10663
+ this.editor.endRow(true);
10664
+ if (this.editor.active && !this.editor.commit(null)) this.editor.cancel();
10665
+ }
10745
10666
  serverFailed(start, end, error) {
10746
10667
  if (this.destroyed) return;
10747
10668
  warn(`datasource.getRows failed for rows ${start}\u2013${end}`);
@@ -10890,7 +10811,7 @@ var Grid = class {
10890
10811
  let m = this.aggCache.map.get(node.group);
10891
10812
  if (!m) this.aggCache.map.set(node.group, m = /* @__PURE__ */ new Map());
10892
10813
  if (!m.has(colId)) {
10893
- const rows = node.group.rows;
10814
+ const rows = this.sourceRowsOf(node.group.rows);
10894
10815
  try {
10895
10816
  m.set(colId, aggregate(def.aggFunc, rows.map((r) => getCellValue(def, r)), rows));
10896
10817
  } catch {
@@ -11083,10 +11004,33 @@ var Grid = class {
11083
11004
  this.renderer.invalidateHeights();
11084
11005
  this.layoutChanged();
11085
11006
  }
11007
+ /** Store and apply the padding; false when the value is not valid. */
11008
+ applyCellPadding(px) {
11009
+ if (px != null && !(typeof px === "number" && Number.isFinite(px) && px >= 0)) {
11010
+ warn("cellPadding must be a number of px, 0 or more");
11011
+ return false;
11012
+ }
11013
+ this.cellPadding = px;
11014
+ if (px == null) this.renderer.root.style.removeProperty("--fg-cell-px");
11015
+ else this.renderer.root.style.setProperty("--fg-cell-px", `${px}px`);
11016
+ return true;
11017
+ }
11018
+ setCellPadding(px) {
11019
+ if (px === this.cellPadding || !this.applyCellPadding(px)) return;
11020
+ this.menu.close();
11021
+ this.layoutChanged();
11022
+ }
11023
+ /** The padding in use: the set value, else the theme / density CSS variable. */
11024
+ getCellPadding() {
11025
+ if (this.cellPadding != null) return this.cellPadding;
11026
+ const v = parseFloat(getComputedStyle(this.renderer.root).getPropertyValue("--fg-cell-px"));
11027
+ return Number.isFinite(v) ? v : 12;
11028
+ }
11086
11029
  /** Copy the grid font variables onto an element rendered outside the root (popups, drag ghost). */
11087
11030
  applyFontTo(target) {
11088
11031
  if (this.font.size) target.style.setProperty("--fg-font-size", `${this.font.size}px`);
11089
11032
  if (this.font.family) target.style.setProperty("--fg-font", this.font.family);
11033
+ if (this.cellPadding != null) target.style.setProperty("--fg-cell-px", `${this.cellPadding}px`);
11090
11034
  }
11091
11035
  /** Current summary configuration and values, or null when there are no summary rows. */
11092
11036
  summary() {
@@ -11115,7 +11059,7 @@ var Grid = class {
11115
11059
  }
11116
11060
  const scope = opts.scope ?? "filtered";
11117
11061
  const rows = scope === "all" ? this.rowModel.allNodes().map((n) => n.data) : scope === "selected" ? this.selectedRows() : this.rowModel.dataRows().map((n) => n.data);
11118
- this.summaryValues = computeSummary(opts.rows, rows, (id) => this.columns.getNode(id)?.def);
11062
+ this.summaryValues = computeSummary(opts.rows, this.sourceRowsOf(rows), (id) => this.columns.getNode(id)?.def);
11119
11063
  }
11120
11064
  // ── cell ranges ─────────────────────────────────────────────
11121
11065
  dataColumns() {
@@ -11208,12 +11152,11 @@ var Grid = class {
11208
11152
  out = [];
11209
11153
  for (let i = b.r1; i <= b.r2; i++) {
11210
11154
  const node = this.rowModel.getRow(i);
11211
- if (!node.kind) out.push(node.data);
11155
+ if (node && !node.kind) out.push(node.data);
11212
11156
  }
11213
11157
  cols = displayed.slice(b.c1, b.c2 + 1).filter((c) => !isSystemCol(c.id));
11214
11158
  } else if (checkedRows) {
11215
- const sel = this.store.state.selected;
11216
- out = this.rowModel.dataRows().filter((n) => sel.has(n.id)).map((n) => n.data);
11159
+ out = this.shownSelectedRows();
11217
11160
  cols = this.dataColumns();
11218
11161
  } else {
11219
11162
  const f = this.store.state.focus;
@@ -11268,8 +11211,17 @@ var Grid = class {
11268
11211
  if (text2) this.clearCells(true);
11269
11212
  return text2;
11270
11213
  }
11214
+ /** Text a copy puts on the clipboard. Server rows: only the loaded rows, with a warning about the rest. */
11215
+ copyText(headers, rows = false) {
11216
+ const m = this.rowModel;
11217
+ if (!(m instanceof ServerRowModel)) return this.selectionTSV(headers, rows);
11218
+ const text2 = m.withoutLoading(() => this.selectionTSV(headers, rows));
11219
+ const missing = rows ? 0 : m.withoutLoading(() => this.unloadedInRanges());
11220
+ if (missing) warn(`copy: ${missing} row${missing > 1 ? "s" : ""} of the range ${missing > 1 ? "are" : "is"} not loaded and ${missing > 1 ? "were" : "was"} not copied`);
11221
+ return text2;
11222
+ }
11271
11223
  copyToClipboard(headers, rows = false) {
11272
- const text2 = this.selectionTSV(headers, rows);
11224
+ const text2 = this.copyText(headers, rows);
11273
11225
  if (text2 && typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
11274
11226
  try {
11275
11227
  navigator.clipboard.writeText(text2)?.catch?.(() => void 0);
@@ -11278,6 +11230,14 @@ var Grid = class {
11278
11230
  }
11279
11231
  return text2;
11280
11232
  }
11233
+ /** Server rows: how many rows the ranges cover that are not loaded (placeholders). */
11234
+ unloadedInRanges() {
11235
+ const seen2 = /* @__PURE__ */ new Set();
11236
+ for (const b of this.allRangeBounds()) for (let i = b.r1; i <= b.r2; i++) {
11237
+ if (this.rowModel.getRow(i)?.kind === "loading") seen2.add(i);
11238
+ }
11239
+ return seen2.size;
11240
+ }
11281
11241
  pasteText(text2) {
11282
11242
  const matrix = parseTSV(text2);
11283
11243
  const result = { changed: 0, skipped: 0 };
@@ -11300,19 +11260,21 @@ var Grid = class {
11300
11260
  const single = matrix.length === 1 && matrix[0].length === 1;
11301
11261
  const rows = single && b ? b.r2 - b.r1 + 1 : matrix.length;
11302
11262
  const width = single && b ? b.c2 - b.c1 + 1 : Math.max(...matrix.map((r) => r.length));
11303
- for (let dr = 0; dr < rows; dr++) {
11304
- const ri = r0 + dr;
11305
- if (ri >= count) break;
11263
+ for (let ri = r0, dr = 0; ri < count && dr < rows; ri++) {
11306
11264
  const node = this.rowModel.getRow(ri);
11307
- if (node.kind) continue;
11265
+ if (node.kind) {
11266
+ if (single && b) dr++;
11267
+ continue;
11268
+ }
11269
+ const line2 = dr++;
11308
11270
  for (let dc = 0; dc < width; dc++) {
11309
11271
  const col = displayed[c0 + dc];
11310
11272
  if (!col) break;
11311
11273
  if (isSystemCol(col.id)) continue;
11312
- const raw = single && b ? matrix[0][0] : matrix[dr]?.[dc];
11274
+ const raw = single && b ? matrix[0][0] : matrix[line2]?.[dc];
11313
11275
  if (raw === void 0) continue;
11314
11276
  const def = col.def;
11315
- if (!def.field || !cellEditable(def, node.data, ri)) {
11277
+ if (!def.field || !this.cellEditable(def, node.data, ri)) {
11316
11278
  result.skipped++;
11317
11279
  continue;
11318
11280
  }
@@ -11380,7 +11342,7 @@ var Grid = class {
11380
11342
  const id = this.rowModel.idOf(row);
11381
11343
  const rowIndex = this.rowModel.indexOfId(id);
11382
11344
  for (const c of cols) {
11383
- if (!cellEditable(c.def, row, rowIndex < 0 ? i : rowIndex)) continue;
11345
+ if (!this.cellEditable(c.def, row, rowIndex < 0 ? i : rowIndex)) continue;
11384
11346
  const message = this.cellError(c.def, row, row[c.def.field]);
11385
11347
  if (message !== null) out.push({ row, rowIndex, colId: c.id, message });
11386
11348
  }
@@ -11645,26 +11607,36 @@ var Grid = class {
11645
11607
  const col = displayed[c];
11646
11608
  const node = this.rowModel.getRow(r);
11647
11609
  const def = col?.def;
11648
- if (!node || node.kind || !def?.field || isSystemCol(col.id) || !cellEditable(def, node.data, r)) return;
11610
+ if (!node || node.kind || !def?.field || isSystemCol(col.id) || !this.cellEditable(def, node.data, r)) return;
11649
11611
  if (this.cellError(def, node.data, value2) !== null) return;
11650
11612
  this.writeCell(changes, node.data, def.field, col.id, r, value2);
11651
11613
  };
11652
11614
  const value = (r, c) => getCellValue(displayed[c].def, this.rowModel.getRow(r).data);
11615
+ const dataRow = (r) => {
11616
+ const node = this.rowModel.getRow(r);
11617
+ return !!node && !node.kind;
11618
+ };
11619
+ const keep = (from, to, ok) => {
11620
+ const out = [];
11621
+ for (let i = from; i <= to; i++) if (ok(i)) out.push(i);
11622
+ return out;
11623
+ };
11624
+ const dataCol = (c) => !!displayed[c] && !isSystemCol(displayed[c].id);
11653
11625
  if (t.r1 < src.r1 || t.r2 > src.r2) {
11626
+ const down = keep(src.r2 + 1, t.r2, dataRow), up = keep(t.r1, src.r1 - 1, dataRow).reverse();
11627
+ const rows = keep(src.r1, src.r2, dataRow);
11654
11628
  for (let c = src.c1; c <= src.c2; c++) {
11655
- const down = [], up = [];
11656
- for (let r = src.r1; r <= src.r2; r++) down.push(value(r, c));
11657
- for (let r = src.r2; r >= src.r1; r--) up.push(value(r, c));
11658
- fillSeries(down, t.r2 - src.r2).forEach((v, k) => write(src.r2 + 1 + k, c, v));
11659
- fillSeries(up, src.r1 - t.r1).forEach((v, k) => write(src.r1 - 1 - k, c, v));
11629
+ const seed = rows.map((r) => value(r, c));
11630
+ fillSeries(seed, down.length).forEach((v, k) => write(down[k], c, v));
11631
+ fillSeries([...seed].reverse(), up.length).forEach((v, k) => write(up[k], c, v));
11660
11632
  }
11661
11633
  } else if (t.c1 < src.c1 || t.c2 > src.c2) {
11662
- for (let r = src.r1; r <= src.r2; r++) {
11663
- const right = [], left = [];
11664
- for (let c = src.c1; c <= src.c2; c++) right.push(value(r, c));
11665
- for (let c = src.c2; c >= src.c1; c--) left.push(value(r, c));
11666
- fillSeries(right, t.c2 - src.c2).forEach((v, k) => write(r, src.c2 + 1 + k, v));
11667
- fillSeries(left, src.c1 - t.c1).forEach((v, k) => write(r, src.c1 - 1 - k, v));
11634
+ const right = keep(src.c2 + 1, t.c2, dataCol), left = keep(t.c1, src.c1 - 1, dataCol).reverse();
11635
+ const cols = keep(src.c1, src.c2, dataCol);
11636
+ for (const r of keep(src.r1, src.r2, dataRow)) {
11637
+ const seed = cols.map((c) => value(r, c));
11638
+ fillSeries(seed, right.length).forEach((v, k) => write(r, right[k], v));
11639
+ fillSeries([...seed].reverse(), left.length).forEach((v, k) => write(r, left[k], v));
11668
11640
  }
11669
11641
  } else return;
11670
11642
  this.endCellTx(changes);
@@ -11733,7 +11705,7 @@ var Grid = class {
11733
11705
  const node = this.rowModel.getRow(r);
11734
11706
  for (let c = box.c1; c <= box.c2; c++) {
11735
11707
  const def = displayed[c]?.def;
11736
- if (node && !node.kind && def?.field && cellEditable(def, node.data, r) && (!cut || this.copyAllowed(def, node.data))) this.writeCell(changes, node.data, def.field, displayed[c].id, r, null);
11708
+ if (node && !node.kind && def?.field && this.cellEditable(def, node.data, r) && (!cut || this.copyAllowed(def, node.data))) this.writeCell(changes, node.data, def.field, displayed[c].id, r, null);
11737
11709
  }
11738
11710
  }
11739
11711
  this.endCellTx(changes);
@@ -11764,7 +11736,7 @@ var Grid = class {
11764
11736
  removeRows(target, record = true) {
11765
11737
  if (this.clientOnly("removeRows")) return [];
11766
11738
  const data = this.store.state.data;
11767
- const match = typeof target === "function" ? target : /* @__PURE__ */ ((set) => (r) => set.has(r))(new Set(target));
11739
+ const match = typeof target === "function" ? target : /* @__PURE__ */ ((set) => (r) => set.has(r))(new Set(Array.isArray(target) ? target.flatMap((r) => rollupSource(r) ?? [r]) : []));
11768
11740
  const removed = [];
11769
11741
  data.forEach((row, index) => {
11770
11742
  let hit = false;
@@ -11887,10 +11859,13 @@ var Grid = class {
11887
11859
  const cols = (ids ? ids.map((id) => this.columns.getColumn(id)) : this.columns.displayed()).filter((c) => !!c && isResizable(c.def));
11888
11860
  const { first, last } = this.renderer.renderedRange();
11889
11861
  const code = this.store.state.locale.code;
11890
- const HEADER_EXTRA = 24 + 22 + 6 + 14;
11891
- const CELL_EXTRA = 24;
11862
+ const pad4 = this.getCellPadding() * 2;
11863
+ const HEADER_EXTRA = pad4 + 6 + 14;
11864
+ const FILTER_BTN = 6 + 22;
11865
+ const filters = this.store.state.filters;
11866
+ const CELL_EXTRA = pad4;
11892
11867
  for (const col of cols) {
11893
- let w = this.renderer.measureText(col.def.header ?? col.def.field ?? "", true) + HEADER_EXTRA;
11868
+ let w = this.renderer.measureText(col.def.header ?? col.def.field ?? "", true) + HEADER_EXTRA + (filters[col.id] ? FILTER_BTN : 0);
11894
11869
  const fmt = getFormatter(col.def, code);
11895
11870
  for (let i = first; i <= last; i++) {
11896
11871
  const node = this.rowModel.getRow(i);
@@ -11906,6 +11881,13 @@ var Grid = class {
11906
11881
  this.columnsChanged();
11907
11882
  }
11908
11883
  setSort(sort) {
11884
+ const valid = this.checkSort(sort);
11885
+ if (!valid) return;
11886
+ this.store.set({ sort: valid });
11887
+ this.events.emit("sortChange", { sort: [...valid] });
11888
+ }
11889
+ /** The known, well-formed sort items, or null when a beforeSort handler refused them. */
11890
+ checkSort(sort) {
11909
11891
  const valid = sort.filter((s) => {
11910
11892
  const node = this.columns.getNode(s.colId);
11911
11893
  if (!node || node.children) {
@@ -11914,9 +11896,7 @@ var Grid = class {
11914
11896
  }
11915
11897
  return s.dir === "asc" || s.dir === "desc";
11916
11898
  });
11917
- if (this.veto("beforeSort", { sort: valid.map((s) => ({ ...s })) })) return;
11918
- this.store.set({ sort: valid });
11919
- this.events.emit("sortChange", { sort: [...valid] });
11899
+ return this.veto("beforeSort", { sort: valid.map((s) => ({ ...s })) }) ? null : valid;
11920
11900
  }
11921
11901
  toggleSort(colId, additive) {
11922
11902
  const cur = this.store.state.sort;
@@ -12082,9 +12062,19 @@ var Grid = class {
12082
12062
  }
12083
12063
  this.setSelected(next);
12084
12064
  }
12065
+ /** The checked rows' data; a merged rollup row stands for its source rows, so these are always rows of the data. */
12085
12066
  selectedRows() {
12086
12067
  const sel = this.store.state.selected;
12087
- return sel.size ? this.rowModel.allNodes().filter((n) => sel.has(n.id)).map((n) => n.data) : [];
12068
+ return sel.size ? this.sourceRowsOf(this.rowModel.allNodes().filter((n) => sel.has(n.id)).map((n) => n.data)) : [];
12069
+ }
12070
+ /** Filtered data rows in display order (group buckets, parents before children), collapsed ones included. */
12071
+ orderedDataRows() {
12072
+ return this.rowModel.orderedDataRows?.() ?? this.rowModel.dataRows();
12073
+ }
12074
+ /** Checked rows that pass the filters, in display order: what copy, export and delete of the checked rows act on. */
12075
+ shownSelectedRows() {
12076
+ const sel = this.store.state.selected;
12077
+ return sel.size ? this.orderedDataRows().filter((n) => sel.has(n.id)).map((n) => n.data) : [];
12088
12078
  }
12089
12079
  withSelect(defs) {
12090
12080
  const sys = [];
@@ -12157,12 +12147,25 @@ var Grid = class {
12157
12147
  });
12158
12148
  this.columnsChanged();
12159
12149
  }
12150
+ /** Close every popup of this grid (menus and dialogs); focus that was in one goes back to the grid. */
12151
+ closePopups() {
12152
+ const r = this.renderer.root.getRootNode();
12153
+ const inPopup = !!(r.activeElement ?? document.activeElement)?.closest(".fg-menu");
12154
+ this.menu.close();
12155
+ this.chooser.close();
12156
+ this.contextMenu.close();
12157
+ this.filterBuilder.close();
12158
+ this.formatDialog.close();
12159
+ if (inPopup) this.renderer.gridEl.focus({ preventScroll: true });
12160
+ }
12160
12161
  onChange(keys) {
12161
12162
  const has = (...k) => k.some((x) => keys.includes(x));
12163
+ if (has("sort", "filters", "quickFilter", "advancedFilter")) this.saveEditBeforeReload();
12162
12164
  if (this.pivotCfg && has("data", "locale")) this.rebuildPivot(false);
12165
+ if (this.rollupOn && has("data")) this.rebuildRollup();
12166
+ if (has("locale", "theme")) this.closePopups();
12163
12167
  if (has("locale")) {
12164
- this.menu.close();
12165
- this.chooser.close();
12168
+ this.finder.relabel();
12166
12169
  this.sideBar?.refresh();
12167
12170
  this.rowModel.invalidate();
12168
12171
  }
@@ -12253,12 +12256,6 @@ var Grid = class {
12253
12256
  if (!this.allow("conditional")) return;
12254
12257
  this.formatDialog.open(colId);
12255
12258
  }),
12256
- // closed: as if `assistant` were not set
12257
- askAssistant: guard((prompt) => askAssistant(this, prompt, this.allow("assistant") ? this.options.assistant : void 0)),
12258
- openAssistant: guard(() => {
12259
- if (!this.allow("assistant")) return;
12260
- this.assistantPanel.open();
12261
- }),
12262
12259
  getState: guard(() => this.getState()),
12263
12260
  setState: guard((s, o) => this.setState(s, o)),
12264
12261
  getScrollPosition: guard(() => this.renderer.getScroll()),
@@ -12267,6 +12264,11 @@ var Grid = class {
12267
12264
  setTheme: guard((t) => this.store.set({ theme: t })),
12268
12265
  setLocale: guard((l) => this.store.set({ locale: resolveLocale(l) })),
12269
12266
  setFont: guard((f) => this.setFont(f)),
12267
+ setCellPadding: guard((px) => this.setCellPadding(px)),
12268
+ getCellPadding: guard(() => this.getCellPadding()),
12269
+ setRollup: guard((cfg) => this.setRollup(cfg)),
12270
+ getRollup: guard(() => this.getRollup()),
12271
+ setColumnAggFunc: guard((id, agg) => this.setColumnAggFunc(id, agg)),
12270
12272
  setColumnVisible: guard((id, v) => this.setColumnsVisible([id], v)),
12271
12273
  autoSizeColumns: guard((ids) => this.autoSize(ids)),
12272
12274
  resetColumnState: guard(() => this.resetColumns()),