v-uixy 1.3.1 → 1.4.0

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.
Files changed (34) hide show
  1. package/package.json +1 -1
  2. package/templates/assets/css/main.css +4 -0
  3. package/templates/components/Avatar/Avatar.component.vue +7 -3
  4. package/templates/components/Avatar/Avatar.styles.ts +3 -3
  5. package/templates/components/Button/Button.styles.ts +3 -3
  6. package/templates/components/Calendar/Calendar.component.vue +26 -15
  7. package/templates/components/Calendar/Calendar.styles.ts +11 -3
  8. package/templates/components/Calendar/Calendar.types.ts +1 -0
  9. package/templates/components/Checkbox/Checkbox.styles.ts +1 -1
  10. package/templates/components/ColorPicker/ColorPicker.component.vue +4 -4
  11. package/templates/components/Command/Command.component.vue +6 -6
  12. package/templates/components/Command/Command.types.ts +1 -0
  13. package/templates/components/Input/Input.component.vue +27 -27
  14. package/templates/components/Input/Input.styles.ts +5 -5
  15. package/templates/components/Input/Input.types.ts +1 -0
  16. package/templates/components/Modal/Modal.component.vue +28 -4
  17. package/templates/components/Modal/Modal.types.ts +3 -1
  18. package/templates/components/Popover/Popover.component.vue +33 -2
  19. package/templates/components/Popover/Popover.types.ts +1 -0
  20. package/templates/components/Select/Select.component.vue +253 -108
  21. package/templates/components/Select/Select.styles.ts +6 -6
  22. package/templates/components/Select/Select.types.ts +12 -0
  23. package/templates/components/Select/SelectTreeItem.vue +154 -0
  24. package/templates/components/Select/composables/useSelect.composable.ts +260 -42
  25. package/templates/components/Tabs/Tabs.component.vue +11 -2
  26. package/templates/components/Tabs/Tabs.types.ts +1 -0
  27. package/templates/components/Textarea/Textarea.component.vue +23 -32
  28. package/templates/components/Textarea/Textarea.styles.ts +4 -4
  29. package/templates/components/Textarea/Textarea.types.ts +1 -0
  30. package/templates/components/TimePicker/TimePicker.component.vue +21 -18
  31. package/templates/components/TimePicker/TimePicker.styles.ts +1 -1
  32. package/templates/components/TimePicker/TimePicker.types.ts +1 -0
  33. package/templates/components.json +1 -1
  34. package/templates/composables/usePosition.ts +17 -2
@@ -5,6 +5,7 @@
5
5
  :class="selectStyles({ status }, $attrs.class as string)"
6
6
  :tabIndex="props.disabled ? -1 : 0"
7
7
  :auto-focus="props.autoFocus"
8
+ ref="refTrigger"
8
9
  @click="handleOpen"
9
10
  @keydown="handleKeydownSelect"
10
11
  v-bind="filteredAttrs"
@@ -20,7 +21,7 @@
20
21
  :class="badgesWrapperStyles({ disabled: props.disabled })"
21
22
  >
22
23
  <uixy-badge
23
- v-for="(item, index) in model"
24
+ v-for="(item, index) in model as string[]"
24
25
  data-badge
25
26
  variant="tertiary"
26
27
  size="xs"
@@ -28,94 +29,135 @@
28
29
  :key="`${item}--${index}`"
29
30
  @click.stop="handleClickSelectedOption(item)"
30
31
  >
31
- {{ item }}
32
+ {{ getLabel(item) }}
32
33
  </uixy-badge>
33
34
  </div>
34
- <p v-else>{{ model }}</p>
35
+ <p v-else>{{ getLabel(model as string) }}</p>
35
36
 
37
+ <uixy-icon
38
+ v-if="props.deselectable && !props.multiple && model"
39
+ name="close"
40
+ class="absolute right-7 top-1/2 h-4 w-4 -translate-y-1/2 cursor-pointer z-10 text-gray-500 dark:text-gray-600 hover:text-black dark:hover:text-gray-400"
41
+ @click.stop="handleDeselect"
42
+ />
36
43
  <uixy-icon name="chevron-down" :class="iconStyles({ open })" />
