@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.d.ts +99 -37
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +885 -27
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
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 (
|
|
1817
|
-
|
|
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
|
|
2879
|
+
return validation.edit(Array.isArray(edits) ? edits : [edits]);
|
|
2028
2880
|
},
|
|
2029
2881
|
undo() {
|
|
2030
|
-
const
|
|
2031
|
-
if (!
|
|
2032
|
-
|
|
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
|
|
2037
|
-
if (!
|
|
2038
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
})
|
|
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
|
}
|