@freegridjs/core 1.0.0 → 1.1.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
@@ -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
  });
@@ -2554,6 +2582,7 @@ var en = {
2554
2582
  clearFilters: "Clear all filters",
2555
2583
  exportCsv: "Export CSV",
2556
2584
  exportExcel: "Export Excel",
2585
+ print: "Print\u2026",
2557
2586
  pivot: "Pivot",
2558
2587
  pivotMode: "Pivot mode",
2559
2588
  pivotRows: "Row groups",
@@ -3075,7 +3104,7 @@ function attachBody(grid) {
3075
3104
  if (!text2 || !e.clipboardData) return;
3076
3105
  e.clipboardData.setData("text/plain", text2);
3077
3106
  e.preventDefault();
3078
- grid.clearCells();
3107
+ grid.clearCells(true);
3079
3108
  };
3080
3109
  const onPaste = (e) => {
3081
3110
  if (fromField(e) || nested(e)) return;
@@ -3615,6 +3644,7 @@ var ContextMenu = class {
3615
3644
  { label: loc.exportExcel, action: () => grid.api.exportXlsx() }
3616
3645
  );
3617
3646
  if (grid.options.advancedFilter !== false && !grid.options.pivot) items.push("separator", { label: `${loc.advancedFilter}\u2026`, action: () => grid.filterBuilder.open() });
3647
+ items.push("separator", { label: loc.print, action: () => grid.api.print() });
3618
3648
  return items;
3619
3649
  }
3620
3650
  pasteFromClipboard() {
@@ -4144,7 +4174,7 @@ var CellEditor = class {
4144
4174
  beginRow(row, rowIndex) {
4145
4175
  if (this.session?.row === row) return;
4146
4176
  if (this.session) this.endRow(true);
4147
- this.session = { row, id: this.grid.rowModel.idOf(row), before: { ...row } };
4177
+ this.session = { row, id: this.grid.rowModel.idOf(row), before: { ...row }, undoMark: this.grid.history.mark() };
4148
4178
  this.grid.renderer.invalidate({ rows: true });
4149
4179
  this.grid.events.emit("rowEditStart", { row, rowIndex });
4150
4180
  }
@@ -4153,8 +4183,8 @@ var CellEditor = class {
4153
4183
  const s = this.session;
4154
4184
  if (!s) return;
4155
4185
  if (this.cur) {
4156
- if (commit) this.commit(null);
4157
- else this.close(false);
4186
+ if (commit && !this.commit(null)) return;
4187
+ if (!commit) this.close(false);
4158
4188
  }
4159
4189
  this.session = null;
4160
4190
  const { grid } = this;
@@ -4167,6 +4197,7 @@ var CellEditor = class {
4167
4197
  if (!Object.is(s.row[f], s.before[f])) grid.writeCell(changes2, s.row, f, c.id, rowIndex, s.before[f]);
4168
4198
  }
4169
4199
  grid.endCellTx(changes2);
4200
+ grid.history.rollback(s.undoMark);
4170
4201
  }
4171
4202
  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
4203
  grid.renderer.invalidate({ rows: true });
@@ -4183,7 +4214,7 @@ var CellEditor = class {
4183
4214
  return !node.kind && cellEditable(col.def, node.data, rowIndex);
4184
4215
  }
4185
4216
  start(rowIndex, colId, initialText) {
4186
- if (this.cur) this.commit(null);
4217
+ if (this.cur && !this.commit(null)) return false;
4187
4218
  if (!this.canEdit(rowIndex, colId)) return false;
4188
4219
  const { grid } = this;
4189
4220
  if (grid.veto("beforeEdit", { row: grid.rowModel.getRow(rowIndex).data, rowIndex, colId })) return false;
@@ -4196,14 +4227,8 @@ var CellEditor = class {
4196
4227
  grid.setFocus(rowIndex, colId);
4197
4228
  renderer.ensureVisible(rowIndex, colId);
4198
4229
  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
4230
  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`;
4231
+ this.place(wrap, rowIndex, colId);
4207
4232
  let control;
4208
4233
  let read;
4209
4234
  let dispose = () => {
@@ -4301,6 +4326,36 @@ var CellEditor = class {
4301
4326
  grid.events.emit("editStart", { rowIndex, colId });
4302
4327
  return true;
4303
4328
  }
4329
+ /** Put the editor over its cell; false when the cell is not rendered. */
4330
+ place(wrap, rowIndex, colId) {
4331
+ const { renderer } = this.grid;
4332
+ const cellEl = renderer.root.querySelector(`.fg-row[data-row="${rowIndex}"] .fg-cell[data-col="${CSS.escape(colId)}"]`);
4333
+ const rootBox = renderer.root.getBoundingClientRect();
4334
+ const box = cellEl?.getBoundingClientRect();
4335
+ wrap.style.left = `${(box?.left ?? rootBox.left) - rootBox.left}px`;
4336
+ wrap.style.top = `${(box?.top ?? rootBox.top) - rootBox.top}px`;
4337
+ wrap.style.width = `${box?.width || this.grid.columns.getColumn(colId)?.width}px`;
4338
+ wrap.style.height = `${box?.height || renderer.opts.rowHeight}px`;
4339
+ return !!cellEl;
4340
+ }
4341
+ /** Display index of a row object, or -1 when it is no longer shown. */
4342
+ indexOf(row) {
4343
+ const m = this.grid.rowModel;
4344
+ const i = m.indexOfId(m.idOf(row));
4345
+ return i >= 0 && m.getRow(i)?.data === row ? i : -1;
4346
+ }
4347
+ /** The rows changed underneath (transactions): follow the edited row, or cancel when it is gone. */
4348
+ sync() {
4349
+ const cur = this.cur;
4350
+ if (!cur) return;
4351
+ const i = this.indexOf(cur.row);
4352
+ if (i < 0) return this.cancel();
4353
+ if (i === cur.rowIndex) return;
4354
+ cur.rowIndex = i;
4355
+ this.grid.setFocus(i, cur.colId);
4356
+ this.grid.renderer.render();
4357
+ if (!this.place(cur.wrap, i, cur.colId)) this.commit(null);
4358
+ }
4304
4359
  /** Validate and write the value. Returns false (editor stays open) when invalid. */
4305
4360
  commit(move) {
4306
4361
  const cur = this.cur;
@@ -4320,18 +4375,30 @@ var CellEditor = class {
4320
4375
  } else value = raw.value;
4321
4376
  const error = grid.cellError(cur.def, cur.row, value);
4322
4377
  if (error !== null) return this.fail(value, error);
4378
+ const rowIndex = this.indexOf(cur.row);
4379
+ if (rowIndex < 0) {
4380
+ this.cancel();
4381
+ return false;
4382
+ }
4383
+ cur.rowIndex = rowIndex;
4323
4384
  this.close(true);
4324
- grid.applyEdit(cur.rowIndex, cur.colId, value);
4385
+ grid.applyEdit(rowIndex, cur.colId, value);
4325
4386
  if (this.session && (move === "down" || move === "up")) {
4326
4387
  this.endRow(true);
4327
4388
  }
4328
- this.move(cur.rowIndex, cur.colId, move);
4389
+ this.move(rowIndex, cur.colId, move);
4329
4390
  return true;
4330
4391
  }
4331
4392
  cancel() {
4332
4393
  this.close(false);
4333
4394
  if (this.session) this.endRow(false);
4334
4395
  }
4396
+ /** Teardown: remove the editor without writing values or emitting events. */
4397
+ destroy() {
4398
+ this.cur?.dispose();
4399
+ this.cur = null;
4400
+ this.session = null;
4401
+ }
4335
4402
  fail(value, message) {
4336
4403
  const cur = this.cur;
4337
4404
  cur.wrap.classList.add("fg-editor-invalid");
@@ -4404,6 +4471,16 @@ var History = class {
4404
4471
  this.entries[this.index++].redo();
4405
4472
  return true;
4406
4473
  }
4474
+ /** The current position, for rollback(). */
4475
+ mark() {
4476
+ return this.entries[this.index - 1] ?? null;
4477
+ }
4478
+ /** Forget every step pushed after `mark` (a cancelled edit session). */
4479
+ rollback(mark) {
4480
+ const i = mark ? this.entries.indexOf(mark) + 1 : 0;
4481
+ this.entries.splice(i);
4482
+ this.index = Math.min(this.index, i);
4483
+ }
4407
4484
  clear() {
4408
4485
  this.entries = [];
4409
4486
  this.index = 0;
@@ -4432,6 +4509,14 @@ var ChangeTracker = class {
4432
4509
  const o = this.originals.get(row);
4433
4510
  return o ? fields.filter((f) => !same(o[f], row[f])) : [];
4434
4511
  }
4512
+ /** Baseline `rows` (row → the values that were saved); `deleted` rows are no longer pending. */
4513
+ commit(rows, deleted) {
4514
+ for (const r of deleted) this.deleted.delete(r);
4515
+ for (const [row, values] of rows) {
4516
+ this.created.delete(row);
4517
+ this.originals.set(row, { ...values });
4518
+ }
4519
+ }
4435
4520
  get size() {
4436
4521
  return this.originals.size + this.created.size + this.deleted.size;
4437
4522
  }
@@ -4594,6 +4679,58 @@ function attachHeader(grid) {
4594
4679
  };
4595
4680
  }
4596
4681
 
4682
+ // src/rows/sort.ts
4683
+ function compareNonBlank(a, b, collator) {
4684
+ if (typeof a === "number" && typeof b === "number") return a - b;
4685
+ if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime();
4686
+ if (typeof a === "boolean" && typeof b === "boolean") return Number(a) - Number(b);
4687
+ return collator.compare(String(a), String(b));
4688
+ }
4689
+ function normalize(def, v) {
4690
+ if (isBlank(v)) return null;
4691
+ if (def.type === "number") {
4692
+ const n = typeof v === "number" ? v : Number(v);
4693
+ return Number.isNaN(n) ? null : n;
4694
+ }
4695
+ if (def.type === "date") return toDate(v)?.getTime() ?? null;
4696
+ return v;
4697
+ }
4698
+ function compareValues(a, b, collator) {
4699
+ const ba = isBlank(a), bb = isBlank(b);
4700
+ if (ba || bb) return ba === bb ? 0 : ba ? 1 : -1;
4701
+ return compareNonBlank(a, b, collator);
4702
+ }
4703
+ function sortNodes(nodes, sort, resolve, collator) {
4704
+ const keys = sort.map((s) => ({ def: resolve(s.colId), mul: s.dir === "desc" ? -1 : 1 })).filter((k) => !!k.def);
4705
+ if (!keys.length) return nodes;
4706
+ const values = keys.map((k) => nodes.map((n) => {
4707
+ const v = getCellValue(k.def, n.data);
4708
+ return k.def.comparator ? v : normalize(k.def, v);
4709
+ }));
4710
+ const order = nodes.map((_, i) => i);
4711
+ order.sort((i, j) => {
4712
+ for (let k = 0; k < keys.length; k++) {
4713
+ const { def, mul } = keys[k];
4714
+ const a = values[k][i], b = values[k][j];
4715
+ let c;
4716
+ if (def.comparator) {
4717
+ try {
4718
+ c = (Number(def.comparator(a, b, nodes[i].data, nodes[j].data)) || 0) * mul;
4719
+ } catch {
4720
+ warn(`comparator of column "${def.id ?? def.field}" threw; treating values as equal`);
4721
+ c = 0;
4722
+ }
4723
+ } else {
4724
+ const ba = isBlank(a), bb = isBlank(b);
4725
+ c = ba || bb ? ba === bb ? 0 : ba ? 1 : -1 : compareNonBlank(a, b, collator) * mul;
4726
+ }
4727
+ if (c) return c;
4728
+ }
4729
+ return i - j;
4730
+ });
4731
+ return order.map((i) => nodes[i]);
4732
+ }
4733
+
4597
4734
  // src/export/csv.ts
4598
4735
  var FORMULA = /^[=+\-@\t\r]/;
4599
4736
  function toCsv(rows, sep = ",") {
@@ -4692,12 +4829,34 @@ function zipStore(files) {
4692
4829
  return out;
4693
4830
  }
4694
4831
  var isZip = (data) => data.length > 4 && data[0] === 80 && data[1] === 75 && data[2] === 3 && data[3] === 4;
4695
- async function inflateRaw(data) {
4832
+ var tooLarge = (max) => new Error(`Zip file is too large: it unpacks to more than ${max >= 1048576 ? `${Math.round(max / 1048576)} MB` : `${max} bytes`}`);
4833
+ async function inflateRaw(data, max) {
4696
4834
  if (typeof DecompressionStream === "undefined") throw new Error("This browser cannot read compressed xlsx files (no DecompressionStream)");
4697
4835
  const stream = new Response(data).body.pipeThrough(new DecompressionStream("deflate-raw"));
4698
- return new Uint8Array(await new Response(stream).arrayBuffer());
4836
+ const reader = stream.getReader();
4837
+ const chunks = [];
4838
+ let size = 0;
4839
+ for (; ; ) {
4840
+ const { done, value } = await reader.read();
4841
+ if (done) break;
4842
+ size += value.length;
4843
+ if (size > max) {
4844
+ await reader.cancel().catch(() => void 0);
4845
+ throw tooLarge(max);
4846
+ }
4847
+ chunks.push(value);
4848
+ }
4849
+ const out = new Uint8Array(size);
4850
+ let at = 0;
4851
+ for (const c of chunks) {
4852
+ out.set(c, at);
4853
+ at += c.length;
4854
+ }
4855
+ return out;
4699
4856
  }
4700
- async function unzip(data) {
4857
+ async function unzip(data, limits = {}) {
4858
+ const maxBytes = limits.maxBytes ?? 200 * 1048576;
4859
+ const maxEntries = limits.maxEntries ?? 1e4;
4701
4860
  const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
4702
4861
  let eocd = -1;
4703
4862
  for (let i = data.length - 22; i >= Math.max(0, data.length - 65557); i--) {
@@ -4708,9 +4867,11 @@ async function unzip(data) {
4708
4867
  }
4709
4868
  if (eocd < 0) throw new Error("Not a zip file");
4710
4869
  const count = view.getUint16(eocd + 10, true);
4870
+ if (count > maxEntries) throw new Error(`Zip file has too many entries (${count}, at most ${maxEntries})`);
4711
4871
  let p = view.getUint32(eocd + 16, true);
4712
4872
  const decoder = new TextDecoder();
4713
4873
  const out = /* @__PURE__ */ new Map();
4874
+ let total = 0;
4714
4875
  for (let i = 0; i < count; i++) {
4715
4876
  if (view.getUint32(p, true) !== 33639248) throw new Error("Broken zip directory");
4716
4877
  const method = view.getUint16(p + 10, true);
@@ -4721,9 +4882,13 @@ async function unzip(data) {
4721
4882
  const lName = view.getUint16(offset + 26, true), lExtra = view.getUint16(offset + 28, true);
4722
4883
  const start = offset + 30 + lName + lExtra;
4723
4884
  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));
4885
+ let bytes;
4886
+ if (method === 0) bytes = raw;
4887
+ else if (method === 8) bytes = await inflateRaw(raw, maxBytes - total);
4726
4888
  else throw new Error(`Unsupported zip compression ${method}`);
4889
+ total += bytes.length;
4890
+ if (total > maxBytes) throw tooLarge(maxBytes);
4891
+ out.set(name, bytes);
4727
4892
  p += 46 + nameLen + extraLen + commentLen;
4728
4893
  }
4729
4894
  return out;
@@ -4844,7 +5009,8 @@ function writeXlsx(sheets) {
4844
5009
  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
5010
  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
5011
  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>` : "";
5012
+ 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));
5013
+ const merges = mergeRefs.length ? `<mergeCells count="${mergeRefs.length}">${mergeRefs.map((m) => `<mergeCell ref="${m}"/>`).join("")}</mergeCells>` : "";
4848
5014
  return `${XML}<worksheet xmlns="${NS}" xmlns:r="${REL_NS}">${views}<sheetFormatPr defaultRowHeight="15"/>${cols}<sheetData>${rowsXml}</sheetData>${filter}${merges}</worksheet>`;
4849
5015
  });
4850
5016
  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 +5058,8 @@ function colIndex(ref) {
4892
5058
  }
4893
5059
  return n - 1;
4894
5060
  }
