@vueye-table/core 3.0.0-alpha.3 → 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
@@ -898,6 +898,8 @@ function createRow(original, key, index, columns, canExpand = false) {
898
898
  childCount: 0,
899
899
  childStatus: "loaded",
900
900
  selection: "none",
901
+ isDirty: false,
902
+ cellIssues: void 0,
901
903
  getValue,
902
904
  getDisplay(columnId) {
903
905
  const cached = displays.get(columnId);
@@ -966,6 +968,366 @@ function freezeState(state, previous) {
966
968
  });
967
969
  }
968
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
+
969
1331
  //#endregion
970
1332
  //#region src/tree.ts
971
1333
  function buildTree(data, columns, readKey, options, lazy) {
@@ -1223,6 +1585,7 @@ function createTable(options) {
1223
1585
  const pendingLoads = /* @__PURE__ */ new Map();
1224
1586
  let destroyed = false;
1225
1587
  const undoStack = createUndoStack(options.historyLimit);
1588
+ const pendingChanges = createPendingChanges();
1226
1589
  const listeners = /* @__PURE__ */ new Set();
1227
1590
  const optionIssues = [];
1228
1591
  const maxPasteCells = pasteLimit(options.pasteLimit?.maxCells, 1e5, "maxCells", optionIssues);
@@ -1239,6 +1602,36 @@ function createTable(options) {
1239
1602
  });
1240
1603
  let state = initialState;
1241
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
+ });
1242
1635
  const readKey = (row, index) => {
1243
1636
  const { rowKey } = options;
1244
1637
  if (typeof rowKey === "function") return rowKey(row, index);
@@ -1280,6 +1673,23 @@ function createTable(options) {
1280
1673
  };
1281
1674
  });
