v-uixy 1.9.2 → 1.9.3
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/components/Select/Select.component.vue +58 -32
- package/templates/components/Select/Select.styles.ts +37 -12
- package/templates/components/Select/Select.types.ts +4 -0
- package/templates/components/Select/SelectTreeItem.vue +11 -3
- package/templates/components/Select/composables/useSelect.composable.ts +244 -122
- package/templates/components/Select/index.ts +2 -0
- package/templates/composables/useOverlayLayer.ts +86 -31
- package/templates/composables/usePosition.ts +1 -1
package/package.json
CHANGED
|
@@ -10,37 +10,49 @@
|
|
|
10
10
|
@keydown="handleKeydownSelect"
|
|
11
11
|
v-bind="filteredAttrs"
|
|
12
12
|
>
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
{{ props.placeholder }}
|
|
18
|
-
</p>
|
|
19
|
-
<div
|
|
20
|
-
v-if="props.multiple"
|
|
21
|
-
:class="badgesWrapperStyles({ disabled: props.disabled })"
|
|
22
|
-
>
|
|
23
|
-
<uixy-badge
|
|
24
|
-
v-for="(item, index) in model as string[]"
|
|
25
|
-
data-badge
|
|
26
|
-
variant="tertiary"
|
|
27
|
-
size="xs"
|
|
28
|
-
shape="pill"
|
|
29
|
-
:key="`${item}--${index}`"
|
|
30
|
-
@click.stop="handleClickSelectedOption(item)"
|
|
13
|
+
<div :class="valueWrapperStyles()">
|
|
14
|
+
<p
|
|
15
|
+
v-if="props.placeholder && (!model || model.length === 0)"
|
|
16
|
+
class="pointer-events-none select-none truncate text-gray-500 dark:text-gray-700"
|
|
31
17
|
>
|
|
32
|
-
{{
|
|
33
|
-
</
|
|
18
|
+
{{ props.placeholder }}
|
|
19
|
+
</p>
|
|
20
|
+
<div
|
|
21
|
+
v-if="props.multiple"
|
|
22
|
+
:class="badgesWrapperStyles({ disabled: props.disabled })"
|
|
23
|
+
>
|
|
24
|
+
<uixy-badge
|
|
25
|
+
v-for="(item, index) in model as string[]"
|
|
26
|
+
data-badge
|
|
27
|
+
variant="tertiary"
|
|
28
|
+
size="xs"
|
|
29
|
+
shape="pill"
|
|
30
|
+
class="max-w-full"
|
|
31
|
+
:key="`${item}--${index}`"
|
|
32
|
+
@click.stop="handleClickSelectedOption(item)"
|
|
33
|
+
>
|
|
34
|
+
<span class="min-w-0 truncate">{{ getLabel(item) }}</span>
|
|
35
|
+
</uixy-badge>
|
|
36
|
+
</div>
|
|
37
|
+
<p v-else class="truncate">{{ getLabel(model as string) }}</p>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div :class="trailingStyles()">
|
|
41
|
+
<span
|
|
42
|
+
v-if="hasMax"
|
|
43
|
+
:class="counterStyles({ full: isAtMax, size })"
|
|
44
|
+
aria-live="polite"
|
|
45
|
+
>
|
|
46
|
+
{{ selectedValues.length }}/{{ props.max }}
|
|
47
|
+
</span>
|
|
48
|
+
<uixy-icon
|
|
49
|
+
v-if="!props.multiple && model && minSelected === 0"
|
|
50
|
+
name="close"
|
|
51
|
+
:class="deselectIconStyles({ size })"
|
|
52
|
+
@click.stop="handleDeselect"
|
|
53
|
+
/>
|
|
54
|
+
<uixy-icon name="chevron-down" :class="iconStyles({ open, size })" />
|
|
34
55
|
</div>
|
|
35
|
-
<p v-else>{{ getLabel(model as string) }}</p>
|
|
36
|
-
|
|
37
|
-
<uixy-icon
|
|
38
|
-
v-if="props.deselectable && !props.multiple && model"
|
|
39
|
-
name="close"
|
|
40
|
-
:class="deselectIconStyles({ size })"
|
|
41
|
-
@click.stop="handleDeselect"
|
|
42
|
-
/>
|
|
43
|
-
<uixy-icon name="chevron-down" :class="iconStyles({ open, size })" />
|
|
44
56
|
</div>
|
|
45
57
|
|
|
46
58
|
<teleport to="body">
|
|
@@ -78,6 +90,7 @@
|
|
|
78
90
|
:depth="0"
|
|
79
91
|
:is-selected="isSelected"
|
|
80
92
|
:is-partially-selected="isPartiallySelected"
|
|
93
|
+
:is-blocked="isBlocked"
|
|
81
94
|
:expanded-items="effectiveExpandedItems"
|
|
82
95
|
:leaf-only="props.leafOnly"
|
|
83
96
|
:multiple="props.multiple"
|
|
@@ -105,12 +118,14 @@
|
|
|
105
118
|
<ul v-else class="flex flex-col gap-1 py-1">
|
|
106
119
|
<li
|
|
107
120
|
v-for="option in filteredOptions"
|
|
108
|
-
:tabIndex="option.disabled ? -1 : 0"
|
|
121
|
+
:tabIndex="option.disabled || isBlocked(option) ? -1 : 0"
|
|
122
|
+
:aria-disabled="isBlocked(option) || undefined"
|
|
109
123
|
:key="option.value ?? option.label"
|
|
110
124
|
:class="
|
|
111
125
|
itemStyles({
|
|
112
126
|
selected: isSelected(option),
|
|
113
127
|
disabled: !!option.disabled,
|
|
128
|
+
blocked: isBlocked(option),
|
|
114
129
|
size,
|
|
115
130
|
})
|
|
116
131
|
"
|
|
@@ -197,6 +212,9 @@
|
|
|
197
212
|
dropdownPanelStyles,
|
|
198
213
|
listItemPaddingStyles,
|
|
199
214
|
checkIconStyles,
|
|
215
|
+
valueWrapperStyles,
|
|
216
|
+
trailingStyles,
|
|
217
|
+
counterStyles,
|
|
200
218
|
} from "./Select.styles";
|
|
201
219
|
import type { UixySelectEmits, UixySelectProps } from "./Select.types";
|
|
202
220
|
import { UixyBadge } from "../Badge";
|
|
@@ -207,7 +225,10 @@
|
|
|
207
225
|
import { useOverlayLayer } from "~/composables";
|
|
208
226
|
import SelectTreeItem from "./SelectTreeItem.vue";
|
|
209
227
|
|
|
210
|
-
const props = defineProps<UixySelectProps>()
|
|
228
|
+
const props = withDefaults(defineProps<UixySelectProps>(), {
|
|
229
|
+
deselectable: true,
|
|
230
|
+
maxBehavior: "block",
|
|
231
|
+
});
|
|
211
232
|
|
|
212
233
|
const emits = defineEmits<UixySelectEmits>();
|
|
213
234
|
|
|
@@ -249,8 +270,13 @@
|
|
|
249
270
|
filteredOptions,
|
|
250
271
|
effectiveExpandedItems,
|
|
251
272
|
customAddVisible,
|
|
273
|
+
selectedValues,
|
|
274
|
+
hasMax,
|
|
275
|
+
isAtMax,
|
|
276
|
+
minSelected,
|
|
252
277
|
isSelected,
|
|
253
278
|
isPartiallySelected,
|
|
279
|
+
isBlocked,
|
|
254
280
|
getLabel,
|
|
255
281
|
toggleExpand,
|
|
256
282
|
handleOpen,
|
|
@@ -340,7 +366,7 @@
|
|
|
340
366
|
|
|
341
367
|
if (!isOpen) return;
|
|
342
368
|
|
|
343
|
-
acquireLayer();
|
|
369
|
+
acquireLayer(refTrigger.value);
|
|
344
370
|
|
|
345
371
|
void updateDropdownPosition();
|
|
346
372
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import styles from "~/utils/styles";
|
|
2
2
|
|
|
3
3
|
export const selectStyles = styles(
|
|
4
|
-
"relative select-none peer transition-colors ease-in-out duration-150 border border-gray-300 dark:border-gray-900 rounded-1 placeholder:text-gray-500 dark:placeholder:text-gray-700 dark:outline-gray-800 outline-offset-1 outline-gray-300",
|
|
4
|
+
"relative flex items-center gap-2 select-none peer transition-colors ease-in-out duration-150 border border-gray-300 dark:border-gray-900 rounded-1 placeholder:text-gray-500 dark:placeholder:text-gray-700 dark:outline-gray-800 outline-offset-1 outline-gray-300",
|
|
5
5
|
{
|
|
6
6
|
status: {
|
|
7
7
|
default:
|
|
@@ -14,35 +14,56 @@ export const selectStyles = styles(
|
|
|
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
|
size: {
|
|
17
|
-
sm: "pl-2.5 pr-5 py-1.5 text-xs min-h-[32px]",
|
|
18
|
-
md: "pl-3 pr-
|
|
19
|
-
xl: "pl-3.5 pr-
|
|
17
|
+
sm: "pl-2.5 pr-1.5 py-1.5 text-xs min-h-[32px]",
|
|
18
|
+
md: "pl-3 pr-2 py-2 text-sm min-h-[38px]",
|
|
19
|
+
xl: "pl-3.5 pr-2.5 py-2.5 text-base min-h-[42px]",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export const valueWrapperStyles = styles("min-w-0 flex-1");
|
|
25
|
+
|
|
26
|
+
export const trailingStyles = styles(
|
|
27
|
+
"ml-auto flex shrink-0 items-center gap-1.5",
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
export const counterStyles = styles(
|
|
31
|
+
"pointer-events-none shrink-0 select-none tabular-nums transition-colors duration-150 ease-in-out",
|
|
32
|
+
{
|
|
33
|
+
full: {
|
|
34
|
+
true: "text-gray-900 dark:text-gray-300",
|
|
35
|
+
false: "text-gray-500 dark:text-gray-600",
|
|
36
|
+
},
|
|
37
|
+
size: {
|
|
38
|
+
sm: "text-[10px]",
|
|
39
|
+
md: "text-xs",
|
|
40
|
+
xl: "text-sm",
|
|
20
41
|
},
|
|
21
42
|
},
|
|
22
43
|
);
|
|
23
44
|
|
|
24
45
|
export const iconStyles = styles(
|
|
25
|
-
"pointer-events-none
|
|
46
|
+
"pointer-events-none shrink-0 select-none transition-transform duration-200 ease-in-out",
|
|
26
47
|
{
|
|
27
48
|
open: {
|
|
28
49
|
true: "rotate-180",
|
|
29
50
|
false: "",
|
|
30
51
|
},
|
|
31
52
|
size: {
|
|
32
|
-
sm: "h-3.5 w-3.5
|
|
33
|
-
md: "h-4 w-4
|
|
34
|
-
xl: "h-4 w-4
|
|
53
|
+
sm: "h-3.5 w-3.5",
|
|
54
|
+
md: "h-4 w-4",
|
|
55
|
+
xl: "h-4 w-4",
|
|
35
56
|
},
|
|
36
57
|
},
|
|
37
58
|
);
|
|
38
59
|
|
|
39
60
|
export const deselectIconStyles = styles(
|
|
40
|
-
"
|
|
61
|
+
"shrink-0 cursor-pointer text-gray-500 dark:text-gray-600 hover:text-black dark:hover:text-gray-400",
|
|
41
62
|
{
|
|
42
63
|
size: {
|
|
43
|
-
sm: "h-3.5 w-3.5
|
|
44
|
-
md: "h-4 w-4
|
|
45
|
-
xl: "h-4 w-4
|
|
64
|
+
sm: "h-3.5 w-3.5",
|
|
65
|
+
md: "h-4 w-4",
|
|
66
|
+
xl: "h-4 w-4",
|
|
46
67
|
},
|
|
47
68
|
},
|
|
48
69
|
);
|
|
@@ -71,6 +92,10 @@ export const itemStyles = styles(
|
|
|
71
92
|
true: "pointer-events-none text-gray-500 dark:text-gray-700",
|
|
72
93
|
false: "",
|
|
73
94
|
},
|
|
95
|
+
blocked: {
|
|
96
|
+
true: "cursor-not-allowed text-gray-400 dark:text-gray-700 hover:bg-transparent dark:hover:bg-transparent focus-visible:bg-transparent dark:focus-visible:bg-transparent",
|
|
97
|
+
false: "",
|
|
98
|
+
},
|
|
74
99
|
size: {
|
|
75
100
|
sm: "py-1.5 px-2.5",
|
|
76
101
|
md: "py-2 px-3",
|
|
@@ -20,6 +20,8 @@ export interface UixySelectEmits {
|
|
|
20
20
|
(event: "openChange", isOpen: boolean): void;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
export type UixySelectMaxBehavior = "block" | "replace";
|
|
24
|
+
|
|
23
25
|
export type UixySelectProps = {
|
|
24
26
|
options: UixySelectOptionType[];
|
|
25
27
|
status: "valid" | "error" | "default";
|
|
@@ -31,7 +33,9 @@ export type UixySelectProps = {
|
|
|
31
33
|
helperText?: string;
|
|
32
34
|
errorText?: string;
|
|
33
35
|
autoFocus?: boolean;
|
|
36
|
+
min?: number;
|
|
34
37
|
max?: number;
|
|
38
|
+
maxBehavior?: UixySelectMaxBehavior;
|
|
35
39
|
tree?: boolean;
|
|
36
40
|
leafOnly?: boolean;
|
|
37
41
|
autoOpen?: boolean;
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<li class="flex flex-col">
|
|
3
3
|
<div
|
|
4
|
-
:tabIndex="option.disabled ? -1 : 0"
|
|
4
|
+
:tabIndex="option.disabled || isOptionBlocked ? -1 : 0"
|
|
5
|
+
:aria-disabled="isOptionBlocked || undefined"
|
|
5
6
|
:class="
|
|
6
7
|
itemStyles({
|
|
7
8
|
selected: isSelected(option),
|
|
8
9
|
disabled: !!option.disabled,
|
|
10
|
+
blocked: isOptionBlocked,
|
|
9
11
|
size: props.size ?? 'md',
|
|
10
12
|
})
|
|
11
13
|
"
|
|
12
14
|
:style="{
|
|
13
15
|
paddingLeft: `${depth * 28 + 12}px`,
|
|
14
|
-
cursor: option.disabled ? 'not-allowed' : 'pointer',
|
|
16
|
+
cursor: option.disabled || isOptionBlocked ? 'not-allowed' : 'pointer',
|
|
15
17
|
}"
|
|
16
18
|
@click.stop="handleItemClick"
|
|
17
19
|
>
|
|
@@ -65,6 +67,7 @@
|
|
|
65
67
|
:depth="depth + 1"
|
|
66
68
|
:is-selected="isSelected"
|
|
67
69
|
:is-partially-selected="isPartiallySelected"
|
|
70
|
+
:is-blocked="isBlocked"
|
|
68
71
|
:expanded-items="expandedItems"
|
|
69
72
|
:leaf-only="leafOnly"
|
|
70
73
|
:multiple="multiple"
|
|
@@ -91,6 +94,7 @@
|
|
|
91
94
|
depth: number;
|
|
92
95
|
isSelected: (option: UixySelectOptionType) => boolean;
|
|
93
96
|
isPartiallySelected?: (option: UixySelectOptionType) => boolean;
|
|
97
|
+
isBlocked?: (option: UixySelectOptionType) => boolean;
|
|
94
98
|
expandedItems: string[];
|
|
95
99
|
leafOnly?: boolean;
|
|
96
100
|
multiple?: boolean;
|
|
@@ -139,6 +143,10 @@
|
|
|
139
143
|
props.expandedItems.includes(getValue(props.option)),
|
|
140
144
|
);
|
|
141
145
|
|
|
146
|
+
const isOptionBlocked = computed(
|
|
147
|
+
() => !!props.isBlocked?.(props.option),
|
|
148
|
+
);
|
|
149
|
+
|
|
142
150
|
const isNotSelectable = computed(
|
|
143
151
|
() =>
|
|
144
152
|
props.option.disabled ||
|
|
@@ -146,7 +154,7 @@
|
|
|
146
154
|
);
|
|
147
155
|
|
|
148
156
|
const handleItemClick = () => {
|
|
149
|
-
if (props.option.disabled) return;
|
|
157
|
+
if (props.option.disabled || isOptionBlocked.value) return;
|
|
150
158
|
|
|
151
159
|
if (props.leafOnly && hasChildrenInOriginal.value && !props.multiple) {
|
|
152
160
|
emit("toggle-expand", getValue(props.option));
|
|
@@ -15,8 +15,8 @@ const getValue = (option: UixySelectOptionType): string =>
|
|
|
15
15
|
option.value ?? option.label;
|
|
16
16
|
|
|
17
17
|
const collectLeafValues = (option: UixySelectOptionType): string[] => {
|
|
18
|
-
if (!option.children
|
|
19
|
-
|
|
18
|
+
if (!option.children?.length) return [getValue(option)];
|
|
19
|
+
|
|
20
20
|
return option.children.flatMap(collectLeafValues);
|
|
21
21
|
};
|
|
22
22
|
|
|
@@ -24,16 +24,23 @@ const findInTree = (
|
|
|
24
24
|
options: UixySelectOptionType[],
|
|
25
25
|
value: string,
|
|
26
26
|
): UixySelectOptionType | null => {
|
|
27
|
-
for (const
|
|
28
|
-
if (getValue(
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
for (const option of options) {
|
|
28
|
+
if (getValue(option) === value) return option;
|
|
29
|
+
|
|
30
|
+
if (option.children?.length) {
|
|
31
|
+
const found = findInTree(option.children, value);
|
|
32
|
+
|
|
31
33
|
if (found) return found;
|
|
32
34
|
}
|
|
33
35
|
}
|
|
36
|
+
|
|
34
37
|
return null;
|
|
35
38
|
};
|
|
36
39
|
|
|
40
|
+
const isNotNull = (
|
|
41
|
+
option: UixySelectOptionType | null,
|
|
42
|
+
): option is UixySelectOptionType => option !== null;
|
|
43
|
+
|
|
37
44
|
export const useSelect = <T extends string | string[]>(
|
|
38
45
|
props: UixySelectProps,
|
|
39
46
|
model: Ref<T>,
|
|
@@ -46,39 +53,43 @@ export const useSelect = <T extends string | string[]>(
|
|
|
46
53
|
const lastScrollAt = ref(0);
|
|
47
54
|
const expandedItems = ref<string[]>([]);
|
|
48
55
|
|
|
56
|
+
const selectedValues = computed<string[]>(() =>
|
|
57
|
+
Array.isArray(model.value) ? (model.value as string[]) : [],
|
|
58
|
+
);
|
|
59
|
+
|
|
49
60
|
const filterTreeOptions = (
|
|
50
61
|
items: UixySelectOptionType[],
|
|
51
62
|
searchTerm: string,
|
|
52
63
|
): UixySelectOptionType[] => {
|
|
53
64
|
if (!searchTerm) return items;
|
|
54
|
-
|
|
65
|
+
|
|
66
|
+
const normalizedSearch = normalizeForSearch(searchTerm);
|
|
67
|
+
|
|
55
68
|
const filterNode = (
|
|
56
69
|
node: UixySelectOptionType,
|
|
57
70
|
): UixySelectOptionType | null => {
|
|
58
|
-
const matches = normalizeForSearch(node.label).includes(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return { ...node, children: filteredChildren };
|
|
67
|
-
}
|
|
68
|
-
return null;
|
|
71
|
+
const matches = normalizeForSearch(node.label).includes(normalizedSearch);
|
|
72
|
+
const filteredChildren = (node.children ?? [])
|
|
73
|
+
.map(filterNode)
|
|
74
|
+
.filter(isNotNull);
|
|
75
|
+
|
|
76
|
+
if (!matches && filteredChildren.length === 0) return null;
|
|
77
|
+
|
|
78
|
+
return { ...node, children: filteredChildren };
|
|
69
79
|
};
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
.filter((n): n is UixySelectOptionType => n !== null);
|
|
80
|
+
|
|
81
|
+
return items.map(filterNode).filter(isNotNull);
|
|
73
82
|
};
|
|
74
83
|
|
|
75
84
|
const filteredOptions = computed(() => {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
return
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
const options = props.options ?? [];
|
|
86
|
+
|
|
87
|
+
if (props.tree) return filterTreeOptions(options, search.value);
|
|
88
|
+
|
|
89
|
+
const normalizedSearch = normalizeForSearch(search.value);
|
|
90
|
+
|
|
91
|
+
return options.filter((option) =>
|
|
92
|
+
normalizeForSearch(option.label).includes(normalizedSearch),
|
|
82
93
|
);
|
|
83
94
|
});
|
|
84
95
|
|
|
@@ -87,133 +98,221 @@ export const useSelect = <T extends string | string[]>(
|
|
|
87
98
|
searchTerm: string,
|
|
88
99
|
): string[] => {
|
|
89
100
|
if (!searchTerm) return [];
|
|
101
|
+
|
|
90
102
|
const lowerSearch = searchTerm.toLowerCase();
|
|
91
103
|
const parentsToExpand: string[] = [];
|
|
104
|
+
|
|
92
105
|
const checkNode = (node: UixySelectOptionType): boolean => {
|
|
93
106
|
const nodeMatches = node.label.toLowerCase().includes(lowerSearch);
|
|
94
107
|
let hasMatchingDescendant = false;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
108
|
+
|
|
109
|
+
for (const child of node.children ?? []) {
|
|
110
|
+
if (checkNode(child)) hasMatchingDescendant = true;
|
|
99
111
|
}
|
|
112
|
+
|
|
100
113
|
if (hasMatchingDescendant) parentsToExpand.push(getValue(node));
|
|
114
|
+
|
|
101
115
|
return nodeMatches || hasMatchingDescendant;
|
|
102
116
|
};
|
|
117
|
+
|
|
103
118
|
for (const item of items) checkNode(item);
|
|
119
|
+
|
|
104
120
|
return parentsToExpand;
|
|
105
121
|
};
|
|
106
122
|
|
|
107
123
|
const effectiveExpandedItems = computed(() => {
|
|
108
124
|
if (!search.value) return expandedItems.value;
|
|
125
|
+
|
|
109
126
|
const autoExpanded = collectParentsWithMatchingDescendants(
|
|
110
127
|
props.options,
|
|
111
128
|
search.value,
|
|
112
129
|
);
|
|
130
|
+
|
|
113
131
|
return Array.from(new Set([...expandedItems.value, ...autoExpanded]));
|
|
114
132
|
});
|
|
115
133
|
|
|
116
134
|
const toggleExpand = (value: string) => {
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
135
|
+
const index = expandedItems.value.indexOf(value);
|
|
136
|
+
|
|
137
|
+
if (index >= 0) {
|
|
138
|
+
expandedItems.value.splice(index, 1);
|
|
139
|
+
} else {
|
|
140
|
+
expandedItems.value.push(value);
|
|
141
|
+
}
|
|
120
142
|
};
|
|
121
143
|
|
|
122
144
|
const isSelected = (option: UixySelectOptionType): boolean => {
|
|
123
|
-
const v = getValue(option);
|
|
124
145
|
if (props.tree && props.multiple && option.children?.length) {
|
|
125
146
|
const leaves = collectLeafValues(option);
|
|
126
|
-
|
|
127
|
-
return
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
leaves.length > 0 &&
|
|
150
|
+
leaves.every((leaf) => selectedValues.value.includes(leaf))
|
|
151
|
+
);
|
|
128
152
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
153
|
+
|
|
154
|
+
const value = getValue(option);
|
|
155
|
+
|
|
156
|
+
if (Array.isArray(model.value)) return selectedValues.value.includes(value);
|
|
157
|
+
|
|
158
|
+
return (model.value as string) === value;
|
|
132
159
|
};
|
|
133
160
|
|
|
134
161
|
const isPartiallySelected = (option: UixySelectOptionType): boolean => {
|
|
135
|
-
if (!props.tree || !props.multiple || !option.children?.length)
|
|
162
|
+
if (!props.tree || !props.multiple || !option.children?.length) {
|
|
136
163
|
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
137
166
|
const leaves = collectLeafValues(option);
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
167
|
+
const selectedCount = leaves.filter((leaf) =>
|
|
168
|
+
selectedValues.value.includes(leaf),
|
|
169
|
+
).length;
|
|
170
|
+
|
|
171
|
+
return selectedCount > 0 && selectedCount < leaves.length;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const minSelected = computed(() => {
|
|
175
|
+
if (!props.multiple) return props.deselectable ? 0 : 1;
|
|
176
|
+
|
|
177
|
+
const min = Math.max(props.min ?? 0, 0);
|
|
178
|
+
|
|
179
|
+
return props.deselectable ? min : Math.max(min, 1);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const hasMax = computed(
|
|
183
|
+
() => !!props.multiple && typeof props.max === "number",
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const isAtMax = computed(
|
|
187
|
+
() => hasMax.value && selectedValues.value.length >= props.max!,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const canReplace = computed(
|
|
191
|
+
() => props.maxBehavior === "replace" && props.max !== 0,
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const isBlocked = (option: UixySelectOptionType): boolean => {
|
|
195
|
+
if (!hasMax.value || canReplace.value || isSelected(option)) return false;
|
|
196
|
+
|
|
197
|
+
const prev = selectedValues.value;
|
|
198
|
+
const values =
|
|
199
|
+
props.tree && option.children?.length
|
|
200
|
+
? collectLeafValues(option)
|
|
201
|
+
: [getValue(option)];
|
|
202
|
+
const added = values.filter((v) => !prev.includes(v)).length;
|
|
203
|
+
|
|
204
|
+
return prev.length + added > props.max!;
|
|
141
205
|
};
|
|
142
206
|
|
|
143
207
|
const setMultiValues = (next: string[]) => {
|
|
144
208
|
(model.value as string[]) = next;
|
|
145
209
|
};
|
|
146
210
|
|
|
147
|
-
const toggleMultiValue = (v: string) => {
|
|
148
|
-
const prev =
|
|
211
|
+
const toggleMultiValue = (v: string): boolean => {
|
|
212
|
+
const prev = selectedValues.value;
|
|
213
|
+
|
|
149
214
|
if (prev.includes(v)) {
|
|
215
|
+
if (prev.length <= minSelected.value) return false;
|
|
216
|
+
|
|
150
217
|
setMultiValues(prev.filter((item) => item !== v));
|
|
151
|
-
|
|
218
|
+
|
|
219
|
+
return true;
|
|
152
220
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return;
|
|
221
|
+
|
|
222
|
+
if (hasMax.value && prev.length >= props.max!) {
|
|
223
|
+
if (!canReplace.value) return false;
|
|
224
|
+
|
|
225
|
+
setMultiValues([...prev.slice(prev.length - props.max! + 1), v]);
|
|
226
|
+
|
|
227
|
+
return true;
|
|
156
228
|
}
|
|
229
|
+
|
|
157
230
|
setMultiValues([...prev, v]);
|
|
231
|
+
|
|
232
|
+
return true;
|
|
158
233
|
};
|
|
159
234
|
|
|
160
|
-
const cascadeMultiTree = (option: UixySelectOptionType) => {
|
|
235
|
+
const cascadeMultiTree = (option: UixySelectOptionType): boolean => {
|
|
161
236
|
const leaves = collectLeafValues(option);
|
|
162
|
-
const prev =
|
|
163
|
-
const allSelected = leaves.every((
|
|
237
|
+
const prev = selectedValues.value;
|
|
238
|
+
const allSelected = leaves.every((leaf) => prev.includes(leaf));
|
|
239
|
+
|
|
164
240
|
if (allSelected) {
|
|
165
|
-
|
|
166
|
-
|
|
241
|
+
const next = prev.filter((v) => !leaves.includes(v));
|
|
242
|
+
|
|
243
|
+
if (next.length < minSelected.value) return false;
|
|
244
|
+
|
|
245
|
+
setMultiValues(next);
|
|
246
|
+
|
|
247
|
+
return true;
|
|
167
248
|
}
|
|
249
|
+
|
|
168
250
|
const merged = Array.from(new Set([...prev, ...leaves]));
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return;
|
|
251
|
+
|
|
252
|
+
if (hasMax.value && merged.length > props.max!) {
|
|
253
|
+
if (!canReplace.value) return false;
|
|
254
|
+
|
|
255
|
+
setMultiValues(merged.slice(merged.length - props.max!));
|
|
256
|
+
|
|
257
|
+
return true;
|
|
172
258
|
}
|
|
259
|
+
|
|
173
260
|
setMultiValues(merged);
|
|
261
|
+
|
|
262
|
+
return true;
|
|
174
263
|
};
|
|
175
264
|
|
|
176
|
-
const applyChange = (v: string) => {
|
|
265
|
+
const applyChange = (v: string): boolean => {
|
|
177
266
|
if (!props.multiple) {
|
|
178
267
|
(model.value as string) = v;
|
|
179
|
-
|
|
268
|
+
|
|
269
|
+
return true;
|
|
180
270
|
}
|
|
271
|
+
|
|
181
272
|
if (props.tree) {
|
|
182
273
|
const node = findInTree(props.options, v);
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
274
|
+
|
|
275
|
+
if (node?.children?.length) return cascadeMultiTree(node);
|
|
187
276
|
}
|
|
188
|
-
|
|
277
|
+
|
|
278
|
+
return toggleMultiValue(v);
|
|
189
279
|
};
|
|
190
280
|
|
|
191
281
|
const handleClick = (value: string) => {
|
|
192
|
-
applyChange(value);
|
|
282
|
+
if (!applyChange(value)) return;
|
|
283
|
+
|
|
193
284
|
emit("change", value);
|
|
285
|
+
|
|
194
286
|
if (props.clearSearchOnSelect) search.value = "";
|
|
195
287
|
if (!props.multiple) open.value = false;
|
|
196
288
|
};
|
|
197
289
|
|
|
198
290
|
const handleClickSelectedOption = (item: string) => {
|
|
199
|
-
applyChange(item);
|
|
291
|
+
if (!applyChange(item)) return;
|
|
292
|
+
|
|
200
293
|
emit("change", item);
|
|
201
294
|
};
|
|
202
295
|
|
|
203
296
|
const handleDeselect = () => {
|
|
297
|
+
if (minSelected.value > 0) return;
|
|
298
|
+
|
|
204
299
|
(model.value as string) = "";
|
|
205
300
|
};
|
|
206
301
|
|
|
207
302
|
const getLabel = (value: string): string => {
|
|
208
303
|
if (!value) return "";
|
|
304
|
+
|
|
209
305
|
return findInTree(props.options, value)?.label ?? value;
|
|
210
306
|
};
|
|
211
307
|
|
|
212
308
|
const customAddVisible = computed(() => {
|
|
213
309
|
if (!props.allowCustom || !search.value) return false;
|
|
310
|
+
if (isAtMax.value && !canReplace.value) return false;
|
|
311
|
+
|
|
214
312
|
const lowerSearch = search.value.toLowerCase();
|
|
313
|
+
|
|
215
314
|
return !filteredOptions.value.some(
|
|
216
|
-
(
|
|
315
|
+
(option) => option.label.toLowerCase() === lowerSearch,
|
|
217
316
|
);
|
|
218
317
|
});
|
|
219
318
|
|
|
@@ -222,61 +321,79 @@ export const useSelect = <T extends string | string[]>(
|
|
|
222
321
|
};
|
|
223
322
|
|
|
224
323
|
const handleOpen = (e?: MouseEvent) => {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
)
|
|
229
|
-
|
|
324
|
+
const isBadgeClick =
|
|
325
|
+
!!e && "badge" in (e.target as HTMLDivElement).dataset;
|
|
326
|
+
|
|
327
|
+
if (props.disabled || isBadgeClick) return;
|
|
328
|
+
|
|
230
329
|
open.value = !open.value;
|
|
231
330
|
};
|
|
232
331
|
|
|
233
332
|
const handleKeydownSelect = (e: KeyboardEvent) => {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if (key === "ArrowDown" || key === "Enter" || key === " ") {
|
|
333
|
+
if (e.key === "Tab") return;
|
|
334
|
+
|
|
335
|
+
if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") {
|
|
237
336
|
e.preventDefault();
|
|
337
|
+
|
|
238
338
|
open.value = !open.value;
|
|
239
339
|
}
|
|
240
340
|
};
|
|
241
341
|
|
|
342
|
+
const getFocusableOptions = (): HTMLElement[] => {
|
|
343
|
+
const container = dropdownRef?.value ?? refOptions.value;
|
|
344
|
+
|
|
345
|
+
if (!container) return [];
|
|
346
|
+
|
|
347
|
+
const elements = container.querySelectorAll<HTMLElement>(
|
|
348
|
+
"[select-dropdown] input, [select-dropdown] li",
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
return Array.from(elements).filter(
|
|
352
|
+
(el) =>
|
|
353
|
+
el.tabIndex >= 0 &&
|
|
354
|
+
el.getAttribute("aria-disabled") !== "true" &&
|
|
355
|
+
!el.hasAttribute("disabled"),
|
|
356
|
+
);
|
|
357
|
+
};
|
|
358
|
+
|
|
242
359
|
const handleKeydownOption = (
|
|
243
360
|
e: KeyboardEvent,
|
|
244
361
|
option: UixySelectOptionType,
|
|
245
362
|
) => {
|
|
246
|
-
const
|
|
247
|
-
|
|
363
|
+
const isSelectKey = e.key === "Enter" || e.key === " ";
|
|
364
|
+
|
|
365
|
+
if (isSelectKey && !option.disabled) {
|
|
248
366
|
e.preventDefault();
|
|
367
|
+
|
|
249
368
|
return handleClick(getValue(option));
|
|
250
369
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const focusables =
|
|
255
|
-
|
|
256
|
-
"[select-dropdown] input, [select-dropdown] li",
|
|
257
|
-
),
|
|
258
|
-
).filter(
|
|
259
|
-
(el) =>
|
|
260
|
-
el.tabIndex >= 0 &&
|
|
261
|
-
el.getAttribute("aria-disabled") !== "true" &&
|
|
262
|
-
!el.hasAttribute("disabled"),
|
|
263
|
-
);
|
|
370
|
+
|
|
371
|
+
if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return;
|
|
372
|
+
|
|
373
|
+
const focusables = getFocusableOptions();
|
|
374
|
+
|
|
264
375
|
if (!focusables.length) return;
|
|
265
|
-
|
|
266
|
-
let index = currentEl ? focusables.indexOf(currentEl) : -1;
|
|
376
|
+
|
|
267
377
|
e.preventDefault();
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
focusables
|
|
378
|
+
|
|
379
|
+
const current = e.currentTarget as HTMLElement | null;
|
|
380
|
+
const currentIndex = current ? focusables.indexOf(current) : -1;
|
|
381
|
+
const step = e.key === "ArrowDown" ? 1 : -1;
|
|
382
|
+
const nextIndex =
|
|
383
|
+
(currentIndex + step + focusables.length) % focusables.length;
|
|
384
|
+
|
|
385
|
+
focusables[nextIndex]?.focus();
|
|
271
386
|
};
|
|
272
387
|
|
|
273
388
|
const handleClickOutside = (e: MouseEvent) => {
|
|
274
389
|
if (Date.now() - lastScrollAt.value < 250) return;
|
|
275
|
-
|
|
390
|
+
|
|
391
|
+
const target = e.target as HTMLElement;
|
|
392
|
+
|
|
276
393
|
if (refOptions.value?.contains(target)) return;
|
|
277
394
|
if (dropdownRef?.value?.contains(target)) return;
|
|
278
|
-
|
|
279
|
-
|
|
395
|
+
if (target.closest?.("[select-dropdown]")) return;
|
|
396
|
+
|
|
280
397
|
open.value = false;
|
|
281
398
|
};
|
|
282
399
|
|
|
@@ -284,34 +401,34 @@ export const useSelect = <T extends string | string[]>(
|
|
|
284
401
|
lastScrollAt.value = Date.now();
|
|
285
402
|
};
|
|
286
403
|
|
|
404
|
+
const addWindowListeners = () => {
|
|
405
|
+
window.addEventListener("click", handleClickOutside);
|
|
406
|
+
window.addEventListener("scroll", markScroll, true);
|
|
407
|
+
window.addEventListener("wheel", markScroll, { passive: true });
|
|
408
|
+
window.addEventListener("touchmove", markScroll, { passive: true });
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const removeWindowListeners = () => {
|
|
412
|
+
window.removeEventListener("click", handleClickOutside);
|
|
413
|
+
window.removeEventListener("scroll", markScroll, true);
|
|
414
|
+
window.removeEventListener("wheel", markScroll);
|
|
415
|
+
window.removeEventListener("touchmove", markScroll);
|
|
416
|
+
};
|
|
417
|
+
|
|
287
418
|
watch(
|
|
288
419
|
() => open.value,
|
|
289
|
-
(isOpen,
|
|
290
|
-
if (!isOpen)
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
requestAnimationFrame(
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
window.addEventListener("wheel", markScroll, { passive: true });
|
|
298
|
-
window.addEventListener("touchmove", markScroll, { passive: true });
|
|
299
|
-
});
|
|
300
|
-
onWatcherCleanup(() => {
|
|
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);
|
|
305
|
-
});
|
|
420
|
+
(isOpen, wasOpen) => {
|
|
421
|
+
if (!isOpen) expandedItems.value = [];
|
|
422
|
+
|
|
423
|
+
if (wasOpen) return;
|
|
424
|
+
|
|
425
|
+
requestAnimationFrame(addWindowListeners);
|
|
426
|
+
|
|
427
|
+
onWatcherCleanup(removeWindowListeners);
|
|
306
428
|
},
|
|
307
429
|
);
|
|
308
430
|
|
|
309
|
-
onUnmounted(
|
|
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);
|
|
314
|
-
});
|
|
431
|
+
onUnmounted(removeWindowListeners);
|
|
315
432
|
|
|
316
433
|
return {
|
|
317
434
|
open,
|
|
@@ -321,8 +438,13 @@ export const useSelect = <T extends string | string[]>(
|
|
|
321
438
|
expandedItems,
|
|
322
439
|
effectiveExpandedItems,
|
|
323
440
|
customAddVisible,
|
|
441
|
+
selectedValues,
|
|
442
|
+
hasMax,
|
|
443
|
+
isAtMax,
|
|
444
|
+
minSelected,
|
|
324
445
|
isSelected,
|
|
325
446
|
isPartiallySelected,
|
|
447
|
+
isBlocked,
|
|
326
448
|
getLabel,
|
|
327
449
|
toggleExpand,
|
|
328
450
|
handleOpen,
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type UixySelectOptionType,
|
|
5
5
|
type UixySingleSelectProps,
|
|
6
6
|
type UixyMultipleSelectProps,
|
|
7
|
+
type UixySelectMaxBehavior,
|
|
7
8
|
} from "./Select.types";
|
|
8
9
|
|
|
9
10
|
export {
|
|
@@ -12,4 +13,5 @@ export {
|
|
|
12
13
|
type UixySelectOptionType,
|
|
13
14
|
type UixySingleSelectProps,
|
|
14
15
|
type UixyMultipleSelectProps,
|
|
16
|
+
type UixySelectMaxBehavior,
|
|
15
17
|
};
|
|
@@ -1,73 +1,123 @@
|
|
|
1
1
|
import { computed, nextTick, onScopeDispose, ref, type ComputedRef } from "vue";
|
|
2
2
|
|
|
3
|
+
const PAGE_ANCHORED_Z = 30;
|
|
3
4
|
const BASE_Z = 60;
|
|
4
5
|
const STEP_Z = 10;
|
|
5
6
|
|
|
7
|
+
const MANAGED_ATTR = "data-uixy-inert-managed";
|
|
8
|
+
const OVERLAY_ATTR = "data-uixy-overlay";
|
|
9
|
+
export const MODAL_ATTR = "data-uixy-modal";
|
|
10
|
+
export const MODAL_ID_ATTR = "data-uixy-modal-id";
|
|
11
|
+
|
|
6
12
|
let seq = 0;
|
|
7
13
|
|
|
8
14
|
const layers = ref<number[]>([]);
|
|
9
15
|
|
|
16
|
+
const modalStack = ref<number[]>([]);
|
|
17
|
+
|
|
18
|
+
const zOf = (el: Element): number => {
|
|
19
|
+
const raw = window.getComputedStyle(el).zIndex;
|
|
20
|
+
const parsed = Number.parseInt(raw ?? "", 10);
|
|
21
|
+
|
|
22
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const hasPinnedAncestor = (el: HTMLElement | null): boolean => {
|
|
26
|
+
while (el) {
|
|
27
|
+
const { position } = window.getComputedStyle(el);
|
|
28
|
+
|
|
29
|
+
if (position === "fixed" || position === "sticky") return true;
|
|
30
|
+
|
|
31
|
+
el = el.parentElement;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return false;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const isAnchoredToPage = (anchor: HTMLElement): boolean => {
|
|
38
|
+
const overlay = anchor.closest(`[${OVERLAY_ATTR}]`);
|
|
39
|
+
|
|
40
|
+
if (overlay && zOf(overlay) > PAGE_ANCHORED_Z) return false;
|
|
41
|
+
|
|
42
|
+
return !hasPinnedAncestor(anchor);
|
|
43
|
+
};
|
|
44
|
+
|
|
10
45
|
export interface UseOverlayLayerReturn {
|
|
11
46
|
id: number;
|
|
12
47
|
zIndex: ComputedRef<number>;
|
|
13
48
|
isTop: ComputedRef<boolean>;
|
|
14
49
|
isActive: ComputedRef<boolean>;
|
|
15
|
-
acquire: () => void;
|
|
50
|
+
acquire: (anchor?: HTMLElement | null) => void;
|
|
16
51
|
release: () => void;
|
|
17
52
|
}
|
|
18
53
|
|
|
19
54
|
export function useOverlayLayer(): UseOverlayLayerReturn {
|
|
20
55
|
const id = ++seq;
|
|
21
56
|
|
|
57
|
+
const pageAnchored = ref(false);
|
|
58
|
+
|
|
22
59
|
const index = computed(() => layers.value.indexOf(id));
|
|
23
60
|
const isActive = computed(() => index.value !== -1);
|
|
24
61
|
|
|
25
|
-
const zIndex = computed(() =>
|
|
26
|
-
|
|
27
|
-
|
|
62
|
+
const zIndex = computed(() => {
|
|
63
|
+
if (pageAnchored.value) return PAGE_ANCHORED_Z;
|
|
64
|
+
|
|
65
|
+
const position = isActive.value ? index.value + 1 : 1;
|
|
66
|
+
|
|
67
|
+
return BASE_Z + position * STEP_Z;
|
|
68
|
+
});
|
|
28
69
|
|
|
29
70
|
const isTop = computed(
|
|
30
71
|
() => isActive.value && index.value === layers.value.length - 1,
|
|
31
72
|
);
|
|
32
73
|
|
|
33
|
-
const acquire = () => {
|
|
34
|
-
if (!layers.value.includes(id)) layers.value = [...layers.value, id];
|
|
35
|
-
};
|
|
36
|
-
|
|
37
74
|
const release = () => {
|
|
38
|
-
|
|
75
|
+
pageAnchored.value = false;
|
|
76
|
+
|
|
77
|
+
if (layers.value.includes(id)) {
|
|
39
78
|
layers.value = layers.value.filter((x) => x !== id);
|
|
79
|
+
}
|
|
40
80
|
};
|
|
41
81
|
|
|
42
|
-
|
|
82
|
+
const acquire = (anchor?: HTMLElement | null) => {
|
|
83
|
+
if (anchor && isAnchoredToPage(anchor)) {
|
|
84
|
+
release();
|
|
43
85
|
|
|
44
|
-
|
|
45
|
-
}
|
|
86
|
+
pageAnchored.value = true;
|
|
46
87
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
export const MODAL_ATTR = "data-uixy-modal";
|
|
50
|
-
export const MODAL_ID_ATTR = "data-uixy-modal-id";
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
51
90
|
|
|
52
|
-
|
|
91
|
+
pageAnchored.value = false;
|
|
53
92
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
93
|
+
if (!layers.value.includes(id)) {
|
|
94
|
+
layers.value = [...layers.value, id];
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
onScopeDispose(release);
|
|
99
|
+
|
|
100
|
+
return { id, zIndex, isTop, isActive, acquire, release };
|
|
101
|
+
}
|
|
59
102
|
|
|
60
103
|
export const getTopModalEl = (): HTMLElement | null => {
|
|
61
104
|
if (typeof document === "undefined") return null;
|
|
62
|
-
|
|
105
|
+
|
|
106
|
+
const modals = Array.from(
|
|
63
107
|
document.querySelectorAll<HTMLElement>(`[${MODAL_ATTR}]`),
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const openModals = modals.filter((el) => {
|
|
111
|
+
const rawId = el.getAttribute(MODAL_ID_ATTR);
|
|
112
|
+
|
|
113
|
+
if (rawId === null) return true;
|
|
114
|
+
|
|
115
|
+
return modalStack.value.includes(Number(rawId));
|
|
68
116
|
});
|
|
69
|
-
|
|
70
|
-
|
|
117
|
+
|
|
118
|
+
if (openModals.length === 0) return null;
|
|
119
|
+
|
|
120
|
+
return openModals.reduce((a, b) => (zOf(b) >= zOf(a) ? b : a));
|
|
71
121
|
};
|
|
72
122
|
|
|
73
123
|
function reconcileInert() {
|
|
@@ -80,6 +130,7 @@ function reconcileInert() {
|
|
|
80
130
|
});
|
|
81
131
|
|
|
82
132
|
const topEl = getTopModalEl();
|
|
133
|
+
|
|
83
134
|
if (!topEl) return;
|
|
84
135
|
|
|
85
136
|
const topZ = zOf(topEl);
|
|
@@ -87,7 +138,6 @@ function reconcileInert() {
|
|
|
87
138
|
Array.from(document.body.children).forEach((el) => {
|
|
88
139
|
if (!(el instanceof HTMLElement)) return;
|
|
89
140
|
if (el === topEl || el.contains(topEl)) return;
|
|
90
|
-
|
|
91
141
|
if (el.hasAttribute(OVERLAY_ATTR) && zOf(el) > topZ) return;
|
|
92
142
|
|
|
93
143
|
el.setAttribute("inert", "");
|
|
@@ -113,14 +163,19 @@ export function useModalLayer(): UseModalLayerReturn {
|
|
|
113
163
|
|
|
114
164
|
const open = () => {
|
|
115
165
|
layer.acquire();
|
|
116
|
-
|
|
166
|
+
|
|
167
|
+
if (!modalStack.value.includes(layer.id)) {
|
|
117
168
|
modalStack.value = [...modalStack.value, layer.id];
|
|
169
|
+
}
|
|
170
|
+
|
|
118
171
|
nextTick(reconcileInert);
|
|
119
172
|
};
|
|
120
173
|
|
|
121
174
|
const close = () => {
|
|
122
175
|
layer.release();
|
|
176
|
+
|
|
123
177
|
modalStack.value = modalStack.value.filter((x) => x !== layer.id);
|
|
178
|
+
|
|
124
179
|
reconcileInert();
|
|
125
180
|
nextTick(reconcileInert);
|
|
126
181
|
};
|
|
@@ -118,7 +118,7 @@ export function usePosition({
|
|
|
118
118
|
[active, () => direction, () => align],
|
|
119
119
|
([isActive]) => {
|
|
120
120
|
if (isActive) {
|
|
121
|
-
acquire();
|
|
121
|
+
acquire(target.value);
|
|
122
122
|
updatePosition();
|
|
123
123
|
observeResize();
|
|
124
124
|
window.addEventListener("resize", updatePosition);
|