@freegridjs/core 1.9.5 → 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_;
@@ -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
  }
@@ -1361,6 +1377,7 @@ var REGIONS2 = ["left", "center", "right"];
1361
1377
  var NUM_OPS = [[">=", "gte"], ["<=", "lte"], ["<>", "ne"], ["!=", "ne"], [">", "gt"], ["<", "lt"], ["=", "eq"]];
1362
1378
  var DATE_OPS = [[">", "after"], ["<", "before"], ["=", "equals"]];
1363
1379
  var RANGE = /^(.+?)\s*(?:~|\.\.)\s*(.+)$/;
1380
+ var MULTI = "~many";
1364
1381
  var toNumber = (s) => {
1365
1382
  const n = Number(s.replace(/[,\s₩$€¥]/g, ""));
1366
1383
  return s.trim() !== "" && Number.isFinite(n) ? n : null;
@@ -1460,8 +1477,21 @@ var FilterRow = class {
1460
1477
  box.closest(".fg-filter-cell").classList.toggle("fg-filter-active", !!model);
1461
1478
  if (box === box.ownerDocument.activeElement || box.getRootNode().activeElement === box) continue;
1462
1479
  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;
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
+ }
1465
1495
  } else box.value = filterText(model);
1466
1496
  box.classList.remove("fg-filter-bad");
1467
1497
  }
