@vueye-table/headless 3.0.0-alpha.9 → 3.0.0-beta.1

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
1
  import { injectDataGrid, injectDataTable, provideDataGrid, provideDataTable, useDataGrid, 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,
@@ -324,6 +336,173 @@ const DataTableLoadMore = defineComponent({
324
336
  }
325
337
  });
326
338
 
339
+ //#endregion
340
+ //#region src/editor.ts
341
+ function editorText(value, kind) {
342
+ if (value === null || value === void 0) return "";
343
+ if (kind === "date" && value instanceof Date) return Number.isFinite(value.getTime()) ? value.toISOString().slice(0, 10) : "";
344
+ return formatValue(value);
345
+ }
346
+ const cellEditorProps = {
347
+ editor: {
348
+ type: Object,
349
+ required: true
350
+ },
351
+ spec: {
352
+ type: Object,
353
+ default: () => ({ kind: "text" })
354
+ },
355
+ /** Keyboard movement is owned by the surrounding cell. */
356
+ finish: {
357
+ type: Function,
358
+ default: void 0
359
+ },
360
+ autofocus: {
361
+ type: Boolean,
362
+ default: true
363
+ },
364
+ blurCommit: {
365
+ type: Boolean,
366
+ default: true
367
+ },
368
+ tabCommit: {
369
+ type: Boolean,
370
+ default: true
371
+ }
372
+ };
373
+ /** Plain native fields. Select results are searched and capped at 50 mounted options. */
374
+ const DataCellEditor = defineComponent({
375
+ name: "DataCellEditor",
376
+ props: cellEditorProps,
377
+ setup(props) {
378
+ const field = ref();
379
+ const query = ref("");
380
+ const options = props.spec.options ?? [];
381
+ const matches = computed(() => {
382
+ const search = query.value.trim().toLocaleLowerCase();
383
+ return options.map((value, index) => ({
384
+ value,
385
+ index
386
+ })).filter(({ value }) => String(value ?? "").toLocaleLowerCase().includes(search));
387
+ });
388
+ onMounted(() => {
389
+ if (!props.autofocus) return;
390
+ field.value?.focus();
391
+ if (field.value && "setSelectionRange" in field.value && ["text", "number"].includes(props.spec.kind)) field.value.setSelectionRange(field.value.value.length, field.value.value.length);
392
+ });
393
+ const finish = (direction) => {
394
+ if (props.finish) props.finish(direction);
395
+ else props.editor.commit();
396
+ };
397
+ const keydown = (event) => {
398
+ if (event.isComposing) return;
399
+ if (event.key === "Escape") {
400
+ event.preventDefault();
401
+ event.stopPropagation();
402
+ if (!props.editor.pending) props.editor.cancel();
403
+ } else if (event.key === "Enter" || event.key === "Tab") {
404
+ if (event.key === "Tab" && !props.tabCommit) return;
405
+ event.preventDefault();
406
+ event.stopPropagation();
407
+ if (!props.editor.pending) finish(event.key === "Tab" ? event.shiftKey ? "left" : "right" : event.shiftKey ? "up" : "down");
408
+ }
409
+ };
410
+ const paste = (event) => {
411
+ const text = event.clipboardData?.getData("text/plain");
412
+ if (text === void 0 || props.editor.pending) return;
413
+ event.preventDefault();
414
+ const target = event.target;
415
+ const start = target.selectionStart ?? 0;
416
+ const end = target.selectionEnd ?? target.value.length;
417
+ props.editor.input(target.value.slice(0, start) + text + target.value.slice(end));
418
+ };
419
+ return () => {
420
+ const { editor, spec } = props;
421
+ const attrs = {
422
+ ...editor.attrs,
423
+ "aria-label": `Edit ${editor.column.header}`,
424
+ "data-editor": "",
425
+ disabled: editor.pending,
426
+ onKeydown: keydown
427
+ };
428
+ if (spec.kind === "select") {
429
+ const shown = matches.value.slice(0, 50);
430
+ return h("div", {
431
+ "data-select-editor": "",
432
+ onKeydown: (event) => {
433
+ if (event.key === "Escape") keydown(event);
434
+ }
435
+ }, [
436
+ h("input", {
437
+ ref: field,
438
+ type: "search",
439
+ value: query.value,
440
+ "aria-label": `Search ${editor.column.header} options`,
441
+ disabled: editor.pending,
442
+ onInput: (event) => {
443
+ query.value = event.target.value;
444
+ },
445
+ onKeydown: (event) => {
446
+ if (event.key === "Enter" && shown[0]) {
447
+ event.preventDefault();
448
+ editor.commit(shown[0].value);
449
+ } else keydown(event);
450
+ }
451
+ }),
452
+ h("select", {
453
+ ...attrs,
454
+ value: options.findIndex((value) => Object.is(value, editor.value)),
455
+ size: Math.max(2, Math.min(5, shown.length)),
456
+ onChange: (event) => {
457
+ const index = Number(event.target.value);
458
+ if (index >= 0) editor.commit(options[index]);
459
+ }
460
+ }, shown.map(({ value, index }) => h("option", {
461
+ key: index,
462
+ value: index
463
+ }, String(value ?? "(empty)")))),
464
+ h("span", {
465
+ role: "status",
466
+ "data-editor-hint": ""
467
+ }, matches.value.length === 0 ? "No matching options. Change your search." : matches.value.length > 50 ? "Showing the first 50 matches. Refine your search." : `${matches.value.length} options`)
468
+ ]);
469
+ }
470
+ return h("input", {
471
+ ...attrs,
472
+ ref: field,
473
+ type: spec.kind === "date" ? "date" : spec.kind === "checkbox" ? "checkbox" : "text",
474
+ inputmode: spec.kind === "number" ? "decimal" : void 0,
475
+ value: editor.draft,
476
+ checked: spec.kind === "checkbox" ? editor.value === true : void 0,
477
+ onInput: spec.kind === "checkbox" ? void 0 : (event) => editor.input(event.target.value),
478
+ onChange: spec.kind === "checkbox" ? (event) => editor.commit(event.target.checked) : void 0,
479
+ onBlur: () => {
480
+ if (props.blurCommit && !editor.pending) finish();
481
+ },
482
+ onPaste: spec.kind === "checkbox" ? void 0 : paste
483
+ });
484
+ };
485
+ }
486
+ });
487
+ /** The cell owns the id so it remains stable while its editor mounts and unmounts. */
488
+ function issueAttrs(id, issues, pending) {
489
+ return {
490
+ "aria-invalid": issues.length ? "true" : void 0,
491
+ "aria-describedby": issues.length ? id : void 0,
492
+ "aria-busy": pending ? "true" : void 0
493
+ };
494
+ }
495
+ function editorIssues(id, issues, pending = false) {
496
+ return issues.length || pending ? h("span", {
497
+ id,
498
+ "data-edit-issues": "",
499
+ role: "status"
500
+ }, pending ? "Checking changes…" : issues.map((problem) => problem.message).join(" ")) : null;
501
+ }
502
+ function useEditorId() {
503
+ return `vt-edit-${useId()}`;
504
+ }
505
+
327
506
  //#endregion
