@design.estate/dees-catalog 3.64.0 → 3.65.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@design.estate/dees-catalog",
3
- "version": "3.64.0",
3
+ "version": "3.65.0",
4
4
  "private": false,
5
5
  "description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
6
6
  "main": "dist_ts_web/index.js",
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@design.estate/dees-catalog',
6
- version: '3.64.0',
6
+ version: '3.65.0',
7
7
  description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
8
8
  }
@@ -55,36 +55,66 @@ export const demoFunc = () => html`
55
55
  <div class="demo-container">
56
56
  <div class="demo-section">
57
57
  <h2 class="demo-title">Basic Table with Actions</h2>
58
- <p class="demo-description">A standard table with row actions, editable fields, and context menu support. Double-click on descriptions to edit. Grid lines are enabled by default.</p>
58
+ <p class="demo-description">A standard table with row actions, editable cells, and context menu support. Double-click any cell to edit. Tab moves to the next editable cell, Enter to the row below, Esc cancels.</p>
59
59
  <dees-table
60
60
  heading1="Current Account Statement"
61
61
  heading2="Bunq - Payment Account 2 - April 2021"
62
- .editableFields="${['description']}"
62
+ .columns=${[
63
+ { key: 'date', header: 'Date', sortable: true, editable: true, editor: 'date' },
64
+ { key: 'amount', header: 'Amount', editable: true, editor: 'text' },
65
+ {
66
+ key: 'category',
67
+ header: 'Category',
68
+ editable: true,
69
+ editor: 'dropdown',
70
+ editorOptions: {
71
+ options: [
72
+ { option: 'Office Supplies', key: 'office' },
73
+ { option: 'Hardware', key: 'hardware' },
74
+ { option: 'Software', key: 'software' },
75
+ { option: 'Travel', key: 'travel' },
76
+ ],
77
+ },
78
+ },
79
+ { key: 'description', header: 'Description', editable: true },
80
+ { key: 'reconciled', header: 'OK', editable: true, editor: 'checkbox' },
81
+ ]}
82
+ @cellEdit=${(e: CustomEvent) => console.log('cellEdit', e.detail)}
63
83
  .data=${[
64
84
  {
65
85
  date: '2021-04-01',
66
86
  amount: '2464.65 €',
67
- description: 'Printing Paper (Office Supplies) - STAPLES BREMEN',
87
+ category: 'office',
88
+ description: 'Printing Paper - STAPLES BREMEN',
89
+ reconciled: true,
68
90
  },
69
91
  {
70
92
  date: '2021-04-02',
71
93
  amount: '165.65 €',
72
- description: 'Logitech Mouse (Hardware) - logi.com OnlineShop',
94
+ category: 'hardware',
95
+ description: 'Logitech Mouse - logi.com OnlineShop',
96
+ reconciled: false,
73
97
  },
74
98
  {
75
99
  date: '2021-04-03',
76
100
  amount: '2999,00 €',
77
- description: 'Macbook Pro 16inch (Hardware) - Apple.de OnlineShop',
101
+ category: 'hardware',
102
+ description: 'Macbook Pro 16inch - Apple.de OnlineShop',
103
+ reconciled: false,
78
104
  },
79
105
  {
80
106
  date: '2021-04-01',
81
107
  amount: '2464.65 €',
108
+ category: 'office',
82
109
  description: 'Office-Supplies - STAPLES BREMEN',
110
+ reconciled: true,
83
111
  },
84
112
  {
85
113
  date: '2021-04-01',
86
114
  amount: '2464.65 €',
115
+ category: 'office',
87
116
  description: 'Office-Supplies - STAPLES BREMEN',
117
+ reconciled: true,
88
118
  },
89
119
  ]}
90
120
  dataName="transactions"
@@ -510,13 +540,13 @@ export const demoFunc = () => html`
510
540
  <h2 class="demo-title">Column Filters + Sticky Header (New)</h2>
511
541
  <p class="demo-description">Per-column quick filters and sticky header with internal scroll. Try filtering the Name column. Uses --table-max-height var.</p>
