@dodlhuat/basix 1.2.0 → 1.2.2

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