@freegridjs/core 1.9.4 → 1.10.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_;
@@ -596,19 +597,56 @@ var ColumnModel = class {
596
597
  relayout() {
597
598
  const visible = this.leafNodes().filter((n) => !n.hide && this.shownByGroups(n));
598
599
  const fixed = visible.reduce((s, n) => s + (n.flex ? 0 : n.width), 0);
599
- const totalFlex = visible.reduce((s, n) => s + (n.flex ?? 0), 0);
600
- const free = Math.max(0, this.containerWidth - fixed);
601
- const widthOf = (n) => n.flex ? clampWidth(n.def, Math.floor(free * n.flex / totalFlex)) : n.width;
600
+ const flexWidth = /* @__PURE__ */ new Map();
601
+ let pending = visible.filter((n) => n.flex);
602
+ let free = Math.max(0, this.containerWidth - fixed);
603
+ while (pending.length) {
604
+ const totalFlex = pending.reduce((s, n) => s + n.flex, 0);
605
+ const share = (n) => Math.floor(free * n.flex / totalFlex);
606
+ const excess = pending.reduce((s, n) => s + clampWidth(n.def, share(n)) - share(n), 0);
607
+ const clamped = pending.filter((n) => {
608
+ const d = clampWidth(n.def, share(n)) - share(n);
609
+ return excess > 0 ? d > 0 : excess < 0 ? d < 0 : false;
610
+ });
611
+ if (!clamped.length) {
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
+ }
625
+ break;
626
+ }
627
+ let used = 0;
628
+ for (const n of clamped) {
629
+ const w = clampWidth(n.def, share(n));
630
+ flexWidth.set(n, w);
631
+ used += w;
632
+ }
633
+ free = Math.max(0, free - used);
634
+ pending = pending.filter((n) => !clamped.includes(n));
635
+ }
636
+ const widthOf = (n) => flexWidth.get(n) ?? n.width;
602
637
  const next = { left: [], center: [], right: [] };
603
638
  this.leafById = /* @__PURE__ */ new Map();
604
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 };
605
643
  for (const r of REGIONS) {
606
- let x = 0;
644
+ const to = unpin ? "center" : r;
607
645
  for (const n of visible) {
608
646
  if ((n.pinned ?? "center") !== r) continue;
609
- const col = { id: n.id, def: n.def, node: n, width: widthOf(n), region: r, left: x, index: index++ };
610
- x += col.width;
611
- 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);
612
650
  this.leafById.set(n.id, col);
613
651
  }
614
652
  }
@@ -1339,6 +1377,7 @@ var REGIONS2 = ["left", "center", "right"];
1339
1377
  var NUM_OPS = [[">=", "gte"], ["<=", "lte"], ["<>", "ne"], ["!=", "ne"], [">", "gt"], ["<", "lt"], ["=", "eq"]];
1340
1378
  var DATE_OPS = [[">", "after"], ["<", "before"], ["=", "equals"]];
1341
1379
  var RANGE = /^(.+?)\s*(?:~|\.\.)\s*(.+)$/;
