@vueye-table/core 3.0.0-alpha.2 → 3.0.0-alpha.4

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,13 @@ 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",
901
+ isDirty: false,
902
+ cellIssues: void 0,
896
903
  getValue,
897
904
  getDisplay(columnId) {
898
905
  const cached = displays.get(columnId);
@@ -961,6 +968,576 @@ function freezeState(state, previous) {
961
968
  });
962
969
  }
963
970
 
971
+ //#endregion
972
+ //#region src/validation.ts
973
+ function isPromise(value) {
974
+ return typeof value === "object" && value !== null && "then" in value;
975
+ }
976
+ /** Validators receive values; their messages remain plain text. */
977
+ function runValidation(validate, rowKey, column) {
978
+ const fail = (message, validationCode) => issue("validation_failed", message, {
979
+ rowKey,
980
+ column,
981
+ validationCode
982
+ });
983
+ const read = (result) => {
984
+ if (result === true) return void 0;
985
+ if (typeof result === "string") return fail(result);
986
+ if (result && typeof result.message === "string") return fail(result.message, result.code);
987
+ return fail("The validator returned an invalid result.");
988
+ };
989
+ const thrown = (error) => fail(error instanceof Error ? error.message : String(error));
990
+ try {
991
+ const result = validate();
992
+ return isPromise(result) ? Promise.resolve(result).then(read, thrown) : read(result);
993
+ } catch (error) {
994
+ return thrown(error);
995
+ }
996
+ }
997
+ function editorFailure(spec, value) {
998
+ if (!spec) return void 0;
999
+ if (spec.maxLength !== void 0 && (!Number.isSafeInteger(spec.maxLength) || spec.maxLength < 0)) return "Editor maxLength must be a non-negative safe integer.";
1000
+ if (spec.min !== void 0 && !Number.isFinite(spec.min) || spec.max !== void 0 && !Number.isFinite(spec.max) || spec.min !== void 0 && spec.max !== void 0 && spec.min > spec.max) return "Editor bounds must be finite and ordered.";
1001
+ if (spec.options && !spec.options.some((option) => Object.is(option, value))) return "Choose one of the allowed values.";
1002
+ if (spec.maxLength !== void 0 && typeof value === "string" && value.length > spec.maxLength) return `Use at most ${spec.maxLength} characters.`;
1003
+ if (spec.min !== void 0 || spec.max !== void 0) {
1004
+ const number = value instanceof Date ? value.getTime() : value;
1005
+ if (typeof number !== "number" || !Number.isFinite(number)) return "Enter a finite numeric value.";
1006
+ if (spec.min !== void 0 && number < spec.min) return `Use a value of at least ${spec.min}.`;
1007
+ if (spec.max !== void 0 && number > spec.max) return `Use a value of at most ${spec.max}.`;
1008
+ }
1009
+ if (spec.pattern !== void 0) try {
1010
+ if (typeof value !== "string" || !new RegExp(spec.pattern, "u").test(value)) return "The value does not match the required pattern.";
1011
+ } catch {
1012
+ return "The editor pattern is invalid.";
1013
+ }
1014
+ }
1015
+
1016
+ //#endregion
1017
+ //#region src/edit-validation.ts
1018
+ function readonlyMap(source) {
1019
+ const copy = new Map(source);
1020
+ const result = {
1021
+ size: copy.size,
1022
+ get: (key) => copy.get(key),
1023
+ has: (key) => copy.has(key),
1024
+ entries: () => copy.entries(),
1025
+ keys: () => copy.keys(),
1026
+ values: () => copy.values(),
1027
+ [Symbol.iterator]: () => copy[Symbol.iterator](),
1028
+ forEach(callback, thisArg) {
1029
+ copy.forEach((value, key) => callback.call(thisArg, value, key, result));
1030
+ }
1031
+ };
1032
+ return Object.freeze(result);
1033
+ }
1034
+ /** All promises in one edit settle together. Tokens fence every cell against stale results. */
1035
+ function createEditValidation(host) {
1036
+ const tokens = /* @__PURE__ */ new Map();
1037
+ const pending = /* @__PURE__ */ new Map();
1038
+ const problems = /* @__PURE__ */ new Map();
1039
+ const current = (candidate) => tokens.get(candidate.edit.rowKey)?.get(candidate.edit.column) === candidate.token;
1040
+ const tokenFor = (rowKey, column) => {
1041
+ const row = tokens.get(rowKey) ?? /* @__PURE__ */ new Map();
1042
+ const token = {};
1043
+ row.set(column, token);
1044
+ tokens.set(rowKey, row);
1045
+ pending.get(rowKey)?.delete(column);
1046
+ problems.get(rowKey)?.delete(column);
1047
+ return token;
1048
+ };
1049
+ const setIssues = (issues) => {
1050
+ for (const problem of issues) {
1051
+ if (problem.rowKey === void 0 || problem.column === void 0) continue;
1052
+ const row = problems.get(problem.rowKey) ?? /* @__PURE__ */ new Map();
1053
+ row.set(problem.column, problem);
1054
+ problems.set(problem.rowKey, row);
1055
+ }
1056
+ };
1057
+ function rowValidation(candidates, failures) {
1058
+ if (!host.validateRow) return [...candidates];
1059
+ const rows = /* @__PURE__ */ new Map();
1060
+ for (const candidate of candidates) {
1061
+ const group = rows.get(candidate.edit.rowKey) ?? [];
1062
+ group.push(candidate);
1063
+ rows.set(candidate.edit.rowKey, group);
1064
+ }
1065
+ const results = [];
1066
+ for (const [key, group] of rows) {
1067
+ const previous = group[0].original;
1068
+ let next = previous;
1069
+ try {
1070
+ for (const candidate of group) next = candidate.column.setValue(next, candidate.edit.value) ?? next;
1071
+ } catch (error) {
1072
+ for (const candidate of group) failures.push(issue("validation_failed", String(error), {
1073
+ rowKey: key,
1074
+ column: candidate.edit.column
1075
+ }));
1076
+ continue;
1077
+ }
1078
+ const validation = runValidation(() => host.validateRow(next, previous), key);
1079
+ const accept = (problem) => {
1080
+ if (!problem) return group;
1081
+ for (const candidate of group) if (current(candidate)) failures.push(Object.freeze({
1082
+ ...problem,
1083
+ column: candidate.edit.column
1084
+ }));
1085
+ return [];
1086
+ };
1087
+ results.push(isPromise(validation) ? validation.then(accept) : accept(validation));
1088
+ }
1089
+ return results.some(isPromise) ? Promise.all(results.map((result) => Promise.resolve(result))).then((groups) => groups.flat()) : results.flat();
1090
+ }
1091
+ function settle(accepted, all, failures, asynchronous) {
1092
+ return host.batch(() => {
1093
+ const live = accepted.filter(current);
1094
+ const liveKeys = new Set(live);
1095
+ const active = all.filter(current);
1096
+ for (const candidate of active) {
1097
+ const key = candidate.edit.rowKey;
1098
+ pending.get(key)?.delete(candidate.edit.column);
1099
+ if (pending.get(key)?.size === 0) pending.delete(key);
1100
+ }
1101
+ const visibleFailures = failures;
1102
+ setIssues(visibleFailures);
1103
+ let result;
1104
+ if (asynchronous && host.optimistic) {
1105
+ const rollback = active.filter((candidate) => !liveKeys.has(candidate)).map((candidate) => ({
1106
+ ...candidate.edit,
1107
+ value: candidate.previous
1108
+ }));
1109
+ result = host.apply(rollback, false, visibleFailures);
1110
+ const changes = live.map((candidate) => Object.freeze({
1111
+ rowKey: candidate.edit.rowKey,
1112
+ column: candidate.edit.column,
1113
+ previous: candidate.previous,
1114
+ value: candidate.edit.value,
1115
+ row: host.snapshot().getRow(candidate.edit.rowKey).original
1116
+ }));
1117
+ if (changes.length) host.record(changes);
1118
+ result = Object.freeze({
1119
+ ...result,
1120
+ changes: Object.freeze(changes),
1121
+ status: changes.length ? visibleFailures.length ? "partial" : "applied" : visibleFailures.length ? "rejected" : "unchanged"
1122
+ });
1123
+ } else result = host.apply(live.map((candidate) => candidate.edit), true, visibleFailures);
1124
+ setIssues(result.issues);
1125
+ if (!result.changes.length && (all.length || result.issues.length) || asynchronous && host.optimistic) host.notify();
1126
+ if (asynchronous && !result.issues.length && active.length) host.onIssues([]);
1127
+ return result;
1128
+ });
1129
+ }
1130
+ return {
1131
+ cells: () => Object.freeze([...pending.values()].flatMap((row) => [...row.values()])),
1132
+ problems: () => new Map([...problems].filter(([, row]) => row.size).map(([key, row]) => [key, readonlyMap(row)])),
1133
+ cancel(keys) {
1134
+ if (!keys) {
1135
+ tokens.clear();
1136
+ pending.clear();
1137
+ problems.clear();
1138
+ } else for (const key of keys) {
1139
+ tokens.delete(key);
1140
+ pending.delete(key);
1141
+ problems.delete(key);
1142
+ }
1143
+ },
1144
+ edit(edits, recovered = []) {
1145
+ const metadataChanged = edits.some((edit) => pending.get(edit.rowKey)?.has(edit.column) || problems.get(edit.rowKey)?.has(edit.column) || host.validateRow && pending.get(edit.rowKey)?.size);
1146
+ const snapshot = host.snapshot();
1147
+ const failures = [...recovered];
1148
+ const candidates = [];
1149
+ const drafts = /* @__PURE__ */ new Map();
1150
+ const samples = /* @__PURE__ */ new Map();
1151
+ if (host.validateRow) for (const key of new Set(edits.map((edit) => edit.rowKey))) {
1152
+ tokens.delete(key);
1153
+ pending.delete(key);
1154
+ }
1155
+ for (const edit of edits) {
1156
+ const token = tokenFor(edit.rowKey, edit.column);
1157
+ const row = snapshot.getRow(edit.rowKey);
1158
+ const column = snapshot.getColumn(edit.column);
1159
+ const fail = (code, message) => {
1160
+ failures.push(issue(code, message, {
1161
+ rowKey: edit.rowKey,
1162
+ column: edit.column
1163
+ }));
1164
+ };
1165
+ if (!isSafePath(edit.column)) {
1166
+ fail("unsafe_path", "This path addresses a prototype and cannot be written.");
1167
+ continue;
1168
+ }
1169
+ if (!row) {
1170
+ fail("unknown_row", `No row has the key "${String(edit.rowKey)}".`);
1171
+ continue;
1172
+ }
1173
+ if (!column) {
1174
+ fail("unknown_column", `No column has the id "${edit.column}".`);
1175
+ continue;
1176
+ }
1177
+ const original = drafts.get(row.key) ?? row.original;
1178
+ try {
1179
+ if (!column.isEditable(original)) {
1180
+ fail("read_only_cell", `The "${column.id}" cell is read-only.`);
1181
+ continue;
1182
+ }
1183
+ let value;
1184
+ if ("input" in edit) {
1185
+ if (!samples.has(column.id)) samples.set(column.id, snapshot.processedRows.find((candidate) => candidate.getValue(column.id) !== null && candidate.getValue(column.id) !== void 0)?.getValue(column.id));
1186
+ const parsed = column.parse(edit.input, original, samples.get(column.id));
1187
+ if (!parsed.ok) {
1188
+ fail("invalid_value", parsed.message);
1189
+ continue;
1190
+ }
1191
+ value = parsed.value;
1192
+ } else value = edit.value;
1193
+ const previous = column.getValue(original);
1194
+ if (Object.is(previous, value)) continue;
1195
+ const constraint = editorFailure(column.definition.editor, value);
1196
+ if (constraint) {
1197
+ fail("validation_failed", constraint);
1198
+ continue;
1199
+ }
1200
+ const next = column.setValue(original, value);
1201
+ if (next === void 0) {
1202
+ fail("read_only_cell", "The column has no way to write a value.");
1203
+ continue;
1204
+ }
1205
+ const writeProblem = host.writeIssue(row, next);
1206
+ if (writeProblem) {
1207
+ failures.push(Object.freeze({
1208
+ ...writeProblem,
1209
+ column: column.id
1210
+ }));
1211
+ continue;
1212
+ }
1213
+ const validate = column.definition.validate;
1214
+ const validation = validate ? runValidation(() => validate(value, row.original), row.key, column.id) : void 0;
1215
+ if (validation && !isPromise(validation)) {
1216
+ failures.push(validation);
1217
+ continue;
1218
+ }
1219
+ drafts.set(row.key, next);
1220
+ candidates.push({
1221
+ edit: {
1222
+ rowKey: row.key,
1223
+ column: column.id,
1224
+ value
1225
+ },
1226
+ previous: column.getValue(row.original),
1227
+ original: row.original,
1228
+ column,
1229
+ validation,
1230
+ token
1231
+ });
1232
+ } catch (error) {
1233
+ fail("invalid_value", error instanceof Error ? error.message : String(error));
1234
+ }
1235
+ }
1236
+ const asynchronous = candidates.some((candidate) => isPromise(candidate.validation));
1237
+ const validateRows = (accepted) => rowValidation(accepted.filter(current), failures);
1238
+ const outcome = asynchronous ? Promise.all(candidates.map(async (candidate) => {
1239
+ const problem = await candidate.validation;
1240
+ if (problem) {
1241
+ if (current(candidate)) failures.push(problem);
1242
+ return;
1243
+ }
1244
+ return candidate;
1245
+ })).then((accepted) => validateRows(accepted.filter((candidate) => candidate !== void 0))) : validateRows(candidates);
1246
+ if (!isPromise(outcome)) {
1247
+ const result = settle(outcome, candidates, failures, false);
1248
+ if (metadataChanged && !candidates.length && !failures.length) host.notify();
1249
+ return result;
1250
+ }
1251
+ for (const candidate of candidates) {
1252
+ const row = pending.get(candidate.edit.rowKey) ?? /* @__PURE__ */ new Map();
1253
+ row.set(candidate.edit.column, Object.freeze({ ...candidate.edit }));
1254
+ pending.set(candidate.edit.rowKey, row);
1255
+ }
1256
+ setIssues(failures);
1257
+ const optimistic = host.optimistic ? host.apply(candidates.map((candidate) => candidate.edit), false, failures) : void 0;
1258
+ if (!optimistic?.changes.length) host.notify();
1259
+ if (!host.optimistic && failures.length) host.onIssues(failures);
1260
+ outcome.then((accepted) => {
1261
+ if (candidates.some(current)) settle(accepted, candidates, failures, true);
1262
+ });
1263
+ return Object.freeze({
1264
+ status: "pending",
1265
+ changes: optimistic?.changes ?? Object.freeze([]),
1266
+ rowChanges: Object.freeze([]),
1267
+ issues: Object.freeze([...failures]),
1268
+ pendingCells: Object.freeze(candidates.map((candidate) => Object.freeze({ ...candidate.edit })))
1269
+ });
1270
+ }
1271
+ };
1272
+ }
1273
+
1274
+ //#endregion
1275
+ //#region src/pending-changes.ts
1276
+ /** A saved baseline only for touched rows; viewing changes never scans source data. */
1277
+ function createPendingChanges() {
1278
+ const entries = /* @__PURE__ */ new Map();
1279
+ return {
1280
+ entries,
1281
+ snapshot() {
1282
+ const inserted = [];
1283
+ const removed = [];
1284
+ const updated = [];
1285
+ for (const [rowKey, entry] of entries) if (entry.previous === void 0 && entry.row !== void 0) inserted.push(Object.freeze({
1286
+ kind: "inserted",
1287
+ rowKey,
1288
+ row: entry.row,
1289
+ position: entry.position
1290
+ }));
1291
+ else if (entry.row === void 0 && entry.previous !== void 0) removed.push(Object.freeze({
1292
+ kind: "removed",
1293
+ rowKey,
1294
+ row: entry.previous,
1295
+ position: entry.position
1296
+ }));
1297
+ else if (entry.row !== void 0 && entry.previous !== void 0) updated.push(Object.freeze({
1298
+ rowKey,
1299
+ row: entry.row,
1300
+ previous: entry.previous,
1301
+ changes: Object.freeze([...entry.cells.values()])
1302
+ }));
1303
+ return Object.freeze({
1304
+ inserted: Object.freeze(inserted),
1305
+ updated: Object.freeze(updated),
1306
+ removed: Object.freeze(removed)
1307
+ });
1308
+ },
1309
+ record(key, previous, row, position, changes) {
1310
+ const entry = entries.get(key) ?? {
1311
+ previous,
1312
+ row,
1313
+ position: Object.freeze({ ...position }),
1314
+ cells: /* @__PURE__ */ new Map()
1315
+ };
1316
+ entry.row = row;
1317
+ for (const change of changes) {
1318
+ const original = entry.cells.has(change.column) ? entry.cells.get(change.column).previous : change.previous;
1319
+ if (Object.is(original, change.value)) entry.cells.delete(change.column);
1320
+ else entry.cells.set(change.column, Object.freeze({
1321
+ ...change,
1322
+ previous: original
1323
+ }));
1324
+ }
1325
+ if (entry.previous === void 0 && row === void 0 || entry.previous !== void 0 && row !== void 0 && entry.cells.size === 0) entries.delete(key);
1326
+ else entries.set(key, entry);
1327
+ }
1328
+ };
1329
+ }
1330
+
1331
+ //#endregion
1332
+ //#region src/tree.ts
1333
+ function buildTree(data, columns, readKey, options, lazy) {
1334
+ const nodes = [];
1335
+ const byKey = /* @__PURE__ */ new Map();
1336
+ const issues = [];
1337
+ const nested = options.getChildren !== void 0;
1338
+ let maxDepth = options.maxDepth ?? 1e3;
1339
+ if (!Number.isSafeInteger(maxDepth) || maxDepth < 1) {
1340
+ issues.push(issue("invalid_tree_option", "maxDepth must be a positive safe integer; 1,000 is used."));
1341
+ maxDepth = 1e3;
1342
+ }
1343
+ if (nested && options.getParentKey) issues.push(issue("invalid_tree_option", "Both tree inputs were supplied; getChildren takes precedence."));
1344
+ const seen = /* @__PURE__ */ new Set();
1345
+ const stack = data.map((original, sourceIndex) => ({
1346
+ original,
1347
+ sourceIndex,
1348
+ parent: void 0,
1349
+ lazy: false
1350
+ })).reverse();
1351
+ while (stack.length) {
1352
+ const entry = stack.pop();
1353
+ const index = nodes.length;
1354
+ let key = readKey(entry.original, index);
1355
+ let parent = entry.parent;
1356
+ const reference = entry.original !== null && (typeof entry.original === "object" || typeof entry.original === "function");
1357
+ const repeated = nested && reference && seen.has(entry.original);
1358
+ if (repeated) {
1359
+ issues.push(issue("tree_cycle", "A repeated nested value recovers as a root without traversing it again.", { rowKey: key }));
1360
+ parent = void 0;
1361
+ }
1362
+ if (reference) seen.add(entry.original);
1363
+ if (byKey.has(key)) {
1364
+ 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 }));
1365
+ if (entry.lazy) continue;
1366
+ const prefix = `${String(key)}#${index}`;
1367
+ key = prefix;
1368
+ let suffix = 0;
1369
+ while (byKey.has(key)) key = `${prefix}#${++suffix}`;
1370
+ }
1371
+ if (parent && parent.depth + 1 > maxDepth) {
1372
+ issues.push(issue("tree_depth_exceeded", "The maximum tree depth was exceeded; this row recovers as a root.", { rowKey: key }));
1373
+ parent = void 0;
1374
+ }
1375
+ const children = repeated ? void 0 : lazy.get(key) ?? options.getChildren?.(entry.original);
1376
+ const node = {
1377
+ row: createRow(entry.original, key, index, columns),
1378
+ parent,
1379
+ children: [],
1380
+ depth: parent ? parent.depth + 1 : 0,
1381
+ sourceIndex: entry.sourceIndex,
1382
+ lazy: entry.lazy,
1383
+ writable: parent?.writable ?? entry.parent === void 0,
1384
+ unloaded: children === void 0 && (options.hasChildren?.(entry.original) ?? false)
1385
+ };
1386
+ nodes.push(node);
1387
+ byKey.set(key, node);
1388
+ if (parent) parent.children.push(node);
1389
+ if (children) for (let i = children.length - 1; i >= 0; i--) stack.push({
1390
+ original: children[i],
1391
+ sourceIndex: i,
1392
+ parent: node,
1393
+ lazy: entry.lazy || lazy.has(key)
1394
+ });
1395
+ }
1396
+ if (!nested && options.getParentKey) {
1397
+ for (const node of nodes) {
1398
+ if (node.lazy) continue;
1399
+ const key = options.getParentKey(node.row.original);
1400
+ if (key === void 0 || key === null) continue;
1401
+ const parent = byKey.get(key);
1402
+ if (!parent) issues.push(issue("tree_orphan", "The parent is missing; this row recovers as a root.", { rowKey: node.row.key }));
1403
+ else node.parent = parent;
1404
+ }
1405
+ const done = /* @__PURE__ */ new Set();
1406
+ for (const node of nodes) {
1407
+ const path = [];
1408
+ const visiting = /* @__PURE__ */ new Set();
1409
+ let cursor = node;
1410
+ while (cursor && !done.has(cursor)) {
1411
+ if (visiting.has(cursor)) {
1412
+ issues.push(issue("tree_cycle", "A cyclic parent reference is detached to a root.", { rowKey: cursor.row.key }));
1413
+ cursor.parent = void 0;
1414
+ break;
1415
+ }
1416
+ visiting.add(cursor);
1417
+ path.push(cursor);
1418
+ cursor = cursor.parent;
1419
+ }
1420
+ for (let i = path.length - 1; i >= 0; i--) {
1421
+ const current = path[i];
1422
+ done.add(current);
1423
+ }
1424
+ }
1425
+ for (const node of nodes) node.children = [];
1426
+ for (const node of nodes) if (node.parent) node.parent.children.push(node);
1427
+ const pending = nodes.filter((node) => !node.parent);
1428
+ while (pending.length) {
1429
+ const node = pending.pop();
1430
+ node.depth = node.parent ? node.parent.depth + 1 : 0;
1431
+ if (node.depth > maxDepth) {
1432
+ node.parent = void 0;
1433
+ node.depth = 0;
1434
+ issues.push(issue("tree_depth_exceeded", "The maximum tree depth was exceeded; this row recovers as a root.", { rowKey: node.row.key }));
1435
+ }
1436
+ for (const child of node.children) pending.push(child);
1437
+ }
1438
+ for (const node of nodes) node.children = node.children.filter((child) => child.parent === node);
1439
+ }
1440
+ return {
1441
+ nodes,
1442
+ rows: nodes.map((node) => node.row),
1443
+ roots: nodes.filter((node) => !node.parent),
1444
+ byKey,
1445
+ issues
1446
+ };
1447
+ }
1448
+ function processTree(model, columns, state, mode, manual) {
1449
+ const raw = model.nodes.map((node) => node.row);
1450
+ const matched = manual ? raw : filterRows(raw, columns, state.search, state.filters);
1451
+ const active = matched !== raw;
1452
+ const included = new Set(matched.map((row) => row.key));
1453
+ const autoExpanded = /* @__PURE__ */ new Set();
1454
+ if (active && mode !== "strict") {
1455
+ if (mode === "descendants") {
1456
+ const stack = matched.map((row) => model.byKey.get(row.key));
1457
+ const visited = /* @__PURE__ */ new Set();
1458
+ while (stack.length) {
1459
+ const node = stack.pop();
1460
+ if (visited.has(node.row.key)) continue;
1461
+ visited.add(node.row.key);
1462
+ included.add(node.row.key);
1463
+ if (node.children.length) autoExpanded.add(node.row.key);
1464
+ for (const child of node.children) stack.push(child);
1465
+ }
1466
+ }
1467
+ for (const row of matched) {
1468
+ let parent = model.byKey.get(row.key).parent;
1469
+ while (parent && !autoExpanded.has(parent.row.key)) {
1470
+ included.add(parent.row.key);
1471
+ autoExpanded.add(parent.row.key);
1472
+ parent = parent.parent;
1473
+ }
1474
+ }
1475
+ }
1476
+ if (active && mode === "strict") return {
1477
+ model,
1478
+ roots: ordered(model.nodes.filter((node) => included.has(node.row.key) && node.children.length === 0 && !node.unloaded)),
1479
+ children: /* @__PURE__ */ new Map(),
1480
+ autoExpanded
1481
+ };
1482
+ const children = /* @__PURE__ */ new Map();
1483
+ for (const node of model.nodes) if (node.children.length) children.set(node, ordered(node.children.filter((child) => included.has(child.row.key))));
1484
+ return {
1485
+ model,
1486
+ roots: ordered(model.roots.filter((node) => included.has(node.row.key))),
1487
+ children,
1488
+ autoExpanded
1489
+ };
1490
+ function ordered(nodes) {
1491
+ if (manual || !state.sorting.length || nodes.length < 2) return nodes;
1492
+ return sortRows(nodes.map((node) => node.row), columns, state.sorting).map((row) => model.byKey.get(row.key));
1493
+ }
1494
+ }
1495
+ /** Expansion changes reuse filtering and sibling order, splicing only a changed subtree. */
1496
+ function createTreeFlattener() {
1497
+ let previous;
1498
+ let previousExpanded = [];
1499
+ let visible = [];
1500
+ let previousAncestors;
1501
+ return (tree, expanded, ancestors) => {
1502
+ if (tree === previous && expanded === previousExpanded && ancestors === previousAncestors) return visible;
1503
+ const keys = expanded === true ? void 0 : new Set(expanded);
1504
+ const open = (node) => expanded === true || (keys?.has(node.row.key) ?? false) || ancestors.has(node.row.key) || tree.autoExpanded.has(node.row.key);
1505
+ const walk = (roots) => {
1506
+ const output = [];
1507
+ const stack = [...roots].reverse();
1508
+ while (stack.length) {
1509
+ const node = stack.pop();
1510
+ output.push(node);
1511
+ if (open(node)) {
1512
+ const children = tree.children.get(node) ?? [];
1513
+ for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]);
1514
+ }
1515
+ }
1516
+ return output;
1517
+ };
1518
+ let changed;
1519
+ if (previous === tree && ancestors === previousAncestors && expanded !== true && previousExpanded !== true) {
1520
+ const before = new Set(previousExpanded);
1521
+ const after = new Set(expanded);
1522
+ changed = [...previousExpanded.filter((key) => !after.has(key)), ...expanded.filter((key) => !before.has(key))];
1523
+ }
1524
+ if (changed?.length === 1 && !tree.autoExpanded.has(changed[0])) {
1525
+ const index = visible.findIndex((node) => node.row.key === changed[0]);
1526
+ if (index >= 0) {
1527
+ const node = visible[index];
1528
+ let end = index + 1;
1529
+ while (end < visible.length && visible[end].depth > node.depth) end++;
1530
+ const inserted = walk(open(node) ? tree.children.get(node) ?? [] : []);
1531
+ visible = visible.slice(0, index + 1).concat(inserted, visible.slice(end));
1532
+ }
1533
+ } else visible = walk(tree.roots);
1534
+ previous = tree;
1535
+ previousExpanded = expanded;
1536
+ previousAncestors = ancestors;
1537
+ return visible;
1538
+ };
1539
+ }
1540
+
964
1541
  //#endregion