37
44
  </div>
38
- <animate-presence>
45
+
46
+ <teleport to="body">
47
+ <animate-presence>
48
+ <motion.div
49
+ v-if="open && !props.disabled"
50
+ class="scrollbar z-50 max-h-60 overflow-y-auto rounded-1 border border-gray-300 bg-white p-1 text-sm shadow-sm dark:border-gray-900 dark:bg-gray-1000"
51
+ :style="dropdownStyles"
52
+ :initial="{ opacity: 0, y: -4, scale: 0.98 }"
53
+ :animate="{ opacity: 1, y: 0, scale: 1 }"
54
+ :exit="{ opacity: 0, scale: 0.98 }"
55
+ :transition="{ duration: 0.12, ease: 'easeInOut' }"
56
+ >
57
+ <div ref="refDropdownEl" select-dropdown>
58
+ <div v-if="props.search" class="mb-2" @click.stop>
59
+ <uixy-input
60
+ ref="refSearchInput"
61
+ v-model="search"
62
+ status="default"
63
+ icon="search"
64
+ iconPositon="right"
65
+ placeholder="Search..."
66
+ hide-helper
67
+ />
68
+ </div>
69
+
70
+ <ul v-if="props.tree" class="flex flex-col gap-1 py-1">
71
+ <template
72
+ v-for="option in filteredOptions"
73
+ :key="option.value ?? option.label"
74
+ >
75
+ <SelectTreeItem
76
+ :option="option"
77
+ :depth="0"
78
+ :is-selected="isSelected"
79
+ :is-partially-selected="isPartiallySelected"
80
+ :expanded-items="effectiveExpandedItems"
81
+ :leaf-only="props.leafOnly"
82
+ :multiple="props.multiple"
83
+ :search-term="search"
84
+ :original-options="props.options"
85
+ @toggle-expand="toggleExpand"
86
+ @select="handleClick"
87
+ />
88
+ </template>
89
+
90
+ <li
91
+ v-if="filteredOptions.length === 0"
92
+ class="px-3 text-center dark:text-gray-600"
93
+ >
94
+ No results found
95
+ </li>
96
+ </ul>
97
+
98
+ <ul v-else class="flex flex-col gap-1 py-1">
99
+ <li
100
+ v-for="option in filteredOptions"
101
+ :tabIndex="option.disabled ? -1 : 0"
102
+ :key="option.value ?? option.label"
103
+ :class="
104
+ itemStyles({
105
+ selected: isSelected(option),
106
+ disabled: !!option.disabled,
107
+ })
108
+ "
109
+ @keydown="handleKeydownOption($event, option)"
110
+ @click="handleClick(option.value ?? option.label)"
111
+ >
112
+ {{ option.label }}
113
+ <uixy-icon
114
+ v-if="isSelected(option)"
115
+ name="check"
116
+ class="absolute right-2 top-2 size-4"
117
+ />
118
+ </li>
119
+
120
+ <li
121
+ v-if="customAddVisible"
122
+ class="relative py-2 px-3 rounded-1 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-900 text-gray-500 dark:text-gray-600"
123
+ @click="handleCustomAdd"
124
+ >
125
+ Add: "{{ search }}"
126
+ </li>
127
+
128
+ <li
129
+ v-if="filteredOptions.length === 0 && !customAddVisible"
130
+ class="px-3 text-center dark:text-gray-600"
131
+ >
132
+ No results found
133
+ </li>
134
+ </ul>
135
+ </div>
136
+ </motion.div>
137
+ </animate-presence>
138
+ </teleport>
139
+ <div v-if="props.label" :class="labelStyles({ status })">
140
+ {{ props.label }}
141
+ </div>
142
+ </div>
143
+ <div v-if="!props.hideHelper" class="h-4">
144
+ <animate-presence mode="wait" :initial="false">
39
145
  <motion.div
