v-uixy 1.3.0 → 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.
- package/package.json +1 -1
- package/templates/assets/css/main.css +8 -0
- package/templates/components/Avatar/Avatar.component.vue +7 -3
- package/templates/components/Avatar/Avatar.styles.ts +3 -3
- package/templates/components/Badge/Badge.styles.ts +2 -2
- package/templates/components/Button/Button.styles.ts +3 -3
- package/templates/components/Calendar/Calendar.component.vue +26 -15
- package/templates/components/Calendar/Calendar.styles.ts +11 -3
- package/templates/components/Calendar/Calendar.types.ts +1 -0
- package/templates/components/ColorPicker/ColorPicker.component.vue +287 -0
- package/templates/components/ColorPicker/ColorPicker.types.ts +3 -0
- package/templates/components/ColorPicker/index.ts +4 -0
- package/templates/components/Command/Command.component.vue +6 -6
- package/templates/components/Command/Command.types.ts +1 -0
- package/templates/components/Input/Input.component.vue +14 -4
- package/templates/components/Input/Input.styles.ts +4 -4
- package/templates/components/Modal/Modal.component.vue +28 -4
- package/templates/components/Modal/Modal.types.ts +3 -1
- package/templates/components/Popover/Popover.component.vue +33 -2
- package/templates/components/Popover/Popover.types.ts +1 -0
- package/templates/components/Select/Select.component.vue +241 -87
- package/templates/components/Select/Select.styles.ts +5 -5
- package/templates/components/Select/Select.types.ts +12 -1
- package/templates/components/Select/SelectTreeItem.vue +154 -0
- package/templates/components/Select/composables/useSelect.composable.ts +260 -42
- package/templates/components/Sheet/Sheet.component.vue +6 -0
- package/templates/components/Tabs/Tabs.component.vue +11 -2
- package/templates/components/Tabs/Tabs.types.ts +1 -0
- package/templates/components/Textarea/Textarea.component.vue +10 -13
- package/templates/components/Textarea/Textarea.styles.ts +3 -3
- package/templates/components/TimePicker/TimePicker.component.vue +21 -18
- package/templates/components/TimePicker/TimePicker.styles.ts +1 -1
- package/templates/components/TimePicker/TimePicker.types.ts +1 -0
- package/templates/components/index.ts +35 -0
- package/templates/components.json +9 -3
- package/templates/composables/useInputValidation.ts +89 -0
- package/templates/composables/usePosition.ts +17 -2
|
@@ -17,7 +17,7 @@ export const inputStyles = styles(
|
|
|
17
17
|
right: "pr-8",
|
|
18
18
|
none: "",
|
|
19
19
|
},
|
|
20
|
-
}
|
|
20
|
+
},
|
|
21
21
|
);
|
|
22
22
|
|
|
23
23
|
export const iconStyles = styles(
|
|
@@ -41,7 +41,7 @@ export const iconStyles = styles(
|
|
|
41
41
|
true: "pointer-events-auto cursor-pointer",
|
|
42
42
|
false: "",
|
|
43
43
|
},
|
|
44
|
-
}
|
|
44
|
+
},
|
|
45
45
|
);
|
|
46
46
|
|
|
47
47
|
export const labelStyles = styles(
|
|
@@ -53,7 +53,7 @@ export const labelStyles = styles(
|
|
|
53
53
|
disabled: "text-gray-500 dark:text-gray-700",
|
|
54
54
|
valid: "",
|
|
55
55
|
},
|
|
56
|
-
}
|
|
56
|
+
},
|
|
57
57
|
);
|
|
58
58
|
|
|
59
59
|
export const helperStyles = styles(
|
|
@@ -65,5 +65,5 @@ export const helperStyles = styles(
|
|
|
65
65
|
disabled: "text-gray-400 dark:text-gray-800",
|
|
66
66
|
valid: "text-gray-600",
|
|
67
67
|
},
|
|
68
|
-
}
|
|
68
|
+
},
|
|
69
69
|
);
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
:class="modalStyles($attrs.class as string)"
|
|
12
12
|
data-modal
|
|
13
13
|
@click.self="handleClick"
|
|
14
|
-
@keydown{esc}="handleClick"
|
|
15
14
|
>
|
|
16
15
|
<motion.div
|
|
17
16
|
:initial="{ scale: 0.8 }"
|
|
@@ -32,7 +31,7 @@
|
|
|
32
31
|
import { modalStyles } from "./Modal.styles";
|
|
33
32
|
import { AnimatePresence, motion } from "motion-v";
|
|
34
33
|
|
|
35
|
-
defineProps<UixyModalProps>();
|
|
34
|
+
const props = defineProps<UixyModalProps>();
|
|
36
35
|
|
|
37
36
|
defineOptions({
|
|
38
37
|
inheritAttrs: false,
|
|
@@ -41,14 +40,30 @@
|
|
|
41
40
|
const open = defineModel<boolean>();
|
|
42
41
|
|
|
43
42
|
const handleClick = () => {
|
|
43
|
+
if (props.persistent) return;
|
|
44
|
+
|
|
44
45
|
open.value = false;
|
|
45
46
|
};
|
|
46
47
|
|
|
48
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
49
|
+
if (e.key === "Escape") {
|
|
50
|
+
handleClick();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (e.key === "Enter") {
|
|
54
|
+
const target = e.target as HTMLElement;
|
|
55
|
+
if (target.tagName === "TEXTAREA" || target.tagName === "BUTTON") return;
|
|
56
|
+
const modalEl = document.querySelector("[data-modal]");
|
|
57
|
+
const form = modalEl?.querySelector("form");
|
|
58
|
+
form?.requestSubmit();
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
47
62
|
const toggleSiblingsInert = (enable?: boolean) => {
|
|
48
63
|
if (typeof document === "undefined") return;
|
|
49
64
|
|
|
50
65
|
const modalEl = document.querySelector(
|
|
51
|
-
"[data-modal]"
|
|
66
|
+
"[data-modal]",
|
|
52
67
|
) as HTMLElement | null;
|
|
53
68
|
|
|
54
69
|
Array.from(document.body.children).forEach((el) => {
|
|
@@ -68,7 +83,16 @@
|
|
|
68
83
|
open,
|
|
69
84
|
(isOpen) => {
|
|
70
85
|
toggleSiblingsInert(isOpen);
|
|
86
|
+
if (isOpen) {
|
|
87
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
88
|
+
} else {
|
|
89
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
90
|
+
}
|
|
71
91
|
},
|
|
72
|
-
{ immediate: true }
|
|
92
|
+
{ immediate: true },
|
|
73
93
|
);
|
|
94
|
+
|
|
95
|
+
onUnmounted(() => {
|
|
96
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
97
|
+
});
|
|
74
98
|
</script>
|
|
@@ -27,12 +27,16 @@
|
|
|
27
27
|
|
|
28
28
|
<script setup lang="ts">
|
|
29
29
|
import { AnimatePresence, motion } from "motion-v";
|
|
30
|
+
import { v4 as uuidv4 } from "uuid";
|
|
30
31
|
import { usePosition } from "~/composables";
|
|
31
32
|
import { onWatcherCleanup } from "vue";
|
|
32
33
|
import type { UixyPopoverProps } from "./Popover.types";
|
|
33
34
|
|
|
34
35
|
const props = defineProps<UixyPopoverProps>();
|
|
35
36
|
|
|
37
|
+
const POPOVER_GROUP_EVENT = "uixy:popover-group-open";
|
|
38
|
+
const instanceId = uuidv4();
|
|
39
|
+
|
|
36
40
|
const { active, handleOpen, handleLeave, styles, refElement } = usePosition({
|
|
37
41
|
direction: props.direction ?? "bottom",
|
|
38
42
|
});
|
|
@@ -45,22 +49,49 @@
|
|
|
45
49
|
handleLeave();
|
|
46
50
|
};
|
|
47
51
|
|
|
52
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
53
|
+
if (e.key === "Escape") handleLeave();
|
|
54
|
+
};
|
|
55
|
+
|
|
48
56
|
watch(
|
|
49
57
|
() => active.value,
|
|
50
|
-
(
|
|
58
|
+
(isActive, prevVal) => {
|
|
51
59
|
if (prevVal) return;
|
|
52
60
|
|
|
61
|
+
if (isActive && props.group) {
|
|
62
|
+
window.dispatchEvent(
|
|
63
|
+
new CustomEvent(POPOVER_GROUP_EVENT, { detail: { id: instanceId } }),
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
53
67
|
requestAnimationFrame(() => {
|
|
54
68
|
window.addEventListener("click", handleClickOutside);
|
|
69
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
55
70
|
});
|
|
56
71
|
|
|
57
72
|
onWatcherCleanup(() => {
|
|
58
73
|
window.removeEventListener("click", handleClickOutside);
|
|
74
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
59
75
|
});
|
|
60
|
-
}
|
|
76
|
+
},
|
|
61
77
|
);
|
|
62
78
|
|
|
79
|
+
const handleGroupOpen = (e: Event) => {
|
|
80
|
+
const event = e as CustomEvent<{ id: string }>;
|
|
81
|
+
if (event.detail.id !== instanceId) handleLeave();
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
onMounted(() => {
|
|
85
|
+
if (props.group) {
|
|
86
|
+
window.addEventListener(POPOVER_GROUP_EVENT, handleGroupOpen);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
63
90
|
onUnmounted(() => {
|
|
64
91
|
window.removeEventListener("click", handleClickOutside);
|
|
92
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
93
|
+
if (props.group) {
|
|
94
|
+
window.removeEventListener(POPOVER_GROUP_EVENT, handleGroupOpen);
|
|
95
|
+
}
|
|
65
96
|
});
|
|
66
97
|
</script>
|
|
@@ -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,64 +29,113 @@
|
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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>
|
|
89
139
|
<div v-if="props.label" :class="labelStyles({ status })">
|
|
90
140
|
{{ props.label }}
|
|
91
141
|
</div>
|
|
@@ -106,7 +156,8 @@
|
|
|
106
156
|
</div>
|
|
107
157
|
</template>
|
|
108
158
|
|
|
109
|
-
<script setup lang="ts" generic="T extends
|
|
159
|
+
<script setup lang="ts" generic="T extends string | string[]">
|
|
160
|
+
import { nextTick, onWatcherCleanup, onMounted } from "vue";
|
|
110
161
|
import {
|
|
111
162
|
selectStyles,
|
|
112
163
|
iconStyles,
|
|
@@ -115,16 +166,13 @@
|
|
|
115
166
|
helperStyles,
|
|
116
167
|
badgesWrapperStyles,
|
|
117
168
|
} from "./Select.styles";
|
|
118
|
-
import type {
|
|
119
|
-
UixySelectEmits,
|
|
120
|
-
UixySelectOptionType,
|
|
121
|
-
UixySelectProps,
|
|
122
|
-
} from "./Select.types";
|
|
169
|
+
import type { UixySelectEmits, UixySelectProps } from "./Select.types";
|
|
123
170
|
import { UixyBadge } from "../Badge";
|
|
124
171
|
import { UixyIcon } from "../Icon";
|
|
125
172
|
import { UixyInput } from "../Input";
|
|
126
173
|
import { motion, AnimatePresence } from "motion-v";
|
|
127
174
|
import { useSelect } from "./composables";
|
|
175
|
+
import SelectTreeItem from "./SelectTreeItem.vue";
|
|
128
176
|
|
|
129
177
|
const props = defineProps<UixySelectProps>();
|
|
130
178
|
|
|
@@ -144,49 +192,155 @@
|
|
|
144
192
|
return rest;
|
|
145
193
|
});
|
|
146
194
|
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (!Array.isArray(prevArray)) prevArray = [];
|
|
195
|
+
const refTrigger = ref<HTMLElement>();
|
|
196
|
+
const refDropdownEl = ref<HTMLElement>();
|
|
197
|
+
const refSearchInput = ref<{ focus: () => void } | null>(null);
|
|
152
198
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
(model.value as string[]) = [...prevArray, v];
|
|
160
|
-
}
|
|
161
|
-
} else {
|
|
162
|
-
(model.value as string) = v;
|
|
163
|
-
}
|
|
164
|
-
};
|
|
199
|
+
const dropdownStyles = ref<Record<string, string>>({
|
|
200
|
+
position: "absolute",
|
|
201
|
+
top: "0px",
|
|
202
|
+
left: "0px",
|
|
203
|
+
width: "0px",
|
|
204
|
+
});
|
|
165
205
|
|
|
166
206
|
const {
|
|
167
207
|
refOptions,
|
|
168
208
|
open,
|
|
169
209
|
search,
|
|
170
210
|
filteredOptions,
|
|
211
|
+
effectiveExpandedItems,
|
|
212
|
+
customAddVisible,
|
|
213
|
+
isSelected,
|
|
214
|
+
isPartiallySelected,
|
|
215
|
+
getLabel,
|
|
216
|
+
toggleExpand,
|
|
171
217
|
handleOpen,
|
|
172
218
|
handleKeydownSelect,
|
|
173
219
|
handleKeydownOption,
|
|
174
220
|
handleClickSelectedOption,
|
|
175
|
-
|
|
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);
|
|
240
|
+
|
|
241
|
+
const scrollY = window.scrollY;
|
|
242
|
+
const scrollX = window.scrollX;
|
|
243
|
+
|
|
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;
|
|
176
250
|
|
|
177
|
-
|
|
178
|
-
|
|
251
|
+
const placeBelow =
|
|
252
|
+
desiredH <= availableBelow || availableBelow >= availableAbove;
|
|
179
253
|
|
|
180
|
-
|
|
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();
|
|
181
282
|
};
|
|
182
283
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
+
);
|
|
187
341
|
|
|
188
342
|
const status = computed(() =>
|
|
189
|
-
props.disabled ? "disabled" : props.status ?? "default"
|
|
343
|
+
props.disabled ? "disabled" : (props.status ?? "default"),
|
|
190
344
|
);
|
|
191
345
|
|
|
192
346
|
const text = computed(
|
|
@@ -196,6 +350,6 @@
|
|
|
196
350
|
default: props.helperText,
|
|
197
351
|
disabled: "",
|
|
198
352
|
valid: "",
|
|
199
|
-
}[props.status ?? "default"]
|
|
353
|
+
})[props.status ?? "default"],
|
|
200
354
|
);
|
|
201
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,7 +23,7 @@ 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(
|
|
@@ -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 = {
|
|
@@ -26,6 +29,14 @@ export type UixySelectProps = {
|
|
|
26
29
|
helperText?: string;
|
|
27
30
|
errorText?: string;
|
|
28
31
|
autoFocus?: boolean;
|
|
29
|
-
hideHelper?: boolean;
|
|
30
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;
|
|
31
42
|
} & (UixyMultipleSelectProps | UixySingleSelectProps);
|