@dodlhuat/basix 1.3.2 → 1.3.4
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 +13 -7
- package/css/accordion.scss +0 -5
- package/css/badge.scss +9 -6
- package/css/bottom-sheet.scss +3 -8
- package/css/breadcrumb.scss +6 -15
- package/css/button.scss +63 -16
- package/css/calendar.scss +0 -54
- package/css/card.scss +0 -5
- package/css/carousel.scss +0 -3
- package/css/chart.scss +0 -25
- package/css/chat-bubbles.scss +0 -15
- package/css/checkbox.scss +3 -2
- package/css/chips.scss +13 -7
- package/css/code-viewer.scss +1 -5
- package/css/colors.scss +4 -0
- package/css/context-menu.scss +4 -6
- package/css/datepicker.scss +4 -7
- package/css/docs.scss +0 -8
- package/css/dropdown.scss +15 -1
- package/css/editor.scss +1 -23
- package/css/file-uploader.scss +2 -2
- package/css/flyout-menu.scss +66 -44
- package/css/form.scss +0 -28
- package/css/gallery.scss +2 -3
- package/css/group-picker.scss +5 -35
- package/css/icons.scss +0 -3
- package/css/lightbox.scss +2 -4
- package/css/mixins.scss +8 -0
- package/css/modal.scss +24 -3
- package/css/parameters.scss +6 -1
- package/css/popover.scss +3 -15
- package/css/progress.scss +0 -6
- package/css/push-menu.scss +3 -28
- package/css/radiobutton.scss +2 -1
- package/css/range-slider.scss +1 -7
- package/css/scrollbar.scss +2 -6
- package/css/sidebar-nav.scss +24 -11
- package/css/stepper.scss +0 -4
- package/css/style.css +287 -89
- package/css/style.css.map +1 -1
- package/css/style.min.css +1 -1
- package/css/style.min.css.map +1 -1
- package/css/style.scss +1 -1
- package/css/table.scss +0 -4
- package/css/tabs.scss +49 -2
- package/css/timeline.scss +1 -13
- package/css/timepicker.scss +6 -7
- package/css/toast.scss +1 -1
- package/css/tooltip.scss +1 -5
- package/css/tree.scss +1 -1
- package/css/typography.scss +38 -9
- package/css/virtual-dropdown.scss +3 -28
- package/js/bottom-sheet.d.ts +3 -1
- package/js/bottom-sheet.js +26 -27
- package/js/calendar.d.ts +7 -0
- package/js/calendar.js +14 -33
- package/js/carousel.d.ts +2 -0
- package/js/carousel.js +13 -5
- package/js/chart.d.ts +4 -0
- package/js/chart.js +13 -31
- package/js/code-viewer.d.ts +1 -0
- package/js/code-viewer.js +4 -0
- package/js/context-menu.d.ts +9 -2
- package/js/context-menu.js +17 -14
- package/js/datepicker.d.ts +4 -0
- package/js/datepicker.js +26 -11
- package/js/dropdown.d.ts +4 -3
- package/js/dropdown.js +17 -9
- package/js/editor.d.ts +1 -0
- package/js/editor.js +9 -3
- package/js/file-uploader.d.ts +4 -0
- package/js/file-uploader.js +52 -43
- package/js/flyout-menu.d.ts +5 -3
- package/js/flyout-menu.js +23 -46
- package/js/gallery.d.ts +5 -0
- package/js/gallery.js +44 -26
- package/js/group-picker.d.ts +5 -0
- package/js/group-picker.js +12 -17
- package/js/lightbox.d.ts +3 -0
- package/js/lightbox.js +12 -6
- package/js/modal.d.ts +3 -1
- package/js/modal.js +15 -12
- package/js/popover.d.ts +2 -0
- package/js/popover.js +26 -30
- package/js/position.d.ts +2 -0
- package/js/position.js +1 -5
- package/js/push-menu.d.ts +2 -0
- package/js/push-menu.js +22 -35
- package/js/range-slider.d.ts +1 -0
- package/js/range-slider.js +5 -3
- package/js/scroll.d.ts +2 -0
- package/js/scroll.js +1 -0
- package/js/scrollbar.d.ts +2 -0
- package/js/scrollbar.js +24 -36
- package/js/select.d.ts +1 -0
- package/js/select.js +5 -10
- package/js/sidebar-nav.d.ts +12 -0
- package/js/sidebar-nav.js +47 -0
- package/js/stepper.d.ts +2 -0
- package/js/stepper.js +7 -1
- package/js/table.d.ts +4 -0
- package/js/table.js +15 -22
- package/js/tabs.d.ts +2 -0
- package/js/tabs.js +6 -14
- package/js/theme.d.ts +1 -0
- package/js/theme.js +5 -13
- package/js/timepicker.d.ts +3 -0
- package/js/timepicker.js +81 -67
- package/js/toast.d.ts +3 -0
- package/js/toast.js +24 -15
- package/js/tooltip.d.ts +2 -0
- package/js/tooltip.js +21 -19
- package/js/tree.d.ts +3 -0
- package/js/tree.js +13 -0
- package/js/utils.d.ts +1 -3
- package/js/utils.js +0 -3
- package/js/virtual-dropdown.d.ts +3 -0
- package/js/virtual-dropdown.js +25 -0
- package/package.json +2 -2
package/js/file-uploader.js
CHANGED
|
@@ -1,49 +1,17 @@
|
|
|
1
1
|
import { escapeHtml } from './utils.js';
|
|
2
|
+
/** Drag-and-drop file uploader with progress tracking and XHR-based uploads. */
|
|
2
3
|
class FileUploader {
|
|
4
|
+
container;
|
|
5
|
+
dropZone;
|
|
6
|
+
fileInput;
|
|
7
|
+
fileList;
|
|
8
|
+
uploadBtn;
|
|
9
|
+
files = new Map();
|
|
10
|
+
uploadUrl;
|
|
11
|
+
maxFileSize;
|
|
12
|
+
allowedTypes;
|
|
13
|
+
abortControllers = new Map();
|
|
3
14
|
constructor(elementOrSelector, config = {}) {
|
|
4
|
-
this.files = new Map();
|
|
5
|
-
this.abortControllers = new Map();
|
|
6
|
-
this.preventDefaults = (e) => {
|
|
7
|
-
e.preventDefault();
|
|
8
|
-
e.stopPropagation();
|
|
9
|
-
};
|
|
10
|
-
this.handleDragEnter = () => {
|
|
11
|
-
this.dropZone.classList.add('drag-over');
|
|
12
|
-
};
|
|
13
|
-
this.handleDragLeave = () => {
|
|
14
|
-
this.dropZone.classList.remove('drag-over');
|
|
15
|
-
};
|
|
16
|
-
this.handleDrop = (e) => {
|
|
17
|
-
const droppedFiles = e.dataTransfer?.files;
|
|
18
|
-
if (droppedFiles) {
|
|
19
|
-
this.handleFiles(droppedFiles);
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
this.handleDropZoneClick = () => {
|
|
23
|
-
this.fileInput.click();
|
|
24
|
-
};
|
|
25
|
-
this.handleFileInputChange = (e) => {
|
|
26
|
-
const target = e.target;
|
|
27
|
-
if (target.files) {
|
|
28
|
-
this.handleFiles(target.files);
|
|
29
|
-
target.value = '';
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
this.handleUploadClick = async () => {
|
|
33
|
-
if (this.files.size === 0)
|
|
34
|
-
return;
|
|
35
|
-
this.uploadBtn.disabled = true;
|
|
36
|
-
this.uploadBtn.textContent = 'Uploading...';
|
|
37
|
-
const uploadPromises = Array.from(this.files.values()).map(({ file, element }) => this.uploadFile(file, element));
|
|
38
|
-
const results = await Promise.allSettled(uploadPromises);
|
|
39
|
-
this.uploadBtn.textContent = 'Upload Complete';
|
|
40
|
-
setTimeout(() => {
|
|
41
|
-
this.dispatchUploadCompletedEvent(results);
|
|
42
|
-
this.fileList.innerHTML = '';
|
|
43
|
-
this.files.clear();
|
|
44
|
-
this.updateUploadButton();
|
|
45
|
-
}, 1500);
|
|
46
|
-
};
|
|
47
15
|
const container = typeof elementOrSelector === 'string'
|
|
48
16
|
? document.querySelector(elementOrSelector)
|
|
49
17
|
: elementOrSelector;
|
|
@@ -89,6 +57,47 @@ class FileUploader {
|
|
|
89
57
|
this.fileInput.addEventListener('change', this.handleFileInputChange);
|
|
90
58
|
this.uploadBtn.addEventListener('click', this.handleUploadClick);
|
|
91
59
|
}
|
|
60
|
+
preventDefaults = (e) => {
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
e.stopPropagation();
|
|
63
|
+
};
|
|
64
|
+
handleDragEnter = () => {
|
|
65
|
+
this.dropZone.classList.add('drag-over');
|
|
66
|
+
};
|
|
67
|
+
handleDragLeave = () => {
|
|
68
|
+
this.dropZone.classList.remove('drag-over');
|
|
69
|
+
};
|
|
70
|
+
handleDrop = (e) => {
|
|
71
|
+
const droppedFiles = e.dataTransfer?.files;
|
|
72
|
+
if (droppedFiles) {
|
|
73
|
+
this.handleFiles(droppedFiles);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
handleDropZoneClick = () => {
|
|
77
|
+
this.fileInput.click();
|
|
78
|
+
};
|
|
79
|
+
handleFileInputChange = (e) => {
|
|
80
|
+
const target = e.target;
|
|
81
|
+
if (target.files) {
|
|
82
|
+
this.handleFiles(target.files);
|
|
83
|
+
target.value = '';
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
handleUploadClick = async () => {
|
|
87
|
+
if (this.files.size === 0)
|
|
88
|
+
return;
|
|
89
|
+
this.uploadBtn.disabled = true;
|
|
90
|
+
this.uploadBtn.textContent = 'Uploading...';
|
|
91
|
+
const uploadPromises = Array.from(this.files.values()).map(({ file, element }) => this.uploadFile(file, element));
|
|
92
|
+
const results = await Promise.allSettled(uploadPromises);
|
|
93
|
+
this.uploadBtn.textContent = 'Upload Complete';
|
|
94
|
+
setTimeout(() => {
|
|
95
|
+
this.dispatchUploadCompletedEvent(results);
|
|
96
|
+
this.fileList.innerHTML = '';
|
|
97
|
+
this.files.clear();
|
|
98
|
+
this.updateUploadButton();
|
|
99
|
+
}, 1500);
|
|
100
|
+
};
|
|
92
101
|
handleFiles(fileList) {
|
|
93
102
|
Array.from(fileList).forEach(file => {
|
|
94
103
|
const key = this.fileKey(file);
|
package/js/flyout-menu.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/** Configuration options for the FlyoutMenu. */
|
|
1
2
|
interface FlyoutMenuOptions {
|
|
2
3
|
triggerSelector?: string;
|
|
3
4
|
menuSelector?: string;
|
|
@@ -11,6 +12,7 @@ interface FlyoutMenuOptions {
|
|
|
11
12
|
enableHeader?: boolean;
|
|
12
13
|
enableFooter?: boolean;
|
|
13
14
|
}
|
|
15
|
+
/** Off-canvas flyout navigation with nested submenu support. */
|
|
14
16
|
declare class FlyoutMenu {
|
|
15
17
|
private options;
|
|
16
18
|
private menuTrigger;
|
|
@@ -19,7 +21,7 @@ declare class FlyoutMenu {
|
|
|
19
21
|
private closeBtn;
|
|
20
22
|
private submenuToggles;
|
|
21
23
|
private menuLinks;
|
|
22
|
-
private
|
|
24
|
+
private abortController;
|
|
23
25
|
constructor(options?: FlyoutMenuOptions);
|
|
24
26
|
private init;
|
|
25
27
|
private hydrateMenu;
|
|
@@ -27,8 +29,8 @@ declare class FlyoutMenu {
|
|
|
27
29
|
private renderHeader;
|
|
28
30
|
private renderFooter;
|
|
29
31
|
private bindEvents;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
open: () => void;
|
|
33
|
+
close: () => void;
|
|
32
34
|
private handleSubmenu;
|
|
33
35
|
private handleKeydown;
|
|
34
36
|
setDirection(direction: 'left' | 'right'): void;
|
package/js/flyout-menu.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
/** Off-canvas flyout navigation with nested submenu support. */
|
|
1
2
|
class FlyoutMenu {
|
|
3
|
+
options;
|
|
4
|
+
menuTrigger;
|
|
5
|
+
flyoutMenu;
|
|
6
|
+
flyoutOverlay;
|
|
7
|
+
closeBtn = null;
|
|
8
|
+
submenuToggles = null;
|
|
9
|
+
menuLinks = null;
|
|
10
|
+
abortController = new AbortController();
|
|
2
11
|
constructor(options = {}) {
|
|
3
|
-
this.closeBtn = null;
|
|
4
|
-
this.submenuToggles = null;
|
|
5
|
-
this.menuLinks = null;
|
|
6
|
-
this.submenuHandlers = new Map();
|
|
7
12
|
this.options = {
|
|
8
13
|
triggerSelector: '.menu-trigger',
|
|
9
14
|
menuSelector: '#flyoutMenu',
|
|
@@ -21,10 +26,6 @@ class FlyoutMenu {
|
|
|
21
26
|
this.menuTrigger = document.querySelector(this.options.triggerSelector);
|
|
22
27
|
this.flyoutMenu = document.querySelector(this.options.menuSelector);
|
|
23
28
|
this.flyoutOverlay = document.querySelector(this.options.overlaySelector);
|
|
24
|
-
this.open = this.open.bind(this);
|
|
25
|
-
this.close = this.close.bind(this);
|
|
26
|
-
this.handleSubmenu = this.handleSubmenu.bind(this);
|
|
27
|
-
this.handleKeydown = this.handleKeydown.bind(this);
|
|
28
29
|
this.init();
|
|
29
30
|
}
|
|
30
31
|
init() {
|
|
@@ -54,16 +55,13 @@ class FlyoutMenu {
|
|
|
54
55
|
processListItems(ul) {
|
|
55
56
|
const items = Array.from(ul.children);
|
|
56
57
|
items.forEach((li, index) => {
|
|
57
|
-
// Check if it has a nested UL
|
|
58
58
|
const nestedUl = li.querySelector('ul');
|
|
59
59
|
if (nestedUl) {
|
|
60
60
|
li.classList.add('has-submenu');
|
|
61
61
|
nestedUl.classList.add('submenu');
|
|
62
|
-
// Get text content (excluding nested UL text)
|
|
63
62
|
const textNode = Array.from(li.childNodes).find(node => node.nodeType === Node.TEXT_NODE && node.textContent?.trim() !== '');
|
|
64
63
|
const text = textNode?.textContent?.trim() || 'Menu Item';
|
|
65
64
|
textNode?.remove();
|
|
66
|
-
// Create Toggle Button
|
|
67
65
|
const button = document.createElement('button');
|
|
68
66
|
button.className = 'submenu-toggle';
|
|
69
67
|
button.style.setProperty('--delay', `${(index + 1) * 0.1}s`);
|
|
@@ -74,11 +72,9 @@ class FlyoutMenu {
|
|
|
74
72
|
</svg>
|
|
75
73
|
`;
|
|
76
74
|
li.insertBefore(button, nestedUl);
|
|
77
|
-
// Recursively process nested UL
|
|
78
75
|
this.processListItems(nestedUl);
|
|
79
76
|
}
|
|
80
77
|
else {
|
|
81
|
-
// Leaf node - ensure it has a link
|
|
82
78
|
const link = li.querySelector('a');
|
|
83
79
|
if (link) {
|
|
84
80
|
link.style.setProperty('--delay', `${(index + 1) * 0.1}s`);
|
|
@@ -113,36 +109,30 @@ class FlyoutMenu {
|
|
|
113
109
|
this.flyoutMenu.append(footer);
|
|
114
110
|
}
|
|
115
111
|
bindEvents() {
|
|
116
|
-
|
|
117
|
-
this.menuTrigger?.addEventListener('click', this.open);
|
|
118
|
-
|
|
119
|
-
this.
|
|
120
|
-
this.flyoutOverlay?.addEventListener('click', this.close);
|
|
121
|
-
// Submenus
|
|
112
|
+
const sig = { signal: this.abortController.signal };
|
|
113
|
+
this.menuTrigger?.addEventListener('click', this.open, sig);
|
|
114
|
+
this.closeBtn?.addEventListener('click', this.close, sig);
|
|
115
|
+
this.flyoutOverlay?.addEventListener('click', this.close, sig);
|
|
122
116
|
this.submenuToggles?.forEach(toggle => {
|
|
123
|
-
|
|
124
|
-
this.submenuHandlers.set(toggle, handler);
|
|
125
|
-
toggle.addEventListener('click', handler);
|
|
117
|
+
toggle.addEventListener('click', (e) => this.handleSubmenu(e, toggle), sig);
|
|
126
118
|
});
|
|
127
|
-
// Close on Link Click
|
|
128
119
|
this.menuLinks?.forEach(link => {
|
|
129
|
-
link.addEventListener('click', this.close);
|
|
120
|
+
link.addEventListener('click', this.close, sig);
|
|
130
121
|
});
|
|
131
|
-
|
|
132
|
-
document.addEventListener('keydown', this.handleKeydown);
|
|
122
|
+
document.addEventListener('keydown', this.handleKeydown, sig);
|
|
133
123
|
}
|
|
134
|
-
open() {
|
|
124
|
+
open = () => {
|
|
135
125
|
this.flyoutMenu?.classList.add('is-open');
|
|
136
126
|
this.flyoutOverlay?.classList.add('is-visible');
|
|
137
127
|
document.body.style.overflow = 'hidden';
|
|
138
128
|
this.menuTrigger?.setAttribute('aria-expanded', 'true');
|
|
139
|
-
}
|
|
140
|
-
close() {
|
|
129
|
+
};
|
|
130
|
+
close = () => {
|
|
141
131
|
this.flyoutMenu?.classList.remove('is-open');
|
|
142
132
|
this.flyoutOverlay?.classList.remove('is-visible');
|
|
143
133
|
document.body.style.overflow = '';
|
|
144
134
|
this.menuTrigger?.setAttribute('aria-expanded', 'false');
|
|
145
|
-
}
|
|
135
|
+
};
|
|
146
136
|
handleSubmenu(e, toggle) {
|
|
147
137
|
e.preventDefault();
|
|
148
138
|
e.stopPropagation();
|
|
@@ -151,7 +141,6 @@ class FlyoutMenu {
|
|
|
151
141
|
const parentUl = parentLi?.parentElement;
|
|
152
142
|
if (!parentUl || !parentLi)
|
|
153
143
|
return;
|
|
154
|
-
// Close other submenus at the same level
|
|
155
144
|
const siblings = Array.from(parentUl.children);
|
|
156
145
|
siblings.forEach(sibling => {
|
|
157
146
|
if (sibling !== parentLi) {
|
|
@@ -166,11 +155,11 @@ class FlyoutMenu {
|
|
|
166
155
|
toggle.classList.toggle('active');
|
|
167
156
|
submenu?.classList.toggle('is-open');
|
|
168
157
|
}
|
|
169
|
-
handleKeydown(e) {
|
|
158
|
+
handleKeydown = (e) => {
|
|
170
159
|
if (e.key === 'Escape' && this.flyoutMenu?.classList.contains('is-open')) {
|
|
171
160
|
this.close();
|
|
172
161
|
}
|
|
173
|
-
}
|
|
162
|
+
};
|
|
174
163
|
setDirection(direction) {
|
|
175
164
|
if (!this.flyoutMenu)
|
|
176
165
|
return;
|
|
@@ -182,19 +171,7 @@ class FlyoutMenu {
|
|
|
182
171
|
this.options.direction = direction;
|
|
183
172
|
}
|
|
184
173
|
destroy() {
|
|
185
|
-
this.
|
|
186
|
-
this.closeBtn?.removeEventListener('click', this.close);
|
|
187
|
-
this.flyoutOverlay?.removeEventListener('click', this.close);
|
|
188
|
-
this.submenuToggles?.forEach(toggle => {
|
|
189
|
-
const handler = this.submenuHandlers.get(toggle);
|
|
190
|
-
if (handler)
|
|
191
|
-
toggle.removeEventListener('click', handler);
|
|
192
|
-
});
|
|
193
|
-
this.submenuHandlers.clear();
|
|
194
|
-
this.menuLinks?.forEach(link => {
|
|
195
|
-
link.removeEventListener('click', this.close);
|
|
196
|
-
});
|
|
197
|
-
document.removeEventListener('keydown', this.handleKeydown);
|
|
174
|
+
this.abortController.abort();
|
|
198
175
|
document.body.style.overflow = '';
|
|
199
176
|
}
|
|
200
177
|
}
|
package/js/gallery.d.ts
CHANGED
|
@@ -1,20 +1,25 @@
|
|
|
1
|
+
/** A single image record for MasonryGallery. */
|
|
1
2
|
interface ImageData {
|
|
2
3
|
src: string;
|
|
3
4
|
title: string;
|
|
4
5
|
desc: string;
|
|
5
6
|
}
|
|
7
|
+
/** Configuration options for MasonryGallery. */
|
|
6
8
|
interface MasonryGalleryOptions {
|
|
7
9
|
fetchFunction: () => Promise<ImageData[]>;
|
|
8
10
|
minColumnWidth?: number;
|
|
9
11
|
scrollThreshold?: number;
|
|
10
12
|
loaderSelector?: string;
|
|
11
13
|
reload?: number;
|
|
14
|
+
enableLightbox?: boolean;
|
|
12
15
|
}
|
|
16
|
+
/** Infinite-scroll masonry gallery that distributes images across dynamically sized columns. */
|
|
13
17
|
declare class MasonryGallery {
|
|
14
18
|
private container;
|
|
15
19
|
private readonly loader;
|
|
16
20
|
private options;
|
|
17
21
|
private columns;
|
|
22
|
+
private allImages;
|
|
18
23
|
private isFetching;
|
|
19
24
|
private resizeObserver;
|
|
20
25
|
private abortController;
|
package/js/gallery.js
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
import { escapeHtml } from './utils.js';
|
|
2
|
+
import { Lightbox } from './lightbox.js';
|
|
3
|
+
/** Infinite-scroll masonry gallery that distributes images across dynamically sized columns. */
|
|
2
4
|
class MasonryGallery {
|
|
5
|
+
container;
|
|
6
|
+
loader;
|
|
7
|
+
options;
|
|
8
|
+
columns = [];
|
|
9
|
+
allImages = [];
|
|
10
|
+
isFetching = false;
|
|
11
|
+
resizeObserver = null;
|
|
12
|
+
abortController = null;
|
|
13
|
+
reloaded = 0;
|
|
3
14
|
constructor(containerId, options) {
|
|
4
|
-
this.columns = [];
|
|
5
|
-
this.isFetching = false;
|
|
6
|
-
this.resizeObserver = null;
|
|
7
|
-
this.abortController = null;
|
|
8
|
-
this.reloaded = 0;
|
|
9
|
-
this.handleScroll = () => {
|
|
10
|
-
if (this.isFetching)
|
|
11
|
-
return;
|
|
12
|
-
const rect = this.container.getBoundingClientRect();
|
|
13
|
-
if (rect.bottom > 0 && rect.bottom <= window.innerHeight + this.options.scrollThreshold) {
|
|
14
|
-
this.loadMoreImages();
|
|
15
|
-
}
|
|
16
|
-
};
|
|
17
15
|
const container = document.getElementById(containerId);
|
|
18
16
|
if (!container) {
|
|
19
17
|
throw new Error(`Container with id "${containerId}" not found`);
|
|
@@ -25,6 +23,7 @@ class MasonryGallery {
|
|
|
25
23
|
scrollThreshold: options.scrollThreshold ?? 100,
|
|
26
24
|
reload: 2,
|
|
27
25
|
fetchFunction: options.fetchFunction,
|
|
26
|
+
enableLightbox: options.enableLightbox ?? true,
|
|
28
27
|
};
|
|
29
28
|
this.init();
|
|
30
29
|
}
|
|
@@ -42,6 +41,7 @@ class MasonryGallery {
|
|
|
42
41
|
}
|
|
43
42
|
buildColumns(count) {
|
|
44
43
|
this.container.innerHTML = '';
|
|
44
|
+
this.container.classList.add('masonry-container');
|
|
45
45
|
this.columns = [];
|
|
46
46
|
for (let i = 0; i < count; i++) {
|
|
47
47
|
const col = document.createElement('div');
|
|
@@ -51,18 +51,14 @@ class MasonryGallery {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
addEventListeners() {
|
|
54
|
+
this.abortController = new AbortController();
|
|
55
|
+
const sig = this.abortController.signal;
|
|
54
56
|
let resizeTimeout;
|
|
55
57
|
window.addEventListener("resize", () => {
|
|
56
58
|
clearTimeout(resizeTimeout);
|
|
57
|
-
resizeTimeout = setTimeout(() =>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
});
|
|
61
|
-
this.abortController = new AbortController();
|
|
62
|
-
window.addEventListener("scroll", this.handleScroll, {
|
|
63
|
-
passive: true,
|
|
64
|
-
signal: this.abortController.signal,
|
|
65
|
-
});
|
|
59
|
+
resizeTimeout = setTimeout(() => this.reLayout(), 200);
|
|
60
|
+
}, { signal: sig });
|
|
61
|
+
window.addEventListener("scroll", this.handleScroll, { passive: true, signal: sig });
|
|
66
62
|
}
|
|
67
63
|
reLayout() {
|
|
68
64
|
const items = this.columns.flatMap(col => Array.from(col.children));
|
|
@@ -76,6 +72,14 @@ class MasonryGallery {
|
|
|
76
72
|
}
|
|
77
73
|
items.forEach(item => this.addToShortestColumn(item));
|
|
78
74
|
}
|
|
75
|
+
handleScroll = () => {
|
|
76
|
+
if (this.isFetching)
|
|
77
|
+
return;
|
|
78
|
+
const rect = this.container.getBoundingClientRect();
|
|
79
|
+
if (rect.bottom > 0 && rect.bottom <= window.innerHeight + this.options.scrollThreshold) {
|
|
80
|
+
this.loadMoreImages();
|
|
81
|
+
}
|
|
82
|
+
};
|
|
79
83
|
async loadMoreImages(isAutoFill = false) {
|
|
80
84
|
if (!isAutoFill)
|
|
81
85
|
this.reloaded++;
|
|
@@ -97,8 +101,6 @@ class MasonryGallery {
|
|
|
97
101
|
finally {
|
|
98
102
|
this.isFetching = false;
|
|
99
103
|
this.toggleLoader(false);
|
|
100
|
-
// If the rendered content doesn't fill the viewport, auto-load the next
|
|
101
|
-
// batch without waiting for a scroll event (multi-column layout is shorter)
|
|
102
104
|
requestAnimationFrame(() => {
|
|
103
105
|
const rect = this.container.getBoundingClientRect();
|
|
104
106
|
if (rect.bottom <= window.innerHeight + this.options.scrollThreshold) {
|
|
@@ -113,14 +115,29 @@ class MasonryGallery {
|
|
|
113
115
|
}
|
|
114
116
|
}
|
|
115
117
|
renderImages(imageDataList) {
|
|
118
|
+
const startIndex = this.allImages.length;
|
|
119
|
+
this.allImages.push(...imageDataList);
|
|
116
120
|
// Sort columns by current height so we start filling from the shortest.
|
|
117
121
|
// Then round-robin across them — this avoids the problem where unloaded
|
|
118
122
|
// images (0 height) cause offsetHeight-based distribution to pile all
|
|
119
123
|
// new items into a single column.
|
|
120
124
|
const sorted = [...this.columns].sort((a, b) => a.offsetHeight - b.offsetHeight);
|
|
121
|
-
imageDataList.forEach((data,
|
|
125
|
+
imageDataList.forEach((data, i) => {
|
|
122
126
|
const item = this.createCard(data);
|
|
123
|
-
|
|
127
|
+
if (this.options.enableLightbox) {
|
|
128
|
+
const index = startIndex + i;
|
|
129
|
+
item.addEventListener('click', () => {
|
|
130
|
+
new Lightbox({
|
|
131
|
+
images: this.allImages.map(img => ({
|
|
132
|
+
src: img.src,
|
|
133
|
+
alt: img.title,
|
|
134
|
+
caption: img.desc,
|
|
135
|
+
})),
|
|
136
|
+
startIndex: index,
|
|
137
|
+
}).show();
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
const col = sorted[i % sorted.length];
|
|
124
141
|
col.appendChild(item);
|
|
125
142
|
requestAnimationFrame(() => {
|
|
126
143
|
const img = item.querySelector("img");
|
|
@@ -170,6 +187,7 @@ class MasonryGallery {
|
|
|
170
187
|
this.abortController.abort();
|
|
171
188
|
this.abortController = null;
|
|
172
189
|
}
|
|
190
|
+
this.allImages = [];
|
|
173
191
|
}
|
|
174
192
|
}
|
|
175
193
|
export { MasonryGallery };
|
package/js/group-picker.d.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
/** A single subgroup item within a GroupPicker group. */
|
|
1
2
|
interface SubgroupData {
|
|
2
3
|
id: string;
|
|
3
4
|
label: string;
|
|
4
5
|
}
|
|
6
|
+
/** A group with an optional list of subgroups for GroupPicker. */
|
|
5
7
|
interface GroupData {
|
|
6
8
|
id: string;
|
|
7
9
|
label: string;
|
|
8
10
|
subgroups?: SubgroupData[];
|
|
9
11
|
}
|
|
12
|
+
/** The current selection state returned by GroupPicker. */
|
|
10
13
|
interface GroupPickerSelection {
|
|
11
14
|
parentGroups: string[];
|
|
12
15
|
subgroups: {
|
|
@@ -14,6 +17,7 @@ interface GroupPickerSelection {
|
|
|
14
17
|
subgroupId: string;
|
|
15
18
|
}[];
|
|
16
19
|
}
|
|
20
|
+
/** Configuration options for the GroupPicker component. */
|
|
17
21
|
interface GroupPickerOptions {
|
|
18
22
|
onSelectionChange?: (selection: GroupPickerSelection) => void;
|
|
19
23
|
searchPlaceholder?: string;
|
|
@@ -22,6 +26,7 @@ interface GroupPickerOptions {
|
|
|
22
26
|
emptyLabel?: string;
|
|
23
27
|
selectionPlaceholder?: string;
|
|
24
28
|
}
|
|
29
|
+
/** Searchable picker for selecting groups and their subgroups. */
|
|
25
30
|
declare class GroupPicker {
|
|
26
31
|
private container;
|
|
27
32
|
private data;
|
package/js/group-picker.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { escapeHtml } from './utils.js';
|
|
2
|
+
/** Searchable picker for selecting groups and their subgroups. */
|
|
2
3
|
class GroupPicker {
|
|
4
|
+
container;
|
|
5
|
+
data;
|
|
6
|
+
options;
|
|
7
|
+
abortController;
|
|
8
|
+
selectedParents = new Set();
|
|
9
|
+
selectedSubs = new Map();
|
|
10
|
+
expandedGroups = new Set();
|
|
11
|
+
searchQuery = '';
|
|
12
|
+
searchInput;
|
|
13
|
+
listEl;
|
|
14
|
+
selectionEl;
|
|
3
15
|
constructor(selector, data, options = {}) {
|
|
4
|
-
// State
|
|
5
|
-
this.selectedParents = new Set();
|
|
6
|
-
this.selectedSubs = new Map();
|
|
7
|
-
this.expandedGroups = new Set();
|
|
8
|
-
this.searchQuery = '';
|
|
9
16
|
const el = typeof selector === 'string'
|
|
10
17
|
? document.querySelector(selector)
|
|
11
18
|
: selector;
|
|
@@ -31,11 +38,9 @@ class GroupPicker {
|
|
|
31
38
|
}
|
|
32
39
|
render() {
|
|
33
40
|
this.container.innerHTML = '';
|
|
34
|
-
// Selection summary — Basix .chips container
|
|
35
41
|
this.selectionEl = document.createElement('div');
|
|
36
42
|
this.selectionEl.className = 'chips group-picker__selection';
|
|
37
43
|
this.selectionEl.dataset.placeholder = this.options.selectionPlaceholder;
|
|
38
|
-
// Search — Basix form input with font icon overlay
|
|
39
44
|
const searchWrap = document.createElement('div');
|
|
40
45
|
searchWrap.className = 'group-picker__search';
|
|
41
46
|
searchWrap.innerHTML = `
|
|
@@ -44,7 +49,6 @@ class GroupPicker {
|
|
|
44
49
|
`;
|
|
45
50
|
this.searchInput = searchWrap.querySelector('input');
|
|
46
51
|
this.searchInput.placeholder = this.options.searchPlaceholder;
|
|
47
|
-
// List
|
|
48
52
|
this.listEl = document.createElement('div');
|
|
49
53
|
this.listEl.className = 'group-picker__list';
|
|
50
54
|
this.container.append(this.selectionEl, searchWrap, this.listEl);
|
|
@@ -99,15 +103,12 @@ class GroupPicker {
|
|
|
99
103
|
? this.highlightText(group.label, query)
|
|
100
104
|
: escapeHtml(group.label);
|
|
101
105
|
if (hasChildren) {
|
|
102
|
-
// Chevron — Basix font icon
|
|
103
106
|
const chevron = document.createElement('span');
|
|
104
107
|
chevron.className = 'icon icon-navigate_next group-picker__chevron';
|
|
105
108
|
chevron.setAttribute('aria-hidden', 'true');
|
|
106
|
-
// Count — Basix badge
|
|
107
109
|
const count = document.createElement('span');
|
|
108
110
|
count.className = 'badge badge-sm';
|
|
109
111
|
count.textContent = `${subs.length}`;
|
|
110
|
-
// Action button — Basix button, button-primary when selected
|
|
111
112
|
const actionBtn = document.createElement('button');
|
|
112
113
|
actionBtn.className = 'group-picker__group-action';
|
|
113
114
|
if (isParentSelected) {
|
|
@@ -125,14 +126,12 @@ class GroupPicker {
|
|
|
125
126
|
header.addEventListener('click', () => {
|
|
126
127
|
this.toggleExpand(group.id);
|
|
127
128
|
}, { signal: this.abortController.signal });
|
|
128
|
-
// Subgroups — Basix .chips container
|
|
129
129
|
const subsContainer = document.createElement('div');
|
|
130
130
|
subsContainer.className = 'group-picker__subgroups';
|
|
131
131
|
const subsList = document.createElement('div');
|
|
132
132
|
subsList.className = 'chips group-picker__subgroup-list';
|
|
133
133
|
const displaySubs = query && !groupMatches ? matchingSubs : subs;
|
|
134
134
|
for (const sub of displaySubs) {
|
|
135
|
-
// Subgroup chip — Basix .chip.clickable
|
|
136
135
|
const subEl = document.createElement('span');
|
|
137
136
|
subEl.className = 'chip clickable group-picker__subgroup';
|
|
138
137
|
subEl.dataset.subId = sub.id;
|
|
@@ -162,7 +161,6 @@ class GroupPicker {
|
|
|
162
161
|
}
|
|
163
162
|
}
|
|
164
163
|
else {
|
|
165
|
-
// Leaf group — Basix font icon check mark
|
|
166
164
|
const checkEl = document.createElement('span');
|
|
167
165
|
checkEl.className = 'icon icon-check group-picker__leaf-check';
|
|
168
166
|
checkEl.setAttribute('aria-hidden', 'true');
|
|
@@ -194,7 +192,6 @@ class GroupPicker {
|
|
|
194
192
|
}
|
|
195
193
|
}
|
|
196
194
|
}
|
|
197
|
-
// Basix .chip.closeable structure
|
|
198
195
|
createChip(label, isParent, onRemove) {
|
|
199
196
|
const chip = document.createElement('span');
|
|
200
197
|
chip.className = isParent
|
|
@@ -210,7 +207,6 @@ class GroupPicker {
|
|
|
210
207
|
chip.append(document.createTextNode(label), btn);
|
|
211
208
|
return chip;
|
|
212
209
|
}
|
|
213
|
-
// State management
|
|
214
210
|
toggleParentGroup(groupId) {
|
|
215
211
|
if (this.selectedParents.has(groupId)) {
|
|
216
212
|
this.selectedParents.delete(groupId);
|
|
@@ -299,7 +295,6 @@ class GroupPicker {
|
|
|
299
295
|
const regex = new RegExp(`(${escapedQuery})`, 'gi');
|
|
300
296
|
return safeText.replace(regex, '<mark>$1</mark>');
|
|
301
297
|
}
|
|
302
|
-
// Public API
|
|
303
298
|
getSelection() {
|
|
304
299
|
const parentGroups = [...this.selectedParents];
|
|
305
300
|
const subgroups = [];
|
package/js/lightbox.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
/** A single image entry for the Lightbox gallery. */
|
|
1
2
|
interface LightboxImage {
|
|
2
3
|
src: string;
|
|
3
4
|
alt?: string;
|
|
4
5
|
caption?: string;
|
|
5
6
|
}
|
|
7
|
+
/** Configuration options for a Lightbox instance. */
|
|
6
8
|
interface LightboxOptions {
|
|
7
9
|
src?: string;
|
|
8
10
|
alt?: string;
|
|
@@ -13,6 +15,7 @@ interface LightboxOptions {
|
|
|
13
15
|
onOpen?: () => void;
|
|
14
16
|
onClose?: () => void;
|
|
15
17
|
}
|
|
18
|
+
/** Full-screen image viewer with gallery navigation, zoom, and touch support. */
|
|
16
19
|
declare class Lightbox {
|
|
17
20
|
private images;
|
|
18
21
|
private currentIndex;
|
package/js/lightbox.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
|
+
/** Full-screen image viewer with gallery navigation, zoom, and touch support. */
|
|
1
2
|
class Lightbox {
|
|
3
|
+
images;
|
|
4
|
+
currentIndex;
|
|
5
|
+
closeable;
|
|
6
|
+
onOpen;
|
|
7
|
+
onClose;
|
|
8
|
+
wrapper = null;
|
|
9
|
+
imgEl = null;
|
|
10
|
+
captionEl = null;
|
|
11
|
+
counterEl = null;
|
|
12
|
+
isZoomed = false;
|
|
13
|
+
abortController = new AbortController();
|
|
2
14
|
constructor(options) {
|
|
3
|
-
this.wrapper = null;
|
|
4
|
-
this.imgEl = null;
|
|
5
|
-
this.captionEl = null;
|
|
6
|
-
this.counterEl = null;
|
|
7
|
-
this.isZoomed = false;
|
|
8
|
-
this.abortController = new AbortController();
|
|
9
15
|
if (options.images && options.images.length > 0) {
|
|
10
16
|
this.images = options.images;
|
|
11
17
|
}
|
package/js/modal.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
type ModalType = 'default' | 'success' | 'error' | 'warning' | 'info';
|
|
2
|
+
/** Configuration options for a Modal dialog. */
|
|
2
3
|
interface ModalOptions {
|
|
3
4
|
content: string;
|
|
4
5
|
header?: string;
|
|
@@ -6,6 +7,7 @@ interface ModalOptions {
|
|
|
6
7
|
closeable?: boolean;
|
|
7
8
|
type?: ModalType;
|
|
8
9
|
}
|
|
10
|
+
/** Overlay dialog with optional header, footer, close button, and type variants. */
|
|
9
11
|
declare class Modal {
|
|
10
12
|
private content;
|
|
11
13
|
private readonly header?;
|
|
@@ -17,7 +19,7 @@ declare class Modal {
|
|
|
17
19
|
constructor(options: ModalOptions);
|
|
18
20
|
constructor(content: string, header?: string, footer?: string, closeable?: boolean, type?: ModalType);
|
|
19
21
|
show(): void;
|
|
20
|
-
hide()
|
|
22
|
+
hide: () => void;
|
|
21
23
|
private handleEscape;
|
|
22
24
|
private handleBackgroundClick;
|
|
23
25
|
private buildTemplate;
|