40
- v-if="open && !props.disabled"
41
- class="scrollbar absolute top-full z-20 mt-[2px] max-h-[320px] w-full overflow-y-auto rounded-1 border border-gray-300 bg-white p-1 text-sm dark:border-gray-900 dark:bg-gray-1000"
42
- :initial="{ opacity: 0, y: -4, scale: 0.8 }"
43
- :animate="{ opacity: 1, y: 0, scale: 1 }"
44
- :exit="{ opacity: 0, scale: 0.8 }"
45
- :transition="{ duration: 0.15, ease: 'easeInOut' }"
46
- select-dropdown
146
+ :initial="{ opacity: 0, y: '-4px' }"
147
+ :animate="{ opacity: 1, y: 0 }"
148
+ :exit="{ opacity: 0, y: '-4px' }"
149
+ :transition="{ duration: 0.125 }"
150
+ :key="status"
47
151
  >
48
- <div v-if="props.search" class="mb-2">
49
- <uixy-input
50
- v-model="search"
51
- status="default"
52
- icon="search"
53
- iconPositon="right"
54
- placeholder="Search..."
55
- auto-focus
56
- />
57
- </div>
58
- <ul class="flex flex-col gap-1 py-1">
59
- <li
60
- v-for="option in filteredOptions"
61
- :tabIndex="option.disabled ? -1 : 0"
62
- :key="option.label"
63
- :class="
64
- itemStyles({
65
- selected: isSelected(option),
66
- disabled: !!option.disabled,
67
- })
68
- "
69
- @keydown="handleKeydownOption($event, option)"
70
- @click="handleClick(option.label)"
71
- >
72
- {{ option.label }}
73
- <uixy-icon
74
- v-if="isSelected(option)"
75
- name="check"
76
- class="absolute right-2 top-2 size-4"
77
- />
78
- </li>
79
- <li
80
- v-if="filteredOptions.length === 0"
81
- class="px-3 text-center dark:text-gray-600"
82
- >
83
- No results found
84
- </li>
85
- </ul>
152
+ <p :class="helperStyles({ status })">{{ text }}</p>
86
153
  </motion.div>
87
154
  </animate-presence>
88
- <div v-if="props.label" :class="labelStyles({ status })">
89
- {{ props.label }}
90
- </div>
91
155
  </div>
92
- <animate-presence mode="wait" :initial="false">
93
- <motion.div
94
- v-if="!!text"
95
- class="overflow-hidden"
96
- :initial="{ height: 0 }"
97
- :animate="{ height: 'auto' }"
98
- :exit="{ height: 0 }"
99
- :transition="{ duration: 0.1, ease: 'easeInOut' }"
100
- >
101
- <animate-presence mode="wait" :initial="false">
102
- <motion.p
103
- :key="status + text"
104
- :class="helperStyles({ status })"
105
- :initial="{ opacity: 0, y: -4 }"
106
- :animate="{ opacity: 1, y: 0 }"
107
- :exit="{ opacity: 0, y: 4 }"
108
- :transition="{ duration: 0.12, ease: 'easeInOut', delay: 0.1 }"
109
- >
110
- {{ text }}
111
- </motion.p>
112
- </animate-presence>
113
- </motion.div>
114
- </animate-presence>
115
156
  </div>
116
157
  </template>
117
158
 
118
- <script setup lang="ts" generic="T extends (string | string[])">
159
+ <script setup lang="ts" generic="T extends string | string[]">
160
+ import { nextTick, onWatcherCleanup, onMounted } from "vue";
119
161
  import {
120
162
  selectStyles,
121
163
  iconStyles,
@@ -124,16 +166,13 @@
124
166
  helperStyles,
125
167
  badgesWrapperStyles,
126
168
  } from "./Select.styles";
127
- import type {
128
- UixySelectEmits,
129
- UixySelectOptionType,
130
- UixySelectProps,
131
- } from "./Select.types";
169
+ import type { UixySelectEmits, UixySelectProps } from "./Select.types";
132
170
  import { UixyBadge } from "../Badge";
