@dodlhuat/basix 1.2.0 → 1.2.1
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 +56 -1
- package/css/accordion.scss +86 -87
- package/css/alert.scss +137 -137
- package/css/button.scss +48 -0
- package/css/calendar.scss +957 -0
- package/css/card.scss +65 -65
- package/css/chart.scss +270 -157
- package/css/chat-bubbles.scss +134 -68
- package/css/chips.scss +109 -19
- package/css/colors.scss +32 -32
- package/css/datepicker.scss +336 -336
- package/css/defaults.scss +90 -90
- package/css/docs.scss +529 -0
- package/css/editor.scss +36 -0
- package/css/file-uploader.scss +1 -1
- package/css/flyout-menu.scss +361 -361
- package/css/form.scss +0 -15
- package/css/gallery.scss +65 -6
- package/css/grid.scss +41 -40
- package/css/group-picker.scss +345 -0
- package/css/guitar-chords.css +250 -250
- package/css/icons.scss +330 -330
- package/css/parameters.scss +3 -3
- package/css/placeholder.scss +33 -33
- package/css/popover.scss +206 -0
- package/css/progress.scss +76 -32
- package/css/properties.scss +51 -36
- package/css/push-menu.scss +302 -174
- package/css/reset.scss +39 -39
- package/css/scrollbar.scss +62 -5
- package/css/sidebar-nav.scss +92 -0
- package/css/spinner.scss +65 -65
- package/css/stepper.scss +48 -12
- package/css/style.css +3159 -254
- package/css/style.css.map +1 -1
- package/css/style.min.css +1 -1
- package/css/style.scss +51 -45
- package/css/table.scss +199 -199
- package/css/tabs.scss +154 -123
- package/css/timeline.scss +83 -38
- package/css/timepicker.scss +100 -5
- package/css/toast.scss +81 -81
- package/css/virtual-dropdown.scss +35 -29
- package/js/calendar.js +532 -0
- package/js/calendar.ts +706 -0
- package/js/chart.js +573 -257
- package/js/chart.ts +692 -0
- package/js/code-viewer.js +10 -10
- package/js/code-viewer.ts +188 -188
- package/js/datepicker.ts +627 -627
- package/js/docs-nav.js +204 -0
- package/js/dropdown.ts +179 -179
- package/js/editor.js +50 -6
- package/js/editor.ts +483 -444
- package/js/file-uploader.js +1 -0
- package/js/file-uploader.ts +1 -0
- package/js/flyout-menu.js +14 -14
- package/js/flyout-menu.ts +249 -249
- package/js/form-builder.js +106 -106
- package/js/gallery.js +14 -8
- package/js/gallery.ts +245 -236
- package/js/group-picker.js +342 -0
- package/js/group-picker.ts +447 -0
- package/js/guitar-chords.js +268 -268
- package/js/lazy-loader.js +121 -121
- package/js/modal.ts +166 -166
- package/js/popover.js +163 -0
- package/js/popover.ts +219 -0
- package/js/position.js +108 -0
- package/js/position.ts +111 -0
- package/js/push-menu.js +113 -0
- package/js/push-menu.ts +284 -145
- package/js/request.js +50 -50
- package/js/scroll.ts +47 -47
- package/js/scrollbar.js +13 -0
- package/js/scrollbar.ts +324 -307
- package/js/select.ts +216 -216
- package/js/sidebar-nav.js +41 -0
- package/js/sidebar-nav.ts +66 -0
- package/js/table.ts +452 -452
- package/js/tabs.ts +279 -279
- package/js/theme.js +17 -6
- package/js/theme.ts +234 -224
- package/js/toast.ts +137 -137
- package/js/tooltip.js +6 -60
- package/js/tooltip.ts +184 -251
- package/js/tsconfig.json +18 -18
- package/js/utils.ts +83 -83
- package/js/virtual-dropdown.js +25 -25
- package/js/virtual-dropdown.ts +365 -365
- package/package.json +39 -39
- package/js/index.js +0 -816
- package/js/index.ts +0 -987
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
class GroupPicker {
|
|
2
|
+
constructor(selector, data, options = {}) {
|
|
3
|
+
// State
|
|
4
|
+
this.selectedParents = new Set();
|
|
5
|
+
this.selectedSubs = new Map();
|
|
6
|
+
this.expandedGroups = new Set();
|
|
7
|
+
this.searchQuery = '';
|
|
8
|
+
const el = typeof selector === 'string'
|
|
9
|
+
? document.querySelector(selector)
|
|
10
|
+
: selector;
|
|
11
|
+
if (!el)
|
|
12
|
+
throw new Error(`GroupPicker: Element not found for "${selector}"`);
|
|
13
|
+
this.container = el;
|
|
14
|
+
this.data = data;
|
|
15
|
+
this.abortController = new AbortController();
|
|
16
|
+
this.options = {
|
|
17
|
+
onSelectionChange: options.onSelectionChange ?? (() => { }),
|
|
18
|
+
searchPlaceholder: options.searchPlaceholder ?? 'Gruppen durchsuchen...',
|
|
19
|
+
selectAllLabel: options.selectAllLabel ?? 'Alle',
|
|
20
|
+
deselectLabel: options.deselectLabel ?? 'Abwählen',
|
|
21
|
+
emptyLabel: options.emptyLabel ?? 'Keine Ergebnisse',
|
|
22
|
+
selectionPlaceholder: options.selectionPlaceholder ?? 'Noch keine Auswahl getroffen',
|
|
23
|
+
};
|
|
24
|
+
this.init();
|
|
25
|
+
}
|
|
26
|
+
init() {
|
|
27
|
+
this.container.classList.add('group-picker');
|
|
28
|
+
this.render();
|
|
29
|
+
this.attachEvents();
|
|
30
|
+
}
|
|
31
|
+
render() {
|
|
32
|
+
this.container.innerHTML = '';
|
|
33
|
+
// Selection summary — Basix .chips container
|
|
34
|
+
this.selectionEl = document.createElement('div');
|
|
35
|
+
this.selectionEl.className = 'chips group-picker__selection';
|
|
36
|
+
this.selectionEl.dataset.placeholder = this.options.selectionPlaceholder;
|
|
37
|
+
// Search — Basix form input with font icon overlay
|
|
38
|
+
const searchWrap = document.createElement('div');
|
|
39
|
+
searchWrap.className = 'group-picker__search';
|
|
40
|
+
searchWrap.innerHTML = `
|
|
41
|
+
<span class="icon icon-search group-picker__search-icon" aria-hidden="true"></span>
|
|
42
|
+
<input type="text" placeholder="${this.options.searchPlaceholder}" />
|
|
43
|
+
`;
|
|
44
|
+
this.searchInput = searchWrap.querySelector('input');
|
|
45
|
+
// List
|
|
46
|
+
this.listEl = document.createElement('div');
|
|
47
|
+
this.listEl.className = 'group-picker__list';
|
|
48
|
+
this.container.append(this.selectionEl, searchWrap, this.listEl);
|
|
49
|
+
this.renderGroups();
|
|
50
|
+
this.renderSelection();
|
|
51
|
+
}
|
|
52
|
+
renderGroups() {
|
|
53
|
+
this.listEl.innerHTML = '';
|
|
54
|
+
const query = this.searchQuery.toLowerCase().trim();
|
|
55
|
+
let visibleCount = 0;
|
|
56
|
+
for (const group of this.data) {
|
|
57
|
+
const subs = group.subgroups ?? [];
|
|
58
|
+
const groupMatches = group.label.toLowerCase().includes(query);
|
|
59
|
+
const matchingSubs = subs.filter(s => s.label.toLowerCase().includes(query));
|
|
60
|
+
if (!groupMatches && matchingSubs.length === 0 && query)
|
|
61
|
+
continue;
|
|
62
|
+
visibleCount++;
|
|
63
|
+
const groupEl = this.createGroupElement(group, query, groupMatches, matchingSubs);
|
|
64
|
+
this.listEl.appendChild(groupEl);
|
|
65
|
+
}
|
|
66
|
+
if (visibleCount === 0) {
|
|
67
|
+
const empty = document.createElement('div');
|
|
68
|
+
empty.className = 'group-picker__empty';
|
|
69
|
+
empty.innerHTML = `
|
|
70
|
+
<span class="icon icon-search" aria-hidden="true"></span>
|
|
71
|
+
<span>${this.options.emptyLabel}</span>
|
|
72
|
+
`;
|
|
73
|
+
this.listEl.appendChild(empty);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
createGroupElement(group, query, groupMatches, matchingSubs) {
|
|
77
|
+
const subs = group.subgroups ?? [];
|
|
78
|
+
const hasChildren = subs.length > 0;
|
|
79
|
+
const el = document.createElement('div');
|
|
80
|
+
el.className = 'group-picker__group';
|
|
81
|
+
el.dataset.groupId = group.id;
|
|
82
|
+
if (!hasChildren)
|
|
83
|
+
el.classList.add('is-leaf');
|
|
84
|
+
const isExpanded = hasChildren && (this.expandedGroups.has(group.id) ||
|
|
85
|
+
(query.length > 0 && matchingSubs.length > 0));
|
|
86
|
+
const isParentSelected = this.selectedParents.has(group.id);
|
|
87
|
+
if (isExpanded)
|
|
88
|
+
el.classList.add('is-expanded');
|
|
89
|
+
if (isParentSelected)
|
|
90
|
+
el.classList.add('is-selected');
|
|
91
|
+
// Header row
|
|
92
|
+
const header = document.createElement('div');
|
|
93
|
+
header.className = 'group-picker__group-header';
|
|
94
|
+
const label = document.createElement('span');
|
|
95
|
+
label.className = 'group-picker__group-label';
|
|
96
|
+
label.innerHTML = query && groupMatches
|
|
97
|
+
? this.highlightText(group.label, query)
|
|
98
|
+
: group.label;
|
|
99
|
+
if (hasChildren) {
|
|
100
|
+
// Chevron — Basix font icon
|
|
101
|
+
const chevron = document.createElement('span');
|
|
102
|
+
chevron.className = 'icon icon-navigate_next group-picker__chevron';
|
|
103
|
+
chevron.setAttribute('aria-hidden', 'true');
|
|
104
|
+
// Count — Basix badge
|
|
105
|
+
const count = document.createElement('span');
|
|
106
|
+
count.className = 'badge badge-sm';
|
|
107
|
+
count.textContent = `${subs.length}`;
|
|
108
|
+
// Action button — Basix button, button-primary when selected
|
|
109
|
+
const actionBtn = document.createElement('button');
|
|
110
|
+
actionBtn.className = 'group-picker__group-action';
|
|
111
|
+
if (isParentSelected) {
|
|
112
|
+
actionBtn.classList.add('button-primary');
|
|
113
|
+
actionBtn.textContent = this.options.deselectLabel;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
actionBtn.textContent = this.options.selectAllLabel;
|
|
117
|
+
}
|
|
118
|
+
actionBtn.addEventListener('click', (e) => {
|
|
119
|
+
e.stopPropagation();
|
|
120
|
+
this.toggleParentGroup(group.id);
|
|
121
|
+
}, { signal: this.abortController.signal });
|
|
122
|
+
header.append(chevron, label, count, actionBtn);
|
|
123
|
+
header.addEventListener('click', () => {
|
|
124
|
+
this.toggleExpand(group.id);
|
|
125
|
+
}, { signal: this.abortController.signal });
|
|
126
|
+
// Subgroups — Basix .chips container
|
|
127
|
+
const subsContainer = document.createElement('div');
|
|
128
|
+
subsContainer.className = 'group-picker__subgroups';
|
|
129
|
+
const subsList = document.createElement('div');
|
|
130
|
+
subsList.className = 'chips group-picker__subgroup-list';
|
|
131
|
+
const displaySubs = query && !groupMatches ? matchingSubs : subs;
|
|
132
|
+
for (const sub of displaySubs) {
|
|
133
|
+
// Subgroup chip — Basix .chip.clickable
|
|
134
|
+
const subEl = document.createElement('span');
|
|
135
|
+
subEl.className = 'chip clickable group-picker__subgroup';
|
|
136
|
+
subEl.dataset.subId = sub.id;
|
|
137
|
+
subEl.innerHTML = query ? this.highlightText(sub.label, query) : sub.label;
|
|
138
|
+
const isSubSelected = this.selectedSubs.get(group.id)?.has(sub.id) ?? false;
|
|
139
|
+
if (isSubSelected)
|
|
140
|
+
subEl.classList.add('is-selected');
|
|
141
|
+
if (isParentSelected)
|
|
142
|
+
subEl.classList.add('is-disabled');
|
|
143
|
+
subEl.addEventListener('click', (e) => {
|
|
144
|
+
e.stopPropagation();
|
|
145
|
+
if (!isParentSelected) {
|
|
146
|
+
this.toggleSubgroup(group.id, sub.id);
|
|
147
|
+
}
|
|
148
|
+
}, { signal: this.abortController.signal });
|
|
149
|
+
subsList.appendChild(subEl);
|
|
150
|
+
}
|
|
151
|
+
subsContainer.appendChild(subsList);
|
|
152
|
+
el.append(header, subsContainer);
|
|
153
|
+
if (isExpanded) {
|
|
154
|
+
requestAnimationFrame(() => {
|
|
155
|
+
subsContainer.style.height = subsContainer.scrollHeight + 'px';
|
|
156
|
+
subsContainer.addEventListener('transitionend', () => {
|
|
157
|
+
subsContainer.style.height = 'auto';
|
|
158
|
+
}, { once: true });
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
// Leaf group — Basix font icon check mark
|
|
164
|
+
const checkEl = document.createElement('span');
|
|
165
|
+
checkEl.className = 'icon icon-check group-picker__leaf-check';
|
|
166
|
+
checkEl.setAttribute('aria-hidden', 'true');
|
|
167
|
+
header.append(label, checkEl);
|
|
168
|
+
header.addEventListener('click', () => {
|
|
169
|
+
this.toggleParentGroup(group.id);
|
|
170
|
+
}, { signal: this.abortController.signal });
|
|
171
|
+
el.appendChild(header);
|
|
172
|
+
}
|
|
173
|
+
return el;
|
|
174
|
+
}
|
|
175
|
+
renderSelection() {
|
|
176
|
+
this.selectionEl.innerHTML = '';
|
|
177
|
+
for (const groupId of this.selectedParents) {
|
|
178
|
+
const group = this.data.find(g => g.id === groupId);
|
|
179
|
+
if (!group)
|
|
180
|
+
continue;
|
|
181
|
+
this.selectionEl.appendChild(this.createChip(group.label, true, () => this.toggleParentGroup(groupId)));
|
|
182
|
+
}
|
|
183
|
+
for (const [groupId, subs] of this.selectedSubs) {
|
|
184
|
+
const group = this.data.find(g => g.id === groupId);
|
|
185
|
+
if (!group)
|
|
186
|
+
continue;
|
|
187
|
+
for (const subId of subs) {
|
|
188
|
+
const sub = group.subgroups?.find(s => s.id === subId);
|
|
189
|
+
if (!sub)
|
|
190
|
+
continue;
|
|
191
|
+
this.selectionEl.appendChild(this.createChip(sub.label, false, () => this.toggleSubgroup(groupId, subId)));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Basix .chip.closeable structure
|
|
196
|
+
createChip(label, isParent, onRemove) {
|
|
197
|
+
const chip = document.createElement('span');
|
|
198
|
+
chip.className = isParent
|
|
199
|
+
? 'chip closeable group-picker__chip--parent'
|
|
200
|
+
: 'chip closeable';
|
|
201
|
+
const btn = document.createElement('button');
|
|
202
|
+
btn.setAttribute('aria-label', `${label} entfernen`);
|
|
203
|
+
btn.innerHTML = `<span class="icon icon-close"></span>`;
|
|
204
|
+
btn.addEventListener('click', (e) => {
|
|
205
|
+
e.stopPropagation();
|
|
206
|
+
onRemove();
|
|
207
|
+
}, { signal: this.abortController.signal });
|
|
208
|
+
chip.append(document.createTextNode(label), btn);
|
|
209
|
+
return chip;
|
|
210
|
+
}
|
|
211
|
+
// State management
|
|
212
|
+
toggleParentGroup(groupId) {
|
|
213
|
+
if (this.selectedParents.has(groupId)) {
|
|
214
|
+
this.selectedParents.delete(groupId);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
this.selectedParents.add(groupId);
|
|
218
|
+
this.selectedSubs.delete(groupId);
|
|
219
|
+
}
|
|
220
|
+
this.refresh();
|
|
221
|
+
this.emitChange();
|
|
222
|
+
}
|
|
223
|
+
toggleSubgroup(groupId, subId) {
|
|
224
|
+
if (!this.selectedSubs.has(groupId)) {
|
|
225
|
+
this.selectedSubs.set(groupId, new Set());
|
|
226
|
+
}
|
|
227
|
+
const subs = this.selectedSubs.get(groupId);
|
|
228
|
+
if (subs.has(subId)) {
|
|
229
|
+
subs.delete(subId);
|
|
230
|
+
if (subs.size === 0)
|
|
231
|
+
this.selectedSubs.delete(groupId);
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
subs.add(subId);
|
|
235
|
+
}
|
|
236
|
+
const group = this.data.find(g => g.id === groupId);
|
|
237
|
+
if (group && subs.size === (group.subgroups ?? []).length) {
|
|
238
|
+
this.selectedSubs.delete(groupId);
|
|
239
|
+
this.selectedParents.add(groupId);
|
|
240
|
+
}
|
|
241
|
+
this.refresh();
|
|
242
|
+
this.emitChange();
|
|
243
|
+
}
|
|
244
|
+
toggleExpand(groupId) {
|
|
245
|
+
const groupEl = this.listEl.querySelector(`[data-group-id="${groupId}"]`);
|
|
246
|
+
const subsEl = groupEl?.querySelector('.group-picker__subgroups');
|
|
247
|
+
if (this.expandedGroups.has(groupId)) {
|
|
248
|
+
this.expandedGroups.delete(groupId);
|
|
249
|
+
groupEl?.classList.remove('is-expanded');
|
|
250
|
+
if (subsEl) {
|
|
251
|
+
subsEl.style.height = subsEl.scrollHeight + 'px';
|
|
252
|
+
requestAnimationFrame(() => {
|
|
253
|
+
subsEl.style.height = '0';
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
this.expandedGroups.add(groupId);
|
|
259
|
+
groupEl?.classList.add('is-expanded');
|
|
260
|
+
if (subsEl) {
|
|
261
|
+
subsEl.style.height = subsEl.scrollHeight + 'px';
|
|
262
|
+
subsEl.addEventListener('transitionend', () => {
|
|
263
|
+
if (this.expandedGroups.has(groupId)) {
|
|
264
|
+
subsEl.style.height = 'auto';
|
|
265
|
+
}
|
|
266
|
+
}, { once: true });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
refresh() {
|
|
271
|
+
this.renderGroups();
|
|
272
|
+
this.renderSelection();
|
|
273
|
+
}
|
|
274
|
+
attachEvents() {
|
|
275
|
+
let debounceTimer;
|
|
276
|
+
this.searchInput.addEventListener('input', () => {
|
|
277
|
+
clearTimeout(debounceTimer);
|
|
278
|
+
debounceTimer = setTimeout(() => {
|
|
279
|
+
this.searchQuery = this.searchInput.value;
|
|
280
|
+
this.renderGroups();
|
|
281
|
+
}, 120);
|
|
282
|
+
}, { signal: this.abortController.signal });
|
|
283
|
+
}
|
|
284
|
+
emitChange() {
|
|
285
|
+
const selection = this.getSelection();
|
|
286
|
+
this.options.onSelectionChange(selection);
|
|
287
|
+
this.container.dispatchEvent(new CustomEvent('group-picker-change', {
|
|
288
|
+
detail: selection,
|
|
289
|
+
bubbles: true,
|
|
290
|
+
}));
|
|
291
|
+
}
|
|
292
|
+
highlightText(text, query) {
|
|
293
|
+
if (!query)
|
|
294
|
+
return text;
|
|
295
|
+
const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
296
|
+
const regex = new RegExp(`(${escaped})`, 'gi');
|
|
297
|
+
return text.replace(regex, '<mark>$1</mark>');
|
|
298
|
+
}
|
|
299
|
+
// Public API
|
|
300
|
+
getSelection() {
|
|
301
|
+
const parentGroups = [...this.selectedParents];
|
|
302
|
+
const subgroups = [];
|
|
303
|
+
for (const [groupId, subs] of this.selectedSubs) {
|
|
304
|
+
for (const subId of subs) {
|
|
305
|
+
subgroups.push({ groupId, subgroupId: subId });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return { parentGroups, subgroups };
|
|
309
|
+
}
|
|
310
|
+
clearSelection() {
|
|
311
|
+
this.selectedParents.clear();
|
|
312
|
+
this.selectedSubs.clear();
|
|
313
|
+
this.refresh();
|
|
314
|
+
this.emitChange();
|
|
315
|
+
}
|
|
316
|
+
setSelection(selection) {
|
|
317
|
+
this.selectedParents = new Set(selection.parentGroups);
|
|
318
|
+
this.selectedSubs.clear();
|
|
319
|
+
for (const { groupId, subgroupId } of selection.subgroups) {
|
|
320
|
+
if (!this.selectedSubs.has(groupId)) {
|
|
321
|
+
this.selectedSubs.set(groupId, new Set());
|
|
322
|
+
}
|
|
323
|
+
this.selectedSubs.get(groupId).add(subgroupId);
|
|
324
|
+
}
|
|
325
|
+
this.refresh();
|
|
326
|
+
this.emitChange();
|
|
327
|
+
}
|
|
328
|
+
expandAll() {
|
|
329
|
+
this.data.forEach(g => this.expandedGroups.add(g.id));
|
|
330
|
+
this.renderGroups();
|
|
331
|
+
}
|
|
332
|
+
collapseAll() {
|
|
333
|
+
this.expandedGroups.clear();
|
|
334
|
+
this.renderGroups();
|
|
335
|
+
}
|
|
336
|
+
destroy() {
|
|
337
|
+
this.abortController.abort();
|
|
338
|
+
this.container.innerHTML = '';
|
|
339
|
+
this.container.classList.remove('group-picker');
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
export { GroupPicker };
|