512
542
  <style>
513
- dees-table[sticky-header] { --table-max-height: 220px; }
543
+ dees-table[fixed-height] { --table-max-height: 220px; }
514
544
  </style>
515
545
  <dees-table
516
546
  heading1="Employees"
517
547
  heading2="Quick filter per column + sticky header"
518
548
  .showColumnFilters=${true}
519
- .stickyHeader=${true}
549
+ .fixedHeight=${true}
520
550
  .columns=${[
521
551
  { key: 'name', header: 'Name', sortable: true },
522
552
  { key: 'email', header: 'Email', sortable: true },
@@ -669,7 +699,7 @@ export const demoFunc = () => html`
669
699
  </style>
670
700
  <dees-table
671
701
  id="scrollSmallHeight"
672
- .stickyHeader=${true}
702
+ .fixedHeight=${true}
673
703
  heading1="People Directory (Scrollable)"
674
704
  heading2="Forced scrolling with many items"
675
705
  .columns=${[
@@ -1,13 +1,25 @@
1
1
  import * as plugins from '../../00plugins.js';
2
2
  import { demoFunc } from './dees-table.demo.js';
3
- import { customElement, html, DeesElement, property, type TemplateResult, directives } from '@design.estate/dees-element';
3
+ import { customElement, html, DeesElement, property, state, type TemplateResult, directives } from '@design.estate/dees-element';
4
4
 
5
5
  import { DeesContextmenu } from '../../00group-overlay/dees-contextmenu/dees-contextmenu.js';
6
6
  import { DeesModal } from '../../00group-overlay/dees-modal/dees-modal.js';
7
7
  import * as domtools from '@design.estate/dees-domtools';
8
8
  import { type TIconKey } from '../../00group-utility/dees-icon/dees-icon.js';
9
9
  import { tableStyles } from './styles.js';
10
- import type { Column, ISortDescriptor, ITableAction, ITableActionDataArg, TDisplayFunction } from './types.js';
10
+ import type {
11
+ Column,
12
+ ISortDescriptor,
13
+ ITableAction,
14
+ ITableActionDataArg,
15
+ TCellEditorType,
16
+ TDisplayFunction,
17
+ } from './types.js';
18
+ import '../../00group-input/dees-input-text/index.js';
19
+ import '../../00group-input/dees-input-checkbox/index.js';
20
+ import '../../00group-input/dees-input-dropdown/index.js';
21
+ import '../../00group-input/dees-input-datepicker/index.js';
22
+ import '../../00group-input/dees-input-tags/index.js';
11
23
  import {
12
24
  computeColumnsFromDisplayFunction as computeColumnsFromDisplayFunctionFn,
13
25
  computeEffectiveColumns as computeEffectiveColumnsFn,
@@ -138,11 +150,6 @@ export class DeesTable<T> extends DeesElement {
138
150
  })
139
151
  accessor selectedDataRow!: T;
140
152
 
141
- @property({
142
- type: Array,
143
- })
144
- accessor editableFields: string[] = [];
145
-
146
153
  @property({
147
154
  type: Boolean,
148
155
  reflect: true,
@@ -224,6 +231,20 @@ export class DeesTable<T> extends DeesElement {
224
231
  */
225
232
  private __selectionAnchorId?: string;
226
233
 
234
+ /**
235
+ * Cell currently focused for keyboard navigation. When set, the cell shows
236
+ * a focus ring and Enter/F2 enters edit mode. Independent from row selection.
237
+ */
238
+ @state()
239
+ private accessor __focusedCell: { rowId: string; colKey: string } | undefined = undefined;
240
+
241
+ /**
242
+ * Cell currently being edited. When set, that cell renders an editor
243
+ * (dees-input-*) instead of its display content.
244
+ */
245
+ @state()
246
+ private accessor __editingCell: { rowId: string; colKey: string } | undefined = undefined;
247
+
227
248
  constructor() {
228
249
  super();
229
250
  // Make the host focusable so it can receive Ctrl/Cmd+C for copy.
@@ -238,24 +259,84 @@ export class DeesTable<T> extends DeesElement {
238
259
  * receive the copy.
239
260
  */
240
261
  private __handleHostKeydown = (eventArg: KeyboardEvent) => {
241
- const isCopy = (eventArg.metaKey || eventArg.ctrlKey) && (eventArg.key === 'c' || eventArg.key === 'C');
242
- if (!isCopy) return;
243
- // Don't hijack copy when the user is selecting text in an input/textarea.
262
+ // Detect whether the keydown originated inside an editor (input/textarea
263
+ // or contenteditable). Used to skip both copy hijacking and grid nav.
244
264
  const path = (eventArg.composedPath?.() || []) as EventTarget[];
265
+ let inEditor = false;
245
266
  for (const t of path) {
246
267
  const tag = (t as HTMLElement)?.tagName;
247
- if (tag === 'INPUT' || tag === 'TEXTAREA') return;
248
- if ((t as HTMLElement)?.isContentEditable) return;
268
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || (t as HTMLElement)?.isContentEditable) {
269
+ inEditor = true;
270
+ break;
271
+ }
249
272
  }
250
- const rows: T[] = [];
251
- if (this.selectedIds.size > 0) {
252
- for (const r of this.data) if (this.selectedIds.has(this.getRowId(r))) rows.push(r);
253
- } else if (this.selectedDataRow) {
254
- rows.push(this.selectedDataRow);
273
+
274
+ // Ctrl/Cmd+C → copy selected rows as JSON (unless typing in an input).
275
+ const isCopy =
276
+ (eventArg.metaKey || eventArg.ctrlKey) && (eventArg.key === 'c' || eventArg.key === 'C');
277
+ if (isCopy) {
278
+ if (inEditor) return;
279
+ const rows: T[] = [];
280
+ if (this.selectedIds.size > 0) {
281
+ for (const r of this.data) if (this.selectedIds.has(this.getRowId(r))) rows.push(r);
282
+ } else if (this.selectedDataRow) {
283
+ rows.push(this.selectedDataRow);
284
+ }
285
+ if (rows.length === 0) return;
286
+ eventArg.preventDefault();
287
+ this.__writeRowsAsJson(rows);
288
+ return;
289
+ }
290
+
291
+ // Cell navigation only when no editor is open.
292
+ if (inEditor || this.__editingCell) return;
293
+ switch (eventArg.key) {
294
+ case 'ArrowLeft':
295
+ eventArg.preventDefault();
296
+ this.moveFocusedCell(-1, 0, false);
297
+ return;
298
+ case 'ArrowRight':
299
+ eventArg.preventDefault();
300
+ this.moveFocusedCell(+1, 0, false);
301
+ return;
302
+ case 'ArrowUp':
303
+ eventArg.preventDefault();
304
+ this.moveFocusedCell(0, -1, false);
305
+ return;
306
+ case 'ArrowDown':
307
+ eventArg.preventDefault();
308
+ this.moveFocusedCell(0, +1, false);
309
+ return;
310
+ case 'Enter':
311
+ case 'F2': {
312
+ if (!this.__focusedCell) return;
313
+ const view: T[] = (this as any)._lastViewData ?? [];
314
+ const item = view.find((r) => this.getRowId(r) === this.__focusedCell!.rowId);
315
+ if (!item) return;
316
+ const allCols: Column<T>[] =
317
+ Array.isArray(this.columns) && this.columns.length > 0
318
+ ? computeEffectiveColumnsFn(
319
+ this.columns,
320
+ this.augmentFromDisplayFunction,
321
+ this.displayFunction,
322
+ this.data
323
+ )
324
+ : computeColumnsFromDisplayFunctionFn(this.displayFunction, this.data);
325
+ const col = allCols.find((c) => String(c.key) === this.__focusedCell!.colKey);
326
+ if (!col || !this.__isColumnEditable(col)) return;
327
+ eventArg.preventDefault();
328
+ this.startEditing(item, col);
329
+ return;
330
+ }
331
+ case 'Escape':
332
+ if (this.__focusedCell) {
333
+ this.__focusedCell = undefined;
334
+ this.requestUpdate();
335
+ }
336
+ return;
337
+ default:
338
+ return;
255
339
  }
256
- if (rows.length === 0) return;
257
- eventArg.preventDefault();
258
- this.__writeRowsAsJson(rows);
259
340
  };
260
341
 
261
342
  /**
@@ -492,20 +573,48 @@ export class DeesTable<T> extends DeesElement {
492
573
  ? col.renderer(value, itemArg, { rowIndex, colIndex, column: col })
493
574
  : value;
494
575
  const editKey = String(col.key);
576
+ const isEditable = !!(col.editable || col.editor);
577
+ const rowId = this.getRowId(itemArg);
578
+ const isFocused =
579
+ this.__focusedCell?.rowId === rowId &&
580
+ this.__focusedCell?.colKey === editKey;
581
+ const isEditing =
582
+ this.__editingCell?.rowId === rowId &&
583
+ this.__editingCell?.colKey === editKey;
584
+ const cellClasses = [
585
+ isEditable ? 'editable' : '',
586
+ isFocused && !isEditing ? 'focused' : '',
587
+ isEditing ? 'editingCell' : '',
588
+ ]
589
+ .filter(Boolean)
590
+ .join(' ');
495
591
  return html`
496
592
  <td
593
+ class=${cellClasses}
594
+ @click=${(e: MouseEvent) => {
595
+ if (isEditing) {
596
+ e.stopPropagation();
597
+ return;
598
+ }
599
+ if (isEditable) {
600
+ this.__focusedCell = { rowId, colKey: editKey };
601
+ }
602
+ }}
497
603
  @dblclick=${(e: Event) => {
498
604
  const dblAction = this.dataActions.find((actionArg) =>
499
605
  actionArg.type?.includes('doubleClick')
500
606
  );
501
- if (this.editableFields.includes(editKey)) {
502
- this.handleCellEditing(e, itemArg, editKey);
607
+ if (isEditable) {
608
+ e.stopPropagation();
609
+ this.startEditing(itemArg, col);
503
610
  } else if (dblAction) {
504
611
  dblAction.actionFunc({ item: itemArg, table: this });
505
612
  }
506
613
  }}
507
614
  >
508
- <div class="innerCellContainer">${content}</div>
615
+ <div class="innerCellContainer">
616
+ ${isEditing ? this.renderCellEditor(itemArg, col) : content}
617
+ </div>
509
618
  </td>
510
619
  `;
511
620
  })}
@@ -1524,43 +1633,216 @@ export class DeesTable<T> extends DeesElement {
1524
1633
  return actions;
1525
1634
  }
1526
1635
 
1527
- async handleCellEditing(event: Event, itemArg: T, key: string) {
1528
- await this.domtoolsPromise;
1529
- const target = event.target as HTMLElement;
1530
- const originalColor = target.style.color;
1531
- target.style.color = 'transparent';
1532
- const transformedItem = this.displayFunction(itemArg);
1533
- const initialValue = ((transformedItem as any)[key] ?? (itemArg as any)[key] ?? '') as string;
1534
- // Create an input element
1535
- const input = document.createElement('input');
1536
- input.type = 'text';
1537
- input.value = initialValue;
1538
-
1539
- const blurInput = async (blurArg = true, saveArg = false) => {
1540
- if (blurArg) {
1541
- input.blur();
1636
+ // ─── Cell editing ─────────────────────────────────────────────────────
1637
+
1638
+ /** True if the column has any in-cell editor configured. */
1639
+ private __isColumnEditable(col: Column<T>): boolean {
1640
+ return !!(col.editable || col.editor);
1641
+ }
1642
+
1643
+ /** Effective columns filtered to those that can be edited (visible only). */
1644
+ private __editableColumns(effectiveColumns: Column<T>[]): Column<T>[] {
1645
+ return effectiveColumns.filter((c) => !c.hidden && this.__isColumnEditable(c));
1646
+ }
1647
+
1648
+ /**
1649
+ * Opens the editor on the given cell. Sets focus + editing state and
1650
+ * focuses the freshly rendered editor on the next frame.
1651
+ */
1652
+ public startEditing(item: T, col: Column<T>) {
1653
+ if (!this.__isColumnEditable(col)) return;
1654
+ const rowId = this.getRowId(item);
1655
+ const colKey = String(col.key);
1656
+ this.__focusedCell = { rowId, colKey };
1657
+ this.__editingCell = { rowId, colKey };
1658
+ this.requestUpdate();
1659
+ this.updateComplete.then(() => {
1660
+ const el = this.shadowRoot?.querySelector(
1661
+ '.editingCell dees-input-text, .editingCell dees-input-checkbox, ' +
1662
+ '.editingCell dees-input-dropdown, .editingCell dees-input-datepicker, ' +
1663
+ '.editingCell dees-input-tags'
1664
+ ) as any;
1665
+ el?.focus?.();
1666
+ });
1667
+ }
1668
+
1669
+ /** Closes the editor without committing. */
1670
+ public cancelCellEdit() {
1671
+ this.__editingCell = undefined;
1672
+ this.requestUpdate();
1673
+ }
1674
+
1675
+ /**
1676
+ * Commits an editor value to the row. Runs `parse` then `validate`. On
1677
+ * validation failure, fires `cellEditError` and leaves the editor open.
1678
+ * On success, mutates `data` in place, fires `cellEdit`, and closes the
1679
+ * editor.
1680
+ */
1681
+ public commitCellEdit(item: T, col: Column<T>, editorValue: any) {
1682
+ const key = String(col.key);
1683
+ const oldValue = (item as any)[col.key];
1684
+ const parsed = col.parse ? col.parse(editorValue, item) : editorValue;
1685
+ if (col.validate) {
1686
+ const result = col.validate(parsed, item);
1687
+ if (typeof result === 'string') {
1688
+ this.dispatchEvent(
1689
+ new CustomEvent('cellEditError', {
1690
+ detail: { row: item, key, value: parsed, message: result },
1691
+ bubbles: true,
1692
+ composed: true,
1693
+ })
1694
+ );
1695
+ return;
1542
1696
  }
1543
- if (saveArg) {
1544
- (itemArg as any)[key] = input.value as any; // Convert string to T (you might need better type casting depending on your data structure)
1545
- this.changeSubject.next(this);
1697
+ }
1698
+ if (parsed !== oldValue) {
1699
+ (item as any)[col.key] = parsed;
1700
+ this.dispatchEvent(
1701
+ new CustomEvent('cellEdit', {
1702
+ detail: { row: item, key, oldValue, newValue: parsed },
1703
+ bubbles: true,
1704
+ composed: true,
1705
+ })
1706
+ );
1707
+ this.changeSubject.next(this);
1708
+ }
1709
+ this.__editingCell = undefined;
1710
+ this.requestUpdate();
1711
+ }
1712
+
1713
+ /** Renders the appropriate dees-input-* component for this column. */
1714
+ private renderCellEditor(item: T, col: Column<T>): TemplateResult {
1715
+ const raw = (item as any)[col.key];
1716
+ const value = col.format ? col.format(raw, item) : raw;
1717
+ const editorType: TCellEditorType = col.editor ?? 'text';
1718
+ const onTextCommit = (target: any) => this.commitCellEdit(item, col, target.value);
1719
+
1720
+ switch (editorType) {
1721
+ case 'checkbox':
1722
+ return html`<dees-input-checkbox
1723
+ .value=${!!value}
1724
+ @newValue=${(e: CustomEvent<boolean>) => {
1725
+ e.stopPropagation();
1726
+ this.commitCellEdit(item, col, e.detail);
1727
+ }}
1728
+ ></dees-input-checkbox>`;
1729
+
1730
+ case 'dropdown': {
1731
+ const options = (col.editorOptions?.options as any[]) ?? [];
1732
+ const selected =
1733
+ options.find((o: any) => (o?.option ?? o?.key ?? o) === value) ?? null;
1734
+ return html`<dees-input-dropdown
1735
+ .options=${options}
1736
+ .selectedOption=${selected}
1737
+ @selectedOption=${(e: CustomEvent<any>) => {
1738
+ e.stopPropagation();
1739
+ const detail = e.detail;
1740
+ const newRaw = detail?.option ?? detail?.key ?? detail;
1741
+ this.commitCellEdit(item, col, newRaw);
1742
+ }}
1743
+ ></dees-input-dropdown>`;
1546
1744
  }
1547
- input.remove();
1548
- target.style.color = originalColor;
1549
- this.requestUpdate();
1550
- };
1551
1745
 
1552
- // When the input loses focus or the Enter key is pressed, update the data
1553
- input.addEventListener('blur', () => {
1554
- blurInput(false, false);
1555
- });
1556
- input.addEventListener('keydown', (e: KeyboardEvent) => {
1557
- if (e.key === 'Enter') {
1558
- blurInput(true, true); // This will trigger the blur event handler above
1746
+ case 'date':
1747
+ return html`<dees-input-datepicker
1748
+ .value=${value}
1749
+ @focusout=${(e: any) => onTextCommit(e.target)}
1750
+ @keydown=${(e: KeyboardEvent) => this.__handleEditorKey(e, item, col)}
1751
+ ></dees-input-datepicker>`;
1752
+
1753
+ case 'tags':
1754
+ return html`<dees-input-tags
1755
+ .value=${(value as any) ?? []}
1756
+ @focusout=${(e: any) => onTextCommit(e.target)}
1757
+ @keydown=${(e: KeyboardEvent) => this.__handleEditorKey(e, item, col)}
1758
+ ></dees-input-tags>`;
1759
+
1760
+ case 'number':
1761
+ case 'text':
1762
+ default:
1763
+ return html`<dees-input-text
1764
+ .value=${value == null ? '' : String(value)}
1765
+ @focusout=${(e: any) => onTextCommit(e.target)}
1766
+ @keydown=${(e: KeyboardEvent) => this.__handleEditorKey(e, item, col)}
1767
+ ></dees-input-text>`;
1768
+ }
1769
+ }
1770
+
1771
+ /**
1772
+ * Centralized keydown handler for text-style editors. Handles Esc (cancel),
1773
+ * Enter (commit + move down) and Tab/Shift+Tab (commit + move horizontally).
1774
+ */
1775
+ private __handleEditorKey(eventArg: KeyboardEvent, item: T, col: Column<T>) {
1776
+ if (eventArg.key === 'Escape') {
1777
+ eventArg.preventDefault();
1778
+ eventArg.stopPropagation();
1779
+ this.cancelCellEdit();
1780
+ // Restore focus to the host so arrow-key navigation can resume.
1781
+ this.focus();
1782
+ } else if (eventArg.key === 'Enter') {
1783
+ eventArg.preventDefault();
1784
+ eventArg.stopPropagation();
1785
+ const target = eventArg.target as any;
1786
+ this.commitCellEdit(item, col, target.value);
1787
+ this.moveFocusedCell(0, +1, true);
1788
+ } else if (eventArg.key === 'Tab') {
1789
+ eventArg.preventDefault();
1790
+ eventArg.stopPropagation();
1791
+ const target = eventArg.target as any;
1792
+ this.commitCellEdit(item, col, target.value);
1793
+ this.moveFocusedCell(eventArg.shiftKey ? -1 : +1, 0, true);
1794
+ }
1795
+ }
1796
+
1797
+ /**
1798
+ * Moves the focused cell by `dx` columns and `dy` rows along the editable
1799
+ * grid. Wraps row-end → next row when moving horizontally. If
1800
+ * `andStartEditing` is true, opens the editor on the new cell.
1801
+ */
1802
+ public moveFocusedCell(dx: number, dy: number, andStartEditing: boolean) {
1803
+ const view: T[] = (this as any)._lastViewData ?? [];
1804
+ if (view.length === 0) return;
1805
+ // Recompute editable columns from the latest effective set.
1806
+ const allCols: Column<T>[] = Array.isArray(this.columns) && this.columns.length > 0
1807
+ ? computeEffectiveColumnsFn(this.columns, this.augmentFromDisplayFunction, this.displayFunction, this.data)
1808
+ : computeColumnsFromDisplayFunctionFn(this.displayFunction, this.data);
1809
+ const editableCols = this.__editableColumns(allCols);
1810
+ if (editableCols.length === 0) return;
1811
+
1812
+ let rowIdx = 0;
1813
+ let colIdx = 0;
1814
+ if (this.__focusedCell) {
1815
+ rowIdx = view.findIndex((r) => this.getRowId(r) === this.__focusedCell!.rowId);
1816
+ colIdx = editableCols.findIndex((c) => String(c.key) === this.__focusedCell!.colKey);
1817
+ if (rowIdx < 0) rowIdx = 0;
1818
+ if (colIdx < 0) colIdx = 0;
1819
+ }
1820
+
1821
+ if (dx !== 0) {
1822
+ colIdx += dx;
1823
+ while (colIdx >= editableCols.length) {
1824
+ colIdx -= editableCols.length;
1825
+ rowIdx += 1;
1559
1826
  }
1560
- });
1827
+ while (colIdx < 0) {
1828
+ colIdx += editableCols.length;
1829
+ rowIdx -= 1;
1830
+ }
1831
+ }
1832
+ if (dy !== 0) rowIdx += dy;
1561
1833
 
1562
- // Replace the cell's content with the input
1563
- target.appendChild(input);
1564
- input.focus();
1834
+ // Clamp to grid bounds.
1835
+ if (rowIdx < 0 || rowIdx >= view.length) {
1836
+ this.cancelCellEdit();
1837
+ return;
1838
+ }
1839
+ const item = view[rowIdx];
1840
+ const col = editableCols[colIdx];
1841
+ this.__focusedCell = { rowId: this.getRowId(item), colKey: String(col.key) };
1842
+ if (andStartEditing) {
1843
+ this.startEditing(item, col);
1844
+ } else {
1845
+ this.requestUpdate();
1846
+ }
1565
1847
  }
1566
1848
  }
@@ -372,32 +372,32 @@ export const tableStyles: CSSResult[] = [
372
372
  min-height: 24px;
373
373
  line-height: 24px;
374
374
  }
375
- td input {
376
- position: absolute;
377
- top: 4px;
378
- bottom: 4px;
379
- left: 20px;
380
- right: 20px;
381
- width: calc(100% - 40px);
382
- height: calc(100% - 8px);
383
- padding: 0 12px;
384
- outline: none;
385
- border: 1px solid var(--dees-color-border-default);
386
- border-radius: 6px;
387
- background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
388
- color: var(--dees-color-text-primary);
389
- font-family: inherit;
390
- font-size: inherit;
391
- font-weight: inherit;
392
- transition: all 0.15s ease;
393
- box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
394
- }
395
-
396
- td input:focus {
397
- border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
398
- outline: 2px solid transparent;
399
- outline-offset: 2px;
400
- box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.2)', 'hsl(217.2 91.2% 59.8% / 0.2)')};
375
+
376
+ /* Editable cell affordances */
377
+ td.editable {
378
+ cursor: text;
379
+ }
380
+ td.focused {
381
+ outline: 2px solid ${cssManager.bdTheme(
382
+ 'hsl(222.2 47.4% 51.2% / 0.6)',
383
+ 'hsl(217.2 91.2% 59.8% / 0.6)'
384
+ )};
385
+ outline-offset: -2px;
386
+ }
387
+ td.editingCell {
388
+ padding: 0;
389
+ }
390
+ td.editingCell .innerCellContainer {
391
+ padding: 0;
392
+ line-height: normal;
393
+ }
394
+ td.editingCell dees-input-text,
395
+ td.editingCell dees-input-checkbox,
396
+ td.editingCell dees-input-dropdown,
397
+ td.editingCell dees-input-datepicker,
398
+ td.editingCell dees-input-tags {
399
+ display: block;
400
+ width: 100%;
401
401
  }
402
402
 
403
403
  /* filter row */