@vueye-table/vue 3.0.0-alpha.1 → 3.0.0-alpha.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { computed, getCurrentScope, inject, onScopeDispose, provide, shallowRef, toRaw, toValue, watch } from "vue";
2
- import { clampPosition, createTable, gridCommand, moveSelection, rangeContains, selectCell, selectionRange } from "@vueye-table/core";
1
+ import { computed, getCurrentInstance, getCurrentScope, inject, nextTick, onMounted, onScopeDispose, provide, shallowReactive, shallowRef, toRaw, toValue, unref, watch, watchEffect } from "vue";
2
+ import { clampPosition, createTable, createVirtualizer, formatValue, getRowItemKey, gridCommand, moveSelection, rangeContains, selectCell, selectionRange, typeOf } from "@vueye-table/core";
3
3
 
4
4
  //#region src/injection.ts
5
5
  const tableKey = Symbol("vueye-table");
@@ -25,19 +25,193 @@ function injectDataGrid() {
25
25
  return inject(gridKey, void 0);
26
26
  }
27
27
 
28
+ //#endregion
29
+ //#region src/row-draft.ts
30
+ function rejectedDraft(rowKey, message) {
31
+ return refused([Object.freeze({
32
+ code: "stale_draft",
33
+ rowKey,
34
+ message
35
+ })]);
36
+ }
37
+ function refused(issues) {
38
+ return Object.freeze({
39
+ status: "rejected",
40
+ changes: Object.freeze([]),
41
+ rowChanges: Object.freeze([]),
42
+ pendingCells: Object.freeze([]),
43
+ issues: Object.freeze([...issues])
44
+ });
45
+ }
46
+ function equal(left, right, seen = /* @__PURE__ */ new WeakMap()) {
47
+ if (Object.is(left, right)) return true;
48
+ if (!left || !right || typeof left !== "object" || typeof right !== "object") return false;
49
+ if (left instanceof Date || right instanceof Date) return left instanceof Date && right instanceof Date && Object.is(left.getTime(), right.getTime());
50
+ if (Array.isArray(left) && Array.isArray(right) && left.length !== right.length) return false;
51
+ if (Object.getPrototypeOf(left) !== Object.getPrototypeOf(right)) return false;
52
+ if (seen.has(left)) return seen.get(left) === right;
53
+ seen.set(left, right);
54
+ if (left instanceof Map && right instanceof Map) return equal([...left], [...right], seen);
55
+ if (left instanceof Set && right instanceof Set) return equal([...left], [...right], seen);
56
+ if (left instanceof RegExp && right instanceof RegExp) return left.source === right.source && left.flags === right.flags;
57
+ if (left instanceof ArrayBuffer && right instanceof ArrayBuffer) return equal([...new Uint8Array(left)], [...new Uint8Array(right)], seen);
58
+ if (ArrayBuffer.isView(left) && ArrayBuffer.isView(right)) return equal([...new Uint8Array(left.buffer, left.byteOffset, left.byteLength)], [...new Uint8Array(right.buffer, right.byteOffset, right.byteLength)], seen);
59
+ const keys = Object.keys(left);
60
+ return keys.length === Object.keys(right).length && keys.every((key) => Object.hasOwn(right, key) && equal(Reflect.get(left, key), Reflect.get(right, key), seen));
61
+ }
62
+ /** Internal factory; disposal is shared with the binding's single engine subscription. */
63
+ function createRowDraft(table, key, register) {
64
+ const snapshot = table.getSnapshot();
65
+ const original = snapshot.getRow(key)?.original;
66
+ const columns = snapshot.allColumns;
67
+ const values = shallowReactive(Object.create(null));
68
+ const initial = /* @__PURE__ */ new Map();
69
+ const inputs = /* @__PURE__ */ new Map();
70
+ const copyIssues = /* @__PURE__ */ new Map();
71
+ const issues = shallowRef([]);
72
+ const status = shallowRef("editing");
73
+ for (const column of columns) {
74
+ if (original === void 0) break;
75
+ try {
76
+ if (!column.isEditable(original)) continue;
77
+ const value = column.getValue(original);
78
+ initial.set(column.id, value);
79
+ values[column.id] = structuredClone(toRaw(value));
80
+ } catch {
81
+ copyIssues.set(column.id, Object.freeze({
82
+ code: "invalid_value",
83
+ rowKey: key,
84
+ column: column.id,
85
+ message: "This value cannot be copied into a row draft. Replace it explicitly before saving."
86
+ }));
87
+ issues.value = [...copyIssues.values()];
88
+ }
89
+ }
90
+ let saving;
91
+ let disposePending;
92
+ let disposed = false;
93
+ const stale = () => rejectedDraft(key, "This draft is closed, or its row or columns changed. Open a new draft.");
94
+ const unregister = register(() => {
95
+ disposed = true;
96
+ const result = stale();
97
+ status.value = "stale";
98
+ issues.value = result.issues;
99
+ disposePending?.(result);
100
+ });
101
+ return Object.freeze({
102
+ rowKey: key,
103
+ values,
104
+ get issues() {
105
+ return issues.value;
106
+ },
107
+ get status() {
108
+ return status.value;
109
+ },
110
+ get pending() {
111
+ return status.value === "saving";
112
+ },
113
+ setInput(column, input) {
114
+ if (status.value === "editing") inputs.set(column, input);
115
+ },
116
+ setValue(column, value) {
117
+ if (status.value === "editing") {
118
+ values[column] = value;
119
+ inputs.delete(column);
120
+ }
121
+ },
122
+ save() {
123
+ if (saving) return saving;
124
+ if (status.value !== "editing" || disposed || original === void 0 || table.getSnapshot().getRow(key)?.original !== original || columns.some((column) => table.getSnapshot().getColumn(column.id)?.definition !== column.definition)) {
125
+ const result = stale();
126
+ issues.value = result.issues;
127
+ status.value = "stale";
128
+ unregister();
129
+ return Promise.resolve(result);
130
+ }
131
+ const edits = [];
132
+ const uncopied = [...copyIssues.values()].filter((problem) => !Object.hasOwn(values, problem.column) && !inputs.has(problem.column));
133
+ if (uncopied.length) {
134
+ issues.value = Object.freeze(uncopied);
135
+ return Promise.resolve(refused(issues.value));
136
+ }
137
+ for (const column of /* @__PURE__ */ new Set([...Object.keys(values), ...inputs.keys()])) try {
138
+ const value = toRaw(values[column]);
139
+ if (inputs.has(column)) edits.push({
140
+ rowKey: key,
141
+ column,
142
+ input: inputs.get(column)
143
+ });
144
+ else if (!initial.has(column) || !equal(value, initial.get(column))) edits.push(typeof value === "string" ? {
145
+ rowKey: key,
146
+ column,
147
+ input: value
148
+ } : {
149
+ rowKey: key,
150
+ column,
151
+ value: value !== null && typeof value === "object" ? structuredClone(value) : value
152
+ });
153
+ } catch {
154
+ issues.value = Object.freeze([Object.freeze({
155
+ code: "invalid_value",
156
+ rowKey: key,
157
+ column,
158
+ message: "This draft value cannot be copied for submission. Replace it before saving."
159
+ })]);
160
+ return Promise.resolve(refused(issues.value));
161
+ }
162
+ status.value = "saving";
163
+ const result = table.edit(edits, { expectedRows: /* @__PURE__ */ new Map([[key, original]]) });
164
+ if (disposed) return Promise.resolve(stale());
165
+ const cancelled = new Promise((resolve) => {
166
+ disposePending = resolve;
167
+ });
168
+ saving = Promise.race([result.completion ?? Promise.resolve(result), cancelled]).then((final) => {
169
+ issues.value = final.issues;
170
+ status.value = disposed || final.issues.some((problem) => problem.code === "stale_draft") ? "stale" : final.status === "rejected" ? "editing" : "saved";
171
+ if (status.value !== "editing") unregister();
172
+ saving = void 0;
173
+ disposePending = void 0;
174
+ return final;
175
+ });
176
+ return saving;
177
+ },
178
+ cancel() {
179
+ if (status.value !== "editing") return false;
180
+ status.value = "cancelled";
181
+ unregister();
182
+ return true;
183
+ }
184
+ });
185
+ }
186
+
28
187
  //#endregion