965
1542
  //#region src/table.ts
966
1543
  function isRecord(value) {
@@ -998,7 +1575,17 @@ function createTable(options) {
998
1575
  const selectionMode = options.selectionMode ?? "multiple";
999
1576
  const selectScope = options.selectScope ?? "all";
1000
1577
  const manual = options.manual ?? false;
1578
+ const treeEnabled = Boolean(options.getChildren || options.getParentKey || options.hasChildren || options.loadChildren);
1579
+ const rootPagination = treeEnabled && (manual || (options.paginateBy ?? "root") === "root");
1580
+ let lazyVersion = 0;
1581
+ const lazyChildren = /* @__PURE__ */ new Map();
1582
+ const childStatuses = /* @__PURE__ */ new Map();
1583
+ const loadIssues = /* @__PURE__ */ new Map();
1584
+ const loadValidationIssues = /* @__PURE__ */ new Map();
1585
+ const pendingLoads = /* @__PURE__ */ new Map();
1586
+ let destroyed = false;
1001
1587
  const undoStack = createUndoStack(options.historyLimit);
1588
+ const pendingChanges = createPendingChanges();
1002
1589
  const listeners = /* @__PURE__ */ new Set();
1003
1590
  const optionIssues = [];
1004
1591
  const maxPasteCells = pasteLimit(options.pasteLimit?.maxCells, 1e5, "maxCells", optionIssues);
@@ -1015,6 +1602,36 @@ function createTable(options) {
1015
1602
  });
1016
1603
  let state = initialState;
1017
1604
  let snapshot;
1605
+ let transaction = false;
1606
+ let deferValidationNotify = false;
1607
+ let validationNotifyRequested = false;
1608
+ const validation = createEditValidation({
1609
+ snapshot: getSnapshot,
1610
+ apply: applyChanges,
1611
+ notify,
1612
+ writeIssue(row, next) {
1613
+ const node = treeEnabled ? treeStage(data, resolveColumns(definitions).byId, lazyVersion).byKey.get(row.key) : void 0;
1614
+ if (node && (!node.writable || node.parent && options.getChildren && !options.setChildren)) return issue("read_only_cell", "This nested row has no immutable writable source path.", { rowKey: row.key });
1615
+ if (readKey(next, row.index) !== readKey(row.original, row.index)) return issue("invalid_row_operation", "Editing a row key is not supported; remove and insert the row instead.", { rowKey: row.key });
1616
+ },
1617
+ batch(run) {
1618
+ deferValidationNotify = true;
1619
+ validationNotifyRequested = false;
1620
+ try {
1621
+ return run();
1622
+ } finally {
1623
+ deferValidationNotify = false;
1624
+ if (validationNotifyRequested) notify();
1625
+ }
1626
+ },
1627
+ record: (changes) => undoStack.record({
1628
+ kind: "cells",
1629
+ changes
1630
+ }),
1631
+ onIssues: (issues) => options.onEditIssues?.(issues),
1632
+ optimistic: options.asyncValidation === "optimistic",
1633
+ validateRow: options.validateRow
1634
+ });
1018
1635
  const readKey = (row, index) => {
1019
1636
  const { rowKey } = options;
1020
1637
  if (typeof rowKey === "function") return rowKey(row, index);
@@ -1055,6 +1672,77 @@ function createTable(options) {
1055
1672
  issues
1056
1673
  };
1057
1674
  });
