@vueye-table/core 3.0.0-alpha.1 → 3.0.0-alpha.10
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 +304 -69
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2580 -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,1050 @@ 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
|
+
const treeEnabled = Boolean(options.getChildren || options.getParentKey || options.hasChildren || options.loadChildren);
|
|
1814
|
+
const rootPagination = treeEnabled && (manual || (options.paginateBy ?? "root") === "root");
|
|
1815
|
+
const projectedRows = /* @__PURE__ */ new WeakMap();
|
|
1816
|
+
function shareRow(base, next) {
|
|
1817
|
+
const previous = projectedRows.get(base);
|
|
1818
|
+
if (previous && Object.keys(next).every((key) => previous[key] === next[key])) return previous;
|
|
1819
|
+
projectedRows.set(base, next);
|
|
1820
|
+
return next;
|
|
1821
|
+
}
|
|
1822
|
+
let lazyVersion = 0;
|
|
1823
|
+
const lazyChildren = /* @__PURE__ */ new Map();
|
|
1824
|
+
const childStatuses = /* @__PURE__ */ new Map();
|
|
1825
|
+
const loadIssues = /* @__PURE__ */ new Map();
|
|
1826
|
+
const loadValidationIssues = /* @__PURE__ */ new Map();
|
|
1827
|
+
const pendingLoads = /* @__PURE__ */ new Map();
|
|
1828
|
+
let destroyed = false;
|
|
1829
|
+
const undoStack = createUndoStack(options.historyLimit);
|
|
1830
|
+
const pendingChanges = createPendingChanges();
|
|
1831
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1832
|
+
const optionIssues = [];
|
|
1833
|
+
const maxPasteCells = pasteLimit(options.pasteLimit?.maxCells, 1e5, "maxCells", optionIssues);
|
|
1834
|
+
const maxPasteLength = pasteLimit(options.pasteLimit?.maxLength, 5e6, "maxLength", optionIssues);
|
|
1835
|
+
const sourceExpected = options.expectedRowCount === void 0 || Number.isSafeInteger(options.expectedRowCount) && options.expectedRowCount >= 0 ? options.expectedRowCount : void 0;
|
|
1836
|
+
if (sourceExpected !== options.expectedRowCount) optionIssues.push(issue("invalid_stream_option", "expectedRowCount must be a non-negative safe integer; it is unknown instead."));
|
|
1837
|
+
let data = options.data;
|
|
1838
|
+
let definitions = options.columns;
|
|
1839
|
+
let rowCount = options.rowCount;
|
|
1840
|
+
let expandedIssue;
|
|
1841
|
+
let initialState = createInitialState({
|
|
1842
|
+
...options.initialState,
|
|
1843
|
+
hiddenColumns: options.initialState?.hiddenColumns ?? options.columns.filter((column) => column.hidden === true).map((column) => column.id)
|
|
1844
|
+
}, (problem) => {
|
|
1845
|
+
expandedIssue = problem;
|
|
1846
|
+
});
|
|
1847
|
+
let state = initialState;
|
|
1848
|
+
let snapshot;
|
|
1849
|
+
let loadState = "idle";
|
|
1850
|
+
let expectedRowCount = sourceExpected;
|
|
1851
|
+
let ingestionIssues = [];
|
|
1852
|
+
let streamIssues = [];
|
|
1853
|
+
const streaming = createStream({
|
|
1854
|
+
ingest: (rows, mode) => ingestData(rows, mode),
|
|
1855
|
+
update(next, expected, problems, publish) {
|
|
1856
|
+
loadState = next;
|
|
1857
|
+
expectedRowCount = expected;
|
|
1858
|
+
streamIssues = [...problems];
|
|
1859
|
+
if (publish) notify();
|
|
1860
|
+
}
|
|
1861
|
+
});
|
|
1862
|
+
let transaction = false;
|
|
1863
|
+
let deferValidationNotify = false;
|
|
1864
|
+
let validationNotifyRequested = false;
|
|
1865
|
+
const validation = createEditValidation({
|
|
1866
|
+
snapshot: getSnapshot,
|
|
1867
|
+
apply: applyChanges,
|
|
1868
|
+
notify,
|
|
1869
|
+
writeIssue(row, next) {
|
|
1870
|
+
const node = treeEnabled ? treeStage(data, resolveColumns(definitions).byId, lazyVersion).byKey.get(row.key) : void 0;
|
|
1871
|
+
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 });
|
|
1872
|
+
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 });
|
|
1873
|
+
},
|
|
1874
|
+
batch(run) {
|
|
1875
|
+
deferValidationNotify = true;
|
|
1876
|
+
validationNotifyRequested = false;
|
|
1877
|
+
try {
|
|
1878
|
+
return run();
|
|
1879
|
+
} finally {
|
|
1880
|
+
deferValidationNotify = false;
|
|
1881
|
+
if (validationNotifyRequested) notify();
|
|
1882
|
+
}
|
|
1883
|
+
},
|
|
1884
|
+
record: (changes) => undoStack.record({
|
|
1885
|
+
kind: "cells",
|
|
1886
|
+
changes
|
|
1887
|
+
}),
|
|
1888
|
+
onIssues: (issues) => options.onEditIssues?.(issues),
|
|
1889
|
+
optimistic: options.asyncValidation === "optimistic",
|
|
1890
|
+
validateRow: options.validateRow
|
|
1891
|
+
});
|
|
1892
|
+
const readKey = (row, index) => {
|
|
1893
|
+
const { rowKey } = options;
|
|
1894
|
+
if (typeof rowKey === "function") return rowKey(row, index);
|
|
1895
|
+
const value = getPath(row, rowKey ?? "id");
|
|
1896
|
+
if (typeof value === "string" || typeof value === "number") return value;
|
|
1897
|
+
return rowKey === void 0 || !isRecord(row) ? index : String(value);
|
|
1898
|
+
};
|
|
1899
|
+
const resolveColumns = memo((defs) => {
|
|
1900
|
+
const issues = [];
|
|
1901
|
+
const columns = defs.flatMap((definition) => {
|
|
1902
|
+
if (!isSafePath(definition.id)) {
|
|
1903
|
+
issues.push(issue("unsafe_path", `The column "${definition.id}" addresses a prototype and is ignored.`, { column: definition.id }));
|
|
1904
|
+
return [];
|
|
1905
|
+
}
|
|
1906
|
+
return [resolveColumn(definition)];
|
|
1907
|
+
});
|
|
1908
|
+
return {
|
|
1909
|
+
columns,
|
|
1910
|
+
byId: new Map(columns.map((column) => [column.id, column])),
|
|
1911
|
+
issues
|
|
1912
|
+
};
|
|
1913
|
+
});
|
|
1914
|
+
let primedIndex;
|
|
1915
|
+
let reusableRows;
|
|
1916
|
+
const indexRows = memo((rows, byId) => {
|
|
1917
|
+
if (primedIndex?.data === rows && primedIndex.columns === byId) return primedIndex.result;
|
|
1918
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1919
|
+
const issues = [];
|
|
1920
|
+
const indexed = rows.map((original, index) => {
|
|
1921
|
+
let key = readKey(original, index);
|
|
1922
|
+
if (byKey.has(key)) {
|
|
1923
|
+
issues.push(issue("duplicate_row_key", `Row ${index} repeats the key "${String(key)}"; it is keyed "${String(key)}#${index}" instead.`, { rowKey: key }));
|
|
1924
|
+
key = `${String(key)}#${index}`;
|
|
1925
|
+
}
|
|
1926
|
+
const previous = reusableRows?.columns === byId ? reusableRows.rows.get(key) : void 0;
|
|
1927
|
+
const row = previous?.original === original && previous.index === index ? previous : createRow(original, key, index, byId, options.getRowCanExpand?.(original) ?? false);
|
|
1928
|
+
byKey.set(key, row);
|
|
1929
|
+
return row;
|
|
1930
|
+
});
|
|
1931
|
+
stableSource.set(rows, stableRows(rows));
|
|
1932
|
+
reusableRows = {
|
|
1933
|
+
columns: byId,
|
|
1934
|
+
rows: byKey
|
|
1935
|
+
};
|
|
1936
|
+
return {
|
|
1937
|
+
rows: indexed,
|
|
744
1938
|
byKey,
|
|
745
1939
|
issues
|
|
746
1940
|
};
|
|
747
1941
|
});
|
|
1942
|
+
const treeStage = memo((rows, byId, _version) => buildTree(rows, byId, readKey, options, lazyChildren));
|
|
1943
|
+
const treeIndex = memo((model) => ({
|
|
1944
|
+
rows: model.rows,
|
|
1945
|
+
byKey: new Map(model.rows.map((row) => [row.key, row])),
|
|
1946
|
+
issues: model.issues
|
|
1947
|
+
}));
|
|
1948
|
+
const treePositions = memo((model) => {
|
|
1949
|
+
const result = /* @__PURE__ */ new Map();
|
|
1950
|
+
const index = (siblings, parent) => {
|
|
1951
|
+
siblings.forEach((node, position) => result.set(node.row.key, Object.freeze({
|
|
1952
|
+
parent,
|
|
1953
|
+
before: siblings[position + 1]?.row.key
|
|
1954
|
+
})));
|
|
1955
|
+
};
|
|
1956
|
+
index(model.roots);
|
|
1957
|
+
for (const node of model.nodes) index(node.children, node.row.key);
|
|
1958
|
+
return result;
|
|
1959
|
+
});
|
|
1960
|
+
const processedTreeStage = memo((model, columns, search, filters, sorting) => processTree(model, columns, {
|
|
1961
|
+
...state,
|
|
1962
|
+
search,
|
|
1963
|
+
filters,
|
|
1964
|
+
sorting
|
|
1965
|
+
}, options.treeFilter ?? "ancestors", manual));
|
|
1966
|
+
const flattenTree = createTreeFlattener();
|
|
1967
|
+
const emptyAncestors = /* @__PURE__ */ new Set();
|
|
1968
|
+
const singleTreePath = memo((model, expanded) => {
|
|
1969
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
1970
|
+
if (expanded !== true) for (const key of expanded) {
|
|
1971
|
+
let parent = model.byKey.get(key)?.parent;
|
|
1972
|
+
while (parent && !ancestors.has(parent.row.key)) {
|
|
1973
|
+
ancestors.add(parent.row.key);
|
|
1974
|
+
parent = parent.parent;
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
return ancestors;
|
|
1978
|
+
});
|
|
1979
|
+
const treeSelection = memo((model, selection) => {
|
|
1980
|
+
const selected = new Set(selection);
|
|
1981
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1982
|
+
const stack = model.roots.map((node) => ({
|
|
1983
|
+
node,
|
|
1984
|
+
exit: false
|
|
1985
|
+
}));
|
|
1986
|
+
while (stack.length) {
|
|
1987
|
+
const { node, exit } = stack.pop();
|
|
1988
|
+
if (!exit) {
|
|
1989
|
+
stack.push({
|
|
1990
|
+
node,
|
|
1991
|
+
exit: true
|
|
1992
|
+
});
|
|
1993
|
+
for (const child of node.children) stack.push({
|
|
1994
|
+
node: child,
|
|
1995
|
+
exit: false
|
|
1996
|
+
});
|
|
1997
|
+
} else {
|
|
1998
|
+
let count = selected.has(node.row.key) ? 1 : 0;
|
|
1999
|
+
let total = 1;
|
|
2000
|
+
for (const child of node.children) {
|
|
2001
|
+
const value = counts.get(child);
|
|
2002
|
+
count += value.selected;
|
|
2003
|
+
total += value.total;
|
|
2004
|
+
}
|
|
2005
|
+
counts.set(node, {
|
|
2006
|
+
selected: count,
|
|
2007
|
+
total
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
return counts;
|
|
2012
|
+
});
|
|
748
2013
|
const orderColumns = memo((columns, columnOrder, hiddenColumns) => {
|
|
749
2014
|
const position = new Map(columnOrder.map((id, index) => [id, index]));
|
|
750
2015
|
const ordered = columns.map((column, index) => ({
|
|
@@ -759,11 +2024,43 @@ function createTable(options) {
|
|
|
759
2024
|
visible: ordered.filter((column) => !hidden.has(column.id))
|
|
760
2025
|
};
|
|
761
2026
|
});
|
|
762
|
-
|
|
763
|
-
|
|
2027
|
+
let primedFilter;
|
|
2028
|
+
let primedSort;
|
|
2029
|
+
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));
|
|
2030
|
+
const sortStage = memo((rows, columns, sorting) => primedSort?.rows === rows && primedSort.columns === columns && primedSort.sorting === sorting ? primedSort.result : sortRows(rows, columns, sorting));
|
|
2031
|
+
const rowMembership = /* @__PURE__ */ new WeakMap();
|
|
2032
|
+
function membership(rows) {
|
|
2033
|
+
let keys = rowMembership.get(rows);
|
|
2034
|
+
if (!keys) {
|
|
2035
|
+
keys = new Set(rows.map((row) => row.key));
|
|
2036
|
+
rowMembership.set(rows, keys);
|
|
2037
|
+
}
|
|
2038
|
+
return keys;
|
|
2039
|
+
}
|
|
764
2040
|
const selectedSet = memo((selection) => new Set(selection));
|
|
2041
|
+
const expansionStage = memo((indexed, expanded, selection) => {
|
|
2042
|
+
const keys = expanded === true ? void 0 : new Set(expanded);
|
|
2043
|
+
const selected = new Set(selection);
|
|
2044
|
+
const rows = mapList(indexed.rows, (row) => {
|
|
2045
|
+
const isExpanded = row.canExpand && (expanded === true || (keys?.has(row.key) ?? false));
|
|
2046
|
+
return isExpanded || selected.has(row.key) ? shareRow(row, Object.freeze({
|
|
2047
|
+
...row,
|
|
2048
|
+
isExpanded,
|
|
2049
|
+
selection: selected.has(row.key) ? "all" : "none"
|
|
2050
|
+
})) : row;
|
|
2051
|
+
});
|
|
2052
|
+
return {
|
|
2053
|
+
rows,
|
|
2054
|
+
byKey: { get(key) {
|
|
2055
|
+
const row = indexed.byKey.get(key);
|
|
2056
|
+
return row && row.index < rows.length ? rows[row.index] : void 0;
|
|
2057
|
+
} }
|
|
2058
|
+
};
|
|
2059
|
+
});
|
|
2060
|
+
const expandedOrder = memo((rows, byKey) => mapList(rows, (row) => byKey.get(row.key)));
|
|
765
2061
|
function stateIssues(byId) {
|
|
766
2062
|
const issues = [];
|
|
2063
|
+
if (expandedIssue) issues.push(expandedIssue);
|
|
767
2064
|
const referenced = /* @__PURE__ */ new Set([
|
|
768
2065
|
...state.sorting.map((rule) => rule.column),
|
|
769
2066
|
...Object.keys(state.filters),
|
|
@@ -772,30 +2069,105 @@ function createTable(options) {
|
|
|
772
2069
|
]);
|
|
773
2070
|
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
2071
|
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.`));
|
|
2072
|
+
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
2073
|
return issues;
|
|
777
2074
|
}
|
|
778
2075
|
function computeSnapshot() {
|
|
779
|
-
const
|
|
780
|
-
const
|
|
2076
|
+
const cellProblems = validation.problems();
|
|
2077
|
+
const dirty = new Set(pendingChanges.entries.keys());
|
|
2078
|
+
const metadata = (row) => dirty.has(row.key) || cellProblems.has(row.key) ? shareRow(row, Object.freeze({
|
|
2079
|
+
...row,
|
|
2080
|
+
isDirty: dirty.has(row.key),
|
|
2081
|
+
cellIssues: cellProblems.get(row.key)
|
|
2082
|
+
})) : row;
|
|
2083
|
+
const { columns, byId, issues: columnIssues } = resolveColumns(definitions);
|
|
2084
|
+
const model = treeEnabled ? treeStage(data, byId, lazyVersion) : void 0;
|
|
2085
|
+
const indexed = model ? {
|
|
2086
|
+
rows: model.rows,
|
|
2087
|
+
issues: model.issues
|
|
2088
|
+
} : indexRows(data, byId);
|
|
2089
|
+
const expanded = model ? void 0 : expansionStage(indexed, state.expanded, state.selection);
|
|
781
2090
|
const ordered = orderColumns(columns, state.columnOrder, state.hiddenColumns);
|
|
782
|
-
const issues = [
|
|
2091
|
+
const issues = [
|
|
2092
|
+
...optionIssues,
|
|
2093
|
+
...ingestionIssues,
|
|
2094
|
+
...streamIssues,
|
|
2095
|
+
...columnIssues,
|
|
2096
|
+
...indexed.issues,
|
|
2097
|
+
...stateIssues(byId),
|
|
2098
|
+
...loadIssues.values(),
|
|
2099
|
+
...Array.from(loadValidationIssues.values()).flat(),
|
|
2100
|
+
...Array.from(cellProblems.values()).flatMap((row) => [...row.values()])
|
|
2101
|
+
];
|
|
783
2102
|
const requestedSize = state.pagination.pageSize;
|
|
784
|
-
|
|
2103
|
+
let pageSize = Number.isInteger(requestedSize) && requestedSize >= 1 ? requestedSize : 10;
|
|
785
2104
|
let processedRows;
|
|
786
2105
|
let rows;
|
|
787
2106
|
let filteredCount;
|
|
788
2107
|
let page;
|
|
789
2108
|
let pageCount;
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
2109
|
+
let treeRow;
|
|
2110
|
+
let totalCount = indexed.rows.length;
|
|
2111
|
+
if (model) {
|
|
2112
|
+
const processed = processedTreeStage(model, columns, state.search, state.filters, state.sorting);
|
|
2113
|
+
const ancestors = expandMode === "single" ? singleTreePath(model, state.expanded) : emptyAncestors;
|
|
2114
|
+
const visible = flattenTree(processed, state.expanded, ancestors);
|
|
2115
|
+
const expandedState = state.expanded;
|
|
2116
|
+
const keys = state.expanded === true ? void 0 : new Set(state.expanded);
|
|
2117
|
+
const selected = treeSelection(model, state.selection);
|
|
2118
|
+
const projected = /* @__PURE__ */ new Map();
|
|
2119
|
+
const statuses = new Map(childStatuses);
|
|
2120
|
+
treeRow = (key) => {
|
|
2121
|
+
const cached = projected.get(key);
|
|
2122
|
+
if (cached) return cached;
|
|
2123
|
+
const node = model.byKey.get(key);
|
|
2124
|
+
if (!node) return void 0;
|
|
2125
|
+
const status = statuses.get(key) ?? (node.children.length || !node.unloaded ? "loaded" : "idle");
|
|
2126
|
+
const count = selected.get(node);
|
|
2127
|
+
const canExpand = node.children.length > 0 || status !== "loaded" || (options.getRowCanExpand?.(node.row.original) ?? false);
|
|
2128
|
+
const result = shareRow(node.row, Object.freeze({
|
|
2129
|
+
...node.row,
|
|
2130
|
+
isDirty: dirty.has(key),
|
|
2131
|
+
cellIssues: cellProblems.get(key),
|
|
2132
|
+
depth: node.depth,
|
|
2133
|
+
parentKey: node.parent?.row.key,
|
|
2134
|
+
childCount: status === "loaded" ? node.children.length : void 0,
|
|
2135
|
+
childStatus: status,
|
|
2136
|
+
canExpand,
|
|
2137
|
+
isExpanded: canExpand && (expandedState === true || (keys?.has(key) ?? false) || ancestors.has(key) || processed.autoExpanded.has(key)),
|
|
2138
|
+
selection: coverage(count.selected, count.total)
|
|
2139
|
+
}));
|
|
2140
|
+
projected.set(key, result);
|
|
2141
|
+
return result;
|
|
2142
|
+
};
|
|
2143
|
+
processedRows = visible.map((node) => treeRow(node.row.key));
|
|
2144
|
+
filteredCount = manual ? rowCount ?? model.roots.length : rootPagination ? processed.roots.length : visible.length;
|
|
2145
|
+
totalCount = rootPagination ? model.roots.length : model.nodes.length;
|
|
2146
|
+
pageCount = countPages(filteredCount, pageSize);
|
|
2147
|
+
page = clampPage(state.pagination.page, pageCount);
|
|
2148
|
+
if (manual) rows = processedRows;
|
|
2149
|
+
else if (rootPagination) {
|
|
2150
|
+
const roots = new Set(processed.roots.slice((page - 1) * pageSize, page * pageSize));
|
|
2151
|
+
const allRoots = new Set(processed.roots);
|
|
2152
|
+
let root;
|
|
2153
|
+
rows = processedRows.filter((_row, index) => {
|
|
2154
|
+
const node = visible[index];
|
|
2155
|
+
if (allRoots.has(node)) root = node;
|
|
2156
|
+
return root !== void 0 && roots.has(root);
|
|
2157
|
+
});
|
|
2158
|
+
} else rows = paginateRows(processedRows, {
|
|
2159
|
+
page,
|
|
2160
|
+
pageSize
|
|
2161
|
+
});
|
|
2162
|
+
} else if (manual) {
|
|
2163
|
+
processedRows = expanded.rows;
|
|
2164
|
+
rows = expanded.rows;
|
|
793
2165
|
filteredCount = rowCount ?? indexed.rows.length;
|
|
794
2166
|
pageCount = countPages(filteredCount, pageSize);
|
|
795
2167
|
page = clampPage(state.pagination.page, pageCount);
|
|
796
2168
|
} else {
|
|
797
2169
|
const filtered = filterStage(indexed.rows, columns, state.search, state.filters);
|
|
798
|
-
processedRows = sortStage(filtered, columns, state.sorting);
|
|
2170
|
+
processedRows = expandedOrder(sortStage(filtered, columns, state.sorting), expanded.byKey);
|
|
799
2171
|
filteredCount = processedRows.length;
|
|
800
2172
|
pageCount = countPages(filteredCount, pageSize);
|
|
801
2173
|
page = clampPage(state.pagination.page, pageCount);
|
|
@@ -804,42 +2176,90 @@ function createTable(options) {
|
|
|
804
2176
|
pageSize
|
|
805
2177
|
});
|
|
806
2178
|
}
|
|
2179
|
+
if (!paginate) {
|
|
2180
|
+
rows = processedRows;
|
|
2181
|
+
page = 1;
|
|
2182
|
+
pageCount = 1;
|
|
2183
|
+
pageSize = Math.max(1, rows.length);
|
|
2184
|
+
}
|
|
2185
|
+
const flatProjected = /* @__PURE__ */ new Map();
|
|
2186
|
+
if (!model) {
|
|
2187
|
+
const project = (row) => {
|
|
2188
|
+
let result = flatProjected.get(row.key);
|
|
2189
|
+
if (!result) {
|
|
2190
|
+
result = metadata(row);
|
|
2191
|
+
flatProjected.set(row.key, result);
|
|
2192
|
+
}
|
|
2193
|
+
return result;
|
|
2194
|
+
};
|
|
2195
|
+
const shared = rows === processedRows;
|
|
2196
|
+
processedRows = mapList(processedRows, project);
|
|
2197
|
+
rows = shared ? processedRows : rows.map(project);
|
|
2198
|
+
}
|
|
807
2199
|
const selected = selectedSet(state.selection);
|
|
808
|
-
const
|
|
2200
|
+
const allData = model ? model.byKey : indexed.byKey;
|
|
2201
|
+
const allProcessedKeys = model ? void 0 : manual ? void 0 : membership(filterStage(indexed.rows, columns, state.search, state.filters));
|
|
2202
|
+
const selectedInProcessed = state.selection.filter((key) => allData.has(key) && (!allProcessedKeys || allProcessedKeys.has(key))).length;
|
|
2203
|
+
const pageSelected = rows === processedRows ? selectedInProcessed : rows.filter((row) => selected.has(row.key)).length;
|
|
809
2204
|
const scopeRows = selectScope === "page" || manual ? rows : processedRows;
|
|
810
|
-
const scopeSelected = scopeRows === rows ? pageSelected : scopeRows.filter((row) => selected.has(row.key)).length;
|
|
2205
|
+
const scopeSelected = scopeRows === rows ? pageSelected : model ? scopeRows.filter((row) => selected.has(row.key)).length : selectedInProcessed;
|
|
811
2206
|
const pageStart = rows.length === 0 ? 0 : (page - 1) * pageSize + 1;
|
|
812
2207
|
const sortIndex = new Map(state.sorting.map((rule, priority) => [rule.column, {
|
|
813
2208
|
direction: rule.direction,
|
|
814
2209
|
priority
|
|
815
2210
|
}]));
|
|
816
2211
|
return Object.freeze({
|
|
2212
|
+
tree: treeEnabled,
|
|
2213
|
+
loadState,
|
|
2214
|
+
loadedRowCount: indexed.rows.length,
|
|
2215
|
+
expectedRowCount,
|
|
817
2216
|
state,
|
|
818
2217
|
columns: ordered.visible,
|
|
819
2218
|
allColumns: ordered.all,
|
|
820
2219
|
rows,
|
|
2220
|
+
renderItems: !model && state.expanded !== true && state.expanded.length === 0 ? mapList(rows, (row, rowIndex) => Object.freeze({
|
|
2221
|
+
kind: "row",
|
|
2222
|
+
key: getRowItemKey(row.key),
|
|
2223
|
+
row,
|
|
2224
|
+
rowIndex
|
|
2225
|
+
})) : Object.freeze(rows.flatMap((row, rowIndex) => {
|
|
2226
|
+
const item = Object.freeze({
|
|
2227
|
+
kind: "row",
|
|
2228
|
+
key: getRowItemKey(row.key),
|
|
2229
|
+
row,
|
|
2230
|
+
rowIndex
|
|
2231
|
+
});
|
|
2232
|
+
return row.isExpanded && (!model || (options.getRowCanExpand?.(row.original) ?? false)) ? [item, Object.freeze({
|
|
2233
|
+
kind: "detail",
|
|
2234
|
+
key: getRowItemKey(row.key, "detail"),
|
|
2235
|
+
row,
|
|
2236
|
+
rowIndex
|
|
2237
|
+
})] : [item];
|
|
2238
|
+
})),
|
|
2239
|
+
paginate,
|
|
821
2240
|
processedRows,
|
|
822
2241
|
page,
|
|
823
2242
|
pageSize,
|
|
824
2243
|
pageCount,
|
|
825
2244
|
rowCount: filteredCount,
|
|
826
|
-
totalRowCount: manual ? filteredCount :
|
|
2245
|
+
totalRowCount: manual ? filteredCount : totalCount,
|
|
827
2246
|
pageStart,
|
|
828
|
-
pageEnd: rows.length === 0 ? 0 : pageStart + rows.length - 1,
|
|
2247
|
+
pageEnd: rows.length === 0 ? 0 : rootPagination ? Math.min(filteredCount, page * pageSize) : pageStart + rows.length - 1,
|
|
829
2248
|
pageItems: paginationItems(page, pageCount),
|
|
830
2249
|
canPreviousPage: page > 1,
|
|
831
2250
|
canNextPage: page < pageCount,
|
|
832
2251
|
selectionMode,
|
|
833
|
-
selectedCount: state.selection.length,
|
|
2252
|
+
selectedCount: model ? state.selection.filter((key) => model.byKey.has(key)).length : state.selection.length,
|
|
834
2253
|
pageSelection: coverage(pageSelected, rows.length),
|
|
835
2254
|
allSelection: coverage(scopeSelected, scopeRows.length),
|
|
836
2255
|
canUndo: undoStack.canUndo,
|
|
837
2256
|
canRedo: undoStack.canRedo,
|
|
838
2257
|
issues: Object.freeze(issues),
|
|
2258
|
+
pendingCells: validation.cells(),
|
|
839
2259
|
isSelected: (key) => selected.has(key),
|
|
840
2260
|
getSort: (columnId) => sortIndex.get(columnId),
|
|
841
2261
|
getColumn: (columnId) => byId.get(columnId),
|
|
842
|
-
getRow: (key) =>
|
|
2262
|
+
getRow: (key) => treeRow ? treeRow(key) : flatProjected.get(key) ?? (expanded.byKey.get(key) ? metadata(expanded.byKey.get(key)) : void 0)
|
|
843
2263
|
});
|
|
844
2264
|
}
|
|
845
2265
|
function getSnapshot() {
|
|
@@ -848,18 +2268,140 @@ function createTable(options) {
|
|
|
848
2268
|
}
|
|
849
2269
|
function notify() {
|
|
850
2270
|
snapshot = void 0;
|
|
2271
|
+
if (deferValidationNotify) {
|
|
2272
|
+
validationNotifyRequested = true;
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
if (transaction) return;
|
|
851
2276
|
if (listeners.size === 0) return;
|
|
852
2277
|
const next = getSnapshot();
|
|
853
2278
|
for (const listener of listeners) listener(next);
|
|
854
2279
|
}
|
|
2280
|
+
function abortLoads(all = false) {
|
|
2281
|
+
for (const [key, controller] of pendingLoads) {
|
|
2282
|
+
const row = all ? void 0 : getSnapshot().getRow(key);
|
|
2283
|
+
const visible = !all && getSnapshot().processedRows.some((candidate) => candidate.key === key);
|
|
2284
|
+
if (all || !row?.isExpanded || !visible) {
|
|
2285
|
+
pendingLoads.delete(key);
|
|
2286
|
+
childStatuses.delete(key);
|
|
2287
|
+
controller.abort();
|
|
2288
|
+
snapshot = void 0;
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
function requestChildren(row) {
|
|
2293
|
+
if (!options.loadChildren || destroyed || pendingLoads.has(row.key) || lazyChildren.has(row.key) || row.childStatus === "loaded") return;
|
|
2294
|
+
const key = row.key;
|
|
2295
|
+
if (!options.createChildLoadController) {
|
|
2296
|
+
childStatuses.set(key, "error");
|
|
2297
|
+
loadIssues.set(key, issue("invalid_tree_option", "Lazy children require createChildLoadController from the caller's runtime.", { rowKey: key }));
|
|
2298
|
+
notify();
|
|
2299
|
+
return;
|
|
2300
|
+
}
|
|
2301
|
+
let controller;
|
|
2302
|
+
try {
|
|
2303
|
+
controller = options.createChildLoadController();
|
|
2304
|
+
} catch (error) {
|
|
2305
|
+
loadIssues.set(key, issue("tree_load_error", String(error), { rowKey: key }));
|
|
2306
|
+
childStatuses.set(key, "error");
|
|
2307
|
+
notify();
|
|
2308
|
+
return;
|
|
2309
|
+
}
|
|
2310
|
+
pendingLoads.set(key, controller);
|
|
2311
|
+
childStatuses.set(key, "loading");
|
|
2312
|
+
loadIssues.delete(key);
|
|
2313
|
+
loadValidationIssues.delete(key);
|
|
2314
|
+
notify();
|
|
2315
|
+
Promise.resolve().then(() => controller.signal.aborted ? [] : options.loadChildren(row, controller.signal)).then((children) => {
|
|
2316
|
+
if (controller.signal.aborted || pendingLoads.get(key) !== controller || destroyed) return void 0;
|
|
2317
|
+
const { byId } = resolveColumns(definitions);
|
|
2318
|
+
const existing = treeStage(data, byId, lazyVersion);
|
|
2319
|
+
const candidate = buildTree(children, byId, readKey, {
|
|
2320
|
+
...options,
|
|
2321
|
+
getParentKey: void 0
|
|
2322
|
+
}, /* @__PURE__ */ new Map());
|
|
2323
|
+
pendingLoads.delete(key);
|
|
2324
|
+
loadValidationIssues.set(key, candidate.issues.filter((problem) => problem.code === "tree_cycle" || problem.code === "tree_depth_exceeded"));
|
|
2325
|
+
if (candidate.nodes.filter((node) => existing.byKey.has(node.row.key)).length || candidate.issues.some((problem) => problem.code === "duplicate_row_key")) {
|
|
2326
|
+
childStatuses.set(key, "error");
|
|
2327
|
+
loadIssues.set(key, issue("tree_duplicate_key", "Loaded children collide with existing keys; the batch is rejected.", { rowKey: key }));
|
|
2328
|
+
} else {
|
|
2329
|
+
lazyChildren.set(key, children);
|
|
2330
|
+
childStatuses.set(key, "loaded");
|
|
2331
|
+
lazyVersion++;
|
|
2332
|
+
if (state.selection.includes(key) && selectionMode === "multiple") {
|
|
2333
|
+
const nextModel = treeStage(data, byId, lazyVersion);
|
|
2334
|
+
applySelection([...state.selection, ...selectionKeys([key], nextModel)]);
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
notify();
|
|
2338
|
+
startVisibleLoads();
|
|
2339
|
+
}).catch((error) => {
|
|
2340
|
+
if (controller.signal.aborted || pendingLoads.get(key) !== controller || destroyed) return;
|
|
2341
|
+
pendingLoads.delete(key);
|
|
2342
|
+
childStatuses.set(key, "error");
|
|
2343
|
+
loadIssues.set(key, issue("tree_load_error", `Children could not be loaded: ${String(error)}`, { rowKey: key }));
|
|
2344
|
+
notify();
|
|
2345
|
+
});
|
|
2346
|
+
}
|
|
2347
|
+
function startVisibleLoads() {
|
|
2348
|
+
if (!treeEnabled || !options.loadChildren || destroyed) return;
|
|
2349
|
+
const current = getSnapshot();
|
|
2350
|
+
for (const row of current.rows) if (row.isExpanded && row.childStatus === "idle") requestChildren(row);
|
|
2351
|
+
}
|
|
2352
|
+
function selectionKeys(keys, model) {
|
|
2353
|
+
if (!treeEnabled || selectionMode !== "multiple") return [...keys];
|
|
2354
|
+
model ??= treeStage(data, resolveColumns(definitions).byId, lazyVersion);
|
|
2355
|
+
const result = new Set(keys);
|
|
2356
|
+
const stack = keys.flatMap((key) => {
|
|
2357
|
+
const node = model.byKey.get(key);
|
|
2358
|
+
return node ? [node] : [];
|
|
2359
|
+
});
|
|
2360
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2361
|
+
while (stack.length) {
|
|
2362
|
+
const node = stack.pop();
|
|
2363
|
+
if (visited.has(node.row.key)) continue;
|
|
2364
|
+
visited.add(node.row.key);
|
|
2365
|
+
result.add(node.row.key);
|
|
2366
|
+
for (const child of node.children) stack.push(child);
|
|
2367
|
+
}
|
|
2368
|
+
return [...result];
|
|
2369
|
+
}
|
|
855
2370
|
/** Apply a change made by a table operation. */
|
|
856
2371
|
function commit(patch) {
|
|
857
2372
|
const previous = state;
|
|
858
|
-
const
|
|
859
|
-
|
|
2373
|
+
const previousIssue = expandedIssue;
|
|
2374
|
+
const next = mergePatch(patch);
|
|
2375
|
+
if (next === previous && previousIssue === expandedIssue) return;
|
|
860
2376
|
state = next;
|
|
861
|
-
|
|
2377
|
+
snapshot = void 0;
|
|
2378
|
+
abortLoads();
|
|
2379
|
+
if (next !== previous) options.onStateChange?.(state, previous);
|
|
862
2380
|
notify();
|
|
2381
|
+
startVisibleLoads();
|
|
2382
|
+
}
|
|
2383
|
+
function canonicalExpanded(expanded, candidate = state) {
|
|
2384
|
+
if (expandMode === "multiple") return expanded;
|
|
2385
|
+
if (expanded !== true) return expanded.slice(-1);
|
|
2386
|
+
const { columns, byId } = resolveColumns(definitions);
|
|
2387
|
+
if (treeEnabled) {
|
|
2388
|
+
const model = treeStage(data, byId, lazyVersion);
|
|
2389
|
+
const first = processedTreeStage(model, columns, candidate.search, candidate.filters, candidate.sorting).roots.find((node) => node.children.length > 0 || (options.hasChildren?.(node.row.original) ?? false) || (options.getRowCanExpand?.(node.row.original) ?? false));
|
|
2390
|
+
return first ? [first.row.key] : [];
|
|
2391
|
+
}
|
|
2392
|
+
const indexed = indexRows(data, byId);
|
|
2393
|
+
const first = (manual ? indexed.rows : sortStage(filterStage(indexed.rows, columns, candidate.search, candidate.filters), columns, candidate.sorting)).find((row) => row.canExpand);
|
|
2394
|
+
return first ? [first.key] : [];
|
|
2395
|
+
}
|
|
2396
|
+
function mergePatch(patch) {
|
|
2397
|
+
if (patch.expanded !== void 0) expandedIssue = void 0;
|
|
2398
|
+
const next = mergeState(state, patch, (problem) => {
|
|
2399
|
+
expandedIssue = problem;
|
|
2400
|
+
});
|
|
2401
|
+
return expandMode === "single" && patch.expanded !== void 0 ? mergeState(state, {
|
|
2402
|
+
...patch,
|
|
2403
|
+
expanded: canonicalExpanded(next.expanded, next)
|
|
2404
|
+
}) : next;
|
|
863
2405
|
}
|
|
864
2406
|
const firstPage = () => ({
|
|
865
2407
|
page: 1,
|
|
@@ -890,10 +2432,14 @@ function createTable(options) {
|
|
|
890
2432
|
if (selection.length === current.length && selection.every((key, i) => key === current[i])) return;
|
|
891
2433
|
commit({ selection });
|
|
892
2434
|
}
|
|
893
|
-
function applyChanges(edits, record) {
|
|
2435
|
+
function applyChanges(edits, record, recoveredIssues = []) {
|
|
894
2436
|
const current = getSnapshot();
|
|
895
2437
|
const working = [...data];
|
|
896
|
-
const
|
|
2438
|
+
const model = treeEnabled ? treeStage(data, resolveColumns(definitions).byId, lazyVersion) : void 0;
|
|
2439
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
2440
|
+
const cache = new Map(lazyChildren);
|
|
2441
|
+
const originalOf = (node) => replacements.get(node) ?? node.row.original;
|
|
2442
|
+
const issues = [...recoveredIssues];
|
|
897
2443
|
const changes = [];
|
|
898
2444
|
const samples = /* @__PURE__ */ new Map();
|
|
899
2445
|
const sampleOf = (columnId) => {
|
|
@@ -916,6 +2462,13 @@ function createTable(options) {
|
|
|
916
2462
|
}));
|
|
917
2463
|
continue;
|
|
918
2464
|
}
|
|
2465
|
+
if (!isSafePath(edit.column)) {
|
|
2466
|
+
issues.push(issue("unsafe_path", `The path "${edit.column}" addresses a prototype and cannot be written.`, {
|
|
2467
|
+
rowKey: edit.rowKey,
|
|
2468
|
+
column: edit.column
|
|
2469
|
+
}));
|
|
2470
|
+
continue;
|
|
2471
|
+
}
|
|
919
2472
|
if (!column) {
|
|
920
2473
|
issues.push(issue("unknown_column", `No column has the id "${edit.column}".`, {
|
|
921
2474
|
rowKey: edit.rowKey,
|
|
@@ -923,7 +2476,22 @@ function createTable(options) {
|
|
|
923
2476
|
}));
|
|
924
2477
|
continue;
|
|
925
2478
|
}
|
|
926
|
-
const
|
|
2479
|
+
const node = model?.byKey.get(row.key);
|
|
2480
|
+
const original = node ? originalOf(node) : working[row.index];
|
|
2481
|
+
if (node && !node.writable) {
|
|
2482
|
+
issues.push(issue("read_only_cell", "A recovered nested root has no unambiguous source path for editing.", {
|
|
2483
|
+
rowKey: row.key,
|
|
2484
|
+
column: column.id
|
|
2485
|
+
}));
|
|
2486
|
+
continue;
|
|
2487
|
+
}
|
|
2488
|
+
if (node?.parent && options.getChildren && !options.setChildren) {
|
|
2489
|
+
issues.push(issue("read_only_cell", "Editing a nested child requires an immutable setChildren callback.", {
|
|
2490
|
+
rowKey: row.key,
|
|
2491
|
+
column: column.id
|
|
2492
|
+
}));
|
|
2493
|
+
continue;
|
|
2494
|
+
}
|
|
927
2495
|
if (record && !column.isEditable(original)) {
|
|
928
2496
|
issues.push(issue("read_only_cell", `The "${column.id}" cell of row "${String(row.key)}" is read-only.`, {
|
|
929
2497
|
rowKey: row.key,
|
|
@@ -945,7 +2513,16 @@ function createTable(options) {
|
|
|
945
2513
|
} else value = edit.value;
|
|
946
2514
|
const previous = column.getValue(original);
|
|
947
2515
|
if (Object.is(previous, value)) continue;
|
|
948
|
-
|
|
2516
|
+
let updated;
|
|
2517
|
+
try {
|
|
2518
|
+
updated = column.setValue(original, value);
|
|
2519
|
+
} catch (error) {
|
|
2520
|
+
issues.push(issue("invalid_value", error instanceof Error ? error.message : String(error), {
|
|
2521
|
+
rowKey: row.key,
|
|
2522
|
+
column: column.id
|
|
2523
|
+
}));
|
|
2524
|
+
continue;
|
|
2525
|
+
}
|
|
949
2526
|
if (updated === void 0) {
|
|
950
2527
|
issues.push(issue("read_only_cell", `The "${column.id}" column has no way to write a value.`, {
|
|
951
2528
|
rowKey: row.key,
|
|
@@ -953,7 +2530,38 @@ function createTable(options) {
|
|
|
953
2530
|
}));
|
|
954
2531
|
continue;
|
|
955
2532
|
}
|
|
956
|
-
|
|
2533
|
+
if (readKey(updated, row.index) !== readKey(original, row.index)) {
|
|
2534
|
+
issues.push(issue("invalid_row_operation", "Editing a row key is not supported; remove and insert the row instead.", {
|
|
2535
|
+
rowKey: row.key,
|
|
2536
|
+
column: column.id
|
|
2537
|
+
}));
|
|
2538
|
+
continue;
|
|
2539
|
+
}
|
|
2540
|
+
if (!node) working[row.index] = updated;
|
|
2541
|
+
else {
|
|
2542
|
+
const planned = /* @__PURE__ */ new Map([[node, updated]]);
|
|
2543
|
+
const plannedCache = /* @__PURE__ */ new Map();
|
|
2544
|
+
let cursor = node;
|
|
2545
|
+
try {
|
|
2546
|
+
while (cursor.parent) {
|
|
2547
|
+
const parent = cursor.parent;
|
|
2548
|
+
const children = parent.children.map((child) => planned.get(child) ?? originalOf(child));
|
|
2549
|
+
if (cursor.lazy) plannedCache.set(parent.row.key, children);
|
|
2550
|
+
if (options.setChildren) planned.set(parent, options.setChildren(originalOf(parent), children));
|
|
2551
|
+
cursor = parent;
|
|
2552
|
+
}
|
|
2553
|
+
} catch (error) {
|
|
2554
|
+
issues.push(issue("invalid_value", error instanceof Error ? error.message : String(error), {
|
|
2555
|
+
rowKey: row.key,
|
|
2556
|
+
column: column.id
|
|
2557
|
+
}));
|
|
2558
|
+
continue;
|
|
2559
|
+
}
|
|
2560
|
+
for (const [item, plannedRow] of planned) replacements.set(item, plannedRow);
|
|
2561
|
+
for (const [key, children] of plannedCache) cache.set(key, children);
|
|
2562
|
+
if (!options.getChildren && !node.lazy) working[node.sourceIndex] = updated;
|
|
2563
|
+
if (options.getChildren || options.setChildren) working[cursor.sourceIndex] = originalOf(cursor);
|
|
2564
|
+
}
|
|
957
2565
|
changes.push({
|
|
958
2566
|
rowKey: row.key,
|
|
959
2567
|
column: column.id,
|
|
@@ -967,17 +2575,390 @@ function createTable(options) {
|
|
|
967
2575
|
else {
|
|
968
2576
|
status = issues.length > 0 ? "partial" : "applied";
|
|
969
2577
|
data = working;
|
|
970
|
-
if (
|
|
971
|
-
|
|
2578
|
+
if (model) {
|
|
2579
|
+
lazyChildren.clear();
|
|
2580
|
+
for (const [key, children] of cache) lazyChildren.set(key, children);
|
|
2581
|
+
lazyVersion++;
|
|
2582
|
+
}
|
|
2583
|
+
if (record) {
|
|
2584
|
+
if (!transaction) undoStack.record({
|
|
2585
|
+
kind: "cells",
|
|
2586
|
+
changes
|
|
2587
|
+
});
|
|
2588
|
+
}
|
|
2589
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2590
|
+
for (const change of changes) {
|
|
2591
|
+
const group = groups.get(change.rowKey) ?? [];
|
|
2592
|
+
group.push(change);
|
|
2593
|
+
groups.set(change.rowKey, group);
|
|
2594
|
+
}
|
|
2595
|
+
for (const [key, group] of groups) pendingChanges.record(key, current.getRow(key).original, group.at(-1).row, positionOf(key), group);
|
|
2596
|
+
if (!transaction) options.onDataChange?.(data, changes);
|
|
972
2597
|
notify();
|
|
973
2598
|
}
|
|
974
|
-
if (issues.length > 0) options.onEditIssues?.(issues);
|
|
2599
|
+
if (issues.length > 0 && !transaction) options.onEditIssues?.(issues);
|
|
975
2600
|
return Object.freeze({
|
|
976
2601
|
status,
|
|
977
2602
|
changes: Object.freeze(changes),
|
|
978
|
-
issues: Object.freeze(issues)
|
|
2603
|
+
issues: Object.freeze(issues),
|
|
2604
|
+
rowChanges: Object.freeze([]),
|
|
2605
|
+
pendingCells: Object.freeze([])
|
|
979
2606
|
});
|
|
980
2607
|
}
|
|
2608
|
+
function sourceRows() {
|
|
2609
|
+
const { byId } = resolveColumns(definitions);
|
|
2610
|
+
const model = treeEnabled ? treeStage(data, byId, lazyVersion) : void 0;
|
|
2611
|
+
return model ? treeIndex(model) : indexRows(data, byId);
|
|
2612
|
+
}
|
|
2613
|
+
function positionOf(key) {
|
|
2614
|
+
const { byId } = resolveColumns(definitions);
|
|
2615
|
+
if (treeEnabled) {
|
|
2616
|
+
const model = treeStage(data, byId, lazyVersion);
|
|
2617
|
+
return treePositions(model).get(key) ?? Object.freeze({});
|
|
2618
|
+
}
|
|
2619
|
+
const indexed = indexRows(data, byId);
|
|
2620
|
+
const row = indexed.byKey.get(key);
|
|
2621
|
+
return Object.freeze({ before: row ? indexed.rows[row.index + 1]?.key : void 0 });
|
|
2622
|
+
}
|
|
2623
|
+
function scene() {
|
|
2624
|
+
return {
|
|
2625
|
+
data,
|
|
2626
|
+
lazy: new Map(lazyChildren)
|
|
2627
|
+
};
|
|
2628
|
+
}
|
|
2629
|
+
function resultForRows(changes, issues, cells = []) {
|
|
2630
|
+
return Object.freeze({
|
|
2631
|
+
status: changes.length || cells.length ? issues.length ? "partial" : "applied" : issues.length ? "rejected" : "unchanged",
|
|
2632
|
+
changes: Object.freeze([...cells]),
|
|
2633
|
+
rowChanges: Object.freeze([...changes]),
|
|
2634
|
+
pendingCells: Object.freeze([]),
|
|
2635
|
+
issues: Object.freeze([...issues])
|
|
2636
|
+
});
|
|
2637
|
+
}
|
|
2638
|
+
function rowProblem(message, rowKey) {
|
|
2639
|
+
return issue("invalid_row_operation", message, { rowKey });
|
|
2640
|
+
}
|
|
2641
|
+
function stableRows(rows) {
|
|
2642
|
+
return options.rowKey !== void 0 || rows.every((row) => {
|
|
2643
|
+
const key = getPath(row, "id");
|
|
2644
|
+
return typeof key === "string" || typeof key === "number";
|
|
2645
|
+
});
|
|
2646
|
+
}
|
|
2647
|
+
function cellDifferences(previous, row, key) {
|
|
2648
|
+
return resolveColumns(definitions).columns.flatMap((column) => {
|
|
2649
|
+
const before = column.getValue(previous);
|
|
2650
|
+
const value = column.getValue(row);
|
|
2651
|
+
return Object.is(before, value) ? [] : [{
|
|
2652
|
+
rowKey: key,
|
|
2653
|
+
column: column.id,
|
|
2654
|
+
previous: before,
|
|
2655
|
+
value,
|
|
2656
|
+
row
|
|
2657
|
+
}];
|
|
2658
|
+
});
|
|
2659
|
+
}
|
|
2660
|
+
function recordRows(previous, keys, positions, baselines = /* @__PURE__ */ new Map()) {
|
|
2661
|
+
const next = sourceRows();
|
|
2662
|
+
const rowChanges = [];
|
|
2663
|
+
for (const key of keys) {
|
|
2664
|
+
const before = previous.byKey.get(key)?.original;
|
|
2665
|
+
const row = next.byKey.get(key)?.original;
|
|
2666
|
+
const position = positions.get(key) ?? positionOf(key);
|
|
2667
|
+
const cells = before !== void 0 && row !== void 0 ? cellDifferences(before, row, key) : [];
|
|
2668
|
+
const baseline = baselines.get(key);
|
|
2669
|
+
const pending = pendingChanges.entries.get(key);
|
|
2670
|
+
if (row === void 0 && baseline !== void 0 && pending?.previous !== void 0) pending.previous = baseline;
|
|
2671
|
+
pendingChanges.record(key, row === void 0 ? baseline ?? before : before, row, position, cells);
|
|
2672
|
+
if (before === void 0 && row !== void 0) rowChanges.push(Object.freeze({
|
|
2673
|
+
kind: "inserted",
|
|
2674
|
+
rowKey: key,
|
|
2675
|
+
row,
|
|
2676
|
+
position: positionOf(key)
|
|
2677
|
+
}));
|
|
2678
|
+
else if (row === void 0 && before !== void 0) rowChanges.push(Object.freeze({
|
|
2679
|
+
kind: "removed",
|
|
2680
|
+
rowKey: key,
|
|
2681
|
+
row: before,
|
|
2682
|
+
position
|
|
2683
|
+
}));
|
|
2684
|
+
}
|
|
2685
|
+
return rowChanges;
|
|
2686
|
+
}
|
|
2687
|
+
function publishRows(before, previous, keys, positions, issues, baselines = /* @__PURE__ */ new Map()) {
|
|
2688
|
+
const rowChanges = recordRows(previous, keys, positions, baselines);
|
|
2689
|
+
if (!transaction) {
|
|
2690
|
+
undoStack.record({
|
|
2691
|
+
kind: "rows",
|
|
2692
|
+
before,
|
|
2693
|
+
after: scene(),
|
|
2694
|
+
keys
|
|
2695
|
+
});
|
|
2696
|
+
options.onDataChange?.(data, []);
|
|
2697
|
+
if (issues.length) options.onEditIssues?.(issues);
|
|
2698
|
+
}
|
|
2699
|
+
notify();
|
|
2700
|
+
return resultForRows(rowChanges, issues);
|
|
2701
|
+
}
|
|
2702
|
+
function restoreScene(entry, direction) {
|
|
2703
|
+
const previous = sourceRows();
|
|
2704
|
+
const baselines = removalBaselines();
|
|
2705
|
+
const positions = new Map(entry.keys.map((key) => [key, positionOf(key)]));
|
|
2706
|
+
const target = direction === "undo" ? entry.before : entry.after;
|
|
2707
|
+
data = target.data;
|
|
2708
|
+
const changed = /* @__PURE__ */ new Set([...entry.before.lazy.keys(), ...entry.after.lazy.keys()]);
|
|
2709
|
+
for (const key of changed) {
|
|
2710
|
+
if (entry.before.lazy.get(key) === entry.after.lazy.get(key)) continue;
|
|
2711
|
+
const rows = target.lazy.get(key);
|
|
2712
|
+
if (rows) lazyChildren.set(key, rows);
|
|
2713
|
+
else lazyChildren.delete(key);
|
|
2714
|
+
}
|
|
2715
|
+
lazyVersion++;
|
|
2716
|
+
recordRows(previous, entry.keys, positions, baselines);
|
|
2717
|
+
options.onDataChange?.(data, []);
|
|
2718
|
+
notify();
|
|
2719
|
+
}
|
|
2720
|
+
function removalBaselines() {
|
|
2721
|
+
const baselines = /* @__PURE__ */ new Map();
|
|
2722
|
+
if (!options.getChildren || !options.setChildren) return baselines;
|
|
2723
|
+
const model = treeStage(data, resolveColumns(definitions).byId, lazyVersion);
|
|
2724
|
+
for (const node of model.nodes.toReversed()) {
|
|
2725
|
+
const entry = pendingChanges.entries.get(node.row.key);
|
|
2726
|
+
if (entry && entry.previous === void 0) continue;
|
|
2727
|
+
let baseline = entry?.previous ?? node.row.original;
|
|
2728
|
+
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);
|
|
2729
|
+
const originalChildren = options.getChildren(baseline) ?? [];
|
|
2730
|
+
if (children.length !== originalChildren.length || children.some((child, index) => child !== originalChildren[index])) baseline = options.setChildren(baseline, children);
|
|
2731
|
+
baselines.set(node.row.key, baseline);
|
|
2732
|
+
}
|
|
2733
|
+
return baselines;
|
|
2734
|
+
}
|
|
2735
|
+
function rewriteNested(model, removed, overrides, cache) {
|
|
2736
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
2737
|
+
for (const node of model.nodes.toReversed()) {
|
|
2738
|
+
if (removed.has(node.row.key)) continue;
|
|
2739
|
+
const children = overrides.get(node.row.key) ?? node.children.filter((child) => !removed.has(child.row.key)).map((child) => replacements.get(child) ?? child.row.original);
|
|
2740
|
+
if (!(overrides.has(node.row.key) || children.length !== node.children.length || children.some((child, index) => child !== node.children[index]?.row.original))) continue;
|
|
2741
|
+
if (!options.setChildren) throw new Error("Changing nested children requires setChildren.");
|
|
2742
|
+
replacements.set(node, options.setChildren(node.row.original, children));
|
|
2743
|
+
if (cache.has(node.row.key)) cache.set(node.row.key, children);
|
|
2744
|
+
}
|
|
2745
|
+
const roots = new Map(model.roots.filter((node) => node.writable).map((node) => [node.sourceIndex, node]));
|
|
2746
|
+
return data.flatMap((row, index) => {
|
|
2747
|
+
const node = roots.get(index);
|
|
2748
|
+
return node && removed.has(node.row.key) ? [] : [node ? replacements.get(node) ?? row : row];
|
|
2749
|
+
});
|
|
2750
|
+
}
|
|
2751
|
+
function insertRows(rows, at = {}) {
|
|
2752
|
+
const before = scene();
|
|
2753
|
+
const previous = sourceRows();
|
|
2754
|
+
const problems = [];
|
|
2755
|
+
const reject = (problem) => {
|
|
2756
|
+
if (!transaction) options.onEditIssues?.([problem]);
|
|
2757
|
+
return resultForRows([], [problem]);
|
|
2758
|
+
};
|
|
2759
|
+
let incoming;
|
|
2760
|
+
try {
|
|
2761
|
+
incoming = rows ?? (options.createRow ? [options.createRow()] : []);
|
|
2762
|
+
} catch (error) {
|
|
2763
|
+
return reject(rowProblem(error instanceof Error ? error.message : String(error)));
|
|
2764
|
+
}
|
|
2765
|
+
if (rows === void 0 && !options.createRow) return reject(rowProblem("insertRows() requires rows or a createRow callback."));
|
|
2766
|
+
if (!incoming.length) return resultForRows([], []);
|
|
2767
|
+
if (!stableRows(data) || !stableRows(incoming)) return reject(rowProblem("Row operations require stable row keys; supply rowKey or an id on every row."));
|
|
2768
|
+
const { byId } = resolveColumns(definitions);
|
|
2769
|
+
const model = treeEnabled ? treeStage(data, byId, lazyVersion) : void 0;
|
|
2770
|
+
const parent = at.parent === void 0 ? void 0 : model?.byKey.get(at.parent);
|
|
2771
|
+
const sibling = at.before === void 0 ? void 0 : previous.byKey.get(at.before);
|
|
2772
|
+
if (at.parent !== void 0 && !parent) return reject(issue("unknown_row", "The insertion parent does not exist.", { rowKey: at.parent }));
|
|
2773
|
+
if (at.before !== void 0 && !sibling) return reject(issue("unknown_row", "The insertion sibling does not exist.", { rowKey: at.before }));
|
|
2774
|
+
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));
|
|
2775
|
+
if (parent && !parent.writable) return reject(rowProblem("A recovered parent has no writable source path.", parent.row.key));
|
|
2776
|
+
if (parent && getSnapshot().getRow(parent.row.key)?.childStatus !== "loaded") return reject(rowProblem("Load the parent's children before inserting rows.", parent.row.key));
|
|
2777
|
+
const cache = new Map(lazyChildren);
|
|
2778
|
+
let next;
|
|
2779
|
+
try {
|
|
2780
|
+
if (options.getChildren && parent && model) {
|
|
2781
|
+
const children = parent.children.map((child) => child.row.original);
|
|
2782
|
+
const index = at.before === void 0 ? children.length : parent.children.findIndex((child) => child.row.key === at.before);
|
|
2783
|
+
children.splice(index, 0, ...incoming);
|
|
2784
|
+
next = rewriteNested(model, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Map([[parent.row.key, children]]), cache);
|
|
2785
|
+
} else if (parent && cache.has(parent.row.key)) {
|
|
2786
|
+
incoming = incoming.map((row) => options.setParentKey ? options.setParentKey(row, parent.row.key) : row);
|
|
2787
|
+
const children = [...cache.get(parent.row.key)];
|
|
2788
|
+
const index = at.before === void 0 ? children.length : parent.children.findIndex((child) => child.row.key === at.before);
|
|
2789
|
+
children.splice(index, 0, ...incoming);
|
|
2790
|
+
cache.set(parent.row.key, children);
|
|
2791
|
+
next = [...data];
|
|
2792
|
+
} else {
|
|
2793
|
+
if (parent && options.getParentKey) {
|
|
2794
|
+
incoming = incoming.map((row) => options.setParentKey ? options.setParentKey(row, parent.row.key) : row);
|
|
2795
|
+
if (incoming.some((row) => options.getParentKey(row) !== parent.row.key)) return reject(rowProblem("Supply setParentKey or rows already addressed to the requested parent."));
|
|
2796
|
+
}
|
|
2797
|
+
const working = [...data];
|
|
2798
|
+
working.splice(sibling?.index ?? working.length, 0, ...incoming);
|
|
2799
|
+
next = working;
|
|
2800
|
+
}
|
|
2801
|
+
const candidate = treeEnabled ? buildTree(next, byId, readKey, options, cache) : indexRows(next, byId);
|
|
2802
|
+
if (!stableRows(candidate.rows.map((row) => row.original))) return reject(rowProblem("Every inserted descendant must have a stable key."));
|
|
2803
|
+
const baseline = /* @__PURE__ */ new Map();
|
|
2804
|
+
for (const problem of previous.issues) {
|
|
2805
|
+
const key = `${problem.code}:${String(problem.rowKey)}`;
|
|
2806
|
+
baseline.set(key, (baseline.get(key) ?? 0) + 1);
|
|
2807
|
+
}
|
|
2808
|
+
problems.push(...candidate.issues.filter((problem) => {
|
|
2809
|
+
const key = `${problem.code}:${String(problem.rowKey)}`;
|
|
2810
|
+
const remaining = baseline.get(key) ?? 0;
|
|
2811
|
+
if (!remaining) return true;
|
|
2812
|
+
baseline.set(key, remaining - 1);
|
|
2813
|
+
return false;
|
|
2814
|
+
}));
|
|
2815
|
+
if (problems.length) {
|
|
2816
|
+
if (!transaction) options.onEditIssues?.(problems);
|
|
2817
|
+
return resultForRows([], problems);
|
|
2818
|
+
}
|
|
2819
|
+
const keys = candidate.rows.filter((row) => !previous.byKey.has(row.key)).map((row) => row.key);
|
|
2820
|
+
if (!keys.length) return reject(rowProblem("No unique rows can be inserted."));
|
|
2821
|
+
data = next;
|
|
2822
|
+
lazyChildren.clear();
|
|
2823
|
+
for (const [key, value] of cache) lazyChildren.set(key, value);
|
|
2824
|
+
lazyVersion++;
|
|
2825
|
+
return publishRows(before, previous, keys, /* @__PURE__ */ new Map(), []);
|
|
2826
|
+
} catch (error) {
|
|
2827
|
+
return reject(rowProblem(error instanceof Error ? error.message : String(error)));
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
function removeRows(keys) {
|
|
2831
|
+
const before = scene();
|
|
2832
|
+
const previous = sourceRows();
|
|
2833
|
+
const problems = [];
|
|
2834
|
+
const removed = /* @__PURE__ */ new Set();
|
|
2835
|
+
const { byId } = resolveColumns(definitions);
|
|
2836
|
+
const model = treeEnabled ? treeStage(data, byId, lazyVersion) : void 0;
|
|
2837
|
+
if (!stableRows(data)) {
|
|
2838
|
+
const problem = rowProblem("Row operations require stable row keys.");
|
|
2839
|
+
if (!transaction) options.onEditIssues?.([problem]);
|
|
2840
|
+
return resultForRows([], [problem]);
|
|
2841
|
+
}
|
|
2842
|
+
for (const key of keys) {
|
|
2843
|
+
if (!previous.byKey.has(key)) {
|
|
2844
|
+
problems.push(issue("unknown_row", "The row to remove does not exist.", { rowKey: key }));
|
|
2845
|
+
continue;
|
|
2846
|
+
}
|
|
2847
|
+
const node = model?.byKey.get(key);
|
|
2848
|
+
if (node && !node.writable) {
|
|
2849
|
+
problems.push(rowProblem("A recovered row has no writable source path.", key));
|
|
2850
|
+
continue;
|
|
2851
|
+
}
|
|
2852
|
+
const stack = node ? [node] : [];
|
|
2853
|
+
removed.add(key);
|
|
2854
|
+
while (stack.length) {
|
|
2855
|
+
const item = stack.pop();
|
|
2856
|
+
removed.add(item.row.key);
|
|
2857
|
+
for (const child of item.children) stack.push(child);
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
if (!removed.size) {
|
|
2861
|
+
if (problems.length && !transaction) options.onEditIssues?.(problems);
|
|
2862
|
+
return resultForRows([], problems);
|
|
2863
|
+
}
|
|
2864
|
+
const positions = new Map([...removed].map((key) => [key, positionOf(key)]));
|
|
2865
|
+
const cache = new Map(lazyChildren);
|
|
2866
|
+
try {
|
|
2867
|
+
const baselines = removalBaselines();
|
|
2868
|
+
const next = options.getChildren && model ? rewriteNested(model, removed, /* @__PURE__ */ new Map(), cache) : data.filter((row, index) => !removed.has(readKey(row, index)));
|
|
2869
|
+
for (const [key, children] of cache) if (removed.has(key)) cache.delete(key);
|
|
2870
|
+
else cache.set(key, children.filter((row, index) => !removed.has(readKey(row, index))));
|
|
2871
|
+
data = next;
|
|
2872
|
+
lazyChildren.clear();
|
|
2873
|
+
for (const [key, value] of cache) lazyChildren.set(key, value);
|
|
2874
|
+
lazyVersion++;
|
|
2875
|
+
validation.cancel([...removed]);
|
|
2876
|
+
abortLoads();
|
|
2877
|
+
return publishRows(before, previous, [...removed], positions, problems, baselines);
|
|
2878
|
+
} catch (error) {
|
|
2879
|
+
problems.push(rowProblem(error instanceof Error ? error.message : String(error)));
|
|
2880
|
+
if (!transaction) options.onEditIssues?.(problems);
|
|
2881
|
+
return resultForRows([], problems);
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
function revert(keys) {
|
|
2885
|
+
const selected = new Set(keys ?? pendingChanges.entries.keys());
|
|
2886
|
+
const entries = [...pendingChanges.entries].filter(([key]) => selected.has(key)).map(([key, entry]) => [key, {
|
|
2887
|
+
...entry,
|
|
2888
|
+
cells: new Map(entry.cells)
|
|
2889
|
+
}]);
|
|
2890
|
+
if (!entries.length) return resultForRows([], []);
|
|
2891
|
+
const before = scene();
|
|
2892
|
+
const previous = sourceRows();
|
|
2893
|
+
const positions = new Map([...selected].map((key) => [key, positionOf(key)]));
|
|
2894
|
+
const problems = [];
|
|
2895
|
+
const cells = [];
|
|
2896
|
+
transaction = true;
|
|
2897
|
+
try {
|
|
2898
|
+
problems.push(...removeRows(entries.filter(([, entry]) => entry.previous === void 0).map(([key]) => key)).issues);
|
|
2899
|
+
const missing = entries.filter(([, entry]) => entry.previous !== void 0 && entry.row === void 0);
|
|
2900
|
+
for (let pass = 0; missing.length && pass <= entries.length; pass++) {
|
|
2901
|
+
let progress = false;
|
|
2902
|
+
for (let index = missing.length - 1; index >= 0; index--) {
|
|
2903
|
+
const [key, entry] = missing[index];
|
|
2904
|
+
if (sourceRows().byKey.has(key)) {
|
|
2905
|
+
missing.splice(index, 1);
|
|
2906
|
+
progress = true;
|
|
2907
|
+
continue;
|
|
2908
|
+
}
|
|
2909
|
+
if (entry.position.parent !== void 0 && !sourceRows().byKey.has(entry.position.parent)) continue;
|
|
2910
|
+
const at = {
|
|
2911
|
+
...entry.position,
|
|
2912
|
+
before: entry.position.before !== void 0 && sourceRows().byKey.has(entry.position.before) ? entry.position.before : void 0
|
|
2913
|
+
};
|
|
2914
|
+
const result = insertRows([entry.previous], at);
|
|
2915
|
+
problems.push(...result.issues);
|
|
2916
|
+
missing.splice(index, 1);
|
|
2917
|
+
progress = true;
|
|
2918
|
+
}
|
|
2919
|
+
if (!progress) break;
|
|
2920
|
+
}
|
|
2921
|
+
for (const [key] of missing) problems.push(rowProblem("The saved parent is missing; restore it before this child.", key));
|
|
2922
|
+
const result = applyChanges(entries.flatMap(([key, entry]) => entry.previous !== void 0 && sourceRows().byKey.has(key) ? [...entry.cells.values()].map((change) => ({
|
|
2923
|
+
rowKey: key,
|
|
2924
|
+
column: change.column,
|
|
2925
|
+
value: change.previous
|
|
2926
|
+
})) : []), false);
|
|
2927
|
+
cells.push(...result.changes);
|
|
2928
|
+
problems.push(...result.issues);
|
|
2929
|
+
for (const [key] of entries) if (!problems.some((problem) => problem.rowKey === key)) pendingChanges.entries.delete(key);
|
|
2930
|
+
validation.cancel([...selected]);
|
|
2931
|
+
} finally {
|
|
2932
|
+
transaction = false;
|
|
2933
|
+
}
|
|
2934
|
+
const after = scene();
|
|
2935
|
+
if (before.data !== after.data || cells.length) undoStack.record({
|
|
2936
|
+
kind: "rows",
|
|
2937
|
+
before,
|
|
2938
|
+
after,
|
|
2939
|
+
keys: [...selected]
|
|
2940
|
+
});
|
|
2941
|
+
const next = sourceRows();
|
|
2942
|
+
const rowChanges = [...selected].flatMap((key) => {
|
|
2943
|
+
const old = previous.byKey.get(key);
|
|
2944
|
+
const row = next.byKey.get(key);
|
|
2945
|
+
return !old && row ? [{
|
|
2946
|
+
kind: "inserted",
|
|
2947
|
+
rowKey: key,
|
|
2948
|
+
row: row.original,
|
|
2949
|
+
position: positionOf(key)
|
|
2950
|
+
}] : old && !row ? [{
|
|
2951
|
+
kind: "removed",
|
|
2952
|
+
rowKey: key,
|
|
2953
|
+
row: old.original,
|
|
2954
|
+
position: positions.get(key)
|
|
2955
|
+
}] : [];
|
|
2956
|
+
});
|
|
2957
|
+
if (before.data !== after.data) options.onDataChange?.(data, cells);
|
|
2958
|
+
if (problems.length) options.onEditIssues?.(problems);
|
|
2959
|
+
notify();
|
|
2960
|
+
return resultForRows(rowChanges, problems, cells);
|
|
2961
|
+
}
|
|
981
2962
|
function rangeEdits(origin, matrix) {
|
|
982
2963
|
const { rows, columns } = getSnapshot();
|
|
983
2964
|
const edits = [];
|
|
@@ -996,13 +2977,356 @@ function createTable(options) {
|
|
|
996
2977
|
return edits;
|
|
997
2978
|
}
|
|
998
2979
|
function replay(changes, direction) {
|
|
999
|
-
applyChanges(changes.map((change) => ({
|
|
2980
|
+
applyChanges((direction === "undo" ? changes.toReversed() : changes).map((change) => ({
|
|
1000
2981
|
rowKey: change.rowKey,
|
|
1001
2982
|
column: change.column,
|
|
1002
2983
|
value: direction === "undo" ? change.previous : change.value
|
|
1003
2984
|
})), false);
|
|
1004
2985
|
}
|
|
2986
|
+
function ingestionResult(appended, replaced, removed, problems) {
|
|
2987
|
+
return Object.freeze({
|
|
2988
|
+
status: appended + replaced + removed ? problems.length ? "partial" : "applied" : problems.length ? "rejected" : "unchanged",
|
|
2989
|
+
appended,
|
|
2990
|
+
replaced,
|
|
2991
|
+
removed,
|
|
2992
|
+
issues: Object.freeze([...problems])
|
|
2993
|
+
});
|
|
2994
|
+
}
|
|
2995
|
+
const stableSource = /* @__PURE__ */ new WeakMap();
|
|
2996
|
+
function ingestionKeys(rows) {
|
|
2997
|
+
let valid = stableSource.get(rows);
|
|
2998
|
+
if (valid === void 0) {
|
|
2999
|
+
valid = stableRows(rows);
|
|
3000
|
+
stableSource.set(rows, valid);
|
|
3001
|
+
}
|
|
3002
|
+
return valid;
|
|
3003
|
+
}
|
|
3004
|
+
function isProtectedSource(key) {
|
|
3005
|
+
return pendingChanges.entries.has(key) || validation.hasPending(key);
|
|
3006
|
+
}
|
|
3007
|
+
function affectedBranch(model, key) {
|
|
3008
|
+
const result = [key];
|
|
3009
|
+
const node = model?.byKey.get(key);
|
|
3010
|
+
if (!node) return result;
|
|
3011
|
+
let parent = node.parent;
|
|
3012
|
+
while (parent) {
|
|
3013
|
+
result.push(parent.row.key);
|
|
3014
|
+
parent = parent.parent;
|
|
3015
|
+
}
|
|
3016
|
+
const stack = [...node.children];
|
|
3017
|
+
while (stack.length) {
|
|
3018
|
+
const child = stack.pop();
|
|
3019
|
+
result.push(child.row.key);
|
|
3020
|
+
for (const descendant of child.children) stack.push(descendant);
|
|
3021
|
+
}
|
|
3022
|
+
return result;
|
|
3023
|
+
}
|
|
3024
|
+
function transformScene(source, updates, removed) {
|
|
3025
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3026
|
+
const replace = (rows) => rows.flatMap((row, index) => {
|
|
3027
|
+
const key = readKey(row, index);
|
|
3028
|
+
seen.add(key);
|
|
3029
|
+
return removed.has(key) ? [] : [updates.get(key) ?? row];
|
|
3030
|
+
});
|
|
3031
|
+
const cache = /* @__PURE__ */ new Map();
|
|
3032
|
+
for (const [key, children] of source.lazy) if (!removed.has(key)) cache.set(key, replace(children));
|
|
3033
|
+
let next;
|
|
3034
|
+
if (!options.getChildren) next = replace(source.data);
|
|
3035
|
+
else {
|
|
3036
|
+
const model = buildTree(source.data, resolveColumns(definitions).byId, readKey, options, source.lazy);
|
|
3037
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
3038
|
+
for (const node of model.nodes.toReversed()) {
|
|
3039
|
+
const key = node.row.key;
|
|
3040
|
+
seen.add(key);
|
|
3041
|
+
if (removed.has(key)) continue;
|
|
3042
|
+
let row = updates.get(key) ?? node.row.original;
|
|
3043
|
+
const supplied = updates.has(key) ? options.getChildren(row) : void 0;
|
|
3044
|
+
if (supplied !== void 0) cache.delete(key);
|
|
3045
|
+
const children = supplied ?? node.children.filter((child) => !removed.has(child.row.key)).map((child) => replacements.get(child) ?? child.row.original);
|
|
3046
|
+
const old = options.getChildren(row) ?? node.children.map((child) => child.row.original);
|
|
3047
|
+
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]))) {
|
|
3048
|
+
if (!options.setChildren) throw new Error("Updating nested source children requires setChildren.");
|
|
3049
|
+
row = options.setChildren(row, children);
|
|
3050
|
+
if (cache.has(key)) cache.set(key, children);
|
|
3051
|
+
}
|
|
3052
|
+
replacements.set(node, row);
|
|
3053
|
+
}
|
|
3054
|
+
const roots = new Map(model.roots.filter((node) => node.writable).map((node) => [node.sourceIndex, node]));
|
|
3055
|
+
next = source.data.flatMap((row, index) => {
|
|
3056
|
+
const node = roots.get(index);
|
|
3057
|
+
return node && removed.has(node.row.key) ? [] : [node ? replacements.get(node) ?? row : row];
|
|
3058
|
+
});
|
|
3059
|
+
}
|
|
3060
|
+
const additions = [...updates].filter(([key]) => !seen.has(key) && !removed.has(key)).map(([, row]) => row);
|
|
3061
|
+
return {
|
|
3062
|
+
data: appendList(next, additions),
|
|
3063
|
+
lazy: cache
|
|
3064
|
+
};
|
|
3065
|
+
}
|
|
3066
|
+
function rebaseBatches(transform, removed) {
|
|
3067
|
+
undoStack.transform((entry) => {
|
|
3068
|
+
if (entry.kind === "cells") {
|
|
3069
|
+
if (!removed.size) return entry;
|
|
3070
|
+
const changes = entry.changes.filter((change) => !removed.has(change.rowKey));
|
|
3071
|
+
return changes.length ? {
|
|
3072
|
+
...entry,
|
|
3073
|
+
changes
|
|
3074
|
+
} : void 0;
|
|
3075
|
+
}
|
|
3076
|
+
const keys = entry.keys.filter((key) => !removed.has(key));
|
|
3077
|
+
return keys.length ? {
|
|
3078
|
+
...entry,
|
|
3079
|
+
keys,
|
|
3080
|
+
before: transform(entry.before),
|
|
3081
|
+
after: transform(entry.after)
|
|
3082
|
+
} : void 0;
|
|
3083
|
+
});
|
|
3084
|
+
}
|
|
3085
|
+
function newTreeIssues(previous, next) {
|
|
3086
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3087
|
+
for (const problem of previous) {
|
|
3088
|
+
const key = JSON.stringify([problem.code, problem.rowKey]);
|
|
3089
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
3090
|
+
}
|
|
3091
|
+
return next.filter((problem) => {
|
|
3092
|
+
const key = JSON.stringify([problem.code, problem.rowKey]);
|
|
3093
|
+
const remaining = counts.get(key) ?? 0;
|
|
3094
|
+
if (!remaining) return true;
|
|
3095
|
+
counts.set(key, remaining - 1);
|
|
3096
|
+
return false;
|
|
3097
|
+
});
|
|
3098
|
+
}
|
|
3099
|
+
function ingestData(incoming, mode) {
|
|
3100
|
+
const problems = [];
|
|
3101
|
+
const reject = (problem) => {
|
|
3102
|
+
ingestionIssues = [problem];
|
|
3103
|
+
notify();
|
|
3104
|
+
return ingestionResult(0, 0, 0, ingestionIssues);
|
|
3105
|
+
};
|
|
3106
|
+
if (destroyed) return reject(issue("invalid_ingestion", "A disposed table cannot receive source data."));
|
|
3107
|
+
if (!incoming.length) return ingestionResult(0, 0, 0, []);
|
|
3108
|
+
try {
|
|
3109
|
+
if (!ingestionKeys(data) || !stableRows(incoming)) return reject(issue("invalid_ingestion", "Source ingestion requires stable rowKey values or ids on every row."));
|
|
3110
|
+
} catch (error) {
|
|
3111
|
+
return reject(issue("invalid_ingestion", error instanceof Error ? error.message : String(error)));
|
|
3112
|
+
}
|
|
3113
|
+
const previous = sourceRows();
|
|
3114
|
+
const { byId, columns } = resolveColumns(definitions);
|
|
3115
|
+
const model = treeEnabled ? treeStage(data, byId, lazyVersion) : void 0;
|
|
3116
|
+
const seenBatch = /* @__PURE__ */ new Set();
|
|
3117
|
+
const updates = /* @__PURE__ */ new Map();
|
|
3118
|
+
for (const row of incoming) try {
|
|
3119
|
+
const key = readKey(row, previous.rows.length + updates.size);
|
|
3120
|
+
if (typeof key !== "string" && (typeof key !== "number" || !Number.isFinite(key))) {
|
|
3121
|
+
problems.push(issue("invalid_ingestion", "An incoming row key must be a string or finite number."));
|
|
3122
|
+
continue;
|
|
3123
|
+
}
|
|
3124
|
+
const existing = previous.byKey.get(key);
|
|
3125
|
+
if (seenBatch.has(key) || mode === "append" && existing) {
|
|
3126
|
+
problems.push(issue("duplicate_row_key", "An incoming source row repeats an existing key and is refused.", { rowKey: key }));
|
|
3127
|
+
continue;
|
|
3128
|
+
}
|
|
3129
|
+
seenBatch.add(key);
|
|
3130
|
+
const parent = options.getParentKey?.(row);
|
|
3131
|
+
const affected = affectedBranch(model, key);
|
|
3132
|
+
if (parent !== void 0 && parent !== null) affected.push(...affectedBranch(model, parent));
|
|
3133
|
+
if (affected.some(isProtectedSource)) {
|
|
3134
|
+
problems.push(issue("ingestion_conflict", "Incoming source data would overwrite a local change or pending validation; retry after save or revert.", { rowKey: key }));
|
|
3135
|
+
continue;
|
|
3136
|
+
}
|
|
3137
|
+
const node = model?.byKey.get(key);
|
|
3138
|
+
if (node && (!node.writable || node.lazy && options.getParentKey && options.getParentKey(row) !== node.parent?.row.key)) {
|
|
3139
|
+
problems.push(issue("invalid_ingestion", "This recovered or lazy tree row has no writable source position.", { rowKey: key }));
|
|
3140
|
+
continue;
|
|
3141
|
+
}
|
|
3142
|
+
if (existing?.original !== row) updates.set(key, row);
|
|
3143
|
+
} catch (error) {
|
|
3144
|
+
problems.push(issue("invalid_ingestion", error instanceof Error ? error.message : String(error)));
|
|
3145
|
+
}
|
|
3146
|
+
if (!updates.size) {
|
|
3147
|
+
ingestionIssues = problems;
|
|
3148
|
+
notify();
|
|
3149
|
+
return ingestionResult(0, 0, 0, problems);
|
|
3150
|
+
}
|
|
3151
|
+
const replaced = [...updates.keys()].filter((key) => previous.byKey.has(key)).length;
|
|
3152
|
+
const appended = updates.size - replaced;
|
|
3153
|
+
const previousState = state;
|
|
3154
|
+
try {
|
|
3155
|
+
if (!model && !replaced) {
|
|
3156
|
+
const added = Object.freeze([...updates.values()]);
|
|
3157
|
+
const indexed = added.map((row, offset) => createRow(row, readKey(row, previous.rows.length + offset), previous.rows.length + offset, byId, options.getRowCanExpand?.(row) ?? false));
|
|
3158
|
+
const filtered = manual ? previous.rows : filterStage(previous.rows, columns, state.search, state.filters);
|
|
3159
|
+
const addedFiltered = manual ? indexed : filterRows(indexed, columns, state.search, state.filters);
|
|
3160
|
+
const allRows = appendList(previous.rows, indexed);
|
|
3161
|
+
const allFiltered = appendList(filtered, addedFiltered);
|
|
3162
|
+
const keys = membership(filtered);
|
|
3163
|
+
for (const row of addedFiltered) keys.add(row.key);
|
|
3164
|
+
rowMembership.set(allFiltered, keys);
|
|
3165
|
+
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);
|
|
3166
|
+
const next = appendList(data, added);
|
|
3167
|
+
rebaseBatches((source) => ({
|
|
3168
|
+
...source,
|
|
3169
|
+
data: appendList(source.data, added)
|
|
3170
|
+
}), /* @__PURE__ */ new Set());
|
|
3171
|
+
for (const row of indexed) previous.byKey.set(row.key, row);
|
|
3172
|
+
primedIndex = {
|
|
3173
|
+
data: next,
|
|
3174
|
+
columns: byId,
|
|
3175
|
+
result: {
|
|
3176
|
+
rows: allRows,
|
|
3177
|
+
byKey: previous.byKey,
|
|
3178
|
+
issues: previous.issues
|
|
3179
|
+
}
|
|
3180
|
+
};
|
|
3181
|
+
primedFilter = {
|
|
3182
|
+
rows: allRows,
|
|
3183
|
+
columns,
|
|
3184
|
+
search: state.search,
|
|
3185
|
+
filters: state.filters,
|
|
3186
|
+
result: allFiltered
|
|
3187
|
+
};
|
|
3188
|
+
primedSort = {
|
|
3189
|
+
rows: allFiltered,
|
|
3190
|
+
columns,
|
|
3191
|
+
sorting: state.sorting,
|
|
3192
|
+
result: sorted
|
|
3193
|
+
};
|
|
3194
|
+
stableSource.set(next, true);
|
|
3195
|
+
data = next;
|
|
3196
|
+
} else {
|
|
3197
|
+
const next = transformScene(scene(), updates, /* @__PURE__ */ new Set());
|
|
3198
|
+
const candidate = treeEnabled ? buildTree(next.data, byId, readKey, options, next.lazy) : indexRows(next.data, byId);
|
|
3199
|
+
const fresh = newTreeIssues(previous.issues, candidate.issues);
|
|
3200
|
+
if (fresh.length || !stableRows(candidate.rows.map((row) => row.original))) {
|
|
3201
|
+
problems.push(...fresh, ...!stableRows(candidate.rows.map((row) => row.original)) ? [issue("invalid_ingestion", "Every incoming descendant needs a stable key.")] : []);
|
|
3202
|
+
ingestionIssues = problems;
|
|
3203
|
+
notify();
|
|
3204
|
+
return ingestionResult(0, 0, 0, problems);
|
|
3205
|
+
}
|
|
3206
|
+
const nextKeys = new Set(candidate.rows.map((row) => row.key));
|
|
3207
|
+
const removedKeys = new Set(previous.rows.filter((row) => !nextKeys.has(row.key)).map((row) => row.key));
|
|
3208
|
+
rebaseBatches((source) => transformScene(source, updates, removedKeys), removedKeys);
|
|
3209
|
+
data = next.data;
|
|
3210
|
+
lazyChildren.clear();
|
|
3211
|
+
for (const [key, rows] of next.lazy) lazyChildren.set(key, rows);
|
|
3212
|
+
lazyVersion++;
|
|
3213
|
+
for (const key of [...updates.keys(), ...removedKeys]) {
|
|
3214
|
+
pendingLoads.get(key)?.abort();
|
|
3215
|
+
pendingLoads.delete(key);
|
|
3216
|
+
childStatuses.delete(key);
|
|
3217
|
+
loadIssues.delete(key);
|
|
3218
|
+
loadValidationIssues.delete(key);
|
|
3219
|
+
}
|
|
3220
|
+
validation.cancel([...removedKeys]);
|
|
3221
|
+
const selection = state.selection.filter((key) => !removedKeys.has(key));
|
|
3222
|
+
state = mergeState(state, {
|
|
3223
|
+
selection: selection.length === state.selection.length ? state.selection : selection,
|
|
3224
|
+
expanded: state.expanded === true ? true : state.expanded.filter((key) => !removedKeys.has(key))
|
|
3225
|
+
});
|
|
3226
|
+
}
|
|
3227
|
+
validation.cancel([...updates.keys()]);
|
|
3228
|
+
ingestionIssues = problems;
|
|
3229
|
+
} catch (error) {
|
|
3230
|
+
return reject(issue("invalid_ingestion", error instanceof Error ? error.message : String(error)));
|
|
3231
|
+
}
|
|
3232
|
+
if (state !== previousState) options.onStateChange?.(state, previousState);
|
|
3233
|
+
options.onDataChange?.(data, []);
|
|
3234
|
+
notify();
|
|
3235
|
+
startVisibleLoads();
|
|
3236
|
+
return ingestionResult(appended, replaced, 0, problems);
|
|
3237
|
+
}
|
|
3238
|
+
function removeData(keys) {
|
|
3239
|
+
const previousState = state;
|
|
3240
|
+
const problems = [];
|
|
3241
|
+
const removed = /* @__PURE__ */ new Set();
|
|
3242
|
+
const previous = sourceRows();
|
|
3243
|
+
const model = treeEnabled ? treeStage(data, resolveColumns(definitions).byId, lazyVersion) : void 0;
|
|
3244
|
+
if (destroyed || !ingestionKeys(data)) problems.push(issue("invalid_ingestion", "Source removal requires a live table and stable row keys."));
|
|
3245
|
+
else for (const key of new Set(keys)) {
|
|
3246
|
+
if (!previous.byKey.has(key)) {
|
|
3247
|
+
problems.push(issue("unknown_row", "The source row to remove does not exist.", { rowKey: key }));
|
|
3248
|
+
continue;
|
|
3249
|
+
}
|
|
3250
|
+
if (affectedBranch(model, key).some(isProtectedSource)) {
|
|
3251
|
+
problems.push(issue("ingestion_conflict", "Source removal would discard local changes or pending validation.", { rowKey: key }));
|
|
3252
|
+
continue;
|
|
3253
|
+
}
|
|
3254
|
+
const node = model?.byKey.get(key);
|
|
3255
|
+
if (node && !node.writable) {
|
|
3256
|
+
problems.push(issue("invalid_ingestion", "A recovered tree row has no writable source position.", { rowKey: key }));
|
|
3257
|
+
continue;
|
|
3258
|
+
}
|
|
3259
|
+
removed.add(key);
|
|
3260
|
+
const stack = node ? [...node.children] : [];
|
|
3261
|
+
while (stack.length) {
|
|
3262
|
+
const child = stack.pop();
|
|
3263
|
+
removed.add(child.row.key);
|
|
3264
|
+
for (const descendant of child.children) stack.push(descendant);
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
try {
|
|
3268
|
+
if (removed.size) {
|
|
3269
|
+
const next = transformScene(scene(), /* @__PURE__ */ new Map(), removed);
|
|
3270
|
+
rebaseBatches((source) => transformScene(source, /* @__PURE__ */ new Map(), removed), removed);
|
|
3271
|
+
data = next.data;
|
|
3272
|
+
lazyChildren.clear();
|
|
3273
|
+
for (const [key, rows] of next.lazy) lazyChildren.set(key, rows);
|
|
3274
|
+
lazyVersion++;
|
|
3275
|
+
validation.cancel([...removed]);
|
|
3276
|
+
for (const key of removed) {
|
|
3277
|
+
pendingLoads.get(key)?.abort();
|
|
3278
|
+
pendingLoads.delete(key);
|
|
3279
|
+
childStatuses.delete(key);
|
|
3280
|
+
loadIssues.delete(key);
|
|
3281
|
+
loadValidationIssues.delete(key);
|
|
3282
|
+
}
|
|
3283
|
+
const selection = state.selection.filter((key) => !removed.has(key));
|
|
3284
|
+
state = mergeState(state, {
|
|
3285
|
+
selection: selection.length === state.selection.length ? state.selection : selection,
|
|
3286
|
+
expanded: state.expanded === true ? true : state.expanded.filter((key) => !removed.has(key))
|
|
3287
|
+
});
|
|
3288
|
+
}
|
|
3289
|
+
ingestionIssues = problems;
|
|
3290
|
+
} catch (error) {
|
|
3291
|
+
ingestionIssues = [issue("invalid_ingestion", error instanceof Error ? error.message : String(error))];
|
|
3292
|
+
notify();
|
|
3293
|
+
return ingestionResult(0, 0, 0, ingestionIssues);
|
|
3294
|
+
}
|
|
3295
|
+
if (state !== previousState) options.onStateChange?.(state, previousState);
|
|
3296
|
+
if (removed.size) options.onDataChange?.(data, []);
|
|
3297
|
+
notify();
|
|
3298
|
+
startVisibleLoads();
|
|
3299
|
+
return ingestionResult(0, 0, removed.size, problems);
|
|
3300
|
+
}
|
|
3301
|
+
if (expandMode === "single") {
|
|
3302
|
+
initialState = mergeState(initialState, { expanded: canonicalExpanded(initialState.expanded) });
|
|
3303
|
+
state = initialState;
|
|
3304
|
+
snapshot = void 0;
|
|
3305
|
+
}
|
|
1005
3306
|
const table = {
|
|
3307
|
+
appendData: (rows) => ingestData(rows, "append"),
|
|
3308
|
+
upsertData: (rows) => ingestData(rows, "upsert"),
|
|
3309
|
+
removeData,
|
|
3310
|
+
stream(source, streamOptions) {
|
|
3311
|
+
if (destroyed) return Promise.resolve(Object.freeze({
|
|
3312
|
+
status: "aborted",
|
|
3313
|
+
receivedRowCount: 0,
|
|
3314
|
+
issues: Object.freeze([issue("invalid_ingestion", "A disposed table cannot start a source.")])
|
|
3315
|
+
}));
|
|
3316
|
+
return streaming.run(source, {
|
|
3317
|
+
expectedRowCount: sourceExpected,
|
|
3318
|
+
...streamOptions
|
|
3319
|
+
});
|
|
3320
|
+
},
|
|
3321
|
+
insertRows,
|
|
3322
|
+
removeRows,
|
|
3323
|
+
revert,
|
|
3324
|
+
getPendingChanges: () => pendingChanges.snapshot(),
|
|
3325
|
+
markSaved(keys) {
|
|
3326
|
+
if (!keys) pendingChanges.entries.clear();
|
|
3327
|
+
else for (const key of keys) pendingChanges.entries.delete(key);
|
|
3328
|
+
notify();
|
|
3329
|
+
},
|
|
1006
3330
|
getSnapshot,
|
|
1007
3331
|
getState: () => state,
|
|
1008
3332
|
subscribe(listener) {
|
|
@@ -1012,21 +3336,40 @@ function createTable(options) {
|
|
|
1012
3336
|
};
|
|
1013
3337
|
},
|
|
1014
3338
|
setState(patch) {
|
|
1015
|
-
const
|
|
1016
|
-
|
|
3339
|
+
const previousIssue = expandedIssue;
|
|
3340
|
+
const next = mergePatch(patch);
|
|
3341
|
+
if (next !== state || previousIssue !== expandedIssue) {
|
|
1017
3342
|
state = next;
|
|
3343
|
+
snapshot = void 0;
|
|
3344
|
+
abortLoads();
|
|
1018
3345
|
notify();
|
|
3346
|
+
startVisibleLoads();
|
|
1019
3347
|
}
|
|
1020
3348
|
},
|
|
1021
3349
|
setData(next) {
|
|
1022
3350
|
if (next === data) return;
|
|
3351
|
+
validation.cancel();
|
|
3352
|
+
streaming.cancel();
|
|
3353
|
+
loadState = "idle";
|
|
3354
|
+
streamIssues = [];
|
|
3355
|
+
ingestionIssues = [];
|
|
3356
|
+
expectedRowCount = sourceExpected;
|
|
3357
|
+
pendingChanges.entries.clear();
|
|
3358
|
+
abortLoads(true);
|
|
3359
|
+
lazyChildren.clear();
|
|
3360
|
+
childStatuses.clear();
|
|
3361
|
+
loadIssues.clear();
|
|
3362
|
+
loadValidationIssues.clear();
|
|
3363
|
+
lazyVersion++;
|
|
1023
3364
|
data = next;
|
|
1024
3365
|
undoStack.clear();
|
|
1025
3366
|
notify();
|
|
3367
|
+
startVisibleLoads();
|
|
1026
3368
|
},
|
|
1027
3369
|
setColumns(next) {
|
|
1028
3370
|
if (next === definitions) return;
|
|
1029
3371
|
definitions = next;
|
|
3372
|
+
validation.cancel();
|
|
1030
3373
|
notify();
|
|
1031
3374
|
},
|
|
1032
3375
|
setRowCount(next) {
|
|
@@ -1069,6 +3412,7 @@ function createTable(options) {
|
|
|
1069
3412
|
});
|
|
1070
3413
|
},
|
|
1071
3414
|
goToPage(page) {
|
|
3415
|
+
if (!paginate) return;
|
|
1072
3416
|
const { pageCount, page: currentPage } = getSnapshot();
|
|
1073
3417
|
const next = clampPage(page, pageCount);
|
|
1074
3418
|
if (next !== currentPage || next !== state.pagination.page) commit({ pagination: {
|
|
@@ -1083,6 +3427,7 @@ function createTable(options) {
|
|
|
1083
3427
|
table.goToPage(getSnapshot().page - 1);
|
|
1084
3428
|
},
|
|
1085
3429
|
setPageSize(pageSize) {
|
|
3430
|
+
if (!paginate) return;
|
|
1086
3431
|
if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize === state.pagination.pageSize) return;
|
|
1087
3432
|
const { pageStart } = getSnapshot();
|
|
1088
3433
|
commit({ pagination: {
|
|
@@ -1091,14 +3436,14 @@ function createTable(options) {
|
|
|
1091
3436
|
} });
|
|
1092
3437
|
},
|
|
1093
3438
|
select(keys) {
|
|
1094
|
-
applySelection([...state.selection, ...keys]);
|
|
3439
|
+
applySelection([...state.selection, ...selectionKeys(keys)]);
|
|
1095
3440
|
},
|
|
1096
3441
|
deselect(keys) {
|
|
1097
|
-
const removed = new Set(keys);
|
|
3442
|
+
const removed = new Set(selectionKeys(keys));
|
|
1098
3443
|
applySelection(state.selection.filter((key) => !removed.has(key)));
|
|
1099
3444
|
},
|
|
1100
3445
|
toggleRow(key, selected) {
|
|
1101
|
-
const isSelected = state.selection.includes(key);
|
|
3446
|
+
const isSelected = treeEnabled ? getSnapshot().getRow(key)?.selection === "all" : state.selection.includes(key);
|
|
1102
3447
|
const next = selected ?? !isSelected;
|
|
1103
3448
|
if (next === isSelected) return;
|
|
1104
3449
|
if (next) table.select([key]);
|
|
@@ -1121,6 +3466,27 @@ function createTable(options) {
|
|
|
1121
3466
|
return row ? [row] : [];
|
|
1122
3467
|
});
|
|
1123
3468
|
},
|
|
3469
|
+
toggleExpanded(key, expanded) {
|
|
3470
|
+
const row = getSnapshot().getRow(key);
|
|
3471
|
+
if (!row?.canExpand) return;
|
|
3472
|
+
const open = expanded ?? !row.isExpanded;
|
|
3473
|
+
if (open === row.isExpanded) {
|
|
3474
|
+
if (open && row.childStatus === "error") requestChildren(row);
|
|
3475
|
+
return;
|
|
3476
|
+
}
|
|
3477
|
+
if (expandMode === "single") commit({ expanded: open ? [key] : [] });
|
|
3478
|
+
else if (state.expanded === true) {
|
|
3479
|
+
const { byId } = resolveColumns(definitions);
|
|
3480
|
+
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) });
|
|
3481
|
+
} else commit({ expanded: open ? [...state.expanded, key] : state.expanded.filter((candidate) => candidate !== key) });
|
|
3482
|
+
if (open) requestChildren(getSnapshot().getRow(key));
|
|
3483
|
+
},
|
|
3484
|
+
expandAll() {
|
|
3485
|
+
commit({ expanded: true });
|
|
3486
|
+
},
|
|
3487
|
+
collapseAll() {
|
|
3488
|
+
commit({ expanded: [] });
|
|
3489
|
+
},
|
|
1124
3490
|
toggleColumn(columnId, visible) {
|
|
1125
3491
|
if (!getSnapshot().getColumn(columnId)) return;
|
|
1126
3492
|
const hidden = state.hiddenColumns.includes(columnId);
|
|
@@ -1138,22 +3504,26 @@ function createTable(options) {
|
|
|
1138
3504
|
order.splice(target, 0, columnId);
|
|
1139
3505
|
commit({ columnOrder: order });
|
|
1140
3506
|
},
|
|
1141
|
-
edit(edits) {
|
|
1142
|
-
return
|
|
3507
|
+
edit(edits, editOptions) {
|
|
3508
|
+
return validation.edit(Array.isArray(edits) ? edits : [edits], [], editOptions);
|
|
1143
3509
|
},
|
|
1144
3510
|
undo() {
|
|
1145
|
-
const
|
|
1146
|
-
if (!
|
|
1147
|
-
|
|
3511
|
+
const entry = undoStack.undo();
|
|
3512
|
+
if (!entry) return false;
|
|
3513
|
+
validation.cancel();
|
|
3514
|
+
if (entry.kind === "cells") replay(entry.changes, "undo");
|
|
3515
|
+
else restoreScene(entry, "undo");
|
|
1148
3516
|
return true;
|
|
1149
3517
|
},
|
|
1150
3518
|
redo() {
|
|
1151
|
-
const
|
|
1152
|
-
if (!
|
|
1153
|
-
|
|
3519
|
+
const entry = undoStack.redo();
|
|
3520
|
+
if (!entry) return false;
|
|
3521
|
+
validation.cancel();
|
|
3522
|
+
if (entry.kind === "cells") replay(entry.changes, "redo");
|
|
3523
|
+
else restoreScene(entry, "redo");
|
|
1154
3524
|
return true;
|
|
1155
3525
|
},
|
|
1156
|
-
copy(range) {
|
|
3526
|
+
copy(range, { escapeFormulas = false } = {}) {
|
|
1157
3527
|
const { rows, columns } = getSnapshot();
|
|
1158
3528
|
const matrix = [];
|
|
1159
3529
|
rows.forEach((row, rowIndex) => {
|
|
@@ -1162,46 +3532,72 @@ function createTable(options) {
|
|
|
1162
3532
|
if (rangeContains(range, {
|
|
1163
3533
|
row: rowIndex,
|
|
1164
3534
|
column: columnIndex
|
|
1165
|
-
}))
|
|
3535
|
+
})) {
|
|
3536
|
+
const display = row.getDisplay(column.id);
|
|
3537
|
+
line.push(escapeFormulas ? escapeFormula(display, row.getValue(column.id)) : display);
|
|
3538
|
+
}
|
|
1166
3539
|
});
|
|
1167
3540
|
if (line.length > 0) matrix.push(line);
|
|
1168
3541
|
});
|
|
1169
3542
|
return toDelimited(matrix);
|
|
1170
3543
|
},
|
|
1171
3544
|
paste(origin, text) {
|
|
1172
|
-
|
|
3545
|
+
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.")]);
|
|
3546
|
+
const { rows, columns } = getSnapshot();
|
|
3547
|
+
const parsed = readDelimited(text, {
|
|
3548
|
+
maxRows: Math.max(0, rows.length - origin.row),
|
|
3549
|
+
maxColumns: Math.max(0, columns.length - origin.column),
|
|
3550
|
+
maxCells: maxPasteCells,
|
|
3551
|
+
maxLength: maxPasteLength
|
|
3552
|
+
});
|
|
3553
|
+
const issues = parsed.truncated ? [issue("paste_truncated", "The paste exceeds the visible grid or parsing limits; only complete cells within those limits are applied.")] : [];
|
|
3554
|
+
return validation.edit(rangeEdits(origin, parsed.rows), issues);
|
|
1173
3555
|
},
|
|
1174
3556
|
clear(range) {
|
|
1175
3557
|
const matrix = Array.from({ length: range.bottom - range.top + 1 }, () => Array.from({ length: range.right - range.left + 1 }, () => ""));
|
|
1176
3558
|
const current = getSnapshot();
|
|
1177
|
-
|
|
3559
|
+
const edits = rangeEdits({
|
|
1178
3560
|
row: range.top,
|
|
1179
3561
|
column: range.left
|
|
1180
3562
|
}, matrix).filter((edit) => {
|
|
1181
3563
|
const row = current.getRow(edit.rowKey);
|
|
1182
3564
|
return row !== void 0 && current.getColumn(edit.column)?.isEditable(row.original) === true;
|
|
1183
|
-
})
|
|
3565
|
+
});
|
|
3566
|
+
return validation.edit(edits);
|
|
1184
3567
|
},
|
|
1185
|
-
exportRows({ format = "csv", columns: ids, pageOnly = false, headers = true } = {}) {
|
|
3568
|
+
exportRows({ format = "csv", columns: ids, pageOnly = false, headers = true, escapeFormulas = true, depth = false, indent = "" } = {}) {
|
|
1186
3569
|
const current = getSnapshot();
|
|
1187
3570
|
const columns = ids ? ids.flatMap((id) => {
|
|
1188
3571
|
const column = current.getColumn(id);
|
|
1189
3572
|
return column ? [column] : [];
|
|
1190
3573
|
}) : current.columns;
|
|
1191
|
-
const
|
|
1192
|
-
|
|
3574
|
+
const rows = pageOnly ? current.rows : current.processedRows;
|
|
3575
|
+
const matrix = rows.map((row) => columns.map((column, index) => {
|
|
3576
|
+
const display = (index === 0 ? indent.repeat(row.depth) : "") + row.getDisplay(column.id);
|
|
3577
|
+
return escapeFormulas ? escapeFormula(display, row.getValue(column.id)) : display;
|
|
3578
|
+
}));
|
|
3579
|
+
if (depth) matrix.forEach((line, index) => line.unshift(String(rows[index].depth)));
|
|
3580
|
+
if (headers) {
|
|
3581
|
+
matrix.unshift(columns.map((column) => escapeFormulas ? escapeFormula(column.header) : column.header));
|
|
3582
|
+
if (depth) matrix[0].unshift("Depth");
|
|
3583
|
+
}
|
|
1193
3584
|
return toDelimited(matrix, format === "csv" ? "," : " ");
|
|
1194
3585
|
},
|
|
1195
3586
|
reset() {
|
|
1196
3587
|
commit(initialState);
|
|
1197
3588
|
},
|
|
1198
3589
|
destroy() {
|
|
3590
|
+
destroyed = true;
|
|
3591
|
+
streaming.cancel();
|
|
3592
|
+
validation.cancel();
|
|
3593
|
+
abortLoads(true);
|
|
1199
3594
|
listeners.clear();
|
|
1200
3595
|
}
|
|
1201
3596
|
};
|
|
3597
|
+
startVisibleLoads();
|
|
1202
3598
|
return table;
|
|
1203
3599
|
}
|
|
1204
3600
|
|
|
1205
3601
|
//#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 };
|
|
3602
|
+
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
3603
|
//# sourceMappingURL=index.js.map
|