@freegridjs/core 1.10.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -654,6 +654,39 @@ var ColumnModel = class {
654
654
  }
655
655
  };
656
656
 
657
+ // src/keyboard.ts
658
+ var imeKey = (e) => e.isComposing || e.keyCode === 229;
659
+ var NAV_KEYS = /* @__PURE__ */ new Set(["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Tab", "Home", "End", "PageUp", "PageDown"]);
660
+ function nextFocus(key, mods, p, s) {
661
+ const r = (i) => Math.max(0, Math.min(s.rows - 1, i));
662
+ const c = (i) => Math.max(0, Math.min(s.cols - 1, i));
663
+ switch (key) {
664
+ case "ArrowUp":
665
+ return { rowIndex: r(p.rowIndex - 1), colIndex: p.colIndex };
666
+ case "ArrowDown":
667
+ return { rowIndex: r(p.rowIndex + 1), colIndex: p.colIndex };
668
+ case "ArrowLeft":
669
+ return { rowIndex: p.rowIndex, colIndex: c(p.colIndex - 1) };
670
+ case "ArrowRight":
671
+ return { rowIndex: p.rowIndex, colIndex: c(p.colIndex + 1) };
672
+ case "PageUp":
673
+ return { rowIndex: r(p.rowIndex - s.pageRows), colIndex: p.colIndex };
674
+ case "PageDown":
675
+ return { rowIndex: r(p.rowIndex + s.pageRows), colIndex: p.colIndex };
676
+ case "Home":
677
+ return { rowIndex: mods.ctrl ? 0 : p.rowIndex, colIndex: 0 };
678
+ case "End":
679
+ return { rowIndex: mods.ctrl ? s.rows - 1 : p.rowIndex, colIndex: s.cols - 1 };
680
+ case "Tab": {
681
+ const flat2 = p.rowIndex * s.cols + p.colIndex + (mods.shift ? -1 : 1);
682
+ if (flat2 < 0 || flat2 >= s.rows * s.cols) return null;
683
+ return { rowIndex: Math.floor(flat2 / s.cols), colIndex: flat2 % s.cols };
684
+ }
685
+ default:
686
+ return null;
687
+ }
688
+ }
689
+
657
690
  // src/formula.ts