328
507
  //#region src/virtual.ts
329
508
  /** Shared opt-in renderer props. Options are applied when the virtual renderer is created. */
@@ -660,10 +839,11 @@ function treeKeydown(event, context, virtual) {
660
839
  else if (event.key === "Home" || event.key === "End") next = table.rows[event.key === "Home" ? 0 : table.rows.length - 1] ?? row;
661
840
  else return;
662
841
  event.preventDefault();
842
+ const root = target.closest("table");
663
843
  context.focus.value = next.key;
664
844
  virtual?.rows.scrollToKey(next.key);
665
845
  const id = itemId(context, next);
666
- nextTick(() => target.closest("table")?.ownerDocument.getElementById(id ?? "")?.focus());
846
+ nextTick(() => root?.ownerDocument.getElementById(id ?? "")?.focus());
667
847
  }
668
848
  const expandToggleProps = {
669
849
  row: rowProp,
@@ -840,16 +1020,19 @@ const DataTableRoot = defineComponent({
840
1020
  },
841
1021
  as: asProp("table"),
842
1022
  ...virtualProps,
843
- ...hierarchyProps
1023
+ ...hierarchyProps,
1024
+ ...columnLayoutProps
844
1025
  },
845
1026
  setup(props, { slots }) {
846
1027
  provideDataTable(props.table);
1028
+ provideColumnOffset(() => props.leadingColumns);
847
1029
  const hierarchy = provideHierarchy(props.table, props);
848
1030
  const element = shallowRef();
1031
+ const editingGrid = injectEditingGrid();
849
1032
  const render = (virtual) => h(props.as, {
850
1033
  role: props.table.tree ? "treegrid" : void 0,
851
1034
  "aria-rowcount": (props.table.tree ? props.table.processedRows.length : props.table.rowCount) + 1,
852
- "aria-colcount": props.table.columns.length,
1035
+ "aria-colcount": props.table.columns.length + props.leadingColumns,
853
1036
  "aria-busy": props.table.loadState === "loading" || props.table.loadState === "streaming" ? "true" : void 0,
854
1037
  "data-empty": flag(props.table.rowCount === 0),
855
1038
  onKeydown: (event) => treeKeydown(event, hierarchy, virtual),
@@ -860,6 +1043,7 @@ const DataTableRoot = defineComponent({
860
1043
  }, slots.default?.({ table: props.table }) ?? [h(DataTableHeader), h(DataTableBody)]);
861
1044
  return () => props.virtual ? h(VirtualContent, {
862
1045
  table: props.table,
1046
+ ...editingGrid ? { grid: editingGrid } : {},
863
1047
  options: virtualOptions(props),
864
1048
  tableElement: () => element.value,
865
1049
  render
@@ -929,7 +1113,7 @@ const DataTableHeaderCell = defineComponent({
929
1113
  },
930
1114
  setup(props, { slots }) {
931
1115
  const table = injectDataTable("<DataTableHeaderCell>");
932
- const virtual = injectVirtual();
1116
+ const columnOffset = injectColumnOffset();
933
1117
  return () => {
934
1118
  const { column } = props;
935
1119
  const sort = table.getSort(column.id);
@@ -947,7 +1131,7 @@ const DataTableHeaderCell = defineComponent({
947
1131
  "data-sortable": flag(column.sortable),
948
1132
  "data-sort": sort?.direction,
949
1133
  style: columnStyle(column),
950
- ...virtual?.columns ? { "aria-colindex": table.columns.indexOf(column) + 1 } : {}
1134
+ "aria-colindex": table.columns.indexOf(column) + columnOffset() + 1
951
1135
  }, content);
952
1136
  };
953
1137
  }
@@ -1088,6 +1272,7 @@ const DataTableRow = defineComponent({
1088
1272
  "aria-selected": table.selectionMode === "none" ? void 0 : String(selected),
1089
1273
  "data-selected": flag(selected),
1090
1274
  "data-key": String(row.key),
1275
+ "data-dirty": flag(row.isDirty),
1091
1276
  ...hierarchyRowAttrs(table, row, hierarchy),
1092
1277
  tabindex: table.tree ? (hierarchy?.focusKey ?? table.rows[0]?.key) === row.key ? 0 : -1 : void 0,
1093
1278
  onFocus: () => {
@@ -1124,6 +1309,7 @@ const DataTableCell = defineComponent({
1124
1309
  },
1125
1310
  setup(props, { slots }) {
1126
1311
  const table = injectDataTable("<DataTableCell>");
1312
+ const columnOffset = injectColumnOffset();
1127
1313
  const hierarchy = injectHierarchy();
1128
1314
  return () => {
1129
1315
  const { row, column } = props;
@@ -1137,7 +1323,8 @@ const DataTableCell = defineComponent({
1137
1323
  }) ?? display;
1138
1324
  return h(props.as, {
1139
1325
  "data-column": column.id,
1140
- "data-align": column.align
1326
+ "data-align": column.align,
1327
+ "aria-colindex": table.columns.indexOf(column) + columnOffset() + 1
1141
1328
  }, props.tree && table.tree && column.id === resolveTreeColumn(table, hierarchy?.treeColumn) ? h(DataTableTreeCell, { row }, { default: content }) : content());
1142
1329
  };
1143
1330
  }
@@ -1153,6 +1340,298 @@ const DataTableEmpty = defineComponent({
1153
1340
  }
1154
1341
  });
1155
1342
 
1343
+ //#endregion
1344
+ //#region src/editing.ts
1345
+ const editingKey = Symbol("vueye-table-editing");
1346
+ function injectEditingGrid() {
1347
+ return inject(editingKey, void 0)?.grid;
1348
+ }
1349
+ /** Renderless provider for editable data-table cells and row forms. */
1350
+ const DataTableEditRoot = defineComponent({
1351
+ name: "DataTableEditRoot",
1352
+ slots: Object,
1353
+ props: {
1354
+ table: {
1355
+ type: Object,
1356
+ required: true
1357
+ },
1358
+ mode: {
1359
+ type: String,
1360
+ default: "cell"
1361
+ }
1362
+ },
1363
+ emits: {
1364
+ save: (_result) => true,
1365
+ cancel: (_key) => true
1366
+ },
1367
+ setup(props, { slots, emit, expose }) {
1368
+ const grid = useDataGrid(props.table);
1369
+ const draft = shallowRef();
1370
+ const context = {
1371
+ grid,
1372
+ get mode() {
1373
+ return props.mode;
1374
+ },
1375
+ get draft() {
1376
+ return draft.value;
1377
+ },
1378
+ start(row) {
1379
+ if (draft.value?.pending) return;
1380
+ if (draft.value) context.cancel(draft.value.rowKey);
1381
+ grid.cancelEdit();
1382
+ const column = props.table.columns.findIndex((candidate) => candidate.isEditable(row.original));
1383
+ grid.focusCell({
1384
+ row: props.table.rows.findIndex((candidate) => candidate.key === row.key),
1385
+ column
1386
+ });
1387
+ grid.startEdit();
1388
+ draft.value = props.table.editRow(row.key);
1389
+ },
1390
+ async save() {
1391
+ const active = draft.value;
1392
+ if (!active || active.pending) return void 0;
1393
+ const result = await active.save();
1394
+ if (draft.value === active && active.status !== "editing" && !active.pending) {
1395
+ draft.value = void 0;
1396
+ grid.cancelEdit();
1397
+ }
1398
+ if (result.status !== "rejected") emit("save", result);
1399
+ return result;
1400
+ },
1401
+ cancel(key) {
1402
+ if (draft.value?.rowKey === key && !draft.value.pending) {
1403
+ draft.value.cancel();
1404
+ draft.value = void 0;
1405
+ grid.cancelEdit();
1406
+ emit("cancel", key);
1407
+ }
1408
+ }
1409
+ };
1410
+ provide(editingKey, context);
1411
+ expose({ editing: context });
1412
+ watch(() => props.table.rows, (rows) => {
1413
+ if (draft.value && !rows.some((row) => row.key === draft.value?.rowKey)) context.cancel(draft.value.rowKey);
1414
+ });
1415
+ onScopeDispose(() => draft.value?.cancel());
1416
+ return () => slots.default?.({ editing: context });
1417
+ }
1418
+ });
1419
+ /** A cell with on-demand editing; row mode shares one draft and one save operation. */
1420
+ const DataTableEditCell = defineComponent({
1421
+ name: "DataTableEditCell",
1422
+ props: {
1423
+ row: rowProp,
1424
+ column: columnProp,
1425
+ rowIndex: {
1426
+ type: Number,
1427
+ default: void 0
1428
+ },
1429
+ tree: {
1430
+ type: Boolean,
1431
+ default: true
1432
+ }
1433
+ },
1434
+ slots: Object,
1435
+ emits: {
1436
+ save: (_result) => true,
1437
+ cancel: (_key) => true
1438
+ },
1439
+ setup(props, { slots, emit }) {
1440
+ const table = injectDataTable("<DataTableEditCell>");
1441
+ const context = inject(editingKey);
1442
+ if (!context) throw new Error("<DataTableEditCell> requires <DataTableEditRoot>.");
1443
+ const { grid } = context;
1444
+ const id = useEditorId();
1445
+ const element = shallowRef();
1446
+ const position = () => ({
1447
+ row: props.rowIndex ?? table.rows.indexOf(props.row),
1448
+ column: table.columns.indexOf(props.column)
1449
+ });
1450
+ const active = computed(() => context.mode === "row" ? context.draft?.rowKey === props.row.key : grid.isEditing(position()));
1451
+ const pending = computed(() => context.mode === "row" ? !!context.draft?.pending : active.value && grid.lastResult?.status === "pending");
1452
+ const localText = shallowRef("");
1453
+ watch(active, (value) => {
1454
+ if (value) localText.value = editorText(props.row.getValue(props.column.id), grid.editorFor(position())?.kind ?? "text");
1455
+ });
1456
+ const focus = (direction) => {
1457
+ nextTick(() => {
1458
+ if (!direction) {
1459
+ element.value?.focus();
1460
+ return;
1461
+ }
1462
+ const all = [...element.value?.closest("table")?.querySelectorAll("[data-edit-cell][data-editable]") ?? []];
1463
+ all[all.indexOf(element.value) + (direction === "left" ? -1 : 1)]?.focus();
1464
+ });
1465
+ };
1466
+ const submit = (value, direction) => {
1467
+ if (context.mode === "row") {
1468
+ if (value !== void 0) {
1469
+ if (typeof value === "string") {
1470
+ localText.value = value;
1471
+ context.draft?.setValue(props.column.id, value);
1472
+ context.draft?.setInput(props.column.id, value);
1473
+ } else context.draft?.setValue(props.column.id, value);
1474
+ }
1475
+ return;
1476
+ }
1477
+ const result = grid.submitEdit(value === void 0 ? void 0 : typeof value === "string" ? { input: value } : { value });
1478
+ const finish = (final) => {
1479
+ if (final.status !== "rejected") {
1480
+ emit("save", final);
1481
+ focus(direction);
1482
+ }
1483
+ };
1484
+ if (result?.completion) result.completion.then(finish);
1485
+ else if (result) finish(result);
1486
+ return result;
1487
+ };
1488
+ return () => {
1489
+ const { row, column } = props;
1490
+ const editable = column.isEditable(row.original);
1491
+ const editing = editable && active.value;
1492
+ const draft = context.draft?.rowKey === row.key ? context.draft : void 0;
1493
+ 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)) ?? [] : []);
1494
+ const spec = editing ? grid.editorFor(position()) ?? { kind: "text" } : void 0;
1495
+ const input = (text) => {
1496
+ localText.value = text;
1497
+ if (context.mode === "row") {
1498
+ draft?.setValue(column.id, text);
1499
+ draft?.setInput(column.id, text);
1500
+ } else grid.updateDraft(text);
1501
+ };
1502
+ const cancel = () => {
1503
+ if (pending.value) return;
1504
+ if (context.mode === "row") context.cancel(row.key);
1505
+ else {
1506
+ grid.cancelEdit();
1507
+ emit("cancel", row.key);
1508
+ }
1509
+ focus();
1510
+ };
1511
+ const editor = {
1512
+ row,
1513
+ column,
1514
+ value: context.mode === "row" ? Reflect.get(draft?.values ?? {}, column.id) : row.getValue(column.id),
1515
+ draft: context.mode === "row" ? localText.value : grid.editor?.draft ?? "",
1516
+ spec: spec ?? { kind: "text" },
1517
+ finish: (direction) => {
1518
+ if (context.mode === "row") context.save();
1519
+ else submit(void 0, direction === "left" || direction === "right" ? direction : void 0);
1520
+ },
1521
+ input,
1522
+ update: input,
1523
+ commit: submit,
1524
+ cancel,
1525
+ issues,
1526
+ pending: pending.value,
1527
+ attrs: issueAttrs(id, issues, pending.value)
1528
+ };
1529
+ return h(DataTableCell, {
1530
+ ref: (target) => {
1531
+ element.value = target && typeof target === "object" && "$el" in target ? target.$el : void 0;
1532
+ },
1533
+ row,
1534
+ column,
1535
+ tree: props.tree,
1536
+ "data-edit-cell": "",
1537
+ "data-editable": editable ? "" : void 0,
1538
+ "data-editing": editing ? "" : void 0,
1539
+ "data-dirty": row.isDirty ? "" : void 0,
1540
+ ...issueAttrs(id, issues, pending.value),
1541
+ tabindex: editable ? 0 : void 0,
1542
+ onFocus: (event) => {
1543
+ if (event.target === element.value && !grid.editor) grid.focusCell(position());
1544
+ },
1545
+ onDblclick: () => {
1546
+ if (editable && context.mode === "cell") {
1547
+ grid.focusCell(position());
1548
+ grid.startEdit(editorText(row.getValue(column.id), grid.editorFor(position())?.kind ?? "text"));
1549
+ }
1550
+ },
1551
+ onKeydown: (event) => {
1552
+ if (event.target === element.value && event.key === "Enter" && editable) {
1553
+ event.preventDefault();
1554
+ event.stopPropagation();
1555
+ if (context.mode === "row") context.start(row);
1556
+ else {
1557
+ grid.focusCell(position());
1558
+ grid.startEdit(editorText(row.getValue(column.id), grid.editorFor(position())?.kind ?? "text"));
1559
+ }
1560
+ }
1561
+ }
1562
+ }, { default: ({ value, display }) => [editing ? slots.editor?.(editor) ?? h(DataCellEditor, {
1563
+ editor,
1564
+ spec: spec ?? { kind: "text" },
1565
+ autofocus: context.mode === "cell" || table.columns.find((candidate) => candidate.isEditable(row.original))?.id === column.id,
1566
+ blurCommit: context.mode === "cell",
1567
+ tabCommit: context.mode === "cell",
1568
+ finish: (direction) => {
1569
+ if (context.mode === "row") context.save();
1570
+ else submit(void 0, direction === "left" || direction === "right" ? direction : void 0);
1571
+ }
1572
+ }) : slots.default?.({
1573
+ row,
1574
+ value,
1575
+ display
1576
+ }) ?? display, editorIssues(id, issues, pending.value)] });
1577
+ };
1578
+ }
1579
+ });
1580
+ /** One active row form. Controls share the provider's draft and core save/undo path. */
1581
+ const DataTableRowActions = defineComponent({
1582
+ name: "DataTableRowActions",
1583
+ props: {
1584
+ row: rowProp,
1585
+ editable: {
1586
+ type: Boolean,
1587
+ default: true
1588
+ },
1589
+ removable: {
1590
+ type: Boolean,
1591
+ default: false
1592
+ }
1593
+ },
1594
+ setup(props) {
1595
+ const table = injectDataTable("<DataTableRowActions>");
1596
+ const context = inject(editingKey, void 0);
1597
+ const id = useEditorId();
1598
+ const element = shallowRef();
1599
+ watch(() => context?.draft?.rowKey === props.row.key, (active, previous) => {
1600
+ const target = element.value;
1601
+ if (previous && !active && !context?.draft && target?.closest("tr")?.contains(target.ownerDocument.activeElement)) nextTick(() => target.querySelector("button[data-edit-row]")?.focus());
1602
+ });
1603
+ return () => {
1604
+ const draft = context?.draft?.rowKey === props.row.key ? context.draft : void 0;
1605
+ const issues = [...new Map(draft?.issues.map((problem) => [problem.message, problem])).values()];
1606
+ const button = (label, action, disabled = false) => h("button", {
1607
+ type: "button",
1608
+ disabled,
1609
+ "aria-label": `${label}, row ${String(props.row.key)}`,
1610
+ "data-edit-row": label === "Edit row" ? "" : void 0,
1611
+ ...label === "Save row" ? issueAttrs(id, issues, !!draft?.pending) : {},
1612
+ onClick: action
1613
+ }, label);
1614
+ return h("div", {
1615
+ ref: element,
1616
+ "data-row-actions": "",
1617
+ role: "group",
1618
+ "aria-label": `Row ${String(props.row.key)} actions`,
1619
+ "aria-busy": draft?.pending ? "true" : void 0
1620
+ }, [
1621
+ ...context?.mode === "row" && props.editable ? draft ? [button("Save row", () => context.save(), draft.pending), button("Cancel", () => context.cancel(props.row.key), draft.pending)] : [button("Edit row", () => context.start(props.row))] : [],
1622
+ h("details", { onToggle: (event) => {
1623
+ const menu = event.currentTarget;
1624
+ if (menu.open) nextTick(() => menu.scrollIntoView?.({
1625
+ block: "nearest",
1626
+ inline: "nearest"
1627
+ }));
1628
+ } }, [h("summary", { "aria-label": `Actions for row ${String(props.row.key)}` }, "Actions"), h("div", { "data-row-menu": "" }, [button("Revert row", () => table.revert([props.row.key]), !props.row.isDirty || !!draft), props.removable ? button("Remove row", () => table.removeRows([props.row.key]), !!draft) : null])]),
1629
+ editorIssues(id, issues, !!draft?.pending)
1630
+ ]);
1631
+ };
1632
+ }
1633
+ });
1634
+
1156
1635
  //#endregion
1157
1636
  //#region src/grid.ts
1158
1637
  const gridContextKey = Symbol("vueye-table-grid-context");
@@ -1188,25 +1667,41 @@ const DataGridRoot = defineComponent({
1188
1667
  },
1189
1668
  ...virtualProps,
1190
1669
  ...hierarchyProps,
1670
+ ...columnLayoutProps,
1191
1671
  /** Width reserved before data columns, for a row-number/header column. */
1192
1672
  gutter: {
1193
1673
  type: Number,
1194
1674
  default: 0
1195
1675
  }
1196
1676
  },
1197
- setup(props, { slots, expose }) {
1677
+ emits: {
1678
+ save: (_result) => true,
1679
+ cancel: (_key) => true
1680
+ },
1681
+ setup(props, { slots, expose, emit }) {
1198
1682
  provideDataTable(props.table);
1683
+ provideColumnOffset(() => props.leadingColumns);
1199
1684
  const grid = useDataGrid(props.table, { treeColumn: () => props.treeColumn });
1200
1685
  provideDataGrid(grid);
1201
1686
  provideHierarchy(props.table, props);
1202
1687
  const element = ref();
1203
1688
  const id = `vt-grid-${useId()}`;
1204
1689
  const focus = () => {
1205
- nextTick(() => element.value?.focus());
1690
+ nextTick(() => {
1691
+ if (!grid.editor) element.value?.focus({ preventScroll: true });
1692
+ });
1206
1693
  };
1207
1694
  provide(gridContextKey, {
1208
1695
  id,
1209
- focus
1696
+ focus,
1697
+ cancel: (key) => emit("cancel", key)
1698
+ });
1699
+ watch(() => grid.lastResult, (result) => {
1700
+ if (result && [
1701
+ "applied",
1702
+ "partial",
1703
+ "unchanged"
1704
+ ].includes(result.status)) emit("save", result);
1210
1705
  });
1211
1706
  expose({
1212
1707
  grid,
@@ -1215,6 +1710,10 @@ const DataGridRoot = defineComponent({
1215
1710
  let virtual;
1216
1711
  const onKeydown = (event) => {
1217
1712
  if (event.target !== element.value) return;
1713
+ if (event.key === " " && !grid.editor && !event.ctrlKey && !event.metaKey && !event.altKey && grid.toggleCheckbox()) {
1714
+ event.preventDefault();
1715
+ return;
1716
+ }
1218
1717
  if (virtual && !grid.editor && (event.ctrlKey || event.metaKey) && (event.key === "Home" || event.key === "End")) {
1219
1718
  grid.focusCell(event.key === "Home" ? {
1220
1719
  row: 0,
@@ -1249,8 +1748,9 @@ const DataGridRoot = defineComponent({
1249
1748
  if (cut) grid.clear();
1250
1749
  };
1251
1750
  const onPaste = (event) => {
1751
+ if (grid.editor || event.defaultPrevented) return;
1252
1752
  const text = event.clipboardData?.getData("text/plain");
1253
- if (grid.editor || text === void 0 || text === "") return;
1753
+ if (text === void 0 || text === "") return;
1254
1754
  event.preventDefault();
1255
1755
  grid.paste(text);
1256
1756
  };
@@ -1265,7 +1765,7 @@ const DataGridRoot = defineComponent({
1265
1765
  "aria-label": props.label,
1266
1766
  "aria-multiselectable": "true",
1267
1767
  "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,
1768
+ "aria-colcount": props.table.columns.length + props.leadingColumns,
1269
1769
  "aria-busy": props.table.loadState === "loading" || props.table.loadState === "streaming" ? "true" : void 0,
1270
1770
  "aria-activedescendant": focusPosition && !grid.editor ? cellId(id, focusPosition.row, focusPosition.column) : void 0,
1271
1771
  onKeydown,
@@ -1341,12 +1841,17 @@ const DataGridBody = defineComponent({
1341
1841
  rowIndex,
1342
1842
  columnIndex,
1343
1843
  tree: props.treeCell
1344
- }, slots.cell ? { default: (cellProps) => slots.cell?.(cellProps) } : void 0), "td");
1844
+ }, {
1845
+ default: slots.cell,
1846
+ editor: slots.editor,
1847
+ $stable: true
1848
+ }), "td");
1345
1849
  return h("tr", {
1346
1850
  key: item.key,
1347
1851
  role: "row",
1348
1852
  "aria-rowindex": table.pageStart + rowIndex + 1,
1349
1853
  "data-key": String(row.key),
1854
+ "data-dirty": flag(row.isDirty),
1350
1855
  ...hierarchyRowAttrs(table, row, hierarchy),
1351
1856
  "data-virtual-row": "",
1352
1857
  ref: (target) => binding.rows.measureElement(target, item.key)
@@ -1365,6 +1870,7 @@ const DataGridBody = defineComponent({
1365
1870
  role: "row",
1366
1871
  "aria-rowindex": rowIndex + 2,
1367
1872
  "data-key": String(row.key),
1873
+ "data-dirty": flag(row.isDirty),
1368
1874
  ...hierarchyRowAttrs(table, row, hierarchy)
1369
1875
  }, [slots.rowHeader?.({
1370
1876
  row,
@@ -1376,7 +1882,11 @@ const DataGridBody = defineComponent({
1376
1882
  rowIndex,
1377
1883
  columnIndex,
1378
1884
  tree: props.treeCell
1379
- }, slots.cell ? { default: (cellProps) => slots.cell?.(cellProps) } : void 0))]);
1885
+ }, {
1886
+ default: slots.cell,
1887
+ editor: slots.editor,
1888
+ $stable: true
1889
+ }))]);
1380
1890
  })());
1381
1891
  const sentinel = loadSentinel(table, props.colspan ?? table.columns.length + (slots.rowHeader ? 1 : 0));
1382
1892
  return h(props.as, [
@@ -1394,52 +1904,6 @@ const DataGridBody = defineComponent({
1394
1904
  }) : render(virtual);
1395
1905
  }
1396
1906
  });
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
1907
  /**
1444
1908
  * A grid cell. A click selects it, Shift-click or dragging extends the range, and a double click
1445
1909
  * edits it. The `editor` slot replaces the default text box.
@@ -1468,10 +1932,12 @@ const DataGridCell = defineComponent({
1468
1932
  },
1469
1933
  setup(props, { slots }) {
1470
1934
  const table = injectDataTable("<DataGridCell>");
1935
+ const columnOffset = injectColumnOffset();
1471
1936
  const { grid, context } = useGrid("<DataGridCell>");
1472
1937
  const virtual = injectVirtual();
1473
1938
  const hierarchy = injectHierarchy();
1474
1939
  const element = ref();
1940
+ const issueId = useEditorId();
1475
1941
  const position = () => ({
1476
1942
  row: props.rowIndex ?? table.rows.indexOf(props.row),
1477
1943
  column: props.columnIndex ?? table.columns.indexOf(props.column)
@@ -1489,14 +1955,48 @@ const DataGridCell = defineComponent({
1489
1955
  const editable = column.isEditable(row.original);
1490
1956
  const value = row.getValue(column.id);
1491
1957
  const display = row.getDisplay(column.id);
1958
+ 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)) ?? [];
1959
+ const pending = table.pendingCells.some((cell) => cell.rowKey === row.key && cell.column === column.id);
1960
+ const finish = (edit, direction) => {
1961
+ const result = grid.submitEdit(edit, direction);
1962
+ const focus = () => {
1963
+ if (!grid.editor) context.focus();
1964
+ };
1965
+ if (result?.completion) result.completion.then(focus);
1966
+ else focus();
1967
+ return result;
1968
+ };
1492
1969
  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 {
1970
+ if (editing) {
1971
+ const editor = {
1972
+ row,
1973
+ column,
1974
+ value,
1975
+ draft: grid.editor?.draft ?? "",
1976
+ spec: grid.editorFor(at) ?? { kind: "text" },
1977
+ finish: (direction) => {
1978
+ finish(void 0, direction);
1979
+ },
1980
+ input: (text) => grid.updateDraft(text),
1981
+ update: (text) => grid.updateDraft(text),
1982
+ commit: (next) => finish(next === void 0 ? void 0 : typeof next === "string" ? { input: next } : { value: next }),
1983
+ cancel: () => {
1984
+ if (!pending) {
1985
+ grid.cancelEdit();
1986
+ context.cancel(row.key);
1987
+ context.focus();
1988
+ }
1989
+ },
1990
+ issues,
1991
+ pending,
1992
+ attrs: issueAttrs(issueId, issues, pending)
1993
+ };
1994
+ content = slots.editor?.(editor) ?? h(DataCellEditor, {
1995
+ editor,
1996
+ spec: grid.editorFor(at) ?? { kind: "text" },
1997
+ finish: (direction) => finish(void 0, direction)
1998
+ });
1999
+ } else {
1500
2000
  const children = () => slots.default?.({
1501
2001
  row,
1502
2002
  column,
@@ -1512,13 +2012,15 @@ const DataGridCell = defineComponent({
1512
2012
  role: "gridcell",
1513
2013
  "aria-selected": String(grid.isSelected(at)),
1514
2014
  "aria-readonly": editable ? void 0 : "true",
1515
- "aria-colindex": at.column + 1,
2015
+ "aria-colindex": at.column + columnOffset() + 1,
1516
2016
  "data-column": column.id,
1517
2017
  "data-align": column.align,
1518
2018
  "data-focused": flag(grid.isFocused(at)),
1519
2019
  "data-selected": flag(grid.isSelected(at)),
1520
2020
  "data-editing": flag(editing),
1521
2021
  "data-readonly": flag(!editable),
2022
+ "data-dirty": flag(row.isDirty),
2023
+ ...issueAttrs(issueId, issues, pending),
1522
2024
  style: columnStyle(column),
1523
2025
  onMousedown: (event) => {
1524
2026
  if (editing || event.button !== 0) return;
@@ -1530,13 +2032,13 @@ const DataGridCell = defineComponent({
1530
2032
  },
1531
2033
  onDblclick: () => {
1532
2034
  grid.focusCell(at);
1533
- grid.startEdit();
2035
+ grid.startEdit(editorText(value, grid.editorFor(at)?.kind ?? "text"));
1534
2036
  }
1535
- }, content);
2037
+ }, [content, editorIssues(issueId, issues, pending)]);
1536
2038
  };
1537
2039
  }
1538
2040
  });
1539
2041
 
1540
2042
  //#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 };
2043
+ 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
2044
  //# sourceMappingURL=index.js.map