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

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,1050 @@ 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
+ const treeEnabled = Boolean(options.getChildren || options.getParentKey || options.hasChildren || options.loadChildren);
1814
+ const rootPagination = treeEnabled && (manual || (options.paginateBy ?? "root") === "root");
1815
+ const projectedRows = /* @__PURE__ */ new WeakMap();
1816
+ function shareRow(base, next) {
1817
+ const previous = projectedRows.get(base);
1818
+ if (previous && Object.keys(next).every((key) => previous[key] === next[key])) return previous;
1819
+ projectedRows.set(base, next);
1820
+ return next;
1821
+ }
1822
+ let lazyVersion = 0;
1823
+ const lazyChildren = /* @__PURE__ */ new Map();
1824
+ const childStatuses = /* @__PURE__ */ new Map();
1825
+ const loadIssues = /* @__PURE__ */ new Map();
1826
+ const loadValidationIssues = /* @__PURE__ */ new Map();
1827
+ const pendingLoads = /* @__PURE__ */ new Map();
1828
+ let destroyed = false;
1829
+ const undoStack = createUndoStack(options.historyLimit);
1830
+ const pendingChanges = createPendingChanges();
1831
+ const listeners = /* @__PURE__ */ new Set();
1832
+ const optionIssues = [];
1833
+ const maxPasteCells = pasteLimit(options.pasteLimit?.maxCells, 1e5, "maxCells", optionIssues);
1834
+ const maxPasteLength = pasteLimit(options.pasteLimit?.maxLength, 5e6, "maxLength", optionIssues);
1835
+ const sourceExpected = options.expectedRowCount === void 0 || Number.isSafeInteger(options.expectedRowCount) && options.expectedRowCount >= 0 ? options.expectedRowCount : void 0;
1836
+ if (sourceExpected !== options.expectedRowCount) optionIssues.push(issue("invalid_stream_option", "expectedRowCount must be a non-negative safe integer; it is unknown instead."));
1837
+ let data = options.data;
1838
+ let definitions = options.columns;
1839
+ let rowCount = options.rowCount;
1840
+ let expandedIssue;
1841
+ let initialState = createInitialState({
1842
+ ...options.initialState,
1843
+ hiddenColumns: options.initialState?.hiddenColumns ?? options.columns.filter((column) => column.hidden === true).map((column) => column.id)
1844
+ }, (problem) => {
1845
+ expandedIssue = problem;
1846
+ });
1847
+ let state = initialState;
1848
+ let snapshot;
1849
+ let loadState = "idle";
1850
+ let expectedRowCount = sourceExpected;
1851
+ let ingestionIssues = [];
1852
+ let streamIssues = [];
1853
+ const streaming = createStream({
1854
+ ingest: (rows, mode) => ingestData(rows, mode),
1855
+ update(next, expected, problems, publish) {
1856
+ loadState = next;
1857
+ expectedRowCount = expected;
1858
+ streamIssues = [...problems];
1859
+ if (publish) notify();
1860
+ }
1861
+ });
1862
+ let transaction = false;
1863
+ let deferValidationNotify = false;
1864
+ let validationNotifyRequested = false;
1865
+ const validation = createEditValidation({
1866
+ snapshot: getSnapshot,
1867
+ apply: applyChanges,
1868
+ notify,
1869
+ writeIssue(row, next) {
1870
+ const node = treeEnabled ? treeStage(data, resolveColumns(definitions).byId, lazyVersion).byKey.get(row.key) : void 0;
1871
+ 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 });
1872
+ 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 });
1873
+ },
1874
+ batch(run) {
1875
+ deferValidationNotify = true;
1876
+ validationNotifyRequested = false;
1877
+ try {
1878
+ return run();
1879
+ } finally {
1880
+ deferValidationNotify = false;
1881
+ if (validationNotifyRequested) notify();
1882
+ }
1883
+ },
1884
+ record: (changes) => undoStack.record({
1885
+ kind: "cells",
1886
+ changes
1887
+ }),
1888
+ onIssues: (issues) => options.onEditIssues?.(issues),
1889
+ optimistic: options.asyncValidation === "optimistic",
1890
+ validateRow: options.validateRow
1891
+ });
1892
+ const readKey = (row, index) => {
1893
+ const { rowKey } = options;
1894
+ if (typeof rowKey === "function") return rowKey(row, index);
1895
+ const value = getPath(row, rowKey ?? "id");
1896
+ if (typeof value === "string" || typeof value === "number") return value;
1897
+ return rowKey === void 0 || !isRecord(row) ? index : String(value);
1898
+ };
1899
+ const resolveColumns = memo((defs) => {
1900
+ const issues = [];
1901
+ const columns = defs.flatMap((definition) => {
1902
+ if (!isSafePath(definition.id)) {
1903
+ issues.push(issue("unsafe_path", `The column "${definition.id}" addresses a prototype and is ignored.`, { column: definition.id }));
1904
+ return [];
1905
+ }
1906
+ return [resolveColumn(definition)];
1907
+ });
1908
+ return {
1909
+ columns,
1910
+ byId: new Map(columns.map((column) => [column.id, column])),
1911
+ issues
1912
+ };
1913
+ });
1914
+ let primedIndex;
1915
+ let reusableRows;
1916
+ const indexRows = memo((rows, byId) => {
1917
+ if (primedIndex?.data === rows && primedIndex.columns === byId) return primedIndex.result;
1918
+ const byKey = /* @__PURE__ */ new Map();
1919
+ const issues = [];
1920
+ const indexed = rows.map((original, index) => {
1921
+ let key = readKey(original, index);
1922
+ if (byKey.has(key)) {
1923
+ issues.push(issue("duplicate_row_key", `Row ${index} repeats the key "${String(key)}"; it is keyed "${String(key)}#${index}" instead.`, { rowKey: key }));
1924
+ key = `${String(key)}#${index}`;
1925
+ }
1926
+ const previous = reusableRows?.columns === byId ? reusableRows.rows.get(key) : void 0;
1927
+ const row = previous?.original === original && previous.index === index ? previous : createRow(original, key, index, byId, options.getRowCanExpand?.(original) ?? false);
1928
+ byKey.set(key, row);
1929
+ return row;
1930
+ });
1931
+ stableSource.set(rows, stableRows(rows));
1932
+ reusableRows = {
1933
+ columns: byId,
1934
+ rows: byKey
1935
+ };
1936
+ return {
1937
+ rows: indexed,
744
1938
  byKey,
745
1939
  issues
746
1940
  };
747
1941
  });
