basecoat-css 0.3.9 → 0.3.10-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/basecoat.cdn.css +20 -2
- package/dist/basecoat.cdn.min.css +1 -1
- package/dist/basecoat.css +9 -1
- package/dist/js/all.js +185 -50
- package/dist/js/all.min.js +1 -1
- package/dist/js/carousel.js +192 -0
- package/dist/js/carousel.min.js +1 -0
- package/dist/js/select.js +185 -50
- package/dist/js/select.min.js +1 -1
- 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/js/select.js
CHANGED
|
@@ -3,31 +3,37 @@
|
|
|
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
|
+
const selectedOptions = isMultiple ? new Set() : null;
|
|
26
|
+
const placeholder = isMultiple ? (selectComponent.dataset.placeholder || '') : null;
|
|
27
|
+
|
|
28
|
+
const getValue = (opt) => opt.dataset.value ?? opt.textContent.trim();
|
|
23
29
|
|
|
24
30
|
const setActiveOption = (index) => {
|
|
25
31
|
if (activeIndex > -1 && options[activeIndex]) {
|
|
26
32
|
options[activeIndex].classList.remove('active');
|
|
27
33
|
}
|
|
28
|
-
|
|
34
|
+
|
|
29
35
|
activeIndex = index;
|
|
30
|
-
|
|
36
|
+
|
|
31
37
|
if (activeIndex > -1) {
|
|
32
38
|
const activeOption = options[activeIndex];
|
|
33
39
|
activeOption.classList.add('active');
|
|
@@ -46,68 +52,118 @@
|
|
|
46
52
|
return parseFloat(style.transitionDuration) > 0 || parseFloat(style.transitionDelay) > 0;
|
|
47
53
|
};
|
|
48
54
|
|
|
49
|
-
const updateValue = (
|
|
50
|
-
|
|
55
|
+
const updateValue = (optionOrOptions, triggerEvent = true) => {
|
|
56
|
+
let value;
|
|
57
|
+
|
|
58
|
+
if (isMultiple) {
|
|
59
|
+
const opts = Array.isArray(optionOrOptions) ? optionOrOptions : [];
|
|
60
|
+
selectedOptions.clear();
|
|
61
|
+
opts.forEach(opt => selectedOptions.add(opt));
|
|
62
|
+
|
|
63
|
+
// Get selected options in DOM order
|
|
64
|
+
const selected = options.filter(opt => selectedOptions.has(opt));
|
|
65
|
+
if (selected.length === 0) {
|
|
66
|
+
selectedLabel.textContent = placeholder;
|
|
67
|
+
selectedLabel.classList.add('text-muted-foreground');
|
|
68
|
+
} else {
|
|
69
|
+
selectedLabel.textContent = selected.map(opt => opt.dataset.label || opt.textContent.trim()).join(', ');
|
|
70
|
+
selectedLabel.classList.remove('text-muted-foreground');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
value = selected.map(getValue);
|
|
74
|
+
input.value = JSON.stringify(value);
|
|
75
|
+
} else {
|
|
76
|
+
const option = optionOrOptions;
|
|
77
|
+
if (!option) return;
|
|
51
78
|
selectedLabel.innerHTML = option.innerHTML;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
79
|
+
value = getValue(option);
|
|
80
|
+
input.value = value;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
options.forEach(opt => {
|
|
84
|
+
const isSelected = isMultiple ? selectedOptions.has(opt) : opt === optionOrOptions;
|
|
85
|
+
if (isSelected) {
|
|
86
|
+
opt.setAttribute('aria-selected', 'true');
|
|
87
|
+
} else {
|
|
88
|
+
opt.removeAttribute('aria-selected');
|
|
62
89
|
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (triggerEvent) {
|
|
93
|
+
selectComponent.dispatchEvent(new CustomEvent('change', {
|
|
94
|
+
detail: { value },
|
|
95
|
+
bubbles: true
|
|
96
|
+
}));
|
|
63
97
|
}
|
|
64
98
|
};
|
|
65
99
|
|
|
66
100
|
const closePopover = (focusOnTrigger = true) => {
|
|
67
101
|
if (popover.getAttribute('aria-hidden') === 'true') return;
|
|
68
|
-
|
|
102
|
+
|
|
69
103
|
if (filter) {
|
|
70
104
|
const resetFilter = () => {
|
|
71
105
|
filter.value = '';
|
|
72
106
|
visibleOptions = [...options];
|
|
73
107
|
allOptions.forEach(opt => opt.setAttribute('aria-hidden', 'false'));
|
|
74
108
|
};
|
|
75
|
-
|
|
109
|
+
|
|
76
110
|
if (hasTransition()) {
|
|
77
111
|
popover.addEventListener('transitionend', resetFilter, { once: true });
|
|
78
112
|
} else {
|
|
79
113
|
resetFilter();
|
|
80
114
|
}
|
|
81
115
|
}
|
|
82
|
-
|
|
116
|
+
|
|
83
117
|
if (focusOnTrigger) trigger.focus();
|
|
84
118
|
popover.setAttribute('aria-hidden', 'true');
|
|
85
119
|
trigger.setAttribute('aria-expanded', 'false');
|
|
86
120
|
setActiveOption(-1);
|
|
87
|
-
}
|
|
121
|
+
};
|
|
88
122
|
|
|
89
|
-
const
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
123
|
+
const toggleMultipleValue = (option) => {
|
|
124
|
+
if (selectedOptions.has(option)) {
|
|
125
|
+
selectedOptions.delete(option);
|
|
126
|
+
} else {
|
|
127
|
+
selectedOptions.add(option);
|
|
128
|
+
}
|
|
129
|
+
updateValue(options.filter(opt => selectedOptions.has(opt)));
|
|
130
|
+
};
|
|
94
131
|
|
|
95
|
-
|
|
96
|
-
|
|
132
|
+
const select = (value) => {
|
|
133
|
+
if (isMultiple) {
|
|
134
|
+
const option = options.find(opt => getValue(opt) === value && !selectedOptions.has(opt));
|
|
135
|
+
if (!option) return;
|
|
136
|
+
selectedOptions.add(option);
|
|
137
|
+
updateValue(options.filter(opt => selectedOptions.has(opt)));
|
|
138
|
+
} else {
|
|
139
|
+
const option = options.find(opt => getValue(opt) === value);
|
|
140
|
+
if (!option) return;
|
|
141
|
+
if (input.value !== value) {
|
|
142
|
+
updateValue(option);
|
|
143
|
+
}
|
|
144
|
+
closePopover();
|
|
97
145
|
}
|
|
98
|
-
|
|
99
|
-
closePopover();
|
|
100
146
|
};
|
|
101
147
|
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
148
|
+
const deselect = (value) => {
|
|
149
|
+
if (!isMultiple) return;
|
|
150
|
+
const option = options.find(opt => getValue(opt) === value && selectedOptions.has(opt));
|
|
151
|
+
if (!option) return;
|
|
152
|
+
selectedOptions.delete(option);
|
|
153
|
+
updateValue(options.filter(opt => selectedOptions.has(opt)));
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const toggle = (value) => {
|
|
157
|
+
if (!isMultiple) return;
|
|
158
|
+
const option = options.find(opt => getValue(opt) === value);
|
|
159
|
+
if (!option) return;
|
|
160
|
+
toggleMultipleValue(option);
|
|
105
161
|
};
|
|
106
162
|
|
|
107
163
|
if (filter) {
|
|
108
164
|
const filterOptions = () => {
|
|
109
165
|
const searchTerm = filter.value.trim().toLowerCase();
|
|
110
|
-
|
|
166
|
+
|
|
111
167
|
setActiveOption(-1);
|
|
112
168
|
|
|
113
169
|
visibleOptions = [];
|
|
@@ -133,17 +189,37 @@
|
|
|
133
189
|
}
|
|
134
190
|
});
|
|
135
191
|
};
|
|
136
|
-
|
|
192
|
+
|
|
137
193
|
filter.addEventListener('input', filterOptions);
|
|
138
194
|
}
|
|
139
195
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
196
|
+
// Initialization
|
|
197
|
+
if (isMultiple) {
|
|
198
|
+
const ariaSelected = options.filter(opt => opt.getAttribute('aria-selected') === 'true');
|
|
199
|
+
try {
|
|
200
|
+
const parsed = JSON.parse(input.value || '[]');
|
|
201
|
+
const validValues = new Set(options.map(getValue));
|
|
202
|
+
const initialValues = Array.isArray(parsed) ? parsed.filter(v => validValues.has(v)) : [];
|
|
145
203
|
|
|
146
|
-
|
|
204
|
+
const initialOptions = [];
|
|
205
|
+
if (initialValues.length > 0) {
|
|
206
|
+
// Match values to options in order, allowing duplicates
|
|
207
|
+
initialValues.forEach(val => {
|
|
208
|
+
const opt = options.find(o => getValue(o) === val && !initialOptions.includes(o));
|
|
209
|
+
if (opt) initialOptions.push(opt);
|
|
210
|
+
});
|
|
211
|
+
} else {
|
|
212
|
+
initialOptions.push(...ariaSelected);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
updateValue(initialOptions, false);
|
|
216
|
+
} catch (e) {
|
|
217
|
+
updateValue(ariaSelected, false);
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
const initialOption = options.find(opt => getValue(opt) === input.value) || options[0];
|
|
221
|
+
if (initialOption) updateValue(initialOption, false);
|
|
222
|
+
}
|
|
147
223
|
|
|
148
224
|
const handleKeyNavigation = (event) => {
|
|
149
225
|
const isPopoverOpen = popover.getAttribute('aria-hidden') === 'false';
|
|
@@ -159,17 +235,25 @@
|
|
|
159
235
|
}
|
|
160
236
|
return;
|
|
161
237
|
}
|
|
162
|
-
|
|
238
|
+
|
|
163
239
|
event.preventDefault();
|
|
164
240
|
|
|
165
241
|
if (event.key === 'Escape') {
|
|
166
242
|
closePopover();
|
|
167
243
|
return;
|
|
168
244
|
}
|
|
169
|
-
|
|
245
|
+
|
|
170
246
|
if (event.key === 'Enter') {
|
|
171
247
|
if (activeIndex > -1) {
|
|
172
|
-
|
|
248
|
+
const option = options[activeIndex];
|
|
249
|
+
if (isMultiple) {
|
|
250
|
+
toggleMultipleValue(option);
|
|
251
|
+
} else {
|
|
252
|
+
if (input.value !== getValue(option)) {
|
|
253
|
+
updateValue(option);
|
|
254
|
+
}
|
|
255
|
+
closePopover();
|
|
256
|
+
}
|
|
173
257
|
}
|
|
174
258
|
return;
|
|
175
259
|
}
|
|
@@ -235,7 +319,7 @@
|
|
|
235
319
|
document.dispatchEvent(new CustomEvent('basecoat:popover', {
|
|
236
320
|
detail: { source: selectComponent }
|
|
237
321
|
}));
|
|
238
|
-
|
|
322
|
+
|
|
239
323
|
if (filter) {
|
|
240
324
|
if (hasTransition()) {
|
|
241
325
|
popover.addEventListener('transitionend', () => {
|
|
@@ -248,7 +332,7 @@
|
|
|
248
332
|
|
|
249
333
|
popover.setAttribute('aria-hidden', 'false');
|
|
250
334
|
trigger.setAttribute('aria-expanded', 'true');
|
|
251
|
-
|
|
335
|
+
|
|
252
336
|
const selectedOption = listbox.querySelector('[role="option"][aria-selected="true"]');
|
|
253
337
|
if (selectedOption) {
|
|
254
338
|
setActiveOption(options.indexOf(selectedOption));
|
|
@@ -267,8 +351,24 @@
|
|
|
267
351
|
|
|
268
352
|
listbox.addEventListener('click', (event) => {
|
|
269
353
|
const clickedOption = event.target.closest('[role="option"]');
|
|
270
|
-
if (clickedOption)
|
|
271
|
-
|
|
354
|
+
if (!clickedOption) return;
|
|
355
|
+
|
|
356
|
+
const option = options.find(opt => opt === clickedOption);
|
|
357
|
+
if (!option) return;
|
|
358
|
+
|
|
359
|
+
if (isMultiple) {
|
|
360
|
+
toggleMultipleValue(option);
|
|
361
|
+
setActiveOption(options.indexOf(option));
|
|
362
|
+
if (filter) {
|
|
363
|
+
filter.focus();
|
|
364
|
+
} else {
|
|
365
|
+
trigger.focus();
|
|
366
|
+
}
|
|
367
|
+
} else {
|
|
368
|
+
if (input.value !== getValue(option)) {
|
|
369
|
+
updateValue(option);
|
|
370
|
+
}
|
|
371
|
+
closePopover();
|
|
272
372
|
}
|
|
273
373
|
});
|
|
274
374
|
|
|
@@ -285,8 +385,43 @@
|
|
|
285
385
|
});
|
|
286
386
|
|
|
287
387
|
popover.setAttribute('aria-hidden', 'true');
|
|
288
|
-
|
|
289
|
-
|
|
388
|
+
|
|
389
|
+
// Public API
|
|
390
|
+
Object.defineProperty(selectComponent, 'value', {
|
|
391
|
+
get: () => {
|
|
392
|
+
if (isMultiple) {
|
|
393
|
+
return options.filter(opt => selectedOptions.has(opt)).map(getValue);
|
|
394
|
+
} else {
|
|
395
|
+
return input.value;
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
set: (val) => {
|
|
399
|
+
if (isMultiple) {
|
|
400
|
+
const values = Array.isArray(val) ? val : (val != null ? [val] : []);
|
|
401
|
+
const opts = [];
|
|
402
|
+
values.forEach(v => {
|
|
403
|
+
const opt = options.find(o => getValue(o) === v && !opts.includes(o));
|
|
404
|
+
if (opt) opts.push(opt);
|
|
405
|
+
});
|
|
406
|
+
updateValue(opts);
|
|
407
|
+
} else {
|
|
408
|
+
const option = options.find(opt => getValue(opt) === val);
|
|
409
|
+
if (option) {
|
|
410
|
+
updateValue(option);
|
|
411
|
+
closePopover();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
selectComponent.select = select;
|
|
418
|
+
selectComponent.selectByValue = select; // Backward compatibility alias
|
|
419
|
+
if (isMultiple) {
|
|
420
|
+
selectComponent.deselect = deselect;
|
|
421
|
+
selectComponent.toggle = toggle;
|
|
422
|
+
selectComponent.selectAll = () => updateValue(options);
|
|
423
|
+
selectComponent.selectNone = () => updateValue([]);
|
|
424
|
+
}
|
|
290
425
|
selectComponent.dataset.selectInitialized = true;
|
|
291
426
|
selectComponent.dispatchEvent(new CustomEvent('basecoat:initialized'));
|
|
292
427
|
};
|
package/dist/js/select.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
(()=>{const e=e=>{const t=e.querySelector(":scope > button"),
|
|
1
|
+
(()=>{const e=e=>{const t=e.querySelector(":scope > button"),r=t.querySelector(":scope > span"),n=e.querySelector(":scope > [data-popover]"),i=n?n.querySelector('[role="listbox"]'):null,a=e.querySelector(':scope > input[type="hidden"]'),s=e.querySelector('header input[type="text"]');if(!(t&&n&&i&&a)){const r=[];return t||r.push("trigger"),n||r.push("popover"),i||r.push("listbox"),a||r.push("input"),void console.error(`Select component initialisation failed. Missing element(s): ${r.join(", ")}`,e)}const o=Array.from(i.querySelectorAll('[role="option"]')),c=o.filter((e=>"true"!==e.getAttribute("aria-disabled")));let l=[...c],d=-1;const u="true"===i.getAttribute("aria-multiselectable"),f=u?new Set:null,p=u?e.dataset.placeholder||"":null,v=e=>e.dataset.value??e.textContent.trim(),h=e=>{if(d>-1&&c[d]&&c[d].classList.remove("active"),d=e,d>-1){const e=c[d];e.classList.add("active"),e.id?t.setAttribute("aria-activedescendant",e.id):t.removeAttribute("aria-activedescendant")}else t.removeAttribute("aria-activedescendant")},b=()=>{const e=getComputedStyle(n);return parseFloat(e.transitionDuration)>0||parseFloat(e.transitionDelay)>0},m=(t,n=!0)=>{let i;if(u){const e=Array.isArray(t)?t:[];f.clear(),e.forEach((e=>f.add(e)));const n=c.filter((e=>f.has(e)));0===n.length?(r.textContent=p,r.classList.add("text-muted-foreground")):(r.textContent=n.map((e=>e.dataset.label||e.textContent.trim())).join(", "),r.classList.remove("text-muted-foreground")),i=n.map(v),a.value=JSON.stringify(i)}else{const e=t;if(!e)return;r.innerHTML=e.innerHTML,i=v(e),a.value=i}c.forEach((e=>{(u?f.has(e):e===t)?e.setAttribute("aria-selected","true"):e.removeAttribute("aria-selected")})),n&&e.dispatchEvent(new CustomEvent("change",{detail:{value:i},bubbles:!0}))},y=(e=!0)=>{if("true"!==n.getAttribute("aria-hidden")){if(s){const e=()=>{s.value="",l=[...c],o.forEach((e=>e.setAttribute("aria-hidden","false")))};b()?n.addEventListener("transitionend",e,{once:!0}):e()}e&&t.focus(),n.setAttribute("aria-hidden","true"),t.setAttribute("aria-expanded","false"),h(-1)}},A=e=>{f.has(e)?f.delete(e):f.add(e),m(c.filter((e=>f.has(e))))},E=e=>{if(u){const t=c.find((t=>v(t)===e&&!f.has(t)));if(!t)return;f.add(t),m(c.filter((e=>f.has(e))))}else{const t=c.find((t=>v(t)===e));if(!t)return;a.value!==e&&m(t),y()}},g=e=>{if(!u)return;const t=c.find((t=>v(t)===e&&f.has(t)));t&&(f.delete(t),m(c.filter((e=>f.has(e)))))},w=e=>{if(!u)return;const t=c.find((t=>v(t)===e));t&&A(t)};if(s){const e=()=>{const e=s.value.trim().toLowerCase();h(-1),l=[],o.forEach((t=>{if(t.hasAttribute("data-force"))return t.setAttribute("aria-hidden","false"),void(c.includes(t)&&l.push(t));const r=(t.dataset.filter||t.textContent).trim().toLowerCase(),n=(t.dataset.keywords||"").toLowerCase().split(/[\s,]+/).filter(Boolean).some((t=>t.includes(e))),i=r.includes(e)||n;t.setAttribute("aria-hidden",String(!i)),i&&c.includes(t)&&l.push(t)}))};s.addEventListener("input",e)}if(u){const e=c.filter((e=>"true"===e.getAttribute("aria-selected")));try{const t=JSON.parse(a.value||"[]"),r=new Set(c.map(v)),n=Array.isArray(t)?t.filter((e=>r.has(e))):[],i=[];n.length>0?n.forEach((e=>{const t=c.find((t=>v(t)===e&&!i.includes(t)));t&&i.push(t)})):i.push(...e),m(i,!1)}catch(t){m(e,!1)}}else{const e=c.find((e=>v(e)===a.value))||c[0];e&&m(e,!1)}const L=e=>{const r="false"===n.getAttribute("aria-hidden");if(!["ArrowDown","ArrowUp","Enter","Home","End","Escape"].includes(e.key))return;if(!r)return void("Enter"!==e.key&&"Escape"!==e.key&&(e.preventDefault(),t.click()));if(e.preventDefault(),"Escape"===e.key)return void y();if("Enter"===e.key){if(d>-1){const e=c[d];u?A(e):(a.value!==v(e)&&m(e),y())}return}if(0===l.length)return;const i=d>-1?l.indexOf(c[d]):-1;let s=i;switch(e.key){case"ArrowDown":i<l.length-1&&(s=i+1);break;case"ArrowUp":i>0?s=i-1:-1===i&&(s=0);break;case"Home":s=0;break;case"End":s=l.length-1}if(s!==i){const e=l[s];h(c.indexOf(e)),e.scrollIntoView({block:"nearest",behavior:"smooth"})}};i.addEventListener("mousemove",(e=>{const t=e.target.closest('[role="option"]');if(t&&l.includes(t)){const e=c.indexOf(t);e!==d&&h(e)}})),i.addEventListener("mouseleave",(()=>{const e=i.querySelector('[role="option"][aria-selected="true"]');h(e?c.indexOf(e):-1)})),t.addEventListener("keydown",L),s&&s.addEventListener("keydown",L);t.addEventListener("click",(()=>{"true"===t.getAttribute("aria-expanded")?y():(()=>{document.dispatchEvent(new CustomEvent("basecoat:popover",{detail:{source:e}})),s&&(b()?n.addEventListener("transitionend",(()=>{s.focus()}),{once:!0}):s.focus()),n.setAttribute("aria-hidden","false"),t.setAttribute("aria-expanded","true");const r=i.querySelector('[role="option"][aria-selected="true"]');r&&(h(c.indexOf(r)),r.scrollIntoView({block:"nearest"}))})()})),i.addEventListener("click",(e=>{const r=e.target.closest('[role="option"]');if(!r)return;const n=c.find((e=>e===r));n&&(u?(A(n),h(c.indexOf(n)),s?s.focus():t.focus()):(a.value!==v(n)&&m(n),y()))})),document.addEventListener("click",(t=>{e.contains(t.target)||y(!1)})),document.addEventListener("basecoat:popover",(t=>{t.detail.source!==e&&y(!1)})),n.setAttribute("aria-hidden","true"),Object.defineProperty(e,"value",{get:()=>u?c.filter((e=>f.has(e))).map(v):a.value,set:e=>{if(u){const t=Array.isArray(e)?e:null!=e?[e]:[],r=[];t.forEach((e=>{const t=c.find((t=>v(t)===e&&!r.includes(t)));t&&r.push(t)})),m(r)}else{const t=c.find((t=>v(t)===e));t&&(m(t),y())}}}),e.select=E,e.selectByValue=E,u&&(e.deselect=g,e.toggle=w,e.selectAll=()=>m(c),e.selectNone=()=>m([])),e.dataset.selectInitialized=!0,e.dispatchEvent(new CustomEvent("basecoat:initialized"))};window.basecoat&&window.basecoat.register("select","div.select:not([data-select-initialized])",e)})();
|