@vueye-table/core 3.0.0-alpha.1 → 3.0.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -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 getValue = accessor ?? ((row) => getPath(row, def.id));
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 endRow = () => {
280
- endField();
281
- rows.push(row);
282
- row = [];
333
+ const append = (char) => {
334
+ if (column < limits.maxColumns) field += char;
283
335
  };
284
- while (index < text.length) {
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
- field += "\"";
340
+ if (index + 1 < end && text.charAt(index + 1) === "\"") {
341
+ append("\"");
290
342
  index += 1;
291
343
  } else quoted = false;
292
- } else field += char;
293
- } else if (char === "\"" && field === "") quoted = true;
294
- else if (char === delimiter) endField();
295
- else if (char === "\n" || char === "\r") {
296
- if (char === "\r" && text.charAt(index + 1) === "\n") index += 1;
297
- endRow();
298
- } else field += char;
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 (field !== "" || row.length > 0) endRow();
302
- return rows;
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
@@ -466,6 +551,196 @@ function createUndoStack(limit = 100) {
466
551
  };
467
552
  }
468
553
 
554
+ //#endregion
555
+ //#region src/virtualizer.ts
556
+ const FALLBACK_SIZE = 40;
557
+ /** Layout arithmetic for either axis. All viewport values and measurements come from the caller. */
558
+ function createVirtualizer(initialOptions) {
559
+ let options = initialOptions;
560
+ let count = 0;
561
+ let overscan = 5;
562
+ let fixedSize;
563
+ let keys = [];
564
+ let byKey = /* @__PURE__ */ new Map();
565
+ let sizes = [];
566
+ let prefix = [0];
567
+ let offset = 0;
568
+ let viewportSize = 0;
569
+ let cached;
570
+ const measured = /* @__PURE__ */ new Map();
571
+ const problems = /* @__PURE__ */ new Map();
572
+ const listeners = /* @__PURE__ */ new Set();
573
+ function recover(code, message) {
574
+ if (!problems.has(code)) {
575
+ const problem = issue(code, message);
576
+ problems.set(code, problem);
577
+ options.onIssue?.(problem);
578
+ cached = void 0;
579
+ }
580
+ }
581
+ function validSize(value) {
582
+ if (Number.isFinite(value) && value > 0) return value;
583
+ recover("invalid_virtual_size", `Item size ${String(value)} is not positive and finite; 40 is used.`);
584
+ return FALLBACK_SIZE;
585
+ }
586
+ function configure(next) {
587
+ options = next;
588
+ problems.clear();
589
+ count = Number.isSafeInteger(next.count) && next.count >= 0 ? next.count : 0;
590
+ if (count !== next.count) recover("invalid_virtual_option", "Virtual item count must be a non-negative safe integer; 0 is used.");
591
+ const requestedOverscan = next.overscan ?? 5;
592
+ overscan = Number.isSafeInteger(requestedOverscan) && requestedOverscan >= 0 ? requestedOverscan : 5;
593
+ if (overscan !== requestedOverscan) recover("invalid_virtual_option", "Overscan must be a non-negative safe integer; 5 is used.");
594
+ const estimate = typeof next.estimateSize === "number" ? validSize(next.estimateSize) : void 0;
595
+ const nextKeys = [];
596
+ const nextSizes = [];
597
+ const nextByKey = /* @__PURE__ */ new Map();
598
+ let firstChanged = count;
599
+ let uniform = estimate !== void 0;
600
+ for (let index = 0; index < count; index += 1) {
601
+ let key = next.getKey ? next.getKey(index) : index;
602
+ if (!(typeof key === "string" || typeof key === "number" && Number.isFinite(key))) {
603
+ recover("invalid_virtual_option", `Item ${index} has an invalid key; its index is used.`);
604
+ key = index;
605
+ }
606
+ if (nextByKey.has(key)) {
607
+ recover("duplicate_virtual_key", `Item ${index} repeats a key; a distinct fallback key is used.`);
608
+ key = JSON.stringify(["virtual", index]);
609
+ while (nextByKey.has(key)) key += ":";
610
+ }
611
+ const size = measured.get(key) ?? estimate ?? validSize(next.estimateSize(index));
612
+ uniform &&= size === estimate;
613
+ if (keys[index] !== key || sizes[index] !== size) firstChanged = Math.min(firstChanged, index);
614
+ nextKeys.push(key);
615
+ nextSizes.push(size);
616
+ nextByKey.set(key, index);
617
+ }
618
+ if (fixedSize !== void 0 || uniform) prefix = [0];
619
+ else prefix.length = Math.min(prefix.length, firstChanged + 1);
620
+ keys = nextKeys;
621
+ sizes = nextSizes;
622
+ byKey = nextByKey;
623
+ fixedSize = uniform ? estimate : void 0;
624
+ cached = void 0;
625
+ }
626
+ function startOf(index) {
627
+ if (fixedSize !== void 0) return index * fixedSize;
628
+ while (prefix.length <= index) {
629
+ const at = prefix.length - 1;
630
+ prefix.push(prefix[at] + sizes[at]);
631
+ }
632
+ return prefix[index];
633
+ }
634
+ function totalSize() {
635
+ return startOf(count);
636
+ }
637
+ function indexAt(position) {
638
+ if (fixedSize !== void 0) return Math.min(count - 1, Math.floor(position / fixedSize));
639
+ let low = 0;
640
+ let high = count - 1;
641
+ while (low < high) {
642
+ const middle = Math.floor((low + high) / 2);
643
+ if (startOf(middle + 1) <= position) low = middle + 1;
644
+ else high = middle;
645
+ }
646
+ return low;
647
+ }
648
+ function getWindow() {
649
+ if (cached) return cached;
650
+ const total = totalSize();
651
+ const clampedOffset = Math.min(offset, Math.max(0, total - viewportSize));
652
+ let first = 0;
653
+ let last = -1;
654
+ if (count > 0 && viewportSize > 0) {
655
+ first = Math.max(0, indexAt(clampedOffset) - overscan);
656
+ const edge = Math.min(total, clampedOffset + viewportSize);
657
+ let visibleEnd = indexAt(edge);
658
+ if (startOf(visibleEnd) >= edge) visibleEnd -= 1;
659
+ last = Math.min(count - 1, visibleEnd + overscan);
660
+ }
661
+ const items = [];
662
+ for (let index = first; index <= last; index += 1) items.push(Object.freeze({
663
+ index,
664
+ key: keys[index],
665
+ start: startOf(index),
666
+ size: sizes[index]
667
+ }));
668
+ const paddingStart = items.length > 0 ? startOf(first) : 0;
669
+ const paddingEnd = items.length > 0 ? Math.max(0, total - startOf(last + 1)) : total;
670
+ cached = Object.freeze({
671
+ items: Object.freeze(items),
672
+ totalSize: total,
673
+ paddingStart,
674
+ paddingEnd,
675
+ startIndex: first,
676
+ endIndex: last,
677
+ offset: clampedOffset,
678
+ viewportSize,
679
+ issues: Object.freeze([...problems.values()])
680
+ });
681
+ return cached;
682
+ }
683
+ function notify() {
684
+ cached = void 0;
685
+ if (listeners.size > 0) {
686
+ const view = getWindow();
687
+ for (const listener of listeners) listener(view);
688
+ }
689
+ }
690
+ configure(initialOptions);
691
+ return {
692
+ setOptions(next) {
693
+ configure(next);
694
+ notify();
695
+ },
696
+ setViewport(nextOffset, nextSize) {
697
+ const issueCount = problems.size;
698
+ const safeOffset = Number.isFinite(nextOffset) && nextOffset >= 0 ? nextOffset : 0;
699
+ const safeSize = Number.isFinite(nextSize) && nextSize >= 0 ? nextSize : 0;
700
+ if (safeOffset !== nextOffset || safeSize !== nextSize) recover("invalid_virtual_viewport", "Viewport offset and size must be non-negative and finite; invalid values use 0.");
701
+ if (offset === safeOffset && viewportSize === safeSize && problems.size === issueCount) return;
702
+ offset = safeOffset;
703
+ viewportSize = safeSize;
704
+ notify();
705
+ },
706
+ measure(key, size) {
707
+ const index = byKey.get(key);
708
+ if (index === void 0) return;
709
+ if (!Number.isFinite(size) || size <= 0) {
710
+ recover("invalid_virtual_size", "Measured size must be positive and finite; the previous size is retained.");
711
+ notify();
712
+ return;
713
+ }
714
+ measured.set(key, size);
715
+ if (sizes[index] === size) return;
716
+ if (fixedSize !== void 0) {
717
+ fixedSize = void 0;
718
+ prefix = [0];
719
+ } else prefix.length = Math.min(prefix.length, index + 1);
720
+ sizes[index] = size;
721
+ notify();
722
+ },
723
+ getWindow,
724
+ getOffsetForIndex(index, align = "auto") {
725
+ if (count === 0) return 0;
726
+ 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.");
727
+ const target = Math.min(count - 1, Math.max(0, Number.isFinite(index) ? Math.trunc(index) : 0));
728
+ const start = startOf(target);
729
+ const end = start + sizes[target];
730
+ const current = getWindow().offset;
731
+ let result = start;
732
+ if (align === "center") result = start - (viewportSize - sizes[target]) / 2;
733
+ else if (align === "end") result = end - viewportSize;
734
+ else if (align === "auto") result = start < current ? start : end > current + viewportSize ? end - viewportSize : current;
735
+ return Math.min(Math.max(0, totalSize() - viewportSize), Math.max(0, result));
736
+ },
737
+ subscribe(listener) {
738
+ listeners.add(listener);
739
+ return () => listeners.delete(listener);
740
+ }
741
+ };
742
+ }
743
+
469
744
  //#endregion