29
188
  //#region src/use-data-grid.ts
30
- function useDataGrid(table) {
189
+ function useDataGrid(table, gridOptions = {}) {
31
190
  const selection = shallowRef();
32
191
  const editor = shallowRef();
33
192
  const lastResult = shallowRef();
193
+ let editing;
194
+ let resultToken;
195
+ const invalidEditors = /* @__PURE__ */ new WeakSet();
196
+ const editorSpecs = /* @__PURE__ */ new WeakMap();
197
+ let submitting;
198
+ const record = (result) => {
199
+ const token = {};
200
+ resultToken = token;
201
+ lastResult.value = result;
202
+ result.completion?.then((final) => {
203
+ if (resultToken === token) lastResult.value = final;
204
+ });
205
+ return result;
206
+ };
34
207
  const bounds = computed(() => ({
35
208
  rows: table.rows.length,
36
209
  columns: table.columns.length
37
210
  }));
38
211
  const range = computed(() => selection.value ? selectionRange(selection.value) : void 0);
39
212
  const same = (left, right) => left !== void 0 && left.row === right.row && left.column === right.column;
40
- watch(bounds, (next) => {
213
+ watch(() => table.snapshot, () => {
214
+ const next = bounds.value;
41
215
  const current = selection.value;
42
216
  if (current && (next.rows === 0 || next.columns === 0)) selection.value = void 0;
43
217
  else if (current) {
@@ -48,9 +222,32 @@ function useDataGrid(table) {
48
222
  focus
49
223
  };
50
224
  }
51
- const position = editor.value?.position;
52
- if (position && (position.row >= next.rows || position.column >= next.columns)) editor.value = void 0;
53
- });
225
+ const active = editing;
226
+ if (active && editor.value) {
227
+ const row = table.rows.findIndex((candidate) => candidate.key === active.key);
228
+ const column = table.columns.findIndex((candidate) => candidate.id === active.column.id);
229
+ if (row < 0 || column < 0 || !submitting && table.getRow(active.key)?.original !== active.original || table.getColumn(active.column.id)?.definition !== active.column.definition) {
230
+ editor.value = void 0;
231
+ editing = void 0;
232
+ record(rejectedDraft(active.key, "The edited cell changed or left the visible grid. Start a new edit."));
233
+ } else if (!same(editor.value.position, {
234
+ row,
235
+ column
236
+ })) {
237
+ editor.value = {
238
+ ...editor.value,
239
+ position: {
240
+ row,
241
+ column
242
+ }
243
+ };
244
+ selection.value = selectCell({
245
+ row,
246
+ column
247
+ });
248
+ }
249
+ }
250
+ }, { flush: "sync" });
54
251
  const cellAt = (position) => {
55
252
  const row = table.rows[position.row];
56
253
  const column = table.columns[position.column];
@@ -68,6 +265,7 @@ function useDataGrid(table) {
68
265
  return selection.value;
69
266
  };
70
267
  const grid = {
268
+ table,
71
269
  get selection() {
72
270
  return selection.value;
73
271
  },
@@ -82,10 +280,60 @@ function useDataGrid(table) {
82
280
  },
83
281
  bounds,
84
282
  cellAt,
283
+ editorFor(position) {
284
+ const cell = cellAt(position);
285
+ if (!cell || !cell.column.isEditable(cell.row.original)) return void 0;
286
+ const spec = cell.column.definition.editor;
287
+ const cached = editorSpecs.get(cell.column.definition);
288
+ if (cached) return cached;
289
+ if (spec && [
290
+ "text",
291
+ "number",
292
+ "select",
293
+ "checkbox",
294
+ "date"
295
+ ].includes(spec.kind) && (spec.options === void 0 || Array.isArray(spec.options) && spec.options.every((value) => value === null || [
296
+ "string",
297
+ "number",
298
+ "boolean"
299
+ ].includes(typeof value)))) {
300
+ const resolved = Object.freeze({
301
+ kind: spec.kind,
302
+ ...spec.options ? { options: Object.freeze([...spec.options]) } : {},
303
+ ...spec.min !== void 0 ? { min: spec.min } : {},
304
+ ...spec.max !== void 0 ? { max: spec.max } : {},
305
+ ...spec.maxLength !== void 0 ? { maxLength: spec.maxLength } : {},
306
+ ...spec.pattern !== void 0 ? { pattern: spec.pattern } : {}
307
+ });
308
+ editorSpecs.set(cell.column.definition, resolved);
309
+ return resolved;
310
+ }
311
+ if (spec) {
312
+ if (!invalidEditors.has(cell.column.definition)) {
313
+ invalidEditors.add(cell.column.definition);
314
+ record(Object.freeze({
315
+ status: "rejected",
316
+ changes: Object.freeze([]),
317
+ rowChanges: Object.freeze([]),
318
+ pendingCells: Object.freeze([]),
319
+ issues: Object.freeze([Object.freeze({
320
+ code: "invalid_value",
321
+ rowKey: cell.row.key,
322
+ column: cell.column.id,
323
+ message: "Invalid editor metadata; text metadata is used."
324
+ })])
325
+ }));
326
+ }
327
+ return Object.freeze({ kind: "text" });
328
+ }
329
+ const type = cell.column.definition.type ?? typeOf(cell.row.getValue(cell.column.id)) ?? "text";
330
+ return Object.freeze({ kind: type === "boolean" ? "checkbox" : type });
331
+ },
85
332
  isFocused: (position) => same(selection.value?.focus, position),
86
333
  isSelected: (position) => range.value ? rangeContains(range.value, position) : false,
87
334
  isEditing: (position) => same(editor.value?.position, position),
88
335
  focusCell(position, { extend = false } = {}) {
336
+ if (submitting) return;
89
337
  const target = clampPosition(position, bounds.value);
90
338
  if (editor.value && !same(editor.value.position, target)) grid.commitEdit();
91
339
  selection.value = extend && selection.value ? {
@@ -110,12 +358,21 @@ function useDataGrid(table) {
110
358
  };
111
359
  },
112
360
  startEdit(initial) {
361
+ if (submitting) return false;
113
362
  const focus = ensureSelection()?.focus;
114
363
  const cell = focus ? cellAt(focus) : void 0;
115
364
  if (!focus || !cell || !cell.column.isEditable(cell.row.original)) return false;
365
+ const value = cell.row.getValue(cell.column.id);
366
+ const kind = grid.editorFor(focus)?.kind;
367
+ const text = kind === "date" && value instanceof Date ? Number.isFinite(value.getTime()) ? value.toISOString().slice(0, 10) : "" : kind && kind !== "text" ? value === null || value === void 0 ? "" : formatValue(value) : cell.row.getDisplay(cell.column.id);
116
368
  editor.value = {
117
369
  position: focus,
118
- draft: initial ?? cell.row.getDisplay(cell.column.id)
370
+ draft: initial ?? text
371
+ };
372
+ editing = {
373
+ key: cell.row.key,
374
+ column: cell.column,
375
+ original: cell.row.original
119
376
  };
120
377
  return true;
121
378
  },
@@ -128,43 +385,104 @@ function useDataGrid(table) {
128
385
  commitEdit(then) {
129
386
  const current = editor.value;
130
387
  if (!current) return;
131
- const cell = cellAt(current.position);
388
+ const active = editing;
132
389
  editor.value = void 0;
133
- if (!cell) return;
134
- const result = table.edit({
135
- rowKey: cell.row.key,
136
- column: cell.column.id,
390
+ editing = void 0;
391
+ if (!active) return;
392
+ const result = record(table.edit({
393
+ rowKey: active.key,
394
+ column: active.column.id,
137
395
  input: current.draft
138
- });
139
- lastResult.value = result;
396
+ }, { expectedRows: /* @__PURE__ */ new Map([[active.key, active.original]]) }));
140
397
  if (then) grid.move(then);
141
398
  return result;
142
399
  },
400
+ submitEdit(edit, then) {
401
+ const current = editor.value;
402
+ const active = editing;
403
+ if (!current || !active || submitting) return void 0;
404
+ const token = {};
405
+ submitting = token;
406
+ const cell = {
407
+ rowKey: active.key,
408
+ column: active.column.id,
409
+ ...edit ?? { input: current.draft }
410
+ };
411
+ const result = record(table.edit(cell, { expectedRows: /* @__PURE__ */ new Map([[active.key, active.original]]) }));
412
+ const settle = (final) => {
413
+ if (submitting !== token) return;
414
+ submitting = void 0;
415
+ if (editing !== active) return;
416
+ if (final.status !== "rejected" || final.issues.some((problem) => problem.code === "stale_draft")) {
417
+ editor.value = void 0;
418
+ editing = void 0;
419
+ if (then) grid.move(then);
420
+ } else editing = {
421
+ ...active,
422
+ original: table.getRow(active.key)?.original ?? active.original
423
+ };
424
+ };
425
+ if (result.completion) result.completion.then(settle);
426
+ else settle(result);
427
+ return result;
428
+ },
143
429
  cancelEdit() {
144
430
  editor.value = void 0;
431
+ editing = void 0;
432
+ submitting = void 0;
145
433
  },
146
- copy() {
147
- return range.value ? table.copy(range.value) : "";
434
+ toggleCheckbox() {
435
+ const focus = ensureSelection()?.focus;
436
+ const cell = focus ? cellAt(focus) : void 0;
437
+ if (!focus || !cell || grid.editorFor(focus)?.kind !== "checkbox") return false;
438
+ record(table.edit({
439
+ rowKey: cell.row.key,
440
+ column: cell.column.id,
441
+ value: cell.row.getValue(cell.column.id) !== true
442
+ }, { expectedRows: /* @__PURE__ */ new Map([[cell.row.key, cell.row.original]]) }));
443
+ return true;
444
+ },
445
+ copy(options) {
446
+ return range.value ? table.copy(range.value, options) : "";
148
447
  },
149
448
  paste(text) {
150
449
  if (!range.value) return;
151
- const result = table.paste({
450
+ return record(table.paste({
152
451
  row: range.value.top,
153
452
  column: range.value.left
154
- }, text);
155
- lastResult.value = result;
156
- return result;
453
+ }, text));
157
454
  },
158
455
  clear() {
159
456
  if (!range.value) return;
160
- const result = table.clear(range.value);
161
- lastResult.value = result;
162
- return result;
457
+ return record(table.clear(range.value));
163
458
  },
164
459
  handleKey(event) {
165
460
  if (editor.value) return false;
166
- const command = gridCommand(event);
461
+ const current = selection.value?.focus ?? {
462
+ row: 0,
463
+ column: 0
464
+ };
465
+ const row = table.rows[current.row];
466
+ const treeColumn = table.columns.find((column) => column.id === gridOptions.treeColumn?.()) ?? table.columns[0];
467
+ const command = gridCommand(event, table.tree && row && table.columns[current.column] === treeColumn ? {
468
+ canExpand: row.canExpand,
469
+ expanded: row.isExpanded
470
+ } : void 0);
167
471
  switch (command?.type) {
472
+ case "tree": {
473
+ if (!row) return false;
474
+ let key = row.key;
475
+ if (command.action === "expand" || command.action === "collapse") table.toggleExpanded(key, command.action === "expand");
476
+ else if (command.action === "siblings") for (const sibling of table.processedRows.filter((candidate) => candidate.parentKey === row.parentKey && candidate.canExpand)) table.toggleExpanded(sibling.key, true);
477
+ else if (command.action === "child") key = table.rows.find((candidate) => candidate.parentKey === row.key)?.key ?? key;
478
+ else key = row.parentKey ?? key;
479
+ const index = table.rows.findIndex((candidate) => candidate.key === key);
480
+ if (index >= 0) grid.focusCell({
481
+ row: index,
482
+ column: current.column
483
+ });
484
+ return true;
485
+ }
168
486
  case "move":
169
487
  grid.move(command.direction, command.options);
170
488
  return true;
@@ -187,12 +505,441 @@ function useDataGrid(table) {
187
505
  return grid;
188
506
  }
189
507
 
508
+ //#endregion
509
+ //#region src/use-virtual.ts
510
+ function useVirtualAxis(source, keyOf, estimate, options, horizontal, project) {
511
+ let items = source();
512
+ let mounted = false;
513
+ let disposed = false;
514
+ let element;
515
+ let observer;
516
+ let detach;
517
+ const elements = /* @__PURE__ */ new Map();
518
+ const keysByElement = /* @__PURE__ */ new Map();
519
+ const requestedCount = options.initialCount ?? 10;
520
+ const initialCount = Number.isSafeInteger(requestedCount) && requestedCount >= 0 ? requestedCount : 10;
521
+ const initialIssue = initialCount === requestedCount ? void 0 : Object.freeze({
522
+ code: "invalid_virtual_option",
523
+ message: "Initial count must be a non-negative safe integer; 10 is used."
524
+ });
525
+ const layoutOptions = () => ({
526
+ count: items.length,
527
+ estimateSize: estimate,
528
+ overscan: mounted ? options.overscan : 0,
529
+ getKey: (index) => keyOf(items[index])
530
+ });
531
+ const virtual = createVirtualizer(layoutOptions());
532
+ function seed() {
533
+ let size = 0;
534
+ for (let index = 0; index < Math.min(initialCount, items.length); index += 1) {
535
+ const value = typeof estimate === "number" ? estimate : estimate(index);
536
+ size += Number.isFinite(value) && value > 0 ? value : 40;
537
+ }
538
+ virtual.setViewport(0, size);
539
+ }
540
+ seed();
541
+ const view = shallowRef(virtual.getWindow());
542
+ const projected = computed(() => Object.freeze(view.value.items.map((item) => Object.freeze(project(item, items[item.index])))));
543
+ const unsubscribe = virtual.subscribe((next) => {
544
+ view.value = next;
545
+ });
546
+ const marginIssue = shallowRef();
547
+ function margin() {
548
+ const value = toValue(options.scrollMargin) ?? 0;
549
+ if (Number.isFinite(value) && value >= 0) {
550
+ marginIssue.value = void 0;
551
+ return value;
552
+ }
553
+ marginIssue.value ??= Object.freeze({
554
+ code: "invalid_virtual_option",
555
+ message: "Scroll margin must be non-negative and finite; 0 is used."
556
+ });
557
+ return 0;
558
+ }
559
+ function viewportSize() {
560
+ return element ? Math.max(0, (horizontal ? element.clientWidth : element.clientHeight) - margin()) : virtual.getWindow().viewportSize;
561
+ }
562
+ function move(offset) {
563
+ if (element) {
564
+ if (horizontal) element.scrollLeft = offset === 0 ? 0 : offset + margin();
565
+ else element.scrollTop = offset === 0 ? 0 : offset + margin();
566
+ }
567
+ virtual.setViewport(offset, viewportSize());
568
+ }
569
+ function anchor() {
570
+ const current = virtual.getWindow();
571
+ const first = current.items.find((item) => item.start + item.size > current.offset);
572
+ return first ? {
573
+ key: first.key,
574
+ inset: current.offset - first.start
575
+ } : void 0;
576
+ }
577
+ function restore(saved, fallback) {
578
+ const index = saved ? items.findIndex((item) => keyOf(item) === saved.key) : -1;
579
+ move(index >= 0 && saved ? virtual.getOffsetForIndex(index, "start") + saved.inset : fallback);
580
+ if (element) {
581
+ const offset = virtual.getWindow().offset;
582
+ if (horizontal) element.scrollLeft = offset === 0 ? 0 : offset + margin();
583
+ else element.scrollTop = offset === 0 ? 0 : offset + margin();
584
+ }
585
+ }
586
+ function measure(target, key) {
587
+ const saved = anchor();
588
+ const offset = virtual.getWindow().offset;
589
+ const box = target.getBoundingClientRect();
590
+ const size = horizontal ? box.width : box.height;
591
+ if (size > 0 && Number.isFinite(size)) {
592
+ const previous = virtual.getWindow();
593
+ virtual.measure(key, size);
594
+ if (virtual.getWindow() === previous) return;
595
+ restore(saved, offset);
596
+ }
597
+ }
598
+ function readViewport() {
599
+ if (element) virtual.setViewport(Math.max(0, (horizontal ? element.scrollLeft : element.scrollTop) - margin()), viewportSize());
600
+ }
601
+ const stopSource = watch(source, (next) => {
602
+ const saved = anchor();
603
+ const offset = virtual.getWindow().offset;
604
+ items = next;
605
+ virtual.setOptions(layoutOptions());
606
+ if (mounted) restore(saved, offset);
607
+ else seed();
608
+ }, { flush: "sync" });
609
+ let stopElement;
610
+ if (getCurrentInstance()) onMounted(() => {
611
+ mounted = true;
612
+ virtual.setOptions(layoutOptions());
613
+ stopElement = watch(() => toValue(options.scrollElement), (next) => {
614
+ detach?.();
615
+ observer?.disconnect();
616
+ observer = void 0;
617
+ element = next;
618
+ if (!next) {
619
+ seed();
620
+ return;
621
+ }
622
+ const owner = next.ownerDocument.defaultView;
623
+ const Observer = owner?.ResizeObserver;
624
+ if (Observer) {
625
+ observer = new Observer((entries) => {
626
+ for (const entry of entries) if (entry.target === element) readViewport();
627
+ else {
628
+ const key = keysByElement.get(entry.target);
629
+ if (key !== void 0) measure(entry.target, key);
630
+ }
631
+ });
632
+ observer.observe(next);
633
+ for (const target of elements.values()) observer.observe(target);
634
+ } else owner?.addEventListener("resize", readViewport);
635
+ next.addEventListener("scroll", readViewport, { passive: true });
636
+ detach = () => {
637
+ next.removeEventListener("scroll", readViewport);
638
+ owner?.removeEventListener("resize", readViewport);
639
+ };
640
+ readViewport();
641
+ for (const [key, target] of elements) measure(target, key);
642
+ }, {
643
+ immediate: true,
644
+ flush: "post"
645
+ });
646
+ });
647
+ if (getCurrentScope()) onScopeDispose(() => {
648
+ disposed = true;
649
+ stopSource();
650
+ stopElement?.();
651
+ detach?.();
652
+ observer?.disconnect();
653
+ unsubscribe();
654
+ elements.clear();
655
+ keysByElement.clear();
656
+ element = void 0;
657
+ });
658
+ margin();
659
+ const binding = {
660
+ getItem(index) {
661
+ view.value;
662
+ const item = virtual.getItem(index);
663
+ return item ? Object.freeze(project(item, items[index])) : void 0;
664
+ },
665
+ scrollToIndex(index, { align = "auto" } = {}) {
666
+ move(virtual.getOffsetForIndex(index, align));
667
+ },
668
+ scrollToKey(key, align) {
669
+ const index = items.findIndex((item) => keyOf(item) === key);
670
+ if (index >= 0) binding.scrollToIndex(index, align);
671
+ },
672
+ measureElement(target, key) {
673
+ if (disposed) return;
674
+ const previous = elements.get(key);
675
+ if (previous === target) return;
676
+ if (previous) {
677
+ observer?.unobserve(previous);
678
+ keysByElement.delete(previous);
679
+ elements.delete(key);
680
+ }
681
+ if (!target || !("getBoundingClientRect" in target)) return;
682
+ elements.set(key, target);
683
+ keysByElement.set(target, key);
684
+ if (mounted) {
685
+ observer?.observe(target);
686
+ nextTick(() => {
687
+ if (!disposed && elements.get(key) === target) measure(target, key);
688
+ });
689
+ }
690
+ }
691
+ };
692
+ for (const key of Object.keys(view.value)) Object.defineProperty(binding, key, {
693
+ enumerable: true,
694
+ get: () => key === "items" ? projected.value : key === "issues" && (initialIssue || marginIssue.value) ? Object.freeze([
695
+ ...view.value.issues,
696
+ ...initialIssue ? [initialIssue] : [],
697
+ ...marginIssue.value ? [marginIssue.value] : []
698
+ ]) : view.value[key]
699
+ });
700
+ return Object.freeze(binding);
701
+ }
702
+ /** Virtualize data and detail render items. scrollToKey accepts the parent data-row key. */
703
+ function useVirtualRows(table, options) {
704
+ const virtual = useVirtualAxis(() => table.renderItems, (item) => item.key, options.estimateRowHeight ?? 40, options, false, (item, renderItem) => ({
705
+ ...item,
706
+ renderItem
707
+ }));
708
+ const stopFocus = watch(() => options.grid?.selection?.focus.row, (row) => {
709
+ if (row === void 0) return;
710
+ const direct = table.renderItems[row];
711
+ const index = direct?.kind === "row" && direct.rowIndex === row ? row : table.renderItems.findIndex((item) => item.kind === "row" && item.rowIndex === row);
712
+ if (index >= 0) virtual.scrollToIndex(index);
713
+ }, { flush: "sync" });
714
+ if (getCurrentScope()) onScopeDispose(stopFocus);
715
+ const binding = Object.defineProperties({}, {
716
+ ...Object.getOwnPropertyDescriptors(virtual),
717
+ scrollToKey: {
718
+ enumerable: true,
719
+ value: (key, align) => virtual.scrollToKey(getRowItemKey(key), align)
720
+ }
721
+ });
722
+ return Object.freeze(binding);
723
+ }
724
+ /** Virtualize visible columns and bring the active logical column into view. */
725
+ function useVirtualColumns(grid, options) {
726
+ const virtual = useVirtualAxis(() => grid.table.columns, (column) => column.id, options.estimateColumnWidth ?? 120, options, true, (item, column) => ({
727
+ ...item,
728
+ column
729
+ }));
730
+ const stopFocus = watch(() => grid.selection?.focus.column, (column) => {
731
+ if (column !== void 0) virtual.scrollToIndex(column);
732
+ }, { flush: "sync" });
733
+ if (getCurrentScope()) onScopeDispose(stopFocus);
734
+ return virtual;
735
+ }
736
+
737
+ //#endregion
738
+ //#region src/source.ts
739
+ /** Internal owner of one cancellable source, using the engine's ingestion/conflict boundary. */
740
+ function createSource(table, options, seed, publish) {
741
+ const instance = getCurrentInstance();
742
+ const requestedThreshold = options.endThreshold ?? 5;
743
+ const endThreshold = Number.isSafeInteger(requestedThreshold) && requestedThreshold >= 0 ? requestedThreshold : 5;
744
+ const optionIssues = requestedThreshold === endThreshold ? [] : [Object.freeze({
745
+ code: "invalid_stream_option",
746
+ message: "endThreshold must be a non-negative safe integer; 5 is used."
747
+ })];
748
+ const mode = shallowRef(void 0);
749
+ const more = shallowRef(false);
750
+ const retryVersion = shallowRef(0);
751
+ const query = shallowRef(table.getState());
752
+ let cursor;
753
+ let controller;
754
+ let task;
755
+ let cancelFrame;
756
+ let pending;
757
+ let active = false;
758
+ let disposed = false;
759
+ let mounted = !instance;
760
+ let first = true;
761
+ let cursorFailed = false;
762
+ let generation = 0;
763
+ let stops = [];
764
+ const initialMode = () => unref(options.source) ? "source" : unref(options.loadMore) ? "cursor" : void 0;
765
+ const schedule = options.scheduleFrame ?? ((flush) => {
766
+ const view = (instance?.proxy?.$el)?.ownerDocument?.defaultView;
767
+ if (view) {
768
+ const id = view.requestAnimationFrame(flush);
769
+ return () => view.cancelAnimationFrame(id);
770
+ }
771
+ const id = setTimeout(flush, 16);
772
+ return () => clearTimeout(id);
773
+ });
774
+ const project = (next) => {
775
+ const loadState = !mounted && initialMode() ? "loading" : mode.value === "cursor" && more.value && next.loadState === "done" ? "idle" : next.loadState;
776
+ return loadState === next.loadState && optionIssues.length === 0 ? next : Object.freeze({
777
+ ...next,
778
+ loadState,
779
+ issues: Object.freeze([...next.issues, ...optionIssues])
780
+ });
781
+ };
782
+ const flush = () => {
783
+ cancelFrame = void 0;
784
+ if (pending && !disposed) {
785
+ active = pending.loadState === "loading" || pending.loadState === "streaming";
786
+ publish(project(pending));
787
+ }
788
+ pending = void 0;
789
+ };
790
+ const update = (next) => {
791
+ const state = next.state;
792
+ const previous = query.value;
793
+ if (previous.sorting !== state.sorting || previous.search !== state.search || previous.filters !== state.filters) {
794
+ query.value = state;
795
+ next = table.getSnapshot();
796
+ }
797
+ if (active) {
798
+ pending = next;
799
+ cancelFrame ??= schedule(flush);
800
+ } else publish(project(next));
801
+ };
802
+ const stop = () => {
803
+ controller?.abort();
804
+ controller = void 0;
805
+ task = void 0;
806
+ active = false;
807
+ cancelFrame?.();
808
+ cancelFrame = void 0;
809
+ pending = void 0;
810
+ };
811
+ const reset = () => {
812
+ stop();
813
+ cursor = void 0;
814
+ cursorFailed = false;
815
+ if (!first) table.setData(seed());
816
+ first = false;
817
+ };
818
+ const loadNext = () => {
819
+ if (disposed || !mounted || mode.value !== "cursor" || !more.value || cursorFailed) return Promise.resolve();
820
+ if (task) return task;
821
+ const loader = unref(options.loadMore);
822
+ const run = new AbortController();
823
+ controller = run;
824
+ active = true;
825
+ const state = table.getState();
826
+ const previousCursor = cursor;
827
+ const source = { async *[Symbol.asyncIterator]() {
828
+ const result = await loader({
829
+ cursor: previousCursor,
830
+ state,
831
+ signal: run.signal
832
+ });
833
+ if (run.signal.aborted) return;
834
+ if (!Array.isArray(result.rows) || typeof result.done !== "boolean") throw new Error("loadMore must return rows and a boolean done flag.");
835
+ if (!result.done && result.rows.length === 0 && Object.is(result.cursor, previousCursor)) throw new Error("loadMore returned no rows or cursor progress. Retry after correcting the response.");
836
+ cursor = result.cursor;
837
+ more.value = !result.done;
838
+ yield result.rows;
839
+ } };
840
+ task = table.stream(source, {
841
+ ...options.streamOptions,
842
+ signal: run.signal
843
+ }).then((result) => {
844
+ if (controller !== run || disposed) return void 0;
845
+ cursorFailed = result.status === "error";
846
+ task = void 0;
847
+ if (!cancelFrame) active = false;
848
+ });
849
+ return task;
850
+ };
851
+ const start = () => {
852
+ if (disposed) return;
853
+ mounted = true;
854
+ stops = [watchEffect((cleanup) => {
855
+ retryVersion.value;
856
+ const source = unref(options.source);
857
+ const loader = unref(options.loadMore);
858
+ const run = new AbortController();
859
+ const ticket = ++generation;
860
+ cleanup(() => run.abort());
861
+ let iterable;
862
+ if (source) {
863
+ try {
864
+ iterable = typeof source === "function" ? source({ signal: run.signal }) : source;
865
+ } catch (error) {
866
+ iterable = { [Symbol.asyncIterator]: () => ({ next: () => Promise.reject(error) }) };
867
+ }
868
+ if (loader) iterable = { [Symbol.asyncIterator]: () => ({ next: () => Promise.reject(/* @__PURE__ */ new Error("Choose source or loadMore, rather than combining them.")) }) };
869
+ }
870
+ queueMicrotask(() => {
871
+ if (disposed || run.signal.aborted || ticket !== generation) return;
872
+ reset();
873
+ mode.value = source ? "source" : loader ? "cursor" : void 0;
874
+ more.value = !!loader && !source;
875
+ if (iterable) {
876
+ controller = run;
877
+ active = true;
878
+ table.stream(iterable, {
879
+ ...options.streamOptions,
880
+ signal: run.signal
881
+ }).then(() => {
882
+ if (controller === run && !cancelFrame) active = false;
883
+ });
884
+ } else if (loader) loadNext();
885
+ else update(table.getSnapshot());
886
+ });
887
+ }), watch(query, () => {
888
+ if (mode.value !== "cursor") return;
889
+ reset();
890
+ more.value = true;
891
+ loadNext();
892
+ }, { flush: "sync" })];
893
+ };
894
+ if (instance) onMounted(start);
895
+ else queueMicrotask(start);
896
+ return {
897
+ endThreshold,
898
+ get loadingMode() {
899
+ return mode.value ?? (!mounted ? initialMode() : void 0);
900
+ },
901
+ get canLoadMore() {
902
+ return more.value && !cursorFailed && !task;
903
+ },
904
+ get loadError() {
905
+ const next = table.getSnapshot();
906
+ return next.loadState === "error" ? next.issues.find((item) => item.code === "stream_error") : void 0;
907
+ },
908
+ loadNext,
909
+ retry() {
910
+ if (disposed) return;
911
+ if (mode.value === "cursor" && cursorFailed) {
912
+ cursorFailed = false;
913
+ loadNext();
914
+ } else retryVersion.value++;
915
+ },
916
+ update,
917
+ initial: () => project(table.getSnapshot()),
918
+ dispose() {
919
+ disposed = true;
920
+ stop();
921
+ for (const disposeWatch of stops) disposeWatch();
922
+ stops = [];
923
+ }
924
+ };
925
+ }
926
+
190
927
  //#endregion
191
928
  //#region src/use-data-table.ts
192
929
  const OPERATIONS = [
930
+ "appendData",
931
+ "upsertData",
932
+ "removeData",
933
+ "stream",
934
+ "insertRows",
935
+ "removeRows",
936
+ "getPendingChanges",
937
+ "markSaved",
938
+ "revert",
193
939
  "setState",
194
940
  "setData",
195
941
  "setColumns",
942
+ "setTreeFilter",
196
943
  "setRowCount",
197
944
  "toggleSort",
198
945
  "sort",
@@ -210,6 +957,9 @@ const OPERATIONS = [
210
957
  "toggleAll",
211
958
  "clearSelection",
212
959
  "getSelectedRows",
960
+ "toggleExpanded",
961
+ "expandAll",
962
+ "collapseAll",
213
963
  "toggleColumn",
214
964
  "moveColumn",
215
965
  "edit",
@@ -225,27 +975,33 @@ const OPERATIONS = [
225
975
  function definedEntries(patch) {
226
976
  return Object.fromEntries(Object.entries(patch ?? {}).filter(([, value]) => value !== void 0));
227
977
  }
228
- /**
229
- * Create a table bound to the current effect scope. The table follows its reactive data,
230
- * columns, row count, and state, and stops listening when the scope ends.
231
- */
232
978
  function useDataTable(options) {
979
+ const empty = Object.freeze([]);
980
+ const seed = () => toRaw(toValue(options.data) ?? empty);
233
981
  const table = createTable({
234
982
  ...options,
235
- data: toRaw(toValue(options.data)),
983
+ manual: options.manual ?? !!unref(options.loadMore),
984
+ data: seed(),
236
985
  columns: toValue(options.columns),
237
986
  rowCount: toValue(options.rowCount),
987
+ treeFilter: toValue(options.treeFilter),
238
988
  initialState: {
239
989
  ...options.initialState,
240
990
  ...definedEntries(toValue(options.state))
241
- }
991
+ },
992
+ createChildLoadController: options.createChildLoadController ?? (() => new AbortController())
242
993
  });
994
+ const drafts = /* @__PURE__ */ new Set();
995
+ let disposed = false;
243
996
  const snapshot = shallowRef(table.getSnapshot());
244
- const unsubscribe = table.subscribe((next) => {
997
+ const source = createSource(table, options, seed, (next) => {
245
998
  snapshot.value = next;
246
999
  });
1000
+ snapshot.value = source.initial();
1001
+ const unsubscribe = table.subscribe(source.update);
247
1002
  const stops = [
248
- watch(() => toValue(options.data), (data) => table.setData(toRaw(data))),
1003
+ watch(() => toValue(options.treeFilter), (mode) => table.setTreeFilter(mode)),
1004
+ watch(() => toValue(options.data), (data) => table.setData(toRaw(data ?? empty))),
249
1005
  watch(() => toValue(options.columns), (columns) => table.setColumns(columns)),
250
1006
  watch(() => toValue(options.rowCount), (rowCount) => {
251
1007
  if (rowCount !== void 0) table.setRowCount(rowCount);
@@ -254,24 +1010,63 @@ function useDataTable(options) {
254
1010
  if (state) table.setState(state);
255
1011
  }, { deep: true })
256
1012
  ];
257
- if (getCurrentScope()) onScopeDispose(() => {
1013
+ const dispose = () => {
1014
+ if (disposed) return;
1015
+ disposed = true;
1016
+ source.dispose();
1017
+ for (const disposeDraft of drafts) disposeDraft();
1018
+ drafts.clear();
258
1019
  for (const stop of stops) stop();
259
1020
  unsubscribe();
260
1021
  table.destroy();
261
- });
1022
+ };
1023
+ if (getCurrentScope()) onScopeDispose(dispose);
262
1024
  const binding = { table };
1025
+ for (const key of [
1026
+ "endThreshold",
1027
+ "loadingMode",
1028
+ "canLoadMore",
1029
+ "loadError"
1030
+ ]) Object.defineProperty(binding, key, {
1031
+ enumerable: true,
1032
+ get: () => {
1033
+ snapshot.value;
1034
+ return source[key];
1035
+ }
1036
+ });
1037
+ binding["retry"] = source.retry;
1038
+ binding["loadNext"] = source.loadNext;
263
1039
  Object.defineProperty(binding, "snapshot", {
264
1040
  enumerable: true,
265
1041
  get: () => snapshot.value
266
1042
  });
1043
+ Object.defineProperty(binding, "expanded", {
1044
+ enumerable: true,
1045
+ get: () => snapshot.value.state.expanded
1046
+ });
1047
+ Object.defineProperty(binding, "pendingChanges", {
1048
+ enumerable: true,
1049
+ get: () => {
1050
+ snapshot.value;
1051
+ return table.getPendingChanges();
1052
+ }
1053
+ });
1054
+ binding["editRow"] = (key) => createRowDraft(table, key, (disposeDraft) => {
1055
+ if (disposed) disposeDraft();
1056
+ else drafts.add(disposeDraft);
1057
+ return () => {
1058
+ drafts.delete(disposeDraft);
1059
+ };
1060
+ });
267
1061
  for (const key of Object.keys(snapshot.value)) Object.defineProperty(binding, key, {
268
1062
  enumerable: true,
269
1063
  get: () => snapshot.value[key]
270
1064
  });
271
1065
  for (const name of OPERATIONS) binding[name] = table[name];
1066
+ binding["destroy"] = dispose;
272
1067
  return Object.freeze(binding);
273
1068
  }
274
1069
 
275
1070
  //#endregion
276
- export { injectDataGrid, injectDataTable, provideDataGrid, provideDataTable, useDataGrid, useDataTable };
1071
+ export { injectDataGrid, injectDataTable, provideDataGrid, provideDataTable, useDataGrid, useDataTable, useVirtualColumns, useVirtualRows };
277
1072
  //# sourceMappingURL=index.js.map