@vueye-table/core 3.0.0-alpha.2 → 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
@@ -893,6 +893,11 @@ function createRow(original, key, index, columns, canExpand = false) {
893
893
  original,
894
894
  canExpand,
895
895
  isExpanded: false,
896
+ depth: 0,
897
+ parentKey: void 0,
898
+ childCount: 0,
899
+ childStatus: "loaded",
900
+ selection: "none",
896
901
  getValue,
897
902
  getDisplay(columnId) {
898
903
  const cached = displays.get(columnId);
@@ -961,6 +966,216 @@ function freezeState(state, previous) {
961
966
  });
962
967
  }
963
968
 
969
+ //#endregion
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
+ };
1085
+ }
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
1119
+ };
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))];
1161
+ }
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
+ };
1177
+ }
1178
+
964
1179
  //#endregion
965
1180
  //#region src/table.ts
966
1181
  function isRecord(value) {
@@ -998,6 +1213,15 @@ function createTable(options) {
998
1213
  const selectionMode = options.selectionMode ?? "multiple";
999
1214
  const selectScope = options.selectScope ?? "all";
1000
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;
1001
1225
  const undoStack = createUndoStack(options.historyLimit);
1002
1226
  const listeners = /* @__PURE__ */ new Set();
1003
1227
  const optionIssues = [];
@@ -1055,6 +1279,60 @@ function createTable(options) {
1055
1279
  issues
1056
1280
  };
1057
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
+ });
1058
1336
  const orderColumns = memo((columns, columnOrder, hiddenColumns) => {
1059
1337
  const position = new Map(columnOrder.map((id, index) => [id, index]));
1060
1338
  const ordered = columns.map((column, index) => ({
@@ -1072,12 +1350,17 @@ function createTable(options) {
1072
1350
  const filterStage = memo(filterRows);
1073
1351
  const sortStage = memo(sortRows);
1074
1352
  const selectedSet = memo((selection) => new Set(selection));
1075
- const expansionStage = memo((indexed, expanded) => {
1353
+ const expansionStage = memo((indexed, expanded, selection) => {
1076
1354
  const keys = expanded === true ? void 0 : new Set(expanded);
1077
- const rows = indexed.rows.map((row) => row.canExpand && (expanded === true || keys?.has(row.key)) ? Object.freeze({
1078
- ...row,
1079
- isExpanded: true
1080
- }) : row);
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
+ });
1081
1364
  return {
1082
1365
  rows,
1083
1366
  byKey: new Map(rows.map((row) => [row.key, row]))
@@ -1100,14 +1383,20 @@ function createTable(options) {
1100
1383
  }
1101
1384
  function computeSnapshot() {
1102
1385
  const { columns, byId, issues: columnIssues } = resolveColumns(definitions);
1103
- const indexed = indexRows(data, byId);
1104
- const expanded = expansionStage(indexed, state.expanded);
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);
1105
1392
  const ordered = orderColumns(columns, state.columnOrder, state.hiddenColumns);
1106
1393
  const issues = [
1107
1394
  ...optionIssues,
1108
1395
  ...columnIssues,
1109
1396
  ...indexed.issues,
1110
- ...stateIssues(byId)
1397
+ ...stateIssues(byId),
1398
+ ...loadIssues.values(),
1399
+ ...Array.from(loadValidationIssues.values()).flat()
1111
1400
  ];
1112
1401
  const requestedSize = state.pagination.pageSize;
1113
1402
  let pageSize = Number.isInteger(requestedSize) && requestedSize >= 1 ? requestedSize : 10;
@@ -1116,7 +1405,58 @@ function createTable(options) {
1116
1405
  let filteredCount;
1117
1406
  let page;
1118
1407
  let pageCount;
1119
- if (manual) {
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) {
1120
1460
  processedRows = expanded.rows;
1121
1461
  rows = expanded.rows;
1122
1462
  filteredCount = rowCount ?? indexed.rows.length;
@@ -1160,7 +1500,7 @@ function createTable(options) {
1160
1500
  row,
1161
1501
  rowIndex
1162
1502
  });
1163
- return row.isExpanded ? [item, Object.freeze({
1503
+ return row.isExpanded && (!model || (options.getRowCanExpand?.(row.original) ?? false)) ? [item, Object.freeze({
1164
1504
  kind: "detail",
1165
1505
  key: getRowItemKey(row.key, "detail"),
1166
1506
  row,
@@ -1173,14 +1513,14 @@ function createTable(options) {
1173
1513
  pageSize,
1174
1514
  pageCount,
1175
1515
  rowCount: filteredCount,
1176
- totalRowCount: manual ? filteredCount : indexed.rows.length,
1516
+ totalRowCount: manual ? filteredCount : totalCount,
1177
1517
  pageStart,
1178
- 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,
1179
1519
  pageItems: paginationItems(page, pageCount),
1180
1520
  canPreviousPage: page > 1,
1181
1521
  canNextPage: page < pageCount,
1182
1522
  selectionMode,
1183
- selectedCount: state.selection.length,
1523
+ selectedCount: model ? state.selection.filter((key) => model.byKey.has(key)).length : state.selection.length,
1184
1524
  pageSelection: coverage(pageSelected, rows.length),
1185
1525
  allSelection: coverage(scopeSelected, scopeRows.length),
1186
1526
  canUndo: undoStack.canUndo,
@@ -1189,7 +1529,7 @@ function createTable(options) {
1189
1529
  isSelected: (key) => selected.has(key),
1190
1530
  getSort: (columnId) => sortIndex.get(columnId),
1191
1531
  getColumn: (columnId) => byId.get(columnId),
1192
- getRow: (key) => expanded.byKey.get(key)
1532
+ getRow: (key) => treeRow ? treeRow(key) : expanded.byKey.get(key)
1193
1533
  });
1194
1534
  }
1195
1535
  function getSnapshot() {
@@ -1202,6 +1542,96 @@ function createTable(options) {
1202
1542
  const next = getSnapshot();
1203
1543
  for (const listener of listeners) listener(next);
1204
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
+ }
1205
1635
  /** Apply a change made by a table operation. */
1206
1636
  function commit(patch) {
1207
1637
  const previous = state;
@@ -1209,13 +1639,21 @@ function createTable(options) {
1209
1639
  const next = mergePatch(patch);
1210
1640
  if (next === previous && previousIssue === expandedIssue) return;
1211
1641
  state = next;
1642
+ snapshot = void 0;
1643
+ abortLoads();
1212
1644
  if (next !== previous) options.onStateChange?.(state, previous);
1213
1645
  notify();
1646
+ startVisibleLoads();
1214
1647
  }
1215
1648
  function canonicalExpanded(expanded, candidate = state) {
1216
1649
  if (expandMode === "multiple") return expanded;
1217
1650
  if (expanded !== true) return expanded.slice(-1);
1218
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
+ }
1219
1657
  const indexed = indexRows(data, byId);
1220
1658
  const first = (manual ? indexed.rows : sortStage(filterStage(indexed.rows, columns, candidate.search, candidate.filters), columns, candidate.sorting)).find((row) => row.canExpand);
1221
1659
  return first ? [first.key] : [];
@@ -1262,6 +1700,9 @@ function createTable(options) {
1262
1700
  function applyChanges(edits, record, recoveredIssues = []) {
1263
1701
  const current = getSnapshot();
1264
1702
  const working = [...data];
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;
1265
1706
  const issues = [...recoveredIssues];
1266
1707
  const changes = [];
1267
1708
  const samples = /* @__PURE__ */ new Map();
@@ -1299,7 +1740,22 @@ function createTable(options) {
1299
1740
  }));
1300
1741
  continue;
1301
1742
  }
1302
- 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
+ }
1303
1759
  if (record && !column.isEditable(original)) {
1304
1760
  issues.push(issue("read_only_cell", `The "${column.id}" cell of row "${String(row.key)}" is read-only.`, {
1305
1761
  rowKey: row.key,
@@ -1329,7 +1785,21 @@ function createTable(options) {
1329
1785
  }));
1330
1786
  continue;
1331
1787
  }
1332
- 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
+ }
1333
1803
  changes.push({
1334
1804
  rowKey: row.key,
1335
1805
  column: column.id,
@@ -1372,7 +1842,7 @@ function createTable(options) {
1372
1842
  return edits;
1373
1843
  }
1374
1844
  function replay(changes, direction) {
1375
- applyChanges(changes.map((change) => ({
1845
+ applyChanges((direction === "undo" ? changes.toReversed() : changes).map((change) => ({
1376
1846
  rowKey: change.rowKey,
1377
1847
  column: change.column,
1378
1848
  value: direction === "undo" ? change.previous : change.value
@@ -1397,14 +1867,24 @@ function createTable(options) {
1397
1867
  const next = mergePatch(patch);
1398
1868
  if (next !== state || previousIssue !== expandedIssue) {
1399
1869
  state = next;
1870
+ snapshot = void 0;
1871
+ abortLoads();
1400
1872
  notify();
1873
+ startVisibleLoads();
1401
1874
  }
1402
1875
  },
1403
1876
  setData(next) {
1404
1877
  if (next === data) return;
1878
+ abortLoads(true);
1879
+ lazyChildren.clear();
1880
+ childStatuses.clear();
1881
+ loadIssues.clear();
1882
+ loadValidationIssues.clear();
1883
+ lazyVersion++;
1405
1884
  data = next;
1406
1885
  undoStack.clear();
1407
1886
  notify();
1887
+ startVisibleLoads();
1408
1888
  },
1409
1889
  setColumns(next) {
1410
1890
  if (next === definitions) return;
@@ -1475,14 +1955,14 @@ function createTable(options) {
1475
1955
  } });
1476
1956
  },
1477
1957
  select(keys) {
1478
- applySelection([...state.selection, ...keys]);
1958
+ applySelection([...state.selection, ...selectionKeys(keys)]);
1479
1959
  },
1480
1960
  deselect(keys) {
1481
- const removed = new Set(keys);
1961
+ const removed = new Set(selectionKeys(keys));
1482
1962
  applySelection(state.selection.filter((key) => !removed.has(key)));
1483
1963
  },
1484
1964
  toggleRow(key, selected) {
1485
- const isSelected = state.selection.includes(key);
1965
+ const isSelected = treeEnabled ? getSnapshot().getRow(key)?.selection === "all" : state.selection.includes(key);
1486
1966
  const next = selected ?? !isSelected;
1487
1967
  if (next === isSelected) return;
1488
1968
  if (next) table.select([key]);
@@ -1509,12 +1989,16 @@ function createTable(options) {
1509
1989
  const row = getSnapshot().getRow(key);
1510
1990
  if (!row?.canExpand) return;
1511
1991
  const open = expanded ?? !row.isExpanded;
1512
- if (open === row.isExpanded) return;
1992
+ if (open === row.isExpanded) {
1993
+ if (open && row.childStatus === "error") requestChildren(row);
1994
+ return;
1995
+ }
1513
1996
  if (expandMode === "single") commit({ expanded: open ? [key] : [] });
1514
1997
  else if (state.expanded === true) {
1515
1998
  const { byId } = resolveColumns(definitions);
1516
- commit({ expanded: indexRows(data, byId).rows.filter((candidate) => candidate.canExpand && candidate.key !== key).map((candidate) => candidate.key) });
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) });
1517
2000
  } else commit({ expanded: open ? [...state.expanded, key] : state.expanded.filter((candidate) => candidate !== key) });
2001
+ if (open) requestChildren(getSnapshot().getRow(key));
1518
2002
  },
1519
2003
  expandAll() {
1520
2004
  commit({ expanded: true });
@@ -1595,26 +2079,34 @@ function createTable(options) {
1595
2079
  return row !== void 0 && current.getColumn(edit.column)?.isEditable(row.original) === true;
1596
2080
  }), true);
1597
2081
  },
1598
- exportRows({ format = "csv", columns: ids, pageOnly = false, headers = true, escapeFormulas = true } = {}) {
2082
+ exportRows({ format = "csv", columns: ids, pageOnly = false, headers = true, escapeFormulas = true, depth = false, indent = "" } = {}) {
1599
2083
  const current = getSnapshot();
1600
2084
  const columns = ids ? ids.flatMap((id) => {
1601
2085
  const column = current.getColumn(id);
1602
2086
  return column ? [column] : [];
1603
2087
  }) : current.columns;
1604
- const matrix = (pageOnly ? current.rows : current.processedRows).map((row) => columns.map((column) => {
1605
- const display = row.getDisplay(column.id);
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);
1606
2091
  return escapeFormulas ? escapeFormula(display, row.getValue(column.id)) : display;
1607
2092
  }));
1608
- if (headers) matrix.unshift(columns.map((column) => escapeFormulas ? escapeFormula(column.header) : column.header));
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
+ }
1609
2098
  return toDelimited(matrix, format === "csv" ? "," : " ");
1610
2099
  },
1611
2100
  reset() {
1612
2101
  commit(initialState);
1613
2102
  },
1614
2103
  destroy() {
2104
+ destroyed = true;
2105
+ abortLoads(true);
1615
2106
  listeners.clear();
1616
2107
  }
1617
2108
  };
2109
+ startVisibleLoads();
1618
2110
  return table;
1619
2111
  }
1620
2112