@@ -1475,6 +1505,10 @@ var FilterRow = class {
1475
1505
  if (!def) continue;
1476
1506
  const v = select.value;
1477
1507
  this.fillOptions(select, def);
1508
+ if (v === MULTI) {
1509
+ this.rebuilt = true;
1510
+ continue;
1511
+ }
1478
1512
  if (v && ![...select.options].some((o) => o.value === v)) {
1479
1513
  const o = el("option", "", { value: v });
1480
1514
  o.textContent = String(JSON.parse(v));
@@ -1495,7 +1529,7 @@ var FilterRow = class {
1495
1529
  select.replaceChildren(all2, ...opts);
1496
1530
  }
1497
1531
  cell(c, h) {
1498
- const cell = el("div", "fg-filter-cell");
1532
+ const cell = el("div", "fg-filter-cell", { role: "gridcell" });
1499
1533
  cell.style.left = `${c.left}px`;
1500
1534
  cell.style.width = `${c.width}px`;
1501
1535
  cell.style.height = `${h}px`;
@@ -2055,6 +2089,7 @@ var Renderer = class {
2055
2089
  };
2056
2090
  this.overlayKey = "";
2057
2091
  this.heightFn = null;
2092
+ this.freeBadge = null;
2058
2093
  this.onCardClick = (e) => {
2059
2094
  const target = e.target;
2060
2095
  const card = target.closest(".fg-card");
@@ -2262,6 +2297,7 @@ var Renderer = class {
2262
2297
  if (ref.nextElementSibling !== b) ref.after(b);
2263
2298
  ref = b;
2264
2299
  }
2300
+ this.placeFreeBadge();
2265
2301
  }
2266
2302
  /** "No rows" / "Loading" over the body, or the noRowsOverlay / loadingOverlay given in the options. */
2267
2303
  paintOverlay(count) {
@@ -2352,7 +2388,7 @@ var Renderer = class {
2352
2388
  const { grid } = this;
2353
2389
  const state = grid.store.state;
2354
2390
  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}`;
2391
+ const key = `${this.version}.${grid.valuesVersion}|${count}|${total}|${state.selected.size}|${JSON.stringify(state.range)}|${JSON.stringify(state.extraRanges)}|${state.locale.code}`;
2356
2392
  if (this.statusEl && key === this.statusKey) return;
2357
2393
  this.statusKey = key;
2358
2394
  this.statusEl ?? (this.statusEl = el("div", "fg-statusbar", { role: "status", "aria-live": "polite" }));
@@ -3224,12 +3260,21 @@ var Renderer = class {
3224
3260
  /** Small "free version" mark in the bottom-right corner (unlicensed company domains flagged on freegrid.app). */
3225
3261
  showFreeBadge() {
3226
3262
  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" });
3263
+ const a = el("div", "fg-free-badge", { "aria-hidden": "true" });
3228
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>';
3229
3265
  const text2 = el("span");
3230
3266
  text2.textContent = `FreeGrid \xB7 ${this.grid.store.state.locale.freeBadge}`;
3231
3267
  a.append(text2);
3232
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;
3233
3278
  }
3234
3279
  /** Card view applies when enabled and the grid is narrower than the breakpoint. */
3235
3280
  useCards() {
@@ -3549,6 +3594,8 @@ var en = {
3549
3594
  apply: "Apply",
3550
3595
  reset: "Reset",
3551
3596
  selectAll: "(Select all)",
3597
+ setSelected: "({n} selected)",
3598
+ noAggregate: "No aggregate",
3552
3599
  search: "Search\u2026",
3553
3600
  blanks: "(Blanks)",
3554
3601
  value: "Value",
@@ -3779,7 +3826,8 @@ var DEFAULT_KEYMAP = {
3779
3826
  "Ctrl+Z": "undo",
3780
3827
  "Ctrl+Y": "redo",
3781
3828
  "Ctrl+Shift+Z": "redo",
3782
- "Ctrl+F": "find"
3829
+ "Ctrl+F": "find",
3830
+ "Alt+ArrowDown": "columnMenu"
3783
3831
  };
3784
3832
  function comboOf(e) {
3785
3833
  const parts = [];
@@ -3928,6 +3976,8 @@ function attachBody(grid) {
3928
3976
  case "paste":
3929
3977
  navigator.clipboard?.readText?.().then((t) => grid.pasteText(t), () => void 0);
3930
3978
  return true;
3979
+ case "columnMenu":
3980
+ return grid.menu.openAtFocus();
3931
3981
  }
3932
3982
  if (!f) return false;
3933
3983
  if (!grid.editor.canEdit(f.rowIndex, f.colId)) return false;
@@ -4156,14 +4206,14 @@ function attachBody(grid) {
4156
4206
  const nested = (e) => e.target?.closest?.(".fg-root") !== root;
4157
4207
  const onCopy = (e) => {
4158
4208
  if (fromField(e) || nested(e)) return;
4159
- const text2 = grid.selectionTSV(false);
4209
+ const text2 = grid.copyText(false);
4160
4210
  if (!text2 || !e.clipboardData) return;
4161
4211
  e.clipboardData.setData("text/plain", text2);
4162
4212
  e.preventDefault();
4163
4213
  };
4164
4214
  const onCut = (e) => {
4165
4215
  if (fromField(e) || nested(e)) return;
4166
- const text2 = grid.selectionTSV(false);
4216
+ const text2 = grid.copyText(false);
4167
4217
  if (!text2 || !e.clipboardData) return;
4168
4218
  e.clipboardData.setData("text/plain", text2);
4169
4219
  e.preventDefault();
@@ -4530,7 +4580,9 @@ var ColumnChooserDialog = class {
4530
4580
  if (!eventInside(e, popup)) this.close();
4531
4581
  };
4532
4582
  const onKey = (e) => {
4533
- if (e.key === "Escape") this.close();
4583
+ if (e.key !== "Escape") return;
4584
+ this.close();
4585
+ this.grid.renderer.gridEl.focus();
4534
4586
  };
4535
4587
  document.addEventListener("pointerdown", onDown, true);
4536
4588
  document.addEventListener("keydown", onKey, true);
@@ -4669,12 +4721,13 @@ var ContextMenu = class {
4669
4721
  const node = grid.columns.getNode(colId);
4670
4722
  const def = node?.def;
4671
4723
  const dataCol = !!def && !isSystemCol(colId);
4724
+ const checked = grid.store.state.selected.size ? grid.shownSelectedRows() : [];
4672
4725
  const items = [
4673
4726
  { label: loc.copy, shortcut: shortcut("C"), action: () => grid.copyToClipboard(false) },
4674
4727
  { label: loc.copyWithHeaders, action: () => grid.copyToClipboard(true) },
4675
4728
  // after the copy items, so defaultItems indexes from earlier versions still point at the same entries
4676
4729
  { label: loc.cut, shortcut: shortcut("X"), action: () => grid.cutToClipboard() },
4677
- ...grid.store.state.selected.size ? [{ label: loc.copyRows, action: () => grid.copyToClipboard(false, true) }] : [],
4730
+ ...checked.length ? [{ label: loc.copyRows, action: () => grid.copyToClipboard(false, true) }] : [],
4678
4731
  { label: loc.paste, shortcut: shortcut("V"), action: () => this.pasteFromClipboard() }
4679
4732
  ];
4680
4733
  if (grid.history.canUndo() || grid.history.canRedo()) {
@@ -4684,8 +4737,8 @@ var ContextMenu = class {
4684
4737
  { label: loc.redo, shortcut: shortcut("Y"), action: () => grid.redo(), disabled: !grid.history.canRedo() }
4685
4738
  );
4686
4739
  }
4687
- if (grid.options.rowSelection && grid.store.state.selected.size) {
4688
- 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) });
4689
4742
  }
4690
4743
  if (dataCol) {
4691
4744
  items.push("separator", { label: loc.autosizeThis, action: () => grid.autoSize([colId]) });
@@ -4780,7 +4833,9 @@ var ContextMenu = class {
4780
4833
  popupParent(this.grid.renderer.root).append(popup);
4781
4834
  const w = popup.offsetWidth || 220, h = popup.offsetHeight || 300;
4782
4835
  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`;
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`;
4784
4839
  this.popup = popup;
4785
4840
  const onDown = (e) => {
4786
4841
  if (!eventInside(e, popup)) this.close();
@@ -5070,10 +5125,28 @@ var Finder = class {
5070
5125
  else this.next();
5071
5126
  }
5072
5127
  });
5073
- 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`;
5074
5133
  this.bar = bar;
5075
5134
  input.focus();
5076
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
+ }
5077
5150
  closeBar() {
5078
5151
  this.bar?.remove();
5079
5152
  this.bar = null;
@@ -5326,7 +5399,7 @@ var CellEditor = class {
5326
5399
  const col = this.grid.columns.getColumn(colId);
5327
5400
  if (!col?.def.field || rowIndex < 0 || rowIndex >= this.grid.rowModel.getRowCount()) return false;
5328
5401
  const node = this.grid.rowModel.getRow(rowIndex);
5329
- return !node.kind && cellEditable(col.def, node.data, rowIndex);
5402
+ return !node.kind && this.grid.cellEditable(col.def, node.data, rowIndex);
5330
5403
  }
5331
5404
  start(rowIndex, colId, initialText) {
5332
5405
  if (this.cur && !this.commit(null)) return false;
@@ -5552,6 +5625,7 @@ var CellEditor = class {
5552
5625
  i = cols.length - 1;
5553
5626
  if (!this.session) r--;
5554
5627
  }
5628
+ while (r !== rowIndex && r >= 0 && r < count && grid.rowModel.getRow(r)?.kind) r += step;
5555
5629
  if (r < 0 || r >= count) return;
5556
5630
  this.start(r, cols[i].id);
5557
5631
  }
@@ -6661,9 +6735,9 @@ function exportColumns(grid, ids) {
6661
6735
  return grid.columns.displayed().filter((c) => !isSystemCol(c.id)).map((c) => ({ id: c.id, def: c.def, node: c.node, width: c.width }));
6662
6736
  }
6663
6737
  function exportRows(grid, mode) {
6664
- if (mode === "selected") return grid.selectedRows();
6738
+ if (mode === "selected") return grid.shownSelectedRows();
6665
6739
  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);
6740
+ return grid.orderedDataRows().map((n) => n.data);
6667
6741
  }
6668
6742
  var header = (def, id) => def.header ?? def.field ?? id;
6669
6743
  var ALIGNS = ["left", "center", "right"];
@@ -7754,6 +7828,35 @@ function buildPivot(source, config, defOf, labels, locale) {
7754
7828
  });
7755
7829
  return { columns: [...rowColumns, ...valueColumns], rows, groupBy: cfg.rows.slice(0, -1) };
7756
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
+ }
7757
7860
 
7758
7861
  // src/features.ts
7759
7862
  var PAID_FEATURES = {
@@ -7779,7 +7882,7 @@ var PAID_FEATURES = {
7779
7882
  };
7780
7883
 
7781
7884
  // src/version.ts
7782
- var version = "1.9.5";
7885
+ var version = "1.10.0";
7783
7886
 
7784
7887
  // src/license.ts
7785
7888
  var PUBLIC_KEY = { kty: "EC", crv: "P-256", x: "sUTKkglZmk-HMKPNub8p7AeLBKesHCU8irIkw9rJlbE", y: "j0U_aKRDR7iApY9UpaD3hUemhBLZJwDv9-QiT_hUyw8" };
@@ -8078,6 +8181,12 @@ var ColumnMenu = class {
8078
8181
  if (grouped.length) g.push([loc.expandAll, () => this.grid.expandAll(true)], [loc.collapseAll, () => this.grid.expandAll(false)]);
8079
8182
  popup.append(this.section(g));
8080
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
+ }
8081
8190
  const items = [
8082
8191
  [loc.autosizeThis, () => this.grid.autoSize([colId])],
8083
8192
  [loc.autosizeAll, () => this.grid.autoSize()]
@@ -8105,14 +8214,30 @@ var ColumnMenu = class {
8105
8214
  this.close();
8106
8215
  this.grid.renderer.gridEl.focus();
8107
8216
  };
8217
+ const onScroll = () => this.close();
8218
+ const scroller = this.grid.renderer.scroller;
8108
8219
  document.addEventListener("pointerdown", onDown, true);
8109
8220
  document.addEventListener("keydown", onKey, true);
8221
+ scroller.addEventListener("scroll", onScroll, { passive: true });
8110
8222
  this.off.push(
8111
8223
  () => document.removeEventListener("pointerdown", onDown, true),
8112
- () => document.removeEventListener("keydown", onKey, true)
8224
+ () => document.removeEventListener("keydown", onKey, true),
8225
+ () => scroller.removeEventListener("scroll", onScroll)
8113
8226
  );
8114
8227
  popup.querySelector(".fg-filter-value:not([hidden]), .fg-set-search, .fg-menu-item")?.focus();
8115
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
+ }
8116
8241
  /** The filter UI of a column (used by the menu and the filters panel); `done` runs after apply/reset. */
8117
8242
  filterSection(colId, done) {
8118
8243
  const node = this.grid.columns.getNode(colId);
@@ -8137,6 +8262,8 @@ var ColumnMenu = class {
8137
8262
  b.addEventListener("click", () => {
8138
8263
  run2();
8139
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 });
8140
8267
  });
8141
8268
  s.append(b);
8142
8269
  }
@@ -8333,6 +8460,7 @@ var RowModel = class {
8333
8460
  this.sorted = [];
8334
8461
  this.display = [];
8335
8462
  this.indexById = null;
8463
+ this.orderedCache = null;
8336
8464
  this.layoutCache = null;
8337
8465
  /** Searchable text of each row, kept between quick filter keystrokes. */
8338
8466
  this.quickCache = null;
@@ -8391,6 +8519,13 @@ var RowModel = class {
8391
8519
  fullCount() {
8392
8520
  return this.full.length;
8393
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
+ }
8394
8529
  dataCount() {
8395
8530
  return this.sorted.length;
8396
8531
  }
@@ -8411,6 +8546,19 @@ var RowModel = class {
8411
8546
  dataRows() {
8412
8547
  return this.sorted;
8413
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
+ }
8414
8562
  allNodes() {
8415
8563
  return this.nodes;
8416
8564
  }
@@ -8813,6 +8961,7 @@ var ServerRowModel = class {
8813
8961
  /** Server-side grouping, while groupBy has columns. */
8814
8962
  this.groups = null;
8815
8963
  this.displayIn = null;
8964
+ this.quiet = false;
8816
8965
  }
8817
8966
  get bs() {
8818
8967
  return this.page ? this.page.size : this.deps.blockSize;
@@ -8911,9 +9060,19 @@ var ServerRowModel = class {
8911
9060
  const abs = this.base + index;
8912
9061
  const hit = this.peek(abs);
8913
9062
  if (hit) return hit;
8914
- 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));
8915
9064
  return this.placeholder(abs);
8916
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
+ }
8917
9076
  ensure(k) {
8918
9077
  const cur = this.blocks.get(k);
8919
9078
  if (cur && (cur.state !== "failed" || Date.now() < (cur.retryAt ?? 0))) return;
@@ -9335,17 +9494,23 @@ var FilterBuilder = class {
9335
9494
  actions.append(reset, ok);
9336
9495
  popup.append(head, join, list, addBtn, actions);
9337
9496
  popupParent(grid.renderer.root).append(popup);
9497
+ if (!rows.length) addBtn.click();
9338
9498
  const r = grid.renderer.root.getBoundingClientRect();
9339
9499
  const w = popup.offsetWidth || 460;
9340
9500
  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`;
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`;
9342
9506
  this.popup = popup;
9343
- if (!rows.length) addBtn.click();
9344
9507
  const onDown = (e) => {
9345
9508
  if (!eventInside(e, popup)) this.close();
9346
9509
  };
9347
9510
  const onKey = (e) => {
9348
- if (e.key === "Escape") this.close();
9511
+ if (e.key !== "Escape") return;
9512
+ this.close();
9513
+ grid.renderer.gridEl.focus();
9349
9514
  };
9350
9515
  document.addEventListener("pointerdown", onDown, true);
9351
9516
  document.addEventListener("keydown", onKey, true);
@@ -9483,8 +9648,8 @@ var FormatDialog = class {
9483
9648
  CF_PRESETS.forEach((p, k) => {
9484
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) });
9485
9650
  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;
9651
+ b.style.background = p.style.background ?? "#ffffff";
9652
+ b.style.color = p.style.color ?? "#1f2430";
9488
9653
  b.addEventListener("click", () => {
9489
9654
  preset = k;
9490
9655
  for (const s of swatches.children) s.setAttribute("aria-pressed", String(s === b));
@@ -9583,7 +9748,9 @@ var FormatDialog = class {
9583
9748
  if (!eventInside(e, popup)) this.close();
9584
9749
  };
9585
9750
  const onKey = (e) => {
9586
- if (e.key === "Escape") this.close();
9751
+ if (e.key !== "Escape") return;
9752
+ this.close();
9753
+ grid.renderer.gridEl.focus();
9587
9754
  };
9588
9755
  document.addEventListener("pointerdown", onDown, true);
9589
9756
  document.addEventListener("keydown", onKey, true);
@@ -10027,6 +10194,10 @@ var Grid = class {
10027
10194
  this.sourceDefs = [];
10028
10195
  this.pivotCfg = null;
10029
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;
10030
10201
  /** Grouping state from before pivot mode, restored when it ends. */
10031
10202
  this.savedGroup = null;
10032
10203
  /** 0-based page and rows per page (pagination only). */
@@ -10093,6 +10264,8 @@ var Grid = class {
10093
10264
  }
10094
10265
  };
10095
10266
  this.density = "normal";
10267
+ /** Set by the cellPadding option or setCellPadding; null: the theme / density value. */
10268
+ this.cellPadding = null;
10096
10269
  // ── merge, aggregates, fill, pinned rows ────────────────────
10097
10270
  /** `sortWithinMerges` is on. */
10098
10271
  this.sortWithin = false;
@@ -10173,6 +10346,7 @@ var Grid = class {
10173
10346
  this.renderer.applyTheme();
10174
10347
  if (options.flashDuration) this.renderer.root.style.setProperty("--fg-flash-duration", `${this.flashMs()}ms`);
10175
10348
  this.renderer.applyFont(this.font.size, this.font.family);
10349
+ if (options.cellPadding != null) this.applyCellPadding(options.cellPadding);
10176
10350
  this.menu = new ColumnMenu(this);
10177
10351
  this.chooser = new ColumnChooserDialog(this);
10178
10352
  this.contextMenu = new ContextMenu(this);
@@ -10208,6 +10382,7 @@ var Grid = class {
10208
10382
  this.disposers.push(() => ro.disconnect());
10209
10383
  }
10210
10384
  if (options.pivot) this.setPivot(options.pivot);
10385
+ if (options.rollup) this.setRollup(true);
10211
10386
  void needsFreeBadge(options.licenseKey).then((show) => {
10212
10387
  if (show && !this.destroyed) this.renderer.showFreeBadge();
10213
10388
  });
@@ -10286,6 +10461,7 @@ var Grid = class {
10286
10461
  }
10287
10462
  /** Column definitions or visibility changed; filters/quick filter may depend on them. */
10288
10463
  columnsChanged() {
10464
+ this.rebuildRollup();
10289
10465
  this.rowModel.invalidate();
10290
10466
  this.updateRows();
10291
10467
  this.refreshSummary();
@@ -10308,7 +10484,7 @@ var Grid = class {
10308
10484
  }
10309
10485
  updateRows() {
10310
10486
  const paged = !!this.options.pagination;
10311
- const data = this.pivotData ?? this.store.state.data;
10487
+ const data = this.viewRows();
10312
10488
  this.rowModel.update({ ...this.store.state, data, display: this.display(), page: paged ? { index: this.page, size: this.pageSize } : null });
10313
10489
  if (paged && this.page >= this.getPageCount()) {
10314
10490
  this.page = this.getPageCount() - 1;
@@ -10316,9 +10492,63 @@ var Grid = class {
10316
10492
  }
10317
10493
  }
10318
10494
  // ── pivot ───────────────────────────────────────────────────
10319
- /** 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. */
10320
10496
  viewRows() {
10321
- 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);
10322
10552
  }
10323
10553
  /** Leaf columns of the user's definitions (not the pivot columns). */
10324
10554
  sourceLeaves() {
@@ -10376,6 +10606,7 @@ var Grid = class {
10376
10606
  afterPivotSwitch() {
10377
10607
  this.page = 0;
10378
10608
  this.store.set({ sort: [], filters: {}, range: null, focus: null });
10609
+ this.rebuildRollup();
10379
10610
  this.rowModel.invalidate();
10380
10611
  this.updateRows();
10381
10612
  this.displayVersion++;
@@ -10484,12 +10715,31 @@ var Grid = class {
10484
10715
  } finally {
10485
10716
  this.keepRange = false;
10486
10717
  }
10718
+ this.clampRanges();
10487
10719
  }
10488
10720
  this.editor.sync();
10489
10721
  if (flashKeys.size && this.options.flashCells !== false) this.flash(flashKeys);
10490
10722
  this.events.emit("transactionApplied", result);
10491
10723
  return result;
10492
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
+ }
10493
10743
  /** One transaction against `data` (remove → update → add); returns the new array. */
10494
10744
  /** Whether rows changed in `fields` could filter, sort, group or move differently. */
10495
10745
  viewReads(fields) {
@@ -10644,14 +10894,24 @@ var Grid = class {
10644
10894
  }
10645
10895
  if (want("groupBy") && Array.isArray(state.groupBy)) this.setGroupBy(state.groupBy.filter(known));
10646
10896
  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 } });
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;
10652
10901
  }
10653
- if (want("advancedFilter")) this.setAdvancedFilter(state.advancedFilter ?? null);
10654
- 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 });
10655
10915
  if (want("expanded") && state.expanded && typeof state.expanded === "object") {
10656
10916
  const expanded = new Map(Object.entries(state.expanded).filter(([, v]) => typeof v === "boolean"));
10657
10917
  if (this.savedGroup) this.savedGroup.expanded = expanded;
@@ -10677,11 +10937,13 @@ var Grid = class {
10677
10937
  rows.forEach((r, k) => {
10678
10938
  nums[k] = r.kind ? 0 : ++n;
10679
10939
  });
10680
- 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 };
10681
10943
  }
10682
10944
  const v = this.rowNumberCache.nums[i];
10683
10945
  if (!v) return null;
10684
- return v + (this.options.pagination && !this.rowModel.server ? this.page * this.pageSize : 0);
10946
+ return v + this.rowNumberCache.offset;
10685
10947
  }
10686
10948
  getPage() {
10687
10949
  return this.options.pagination ? this.page : 0;
@@ -10733,6 +10995,7 @@ var Grid = class {
10733
10995
  /** Server rows: drop the cache and load again. */
10734
10996
  refreshRows() {
10735
10997
  if (this.rowModel instanceof ServerRowModel) {
10998
+ this.saveEditBeforeReload();
10736
10999
  this.rowModel.purge();
10737
11000
  this.renderer.invalidate({ rows: true });
10738
11001
  }
@@ -10742,6 +11005,12 @@ var Grid = class {
10742
11005
  this.renderer.invalidate({ header: true, rows: true });
10743
11006
  this.events.emit("rowsLoaded", total === null ? { start, end } : { start, end, total });
10744
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
+ }
10745
11014
  serverFailed(start, end, error) {
10746
11015
  if (this.destroyed) return;
10747
11016
  warn(`datasource.getRows failed for rows ${start}\u2013${end}`);
@@ -10890,7 +11159,7 @@ var Grid = class {
10890
11159
  let m = this.aggCache.map.get(node.group);
10891
11160
  if (!m) this.aggCache.map.set(node.group, m = /* @__PURE__ */ new Map());
10892
11161
  if (!m.has(colId)) {
10893
- const rows = node.group.rows;
11162
+ const rows = this.sourceRowsOf(node.group.rows);
10894
11163
  try {
10895
11164
  m.set(colId, aggregate(def.aggFunc, rows.map((r) => getCellValue(def, r)), rows));
10896
11165
  } catch {
@@ -11083,10 +11352,33 @@ var Grid = class {
11083
11352
  this.renderer.invalidateHeights();
11084
11353
  this.layoutChanged();
11085
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
+ }
11086
11377
  /** Copy the grid font variables onto an element rendered outside the root (popups, drag ghost). */
11087
11378
  applyFontTo(target) {
11088
11379
  if (this.font.size) target.style.setProperty("--fg-font-size", `${this.font.size}px`);
11089
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`);
11090
11382
  }
11091
11383
  /** Current summary configuration and values, or null when there are no summary rows. */
11092
11384
  summary() {
@@ -11115,7 +11407,7 @@ var Grid = class {
11115
11407
  }
11116
11408
  const scope = opts.scope ?? "filtered";
11117
11409
  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);
11410
+ this.summaryValues = computeSummary(opts.rows, this.sourceRowsOf(rows), (id) => this.columns.getNode(id)?.def);
11119
11411
  }
11120
11412
  // ── cell ranges ─────────────────────────────────────────────
11121
11413
  dataColumns() {
@@ -11208,12 +11500,11 @@ var Grid = class {
11208
11500
  out = [];
11209
11501
  for (let i = b.r1; i <= b.r2; i++) {
11210
11502
  const node = this.rowModel.getRow(i);
11211
- if (!node.kind) out.push(node.data);
11503
+ if (node && !node.kind) out.push(node.data);
11212
11504
  }
11213
11505
  cols = displayed.slice(b.c1, b.c2 + 1).filter((c) => !isSystemCol(c.id));
11214
11506
  } 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);
11507
+ out = this.shownSelectedRows();
11217
11508
  cols = this.dataColumns();
11218
11509
  } else {
11219
11510
  const f = this.store.state.focus;
@@ -11268,8 +11559,17 @@ var Grid = class {
11268
11559
  if (text2) this.clearCells(true);
11269
11560
  return text2;
11270
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
+ }
11271
11571
  copyToClipboard(headers, rows = false) {
11272
- const text2 = this.selectionTSV(headers, rows);
11572
+ const text2 = this.copyText(headers, rows);
11273
11573
  if (text2 && typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
11274
11574
  try {
11275
11575
  navigator.clipboard.writeText(text2)?.catch?.(() => void 0);
@@ -11278,6 +11578,14 @@ var Grid = class {
11278
11578
  }
11279
11579
  return text2;
11280
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
+ }
11281
11589
  pasteText(text2) {
11282
11590
  const matrix = parseTSV(text2);
11283
11591
  const result = { changed: 0, skipped: 0 };
@@ -11300,19 +11608,21 @@ var Grid = class {
11300
11608
  const single = matrix.length === 1 && matrix[0].length === 1;
11301
11609
  const rows = single && b ? b.r2 - b.r1 + 1 : matrix.length;
11302
11610
  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;
11611
+ for (let ri = r0, dr = 0; ri < count && dr < rows; ri++) {
11306
11612
  const node = this.rowModel.getRow(ri);
11307
- if (node.kind) continue;
11613
+ if (node.kind) {
11614
+ if (single && b) dr++;
11615
+ continue;
11616
+ }
11617
+ const line2 = dr++;
11308
11618
  for (let dc = 0; dc < width; dc++) {
11309
11619
  const col = displayed[c0 + dc];
11310
11620
  if (!col) break;
11311
11621
  if (isSystemCol(col.id)) continue;
11312
- const raw = single && b ? matrix[0][0] : matrix[dr]?.[dc];
11622
+ const raw = single && b ? matrix[0][0] : matrix[line2]?.[dc];
11313
11623
  if (raw === void 0) continue;
11314
11624
  const def = col.def;
11315
- if (!def.field || !cellEditable(def, node.data, ri)) {
11625
+ if (!def.field || !this.cellEditable(def, node.data, ri)) {
11316
11626
  result.skipped++;
11317
11627
  continue;
11318
11628
  }
@@ -11380,7 +11690,7 @@ var Grid = class {
11380
11690
  const id = this.rowModel.idOf(row);
11381
11691
  const rowIndex = this.rowModel.indexOfId(id);
11382
11692
  for (const c of cols) {
11383
- if (!cellEditable(c.def, row, rowIndex < 0 ? i : rowIndex)) continue;
11693
+ if (!this.cellEditable(c.def, row, rowIndex < 0 ? i : rowIndex)) continue;
11384
11694
  const message = this.cellError(c.def, row, row[c.def.field]);
11385
11695
  if (message !== null) out.push({ row, rowIndex, colId: c.id, message });
11386
11696
  }
@@ -11645,26 +11955,36 @@ var Grid = class {
11645
11955
  const col = displayed[c];
11646
11956
  const node = this.rowModel.getRow(r);
11647
11957
  const def = col?.def;
11648
- 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;
11649
11959
  if (this.cellError(def, node.data, value2) !== null) return;
11650
11960
  this.writeCell(changes, node.data, def.field, col.id, r, value2);
11651
11961
  };
11652
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);
11653
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);
11654
11976
  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));
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));
11660
11980
  }
11661
11981
  } 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));
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));
11668
11988
  }
11669
11989
  } else return;
11670
11990
  this.endCellTx(changes);
@@ -11733,7 +12053,7 @@ var Grid = class {
11733
12053
  const node = this.rowModel.getRow(r);
11734
12054
  for (let c = box.c1; c <= box.c2; c++) {
11735
12055
  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);
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);
11737
12057
  }
11738
12058
  }
11739
12059
  this.endCellTx(changes);
@@ -11887,8 +12207,9 @@ var Grid = class {
11887
12207
  const cols = (ids ? ids.map((id) => this.columns.getColumn(id)) : this.columns.displayed()).filter((c) => !!c && isResizable(c.def));
11888
12208
  const { first, last } = this.renderer.renderedRange();
11889
12209
  const code = this.store.state.locale.code;
11890
- const HEADER_EXTRA = 24 + 22 + 6 + 14;
11891
- const CELL_EXTRA = 24;
12210
+ const pad4 = this.getCellPadding() * 2;
12211
+ const HEADER_EXTRA = pad4 + 22 + 6 + 14;
12212
+ const CELL_EXTRA = pad4;
11892
12213
  for (const col of cols) {
11893
12214
  let w = this.renderer.measureText(col.def.header ?? col.def.field ?? "", true) + HEADER_EXTRA;
11894
12215
  const fmt = getFormatter(col.def, code);
@@ -11906,6 +12227,13 @@ var Grid = class {
11906
12227
  this.columnsChanged();
11907
12228
  }
11908
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) {
11909
12237
  const valid = sort.filter((s) => {
11910
12238
  const node = this.columns.getNode(s.colId);
11911
12239
  if (!node || node.children) {
@@ -11914,9 +12242,7 @@ var Grid = class {
11914
12242
  }
11915
12243
  return s.dir === "asc" || s.dir === "desc";
11916
12244
  });
11917
- if (this.veto("beforeSort", { sort: valid.map((s) => ({ ...s })) })) return;
11918
- this.store.set({ sort: valid });
11919
- this.events.emit("sortChange", { sort: [...valid] });
12245
+ return this.veto("beforeSort", { sort: valid.map((s) => ({ ...s })) }) ? null : valid;
11920
12246
  }
11921
12247
  toggleSort(colId, additive) {
11922
12248
  const cur = this.store.state.sort;
@@ -12086,6 +12412,15 @@ var Grid = class {
12086
12412
  const sel = this.store.state.selected;
12087
12413
  return sel.size ? this.rowModel.allNodes().filter((n) => sel.has(n.id)).map((n) => n.data) : [];
12088
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
+ }
12089
12424
  withSelect(defs) {
12090
12425
  const sys = [];
12091
12426
  const rn = this.options.rowNumbers;
@@ -12157,12 +12492,26 @@ var Grid = class {
12157
12492
  });
12158
12493
  this.columnsChanged();
12159
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
+ }
12160
12507
  onChange(keys) {
12161
12508
  const has = (...k) => k.some((x) => keys.includes(x));
12509
+ if (has("sort", "filters", "quickFilter", "advancedFilter")) this.saveEditBeforeReload();
12162
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();
12163
12513
  if (has("locale")) {
12164
- this.menu.close();
12165
- this.chooser.close();
12514
+ this.finder.relabel();
12166
12515
  this.sideBar?.refresh();
12167
12516
  this.rowModel.invalidate();
12168
12517
  }
@@ -12267,6 +12616,11 @@ var Grid = class {
12267
12616
  setTheme: guard((t) => this.store.set({ theme: t })),
12268
12617
  setLocale: guard((l) => this.store.set({ locale: resolveLocale(l) })),
12269
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)),
12270
12624
  setColumnVisible: guard((id, v) => this.setColumnsVisible([id], v)),
12271
12625
  autoSizeColumns: guard((ids) => this.autoSize(ids)),
12272
12626
  resetColumnState: guard(() => this.resetColumns()),