@dodlhuat/basix 1.3.2 → 1.3.3
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 +1 -6
- package/css/bottom-sheet.scss +3 -8
- package/css/breadcrumb.scss +6 -15
- package/css/button.scss +2 -1
- 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 +3 -7
- package/css/code-viewer.scss +1 -5
- package/css/context-menu.scss +4 -6
- package/css/datepicker.scss +4 -7
- package/css/docs.scss +0 -4
- package/css/dropdown.scss +1 -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 +3 -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 +0 -12
- package/css/stepper.scss +0 -4
- package/css/style.css +63 -68
- 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 +0 -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 +3 -3
- 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 +3 -3
- package/js/dropdown.js +6 -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 +3 -0
- package/js/gallery.js +22 -24
- 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 +14 -11
- 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 +2 -0
- package/js/sidebar-nav.js +8 -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/gallery.js
CHANGED
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
import { escapeHtml } from './utils.js';
|
|
2
|
+
/** Infinite-scroll masonry gallery that distributes images across dynamically sized columns. */
|
|
2
3
|
class MasonryGallery {
|
|
4
|
+
container;
|
|
5
|
+
loader;
|
|
6
|
+
options;
|
|
7
|
+
columns = [];
|
|
8
|
+
isFetching = false;
|
|
9
|
+
resizeObserver = null;
|
|
10
|
+
abortController = null;
|
|
11
|
+
reloaded = 0;
|
|
3
12
|
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
13
|
const container = document.getElementById(containerId);
|
|
18
14
|
if (!container) {
|
|
19
15
|
throw new Error(`Container with id "${containerId}" not found`);
|
|
@@ -51,18 +47,14 @@ class MasonryGallery {
|
|
|
51
47
|
}
|
|
52
48
|
}
|
|
53
49
|
addEventListeners() {
|
|
50
|
+
this.abortController = new AbortController();
|
|
51
|
+
const sig = this.abortController.signal;
|
|
54
52
|
let resizeTimeout;
|
|
55
53
|
window.addEventListener("resize", () => {
|
|
56
54
|
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
|
-
});
|
|
55
|
+
resizeTimeout = setTimeout(() => this.reLayout(), 200);
|
|
56
|
+
}, { signal: sig });
|
|
57
|
+
window.addEventListener("scroll", this.handleScroll, { passive: true, signal: sig });
|
|
66
58
|
}
|
|
67
59
|
reLayout() {
|
|
68
60
|
const items = this.columns.flatMap(col => Array.from(col.children));
|
|
@@ -76,6 +68,14 @@ class MasonryGallery {
|
|
|
76
68
|
}
|
|
77
69
|
items.forEach(item => this.addToShortestColumn(item));
|
|
78
70
|
}
|
|
71
|
+
handleScroll = () => {
|
|
72
|
+
if (this.isFetching)
|
|
73
|
+
return;
|
|
74
|
+
const rect = this.container.getBoundingClientRect();
|
|
75
|
+
if (rect.bottom > 0 && rect.bottom <= window.innerHeight + this.options.scrollThreshold) {
|
|
76
|
+
this.loadMoreImages();
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
79
|
async loadMoreImages(isAutoFill = false) {
|
|
80
80
|
if (!isAutoFill)
|
|
81
81
|
this.reloaded++;
|
|
@@ -97,8 +97,6 @@ class MasonryGallery {
|
|
|
97
97
|
finally {
|
|
98
98
|
this.isFetching = false;
|
|
99
99
|
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
100
|
requestAnimationFrame(() => {
|
|
103
101
|
const rect = this.container.getBoundingClientRect();
|
|
104
102
|
if (rect.bottom <= window.innerHeight + this.options.scrollThreshold) {
|
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;
|
package/js/modal.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { sanitizeHtml } from './utils.js';
|
|
2
2
|
const CLOSE_ICON = '<div class="icon icon-close close"></div>';
|
|
3
|
+
/** Overlay dialog with optional header, footer, close button, and type variants. */
|
|
3
4
|
class Modal {
|
|
5
|
+
content;
|
|
6
|
+
header;
|
|
7
|
+
footer;
|
|
8
|
+
closeable;
|
|
9
|
+
type;
|
|
10
|
+
template;
|
|
11
|
+
modalWrapper = null;
|
|
4
12
|
constructor(contentOrOptions, header, footer, closeable = true, type = 'default') {
|
|
5
|
-
this.modalWrapper = null;
|
|
6
13
|
if (typeof contentOrOptions === 'object') {
|
|
7
14
|
this.content = contentOrOptions.content;
|
|
8
15
|
this.header = contentOrOptions.header;
|
|
@@ -18,9 +25,6 @@ class Modal {
|
|
|
18
25
|
this.type = type;
|
|
19
26
|
}
|
|
20
27
|
this.template = this.buildTemplate();
|
|
21
|
-
this.hide = this.hide.bind(this);
|
|
22
|
-
this.handleEscape = this.handleEscape.bind(this);
|
|
23
|
-
this.handleBackgroundClick = this.handleBackgroundClick.bind(this);
|
|
24
28
|
}
|
|
25
29
|
show() {
|
|
26
30
|
this.hide();
|
|
@@ -45,11 +49,10 @@ class Modal {
|
|
|
45
49
|
wrapper.classList.add('is-visible');
|
|
46
50
|
});
|
|
47
51
|
}
|
|
48
|
-
hide() {
|
|
52
|
+
hide = () => {
|
|
49
53
|
const wrapper = this.modalWrapper;
|
|
50
54
|
if (!wrapper)
|
|
51
55
|
return;
|
|
52
|
-
// Remove event listeners
|
|
53
56
|
const closeBtn = wrapper.querySelector('.close');
|
|
54
57
|
closeBtn?.removeEventListener('click', this.hide);
|
|
55
58
|
const background = wrapper.querySelector('.modal-background');
|
|
@@ -63,17 +66,17 @@ class Modal {
|
|
|
63
66
|
this.modalWrapper = null;
|
|
64
67
|
}
|
|
65
68
|
}, 300);
|
|
66
|
-
}
|
|
67
|
-
handleEscape(e) {
|
|
69
|
+
};
|
|
70
|
+
handleEscape = (e) => {
|
|
68
71
|
if (e.key === 'Escape') {
|
|
69
72
|
this.hide();
|
|
70
73
|
}
|
|
71
|
-
}
|
|
72
|
-
handleBackgroundClick(e) {
|
|
74
|
+
};
|
|
75
|
+
handleBackgroundClick = (e) => {
|
|
73
76
|
if (e.target?.classList.contains('modal-background')) {
|
|
74
77
|
this.hide();
|
|
75
78
|
}
|
|
76
|
-
}
|
|
79
|
+
};
|
|
77
80
|
buildTemplate() {
|
|
78
81
|
const parts = ['<div class="modal">'];
|
|
79
82
|
if (this.closeable) {
|
package/js/popover.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Placement } from './position.js';
|
|
|
2
2
|
type PopoverPlacement = Placement | 'auto';
|
|
3
3
|
type PopoverAlign = 'start' | 'center' | 'end';
|
|
4
4
|
type PopoverTrigger = 'click' | 'hover';
|
|
5
|
+
/** Configuration options for a Popover instance. */
|
|
5
6
|
interface PopoverOptions {
|
|
6
7
|
content: string;
|
|
7
8
|
placement?: PopoverPlacement;
|
|
@@ -15,6 +16,7 @@ interface PopoverOptions {
|
|
|
15
16
|
onOpen?: () => void;
|
|
16
17
|
onClose?: () => void;
|
|
17
18
|
}
|
|
19
|
+
/** Anchored popover triggered by click or hover, with auto-placement and optional arrow. */
|
|
18
20
|
declare class Popover {
|
|
19
21
|
private static openPopovers;
|
|
20
22
|
private static idCounter;
|
package/js/popover.js
CHANGED
|
@@ -1,31 +1,16 @@
|
|
|
1
1
|
import { computePosition } from './position.js';
|
|
2
2
|
import { sanitizeHtml } from './utils.js';
|
|
3
|
-
// Must match $arrow in popover.scss
|
|
4
3
|
const ARROW_SIZE = 6;
|
|
4
|
+
/** Anchored popover triggered by click or hover, with auto-placement and optional arrow. */
|
|
5
5
|
class Popover {
|
|
6
|
+
static openPopovers = new Set();
|
|
7
|
+
static idCounter = 0;
|
|
8
|
+
trigger;
|
|
9
|
+
opts;
|
|
10
|
+
popoverEl = null;
|
|
11
|
+
_isOpen = false;
|
|
12
|
+
hoverTimer = null;
|
|
6
13
|
constructor(triggerEl, options) {
|
|
7
|
-
this.popoverEl = null;
|
|
8
|
-
this._isOpen = false;
|
|
9
|
-
this.hoverTimer = null;
|
|
10
|
-
// ── Event handlers ─────────────────────────────────────────────────────────
|
|
11
|
-
this.onClick = () => { this.toggle(); };
|
|
12
|
-
this.onMouseEnter = () => {
|
|
13
|
-
if (this.hoverTimer !== null)
|
|
14
|
-
clearTimeout(this.hoverTimer);
|
|
15
|
-
this.open();
|
|
16
|
-
};
|
|
17
|
-
this.onMouseLeave = () => {
|
|
18
|
-
this.hoverTimer = window.setTimeout(() => this.close(), 120);
|
|
19
|
-
};
|
|
20
|
-
this.onOutsideClick = (e) => {
|
|
21
|
-
const t = e.target;
|
|
22
|
-
if (!this.popoverEl?.contains(t) && !this.trigger.contains(t))
|
|
23
|
-
this.close();
|
|
24
|
-
};
|
|
25
|
-
this.onEscape = (e) => {
|
|
26
|
-
if (e.key === 'Escape')
|
|
27
|
-
this.close();
|
|
28
|
-
};
|
|
29
14
|
const el = typeof triggerEl === 'string'
|
|
30
15
|
? document.querySelector(triggerEl)
|
|
31
16
|
: triggerEl;
|
|
@@ -47,7 +32,6 @@ class Popover {
|
|
|
47
32
|
};
|
|
48
33
|
this.attachTrigger();
|
|
49
34
|
}
|
|
50
|
-
// ── Public API ─────────────────────────────────────────────────────────────
|
|
51
35
|
get isOpen() { return this._isOpen; }
|
|
52
36
|
open() {
|
|
53
37
|
if (this._isOpen)
|
|
@@ -109,7 +93,6 @@ class Popover {
|
|
|
109
93
|
});
|
|
110
94
|
});
|
|
111
95
|
}
|
|
112
|
-
// ── Build ──────────────────────────────────────────────────────────────────
|
|
113
96
|
buildEl() {
|
|
114
97
|
const id = `popover-${++Popover.idCounter}`;
|
|
115
98
|
const el = document.createElement('div');
|
|
@@ -117,8 +100,6 @@ class Popover {
|
|
|
117
100
|
el.id = id;
|
|
118
101
|
el.setAttribute('role', 'dialog');
|
|
119
102
|
el.setAttribute('data-arrow', String(this.opts.arrow));
|
|
120
|
-
// Wrap plain content in .popover-body so it gets proper padding.
|
|
121
|
-
// Skip wrapping when content already uses structured popover elements.
|
|
122
103
|
const hasStructure = /class="popover-(header|body|footer|menu)/.test(this.opts.content);
|
|
123
104
|
const safeContent = sanitizeHtml(this.opts.content);
|
|
124
105
|
el.innerHTML = hasStructure
|
|
@@ -128,7 +109,6 @@ class Popover {
|
|
|
128
109
|
this.trigger.setAttribute('aria-controls', id);
|
|
129
110
|
return el;
|
|
130
111
|
}
|
|
131
|
-
// ── Positioning ────────────────────────────────────────────────────────────
|
|
132
112
|
reposition() {
|
|
133
113
|
if (!this.popoverEl)
|
|
134
114
|
return;
|
|
@@ -145,6 +125,24 @@ class Popover {
|
|
|
145
125
|
this.popoverEl.style.left = `${left}px`;
|
|
146
126
|
this.popoverEl.style.top = `${top}px`;
|
|
147
127
|
}
|
|
128
|
+
onClick = () => { this.toggle(); };
|
|
129
|
+
onMouseEnter = () => {
|
|
130
|
+
if (this.hoverTimer !== null)
|
|
131
|
+
clearTimeout(this.hoverTimer);
|
|
132
|
+
this.open();
|
|
133
|
+
};
|
|
134
|
+
onMouseLeave = () => {
|
|
135
|
+
this.hoverTimer = window.setTimeout(() => this.close(), 120);
|
|
136
|
+
};
|
|
137
|
+
onOutsideClick = (e) => {
|
|
138
|
+
const t = e.target;
|
|
139
|
+
if (!this.popoverEl?.contains(t) && !this.trigger.contains(t))
|
|
140
|
+
this.close();
|
|
141
|
+
};
|
|
142
|
+
onEscape = (e) => {
|
|
143
|
+
if (e.key === 'Escape')
|
|
144
|
+
this.close();
|
|
145
|
+
};
|
|
148
146
|
attachTrigger() {
|
|
149
147
|
if (this.opts.triggerMode === 'click') {
|
|
150
148
|
this.trigger.addEventListener('click', this.onClick);
|
|
@@ -160,6 +158,4 @@ class Popover {
|
|
|
160
158
|
this.trigger.removeEventListener('mouseleave', this.onMouseLeave);
|
|
161
159
|
}
|
|
162
160
|
}
|
|
163
|
-
Popover.openPopovers = new Set();
|
|
164
|
-
Popover.idCounter = 0;
|
|
165
161
|
export { Popover };
|
package/js/position.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
type Placement = 'top' | 'bottom' | 'left' | 'right';
|
|
6
6
|
type Align = 'start' | 'center' | 'end';
|
|
7
|
+
/** Options accepted by `computePosition`. */
|
|
7
8
|
interface PositionOptions {
|
|
8
9
|
placement: Placement | 'auto';
|
|
9
10
|
align?: Align;
|
|
@@ -11,6 +12,7 @@ interface PositionOptions {
|
|
|
11
12
|
margin?: number;
|
|
12
13
|
arrowSize?: number;
|
|
13
14
|
}
|
|
15
|
+
/** Result returned by `computePosition`. */
|
|
14
16
|
interface PositionResult {
|
|
15
17
|
left: number;
|
|
16
18
|
top: number;
|
package/js/position.js
CHANGED
|
@@ -44,7 +44,6 @@ function computePosition(trigger, floating, opts) {
|
|
|
44
44
|
const placement = opts.placement === 'auto'
|
|
45
45
|
? bestPlacement(trigger, floating, offset)
|
|
46
46
|
: maybeFlip(opts.placement, trigger, floating, offset);
|
|
47
|
-
// Main-axis offset
|
|
48
47
|
let left = 0, top = 0;
|
|
49
48
|
switch (placement) {
|
|
50
49
|
case 'top':
|
|
@@ -60,7 +59,6 @@ function computePosition(trigger, floating, opts) {
|
|
|
60
59
|
left = trigger.right + offset;
|
|
61
60
|
break;
|
|
62
61
|
}
|
|
63
|
-
// Cross-axis alignment
|
|
64
62
|
if (placement === 'top' || placement === 'bottom') {
|
|
65
63
|
switch (align) {
|
|
66
64
|
case 'start':
|
|
@@ -87,13 +85,11 @@ function computePosition(trigger, floating, opts) {
|
|
|
87
85
|
break;
|
|
88
86
|
}
|
|
89
87
|
}
|
|
90
|
-
// Clamp to viewport
|
|
91
88
|
const l = Math.max(margin, Math.min(window.innerWidth - floating.width - margin, left));
|
|
92
89
|
const t = Math.max(margin, Math.min(window.innerHeight - floating.height - margin, top));
|
|
93
|
-
// Arrow offset: keep arrow centred on the trigger even after viewport clamping
|
|
94
90
|
let arrowOffset;
|
|
95
91
|
if (opts.arrowSize !== undefined) {
|
|
96
|
-
const minOff = opts.arrowSize + 8;
|
|
92
|
+
const minOff = opts.arrowSize + 8;
|
|
97
93
|
if (placement === 'top' || placement === 'bottom') {
|
|
98
94
|
const raw = trigger.left + trigger.width / 2 - l;
|
|
99
95
|
arrowOffset = Math.max(minOff, Math.min(floating.width - minOff, raw));
|
package/js/push-menu.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/** DOM element references managed by the PushMenu static class. */
|
|
1
2
|
interface PushMenuElements {
|
|
2
3
|
navigation: HTMLElement | null;
|
|
3
4
|
content: HTMLElement | null;
|
|
@@ -6,6 +7,7 @@ interface PushMenuElements {
|
|
|
6
7
|
controlIcon: HTMLElement | null;
|
|
7
8
|
backdrop: HTMLElement | null;
|
|
8
9
|
}
|
|
10
|
+
/** Static class that manages a push-style side navigation panel. */
|
|
9
11
|
declare class PushMenu {
|
|
10
12
|
private static elements;
|
|
11
13
|
private static initialized;
|
package/js/push-menu.js
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
|
+
/** Static class that manages a push-style side navigation panel. */
|
|
1
2
|
class PushMenu {
|
|
3
|
+
static elements = {
|
|
4
|
+
navigation: null,
|
|
5
|
+
content: null,
|
|
6
|
+
menu: null,
|
|
7
|
+
header: null,
|
|
8
|
+
controlIcon: null,
|
|
9
|
+
backdrop: null
|
|
10
|
+
};
|
|
11
|
+
static initialized = false;
|
|
12
|
+
static panelStack = [];
|
|
13
|
+
static boundHandleNavigationChange;
|
|
2
14
|
static init() {
|
|
3
15
|
if (this.initialized) {
|
|
4
16
|
console.warn('PushMenu: Already initialized');
|
|
@@ -14,7 +26,6 @@ class PushMenu {
|
|
|
14
26
|
this.elements.backdrop?.addEventListener('click', this.handleBackdropClick);
|
|
15
27
|
this.initialized = true;
|
|
16
28
|
}
|
|
17
|
-
// ─── Panel construction ────────────────────────────────────────────────
|
|
18
29
|
static buildPanels() {
|
|
19
30
|
const menu = this.elements.menu;
|
|
20
31
|
if (!menu)
|
|
@@ -22,18 +33,15 @@ class PushMenu {
|
|
|
22
33
|
const rootUl = menu.querySelector(':scope > ul');
|
|
23
34
|
if (!rootUl)
|
|
24
35
|
return;
|
|
25
|
-
// Wrap root ul in a panel
|
|
26
36
|
const rootPanel = document.createElement('div');
|
|
27
37
|
rootPanel.classList.add('push-menu-panel', 'is-active');
|
|
28
38
|
rootPanel.dataset.level = '0';
|
|
29
39
|
rootPanel.appendChild(rootUl);
|
|
30
40
|
menu.appendChild(rootPanel);
|
|
31
|
-
// Recursively extract nested uls into sibling panels
|
|
32
41
|
this.extractSubPanels(rootPanel, 1);
|
|
33
42
|
this.panelStack = [rootPanel];
|
|
34
43
|
}
|
|
35
44
|
static extractSubPanels(panel, level) {
|
|
36
|
-
// Collect all uls currently in this panel before any mutations
|
|
37
45
|
const uls = Array.from(panel.querySelectorAll('ul'));
|
|
38
46
|
for (const ul of uls) {
|
|
39
47
|
const listItems = Array.from(ul.children);
|
|
@@ -41,14 +49,11 @@ class PushMenu {
|
|
|
41
49
|
const childUl = li.querySelector(':scope > ul');
|
|
42
50
|
if (!childUl)
|
|
43
51
|
continue;
|
|
44
|
-
// Determine label from the immediate anchor child
|
|
45
52
|
const parentAnchor = li.querySelector(':scope > a');
|
|
46
53
|
const title = parentAnchor?.textContent?.trim() ?? '';
|
|
47
|
-
// ── Build sub-panel ──────────────────────────────────────
|
|
48
54
|
const subPanel = document.createElement('div');
|
|
49
55
|
subPanel.classList.add('push-menu-panel');
|
|
50
56
|
subPanel.dataset.level = String(level);
|
|
51
|
-
// Header: back button + breadcrumb title
|
|
52
57
|
const header = document.createElement('div');
|
|
53
58
|
header.classList.add('push-menu-panel-header');
|
|
54
59
|
const backBtn = document.createElement('button');
|
|
@@ -62,15 +67,11 @@ class PushMenu {
|
|
|
62
67
|
header.appendChild(backBtn);
|
|
63
68
|
header.appendChild(titleEl);
|
|
64
69
|
subPanel.appendChild(header);
|
|
65
|
-
// Move the child ul into the sub-panel
|
|
66
70
|
subPanel.appendChild(childUl);
|
|
67
|
-
// Append sub-panel as sibling inside the nav
|
|
68
71
|
this.elements.menu?.appendChild(subPanel);
|
|
69
|
-
// ── Replace anchor with a trigger span in the parent li ──
|
|
70
72
|
const trigger = document.createElement('span');
|
|
71
73
|
trigger.classList.add('push-menu-item');
|
|
72
74
|
trigger.textContent = title;
|
|
73
|
-
// Chevron icon
|
|
74
75
|
const chevron = document.createElement('span');
|
|
75
76
|
chevron.classList.add('push-menu-chevron');
|
|
76
77
|
chevron.setAttribute('aria-hidden', 'true');
|
|
@@ -83,12 +84,10 @@ class PushMenu {
|
|
|
83
84
|
li.prepend(trigger);
|
|
84
85
|
}
|
|
85
86
|
trigger.addEventListener('click', () => PushMenu.openPanel(subPanel));
|
|
86
|
-
// Recurse into the newly created sub-panel
|
|
87
87
|
this.extractSubPanels(subPanel, level + 1);
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
|
-
// ─── Panel navigation ──────────────────────────────────────────────────
|
|
92
91
|
static openPanel(panel) {
|
|
93
92
|
const currentPanel = this.panelStack[this.panelStack.length - 1];
|
|
94
93
|
currentPanel.classList.remove('is-active');
|
|
@@ -109,7 +108,6 @@ class PushMenu {
|
|
|
109
108
|
const menu = this.elements.menu;
|
|
110
109
|
if (!menu)
|
|
111
110
|
return;
|
|
112
|
-
// Wait for the close animation before snapping panels back
|
|
113
111
|
setTimeout(() => {
|
|
114
112
|
const panels = Array.from(menu.querySelectorAll('.push-menu-panel'));
|
|
115
113
|
panels.forEach((panel, index) => {
|
|
@@ -122,7 +120,6 @@ class PushMenu {
|
|
|
122
120
|
}
|
|
123
121
|
}, 300);
|
|
124
122
|
}
|
|
125
|
-
// ─── Open / close ──────────────────────────────────────────────────────
|
|
126
123
|
static handleNavigationChange() {
|
|
127
124
|
const isPushed = this.elements.content?.classList.contains('pushed') ?? false;
|
|
128
125
|
if (!isPushed) {
|
|
@@ -154,6 +151,16 @@ class PushMenu {
|
|
|
154
151
|
}
|
|
155
152
|
}
|
|
156
153
|
}
|
|
154
|
+
static clickNav = () => {
|
|
155
|
+
const navigation = PushMenu.elements.navigation;
|
|
156
|
+
navigation?.click();
|
|
157
|
+
};
|
|
158
|
+
static handleBackdropClick = () => {
|
|
159
|
+
if (PushMenu.isOpen()) {
|
|
160
|
+
const navigation = PushMenu.elements.navigation;
|
|
161
|
+
navigation?.click();
|
|
162
|
+
}
|
|
163
|
+
};
|
|
157
164
|
static open() {
|
|
158
165
|
if (!this.elements.content?.classList.contains('pushed')) {
|
|
159
166
|
this.pushToggle();
|
|
@@ -194,24 +201,4 @@ class PushMenu {
|
|
|
194
201
|
this.elements.backdrop = document.querySelector('.push-menu-backdrop');
|
|
195
202
|
}
|
|
196
203
|
}
|
|
197
|
-
PushMenu.elements = {
|
|
198
|
-
navigation: null,
|
|
199
|
-
content: null,
|
|
200
|
-
menu: null,
|
|
201
|
-
header: null,
|
|
202
|
-
controlIcon: null,
|
|
203
|
-
backdrop: null
|
|
204
|
-
};
|
|
205
|
-
PushMenu.initialized = false;
|
|
206
|
-
PushMenu.panelStack = [];
|
|
207
|
-
PushMenu.clickNav = () => {
|
|
208
|
-
const navigation = PushMenu.elements.navigation;
|
|
209
|
-
navigation?.click();
|
|
210
|
-
};
|
|
211
|
-
PushMenu.handleBackdropClick = () => {
|
|
212
|
-
if (PushMenu.isOpen()) {
|
|
213
|
-
const navigation = PushMenu.elements.navigation;
|
|
214
|
-
navigation?.click();
|
|
215
|
-
}
|
|
216
|
-
};
|
|
217
204
|
export { PushMenu };
|
package/js/range-slider.d.ts
CHANGED