@dodlhuat/basix 1.2.7 → 1.2.9
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/README.md +1 -1
- package/js/bottom-sheet.d.ts +37 -0
- package/js/calendar.d.ts +115 -0
- package/js/carousel.d.ts +34 -0
- package/js/chart.d.ts +73 -0
- package/js/code-viewer.d.ts +16 -0
- package/js/context-menu.d.ts +31 -0
- package/js/datepicker.d.ts +55 -0
- package/js/dropdown.d.ts +30 -0
- package/js/editor.d.ts +41 -0
- package/js/file-uploader.d.ts +48 -0
- package/js/flyout-menu.d.ts +37 -0
- package/js/gallery.d.ts +35 -0
- package/js/group-picker.d.ts +59 -0
- package/js/lightbox.d.ts +46 -0
- package/js/modal.d.ts +28 -0
- package/js/popover.d.ts +46 -0
- package/js/position.d.ts +31 -0
- package/js/push-menu.d.ts +31 -0
- package/js/range-slider.d.ts +9 -0
- package/js/scroll.d.ts +15 -0
- package/js/scrollbar.d.ts +48 -0
- package/js/select.d.ts +16 -0
- package/js/sidebar-nav.d.ts +22 -0
- package/js/stepper.d.ts +26 -0
- package/js/table.d.ts +98 -0
- package/js/tabs.d.ts +57 -0
- package/js/theme.d.ts +65 -0
- package/js/timepicker.d.ts +37 -0
- package/js/toast.d.ts +26 -0
- package/js/tooltip.d.ts +34 -0
- package/js/tree.d.ts +40 -0
- package/js/utils.d.ts +24 -0
- package/js/virtual-dropdown.d.ts +55 -0
- package/package.json +1 -1
- package/js/bottom-sheet.ts +0 -224
- package/js/calendar.ts +0 -774
- package/js/carousel.ts +0 -222
- package/js/chart.ts +0 -694
- package/js/code-viewer.ts +0 -188
- package/js/context-menu.ts +0 -252
- package/js/datepicker.ts +0 -640
- package/js/dropdown.ts +0 -180
- package/js/editor.ts +0 -492
- package/js/file-uploader.ts +0 -361
- package/js/flyout-menu.ts +0 -255
- package/js/gallery.ts +0 -237
- package/js/group-picker.ts +0 -451
- package/js/lightbox.ts +0 -333
- package/js/modal.ts +0 -171
- package/js/popover.ts +0 -221
- package/js/position.ts +0 -111
- package/js/push-menu.ts +0 -286
- package/js/range-slider.ts +0 -33
- package/js/scroll.ts +0 -47
- package/js/scrollbar.ts +0 -335
- package/js/select.ts +0 -235
- package/js/sidebar-nav.ts +0 -66
- package/js/stepper.ts +0 -109
- package/js/table.ts +0 -459
- package/js/tabs.ts +0 -280
- package/js/theme.ts +0 -235
- package/js/timepicker.ts +0 -202
- package/js/toast.ts +0 -134
- package/js/tooltip.ts +0 -196
- package/js/tree.ts +0 -244
- package/js/tsconfig.json +0 -18
- package/js/utils.ts +0 -119
- package/js/virtual-dropdown.ts +0 -396
package/js/gallery.ts
DELETED
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
import { escapeHtml } from './utils.js';
|
|
2
|
-
|
|
3
|
-
interface ImageData {
|
|
4
|
-
src: string;
|
|
5
|
-
title: string;
|
|
6
|
-
desc: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
interface MasonryGalleryOptions {
|
|
10
|
-
fetchFunction: () => Promise<ImageData[]>;
|
|
11
|
-
minColumnWidth?: number;
|
|
12
|
-
scrollThreshold?: number;
|
|
13
|
-
loaderSelector?: string;
|
|
14
|
-
reload?: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
class MasonryGallery {
|
|
18
|
-
private container: HTMLElement;
|
|
19
|
-
private readonly loader: HTMLElement | null;
|
|
20
|
-
private options: Required<Omit<MasonryGalleryOptions, "loaderSelector">>;
|
|
21
|
-
private columns: HTMLDivElement[] = [];
|
|
22
|
-
private isFetching: boolean = false;
|
|
23
|
-
private resizeObserver: ResizeObserver | null = null;
|
|
24
|
-
private abortController: AbortController | null = null;
|
|
25
|
-
private reloaded = 0;
|
|
26
|
-
|
|
27
|
-
constructor(containerId: string, options: MasonryGalleryOptions) {
|
|
28
|
-
const container = document.getElementById(containerId);
|
|
29
|
-
if (!container) {
|
|
30
|
-
throw new Error(`Container with id "${containerId}" not found`);
|
|
31
|
-
}
|
|
32
|
-
this.container = container;
|
|
33
|
-
this.loader = document.querySelector(options.loaderSelector || ".loader");
|
|
34
|
-
|
|
35
|
-
this.options = {
|
|
36
|
-
minColumnWidth: options.minColumnWidth ?? 250,
|
|
37
|
-
scrollThreshold: options.scrollThreshold ?? 100,
|
|
38
|
-
reload: 2,
|
|
39
|
-
fetchFunction: options.fetchFunction,
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
this.init();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
private init(): void {
|
|
46
|
-
this.setupLayout();
|
|
47
|
-
this.loadMoreImages();
|
|
48
|
-
this.addEventListeners();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
private setupLayout(): void {
|
|
52
|
-
const containerWidth = this.container.getBoundingClientRect().width;
|
|
53
|
-
const numColumns = Math.max(
|
|
54
|
-
1,
|
|
55
|
-
Math.floor(containerWidth / this.options.minColumnWidth),
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
if (this.columns.length !== numColumns) {
|
|
59
|
-
this.container.innerHTML = "";
|
|
60
|
-
this.columns = [];
|
|
61
|
-
|
|
62
|
-
for (let i = 0; i < numColumns; i++) {
|
|
63
|
-
const col = document.createElement("div");
|
|
64
|
-
col.className = "masonry-column";
|
|
65
|
-
this.container.appendChild(col);
|
|
66
|
-
this.columns.push(col);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
private addEventListeners(): void {
|
|
72
|
-
let resizeTimeout: number;
|
|
73
|
-
window.addEventListener("resize", () => {
|
|
74
|
-
clearTimeout(resizeTimeout);
|
|
75
|
-
resizeTimeout = setTimeout(() => {
|
|
76
|
-
this.reLayout();
|
|
77
|
-
}, 200);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
this.abortController = new AbortController();
|
|
81
|
-
window.addEventListener("scroll", this.handleScroll, {
|
|
82
|
-
passive: true,
|
|
83
|
-
signal: this.abortController.signal,
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
private reLayout(): void {
|
|
88
|
-
const items: HTMLElement[] = [];
|
|
89
|
-
this.columns.forEach((col) => {
|
|
90
|
-
Array.from(col.children).forEach((child) => {
|
|
91
|
-
items.push(child as HTMLElement);
|
|
92
|
-
});
|
|
93
|
-
col.innerHTML = "";
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
const availableWidth = Math.min(1200, window.innerWidth - 40);
|
|
97
|
-
const numColumns = Math.max(
|
|
98
|
-
1,
|
|
99
|
-
Math.floor(availableWidth / this.options.minColumnWidth),
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
if (this.columns.length !== numColumns) {
|
|
103
|
-
this.container.innerHTML = "";
|
|
104
|
-
this.columns = [];
|
|
105
|
-
|
|
106
|
-
for (let i = 0; i < numColumns; i++) {
|
|
107
|
-
const col = document.createElement("div");
|
|
108
|
-
col.className = "masonry-column";
|
|
109
|
-
this.container.appendChild(col);
|
|
110
|
-
this.columns.push(col);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
items.forEach((item) => {
|
|
115
|
-
this.addToShortestColumn(item);
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
private handleScroll = (): void => {
|
|
120
|
-
if (this.isFetching) return;
|
|
121
|
-
|
|
122
|
-
const rect = this.container.getBoundingClientRect();
|
|
123
|
-
if (rect.bottom > 0 && rect.bottom <= window.innerHeight + this.options.scrollThreshold) {
|
|
124
|
-
this.loadMoreImages();
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
private async loadMoreImages(isAutoFill = false): Promise<void> {
|
|
129
|
-
if (!isAutoFill) this.reloaded++;
|
|
130
|
-
if (this.options.reload > 0 && this.reloaded > this.options.reload) {
|
|
131
|
-
console.warn("Maximum reload limit reached.");
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
if (this.isFetching) return;
|
|
135
|
-
|
|
136
|
-
this.isFetching = true;
|
|
137
|
-
this.toggleLoader(true);
|
|
138
|
-
|
|
139
|
-
try {
|
|
140
|
-
const newImages = await this.options.fetchFunction();
|
|
141
|
-
this.renderImages(newImages);
|
|
142
|
-
} catch (error) {
|
|
143
|
-
throw new Error("Error loading images: " + error);
|
|
144
|
-
} finally {
|
|
145
|
-
this.isFetching = false;
|
|
146
|
-
this.toggleLoader(false);
|
|
147
|
-
// If the rendered content doesn't fill the viewport, auto-load the next
|
|
148
|
-
// batch without waiting for a scroll event (multi-column layout is shorter)
|
|
149
|
-
requestAnimationFrame(() => {
|
|
150
|
-
const rect = this.container.getBoundingClientRect();
|
|
151
|
-
if (rect.bottom <= window.innerHeight + this.options.scrollThreshold) {
|
|
152
|
-
this.loadMoreImages(true);
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
private toggleLoader(show: boolean): void {
|
|
159
|
-
if (this.loader) {
|
|
160
|
-
this.loader.classList.toggle("hidden", !show);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
private renderImages(imageDataList: ImageData[]): void {
|
|
165
|
-
// Sort columns by current height so we start filling from the shortest.
|
|
166
|
-
// Then round-robin across them — this avoids the problem where unloaded
|
|
167
|
-
// images (0 height) cause offsetHeight-based distribution to pile all
|
|
168
|
-
// new items into a single column.
|
|
169
|
-
const sorted = [...this.columns].sort(
|
|
170
|
-
(a, b) => a.offsetHeight - b.offsetHeight,
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
imageDataList.forEach((data, index) => {
|
|
174
|
-
const item = this.createCard(data);
|
|
175
|
-
const col = sorted[index % sorted.length];
|
|
176
|
-
col.appendChild(item);
|
|
177
|
-
|
|
178
|
-
requestAnimationFrame(() => {
|
|
179
|
-
const img = item.querySelector("img");
|
|
180
|
-
if (img) {
|
|
181
|
-
img.addEventListener("load", () => img.classList.add("loaded"), {
|
|
182
|
-
once: true,
|
|
183
|
-
});
|
|
184
|
-
if (img.complete) {
|
|
185
|
-
img.classList.add("loaded");
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
private createCard(data: ImageData): HTMLDivElement {
|
|
193
|
-
const div = document.createElement("div");
|
|
194
|
-
div.className = "masonry-item";
|
|
195
|
-
|
|
196
|
-
div.innerHTML = `
|
|
197
|
-
<img src="${escapeHtml(data.src)}" alt="${escapeHtml(data.title)}" loading="lazy">
|
|
198
|
-
<div class="masonry-item-info">
|
|
199
|
-
<h3 class="masonry-item-title">${escapeHtml(data.title)}</h3>
|
|
200
|
-
<p class="masonry-item-desc">${escapeHtml(data.desc)}</p>
|
|
201
|
-
</div>
|
|
202
|
-
`;
|
|
203
|
-
|
|
204
|
-
return div;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
private addToShortestColumn(element: HTMLElement): void {
|
|
208
|
-
if (this.columns.length === 0) return;
|
|
209
|
-
|
|
210
|
-
let shortestCol = this.columns[0];
|
|
211
|
-
let minHeight = shortestCol.offsetHeight;
|
|
212
|
-
|
|
213
|
-
for (let i = 1; i < this.columns.length; i++) {
|
|
214
|
-
const h = this.columns[i].offsetHeight;
|
|
215
|
-
if (h < minHeight) {
|
|
216
|
-
minHeight = h;
|
|
217
|
-
shortestCol = this.columns[i];
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
shortestCol.appendChild(element);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
public destroy(): void {
|
|
225
|
-
if (this.resizeObserver) {
|
|
226
|
-
this.resizeObserver.disconnect();
|
|
227
|
-
this.resizeObserver = null;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (this.abortController) {
|
|
231
|
-
this.abortController.abort();
|
|
232
|
-
this.abortController = null;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export { MasonryGallery, ImageData };
|
package/js/group-picker.ts
DELETED
|
@@ -1,451 +0,0 @@
|
|
|
1
|
-
import { escapeHtml } from './utils.js';
|
|
2
|
-
|
|
3
|
-
interface SubgroupData {
|
|
4
|
-
id: string;
|
|
5
|
-
label: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
interface GroupData {
|
|
9
|
-
id: string;
|
|
10
|
-
label: string;
|
|
11
|
-
subgroups?: SubgroupData[];
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface GroupPickerSelection {
|
|
15
|
-
parentGroups: string[];
|
|
16
|
-
subgroups: { groupId: string; subgroupId: string }[];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
interface GroupPickerOptions {
|
|
20
|
-
onSelectionChange?: (selection: GroupPickerSelection) => void;
|
|
21
|
-
searchPlaceholder?: string;
|
|
22
|
-
selectAllLabel?: string;
|
|
23
|
-
deselectLabel?: string;
|
|
24
|
-
emptyLabel?: string;
|
|
25
|
-
selectionPlaceholder?: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
class GroupPicker {
|
|
29
|
-
private container: HTMLElement;
|
|
30
|
-
private data: GroupData[];
|
|
31
|
-
private options: Required<GroupPickerOptions>;
|
|
32
|
-
private abortController: AbortController;
|
|
33
|
-
|
|
34
|
-
// State
|
|
35
|
-
private selectedParents: Set<string> = new Set();
|
|
36
|
-
private selectedSubs: Map<string, Set<string>> = new Map();
|
|
37
|
-
private expandedGroups: Set<string> = new Set();
|
|
38
|
-
private searchQuery: string = '';
|
|
39
|
-
|
|
40
|
-
// DOM refs
|
|
41
|
-
private searchInput!: HTMLInputElement;
|
|
42
|
-
private listEl!: HTMLElement;
|
|
43
|
-
private selectionEl!: HTMLElement;
|
|
44
|
-
|
|
45
|
-
constructor(
|
|
46
|
-
selector: string | HTMLElement,
|
|
47
|
-
data: GroupData[],
|
|
48
|
-
options: GroupPickerOptions = {}
|
|
49
|
-
) {
|
|
50
|
-
const el = typeof selector === 'string'
|
|
51
|
-
? document.querySelector<HTMLElement>(selector)
|
|
52
|
-
: selector;
|
|
53
|
-
|
|
54
|
-
if (!el) throw new Error(`GroupPicker: Element not found for "${selector}"`);
|
|
55
|
-
|
|
56
|
-
this.container = el;
|
|
57
|
-
this.data = data;
|
|
58
|
-
this.abortController = new AbortController();
|
|
59
|
-
|
|
60
|
-
this.options = {
|
|
61
|
-
onSelectionChange: options.onSelectionChange ?? (() => {}),
|
|
62
|
-
searchPlaceholder: options.searchPlaceholder ?? 'Gruppen durchsuchen...',
|
|
63
|
-
selectAllLabel: options.selectAllLabel ?? 'Alle',
|
|
64
|
-
deselectLabel: options.deselectLabel ?? 'Abwählen',
|
|
65
|
-
emptyLabel: options.emptyLabel ?? 'Keine Ergebnisse',
|
|
66
|
-
selectionPlaceholder: options.selectionPlaceholder ?? 'Noch keine Auswahl getroffen',
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
this.init();
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
private init(): void {
|
|
73
|
-
this.container.classList.add('group-picker');
|
|
74
|
-
this.render();
|
|
75
|
-
this.attachEvents();
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
private render(): void {
|
|
79
|
-
this.container.innerHTML = '';
|
|
80
|
-
|
|
81
|
-
// Selection summary — Basix .chips container
|
|
82
|
-
this.selectionEl = document.createElement('div');
|
|
83
|
-
this.selectionEl.className = 'chips group-picker__selection';
|
|
84
|
-
this.selectionEl.dataset.placeholder = this.options.selectionPlaceholder;
|
|
85
|
-
|
|
86
|
-
// Search — Basix form input with font icon overlay
|
|
87
|
-
const searchWrap = document.createElement('div');
|
|
88
|
-
searchWrap.className = 'group-picker__search';
|
|
89
|
-
searchWrap.innerHTML = `
|
|
90
|
-
<span class="icon icon-search group-picker__search-icon" aria-hidden="true"></span>
|
|
91
|
-
<input type="text" />
|
|
92
|
-
`;
|
|
93
|
-
this.searchInput = searchWrap.querySelector('input')!;
|
|
94
|
-
this.searchInput.placeholder = this.options.searchPlaceholder;
|
|
95
|
-
|
|
96
|
-
// List
|
|
97
|
-
this.listEl = document.createElement('div');
|
|
98
|
-
this.listEl.className = 'group-picker__list';
|
|
99
|
-
|
|
100
|
-
this.container.append(this.selectionEl, searchWrap, this.listEl);
|
|
101
|
-
this.renderGroups();
|
|
102
|
-
this.renderSelection();
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
private renderGroups(): void {
|
|
106
|
-
this.listEl.innerHTML = '';
|
|
107
|
-
const query = this.searchQuery.toLowerCase().trim();
|
|
108
|
-
let visibleCount = 0;
|
|
109
|
-
|
|
110
|
-
for (const group of this.data) {
|
|
111
|
-
const subs = group.subgroups ?? [];
|
|
112
|
-
const groupMatches = group.label.toLowerCase().includes(query);
|
|
113
|
-
const matchingSubs = subs.filter(s =>
|
|
114
|
-
s.label.toLowerCase().includes(query)
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
if (!groupMatches && matchingSubs.length === 0 && query) continue;
|
|
118
|
-
|
|
119
|
-
visibleCount++;
|
|
120
|
-
const groupEl = this.createGroupElement(group, query, groupMatches, matchingSubs);
|
|
121
|
-
this.listEl.appendChild(groupEl);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (visibleCount === 0) {
|
|
125
|
-
const empty = document.createElement('div');
|
|
126
|
-
empty.className = 'group-picker__empty';
|
|
127
|
-
empty.innerHTML = `
|
|
128
|
-
<span class="icon icon-search" aria-hidden="true"></span>
|
|
129
|
-
<span>${this.options.emptyLabel}</span>
|
|
130
|
-
`;
|
|
131
|
-
this.listEl.appendChild(empty);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
private createGroupElement(
|
|
136
|
-
group: GroupData,
|
|
137
|
-
query: string,
|
|
138
|
-
groupMatches: boolean,
|
|
139
|
-
matchingSubs: SubgroupData[]
|
|
140
|
-
): HTMLElement {
|
|
141
|
-
const subs = group.subgroups ?? [];
|
|
142
|
-
const hasChildren = subs.length > 0;
|
|
143
|
-
|
|
144
|
-
const el = document.createElement('div');
|
|
145
|
-
el.className = 'group-picker__group';
|
|
146
|
-
el.dataset.groupId = group.id;
|
|
147
|
-
if (!hasChildren) el.classList.add('is-leaf');
|
|
148
|
-
|
|
149
|
-
const isExpanded = hasChildren && (
|
|
150
|
-
this.expandedGroups.has(group.id) ||
|
|
151
|
-
(query.length > 0 && matchingSubs.length > 0)
|
|
152
|
-
);
|
|
153
|
-
const isParentSelected = this.selectedParents.has(group.id);
|
|
154
|
-
|
|
155
|
-
if (isExpanded) el.classList.add('is-expanded');
|
|
156
|
-
if (isParentSelected) el.classList.add('is-selected');
|
|
157
|
-
|
|
158
|
-
// Header row
|
|
159
|
-
const header = document.createElement('div');
|
|
160
|
-
header.className = 'group-picker__group-header';
|
|
161
|
-
|
|
162
|
-
const label = document.createElement('span');
|
|
163
|
-
label.className = 'group-picker__group-label';
|
|
164
|
-
label.innerHTML = query && groupMatches
|
|
165
|
-
? this.highlightText(group.label, query)
|
|
166
|
-
: escapeHtml(group.label);
|
|
167
|
-
|
|
168
|
-
if (hasChildren) {
|
|
169
|
-
// Chevron — Basix font icon
|
|
170
|
-
const chevron = document.createElement('span');
|
|
171
|
-
chevron.className = 'icon icon-navigate_next group-picker__chevron';
|
|
172
|
-
chevron.setAttribute('aria-hidden', 'true');
|
|
173
|
-
|
|
174
|
-
// Count — Basix badge
|
|
175
|
-
const count = document.createElement('span');
|
|
176
|
-
count.className = 'badge badge-sm';
|
|
177
|
-
count.textContent = `${subs.length}`;
|
|
178
|
-
|
|
179
|
-
// Action button — Basix button, button-primary when selected
|
|
180
|
-
const actionBtn = document.createElement('button');
|
|
181
|
-
actionBtn.className = 'group-picker__group-action';
|
|
182
|
-
if (isParentSelected) {
|
|
183
|
-
actionBtn.classList.add('button-primary');
|
|
184
|
-
actionBtn.textContent = this.options.deselectLabel;
|
|
185
|
-
} else {
|
|
186
|
-
actionBtn.textContent = this.options.selectAllLabel;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
actionBtn.addEventListener('click', (e) => {
|
|
190
|
-
e.stopPropagation();
|
|
191
|
-
this.toggleParentGroup(group.id);
|
|
192
|
-
}, { signal: this.abortController.signal });
|
|
193
|
-
|
|
194
|
-
header.append(chevron, label, count, actionBtn);
|
|
195
|
-
|
|
196
|
-
header.addEventListener('click', () => {
|
|
197
|
-
this.toggleExpand(group.id);
|
|
198
|
-
}, { signal: this.abortController.signal });
|
|
199
|
-
|
|
200
|
-
// Subgroups — Basix .chips container
|
|
201
|
-
const subsContainer = document.createElement('div');
|
|
202
|
-
subsContainer.className = 'group-picker__subgroups';
|
|
203
|
-
|
|
204
|
-
const subsList = document.createElement('div');
|
|
205
|
-
subsList.className = 'chips group-picker__subgroup-list';
|
|
206
|
-
|
|
207
|
-
const displaySubs = query && !groupMatches ? matchingSubs : subs;
|
|
208
|
-
|
|
209
|
-
for (const sub of displaySubs) {
|
|
210
|
-
// Subgroup chip — Basix .chip.clickable
|
|
211
|
-
const subEl = document.createElement('span');
|
|
212
|
-
subEl.className = 'chip clickable group-picker__subgroup';
|
|
213
|
-
subEl.dataset.subId = sub.id;
|
|
214
|
-
subEl.innerHTML = query ? this.highlightText(sub.label, query) : escapeHtml(sub.label);
|
|
215
|
-
|
|
216
|
-
const isSubSelected = this.selectedSubs.get(group.id)?.has(sub.id) ?? false;
|
|
217
|
-
if (isSubSelected) subEl.classList.add('is-selected');
|
|
218
|
-
if (isParentSelected) subEl.classList.add('is-disabled');
|
|
219
|
-
|
|
220
|
-
subEl.addEventListener('click', (e) => {
|
|
221
|
-
e.stopPropagation();
|
|
222
|
-
if (!isParentSelected) {
|
|
223
|
-
this.toggleSubgroup(group.id, sub.id);
|
|
224
|
-
}
|
|
225
|
-
}, { signal: this.abortController.signal });
|
|
226
|
-
|
|
227
|
-
subsList.appendChild(subEl);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
subsContainer.appendChild(subsList);
|
|
231
|
-
el.append(header, subsContainer);
|
|
232
|
-
|
|
233
|
-
if (isExpanded) {
|
|
234
|
-
requestAnimationFrame(() => {
|
|
235
|
-
subsContainer.style.height = subsContainer.scrollHeight + 'px';
|
|
236
|
-
subsContainer.addEventListener('transitionend', () => {
|
|
237
|
-
subsContainer.style.height = 'auto';
|
|
238
|
-
}, { once: true });
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
} else {
|
|
242
|
-
// Leaf group — Basix font icon check mark
|
|
243
|
-
const checkEl = document.createElement('span');
|
|
244
|
-
checkEl.className = 'icon icon-check group-picker__leaf-check';
|
|
245
|
-
checkEl.setAttribute('aria-hidden', 'true');
|
|
246
|
-
|
|
247
|
-
header.append(label, checkEl);
|
|
248
|
-
|
|
249
|
-
header.addEventListener('click', () => {
|
|
250
|
-
this.toggleParentGroup(group.id);
|
|
251
|
-
}, { signal: this.abortController.signal });
|
|
252
|
-
|
|
253
|
-
el.appendChild(header);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return el;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
private renderSelection(): void {
|
|
260
|
-
this.selectionEl.innerHTML = '';
|
|
261
|
-
|
|
262
|
-
for (const groupId of this.selectedParents) {
|
|
263
|
-
const group = this.data.find(g => g.id === groupId);
|
|
264
|
-
if (!group) continue;
|
|
265
|
-
this.selectionEl.appendChild(
|
|
266
|
-
this.createChip(group.label, true, () => this.toggleParentGroup(groupId))
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
for (const [groupId, subs] of this.selectedSubs) {
|
|
271
|
-
const group = this.data.find(g => g.id === groupId);
|
|
272
|
-
if (!group) continue;
|
|
273
|
-
for (const subId of subs) {
|
|
274
|
-
const sub = group.subgroups?.find(s => s.id === subId);
|
|
275
|
-
if (!sub) continue;
|
|
276
|
-
this.selectionEl.appendChild(
|
|
277
|
-
this.createChip(sub.label, false, () => this.toggleSubgroup(groupId, subId))
|
|
278
|
-
);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Basix .chip.closeable structure
|
|
284
|
-
private createChip(label: string, isParent: boolean, onRemove: () => void): HTMLElement {
|
|
285
|
-
const chip = document.createElement('span');
|
|
286
|
-
chip.className = isParent
|
|
287
|
-
? 'chip closeable group-picker__chip--parent'
|
|
288
|
-
: 'chip closeable';
|
|
289
|
-
|
|
290
|
-
const btn = document.createElement('button');
|
|
291
|
-
btn.setAttribute('aria-label', `${label} entfernen`);
|
|
292
|
-
btn.innerHTML = `<span class="icon icon-close"></span>`;
|
|
293
|
-
btn.addEventListener('click', (e) => {
|
|
294
|
-
e.stopPropagation();
|
|
295
|
-
onRemove();
|
|
296
|
-
}, { signal: this.abortController.signal });
|
|
297
|
-
|
|
298
|
-
chip.append(document.createTextNode(label), btn);
|
|
299
|
-
return chip;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// State management
|
|
303
|
-
|
|
304
|
-
private toggleParentGroup(groupId: string): void {
|
|
305
|
-
if (this.selectedParents.has(groupId)) {
|
|
306
|
-
this.selectedParents.delete(groupId);
|
|
307
|
-
} else {
|
|
308
|
-
this.selectedParents.add(groupId);
|
|
309
|
-
this.selectedSubs.delete(groupId);
|
|
310
|
-
}
|
|
311
|
-
this.refresh();
|
|
312
|
-
this.emitChange();
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
private toggleSubgroup(groupId: string, subId: string): void {
|
|
316
|
-
if (!this.selectedSubs.has(groupId)) {
|
|
317
|
-
this.selectedSubs.set(groupId, new Set());
|
|
318
|
-
}
|
|
319
|
-
const subs = this.selectedSubs.get(groupId)!;
|
|
320
|
-
|
|
321
|
-
if (subs.has(subId)) {
|
|
322
|
-
subs.delete(subId);
|
|
323
|
-
if (subs.size === 0) this.selectedSubs.delete(groupId);
|
|
324
|
-
} else {
|
|
325
|
-
subs.add(subId);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const group = this.data.find(g => g.id === groupId);
|
|
329
|
-
if (group && subs.size === (group.subgroups ?? []).length) {
|
|
330
|
-
this.selectedSubs.delete(groupId);
|
|
331
|
-
this.selectedParents.add(groupId);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
this.refresh();
|
|
335
|
-
this.emitChange();
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
private toggleExpand(groupId: string): void {
|
|
339
|
-
const groupEl = this.listEl.querySelector(
|
|
340
|
-
`[data-group-id="${groupId}"]`
|
|
341
|
-
) as HTMLElement | null;
|
|
342
|
-
const subsEl = groupEl?.querySelector('.group-picker__subgroups') as HTMLElement | null;
|
|
343
|
-
|
|
344
|
-
if (this.expandedGroups.has(groupId)) {
|
|
345
|
-
this.expandedGroups.delete(groupId);
|
|
346
|
-
groupEl?.classList.remove('is-expanded');
|
|
347
|
-
if (subsEl) {
|
|
348
|
-
subsEl.style.height = subsEl.scrollHeight + 'px';
|
|
349
|
-
requestAnimationFrame(() => {
|
|
350
|
-
subsEl.style.height = '0';
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
} else {
|
|
354
|
-
this.expandedGroups.add(groupId);
|
|
355
|
-
groupEl?.classList.add('is-expanded');
|
|
356
|
-
if (subsEl) {
|
|
357
|
-
subsEl.style.height = subsEl.scrollHeight + 'px';
|
|
358
|
-
subsEl.addEventListener('transitionend', () => {
|
|
359
|
-
if (this.expandedGroups.has(groupId)) {
|
|
360
|
-
subsEl.style.height = 'auto';
|
|
361
|
-
}
|
|
362
|
-
}, { once: true });
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
private refresh(): void {
|
|
368
|
-
this.renderGroups();
|
|
369
|
-
this.renderSelection();
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
private attachEvents(): void {
|
|
373
|
-
let debounceTimer: ReturnType<typeof setTimeout>;
|
|
374
|
-
this.searchInput.addEventListener('input', () => {
|
|
375
|
-
clearTimeout(debounceTimer);
|
|
376
|
-
debounceTimer = setTimeout(() => {
|
|
377
|
-
this.searchQuery = this.searchInput.value;
|
|
378
|
-
this.renderGroups();
|
|
379
|
-
}, 120);
|
|
380
|
-
}, { signal: this.abortController.signal });
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
private emitChange(): void {
|
|
384
|
-
const selection = this.getSelection();
|
|
385
|
-
this.options.onSelectionChange(selection);
|
|
386
|
-
this.container.dispatchEvent(new CustomEvent('group-picker-change', {
|
|
387
|
-
detail: selection,
|
|
388
|
-
bubbles: true,
|
|
389
|
-
}));
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
private highlightText(text: string, query: string): string {
|
|
393
|
-
const safeText = escapeHtml(text);
|
|
394
|
-
if (!query) return safeText;
|
|
395
|
-
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
396
|
-
const regex = new RegExp(`(${escapedQuery})`, 'gi');
|
|
397
|
-
return safeText.replace(regex, '<mark>$1</mark>');
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// Public API
|
|
401
|
-
|
|
402
|
-
public getSelection(): GroupPickerSelection {
|
|
403
|
-
const parentGroups = [...this.selectedParents];
|
|
404
|
-
const subgroups: { groupId: string; subgroupId: string }[] = [];
|
|
405
|
-
for (const [groupId, subs] of this.selectedSubs) {
|
|
406
|
-
for (const subId of subs) {
|
|
407
|
-
subgroups.push({ groupId, subgroupId: subId });
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
return { parentGroups, subgroups };
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
public clearSelection(): void {
|
|
414
|
-
this.selectedParents.clear();
|
|
415
|
-
this.selectedSubs.clear();
|
|
416
|
-
this.refresh();
|
|
417
|
-
this.emitChange();
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
public setSelection(selection: GroupPickerSelection): void {
|
|
421
|
-
this.selectedParents = new Set(selection.parentGroups);
|
|
422
|
-
this.selectedSubs.clear();
|
|
423
|
-
for (const { groupId, subgroupId } of selection.subgroups) {
|
|
424
|
-
if (!this.selectedSubs.has(groupId)) {
|
|
425
|
-
this.selectedSubs.set(groupId, new Set());
|
|
426
|
-
}
|
|
427
|
-
this.selectedSubs.get(groupId)!.add(subgroupId);
|
|
428
|
-
}
|
|
429
|
-
this.refresh();
|
|
430
|
-
this.emitChange();
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
public expandAll(): void {
|
|
434
|
-
this.data.forEach(g => this.expandedGroups.add(g.id));
|
|
435
|
-
this.renderGroups();
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
public collapseAll(): void {
|
|
439
|
-
this.expandedGroups.clear();
|
|
440
|
-
this.renderGroups();
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
public destroy(): void {
|
|
444
|
-
this.abortController.abort();
|
|
445
|
-
this.container.innerHTML = '';
|
|
446
|
-
this.container.classList.remove('group-picker');
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
export { GroupPicker };
|
|
451
|
-
export type { GroupData, SubgroupData, GroupPickerSelection, GroupPickerOptions };
|