1282
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
+ });
1283
1693
  const processedTreeStage = memo((model, columns, search, filters, sorting) => processTree(model, columns, {
1284
1694
  ...state,
1285
1695
  search,
@@ -1382,6 +1792,13 @@ function createTable(options) {
1382
1792
  return issues;
1383
1793
  }
1384
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;
1385
1802
  const { columns, byId, issues: columnIssues } = resolveColumns(definitions);
1386
1803
  const model = treeEnabled ? treeStage(data, byId, lazyVersion) : void 0;
1387
1804
  const indexed = model ? {
@@ -1396,7 +1813,8 @@ function createTable(options) {
1396
1813
  ...indexed.issues,
1397
1814
  ...stateIssues(byId),
1398
1815
  ...loadIssues.values(),
1399
- ...Array.from(loadValidationIssues.values()).flat()
1816
+ ...Array.from(loadValidationIssues.values()).flat(),
1817
+ ...Array.from(cellProblems.values()).flatMap((row) => [...row.values()])
1400
1818
  ];
1401
1819
  const requestedSize = state.pagination.pageSize;
1402
1820
  let pageSize = Number.isInteger(requestedSize) && requestedSize >= 1 ? requestedSize : 10;
@@ -1426,6 +1844,8 @@ function createTable(options) {
1426
1844
  const canExpand = node.children.length > 0 || status !== "loaded" || (options.getRowCanExpand?.(node.row.original) ?? false);
1427
1845
  const result = Object.freeze({
1428
1846
  ...node.row,
1847
+ isDirty: dirty.has(key),
1848
+ cellIssues: cellProblems.get(key),
1429
1849
  depth: node.depth,
1430
1850
  parentKey: node.parent?.row.key,
1431
1851
  childCount: status === "loaded" ? node.children.length : void 0,
@@ -1479,6 +1899,20 @@ function createTable(options) {
1479
1899
  pageCount = 1;
1480
1900
  pageSize = Math.max(1, rows.length);
1481
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
+ }
1482
1916
  const selected = selectedSet(state.selection);
1483
1917
  const pageSelected = rows.filter((row) => selected.has(row.key)).length;
1484
1918
  const scopeRows = selectScope === "page" || manual ? rows : processedRows;
@@ -1526,10 +1960,11 @@ function createTable(options) {
1526
1960
  canUndo: undoStack.canUndo,
1527
1961
  canRedo: undoStack.canRedo,
1528
1962
  issues: Object.freeze(issues),
1963
+ pendingCells: validation.cells(),
1529
1964
  isSelected: (key) => selected.has(key),
1530
1965
  getSort: (columnId) => sortIndex.get(columnId),
1531
1966
  getColumn: (columnId) => byId.get(columnId),
1532
- getRow: (key) => treeRow ? treeRow(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)
1533
1968
  });
1534
1969
  }
1535
1970
  function getSnapshot() {
@@ -1538,6 +1973,11 @@ function createTable(options) {
1538
1973
  }
1539
1974
  function notify() {
1540
1975
  snapshot = void 0;
1976
+ if (deferValidationNotify) {
1977
+ validationNotifyRequested = true;
1978
+ return;
1979
+ }
1980
+ if (transaction) return;
1541
1981
  if (listeners.size === 0) return;
1542
1982
  const next = getSnapshot();
1543
1983
  for (const listener of listeners) listener(next);
@@ -1702,6 +2142,7 @@ function createTable(options) {
1702
2142
  const working = [...data];
1703
2143
  const model = treeEnabled ? treeStage(data, resolveColumns(definitions).byId, lazyVersion) : void 0;
1704
2144
  const replacements = /* @__PURE__ */ new Map();
2145
+ const cache = new Map(lazyChildren);
1705
2146
  const originalOf = (node) => replacements.get(node) ?? node.row.original;
1706
2147
  const issues = [...recoveredIssues];
1707
2148
  const changes = [];
@@ -1777,7 +2218,16 @@ function createTable(options) {
1777
2218
  } else value = edit.value;
1778
2219
  const previous = column.getValue(original);
1779
2220
  if (Object.is(previous, value)) continue;
1780
- 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
+ }
1781
2231
  if (updated === void 0) {
1782
2232
  issues.push(issue("read_only_cell", `The "${column.id}" column has no way to write a value.`, {
1783
2233
  rowKey: row.key,
@@ -1785,20 +2235,37 @@ function createTable(options) {
1785
2235
  }));
1786
2236
  continue;
1787
2237
  }
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
+ }
1788
2245
  if (!node) working[row.index] = updated;
1789
2246
  else {
2247
+ const planned = /* @__PURE__ */ new Map([[node, updated]]);
2248
+ const plannedCache = /* @__PURE__ */ new Map();
1790
2249
  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;
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;
1799
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;
1800
2268
  if (options.getChildren || options.setChildren) working[cursor.sourceIndex] = originalOf(cursor);
1801
- lazyVersion++;
1802
2269
  }
1803
2270
  changes.push({
1804
2271
  rowKey: row.key,
@@ -1813,17 +2280,390 @@ function createTable(options) {
1813
2280
  else {
1814
2281
  status = issues.length > 0 ? "partial" : "applied";
1815
2282
  data = working;
1816
- if (record) undoStack.record(changes);
1817
- 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);
1818
2302
  notify();
1819
2303
  }
1820
- if (issues.length > 0) options.onEditIssues?.(issues);
2304
+ if (issues.length > 0 && !transaction) options.onEditIssues?.(issues);
1821
2305
  return Object.freeze({
1822
2306
  status,
1823
2307
  changes: Object.freeze(changes),
1824
- issues: Object.freeze(issues)
2308
+ issues: Object.freeze(issues),
2309
+ rowChanges: Object.freeze([]),
2310
+ pendingCells: Object.freeze([])
2311
+ });
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])
1825
2341
  });
1826
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
+ }
1827
2667
  function rangeEdits(origin, matrix) {
1828
2668
  const { rows, columns } = getSnapshot();
1829
2669
  const edits = [];
@@ -1854,6 +2694,15 @@ function createTable(options) {
1854
2694
  snapshot = void 0;
1855
2695
  }
1856
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
+ },
1857
2706
  getSnapshot,
1858
2707
  getState: () => state,
1859
2708
  subscribe(listener) {
@@ -1875,6 +2724,8 @@ function createTable(options) {
1875
2724
  },
1876
2725
  setData(next) {
1877
2726
  if (next === data) return;
2727
+ validation.cancel();
2728
+ pendingChanges.entries.clear();
1878
2729
  abortLoads(true);
1879
2730
  lazyChildren.clear();
1880
2731
  childStatuses.clear();
@@ -1889,6 +2740,7 @@ function createTable(options) {
1889
2740
  setColumns(next) {
1890
2741
  if (next === definitions) return;
1891
2742
  definitions = next;
2743
+ validation.cancel();
1892
2744
  notify();
1893
2745
  },
1894
2746
  setRowCount(next) {
@@ -2024,18 +2876,22 @@ function createTable(options) {
2024
2876
  commit({ columnOrder: order });
2025
2877
  },
2026
2878
  edit(edits) {
2027
- return applyChanges(Array.isArray(edits) ? edits : [edits], true);
2879
+ return validation.edit(Array.isArray(edits) ? edits : [edits]);
2028
2880
  },
2029
2881
  undo() {
2030
- const changes = undoStack.undo();
2031
- if (!changes) return false;
2032
- 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");
2033
2887
  return true;
2034
2888
  },
2035
2889
  redo() {
2036
- const changes = undoStack.redo();
2037
- if (!changes) return false;
2038
- 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");
2039
2895
  return true;
2040
2896
  },
2041
2897
  copy(range, { escapeFormulas = false } = {}) {
@@ -2057,7 +2913,7 @@ function createTable(options) {
2057
2913
  return toDelimited(matrix);
2058
2914
  },
2059
2915
  paste(origin, text) {
2060
- if (!Number.isInteger(origin.row) || !Number.isInteger(origin.column) || origin.row < 0 || origin.column < 0) return applyChanges([], true, [issue("invalid_value", "The paste origin must use non-negative whole row and column indices.")]);
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.")]);
2061
2917
  const { rows, columns } = getSnapshot();
2062
2918
  const parsed = readDelimited(text, {
2063
2919
  maxRows: Math.max(0, rows.length - origin.row),
@@ -2066,18 +2922,19 @@ function createTable(options) {
2066
2922
  maxLength: maxPasteLength
2067
2923
  });
2068
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.")] : [];
2069
- return applyChanges(rangeEdits(origin, parsed.rows), true, issues);
2925
+ return validation.edit(rangeEdits(origin, parsed.rows), issues);
2070
2926
  },
2071
2927
  clear(range) {
2072
2928
  const matrix = Array.from({ length: range.bottom - range.top + 1 }, () => Array.from({ length: range.right - range.left + 1 }, () => ""));
2073
2929
  const current = getSnapshot();
2074
- return applyChanges(rangeEdits({
2930
+ const edits = rangeEdits({
2075
2931
  row: range.top,
2076
2932
  column: range.left
2077
2933
  }, matrix).filter((edit) => {
2078
2934
  const row = current.getRow(edit.rowKey);
2079
2935
  return row !== void 0 && current.getColumn(edit.column)?.isEditable(row.original) === true;
2080
- }), true);
2936
+ });
2937
+ return validation.edit(edits);
2081
2938
  },
2082
2939
  exportRows({ format = "csv", columns: ids, pageOnly = false, headers = true, escapeFormulas = true, depth = false, indent = "" } = {}) {
2083
2940
  const current = getSnapshot();
@@ -2102,6 +2959,7 @@ function createTable(options) {
2102
2959
  },
2103
2960
  destroy() {
2104
2961
  destroyed = true;
2962
+ validation.cancel();
2105
2963
  abortLoads(true);
2106
2964
  listeners.clear();
2107
2965
  }