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