@vueye-table/headless 3.0.0-alpha.9 → 3.0.0-beta.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
@@ -1,8 +1,20 @@
1
- import { injectDataGrid, injectDataTable, provideDataGrid, provideDataTable, useDataGrid, useVirtualColumns, useVirtualRows } from "@vueye-table/vue";
1
+ import { injectDataGrid, injectDataTable, provideDataGrid, provideDataTable, useDataGrid, useTableLocale, useVirtualColumns, useVirtualRows } from "@vueye-table/vue";
2
2
  import { Fragment, computed, defineComponent, h, inject, nextTick, onMounted, onScopeDispose, provide, ref, shallowRef, useId, watch } from "vue";
3
- import { getRowItemKey, gridCommand } from "@vueye-table/core";
3
+ import { formatValue, getRowItemKey, gridCommand } from "@vueye-table/core";
4
4
 
5
5
  //#region src/shared.ts
6
+ /** Leading native utility cells, independent of data-column coordinates. */
7
+ const columnLayoutProps = { leadingColumns: {
8
+ type: Number,
9
+ default: 0
10
+ } };
11
+ const columnOffsetKey = Symbol("vt-leading-columns");
12
+ function provideColumnOffset(offset) {
13
+ provide(columnOffsetKey, offset);
14
+ }
15
+ function injectColumnOffset() {
16
+ return inject(columnOffsetKey, () => 0);
17
+ }
6
18
  /** The element a component renders. Every headless component accepts `as`. */