658
691
  var FormulaError = class extends Error {
659
692
  constructor(code) {
@@ -1336,25 +1369,6 @@ function columnFormulaValue(def, row) {
1336
1369
  }
1337
1370
  var formulasOff = (def) => !!scopes.get(def)?.off?.();
1338
1371
  var isFormula = (v) => typeof v === "string" && v.length > 1 && v.startsWith("=");
1339
- function formulaRefs(formula) {
1340
- let n;
1341
- try {
1342
- n = compile(formula);
1343
- } catch {
1344
- return null;
1345
- }
1346
- const out = /* @__PURE__ */ new Set();
1347
- const walk = (x) => {
1348
- if (x.k === "ref") out.add(x.name);
1349
- else if (x.k === "un") walk(x.a);
1350
- else if (x.k === "bin") {
1351
- walk(x.a);
1352
- walk(x.b);
1353
- } else if (x.k === "call") x.args.forEach(walk);
1354
- };
1355
- walk(n);
1356
- return [...out];
1357
- }
1358
1372
 
1359
1373
  // src/rows/values.ts
1360
1374
  function getCellValue(def, row) {
@@ -1563,6 +1577,7 @@ var FilterRow = class {
1563
1577
  });
1564
1578
  input.addEventListener("keydown", (e) => {
1565
1579
  e.stopPropagation();
1580
+ if (imeKey(e)) return;
1566
1581
  if (e.key === "Enter") {
1567
1582
  clearTimeout(this.timers.get(c.id));
1568
1583
  apply();
@@ -1852,8 +1867,10 @@ function extreme(values, pickMax) {
1852
1867
  function aggregate(agg, values, rows) {
1853
1868
  if (typeof agg === "function") return agg(values, rows);
1854
1869
  switch (agg) {
1855
- case "sum":
1856
- return numbers(values).reduce((s, n) => s + n, 0);
1870
+ case "sum": {
1871
+ const nums = numbers(values);
1872
+ return nums.length || !values.length ? nums.reduce((s, n) => s + n, 0) : null;
1873
+ }
1857
1874
  case "avg": {
1858
1875
  const nums = numbers(values);
1859
1876
  return nums.length ? nums.reduce((s, n) => s + n, 0) / nums.length : null;
@@ -2238,7 +2255,8 @@ var Renderer = class {
2238
2255
  }
2239
2256
  if (options.autoHeight && !this.useCards()) {
2240
2257
  this.root.classList.add("fg-auto-height");
2241
- this.bodyEl.style.height = `${this.rowsHeight(count) + this.topOffset + bottomExtra + Math.max(0, this.scroller.offsetHeight - this.scroller.clientHeight)}px`;
2258
+ const rowsH = count > 0 ? this.rowsHeight(count) : Math.max(this.rowsHeight(count), 3 * rh);
2259
+ this.bodyEl.style.height = `${rowsH + this.topOffset + bottomExtra + Math.max(0, this.scroller.offsetHeight - this.scroller.clientHeight)}px`;
2242
2260
  }
2243
2261
  const sig = `${headerHeight}|${REGIONS3.map((r) => columns.regionWidth(r)).join(",")}|${this.rowsHeight(count) + this.topOffset + bottomExtra}|${summary && sumPinned ? sumPos : ""}|${sumRows}|${options.autoHeight ? 1 : 0}`;
2244
2262
  if (sig !== this.layoutSig) {
@@ -3682,7 +3700,6 @@ var en = {
3682
3700
  freeBadge: "Free version",
3683
3701
  exportPdf: "Export PDF",
3684
3702
  exportJson: "Export JSON",
3685
- ai: { title: "AI assistant", placeholder: "Ask about the data or tell the grid what to do", send: "Send", thinking: "Working\u2026", undo: "Undo", undone: "Undone", failed: "The assistant did not answer", skipped: "Skipped", done: "Done" },
3686
3703
  cf: {
3687
3704
  title: "Conditional formatting",
3688
3705
  add: "Add rule",
@@ -3779,38 +3796,6 @@ function eventInside(e, ...els) {
3779
3796
  return els.some((el2) => !!el2 && el2.contains(e.target));
3780
3797
  }
3781
3798
 
3782
- // src/keyboard.ts
3783
- var NAV_KEYS = /* @__PURE__ */ new Set(["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Tab", "Home", "End", "PageUp", "PageDown"]);
3784
- function nextFocus(key, mods, p, s) {
3785
- const r = (i) => Math.max(0, Math.min(s.rows - 1, i));
3786
- const c = (i) => Math.max(0, Math.min(s.cols - 1, i));
3787
- switch (key) {
3788
- case "ArrowUp":
3789
- return { rowIndex: r(p.rowIndex - 1), colIndex: p.colIndex };
3790
- case "ArrowDown":
3791
- return { rowIndex: r(p.rowIndex + 1), colIndex: p.colIndex };
3792
- case "ArrowLeft":
3793
- return { rowIndex: p.rowIndex, colIndex: c(p.colIndex - 1) };
3794
- case "ArrowRight":
3795
- return { rowIndex: p.rowIndex, colIndex: c(p.colIndex + 1) };
3796
- case "PageUp":
3797
- return { rowIndex: r(p.rowIndex - s.pageRows), colIndex: p.colIndex };
3798
- case "PageDown":
3799
- return { rowIndex: r(p.rowIndex + s.pageRows), colIndex: p.colIndex };
3800
- case "Home":
3801
- return { rowIndex: mods.ctrl ? 0 : p.rowIndex, colIndex: 0 };
3802
- case "End":
3803
- return { rowIndex: mods.ctrl ? s.rows - 1 : p.rowIndex, colIndex: s.cols - 1 };
3804
- case "Tab": {
3805
- const flat2 = p.rowIndex * s.cols + p.colIndex + (mods.shift ? -1 : 1);
3806
- if (flat2 < 0 || flat2 >= s.rows * s.cols) return null;
3807
- return { rowIndex: Math.floor(flat2 / s.cols), colIndex: flat2 % s.cols };
3808
- }
3809
- default:
3810
- return null;
3811
- }
3812
- }
3813
-
3814
3799
  // src/registry.ts
3815
3800
  var liveGrids = /* @__PURE__ */ new Map();
3816
3801
  var apiGrids = /* @__PURE__ */ new WeakMap();
@@ -3993,6 +3978,13 @@ function attachBody(grid) {
3993
3978
  }
3994
3979
  return false;
3995
3980
  };
3981
+ const onImeKey = (e) => {
3982
+ const f = grid.store.state.focus;
3983
+ if (e.isComposing || e.key !== "Process" || e.ctrlKey || e.metaKey || e.altKey) return;
3984
+ if (!f || !grid.editor.canEdit(f.rowIndex, f.colId)) return;
3985
+ if (grid.columns.getColumn(f.colId).def.type === "boolean") return;
3986
+ grid.editor.start(f.rowIndex, f.colId, "");
3987
+ };
3996
3988
  const onExpandKey = (e) => {
3997
3989
  const f = grid.store.state.focus;
3998
3990
  if (!f || e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) return false;
@@ -4142,6 +4134,10 @@ function attachBody(grid) {
4142
4134
  const cols = grid.columns.displayed();
4143
4135
  const count = grid.rowModel.getRowCount();
4144
4136
  if (!cols.length || !count) return;
4137
+ if (imeKey(e)) {
4138
+ onImeKey(e);
4139
+ return;
4140
+ }
4145
4141
  if (onExpandKey(e)) {
4146
4142
  e.preventDefault();
4147
4143
  return;
@@ -4766,7 +4762,6 @@ var ContextMenu = class {
4766
4762
  { label: loc.exportCsv, action: () => grid.api.exportCsv() },
4767
4763
  ...grid.paid ? [{ label: loc.exportExcel, action: () => grid.api.exportXlsx() }] : [],
4768
4764
  { label: loc.exportJson, action: () => grid.api.exportJson() },
4769
- ...grid.options.assistant?.url && grid.paid ? [{ label: `${loc.ai.title}\u2026`, action: () => grid.api.openAssistant() }] : [],
4770
4765
  ...grid.paid ? [{ label: loc.exportPdf, action: () => grid.api.exportPdf() }] : []
4771
4766
  );
4772
4767
  if (grid.options.advancedFilter !== false && !grid.options.pivot && grid.paid) items.push("separator", { label: `${loc.advancedFilter}\u2026`, action: () => grid.filterBuilder.open() });
@@ -5116,6 +5111,7 @@ var Finder = class {
5116
5111
  input.addEventListener("input", () => this.find(input.value));
5117
5112
  bar.addEventListener("keydown", (e) => {
5118
5113
  e.stopPropagation();
5114
+ if (imeKey(e)) return;
5119
5115
  if (e.key === "Escape") {
5120
5116
  e.preventDefault();
5121
5117
  this.closeBar();
@@ -5472,6 +5468,7 @@ var CellEditor = class {
5472
5468
  wrap2.append(msg);
5473
5469
  const onKey = (e) => {
5474
5470
  e.stopPropagation();
5471
+ if (imeKey(e)) return;
5475
5472
  if (e.key === "Escape") {
5476
5473
  e.preventDefault();
5477
5474
  this.cancel();
@@ -7192,8 +7189,10 @@ function buildXlsxSheet(grid, opts = {}) {
7192
7189
  return d ? { v: d, fmt } : String(v);
7193
7190
  }
7194
7191
  case "boolean":
7192
+ if (typeof v === "number" && Number.isFinite(v)) return v;
7195
7193
  return typeof v === "boolean" ? v : String(v);
7196
7194
  default:
7195
+ if (typeof v === "number" && Number.isFinite(v)) return v;
7197
7196
  return formatters[i](v);
7198
7197
  }
7199
7198
  };
@@ -7832,9 +7831,12 @@ var ROLLUP = /* @__PURE__ */ new WeakMap();
7832
7831
  function rollupSource(row) {
7833
7832
  return ROLLUP.get(row);
7834
7833
  }
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);
7834
+ function buildRollup(source, cols, by) {
7835
+ const isKey = (c) => by ? by.includes(c.id) : !c.def.aggFunc;
7836
+ const keys = cols.filter(isKey).map((c) => c.def);
7837
+ const rest = cols.filter((c) => !isKey(c) && c.def.field && !c.def.valueGetter).map((c) => c.def);
7838
+ const values = rest.filter((d) => d.aggFunc);
7839
+ const others = rest.filter((d) => !d.aggFunc);
7838
7840
  const buckets = /* @__PURE__ */ new Map();
7839
7841
  for (const r of source) {
7840
7842
  const k = keys.map((d) => setKey(getCellValue(d, r))).join("");
@@ -7852,6 +7854,12 @@ function buildRollup(source, defs) {
7852
7854
  row[d.field] = null;
7853
7855
  }
7854
7856
  }
7857
+ if (src.length > 1) {
7858
+ for (const d of others) {
7859
+ const first = setKey(getCellValue(d, src[0]));
7860
+ if (src.some((r) => setKey(getCellValue(d, r)) !== first)) row[d.field] = null;
7861
+ }
7862
+ }
7855
7863
  ROLLUP.set(row, src);
7856
7864
  rows.push(row);
7857
7865
  }
@@ -7876,13 +7884,12 @@ var PAID_FEATURES = {
7876
7884
  formulas: { en: "Formulas", ko: "\uC218\uC2DD" },
7877
7885
  find: { en: "Find", ko: "\uCC3E\uAE30" },
7878
7886
  conditional: { en: "Conditional formatting", ko: "\uC870\uAC74\uBD80 \uC11C\uC2DD" },
7879
- assistant: { en: "AI assistant", ko: "AI \uC5B4\uC2DC\uC2A4\uD134\uD2B8" },
7880
7887
  merge: { en: "Merged cells", ko: "\uC140 \uBCD1\uD569" },
7881
7888
  workbook: { en: "Workbook", ko: "\uC6CC\uD06C\uBD81" }
7882
7889
  };
7883
7890
 
7884
7891
  // src/version.ts
7885
- var version = "1.10.0";
7892
+ var version = "1.11.0";
7886
7893
 
7887
7894
  // src/license.ts
7888
7895
  var PUBLIC_KEY = { kty: "EC", crv: "P-256", x: "sUTKkglZmk-HMKPNub8p7AeLBKesHCU8irIkw9rJlbE", y: "j0U_aKRDR7iApY9UpaD3hUemhBLZJwDv9-QiT_hUyw8" };
@@ -8181,7 +8188,7 @@ var ColumnMenu = class {
8181
8188
  if (grouped.length) g.push([loc.expandAll, () => this.grid.expandAll(true)], [loc.collapseAll, () => this.grid.expandAll(false)]);
8182
8189
  popup.append(this.section(g));
8183
8190
  }
8184
- if (this.grid.getRollup()) {
8191
+ if (this.grid.getRollup() && !this.grid.isRollupKey(colId)) {
8185
8192
  const cur = def.aggFunc ?? null;
8186
8193
  const mark = (on, label) => `${on ? "\u2713 " : ""}${label}`;
8187
8194
  const aggs = ["sum", "avg", "min", "max", "count"].map((a) => [mark(cur === a, loc[a]), () => this.grid.setColumnAggFunc(colId, a)]);
@@ -8318,7 +8325,7 @@ var ColumnMenu = class {
8318
8325
  };
8319
8326
  select.addEventListener("change", sync);
8320
8327
  sync();
8321
- for (const input of [v1, v2]) input.addEventListener("keydown", (e) => e.key === "Enter" && apply());
8328
+ for (const input of [v1, v2]) input.addEventListener("keydown", (e) => e.key === "Enter" && !imeKey(e) && apply());
8322
8329
  row.append(select, v1, v2);
8323
8330
  const read = () => {
8324
8331
  const op = select.value;
@@ -8430,7 +8437,7 @@ var ColumnMenu = class {
8430
8437
  grid.setFilter(colId, checked.size === distinct.size ? null : { type: "set", values: entries.filter(([k]) => checked.has(k)).map(([, v]) => v) });
8431
8438
  done();
8432
8439
  };
8433
- search.addEventListener("keydown", (e) => e.key === "Enter" && apply());
8440
+ search.addEventListener("keydown", (e) => e.key === "Enter" && !imeKey(e) && apply());
8434
8441
  draw();
8435
8442
  box.append(this.title(loc), search, allLabel, list, this.actions(colId, loc, apply, done));
8436
8443
  return box;
@@ -9767,394 +9774,6 @@ var FormatDialog = class {
9767
9774
  }
9768
9775
  };
9769
9776
 
9770
- // src/assistant.ts
9771
- var FILTER_TYPES = ["text", "number", "date", "set", "combined"];
9772
- var CHART_TYPES = ["bar", "stackedBar", "line", "area", "pie", "donut", "combo", "scatter"];
9773
- var MAX_ACTIONS = 30;
9774
- function describeColumn(grid, id, def, rows) {
9775
- const col = { id, header: def.header ?? def.field ?? id };
9776
- if (def.type) col.type = def.type;
9777
- if (def.formula) col.formula = def.formula;
9778
- if (def.aggFunc && typeof def.aggFunc === "string") col.aggFunc = def.aggFunc;
9779
- let blanks = 0;
9780
- const counts = /* @__PURE__ */ new Map();
9781
- let min = Infinity, max = -Infinity, sum = 0, nums = 0;
9782
- let minText = "", maxText = "";
9783
- for (const r of rows) {
9784
- const v = getCellValue(def, r);
9785
- if (isBlank(v)) {
9786
- blanks++;
9787
- continue;
9788
- }
9789
- if (typeof v === "number" && Number.isFinite(v)) {
9790
- nums++;
9791
- sum += v;
9792
- if (v < min) min = v;
9793
- if (v > max) max = v;
9794
- } else if (def.type === "date") {
9795
- const t = v instanceof Date ? v.toISOString().slice(0, 10) : String(v);
9796
- if (!minText || t < minText) minText = t;
9797
- if (!maxText || t > maxText) maxText = t;
9798
- }
9799
- if (counts.size < 5e3) {
9800
- const k = setKey(v);
9801
- const c = counts.get(k);
9802
- if (c) c.n++;
9803
- else counts.set(k, { value: v, n: 1 });
9804
- }
9805
- }
9806
- col.blanks = blanks;
9807
- col.distinct = counts.size;
9808
- if (nums) Object.assign(col, { min, max, avg: Math.round(sum / nums * 1e3) / 1e3 });
9809
- if (minText) Object.assign(col, { min: minText, max: maxText });
9810
- if (!nums && counts.size) {
9811
- col.top = [...counts.values()].sort((a, b) => b.n - a.n).slice(0, 12).map((c) => ({ value: String(c.value).slice(0, 80), count: c.n }));
9812
- }
9813
- return col;
9814
- }
9815
- function assistantRequest(grid, prompt, options) {
9816
- const rows = grid.rowModel.dataRows().map((n) => n.data);
9817
- const all2 = grid.columns.getState().map((s2) => grid.columns.getNode(s2.colId)).filter((n) => n && !n.children && !isSystemCol(n.id));
9818
- const columns = all2.map((n) => ({ ...describeColumn(grid, n.id, n.def, rows), hidden: grid.columns.getState().find((s2) => s2.colId === n.id)?.hide || void 0 }));
9819
- const sampleN = typeof options.sampleRows === "number" && options.sampleRows >= 0 ? Math.min(50, options.sampleRows) : 5;
9820
- const sample = rows.slice(0, sampleN).map((r) => Object.fromEntries(all2.map((n) => [n.id, getCellValue(n.def, r)])));
9821
- const s = grid.store.state;
9822
- return {
9823
- prompt: String(prompt).slice(0, 2e3),
9824
- locale: s.locale.code,
9825
- rowCount: rows.length,
9826
- totalRows: grid.rowModel.server ? rows.length : grid.store.state.data.length,
9827
- columns,
9828
- sample,
9829
- state: {
9830
- sort: s.sort,
9831
- filters: s.filters,
9832
- quickFilter: s.quickFilter,
9833
- advancedFilter: s.advancedFilter,
9834
- groupBy: grid.getGroupBy(),
9835
- pivot: grid.getPivot(),
9836
- conditionalFormats: grid.getConditionalFormats()
9837
- },
9838
- ...options.context ? { context: options.context() } : {}
9839
- };
9840
- }
9841
- function applyAssistantActions(grid, actions) {
9842
- const before = { state: grid.api.getState(), defs: grid.columnDefs(), summary: grid.summaryOptions() };
9843
- const applied = [];
9844
- const skipped = [];
9845
- const charts = [];
9846
- const list = Array.isArray(actions) ? actions.slice(0, MAX_ACTIONS) : [];
9847
- const known = (id) => typeof id === "string" && !!grid.columns.getNode(id) && !grid.columns.getNode(id).children && !isSystemCol(id);
9848
- const ok = (a) => applied.push(a);
9849
- const skip = (a, reason) => skipped.push({ action: a, reason });
9850
- for (const raw of list) {
9851
- const a = raw;
9852
- if (!a || typeof a !== "object" || typeof a.type !== "string") {
9853
- skip(raw, "not an action");
9854
- continue;
9855
- }
9856
- try {
9857
- switch (a.type) {
9858
- case "sort": {
9859
- const sort = (Array.isArray(a.sort) ? a.sort : []).filter((x) => !!x && known(x.colId) && (x.dir === "asc" || x.dir === "desc"));
9860
- if (Array.isArray(a.sort) && sort.length !== a.sort.length) skip(a, "unknown column in sort");
9861
- grid.setSort(sort.map((x) => ({ colId: x.colId, dir: x.dir })));
9862
- ok(a);
9863
- break;
9864
- }
9865
- case "filter": {
9866
- if (!known(a.colId)) {
9867
- skip(a, "unknown column");
9868
- break;
9869
- }
9870
- const m = a.model;
9871
- if (m !== null && (!m || typeof m !== "object" || !FILTER_TYPES.includes(m.type))) {
9872
- skip(a, "bad filter model");
9873
- break;
9874
- }
9875
- grid.setFilter(a.colId, m);
9876
- ok(a);
9877
- break;
9878
- }
9879
- case "clearFilters":
9880
- for (const id of Object.keys(grid.store.state.filters)) grid.setFilter(id, null);
9881
- grid.setAdvancedFilter(null);
9882
- grid.api.setQuickFilter("");
9883
- ok(a);
9884
- break;
9885
- case "quickFilter":
9886
- grid.api.setQuickFilter(typeof a.text === "string" ? a.text.slice(0, 200) : "");
9887
- ok(a);
9888
- break;
9889
- case "advancedFilter": {
9890
- const f = a.filter;
9891
- const ids = [];
9892
- const walk = (x) => {
9893
- const o = x;
9894
- if (Array.isArray(o?.conditions)) o.conditions.forEach(walk);
9895
- else ids.push(o?.colId);
9896
- };
9897
- if (f) walk(f);
9898
- if (f && (!Array.isArray(f.conditions) || !ids.every(known))) {
9899
- skip(a, "unknown column in advanced filter");
9900
- break;
9901
- }
9902
- grid.setAdvancedFilter(f ?? null);
9903
- ok(a);
9904
- break;
9905
- }
9906
- case "groupBy": {
9907
- const ids = Array.isArray(a.colIds) ? a.colIds.filter(known) : [];
9908
- grid.setGroupBy(ids);
9909
- if (a.expand) grid.expandAll(true);
9910
- ok(a);
9911
- break;
9912
- }
9913
- case "pivot": {
9914
- const p = a.pivot;
9915
- if (p && (!Array.isArray(p.rows) || !Array.isArray(p.values) || !p.rows.every(known) || !(p.columns ?? []).every(known) || !p.values.every((v) => v && known(v.colId)))) {
9916
- skip(a, "unknown column in pivot");
9917
- break;
9918
- }
9919
- grid.setPivot(p ?? null);
9920
- ok(a);
9921
- break;
9922
- }
9923
- case "showColumns":
9924
- case "hideColumns": {
9925
- const ids = Array.isArray(a.colIds) ? a.colIds.filter(known) : [];
9926
- if (ids.length) grid.setColumnsVisible(ids, a.type === "showColumns");
9927
- ok(a);
9928
- break;
9929
- }
9930
- case "conditionalFormat": {
9931
- const f = a.format;
9932
- if (!f || !Array.isArray(f.columns) || !f.columns.length || !f.columns.every(known)) {
9933
- skip(a, "unknown column in format");
9934
- break;
9935
- }
9936
- grid.setConditionalFormats([...grid.getConditionalFormats(), f]);
9937
- ok(a);
9938
- break;
9939
- }
9940
- case "clearConditionalFormats":
9941
- grid.setConditionalFormats([]);
9942
- ok(a);
9943
- break;
9944
- case "addColumn": {
9945
- const c = a.column;
9946
- if (!c || typeof c.id !== "string" || !/^[\w.-]{1,40}$/.test(c.id) || grid.columns.getNode(c.id)) {
9947
- skip(a, "bad or taken column id");
9948
- break;
9949
- }
9950
- const f = typeof c.formula === "string" ? c.formula.startsWith("=") ? c.formula : `=${c.formula}` : "";
9951
- const refs = f ? formulaRefs(f) : null;
9952
- if (!refs) {
9953
- skip(a, "formula does not parse");
9954
- break;
9955
- }
9956
- const def = { id: c.id, header: typeof c.header === "string" ? c.header.slice(0, 80) : c.id, formula: f };
9957
- if (c.type === "number" || c.type === "date" || c.type === "text") def.type = c.type;
9958
- grid.api.setColumns([...grid.columnDefs(), def]);
9959
- ok(a);
9960
- break;
9961
- }
9962
- case "summary": {
9963
- const s = a.summary;
9964
- if (s && (!Array.isArray(s.rows) || !s.rows.every((r) => r && Object.keys(r.columns ?? {}).every(known)))) {
9965
- skip(a, "unknown column in summary");
9966
- break;
9967
- }
9968
- grid.setSummary(s ?? null);
9969
- ok(a);
9970
- break;
9971
- }
9972
- case "chart": {
9973
- const ids = Array.isArray(a.colIds) ? a.colIds.filter(known) : [];
9974
- const displayed = grid.columns.displayed().map((c) => c.id);
9975
- const idx = ids.map((id) => displayed.indexOf(id)).filter((i) => i >= 0);
9976
- const count = grid.rowModel.getRowCount();
9977
- if (!idx.length || !count) {
9978
- skip(a, "nothing to chart");
9979
- break;
9980
- }
9981
- const range = { start: { rowIndex: 0, colId: displayed[Math.min(...idx)] }, end: { rowIndex: Math.min(count, 200) - 1, colId: displayed[Math.max(...idx)] } };
9982
- charts.push(grid.api.createChart({ type: CHART_TYPES.includes(a.chartType) ? a.chartType : "bar", range, title: typeof a.title === "string" ? a.title.slice(0, 80) : void 0 }));
9983
- ok(a);
9984
- break;
9985
- }
9986
- case "message":
9987
- ok(a);
9988
- break;
9989
- default:
9990
- skip(a, "unknown action");
9991
- }
9992
- } catch {
9993
- skip(a, "failed");
9994
- }
9995
- }
9996
- const undo = () => {
9997
- for (const c of charts) c.destroy();
9998
- if (grid.columnDefs().length !== before.defs.length || grid.columnDefs().some((d, i) => d !== before.defs[i])) grid.api.setColumns(before.defs);
9999
- grid.setSummary(before.summary);
10000
- grid.api.setState(before.state);
10001
- if (!before.state.conditionalFormats) grid.setConditionalFormats([]);
10002
- };
10003
- return { applied, skipped, undo };
10004
- }
10005
- async function askAssistant(grid, prompt, options, signal) {
10006
- if (!options?.url) throw new Error("assistant.url is not set");
10007
- const headers = typeof options.headers === "function" ? options.headers() : options.headers ?? {};
10008
- const res = await fetch(options.url, {
10009
- method: "POST",
10010
- credentials: options.credentials ?? "same-origin",
10011
- headers: { "Content-Type": "application/json", Accept: "application/json", ...headers },
10012
- body: JSON.stringify(assistantRequest(grid, prompt, options)),
10013
- signal
10014
- });
10015
- if (!res.ok) throw new Error(`HTTP ${res.status} from the assistant`);
10016
- const json = await res.json();
10017
- const result = applyAssistantActions(grid, Array.isArray(json?.actions) ? json.actions : []);
10018
- const say = typeof json?.message === "string" ? json.message : result.applied.filter((a) => a.type === "message").map((a) => a.text ?? "").join("\n");
10019
- return { ...result, message: say.slice(0, 4e3) };
10020
- }
10021
-
10022
- // src/interactions/assistantPanel.ts
10023
- var AssistantPanel = class {
10024
- constructor(grid) {
10025
- this.grid = grid;
10026
- this.popup = null;
10027
- this.log = null;
10028
- this.off = [];
10029
- this.busy = null;
10030
- }
10031
- get isOpen() {
10032
- return !!this.popup;
10033
- }
10034
- open() {
10035
- if (this.popup) {
10036
- this.popup.querySelector(".fg-ai-input")?.focus();
10037
- return;
10038
- }
10039
- const { grid } = this;
10040
- const loc = grid.store.state.locale;
10041
- const opts = grid.options.assistant;
10042
- const popup = el("div", `fg-menu fg-ai-panel fg-theme-${grid.store.state.theme}`, { role: "dialog", "aria-label": loc.ai.title });
10043
- grid.applyFontTo(popup);
10044
- const head = el("div", "fg-chooser-head");
10045
- const title = el("span", "fg-menu-title");
10046
- title.textContent = loc.ai.title;
10047
- const x = el("button", "fg-chooser-close", { type: "button", "aria-label": loc.close });
10048
- x.textContent = "\xD7";
10049
- x.addEventListener("click", () => this.close());
10050
- head.append(title, x);
10051
- const log = el("div", "fg-ai-log", { role: "log", "aria-live": "polite" });
10052
- this.log = log;
10053
- const form = el("form", "fg-ai-form");
10054
- const input = el("textarea", "fg-input fg-ai-input", { rows: "2", placeholder: loc.ai.placeholder, "aria-label": loc.ai.placeholder, maxlength: "2000" });
10055
- const send2 = el("button", "fg-btn fg-btn-primary fg-ai-send", { type: "submit" });
10056
- send2.textContent = loc.ai.send;
10057
- form.append(input, send2);
10058
- const tips = el("div", "fg-ai-tips");
10059
- for (const s of (opts?.suggestions ?? []).slice(0, 6)) {
10060
- const b = el("button", "fg-ai-tip", { type: "button" });
10061
- b.textContent = s;
10062
- b.addEventListener("click", () => {
10063
- input.value = s;
10064
- input.focus();
10065
- });
10066
- tips.append(b);
10067
- }
10068
- input.addEventListener("keydown", (e) => {
10069
- if (e.key === "Enter" && !e.shiftKey && !e.isComposing) {
10070
- e.preventDefault();
10071
- form.requestSubmit?.() ?? form.dispatchEvent(new Event("submit", { cancelable: true }));
10072
- }
10073
- });
10074
- form.addEventListener("submit", (e) => {
10075
- e.preventDefault();
10076
- const prompt = input.value.trim();
10077
- if (!prompt || this.busy) return;
10078
- input.value = "";
10079
- void this.ask(prompt, send2);
10080
- });
10081
- popup.append(head, log, form);
10082
- if (tips.childElementCount) popup.append(tips);
10083
- popupParent(grid.renderer.root).append(popup);
10084
- const r = grid.renderer.root.getBoundingClientRect();
10085
- const w = popup.offsetWidth || 360;
10086
- popup.style.left = `${Math.max(4, Math.min(r.right - w - 16, (window.innerWidth || 1024) - w - 4))}px`;
10087
- popup.style.top = `${Math.max(4, r.top + 16)}px`;
10088
- this.popup = popup;
10089
- input.focus();
10090
- const onDown = (e) => {
10091
- if (!eventInside(e, popup) && !this.busy) this.close();
10092
- };
10093
- const onKey = (e) => {
10094
- if (e.key === "Escape") this.close();
10095
- };
10096
- document.addEventListener("pointerdown", onDown, true);
10097
- document.addEventListener("keydown", onKey, true);
10098
- this.off.push(
10099
- () => document.removeEventListener("pointerdown", onDown, true),
10100
- () => document.removeEventListener("keydown", onKey, true)
10101
- );
10102
- }
10103
- say(who, text2) {
10104
- const msg = el("div", `fg-ai-msg fg-ai-${who}`);
10105
- msg.textContent = text2;
10106
- this.log?.append(msg);
10107
- if (this.log) this.log.scrollTop = this.log.scrollHeight;
10108
- return msg;
10109
- }
10110
- async ask(prompt, send2) {
10111
- const loc = this.grid.store.state.locale;
10112
- this.say("user", prompt);
10113
- const wait = this.say("ai", loc.ai.thinking);
10114
- wait.classList.add("fg-ai-wait");
10115
- this.busy = new AbortController();
10116
- send2.disabled = true;
10117
- try {
10118
- const res = await askAssistant(this.grid, prompt, this.grid.options.assistant, this.busy.signal);
10119
- wait.classList.remove("fg-ai-wait");
10120
- const changed = res.applied.filter((a) => a.type !== "message").length;
10121
- wait.textContent = res.message || (changed ? loc.ai.done : "");
10122
- if (res.skipped.length) {
10123
- const note = el("div", "fg-ai-note");
10124
- note.textContent = `${loc.ai.skipped}: ${res.skipped.map((s) => s.reason).join(", ")}`;
10125
- wait.append(note);
10126
- }
10127
- if (changed) {
10128
- const undo = el("button", "fg-btn fg-ai-undo", { type: "button" });
10129
- undo.textContent = loc.ai.undo;
10130
- undo.addEventListener("click", () => {
10131
- res.undo();
10132
- undo.disabled = true;
10133
- undo.textContent = loc.ai.undone;
10134
- });
10135
- wait.append(undo);
10136
- }
10137
- this.grid.events.emit("assistantAnswer", { prompt, message: res.message, applied: res.applied, skipped: res.skipped });
10138
- } catch (e) {
10139
- if (e?.name === "AbortError") return;
10140
- wait.remove();
10141
- this.say("error", loc.ai.failed);
10142
- } finally {
10143
- this.busy = null;
10144
- send2.disabled = false;
10145
- }
10146
- }
10147
- close() {
10148
- this.busy?.abort();
10149
- this.busy = null;
10150
- for (const f of this.off) f();
10151
- this.off = [];
10152
- this.popup?.remove();
10153
- this.popup = null;
10154
- this.log = null;
10155
- }
10156
- };
10157
-
10158
9777
  // src/grid.ts
10159
9778
  var autoRowHeight = (size) => Math.round(size * 2.75);
10160
9779
  var autoHeaderHeight = (size) => Math.round(size * 3.1);
@@ -10196,6 +9815,8 @@ var Grid = class {
10196
9815
  this.pivotData = null;
10197
9816
  /** rollup option / setRollup: rows merged by the shown columns without an aggFunc. */
10198
9817
  this.rollupOn = false;
9818
+ /** rollup { by }: the columns the rows merge by (only the shown ones count); null: every shown column without an aggFunc. */
9819
+ this.rollupBy = null;
10199
9820
  /** The merged rows while rollup applies (not while pivoting), else null. */
10200
9821
  this.rollupData = null;
10201
9822
  /** Grouping state from before pivot mode, restored when it ends. */
@@ -10238,7 +9859,8 @@ var Grid = class {
10238
9859
  formula: (f, row) => evaluateFormula(f, row, /* @__PURE__ */ new Set(), 0, /* @__PURE__ */ new Map(), this.formulaScope)
10239
9860
  });
10240
9861
  this.formatDialog = new FormatDialog(this);
10241
- this.assistantPanel = new AssistantPanel(this);
9862
+ /** Selection waiting for the row model to hold the newly merged rows (set by updateRows). */
9863
+ this.rollupSelection = null;
10242
9864
  // ── pagination and server rows ──────────────────────────────
10243
9865
  /** Cells flashing after a transaction: `${rowId}\u0000${colId}` → time the flash ends. */
10244
9866
  /** showOverlay: an overlay the app asked for, shown whatever the row count. */
@@ -10373,7 +9995,6 @@ var Grid = class {
10373
9995
  () => this.charts.destroy(),
10374
9996
  () => this.filterBuilder.close(),
10375
9997
  () => this.formatDialog.close(),
10376
- () => this.assistantPanel.close(),
10377
9998
  attachTooltips(this)
10378
9999
  );
10379
10000
  if (typeof ResizeObserver !== "undefined") {
@@ -10382,7 +10003,7 @@ var Grid = class {
10382
10003
  this.disposers.push(() => ro.disconnect());
10383
10004
  }
10384
10005
  if (options.pivot) this.setPivot(options.pivot);
10385
- if (options.rollup) this.setRollup(true);
10006
+ if (options.rollup) this.setRollup(options.rollup);
10386
10007
  void needsFreeBadge(options.licenseKey).then((show) => {
10387
10008
  if (show && !this.destroyed) this.renderer.showFreeBadge();
10388
10009
  });
@@ -10486,6 +10107,11 @@ var Grid = class {
10486
10107
  const paged = !!this.options.pagination;
10487
10108
  const data = this.viewRows();
10488
10109
  this.rowModel.update({ ...this.store.state, data, display: this.display(), page: paged ? { index: this.page, size: this.pageSize } : null });
10110
+ const remapped = this.rollupSelection;
10111
+ if (remapped) {
10112
+ this.rollupSelection = null;
10113
+ this.setSelected(remapped);
10114
+ }
10489
10115
  if (paged && this.page >= this.getPageCount()) {
10490
10116
  this.page = this.getPageCount() - 1;
10491
10117
  this.rowModel.update({ ...this.store.state, data, display: this.display(), page: { index: this.page, size: this.pageSize } });
@@ -10498,25 +10124,51 @@ var Grid = class {
10498
10124
  }
10499
10125
  // ── rollup ──────────────────────────────────────────────────
10500
10126
  getRollup() {
10501
- return this.rollupOn;
10127
+ return this.rollupBy ? { by: [...this.rollupBy] } : this.rollupOn;
10502
10128
  }
10503
- setRollup(on) {
10504
- on = !!on;
10129
+ /** true: merge by every shown column without an aggFunc; { by }: merge by the shown `by` columns; false: off. */
10130
+ setRollup(cfg) {
10131
+ const by = cfg && typeof cfg === "object" ? Array.isArray(cfg.by) ? cfg.by.filter((id) => typeof id === "string") : null : null;
10132
+ if (cfg && typeof cfg === "object" && !by) {
10133
+ warn("setRollup: expected true, false or { by: string[] }");
10134
+ return;
10135
+ }
10136
+ const on = !!cfg;
10505
10137
  if (on && !this.allow("pivot")) return;
10506
10138
  if (on && this.clientOnly("rollup")) return;
10507
10139
  if (on && this.options.treeData) {
10508
10140
  warn("rollup is not available with treeData");
10509
10141
  return;
10510
10142
  }
10511
- if (on === this.rollupOn) return;
10143
+ if (on === this.rollupOn && (by ?? []).join("") === (this.rollupBy ?? []).join("") && !by === !this.rollupBy) return;
10512
10144
  this.editor.cancel();
10513
10145
  this.rollupOn = on;
10146
+ this.rollupBy = on ? by : null;
10514
10147
  this.store.set({ range: null, focus: null });
10515
10148
  this.columnsChanged();
10516
10149
  }
10517
10150
  /** Merge the source rows again (rollup on, not pivoting); the view is updated by the caller. */
10518
10151
  rebuildRollup() {
10519
- this.rollupData = this.rollupOn && !this.pivotCfg ? buildRollup(this.store.state.data, this.dataColumns().map((c) => c.def)) : null;
10152
+ const old = this.rollupData;
10153
+ this.rollupData = this.rollupOn && !this.pivotCfg ? buildRollup(this.store.state.data, this.dataColumns().map((c) => ({ id: c.id, def: c.def })), this.rollupBy) : null;
10154
+ if (old || this.rollupData) this.remapRollupSelection(old);
10155
+ }
10156
+ /**
10157
+ * Keep the selection across a re-merge: a selected merged row stands for its source rows, and a newly merged
10158
+ * row is selected when every one of its source rows was (so a delete never reaches a row nobody checked).
10159
+ */
10160
+ remapRollupSelection(old) {
10161
+ const sel = this.store.state.selected;
10162
+ if (!sel.size) return;
10163
+ const idOf = (r) => this.rowModel.idOf(r);
10164
+ const picked = old ? new Set(old.filter((r) => sel.has(idOf(r))).flatMap((r) => rollupSource(r) ?? [r]).map(idOf)) : sel;
10165
+ const now = this.rollupData;
10166
+ const next = now ? now.filter((r) => (rollupSource(r) ?? [r]).every((s) => picked.has(idOf(s)))).map(idOf) : this.store.state.data.map(idOf).filter((id) => picked.has(id));
10167
+ if (next.length !== sel.size || next.some((id) => !sel.has(id))) this.rollupSelection = new Set(next);
10168
+ }
10169
+ /** With rollup { by }: whether the rows merge by this column (it is one of `by`). */
10170
+ isRollupKey(colId) {
10171
+ return !!this.rollupBy?.includes(colId);
10520
10172
  }
10521
10173
  /** The source rows behind `rows`: merged rollup rows stand for the rows they merge (for group and summary values). */
10522
10174
  sourceRowsOf(rows) {
@@ -12084,7 +11736,7 @@ var Grid = class {
12084
11736
  removeRows(target, record = true) {
12085
11737
  if (this.clientOnly("removeRows")) return [];
12086
11738
  const data = this.store.state.data;
12087
- const match = typeof target === "function" ? target : /* @__PURE__ */ ((set) => (r) => set.has(r))(new Set(target));
11739
+ const match = typeof target === "function" ? target : /* @__PURE__ */ ((set) => (r) => set.has(r))(new Set(Array.isArray(target) ? target.flatMap((r) => rollupSource(r) ?? [r]) : []));
12088
11740
  const removed = [];
12089
11741
  data.forEach((row, index) => {
12090
11742
  let hit = false;
@@ -12208,10 +11860,12 @@ var Grid = class {
12208
11860
  const { first, last } = this.renderer.renderedRange();
12209
11861
  const code = this.store.state.locale.code;
12210
11862
  const pad4 = this.getCellPadding() * 2;
12211
- const HEADER_EXTRA = pad4 + 22 + 6 + 14;
11863
+ const HEADER_EXTRA = pad4 + 6 + 14;
11864
+ const FILTER_BTN = 6 + 22;
11865
+ const filters = this.store.state.filters;
12212
11866
  const CELL_EXTRA = pad4;
12213
11867
  for (const col of cols) {
12214
- let w = this.renderer.measureText(col.def.header ?? col.def.field ?? "", true) + HEADER_EXTRA;
11868
+ let w = this.renderer.measureText(col.def.header ?? col.def.field ?? "", true) + HEADER_EXTRA + (filters[col.id] ? FILTER_BTN : 0);
12215
11869
  const fmt = getFormatter(col.def, code);
12216
11870
  for (let i = first; i <= last; i++) {
12217
11871
  const node = this.rowModel.getRow(i);
@@ -12408,9 +12062,10 @@ var Grid = class {
12408
12062
  }
12409
12063
  this.setSelected(next);
12410
12064
  }
12065
+ /** The checked rows' data; a merged rollup row stands for its source rows, so these are always rows of the data. */
12411
12066
  selectedRows() {
12412
12067
  const sel = this.store.state.selected;
12413
- return sel.size ? this.rowModel.allNodes().filter((n) => sel.has(n.id)).map((n) => n.data) : [];
12068
+ return sel.size ? this.sourceRowsOf(this.rowModel.allNodes().filter((n) => sel.has(n.id)).map((n) => n.data)) : [];
12414
12069
  }
12415
12070
  /** Filtered data rows in display order (group buckets, parents before children), collapsed ones included. */
12416
12071
  orderedDataRows() {
@@ -12501,7 +12156,6 @@ var Grid = class {
12501
12156
  this.contextMenu.close();
12502
12157
  this.filterBuilder.close();
12503
12158
  this.formatDialog.close();
12504
- this.assistantPanel.close();
12505
12159
  if (inPopup) this.renderer.gridEl.focus({ preventScroll: true });
12506
12160
  }
12507
12161
  onChange(keys) {
@@ -12602,12 +12256,6 @@ var Grid = class {
12602
12256
  if (!this.allow("conditional")) return;
12603
12257
  this.formatDialog.open(colId);
12604
12258
  }),
12605
- // closed: as if `assistant` were not set
12606
- askAssistant: guard((prompt) => askAssistant(this, prompt, this.allow("assistant") ? this.options.assistant : void 0)),
12607
- openAssistant: guard(() => {
12608
- if (!this.allow("assistant")) return;
12609
- this.assistantPanel.open();
12610
- }),
12611
12259
  getState: guard(() => this.getState()),
12612
12260
  setState: guard((s, o) => this.setState(s, o)),
12613
12261
  getScrollPosition: guard(() => this.renderer.getScroll()),
@@ -12618,7 +12266,7 @@ var Grid = class {
12618
12266
  setFont: guard((f) => this.setFont(f)),
12619
12267
  setCellPadding: guard((px) => this.setCellPadding(px)),
12620
12268
  getCellPadding: guard(() => this.getCellPadding()),
12621
- setRollup: guard((on) => this.setRollup(on)),
12269
+ setRollup: guard((cfg) => this.setRollup(cfg)),
12622
12270
  getRollup: guard(() => this.getRollup()),
12623
12271
  setColumnAggFunc: guard((id, agg) => this.setColumnAggFunc(id, agg)),
12624
12272
  setColumnVisible: guard((id, v) => this.setColumnsVisible([id], v)),