basecoat-cli 0.3.9 → 0.3.10-beta.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/dist/assets/jinja/carousel.html.jinja +111 -0
- package/dist/assets/jinja/select.html.jinja +48 -20
- package/dist/assets/js/all.js +159 -25
- package/dist/assets/js/all.min.js +1 -1
- package/dist/assets/js/carousel.js +192 -0
- package/dist/assets/js/carousel.min.js +1 -0
- package/dist/assets/js/select.js +159 -25
- package/dist/assets/js/select.min.js +1 -1
- package/dist/assets/nunjucks/carousel.njk +111 -0
- package/dist/assets/nunjucks/select.njk +48 -20
- package/package.json +1 -1
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
const initCarousel = (carouselComponent) => {
|
|
3
|
+
const slidesContainer = carouselComponent.querySelector('.carousel-slides');
|
|
4
|
+
if (!slidesContainer) return;
|
|
5
|
+
|
|
6
|
+
const slides = Array.from(carouselComponent.querySelectorAll('.carousel-item'));
|
|
7
|
+
const prevButton = carouselComponent.querySelector('.carousel-prev');
|
|
8
|
+
const nextButton = carouselComponent.querySelector('.carousel-next');
|
|
9
|
+
const indicators = Array.from(carouselComponent.querySelectorAll('.carousel-indicators button'));
|
|
10
|
+
|
|
11
|
+
const loop = carouselComponent.dataset.carouselLoop === 'true';
|
|
12
|
+
const autoplayDelay = parseInt(carouselComponent.dataset.carouselAutoplay, 10);
|
|
13
|
+
const orientation = carouselComponent.dataset.orientation || 'horizontal';
|
|
14
|
+
|
|
15
|
+
let currentIndex = 0;
|
|
16
|
+
let autoplayInterval = null;
|
|
17
|
+
|
|
18
|
+
const getScrollAmount = () => {
|
|
19
|
+
if (slides.length === 0) return 0;
|
|
20
|
+
const firstSlide = slides[0];
|
|
21
|
+
return orientation === 'vertical'
|
|
22
|
+
? firstSlide.offsetHeight + parseInt(getComputedStyle(slidesContainer).gap || 0)
|
|
23
|
+
: firstSlide.offsetWidth + parseInt(getComputedStyle(slidesContainer).gap || 0);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const scrollToIndex = (index) => {
|
|
27
|
+
const scrollAmount = getScrollAmount();
|
|
28
|
+
if (orientation === 'vertical') {
|
|
29
|
+
slidesContainer.scrollTo({ top: scrollAmount * index, behavior: 'smooth' });
|
|
30
|
+
} else {
|
|
31
|
+
slidesContainer.scrollTo({ left: scrollAmount * index, behavior: 'smooth' });
|
|
32
|
+
}
|
|
33
|
+
currentIndex = index;
|
|
34
|
+
updateIndicators();
|
|
35
|
+
updateButtonStates();
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const updateIndicators = () => {
|
|
39
|
+
indicators.forEach((indicator, index) => {
|
|
40
|
+
const isActive = index === currentIndex;
|
|
41
|
+
indicator.setAttribute('aria-current', isActive ? 'true' : 'false');
|
|
42
|
+
indicator.setAttribute('aria-label', `Slide ${index + 1}${isActive ? ' (current)' : ''}`);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
slides.forEach((slide, index) => {
|
|
46
|
+
slide.setAttribute('aria-hidden', index === currentIndex ? 'false' : 'true');
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const updateButtonStates = () => {
|
|
51
|
+
if (!prevButton || !nextButton) return;
|
|
52
|
+
|
|
53
|
+
if (loop) {
|
|
54
|
+
prevButton.disabled = false;
|
|
55
|
+
nextButton.disabled = false;
|
|
56
|
+
} else {
|
|
57
|
+
prevButton.disabled = currentIndex === 0;
|
|
58
|
+
nextButton.disabled = currentIndex === slides.length - 1;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const goToPrevious = () => {
|
|
63
|
+
if (currentIndex > 0) {
|
|
64
|
+
scrollToIndex(currentIndex - 1);
|
|
65
|
+
} else if (loop) {
|
|
66
|
+
scrollToIndex(slides.length - 1);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const goToNext = () => {
|
|
71
|
+
if (currentIndex < slides.length - 1) {
|
|
72
|
+
scrollToIndex(currentIndex + 1);
|
|
73
|
+
} else if (loop) {
|
|
74
|
+
scrollToIndex(0);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const startAutoplay = () => {
|
|
79
|
+
if (!autoplayDelay || autoplayDelay <= 0) return;
|
|
80
|
+
|
|
81
|
+
autoplayInterval = setInterval(() => {
|
|
82
|
+
goToNext();
|
|
83
|
+
}, autoplayDelay);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const stopAutoplay = () => {
|
|
87
|
+
if (autoplayInterval) {
|
|
88
|
+
clearInterval(autoplayInterval);
|
|
89
|
+
autoplayInterval = null;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const detectCurrentSlide = () => {
|
|
94
|
+
const scrollPosition = orientation === 'vertical'
|
|
95
|
+
? slidesContainer.scrollTop
|
|
96
|
+
: slidesContainer.scrollLeft;
|
|
97
|
+
const scrollAmount = getScrollAmount();
|
|
98
|
+
const newIndex = Math.round(scrollPosition / scrollAmount);
|
|
99
|
+
|
|
100
|
+
if (newIndex !== currentIndex && newIndex >= 0 && newIndex < slides.length) {
|
|
101
|
+
currentIndex = newIndex;
|
|
102
|
+
updateIndicators();
|
|
103
|
+
updateButtonStates();
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Previous/Next button handlers
|
|
108
|
+
if (prevButton) {
|
|
109
|
+
prevButton.addEventListener('click', () => {
|
|
110
|
+
stopAutoplay();
|
|
111
|
+
goToPrevious();
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (nextButton) {
|
|
116
|
+
nextButton.addEventListener('click', () => {
|
|
117
|
+
stopAutoplay();
|
|
118
|
+
goToNext();
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Indicator click handlers
|
|
123
|
+
indicators.forEach((indicator, index) => {
|
|
124
|
+
indicator.addEventListener('click', () => {
|
|
125
|
+
stopAutoplay();
|
|
126
|
+
scrollToIndex(index);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Keyboard navigation
|
|
131
|
+
carouselComponent.addEventListener('keydown', (event) => {
|
|
132
|
+
const isVertical = orientation === 'vertical';
|
|
133
|
+
const prevKey = isVertical ? 'ArrowUp' : 'ArrowLeft';
|
|
134
|
+
const nextKey = isVertical ? 'ArrowDown' : 'ArrowRight';
|
|
135
|
+
|
|
136
|
+
switch (event.key) {
|
|
137
|
+
case prevKey:
|
|
138
|
+
event.preventDefault();
|
|
139
|
+
stopAutoplay();
|
|
140
|
+
goToPrevious();
|
|
141
|
+
break;
|
|
142
|
+
case nextKey:
|
|
143
|
+
event.preventDefault();
|
|
144
|
+
stopAutoplay();
|
|
145
|
+
goToNext();
|
|
146
|
+
break;
|
|
147
|
+
case 'Home':
|
|
148
|
+
event.preventDefault();
|
|
149
|
+
stopAutoplay();
|
|
150
|
+
scrollToIndex(0);
|
|
151
|
+
break;
|
|
152
|
+
case 'End':
|
|
153
|
+
event.preventDefault();
|
|
154
|
+
stopAutoplay();
|
|
155
|
+
scrollToIndex(slides.length - 1);
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Detect scroll position changes (for touch/manual scrolling)
|
|
161
|
+
let scrollTimeout;
|
|
162
|
+
slidesContainer.addEventListener('scroll', () => {
|
|
163
|
+
clearTimeout(scrollTimeout);
|
|
164
|
+
scrollTimeout = setTimeout(() => {
|
|
165
|
+
detectCurrentSlide();
|
|
166
|
+
}, 100);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Pause autoplay on hover or focus
|
|
170
|
+
if (autoplayDelay) {
|
|
171
|
+
carouselComponent.addEventListener('mouseenter', stopAutoplay);
|
|
172
|
+
carouselComponent.addEventListener('mouseleave', startAutoplay);
|
|
173
|
+
carouselComponent.addEventListener('focusin', stopAutoplay);
|
|
174
|
+
carouselComponent.addEventListener('focusout', startAutoplay);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Initialize
|
|
178
|
+
updateIndicators();
|
|
179
|
+
updateButtonStates();
|
|
180
|
+
|
|
181
|
+
if (autoplayDelay) {
|
|
182
|
+
startAutoplay();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
carouselComponent.dataset.carouselInitialized = true;
|
|
186
|
+
carouselComponent.dispatchEvent(new CustomEvent('basecoat:initialized'));
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
if (window.basecoat) {
|
|
190
|
+
window.basecoat.register('carousel', '.carousel:not([data-carousel-initialized])', initCarousel);
|
|
191
|
+
}
|
|
192
|
+
})();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(()=>{const e=e=>{const t=e.querySelector(".carousel-slides");if(!t)return;const r=Array.from(e.querySelectorAll(".carousel-item")),a=e.querySelector(".carousel-prev"),o=e.querySelector(".carousel-next"),l=Array.from(e.querySelectorAll(".carousel-indicators button")),s="true"===e.dataset.carouselLoop,n=parseInt(e.dataset.carouselAutoplay,10),i=e.dataset.orientation||"horizontal";let c=0,d=null;const u=()=>{if(0===r.length)return 0;const e=r[0];return"vertical"===i?e.offsetHeight+parseInt(getComputedStyle(t).gap||0):e.offsetWidth+parseInt(getComputedStyle(t).gap||0)},v=e=>{const r=u();"vertical"===i?t.scrollTo({top:r*e,behavior:"smooth"}):t.scrollTo({left:r*e,behavior:"smooth"}),c=e,f(),h()},f=()=>{l.forEach(((e,t)=>{const r=t===c;e.setAttribute("aria-current",r?"true":"false"),e.setAttribute("aria-label",`Slide ${t+1}${r?" (current)":""}`)})),r.forEach(((e,t)=>{e.setAttribute("aria-hidden",t===c?"false":"true")}))},h=()=>{a&&o&&(s?(a.disabled=!1,o.disabled=!1):(a.disabled=0===c,o.disabled=c===r.length-1))},p=()=>{c>0?v(c-1):s&&v(r.length-1)},b=()=>{c<r.length-1?v(c+1):s&&v(0)},E=()=>{!n||n<=0||(d=setInterval((()=>{b()}),n))},g=()=>{d&&(clearInterval(d),d=null)};let m;a&&a.addEventListener("click",(()=>{g(),p()})),o&&o.addEventListener("click",(()=>{g(),b()})),l.forEach(((e,t)=>{e.addEventListener("click",(()=>{g(),v(t)}))})),e.addEventListener("keydown",(e=>{const t="vertical"===i,a=t?"ArrowUp":"ArrowLeft",o=t?"ArrowDown":"ArrowRight";switch(e.key){case a:e.preventDefault(),g(),p();break;case o:e.preventDefault(),g(),b();break;case"Home":e.preventDefault(),g(),v(0);break;case"End":e.preventDefault(),g(),v(r.length-1)}})),t.addEventListener("scroll",(()=>{clearTimeout(m),m=setTimeout((()=>{(()=>{const e="vertical"===i?t.scrollTop:t.scrollLeft,a=u(),o=Math.round(e/a);o!==c&&o>=0&&o<r.length&&(c=o,f(),h())})()}),100)})),n&&(e.addEventListener("mouseenter",g),e.addEventListener("mouseleave",E),e.addEventListener("focusin",g),e.addEventListener("focusout",E)),f(),h(),n&&E(),e.dataset.carouselInitialized=!0,e.dispatchEvent(new CustomEvent("basecoat:initialized"))};window.basecoat&&window.basecoat.register("carousel",".carousel:not([data-carousel-initialized])",e)})();
|
package/dist/assets/js/select.js
CHANGED
|
@@ -3,23 +3,31 @@
|
|
|
3
3
|
const trigger = selectComponent.querySelector(':scope > button');
|
|
4
4
|
const selectedLabel = trigger.querySelector(':scope > span');
|
|
5
5
|
const popover = selectComponent.querySelector(':scope > [data-popover]');
|
|
6
|
-
const listbox = popover.querySelector('[role="listbox"]');
|
|
6
|
+
const listbox = popover ? popover.querySelector('[role="listbox"]') : null;
|
|
7
7
|
const input = selectComponent.querySelector(':scope > input[type="hidden"]');
|
|
8
8
|
const filter = selectComponent.querySelector('header input[type="text"]');
|
|
9
|
+
|
|
9
10
|
if (!trigger || !popover || !listbox || !input) {
|
|
10
11
|
const missing = [];
|
|
11
12
|
if (!trigger) missing.push('trigger');
|
|
12
13
|
if (!popover) missing.push('popover');
|
|
13
14
|
if (!listbox) missing.push('listbox');
|
|
14
|
-
if (!input)
|
|
15
|
+
if (!input) missing.push('input');
|
|
15
16
|
console.error(`Select component initialisation failed. Missing element(s): ${missing.join(', ')}`, selectComponent);
|
|
16
17
|
return;
|
|
17
18
|
}
|
|
18
|
-
|
|
19
|
+
|
|
19
20
|
const allOptions = Array.from(listbox.querySelectorAll('[role="option"]'));
|
|
20
21
|
const options = allOptions.filter(opt => opt.getAttribute('aria-disabled') !== 'true');
|
|
21
22
|
let visibleOptions = [...options];
|
|
22
23
|
let activeIndex = -1;
|
|
24
|
+
const isMultiple = listbox.getAttribute('aria-multiselectable') === 'true';
|
|
25
|
+
let selectedValues = isMultiple ? new Set() : null;
|
|
26
|
+
let placeholder = null;
|
|
27
|
+
|
|
28
|
+
if (isMultiple) {
|
|
29
|
+
placeholder = selectComponent.dataset.placeholder || '';
|
|
30
|
+
}
|
|
23
31
|
|
|
24
32
|
const setActiveOption = (index) => {
|
|
25
33
|
if (activeIndex > -1 && options[activeIndex]) {
|
|
@@ -46,23 +54,92 @@
|
|
|
46
54
|
return parseFloat(style.transitionDuration) > 0 || parseFloat(style.transitionDelay) > 0;
|
|
47
55
|
};
|
|
48
56
|
|
|
49
|
-
const
|
|
50
|
-
if (
|
|
57
|
+
const syncMultipleInputs = () => {
|
|
58
|
+
if (!isMultiple) return;
|
|
59
|
+
const values = Array.from(selectedValues);
|
|
60
|
+
const inputs = Array.from(selectComponent.querySelectorAll(':scope > input[type="hidden"]'));
|
|
61
|
+
inputs.slice(1).forEach(inp => inp.remove());
|
|
62
|
+
|
|
63
|
+
if (values.length === 0) {
|
|
64
|
+
input.value = '';
|
|
65
|
+
} else {
|
|
66
|
+
input.value = values[0];
|
|
67
|
+
let insertAfter = input;
|
|
68
|
+
for (let i = 1; i < values.length; i++) {
|
|
69
|
+
const clone = input.cloneNode(true);
|
|
70
|
+
clone.removeAttribute('id');
|
|
71
|
+
clone.value = values[i];
|
|
72
|
+
insertAfter.after(clone);
|
|
73
|
+
insertAfter = clone;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const updateMultipleLabel = () => {
|
|
79
|
+
if (!isMultiple) return;
|
|
80
|
+
const selected = options.filter(opt => selectedValues.has(opt.dataset.value));
|
|
81
|
+
if (selected.length === 0) {
|
|
82
|
+
selectedLabel.textContent = placeholder;
|
|
83
|
+
selectedLabel.classList.add('text-muted-foreground');
|
|
84
|
+
} else {
|
|
85
|
+
selectedLabel.textContent = selected.map(opt => opt.dataset.label || opt.textContent.trim()).join(', ');
|
|
86
|
+
selectedLabel.classList.remove('text-muted-foreground');
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const updateValue = (optionOrOptions, triggerEvent = true) => {
|
|
91
|
+
let value;
|
|
92
|
+
|
|
93
|
+
if (isMultiple) {
|
|
94
|
+
const opts = Array.isArray(optionOrOptions) ? optionOrOptions : [];
|
|
95
|
+
selectedValues = new Set(opts.map(opt => opt.dataset.value));
|
|
96
|
+
options.forEach(opt => {
|
|
97
|
+
if (selectedValues.has(opt.dataset.value)) {
|
|
98
|
+
opt.setAttribute('aria-selected', 'true');
|
|
99
|
+
} else {
|
|
100
|
+
opt.removeAttribute('aria-selected');
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
updateMultipleLabel();
|
|
104
|
+
syncMultipleInputs();
|
|
105
|
+
value = Array.from(selectedValues);
|
|
106
|
+
} else {
|
|
107
|
+
const option = optionOrOptions;
|
|
108
|
+
if (!option) return;
|
|
51
109
|
selectedLabel.innerHTML = option.innerHTML;
|
|
52
110
|
input.value = option.dataset.value;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
111
|
+
options.forEach(opt => {
|
|
112
|
+
if (opt === option) {
|
|
113
|
+
opt.setAttribute('aria-selected', 'true');
|
|
114
|
+
} else {
|
|
115
|
+
opt.removeAttribute('aria-selected');
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
value = option.dataset.value;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (triggerEvent) {
|
|
122
|
+
selectComponent.dispatchEvent(new CustomEvent('change', {
|
|
123
|
+
detail: { value },
|
|
124
|
+
bubbles: true
|
|
125
|
+
}));
|
|
63
126
|
}
|
|
64
127
|
};
|
|
65
128
|
|
|
129
|
+
const toggleMultipleValue = (value, triggerEvent = true) => {
|
|
130
|
+
if (!isMultiple || value == null) return;
|
|
131
|
+
|
|
132
|
+
const newValues = new Set(selectedValues);
|
|
133
|
+
if (newValues.has(value)) {
|
|
134
|
+
newValues.delete(value);
|
|
135
|
+
} else {
|
|
136
|
+
newValues.add(value);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const selectedOptions = options.filter(opt => newValues.has(opt.dataset.value));
|
|
140
|
+
updateValue(selectedOptions, triggerEvent);
|
|
141
|
+
};
|
|
142
|
+
|
|
66
143
|
const closePopover = (focusOnTrigger = true) => {
|
|
67
144
|
if (popover.getAttribute('aria-hidden') === 'true') return;
|
|
68
145
|
|
|
@@ -101,7 +178,27 @@
|
|
|
101
178
|
|
|
102
179
|
const selectByValue = (value) => {
|
|
103
180
|
const option = options.find(opt => opt.dataset.value === value);
|
|
104
|
-
|
|
181
|
+
if (isMultiple) {
|
|
182
|
+
if (value != null && selectedValues.has(value)) return;
|
|
183
|
+
if (option && value != null) {
|
|
184
|
+
const newValues = new Set(selectedValues);
|
|
185
|
+
newValues.add(value);
|
|
186
|
+
const selectedOptions = options.filter(opt => newValues.has(opt.dataset.value));
|
|
187
|
+
updateValue(selectedOptions);
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
selectOption(option);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const selectAll = () => {
|
|
195
|
+
if (!isMultiple) return;
|
|
196
|
+
updateValue(options.filter(opt => opt.dataset.value != null));
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const selectNone = () => {
|
|
200
|
+
if (!isMultiple) return;
|
|
201
|
+
updateValue([]);
|
|
105
202
|
};
|
|
106
203
|
|
|
107
204
|
if (filter) {
|
|
@@ -137,13 +234,32 @@
|
|
|
137
234
|
filter.addEventListener('input', filterOptions);
|
|
138
235
|
}
|
|
139
236
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
237
|
+
if (isMultiple) {
|
|
238
|
+
const validValues = new Set(options.map(opt => opt.dataset.value).filter(v => v != null));
|
|
239
|
+
const inputs = Array.from(selectComponent.querySelectorAll(':scope > input[type="hidden"]'));
|
|
240
|
+
const initialValues = inputs
|
|
241
|
+
.map(inp => inp.value)
|
|
242
|
+
.filter(v => v != null && validValues.has(v));
|
|
243
|
+
|
|
244
|
+
let initialOptions;
|
|
245
|
+
if (initialValues.length > 0) {
|
|
246
|
+
initialOptions = options.filter(opt => initialValues.includes(opt.dataset.value));
|
|
247
|
+
} else {
|
|
248
|
+
initialOptions = options.filter(opt => opt.getAttribute('aria-selected') === 'true');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
updateValue(initialOptions, false);
|
|
252
|
+
} else {
|
|
253
|
+
let initialOption = options.find(opt => opt.dataset.value === input.value);
|
|
145
254
|
|
|
146
|
-
|
|
255
|
+
if (!initialOption) {
|
|
256
|
+
initialOption = options.find(opt => opt.dataset.value !== undefined) ?? options[0];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (initialOption) {
|
|
260
|
+
updateValue(initialOption, false);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
147
263
|
|
|
148
264
|
const handleKeyNavigation = (event) => {
|
|
149
265
|
const isPopoverOpen = popover.getAttribute('aria-hidden') === 'false';
|
|
@@ -169,7 +285,11 @@
|
|
|
169
285
|
|
|
170
286
|
if (event.key === 'Enter') {
|
|
171
287
|
if (activeIndex > -1) {
|
|
172
|
-
|
|
288
|
+
if (isMultiple) {
|
|
289
|
+
toggleMultipleValue(options[activeIndex].dataset.value);
|
|
290
|
+
} else {
|
|
291
|
+
selectOption(options[activeIndex]);
|
|
292
|
+
}
|
|
173
293
|
}
|
|
174
294
|
return;
|
|
175
295
|
}
|
|
@@ -268,7 +388,17 @@
|
|
|
268
388
|
listbox.addEventListener('click', (event) => {
|
|
269
389
|
const clickedOption = event.target.closest('[role="option"]');
|
|
270
390
|
if (clickedOption) {
|
|
271
|
-
|
|
391
|
+
if (isMultiple) {
|
|
392
|
+
toggleMultipleValue(clickedOption.dataset.value);
|
|
393
|
+
setActiveOption(options.indexOf(clickedOption));
|
|
394
|
+
if (filter) {
|
|
395
|
+
filter.focus();
|
|
396
|
+
} else {
|
|
397
|
+
trigger.focus();
|
|
398
|
+
}
|
|
399
|
+
} else {
|
|
400
|
+
selectOption(clickedOption);
|
|
401
|
+
}
|
|
272
402
|
}
|
|
273
403
|
});
|
|
274
404
|
|
|
@@ -285,8 +415,12 @@
|
|
|
285
415
|
});
|
|
286
416
|
|
|
287
417
|
popover.setAttribute('aria-hidden', 'true');
|
|
288
|
-
|
|
418
|
+
|
|
289
419
|
selectComponent.selectByValue = selectByValue;
|
|
420
|
+
if (isMultiple) {
|
|
421
|
+
selectComponent.selectAll = selectAll;
|
|
422
|
+
selectComponent.selectNone = selectNone;
|
|
423
|
+
}
|
|
290
424
|
selectComponent.dataset.selectInitialized = true;
|
|
291
425
|
selectComponent.dispatchEvent(new CustomEvent('basecoat:initialized'));
|
|
292
426
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
(()=>{const e=e=>{const t=e.querySelector(":scope > button"),
|
|
1
|
+
(()=>{const e=e=>{const t=e.querySelector(":scope > button"),a=t.querySelector(":scope > span"),r=e.querySelector(":scope > [data-popover]"),n=r?r.querySelector('[role="listbox"]'):null,i=e.querySelector(':scope > input[type="hidden"]'),s=e.querySelector('header input[type="text"]');if(!(t&&r&&n&&i)){const a=[];return t||a.push("trigger"),r||a.push("popover"),n||a.push("listbox"),i||a.push("input"),void console.error(`Select component initialisation failed. Missing element(s): ${a.join(", ")}`,e)}const o=Array.from(n.querySelectorAll('[role="option"]')),l=o.filter((e=>"true"!==e.getAttribute("aria-disabled")));let d=[...l],u=-1;const c="true"===n.getAttribute("aria-multiselectable");let f=c?new Set:null,v=null;c&&(v=e.dataset.placeholder||"");const p=e=>{if(u>-1&&l[u]&&l[u].classList.remove("active"),u=e,u>-1){const e=l[u];e.classList.add("active"),e.id?t.setAttribute("aria-activedescendant",e.id):t.removeAttribute("aria-activedescendant")}else t.removeAttribute("aria-activedescendant")},h=()=>{const e=getComputedStyle(r);return parseFloat(e.transitionDuration)>0||parseFloat(e.transitionDelay)>0},b=(t,r=!0)=>{let n;if(c){const r=Array.isArray(t)?t:[];f=new Set(r.map((e=>e.dataset.value))),l.forEach((e=>{f.has(e.dataset.value)?e.setAttribute("aria-selected","true"):e.removeAttribute("aria-selected")})),(()=>{if(!c)return;const e=l.filter((e=>f.has(e.dataset.value)));0===e.length?(a.textContent=v,a.classList.add("text-muted-foreground")):(a.textContent=e.map((e=>e.dataset.label||e.textContent.trim())).join(", "),a.classList.remove("text-muted-foreground"))})(),(()=>{if(!c)return;const t=Array.from(f);if(Array.from(e.querySelectorAll(':scope > input[type="hidden"]')).slice(1).forEach((e=>e.remove())),0===t.length)i.value="";else{i.value=t[0];let e=i;for(let a=1;a<t.length;a++){const r=i.cloneNode(!0);r.removeAttribute("id"),r.value=t[a],e.after(r),e=r}}})(),n=Array.from(f)}else{const e=t;if(!e)return;a.innerHTML=e.innerHTML,i.value=e.dataset.value,l.forEach((t=>{t===e?t.setAttribute("aria-selected","true"):t.removeAttribute("aria-selected")})),n=e.dataset.value}r&&e.dispatchEvent(new CustomEvent("change",{detail:{value:n},bubbles:!0}))},m=(e,t=!0)=>{if(!c||null==e)return;const a=new Set(f);a.has(e)?a.delete(e):a.add(e);const r=l.filter((e=>a.has(e.dataset.value)));b(r,t)},A=(e=!0)=>{if("true"!==r.getAttribute("aria-hidden")){if(s){const e=()=>{s.value="",d=[...l],o.forEach((e=>e.setAttribute("aria-hidden","false")))};h()?r.addEventListener("transitionend",e,{once:!0}):e()}e&&t.focus(),r.setAttribute("aria-hidden","true"),t.setAttribute("aria-expanded","false"),p(-1)}},y=e=>{if(!e)return;const t=i.value,a=e.dataset.value;null!=a&&a!==t&&b(e),A()},E=()=>{c&&b(l.filter((e=>null!=e.dataset.value)))},w=()=>{c&&b([])};if(s){const e=()=>{const e=s.value.trim().toLowerCase();p(-1),d=[],o.forEach((t=>{if(t.hasAttribute("data-force"))return t.setAttribute("aria-hidden","false"),void(l.includes(t)&&d.push(t));const a=(t.dataset.filter||t.textContent).trim().toLowerCase(),r=(t.dataset.keywords||"").toLowerCase().split(/[\s,]+/).filter(Boolean).some((t=>t.includes(e))),n=a.includes(e)||r;t.setAttribute("aria-hidden",String(!n)),n&&l.includes(t)&&d.push(t)}))};s.addEventListener("input",e)}if(c){const t=new Set(l.map((e=>e.dataset.value)).filter((e=>null!=e))),a=Array.from(e.querySelectorAll(':scope > input[type="hidden"]')).map((e=>e.value)).filter((e=>null!=e&&t.has(e)));let r;r=a.length>0?l.filter((e=>a.includes(e.dataset.value))):l.filter((e=>"true"===e.getAttribute("aria-selected"))),b(r,!1)}else{let e=l.find((e=>e.dataset.value===i.value));e||(e=l.find((e=>void 0!==e.dataset.value))??l[0]),e&&b(e,!1)}const g=e=>{const a="false"===r.getAttribute("aria-hidden");if(!["ArrowDown","ArrowUp","Enter","Home","End","Escape"].includes(e.key))return;if(!a)return void("Enter"!==e.key&&"Escape"!==e.key&&(e.preventDefault(),t.click()));if(e.preventDefault(),"Escape"===e.key)return void A();if("Enter"===e.key)return void(u>-1&&(c?m(l[u].dataset.value):y(l[u])));if(0===d.length)return;const n=u>-1?d.indexOf(l[u]):-1;let i=n;switch(e.key){case"ArrowDown":n<d.length-1&&(i=n+1);break;case"ArrowUp":n>0?i=n-1:-1===n&&(i=0);break;case"Home":i=0;break;case"End":i=d.length-1}if(i!==n){const e=d[i];p(l.indexOf(e)),e.scrollIntoView({block:"nearest",behavior:"smooth"})}};n.addEventListener("mousemove",(e=>{const t=e.target.closest('[role="option"]');if(t&&d.includes(t)){const e=l.indexOf(t);e!==u&&p(e)}})),n.addEventListener("mouseleave",(()=>{const e=n.querySelector('[role="option"][aria-selected="true"]');p(e?l.indexOf(e):-1)})),t.addEventListener("keydown",g),s&&s.addEventListener("keydown",g);t.addEventListener("click",(()=>{"true"===t.getAttribute("aria-expanded")?A():(()=>{document.dispatchEvent(new CustomEvent("basecoat:popover",{detail:{source:e}})),s&&(h()?r.addEventListener("transitionend",(()=>{s.focus()}),{once:!0}):s.focus()),r.setAttribute("aria-hidden","false"),t.setAttribute("aria-expanded","true");const a=n.querySelector('[role="option"][aria-selected="true"]');a&&(p(l.indexOf(a)),a.scrollIntoView({block:"nearest"}))})()})),n.addEventListener("click",(e=>{const a=e.target.closest('[role="option"]');a&&(c?(m(a.dataset.value),p(l.indexOf(a)),s?s.focus():t.focus()):y(a))})),document.addEventListener("click",(t=>{e.contains(t.target)||A(!1)})),document.addEventListener("basecoat:popover",(t=>{t.detail.source!==e&&A(!1)})),r.setAttribute("aria-hidden","true"),e.selectByValue=e=>{const t=l.find((t=>t.dataset.value===e));if(c){if(null!=e&&f.has(e))return;if(t&&null!=e){const t=new Set(f);t.add(e);const a=l.filter((e=>t.has(e.dataset.value)));b(a)}}else y(t)},c&&(e.selectAll=E,e.selectNone=w),e.dataset.selectInitialized=!0,e.dispatchEvent(new CustomEvent("basecoat:initialized"))};window.basecoat&&window.basecoat.register("select","div.select:not([data-select-initialized])",e)})();
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
{#
|
|
2
|
+
Renders a carousel component with navigation controls and optional indicators.
|
|
3
|
+
|
|
4
|
+
@param id {string} [optional] - Unique identifier for the carousel component. Auto-generated if not provided.
|
|
5
|
+
@param slides {array} - An array of objects representing carousel slides.
|
|
6
|
+
Each object should have:
|
|
7
|
+
- content {string}: HTML content for the slide.
|
|
8
|
+
- attrs {object} [optional]: Additional HTML attributes for the slide item.
|
|
9
|
+
@param loop {boolean} [optional] [default=false] - Enable continuous looping.
|
|
10
|
+
@param autoplay {number} [optional] [default=0] - Auto-advance delay in milliseconds (0 = disabled).
|
|
11
|
+
@param align {string} [optional] [default='start'] - Slide alignment ('start' or 'center').
|
|
12
|
+
@param orientation {string} [optional] [default='horizontal'] - Carousel orientation ('horizontal' or 'vertical').
|
|
13
|
+
@param show_controls {boolean} [optional] [default=true] - Show previous/next navigation buttons.
|
|
14
|
+
@param show_indicators {boolean} [optional] [default=true] - Show slide indicator dots.
|
|
15
|
+
@param main_attrs {object} [optional] - Additional HTML attributes for the main container.
|
|
16
|
+
@param viewport_attrs {object} [optional] - Additional HTML attributes for the viewport container.
|
|
17
|
+
#}
|
|
18
|
+
{% macro carousel(
|
|
19
|
+
id=None,
|
|
20
|
+
slides=[],
|
|
21
|
+
loop=false,
|
|
22
|
+
autoplay=0,
|
|
23
|
+
align='start',
|
|
24
|
+
orientation='horizontal',
|
|
25
|
+
show_controls=true,
|
|
26
|
+
show_indicators=true,
|
|
27
|
+
main_attrs={},
|
|
28
|
+
viewport_attrs={}
|
|
29
|
+
)
|
|
30
|
+
%}
|
|
31
|
+
{% set id = id or ("carousel-" + (range(100000, 999999) | random | string)) %}
|
|
32
|
+
<div
|
|
33
|
+
class="carousel {{ main_attrs.class }}"
|
|
34
|
+
id="{{ id }}"
|
|
35
|
+
data-carousel-loop="{{ 'true' if loop else 'false' }}"
|
|
36
|
+
{% if autoplay > 0 %}data-carousel-autoplay="{{ autoplay }}"{% endif %}
|
|
37
|
+
data-orientation="{{ orientation }}"
|
|
38
|
+
role="region"
|
|
39
|
+
aria-roledescription="carousel"
|
|
40
|
+
aria-label="Carousel"
|
|
41
|
+
tabindex="0"
|
|
42
|
+
{% for key, value in main_attrs %}
|
|
43
|
+
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
|
44
|
+
{% endfor %}
|
|
45
|
+
>
|
|
46
|
+
<div
|
|
47
|
+
class="carousel-viewport {{ viewport_attrs.class }}"
|
|
48
|
+
{% for key, value in viewport_attrs %}
|
|
49
|
+
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
|
50
|
+
{% endfor %}
|
|
51
|
+
>
|
|
52
|
+
<div
|
|
53
|
+
class="carousel-slides"
|
|
54
|
+
data-orientation="{{ orientation }}"
|
|
55
|
+
>
|
|
56
|
+
{% for slide in slides %}
|
|
57
|
+
<div
|
|
58
|
+
class="carousel-item"
|
|
59
|
+
role="group"
|
|
60
|
+
aria-roledescription="slide"
|
|
61
|
+
aria-label="{{ loop.index }} of {{ slides | length }}"
|
|
62
|
+
{% if align == 'center' %}data-align="center"{% endif %}
|
|
63
|
+
{% if slide.attrs %}
|
|
64
|
+
{% for key, value in slide.attrs %}
|
|
65
|
+
{{ key }}="{{ value }}"
|
|
66
|
+
{% endfor %}
|
|
67
|
+
{% endif %}
|
|
68
|
+
>
|
|
69
|
+
{{ slide.content | safe }}
|
|
70
|
+
</div>
|
|
71
|
+
{% endfor %}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
{% if show_controls %}
|
|
76
|
+
<div class="carousel-controls">
|
|
77
|
+
<button
|
|
78
|
+
type="button"
|
|
79
|
+
class="carousel-prev"
|
|
80
|
+
aria-label="Previous slide"
|
|
81
|
+
>
|
|
82
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
83
|
+
<path d="m15 18-6-6 6-6"/>
|
|
84
|
+
</svg>
|
|
85
|
+
</button>
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
class="carousel-next"
|
|
89
|
+
aria-label="Next slide"
|
|
90
|
+
>
|
|
91
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
92
|
+
<path d="m9 18 6-6-6-6"/>
|
|
93
|
+
</svg>
|
|
94
|
+
</button>
|
|
95
|
+
</div>
|
|
96
|
+
{% endif %}
|
|
97
|
+
|
|
98
|
+
{% if show_indicators %}
|
|
99
|
+
<div class="carousel-indicators" role="tablist" aria-label="Slides">
|
|
100
|
+
{% for slide in slides %}
|
|
101
|
+
<button
|
|
102
|
+
type="button"
|
|
103
|
+
role="tab"
|
|
104
|
+
aria-label="Slide {{ loop.index }}"
|
|
105
|
+
{% if loop.index == 1 %}aria-current="true"{% else %}aria-current="false"{% endif %}
|
|
106
|
+
></button>
|
|
107
|
+
{% endfor %}
|
|
108
|
+
</div>
|
|
109
|
+
{% endif %}
|
|
110
|
+
</div>
|
|
111
|
+
{% endmacro %}
|