@studiocms/ui 0.4.12 → 0.4.13

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.
@@ -1,169 +1,370 @@
1
1
  function loadSearchSelects() {
2
- const allSearchSelects = document.querySelectorAll(".sui-search-select-label");
3
- for (const container of allSearchSelects) {
4
- let hideOnClickOutside2 = function(element) {
5
- const outsideClickListener = (event) => {
6
- if (!element.contains(event.target) && isVisible(element) && active === true) {
7
- dropdown.classList.remove("active", "above");
8
- active = false;
9
- }
10
- };
11
- document.addEventListener("click", outsideClickListener);
12
- }, constructOptionsBasedOnOptions2 = function(options2) {
13
- dropdown.innerHTML = "";
14
- if (options2.length === 0) {
15
- const element = document.createElement("li");
16
- element.classList.add("empty-search-results");
17
- element.textContent = "No results found.";
18
- dropdown.appendChild(element);
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
+ };
25
+ };
26
+ const createSelectBadge = (value, label) => {
27
+ const badge = document.createElement("span");
28
+ badge.classList.add("sui-badge", "primary", "sm", "default", "full", "sui-search-select-badge");
29
+ badge.setAttribute("data-value", value);
30
+ 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>`;
31
+ return badge;
32
+ };
33
+ const recalculateBadges = (state2, container) => {
34
+ const badgeContainer = container.querySelector(".sui-search-select-badge-container");
35
+ if (!badgeContainer || !container.input) return;
36
+ badgeContainer.innerHTML = "";
37
+ const selectedValues = state2.selectedOptionsMap[container.dataset.id] || [];
38
+ const allOptions = state2.optionsMap[container.dataset.id] || [];
39
+ if (selectedValues.length === 0) {
40
+ container.input.placeholder = state2.placeholderMap[container.dataset.id] ?? "";
41
+ return;
42
+ }
43
+ for (const value of selectedValues.sort((a, b) => {
44
+ const numA = Number.parseInt(a.match(/\d+/)?.[0] || "0");
45
+ const numB = Number.parseInt(b.match(/\d+/)?.[0] || "0");
46
+ return numA - numB;
47
+ })) {
48
+ const option = allOptions.find((opt) => opt.value === value);
49
+ if (option) {
50
+ const newBadge = createSelectBadge(value, option.label);
51
+ badgeContainer.appendChild(newBadge);
19
52
  }
20
- let i = 0;
21
- for (const option of options2) {
22
- const element = document.createElement("li");
23
- element.classList.add(
24
- ...[
25
- "sui-search-select-option",
26
- option.disabled && "disabled",
27
- focusIndex === i && "focused"
28
- ].filter((x) => typeof x === "string")
29
- );
30
- element.role = "option";
31
- element.value = Number.parseInt(option.value);
32
- element.id = "";
33
- element.dataset.optionIndex = i.toString();
34
- element.dataset.value = option.value;
35
- element.textContent = option.label;
36
- element.addEventListener("click", (e) => handleSelection(e, element));
37
- dropdown.appendChild(element);
38
- i++;
53
+ }
54
+ };
55
+ const updateLabel = (isMultiple, state2, container) => {
56
+ const selectedInput = container?.input;
57
+ if (isMultiple) {
58
+ recalculateBadges(state2, container);
59
+ if (selectedInput) {
60
+ selectedInput.placeholder = state2.placeholderMap[container.dataset.id] ?? "";
39
61
  }
40
- optionElements = container.querySelectorAll("li");
41
- };
42
- var hideOnClickOutside = hideOnClickOutside2, constructOptionsBasedOnOptions = constructOptionsBasedOnOptions2;
43
- const hiddenSelect = container.querySelector("select");
44
- const searchWrapper = container.querySelector(".sui-search-input-wrapper");
45
- const searchInput = searchWrapper.querySelector("input");
46
- const dropdown = container.querySelector(".sui-search-select-dropdown");
47
- let optionElements = container.querySelectorAll("li");
48
- let active = false;
49
- const options = JSON.parse(container.dataset.options);
50
- const id = container.dataset.id;
51
- let filteredOptions = options;
52
- searchWrapper.addEventListener("click", () => {
53
- const { bottom, left, right, width, x, y, height } = searchWrapper.getBoundingClientRect();
54
- const optionHeight = 36;
55
- const totalBorderSize = 2;
56
- const margin = 4;
57
- const dropdownHeight = options.length * optionHeight + totalBorderSize + margin;
58
- const CustomRect = {
59
- top: bottom + margin,
60
- left,
61
- right,
62
- bottom: bottom + margin + dropdownHeight,
63
- width,
64
- height: dropdownHeight,
65
- x,
66
- y: y + height + margin
67
- };
68
- if (active) {
69
- searchInput.ariaExpanded = "false";
70
- dropdown.classList.remove("active", "above");
71
- active = false;
72
- return;
62
+ } else {
63
+ const selected = container.querySelector(
64
+ ".sui-search-select-option.selected"
65
+ );
66
+ if (selected && selectedInput) {
67
+ selectedInput.placeholder = selected.innerText.trim();
73
68
  }
74
- active = true;
75
- searchInput.ariaExpanded = "true";
76
- if (CustomRect.top >= 0 && CustomRect.left >= 0 && CustomRect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && CustomRect.right <= (window.innerWidth || document.documentElement.clientWidth)) {
77
- dropdown.classList.add("active");
69
+ }
70
+ };
71
+ const updateOptionSelection = (value, container, state2, forceState) => {
72
+ const currentSelected = state2.selectedOptionsMap[container.dataset.id] || [];
73
+ const isCurrentlySelected = currentSelected.includes(value);
74
+ const max = Number.parseInt(container.dataset.multipleMax);
75
+ if (!isCurrentlySelected && !Number.isNaN(max) && currentSelected.length >= max) {
76
+ return false;
77
+ }
78
+ const newSelected = isCurrentlySelected ? currentSelected.filter((v) => v !== value) : [...currentSelected, value];
79
+ state2.selectedOptionsMap[container.dataset.id] = newSelected;
80
+ const option = container.dropdown?.querySelector(
81
+ `.sui-search-select-option[data-value='${value}']`
82
+ );
83
+ if (option) {
84
+ option.classList.toggle("selected", forceState ?? !isCurrentlySelected);
85
+ }
86
+ const selectOption = container.select?.querySelector(
87
+ `option[value='${value}']`
88
+ );
89
+ if (selectOption) {
90
+ selectOption.selected = forceState ?? !isCurrentlySelected;
91
+ }
92
+ const selectedCountEl = container.querySelector(
93
+ ".sui-search-select-max-span .sui-search-select-select-count"
94
+ );
95
+ if (selectedCountEl) {
96
+ selectedCountEl.innerText = String(newSelected.length);
97
+ }
98
+ return true;
99
+ };
100
+ const toggleMultiOption = (id, container, state2) => {
101
+ const success = updateOptionSelection(id, container, state2);
102
+ if (success) {
103
+ recalculateBadges(state2, container);
104
+ }
105
+ };
106
+ const recomputeOptions = (state2, container) => {
107
+ const optionElements = container?.dropdown?.querySelectorAll(
108
+ ".sui-search-select-option"
109
+ );
110
+ for (const entry of optionElements) {
111
+ if (Number.parseInt(entry.dataset.optionIndex) === state2.focusIndex) {
112
+ entry.classList.add("focused");
78
113
  } else {
79
- dropdown.classList.add("active", "above");
114
+ entry.classList.remove("focused");
80
115
  }
81
- });
82
- const handleSelection = (e, option) => {
83
- e.stopImmediatePropagation();
84
- if (option.id === `${id}-selected` || !id) return;
85
- const currentlySelected = document.getElementById(`${id}-selected`);
86
- if (currentlySelected) {
87
- currentlySelected.classList.remove("selected");
88
- currentlySelected.id = "";
116
+ }
117
+ };
118
+ const reconstructOptions = (filteredOptions, state2, container) => {
119
+ container.dropdown.innerHTML = "";
120
+ let i = 0;
121
+ const selectedValues = state2.selectedOptionsMap[container.dataset.id] || [];
122
+ if (filteredOptions.length === 0) {
123
+ container.dropdown.innerHTML = '<li class="empty-search-results">No results found</li>';
124
+ return;
125
+ }
126
+ for (const option of filteredOptions) {
127
+ const element = document.createElement("li");
128
+ element.classList.add("sui-search-select-option");
129
+ if (option.disabled) {
130
+ element.classList.add("disabled");
89
131
  }
90
- option.id = `${id}-selected`;
91
- option.classList.add("selected");
92
- const index = options.findIndex((x) => x.value === option.dataset.value);
93
- focusIndex = index;
94
- const opt = options[index];
95
- hiddenSelect.value = opt.value;
96
- searchInput.placeholder = opt.label;
97
- dropdown.classList.remove("active", "above");
98
- searchInput.value = "";
99
- filteredOptions = options;
100
- constructOptionsBasedOnOptions2(options);
101
- active = false;
102
- };
103
- for (const option of optionElements) {
104
- option.addEventListener("click", (e) => handleSelection(e, option));
132
+ if (selectedValues.includes(option.value)) {
133
+ element.classList.add("selected");
134
+ }
135
+ element.role = "option";
136
+ element.dataset.optionIndex = i.toString();
137
+ element.dataset.value = option.value;
138
+ element.textContent = option.label;
139
+ container.dropdown?.appendChild(element);
140
+ i++;
105
141
  }
106
- window.addEventListener("scroll", () => {
107
- dropdown.classList.remove("active", "above");
108
- active = false;
109
- });
110
- hideOnClickOutside2(container);
111
- const isVisible = (elem) => !!elem && !!(elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length);
112
- let focusIndex = 0;
113
- const recomputeOptions = () => {
114
- for (const entry of optionElements) {
115
- if (Number.parseInt(entry.dataset.optionIndex) === focusIndex) {
116
- entry.classList.add("focused");
117
- } else {
118
- entry.classList.remove("focused");
119
- }
142
+ };
143
+ const getInteractiveOptions = (container) => {
144
+ const allOptions = container?.dropdown?.querySelectorAll(
145
+ ".sui-search-select-option"
146
+ );
147
+ return Array.from(allOptions).filter(
148
+ (option) => !option.classList.contains("hidden") && !option.classList.contains("disabled") && !option.hasAttribute("disabled")
149
+ );
150
+ };
151
+ const handleContainerMouseDown = (e, state2, container) => {
152
+ const target = e.target;
153
+ if (!target.closest("input")) {
154
+ e.preventDefault();
155
+ }
156
+ state2.isSelectingOption = true;
157
+ setTimeout(() => {
158
+ state2.isSelectingOption = false;
159
+ }, 0);
160
+ if (target.closest(".sui-search-select-badge svg")) {
161
+ const value = target.closest(".sui-search-select-badge")?.getAttribute("data-value");
162
+ const success = updateOptionSelection(value, container, state2);
163
+ if (success) {
164
+ recalculateBadges(state2, container);
120
165
  }
121
- };
122
- searchInput.addEventListener("keydown", (e) => {
123
- if (e.key === "Escape") {
124
- e.preventDefault();
125
- e.stopImmediatePropagation();
126
- active = false;
127
- dropdown.classList.remove("active", "above");
128
- searchInput.blur();
129
- return;
166
+ return;
167
+ }
168
+ const opt = target.closest(".sui-search-select-option");
169
+ if (!opt?.dataset.value) return;
170
+ if (opt.classList.contains("disabled") || opt.hasAttribute("disabled")) {
171
+ container.input?.focus();
172
+ return;
173
+ }
174
+ const isMultiple = state2.isMultipleMap[container.dataset.id];
175
+ if (isMultiple) {
176
+ const success = updateOptionSelection(opt.dataset.value, container, state2);
177
+ if (success) {
178
+ updateLabel(true, state2, container);
179
+ recalculateBadges(state2, container);
130
180
  }
131
- if (e.key === "ArrowUp" && focusIndex > 0) {
132
- e.preventDefault();
133
- e.stopImmediatePropagation();
134
- focusIndex--;
135
- recomputeOptions();
136
- return;
181
+ } else {
182
+ const currentSelected = state2.selectedOptionsMap[container.dataset.id] || [];
183
+ for (const value of currentSelected) {
184
+ updateOptionSelection(value, container, state2, false);
137
185
  }
138
- if (e.key === "ArrowDown" && focusIndex + 1 < filteredOptions.filter((x) => !x.disabled).length) {
186
+ updateOptionSelection(opt.dataset.value, container, state2, true);
187
+ updateLabel(false, state2, container);
188
+ container.dropdown?.classList.remove("active", "above");
189
+ container.input?.blur();
190
+ container.input.value = "";
191
+ }
192
+ };
193
+ const handleSelectKeyDown = (e, state2, container) => {
194
+ const focusedElement = document.activeElement;
195
+ if (e.key === "Escape" || e.key === "Tab") {
196
+ container.input?.blur();
197
+ container.dropdown?.classList.remove("active", "above");
198
+ return;
199
+ }
200
+ if ((e.key === "Enter" || e.key === " ") && focusedElement?.tagName.toLowerCase() === "svg") {
201
+ const badgeElement = focusedElement?.closest(".sui-search-select-badge");
202
+ if (badgeElement && state2.isMultipleMap[container?.dataset.id]) {
203
+ const badgeValue = badgeElement.getAttribute("data-value");
204
+ let nextBadge = badgeElement.previousElementSibling;
205
+ if (!nextBadge) {
206
+ nextBadge = badgeElement.nextElementSibling;
207
+ }
208
+ const nextBadgeValue = nextBadge?.getAttribute("data-value");
209
+ toggleMultiOption(badgeValue, container, state2);
210
+ recalculateBadges(state2, container);
211
+ setTimeout(() => {
212
+ if (nextBadgeValue) {
213
+ const badgeToFocus = container?.querySelector(
214
+ `.sui-search-select-badge[data-value="${nextBadgeValue}"] svg`
215
+ );
216
+ if (badgeToFocus) {
217
+ badgeToFocus.focus();
218
+ }
219
+ }
220
+ }, 0);
139
221
  e.preventDefault();
140
222
  e.stopImmediatePropagation();
141
- focusIndex++;
142
- recomputeOptions();
143
223
  return;
144
224
  }
145
- if (e.key === "Enter") {
146
- e.preventDefault();
147
- e.stopImmediatePropagation();
148
- for (const entry of optionElements) {
149
- if (Number.parseInt(entry.dataset.optionIndex) === focusIndex) {
150
- entry.click();
225
+ }
226
+ const interactiveOptions = getInteractiveOptions(container);
227
+ const currentInteractiveIndex = interactiveOptions.findIndex(
228
+ (option) => option.classList.contains("focused")
229
+ );
230
+ if (e.key === "ArrowUp" && currentInteractiveIndex > 0) {
231
+ state2.focusIndex = Array.from(
232
+ container?.dropdown?.querySelectorAll(".sui-search-select-option") || []
233
+ ).indexOf(interactiveOptions[currentInteractiveIndex - 1]);
234
+ recomputeOptions(state2, container);
235
+ return;
236
+ }
237
+ if (e.key === "ArrowDown" && currentInteractiveIndex < interactiveOptions.length - 1) {
238
+ state2.focusIndex = Array.from(
239
+ container?.dropdown?.querySelectorAll(".sui-search-select-option") || []
240
+ ).indexOf(interactiveOptions[currentInteractiveIndex + 1]);
241
+ recomputeOptions(state2, container);
242
+ return;
243
+ }
244
+ if (e.key === "PageUp") {
245
+ state2.focusIndex = Array.from(
246
+ container?.dropdown?.querySelectorAll(".sui-search-select-option") || []
247
+ ).indexOf(interactiveOptions[0]);
248
+ recomputeOptions(state2, container);
249
+ return;
250
+ }
251
+ if (e.key === "PageDown") {
252
+ state2.focusIndex = Array.from(
253
+ container?.dropdown?.querySelectorAll(".sui-search-select-option") || []
254
+ ).indexOf(interactiveOptions[interactiveOptions.length - 1]);
255
+ recomputeOptions(state2, container);
256
+ return;
257
+ }
258
+ if (e.key === "Enter") {
259
+ e.preventDefault();
260
+ e.stopImmediatePropagation();
261
+ const optionElements = container?.dropdown?.querySelectorAll(
262
+ ".sui-search-select-option"
263
+ );
264
+ const focusedOption = Array.from(optionElements).find(
265
+ (entry) => Number.parseInt(entry.dataset.optionIndex) === state2.focusIndex
266
+ );
267
+ if (focusedOption && !focusedOption.classList.contains("disabled") && !focusedOption.hasAttribute("disabled")) {
268
+ const value = focusedOption.dataset.value;
269
+ if (!value) return;
270
+ const isMultiple = state2.isMultipleMap[container.dataset.id];
271
+ if (isMultiple) {
272
+ const success = updateOptionSelection(value, container, state2);
273
+ if (success) {
274
+ updateLabel(true, state2, container);
275
+ recalculateBadges(state2, container);
151
276
  }
277
+ } else {
278
+ const currentSelected = state2.selectedOptionsMap[container.dataset.id] || [];
279
+ for (const existingValue of currentSelected) {
280
+ updateOptionSelection(existingValue, container, state2, false);
281
+ }
282
+ updateOptionSelection(value, container, state2, true);
283
+ updateLabel(false, state2, container);
284
+ container.dropdown?.classList.remove("active", "above");
285
+ container.input.value = "";
152
286
  }
153
- return;
154
287
  }
155
- });
156
- searchInput.addEventListener("keyup", (e) => {
157
- if (["Enter", "ArrowUp", "ArrowDown"].includes(e.key)) return;
158
- if (searchInput.value.trim().length === 0) {
159
- constructOptionsBasedOnOptions2(options);
160
- filteredOptions = options;
161
- return;
288
+ return;
289
+ }
290
+ };
291
+ const handleInputKeyup = (e, state2, container) => {
292
+ if (["Enter", "ArrowUp", "ArrowDown"].includes(e.key)) return;
293
+ const value = container.input.value.trim().toLowerCase();
294
+ const allOptions = state2.optionsMap[container.dataset.id];
295
+ if (value.length === 0) {
296
+ reconstructOptions(allOptions, state2, container);
297
+ return;
298
+ }
299
+ const filteredOptions = allOptions?.filter((option) => option.label.toLowerCase().includes(value)) ?? [];
300
+ state2.focusIndex = 0;
301
+ reconstructOptions(filteredOptions, state2, container);
302
+ };
303
+ const handleContainerFocusOut = (state2, container) => {
304
+ if (state2.isSelectingOption) return;
305
+ container.input.value = "";
306
+ reconstructOptions(state2.optionsMap[container.dataset.id] ?? [], state2, container);
307
+ container.dropdown?.classList.remove("active", "above");
308
+ };
309
+ const handleContainerFocusIn = (state2, container) => {
310
+ const allDropdowns = document.querySelectorAll(".sui-search-select-dropdown");
311
+ for (const dropdown of allDropdowns) {
312
+ if (dropdown !== container.dropdown) {
313
+ dropdown.classList.remove("active", "above");
162
314
  }
163
- filteredOptions = options.filter((x) => x.label.includes(searchInput.value));
164
- focusIndex = 0;
165
- constructOptionsBasedOnOptions2(filteredOptions);
315
+ }
316
+ const { isAbove } = getDropdownPosition(
317
+ container.input,
318
+ state2.optionsMap[container.dataset.id]?.length ?? 0
319
+ );
320
+ container.dropdown?.classList.add("active", ...isAbove ? [] : ["above"]);
321
+ };
322
+ const state = {
323
+ optionsMap: {},
324
+ isMultipleMap: {},
325
+ placeholderMap: {},
326
+ selectedOptionsMap: {},
327
+ focusIndex: 0,
328
+ isSelectingOption: false
329
+ };
330
+ const selects = document.querySelectorAll(".sui-search-select-label");
331
+ for (const container of selects) {
332
+ const id = container.dataset.id;
333
+ const specialContainer = Object.assign(container, {
334
+ input: container.querySelector("input"),
335
+ dropdown: container.querySelector(".sui-search-select-dropdown"),
336
+ select: container.querySelector("select")
166
337
  });
338
+ const selectedOptions = Array.from(
339
+ specialContainer.dropdown?.querySelectorAll(".sui-search-select-option.selected") ?? []
340
+ );
341
+ state.placeholderMap[id] = specialContainer.input?.placeholder ?? "";
342
+ state.optionsMap[id] = JSON.parse(container.dataset.options ?? "{}");
343
+ state.isMultipleMap[id] = container.dataset.multiple === "true";
344
+ state.selectedOptionsMap[id] = selectedOptions.map((x) => x.getAttribute("data-value") ?? "");
345
+ specialContainer.input?.addEventListener(
346
+ "focusin",
347
+ () => handleContainerFocusIn(state, specialContainer)
348
+ );
349
+ specialContainer.addEventListener(
350
+ "focusout",
351
+ () => handleContainerFocusOut(state, specialContainer)
352
+ );
353
+ specialContainer.addEventListener(
354
+ "keydown",
355
+ (e) => handleSelectKeyDown(e, state, specialContainer)
356
+ );
357
+ specialContainer.input?.addEventListener(
358
+ "keyup",
359
+ (e) => handleInputKeyup(e, state, specialContainer)
360
+ );
361
+ specialContainer.addEventListener(
362
+ "mousedown",
363
+ (e) => handleContainerMouseDown(e, state, specialContainer)
364
+ );
365
+ if (state.isMultipleMap[id]) {
366
+ recalculateBadges(state, specialContainer);
367
+ }
167
368
  }
168
369
  }
169
370
  document.addEventListener("astro:page-load", loadSearchSelects);