@design.estate/dees-catalog 3.63.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/dist_bundle/bundle.js +502 -114
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/00group-dataview/dees-table/dees-table.d.ts +83 -2
- package/dist_ts_web/elements/00group-dataview/dees-table/dees-table.demo.js +39 -9
- package/dist_ts_web/elements/00group-dataview/dees-table/dees-table.js +510 -73
- package/dist_ts_web/elements/00group-dataview/dees-table/styles.js +24 -26
- package/dist_ts_web/elements/00group-dataview/dees-table/types.d.ts +31 -0
- package/dist_watch/bundle.js +500 -112
- package/dist_watch/bundle.js.map +3 -3
- package/package.json +1 -1
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/00group-dataview/dees-table/dees-table.demo.ts +38 -8
- package/ts_web/elements/00group-dataview/dees-table/dees-table.ts +514 -73
- package/ts_web/elements/00group-dataview/dees-table/styles.ts +26 -25
- package/ts_web/elements/00group-dataview/dees-table/types.ts +40 -0
|
@@ -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 {
|
|
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,
|
|
@@ -184,6 +191,14 @@ export class DeesTable<T> extends DeesElement {
|
|
|
184
191
|
accessor columnFilters: Record<string, string> = {};
|
|
185
192
|
@property({ type: Boolean, attribute: 'show-column-filters' })
|
|
186
193
|
accessor showColumnFilters: boolean = false;
|
|
194
|
+
/**
|
|
195
|
+
* When true, the table renders a leftmost checkbox column for click-driven
|
|
196
|
+
* (de)selection. Row selection by mouse (plain/shift/ctrl click) is always
|
|
197
|
+
* available regardless of this flag.
|
|
198
|
+
*/
|
|
199
|
+
@property({ type: Boolean, reflect: true, attribute: 'show-selection-checkbox' })
|
|
200
|
+
accessor showSelectionCheckbox: boolean = false;
|
|
201
|
+
|
|
187
202
|
/**
|
|
188
203
|
* When set, the table renders inside a fixed-height scroll container
|
|
189
204
|
* (`max-height: var(--table-max-height, 360px)`) and the header sticks
|
|
@@ -209,9 +224,146 @@ export class DeesTable<T> extends DeesElement {
|
|
|
209
224
|
accessor selectedIds: Set<string> = new Set();
|
|
210
225
|
private _rowIdMap = new WeakMap<object, string>();
|
|
211
226
|
private _rowIdCounter = 0;
|
|
227
|
+
/**
|
|
228
|
+
* Anchor row id for shift+click range selection. Set whenever the user
|
|
229
|
+
* makes a non-range click (plain or cmd/ctrl) so the next shift+click
|
|
230
|
+
* can compute a contiguous range from this anchor.
|
|
231
|
+
*/
|
|
232
|
+
private __selectionAnchorId?: string;
|
|
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;
|
|
212
247
|
|
|
213
248
|
constructor() {
|
|
214
249
|
super();
|
|
250
|
+
// Make the host focusable so it can receive Ctrl/Cmd+C for copy.
|
|
251
|
+
if (!this.hasAttribute('tabindex')) this.setAttribute('tabindex', '0');
|
|
252
|
+
this.addEventListener('keydown', this.__handleHostKeydown);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Ctrl/Cmd+C copies the currently selected rows as a JSON array. Falls
|
|
257
|
+
* back to copying the focused-row (`selectedDataRow`) if no multi
|
|
258
|
+
* selection exists. No-op if a focused input/textarea would normally
|
|
259
|
+
* receive the copy.
|
|
260
|
+
*/
|
|
261
|
+
private __handleHostKeydown = (eventArg: KeyboardEvent) => {
|
|
262
|
+
// Detect whether the keydown originated inside an editor (input/textarea
|
|
263
|
+
// or contenteditable). Used to skip both copy hijacking and grid nav.
|
|
264
|
+
const path = (eventArg.composedPath?.() || []) as EventTarget[];
|
|
265
|
+
let inEditor = false;
|
|
266
|
+
for (const t of path) {
|
|
267
|
+
const tag = (t as HTMLElement)?.tagName;
|
|
268
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || (t as HTMLElement)?.isContentEditable) {
|
|
269
|
+
inEditor = true;
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
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;
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Copies the current selection as a JSON array. If `fallbackRow` is given
|
|
344
|
+
* and there is no multi-selection, that row is copied instead. Used both
|
|
345
|
+
* by the Ctrl/Cmd+C handler and by the default context-menu action.
|
|
346
|
+
*/
|
|
347
|
+
public copySelectionAsJson(fallbackRow?: T) {
|
|
348
|
+
const rows: T[] = [];
|
|
349
|
+
if (this.selectedIds.size > 0) {
|
|
350
|
+
for (const r of this.data) if (this.selectedIds.has(this.getRowId(r))) rows.push(r);
|
|
351
|
+
} else if (fallbackRow) {
|
|
352
|
+
rows.push(fallbackRow);
|
|
353
|
+
} else if (this.selectedDataRow) {
|
|
354
|
+
rows.push(this.selectedDataRow);
|
|
355
|
+
}
|
|
356
|
+
if (rows.length === 0) return;
|
|
357
|
+
this.__writeRowsAsJson(rows);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private __writeRowsAsJson(rows: T[]) {
|
|
361
|
+
try {
|
|
362
|
+
const json = JSON.stringify(rows, null, 2);
|
|
363
|
+
navigator.clipboard?.writeText(json);
|
|
364
|
+
} catch {
|
|
365
|
+
/* ignore — clipboard may be unavailable */
|
|
366
|
+
}
|
|
215
367
|
}
|
|
216
368
|
|
|
217
369
|
public static styles = tableStyles;
|
|
@@ -319,15 +471,11 @@ export class DeesTable<T> extends DeesElement {
|
|
|
319
471
|
};
|
|
320
472
|
return html`
|
|
321
473
|
<tr
|
|
322
|
-
@click=${() =>
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
this.selectedIds.add(id);
|
|
328
|
-
this.emitSelectionChange();
|
|
329
|
-
this.requestUpdate();
|
|
330
|
-
}
|
|
474
|
+
@click=${(e: MouseEvent) => this.handleRowClick(e, itemArg, rowIndex, viewData)}
|
|
475
|
+
@mousedown=${(e: MouseEvent) => {
|
|
476
|
+
// Prevent the browser's native shift-click text
|
|
477
|
+
// selection so range-select doesn't highlight text.
|
|
478
|
+
if (e.shiftKey && this.selectionMode !== 'single') e.preventDefault();
|
|
331
479
|
}}
|
|
332
480
|
@dragenter=${async (eventArg: DragEvent) => {
|
|
333
481
|
eventArg.preventDefault();
|
|
@@ -362,27 +510,51 @@ export class DeesTable<T> extends DeesElement {
|
|
|
362
510
|
}
|
|
363
511
|
}}
|
|
364
512
|
@contextmenu=${async (eventArg: MouseEvent) => {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
513
|
+
// If the right-clicked row isn't part of the
|
|
514
|
+
// current selection, treat it like a plain click
|
|
515
|
+
// first so the context menu acts on a sensible
|
|
516
|
+
// selection (matches file-manager behavior).
|
|
517
|
+
if (!this.isRowSelected(itemArg)) {
|
|
518
|
+
this.selectedDataRow = itemArg;
|
|
519
|
+
this.selectedIds.clear();
|
|
520
|
+
this.selectedIds.add(this.getRowId(itemArg));
|
|
521
|
+
this.__selectionAnchorId = this.getRowId(itemArg);
|
|
522
|
+
this.emitSelectionChange();
|
|
523
|
+
this.requestUpdate();
|
|
524
|
+
}
|
|
525
|
+
const userItems: plugins.tsclass.website.IMenuItem[] =
|
|
526
|
+
this.getActionsForType('contextmenu').map((action) => ({
|
|
527
|
+
name: action.name,
|
|
528
|
+
iconName: action.iconName as any,
|
|
529
|
+
action: async () => {
|
|
530
|
+
await action.actionFunc({
|
|
531
|
+
item: itemArg,
|
|
532
|
+
table: this,
|
|
533
|
+
});
|
|
534
|
+
return null;
|
|
535
|
+
},
|
|
536
|
+
}));
|
|
537
|
+
const defaultItems: plugins.tsclass.website.IMenuItem[] = [
|
|
538
|
+
{
|
|
539
|
+
name:
|
|
540
|
+
this.selectedIds.size > 1
|
|
541
|
+
? `Copy ${this.selectedIds.size} rows as JSON`
|
|
542
|
+
: 'Copy row as JSON',
|
|
543
|
+
iconName: 'lucide:Copy' as any,
|
|
544
|
+
action: async () => {
|
|
545
|
+
this.copySelectionAsJson(itemArg);
|
|
546
|
+
return null;
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
];
|
|
550
|
+
DeesContextmenu.openContextMenuWithOptions(eventArg, [
|
|
551
|
+
...userItems,
|
|
552
|
+
...defaultItems,
|
|
553
|
+
]);
|
|
382
554
|
}}
|
|
383
|
-
class="${itemArg === this.selectedDataRow ? 'selected' : ''}"
|
|
555
|
+
class="${itemArg === this.selectedDataRow || this.isRowSelected(itemArg) ? 'selected' : ''}"
|
|
384
556
|
>
|
|
385
|
-
${this.
|
|
557
|
+
${this.showSelectionCheckbox
|
|
386
558
|
? html`<td style="width:42px; text-align:center;">
|
|
387
559
|
<dees-input-checkbox
|
|
388
560
|
.value=${this.isRowSelected(itemArg)}
|
|
@@ -401,20 +573,48 @@ export class DeesTable<T> extends DeesElement {
|
|
|
401
573
|
? col.renderer(value, itemArg, { rowIndex, colIndex, column: col })
|
|
402
574
|
: value;
|
|
403
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(' ');
|
|
404
591
|
return html`
|
|
405
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
|
+
}}
|
|
406
603
|
@dblclick=${(e: Event) => {
|
|
407
604
|
const dblAction = this.dataActions.find((actionArg) =>
|
|
408
605
|
actionArg.type?.includes('doubleClick')
|
|
409
606
|
);
|
|
410
|
-
if (
|
|
411
|
-
|
|
607
|
+
if (isEditable) {
|
|
608
|
+
e.stopPropagation();
|
|
609
|
+
this.startEditing(itemArg, col);
|
|
412
610
|
} else if (dblAction) {
|
|
413
611
|
dblAction.actionFunc({ item: itemArg, table: this });
|
|
414
612
|
}
|
|
415
613
|
}}
|
|
416
614
|
>
|
|
417
|
-
<div class="innerCellContainer"
|
|
615
|
+
<div class="innerCellContainer">
|
|
616
|
+
${isEditing ? this.renderCellEditor(itemArg, col) : content}
|
|
617
|
+
</div>
|
|
418
618
|
</td>
|
|
419
619
|
`;
|
|
420
620
|
})}
|
|
@@ -502,7 +702,7 @@ export class DeesTable<T> extends DeesElement {
|
|
|
502
702
|
private renderHeaderRows(effectiveColumns: Column<T>[]): TemplateResult {
|
|
503
703
|
return html`
|
|
504
704
|
<tr>
|
|
505
|
-
${this.
|
|
705
|
+
${this.showSelectionCheckbox
|
|
506
706
|
? html`
|
|
507
707
|
<th style="width:42px; text-align:center;">
|
|
508
708
|
${this.selectionMode === 'multi'
|
|
@@ -547,7 +747,7 @@ export class DeesTable<T> extends DeesElement {
|
|
|
547
747
|
</tr>
|
|
548
748
|
${this.showColumnFilters
|
|
549
749
|
? html`<tr class="filtersRow">
|
|
550
|
-
${this.
|
|
750
|
+
${this.showSelectionCheckbox
|
|
551
751
|
? html`<th style="width:42px;"></th>`
|
|
552
752
|
: html``}
|
|
553
753
|
${effectiveColumns
|
|
@@ -1302,6 +1502,74 @@ export class DeesTable<T> extends DeesElement {
|
|
|
1302
1502
|
this.requestUpdate();
|
|
1303
1503
|
}
|
|
1304
1504
|
|
|
1505
|
+
/**
|
|
1506
|
+
* Handles row clicks with file-manager style selection semantics:
|
|
1507
|
+
* - plain click: select only this row, set anchor
|
|
1508
|
+
* - cmd/ctrl+click: toggle this row in/out, set anchor
|
|
1509
|
+
* - shift+click: select the contiguous range from the anchor to this row
|
|
1510
|
+
*
|
|
1511
|
+
* Multi-row click selection is always available (`selectionMode === 'none'`
|
|
1512
|
+
* and `'multi'` both behave this way) so consumers can always copy a set
|
|
1513
|
+
* of rows. Only `selectionMode === 'single'` restricts to one row.
|
|
1514
|
+
*/
|
|
1515
|
+
private handleRowClick(eventArg: MouseEvent, item: T, rowIndex: number, view: T[]) {
|
|
1516
|
+
const id = this.getRowId(item);
|
|
1517
|
+
|
|
1518
|
+
if (this.selectionMode === 'single') {
|
|
1519
|
+
this.selectedDataRow = item;
|
|
1520
|
+
this.selectedIds.clear();
|
|
1521
|
+
this.selectedIds.add(id);
|
|
1522
|
+
this.__selectionAnchorId = id;
|
|
1523
|
+
this.emitSelectionChange();
|
|
1524
|
+
this.requestUpdate();
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
// multi
|
|
1529
|
+
const isToggle = eventArg.metaKey || eventArg.ctrlKey;
|
|
1530
|
+
const isRange = eventArg.shiftKey;
|
|
1531
|
+
|
|
1532
|
+
if (isRange && this.__selectionAnchorId !== undefined) {
|
|
1533
|
+
// Clear any text selection the browser may have created.
|
|
1534
|
+
window.getSelection?.()?.removeAllRanges();
|
|
1535
|
+
const anchorIdx = view.findIndex((r) => this.getRowId(r) === this.__selectionAnchorId);
|
|
1536
|
+
if (anchorIdx >= 0) {
|
|
1537
|
+
const [a, b] = anchorIdx <= rowIndex ? [anchorIdx, rowIndex] : [rowIndex, anchorIdx];
|
|
1538
|
+
this.selectedIds.clear();
|
|
1539
|
+
for (let i = a; i <= b; i++) this.selectedIds.add(this.getRowId(view[i]));
|
|
1540
|
+
} else {
|
|
1541
|
+
// Anchor no longer in view (filter changed, etc.) — fall back to single select.
|
|
1542
|
+
this.selectedIds.clear();
|
|
1543
|
+
this.selectedIds.add(id);
|
|
1544
|
+
this.__selectionAnchorId = id;
|
|
1545
|
+
}
|
|
1546
|
+
this.selectedDataRow = item;
|
|
1547
|
+
} else if (isToggle) {
|
|
1548
|
+
const wasSelected = this.selectedIds.has(id);
|
|
1549
|
+
if (wasSelected) {
|
|
1550
|
+
this.selectedIds.delete(id);
|
|
1551
|
+
// If we just deselected the focused row, move focus to another
|
|
1552
|
+
// selected row (or clear it) so the highlight goes away.
|
|
1553
|
+
if (this.selectedDataRow === item) {
|
|
1554
|
+
const remaining = view.find((r) => this.selectedIds.has(this.getRowId(r)));
|
|
1555
|
+
this.selectedDataRow = remaining as T;
|
|
1556
|
+
}
|
|
1557
|
+
} else {
|
|
1558
|
+
this.selectedIds.add(id);
|
|
1559
|
+
this.selectedDataRow = item;
|
|
1560
|
+
}
|
|
1561
|
+
this.__selectionAnchorId = id;
|
|
1562
|
+
} else {
|
|
1563
|
+
this.selectedDataRow = item;
|
|
1564
|
+
this.selectedIds.clear();
|
|
1565
|
+
this.selectedIds.add(id);
|
|
1566
|
+
this.__selectionAnchorId = id;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
this.emitSelectionChange();
|
|
1570
|
+
this.requestUpdate();
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1305
1573
|
private setRowSelected(row: T, checked: boolean) {
|
|
1306
1574
|
const id = this.getRowId(row);
|
|
1307
1575
|
if (this.selectionMode === 'single') {
|
|
@@ -1365,43 +1633,216 @@ export class DeesTable<T> extends DeesElement {
|
|
|
1365
1633
|
return actions;
|
|
1366
1634
|
}
|
|
1367
1635
|
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
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;
|
|
1383
1696
|
}
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
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>`;
|
|
1387
1744
|
}
|
|
1388
|
-
input.remove();
|
|
1389
|
-
target.style.color = originalColor;
|
|
1390
|
-
this.requestUpdate();
|
|
1391
|
-
};
|
|
1392
1745
|
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
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;
|
|
1400
1826
|
}
|
|
1401
|
-
|
|
1827
|
+
while (colIdx < 0) {
|
|
1828
|
+
colIdx += editableCols.length;
|
|
1829
|
+
rowIdx -= 1;
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
if (dy !== 0) rowIdx += dy;
|
|
1402
1833
|
|
|
1403
|
-
//
|
|
1404
|
-
|
|
1405
|
-
|
|
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
|
+
}
|
|
1406
1847
|
}
|
|
1407
1848
|
}
|