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

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
@@ -466,6 +551,196 @@ function createUndoStack(limit = 100) {
466
551
  };
467
552
  }
468
553
 
554
+ //#endregion
555
+ //#region src/virtualizer.ts
556
+ const FALLBACK_SIZE = 40;
557
+ /** Layout arithmetic for either axis. All viewport values and measurements come from the caller. */
558
+ function createVirtualizer(initialOptions) {
559
+ let options = initialOptions;
560
+ let count = 0;
561
+ let overscan = 5;
562
+ let fixedSize;
563
+ let keys = [];
564
+ let byKey = /* @__PURE__ */ new Map();
565
+ let sizes = [];
566
+ let prefix = [0];
567
+ let offset = 0;
568
+ let viewportSize = 0;
569
+ let cached;
570
+ const measured = /* @__PURE__ */ new Map();
571
+ const problems = /* @__PURE__ */ new Map();
572
+ const listeners = /* @__PURE__ */ new Set();
573
+ function recover(code, message) {
574
+ if (!problems.has(code)) {
575
+ const problem = issue(code, message);
576
+ problems.set(code, problem);
577
+ options.onIssue?.(problem);
578
+ cached = void 0;
579
+ }
580
+ }
581
+ function validSize(value) {
582
+ if (Number.isFinite(value) && value > 0) return value;
583
+ recover("invalid_virtual_size", `Item size ${String(value)} is not positive and finite; 40 is used.`);
584
+ return FALLBACK_SIZE;
585
+ }
586
+ function configure(next) {
587
+ options = next;
588
+ problems.clear();
589
+ count = Number.isSafeInteger(next.count) && next.count >= 0 ? next.count : 0;
590
+ if (count !== next.count) recover("invalid_virtual_option", "Virtual item count must be a non-negative safe integer; 0 is used.");
591
+ const requestedOverscan = next.overscan ?? 5;
592
+ overscan = Number.isSafeInteger(requestedOverscan) && requestedOverscan >= 0 ? requestedOverscan : 5;
593
+ if (overscan !== requestedOverscan) recover("invalid_virtual_option", "Overscan must be a non-negative safe integer; 5 is used.");
594
+ const estimate = typeof next.estimateSize === "number" ? validSize(next.estimateSize) : void 0;
595
+ const nextKeys = [];
596
+ const nextSizes = [];
597
+ const nextByKey = /* @__PURE__ */ new Map();
598
+ let firstChanged = count;
599
+ let uniform = estimate !== void 0;
600
+ for (let index = 0; index < count; index += 1) {
601
+ let key = next.getKey ? next.getKey(index) : index;
602
+ if (!(typeof key === "string" || typeof key === "number" && Number.isFinite(key))) {
603
+ recover("invalid_virtual_option", `Item ${index} has an invalid key; its index is used.`);
604
+ key = index;
605
+ }
606
+ if (nextByKey.has(key)) {
607
+ recover("duplicate_virtual_key", `Item ${index} repeats a key; a distinct fallback key is used.`);
608
+ key = JSON.stringify(["virtual", index]);
609
+ while (nextByKey.has(key)) key += ":";
610
+ }
611
+ const size = measured.get(key) ?? estimate ?? validSize(next.estimateSize(index));
612
+ uniform &&= size === estimate;
613
+ if (keys[index] !== key || sizes[index] !== size) firstChanged = Math.min(firstChanged, index);
614
+ nextKeys.push(key);
615
+ nextSizes.push(size);
616
+ nextByKey.set(key, index);
617
+ }
618
+ if (fixedSize !== void 0 || uniform) prefix = [0];
619
+ else prefix.length = Math.min(prefix.length, firstChanged + 1);
620
+ keys = nextKeys;
621
+ sizes = nextSizes;
622
+ byKey = nextByKey;
623
+ fixedSize = uniform ? estimate : void 0;
624
+ cached = void 0;
625
+ }
626
+ function startOf(index) {
627
+ if (fixedSize !== void 0) return index * fixedSize;
628
+ while (prefix.length <= index) {
629
+ const at = prefix.length - 1;
630
+ prefix.push(prefix[at] + sizes[at]);
631
+ }
632
+ return prefix[index];
633
+ }
634
+ function totalSize() {
635
+ return startOf(count);
636
+ }
637
+ function indexAt(position) {
638
+ if (fixedSize !== void 0) return Math.min(count - 1, Math.floor(position / fixedSize));
639
+ let low = 0;
640
+ let high = count - 1;
641
+ while (low < high) {
642
+ const middle = Math.floor((low + high) / 2);
643
+ if (startOf(middle + 1) <= position) low = middle + 1;
644
+ else high = middle;
645
+ }
646
+ return low;
647
+ }
648
+ function getWindow() {
649
+ if (cached) return cached;
650
+ const total = totalSize();
651
+ const clampedOffset = Math.min(offset, Math.max(0, total - viewportSize));
652
+ let first = 0;
653
+ let last = -1;
654
+ if (count > 0 && viewportSize > 0) {
655
+ first = Math.max(0, indexAt(clampedOffset) - overscan);
656
+ const edge = Math.min(total, clampedOffset + viewportSize);
657
+ let visibleEnd = indexAt(edge);
658
+ if (startOf(visibleEnd) >= edge) visibleEnd -= 1;
659
+ last = Math.min(count - 1, visibleEnd + overscan);
660
+ }
661
+ const items = [];
662
+ for (let index = first; index <= last; index += 1) items.push(Object.freeze({
663
+ index,
664
+ key: keys[index],
665
+ start: startOf(index),
666
+ size: sizes[index]
667
+ }));
668
+ const paddingStart = items.length > 0 ? startOf(first) : 0;
669
+ const paddingEnd = items.length > 0 ? Math.max(0, total - startOf(last + 1)) : total;
670
+ cached = Object.freeze({
671
+ items: Object.freeze(items),
672
+ totalSize: total,
673
+ paddingStart,
674
+ paddingEnd,
675
+ startIndex: first,
676
+ endIndex: last,
677
+ offset: clampedOffset,
678
+ viewportSize,
679
+ issues: Object.freeze([...problems.values()])
680
+ });
681
+ return cached;
682
+ }
683
+ function notify() {
684
+ cached = void 0;
685
+ if (listeners.size > 0) {
686
+ const view = getWindow();
687
+ for (const listener of listeners) listener(view);
688
+ }
689
+ }
690
+ configure(initialOptions);
691
+ return {
692
+ setOptions(next) {
693
+ configure(next);
694
+ notify();
695
+ },
696
+ setViewport(nextOffset, nextSize) {
697
+ const issueCount = problems.size;
698
+ const safeOffset = Number.isFinite(nextOffset) && nextOffset >= 0 ? nextOffset : 0;
699
+ const safeSize = Number.isFinite(nextSize) && nextSize >= 0 ? nextSize : 0;
700
+ if (safeOffset !== nextOffset || safeSize !== nextSize) recover("invalid_virtual_viewport", "Viewport offset and size must be non-negative and finite; invalid values use 0.");
701
+ if (offset === safeOffset && viewportSize === safeSize && problems.size === issueCount) return;
702
+ offset = safeOffset;
703
+ viewportSize = safeSize;
704
+ notify();
705
+ },
706
+ measure(key, size) {
707
+ const index = byKey.get(key);
708
+ if (index === void 0) return;
709
+ if (!Number.isFinite(size) || size <= 0) {
710
+ recover("invalid_virtual_size", "Measured size must be positive and finite; the previous size is retained.");
711
+ notify();
712
+ return;
713
+ }
714
+ measured.set(key, size);
715
+ if (sizes[index] === size) return;
716
+ if (fixedSize !== void 0) {
717
+ fixedSize = void 0;
718
+ prefix = [0];
719
+ } else prefix.length = Math.min(prefix.length, index + 1);
720
+ sizes[index] = size;
721
+ notify();
722
+ },
723
+ getWindow,
724
+ getOffsetForIndex(index, align = "auto") {
725
+ if (count === 0) return 0;
726
+ 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.");
727
+ const target = Math.min(count - 1, Math.max(0, Number.isFinite(index) ? Math.trunc(index) : 0));
728
+ const start = startOf(target);
729
+ const end = start + sizes[target];
730
+ const current = getWindow().offset;
731
+ let result = start;
732
+ if (align === "center") result = start - (viewportSize - sizes[target]) / 2;
733
+ else if (align === "end") result = end - viewportSize;
734
+ else if (align === "auto") result = start < current ? start : end > current + viewportSize ? end - viewportSize : current;
735
+ return Math.min(Math.max(0, totalSize() - viewportSize), Math.max(0, result));
736
+ },
737
+ subscribe(listener) {
738
+ listeners.add(listener);
739
+ return () => listeners.delete(listener);
740
+ }
741
+ };
742
+ }
743
+
469
744
  //#endregion