470
745
  //#region src/infer.ts
471
746
  function isPlainObject(value) {
@@ -593,10 +868,47 @@ function paginationItems(page, pageCount, siblings = 1) {
593
868
  return items;
594
869
  }
595
870
 
871
+ //#endregion
872
+ //#region src/row.ts
873
+ /** Disjoint namespaces and typed keys avoid collisions between 1, "1", and detail-like ids. */
874
+ function getRowItemKey(key, kind = "row") {
875
+ return JSON.stringify([
876
+ kind,
877
+ typeof key,
878
+ key
879
+ ]);
880
+ }
881
+ function createRow(original, key, index, columns, canExpand = false) {
882
+ const values = /* @__PURE__ */ new Map();
883
+ const displays = /* @__PURE__ */ new Map();
884
+ const getValue = (columnId) => {
885
+ if (values.has(columnId)) return values.get(columnId);
886
+ const value = columns.get(columnId)?.getValue(original);
887
+ values.set(columnId, value);
888
+ return value;
889
+ };
890
+ return Object.freeze({
891
+ key,
892
+ index,
893
+ original,
894
+ canExpand,
895
+ isExpanded: false,
896
+ getValue,
897
+ getDisplay(columnId) {
898
+ const cached = displays.get(columnId);
899
+ if (cached !== void 0) return cached;
900
+ const column = columns.get(columnId);
901
+ const display = column ? column.format(getValue(columnId), original) : "";
902
+ displays.set(columnId, display);
903
+ return display;
904
+ }
905
+ });
906
+ }
907
+
596
908
  //#endregion
597
909
  //#region src/state.ts
598
910
  const DEFAULT_PAGE_SIZE = 10;
599
- function createInitialState(patch = {}) {
911
+ function createInitialState(patch = {}, onIssue) {
600
912
  return freezeState({
601
913
  sorting: patch.sorting ?? [],
602
914
  search: patch.search ?? "",
@@ -606,70 +918,46 @@ function createInitialState(patch = {}) {
606
918
  pageSize: 10
607
919
  },
608
920
  selection: patch.selection ?? [],
921
+ expanded: normalizeExpanded(patch.expanded === void 0 ? [] : patch.expanded, onIssue),
609
922
  hiddenColumns: patch.hiddenColumns ?? [],
610
923
  columnOrder: patch.columnOrder ?? []
611
924
  });
612
925
  }
613
- function mergeState(state, patch) {
926
+ function mergeState(state, patch, onIssue) {
614
927
  let changed = false;
615
928
  const next = { ...state };
616
- for (const [key, value] of Object.entries(patch)) {
929
+ for (const [key, input] of Object.entries(patch)) {
930
+ const value = key === "expanded" && input !== void 0 ? normalizeExpanded(input, onIssue) : input;
617
931
  if (value === void 0 || Object.is(next[key], value)) continue;
618
932
  if (key === "pagination" && samePagination(next[key], value)) continue;
933
+ if (key === "expanded" && sameExpanded(state.expanded, value)) continue;
619
934
  next[key] = value;
620
935
  changed = true;
621
936
  }
622
- return changed ? freezeState(next) : state;
937
+ return changed ? freezeState(next, state) : state;
938
+ }
939
+ function normalizeExpanded(value, onIssue) {
940
+ if (value === true) return true;
941
+ if (Array.isArray(value) && value.every((key) => typeof key === "string" || typeof key === "number" && Number.isFinite(key))) return [...new Set(value)];
942
+ onIssue?.(issue("invalid_expanded", "Expanded state must be true or an array of string/finite number keys; an empty array is used."));
943
+ return [];
944
+ }
945
+ function sameExpanded(left, right) {
946
+ return left === right || left !== true && right !== true && left.length === right.length && left.every((key, index) => key === right[index]);
623
947
  }
624
948
  function samePagination(left, right) {
625
949
  return left.page === right.page && left.pageSize === right.pageSize;
626
950
  }
627
- function freezeState(state) {
951
+ function freezeState(state, previous) {
628
952
  return Object.freeze({
629
- sorting: Object.freeze(state.sorting.map((rule) => Object.freeze({ ...rule }))),
953
+ sorting: state.sorting === previous?.sorting ? previous.sorting : Object.freeze(state.sorting.map((rule) => Object.freeze({ ...rule }))),
630
954
  search: state.search,
631
- filters: Object.freeze({ ...state.filters }),
632
- pagination: Object.freeze({ ...state.pagination }),
633
- selection: Object.freeze([...state.selection]),
634
- hiddenColumns: Object.freeze([...state.hiddenColumns]),
635
- columnOrder: Object.freeze([...state.columnOrder])
636
- });
637
- }
638
-
639
- //#endregion
640
- //#region src/issues.ts
641
- function issue(code, message, details = {}) {
642
- return Object.freeze({
643
- code,
644
- message,
645
- ...details
646
- });
647
- }
648
-
649
- //#endregion
650
- //#region src/row.ts
651
- function createRow(original, key, index, columns) {
652
- const values = /* @__PURE__ */ new Map();
653
- const displays = /* @__PURE__ */ new Map();
654
- const getValue = (columnId) => {
655
- if (values.has(columnId)) return values.get(columnId);
656
- const value = columns.get(columnId)?.getValue(original);
657
- values.set(columnId, value);
658
- return value;
659
- };
660
- return Object.freeze({
661
- key,
662
- index,
663
- original,
664
- getValue,
665
- getDisplay(columnId) {
666
- const cached = displays.get(columnId);
667
- if (cached !== void 0) return cached;
668
- const column = columns.get(columnId);
669
- const display = column ? column.format(getValue(columnId), original) : "";
670
- displays.set(columnId, display);
671
- return display;
672
- }
955
+ filters: state.filters === previous?.filters ? previous.filters : Object.freeze({ ...state.filters }),
956
+ pagination: state.pagination === previous?.pagination ? previous.pagination : Object.freeze({ ...state.pagination }),
957
+ selection: state.selection === previous?.selection ? previous.selection : Object.freeze([...state.selection]),
958
+ expanded: state.expanded === previous?.expanded ? previous.expanded : state.expanded === true ? true : Object.freeze([...state.expanded]),
959
+ hiddenColumns: state.hiddenColumns === previous?.hiddenColumns ? previous.hiddenColumns : Object.freeze([...state.hiddenColumns]),
960
+ columnOrder: state.columnOrder === previous?.columnOrder ? previous.columnOrder : Object.freeze([...state.columnOrder])
673
961
  });
674
962
  }
675
963
 
@@ -697,19 +985,33 @@ function coverage(selected, total) {
697
985
  if (selected === 0 || total === 0) return "none";
698
986
  return selected === total ? "all" : "some";
699
987
  }
988
+ function pasteLimit(value, fallback, name, issues) {
989
+ if (value === void 0) return fallback;
990
+ if (Number.isSafeInteger(value) && value > 0) return value;
991
+ issues.push(issue("invalid_paste_limit", `Paste ${name} ${String(value)} is not a positive safe integer; ${fallback} is used.`));
992
+ return fallback;
993
+ }
700
994
  /** Create a data table. */
701
995
  function createTable(options) {
996
+ const paginate = options.paginate ?? true;
997
+ const expandMode = options.expandMode ?? "multiple";
702
998
  const selectionMode = options.selectionMode ?? "multiple";
703
999
  const selectScope = options.selectScope ?? "all";
704
1000
  const manual = options.manual ?? false;
705
1001
  const undoStack = createUndoStack(options.historyLimit);
706
1002
  const listeners = /* @__PURE__ */ new Set();
1003
+ const optionIssues = [];
1004
+ const maxPasteCells = pasteLimit(options.pasteLimit?.maxCells, 1e5, "maxCells", optionIssues);
1005
+ const maxPasteLength = pasteLimit(options.pasteLimit?.maxLength, 5e6, "maxLength", optionIssues);
707
1006
  let data = options.data;
708
1007
  let definitions = options.columns;
709
1008
  let rowCount = options.rowCount;
710
- const initialState = createInitialState({
1009
+ let expandedIssue;
1010
+ let initialState = createInitialState({
711
1011
  ...options.initialState,
712
1012
  hiddenColumns: options.initialState?.hiddenColumns ?? options.columns.filter((column) => column.hidden === true).map((column) => column.id)
1013
+ }, (problem) => {
1014
+ expandedIssue = problem;
713
1015
  });
714
1016
  let state = initialState;
715
1017
  let snapshot;
@@ -721,10 +1023,18 @@ function createTable(options) {
721
1023
  return rowKey === void 0 || !isRecord(row) ? index : String(value);
722
1024
  };
723
1025
  const resolveColumns = memo((defs) => {
724
- const columns = defs.map((definition) => resolveColumn(definition));
1026
+ const issues = [];
1027
+ const columns = defs.flatMap((definition) => {
1028
+ if (!isSafePath(definition.id)) {
1029
+ issues.push(issue("unsafe_path", `The column "${definition.id}" addresses a prototype and is ignored.`, { column: definition.id }));
1030
+ return [];
1031
+ }
1032
+ return [resolveColumn(definition)];
1033
+ });
725
1034
  return {
726
1035
  columns,
727
- byId: new Map(columns.map((column) => [column.id, column]))
1036
+ byId: new Map(columns.map((column) => [column.id, column])),
1037
+ issues
728
1038
  };
729
1039
  });
730
1040
  const indexRows = memo((rows, byId) => {
@@ -737,7 +1047,7 @@ function createTable(options) {
737
1047
  issues.push(issue("duplicate_row_key", `Row ${index} repeats the key "${String(key)}"; it is keyed "${String(key)}#${index}" instead.`, { rowKey: key }));
738
1048
  key = `${String(key)}#${index}`;
739
1049
  }
740
- const row = createRow(original, key, index, byId);
1050
+ const row = createRow(original, key, index, byId, options.getRowCanExpand?.(original) ?? false);
741
1051
  byKey.set(key, row);
742
1052
  return row;
743
1053
  }),
@@ -762,8 +1072,21 @@ function createTable(options) {
762
1072
  const filterStage = memo(filterRows);
763
1073
  const sortStage = memo(sortRows);
764
1074
  const selectedSet = memo((selection) => new Set(selection));
1075
+ const expansionStage = memo((indexed, expanded) => {
1076
+ const keys = expanded === true ? void 0 : new Set(expanded);
1077
+ const rows = indexed.rows.map((row) => row.canExpand && (expanded === true || keys?.has(row.key)) ? Object.freeze({
1078
+ ...row,
1079
+ isExpanded: true
1080
+ }) : row);
1081
+ return {
1082
+ rows,
1083
+ byKey: new Map(rows.map((row) => [row.key, row]))
1084
+ };
1085
+ });
1086
+ const expandedOrder = memo((rows, byKey) => rows.map((row) => byKey.get(row.key)));
765
1087
  function stateIssues(byId) {
766
1088
  const issues = [];
1089
+ if (expandedIssue) issues.push(expandedIssue);
767
1090
  const referenced = /* @__PURE__ */ new Set([
768
1091
  ...state.sorting.map((rule) => rule.column),
769
1092
  ...Object.keys(state.filters),
@@ -772,30 +1095,36 @@ function createTable(options) {
772
1095
  ]);
773
1096
  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
1097
  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.`));
1098
+ 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
1099
  return issues;
777
1100
  }
778
1101
  function computeSnapshot() {
779
- const { columns, byId } = resolveColumns(definitions);
1102
+ const { columns, byId, issues: columnIssues } = resolveColumns(definitions);
780
1103
  const indexed = indexRows(data, byId);
1104
+ const expanded = expansionStage(indexed, state.expanded);
781
1105
  const ordered = orderColumns(columns, state.columnOrder, state.hiddenColumns);
782
- const issues = [...indexed.issues, ...stateIssues(byId)];
1106
+ const issues = [
1107
+ ...optionIssues,
1108
+ ...columnIssues,
1109
+ ...indexed.issues,
1110
+ ...stateIssues(byId)
1111
+ ];
783
1112
  const requestedSize = state.pagination.pageSize;
784
- const pageSize = Number.isInteger(requestedSize) && requestedSize >= 1 ? requestedSize : 10;
1113
+ let pageSize = Number.isInteger(requestedSize) && requestedSize >= 1 ? requestedSize : 10;
785
1114
  let processedRows;
786
1115
  let rows;
787
1116
  let filteredCount;
788
1117
  let page;
789
1118
  let pageCount;
790
1119
  if (manual) {
791
- processedRows = indexed.rows;
792
- rows = indexed.rows;
1120
+ processedRows = expanded.rows;
1121
+ rows = expanded.rows;
793
1122
  filteredCount = rowCount ?? indexed.rows.length;
794
1123
  pageCount = countPages(filteredCount, pageSize);
795
1124
  page = clampPage(state.pagination.page, pageCount);
796
1125
  } else {
797
1126
  const filtered = filterStage(indexed.rows, columns, state.search, state.filters);
798
- processedRows = sortStage(filtered, columns, state.sorting);
1127
+ processedRows = expandedOrder(sortStage(filtered, columns, state.sorting), expanded.byKey);
799
1128
  filteredCount = processedRows.length;
800
1129
  pageCount = countPages(filteredCount, pageSize);
801
1130
  page = clampPage(state.pagination.page, pageCount);
@@ -804,6 +1133,12 @@ function createTable(options) {
804
1133
  pageSize
805
1134
  });
806
1135
  }
1136
+ if (!paginate) {
1137
+ rows = processedRows;
1138
+ page = 1;
1139
+ pageCount = 1;
1140
+ pageSize = Math.max(1, rows.length);
1141
+ }
807
1142
  const selected = selectedSet(state.selection);
808
1143
  const pageSelected = rows.filter((row) => selected.has(row.key)).length;
809
1144
  const scopeRows = selectScope === "page" || manual ? rows : processedRows;
@@ -818,6 +1153,21 @@ function createTable(options) {
818
1153
  columns: ordered.visible,
819
1154
  allColumns: ordered.all,
820
1155
  rows,
1156
+ renderItems: Object.freeze(rows.flatMap((row, rowIndex) => {
1157
+ const item = Object.freeze({
1158
+ kind: "row",
1159
+ key: getRowItemKey(row.key),
1160
+ row,
1161
+ rowIndex
1162
+ });
1163
+ return row.isExpanded ? [item, Object.freeze({
1164
+ kind: "detail",
1165
+ key: getRowItemKey(row.key, "detail"),
1166
+ row,
1167
+ rowIndex
1168
+ })] : [item];
1169
+ })),
1170
+ paginate,
821
1171
  processedRows,
822
1172
  page,
823
1173
  pageSize,
@@ -839,7 +1189,7 @@ function createTable(options) {
839
1189
  isSelected: (key) => selected.has(key),
840
1190
  getSort: (columnId) => sortIndex.get(columnId),
841
1191
  getColumn: (columnId) => byId.get(columnId),
842
- getRow: (key) => indexed.byKey.get(key)
1192
+ getRow: (key) => expanded.byKey.get(key)
843
1193
  });
844
1194
  }
845
1195
  function getSnapshot() {
@@ -855,12 +1205,31 @@ function createTable(options) {
855
1205
  /** Apply a change made by a table operation. */
856
1206
  function commit(patch) {
857
1207
  const previous = state;
858
- const next = mergeState(state, patch);
859
- if (next === previous) return;
1208
+ const previousIssue = expandedIssue;
1209
+ const next = mergePatch(patch);
1210
+ if (next === previous && previousIssue === expandedIssue) return;
860
1211
  state = next;
861
- options.onStateChange?.(state, previous);
1212
+ if (next !== previous) options.onStateChange?.(state, previous);
862
1213
  notify();
863
1214
  }
1215
+ function canonicalExpanded(expanded, candidate = state) {
1216
+ if (expandMode === "multiple") return expanded;
1217
+ if (expanded !== true) return expanded.slice(-1);
1218
+ const { columns, byId } = resolveColumns(definitions);
1219
+ const indexed = indexRows(data, byId);
1220
+ const first = (manual ? indexed.rows : sortStage(filterStage(indexed.rows, columns, candidate.search, candidate.filters), columns, candidate.sorting)).find((row) => row.canExpand);
1221
+ return first ? [first.key] : [];
1222
+ }
1223
+ function mergePatch(patch) {
1224
+ if (patch.expanded !== void 0) expandedIssue = void 0;
1225
+ const next = mergeState(state, patch, (problem) => {
1226
+ expandedIssue = problem;
1227
+ });
1228
+ return expandMode === "single" && patch.expanded !== void 0 ? mergeState(state, {
1229
+ ...patch,
1230
+ expanded: canonicalExpanded(next.expanded, next)
1231
+ }) : next;
1232
+ }
864
1233
  const firstPage = () => ({
865
1234
  page: 1,
866
1235
  pageSize: state.pagination.pageSize
@@ -890,10 +1259,10 @@ function createTable(options) {
890
1259
  if (selection.length === current.length && selection.every((key, i) => key === current[i])) return;
891
1260
  commit({ selection });
892
1261
  }
893
- function applyChanges(edits, record) {
1262
+ function applyChanges(edits, record, recoveredIssues = []) {
894
1263
  const current = getSnapshot();
895
1264
  const working = [...data];
896
- const issues = [];
1265
+ const issues = [...recoveredIssues];
897
1266
  const changes = [];
898
1267
  const samples = /* @__PURE__ */ new Map();
899
1268
  const sampleOf = (columnId) => {
@@ -916,6 +1285,13 @@ function createTable(options) {
916
1285
  }));
917
1286
  continue;
918
1287
  }
1288
+ if (!isSafePath(edit.column)) {
1289
+ issues.push(issue("unsafe_path", `The path "${edit.column}" addresses a prototype and cannot be written.`, {
1290
+ rowKey: edit.rowKey,
1291
+ column: edit.column
1292
+ }));
1293
+ continue;
1294
+ }
919
1295
  if (!column) {
920
1296
  issues.push(issue("unknown_column", `No column has the id "${edit.column}".`, {
921
1297
  rowKey: edit.rowKey,
@@ -1002,6 +1378,11 @@ function createTable(options) {
1002
1378
  value: direction === "undo" ? change.previous : change.value
1003
1379
  })), false);
1004
1380
  }
1381
+ if (expandMode === "single") {
1382
+ initialState = mergeState(initialState, { expanded: canonicalExpanded(initialState.expanded) });
1383
+ state = initialState;
1384
+ snapshot = void 0;
1385
+ }
1005
1386
  const table = {
1006
1387
  getSnapshot,
1007
1388
  getState: () => state,
@@ -1012,8 +1393,9 @@ function createTable(options) {
1012
1393
  };
1013
1394
  },
1014
1395
  setState(patch) {
1015
- const next = mergeState(state, patch);
1016
- if (next !== state) {
1396
+ const previousIssue = expandedIssue;
1397
+ const next = mergePatch(patch);
1398
+ if (next !== state || previousIssue !== expandedIssue) {
1017
1399
  state = next;
1018
1400
  notify();
1019
1401
  }
@@ -1069,6 +1451,7 @@ function createTable(options) {
1069
1451
  });
1070
1452
  },
1071
1453
  goToPage(page) {
1454
+ if (!paginate) return;
1072
1455
  const { pageCount, page: currentPage } = getSnapshot();
1073
1456
  const next = clampPage(page, pageCount);
1074
1457
  if (next !== currentPage || next !== state.pagination.page) commit({ pagination: {
@@ -1083,6 +1466,7 @@ function createTable(options) {
1083
1466
  table.goToPage(getSnapshot().page - 1);
1084
1467
  },
1085
1468
  setPageSize(pageSize) {
1469
+ if (!paginate) return;
1086
1470
  if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize === state.pagination.pageSize) return;
1087
1471
  const { pageStart } = getSnapshot();
1088
1472
  commit({ pagination: {
@@ -1121,6 +1505,23 @@ function createTable(options) {
1121
1505
  return row ? [row] : [];
1122
1506
  });
1123
1507
  },
1508
+ toggleExpanded(key, expanded) {
1509
+ const row = getSnapshot().getRow(key);
1510
+ if (!row?.canExpand) return;
1511
+ const open = expanded ?? !row.isExpanded;
1512
+ if (open === row.isExpanded) return;
1513
+ if (expandMode === "single") commit({ expanded: open ? [key] : [] });
1514
+ else if (state.expanded === true) {
1515
+ const { byId } = resolveColumns(definitions);
1516
+ commit({ expanded: indexRows(data, byId).rows.filter((candidate) => candidate.canExpand && candidate.key !== key).map((candidate) => candidate.key) });
1517
+ } else commit({ expanded: open ? [...state.expanded, key] : state.expanded.filter((candidate) => candidate !== key) });
1518
+ },
1519
+ expandAll() {
1520
+ commit({ expanded: true });
1521
+ },
1522
+ collapseAll() {
1523
+ commit({ expanded: [] });
1524
+ },
1124
1525
  toggleColumn(columnId, visible) {
1125
1526
  if (!getSnapshot().getColumn(columnId)) return;
1126
1527
  const hidden = state.hiddenColumns.includes(columnId);
@@ -1153,7 +1554,7 @@ function createTable(options) {
1153
1554
  replay(changes, "redo");
1154
1555
  return true;
1155
1556
  },
1156
- copy(range) {
1557
+ copy(range, { escapeFormulas = false } = {}) {
1157
1558
  const { rows, columns } = getSnapshot();
1158
1559
  const matrix = [];
1159
1560
  rows.forEach((row, rowIndex) => {
@@ -1162,14 +1563,26 @@ function createTable(options) {
1162
1563
  if (rangeContains(range, {
1163
1564
  row: rowIndex,
1164
1565
  column: columnIndex
1165
- })) line.push(row.getDisplay(column.id));
1566
+ })) {
1567
+ const display = row.getDisplay(column.id);
1568
+ line.push(escapeFormulas ? escapeFormula(display, row.getValue(column.id)) : display);
1569
+ }
1166
1570
  });
1167
1571
  if (line.length > 0) matrix.push(line);
1168
1572
  });
1169
1573
  return toDelimited(matrix);
1170
1574
  },
1171
1575
  paste(origin, text) {
1172
- return applyChanges(rangeEdits(origin, parseDelimited(text)), true);
1576
+ if (!Number.isInteger(origin.row) || !Number.isInteger(origin.column) || origin.row < 0 || origin.column < 0) return applyChanges([], true, [issue("invalid_value", "The paste origin must use non-negative whole row and column indices.")]);
1577
+ const { rows, columns } = getSnapshot();
1578
+ const parsed = readDelimited(text, {
1579
+ maxRows: Math.max(0, rows.length - origin.row),
1580
+ maxColumns: Math.max(0, columns.length - origin.column),
1581
+ maxCells: maxPasteCells,
1582
+ maxLength: maxPasteLength
1583
+ });
1584
+ const issues = parsed.truncated ? [issue("paste_truncated", "The paste exceeds the visible grid or parsing limits; only complete cells within those limits are applied.")] : [];
1585
+ return applyChanges(rangeEdits(origin, parsed.rows), true, issues);
1173
1586
  },
1174
1587
  clear(range) {
1175
1588
  const matrix = Array.from({ length: range.bottom - range.top + 1 }, () => Array.from({ length: range.right - range.left + 1 }, () => ""));
@@ -1182,14 +1595,17 @@ function createTable(options) {
1182
1595
  return row !== void 0 && current.getColumn(edit.column)?.isEditable(row.original) === true;
1183
1596
  }), true);
1184
1597
  },
1185
- exportRows({ format = "csv", columns: ids, pageOnly = false, headers = true } = {}) {
1598
+ exportRows({ format = "csv", columns: ids, pageOnly = false, headers = true, escapeFormulas = true } = {}) {
1186
1599
  const current = getSnapshot();
1187
1600
  const columns = ids ? ids.flatMap((id) => {
1188
1601
  const column = current.getColumn(id);
1189
1602
  return column ? [column] : [];
1190
1603
  }) : current.columns;
1191
- const matrix = (pageOnly ? current.rows : current.processedRows).map((row) => columns.map((column) => row.getDisplay(column.id)));
1192
- if (headers) matrix.unshift(columns.map((column) => column.header));
1604
+ const matrix = (pageOnly ? current.rows : current.processedRows).map((row) => columns.map((column) => {
1605
+ const display = row.getDisplay(column.id);
1606
+ return escapeFormulas ? escapeFormula(display, row.getValue(column.id)) : display;
1607
+ }));
1608
+ if (headers) matrix.unshift(columns.map((column) => escapeFormulas ? escapeFormula(column.header) : column.header));
1193
1609
  return toDelimited(matrix, format === "csv" ? "," : " ");
1194
1610
  },
1195
1611
  reset() {
@@ -1203,5 +1619,5 @@ function createTable(options) {
1203
1619
  }
1204
1620
 
1205
1621
  //#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 };
1622
+ 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
1623
  //# sourceMappingURL=index.js.map