@design.estate/dees-catalog 3.63.0 → 3.64.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 +167 -38
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/00group-dataview/dees-table/dees-table.d.ts +37 -0
- package/dist_ts_web/elements/00group-dataview/dees-table/dees-table.js +203 -26
- package/dist_ts_web/elements/00group-dataview/dees-table/styles.js +2 -1
- package/dist_watch/bundle.js +165 -36
- 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.ts +189 -30
- package/ts_web/elements/00group-dataview/dees-table/styles.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@design.estate/dees-catalog",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.64.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.
|
|
6
|
+
version: '3.64.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
|
}
|
|
@@ -184,6 +184,14 @@ export class DeesTable<T> extends DeesElement {
|
|
|
184
184
|
accessor columnFilters: Record<string, string> = {};
|
|
185
185
|
@property({ type: Boolean, attribute: 'show-column-filters' })
|
|
186
186
|
accessor showColumnFilters: boolean = false;
|
|
187
|
+
/**
|
|
188
|
+
* When true, the table renders a leftmost checkbox column for click-driven
|
|
189
|
+
* (de)selection. Row selection by mouse (plain/shift/ctrl click) is always
|
|
190
|
+
* available regardless of this flag.
|
|
191
|
+
*/
|
|
192
|
+
@property({ type: Boolean, reflect: true, attribute: 'show-selection-checkbox' })
|
|
193
|
+
accessor showSelectionCheckbox: boolean = false;
|
|
194
|
+
|
|
187
195
|
/**
|
|
188
196
|
* When set, the table renders inside a fixed-height scroll container
|
|
189
197
|
* (`max-height: var(--table-max-height, 360px)`) and the header sticks
|
|
@@ -209,9 +217,72 @@ export class DeesTable<T> extends DeesElement {
|
|
|
209
217
|
accessor selectedIds: Set<string> = new Set();
|
|
210
218
|
private _rowIdMap = new WeakMap<object, string>();
|
|
211
219
|
private _rowIdCounter = 0;
|
|
220
|
+
/**
|
|
221
|
+
* Anchor row id for shift+click range selection. Set whenever the user
|
|
222
|
+
* makes a non-range click (plain or cmd/ctrl) so the next shift+click
|
|
223
|
+
* can compute a contiguous range from this anchor.
|
|
224
|
+
*/
|
|
225
|
+
private __selectionAnchorId?: string;
|
|
212
226
|
|
|
213
227
|
constructor() {
|
|
214
228
|
super();
|
|
229
|
+
// Make the host focusable so it can receive Ctrl/Cmd+C for copy.
|
|
230
|
+
if (!this.hasAttribute('tabindex')) this.setAttribute('tabindex', '0');
|
|
231
|
+
this.addEventListener('keydown', this.__handleHostKeydown);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Ctrl/Cmd+C copies the currently selected rows as a JSON array. Falls
|
|
236
|
+
* back to copying the focused-row (`selectedDataRow`) if no multi
|
|
237
|
+
* selection exists. No-op if a focused input/textarea would normally
|
|
238
|
+
* receive the copy.
|
|
239
|
+
*/
|
|
240
|
+
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.
|
|
244
|
+
const path = (eventArg.composedPath?.() || []) as EventTarget[];
|
|
245
|
+
for (const t of path) {
|
|
246
|
+
const tag = (t as HTMLElement)?.tagName;
|
|
247
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA') return;
|
|
248
|
+
if ((t as HTMLElement)?.isContentEditable) return;
|
|
249
|
+
}
|
|
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);
|
|
255
|
+
}
|
|
256
|
+
if (rows.length === 0) return;
|
|
257
|
+
eventArg.preventDefault();
|
|
258
|
+
this.__writeRowsAsJson(rows);
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Copies the current selection as a JSON array. If `fallbackRow` is given
|
|
263
|
+
* and there is no multi-selection, that row is copied instead. Used both
|
|
264
|
+
* by the Ctrl/Cmd+C handler and by the default context-menu action.
|
|
265
|
+
*/
|
|
266
|
+
public copySelectionAsJson(fallbackRow?: T) {
|
|
267
|
+
const rows: T[] = [];
|
|
268
|
+
if (this.selectedIds.size > 0) {
|
|
269
|
+
for (const r of this.data) if (this.selectedIds.has(this.getRowId(r))) rows.push(r);
|
|
270
|
+
} else if (fallbackRow) {
|
|
271
|
+
rows.push(fallbackRow);
|
|
272
|
+
} else if (this.selectedDataRow) {
|
|
273
|
+
rows.push(this.selectedDataRow);
|
|
274
|
+
}
|
|
275
|
+
if (rows.length === 0) return;
|
|
276
|
+
this.__writeRowsAsJson(rows);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private __writeRowsAsJson(rows: T[]) {
|
|
280
|
+
try {
|
|
281
|
+
const json = JSON.stringify(rows, null, 2);
|
|
282
|
+
navigator.clipboard?.writeText(json);
|
|
283
|
+
} catch {
|
|
284
|
+
/* ignore — clipboard may be unavailable */
|
|
285
|
+
}
|
|
215
286
|
}
|
|
216
287
|
|
|
217
288
|
public static styles = tableStyles;
|
|
@@ -319,15 +390,11 @@ export class DeesTable<T> extends DeesElement {
|
|
|
319
390
|
};
|
|
320
391
|
return html`
|
|
321
392
|
<tr
|
|
322
|
-
@click=${() =>
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
this.selectedIds.add(id);
|
|
328
|
-
this.emitSelectionChange();
|
|
329
|
-
this.requestUpdate();
|
|
330
|
-
}
|
|
393
|
+
@click=${(e: MouseEvent) => this.handleRowClick(e, itemArg, rowIndex, viewData)}
|
|
394
|
+
@mousedown=${(e: MouseEvent) => {
|
|
395
|
+
// Prevent the browser's native shift-click text
|
|
396
|
+
// selection so range-select doesn't highlight text.
|
|
397
|
+
if (e.shiftKey && this.selectionMode !== 'single') e.preventDefault();
|
|
331
398
|
}}
|
|
332
399
|
@dragenter=${async (eventArg: DragEvent) => {
|
|
333
400
|
eventArg.preventDefault();
|
|
@@ -362,27 +429,51 @@ export class DeesTable<T> extends DeesElement {
|
|
|
362
429
|
}
|
|
363
430
|
}}
|
|
364
431
|
@contextmenu=${async (eventArg: MouseEvent) => {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
432
|
+
// If the right-clicked row isn't part of the
|
|
433
|
+
// current selection, treat it like a plain click
|
|
434
|
+
// first so the context menu acts on a sensible
|
|
435
|
+
// selection (matches file-manager behavior).
|
|
436
|
+
if (!this.isRowSelected(itemArg)) {
|
|
437
|
+
this.selectedDataRow = itemArg;
|
|
438
|
+
this.selectedIds.clear();
|
|
439
|
+
this.selectedIds.add(this.getRowId(itemArg));
|
|
440
|
+
this.__selectionAnchorId = this.getRowId(itemArg);
|
|
441
|
+
this.emitSelectionChange();
|
|
442
|
+
this.requestUpdate();
|
|
443
|
+
}
|
|
444
|
+
const userItems: plugins.tsclass.website.IMenuItem[] =
|
|
445
|
+
this.getActionsForType('contextmenu').map((action) => ({
|
|
446
|
+
name: action.name,
|
|
447
|
+
iconName: action.iconName as any,
|
|
448
|
+
action: async () => {
|
|
449
|
+
await action.actionFunc({
|
|
450
|
+
item: itemArg,
|
|
451
|
+
table: this,
|
|
452
|
+
});
|
|
453
|
+
return null;
|
|
454
|
+
},
|
|
455
|
+
}));
|
|
456
|
+
const defaultItems: plugins.tsclass.website.IMenuItem[] = [
|
|
457
|
+
{
|
|
458
|
+
name:
|
|
459
|
+
this.selectedIds.size > 1
|
|
460
|
+
? `Copy ${this.selectedIds.size} rows as JSON`
|
|
461
|
+
: 'Copy row as JSON',
|
|
462
|
+
iconName: 'lucide:Copy' as any,
|
|
463
|
+
action: async () => {
|
|
464
|
+
this.copySelectionAsJson(itemArg);
|
|
465
|
+
return null;
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
];
|
|
469
|
+
DeesContextmenu.openContextMenuWithOptions(eventArg, [
|
|
470
|
+
...userItems,
|
|
471
|
+
...defaultItems,
|
|
472
|
+
]);
|
|
382
473
|
}}
|
|
383
|
-
class="${itemArg === this.selectedDataRow ? 'selected' : ''}"
|
|
474
|
+
class="${itemArg === this.selectedDataRow || this.isRowSelected(itemArg) ? 'selected' : ''}"
|
|
384
475
|
>
|
|
385
|
-
${this.
|
|
476
|
+
${this.showSelectionCheckbox
|
|
386
477
|
? html`<td style="width:42px; text-align:center;">
|
|
387
478
|
<dees-input-checkbox
|
|
388
479
|
.value=${this.isRowSelected(itemArg)}
|
|
@@ -502,7 +593,7 @@ export class DeesTable<T> extends DeesElement {
|
|
|
502
593
|
private renderHeaderRows(effectiveColumns: Column<T>[]): TemplateResult {
|
|
503
594
|
return html`
|
|
504
595
|
<tr>
|
|
505
|
-
${this.
|
|
596
|
+
${this.showSelectionCheckbox
|
|
506
597
|
? html`
|
|
507
598
|
<th style="width:42px; text-align:center;">
|
|
508
599
|
${this.selectionMode === 'multi'
|
|
@@ -547,7 +638,7 @@ export class DeesTable<T> extends DeesElement {
|
|
|
547
638
|
</tr>
|
|
548
639
|
${this.showColumnFilters
|
|
549
640
|
? html`<tr class="filtersRow">
|
|
550
|
-
${this.
|
|
641
|
+
${this.showSelectionCheckbox
|
|
551
642
|
? html`<th style="width:42px;"></th>`
|
|
552
643
|
: html``}
|
|
553
644
|
${effectiveColumns
|
|
@@ -1302,6 +1393,74 @@ export class DeesTable<T> extends DeesElement {
|
|
|
1302
1393
|
this.requestUpdate();
|
|
1303
1394
|
}
|
|
1304
1395
|
|
|
1396
|
+
/**
|
|
1397
|
+
* Handles row clicks with file-manager style selection semantics:
|
|
1398
|
+
* - plain click: select only this row, set anchor
|
|
1399
|
+
* - cmd/ctrl+click: toggle this row in/out, set anchor
|
|
1400
|
+
* - shift+click: select the contiguous range from the anchor to this row
|
|
1401
|
+
*
|
|
1402
|
+
* Multi-row click selection is always available (`selectionMode === 'none'`
|
|
1403
|
+
* and `'multi'` both behave this way) so consumers can always copy a set
|
|
1404
|
+
* of rows. Only `selectionMode === 'single'` restricts to one row.
|
|
1405
|
+
*/
|
|
1406
|
+
private handleRowClick(eventArg: MouseEvent, item: T, rowIndex: number, view: T[]) {
|
|
1407
|
+
const id = this.getRowId(item);
|
|
1408
|
+
|
|
1409
|
+
if (this.selectionMode === 'single') {
|
|
1410
|
+
this.selectedDataRow = item;
|
|
1411
|
+
this.selectedIds.clear();
|
|
1412
|
+
this.selectedIds.add(id);
|
|
1413
|
+
this.__selectionAnchorId = id;
|
|
1414
|
+
this.emitSelectionChange();
|
|
1415
|
+
this.requestUpdate();
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// multi
|
|
1420
|
+
const isToggle = eventArg.metaKey || eventArg.ctrlKey;
|
|
1421
|
+
const isRange = eventArg.shiftKey;
|
|
1422
|
+
|
|
1423
|
+
if (isRange && this.__selectionAnchorId !== undefined) {
|
|
1424
|
+
// Clear any text selection the browser may have created.
|
|
1425
|
+
window.getSelection?.()?.removeAllRanges();
|
|
1426
|
+
const anchorIdx = view.findIndex((r) => this.getRowId(r) === this.__selectionAnchorId);
|
|
1427
|
+
if (anchorIdx >= 0) {
|
|
1428
|
+
const [a, b] = anchorIdx <= rowIndex ? [anchorIdx, rowIndex] : [rowIndex, anchorIdx];
|
|
1429
|
+
this.selectedIds.clear();
|
|
1430
|
+
for (let i = a; i <= b; i++) this.selectedIds.add(this.getRowId(view[i]));
|
|
1431
|
+
} else {
|
|
1432
|
+
// Anchor no longer in view (filter changed, etc.) — fall back to single select.
|
|
1433
|
+
this.selectedIds.clear();
|
|
1434
|
+
this.selectedIds.add(id);
|
|
1435
|
+
this.__selectionAnchorId = id;
|
|
1436
|
+
}
|
|
1437
|
+
this.selectedDataRow = item;
|
|
1438
|
+
} else if (isToggle) {
|
|
1439
|
+
const wasSelected = this.selectedIds.has(id);
|
|
1440
|
+
if (wasSelected) {
|
|
1441
|
+
this.selectedIds.delete(id);
|
|
1442
|
+
// If we just deselected the focused row, move focus to another
|
|
1443
|
+
// selected row (or clear it) so the highlight goes away.
|
|
1444
|
+
if (this.selectedDataRow === item) {
|
|
1445
|
+
const remaining = view.find((r) => this.selectedIds.has(this.getRowId(r)));
|
|
1446
|
+
this.selectedDataRow = remaining as T;
|
|
1447
|
+
}
|
|
1448
|
+
} else {
|
|
1449
|
+
this.selectedIds.add(id);
|
|
1450
|
+
this.selectedDataRow = item;
|
|
1451
|
+
}
|
|
1452
|
+
this.__selectionAnchorId = id;
|
|
1453
|
+
} else {
|
|
1454
|
+
this.selectedDataRow = item;
|
|
1455
|
+
this.selectedIds.clear();
|
|
1456
|
+
this.selectedIds.add(id);
|
|
1457
|
+
this.__selectionAnchorId = id;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
this.emitSelectionChange();
|
|
1461
|
+
this.requestUpdate();
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1305
1464
|
private setRowSelected(row: T, checked: boolean) {
|
|
1306
1465
|
const id = this.getRowId(row);
|
|
1307
1466
|
if (this.selectionMode === 'single') {
|