@vueye-table/core 3.0.0-alpha.1 → 3.0.0-alpha.11
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 +6 -0
- package/dist/index.d.ts +306 -69
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2587 -184
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8,13 +8,34 @@ function humanize(id) {
|
|
|
8
8
|
return sentence.charAt(0).toUpperCase() + sentence.slice(1);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/issues.ts
|
|
13
|
+
function issue(code, message, details = {}) {
|
|
14
|
+
return Object.freeze({
|
|
15
|
+
code,
|
|
16
|
+
message,
|
|
17
|
+
...details
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
11
21
|
//#endregion
|
|
12
22
|
//#region src/path.ts
|
|
23
|
+
/**
|
|
24
|
+
* Dotted property paths.
|
|
25
|
+
*
|
|
26
|
+
* A column addresses nested data with a path such as `"address.city"`. The types below give every
|
|
27
|
+
* such path its value type, and the functions read and write through a path without mutating.
|
|
28
|
+
*/
|
|
13
29
|
function isRecord$1(value) {
|
|
14
30
|
return typeof value === "object" && value !== null;
|
|
15
31
|
}
|
|
32
|
+
/** Whether every path segment can be read or written without addressing a prototype. */
|
|
33
|
+
function isSafePath(path) {
|
|
34
|
+
return path.split(".").every((segment) => segment !== "__proto__" && segment !== "constructor" && segment !== "prototype");
|
|
35
|
+
}
|
|
16
36
|
/** Read the value at a dotted path, or `undefined` when a step is missing. */
|
|
17
37
|
function getPath(source, path) {
|
|
38
|
+
if (!isSafePath(path)) return;
|
|
18
39
|
let current = source;
|
|
19
40
|
for (const segment of path.split(".")) {
|
|
20
41
|
if (!isRecord$1(current)) return;
|
|
@@ -24,13 +45,18 @@ function getPath(source, path) {
|
|
|
24
45
|
}
|
|
25
46
|
/**
|
|
26
47
|
* Return a copy of `source` with `value` at `path`. Only the objects along the path are copied;
|
|
27
|
-
* everything else is shared. Missing steps are created as plain objects.
|
|
48
|
+
* everything else is shared. Missing steps are created as plain objects. Unsafe paths return
|
|
49
|
+
* `source` unchanged and report an `unsafe_path` issue through `onIssue`, when supplied.
|
|
28
50
|
*/
|
|
29
|
-
function setPath(source, path, value) {
|
|
51
|
+
function setPath(source, path, value, onIssue) {
|
|
52
|
+
if (!isSafePath(path)) {
|
|
53
|
+
onIssue?.(issue("unsafe_path", `The path "${path}" addresses a prototype and cannot be written.`, { column: path }));
|
|
54
|
+
return source;
|
|
55
|
+
}
|
|
30
56
|
const [head, ...rest] = path.split(".");
|
|
31
57
|
const base = isRecord$1(source) ? { ...source } : {};
|
|
32
58
|
if (head === void 0) return source;
|
|
33
|
-
base[head] = rest.length === 0 ? value : setPath(base[head], rest.join("."), value);
|
|
59
|
+
base[head] = rest.length === 0 ? value : setPath(Object.hasOwn(base, head) ? base[head] : void 0, rest.join("."), value);
|
|
34
60
|
return base;
|
|
35
61
|
}
|
|
36
62
|
|
|
@@ -197,14 +223,15 @@ function parseAs(input, type) {
|
|
|
197
223
|
function resolveColumn(definition) {
|
|
198
224
|
const def = definition;
|
|
199
225
|
const { accessor } = def;
|
|
200
|
-
const
|
|
226
|
+
const safe = isSafePath(def.id);
|
|
227
|
+
const getValue = safe ? accessor ?? ((row) => getPath(row, def.id)) : () => void 0;
|
|
201
228
|
const format = def.format ?? ((value) => formatValue(value));
|
|
202
229
|
const compare = def.compare ?? compareValues;
|
|
203
230
|
const customFilter = def.filter;
|
|
204
231
|
const customParse = def.parse;
|
|
205
232
|
const editable = def.editable ?? false;
|
|
206
233
|
const customSetValue = def.setValue;
|
|
207
|
-
const canWrite = accessor === void 0 || customSetValue !== void 0;
|
|
234
|
+
const canWrite = safe && (accessor === void 0 || customSetValue !== void 0);
|
|
208
235
|
return Object.freeze({
|
|
209
236
|
id: def.id,
|
|
210
237
|
header: def.header ?? humanize(def.id),
|
|
@@ -241,6 +268,7 @@ function resolveColumn(definition) {
|
|
|
241
268
|
return parseAs(input, def.type ?? typeOf(getValue(row)) ?? typeOf(sample) ?? "text");
|
|
242
269
|
},
|
|
243
270
|
setValue(row, value) {
|
|
271
|
+
if (!safe) return;
|
|
244
272
|
if (customSetValue) return customSetValue(row, value);
|
|
245
273
|
return accessor === void 0 ? setPath(row, def.id, value) : void 0;
|
|
246
274
|
}
|
|
@@ -258,6 +286,10 @@ function resolveColumn(definition) {
|
|
|
258
286
|
function quote(field, delimiter) {
|
|
259
287
|
return field.includes(delimiter) || field.includes("\"") || field.includes("\n") || field.includes("\r") ? `"${field.replaceAll("\"", "\"\"")}"` : field;
|
|
260
288
|
}
|
|
289
|
+
/** Escape formula-like display text; a finite number formatted as a number stays numeric. */
|
|
290
|
+
function escapeFormula(text, value) {
|
|
291
|
+
return !(typeof value === "number" && Number.isFinite(value) && /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/u.test(text)) && /^[=+\-@\t\r\n=+-@]/u.test(text) ? "'" + text : text;
|
|
292
|
+
}
|
|
261
293
|
/** Write a matrix of text as delimited lines joined with `\n`. */
|
|
262
294
|
function toDelimited(matrix, delimiter = " ") {
|
|
263
295
|
return matrix.map((line) => line.map((field) => quote(field, delimiter)).join(delimiter)).join("\n");
|
|
@@ -267,39 +299,92 @@ function toDelimited(matrix, delimiter = " ") {
|
|
|
267
299
|
* and embedded line breaks, and ignores one trailing line break.
|
|
268
300
|
*/
|
|
269
301
|
function parseDelimited(text, delimiter = " ") {
|
|
302
|
+
return readDelimited(text, {
|
|
303
|
+
maxRows: Infinity,
|
|
304
|
+
maxColumns: Infinity,
|
|
305
|
+
maxCells: Infinity,
|
|
306
|
+
maxLength: Infinity
|
|
307
|
+
}, delimiter).rows;
|
|
308
|
+
}
|
|
309
|
+
/** Read complete fields within a grid and work budget, without materializing discarded fields. */
|
|
310
|
+
function readDelimited(text, limits, delimiter = " ") {
|
|
270
311
|
const rows = [];
|
|
312
|
+
if (limits.maxRows === 0 || limits.maxColumns === 0) return {
|
|
313
|
+
rows,
|
|
314
|
+
truncated: text.length > 0
|
|
315
|
+
};
|
|
271
316
|
let row = [];
|
|
272
317
|
let field = "";
|
|
273
318
|
let quoted = false;
|
|
319
|
+
let started = false;
|
|
274
320
|
let index = 0;
|
|
321
|
+
let column = 0;
|
|
322
|
+
let cells = 0;
|
|
323
|
+
let truncated = false;
|
|
324
|
+
const end = Math.min(text.length, limits.maxLength);
|
|
275
325
|
const endField = () => {
|
|
276
|
-
row.push(field);
|
|
326
|
+
if (column < limits.maxColumns) row.push(field);
|
|
327
|
+
else truncated = true;
|
|
328
|
+
column += 1;
|
|
329
|
+
cells += 1;
|
|
277
330
|
field = "";
|
|
331
|
+
started = false;
|
|
278
332
|
};
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
rows.push(row);
|
|
282
|
-
row = [];
|
|
333
|
+
const append = (char) => {
|
|
334
|
+
if (column < limits.maxColumns) field += char;
|
|
283
335
|
};
|
|
284
|
-
while (index <
|
|
336
|
+
while (index < end) {
|
|
285
337
|
const char = text.charAt(index);
|
|
286
338
|
if (quoted) {
|
|
287
339
|
if (char === "\"") {
|
|
288
|
-
if (text.charAt(index + 1) === "\"") {
|
|
289
|
-
|
|
340
|
+
if (index + 1 < end && text.charAt(index + 1) === "\"") {
|
|
341
|
+
append("\"");
|
|
290
342
|
index += 1;
|
|
291
343
|
} else quoted = false;
|
|
292
|
-
} else
|
|
293
|
-
} else if (char === "\"" &&
|
|
294
|
-
else if (char === delimiter)
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
344
|
+
} else append(char);
|
|
345
|
+
} else if (char === "\"" && !started) quoted = true;
|
|
346
|
+
else if (char === delimiter) {
|
|
347
|
+
endField();
|
|
348
|
+
index += 1;
|
|
349
|
+
if (cells >= limits.maxCells || rows.length + 1 >= limits.maxRows && column >= limits.maxColumns) {
|
|
350
|
+
rows.push(row);
|
|
351
|
+
return {
|
|
352
|
+
rows,
|
|
353
|
+
truncated: true
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
continue;
|
|
357
|
+
} else if (char === "\n" || char === "\r") {
|
|
358
|
+
if (char === "\r" && index + 1 < end && text.charAt(index + 1) === "\n") index += 1;
|
|
359
|
+
endField();
|
|
360
|
+
rows.push(row);
|
|
361
|
+
row = [];
|
|
362
|
+
column = 0;
|
|
363
|
+
index += 1;
|
|
364
|
+
if (cells >= limits.maxCells || rows.length >= limits.maxRows) return {
|
|
365
|
+
rows,
|
|
366
|
+
truncated: truncated || index < text.length
|
|
367
|
+
};
|
|
368
|
+
continue;
|
|
369
|
+
} else append(char);
|
|
370
|
+
started = true;
|
|
299
371
|
index += 1;
|
|
300
372
|
}
|
|
301
|
-
if (
|
|
302
|
-
|
|
373
|
+
if (end < text.length) {
|
|
374
|
+
if (row.length > 0) rows.push(row);
|
|
375
|
+
return {
|
|
376
|
+
rows,
|
|
377
|
+
truncated: true
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
if (started || column > 0) {
|
|
381
|
+
endField();
|
|
382
|
+
rows.push(row);
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
rows,
|
|
386
|
+
truncated
|
|
387
|
+
};
|
|
303
388
|
}
|
|
304
389
|
|
|
305
390
|
//#endregion
|
|
@@ -380,15 +465,23 @@ const ARROWS = {
|
|
|
380
465
|
ArrowLeft: "left",
|
|
381
466
|
ArrowRight: "right"
|
|
382
467
|
};
|
|
383
|
-
|
|
384
|
-
* Map a key press to a grid command, spreadsheet style: arrows move (Shift extends, Ctrl or Cmd
|
|
385
|
-
* jumps to the edge), Tab moves across, Home and End reach the row's ends, Enter or F2 edit,
|
|
386
|
-
* a printable character starts editing with that character, Delete and Backspace clear, Escape
|
|
387
|
-
* collapses the selection, and Ctrl or Cmd with A, Z, Y select all, undo, and redo.
|
|
388
|
-
*/
|
|
389
|
-
function gridCommand(event) {
|
|
468
|
+
function gridCommand(event, tree) {
|
|
390
469
|
const mod = event.ctrlKey === true || event.metaKey === true;
|
|
391
470
|
const shift = event.shiftKey === true;
|
|
471
|
+
if (tree && !mod && !event.altKey) {
|
|
472
|
+
if (event.key === "*") return {
|
|
473
|
+
type: "tree",
|
|
474
|
+
action: "siblings"
|
|
475
|
+
};
|
|
476
|
+
if (!shift && event.key === "ArrowRight" && tree.canExpand) return {
|
|
477
|
+
type: "tree",
|
|
478
|
+
action: tree.expanded ? "child" : "expand"
|
|
479
|
+
};
|
|
480
|
+
if (!shift && event.key === "ArrowLeft") return {
|
|
481
|
+
type: "tree",
|
|
482
|
+
action: tree.canExpand && tree.expanded ? "collapse" : "parent"
|
|
483
|
+
};
|
|
484
|
+
}
|
|
392
485
|
const direction = ARROWS[event.key];
|
|
393
486
|
if (direction) return {
|
|
394
487
|
type: "move",
|
|
@@ -462,6 +555,214 @@ function createUndoStack(limit = 100) {
|
|
|
462
555
|
clear() {
|
|
463
556
|
past.length = 0;
|
|
464
557
|
future.length = 0;
|
|
558
|
+
},
|
|
559
|
+
transform(map) {
|
|
560
|
+
const update = (entries) => entries.flatMap((entry) => {
|
|
561
|
+
const value = map(entry);
|
|
562
|
+
return value === void 0 ? [] : [value];
|
|
563
|
+
});
|
|
564
|
+
const nextPast = update(past);
|
|
565
|
+
const nextFuture = update(future);
|
|
566
|
+
past.splice(0, past.length, ...nextPast);
|
|
567
|
+
future.splice(0, future.length, ...nextFuture);
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
//#endregion
|
|
573
|
+
//#region src/virtualizer.ts
|
|
574
|
+
const FALLBACK_SIZE = 40;
|
|
575
|
+
/** Layout arithmetic for either axis. All viewport values and measurements come from the caller. */
|
|
576
|
+
function createVirtualizer(initialOptions) {
|
|
577
|
+
let options = initialOptions;
|
|
578
|
+
let count = 0;
|
|
579
|
+
let overscan = 5;
|
|
580
|
+
let fixedSize;
|
|
581
|
+
let keys = [];
|
|
582
|
+
let byKey = /* @__PURE__ */ new Map();
|
|
583
|
+
let sizes = [];
|
|
584
|
+
let prefix = [0];
|
|
585
|
+
let offset = 0;
|
|
586
|
+
let viewportSize = 0;
|
|
587
|
+
let cached;
|
|
588
|
+
const measured = /* @__PURE__ */ new Map();
|
|
589
|
+
const problems = /* @__PURE__ */ new Map();
|
|
590
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
591
|
+
function recover(code, message) {
|
|
592
|
+
if (!problems.has(code)) {
|
|
593
|
+
const problem = issue(code, message);
|
|
594
|
+
problems.set(code, problem);
|
|
595
|
+
options.onIssue?.(problem);
|
|
596
|
+
cached = void 0;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
function validSize(value) {
|
|
600
|
+
if (Number.isFinite(value) && value > 0) return value;
|
|
601
|
+
recover("invalid_virtual_size", `Item size ${String(value)} is not positive and finite; 40 is used.`);
|
|
602
|
+
return FALLBACK_SIZE;
|
|
603
|
+
}
|
|
604
|
+
function configure(next) {
|
|
605
|
+
options = next;
|
|
606
|
+
problems.clear();
|
|
607
|
+
count = Number.isSafeInteger(next.count) && next.count >= 0 ? next.count : 0;
|
|
608
|
+
if (count !== next.count) recover("invalid_virtual_option", "Virtual item count must be a non-negative safe integer; 0 is used.");
|
|
609
|
+
const requestedOverscan = next.overscan ?? 5;
|
|
610
|
+
overscan = Number.isSafeInteger(requestedOverscan) && requestedOverscan >= 0 ? requestedOverscan : 5;
|
|
611
|
+
if (overscan !== requestedOverscan) recover("invalid_virtual_option", "Overscan must be a non-negative safe integer; 5 is used.");
|
|
612
|
+
const estimate = typeof next.estimateSize === "number" ? validSize(next.estimateSize) : void 0;
|
|
613
|
+
const nextKeys = [];
|
|
614
|
+
const nextSizes = [];
|
|
615
|
+
const nextByKey = /* @__PURE__ */ new Map();
|
|
616
|
+
let firstChanged = count;
|
|
617
|
+
let uniform = estimate !== void 0;
|
|
618
|
+
for (let index = 0; index < count; index += 1) {
|
|
619
|
+
let key = next.getKey ? next.getKey(index) : index;
|
|
620
|
+
if (!(typeof key === "string" || typeof key === "number" && Number.isFinite(key))) {
|
|
621
|
+
recover("invalid_virtual_option", `Item ${index} has an invalid key; its index is used.`);
|
|
622
|
+
key = index;
|
|
623
|
+
}
|
|
624
|
+
if (nextByKey.has(key)) {
|
|
625
|
+
recover("duplicate_virtual_key", `Item ${index} repeats a key; a distinct fallback key is used.`);
|
|
626
|
+
key = JSON.stringify(["virtual", index]);
|
|
627
|
+
while (nextByKey.has(key)) key += ":";
|
|
628
|
+
}
|
|
629
|
+
const size = measured.get(key) ?? estimate ?? validSize(next.estimateSize(index));
|
|
630
|
+
uniform &&= size === estimate;
|
|
631
|
+
if (keys[index] !== key || sizes[index] !== size) firstChanged = Math.min(firstChanged, index);
|
|
632
|
+
nextKeys.push(key);
|
|
633
|
+
nextSizes.push(size);
|
|
634
|
+
nextByKey.set(key, index);
|
|
635
|
+
}
|
|
636
|
+
if (fixedSize !== void 0 || uniform) prefix = [0];
|
|
637
|
+
else prefix.length = Math.min(prefix.length, firstChanged + 1);
|
|
638
|
+
keys = nextKeys;
|
|
639
|
+
sizes = nextSizes;
|
|
640
|
+
byKey = nextByKey;
|
|
641
|
+
fixedSize = uniform ? estimate : void 0;
|
|
642
|
+
cached = void 0;
|
|
643
|
+
}
|
|
644
|
+
function startOf(index) {
|
|
645
|
+
if (fixedSize !== void 0) return index * fixedSize;
|
|
646
|
+
while (prefix.length <= index) {
|
|
647
|
+
const at = prefix.length - 1;
|
|
648
|
+
prefix.push(prefix[at] + sizes[at]);
|
|
649
|
+
}
|
|
650
|
+
return prefix[index];
|
|
651
|
+
}
|
|
652
|
+
function totalSize() {
|
|
653
|
+
return startOf(count);
|
|
654
|
+
}
|
|
655
|
+
function indexAt(position) {
|
|
656
|
+
if (fixedSize !== void 0) return Math.min(count - 1, Math.floor(position / fixedSize));
|
|
657
|
+
let low = 0;
|
|
658
|
+
let high = count - 1;
|
|
659
|
+
while (low < high) {
|
|
660
|
+
const middle = Math.floor((low + high) / 2);
|
|
661
|
+
if (startOf(middle + 1) <= position) low = middle + 1;
|
|
662
|
+
else high = middle;
|
|
663
|
+
}
|
|
664
|
+
return low;
|
|
665
|
+
}
|
|
666
|
+
function getWindow() {
|
|
667
|
+
if (cached) return cached;
|
|
668
|
+
const total = totalSize();
|
|
669
|
+
const clampedOffset = Math.min(offset, Math.max(0, total - viewportSize));
|
|
670
|
+
let first = 0;
|
|
671
|
+
let last = -1;
|
|
672
|
+
if (count > 0 && viewportSize > 0) {
|
|
673
|
+
first = Math.max(0, indexAt(clampedOffset) - overscan);
|
|
674
|
+
const edge = Math.min(total, clampedOffset + viewportSize);
|
|
675
|
+
let visibleEnd = indexAt(edge);
|
|
676
|
+
if (startOf(visibleEnd) >= edge) visibleEnd -= 1;
|
|
677
|
+
last = Math.min(count - 1, visibleEnd + overscan);
|
|
678
|
+
}
|
|
679
|
+
const items = [];
|
|
680
|
+
for (let index = first; index <= last; index += 1) items.push(Object.freeze({
|
|
681
|
+
index,
|
|
682
|
+
key: keys[index],
|
|
683
|
+
start: startOf(index),
|
|
684
|
+
size: sizes[index]
|
|
685
|
+
}));
|
|
686
|
+
const paddingStart = items.length > 0 ? startOf(first) : 0;
|
|
687
|
+
const paddingEnd = items.length > 0 ? Math.max(0, total - startOf(last + 1)) : total;
|
|
688
|
+
cached = Object.freeze({
|
|
689
|
+
items: Object.freeze(items),
|
|
690
|
+
totalSize: total,
|
|
691
|
+
paddingStart,
|
|
692
|
+
paddingEnd,
|
|
693
|
+
startIndex: first,
|
|
694
|
+
endIndex: last,
|
|
695
|
+
offset: clampedOffset,
|
|
696
|
+
viewportSize,
|
|
697
|
+
issues: Object.freeze([...problems.values()])
|
|
698
|
+
});
|
|
699
|
+
return cached;
|
|
700
|
+
}
|
|
701
|
+
function notify() {
|
|
702
|
+
cached = void 0;
|
|
703
|
+
if (listeners.size > 0) {
|
|
704
|
+
const view = getWindow();
|
|
705
|
+
for (const listener of listeners) listener(view);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
configure(initialOptions);
|
|
709
|
+
return {
|
|
710
|
+
setOptions(next) {
|
|
711
|
+
configure(next);
|
|
712
|
+
notify();
|
|
713
|
+
},
|
|
714
|
+
setViewport(nextOffset, nextSize) {
|
|
715
|
+
const issueCount = problems.size;
|
|
716
|
+
const safeOffset = Number.isFinite(nextOffset) && nextOffset >= 0 ? nextOffset : 0;
|
|
717
|
+
const safeSize = Number.isFinite(nextSize) && nextSize >= 0 ? nextSize : 0;
|
|
718
|
+
if (safeOffset !== nextOffset || safeSize !== nextSize) recover("invalid_virtual_viewport", "Viewport offset and size must be non-negative and finite; invalid values use 0.");
|
|
719
|
+
if (offset === safeOffset && viewportSize === safeSize && problems.size === issueCount) return;
|
|
720
|
+
offset = safeOffset;
|
|
721
|
+
viewportSize = safeSize;
|
|
722
|
+
notify();
|
|
723
|
+
},
|
|
724
|
+
measure(key, size) {
|
|
725
|
+
const index = byKey.get(key);
|
|
726
|
+
if (index === void 0) return;
|
|
727
|
+
if (!Number.isFinite(size) || size <= 0) {
|
|
728
|
+
recover("invalid_virtual_size", "Measured size must be positive and finite; the previous size is retained.");
|
|
729
|
+
notify();
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
measured.set(key, size);
|
|
733
|
+
if (sizes[index] === size) return;
|
|
734
|
+
if (fixedSize !== void 0) {
|
|
735
|
+
fixedSize = void 0;
|
|
736
|
+
prefix = [0];
|
|
737
|
+
} else prefix.length = Math.min(prefix.length, index + 1);
|
|
738
|
+
sizes[index] = size;
|
|
739
|
+
notify();
|
|
740
|
+
},
|
|
741
|
+
getWindow,
|
|
742
|
+
getItem(index) {
|
|
743
|
+
return Number.isSafeInteger(index) && index >= 0 && index < count ? Object.freeze({
|
|
744
|
+
index,
|
|
745
|
+
key: keys[index],
|
|
746
|
+
start: startOf(index),
|
|
747
|
+
size: sizes[index]
|
|
748
|
+
}) : void 0;
|
|
749
|
+
},
|
|
750
|
+
getOffsetForIndex(index, align = "auto") {
|
|
751
|
+
if (count === 0) return 0;
|
|
752
|
+
if (!Number.isFinite(index) || !Number.isInteger(index)) recover("invalid_virtual_option", "Scroll index must be a finite integer; it is clamped to a valid item.");
|
|
753
|
+
const target = Math.min(count - 1, Math.max(0, Number.isFinite(index) ? Math.trunc(index) : 0));
|
|
754
|
+
const start = startOf(target);
|
|
755
|
+
const end = start + sizes[target];
|
|
756
|
+
const current = getWindow().offset;
|
|
757
|
+
let result = start;
|
|
758
|
+
if (align === "center") result = start - (viewportSize - sizes[target]) / 2;
|
|
759
|
+
else if (align === "end") result = end - viewportSize;
|
|
760
|
+
else if (align === "auto") result = start < current ? start : end > current + viewportSize ? end - viewportSize : current;
|
|
761
|
+
return Math.min(Math.max(0, totalSize() - viewportSize), Math.max(0, result));
|
|
762
|
+
},
|
|
763
|
+
subscribe(listener) {
|
|
764
|
+
listeners.add(listener);
|
|
765
|
+
return () => listeners.delete(listener);
|
|
465
766
|
}
|
|
466
767
|
};
|
|
467
768
|
}
|
|
@@ -526,6 +827,10 @@ function filterRows(rows, columns, search, filters) {
|
|
|
526
827
|
}
|
|
527
828
|
/** Sort rows by each rule in turn. The sort is stable: ties keep their incoming order. */
|
|
528
829
|
function sortRows(rows, columns, sorting) {
|
|
830
|
+
if (!sorting.some((rule) => columns.some((column) => column.id === rule.column))) return rows;
|
|
831
|
+
return rows.toSorted(compareRows(columns, sorting));
|
|
832
|
+
}
|
|
833
|
+
function compareRows(columns, sorting) {
|
|
529
834
|
const byId = new Map(columns.map((column) => [column.id, column]));
|
|
530
835
|
const rules = sorting.flatMap((rule) => {
|
|
531
836
|
const column = byId.get(rule.column);
|
|
@@ -534,8 +839,7 @@ function sortRows(rows, columns, sorting) {
|
|
|
534
839
|
sign: rule.direction === "desc" ? -1 : 1
|
|
535
840
|
}] : [];
|
|
536
841
|
});
|
|
537
|
-
|
|
538
|
-
return rows.toSorted((left, right) => {
|
|
842
|
+
return (left, right) => {
|
|
539
843
|
for (const { column, sign } of rules) {
|
|
540
844
|
const leftValue = left.getValue(column.id);
|
|
541
845
|
const rightValue = right.getValue(column.id);
|
|
@@ -545,7 +849,19 @@ function sortRows(rows, columns, sorting) {
|
|
|
545
849
|
if (order !== 0) return order;
|
|
546
850
|
}
|
|
547
851
|
return 0;
|
|
548
|
-
}
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
/** Stable merge: an existing row wins a tie against an incoming row. */
|
|
855
|
+
function mergeRows(previous, incoming, columns, sorting) {
|
|
856
|
+
const compare = compareRows(columns, sorting);
|
|
857
|
+
const added = sortRows(incoming, columns, sorting);
|
|
858
|
+
const result = [];
|
|
859
|
+
let left = 0;
|
|
860
|
+
let right = 0;
|
|
861
|
+
while (left < previous.length && right < added.length) result.push(compare(previous[left], added[right]) <= 0 ? previous[left++] : added[right++]);
|
|
862
|
+
while (left < previous.length) result.push(previous[left++]);
|
|
863
|
+
while (right < added.length) result.push(added[right++]);
|
|
864
|
+
return result;
|
|
549
865
|
}
|
|
550
866
|
/** The number of pages for a row count. An empty table still has one page. */
|
|
551
867
|
function countPages(rowCount, pageSize) {
|
|
@@ -593,10 +909,54 @@ function paginationItems(page, pageCount, siblings = 1) {
|
|
|
593
909
|
return items;
|
|
594
910
|
}
|
|
595
911
|
|
|
912
|
+
//#endregion
|
|
913
|
+
//#region src/row.ts
|
|
914
|
+
/** Disjoint namespaces and typed keys avoid collisions between 1, "1", and detail-like ids. */
|
|
915
|
+
function getRowItemKey(key, kind = "row") {
|
|
916
|
+
return JSON.stringify([
|
|
917
|
+
kind,
|
|
918
|
+
typeof key,
|
|
919
|
+
key
|
|
920
|
+
]);
|
|
921
|
+
}
|
|
922
|
+
function createRow(original, key, index, columns, canExpand = false) {
|
|
923
|
+
const values = /* @__PURE__ */ new Map();
|
|
924
|
+
const displays = /* @__PURE__ */ new Map();
|
|
925
|
+
const getValue = (columnId) => {
|
|
926
|
+
if (values.has(columnId)) return values.get(columnId);
|
|
927
|
+
const value = columns.get(columnId)?.getValue(original);
|
|
928
|
+
values.set(columnId, value);
|
|
929
|
+
return value;
|
|
930
|
+
};
|
|
931
|
+
return Object.freeze({
|
|
932
|
+
key,
|
|
933
|
+
index,
|
|
934
|
+
original,
|
|
935
|
+
canExpand,
|
|
936
|
+
isExpanded: false,
|
|
937
|
+
depth: 0,
|
|
938
|
+
parentKey: void 0,
|
|
939
|
+
childCount: 0,
|
|
940
|
+
childStatus: "loaded",
|
|
941
|
+
selection: "none",
|
|
942
|
+
isDirty: false,
|
|
943
|
+
cellIssues: void 0,
|
|
944
|
+
getValue,
|
|
945
|
+
getDisplay(columnId) {
|
|
946
|
+
const cached = displays.get(columnId);
|
|
947
|
+
if (cached !== void 0) return cached;
|
|
948
|
+
const column = columns.get(columnId);
|
|
949
|
+
const display = column ? column.format(getValue(columnId), original) : "";
|
|
950
|
+
displays.set(columnId, display);
|
|
951
|
+
return display;
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
|
|
596
956
|
//#endregion
|
|
597
957
|
//#region src/state.ts
|
|
598
958
|
const DEFAULT_PAGE_SIZE = 10;
|
|
599
|
-
function createInitialState(patch = {}) {
|
|
959
|
+
function createInitialState(patch = {}, onIssue) {
|
|
600
960
|
return freezeState({
|
|
601
961
|
sorting: patch.sorting ?? [],
|
|
602
962
|
search: patch.search ?? "",
|
|
@@ -606,145 +966,1051 @@ function createInitialState(patch = {}) {
|
|
|
606
966
|
pageSize: 10
|
|
607
967
|
},
|
|
608
968
|
selection: patch.selection ?? [],
|
|
969
|
+
expanded: normalizeExpanded(patch.expanded === void 0 ? [] : patch.expanded, onIssue),
|
|
609
970
|
hiddenColumns: patch.hiddenColumns ?? [],
|
|
610
971
|
columnOrder: patch.columnOrder ?? []
|
|
611
972
|
});
|
|
612
973
|
}
|
|
613
|
-
function mergeState(state, patch) {
|
|
974
|
+
function mergeState(state, patch, onIssue) {
|
|
614
975
|
let changed = false;
|
|
615
976
|
const next = { ...state };
|
|
616
|
-
for (const [key,
|
|
977
|
+
for (const [key, input] of Object.entries(patch)) {
|
|
978
|
+
const value = key === "expanded" && input !== void 0 ? normalizeExpanded(input, onIssue) : input;
|
|
617
979
|
if (value === void 0 || Object.is(next[key], value)) continue;
|
|
618
980
|
if (key === "pagination" && samePagination(next[key], value)) continue;
|
|
981
|
+
if (key === "expanded" && sameExpanded(state.expanded, value)) continue;
|
|
619
982
|
next[key] = value;
|
|
620
983
|
changed = true;
|
|
621
984
|
}
|
|
622
|
-
return changed ? freezeState(next) : state;
|
|
985
|
+
return changed ? freezeState(next, state) : state;
|
|
986
|
+
}
|
|
987
|
+
function normalizeExpanded(value, onIssue) {
|
|
988
|
+
if (value === true) return true;
|
|
989
|
+
if (Array.isArray(value) && value.every((key) => typeof key === "string" || typeof key === "number" && Number.isFinite(key))) return [...new Set(value)];
|
|
990
|
+
onIssue?.(issue("invalid_expanded", "Expanded state must be true or an array of string/finite number keys; an empty array is used."));
|
|
991
|
+
return [];
|
|
992
|
+
}
|
|
993
|
+
function sameExpanded(left, right) {
|
|
994
|
+
return left === right || left !== true && right !== true && left.length === right.length && left.every((key, index) => key === right[index]);
|
|
623
995
|
}
|
|
624
996
|
function samePagination(left, right) {
|
|
625
997
|
return left.page === right.page && left.pageSize === right.pageSize;
|
|
626
998
|
}
|
|
627
|
-
function freezeState(state) {
|
|
999
|
+
function freezeState(state, previous) {
|
|
628
1000
|
return Object.freeze({
|
|
629
|
-
sorting: Object.freeze(state.sorting.map((rule) => Object.freeze({ ...rule }))),
|
|
1001
|
+
sorting: state.sorting === previous?.sorting ? previous.sorting : Object.freeze(state.sorting.map((rule) => Object.freeze({ ...rule }))),
|
|
630
1002
|
search: state.search,
|
|
631
|
-
filters: Object.freeze({ ...state.filters }),
|
|
632
|
-
pagination: Object.freeze({ ...state.pagination }),
|
|
633
|
-
selection: Object.freeze([...state.selection]),
|
|
634
|
-
|
|
635
|
-
|
|
1003
|
+
filters: state.filters === previous?.filters ? previous.filters : Object.freeze({ ...state.filters }),
|
|
1004
|
+
pagination: state.pagination === previous?.pagination ? previous.pagination : Object.freeze({ ...state.pagination }),
|
|
1005
|
+
selection: state.selection === previous?.selection ? previous.selection : Object.freeze([...state.selection]),
|
|
1006
|
+
expanded: state.expanded === previous?.expanded ? previous.expanded : state.expanded === true ? true : Object.freeze([...state.expanded]),
|
|
1007
|
+
hiddenColumns: state.hiddenColumns === previous?.hiddenColumns ? previous.hiddenColumns : Object.freeze([...state.hiddenColumns]),
|
|
1008
|
+
columnOrder: state.columnOrder === previous?.columnOrder ? previous.columnOrder : Object.freeze([...state.columnOrder])
|
|
636
1009
|
});
|
|
637
1010
|
}
|
|
638
1011
|
|
|
639
1012
|
//#endregion
|
|
640
|
-
//#region src/
|
|
641
|
-
function
|
|
642
|
-
return
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
1013
|
+
//#region src/validation.ts
|
|
1014
|
+
function isPromise(value) {
|
|
1015
|
+
return typeof value === "object" && value !== null && "then" in value;
|
|
1016
|
+
}
|
|
1017
|
+
/** Validators receive values; their messages remain plain text. */
|
|
1018
|
+
function runValidation(validate, rowKey, column) {
|
|
1019
|
+
const fail = (message, validationCode) => issue("validation_failed", message, {
|
|
1020
|
+
rowKey,
|
|
1021
|
+
column,
|
|
1022
|
+
validationCode
|
|
646
1023
|
});
|
|
1024
|
+
const read = (result) => {
|
|
1025
|
+
if (result === true) return void 0;
|
|
1026
|
+
if (typeof result === "string") return fail(result);
|
|
1027
|
+
if (result && typeof result.message === "string") return fail(result.message, result.code);
|
|
1028
|
+
return fail("The validator returned an invalid result.");
|
|
1029
|
+
};
|
|
1030
|
+
const thrown = (error) => fail(error instanceof Error ? error.message : String(error));
|
|
1031
|
+
try {
|
|
1032
|
+
const result = validate();
|
|
1033
|
+
return isPromise(result) ? Promise.resolve(result).then(read, thrown) : read(result);
|
|
1034
|
+
} catch (error) {
|
|
1035
|
+
return thrown(error);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
function editorFailure(spec, value) {
|
|
1039
|
+
if (!spec) return void 0;
|
|
1040
|
+
if (spec.maxLength !== void 0 && (!Number.isSafeInteger(spec.maxLength) || spec.maxLength < 0)) return "Editor maxLength must be a non-negative safe integer.";
|
|
1041
|
+
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.";
|
|
1042
|
+
if (spec.options && !spec.options.some((option) => Object.is(option, value))) return "Choose one of the allowed values.";
|
|
1043
|
+
if (spec.maxLength !== void 0 && typeof value === "string" && value.length > spec.maxLength) return `Use at most ${spec.maxLength} characters.`;
|
|
1044
|
+
if (spec.min !== void 0 || spec.max !== void 0) {
|
|
1045
|
+
const number = value instanceof Date ? value.getTime() : value;
|
|
1046
|
+
if (typeof number !== "number" || !Number.isFinite(number)) return "Enter a finite numeric value.";
|
|
1047
|
+
if (spec.min !== void 0 && number < spec.min) return `Use a value of at least ${spec.min}.`;
|
|
1048
|
+
if (spec.max !== void 0 && number > spec.max) return `Use a value of at most ${spec.max}.`;
|
|
1049
|
+
}
|
|
1050
|
+
if (spec.pattern !== void 0) try {
|
|
1051
|
+
if (typeof value !== "string" || !new RegExp(spec.pattern, "u").test(value)) return "The value does not match the required pattern.";
|
|
1052
|
+
} catch {
|
|
1053
|
+
return "The editor pattern is invalid.";
|
|
1054
|
+
}
|
|
647
1055
|
}
|
|
648
1056
|
|
|
649
1057
|
//#endregion
|
|
650
|
-
//#region src/
|
|
651
|
-
function
|
|
652
|
-
const
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
1058
|
+
//#region src/edit-validation.ts
|
|
1059
|
+
function readonlyMap(source) {
|
|
1060
|
+
const copy = new Map(source);
|
|
1061
|
+
const result = {
|
|
1062
|
+
size: copy.size,
|
|
1063
|
+
get: (key) => copy.get(key),
|
|
1064
|
+
has: (key) => copy.has(key),
|
|
1065
|
+
entries: () => copy.entries(),
|
|
1066
|
+
keys: () => copy.keys(),
|
|
1067
|
+
values: () => copy.values(),
|
|
1068
|
+
[Symbol.iterator]: () => copy[Symbol.iterator](),
|
|
1069
|
+
forEach(callback, thisArg) {
|
|
1070
|
+
copy.forEach((value, key) => callback.call(thisArg, value, key, result));
|
|
1071
|
+
}
|
|
659
1072
|
};
|
|
660
|
-
return Object.freeze(
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
1073
|
+
return Object.freeze(result);
|
|
1074
|
+
}
|
|
1075
|
+
/** All promises in one edit settle together. Tokens fence every cell against stale results. */
|
|
1076
|
+
function createEditValidation(host) {
|
|
1077
|
+
const tokens = /* @__PURE__ */ new Map();
|
|
1078
|
+
const pending = /* @__PURE__ */ new Map();
|
|
1079
|
+
const problems = /* @__PURE__ */ new Map();
|
|
1080
|
+
const current = (candidate) => tokens.get(candidate.edit.rowKey)?.get(candidate.edit.column) === candidate.token;
|
|
1081
|
+
const fresh = (candidate) => !candidate.guard || host.snapshot().getRow(candidate.edit.rowKey)?.original === candidate.guard.row;
|
|
1082
|
+
const tokenFor = (rowKey, column) => {
|
|
1083
|
+
const row = tokens.get(rowKey) ?? /* @__PURE__ */ new Map();
|
|
1084
|
+
const token = {};
|
|
1085
|
+
row.set(column, token);
|
|
1086
|
+
tokens.set(rowKey, row);
|
|
1087
|
+
pending.get(rowKey)?.delete(column);
|
|
1088
|
+
problems.get(rowKey)?.delete(column);
|
|
1089
|
+
return token;
|
|
1090
|
+
};
|
|
1091
|
+
const setIssues = (issues) => {
|
|
1092
|
+
for (const problem of issues) {
|
|
1093
|
+
if (problem.rowKey === void 0 || problem.column === void 0) continue;
|
|
1094
|
+
const row = problems.get(problem.rowKey) ?? /* @__PURE__ */ new Map();
|
|
1095
|
+
row.set(problem.column, problem);
|
|
1096
|
+
problems.set(problem.rowKey, row);
|
|
672
1097
|
}
|
|
673
|
-
}
|
|
1098
|
+
};
|
|
1099
|
+
function rowValidation(candidates, failures) {
|
|
1100
|
+
if (!host.validateRow) return [...candidates];
|
|
1101
|
+
const rows = /* @__PURE__ */ new Map();
|
|
1102
|
+
for (const candidate of candidates) {
|
|
1103
|
+
const group = rows.get(candidate.edit.rowKey) ?? [];
|
|
1104
|
+
group.push(candidate);
|
|
1105
|
+
rows.set(candidate.edit.rowKey, group);
|
|
1106
|
+
}
|
|
1107
|
+
const results = [];
|
|
1108
|
+
for (const [key, group] of rows) {
|
|
1109
|
+
const previous = group[0].original;
|
|
1110
|
+
let next = previous;
|
|
1111
|
+
try {
|
|
1112
|
+
for (const candidate of group) next = candidate.column.setValue(next, candidate.edit.value) ?? next;
|
|
1113
|
+
} catch (error) {
|
|
1114
|
+
for (const candidate of group) failures.push(issue("validation_failed", String(error), {
|
|
1115
|
+
rowKey: key,
|
|
1116
|
+
column: candidate.edit.column
|
|
1117
|
+
}));
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
const validation = runValidation(() => host.validateRow(next, previous), key);
|
|
1121
|
+
const accept = (problem) => {
|
|
1122
|
+
if (!problem) return group;
|
|
1123
|
+
for (const candidate of group) if (current(candidate)) failures.push(Object.freeze({
|
|
1124
|
+
...problem,
|
|
1125
|
+
column: candidate.edit.column
|
|
1126
|
+
}));
|
|
1127
|
+
return [];
|
|
1128
|
+
};
|
|
1129
|
+
results.push(isPromise(validation) ? validation.then(accept) : accept(validation));
|
|
1130
|
+
}
|
|
1131
|
+
return results.some(isPromise) ? Promise.all(results.map((result) => Promise.resolve(result))).then((groups) => groups.flat()) : results.flat();
|
|
1132
|
+
}
|
|
1133
|
+
function settle(accepted, all, failures, asynchronous) {
|
|
1134
|
+
return host.batch(() => {
|
|
1135
|
+
const live = accepted.filter((candidate) => current(candidate) && fresh(candidate));
|
|
1136
|
+
const liveKeys = new Set(live);
|
|
1137
|
+
const active = all.filter(current);
|
|
1138
|
+
for (const candidate of active) {
|
|
1139
|
+
const key = candidate.edit.rowKey;
|
|
1140
|
+
pending.get(key)?.delete(candidate.edit.column);
|
|
1141
|
+
if (pending.get(key)?.size === 0) pending.delete(key);
|
|
1142
|
+
}
|
|
1143
|
+
const visibleFailures = failures;
|
|
1144
|
+
setIssues(visibleFailures);
|
|
1145
|
+
let result;
|
|
1146
|
+
if (asynchronous && host.optimistic) {
|
|
1147
|
+
const rollback = active.filter((candidate) => !liveKeys.has(candidate)).map((candidate) => ({
|
|
1148
|
+
...candidate.edit,
|
|
1149
|
+
value: candidate.previous
|
|
1150
|
+
}));
|
|
1151
|
+
result = host.apply(rollback, false, visibleFailures);
|
|
1152
|
+
const changes = live.map((candidate) => Object.freeze({
|
|
1153
|
+
rowKey: candidate.edit.rowKey,
|
|
1154
|
+
column: candidate.edit.column,
|
|
1155
|
+
previous: candidate.previous,
|
|
1156
|
+
value: candidate.edit.value,
|
|
1157
|
+
row: host.snapshot().getRow(candidate.edit.rowKey).original
|
|
1158
|
+
}));
|
|
1159
|
+
if (changes.length) host.record(changes);
|
|
1160
|
+
result = Object.freeze({
|
|
1161
|
+
...result,
|
|
1162
|
+
changes: Object.freeze(changes),
|
|
1163
|
+
status: changes.length ? visibleFailures.length ? "partial" : "applied" : visibleFailures.length ? "rejected" : "unchanged"
|
|
1164
|
+
});
|
|
1165
|
+
} else result = host.apply(live.map((candidate) => candidate.edit), true, visibleFailures);
|
|
1166
|
+
setIssues(result.issues);
|
|
1167
|
+
if (!result.changes.length && (all.length || result.issues.length) || asynchronous && host.optimistic) host.notify();
|
|
1168
|
+
if (asynchronous && !result.issues.length && active.length) host.onIssues([]);
|
|
1169
|
+
return result;
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
return {
|
|
1173
|
+
hasPending: (key) => (pending.get(key)?.size ?? 0) > 0,
|
|
1174
|
+
cells: () => Object.freeze([...pending.values()].flatMap((row) => [...row.values()])),
|
|
1175
|
+
problems: () => new Map([...problems].filter(([, row]) => row.size).map(([key, row]) => [key, readonlyMap(row)])),
|
|
1176
|
+
cancel(keys) {
|
|
1177
|
+
if (!keys) {
|
|
1178
|
+
tokens.clear();
|
|
1179
|
+
pending.clear();
|
|
1180
|
+
problems.clear();
|
|
1181
|
+
} else for (const key of keys) {
|
|
1182
|
+
tokens.delete(key);
|
|
1183
|
+
pending.delete(key);
|
|
1184
|
+
problems.delete(key);
|
|
1185
|
+
}
|
|
1186
|
+
},
|
|
1187
|
+
edit(edits, recovered = [], options) {
|
|
1188
|
+
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);
|
|
1189
|
+
const snapshot = host.snapshot();
|
|
1190
|
+
const failures = [...recovered];
|
|
1191
|
+
const candidates = [];
|
|
1192
|
+
const guards = new Map([...options?.expectedRows ?? []].map(([key, row]) => [key, { row }]));
|
|
1193
|
+
const drafts = /* @__PURE__ */ new Map();
|
|
1194
|
+
const samples = /* @__PURE__ */ new Map();
|
|
1195
|
+
if (host.validateRow) for (const key of new Set(edits.map((edit) => edit.rowKey))) {
|
|
1196
|
+
tokens.delete(key);
|
|
1197
|
+
pending.delete(key);
|
|
1198
|
+
}
|
|
1199
|
+
for (const edit of edits) {
|
|
1200
|
+
const token = tokenFor(edit.rowKey, edit.column);
|
|
1201
|
+
const row = snapshot.getRow(edit.rowKey);
|
|
1202
|
+
const column = snapshot.getColumn(edit.column);
|
|
1203
|
+
const fail = (code, message) => {
|
|
1204
|
+
failures.push(issue(code, message, {
|
|
1205
|
+
rowKey: edit.rowKey,
|
|
1206
|
+
column: edit.column
|
|
1207
|
+
}));
|
|
1208
|
+
};
|
|
1209
|
+
if (!isSafePath(edit.column)) {
|
|
1210
|
+
fail("unsafe_path", "This path addresses a prototype and cannot be written.");
|
|
1211
|
+
continue;
|
|
1212
|
+
}
|
|
1213
|
+
if (!row) {
|
|
1214
|
+
fail("unknown_row", `No row has the key "${String(edit.rowKey)}".`);
|
|
1215
|
+
continue;
|
|
1216
|
+
}
|
|
1217
|
+
const guard = guards.get(row.key);
|
|
1218
|
+
if (guard && row.original !== guard.row) {
|
|
1219
|
+
fail("stale_draft", "The row changed after this draft was opened.");
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1222
|
+
if (!column) {
|
|
1223
|
+
fail("unknown_column", `No column has the id "${edit.column}".`);
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1226
|
+
const original = drafts.get(row.key) ?? row.original;
|
|
1227
|
+
try {
|
|
1228
|
+
if (!column.isEditable(original)) {
|
|
1229
|
+
fail("read_only_cell", `The "${column.id}" cell is read-only.`);
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
let value;
|
|
1233
|
+
if ("input" in edit) {
|
|
1234
|
+
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));
|
|
1235
|
+
const parsed = column.parse(edit.input, original, samples.get(column.id));
|
|
1236
|
+
if (!parsed.ok) {
|
|
1237
|
+
fail("invalid_value", parsed.message);
|
|
1238
|
+
continue;
|
|
1239
|
+
}
|
|
1240
|
+
value = parsed.value;
|
|
1241
|
+
} else value = edit.value;
|
|
1242
|
+
const previous = column.getValue(original);
|
|
1243
|
+
if (Object.is(previous, value)) continue;
|
|
1244
|
+
const constraint = editorFailure(column.definition.editor, value);
|
|
1245
|
+
if (constraint) {
|
|
1246
|
+
fail("validation_failed", constraint);
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
const next = column.setValue(original, value);
|
|
1250
|
+
if (next === void 0) {
|
|
1251
|
+
fail("read_only_cell", "The column has no way to write a value.");
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1254
|
+
const writeProblem = host.writeIssue(row, next);
|
|
1255
|
+
if (writeProblem) {
|
|
1256
|
+
failures.push(Object.freeze({
|
|
1257
|
+
...writeProblem,
|
|
1258
|
+
column: column.id
|
|
1259
|
+
}));
|
|
1260
|
+
continue;
|
|
1261
|
+
}
|
|
1262
|
+
const validate = column.definition.validate;
|
|
1263
|
+
const validation = validate ? runValidation(() => validate(value, row.original), row.key, column.id) : void 0;
|
|
1264
|
+
if (validation && !isPromise(validation)) {
|
|
1265
|
+
failures.push(validation);
|
|
1266
|
+
continue;
|
|
1267
|
+
}
|
|
1268
|
+
drafts.set(row.key, next);
|
|
1269
|
+
candidates.push({
|
|
1270
|
+
edit: {
|
|
1271
|
+
rowKey: row.key,
|
|
1272
|
+
column: column.id,
|
|
1273
|
+
value
|
|
1274
|
+
},
|
|
1275
|
+
previous: column.getValue(row.original),
|
|
1276
|
+
original: row.original,
|
|
1277
|
+
column,
|
|
1278
|
+
validation,
|
|
1279
|
+
token,
|
|
1280
|
+
guard
|
|
1281
|
+
});
|
|
1282
|
+
} catch (error) {
|
|
1283
|
+
fail("invalid_value", error instanceof Error ? error.message : String(error));
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
const asynchronous = candidates.some((candidate) => isPromise(candidate.validation));
|
|
1287
|
+
const validateRows = (accepted) => rowValidation(accepted.filter((candidate) => current(candidate) && fresh(candidate)), failures);
|
|
1288
|
+
const outcome = asynchronous ? Promise.all(candidates.map(async (candidate) => {
|
|
1289
|
+
const problem = await candidate.validation;
|
|
1290
|
+
if (problem) {
|
|
1291
|
+
if (current(candidate)) failures.push(problem);
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
return candidate;
|
|
1295
|
+
})).then((accepted) => validateRows(accepted.filter((candidate) => candidate !== void 0))) : validateRows(candidates);
|
|
1296
|
+
if (!isPromise(outcome)) {
|
|
1297
|
+
const result = settle(outcome, candidates, failures, false);
|
|
1298
|
+
if (metadataChanged && !candidates.length && !failures.length) host.notify();
|
|
1299
|
+
return result;
|
|
1300
|
+
}
|
|
1301
|
+
for (const candidate of candidates) {
|
|
1302
|
+
const row = pending.get(candidate.edit.rowKey) ?? /* @__PURE__ */ new Map();
|
|
1303
|
+
row.set(candidate.edit.column, Object.freeze({ ...candidate.edit }));
|
|
1304
|
+
pending.set(candidate.edit.rowKey, row);
|
|
1305
|
+
}
|
|
1306
|
+
setIssues(failures);
|
|
1307
|
+
const optimistic = host.optimistic ? host.apply(candidates.map((candidate) => candidate.edit), false, failures) : void 0;
|
|
1308
|
+
if (optimistic) for (const [key, guard] of guards) {
|
|
1309
|
+
const row = host.snapshot().getRow(key);
|
|
1310
|
+
if (row) guard.row = row.original;
|
|
1311
|
+
}
|
|
1312
|
+
if (!optimistic?.changes.length) host.notify();
|
|
1313
|
+
if (!host.optimistic && failures.length) host.onIssues(failures);
|
|
1314
|
+
const completion = outcome.then((accepted) => {
|
|
1315
|
+
const staleCandidates = candidates.filter((candidate) => !current(candidate) || !fresh(candidate)).map((candidate) => ({
|
|
1316
|
+
candidate,
|
|
1317
|
+
problem: issue("stale_draft", "This edit was superseded or its row is no longer current.", {
|
|
1318
|
+
rowKey: candidate.edit.rowKey,
|
|
1319
|
+
column: candidate.edit.column
|
|
1320
|
+
})
|
|
1321
|
+
}));
|
|
1322
|
+
const stale = staleCandidates.map(({ problem }) => problem);
|
|
1323
|
+
const issues = [...failures, ...stale];
|
|
1324
|
+
if (candidates.some(current)) {
|
|
1325
|
+
const superseded = staleCandidates.filter(({ candidate }) => !current(candidate)).map(({ problem }) => problem);
|
|
1326
|
+
const result = settle(accepted, candidates, [...failures, ...staleCandidates.filter(({ candidate }) => current(candidate)).map(({ problem }) => problem)], true);
|
|
1327
|
+
if (!stale.length) return result;
|
|
1328
|
+
return Object.freeze({
|
|
1329
|
+
...result,
|
|
1330
|
+
issues: Object.freeze([...result.issues, ...superseded]),
|
|
1331
|
+
status: result.changes.length ? "partial" : "rejected"
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
return Object.freeze({
|
|
1335
|
+
status: "rejected",
|
|
1336
|
+
changes: Object.freeze([]),
|
|
1337
|
+
rowChanges: Object.freeze([]),
|
|
1338
|
+
issues: Object.freeze(issues),
|
|
1339
|
+
pendingCells: Object.freeze([])
|
|
1340
|
+
});
|
|
1341
|
+
});
|
|
1342
|
+
return Object.freeze({
|
|
1343
|
+
status: "pending",
|
|
1344
|
+
changes: optimistic?.changes ?? Object.freeze([]),
|
|
1345
|
+
rowChanges: Object.freeze([]),
|
|
1346
|
+
issues: Object.freeze([...failures]),
|
|
1347
|
+
pendingCells: Object.freeze(candidates.map((candidate) => Object.freeze({ ...candidate.edit }))),
|
|
1348
|
+
completion
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
};
|
|
674
1352
|
}
|
|
675
1353
|
|
|
676
1354
|
//#endregion
|
|
677
|
-
//#region src/
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
1355
|
+
//#region src/pending-changes.ts
|
|
1356
|
+
/** A saved baseline only for touched rows; viewing changes never scans source data. */
|
|
1357
|
+
function createPendingChanges() {
|
|
1358
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1359
|
+
return {
|
|
1360
|
+
entries,
|
|
1361
|
+
snapshot() {
|
|
1362
|
+
const inserted = [];
|
|
1363
|
+
const removed = [];
|
|
1364
|
+
const updated = [];
|
|
1365
|
+
for (const [rowKey, entry] of entries) if (entry.previous === void 0 && entry.row !== void 0) inserted.push(Object.freeze({
|
|
1366
|
+
kind: "inserted",
|
|
1367
|
+
rowKey,
|
|
1368
|
+
row: entry.row,
|
|
1369
|
+
position: entry.position
|
|
1370
|
+
}));
|
|
1371
|
+
else if (entry.row === void 0 && entry.previous !== void 0) removed.push(Object.freeze({
|
|
1372
|
+
kind: "removed",
|
|
1373
|
+
rowKey,
|
|
1374
|
+
row: entry.previous,
|
|
1375
|
+
position: entry.position
|
|
1376
|
+
}));
|
|
1377
|
+
else if (entry.row !== void 0 && entry.previous !== void 0) updated.push(Object.freeze({
|
|
1378
|
+
rowKey,
|
|
1379
|
+
row: entry.row,
|
|
1380
|
+
previous: entry.previous,
|
|
1381
|
+
changes: Object.freeze([...entry.cells.values()])
|
|
1382
|
+
}));
|
|
1383
|
+
return Object.freeze({
|
|
1384
|
+
inserted: Object.freeze(inserted),
|
|
1385
|
+
updated: Object.freeze(updated),
|
|
1386
|
+
removed: Object.freeze(removed)
|
|
1387
|
+
});
|
|
1388
|
+
},
|
|
1389
|
+
record(key, previous, row, position, changes) {
|
|
1390
|
+
const entry = entries.get(key) ?? {
|
|
1391
|
+
previous,
|
|
1392
|
+
row,
|
|
1393
|
+
position: Object.freeze({ ...position }),
|
|
1394
|
+
cells: /* @__PURE__ */ new Map()
|
|
1395
|
+
};
|
|
1396
|
+
entry.row = row;
|
|
1397
|
+
for (const change of changes) {
|
|
1398
|
+
const original = entry.cells.has(change.column) ? entry.cells.get(change.column).previous : change.previous;
|
|
1399
|
+
if (Object.is(original, change.value)) entry.cells.delete(change.column);
|
|
1400
|
+
else entry.cells.set(change.column, Object.freeze({
|
|
1401
|
+
...change,
|
|
1402
|
+
previous: original
|
|
1403
|
+
}));
|
|
1404
|
+
}
|
|
1405
|
+
if (entry.previous === void 0 && row === void 0 || entry.previous !== void 0 && row !== void 0 && entry.cells.size === 0) entries.delete(key);
|
|
1406
|
+
else entries.set(key, entry);
|
|
1407
|
+
}
|
|
691
1408
|
};
|
|
692
1409
|
}
|
|
693
|
-
|
|
694
|
-
|
|
1410
|
+
|
|
1411
|
+
//#endregion
|
|
1412
|
+
//#region src/row-list.ts
|
|
1413
|
+
/** Read-only array views keep append publication independent of existing list length. */
|
|
1414
|
+
function indexOf(property) {
|
|
1415
|
+
if (typeof property !== "string" || !/^(0|[1-9]\d*)$/u.test(property)) return void 0;
|
|
1416
|
+
return Number(property);
|
|
695
1417
|
}
|
|
696
|
-
function
|
|
697
|
-
|
|
698
|
-
|
|
1418
|
+
function rowList(length, read) {
|
|
1419
|
+
return new Proxy([], {
|
|
1420
|
+
get(target, property, receiver) {
|
|
1421
|
+
if (property === "length") return length;
|
|
1422
|
+
const index = indexOf(property);
|
|
1423
|
+
return index === void 0 ? Reflect.get(target, property, receiver) : index < length ? read(index) : void 0;
|
|
1424
|
+
},
|
|
1425
|
+
has(target, property) {
|
|
1426
|
+
const index = indexOf(property);
|
|
1427
|
+
return index === void 0 ? Reflect.has(target, property) : index < length;
|
|
1428
|
+
},
|
|
1429
|
+
ownKeys: () => [...Array.from({ length }, (_, index) => String(index)), "length"],
|
|
1430
|
+
getOwnPropertyDescriptor(target, property) {
|
|
1431
|
+
if (property === "length") return {
|
|
1432
|
+
...Reflect.getOwnPropertyDescriptor(target, property),
|
|
1433
|
+
value: length
|
|
1434
|
+
};
|
|
1435
|
+
const index = indexOf(property);
|
|
1436
|
+
return index === void 0 ? Reflect.getOwnPropertyDescriptor(target, property) : index < length ? {
|
|
1437
|
+
configurable: true,
|
|
1438
|
+
enumerable: true,
|
|
1439
|
+
writable: false,
|
|
1440
|
+
value: read(index)
|
|
1441
|
+
} : void 0;
|
|
1442
|
+
},
|
|
1443
|
+
set: () => false,
|
|
1444
|
+
deleteProperty: () => false,
|
|
1445
|
+
defineProperty: () => false,
|
|
1446
|
+
preventExtensions: () => false,
|
|
1447
|
+
setPrototypeOf: () => false
|
|
1448
|
+
});
|
|
699
1449
|
}
|
|
700
|
-
|
|
701
|
-
function
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
const
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
hiddenColumns: options.initialState?.hiddenColumns ?? options.columns.filter((column) => column.hidden === true).map((column) => column.id)
|
|
1450
|
+
const stores = /* @__PURE__ */ new WeakMap();
|
|
1451
|
+
function appendList(previous, incoming) {
|
|
1452
|
+
if (!incoming.length) return previous;
|
|
1453
|
+
let store = stores.get(previous);
|
|
1454
|
+
const last = store?.chunks.at(-1);
|
|
1455
|
+
if (!store || last && last.start + last.rows.length !== previous.length) store = { chunks: [{
|
|
1456
|
+
start: 0,
|
|
1457
|
+
rows: previous
|
|
1458
|
+
}] };
|
|
1459
|
+
store.chunks.push({
|
|
1460
|
+
start: previous.length,
|
|
1461
|
+
rows: incoming
|
|
713
1462
|
});
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
return {
|
|
726
|
-
columns,
|
|
727
|
-
byId: new Map(columns.map((column) => [column.id, column]))
|
|
728
|
-
};
|
|
1463
|
+
const chunks = store.chunks;
|
|
1464
|
+
const result = rowList(previous.length + incoming.length, (index) => {
|
|
1465
|
+
let low = 0;
|
|
1466
|
+
let high = chunks.length - 1;
|
|
1467
|
+
while (low < high) {
|
|
1468
|
+
const mid = Math.ceil((low + high) / 2);
|
|
1469
|
+
if (chunks[mid].start <= index) low = mid;
|
|
1470
|
+
else high = mid - 1;
|
|
1471
|
+
}
|
|
1472
|
+
const chunk = chunks[low];
|
|
1473
|
+
return chunk.rows[index - chunk.start];
|
|
729
1474
|
});
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
1475
|
+
stores.set(result, store);
|
|
1476
|
+
return result;
|
|
1477
|
+
}
|
|
1478
|
+
function mapList(rows, map) {
|
|
1479
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1480
|
+
return rowList(rows.length, (index) => {
|
|
1481
|
+
if (!cache.has(index)) cache.set(index, map(rows[index], index));
|
|
1482
|
+
return cache.get(index);
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
//#endregion
|
|
1487
|
+
//#region src/stream.ts
|
|
1488
|
+
function createStream(host) {
|
|
1489
|
+
let active;
|
|
1490
|
+
return {
|
|
1491
|
+
cancel() {
|
|
1492
|
+
active?.cancel();
|
|
1493
|
+
active = void 0;
|
|
1494
|
+
},
|
|
1495
|
+
async run(source, options = {}) {
|
|
1496
|
+
active?.cancel();
|
|
1497
|
+
const problems = [];
|
|
1498
|
+
const requested = options.batchSize ?? 1e3;
|
|
1499
|
+
const batchSize = Number.isSafeInteger(requested) && requested > 0 ? requested : 1e3;
|
|
1500
|
+
if (batchSize !== requested) problems.push(issue("invalid_stream_option", "batchSize must be a positive safe integer; 1,000 is used."));
|
|
1501
|
+
const requestedCount = options.expectedRowCount;
|
|
1502
|
+
const expected = requestedCount === void 0 || Number.isSafeInteger(requestedCount) && requestedCount >= 0 ? requestedCount : void 0;
|
|
1503
|
+
if (expected !== requestedCount) problems.push(issue("invalid_stream_option", "expectedRowCount must be a non-negative safe integer; it is unknown instead."));
|
|
1504
|
+
let cancelled = false;
|
|
1505
|
+
let wake = () => void 0;
|
|
1506
|
+
const cancellation = new Promise((resolve) => {
|
|
1507
|
+
wake = () => resolve(void 0);
|
|
1508
|
+
});
|
|
1509
|
+
const cancel = () => {
|
|
1510
|
+
cancelled = true;
|
|
1511
|
+
wake();
|
|
1512
|
+
};
|
|
1513
|
+
const run = { cancel };
|
|
1514
|
+
active = run;
|
|
1515
|
+
const signal = options.signal;
|
|
1516
|
+
signal?.addEventListener?.("abort", cancel, { once: true });
|
|
1517
|
+
if (signal?.aborted) run.cancel();
|
|
1518
|
+
let receivedRowCount = 0;
|
|
1519
|
+
let status = "done";
|
|
1520
|
+
let iterator;
|
|
1521
|
+
host.update(cancelled ? "aborted" : "loading", expected, problems, true);
|
|
1522
|
+
try {
|
|
1523
|
+
if (!cancelled) iterator = source[Symbol.asyncIterator]();
|
|
1524
|
+
while (true) {
|
|
1525
|
+
if (cancelled || !iterator) break;
|
|
1526
|
+
const next = await Promise.race([iterator.next(), cancellation]);
|
|
1527
|
+
if (!next || cancelled || signal?.aborted) {
|
|
1528
|
+
run.cancel();
|
|
1529
|
+
break;
|
|
1530
|
+
}
|
|
1531
|
+
if (next.done) break;
|
|
1532
|
+
const rows = Array.isArray(next.value) ? next.value : [next.value];
|
|
1533
|
+
for (let start = 0; start < rows.length; start += batchSize) {
|
|
1534
|
+
await Promise.resolve();
|
|
1535
|
+
if (cancelled || signal?.aborted) {
|
|
1536
|
+
run.cancel();
|
|
1537
|
+
break;
|
|
1538
|
+
}
|
|
1539
|
+
host.update("streaming", expected, problems, false);
|
|
1540
|
+
const result = host.ingest(rows.slice(start, start + batchSize), options.mode ?? "append");
|
|
1541
|
+
receivedRowCount += result.appended + result.replaced;
|
|
1542
|
+
problems.push(...result.issues);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
} catch (error) {
|
|
1546
|
+
status = "error";
|
|
1547
|
+
problems.push(issue("stream_error", error instanceof Error ? error.message : String(error)));
|
|
1548
|
+
} finally {
|
|
1549
|
+
signal?.removeEventListener?.("abort", cancel);
|
|
1550
|
+
if (cancelled) status = "aborted";
|
|
1551
|
+
if (active === run) {
|
|
1552
|
+
active = void 0;
|
|
1553
|
+
host.update(status, expected, problems, true);
|
|
1554
|
+
}
|
|
1555
|
+
if (status !== "done" && iterator?.return) Promise.resolve().then(() => iterator.return()).catch(() => void 0);
|
|
1556
|
+
}
|
|
1557
|
+
return Object.freeze({
|
|
1558
|
+
status,
|
|
1559
|
+
receivedRowCount,
|
|
1560
|
+
issues: Object.freeze([...problems])
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
//#endregion
|
|
1567
|
+
//#region src/tree.ts
|
|
1568
|
+
function buildTree(data, columns, readKey, options, lazy) {
|
|
1569
|
+
const nodes = [];
|
|
1570
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1571
|
+
const issues = [];
|
|
1572
|
+
const nested = options.getChildren !== void 0;
|
|
1573
|
+
let maxDepth = options.maxDepth ?? 1e3;
|
|
1574
|
+
if (!Number.isSafeInteger(maxDepth) || maxDepth < 1) {
|
|
1575
|
+
issues.push(issue("invalid_tree_option", "maxDepth must be a positive safe integer; 1,000 is used."));
|
|
1576
|
+
maxDepth = 1e3;
|
|
1577
|
+
}
|
|
1578
|
+
if (nested && options.getParentKey) issues.push(issue("invalid_tree_option", "Both tree inputs were supplied; getChildren takes precedence."));
|
|
1579
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1580
|
+
const stack = data.map((original, sourceIndex) => ({
|
|
1581
|
+
original,
|
|
1582
|
+
sourceIndex,
|
|
1583
|
+
parent: void 0,
|
|
1584
|
+
lazy: false
|
|
1585
|
+
})).reverse();
|
|
1586
|
+
while (stack.length) {
|
|
1587
|
+
const entry = stack.pop();
|
|
1588
|
+
const index = nodes.length;
|
|
1589
|
+
let key = readKey(entry.original, index);
|
|
1590
|
+
let parent = entry.parent;
|
|
1591
|
+
const reference = entry.original !== null && (typeof entry.original === "object" || typeof entry.original === "function");
|
|
1592
|
+
const repeated = nested && reference && seen.has(entry.original);
|
|
1593
|
+
if (repeated) {
|
|
1594
|
+
issues.push(issue("tree_cycle", "A repeated nested value recovers as a root without traversing it again.", { rowKey: key }));
|
|
1595
|
+
parent = void 0;
|
|
1596
|
+
}
|
|
1597
|
+
if (reference) seen.add(entry.original);
|
|
1598
|
+
if (byKey.has(key)) {
|
|
1599
|
+
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 }));
|
|
1600
|
+
if (entry.lazy) continue;
|
|
1601
|
+
const prefix = `${String(key)}#${index}`;
|
|
1602
|
+
key = prefix;
|
|
1603
|
+
let suffix = 0;
|
|
1604
|
+
while (byKey.has(key)) key = `${prefix}#${++suffix}`;
|
|
1605
|
+
}
|
|
1606
|
+
if (parent && parent.depth + 1 > maxDepth) {
|
|
1607
|
+
issues.push(issue("tree_depth_exceeded", "The maximum tree depth was exceeded; this row recovers as a root.", { rowKey: key }));
|
|
1608
|
+
parent = void 0;
|
|
1609
|
+
}
|
|
1610
|
+
const children = repeated ? void 0 : lazy.get(key) ?? options.getChildren?.(entry.original);
|
|
1611
|
+
const node = {
|
|
1612
|
+
row: createRow(entry.original, key, index, columns),
|
|
1613
|
+
parent,
|
|
1614
|
+
children: [],
|
|
1615
|
+
depth: parent ? parent.depth + 1 : 0,
|
|
1616
|
+
sourceIndex: entry.sourceIndex,
|
|
1617
|
+
lazy: entry.lazy,
|
|
1618
|
+
writable: parent?.writable ?? entry.parent === void 0,
|
|
1619
|
+
unloaded: children === void 0 && (options.hasChildren?.(entry.original) ?? false)
|
|
1620
|
+
};
|
|
1621
|
+
nodes.push(node);
|
|
1622
|
+
byKey.set(key, node);
|
|
1623
|
+
if (parent) parent.children.push(node);
|
|
1624
|
+
if (children) for (let i = children.length - 1; i >= 0; i--) stack.push({
|
|
1625
|
+
original: children[i],
|
|
1626
|
+
sourceIndex: i,
|
|
1627
|
+
parent: node,
|
|
1628
|
+
lazy: entry.lazy || lazy.has(key)
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
if (!nested && options.getParentKey) {
|
|
1632
|
+
for (const node of nodes) {
|
|
1633
|
+
if (node.lazy) continue;
|
|
1634
|
+
const key = options.getParentKey(node.row.original);
|
|
1635
|
+
if (key === void 0 || key === null) continue;
|
|
1636
|
+
const parent = byKey.get(key);
|
|
1637
|
+
if (!parent) issues.push(issue("tree_orphan", "The parent is missing; this row recovers as a root.", { rowKey: node.row.key }));
|
|
1638
|
+
else node.parent = parent;
|
|
1639
|
+
}
|
|
1640
|
+
const done = /* @__PURE__ */ new Set();
|
|
1641
|
+
for (const node of nodes) {
|
|
1642
|
+
const path = [];
|
|
1643
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
1644
|
+
let cursor = node;
|
|
1645
|
+
while (cursor && !done.has(cursor)) {
|
|
1646
|
+
if (visiting.has(cursor)) {
|
|
1647
|
+
issues.push(issue("tree_cycle", "A cyclic parent reference is detached to a root.", { rowKey: cursor.row.key }));
|
|
1648
|
+
cursor.parent = void 0;
|
|
1649
|
+
break;
|
|
1650
|
+
}
|
|
1651
|
+
visiting.add(cursor);
|
|
1652
|
+
path.push(cursor);
|
|
1653
|
+
cursor = cursor.parent;
|
|
1654
|
+
}
|
|
1655
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
1656
|
+
const current = path[i];
|
|
1657
|
+
done.add(current);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
for (const node of nodes) node.children = [];
|
|
1661
|
+
for (const node of nodes) if (node.parent) node.parent.children.push(node);
|
|
1662
|
+
const pending = nodes.filter((node) => !node.parent);
|
|
1663
|
+
while (pending.length) {
|
|
1664
|
+
const node = pending.pop();
|
|
1665
|
+
node.depth = node.parent ? node.parent.depth + 1 : 0;
|
|
1666
|
+
if (node.depth > maxDepth) {
|
|
1667
|
+
node.parent = void 0;
|
|
1668
|
+
node.depth = 0;
|
|
1669
|
+
issues.push(issue("tree_depth_exceeded", "The maximum tree depth was exceeded; this row recovers as a root.", { rowKey: node.row.key }));
|
|
1670
|
+
}
|
|
1671
|
+
for (const child of node.children) pending.push(child);
|
|
1672
|
+
}
|
|
1673
|
+
for (const node of nodes) node.children = node.children.filter((child) => child.parent === node);
|
|
1674
|
+
}
|
|
1675
|
+
return {
|
|
1676
|
+
nodes,
|
|
1677
|
+
rows: nodes.map((node) => node.row),
|
|
1678
|
+
roots: nodes.filter((node) => !node.parent),
|
|
1679
|
+
byKey,
|
|
1680
|
+
issues
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
function processTree(model, columns, state, mode, manual) {
|
|
1684
|
+
const raw = model.nodes.map((node) => node.row);
|
|
1685
|
+
const matched = manual ? raw : filterRows(raw, columns, state.search, state.filters);
|
|
1686
|
+
const active = matched !== raw;
|
|
1687
|
+
const included = new Set(matched.map((row) => row.key));
|
|
1688
|
+
const autoExpanded = /* @__PURE__ */ new Set();
|
|
1689
|
+
if (active && mode !== "strict") {
|
|
1690
|
+
if (mode === "descendants") {
|
|
1691
|
+
const stack = matched.map((row) => model.byKey.get(row.key));
|
|
1692
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1693
|
+
while (stack.length) {
|
|
1694
|
+
const node = stack.pop();
|
|
1695
|
+
if (visited.has(node.row.key)) continue;
|
|
1696
|
+
visited.add(node.row.key);
|
|
1697
|
+
included.add(node.row.key);
|
|
1698
|
+
if (node.children.length) autoExpanded.add(node.row.key);
|
|
1699
|
+
for (const child of node.children) stack.push(child);
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
for (const row of matched) {
|
|
1703
|
+
let parent = model.byKey.get(row.key).parent;
|
|
1704
|
+
while (parent && !autoExpanded.has(parent.row.key)) {
|
|
1705
|
+
included.add(parent.row.key);
|
|
1706
|
+
autoExpanded.add(parent.row.key);
|
|
1707
|
+
parent = parent.parent;
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
if (active && mode === "strict") return {
|
|
1712
|
+
model,
|
|
1713
|
+
roots: ordered(model.nodes.filter((node) => included.has(node.row.key) && node.children.length === 0 && !node.unloaded)),
|
|
1714
|
+
children: /* @__PURE__ */ new Map(),
|
|
1715
|
+
autoExpanded
|
|
1716
|
+
};
|
|
1717
|
+
const children = /* @__PURE__ */ new Map();
|
|
1718
|
+
for (const node of model.nodes) if (node.children.length) children.set(node, ordered(node.children.filter((child) => included.has(child.row.key))));
|
|
1719
|
+
return {
|
|
1720
|
+
model,
|
|
1721
|
+
roots: ordered(model.roots.filter((node) => included.has(node.row.key))),
|
|
1722
|
+
children,
|
|
1723
|
+
autoExpanded
|
|
1724
|
+
};
|
|
1725
|
+
function ordered(nodes) {
|
|
1726
|
+
if (manual || !state.sorting.length || nodes.length < 2) return nodes;
|
|
1727
|
+
return sortRows(nodes.map((node) => node.row), columns, state.sorting).map((row) => model.byKey.get(row.key));
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
/** Expansion changes reuse filtering and sibling order, splicing only a changed subtree. */
|
|
1731
|
+
function createTreeFlattener() {
|
|
1732
|
+
let previous;
|
|
1733
|
+
let previousExpanded = [];
|
|
1734
|
+
let visible = [];
|
|
1735
|
+
let previousAncestors;
|
|
1736
|
+
return (tree, expanded, ancestors) => {
|
|
1737
|
+
if (tree === previous && expanded === previousExpanded && ancestors === previousAncestors) return visible;
|
|
1738
|
+
const keys = expanded === true ? void 0 : new Set(expanded);
|
|
1739
|
+
const open = (node) => expanded === true || (keys?.has(node.row.key) ?? false) || ancestors.has(node.row.key) || tree.autoExpanded.has(node.row.key);
|
|
1740
|
+
const walk = (roots) => {
|
|
1741
|
+
const output = [];
|
|
1742
|
+
const stack = [...roots].reverse();
|
|
1743
|
+
while (stack.length) {
|
|
1744
|
+
const node = stack.pop();
|
|
1745
|
+
output.push(node);
|
|
1746
|
+
if (open(node)) {
|
|
1747
|
+
const children = tree.children.get(node) ?? [];
|
|
1748
|
+
for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
return output;
|
|
1752
|
+
};
|
|
1753
|
+
let changed;
|
|
1754
|
+
if (previous === tree && ancestors === previousAncestors && expanded !== true && previousExpanded !== true) {
|
|
1755
|
+
const before = new Set(previousExpanded);
|
|
1756
|
+
const after = new Set(expanded);
|
|
1757
|
+
changed = [...previousExpanded.filter((key) => !after.has(key)), ...expanded.filter((key) => !before.has(key))];
|
|
1758
|
+
}
|
|
1759
|
+
if (changed?.length === 1 && !tree.autoExpanded.has(changed[0])) {
|
|
1760
|
+
const index = visible.findIndex((node) => node.row.key === changed[0]);
|
|
1761
|
+
if (index >= 0) {
|
|
1762
|
+
const node = visible[index];
|
|
1763
|
+
let end = index + 1;
|
|
1764
|
+
while (end < visible.length && visible[end].depth > node.depth) end++;
|
|
1765
|
+
const inserted = walk(open(node) ? tree.children.get(node) ?? [] : []);
|
|
1766
|
+
visible = visible.slice(0, index + 1).concat(inserted, visible.slice(end));
|
|
1767
|
+
}
|
|
1768
|
+
} else visible = walk(tree.roots);
|
|
1769
|
+
previous = tree;
|
|
1770
|
+
previousExpanded = expanded;
|
|
1771
|
+
previousAncestors = ancestors;
|
|
1772
|
+
return visible;
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
//#endregion
|
|
1777
|
+
//#region src/table.ts
|
|
1778
|
+
function isRecord(value) {
|
|
1779
|
+
return typeof value === "object" && value !== null;
|
|
1780
|
+
}
|
|
1781
|
+
function memo(compute) {
|
|
1782
|
+
let last;
|
|
1783
|
+
return (...deps) => {
|
|
1784
|
+
if (last && last.deps.length === deps.length && last.deps.every((d, i) => Object.is(d, deps[i]))) return last.result;
|
|
1785
|
+
const result = compute(...deps);
|
|
1786
|
+
last = {
|
|
1787
|
+
deps,
|
|
1788
|
+
result
|
|
1789
|
+
};
|
|
1790
|
+
return result;
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
function isInputEdit(edit) {
|
|
1794
|
+
return "input" in edit;
|
|
1795
|
+
}
|
|
1796
|
+
function coverage(selected, total) {
|
|
1797
|
+
if (selected === 0 || total === 0) return "none";
|
|
1798
|
+
return selected === total ? "all" : "some";
|
|
1799
|
+
}
|
|
1800
|
+
function pasteLimit(value, fallback, name, issues) {
|
|
1801
|
+
if (value === void 0) return fallback;
|
|
1802
|
+
if (Number.isSafeInteger(value) && value > 0) return value;
|
|
1803
|
+
issues.push(issue("invalid_paste_limit", `Paste ${name} ${String(value)} is not a positive safe integer; ${fallback} is used.`));
|
|
1804
|
+
return fallback;
|
|
1805
|
+
}
|
|
1806
|
+
/** Create a data table. */
|
|
1807
|
+
function createTable(options) {
|
|
1808
|
+
const paginate = options.paginate ?? true;
|
|
1809
|
+
const expandMode = options.expandMode ?? "multiple";
|
|
1810
|
+
const selectionMode = options.selectionMode ?? "multiple";
|
|
1811
|
+
const selectScope = options.selectScope ?? "all";
|
|
1812
|
+
const manual = options.manual ?? false;
|
|
1813
|
+
let treeFilter = options.treeFilter ?? "ancestors";
|
|
1814
|
+
const treeEnabled = Boolean(options.getChildren || options.getParentKey || options.hasChildren || options.loadChildren);
|
|
1815
|
+
const rootPagination = treeEnabled && (manual || (options.paginateBy ?? "root") === "root");
|
|
1816
|
+
const projectedRows = /* @__PURE__ */ new WeakMap();
|
|
1817
|
+
function shareRow(base, next) {
|
|
1818
|
+
const previous = projectedRows.get(base);
|
|
1819
|
+
if (previous && Object.keys(next).every((key) => previous[key] === next[key])) return previous;
|
|
1820
|
+
projectedRows.set(base, next);
|
|
1821
|
+
return next;
|
|
1822
|
+
}
|
|
1823
|
+
let lazyVersion = 0;
|
|
1824
|
+
const lazyChildren = /* @__PURE__ */ new Map();
|
|
1825
|
+
const childStatuses = /* @__PURE__ */ new Map();
|
|
1826
|
+
const loadIssues = /* @__PURE__ */ new Map();
|
|
1827
|
+
const loadValidationIssues = /* @__PURE__ */ new Map();
|
|
1828
|
+
const pendingLoads = /* @__PURE__ */ new Map();
|
|
1829
|
+
let destroyed = false;
|
|
1830
|
+
const undoStack = createUndoStack(options.historyLimit);
|
|
1831
|
+
const pendingChanges = createPendingChanges();
|
|
1832
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1833
|
+
const optionIssues = [];
|
|
1834
|
+
const maxPasteCells = pasteLimit(options.pasteLimit?.maxCells, 1e5, "maxCells", optionIssues);
|
|
1835
|
+
const maxPasteLength = pasteLimit(options.pasteLimit?.maxLength, 5e6, "maxLength", optionIssues);
|
|
1836
|
+
const sourceExpected = options.expectedRowCount === void 0 || Number.isSafeInteger(options.expectedRowCount) && options.expectedRowCount >= 0 ? options.expectedRowCount : void 0;
|
|
1837
|
+
if (sourceExpected !== options.expectedRowCount) optionIssues.push(issue("invalid_stream_option", "expectedRowCount must be a non-negative safe integer; it is unknown instead."));
|
|
1838
|
+
let data = options.data;
|
|
1839
|
+
let definitions = options.columns;
|
|
1840
|
+
let rowCount = options.rowCount;
|
|
1841
|
+
let expandedIssue;
|
|
1842
|
+
let initialState = createInitialState({
|
|
1843
|
+
...options.initialState,
|
|
1844
|
+
hiddenColumns: options.initialState?.hiddenColumns ?? options.columns.filter((column) => column.hidden === true).map((column) => column.id)
|
|
1845
|
+
}, (problem) => {
|
|
1846
|
+
expandedIssue = problem;
|
|
1847
|
+
});
|
|
1848
|
+
let state = initialState;
|
|
1849
|
+
let snapshot;
|
|
1850
|
+
let loadState = "idle";
|
|
1851
|
+
let expectedRowCount = sourceExpected;
|
|
1852
|
+
let ingestionIssues = [];
|
|
1853
|
+
let streamIssues = [];
|
|
1854
|
+
const streaming = createStream({
|
|
1855
|
+
ingest: (rows, mode) => ingestData(rows, mode),
|
|
1856
|
+
update(next, expected, problems, publish) {
|
|
1857
|
+
loadState = next;
|
|
1858
|
+
expectedRowCount = expected;
|
|
1859
|
+
streamIssues = [...problems];
|
|
1860
|
+
if (publish) notify();
|
|
1861
|
+
}
|
|
1862
|
+
});
|
|
1863
|
+
let transaction = false;
|
|
1864
|
+
let deferValidationNotify = false;
|
|
1865
|
+
let validationNotifyRequested = false;
|
|
1866
|
+
const validation = createEditValidation({
|
|
1867
|
+
snapshot: getSnapshot,
|
|
1868
|
+
apply: applyChanges,
|
|
1869
|
+
notify,
|
|
1870
|
+
writeIssue(row, next) {
|
|
1871
|
+
const node = treeEnabled ? treeStage(data, resolveColumns(definitions).byId, lazyVersion).byKey.get(row.key) : void 0;
|
|
1872
|
+
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 });
|
|
1873
|
+
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 });
|
|
1874
|
+
},
|
|
1875
|
+
batch(run) {
|
|
1876
|
+
deferValidationNotify = true;
|
|
1877
|
+
validationNotifyRequested = false;
|
|
1878
|
+
try {
|
|
1879
|
+
return run();
|
|
1880
|
+
} finally {
|
|
1881
|
+
deferValidationNotify = false;
|
|
1882
|
+
if (validationNotifyRequested) notify();
|
|
1883
|
+
}
|
|
1884
|
+
},
|
|
1885
|
+
record: (changes) => undoStack.record({
|
|
1886
|
+
kind: "cells",
|
|
1887
|
+
changes
|
|
1888
|
+
}),
|
|
1889
|
+
onIssues: (issues) => options.onEditIssues?.(issues),
|
|
1890
|
+
optimistic: options.asyncValidation === "optimistic",
|
|
1891
|
+
validateRow: options.validateRow
|
|
1892
|
+
});
|
|
1893
|
+
const readKey = (row, index) => {
|
|
1894
|
+
const { rowKey } = options;
|
|
1895
|
+
if (typeof rowKey === "function") return rowKey(row, index);
|
|
1896
|
+
const value = getPath(row, rowKey ?? "id");
|
|
1897
|
+
if (typeof value === "string" || typeof value === "number") return value;
|
|
1898
|
+
return rowKey === void 0 || !isRecord(row) ? index : String(value);
|
|
1899
|
+
};
|
|
1900
|
+
const resolveColumns = memo((defs) => {
|
|
1901
|
+
const issues = [];
|
|
1902
|
+
const columns = defs.flatMap((definition) => {
|
|
1903
|
+
if (!isSafePath(definition.id)) {
|
|
1904
|
+
issues.push(issue("unsafe_path", `The column "${definition.id}" addresses a prototype and is ignored.`, { column: definition.id }));
|
|
1905
|
+
return [];
|
|
1906
|
+
}
|
|
1907
|
+
return [resolveColumn(definition)];
|
|
1908
|
+
});
|
|
1909
|
+
return {
|
|
1910
|
+
columns,
|
|
1911
|
+
byId: new Map(columns.map((column) => [column.id, column])),
|
|
1912
|
+
issues
|
|
1913
|
+
};
|
|
1914
|
+
});
|
|
1915
|
+
let primedIndex;
|
|
1916
|
+
let reusableRows;
|
|
1917
|
+
const indexRows = memo((rows, byId) => {
|
|
1918
|
+
if (primedIndex?.data === rows && primedIndex.columns === byId) return primedIndex.result;
|
|
1919
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1920
|
+
const issues = [];
|
|
1921
|
+
const indexed = rows.map((original, index) => {
|
|
1922
|
+
let key = readKey(original, index);
|
|
1923
|
+
if (byKey.has(key)) {
|
|
1924
|
+
issues.push(issue("duplicate_row_key", `Row ${index} repeats the key "${String(key)}"; it is keyed "${String(key)}#${index}" instead.`, { rowKey: key }));
|
|
1925
|
+
key = `${String(key)}#${index}`;
|
|
1926
|
+
}
|
|
1927
|
+
const previous = reusableRows?.columns === byId ? reusableRows.rows.get(key) : void 0;
|
|
1928
|
+
const row = previous?.original === original && previous.index === index ? previous : createRow(original, key, index, byId, options.getRowCanExpand?.(original) ?? false);
|
|
1929
|
+
byKey.set(key, row);
|
|
1930
|
+
return row;
|
|
1931
|
+
});
|
|
1932
|
+
stableSource.set(rows, stableRows(rows));
|
|
1933
|
+
reusableRows = {
|
|
1934
|
+
columns: byId,
|
|
1935
|
+
rows: byKey
|
|
1936
|
+
};
|
|
1937
|
+
return {
|
|
1938
|
+
rows: indexed,
|
|
744
1939
|
byKey,
|
|
745
1940
|
issues
|
|
746
1941
|
};
|
|
747
1942
|
});
|
|
1943
|
+
const treeStage = memo((rows, byId, _version) => buildTree(rows, byId, readKey, options, lazyChildren));
|
|
1944
|
+
const treeIndex = memo((model) => ({
|
|
1945
|
+
rows: model.rows,
|
|
1946
|
+
byKey: new Map(model.rows.map((row) => [row.key, row])),
|
|
1947
|
+
issues: model.issues
|
|
1948
|
+
}));
|
|
1949
|
+
const treePositions = memo((model) => {
|
|
1950
|
+
const result = /* @__PURE__ */ new Map();
|
|
1951
|
+
const index = (siblings, parent) => {
|
|
1952
|
+
siblings.forEach((node, position) => result.set(node.row.key, Object.freeze({
|
|
1953
|
+
parent,
|
|
1954
|
+
before: siblings[position + 1]?.row.key
|
|
1955
|
+
})));
|
|
1956
|
+
};
|
|
1957
|
+
index(model.roots);
|
|
1958
|
+
for (const node of model.nodes) index(node.children, node.row.key);
|
|
1959
|
+
return result;
|
|
1960
|
+
});
|
|
1961
|
+
const processedTreeStage = memo((model, columns, search, filters, sorting, mode) => processTree(model, columns, {
|
|
1962
|
+
...state,
|
|
1963
|
+
search,
|
|
1964
|
+
filters,
|
|
1965
|
+
sorting
|
|
1966
|
+
}, mode, manual));
|
|
1967
|
+
const flattenTree = createTreeFlattener();
|
|
1968
|
+
const emptyAncestors = /* @__PURE__ */ new Set();
|
|
1969
|
+
const singleTreePath = memo((model, expanded) => {
|
|
1970
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
1971
|
+
if (expanded !== true) for (const key of expanded) {
|
|
1972
|
+
let parent = model.byKey.get(key)?.parent;
|
|
1973
|
+
while (parent && !ancestors.has(parent.row.key)) {
|
|
1974
|
+
ancestors.add(parent.row.key);
|
|
1975
|
+
parent = parent.parent;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
return ancestors;
|
|
1979
|
+
});
|
|
1980
|
+
const treeSelection = memo((model, selection) => {
|
|
1981
|
+
const selected = new Set(selection);
|
|
1982
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1983
|
+
const stack = model.roots.map((node) => ({
|
|
1984
|
+
node,
|
|
1985
|
+
exit: false
|
|
1986
|
+
}));
|
|
1987
|
+
while (stack.length) {
|
|
1988
|
+
const { node, exit } = stack.pop();
|
|
1989
|
+
if (!exit) {
|
|
1990
|
+
stack.push({
|
|
1991
|
+
node,
|
|
1992
|
+
exit: true
|
|
1993
|
+
});
|
|
1994
|
+
for (const child of node.children) stack.push({
|
|
1995
|
+
node: child,
|
|
1996
|
+
exit: false
|
|
1997
|
+
});
|
|
1998
|
+
} else {
|
|
1999
|
+
let count = selected.has(node.row.key) ? 1 : 0;
|
|
2000
|
+
let total = 1;
|
|
2001
|
+
for (const child of node.children) {
|
|
2002
|
+
const value = counts.get(child);
|
|
2003
|
+
count += value.selected;
|
|
2004
|
+
total += value.total;
|
|
2005
|
+
}
|
|
2006
|
+
counts.set(node, {
|
|
2007
|
+
selected: count,
|
|
2008
|
+
total
|
|
2009
|
+
});
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
return counts;
|
|
2013
|
+
});
|
|
748
2014
|
const orderColumns = memo((columns, columnOrder, hiddenColumns) => {
|
|
749
2015
|
const position = new Map(columnOrder.map((id, index) => [id, index]));
|
|
750
2016
|
const ordered = columns.map((column, index) => ({
|
|
@@ -759,11 +2025,43 @@ function createTable(options) {
|
|
|
759
2025
|
visible: ordered.filter((column) => !hidden.has(column.id))
|
|
760
2026
|
};
|
|
761
2027
|
});
|
|
762
|
-
|
|
763
|
-
|
|
2028
|
+
let primedFilter;
|
|
2029
|
+
let primedSort;
|
|
2030
|
+
const filterStage = memo((rows, columns, search, filters) => primedFilter?.rows === rows && primedFilter.columns === columns && primedFilter.search === search && primedFilter.filters === filters ? primedFilter.result : filterRows(rows, columns, search, filters));
|
|
2031
|
+
const sortStage = memo((rows, columns, sorting) => primedSort?.rows === rows && primedSort.columns === columns && primedSort.sorting === sorting ? primedSort.result : sortRows(rows, columns, sorting));
|
|
2032
|
+
const rowMembership = /* @__PURE__ */ new WeakMap();
|
|
2033
|
+
function membership(rows) {
|
|
2034
|
+
let keys = rowMembership.get(rows);
|
|
2035
|
+
if (!keys) {
|
|
2036
|
+
keys = new Set(rows.map((row) => row.key));
|
|
2037
|
+
rowMembership.set(rows, keys);
|
|
2038
|
+
}
|
|
2039
|
+
return keys;
|
|
2040
|
+
}
|
|
764
2041
|
const selectedSet = memo((selection) => new Set(selection));
|
|
2042
|
+
const expansionStage = memo((indexed, expanded, selection) => {
|
|
2043
|
+
const keys = expanded === true ? void 0 : new Set(expanded);
|
|
2044
|
+
const selected = new Set(selection);
|
|
2045
|
+
const rows = mapList(indexed.rows, (row) => {
|
|
2046
|
+
const isExpanded = row.canExpand && (expanded === true || (keys?.has(row.key) ?? false));
|
|
2047
|
+
return isExpanded || selected.has(row.key) ? shareRow(row, Object.freeze({
|
|
2048
|
+
...row,
|
|
2049
|
+
isExpanded,
|
|
2050
|
+
selection: selected.has(row.key) ? "all" : "none"
|
|
2051
|
+
})) : row;
|
|
2052
|
+
});
|
|
2053
|
+
return {
|
|
2054
|
+
rows,
|
|
2055
|
+
byKey: { get(key) {
|
|
2056
|
+
const row = indexed.byKey.get(key);
|
|
2057
|
+
return row && row.index < rows.length ? rows[row.index] : void 0;
|
|
2058
|
+
} }
|
|
2059
|
+
};
|
|
2060
|
+
});
|
|
2061
|
+
const expandedOrder = memo((rows, byKey) => mapList(rows, (row) => byKey.get(row.key)));
|
|
765
2062
|
function stateIssues(byId) {
|
|
766
2063
|
const issues = [];
|
|
2064
|
+
if (expandedIssue) issues.push(expandedIssue);
|
|
767
2065
|
const referenced = /* @__PURE__ */ new Set([
|
|
768
2066
|
...state.sorting.map((rule) => rule.column),
|
|
769
2067
|
...Object.keys(state.filters),
|
|
@@ -772,30 +2070,105 @@ function createTable(options) {
|
|
|
772
2070
|
]);
|
|
773
2071
|
for (const id of referenced) if (!byId.has(id)) issues.push(issue("unknown_column", `The state refers to a column "${id}" that is not defined.`, { column: id }));
|
|
774
2072
|
const { pageSize } = state.pagination;
|
|
775
|
-
if (!Number.isInteger(pageSize) || pageSize < 1) issues.push(issue("invalid_page_size", `Page size ${String(pageSize)} is not a positive whole number; ${10} is used.`));
|
|
2073
|
+
if (paginate && (!Number.isInteger(pageSize) || pageSize < 1)) issues.push(issue("invalid_page_size", `Page size ${String(pageSize)} is not a positive whole number; ${10} is used.`));
|
|
776
2074
|
return issues;
|
|
777
2075
|
}
|
|
778
2076
|
function computeSnapshot() {
|
|
779
|
-
const
|
|
780
|
-
const
|
|
2077
|
+
const cellProblems = validation.problems();
|
|
2078
|
+
const dirty = new Set(pendingChanges.entries.keys());
|
|
2079
|
+
const metadata = (row) => dirty.has(row.key) || cellProblems.has(row.key) ? shareRow(row, Object.freeze({
|
|
2080
|
+
...row,
|
|
2081
|
+
isDirty: dirty.has(row.key),
|
|
2082
|
+
cellIssues: cellProblems.get(row.key)
|
|
2083
|
+
})) : row;
|
|
2084
|
+
const { columns, byId, issues: columnIssues } = resolveColumns(definitions);
|
|
2085
|
+
const model = treeEnabled ? treeStage(data, byId, lazyVersion) : void 0;
|
|
2086
|
+
const indexed = model ? {
|
|
2087
|
+
rows: model.rows,
|
|
2088
|
+
issues: model.issues
|
|
2089
|
+
} : indexRows(data, byId);
|
|
2090
|
+
const expanded = model ? void 0 : expansionStage(indexed, state.expanded, state.selection);
|
|
781
2091
|
const ordered = orderColumns(columns, state.columnOrder, state.hiddenColumns);
|
|
782
|
-
const issues = [
|
|
2092
|
+
const issues = [
|
|
2093
|
+
...optionIssues,
|
|
2094
|
+
...ingestionIssues,
|
|
2095
|
+
...streamIssues,
|
|
2096
|
+
...columnIssues,
|
|
2097
|
+
...indexed.issues,
|
|
2098
|
+
...stateIssues(byId),
|
|
2099
|
+
...loadIssues.values(),
|
|
2100
|
+
...Array.from(loadValidationIssues.values()).flat(),
|
|
2101
|
+
...Array.from(cellProblems.values()).flatMap((row) => [...row.values()])
|
|
2102
|
+
];
|
|
783
2103
|
const requestedSize = state.pagination.pageSize;
|
|
784
|
-
|
|
2104
|
+
let pageSize = Number.isInteger(requestedSize) && requestedSize >= 1 ? requestedSize : 10;
|
|
785
2105
|
let processedRows;
|
|
786
2106
|
let rows;
|
|
787
2107
|
let filteredCount;
|
|
788
2108
|
let page;
|
|
789
2109
|
let pageCount;
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
2110
|
+
let treeRow;
|
|
2111
|
+
let totalCount = indexed.rows.length;
|
|
2112
|
+
if (model) {
|
|
2113
|
+
const processed = processedTreeStage(model, columns, state.search, state.filters, state.sorting, treeFilter);
|
|
2114
|
+
const ancestors = expandMode === "single" ? singleTreePath(model, state.expanded) : emptyAncestors;
|
|
2115
|
+
const visible = flattenTree(processed, state.expanded, ancestors);
|
|
2116
|
+
const expandedState = state.expanded;
|
|
2117
|
+
const keys = state.expanded === true ? void 0 : new Set(state.expanded);
|
|
2118
|
+
const selected = treeSelection(model, state.selection);
|
|
2119
|
+
const projected = /* @__PURE__ */ new Map();
|
|
2120
|
+
const statuses = new Map(childStatuses);
|
|
2121
|
+
treeRow = (key) => {
|
|
2122
|
+
const cached = projected.get(key);
|
|
2123
|
+
if (cached) return cached;
|
|
2124
|
+
const node = model.byKey.get(key);
|
|
2125
|
+
if (!node) return void 0;
|
|
2126
|
+
const status = statuses.get(key) ?? (node.children.length || !node.unloaded ? "loaded" : "idle");
|
|
2127
|
+
const count = selected.get(node);
|
|
2128
|
+
const canExpand = node.children.length > 0 || status !== "loaded" || (options.getRowCanExpand?.(node.row.original) ?? false);
|
|
2129
|
+
const result = shareRow(node.row, Object.freeze({
|
|
2130
|
+
...node.row,
|
|
2131
|
+
isDirty: dirty.has(key),
|
|
2132
|
+
cellIssues: cellProblems.get(key),
|
|
2133
|
+
depth: node.depth,
|
|
2134
|
+
parentKey: node.parent?.row.key,
|
|
2135
|
+
childCount: status === "loaded" ? node.children.length : void 0,
|
|
2136
|
+
childStatus: status,
|
|
2137
|
+
canExpand,
|
|
2138
|
+
isExpanded: canExpand && (expandedState === true || (keys?.has(key) ?? false) || ancestors.has(key) || processed.autoExpanded.has(key)),
|
|
2139
|
+
selection: coverage(count.selected, count.total)
|
|
2140
|
+
}));
|
|
2141
|
+
projected.set(key, result);
|
|
2142
|
+
return result;
|
|
2143
|
+
};
|
|
2144
|
+
processedRows = visible.map((node) => treeRow(node.row.key));
|
|
2145
|
+
filteredCount = manual ? rowCount ?? model.roots.length : rootPagination ? processed.roots.length : visible.length;
|
|
2146
|
+
totalCount = rootPagination ? model.roots.length : model.nodes.length;
|
|
2147
|
+
pageCount = countPages(filteredCount, pageSize);
|
|
2148
|
+
page = clampPage(state.pagination.page, pageCount);
|
|
2149
|
+
if (manual) rows = processedRows;
|
|
2150
|
+
else if (rootPagination) {
|
|
2151
|
+
const roots = new Set(processed.roots.slice((page - 1) * pageSize, page * pageSize));
|
|
2152
|
+
const allRoots = new Set(processed.roots);
|
|
2153
|
+
let root;
|
|
2154
|
+
rows = processedRows.filter((_row, index) => {
|
|
2155
|
+
const node = visible[index];
|
|
2156
|
+
if (allRoots.has(node)) root = node;
|
|
2157
|
+
return root !== void 0 && roots.has(root);
|
|
2158
|
+
});
|
|
2159
|
+
} else rows = paginateRows(processedRows, {
|
|
2160
|
+
page,
|
|
2161
|
+
pageSize
|
|
2162
|
+
});
|
|
2163
|
+
} else if (manual) {
|
|
2164
|
+
processedRows = expanded.rows;
|
|
2165
|
+
rows = expanded.rows;
|
|
793
2166
|
filteredCount = rowCount ?? indexed.rows.length;
|
|
794
2167
|
pageCount = countPages(filteredCount, pageSize);
|
|
795
2168
|
page = clampPage(state.pagination.page, pageCount);
|
|
796
2169
|
} else {
|
|
797
2170
|
const filtered = filterStage(indexed.rows, columns, state.search, state.filters);
|
|
798
|
-
processedRows = sortStage(filtered, columns, state.sorting);
|
|
2171
|
+
processedRows = expandedOrder(sortStage(filtered, columns, state.sorting), expanded.byKey);
|
|
799
2172
|
filteredCount = processedRows.length;
|
|
800
2173
|
pageCount = countPages(filteredCount, pageSize);
|
|
801
2174
|
page = clampPage(state.pagination.page, pageCount);
|
|
@@ -804,42 +2177,90 @@ function createTable(options) {
|
|
|
804
2177
|
pageSize
|
|
805
2178
|
});
|
|
806
2179
|
}
|
|
2180
|
+
if (!paginate) {
|
|
2181
|
+
rows = processedRows;
|
|
2182
|
+
page = 1;
|
|
2183
|
+
pageCount = 1;
|
|
2184
|
+
pageSize = Math.max(1, rows.length);
|
|
2185
|
+
}
|
|
2186
|
+
const flatProjected = /* @__PURE__ */ new Map();
|
|
2187
|
+
if (!model) {
|
|
2188
|
+
const project = (row) => {
|
|
2189
|
+
let result = flatProjected.get(row.key);
|
|
2190
|
+
if (!result) {
|
|
2191
|
+
result = metadata(row);
|
|
2192
|
+
flatProjected.set(row.key, result);
|
|
2193
|
+
}
|
|
2194
|
+
return result;
|
|
2195
|
+
};
|
|
2196
|
+
const shared = rows === processedRows;
|
|
2197
|
+
processedRows = mapList(processedRows, project);
|
|
2198
|
+
rows = shared ? processedRows : rows.map(project);
|
|
2199
|
+
}
|
|
807
2200
|
const selected = selectedSet(state.selection);
|
|
808
|
-
const
|
|
2201
|
+
const allData = model ? model.byKey : indexed.byKey;
|
|
2202
|
+
const allProcessedKeys = model ? void 0 : manual ? void 0 : membership(filterStage(indexed.rows, columns, state.search, state.filters));
|
|
2203
|
+
const selectedInProcessed = state.selection.filter((key) => allData.has(key) && (!allProcessedKeys || allProcessedKeys.has(key))).length;
|
|
2204
|
+
const pageSelected = rows === processedRows ? selectedInProcessed : rows.filter((row) => selected.has(row.key)).length;
|
|
809
2205
|
const scopeRows = selectScope === "page" || manual ? rows : processedRows;
|
|
810
|
-
const scopeSelected = scopeRows === rows ? pageSelected : scopeRows.filter((row) => selected.has(row.key)).length;
|
|
2206
|
+
const scopeSelected = scopeRows === rows ? pageSelected : model ? scopeRows.filter((row) => selected.has(row.key)).length : selectedInProcessed;
|
|
811
2207
|
const pageStart = rows.length === 0 ? 0 : (page - 1) * pageSize + 1;
|
|
812
2208
|
const sortIndex = new Map(state.sorting.map((rule, priority) => [rule.column, {
|
|
813
2209
|
direction: rule.direction,
|
|
814
2210
|
priority
|
|
815
2211
|
}]));
|
|
816
2212
|
return Object.freeze({
|
|
2213
|
+
tree: treeEnabled,
|
|
2214
|
+
loadState,
|
|
2215
|
+
loadedRowCount: indexed.rows.length,
|
|
2216
|
+
expectedRowCount,
|
|
817
2217
|
state,
|
|
818
2218
|
columns: ordered.visible,
|
|
819
2219
|
allColumns: ordered.all,
|
|
820
2220
|
rows,
|
|
2221
|
+
renderItems: !model && state.expanded !== true && state.expanded.length === 0 ? mapList(rows, (row, rowIndex) => Object.freeze({
|
|
2222
|
+
kind: "row",
|
|
2223
|
+
key: getRowItemKey(row.key),
|
|
2224
|
+
row,
|
|
2225
|
+
rowIndex
|
|
2226
|
+
})) : Object.freeze(rows.flatMap((row, rowIndex) => {
|
|
2227
|
+
const item = Object.freeze({
|
|
2228
|
+
kind: "row",
|
|
2229
|
+
key: getRowItemKey(row.key),
|
|
2230
|
+
row,
|
|
2231
|
+
rowIndex
|
|
2232
|
+
});
|
|
2233
|
+
return row.isExpanded && (!model || (options.getRowCanExpand?.(row.original) ?? false)) ? [item, Object.freeze({
|
|
2234
|
+
kind: "detail",
|
|
2235
|
+
key: getRowItemKey(row.key, "detail"),
|
|
2236
|
+
row,
|
|
2237
|
+
rowIndex
|
|
2238
|
+
})] : [item];
|
|
2239
|
+
})),
|
|
2240
|
+
paginate,
|
|
821
2241
|
processedRows,
|
|
822
2242
|
page,
|
|
823
2243
|
pageSize,
|
|
824
2244
|
pageCount,
|
|
825
2245
|
rowCount: filteredCount,
|
|
826
|
-
totalRowCount: manual ? filteredCount :
|
|
2246
|
+
totalRowCount: manual ? filteredCount : totalCount,
|
|
827
2247
|
pageStart,
|
|
828
|
-
pageEnd: rows.length === 0 ? 0 : pageStart + rows.length - 1,
|
|
2248
|
+
pageEnd: rows.length === 0 ? 0 : rootPagination ? Math.min(filteredCount, page * pageSize) : pageStart + rows.length - 1,
|
|
829
2249
|
pageItems: paginationItems(page, pageCount),
|
|
830
2250
|
canPreviousPage: page > 1,
|
|
831
2251
|
canNextPage: page < pageCount,
|
|
832
2252
|
selectionMode,
|
|
833
|
-
selectedCount: state.selection.length,
|
|
2253
|
+
selectedCount: model ? state.selection.filter((key) => model.byKey.has(key)).length : state.selection.length,
|
|
834
2254
|
pageSelection: coverage(pageSelected, rows.length),
|
|
835
2255
|
allSelection: coverage(scopeSelected, scopeRows.length),
|
|
836
2256
|
canUndo: undoStack.canUndo,
|
|
837
2257
|
canRedo: undoStack.canRedo,
|
|
838
2258
|
issues: Object.freeze(issues),
|
|
2259
|
+
pendingCells: validation.cells(),
|
|
839
2260
|
isSelected: (key) => selected.has(key),
|
|
840
2261
|
getSort: (columnId) => sortIndex.get(columnId),
|
|
841
2262
|
getColumn: (columnId) => byId.get(columnId),
|
|
842
|
-
getRow: (key) =>
|
|
2263
|
+
getRow: (key) => treeRow ? treeRow(key) : flatProjected.get(key) ?? (expanded.byKey.get(key) ? metadata(expanded.byKey.get(key)) : void 0)
|
|
843
2264
|
});
|
|
844
2265
|
}
|
|
845
2266
|
function getSnapshot() {
|
|
@@ -848,18 +2269,140 @@ function createTable(options) {
|
|
|
848
2269
|
}
|
|
849
2270
|
function notify() {
|
|
850
2271
|
snapshot = void 0;
|
|
2272
|
+
if (deferValidationNotify) {
|
|
2273
|
+
validationNotifyRequested = true;
|
|
2274
|
+
return;
|
|
2275
|
+
}
|
|
2276
|
+
if (transaction) return;
|
|
851
2277
|
if (listeners.size === 0) return;
|
|
852
2278
|
const next = getSnapshot();
|
|
853
2279
|
for (const listener of listeners) listener(next);
|
|
854
2280
|
}
|
|
2281
|
+
function abortLoads(all = false) {
|
|
2282
|
+
for (const [key, controller] of pendingLoads) {
|
|
2283
|
+
const row = all ? void 0 : getSnapshot().getRow(key);
|
|
2284
|
+
const visible = !all && getSnapshot().processedRows.some((candidate) => candidate.key === key);
|
|
2285
|
+
if (all || !row?.isExpanded || !visible) {
|
|
2286
|
+
pendingLoads.delete(key);
|
|
2287
|
+
childStatuses.delete(key);
|
|
2288
|
+
controller.abort();
|
|
2289
|
+
snapshot = void 0;
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
function requestChildren(row) {
|
|
2294
|
+
if (!options.loadChildren || destroyed || pendingLoads.has(row.key) || lazyChildren.has(row.key) || row.childStatus === "loaded") return;
|
|
2295
|
+
const key = row.key;
|
|
2296
|
+
if (!options.createChildLoadController) {
|
|
2297
|
+
childStatuses.set(key, "error");
|
|
2298
|
+
loadIssues.set(key, issue("invalid_tree_option", "Lazy children require createChildLoadController from the caller's runtime.", { rowKey: key }));
|
|
2299
|
+
notify();
|
|
2300
|
+
return;
|
|
2301
|
+
}
|
|
2302
|
+
let controller;
|
|
2303
|
+
try {
|
|
2304
|
+
controller = options.createChildLoadController();
|
|
2305
|
+
} catch (error) {
|
|
2306
|
+
loadIssues.set(key, issue("tree_load_error", String(error), { rowKey: key }));
|
|
2307
|
+
childStatuses.set(key, "error");
|
|
2308
|
+
notify();
|
|
2309
|
+
return;
|
|
2310
|
+
}
|
|
2311
|
+
pendingLoads.set(key, controller);
|
|
2312
|
+
childStatuses.set(key, "loading");
|
|
2313
|
+
loadIssues.delete(key);
|
|
2314
|
+
loadValidationIssues.delete(key);
|
|
2315
|
+
notify();
|
|
2316
|
+
Promise.resolve().then(() => controller.signal.aborted ? [] : options.loadChildren(row, controller.signal)).then((children) => {
|
|
2317
|
+
if (controller.signal.aborted || pendingLoads.get(key) !== controller || destroyed) return void 0;
|
|
2318
|
+
const { byId } = resolveColumns(definitions);
|
|
2319
|
+
const existing = treeStage(data, byId, lazyVersion);
|
|
2320
|
+
const candidate = buildTree(children, byId, readKey, {
|
|
2321
|
+
...options,
|
|
2322
|
+
getParentKey: void 0
|
|
2323
|
+
}, /* @__PURE__ */ new Map());
|
|
2324
|
+
pendingLoads.delete(key);
|
|
2325
|
+
loadValidationIssues.set(key, candidate.issues.filter((problem) => problem.code === "tree_cycle" || problem.code === "tree_depth_exceeded"));
|
|
2326
|
+
if (candidate.nodes.filter((node) => existing.byKey.has(node.row.key)).length || candidate.issues.some((problem) => problem.code === "duplicate_row_key")) {
|
|
2327
|
+
childStatuses.set(key, "error");
|
|
2328
|
+
loadIssues.set(key, issue("tree_duplicate_key", "Loaded children collide with existing keys; the batch is rejected.", { rowKey: key }));
|
|
2329
|
+
} else {
|
|
2330
|
+
lazyChildren.set(key, children);
|
|
2331
|
+
childStatuses.set(key, "loaded");
|
|
2332
|
+
lazyVersion++;
|
|
2333
|
+
if (state.selection.includes(key) && selectionMode === "multiple") {
|
|
2334
|
+
const nextModel = treeStage(data, byId, lazyVersion);
|
|
2335
|
+
applySelection([...state.selection, ...selectionKeys([key], nextModel)]);
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
notify();
|
|
2339
|
+
startVisibleLoads();
|
|
2340
|
+
}).catch((error) => {
|
|
2341
|
+
if (controller.signal.aborted || pendingLoads.get(key) !== controller || destroyed) return;
|
|
2342
|
+
pendingLoads.delete(key);
|
|
2343
|
+
childStatuses.set(key, "error");
|
|
2344
|
+
loadIssues.set(key, issue("tree_load_error", `Children could not be loaded: ${String(error)}`, { rowKey: key }));
|
|
2345
|
+
notify();
|
|
2346
|
+
});
|
|
2347
|
+
}
|
|
2348
|
+
function startVisibleLoads() {
|
|
2349
|
+
if (!treeEnabled || !options.loadChildren || destroyed) return;
|
|
2350
|
+
const current = getSnapshot();
|
|
2351
|
+
for (const row of current.rows) if (row.isExpanded && row.childStatus === "idle") requestChildren(row);
|
|
2352
|
+
}
|
|
2353
|
+
function selectionKeys(keys, model) {
|
|
2354
|
+
if (!treeEnabled || selectionMode !== "multiple") return [...keys];
|
|
2355
|
+
model ??= treeStage(data, resolveColumns(definitions).byId, lazyVersion);
|
|
2356
|
+
const result = new Set(keys);
|
|
2357
|
+
const stack = keys.flatMap((key) => {
|
|
2358
|
+
const node = model.byKey.get(key);
|
|
2359
|
+
return node ? [node] : [];
|
|
2360
|
+
});
|
|
2361
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2362
|
+
while (stack.length) {
|
|
2363
|
+
const node = stack.pop();
|
|
2364
|
+
if (visited.has(node.row.key)) continue;
|
|
2365
|
+
visited.add(node.row.key);
|
|
2366
|
+
result.add(node.row.key);
|
|
2367
|
+
for (const child of node.children) stack.push(child);
|
|
2368
|
+
}
|
|
2369
|
+
return [...result];
|
|
2370
|
+
}
|
|
855
2371
|
/** Apply a change made by a table operation. */
|
|
856
2372
|
function commit(patch) {
|
|
857
2373
|
const previous = state;
|
|
858
|
-
const
|
|
859
|
-
|
|
2374
|
+
const previousIssue = expandedIssue;
|
|
2375
|
+
const next = mergePatch(patch);
|
|
2376
|
+
if (next === previous && previousIssue === expandedIssue) return;
|
|
860
2377
|
state = next;
|
|
861
|
-
|
|
2378
|
+
snapshot = void 0;
|
|
2379
|
+
abortLoads();
|
|
2380
|
+
if (next !== previous) options.onStateChange?.(state, previous);
|
|
862
2381
|
notify();
|
|
2382
|
+
startVisibleLoads();
|
|
2383
|
+
}
|
|
2384
|
+
function canonicalExpanded(expanded, candidate = state) {
|
|
2385
|
+
if (expandMode === "multiple") return expanded;
|
|
2386
|
+
if (expanded !== true) return expanded.slice(-1);
|
|
2387
|
+
const { columns, byId } = resolveColumns(definitions);
|
|
2388
|
+
if (treeEnabled) {
|
|
2389
|
+
const model = treeStage(data, byId, lazyVersion);
|
|
2390
|
+
const first = processedTreeStage(model, columns, candidate.search, candidate.filters, candidate.sorting, treeFilter).roots.find((node) => node.children.length > 0 || (options.hasChildren?.(node.row.original) ?? false) || (options.getRowCanExpand?.(node.row.original) ?? false));
|
|
2391
|
+
return first ? [first.row.key] : [];
|
|
2392
|
+
}
|
|
2393
|
+
const indexed = indexRows(data, byId);
|
|
2394
|
+
const first = (manual ? indexed.rows : sortStage(filterStage(indexed.rows, columns, candidate.search, candidate.filters), columns, candidate.sorting)).find((row) => row.canExpand);
|
|
2395
|
+
return first ? [first.key] : [];
|
|
2396
|
+
}
|
|
2397
|
+
function mergePatch(patch) {
|
|
2398
|
+
if (patch.expanded !== void 0) expandedIssue = void 0;
|
|
2399
|
+
const next = mergeState(state, patch, (problem) => {
|
|
2400
|
+
expandedIssue = problem;
|
|
2401
|
+
});
|
|
2402
|
+
return expandMode === "single" && patch.expanded !== void 0 ? mergeState(state, {
|
|
2403
|
+
...patch,
|
|
2404
|
+
expanded: canonicalExpanded(next.expanded, next)
|
|
2405
|
+
}) : next;
|
|
863
2406
|
}
|
|
864
2407
|
const firstPage = () => ({
|
|
865
2408
|
page: 1,
|
|
@@ -890,10 +2433,14 @@ function createTable(options) {
|
|
|
890
2433
|
if (selection.length === current.length && selection.every((key, i) => key === current[i])) return;
|
|
891
2434
|
commit({ selection });
|
|
892
2435
|
}
|
|
893
|
-
function applyChanges(edits, record) {
|
|
2436
|
+
function applyChanges(edits, record, recoveredIssues = []) {
|
|
894
2437
|
const current = getSnapshot();
|
|
895
2438
|
const working = [...data];
|
|
896
|
-
const
|
|
2439
|
+
const model = treeEnabled ? treeStage(data, resolveColumns(definitions).byId, lazyVersion) : void 0;
|
|
2440
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
2441
|
+
const cache = new Map(lazyChildren);
|
|
2442
|
+
const originalOf = (node) => replacements.get(node) ?? node.row.original;
|
|
2443
|
+
const issues = [...recoveredIssues];
|
|
897
2444
|
const changes = [];
|
|
898
2445
|
const samples = /* @__PURE__ */ new Map();
|
|
899
2446
|
const sampleOf = (columnId) => {
|
|
@@ -916,6 +2463,13 @@ function createTable(options) {
|
|
|
916
2463
|
}));
|
|
917
2464
|
continue;
|
|
918
2465
|
}
|
|
2466
|
+
if (!isSafePath(edit.column)) {
|
|
2467
|
+
issues.push(issue("unsafe_path", `The path "${edit.column}" addresses a prototype and cannot be written.`, {
|
|
2468
|
+
rowKey: edit.rowKey,
|
|
2469
|
+
column: edit.column
|
|
2470
|
+
}));
|
|
2471
|
+
continue;
|
|
2472
|
+
}
|
|
919
2473
|
if (!column) {
|
|
920
2474
|
issues.push(issue("unknown_column", `No column has the id "${edit.column}".`, {
|
|
921
2475
|
rowKey: edit.rowKey,
|
|
@@ -923,7 +2477,22 @@ function createTable(options) {
|
|
|
923
2477
|
}));
|
|
924
2478
|
continue;
|
|
925
2479
|
}
|
|
926
|
-
const
|
|
2480
|
+
const node = model?.byKey.get(row.key);
|
|
2481
|
+
const original = node ? originalOf(node) : working[row.index];
|
|
2482
|
+
if (node && !node.writable) {
|
|
2483
|
+
issues.push(issue("read_only_cell", "A recovered nested root has no unambiguous source path for editing.", {
|
|
2484
|
+
rowKey: row.key,
|
|
2485
|
+
column: column.id
|
|
2486
|
+
}));
|
|
2487
|
+
continue;
|
|
2488
|
+
}
|
|
2489
|
+
if (node?.parent && options.getChildren && !options.setChildren) {
|
|
2490
|
+
issues.push(issue("read_only_cell", "Editing a nested child requires an immutable setChildren callback.", {
|
|
2491
|
+
rowKey: row.key,
|
|
2492
|
+
column: column.id
|
|
2493
|
+
}));
|
|
2494
|
+
continue;
|
|
2495
|
+
}
|
|
927
2496
|
if (record && !column.isEditable(original)) {
|
|
928
2497
|
issues.push(issue("read_only_cell", `The "${column.id}" cell of row "${String(row.key)}" is read-only.`, {
|
|
929
2498
|
rowKey: row.key,
|
|
@@ -945,7 +2514,16 @@ function createTable(options) {
|
|
|
945
2514
|
} else value = edit.value;
|
|
946
2515
|
const previous = column.getValue(original);
|
|
947
2516
|
if (Object.is(previous, value)) continue;
|
|
948
|
-
|
|
2517
|
+
let updated;
|
|
2518
|
+
try {
|
|
2519
|
+
updated = column.setValue(original, value);
|
|
2520
|
+
} catch (error) {
|
|
2521
|
+
issues.push(issue("invalid_value", error instanceof Error ? error.message : String(error), {
|
|
2522
|
+
rowKey: row.key,
|
|
2523
|
+
column: column.id
|
|
2524
|
+
}));
|
|
2525
|
+
continue;
|
|
2526
|
+
}
|
|
949
2527
|
if (updated === void 0) {
|
|
950
2528
|
issues.push(issue("read_only_cell", `The "${column.id}" column has no way to write a value.`, {
|
|
951
2529
|
rowKey: row.key,
|
|
@@ -953,7 +2531,38 @@ function createTable(options) {
|
|
|
953
2531
|
}));
|
|
954
2532
|
continue;
|
|
955
2533
|
}
|
|
956
|
-
|
|
2534
|
+
if (readKey(updated, row.index) !== readKey(original, row.index)) {
|
|
2535
|
+
issues.push(issue("invalid_row_operation", "Editing a row key is not supported; remove and insert the row instead.", {
|
|
2536
|
+
rowKey: row.key,
|
|
2537
|
+
column: column.id
|
|
2538
|
+
}));
|
|
2539
|
+
continue;
|
|
2540
|
+
}
|
|
2541
|
+
if (!node) working[row.index] = updated;
|
|
2542
|
+
else {
|
|
2543
|
+
const planned = /* @__PURE__ */ new Map([[node, updated]]);
|
|
2544
|
+
const plannedCache = /* @__PURE__ */ new Map();
|
|
2545
|
+
let cursor = node;
|
|
2546
|
+
try {
|
|
2547
|
+
while (cursor.parent) {
|
|
2548
|
+
const parent = cursor.parent;
|
|
2549
|
+
const children = parent.children.map((child) => planned.get(child) ?? originalOf(child));
|
|
2550
|
+
if (cursor.lazy) plannedCache.set(parent.row.key, children);
|
|
2551
|
+
if (options.setChildren) planned.set(parent, options.setChildren(originalOf(parent), children));
|
|
2552
|
+
cursor = parent;
|
|
2553
|
+
}
|
|
2554
|
+
} catch (error) {
|
|
2555
|
+
issues.push(issue("invalid_value", error instanceof Error ? error.message : String(error), {
|
|
2556
|
+
rowKey: row.key,
|
|
2557
|
+
column: column.id
|
|
2558
|
+
}));
|
|
2559
|
+
continue;
|
|
2560
|
+
}
|
|
2561
|
+
for (const [item, plannedRow] of planned) replacements.set(item, plannedRow);
|
|
2562
|
+
for (const [key, children] of plannedCache) cache.set(key, children);
|
|
2563
|
+
if (!options.getChildren && !node.lazy) working[node.sourceIndex] = updated;
|
|
2564
|
+
if (options.getChildren || options.setChildren) working[cursor.sourceIndex] = originalOf(cursor);
|
|
2565
|
+
}
|
|
957
2566
|
changes.push({
|
|
958
2567
|
rowKey: row.key,
|
|
959
2568
|
column: column.id,
|
|
@@ -967,16 +2576,389 @@ function createTable(options) {
|
|
|
967
2576
|
else {
|
|
968
2577
|
status = issues.length > 0 ? "partial" : "applied";
|
|
969
2578
|
data = working;
|
|
970
|
-
if (
|
|
971
|
-
|
|
2579
|
+
if (model) {
|
|
2580
|
+
lazyChildren.clear();
|
|
2581
|
+
for (const [key, children] of cache) lazyChildren.set(key, children);
|
|
2582
|
+
lazyVersion++;
|
|
2583
|
+
}
|
|
2584
|
+
if (record) {
|
|
2585
|
+
if (!transaction) undoStack.record({
|
|
2586
|
+
kind: "cells",
|
|
2587
|
+
changes
|
|
2588
|
+
});
|
|
2589
|
+
}
|
|
2590
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2591
|
+
for (const change of changes) {
|
|
2592
|
+
const group = groups.get(change.rowKey) ?? [];
|
|
2593
|
+
group.push(change);
|
|
2594
|
+
groups.set(change.rowKey, group);
|
|
2595
|
+
}
|
|
2596
|
+
for (const [key, group] of groups) pendingChanges.record(key, current.getRow(key).original, group.at(-1).row, positionOf(key), group);
|
|
2597
|
+
if (!transaction) options.onDataChange?.(data, changes);
|
|
972
2598
|
notify();
|
|
973
2599
|
}
|
|
974
|
-
if (issues.length > 0) options.onEditIssues?.(issues);
|
|
2600
|
+
if (issues.length > 0 && !transaction) options.onEditIssues?.(issues);
|
|
975
2601
|
return Object.freeze({
|
|
976
2602
|
status,
|
|
977
2603
|
changes: Object.freeze(changes),
|
|
978
|
-
issues: Object.freeze(issues)
|
|
2604
|
+
issues: Object.freeze(issues),
|
|
2605
|
+
rowChanges: Object.freeze([]),
|
|
2606
|
+
pendingCells: Object.freeze([])
|
|
2607
|
+
});
|
|
2608
|
+
}
|
|
2609
|
+
function sourceRows() {
|
|
2610
|
+
const { byId } = resolveColumns(definitions);
|
|
2611
|
+
const model = treeEnabled ? treeStage(data, byId, lazyVersion) : void 0;
|
|
2612
|
+
return model ? treeIndex(model) : indexRows(data, byId);
|
|
2613
|
+
}
|
|
2614
|
+
function positionOf(key) {
|
|
2615
|
+
const { byId } = resolveColumns(definitions);
|
|
2616
|
+
if (treeEnabled) {
|
|
2617
|
+
const model = treeStage(data, byId, lazyVersion);
|
|
2618
|
+
return treePositions(model).get(key) ?? Object.freeze({});
|
|
2619
|
+
}
|
|
2620
|
+
const indexed = indexRows(data, byId);
|
|
2621
|
+
const row = indexed.byKey.get(key);
|
|
2622
|
+
return Object.freeze({ before: row ? indexed.rows[row.index + 1]?.key : void 0 });
|
|
2623
|
+
}
|
|
2624
|
+
function scene() {
|
|
2625
|
+
return {
|
|
2626
|
+
data,
|
|
2627
|
+
lazy: new Map(lazyChildren)
|
|
2628
|
+
};
|
|
2629
|
+
}
|
|
2630
|
+
function resultForRows(changes, issues, cells = []) {
|
|
2631
|
+
return Object.freeze({
|
|
2632
|
+
status: changes.length || cells.length ? issues.length ? "partial" : "applied" : issues.length ? "rejected" : "unchanged",
|
|
2633
|
+
changes: Object.freeze([...cells]),
|
|
2634
|
+
rowChanges: Object.freeze([...changes]),
|
|
2635
|
+
pendingCells: Object.freeze([]),
|
|
2636
|
+
issues: Object.freeze([...issues])
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2639
|
+
function rowProblem(message, rowKey) {
|
|
2640
|
+
return issue("invalid_row_operation", message, { rowKey });
|
|
2641
|
+
}
|
|
2642
|
+
function stableRows(rows) {
|
|
2643
|
+
return options.rowKey !== void 0 || rows.every((row) => {
|
|
2644
|
+
const key = getPath(row, "id");
|
|
2645
|
+
return typeof key === "string" || typeof key === "number";
|
|
2646
|
+
});
|
|
2647
|
+
}
|
|
2648
|
+
function cellDifferences(previous, row, key) {
|
|
2649
|
+
return resolveColumns(definitions).columns.flatMap((column) => {
|
|
2650
|
+
const before = column.getValue(previous);
|
|
2651
|
+
const value = column.getValue(row);
|
|
2652
|
+
return Object.is(before, value) ? [] : [{
|
|
2653
|
+
rowKey: key,
|
|
2654
|
+
column: column.id,
|
|
2655
|
+
previous: before,
|
|
2656
|
+
value,
|
|
2657
|
+
row
|
|
2658
|
+
}];
|
|
2659
|
+
});
|
|
2660
|
+
}
|
|
2661
|
+
function recordRows(previous, keys, positions, baselines = /* @__PURE__ */ new Map()) {
|
|
2662
|
+
const next = sourceRows();
|
|
2663
|
+
const rowChanges = [];
|
|
2664
|
+
for (const key of keys) {
|
|
2665
|
+
const before = previous.byKey.get(key)?.original;
|
|
2666
|
+
const row = next.byKey.get(key)?.original;
|
|
2667
|
+
const position = positions.get(key) ?? positionOf(key);
|
|
2668
|
+
const cells = before !== void 0 && row !== void 0 ? cellDifferences(before, row, key) : [];
|
|
2669
|
+
const baseline = baselines.get(key);
|
|
2670
|
+
const pending = pendingChanges.entries.get(key);
|
|
2671
|
+
if (row === void 0 && baseline !== void 0 && pending?.previous !== void 0) pending.previous = baseline;
|
|
2672
|
+
pendingChanges.record(key, row === void 0 ? baseline ?? before : before, row, position, cells);
|
|
2673
|
+
if (before === void 0 && row !== void 0) rowChanges.push(Object.freeze({
|
|
2674
|
+
kind: "inserted",
|
|
2675
|
+
rowKey: key,
|
|
2676
|
+
row,
|
|
2677
|
+
position: positionOf(key)
|
|
2678
|
+
}));
|
|
2679
|
+
else if (row === void 0 && before !== void 0) rowChanges.push(Object.freeze({
|
|
2680
|
+
kind: "removed",
|
|
2681
|
+
rowKey: key,
|
|
2682
|
+
row: before,
|
|
2683
|
+
position
|
|
2684
|
+
}));
|
|
2685
|
+
}
|
|
2686
|
+
return rowChanges;
|
|
2687
|
+
}
|
|
2688
|
+
function publishRows(before, previous, keys, positions, issues, baselines = /* @__PURE__ */ new Map()) {
|
|
2689
|
+
const rowChanges = recordRows(previous, keys, positions, baselines);
|
|
2690
|
+
if (!transaction) {
|
|
2691
|
+
undoStack.record({
|
|
2692
|
+
kind: "rows",
|
|
2693
|
+
before,
|
|
2694
|
+
after: scene(),
|
|
2695
|
+
keys
|
|
2696
|
+
});
|
|
2697
|
+
options.onDataChange?.(data, []);
|
|
2698
|
+
if (issues.length) options.onEditIssues?.(issues);
|
|
2699
|
+
}
|
|
2700
|
+
notify();
|
|
2701
|
+
return resultForRows(rowChanges, issues);
|
|
2702
|
+
}
|
|
2703
|
+
function restoreScene(entry, direction) {
|
|
2704
|
+
const previous = sourceRows();
|
|
2705
|
+
const baselines = removalBaselines();
|
|
2706
|
+
const positions = new Map(entry.keys.map((key) => [key, positionOf(key)]));
|
|
2707
|
+
const target = direction === "undo" ? entry.before : entry.after;
|
|
2708
|
+
data = target.data;
|
|
2709
|
+
const changed = /* @__PURE__ */ new Set([...entry.before.lazy.keys(), ...entry.after.lazy.keys()]);
|
|
2710
|
+
for (const key of changed) {
|
|
2711
|
+
if (entry.before.lazy.get(key) === entry.after.lazy.get(key)) continue;
|
|
2712
|
+
const rows = target.lazy.get(key);
|
|
2713
|
+
if (rows) lazyChildren.set(key, rows);
|
|
2714
|
+
else lazyChildren.delete(key);
|
|
2715
|
+
}
|
|
2716
|
+
lazyVersion++;
|
|
2717
|
+
recordRows(previous, entry.keys, positions, baselines);
|
|
2718
|
+
options.onDataChange?.(data, []);
|
|
2719
|
+
notify();
|
|
2720
|
+
}
|
|
2721
|
+
function removalBaselines() {
|
|
2722
|
+
const baselines = /* @__PURE__ */ new Map();
|
|
2723
|
+
if (!options.getChildren || !options.setChildren) return baselines;
|
|
2724
|
+
const model = treeStage(data, resolveColumns(definitions).byId, lazyVersion);
|
|
2725
|
+
for (const node of model.nodes.toReversed()) {
|
|
2726
|
+
const entry = pendingChanges.entries.get(node.row.key);
|
|
2727
|
+
if (entry && entry.previous === void 0) continue;
|
|
2728
|
+
let baseline = entry?.previous ?? node.row.original;
|
|
2729
|
+
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);
|
|
2730
|
+
const originalChildren = options.getChildren(baseline) ?? [];
|
|
2731
|
+
if (children.length !== originalChildren.length || children.some((child, index) => child !== originalChildren[index])) baseline = options.setChildren(baseline, children);
|
|
2732
|
+
baselines.set(node.row.key, baseline);
|
|
2733
|
+
}
|
|
2734
|
+
return baselines;
|
|
2735
|
+
}
|
|
2736
|
+
function rewriteNested(model, removed, overrides, cache) {
|
|
2737
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
2738
|
+
for (const node of model.nodes.toReversed()) {
|
|
2739
|
+
if (removed.has(node.row.key)) continue;
|
|
2740
|
+
const children = overrides.get(node.row.key) ?? node.children.filter((child) => !removed.has(child.row.key)).map((child) => replacements.get(child) ?? child.row.original);
|
|
2741
|
+
if (!(overrides.has(node.row.key) || children.length !== node.children.length || children.some((child, index) => child !== node.children[index]?.row.original))) continue;
|
|
2742
|
+
if (!options.setChildren) throw new Error("Changing nested children requires setChildren.");
|
|
2743
|
+
replacements.set(node, options.setChildren(node.row.original, children));
|
|
2744
|
+
if (cache.has(node.row.key)) cache.set(node.row.key, children);
|
|
2745
|
+
}
|
|
2746
|
+
const roots = new Map(model.roots.filter((node) => node.writable).map((node) => [node.sourceIndex, node]));
|
|
2747
|
+
return data.flatMap((row, index) => {
|
|
2748
|
+
const node = roots.get(index);
|
|
2749
|
+
return node && removed.has(node.row.key) ? [] : [node ? replacements.get(node) ?? row : row];
|
|
2750
|
+
});
|
|
2751
|
+
}
|
|
2752
|
+
function insertRows(rows, at = {}) {
|
|
2753
|
+
const before = scene();
|
|
2754
|
+
const previous = sourceRows();
|
|
2755
|
+
const problems = [];
|
|
2756
|
+
const reject = (problem) => {
|
|
2757
|
+
if (!transaction) options.onEditIssues?.([problem]);
|
|
2758
|
+
return resultForRows([], [problem]);
|
|
2759
|
+
};
|
|
2760
|
+
let incoming;
|
|
2761
|
+
try {
|
|
2762
|
+
incoming = rows ?? (options.createRow ? [options.createRow()] : []);
|
|
2763
|
+
} catch (error) {
|
|
2764
|
+
return reject(rowProblem(error instanceof Error ? error.message : String(error)));
|
|
2765
|
+
}
|
|
2766
|
+
if (rows === void 0 && !options.createRow) return reject(rowProblem("insertRows() requires rows or a createRow callback."));
|
|
2767
|
+
if (!incoming.length) return resultForRows([], []);
|
|
2768
|
+
if (!stableRows(data) || !stableRows(incoming)) return reject(rowProblem("Row operations require stable row keys; supply rowKey or an id on every row."));
|
|
2769
|
+
const { byId } = resolveColumns(definitions);
|
|
2770
|
+
const model = treeEnabled ? treeStage(data, byId, lazyVersion) : void 0;
|
|
2771
|
+
const parent = at.parent === void 0 ? void 0 : model?.byKey.get(at.parent);
|
|
2772
|
+
const sibling = at.before === void 0 ? void 0 : previous.byKey.get(at.before);
|
|
2773
|
+
if (at.parent !== void 0 && !parent) return reject(issue("unknown_row", "The insertion parent does not exist.", { rowKey: at.parent }));
|
|
2774
|
+
if (at.before !== void 0 && !sibling) return reject(issue("unknown_row", "The insertion sibling does not exist.", { rowKey: at.before }));
|
|
2775
|
+
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));
|
|
2776
|
+
if (parent && !parent.writable) return reject(rowProblem("A recovered parent has no writable source path.", parent.row.key));
|
|
2777
|
+
if (parent && getSnapshot().getRow(parent.row.key)?.childStatus !== "loaded") return reject(rowProblem("Load the parent's children before inserting rows.", parent.row.key));
|
|
2778
|
+
const cache = new Map(lazyChildren);
|
|
2779
|
+
let next;
|
|
2780
|
+
try {
|
|
2781
|
+
if (options.getChildren && parent && model) {
|
|
2782
|
+
const children = parent.children.map((child) => child.row.original);
|
|
2783
|
+
const index = at.before === void 0 ? children.length : parent.children.findIndex((child) => child.row.key === at.before);
|
|
2784
|
+
children.splice(index, 0, ...incoming);
|
|
2785
|
+
next = rewriteNested(model, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Map([[parent.row.key, children]]), cache);
|
|
2786
|
+
} else if (parent && cache.has(parent.row.key)) {
|
|
2787
|
+
incoming = incoming.map((row) => options.setParentKey ? options.setParentKey(row, parent.row.key) : row);
|
|
2788
|
+
const children = [...cache.get(parent.row.key)];
|
|
2789
|
+
const index = at.before === void 0 ? children.length : parent.children.findIndex((child) => child.row.key === at.before);
|
|
2790
|
+
children.splice(index, 0, ...incoming);
|
|
2791
|
+
cache.set(parent.row.key, children);
|
|
2792
|
+
next = [...data];
|
|
2793
|
+
} else {
|
|
2794
|
+
if (parent && options.getParentKey) {
|
|
2795
|
+
incoming = incoming.map((row) => options.setParentKey ? options.setParentKey(row, parent.row.key) : row);
|
|
2796
|
+
if (incoming.some((row) => options.getParentKey(row) !== parent.row.key)) return reject(rowProblem("Supply setParentKey or rows already addressed to the requested parent."));
|
|
2797
|
+
}
|
|
2798
|
+
const working = [...data];
|
|
2799
|
+
working.splice(sibling?.index ?? working.length, 0, ...incoming);
|
|
2800
|
+
next = working;
|
|
2801
|
+
}
|
|
2802
|
+
const candidate = treeEnabled ? buildTree(next, byId, readKey, options, cache) : indexRows(next, byId);
|
|
2803
|
+
if (!stableRows(candidate.rows.map((row) => row.original))) return reject(rowProblem("Every inserted descendant must have a stable key."));
|
|
2804
|
+
const baseline = /* @__PURE__ */ new Map();
|
|
2805
|
+
for (const problem of previous.issues) {
|
|
2806
|
+
const key = `${problem.code}:${String(problem.rowKey)}`;
|
|
2807
|
+
baseline.set(key, (baseline.get(key) ?? 0) + 1);
|
|
2808
|
+
}
|
|
2809
|
+
problems.push(...candidate.issues.filter((problem) => {
|
|
2810
|
+
const key = `${problem.code}:${String(problem.rowKey)}`;
|
|
2811
|
+
const remaining = baseline.get(key) ?? 0;
|
|
2812
|
+
if (!remaining) return true;
|
|
2813
|
+
baseline.set(key, remaining - 1);
|
|
2814
|
+
return false;
|
|
2815
|
+
}));
|
|
2816
|
+
if (problems.length) {
|
|
2817
|
+
if (!transaction) options.onEditIssues?.(problems);
|
|
2818
|
+
return resultForRows([], problems);
|
|
2819
|
+
}
|
|
2820
|
+
const keys = candidate.rows.filter((row) => !previous.byKey.has(row.key)).map((row) => row.key);
|
|
2821
|
+
if (!keys.length) return reject(rowProblem("No unique rows can be inserted."));
|
|
2822
|
+
data = next;
|
|
2823
|
+
lazyChildren.clear();
|
|
2824
|
+
for (const [key, value] of cache) lazyChildren.set(key, value);
|
|
2825
|
+
lazyVersion++;
|
|
2826
|
+
return publishRows(before, previous, keys, /* @__PURE__ */ new Map(), []);
|
|
2827
|
+
} catch (error) {
|
|
2828
|
+
return reject(rowProblem(error instanceof Error ? error.message : String(error)));
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
function removeRows(keys) {
|
|
2832
|
+
const before = scene();
|
|
2833
|
+
const previous = sourceRows();
|
|
2834
|
+
const problems = [];
|
|
2835
|
+
const removed = /* @__PURE__ */ new Set();
|
|
2836
|
+
const { byId } = resolveColumns(definitions);
|
|
2837
|
+
const model = treeEnabled ? treeStage(data, byId, lazyVersion) : void 0;
|
|
2838
|
+
if (!stableRows(data)) {
|
|
2839
|
+
const problem = rowProblem("Row operations require stable row keys.");
|
|
2840
|
+
if (!transaction) options.onEditIssues?.([problem]);
|
|
2841
|
+
return resultForRows([], [problem]);
|
|
2842
|
+
}
|
|
2843
|
+
for (const key of keys) {
|
|
2844
|
+
if (!previous.byKey.has(key)) {
|
|
2845
|
+
problems.push(issue("unknown_row", "The row to remove does not exist.", { rowKey: key }));
|
|
2846
|
+
continue;
|
|
2847
|
+
}
|
|
2848
|
+
const node = model?.byKey.get(key);
|
|
2849
|
+
if (node && !node.writable) {
|
|
2850
|
+
problems.push(rowProblem("A recovered row has no writable source path.", key));
|
|
2851
|
+
continue;
|
|
2852
|
+
}
|
|
2853
|
+
const stack = node ? [node] : [];
|
|
2854
|
+
removed.add(key);
|
|
2855
|
+
while (stack.length) {
|
|
2856
|
+
const item = stack.pop();
|
|
2857
|
+
removed.add(item.row.key);
|
|
2858
|
+
for (const child of item.children) stack.push(child);
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
if (!removed.size) {
|
|
2862
|
+
if (problems.length && !transaction) options.onEditIssues?.(problems);
|
|
2863
|
+
return resultForRows([], problems);
|
|
2864
|
+
}
|
|
2865
|
+
const positions = new Map([...removed].map((key) => [key, positionOf(key)]));
|
|
2866
|
+
const cache = new Map(lazyChildren);
|
|
2867
|
+
try {
|
|
2868
|
+
const baselines = removalBaselines();
|
|
2869
|
+
const next = options.getChildren && model ? rewriteNested(model, removed, /* @__PURE__ */ new Map(), cache) : data.filter((row, index) => !removed.has(readKey(row, index)));
|
|
2870
|
+
for (const [key, children] of cache) if (removed.has(key)) cache.delete(key);
|
|
2871
|
+
else cache.set(key, children.filter((row, index) => !removed.has(readKey(row, index))));
|
|
2872
|
+
data = next;
|
|
2873
|
+
lazyChildren.clear();
|
|
2874
|
+
for (const [key, value] of cache) lazyChildren.set(key, value);
|
|
2875
|
+
lazyVersion++;
|
|
2876
|
+
validation.cancel([...removed]);
|
|
2877
|
+
abortLoads();
|
|
2878
|
+
return publishRows(before, previous, [...removed], positions, problems, baselines);
|
|
2879
|
+
} catch (error) {
|
|
2880
|
+
problems.push(rowProblem(error instanceof Error ? error.message : String(error)));
|
|
2881
|
+
if (!transaction) options.onEditIssues?.(problems);
|
|
2882
|
+
return resultForRows([], problems);
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
function revert(keys) {
|
|
2886
|
+
const selected = new Set(keys ?? pendingChanges.entries.keys());
|
|
2887
|
+
const entries = [...pendingChanges.entries].filter(([key]) => selected.has(key)).map(([key, entry]) => [key, {
|
|
2888
|
+
...entry,
|
|
2889
|
+
cells: new Map(entry.cells)
|
|
2890
|
+
}]);
|
|
2891
|
+
if (!entries.length) return resultForRows([], []);
|
|
2892
|
+
const before = scene();
|
|
2893
|
+
const previous = sourceRows();
|
|
2894
|
+
const positions = new Map([...selected].map((key) => [key, positionOf(key)]));
|
|
2895
|
+
const problems = [];
|
|
2896
|
+
const cells = [];
|
|
2897
|
+
transaction = true;
|
|
2898
|
+
try {
|
|
2899
|
+
problems.push(...removeRows(entries.filter(([, entry]) => entry.previous === void 0).map(([key]) => key)).issues);
|
|
2900
|
+
const missing = entries.filter(([, entry]) => entry.previous !== void 0 && entry.row === void 0);
|
|
2901
|
+
for (let pass = 0; missing.length && pass <= entries.length; pass++) {
|
|
2902
|
+
let progress = false;
|
|
2903
|
+
for (let index = missing.length - 1; index >= 0; index--) {
|
|
2904
|
+
const [key, entry] = missing[index];
|
|
2905
|
+
if (sourceRows().byKey.has(key)) {
|
|
2906
|
+
missing.splice(index, 1);
|
|
2907
|
+
progress = true;
|
|
2908
|
+
continue;
|
|
2909
|
+
}
|
|
2910
|
+
if (entry.position.parent !== void 0 && !sourceRows().byKey.has(entry.position.parent)) continue;
|
|
2911
|
+
const at = {
|
|
2912
|
+
...entry.position,
|
|
2913
|
+
before: entry.position.before !== void 0 && sourceRows().byKey.has(entry.position.before) ? entry.position.before : void 0
|
|
2914
|
+
};
|
|
2915
|
+
const result = insertRows([entry.previous], at);
|
|
2916
|
+
problems.push(...result.issues);
|
|
2917
|
+
missing.splice(index, 1);
|
|
2918
|
+
progress = true;
|
|
2919
|
+
}
|
|
2920
|
+
if (!progress) break;
|
|
2921
|
+
}
|
|
2922
|
+
for (const [key] of missing) problems.push(rowProblem("The saved parent is missing; restore it before this child.", key));
|
|
2923
|
+
const result = applyChanges(entries.flatMap(([key, entry]) => entry.previous !== void 0 && sourceRows().byKey.has(key) ? [...entry.cells.values()].map((change) => ({
|
|
2924
|
+
rowKey: key,
|
|
2925
|
+
column: change.column,
|
|
2926
|
+
value: change.previous
|
|
2927
|
+
})) : []), false);
|
|
2928
|
+
cells.push(...result.changes);
|
|
2929
|
+
problems.push(...result.issues);
|
|
2930
|
+
for (const [key] of entries) if (!problems.some((problem) => problem.rowKey === key)) pendingChanges.entries.delete(key);
|
|
2931
|
+
validation.cancel([...selected]);
|
|
2932
|
+
} finally {
|
|
2933
|
+
transaction = false;
|
|
2934
|
+
}
|
|
2935
|
+
const after = scene();
|
|
2936
|
+
if (before.data !== after.data || cells.length) undoStack.record({
|
|
2937
|
+
kind: "rows",
|
|
2938
|
+
before,
|
|
2939
|
+
after,
|
|
2940
|
+
keys: [...selected]
|
|
979
2941
|
});
|
|
2942
|
+
const next = sourceRows();
|
|
2943
|
+
const rowChanges = [...selected].flatMap((key) => {
|
|
2944
|
+
const old = previous.byKey.get(key);
|
|
2945
|
+
const row = next.byKey.get(key);
|
|
2946
|
+
return !old && row ? [{
|
|
2947
|
+
kind: "inserted",
|
|
2948
|
+
rowKey: key,
|
|
2949
|
+
row: row.original,
|
|
2950
|
+
position: positionOf(key)
|
|
2951
|
+
}] : old && !row ? [{
|
|
2952
|
+
kind: "removed",
|
|
2953
|
+
rowKey: key,
|
|
2954
|
+
row: old.original,
|
|
2955
|
+
position: positions.get(key)
|
|
2956
|
+
}] : [];
|
|
2957
|
+
});
|
|
2958
|
+
if (before.data !== after.data) options.onDataChange?.(data, cells);
|
|
2959
|
+
if (problems.length) options.onEditIssues?.(problems);
|
|
2960
|
+
notify();
|
|
2961
|
+
return resultForRows(rowChanges, problems, cells);
|
|
980
2962
|
}
|
|
981
2963
|
function rangeEdits(origin, matrix) {
|
|
982
2964
|
const { rows, columns } = getSnapshot();
|
|
@@ -996,13 +2978,356 @@ function createTable(options) {
|
|
|
996
2978
|
return edits;
|
|
997
2979
|
}
|
|
998
2980
|
function replay(changes, direction) {
|
|
999
|
-
applyChanges(changes.map((change) => ({
|
|
2981
|
+
applyChanges((direction === "undo" ? changes.toReversed() : changes).map((change) => ({
|
|
1000
2982
|
rowKey: change.rowKey,
|
|
1001
2983
|
column: change.column,
|
|
1002
2984
|
value: direction === "undo" ? change.previous : change.value
|
|
1003
2985
|
})), false);
|
|
1004
2986
|
}
|
|
2987
|
+
function ingestionResult(appended, replaced, removed, problems) {
|
|
2988
|
+
return Object.freeze({
|
|
2989
|
+
status: appended + replaced + removed ? problems.length ? "partial" : "applied" : problems.length ? "rejected" : "unchanged",
|
|
2990
|
+
appended,
|
|
2991
|
+
replaced,
|
|
2992
|
+
removed,
|
|
2993
|
+
issues: Object.freeze([...problems])
|
|
2994
|
+
});
|
|
2995
|
+
}
|
|
2996
|
+
const stableSource = /* @__PURE__ */ new WeakMap();
|
|
2997
|
+
function ingestionKeys(rows) {
|
|
2998
|
+
let valid = stableSource.get(rows);
|
|
2999
|
+
if (valid === void 0) {
|
|
3000
|
+
valid = stableRows(rows);
|
|
3001
|
+
stableSource.set(rows, valid);
|
|
3002
|
+
}
|
|
3003
|
+
return valid;
|
|
3004
|
+
}
|
|
3005
|
+
function isProtectedSource(key) {
|
|
3006
|
+
return pendingChanges.entries.has(key) || validation.hasPending(key);
|
|
3007
|
+
}
|
|
3008
|
+
function affectedBranch(model, key) {
|
|
3009
|
+
const result = [key];
|
|
3010
|
+
const node = model?.byKey.get(key);
|
|
3011
|
+
if (!node) return result;
|
|
3012
|
+
let parent = node.parent;
|
|
3013
|
+
while (parent) {
|
|
3014
|
+
result.push(parent.row.key);
|
|
3015
|
+
parent = parent.parent;
|
|
3016
|
+
}
|
|
3017
|
+
const stack = [...node.children];
|
|
3018
|
+
while (stack.length) {
|
|
3019
|
+
const child = stack.pop();
|
|
3020
|
+
result.push(child.row.key);
|
|
3021
|
+
for (const descendant of child.children) stack.push(descendant);
|
|
3022
|
+
}
|
|
3023
|
+
return result;
|
|
3024
|
+
}
|
|
3025
|
+
function transformScene(source, updates, removed) {
|
|
3026
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3027
|
+
const replace = (rows) => rows.flatMap((row, index) => {
|
|
3028
|
+
const key = readKey(row, index);
|
|
3029
|
+
seen.add(key);
|
|
3030
|
+
return removed.has(key) ? [] : [updates.get(key) ?? row];
|
|
3031
|
+
});
|
|
3032
|
+
const cache = /* @__PURE__ */ new Map();
|
|
3033
|
+
for (const [key, children] of source.lazy) if (!removed.has(key)) cache.set(key, replace(children));
|
|
3034
|
+
let next;
|
|
3035
|
+
if (!options.getChildren) next = replace(source.data);
|
|
3036
|
+
else {
|
|
3037
|
+
const model = buildTree(source.data, resolveColumns(definitions).byId, readKey, options, source.lazy);
|
|
3038
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
3039
|
+
for (const node of model.nodes.toReversed()) {
|
|
3040
|
+
const key = node.row.key;
|
|
3041
|
+
seen.add(key);
|
|
3042
|
+
if (removed.has(key)) continue;
|
|
3043
|
+
let row = updates.get(key) ?? node.row.original;
|
|
3044
|
+
const supplied = updates.has(key) ? options.getChildren(row) : void 0;
|
|
3045
|
+
if (supplied !== void 0) cache.delete(key);
|
|
3046
|
+
const children = supplied ?? node.children.filter((child) => !removed.has(child.row.key)).map((child) => replacements.get(child) ?? child.row.original);
|
|
3047
|
+
const old = options.getChildren(row) ?? node.children.map((child) => child.row.original);
|
|
3048
|
+
if (supplied === void 0 && (updates.has(key) && node.children.length > 0 && !cache.has(key) || children.length !== old.length || children.some((child, index) => child !== old[index]))) {
|
|
3049
|
+
if (!options.setChildren) throw new Error("Updating nested source children requires setChildren.");
|
|
3050
|
+
row = options.setChildren(row, children);
|
|
3051
|
+
if (cache.has(key)) cache.set(key, children);
|
|
3052
|
+
}
|
|
3053
|
+
replacements.set(node, row);
|
|
3054
|
+
}
|
|
3055
|
+
const roots = new Map(model.roots.filter((node) => node.writable).map((node) => [node.sourceIndex, node]));
|
|
3056
|
+
next = source.data.flatMap((row, index) => {
|
|
3057
|
+
const node = roots.get(index);
|
|
3058
|
+
return node && removed.has(node.row.key) ? [] : [node ? replacements.get(node) ?? row : row];
|
|
3059
|
+
});
|
|
3060
|
+
}
|
|
3061
|
+
const additions = [...updates].filter(([key]) => !seen.has(key) && !removed.has(key)).map(([, row]) => row);
|
|
3062
|
+
return {
|
|
3063
|
+
data: appendList(next, additions),
|
|
3064
|
+
lazy: cache
|
|
3065
|
+
};
|
|
3066
|
+
}
|
|
3067
|
+
function rebaseBatches(transform, removed) {
|
|
3068
|
+
undoStack.transform((entry) => {
|
|
3069
|
+
if (entry.kind === "cells") {
|
|
3070
|
+
if (!removed.size) return entry;
|
|
3071
|
+
const changes = entry.changes.filter((change) => !removed.has(change.rowKey));
|
|
3072
|
+
return changes.length ? {
|
|
3073
|
+
...entry,
|
|
3074
|
+
changes
|
|
3075
|
+
} : void 0;
|
|
3076
|
+
}
|
|
3077
|
+
const keys = entry.keys.filter((key) => !removed.has(key));
|
|
3078
|
+
return keys.length ? {
|
|
3079
|
+
...entry,
|
|
3080
|
+
keys,
|
|
3081
|
+
before: transform(entry.before),
|
|
3082
|
+
after: transform(entry.after)
|
|
3083
|
+
} : void 0;
|
|
3084
|
+
});
|
|
3085
|
+
}
|
|
3086
|
+
function newTreeIssues(previous, next) {
|
|
3087
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3088
|
+
for (const problem of previous) {
|
|
3089
|
+
const key = JSON.stringify([problem.code, problem.rowKey]);
|
|
3090
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
3091
|
+
}
|
|
3092
|
+
return next.filter((problem) => {
|
|
3093
|
+
const key = JSON.stringify([problem.code, problem.rowKey]);
|
|
3094
|
+
const remaining = counts.get(key) ?? 0;
|
|
3095
|
+
if (!remaining) return true;
|
|
3096
|
+
counts.set(key, remaining - 1);
|
|
3097
|
+
return false;
|
|
3098
|
+
});
|
|
3099
|
+
}
|
|
3100
|
+
function ingestData(incoming, mode) {
|
|
3101
|
+
const problems = [];
|
|
3102
|
+
const reject = (problem) => {
|
|
3103
|
+
ingestionIssues = [problem];
|
|
3104
|
+
notify();
|
|
3105
|
+
return ingestionResult(0, 0, 0, ingestionIssues);
|
|
3106
|
+
};
|
|
3107
|
+
if (destroyed) return reject(issue("invalid_ingestion", "A disposed table cannot receive source data."));
|
|
3108
|
+
if (!incoming.length) return ingestionResult(0, 0, 0, []);
|
|
3109
|
+
try {
|
|
3110
|
+
if (!ingestionKeys(data) || !stableRows(incoming)) return reject(issue("invalid_ingestion", "Source ingestion requires stable rowKey values or ids on every row."));
|
|
3111
|
+
} catch (error) {
|
|
3112
|
+
return reject(issue("invalid_ingestion", error instanceof Error ? error.message : String(error)));
|
|
3113
|
+
}
|
|
3114
|
+
const previous = sourceRows();
|
|
3115
|
+
const { byId, columns } = resolveColumns(definitions);
|
|
3116
|
+
const model = treeEnabled ? treeStage(data, byId, lazyVersion) : void 0;
|
|
3117
|
+
const seenBatch = /* @__PURE__ */ new Set();
|
|
3118
|
+
const updates = /* @__PURE__ */ new Map();
|
|
3119
|
+
for (const row of incoming) try {
|
|
3120
|
+
const key = readKey(row, previous.rows.length + updates.size);
|
|
3121
|
+
if (typeof key !== "string" && (typeof key !== "number" || !Number.isFinite(key))) {
|
|
3122
|
+
problems.push(issue("invalid_ingestion", "An incoming row key must be a string or finite number."));
|
|
3123
|
+
continue;
|
|
3124
|
+
}
|
|
3125
|
+
const existing = previous.byKey.get(key);
|
|
3126
|
+
if (seenBatch.has(key) || mode === "append" && existing) {
|
|
3127
|
+
problems.push(issue("duplicate_row_key", "An incoming source row repeats an existing key and is refused.", { rowKey: key }));
|
|
3128
|
+
continue;
|
|
3129
|
+
}
|
|
3130
|
+
seenBatch.add(key);
|
|
3131
|
+
const parent = options.getParentKey?.(row);
|
|
3132
|
+
const affected = affectedBranch(model, key);
|
|
3133
|
+
if (parent !== void 0 && parent !== null) affected.push(...affectedBranch(model, parent));
|
|
3134
|
+
if (affected.some(isProtectedSource)) {
|
|
3135
|
+
problems.push(issue("ingestion_conflict", "Incoming source data would overwrite a local change or pending validation; retry after save or revert.", { rowKey: key }));
|
|
3136
|
+
continue;
|
|
3137
|
+
}
|
|
3138
|
+
const node = model?.byKey.get(key);
|
|
3139
|
+
if (node && (!node.writable || node.lazy && options.getParentKey && options.getParentKey(row) !== node.parent?.row.key)) {
|
|
3140
|
+
problems.push(issue("invalid_ingestion", "This recovered or lazy tree row has no writable source position.", { rowKey: key }));
|
|
3141
|
+
continue;
|
|
3142
|
+
}
|
|
3143
|
+
if (existing?.original !== row) updates.set(key, row);
|
|
3144
|
+
} catch (error) {
|
|
3145
|
+
problems.push(issue("invalid_ingestion", error instanceof Error ? error.message : String(error)));
|
|
3146
|
+
}
|
|
3147
|
+
if (!updates.size) {
|
|
3148
|
+
ingestionIssues = problems;
|
|
3149
|
+
notify();
|
|
3150
|
+
return ingestionResult(0, 0, 0, problems);
|
|
3151
|
+
}
|
|
3152
|
+
const replaced = [...updates.keys()].filter((key) => previous.byKey.has(key)).length;
|
|
3153
|
+
const appended = updates.size - replaced;
|
|
3154
|
+
const previousState = state;
|
|
3155
|
+
try {
|
|
3156
|
+
if (!model && !replaced) {
|
|
3157
|
+
const added = Object.freeze([...updates.values()]);
|
|
3158
|
+
const indexed = added.map((row, offset) => createRow(row, readKey(row, previous.rows.length + offset), previous.rows.length + offset, byId, options.getRowCanExpand?.(row) ?? false));
|
|
3159
|
+
const filtered = manual ? previous.rows : filterStage(previous.rows, columns, state.search, state.filters);
|
|
3160
|
+
const addedFiltered = manual ? indexed : filterRows(indexed, columns, state.search, state.filters);
|
|
3161
|
+
const allRows = appendList(previous.rows, indexed);
|
|
3162
|
+
const allFiltered = appendList(filtered, addedFiltered);
|
|
3163
|
+
const keys = membership(filtered);
|
|
3164
|
+
for (const row of addedFiltered) keys.add(row.key);
|
|
3165
|
+
rowMembership.set(allFiltered, keys);
|
|
3166
|
+
const sorted = manual || !state.sorting.some((rule) => columns.some((column) => column.id === rule.column)) ? allFiltered : mergeRows(sortStage(filtered, columns, state.sorting), addedFiltered, columns, state.sorting);
|
|
3167
|
+
const next = appendList(data, added);
|
|
3168
|
+
rebaseBatches((source) => ({
|
|
3169
|
+
...source,
|
|
3170
|
+
data: appendList(source.data, added)
|
|
3171
|
+
}), /* @__PURE__ */ new Set());
|
|
3172
|
+
for (const row of indexed) previous.byKey.set(row.key, row);
|
|
3173
|
+
primedIndex = {
|
|
3174
|
+
data: next,
|
|
3175
|
+
columns: byId,
|
|
3176
|
+
result: {
|
|
3177
|
+
rows: allRows,
|
|
3178
|
+
byKey: previous.byKey,
|
|
3179
|
+
issues: previous.issues
|
|
3180
|
+
}
|
|
3181
|
+
};
|
|
3182
|
+
primedFilter = {
|
|
3183
|
+
rows: allRows,
|
|
3184
|
+
columns,
|
|
3185
|
+
search: state.search,
|
|
3186
|
+
filters: state.filters,
|
|
3187
|
+
result: allFiltered
|
|
3188
|
+
};
|
|
3189
|
+
primedSort = {
|
|
3190
|
+
rows: allFiltered,
|
|
3191
|
+
columns,
|
|
3192
|
+
sorting: state.sorting,
|
|
3193
|
+
result: sorted
|
|
3194
|
+
};
|
|
3195
|
+
stableSource.set(next, true);
|
|
3196
|
+
data = next;
|
|
3197
|
+
} else {
|
|
3198
|
+
const next = transformScene(scene(), updates, /* @__PURE__ */ new Set());
|
|
3199
|
+
const candidate = treeEnabled ? buildTree(next.data, byId, readKey, options, next.lazy) : indexRows(next.data, byId);
|
|
3200
|
+
const fresh = newTreeIssues(previous.issues, candidate.issues);
|
|
3201
|
+
if (fresh.length || !stableRows(candidate.rows.map((row) => row.original))) {
|
|
3202
|
+
problems.push(...fresh, ...!stableRows(candidate.rows.map((row) => row.original)) ? [issue("invalid_ingestion", "Every incoming descendant needs a stable key.")] : []);
|
|
3203
|
+
ingestionIssues = problems;
|
|
3204
|
+
notify();
|
|
3205
|
+
return ingestionResult(0, 0, 0, problems);
|
|
3206
|
+
}
|
|
3207
|
+
const nextKeys = new Set(candidate.rows.map((row) => row.key));
|
|
3208
|
+
const removedKeys = new Set(previous.rows.filter((row) => !nextKeys.has(row.key)).map((row) => row.key));
|
|
3209
|
+
rebaseBatches((source) => transformScene(source, updates, removedKeys), removedKeys);
|
|
3210
|
+
data = next.data;
|
|
3211
|
+
lazyChildren.clear();
|
|
3212
|
+
for (const [key, rows] of next.lazy) lazyChildren.set(key, rows);
|
|
3213
|
+
lazyVersion++;
|
|
3214
|
+
for (const key of [...updates.keys(), ...removedKeys]) {
|
|
3215
|
+
pendingLoads.get(key)?.abort();
|
|
3216
|
+
pendingLoads.delete(key);
|
|
3217
|
+
childStatuses.delete(key);
|
|
3218
|
+
loadIssues.delete(key);
|
|
3219
|
+
loadValidationIssues.delete(key);
|
|
3220
|
+
}
|
|
3221
|
+
validation.cancel([...removedKeys]);
|
|
3222
|
+
const selection = state.selection.filter((key) => !removedKeys.has(key));
|
|
3223
|
+
state = mergeState(state, {
|
|
3224
|
+
selection: selection.length === state.selection.length ? state.selection : selection,
|
|
3225
|
+
expanded: state.expanded === true ? true : state.expanded.filter((key) => !removedKeys.has(key))
|
|
3226
|
+
});
|
|
3227
|
+
}
|
|
3228
|
+
validation.cancel([...updates.keys()]);
|
|
3229
|
+
ingestionIssues = problems;
|
|
3230
|
+
} catch (error) {
|
|
3231
|
+
return reject(issue("invalid_ingestion", error instanceof Error ? error.message : String(error)));
|
|
3232
|
+
}
|
|
3233
|
+
if (state !== previousState) options.onStateChange?.(state, previousState);
|
|
3234
|
+
options.onDataChange?.(data, []);
|
|
3235
|
+
notify();
|
|
3236
|
+
startVisibleLoads();
|
|
3237
|
+
return ingestionResult(appended, replaced, 0, problems);
|
|
3238
|
+
}
|
|
3239
|
+
function removeData(keys) {
|
|
3240
|
+
const previousState = state;
|
|
3241
|
+
const problems = [];
|
|
3242
|
+
const removed = /* @__PURE__ */ new Set();
|
|
3243
|
+
const previous = sourceRows();
|
|
3244
|
+
const model = treeEnabled ? treeStage(data, resolveColumns(definitions).byId, lazyVersion) : void 0;
|
|
3245
|
+
if (destroyed || !ingestionKeys(data)) problems.push(issue("invalid_ingestion", "Source removal requires a live table and stable row keys."));
|
|
3246
|
+
else for (const key of new Set(keys)) {
|
|
3247
|
+
if (!previous.byKey.has(key)) {
|
|
3248
|
+
problems.push(issue("unknown_row", "The source row to remove does not exist.", { rowKey: key }));
|
|
3249
|
+
continue;
|
|
3250
|
+
}
|
|
3251
|
+
if (affectedBranch(model, key).some(isProtectedSource)) {
|
|
3252
|
+
problems.push(issue("ingestion_conflict", "Source removal would discard local changes or pending validation.", { rowKey: key }));
|
|
3253
|
+
continue;
|
|
3254
|
+
}
|
|
3255
|
+
const node = model?.byKey.get(key);
|
|
3256
|
+
if (node && !node.writable) {
|
|
3257
|
+
problems.push(issue("invalid_ingestion", "A recovered tree row has no writable source position.", { rowKey: key }));
|
|
3258
|
+
continue;
|
|
3259
|
+
}
|
|
3260
|
+
removed.add(key);
|
|
3261
|
+
const stack = node ? [...node.children] : [];
|
|
3262
|
+
while (stack.length) {
|
|
3263
|
+
const child = stack.pop();
|
|
3264
|
+
removed.add(child.row.key);
|
|
3265
|
+
for (const descendant of child.children) stack.push(descendant);
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
try {
|
|
3269
|
+
if (removed.size) {
|
|
3270
|
+
const next = transformScene(scene(), /* @__PURE__ */ new Map(), removed);
|
|
3271
|
+
rebaseBatches((source) => transformScene(source, /* @__PURE__ */ new Map(), removed), removed);
|
|
3272
|
+
data = next.data;
|
|
3273
|
+
lazyChildren.clear();
|
|
3274
|
+
for (const [key, rows] of next.lazy) lazyChildren.set(key, rows);
|
|
3275
|
+
lazyVersion++;
|
|
3276
|
+
validation.cancel([...removed]);
|
|
3277
|
+
for (const key of removed) {
|
|
3278
|
+
pendingLoads.get(key)?.abort();
|
|
3279
|
+
pendingLoads.delete(key);
|
|
3280
|
+
childStatuses.delete(key);
|
|
3281
|
+
loadIssues.delete(key);
|
|
3282
|
+
loadValidationIssues.delete(key);
|
|
3283
|
+
}
|
|
3284
|
+
const selection = state.selection.filter((key) => !removed.has(key));
|
|
3285
|
+
state = mergeState(state, {
|
|
3286
|
+
selection: selection.length === state.selection.length ? state.selection : selection,
|
|
3287
|
+
expanded: state.expanded === true ? true : state.expanded.filter((key) => !removed.has(key))
|
|
3288
|
+
});
|
|
3289
|
+
}
|
|
3290
|
+
ingestionIssues = problems;
|
|
3291
|
+
} catch (error) {
|
|
3292
|
+
ingestionIssues = [issue("invalid_ingestion", error instanceof Error ? error.message : String(error))];
|
|
3293
|
+
notify();
|
|
3294
|
+
return ingestionResult(0, 0, 0, ingestionIssues);
|
|
3295
|
+
}
|
|
3296
|
+
if (state !== previousState) options.onStateChange?.(state, previousState);
|
|
3297
|
+
if (removed.size) options.onDataChange?.(data, []);
|
|
3298
|
+
notify();
|
|
3299
|
+
startVisibleLoads();
|
|
3300
|
+
return ingestionResult(0, 0, removed.size, problems);
|
|
3301
|
+
}
|
|
3302
|
+
if (expandMode === "single") {
|
|
3303
|
+
initialState = mergeState(initialState, { expanded: canonicalExpanded(initialState.expanded) });
|
|
3304
|
+
state = initialState;
|
|
3305
|
+
snapshot = void 0;
|
|
3306
|
+
}
|
|
1005
3307
|
const table = {
|
|
3308
|
+
appendData: (rows) => ingestData(rows, "append"),
|
|
3309
|
+
upsertData: (rows) => ingestData(rows, "upsert"),
|
|
3310
|
+
removeData,
|
|
3311
|
+
stream(source, streamOptions) {
|
|
3312
|
+
if (destroyed) return Promise.resolve(Object.freeze({
|
|
3313
|
+
status: "aborted",
|
|
3314
|
+
receivedRowCount: 0,
|
|
3315
|
+
issues: Object.freeze([issue("invalid_ingestion", "A disposed table cannot start a source.")])
|
|
3316
|
+
}));
|
|
3317
|
+
return streaming.run(source, {
|
|
3318
|
+
expectedRowCount: sourceExpected,
|
|
3319
|
+
...streamOptions
|
|
3320
|
+
});
|
|
3321
|
+
},
|
|
3322
|
+
insertRows,
|
|
3323
|
+
removeRows,
|
|
3324
|
+
revert,
|
|
3325
|
+
getPendingChanges: () => pendingChanges.snapshot(),
|
|
3326
|
+
markSaved(keys) {
|
|
3327
|
+
if (!keys) pendingChanges.entries.clear();
|
|
3328
|
+
else for (const key of keys) pendingChanges.entries.delete(key);
|
|
3329
|
+
notify();
|
|
3330
|
+
},
|
|
1006
3331
|
getSnapshot,
|
|
1007
3332
|
getState: () => state,
|
|
1008
3333
|
subscribe(listener) {
|
|
@@ -1012,21 +3337,40 @@ function createTable(options) {
|
|
|
1012
3337
|
};
|
|
1013
3338
|
},
|
|
1014
3339
|
setState(patch) {
|
|
1015
|
-
const
|
|
1016
|
-
|
|
3340
|
+
const previousIssue = expandedIssue;
|
|
3341
|
+
const next = mergePatch(patch);
|
|
3342
|
+
if (next !== state || previousIssue !== expandedIssue) {
|
|
1017
3343
|
state = next;
|
|
3344
|
+
snapshot = void 0;
|
|
3345
|
+
abortLoads();
|
|
1018
3346
|
notify();
|
|
3347
|
+
startVisibleLoads();
|
|
1019
3348
|
}
|
|
1020
3349
|
},
|
|
1021
3350
|
setData(next) {
|
|
1022
3351
|
if (next === data) return;
|
|
3352
|
+
validation.cancel();
|
|
3353
|
+
streaming.cancel();
|
|
3354
|
+
loadState = "idle";
|
|
3355
|
+
streamIssues = [];
|
|
3356
|
+
ingestionIssues = [];
|
|
3357
|
+
expectedRowCount = sourceExpected;
|
|
3358
|
+
pendingChanges.entries.clear();
|
|
3359
|
+
abortLoads(true);
|
|
3360
|
+
lazyChildren.clear();
|
|
3361
|
+
childStatuses.clear();
|
|
3362
|
+
loadIssues.clear();
|
|
3363
|
+
loadValidationIssues.clear();
|
|
3364
|
+
lazyVersion++;
|
|
1023
3365
|
data = next;
|
|
1024
3366
|
undoStack.clear();
|
|
1025
3367
|
notify();
|
|
3368
|
+
startVisibleLoads();
|
|
1026
3369
|
},
|
|
1027
3370
|
setColumns(next) {
|
|
1028
3371
|
if (next === definitions) return;
|
|
1029
3372
|
definitions = next;
|
|
3373
|
+
validation.cancel();
|
|
1030
3374
|
notify();
|
|
1031
3375
|
},
|
|
1032
3376
|
setRowCount(next) {
|
|
@@ -1034,6 +3378,12 @@ function createTable(options) {
|
|
|
1034
3378
|
rowCount = next;
|
|
1035
3379
|
notify();
|
|
1036
3380
|
},
|
|
3381
|
+
setTreeFilter(next) {
|
|
3382
|
+
const mode = next ?? "ancestors";
|
|
3383
|
+
if (mode === treeFilter) return;
|
|
3384
|
+
treeFilter = mode;
|
|
3385
|
+
notify();
|
|
3386
|
+
},
|
|
1037
3387
|
toggleSort(columnId, { multi = false } = {}) {
|
|
1038
3388
|
const current = state.sorting.find((rule) => rule.column === columnId)?.direction;
|
|
1039
3389
|
setSort(columnId, current === void 0 ? "asc" : current === "asc" ? "desc" : void 0, multi);
|
|
@@ -1069,6 +3419,7 @@ function createTable(options) {
|
|
|
1069
3419
|
});
|
|
1070
3420
|
},
|
|
1071
3421
|
goToPage(page) {
|
|
3422
|
+
if (!paginate) return;
|
|
1072
3423
|
const { pageCount, page: currentPage } = getSnapshot();
|
|
1073
3424
|
const next = clampPage(page, pageCount);
|
|
1074
3425
|
if (next !== currentPage || next !== state.pagination.page) commit({ pagination: {
|
|
@@ -1083,6 +3434,7 @@ function createTable(options) {
|
|
|
1083
3434
|
table.goToPage(getSnapshot().page - 1);
|
|
1084
3435
|
},
|
|
1085
3436
|
setPageSize(pageSize) {
|
|
3437
|
+
if (!paginate) return;
|
|
1086
3438
|
if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize === state.pagination.pageSize) return;
|
|
1087
3439
|
const { pageStart } = getSnapshot();
|
|
1088
3440
|
commit({ pagination: {
|
|
@@ -1091,14 +3443,14 @@ function createTable(options) {
|
|
|
1091
3443
|
} });
|
|
1092
3444
|
},
|
|
1093
3445
|
select(keys) {
|
|
1094
|
-
applySelection([...state.selection, ...keys]);
|
|
3446
|
+
applySelection([...state.selection, ...selectionKeys(keys)]);
|
|
1095
3447
|
},
|
|
1096
3448
|
deselect(keys) {
|
|
1097
|
-
const removed = new Set(keys);
|
|
3449
|
+
const removed = new Set(selectionKeys(keys));
|
|
1098
3450
|
applySelection(state.selection.filter((key) => !removed.has(key)));
|
|
1099
3451
|
},
|
|
1100
3452
|
toggleRow(key, selected) {
|
|
1101
|
-
const isSelected = state.selection.includes(key);
|
|
3453
|
+
const isSelected = treeEnabled ? getSnapshot().getRow(key)?.selection === "all" : state.selection.includes(key);
|
|
1102
3454
|
const next = selected ?? !isSelected;
|
|
1103
3455
|
if (next === isSelected) return;
|
|
1104
3456
|
if (next) table.select([key]);
|
|
@@ -1121,6 +3473,27 @@ function createTable(options) {
|
|
|
1121
3473
|
return row ? [row] : [];
|
|
1122
3474
|
});
|
|
1123
3475
|
},
|
|
3476
|
+
toggleExpanded(key, expanded) {
|
|
3477
|
+
const row = getSnapshot().getRow(key);
|
|
3478
|
+
if (!row?.canExpand) return;
|
|
3479
|
+
const open = expanded ?? !row.isExpanded;
|
|
3480
|
+
if (open === row.isExpanded) {
|
|
3481
|
+
if (open && row.childStatus === "error") requestChildren(row);
|
|
3482
|
+
return;
|
|
3483
|
+
}
|
|
3484
|
+
if (expandMode === "single") commit({ expanded: open ? [key] : [] });
|
|
3485
|
+
else if (state.expanded === true) {
|
|
3486
|
+
const { byId } = resolveColumns(definitions);
|
|
3487
|
+
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) });
|
|
3488
|
+
} else commit({ expanded: open ? [...state.expanded, key] : state.expanded.filter((candidate) => candidate !== key) });
|
|
3489
|
+
if (open) requestChildren(getSnapshot().getRow(key));
|
|
3490
|
+
},
|
|
3491
|
+
expandAll() {
|
|
3492
|
+
commit({ expanded: true });
|
|
3493
|
+
},
|
|
3494
|
+
collapseAll() {
|
|
3495
|
+
commit({ expanded: [] });
|
|
3496
|
+
},
|
|
1124
3497
|
toggleColumn(columnId, visible) {
|
|
1125
3498
|
if (!getSnapshot().getColumn(columnId)) return;
|
|
1126
3499
|
const hidden = state.hiddenColumns.includes(columnId);
|
|
@@ -1138,22 +3511,26 @@ function createTable(options) {
|
|
|
1138
3511
|
order.splice(target, 0, columnId);
|
|
1139
3512
|
commit({ columnOrder: order });
|
|
1140
3513
|
},
|
|
1141
|
-
edit(edits) {
|
|
1142
|
-
return
|
|
3514
|
+
edit(edits, editOptions) {
|
|
3515
|
+
return validation.edit(Array.isArray(edits) ? edits : [edits], [], editOptions);
|
|
1143
3516
|
},
|
|
1144
3517
|
undo() {
|
|
1145
|
-
const
|
|
1146
|
-
if (!
|
|
1147
|
-
|
|
3518
|
+
const entry = undoStack.undo();
|
|
3519
|
+
if (!entry) return false;
|
|
3520
|
+
validation.cancel();
|
|
3521
|
+
if (entry.kind === "cells") replay(entry.changes, "undo");
|
|
3522
|
+
else restoreScene(entry, "undo");
|
|
1148
3523
|
return true;
|
|
1149
3524
|
},
|
|
1150
3525
|
redo() {
|
|
1151
|
-
const
|
|
1152
|
-
if (!
|
|
1153
|
-
|
|
3526
|
+
const entry = undoStack.redo();
|
|
3527
|
+
if (!entry) return false;
|
|
3528
|
+
validation.cancel();
|
|
3529
|
+
if (entry.kind === "cells") replay(entry.changes, "redo");
|
|
3530
|
+
else restoreScene(entry, "redo");
|
|
1154
3531
|
return true;
|
|
1155
3532
|
},
|
|
1156
|
-
copy(range) {
|
|
3533
|
+
copy(range, { escapeFormulas = false } = {}) {
|
|
1157
3534
|
const { rows, columns } = getSnapshot();
|
|
1158
3535
|
const matrix = [];
|
|
1159
3536
|
rows.forEach((row, rowIndex) => {
|
|
@@ -1162,46 +3539,72 @@ function createTable(options) {
|
|
|
1162
3539
|
if (rangeContains(range, {
|
|
1163
3540
|
row: rowIndex,
|
|
1164
3541
|
column: columnIndex
|
|
1165
|
-
}))
|
|
3542
|
+
})) {
|
|
3543
|
+
const display = row.getDisplay(column.id);
|
|
3544
|
+
line.push(escapeFormulas ? escapeFormula(display, row.getValue(column.id)) : display);
|
|
3545
|
+
}
|
|
1166
3546
|
});
|
|
1167
3547
|
if (line.length > 0) matrix.push(line);
|
|
1168
3548
|
});
|
|
1169
3549
|
return toDelimited(matrix);
|
|
1170
3550
|
},
|
|
1171
3551
|
paste(origin, text) {
|
|
1172
|
-
|
|
3552
|
+
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.")]);
|
|
3553
|
+
const { rows, columns } = getSnapshot();
|
|
3554
|
+
const parsed = readDelimited(text, {
|
|
3555
|
+
maxRows: Math.max(0, rows.length - origin.row),
|
|
3556
|
+
maxColumns: Math.max(0, columns.length - origin.column),
|
|
3557
|
+
maxCells: maxPasteCells,
|
|
3558
|
+
maxLength: maxPasteLength
|
|
3559
|
+
});
|
|
3560
|
+
const issues = parsed.truncated ? [issue("paste_truncated", "The paste exceeds the visible grid or parsing limits; only complete cells within those limits are applied.")] : [];
|
|
3561
|
+
return validation.edit(rangeEdits(origin, parsed.rows), issues);
|
|
1173
3562
|
},
|
|
1174
3563
|
clear(range) {
|
|
1175
3564
|
const matrix = Array.from({ length: range.bottom - range.top + 1 }, () => Array.from({ length: range.right - range.left + 1 }, () => ""));
|
|
1176
3565
|
const current = getSnapshot();
|
|
1177
|
-
|
|
3566
|
+
const edits = rangeEdits({
|
|
1178
3567
|
row: range.top,
|
|
1179
3568
|
column: range.left
|
|
1180
3569
|
}, matrix).filter((edit) => {
|
|
1181
3570
|
const row = current.getRow(edit.rowKey);
|
|
1182
3571
|
return row !== void 0 && current.getColumn(edit.column)?.isEditable(row.original) === true;
|
|
1183
|
-
})
|
|
3572
|
+
});
|
|
3573
|
+
return validation.edit(edits);
|
|
1184
3574
|
},
|
|
1185
|
-
exportRows({ format = "csv", columns: ids, pageOnly = false, headers = true } = {}) {
|
|
3575
|
+
exportRows({ format = "csv", columns: ids, pageOnly = false, headers = true, escapeFormulas = true, depth = false, indent = "" } = {}) {
|
|
1186
3576
|
const current = getSnapshot();
|
|
1187
3577
|
const columns = ids ? ids.flatMap((id) => {
|
|
1188
3578
|
const column = current.getColumn(id);
|
|
1189
3579
|
return column ? [column] : [];
|
|
1190
3580
|
}) : current.columns;
|
|
1191
|
-
const
|
|
1192
|
-
|
|
3581
|
+
const rows = pageOnly ? current.rows : current.processedRows;
|
|
3582
|
+
const matrix = rows.map((row) => columns.map((column, index) => {
|
|
3583
|
+
const display = (index === 0 ? indent.repeat(row.depth) : "") + row.getDisplay(column.id);
|
|
3584
|
+
return escapeFormulas ? escapeFormula(display, row.getValue(column.id)) : display;
|
|
3585
|
+
}));
|
|
3586
|
+
if (depth) matrix.forEach((line, index) => line.unshift(String(rows[index].depth)));
|
|
3587
|
+
if (headers) {
|
|
3588
|
+
matrix.unshift(columns.map((column) => escapeFormulas ? escapeFormula(column.header) : column.header));
|
|
3589
|
+
if (depth) matrix[0].unshift("Depth");
|
|
3590
|
+
}
|
|
1193
3591
|
return toDelimited(matrix, format === "csv" ? "," : " ");
|
|
1194
3592
|
},
|
|
1195
3593
|
reset() {
|
|
1196
3594
|
commit(initialState);
|
|
1197
3595
|
},
|
|
1198
3596
|
destroy() {
|
|
3597
|
+
destroyed = true;
|
|
3598
|
+
streaming.cancel();
|
|
3599
|
+
validation.cancel();
|
|
3600
|
+
abortLoads(true);
|
|
1199
3601
|
listeners.clear();
|
|
1200
3602
|
}
|
|
1201
3603
|
};
|
|
3604
|
+
startVisibleLoads();
|
|
1202
3605
|
return table;
|
|
1203
3606
|
}
|
|
1204
3607
|
|
|
1205
3608
|
//#endregion
|
|
1206
|
-
export { DEFAULT_PAGE_SIZE, clampPage, clampPosition, columnLabel, compareValues, countPages, createTable, createUndoStack, defineColumns, filterRows, formatValue, fromA1, getPath, gridCommand, humanize, inferColumns, matchesFilter, moveSelection, paginateRows, paginationItems, parseAs, parseDelimited, rangeContains, resolveColumn, selectCell, selectionRange, setPath, sortRows, toA1, toDelimited, typeOf };
|
|
3609
|
+
export { DEFAULT_PAGE_SIZE, clampPage, clampPosition, columnLabel, compareValues, countPages, createTable, createUndoStack, createVirtualizer, defineColumns, filterRows, formatValue, fromA1, getPath, getRowItemKey, gridCommand, humanize, inferColumns, matchesFilter, moveSelection, paginateRows, paginationItems, parseAs, parseDelimited, rangeContains, resolveColumn, selectCell, selectionRange, setPath, sortRows, toA1, toDelimited, typeOf };
|
|
1207
3610
|
//# sourceMappingURL=index.js.map
|