@vueye-table/core 3.0.0-alpha.1 → 3.0.0-alpha.11

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
@@ -8,13 +8,34 @@ function humanize(id) {
8
8
  return sentence.charAt(0).toUpperCase() + sentence.slice(1);
9
9
  }
10
10
 
11
+ //#endregion
12
+ //#region src/issues.ts
13
+ function issue(code, message, details = {}) {
14
+ return Object.freeze({
15
+ code,
16
+ message,
17
+ ...details
18
+ });
19
+ }
20
+
11
21
  //#endregion
12
22
  //#region src/path.ts
23
+ /**
24
+ * Dotted property paths.
25
+ *
26
+ * A column addresses nested data with a path such as `"address.city"`. The types below give every
27
+ * such path its value type, and the functions read and write through a path without mutating.
28
+ */
13
29
  function isRecord$1(value) {
14
30
  return typeof value === "object" && value !== null;
15
31
  }
32
+ /** Whether every path segment can be read or written without addressing a prototype. */
33
+ function isSafePath(path) {
34
+ return path.split(".").every((segment) => segment !== "__proto__" && segment !== "constructor" && segment !== "prototype");
35
+ }
16
36
  /** Read the value at a dotted path, or `undefined` when a step is missing. */
17
37
  function getPath(source, path) {
38
+ if (!isSafePath(path)) return;
18
39
  let current = source;
19
40
  for (const segment of path.split(".")) {
20
41
  if (!isRecord$1(current)) return;
@@ -24,13 +45,18 @@ function getPath(source, path) {
24
45
  }
25
46
  /**
26
47
  * Return a copy of `source` with `value` at `path`. Only the objects along the path are copied;
27
- * everything else is shared. Missing steps are created as plain objects.
48
+ * everything else is shared. Missing steps are created as plain objects. Unsafe paths return
49
+ * `source` unchanged and report an `unsafe_path` issue through `onIssue`, when supplied.
28
50
  */
29
- function setPath(source, path, value) {
51
+ function setPath(source, path, value, onIssue) {
52
+ if (!isSafePath(path)) {
53
+ onIssue?.(issue("unsafe_path", `The path "${path}" addresses a prototype and cannot be written.`, { column: path }));
54
+ return source;
55
+ }
30
56
  const [head, ...rest] = path.split(".");
31
57
  const base = isRecord$1(source) ? { ...source } : {};
32
58
  if (head === void 0) return source;
33
- base[head] = rest.length === 0 ? value : setPath(base[head], rest.join("."), value);
59
+ base[head] = rest.length === 0 ? value : setPath(Object.hasOwn(base, head) ? base[head] : void 0, rest.join("."), value);
34
60
  return base;
35
61
  }
36
62
 
@@ -197,14 +223,15 @@ function parseAs(input, type) {
197
223
  function resolveColumn(definition) {
198
224
  const def = definition;
199
225
  const { accessor } = def;
200
- const getValue = accessor ?? ((row) => getPath(row, def.id));
226
+ const safe = isSafePath(def.id);
227
+ const getValue = safe ? accessor ?? ((row) => getPath(row, def.id)) : () => void 0;
201
228
  const format = def.format ?? ((value) => formatValue(value));
202
229
  const compare = def.compare ?? compareValues;
203
230
  const customFilter = def.filter;
204
231
  const customParse = def.parse;
205
232
  const editable = def.editable ?? false;
206
233
  const customSetValue = def.setValue;
207
- const canWrite = accessor === void 0 || customSetValue !== void 0;
234
+ const canWrite = safe && (accessor === void 0 || customSetValue !== void 0);
208
235
  return Object.freeze({
209
236
  id: def.id,
210
237
  header: def.header ?? humanize(def.id),
@@ -241,6 +268,7 @@ function resolveColumn(definition) {
241
268
  return parseAs(input, def.type ?? typeOf(getValue(row)) ?? typeOf(sample) ?? "text");
242
269
  },
243
270
  setValue(row, value) {
271
+ if (!safe) return;
244
272
  if (customSetValue) return customSetValue(row, value);
245
273
  return accessor === void 0 ? setPath(row, def.id, value) : void 0;
246
274
  }
@@ -258,6 +286,10 @@ function resolveColumn(definition) {
258
286
  function quote(field, delimiter) {
259
287
  return field.includes(delimiter) || field.includes("\"") || field.includes("\n") || field.includes("\r") ? `"${field.replaceAll("\"", "\"\"")}"` : field;
260
288
  }
289
+ /** Escape formula-like display text; a finite number formatted as a number stays numeric. */
290
+ function escapeFormula(text, value) {
291
+ return !(typeof value === "number" && Number.isFinite(value) && /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/u.test(text)) && /^[=+\-@\t\r\n=+-@]/u.test(text) ? "'" + text : text;
292
+ }
261
293
  /** Write a matrix of text as delimited lines joined with `\n`. */
262
294
  function toDelimited(matrix, delimiter = " ") {
263
295
  return matrix.map((line) => line.map((field) => quote(field, delimiter)).join(delimiter)).join("\n");
@@ -267,39 +299,92 @@ function toDelimited(matrix, delimiter = " ") {
267
299
  * and embedded line breaks, and ignores one trailing line break.
268
300
  */
269
301
  function parseDelimited(text, delimiter = " ") {
302
+ return readDelimited(text, {
303
+ maxRows: Infinity,
304
+ maxColumns: Infinity,
305
+ maxCells: Infinity,
306
+ maxLength: Infinity
307
+ }, delimiter).rows;
308
+ }
309
+ /** Read complete fields within a grid and work budget, without materializing discarded fields. */
310
+ function readDelimited(text, limits, delimiter = " ") {
270
311
  const rows = [];
312
+ if (limits.maxRows === 0 || limits.maxColumns === 0) return {
313
+ rows,
314
+ truncated: text.length > 0
315
+ };
271
316
  let row = [];
272
317
  let field = "";
273
318
  let quoted = false;
319
+ let started = false;
274
320
  let index = 0;
321
+ let column = 0;
322
+ let cells = 0;
323
+ let truncated = false;
324
+ const end = Math.min(text.length, limits.maxLength);
275
325
  const endField = () => {
276
- row.push(field);
326
+ if (column < limits.maxColumns) row.push(field);
327
+ else truncated = true;
328
+ column += 1;
329
+ cells += 1;
277
330
  field = "";
331
+ started = false;
278
332
  };
279
- const endRow = () => {
280
- endField();
281
- rows.push(row);
282
- row = [];
333
+ const append = (char) => {
334
+ if (column < limits.maxColumns) field += char;
283
335
  };
284
- while (index < text.length) {
336
+ while (index < end) {
285
337
  const char = text.charAt(index);
286
338
  if (quoted) {
287
339
  if (char === "\"") {
288
- if (text.charAt(index + 1) === "\"") {
289
- field += "\"";
340
+ if (index + 1 < end && text.charAt(index + 1) === "\"") {
341
+ append("\"");
290
342
  index += 1;
291
343
  } else quoted = false;
292
- } else field += char;
293
- } else if (char === "\"" && field === "") quoted = true;
294
- else if (char === delimiter) endField();
295
- else if (char === "\n" || char === "\r") {
296
- if (char === "\r" && text.charAt(index + 1) === "\n") index += 1;
297
- endRow();
298
- } else field += char;
344
+ } else append(char);
345
+ } else if (char === "\"" && !started) quoted = true;
346
+ else if (char === delimiter) {
347
+ endField();
348
+ index += 1;
349
+ if (cells >= limits.maxCells || rows.length + 1 >= limits.maxRows && column >= limits.maxColumns) {
350
+ rows.push(row);
351
+ return {
352
+ rows,
353
+ truncated: true
354
+ };
355
+ }
356
+ continue;
357
+ } else if (char === "\n" || char === "\r") {
358
+ if (char === "\r" && index + 1 < end && text.charAt(index + 1) === "\n") index += 1;
359
+ endField();
360
+ rows.push(row);
361
+ row = [];
362
+ column = 0;
363
+ index += 1;
364
+ if (cells >= limits.maxCells || rows.length >= limits.maxRows) return {
365
+ rows,
366
+ truncated: truncated || index < text.length
367
+ };
368
+ continue;
369
+ } else append(char);
370
+ started = true;
299
371
  index += 1;
300
372
  }
301
- if (field !== "" || row.length > 0) endRow();
302
- return rows;
373
+ if (end < text.length) {
374
+ if (row.length > 0) rows.push(row);
375
+ return {
376
+ rows,
377
+ truncated: true
378
+ };
379
+ }
380
+ if (started || column > 0) {
381
+ endField();
382
+ rows.push(row);
383
+ }
384
+ return {
385
+ rows,
386
+ truncated
387
+ };
303
388
  }
304
389
 
305
390
  //#endregion
@@ -380,15 +465,23 @@ const ARROWS = {
380
465
  ArrowLeft: "left",
381
466
  ArrowRight: "right"
382
467
  };
383
- /**
384
- * Map a key press to a grid command, spreadsheet style: arrows move (Shift extends, Ctrl or Cmd
385
- * jumps to the edge), Tab moves across, Home and End reach the row's ends, Enter or F2 edit,
386
- * a printable character starts editing with that character, Delete and Backspace clear, Escape
387
- * collapses the selection, and Ctrl or Cmd with A, Z, Y select all, undo, and redo.
388
- */
389
- function gridCommand(event) {
468
+ function gridCommand(event, tree) {
390
469
  const mod = event.ctrlKey === true || event.metaKey === true;
391
470
  const shift = event.shiftKey === true;
471
+ if (tree && !mod && !event.altKey) {
472
+ if (event.key === "*") return {
473
+ type: "tree",
474
+ action: "siblings"
475
+ };
476
+ if (!shift && event.key === "ArrowRight" && tree.canExpand) return {
477
+ type: "tree",
478
+ action: tree.expanded ? "child" : "expand"
479
+ };
480
+ if (!shift && event.key === "ArrowLeft") return {
481
+ type: "tree",
482
+ action: tree.canExpand && tree.expanded ? "collapse" : "parent"
483
+ };
484
+ }
392
485
  const direction = ARROWS[event.key];
393
486
  if (direction) return {
394
487
  type: "move",
@@ -462,6 +555,214 @@ function createUndoStack(limit = 100) {
462
555
  clear() {
463
556
  past.length = 0;
464
557
  future.length = 0;
558
+ },
559
+ transform(map) {
560
+ const update = (entries) => entries.flatMap((entry) => {
561
+ const value = map(entry);
562
+ return value === void 0 ? [] : [value];
563
+ });
564
+ const nextPast = update(past);
565
+ const nextFuture = update(future);
566
+ past.splice(0, past.length, ...nextPast);
567
+ future.splice(0, future.length, ...nextFuture);
568
+ }
569
+ };
570
+ }
571
+
572
+ //#endregion
573
+ //#region src/virtualizer.ts
574
+ const FALLBACK_SIZE = 40;
575
+ /** Layout arithmetic for either axis. All viewport values and measurements come from the caller. */
576
+ function createVirtualizer(initialOptions) {
577
+ let options = initialOptions;
578
+ let count = 0;
579
+ let overscan = 5;
580
+ let fixedSize;
581
+ let keys = [];
582
+ let byKey = /* @__PURE__ */ new Map();
583
+ let sizes = [];
584
+ let prefix = [0];
585
+ let offset = 0;
586
+ let viewportSize = 0;
587
+ let cached;
588
+ const measured = /* @__PURE__ */ new Map();
589
+ const problems = /* @__PURE__ */ new Map();
590
+ const listeners = /* @__PURE__ */ new Set();
591
+ function recover(code, message) {
592
+ if (!problems.has(code)) {
593
+ const problem = issue(code, message);
594
+ problems.set(code, problem);
595
+ options.onIssue?.(problem);
596
+ cached = void 0;
597
+ }
598
+ }
599
+ function validSize(value) {
600
+ if (Number.isFinite(value) && value > 0) return value;
601
+ recover("invalid_virtual_size", `Item size ${String(value)} is not positive and finite; 40 is used.`);
602
+ return FALLBACK_SIZE;
603
+ }
604
+ function configure(next) {
605
+ options = next;
606
+ problems.clear();
607
+ count = Number.isSafeInteger(next.count) && next.count >= 0 ? next.count : 0;
608
+ if (count !== next.count) recover("invalid_virtual_option", "Virtual item count must be a non-negative safe integer; 0 is used.");
609
+ const requestedOverscan = next.overscan ?? 5;
610
+ overscan = Number.isSafeInteger(requestedOverscan) && requestedOverscan >= 0 ? requestedOverscan : 5;
611
+ if (overscan !== requestedOverscan) recover("invalid_virtual_option", "Overscan must be a non-negative safe integer; 5 is used.");
612
+ const estimate = typeof next.estimateSize === "number" ? validSize(next.estimateSize) : void 0;
613
+ const nextKeys = [];
614
+ const nextSizes = [];
615
+ const nextByKey = /* @__PURE__ */ new Map();
616
+ let firstChanged = count;
617
+ let uniform = estimate !== void 0;
618
+ for (let index = 0; index < count; index += 1) {
619
+ let key = next.getKey ? next.getKey(index) : index;
620
+ if (!(typeof key === "string" || typeof key === "number" && Number.isFinite(key))) {
621
+ recover("invalid_virtual_option", `Item ${index} has an invalid key; its index is used.`);
622
+ key = index;
623
+ }
624
+ if (nextByKey.has(key)) {
625
+ recover("duplicate_virtual_key", `Item ${index} repeats a key; a distinct fallback key is used.`);
626
+ key = JSON.stringify(["virtual", index]);
627
+ while (nextByKey.has(key)) key += ":";
628
+ }
629
+ const size = measured.get(key) ?? estimate ?? validSize(next.estimateSize(index));
630
+ uniform &&= size === estimate;
631
+ if (keys[index] !== key || sizes[index] !== size) firstChanged = Math.min(firstChanged, index);
632
+ nextKeys.push(key);
633
+ nextSizes.push(size);
634
+ nextByKey.set(key, index);
635
+ }
636
+ if (fixedSize !== void 0 || uniform) prefix = [0];
637
+ else prefix.length = Math.min(prefix.length, firstChanged + 1);
638
+ keys = nextKeys;
639
+ sizes = nextSizes;
640
+ byKey = nextByKey;
641
+ fixedSize = uniform ? estimate : void 0;
642
+ cached = void 0;
643
+ }
644
+ function startOf(index) {
645
+ if (fixedSize !== void 0) return index * fixedSize;
646
+ while (prefix.length <= index) {
647
+ const at = prefix.length - 1;
648
+ prefix.push(prefix[at] + sizes[at]);
649
+ }
650
+ return prefix[index];
651
+ }
652
+ function totalSize() {
653
+ return startOf(count);
654
+ }
655
+ function indexAt(position) {
656
+ if (fixedSize !== void 0) return Math.min(count - 1, Math.floor(position / fixedSize));
657
+ let low = 0;
658
+ let high = count - 1;
659
+ while (low < high) {
660
+ const middle = Math.floor((low + high) / 2);
661
+ if (startOf(middle + 1) <= position) low = middle + 1;
662
+ else high = middle;
663
+ }
664
+ return low;
665
+ }
666
+ function getWindow() {
667
+ if (cached) return cached;
668
+ const total = totalSize();
669
+ const clampedOffset = Math.min(offset, Math.max(0, total - viewportSize));
670
+ let first = 0;
671
+ let last = -1;
672
+ if (count > 0 && viewportSize > 0) {
673
+ first = Math.max(0, indexAt(clampedOffset) - overscan);
674
+ const edge = Math.min(total, clampedOffset + viewportSize);
675
+ let visibleEnd = indexAt(edge);
676
+ if (startOf(visibleEnd) >= edge) visibleEnd -= 1;
677
+ last = Math.min(count - 1, visibleEnd + overscan);
678
+ }
679
+ const items = [];
680
+ for (let index = first; index <= last; index += 1) items.push(Object.freeze({
681
+ index,
682
+ key: keys[index],
683
+ start: startOf(index),
684
+ size: sizes[index]
685
+ }));
686
+ const paddingStart = items.length > 0 ? startOf(first) : 0;
687
+ const paddingEnd = items.length > 0 ? Math.max(0, total - startOf(last + 1)) : total;
688
+ cached = Object.freeze({
689
+ items: Object.freeze(items),
690
+ totalSize: total,
691
+ paddingStart,
692
+ paddingEnd,
693
+ startIndex: first,
694
+ endIndex: last,
695
+ offset: clampedOffset,
696
+ viewportSize,
697
+ issues: Object.freeze([...problems.values()])
698
+ });
699
+ return cached;
700
+ }
701
+ function notify() {
702
+ cached = void 0;
703
+ if (listeners.size > 0) {
704
+ const view = getWindow();
705
+ for (const listener of listeners) listener(view);
706
+ }
707
+ }
708
+ configure(initialOptions);
709
+ return {
710
+ setOptions(next) {
711
+ configure(next);
712
+ notify();
713
+ },
714
+ setViewport(nextOffset, nextSize) {
715
+ const issueCount = problems.size;
716
+ const safeOffset = Number.isFinite(nextOffset) && nextOffset >= 0 ? nextOffset : 0;
717
+ const safeSize = Number.isFinite(nextSize) && nextSize >= 0 ? nextSize : 0;
718
+ if (safeOffset !== nextOffset || safeSize !== nextSize) recover("invalid_virtual_viewport", "Viewport offset and size must be non-negative and finite; invalid values use 0.");
719
+ if (offset === safeOffset && viewportSize === safeSize && problems.size === issueCount) return;
720
+ offset = safeOffset;
721
+ viewportSize = safeSize;
722
+ notify();
723
+ },
724
+ measure(key, size) {
725
+ const index = byKey.get(key);
726
+ if (index === void 0) return;
727
+ if (!Number.isFinite(size) || size <= 0) {
728
+ recover("invalid_virtual_size", "Measured size must be positive and finite; the previous size is retained.");
729
+ notify();
730
+ return;
731
+ }
732
+ measured.set(key, size);
733
+ if (sizes[index] === size) return;
734
+ if (fixedSize !== void 0) {
735
+ fixedSize = void 0;
736
+ prefix = [0];
737
+ } else prefix.length = Math.min(prefix.length, index + 1);
738
+ sizes[index] = size;
739
+ notify();
740
+ },
741
+ getWindow,
742
+ getItem(index) {
743
+ return Number.isSafeInteger(index) && index >= 0 && index < count ? Object.freeze({
744
+ index,
745
+ key: keys[index],
746
+ start: startOf(index),
747
+ size: sizes[index]
748
+ }) : void 0;
749
+ },
750
+ getOffsetForIndex(index, align = "auto") {
751
+ if (count === 0) return 0;
752
+ if (!Number.isFinite(index) || !Number.isInteger(index)) recover("invalid_virtual_option", "Scroll index must be a finite integer; it is clamped to a valid item.");
753
+ const target = Math.min(count - 1, Math.max(0, Number.isFinite(index) ? Math.trunc(index) : 0));
754
+ const start = startOf(target);
755
+ const end = start + sizes[target];
756
+ const current = getWindow().offset;
757
+ let result = start;
758
+ if (align === "center") result = start - (viewportSize - sizes[target]) / 2;
759
+ else if (align === "end") result = end - viewportSize;
760
+ else if (align === "auto") result = start < current ? start : end > current + viewportSize ? end - viewportSize : current;
761
+ return Math.min(Math.max(0, totalSize() - viewportSize), Math.max(0, result));
762
+ },
763
+ subscribe(listener) {
764
+ listeners.add(listener);
765
+ return () => listeners.delete(listener);
465
766
  }
466
767
  };
467
768
  }
@@ -526,6 +827,10 @@ function filterRows(rows, columns, search, filters) {
526
827
  }
527
828
  /** Sort rows by each rule in turn. The sort is stable: ties keep their incoming order. */
528
829
  function sortRows(rows, columns, sorting) {
830
+ if (!sorting.some((rule) => columns.some((column) => column.id === rule.column))) return rows;
831
+ return rows.toSorted(compareRows(columns, sorting));
832
+ }
833
+ function compareRows(columns, sorting) {
529
834
  const byId = new Map(columns.map((column) => [column.id, column]));
530
835
  const rules = sorting.flatMap((rule) => {
531
836
  const column = byId.get(rule.column);
@@ -534,8 +839,7 @@ function sortRows(rows, columns, sorting) {
534
839
  sign: rule.direction === "desc" ? -1 : 1
535
840
  }] : [];
536
841
  });
537
- if (rules.length === 0) return rows;
538
- return rows.toSorted((left, right) => {
842
+ return (left, right) => {
539
843
  for (const { column, sign } of rules) {
540
844
  const leftValue = left.getValue(column.id);
541
845
  const rightValue = right.getValue(column.id);
@@ -545,7 +849,19 @@ function sortRows(rows, columns, sorting) {
545
849
  if (order !== 0) return order;
546
850
  }
547
851
  return 0;
548
- });
852
+ };
853
+ }
854
+ /** Stable merge: an existing row wins a tie against an incoming row. */
855
+ function mergeRows(previous, incoming, columns, sorting) {
856
+ const compare = compareRows(columns, sorting);
857
+ const added = sortRows(incoming, columns, sorting);
858
+ const result = [];
859
+ let left = 0;
860
+ let right = 0;
861
+ while (left < previous.length && right < added.length) result.push(compare(previous[left], added[right]) <= 0 ? previous[left++] : added[right++]);
862
+ while (left < previous.length) result.push(previous[left++]);
863
+ while (right < added.length) result.push(added[right++]);
864
+ return result;
549
865
  }
550
866
  /** The number of pages for a row count. An empty table still has one page. */
551
867
  function countPages(rowCount, pageSize) {
@@ -593,10 +909,54 @@ function paginationItems(page, pageCount, siblings = 1) {
593
909
  return items;
594
910
  }
595
911
 
912
+ //#endregion
913
+ //#region src/row.ts
914
+ /** Disjoint namespaces and typed keys avoid collisions between 1, "1", and detail-like ids. */
915
+ function getRowItemKey(key, kind = "row") {
916
+ return JSON.stringify([
917
+ kind,
918
+ typeof key,
919
+ key
920
+ ]);
921
+ }
922
+ function createRow(original, key, index, columns, canExpand = false) {
923
+ const values = /* @__PURE__ */ new Map();
924
+ const displays = /* @__PURE__ */ new Map();
925
+ const getValue = (columnId) => {
926
+ if (values.has(columnId)) return values.get(columnId);
927
+ const value = columns.get(columnId)?.getValue(original);
928
+ values.set(columnId, value);
929
+ return value;
930
+ };
931
+ return Object.freeze({
932
+ key,
933
+ index,
934
+ original,
935
+ canExpand,
936
+ isExpanded: false,
937
+ depth: 0,
938
+ parentKey: void 0,
939
+ childCount: 0,
940
+ childStatus: "loaded",
941
+ selection: "none",
942
+ isDirty: false,
943
+ cellIssues: void 0,
944
+ getValue,
945
+ getDisplay(columnId) {
946
+ const cached = displays.get(columnId);
947
+ if (cached !== void 0) return cached;
948
+ const column = columns.get(columnId);
949
+ const display = column ? column.format(getValue(columnId), original) : "";
950
+ displays.set(columnId, display);
951
+ return display;
952
+ }
953
+ });
954
+ }
955
+
596
956
  //#endregion
597
957
  //#region src/state.ts
598
958
  const DEFAULT_PAGE_SIZE = 10;
599
- function createInitialState(patch = {}) {
959
+ function createInitialState(patch = {}, onIssue) {
600
960
  return freezeState({
601
961
  sorting: patch.sorting ?? [],
602
962
  search: patch.search ?? "",
@@ -606,145 +966,1051 @@ function createInitialState(patch = {}) {
606
966
  pageSize: 10
607
967
  },
608
968
  selection: patch.selection ?? [],
969
+ expanded: normalizeExpanded(patch.expanded === void 0 ? [] : patch.expanded, onIssue),
609
970
  hiddenColumns: patch.hiddenColumns ?? [],
610
971
  columnOrder: patch.columnOrder ?? []
611
972
  });
612
973
  }
613
- function mergeState(state, patch) {
974
+ function mergeState(state, patch, onIssue) {
614
975
  let changed = false;
615
976
  const next = { ...state };
616
- for (const [key, value] of Object.entries(patch)) {
977
+ for (const [key, input] of Object.entries(patch)) {
978
+ const value = key === "expanded" && input !== void 0 ? normalizeExpanded(input, onIssue) : input;
617
979
  if (value === void 0 || Object.is(next[key], value)) continue;
618
980
  if (key === "pagination" && samePagination(next[key], value)) continue;
981
+ if (key === "expanded" && sameExpanded(state.expanded, value)) continue;
619
982
  next[key] = value;
620
983
  changed = true;
621
984
  }
622
- return changed ? freezeState(next) : state;
985
+ return changed ? freezeState(next, state) : state;
986
+ }
987
+ function normalizeExpanded(value, onIssue) {
988
+ if (value === true) return true;
989
+ if (Array.isArray(value) && value.every((key) => typeof key === "string" || typeof key === "number" && Number.isFinite(key))) return [...new Set(value)];
990
+ onIssue?.(issue("invalid_expanded", "Expanded state must be true or an array of string/finite number keys; an empty array is used."));
991
+ return [];
992
+ }
993
+ function sameExpanded(left, right) {
994
+ return left === right || left !== true && right !== true && left.length === right.length && left.every((key, index) => key === right[index]);
623
995
  }
624
996
  function samePagination(left, right) {
625
997
  return left.page === right.page && left.pageSize === right.pageSize;
626
998
  }
627
- function freezeState(state) {
999
+ function freezeState(state, previous) {
628
1000
  return Object.freeze({
629
- sorting: Object.freeze(state.sorting.map((rule) => Object.freeze({ ...rule }))),
1001
+ sorting: state.sorting === previous?.sorting ? previous.sorting : Object.freeze(state.sorting.map((rule) => Object.freeze({ ...rule }))),
630
1002
  search: state.search,
631
- filters: Object.freeze({ ...state.filters }),
632
- pagination: Object.freeze({ ...state.pagination }),
633
- selection: Object.freeze([...state.selection]),
634
- hiddenColumns: Object.freeze([...state.hiddenColumns]),
635
- columnOrder: Object.freeze([...state.columnOrder])
1003
+ filters: state.filters === previous?.filters ? previous.filters : Object.freeze({ ...state.filters }),
1004
+ pagination: state.pagination === previous?.pagination ? previous.pagination : Object.freeze({ ...state.pagination }),
1005
+ selection: state.selection === previous?.selection ? previous.selection : Object.freeze([...state.selection]),
1006
+ expanded: state.expanded === previous?.expanded ? previous.expanded : state.expanded === true ? true : Object.freeze([...state.expanded]),
1007
+ hiddenColumns: state.hiddenColumns === previous?.hiddenColumns ? previous.hiddenColumns : Object.freeze([...state.hiddenColumns]),
1008
+ columnOrder: state.columnOrder === previous?.columnOrder ? previous.columnOrder : Object.freeze([...state.columnOrder])
636
1009
  });
637
1010
  }
638
1011
 
639
1012
  //#endregion
640
- //#region src/issues.ts
641
- function issue(code, message, details = {}) {
642
- return Object.freeze({
643
- code,
644
- message,
645
- ...details
1013
+ //#region src/validation.ts
1014
+ function isPromise(value) {
1015
+ return typeof value === "object" && value !== null && "then" in value;
1016
+ }
1017
+ /** Validators receive values; their messages remain plain text. */
1018
+ function runValidation(validate, rowKey, column) {
1019
+ const fail = (message, validationCode) => issue("validation_failed", message, {
1020
+ rowKey,
1021
+ column,
1022
+ validationCode
646
1023
  });
1024
+ const read = (result) => {
1025
+ if (result === true) return void 0;
1026
+ if (typeof result === "string") return fail(result);
1027
+ if (result && typeof result.message === "string") return fail(result.message, result.code);
1028
+ return fail("The validator returned an invalid result.");
1029
+ };
1030
+ const thrown = (error) => fail(error instanceof Error ? error.message : String(error));
1031
+ try {
1032
+ const result = validate();
1033
+ return isPromise(result) ? Promise.resolve(result).then(read, thrown) : read(result);
1034
+ } catch (error) {
1035
+ return thrown(error);
1036
+ }
1037
+ }
1038
+ function editorFailure(spec, value) {
1039
+ if (!spec) return void 0;
1040
+ if (spec.maxLength !== void 0 && (!Number.isSafeInteger(spec.maxLength) || spec.maxLength < 0)) return "Editor maxLength must be a non-negative safe integer.";
1041
+ if (spec.min !== void 0 && !Number.isFinite(spec.min) || spec.max !== void 0 && !Number.isFinite(spec.max) || spec.min !== void 0 && spec.max !== void 0 && spec.min > spec.max) return "Editor bounds must be finite and ordered.";
1042
+ if (spec.options && !spec.options.some((option) => Object.is(option, value))) return "Choose one of the allowed values.";
1043
+ if (spec.maxLength !== void 0 && typeof value === "string" && value.length > spec.maxLength) return `Use at most ${spec.maxLength} characters.`;
1044
+ if (spec.min !== void 0 || spec.max !== void 0) {
1045
+ const number = value instanceof Date ? value.getTime() : value;
1046
+ if (typeof number !== "number" || !Number.isFinite(number)) return "Enter a finite numeric value.";
1047
+ if (spec.min !== void 0 && number < spec.min) return `Use a value of at least ${spec.min}.`;
1048
+ if (spec.max !== void 0 && number > spec.max) return `Use a value of at most ${spec.max}.`;
1049
+ }
1050
+ if (spec.pattern !== void 0) try {
1051
+ if (typeof value !== "string" || !new RegExp(spec.pattern, "u").test(value)) return "The value does not match the required pattern.";
1052
+ } catch {
1053
+ return "The editor pattern is invalid.";
1054
+ }
647
1055
  }
648
1056
 
649
1057
  //#endregion
650
- //#region src/row.ts
651
- function createRow(original, key, index, columns) {
652
- const values = /* @__PURE__ */ new Map();
653
- const displays = /* @__PURE__ */ new Map();
654
- const getValue = (columnId) => {
655
- if (values.has(columnId)) return values.get(columnId);
656
- const value = columns.get(columnId)?.getValue(original);
657
- values.set(columnId, value);
658
- return value;
1058
+ //#region src/edit-validation.ts
1059
+ function readonlyMap(source) {
1060
+ const copy = new Map(source);
1061
+ const result = {
1062
+ size: copy.size,
1063
+ get: (key) => copy.get(key),
1064
+ has: (key) => copy.has(key),
1065
+ entries: () => copy.entries(),
1066
+ keys: () => copy.keys(),
1067
+ values: () => copy.values(),
1068
+ [Symbol.iterator]: () => copy[Symbol.iterator](),
1069
+ forEach(callback, thisArg) {
1070
+ copy.forEach((value, key) => callback.call(thisArg, value, key, result));
1071
+ }
659
1072
  };
660
- return Object.freeze({
661
- key,
662
- index,
663
- original,
664
- getValue,
665
- getDisplay(columnId) {
666
- const cached = displays.get(columnId);
667
- if (cached !== void 0) return cached;
668
- const column = columns.get(columnId);
669
- const display = column ? column.format(getValue(columnId), original) : "";
670
- displays.set(columnId, display);
671
- return display;
1073
+ return Object.freeze(result);
1074
+ }
1075
+ /** All promises in one edit settle together. Tokens fence every cell against stale results. */
1076
+ function createEditValidation(host) {
1077
+ const tokens = /* @__PURE__ */ new Map();
1078
+ const pending = /* @__PURE__ */ new Map();
1079
+ const problems = /* @__PURE__ */ new Map();
1080
+ const current = (candidate) => tokens.get(candidate.edit.rowKey)?.get(candidate.edit.column) === candidate.token;
1081
+ const fresh = (candidate) => !candidate.guard || host.snapshot().getRow(candidate.edit.rowKey)?.original === candidate.guard.row;
1082
+ const tokenFor = (rowKey, column) => {
1083
+ const row = tokens.get(rowKey) ?? /* @__PURE__ */ new Map();
1084
+ const token = {};
1085
+ row.set(column, token);
1086
+ tokens.set(rowKey, row);
1087
+ pending.get(rowKey)?.delete(column);
1088
+ problems.get(rowKey)?.delete(column);
1089
+ return token;
1090
+ };
1091
+ const setIssues = (issues) => {
1092
+ for (const problem of issues) {
1093
+ if (problem.rowKey === void 0 || problem.column === void 0) continue;
1094
+ const row = problems.get(problem.rowKey) ?? /* @__PURE__ */ new Map();
1095
+ row.set(problem.column, problem);
1096
+ problems.set(problem.rowKey, row);
672
1097
  }
673
- });
1098
+ };
1099
+ function rowValidation(candidates, failures) {
1100
+ if (!host.validateRow) return [...candidates];
1101
+ const rows = /* @__PURE__ */ new Map();
1102
+ for (const candidate of candidates) {
1103
+ const group = rows.get(candidate.edit.rowKey) ?? [];
1104
+ group.push(candidate);
1105
+ rows.set(candidate.edit.rowKey, group);
1106
+ }
1107
+ const results = [];
1108
+ for (const [key, group] of rows) {
1109
+ const previous = group[0].original;
1110
+ let next = previous;
1111
+ try {
1112
+ for (const candidate of group) next = candidate.column.setValue(next, candidate.edit.value) ?? next;
1113
+ } catch (error) {
1114
+ for (const candidate of group) failures.push(issue("validation_failed", String(error), {
1115
+ rowKey: key,
1116
+ column: candidate.edit.column
1117
+ }));
1118
+ continue;
1119
+ }
1120
+ const validation = runValidation(() => host.validateRow(next, previous), key);
1121
+ const accept = (problem) => {
1122
+ if (!problem) return group;
1123
+ for (const candidate of group) if (current(candidate)) failures.push(Object.freeze({
1124
+ ...problem,
1125
+ column: candidate.edit.column
1126
+ }));
1127
+ return [];
1128
+ };
1129
+ results.push(isPromise(validation) ? validation.then(accept) : accept(validation));
1130
+ }
1131
+ return results.some(isPromise) ? Promise.all(results.map((result) => Promise.resolve(result))).then((groups) => groups.flat()) : results.flat();
1132
+ }
1133
+ function settle(accepted, all, failures, asynchronous) {
1134
+ return host.batch(() => {
1135
+ const live = accepted.filter((candidate) => current(candidate) && fresh(candidate));
1136
+ const liveKeys = new Set(live);
1137
+ const active = all.filter(current);
1138
+ for (const candidate of active) {
1139
+ const key = candidate.edit.rowKey;
1140
+ pending.get(key)?.delete(candidate.edit.column);
1141
+ if (pending.get(key)?.size === 0) pending.delete(key);
1142
+ }
1143
+ const visibleFailures = failures;
1144
+ setIssues(visibleFailures);
1145
+ let result;
1146
+ if (asynchronous && host.optimistic) {
1147
+ const rollback = active.filter((candidate) => !liveKeys.has(candidate)).map((candidate) => ({
1148
+ ...candidate.edit,
1149
+ value: candidate.previous
1150
+ }));
1151
+ result = host.apply(rollback, false, visibleFailures);
1152
+ const changes = live.map((candidate) => Object.freeze({
1153
+ rowKey: candidate.edit.rowKey,
1154
+ column: candidate.edit.column,
1155
+ previous: candidate.previous,
1156
+ value: candidate.edit.value,
1157
+ row: host.snapshot().getRow(candidate.edit.rowKey).original
1158
+ }));
1159
+ if (changes.length) host.record(changes);
1160
+ result = Object.freeze({
1161
+ ...result,
1162
+ changes: Object.freeze(changes),
1163
+ status: changes.length ? visibleFailures.length ? "partial" : "applied" : visibleFailures.length ? "rejected" : "unchanged"
1164
+ });
1165
+ } else result = host.apply(live.map((candidate) => candidate.edit), true, visibleFailures);
1166
+ setIssues(result.issues);
1167
+ if (!result.changes.length && (all.length || result.issues.length) || asynchronous && host.optimistic) host.notify();
1168
+ if (asynchronous && !result.issues.length && active.length) host.onIssues([]);
1169
+ return result;
1170
+ });
1171
+ }
1172
+ return {
1173
+ hasPending: (key) => (pending.get(key)?.size ?? 0) > 0,
1174
+ cells: () => Object.freeze([...pending.values()].flatMap((row) => [...row.values()])),
1175
+ problems: () => new Map([...problems].filter(([, row]) => row.size).map(([key, row]) => [key, readonlyMap(row)])),
1176
+ cancel(keys) {
1177
+ if (!keys) {
1178
+ tokens.clear();
1179
+ pending.clear();
1180
+ problems.clear();
1181
+ } else for (const key of keys) {
1182
+ tokens.delete(key);
1183
+ pending.delete(key);
1184
+ problems.delete(key);
1185
+ }
1186
+ },
1187
+ edit(edits, recovered = [], options) {
1188
+ const metadataChanged = edits.some((edit) => pending.get(edit.rowKey)?.has(edit.column) || problems.get(edit.rowKey)?.has(edit.column) || host.validateRow && pending.get(edit.rowKey)?.size);
1189
+ const snapshot = host.snapshot();
1190
+ const failures = [...recovered];
1191
+ const candidates = [];
1192
+ const guards = new Map([...options?.expectedRows ?? []].map(([key, row]) => [key, { row }]));
1193
+ const drafts = /* @__PURE__ */ new Map();
1194
+ const samples = /* @__PURE__ */ new Map();
1195
+ if (host.validateRow) for (const key of new Set(edits.map((edit) => edit.rowKey))) {
1196
+ tokens.delete(key);
1197
+ pending.delete(key);
1198
+ }
1199
+ for (const edit of edits) {
1200
+ const token = tokenFor(edit.rowKey, edit.column);
1201
+ const row = snapshot.getRow(edit.rowKey);
1202
+ const column = snapshot.getColumn(edit.column);
1203
+ const fail = (code, message) => {
1204
+ failures.push(issue(code, message, {
1205
+ rowKey: edit.rowKey,
1206
+ column: edit.column
1207
+ }));
1208
+ };
1209
+ if (!isSafePath(edit.column)) {
1210
+ fail("unsafe_path", "This path addresses a prototype and cannot be written.");
1211
+ continue;
1212
+ }
1213
+ if (!row) {
1214
+ fail("unknown_row", `No row has the key "${String(edit.rowKey)}".`);
1215
+ continue;
1216
+ }
1217
+ const guard = guards.get(row.key);
1218
+ if (guard && row.original !== guard.row) {
1219
+ fail("stale_draft", "The row changed after this draft was opened.");
1220
+ continue;
1221
+ }
1222
+ if (!column) {
1223
+ fail("unknown_column", `No column has the id "${edit.column}".`);
1224
+ continue;
1225
+ }
1226
+ const original = drafts.get(row.key) ?? row.original;
1227
+ try {
1228
+ if (!column.isEditable(original)) {
1229
+ fail("read_only_cell", `The "${column.id}" cell is read-only.`);
1230
+ continue;
1231
+ }
1232
+ let value;
1233
+ if ("input" in edit) {
1234
+ if (!samples.has(column.id)) samples.set(column.id, snapshot.processedRows.find((candidate) => candidate.getValue(column.id) !== null && candidate.getValue(column.id) !== void 0)?.getValue(column.id));
1235
+ const parsed = column.parse(edit.input, original, samples.get(column.id));
1236
+ if (!parsed.ok) {
1237
+ fail("invalid_value", parsed.message);
1238
+ continue;
1239
+ }
1240
+ value = parsed.value;
1241
+ } else value = edit.value;
1242
+ const previous = column.getValue(original);
1243
+ if (Object.is(previous, value)) continue;
1244
+ const constraint = editorFailure(column.definition.editor, value);
1245
+ if (constraint) {
1246
+ fail("validation_failed", constraint);
1247
+ continue;
1248
+ }
1249
+ const next = column.setValue(original, value);
1250
+ if (next === void 0) {
1251
+ fail("read_only_cell", "The column has no way to write a value.");
1252
+ continue;
1253
+ }
1254
+ const writeProblem = host.writeIssue(row, next);
1255
+ if (writeProblem) {
1256
+ failures.push(Object.freeze({
1257
+ ...writeProblem,
1258
+ column: column.id
1259
+ }));
1260
+ continue;
1261
+ }
1262
+ const validate = column.definition.validate;
1263
+ const validation = validate ? runValidation(() => validate(value, row.original), row.key, column.id) : void 0;
1264
+ if (validation && !isPromise(validation)) {
1265
+ failures.push(validation);
1266
+ continue;
1267
+ }
1268
+ drafts.set(row.key, next);
1269
+ candidates.push({
1270
+ edit: {
1271
+ rowKey: row.key,
1272
+ column: column.id,
1273
+ value
1274
+ },
1275
+ previous: column.getValue(row.original),
1276
+ original: row.original,
1277
+ column,
1278
+ validation,
1279
+ token,
1280
+ guard
1281
+ });
1282
+ } catch (error) {
1283
+ fail("invalid_value", error instanceof Error ? error.message : String(error));
1284
+ }
1285
+ }
1286
+ const asynchronous = candidates.some((candidate) => isPromise(candidate.validation));
1287
+ const validateRows = (accepted) => rowValidation(accepted.filter((candidate) => current(candidate) && fresh(candidate)), failures);
1288
+ const outcome = asynchronous ? Promise.all(candidates.map(async (candidate) => {
1289
+ const problem = await candidate.validation;
1290
+ if (problem) {
1291
+ if (current(candidate)) failures.push(problem);
1292
+ return;
1293
+ }
1294
+ return candidate;
1295
+ })).then((accepted) => validateRows(accepted.filter((candidate) => candidate !== void 0))) : validateRows(candidates);
1296
+ if (!isPromise(outcome)) {
1297
+ const result = settle(outcome, candidates, failures, false);
1298
+ if (metadataChanged && !candidates.length && !failures.length) host.notify();
1299
+ return result;
1300
+ }
1301
+ for (const candidate of candidates) {
1302
+ const row = pending.get(candidate.edit.rowKey) ?? /* @__PURE__ */ new Map();
1303
+ row.set(candidate.edit.column, Object.freeze({ ...candidate.edit }));
1304
+ pending.set(candidate.edit.rowKey, row);
1305
+ }
1306
+ setIssues(failures);
1307
+ const optimistic = host.optimistic ? host.apply(candidates.map((candidate) => candidate.edit), false, failures) : void 0;
1308
+ if (optimistic) for (const [key, guard] of guards) {
1309
+ const row = host.snapshot().getRow(key);
1310
+ if (row) guard.row = row.original;
1311
+ }
1312
+ if (!optimistic?.changes.length) host.notify();
1313
+ if (!host.optimistic && failures.length) host.onIssues(failures);
1314
+ const completion = outcome.then((accepted) => {
1315
+ const staleCandidates = candidates.filter((candidate) => !current(candidate) || !fresh(candidate)).map((candidate) => ({
1316
+ candidate,
1317
+ problem: issue("stale_draft", "This edit was superseded or its row is no longer current.", {
1318
+ rowKey: candidate.edit.rowKey,
1319
+ column: candidate.edit.column
1320
+ })
1321
+ }));
1322
+ const stale = staleCandidates.map(({ problem }) => problem);
1323
+ const issues = [...failures, ...stale];
1324
+ if (candidates.some(current)) {
1325
+ const superseded = staleCandidates.filter(({ candidate }) => !current(candidate)).map(({ problem }) => problem);
1326
+ const result = settle(accepted, candidates, [...failures, ...staleCandidates.filter(({ candidate }) => current(candidate)).map(({ problem }) => problem)], true);
1327
+ if (!stale.length) return result;
1328
+ return Object.freeze({
1329
+ ...result,
1330
+ issues: Object.freeze([...result.issues, ...superseded]),
1331
+ status: result.changes.length ? "partial" : "rejected"
1332
+ });
1333
+ }
1334
+ return Object.freeze({
1335
+ status: "rejected",
1336
+ changes: Object.freeze([]),
1337
+ rowChanges: Object.freeze([]),
1338
+ issues: Object.freeze(issues),
1339
+ pendingCells: Object.freeze([])
1340
+ });
1341
+ });
1342
+ return Object.freeze({
1343
+ status: "pending",
1344
+ changes: optimistic?.changes ?? Object.freeze([]),
1345
+ rowChanges: Object.freeze([]),
1346
+ issues: Object.freeze([...failures]),
1347
+ pendingCells: Object.freeze(candidates.map((candidate) => Object.freeze({ ...candidate.edit }))),
1348
+ completion
1349
+ });
1350
+ }
1351
+ };
674
1352
  }
675
1353
 
676
1354
  //#endregion
677
- //#region src/table.ts
678
- function isRecord(value) {
679
- return typeof value === "object" && value !== null;
680
- }
681
- function memo(compute) {
682
- let last;
683
- return (...deps) => {
684
- if (last && last.deps.length === deps.length && last.deps.every((d, i) => Object.is(d, deps[i]))) return last.result;
685
- const result = compute(...deps);
686
- last = {
687
- deps,
688
- result
689
- };
690
- return result;
1355
+ //#region src/pending-changes.ts
1356
+ /** A saved baseline only for touched rows; viewing changes never scans source data. */
1357
+ function createPendingChanges() {
1358
+ const entries = /* @__PURE__ */ new Map();
1359
+ return {
1360
+ entries,
1361
+ snapshot() {
1362
+ const inserted = [];
1363
+ const removed = [];
1364
+ const updated = [];
1365
+ for (const [rowKey, entry] of entries) if (entry.previous === void 0 && entry.row !== void 0) inserted.push(Object.freeze({
1366
+ kind: "inserted",
1367
+ rowKey,
1368
+ row: entry.row,
1369
+ position: entry.position
1370
+ }));
1371
+ else if (entry.row === void 0 && entry.previous !== void 0) removed.push(Object.freeze({
1372
+ kind: "removed",
1373
+ rowKey,
1374
+ row: entry.previous,
1375
+ position: entry.position
1376
+ }));
1377
+ else if (entry.row !== void 0 && entry.previous !== void 0) updated.push(Object.freeze({
1378
+ rowKey,
1379
+ row: entry.row,
1380
+ previous: entry.previous,
1381
+ changes: Object.freeze([...entry.cells.values()])
1382
+ }));
1383
+ return Object.freeze({
1384
+ inserted: Object.freeze(inserted),
1385
+ updated: Object.freeze(updated),
1386
+ removed: Object.freeze(removed)
1387
+ });
1388
+ },
1389
+ record(key, previous, row, position, changes) {
1390
+ const entry = entries.get(key) ?? {
1391
+ previous,
1392
+ row,
1393
+ position: Object.freeze({ ...position }),
1394
+ cells: /* @__PURE__ */ new Map()
1395
+ };
1396
+ entry.row = row;
1397
+ for (const change of changes) {
1398
+ const original = entry.cells.has(change.column) ? entry.cells.get(change.column).previous : change.previous;
1399
+ if (Object.is(original, change.value)) entry.cells.delete(change.column);
1400
+ else entry.cells.set(change.column, Object.freeze({
1401
+ ...change,
1402
+ previous: original
1403
+ }));
1404
+ }
1405
+ if (entry.previous === void 0 && row === void 0 || entry.previous !== void 0 && row !== void 0 && entry.cells.size === 0) entries.delete(key);
1406
+ else entries.set(key, entry);
1407
+ }
691
1408
  };
692
1409
  }
693
- function isInputEdit(edit) {
694
- return "input" in edit;
1410
+
1411
+ //#endregion
1412
+ //#region src/row-list.ts
1413
+ /** Read-only array views keep append publication independent of existing list length. */
1414
+ function indexOf(property) {
1415
+ if (typeof property !== "string" || !/^(0|[1-9]\d*)$/u.test(property)) return void 0;
1416
+ return Number(property);
695
1417
  }
696
- function coverage(selected, total) {
697
- if (selected === 0 || total === 0) return "none";
698
- return selected === total ? "all" : "some";
1418
+ function rowList(length, read) {
1419
+ return new Proxy([], {
1420
+ get(target, property, receiver) {
1421
+ if (property === "length") return length;
1422
+ const index = indexOf(property);
1423
+ return index === void 0 ? Reflect.get(target, property, receiver) : index < length ? read(index) : void 0;
1424
+ },
1425
+ has(target, property) {
1426
+ const index = indexOf(property);
1427
+ return index === void 0 ? Reflect.has(target, property) : index < length;
1428
+ },
1429
+ ownKeys: () => [...Array.from({ length }, (_, index) => String(index)), "length"],
1430
+ getOwnPropertyDescriptor(target, property) {
1431
+ if (property === "length") return {
1432
+ ...Reflect.getOwnPropertyDescriptor(target, property),
1433
+ value: length
1434
+ };
1435
+ const index = indexOf(property);
1436
+ return index === void 0 ? Reflect.getOwnPropertyDescriptor(target, property) : index < length ? {
1437
+ configurable: true,
1438
+ enumerable: true,
1439
+ writable: false,
1440
+ value: read(index)
1441
+ } : void 0;
1442
+ },
1443
+ set: () => false,
1444
+ deleteProperty: () => false,
1445
+ defineProperty: () => false,
1446
+ preventExtensions: () => false,
1447
+ setPrototypeOf: () => false
1448
+ });
699
1449
  }
700
- /** Create a data table. */
701
- function createTable(options) {
702
- const selectionMode = options.selectionMode ?? "multiple";
703
- const selectScope = options.selectScope ?? "all";
704
- const manual = options.manual ?? false;
705
- const undoStack = createUndoStack(options.historyLimit);
706
- const listeners = /* @__PURE__ */ new Set();
707
- let data = options.data;
708
- let definitions = options.columns;
709
- let rowCount = options.rowCount;
710
- const initialState = createInitialState({
711
- ...options.initialState,
712
- hiddenColumns: options.initialState?.hiddenColumns ?? options.columns.filter((column) => column.hidden === true).map((column) => column.id)
1450
+ const stores = /* @__PURE__ */ new WeakMap();
1451
+ function appendList(previous, incoming) {
1452
+ if (!incoming.length) return previous;
1453
+ let store = stores.get(previous);
1454
+ const last = store?.chunks.at(-1);
1455
+ if (!store || last && last.start + last.rows.length !== previous.length) store = { chunks: [{
1456
+ start: 0,
1457
+ rows: previous
1458
+ }] };
1459
+ store.chunks.push({
1460
+ start: previous.length,
1461
+ rows: incoming
713
1462
  });
714
- let state = initialState;
715
- let snapshot;
716
- const readKey = (row, index) => {
717
- const { rowKey } = options;
718
- if (typeof rowKey === "function") return rowKey(row, index);
719
- const value = getPath(row, rowKey ?? "id");
720
- if (typeof value === "string" || typeof value === "number") return value;
721
- return rowKey === void 0 || !isRecord(row) ? index : String(value);
722
- };
723
- const resolveColumns = memo((defs) => {
724
- const columns = defs.map((definition) => resolveColumn(definition));
725
- return {
726
- columns,
727
- byId: new Map(columns.map((column) => [column.id, column]))
728
- };
1463
+ const chunks = store.chunks;
1464
+ const result = rowList(previous.length + incoming.length, (index) => {
1465
+ let low = 0;
1466
+ let high = chunks.length - 1;
1467
+ while (low < high) {
1468
+ const mid = Math.ceil((low + high) / 2);
1469
+ if (chunks[mid].start <= index) low = mid;
1470
+ else high = mid - 1;
1471
+ }
1472
+ const chunk = chunks[low];
1473
+ return chunk.rows[index - chunk.start];
729
1474
  });
730
- const indexRows = memo((rows, byId) => {
731
- const byKey = /* @__PURE__ */ new Map();
732
- const issues = [];
733
- return {
734
- rows: rows.map((original, index) => {
735
- let key = readKey(original, index);
736
- if (byKey.has(key)) {
737
- issues.push(issue("duplicate_row_key", `Row ${index} repeats the key "${String(key)}"; it is keyed "${String(key)}#${index}" instead.`, { rowKey: key }));
738
- key = `${String(key)}#${index}`;
739
- }
740
- const row = createRow(original, key, index, byId);
741
- byKey.set(key, row);
742
- return row;
743
- }),
1475
+ stores.set(result, store);
1476
+ return result;
1477
+ }
1478
+ function mapList(rows, map) {
1479
+ const cache = /* @__PURE__ */ new Map();
1480
+ return rowList(rows.length, (index) => {
1481
+ if (!cache.has(index)) cache.set(index, map(rows[index], index));
1482
+ return cache.get(index);
1483
+ });
1484
+ }
1485
+
1486
+ //#endregion
1487
+ //#region src/stream.ts
1488
+ function createStream(host) {
1489
+ let active;
1490
+ return {
1491
+ cancel() {
1492
+ active?.cancel();
1493
+ active = void 0;
1494
+ },
1495
+ async run(source, options = {}) {
1496
+ active?.cancel();
1497
+ const problems = [];
1498
+ const requested = options.batchSize ?? 1e3;
1499
+ const batchSize = Number.isSafeInteger(requested) && requested > 0 ? requested : 1e3;
1500
+ if (batchSize !== requested) problems.push(issue("invalid_stream_option", "batchSize must be a positive safe integer; 1,000 is used."));
1501
+ const requestedCount = options.expectedRowCount;
1502
+ const expected = requestedCount === void 0 || Number.isSafeInteger(requestedCount) && requestedCount >= 0 ? requestedCount : void 0;
1503
+ if (expected !== requestedCount) problems.push(issue("invalid_stream_option", "expectedRowCount must be a non-negative safe integer; it is unknown instead."));
1504
+ let cancelled = false;
1505
+ let wake = () => void 0;
1506
+ const cancellation = new Promise((resolve) => {
1507
+ wake = () => resolve(void 0);
1508
+ });
1509
+ const cancel = () => {
1510
+ cancelled = true;
1511
+ wake();
1512
+ };
1513
+ const run = { cancel };
1514
+ active = run;
1515
+ const signal = options.signal;
1516
+ signal?.addEventListener?.("abort", cancel, { once: true });
1517
+ if (signal?.aborted) run.cancel();
1518
+ let receivedRowCount = 0;
1519
+ let status = "done";
1520
+ let iterator;
1521
+ host.update(cancelled ? "aborted" : "loading", expected, problems, true);
1522
+ try {
1523
+ if (!cancelled) iterator = source[Symbol.asyncIterator]();
1524
+ while (true) {
1525
+ if (cancelled || !iterator) break;
1526
+ const next = await Promise.race([iterator.next(), cancellation]);
1527
+ if (!next || cancelled || signal?.aborted) {
1528
+ run.cancel();
1529
+ break;
1530
+ }
1531
+ if (next.done) break;
1532
+ const rows = Array.isArray(next.value) ? next.value : [next.value];
1533
+ for (let start = 0; start < rows.length; start += batchSize) {
1534
+ await Promise.resolve();
1535
+ if (cancelled || signal?.aborted) {
1536
+ run.cancel();
1537
+ break;
1538
+ }
1539
+ host.update("streaming", expected, problems, false);
1540
+ const result = host.ingest(rows.slice(start, start + batchSize), options.mode ?? "append");
1541
+ receivedRowCount += result.appended + result.replaced;
1542
+ problems.push(...result.issues);
1543
+ }
1544
+ }
1545
+ } catch (error) {
1546
+ status = "error";
1547
+ problems.push(issue("stream_error", error instanceof Error ? error.message : String(error)));
1548
+ } finally {
1549
+ signal?.removeEventListener?.("abort", cancel);
1550
+ if (cancelled) status = "aborted";
1551
+ if (active === run) {
1552
+ active = void 0;
1553
+ host.update(status, expected, problems, true);
1554
+ }
1555
+ if (status !== "done" && iterator?.return) Promise.resolve().then(() => iterator.return()).catch(() => void 0);
1556
+ }
1557
+ return Object.freeze({
1558
+ status,
1559
+ receivedRowCount,
1560
+ issues: Object.freeze([...problems])
1561
+ });
1562
+ }
1563
+ };
1564
+ }
1565
+
1566
+ //#endregion
1567
+ //#region src/tree.ts
1568
+ function buildTree(data, columns, readKey, options, lazy) {
1569
+ const nodes = [];
1570
+ const byKey = /* @__PURE__ */ new Map();
1571
+ const issues = [];
1572
+ const nested = options.getChildren !== void 0;
1573
+ let maxDepth = options.maxDepth ?? 1e3;
1574
+ if (!Number.isSafeInteger(maxDepth) || maxDepth < 1) {
1575
+ issues.push(issue("invalid_tree_option", "maxDepth must be a positive safe integer; 1,000 is used."));
1576
+ maxDepth = 1e3;
1577
+ }
1578
+ if (nested && options.getParentKey) issues.push(issue("invalid_tree_option", "Both tree inputs were supplied; getChildren takes precedence."));
1579
+ const seen = /* @__PURE__ */ new Set();
1580
+ const stack = data.map((original, sourceIndex) => ({
1581
+ original,
1582
+ sourceIndex,
1583
+ parent: void 0,
1584
+ lazy: false
1585
+ })).reverse();
1586
+ while (stack.length) {
1587
+ const entry = stack.pop();
1588
+ const index = nodes.length;
1589
+ let key = readKey(entry.original, index);
1590
+ let parent = entry.parent;
1591
+ const reference = entry.original !== null && (typeof entry.original === "object" || typeof entry.original === "function");
1592
+ const repeated = nested && reference && seen.has(entry.original);
1593
+ if (repeated) {
1594
+ issues.push(issue("tree_cycle", "A repeated nested value recovers as a root without traversing it again.", { rowKey: key }));
1595
+ parent = void 0;
1596
+ }
1597
+ if (reference) seen.add(entry.original);
1598
+ if (byKey.has(key)) {
1599
+ issues.push(issue(entry.lazy ? "tree_duplicate_key" : "duplicate_row_key", entry.lazy ? "A loaded child repeats an existing key and is rejected." : "A repeated row key is replaced with a unique key.", { rowKey: key }));
1600
+ if (entry.lazy) continue;
1601
+ const prefix = `${String(key)}#${index}`;
1602
+ key = prefix;
1603
+ let suffix = 0;
1604
+ while (byKey.has(key)) key = `${prefix}#${++suffix}`;
1605
+ }
1606
+ if (parent && parent.depth + 1 > maxDepth) {
1607
+ issues.push(issue("tree_depth_exceeded", "The maximum tree depth was exceeded; this row recovers as a root.", { rowKey: key }));
1608
+ parent = void 0;
1609
+ }
1610
+ const children = repeated ? void 0 : lazy.get(key) ?? options.getChildren?.(entry.original);
1611
+ const node = {
1612
+ row: createRow(entry.original, key, index, columns),
1613
+ parent,
1614
+ children: [],
1615
+ depth: parent ? parent.depth + 1 : 0,
1616
+ sourceIndex: entry.sourceIndex,
1617
+ lazy: entry.lazy,
1618
+ writable: parent?.writable ?? entry.parent === void 0,
1619
+ unloaded: children === void 0 && (options.hasChildren?.(entry.original) ?? false)
1620
+ };
1621
+ nodes.push(node);
1622
+ byKey.set(key, node);
1623
+ if (parent) parent.children.push(node);
1624
+ if (children) for (let i = children.length - 1; i >= 0; i--) stack.push({
1625
+ original: children[i],
1626
+ sourceIndex: i,
1627
+ parent: node,
1628
+ lazy: entry.lazy || lazy.has(key)
1629
+ });
1630
+ }
1631
+ if (!nested && options.getParentKey) {
1632
+ for (const node of nodes) {
1633
+ if (node.lazy) continue;
1634
+ const key = options.getParentKey(node.row.original);
1635
+ if (key === void 0 || key === null) continue;
1636
+ const parent = byKey.get(key);
1637
+ if (!parent) issues.push(issue("tree_orphan", "The parent is missing; this row recovers as a root.", { rowKey: node.row.key }));
1638
+ else node.parent = parent;
1639
+ }
1640
+ const done = /* @__PURE__ */ new Set();
1641
+ for (const node of nodes) {
1642
+ const path = [];
1643
+ const visiting = /* @__PURE__ */ new Set();
1644
+ let cursor = node;
1645
+ while (cursor && !done.has(cursor)) {
1646
+ if (visiting.has(cursor)) {
1647
+ issues.push(issue("tree_cycle", "A cyclic parent reference is detached to a root.", { rowKey: cursor.row.key }));
1648
+ cursor.parent = void 0;
1649
+ break;
1650
+ }
1651
+ visiting.add(cursor);
1652
+ path.push(cursor);
1653
+ cursor = cursor.parent;
1654
+ }
1655
+ for (let i = path.length - 1; i >= 0; i--) {
1656
+ const current = path[i];
1657
+ done.add(current);
1658
+ }
1659
+ }
1660
+ for (const node of nodes) node.children = [];
1661
+ for (const node of nodes) if (node.parent) node.parent.children.push(node);
1662
+ const pending = nodes.filter((node) => !node.parent);
1663
+ while (pending.length) {
1664
+ const node = pending.pop();
1665
+ node.depth = node.parent ? node.parent.depth + 1 : 0;
1666
+ if (node.depth > maxDepth) {
1667
+ node.parent = void 0;
1668
+ node.depth = 0;
1669
+ issues.push(issue("tree_depth_exceeded", "The maximum tree depth was exceeded; this row recovers as a root.", { rowKey: node.row.key }));
1670
+ }
1671
+ for (const child of node.children) pending.push(child);
1672
+ }
1673
+ for (const node of nodes) node.children = node.children.filter((child) => child.parent === node);
1674
+ }
1675
+ return {
1676
+ nodes,
1677
+ rows: nodes.map((node) => node.row),
1678
+ roots: nodes.filter((node) => !node.parent),
1679
+ byKey,
1680
+ issues
1681
+ };
1682
+ }
1683
+ function processTree(model, columns, state, mode, manual) {
1684
+ const raw = model.nodes.map((node) => node.row);
1685
+ const matched = manual ? raw : filterRows(raw, columns, state.search, state.filters);
1686
+ const active = matched !== raw;
1687
+ const included = new Set(matched.map((row) => row.key));
1688
+ const autoExpanded = /* @__PURE__ */ new Set();
1689
+ if (active && mode !== "strict") {
1690
+ if (mode === "descendants") {
1691
+ const stack = matched.map((row) => model.byKey.get(row.key));
1692
+ const visited = /* @__PURE__ */ new Set();
1693
+ while (stack.length) {
1694
+ const node = stack.pop();
1695
+ if (visited.has(node.row.key)) continue;
1696
+ visited.add(node.row.key);
1697
+ included.add(node.row.key);
1698
+ if (node.children.length) autoExpanded.add(node.row.key);
1699
+ for (const child of node.children) stack.push(child);
1700
+ }
1701
+ }
1702
+ for (const row of matched) {
1703
+ let parent = model.byKey.get(row.key).parent;
1704
+ while (parent && !autoExpanded.has(parent.row.key)) {
1705
+ included.add(parent.row.key);
1706
+ autoExpanded.add(parent.row.key);
1707
+ parent = parent.parent;
1708
+ }
1709
+ }
1710
+ }
1711
+ if (active && mode === "strict") return {
1712
+ model,
1713
+ roots: ordered(model.nodes.filter((node) => included.has(node.row.key) && node.children.length === 0 && !node.unloaded)),
1714
+ children: /* @__PURE__ */ new Map(),
1715
+ autoExpanded
1716
+ };
1717
+ const children = /* @__PURE__ */ new Map();
1718
+ for (const node of model.nodes) if (node.children.length) children.set(node, ordered(node.children.filter((child) => included.has(child.row.key))));
1719
+ return {
1720
+ model,
1721
+ roots: ordered(model.roots.filter((node) => included.has(node.row.key))),
1722
+ children,
1723
+ autoExpanded
1724
+ };
1725
+ function ordered(nodes) {
1726
+ if (manual || !state.sorting.length || nodes.length < 2) return nodes;
1727
+ return sortRows(nodes.map((node) => node.row), columns, state.sorting).map((row) => model.byKey.get(row.key));
1728
+ }
1729
+ }
1730
+ /** Expansion changes reuse filtering and sibling order, splicing only a changed subtree. */
1731
+ function createTreeFlattener() {
1732
+ let previous;
1733
+ let previousExpanded = [];
1734
+ let visible = [];
1735
+ let previousAncestors;
1736
+ return (tree, expanded, ancestors) => {
1737
+ if (tree === previous && expanded === previousExpanded && ancestors === previousAncestors) return visible;
1738
+ const keys = expanded === true ? void 0 : new Set(expanded);
1739
+ const open = (node) => expanded === true || (keys?.has(node.row.key) ?? false) || ancestors.has(node.row.key) || tree.autoExpanded.has(node.row.key);
1740
+ const walk = (roots) => {
1741
+ const output = [];
1742
+ const stack = [...roots].reverse();
1743
+ while (stack.length) {
1744
+ const node = stack.pop();
1745
+ output.push(node);
1746
+ if (open(node)) {
1747
+ const children = tree.children.get(node) ?? [];
1748
+ for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]);
1749
+ }
1750
+ }
1751
+ return output;
1752
+ };
1753
+ let changed;
1754
+ if (previous === tree && ancestors === previousAncestors && expanded !== true && previousExpanded !== true) {
1755
+ const before = new Set(previousExpanded);
1756
+ const after = new Set(expanded);
1757
+ changed = [...previousExpanded.filter((key) => !after.has(key)), ...expanded.filter((key) => !before.has(key))];
1758
+ }
1759
+ if (changed?.length === 1 && !tree.autoExpanded.has(changed[0])) {
1760
+ const index = visible.findIndex((node) => node.row.key === changed[0]);
1761
+ if (index >= 0) {
1762
+ const node = visible[index];
1763
+ let end = index + 1;
1764
+ while (end < visible.length && visible[end].depth > node.depth) end++;
1765
+ const inserted = walk(open(node) ? tree.children.get(node) ?? [] : []);
1766
+ visible = visible.slice(0, index + 1).concat(inserted, visible.slice(end));
1767
+ }
1768
+ } else visible = walk(tree.roots);
1769
+ previous = tree;
1770
+ previousExpanded = expanded;
1771
+ previousAncestors = ancestors;
1772
+ return visible;
1773
+ };
1774
+ }
1775
+
1776
+ //#endregion
1777
+ //#region src/table.ts
1778
+ function isRecord(value) {
1779
+ return typeof value === "object" && value !== null;
1780
+ }
1781
+ function memo(compute) {
1782
+ let last;
1783
+ return (...deps) => {
1784
+ if (last && last.deps.length === deps.length && last.deps.every((d, i) => Object.is(d, deps[i]))) return last.result;
1785
+ const result = compute(...deps);
1786
+ last = {
1787
+ deps,
1788
+ result
1789
+ };
1790
+ return result;
1791
+ };
1792
+ }
1793
+ function isInputEdit(edit) {
1794
+ return "input" in edit;
1795
+ }
1796
+ function coverage(selected, total) {
1797
+ if (selected === 0 || total === 0) return "none";
1798
+ return selected === total ? "all" : "some";
1799
+ }
1800
+ function pasteLimit(value, fallback, name, issues) {
1801
+ if (value === void 0) return fallback;
1802
+ if (Number.isSafeInteger(value) && value > 0) return value;
1803
+ issues.push(issue("invalid_paste_limit", `Paste ${name} ${String(value)} is not a positive safe integer; ${fallback} is used.`));
1804
+ return fallback;
1805
+ }
1806
+ /** Create a data table. */
1807
+ function createTable(options) {
1808
+ const paginate = options.paginate ?? true;
1809
+ const expandMode = options.expandMode ?? "multiple";
1810
+ const selectionMode = options.selectionMode ?? "multiple";
1811
+ const selectScope = options.selectScope ?? "all";
1812
+ const manual = options.manual ?? false;
1813
+ let treeFilter = options.treeFilter ?? "ancestors";
1814
+ const treeEnabled = Boolean(options.getChildren || options.getParentKey || options.hasChildren || options.loadChildren);
1815
+ const rootPagination = treeEnabled && (manual || (options.paginateBy ?? "root") === "root");
1816
+ const projectedRows = /* @__PURE__ */ new WeakMap();
1817
+ function shareRow(base, next) {
1818
+ const previous = projectedRows.get(base);
1819
+ if (previous && Object.keys(next).every((key) => previous[key] === next[key])) return previous;
1820
+ projectedRows.set(base, next);
1821
+ return next;
1822
+ }
1823
+ let lazyVersion = 0;
1824
+ const lazyChildren = /* @__PURE__ */ new Map();
1825
+ const childStatuses = /* @__PURE__ */ new Map();
1826
+ const loadIssues = /* @__PURE__ */ new Map();
1827
+ const loadValidationIssues = /* @__PURE__ */ new Map();
1828
+ const pendingLoads = /* @__PURE__ */ new Map();
1829
+ let destroyed = false;
1830
+ const undoStack = createUndoStack(options.historyLimit);
1831
+ const pendingChanges = createPendingChanges();
1832
+ const listeners = /* @__PURE__ */ new Set();
1833
+ const optionIssues = [];
1834
+ const maxPasteCells = pasteLimit(options.pasteLimit?.maxCells, 1e5, "maxCells", optionIssues);
1835
+ const maxPasteLength = pasteLimit(options.pasteLimit?.maxLength, 5e6, "maxLength", optionIssues);
1836
+ const sourceExpected = options.expectedRowCount === void 0 || Number.isSafeInteger(options.expectedRowCount) && options.expectedRowCount >= 0 ? options.expectedRowCount : void 0;
1837
+ if (sourceExpected !== options.expectedRowCount) optionIssues.push(issue("invalid_stream_option", "expectedRowCount must be a non-negative safe integer; it is unknown instead."));
1838
+ let data = options.data;
1839
+ let definitions = options.columns;
1840
+ let rowCount = options.rowCount;
1841
+ let expandedIssue;
1842
+ let initialState = createInitialState({
1843
+ ...options.initialState,
1844
+ hiddenColumns: options.initialState?.hiddenColumns ?? options.columns.filter((column) => column.hidden === true).map((column) => column.id)
1845
+ }, (problem) => {
1846
+ expandedIssue = problem;
1847
+ });
1848
+ let state = initialState;
1849
+ let snapshot;
1850
+ let loadState = "idle";
1851
+ let expectedRowCount = sourceExpected;
1852
+ let ingestionIssues = [];
1853
+ let streamIssues = [];
1854
+ const streaming = createStream({
1855
+ ingest: (rows, mode) => ingestData(rows, mode),
1856
+ update(next, expected, problems, publish) {
1857
+ loadState = next;
1858
+ expectedRowCount = expected;
1859
+ streamIssues = [...problems];
1860
+ if (publish) notify();
1861
+ }
1862
+ });
1863
+ let transaction = false;
1864
+ let deferValidationNotify = false;
1865
+ let validationNotifyRequested = false;
1866
+ const validation = createEditValidation({
1867
+ snapshot: getSnapshot,
1868
+ apply: applyChanges,
1869
+ notify,
1870
+ writeIssue(row, next) {
1871
+ const node = treeEnabled ? treeStage(data, resolveColumns(definitions).byId, lazyVersion).byKey.get(row.key) : void 0;
1872
+ if (node && (!node.writable || node.parent && options.getChildren && !options.setChildren)) return issue("read_only_cell", "This nested row has no immutable writable source path.", { rowKey: row.key });
1873
+ if (readKey(next, row.index) !== readKey(row.original, row.index)) return issue("invalid_row_operation", "Editing a row key is not supported; remove and insert the row instead.", { rowKey: row.key });
1874
+ },
1875
+ batch(run) {
1876
+ deferValidationNotify = true;
1877
+ validationNotifyRequested = false;
1878
+ try {
1879
+ return run();
1880
+ } finally {
1881
+ deferValidationNotify = false;
1882
+ if (validationNotifyRequested) notify();
1883
+ }
1884
+ },
1885
+ record: (changes) => undoStack.record({
1886
+ kind: "cells",
1887
+ changes
1888
+ }),
1889
+ onIssues: (issues) => options.onEditIssues?.(issues),
1890
+ optimistic: options.asyncValidation === "optimistic",
1891
+ validateRow: options.validateRow
1892
+ });
1893
+ const readKey = (row, index) => {
1894
+ const { rowKey } = options;
1895
+ if (typeof rowKey === "function") return rowKey(row, index);
1896
+ const value = getPath(row, rowKey ?? "id");
1897
+ if (typeof value === "string" || typeof value === "number") return value;
1898
+ return rowKey === void 0 || !isRecord(row) ? index : String(value);
1899
+ };
1900
+ const resolveColumns = memo((defs) => {
1901
+ const issues = [];
1902
+ const columns = defs.flatMap((definition) => {
1903
+ if (!isSafePath(definition.id)) {
1904
+ issues.push(issue("unsafe_path", `The column "${definition.id}" addresses a prototype and is ignored.`, { column: definition.id }));
1905
+ return [];
1906
+ }
1907
+ return [resolveColumn(definition)];
1908
+ });
1909
+ return {
1910
+ columns,
1911
+ byId: new Map(columns.map((column) => [column.id, column])),
1912
+ issues
1913
+ };
1914
+ });
1915
+ let primedIndex;
1916
+ let reusableRows;
1917
+ const indexRows = memo((rows, byId) => {
1918
+ if (primedIndex?.data === rows && primedIndex.columns === byId) return primedIndex.result;
1919
+ const byKey = /* @__PURE__ */ new Map();
1920
+ const issues = [];
1921
+ const indexed = rows.map((original, index) => {
1922
+ let key = readKey(original, index);
1923
+ if (byKey.has(key)) {
1924
+ issues.push(issue("duplicate_row_key", `Row ${index} repeats the key "${String(key)}"; it is keyed "${String(key)}#${index}" instead.`, { rowKey: key }));
1925
+ key = `${String(key)}#${index}`;
1926
+ }
1927
+ const previous = reusableRows?.columns === byId ? reusableRows.rows.get(key) : void 0;
1928
+ const row = previous?.original === original && previous.index === index ? previous : createRow(original, key, index, byId, options.getRowCanExpand?.(original) ?? false);
1929
+ byKey.set(key, row);
1930
+ return row;
1931
+ });
1932
+ stableSource.set(rows, stableRows(rows));
1933
+ reusableRows = {
1934
+ columns: byId,
1935
+ rows: byKey
1936
+ };
1937
+ return {
1938
+ rows: indexed,
744
1939
  byKey,
745
1940
  issues
746
1941
  };
747
1942
  });
1943
+ const treeStage = memo((rows, byId, _version) => buildTree(rows, byId, readKey, options, lazyChildren));
1944
+ const treeIndex = memo((model) => ({
1945
+ rows: model.rows,
1946
+ byKey: new Map(model.rows.map((row) => [row.key, row])),
1947
+ issues: model.issues
1948
+ }));
1949
+ const treePositions = memo((model) => {
1950
+ const result = /* @__PURE__ */ new Map();
1951
+ const index = (siblings, parent) => {
1952
+ siblings.forEach((node, position) => result.set(node.row.key, Object.freeze({
1953
+ parent,
1954
+ before: siblings[position + 1]?.row.key
1955
+ })));
1956
+ };
1957
+ index(model.roots);
1958
+ for (const node of model.nodes) index(node.children, node.row.key);
1959
+ return result;
1960
+ });
1961
+ const processedTreeStage = memo((model, columns, search, filters, sorting, mode) => processTree(model, columns, {
1962
+ ...state,
1963
+ search,
1964
+ filters,
1965
+ sorting
1966
+ }, mode, manual));
1967
+ const flattenTree = createTreeFlattener();
1968
+ const emptyAncestors = /* @__PURE__ */ new Set();
1969
+ const singleTreePath = memo((model, expanded) => {
1970
+ const ancestors = /* @__PURE__ */ new Set();
1971
+ if (expanded !== true) for (const key of expanded) {
1972
+ let parent = model.byKey.get(key)?.parent;
1973
+ while (parent && !ancestors.has(parent.row.key)) {
1974
+ ancestors.add(parent.row.key);
1975
+ parent = parent.parent;
1976
+ }
1977
+ }
1978
+ return ancestors;
1979
+ });
1980
+ const treeSelection = memo((model, selection) => {
1981
+ const selected = new Set(selection);
1982
+ const counts = /* @__PURE__ */ new Map();
1983
+ const stack = model.roots.map((node) => ({
1984
+ node,
1985
+ exit: false
1986
+ }));
1987
+ while (stack.length) {
1988
+ const { node, exit } = stack.pop();
1989
+ if (!exit) {
1990
+ stack.push({
1991
+ node,
1992
+ exit: true
1993
+ });
1994
+ for (const child of node.children) stack.push({
1995
+ node: child,
1996
+ exit: false
1997
+ });
1998
+ } else {
1999
+ let count = selected.has(node.row.key) ? 1 : 0;
2000
+ let total = 1;
2001
+ for (const child of node.children) {
2002
+ const value = counts.get(child);
2003
+ count += value.selected;
2004
+ total += value.total;
2005
+ }
2006
+ counts.set(node, {
2007
+ selected: count,
2008
+ total
2009
+ });
2010
+ }
2011
+ }
2012
+ return counts;
2013
+ });
748
2014
  const orderColumns = memo((columns, columnOrder, hiddenColumns) => {
749
2015
  const position = new Map(columnOrder.map((id, index) => [id, index]));
750
2016
  const ordered = columns.map((column, index) => ({
@@ -759,11 +2025,43 @@ function createTable(options) {
759
2025
  visible: ordered.filter((column) => !hidden.has(column.id))
760
2026
  };
761
2027
  });
762
- const filterStage = memo(filterRows);
763
- const sortStage = memo(sortRows);
2028
+ let primedFilter;
2029
+ let primedSort;
2030
+ const filterStage = memo((rows, columns, search, filters) => primedFilter?.rows === rows && primedFilter.columns === columns && primedFilter.search === search && primedFilter.filters === filters ? primedFilter.result : filterRows(rows, columns, search, filters));
2031
+ const sortStage = memo((rows, columns, sorting) => primedSort?.rows === rows && primedSort.columns === columns && primedSort.sorting === sorting ? primedSort.result : sortRows(rows, columns, sorting));
2032
+ const rowMembership = /* @__PURE__ */ new WeakMap();
2033
+ function membership(rows) {
2034
+ let keys = rowMembership.get(rows);
2035
+ if (!keys) {
2036
+ keys = new Set(rows.map((row) => row.key));
2037
+ rowMembership.set(rows, keys);
2038
+ }
2039
+ return keys;
2040
+ }
764
2041
  const selectedSet = memo((selection) => new Set(selection));
2042
+ const expansionStage = memo((indexed, expanded, selection) => {
2043
+ const keys = expanded === true ? void 0 : new Set(expanded);
2044
+ const selected = new Set(selection);
2045
+ const rows = mapList(indexed.rows, (row) => {
2046
+ const isExpanded = row.canExpand && (expanded === true || (keys?.has(row.key) ?? false));
2047
+ return isExpanded || selected.has(row.key) ? shareRow(row, Object.freeze({
2048
+ ...row,
2049
+ isExpanded,
2050
+ selection: selected.has(row.key) ? "all" : "none"
2051
+ })) : row;
2052
+ });
2053
+ return {
2054
+ rows,
2055
+ byKey: { get(key) {
2056
+ const row = indexed.byKey.get(key);
2057
+ return row && row.index < rows.length ? rows[row.index] : void 0;
2058
+ } }
2059
+ };
2060
+ });
2061
+ const expandedOrder = memo((rows, byKey) => mapList(rows, (row) => byKey.get(row.key)));
765
2062
  function stateIssues(byId) {
766
2063
  const issues = [];
2064
+ if (expandedIssue) issues.push(expandedIssue);
767
2065
  const referenced = /* @__PURE__ */ new Set([
768
2066
  ...state.sorting.map((rule) => rule.column),
769
2067
  ...Object.keys(state.filters),
@@ -772,30 +2070,105 @@ function createTable(options) {
772
2070
  ]);
773
2071
  for (const id of referenced) if (!byId.has(id)) issues.push(issue("unknown_column", `The state refers to a column "${id}" that is not defined.`, { column: id }));
774
2072
  const { pageSize } = state.pagination;
775
- if (!Number.isInteger(pageSize) || pageSize < 1) issues.push(issue("invalid_page_size", `Page size ${String(pageSize)} is not a positive whole number; ${10} is used.`));
2073
+ if (paginate && (!Number.isInteger(pageSize) || pageSize < 1)) issues.push(issue("invalid_page_size", `Page size ${String(pageSize)} is not a positive whole number; ${10} is used.`));
776
2074
  return issues;
777
2075
  }
778
2076
  function computeSnapshot() {
779
- const { columns, byId } = resolveColumns(definitions);
780
- const indexed = indexRows(data, byId);
2077
+ const cellProblems = validation.problems();
2078
+ const dirty = new Set(pendingChanges.entries.keys());
2079
+ const metadata = (row) => dirty.has(row.key) || cellProblems.has(row.key) ? shareRow(row, Object.freeze({
2080
+ ...row,
2081
+ isDirty: dirty.has(row.key),
2082
+ cellIssues: cellProblems.get(row.key)
2083
+ })) : row;
2084
+ const { columns, byId, issues: columnIssues } = resolveColumns(definitions);
2085
+ const model = treeEnabled ? treeStage(data, byId, lazyVersion) : void 0;
2086
+ const indexed = model ? {
2087
+ rows: model.rows,
2088
+ issues: model.issues
2089
+ } : indexRows(data, byId);
2090
+ const expanded = model ? void 0 : expansionStage(indexed, state.expanded, state.selection);
781
2091
  const ordered = orderColumns(columns, state.columnOrder, state.hiddenColumns);
782
- const issues = [...indexed.issues, ...stateIssues(byId)];
2092
+ const issues = [
2093
+ ...optionIssues,
2094
+ ...ingestionIssues,
2095
+ ...streamIssues,
2096
+ ...columnIssues,
2097
+ ...indexed.issues,
2098
+ ...stateIssues(byId),
2099
+ ...loadIssues.values(),
2100
+ ...Array.from(loadValidationIssues.values()).flat(),
2101
+ ...Array.from(cellProblems.values()).flatMap((row) => [...row.values()])
2102
+ ];
783
2103
  const requestedSize = state.pagination.pageSize;
784
- const pageSize = Number.isInteger(requestedSize) && requestedSize >= 1 ? requestedSize : 10;
2104
+ let pageSize = Number.isInteger(requestedSize) && requestedSize >= 1 ? requestedSize : 10;
785
2105
  let processedRows;
786
2106
  let rows;
787
2107
  let filteredCount;
788
2108
  let page;
789
2109
  let pageCount;
790
- if (manual) {
791
- processedRows = indexed.rows;
792
- rows = indexed.rows;
2110
+ let treeRow;
2111
+ let totalCount = indexed.rows.length;
2112
+ if (model) {
2113
+ const processed = processedTreeStage(model, columns, state.search, state.filters, state.sorting, treeFilter);
2114
+ const ancestors = expandMode === "single" ? singleTreePath(model, state.expanded) : emptyAncestors;
2115
+ const visible = flattenTree(processed, state.expanded, ancestors);
2116
+ const expandedState = state.expanded;
2117
+ const keys = state.expanded === true ? void 0 : new Set(state.expanded);
2118
+ const selected = treeSelection(model, state.selection);
2119
+ const projected = /* @__PURE__ */ new Map();
2120
+ const statuses = new Map(childStatuses);
2121
+ treeRow = (key) => {
2122
+ const cached = projected.get(key);
2123
+ if (cached) return cached;
2124
+ const node = model.byKey.get(key);
2125
+ if (!node) return void 0;
2126
+ const status = statuses.get(key) ?? (node.children.length || !node.unloaded ? "loaded" : "idle");
2127
+ const count = selected.get(node);
2128
+ const canExpand = node.children.length > 0 || status !== "loaded" || (options.getRowCanExpand?.(node.row.original) ?? false);
2129
+ const result = shareRow(node.row, Object.freeze({
2130
+ ...node.row,
2131
+ isDirty: dirty.has(key),
2132
+ cellIssues: cellProblems.get(key),
2133
+ depth: node.depth,
2134
+ parentKey: node.parent?.row.key,
2135
+ childCount: status === "loaded" ? node.children.length : void 0,
2136
+ childStatus: status,
2137
+ canExpand,
2138
+ isExpanded: canExpand && (expandedState === true || (keys?.has(key) ?? false) || ancestors.has(key) || processed.autoExpanded.has(key)),
2139
+ selection: coverage(count.selected, count.total)
2140
+ }));
2141
+ projected.set(key, result);
2142
+ return result;
2143
+ };
2144
+ processedRows = visible.map((node) => treeRow(node.row.key));
2145
+ filteredCount = manual ? rowCount ?? model.roots.length : rootPagination ? processed.roots.length : visible.length;
2146
+ totalCount = rootPagination ? model.roots.length : model.nodes.length;
2147
+ pageCount = countPages(filteredCount, pageSize);
2148
+ page = clampPage(state.pagination.page, pageCount);
2149
+ if (manual) rows = processedRows;
2150
+ else if (rootPagination) {
2151
+ const roots = new Set(processed.roots.slice((page - 1) * pageSize, page * pageSize));
2152
+ const allRoots = new Set(processed.roots);
2153
+ let root;
2154
+ rows = processedRows.filter((_row, index) => {
2155
+ const node = visible[index];
2156
+ if (allRoots.has(node)) root = node;
2157
+ return root !== void 0 && roots.has(root);
2158
+ });
2159
+ } else rows = paginateRows(processedRows, {
2160
+ page,
2161
+ pageSize
2162
+ });
2163
+ } else if (manual) {
2164
+ processedRows = expanded.rows;
2165
+ rows = expanded.rows;
793
2166
  filteredCount = rowCount ?? indexed.rows.length;
794
2167
  pageCount = countPages(filteredCount, pageSize);
795
2168
  page = clampPage(state.pagination.page, pageCount);
796
2169
  } else {
797
2170
  const filtered = filterStage(indexed.rows, columns, state.search, state.filters);
798
- processedRows = sortStage(filtered, columns, state.sorting);
2171
+ processedRows = expandedOrder(sortStage(filtered, columns, state.sorting), expanded.byKey);
799
2172
  filteredCount = processedRows.length;
800
2173
  pageCount = countPages(filteredCount, pageSize);
801
2174
  page = clampPage(state.pagination.page, pageCount);
@@ -804,42 +2177,90 @@ function createTable(options) {
804
2177
  pageSize
805
2178
  });
806
2179
  }
2180
+ if (!paginate) {
2181
+ rows = processedRows;
2182
+ page = 1;
2183
+ pageCount = 1;
2184
+ pageSize = Math.max(1, rows.length);
2185
+ }
2186
+ const flatProjected = /* @__PURE__ */ new Map();
2187
+ if (!model) {
2188
+ const project = (row) => {
2189
+ let result = flatProjected.get(row.key);
2190
+ if (!result) {
2191
+ result = metadata(row);
2192
+ flatProjected.set(row.key, result);
2193
+ }
2194
+ return result;
2195
+ };
2196
+ const shared = rows === processedRows;
2197
+ processedRows = mapList(processedRows, project);
2198
+ rows = shared ? processedRows : rows.map(project);
2199
+ }
807
2200
  const selected = selectedSet(state.selection);
808
- const pageSelected = rows.filter((row) => selected.has(row.key)).length;
2201
+ const allData = model ? model.byKey : indexed.byKey;
2202
+ const allProcessedKeys = model ? void 0 : manual ? void 0 : membership(filterStage(indexed.rows, columns, state.search, state.filters));
2203
+ const selectedInProcessed = state.selection.filter((key) => allData.has(key) && (!allProcessedKeys || allProcessedKeys.has(key))).length;
2204
+ const pageSelected = rows === processedRows ? selectedInProcessed : rows.filter((row) => selected.has(row.key)).length;
809
2205
  const scopeRows = selectScope === "page" || manual ? rows : processedRows;
810
- const scopeSelected = scopeRows === rows ? pageSelected : scopeRows.filter((row) => selected.has(row.key)).length;
2206
+ const scopeSelected = scopeRows === rows ? pageSelected : model ? scopeRows.filter((row) => selected.has(row.key)).length : selectedInProcessed;
811
2207
  const pageStart = rows.length === 0 ? 0 : (page - 1) * pageSize + 1;
812
2208
  const sortIndex = new Map(state.sorting.map((rule, priority) => [rule.column, {
813
2209
  direction: rule.direction,
814
2210
  priority
815
2211
  }]));
816
2212
  return Object.freeze({
2213
+ tree: treeEnabled,
2214
+ loadState,
2215
+ loadedRowCount: indexed.rows.length,
2216
+ expectedRowCount,
817
2217
  state,
818
2218
  columns: ordered.visible,
819
2219
  allColumns: ordered.all,
820
2220
  rows,
2221
+ renderItems: !model && state.expanded !== true && state.expanded.length === 0 ? mapList(rows, (row, rowIndex) => Object.freeze({
2222
+ kind: "row",
2223
+ key: getRowItemKey(row.key),
2224
+ row,
2225
+ rowIndex
2226
+ })) : Object.freeze(rows.flatMap((row, rowIndex) => {
2227
+ const item = Object.freeze({
2228
+ kind: "row",
2229
+ key: getRowItemKey(row.key),
2230
+ row,
2231
+ rowIndex
2232
+ });
2233
+ return row.isExpanded && (!model || (options.getRowCanExpand?.(row.original) ?? false)) ? [item, Object.freeze({
2234
+ kind: "detail",
2235
+ key: getRowItemKey(row.key, "detail"),
2236
+ row,
2237
+ rowIndex
2238
+ })] : [item];
2239
+ })),
2240
+ paginate,
821
2241
  processedRows,
822
2242
  page,
823
2243
  pageSize,
824
2244
  pageCount,
825
2245
  rowCount: filteredCount,
826
- totalRowCount: manual ? filteredCount : indexed.rows.length,
2246
+ totalRowCount: manual ? filteredCount : totalCount,
827
2247
  pageStart,
828
- pageEnd: rows.length === 0 ? 0 : pageStart + rows.length - 1,
2248
+ pageEnd: rows.length === 0 ? 0 : rootPagination ? Math.min(filteredCount, page * pageSize) : pageStart + rows.length - 1,
829
2249
  pageItems: paginationItems(page, pageCount),
830
2250
  canPreviousPage: page > 1,
831
2251
  canNextPage: page < pageCount,
832
2252
  selectionMode,
833
- selectedCount: state.selection.length,
2253
+ selectedCount: model ? state.selection.filter((key) => model.byKey.has(key)).length : state.selection.length,
834
2254
  pageSelection: coverage(pageSelected, rows.length),
835
2255
  allSelection: coverage(scopeSelected, scopeRows.length),
836
2256
  canUndo: undoStack.canUndo,
837
2257
  canRedo: undoStack.canRedo,
838
2258
  issues: Object.freeze(issues),
2259
+ pendingCells: validation.cells(),
839
2260
  isSelected: (key) => selected.has(key),
840
2261
  getSort: (columnId) => sortIndex.get(columnId),
841
2262
  getColumn: (columnId) => byId.get(columnId),
842
- getRow: (key) => indexed.byKey.get(key)
2263
+ getRow: (key) => treeRow ? treeRow(key) : flatProjected.get(key) ?? (expanded.byKey.get(key) ? metadata(expanded.byKey.get(key)) : void 0)
843
2264
  });
844
2265
  }
845
2266
  function getSnapshot() {
@@ -848,18 +2269,140 @@ function createTable(options) {
848
2269
  }
849
2270
  function notify() {
850
2271
  snapshot = void 0;
2272
+ if (deferValidationNotify) {
2273
+ validationNotifyRequested = true;
2274
+ return;
2275
+ }
2276
+ if (transaction) return;
851
2277
  if (listeners.size === 0) return;
852
2278
  const next = getSnapshot();
853
2279
  for (const listener of listeners) listener(next);
854
2280
  }
2281
+ function abortLoads(all = false) {
2282
+ for (const [key, controller] of pendingLoads) {
2283
+ const row = all ? void 0 : getSnapshot().getRow(key);
2284
+ const visible = !all && getSnapshot().processedRows.some((candidate) => candidate.key === key);
2285
+ if (all || !row?.isExpanded || !visible) {
2286
+ pendingLoads.delete(key);
2287
+ childStatuses.delete(key);
2288
+ controller.abort();
2289
+ snapshot = void 0;
2290
+ }
2291
+ }
2292
+ }
2293
+ function requestChildren(row) {
2294
+ if (!options.loadChildren || destroyed || pendingLoads.has(row.key) || lazyChildren.has(row.key) || row.childStatus === "loaded") return;
2295
+ const key = row.key;
2296
+ if (!options.createChildLoadController) {
2297
+ childStatuses.set(key, "error");
2298
+ loadIssues.set(key, issue("invalid_tree_option", "Lazy children require createChildLoadController from the caller's runtime.", { rowKey: key }));
2299
+ notify();
2300
+ return;
2301
+ }
2302
+ let controller;
2303
+ try {
2304
+ controller = options.createChildLoadController();
2305
+ } catch (error) {
2306
+ loadIssues.set(key, issue("tree_load_error", String(error), { rowKey: key }));
2307
+ childStatuses.set(key, "error");
2308
+ notify();
2309
+ return;
2310
+ }
2311
+ pendingLoads.set(key, controller);
2312
+ childStatuses.set(key, "loading");
2313
+ loadIssues.delete(key);
2314
+ loadValidationIssues.delete(key);
2315
+ notify();
2316
+ Promise.resolve().then(() => controller.signal.aborted ? [] : options.loadChildren(row, controller.signal)).then((children) => {
2317
+ if (controller.signal.aborted || pendingLoads.get(key) !== controller || destroyed) return void 0;
2318
+ const { byId } = resolveColumns(definitions);
2319
+ const existing = treeStage(data, byId, lazyVersion);
2320
+ const candidate = buildTree(children, byId, readKey, {
2321
+ ...options,
2322
+ getParentKey: void 0
2323
+ }, /* @__PURE__ */ new Map());
2324
+ pendingLoads.delete(key);
2325
+ loadValidationIssues.set(key, candidate.issues.filter((problem) => problem.code === "tree_cycle" || problem.code === "tree_depth_exceeded"));
2326
+ if (candidate.nodes.filter((node) => existing.byKey.has(node.row.key)).length || candidate.issues.some((problem) => problem.code === "duplicate_row_key")) {
2327
+ childStatuses.set(key, "error");
2328
+ loadIssues.set(key, issue("tree_duplicate_key", "Loaded children collide with existing keys; the batch is rejected.", { rowKey: key }));
2329
+ } else {
2330
+ lazyChildren.set(key, children);
2331
+ childStatuses.set(key, "loaded");
2332
+ lazyVersion++;
2333
+ if (state.selection.includes(key) && selectionMode === "multiple") {
2334
+ const nextModel = treeStage(data, byId, lazyVersion);
2335
+ applySelection([...state.selection, ...selectionKeys([key], nextModel)]);
2336
+ }
2337
+ }
2338
+ notify();
2339
+ startVisibleLoads();
2340
+ }).catch((error) => {
2341
+ if (controller.signal.aborted || pendingLoads.get(key) !== controller || destroyed) return;
2342
+ pendingLoads.delete(key);
2343
+ childStatuses.set(key, "error");
2344
+ loadIssues.set(key, issue("tree_load_error", `Children could not be loaded: ${String(error)}`, { rowKey: key }));
2345
+ notify();
2346
+ });
2347
+ }
2348
+ function startVisibleLoads() {
2349
+ if (!treeEnabled || !options.loadChildren || destroyed) return;
2350
+ const current = getSnapshot();
2351
+ for (const row of current.rows) if (row.isExpanded && row.childStatus === "idle") requestChildren(row);
2352
+ }
2353
+ function selectionKeys(keys, model) {
2354
+ if (!treeEnabled || selectionMode !== "multiple") return [...keys];
2355
+ model ??= treeStage(data, resolveColumns(definitions).byId, lazyVersion);
2356
+ const result = new Set(keys);
2357
+ const stack = keys.flatMap((key) => {
2358
+ const node = model.byKey.get(key);
2359
+ return node ? [node] : [];
2360
+ });
2361
+ const visited = /* @__PURE__ */ new Set();
2362
+ while (stack.length) {
2363
+ const node = stack.pop();
2364
+ if (visited.has(node.row.key)) continue;
2365
+ visited.add(node.row.key);
2366
+ result.add(node.row.key);
2367
+ for (const child of node.children) stack.push(child);
2368
+ }
2369
+ return [...result];
2370
+ }
855
2371
  /** Apply a change made by a table operation. */
856
2372
  function commit(patch) {
857
2373
  const previous = state;
858
- const next = mergeState(state, patch);
859
- if (next === previous) return;
2374
+ const previousIssue = expandedIssue;
2375
+ const next = mergePatch(patch);
2376
+ if (next === previous && previousIssue === expandedIssue) return;
860
2377
  state = next;
861
- options.onStateChange?.(state, previous);
2378
+ snapshot = void 0;
2379
+ abortLoads();
2380
+ if (next !== previous) options.onStateChange?.(state, previous);
862
2381
  notify();
2382
+ startVisibleLoads();
2383
+ }
2384
+ function canonicalExpanded(expanded, candidate = state) {
2385
+ if (expandMode === "multiple") return expanded;
2386
+ if (expanded !== true) return expanded.slice(-1);
2387
+ const { columns, byId } = resolveColumns(definitions);
2388
+ if (treeEnabled) {
2389
+ const model = treeStage(data, byId, lazyVersion);
2390
+ const first = processedTreeStage(model, columns, candidate.search, candidate.filters, candidate.sorting, treeFilter).roots.find((node) => node.children.length > 0 || (options.hasChildren?.(node.row.original) ?? false) || (options.getRowCanExpand?.(node.row.original) ?? false));
2391
+ return first ? [first.row.key] : [];
2392
+ }
2393
+ const indexed = indexRows(data, byId);
2394
+ const first = (manual ? indexed.rows : sortStage(filterStage(indexed.rows, columns, candidate.search, candidate.filters), columns, candidate.sorting)).find((row) => row.canExpand);
2395
+ return first ? [first.key] : [];
2396
+ }
2397
+ function mergePatch(patch) {
2398
+ if (patch.expanded !== void 0) expandedIssue = void 0;
2399
+ const next = mergeState(state, patch, (problem) => {
2400
+ expandedIssue = problem;
2401
+ });
2402
+ return expandMode === "single" && patch.expanded !== void 0 ? mergeState(state, {
2403
+ ...patch,
2404
+ expanded: canonicalExpanded(next.expanded, next)
2405
+ }) : next;
863
2406
  }
864
2407
  const firstPage = () => ({
865
2408
  page: 1,
@@ -890,10 +2433,14 @@ function createTable(options) {
890
2433
  if (selection.length === current.length && selection.every((key, i) => key === current[i])) return;
891
2434
  commit({ selection });
892
2435
  }
893
- function applyChanges(edits, record) {
2436
+ function applyChanges(edits, record, recoveredIssues = []) {
894
2437
  const current = getSnapshot();
895
2438
  const working = [...data];
896
- const issues = [];
2439
+ const model = treeEnabled ? treeStage(data, resolveColumns(definitions).byId, lazyVersion) : void 0;
2440
+ const replacements = /* @__PURE__ */ new Map();
2441
+ const cache = new Map(lazyChildren);
2442
+ const originalOf = (node) => replacements.get(node) ?? node.row.original;
2443
+ const issues = [...recoveredIssues];
897
2444
  const changes = [];
898
2445
  const samples = /* @__PURE__ */ new Map();
899
2446
  const sampleOf = (columnId) => {
@@ -916,6 +2463,13 @@ function createTable(options) {
916
2463
  }));
917
2464
  continue;
918
2465
  }
2466
+ if (!isSafePath(edit.column)) {
2467
+ issues.push(issue("unsafe_path", `The path "${edit.column}" addresses a prototype and cannot be written.`, {
2468
+ rowKey: edit.rowKey,
2469
+ column: edit.column
2470
+ }));
2471
+ continue;
2472
+ }
919
2473
  if (!column) {
920
2474
  issues.push(issue("unknown_column", `No column has the id "${edit.column}".`, {
921
2475
  rowKey: edit.rowKey,
@@ -923,7 +2477,22 @@ function createTable(options) {
923
2477
  }));
924
2478
  continue;
925
2479
  }
926
- const original = working[row.index];
2480
+ const node = model?.byKey.get(row.key);
2481
+ const original = node ? originalOf(node) : working[row.index];
2482
+ if (node && !node.writable) {
2483
+ issues.push(issue("read_only_cell", "A recovered nested root has no unambiguous source path for editing.", {
2484
+ rowKey: row.key,
2485
+ column: column.id
2486
+ }));
2487
+ continue;
2488
+ }
2489
+ if (node?.parent && options.getChildren && !options.setChildren) {
2490
+ issues.push(issue("read_only_cell", "Editing a nested child requires an immutable setChildren callback.", {
2491
+ rowKey: row.key,
2492
+ column: column.id
2493
+ }));
2494
+ continue;
2495
+ }
927
2496
  if (record && !column.isEditable(original)) {
928
2497
  issues.push(issue("read_only_cell", `The "${column.id}" cell of row "${String(row.key)}" is read-only.`, {
929
2498
  rowKey: row.key,
@@ -945,7 +2514,16 @@ function createTable(options) {
945
2514
  } else value = edit.value;
946
2515
  const previous = column.getValue(original);
947
2516
  if (Object.is(previous, value)) continue;
948
- const updated = column.setValue(original, value);
2517
+ let updated;
2518
+ try {
2519
+ updated = column.setValue(original, value);
2520
+ } catch (error) {
2521
+ issues.push(issue("invalid_value", error instanceof Error ? error.message : String(error), {
2522
+ rowKey: row.key,
2523
+ column: column.id
2524
+ }));
2525
+ continue;
2526
+ }
949
2527
  if (updated === void 0) {
950
2528
  issues.push(issue("read_only_cell", `The "${column.id}" column has no way to write a value.`, {
951
2529
  rowKey: row.key,
@@ -953,7 +2531,38 @@ function createTable(options) {
953
2531
  }));
954
2532
  continue;
955
2533
  }
956
- working[row.index] = updated;
2534
+ if (readKey(updated, row.index) !== readKey(original, row.index)) {
2535
+ issues.push(issue("invalid_row_operation", "Editing a row key is not supported; remove and insert the row instead.", {
2536
+ rowKey: row.key,
2537
+ column: column.id
2538
+ }));
2539
+ continue;
2540
+ }
2541
+ if (!node) working[row.index] = updated;
2542
+ else {
2543
+ const planned = /* @__PURE__ */ new Map([[node, updated]]);
2544
+ const plannedCache = /* @__PURE__ */ new Map();
2545
+ let cursor = node;
2546
+ try {
2547
+ while (cursor.parent) {
2548
+ const parent = cursor.parent;
2549
+ const children = parent.children.map((child) => planned.get(child) ?? originalOf(child));
2550
+ if (cursor.lazy) plannedCache.set(parent.row.key, children);
2551
+ if (options.setChildren) planned.set(parent, options.setChildren(originalOf(parent), children));
2552
+ cursor = parent;
2553
+ }
2554
+ } catch (error) {
2555
+ issues.push(issue("invalid_value", error instanceof Error ? error.message : String(error), {
2556
+ rowKey: row.key,
2557
+ column: column.id
2558
+ }));
2559
+ continue;
2560
+ }
2561
+ for (const [item, plannedRow] of planned) replacements.set(item, plannedRow);
2562
+ for (const [key, children] of plannedCache) cache.set(key, children);
2563
+ if (!options.getChildren && !node.lazy) working[node.sourceIndex] = updated;
2564
+ if (options.getChildren || options.setChildren) working[cursor.sourceIndex] = originalOf(cursor);
2565
+ }
957
2566
  changes.push({
958
2567
  rowKey: row.key,
959
2568
  column: column.id,
@@ -967,16 +2576,389 @@ function createTable(options) {
967
2576
  else {
968
2577
  status = issues.length > 0 ? "partial" : "applied";
969
2578
  data = working;
970
- if (record) undoStack.record(changes);
971
- options.onDataChange?.(data, changes);
2579
+ if (model) {
2580
+ lazyChildren.clear();
2581
+ for (const [key, children] of cache) lazyChildren.set(key, children);
2582
+ lazyVersion++;
2583
+ }
2584
+ if (record) {
2585
+ if (!transaction) undoStack.record({
2586
+ kind: "cells",
2587
+ changes
2588
+ });
2589
+ }
2590
+ const groups = /* @__PURE__ */ new Map();
2591
+ for (const change of changes) {
2592
+ const group = groups.get(change.rowKey) ?? [];
2593
+ group.push(change);
2594
+ groups.set(change.rowKey, group);
2595
+ }
2596
+ for (const [key, group] of groups) pendingChanges.record(key, current.getRow(key).original, group.at(-1).row, positionOf(key), group);
2597
+ if (!transaction) options.onDataChange?.(data, changes);
972
2598
  notify();
973
2599
  }
974
- if (issues.length > 0) options.onEditIssues?.(issues);
2600
+ if (issues.length > 0 && !transaction) options.onEditIssues?.(issues);
975
2601
  return Object.freeze({
976
2602
  status,
977
2603
  changes: Object.freeze(changes),
978
- issues: Object.freeze(issues)
2604
+ issues: Object.freeze(issues),
2605
+ rowChanges: Object.freeze([]),
2606
+ pendingCells: Object.freeze([])
2607
+ });
2608
+ }
2609
+ function sourceRows() {
2610
+ const { byId } = resolveColumns(definitions);
2611
+ const model = treeEnabled ? treeStage(data, byId, lazyVersion) : void 0;
2612
+ return model ? treeIndex(model) : indexRows(data, byId);
2613
+ }
2614
+ function positionOf(key) {
2615
+ const { byId } = resolveColumns(definitions);
2616
+ if (treeEnabled) {
2617
+ const model = treeStage(data, byId, lazyVersion);
2618
+ return treePositions(model).get(key) ?? Object.freeze({});
2619
+ }
2620
+ const indexed = indexRows(data, byId);
2621
+ const row = indexed.byKey.get(key);
2622
+ return Object.freeze({ before: row ? indexed.rows[row.index + 1]?.key : void 0 });
2623
+ }
2624
+ function scene() {
2625
+ return {
2626
+ data,
2627
+ lazy: new Map(lazyChildren)
2628
+ };
2629
+ }
2630
+ function resultForRows(changes, issues, cells = []) {
2631
+ return Object.freeze({
2632
+ status: changes.length || cells.length ? issues.length ? "partial" : "applied" : issues.length ? "rejected" : "unchanged",
2633
+ changes: Object.freeze([...cells]),
2634
+ rowChanges: Object.freeze([...changes]),
2635
+ pendingCells: Object.freeze([]),
2636
+ issues: Object.freeze([...issues])
2637
+ });
2638
+ }
2639
+ function rowProblem(message, rowKey) {
2640
+ return issue("invalid_row_operation", message, { rowKey });
2641
+ }
2642
+ function stableRows(rows) {
2643
+ return options.rowKey !== void 0 || rows.every((row) => {
2644
+ const key = getPath(row, "id");
2645
+ return typeof key === "string" || typeof key === "number";
2646
+ });
2647
+ }
2648
+ function cellDifferences(previous, row, key) {
2649
+ return resolveColumns(definitions).columns.flatMap((column) => {
2650
+ const before = column.getValue(previous);
2651
+ const value = column.getValue(row);
2652
+ return Object.is(before, value) ? [] : [{
2653
+ rowKey: key,
2654
+ column: column.id,
2655
+ previous: before,
2656
+ value,
2657
+ row
2658
+ }];
2659
+ });
2660
+ }
2661
+ function recordRows(previous, keys, positions, baselines = /* @__PURE__ */ new Map()) {
2662
+ const next = sourceRows();
2663
+ const rowChanges = [];
2664
+ for (const key of keys) {
2665
+ const before = previous.byKey.get(key)?.original;
2666
+ const row = next.byKey.get(key)?.original;
2667
+ const position = positions.get(key) ?? positionOf(key);
2668
+ const cells = before !== void 0 && row !== void 0 ? cellDifferences(before, row, key) : [];
2669
+ const baseline = baselines.get(key);
2670
+ const pending = pendingChanges.entries.get(key);
2671
+ if (row === void 0 && baseline !== void 0 && pending?.previous !== void 0) pending.previous = baseline;
2672
+ pendingChanges.record(key, row === void 0 ? baseline ?? before : before, row, position, cells);
2673
+ if (before === void 0 && row !== void 0) rowChanges.push(Object.freeze({
2674
+ kind: "inserted",
2675
+ rowKey: key,
2676
+ row,
2677
+ position: positionOf(key)
2678
+ }));
2679
+ else if (row === void 0 && before !== void 0) rowChanges.push(Object.freeze({
2680
+ kind: "removed",
2681
+ rowKey: key,
2682
+ row: before,
2683
+ position
2684
+ }));
2685
+ }
2686
+ return rowChanges;
2687
+ }
2688
+ function publishRows(before, previous, keys, positions, issues, baselines = /* @__PURE__ */ new Map()) {
2689
+ const rowChanges = recordRows(previous, keys, positions, baselines);
2690
+ if (!transaction) {
2691
+ undoStack.record({
2692
+ kind: "rows",
2693
+ before,
2694
+ after: scene(),
2695
+ keys
2696
+ });
2697
+ options.onDataChange?.(data, []);
2698
+ if (issues.length) options.onEditIssues?.(issues);
2699
+ }
2700
+ notify();
2701
+ return resultForRows(rowChanges, issues);
2702
+ }
2703
+ function restoreScene(entry, direction) {
2704
+ const previous = sourceRows();
2705
+ const baselines = removalBaselines();
2706
+ const positions = new Map(entry.keys.map((key) => [key, positionOf(key)]));
2707
+ const target = direction === "undo" ? entry.before : entry.after;
2708
+ data = target.data;
2709
+ const changed = /* @__PURE__ */ new Set([...entry.before.lazy.keys(), ...entry.after.lazy.keys()]);
2710
+ for (const key of changed) {
2711
+ if (entry.before.lazy.get(key) === entry.after.lazy.get(key)) continue;
2712
+ const rows = target.lazy.get(key);
2713
+ if (rows) lazyChildren.set(key, rows);
2714
+ else lazyChildren.delete(key);
2715
+ }
2716
+ lazyVersion++;
2717
+ recordRows(previous, entry.keys, positions, baselines);
2718
+ options.onDataChange?.(data, []);
2719
+ notify();
2720
+ }
2721
+ function removalBaselines() {
2722
+ const baselines = /* @__PURE__ */ new Map();
2723
+ if (!options.getChildren || !options.setChildren) return baselines;
2724
+ const model = treeStage(data, resolveColumns(definitions).byId, lazyVersion);
2725
+ for (const node of model.nodes.toReversed()) {
2726
+ const entry = pendingChanges.entries.get(node.row.key);
2727
+ if (entry && entry.previous === void 0) continue;
2728
+ let baseline = entry?.previous ?? node.row.original;
2729
+ const children = node.children.filter((child) => !pendingChanges.entries.has(child.row.key) || pendingChanges.entries.get(child.row.key).previous !== void 0).map((child) => baselines.get(child.row.key) ?? child.row.original);
2730
+ const originalChildren = options.getChildren(baseline) ?? [];
2731
+ if (children.length !== originalChildren.length || children.some((child, index) => child !== originalChildren[index])) baseline = options.setChildren(baseline, children);
2732
+ baselines.set(node.row.key, baseline);
2733
+ }
2734
+ return baselines;
2735
+ }
2736
+ function rewriteNested(model, removed, overrides, cache) {
2737
+ const replacements = /* @__PURE__ */ new Map();
2738
+ for (const node of model.nodes.toReversed()) {
2739
+ if (removed.has(node.row.key)) continue;
2740
+ const children = overrides.get(node.row.key) ?? node.children.filter((child) => !removed.has(child.row.key)).map((child) => replacements.get(child) ?? child.row.original);
2741
+ if (!(overrides.has(node.row.key) || children.length !== node.children.length || children.some((child, index) => child !== node.children[index]?.row.original))) continue;
2742
+ if (!options.setChildren) throw new Error("Changing nested children requires setChildren.");
2743
+ replacements.set(node, options.setChildren(node.row.original, children));
2744
+ if (cache.has(node.row.key)) cache.set(node.row.key, children);
2745
+ }
2746
+ const roots = new Map(model.roots.filter((node) => node.writable).map((node) => [node.sourceIndex, node]));
2747
+ return data.flatMap((row, index) => {
2748
+ const node = roots.get(index);
2749
+ return node && removed.has(node.row.key) ? [] : [node ? replacements.get(node) ?? row : row];
2750
+ });
2751
+ }
2752
+ function insertRows(rows, at = {}) {
2753
+ const before = scene();
2754
+ const previous = sourceRows();
2755
+ const problems = [];
2756
+ const reject = (problem) => {
2757
+ if (!transaction) options.onEditIssues?.([problem]);
2758
+ return resultForRows([], [problem]);
2759
+ };
2760
+ let incoming;
2761
+ try {
2762
+ incoming = rows ?? (options.createRow ? [options.createRow()] : []);
2763
+ } catch (error) {
2764
+ return reject(rowProblem(error instanceof Error ? error.message : String(error)));
2765
+ }
2766
+ if (rows === void 0 && !options.createRow) return reject(rowProblem("insertRows() requires rows or a createRow callback."));
2767
+ if (!incoming.length) return resultForRows([], []);
2768
+ if (!stableRows(data) || !stableRows(incoming)) return reject(rowProblem("Row operations require stable row keys; supply rowKey or an id on every row."));
2769
+ const { byId } = resolveColumns(definitions);
2770
+ const model = treeEnabled ? treeStage(data, byId, lazyVersion) : void 0;
2771
+ const parent = at.parent === void 0 ? void 0 : model?.byKey.get(at.parent);
2772
+ const sibling = at.before === void 0 ? void 0 : previous.byKey.get(at.before);
2773
+ if (at.parent !== void 0 && !parent) return reject(issue("unknown_row", "The insertion parent does not exist.", { rowKey: at.parent }));
2774
+ if (at.before !== void 0 && !sibling) return reject(issue("unknown_row", "The insertion sibling does not exist.", { rowKey: at.before }));
2775
+ if (sibling && (model?.byKey.get(sibling.key)?.parent?.row.key ?? sibling.parentKey) !== at.parent) return reject(rowProblem("The before row must belong to the requested parent.", sibling.key));
2776
+ if (parent && !parent.writable) return reject(rowProblem("A recovered parent has no writable source path.", parent.row.key));
2777
+ if (parent && getSnapshot().getRow(parent.row.key)?.childStatus !== "loaded") return reject(rowProblem("Load the parent's children before inserting rows.", parent.row.key));
2778
+ const cache = new Map(lazyChildren);
2779
+ let next;
2780
+ try {
2781
+ if (options.getChildren && parent && model) {
2782
+ const children = parent.children.map((child) => child.row.original);
2783
+ const index = at.before === void 0 ? children.length : parent.children.findIndex((child) => child.row.key === at.before);
2784
+ children.splice(index, 0, ...incoming);
2785
+ next = rewriteNested(model, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Map([[parent.row.key, children]]), cache);
2786
+ } else if (parent && cache.has(parent.row.key)) {
2787
+ incoming = incoming.map((row) => options.setParentKey ? options.setParentKey(row, parent.row.key) : row);
2788
+ const children = [...cache.get(parent.row.key)];
2789
+ const index = at.before === void 0 ? children.length : parent.children.findIndex((child) => child.row.key === at.before);
2790
+ children.splice(index, 0, ...incoming);
2791
+ cache.set(parent.row.key, children);
2792
+ next = [...data];
2793
+ } else {
2794
+ if (parent && options.getParentKey) {
2795
+ incoming = incoming.map((row) => options.setParentKey ? options.setParentKey(row, parent.row.key) : row);
2796
+ if (incoming.some((row) => options.getParentKey(row) !== parent.row.key)) return reject(rowProblem("Supply setParentKey or rows already addressed to the requested parent."));
2797
+ }
2798
+ const working = [...data];
2799
+ working.splice(sibling?.index ?? working.length, 0, ...incoming);
2800
+ next = working;
2801
+ }
2802
+ const candidate = treeEnabled ? buildTree(next, byId, readKey, options, cache) : indexRows(next, byId);
2803
+ if (!stableRows(candidate.rows.map((row) => row.original))) return reject(rowProblem("Every inserted descendant must have a stable key."));
2804
+ const baseline = /* @__PURE__ */ new Map();
2805
+ for (const problem of previous.issues) {
2806
+ const key = `${problem.code}:${String(problem.rowKey)}`;
2807
+ baseline.set(key, (baseline.get(key) ?? 0) + 1);
2808
+ }
2809
+ problems.push(...candidate.issues.filter((problem) => {
2810
+ const key = `${problem.code}:${String(problem.rowKey)}`;
2811
+ const remaining = baseline.get(key) ?? 0;
2812
+ if (!remaining) return true;
2813
+ baseline.set(key, remaining - 1);
2814
+ return false;
2815
+ }));
2816
+ if (problems.length) {
2817
+ if (!transaction) options.onEditIssues?.(problems);
2818
+ return resultForRows([], problems);
2819
+ }
2820
+ const keys = candidate.rows.filter((row) => !previous.byKey.has(row.key)).map((row) => row.key);
2821
+ if (!keys.length) return reject(rowProblem("No unique rows can be inserted."));
2822
+ data = next;
2823
+ lazyChildren.clear();
2824
+ for (const [key, value] of cache) lazyChildren.set(key, value);
2825
+ lazyVersion++;
2826
+ return publishRows(before, previous, keys, /* @__PURE__ */ new Map(), []);
2827
+ } catch (error) {
2828
+ return reject(rowProblem(error instanceof Error ? error.message : String(error)));
2829
+ }
2830
+ }
2831
+ function removeRows(keys) {
2832
+ const before = scene();
2833
+ const previous = sourceRows();
2834
+ const problems = [];
2835
+ const removed = /* @__PURE__ */ new Set();
2836
+ const { byId } = resolveColumns(definitions);
2837
+ const model = treeEnabled ? treeStage(data, byId, lazyVersion) : void 0;
2838
+ if (!stableRows(data)) {
2839
+ const problem = rowProblem("Row operations require stable row keys.");
2840
+ if (!transaction) options.onEditIssues?.([problem]);
2841
+ return resultForRows([], [problem]);
2842
+ }
2843
+ for (const key of keys) {
2844
+ if (!previous.byKey.has(key)) {
2845
+ problems.push(issue("unknown_row", "The row to remove does not exist.", { rowKey: key }));
2846
+ continue;
2847
+ }
2848
+ const node = model?.byKey.get(key);
2849
+ if (node && !node.writable) {
2850
+ problems.push(rowProblem("A recovered row has no writable source path.", key));
2851
+ continue;
2852
+ }
2853
+ const stack = node ? [node] : [];
2854
+ removed.add(key);
2855
+ while (stack.length) {
2856
+ const item = stack.pop();
2857
+ removed.add(item.row.key);
2858
+ for (const child of item.children) stack.push(child);
2859
+ }
2860
+ }
2861
+ if (!removed.size) {
2862
+ if (problems.length && !transaction) options.onEditIssues?.(problems);
2863
+ return resultForRows([], problems);
2864
+ }
2865
+ const positions = new Map([...removed].map((key) => [key, positionOf(key)]));
2866
+ const cache = new Map(lazyChildren);
2867
+ try {
2868
+ const baselines = removalBaselines();
2869
+ const next = options.getChildren && model ? rewriteNested(model, removed, /* @__PURE__ */ new Map(), cache) : data.filter((row, index) => !removed.has(readKey(row, index)));
2870
+ for (const [key, children] of cache) if (removed.has(key)) cache.delete(key);
2871
+ else cache.set(key, children.filter((row, index) => !removed.has(readKey(row, index))));
2872
+ data = next;
2873
+ lazyChildren.clear();
2874
+ for (const [key, value] of cache) lazyChildren.set(key, value);
2875
+ lazyVersion++;
2876
+ validation.cancel([...removed]);
2877
+ abortLoads();
2878
+ return publishRows(before, previous, [...removed], positions, problems, baselines);
2879
+ } catch (error) {
2880
+ problems.push(rowProblem(error instanceof Error ? error.message : String(error)));
2881
+ if (!transaction) options.onEditIssues?.(problems);
2882
+ return resultForRows([], problems);
2883
+ }
2884
+ }
2885
+ function revert(keys) {
2886
+ const selected = new Set(keys ?? pendingChanges.entries.keys());
2887
+ const entries = [...pendingChanges.entries].filter(([key]) => selected.has(key)).map(([key, entry]) => [key, {
2888
+ ...entry,
2889
+ cells: new Map(entry.cells)
2890
+ }]);
2891
+ if (!entries.length) return resultForRows([], []);
2892
+ const before = scene();
2893
+ const previous = sourceRows();
2894
+ const positions = new Map([...selected].map((key) => [key, positionOf(key)]));
2895
+ const problems = [];
2896
+ const cells = [];
2897
+ transaction = true;
2898
+ try {
2899
+ problems.push(...removeRows(entries.filter(([, entry]) => entry.previous === void 0).map(([key]) => key)).issues);
2900
+ const missing = entries.filter(([, entry]) => entry.previous !== void 0 && entry.row === void 0);
2901
+ for (let pass = 0; missing.length && pass <= entries.length; pass++) {
2902
+ let progress = false;
2903
+ for (let index = missing.length - 1; index >= 0; index--) {
2904
+ const [key, entry] = missing[index];
2905
+ if (sourceRows().byKey.has(key)) {
2906
+ missing.splice(index, 1);
2907
+ progress = true;
2908
+ continue;
2909
+ }
2910
+ if (entry.position.parent !== void 0 && !sourceRows().byKey.has(entry.position.parent)) continue;
2911
+ const at = {
2912
+ ...entry.position,
2913
+ before: entry.position.before !== void 0 && sourceRows().byKey.has(entry.position.before) ? entry.position.before : void 0
2914
+ };
2915
+ const result = insertRows([entry.previous], at);
2916
+ problems.push(...result.issues);
2917
+ missing.splice(index, 1);
2918
+ progress = true;
2919
+ }
2920
+ if (!progress) break;
2921
+ }
2922
+ for (const [key] of missing) problems.push(rowProblem("The saved parent is missing; restore it before this child.", key));
2923
+ const result = applyChanges(entries.flatMap(([key, entry]) => entry.previous !== void 0 && sourceRows().byKey.has(key) ? [...entry.cells.values()].map((change) => ({
2924
+ rowKey: key,
2925
+ column: change.column,
2926
+ value: change.previous
2927
+ })) : []), false);
2928
+ cells.push(...result.changes);
2929
+ problems.push(...result.issues);
2930
+ for (const [key] of entries) if (!problems.some((problem) => problem.rowKey === key)) pendingChanges.entries.delete(key);
2931
+ validation.cancel([...selected]);
2932
+ } finally {
2933
+ transaction = false;
2934
+ }
2935
+ const after = scene();
2936
+ if (before.data !== after.data || cells.length) undoStack.record({
2937
+ kind: "rows",
2938
+ before,
2939
+ after,
2940
+ keys: [...selected]
979
2941
  });
2942
+ const next = sourceRows();
2943
+ const rowChanges = [...selected].flatMap((key) => {
2944
+ const old = previous.byKey.get(key);
2945
+ const row = next.byKey.get(key);
2946
+ return !old && row ? [{
2947
+ kind: "inserted",
2948
+ rowKey: key,
2949
+ row: row.original,
2950
+ position: positionOf(key)
2951
+ }] : old && !row ? [{
2952
+ kind: "removed",
2953
+ rowKey: key,
2954
+ row: old.original,
2955
+ position: positions.get(key)
2956
+ }] : [];
2957
+ });
2958
+ if (before.data !== after.data) options.onDataChange?.(data, cells);
2959
+ if (problems.length) options.onEditIssues?.(problems);
2960
+ notify();
2961
+ return resultForRows(rowChanges, problems, cells);
980
2962
  }
981
2963
  function rangeEdits(origin, matrix) {
982
2964
  const { rows, columns } = getSnapshot();
@@ -996,13 +2978,356 @@ function createTable(options) {
996
2978
  return edits;
997
2979
  }
998
2980
  function replay(changes, direction) {
999
- applyChanges(changes.map((change) => ({
2981
+ applyChanges((direction === "undo" ? changes.toReversed() : changes).map((change) => ({
1000
2982
  rowKey: change.rowKey,
1001
2983
  column: change.column,
1002
2984
  value: direction === "undo" ? change.previous : change.value
1003
2985
  })), false);
1004
2986
  }
2987
+ function ingestionResult(appended, replaced, removed, problems) {
2988
+ return Object.freeze({
2989
+ status: appended + replaced + removed ? problems.length ? "partial" : "applied" : problems.length ? "rejected" : "unchanged",
2990
+ appended,
2991
+ replaced,
2992
+ removed,
2993
+ issues: Object.freeze([...problems])
2994
+ });
2995
+ }
2996
+ const stableSource = /* @__PURE__ */ new WeakMap();
2997
+ function ingestionKeys(rows) {
2998
+ let valid = stableSource.get(rows);
2999
+ if (valid === void 0) {
3000
+ valid = stableRows(rows);
3001
+ stableSource.set(rows, valid);
3002
+ }
3003
+ return valid;
3004
+ }
3005
+ function isProtectedSource(key) {
3006
+ return pendingChanges.entries.has(key) || validation.hasPending(key);
3007
+ }
3008
+ function affectedBranch(model, key) {
3009
+ const result = [key];
3010
+ const node = model?.byKey.get(key);
3011
+ if (!node) return result;
3012
+ let parent = node.parent;
3013
+ while (parent) {
3014
+ result.push(parent.row.key);
3015
+ parent = parent.parent;
3016
+ }
3017
+ const stack = [...node.children];
3018
+ while (stack.length) {
3019
+ const child = stack.pop();
3020
+ result.push(child.row.key);
3021
+ for (const descendant of child.children) stack.push(descendant);
3022
+ }
3023
+ return result;
3024
+ }
3025
+ function transformScene(source, updates, removed) {
3026
+ const seen = /* @__PURE__ */ new Set();
3027
+ const replace = (rows) => rows.flatMap((row, index) => {
3028
+ const key = readKey(row, index);
3029
+ seen.add(key);
3030
+ return removed.has(key) ? [] : [updates.get(key) ?? row];
3031
+ });
3032
+ const cache = /* @__PURE__ */ new Map();
3033
+ for (const [key, children] of source.lazy) if (!removed.has(key)) cache.set(key, replace(children));
3034
+ let next;
3035
+ if (!options.getChildren) next = replace(source.data);
3036
+ else {
3037
+ const model = buildTree(source.data, resolveColumns(definitions).byId, readKey, options, source.lazy);
3038
+ const replacements = /* @__PURE__ */ new Map();
3039
+ for (const node of model.nodes.toReversed()) {
3040
+ const key = node.row.key;
3041
+ seen.add(key);
3042
+ if (removed.has(key)) continue;
3043
+ let row = updates.get(key) ?? node.row.original;
3044
+ const supplied = updates.has(key) ? options.getChildren(row) : void 0;
3045
+ if (supplied !== void 0) cache.delete(key);
3046
+ const children = supplied ?? node.children.filter((child) => !removed.has(child.row.key)).map((child) => replacements.get(child) ?? child.row.original);
3047
+ const old = options.getChildren(row) ?? node.children.map((child) => child.row.original);
3048
+ if (supplied === void 0 && (updates.has(key) && node.children.length > 0 && !cache.has(key) || children.length !== old.length || children.some((child, index) => child !== old[index]))) {
3049
+ if (!options.setChildren) throw new Error("Updating nested source children requires setChildren.");
3050
+ row = options.setChildren(row, children);
3051
+ if (cache.has(key)) cache.set(key, children);
3052
+ }
3053
+ replacements.set(node, row);
3054
+ }
3055
+ const roots = new Map(model.roots.filter((node) => node.writable).map((node) => [node.sourceIndex, node]));
3056
+ next = source.data.flatMap((row, index) => {
3057
+ const node = roots.get(index);
3058
+ return node && removed.has(node.row.key) ? [] : [node ? replacements.get(node) ?? row : row];
3059
+ });
3060
+ }
3061
+ const additions = [...updates].filter(([key]) => !seen.has(key) && !removed.has(key)).map(([, row]) => row);
3062
+ return {
3063
+ data: appendList(next, additions),
3064
+ lazy: cache
3065
+ };
3066
+ }
3067
+ function rebaseBatches(transform, removed) {
3068
+ undoStack.transform((entry) => {
3069
+ if (entry.kind === "cells") {
3070
+ if (!removed.size) return entry;
3071
+ const changes = entry.changes.filter((change) => !removed.has(change.rowKey));
3072
+ return changes.length ? {
3073
+ ...entry,
3074
+ changes
3075
+ } : void 0;
3076
+ }
3077
+ const keys = entry.keys.filter((key) => !removed.has(key));
3078
+ return keys.length ? {
3079
+ ...entry,
3080
+ keys,
3081
+ before: transform(entry.before),
3082
+ after: transform(entry.after)
3083
+ } : void 0;
3084
+ });
3085
+ }
3086
+ function newTreeIssues(previous, next) {
3087
+ const counts = /* @__PURE__ */ new Map();
3088
+ for (const problem of previous) {
3089
+ const key = JSON.stringify([problem.code, problem.rowKey]);
3090
+ counts.set(key, (counts.get(key) ?? 0) + 1);
3091
+ }
3092
+ return next.filter((problem) => {
3093
+ const key = JSON.stringify([problem.code, problem.rowKey]);
3094
+ const remaining = counts.get(key) ?? 0;
3095
+ if (!remaining) return true;
3096
+ counts.set(key, remaining - 1);
3097
+ return false;
3098
+ });
3099
+ }
3100
+ function ingestData(incoming, mode) {
3101
+ const problems = [];
3102
+ const reject = (problem) => {
3103
+ ingestionIssues = [problem];
3104
+ notify();
3105
+ return ingestionResult(0, 0, 0, ingestionIssues);
3106
+ };
3107
+ if (destroyed) return reject(issue("invalid_ingestion", "A disposed table cannot receive source data."));
3108
+ if (!incoming.length) return ingestionResult(0, 0, 0, []);
3109
+ try {
3110
+ if (!ingestionKeys(data) || !stableRows(incoming)) return reject(issue("invalid_ingestion", "Source ingestion requires stable rowKey values or ids on every row."));
3111
+ } catch (error) {
3112
+ return reject(issue("invalid_ingestion", error instanceof Error ? error.message : String(error)));
3113
+ }
3114
+ const previous = sourceRows();
3115
+ const { byId, columns } = resolveColumns(definitions);
3116
+ const model = treeEnabled ? treeStage(data, byId, lazyVersion) : void 0;
3117
+ const seenBatch = /* @__PURE__ */ new Set();
3118
+ const updates = /* @__PURE__ */ new Map();
3119
+ for (const row of incoming) try {
3120
+ const key = readKey(row, previous.rows.length + updates.size);
3121
+ if (typeof key !== "string" && (typeof key !== "number" || !Number.isFinite(key))) {
3122
+ problems.push(issue("invalid_ingestion", "An incoming row key must be a string or finite number."));
3123
+ continue;
3124
+ }
3125
+ const existing = previous.byKey.get(key);
3126
+ if (seenBatch.has(key) || mode === "append" && existing) {
3127
+ problems.push(issue("duplicate_row_key", "An incoming source row repeats an existing key and is refused.", { rowKey: key }));
3128
+ continue;
3129
+ }
3130
+ seenBatch.add(key);
3131
+ const parent = options.getParentKey?.(row);
3132
+ const affected = affectedBranch(model, key);
3133
+ if (parent !== void 0 && parent !== null) affected.push(...affectedBranch(model, parent));
3134
+ if (affected.some(isProtectedSource)) {
3135
+ problems.push(issue("ingestion_conflict", "Incoming source data would overwrite a local change or pending validation; retry after save or revert.", { rowKey: key }));
3136
+ continue;
3137
+ }
3138
+ const node = model?.byKey.get(key);
3139
+ if (node && (!node.writable || node.lazy && options.getParentKey && options.getParentKey(row) !== node.parent?.row.key)) {
3140
+ problems.push(issue("invalid_ingestion", "This recovered or lazy tree row has no writable source position.", { rowKey: key }));
3141
+ continue;
3142
+ }
3143
+ if (existing?.original !== row) updates.set(key, row);
3144
+ } catch (error) {
3145
+ problems.push(issue("invalid_ingestion", error instanceof Error ? error.message : String(error)));
3146
+ }
3147
+ if (!updates.size) {
3148
+ ingestionIssues = problems;
3149
+ notify();
3150
+ return ingestionResult(0, 0, 0, problems);
3151
+ }
3152
+ const replaced = [...updates.keys()].filter((key) => previous.byKey.has(key)).length;
3153
+ const appended = updates.size - replaced;
3154
+ const previousState = state;
3155
+ try {
3156
+ if (!model && !replaced) {
3157
+ const added = Object.freeze([...updates.values()]);
3158
+ const indexed = added.map((row, offset) => createRow(row, readKey(row, previous.rows.length + offset), previous.rows.length + offset, byId, options.getRowCanExpand?.(row) ?? false));
3159
+ const filtered = manual ? previous.rows : filterStage(previous.rows, columns, state.search, state.filters);
3160
+ const addedFiltered = manual ? indexed : filterRows(indexed, columns, state.search, state.filters);
3161
+ const allRows = appendList(previous.rows, indexed);
3162
+ const allFiltered = appendList(filtered, addedFiltered);
3163
+ const keys = membership(filtered);
3164
+ for (const row of addedFiltered) keys.add(row.key);
3165
+ rowMembership.set(allFiltered, keys);
3166
+ const sorted = manual || !state.sorting.some((rule) => columns.some((column) => column.id === rule.column)) ? allFiltered : mergeRows(sortStage(filtered, columns, state.sorting), addedFiltered, columns, state.sorting);
3167
+ const next = appendList(data, added);
3168
+ rebaseBatches((source) => ({
3169
+ ...source,
3170
+ data: appendList(source.data, added)
3171
+ }), /* @__PURE__ */ new Set());
3172
+ for (const row of indexed) previous.byKey.set(row.key, row);
3173
+ primedIndex = {
3174
+ data: next,
3175
+ columns: byId,
3176
+ result: {
3177
+ rows: allRows,
3178
+ byKey: previous.byKey,
3179
+ issues: previous.issues
3180
+ }
3181
+ };
3182
+ primedFilter = {
3183
+ rows: allRows,
3184
+ columns,
3185
+ search: state.search,
3186
+ filters: state.filters,
3187
+ result: allFiltered
3188
+ };
3189
+ primedSort = {
3190
+ rows: allFiltered,
3191
+ columns,
3192
+ sorting: state.sorting,
3193
+ result: sorted
3194
+ };
3195
+ stableSource.set(next, true);
3196
+ data = next;
3197
+ } else {
3198
+ const next = transformScene(scene(), updates, /* @__PURE__ */ new Set());
3199
+ const candidate = treeEnabled ? buildTree(next.data, byId, readKey, options, next.lazy) : indexRows(next.data, byId);
3200
+ const fresh = newTreeIssues(previous.issues, candidate.issues);
3201
+ if (fresh.length || !stableRows(candidate.rows.map((row) => row.original))) {
3202
+ problems.push(...fresh, ...!stableRows(candidate.rows.map((row) => row.original)) ? [issue("invalid_ingestion", "Every incoming descendant needs a stable key.")] : []);
3203
+ ingestionIssues = problems;
3204
+ notify();
3205
+ return ingestionResult(0, 0, 0, problems);
3206
+ }
3207
+ const nextKeys = new Set(candidate.rows.map((row) => row.key));
3208
+ const removedKeys = new Set(previous.rows.filter((row) => !nextKeys.has(row.key)).map((row) => row.key));
3209
+ rebaseBatches((source) => transformScene(source, updates, removedKeys), removedKeys);
3210
+ data = next.data;
3211
+ lazyChildren.clear();
3212
+ for (const [key, rows] of next.lazy) lazyChildren.set(key, rows);
3213
+ lazyVersion++;
3214
+ for (const key of [...updates.keys(), ...removedKeys]) {
3215
+ pendingLoads.get(key)?.abort();
3216
+ pendingLoads.delete(key);
3217
+ childStatuses.delete(key);
3218
+ loadIssues.delete(key);
3219
+ loadValidationIssues.delete(key);
3220
+ }
3221
+ validation.cancel([...removedKeys]);
3222
+ const selection = state.selection.filter((key) => !removedKeys.has(key));
3223
+ state = mergeState(state, {
3224
+ selection: selection.length === state.selection.length ? state.selection : selection,
3225
+ expanded: state.expanded === true ? true : state.expanded.filter((key) => !removedKeys.has(key))
3226
+ });
3227
+ }
3228
+ validation.cancel([...updates.keys()]);
3229
+ ingestionIssues = problems;
3230
+ } catch (error) {
3231
+ return reject(issue("invalid_ingestion", error instanceof Error ? error.message : String(error)));
3232
+ }
3233
+ if (state !== previousState) options.onStateChange?.(state, previousState);
3234
+ options.onDataChange?.(data, []);
3235
+ notify();
3236
+ startVisibleLoads();
3237
+ return ingestionResult(appended, replaced, 0, problems);
3238
+ }
3239
+ function removeData(keys) {
3240
+ const previousState = state;
3241
+ const problems = [];
3242
+ const removed = /* @__PURE__ */ new Set();
3243
+ const previous = sourceRows();
3244
+ const model = treeEnabled ? treeStage(data, resolveColumns(definitions).byId, lazyVersion) : void 0;
3245
+ if (destroyed || !ingestionKeys(data)) problems.push(issue("invalid_ingestion", "Source removal requires a live table and stable row keys."));
3246
+ else for (const key of new Set(keys)) {
3247
+ if (!previous.byKey.has(key)) {
3248
+ problems.push(issue("unknown_row", "The source row to remove does not exist.", { rowKey: key }));
3249
+ continue;
3250
+ }
3251
+ if (affectedBranch(model, key).some(isProtectedSource)) {
3252
+ problems.push(issue("ingestion_conflict", "Source removal would discard local changes or pending validation.", { rowKey: key }));
3253
+ continue;
3254
+ }
3255
+ const node = model?.byKey.get(key);
3256
+ if (node && !node.writable) {
3257
+ problems.push(issue("invalid_ingestion", "A recovered tree row has no writable source position.", { rowKey: key }));
3258
+ continue;
3259
+ }
3260
+ removed.add(key);
3261
+ const stack = node ? [...node.children] : [];
3262
+ while (stack.length) {
3263
+ const child = stack.pop();
3264
+ removed.add(child.row.key);
3265
+ for (const descendant of child.children) stack.push(descendant);
3266
+ }
3267
+ }
3268
+ try {
3269
+ if (removed.size) {
3270
+ const next = transformScene(scene(), /* @__PURE__ */ new Map(), removed);
3271
+ rebaseBatches((source) => transformScene(source, /* @__PURE__ */ new Map(), removed), removed);
3272
+ data = next.data;
3273
+ lazyChildren.clear();
3274
+ for (const [key, rows] of next.lazy) lazyChildren.set(key, rows);
3275
+ lazyVersion++;
3276
+ validation.cancel([...removed]);
3277
+ for (const key of removed) {
3278
+ pendingLoads.get(key)?.abort();
3279
+ pendingLoads.delete(key);
3280
+ childStatuses.delete(key);
3281
+ loadIssues.delete(key);
3282
+ loadValidationIssues.delete(key);
3283
+ }
3284
+ const selection = state.selection.filter((key) => !removed.has(key));
3285
+ state = mergeState(state, {
3286
+ selection: selection.length === state.selection.length ? state.selection : selection,
3287
+ expanded: state.expanded === true ? true : state.expanded.filter((key) => !removed.has(key))
3288
+ });
3289
+ }
3290
+ ingestionIssues = problems;
3291
+ } catch (error) {
3292
+ ingestionIssues = [issue("invalid_ingestion", error instanceof Error ? error.message : String(error))];
3293
+ notify();
3294
+ return ingestionResult(0, 0, 0, ingestionIssues);
3295
+ }
3296
+ if (state !== previousState) options.onStateChange?.(state, previousState);
3297
+ if (removed.size) options.onDataChange?.(data, []);
3298
+ notify();
3299
+ startVisibleLoads();
3300
+ return ingestionResult(0, 0, removed.size, problems);
3301
+ }
3302
+ if (expandMode === "single") {
3303
+ initialState = mergeState(initialState, { expanded: canonicalExpanded(initialState.expanded) });
3304
+ state = initialState;
3305
+ snapshot = void 0;
3306
+ }
1005
3307
  const table = {
3308
+ appendData: (rows) => ingestData(rows, "append"),
3309
+ upsertData: (rows) => ingestData(rows, "upsert"),
3310
+ removeData,
3311
+ stream(source, streamOptions) {
3312
+ if (destroyed) return Promise.resolve(Object.freeze({
3313
+ status: "aborted",
3314
+ receivedRowCount: 0,
3315
+ issues: Object.freeze([issue("invalid_ingestion", "A disposed table cannot start a source.")])
3316
+ }));
3317
+ return streaming.run(source, {
3318
+ expectedRowCount: sourceExpected,
3319
+ ...streamOptions
3320
+ });
3321
+ },
3322
+ insertRows,
3323
+ removeRows,
3324
+ revert,
3325
+ getPendingChanges: () => pendingChanges.snapshot(),
3326
+ markSaved(keys) {
3327
+ if (!keys) pendingChanges.entries.clear();
3328
+ else for (const key of keys) pendingChanges.entries.delete(key);
3329
+ notify();
3330
+ },
1006
3331
  getSnapshot,
1007
3332
  getState: () => state,
1008
3333
  subscribe(listener) {
@@ -1012,21 +3337,40 @@ function createTable(options) {
1012
3337
  };
1013
3338
  },
1014
3339
  setState(patch) {
1015
- const next = mergeState(state, patch);
1016
- if (next !== state) {
3340
+ const previousIssue = expandedIssue;
3341
+ const next = mergePatch(patch);
3342
+ if (next !== state || previousIssue !== expandedIssue) {
1017
3343
  state = next;
3344
+ snapshot = void 0;
3345
+ abortLoads();
1018
3346
  notify();
3347
+ startVisibleLoads();
1019
3348
  }
1020
3349
  },
1021
3350
  setData(next) {
1022
3351
  if (next === data) return;
3352
+ validation.cancel();
3353
+ streaming.cancel();
3354
+ loadState = "idle";
3355
+ streamIssues = [];
3356
+ ingestionIssues = [];
3357
+ expectedRowCount = sourceExpected;
3358
+ pendingChanges.entries.clear();
3359
+ abortLoads(true);
3360
+ lazyChildren.clear();
3361
+ childStatuses.clear();
3362
+ loadIssues.clear();
3363
+ loadValidationIssues.clear();
3364
+ lazyVersion++;
1023
3365
  data = next;
1024
3366
  undoStack.clear();
1025
3367
  notify();
3368
+ startVisibleLoads();
1026
3369
  },
1027
3370
  setColumns(next) {
1028
3371
  if (next === definitions) return;
1029
3372
  definitions = next;
3373
+ validation.cancel();
1030
3374
  notify();
1031
3375
  },
1032
3376
  setRowCount(next) {
@@ -1034,6 +3378,12 @@ function createTable(options) {
1034
3378
  rowCount = next;
1035
3379
  notify();
1036
3380
  },
3381
+ setTreeFilter(next) {
3382
+ const mode = next ?? "ancestors";
3383
+ if (mode === treeFilter) return;
3384
+ treeFilter = mode;
3385
+ notify();
3386
+ },
1037
3387
  toggleSort(columnId, { multi = false } = {}) {
1038
3388
  const current = state.sorting.find((rule) => rule.column === columnId)?.direction;
1039
3389
  setSort(columnId, current === void 0 ? "asc" : current === "asc" ? "desc" : void 0, multi);
@@ -1069,6 +3419,7 @@ function createTable(options) {
1069
3419
  });
1070
3420
  },
1071
3421
  goToPage(page) {
3422
+ if (!paginate) return;
1072
3423
  const { pageCount, page: currentPage } = getSnapshot();
1073
3424
  const next = clampPage(page, pageCount);
1074
3425
  if (next !== currentPage || next !== state.pagination.page) commit({ pagination: {
@@ -1083,6 +3434,7 @@ function createTable(options) {
1083
3434
  table.goToPage(getSnapshot().page - 1);
1084
3435
  },
1085
3436
  setPageSize(pageSize) {
3437
+ if (!paginate) return;
1086
3438
  if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize === state.pagination.pageSize) return;
1087
3439
  const { pageStart } = getSnapshot();
1088
3440
  commit({ pagination: {
@@ -1091,14 +3443,14 @@ function createTable(options) {
1091
3443
  } });
1092
3444
  },
1093
3445
  select(keys) {
1094
- applySelection([...state.selection, ...keys]);
3446
+ applySelection([...state.selection, ...selectionKeys(keys)]);
1095
3447
  },
1096
3448
  deselect(keys) {
1097
- const removed = new Set(keys);
3449
+ const removed = new Set(selectionKeys(keys));
1098
3450
  applySelection(state.selection.filter((key) => !removed.has(key)));
1099
3451
  },
1100
3452
  toggleRow(key, selected) {
1101
- const isSelected = state.selection.includes(key);
3453
+ const isSelected = treeEnabled ? getSnapshot().getRow(key)?.selection === "all" : state.selection.includes(key);
1102
3454
  const next = selected ?? !isSelected;
1103
3455
  if (next === isSelected) return;
1104
3456
  if (next) table.select([key]);
@@ -1121,6 +3473,27 @@ function createTable(options) {
1121
3473
  return row ? [row] : [];
1122
3474
  });
1123
3475
  },
3476
+ toggleExpanded(key, expanded) {
3477
+ const row = getSnapshot().getRow(key);
3478
+ if (!row?.canExpand) return;
3479
+ const open = expanded ?? !row.isExpanded;
3480
+ if (open === row.isExpanded) {
3481
+ if (open && row.childStatus === "error") requestChildren(row);
3482
+ return;
3483
+ }
3484
+ if (expandMode === "single") commit({ expanded: open ? [key] : [] });
3485
+ else if (state.expanded === true) {
3486
+ const { byId } = resolveColumns(definitions);
3487
+ commit({ expanded: (treeEnabled ? treeStage(data, byId, lazyVersion).rows.map((candidate) => getSnapshot().getRow(candidate.key)) : indexRows(data, byId).rows).filter((candidate) => candidate.canExpand && candidate.key !== key).map((candidate) => candidate.key) });
3488
+ } else commit({ expanded: open ? [...state.expanded, key] : state.expanded.filter((candidate) => candidate !== key) });
3489
+ if (open) requestChildren(getSnapshot().getRow(key));
3490
+ },
3491
+ expandAll() {
3492
+ commit({ expanded: true });
3493
+ },
3494
+ collapseAll() {
3495
+ commit({ expanded: [] });
3496
+ },
1124
3497
  toggleColumn(columnId, visible) {
1125
3498
  if (!getSnapshot().getColumn(columnId)) return;
1126
3499
  const hidden = state.hiddenColumns.includes(columnId);
@@ -1138,22 +3511,26 @@ function createTable(options) {
1138
3511
  order.splice(target, 0, columnId);
1139
3512
  commit({ columnOrder: order });
1140
3513
  },
1141
- edit(edits) {
1142
- return applyChanges(Array.isArray(edits) ? edits : [edits], true);
3514
+ edit(edits, editOptions) {
3515
+ return validation.edit(Array.isArray(edits) ? edits : [edits], [], editOptions);
1143
3516
  },
1144
3517
  undo() {
1145
- const changes = undoStack.undo();
1146
- if (!changes) return false;
1147
- replay(changes, "undo");
3518
+ const entry = undoStack.undo();
3519
+ if (!entry) return false;
3520
+ validation.cancel();
3521
+ if (entry.kind === "cells") replay(entry.changes, "undo");
3522
+ else restoreScene(entry, "undo");
1148
3523
  return true;
1149
3524
  },
1150
3525
  redo() {
1151
- const changes = undoStack.redo();
1152
- if (!changes) return false;
1153
- replay(changes, "redo");
3526
+ const entry = undoStack.redo();
3527
+ if (!entry) return false;
3528
+ validation.cancel();
3529
+ if (entry.kind === "cells") replay(entry.changes, "redo");
3530
+ else restoreScene(entry, "redo");
1154
3531
  return true;
1155
3532
  },
1156
- copy(range) {
3533
+ copy(range, { escapeFormulas = false } = {}) {
1157
3534
  const { rows, columns } = getSnapshot();
1158
3535
  const matrix = [];
1159
3536
  rows.forEach((row, rowIndex) => {
@@ -1162,46 +3539,72 @@ function createTable(options) {
1162
3539
  if (rangeContains(range, {
1163
3540
  row: rowIndex,
1164
3541
  column: columnIndex
1165
- })) line.push(row.getDisplay(column.id));
3542
+ })) {
3543
+ const display = row.getDisplay(column.id);
3544
+ line.push(escapeFormulas ? escapeFormula(display, row.getValue(column.id)) : display);
3545
+ }
1166
3546
  });
1167
3547
  if (line.length > 0) matrix.push(line);
1168
3548
  });
1169
3549
  return toDelimited(matrix);
1170
3550
  },
1171
3551
  paste(origin, text) {
1172
- return applyChanges(rangeEdits(origin, parseDelimited(text)), true);
3552
+ if (!Number.isInteger(origin.row) || !Number.isInteger(origin.column) || origin.row < 0 || origin.column < 0) return validation.edit([], [issue("invalid_value", "The paste origin must use non-negative whole row and column indices.")]);
3553
+ const { rows, columns } = getSnapshot();
3554
+ const parsed = readDelimited(text, {
3555
+ maxRows: Math.max(0, rows.length - origin.row),
3556
+ maxColumns: Math.max(0, columns.length - origin.column),
3557
+ maxCells: maxPasteCells,
3558
+ maxLength: maxPasteLength
3559
+ });
3560
+ const issues = parsed.truncated ? [issue("paste_truncated", "The paste exceeds the visible grid or parsing limits; only complete cells within those limits are applied.")] : [];
3561
+ return validation.edit(rangeEdits(origin, parsed.rows), issues);
1173
3562
  },
1174
3563
  clear(range) {
1175
3564
  const matrix = Array.from({ length: range.bottom - range.top + 1 }, () => Array.from({ length: range.right - range.left + 1 }, () => ""));
1176
3565
  const current = getSnapshot();
1177
- return applyChanges(rangeEdits({
3566
+ const edits = rangeEdits({
1178
3567
  row: range.top,
1179
3568
  column: range.left
1180
3569
  }, matrix).filter((edit) => {
1181
3570
  const row = current.getRow(edit.rowKey);
1182
3571
  return row !== void 0 && current.getColumn(edit.column)?.isEditable(row.original) === true;
1183
- }), true);
3572
+ });
3573
+ return validation.edit(edits);
1184
3574
  },
1185
- exportRows({ format = "csv", columns: ids, pageOnly = false, headers = true } = {}) {
3575
+ exportRows({ format = "csv", columns: ids, pageOnly = false, headers = true, escapeFormulas = true, depth = false, indent = "" } = {}) {
1186
3576
  const current = getSnapshot();
1187
3577
  const columns = ids ? ids.flatMap((id) => {
1188
3578
  const column = current.getColumn(id);
1189
3579
  return column ? [column] : [];
1190
3580
  }) : current.columns;
1191
- const matrix = (pageOnly ? current.rows : current.processedRows).map((row) => columns.map((column) => row.getDisplay(column.id)));
1192
- if (headers) matrix.unshift(columns.map((column) => column.header));
3581
+ const rows = pageOnly ? current.rows : current.processedRows;
3582
+ const matrix = rows.map((row) => columns.map((column, index) => {
3583
+ const display = (index === 0 ? indent.repeat(row.depth) : "") + row.getDisplay(column.id);
3584
+ return escapeFormulas ? escapeFormula(display, row.getValue(column.id)) : display;
3585
+ }));
3586
+ if (depth) matrix.forEach((line, index) => line.unshift(String(rows[index].depth)));
3587
+ if (headers) {
3588
+ matrix.unshift(columns.map((column) => escapeFormulas ? escapeFormula(column.header) : column.header));
3589
+ if (depth) matrix[0].unshift("Depth");
3590
+ }
1193
3591
  return toDelimited(matrix, format === "csv" ? "," : " ");
1194
3592
  },
1195
3593
  reset() {
1196
3594
  commit(initialState);
1197
3595
  },
1198
3596
  destroy() {
3597
+ destroyed = true;
3598
+ streaming.cancel();
3599
+ validation.cancel();
3600
+ abortLoads(true);
1199
3601
  listeners.clear();
1200
3602
  }
1201
3603
  };
3604
+ startVisibleLoads();
1202
3605
  return table;
1203
3606
  }
1204
3607
 
1205
3608
  //#endregion
1206
- export { DEFAULT_PAGE_SIZE, clampPage, clampPosition, columnLabel, compareValues, countPages, createTable, createUndoStack, defineColumns, filterRows, formatValue, fromA1, getPath, gridCommand, humanize, inferColumns, matchesFilter, moveSelection, paginateRows, paginationItems, parseAs, parseDelimited, rangeContains, resolveColumn, selectCell, selectionRange, setPath, sortRows, toA1, toDelimited, typeOf };
3609
+ export { DEFAULT_PAGE_SIZE, clampPage, clampPosition, columnLabel, compareValues, countPages, createTable, createUndoStack, createVirtualizer, defineColumns, filterRows, formatValue, fromA1, getPath, getRowItemKey, gridCommand, humanize, inferColumns, matchesFilter, moveSelection, paginateRows, paginationItems, parseAs, parseDelimited, rangeContains, resolveColumn, selectCell, selectionRange, setPath, sortRows, toA1, toDelimited, typeOf };
1207
3610
  //# sourceMappingURL=index.js.map