1380
+ var MULTI = "~many";
1342
1381
  var toNumber = (s) => {
1343
1382
  const n = Number(s.replace(/[,\s₩$€¥]/g, ""));
1344
1383
  return s.trim() !== "" && Number.isFinite(n) ? n : null;
@@ -1438,8 +1477,21 @@ var FilterRow = class {
1438
1477
  box.closest(".fg-filter-cell").classList.toggle("fg-filter-active", !!model);
1439
1478
  if (box === box.ownerDocument.activeElement || box.getRootNode().activeElement === box) continue;
1440
1479
  if (box instanceof HTMLSelectElement) {
1441
- const v = model?.type === "set" && model.values.length === 1 ? JSON.stringify(model.values[0]) : "";
1442
- if ([...box.options].some((o) => o.value === v)) box.value = v;
1480
+ const many = model?.type === "set" && model.values.length > 1 ? model.values.length : 0;
1481
+ let multi = box.querySelector(`option[value="${MULTI}"]`);
1482
+ if (many) {
1483
+ if (!multi) {
1484
+ multi = el("option", "", { value: MULTI });
1485
+ multi.disabled = true;
1486
+ box.options[0].after(multi);
1487
+ }
1488
+ multi.textContent = this.grid.store.state.locale.setSelected.replace("{n}", String(many));
1489
+ box.value = MULTI;
1490
+ } else {
1491
+ multi?.remove();
1492
+ const v = model?.type === "set" && model.values.length === 1 ? JSON.stringify(model.values[0]) : "";
1493
+ if ([...box.options].some((o) => o.value === v)) box.value = v;
1494
+ }
1443
1495
  } else box.value = filterText(model);
1444
1496
  box.classList.remove("fg-filter-bad");
1445
1497
  }
@@ -1453,6 +1505,10 @@ var FilterRow = class {
1453
1505
  if (!def) continue;
1454
1506
  const v = select.value;
1455
1507
  this.fillOptions(select, def);
1508
+ if (v === MULTI) {
1509
+ this.rebuilt = true;
1510
+ continue;
1511
+ }
1456
1512
  if (v && ![...select.options].some((o) => o.value === v)) {
1457
1513
  const o = el("option", "", { value: v });
1458
1514
  o.textContent = String(JSON.parse(v));
@@ -1473,7 +1529,7 @@ var FilterRow = class {
1473
1529
  select.replaceChildren(all2, ...opts);
1474
1530
  }
1475
1531
  cell(c, h) {
1476
- const cell = el("div", "fg-filter-cell");
1532
+ const cell = el("div", "fg-filter-cell", { role: "gridcell" });
1477
1533
  cell.style.left = `${c.left}px`;
1478
1534
  cell.style.width = `${c.width}px`;
1479
1535
  cell.style.height = `${h}px`;
@@ -2033,6 +2089,7 @@ var Renderer = class {
2033
2089
  };
2034
2090
  this.overlayKey = "";
2035
2091
  this.heightFn = null;
2092
+ this.freeBadge = null;
2036
2093
  this.onCardClick = (e) => {
2037
2094
  const target = e.target;
2038
2095
  const card = target.closest(".fg-card");
@@ -2240,6 +2297,7 @@ var Renderer = class {
2240
2297
  if (ref.nextElementSibling !== b) ref.after(b);
2241
2298
  ref = b;
2242
2299
  }
2300
+ this.placeFreeBadge();
2243
2301
  }
2244
2302
  /** "No rows" / "Loading" over the body, or the noRowsOverlay / loadingOverlay given in the options. */
2245
2303
  paintOverlay(count) {
@@ -2330,7 +2388,7 @@ var Renderer = class {
2330
2388
  const { grid } = this;
2331
2389
  const state = grid.store.state;
2332
2390
  const total = grid.rowModel.server ? count : grid.viewRows().length;
2333
- const key = `${this.version}.${grid.valuesVersion}|${count}|${total}|${state.selected.size}|${JSON.stringify(state.range)}|${state.locale.code}`;
2391
+ const key = `${this.version}.${grid.valuesVersion}|${count}|${total}|${state.selected.size}|${JSON.stringify(state.range)}|${JSON.stringify(state.extraRanges)}|${state.locale.code}`;
2334
2392
  if (this.statusEl && key === this.statusKey) return;
2335
2393
  this.statusKey = key;
2336
2394
  this.statusEl ?? (this.statusEl = el("div", "fg-statusbar", { role: "status", "aria-live": "polite" }));
@@ -3202,12 +3260,21 @@ var Renderer = class {
3202
3260
  /** Small "free version" mark in the bottom-right corner (unlicensed company domains flagged on freegrid.app). */
3203
3261
  showFreeBadge() {
3204
3262
  if (this.root.querySelector(".fg-free-badge")) return;
3205
- const a = el("a", "fg-free-badge", { href: "https://freegrid.app/pricing/", target: "_blank", rel: "noopener" });
3263
+ const a = el("div", "fg-free-badge", { "aria-hidden": "true" });
3206
3264
  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>';
3207
3265
  const text2 = el("span");
3208
3266
  text2.textContent = `FreeGrid \xB7 ${this.grid.store.state.locale.freeBadge}`;
3209
3267
  a.append(text2);
3210
3268
  this.root.append(a);
3269
+ this.freeBadge = a;
3270
+ this.placeFreeBadge();
3271
+ }
3272
+ /** The free badge sits above the status bar and pager, not on them. */
3273
+ placeFreeBadge() {
3274
+ if (!this.freeBadge) return;
3275
+ const below = (this.statusEl?.isConnected ? this.statusEl.offsetHeight : 0) + (this.pagerEl?.isConnected ? this.pagerEl.offsetHeight : 0);
3276
+ const bottom = below ? `${below + 22}px` : "";
3277
+ if (this.freeBadge.style.bottom !== bottom) this.freeBadge.style.bottom = bottom;
3211
3278
  }
3212
3279
  /** Card view applies when enabled and the grid is narrower than the breakpoint. */
3213
3280
  useCards() {
@@ -3527,6 +3594,8 @@ var en = {
3527
3594
  apply: "Apply",
3528
3595
  reset: "Reset",
3529
3596
  selectAll: "(Select all)",
3597
+ setSelected: "({n} selected)",
3598
+ noAggregate: "No aggregate",
3530
3599
  search: "Search\u2026",
3531
3600
  blanks: "(Blanks)",
3532
3601
  value: "Value",
@@ -3757,7 +3826,8 @@ var DEFAULT_KEYMAP = {
3757
3826
  "Ctrl+Z": "undo",
3758
3827
  "Ctrl+Y": "redo",
3759
3828
  "Ctrl+Shift+Z": "redo",
3760
- "Ctrl+F": "find"
3829
+ "Ctrl+F": "find",
3830
+ "Alt+ArrowDown": "columnMenu"
3761
3831
  };
3762
3832
  function comboOf(e) {
3763
3833
  const parts = [];
@@ -3906,6 +3976,8 @@ function attachBody(grid) {
3906
3976
  case "paste":
3907
3977
  navigator.clipboard?.readText?.().then((t) => grid.pasteText(t), () => void 0);
3908
3978
  return true;
3979
+ case "columnMenu":
3980
+ return grid.menu.openAtFocus();
3909
3981
  }
3910
3982
  if (!f) return false;
3911
3983
  if (!grid.editor.canEdit(f.rowIndex, f.colId)) return false;
@@ -4134,14 +4206,14 @@ function attachBody(grid) {
4134
4206
  const nested = (e) => e.target?.closest?.(".fg-root") !== root;
4135
4207
  const onCopy = (e) => {
4136
4208
  if (fromField(e) || nested(e)) return;
4137
- const text2 = grid.selectionTSV(false);
4209
+ const text2 = grid.copyText(false);
4138
4210
  if (!text2 || !e.clipboardData) return;
4139
4211
  e.clipboardData.setData("text/plain", text2);
4140
4212
  e.preventDefault();
4141
4213
  };
4142
4214
  const onCut = (e) => {
4143
4215
  if (fromField(e) || nested(e)) return;
4144
- const text2 = grid.selectionTSV(false);
4216
+ const text2 = grid.copyText(false);
4145
4217
  if (!text2 || !e.clipboardData) return;
4146
4218
  e.clipboardData.setData("text/plain", text2);
4147
4219
  e.preventDefault();
@@ -4508,7 +4580,9 @@ var ColumnChooserDialog = class {
4508
4580
  if (!eventInside(e, popup)) this.close();
4509
4581
  };
4510
4582
  const onKey = (e) => {
4511
- if (e.key === "Escape") this.close();
4583
+ if (e.key !== "Escape") return;
4584
+ this.close();
4585
+ this.grid.renderer.gridEl.focus();
4512
4586
  };
4513
4587
  document.addEventListener("pointerdown", onDown, true);
4514
4588
  document.addEventListener("keydown", onKey, true);
@@ -4647,12 +4721,13 @@ var ContextMenu = class {
4647
4721
  const node = grid.columns.getNode(colId);
4648
4722
  const def = node?.def;
4649
4723
  const dataCol = !!def && !isSystemCol(colId);
4724
+ const checked = grid.store.state.selected.size ? grid.shownSelectedRows() : [];
4650
4725
  const items = [
4651
4726
  { label: loc.copy, shortcut: shortcut("C"), action: () => grid.copyToClipboard(false) },
4652
4727
  { label: loc.copyWithHeaders, action: () => grid.copyToClipboard(true) },
4653
4728
  // after the copy items, so defaultItems indexes from earlier versions still point at the same entries
4654
4729
  { label: loc.cut, shortcut: shortcut("X"), action: () => grid.cutToClipboard() },
4655
- ...grid.store.state.selected.size ? [{ label: loc.copyRows, action: () => grid.copyToClipboard(false, true) }] : [],
4730
+ ...checked.length ? [{ label: loc.copyRows, action: () => grid.copyToClipboard(false, true) }] : [],
4656
4731
  { label: loc.paste, shortcut: shortcut("V"), action: () => this.pasteFromClipboard() }
4657
4732
  ];
4658
4733
  if (grid.history.canUndo() || grid.history.canRedo()) {
@@ -4662,8 +4737,8 @@ var ContextMenu = class {
4662
4737
  { label: loc.redo, shortcut: shortcut("Y"), action: () => grid.redo(), disabled: !grid.history.canRedo() }
4663
4738
  );
4664
4739
  }
4665
- if (grid.options.rowSelection && grid.store.state.selected.size) {
4666
- items.push({ label: loc.deleteRows, action: () => grid.api.removeSelectedRows() });
4740
+ if (grid.options.rowSelection && checked.length) {
4741
+ items.push({ label: loc.deleteRows, action: () => grid.removeRows(checked) });
4667
4742
  }
4668
4743
  if (dataCol) {
4669
4744
  items.push("separator", { label: loc.autosizeThis, action: () => grid.autoSize([colId]) });
@@ -4758,7 +4833,9 @@ var ContextMenu = class {
4758
4833
  popupParent(this.grid.renderer.root).append(popup);
4759
4834
  const w = popup.offsetWidth || 220, h = popup.offsetHeight || 300;
4760
4835
  popup.style.left = `${Math.max(4, Math.min(x, window.innerWidth - w - 4))}px`;
4761
- popup.style.top = `${Math.max(4, Math.min(y, window.innerHeight - h - 4))}px`;
4836
+ const top = Math.max(4, Math.min(y, window.innerHeight - h - 4));
4837
+ popup.style.top = `${top}px`;
4838
+ popup.style.maxHeight = `${Math.max(120, window.innerHeight - top - 8)}px`;
4762
4839
  this.popup = popup;
4763
4840
  const onDown = (e) => {
4764
4841
  if (!eventInside(e, popup)) this.close();
@@ -5048,10 +5125,28 @@ var Finder = class {
5048
5125
  else this.next();
5049
5126
  }
5050
5127
  });
5051
- this.grid.renderer.root.append(bar);
5128
+ const { root, headerEl, main } = this.grid.renderer;
5129
+ root.append(bar);
5130
+ const box = root.getBoundingClientRect(), head = headerEl.getBoundingClientRect(), area = main.getBoundingClientRect();
5131
+ if (head.height) bar.style.top = `${Math.round(head.bottom - box.top) + 6}px`;
5132
+ if (area.width) bar.style.right = `${Math.max(0, Math.round(box.right - area.right)) + 18}px`;
5052
5133
  this.bar = bar;
5053
5134
  input.focus();
5054
5135
  }
5136
+ /** setLocale: the open bar's placeholder, button labels and "no matches" in the new language. */
5137
+ relabel() {
5138
+ if (!this.bar) return;
5139
+ const loc = this.grid.store.state.locale;
5140
+ const input = this.bar.querySelector("input");
5141
+ input.placeholder = loc.findPlaceholder;
5142
+ input.setAttribute("aria-label", loc.findPlaceholder);
5143
+ const labels = [loc.findPrevious, loc.findNext, loc.close];
5144
+ this.bar.querySelectorAll(".fg-findbar-btn").forEach((b, i) => {
5145
+ b.setAttribute("aria-label", labels[i]);
5146
+ b.title = labels[i];
5147
+ });
5148
+ this.updateCount();
5149
+ }
5055
5150
  closeBar() {
5056
5151
  this.bar?.remove();
5057
5152
  this.bar = null;
@@ -5304,7 +5399,7 @@ var CellEditor = class {
5304
5399
  const col = this.grid.columns.getColumn(colId);
5305
5400
  if (!col?.def.field || rowIndex < 0 || rowIndex >= this.grid.rowModel.getRowCount()) return false;
5306
5401
  const node = this.grid.rowModel.getRow(rowIndex);
5307
- return !node.kind && cellEditable(col.def, node.data, rowIndex);
5402
+ return !node.kind && this.grid.cellEditable(col.def, node.data, rowIndex);
5308
5403
  }
5309
5404
  start(rowIndex, colId, initialText) {
5310
5405
  if (this.cur && !this.commit(null)) return false;
@@ -5530,6 +5625,7 @@ var CellEditor = class {
5530
5625
  i = cols.length - 1;
5531
5626
  if (!this.session) r--;
5532
5627
  }
5628
+ while (r !== rowIndex && r >= 0 && r < count && grid.rowModel.getRow(r)?.kind) r += step;
5533
5629
  if (r < 0 || r >= count) return;
5534
5630
  this.start(r, cols[i].id);
5535
5631
  }
@@ -6639,9 +6735,9 @@ function exportColumns(grid, ids) {
6639
6735
  return grid.columns.displayed().filter((c) => !isSystemCol(c.id)).map((c) => ({ id: c.id, def: c.def, node: c.node, width: c.width }));
6640
6736
  }
6641
6737
  function exportRows(grid, mode) {
6642
- if (mode === "selected") return grid.selectedRows();
6738
+ if (mode === "selected") return grid.shownSelectedRows();
6643
6739
  if (mode === "all") return grid.rowModel.server ? grid.rowModel.dataRows().map((n) => n.data) : grid.viewRows();
6644
- return grid.rowModel.dataRows().map((n) => n.data);
6740
+ return grid.orderedDataRows().map((n) => n.data);
6645
6741
  }
6646
6742
  var header = (def, id) => def.header ?? def.field ?? id;
6647
6743
  var ALIGNS = ["left", "center", "right"];
@@ -7732,6 +7828,35 @@ function buildPivot(source, config, defOf, labels, locale) {
7732
7828
  });
7733
7829
  return { columns: [...rowColumns, ...valueColumns], rows, groupBy: cfg.rows.slice(0, -1) };
7734
7830
  }
7831
+ var ROLLUP = /* @__PURE__ */ new WeakMap();
7832
+ function rollupSource(row) {
7833
+ return ROLLUP.get(row);
7834
+ }
7835
+ function buildRollup(source, defs) {
7836
+ const keys = defs.filter((d) => !d.aggFunc);
7837
+ const values = defs.filter((d) => d.aggFunc && d.field && !d.valueGetter);
7838
+ const buckets = /* @__PURE__ */ new Map();
7839
+ for (const r of source) {
7840
+ const k = keys.map((d) => setKey(getCellValue(d, r))).join("");
7841
+ let list = buckets.get(k);
7842
+ if (!list) buckets.set(k, list = []);
7843
+ list.push(r);
7844
+ }
7845
+ const rows = [];
7846
+ for (const src of buckets.values()) {
7847
+ const row = { ...src[0] };
7848
+ for (const d of values) {
7849
+ try {
7850
+ row[d.field] = aggregate(d.aggFunc, src.map((r) => getCellValue(d, r)), src);
7851
+ } catch {
7852
+ row[d.field] = null;
7853
+ }
7854
+ }
7855
+ ROLLUP.set(row, src);
7856
+ rows.push(row);
7857
+ }
7858
+ return rows;
7859
+ }
7735
7860
 
7736
7861
  // src/features.ts
7737
7862
  var PAID_FEATURES = {
@@ -7757,7 +7882,7 @@ var PAID_FEATURES = {
7757
7882
  };
7758
7883
 
7759
7884
  // src/version.ts
7760
- var version = "1.9.4";
7885
+ var version = "1.10.0";
7761
7886
 
7762
7887
  // src/license.ts
7763
7888
  var PUBLIC_KEY = { kty: "EC", crv: "P-256", x: "sUTKkglZmk-HMKPNub8p7AeLBKesHCU8irIkw9rJlbE", y: "j0U_aKRDR7iApY9UpaD3hUemhBLZJwDv9-QiT_hUyw8" };
@@ -8056,6 +8181,12 @@ var ColumnMenu = class {
8056
8181
  if (grouped.length) g.push([loc.expandAll, () => this.grid.expandAll(true)], [loc.collapseAll, () => this.grid.expandAll(false)]);
8057
8182
  popup.append(this.section(g));
8058
8183
  }
8184
+ if (this.grid.getRollup()) {
8185
+ const cur = def.aggFunc ?? null;
8186
+ const mark = (on, label) => `${on ? "\u2713 " : ""}${label}`;
8187
+ const aggs = ["sum", "avg", "min", "max", "count"].map((a) => [mark(cur === a, loc[a]), () => this.grid.setColumnAggFunc(colId, a)]);
8188
+ popup.append(this.section([[mark(!cur, loc.noAggregate), () => this.grid.setColumnAggFunc(colId, null)], ...aggs]));
8189
+ }
8059
8190
  const items = [
8060
8191
  [loc.autosizeThis, () => this.grid.autoSize([colId])],
8061
8192
  [loc.autosizeAll, () => this.grid.autoSize()]
@@ -8083,14 +8214,30 @@ var ColumnMenu = class {
8083
8214
  this.close();
8084
8215
  this.grid.renderer.gridEl.focus();
8085
8216
  };
8217
+ const onScroll = () => this.close();
8218
+ const scroller = this.grid.renderer.scroller;
8086
8219
  document.addEventListener("pointerdown", onDown, true);
8087
8220
  document.addEventListener("keydown", onKey, true);
8221
+ scroller.addEventListener("scroll", onScroll, { passive: true });
8088
8222
  this.off.push(
8089
8223
  () => document.removeEventListener("pointerdown", onDown, true),
8090
- () => document.removeEventListener("keydown", onKey, true)
8224
+ () => document.removeEventListener("keydown", onKey, true),
8225
+ () => scroller.removeEventListener("scroll", onScroll)
8091
8226
  );
8092
8227
  popup.querySelector(".fg-filter-value:not([hidden]), .fg-set-search, .fg-menu-item")?.focus();
8093
8228
  }
8229
+ /** Keyboard (Alt+ArrowDown): the menu of the focused cell's column (else the first column), under its header. */
8230
+ openAtFocus() {
8231
+ const { grid } = this;
8232
+ const f = grid.store.state.focus;
8233
+ const colId = f && !isSystemCol(f.colId) ? f.colId : grid.columns.displayed().find((c) => !isSystemCol(c.id))?.id;
8234
+ if (!colId) return false;
8235
+ const root = grid.renderer.root;
8236
+ const id = CSS.escape(colId);
8237
+ 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;
8238
+ this.open(colId, anchor);
8239
+ return true;
8240
+ }
8094
8241
  /** The filter UI of a column (used by the menu and the filters panel); `done` runs after apply/reset. */
8095
8242
  filterSection(colId, done) {
8096
8243
  const node = this.grid.columns.getNode(colId);
@@ -8115,6 +8262,8 @@ var ColumnMenu = class {
8115
8262
  b.addEventListener("click", () => {
8116
8263
  run2();
8117
8264
  this.close();
8265
+ const a = activeElement(this.grid.renderer.root);
8266
+ if (!a || a === document.body || !a.isConnected) this.grid.renderer.gridEl.focus({ preventScroll: true });
8118
8267
  });
8119
8268
  s.append(b);
8120
8269
  }
@@ -8311,6 +8460,7 @@ var RowModel = class {
8311
8460
  this.sorted = [];
8312
8461
  this.display = [];
8313
8462
  this.indexById = null;
8463
+ this.orderedCache = null;
8314
8464
  this.layoutCache = null;
8315
8465
  /** Searchable text of each row, kept between quick filter keystrokes. */
8316
8466
  this.quickCache = null;
@@ -8369,6 +8519,13 @@ var RowModel = class {
8369
8519
  fullCount() {
8370
8520
  return this.full.length;
8371
8521
  }
8522
+ dataRowsBefore(index) {
8523
+ const end = Math.min(index, this.full.length);
8524
+ if (this.full === this.sorted) return end;
8525
+ let n = 0;
8526
+ for (let k = 0; k < end; k++) if (!this.full[k].kind) n++;
8527
+ return n;
8528
+ }
8372
8529
  dataCount() {
8373
8530
  return this.sorted.length;
8374
8531
  }
@@ -8389,6 +8546,19 @@ var RowModel = class {
8389
8546
  dataRows() {
8390
8547
  return this.sorted;
8391
8548
  }
8549
+ /** Filtered data rows in display order, as if every group and tree row were expanded. */
8550
+ orderedDataRows() {
8551
+ const c = this.orderedCache;
8552
+ if (c && c.full === this.full) return c.rows;
8553
+ const d = this.last.display;
8554
+ let rows = this.sorted;
8555
+ if (d && (d.getParentId || d.groupBy.length)) {
8556
+ const open = { ...d, expanded: /* @__PURE__ */ new Map(), defaultExpanded: -1, detailOpen: /* @__PURE__ */ new Set(), lazyChildren: void 0 };
8557
+ rows = this.buildDisplay(open, this.last.sort ?? [], this.last.collator ?? this.deps.collator()).filter((n) => !n.kind);
8558
+ }
8559
+ this.orderedCache = { full: this.full, rows };
8560
+ return rows;
8561
+ }
8392
8562
  allNodes() {
8393
8563
  return this.nodes;
8394
8564
  }
@@ -8791,6 +8961,7 @@ var ServerRowModel = class {
8791
8961
  /** Server-side grouping, while groupBy has columns. */
8792
8962
  this.groups = null;
8793
8963
  this.displayIn = null;
8964
+ this.quiet = false;
8794
8965
  }
8795
8966
  get bs() {
8796
8967
  return this.page ? this.page.size : this.deps.blockSize;
@@ -8889,9 +9060,19 @@ var ServerRowModel = class {
8889
9060
  const abs = this.base + index;
8890
9061
  const hit = this.peek(abs);
8891
9062
  if (hit) return hit;
8892
- if (index >= 0 && index < this.getRowCount()) this.ensure(Math.floor(abs / this.bs));
9063
+ if (!this.quiet && index >= 0 && index < this.getRowCount()) this.ensure(Math.floor(abs / this.bs));
8893
9064
  return this.placeholder(abs);
8894
9065
  }
9066
+ /** Run `fn` with getRow answering from loaded blocks only (rows not loaded stay placeholders, nothing is requested). */
9067
+ withoutLoading(fn) {
9068
+ const was = this.quiet;
9069
+ this.quiet = true;
9070
+ try {
9071
+ return fn();
9072
+ } finally {
9073
+ this.quiet = was;
9074
+ }
9075
+ }
8895
9076
  ensure(k) {
8896
9077
  const cur = this.blocks.get(k);
8897
9078
  if (cur && (cur.state !== "failed" || Date.now() < (cur.retryAt ?? 0))) return;
@@ -9313,17 +9494,23 @@ var FilterBuilder = class {
9313
9494
  actions.append(reset, ok);
9314
9495
  popup.append(head, join, list, addBtn, actions);
9315
9496
  popupParent(grid.renderer.root).append(popup);
9497
+ if (!rows.length) addBtn.click();
9316
9498
  const r = grid.renderer.root.getBoundingClientRect();
9317
9499
  const w = popup.offsetWidth || 460;
9318
9500
  popup.style.left = `${Math.max(4, Math.min(r.left + 24, (window.innerWidth || 1024) - w - 4))}px`;
9319
- popup.style.top = `${Math.max(4, r.top + 24)}px`;
9501
+ const header2 = grid.renderer.headerEl.getBoundingClientRect();
9502
+ const vh = window.innerHeight || 768;
9503
+ const top = Math.max(4, Math.min((header2.height ? header2.bottom : r.top) + 8, vh - (popup.offsetHeight || 0) - 4));
9504
+ popup.style.top = `${top}px`;
9505
+ popup.style.maxHeight = `${Math.max(160, vh - top - 8)}px`;
9320
9506
  this.popup = popup;
9321
- if (!rows.length) addBtn.click();
9322
9507
  const onDown = (e) => {
9323
9508
  if (!eventInside(e, popup)) this.close();
9324
9509
  };
9325
9510
  const onKey = (e) => {
9326
- if (e.key === "Escape") this.close();
9511
+ if (e.key !== "Escape") return;
9512
+ this.close();
9513
+ grid.renderer.gridEl.focus();
9327
9514
  };
9328
9515
  document.addEventListener("pointerdown", onDown, true);
9329
9516
  document.addEventListener("keydown", onKey, true);
@@ -9461,8 +9648,8 @@ var FormatDialog = class {
9461
9648
  CF_PRESETS.forEach((p, k) => {
9462
9649
  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) });
9463
9650
  b.textContent = p.key === "bold" ? "B" : "Aa";
9464
- if (p.style.background) b.style.background = p.style.background;
9465
- if (p.style.color) b.style.color = p.style.color;
9651
+ b.style.background = p.style.background ?? "#ffffff";
9652
+ b.style.color = p.style.color ?? "#1f2430";
9466
9653
  b.addEventListener("click", () => {
9467
9654
  preset = k;
9468
9655
  for (const s of swatches.children) s.setAttribute("aria-pressed", String(s === b));
@@ -9561,7 +9748,9 @@ var FormatDialog = class {
9561
9748
  if (!eventInside(e, popup)) this.close();
9562
9749
  };
9563
9750
  const onKey = (e) => {
9564
- if (e.key === "Escape") this.close();
9751
+ if (e.key !== "Escape") return;
9752
+ this.close();
9753
+ grid.renderer.gridEl.focus();
9565
9754
  };
9566
9755
  document.addEventListener("pointerdown", onDown, true);
9567
9756
  document.addEventListener("keydown", onKey, true);
@@ -10005,6 +10194,10 @@ var Grid = class {
10005
10194
  this.sourceDefs = [];
10006
10195
  this.pivotCfg = null;
10007
10196
  this.pivotData = null;
10197
+ /** rollup option / setRollup: rows merged by the shown columns without an aggFunc. */
10198
+ this.rollupOn = false;
10199
+ /** The merged rows while rollup applies (not while pivoting), else null. */
10200
+ this.rollupData = null;
10008
10201
  /** Grouping state from before pivot mode, restored when it ends. */
10009
10202
  this.savedGroup = null;
10010
10203
  /** 0-based page and rows per page (pagination only). */
@@ -10071,6 +10264,8 @@ var Grid = class {
10071
10264
  }
10072
10265
  };
10073
10266
  this.density = "normal";
10267
+ /** Set by the cellPadding option or setCellPadding; null: the theme / density value. */
10268
+ this.cellPadding = null;
10074
10269
  // ── merge, aggregates, fill, pinned rows ────────────────────
10075
10270
  /** `sortWithinMerges` is on. */
10076
10271
  this.sortWithin = false;
@@ -10151,6 +10346,7 @@ var Grid = class {
10151
10346
  this.renderer.applyTheme();
10152
10347
  if (options.flashDuration) this.renderer.root.style.setProperty("--fg-flash-duration", `${this.flashMs()}ms`);
10153
10348
  this.renderer.applyFont(this.font.size, this.font.family);
10349
+ if (options.cellPadding != null) this.applyCellPadding(options.cellPadding);
10154
10350
  this.menu = new ColumnMenu(this);
10155
10351
  this.chooser = new ColumnChooserDialog(this);
10156
10352
  this.contextMenu = new ContextMenu(this);
@@ -10186,6 +10382,7 @@ var Grid = class {
10186
10382
  this.disposers.push(() => ro.disconnect());
10187
10383
  }
10188
10384
  if (options.pivot) this.setPivot(options.pivot);
10385
+ if (options.rollup) this.setRollup(true);
10189
10386
  void needsFreeBadge(options.licenseKey).then((show) => {
10190
10387
  if (show && !this.destroyed) this.renderer.showFreeBadge();
10191
10388
  });
@@ -10264,6 +10461,7 @@ var Grid = class {
10264
10461
  }
10265
10462
  /** Column definitions or visibility changed; filters/quick filter may depend on them. */
10266
10463
  columnsChanged() {
10464
+ this.rebuildRollup();
10267
10465
  this.rowModel.invalidate();
10268
10466
  this.updateRows();
10269
10467
  this.refreshSummary();
@@ -10286,7 +10484,7 @@ var Grid = class {
10286
10484
  }
10287
10485
  updateRows() {
10288
10486
  const paged = !!this.options.pagination;
10289
- const data = this.pivotData ?? this.store.state.data;
10487
+ const data = this.viewRows();
10290
10488
  this.rowModel.update({ ...this.store.state, data, display: this.display(), page: paged ? { index: this.page, size: this.pageSize } : null });
10291
10489
  if (paged && this.page >= this.getPageCount()) {
10292
10490
  this.page = this.getPageCount() - 1;
@@ -10294,9 +10492,63 @@ var Grid = class {
10294
10492
  }
10295
10493
  }
10296
10494
  // ── pivot ───────────────────────────────────────────────────
10297
- /** Rows behind the view: the pivot table while pivoting, else the source data. */
10495
+ /** Rows behind the view: the pivot table while pivoting, the merged rows with rollup, else the source data. */
10298
10496
  viewRows() {
10299
- return this.pivotData ?? this.store.state.data;
10497
+ return this.pivotData ?? this.rollupData ?? this.store.state.data;
10498
+ }
10499
+ // ── rollup ──────────────────────────────────────────────────
10500
+ getRollup() {
10501
+ return this.rollupOn;
10502
+ }
10503
+ setRollup(on) {
10504
+ on = !!on;
10505
+ if (on && !this.allow("pivot")) return;
10506
+ if (on && this.clientOnly("rollup")) return;
10507
+ if (on && this.options.treeData) {
10508
+ warn("rollup is not available with treeData");
10509
+ return;
10510
+ }
10511
+ if (on === this.rollupOn) return;
10512
+ this.editor.cancel();
10513
+ this.rollupOn = on;
10514
+ this.store.set({ range: null, focus: null });
10515
+ this.columnsChanged();
10516
+ }
10517
+ /** Merge the source rows again (rollup on, not pivoting); the view is updated by the caller. */
10518
+ rebuildRollup() {
10519
+ this.rollupData = this.rollupOn && !this.pivotCfg ? buildRollup(this.store.state.data, this.dataColumns().map((c) => c.def)) : null;
10520
+ }
10521
+ /** The source rows behind `rows`: merged rollup rows stand for the rows they merge (for group and summary values). */
10522
+ sourceRowsOf(rows) {
10523
+ return this.rollupData ? rows.flatMap((r) => rollupSource(r) ?? [r]) : rows;
10524
+ }
10525
+ /** Whether a cell can be changed: never on merged rollup rows. */
10526
+ cellEditable(def, row, rowIndex) {
10527
+ return !this.rollupData && cellEditable(def, row, rowIndex);
10528
+ }
10529
+ /** Set or clear a column's aggFunc (null: no aggregate, so with rollup it becomes a column the rows merge by). */
10530
+ setColumnAggFunc(colId, aggFunc) {
10531
+ if (aggFunc !== null && typeof aggFunc !== "function" && !["sum", "avg", "min", "max", "count"].includes(aggFunc)) {
10532
+ warn(`setColumnAggFunc: unknown aggFunc "${String(aggFunc)}"`);
10533
+ return;
10534
+ }
10535
+ let found = false;
10536
+ const map = (defs2) => defs2.map((d) => {
10537
+ if (!d || typeof d !== "object") return d;
10538
+ if (Array.isArray(d.children)) return { ...d, children: map(d.children) };
10539
+ if ((d.id ?? d.field) !== colId) return d;
10540
+ found = true;
10541
+ const next = { ...d };
10542
+ if (aggFunc) next.aggFunc = aggFunc;
10543
+ else delete next.aggFunc;
10544
+ return next;
10545
+ });
10546
+ const defs = map(this.sourceDefs);
10547
+ if (!found) {
10548
+ warn(`setColumnAggFunc: no column "${colId}"`);
10549
+ return;
10550
+ }
10551
+ this.setColumns(defs);
10300
10552
  }
10301
10553
  /** Leaf columns of the user's definitions (not the pivot columns). */
10302
10554
  sourceLeaves() {
@@ -10354,6 +10606,7 @@ var Grid = class {
10354
10606
  afterPivotSwitch() {
10355
10607
  this.page = 0;
10356
10608
  this.store.set({ sort: [], filters: {}, range: null, focus: null });
10609
+ this.rebuildRollup();
10357
10610
  this.rowModel.invalidate();
10358
10611
  this.updateRows();
10359
10612
  this.displayVersion++;
@@ -10462,12 +10715,31 @@ var Grid = class {
10462
10715
  } finally {
10463
10716
  this.keepRange = false;
10464
10717
  }
10718
+ this.clampRanges();
10465
10719
  }
10466
10720
  this.editor.sync();
10467
10721
  if (flashKeys.size && this.options.flashCells !== false) this.flash(flashKeys);
10468
10722
  this.events.emit("transactionApplied", result);
10469
10723
  return result;
10470
10724
  }
10725
+ /** Kept ranges and focus after the row count shrank: cut to the last row, dropped when wholly past it. */
10726
+ clampRanges() {
10727
+ const count = this.rowModel.getRowCount();
10728
+ const { range, extraRanges, focus } = this.store.state;
10729
+ const clamp = (r) => {
10730
+ if (!r || r.start.rowIndex < count && r.end.rowIndex < count) return r;
10731
+ if (Math.min(r.start.rowIndex, r.end.rowIndex) >= count) return null;
10732
+ return { start: { ...r.start, rowIndex: Math.min(r.start.rowIndex, count - 1) }, end: { ...r.end, rowIndex: Math.min(r.end.rowIndex, count - 1) } };
10733
+ };
10734
+ const extras = extraRanges.map(clamp).filter((r) => !!r);
10735
+ if (extras.length !== extraRanges.length || extras.some((r, i) => r !== extraRanges[i])) this.store.set({ extraRanges: extras });
10736
+ if (focus && focus.rowIndex >= count) this.store.set({ focus: count ? { ...focus, rowIndex: count - 1 } : null });
10737
+ const next = clamp(range);
10738
+ if (next !== range) {
10739
+ this.store.set({ range: next });
10740
+ this.events.emit("rangeChange", { range: next && { start: { ...next.start }, end: { ...next.end } } });
10741
+ }
10742
+ }
10471
10743
  /** One transaction against `data` (remove → update → add); returns the new array. */
10472
10744
  /** Whether rows changed in `fields` could filter, sort, group or move differently. */
10473
10745
  viewReads(fields) {
@@ -10622,14 +10894,24 @@ var Grid = class {
10622
10894
  }
10623
10895
  if (want("groupBy") && Array.isArray(state.groupBy)) this.setGroupBy(state.groupBy.filter(known));
10624
10896
  if (want("pivot")) this.setPivot(state.pivot ?? null);
10625
- if (want("sort") && Array.isArray(state.sort)) this.setSort(state.sort.filter((x) => x && known(x.colId)));
10626
- if (want("filters") && state.filters && typeof state.filters === "object") {
10627
- const filters = Object.fromEntries(Object.entries(state.filters).filter(([id]) => known(id)));
10628
- this.store.set({ filters });
10629
- this.events.emit("filterChange", { filters: { ...filters } });
10897
+ const query = {};
10898
+ if (want("sort") && Array.isArray(state.sort)) {
10899
+ const sort = this.checkSort(state.sort.filter((x) => x && known(x.colId)));
10900
+ if (sort) query.sort = sort;
10630
10901
  }
10631
- if (want("advancedFilter")) this.setAdvancedFilter(state.advancedFilter ?? null);
10632
- if (want("quickFilter") && typeof state.quickFilter === "string") this.store.set({ quickFilter: state.quickFilter });
10902
+ if (want("filters") && state.filters && typeof state.filters === "object") {
10903
+ query.filters = Object.fromEntries(Object.entries(state.filters).filter(([id]) => known(id)));
10904
+ }
10905
+ if (want("advancedFilter")) {
10906
+ const f = state.advancedFilter;
10907
+ const next = f && typeof f === "object" && Array.isArray(f.conditions) && f.conditions.length ? f : null;
10908
+ if (next !== this.store.state.advancedFilter && (!next || this.allow("advancedFilter"))) query.advancedFilter = next;
10909
+ }
10910
+ if (want("quickFilter") && typeof state.quickFilter === "string") query.quickFilter = state.quickFilter;
10911
+ this.store.set(query);
10912
+ if (query.sort) this.events.emit("sortChange", { sort: [...query.sort] });
10913
+ if (query.filters) this.events.emit("filterChange", { filters: { ...query.filters } });
10914
+ if ("advancedFilter" in query) this.events.emit("advancedFilterChange", { advancedFilter: query.advancedFilter ?? null });
10633
10915
  if (want("expanded") && state.expanded && typeof state.expanded === "object") {
10634
10916
  const expanded = new Map(Object.entries(state.expanded).filter(([, v]) => typeof v === "boolean"));
10635
10917
  if (this.savedGroup) this.savedGroup.expanded = expanded;
@@ -10655,11 +10937,13 @@ var Grid = class {
10655
10937
  rows.forEach((r, k) => {
10656
10938
  nums[k] = r.kind ? 0 : ++n;
10657
10939
  });
10658
- this.rowNumberCache = { rows, nums };
10940
+ const first = this.page * this.pageSize;
10941
+ const offset = this.options.pagination && !this.rowModel.server ? this.rowModel.dataRowsBefore?.(first) ?? first : 0;
10942
+ this.rowNumberCache = { rows, nums, offset };
10659
10943
  }
10660
10944
  const v = this.rowNumberCache.nums[i];
10661
10945
  if (!v) return null;
10662
- return v + (this.options.pagination && !this.rowModel.server ? this.page * this.pageSize : 0);
10946
+ return v + this.rowNumberCache.offset;
10663
10947
  }
10664
10948
  getPage() {
10665
10949
  return this.options.pagination ? this.page : 0;
@@ -10711,6 +10995,7 @@ var Grid = class {
10711
10995
  /** Server rows: drop the cache and load again. */
10712
10996
  refreshRows() {
10713
10997
  if (this.rowModel instanceof ServerRowModel) {
10998
+ this.saveEditBeforeReload();
10714
10999
  this.rowModel.purge();
10715
11000
  this.renderer.invalidate({ rows: true });
10716
11001
  }
@@ -10720,6 +11005,12 @@ var Grid = class {
10720
11005
  this.renderer.invalidate({ header: true, rows: true });
10721
11006
  this.events.emit("rowsLoaded", total === null ? { start, end } : { start, end, total });
10722
11007
  }
11008
+ /** Server rows are about to be dropped: save an open editor to its row first, else it is left over another row. */
11009
+ saveEditBeforeReload() {
11010
+ if (!this.rowModel.server) return;
11011
+ this.editor.endRow(true);
11012
+ if (this.editor.active && !this.editor.commit(null)) this.editor.cancel();
11013
+ }
10723
11014
  serverFailed(start, end, error) {
10724
11015
  if (this.destroyed) return;
10725
11016
  warn(`datasource.getRows failed for rows ${start}\u2013${end}`);
@@ -10868,7 +11159,7 @@ var Grid = class {
10868
11159
  let m = this.aggCache.map.get(node.group);
10869
11160
  if (!m) this.aggCache.map.set(node.group, m = /* @__PURE__ */ new Map());
10870
11161
  if (!m.has(colId)) {
10871
- const rows = node.group.rows;
11162
+ const rows = this.sourceRowsOf(node.group.rows);
10872
11163
  try {
10873
11164
  m.set(colId, aggregate(def.aggFunc, rows.map((r) => getCellValue(def, r)), rows));
10874
11165
  } catch {
@@ -11061,10 +11352,33 @@ var Grid = class {
11061
11352
  this.renderer.invalidateHeights();
11062
11353
  this.layoutChanged();
11063
11354
  }
11355
+ /** Store and apply the padding; false when the value is not valid. */
11356
+ applyCellPadding(px) {
11357
+ if (px != null && !(typeof px === "number" && Number.isFinite(px) && px >= 0)) {
11358
+ warn("cellPadding must be a number of px, 0 or more");
11359
+ return false;
11360
+ }
11361
+ this.cellPadding = px;
11362
+ if (px == null) this.renderer.root.style.removeProperty("--fg-cell-px");
11363
+ else this.renderer.root.style.setProperty("--fg-cell-px", `${px}px`);
11364
+ return true;
11365
+ }
11366
+ setCellPadding(px) {
11367
+ if (px === this.cellPadding || !this.applyCellPadding(px)) return;
11368
+ this.menu.close();
11369
+ this.layoutChanged();
11370
+ }
11371
+ /** The padding in use: the set value, else the theme / density CSS variable. */
11372
+ getCellPadding() {
11373
+ if (this.cellPadding != null) return this.cellPadding;
11374
+ const v = parseFloat(getComputedStyle(this.renderer.root).getPropertyValue("--fg-cell-px"));
11375
+ return Number.isFinite(v) ? v : 12;
11376
+ }
11064
11377
  /** Copy the grid font variables onto an element rendered outside the root (popups, drag ghost). */
11065
11378
  applyFontTo(target) {
11066
11379
  if (this.font.size) target.style.setProperty("--fg-font-size", `${this.font.size}px`);
11067
11380
  if (this.font.family) target.style.setProperty("--fg-font", this.font.family);
11381
+ if (this.cellPadding != null) target.style.setProperty("--fg-cell-px", `${this.cellPadding}px`);
11068
11382
  }
11069
11383
  /** Current summary configuration and values, or null when there are no summary rows. */
11070
11384
  summary() {
@@ -11093,7 +11407,7 @@ var Grid = class {
11093
11407
  }
11094
11408
  const scope = opts.scope ?? "filtered";
11095
11409
  const rows = scope === "all" ? this.rowModel.allNodes().map((n) => n.data) : scope === "selected" ? this.selectedRows() : this.rowModel.dataRows().map((n) => n.data);
11096
- this.summaryValues = computeSummary(opts.rows, rows, (id) => this.columns.getNode(id)?.def);
11410
+ this.summaryValues = computeSummary(opts.rows, this.sourceRowsOf(rows), (id) => this.columns.getNode(id)?.def);
11097
11411
  }
11098
11412
  // ── cell ranges ─────────────────────────────────────────────
11099
11413
  dataColumns() {
@@ -11186,12 +11500,11 @@ var Grid = class {
11186
11500
  out = [];
11187
11501
  for (let i = b.r1; i <= b.r2; i++) {
11188
11502
  const node = this.rowModel.getRow(i);
11189
- if (!node.kind) out.push(node.data);
11503
+ if (node && !node.kind) out.push(node.data);
11190
11504
  }
11191
11505
  cols = displayed.slice(b.c1, b.c2 + 1).filter((c) => !isSystemCol(c.id));
11192
11506
  } else if (checkedRows) {
11193
- const sel = this.store.state.selected;
11194
- out = this.rowModel.dataRows().filter((n) => sel.has(n.id)).map((n) => n.data);
11507
+ out = this.shownSelectedRows();
11195
11508
  cols = this.dataColumns();
11196
11509
  } else {
11197
11510
  const f = this.store.state.focus;
@@ -11246,8 +11559,17 @@ var Grid = class {
11246
11559
  if (text2) this.clearCells(true);
11247
11560
  return text2;
11248
11561
  }
11562
+ /** Text a copy puts on the clipboard. Server rows: only the loaded rows, with a warning about the rest. */
11563
+ copyText(headers, rows = false) {
11564
+ const m = this.rowModel;
11565
+ if (!(m instanceof ServerRowModel)) return this.selectionTSV(headers, rows);
11566
+ const text2 = m.withoutLoading(() => this.selectionTSV(headers, rows));
11567
+ const missing = rows ? 0 : m.withoutLoading(() => this.unloadedInRanges());
11568
+ 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`);
11569
+ return text2;
11570
+ }
11249
11571
  copyToClipboard(headers, rows = false) {
11250
- const text2 = this.selectionTSV(headers, rows);
11572
+ const text2 = this.copyText(headers, rows);
11251
11573
  if (text2 && typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
11252
11574
  try {
11253
11575
  navigator.clipboard.writeText(text2)?.catch?.(() => void 0);
@@ -11256,6 +11578,14 @@ var Grid = class {
11256
11578
  }
11257
11579
  return text2;
11258
11580
  }
11581
+ /** Server rows: how many rows the ranges cover that are not loaded (placeholders). */
11582
+ unloadedInRanges() {
11583
+ const seen2 = /* @__PURE__ */ new Set();
11584
+ for (const b of this.allRangeBounds()) for (let i = b.r1; i <= b.r2; i++) {
11585
+ if (this.rowModel.getRow(i)?.kind === "loading") seen2.add(i);
11586
+ }
11587
+ return seen2.size;
11588
+ }
11259
11589
  pasteText(text2) {
11260
11590
  const matrix = parseTSV(text2);
11261
11591
  const result = { changed: 0, skipped: 0 };
@@ -11278,19 +11608,21 @@ var Grid = class {
11278
11608
  const single = matrix.length === 1 && matrix[0].length === 1;
11279
11609
  const rows = single && b ? b.r2 - b.r1 + 1 : matrix.length;
11280
11610
  const width = single && b ? b.c2 - b.c1 + 1 : Math.max(...matrix.map((r) => r.length));
11281
- for (let dr = 0; dr < rows; dr++) {
11282
- const ri = r0 + dr;
11283
- if (ri >= count) break;
11611
+ for (let ri = r0, dr = 0; ri < count && dr < rows; ri++) {
11284
11612
  const node = this.rowModel.getRow(ri);
11285
- if (node.kind) continue;
11613
+ if (node.kind) {
11614
+ if (single && b) dr++;
11615
+ continue;
11616
+ }
11617
+ const line2 = dr++;
11286
11618
  for (let dc = 0; dc < width; dc++) {
11287
11619
  const col = displayed[c0 + dc];
11288
11620
  if (!col) break;
11289
11621
  if (isSystemCol(col.id)) continue;
11290
- const raw = single && b ? matrix[0][0] : matrix[dr]?.[dc];
11622
+ const raw = single && b ? matrix[0][0] : matrix[line2]?.[dc];
11291
11623
  if (raw === void 0) continue;
11292
11624
  const def = col.def;
11293
- if (!def.field || !cellEditable(def, node.data, ri)) {
11625
+ if (!def.field || !this.cellEditable(def, node.data, ri)) {
11294
11626
  result.skipped++;
11295
11627
  continue;
11296
11628
  }
@@ -11358,7 +11690,7 @@ var Grid = class {
11358
11690
  const id = this.rowModel.idOf(row);
11359
11691
  const rowIndex = this.rowModel.indexOfId(id);
11360
11692
  for (const c of cols) {
11361
- if (!cellEditable(c.def, row, rowIndex < 0 ? i : rowIndex)) continue;
11693
+ if (!this.cellEditable(c.def, row, rowIndex < 0 ? i : rowIndex)) continue;
11362
11694
  const message = this.cellError(c.def, row, row[c.def.field]);
11363
11695
  if (message !== null) out.push({ row, rowIndex, colId: c.id, message });
11364
11696
  }
@@ -11623,26 +11955,36 @@ var Grid = class {
11623
11955
  const col = displayed[c];
11624
11956
  const node = this.rowModel.getRow(r);
11625
11957
  const def = col?.def;
11626
- if (!node || node.kind || !def?.field || isSystemCol(col.id) || !cellEditable(def, node.data, r)) return;
11958
+ if (!node || node.kind || !def?.field || isSystemCol(col.id) || !this.cellEditable(def, node.data, r)) return;
11627
11959
  if (this.cellError(def, node.data, value2) !== null) return;
11628
11960
  this.writeCell(changes, node.data, def.field, col.id, r, value2);
11629
11961
  };
11630
11962
  const value = (r, c) => getCellValue(displayed[c].def, this.rowModel.getRow(r).data);
11963
+ const dataRow = (r) => {
11964
+ const node = this.rowModel.getRow(r);
11965
+ return !!node && !node.kind;
11966
+ };
11967
+ const keep = (from, to, ok) => {
11968
+ const out = [];
11969
+ for (let i = from; i <= to; i++) if (ok(i)) out.push(i);
11970
+ return out;
11971
+ };
11972
+ const dataCol = (c) => !!displayed[c] && !isSystemCol(displayed[c].id);
11631
11973
  if (t.r1 < src.r1 || t.r2 > src.r2) {
11974
+ const down = keep(src.r2 + 1, t.r2, dataRow), up = keep(t.r1, src.r1 - 1, dataRow).reverse();
11975
+ const rows = keep(src.r1, src.r2, dataRow);
11632
11976
  for (let c = src.c1; c <= src.c2; c++) {
11633
- const down = [], up = [];
11634
- for (let r = src.r1; r <= src.r2; r++) down.push(value(r, c));
11635
- for (let r = src.r2; r >= src.r1; r--) up.push(value(r, c));
11636
- fillSeries(down, t.r2 - src.r2).forEach((v, k) => write(src.r2 + 1 + k, c, v));
11637
- fillSeries(up, src.r1 - t.r1).forEach((v, k) => write(src.r1 - 1 - k, c, v));
11977
+ const seed = rows.map((r) => value(r, c));
11978
+ fillSeries(seed, down.length).forEach((v, k) => write(down[k], c, v));
11979
+ fillSeries([...seed].reverse(), up.length).forEach((v, k) => write(up[k], c, v));
11638
11980
  }
11639
11981
  } else if (t.c1 < src.c1 || t.c2 > src.c2) {
11640
- for (let r = src.r1; r <= src.r2; r++) {
11641
- const right = [], left = [];
11642
- for (let c = src.c1; c <= src.c2; c++) right.push(value(r, c));
11643
- for (let c = src.c2; c >= src.c1; c--) left.push(value(r, c));
11644
- fillSeries(right, t.c2 - src.c2).forEach((v, k) => write(r, src.c2 + 1 + k, v));
11645
- fillSeries(left, src.c1 - t.c1).forEach((v, k) => write(r, src.c1 - 1 - k, v));
11982
+ const right = keep(src.c2 + 1, t.c2, dataCol), left = keep(t.c1, src.c1 - 1, dataCol).reverse();
11983
+ const cols = keep(src.c1, src.c2, dataCol);
11984
+ for (const r of keep(src.r1, src.r2, dataRow)) {
11985
+ const seed = cols.map((c) => value(r, c));
11986
+ fillSeries(seed, right.length).forEach((v, k) => write(r, right[k], v));
11987
+ fillSeries([...seed].reverse(), left.length).forEach((v, k) => write(r, left[k], v));
11646
11988
  }
11647
11989
  } else return;
11648
11990
  this.endCellTx(changes);
@@ -11711,7 +12053,7 @@ var Grid = class {
11711
12053
  const node = this.rowModel.getRow(r);
11712
12054
  for (let c = box.c1; c <= box.c2; c++) {
11713
12055
  const def = displayed[c]?.def;
11714
- 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);
12056
+ 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);
11715
12057
  }
11716
12058
  }
11717
12059
  this.endCellTx(changes);
@@ -11865,8 +12207,9 @@ var Grid = class {
11865
12207
  const cols = (ids ? ids.map((id) => this.columns.getColumn(id)) : this.columns.displayed()).filter((c) => !!c && isResizable(c.def));
11866
12208
  const { first, last } = this.renderer.renderedRange();
11867
12209
  const code = this.store.state.locale.code;
11868
- const HEADER_EXTRA = 24 + 22 + 6 + 14;
11869
- const CELL_EXTRA = 24;
12210
+ const pad4 = this.getCellPadding() * 2;
12211
+ const HEADER_EXTRA = pad4 + 22 + 6 + 14;
12212
+ const CELL_EXTRA = pad4;
11870
12213
  for (const col of cols) {
11871
12214
  let w = this.renderer.measureText(col.def.header ?? col.def.field ?? "", true) + HEADER_EXTRA;
11872
12215
  const fmt = getFormatter(col.def, code);
@@ -11884,6 +12227,13 @@ var Grid = class {
11884
12227
  this.columnsChanged();
11885
12228
  }
11886
12229
  setSort(sort) {
12230
+ const valid = this.checkSort(sort);
12231
+ if (!valid) return;
12232
+ this.store.set({ sort: valid });
12233
+ this.events.emit("sortChange", { sort: [...valid] });
12234
+ }
12235
+ /** The known, well-formed sort items, or null when a beforeSort handler refused them. */
12236
+ checkSort(sort) {
11887
12237
  const valid = sort.filter((s) => {
11888
12238
  const node = this.columns.getNode(s.colId);
11889
12239
  if (!node || node.children) {
@@ -11892,9 +12242,7 @@ var Grid = class {
11892
12242
  }
11893
12243
  return s.dir === "asc" || s.dir === "desc";
11894
12244
  });
11895
- if (this.veto("beforeSort", { sort: valid.map((s) => ({ ...s })) })) return;
11896
- this.store.set({ sort: valid });
11897
- this.events.emit("sortChange", { sort: [...valid] });
12245
+ return this.veto("beforeSort", { sort: valid.map((s) => ({ ...s })) }) ? null : valid;
11898
12246
  }
11899
12247
  toggleSort(colId, additive) {
11900
12248
  const cur = this.store.state.sort;
@@ -12064,6 +12412,15 @@ var Grid = class {
12064
12412
  const sel = this.store.state.selected;
12065
12413
  return sel.size ? this.rowModel.allNodes().filter((n) => sel.has(n.id)).map((n) => n.data) : [];
12066
12414
  }
12415
+ /** Filtered data rows in display order (group buckets, parents before children), collapsed ones included. */
12416
+ orderedDataRows() {
12417
+ return this.rowModel.orderedDataRows?.() ?? this.rowModel.dataRows();
12418
+ }
12419
+ /** Checked rows that pass the filters, in display order: what copy, export and delete of the checked rows act on. */
12420
+ shownSelectedRows() {
12421
+ const sel = this.store.state.selected;
12422
+ return sel.size ? this.orderedDataRows().filter((n) => sel.has(n.id)).map((n) => n.data) : [];
12423
+ }
12067
12424
  withSelect(defs) {
12068
12425
  const sys = [];
12069
12426
  const rn = this.options.rowNumbers;
@@ -12135,12 +12492,26 @@ var Grid = class {
12135
12492
  });
12136
12493
  this.columnsChanged();
12137
12494
  }
12495
+ /** Close every popup of this grid (menus and dialogs); focus that was in one goes back to the grid. */
12496
+ closePopups() {
12497
+ const r = this.renderer.root.getRootNode();
12498
+ const inPopup = !!(r.activeElement ?? document.activeElement)?.closest(".fg-menu");
12499
+ this.menu.close();
12500
+ this.chooser.close();
12501
+ this.contextMenu.close();
12502
+ this.filterBuilder.close();
12503
+ this.formatDialog.close();
12504
+ this.assistantPanel.close();
12505
+ if (inPopup) this.renderer.gridEl.focus({ preventScroll: true });
12506
+ }
12138
12507
  onChange(keys) {
12139
12508
  const has = (...k) => k.some((x) => keys.includes(x));
12509
+ if (has("sort", "filters", "quickFilter", "advancedFilter")) this.saveEditBeforeReload();
12140
12510
  if (this.pivotCfg && has("data", "locale")) this.rebuildPivot(false);
12511
+ if (this.rollupOn && has("data")) this.rebuildRollup();
12512
+ if (has("locale", "theme")) this.closePopups();
12141
12513
  if (has("locale")) {
12142
- this.menu.close();
12143
- this.chooser.close();
12514
+ this.finder.relabel();
12144
12515
  this.sideBar?.refresh();
12145
12516
  this.rowModel.invalidate();
12146
12517
  }
@@ -12245,6 +12616,11 @@ var Grid = class {
12245
12616
  setTheme: guard((t) => this.store.set({ theme: t })),
12246
12617
  setLocale: guard((l) => this.store.set({ locale: resolveLocale(l) })),
12247
12618
  setFont: guard((f) => this.setFont(f)),
12619
+ setCellPadding: guard((px) => this.setCellPadding(px)),
12620
+ getCellPadding: guard(() => this.getCellPadding()),
12621
+ setRollup: guard((on) => this.setRollup(on)),
12622
+ getRollup: guard(() => this.getRollup()),
12623
+ setColumnAggFunc: guard((id, agg) => this.setColumnAggFunc(id, agg)),
12248
12624
  setColumnVisible: guard((id, v) => this.setColumnsVisible([id], v)),
12249
12625
  autoSizeColumns: guard((ids) => this.autoSize(ids)),
12250
12626
  resetColumnState: guard(() => this.resetColumns()),