@unofficialbox/box-open-elements 0.2.1 → 0.4.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/components/actions/button.d.ts +15 -0
- package/dist/components/actions/button.js +107 -3
- package/dist/components/actions/link-button.d.ts +9 -0
- package/dist/components/actions/link-button.js +55 -4
- package/dist/components/actions/menu.d.ts +10 -0
- package/dist/components/actions/menu.js +68 -11
- package/dist/components/collections/carousel.d.ts +7 -0
- package/dist/components/collections/carousel.js +71 -4
- package/dist/components/collections/datalist-item.d.ts +6 -0
- package/dist/components/collections/datalist-item.js +39 -1
- package/dist/components/collections/draggable-list.js +1 -1
- package/dist/components/collections/grid-view.js +7 -3
- package/dist/components/collections/table.d.ts +53 -0
- package/dist/components/collections/table.js +323 -0
- package/dist/components/collections/thumbnail-card.d.ts +19 -0
- package/dist/components/collections/thumbnail-card.js +210 -0
- package/dist/components/feedback/alert.d.ts +2 -0
- package/dist/components/feedback/alert.js +30 -1
- package/dist/components/feedback/badge.d.ts +10 -0
- package/dist/components/feedback/badge.js +70 -4
- package/dist/components/feedback/badgeable.d.ts +14 -0
- package/dist/components/feedback/badgeable.js +84 -0
- package/dist/components/feedback/chip.d.ts +5 -0
- package/dist/components/feedback/chip.js +74 -2
- package/dist/components/feedback/error-mask.d.ts +2 -0
- package/dist/components/feedback/error-mask.js +23 -0
- package/dist/components/feedback/help-text.js +7 -0
- package/dist/components/feedback/spinner.d.ts +4 -0
- package/dist/components/feedback/spinner.js +30 -2
- package/dist/components/feedback/toast.d.ts +5 -0
- package/dist/components/feedback/toast.js +59 -11
- package/dist/components/forms/category-selector.d.ts +11 -0
- package/dist/components/forms/category-selector.js +211 -17
- package/dist/components/forms/checkbox.d.ts +6 -0
- package/dist/components/forms/checkbox.js +58 -1
- package/dist/components/forms/combobox.d.ts +30 -1
- package/dist/components/forms/combobox.js +348 -44
- package/dist/components/forms/date-field.d.ts +8 -0
- package/dist/components/forms/date-field.js +80 -3
- package/dist/components/forms/dropdown.d.ts +5 -0
- package/dist/components/forms/dropdown.js +32 -3
- package/dist/components/forms/pill-selector-dropdown.d.ts +11 -0
- package/dist/components/forms/pill-selector-dropdown.js +127 -0
- package/dist/components/forms/radio-group.d.ts +9 -8
- package/dist/components/forms/radio-group.js +43 -6
- package/dist/components/forms/search-field.d.ts +9 -0
- package/dist/components/forms/search-field.js +62 -15
- package/dist/components/forms/select.d.ts +19 -8
- package/dist/components/forms/select.js +110 -15
- package/dist/components/forms/text-field.d.ts +14 -0
- package/dist/components/forms/text-field.js +144 -3
- package/dist/components/forms/time-field.d.ts +11 -0
- package/dist/components/forms/time-field.js +51 -0
- package/dist/components/identity/avatar.d.ts +4 -0
- package/dist/components/identity/avatar.js +65 -1
- package/dist/components/identity/contact-datalist-item.d.ts +8 -0
- package/dist/components/identity/contact-datalist-item.js +71 -2
- package/dist/components/layout/nav-sidebar.js +23 -0
- package/dist/components/layout/sidebar-toggle-button.d.ts +3 -0
- package/dist/components/layout/sidebar-toggle-button.js +21 -2
- package/dist/components/navigation/accordion.d.ts +3 -0
- package/dist/components/navigation/accordion.js +17 -2
- package/dist/components/navigation/breadcrumb.d.ts +30 -0
- package/dist/components/navigation/breadcrumb.js +190 -0
- package/dist/components/navigation/tabs.d.ts +4 -0
- package/dist/components/navigation/tabs.js +53 -5
- package/dist/components/overlays/context-menu.d.ts +42 -0
- package/dist/components/overlays/context-menu.js +260 -0
- package/dist/components/overlays/dialog.d.ts +8 -0
- package/dist/components/overlays/dialog.js +45 -2
- package/dist/components/overlays/guide-tooltip.d.ts +51 -0
- package/dist/components/overlays/guide-tooltip.js +361 -0
- package/dist/components/overlays/popover.d.ts +9 -1
- package/dist/components/overlays/popover.js +37 -22
- package/dist/components/overlays/tooltip.d.ts +10 -0
- package/dist/components/overlays/tooltip.js +74 -11
- package/dist/core/form-associated.d.ts +32 -0
- package/dist/core/form-associated.js +114 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/foundations/overlay/index.d.ts +2 -0
- package/dist/foundations/overlay/index.js +1 -0
- package/dist/foundations/overlay/positioning.d.ts +74 -0
- package/dist/foundations/overlay/positioning.js +162 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/package.json +1 -1
|
@@ -28,6 +28,11 @@ const datalistItemStyles = `
|
|
|
28
28
|
background: color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 10%, var(--boe-token-surface-surface, #ffffff) 90%);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/* Active-descendant highlight (host sets active; DOM focus stays on the input). */
|
|
32
|
+
[part="item"][data-active="true"] {
|
|
33
|
+
background: var(--boe-token-surface-surface-hover, #f4f4f4);
|
|
34
|
+
}
|
|
35
|
+
|
|
31
36
|
[part="item"][data-disabled="true"] {
|
|
32
37
|
opacity: 0.55;
|
|
33
38
|
cursor: not-allowed;
|
|
@@ -73,6 +78,16 @@ const datalistItemStyles = `
|
|
|
73
78
|
[part="meta"][hidden] {
|
|
74
79
|
display: none;
|
|
75
80
|
}
|
|
81
|
+
|
|
82
|
+
/* Arbitrary slotted content replaces the default label/meta when present. */
|
|
83
|
+
[part="content"]:not(.has-content) {
|
|
84
|
+
display: none;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
[part="body"].has-slotted [part="label"],
|
|
88
|
+
[part="body"].has-slotted [part="meta"] {
|
|
89
|
+
display: none;
|
|
90
|
+
}
|
|
76
91
|
`;
|
|
77
92
|
/**
|
|
78
93
|
* A single selectable row for a picker/typeahead list — a leading glyph, a
|
|
@@ -83,12 +98,21 @@ const datalistItemStyles = `
|
|
|
83
98
|
*/
|
|
84
99
|
export class BoxDatalistItemElement extends BaseElement {
|
|
85
100
|
static get observedAttributes() {
|
|
86
|
-
return ["disabled", "icon", "label", "meta", "selected", "value"];
|
|
101
|
+
return ["active", "disabled", "icon", "label", "meta", "selected", "value"];
|
|
87
102
|
}
|
|
88
103
|
itemEl;
|
|
89
104
|
thumbEl;
|
|
105
|
+
bodyEl;
|
|
90
106
|
labelEl;
|
|
91
107
|
metaEl;
|
|
108
|
+
contentSlot;
|
|
109
|
+
/** Active-descendant highlight — set by a host that keeps DOM focus elsewhere. */
|
|
110
|
+
get active() {
|
|
111
|
+
return this.hasAttribute("active");
|
|
112
|
+
}
|
|
113
|
+
set active(value) {
|
|
114
|
+
this.toggleAttribute("active", value);
|
|
115
|
+
}
|
|
92
116
|
get label() {
|
|
93
117
|
return this.getAttribute("label") ?? "";
|
|
94
118
|
}
|
|
@@ -146,13 +170,16 @@ export class BoxDatalistItemElement extends BaseElement {
|
|
|
146
170
|
<span part="body">
|
|
147
171
|
<span part="label"></span>
|
|
148
172
|
<span part="meta" hidden></span>
|
|
173
|
+
<slot part="content"></slot>
|
|
149
174
|
</span>
|
|
150
175
|
</div>
|
|
151
176
|
`;
|
|
152
177
|
this.itemEl = this.shadowRoot.querySelector('[part="item"]');
|
|
153
178
|
this.thumbEl = this.shadowRoot.querySelector('[part="thumb"]');
|
|
179
|
+
this.bodyEl = this.shadowRoot.querySelector('[part="body"]');
|
|
154
180
|
this.labelEl = this.shadowRoot.querySelector('[part="label"]');
|
|
155
181
|
this.metaEl = this.shadowRoot.querySelector('[part="meta"]');
|
|
182
|
+
this.contentSlot = this.shadowRoot.querySelector('slot[part="content"]');
|
|
156
183
|
}
|
|
157
184
|
setupListeners() {
|
|
158
185
|
this.itemEl.addEventListener("click", () => this.choose());
|
|
@@ -162,6 +189,15 @@ export class BoxDatalistItemElement extends BaseElement {
|
|
|
162
189
|
this.choose();
|
|
163
190
|
}
|
|
164
191
|
});
|
|
192
|
+
this.contentSlot.addEventListener("slotchange", () => this.syncSlot());
|
|
193
|
+
}
|
|
194
|
+
syncSlot() {
|
|
195
|
+
if (!this.contentSlot) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const hasContent = this.contentSlot.assignedNodes({ flatten: true }).length > 0;
|
|
199
|
+
this.contentSlot.classList.toggle("has-content", hasContent);
|
|
200
|
+
this.bodyEl.classList.toggle("has-slotted", hasContent);
|
|
165
201
|
}
|
|
166
202
|
update() {
|
|
167
203
|
if (!this.itemEl || !this.thumbEl || !this.labelEl || !this.metaEl) {
|
|
@@ -173,7 +209,9 @@ export class BoxDatalistItemElement extends BaseElement {
|
|
|
173
209
|
this.itemEl.setAttribute("aria-selected", selected ? "true" : "false");
|
|
174
210
|
this.itemEl.dataset.selected = selected ? "true" : "false";
|
|
175
211
|
this.itemEl.dataset.disabled = disabled ? "true" : "false";
|
|
212
|
+
this.itemEl.dataset.active = this.active ? "true" : "false";
|
|
176
213
|
this.itemEl.tabIndex = disabled ? -1 : 0;
|
|
214
|
+
this.syncSlot();
|
|
177
215
|
if (disabled) {
|
|
178
216
|
this.itemEl.setAttribute("aria-disabled", "true");
|
|
179
217
|
}
|
|
@@ -172,7 +172,7 @@ export class BoxDraggableListElement extends BaseElement {
|
|
|
172
172
|
<path d="M6 4h.01M10 4h.01M6 8h.01M10 8h.01M6 12h.01M10 12h.01" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
173
173
|
</svg>
|
|
174
174
|
</button>
|
|
175
|
-
<span part="item-label">${escapeHtml(item.label)}</span>
|
|
175
|
+
<slot name="row-${escapeHtml(item.value)}" part="item-content"><span part="item-label">${escapeHtml(item.label)}</span></slot>
|
|
176
176
|
</li>
|
|
177
177
|
`;
|
|
178
178
|
})
|
|
@@ -202,6 +202,8 @@ export class BoxGridViewElement extends BaseElement {
|
|
|
202
202
|
const metaMarkup = item.meta
|
|
203
203
|
? `<span part="meta">${escapeHtml(item.meta)}</span>`
|
|
204
204
|
: "";
|
|
205
|
+
// Per-item named slot lets a host override the tile body (slotRenderer
|
|
206
|
+
// parity); the default thumb/label/meta is the slot's fallback content.
|
|
205
207
|
return `
|
|
206
208
|
<button
|
|
207
209
|
type="button"
|
|
@@ -211,9 +213,11 @@ export class BoxGridViewElement extends BaseElement {
|
|
|
211
213
|
aria-selected="${String(isSelected)}"
|
|
212
214
|
tabindex="${item.value === tabbableValue ? "0" : "-1"}"
|
|
213
215
|
>
|
|
214
|
-
<
|
|
215
|
-
|
|
216
|
-
|
|
216
|
+
<slot name="tile-${escapeHtml(item.value)}" part="tile-slot">
|
|
217
|
+
<span part="thumb" aria-hidden="true">${iconMarkup}</span>
|
|
218
|
+
<span part="tile-label">${escapeHtml(item.label)}</span>
|
|
219
|
+
${metaMarkup}
|
|
220
|
+
</slot>
|
|
217
221
|
</button>
|
|
218
222
|
`;
|
|
219
223
|
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
export interface TableColumn {
|
|
3
|
+
key: string;
|
|
4
|
+
label: string;
|
|
5
|
+
/** Cross-axis alignment of the column's cells. */
|
|
6
|
+
align?: "start" | "end" | "center";
|
|
7
|
+
/** Mark the column sortable — clicking its header emits `sort`. */
|
|
8
|
+
sortable?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface TableRow {
|
|
11
|
+
id: string;
|
|
12
|
+
/** Cell text keyed by column key, or positional strings matching `columns`. */
|
|
13
|
+
cells: Record<string, string> | string[];
|
|
14
|
+
}
|
|
15
|
+
export type TableSelectionMode = "none" | "single" | "multiple";
|
|
16
|
+
/**
|
|
17
|
+
* A semantic, selectable data table — box-ui-elements `Table` + `makeSelectable`.
|
|
18
|
+
* Rows and columns come from JSON. Selection supports single / Ctrl-click toggle
|
|
19
|
+
* / Shift-click range, plus keyboard (Arrow, Shift+Arrow to extend, Space to
|
|
20
|
+
* toggle, Ctrl/Cmd+A select-all, Escape clear). Sorting is host-owned: sortable
|
|
21
|
+
* headers emit `sort` and the host reorders `rows`.
|
|
22
|
+
*/
|
|
23
|
+
export declare class BoxTableElement extends BaseElement {
|
|
24
|
+
static get observedAttributes(): string[];
|
|
25
|
+
private bodyEl;
|
|
26
|
+
private headEl;
|
|
27
|
+
private selected;
|
|
28
|
+
private anchorIndex;
|
|
29
|
+
get columns(): TableColumn[];
|
|
30
|
+
set columns(value: TableColumn[]);
|
|
31
|
+
get rows(): TableRow[];
|
|
32
|
+
set rows(value: TableRow[]);
|
|
33
|
+
get selectionMode(): TableSelectionMode;
|
|
34
|
+
set selectionMode(value: TableSelectionMode);
|
|
35
|
+
get selectedIds(): string[];
|
|
36
|
+
set selectedIds(value: string[]);
|
|
37
|
+
get label(): string;
|
|
38
|
+
set label(value: string);
|
|
39
|
+
private parseJson;
|
|
40
|
+
private cellText;
|
|
41
|
+
protected renderTemplate(): void;
|
|
42
|
+
protected setupListeners(): void;
|
|
43
|
+
private rowElements;
|
|
44
|
+
private onBodyKeydown;
|
|
45
|
+
private toggleSort;
|
|
46
|
+
private activateRow;
|
|
47
|
+
private selectRange;
|
|
48
|
+
private selectAll;
|
|
49
|
+
private setSelection;
|
|
50
|
+
private applySelectionState;
|
|
51
|
+
protected update(): void;
|
|
52
|
+
}
|
|
53
|
+
export declare const defineBoxTableElement: (tagName?: string) => typeof BoxTableElement;
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
import { boePanel, boeRadius, boeSpace } from "../../foundations/geometry/index.js";
|
|
3
|
+
const DEFAULT_TAG_NAME = "box-table";
|
|
4
|
+
const escapeHtml = (value) => value
|
|
5
|
+
.replaceAll("&", "&")
|
|
6
|
+
.replaceAll("<", "<")
|
|
7
|
+
.replaceAll(">", ">")
|
|
8
|
+
.replaceAll('"', """);
|
|
9
|
+
const tableStyles = `
|
|
10
|
+
:host {
|
|
11
|
+
display: block;
|
|
12
|
+
color: inherit;
|
|
13
|
+
font: inherit;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
[part="shell"] {
|
|
17
|
+
border: ${boePanel.border};
|
|
18
|
+
border-radius: ${boeRadius.large};
|
|
19
|
+
overflow: auto;
|
|
20
|
+
background: var(--boe-token-surface-surface, #ffffff);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
table {
|
|
24
|
+
width: 100%;
|
|
25
|
+
border-collapse: collapse;
|
|
26
|
+
font-size: 13px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
thead th {
|
|
30
|
+
text-align: start;
|
|
31
|
+
padding: ${boeSpace[2]} ${boeSpace[3]};
|
|
32
|
+
background: var(--boe-token-surface-surface-secondary, #fbfbfb);
|
|
33
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
34
|
+
font-size: 11px;
|
|
35
|
+
font-weight: 700;
|
|
36
|
+
letter-spacing: 0.06em;
|
|
37
|
+
text-transform: uppercase;
|
|
38
|
+
white-space: nowrap;
|
|
39
|
+
border-bottom: 1px solid var(--boe-token-stroke-stroke, #e8e8e8);
|
|
40
|
+
position: sticky;
|
|
41
|
+
top: 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
th[data-align="end"], td[data-align="end"] { text-align: end; }
|
|
45
|
+
th[data-align="center"], td[data-align="center"] { text-align: center; }
|
|
46
|
+
|
|
47
|
+
th[part="sortable"] {
|
|
48
|
+
cursor: pointer;
|
|
49
|
+
user-select: none;
|
|
50
|
+
}
|
|
51
|
+
th[part="sortable"]:hover { color: var(--boe-token-text-text, #222222); }
|
|
52
|
+
th[aria-sort] .boe-sort-arrow::after { content: " ↕"; opacity: 0.5; }
|
|
53
|
+
th[aria-sort="ascending"] .boe-sort-arrow::after { content: " ↑"; opacity: 1; }
|
|
54
|
+
th[aria-sort="descending"] .boe-sort-arrow::after { content: " ↓"; opacity: 1; }
|
|
55
|
+
|
|
56
|
+
tbody td {
|
|
57
|
+
padding: ${boeSpace[2]} ${boeSpace[3]};
|
|
58
|
+
border-bottom: 1px solid var(--boe-token-stroke-stroke, #e8e8e8);
|
|
59
|
+
color: var(--boe-token-text-text, #222222);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
tbody tr:last-child td { border-bottom: none; }
|
|
63
|
+
|
|
64
|
+
tbody tr[part="row"] {
|
|
65
|
+
outline: none;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
:host([selection-mode]:not([selection-mode="none"])) tbody tr[part="row"] {
|
|
69
|
+
cursor: pointer;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
tbody tr[aria-selected="true"] td {
|
|
73
|
+
background: color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 10%, transparent);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
tbody tr[part="row"]:focus-visible td {
|
|
77
|
+
box-shadow: inset 0 0 0 2px var(--boe-token-surface-surface-brand, #0061d5);
|
|
78
|
+
}
|
|
79
|
+
`;
|
|
80
|
+
/**
|
|
81
|
+
* A semantic, selectable data table — box-ui-elements `Table` + `makeSelectable`.
|
|
82
|
+
* Rows and columns come from JSON. Selection supports single / Ctrl-click toggle
|
|
83
|
+
* / Shift-click range, plus keyboard (Arrow, Shift+Arrow to extend, Space to
|
|
84
|
+
* toggle, Ctrl/Cmd+A select-all, Escape clear). Sorting is host-owned: sortable
|
|
85
|
+
* headers emit `sort` and the host reorders `rows`.
|
|
86
|
+
*/
|
|
87
|
+
export class BoxTableElement extends BaseElement {
|
|
88
|
+
static get observedAttributes() {
|
|
89
|
+
return ["columns", "rows", "selection-mode", "label", "sort-key", "sort-direction"];
|
|
90
|
+
}
|
|
91
|
+
bodyEl;
|
|
92
|
+
headEl;
|
|
93
|
+
selected = new Set();
|
|
94
|
+
anchorIndex = -1;
|
|
95
|
+
get columns() {
|
|
96
|
+
return this.parseJson("columns", []);
|
|
97
|
+
}
|
|
98
|
+
set columns(value) {
|
|
99
|
+
this.setAttribute("columns", JSON.stringify(value));
|
|
100
|
+
}
|
|
101
|
+
get rows() {
|
|
102
|
+
return this.parseJson("rows", []);
|
|
103
|
+
}
|
|
104
|
+
set rows(value) {
|
|
105
|
+
this.setAttribute("rows", JSON.stringify(value));
|
|
106
|
+
}
|
|
107
|
+
get selectionMode() {
|
|
108
|
+
const mode = this.getAttribute("selection-mode");
|
|
109
|
+
return mode === "single" || mode === "multiple" ? mode : "none";
|
|
110
|
+
}
|
|
111
|
+
set selectionMode(value) {
|
|
112
|
+
this.setAttribute("selection-mode", value);
|
|
113
|
+
}
|
|
114
|
+
get selectedIds() {
|
|
115
|
+
return [...this.selected];
|
|
116
|
+
}
|
|
117
|
+
set selectedIds(value) {
|
|
118
|
+
this.selected = new Set(value);
|
|
119
|
+
if (this.isRendered) {
|
|
120
|
+
this.update();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
get label() {
|
|
124
|
+
return this.getAttribute("label") ?? "Table";
|
|
125
|
+
}
|
|
126
|
+
set label(value) {
|
|
127
|
+
this.setAttribute("label", value);
|
|
128
|
+
}
|
|
129
|
+
parseJson(attr, fallback) {
|
|
130
|
+
const raw = this.getAttribute(attr);
|
|
131
|
+
if (!raw)
|
|
132
|
+
return fallback;
|
|
133
|
+
try {
|
|
134
|
+
const parsed = JSON.parse(raw);
|
|
135
|
+
return parsed ?? fallback;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return fallback;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
cellText(row, column, index) {
|
|
142
|
+
if (Array.isArray(row.cells)) {
|
|
143
|
+
return row.cells[index] ?? "";
|
|
144
|
+
}
|
|
145
|
+
return row.cells[column.key] ?? "";
|
|
146
|
+
}
|
|
147
|
+
renderTemplate() {
|
|
148
|
+
if (!this.shadowRoot)
|
|
149
|
+
return;
|
|
150
|
+
this.shadowRoot.innerHTML = `
|
|
151
|
+
<style>${tableStyles}</style>
|
|
152
|
+
<div part="shell">
|
|
153
|
+
<table part="table">
|
|
154
|
+
<thead><tr part="header-row"></tr></thead>
|
|
155
|
+
<tbody part="body"></tbody>
|
|
156
|
+
</table>
|
|
157
|
+
</div>
|
|
158
|
+
`;
|
|
159
|
+
this.headEl = this.shadowRoot.querySelector('[part="header-row"]');
|
|
160
|
+
this.bodyEl = this.shadowRoot.querySelector('[part="body"]');
|
|
161
|
+
}
|
|
162
|
+
setupListeners() {
|
|
163
|
+
this.headEl.addEventListener("click", event => {
|
|
164
|
+
const th = event.target.closest('th[part="sortable"]');
|
|
165
|
+
if (!th)
|
|
166
|
+
return;
|
|
167
|
+
this.toggleSort(th.dataset.key ?? "");
|
|
168
|
+
});
|
|
169
|
+
this.bodyEl.addEventListener("click", event => {
|
|
170
|
+
const row = event.target.closest('[part="row"]');
|
|
171
|
+
if (!row)
|
|
172
|
+
return;
|
|
173
|
+
const mouse = event;
|
|
174
|
+
this.activateRow(Number(row.dataset.index), { shift: mouse.shiftKey, toggle: mouse.ctrlKey || mouse.metaKey });
|
|
175
|
+
});
|
|
176
|
+
this.bodyEl.addEventListener("keydown", event => this.onBodyKeydown(event));
|
|
177
|
+
}
|
|
178
|
+
rowElements() {
|
|
179
|
+
return Array.from(this.bodyEl.querySelectorAll('[part="row"]'));
|
|
180
|
+
}
|
|
181
|
+
onBodyKeydown(event) {
|
|
182
|
+
const rows = this.rowElements();
|
|
183
|
+
if (rows.length === 0)
|
|
184
|
+
return;
|
|
185
|
+
const currentRow = event.target.closest('[part="row"]');
|
|
186
|
+
const index = currentRow ? Number(currentRow.dataset.index) : -1;
|
|
187
|
+
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "a") {
|
|
188
|
+
if (this.selectionMode === "multiple") {
|
|
189
|
+
event.preventDefault();
|
|
190
|
+
this.selectAll();
|
|
191
|
+
}
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (event.key === "Escape") {
|
|
195
|
+
if (this.selected.size) {
|
|
196
|
+
event.preventDefault();
|
|
197
|
+
this.setSelection([]);
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (event.key === " ") {
|
|
202
|
+
event.preventDefault();
|
|
203
|
+
this.activateRow(index, { toggle: this.selectionMode === "multiple" });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
let nextIndex = index;
|
|
207
|
+
if (event.key === "ArrowDown")
|
|
208
|
+
nextIndex = Math.min(index + 1, rows.length - 1);
|
|
209
|
+
else if (event.key === "ArrowUp")
|
|
210
|
+
nextIndex = Math.max(index - 1, 0);
|
|
211
|
+
else if (event.key === "Home")
|
|
212
|
+
nextIndex = 0;
|
|
213
|
+
else if (event.key === "End")
|
|
214
|
+
nextIndex = rows.length - 1;
|
|
215
|
+
else
|
|
216
|
+
return;
|
|
217
|
+
event.preventDefault();
|
|
218
|
+
rows[nextIndex]?.focus();
|
|
219
|
+
// Shift+Arrow extends the selection from the anchor.
|
|
220
|
+
if (event.shiftKey && this.selectionMode === "multiple") {
|
|
221
|
+
this.selectRange(this.anchorIndex < 0 ? index : this.anchorIndex, nextIndex);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
toggleSort(key) {
|
|
225
|
+
const currentKey = this.getAttribute("sort-key");
|
|
226
|
+
const currentDir = this.getAttribute("sort-direction");
|
|
227
|
+
const direction = currentKey === key && currentDir === "ascending" ? "descending" : "ascending";
|
|
228
|
+
this.dispatchEvent(new CustomEvent("sort", { bubbles: true, composed: true, detail: { key, direction } }));
|
|
229
|
+
}
|
|
230
|
+
activateRow(index, options = {}) {
|
|
231
|
+
const rows = this.rows;
|
|
232
|
+
const row = rows[index];
|
|
233
|
+
if (!row || this.selectionMode === "none")
|
|
234
|
+
return;
|
|
235
|
+
if (this.selectionMode === "single") {
|
|
236
|
+
this.setSelection([row.id]);
|
|
237
|
+
this.anchorIndex = index;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (options.shift && this.anchorIndex >= 0) {
|
|
241
|
+
this.selectRange(this.anchorIndex, index);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (options.toggle) {
|
|
245
|
+
const next = new Set(this.selected);
|
|
246
|
+
next.has(row.id) ? next.delete(row.id) : next.add(row.id);
|
|
247
|
+
this.setSelection([...next]);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
this.setSelection([row.id]);
|
|
251
|
+
}
|
|
252
|
+
this.anchorIndex = index;
|
|
253
|
+
}
|
|
254
|
+
selectRange(from, to) {
|
|
255
|
+
const rows = this.rows;
|
|
256
|
+
const [start, end] = from <= to ? [from, to] : [to, from];
|
|
257
|
+
const ids = rows.slice(start, end + 1).map(row => row.id);
|
|
258
|
+
this.setSelection(ids);
|
|
259
|
+
}
|
|
260
|
+
selectAll() {
|
|
261
|
+
this.setSelection(this.rows.map(row => row.id));
|
|
262
|
+
}
|
|
263
|
+
setSelection(ids) {
|
|
264
|
+
this.selected = new Set(ids);
|
|
265
|
+
this.applySelectionState();
|
|
266
|
+
this.dispatchEvent(new CustomEvent("selection-changed", {
|
|
267
|
+
bubbles: true,
|
|
268
|
+
composed: true,
|
|
269
|
+
detail: { selectedIds: [...this.selected] },
|
|
270
|
+
}));
|
|
271
|
+
}
|
|
272
|
+
applySelectionState() {
|
|
273
|
+
for (const row of this.rowElements()) {
|
|
274
|
+
const id = row.dataset.id ?? "";
|
|
275
|
+
row.setAttribute("aria-selected", String(this.selected.has(id)));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
update() {
|
|
279
|
+
if (!this.bodyEl || !this.headEl)
|
|
280
|
+
return;
|
|
281
|
+
const columns = this.columns;
|
|
282
|
+
const rows = this.rows;
|
|
283
|
+
const selectable = this.selectionMode !== "none";
|
|
284
|
+
const sortKey = this.getAttribute("sort-key");
|
|
285
|
+
const sortDir = this.getAttribute("sort-direction");
|
|
286
|
+
this.shadowRoot?.querySelector('[part="table"]')?.setAttribute("aria-label", this.label);
|
|
287
|
+
this.shadowRoot?.querySelector('[part="table"]')?.setAttribute("role", selectable ? "grid" : "table");
|
|
288
|
+
this.headEl.innerHTML = columns
|
|
289
|
+
.map(column => {
|
|
290
|
+
const align = column.align ? ` data-align="${column.align}"` : "";
|
|
291
|
+
if (column.sortable) {
|
|
292
|
+
const sort = sortKey === column.key ? ` aria-sort="${sortDir === "descending" ? "descending" : "ascending"}"` : ' aria-sort="none"';
|
|
293
|
+
return `<th part="sortable" scope="col" data-key="${escapeHtml(column.key)}"${align}${sort}>${escapeHtml(column.label)}<span class="boe-sort-arrow" aria-hidden="true"></span></th>`;
|
|
294
|
+
}
|
|
295
|
+
return `<th scope="col"${align}>${escapeHtml(column.label)}</th>`;
|
|
296
|
+
})
|
|
297
|
+
.join("");
|
|
298
|
+
this.bodyEl.innerHTML = rows
|
|
299
|
+
.map((row, index) => {
|
|
300
|
+
const selected = this.selected.has(row.id);
|
|
301
|
+
const rowAttrs = selectable
|
|
302
|
+
? ` part="row" role="row" tabindex="${index === 0 ? "0" : "-1"}" aria-selected="${selected}"`
|
|
303
|
+
: ' part="row" role="row"';
|
|
304
|
+
const cells = columns
|
|
305
|
+
.map(column => {
|
|
306
|
+
const align = column.align ? ` data-align="${column.align}"` : "";
|
|
307
|
+
const role = selectable ? ' role="gridcell"' : ' role="cell"';
|
|
308
|
+
return `<td${align}${role}>${escapeHtml(this.cellText(row, column, columns.indexOf(column)))}</td>`;
|
|
309
|
+
})
|
|
310
|
+
.join("");
|
|
311
|
+
return `<tr${rowAttrs} data-index="${index}" data-id="${escapeHtml(row.id)}">${cells}</tr>`;
|
|
312
|
+
})
|
|
313
|
+
.join("");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
export const defineBoxTableElement = (tagName = DEFAULT_TAG_NAME) => {
|
|
317
|
+
const existingElement = customElements.get(tagName);
|
|
318
|
+
if (existingElement) {
|
|
319
|
+
return existingElement;
|
|
320
|
+
}
|
|
321
|
+
customElements.define(tagName, BoxTableElement);
|
|
322
|
+
return BoxTableElement;
|
|
323
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
export declare class BoxThumbnailCardElement extends BaseElement {
|
|
3
|
+
static get observedAttributes(): string[];
|
|
4
|
+
private cardEl;
|
|
5
|
+
private titleEl;
|
|
6
|
+
private subtitleEl;
|
|
7
|
+
get cardTitle(): string;
|
|
8
|
+
set cardTitle(value: string);
|
|
9
|
+
get subtitle(): string;
|
|
10
|
+
set subtitle(value: string);
|
|
11
|
+
/** When set, the card is a button: focusable, keyboard-activated, emits `activate`. */
|
|
12
|
+
get interactive(): boolean;
|
|
13
|
+
set interactive(value: boolean);
|
|
14
|
+
protected renderTemplate(): void;
|
|
15
|
+
protected setupListeners(): void;
|
|
16
|
+
private activate;
|
|
17
|
+
protected update(): void;
|
|
18
|
+
}
|
|
19
|
+
export declare const defineBoxThumbnailCardElement: (tagName?: string) => typeof BoxThumbnailCardElement;
|