@studiocms/ui 1.0.0-beta.4 → 1.0.0-beta.6
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/components/Input/input.css +1 -0
- package/dist/components/Modal/modal.css +1 -0
- package/dist/components/SearchSelect/SearchSelect.astro +2 -2
- package/dist/components/SearchSelect/searchselect.d.ts +1 -19
- package/dist/components/SearchSelect/searchselect.js +147 -201
- package/dist/components/Select/Select.astro +4 -5
- package/dist/components/Select/select.d.ts +52 -11
- package/dist/components/Select/select.js +140 -150
- package/dist/index.js +3 -0
- package/dist/utils/headers.d.ts +1 -1
- package/dist/virtuals.d.ts +10 -0
- package/package.json +1 -1
|
@@ -132,7 +132,7 @@ const defaultLabel = selected
|
|
|
132
132
|
: placeholder;
|
|
133
133
|
---
|
|
134
134
|
|
|
135
|
-
<
|
|
135
|
+
<sui-combobox
|
|
136
136
|
id={`${name}-container`}
|
|
137
137
|
class="sui-search-select-label"
|
|
138
138
|
class:list={[disabled && "disabled", className, fullWidth && "full"]}
|
|
@@ -222,7 +222,7 @@ const defaultLabel = selected
|
|
|
222
222
|
</div>
|
|
223
223
|
)
|
|
224
224
|
}
|
|
225
|
-
</
|
|
225
|
+
</sui-combobox>
|
|
226
226
|
<script>
|
|
227
227
|
import "studiocms:ui/scripts/searchselect"
|
|
228
228
|
</script>
|
|
@@ -1,19 +1 @@
|
|
|
1
|
-
|
|
2
|
-
label: string;
|
|
3
|
-
value: string;
|
|
4
|
-
disabled?: boolean;
|
|
5
|
-
};
|
|
6
|
-
type SearchSelectContainer = HTMLDivElement & {
|
|
7
|
-
input: HTMLInputElement | null;
|
|
8
|
-
dropdown: Element | null;
|
|
9
|
-
select: HTMLSelectElement | null;
|
|
10
|
-
};
|
|
11
|
-
type SearchSelectState = {
|
|
12
|
-
optionsMap: Record<string, SelectOption[]>;
|
|
13
|
-
isMultipleMap: Record<string, boolean>;
|
|
14
|
-
selectedOptionsMap: Record<string, string[]>;
|
|
15
|
-
placeholderMap: Record<string, string>;
|
|
16
|
-
focusIndex: number;
|
|
17
|
-
isSelectingOption: boolean;
|
|
18
|
-
};
|
|
19
|
-
declare function loadSearchSelects(): void;
|
|
1
|
+
export {};
|
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const customRect = {
|
|
12
|
-
top: rect.bottom + CONSTANTS.MARGIN,
|
|
13
|
-
bottom: rect.bottom + CONSTANTS.MARGIN + dropdownHeight,
|
|
14
|
-
left: rect.left,
|
|
15
|
-
right: rect.right,
|
|
16
|
-
width: rect.width,
|
|
17
|
-
x: rect.x,
|
|
18
|
-
y: rect.y + rect.height + CONSTANTS.MARGIN,
|
|
19
|
-
height: dropdownHeight
|
|
20
|
-
};
|
|
21
|
-
return {
|
|
22
|
-
isAbove: customRect.top >= 0 && customRect.left >= 0 && customRect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && customRect.right <= (window.innerWidth || document.documentElement.clientWidth),
|
|
23
|
-
customRect
|
|
24
|
-
};
|
|
1
|
+
import { SUISelectElement } from "studiocms:ui/components/select/script";
|
|
2
|
+
class SUIComboboxElement extends SUISelectElement {
|
|
3
|
+
input;
|
|
4
|
+
state = {
|
|
5
|
+
options: [],
|
|
6
|
+
isMultiple: false,
|
|
7
|
+
focusIndex: -1,
|
|
8
|
+
placeholder: "",
|
|
9
|
+
selectedOptions: [],
|
|
10
|
+
isSelectingOption: false
|
|
25
11
|
};
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
"sui-search-select-badge"
|
|
12
|
+
constructor() {
|
|
13
|
+
super();
|
|
14
|
+
}
|
|
15
|
+
connectedCallback() {
|
|
16
|
+
this.input = this.querySelector("input");
|
|
17
|
+
this.dropdown = this.querySelector(".sui-search-select-dropdown-list");
|
|
18
|
+
const selectedOptions = Array.from(
|
|
19
|
+
this.dropdown?.querySelectorAll(".sui-search-select-option.selected") ?? []
|
|
35
20
|
);
|
|
36
|
-
|
|
37
|
-
|
|
21
|
+
this.state.placeholder = this.input?.placeholder ?? "";
|
|
22
|
+
this.state.options = JSON.parse(this.dataset.options ?? "{}");
|
|
23
|
+
this.state.isMultiple = this.dataset.multiple === "true";
|
|
24
|
+
this.state.selectedOptions = selectedOptions.map((x) => x.getAttribute("data-value") ?? "");
|
|
25
|
+
this.input?.addEventListener("focusin", () => this.handleContainerFocusIn());
|
|
26
|
+
this.addEventListener("focusout", () => this.handleContainerFocusOut());
|
|
27
|
+
this.addEventListener("keydown", (e) => this.handleSelectKeyDown(e));
|
|
28
|
+
this.input?.addEventListener("keyup", (e) => this.handleInputKeyup(e));
|
|
29
|
+
this.addEventListener("mousedown", (e) => this.handleContainerMouseDown(e));
|
|
30
|
+
if (this.state.isMultiple) {
|
|
31
|
+
this.recalculateBadges();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
createSearchSelectBadge = (value, label) => {
|
|
35
|
+
const badge = this.createSelectBadge(value, label);
|
|
36
|
+
badge.classList.remove("sui-select-badge");
|
|
37
|
+
badge.classList.add("sui-search-select-badge");
|
|
38
38
|
return badge;
|
|
39
39
|
};
|
|
40
|
-
|
|
41
|
-
const badgeContainer =
|
|
42
|
-
if (!badgeContainer || !
|
|
40
|
+
recalculateBadges = () => {
|
|
41
|
+
const badgeContainer = this.querySelector(".sui-search-select-badge-container");
|
|
42
|
+
if (!badgeContainer || !this.input) return;
|
|
43
43
|
badgeContainer.innerHTML = "";
|
|
44
|
-
const selectedValues =
|
|
45
|
-
const allOptions =
|
|
44
|
+
const selectedValues = this.state.selectedOptions;
|
|
45
|
+
const allOptions = this.state.options;
|
|
46
46
|
if (selectedValues.length === 0) {
|
|
47
|
-
|
|
47
|
+
this.input.placeholder = this.state.placeholder ?? "";
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
50
50
|
for (const value of selectedValues.sort((a, b) => {
|
|
@@ -54,46 +54,44 @@ function loadSearchSelects() {
|
|
|
54
54
|
})) {
|
|
55
55
|
const option = allOptions.find((opt) => opt.value === value);
|
|
56
56
|
if (option) {
|
|
57
|
-
const newBadge =
|
|
57
|
+
const newBadge = this.createSearchSelectBadge(value, option.label);
|
|
58
58
|
badgeContainer.appendChild(newBadge);
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
};
|
|
62
|
-
|
|
63
|
-
const selectedInput =
|
|
64
|
-
if (isMultiple) {
|
|
65
|
-
recalculateBadges(
|
|
62
|
+
updateLabel = () => {
|
|
63
|
+
const selectedInput = this.input;
|
|
64
|
+
if (this.state.isMultiple) {
|
|
65
|
+
this.recalculateBadges();
|
|
66
66
|
if (selectedInput) {
|
|
67
|
-
selectedInput.placeholder =
|
|
67
|
+
selectedInput.placeholder = this.state.placeholder;
|
|
68
68
|
}
|
|
69
69
|
} else {
|
|
70
|
-
const selected =
|
|
71
|
-
".sui-search-select-option.selected"
|
|
72
|
-
);
|
|
70
|
+
const selected = this.querySelector(".sui-search-select-option.selected");
|
|
73
71
|
if (selected && selectedInput) {
|
|
74
72
|
selectedInput.placeholder = selected.innerText.trim();
|
|
75
73
|
}
|
|
76
74
|
}
|
|
77
75
|
};
|
|
78
|
-
|
|
79
|
-
const currentSelected =
|
|
76
|
+
updateOptionSelection = (value, forceState) => {
|
|
77
|
+
const currentSelected = this.state.selectedOptions;
|
|
80
78
|
const isCurrentlySelected = currentSelected.includes(value);
|
|
81
|
-
const max = Number.parseInt(
|
|
79
|
+
const max = Number.parseInt(this.dataset.multipleMax, 10);
|
|
82
80
|
if (!isCurrentlySelected && !Number.isNaN(max) && currentSelected.length >= max) {
|
|
83
81
|
return false;
|
|
84
82
|
}
|
|
85
83
|
const newSelected = isCurrentlySelected ? currentSelected.filter((v) => v !== value) : [...currentSelected, value];
|
|
86
|
-
|
|
87
|
-
const option =
|
|
84
|
+
this.state.selectedOptions = newSelected;
|
|
85
|
+
const option = this.dropdown?.querySelector(
|
|
88
86
|
`.sui-search-select-option[data-value='${value}']`
|
|
89
87
|
);
|
|
90
88
|
if (option) {
|
|
91
89
|
option.classList.toggle("selected", forceState ?? !isCurrentlySelected);
|
|
92
|
-
if (
|
|
93
|
-
|
|
90
|
+
if (this.select) {
|
|
91
|
+
this.select.value = option.getAttribute("value");
|
|
94
92
|
}
|
|
95
93
|
}
|
|
96
|
-
const selectedCountEl =
|
|
94
|
+
const selectedCountEl = this.querySelector(
|
|
97
95
|
".sui-search-select-max-span .sui-search-select-select-count"
|
|
98
96
|
);
|
|
99
97
|
if (selectedCountEl) {
|
|
@@ -101,29 +99,29 @@ function loadSearchSelects() {
|
|
|
101
99
|
}
|
|
102
100
|
return true;
|
|
103
101
|
};
|
|
104
|
-
|
|
105
|
-
const success = updateOptionSelection(id
|
|
102
|
+
toggleMultiOption = (id) => {
|
|
103
|
+
const success = this.updateOptionSelection(id);
|
|
106
104
|
if (success) {
|
|
107
|
-
recalculateBadges(
|
|
105
|
+
this.recalculateBadges();
|
|
108
106
|
}
|
|
109
107
|
};
|
|
110
|
-
|
|
111
|
-
const optionElements =
|
|
108
|
+
recomputeOptions = () => {
|
|
109
|
+
const optionElements = this.dropdown?.querySelectorAll(
|
|
112
110
|
".sui-search-select-option"
|
|
113
111
|
);
|
|
114
112
|
for (const entry of optionElements) {
|
|
115
|
-
if (Number.parseInt(entry.dataset.optionIndex, 10) ===
|
|
113
|
+
if (Number.parseInt(entry.dataset.optionIndex, 10) === this.state.focusIndex) {
|
|
116
114
|
entry.classList.add("focused");
|
|
117
115
|
} else {
|
|
118
116
|
entry.classList.remove("focused");
|
|
119
117
|
}
|
|
120
118
|
}
|
|
121
119
|
};
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const selectedValues =
|
|
120
|
+
reconstructOptions = (filteredOptions) => {
|
|
121
|
+
this.dropdown.innerHTML = "";
|
|
122
|
+
const selectedValues = this.state.selectedOptions;
|
|
125
123
|
if (filteredOptions.length === 0) {
|
|
126
|
-
|
|
124
|
+
this.dropdown.innerHTML = '<li class="empty-search-results">No results found</li>';
|
|
127
125
|
return;
|
|
128
126
|
}
|
|
129
127
|
let i = 0;
|
|
@@ -140,101 +138,101 @@ function loadSearchSelects() {
|
|
|
140
138
|
element.dataset.optionIndex = i.toString();
|
|
141
139
|
element.dataset.value = option.value;
|
|
142
140
|
element.textContent = option.label;
|
|
143
|
-
|
|
141
|
+
this.dropdown?.appendChild(element);
|
|
144
142
|
i++;
|
|
145
143
|
}
|
|
146
144
|
};
|
|
147
|
-
|
|
148
|
-
const allOptions =
|
|
145
|
+
getInteractiveOptions = () => {
|
|
146
|
+
const allOptions = this.dropdown?.querySelectorAll(
|
|
149
147
|
".sui-search-select-option"
|
|
150
148
|
);
|
|
151
149
|
return Array.from(allOptions).filter(
|
|
152
150
|
(option) => !option.classList.contains("hidden") && !option.classList.contains("disabled") && !option.hasAttribute("disabled")
|
|
153
151
|
);
|
|
154
152
|
};
|
|
155
|
-
|
|
153
|
+
handleContainerMouseDown = (e) => {
|
|
156
154
|
const target = e.target;
|
|
157
155
|
if (!target.closest("input")) {
|
|
158
156
|
e.preventDefault();
|
|
159
157
|
}
|
|
160
|
-
if (
|
|
161
|
-
reconstructOptions(
|
|
158
|
+
if (this.input?.value.length === 0) {
|
|
159
|
+
this.reconstructOptions(this.state.options);
|
|
162
160
|
}
|
|
163
161
|
if (target.closest(".sui-search-select-indicator")) {
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
162
|
+
if (this.dropdown?.parentElement?.classList.contains("active")) {
|
|
163
|
+
this.dropdown?.parentElement?.classList.remove("active", "above");
|
|
164
|
+
this.input?.blur();
|
|
165
|
+
this.input.value = "";
|
|
168
166
|
} else {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
167
|
+
this.dropdown?.parentElement?.classList.add("active");
|
|
168
|
+
this.input?.focus();
|
|
169
|
+
this.input.value = "";
|
|
172
170
|
}
|
|
173
171
|
return;
|
|
174
172
|
}
|
|
175
173
|
if (target.closest(".sui-search-select-badge-container")) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
174
|
+
this.dropdown?.parentElement?.classList.remove("active", "above");
|
|
175
|
+
this.input?.blur();
|
|
176
|
+
this.input.value = "";
|
|
179
177
|
}
|
|
180
|
-
|
|
178
|
+
this.state.isSelectingOption = true;
|
|
181
179
|
setTimeout(() => {
|
|
182
|
-
|
|
180
|
+
this.state.isSelectingOption = false;
|
|
183
181
|
}, 0);
|
|
184
182
|
if (target.closest(".sui-search-select-badge svg")) {
|
|
185
183
|
const value = target.closest(".sui-search-select-badge")?.getAttribute("data-value");
|
|
186
|
-
const success = updateOptionSelection(value
|
|
184
|
+
const success = this.updateOptionSelection(value);
|
|
187
185
|
if (success) {
|
|
188
|
-
recalculateBadges(
|
|
186
|
+
this.recalculateBadges();
|
|
189
187
|
}
|
|
190
188
|
return;
|
|
191
189
|
}
|
|
192
190
|
const opt = target.closest(".sui-search-select-option");
|
|
193
191
|
if (!opt?.dataset.value) return;
|
|
194
192
|
if (opt.classList.contains("disabled") || opt.hasAttribute("disabled")) {
|
|
195
|
-
|
|
193
|
+
this.input?.focus();
|
|
196
194
|
return;
|
|
197
195
|
}
|
|
198
|
-
const isMultiple =
|
|
196
|
+
const isMultiple = this.state.isMultiple;
|
|
199
197
|
if (isMultiple) {
|
|
200
|
-
const success = updateOptionSelection(opt.dataset.value
|
|
198
|
+
const success = this.updateOptionSelection(opt.dataset.value);
|
|
201
199
|
if (success) {
|
|
202
|
-
updateLabel(
|
|
203
|
-
recalculateBadges(
|
|
200
|
+
this.updateLabel();
|
|
201
|
+
this.recalculateBadges();
|
|
204
202
|
}
|
|
205
203
|
} else {
|
|
206
|
-
const currentSelected =
|
|
204
|
+
const currentSelected = this.state.selectedOptions;
|
|
207
205
|
for (const value of currentSelected) {
|
|
208
|
-
updateOptionSelection(value,
|
|
206
|
+
this.updateOptionSelection(value, false);
|
|
209
207
|
}
|
|
210
|
-
updateOptionSelection(opt.dataset.value,
|
|
211
|
-
updateLabel(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
208
|
+
this.updateOptionSelection(opt.dataset.value, true);
|
|
209
|
+
this.updateLabel();
|
|
210
|
+
this.dropdown?.parentElement?.classList.remove("active", "above");
|
|
211
|
+
this.input?.blur();
|
|
212
|
+
this.input.value = "";
|
|
215
213
|
}
|
|
216
214
|
};
|
|
217
|
-
|
|
215
|
+
handleSelectKeyDown = (e) => {
|
|
218
216
|
const focusedElement = document.activeElement;
|
|
219
217
|
if (e.key === "Escape" || e.key === "Tab") {
|
|
220
|
-
|
|
221
|
-
|
|
218
|
+
this.input?.blur();
|
|
219
|
+
this.dropdown?.parentElement?.classList.remove("active", "above");
|
|
222
220
|
return;
|
|
223
221
|
}
|
|
224
222
|
if ((e.key === "Enter" || e.key === " ") && focusedElement?.tagName.toLowerCase() === "svg") {
|
|
225
223
|
const badgeElement = focusedElement?.closest(".sui-search-select-badge");
|
|
226
|
-
if (badgeElement &&
|
|
224
|
+
if (badgeElement && this.state.isMultiple) {
|
|
227
225
|
const badgeValue = badgeElement.getAttribute("data-value");
|
|
228
226
|
let nextBadge = badgeElement.previousElementSibling;
|
|
229
227
|
if (!nextBadge) {
|
|
230
228
|
nextBadge = badgeElement.nextElementSibling;
|
|
231
229
|
}
|
|
232
230
|
const nextBadgeValue = nextBadge?.getAttribute("data-value");
|
|
233
|
-
toggleMultiOption(badgeValue
|
|
234
|
-
recalculateBadges(
|
|
231
|
+
this.toggleMultiOption(badgeValue);
|
|
232
|
+
this.recalculateBadges();
|
|
235
233
|
setTimeout(() => {
|
|
236
234
|
if (nextBadgeValue) {
|
|
237
|
-
const badgeToFocus =
|
|
235
|
+
const badgeToFocus = this.querySelector(
|
|
238
236
|
`.sui-search-select-badge[data-value="${nextBadgeValue}"] svg`
|
|
239
237
|
);
|
|
240
238
|
if (badgeToFocus) {
|
|
@@ -247,150 +245,98 @@ function loadSearchSelects() {
|
|
|
247
245
|
return;
|
|
248
246
|
}
|
|
249
247
|
}
|
|
250
|
-
const interactiveOptions = getInteractiveOptions(
|
|
248
|
+
const interactiveOptions = this.getInteractiveOptions();
|
|
251
249
|
const currentInteractiveIndex = interactiveOptions.findIndex(
|
|
252
250
|
(option) => option.classList.contains("focused")
|
|
253
251
|
);
|
|
254
252
|
if (e.key === "ArrowUp" && currentInteractiveIndex > 0) {
|
|
255
|
-
|
|
256
|
-
|
|
253
|
+
this.state.focusIndex = Array.from(
|
|
254
|
+
this.dropdown?.querySelectorAll(".sui-search-select-option") || []
|
|
257
255
|
).indexOf(interactiveOptions[currentInteractiveIndex - 1]);
|
|
258
|
-
recomputeOptions(
|
|
256
|
+
this.recomputeOptions();
|
|
259
257
|
return;
|
|
260
258
|
}
|
|
261
259
|
if (e.key === "ArrowDown" && currentInteractiveIndex < interactiveOptions.length - 1) {
|
|
262
|
-
|
|
263
|
-
|
|
260
|
+
this.state.focusIndex = Array.from(
|
|
261
|
+
this.dropdown?.querySelectorAll(".sui-search-select-option") || []
|
|
264
262
|
).indexOf(interactiveOptions[currentInteractiveIndex + 1]);
|
|
265
|
-
recomputeOptions(
|
|
263
|
+
this.recomputeOptions();
|
|
266
264
|
return;
|
|
267
265
|
}
|
|
268
266
|
if (e.key === "PageUp") {
|
|
269
|
-
|
|
270
|
-
|
|
267
|
+
this.state.focusIndex = Array.from(
|
|
268
|
+
this.dropdown?.querySelectorAll(".sui-search-select-option") || []
|
|
271
269
|
).indexOf(interactiveOptions[0]);
|
|
272
|
-
recomputeOptions(
|
|
270
|
+
this.recomputeOptions();
|
|
273
271
|
return;
|
|
274
272
|
}
|
|
275
273
|
if (e.key === "PageDown") {
|
|
276
|
-
|
|
277
|
-
|
|
274
|
+
this.state.focusIndex = Array.from(
|
|
275
|
+
this.dropdown?.querySelectorAll(".sui-search-select-option") || []
|
|
278
276
|
).indexOf(interactiveOptions[interactiveOptions.length - 1]);
|
|
279
|
-
recomputeOptions(
|
|
277
|
+
this.recomputeOptions();
|
|
280
278
|
return;
|
|
281
279
|
}
|
|
282
280
|
if (e.key === "Enter") {
|
|
283
281
|
e.preventDefault();
|
|
284
282
|
e.stopImmediatePropagation();
|
|
285
|
-
const optionElements =
|
|
283
|
+
const optionElements = this.dropdown?.querySelectorAll(
|
|
286
284
|
".sui-search-select-option"
|
|
287
285
|
);
|
|
288
286
|
const focusedOption = Array.from(optionElements).find(
|
|
289
|
-
(entry) => Number.parseInt(entry.dataset.optionIndex, 10) ===
|
|
287
|
+
(entry) => Number.parseInt(entry.dataset.optionIndex, 10) === this.state.focusIndex
|
|
290
288
|
);
|
|
291
289
|
if (focusedOption && !focusedOption.classList.contains("disabled") && !focusedOption.hasAttribute("disabled")) {
|
|
292
290
|
const value = focusedOption.dataset.value;
|
|
293
291
|
if (!value) return;
|
|
294
|
-
const isMultiple =
|
|
292
|
+
const isMultiple = this.state.isMultiple;
|
|
295
293
|
if (isMultiple) {
|
|
296
|
-
const success = updateOptionSelection(value
|
|
294
|
+
const success = this.updateOptionSelection(value);
|
|
297
295
|
if (success) {
|
|
298
|
-
updateLabel(
|
|
299
|
-
recalculateBadges(
|
|
296
|
+
this.updateLabel();
|
|
297
|
+
this.recalculateBadges();
|
|
300
298
|
}
|
|
301
299
|
} else {
|
|
302
|
-
const currentSelected =
|
|
300
|
+
const currentSelected = this.state.selectedOptions;
|
|
303
301
|
for (const existingValue of currentSelected) {
|
|
304
|
-
updateOptionSelection(existingValue,
|
|
302
|
+
this.updateOptionSelection(existingValue, false);
|
|
305
303
|
}
|
|
306
|
-
updateOptionSelection(value,
|
|
307
|
-
updateLabel(
|
|
308
|
-
|
|
309
|
-
|
|
304
|
+
this.updateOptionSelection(value, true);
|
|
305
|
+
this.updateLabel();
|
|
306
|
+
this.dropdown?.parentElement?.classList.remove("active", "above");
|
|
307
|
+
this.input.value = "";
|
|
310
308
|
}
|
|
311
309
|
}
|
|
312
310
|
return;
|
|
313
311
|
}
|
|
314
312
|
};
|
|
315
|
-
|
|
313
|
+
handleInputKeyup = (e) => {
|
|
316
314
|
if (["Enter", "ArrowUp", "ArrowDown"].includes(e.key)) return;
|
|
317
|
-
const value =
|
|
318
|
-
const allOptions =
|
|
315
|
+
const value = this.input.value.trim().toLowerCase();
|
|
316
|
+
const allOptions = this.state.options;
|
|
319
317
|
if (value.length === 0) {
|
|
320
|
-
reconstructOptions(allOptions
|
|
318
|
+
this.reconstructOptions(allOptions);
|
|
321
319
|
return;
|
|
322
320
|
}
|
|
323
|
-
const filteredOptions = allOptions
|
|
324
|
-
|
|
325
|
-
reconstructOptions(filteredOptions
|
|
321
|
+
const filteredOptions = allOptions.filter((option) => option.label.toLowerCase().includes(value)) ?? [];
|
|
322
|
+
this.state.focusIndex = 0;
|
|
323
|
+
this.reconstructOptions(filteredOptions);
|
|
326
324
|
};
|
|
327
|
-
|
|
328
|
-
if (
|
|
329
|
-
|
|
330
|
-
reconstructOptions(
|
|
331
|
-
|
|
325
|
+
handleContainerFocusOut = () => {
|
|
326
|
+
if (this.state.isSelectingOption) return;
|
|
327
|
+
this.input.value = "";
|
|
328
|
+
this.reconstructOptions(this.state.options);
|
|
329
|
+
this.dropdown?.parentElement?.classList.remove("active", "above");
|
|
332
330
|
};
|
|
333
|
-
|
|
331
|
+
handleContainerFocusIn = () => {
|
|
334
332
|
const allDropdowns = document.querySelectorAll(".sui-search-select-dropdown-list");
|
|
335
333
|
for (const dropdown of allDropdowns) {
|
|
336
|
-
if (dropdown !==
|
|
334
|
+
if (dropdown !== this.dropdown) {
|
|
337
335
|
dropdown.parentElement?.classList.remove("active", "above");
|
|
338
336
|
}
|
|
339
337
|
}
|
|
340
|
-
const { isAbove } = getDropdownPosition(
|
|
341
|
-
|
|
342
|
-
state2.optionsMap[container.dataset.id]?.length ?? 0
|
|
343
|
-
);
|
|
344
|
-
container.dropdown?.parentElement?.classList.add("active", ...isAbove ? [] : ["above"]);
|
|
345
|
-
};
|
|
346
|
-
const state = {
|
|
347
|
-
optionsMap: {},
|
|
348
|
-
isMultipleMap: {},
|
|
349
|
-
placeholderMap: {},
|
|
350
|
-
selectedOptionsMap: {},
|
|
351
|
-
focusIndex: 0,
|
|
352
|
-
isSelectingOption: false
|
|
338
|
+
const { isAbove } = this.getDropdownPosition(this.input);
|
|
339
|
+
this.dropdown?.parentElement?.classList.add("active", ...isAbove ? [] : ["above"]);
|
|
353
340
|
};
|
|
354
|
-
const selects = document.querySelectorAll(".sui-search-select-label");
|
|
355
|
-
for (const container of selects) {
|
|
356
|
-
if (container.dataset.initialized === "true") continue;
|
|
357
|
-
const id = container.dataset.id;
|
|
358
|
-
const specialContainer = Object.assign(container, {
|
|
359
|
-
input: container.querySelector("input"),
|
|
360
|
-
dropdown: container.querySelector(".sui-search-select-dropdown-list"),
|
|
361
|
-
select: container.querySelector("select")
|
|
362
|
-
});
|
|
363
|
-
const selectedOptions = Array.from(
|
|
364
|
-
specialContainer.dropdown?.querySelectorAll(".sui-search-select-option.selected") ?? []
|
|
365
|
-
);
|
|
366
|
-
state.placeholderMap[id] = specialContainer.input?.placeholder ?? "";
|
|
367
|
-
state.optionsMap[id] = JSON.parse(container.dataset.options ?? "{}");
|
|
368
|
-
state.isMultipleMap[id] = container.dataset.multiple === "true";
|
|
369
|
-
state.selectedOptionsMap[id] = selectedOptions.map((x) => x.getAttribute("data-value") ?? "");
|
|
370
|
-
specialContainer.input?.addEventListener(
|
|
371
|
-
"focusin",
|
|
372
|
-
() => handleContainerFocusIn(state, specialContainer)
|
|
373
|
-
);
|
|
374
|
-
specialContainer.addEventListener(
|
|
375
|
-
"focusout",
|
|
376
|
-
() => handleContainerFocusOut(state, specialContainer)
|
|
377
|
-
);
|
|
378
|
-
specialContainer.addEventListener(
|
|
379
|
-
"keydown",
|
|
380
|
-
(e) => handleSelectKeyDown(e, state, specialContainer)
|
|
381
|
-
);
|
|
382
|
-
specialContainer.input?.addEventListener(
|
|
383
|
-
"keyup",
|
|
384
|
-
(e) => handleInputKeyup(e, state, specialContainer)
|
|
385
|
-
);
|
|
386
|
-
specialContainer.addEventListener(
|
|
387
|
-
"mousedown",
|
|
388
|
-
(e) => handleContainerMouseDown(e, state, specialContainer)
|
|
389
|
-
);
|
|
390
|
-
if (state.isMultipleMap[id]) {
|
|
391
|
-
recalculateBadges(state, specialContainer);
|
|
392
|
-
}
|
|
393
|
-
container.dataset.initialized = "true";
|
|
394
|
-
}
|
|
395
341
|
}
|
|
396
|
-
|
|
342
|
+
customElements.define("sui-combobox", SUIComboboxElement);
|
|
@@ -131,14 +131,13 @@ const defaultLabel = selected
|
|
|
131
131
|
: placeholder;
|
|
132
132
|
---
|
|
133
133
|
|
|
134
|
-
<
|
|
134
|
+
<sui-select
|
|
135
135
|
id={`${name}-container`}
|
|
136
136
|
class="sui-select-label"
|
|
137
137
|
class:list={[disabled && "disabled", className, fullWidth && "full"]}
|
|
138
138
|
data-options={JSON.stringify(options)}
|
|
139
139
|
data-multiple={multiple ? "true" : undefined}
|
|
140
140
|
data-multiple-max={multiple && max !== undefined ? max : undefined}
|
|
141
|
-
data-id={name}
|
|
142
141
|
>
|
|
143
142
|
{label && (
|
|
144
143
|
<label class="label" for={`${name}-select-btn`}>
|
|
@@ -200,7 +199,7 @@ const defaultLabel = selected
|
|
|
200
199
|
const isSelected = Array.isArray(selected)
|
|
201
200
|
? selected.map((y) => y && y.value).includes(x.value)
|
|
202
201
|
: selected?.value === x.value;
|
|
203
|
-
|
|
202
|
+
|
|
204
203
|
return (
|
|
205
204
|
<option
|
|
206
205
|
value={x.value}
|
|
@@ -219,11 +218,11 @@ const defaultLabel = selected
|
|
|
219
218
|
</span>
|
|
220
219
|
)}
|
|
221
220
|
{
|
|
222
|
-
multiple && Array.isArray(selected ?? []) && (
|
|
221
|
+
multiple && Array.isArray(selected ?? []) && (
|
|
223
222
|
<div class="sui-select-badge-container-below"></div>
|
|
224
223
|
)
|
|
225
224
|
}
|
|
226
|
-
</
|
|
225
|
+
</sui-select>
|
|
227
226
|
<script>
|
|
228
227
|
import "studiocms:ui/scripts/select"
|
|
229
228
|
</script>
|
|
@@ -1,18 +1,59 @@
|
|
|
1
|
-
type SelectOption = {
|
|
1
|
+
export type SelectOption = {
|
|
2
2
|
value: string;
|
|
3
3
|
label: string;
|
|
4
4
|
disabled?: boolean;
|
|
5
5
|
};
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
select: HTMLSelectElement | null;
|
|
10
|
-
};
|
|
11
|
-
type SelectState = {
|
|
12
|
-
optionsMap: Record<string, SelectOption[]>;
|
|
13
|
-
isMultipleMap: Record<string, boolean>;
|
|
6
|
+
interface SelectState {
|
|
7
|
+
options: SelectOption[];
|
|
8
|
+
isMultiple: boolean;
|
|
14
9
|
focusIndex: number;
|
|
15
10
|
placeholder: string;
|
|
16
|
-
}
|
|
11
|
+
}
|
|
17
12
|
type ResizeCallback = (width: number, height: number, element: Element) => void;
|
|
18
|
-
declare
|
|
13
|
+
export declare class SUISelectElement extends HTMLElement {
|
|
14
|
+
readonly CONSTANTS: {
|
|
15
|
+
OPTION_HEIGHT: number;
|
|
16
|
+
BORDER_SIZE: number;
|
|
17
|
+
MARGIN: number;
|
|
18
|
+
BADGE_PADDING: number;
|
|
19
|
+
};
|
|
20
|
+
observerMap: WeakMap<Element, {
|
|
21
|
+
observer: ResizeObserver;
|
|
22
|
+
callback: ResizeCallback;
|
|
23
|
+
}>;
|
|
24
|
+
state: SelectState;
|
|
25
|
+
button: HTMLButtonElement | undefined;
|
|
26
|
+
dropdown: HTMLDivElement | undefined;
|
|
27
|
+
select: HTMLSelectElement | undefined;
|
|
28
|
+
constructor();
|
|
29
|
+
connectedCallback(): void;
|
|
30
|
+
private observeResize;
|
|
31
|
+
private unobserveResize;
|
|
32
|
+
private isVisible;
|
|
33
|
+
getDropdownPosition: (element: HTMLElement) => {
|
|
34
|
+
isAbove: boolean;
|
|
35
|
+
customRect: {
|
|
36
|
+
top: number;
|
|
37
|
+
bottom: number;
|
|
38
|
+
left: number;
|
|
39
|
+
right: number;
|
|
40
|
+
width: number;
|
|
41
|
+
x: number;
|
|
42
|
+
y: number;
|
|
43
|
+
height: number;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
private closeDropdown;
|
|
47
|
+
private openDropdown;
|
|
48
|
+
createSelectBadge: (value: string, label: string) => HTMLSpanElement;
|
|
49
|
+
private measureBadgesWidth;
|
|
50
|
+
private handleBadgeOverflow;
|
|
51
|
+
updateLabel: () => void;
|
|
52
|
+
private deselectMultiOption;
|
|
53
|
+
recomputeOptions: () => void;
|
|
54
|
+
getInteractiveOptions: () => HTMLLIElement[];
|
|
55
|
+
private handleOptionSelect;
|
|
56
|
+
private handleContainerClick;
|
|
57
|
+
handleSelectKeyDown: (e: KeyboardEvent) => void;
|
|
58
|
+
}
|
|
59
|
+
export {};
|
|
@@ -1,14 +1,50 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
class SUISelectElement extends HTMLElement {
|
|
2
|
+
CONSTANTS = {
|
|
3
3
|
OPTION_HEIGHT: 36,
|
|
4
4
|
BORDER_SIZE: 2,
|
|
5
5
|
MARGIN: 4,
|
|
6
6
|
BADGE_PADDING: 80
|
|
7
7
|
};
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
observerMap = /* @__PURE__ */ new WeakMap();
|
|
9
|
+
state = {
|
|
10
|
+
options: [],
|
|
11
|
+
isMultiple: false,
|
|
12
|
+
focusIndex: -1,
|
|
13
|
+
placeholder: ""
|
|
14
|
+
};
|
|
15
|
+
button;
|
|
16
|
+
dropdown;
|
|
17
|
+
select;
|
|
18
|
+
constructor() {
|
|
19
|
+
super();
|
|
20
|
+
}
|
|
21
|
+
connectedCallback() {
|
|
22
|
+
this.button = this.querySelector(".sui-select-button");
|
|
23
|
+
this.dropdown = this.querySelector(".sui-select-dropdown");
|
|
24
|
+
this.select = this.querySelector("select");
|
|
25
|
+
document.addEventListener("click", ({ target }) => {
|
|
26
|
+
if (this.dropdown?.classList.contains("active") || !target) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (!this.contains(target) && this.isVisible(this)) {
|
|
30
|
+
this.closeDropdown();
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
this.state.placeholder = this.button?.querySelector(".sui-select-value-span")?.innerText ?? "";
|
|
34
|
+
this.state.options = JSON.parse(this.dataset.options);
|
|
35
|
+
this.state.isMultiple = this.dataset.multiple === "true";
|
|
36
|
+
this.addEventListener("click", (e) => this.handleContainerClick(e));
|
|
37
|
+
this.addEventListener("keydown", (e) => this.handleSelectKeyDown(e));
|
|
38
|
+
if (this.state.isMultiple) {
|
|
39
|
+
this.observeResize(this.button, () => {
|
|
40
|
+
this.handleBadgeOverflow();
|
|
41
|
+
});
|
|
42
|
+
this.handleBadgeOverflow();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
observeResize = (element, callback) => {
|
|
46
|
+
if (this.observerMap.has(element)) {
|
|
47
|
+
this.unobserveResize(element);
|
|
12
48
|
}
|
|
13
49
|
const observer = new ResizeObserver((entries) => {
|
|
14
50
|
for (const entry of entries) {
|
|
@@ -17,28 +53,30 @@ function loadSelects() {
|
|
|
17
53
|
}
|
|
18
54
|
});
|
|
19
55
|
observer.observe(element);
|
|
20
|
-
observerMap.set(element, { observer, callback });
|
|
21
|
-
return () => unobserveResize(element);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const data = observerMap.get(element);
|
|
56
|
+
this.observerMap.set(element, { observer, callback });
|
|
57
|
+
return () => this.unobserveResize(element);
|
|
58
|
+
};
|
|
59
|
+
unobserveResize = (element) => {
|
|
60
|
+
const data = this.observerMap.get(element);
|
|
25
61
|
if (data) {
|
|
26
62
|
data.observer.disconnect();
|
|
27
|
-
observerMap.delete(element);
|
|
63
|
+
this.observerMap.delete(element);
|
|
28
64
|
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const rect =
|
|
33
|
-
const
|
|
65
|
+
};
|
|
66
|
+
isVisible = (elem) => elem.offsetWidth > 0 || elem.offsetHeight > 0 || elem.getClientRects().length > 0;
|
|
67
|
+
getDropdownPosition = (element) => {
|
|
68
|
+
const rect = element.getBoundingClientRect();
|
|
69
|
+
const optionsCount = this.state.options?.length ?? 0;
|
|
70
|
+
const { OPTION_HEIGHT, BORDER_SIZE, MARGIN } = this.CONSTANTS;
|
|
71
|
+
const dropdownHeight = optionsCount * OPTION_HEIGHT + BORDER_SIZE + MARGIN;
|
|
34
72
|
const customRect = {
|
|
35
|
-
top: rect.bottom +
|
|
36
|
-
bottom: rect.bottom +
|
|
73
|
+
top: rect.bottom + MARGIN,
|
|
74
|
+
bottom: rect.bottom + MARGIN + dropdownHeight,
|
|
37
75
|
left: rect.left,
|
|
38
76
|
right: rect.right,
|
|
39
77
|
width: rect.width,
|
|
40
78
|
x: rect.x,
|
|
41
|
-
y: rect.y + rect.height + CONSTANTS.MARGIN,
|
|
79
|
+
y: rect.y + rect.height + this.CONSTANTS.MARGIN,
|
|
42
80
|
height: dropdownHeight
|
|
43
81
|
};
|
|
44
82
|
return {
|
|
@@ -46,35 +84,32 @@ function loadSelects() {
|
|
|
46
84
|
customRect
|
|
47
85
|
};
|
|
48
86
|
};
|
|
49
|
-
|
|
50
|
-
if (!
|
|
51
|
-
|
|
52
|
-
|
|
87
|
+
closeDropdown = () => {
|
|
88
|
+
if (!this?.button || !this?.dropdown) return;
|
|
89
|
+
this.dropdown.classList.remove("active", "above");
|
|
90
|
+
this.button.ariaExpanded = "false";
|
|
53
91
|
};
|
|
54
|
-
|
|
55
|
-
if (!
|
|
56
|
-
const { isAbove } = getDropdownPosition(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
);
|
|
60
|
-
container.button.ariaExpanded = "true";
|
|
61
|
-
container.dropdown.classList.add("active", ...isAbove ? [] : ["above"]);
|
|
92
|
+
openDropdown = () => {
|
|
93
|
+
if (!this.button || !this.dropdown) return;
|
|
94
|
+
const { isAbove } = this.getDropdownPosition(this.button);
|
|
95
|
+
this.button.ariaExpanded = "true";
|
|
96
|
+
this.dropdown.classList.add("active", ...isAbove ? [] : ["above"]);
|
|
62
97
|
};
|
|
63
|
-
|
|
98
|
+
createSelectBadge = (value, label) => {
|
|
64
99
|
const badge = document.createElement("span");
|
|
65
100
|
badge.classList.add("sui-badge", "primary", "sm", "outlined", "full", "sui-select-badge");
|
|
66
101
|
badge.setAttribute("data-value", value);
|
|
67
102
|
badge.innerHTML = `${label} <svg style='min-width: 8px' xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 24 24'><path fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 18L18 6M6 6l12 12'></path></svg>`;
|
|
68
103
|
return badge;
|
|
69
104
|
};
|
|
70
|
-
|
|
105
|
+
measureBadgesWidth = (activeSelects) => {
|
|
71
106
|
const tempContainer = document.createElement("div");
|
|
72
107
|
tempContainer.classList.add("sui-select-badge-container");
|
|
73
108
|
tempContainer.style.position = "absolute";
|
|
74
109
|
tempContainer.style.visibility = "hidden";
|
|
75
110
|
document.body.appendChild(tempContainer);
|
|
76
111
|
const badges = Array.from(activeSelects).map(
|
|
77
|
-
(select) => createSelectBadge(
|
|
112
|
+
(select) => this.createSelectBadge(
|
|
78
113
|
select.getAttribute("value") ?? "",
|
|
79
114
|
select.innerText.trim()
|
|
80
115
|
)
|
|
@@ -88,26 +123,24 @@ function loadSelects() {
|
|
|
88
123
|
}, 0);
|
|
89
124
|
return { totalWidth, badges, tempContainer };
|
|
90
125
|
};
|
|
91
|
-
|
|
92
|
-
const buttonContainer =
|
|
93
|
-
const buttonValueSpan =
|
|
94
|
-
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
const overflowContainer = container?.querySelector(".sui-select-badge-container-below");
|
|
98
|
-
if (!buttonContainer || !overflowContainer || !container?.button || !activeSelects) return;
|
|
126
|
+
handleBadgeOverflow = () => {
|
|
127
|
+
const buttonContainer = this.button?.parentElement?.parentElement;
|
|
128
|
+
const buttonValueSpan = this.button?.querySelector(".sui-select-value-span");
|
|
129
|
+
const activeSelects = this.dropdown?.querySelectorAll(".sui-select-option.selected");
|
|
130
|
+
const overflowContainer = this.querySelector(".sui-select-badge-container-below");
|
|
131
|
+
if (!buttonContainer || !overflowContainer || !this.button || !activeSelects) return;
|
|
99
132
|
const parentContainer = buttonContainer.parentElement;
|
|
100
133
|
if (!parentContainer) return;
|
|
101
134
|
overflowContainer.innerHTML = "";
|
|
102
135
|
buttonValueSpan.innerHTML = "";
|
|
103
136
|
if (activeSelects.length === 0) {
|
|
104
|
-
buttonValueSpan.innerText =
|
|
137
|
+
buttonValueSpan.innerText = this.state.placeholder;
|
|
105
138
|
return;
|
|
106
139
|
}
|
|
107
|
-
const { totalWidth, badges, tempContainer } = measureBadgesWidth(activeSelects);
|
|
140
|
+
const { totalWidth, badges, tempContainer } = this.measureBadgesWidth(activeSelects);
|
|
108
141
|
const parentStyles = window.getComputedStyle(parentContainer);
|
|
109
142
|
const availableWidth = parentContainer.clientWidth - (Number.parseFloat(parentStyles.paddingLeft) || 0) - (Number.parseFloat(parentStyles.paddingRight) || 0);
|
|
110
|
-
const effectiveAvailableWidth = availableWidth - CONSTANTS.BADGE_PADDING;
|
|
143
|
+
const effectiveAvailableWidth = availableWidth - this.CONSTANTS.BADGE_PADDING;
|
|
111
144
|
document.body.removeChild(tempContainer);
|
|
112
145
|
const finalBadgeContainer = document.createElement("div");
|
|
113
146
|
finalBadgeContainer.classList.add("sui-select-badge-container");
|
|
@@ -121,13 +154,12 @@ function loadSelects() {
|
|
|
121
154
|
buttonValueSpan.appendChild(finalBadgeContainer);
|
|
122
155
|
}
|
|
123
156
|
};
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
handleBadgeOverflow(state2, container);
|
|
157
|
+
updateLabel = () => {
|
|
158
|
+
if (this.state.isMultiple) {
|
|
159
|
+
this.handleBadgeOverflow();
|
|
128
160
|
} else {
|
|
129
|
-
const selected =
|
|
130
|
-
const selectedButtonSpan =
|
|
161
|
+
const selected = this.querySelector(".sui-select-option.selected");
|
|
162
|
+
const selectedButtonSpan = this.button?.querySelector(
|
|
131
163
|
".sui-select-value-span"
|
|
132
164
|
);
|
|
133
165
|
if (selected && selectedButtonSpan) {
|
|
@@ -135,19 +167,19 @@ function loadSelects() {
|
|
|
135
167
|
}
|
|
136
168
|
}
|
|
137
169
|
};
|
|
138
|
-
|
|
139
|
-
const selectOpt =
|
|
170
|
+
deselectMultiOption = (id) => {
|
|
171
|
+
const selectOpt = this.dropdown?.querySelector(
|
|
140
172
|
`.sui-select-option[value='${id}']`
|
|
141
173
|
);
|
|
142
|
-
const max = Number.parseInt(
|
|
143
|
-
const selectedCount =
|
|
144
|
-
const selectedCountEl =
|
|
174
|
+
const max = Number.parseInt(this.dataset.multipleMax, 10);
|
|
175
|
+
const selectedCount = this.querySelectorAll(".sui-select-option.selected").length ?? 0;
|
|
176
|
+
const selectedCountEl = this.querySelector(
|
|
145
177
|
".sui-select-max-span .sui-select-select-count"
|
|
146
178
|
);
|
|
147
179
|
const isSelected = selectOpt?.classList.contains("selected");
|
|
148
180
|
if (selectOpt && (isSelected || Number.isNaN(max) || selectedCount < max)) {
|
|
149
181
|
selectOpt.classList.toggle("selected");
|
|
150
|
-
const selectOptEl =
|
|
182
|
+
const selectOptEl = this.select?.querySelector(
|
|
151
183
|
`option[value='${selectOpt.getAttribute("value")}']`
|
|
152
184
|
);
|
|
153
185
|
if (selectOptEl) {
|
|
@@ -156,99 +188,97 @@ function loadSelects() {
|
|
|
156
188
|
if (selectedCountEl) {
|
|
157
189
|
selectedCountEl.innerText = String(selectedCount + (isSelected ? -1 : 1));
|
|
158
190
|
}
|
|
159
|
-
updateLabel(
|
|
191
|
+
this.updateLabel();
|
|
160
192
|
}
|
|
161
193
|
};
|
|
162
|
-
|
|
163
|
-
const optionElements =
|
|
194
|
+
recomputeOptions = () => {
|
|
195
|
+
const optionElements = this.dropdown?.querySelectorAll(
|
|
164
196
|
".sui-select-option"
|
|
165
197
|
);
|
|
166
198
|
for (const entry of optionElements) {
|
|
167
|
-
if (Number.parseInt(entry.dataset.optionIndex, 10) ===
|
|
199
|
+
if (Number.parseInt(entry.dataset.optionIndex, 10) === this.state.focusIndex) {
|
|
168
200
|
entry.classList.add("focused");
|
|
169
201
|
} else {
|
|
170
202
|
entry.classList.remove("focused");
|
|
171
203
|
}
|
|
172
204
|
}
|
|
173
205
|
};
|
|
174
|
-
|
|
175
|
-
const allOptions =
|
|
206
|
+
getInteractiveOptions = () => {
|
|
207
|
+
const allOptions = this.dropdown?.querySelectorAll(
|
|
176
208
|
".sui-select-option"
|
|
177
209
|
);
|
|
178
210
|
return Array.from(allOptions).filter(
|
|
179
211
|
(option) => !option.classList.contains("hidden") && !option.classList.contains("disabled") && !option.hasAttribute("disabled")
|
|
180
212
|
);
|
|
181
213
|
};
|
|
182
|
-
|
|
214
|
+
handleOptionSelect = (target) => {
|
|
183
215
|
const option = target.closest(".sui-select-option");
|
|
184
|
-
const lastActive =
|
|
185
|
-
const isMultiple =
|
|
216
|
+
const lastActive = this.dropdown?.querySelector(".sui-select-option.selected");
|
|
217
|
+
const isMultiple = this.state.isMultiple;
|
|
186
218
|
if (isMultiple) {
|
|
187
|
-
deselectMultiOption(
|
|
219
|
+
this.deselectMultiOption(option?.getAttribute("value"));
|
|
188
220
|
} else {
|
|
189
221
|
if (lastActive) {
|
|
190
222
|
lastActive.classList.remove("selected");
|
|
191
223
|
}
|
|
192
224
|
if (option) {
|
|
193
225
|
option.classList.add("selected");
|
|
194
|
-
if (
|
|
195
|
-
|
|
226
|
+
if (this.select) {
|
|
227
|
+
this.select.value = option.getAttribute("value");
|
|
196
228
|
}
|
|
197
|
-
updateLabel(
|
|
229
|
+
this.updateLabel();
|
|
198
230
|
}
|
|
199
|
-
closeDropdown(
|
|
231
|
+
this.closeDropdown();
|
|
200
232
|
}
|
|
201
233
|
};
|
|
202
|
-
|
|
234
|
+
handleContainerClick = (e) => {
|
|
203
235
|
const target = e.target;
|
|
204
236
|
if (target.closest(".sui-select-badge svg")) {
|
|
205
|
-
deselectMultiOption(
|
|
206
|
-
|
|
207
|
-
target.closest(".sui-select-badge")?.getAttribute("data-value"),
|
|
208
|
-
container
|
|
237
|
+
this.deselectMultiOption(
|
|
238
|
+
target.closest(".sui-select-badge")?.getAttribute("data-value")
|
|
209
239
|
);
|
|
210
|
-
handleBadgeOverflow(
|
|
240
|
+
this.handleBadgeOverflow();
|
|
211
241
|
}
|
|
212
242
|
if (target.closest(".sui-select-button")) {
|
|
213
|
-
const
|
|
214
|
-
if (
|
|
215
|
-
closeDropdown(
|
|
243
|
+
const container = target.closest(".sui-select-label");
|
|
244
|
+
if (container.dropdown?.classList.contains("active")) {
|
|
245
|
+
this.closeDropdown();
|
|
216
246
|
} else {
|
|
217
|
-
openDropdown(
|
|
247
|
+
this.openDropdown();
|
|
218
248
|
}
|
|
219
249
|
}
|
|
220
250
|
if (target.closest(".sui-select-dropdown.active")) {
|
|
221
|
-
handleOptionSelect(target
|
|
251
|
+
this.handleOptionSelect(target);
|
|
222
252
|
}
|
|
223
253
|
};
|
|
224
|
-
|
|
225
|
-
const active = !!
|
|
254
|
+
handleSelectKeyDown = (e) => {
|
|
255
|
+
const active = !!this.dropdown?.classList.contains("active");
|
|
226
256
|
const focusedElement = document.activeElement;
|
|
227
257
|
if (e.key === "Tab" || e.key === "Escape") {
|
|
228
|
-
closeDropdown(
|
|
258
|
+
this.closeDropdown();
|
|
229
259
|
return;
|
|
230
260
|
}
|
|
231
261
|
if ((e.key === "Enter" || e.key === " ") && focusedElement?.tagName.toLowerCase() === "svg") {
|
|
232
262
|
const badgeElement = focusedElement?.closest(".sui-select-badge");
|
|
233
|
-
if (badgeElement &&
|
|
263
|
+
if (badgeElement && this.state.isMultiple) {
|
|
234
264
|
const badgeValue = badgeElement.getAttribute("data-value");
|
|
235
265
|
let nextBadge = badgeElement.previousElementSibling;
|
|
236
266
|
if (!nextBadge) {
|
|
237
267
|
nextBadge = badgeElement.nextElementSibling;
|
|
238
268
|
}
|
|
239
269
|
const nextBadgeValue = nextBadge?.getAttribute("data-value");
|
|
240
|
-
deselectMultiOption(
|
|
241
|
-
handleBadgeOverflow(
|
|
270
|
+
this.deselectMultiOption(badgeValue);
|
|
271
|
+
this.handleBadgeOverflow();
|
|
242
272
|
setTimeout(() => {
|
|
243
273
|
if (nextBadgeValue) {
|
|
244
|
-
const badgeToFocus =
|
|
274
|
+
const badgeToFocus = this.querySelector(
|
|
245
275
|
`.sui-select-badge[data-value="${nextBadgeValue}"] svg`
|
|
246
276
|
);
|
|
247
277
|
if (badgeToFocus) {
|
|
248
278
|
badgeToFocus.focus();
|
|
249
279
|
}
|
|
250
280
|
} else {
|
|
251
|
-
|
|
281
|
+
this.button?.focus();
|
|
252
282
|
}
|
|
253
283
|
}, 0);
|
|
254
284
|
e.preventDefault();
|
|
@@ -257,13 +287,13 @@ function loadSelects() {
|
|
|
257
287
|
}
|
|
258
288
|
}
|
|
259
289
|
if ((e.key === " " || e.key === "Enter") && !active) {
|
|
260
|
-
openDropdown(
|
|
290
|
+
this.openDropdown();
|
|
261
291
|
e.preventDefault();
|
|
262
292
|
e.stopImmediatePropagation();
|
|
263
293
|
return;
|
|
264
294
|
}
|
|
265
295
|
if (e.key === "Enter" && active) {
|
|
266
|
-
const currentlyFocused =
|
|
296
|
+
const currentlyFocused = this.querySelector(".sui-select-option.focused");
|
|
267
297
|
if (currentlyFocused) {
|
|
268
298
|
currentlyFocused.click();
|
|
269
299
|
e.preventDefault();
|
|
@@ -273,77 +303,37 @@ function loadSelects() {
|
|
|
273
303
|
}
|
|
274
304
|
e.preventDefault();
|
|
275
305
|
e.stopImmediatePropagation();
|
|
276
|
-
const interactiveOptions = getInteractiveOptions(
|
|
306
|
+
const interactiveOptions = this.getInteractiveOptions();
|
|
277
307
|
const currentInteractiveIndex = interactiveOptions.findIndex(
|
|
278
308
|
(option) => option.classList.contains("focused")
|
|
279
309
|
);
|
|
280
310
|
if (e.key === "ArrowUp" && currentInteractiveIndex > 0) {
|
|
281
|
-
|
|
282
|
-
|
|
311
|
+
this.state.focusIndex = Array.from(
|
|
312
|
+
this.dropdown?.querySelectorAll(".sui-select-option") || []
|
|
283
313
|
).indexOf(interactiveOptions[currentInteractiveIndex - 1]);
|
|
284
|
-
recomputeOptions(
|
|
314
|
+
this.recomputeOptions();
|
|
285
315
|
}
|
|
286
316
|
if (e.key === "ArrowDown" && currentInteractiveIndex < interactiveOptions.length - 1) {
|
|
287
|
-
|
|
288
|
-
|
|
317
|
+
this.state.focusIndex = Array.from(
|
|
318
|
+
this.dropdown?.querySelectorAll(".sui-select-option") || []
|
|
289
319
|
).indexOf(interactiveOptions[currentInteractiveIndex + 1]);
|
|
290
|
-
recomputeOptions(
|
|
320
|
+
this.recomputeOptions();
|
|
291
321
|
}
|
|
292
322
|
if (e.key === "PageUp") {
|
|
293
|
-
|
|
294
|
-
|
|
323
|
+
this.state.focusIndex = Array.from(
|
|
324
|
+
this.dropdown?.querySelectorAll(".sui-select-option") || []
|
|
295
325
|
).indexOf(interactiveOptions[0]);
|
|
296
|
-
recomputeOptions(
|
|
326
|
+
this.recomputeOptions();
|
|
297
327
|
}
|
|
298
328
|
if (e.key === "PageDown") {
|
|
299
|
-
|
|
300
|
-
|
|
329
|
+
this.state.focusIndex = Array.from(
|
|
330
|
+
this.dropdown?.querySelectorAll(".sui-select-option") || []
|
|
301
331
|
).indexOf(interactiveOptions[interactiveOptions.length - 1]);
|
|
302
|
-
recomputeOptions(
|
|
332
|
+
this.recomputeOptions();
|
|
303
333
|
}
|
|
304
334
|
};
|
|
305
|
-
const state = {
|
|
306
|
-
optionsMap: {},
|
|
307
|
-
isMultipleMap: {},
|
|
308
|
-
focusIndex: -1,
|
|
309
|
-
placeholder: ""
|
|
310
|
-
};
|
|
311
|
-
const selects = document.querySelectorAll(".sui-select-label");
|
|
312
|
-
document.addEventListener("click", ({ target }) => {
|
|
313
|
-
for (const container of selects) {
|
|
314
|
-
if (!container.dropdown?.classList.contains("active") || !target)
|
|
315
|
-
continue;
|
|
316
|
-
if (!container.contains(target) && isVisible(container)) {
|
|
317
|
-
closeDropdown(container);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
});
|
|
321
|
-
for (const container of selects) {
|
|
322
|
-
if (container.dataset.initialized === "true") continue;
|
|
323
|
-
const id = container.dataset.id;
|
|
324
|
-
const specialContainer = Object.assign(container, {
|
|
325
|
-
button: container.querySelector("button"),
|
|
326
|
-
dropdown: container.querySelector(".sui-select-dropdown"),
|
|
327
|
-
select: container.querySelector("select")
|
|
328
|
-
});
|
|
329
|
-
state.placeholder = specialContainer.button?.querySelector(".sui-select-value-span")?.innerText ?? "";
|
|
330
|
-
state.optionsMap[id] = JSON.parse(container.dataset.options);
|
|
331
|
-
state.isMultipleMap[id] = container.dataset.multiple === "true";
|
|
332
|
-
specialContainer.addEventListener(
|
|
333
|
-
"click",
|
|
334
|
-
(e) => handleContainerClick(e, state, specialContainer)
|
|
335
|
-
);
|
|
336
|
-
specialContainer.addEventListener(
|
|
337
|
-
"keydown",
|
|
338
|
-
(e) => handleSelectKeyDown(e, state, specialContainer)
|
|
339
|
-
);
|
|
340
|
-
if (state.isMultipleMap[id]) {
|
|
341
|
-
observeResize(specialContainer.button, () => {
|
|
342
|
-
handleBadgeOverflow(state, specialContainer);
|
|
343
|
-
});
|
|
344
|
-
handleBadgeOverflow(state, specialContainer);
|
|
345
|
-
}
|
|
346
|
-
container.dataset.initialized = "true";
|
|
347
|
-
}
|
|
348
335
|
}
|
|
349
|
-
|
|
336
|
+
customElements.define("sui-select", SUISelectElement);
|
|
337
|
+
export {
|
|
338
|
+
SUISelectElement
|
|
339
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -133,6 +133,9 @@ function integration(options = {}) {
|
|
|
133
133
|
"studiocms:ui/scripts/tooltip": `import '${resolve("./components/Tooltip/tooltip.js")}';`,
|
|
134
134
|
"studiocms:ui/scripts/accordion": `import '${resolve("./components/Accordion/accordion.js")}';`,
|
|
135
135
|
"studiocms:ui/scripts/progress": `import '${resolve("./components/Progress/progress.js")}';`,
|
|
136
|
+
"studiocms:ui/components/select/script": `
|
|
137
|
+
export { SUISelectElement } from '${resolve("./components/Select/select.js")}';
|
|
138
|
+
`,
|
|
136
139
|
// Components
|
|
137
140
|
...virtualComponents,
|
|
138
141
|
"studiocms:ui/utils": `
|
package/dist/utils/headers.d.ts
CHANGED
|
@@ -80,7 +80,7 @@ export declare function sortHead(head: HeadConfig): {
|
|
|
80
80
|
content: string;
|
|
81
81
|
}[];
|
|
82
82
|
/** Get the relative importance of a specific head tag. */
|
|
83
|
-
export declare function getImportance(entry: HeadConfig[number]): 0 |
|
|
83
|
+
export declare function getImportance(entry: HeadConfig[number]): 0 | 80 | 100 | 90 | 70;
|
|
84
84
|
/** Create a fully parsed, merged, and sorted head entry array from multiple sources. */
|
|
85
85
|
export declare function createHead(defaultHeaders: HeadUserConfig, ...heads: HeadConfig[]): {
|
|
86
86
|
tag: "link" | "base" | "meta" | "noscript" | "script" | "style" | "template" | "title";
|
package/dist/virtuals.d.ts
CHANGED
|
@@ -116,6 +116,16 @@ declare module 'studiocms:ui/components/select' {
|
|
|
116
116
|
export const SearchSelect: typeof import('./components/SearchSelect/SearchSelect.astro').default;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
declare module 'studiocms:ui/components/select/script' {
|
|
120
|
+
export type SelectOption = {
|
|
121
|
+
value: string;
|
|
122
|
+
label: string;
|
|
123
|
+
disabled?: boolean;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const SUISelectElement: typeof import('./components/Select/select.js').SUISelectElement;
|
|
127
|
+
}
|
|
128
|
+
|
|
119
129
|
declare module 'studiocms:ui/components/dropdown' {
|
|
120
130
|
export const Dropdown: typeof import('./components/Dropdown/Dropdown.astro').default;
|
|
121
131
|
}
|