470
745
  //#region src/infer.ts
471
746
  function isPlainObject(value) {
@@ -593,10 +868,52 @@ function paginationItems(page, pageCount, siblings = 1) {
593
868
  return items;
594
869
  }
595
870
 
871
+ //#endregion
872
+ //#region src/row.ts
873
+ /** Disjoint namespaces and typed keys avoid collisions between 1, "1", and detail-like ids. */
874
+ function getRowItemKey(key, kind = "row") {
875
+ return JSON.stringify([
876
+ kind,
877
+ typeof key,
878
+ key
879
+ ]);
880
+ }
881
+ function createRow(original, key, index, columns, canExpand = false) {
882
+ const values = /* @__PURE__ */ new Map();
883
+ const displays = /* @__PURE__ */ new Map();
884
+ const getValue = (columnId) => {
885
+ if (values.has(columnId)) return values.get(columnId);
886
+ const value = columns.get(columnId)?.getValue(original);
887
+ values.set(columnId, value);
888
+ return value;
889
+ };
890
+ return Object.freeze({
891
+ key,
892
+ index,
893
+ original,
894
+ canExpand,
895
+ isExpanded: false,
896
+ depth: 0,
897
+ parentKey: void 0,
898
+ childCount: 0,
899
+ childStatus: "loaded",
900
+ selection: "none",
901
+ getValue,
902
+ getDisplay(columnId) {
903
+ const cached = displays.get(columnId);
904
+ if (cached !== void 0) return cached;
905
+ const column = columns.get(columnId);
906
+ const display = column ? column.format(getValue(columnId), original) : "";
907
+ displays.set(columnId, display);
908
+ return display;
909
+ }
910
+ });
911
+ }
912
+
596
913
  //#endregion
597
914
  //#region src/state.ts
598
915
  const DEFAULT_PAGE_SIZE = 10;
599
- function createInitialState(patch = {}) {
916
+ function createInitialState(patch = {}, onIssue) {
600
917
  return freezeState({
601
918
  sorting: patch.sorting ?? [],
602
919
  search: patch.search ?? "",
@@ -606,71 +923,257 @@ function createInitialState(patch = {}) {
606
923
  pageSize: 10
607
924
  },
608
925
  selection: patch.selection ?? [],
926
+ expanded: normalizeExpanded(patch.expanded === void 0 ? [] : patch.expanded, onIssue),
609
927
  hiddenColumns: patch.hiddenColumns ?? [],
610
928
  columnOrder: patch.columnOrder ?? []
611
929
  });
612
930
  }
613
- function mergeState(state, patch) {
931
+ function mergeState(state, patch, onIssue) {
614
932
  let changed = false;
615
933
  const next = { ...state };
616
- for (const [key, value] of Object.entries(patch)) {
934
+ for (const [key, input] of Object.entries(patch)) {
935
+ const value = key === "expanded" && input !== void 0 ? normalizeExpanded(input, onIssue) : input;
617
936
  if (value === void 0 || Object.is(next[key], value)) continue;
618
937
  if (key === "pagination" && samePagination(next[key], value)) continue;
938
+ if (key === "expanded" && sameExpanded(state.expanded, value)) continue;
619
939
  next[key] = value;
620
940
  changed = true;
621
941
  }
622
- return changed ? freezeState(next) : state;
942
+ return changed ? freezeState(next, state) : state;
943
+ }
944
+ function normalizeExpanded(value, onIssue) {
945
+ if (value === true) return true;
946
+ if (Array.isArray(value) && value.every((key) => typeof key === "string" || typeof key === "number" && Number.isFinite(key))) return [...new Set(value)];
947
+ onIssue?.(issue("invalid_expanded", "Expanded state must be true or an array of string/finite number keys; an empty array is used."));
948
+ return [];
949
+ }
950
+ function sameExpanded(left, right) {
951
+ return left === right || left !== true && right !== true && left.length === right.length && left.every((key, index) => key === right[index]);
623
952
  }
624
953
  function samePagination(left, right) {
625
954
  return left.page === right.page && left.pageSize === right.pageSize;
626
955
  }
627
- function freezeState(state) {
956
+ function freezeState(state, previous) {
628
957
  return Object.freeze({
629
- sorting: Object.freeze(state.sorting.map((rule) => Object.freeze({ ...rule }))),
958
+ sorting: state.sorting === previous?.sorting ? previous.sorting : Object.freeze(state.sorting.map((rule) => Object.freeze({ ...rule }))),
630
959
  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])
960
+ filters: state.filters === previous?.filters ? previous.filters : Object.freeze({ ...state.filters }),
961
+ pagination: state.pagination === previous?.pagination ? previous.pagination : Object.freeze({ ...state.pagination }),
962
+ selection: state.selection === previous?.selection ? previous.selection : Object.freeze([...state.selection]),
963
+ expanded: state.expanded === previous?.expanded ? previous.expanded : state.expanded === true ? true : Object.freeze([...state.expanded]),
964
+ hiddenColumns: state.hiddenColumns === previous?.hiddenColumns ? previous.hiddenColumns : Object.freeze([...state.hiddenColumns]),
965
+ columnOrder: state.columnOrder === previous?.columnOrder ? previous.columnOrder : Object.freeze([...state.columnOrder])
636
966
  });
637
967
  }
638
968
 
639
969
  //#endregion
640
- //#region src/issues.ts
641
- function issue(code, message, details = {}) {
642
- return Object.freeze({
643
- code,
644
- message,
645
- ...details
646
- });
970
+ //#region src/tree.ts
971
+ function buildTree(data, columns, readKey, options, lazy) {
972
+ const nodes = [];
973
+ const byKey = /* @__PURE__ */ new Map();
974
+ const issues = [];
975
+ const nested = options.getChildren !== void 0;
976
+ let maxDepth = options.maxDepth ?? 1e3;
977
+ if (!Number.isSafeInteger(maxDepth) || maxDepth < 1) {
978
+ issues.push(issue("invalid_tree_option", "maxDepth must be a positive safe integer; 1,000 is used."));
979
+ maxDepth = 1e3;
980
+ }
981
+ if (nested && options.getParentKey) issues.push(issue("invalid_tree_option", "Both tree inputs were supplied; getChildren takes precedence."));
982
+ const seen = /* @__PURE__ */ new Set();
983
+ const stack = data.map((original, sourceIndex) => ({
984
+ original,
985
+ sourceIndex,
986
+ parent: void 0,
987
+ lazy: false
988
+ })).reverse();
989
+ while (stack.length) {
990
+ const entry = stack.pop();
991
+ const index = nodes.length;
992
+ let key = readKey(entry.original, index);
993
+ let parent = entry.parent;
994
+ const reference = entry.original !== null && (typeof entry.original === "object" || typeof entry.original === "function");
995
+ const repeated = nested && reference && seen.has(entry.original);
996
+ if (repeated) {
997
+ issues.push(issue("tree_cycle", "A repeated nested value recovers as a root without traversing it again.", { rowKey: key }));
998
+ parent = void 0;
999
+ }
1000
+ if (reference) seen.add(entry.original);
1001
+ if (byKey.has(key)) {
1002
+ 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 }));
1003
+ if (entry.lazy) continue;
1004
+ const prefix = `${String(key)}#${index}`;
1005
+ key = prefix;
1006
+ let suffix = 0;
1007
+ while (byKey.has(key)) key = `${prefix}#${++suffix}`;
1008
+ }
1009
+ if (parent && parent.depth + 1 > maxDepth) {
1010
+ issues.push(issue("tree_depth_exceeded", "The maximum tree depth was exceeded; this row recovers as a root.", { rowKey: key }));
1011
+ parent = void 0;
1012
+ }
1013
+ const children = repeated ? void 0 : lazy.get(key) ?? options.getChildren?.(entry.original);
1014
+ const node = {
1015
+ row: createRow(entry.original, key, index, columns),
1016
+ parent,
1017
+ children: [],
1018
+ depth: parent ? parent.depth + 1 : 0,
1019
+ sourceIndex: entry.sourceIndex,
1020
+ lazy: entry.lazy,
1021
+ writable: parent?.writable ?? entry.parent === void 0,
1022
+ unloaded: children === void 0 && (options.hasChildren?.(entry.original) ?? false)
1023
+ };
1024
+ nodes.push(node);
1025
+ byKey.set(key, node);
1026
+ if (parent) parent.children.push(node);
1027
+ if (children) for (let i = children.length - 1; i >= 0; i--) stack.push({
1028
+ original: children[i],
1029
+ sourceIndex: i,
1030
+ parent: node,
1031
+ lazy: entry.lazy || lazy.has(key)
1032
+ });
1033
+ }
1034
+ if (!nested && options.getParentKey) {
1035
+ for (const node of nodes) {
1036
+ if (node.lazy) continue;
1037
+ const key = options.getParentKey(node.row.original);
1038
+ if (key === void 0 || key === null) continue;
1039
+ const parent = byKey.get(key);
1040
+ if (!parent) issues.push(issue("tree_orphan", "The parent is missing; this row recovers as a root.", { rowKey: node.row.key }));
1041
+ else node.parent = parent;
1042
+ }
1043
+ const done = /* @__PURE__ */ new Set();
1044
+ for (const node of nodes) {
1045
+ const path = [];
1046
+ const visiting = /* @__PURE__ */ new Set();
1047
+ let cursor = node;
1048
+ while (cursor && !done.has(cursor)) {
1049
+ if (visiting.has(cursor)) {
1050
+ issues.push(issue("tree_cycle", "A cyclic parent reference is detached to a root.", { rowKey: cursor.row.key }));
1051
+ cursor.parent = void 0;
1052
+ break;
1053
+ }
1054
+ visiting.add(cursor);
1055
+ path.push(cursor);
1056
+ cursor = cursor.parent;
1057
+ }
1058
+ for (let i = path.length - 1; i >= 0; i--) {
1059
+ const current = path[i];
1060
+ done.add(current);
1061
+ }
1062
+ }
1063
+ for (const node of nodes) node.children = [];
1064
+ for (const node of nodes) if (node.parent) node.parent.children.push(node);
1065
+ const pending = nodes.filter((node) => !node.parent);
1066
+ while (pending.length) {
1067
+ const node = pending.pop();
1068
+ node.depth = node.parent ? node.parent.depth + 1 : 0;
1069
+ if (node.depth > maxDepth) {
1070
+ node.parent = void 0;
1071
+ node.depth = 0;
1072
+ issues.push(issue("tree_depth_exceeded", "The maximum tree depth was exceeded; this row recovers as a root.", { rowKey: node.row.key }));
1073
+ }
1074
+ for (const child of node.children) pending.push(child);
1075
+ }
1076
+ for (const node of nodes) node.children = node.children.filter((child) => child.parent === node);
1077
+ }
1078
+ return {
1079
+ nodes,
1080
+ rows: nodes.map((node) => node.row),
1081
+ roots: nodes.filter((node) => !node.parent),
1082
+ byKey,
1083
+ issues
1084
+ };
647
1085
  }
648
-
649
- //#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;
1086
+ function processTree(model, columns, state, mode, manual) {
1087
+ const raw = model.nodes.map((node) => node.row);
1088
+ const matched = manual ? raw : filterRows(raw, columns, state.search, state.filters);
1089
+ const active = matched !== raw;
1090
+ const included = new Set(matched.map((row) => row.key));
1091
+ const autoExpanded = /* @__PURE__ */ new Set();
1092
+ if (active && mode !== "strict") {
1093
+ if (mode === "descendants") {
1094
+ const stack = matched.map((row) => model.byKey.get(row.key));
1095
+ const visited = /* @__PURE__ */ new Set();
1096
+ while (stack.length) {
1097
+ const node = stack.pop();
1098
+ if (visited.has(node.row.key)) continue;
1099
+ visited.add(node.row.key);
1100
+ included.add(node.row.key);
1101
+ if (node.children.length) autoExpanded.add(node.row.key);
1102
+ for (const child of node.children) stack.push(child);
1103
+ }
1104
+ }
1105
+ for (const row of matched) {
1106
+ let parent = model.byKey.get(row.key).parent;
1107
+ while (parent && !autoExpanded.has(parent.row.key)) {
1108
+ included.add(parent.row.key);
1109
+ autoExpanded.add(parent.row.key);
1110
+ parent = parent.parent;
1111
+ }
1112
+ }
1113
+ }
1114
+ if (active && mode === "strict") return {
1115
+ model,
1116
+ roots: ordered(model.nodes.filter((node) => included.has(node.row.key) && node.children.length === 0 && !node.unloaded)),
1117
+ children: /* @__PURE__ */ new Map(),
1118
+ autoExpanded
659
1119
  };
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;
1120
+ const children = /* @__PURE__ */ new Map();
1121
+ for (const node of model.nodes) if (node.children.length) children.set(node, ordered(node.children.filter((child) => included.has(child.row.key))));
1122
+ return {
1123
+ model,
1124
+ roots: ordered(model.roots.filter((node) => included.has(node.row.key))),
1125
+ children,
1126
+ autoExpanded
1127
+ };
1128
+ function ordered(nodes) {
1129
+ if (manual || !state.sorting.length || nodes.length < 2) return nodes;
1130
+ return sortRows(nodes.map((node) => node.row), columns, state.sorting).map((row) => model.byKey.get(row.key));
1131
+ }
1132
+ }
1133
+ /** Expansion changes reuse filtering and sibling order, splicing only a changed subtree. */
1134
+ function createTreeFlattener() {
1135
+ let previous;
1136
+ let previousExpanded = [];
1137
+ let visible = [];
1138
+ let previousAncestors;
1139
+ return (tree, expanded, ancestors) => {
1140
+ if (tree === previous && expanded === previousExpanded && ancestors === previousAncestors) return visible;
1141
+ const keys = expanded === true ? void 0 : new Set(expanded);
1142
+ const open = (node) => expanded === true || (keys?.has(node.row.key) ?? false) || ancestors.has(node.row.key) || tree.autoExpanded.has(node.row.key);
1143
+ const walk = (roots) => {
1144
+ const output = [];
1145
+ const stack = [...roots].reverse();
1146
+ while (stack.length) {
1147
+ const node = stack.pop();
1148
+ output.push(node);
1149
+ if (open(node)) {
1150
+ const children = tree.children.get(node) ?? [];
1151
+ for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]);
1152
+ }
1153
+ }
1154
+ return output;
1155
+ };
1156
+ let changed;
1157
+ if (previous === tree && ancestors === previousAncestors && expanded !== true && previousExpanded !== true) {
1158
+ const before = new Set(previousExpanded);
1159
+ const after = new Set(expanded);
1160
+ changed = [...previousExpanded.filter((key) => !after.has(key)), ...expanded.filter((key) => !before.has(key))];
672
1161
  }
673
- });
1162
+ if (changed?.length === 1 && !tree.autoExpanded.has(changed[0])) {
1163
+ const index = visible.findIndex((node) => node.row.key === changed[0]);
1164
+ if (index >= 0) {
1165
+ const node = visible[index];
1166
+ let end = index + 1;
1167
+ while (end < visible.length && visible[end].depth > node.depth) end++;
1168
+ const inserted = walk(open(node) ? tree.children.get(node) ?? [] : []);
1169
+ visible = visible.slice(0, index + 1).concat(inserted, visible.slice(end));
1170
+ }
1171
+ } else visible = walk(tree.roots);
1172
+ previous = tree;
1173
+ previousExpanded = expanded;
1174
+ previousAncestors = ancestors;
1175
+ return visible;
1176
+ };
674
1177
  }
