@freegridjs/core 1.0.0 → 1.0.1

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
@@ -512,14 +512,19 @@ var Parser = class {
512
512
  }
513
513
  };
514
514
  var MAX_DEPTH = 32;
515
- function evaluateFormula(formula, row, seen2 = /* @__PURE__ */ new Set(), depth = 0) {
515
+ function evaluateFormula(formula, row, seen2 = /* @__PURE__ */ new Set(), depth = 0, memo = /* @__PURE__ */ new Map()) {
516
516
  try {
517
517
  const resolve = (name) => {
518
518
  if (!row || !Object.hasOwn(row, name)) throw new FormulaError("#REF!");
519
519
  const v2 = row[name];
520
520
  if (typeof v2 === "string" && v2.startsWith("=")) {
521
521
  if (seen2.has(name) || depth >= MAX_DEPTH) throw new FormulaError("#CYCLE!");
522
- const r = evaluateFormula(v2, row, /* @__PURE__ */ new Set([...seen2, name]), depth + 1);
522
+ let r;
523
+ if (memo.has(name)) r = memo.get(name);
524
+ else {
525
+ r = evaluateFormula(v2, row, /* @__PURE__ */ new Set([...seen2, name]), depth + 1, memo);
526
+ memo.set(name, r);
527
+ }
523
528
  if (typeof r === "string" && /^#[A-Z/0]+[!?]$/.test(r)) throw new FormulaError(r);
524
529
  return r;
525
530
  }
@@ -560,7 +565,10 @@ var toNumber = (s) => {
560
565
  };
561
566
  var toDateKey = (s) => {
562
567
  const m = /^(\d{4})[-./]\s*(\d{1,2})[-./]\s*(\d{1,2})\.?$/.exec(s.trim());
563
- return m ? `${m[1]}-${m[2].padStart(2, "0")}-${m[3].padStart(2, "0")}` : null;
568
+ if (!m) return null;
569
+ const y = Number(m[1]), mo = Number(m[2]), d = Number(m[3]);
570
+ if (mo < 1 || mo > 12 || d < 1 || d > new Date(Date.UTC(y, mo, 0)).getUTCDate()) return null;
571
+ return `${m[1]}-${m[2].padStart(2, "0")}-${m[3].padStart(2, "0")}`;
564
572
  };
565
573
  function parseFilterText(type, text2) {
566
574
  const t = text2.trim();
@@ -620,10 +628,6 @@ var FilterRow = class {
620
628
  const { columns, store } = this.grid;
621
629
  const h = this.height;
622
630
  const cols = columns.displayed();
623
- if (store.state.data !== this.data) {
624
- this.data = store.state.data;
625
- this.layout = "";
626
- }
627
631
  const key = `${h}|${store.state.locale.code}|${cols.map((c) => `${c.id}:${c.left}:${c.width}:${c.region}`).join(",")}`;
628
632
  if (key !== this.layout) {
629
633
  this.layout = key;
@@ -633,7 +637,11 @@ var FilterRow = class {
633
637
  inner.style.height = `${h}px`;
634
638
  inner.replaceChildren(...columns.region(r).map((c) => this.cell(c, h)));
635
639
  }
640
+ this.data = store.state.data;
636
641
  this.rebuilt = true;
642
+ } else if (store.state.data !== this.data) {
643
+ this.data = store.state.data;
644
+ this.refreshLists();
637
645
  }
638
646
  this.sync();
639
647
  }
@@ -657,6 +665,33 @@ var FilterRow = class {
657
665
  }
658
666
  }
659
667
  }
668
+ /** Refill the set lists from the current rows, keeping each list's choice. */
669
+ refreshLists() {
670
+ for (const r of REGIONS2) {
671
+ for (const select of this.inners[r].querySelectorAll("select[data-col]")) {
672
+ const def = this.grid.columns.getNode(select.dataset.col)?.def;
673
+ if (!def) continue;
674
+ const v = select.value;
675
+ this.fillOptions(select, def);
676
+ if (v && ![...select.options].some((o) => o.value === v)) {
677
+ const o = el("option", "", { value: v });
678
+ o.textContent = String(JSON.parse(v));
679
+ select.append(o);
680
+ }
681
+ select.value = v;
682
+ }
683
+ }
684
+ }
685
+ fillOptions(select, def) {
686
+ const all2 = el("option", "", { value: "" });
687
+ all2.textContent = this.grid.store.state.locale.selectAll;
688
+ const opts = this.distinct(def).map((v) => {
689
+ const o = el("option", "", { value: JSON.stringify(v) });
690
+ o.textContent = v === true ? "\u2713" : v === false ? "\u2717" : String(v);
691
+ return o;
692
+ });
693
+ select.replaceChildren(all2, ...opts);
694
+ }
660
695
  cell(c, h) {
661
696
  const cell = el("div", "fg-filter-cell");
662
697
  cell.style.left = `${c.left}px`;
@@ -669,14 +704,7 @@ var FilterRow = class {
669
704
  if (type === "set") {
670
705
  const select = el("select", "fg-filter-input", { "aria-label": label });
671
706
  select.dataset.col = c.id;
672
- const all2 = el("option", "", { value: "" });
673
- all2.textContent = loc.selectAll;
674
- select.append(all2);
675
- for (const v of this.distinct(c.def)) {
676
- const o = el("option", "", { value: JSON.stringify(v) });
677
- o.textContent = v === true ? "\u2713" : v === false ? "\u2717" : String(v);
678
- select.append(o);
679
- }
707
+ this.fillOptions(select, c.def);
680
708
  select.addEventListener("change", () => {
681
709
  this.grid.setFilter(c.id, select.value ? { type: "set", values: [JSON.parse(select.value)] } : null);
682
710
  });
@@ -3075,7 +3103,7 @@ function attachBody(grid) {
3075
3103
  if (!text2 || !e.clipboardData) return;
3076
3104
  e.clipboardData.setData("text/plain", text2);
3077
3105
  e.preventDefault();
3078
- grid.clearCells();
3106
+ grid.clearCells(true);
3079
3107
  };
3080
3108
  const onPaste = (e) => {
3081
3109
  if (fromField(e) || nested(e)) return;
@@ -4144,7 +4172,7 @@ var CellEditor = class {
4144
4172
  beginRow(row, rowIndex) {
4145
4173
  if (this.session?.row === row) return;
4146
4174
  if (this.session) this.endRow(true);
4147
- this.session = { row, id: this.grid.rowModel.idOf(row), before: { ...row } };
4175
+ this.session = { row, id: this.grid.rowModel.idOf(row), before: { ...row }, undoMark: this.grid.history.mark() };
4148
4176
  this.grid.renderer.invalidate({ rows: true });
4149
4177
  this.grid.events.emit("rowEditStart", { row, rowIndex });
4150
4178
  }
@@ -4153,8 +4181,8 @@ var CellEditor = class {
4153
4181
  const s = this.session;
4154
4182
  if (!s) return;
4155
4183
  if (this.cur) {
4156
- if (commit) this.commit(null);
4157
- else this.close(false);
4184
+ if (commit && !this.commit(null)) return;
4185
+ if (!commit) this.close(false);
4158
4186
  }
4159
4187
  this.session = null;
4160
4188
  const { grid } = this;
@@ -4167,6 +4195,7 @@ var CellEditor = class {
4167
4195
  if (!Object.is(s.row[f], s.before[f])) grid.writeCell(changes2, s.row, f, c.id, rowIndex, s.before[f]);
4168
4196
  }
4169
4197
  grid.endCellTx(changes2);
4198
+ grid.history.rollback(s.undoMark);
4170
4199
  }
4171
4200
  const changes = cols.filter((c) => !Object.is(s.row[c.def.field], s.before[c.def.field])).map((c) => ({ colId: c.id, oldValue: s.before[c.def.field], newValue: s.row[c.def.field] }));
4172
4201
  grid.renderer.invalidate({ rows: true });
@@ -4183,7 +4212,7 @@ var CellEditor = class {
4183
4212
  return !node.kind && cellEditable(col.def, node.data, rowIndex);
4184
4213
  }
4185
4214
  start(rowIndex, colId, initialText) {
4186
- if (this.cur) this.commit(null);
4215
+ if (this.cur && !this.commit(null)) return false;
4187
4216
  if (!this.canEdit(rowIndex, colId)) return false;
4188
4217
  const { grid } = this;
4189
4218
  if (grid.veto("beforeEdit", { row: grid.rowModel.getRow(rowIndex).data, rowIndex, colId })) return false;
@@ -4196,14 +4225,8 @@ var CellEditor = class {
4196
4225
  grid.setFocus(rowIndex, colId);
4197
4226
  renderer.ensureVisible(rowIndex, colId);
4198
4227
  renderer.render();
4199
- const cellEl = renderer.root.querySelector(`.fg-row[data-row="${rowIndex}"] .fg-cell[data-col="${CSS.escape(colId)}"]`);
4200
- const rootBox = renderer.root.getBoundingClientRect();
4201
- const box = cellEl?.getBoundingClientRect();
4202
4228
  const wrap = el("div", "fg-editor");
4203
- wrap.style.left = `${(box?.left ?? rootBox.left) - rootBox.left}px`;
4204
- wrap.style.top = `${(box?.top ?? rootBox.top) - rootBox.top}px`;
4205
- wrap.style.width = `${box?.width || col.width}px`;
4206
- wrap.style.height = `${box?.height || renderer.opts.rowHeight}px`;
4229
+ this.place(wrap, rowIndex, colId);
4207
4230
  let control;
4208
4231
  let read;
4209
4232
  let dispose = () => {
@@ -4301,6 +4324,36 @@ var CellEditor = class {
4301
4324
  grid.events.emit("editStart", { rowIndex, colId });
4302
4325
  return true;
4303
4326
  }
4327
+ /** Put the editor over its cell; false when the cell is not rendered. */
4328
+ place(wrap, rowIndex, colId) {
4329
+ const { renderer } = this.grid;
4330
+ const cellEl = renderer.root.querySelector(`.fg-row[data-row="${rowIndex}"] .fg-cell[data-col="${CSS.escape(colId)}"]`);
4331
+ const rootBox = renderer.root.getBoundingClientRect();
4332
+ const box = cellEl?.getBoundingClientRect();
4333
+ wrap.style.left = `${(box?.left ?? rootBox.left) - rootBox.left}px`;
4334
+ wrap.style.top = `${(box?.top ?? rootBox.top) - rootBox.top}px`;
4335
+ wrap.style.width = `${box?.width || this.grid.columns.getColumn(colId)?.width}px`;
4336
+ wrap.style.height = `${box?.height || renderer.opts.rowHeight}px`;
4337
+ return !!cellEl;
4338
+ }
4339
+ /** Display index of a row object, or -1 when it is no longer shown. */
4340
+ indexOf(row) {
4341
+ const m = this.grid.rowModel;
4342
+ const i = m.indexOfId(m.idOf(row));
4343
+ return i >= 0 && m.getRow(i)?.data === row ? i : -1;
4344
+ }
4345
+ /** The rows changed underneath (transactions): follow the edited row, or cancel when it is gone. */
4346
+ sync() {
4347
+ const cur = this.cur;
4348
+ if (!cur) return;
4349
+ const i = this.indexOf(cur.row);
4350
+ if (i < 0) return this.cancel();
4351
+ if (i === cur.rowIndex) return;
4352
+ cur.rowIndex = i;
4353
+ this.grid.setFocus(i, cur.colId);
4354
+ this.grid.renderer.render();
4355
+ if (!this.place(cur.wrap, i, cur.colId)) this.commit(null);
4356
+ }
4304
4357
  /** Validate and write the value. Returns false (editor stays open) when invalid. */
4305
4358
  commit(move) {
4306
4359
  const cur = this.cur;
@@ -4320,18 +4373,30 @@ var CellEditor = class {
4320
4373
  } else value = raw.value;
4321
4374
  const error = grid.cellError(cur.def, cur.row, value);
4322
4375
  if (error !== null) return this.fail(value, error);
4376
+ const rowIndex = this.indexOf(cur.row);
4377
+ if (rowIndex < 0) {
4378
+ this.cancel();
4379
+ return false;
4380
+ }
4381
+ cur.rowIndex = rowIndex;
4323
4382
  this.close(true);
4324
- grid.applyEdit(cur.rowIndex, cur.colId, value);
4383
+ grid.applyEdit(rowIndex, cur.colId, value);
4325
4384
  if (this.session && (move === "down" || move === "up")) {
4326
4385
  this.endRow(true);
4327
4386
  }
4328
- this.move(cur.rowIndex, cur.colId, move);
4387
+ this.move(rowIndex, cur.colId, move);
4329
4388
  return true;
4330
4389
  }
4331
4390
  cancel() {
4332
4391
  this.close(false);
4333
4392
  if (this.session) this.endRow(false);
4334
4393
  }
4394
+ /** Teardown: remove the editor without writing values or emitting events. */
4395
+ destroy() {
4396
+ this.cur?.dispose();
4397
+ this.cur = null;
4398
+ this.session = null;
4399
+ }
4335
4400
  fail(value, message) {
4336
4401
  const cur = this.cur;
4337
4402
  cur.wrap.classList.add("fg-editor-invalid");
@@ -4404,6 +4469,16 @@ var History = class {
4404
4469
  this.entries[this.index++].redo();
4405
4470
  return true;
4406
4471
  }
4472
+ /** The current position, for rollback(). */
4473
+ mark() {
4474
+ return this.entries[this.index - 1] ?? null;
4475
+ }
4476
+ /** Forget every step pushed after `mark` (a cancelled edit session). */
4477
+ rollback(mark) {
4478
+ const i = mark ? this.entries.indexOf(mark) + 1 : 0;
4479
+ this.entries.splice(i);
4480
+ this.index = Math.min(this.index, i);
4481
+ }
4407
4482
  clear() {
4408
4483
  this.entries = [];
4409
4484
  this.index = 0;
@@ -4432,6 +4507,14 @@ var ChangeTracker = class {
4432
4507
  const o = this.originals.get(row);
4433
4508
  return o ? fields.filter((f) => !same(o[f], row[f])) : [];
4434
4509
  }
4510
+ /** Baseline `rows` (row → the values that were saved); `deleted` rows are no longer pending. */
4511
+ commit(rows, deleted) {
4512
+ for (const r of deleted) this.deleted.delete(r);
4513
+ for (const [row, values] of rows) {
4514
+ this.created.delete(row);
4515
+ this.originals.set(row, { ...values });
4516
+ }
4517
+ }
4435
4518
  get size() {
4436
4519
  return this.originals.size + this.created.size + this.deleted.size;
4437
4520
  }
@@ -4692,12 +4775,34 @@ function zipStore(files) {
4692
4775
  return out;
4693
4776
  }
4694
4777
  var isZip = (data) => data.length > 4 && data[0] === 80 && data[1] === 75 && data[2] === 3 && data[3] === 4;
4695
- async function inflateRaw(data) {
4778
+ var tooLarge = (max) => new Error(`Zip file is too large: it unpacks to more than ${max >= 1048576 ? `${Math.round(max / 1048576)} MB` : `${max} bytes`}`);
4779
+ async function inflateRaw(data, max) {
4696
4780
  if (typeof DecompressionStream === "undefined") throw new Error("This browser cannot read compressed xlsx files (no DecompressionStream)");
4697
4781
  const stream = new Response(data).body.pipeThrough(new DecompressionStream("deflate-raw"));
4698
- return new Uint8Array(await new Response(stream).arrayBuffer());
4782
+ const reader = stream.getReader();
4783
+ const chunks = [];
4784
+ let size = 0;
4785
+ for (; ; ) {
4786
+ const { done, value } = await reader.read();
4787
+ if (done) break;
4788
+ size += value.length;
4789
+ if (size > max) {
4790
+ await reader.cancel().catch(() => void 0);
4791
+ throw tooLarge(max);
4792
+ }
4793
+ chunks.push(value);
4794
+ }
4795
+ const out = new Uint8Array(size);
4796
+ let at = 0;
4797
+ for (const c of chunks) {
4798
+ out.set(c, at);
4799
+ at += c.length;
4800
+ }
4801
+ return out;
4699
4802
  }
4700
- async function unzip(data) {
4803
+ async function unzip(data, limits = {}) {
4804
+ const maxBytes = limits.maxBytes ?? 200 * 1048576;
4805
+ const maxEntries = limits.maxEntries ?? 1e4;
4701
4806
  const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
4702
4807
  let eocd = -1;
4703
4808
  for (let i = data.length - 22; i >= Math.max(0, data.length - 65557); i--) {
@@ -4708,9 +4813,11 @@ async function unzip(data) {
4708
4813
  }
4709
4814
  if (eocd < 0) throw new Error("Not a zip file");
4710
4815
  const count = view.getUint16(eocd + 10, true);
4816
+ if (count > maxEntries) throw new Error(`Zip file has too many entries (${count}, at most ${maxEntries})`);
4711
4817
  let p = view.getUint32(eocd + 16, true);
4712
4818
  const decoder = new TextDecoder();
4713
4819
  const out = /* @__PURE__ */ new Map();
4820
+ let total = 0;
4714
4821
  for (let i = 0; i < count; i++) {
4715
4822
  if (view.getUint32(p, true) !== 33639248) throw new Error("Broken zip directory");
4716
4823
  const method = view.getUint16(p + 10, true);
@@ -4721,9 +4828,13 @@ async function unzip(data) {
4721
4828
  const lName = view.getUint16(offset + 26, true), lExtra = view.getUint16(offset + 28, true);
4722
4829
  const start = offset + 30 + lName + lExtra;
4723
4830
  const raw = data.subarray(start, start + size);
4724
- if (method === 0) out.set(name, raw);
4725
- else if (method === 8) out.set(name, await inflateRaw(raw));
4831
+ let bytes;
4832
+ if (method === 0) bytes = raw;
4833
+ else if (method === 8) bytes = await inflateRaw(raw, maxBytes - total);
4726
4834
  else throw new Error(`Unsupported zip compression ${method}`);
4835
+ total += bytes.length;
4836
+ if (total > maxBytes) throw tooLarge(maxBytes);
4837
+ out.set(name, bytes);
4727
4838
  p += 46 + nameLen + extraLen + commentLen;
4728
4839
  }
4729
4840
  return out;
@@ -4844,7 +4955,8 @@ function writeXlsx(sheets) {
4844
4955
  const views = freeze ? `<sheetViews><sheetView workbookViewId="0"><pane ySplit="${freeze}" topLeftCell="A${freeze + 1}" activePane="bottomLeft" state="frozen"/></sheetView></sheetViews>` : '<sheetViews><sheetView workbookViewId="0"/></sheetViews>';
4845
4956
  const cols = sheet.widths?.length ? `<cols>${sheet.widths.map((w, i) => `<col min="${i + 1}" max="${i + 1}" width="${Math.max(4, Math.min(120, w)).toFixed(1)}" customWidth="1"/>`).join("")}</cols>` : "";
4846
4957
  const filter = sheet.autoFilter && width && freeze && sheet.rows.length >= freeze ? `<autoFilter ref="A${freeze}:${colName(width - 1)}${sheet.rows.length}"/>` : "";
4847
- const merges = sheet.merges?.length ? `<mergeCells count="${sheet.merges.length}">${sheet.merges.map((m) => `<mergeCell ref="${m}"/>`).join("")}</mergeCells>` : "";
4958
+ const mergeRefs = (sheet.merges ?? []).filter((m) => typeof m === "string" && /^[A-Z]{1,3}\d{1,7}:[A-Z]{1,3}\d{1,7}$/.test(m));
4959
+ const merges = mergeRefs.length ? `<mergeCells count="${mergeRefs.length}">${mergeRefs.map((m) => `<mergeCell ref="${m}"/>`).join("")}</mergeCells>` : "";
4848
4960
  return `${XML}<worksheet xmlns="${NS}" xmlns:r="${REL_NS}">${views}<sheetFormatPr defaultRowHeight="15"/>${cols}<sheetData>${rowsXml}</sheetData>${filter}${merges}</worksheet>`;
4849
4961
  });
4850
4962
  const styles = `${XML}<styleSheet xmlns="${NS}">` + (customFmts.length ? `<numFmts count="${customFmts.length}">${customFmts.map((f, i) => `<numFmt numFmtId="${164 + i}" formatCode="${esc(f)}"/>`).join("")}</numFmts>` : "") + `<fonts count="${fonts.length}">${fonts.join("")}</fonts><fills count="${fills.length}">${fills.join("")}</fills><borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders><cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs><cellXfs count="${xfs.length}">${xfs.join("")}</cellXfs><cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles></styleSheet>`;
@@ -4892,6 +5004,8 @@ function colIndex(ref) {
4892
5004
  }
4893
5005
  return n - 1;
4894
5006
  }
5007
+ var MAX_ROWS = 1048576;
5008
+ var MAX_COLS = 16384;
4895
5009
  async function readXlsx(input) {
4896
5010
  const data = input instanceof Uint8Array ? input : new Uint8Array(input);
4897
5011
  if (!isZip(data)) throw new Error("Not an xlsx file");
@@ -4919,11 +5033,13 @@ async function readXlsx(input) {
4919
5033
  if (doc) {
4920
5034
  for (const row of tags(doc, "row")) {
4921
5035
  const r = Number(row.getAttribute("r") ?? rows.length + 1) - 1;
5036
+ if (!Number.isInteger(r) || r < 0 || r >= MAX_ROWS) continue;
4922
5037
  const out = [];
4923
5038
  let next = 0;
4924
5039
  for (const c of [...row.children].filter((x) => x.localName === "c")) {
4925
5040
  const ref = c.getAttribute("r");
4926
5041
  const ci = ref ? colIndex(ref) : next;
5042
+ if (ci < 0 || ci >= MAX_COLS) continue;
4927
5043
  next = ci + 1;
4928
5044
  const t = c.getAttribute("t");
4929
5045
  const v = tags(c, "v")[0]?.textContent ?? null;
@@ -4968,6 +5084,8 @@ function exportRows(grid, mode) {
4968
5084
  return grid.rowModel.dataRows().map((n) => n.data);
4969
5085
  }
4970
5086
  var header = (def, id) => def.header ?? def.field ?? id;
5087
+ var ALIGNS = ["left", "center", "right"];
5088
+ var PLAIN_NUMBER = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i;
4971
5089
  var escHtml = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
4972
5090
  function buildPrintHtml(grid, opts = {}) {
4973
5091
  const cols = exportColumns(grid, opts.columns);
@@ -4997,7 +5115,7 @@ function buildPrintHtml(grid, opts = {}) {
4997
5115
  }
4998
5116
  head.push(`<tr>${tr}</tr>`);
4999
5117
  }
5000
- const align = (def) => def.align ?? (def.type === "number" ? "right" : def.type === "boolean" ? "center" : "left");
5118
+ const align = (def) => ALIGNS.includes(def.align) ? def.align : def.type === "number" ? "right" : def.type === "boolean" ? "center" : "left";
5001
5119
  const body = exportRows(grid, opts.rows).map((r) => `<tr>${cols.map((c, i) => {
5002
5120
  const v = getCellValue(c.def, r);
5003
5121
  return `<td style="text-align:${align(c.def)}">${escHtml(isBlank(v) ? "" : fmts[i](v))}</td>`;
@@ -5048,7 +5166,8 @@ function buildCsv(grid, opts = {}) {
5048
5166
  const matrix = exportRows(grid, opts.rows).map((r) => cols.map((c) => {
5049
5167
  const v = getCellValue(c.def, r);
5050
5168
  const t = c.def.type;
5051
- return { value: copyValue(c.def, v, code), text: !(t === "number" || t === "date" || t === "boolean") || typeof v === "string" };
5169
+ const value = copyValue(c.def, v, code);
5170
+ return { value, text: !(t === "number" || t === "date" || t === "boolean") || typeof v === "string" && !PLAIN_NUMBER.test(value) };
5052
5171
  }));
5053
5172
  if (opts.headers !== false) matrix.unshift(cols.map((c) => ({ value: header(c.def, c.id), text: true })));
5054
5173
  return toCsv(matrix, sep);
@@ -5559,7 +5678,7 @@ function drawScatter(root, spec, series, top, bottom, nf, short) {
5559
5678
 
5560
5679
  // src/interactions/charts.ts
5561
5680
  var MAX_SERIES = 12;
5562
- var MAX_ROWS = 500;
5681
+ var MAX_ROWS2 = 500;
5563
5682
  var TYPES = ["bar", "stackedBar", "line", "area", "combo", "pie", "donut", "scatter"];
5564
5683
  var GridCharts = class {
5565
5684
  constructor(grid) {
@@ -5577,7 +5696,7 @@ var GridCharts = class {
5577
5696
  if (a && b) {
5578
5697
  const box = normalizeRange({ start: { rowIndex: r.start.rowIndex, colIndex: a.index }, end: { rowIndex: r.end.rowIndex, colIndex: b.index } });
5579
5698
  const rows = [];
5580
- for (let i = box.r1; i <= box.r2 && rows.length < MAX_ROWS; i++) {
5699
+ for (let i = box.r1; i <= box.r2 && rows.length < MAX_ROWS2; i++) {
5581
5700
  const n = grid.rowModel.getRow(i);
5582
5701
  if (n && !n.kind) rows.push(n.data);
5583
5702
  }
@@ -5585,7 +5704,7 @@ var GridCharts = class {
5585
5704
  }
5586
5705
  }
5587
5706
  const data = () => {
5588
- const rows = fixed ? fixed.rows : grid.rowModel.dataRows().slice(0, MAX_ROWS).map((n) => n.data);
5707
+ const rows = fixed ? fixed.rows : grid.rowModel.dataRows().slice(0, MAX_ROWS2).map((n) => n.data);
5589
5708
  const cols = (fixed ? fixed.colIds.map((id) => grid.columns.getColumn(id)) : grid.columns.displayed().filter((c) => !isSystemCol(c.id))).filter((c) => !!c);
5590
5709
  const cat = cols[0] && cols[0].def.type !== "number" ? cols[0] : fixed ? null : cols.find((c) => c.def.type !== "number") ?? null;
5591
5710
  const isTotal = (c) => !!c.node.parent && c.node.parent.id === "pivotgroup|total";
@@ -5836,7 +5955,7 @@ function buildPivot(source, config, defOf, labels, locale) {
5836
5955
  }
5837
5956
 
5838
5957
  // src/version.ts
5839
- var version = "1.0.0";
5958
+ var version = "1.0.1";
5840
5959
 
5841
5960
  // src/license.ts
5842
5961
  var PUBLIC_KEY = { kty: "EC", crv: "P-256", x: "sUTKkglZmk-HMKPNub8p7AeLBKesHCU8irIkw9rJlbE", y: "j0U_aKRDR7iApY9UpaD3hUemhBLZJwDv9-QiT_hUyw8" };
@@ -5862,6 +5981,12 @@ function domainMatches(host, domains) {
5862
5981
  return wildcard && h.endsWith(`.${dom}`);
5863
5982
  });
5864
5983
  }
5984
+ var isDay = (s) => {
5985
+ const m = typeof s === "string" ? /^(\d{4})-(\d{2})-(\d{2})$/.exec(s) : null;
5986
+ if (!m) return false;
5987
+ const d = new Date(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])));
5988
+ return d.getUTCMonth() === Number(m[2]) - 1 && d.getUTCDate() === Number(m[3]);
5989
+ };
5865
5990
  var today = (now) => `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
5866
5991
  async function verifyLicense(key, host, opts = {}) {
5867
5992
  const parts = typeof key === "string" ? key.trim().split(".") : [];
@@ -5869,7 +5994,7 @@ async function verifyLicense(key, host, opts = {}) {
5869
5994
  let info;
5870
5995
  try {
5871
5996
  info = JSON.parse(new TextDecoder().decode(b64urlDecode(parts[1])));
5872
- if (!Array.isArray(info.domains) || typeof info.expires !== "string") return { ok: false, reason: "format" };
5997
+ if (!Array.isArray(info.domains) || !isDay(info.expires)) return { ok: false, reason: "format" };
5873
5998
  } catch {
5874
5999
  return { ok: false, reason: "format" };
5875
6000
  }
@@ -6479,17 +6604,25 @@ var RowModel = class {
6479
6604
 
6480
6605
  // src/rows/serverGroups.ts
6481
6606
  var LEVEL_ROWS = 1e4;
6607
+ var MAX_LEVEL_ROWS = 1e5;
6608
+ var MAX_IN_FLIGHT = 4;
6609
+ var RETRY_MS = 3e3;
6482
6610
  var ServerGroups = class {
6483
- constructor(datasource, getRowId, groupBy, query, onChange) {
6611
+ constructor(datasource, getRowId, groupBy, query, onChange, onFailed) {
6484
6612
  this.datasource = datasource;
6485
6613
  this.getRowId = getRowId;
6486
6614
  this.groupBy = groupBy;
6487
6615
  this.query = query;
6488
6616
  this.onChange = onChange;
6617
+ this.onFailed = onFailed;
6489
6618
  this.levels = /* @__PURE__ */ new Map();
6490
6619
  this.gen = 0;
6491
6620
  this.display = null;
6492
6621
  this.ids = null;
6622
+ this.queue = [];
6623
+ this.inFlight = 0;
6624
+ /** Earliest time a failed level in the display may be asked again (0: none). */
6625
+ this.retryAt = 0;
6493
6626
  this.version = 0;
6494
6627
  this.builtFor = -1;
6495
6628
  }
@@ -6498,9 +6631,15 @@ var ServerGroups = class {
6498
6631
  }
6499
6632
  purge() {
6500
6633
  this.gen++;
6634
+ this.queue = [];
6501
6635
  this.levels.clear();
6502
6636
  this.touch();
6503
6637
  }
6638
+ /** Replaced by another grouping: answers still on their way are dropped, no more change calls. */
6639
+ dispose() {
6640
+ this.gen++;
6641
+ this.queue = [];
6642
+ }
6504
6643
  touch() {
6505
6644
  this.version++;
6506
6645
  this.display = null;
@@ -6508,15 +6647,33 @@ var ServerGroups = class {
6508
6647
  }
6509
6648
  load(path, keys, level) {
6510
6649
  if (this.levels.has(path)) return;
6511
- const gen = this.gen;
6512
6650
  this.levels.set(path, { state: "loading", nodes: [] });
6651
+ this.fetch({ path, keys, level, start: 0, gen: this.gen });
6652
+ }
6653
+ fetch(job) {
6654
+ this.queue.push(job);
6655
+ this.pump();
6656
+ }
6657
+ pump() {
6658
+ while (this.inFlight < MAX_IN_FLIGHT && this.queue.length) {
6659
+ const job = this.queue.shift();
6660
+ if (job.gen !== this.gen) continue;
6661
+ this.inFlight++;
6662
+ void this.request(job).finally(() => {
6663
+ this.inFlight--;
6664
+ this.pump();
6665
+ });
6666
+ }
6667
+ }
6668
+ request({ path, keys, level, start, gen }) {
6513
6669
  const last = level >= this.groupBy.length;
6514
6670
  const q = this.query;
6671
+ const end = start + LEVEL_ROWS;
6515
6672
  let result;
6516
6673
  try {
6517
6674
  result = Promise.resolve(this.datasource.getRows({
6518
- start: 0,
6519
- end: LEVEL_ROWS,
6675
+ start,
6676
+ end,
6520
6677
  sort: [...q.sort],
6521
6678
  filters: { ...q.filters },
6522
6679
  quickFilter: q.quickFilter,
@@ -6527,11 +6684,11 @@ var ServerGroups = class {
6527
6684
  } catch (e) {
6528
6685
  result = Promise.reject(e);
6529
6686
  }
6530
- result.then((res) => {
6687
+ return result.then((res) => {
6531
6688
  if (gen !== this.gen) return;
6532
6689
  const rows = Array.isArray(res?.rows) ? res.rows.filter((r) => r !== null && typeof r === "object") : [];
6533
6690
  if (!Array.isArray(res?.rows)) warn("datasource.getRows must resolve to { rows: Row[] }");
6534
- const nodes = last ? rows.map((data, i) => ({ id: this.getRowId ? String(this.getRowId(data)) : autoId(data), data, sourceIndex: i, level })) : rows.map((data) => {
6691
+ const added = last ? rows.map((data, i) => ({ id: this.getRowId ? String(this.getRowId(data)) : autoId(data), data, sourceIndex: start + i, level })) : rows.map((data) => {
6535
6692
  const colId = this.groupBy[level];
6536
6693
  const value = data[colId];
6537
6694
  const key = `${path ? `${path}|` : ""}${colId}:${setKey(value)}`;
@@ -6545,14 +6702,23 @@ var ServerGroups = class {
6545
6702
  group: { key, colId, value, rows: [], expanded: false, count, aggregates: data, keys: [...keys, value] }
6546
6703
  };
6547
6704
  });
6548
- this.levels.set(path, { state: "loaded", nodes });
6705
+ const nodes = start ? [...this.levels.get(path)?.nodes ?? [], ...added] : added;
6706
+ const total = typeof res?.total === "number" && res.total >= 0 ? res.total : null;
6707
+ const more = rows.length > 0 && (total !== null ? nodes.length < total : rows.length >= end - start);
6708
+ if (more && nodes.length < MAX_LEVEL_ROWS) {
6709
+ this.levels.set(path, { state: "loading", nodes });
6710
+ this.fetch({ path, keys, level, start: nodes.length, gen });
6711
+ } else {
6712
+ if (more) warn(`a server group level has more than ${MAX_LEVEL_ROWS} rows; only the first ${MAX_LEVEL_ROWS} are shown`);
6713
+ this.levels.set(path, { state: "loaded", nodes });
6714
+ }
6549
6715
  this.touch();
6550
6716
  this.onChange();
6551
- }, () => {
6717
+ }, (error) => {
6552
6718
  if (gen !== this.gen) return;
6553
- this.levels.set(path, { state: "failed", nodes: [] });
6719
+ this.levels.set(path, { state: "failed", nodes: [], retryAt: Date.now() + RETRY_MS });
6554
6720
  this.touch();
6555
- this.onChange();
6721
+ this.onFailed(start, end, error);
6556
6722
  });
6557
6723
  }
6558
6724
  isOpen(d, key, level) {
@@ -6562,17 +6728,24 @@ var ServerGroups = class {
6562
6728
  }
6563
6729
  /** Display rows: open groups show their loaded children, or a loading row while they arrive. */
6564
6730
  rows(d, displayVersion) {
6565
- if (this.display && this.builtFor === displayVersion) return this.display;
6731
+ const now = Date.now();
6732
+ if (this.display && this.builtFor === displayVersion && !(this.retryAt && now >= this.retryAt)) return this.display;
6566
6733
  const out = [];
6734
+ let retryAt = 0;
6567
6735
  const walk = (path, keys, level) => {
6568
- const lv = this.levels.get(path);
6736
+ let lv = this.levels.get(path);
6737
+ if (lv?.state === "failed" && now >= (lv.retryAt ?? 0)) {
6738
+ this.levels.delete(path);
6739
+ lv = void 0;
6740
+ }
6569
6741
  if (!lv) {
6570
6742
  this.load(path, keys, level);
6571
6743
  out.push({ id: `loading:${path}`, data: {}, sourceIndex: -1, kind: "loading", level });
6572
6744
  return;
6573
6745
  }
6574
- if (lv.state !== "loaded") {
6575
- if (lv.state === "loading") out.push({ id: `loading:${path}`, data: {}, sourceIndex: -1, kind: "loading", level });
6746
+ if (lv.state === "failed") {
6747
+ const at = lv.retryAt ?? 0;
6748
+ retryAt = retryAt ? Math.min(retryAt, at) : at;
6576
6749
  return;
6577
6750
  }
6578
6751
  for (const n of lv.nodes) {
@@ -6586,8 +6759,10 @@ var ServerGroups = class {
6586
6759
  out.push(node);
6587
6760
  if (open) walk(g.key, g.keys ?? [], level + 1);
6588
6761
  }
6762
+ if (lv.state === "loading") out.push({ id: `loading:${path}`, data: {}, sourceIndex: -1, kind: "loading", level });
6589
6763
  };
6590
6764
  walk("", [], 0);
6765
+ this.retryAt = retryAt;
6591
6766
  this.display = out;
6592
6767
  this.builtFor = displayVersion;
6593
6768
  this.ids = null;
@@ -6621,7 +6796,6 @@ var ServerGroups = class {
6621
6796
  };
6622
6797
 
6623
6798
  // src/rows/serverRowModel.ts
6624
- var RETRY_MS = 3e3;
6625
6799
  var NO_OFFSETS = new Float64Array(0);
6626
6800
  var ServerRowModel = class {
6627
6801
  constructor(deps) {
@@ -6655,7 +6829,15 @@ var ServerRowModel = class {
6655
6829
  if (groupBy.length && !input.display?.getParentId) {
6656
6830
  const q2 = { sort: input.sort, filters: input.filters, quickFilter: input.quickFilter, advancedFilter: input.advancedFilter ?? null };
6657
6831
  if (!this.groups || !this.groups.matches(groupBy, q2)) {
6658
- this.groups = new ServerGroups(this.deps.datasource, this.deps.getRowId, [...groupBy], q2, () => this.deps.onLoaded(0, 0, null));
6832
+ this.groups?.dispose();
6833
+ this.groups = new ServerGroups(
6834
+ this.deps.datasource,
6835
+ this.deps.getRowId,
6836
+ [...groupBy],
6837
+ q2,
6838
+ () => this.deps.onLoaded(0, 0, null),
6839
+ (start, end, error) => this.deps.onFailed(start, end, error)
6840
+ );
6659
6841
  this.touch();
6660
6842
  }
6661
6843
  this.displayIn = input.display ?? null;
@@ -6663,6 +6845,7 @@ var ServerRowModel = class {
6663
6845
  return;
6664
6846
  }
6665
6847
  if (this.groups) {
6848
+ this.groups.dispose();
6666
6849
  this.groups = null;
6667
6850
  this.query = null;
6668
6851
  this.touch();
@@ -7066,22 +7249,30 @@ var FilterBuilder = class {
7066
7249
  del.addEventListener("click", () => remove(state));
7067
7250
  return state;
7068
7251
  };
7069
- const groupRow = (g) => {
7252
+ const keptRow = (kept, label) => {
7070
7253
  const rowEl = el("div", "fg-fb-row fg-fb-group");
7071
7254
  const text2 = el("span", "fg-fb-group-label");
7072
- text2.textContent = `( ${g.conditions.length} \xD7 ${loc[g.operator]} )`;
7255
+ text2.textContent = label;
7073
7256
  const del = el("button", "fg-chooser-close fg-fb-remove", { type: "button", "aria-label": loc.close });
7074
7257
  del.textContent = "\xD7";
7075
7258
  rowEl.append(text2, del);
7076
- const state = { el: rowEl, read: () => g };
7259
+ const state = { el: rowEl, read: () => kept };
7077
7260
  del.addEventListener("click", () => remove(state));
7078
7261
  return state;
7079
7262
  };
7263
+ const rowFor = (c) => {
7264
+ if ("conditions" in c) return keptRow(c, `( ${c.conditions.length} \xD7 ${loc[c.operator]} )`);
7265
+ const col = cols.find((x2) => x2.id === c.colId);
7266
+ const m = c.model;
7267
+ if (col && m && (m.type === "text" || m.type === "number" || m.type === "date")) return conditionRow(c);
7268
+ const what = m?.type === "set" ? m.values.map(String).join(", ") : m?.type === "combined" ? `( ${m.conditions.length} \xD7 ${loc[m.operator]} )` : "";
7269
+ return keptRow(c, `${col?.label ?? c.colId}: ${what}`);
7270
+ };
7080
7271
  const add = (row) => {
7081
7272
  rows.push(row);
7082
7273
  list.append(row.el);
7083
7274
  };
7084
- for (const c of cur?.conditions ?? []) add("conditions" in c ? groupRow(c) : conditionRow(c));
7275
+ for (const c of cur?.conditions ?? []) add(rowFor(c));
7085
7276
  const addBtn = el("button", "fg-btn fg-fb-add", { type: "button" });
7086
7277
  addBtn.textContent = `+ ${loc.addCondition}`;
7087
7278
  addBtn.addEventListener("click", () => {
@@ -7279,7 +7470,7 @@ var Grid = class {
7279
7470
  () => this.menu.close(),
7280
7471
  () => this.chooser.close(),
7281
7472
  () => this.contextMenu.destroy(),
7282
- () => this.editor.cancel(),
7473
+ () => this.editor.destroy(),
7283
7474
  () => this.fillHandle.destroy(),
7284
7475
  () => this.groupPanel?.destroy(),
7285
7476
  () => this.charts.destroy(),
@@ -7449,35 +7640,68 @@ var Grid = class {
7449
7640
  this.flashTimer = window.setTimeout(tick, 1e3);
7450
7641
  }
7451
7642
  applyTransaction(tx) {
7643
+ if (!tx || typeof tx !== "object") return { added: [], updated: [], removed: [], notFound: [] };
7644
+ return this.applyTransactions([tx]);
7645
+ }
7646
+ /** Apply transactions one after another, then render and emit once. */
7647
+ applyTransactions(list) {
7452
7648
  const result = { added: [], updated: [], removed: [], notFound: [] };
7453
- if (this.clientOnly("applyTransaction") || !tx || typeof tx !== "object") return result;
7454
- const data = this.store.state.data;
7649
+ if (this.clientOnly("applyTransaction")) return result;
7650
+ const colsByField = /* @__PURE__ */ new Map();
7651
+ for (const st of this.columns.getState()) {
7652
+ const f = this.columns.getNode(st.colId)?.def.field;
7653
+ if (f) colsByField.set(f, [...colsByField.get(f) ?? [], st.colId]);
7654
+ }
7655
+ const flashKeys = [];
7656
+ let next = this.store.state.data;
7657
+ const present = new Set(next);
7658
+ const byId = new Map(next.map((r) => [this.rowModel.idOf(r), r]));
7659
+ for (const tx of list) {
7660
+ if (tx && typeof tx === "object") next = this.txStep(tx, next, { present, byId }, result, flashKeys, colsByField);
7661
+ }
7662
+ if (!result.added.length && !result.updated.length && !result.removed.length) {
7663
+ if (result.notFound.length) this.events.emit("transactionApplied", result);
7664
+ return result;
7665
+ }
7666
+ this.editVersion++;
7667
+ this.renderer.invalidateHeights();
7668
+ this.keepRange = true;
7669
+ try {
7670
+ this.replaceData(next === this.store.state.data ? [...next] : next);
7671
+ } finally {
7672
+ this.keepRange = false;
7673
+ }
7674
+ this.editor.sync();
7675
+ if (flashKeys.length && this.options.flashCells !== false) this.flash(flashKeys);
7676
+ this.events.emit("transactionApplied", result);
7677
+ return result;
7678
+ }
7679
+ /** One transaction against `data` (remove → update → add); returns the new array. */
7680
+ txStep(tx, data, index, result, flashKeys, colsByField) {
7455
7681
  const getId = this.options.getRowId;
7682
+ const { present, byId } = index;
7456
7683
  const keyOf = (r) => {
7457
7684
  if (!r || typeof r !== "object") return null;
7458
- if (data.includes(r)) return this.rowModel.idOf(r);
7685
+ if (present.has(r)) return this.rowModel.idOf(r);
7459
7686
  try {
7460
7687
  return getId ? String(getId(r)) : null;
7461
7688
  } catch {
7462
7689
  return null;
7463
7690
  }
7464
7691
  };
7465
- const byId = new Map(data.map((r) => [this.rowModel.idOf(r), r]));
7466
7692
  const gone = /* @__PURE__ */ new Set();
7467
7693
  for (const r of tx.remove ?? []) {
7468
7694
  const k = keyOf(r);
7469
7695
  const row = k === null ? void 0 : byId.get(k);
7470
7696
  if (row && !gone.has(row)) {
7471
7697
  gone.add(row);
7698
+ present.delete(row);
7699
+ byId.delete(k);
7472
7700
  result.removed.push(row);
7701
+ this.tracker.created.delete(row);
7702
+ this.tracker.originals.delete(row);
7473
7703
  } else if (!row) result.notFound.push(r);
7474
7704
  }
7475
- const colsByField = /* @__PURE__ */ new Map();
7476
- for (const st of this.columns.getState()) {
7477
- const f = this.columns.getNode(st.colId)?.def.field;
7478
- if (f) colsByField.set(f, [...colsByField.get(f) ?? [], st.colId]);
7479
- }
7480
- const flashKeys = [];
7481
7705
  for (const r of tx.update ?? []) {
7482
7706
  const k = keyOf(r);
7483
7707
  const row = k === null ? void 0 : byId.get(k);
@@ -7485,7 +7709,9 @@ var Grid = class {
7485
7709
  result.notFound.push(r);
7486
7710
  continue;
7487
7711
  }
7712
+ const orig = this.tracker.originals.get(row);
7488
7713
  for (const [field, value] of Object.entries(r)) {
7714
+ if (orig) orig[field] = value;
7489
7715
  if (Object.is(row[field], value)) continue;
7490
7716
  row[field] = value;
7491
7717
  for (const colId of colsByField.get(field) ?? []) flashKeys.push(`${k}\0${colId}`);
@@ -7494,24 +7720,15 @@ var Grid = class {
7494
7720
  }
7495
7721
  const added = cleanRows(Array.isArray(tx.add) ? tx.add : []);
7496
7722
  result.added.push(...added);
7497
- if (!result.added.length && !result.updated.length && !result.removed.length) {
7498
- if (result.notFound.length) this.events.emit("transactionApplied", result);
7499
- return result;
7723
+ for (const r of added) {
7724
+ present.add(r);
7725
+ byId.set(this.rowModel.idOf(r), r);
7500
7726
  }
7727
+ if (!gone.size && !added.length) return data;
7501
7728
  const next = gone.size ? data.filter((r) => !gone.has(r)) : [...data];
7502
7729
  const at = typeof tx.addIndex === "number" ? Math.max(0, Math.min(next.length, Math.floor(tx.addIndex))) : next.length;
7503
7730
  next.splice(at, 0, ...added);
7504
- this.editVersion++;
7505
- this.renderer.invalidateHeights();
7506
- this.keepRange = true;
7507
- try {
7508
- this.replaceData(next);
7509
- } finally {
7510
- this.keepRange = false;
7511
- }
7512
- if (flashKeys.length && this.options.flashCells !== false) this.flash(flashKeys);
7513
- this.events.emit("transactionApplied", result);
7514
- return result;
7731
+ return next;
7515
7732
  }
7516
7733
  applyTransactionAsync(tx) {
7517
7734
  if (!tx || typeof tx !== "object") return;
@@ -7522,12 +7739,7 @@ var Grid = class {
7522
7739
  const queue = this.txQueue;
7523
7740
  this.txQueue = [];
7524
7741
  if (this.destroyed || !queue.length) return;
7525
- this.applyTransaction({
7526
- add: queue.flatMap((t) => t.add ?? []),
7527
- addIndex: queue.find((t) => typeof t.addIndex === "number")?.addIndex,
7528
- update: queue.flatMap((t) => t.update ?? []),
7529
- remove: queue.flatMap((t) => t.remove ?? [])
7530
- });
7742
+ this.applyTransactions(queue);
7531
7743
  }, 50);
7532
7744
  }
7533
7745
  setAdvancedFilter(filter) {
@@ -7543,6 +7755,8 @@ var Grid = class {
7543
7755
  }
7544
7756
  getState() {
7545
7757
  const s = this.store.state;
7758
+ const groupBy = this.savedGroup?.groupBy ?? this.groupBy;
7759
+ const expanded = this.savedGroup?.expanded ?? this.expanded;
7546
7760
  const state = {
7547
7761
  columns: this.columns.getState(),
7548
7762
  columnGroups: this.columns.groupOpenState(),
@@ -7550,9 +7764,9 @@ var Grid = class {
7550
7764
  sort: s.sort.map((x) => ({ ...x })),
7551
7765
  filters: JSON.parse(JSON.stringify(s.filters)),
7552
7766
  quickFilter: s.quickFilter,
7553
- groupBy: [...this.groupBy],
7767
+ groupBy: [...groupBy],
7554
7768
  pivot: this.pivotCfg ? JSON.parse(JSON.stringify(this.pivotCfg)) : null,
7555
- expanded: Object.fromEntries(this.expanded),
7769
+ expanded: Object.fromEntries(expanded),
7556
7770
  selected: [...s.selected]
7557
7771
  };
7558
7772
  if (this.options.pagination) {
@@ -7583,8 +7797,12 @@ var Grid = class {
7583
7797
  if (want("advancedFilter")) this.setAdvancedFilter(state.advancedFilter ?? null);
7584
7798
  if (want("quickFilter") && typeof state.quickFilter === "string") this.store.set({ quickFilter: state.quickFilter });
7585
7799
  if (want("expanded") && state.expanded && typeof state.expanded === "object") {
7586
- this.expanded = new Map(Object.entries(state.expanded).filter(([, v]) => typeof v === "boolean"));
7587
- this.displayChanged();
7800
+ const expanded = new Map(Object.entries(state.expanded).filter(([, v]) => typeof v === "boolean"));
7801
+ if (this.savedGroup) this.savedGroup.expanded = expanded;
7802
+ else {
7803
+ this.expanded = expanded;
7804
+ this.displayChanged();
7805
+ }
7588
7806
  }
7589
7807
  if (want("pageSize") && typeof state.pageSize === "number") this.setPageSize(state.pageSize);
7590
7808
  if (want("page") && typeof state.page === "number") this.setPage(state.page);
@@ -8108,7 +8326,7 @@ var Grid = class {
8108
8326
  if (all2.length > 1) {
8109
8327
  const sameCols = all2.every((x) => x.c1 === all2[0].c1 && x.c2 === all2[0].c2);
8110
8328
  const sameRows = all2.every((x) => x.r1 === all2[0].r1 && x.r2 === all2[0].r2);
8111
- if (sameCols || sameRows) {
8329
+ if (this.rangesStack(all2)) {
8112
8330
  const parts = [...all2].sort((x, y) => sameCols ? x.r1 - y.r1 : x.c1 - y.c1);
8113
8331
  const blocks = parts.map((p) => this.rangeMatrix(p));
8114
8332
  const matrix2 = sameCols ? blocks.flat() : blocks[0].map((_, ri) => blocks.flatMap((bl) => bl[ri] ?? []));
@@ -8139,6 +8357,10 @@ var Grid = class {
8139
8357
  if (headers) matrix.unshift(cols.map((c) => c.def.header ?? c.def.field ?? c.id));
8140
8358
  return toTSV(matrix);
8141
8359
  }
8360
+ /** Like Excel: ranges stack when they share columns or rows; otherwise only the active one is copied. */
8361
+ rangesStack(all2) {
8362
+ return all2.length > 1 && (all2.every((x) => x.c1 === all2[0].c1 && x.c2 === all2[0].c2) || all2.every((x) => x.r1 === all2[0].r1 && x.r2 === all2[0].r2));
8363
+ }
8142
8364
  rangeHeaders(b) {
8143
8365
  return this.columns.displayed().slice(b.c1, b.c2 + 1).filter((c) => !isSystemCol(c.id)).map((c) => c.def.header ?? c.def.field ?? c.id);
8144
8366
  }
@@ -8154,24 +8376,21 @@ var Grid = class {
8154
8376
  return out;
8155
8377
  }
8156
8378
  copyCell(def, row, code) {
8157
- const value = getCellValue(def, row);
8379
+ return this.copyAllowed(def, row) ? copyValue(def, getCellValue(def, row), code) : "";
8380
+ }
8381
+ copyAllowed(def, row) {
8158
8382
  const rule = def.copyable;
8159
- if (rule === false) return "";
8160
- if (typeof rule === "function") {
8161
- let ok = false;
8162
- try {
8163
- ok = rule({ value, row, column: def }) !== false;
8164
- } catch {
8165
- ok = false;
8166
- }
8167
- if (!ok) return "";
8383
+ if (typeof rule !== "function") return rule !== false;
8384
+ try {
8385
+ return rule({ value: getCellValue(def, row), row, column: def }) !== false;
8386
+ } catch {
8387
+ return false;
8168
8388
  }
8169
- return copyValue(def, value, code);
8170
8389
  }
8171
8390
  /** Copy, then clear the editable cells of the selection. */
8172
8391
  cutToClipboard() {
8173
8392
  const text2 = this.copyToClipboard(false);
8174
- if (text2) this.clearCells();
8393
+ if (text2) this.clearCells(true);
8175
8394
  return text2;
8176
8395
  }
8177
8396
  copyToClipboard(headers, rows = false) {
@@ -8430,10 +8649,14 @@ var Grid = class {
8430
8649
  this.writeCell(changes, node.data, def.field, colId, rowIndex, value);
8431
8650
  this.endCellTx(changes);
8432
8651
  }
8433
- /** Delete/Backspace: clear editable cells in the range, or the focused cell. */
8434
- clearCells() {
8652
+ /** Delete/Backspace: clear editable cells in the range, or the focused cell. `cut` clears only what a copy takes. */
8653
+ clearCells(cut = false) {
8435
8654
  const displayed = this.columns.displayed();
8436
- const all2 = this.allRangeBounds();
8655
+ let all2 = this.allRangeBounds();
8656
+ if (cut && !this.rangesStack(all2)) {
8657
+ const active = this.rangeBounds();
8658
+ all2 = active ? [active] : [];
8659
+ }
8437
8660
  const f = this.store.state.focus;
8438
8661
  const fi = f ? this.columns.getColumn(f.colId)?.index : void 0;
8439
8662
  const boxes = all2.length ? all2 : f && fi !== void 0 ? [{ r1: f.rowIndex, r2: f.rowIndex, c1: fi, c2: fi }] : [];
@@ -8443,7 +8666,7 @@ var Grid = class {
8443
8666
  const node = this.rowModel.getRow(r);
8444
8667
  for (let c = box.c1; c <= box.c2; c++) {
8445
8668
  const def = displayed[c]?.def;
8446
- if (node && !node.kind && def?.field && cellEditable(def, node.data, r)) this.writeCell(changes, node.data, def.field, displayed[c].id, r, null);
8669
+ 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);
8447
8670
  }
8448
8671
  }
8449
8672
  this.endCellTx(changes);
@@ -8545,8 +8768,9 @@ var Grid = class {
8545
8768
  const m = this.getModifiedRows();
8546
8769
  return m.created.length + m.updated.length + m.deleted.length > 0;
8547
8770
  }
8548
- commitChanges() {
8549
- this.tracker.reset();
8771
+ commitChanges(saved) {
8772
+ if (saved && saved.rows instanceof Map) this.tracker.commit(saved.rows, Array.isArray(saved.deleted) ? saved.deleted : []);
8773
+ else this.tracker.reset();
8550
8774
  this.renderer.invalidate({ rows: true });
8551
8775
  }
8552
8776
  revertChanges() {
@@ -8950,7 +9174,7 @@ var Grid = class {
8950
9174
  getModifiedRows: guard(() => this.getModifiedRows()),
8951
9175
  getChanges: guard(() => this.getChanges()),
8952
9176
  hasChanges: guard(() => this.hasChanges()),
8953
- commitChanges: guard(() => this.commitChanges()),
9177
+ commitChanges: guard((saved) => this.commitChanges(saved)),
8954
9178
  revertChanges: guard(() => this.revertChanges()),
8955
9179
  undo: guard(() => this.undo()),
8956
9180
  redo: guard(() => this.redo()),
@@ -9021,26 +9245,32 @@ function exportWorkbook(filename, sheets) {
9021
9245
  var headersOf = (h) => typeof h === "function" ? h() : h ?? {};
9022
9246
  async function send(url, method, body, opts) {
9023
9247
  let target = url;
9248
+ const where = url.split(/[?#]/)[0];
9024
9249
  const init = { method, credentials: opts.credentials ?? "same-origin", headers: { Accept: "application/json", ...headersOf(opts.headers) } };
9025
9250
  if (method === "GET") {
9026
9251
  const q = new URLSearchParams();
9027
- for (const [k, v] of Object.entries(body)) q.set(k, typeof v === "string" ? v : JSON.stringify(v));
9252
+ for (const [k, v] of Object.entries(body)) if (v !== void 0) q.set(k, typeof v === "string" ? v : JSON.stringify(v));
9028
9253
  target += (url.includes("?") ? "&" : "?") + q.toString();
9029
9254
  } else {
9030
9255
  init.headers["Content-Type"] = "application/json";
9031
9256
  init.body = JSON.stringify(body);
9032
9257
  }
9033
9258
  const res = await fetch(target, init);
9034
- if (!res.ok) throw new Error(`HTTP ${res.status} from ${url}`);
9259
+ if (!res.ok) throw new Error(`HTTP ${res.status} from ${where}`);
9035
9260
  const text2 = await res.text();
9036
- return text2 ? JSON.parse(text2) : null;
9261
+ if (!text2) return null;
9262
+ try {
9263
+ return JSON.parse(text2);
9264
+ } catch {
9265
+ throw new Error(`HTTP ${res.status} from ${where}: the response is not JSON`);
9266
+ }
9037
9267
  }
9038
9268
  function readRows(json) {
9039
9269
  if (Array.isArray(json)) return { rows: json };
9040
9270
  const o = json ?? {};
9041
9271
  const rows = [o.rows, o.list, o.content, o.data, o.items, o.resultList].find(Array.isArray);
9042
9272
  const total = [o.total, o.totalCount, o.totalElements, o.totCnt, o.count].find((v) => typeof v === "number");
9043
- return { rows: rows ?? [], total };
9273
+ return { rows: (rows ?? []).filter((r) => r !== null && typeof r === "object"), total };
9044
9274
  }
9045
9275
  function restDatasource(url, options = {}) {
9046
9276
  return {
@@ -9054,6 +9284,10 @@ function restDatasource(url, options = {}) {
9054
9284
  sort: p.sort,
9055
9285
  filters: p.filters,
9056
9286
  quickFilter: p.quickFilter,
9287
+ // only when set (undefined is left out of the request)
9288
+ advancedFilter: p.advancedFilter ?? void 0,
9289
+ groupBy: p.groupBy,
9290
+ groupKeys: p.groupKeys,
9057
9291
  ...options.params?.() ?? {}
9058
9292
  };
9059
9293
  const json = await send(url, options.method ?? "POST", body, options);
@@ -9061,12 +9295,20 @@ function restDatasource(url, options = {}) {
9061
9295
  }
9062
9296
  };
9063
9297
  }
9064
- async function saveChanges(grid, url, options = {}) {
9065
- if (!grid.hasChanges()) return null;
9298
+ var saving = /* @__PURE__ */ new WeakMap();
9299
+ function saveChanges(grid, url, options = {}) {
9300
+ const pending = saving.get(grid);
9301
+ if (pending) return pending;
9302
+ if (!grid.hasChanges()) return Promise.resolve(null);
9066
9303
  const { created, updated, deleted } = grid.getModifiedRows();
9067
- const json = await send(url, "POST", { created, updated, deleted, ...options.params?.() ?? {} }, options);
9068
- grid.commitChanges();
9069
- return json;
9304
+ const rows = new Map([...created, ...updated].map((r) => [r, { ...r }]));
9305
+ const run = (async () => {
9306
+ const json = await send(url, "POST", { created, updated, deleted, ...options.params?.() ?? {} }, options);
9307
+ grid.commitChanges({ rows, deleted });
9308
+ return json;
9309
+ })().finally(() => saving.delete(grid));
9310
+ saving.set(grid, run);
9311
+ return run;
9070
9312
  }
9071
9313
  async function loadData(grid, url, options = {}) {
9072
9314
  const { rows } = await restDatasource(url, options).getRows({ start: 0, end: options.maxRows ?? 1e4, sort: [], filters: {}, quickFilter: "" });