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
|
@@ -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>
|
|
@@ -1,33 +1,238 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
onWatcherCleanup,
|
|
3
|
+
watch,
|
|
4
|
+
computed,
|
|
5
|
+
ref,
|
|
6
|
+
onUnmounted,
|
|
7
|
+
type Ref,
|
|
8
|
+
} from "vue";
|
|
9
|
+
import type { UixySelectOptionType, UixySelectProps } from "../Select.types";
|
|
3
10
|
|
|
4
|
-
|
|
11
|
+
const normalizeForSearch = (str: string): string =>
|
|
12
|
+
str.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "");
|
|
13
|
+
|
|
14
|
+
const getValue = (option: UixySelectOptionType): string =>
|
|
15
|
+
option.value ?? option.label;
|
|
16
|
+
|
|
17
|
+
const collectLeafValues = (option: UixySelectOptionType): string[] => {
|
|
18
|
+
if (!option.children || option.children.length === 0)
|
|
19
|
+
return [getValue(option)];
|
|
20
|
+
return option.children.flatMap(collectLeafValues);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const findInTree = (
|
|
5
24
|
options: UixySelectOptionType[],
|
|
6
|
-
|
|
7
|
-
|
|
25
|
+
value: string,
|
|
26
|
+
): UixySelectOptionType | null => {
|
|
27
|
+
for (const opt of options) {
|
|
28
|
+
if (getValue(opt) === value) return opt;
|
|
29
|
+
if (opt.children?.length) {
|
|
30
|
+
const found = findInTree(opt.children, value);
|
|
31
|
+
if (found) return found;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const useSelect = <T extends string | string[]>(
|
|
38
|
+
props: UixySelectProps,
|
|
39
|
+
model: Ref<T>,
|
|
40
|
+
emit: (event: "change", v: string) => void,
|
|
41
|
+
dropdownRef?: Ref<HTMLElement | undefined>,
|
|
8
42
|
) => {
|
|
9
43
|
const open = ref(false);
|
|
10
44
|
const search = ref("");
|
|
11
45
|
const refOptions = ref<HTMLDivElement>();
|
|
46
|
+
const lastScrollAt = ref(0);
|
|
47
|
+
const expandedItems = ref<string[]>([]);
|
|
12
48
|
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
49
|
+
const filterTreeOptions = (
|
|
50
|
+
items: UixySelectOptionType[],
|
|
51
|
+
searchTerm: string,
|
|
52
|
+
): UixySelectOptionType[] => {
|
|
53
|
+
if (!searchTerm) return items;
|
|
54
|
+
const normSearch = normalizeForSearch(searchTerm);
|
|
55
|
+
const filterNode = (
|
|
56
|
+
node: UixySelectOptionType,
|
|
57
|
+
): UixySelectOptionType | null => {
|
|
58
|
+
const matches = normalizeForSearch(node.label).includes(normSearch);
|
|
59
|
+
let filteredChildren: UixySelectOptionType[] = [];
|
|
60
|
+
if (node.children?.length) {
|
|
61
|
+
filteredChildren = node.children
|
|
62
|
+
.map(filterNode)
|
|
63
|
+
.filter((n): n is UixySelectOptionType => n !== null);
|
|
64
|
+
}
|
|
65
|
+
if (matches || filteredChildren.length > 0) {
|
|
66
|
+
return { ...node, children: filteredChildren };
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
};
|
|
70
|
+
return items
|
|
71
|
+
.map(filterNode)
|
|
72
|
+
.filter((n): n is UixySelectOptionType => n !== null);
|
|
73
|
+
};
|
|
18
74
|
|
|
19
|
-
const
|
|
20
|
-
|
|
75
|
+
const filteredOptions = computed(() => {
|
|
76
|
+
const list = props.options ?? [];
|
|
77
|
+
if (props.tree) return filterTreeOptions(list, search.value);
|
|
78
|
+
return list.filter((option) =>
|
|
79
|
+
normalizeForSearch(option.label).includes(
|
|
80
|
+
normalizeForSearch(search.value),
|
|
81
|
+
),
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const collectParentsWithMatchingDescendants = (
|
|
86
|
+
items: UixySelectOptionType[],
|
|
87
|
+
searchTerm: string,
|
|
88
|
+
): string[] => {
|
|
89
|
+
if (!searchTerm) return [];
|
|
90
|
+
const lowerSearch = searchTerm.toLowerCase();
|
|
91
|
+
const parentsToExpand: string[] = [];
|
|
92
|
+
const checkNode = (node: UixySelectOptionType): boolean => {
|
|
93
|
+
const nodeMatches = node.label.toLowerCase().includes(lowerSearch);
|
|
94
|
+
let hasMatchingDescendant = false;
|
|
95
|
+
if (node.children?.length) {
|
|
96
|
+
for (const child of node.children) {
|
|
97
|
+
if (checkNode(child)) hasMatchingDescendant = true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (hasMatchingDescendant) parentsToExpand.push(getValue(node));
|
|
101
|
+
return nodeMatches || hasMatchingDescendant;
|
|
102
|
+
};
|
|
103
|
+
for (const item of items) checkNode(item);
|
|
104
|
+
return parentsToExpand;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const effectiveExpandedItems = computed(() => {
|
|
108
|
+
if (!search.value) return expandedItems.value;
|
|
109
|
+
const autoExpanded = collectParentsWithMatchingDescendants(
|
|
110
|
+
props.options,
|
|
111
|
+
search.value,
|
|
112
|
+
);
|
|
113
|
+
return Array.from(new Set([...expandedItems.value, ...autoExpanded]));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const toggleExpand = (value: string) => {
|
|
117
|
+
const idx = expandedItems.value.indexOf(value);
|
|
118
|
+
if (idx >= 0) expandedItems.value.splice(idx, 1);
|
|
119
|
+
else expandedItems.value.push(value);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const isSelected = (option: UixySelectOptionType): boolean => {
|
|
123
|
+
const v = getValue(option);
|
|
124
|
+
if (props.tree && props.multiple && option.children?.length) {
|
|
125
|
+
const leaves = collectLeafValues(option);
|
|
126
|
+
const arr = (model.value as string[]) ?? [];
|
|
127
|
+
return leaves.length > 0 && leaves.every((l) => arr.includes(l));
|
|
128
|
+
}
|
|
129
|
+
return Array.isArray(model.value)
|
|
130
|
+
? (model.value as string[]).includes(v)
|
|
131
|
+
: (model.value as string) === v;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const isPartiallySelected = (option: UixySelectOptionType): boolean => {
|
|
135
|
+
if (!props.tree || !props.multiple || !option.children?.length)
|
|
136
|
+
return false;
|
|
137
|
+
const leaves = collectLeafValues(option);
|
|
138
|
+
const arr = (model.value as string[]) ?? [];
|
|
139
|
+
const selected = leaves.filter((l) => arr.includes(l)).length;
|
|
140
|
+
return selected > 0 && selected < leaves.length;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const setMultiValues = (next: string[]) => {
|
|
144
|
+
(model.value as string[]) = next;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const toggleMultiValue = (v: string) => {
|
|
148
|
+
const prev = Array.isArray(model.value) ? (model.value as string[]) : [];
|
|
149
|
+
if (prev.includes(v)) {
|
|
150
|
+
setMultiValues(prev.filter((item) => item !== v));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (props.max && prev.length >= props.max) {
|
|
154
|
+
setMultiValues([...prev.slice(1), v]);
|
|
21
155
|
return;
|
|
156
|
+
}
|
|
157
|
+
setMultiValues([...prev, v]);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const cascadeMultiTree = (option: UixySelectOptionType) => {
|
|
161
|
+
const leaves = collectLeafValues(option);
|
|
162
|
+
const prev = Array.isArray(model.value) ? (model.value as string[]) : [];
|
|
163
|
+
const allSelected = leaves.every((l) => prev.includes(l));
|
|
164
|
+
if (allSelected) {
|
|
165
|
+
setMultiValues(prev.filter((v) => !leaves.includes(v)));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const merged = Array.from(new Set([...prev, ...leaves]));
|
|
169
|
+
if (props.max && merged.length > props.max) {
|
|
170
|
+
setMultiValues(merged.slice(merged.length - props.max));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
setMultiValues(merged);
|
|
174
|
+
};
|
|
22
175
|
|
|
176
|
+
const applyChange = (v: string) => {
|
|
177
|
+
if (!props.multiple) {
|
|
178
|
+
(model.value as string) = v;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (props.tree) {
|
|
182
|
+
const node = findInTree(props.options, v);
|
|
183
|
+
if (node?.children?.length) {
|
|
184
|
+
cascadeMultiTree(node);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
toggleMultiValue(v);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const handleClick = (value: string) => {
|
|
192
|
+
applyChange(value);
|
|
193
|
+
emit("change", value);
|
|
194
|
+
if (props.clearSearchOnSelect) search.value = "";
|
|
195
|
+
if (!props.multiple) open.value = false;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const handleClickSelectedOption = (item: string) => {
|
|
199
|
+
applyChange(item);
|
|
200
|
+
emit("change", item);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const handleDeselect = () => {
|
|
204
|
+
(model.value as string) = "";
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const getLabel = (value: string): string => {
|
|
208
|
+
if (!value) return "";
|
|
209
|
+
return findInTree(props.options, value)?.label ?? value;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const customAddVisible = computed(() => {
|
|
213
|
+
if (!props.allowCustom || !search.value) return false;
|
|
214
|
+
const lowerSearch = search.value.toLowerCase();
|
|
215
|
+
return !filteredOptions.value.some(
|
|
216
|
+
(opt) => opt.label.toLowerCase() === lowerSearch,
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const handleCustomAdd = () => {
|
|
221
|
+
handleClick(search.value);
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const handleOpen = (e?: MouseEvent) => {
|
|
225
|
+
if (
|
|
226
|
+
props.disabled ||
|
|
227
|
+
(e && "badge" in (e?.target as HTMLDivElement).dataset)
|
|
228
|
+
)
|
|
229
|
+
return;
|
|
23
230
|
open.value = !open.value;
|
|
24
231
|
};
|
|
25
232
|
|
|
26
233
|
const handleKeydownSelect = (e: KeyboardEvent) => {
|
|
27
234
|
const key = e.key;
|
|
28
|
-
|
|
29
235
|
if (key === "Tab") return;
|
|
30
|
-
|
|
31
236
|
if (key === "ArrowDown" || key === "Enter" || key === " ") {
|
|
32
237
|
e.preventDefault();
|
|
33
238
|
open.value = !open.value;
|
|
@@ -36,73 +241,76 @@ export const useSelect = <T>(
|
|
|
36
241
|
|
|
37
242
|
const handleKeydownOption = (
|
|
38
243
|
e: KeyboardEvent,
|
|
39
|
-
option: UixySelectOptionType
|
|
244
|
+
option: UixySelectOptionType,
|
|
40
245
|
) => {
|
|
41
246
|
const key = e.key;
|
|
42
|
-
|
|
43
247
|
if ((key === "Enter" || key === " ") && !option.disabled) {
|
|
44
248
|
e.preventDefault();
|
|
45
|
-
return
|
|
249
|
+
return handleClick(getValue(option));
|
|
46
250
|
}
|
|
47
|
-
|
|
48
251
|
if (key !== "ArrowDown" && key !== "ArrowUp") return;
|
|
49
|
-
|
|
50
|
-
const container = refOptions.value;
|
|
51
|
-
|
|
252
|
+
const container = dropdownRef?.value ?? refOptions.value;
|
|
52
253
|
if (!container) return;
|
|
53
|
-
|
|
54
254
|
const focusables = Array.from(
|
|
55
255
|
container.querySelectorAll<HTMLElement>(
|
|
56
|
-
"[select-dropdown] input, [select-dropdown] li"
|
|
57
|
-
)
|
|
256
|
+
"[select-dropdown] input, [select-dropdown] li",
|
|
257
|
+
),
|
|
58
258
|
).filter(
|
|
59
259
|
(el) =>
|
|
60
260
|
el.tabIndex >= 0 &&
|
|
61
261
|
el.getAttribute("aria-disabled") !== "true" &&
|
|
62
|
-
!el.hasAttribute("disabled")
|
|
262
|
+
!el.hasAttribute("disabled"),
|
|
63
263
|
);
|
|
64
|
-
|
|
65
264
|
if (!focusables.length) return;
|
|
66
|
-
|
|
67
265
|
const currentEl = (e.currentTarget as HTMLElement) || null;
|
|
68
266
|
let index = currentEl ? focusables.indexOf(currentEl) : -1;
|
|
69
|
-
|
|
70
267
|
e.preventDefault();
|
|
71
|
-
|
|
72
268
|
const val = key === "ArrowDown" ? 1 : -1;
|
|
73
|
-
|
|
74
269
|
index = (index + val + focusables.length) % focusables.length;
|
|
75
|
-
|
|
76
|
-
const nextEl = focusables[index];
|
|
77
|
-
nextEl?.focus();
|
|
270
|
+
focusables[index]?.focus();
|
|
78
271
|
};
|
|
79
272
|
|
|
80
|
-
const
|
|
81
|
-
|
|
273
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
274
|
+
if (Date.now() - lastScrollAt.value < 250) return;
|
|
275
|
+
const target = e.target as Node;
|
|
276
|
+
if (refOptions.value?.contains(target)) return;
|
|
277
|
+
if (dropdownRef?.value?.contains(target)) return;
|
|
278
|
+
const el = target as HTMLElement;
|
|
279
|
+
if (el.closest?.("[select-dropdown]")) return;
|
|
280
|
+
open.value = false;
|
|
82
281
|
};
|
|
83
282
|
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
open.value = false;
|
|
283
|
+
const markScroll = () => {
|
|
284
|
+
lastScrollAt.value = Date.now();
|
|
87
285
|
};
|
|
88
286
|
|
|
89
287
|
watch(
|
|
90
288
|
() => open.value,
|
|
91
|
-
(
|
|
289
|
+
(isOpen, prevVal) => {
|
|
290
|
+
if (!isOpen) {
|
|
291
|
+
expandedItems.value = [];
|
|
292
|
+
}
|
|
92
293
|
if (prevVal) return;
|
|
93
|
-
|
|
94
294
|
requestAnimationFrame(() => {
|
|
95
295
|
window.addEventListener("click", handleClickOutside);
|
|
296
|
+
window.addEventListener("scroll", markScroll, true);
|
|
297
|
+
window.addEventListener("wheel", markScroll, { passive: true });
|
|
298
|
+
window.addEventListener("touchmove", markScroll, { passive: true });
|
|
96
299
|
});
|
|
97
|
-
|
|
98
300
|
onWatcherCleanup(() => {
|
|
99
301
|
window.removeEventListener("click", handleClickOutside);
|
|
302
|
+
window.removeEventListener("scroll", markScroll, true);
|
|
303
|
+
window.removeEventListener("wheel", markScroll as any);
|
|
304
|
+
window.removeEventListener("touchmove", markScroll as any);
|
|
100
305
|
});
|
|
101
|
-
}
|
|
306
|
+
},
|
|
102
307
|
);
|
|
103
308
|
|
|
104
309
|
onUnmounted(() => {
|
|
105
310
|
window.removeEventListener("click", handleClickOutside);
|
|
311
|
+
window.removeEventListener("scroll", markScroll, true);
|
|
312
|
+
window.removeEventListener("wheel", markScroll as any);
|
|
313
|
+
window.removeEventListener("touchmove", markScroll as any);
|
|
106
314
|
});
|
|
107
315
|
|
|
108
316
|
return {
|
|
@@ -110,9 +318,19 @@ export const useSelect = <T>(
|
|
|
110
318
|
search,
|
|
111
319
|
refOptions,
|
|
112
320
|
filteredOptions,
|
|
321
|
+
expandedItems,
|
|
322
|
+
effectiveExpandedItems,
|
|
323
|
+
customAddVisible,
|
|
324
|
+
isSelected,
|
|
325
|
+
isPartiallySelected,
|
|
326
|
+
getLabel,
|
|
327
|
+
toggleExpand,
|
|
113
328
|
handleOpen,
|
|
114
329
|
handleKeydownSelect,
|
|
115
330
|
handleKeydownOption,
|
|
116
331
|
handleClickSelectedOption,
|
|
332
|
+
handleClick,
|
|
333
|
+
handleDeselect,
|
|
334
|
+
handleCustomAdd,
|
|
117
335
|
};
|
|
118
336
|
};
|
|
@@ -23,7 +23,16 @@
|
|
|
23
23
|
</div>
|
|
24
24
|
<slot name="nav-extra" />
|
|
25
25
|
<div>
|
|
26
|
-
<
|
|
26
|
+
<template v-if="props.keepAlive">
|
|
27
|
+
<div
|
|
28
|
+
v-for="tab in props.tabs"
|
|
29
|
+
:key="tab.value"
|
|
30
|
+
v-show="active === tab.value"
|
|
31
|
+
>
|
|
32
|
+
<slot :name="tab.value" />
|
|
33
|
+
</div>
|
|
34
|
+
</template>
|
|
35
|
+
<animate-presence v-else mode="wait" :initial="false">
|
|
27
36
|
<motion.div
|
|
28
37
|
:key="active"
|
|
29
38
|
:initial="{ opacity: 0, y: 4 }"
|
|
@@ -55,6 +64,6 @@
|
|
|
55
64
|
const layoutId = computed(() => `uixy-tabs-${uuidv4()}`);
|
|
56
65
|
|
|
57
66
|
onBeforeMount(() => {
|
|
58
|
-
if (props.tabs.length > 0) active.value = props.tabs[0]
|
|
67
|
+
if (props.tabs.length > 0) active.value = props.tabs[0]?.value;
|
|
59
68
|
});
|
|
60
69
|
</script>
|
|
@@ -9,17 +9,19 @@
|
|
|
9
9
|
:auto-focus="props.autoFocus"
|
|
10
10
|
:rows="props.rows ?? 3"
|
|
11
11
|
:max-length="props.maxLength"
|
|
12
|
-
:class="
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
:class="
|
|
13
|
+
textareaStyles(
|
|
14
|
+
{ status, noResize: !!props.noResize },
|
|
15
|
+
$attrs.class as string,
|
|
16
|
+
)
|
|
17
|
+
"
|
|
16
18
|
v-bind="filteredAttrs"
|
|
17
19
|
/>
|
|
18
20
|
<label v-if="props.label" :for="id" :class="labelStyles({ status })">
|
|
19
21
|
{{ props.label }}
|
|
20
22
|
</label>
|
|
21
23
|
</div>
|
|
22
|
-
<div
|
|
24
|
+
<div v-if="!props.hideHelper" class="h-4">
|
|
23
25
|
<animate-presence mode="wait" :initial="false">
|
|
24
26
|
<motion.div
|
|
25
27
|
:initial="{ opacity: 0, y: '-4px' }"
|
|
@@ -29,12 +31,7 @@
|
|
|
29
31
|
:key="status"
|
|
30
32
|
>
|
|
31
33
|
<p
|
|
32
|
-
:class="
|
|
33
|
-
helperStyles({
|
|
34
|
-
status,
|
|
35
|
-
align: props.alignText ?? 'left',
|
|
36
|
-
})
|
|
37
|
-
"
|
|
34
|
+
:class="helperStyles({ status, align: props.alignText ?? 'left' })"
|
|
38
35
|
>
|
|
39
36
|
{{ text }}
|
|
40
37
|
</p>
|
|
@@ -68,7 +65,7 @@
|
|
|
68
65
|
const id = useId();
|
|
69
66
|
|
|
70
67
|
const status = computed(() =>
|
|
71
|
-
props.disabled ? "disabled" : props.status ?? "default"
|
|
68
|
+
props.disabled ? "disabled" : (props.status ?? "default"),
|
|
72
69
|
);
|
|
73
70
|
|
|
74
71
|
const text = computed(
|
|
@@ -78,6 +75,6 @@
|
|
|
78
75
|
default: props.helperText,
|
|
79
76
|
disabled: "",
|
|
80
77
|
valid: "",
|
|
81
|
-
}[props.status ?? "default"]
|
|
78
|
+
})[props.status ?? "default"],
|
|
82
79
|
);
|
|
83
80
|
</script>
|
|
@@ -17,7 +17,7 @@ export const textareaStyles = styles(
|
|
|
17
17
|
true: "resize-none",
|
|
18
18
|
false: "",
|
|
19
19
|
},
|
|
20
|
-
}
|
|
20
|
+
},
|
|
21
21
|
);
|
|
22
22
|
|
|
23
23
|
export const labelStyles = styles(
|
|
@@ -29,7 +29,7 @@ export const labelStyles = styles(
|
|
|
29
29
|
disabled: "text-gray-500 dark:text-gray-700",
|
|
30
30
|
valid: "",
|
|
31
31
|
},
|
|
32
|
-
}
|
|
32
|
+
},
|
|
33
33
|
);
|
|
34
34
|
|
|
35
35
|
export const helperStyles = styles(
|
|
@@ -46,5 +46,5 @@ export const helperStyles = styles(
|
|
|
46
46
|
right: "text-right",
|
|
47
47
|
center: "text-center",
|
|
48
48
|
},
|
|
49
|
-
}
|
|
49
|
+
},
|
|
50
50
|
);
|