1675
+ const treeStage = memo((rows, byId, _version) => buildTree(rows, byId, readKey, options, lazyChildren));
1676
+ const treeIndex = memo((model) => ({
1677
+ rows: model.rows,
1678
+ byKey: new Map(model.rows.map((row) => [row.key, row])),
1679
+ issues: model.issues
1680
+ }));
1681
+ const treePositions = memo((model) => {
1682
+ const result = /* @__PURE__ */ new Map();
1683
+ const index = (siblings, parent) => {
1684
+ siblings.forEach((node, position) => result.set(node.row.key, Object.freeze({
1685
+ parent,
1686
+ before: siblings[position + 1]?.row.key
1687
+ })));
1688
+ };
1689
+ index(model.roots);
1690
+ for (const node of model.nodes) index(node.children, node.row.key);
1691
+ return result;
1692
+ });
1693
+ const processedTreeStage = memo((model, columns, search, filters, sorting) => processTree(model, columns, {
1694
+ ...state,
1695
+ search,
1696
+ filters,
1697
+ sorting
1698
+ }, options.treeFilter ?? "ancestors", manual));
1699
+ const flattenTree = createTreeFlattener();
1700
+ const emptyAncestors = /* @__PURE__ */ new Set();
1701
+ const singleTreePath = memo((model, expanded) => {
1702
+ const ancestors = /* @__PURE__ */ new Set();
1703
+ if (expanded !== true) for (const key of expanded) {
1704
+ let parent = model.byKey.get(key)?.parent;
1705
+ while (parent && !ancestors.has(parent.row.key)) {
1706
+ ancestors.add(parent.row.key);
1707
+ parent = parent.parent;
1708
+ }
1709
+ }
1710
+ return ancestors;
1711
+ });
1712
+ const treeSelection = memo((model, selection) => {
1713
+ const selected = new Set(selection);
1714
+ const counts = /* @__PURE__ */ new Map();
1715
+ const stack = model.roots.map((node) => ({
1716
+ node,
1717
+ exit: false
1718
+ }));
1719
+ while (stack.length) {
1720
+ const { node, exit } = stack.pop();
1721
+ if (!exit) {
1722
+ stack.push({
1723
+ node,
1724
+ exit: true
1725
+ });
1726
+ for (const child of node.children) stack.push({
1727
+ node: child,
1728
+ exit: false
1729
+ });
1730
+ } else {
1731
+ let count = selected.has(node.row.key) ? 1 : 0;
1732
+ let total = 1;
1733
+ for (const child of node.children) {
1734
+ const value = counts.get(child);
1735
+ count += value.selected;
1736
+ total += value.total;
1737
+ }
1738
+ counts.set(node, {
1739
+ selected: count,
1740
+ total
1741
+ });
1742
+ }
1743
+ }
1744
+ return counts;
1745
+ });
1058
1746
  const orderColumns = memo((columns, columnOrder, hiddenColumns) => {
1059
1747
  const position = new Map(columnOrder.map((id, index) => [id, index]));
1060
1748
  const ordered = columns.map((column, index) => ({
@@ -1072,12 +1760,17 @@ function createTable(options) {
1072
1760
  const filterStage = memo(filterRows);
1073
1761
  const sortStage = memo(sortRows);
1074
1762
  const selectedSet = memo((selection) => new Set(selection));
1075
- const expansionStage = memo((indexed, expanded) => {
1763
+ const expansionStage = memo((indexed, expanded, selection) => {
1076
1764
  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);
1765
+ const selected = new Set(selection);
1766
+ const rows = indexed.rows.map((row) => {
1767
+ const isExpanded = row.canExpand && (expanded === true || (keys?.has(row.key) ?? false));
1768
+ return isExpanded || selected.has(row.key) ? Object.freeze({
1769
+ ...row,
1770
+ isExpanded,
1771
+ selection: selected.has(row.key) ? "all" : "none"
1772
+ }) : row;
1773
+ });
1081
1774
  return {
1082
1775
  rows,
1083
1776
  byKey: new Map(rows.map((row) => [row.key, row]))
@@ -1099,15 +1792,29 @@ function createTable(options) {
1099
1792
  return issues;
1100
1793
  }
1101
1794
  function computeSnapshot() {
1795
+ const cellProblems = validation.problems();
1796
+ const dirty = new Set(pendingChanges.entries.keys());
1797
+ const metadata = (row) => dirty.has(row.key) || cellProblems.has(row.key) ? Object.freeze({
1798
+ ...row,
1799
+ isDirty: dirty.has(row.key),
1800
+ cellIssues: cellProblems.get(row.key)
1801
+ }) : row;
1102
1802
  const { columns, byId, issues: columnIssues } = resolveColumns(definitions);
1103
- const indexed = indexRows(data, byId);
1104
- const expanded = expansionStage(indexed, state.expanded);
1803
+ const model = treeEnabled ? treeStage(data, byId, lazyVersion) : void 0;
1804
+ const indexed = model ? {
1805
+ rows: model.rows,
1806
+ issues: model.issues
1807
+ } : indexRows(data, byId);
1808
+ const expanded = model ? void 0 : expansionStage(indexed, state.expanded, state.selection);
1105
1809
  const ordered = orderColumns(columns, state.columnOrder, state.hiddenColumns);
1106
1810
  const issues = [
1107
1811
  ...optionIssues,
1108
1812
  ...columnIssues,
1109
1813
  ...indexed.issues,
1110
- ...stateIssues(byId)
1814
+ ...stateIssues(byId),
1815
+ ...loadIssues.values(),
1816
+ ...Array.from(loadValidationIssues.values()).flat(),
1817
+ ...Array.from(cellProblems.values()).flatMap((row) => [...row.values()])
1111
1818
  ];
1112
1819
  const requestedSize = state.pagination.pageSize;
1113
1820
  let pageSize = Number.isInteger(requestedSize) && requestedSize >= 1 ? requestedSize : 10;
@@ -1116,7 +1823,60 @@ function createTable(options) {
1116
1823
  let filteredCount;
1117
1824
  let page;
1118
1825
  let pageCount;
1119
- if (manual) {
1826
+ let treeRow;
1827
+ let totalCount = indexed.rows.length;
1828
+ if (model) {
1829
+ const processed = processedTreeStage(model, columns, state.search, state.filters, state.sorting);
1830
+ const ancestors = expandMode === "single" ? singleTreePath(model, state.expanded) : emptyAncestors;
1831
+ const visible = flattenTree(processed, state.expanded, ancestors);
1832
+ const expandedState = state.expanded;
1833
+ const keys = state.expanded === true ? void 0 : new Set(state.expanded);
1834
+ const selected = treeSelection(model, state.selection);
1835
+ const projected = /* @__PURE__ */ new Map();
1836
+ const statuses = new Map(childStatuses);
1837
+ treeRow = (key) => {
1838
+ const cached = projected.get(key);
1839
+ if (cached) return cached;
1840
+ const node = model.byKey.get(key);
1841
+ if (!node) return void 0;
1842
+ const status = statuses.get(key) ?? (node.children.length || !node.unloaded ? "loaded" : "idle");
1843
+ const count = selected.get(node);
1844
+ const canExpand = node.children.length > 0 || status !== "loaded" || (options.getRowCanExpand?.(node.row.original) ?? false);
1845
+ const result = Object.freeze({
1846
+ ...node.row,
1847
+ isDirty: dirty.has(key),
1848
+ cellIssues: cellProblems.get(key),
1849
+ depth: node.depth,
1850
+ parentKey: node.parent?.row.key,
1851
+ childCount: status === "loaded" ? node.children.length : void 0,
1852
+ childStatus: status,
1853
+ canExpand,
1854
+ isExpanded: canExpand && (expandedState === true || (keys?.has(key) ?? false) || ancestors.has(key) || processed.autoExpanded.has(key)),
1855
+ selection: coverage(count.selected, count.total)
1856
+ });
1857
+ projected.set(key, result);
1858
+ return result;
1859
+ };
1860
+ processedRows = visible.map((node) => treeRow(node.row.key));
1861
+ filteredCount = manual ? rowCount ?? model.roots.length : rootPagination ? processed.roots.length : visible.length;
1862
+ totalCount = rootPagination ? model.roots.length : model.nodes.length;
1863
+ pageCount = countPages(filteredCount, pageSize);
1864
+ page = clampPage(state.pagination.page, pageCount);
1865
+ if (manual) rows = processedRows;
1866
+ else if (rootPagination) {
1867
+ const roots = new Set(processed.roots.slice((page - 1) * pageSize, page * pageSize));
1868
+ const allRoots = new Set(processed.roots);
1869
+ let root;
1870
+ rows = processedRows.filter((_row, index) => {
1871
+ const node = visible[index];
1872
+ if (allRoots.has(node)) root = node;
1873
+ return root !== void 0 && roots.has(root);
1874
+ });
1875
+ } else rows = paginateRows(processedRows, {
1876
+ page,
1877
+ pageSize
1878
+ });
1879
+ } else if (manual) {
1120
1880
  processedRows = expanded.rows;
1121
1881
  rows = expanded.rows;
1122
1882
  filteredCount = rowCount ?? indexed.rows.length;
@@ -1139,6 +1899,20 @@ function createTable(options) {
1139
1899
  pageCount = 1;
1140
1900
  pageSize = Math.max(1, rows.length);
1141
1901
  }
1902
+ const flatProjected = /* @__PURE__ */ new Map();
1903
+ if (!model) {
1904
+ const project = (row) => {
1905
+ let result = flatProjected.get(row.key);
1906
+ if (!result) {
1907
+ result = metadata(row);
1908
+ flatProjected.set(row.key, result);
1909
+ }
1910
+ return result;
1911
+ };
1912
+ const shared = rows === processedRows;
1913
+ processedRows = processedRows.map(project);
1914
+ rows = shared ? processedRows : rows.map(project);
1915
+ }
1142
1916
  const selected = selectedSet(state.selection);
1143
1917
  const pageSelected = rows.filter((row) => selected.has(row.key)).length;
1144
1918
  const scopeRows = selectScope === "page" || manual ? rows : processedRows;
@@ -1160,7 +1934,7 @@ function createTable(options) {
1160
1934
  row,
1161
1935
  rowIndex
1162
1936
  });
1163
- return row.isExpanded ? [item, Object.freeze({
1937
+ return row.isExpanded && (!model || (options.getRowCanExpand?.(row.original) ?? false)) ? [item, Object.freeze({
1164
1938
  kind: "detail",
1165
1939
  key: getRowItemKey(row.key, "detail"),
1166
1940
  row,
@@ -1173,23 +1947,24 @@ function createTable(options) {
1173
1947
  pageSize,
1174
1948
  pageCount,
1175
1949
  rowCount: filteredCount,
1176
- totalRowCount: manual ? filteredCount : indexed.rows.length,
1950
+ totalRowCount: manual ? filteredCount : totalCount,
1177
1951
  pageStart,
1178
- pageEnd: rows.length === 0 ? 0 : pageStart + rows.length - 1,
1952
+ pageEnd: rows.length === 0 ? 0 : rootPagination ? Math.min(filteredCount, page * pageSize) : pageStart + rows.length - 1,
1179
1953
  pageItems: paginationItems(page, pageCount),
1180
1954
  canPreviousPage: page > 1,
1181
1955
  canNextPage: page < pageCount,
1182
1956
  selectionMode,
1183
- selectedCount: state.selection.length,
1957
+ selectedCount: model ? state.selection.filter((key) => model.byKey.has(key)).length : state.selection.length,
1184
1958
  pageSelection: coverage(pageSelected, rows.length),
1185
1959
  allSelection: coverage(scopeSelected, scopeRows.length),
1186
1960
  canUndo: undoStack.canUndo,
1187
1961
  canRedo: undoStack.canRedo,
1188
1962
  issues: Object.freeze(issues),
1963
+ pendingCells: validation.cells(),
1189
1964
  isSelected: (key) => selected.has(key),
1190
1965
  getSort: (columnId) => sortIndex.get(columnId),
1191
1966
  getColumn: (columnId) => byId.get(columnId),
1192
- getRow: (key) => expanded.byKey.get(key)
1967
+ getRow: (key) => treeRow ? treeRow(key) : flatProjected.get(key) ?? (expanded.byKey.get(key) ? metadata(expanded.byKey.get(key)) : void 0)
1193
1968
  });
1194
1969
  }
1195
1970
  function getSnapshot() {
@@ -1198,10 +1973,105 @@ function createTable(options) {
1198
1973
  }
1199
1974
  function notify() {
1200
1975
  snapshot = void 0;
1976
+ if (deferValidationNotify) {
1977
+ validationNotifyRequested = true;
1978
+ return;
1979
+ }
1980
+ if (transaction) return;
1201
1981
  if (listeners.size === 0) return;
1202
1982
  const next = getSnapshot();
1203
1983
  for (const listener of listeners) listener(next);
1204
1984
  }
1985
+ function abortLoads(all = false) {
1986
+ for (const [key, controller] of pendingLoads) {
1987
+ const row = all ? void 0 : getSnapshot().getRow(key);
1988
+ const visible = !all && getSnapshot().processedRows.some((candidate) => candidate.key === key);
1989
+ if (all || !row?.isExpanded || !visible) {
1990
+ pendingLoads.delete(key);
1991
+ childStatuses.delete(key);
1992
+ controller.abort();
1993
+ snapshot = void 0;
1994
+ }
1995
+ }
1996
+ }
1997
+ function requestChildren(row) {
1998
+ if (!options.loadChildren || destroyed || pendingLoads.has(row.key) || lazyChildren.has(row.key) || row.childStatus === "loaded") return;
1999
+ const key = row.key;
2000
+ if (!options.createChildLoadController) {
2001
+ childStatuses.set(key, "error");
2002
+ loadIssues.set(key, issue("invalid_tree_option", "Lazy children require createChildLoadController from the caller's runtime.", { rowKey: key }));
2003
+ notify();
2004
+ return;
2005
+ }
2006
+ let controller;
2007
+ try {
2008
+ controller = options.createChildLoadController();
2009
+ } catch (error) {
2010
+ loadIssues.set(key, issue("tree_load_error", String(error), { rowKey: key }));
2011
+ childStatuses.set(key, "error");
2012
+ notify();
2013
+ return;
2014
+ }
2015
+ pendingLoads.set(key, controller);
2016
+ childStatuses.set(key, "loading");
2017
+ loadIssues.delete(key);
2018
+ loadValidationIssues.delete(key);
2019
+ notify();
2020
+ Promise.resolve().then(() => controller.signal.aborted ? [] : options.loadChildren(row, controller.signal)).then((children) => {
2021
+ if (controller.signal.aborted || pendingLoads.get(key) !== controller || destroyed) return void 0;
2022
+ const { byId } = resolveColumns(definitions);
2023
+ const existing = treeStage(data, byId, lazyVersion);
2024
+ const candidate = buildTree(children, byId, readKey, {
2025
+ ...options,
2026
+ getParentKey: void 0
2027
+ }, /* @__PURE__ */ new Map());
2028
+ pendingLoads.delete(key);
2029
+ loadValidationIssues.set(key, candidate.issues.filter((problem) => problem.code === "tree_cycle" || problem.code === "tree_depth_exceeded"));
2030
+ if (candidate.nodes.filter((node) => existing.byKey.has(node.row.key)).length || candidate.issues.some((problem) => problem.code === "duplicate_row_key")) {
2031
+ childStatuses.set(key, "error");
2032
+ loadIssues.set(key, issue("tree_duplicate_key", "Loaded children collide with existing keys; the batch is rejected.", { rowKey: key }));
2033
+ } else {
2034
+ lazyChildren.set(key, children);
2035
+ childStatuses.set(key, "loaded");
2036
+ lazyVersion++;
2037
+ if (state.selection.includes(key) && selectionMode === "multiple") {
2038
+ const nextModel = treeStage(data, byId, lazyVersion);
2039
+ applySelection([...state.selection, ...selectionKeys([key], nextModel)]);
2040
+ }
2041
+ }
2042
+ notify();
2043
+ startVisibleLoads();
2044
+ }).catch((error) => {
2045
+ if (controller.signal.aborted || pendingLoads.get(key) !== controller || destroyed) return;
2046
+ pendingLoads.delete(key);
2047
+ childStatuses.set(key, "error");
2048
+ loadIssues.set(key, issue("tree_load_error", `Children could not be loaded: ${String(error)}`, { rowKey: key }));
2049
+ notify();
2050
+ });
2051
+ }
2052
+ function startVisibleLoads() {
2053
+ if (!treeEnabled || !options.loadChildren || destroyed) return;
2054
+ const current = getSnapshot();
2055
+ for (const row of current.rows) if (row.isExpanded && row.childStatus === "idle") requestChildren(row);
2056
+ }
2057
+ function selectionKeys(keys, model) {
2058
+ if (!treeEnabled || selectionMode !== "multiple") return [...keys];
2059
+ model ??= treeStage(data, resolveColumns(definitions).byId, lazyVersion);
2060
+ const result = new Set(keys);
2061
+ const stack = keys.flatMap((key) => {
2062
+ const node = model.byKey.get(key);
2063
+ return node ? [node] : [];
2064
+ });
2065
+ const visited = /* @__PURE__ */ new Set();
2066
+ while (stack.length) {
2067
+ const node = stack.pop();
2068
+ if (visited.has(node.row.key)) continue;
2069
+ visited.add(node.row.key);
2070
+ result.add(node.row.key);
2071
+ for (const child of node.children) stack.push(child);
2072
+ }
2073
+ return [...result];
2074
+ }
1205
2075
  /** Apply a change made by a table operation. */
1206
2076
  function commit(patch) {
1207
2077
  const previous = state;
@@ -1209,13 +2079,21 @@ function createTable(options) {
1209
2079
  const next = mergePatch(patch);
1210
2080
  if (next === previous && previousIssue === expandedIssue) return;
1211
2081
  state = next;
2082
+ snapshot = void 0;
2083
+ abortLoads();
1212
2084
  if (next !== previous) options.onStateChange?.(state, previous);
1213
2085
  notify();
2086
+ startVisibleLoads();
1214
2087
  }
1215
2088
  function canonicalExpanded(expanded, candidate = state) {
1216
2089
  if (expandMode === "multiple") return expanded;
1217
2090
  if (expanded !== true) return expanded.slice(-1);
1218
2091
  const { columns, byId } = resolveColumns(definitions);
2092
+ if (treeEnabled) {
2093
+ const model = treeStage(data, byId, lazyVersion);
2094
+ 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));
2095
+ return first ? [first.row.key] : [];
2096
+ }
1219
2097
  const indexed = indexRows(data, byId);
1220
2098
  const first = (manual ? indexed.rows : sortStage(filterStage(indexed.rows, columns, candidate.search, candidate.filters), columns, candidate.sorting)).find((row) => row.canExpand);
1221
2099
  return first ? [first.key] : [];
@@ -1262,6 +2140,10 @@ function createTable(options) {
1262
2140
  function applyChanges(edits, record, recoveredIssues = []) {
1263
2141
  const current = getSnapshot();
1264
2142
  const working = [...data];
2143
+ const model = treeEnabled ? treeStage(data, resolveColumns(definitions).byId, lazyVersion) : void 0;
2144
+ const replacements = /* @__PURE__ */ new Map();
2145
+ const cache = new Map(lazyChildren);
2146
+ const originalOf = (node) => replacements.get(node) ?? node.row.original;
1265
2147
  const issues = [...recoveredIssues];
1266
2148
  const changes = [];
1267
2149
  const samples = /* @__PURE__ */ new Map();
@@ -1299,7 +2181,22 @@ function createTable(options) {
1299
2181
  }));
1300
2182
  continue;
1301
2183
  }
1302
- const original = working[row.index];
2184
+ const node = model?.byKey.get(row.key);
2185
+ const original = node ? originalOf(node) : working[row.index];
2186
+ if (node && !node.writable) {
2187
+ issues.push(issue("read_only_cell", "A recovered nested root has no unambiguous source path for editing.", {
2188
+ rowKey: row.key,
2189
+ column: column.id
2190
+ }));
2191
+ continue;
2192
+ }
2193
+ if (node?.parent && options.getChildren && !options.setChildren) {
2194
+ issues.push(issue("read_only_cell", "Editing a nested child requires an immutable setChildren callback.", {
2195
+ rowKey: row.key,
2196
+ column: column.id
2197
+ }));
2198
+ continue;
2199
+ }
1303
2200
  if (record && !column.isEditable(original)) {
1304
2201
  issues.push(issue("read_only_cell", `The "${column.id}" cell of row "${String(row.key)}" is read-only.`, {
1305
2202
  rowKey: row.key,
@@ -1321,7 +2218,16 @@ function createTable(options) {
1321
2218
  } else value = edit.value;
1322
2219
  const previous = column.getValue(original);
1323
2220
  if (Object.is(previous, value)) continue;
1324
- const updated = column.setValue(original, value);
2221
+ let updated;
2222
+ try {
2223
+ updated = column.setValue(original, value);
2224
+ } catch (error) {
2225
+ issues.push(issue("invalid_value", error instanceof Error ? error.message : String(error), {
2226
+ rowKey: row.key,
2227
+ column: column.id
2228
+ }));
2229
+ continue;
2230
+ }
1325
2231
  if (updated === void 0) {
1326
2232
  issues.push(issue("read_only_cell", `The "${column.id}" column has no way to write a value.`, {
1327
2233
  rowKey: row.key,
@@ -1329,7 +2235,38 @@ function createTable(options) {
1329
2235
  }));
1330
2236
  continue;
1331
2237
  }
1332
- working[row.index] = updated;
2238
+ if (readKey(updated, row.index) !== readKey(original, row.index)) {
2239
+ issues.push(issue("invalid_row_operation", "Editing a row key is not supported; remove and insert the row instead.", {
2240
+ rowKey: row.key,
2241
+ column: column.id
2242
+ }));
2243
+ continue;
2244
+ }
2245
+ if (!node) working[row.index] = updated;
2246
+ else {
2247
+ const planned = /* @__PURE__ */ new Map([[node, updated]]);
2248
+ const plannedCache = /* @__PURE__ */ new Map();
2249
+ let cursor = node;
2250
+ try {
2251
+ while (cursor.parent) {
2252
+ const parent = cursor.parent;
2253
+ const children = parent.children.map((child) => planned.get(child) ?? originalOf(child));
2254
+ if (cursor.lazy) plannedCache.set(parent.row.key, children);
2255
+ if (options.setChildren) planned.set(parent, options.setChildren(originalOf(parent), children));
2256
+ cursor = parent;
2257
+ }
2258
+ } catch (error) {
2259
+ issues.push(issue("invalid_value", error instanceof Error ? error.message : String(error), {
2260
+ rowKey: row.key,
2261
+ column: column.id
2262
+ }));
2263
+ continue;
2264
+ }
2265
+ for (const [item, plannedRow] of planned) replacements.set(item, plannedRow);
2266
+ for (const [key, children] of plannedCache) cache.set(key, children);
2267
+ if (!options.getChildren && !node.lazy) working[node.sourceIndex] = updated;
2268
+ if (options.getChildren || options.setChildren) working[cursor.sourceIndex] = originalOf(cursor);
2269
+ }
1333
2270
  changes.push({
1334
2271
  rowKey: row.key,
1335
2272
  column: column.id,
@@ -1343,17 +2280,390 @@ function createTable(options) {
1343
2280
  else {
1344
2281
  status = issues.length > 0 ? "partial" : "applied";
1345
2282
  data = working;
1346
- if (record) undoStack.record(changes);
1347
- options.onDataChange?.(data, changes);
2283
+ if (model) {
2284
+ lazyChildren.clear();
2285
+ for (const [key, children] of cache) lazyChildren.set(key, children);
2286
+ lazyVersion++;
2287
+ }
2288
+ if (record) {
2289
+ if (!transaction) undoStack.record({
2290
+ kind: "cells",
2291
+ changes
2292
+ });
2293
+ }
2294
+ const groups = /* @__PURE__ */ new Map();
2295
+ for (const change of changes) {
2296
+ const group = groups.get(change.rowKey) ?? [];
2297
+ group.push(change);
2298
+ groups.set(change.rowKey, group);
2299
+ }
2300
+ for (const [key, group] of groups) pendingChanges.record(key, current.getRow(key).original, group.at(-1).row, positionOf(key), group);
2301
+ if (!transaction) options.onDataChange?.(data, changes);
1348
2302
  notify();
1349
2303
  }
1350
- if (issues.length > 0) options.onEditIssues?.(issues);
2304
+ if (issues.length > 0 && !transaction) options.onEditIssues?.(issues);
1351
2305
  return Object.freeze({
1352
2306
  status,
1353
2307
  changes: Object.freeze(changes),
1354
- issues: Object.freeze(issues)
2308
+ issues: Object.freeze(issues),
2309
+ rowChanges: Object.freeze([]),
2310
+ pendingCells: Object.freeze([])
1355
2311
  });
1356
2312
  }
2313
+ function sourceRows() {
2314
+ const { byId } = resolveColumns(definitions);
2315
+ const model = treeEnabled ? treeStage(data, byId, lazyVersion) : void 0;
2316
+ return model ? treeIndex(model) : indexRows(data, byId);
2317
+ }
2318
+ function positionOf(key) {
2319
+ const { byId } = resolveColumns(definitions);
2320
+ if (treeEnabled) {
2321
+ const model = treeStage(data, byId, lazyVersion);
2322
+ return treePositions(model).get(key) ?? Object.freeze({});
2323
+ }
2324
+ const indexed = indexRows(data, byId);
2325
+ const row = indexed.byKey.get(key);
2326
+ return Object.freeze({ before: row ? indexed.rows[row.index + 1]?.key : void 0 });
2327
+ }
2328
+ function scene() {
2329
+ return {
2330
+ data,
2331
+ lazy: new Map(lazyChildren)
2332
+ };
2333
+ }
2334
+ function resultForRows(changes, issues, cells = []) {
2335
+ return Object.freeze({
2336
+ status: changes.length || cells.length ? issues.length ? "partial" : "applied" : issues.length ? "rejected" : "unchanged",
2337
+ changes: Object.freeze([...cells]),
2338
+ rowChanges: Object.freeze([...changes]),
2339
+ pendingCells: Object.freeze([]),
2340
+ issues: Object.freeze([...issues])
2341
+ });
2342
+ }
2343
+ function rowProblem(message, rowKey) {
2344
+ return issue("invalid_row_operation", message, { rowKey });
2345
+ }
2346
+ function stableRows(rows) {
2347
+ return options.rowKey !== void 0 || rows.every((row) => {
2348
+ const key = getPath(row, "id");
2349
+ return typeof key === "string" || typeof key === "number";
2350
+ });
2351
+ }
2352
+ function cellDifferences(previous, row, key) {
2353
+ return resolveColumns(definitions).columns.flatMap((column) => {
2354
+ const before = column.getValue(previous);
2355
+ const value = column.getValue(row);
2356
+ return Object.is(before, value) ? [] : [{
2357
+ rowKey: key,
2358
+ column: column.id,
2359
+ previous: before,
2360
+ value,
2361
+ row
2362
+ }];
2363
+ });
2364
+ }
2365
+ function recordRows(previous, keys, positions, baselines = /* @__PURE__ */ new Map()) {
2366
+ const next = sourceRows();
2367
+ const rowChanges = [];
2368
+ for (const key of keys) {
2369
+ const before = previous.byKey.get(key)?.original;
2370
+ const row = next.byKey.get(key)?.original;
2371
+ const position = positions.get(key) ?? positionOf(key);
2372
+ const cells = before !== void 0 && row !== void 0 ? cellDifferences(before, row, key) : [];
2373
+ const baseline = baselines.get(key);
2374
+ const pending = pendingChanges.entries.get(key);
2375
+ if (row === void 0 && baseline !== void 0 && pending?.previous !== void 0) pending.previous = baseline;
2376
+ pendingChanges.record(key, row === void 0 ? baseline ?? before : before, row, position, cells);
2377
+ if (before === void 0 && row !== void 0) rowChanges.push(Object.freeze({
2378
+ kind: "inserted",
2379
+ rowKey: key,
2380
+ row,
2381
+ position: positionOf(key)
2382
+ }));
2383
+ else if (row === void 0 && before !== void 0) rowChanges.push(Object.freeze({
2384
+ kind: "removed",
2385
+ rowKey: key,
2386
+ row: before,
2387
+ position
2388
+ }));
2389
+ }
2390
+ return rowChanges;
2391
+ }
2392
+ function publishRows(before, previous, keys, positions, issues, baselines = /* @__PURE__ */ new Map()) {
2393
+ const rowChanges = recordRows(previous, keys, positions, baselines);
2394
+ if (!transaction) {
2395
+ undoStack.record({
2396
+ kind: "rows",
2397
+ before,
2398
+ after: scene(),
2399
+ keys
2400
+ });
2401
+ options.onDataChange?.(data, []);
2402
+ if (issues.length) options.onEditIssues?.(issues);
2403
+ }
2404
+ notify();
2405
+ return resultForRows(rowChanges, issues);
2406
+ }
2407
+ function restoreScene(entry, direction) {
2408
+ const previous = sourceRows();
2409
+ const baselines = removalBaselines();
2410
+ const positions = new Map(entry.keys.map((key) => [key, positionOf(key)]));
2411
+ const target = direction === "undo" ? entry.before : entry.after;
2412
+ data = target.data;
2413
+ const changed = /* @__PURE__ */ new Set([...entry.before.lazy.keys(), ...entry.after.lazy.keys()]);
2414
+ for (const key of changed) {
2415
+ if (entry.before.lazy.get(key) === entry.after.lazy.get(key)) continue;
2416
+ const rows = target.lazy.get(key);
2417
+ if (rows) lazyChildren.set(key, rows);
2418
+ else lazyChildren.delete(key);
2419
+ }
2420
+ lazyVersion++;
2421
+ recordRows(previous, entry.keys, positions, baselines);
2422
+ options.onDataChange?.(data, []);
2423
+ notify();
2424
+ }
2425
+ function removalBaselines() {
2426
+ const baselines = /* @__PURE__ */ new Map();
2427
+ if (!options.getChildren || !options.setChildren) return baselines;
2428
+ const model = treeStage(data, resolveColumns(definitions).byId, lazyVersion);
2429
+ for (const node of model.nodes.toReversed()) {
2430
+ const entry = pendingChanges.entries.get(node.row.key);
2431
+ if (entry && entry.previous === void 0) continue;
2432
+ let baseline = entry?.previous ?? node.row.original;
2433
+ const children = node.children.filter((child) => !pendingChanges.entries.has(child.row.key) || pendingChanges.entries.get(child.row.key).previous !== void 0).map((child) => baselines.get(child.row.key) ?? child.row.original);
2434
+ const originalChildren = options.getChildren(baseline) ?? [];
2435
+ if (children.length !== originalChildren.length || children.some((child, index) => child !== originalChildren[index])) baseline = options.setChildren(baseline, children);
2436
+ baselines.set(node.row.key, baseline);
2437
+ }
2438
+ return baselines;
2439
+ }
2440
+ function rewriteNested(model, removed, overrides, cache) {
2441
+ const replacements = /* @__PURE__ */ new Map();
2442
+ for (const node of model.nodes.toReversed()) {
2443
+ if (removed.has(node.row.key)) continue;
2444
+ const children = overrides.get(node.row.key) ?? node.children.filter((child) => !removed.has(child.row.key)).map((child) => replacements.get(child) ?? child.row.original);
2445
+ if (!(overrides.has(node.row.key) || children.length !== node.children.length || children.some((child, index) => child !== node.children[index]?.row.original))) continue;
2446
+ if (!options.setChildren) throw new Error("Changing nested children requires setChildren.");
2447
+ replacements.set(node, options.setChildren(node.row.original, children));
2448
+ if (cache.has(node.row.key)) cache.set(node.row.key, children);
2449
+ }
2450
+ const roots = new Map(model.roots.filter((node) => node.writable).map((node) => [node.sourceIndex, node]));
2451
+ return data.flatMap((row, index) => {
2452
+ const node = roots.get(index);
2453
+ return node && removed.has(node.row.key) ? [] : [node ? replacements.get(node) ?? row : row];
2454
+ });
2455
+ }
2456
+ function insertRows(rows, at = {}) {
2457
+ const before = scene();
2458
+ const previous = sourceRows();
2459
+ const problems = [];
2460
+ const reject = (problem) => {
2461
+ if (!transaction) options.onEditIssues?.([problem]);
2462
+ return resultForRows([], [problem]);
2463
+ };
2464
+ let incoming;
2465
+ try {
2466
+ incoming = rows ?? (options.createRow ? [options.createRow()] : []);
2467
+ } catch (error) {
2468
+ return reject(rowProblem(error instanceof Error ? error.message : String(error)));
2469
+ }
2470
+ if (rows === void 0 && !options.createRow) return reject(rowProblem("insertRows() requires rows or a createRow callback."));
2471
+ if (!incoming.length) return resultForRows([], []);
2472
+ if (!stableRows(data) || !stableRows(incoming)) return reject(rowProblem("Row operations require stable row keys; supply rowKey or an id on every row."));
2473
+ const { byId } = resolveColumns(definitions);
2474
+ const model = treeEnabled ? treeStage(data, byId, lazyVersion) : void 0;
2475
+ const parent = at.parent === void 0 ? void 0 : model?.byKey.get(at.parent);
2476
+ const sibling = at.before === void 0 ? void 0 : previous.byKey.get(at.before);
2477
+ if (at.parent !== void 0 && !parent) return reject(issue("unknown_row", "The insertion parent does not exist.", { rowKey: at.parent }));
2478
+ if (at.before !== void 0 && !sibling) return reject(issue("unknown_row", "The insertion sibling does not exist.", { rowKey: at.before }));
2479
+ if (sibling && (model?.byKey.get(sibling.key)?.parent?.row.key ?? sibling.parentKey) !== at.parent) return reject(rowProblem("The before row must belong to the requested parent.", sibling.key));
2480
+ if (parent && !parent.writable) return reject(rowProblem("A recovered parent has no writable source path.", parent.row.key));
2481
+ if (parent && getSnapshot().getRow(parent.row.key)?.childStatus !== "loaded") return reject(rowProblem("Load the parent's children before inserting rows.", parent.row.key));
2482
+ const cache = new Map(lazyChildren);
2483
+ let next;
2484
+ try {
2485
+ if (options.getChildren && parent && model) {
2486
+ const children = parent.children.map((child) => child.row.original);
2487
+ const index = at.before === void 0 ? children.length : parent.children.findIndex((child) => child.row.key === at.before);
2488
+ children.splice(index, 0, ...incoming);
2489
+ next = rewriteNested(model, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Map([[parent.row.key, children]]), cache);
2490
+ } else if (parent && cache.has(parent.row.key)) {
2491
+ incoming = incoming.map((row) => options.setParentKey ? options.setParentKey(row, parent.row.key) : row);
2492
+ const children = [...cache.get(parent.row.key)];
2493
+ const index = at.before === void 0 ? children.length : parent.children.findIndex((child) => child.row.key === at.before);
2494
+ children.splice(index, 0, ...incoming);
2495
+ cache.set(parent.row.key, children);
2496
+ next = [...data];
2497
+ } else {
2498
+ if (parent && options.getParentKey) {
2499
+ incoming = incoming.map((row) => options.setParentKey ? options.setParentKey(row, parent.row.key) : row);
2500
+ if (incoming.some((row) => options.getParentKey(row) !== parent.row.key)) return reject(rowProblem("Supply setParentKey or rows already addressed to the requested parent."));
2501
+ }
2502
+ const working = [...data];
2503
+ working.splice(sibling?.index ?? working.length, 0, ...incoming);
2504
+ next = working;
2505
+ }
2506
+ const candidate = treeEnabled ? buildTree(next, byId, readKey, options, cache) : indexRows(next, byId);
2507
+ if (!stableRows(candidate.rows.map((row) => row.original))) return reject(rowProblem("Every inserted descendant must have a stable key."));
2508
+ const baseline = /* @__PURE__ */ new Map();
2509
+ for (const problem of previous.issues) {
2510
+ const key = `${problem.code}:${String(problem.rowKey)}`;
2511
+ baseline.set(key, (baseline.get(key) ?? 0) + 1);
2512
+ }
2513
+ problems.push(...candidate.issues.filter((problem) => {
2514
+ const key = `${problem.code}:${String(problem.rowKey)}`;
2515
+ const remaining = baseline.get(key) ?? 0;
2516
+ if (!remaining) return true;
2517
+ baseline.set(key, remaining - 1);
2518
+ return false;
2519
+ }));
2520
+ if (problems.length) {
2521
+ if (!transaction) options.onEditIssues?.(problems);
2522
+ return resultForRows([], problems);
2523
+ }
2524
+ const keys = candidate.rows.filter((row) => !previous.byKey.has(row.key)).map((row) => row.key);
2525
+ if (!keys.length) return reject(rowProblem("No unique rows can be inserted."));
2526
+ data = next;
2527
+ lazyChildren.clear();
2528
+ for (const [key, value] of cache) lazyChildren.set(key, value);
2529
+ lazyVersion++;
2530
+ return publishRows(before, previous, keys, /* @__PURE__ */ new Map(), []);
2531
+ } catch (error) {
2532
+ return reject(rowProblem(error instanceof Error ? error.message : String(error)));
2533
+ }
2534
+ }
2535
+ function removeRows(keys) {
2536
+ const before = scene();
2537
+ const previous = sourceRows();
2538
+ const problems = [];
2539
+ const removed = /* @__PURE__ */ new Set();
2540
+ const { byId } = resolveColumns(definitions);
2541
+ const model = treeEnabled ? treeStage(data, byId, lazyVersion) : void 0;
2542
+ if (!stableRows(data)) {
2543
+ const problem = rowProblem("Row operations require stable row keys.");
2544
+ if (!transaction) options.onEditIssues?.([problem]);
2545
+ return resultForRows([], [problem]);
2546
+ }
2547
+ for (const key of keys) {
2548
+ if (!previous.byKey.has(key)) {
2549
+ problems.push(issue("unknown_row", "The row to remove does not exist.", { rowKey: key }));
2550
+ continue;
2551
+ }
2552
+ const node = model?.byKey.get(key);
2553
+ if (node && !node.writable) {
2554
+ problems.push(rowProblem("A recovered row has no writable source path.", key));
2555
+ continue;
2556
+ }
2557
+ const stack = node ? [node] : [];
2558
+ removed.add(key);
2559
+ while (stack.length) {
2560
+ const item = stack.pop();
2561
+ removed.add(item.row.key);
2562
+ for (const child of item.children) stack.push(child);
2563
+ }
2564
+ }
2565
+ if (!removed.size) {
2566
+ if (problems.length && !transaction) options.onEditIssues?.(problems);
2567
+ return resultForRows([], problems);
2568
+ }
2569
+ const positions = new Map([...removed].map((key) => [key, positionOf(key)]));
2570
+ const cache = new Map(lazyChildren);
2571
+ try {
2572
+ const baselines = removalBaselines();
2573
+ const next = options.getChildren && model ? rewriteNested(model, removed, /* @__PURE__ */ new Map(), cache) : data.filter((row, index) => !removed.has(readKey(row, index)));
2574
+ for (const [key, children] of cache) if (removed.has(key)) cache.delete(key);
2575
+ else cache.set(key, children.filter((row, index) => !removed.has(readKey(row, index))));
2576
+ data = next;
2577
+ lazyChildren.clear();
2578
+ for (const [key, value] of cache) lazyChildren.set(key, value);
2579
+ lazyVersion++;
2580
+ validation.cancel([...removed]);
2581
+ abortLoads();
2582
+ return publishRows(before, previous, [...removed], positions, problems, baselines);
2583
+ } catch (error) {
2584
+ problems.push(rowProblem(error instanceof Error ? error.message : String(error)));
2585
+ if (!transaction) options.onEditIssues?.(problems);
2586
+ return resultForRows([], problems);
2587
+ }
2588
+ }
2589
+ function revert(keys) {
2590
+ const selected = new Set(keys ?? pendingChanges.entries.keys());
2591
+ const entries = [...pendingChanges.entries].filter(([key]) => selected.has(key)).map(([key, entry]) => [key, {
2592
+ ...entry,
2593
+ cells: new Map(entry.cells)
2594
+ }]);
2595
+ if (!entries.length) return resultForRows([], []);
2596
+ const before = scene();
2597
+ const previous = sourceRows();
2598
+ const positions = new Map([...selected].map((key) => [key, positionOf(key)]));
2599
+ const problems = [];
2600
+ const cells = [];
2601
+ transaction = true;
2602
+ try {
2603
+ problems.push(...removeRows(entries.filter(([, entry]) => entry.previous === void 0).map(([key]) => key)).issues);
2604
+ const missing = entries.filter(([, entry]) => entry.previous !== void 0 && entry.row === void 0);
2605
+ for (let pass = 0; missing.length && pass <= entries.length; pass++) {
2606
+ let progress = false;
2607
+ for (let index = missing.length - 1; index >= 0; index--) {
2608
+ const [key, entry] = missing[index];
2609
+ if (sourceRows().byKey.has(key)) {
2610
+ missing.splice(index, 1);
2611
+ progress = true;
2612
+ continue;
2613
+ }
2614
+ if (entry.position.parent !== void 0 && !sourceRows().byKey.has(entry.position.parent)) continue;
2615
+ const at = {
2616
+ ...entry.position,
2617
+ before: entry.position.before !== void 0 && sourceRows().byKey.has(entry.position.before) ? entry.position.before : void 0
2618
+ };
2619
+ const result = insertRows([entry.previous], at);
2620
+ problems.push(...result.issues);
2621
+ missing.splice(index, 1);
2622
+ progress = true;
2623
+ }
2624
+ if (!progress) break;
2625
+ }
2626
+ for (const [key] of missing) problems.push(rowProblem("The saved parent is missing; restore it before this child.", key));
2627
+ const result = applyChanges(entries.flatMap(([key, entry]) => entry.previous !== void 0 && sourceRows().byKey.has(key) ? [...entry.cells.values()].map((change) => ({
2628
+ rowKey: key,
2629
+ column: change.column,
2630
+ value: change.previous
2631
+ })) : []), false);
2632
+ cells.push(...result.changes);
2633
+ problems.push(...result.issues);
2634
+ for (const [key] of entries) if (!problems.some((problem) => problem.rowKey === key)) pendingChanges.entries.delete(key);
2635
+ validation.cancel([...selected]);
2636
+ } finally {
2637
+ transaction = false;
2638
+ }
2639
+ const after = scene();
2640
+ if (before.data !== after.data || cells.length) undoStack.record({
2641
+ kind: "rows",
2642
+ before,
2643
+ after,
2644
+ keys: [...selected]
2645
+ });
2646
+ const next = sourceRows();
2647
+ const rowChanges = [...selected].flatMap((key) => {
2648
+ const old = previous.byKey.get(key);
2649
+ const row = next.byKey.get(key);
2650
+ return !old && row ? [{
2651
+ kind: "inserted",
2652
+ rowKey: key,
2653
+ row: row.original,
2654
+ position: positionOf(key)
2655
+ }] : old && !row ? [{
2656
+ kind: "removed",
2657
+ rowKey: key,
2658
+ row: old.original,
2659
+ position: positions.get(key)
2660
+ }] : [];
2661
+ });
2662
+ if (before.data !== after.data) options.onDataChange?.(data, cells);
2663
+ if (problems.length) options.onEditIssues?.(problems);
2664
+ notify();
2665
+ return resultForRows(rowChanges, problems, cells);
2666
+ }
1357
2667
  function rangeEdits(origin, matrix) {
1358
2668
  const { rows, columns } = getSnapshot();
1359
2669
  const edits = [];
@@ -1372,7 +2682,7 @@ function createTable(options) {
1372
2682
  return edits;
1373
2683
  }
1374
2684
  function replay(changes, direction) {
1375
- applyChanges(changes.map((change) => ({
2685
+ applyChanges((direction === "undo" ? changes.toReversed() : changes).map((change) => ({
1376
2686
  rowKey: change.rowKey,
1377
2687
  column: change.column,
1378
2688
  value: direction === "undo" ? change.previous : change.value
@@ -1384,6 +2694,15 @@ function createTable(options) {
1384
2694
  snapshot = void 0;
1385
2695
  }
1386
2696
  const table = {
2697
+ insertRows,
2698
+ removeRows,
2699
+ revert,
2700
+ getPendingChanges: () => pendingChanges.snapshot(),
2701
+ markSaved(keys) {
2702
+ if (!keys) pendingChanges.entries.clear();
2703
+ else for (const key of keys) pendingChanges.entries.delete(key);
2704
+ notify();
2705
+ },
1387
2706
  getSnapshot,
1388
2707
  getState: () => state,
1389
2708
  subscribe(listener) {
@@ -1397,18 +2716,31 @@ function createTable(options) {
1397
2716
  const next = mergePatch(patch);
1398
2717
  if (next !== state || previousIssue !== expandedIssue) {
1399
2718
  state = next;
2719
+ snapshot = void 0;
2720
+ abortLoads();
1400
2721
  notify();
2722
+ startVisibleLoads();
1401
2723
  }
1402
2724
  },
1403
2725
  setData(next) {
1404
2726
  if (next === data) return;
2727
+ validation.cancel();
2728
+ pendingChanges.entries.clear();
2729
+ abortLoads(true);
2730
+ lazyChildren.clear();
2731
+ childStatuses.clear();
2732
+ loadIssues.clear();
2733
+ loadValidationIssues.clear();
2734
+ lazyVersion++;
1405
2735
  data = next;
1406
2736
  undoStack.clear();
1407
2737
  notify();
2738
+ startVisibleLoads();
1408
2739
  },
1409
2740
  setColumns(next) {
1410
2741
  if (next === definitions) return;
1411
2742
  definitions = next;
2743
+ validation.cancel();
1412
2744
  notify();
1413
2745
  },
1414
2746
  setRowCount(next) {
@@ -1475,14 +2807,14 @@ function createTable(options) {
1475
2807
  } });
1476
2808
  },
1477
2809
  select(keys) {
1478
- applySelection([...state.selection, ...keys]);
2810
+ applySelection([...state.selection, ...selectionKeys(keys)]);
1479
2811
  },
1480
2812
  deselect(keys) {
1481
- const removed = new Set(keys);
2813
+ const removed = new Set(selectionKeys(keys));
1482
2814
  applySelection(state.selection.filter((key) => !removed.has(key)));
1483
2815
  },
1484
2816
  toggleRow(key, selected) {
1485
- const isSelected = state.selection.includes(key);
2817
+ const isSelected = treeEnabled ? getSnapshot().getRow(key)?.selection === "all" : state.selection.includes(key);
1486
2818
  const next = selected ?? !isSelected;
1487
2819
  if (next === isSelected) return;
1488
2820
  if (next) table.select([key]);
@@ -1509,12 +2841,16 @@ function createTable(options) {
1509
2841
  const row = getSnapshot().getRow(key);
1510
2842
  if (!row?.canExpand) return;
1511
2843
  const open = expanded ?? !row.isExpanded;
1512
- if (open === row.isExpanded) return;
2844
+ if (open === row.isExpanded) {
2845
+ if (open && row.childStatus === "error") requestChildren(row);
2846
+ return;
2847
+ }
1513
2848
  if (expandMode === "single") commit({ expanded: open ? [key] : [] });
1514
2849
  else if (state.expanded === true) {
1515
2850
  const { byId } = resolveColumns(definitions);
1516
- commit({ expanded: indexRows(data, byId).rows.filter((candidate) => candidate.canExpand && candidate.key !== key).map((candidate) => candidate.key) });
2851
+ 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
2852
  } else commit({ expanded: open ? [...state.expanded, key] : state.expanded.filter((candidate) => candidate !== key) });
2853
+ if (open) requestChildren(getSnapshot().getRow(key));
1518
2854
  },
1519
2855
  expandAll() {
1520
2856
  commit({ expanded: true });
@@ -1540,18 +2876,22 @@ function createTable(options) {
1540
2876
  commit({ columnOrder: order });
1541
2877
  },
1542
2878
  edit(edits) {
1543
- return applyChanges(Array.isArray(edits) ? edits : [edits], true);
2879
+ return validation.edit(Array.isArray(edits) ? edits : [edits]);
1544
2880
  },
1545
2881
  undo() {
1546
- const changes = undoStack.undo();
1547
- if (!changes) return false;
1548
- replay(changes, "undo");
2882
+ const entry = undoStack.undo();
2883
+ if (!entry) return false;
2884
+ validation.cancel();
2885
+ if (entry.kind === "cells") replay(entry.changes, "undo");
2886
+ else restoreScene(entry, "undo");
1549
2887
  return true;
1550
2888
  },
1551
2889
  redo() {
1552
- const changes = undoStack.redo();
1553
- if (!changes) return false;
1554
- replay(changes, "redo");
2890
+ const entry = undoStack.redo();
2891
+ if (!entry) return false;
2892
+ validation.cancel();
2893
+ if (entry.kind === "cells") replay(entry.changes, "redo");
2894
+ else restoreScene(entry, "redo");
1555
2895
  return true;
1556
2896
  },
1557
2897
  copy(range, { escapeFormulas = false } = {}) {
@@ -1573,7 +2913,7 @@ function createTable(options) {
1573
2913
  return toDelimited(matrix);
1574
2914
  },
1575
2915
  paste(origin, text) {
1576
- 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.")]);
2916
+ if (!Number.isInteger(origin.row) || !Number.isInteger(origin.column) || origin.row < 0 || origin.column < 0) return validation.edit([], [issue("invalid_value", "The paste origin must use non-negative whole row and column indices.")]);
1577
2917
  const { rows, columns } = getSnapshot();
1578
2918
  const parsed = readDelimited(text, {
1579
2919
  maxRows: Math.max(0, rows.length - origin.row),
@@ -1582,39 +2922,49 @@ function createTable(options) {
1582
2922
  maxLength: maxPasteLength
1583
2923
  });
1584
2924
  const issues = parsed.truncated ? [issue("paste_truncated", "The paste exceeds the visible grid or parsing limits; only complete cells within those limits are applied.")] : [];
1585
- return applyChanges(rangeEdits(origin, parsed.rows), true, issues);
2925
+ return validation.edit(rangeEdits(origin, parsed.rows), issues);
1586
2926
  },
1587
2927
  clear(range) {
1588
2928
  const matrix = Array.from({ length: range.bottom - range.top + 1 }, () => Array.from({ length: range.right - range.left + 1 }, () => ""));
1589
2929
  const current = getSnapshot();
1590
- return applyChanges(rangeEdits({
2930
+ const edits = rangeEdits({
1591
2931
  row: range.top,
1592
2932
  column: range.left
1593
2933
  }, matrix).filter((edit) => {
1594
2934
  const row = current.getRow(edit.rowKey);
1595
2935
  return row !== void 0 && current.getColumn(edit.column)?.isEditable(row.original) === true;
1596
- }), true);
2936
+ });
2937
+ return validation.edit(edits);
1597
2938
  },
1598
- exportRows({ format = "csv", columns: ids, pageOnly = false, headers = true, escapeFormulas = true } = {}) {
2939
+ exportRows({ format = "csv", columns: ids, pageOnly = false, headers = true, escapeFormulas = true, depth = false, indent = "" } = {}) {
1599
2940
  const current = getSnapshot();
1600
2941
  const columns = ids ? ids.flatMap((id) => {
1601
2942
  const column = current.getColumn(id);
1602
2943
  return column ? [column] : [];
1603
2944
  }) : current.columns;
1604
- const matrix = (pageOnly ? current.rows : current.processedRows).map((row) => columns.map((column) => {
1605
- const display = row.getDisplay(column.id);
2945
+ const rows = pageOnly ? current.rows : current.processedRows;
2946
+ const matrix = rows.map((row) => columns.map((column, index) => {
2947
+ const display = (index === 0 ? indent.repeat(row.depth) : "") + row.getDisplay(column.id);
1606
2948
  return escapeFormulas ? escapeFormula(display, row.getValue(column.id)) : display;
1607
2949
  }));
1608
- if (headers) matrix.unshift(columns.map((column) => escapeFormulas ? escapeFormula(column.header) : column.header));
2950
+ if (depth) matrix.forEach((line, index) => line.unshift(String(rows[index].depth)));
2951
+ if (headers) {
2952
+ matrix.unshift(columns.map((column) => escapeFormulas ? escapeFormula(column.header) : column.header));
2953
+ if (depth) matrix[0].unshift("Depth");
2954
+ }
1609
2955
  return toDelimited(matrix, format === "csv" ? "," : " ");
1610
2956
  },
1611
2957
  reset() {
1612
2958
  commit(initialState);
1613
2959
  },
1614
2960
  destroy() {
2961
+ destroyed = true;
2962
+ validation.cancel();
2963
+ abortLoads(true);
1615
2964
  listeners.clear();
1616
2965
  }
1617
2966
  };
2967
+ startVisibleLoads();
1618
2968
  return table;
1619
2969
  }
1620
2970