675
1178
 
676
1179
  //#endregion
@@ -697,19 +1200,42 @@ function coverage(selected, total) {
697
1200
  if (selected === 0 || total === 0) return "none";
698
1201
  return selected === total ? "all" : "some";
699
1202
  }
1203
+ function pasteLimit(value, fallback, name, issues) {
1204
+ if (value === void 0) return fallback;
1205
+ if (Number.isSafeInteger(value) && value > 0) return value;
1206
+ issues.push(issue("invalid_paste_limit", `Paste ${name} ${String(value)} is not a positive safe integer; ${fallback} is used.`));
1207
+ return fallback;
1208
+ }
700
1209
  /** Create a data table. */
701
1210
  function createTable(options) {
1211
+ const paginate = options.paginate ?? true;
1212
+ const expandMode = options.expandMode ?? "multiple";
702
1213
  const selectionMode = options.selectionMode ?? "multiple";
703
1214
  const selectScope = options.selectScope ?? "all";
704
1215
  const manual = options.manual ?? false;
1216
+ const treeEnabled = Boolean(options.getChildren || options.getParentKey || options.hasChildren || options.loadChildren);
1217
+ const rootPagination = treeEnabled && (manual || (options.paginateBy ?? "root") === "root");
1218
+ let lazyVersion = 0;
1219
+ const lazyChildren = /* @__PURE__ */ new Map();
1220
+ const childStatuses = /* @__PURE__ */ new Map();
1221
+ const loadIssues = /* @__PURE__ */ new Map();
1222
+ const loadValidationIssues = /* @__PURE__ */ new Map();
1223
+ const pendingLoads = /* @__PURE__ */ new Map();
1224
+ let destroyed = false;
705
1225
  const undoStack = createUndoStack(options.historyLimit);
706
1226
  const listeners = /* @__PURE__ */ new Set();
1227
+ const optionIssues = [];
1228
+ const maxPasteCells = pasteLimit(options.pasteLimit?.maxCells, 1e5, "maxCells", optionIssues);
1229
+ const maxPasteLength = pasteLimit(options.pasteLimit?.maxLength, 5e6, "maxLength", optionIssues);
707
1230
  let data = options.data;
708
1231
  let definitions = options.columns;
709
1232
  let rowCount = options.rowCount;
710
- const initialState = createInitialState({
1233
+ let expandedIssue;
1234
+ let initialState = createInitialState({
711
1235
  ...options.initialState,
712
1236
  hiddenColumns: options.initialState?.hiddenColumns ?? options.columns.filter((column) => column.hidden === true).map((column) => column.id)
1237
+ }, (problem) => {
1238
+ expandedIssue = problem;
713
1239
  });
714
1240
  let state = initialState;
715
1241
  let snapshot;
@@ -721,10 +1247,18 @@ function createTable(options) {
721
1247
  return rowKey === void 0 || !isRecord(row) ? index : String(value);
722
1248
  };
723
1249
  const resolveColumns = memo((defs) => {
724
- const columns = defs.map((definition) => resolveColumn(definition));
1250
+ const issues = [];
1251
+ const columns = defs.flatMap((definition) => {
1252
+ if (!isSafePath(definition.id)) {
1253
+ issues.push(issue("unsafe_path", `The column "${definition.id}" addresses a prototype and is ignored.`, { column: definition.id }));
1254
+ return [];
1255
+ }
1256
+ return [resolveColumn(definition)];
1257
+ });
725
1258
  return {
726
1259
  columns,
727
- byId: new Map(columns.map((column) => [column.id, column]))
1260
+ byId: new Map(columns.map((column) => [column.id, column])),
1261
+ issues
728
1262
  };
729
1263
  });
730
1264
  const indexRows = memo((rows, byId) => {
@@ -737,7 +1271,7 @@ function createTable(options) {
737
1271
  issues.push(issue("duplicate_row_key", `Row ${index} repeats the key "${String(key)}"; it is keyed "${String(key)}#${index}" instead.`, { rowKey: key }));
738
1272
  key = `${String(key)}#${index}`;
739
1273
  }
740
- const row = createRow(original, key, index, byId);
1274
+ const row = createRow(original, key, index, byId, options.getRowCanExpand?.(original) ?? false);
741
1275
  byKey.set(key, row);
742
1276
  return row;
743
1277
  }),
@@ -745,6 +1279,60 @@ function createTable(options) {
745
1279
  issues
746
1280
  };
747
1281
  });
1282
+ const treeStage = memo((rows, byId, _version) => buildTree(rows, byId, readKey, options, lazyChildren));
1283
+ const processedTreeStage = memo((model, columns, search, filters, sorting) => processTree(model, columns, {
1284
+ ...state,
1285
+ search,
1286
+ filters,
1287
+ sorting
1288
+ }, options.treeFilter ?? "ancestors", manual));
1289
+ const flattenTree = createTreeFlattener();
1290
+ const emptyAncestors = /* @__PURE__ */ new Set();
1291
+ const singleTreePath = memo((model, expanded) => {
1292
+ const ancestors = /* @__PURE__ */ new Set();
1293
+ if (expanded !== true) for (const key of expanded) {
1294
+ let parent = model.byKey.get(key)?.parent;
1295
+ while (parent && !ancestors.has(parent.row.key)) {
1296
+ ancestors.add(parent.row.key);
1297
+ parent = parent.parent;
1298
+ }
1299
+ }
1300
+ return ancestors;
1301
+ });
1302
+ const treeSelection = memo((model, selection) => {
1303
+ const selected = new Set(selection);
1304
+ const counts = /* @__PURE__ */ new Map();
1305
+ const stack = model.roots.map((node) => ({
1306
+ node,
1307
+ exit: false
1308
+ }));
1309
+ while (stack.length) {
1310
+ const { node, exit } = stack.pop();
1311
+ if (!exit) {
1312
+ stack.push({
1313
+ node,
1314
+ exit: true
1315
+ });
1316
+ for (const child of node.children) stack.push({
1317
+ node: child,
1318
+ exit: false
1319
+ });
1320
+ } else {
1321
+ let count = selected.has(node.row.key) ? 1 : 0;
1322
+ let total = 1;
1323
+ for (const child of node.children) {
1324
+ const value = counts.get(child);
1325
+ count += value.selected;
1326
+ total += value.total;
1327
+ }
1328
+ counts.set(node, {
1329
+ selected: count,
1330
+ total
1331
+ });
1332
+ }
1333
+ }
1334
+ return counts;
1335
+ });
748
1336
  const orderColumns = memo((columns, columnOrder, hiddenColumns) => {
749
1337
  const position = new Map(columnOrder.map((id, index) => [id, index]));
750
1338
  const ordered = columns.map((column, index) => ({
@@ -762,8 +1350,26 @@ function createTable(options) {
762
1350
  const filterStage = memo(filterRows);
763
1351
  const sortStage = memo(sortRows);
764
1352
  const selectedSet = memo((selection) => new Set(selection));
1353
+ const expansionStage = memo((indexed, expanded, selection) => {
1354
+ const keys = expanded === true ? void 0 : new Set(expanded);
1355
+ const selected = new Set(selection);
1356
+ const rows = indexed.rows.map((row) => {
1357
+ const isExpanded = row.canExpand && (expanded === true || (keys?.has(row.key) ?? false));
1358
+ return isExpanded || selected.has(row.key) ? Object.freeze({
1359
+ ...row,
1360
+ isExpanded,
1361
+ selection: selected.has(row.key) ? "all" : "none"
1362
+ }) : row;
1363
+ });
1364
+ return {
1365
+ rows,
1366
+ byKey: new Map(rows.map((row) => [row.key, row]))
1367
+ };
1368
+ });
1369
+ const expandedOrder = memo((rows, byKey) => rows.map((row) => byKey.get(row.key)));
765
1370
  function stateIssues(byId) {
766
1371
  const issues = [];
1372
+ if (expandedIssue) issues.push(expandedIssue);
767
1373
  const referenced = /* @__PURE__ */ new Set([
768
1374
  ...state.sorting.map((rule) => rule.column),
769
1375
  ...Object.keys(state.filters),
@@ -772,30 +1378,93 @@ function createTable(options) {
772
1378
  ]);
773
1379
  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
1380
  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.`));
1381
+ 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
1382
  return issues;
777
1383
  }
778
1384
  function computeSnapshot() {
779
- const { columns, byId } = resolveColumns(definitions);
780
- const indexed = indexRows(data, byId);
1385
+ const { columns, byId, issues: columnIssues } = resolveColumns(definitions);
1386
+ const model = treeEnabled ? treeStage(data, byId, lazyVersion) : void 0;
1387
+ const indexed = model ? {
1388
+ rows: model.rows,
1389
+ issues: model.issues
1390
+ } : indexRows(data, byId);
1391
+ const expanded = model ? void 0 : expansionStage(indexed, state.expanded, state.selection);
781
1392
  const ordered = orderColumns(columns, state.columnOrder, state.hiddenColumns);
782
- const issues = [...indexed.issues, ...stateIssues(byId)];
1393
+ const issues = [
1394
+ ...optionIssues,
1395
+ ...columnIssues,
1396
+ ...indexed.issues,
1397
+ ...stateIssues(byId),
1398
+ ...loadIssues.values(),
1399
+ ...Array.from(loadValidationIssues.values()).flat()
1400
+ ];
783
1401
  const requestedSize = state.pagination.pageSize;
784
- const pageSize = Number.isInteger(requestedSize) && requestedSize >= 1 ? requestedSize : 10;
1402
+ let pageSize = Number.isInteger(requestedSize) && requestedSize >= 1 ? requestedSize : 10;
785
1403
  let processedRows;
786
1404
  let rows;
787
1405
  let filteredCount;
788
1406
  let page;
789
1407
  let pageCount;
790
- if (manual) {
791
- processedRows = indexed.rows;
792
- rows = indexed.rows;
1408
+ let treeRow;
1409
+ let totalCount = indexed.rows.length;
1410
+ if (model) {
1411
+ const processed = processedTreeStage(model, columns, state.search, state.filters, state.sorting);
1412
+ const ancestors = expandMode === "single" ? singleTreePath(model, state.expanded) : emptyAncestors;
1413
+ const visible = flattenTree(processed, state.expanded, ancestors);
1414
+ const expandedState = state.expanded;
1415
+ const keys = state.expanded === true ? void 0 : new Set(state.expanded);
1416
+ const selected = treeSelection(model, state.selection);
1417
+ const projected = /* @__PURE__ */ new Map();
1418
+ const statuses = new Map(childStatuses);
1419
+ treeRow = (key) => {
1420
+ const cached = projected.get(key);
1421
+ if (cached) return cached;
1422
+ const node = model.byKey.get(key);
1423
+ if (!node) return void 0;
1424
+ const status = statuses.get(key) ?? (node.children.length || !node.unloaded ? "loaded" : "idle");
1425
+ const count = selected.get(node);
1426
+ const canExpand = node.children.length > 0 || status !== "loaded" || (options.getRowCanExpand?.(node.row.original) ?? false);
1427
+ const result = Object.freeze({
1428
+ ...node.row,
1429
+ depth: node.depth,
1430
+ parentKey: node.parent?.row.key,
1431
+ childCount: status === "loaded" ? node.children.length : void 0,
1432
+ childStatus: status,
1433
+ canExpand,
1434
+ isExpanded: canExpand && (expandedState === true || (keys?.has(key) ?? false) || ancestors.has(key) || processed.autoExpanded.has(key)),
1435
+ selection: coverage(count.selected, count.total)
1436
+ });
1437
+ projected.set(key, result);
1438
+ return result;
1439
+ };
1440
+ processedRows = visible.map((node) => treeRow(node.row.key));
1441
+ filteredCount = manual ? rowCount ?? model.roots.length : rootPagination ? processed.roots.length : visible.length;
1442
+ totalCount = rootPagination ? model.roots.length : model.nodes.length;
1443
+ pageCount = countPages(filteredCount, pageSize);
1444
+ page = clampPage(state.pagination.page, pageCount);
1445
+ if (manual) rows = processedRows;
1446
+ else if (rootPagination) {
1447
+ const roots = new Set(processed.roots.slice((page - 1) * pageSize, page * pageSize));
1448
+ const allRoots = new Set(processed.roots);
1449
+ let root;
1450
+ rows = processedRows.filter((_row, index) => {
1451
+ const node = visible[index];
1452
+ if (allRoots.has(node)) root = node;
1453
+ return root !== void 0 && roots.has(root);
1454
+ });
1455
+ } else rows = paginateRows(processedRows, {
1456
+ page,
1457
+ pageSize
1458
+ });
1459
+ } else if (manual) {
1460
+ processedRows = expanded.rows;
1461
+ rows = expanded.rows;
793
1462
  filteredCount = rowCount ?? indexed.rows.length;
794
1463
  pageCount = countPages(filteredCount, pageSize);
795
1464
  page = clampPage(state.pagination.page, pageCount);
796
1465
  } else {
797
1466
  const filtered = filterStage(indexed.rows, columns, state.search, state.filters);
798
- processedRows = sortStage(filtered, columns, state.sorting);
1467
+ processedRows = expandedOrder(sortStage(filtered, columns, state.sorting), expanded.byKey);
799
1468
  filteredCount = processedRows.length;
800
1469
  pageCount = countPages(filteredCount, pageSize);
801
1470
  page = clampPage(state.pagination.page, pageCount);
@@ -804,6 +1473,12 @@ function createTable(options) {
804
1473
  pageSize
805
1474
  });
806
1475
  }
1476
+ if (!paginate) {
1477
+ rows = processedRows;
1478
+ page = 1;
1479
+ pageCount = 1;
1480
+ pageSize = Math.max(1, rows.length);
1481
+ }
807
1482
  const selected = selectedSet(state.selection);
808
1483
  const pageSelected = rows.filter((row) => selected.has(row.key)).length;
809
1484
  const scopeRows = selectScope === "page" || manual ? rows : processedRows;
@@ -818,19 +1493,34 @@ function createTable(options) {
818
1493
  columns: ordered.visible,
819
1494
  allColumns: ordered.all,
820
1495
  rows,
1496
+ renderItems: Object.freeze(rows.flatMap((row, rowIndex) => {
1497
+ const item = Object.freeze({
1498
+ kind: "row",
1499
+ key: getRowItemKey(row.key),
1500
+ row,
1501
+ rowIndex
1502
+ });
1503
+ return row.isExpanded && (!model || (options.getRowCanExpand?.(row.original) ?? false)) ? [item, Object.freeze({
1504
+ kind: "detail",
1505
+ key: getRowItemKey(row.key, "detail"),
1506
+ row,
1507
+ rowIndex
1508
+ })] : [item];
1509
+ })),
1510
+ paginate,
821
1511
  processedRows,
822
1512
  page,
823
1513
  pageSize,
824
1514
  pageCount,
825
1515
  rowCount: filteredCount,
826
- totalRowCount: manual ? filteredCount : indexed.rows.length,
1516
+ totalRowCount: manual ? filteredCount : totalCount,
827
1517
  pageStart,
828
- pageEnd: rows.length === 0 ? 0 : pageStart + rows.length - 1,
1518
+ pageEnd: rows.length === 0 ? 0 : rootPagination ? Math.min(filteredCount, page * pageSize) : pageStart + rows.length - 1,
829
1519
  pageItems: paginationItems(page, pageCount),
830
1520
  canPreviousPage: page > 1,
831
1521
  canNextPage: page < pageCount,
832
1522
  selectionMode,
833
- selectedCount: state.selection.length,
1523
+ selectedCount: model ? state.selection.filter((key) => model.byKey.has(key)).length : state.selection.length,
834
1524
  pageSelection: coverage(pageSelected, rows.length),
835
1525
  allSelection: coverage(scopeSelected, scopeRows.length),
836
1526
  canUndo: undoStack.canUndo,
@@ -839,7 +1529,7 @@ function createTable(options) {
839
1529
  isSelected: (key) => selected.has(key),
840
1530
  getSort: (columnId) => sortIndex.get(columnId),
841
1531
  getColumn: (columnId) => byId.get(columnId),
842
- getRow: (key) => indexed.byKey.get(key)
1532
+ getRow: (key) => treeRow ? treeRow(key) : expanded.byKey.get(key)
843
1533
  });
844
1534
  }
845
1535
  function getSnapshot() {
@@ -852,14 +1542,131 @@ function createTable(options) {
852
1542
  const next = getSnapshot();
853
1543
  for (const listener of listeners) listener(next);
854
1544
  }
1545
+ function abortLoads(all = false) {
1546
+ for (const [key, controller] of pendingLoads) {
1547
+ const row = all ? void 0 : getSnapshot().getRow(key);
1548
+ const visible = !all && getSnapshot().processedRows.some((candidate) => candidate.key === key);
1549
+ if (all || !row?.isExpanded || !visible) {
1550
+ pendingLoads.delete(key);
1551
+ childStatuses.delete(key);
1552
+ controller.abort();
1553
+ snapshot = void 0;
1554
+ }
1555
+ }
1556
+ }
1557
+ function requestChildren(row) {
1558
+ if (!options.loadChildren || destroyed || pendingLoads.has(row.key) || lazyChildren.has(row.key) || row.childStatus === "loaded") return;
1559
+ const key = row.key;
1560
+ if (!options.createChildLoadController) {
1561
+ childStatuses.set(key, "error");
1562
+ loadIssues.set(key, issue("invalid_tree_option", "Lazy children require createChildLoadController from the caller's runtime.", { rowKey: key }));
1563
+ notify();
1564
+ return;
1565
+ }
1566
+ let controller;
1567
+ try {
1568
+ controller = options.createChildLoadController();
1569
+ } catch (error) {
1570
+ loadIssues.set(key, issue("tree_load_error", String(error), { rowKey: key }));
1571
+ childStatuses.set(key, "error");
1572
+ notify();
1573
+ return;
1574
+ }
1575
+ pendingLoads.set(key, controller);
1576
+ childStatuses.set(key, "loading");
1577
+ loadIssues.delete(key);
1578
+ loadValidationIssues.delete(key);
1579
+ notify();
1580
+ Promise.resolve().then(() => controller.signal.aborted ? [] : options.loadChildren(row, controller.signal)).then((children) => {
1581
+ if (controller.signal.aborted || pendingLoads.get(key) !== controller || destroyed) return void 0;
1582
+ const { byId } = resolveColumns(definitions);
1583
+ const existing = treeStage(data, byId, lazyVersion);
1584
+ const candidate = buildTree(children, byId, readKey, {
1585
+ ...options,
1586
+ getParentKey: void 0
1587
+ }, /* @__PURE__ */ new Map());
1588
+ pendingLoads.delete(key);
1589
+ loadValidationIssues.set(key, candidate.issues.filter((problem) => problem.code === "tree_cycle" || problem.code === "tree_depth_exceeded"));
1590
+ if (candidate.nodes.filter((node) => existing.byKey.has(node.row.key)).length || candidate.issues.some((problem) => problem.code === "duplicate_row_key")) {
1591
+ childStatuses.set(key, "error");
1592
+ loadIssues.set(key, issue("tree_duplicate_key", "Loaded children collide with existing keys; the batch is rejected.", { rowKey: key }));
1593
+ } else {
1594
+ lazyChildren.set(key, children);
1595
+ childStatuses.set(key, "loaded");
1596
+ lazyVersion++;
1597
+ if (state.selection.includes(key) && selectionMode === "multiple") {
1598
+ const nextModel = treeStage(data, byId, lazyVersion);
1599
+ applySelection([...state.selection, ...selectionKeys([key], nextModel)]);
1600
+ }
1601
+ }
1602
+ notify();
1603
+ startVisibleLoads();
1604
+ }).catch((error) => {
1605
+ if (controller.signal.aborted || pendingLoads.get(key) !== controller || destroyed) return;
1606
+ pendingLoads.delete(key);
1607
+ childStatuses.set(key, "error");
1608
+ loadIssues.set(key, issue("tree_load_error", `Children could not be loaded: ${String(error)}`, { rowKey: key }));
1609
+ notify();
1610
+ });
1611
+ }
1612
+ function startVisibleLoads() {
1613
+ if (!treeEnabled || !options.loadChildren || destroyed) return;
1614
+ const current = getSnapshot();
1615
+ for (const row of current.rows) if (row.isExpanded && row.childStatus === "idle") requestChildren(row);
1616
+ }
1617
+ function selectionKeys(keys, model) {
1618
+ if (!treeEnabled || selectionMode !== "multiple") return [...keys];
1619
+ model ??= treeStage(data, resolveColumns(definitions).byId, lazyVersion);
1620
+ const result = new Set(keys);
1621
+ const stack = keys.flatMap((key) => {
1622
+ const node = model.byKey.get(key);
1623
+ return node ? [node] : [];
1624
+ });
1625
+ const visited = /* @__PURE__ */ new Set();
1626
+ while (stack.length) {
1627
+ const node = stack.pop();
1628
+ if (visited.has(node.row.key)) continue;
1629
+ visited.add(node.row.key);
1630
+ result.add(node.row.key);
1631
+ for (const child of node.children) stack.push(child);
1632
+ }
1633
+ return [...result];
1634
+ }
855
1635
  /** Apply a change made by a table operation. */
856
1636
  function commit(patch) {
857
1637
  const previous = state;
858
- const next = mergeState(state, patch);
859
- if (next === previous) return;
1638
+ const previousIssue = expandedIssue;
1639
+ const next = mergePatch(patch);
1640
+ if (next === previous && previousIssue === expandedIssue) return;
860
1641
  state = next;
861
- options.onStateChange?.(state, previous);
1642
+ snapshot = void 0;
1643
+ abortLoads();
1644
+ if (next !== previous) options.onStateChange?.(state, previous);
862
1645
  notify();
1646
+ startVisibleLoads();
1647
+ }
1648
+ function canonicalExpanded(expanded, candidate = state) {
1649
+ if (expandMode === "multiple") return expanded;
1650
+ if (expanded !== true) return expanded.slice(-1);
1651
+ const { columns, byId } = resolveColumns(definitions);
1652
+ if (treeEnabled) {
1653
+ const model = treeStage(data, byId, lazyVersion);
1654
+ 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));
1655
+ return first ? [first.row.key] : [];
1656
+ }
1657
+ const indexed = indexRows(data, byId);
1658
+ const first = (manual ? indexed.rows : sortStage(filterStage(indexed.rows, columns, candidate.search, candidate.filters), columns, candidate.sorting)).find((row) => row.canExpand);
1659
+ return first ? [first.key] : [];
1660
+ }
1661
+ function mergePatch(patch) {
1662
+ if (patch.expanded !== void 0) expandedIssue = void 0;
1663
+ const next = mergeState(state, patch, (problem) => {
1664
+ expandedIssue = problem;
1665
+ });
1666
+ return expandMode === "single" && patch.expanded !== void 0 ? mergeState(state, {
1667
+ ...patch,
1668
+ expanded: canonicalExpanded(next.expanded, next)
1669
+ }) : next;
863
1670
  }
864
1671
  const firstPage = () => ({
865
1672
  page: 1,
@@ -890,10 +1697,13 @@ function createTable(options) {
890
1697
  if (selection.length === current.length && selection.every((key, i) => key === current[i])) return;
891
1698
  commit({ selection });
892
1699
  }
893
- function applyChanges(edits, record) {
1700
+ function applyChanges(edits, record, recoveredIssues = []) {
894
1701
  const current = getSnapshot();
895
1702
  const working = [...data];
896
- const issues = [];
1703
+ const model = treeEnabled ? treeStage(data, resolveColumns(definitions).byId, lazyVersion) : void 0;
1704
+ const replacements = /* @__PURE__ */ new Map();
1705
+ const originalOf = (node) => replacements.get(node) ?? node.row.original;
1706
+ const issues = [...recoveredIssues];
897
1707
  const changes = [];
898
1708
  const samples = /* @__PURE__ */ new Map();
899
1709
  const sampleOf = (columnId) => {
@@ -916,6 +1726,13 @@ function createTable(options) {
916
1726
  }));
917
1727
  continue;
918
1728
  }
1729
+ if (!isSafePath(edit.column)) {
1730
+ issues.push(issue("unsafe_path", `The path "${edit.column}" addresses a prototype and cannot be written.`, {
1731
+ rowKey: edit.rowKey,
1732
+ column: edit.column
1733
+ }));
1734
+ continue;
1735
+ }
919
1736
  if (!column) {
920
1737
  issues.push(issue("unknown_column", `No column has the id "${edit.column}".`, {
921
1738
  rowKey: edit.rowKey,
@@ -923,7 +1740,22 @@ function createTable(options) {
923
1740
  }));
924
1741
  continue;
925
1742
  }
926
- const original = working[row.index];
1743
+ const node = model?.byKey.get(row.key);
1744
+ const original = node ? originalOf(node) : working[row.index];
1745
+ if (node && !node.writable) {
1746
+ issues.push(issue("read_only_cell", "A recovered nested root has no unambiguous source path for editing.", {
1747
+ rowKey: row.key,
1748
+ column: column.id
1749
+ }));
1750
+ continue;
1751
+ }
1752
+ if (node?.parent && options.getChildren && !options.setChildren) {
1753
+ issues.push(issue("read_only_cell", "Editing a nested child requires an immutable setChildren callback.", {
1754
+ rowKey: row.key,
1755
+ column: column.id
1756
+ }));
1757
+ continue;
1758
+ }
927
1759
  if (record && !column.isEditable(original)) {
928
1760
  issues.push(issue("read_only_cell", `The "${column.id}" cell of row "${String(row.key)}" is read-only.`, {
929
1761
  rowKey: row.key,
@@ -953,7 +1785,21 @@ function createTable(options) {
953
1785
  }));
954
1786
  continue;
955
1787
  }
956
- working[row.index] = updated;
1788
+ if (!node) working[row.index] = updated;
1789
+ else {
1790
+ let cursor = node;
1791
+ replacements.set(cursor, updated);
1792
+ if (!options.getChildren && !cursor.lazy) working[cursor.sourceIndex] = updated;
1793
+ while (cursor.parent) {
1794
+ const parent = cursor.parent;
1795
+ const children = parent.children.map(originalOf);
1796
+ if (cursor.lazy) lazyChildren.set(parent.row.key, children);
1797
+ if (options.setChildren) replacements.set(parent, options.setChildren(originalOf(parent), children));
1798
+ cursor = parent;
1799
+ }
1800
+ if (options.getChildren || options.setChildren) working[cursor.sourceIndex] = originalOf(cursor);
1801
+ lazyVersion++;
1802
+ }
957
1803
  changes.push({
958
1804
  rowKey: row.key,
959
1805
  column: column.id,
@@ -996,12 +1842,17 @@ function createTable(options) {
996
1842
  return edits;
997
1843
  }
998
1844
  function replay(changes, direction) {
999
- applyChanges(changes.map((change) => ({
1845
+ applyChanges((direction === "undo" ? changes.toReversed() : changes).map((change) => ({
1000
1846
  rowKey: change.rowKey,
1001
1847
  column: change.column,
1002
1848
  value: direction === "undo" ? change.previous : change.value
1003
1849
  })), false);
1004
1850
  }
1851
+ if (expandMode === "single") {
1852
+ initialState = mergeState(initialState, { expanded: canonicalExpanded(initialState.expanded) });
1853
+ state = initialState;
1854
+ snapshot = void 0;
1855
+ }
1005
1856
  const table = {
1006
1857
  getSnapshot,
1007
1858
  getState: () => state,
@@ -1012,17 +1863,28 @@ function createTable(options) {
1012
1863
  };
1013
1864
  },
1014
1865
  setState(patch) {
1015
- const next = mergeState(state, patch);
1016
- if (next !== state) {
1866
+ const previousIssue = expandedIssue;
1867
+ const next = mergePatch(patch);
1868
+ if (next !== state || previousIssue !== expandedIssue) {
1017
1869
  state = next;
1870
+ snapshot = void 0;
1871
+ abortLoads();
1018
1872
  notify();
1873
+ startVisibleLoads();
1019
1874
  }
1020
1875
  },
1021
1876
  setData(next) {
1022
1877
  if (next === data) return;
1878
+ abortLoads(true);
1879
+ lazyChildren.clear();
1880
+ childStatuses.clear();
1881
+ loadIssues.clear();
1882
+ loadValidationIssues.clear();
1883
+ lazyVersion++;
1023
1884
  data = next;
1024
1885
  undoStack.clear();
1025
1886
  notify();
1887
+ startVisibleLoads();
1026
1888
  },
1027
1889
  setColumns(next) {
1028
1890
  if (next === definitions) return;
@@ -1069,6 +1931,7 @@ function createTable(options) {
1069
1931
  });
1070
1932
  },
1071
1933
  goToPage(page) {
1934
+ if (!paginate) return;
1072
1935
  const { pageCount, page: currentPage } = getSnapshot();
1073
1936
  const next = clampPage(page, pageCount);
1074
1937
  if (next !== currentPage || next !== state.pagination.page) commit({ pagination: {
@@ -1083,6 +1946,7 @@ function createTable(options) {
1083
1946
  table.goToPage(getSnapshot().page - 1);
1084
1947
  },
1085
1948
  setPageSize(pageSize) {
1949
+ if (!paginate) return;
1086
1950
  if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize === state.pagination.pageSize) return;
1087
1951
  const { pageStart } = getSnapshot();
1088
1952
  commit({ pagination: {
@@ -1091,14 +1955,14 @@ function createTable(options) {
1091
1955
  } });
1092
1956
  },
1093
1957
  select(keys) {
1094
- applySelection([...state.selection, ...keys]);
1958
+ applySelection([...state.selection, ...selectionKeys(keys)]);
1095
1959
  },
1096
1960
  deselect(keys) {
1097
- const removed = new Set(keys);
1961
+ const removed = new Set(selectionKeys(keys));
1098
1962
  applySelection(state.selection.filter((key) => !removed.has(key)));
1099
1963
  },
1100
1964
  toggleRow(key, selected) {
1101
- const isSelected = state.selection.includes(key);
1965
+ const isSelected = treeEnabled ? getSnapshot().getRow(key)?.selection === "all" : state.selection.includes(key);
1102
1966
  const next = selected ?? !isSelected;
1103
1967
  if (next === isSelected) return;
1104
1968
  if (next) table.select([key]);
@@ -1121,6 +1985,27 @@ function createTable(options) {
1121
1985
  return row ? [row] : [];
1122
1986
  });
1123
1987
  },
1988
+ toggleExpanded(key, expanded) {
1989
+ const row = getSnapshot().getRow(key);
1990
+ if (!row?.canExpand) return;
1991
+ const open = expanded ?? !row.isExpanded;
1992
+ if (open === row.isExpanded) {
1993
+ if (open && row.childStatus === "error") requestChildren(row);
1994
+ return;
1995
+ }
1996
+ if (expandMode === "single") commit({ expanded: open ? [key] : [] });
1997
+ else if (state.expanded === true) {
1998
+ const { byId } = resolveColumns(definitions);
1999
+ 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) });
2000
+ } else commit({ expanded: open ? [...state.expanded, key] : state.expanded.filter((candidate) => candidate !== key) });
2001
+ if (open) requestChildren(getSnapshot().getRow(key));
2002
+ },
2003
+ expandAll() {
2004
+ commit({ expanded: true });
2005
+ },
2006
+ collapseAll() {
2007
+ commit({ expanded: [] });
2008
+ },
1124
2009
  toggleColumn(columnId, visible) {
1125
2010
  if (!getSnapshot().getColumn(columnId)) return;
1126
2011
  const hidden = state.hiddenColumns.includes(columnId);
@@ -1153,7 +2038,7 @@ function createTable(options) {
1153
2038
  replay(changes, "redo");
1154
2039
  return true;
1155
2040
  },
1156
- copy(range) {
2041
+ copy(range, { escapeFormulas = false } = {}) {
1157
2042
  const { rows, columns } = getSnapshot();
1158
2043
  const matrix = [];
1159
2044
  rows.forEach((row, rowIndex) => {
@@ -1162,14 +2047,26 @@ function createTable(options) {
1162
2047
  if (rangeContains(range, {
1163
2048
  row: rowIndex,
1164
2049
  column: columnIndex
1165
- })) line.push(row.getDisplay(column.id));
2050
+ })) {
2051
+ const display = row.getDisplay(column.id);
2052
+ line.push(escapeFormulas ? escapeFormula(display, row.getValue(column.id)) : display);
2053
+ }
1166
2054
  });
1167
2055
  if (line.length > 0) matrix.push(line);
1168
2056
  });
1169
2057
  return toDelimited(matrix);
1170
2058
  },
1171
2059
  paste(origin, text) {
1172
- return applyChanges(rangeEdits(origin, parseDelimited(text)), true);
2060
+ if (!Number.isInteger(origin.row) || !Number.isInteger(origin.column) || origin.row < 0 || origin.column < 0) return applyChanges([], true, [issue("invalid_value", "The paste origin must use non-negative whole row and column indices.")]);
2061
+ const { rows, columns } = getSnapshot();
2062
+ const parsed = readDelimited(text, {
2063
+ maxRows: Math.max(0, rows.length - origin.row),
2064
+ maxColumns: Math.max(0, columns.length - origin.column),
2065
+ maxCells: maxPasteCells,
2066
+ maxLength: maxPasteLength
2067
+ });
2068
+ const issues = parsed.truncated ? [issue("paste_truncated", "The paste exceeds the visible grid or parsing limits; only complete cells within those limits are applied.")] : [];
2069
+ return applyChanges(rangeEdits(origin, parsed.rows), true, issues);
1173
2070
  },
1174
2071
  clear(range) {
1175
2072
  const matrix = Array.from({ length: range.bottom - range.top + 1 }, () => Array.from({ length: range.right - range.left + 1 }, () => ""));
@@ -1182,26 +2079,37 @@ function createTable(options) {
1182
2079
  return row !== void 0 && current.getColumn(edit.column)?.isEditable(row.original) === true;
1183
2080
  }), true);
1184
2081
  },
1185
- exportRows({ format = "csv", columns: ids, pageOnly = false, headers = true } = {}) {
2082
+ exportRows({ format = "csv", columns: ids, pageOnly = false, headers = true, escapeFormulas = true, depth = false, indent = "" } = {}) {
1186
2083
  const current = getSnapshot();
1187
2084
  const columns = ids ? ids.flatMap((id) => {
1188
2085
  const column = current.getColumn(id);
1189
2086
  return column ? [column] : [];
1190
2087
  }) : 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));
2088
+ const rows = pageOnly ? current.rows : current.processedRows;
2089
+ const matrix = rows.map((row) => columns.map((column, index) => {
2090
+ const display = (index === 0 ? indent.repeat(row.depth) : "") + row.getDisplay(column.id);
2091
+ return escapeFormulas ? escapeFormula(display, row.getValue(column.id)) : display;
2092
+ }));
2093
+ if (depth) matrix.forEach((line, index) => line.unshift(String(rows[index].depth)));
2094
+ if (headers) {
2095
+ matrix.unshift(columns.map((column) => escapeFormulas ? escapeFormula(column.header) : column.header));
2096
+ if (depth) matrix[0].unshift("Depth");
2097
+ }
1193
2098
  return toDelimited(matrix, format === "csv" ? "," : " ");
1194
2099
  },
1195
2100
  reset() {
1196
2101
  commit(initialState);
1197
2102
  },
1198
2103
  destroy() {
2104
+ destroyed = true;
2105
+ abortLoads(true);
1199
2106
  listeners.clear();
1200
2107
  }
1201
2108
  };
2109
+ startVisibleLoads();
1202
2110
  return table;
1203
2111
  }
1204
2112
 
1205
2113
  //#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 };
2114
+ 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
2115
  //# sourceMappingURL=index.js.map