@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.
@@ -45,6 +45,7 @@
45
45
  position: absolute;
46
46
  top: 50%;
47
47
  transform: translateY(-50%);
48
+ color: var(--text-muted);
48
49
  }
49
50
  .input-icon.icon-left {
50
51
  left: 0.5rem;
@@ -8,6 +8,7 @@
8
8
  margin: auto;
9
9
  z-index: 50;
10
10
  max-width: calc(100% - 4rem);
11
+ background-color: var(--background-base);
11
12
  }
12
13
  .sui-modal.sm {
13
14
  width: 384px;
@@ -132,7 +132,7 @@ const defaultLabel = selected
132
132
  : placeholder;
133
133
  ---
134
134
 
135
- <div
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
- </div>
225
+ </sui-combobox>
226
226
  <script>
227
227
  import "studiocms:ui/scripts/searchselect"
228
228
  </script>
@@ -1,19 +1 @@
1
- type SearchSelectOption = {
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
- function loadSearchSelects() {
2
- const CONSTANTS = {
3
- OPTION_HEIGHT: 36,
4
- BORDER_SIZE: 2,
5
- MARGIN: 4,
6
- BADGE_PADDING: 80
7
- };
8
- const getDropdownPosition = (input, optionsCount) => {
9
- const rect = input.getBoundingClientRect();
10
- const dropdownHeight = optionsCount * CONSTANTS.OPTION_HEIGHT + CONSTANTS.BORDER_SIZE + CONSTANTS.MARGIN;
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
- const createSelectBadge = (value, label) => {
27
- const badge = document.createElement("span");
28
- badge.classList.add(
29
- "sui-badge",
30
- "primary",
31
- "sm",
32
- "outlined",
33
- "full",
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
- badge.setAttribute("data-value", value);
37
- badge.innerHTML = `${label} <svg style='min-width: 8px' xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 24 24' role="button" tabindex="0"><path fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 18L18 6M6 6l12 12'></path></svg>`;
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
- const recalculateBadges = (state2, container) => {
41
- const badgeContainer = container.querySelector(".sui-search-select-badge-container");
42
- if (!badgeContainer || !container.input) return;
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 = state2.selectedOptionsMap[container.dataset.id] || [];
45
- const allOptions = state2.optionsMap[container.dataset.id] || [];
44
+ const selectedValues = this.state.selectedOptions;
45
+ const allOptions = this.state.options;
46
46
  if (selectedValues.length === 0) {
47
- container.input.placeholder = state2.placeholderMap[container.dataset.id] ?? "";
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 = createSelectBadge(value, option.label);
57
+ const newBadge = this.createSearchSelectBadge(value, option.label);
58
58
  badgeContainer.appendChild(newBadge);
59
59
  }
60
60
  }
61
61
  };
62
- const updateLabel = (isMultiple, state2, container) => {
63
- const selectedInput = container?.input;
64
- if (isMultiple) {
65
- recalculateBadges(state2, container);
62
+ updateLabel = () => {
63
+ const selectedInput = this.input;
64
+ if (this.state.isMultiple) {
65
+ this.recalculateBadges();
66
66
  if (selectedInput) {
67
- selectedInput.placeholder = state2.placeholderMap[container.dataset.id] ?? "";
67
+ selectedInput.placeholder = this.state.placeholder;
68
68
  }
69
69
  } else {
70
- const selected = container.querySelector(
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
- const updateOptionSelection = (value, container, state2, forceState) => {
79
- const currentSelected = state2.selectedOptionsMap[container.dataset.id] || [];
76
+ updateOptionSelection = (value, forceState) => {
77
+ const currentSelected = this.state.selectedOptions;
80
78
  const isCurrentlySelected = currentSelected.includes(value);
81
- const max = Number.parseInt(container.dataset.multipleMax, 10);
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
- state2.selectedOptionsMap[container.dataset.id] = newSelected;
87
- const option = container.dropdown?.querySelector(
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 (container?.select) {
93
- container.select.value = option.getAttribute("value");
90
+ if (this.select) {
91
+ this.select.value = option.getAttribute("value");
94
92
  }
95
93
  }
96
- const selectedCountEl = container.querySelector(
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
- const toggleMultiOption = (id, container, state2) => {
105
- const success = updateOptionSelection(id, container, state2);
102
+ toggleMultiOption = (id) => {
103
+ const success = this.updateOptionSelection(id);
106
104
  if (success) {
107
- recalculateBadges(state2, container);
105
+ this.recalculateBadges();
108
106
  }
109
107
  };
110
- const recomputeOptions = (state2, container) => {
111
- const optionElements = container?.dropdown?.querySelectorAll(
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) === state2.focusIndex) {
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
- const reconstructOptions = (filteredOptions, state2, container) => {
123
- container.dropdown.innerHTML = "";
124
- const selectedValues = state2.selectedOptionsMap[container.dataset.id] || [];
120
+ reconstructOptions = (filteredOptions) => {
121
+ this.dropdown.innerHTML = "";
122
+ const selectedValues = this.state.selectedOptions;
125
123
  if (filteredOptions.length === 0) {
126
- container.dropdown.innerHTML = '<li class="empty-search-results">No results found</li>';
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
- container.dropdown?.appendChild(element);
141
+ this.dropdown?.appendChild(element);
144
142
  i++;
145
143
  }
146
144
  };
147
- const getInteractiveOptions = (container) => {
148
- const allOptions = container?.dropdown?.querySelectorAll(
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
- const handleContainerMouseDown = (e, state2, container) => {
153
+ handleContainerMouseDown = (e) => {
156
154
  const target = e.target;
157
155
  if (!target.closest("input")) {
158
156
  e.preventDefault();
159
157
  }
160
- if (container.input?.value.length === 0) {
161
- reconstructOptions(state2.optionsMap[container.dataset.id] ?? [], state2, container);
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 (container.dropdown?.parentElement?.classList.contains("active")) {
165
- container.dropdown?.parentElement?.classList.remove("active", "above");
166
- container.input?.blur();
167
- container.input.value = "";
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
- container.dropdown?.parentElement?.classList.add("active");
170
- container.input?.focus();
171
- container.input.value = "";
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
- container.dropdown?.parentElement?.classList.remove("active", "above");
177
- container.input?.blur();
178
- container.input.value = "";
174
+ this.dropdown?.parentElement?.classList.remove("active", "above");
175
+ this.input?.blur();
176
+ this.input.value = "";
179
177
  }
180
- state2.isSelectingOption = true;
178
+ this.state.isSelectingOption = true;
181
179
  setTimeout(() => {
182
- state2.isSelectingOption = false;
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, container, state2);
184
+ const success = this.updateOptionSelection(value);
187
185
  if (success) {
188
- recalculateBadges(state2, container);
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
- container.input?.focus();
193
+ this.input?.focus();
196
194
  return;
197
195
  }
198
- const isMultiple = state2.isMultipleMap[container.dataset.id];
196
+ const isMultiple = this.state.isMultiple;
199
197
  if (isMultiple) {
200
- const success = updateOptionSelection(opt.dataset.value, container, state2);
198
+ const success = this.updateOptionSelection(opt.dataset.value);
201
199
  if (success) {
202
- updateLabel(true, state2, container);
203
- recalculateBadges(state2, container);
200
+ this.updateLabel();
201
+ this.recalculateBadges();
204
202
  }
205
203
  } else {
206
- const currentSelected = state2.selectedOptionsMap[container.dataset.id] || [];
204
+ const currentSelected = this.state.selectedOptions;
207
205
  for (const value of currentSelected) {
208
- updateOptionSelection(value, container, state2, false);
206
+ this.updateOptionSelection(value, false);
209
207
  }
210
- updateOptionSelection(opt.dataset.value, container, state2, true);
211
- updateLabel(false, state2, container);
212
- container.dropdown?.parentElement?.classList.remove("active", "above");
213
- container.input?.blur();
214
- container.input.value = "";
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
- const handleSelectKeyDown = (e, state2, container) => {
215
+ handleSelectKeyDown = (e) => {
218
216
  const focusedElement = document.activeElement;
219
217
  if (e.key === "Escape" || e.key === "Tab") {
220
- container.input?.blur();
221
- container.dropdown?.parentElement?.classList.remove("active", "above");
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 && state2.isMultipleMap[container?.dataset.id]) {
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, container, state2);
234
- recalculateBadges(state2, container);
231
+ this.toggleMultiOption(badgeValue);
232
+ this.recalculateBadges();
235
233
  setTimeout(() => {
236
234
  if (nextBadgeValue) {
237
- const badgeToFocus = container?.querySelector(
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(container);
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
- state2.focusIndex = Array.from(
256
- container?.dropdown?.querySelectorAll(".sui-search-select-option") || []
253
+ this.state.focusIndex = Array.from(
254
+ this.dropdown?.querySelectorAll(".sui-search-select-option") || []
257
255
  ).indexOf(interactiveOptions[currentInteractiveIndex - 1]);
258
- recomputeOptions(state2, container);
256
+ this.recomputeOptions();
259
257
  return;
260
258
  }
261
259
  if (e.key === "ArrowDown" && currentInteractiveIndex < interactiveOptions.length - 1) {
262
- state2.focusIndex = Array.from(
263
- container?.dropdown?.querySelectorAll(".sui-search-select-option") || []
260
+ this.state.focusIndex = Array.from(
261
+ this.dropdown?.querySelectorAll(".sui-search-select-option") || []
264
262
  ).indexOf(interactiveOptions[currentInteractiveIndex + 1]);
265
- recomputeOptions(state2, container);
263
+ this.recomputeOptions();
266
264
  return;
267
265
  }
268
266
  if (e.key === "PageUp") {
269
- state2.focusIndex = Array.from(
270
- container?.dropdown?.querySelectorAll(".sui-search-select-option") || []
267
+ this.state.focusIndex = Array.from(
268
+ this.dropdown?.querySelectorAll(".sui-search-select-option") || []
271
269
  ).indexOf(interactiveOptions[0]);
272
- recomputeOptions(state2, container);
270
+ this.recomputeOptions();
273
271
  return;
274
272
  }
275
273
  if (e.key === "PageDown") {
276
- state2.focusIndex = Array.from(
277
- container?.dropdown?.querySelectorAll(".sui-search-select-option") || []
274
+ this.state.focusIndex = Array.from(
275
+ this.dropdown?.querySelectorAll(".sui-search-select-option") || []
278
276
  ).indexOf(interactiveOptions[interactiveOptions.length - 1]);
279
- recomputeOptions(state2, container);
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 = container?.dropdown?.querySelectorAll(
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) === state2.focusIndex
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 = state2.isMultipleMap[container.dataset.id];
292
+ const isMultiple = this.state.isMultiple;
295
293
  if (isMultiple) {
296
- const success = updateOptionSelection(value, container, state2);
294
+ const success = this.updateOptionSelection(value);
297
295
  if (success) {
298
- updateLabel(true, state2, container);
299
- recalculateBadges(state2, container);
296
+ this.updateLabel();
297
+ this.recalculateBadges();
300
298
  }
301
299
  } else {
302
- const currentSelected = state2.selectedOptionsMap[container.dataset.id] || [];
300
+ const currentSelected = this.state.selectedOptions;
303
301
  for (const existingValue of currentSelected) {
304
- updateOptionSelection(existingValue, container, state2, false);
302
+ this.updateOptionSelection(existingValue, false);
305
303
  }
306
- updateOptionSelection(value, container, state2, true);
307
- updateLabel(false, state2, container);
308
- container.dropdown?.parentElement?.classList.remove("active", "above");
309
- container.input.value = "";
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
- const handleInputKeyup = (e, state2, container) => {
313
+ handleInputKeyup = (e) => {
316
314
  if (["Enter", "ArrowUp", "ArrowDown"].includes(e.key)) return;
317
- const value = container.input.value.trim().toLowerCase();
318
- const allOptions = state2.optionsMap[container.dataset.id];
315
+ const value = this.input.value.trim().toLowerCase();
316
+ const allOptions = this.state.options;
319
317
  if (value.length === 0) {
320
- reconstructOptions(allOptions, state2, container);
318
+ this.reconstructOptions(allOptions);
321
319
  return;
322
320
  }
323
- const filteredOptions = allOptions?.filter((option) => option.label.toLowerCase().includes(value)) ?? [];
324
- state2.focusIndex = 0;
325
- reconstructOptions(filteredOptions, state2, container);
321
+ const filteredOptions = allOptions.filter((option) => option.label.toLowerCase().includes(value)) ?? [];
322
+ this.state.focusIndex = 0;
323
+ this.reconstructOptions(filteredOptions);
326
324
  };
327
- const handleContainerFocusOut = (state2, container) => {
328
- if (state2.isSelectingOption) return;
329
- container.input.value = "";
330
- reconstructOptions(state2.optionsMap[container.dataset.id] ?? [], state2, container);
331
- container.dropdown?.parentElement?.classList.remove("active", "above");
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
- const handleContainerFocusIn = (state2, container) => {
331
+ handleContainerFocusIn = () => {
334
332
  const allDropdowns = document.querySelectorAll(".sui-search-select-dropdown-list");
335
333
  for (const dropdown of allDropdowns) {
336
- if (dropdown !== container.dropdown) {
334
+ if (dropdown !== this.dropdown) {
337
335
  dropdown.parentElement?.classList.remove("active", "above");
338
336
  }
339
337
  }
340
- const { isAbove } = getDropdownPosition(
341
- container.input,
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
- document.addEventListener("astro:page-load", loadSearchSelects);
342
+ customElements.define("sui-combobox", SUIComboboxElement);
@@ -131,14 +131,13 @@ const defaultLabel = selected
131
131
  : placeholder;
132
132
  ---
133
133
 
134
- <div
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
- </div>
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
- type SelectContainer = HTMLDivElement & {
7
- button: HTMLButtonElement | null;
8
- dropdown: HTMLUListElement | null;
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 function loadSelects(): void;
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
- function loadSelects() {
2
- const CONSTANTS = {
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
- const observerMap = /* @__PURE__ */ new WeakMap();
9
- function observeResize(element, callback) {
10
- if (observerMap.has(element)) {
11
- unobserveResize(element);
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
- function unobserveResize(element) {
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
- const isVisible = (elem) => elem.offsetWidth > 0 || elem.offsetHeight > 0 || elem.getClientRects().length > 0;
31
- const getDropdownPosition = (button, optionsCount) => {
32
- const rect = button.getBoundingClientRect();
33
- const dropdownHeight = optionsCount * CONSTANTS.OPTION_HEIGHT + CONSTANTS.BORDER_SIZE + CONSTANTS.MARGIN;
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 + CONSTANTS.MARGIN,
36
- bottom: rect.bottom + CONSTANTS.MARGIN + dropdownHeight,
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
- const closeDropdown = (container) => {
50
- if (!container?.button || !container?.dropdown) return;
51
- container.dropdown.classList.remove("active", "above");
52
- container.button.ariaExpanded = "false";
87
+ closeDropdown = () => {
88
+ if (!this?.button || !this?.dropdown) return;
89
+ this.dropdown.classList.remove("active", "above");
90
+ this.button.ariaExpanded = "false";
53
91
  };
54
- const openDropdown = (state2, container) => {
55
- if (!container?.button || !container?.dropdown) return;
56
- const { isAbove } = getDropdownPosition(
57
- container.button,
58
- state2.optionsMap[container.dataset.id]?.length ?? 0
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
- const createSelectBadge = (value, label) => {
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
- const measureBadgesWidth = (activeSelects) => {
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
- const handleBadgeOverflow = (state2, container) => {
92
- const buttonContainer = container?.button?.parentElement?.parentElement;
93
- const buttonValueSpan = container?.button?.querySelector(
94
- ".sui-select-value-span"
95
- );
96
- const activeSelects = container?.dropdown?.querySelectorAll(".sui-select-option.selected");
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 = state2.placeholder;
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
- const updateLabel = (state2, container) => {
125
- const isMultiple = state2.isMultipleMap[container?.dataset.id];
126
- if (isMultiple) {
127
- handleBadgeOverflow(state2, container);
157
+ updateLabel = () => {
158
+ if (this.state.isMultiple) {
159
+ this.handleBadgeOverflow();
128
160
  } else {
129
- const selected = container?.querySelector(".sui-select-option.selected");
130
- const selectedButtonSpan = container?.button?.querySelector(
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
- const deselectMultiOption = (state2, id, container) => {
139
- const selectOpt = container?.dropdown?.querySelector(
170
+ deselectMultiOption = (id) => {
171
+ const selectOpt = this.dropdown?.querySelector(
140
172
  `.sui-select-option[value='${id}']`
141
173
  );
142
- const max = Number.parseInt(container?.dataset.multipleMax, 10);
143
- const selectedCount = container?.querySelectorAll(".sui-select-option.selected").length ?? 0;
144
- const selectedCountEl = container?.querySelector(
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 = container?.select?.querySelector(
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(state2, container);
191
+ this.updateLabel();
160
192
  }
161
193
  };
162
- const recomputeOptions = (state2, container) => {
163
- const optionElements = container?.dropdown?.querySelectorAll(
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) === state2.focusIndex) {
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
- const getInteractiveOptions = (container) => {
175
- const allOptions = container?.dropdown?.querySelectorAll(
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
- const handleOptionSelect = (target, state2, container) => {
214
+ handleOptionSelect = (target) => {
183
215
  const option = target.closest(".sui-select-option");
184
- const lastActive = container?.dropdown?.querySelector(".sui-select-option.selected");
185
- const isMultiple = state2.isMultipleMap[container.dataset.id];
216
+ const lastActive = this.dropdown?.querySelector(".sui-select-option.selected");
217
+ const isMultiple = this.state.isMultiple;
186
218
  if (isMultiple) {
187
- deselectMultiOption(state2, option?.getAttribute("value"), container);
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 (container?.select) {
195
- container.select.value = option.getAttribute("value");
226
+ if (this.select) {
227
+ this.select.value = option.getAttribute("value");
196
228
  }
197
- updateLabel(state2, container);
229
+ this.updateLabel();
198
230
  }
199
- closeDropdown(container);
231
+ this.closeDropdown();
200
232
  }
201
233
  };
202
- const handleContainerClick = (e, state2, container) => {
234
+ handleContainerClick = (e) => {
203
235
  const target = e.target;
204
236
  if (target.closest(".sui-select-badge svg")) {
205
- deselectMultiOption(
206
- state2,
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(state2, container);
240
+ this.handleBadgeOverflow();
211
241
  }
212
242
  if (target.closest(".sui-select-button")) {
213
- const container2 = target.closest(".sui-select-label");
214
- if (container2.dropdown?.classList.contains("active")) {
215
- closeDropdown(container2);
243
+ const container = target.closest(".sui-select-label");
244
+ if (container.dropdown?.classList.contains("active")) {
245
+ this.closeDropdown();
216
246
  } else {
217
- openDropdown(state2, container2);
247
+ this.openDropdown();
218
248
  }
219
249
  }
220
250
  if (target.closest(".sui-select-dropdown.active")) {
221
- handleOptionSelect(target, state2, container);
251
+ this.handleOptionSelect(target);
222
252
  }
223
253
  };
224
- const handleSelectKeyDown = (e, state2, container) => {
225
- const active = !!container.dropdown?.classList.contains("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(container);
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 && state2.isMultipleMap[container?.dataset.id]) {
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(state2, badgeValue, container);
241
- handleBadgeOverflow(state2, container);
270
+ this.deselectMultiOption(badgeValue);
271
+ this.handleBadgeOverflow();
242
272
  setTimeout(() => {
243
273
  if (nextBadgeValue) {
244
- const badgeToFocus = container?.querySelector(
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
- container?.button?.focus();
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(state2, container);
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 = container?.querySelector(".sui-select-option.focused");
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(container);
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
- state2.focusIndex = Array.from(
282
- container?.dropdown?.querySelectorAll(".sui-select-option") || []
311
+ this.state.focusIndex = Array.from(
312
+ this.dropdown?.querySelectorAll(".sui-select-option") || []
283
313
  ).indexOf(interactiveOptions[currentInteractiveIndex - 1]);
284
- recomputeOptions(state2, container);
314
+ this.recomputeOptions();
285
315
  }
286
316
  if (e.key === "ArrowDown" && currentInteractiveIndex < interactiveOptions.length - 1) {
287
- state2.focusIndex = Array.from(
288
- container?.dropdown?.querySelectorAll(".sui-select-option") || []
317
+ this.state.focusIndex = Array.from(
318
+ this.dropdown?.querySelectorAll(".sui-select-option") || []
289
319
  ).indexOf(interactiveOptions[currentInteractiveIndex + 1]);
290
- recomputeOptions(state2, container);
320
+ this.recomputeOptions();
291
321
  }
292
322
  if (e.key === "PageUp") {
293
- state2.focusIndex = Array.from(
294
- container?.dropdown?.querySelectorAll(".sui-select-option") || []
323
+ this.state.focusIndex = Array.from(
324
+ this.dropdown?.querySelectorAll(".sui-select-option") || []
295
325
  ).indexOf(interactiveOptions[0]);
296
- recomputeOptions(state2, container);
326
+ this.recomputeOptions();
297
327
  }
298
328
  if (e.key === "PageDown") {
299
- state2.focusIndex = Array.from(
300
- container?.dropdown?.querySelectorAll(".sui-select-option") || []
329
+ this.state.focusIndex = Array.from(
330
+ this.dropdown?.querySelectorAll(".sui-select-option") || []
301
331
  ).indexOf(interactiveOptions[interactiveOptions.length - 1]);
302
- recomputeOptions(state2, container);
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
- document.addEventListener("astro:page-load", loadSelects);
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": `
@@ -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 | 100 | 80 | 90 | 70;
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";
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@studiocms/ui",
3
- "version": "1.0.0-beta.4",
3
+ "version": "1.0.0-beta.6",
4
4
  "description": "The UI library for StudioCMS. Includes the layouts & components we use to build StudioCMS.",
5
5
  "repository": {
6
6
  "type": "git",