5061
+ var MAX_ROWS = 1048576;
5062
+ var MAX_COLS = 16384;
4895
5063
  async function readXlsx(input) {
4896
5064
  const data = input instanceof Uint8Array ? input : new Uint8Array(input);
4897
5065
  if (!isZip(data)) throw new Error("Not an xlsx file");
@@ -4919,11 +5087,13 @@ async function readXlsx(input) {
4919
5087
  if (doc) {
4920
5088
  for (const row of tags(doc, "row")) {
4921
5089
  const r = Number(row.getAttribute("r") ?? rows.length + 1) - 1;
5090
+ if (!Number.isInteger(r) || r < 0 || r >= MAX_ROWS) continue;
4922
5091
  const out = [];
4923
5092
  let next = 0;
4924
5093
  for (const c of [...row.children].filter((x) => x.localName === "c")) {
4925
5094
  const ref = c.getAttribute("r");
4926
5095
  const ci = ref ? colIndex(ref) : next;
5096
+ if (ci < 0 || ci >= MAX_COLS) continue;
4927
5097
  next = ci + 1;
4928
5098
  const t = c.getAttribute("t");
4929
5099
  const v = tags(c, "v")[0]?.textContent ?? null;
@@ -4968,11 +5138,71 @@ function exportRows(grid, mode) {
4968
5138
  return grid.rowModel.dataRows().map((n) => n.data);
4969
5139
  }
4970
5140
  var header = (def, id) => def.header ?? def.field ?? id;
5141
+ var ALIGNS = ["left", "center", "right"];
5142
+ var PLAIN_NUMBER = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i;
4971
5143
  var escHtml = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
5144
+ var PAPERS = ["A4", "A3", "A5", "B4", "B5", "letter", "legal"];
5145
+ var PORTRAIT_WIDTH = 760;
5146
+ var cssStr = (s) => `"${s.replace(/[\\"<\n\r]/g, (c) => c === "\n" ? "\\A " : c === "\r" ? "" : `\\${c.charCodeAt(0).toString(16)} `)}"`;
5147
+ var hexOf = (argb) => argb ? `#${argb.slice(2)}` : "";
5148
+ function printLines(grid, rows, groupBy) {
5149
+ const out = [];
5150
+ let n = 0;
5151
+ const sort = grid.store.state.sort;
5152
+ const collator = grid.collator();
5153
+ const walk = (list, level) => {
5154
+ if (level >= groupBy.length) {
5155
+ for (const row of list) out.push({ kind: "row", row, n: ++n });
5156
+ return;
5157
+ }
5158
+ const colId = groupBy[level];
5159
+ const def = grid.columns.getNode(colId)?.def;
5160
+ if (!def) return walk(list, level + 1);
5161
+ const buckets = /* @__PURE__ */ new Map();
5162
+ for (const r of list) {
5163
+ const v = getCellValue(def, r);
5164
+ const k = isBlank(v) ? "" : setKey(v);
5165
+ let b = buckets.get(k);
5166
+ if (!b) buckets.set(k, b = { value: v, rows: [] });
5167
+ b.rows.push(r);
5168
+ }
5169
+ const dir = sort.find((x) => x.colId === colId)?.dir === "desc" ? -1 : 1;
5170
+ for (const b of [...buckets.values()].sort((a, c) => compareValues(a.value, c.value, collator) * dir)) {
5171
+ out.push({ kind: "group", colId, value: b.value, rows: b.rows, level });
5172
+ walk(b.rows, level + 1);
5173
+ }
5174
+ };
5175
+ walk(rows, 0);
5176
+ return out;
5177
+ }
5178
+ var PRINT_CSS = `
5179
+ * { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
5180
+ body { font: 10.5px/1.45 system-ui, 'Pretendard', 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif; color: #111827; margin: 0; }
5181
+ header { display: flex; justify-content: space-between; align-items: flex-end; gap: 16px; margin: 0 0 10px; padding-bottom: 8px; border-bottom: 2px solid #111827; }
5182
+ h1 { font-size: 17px; margin: 0; letter-spacing: -.01em; }
5183
+ .sub { margin: 3px 0 0; color: #4b5563; font-size: 11px; }
5184
+ .printed { margin: 0; color: #6b7280; font-size: 9.5px; white-space: nowrap; }
5185
+ table { border-collapse: collapse; width: 100%; }
5186
+ table.fit { table-layout: fixed; }
5187
+ th, td { padding: 4px 7px; border-bottom: 1px solid #e5e7eb; vertical-align: top; overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
5188
+ th { background: #f3f4f6; color: #374151; font-weight: 600; text-align: left; vertical-align: bottom; border-bottom: 1.5px solid #9ca3af; border-left: 1px solid #e5e7eb; }
5189
+ th.span { text-align: center; }
5190
+ th:first-child { border-left: 0; }
5191
+ thead { display: table-header-group; }
5192
+ tr { break-inside: avoid; page-break-inside: avoid; }
5193
+ .num { text-align: right; color: #9ca3af; }
5194
+ tr.group td { background: #eef2f7; font-weight: 600; border-top: 1px solid #cbd5e1; break-after: avoid; page-break-after: avoid; }
5195
+ tr.group.level-0 td { background: #e2e8f0; }
5196
+ tr.total td { font-weight: 700; background: #f3f4f6; border-top: 1.5px solid #111827; border-bottom: 0; }
5197
+ `;
4972
5198
  function buildPrintHtml(grid, opts = {}) {
4973
5199
  const cols = exportColumns(grid, opts.columns);
4974
- const code = grid.store.state.locale.code;
5200
+ const loc = grid.store.state.locale;
5201
+ const code = loc.code;
4975
5202
  const fmts = cols.map((c) => getFormatter(c.def, code));
5203
+ const nf = new Intl.NumberFormat(code);
5204
+ const numbered = opts.rowNumbers ?? !!grid.options.rowNumbers;
5205
+ const align = (def) => ALIGNS.includes(def.align) ? def.align : def.type === "number" ? "right" : def.type === "boolean" ? "center" : "left";
4976
5206
  const chains = cols.map((c) => {
4977
5207
  const chain = [];
4978
5208
  for (let n = c.node; n; n = n.parent) chain.unshift(n);
@@ -4981,7 +5211,7 @@ function buildPrintHtml(grid, opts = {}) {
4981
5211
  const depth = Math.max(1, ...chains.map((ch) => ch.length));
4982
5212
  const head = [];
4983
5213
  for (let level = 0; level < depth; level++) {
4984
- let tr = "";
5214
+ let tr = level === 0 && numbered ? `<th class="num" rowspan="${depth}">#</th>` : "";
4985
5215
  for (let ci = 0; ci < cols.length; ci++) {
4986
5216
  const chain = chains[ci];
4987
5217
  const leaf = chain.length - 1;
@@ -4990,29 +5220,108 @@ function buildPrintHtml(grid, opts = {}) {
4990
5220
  if (ci > 0 && chains[ci - 1][level] === g) continue;
4991
5221
  let span = 1;
4992
5222
  while (ci + span < cols.length && chains[ci + span][level] === g) span++;
4993
- tr += `<th colspan="${span}">${escHtml(header(g.def, g.id))}</th>`;
5223
+ tr += `<th colspan="${span}"${span > 1 ? ' class="span"' : ""}>${escHtml(header(g.def, g.id))}</th>`;
4994
5224
  } else if (level === leaf) {
4995
- tr += `<th colspan="1" rowspan="${depth - level}">${escHtml(header(cols[ci].def, cols[ci].id))}</th>`;
5225
+ const a = align(cols[ci].def);
5226
+ tr += `<th colspan="1" rowspan="${depth - level}"${a === "left" ? "" : ` style="text-align:${a}"`}>${escHtml(header(cols[ci].def, cols[ci].id))}</th>`;
4996
5227
  }
4997
5228
  }
4998
5229
  head.push(`<tr>${tr}</tr>`);
4999
5230
  }
5000
- const align = (def) => def.align ?? (def.type === "number" ? "right" : def.type === "boolean" ? "center" : "left");
5001
- const body = exportRows(grid, opts.rows).map((r) => `<tr>${cols.map((c, i) => {
5002
- const v = getCellValue(c.def, r);
5003
- return `<td style="text-align:${align(c.def)}">${escHtml(isBlank(v) ? "" : fmts[i](v))}</td>`;
5004
- }).join("")}</tr>`).join("");
5231
+ const aggText = (c, i, v, agg) => {
5232
+ if (v == null || v === "") return "";
5233
+ if (agg === "count") return nf.format(v);
5234
+ const type = c.def.type ?? (typeof v === "number" ? "number" : v instanceof Date ? "date" : void 0);
5235
+ return type === c.def.type ? fmts[i](v) : getFormatter({ ...c.def, type }, code)(v);
5236
+ };
5237
+ const labelled = (cls, label, values, indent = 0) => {
5238
+ let lead = 0;
5239
+ while (lead < cols.length - 1 && !values[lead]) lead++;
5240
+ lead = Math.max(1, lead);
5241
+ let tr = numbered ? '<td class="num"></td>' : "";
5242
+ tr += `<td colspan="${lead}"${indent ? ` style="padding-left:${7 + indent * 14}px"` : ""}>${escHtml(label)}</td>`;
5243
+ for (let i = lead; i < cols.length; i++) tr += `<td style="text-align:${align(cols[i].def)}">${escHtml(values[i] ?? "")}</td>`;
5244
+ return `<tr class="${cls}">${tr}</tr>`;
5245
+ };
5246
+ const rows = exportRows(grid, opts.rows);
5247
+ const groupBy = opts.groups !== false && !grid.rowModel.server && !grid.options.treeData ? grid.getGroupBy() : [];
5248
+ const probe = opts.styles !== false && cols.some((c) => c.def.cellClass) ? classStyles(grid) : null;
5249
+ let body = "";
5250
+ try {
5251
+ for (const line of printLines(grid, rows, groupBy)) {
5252
+ if (line.kind === "group") {
5253
+ const def = grid.columns.getNode(line.colId)?.def;
5254
+ const value = isBlank(line.value) ? loc.blanks : def ? getFormatter(def, code)(line.value) : String(line.value);
5255
+ const label = `${def ? header(def, line.colId) : line.colId}: ${value} (${nf.format(line.rows.length)})`;
5256
+ const values = cols.map((c, i) => {
5257
+ if (!c.def.aggFunc || groupBy.includes(c.id)) return void 0;
5258
+ try {
5259
+ return aggText(c, i, aggregate(c.def.aggFunc, line.rows.map((r2) => getCellValue(c.def, r2)), line.rows), c.def.aggFunc) || void 0;
5260
+ } catch {
5261
+ return void 0;
5262
+ }
5263
+ });
5264
+ body += labelled(`group level-${Math.min(line.level, 5)}`, label, values, line.level);
5265
+ continue;
5266
+ }
5267
+ const r = line.row;
5268
+ let tr = numbered ? `<td class="num">${line.n}</td>` : "";
5269
+ cols.forEach((c, i) => {
5270
+ const v = getCellValue(c.def, r);
5271
+ const text2 = isBlank(v) ? "" : fmts[i](v);
5272
+ let style = `text-align:${align(c.def)}`;
5273
+ if (probe && c.def.cellClass) {
5274
+ let cls;
5275
+ try {
5276
+ cls = typeof c.def.cellClass === "function" ? c.def.cellClass({ value: v, formatted: text2, row: r, rowIndex: line.n - 1, column: c.def }) : c.def.cellClass;
5277
+ } catch {
5278
+ cls = void 0;
5279
+ }
5280
+ if (cls) {
5281
+ const st = probe.of(cls);
5282
+ if (st.color) style += `;color:${hexOf(st.color)}`;
5283
+ if (st.fill) style += `;background:${hexOf(st.fill)}`;
5284
+ if (st.bold) style += ";font-weight:600";
5285
+ }
5286
+ }
5287
+ tr += `<td style="${style}">${escHtml(text2)}</td>`;
5288
+ });
5289
+ body += `<tr>${tr}</tr>`;
5290
+ }
5291
+ } finally {
5292
+ probe?.dispose();
5293
+ }
5294
+ let foot = "";
5295
+ const summary = opts.summary !== false ? grid.summary() : null;
5296
+ if (summary) {
5297
+ summary.opts.rows.forEach((def, ri) => {
5298
+ const values = cols.map((c, i) => {
5299
+ const spec = def.columns?.[c.id];
5300
+ return spec === void 0 ? void 0 : aggText(c, i, summary.values[ri]?.[c.id], cellSpec(spec).agg) || void 0;
5301
+ });
5302
+ foot += labelled("total", def.label ?? "", values);
5303
+ });
5304
+ }
5305
+ const widths = cols.map((c) => Math.max(1, c.width));
5306
+ const sum = widths.reduce((a, b) => a + b, 0);
5307
+ const paper = PAPERS.includes(opts.paper) ? opts.paper : "A4";
5308
+ const orientation = opts.orientation === "portrait" || opts.orientation === "landscape" ? opts.orientation : sum + (numbered ? 40 : 0) > PORTRAIT_WIDTH ? "landscape" : "portrait";
5309
+ const fit = opts.fitToWidth !== false;
5310
+ const colgroup = fit ? `<colgroup>${numbered ? '<col style="width:40px">' : ""}${widths.map((w) => `<col style="width:${Math.round(w / sum * 1e3) / 10}%">`).join("")}</colgroup>` : "";
5005
5311
  const title = escHtml(opts.title ?? document.title ?? "");
5312
+ const printed = opts.printedAt !== false ? new Intl.DateTimeFormat(code, { dateStyle: "medium", timeStyle: "short" }).format(/* @__PURE__ */ new Date()) : "";
5313
+ const heading = opts.title || opts.subtitle ? `<div>${opts.title ? `<h1>${title}</h1>` : ""}${opts.subtitle ? `<p class="sub">${escHtml(opts.subtitle)}</p>` : ""}</div>` : "<div></div>";
5314
+ const top = opts.title || opts.subtitle || printed ? `<header>${heading}${printed ? `<p class="printed">${escHtml(printed)}</p>` : ""}</header>` : "";
5315
+ const small = "font: 8.5px system-ui, sans-serif; color: #6b7280;";
5316
+ const page = [
5317
+ `size: ${paper} ${orientation}; margin: 12mm 12mm 14mm;`,
5318
+ opts.footer ? `@bottom-left { content: ${cssStr(opts.footer)}; ${small} }` : "",
5319
+ opts.pageNumbers !== false ? `@bottom-right { content: counter(page) " / " counter(pages); ${small} }` : ""
5320
+ ].join(" ");
5321
+ const stripes = opts.striped !== false ? "tbody tr:not(.group):nth-child(even) td { background: #f9fafb; }" : "";
5006
5322
  return `<!doctype html><html lang="${escHtml(code)}"><head><meta charset="utf-8"><title>${title}</title><style>
5007
- @page { size: auto; margin: 12mm; }
5008
- body { font: 11px/1.4 system-ui, 'Pretendard', 'Malgun Gothic', sans-serif; color: #111; margin: 0; }
5009
- h1 { font-size: 15px; margin: 0 0 8px; }
5010
- table { border-collapse: collapse; width: 100%; }
5011
- th, td { border: 1px solid #999; padding: 3px 6px; }
5012
- th { background: #eee; font-weight: 600; }
5013
- thead { display: table-header-group; }
5014
- tr { page-break-inside: avoid; }
5015
- </style></head><body>${opts.title ? `<h1>${title}</h1>` : ""}<table><thead>${opts.headers === false ? "" : head.join("")}</thead><tbody>${body}</tbody></table></body></html>`;
5323
+ @page { ${page} }${PRINT_CSS}${stripes}
5324
+ </style></head><body>${top}<table${fit ? ' class="fit"' : ""}>${colgroup}<thead>${opts.headers === false ? "" : head.join("")}</thead><tbody>${body}</tbody>${foot ? `<tfoot>${foot}</tfoot>` : ""}</table></body></html>`;
5016
5325
  }
5017
5326
  function printHtml(html) {
5018
5327
  if (typeof document === "undefined") return;
@@ -5048,7 +5357,8 @@ function buildCsv(grid, opts = {}) {
5048
5357
  const matrix = exportRows(grid, opts.rows).map((r) => cols.map((c) => {
5049
5358
  const v = getCellValue(c.def, r);
5050
5359
  const t = c.def.type;
5051
- return { value: copyValue(c.def, v, code), text: !(t === "number" || t === "date" || t === "boolean") || typeof v === "string" };
5360
+ const value = copyValue(c.def, v, code);
5361
+ return { value, text: !(t === "number" || t === "date" || t === "boolean") || typeof v === "string" && !PLAIN_NUMBER.test(value) };
5052
5362
  }));
5053
5363
  if (opts.headers !== false) matrix.unshift(cols.map((c) => ({ value: header(c.def, c.id), text: true })));
5054
5364
  return toCsv(matrix, sep);
@@ -5559,7 +5869,7 @@ function drawScatter(root, spec, series, top, bottom, nf, short) {
5559
5869
 
5560
5870
  // src/interactions/charts.ts
5561
5871
  var MAX_SERIES = 12;
5562
- var MAX_ROWS = 500;
5872
+ var MAX_ROWS2 = 500;
5563
5873
  var TYPES = ["bar", "stackedBar", "line", "area", "combo", "pie", "donut", "scatter"];
5564
5874
  var GridCharts = class {
5565
5875
  constructor(grid) {
@@ -5577,7 +5887,7 @@ var GridCharts = class {
5577
5887
  if (a && b) {
5578
5888
  const box = normalizeRange({ start: { rowIndex: r.start.rowIndex, colIndex: a.index }, end: { rowIndex: r.end.rowIndex, colIndex: b.index } });
5579
5889
  const rows = [];
5580
- for (let i = box.r1; i <= box.r2 && rows.length < MAX_ROWS; i++) {
5890
+ for (let i = box.r1; i <= box.r2 && rows.length < MAX_ROWS2; i++) {
5581
5891
  const n = grid.rowModel.getRow(i);
5582
5892
  if (n && !n.kind) rows.push(n.data);
5583
5893
  }
@@ -5585,7 +5895,7 @@ var GridCharts = class {
5585
5895
  }
5586
5896
  }
5587
5897
  const data = () => {
5588
- const rows = fixed ? fixed.rows : grid.rowModel.dataRows().slice(0, MAX_ROWS).map((n) => n.data);
5898
+ const rows = fixed ? fixed.rows : grid.rowModel.dataRows().slice(0, MAX_ROWS2).map((n) => n.data);
5589
5899
  const cols = (fixed ? fixed.colIds.map((id) => grid.columns.getColumn(id)) : grid.columns.displayed().filter((c) => !isSystemCol(c.id))).filter((c) => !!c);
5590
5900
  const cat = cols[0] && cols[0].def.type !== "number" ? cols[0] : fixed ? null : cols.find((c) => c.def.type !== "number") ?? null;
5591
5901
  const isTotal = (c) => !!c.node.parent && c.node.parent.id === "pivotgroup|total";
@@ -5659,58 +5969,6 @@ var GridCharts = class {
5659
5969
  }
5660
5970
  };
5661
5971
 
5662
- // src/rows/sort.ts
5663
- function compareNonBlank(a, b, collator) {
5664
- if (typeof a === "number" && typeof b === "number") return a - b;
5665
- if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime();
5666
- if (typeof a === "boolean" && typeof b === "boolean") return Number(a) - Number(b);
5667
- return collator.compare(String(a), String(b));
5668
- }
5669
- function normalize(def, v) {
5670
- if (isBlank(v)) return null;
5671
- if (def.type === "number") {
5672
- const n = typeof v === "number" ? v : Number(v);
5673
- return Number.isNaN(n) ? null : n;
5674
- }
5675
- if (def.type === "date") return toDate(v)?.getTime() ?? null;
5676
- return v;
5677
- }
5678
- function compareValues(a, b, collator) {
5679
- const ba = isBlank(a), bb = isBlank(b);
5680
- if (ba || bb) return ba === bb ? 0 : ba ? 1 : -1;
5681
- return compareNonBlank(a, b, collator);
5682
- }
5683
- function sortNodes(nodes, sort, resolve, collator) {
5684
- const keys = sort.map((s) => ({ def: resolve(s.colId), mul: s.dir === "desc" ? -1 : 1 })).filter((k) => !!k.def);
5685
- if (!keys.length) return nodes;
5686
- const values = keys.map((k) => nodes.map((n) => {
5687
- const v = getCellValue(k.def, n.data);
5688
- return k.def.comparator ? v : normalize(k.def, v);
5689
- }));
5690
- const order = nodes.map((_, i) => i);
5691
- order.sort((i, j) => {
5692
- for (let k = 0; k < keys.length; k++) {
5693
- const { def, mul } = keys[k];
5694
- const a = values[k][i], b = values[k][j];
5695
- let c;
5696
- if (def.comparator) {
5697
- try {
5698
- c = (Number(def.comparator(a, b, nodes[i].data, nodes[j].data)) || 0) * mul;
5699
- } catch {
5700
- warn(`comparator of column "${def.id ?? def.field}" threw; treating values as equal`);
5701
- c = 0;
5702
- }
5703
- } else {
5704
- const ba = isBlank(a), bb = isBlank(b);
5705
- c = ba || bb ? ba === bb ? 0 : ba ? 1 : -1 : compareNonBlank(a, b, collator) * mul;
5706
- }
5707
- if (c) return c;
5708
- }
5709
- return i - j;
5710
- });
5711
- return order.map((i) => nodes[i]);
5712
- }
5713
-
5714
5972
  // src/pivot.ts
5715
5973
  var SOURCE = /* @__PURE__ */ new WeakMap();
5716
5974
  var aggName = (a) => typeof a === "function" ? "custom" : a;
@@ -5836,7 +6094,7 @@ function buildPivot(source, config, defOf, labels, locale) {
5836
6094
  }
5837
6095
 
5838
6096
  // src/version.ts
5839
- var version = "1.0.0";
6097
+ var version = "1.1.0";
5840
6098
 
5841
6099
  // src/license.ts
5842
6100
  var PUBLIC_KEY = { kty: "EC", crv: "P-256", x: "sUTKkglZmk-HMKPNub8p7AeLBKesHCU8irIkw9rJlbE", y: "j0U_aKRDR7iApY9UpaD3hUemhBLZJwDv9-QiT_hUyw8" };
@@ -5862,6 +6120,12 @@ function domainMatches(host, domains) {
5862
6120
  return wildcard && h.endsWith(`.${dom}`);
5863
6121
  });
5864
6122
  }
6123
+ var isDay = (s) => {
6124
+ const m = typeof s === "string" ? /^(\d{4})-(\d{2})-(\d{2})$/.exec(s) : null;
6125
+ if (!m) return false;
6126
+ const d = new Date(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])));
6127
+ return d.getUTCMonth() === Number(m[2]) - 1 && d.getUTCDate() === Number(m[3]);
6128
+ };
5865
6129
  var today = (now) => `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
5866
6130
  async function verifyLicense(key, host, opts = {}) {
5867
6131
  const parts = typeof key === "string" ? key.trim().split(".") : [];
@@ -5869,7 +6133,7 @@ async function verifyLicense(key, host, opts = {}) {
5869
6133
  let info;
5870
6134
  try {
5871
6135
  info = JSON.parse(new TextDecoder().decode(b64urlDecode(parts[1])));
5872
- if (!Array.isArray(info.domains) || typeof info.expires !== "string") return { ok: false, reason: "format" };
6136
+ if (!Array.isArray(info.domains) || !isDay(info.expires)) return { ok: false, reason: "format" };
5873
6137
  } catch {
5874
6138
  return { ok: false, reason: "format" };
5875
6139
  }
@@ -6479,17 +6743,25 @@ var RowModel = class {
6479
6743
 
6480
6744
  // src/rows/serverGroups.ts
6481
6745
  var LEVEL_ROWS = 1e4;
6746
+ var MAX_LEVEL_ROWS = 1e5;
6747
+ var MAX_IN_FLIGHT = 4;
6748
+ var RETRY_MS = 3e3;
6482
6749
  var ServerGroups = class {
6483
- constructor(datasource, getRowId, groupBy, query, onChange) {
6750
+ constructor(datasource, getRowId, groupBy, query, onChange, onFailed) {
6484
6751
  this.datasource = datasource;
6485
6752
  this.getRowId = getRowId;
6486
6753
  this.groupBy = groupBy;
6487
6754
  this.query = query;
6488
6755
  this.onChange = onChange;
6756
+ this.onFailed = onFailed;
6489
6757
  this.levels = /* @__PURE__ */ new Map();
6490
6758
  this.gen = 0;
6491
6759
  this.display = null;
6492
6760
  this.ids = null;
6761
+ this.queue = [];
6762
+ this.inFlight = 0;
6763
+ /** Earliest time a failed level in the display may be asked again (0: none). */
6764
+ this.retryAt = 0;
6493
6765
  this.version = 0;
6494
6766
  this.builtFor = -1;
6495
6767
  }
@@ -6498,9 +6770,15 @@ var ServerGroups = class {
6498
6770
  }
6499
6771
  purge() {
6500
6772
  this.gen++;
6773
+ this.queue = [];
6501
6774
  this.levels.clear();
6502
6775
  this.touch();
6503
6776
  }
6777
+ /** Replaced by another grouping: answers still on their way are dropped, no more change calls. */
6778
+ dispose() {
6779
+ this.gen++;
6780
+ this.queue = [];
6781
+ }
6504
6782
  touch() {
6505
6783
  this.version++;
6506
6784
  this.display = null;
@@ -6508,15 +6786,33 @@ var ServerGroups = class {
6508
6786
  }
6509
6787
  load(path, keys, level) {
6510
6788
  if (this.levels.has(path)) return;
6511
- const gen = this.gen;
6512
6789
  this.levels.set(path, { state: "loading", nodes: [] });
6790
+ this.fetch({ path, keys, level, start: 0, gen: this.gen });
6791
+ }
6792
+ fetch(job) {
6793
+ this.queue.push(job);
6794
+ this.pump();
6795
+ }
6796
+ pump() {
6797
+ while (this.inFlight < MAX_IN_FLIGHT && this.queue.length) {
6798
+ const job = this.queue.shift();
6799
+ if (job.gen !== this.gen) continue;
6800
+ this.inFlight++;
6801
+ void this.request(job).finally(() => {
6802
+ this.inFlight--;
6803
+ this.pump();
6804
+ });
6805
+ }
6806
+ }
6807
+ request({ path, keys, level, start, gen }) {
6513
6808
  const last = level >= this.groupBy.length;
6514
6809
  const q = this.query;
6810
+ const end = start + LEVEL_ROWS;
6515
6811
  let result;
6516
6812
  try {
6517
6813
  result = Promise.resolve(this.datasource.getRows({
6518
- start: 0,
6519
- end: LEVEL_ROWS,
6814
+ start,
6815
+ end,
6520
6816
  sort: [...q.sort],
6521
6817
  filters: { ...q.filters },
6522
6818
  quickFilter: q.quickFilter,
@@ -6527,11 +6823,11 @@ var ServerGroups = class {
6527
6823
  } catch (e) {
6528
6824
  result = Promise.reject(e);
6529
6825
  }
6530
- result.then((res) => {
6826
+ return result.then((res) => {
6531
6827
  if (gen !== this.gen) return;
6532
6828
  const rows = Array.isArray(res?.rows) ? res.rows.filter((r) => r !== null && typeof r === "object") : [];
6533
6829
  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) => {
6830
+ 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
6831
  const colId = this.groupBy[level];
6536
6832
  const value = data[colId];
6537
6833
  const key = `${path ? `${path}|` : ""}${colId}:${setKey(value)}`;
@@ -6545,14 +6841,23 @@ var ServerGroups = class {
6545
6841
  group: { key, colId, value, rows: [], expanded: false, count, aggregates: data, keys: [...keys, value] }
6546
6842
  };
6547
6843
  });
6548
- this.levels.set(path, { state: "loaded", nodes });
6844
+ const nodes = start ? [...this.levels.get(path)?.nodes ?? [], ...added] : added;
6845
+ const total = typeof res?.total === "number" && res.total >= 0 ? res.total : null;
6846
+ const more = rows.length > 0 && (total !== null ? nodes.length < total : rows.length >= end - start);
6847
+ if (more && nodes.length < MAX_LEVEL_ROWS) {
6848
+ this.levels.set(path, { state: "loading", nodes });
6849
+ this.fetch({ path, keys, level, start: nodes.length, gen });
6850
+ } else {
6851
+ if (more) warn(`a server group level has more than ${MAX_LEVEL_ROWS} rows; only the first ${MAX_LEVEL_ROWS} are shown`);
6852
+ this.levels.set(path, { state: "loaded", nodes });
6853
+ }
6549
6854
  this.touch();
6550
6855
  this.onChange();
6551
- }, () => {
6856
+ }, (error) => {
6552
6857
  if (gen !== this.gen) return;
6553
- this.levels.set(path, { state: "failed", nodes: [] });
6858
+ this.levels.set(path, { state: "failed", nodes: [], retryAt: Date.now() + RETRY_MS });
6554
6859
  this.touch();
6555
- this.onChange();
6860
+ this.onFailed(start, end, error);
6556
6861
  });
6557
6862
  }
6558
6863
  isOpen(d, key, level) {
@@ -6562,17 +6867,24 @@ var ServerGroups = class {
6562
6867
  }
6563
6868
  /** Display rows: open groups show their loaded children, or a loading row while they arrive. */
6564
6869
  rows(d, displayVersion) {
6565
- if (this.display && this.builtFor === displayVersion) return this.display;
6870
+ const now = Date.now();
6871
+ if (this.display && this.builtFor === displayVersion && !(this.retryAt && now >= this.retryAt)) return this.display;
6566
6872
  const out = [];
6873
+ let retryAt = 0;
6567
6874
  const walk = (path, keys, level) => {
6568
- const lv = this.levels.get(path);
6875
+ let lv = this.levels.get(path);
6876
+ if (lv?.state === "failed" && now >= (lv.retryAt ?? 0)) {
6877
+ this.levels.delete(path);
6878
+ lv = void 0;
6879
+ }
6569
6880
  if (!lv) {
6570
6881
  this.load(path, keys, level);
6571
6882
  out.push({ id: `loading:${path}`, data: {}, sourceIndex: -1, kind: "loading", level });
6572
6883
  return;
6573
6884
  }
6574
- if (lv.state !== "loaded") {
6575
- if (lv.state === "loading") out.push({ id: `loading:${path}`, data: {}, sourceIndex: -1, kind: "loading", level });
6885
+ if (lv.state === "failed") {
6886
+ const at = lv.retryAt ?? 0;
6887
+ retryAt = retryAt ? Math.min(retryAt, at) : at;
6576
6888
  return;
6577
6889
  }
6578
6890
  for (const n of lv.nodes) {
@@ -6586,8 +6898,10 @@ var ServerGroups = class {
6586
6898
  out.push(node);
6587
6899
  if (open) walk(g.key, g.keys ?? [], level + 1);
6588
6900
  }
6901
+ if (lv.state === "loading") out.push({ id: `loading:${path}`, data: {}, sourceIndex: -1, kind: "loading", level });
6589
6902
  };
6590
6903
  walk("", [], 0);
6904
+ this.retryAt = retryAt;
6591
6905
  this.display = out;
6592
6906
  this.builtFor = displayVersion;
6593
6907
  this.ids = null;
@@ -6621,7 +6935,6 @@ var ServerGroups = class {
6621
6935
  };
6622
6936
 
6623
6937
  // src/rows/serverRowModel.ts
6624
- var RETRY_MS = 3e3;
6625
6938
  var NO_OFFSETS = new Float64Array(0);
6626
6939
  var ServerRowModel = class {
6627
6940
  constructor(deps) {
@@ -6655,7 +6968,15 @@ var ServerRowModel = class {
6655
6968
  if (groupBy.length && !input.display?.getParentId) {
6656
6969
  const q2 = { sort: input.sort, filters: input.filters, quickFilter: input.quickFilter, advancedFilter: input.advancedFilter ?? null };
6657
6970
  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));
6971
+ this.groups?.dispose();
6972
+ this.groups = new ServerGroups(
6973
+ this.deps.datasource,
6974
+ this.deps.getRowId,
6975
+ [...groupBy],
6976
+ q2,
6977
+ () => this.deps.onLoaded(0, 0, null),
6978
+ (start, end, error) => this.deps.onFailed(start, end, error)
6979
+ );
6659
6980
  this.touch();
6660
6981
  }
6661
6982
  this.displayIn = input.display ?? null;
@@ -6663,6 +6984,7 @@ var ServerRowModel = class {
6663
6984
  return;
6664
6985
  }
6665
6986
  if (this.groups) {
6987
+ this.groups.dispose();
6666
6988
  this.groups = null;
6667
6989
  this.query = null;
6668
6990
  this.touch();
@@ -7066,22 +7388,30 @@ var FilterBuilder = class {
7066
7388
  del.addEventListener("click", () => remove(state));
7067
7389
  return state;
7068
7390
  };
7069
- const groupRow = (g) => {
7391
+ const keptRow = (kept, label) => {
7070
7392
  const rowEl = el("div", "fg-fb-row fg-fb-group");
7071
7393
  const text2 = el("span", "fg-fb-group-label");
7072
- text2.textContent = `( ${g.conditions.length} \xD7 ${loc[g.operator]} )`;
7394
+ text2.textContent = label;
7073
7395
  const del = el("button", "fg-chooser-close fg-fb-remove", { type: "button", "aria-label": loc.close });
7074
7396
  del.textContent = "\xD7";
7075
7397
  rowEl.append(text2, del);
7076
- const state = { el: rowEl, read: () => g };
7398
+ const state = { el: rowEl, read: () => kept };
7077
7399
  del.addEventListener("click", () => remove(state));
7078
7400
  return state;
7079
7401
  };
7402
+ const rowFor = (c) => {
7403
+ if ("conditions" in c) return keptRow(c, `( ${c.conditions.length} \xD7 ${loc[c.operator]} )`);
7404
+ const col = cols.find((x2) => x2.id === c.colId);
7405
+ const m = c.model;
7406
+ if (col && m && (m.type === "text" || m.type === "number" || m.type === "date")) return conditionRow(c);
7407
+ const what = m?.type === "set" ? m.values.map(String).join(", ") : m?.type === "combined" ? `( ${m.conditions.length} \xD7 ${loc[m.operator]} )` : "";
7408
+ return keptRow(c, `${col?.label ?? c.colId}: ${what}`);
7409
+ };
7080
7410
  const add = (row) => {
7081
7411
  rows.push(row);
7082
7412
  list.append(row.el);
7083
7413
  };
7084
- for (const c of cur?.conditions ?? []) add("conditions" in c ? groupRow(c) : conditionRow(c));
7414
+ for (const c of cur?.conditions ?? []) add(rowFor(c));
7085
7415
  const addBtn = el("button", "fg-btn fg-fb-add", { type: "button" });
7086
7416
  addBtn.textContent = `+ ${loc.addCondition}`;
7087
7417
  addBtn.addEventListener("click", () => {
@@ -7279,7 +7609,7 @@ var Grid = class {
7279
7609
  () => this.menu.close(),
7280
7610
  () => this.chooser.close(),
7281
7611
  () => this.contextMenu.destroy(),
7282
- () => this.editor.cancel(),
7612
+ () => this.editor.destroy(),
7283
7613
  () => this.fillHandle.destroy(),
7284
7614
  () => this.groupPanel?.destroy(),
7285
7615
  () => this.charts.destroy(),
@@ -7449,35 +7779,68 @@ var Grid = class {
7449
7779
  this.flashTimer = window.setTimeout(tick, 1e3);
7450
7780
  }
7451
7781
  applyTransaction(tx) {
7782
+ if (!tx || typeof tx !== "object") return { added: [], updated: [], removed: [], notFound: [] };
7783
+ return this.applyTransactions([tx]);
7784
+ }
7785
+ /** Apply transactions one after another, then render and emit once. */
7786
+ applyTransactions(list) {
7452
7787
  const result = { added: [], updated: [], removed: [], notFound: [] };
7453
- if (this.clientOnly("applyTransaction") || !tx || typeof tx !== "object") return result;
7454
- const data = this.store.state.data;
7788
+ if (this.clientOnly("applyTransaction")) return result;
7789
+ const colsByField = /* @__PURE__ */ new Map();
7790
+ for (const st of this.columns.getState()) {
7791
+ const f = this.columns.getNode(st.colId)?.def.field;
7792
+ if (f) colsByField.set(f, [...colsByField.get(f) ?? [], st.colId]);
7793
+ }
7794
+ const flashKeys = [];
7795
+ let next = this.store.state.data;
7796
+ const present = new Set(next);
7797
+ const byId = new Map(next.map((r) => [this.rowModel.idOf(r), r]));
7798
+ for (const tx of list) {
7799
+ if (tx && typeof tx === "object") next = this.txStep(tx, next, { present, byId }, result, flashKeys, colsByField);
7800
+ }
7801
+ if (!result.added.length && !result.updated.length && !result.removed.length) {
7802
+ if (result.notFound.length) this.events.emit("transactionApplied", result);
7803
+ return result;
7804
+ }
7805
+ this.editVersion++;
7806
+ this.renderer.invalidateHeights();
7807
+ this.keepRange = true;
7808
+ try {
7809
+ this.replaceData(next === this.store.state.data ? [...next] : next);
7810
+ } finally {
7811
+ this.keepRange = false;
7812
+ }
7813
+ this.editor.sync();
7814
+ if (flashKeys.length && this.options.flashCells !== false) this.flash(flashKeys);
7815
+ this.events.emit("transactionApplied", result);
7816
+ return result;
7817
+ }
7818
+ /** One transaction against `data` (remove → update → add); returns the new array. */
7819
+ txStep(tx, data, index, result, flashKeys, colsByField) {
7455
7820
  const getId = this.options.getRowId;
7821
+ const { present, byId } = index;
7456
7822
  const keyOf = (r) => {
7457
7823
  if (!r || typeof r !== "object") return null;
7458
- if (data.includes(r)) return this.rowModel.idOf(r);
7824
+ if (present.has(r)) return this.rowModel.idOf(r);
7459
7825
  try {
7460
7826
  return getId ? String(getId(r)) : null;
7461
7827
  } catch {
7462
7828
  return null;
7463
7829
  }
7464
7830
  };
7465
- const byId = new Map(data.map((r) => [this.rowModel.idOf(r), r]));
7466
7831
  const gone = /* @__PURE__ */ new Set();
7467
7832
  for (const r of tx.remove ?? []) {
7468
7833
  const k = keyOf(r);
7469
7834
  const row = k === null ? void 0 : byId.get(k);
7470
7835
  if (row && !gone.has(row)) {
7471
7836
  gone.add(row);
7837
+ present.delete(row);
7838
+ byId.delete(k);
7472
7839
  result.removed.push(row);
7840
+ this.tracker.created.delete(row);
7841
+ this.tracker.originals.delete(row);
7473
7842
  } else if (!row) result.notFound.push(r);
7474
7843
  }
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
7844
  for (const r of tx.update ?? []) {
7482
7845
  const k = keyOf(r);
7483
7846
  const row = k === null ? void 0 : byId.get(k);
@@ -7485,7 +7848,9 @@ var Grid = class {
7485
7848
  result.notFound.push(r);
7486
7849
  continue;
7487
7850
  }
7851
+ const orig = this.tracker.originals.get(row);
7488
7852
  for (const [field, value] of Object.entries(r)) {
7853
+ if (orig) orig[field] = value;
7489
7854
  if (Object.is(row[field], value)) continue;
7490
7855
  row[field] = value;
7491
7856
  for (const colId of colsByField.get(field) ?? []) flashKeys.push(`${k}\0${colId}`);
@@ -7494,24 +7859,15 @@ var Grid = class {
7494
7859
  }
7495
7860
  const added = cleanRows(Array.isArray(tx.add) ? tx.add : []);
7496
7861
  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;
7862
+ for (const r of added) {
7863
+ present.add(r);
7864
+ byId.set(this.rowModel.idOf(r), r);
7500
7865
  }
7866
+ if (!gone.size && !added.length) return data;
7501
7867
  const next = gone.size ? data.filter((r) => !gone.has(r)) : [...data];
7502
7868
  const at = typeof tx.addIndex === "number" ? Math.max(0, Math.min(next.length, Math.floor(tx.addIndex))) : next.length;
7503
7869
  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;
7870
+ return next;
7515
7871
  }
7516
7872
  applyTransactionAsync(tx) {
7517
7873
  if (!tx || typeof tx !== "object") return;
@@ -7522,12 +7878,7 @@ var Grid = class {
7522
7878
  const queue = this.txQueue;
7523
7879
  this.txQueue = [];
7524
7880
  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
- });
7881
+ this.applyTransactions(queue);
7531
7882
  }, 50);
7532
7883
  }
7533
7884
  setAdvancedFilter(filter) {
@@ -7543,6 +7894,8 @@ var Grid = class {
7543
7894
  }
7544
7895
  getState() {
7545
7896
  const s = this.store.state;
7897
+ const groupBy = this.savedGroup?.groupBy ?? this.groupBy;
7898
+ const expanded = this.savedGroup?.expanded ?? this.expanded;
7546
7899
  const state = {
7547
7900
  columns: this.columns.getState(),
7548
7901
  columnGroups: this.columns.groupOpenState(),
@@ -7550,9 +7903,9 @@ var Grid = class {
7550
7903
  sort: s.sort.map((x) => ({ ...x })),
7551
7904
  filters: JSON.parse(JSON.stringify(s.filters)),
7552
7905
  quickFilter: s.quickFilter,
7553
- groupBy: [...this.groupBy],
7906
+ groupBy: [...groupBy],
7554
7907
  pivot: this.pivotCfg ? JSON.parse(JSON.stringify(this.pivotCfg)) : null,
7555
- expanded: Object.fromEntries(this.expanded),
7908
+ expanded: Object.fromEntries(expanded),
7556
7909
  selected: [...s.selected]
7557
7910
  };
7558
7911
  if (this.options.pagination) {
@@ -7583,8 +7936,12 @@ var Grid = class {
7583
7936
  if (want("advancedFilter")) this.setAdvancedFilter(state.advancedFilter ?? null);
7584
7937
  if (want("quickFilter") && typeof state.quickFilter === "string") this.store.set({ quickFilter: state.quickFilter });
7585
7938
  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();
7939
+ const expanded = new Map(Object.entries(state.expanded).filter(([, v]) => typeof v === "boolean"));
7940
+ if (this.savedGroup) this.savedGroup.expanded = expanded;
7941
+ else {
7942
+ this.expanded = expanded;
7943
+ this.displayChanged();
7944
+ }
7588
7945
  }
7589
7946
  if (want("pageSize") && typeof state.pageSize === "number") this.setPageSize(state.pageSize);
7590
7947
  if (want("page") && typeof state.page === "number") this.setPage(state.page);
@@ -8108,7 +8465,7 @@ var Grid = class {
8108
8465
  if (all2.length > 1) {
8109
8466
  const sameCols = all2.every((x) => x.c1 === all2[0].c1 && x.c2 === all2[0].c2);
8110
8467
  const sameRows = all2.every((x) => x.r1 === all2[0].r1 && x.r2 === all2[0].r2);
8111
- if (sameCols || sameRows) {
8468
+ if (this.rangesStack(all2)) {
8112
8469
  const parts = [...all2].sort((x, y) => sameCols ? x.r1 - y.r1 : x.c1 - y.c1);
8113
8470
  const blocks = parts.map((p) => this.rangeMatrix(p));
8114
8471
  const matrix2 = sameCols ? blocks.flat() : blocks[0].map((_, ri) => blocks.flatMap((bl) => bl[ri] ?? []));
@@ -8139,6 +8496,10 @@ var Grid = class {
8139
8496
  if (headers) matrix.unshift(cols.map((c) => c.def.header ?? c.def.field ?? c.id));
8140
8497
  return toTSV(matrix);
8141
8498
  }
8499
+ /** Like Excel: ranges stack when they share columns or rows; otherwise only the active one is copied. */
8500
+ rangesStack(all2) {
8501
+ 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));
8502
+ }
8142
8503
  rangeHeaders(b) {
8143
8504
  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
8505
  }
@@ -8154,24 +8515,21 @@ var Grid = class {
8154
8515
  return out;
8155
8516
  }
8156
8517
  copyCell(def, row, code) {
8157
- const value = getCellValue(def, row);
8518
+ return this.copyAllowed(def, row) ? copyValue(def, getCellValue(def, row), code) : "";
8519
+ }
8520
+ copyAllowed(def, row) {
8158
8521
  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 "";
8522
+ if (typeof rule !== "function") return rule !== false;
8523
+ try {
8524
+ return rule({ value: getCellValue(def, row), row, column: def }) !== false;
8525
+ } catch {
8526
+ return false;
8168
8527
  }
8169
- return copyValue(def, value, code);
8170
8528
  }
8171
8529
  /** Copy, then clear the editable cells of the selection. */
8172
8530
  cutToClipboard() {
8173
8531
  const text2 = this.copyToClipboard(false);
8174
- if (text2) this.clearCells();
8532
+ if (text2) this.clearCells(true);
8175
8533
  return text2;
8176
8534
  }
8177
8535
  copyToClipboard(headers, rows = false) {
@@ -8430,10 +8788,14 @@ var Grid = class {
8430
8788
  this.writeCell(changes, node.data, def.field, colId, rowIndex, value);
8431
8789
  this.endCellTx(changes);
8432
8790
  }
8433
- /** Delete/Backspace: clear editable cells in the range, or the focused cell. */
8434
- clearCells() {
8791
+ /** Delete/Backspace: clear editable cells in the range, or the focused cell. `cut` clears only what a copy takes. */
8792
+ clearCells(cut = false) {
8435
8793
  const displayed = this.columns.displayed();
8436
- const all2 = this.allRangeBounds();
8794
+ let all2 = this.allRangeBounds();
8795
+ if (cut && !this.rangesStack(all2)) {
8796
+ const active = this.rangeBounds();
8797
+ all2 = active ? [active] : [];
8798
+ }
8437
8799
  const f = this.store.state.focus;
8438
8800
  const fi = f ? this.columns.getColumn(f.colId)?.index : void 0;
8439
8801
  const boxes = all2.length ? all2 : f && fi !== void 0 ? [{ r1: f.rowIndex, r2: f.rowIndex, c1: fi, c2: fi }] : [];
@@ -8443,7 +8805,7 @@ var Grid = class {
8443
8805
  const node = this.rowModel.getRow(r);
8444
8806
  for (let c = box.c1; c <= box.c2; c++) {
8445
8807
  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);
8808
+ 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
8809
  }
8448
8810
  }
8449
8811
  this.endCellTx(changes);
@@ -8545,8 +8907,9 @@ var Grid = class {
8545
8907
  const m = this.getModifiedRows();
8546
8908
  return m.created.length + m.updated.length + m.deleted.length > 0;
8547
8909
  }
8548
- commitChanges() {
8549
- this.tracker.reset();
8910
+ commitChanges(saved) {
8911
+ if (saved && saved.rows instanceof Map) this.tracker.commit(saved.rows, Array.isArray(saved.deleted) ? saved.deleted : []);
8912
+ else this.tracker.reset();
8550
8913
  this.renderer.invalidate({ rows: true });
8551
8914
  }
8552
8915
  revertChanges() {
@@ -8950,7 +9313,7 @@ var Grid = class {
8950
9313
  getModifiedRows: guard(() => this.getModifiedRows()),
8951
9314
  getChanges: guard(() => this.getChanges()),
8952
9315
  hasChanges: guard(() => this.hasChanges()),
8953
- commitChanges: guard(() => this.commitChanges()),
9316
+ commitChanges: guard((saved) => this.commitChanges(saved)),
8954
9317
  revertChanges: guard(() => this.revertChanges()),
8955
9318
  undo: guard(() => this.undo()),
8956
9319
  redo: guard(() => this.redo()),
@@ -9021,26 +9384,32 @@ function exportWorkbook(filename, sheets) {
9021
9384
  var headersOf = (h) => typeof h === "function" ? h() : h ?? {};
9022
9385
  async function send(url, method, body, opts) {
9023
9386
  let target = url;
9387
+ const where = url.split(/[?#]/)[0];
9024
9388
  const init = { method, credentials: opts.credentials ?? "same-origin", headers: { Accept: "application/json", ...headersOf(opts.headers) } };
9025
9389
  if (method === "GET") {
9026
9390
  const q = new URLSearchParams();
9027
- for (const [k, v] of Object.entries(body)) q.set(k, typeof v === "string" ? v : JSON.stringify(v));
9391
+ for (const [k, v] of Object.entries(body)) if (v !== void 0) q.set(k, typeof v === "string" ? v : JSON.stringify(v));
9028
9392
  target += (url.includes("?") ? "&" : "?") + q.toString();
9029
9393
  } else {
9030
9394
  init.headers["Content-Type"] = "application/json";
9031
9395
  init.body = JSON.stringify(body);
9032
9396
  }
9033
9397
  const res = await fetch(target, init);
9034
- if (!res.ok) throw new Error(`HTTP ${res.status} from ${url}`);
9398
+ if (!res.ok) throw new Error(`HTTP ${res.status} from ${where}`);
9035
9399
  const text2 = await res.text();
9036
- return text2 ? JSON.parse(text2) : null;
9400
+ if (!text2) return null;
9401
+ try {
9402
+ return JSON.parse(text2);
9403
+ } catch {
9404
+ throw new Error(`HTTP ${res.status} from ${where}: the response is not JSON`);
9405
+ }
9037
9406
  }
9038
9407
  function readRows(json) {
9039
9408
  if (Array.isArray(json)) return { rows: json };
9040
9409
  const o = json ?? {};
9041
9410
  const rows = [o.rows, o.list, o.content, o.data, o.items, o.resultList].find(Array.isArray);
9042
9411
  const total = [o.total, o.totalCount, o.totalElements, o.totCnt, o.count].find((v) => typeof v === "number");
9043
- return { rows: rows ?? [], total };
9412
+ return { rows: (rows ?? []).filter((r) => r !== null && typeof r === "object"), total };
9044
9413
  }
9045
9414
  function restDatasource(url, options = {}) {
9046
9415
  return {
@@ -9054,6 +9423,10 @@ function restDatasource(url, options = {}) {
9054
9423
  sort: p.sort,
9055
9424
  filters: p.filters,
9056
9425
  quickFilter: p.quickFilter,
9426
+ // only when set (undefined is left out of the request)
9427
+ advancedFilter: p.advancedFilter ?? void 0,
9428
+ groupBy: p.groupBy,
9429
+ groupKeys: p.groupKeys,
9057
9430
  ...options.params?.() ?? {}
9058
9431
  };
9059
9432
  const json = await send(url, options.method ?? "POST", body, options);
@@ -9061,12 +9434,20 @@ function restDatasource(url, options = {}) {
9061
9434
  }
9062
9435
  };
9063
9436
  }
9064
- async function saveChanges(grid, url, options = {}) {
9065
- if (!grid.hasChanges()) return null;
9437
+ var saving = /* @__PURE__ */ new WeakMap();
9438
+ function saveChanges(grid, url, options = {}) {
9439
+ const pending = saving.get(grid);
9440
+ if (pending) return pending;
9441
+ if (!grid.hasChanges()) return Promise.resolve(null);
9066
9442
  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;
9443
+ const rows = new Map([...created, ...updated].map((r) => [r, { ...r }]));
9444
+ const run = (async () => {
9445
+ const json = await send(url, "POST", { created, updated, deleted, ...options.params?.() ?? {} }, options);
9446
+ grid.commitChanges({ rows, deleted });
9447
+ return json;
9448
+ })().finally(() => saving.delete(grid));
9449
+ saving.set(grid, run);
9450
+ return run;
9070
9451
  }
9071
9452
  async function loadData(grid, url, options = {}) {
9072
9453
  const { rows } = await restDatasource(url, options).getRows({ start: 0, end: options.maxRows ?? 1e4, sort: [], filters: {}, quickFilter: "" });