133
171
  import { UixyIcon } from "../Icon";
134
172
  import { UixyInput } from "../Input";
135
173
  import { motion, AnimatePresence } from "motion-v";
136
174
  import { useSelect } from "./composables";
175
+ import SelectTreeItem from "./SelectTreeItem.vue";
137
176
 
138
177
  const props = defineProps<UixySelectProps>();
139
178
 
@@ -153,49 +192,155 @@
153
192
  return rest;
154
193
  });
155
194
 
156
- const handleChange = (v: string) => {
157
- if (props.multiple) {
158
- let prevArray = model.value as string[];
195
+ const refTrigger = ref<HTMLElement>();
196
+ const refDropdownEl = ref<HTMLElement>();
197
+ const refSearchInput = ref<{ focus: () => void } | null>(null);
159
198
 
160
- if (!Array.isArray(prevArray)) prevArray = [];
161
-
162
- if (prevArray.includes(v)) {
163
- (model.value as string[]) = prevArray.filter((item) => item !== v);
164
- } else {
165
- if (props.max && prevArray.length >= props.max)
166
- (model.value as string[]) = [...prevArray.slice(1), v];
167
-
168
- (model.value as string[]) = [...prevArray, v];
169
- }
170
- } else {
171
- (model.value as string) = v;
172
- }
173
- };
199
+ const dropdownStyles = ref<Record<string, string>>({
200
+ position: "absolute",
201
+ top: "0px",
202
+ left: "0px",
203
+ width: "0px",
204
+ });
174
205
 