1942
+ const treeStage = memo((rows, byId, _version) => buildTree(rows, byId, readKey, options, lazyChildren));
1943
+ const treeIndex = memo((model) => ({
1944
+ rows: model.rows,
1945
+ byKey: new Map(model.rows.map((row) => [row.key, row])),
1946
+ issues: model.issues
1947
+ }));
1948
+ const treePositions = memo((model) => {
1949
+ const result = /* @__PURE__ */ new Map();
1950
+ const index = (siblings, parent) => {
1951
+ siblings.forEach((node, position) => result.set(node.row.key, Object.freeze({
1952
+ parent,
1953
+ before: siblings[position + 1]?.row.key
1954
+ })));
1955
+ };
1956
+ index(model.roots);
1957
+ for (const node of model.nodes) index(node.children, node.row.key);
1958
+ return result;
1959
+ });
1960
+ const processedTreeStage = memo((model, columns, search, filters, sorting) => processTree(model, columns, {
1961
+ ...state,
1962
+ search,
1963
+ filters,
1964
+ sorting
1965
+ }, options.treeFilter ?? "ancestors", manual));
1966
+ const flattenTree = createTreeFlattener();
1967
+ const emptyAncestors = /* @__PURE__ */ new Set();
1968
+ const singleTreePath = memo((model, expanded) => {
1969
+ const ancestors = /* @__PURE__ */ new Set();
1970
+ if (expanded !== true) for (const key of expanded) {
1971
+ let parent = model.byKey.get(key)?.parent;
1972
+ while (parent && !ancestors.has(parent.row.key)) {
1973
+ ancestors.add(parent.row.key);
1974
+ parent = parent.parent;
1975
+ }
1976
+ }
1977
+ return ancestors;
1978
+ });
1979
+ const treeSelection = memo((model, selection) => {
1980
+ const selected = new Set(selection);
1981
+ const counts = /* @__PURE__ */ new Map();
1982
+ const stack = model.roots.map((node) => ({
1983
+ node,
1984
+ exit: false
1985
+ }));
1986
+ while (stack.length) {
1987
+ const { node, exit } = stack.pop();
1988
+ if (!exit) {
1989
+ stack.push({
1990
+ node,
1991
+ exit: true
1992
+ });
1993
+ for (const child of node.children) stack.push({
1994
+ node: child,
1995
+ exit: false
1996
+ });
1997
+ } else {
1998
+ let count = selected.has(node.row.key) ? 1 : 0;
1999
+ let total = 1;
2000
+ for (const child of node.children) {
2001
+ const value = counts.get(child);
2002
+ count += value.selected;
2003
+ total += value.total;
2004
+ }
2005
+ counts.set(node, {
2006
+ selected: count,
2007
+ total
2008
+ });
2009
+ }
2010
+ }
2011
+ return counts;
2012
+ });
748
2013
  const orderColumns = memo((columns, columnOrder, hiddenColumns) => {
749
2014
  const position = new Map(columnOrder.map((id, index) => [id, index]));
750
2015
  const ordered = columns.map((column, index) => ({
@@ -759,11 +2024,43 @@ function createTable(options) {
759
2024
  visible: ordered.filter((column) => !hidden.has(column.id))
760
2025
  };
761
2026
  });
762
- const filterStage = memo(filterRows);
763
- const sortStage = memo(sortRows);
2027
+ let primedFilter;
2028
+ let primedSort;
2029
+ 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));
2030
+ const sortStage = memo((rows, columns, sorting) => primedSort?.rows === rows && primedSort.columns === columns && primedSort.sorting === sorting ? primedSort.result : sortRows(rows, columns, sorting));
2031
+ const rowMembership = /* @__PURE__ */ new WeakMap();
2032
+ function membership(rows) {
2033
+ let keys = rowMembership.get(rows);
2034
+ if (!keys) {
2035
+ keys = new Set(rows.map((row) => row.key));
2036
+ rowMembership.set(rows, keys);
2037
+ }
2038
+ return keys;
2039
+ }
764
2040
  const selectedSet = memo((selection) => new Set(selection));
2041
+ const expansionStage = memo((indexed, expanded, selection) => {
2042
+ const keys = expanded === true ? void 0 : new Set(expanded);
2043
+ const selected = new Set(selection);
2044
+ const rows = mapList(indexed.rows, (row) => {
2045
+ const isExpanded = row.canExpand && (expanded === true || (keys?.has(row.key) ?? false));
2046
+ return isExpanded || selected.has(row.key) ? shareRow(row, Object.freeze({
2047
+ ...row,
2048
+ isExpanded,
2049
+ selection: selected.has(row.key) ? "all" : "none"
2050
+ })) : row;
2051
+ });
2052
+ return {
2053
+ rows,
2054
+ byKey: { get(key) {
2055
+ const row = indexed.byKey.get(key);
2056
+ return row && row.index < rows.length ? rows[row.index] : void 0;
2057
+ } }
2058
+ };
2059
+ });
2060
+ const expandedOrder = memo((rows, byKey) => mapList(rows, (row) => byKey.get(row.key)));
765
2061
  function stateIssues(byId) {
766
2062
  const issues = [];
2063
+ if (expandedIssue) issues.push(expandedIssue);
767
2064
  const referenced = /* @__PURE__ */ new Set([
768
2065
  ...state.sorting.map((rule) => rule.column),
769
2066
  ...Object.keys(state.filters),
@@ -772,30 +2069,105 @@ function createTable(options) {
772
2069
  ]);
773
2070
  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
2071
  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.`));
2072
+ 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
2073
  return issues;
777
2074
  }
778
2075
  function computeSnapshot() {
779
- const { columns, byId } = resolveColumns(definitions);
780
- const indexed = indexRows(data, byId);
2076
+ const cellProblems = validation.problems();
2077
+ const dirty = new Set(pendingChanges.entries.keys());
2078
+ const metadata = (row) => dirty.has(row.key) || cellProblems.has(row.key) ? shareRow(row, Object.freeze({
2079
+ ...row,
2080
+ isDirty: dirty.has(row.key),
2081
+ cellIssues: cellProblems.get(row.key)
2082
+ })) : row;
2083
+ const { columns, byId, issues: columnIssues } = resolveColumns(definitions);
2084
+ const model = treeEnabled ? treeStage(data, byId, lazyVersion) : void 0;
2085
+ const indexed = model ? {
2086
+ rows: model.rows,
2087
+ issues: model.issues
2088
+ } : indexRows(data, byId);
2089
+ const expanded = model ? void 0 : expansionStage(indexed, state.expanded, state.selection);
781
2090
  const ordered = orderColumns(columns, state.columnOrder, state.hiddenColumns);
782
- const issues = [...indexed.issues, ...stateIssues(byId)];
2091
+ const issues = [
2092
+ ...optionIssues,
2093
+ ...ingestionIssues,
2094
+ ...streamIssues,
2095
+ ...columnIssues,
2096
+ ...indexed.issues,
2097
+ ...stateIssues(byId),
2098
+ ...loadIssues.values(),
2099
+ ...Array.from(loadValidationIssues.values()).flat(),
2100
+ ...Array.from(cellProblems.values()).flatMap((row) => [...row.values()])
2101
+ ];
783
2102
  const requestedSize = state.pagination.pageSize;
784
- const pageSize = Number.isInteger(requestedSize) && requestedSize >= 1 ? requestedSize : 10;
2103
+ let pageSize = Number.isInteger(requestedSize) && requestedSize >= 1 ? requestedSize : 10;
785
2104
  let processedRows;
786
2105
  let rows;
787
2106
  let filteredCount;
788
2107
  let page;
789
2108
  let pageCount;
790
- if (manual) {
791
- processedRows = indexed.rows;
792
- rows = indexed.rows;
2109
+ let treeRow;
2110
+ let totalCount = indexed.rows.length;
2111
+ if (model) {
2112
+ const processed = processedTreeStage(model, columns, state.search, state.filters, state.sorting);
2113
+ const ancestors = expandMode === "single" ? singleTreePath(model, state.expanded) : emptyAncestors;
2114
+ const visible = flattenTree(processed, state.expanded, ancestors);
2115
+ const expandedState = state.expanded;
2116
+ const keys = state.expanded === true ? void 0 : new Set(state.expanded);
2117
+ const selected = treeSelection(model, state.selection);
2118
+ const projected = /* @__PURE__ */ new Map();
2119
+ const statuses = new Map(childStatuses);
2120
+ treeRow = (key) => {
2121
+ const cached = projected.get(key);
2122
+ if (cached) return cached;
2123
+ const node = model.byKey.get(key);
2124
+ if (!node) return void 0;
2125
+ const status = statuses.get(key) ?? (node.children.length || !node.unloaded ? "loaded" : "idle");
2126
+ const count = selected.get(node);
2127
+ const canExpand = node.children.length > 0 || status !== "loaded" || (options.getRowCanExpand?.(node.row.original) ?? false);
2128
+ const result = shareRow(node.row, Object.freeze({
2129
+ ...node.row,
2130
+ isDirty: dirty.has(key),
2131
+ cellIssues: cellProblems.get(key),
2132
+ depth: node.depth,
2133
+ parentKey: node.parent?.row.key,
2134
+ childCount: status === "loaded" ? node.children.length : void 0,
2135
+ childStatus: status,
2136
+ canExpand,
2137
+ isExpanded: canExpand && (expandedState === true || (keys?.has(key) ?? false) || ancestors.has(key) || processed.autoExpanded.has(key)),
2138
+ selection: coverage(count.selected, count.total)
2139
+ }));
2140
+ projected.set(key, result);
2141
+ return result;
2142
+ };
2143
+ processedRows = visible.map((node) => treeRow(node.row.key));
2144
+ filteredCount = manual ? rowCount ?? model.roots.length : rootPagination ? processed.roots.length : visible.length;
2145
+ totalCount = rootPagination ? model.roots.length : model.nodes.length;
2146
+ pageCount = countPages(filteredCount, pageSize);
2147
+ page = clampPage(state.pagination.page, pageCount);
2148
+ if (manual) rows = processedRows;
2149
+ else if (rootPagination) {
2150
+ const roots = new Set(processed.roots.slice((page - 1) * pageSize, page * pageSize));
2151
+ const allRoots = new Set(processed.roots);
2152
+ let root;
2153
+ rows = processedRows.filter((_row, index) => {
2154
+ const node = visible[index];
2155
+ if (allRoots.has(node)) root = node;
2156
+ return root !== void 0 && roots.has(root);
2157
+ });
2158
+ } else rows = paginateRows(processedRows, {
2159
+ page,
2160
+ pageSize
2161
+ });
2162
+ } else if (manual) {
2163
+ processedRows = expanded.rows;
2164
+ rows = expanded.rows;
793
2165
  filteredCount = rowCount ?? indexed.rows.length;
794
2166
  pageCount = countPages(filteredCount, pageSize);
795
2167
  page = clampPage(state.pagination.page, pageCount);
796
2168
  } else {
797
2169
  const filtered = filterStage(indexed.rows, columns, state.search, state.filters);
798
- processedRows = sortStage(filtered, columns, state.sorting);
2170
+ processedRows = expandedOrder(sortStage(filtered, columns, state.sorting), expanded.byKey);
799
2171
  filteredCount = processedRows.length;
800
2172
  pageCount = countPages(filteredCount, pageSize);
801
2173
  page = clampPage(state.pagination.page, pageCount);
@@ -804,42 +2176,90 @@ function createTable(options) {
804
2176
  pageSize
805
2177
  });
806
2178
  }
2179
+ if (!paginate) {
2180
+ rows = processedRows;
2181
+ page = 1;
2182
+ pageCount = 1;
2183
+ pageSize = Math.max(1, rows.length);
2184
+ }
2185
+ const flatProjected = /* @__PURE__ */ new Map();
2186
+ if (!model) {
2187
+ const project = (row) => {
2188
+ let result = flatProjected.get(row.key);
2189
+ if (!result) {
2190
+ result = metadata(row);
2191
+ flatProjected.set(row.key, result);
2192
+ }
2193
+ return result;
2194
+ };
2195
+ const shared = rows === processedRows;
2196
+ processedRows = mapList(processedRows, project);
2197
+ rows = shared ? processedRows : rows.map(project);
2198
+ }
807
2199
  const selected = selectedSet(state.selection);
808
- const pageSelected = rows.filter((row) => selected.has(row.key)).length;
2200
+ const allData = model ? model.byKey : indexed.byKey;
2201
+ const allProcessedKeys = model ? void 0 : manual ? void 0 : membership(filterStage(indexed.rows, columns, state.search, state.filters));
2202
+ const selectedInProcessed = state.selection.filter((key) => allData.has(key) && (!allProcessedKeys || allProcessedKeys.has(key))).length;
2203
+ const pageSelected = rows === processedRows ? selectedInProcessed : rows.filter((row) => selected.has(row.key)).length;
809
2204
  const scopeRows = selectScope === "page" || manual ? rows : processedRows;
810
- const scopeSelected = scopeRows === rows ? pageSelected : scopeRows.filter((row) => selected.has(row.key)).length;
2205
+ const scopeSelected = scopeRows === rows ? pageSelected : model ? scopeRows.filter((row) => selected.has(row.key)).length : selectedInProcessed;
811
2206
  const pageStart = rows.length === 0 ? 0 : (page - 1) * pageSize + 1;
812
2207
  const sortIndex = new Map(state.sorting.map((rule, priority) => [rule.column, {
813
2208
  direction: rule.direction,
814
2209
  priority
815
2210
  }]));
816
2211
  return Object.freeze({
2212
+ tree: treeEnabled,
2213
+ loadState,
2214
+ loadedRowCount: indexed.rows.length,
2215
+ expectedRowCount,
817
2216
  state,
818
2217
  columns: ordered.visible,
819
2218
  allColumns: ordered.all,
820
2219
  rows,
2220
+ renderItems: !model && state.expanded !== true && state.expanded.length === 0 ? mapList(rows, (row, rowIndex) => Object.freeze({
2221
+ kind: "row",
2222
+ key: getRowItemKey(row.key),
2223
+ row,
2224
+ rowIndex
2225
+ })) : Object.freeze(rows.flatMap((row, rowIndex) => {
2226
+ const item = Object.freeze({
2227
+ kind: "row",
2228
+ key: getRowItemKey(row.key),
2229
+ row,
2230
+ rowIndex
2231
+ });
2232
+ return row.isExpanded && (!model || (options.getRowCanExpand?.(row.original) ?? false)) ? [item, Object.freeze({
2233
+ kind: "detail",
2234
+ key: getRowItemKey(row.key, "detail"),
2235
+ row,
2236
+ rowIndex
2237
+ })] : [item];
2238
+ })),
2239
+ paginate,
821
2240
  processedRows,
822
2241
  page,
823
2242
  pageSize,
824
2243
  pageCount,
825
2244
  rowCount: filteredCount,
826
- totalRowCount: manual ? filteredCount : indexed.rows.length,
2245
+ totalRowCount: manual ? filteredCount : totalCount,
827
2246
  pageStart,
828
- pageEnd: rows.length === 0 ? 0 : pageStart + rows.length - 1,
2247
+ pageEnd: rows.length === 0 ? 0 : rootPagination ? Math.min(filteredCount, page * pageSize) : pageStart + rows.length - 1,
829
2248
  pageItems: paginationItems(page, pageCount),
830
2249
  canPreviousPage: page > 1,
831
2250
  canNextPage: page < pageCount,
832
2251
  selectionMode,
833
- selectedCount: state.selection.length,
2252
+ selectedCount: model ? state.selection.filter((key) => model.byKey.has(key)).length : state.selection.length,
834
2253
  pageSelection: coverage(pageSelected, rows.length),
835
2254
  allSelection: coverage(scopeSelected, scopeRows.length),
836
2255
  canUndo: undoStack.canUndo,
837
2256
  canRedo: undoStack.canRedo,
838
2257
  issues: Object.freeze(issues),
2258
+ pendingCells: validation.cells(),
839
2259
  isSelected: (key) => selected.has(key),
840
2260
  getSort: (columnId) => sortIndex.get(columnId),
841
2261
  getColumn: (columnId) => byId.get(columnId),
842
- getRow: (key) => indexed.byKey.get(key)
2262
+ getRow: (key) => treeRow ? treeRow(key) : flatProjected.get(key) ?? (expanded.byKey.get(key) ? metadata(expanded.byKey.get(key)) : void 0)
843
2263
  });
844
2264
  }
845
2265
  function getSnapshot() {
@@ -848,18 +2268,140 @@ function createTable(options) {
848
2268
  }
849
2269
  function notify() {
850
2270
  snapshot = void 0;
2271
+ if (deferValidationNotify) {
2272
+ validationNotifyRequested = true;
2273
+ return;
2274
+ }
2275
+ if (transaction) return;
851
2276
  if (listeners.size === 0) return;
852
2277
  const next = getSnapshot();
853
2278
  for (const listener of listeners) listener(next);
854
2279
  }
2280
+ function abortLoads(all = false) {
2281
+ for (const [key, controller] of pendingLoads) {
2282
+ const row = all ? void 0 : getSnapshot().getRow(key);
2283
+ const visible = !all && getSnapshot().processedRows.some((candidate) => candidate.key === key);
2284
+ if (all || !row?.isExpanded || !visible) {
2285
+ pendingLoads.delete(key);
2286
+ childStatuses.delete(key);
2287
+ controller.abort();
2288
+ snapshot = void 0;
2289
+ }
2290
+ }
2291
+ }
2292
+ function requestChildren(row) {
2293
+ if (!options.loadChildren || destroyed || pendingLoads.has(row.key) || lazyChildren.has(row.key) || row.childStatus === "loaded") return;
2294
+ const key = row.key;
2295
+ if (!options.createChildLoadController) {
2296
+ childStatuses.set(key, "error");
2297
+ loadIssues.set(key, issue("invalid_tree_option", "Lazy children require createChildLoadController from the caller's runtime.", { rowKey: key }));
2298
+ notify();
2299
+ return;
2300
+ }
2301
+ let controller;
2302
+ try {
2303
+ controller = options.createChildLoadController();
2304
+ } catch (error) {
2305
+ loadIssues.set(key, issue("tree_load_error", String(error), { rowKey: key }));
2306
+ childStatuses.set(key, "error");
2307
+ notify();
2308
+ return;
2309
+ }
2310
+ pendingLoads.set(key, controller);
2311
+ childStatuses.set(key, "loading");
2312
+ loadIssues.delete(key);
2313
+ loadValidationIssues.delete(key);
2314
+ notify();
2315
+ Promise.resolve().then(() => controller.signal.aborted ? [] : options.loadChildren(row, controller.signal)).then((children) => {
2316
+ if (controller.signal.aborted || pendingLoads.get(key) !== controller || destroyed) return void 0;
2317
+ const { byId } = resolveColumns(definitions);
2318
+ const existing = treeStage(data, byId, lazyVersion);
2319
+ const candidate = buildTree(children, byId, readKey, {
2320
+ ...options,
2321
+ getParentKey: void 0
2322
+ }, /* @__PURE__ */ new Map());
2323
+ pendingLoads.delete(key);
2324
+ loadValidationIssues.set(key, candidate.issues.filter((problem) => problem.code === "tree_cycle" || problem.code === "tree_depth_exceeded"));
2325
+ if (candidate.nodes.filter((node) => existing.byKey.has(node.row.key)).length || candidate.issues.some((problem) => problem.code === "duplicate_row_key")) {
2326
+ childStatuses.set(key, "error");
2327
+ loadIssues.set(key, issue("tree_duplicate_key", "Loaded children collide with existing keys; the batch is rejected.", { rowKey: key }));
2328
+ } else {
2329
+ lazyChildren.set(key, children);
2330
+ childStatuses.set(key, "loaded");
2331
+ lazyVersion++;
2332
+ if (state.selection.includes(key) && selectionMode === "multiple") {
2333
+ const nextModel = treeStage(data, byId, lazyVersion);
2334
+ applySelection([...state.selection, ...selectionKeys([key], nextModel)]);
2335
+ }
2336
+ }
2337
+ notify();
2338
+ startVisibleLoads();
2339
+ }).catch((error) => {
2340
+ if (controller.signal.aborted || pendingLoads.get(key) !== controller || destroyed) return;
2341
+ pendingLoads.delete(key);
2342
+ childStatuses.set(key, "error");
2343
+ loadIssues.set(key, issue("tree_load_error", `Children could not be loaded: ${String(error)}`, { rowKey: key }));
2344
+ notify();
2345
+ });
2346
+ }
2347
+ function startVisibleLoads() {
2348
+ if (!treeEnabled || !options.loadChildren || destroyed) return;
2349
+ const current = getSnapshot();
2350
+ for (const row of current.rows) if (row.isExpanded && row.childStatus === "idle") requestChildren(row);
2351
+ }
2352
+ function selectionKeys(keys, model) {
2353
+ if (!treeEnabled || selectionMode !== "multiple") return [...keys];
2354
+ model ??= treeStage(data, resolveColumns(definitions).byId, lazyVersion);
2355
+ const result = new Set(keys);
2356
+ const stack = keys.flatMap((key) => {
2357
+ const node = model.byKey.get(key);
2358
+ return node ? [node] : [];
2359
+ });
2360
+ const visited = /* @__PURE__ */ new Set();
2361
+ while (stack.length) {
2362
+ const node = stack.pop();
2363
+ if (visited.has(node.row.key)) continue;
2364
+ visited.add(node.row.key);
2365
+ result.add(node.row.key);
2366
+ for (const child of node.children) stack.push(child);
2367
+ }
2368
+ return [...result];
2369
+ }
855
2370
  /** Apply a change made by a table operation. */
856
2371
  function commit(patch) {
857
2372
  const previous = state;
858
- const next = mergeState(state, patch);
859
- if (next === previous) return;
2373
+ const previousIssue = expandedIssue;
2374
+ const next = mergePatch(patch);
2375
+ if (next === previous && previousIssue === expandedIssue) return;
860
2376
  state = next;
861
- options.onStateChange?.(state, previous);
2377
+ snapshot = void 0;
2378
+ abortLoads();
2379
+ if (next !== previous) options.onStateChange?.(state, previous);
862
2380
  notify();
2381
+ startVisibleLoads();
2382
+ }
2383
+ function canonicalExpanded(expanded, candidate = state) {
2384
+ if (expandMode === "multiple") return expanded;
2385
+ if (expanded !== true) return expanded.slice(-1);
2386
+ const { columns, byId } = resolveColumns(definitions);
2387
+ if (treeEnabled) {
2388
+ const model = treeStage(data, byId, lazyVersion);
2389
+ const first = processedTreeStage(model, columns, candidate.search, candidate.filters, candidate.sorting).roots.find((node) => node.children.length > 0 || (options.hasChildren?.(node.row.original) ?? false) || (options.getRowCanExpand?.(node.row.original) ?? false));
2390
+ return first ? [first.row.key] : [];
2391
+ }
2392
+ const indexed = indexRows(data, byId);
2393
+ const first = (manual ? indexed.rows : sortStage(filterStage(indexed.rows, columns, candidate.search, candidate.filters), columns, candidate.sorting)).find((row) => row.canExpand);
2394
+ return first ? [first.key] : [];
2395
+ }
2396
+ function mergePatch(patch) {
2397
+ if (patch.expanded !== void 0) expandedIssue = void 0;
2398
+ const next = mergeState(state, patch, (problem) => {
2399
+ expandedIssue = problem;
2400
+ });
2401
+ return expandMode === "single" && patch.expanded !== void 0 ? mergeState(state, {
2402
+ ...patch,
2403
+ expanded: canonicalExpanded(next.expanded, next)
2404
+ }) : next;
863
2405
  }
864
2406
  const firstPage = () => ({
865
2407
  page: 1,
@@ -890,10 +2432,14 @@ function createTable(options) {
890
2432
  if (selection.length === current.length && selection.every((key, i) => key === current[i])) return;
891
2433
  commit({ selection });
892
2434
  }
893
- function applyChanges(edits, record) {
2435
+ function applyChanges(edits, record, recoveredIssues = []) {
894
2436
  const current = getSnapshot();
895
2437
  const working = [...data];
896
- const issues = [];
2438
+ const model = treeEnabled ? treeStage(data, resolveColumns(definitions).byId, lazyVersion) : void 0;
2439
+ const replacements = /* @__PURE__ */ new Map();
2440
+ const cache = new Map(lazyChildren);
2441
+ const originalOf = (node) => replacements.get(node) ?? node.row.original;
2442
+ const issues = [...recoveredIssues];
897
2443
  const changes = [];
898
2444
  const samples = /* @__PURE__ */ new Map();
899
2445
  const sampleOf = (columnId) => {
@@ -916,6 +2462,13 @@ function createTable(options) {
916
2462
  }));
917
2463
  continue;
918
2464
  }
2465
+ if (!isSafePath(edit.column)) {
2466
+ issues.push(issue("unsafe_path", `The path "${edit.column}" addresses a prototype and cannot be written.`, {
2467
+ rowKey: edit.rowKey,
2468
+ column: edit.column
2469
+ }));
2470
+ continue;
2471
+ }
919
2472
  if (!column) {
920
2473
  issues.push(issue("unknown_column", `No column has the id "${edit.column}".`, {
921
2474
  rowKey: edit.rowKey,
@@ -923,7 +2476,22 @@ function createTable(options) {
923
2476
  }));
924
2477
  continue;
925
2478
  }
926
- const original = working[row.index];
2479
+ const node = model?.byKey.get(row.key);
2480
+ const original = node ? originalOf(node) : working[row.index];
2481
+ if (node && !node.writable) {
2482
+ issues.push(issue("read_only_cell", "A recovered nested root has no unambiguous source path for editing.", {
2483
+ rowKey: row.key,
2484
+ column: column.id
2485
+ }));
2486
+ continue;
2487
+ }
2488
+ if (node?.parent && options.getChildren && !options.setChildren) {
2489
+ issues.push(issue("read_only_cell", "Editing a nested child requires an immutable setChildren callback.", {
2490
+ rowKey: row.key,
2491
+ column: column.id
2492
+ }));
2493
+ continue;
2494
+ }
927
2495
  if (record && !column.isEditable(original)) {
928
2496
  issues.push(issue("read_only_cell", `The "${column.id}" cell of row "${String(row.key)}" is read-only.`, {
929
2497
  rowKey: row.key,
@@ -945,7 +2513,16 @@ function createTable(options) {
945
2513
  } else value = edit.value;
946
2514
  const previous = column.getValue(original);
947
2515
  if (Object.is(previous, value)) continue;
948
- const updated = column.setValue(original, value);
2516
+ let updated;
2517
+ try {
2518
+ updated = column.setValue(original, value);
2519
+ } catch (error) {
2520
+ issues.push(issue("invalid_value", error instanceof Error ? error.message : String(error), {
2521
+ rowKey: row.key,
2522
+ column: column.id
2523
+ }));
2524
+ continue;
2525
+ }
949
2526
  if (updated === void 0) {
950
2527
  issues.push(issue("read_only_cell", `The "${column.id}" column has no way to write a value.`, {
951
2528
  rowKey: row.key,
@@ -953,7 +2530,38 @@ function createTable(options) {
953
2530
  }));
954
2531
  continue;
955
2532
  }
956
- working[row.index] = updated;
2533
+ if (readKey(updated, row.index) !== readKey(original, row.index)) {
2534
+ issues.push(issue("invalid_row_operation", "Editing a row key is not supported; remove and insert the row instead.", {
2535
+ rowKey: row.key,
2536
+ column: column.id
2537
+ }));
2538
+ continue;
2539
+ }
2540
+ if (!node) working[row.index] = updated;
2541
+ else {
2542
+ const planned = /* @__PURE__ */ new Map([[node, updated]]);
2543
+ const plannedCache = /* @__PURE__ */ new Map();
2544
+ let cursor = node;
2545
+ try {
2546
+ while (cursor.parent) {
2547
+ const parent = cursor.parent;
2548
+ const children = parent.children.map((child) => planned.get(child) ?? originalOf(child));
2549
+ if (cursor.lazy) plannedCache.set(parent.row.key, children);
2550
+ if (options.setChildren) planned.set(parent, options.setChildren(originalOf(parent), children));
2551
+ cursor = parent;
2552
+ }
2553
+ } catch (error) {
2554
+ issues.push(issue("invalid_value", error instanceof Error ? error.message : String(error), {
2555
+ rowKey: row.key,
2556
+ column: column.id
2557
+ }));
2558
+ continue;
2559
+ }
2560
+ for (const [item, plannedRow] of planned) replacements.set(item, plannedRow);
2561
+ for (const [key, children] of plannedCache) cache.set(key, children);
2562
+ if (!options.getChildren && !node.lazy) working[node.sourceIndex] = updated;
2563
+ if (options.getChildren || options.setChildren) working[cursor.sourceIndex] = originalOf(cursor);
2564
+ }
957
2565
  changes.push({
958
2566
  rowKey: row.key,
959
2567
  column: column.id,
@@ -967,17 +2575,390 @@ function createTable(options) {
967
2575
  else {
968
2576
  status = issues.length > 0 ? "partial" : "applied";
969
2577
  data = working;
970
- if (record) undoStack.record(changes);
971
- options.onDataChange?.(data, changes);
2578
+ if (model) {
2579
+ lazyChildren.clear();
2580
+ for (const [key, children] of cache) lazyChildren.set(key, children);
2581
+ lazyVersion++;
2582
+ }
2583
+ if (record) {
2584
+ if (!transaction) undoStack.record({
2585
+ kind: "cells",
2586
+ changes
2587
+ });
2588
+ }
2589
+ const groups = /* @__PURE__ */ new Map();
2590
+ for (const change of changes) {
2591
+ const group = groups.get(change.rowKey) ?? [];
2592
+ group.push(change);
2593
+ groups.set(change.rowKey, group);
2594
+ }
2595
+ for (const [key, group] of groups) pendingChanges.record(key, current.getRow(key).original, group.at(-1).row, positionOf(key), group);
2596
+ if (!transaction) options.onDataChange?.(data, changes);
972
2597
  notify();
973
2598
  }
974
- if (issues.length > 0) options.onEditIssues?.(issues);
2599
+ if (issues.length > 0 && !transaction) options.onEditIssues?.(issues);
975
2600
  return Object.freeze({
976
2601
  status,
977
2602
  changes: Object.freeze(changes),
978
- issues: Object.freeze(issues)
2603
+ issues: Object.freeze(issues),
2604
+ rowChanges: Object.freeze([]),
2605
+ pendingCells: Object.freeze([])
979
2606
  });
980
2607
  }
2608
+ function sourceRows() {
2609
+ const { byId } = resolveColumns(definitions);
2610
+ const model = treeEnabled ? treeStage(data, byId, lazyVersion) : void 0;
2611
+ return model ? treeIndex(model) : indexRows(data, byId);
2612
+ }
2613
+ function positionOf(key) {
2614
+ const { byId } = resolveColumns(definitions);
2615
+ if (treeEnabled) {
2616
+ const model = treeStage(data, byId, lazyVersion);
2617
+ return treePositions(model).get(key) ?? Object.freeze({});
2618
+ }
2619
+ const indexed = indexRows(data, byId);
2620
+ const row = indexed.byKey.get(key);
2621
+ return Object.freeze({ before: row ? indexed.rows[row.index + 1]?.key : void 0 });
2622
+ }
2623
+ function scene() {
2624
+ return {
2625
+ data,
2626
+ lazy: new Map(lazyChildren)
2627
+ };
2628
+ }
2629
+ function resultForRows(changes, issues, cells = []) {
2630
+ return Object.freeze({
2631
+ status: changes.length || cells.length ? issues.length ? "partial" : "applied" : issues.length ? "rejected" : "unchanged",
2632
+ changes: Object.freeze([...cells]),
2633
+ rowChanges: Object.freeze([...changes]),
2634
+ pendingCells: Object.freeze([]),
2635
+ issues: Object.freeze([...issues])
2636
+ });
2637
+ }
2638
+ function rowProblem(message, rowKey) {
2639
+ return issue("invalid_row_operation", message, { rowKey });
2640
+ }
2641
+ function stableRows(rows) {
2642
+ return options.rowKey !== void 0 || rows.every((row) => {
2643
+ const key = getPath(row, "id");
2644
+ return typeof key === "string" || typeof key === "number";
2645
+ });
2646
+ }
2647
+ function cellDifferences(previous, row, key) {
2648
+ return resolveColumns(definitions).columns.flatMap((column) => {
2649
+ const before = column.getValue(previous);
2650
+ const value = column.getValue(row);
2651
+ return Object.is(before, value) ? [] : [{
2652
+ rowKey: key,
2653
+ column: column.id,
2654
+ previous: before,
2655
+ value,
2656
+ row
2657
+ }];
2658
+ });
2659
+ }
2660
+ function recordRows(previous, keys, positions, baselines = /* @__PURE__ */ new Map()) {
2661
+ const next = sourceRows();
2662
+ const rowChanges = [];
2663
+ for (const key of keys) {
2664
+ const before = previous.byKey.get(key)?.original;
2665
+ const row = next.byKey.get(key)?.original;
2666
+ const position = positions.get(key) ?? positionOf(key);
2667
+ const cells = before !== void 0 && row !== void 0 ? cellDifferences(before, row, key) : [];
2668
+ const baseline = baselines.get(key);
2669
+ const pending = pendingChanges.entries.get(key);
2670
+ if (row === void 0 && baseline !== void 0 && pending?.previous !== void 0) pending.previous = baseline;
2671
+ pendingChanges.record(key, row === void 0 ? baseline ?? before : before, row, position, cells);
2672
+ if (before === void 0 && row !== void 0) rowChanges.push(Object.freeze({
2673
+ kind: "inserted",
2674
+ rowKey: key,
2675
+ row,
2676
+ position: positionOf(key)
2677
+ }));
2678
+ else if (row === void 0 && before !== void 0) rowChanges.push(Object.freeze({
2679
+ kind: "removed",
2680
+ rowKey: key,
2681
+ row: before,
2682
+ position
2683
+ }));
2684
+ }
2685
+ return rowChanges;
2686
+ }
2687
+ function publishRows(before, previous, keys, positions, issues, baselines = /* @__PURE__ */ new Map()) {
2688
+ const rowChanges = recordRows(previous, keys, positions, baselines);
2689
+ if (!transaction) {
2690
+ undoStack.record({
2691
+ kind: "rows",
2692
+ before,
2693
+ after: scene(),
2694
+ keys
2695
+ });
2696
+ options.onDataChange?.(data, []);
2697
+ if (issues.length) options.onEditIssues?.(issues);
2698
+ }
2699
+ notify();
2700
+ return resultForRows(rowChanges, issues);
2701
+ }
2702
+ function restoreScene(entry, direction) {
2703
+ const previous = sourceRows();
2704
+ const baselines = removalBaselines();
2705
+ const positions = new Map(entry.keys.map((key) => [key, positionOf(key)]));
2706
+ const target = direction === "undo" ? entry.before : entry.after;
2707
+ data = target.data;
2708
+ const changed = /* @__PURE__ */ new Set([...entry.before.lazy.keys(), ...entry.after.lazy.keys()]);
2709
+ for (const key of changed) {
2710
+ if (entry.before.lazy.get(key) === entry.after.lazy.get(key)) continue;
2711
+ const rows = target.lazy.get(key);
2712
+ if (rows) lazyChildren.set(key, rows);
2713
+ else lazyChildren.delete(key);
2714
+ }
2715
+ lazyVersion++;
2716
+ recordRows(previous, entry.keys, positions, baselines);
2717
+ options.onDataChange?.(data, []);
2718
+ notify();
2719
+ }
2720
+ function removalBaselines() {
2721
+ const baselines = /* @__PURE__ */ new Map();
2722
+ if (!options.getChildren || !options.setChildren) return baselines;
2723
+ const model = treeStage(data, resolveColumns(definitions).byId, lazyVersion);
2724
+ for (const node of model.nodes.toReversed()) {
2725
+ const entry = pendingChanges.entries.get(node.row.key);
2726
+ if (entry && entry.previous === void 0) continue;
2727
+ let baseline = entry?.previous ?? node.row.original;
2728
+ 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);
2729
+ const originalChildren = options.getChildren(baseline) ?? [];
2730
+ if (children.length !== originalChildren.length || children.some((child, index) => child !== originalChildren[index])) baseline = options.setChildren(baseline, children);
2731
+ baselines.set(node.row.key, baseline);
2732
+ }
2733
+ return baselines;
2734
+ }
2735
+ function rewriteNested(model, removed, overrides, cache) {
2736
+ const replacements = /* @__PURE__ */ new Map();
2737
+ for (const node of model.nodes.toReversed()) {
2738
+ if (removed.has(node.row.key)) continue;
2739
+ const children = overrides.get(node.row.key) ?? node.children.filter((child) => !removed.has(child.row.key)).map((child) => replacements.get(child) ?? child.row.original);
2740
+ if (!(overrides.has(node.row.key) || children.length !== node.children.length || children.some((child, index) => child !== node.children[index]?.row.original))) continue;
2741
+ if (!options.setChildren) throw new Error("Changing nested children requires setChildren.");
2742
+ replacements.set(node, options.setChildren(node.row.original, children));
2743
+ if (cache.has(node.row.key)) cache.set(node.row.key, children);
2744
+ }
2745
+ const roots = new Map(model.roots.filter((node) => node.writable).map((node) => [node.sourceIndex, node]));
2746
+ return data.flatMap((row, index) => {
2747
+ const node = roots.get(index);
2748
+ return node && removed.has(node.row.key) ? [] : [node ? replacements.get(node) ?? row : row];
2749
+ });
2750
+ }
2751
+ function insertRows(rows, at = {}) {
2752
+ const before = scene();
2753
+ const previous = sourceRows();
2754
+ const problems = [];
2755
+ const reject = (problem) => {
2756
+ if (!transaction) options.onEditIssues?.([problem]);
2757
+ return resultForRows([], [problem]);
2758
+ };
2759
+ let incoming;
2760
+ try {
2761
+ incoming = rows ?? (options.createRow ? [options.createRow()] : []);
2762
+ } catch (error) {
2763
+ return reject(rowProblem(error instanceof Error ? error.message : String(error)));
2764
+ }
2765
+ if (rows === void 0 && !options.createRow) return reject(rowProblem("insertRows() requires rows or a createRow callback."));
2766
+ if (!incoming.length) return resultForRows([], []);
2767
+ if (!stableRows(data) || !stableRows(incoming)) return reject(rowProblem("Row operations require stable row keys; supply rowKey or an id on every row."));
2768
+ const { byId } = resolveColumns(definitions);
2769
+ const model = treeEnabled ? treeStage(data, byId, lazyVersion) : void 0;
2770
+ const parent = at.parent === void 0 ? void 0 : model?.byKey.get(at.parent);
2771
+ const sibling = at.before === void 0 ? void 0 : previous.byKey.get(at.before);
2772
+ if (at.parent !== void 0 && !parent) return reject(issue("unknown_row", "The insertion parent does not exist.", { rowKey: at.parent }));
2773
+ if (at.before !== void 0 && !sibling) return reject(issue("unknown_row", "The insertion sibling does not exist.", { rowKey: at.before }));
2774
+ 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));
2775
+ if (parent && !parent.writable) return reject(rowProblem("A recovered parent has no writable source path.", parent.row.key));
2776
+ if (parent && getSnapshot().getRow(parent.row.key)?.childStatus !== "loaded") return reject(rowProblem("Load the parent's children before inserting rows.", parent.row.key));
2777
+ const cache = new Map(lazyChildren);
2778
+ let next;
2779
+ try {
2780
+ if (options.getChildren && parent && model) {
2781
+ const children = parent.children.map((child) => child.row.original);
2782
+ const index = at.before === void 0 ? children.length : parent.children.findIndex((child) => child.row.key === at.before);
2783
+ children.splice(index, 0, ...incoming);
2784
+ next = rewriteNested(model, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Map([[parent.row.key, children]]), cache);
2785
+ } else if (parent && cache.has(parent.row.key)) {
2786
+ incoming = incoming.map((row) => options.setParentKey ? options.setParentKey(row, parent.row.key) : row);
2787
+ const children = [...cache.get(parent.row.key)];
2788
+ const index = at.before === void 0 ? children.length : parent.children.findIndex((child) => child.row.key === at.before);
2789
+ children.splice(index, 0, ...incoming);
2790
+ cache.set(parent.row.key, children);
2791
+ next = [...data];
2792
+ } else {
2793
+ if (parent && options.getParentKey) {
2794
+ incoming = incoming.map((row) => options.setParentKey ? options.setParentKey(row, parent.row.key) : row);
2795
+ if (incoming.some((row) => options.getParentKey(row) !== parent.row.key)) return reject(rowProblem("Supply setParentKey or rows already addressed to the requested parent."));
2796
+ }
2797
+ const working = [...data];
2798
+ working.splice(sibling?.index ?? working.length, 0, ...incoming);
2799
+ next = working;
2800
+ }
2801
+ const candidate = treeEnabled ? buildTree(next, byId, readKey, options, cache) : indexRows(next, byId);
2802
+ if (!stableRows(candidate.rows.map((row) => row.original))) return reject(rowProblem("Every inserted descendant must have a stable key."));
2803
+ const baseline = /* @__PURE__ */ new Map();
2804
+ for (const problem of previous.issues) {
2805
+ const key = `${problem.code}:${String(problem.rowKey)}`;
2806
+ baseline.set(key, (baseline.get(key) ?? 0) + 1);
2807
+ }
2808
+ problems.push(...candidate.issues.filter((problem) => {
2809
+ const key = `${problem.code}:${String(problem.rowKey)}`;
2810
+ const remaining = baseline.get(key) ?? 0;
2811
+ if (!remaining) return true;
2812
+ baseline.set(key, remaining - 1);
2813
+ return false;
2814
+ }));
2815
+ if (problems.length) {
2816
+ if (!transaction) options.onEditIssues?.(problems);
2817
+ return resultForRows([], problems);
2818
+ }
2819
+ const keys = candidate.rows.filter((row) => !previous.byKey.has(row.key)).map((row) => row.key);
2820
+ if (!keys.length) return reject(rowProblem("No unique rows can be inserted."));
2821
+ data = next;
2822
+ lazyChildren.clear();
2823
+ for (const [key, value] of cache) lazyChildren.set(key, value);
2824
+ lazyVersion++;
2825
+ return publishRows(before, previous, keys, /* @__PURE__ */ new Map(), []);
2826
+ } catch (error) {
2827
+ return reject(rowProblem(error instanceof Error ? error.message : String(error)));
2828
+ }
2829
+ }
2830
+ function removeRows(keys) {
2831
+ const before = scene();
2832
+ const previous = sourceRows();
2833
+ const problems = [];
2834
+ const removed = /* @__PURE__ */ new Set();
2835
+ const { byId } = resolveColumns(definitions);
2836
+ const model = treeEnabled ? treeStage(data, byId, lazyVersion) : void 0;
2837
+ if (!stableRows(data)) {
2838
+ const problem = rowProblem("Row operations require stable row keys.");
2839
+ if (!transaction) options.onEditIssues?.([problem]);
2840
+ return resultForRows([], [problem]);
2841
+ }
2842
+ for (const key of keys) {
2843
+ if (!previous.byKey.has(key)) {
2844
+ problems.push(issue("unknown_row", "The row to remove does not exist.", { rowKey: key }));
2845
+ continue;
2846
+ }
2847
+ const node = model?.byKey.get(key);
2848
+ if (node && !node.writable) {
2849
+ problems.push(rowProblem("A recovered row has no writable source path.", key));
2850
+ continue;
2851
+ }
2852
+ const stack = node ? [node] : [];
2853
+ removed.add(key);
2854
+ while (stack.length) {
2855
+ const item = stack.pop();
2856
+ removed.add(item.row.key);
2857
+ for (const child of item.children) stack.push(child);
2858
+ }
2859
+ }
2860
+ if (!removed.size) {
2861
+ if (problems.length && !transaction) options.onEditIssues?.(problems);
2862
+ return resultForRows([], problems);
2863
+ }
2864
+ const positions = new Map([...removed].map((key) => [key, positionOf(key)]));
2865
+ const cache = new Map(lazyChildren);
2866
+ try {
2867
+ const baselines = removalBaselines();
2868
+ const next = options.getChildren && model ? rewriteNested(model, removed, /* @__PURE__ */ new Map(), cache) : data.filter((row, index) => !removed.has(readKey(row, index)));
2869
+ for (const [key, children] of cache) if (removed.has(key)) cache.delete(key);
2870
+ else cache.set(key, children.filter((row, index) => !removed.has(readKey(row, index))));
2871
+ data = next;
2872
+ lazyChildren.clear();
2873
+ for (const [key, value] of cache) lazyChildren.set(key, value);
2874
+ lazyVersion++;
2875
+ validation.cancel([...removed]);
2876
+ abortLoads();
2877
+ return publishRows(before, previous, [...removed], positions, problems, baselines);
2878
+ } catch (error) {
2879
+ problems.push(rowProblem(error instanceof Error ? error.message : String(error)));
2880
+ if (!transaction) options.onEditIssues?.(problems);
2881
+ return resultForRows([], problems);
2882
+ }
2883
+ }
2884
+ function revert(keys) {
2885
+ const selected = new Set(keys ?? pendingChanges.entries.keys());
2886
+ const entries = [...pendingChanges.entries].filter(([key]) => selected.has(key)).map(([key, entry]) => [key, {
2887
+ ...entry,
2888
+ cells: new Map(entry.cells)
2889
+ }]);
2890
+ if (!entries.length) return resultForRows([], []);
2891
+ const before = scene();
2892
+ const previous = sourceRows();
2893
+ const positions = new Map([...selected].map((key) => [key, positionOf(key)]));
2894
+ const problems = [];
2895
+ const cells = [];
2896
+ transaction = true;
2897
+ try {
2898
+ problems.push(...removeRows(entries.filter(([, entry]) => entry.previous === void 0).map(([key]) => key)).issues);
2899
+ const missing = entries.filter(([, entry]) => entry.previous !== void 0 && entry.row === void 0);
2900
+ for (let pass = 0; missing.length && pass <= entries.length; pass++) {
2901
+ let progress = false;
2902
+ for (let index = missing.length - 1; index >= 0; index--) {
2903
+ const [key, entry] = missing[index];
2904
+ if (sourceRows().byKey.has(key)) {
2905
+ missing.splice(index, 1);
2906
+ progress = true;
2907
+ continue;
2908
+ }
2909
+ if (entry.position.parent !== void 0 && !sourceRows().byKey.has(entry.position.parent)) continue;
2910
+ const at = {
2911
+ ...entry.position,
2912
+ before: entry.position.before !== void 0 && sourceRows().byKey.has(entry.position.before) ? entry.position.before : void 0
2913
+ };
2914
+ const result = insertRows([entry.previous], at);
2915
+ problems.push(...result.issues);
2916
+ missing.splice(index, 1);
2917
+ progress = true;
2918
+ }
2919
+ if (!progress) break;
2920
+ }
2921
+ for (const [key] of missing) problems.push(rowProblem("The saved parent is missing; restore it before this child.", key));
2922
+ const result = applyChanges(entries.flatMap(([key, entry]) => entry.previous !== void 0 && sourceRows().byKey.has(key) ? [...entry.cells.values()].map((change) => ({
2923
+ rowKey: key,
2924
+ column: change.column,
2925
+ value: change.previous
2926
+ })) : []), false);
2927
+ cells.push(...result.changes);
2928
+ problems.push(...result.issues);
2929
+ for (const [key] of entries) if (!problems.some((problem) => problem.rowKey === key)) pendingChanges.entries.delete(key);
2930
+ validation.cancel([...selected]);
2931
+ } finally {
2932
+ transaction = false;
2933
+ }
2934
+ const after = scene();
2935
+ if (before.data !== after.data || cells.length) undoStack.record({
2936
+ kind: "rows",
2937
+ before,
2938
+ after,
2939
+ keys: [...selected]
2940
+ });
2941
+ const next = sourceRows();
2942
+ const rowChanges = [...selected].flatMap((key) => {
2943
+ const old = previous.byKey.get(key);
2944
+ const row = next.byKey.get(key);
2945
+ return !old && row ? [{
2946
+ kind: "inserted",
2947
+ rowKey: key,
2948
+ row: row.original,
2949
+ position: positionOf(key)
2950
+ }] : old && !row ? [{
2951
+ kind: "removed",
2952
+ rowKey: key,
2953
+ row: old.original,
2954
+ position: positions.get(key)
2955
+ }] : [];
2956
+ });
2957
+ if (before.data !== after.data) options.onDataChange?.(data, cells);
2958
+ if (problems.length) options.onEditIssues?.(problems);
2959
+ notify();
2960
+ return resultForRows(rowChanges, problems, cells);
2961
+ }
981
2962
  function rangeEdits(origin, matrix) {
982
2963
  const { rows, columns } = getSnapshot();
983
2964
  const edits = [];
@@ -996,13 +2977,356 @@ function createTable(options) {
996
2977
  return edits;
997
2978
  }
998
2979
  function replay(changes, direction) {
999
- applyChanges(changes.map((change) => ({
2980
+ applyChanges((direction === "undo" ? changes.toReversed() : changes).map((change) => ({
1000
2981
  rowKey: change.rowKey,
1001
2982
  column: change.column,
1002
2983
  value: direction === "undo" ? change.previous : change.value
1003
2984
  })), false);
1004
2985
  }
2986
+ function ingestionResult(appended, replaced, removed, problems) {
2987
+ return Object.freeze({
2988
+ status: appended + replaced + removed ? problems.length ? "partial" : "applied" : problems.length ? "rejected" : "unchanged",
2989
+ appended,
2990
+ replaced,
2991
+ removed,
2992
+ issues: Object.freeze([...problems])
2993
+ });
2994
+ }
2995
+ const stableSource = /* @__PURE__ */ new WeakMap();
2996
+ function ingestionKeys(rows) {
2997
+ let valid = stableSource.get(rows);
2998
+ if (valid === void 0) {
2999
+ valid = stableRows(rows);
3000
+ stableSource.set(rows, valid);
3001
+ }
3002
+ return valid;
3003
+ }
3004
+ function isProtectedSource(key) {
3005
+ return pendingChanges.entries.has(key) || validation.hasPending(key);
3006
+ }
3007
+ function affectedBranch(model, key) {
3008
+ const result = [key];
3009
+ const node = model?.byKey.get(key);
3010
+ if (!node) return result;
3011
+ let parent = node.parent;
3012
+ while (parent) {
3013
+ result.push(parent.row.key);
3014
+ parent = parent.parent;
3015
+ }
3016
+ const stack = [...node.children];
3017
+ while (stack.length) {
3018
+ const child = stack.pop();
3019
+ result.push(child.row.key);
3020
+ for (const descendant of child.children) stack.push(descendant);
3021
+ }
3022
+ return result;
3023
+ }
3024
+ function transformScene(source, updates, removed) {
3025
+ const seen = /* @__PURE__ */ new Set();
3026
+ const replace = (rows) => rows.flatMap((row, index) => {
3027
+ const key = readKey(row, index);
3028
+ seen.add(key);
3029
+ return removed.has(key) ? [] : [updates.get(key) ?? row];
3030
+ });
3031
+ const cache = /* @__PURE__ */ new Map();
3032
+ for (const [key, children] of source.lazy) if (!removed.has(key)) cache.set(key, replace(children));
3033
+ let next;
3034
+ if (!options.getChildren) next = replace(source.data);
3035
+ else {
3036
+ const model = buildTree(source.data, resolveColumns(definitions).byId, readKey, options, source.lazy);
3037
+ const replacements = /* @__PURE__ */ new Map();
3038
+ for (const node of model.nodes.toReversed()) {
3039
+ const key = node.row.key;
3040
+ seen.add(key);
3041
+ if (removed.has(key)) continue;
3042
+ let row = updates.get(key) ?? node.row.original;
3043
+ const supplied = updates.has(key) ? options.getChildren(row) : void 0;
3044
+ if (supplied !== void 0) cache.delete(key);
3045
+ const children = supplied ?? node.children.filter((child) => !removed.has(child.row.key)).map((child) => replacements.get(child) ?? child.row.original);
3046
+ const old = options.getChildren(row) ?? node.children.map((child) => child.row.original);
3047
+ 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]))) {
3048
+ if (!options.setChildren) throw new Error("Updating nested source children requires setChildren.");
3049
+ row = options.setChildren(row, children);
3050
+ if (cache.has(key)) cache.set(key, children);
3051
+ }
3052
+ replacements.set(node, row);
3053
+ }
3054
+ const roots = new Map(model.roots.filter((node) => node.writable).map((node) => [node.sourceIndex, node]));
3055
+ next = source.data.flatMap((row, index) => {
3056
+ const node = roots.get(index);
3057
+ return node && removed.has(node.row.key) ? [] : [node ? replacements.get(node) ?? row : row];
3058
+ });
3059
+ }
3060
+ const additions = [...updates].filter(([key]) => !seen.has(key) && !removed.has(key)).map(([, row]) => row);
3061
+ return {
3062
+ data: appendList(next, additions),
3063
+ lazy: cache
3064
+ };
3065
+ }
3066
+ function rebaseBatches(transform, removed) {
3067
+ undoStack.transform((entry) => {
3068
+ if (entry.kind === "cells") {
3069
+ if (!removed.size) return entry;
3070
+ const changes = entry.changes.filter((change) => !removed.has(change.rowKey));
3071
+ return changes.length ? {
3072
+ ...entry,
3073
+ changes
3074
+ } : void 0;
3075
+ }
3076
+ const keys = entry.keys.filter((key) => !removed.has(key));
3077
+ return keys.length ? {
3078
+ ...entry,
3079
+ keys,
3080
+ before: transform(entry.before),
3081
+ after: transform(entry.after)
3082
+ } : void 0;
3083
+ });
3084
+ }
3085
+ function newTreeIssues(previous, next) {
3086
+ const counts = /* @__PURE__ */ new Map();
3087
+ for (const problem of previous) {
3088
+ const key = JSON.stringify([problem.code, problem.rowKey]);
3089
+ counts.set(key, (counts.get(key) ?? 0) + 1);
3090
+ }
3091
+ return next.filter((problem) => {
3092
+ const key = JSON.stringify([problem.code, problem.rowKey]);
3093
+ const remaining = counts.get(key) ?? 0;
3094
+ if (!remaining) return true;
3095
+ counts.set(key, remaining - 1);
3096
+ return false;
3097
+ });
3098
+ }
3099
+ function ingestData(incoming, mode) {
3100
+ const problems = [];
3101
+ const reject = (problem) => {
3102
+ ingestionIssues = [problem];
3103
+ notify();
3104
+ return ingestionResult(0, 0, 0, ingestionIssues);
3105
+ };
3106
+ if (destroyed) return reject(issue("invalid_ingestion", "A disposed table cannot receive source data."));
3107
+ if (!incoming.length) return ingestionResult(0, 0, 0, []);
3108
+ try {
3109
+ if (!ingestionKeys(data) || !stableRows(incoming)) return reject(issue("invalid_ingestion", "Source ingestion requires stable rowKey values or ids on every row."));
3110
+ } catch (error) {
3111
+ return reject(issue("invalid_ingestion", error instanceof Error ? error.message : String(error)));
3112
+ }
3113
+ const previous = sourceRows();
3114
+ const { byId, columns } = resolveColumns(definitions);
3115
+ const model = treeEnabled ? treeStage(data, byId, lazyVersion) : void 0;
3116
+ const seenBatch = /* @__PURE__ */ new Set();
3117
+ const updates = /* @__PURE__ */ new Map();
3118
+ for (const row of incoming) try {
3119
+ const key = readKey(row, previous.rows.length + updates.size);
3120
+ if (typeof key !== "string" && (typeof key !== "number" || !Number.isFinite(key))) {
3121
+ problems.push(issue("invalid_ingestion", "An incoming row key must be a string or finite number."));
3122
+ continue;
3123
+ }
3124
+ const existing = previous.byKey.get(key);
3125
+ if (seenBatch.has(key) || mode === "append" && existing) {
3126
+ problems.push(issue("duplicate_row_key", "An incoming source row repeats an existing key and is refused.", { rowKey: key }));
3127
+ continue;
3128
+ }
3129
+ seenBatch.add(key);
3130
+ const parent = options.getParentKey?.(row);
3131
+ const affected = affectedBranch(model, key);
3132
+ if (parent !== void 0 && parent !== null) affected.push(...affectedBranch(model, parent));
3133
+ if (affected.some(isProtectedSource)) {
3134
+ problems.push(issue("ingestion_conflict", "Incoming source data would overwrite a local change or pending validation; retry after save or revert.", { rowKey: key }));
3135
+ continue;
3136
+ }
3137
+ const node = model?.byKey.get(key);
3138
+ if (node && (!node.writable || node.lazy && options.getParentKey && options.getParentKey(row) !== node.parent?.row.key)) {
3139
+ problems.push(issue("invalid_ingestion", "This recovered or lazy tree row has no writable source position.", { rowKey: key }));
3140
+ continue;
3141
+ }
3142
+ if (existing?.original !== row) updates.set(key, row);
3143
+ } catch (error) {
3144
+ problems.push(issue("invalid_ingestion", error instanceof Error ? error.message : String(error)));
3145
+ }
3146
+ if (!updates.size) {
3147
+ ingestionIssues = problems;
3148
+ notify();
3149
+ return ingestionResult(0, 0, 0, problems);
3150
+ }
3151
+ const replaced = [...updates.keys()].filter((key) => previous.byKey.has(key)).length;
3152
+ const appended = updates.size - replaced;
3153
+ const previousState = state;
3154
+ try {
3155
+ if (!model && !replaced) {
3156
+ const added = Object.freeze([...updates.values()]);
3157
+ const indexed = added.map((row, offset) => createRow(row, readKey(row, previous.rows.length + offset), previous.rows.length + offset, byId, options.getRowCanExpand?.(row) ?? false));
3158
+ const filtered = manual ? previous.rows : filterStage(previous.rows, columns, state.search, state.filters);
3159
+ const addedFiltered = manual ? indexed : filterRows(indexed, columns, state.search, state.filters);
3160
+ const allRows = appendList(previous.rows, indexed);
3161
+ const allFiltered = appendList(filtered, addedFiltered);
3162
+ const keys = membership(filtered);
3163
+ for (const row of addedFiltered) keys.add(row.key);
3164
+ rowMembership.set(allFiltered, keys);
3165
+ 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);
3166
+ const next = appendList(data, added);
3167
+ rebaseBatches((source) => ({
3168
+ ...source,
3169
+ data: appendList(source.data, added)
3170
+ }), /* @__PURE__ */ new Set());
3171
+ for (const row of indexed) previous.byKey.set(row.key, row);
3172
+ primedIndex = {
3173
+ data: next,
3174
+ columns: byId,
3175
+ result: {
3176
+ rows: allRows,
3177
+ byKey: previous.byKey,
3178
+ issues: previous.issues
3179
+ }
3180
+ };
3181
+ primedFilter = {
3182
+ rows: allRows,
3183
+ columns,
3184
+ search: state.search,
3185
+ filters: state.filters,
3186
+ result: allFiltered
3187
+ };
3188
+ primedSort = {
3189
+ rows: allFiltered,
3190
+ columns,
3191
+ sorting: state.sorting,
3192
+ result: sorted
3193
+ };
3194
+ stableSource.set(next, true);
3195
+ data = next;
3196
+ } else {
3197
+ const next = transformScene(scene(), updates, /* @__PURE__ */ new Set());
3198
+ const candidate = treeEnabled ? buildTree(next.data, byId, readKey, options, next.lazy) : indexRows(next.data, byId);
3199
+ const fresh = newTreeIssues(previous.issues, candidate.issues);
3200
+ if (fresh.length || !stableRows(candidate.rows.map((row) => row.original))) {
3201
+ problems.push(...fresh, ...!stableRows(candidate.rows.map((row) => row.original)) ? [issue("invalid_ingestion", "Every incoming descendant needs a stable key.")] : []);
3202
+ ingestionIssues = problems;
3203
+ notify();
3204
+ return ingestionResult(0, 0, 0, problems);
3205
+ }
3206
+ const nextKeys = new Set(candidate.rows.map((row) => row.key));
3207
+ const removedKeys = new Set(previous.rows.filter((row) => !nextKeys.has(row.key)).map((row) => row.key));
3208
+ rebaseBatches((source) => transformScene(source, updates, removedKeys), removedKeys);
3209
+ data = next.data;
3210
+ lazyChildren.clear();
3211
+ for (const [key, rows] of next.lazy) lazyChildren.set(key, rows);
3212
+ lazyVersion++;
3213
+ for (const key of [...updates.keys(), ...removedKeys]) {
3214
+ pendingLoads.get(key)?.abort();
3215
+ pendingLoads.delete(key);
3216
+ childStatuses.delete(key);
3217
+ loadIssues.delete(key);
3218
+ loadValidationIssues.delete(key);
3219
+ }
3220
+ validation.cancel([...removedKeys]);
3221
+ const selection = state.selection.filter((key) => !removedKeys.has(key));
3222
+ state = mergeState(state, {
3223
+ selection: selection.length === state.selection.length ? state.selection : selection,
3224
+ expanded: state.expanded === true ? true : state.expanded.filter((key) => !removedKeys.has(key))
3225
+ });
3226
+ }
3227
+ validation.cancel([...updates.keys()]);
3228
+ ingestionIssues = problems;
3229
+ } catch (error) {
3230
+ return reject(issue("invalid_ingestion", error instanceof Error ? error.message : String(error)));
3231
+ }
3232
+ if (state !== previousState) options.onStateChange?.(state, previousState);
3233
+ options.onDataChange?.(data, []);
3234
+ notify();
3235
+ startVisibleLoads();
3236
+ return ingestionResult(appended, replaced, 0, problems);
3237
+ }
3238
+ function removeData(keys) {
3239
+ const previousState = state;
3240
+ const problems = [];
3241
+ const removed = /* @__PURE__ */ new Set();
3242
+ const previous = sourceRows();
3243
+ const model = treeEnabled ? treeStage(data, resolveColumns(definitions).byId, lazyVersion) : void 0;
3244
+ if (destroyed || !ingestionKeys(data)) problems.push(issue("invalid_ingestion", "Source removal requires a live table and stable row keys."));
3245
+ else for (const key of new Set(keys)) {
3246
+ if (!previous.byKey.has(key)) {
3247
+ problems.push(issue("unknown_row", "The source row to remove does not exist.", { rowKey: key }));
3248
+ continue;
3249
+ }
3250
+ if (affectedBranch(model, key).some(isProtectedSource)) {
3251
+ problems.push(issue("ingestion_conflict", "Source removal would discard local changes or pending validation.", { rowKey: key }));
3252
+ continue;
3253
+ }
3254
+ const node = model?.byKey.get(key);
3255
+ if (node && !node.writable) {
3256
+ problems.push(issue("invalid_ingestion", "A recovered tree row has no writable source position.", { rowKey: key }));
3257
+ continue;
3258
+ }
3259
+ removed.add(key);
3260
+ const stack = node ? [...node.children] : [];
3261
+ while (stack.length) {
3262
+ const child = stack.pop();
3263
+ removed.add(child.row.key);
3264
+ for (const descendant of child.children) stack.push(descendant);
3265
+ }
3266
+ }
3267
+ try {
3268
+ if (removed.size) {
3269
+ const next = transformScene(scene(), /* @__PURE__ */ new Map(), removed);
3270
+ rebaseBatches((source) => transformScene(source, /* @__PURE__ */ new Map(), removed), removed);
3271
+ data = next.data;
3272
+ lazyChildren.clear();
3273
+ for (const [key, rows] of next.lazy) lazyChildren.set(key, rows);
3274
+ lazyVersion++;
3275
+ validation.cancel([...removed]);
3276
+ for (const key of removed) {
3277
+ pendingLoads.get(key)?.abort();
3278
+ pendingLoads.delete(key);
3279
+ childStatuses.delete(key);
3280
+ loadIssues.delete(key);
3281
+ loadValidationIssues.delete(key);
3282
+ }
3283
+ const selection = state.selection.filter((key) => !removed.has(key));
3284
+ state = mergeState(state, {
3285
+ selection: selection.length === state.selection.length ? state.selection : selection,
3286
+ expanded: state.expanded === true ? true : state.expanded.filter((key) => !removed.has(key))
3287
+ });
3288
+ }
3289
+ ingestionIssues = problems;
3290
+ } catch (error) {
3291
+ ingestionIssues = [issue("invalid_ingestion", error instanceof Error ? error.message : String(error))];
3292
+ notify();
3293
+ return ingestionResult(0, 0, 0, ingestionIssues);
3294
+ }
3295
+ if (state !== previousState) options.onStateChange?.(state, previousState);
3296
+ if (removed.size) options.onDataChange?.(data, []);
3297
+ notify();
3298
+ startVisibleLoads();
3299
+ return ingestionResult(0, 0, removed.size, problems);
3300
+ }
3301
+ if (expandMode === "single") {
3302
+ initialState = mergeState(initialState, { expanded: canonicalExpanded(initialState.expanded) });
3303
+ state = initialState;
3304
+ snapshot = void 0;
3305
+ }
1005
3306
  const table = {
3307
+ appendData: (rows) => ingestData(rows, "append"),
3308
+ upsertData: (rows) => ingestData(rows, "upsert"),
3309
+ removeData,
3310
+ stream(source, streamOptions) {
3311
+ if (destroyed) return Promise.resolve(Object.freeze({
3312
+ status: "aborted",
3313
+ receivedRowCount: 0,
3314
+ issues: Object.freeze([issue("invalid_ingestion", "A disposed table cannot start a source.")])
3315
+ }));
3316
+ return streaming.run(source, {
3317
+ expectedRowCount: sourceExpected,
3318
+ ...streamOptions
3319
+ });
3320
+ },
3321
+ insertRows,
3322
+ removeRows,
3323
+ revert,
3324
+ getPendingChanges: () => pendingChanges.snapshot(),
3325
+ markSaved(keys) {
3326
+ if (!keys) pendingChanges.entries.clear();
3327
+ else for (const key of keys) pendingChanges.entries.delete(key);
3328
+ notify();
3329
+ },
1006
3330
  getSnapshot,
1007
3331
  getState: () => state,
1008
3332
  subscribe(listener) {
@@ -1012,21 +3336,40 @@ function createTable(options) {
1012
3336
  };
1013
3337
  },
1014
3338
  setState(patch) {
1015
- const next = mergeState(state, patch);
1016
- if (next !== state) {
3339
+ const previousIssue = expandedIssue;
3340
+ const next = mergePatch(patch);
3341
+ if (next !== state || previousIssue !== expandedIssue) {
1017
3342
  state = next;
3343
+ snapshot = void 0;
3344
+ abortLoads();
1018
3345
  notify();
3346
+ startVisibleLoads();
1019
3347
  }
1020
3348
  },
1021
3349
  setData(next) {
1022
3350
  if (next === data) return;
3351
+ validation.cancel();
3352
+ streaming.cancel();
3353
+ loadState = "idle";
3354
+ streamIssues = [];
3355
+ ingestionIssues = [];
3356
+ expectedRowCount = sourceExpected;
3357
+ pendingChanges.entries.clear();
3358
+ abortLoads(true);
3359
+ lazyChildren.clear();
3360
+ childStatuses.clear();
3361
+ loadIssues.clear();
3362
+ loadValidationIssues.clear();
3363
+ lazyVersion++;
1023
3364
  data = next;
1024
3365
  undoStack.clear();
1025
3366
  notify();
3367
+ startVisibleLoads();
1026
3368
  },
1027
3369
  setColumns(next) {
1028
3370
  if (next === definitions) return;
1029
3371
  definitions = next;
3372
+ validation.cancel();
1030
3373
  notify();
1031
3374
  },
1032
3375
  setRowCount(next) {
@@ -1069,6 +3412,7 @@ function createTable(options) {
1069
3412
  });
1070
3413
  },
1071
3414
  goToPage(page) {
3415
+ if (!paginate) return;
1072
3416
  const { pageCount, page: currentPage } = getSnapshot();
1073
3417
  const next = clampPage(page, pageCount);
1074
3418
  if (next !== currentPage || next !== state.pagination.page) commit({ pagination: {
@@ -1083,6 +3427,7 @@ function createTable(options) {
1083
3427
  table.goToPage(getSnapshot().page - 1);
1084
3428
  },
1085
3429
  setPageSize(pageSize) {
3430
+ if (!paginate) return;
1086
3431
  if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize === state.pagination.pageSize) return;
1087
3432
  const { pageStart } = getSnapshot();
1088
3433
  commit({ pagination: {
@@ -1091,14 +3436,14 @@ function createTable(options) {
1091
3436
  } });
1092
3437
  },
1093
3438
  select(keys) {
1094
- applySelection([...state.selection, ...keys]);
3439
+ applySelection([...state.selection, ...selectionKeys(keys)]);
1095
3440
  },
1096
3441
  deselect(keys) {
1097
- const removed = new Set(keys);
3442
+ const removed = new Set(selectionKeys(keys));
1098
3443
  applySelection(state.selection.filter((key) => !removed.has(key)));
1099
3444
  },
1100
3445
  toggleRow(key, selected) {
1101
- const isSelected = state.selection.includes(key);
3446
+ const isSelected = treeEnabled ? getSnapshot().getRow(key)?.selection === "all" : state.selection.includes(key);
1102
3447
  const next = selected ?? !isSelected;
1103
3448
  if (next === isSelected) return;
1104
3449
  if (next) table.select([key]);
@@ -1121,6 +3466,27 @@ function createTable(options) {
1121
3466
  return row ? [row] : [];
1122
3467
  });
1123
3468
  },
3469
+ toggleExpanded(key, expanded) {
3470
+ const row = getSnapshot().getRow(key);
3471
+ if (!row?.canExpand) return;
3472
+ const open = expanded ?? !row.isExpanded;
3473
+ if (open === row.isExpanded) {
3474
+ if (open && row.childStatus === "error") requestChildren(row);
3475
+ return;
3476
+ }
3477
+ if (expandMode === "single") commit({ expanded: open ? [key] : [] });
3478
+ else if (state.expanded === true) {
3479
+ const { byId } = resolveColumns(definitions);
3480
+ 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) });
3481
+ } else commit({ expanded: open ? [...state.expanded, key] : state.expanded.filter((candidate) => candidate !== key) });
3482
+ if (open) requestChildren(getSnapshot().getRow(key));
3483
+ },
3484
+ expandAll() {
3485
+ commit({ expanded: true });
3486
+ },
3487
+ collapseAll() {
3488
+ commit({ expanded: [] });
3489
+ },
1124
3490
  toggleColumn(columnId, visible) {
1125
3491
  if (!getSnapshot().getColumn(columnId)) return;
1126
3492
  const hidden = state.hiddenColumns.includes(columnId);
@@ -1138,22 +3504,26 @@ function createTable(options) {
1138
3504
  order.splice(target, 0, columnId);
1139
3505
  commit({ columnOrder: order });
1140
3506
  },
1141
- edit(edits) {
1142
- return applyChanges(Array.isArray(edits) ? edits : [edits], true);
3507
+ edit(edits, editOptions) {
3508
+ return validation.edit(Array.isArray(edits) ? edits : [edits], [], editOptions);
1143
3509
  },
1144
3510
  undo() {
1145
- const changes = undoStack.undo();
1146
- if (!changes) return false;
1147
- replay(changes, "undo");
3511
+ const entry = undoStack.undo();
3512
+ if (!entry) return false;
3513
+ validation.cancel();
3514
+ if (entry.kind === "cells") replay(entry.changes, "undo");
3515
+ else restoreScene(entry, "undo");
1148
3516
  return true;
1149
3517
  },
1150
3518
  redo() {
1151
- const changes = undoStack.redo();
1152
- if (!changes) return false;
1153
- replay(changes, "redo");
3519
+ const entry = undoStack.redo();
3520
+ if (!entry) return false;
3521
+ validation.cancel();
3522
+ if (entry.kind === "cells") replay(entry.changes, "redo");
3523
+ else restoreScene(entry, "redo");
1154
3524
  return true;
1155
3525
  },
1156
- copy(range) {
3526
+ copy(range, { escapeFormulas = false } = {}) {
1157
3527
  const { rows, columns } = getSnapshot();
1158
3528
  const matrix = [];
1159
3529
  rows.forEach((row, rowIndex) => {
@@ -1162,46 +3532,72 @@ function createTable(options) {
1162
3532
  if (rangeContains(range, {
1163
3533
  row: rowIndex,
1164
3534
  column: columnIndex
1165
- })) line.push(row.getDisplay(column.id));
3535
+ })) {
3536
+ const display = row.getDisplay(column.id);
3537
+ line.push(escapeFormulas ? escapeFormula(display, row.getValue(column.id)) : display);
3538
+ }
1166
3539
  });
1167
3540
  if (line.length > 0) matrix.push(line);
1168
3541
  });
1169
3542
  return toDelimited(matrix);
1170
3543
  },
1171
3544
  paste(origin, text) {
1172
- return applyChanges(rangeEdits(origin, parseDelimited(text)), true);
3545
+ 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.")]);
3546
+ const { rows, columns } = getSnapshot();
3547
+ const parsed = readDelimited(text, {
3548
+ maxRows: Math.max(0, rows.length - origin.row),
3549
+ maxColumns: Math.max(0, columns.length - origin.column),
3550
+ maxCells: maxPasteCells,
3551
+ maxLength: maxPasteLength
3552
+ });
3553
+ const issues = parsed.truncated ? [issue("paste_truncated", "The paste exceeds the visible grid or parsing limits; only complete cells within those limits are applied.")] : [];
3554
+ return validation.edit(rangeEdits(origin, parsed.rows), issues);
1173
3555
  },
1174
3556
  clear(range) {
1175
3557
  const matrix = Array.from({ length: range.bottom - range.top + 1 }, () => Array.from({ length: range.right - range.left + 1 }, () => ""));
1176
3558
  const current = getSnapshot();
1177
- return applyChanges(rangeEdits({
3559
+ const edits = rangeEdits({
1178
3560
  row: range.top,
1179
3561
  column: range.left
1180
3562
  }, matrix).filter((edit) => {
1181
3563
  const row = current.getRow(edit.rowKey);
1182
3564
  return row !== void 0 && current.getColumn(edit.column)?.isEditable(row.original) === true;
1183
- }), true);
3565
+ });
3566
+ return validation.edit(edits);
1184
3567
  },
1185
- exportRows({ format = "csv", columns: ids, pageOnly = false, headers = true } = {}) {
3568
+ exportRows({ format = "csv", columns: ids, pageOnly = false, headers = true, escapeFormulas = true, depth = false, indent = "" } = {}) {
1186
3569
  const current = getSnapshot();
1187
3570
  const columns = ids ? ids.flatMap((id) => {
1188
3571
  const column = current.getColumn(id);
1189
3572
  return column ? [column] : [];
1190
3573
  }) : 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));
3574
+ const rows = pageOnly ? current.rows : current.processedRows;
3575
+ const matrix = rows.map((row) => columns.map((column, index) => {
3576
+ const display = (index === 0 ? indent.repeat(row.depth) : "") + row.getDisplay(column.id);
3577
+ return escapeFormulas ? escapeFormula(display, row.getValue(column.id)) : display;
3578
+ }));
3579
+ if (depth) matrix.forEach((line, index) => line.unshift(String(rows[index].depth)));
3580
+ if (headers) {
3581
+ matrix.unshift(columns.map((column) => escapeFormulas ? escapeFormula(column.header) : column.header));
3582
+ if (depth) matrix[0].unshift("Depth");
3583
+ }
1193
3584
  return toDelimited(matrix, format === "csv" ? "," : " ");
1194
3585
  },
1195
3586
  reset() {
1196
3587
  commit(initialState);
1197
3588
  },
1198
3589
  destroy() {
3590
+ destroyed = true;
3591
+ streaming.cancel();
3592
+ validation.cancel();
3593
+ abortLoads(true);
1199
3594
  listeners.clear();
1200
3595
  }
1201
3596
  };
3597
+ startVisibleLoads();
1202
3598
  return table;
1203
3599
  }
1204
3600
 
1205
3601
  //#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 };
3602
+ 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
3603
  //# sourceMappingURL=index.js.map