@design.estate/dees-catalog 3.61.2 → 3.62.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 +404 -59
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/00group-dataview/dees-table/data.d.ts +8 -2
- package/dist_ts_web/elements/00group-dataview/dees-table/data.js +40 -22
- package/dist_ts_web/elements/00group-dataview/dees-table/dees-table.d.ts +64 -5
- package/dist_ts_web/elements/00group-dataview/dees-table/dees-table.demo.js +39 -1
- package/dist_ts_web/elements/00group-dataview/dees-table/dees-table.js +332 -42
- package/dist_ts_web/elements/00group-dataview/dees-table/styles.js +27 -1
- package/dist_ts_web/elements/00group-dataview/dees-table/types.d.ts +8 -0
- package/dist_watch/bundle.js +402 -57
- 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/data.ts +33 -17
- package/ts_web/elements/00group-dataview/dees-table/dees-table.demo.ts +38 -0
- package/ts_web/elements/00group-dataview/dees-table/dees-table.ts +353 -27
- package/ts_web/elements/00group-dataview/dees-table/styles.ts +26 -0
- package/ts_web/elements/00group-dataview/dees-table/types.ts +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@design.estate/dees-catalog",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.62.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.62.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
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Column, TDisplayFunction } from './types.js';
|
|
1
|
+
import type { Column, ISortDescriptor, TDisplayFunction } from './types.js';
|
|
2
2
|
|
|
3
3
|
export function computeColumnsFromDisplayFunction<T>(
|
|
4
4
|
displayFunction: TDisplayFunction<T>,
|
|
@@ -36,11 +36,31 @@ export function getCellValue<T>(row: T, col: Column<T>, displayFunction?: TDispl
|
|
|
36
36
|
return col.value ? col.value(row) : (row as any)[col.key as any];
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Compares two cell values in ascending order. Returns -1, 0, or 1.
|
|
41
|
+
* Null/undefined values sort before defined values. Numbers compare numerically;
|
|
42
|
+
* everything else compares as case-insensitive strings.
|
|
43
|
+
*/
|
|
44
|
+
export function compareCellValues(va: any, vb: any): number {
|
|
45
|
+
if (va == null && vb == null) return 0;
|
|
46
|
+
if (va == null) return -1;
|
|
47
|
+
if (vb == null) return 1;
|
|
48
|
+
if (typeof va === 'number' && typeof vb === 'number') {
|
|
49
|
+
if (va < vb) return -1;
|
|
50
|
+
if (va > vb) return 1;
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
const sa = String(va).toLowerCase();
|
|
54
|
+
const sb = String(vb).toLowerCase();
|
|
55
|
+
if (sa < sb) return -1;
|
|
56
|
+
if (sa > sb) return 1;
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
39
60
|
export function getViewData<T>(
|
|
40
61
|
data: T[],
|
|
41
62
|
effectiveColumns: Column<T>[],
|
|
42
|
-
|
|
43
|
-
sortDir?: 'asc' | 'desc' | null,
|
|
63
|
+
sortBy: ISortDescriptor[],
|
|
44
64
|
filterText?: string,
|
|
45
65
|
columnFilters?: Record<string, string>,
|
|
46
66
|
filterMode: 'table' | 'data' = 'table',
|
|
@@ -94,21 +114,17 @@ export function getViewData<T>(
|
|
|
94
114
|
return true;
|
|
95
115
|
});
|
|
96
116
|
}
|
|
97
|
-
if (!
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
117
|
+
if (!sortBy || sortBy.length === 0) return arr;
|
|
118
|
+
// Pre-resolve descriptors -> columns once for performance.
|
|
119
|
+
const resolved = sortBy
|
|
120
|
+
.map((desc) => ({ desc, col: effectiveColumns.find((c) => String(c.key) === desc.key) }))
|
|
121
|
+
.filter((entry): entry is { desc: ISortDescriptor; col: Column<T> } => !!entry.col);
|
|
122
|
+
if (resolved.length === 0) return arr;
|
|
101
123
|
arr.sort((a, b) => {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (vb == null) return 1 * dir;
|
|
107
|
-
if (typeof va === 'number' && typeof vb === 'number') return (va - vb) * dir;
|
|
108
|
-
const sa = String(va).toLowerCase();
|
|
109
|
-
const sb = String(vb).toLowerCase();
|
|
110
|
-
if (sa < sb) return -1 * dir;
|
|
111
|
-
if (sa > sb) return 1 * dir;
|
|
124
|
+
for (const { desc, col } of resolved) {
|
|
125
|
+
const cmp = compareCellValues(getCellValue(a, col), getCellValue(b, col));
|
|
126
|
+
if (cmp !== 0) return desc.dir === 'asc' ? cmp : -cmp;
|
|
127
|
+
}
|
|
112
128
|
return 0;
|
|
113
129
|
});
|
|
114
130
|
return arr;
|
|
@@ -580,6 +580,44 @@ export const demoFunc = () => html`
|
|
|
580
580
|
></dees-table>
|
|
581
581
|
</div>
|
|
582
582
|
|
|
583
|
+
<div class="demo-section">
|
|
584
|
+
<h2 class="demo-title">Multi-Column Sort</h2>
|
|
585
|
+
<p class="demo-description">
|
|
586
|
+
Click any column header for a single-column sort. Hold Shift while clicking to add the
|
|
587
|
+
column to a multi-sort cascade (or cycle its direction). Right-click any sortable header
|
|
588
|
+
to open a menu where you can pin a column to a specific priority slot, remove it, or
|
|
589
|
+
clear the cascade.
|
|
590
|
+
</p>
|
|
591
|
+
<dees-table
|
|
592
|
+
heading1="People Directory"
|
|
593
|
+
heading2="Pre-seeded with department ▲ 1, name ▲ 2"
|
|
594
|
+
.sortBy=${[
|
|
595
|
+
{ key: 'department', dir: 'asc' },
|
|
596
|
+
{ key: 'name', dir: 'asc' },
|
|
597
|
+
]}
|
|
598
|
+
.columns=${[
|
|
599
|
+
{ key: 'department', header: 'Department', sortable: true },
|
|
600
|
+
{ key: 'name', header: 'Name', sortable: true },
|
|
601
|
+
{ key: 'role', header: 'Role', sortable: true },
|
|
602
|
+
{ key: 'createdAt', header: 'Created', sortable: true },
|
|
603
|
+
{ key: 'location', header: 'Location', sortable: true },
|
|
604
|
+
{ key: 'status', header: 'Status', sortable: true },
|
|
605
|
+
]}
|
|
606
|
+
.data=${[
|
|
607
|
+
{ department: 'R&D', name: 'Alice Johnson', role: 'Engineer', createdAt: '2023-01-12', location: 'Berlin', status: 'Active' },
|
|
608
|
+
{ department: 'R&D', name: 'Diana Martinez', role: 'Engineer', createdAt: '2020-06-30', location: 'Madrid', status: 'Active' },
|
|
609
|
+
{ department: 'R&D', name: 'Mark Lee', role: 'Engineer', createdAt: '2024-03-04', location: 'Berlin', status: 'Active' },
|
|
610
|
+
{ department: 'Design', name: 'Bob Smith', role: 'Designer', createdAt: '2022-11-05', location: 'Paris', status: 'Active' },
|
|
611
|
+
{ department: 'Design', name: 'Sara Kim', role: 'Designer', createdAt: '2021-08-19', location: 'Paris', status: 'On Leave' },
|
|
612
|
+
{ department: 'Ops', name: 'Charlie Davis', role: 'Manager', createdAt: '2021-04-21', location: 'London', status: 'On Leave' },
|
|
613
|
+
{ department: 'Ops', name: 'Helena Voss', role: 'SRE', createdAt: '2023-07-22', location: 'London', status: 'Active' },
|
|
614
|
+
{ department: 'QA', name: 'Fiona Clark', role: 'QA', createdAt: '2022-03-14', location: 'Vienna', status: 'Active' },
|
|
615
|
+
{ department: 'QA', name: 'Tomás Rivera', role: 'QA', createdAt: '2024-01-09', location: 'Madrid', status: 'Active' },
|
|
616
|
+
{ department: 'CS', name: 'Ethan Brown', role: 'Support', createdAt: '2019-09-18', location: 'Rome', status: 'Inactive' },
|
|
617
|
+
]}
|
|
618
|
+
></dees-table>
|
|
619
|
+
</div>
|
|
620
|
+
|
|
583
621
|
<div class="demo-section">
|
|
584
622
|
<h2 class="demo-title">Wide Properties + Many Actions</h2>
|
|
585
623
|
<p class="demo-description">A table with many columns and rich actions to stress test layout and sticky Actions.</p>
|
|
@@ -3,10 +3,11 @@ import { demoFunc } from './dees-table.demo.js';
|
|
|
3
3
|
import { customElement, html, DeesElement, property, type TemplateResult, directives } from '@design.estate/dees-element';
|
|
4
4
|
|
|
5
5
|
import { DeesContextmenu } from '../../00group-overlay/dees-contextmenu/dees-contextmenu.js';
|
|
6
|
+
import { DeesModal } from '../../00group-overlay/dees-modal/dees-modal.js';
|
|
6
7
|
import * as domtools from '@design.estate/dees-domtools';
|
|
7
8
|
import { type TIconKey } from '../../00group-utility/dees-icon/dees-icon.js';
|
|
8
9
|
import { tableStyles } from './styles.js';
|
|
9
|
-
import type { Column, ITableAction, ITableActionDataArg, TDisplayFunction } from './types.js';
|
|
10
|
+
import type { Column, ISortDescriptor, ITableAction, ITableActionDataArg, TDisplayFunction } from './types.js';
|
|
10
11
|
import {
|
|
11
12
|
computeColumnsFromDisplayFunction as computeColumnsFromDisplayFunctionFn,
|
|
12
13
|
computeEffectiveColumns as computeEffectiveColumnsFn,
|
|
@@ -17,7 +18,14 @@ import { compileLucenePredicate } from './lucene.js';
|
|
|
17
18
|
import { themeDefaultStyles } from '../../00theme.js';
|
|
18
19
|
import '../../00group-layout/dees-tile/dees-tile.js';
|
|
19
20
|
|
|
20
|
-
export type { Column, ITableAction, ITableActionDataArg, TDisplayFunction } from './types.js';
|
|
21
|
+
export type { Column, ISortDescriptor, ITableAction, ITableActionDataArg, TDisplayFunction } from './types.js';
|
|
22
|
+
|
|
23
|
+
/** Returns the English ordinal label for a 1-based position (e.g. 1 → "1st"). */
|
|
24
|
+
function ordinalLabel(n: number): string {
|
|
25
|
+
const s = ['th', 'st', 'nd', 'rd'];
|
|
26
|
+
const v = n % 100;
|
|
27
|
+
return n + (s[(v - 20) % 10] || s[v] || s[0]);
|
|
28
|
+
}
|
|
21
29
|
|
|
22
30
|
declare global {
|
|
23
31
|
interface HTMLElementTagNameMap {
|
|
@@ -161,11 +169,12 @@ export class DeesTable<T> extends DeesElement {
|
|
|
161
169
|
|
|
162
170
|
public dataChangeSubject = new domtools.plugins.smartrx.rxjs.Subject();
|
|
163
171
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
172
|
+
/**
|
|
173
|
+
* Multi-column sort cascade. The first entry is the primary sort key,
|
|
174
|
+
* subsequent entries are tiebreakers in priority order.
|
|
175
|
+
*/
|
|
167
176
|
@property({ attribute: false })
|
|
168
|
-
accessor
|
|
177
|
+
accessor sortBy: ISortDescriptor[] = [];
|
|
169
178
|
|
|
170
179
|
// simple client-side filtering (Phase 1)
|
|
171
180
|
@property({ type: String })
|
|
@@ -213,8 +222,7 @@ export class DeesTable<T> extends DeesElement {
|
|
|
213
222
|
const viewData = getViewDataFn(
|
|
214
223
|
this.data,
|
|
215
224
|
effectiveColumns,
|
|
216
|
-
this.
|
|
217
|
-
this.sortDir,
|
|
225
|
+
this.sortBy,
|
|
218
226
|
this.filterText,
|
|
219
227
|
this.columnFilters,
|
|
220
228
|
this.searchMode === 'data' ? 'data' : 'table',
|
|
@@ -318,7 +326,12 @@ export class DeesTable<T> extends DeesElement {
|
|
|
318
326
|
role="columnheader"
|
|
319
327
|
aria-sort=${ariaSort}
|
|
320
328
|
style="${isSortable ? 'cursor: pointer;' : ''}"
|
|
321
|
-
@click=${() =>
|
|
329
|
+
@click=${(eventArg: MouseEvent) =>
|
|
330
|
+
isSortable ? this.handleHeaderClick(eventArg, col, effectiveColumns) : null}
|
|
331
|
+
@contextmenu=${(eventArg: MouseEvent) =>
|
|
332
|
+
isSortable
|
|
333
|
+
? this.openHeaderContextMenu(eventArg, col, effectiveColumns)
|
|
334
|
+
: null}
|
|
322
335
|
>
|
|
323
336
|
${col.header ?? (col.key as any)}
|
|
324
337
|
${this.renderSortIndicator(col)}
|
|
@@ -651,35 +664,348 @@ export class DeesTable<T> extends DeesElement {
|
|
|
651
664
|
|
|
652
665
|
// compute helpers moved to ./data.ts
|
|
653
666
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
667
|
+
// ─── sort: public API ────────────────────────────────────────────────
|
|
668
|
+
|
|
669
|
+
/** Returns the descriptor for `key` if the column is currently in the cascade. */
|
|
670
|
+
public getSortDescriptor(key: string): ISortDescriptor | undefined {
|
|
671
|
+
return this.sortBy.find((d) => d.key === key);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/** Returns the 0-based priority of `key` in the cascade, or -1 if not present. */
|
|
675
|
+
public getSortPriority(key: string): number {
|
|
676
|
+
return this.sortBy.findIndex((d) => d.key === key);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/** Replaces the cascade with a single sort entry. */
|
|
680
|
+
public setSort(key: string, dir: 'asc' | 'desc'): void {
|
|
681
|
+
this.sortBy = [{ key, dir }];
|
|
682
|
+
this.emitSortChange();
|
|
683
|
+
this.requestUpdate();
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Inserts (or moves) `key` to a 0-based position in the cascade. If the key is
|
|
688
|
+
* already present elsewhere, its previous entry is removed before insertion so
|
|
689
|
+
* a column appears at most once.
|
|
690
|
+
*/
|
|
691
|
+
public addSortAt(key: string, position: number, dir: 'asc' | 'desc'): void {
|
|
692
|
+
const next = this.sortBy.filter((d) => d.key !== key);
|
|
693
|
+
const clamped = Math.max(0, Math.min(position, next.length));
|
|
694
|
+
next.splice(clamped, 0, { key, dir });
|
|
695
|
+
this.sortBy = next;
|
|
696
|
+
this.emitSortChange();
|
|
697
|
+
this.requestUpdate();
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/** Appends `key` to the end of the cascade (or moves it there if already present). */
|
|
701
|
+
public appendSort(key: string, dir: 'asc' | 'desc'): void {
|
|
702
|
+
const next = this.sortBy.filter((d) => d.key !== key);
|
|
703
|
+
next.push({ key, dir });
|
|
704
|
+
this.sortBy = next;
|
|
705
|
+
this.emitSortChange();
|
|
706
|
+
this.requestUpdate();
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/** Removes `key` from the cascade. No-op if not present. */
|
|
710
|
+
public removeSort(key: string): void {
|
|
711
|
+
if (!this.sortBy.some((d) => d.key === key)) return;
|
|
712
|
+
this.sortBy = this.sortBy.filter((d) => d.key !== key);
|
|
713
|
+
this.emitSortChange();
|
|
714
|
+
this.requestUpdate();
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/** Empties the cascade. */
|
|
718
|
+
public clearSorts(): void {
|
|
719
|
+
if (this.sortBy.length === 0) return;
|
|
720
|
+
this.sortBy = [];
|
|
721
|
+
this.emitSortChange();
|
|
722
|
+
this.requestUpdate();
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
private emitSortChange() {
|
|
666
726
|
this.dispatchEvent(
|
|
667
727
|
new CustomEvent('sortChange', {
|
|
668
|
-
detail: {
|
|
728
|
+
detail: { sortBy: this.sortBy.map((d) => ({ ...d })) },
|
|
669
729
|
bubbles: true,
|
|
670
730
|
})
|
|
671
731
|
);
|
|
672
|
-
this.requestUpdate();
|
|
673
732
|
}
|
|
674
733
|
|
|
734
|
+
// ─── sort: header interaction handlers ───────────────────────────────
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Plain left-click on a sortable header. Cycles `none → asc → desc → none`
|
|
738
|
+
* collapsing the cascade to a single column. If a multi-column cascade is
|
|
739
|
+
* active, asks the user to confirm the destructive replacement first. A
|
|
740
|
+
* Shift+click bypasses the modal and routes through the multi-sort cycle.
|
|
741
|
+
*/
|
|
742
|
+
private async handleHeaderClick(
|
|
743
|
+
eventArg: MouseEvent,
|
|
744
|
+
col: Column<T>,
|
|
745
|
+
_effectiveColumns: Column<T>[]
|
|
746
|
+
) {
|
|
747
|
+
if (eventArg.shiftKey) {
|
|
748
|
+
this.handleHeaderShiftClick(col);
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
const proceed = await this.confirmReplaceCascade(col);
|
|
752
|
+
if (!proceed) return;
|
|
753
|
+
this.cycleSingleSort(col);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Cycles a single column through `none → asc → desc → none`, collapsing the
|
|
758
|
+
* cascade. Used by both plain click and the menu's "Sort Ascending/Descending"
|
|
759
|
+
* shortcuts (after confirmation).
|
|
760
|
+
*/
|
|
761
|
+
private cycleSingleSort(col: Column<T>) {
|
|
762
|
+
const key = String(col.key);
|
|
763
|
+
const current = this.sortBy.length === 1 && this.sortBy[0].key === key ? this.sortBy[0].dir : null;
|
|
764
|
+
if (current === 'asc') this.setSort(key, 'desc');
|
|
765
|
+
else if (current === 'desc') this.clearSorts();
|
|
766
|
+
else this.setSort(key, 'asc');
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Shift+click cycle on a sortable header. Edits the cascade in place without
|
|
771
|
+
* destroying other sort keys: append → flip dir → remove.
|
|
772
|
+
*/
|
|
773
|
+
private handleHeaderShiftClick(col: Column<T>) {
|
|
774
|
+
const key = String(col.key);
|
|
775
|
+
const existing = this.getSortDescriptor(key);
|
|
776
|
+
if (!existing) {
|
|
777
|
+
this.appendSort(key, 'asc');
|
|
778
|
+
} else if (existing.dir === 'asc') {
|
|
779
|
+
this.sortBy = this.sortBy.map((d) => (d.key === key ? { key, dir: 'desc' } : d));
|
|
780
|
+
this.emitSortChange();
|
|
781
|
+
this.requestUpdate();
|
|
782
|
+
} else {
|
|
783
|
+
this.removeSort(key);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Opens a confirmation modal when the cascade has more than one entry and the
|
|
789
|
+
* user attempts a destructive single-sort replacement. Resolves to `true` if
|
|
790
|
+
* the user accepts, `false` if they cancel. If the cascade has 0 or 1 entries
|
|
791
|
+
* the modal is skipped and we resolve to `true` immediately.
|
|
792
|
+
*/
|
|
793
|
+
private confirmReplaceCascade(targetCol: Column<T>): Promise<boolean> {
|
|
794
|
+
if (this.sortBy.length <= 1) return Promise.resolve(true);
|
|
795
|
+
return new Promise((resolve) => {
|
|
796
|
+
let settled = false;
|
|
797
|
+
const settle = (result: boolean) => {
|
|
798
|
+
if (settled) return;
|
|
799
|
+
settled = true;
|
|
800
|
+
resolve(result);
|
|
801
|
+
};
|
|
802
|
+
const summary = this.sortBy
|
|
803
|
+
.map((d, i) => {
|
|
804
|
+
const c = (this as any)._lookupColumnByKey?.(d.key) as Column<T> | undefined;
|
|
805
|
+
const label = c?.header ?? d.key;
|
|
806
|
+
return html`<li>${i + 1}. ${label} ${d.dir === 'asc' ? '▲' : '▼'}</li>`;
|
|
807
|
+
});
|
|
808
|
+
DeesModal.createAndShow({
|
|
809
|
+
heading: 'Replace multi-column sort?',
|
|
810
|
+
width: 'small',
|
|
811
|
+
showCloseButton: true,
|
|
812
|
+
content: html`
|
|
813
|
+
<div style="font-size:13px; line-height:1.55;">
|
|
814
|
+
<p style="margin:0 0 8px;">
|
|
815
|
+
You currently have a ${this.sortBy.length}-column sort active:
|
|
816
|
+
</p>
|
|
817
|
+
<ul style="margin:0 0 12px; padding-left:18px;">${summary}</ul>
|
|
818
|
+
<p style="margin:0;">
|
|
819
|
+
Continuing will discard the cascade and replace it with a single sort on
|
|
820
|
+
<strong>${targetCol.header ?? String(targetCol.key)}</strong>.
|
|
821
|
+
</p>
|
|
822
|
+
</div>
|
|
823
|
+
`,
|
|
824
|
+
menuOptions: [
|
|
825
|
+
{
|
|
826
|
+
name: 'Cancel',
|
|
827
|
+
iconName: 'lucide:x',
|
|
828
|
+
action: async (modal) => {
|
|
829
|
+
settle(false);
|
|
830
|
+
await modal!.destroy();
|
|
831
|
+
return null;
|
|
832
|
+
},
|
|
833
|
+
},
|
|
834
|
+
{
|
|
835
|
+
name: 'Replace',
|
|
836
|
+
iconName: 'lucide:check',
|
|
837
|
+
action: async (modal) => {
|
|
838
|
+
settle(true);
|
|
839
|
+
await modal!.destroy();
|
|
840
|
+
return null;
|
|
841
|
+
},
|
|
842
|
+
},
|
|
843
|
+
],
|
|
844
|
+
});
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Looks up a column by its string key in the currently effective column set.
|
|
850
|
+
* Used by the modal helper to render human-friendly labels.
|
|
851
|
+
*/
|
|
852
|
+
private _lookupColumnByKey(key: string): Column<T> | undefined {
|
|
853
|
+
const usingColumns = Array.isArray(this.columns) && this.columns.length > 0;
|
|
854
|
+
const effective = usingColumns
|
|
855
|
+
? computeEffectiveColumnsFn(this.columns, this.augmentFromDisplayFunction, this.displayFunction, this.data)
|
|
856
|
+
: computeColumnsFromDisplayFunctionFn(this.displayFunction, this.data);
|
|
857
|
+
return effective.find((c) => String(c.key) === key);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Opens the header context menu for explicit multi-sort priority control.
|
|
862
|
+
*/
|
|
863
|
+
private openHeaderContextMenu(
|
|
864
|
+
eventArg: MouseEvent,
|
|
865
|
+
col: Column<T>,
|
|
866
|
+
effectiveColumns: Column<T>[]
|
|
867
|
+
) {
|
|
868
|
+
const items = this.buildHeaderMenuItems(col, effectiveColumns);
|
|
869
|
+
DeesContextmenu.openContextMenuWithOptions(eventArg, items as any);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Builds the dynamic context-menu structure for a single column header.
|
|
874
|
+
*/
|
|
875
|
+
private buildHeaderMenuItems(col: Column<T>, effectiveColumns: Column<T>[]) {
|
|
876
|
+
const key = String(col.key);
|
|
877
|
+
const existing = this.getSortDescriptor(key);
|
|
878
|
+
const cascadeLen = this.sortBy.length;
|
|
879
|
+
// Maximum exposed slot: one beyond the current cascade, capped at the
|
|
880
|
+
// number of sortable columns. If the column is already in the cascade we
|
|
881
|
+
// never need to grow the slot count.
|
|
882
|
+
const sortableColumnCount = effectiveColumns.filter((c) => !!c.sortable).length;
|
|
883
|
+
const maxSlot = Math.min(
|
|
884
|
+
Math.max(cascadeLen + (existing ? 0 : 1), 1),
|
|
885
|
+
Math.max(sortableColumnCount, 1)
|
|
886
|
+
);
|
|
887
|
+
|
|
888
|
+
const items: any[] = [];
|
|
889
|
+
|
|
890
|
+
// Single-sort shortcuts. These are destructive when a cascade is active, so
|
|
891
|
+
// they go through confirmReplaceCascade just like a plain click.
|
|
892
|
+
items.push({
|
|
893
|
+
name: 'Sort Ascending',
|
|
894
|
+
iconName: cascadeLen === 1 && existing?.dir === 'asc' ? 'lucide:check' : 'lucide:arrowUp',
|
|
895
|
+
action: async () => {
|
|
896
|
+
if (await this.confirmReplaceCascade(col)) this.setSort(key, 'asc');
|
|
897
|
+
return null;
|
|
898
|
+
},
|
|
899
|
+
});
|
|
900
|
+
items.push({
|
|
901
|
+
name: 'Sort Descending',
|
|
902
|
+
iconName: cascadeLen === 1 && existing?.dir === 'desc' ? 'lucide:check' : 'lucide:arrowDown',
|
|
903
|
+
action: async () => {
|
|
904
|
+
if (await this.confirmReplaceCascade(col)) this.setSort(key, 'desc');
|
|
905
|
+
return null;
|
|
906
|
+
},
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
items.push({ divider: true });
|
|
910
|
+
|
|
911
|
+
// Priority slot entries (1..maxSlot). Each slot has an asc/desc submenu.
|
|
912
|
+
for (let slot = 1; slot <= maxSlot; slot++) {
|
|
913
|
+
const ordinal = ordinalLabel(slot);
|
|
914
|
+
const isCurrentSlot = existing && this.getSortPriority(key) === slot - 1;
|
|
915
|
+
items.push({
|
|
916
|
+
name: `Set as ${ordinal} sort`,
|
|
917
|
+
iconName: isCurrentSlot ? 'lucide:check' : 'lucide:listOrdered',
|
|
918
|
+
submenu: [
|
|
919
|
+
{
|
|
920
|
+
name: 'Ascending',
|
|
921
|
+
iconName: 'lucide:arrowUp',
|
|
922
|
+
action: async () => {
|
|
923
|
+
this.addSortAt(key, slot - 1, 'asc');
|
|
924
|
+
return null;
|
|
925
|
+
},
|
|
926
|
+
},
|
|
927
|
+
{
|
|
928
|
+
name: 'Descending',
|
|
929
|
+
iconName: 'lucide:arrowDown',
|
|
930
|
+
action: async () => {
|
|
931
|
+
this.addSortAt(key, slot - 1, 'desc');
|
|
932
|
+
return null;
|
|
933
|
+
},
|
|
934
|
+
},
|
|
935
|
+
],
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
items.push({ divider: true });
|
|
940
|
+
|
|
941
|
+
items.push({
|
|
942
|
+
name: 'Append to sort',
|
|
943
|
+
iconName: 'lucide:plus',
|
|
944
|
+
submenu: [
|
|
945
|
+
{
|
|
946
|
+
name: 'Ascending',
|
|
947
|
+
iconName: 'lucide:arrowUp',
|
|
948
|
+
action: async () => {
|
|
949
|
+
this.appendSort(key, 'asc');
|
|
950
|
+
return null;
|
|
951
|
+
},
|
|
952
|
+
},
|
|
953
|
+
{
|
|
954
|
+
name: 'Descending',
|
|
955
|
+
iconName: 'lucide:arrowDown',
|
|
956
|
+
action: async () => {
|
|
957
|
+
this.appendSort(key, 'desc');
|
|
958
|
+
return null;
|
|
959
|
+
},
|
|
960
|
+
},
|
|
961
|
+
],
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
if (existing) {
|
|
965
|
+
items.push({ divider: true });
|
|
966
|
+
items.push({
|
|
967
|
+
name: 'Remove from sort',
|
|
968
|
+
iconName: 'lucide:minus',
|
|
969
|
+
action: async () => {
|
|
970
|
+
this.removeSort(key);
|
|
971
|
+
return null;
|
|
972
|
+
},
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
if (cascadeLen > 0) {
|
|
977
|
+
if (!existing) items.push({ divider: true });
|
|
978
|
+
items.push({
|
|
979
|
+
name: 'Clear all sorts',
|
|
980
|
+
iconName: 'lucide:trash',
|
|
981
|
+
action: async () => {
|
|
982
|
+
this.clearSorts();
|
|
983
|
+
return null;
|
|
984
|
+
},
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
return items;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// ─── sort: indicator + ARIA ──────────────────────────────────────────
|
|
992
|
+
|
|
675
993
|
private getAriaSort(col: Column<T>): 'none' | 'ascending' | 'descending' {
|
|
676
|
-
|
|
677
|
-
|
|
994
|
+
// ARIA sort reflects only the primary sort key (standard grid pattern).
|
|
995
|
+
const primary = this.sortBy[0];
|
|
996
|
+
if (!primary || primary.key !== String(col.key)) return 'none';
|
|
997
|
+
return primary.dir === 'asc' ? 'ascending' : 'descending';
|
|
678
998
|
}
|
|
679
999
|
|
|
680
1000
|
private renderSortIndicator(col: Column<T>) {
|
|
681
|
-
|
|
682
|
-
|
|
1001
|
+
const idx = this.getSortPriority(String(col.key));
|
|
1002
|
+
if (idx < 0) return html``;
|
|
1003
|
+
const desc = this.sortBy[idx];
|
|
1004
|
+
const arrow = desc.dir === 'asc' ? '▲' : '▼';
|
|
1005
|
+
if (this.sortBy.length === 1) {
|
|
1006
|
+
return html`<span class="sortArrow">${arrow}</span>`;
|
|
1007
|
+
}
|
|
1008
|
+
return html`<span class="sortArrow">${arrow}</span><span class="sortBadge">${idx + 1}</span>`;
|
|
683
1009
|
}
|
|
684
1010
|
|
|
685
1011
|
// filtering helpers
|
|
@@ -276,6 +276,32 @@ export const tableStyles: CSSResult[] = [
|
|
|
276
276
|
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
|
|
277
277
|
letter-spacing: -0.01em;
|
|
278
278
|
}
|
|
279
|
+
|
|
280
|
+
th[role='columnheader']:hover {
|
|
281
|
+
color: var(--dees-color-text-primary);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
th .sortArrow {
|
|
285
|
+
display: inline-block;
|
|
286
|
+
margin-left: 6px;
|
|
287
|
+
font-size: 10px;
|
|
288
|
+
line-height: 1;
|
|
289
|
+
opacity: 0.7;
|
|
290
|
+
vertical-align: middle;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
th .sortBadge {
|
|
294
|
+
display: inline-block;
|
|
295
|
+
margin-left: 3px;
|
|
296
|
+
padding: 1px 5px;
|
|
297
|
+
font-size: 10px;
|
|
298
|
+
font-weight: 600;
|
|
299
|
+
line-height: 1;
|
|
300
|
+
color: ${cssManager.bdTheme('hsl(222.2 47.4% 30%)', 'hsl(217.2 91.2% 75%)')};
|
|
301
|
+
background: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.12)', 'hsl(217.2 91.2% 59.8% / 0.18)')};
|
|
302
|
+
border-radius: 999px;
|
|
303
|
+
vertical-align: middle;
|
|
304
|
+
}
|
|
279
305
|
|
|
280
306
|
:host([show-vertical-lines]) th {
|
|
281
307
|
border-right: 1px solid var(--dees-color-border-default);
|
|
@@ -26,4 +26,13 @@ export interface Column<T = any> {
|
|
|
26
26
|
hidden?: boolean;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* One entry in a multi-column sort cascade. Order in the array reflects priority:
|
|
31
|
+
* index 0 is the primary sort key, index 1 the secondary tiebreaker, and so on.
|
|
32
|
+
*/
|
|
33
|
+
export interface ISortDescriptor {
|
|
34
|
+
key: string;
|
|
35
|
+
dir: 'asc' | 'desc';
|
|
36
|
+
}
|
|
37
|
+
|
|
29
38
|
export type TDisplayFunction<T = any> = (itemArg: T) => Record<string, any>;
|