175
206
  const {
176
207
  refOptions,
177
208
  open,
178
209
  search,
179
210
  filteredOptions,
211
+ effectiveExpandedItems,
212
+ customAddVisible,
213
+ isSelected,
214
+ isPartiallySelected,
215
+ getLabel,
216
+ toggleExpand,
180
217
  handleOpen,
181
218
  handleKeydownSelect,
182
219
  handleKeydownOption,
183
220
  handleClickSelectedOption,
184
- } = useSelect<T>(props.options, !!props.disabled, handleChange);
221
+ handleClick,
222
+ handleDeselect,
223
+ handleCustomAdd,
224
+ } = useSelect<T>(props, model as Ref<T>, emits, refDropdownEl);
225
+
226
+ const updateDropdownPosition = async () => {
227
+ if (!open.value || props.disabled) return;
228
+
229
+ await nextTick();
230
+ await new Promise<void>((r) => requestAnimationFrame(() => r()));
231
+
232
+ const trigger = refTrigger.value;
233
+ const dropdown = refDropdownEl.value;
234
+ if (!trigger || !dropdown) return;
235
+
236
+ const triggerRect = trigger.getBoundingClientRect();
237
+ const gap = 4;
238
+ const pad = 8;
239
+ const width = Math.max(80, triggerRect.width);
185
240
 
186
- const handleClick = (label: string) => {
187
- handleChange(label);
241
+ const scrollY = window.scrollY;
242
+ const scrollX = window.scrollX;
188
243
 
189
- if (!props.multiple) open.value = false;
244
+ const cs = window.getComputedStyle(dropdown);
245
+ const maxH = Number.parseFloat(cs.maxHeight || "") || 240;
246
+ const desiredH = Math.min(dropdown.scrollHeight || 0, maxH);
247
+
248
+ const availableBelow = window.innerHeight - triggerRect.bottom - gap - pad;
249
+ const availableAbove = triggerRect.top - gap - pad;
250
+
251
+ const placeBelow =
252
+ desiredH <= availableBelow || availableBelow >= availableAbove;
253
+
254
+ let top =
255
+ (placeBelow
256
+ ? triggerRect.bottom + gap
257
+ : triggerRect.top - gap - desiredH) + scrollY;
258
+
259
+ const minTop = scrollY + pad;
260
+ const maxTop = scrollY + window.innerHeight - desiredH - pad;
261
+ top = Math.min(Math.max(minTop, top), Math.max(minTop, maxTop));
262
+
263
+ let left = triggerRect.left + scrollX;
264
+ const minLeft = scrollX + pad;
265
+ const maxLeft = scrollX + window.innerWidth - width - pad;
266
+ left = Math.min(Math.max(minLeft, left), Math.max(minLeft, maxLeft));
267
+
268
+ dropdownStyles.value = {
269
+ position: "absolute",
270
+ top: `${Math.round(top)}px`,
271
+ left: `${Math.round(left)}px`,
272
+ width: `${Math.round(width)}px`,
273
+ };
274
+ };
275
+
276
+ const onResize = () => {
277
+ void updateDropdownPosition();
278
+ };
279
+
280
+ const onAnyScroll = () => {
281
+ void updateDropdownPosition();
190
282
  };
191
283
 
192
- const isSelected = (option: UixySelectOptionType) =>
193
- Array.isArray(model.value)
194
- ? model.value.includes(option.label)
195
- : model.value === option.label;
284
+ watch(
285
+ () => open.value,
286
+ (isOpen) => {
287
+ emits("openChange", isOpen);
288
+
289
+ if (!isOpen) return;
290
+
291
+ void updateDropdownPosition();
292
+
293
+ if (props.search) {
294
+ nextTick(() => refSearchInput.value?.focus());
295
+ }
296
+
297
+ window.addEventListener("resize", onResize);
298
+ if (props.followOnScroll) {
299
+ window.addEventListener("scroll", onAnyScroll, true);
300
+ }
301
+
302
+ onWatcherCleanup(() => {
303
+ window.removeEventListener("resize", onResize);
304
+ window.removeEventListener("scroll", onAnyScroll, true);
305
+ });
306
+ },
307
+ );
308
+
309
+ onMounted(() => {
310
+ if (props.autoOpen && !props.disabled) {
311
+ const delayMs = (props.delay ?? 0) * 1000;
312
+ setTimeout(() => {
313
+ nextTick(() => {
314
+ open.value = true;
315
+ });
316
+ }, delayMs);
317
+ }
318
+ });
319
+
320
+ watch(search, () => {
321
+ if (!open.value) return;
322
+ void updateDropdownPosition();
323
+ });
324
+
325
+ watch(
326
+ () => filteredOptions.value.length,
327
+ () => {
328
+ if (!open.value) return;
329
+ void updateDropdownPosition();
330
+ },
331
+ );
332
+
333
+ watch(
334
+ () => model.value,
335
+ () => {
336
+ if (!open.value) return;
337
+ void updateDropdownPosition();
338
+ },
339
+ { deep: true },
340
+ );
196
341
 
197
342
  const status = computed(() =>
198
- props.disabled ? "disabled" : props.status ?? "default"
343
+ props.disabled ? "disabled" : (props.status ?? "default"),
199
344
  );
200
345
 
201
346
  const text = computed(
@@ -205,6 +350,6 @@
205
350
  default: props.helperText,
206
351
  disabled: "",
207
352
  valid: "",
208
- }[props.status ?? "default"])
353
+ })[props.status ?? "default"],
209
354
  );
210
355
  </script>
@@ -13,7 +13,7 @@ export const selectStyles = styles(
13
13
  valid:
14
14
  "bg-white dark:bg-gray-1000 focus-visible:border-gray-500 dark:focus-visible:border-gray-600 cursor-pointer",
15
15
  },
16
- }
16
+ },
17
17
  );
18
18
 
19
19
  export const iconStyles = styles(
@@ -23,11 +23,11 @@ export const iconStyles = styles(
23
23
  true: "rotate-180",
24
24
  false: "",
25
25
  },
26
- }
26
+ },
27
27
  );
28
28
 
29
29
  export const labelStyles = styles(
30
- "transition-colors ease-in-out duration-150 text-xs font-500",
30
+ "transition-colors ease-in-out duration-150 text-sm font-500",
31
31
  {
32
32
  status: {
33
33
  default: "",
@@ -36,7 +36,7 @@ export const labelStyles = styles(
36
36
  "text-gray-500 dark:text-gray-700 pointer-events-none select-none",
37
37
  valid: "",
38
38
  },
39
- }
39
+ },
40
40
  );