7
19
  const asProp = (tag) => ({
8
20
  type: String,
@@ -20,6 +32,13 @@ const rowProp = {
20
32
  function flag(value) {
21
33
  return value ? "" : void 0;
22
34
  }
35
+ /**
36
+ * The direction an element is laid out in, read from the nearest `dir` attribute, so keyboard
37
+ * arrows can follow what the reader sees. Without one, left to right.
38
+ */
39
+ function directionOf(element) {
40
+ return element?.closest("[dir]")?.getAttribute("dir")?.toLowerCase() === "rtl" ? "rtl" : "ltr";
41
+ }
23
42
 
24
43
  //#endregion
25
44
  //#region src/controls.ts
@@ -43,10 +62,11 @@ const DataTableSelectAll = defineComponent({
43
62
  slots: Object,
44
63
  props: { label: {
45
64
  type: String,
46
- default: "Select all rows"
65
+ default: void 0
47
66
  } },
48
67
  setup(props, { slots }) {
49
68
  const table = injectDataTable("<DataTableSelectAll>");
69
+ const locale = useTableLocale();
50
70
  return () => {
51
71
  const state = table.allSelection;
52
72
  const toggle = () => table.toggleAll();
@@ -56,7 +76,7 @@ const DataTableSelectAll = defineComponent({
56
76
  }) ?? checkbox({
57
77
  checked: state === "all",
58
78
  indeterminate: state === "some",
59
- label: props.label,
79
+ label: props.label ?? locale().messages.selectAllRows,
60
80
  disabled: table.selectionMode !== "multiple" || table.rows.length === 0,
61
81
  onChange: toggle
62
82
  });
@@ -69,13 +89,19 @@ const DataTableSelectRow = defineComponent({
69
89
  slots: Object,
70
90
  props: {
71
91
  row: rowProp,
92
+ /** Defaults to the locale's "Select row 3", the row's place in the table as shown. */
72
93
  label: {
73
94
  type: String,
74
- default: "Select row"
95
+ default: void 0
75
96
  }
76
97
  },
77
98
  setup(props, { slots }) {
78
99
  const table = injectDataTable("<DataTableSelectRow>");
100
+ const locale = useTableLocale();
101
+ const position = () => {
102
+ const index = table.rows.indexOf(props.row);
103
+ return index < 0 ? props.row.index + 1 : Math.max(1, table.pageStart) + index;
104
+ };
79
105
  return () => {
80
106
  const selected = table.tree ? props.row.selection === "all" : table.isSelected(props.row.key);
81
107
  const indeterminate = table.tree && props.row.selection === "some";
@@ -87,7 +113,7 @@ const DataTableSelectRow = defineComponent({
87
113
  }) ?? checkbox({
88
114
  checked: selected,
89
115
  indeterminate,
90
- label: props.label,
116
+ label: props.label ?? locale().messages.selectRow({ position: locale().count(position()) }),
91
117
  disabled: table.selectionMode === "none",
92
118
  onChange: toggle
93
119
  });
@@ -107,7 +133,7 @@ const DataTableSearch = defineComponent({
107
133
  },
108
134
  label: {
109
135
  type: String,
110
- default: "Search"
136
+ default: void 0
111
137
  },
112
138
  placeholder: {
113
139
  type: String,
@@ -116,13 +142,14 @@ const DataTableSearch = defineComponent({
116
142
  },
117
143
  setup(props) {
118
144
  const table = injectDataTable("<DataTableSearch>");
145
+ const locale = useTableLocale();
119
146
  let timer;
120
147
  onScopeDispose(() => clearTimeout(timer));
121
148
  return () => h("input", {
122
149
  type: "search",
123
150
  value: table.state.search,
124
151
  placeholder: props.placeholder,
125
- "aria-label": props.label,
152
+ "aria-label": props.label ?? locale().messages.search,
126
153
  onInput: (event) => {
127
154
  const { value } = event.target;
128
155
  clearTimeout(timer);
@@ -143,12 +170,14 @@ const DataTablePagination = defineComponent({
143
170
  as: asProp("nav"),
144
171
  label: {
145
172
  type: String,
146
- default: "Pagination"
173
+ default: void 0
147
174
  }
148
175
  },
149
176
  setup(props, { slots }) {
150
177
  const table = injectDataTable("<DataTablePagination>");
178
+ const locale = useTableLocale();
151
179
  return () => {
180
+ const { messages, count } = locale();
152
181
  const slotProps = {
153
182
  page: table.page,
154
183
  pageCount: table.pageCount,
@@ -167,14 +196,14 @@ const DataTablePagination = defineComponent({
167
196
  "data-current": flag(current),
168
197
  onClick
169
198
  }, content);
170
- return h(props.as, { "aria-label": props.label }, slots.default?.(slotProps) ?? [
171
- button("Previous page", "‹", !table.canPreviousPage, table.previousPage),
199
+ return h(props.as, { "aria-label": props.label ?? messages.pagination }, slots.default?.(slotProps) ?? [
200
+ button(messages.previousPage, "‹", !table.canPreviousPage, table.previousPage),
172
201
  ...table.pageItems.map((item) => item.type === "gap" ? h("span", {
173
202
  key: item.key,
174
203
  "aria-hidden": "true",
175
204
  "data-gap": ""
176
- }, "…") : button(`Page ${item.page}`, String(item.page), false, () => table.goToPage(item.page), item.current)),
177
- button("Next page", "›", !table.canNextPage, table.nextPage)
205
+ }, "…") : button(messages.page(count(item.page)), count(item.page).text, false, () => table.goToPage(item.page), item.current)),
206
+ button(messages.nextPage, "›", !table.canNextPage, table.nextPage)
178
207
  ]);
179
208
  };
180
209
  }
@@ -194,22 +223,23 @@ const DataTablePageSize = defineComponent({
194
223
  },
195
224
  label: {
196
225
  type: String,
197
- default: "Rows per page"
226
+ default: void 0
198
227
  }
199
228
  },
200
229
  setup(props) {
201
230
  const table = injectDataTable("<DataTablePageSize>");
231
+ const locale = useTableLocale();
202
232
  return () => {
203
233
  const sizes = props.options.includes(table.pageSize) ? props.options : [...props.options, table.pageSize].toSorted((left, right) => left - right);
204
234
  return h("select", {
205
235
  value: table.pageSize,
206
- "aria-label": props.label,
236
+ "aria-label": props.label ?? locale().messages.rowsPerPage,
207
237
  onChange: (event) => table.setPageSize(Number(event.target.value))
208
238
  }, sizes.map((size) => h("option", {
209
239
  key: size,
210
240
  value: size,
211
241
  selected: size === table.pageSize
212
- }, String(size))));
242
+ }, locale().count(size).text)));
213
243
  };
214
244
  }
215
245
  });
@@ -221,11 +251,12 @@ const DataTableColumnVisibility = defineComponent({
221
251
  as: asProp("fieldset"),
222
252
  legend: {
223
253
  type: String,
224
- default: "Columns"
254
+ default: void 0
225
255
  }
226
256
  },
227
257
  setup(props, { slots }) {
228
258
  const table = injectDataTable("<DataTableColumnVisibility>");
259
+ const locale = useTableLocale();
229
260
  return () => {
230
261
  const hidden = new Set(table.state.hiddenColumns);
231
262
  const columns = table.allColumns.map((column) => ({
@@ -235,7 +266,7 @@ const DataTableColumnVisibility = defineComponent({
235
266
  return h(props.as, slots.default?.({
236
267
  columns,
237
268
  toggle: table.toggleColumn
238
- }) ?? [props.as === "fieldset" ? h("legend", props.legend) : null, ...columns.map(({ column, visible }) => h("label", {
269
+ }) ?? [props.as === "fieldset" ? h("legend", props.legend ?? locale().messages.columns) : null, ...columns.map(({ column, visible }) => h("label", {
239
270
  key: column.id,
240
271
  "data-column": column.id
241
272
  }, [h("input", {
@@ -253,7 +284,9 @@ const DataTableStatus = defineComponent({
253
284
  props: { as: asProp("p") },
254
285
  setup(props, { slots }) {
255
286
  const table = injectDataTable("<DataTableStatus>");
287
+ const locale = useTableLocale();
256
288
  return () => {
289
+ const { messages, count } = locale();
257
290
  const slotProps = {
258
291
  start: table.pageStart,
259
292
  end: table.pageEnd,
@@ -269,12 +302,23 @@ const DataTableStatus = defineComponent({
269
302
  loadingChildrenCount: table.tree ? table.processedRows.filter((row) => row.childStatus === "loading").length : 0,
270
303
  childErrorCount: table.tree ? table.processedRows.filter((row) => row.childStatus === "error").length : 0
271
304
  };
272
- const count = `Loaded ${table.loadedRowCount.toLocaleString()} ${table.loadedRowCount === 1 ? "row" : "rows"}`;
273
- const text = table.loadingMode ? table.loadState === "error" ? `${count}. Could not load rows. Retry loading.` : table.loadState === "aborted" ? `${count}. Loading stopped.` : table.loadState === "loading" || table.loadState === "streaming" ? `${count}…` : `${count}${table.loadState === "done" ? ". All rows loaded." : "."}` : table.rowCount === 0 ? "No rows" : `${table.pageStart}–${table.pageEnd} of ${table.rowCount} rows` + (table.selectedCount > 0 ? `, ${table.selectedCount} selected` : "");
305
+ const text = table.loadingMode ? messages.loadedStatus({
306
+ loaded: count(table.loadedRowCount),
307
+ state: table.loadState
308
+ }) : table.rowCount === 0 ? messages.noRows : messages.rangeStatus({
309
+ start: count(table.pageStart),
310
+ end: count(table.pageEnd),
311
+ total: count(table.rowCount),
312
+ selected: count(table.selectedCount)
313
+ });
274
314
  return h(props.as, {
275
315
  role: "status",
276
316
  "aria-live": "polite"
277
- }, slots.default?.(slotProps) ?? text + (slotProps.loadingChildrenCount ? " Loading children…" : "") + (slotProps.childErrorCount ? " Could not load children. Retry children in the affected row." : ""));
317
+ }, slots.default?.(slotProps) ?? [
318
+ text,
319
+ slotProps.loadingChildrenCount ? messages.loadingChildren : "",
320
+ slotProps.childErrorCount ? messages.childrenError : ""
321
+ ].filter((part) => part !== "").join(" "));
278
322
  };
279
323
  }
280
324
  });
@@ -282,7 +326,7 @@ const DataTableStatus = defineComponent({
282
326
  //#endregion
283
327
  //#region src/loading.ts
284
328
  /** Keep this visual sentinel out of the live region and logical data-row count. */
285
- function loadSentinel(table, colspan) {
329
+ function loadSentinel(table, colspan, locale) {
286
330
  return table.loadingMode === "cursor" && table.loadedRowCount > 0 && (table.loadState === "loading" || table.loadState === "streaming") ? h("tr", {
287
331
  key: "vt-load-sentinel",
288
332
  "data-load-sentinel": "",
@@ -290,7 +334,7 @@ function loadSentinel(table, colspan) {
290
334
  }, [h("td", {
291
335
  colspan: Math.max(1, colspan),
292
336
  class: "vt-load-sentinel"
293
- }, "Loading more rows…")]) : void 0;
337
+ }, locale?.messages.loadingMoreRows ?? "Loading more rows…")]) : void 0;
294
338
  }
295
339
  /** A keyboard-accessible alternative to automatic virtual-end loading, with error recovery. */
296
340
  const DataTableLoadMore = defineComponent({
@@ -298,32 +342,202 @@ const DataTableLoadMore = defineComponent({
298
342
  props: {
299
343
  label: {
300
344
  type: String,
301
- default: "Load more rows"
345
+ default: void 0
302
346
  },
303
347
  retryLabel: {
304
348
  type: String,
305
- default: "Retry loading"
349
+ default: void 0
306
350
  },
307
351
  loadingLabel: {
308
352
  type: String,
309
- default: "Loading rows…"
353
+ default: void 0
310
354
  }
311
355
  },
312
356
  setup(props) {
313
357
  const table = injectDataTable("<DataTableLoadMore>");
358
+ const locale = useTableLocale();
314
359
  return () => table.loadState === "error" ? h("button", {
315
360
  type: "button",
316
361
  onClick: () => table.retry()
317
- }, props.retryLabel) : table.loadingMode === "cursor" && table.loadState !== "done" ? h("button", {
362
+ }, props.retryLabel ?? locale().messages.retryLoading) : table.loadingMode === "cursor" && table.loadState !== "done" ? h("button", {
318
363
  type: "button",
319
364
  disabled: !table.canLoadMore,
320
365
  onClick: () => {
321
366
  table.loadNext();
322
367
  }
323
- }, table.loadState === "loading" || table.loadState === "streaming" ? props.loadingLabel : props.label) : null;
368
+ }, table.loadState === "loading" || table.loadState === "streaming" ? props.loadingLabel ?? locale().messages.loadingRows : props.label ?? locale().messages.loadMoreRows) : null;
324
369
  }
325
370
  });
326
371
 
372
+ //#endregion
373
+ //#region src/editor.ts
374
+ function editorText(value, kind) {
375
+ if (value === null || value === void 0) return "";
376
+ if (kind === "date" && value instanceof Date) return Number.isFinite(value.getTime()) ? value.toISOString().slice(0, 10) : "";
377
+ return formatValue(value);
378
+ }
379
+ const cellEditorProps = {
380
+ editor: {
381
+ type: Object,
382
+ required: true
383
+ },
384
+ spec: {
385
+ type: Object,
386
+ default: () => ({ kind: "text" })
387
+ },
388
+ /** Keyboard movement is owned by the surrounding cell. */
389
+ finish: {
390
+ type: Function,
391
+ default: void 0
392
+ },
393
+ autofocus: {
394
+ type: Boolean,
395
+ default: true
396
+ },
397
+ blurCommit: {
398
+ type: Boolean,
399
+ default: true
400
+ },
401
+ tabCommit: {
402
+ type: Boolean,
403
+ default: true
404
+ }
405
+ };
406
+ /** Plain native fields. Select results are searched and capped at 50 mounted options. */
407
+ const DataCellEditor = defineComponent({
408
+ name: "DataCellEditor",
409
+ props: cellEditorProps,
410
+ setup(props) {
411
+ const locale = useTableLocale();
412
+ const field = ref();
413
+ const query = ref("");
414
+ const options = props.spec.options ?? [];
415
+ const matches = computed(() => {
416
+ const search = query.value.trim().toLocaleLowerCase();
417
+ return options.map((value, index) => ({
418
+ value,
419
+ index
420
+ })).filter(({ value }) => String(value ?? "").toLocaleLowerCase().includes(search));
421
+ });
422
+ onMounted(() => {
423
+ if (!props.autofocus) return;
424
+ field.value?.focus();
425
+ if (field.value && "setSelectionRange" in field.value && ["text", "number"].includes(props.spec.kind)) field.value.setSelectionRange(field.value.value.length, field.value.value.length);
426
+ });
427
+ const finish = (direction) => {
428
+ if (props.finish) props.finish(direction);
429
+ else props.editor.commit();
430
+ };
431
+ const keydown = (event) => {
432
+ if (event.isComposing) return;
433
+ if (event.key === "Escape") {
434
+ event.preventDefault();
435
+ event.stopPropagation();
436
+ if (!props.editor.pending) props.editor.cancel();
437
+ } else if (event.key === "Enter" || event.key === "Tab") {
438
+ if (event.key === "Tab" && !props.tabCommit) return;
439
+ event.preventDefault();
440
+ event.stopPropagation();
441
+ if (!props.editor.pending) finish(event.key === "Tab" ? event.shiftKey ? "left" : "right" : event.shiftKey ? "up" : "down");
442
+ }
443
+ };
444
+ const paste = (event) => {
445
+ const text = event.clipboardData?.getData("text/plain");
446
+ if (text === void 0 || props.editor.pending) return;
447
+ event.preventDefault();
448
+ const target = event.target;
449
+ const start = target.selectionStart ?? 0;
450
+ const end = target.selectionEnd ?? target.value.length;
451
+ props.editor.input(target.value.slice(0, start) + text + target.value.slice(end));
452
+ };
453
+ return () => {
454
+ const { editor, spec } = props;
455
+ const { messages, count } = locale();
456
+ const attrs = {
457
+ ...editor.attrs,
458
+ "aria-label": messages.editCell(editor.column.header),
459
+ "data-editor": "",
460
+ disabled: editor.pending,
461
+ onKeydown: keydown
462
+ };
463
+ if (spec.kind === "select") {
464
+ const shown = matches.value.slice(0, 50);
465
+ return h("div", {
466
+ "data-select-editor": "",
467
+ onKeydown: (event) => {
468
+ if (event.key === "Escape") keydown(event);
469
+ }
470
+ }, [
471
+ h("input", {
472
+ ref: field,
473
+ type: "search",
474
+ value: query.value,
475
+ "aria-label": messages.searchOptions(editor.column.header),
476
+ disabled: editor.pending,
477
+ onInput: (event) => {
478
+ query.value = event.target.value;
479
+ },
480
+ onKeydown: (event) => {
481
+ if (event.key === "Enter" && shown[0]) {
482
+ event.preventDefault();
483
+ editor.commit(shown[0].value);
484
+ } else keydown(event);
485
+ }
486
+ }),
487
+ h("select", {
488
+ ...attrs,
489
+ value: options.findIndex((value) => Object.is(value, editor.value)),
490
+ size: Math.max(2, Math.min(5, shown.length)),
491
+ onChange: (event) => {
492
+ const index = Number(event.target.value);
493
+ if (index >= 0) editor.commit(options[index]);
494
+ }
495
+ }, shown.map(({ value, index }) => h("option", {
496
+ key: index,
497
+ value: index
498
+ }, value === null || value === void 0 ? messages.emptyOption : String(value)))),
499
+ h("span", {
500
+ role: "status",
501
+ "data-editor-hint": ""
502
+ }, matches.value.length === 0 ? messages.noMatchingOptions : matches.value.length > 50 ? messages.firstMatchesOnly(count(50)) : messages.optionCount(count(matches.value.length)))
503
+ ]);
504
+ }
505
+ return h("input", {
506
+ ...attrs,
507
+ ref: field,
508
+ type: spec.kind === "date" ? "date" : spec.kind === "checkbox" ? "checkbox" : "text",
509
+ inputmode: spec.kind === "number" ? "decimal" : void 0,
510
+ value: editor.draft,
511
+ checked: spec.kind === "checkbox" ? editor.value === true : void 0,
512
+ onInput: spec.kind === "checkbox" ? void 0 : (event) => editor.input(event.target.value),
513
+ onChange: spec.kind === "checkbox" ? (event) => editor.commit(event.target.checked) : void 0,
514
+ onBlur: () => {
515
+ if (props.blurCommit && !editor.pending) finish();
516
+ },
517
+ onPaste: spec.kind === "checkbox" ? void 0 : paste
518
+ });
519
+ };
520
+ }
521
+ });
522
+ /** The cell owns the id so it remains stable while its editor mounts and unmounts. */
523
+ function issueAttrs(id, issues, pending) {
524
+ return {
525
+ "aria-invalid": issues.length ? "true" : void 0,
526
+ "aria-describedby": issues.length ? id : void 0,
527
+ "aria-busy": pending ? "true" : void 0
528
+ };
529
+ }
530
+ function editorIssues(id, issues, pending = false) {
531
+ return issues.length || pending ? h("span", {
532
+ id,
533
+ "data-edit-issues": "",
534
+ role: "status"
535
+ }, pending ? "Checking changes…" : issues.map((problem) => problem.message).join(" ")) : null;
536
+ }
537
+ function useEditorId() {
538
+ return `vt-edit-${useId()}`;
539
+ }
540
+
327
541
  //#endregion
328
542
  //#region src/virtual.ts
329
543
  /** Shared opt-in renderer props. Options are applied when the virtual renderer is created. */
@@ -646,7 +860,14 @@ function treeKeydown(event, context, virtual) {
646
860
  const index = table.rows.findIndex((row) => getRowItemKey(row.key) === key);
647
861
  const row = table.rows[index];
648
862
  if (!row) return;
649
- const command = gridCommand(event, {
863
+ const command = gridCommand({
864
+ key: event.key,
865
+ shiftKey: event.shiftKey,
866
+ ctrlKey: event.ctrlKey,
867
+ metaKey: event.metaKey,
868
+ altKey: event.altKey,
869
+ direction: directionOf(target)
870
+ }, {
650
871
  canExpand: row.canExpand,
651
872
  expanded: row.isExpanded
652
873
  });
@@ -660,10 +881,11 @@ function treeKeydown(event, context, virtual) {
660
881
  else if (event.key === "Home" || event.key === "End") next = table.rows[event.key === "Home" ? 0 : table.rows.length - 1] ?? row;
661
882
  else return;
662
883
  event.preventDefault();
884
+ const root = target.closest("table");
663
885
  context.focus.value = next.key;
664
886
  virtual?.rows.scrollToKey(next.key);
665
887
  const id = itemId(context, next);
666
- nextTick(() => target.closest("table")?.ownerDocument.getElementById(id ?? "")?.focus());
888
+ nextTick(() => root?.ownerDocument.getElementById(id ?? "")?.focus());
667
889
  }
668
890
  const expandToggleProps = {
669
891
  row: rowProp,
@@ -673,11 +895,11 @@ const expandToggleProps = {
673
895
  },
674
896
  expandLabel: {
675
897
  type: String,
676
- default: "Expand row"
898
+ default: void 0
677
899
  },
678
900
  collapseLabel: {
679
901
  type: String,
680
- default: "Collapse row"
902
+ default: void 0
681
903
  }
682
904
  };
683
905
  const treeCellProps = {
@@ -692,13 +914,15 @@ const DataTableExpandToggle = defineComponent({
692
914
  const table = injectDataTable("<DataTableExpandToggle>");
693
915
  const context = injectHierarchy();
694
916
  const grid = injectDataGrid();
917
+ const locale = useTableLocale();
918
+ const label = () => props.row.isExpanded ? props.collapseLabel ?? locale().messages.collapseRow : props.expandLabel ?? locale().messages.expandRow;
695
919
  return () => props.row.canExpand ? h("button", {
696
920
  type: "button",
697
921
  "data-expand-toggle": "",
698
922
  tabindex: table.tree ? -1 : void 0,
699
923
  "aria-expanded": String(props.row.isExpanded),
700
924
  "aria-controls": props.controls ?? (!table.tree ? itemId(context, props.row, true) : props.row.isExpanded ? table.rows.filter((row) => row.parentKey === props.row.key).map((row) => itemId(context, row)).filter(Boolean).join(" ") || void 0 : void 0),
701
- "aria-label": props.row.isExpanded ? props.collapseLabel : props.expandLabel,
925
+ "aria-label": label(),
702
926
  onMousedown: (event) => event.stopPropagation(),
703
927
  onDblclick: (event) => event.stopPropagation(),
704
928
  onClick: (event) => {
@@ -713,7 +937,7 @@ const DataTableExpandToggle = defineComponent({
713
937
  }, slots.default?.({
714
938
  row: props.row,
715
939
  expanded: props.row.isExpanded
716
- }) ?? (props.row.isExpanded ? props.collapseLabel : props.expandLabel)) : null;
940
+ }) ?? label()) : null;
717
941
  }
718
942
  });
719
943
  const DataTableTreeCell = defineComponent({
@@ -724,6 +948,7 @@ const DataTableTreeCell = defineComponent({
724
948
  const table = injectDataTable("<DataTableTreeCell>");
725
949
  const context = injectHierarchy();
726
950
  const grid = injectDataGrid();
951
+ const locale = useTableLocale();
727
952
  return () => h(props.as, {
728
953
  class: "vt-tree-cell",
729
954
  "data-tree-cell": "",
@@ -731,8 +956,8 @@ const DataTableTreeCell = defineComponent({
731
956
  }, [
732
957
  h("span", { class: "vt-tree-toggle" }, slots.toggle?.({ row: props.row }) ?? h(DataTableExpandToggle, { row: props.row })),
733
958
  h("span", { class: "vt-tree-label" }, [slots.default?.()]),
734
- props.row.childStatus === "loading" ? h("span", { class: "vt-tree-loading" }, "Loading children…") : null,
735
- props.row.childStatus === "error" ? h("span", { class: "vt-tree-error" }, ["Could not load children. ", h("button", {
959
+ props.row.childStatus === "loading" ? h("span", { class: "vt-tree-loading" }, locale().messages.loadingChildren) : null,
960
+ props.row.childStatus === "error" ? h("span", { class: "vt-tree-error" }, [`${locale().messages.childrenError} `, h("button", {
736
961
  type: "button",
737
962
  class: "vt-tree-retry",
738
963
  onMousedown: (event) => event.stopPropagation(),
@@ -749,7 +974,7 @@ const DataTableTreeCell = defineComponent({
749
974
  else element?.ownerDocument.getElementById(itemId(context, props.row) ?? "")?.focus();
750
975
  });
751
976
  }
752
- }, "Retry children")]) : null
977
+ }, locale().messages.retryChildren)]) : null
753
978
  ]);
754
979
  }
755
980
  });
@@ -840,16 +1065,19 @@ const DataTableRoot = defineComponent({
840
1065
  },
841
1066
  as: asProp("table"),
842
1067
  ...virtualProps,
843
- ...hierarchyProps
1068
+ ...hierarchyProps,
1069
+ ...columnLayoutProps
844
1070
  },
845
1071
  setup(props, { slots }) {
846
1072
  provideDataTable(props.table);
1073
+ provideColumnOffset(() => props.leadingColumns);
847
1074
  const hierarchy = provideHierarchy(props.table, props);
848
1075
  const element = shallowRef();
1076
+ const editingGrid = injectEditingGrid();
849
1077
  const render = (virtual) => h(props.as, {
850
1078
  role: props.table.tree ? "treegrid" : void 0,
851
1079
  "aria-rowcount": (props.table.tree ? props.table.processedRows.length : props.table.rowCount) + 1,
852
- "aria-colcount": props.table.columns.length,
1080
+ "aria-colcount": props.table.columns.length + props.leadingColumns,
853
1081
  "aria-busy": props.table.loadState === "loading" || props.table.loadState === "streaming" ? "true" : void 0,
854
1082
  "data-empty": flag(props.table.rowCount === 0),
855
1083
  onKeydown: (event) => treeKeydown(event, hierarchy, virtual),
@@ -860,6 +1088,7 @@ const DataTableRoot = defineComponent({
860
1088
  }, slots.default?.({ table: props.table }) ?? [h(DataTableHeader), h(DataTableBody)]);
861
1089
  return () => props.virtual ? h(VirtualContent, {
862
1090
  table: props.table,
1091
+ ...editingGrid ? { grid: editingGrid } : {},
863
1092
  options: virtualOptions(props),
864
1093
  tableElement: () => element.value,
865
1094
  render
@@ -929,7 +1158,7 @@ const DataTableHeaderCell = defineComponent({
929
1158
  },
930
1159
  setup(props, { slots }) {
931
1160
  const table = injectDataTable("<DataTableHeaderCell>");
932
- const virtual = injectVirtual();
1161
+ const columnOffset = injectColumnOffset();
933
1162
  return () => {
934
1163
  const { column } = props;
935
1164
  const sort = table.getSort(column.id);
@@ -947,7 +1176,7 @@ const DataTableHeaderCell = defineComponent({
947
1176
  "data-sortable": flag(column.sortable),
948
1177
  "data-sort": sort?.direction,
949
1178
  style: columnStyle(column),
950
- ...virtual?.columns ? { "aria-colindex": table.columns.indexOf(column) + 1 } : {}
1179
+ "aria-colindex": table.columns.indexOf(column) + columnOffset() + 1
951
1180
  }, content);
952
1181
  };
953
1182
  }
@@ -971,13 +1200,14 @@ const DataTableSortButton = defineComponent({
971
1200
  },
972
1201
  setup(props, { slots }) {
973
1202
  const table = injectDataTable("<DataTableSortButton>");
1203
+ const locale = useTableLocale();
974
1204
  return () => {
975
1205
  const sort = table.getSort(props.column.id);
976
1206
  const next = !sort ? "ascending" : sort.direction === "asc" ? "descending" : "unsorted";
977
1207
  return h(props.as, {
978
1208
  type: props.as === "button" ? "button" : void 0,
979
1209
  "data-sort": sort?.direction,
980
- "aria-label": `${props.column.header}, sort ${next}`,
1210
+ "aria-label": locale().messages.sortBy(props.column.header, next),
981
1211
  onClick: (event) => table.toggleSort(props.column.id, { multi: event.shiftKey })
982
1212
  }, [slots.default?.({ sort }) ?? props.column.header, slots.indicator?.({ sort })]);
983
1213
  };
@@ -1000,6 +1230,7 @@ const DataTableBody = defineComponent({
1000
1230
  },
1001
1231
  setup(props, { slots }) {
1002
1232
  const table = injectDataTable("<DataTableBody>");
1233
+ const locale = useTableLocale();
1003
1234
  const virtual = injectVirtual();
1004
1235
  const details = createDetails(table, () => slots.detail, () => props.colspan ?? table.columns.length);
1005
1236
  return () => {
@@ -1013,7 +1244,7 @@ const DataTableBody = defineComponent({
1013
1244
  }) : details.render(item.row, item.rowIndex));
1014
1245
  const visibleDetails = new Set(table.renderItems.filter((item) => item.kind === "detail").map((item) => item.row.key));
1015
1246
  const retained = slots.default ? [] : details.retained(visibleDetails);
1016
- const sentinel = loadSentinel(table, props.colspan ?? table.columns.length);
1247
+ const sentinel = loadSentinel(table, props.colspan ?? table.columns.length, locale());
1017
1248
  return h(props.as, [
1018
1249
  ...Array.isArray(content) ? content : content ? [content] : [],
1019
1250
  ...retained,
@@ -1088,6 +1319,7 @@ const DataTableRow = defineComponent({
1088
1319
  "aria-selected": table.selectionMode === "none" ? void 0 : String(selected),
1089
1320
  "data-selected": flag(selected),
1090
1321
  "data-key": String(row.key),
1322
+ "data-dirty": flag(row.isDirty),
1091
1323
  ...hierarchyRowAttrs(table, row, hierarchy),
1092
1324
  tabindex: table.tree ? (hierarchy?.focusKey ?? table.rows[0]?.key) === row.key ? 0 : -1 : void 0,
1093
1325
  onFocus: () => {
@@ -1124,6 +1356,7 @@ const DataTableCell = defineComponent({
1124
1356
  },
1125
1357
  setup(props, { slots }) {
1126
1358
  const table = injectDataTable("<DataTableCell>");
1359
+ const columnOffset = injectColumnOffset();
1127
1360
  const hierarchy = injectHierarchy();
1128
1361
  return () => {
1129
1362
  const { row, column } = props;
@@ -1137,7 +1370,8 @@ const DataTableCell = defineComponent({
1137
1370
  }) ?? display;
1138
1371
  return h(props.as, {
1139
1372
  "data-column": column.id,
1140
- "data-align": column.align
1373
+ "data-align": column.align,
1374
+ "aria-colindex": table.columns.indexOf(column) + columnOffset() + 1
1141
1375
  }, props.tree && table.tree && column.id === resolveTreeColumn(table, hierarchy?.treeColumn) ? h(DataTableTreeCell, { row }, { default: content }) : content());
1142
1376
  };
1143
1377
  }
@@ -1153,6 +1387,301 @@ const DataTableEmpty = defineComponent({
1153
1387
  }
1154
1388
  });
1155
1389
 
1390
+ //#endregion
1391
+ //#region src/editing.ts
1392
+ const editingKey = Symbol("vueye-table-editing");
1393
+ function injectEditingGrid() {
1394
+ return inject(editingKey, void 0)?.grid;
1395
+ }
1396
+ /** Renderless provider for editable data-table cells and row forms. */
1397
+ const DataTableEditRoot = defineComponent({
1398
+ name: "DataTableEditRoot",
1399
+ slots: Object,
1400
+ props: {
1401
+ table: {
1402
+ type: Object,
1403
+ required: true
1404
+ },
1405
+ mode: {
1406
+ type: String,
1407
+ default: "cell"
1408
+ }
1409
+ },
1410
+ emits: {
1411
+ save: (_result) => true,
1412
+ cancel: (_key) => true
1413
+ },
1414
+ setup(props, { slots, emit, expose }) {
1415
+ const grid = useDataGrid(props.table);
1416
+ const draft = shallowRef();
1417
+ const context = {
1418
+ grid,
1419
+ get mode() {
1420
+ return props.mode;
1421
+ },
1422
+ get draft() {
1423
+ return draft.value;
1424
+ },
1425
+ start(row) {
1426
+ if (draft.value?.pending) return;
1427
+ if (draft.value) context.cancel(draft.value.rowKey);
1428
+ grid.cancelEdit();
1429
+ const column = props.table.columns.findIndex((candidate) => candidate.isEditable(row.original));
1430
+ grid.focusCell({
1431
+ row: props.table.rows.findIndex((candidate) => candidate.key === row.key),
1432
+ column
1433
+ });
1434
+ grid.startEdit();
1435
+ draft.value = props.table.editRow(row.key);
1436
+ },
1437
+ async save() {
1438
+ const active = draft.value;
1439
+ if (!active || active.pending) return void 0;
1440
+ const result = await active.save();
1441
+ if (draft.value === active && active.status !== "editing" && !active.pending) {
1442
+ draft.value = void 0;
1443
+ grid.cancelEdit();
1444
+ }
1445
+ if (result.status !== "rejected") emit("save", result);
1446
+ return result;
1447
+ },
1448
+ cancel(key) {
1449
+ if (draft.value?.rowKey === key && !draft.value.pending) {
1450
+ draft.value.cancel();
1451
+ draft.value = void 0;
1452
+ grid.cancelEdit();
1453
+ emit("cancel", key);
1454
+ }
1455
+ }
1456
+ };
1457
+ provide(editingKey, context);
1458
+ expose({ editing: context });
1459
+ watch(() => props.table.rows, (rows) => {
1460
+ if (draft.value && !rows.some((row) => row.key === draft.value?.rowKey)) context.cancel(draft.value.rowKey);
1461
+ });
1462
+ onScopeDispose(() => draft.value?.cancel());
1463
+ return () => slots.default?.({ editing: context });
1464
+ }
1465
+ });
1466
+ /** A cell with on-demand editing; row mode shares one draft and one save operation. */
1467
+ const DataTableEditCell = defineComponent({
1468
+ name: "DataTableEditCell",
1469
+ props: {
1470
+ row: rowProp,
1471
+ column: columnProp,
1472
+ rowIndex: {
1473
+ type: Number,
1474
+ default: void 0
1475
+ },
1476
+ tree: {
1477
+ type: Boolean,
1478
+ default: true
1479
+ }
1480
+ },
1481
+ slots: Object,
1482
+ emits: {
1483
+ save: (_result) => true,
1484
+ cancel: (_key) => true
1485
+ },
1486
+ setup(props, { slots, emit }) {
1487
+ const table = injectDataTable("<DataTableEditCell>");
1488
+ const context = inject(editingKey);
1489
+ if (!context) throw new Error("<DataTableEditCell> requires <DataTableEditRoot>.");
1490
+ const { grid } = context;
1491
+ const id = useEditorId();
1492
+ const element = shallowRef();
1493
+ const position = () => ({
1494
+ row: props.rowIndex ?? table.rows.indexOf(props.row),
1495
+ column: table.columns.indexOf(props.column)
1496
+ });
1497
+ const active = computed(() => context.mode === "row" ? context.draft?.rowKey === props.row.key : grid.isEditing(position()));
1498
+ const pending = computed(() => context.mode === "row" ? !!context.draft?.pending : active.value && grid.lastResult?.status === "pending");
1499
+ const localText = shallowRef("");
1500
+ watch(active, (value) => {
1501
+ if (value) localText.value = editorText(props.row.getValue(props.column.id), grid.editorFor(position())?.kind ?? "text");
1502
+ });
1503
+ const focus = (direction) => {
1504
+ nextTick(() => {
1505
+ if (!direction) {
1506
+ element.value?.focus();
1507
+ return;
1508
+ }
1509
+ const all = [...element.value?.closest("table")?.querySelectorAll("[data-edit-cell][data-editable]") ?? []];
1510
+ all[all.indexOf(element.value) + (direction === "left" ? -1 : 1)]?.focus();
1511
+ });
1512
+ };
1513
+ const submit = (value, direction) => {
1514
+ if (context.mode === "row") {
1515
+ if (value !== void 0) {
1516
+ if (typeof value === "string") {
1517
+ localText.value = value;
1518
+ context.draft?.setValue(props.column.id, value);
1519
+ context.draft?.setInput(props.column.id, value);
1520
+ } else context.draft?.setValue(props.column.id, value);
1521
+ }
1522
+ return;
1523
+ }
1524
+ const result = grid.submitEdit(value === void 0 ? void 0 : typeof value === "string" ? { input: value } : { value });
1525
+ const finish = (final) => {
1526
+ if (final.status !== "rejected") {
1527
+ emit("save", final);
1528
+ focus(direction);
1529
+ }
1530
+ };
1531
+ if (result?.completion) result.completion.then(finish);
1532
+ else if (result) finish(result);
1533
+ return result;
1534
+ };
1535
+ return () => {
1536
+ const { row, column } = props;
1537
+ const editable = column.isEditable(row.original);
1538
+ const editing = editable && active.value;
1539
+ const draft = context.draft?.rowKey === row.key ? context.draft : void 0;
1540
+ const issues = draft?.issues.filter((problem) => problem.column === column.id) ?? (row.cellIssues?.get(column.id) ? [row.cellIssues.get(column.id)] : active.value ? grid.lastResult?.issues.filter((problem) => problem.rowKey === row.key && (problem.column === column.id || !problem.column)) ?? [] : []);
1541
+ const spec = editing ? grid.editorFor(position()) ?? { kind: "text" } : void 0;
1542
+ const input = (text) => {
1543
+ localText.value = text;
1544
+ if (context.mode === "row") {
1545
+ draft?.setValue(column.id, text);
1546
+ draft?.setInput(column.id, text);
1547
+ } else grid.updateDraft(text);
1548
+ };
1549
+ const cancel = () => {
1550
+ if (pending.value) return;
1551
+ if (context.mode === "row") context.cancel(row.key);
1552
+ else {
1553
+ grid.cancelEdit();
1554
+ emit("cancel", row.key);
1555
+ }
1556
+ focus();
1557
+ };
1558
+ const editor = {
1559
+ row,
1560
+ column,
1561
+ value: context.mode === "row" ? Reflect.get(draft?.values ?? {}, column.id) : row.getValue(column.id),
1562
+ draft: context.mode === "row" ? localText.value : grid.editor?.draft ?? "",
1563
+ spec: spec ?? { kind: "text" },
1564
+ finish: (direction) => {
1565
+ if (context.mode === "row") context.save();
1566
+ else submit(void 0, direction === "left" || direction === "right" ? direction : void 0);
1567
+ },
1568
+ input,
1569
+ update: input,
1570
+ commit: submit,
1571
+ cancel,
1572
+ issues,
1573
+ pending: pending.value,
1574
+ attrs: issueAttrs(id, issues, pending.value)
1575
+ };
1576
+ return h(DataTableCell, {
1577
+ ref: (target) => {
1578
+ element.value = target && typeof target === "object" && "$el" in target ? target.$el : void 0;
1579
+ },
1580
+ row,
1581
+ column,
1582
+ tree: props.tree,
1583
+ "data-edit-cell": "",
1584
+ "data-editable": editable ? "" : void 0,
1585
+ "data-editing": editing ? "" : void 0,
1586
+ "data-dirty": row.isDirty ? "" : void 0,
1587
+ ...issueAttrs(id, issues, pending.value),
1588
+ tabindex: editable ? 0 : void 0,
1589
+ onFocus: (event) => {
1590
+ if (event.target === element.value && !grid.editor) grid.focusCell(position());
1591
+ },
1592
+ onDblclick: () => {
1593
+ if (editable && context.mode === "cell") {
1594
+ grid.focusCell(position());
1595
+ grid.startEdit(editorText(row.getValue(column.id), grid.editorFor(position())?.kind ?? "text"));
1596
+ }
1597
+ },
1598
+ onKeydown: (event) => {
1599
+ if (event.target === element.value && event.key === "Enter" && editable) {
1600
+ event.preventDefault();
1601
+ event.stopPropagation();
1602
+ if (context.mode === "row") context.start(row);
1603
+ else {
1604
+ grid.focusCell(position());
1605
+ grid.startEdit(editorText(row.getValue(column.id), grid.editorFor(position())?.kind ?? "text"));
1606
+ }
1607
+ }
1608
+ }
1609
+ }, { default: ({ value, display }) => [editing ? slots.editor?.(editor) ?? h(DataCellEditor, {
1610
+ editor,
1611
+ spec: spec ?? { kind: "text" },
1612
+ autofocus: context.mode === "cell" || table.columns.find((candidate) => candidate.isEditable(row.original))?.id === column.id,
1613
+ blurCommit: context.mode === "cell",
1614
+ tabCommit: context.mode === "cell",
1615
+ finish: (direction) => {
1616
+ if (context.mode === "row") context.save();
1617
+ else submit(void 0, direction === "left" || direction === "right" ? direction : void 0);
1618
+ }
1619
+ }) : slots.default?.({
1620
+ row,
1621
+ value,
1622
+ display
1623
+ }) ?? display, editorIssues(id, issues, pending.value)] });
1624
+ };
1625
+ }
1626
+ });
1627
+ /** One active row form. Controls share the provider's draft and core save/undo path. */
1628
+ const DataTableRowActions = defineComponent({
1629
+ name: "DataTableRowActions",
1630
+ props: {
1631
+ row: rowProp,
1632
+ editable: {
1633
+ type: Boolean,
1634
+ default: true
1635
+ },
1636
+ removable: {
1637
+ type: Boolean,
1638
+ default: false
1639
+ }
1640
+ },
1641
+ setup(props) {
1642
+ const table = injectDataTable("<DataTableRowActions>");
1643
+ const locale = useTableLocale();
1644
+ const context = inject(editingKey, void 0);
1645
+ const id = useEditorId();
1646
+ const element = shallowRef();
1647
+ watch(() => context?.draft?.rowKey === props.row.key, (active, previous) => {
1648
+ const target = element.value;
1649
+ if (previous && !active && !context?.draft && target?.closest("tr")?.contains(target.ownerDocument.activeElement)) nextTick(() => target.querySelector("button[data-edit-row]")?.focus());
1650
+ });
1651
+ return () => {
1652
+ const { messages } = locale();
1653
+ const rowName = String(props.row.key);
1654
+ const draft = context?.draft?.rowKey === props.row.key ? context.draft : void 0;
1655
+ const issues = [...new Map(draft?.issues.map((problem) => [problem.message, problem])).values()];
1656
+ const button = (label, action, disabled = false, role) => h("button", {
1657
+ type: "button",
1658
+ disabled,
1659
+ "aria-label": messages.actionOnRow(label, rowName),
1660
+ "data-edit-row": role === "edit" ? "" : void 0,
1661
+ ...role === "save" ? issueAttrs(id, issues, !!draft?.pending) : {},
1662
+ onClick: action
1663
+ }, label);
1664
+ return h("div", {
1665
+ ref: element,
1666
+ "data-row-actions": "",
1667
+ role: "group",
1668
+ "aria-label": messages.rowActionsFor(rowName),
1669
+ "aria-busy": draft?.pending ? "true" : void 0
1670
+ }, [
1671
+ ...context?.mode === "row" && props.editable ? draft ? [button(messages.saveRow, () => context.save(), draft.pending, "save"), button(messages.cancel, () => context.cancel(props.row.key), draft.pending)] : [button(messages.editRow, () => context.start(props.row), false, "edit")] : [],
1672
+ h("details", { onToggle: (event) => {
1673
+ const menu = event.currentTarget;
1674
+ if (menu.open) nextTick(() => menu.scrollIntoView?.({
1675
+ block: "nearest",
1676
+ inline: "nearest"
1677
+ }));
1678
+ } }, [h("summary", { "aria-label": messages.actionsForRow(rowName) }, messages.actions), h("div", { "data-row-menu": "" }, [button(messages.revertRow, () => table.revert([props.row.key]), !props.row.isDirty || !!draft), props.removable ? button(messages.removeRow, () => table.removeRows([props.row.key]), !!draft) : null])]),
1679
+ editorIssues(id, issues, !!draft?.pending)
1680
+ ]);
1681
+ };
1682
+ }
1683
+ });
1684
+
1156
1685
  //#endregion
1157
1686
  //#region src/grid.ts
1158
1687
  const gridContextKey = Symbol("vueye-table-grid-context");
@@ -1188,25 +1717,41 @@ const DataGridRoot = defineComponent({
1188
1717
  },
1189
1718
  ...virtualProps,
1190
1719
  ...hierarchyProps,
1720
+ ...columnLayoutProps,
1191
1721
  /** Width reserved before data columns, for a row-number/header column. */
1192
1722
  gutter: {
1193
1723
  type: Number,
1194
1724
  default: 0
1195
1725
  }
1196
1726
  },
1197
- setup(props, { slots, expose }) {
1727
+ emits: {
1728
+ save: (_result) => true,
1729
+ cancel: (_key) => true
1730
+ },
1731
+ setup(props, { slots, expose, emit }) {
1198
1732
  provideDataTable(props.table);
1733
+ provideColumnOffset(() => props.leadingColumns);
1199
1734
  const grid = useDataGrid(props.table, { treeColumn: () => props.treeColumn });
1200
1735
  provideDataGrid(grid);
1201
1736
  provideHierarchy(props.table, props);
1202
1737
  const element = ref();
1203
1738
  const id = `vt-grid-${useId()}`;
1204
1739
  const focus = () => {
1205
- nextTick(() => element.value?.focus());
1740
+ nextTick(() => {
1741
+ if (!grid.editor) element.value?.focus({ preventScroll: true });
1742
+ });
1206
1743
  };
1207
1744
  provide(gridContextKey, {
1208
1745
  id,
1209
- focus
1746
+ focus,
1747
+ cancel: (key) => emit("cancel", key)
1748
+ });
1749
+ watch(() => grid.lastResult, (result) => {
1750
+ if (result && [
1751
+ "applied",
1752
+ "partial",
1753
+ "unchanged"
1754
+ ].includes(result.status)) emit("save", result);
1210
1755
  });
1211
1756
  expose({
1212
1757
  grid,
@@ -1215,6 +1760,10 @@ const DataGridRoot = defineComponent({
1215
1760
  let virtual;
1216
1761
  const onKeydown = (event) => {
1217
1762
  if (event.target !== element.value) return;
1763
+ if (event.key === " " && !grid.editor && !event.ctrlKey && !event.metaKey && !event.altKey && grid.toggleCheckbox()) {
1764
+ event.preventDefault();
1765
+ return;
1766
+ }
1218
1767
  if (virtual && !grid.editor && (event.ctrlKey || event.metaKey) && (event.key === "Home" || event.key === "End")) {
1219
1768
  grid.focusCell(event.key === "Home" ? {
1220
1769
  row: 0,
@@ -1240,7 +1789,14 @@ const DataGridRoot = defineComponent({
1240
1789
  event.preventDefault();
1241
1790
  return;
1242
1791
  }
1243
- if (grid.handleKey(event)) event.preventDefault();
1792
+ if (grid.handleKey({
1793
+ key: event.key,
1794
+ shiftKey: event.shiftKey,
1795
+ ctrlKey: event.ctrlKey,
1796
+ metaKey: event.metaKey,
1797
+ altKey: event.altKey,
1798
+ direction: directionOf(event.currentTarget)
1799
+ })) event.preventDefault();
1244
1800
  };
1245
1801
  const onCopy = (event, cut = false) => {
1246
1802
  if (grid.editor || !grid.range || !event.clipboardData) return;
@@ -1249,8 +1805,9 @@ const DataGridRoot = defineComponent({
1249
1805
  if (cut) grid.clear();
1250
1806
  };
1251
1807
  const onPaste = (event) => {
1808
+ if (grid.editor || event.defaultPrevented) return;
1252
1809
  const text = event.clipboardData?.getData("text/plain");
1253
- if (grid.editor || text === void 0 || text === "") return;
1810
+ if (text === void 0 || text === "") return;
1254
1811
  event.preventDefault();
1255
1812
  grid.paste(text);
1256
1813
  };
@@ -1265,7 +1822,7 @@ const DataGridRoot = defineComponent({
1265
1822
  "aria-label": props.label,
1266
1823
  "aria-multiselectable": "true",
1267
1824
  "aria-rowcount": (props.table.tree ? props.table.processedRows.length : binding ? props.table.rowCount : props.table.rows.length) + 1,
1268
- "aria-colcount": props.table.columns.length,
1825
+ "aria-colcount": props.table.columns.length + props.leadingColumns,
1269
1826
  "aria-busy": props.table.loadState === "loading" || props.table.loadState === "streaming" ? "true" : void 0,
1270
1827
  "aria-activedescendant": focusPosition && !grid.editor ? cellId(id, focusPosition.row, focusPosition.column) : void 0,
1271
1828
  onKeydown,
@@ -1341,12 +1898,17 @@ const DataGridBody = defineComponent({
1341
1898
  rowIndex,
1342
1899
  columnIndex,
1343
1900
  tree: props.treeCell
1344
- }, slots.cell ? { default: (cellProps) => slots.cell?.(cellProps) } : void 0), "td");
1901
+ }, {
1902
+ default: slots.cell,
1903
+ editor: slots.editor,
1904
+ $stable: true
1905
+ }), "td");
1345
1906
  return h("tr", {
1346
1907
  key: item.key,
1347
1908
  role: "row",
1348
1909
  "aria-rowindex": table.pageStart + rowIndex + 1,
1349
1910
  "data-key": String(row.key),
1911
+ "data-dirty": flag(row.isDirty),
1350
1912
  ...hierarchyRowAttrs(table, row, hierarchy),
1351
1913
  "data-virtual-row": "",
1352
1914
  ref: (target) => binding.rows.measureElement(target, item.key)
@@ -1365,6 +1927,7 @@ const DataGridBody = defineComponent({
1365
1927
  role: "row",
1366
1928
  "aria-rowindex": rowIndex + 2,
1367
1929
  "data-key": String(row.key),
1930
+ "data-dirty": flag(row.isDirty),
1368
1931
  ...hierarchyRowAttrs(table, row, hierarchy)
1369
1932
  }, [slots.rowHeader?.({
1370
1933
  row,
@@ -1376,7 +1939,11 @@ const DataGridBody = defineComponent({
1376
1939
  rowIndex,
1377
1940
  columnIndex,
1378
1941
  tree: props.treeCell
1379
- }, slots.cell ? { default: (cellProps) => slots.cell?.(cellProps) } : void 0))]);
1942
+ }, {
1943
+ default: slots.cell,
1944
+ editor: slots.editor,
1945
+ $stable: true
1946
+ }))]);
1380
1947
  })());
1381
1948
  const sentinel = loadSentinel(table, props.colspan ?? table.columns.length + (slots.rowHeader ? 1 : 0));
1382
1949
  return h(props.as, [
@@ -1394,52 +1961,6 @@ const DataGridBody = defineComponent({
1394
1961
  }) : render(virtual);
1395
1962
  }
1396
1963
  });
1397
- /** The text box shown in a cell while it is edited. */
1398
- const DataGridEditor = defineComponent({
1399
- name: "DataGridEditor",
1400
- props: { label: {
1401
- type: String,
1402
- required: true
1403
- } },
1404
- setup(props) {
1405
- const { grid, context } = useGrid("<DataGridEditor>");
1406
- const input = ref();
1407
- onMounted(() => {
1408
- const element = input.value;
1409
- if (element) {
1410
- element.focus();
1411
- element.setSelectionRange(element.value.length, element.value.length);
1412
- }
1413
- });
1414
- const finish = (then) => {
1415
- grid.commitEdit(then);
1416
- context.focus();
1417
- };
1418
- return () => h("input", {
1419
- ref: input,
1420
- value: grid.editor?.draft ?? "",
1421
- "aria-label": props.label,
1422
- "data-editor": "",
1423
- onInput: (event) => grid.updateDraft(event.target.value),
1424
- onBlur: () => {
1425
- if (grid.editor) grid.commitEdit();
1426
- },
1427
- onKeydown: (event) => {
1428
- if (event.key === "Enter") {
1429
- event.preventDefault();
1430
- finish(event.shiftKey ? "up" : "down");
1431
- } else if (event.key === "Tab") {
1432
- event.preventDefault();
1433
- finish(event.shiftKey ? "left" : "right");
1434
- } else if (event.key === "Escape") {
1435
- event.preventDefault();
1436
- grid.cancelEdit();
1437
- context.focus();
1438
- }
1439
- }
1440
- });
1441
- }
1442
- });
1443
1964
  /**
1444
1965
  * A grid cell. A click selects it, Shift-click or dragging extends the range, and a double click
1445
1966
  * edits it. The `editor` slot replaces the default text box.
@@ -1468,10 +1989,12 @@ const DataGridCell = defineComponent({
1468
1989
  },
1469
1990
  setup(props, { slots }) {
1470
1991
  const table = injectDataTable("<DataGridCell>");
1992
+ const columnOffset = injectColumnOffset();
1471
1993
  const { grid, context } = useGrid("<DataGridCell>");
1472
1994
  const virtual = injectVirtual();
1473
1995
  const hierarchy = injectHierarchy();
1474
1996
  const element = ref();
1997
+ const issueId = useEditorId();
1475
1998
  const position = () => ({
1476
1999
  row: props.rowIndex ?? table.rows.indexOf(props.row),
1477
2000
  column: props.columnIndex ?? table.columns.indexOf(props.column)
@@ -1489,14 +2012,48 @@ const DataGridCell = defineComponent({
1489
2012
  const editable = column.isEditable(row.original);
1490
2013
  const value = row.getValue(column.id);
1491
2014
  const display = row.getDisplay(column.id);
2015
+ const issues = row.cellIssues?.get(column.id) ? [row.cellIssues.get(column.id)] : grid.lastResult?.issues.filter((problem) => problem.rowKey === row.key && (problem.column === column.id || !problem.column)) ?? [];
2016
+ const pending = table.pendingCells.some((cell) => cell.rowKey === row.key && cell.column === column.id);
2017
+ const finish = (edit, direction) => {
2018
+ const result = grid.submitEdit(edit, direction);
2019
+ const focus = () => {
2020
+ if (!grid.editor) context.focus();
2021
+ };
2022
+ if (result?.completion) result.completion.then(focus);
2023
+ else focus();
2024
+ return result;
2025
+ };
1492
2026
  let content;
1493
- if (editing) content = slots.editor?.({
1494
- draft: grid.editor?.draft ?? "",
1495
- update: (draft) => grid.updateDraft(draft),
1496
- commit: (then) => grid.commitEdit(then),
1497
- cancel: () => grid.cancelEdit()
1498
- }) ?? h(DataGridEditor, { label: `Edit ${column.header}` });
1499
- else {
2027
+ if (editing) {
2028
+ const editor = {
2029
+ row,
2030
+ column,
2031
+ value,
2032
+ draft: grid.editor?.draft ?? "",
2033
+ spec: grid.editorFor(at) ?? { kind: "text" },
2034
+ finish: (direction) => {
2035
+ finish(void 0, direction);
2036
+ },
2037
+ input: (text) => grid.updateDraft(text),
2038
+ update: (text) => grid.updateDraft(text),
2039
+ commit: (next) => finish(next === void 0 ? void 0 : typeof next === "string" ? { input: next } : { value: next }),
2040
+ cancel: () => {
2041
+ if (!pending) {
2042
+ grid.cancelEdit();
2043
+ context.cancel(row.key);
2044
+ context.focus();
2045
+ }
2046
+ },
2047
+ issues,
2048
+ pending,
2049
+ attrs: issueAttrs(issueId, issues, pending)
2050
+ };
2051
+ content = slots.editor?.(editor) ?? h(DataCellEditor, {
2052
+ editor,
2053
+ spec: grid.editorFor(at) ?? { kind: "text" },
2054
+ finish: (direction) => finish(void 0, direction)
2055
+ });
2056
+ } else {
1500
2057
  const children = () => slots.default?.({
1501
2058
  row,
1502
2059
  column,
@@ -1512,13 +2069,15 @@ const DataGridCell = defineComponent({
1512
2069
  role: "gridcell",
1513
2070
  "aria-selected": String(grid.isSelected(at)),
1514
2071
  "aria-readonly": editable ? void 0 : "true",
1515
- "aria-colindex": at.column + 1,
2072
+ "aria-colindex": at.column + columnOffset() + 1,
1516
2073
  "data-column": column.id,
1517
2074
  "data-align": column.align,
1518
2075
  "data-focused": flag(grid.isFocused(at)),
1519
2076
  "data-selected": flag(grid.isSelected(at)),
1520
2077
  "data-editing": flag(editing),
1521
2078
  "data-readonly": flag(!editable),
2079
+ "data-dirty": flag(row.isDirty),
2080
+ ...issueAttrs(issueId, issues, pending),
1522
2081
  style: columnStyle(column),
1523
2082
  onMousedown: (event) => {
1524
2083
  if (editing || event.button !== 0) return;
@@ -1530,13 +2089,13 @@ const DataGridCell = defineComponent({
1530
2089
  },
1531
2090
  onDblclick: () => {
1532
2091
  grid.focusCell(at);
1533
- grid.startEdit();
2092
+ grid.startEdit(editorText(value, grid.editorFor(at)?.kind ?? "text"));
1534
2093
  }
1535
- }, content);
2094
+ }, [content, editorIssues(issueId, issues, pending)]);
1536
2095
  };
1537
2096
  }
1538
2097
  });
1539
2098
 
1540
2099
  //#endregion
1541
- export { DataGridBody, DataGridCell, DataGridRoot, DataTableBody, DataTableCaption, DataTableCell, DataTableColumnVisibility, DataTableDetailRow, DataTableEmpty, DataTableExpandToggle, DataTableHeader, DataTableHeaderCell, DataTableHeaderRow, DataTableLoadMore, DataTablePageSize, DataTablePagination, DataTableRoot, DataTableRow, DataTableSearch, DataTableSelectAll, DataTableSelectRow, DataTableSortButton, DataTableStatus, DataTableTreeCell, DataTableViewport, DataTableVirtualColumns, cellId, columnStyle, expandToggleProps, hierarchyProps, injectVirtual as injectVirtualRenderer, resolveTreeColumn, treeCellProps, virtualProps };
2100
+ export { DataCellEditor, DataGridBody, DataGridCell, DataGridRoot, DataTableBody, DataTableCaption, DataTableCell, DataTableColumnVisibility, DataTableDetailRow, DataTableEditCell, DataTableEditRoot, DataTableEmpty, DataTableExpandToggle, DataTableHeader, DataTableHeaderCell, DataTableHeaderRow, DataTableLoadMore, DataTablePageSize, DataTablePagination, DataTableRoot, DataTableRow, DataTableRowActions, DataTableSearch, DataTableSelectAll, DataTableSelectRow, DataTableSortButton, DataTableStatus, DataTableTreeCell, DataTableViewport, DataTableVirtualColumns, cellEditorProps, cellId, columnStyle, expandToggleProps, hierarchyProps, injectVirtual as injectVirtualRenderer, resolveTreeColumn, treeCellProps, virtualProps };
1542
2101
  //# sourceMappingURL=index.js.map