41
41
 
42
42
  export const itemStyles = styles(
@@ -50,7 +50,7 @@ export const itemStyles = styles(
50
50
  true: "pointer-events-none text-gray-500 dark:text-gray-700",
51
51
  false: "",
52
52
  },
53
- }
53
+ },
54
54
  );
55
55
 
56
56
  export const helperStyles = styles(
@@ -63,7 +63,7 @@ export const helperStyles = styles(
63
63
  "text-gray-400 dark:text-gray-800 pointer-events-none select-none",
64
64
  valid: "text-gray-600",
65
65
  },
66
- }
66
+ },
67
67
  );
68
68
 
69
69
  export const badgesWrapperStyles = styles("flex flex-wrap gap-1", {
@@ -1,6 +1,8 @@
1
1
  export interface UixySelectOptionType {
2
2
  label: string;
3
+ value?: string;
3
4
  disabled?: boolean;
5
+ children?: UixySelectOptionType[];
4
6
  }
5
7
 
6
8
  export interface UixyMultipleSelectProps {
@@ -13,6 +15,7 @@ export interface UixySingleSelectProps {
13
15
 
14
16
  export interface UixySelectEmits {
15
17
  (event: "change", v: string): void;
18
+ (event: "openChange", isOpen: boolean): void;
16
19
  }
17
20
 
18
21
  export type UixySelectProps = {
@@ -27,4 +30,13 @@ export type UixySelectProps = {
27
30
  errorText?: string;
28
31
  autoFocus?: boolean;
29
32
  max?: number;
33
+ tree?: boolean;
34
+ leafOnly?: boolean;
35
+ autoOpen?: boolean;
36
+ delay?: number;
37
+ hideHelper?: boolean;
38
+ deselectable?: boolean;
39
+ allowCustom?: boolean;
40
+ clearSearchOnSelect?: boolean;
41
+ followOnScroll?: boolean;
30
42
  } & (UixyMultipleSelectProps | UixySingleSelectProps);
@@ -0,0 +1,154 @@
1
+ <template>
2
+ <li class="flex flex-col">
3
+ <div
4
+ :tabIndex="option.disabled ? -1 : 0"
5
+ :class="
6
+ itemStyles({
7
+ selected: isSelected(option),
8
+ disabled: !!option.disabled,
9
+ })
10
+ "
11
+ :style="{
12
+ paddingLeft: `${depth * 28 + 12}px`,
13
+ cursor: option.disabled ? 'not-allowed' : 'pointer',
14
+ }"
15
+ @click.stop="handleItemClick"
16
+ >
17
+ <div class="flex items-center gap-2 flex-1 min-h-6">
18
+ <div
19
+ v-if="hasChildren"
20
+ class="shrink-0 size-6 flex items-center justify-center -ml-1 hover:bg-gray-200 dark:hover:bg-gray-800 rounded transition cursor-pointer"
21
+ @click.stop="$emit('toggle-expand', getValue(option))"
22
+ >
23
+ <uixy-icon
24
+ name="chevron-right"
25
+ class="size-4.5 transition-transform"
26
+ :class="{ 'rotate-90': isExpanded }"
27
+ />
28
+ </div>
29
+ <span
30
+ class="truncate"
31
+ :class="{
32
+ 'text-gray-400 dark:text-gray-600':
33
+ isNotSelectable && !option.disabled,
34
+ }"
35
+ >{{ option.label }}</span
36
+ >
37
+ </div>
38
+ <uixy-icon
39
+ v-if="isSelected(option)"
40
+ name="check"
41
+ class="absolute right-2 top-1/2 -translate-y-1/2 size-4"
42
+ />
43
+ <div
44
+ v-else-if="isPartiallySelected?.(option)"
45
+ class="absolute right-2 top-1/2 -translate-y-1/2 size-4 flex items-center justify-center"
46
+ >
47
+ <div class="h-0.5 w-2.5 rounded bg-current"></div>
48
+ </div>
49
+ </div>
50
+
51
+ <animate-presence>
52
+ <motion.ul
53
+ v-if="hasChildren && isExpanded"
54
+ class="flex flex-col gap-1 pt-1 overflow-hidden"
55
+ :initial="{ opacity: 0, height: 0 }"
56
+ :animate="{ opacity: 1, height: 'auto' }"
57
+ :exit="{ opacity: 0, height: 0 }"
58
+ :transition="{ duration: 0.15 }"
59
+ >
60
+ <SelectTreeItem
61
+ v-for="child in option.children"
62
+ :key="child.value ?? child.label"
63
+ :option="child"
64
+ :depth="depth + 1"
65
+ :is-selected="isSelected"
66
+ :is-partially-selected="isPartiallySelected"
67
+ :expanded-items="expandedItems"
68
+ :leaf-only="leafOnly"
69
+ :multiple="multiple"
70
+ :search-term="searchTerm"
71
+ :original-options="originalOptions"
72
+ @toggle-expand="$emit('toggle-expand', $event)"
73
+ @select="$emit('select', $event)"
74
+ />
75
+ </motion.ul>
76
+ </animate-presence>
77
+ </li>
78
+ </template>
79
+
80
+ <script setup lang="ts">
81
+ import { motion, AnimatePresence } from "motion-v";
82
+ import { itemStyles } from "./Select.styles";
83
+ import type { UixySelectOptionType } from "./Select.types";
84
+ import { UixyIcon } from "../Icon";
85
+
86
+ const props = defineProps<{
87
+ option: UixySelectOptionType;
88
+ depth: number;
89
+ isSelected: (option: UixySelectOptionType) => boolean;
90
+ isPartiallySelected?: (option: UixySelectOptionType) => boolean;
91
+ expandedItems: string[];
92
+ leafOnly?: boolean;
93
+ multiple?: boolean;
94
+ searchTerm?: string;
95
+ originalOptions?: UixySelectOptionType[];
96
+ }>();
97
+
98
+ const emit = defineEmits<{
99
+ (e: "toggle-expand", value: string): void;
100
+ (e: "select", value: string): void;
101
+ }>();
102
+
103
+ const getValue = (option: UixySelectOptionType) =>
104
+ option.value ?? option.label;
105
+
106
+ const hasChildren = computed(
107
+ () => props.option.children && props.option.children.length > 0,
108
+ );
109
+
110
+ const hasChildrenInOriginal = computed(() => {
111
+ if (!props.originalOptions) return hasChildren.value;
112
+
113
+ const findInTree = (
114
+ options: UixySelectOptionType[],
115
+ value: string,
116
+ ): UixySelectOptionType | null => {
117
+ for (const opt of options) {
118
+ if ((opt.value ?? opt.label) === value) return opt;
119
+ if (opt.children && opt.children.length > 0) {
120
+ const found = findInTree(opt.children, value);
121
+ if (found) return found;
122
+ }
123
+ }
124
+ return null;
125
+ };
126
+
127
+ const originalOption = findInTree(
128
+ props.originalOptions,
129
+ getValue(props.option),
130
+ );
131
+ return originalOption?.children && originalOption.children.length > 0;
132
+ });
133
+
134
+ const isExpanded = computed(() =>
135
+ props.expandedItems.includes(getValue(props.option)),
136
+ );
137
+
138
+ const isNotSelectable = computed(
139
+ () =>
140
+ props.option.disabled ||
141
+ (props.leafOnly && hasChildrenInOriginal.value && !props.multiple),
142
+ );
143
+
144
+ const handleItemClick = () => {
145
+ if (props.option.disabled) return;
146
+
147
+ if (props.leafOnly && hasChildrenInOriginal.value && !props.multiple) {
148
+ emit("toggle-expand", getValue(props.option));
149
+ return;
150
+ }
151
+
152
+ emit("select", getValue(props.option));
153
+ };
154
+ </script>