design-system-next 2.9.6 → 2.9.7
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/dist/design-system-next.js +6505 -6485
- package/dist/design-system-next.js.gz +0 -0
- package/dist/main.css +1 -1
- package/dist/main.css.gz +0 -0
- package/dist/package.json.d.ts +1 -1
- package/package.json +1 -1
- package/src/App.vue +80 -1
- package/src/components/select/select-ladderized/use-select-ladderized.ts +164 -164
- package/src/components/select/select-multiple/select-multiple.ts +4 -0
- package/src/components/select/select-multiple/select-multiple.vue +75 -30
- package/src/components/select/select-multiple/use-select-multiple.ts +103 -2
|
@@ -1,164 +1,164 @@
|
|
|
1
|
-
import { ref, toRefs, computed, watch } from 'vue';
|
|
2
|
-
import { useVModel, useDebounceFn, onClickOutside } from '@vueuse/core';
|
|
3
|
-
|
|
4
|
-
import type { SelectLadderizedPropTypes } from './select-ladderized';
|
|
5
|
-
|
|
6
|
-
import type { MenuListType } from '@/components/list/list';
|
|
7
|
-
|
|
8
|
-
export const useSelectLadderized = (
|
|
9
|
-
props: SelectLadderizedPropTypes,
|
|
10
|
-
emit: (event: string, ...args: unknown[]) => void,
|
|
11
|
-
) => {
|
|
12
|
-
const { options, disabled } = toRefs(props);
|
|
13
|
-
|
|
14
|
-
const ladderizedClasses = computed(() => ({
|
|
15
|
-
baseClasses: 'spr-flex spr-flex-col spr-gap-size-spacing-4xs',
|
|
16
|
-
labelClasses: 'spr-body-sm-regular spr-text-color-strong spr-block',
|
|
17
|
-
}));
|
|
18
|
-
|
|
19
|
-
// Popper Variables
|
|
20
|
-
const ladderizedSelectPopperState = ref(false);
|
|
21
|
-
const ladderizedSelectRef = ref(null);
|
|
22
|
-
const isLadderizedSelectPopperDisabled = computed(() => disabled.value);
|
|
23
|
-
|
|
24
|
-
// Ladderized Select Model
|
|
25
|
-
const ladderizedSelectModel = useVModel(props, 'modelValue', emit);
|
|
26
|
-
const ladderizedSelectOptions = computed(() => options.value);
|
|
27
|
-
|
|
28
|
-
// Input Variables
|
|
29
|
-
const inputText = ref<string>('');
|
|
30
|
-
const isSearching = ref(false);
|
|
31
|
-
const wasCleared = ref(false);
|
|
32
|
-
|
|
33
|
-
const isLeafNode = (item: MenuListType): boolean => {
|
|
34
|
-
return !item.sublevel || item.sublevel.length === 0;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
// Helper to find the path to a selected value in the menu tree
|
|
38
|
-
const findPathToValue = (items: MenuListType[], value: string | number, path: string[] = []): string[] | null => {
|
|
39
|
-
for (const item of items) {
|
|
40
|
-
const newPath = [...path, item.text];
|
|
41
|
-
|
|
42
|
-
if (item.value === value) {
|
|
43
|
-
return newPath;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (item.sublevel) {
|
|
47
|
-
const result = findPathToValue(item.sublevel, value, newPath);
|
|
48
|
-
|
|
49
|
-
if (result) return result;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return null;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const handleSelectedLadderizedItem = (selectedItems: string[], selectedItem?: MenuListType) => {
|
|
57
|
-
wasCleared.value = false;
|
|
58
|
-
ladderizedSelectModel.value = selectedItems;
|
|
59
|
-
|
|
60
|
-
let itemToCheck = selectedItem;
|
|
61
|
-
|
|
62
|
-
// Fallback: if selectedItem is not provided, try to find it from the value
|
|
63
|
-
if (!itemToCheck && selectedItems.length > 0) {
|
|
64
|
-
const findItemByValue = (items: MenuListType[], value: string | number): MenuListType | undefined => {
|
|
65
|
-
for (const item of items) {
|
|
66
|
-
if (item.value === value) return item;
|
|
67
|
-
|
|
68
|
-
if (item.sublevel) {
|
|
69
|
-
const found = findItemByValue(item.sublevel, value);
|
|
70
|
-
|
|
71
|
-
if (found) return found;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return undefined;
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
itemToCheck = findItemByValue(ladderizedSelectOptions.value, selectedItems[selectedItems.length - 1]);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (itemToCheck) {
|
|
82
|
-
const path = findPathToValue(ladderizedSelectOptions.value, itemToCheck.value);
|
|
83
|
-
|
|
84
|
-
inputText.value = path ? path.join(' > ') : itemToCheck.text || '';
|
|
85
|
-
|
|
86
|
-
if (isLeafNode(itemToCheck)) {
|
|
87
|
-
ladderizedSelectPopperState.value = false;
|
|
88
|
-
}
|
|
89
|
-
} else if (selectedItems.length === 0 && !wasCleared.value) {
|
|
90
|
-
inputText.value = '';
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
const handleSearch = () => {
|
|
95
|
-
isSearching.value = true;
|
|
96
|
-
|
|
97
|
-
debouncedEmitSearch();
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
const debouncedEmitSearch = useDebounceFn(() => {
|
|
101
|
-
// Optionally emit search event here if needed
|
|
102
|
-
}, 300);
|
|
103
|
-
|
|
104
|
-
const handleClear = () => {
|
|
105
|
-
wasCleared.value = true;
|
|
106
|
-
|
|
107
|
-
inputText.value = '';
|
|
108
|
-
|
|
109
|
-
emit('update:modelValue', []);
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const handleOptionsToggle = () => {
|
|
113
|
-
ladderizedSelectPopperState.value = true;
|
|
114
|
-
|
|
115
|
-
isSearching.value = false;
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
// Watch for changes in modelValue to update inputText
|
|
119
|
-
watch(
|
|
120
|
-
() => ladderizedSelectModel.value,
|
|
121
|
-
(newVal) => {
|
|
122
|
-
if (wasCleared.value) {
|
|
123
|
-
inputText.value = '';
|
|
124
|
-
wasCleared.value = false;
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (Array.isArray(newVal) && newVal.length > 0) {
|
|
129
|
-
// Treat the array as a single path for ladderized select
|
|
130
|
-
let currentLevel = ladderizedSelectOptions.value;
|
|
131
|
-
|
|
132
|
-
const pathTexts: string[] = [];
|
|
133
|
-
|
|
134
|
-
for (const value of newVal) {
|
|
135
|
-
const found = currentLevel.find((item) => item.value === value);
|
|
136
|
-
if (!found) break;
|
|
137
|
-
pathTexts.push(found.text);
|
|
138
|
-
currentLevel = found.sublevel || [];
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
inputText.value = pathTexts.join(' > ');
|
|
142
|
-
}
|
|
143
|
-
},
|
|
144
|
-
{ immediate: true },
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
onClickOutside(ladderizedSelectRef, () => {
|
|
148
|
-
ladderizedSelectPopperState.value = false;
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
return {
|
|
152
|
-
ladderizedClasses,
|
|
153
|
-
ladderizedSelectPopperState,
|
|
154
|
-
ladderizedSelectRef,
|
|
155
|
-
ladderizedSelectOptions,
|
|
156
|
-
isLadderizedSelectPopperDisabled,
|
|
157
|
-
ladderizedSelectModel,
|
|
158
|
-
inputText,
|
|
159
|
-
handleSelectedLadderizedItem,
|
|
160
|
-
handleSearch,
|
|
161
|
-
handleClear,
|
|
162
|
-
handleOptionsToggle,
|
|
163
|
-
};
|
|
164
|
-
};
|
|
1
|
+
import { ref, toRefs, computed, watch } from 'vue';
|
|
2
|
+
import { useVModel, useDebounceFn, onClickOutside } from '@vueuse/core';
|
|
3
|
+
|
|
4
|
+
import type { SelectLadderizedPropTypes } from './select-ladderized';
|
|
5
|
+
|
|
6
|
+
import type { MenuListType } from '@/components/list/list';
|
|
7
|
+
|
|
8
|
+
export const useSelectLadderized = (
|
|
9
|
+
props: SelectLadderizedPropTypes,
|
|
10
|
+
emit: (event: string, ...args: unknown[]) => void,
|
|
11
|
+
) => {
|
|
12
|
+
const { options, disabled } = toRefs(props);
|
|
13
|
+
|
|
14
|
+
const ladderizedClasses = computed(() => ({
|
|
15
|
+
baseClasses: 'spr-flex spr-flex-col spr-gap-size-spacing-4xs',
|
|
16
|
+
labelClasses: 'spr-body-sm-regular spr-text-color-strong spr-block',
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
// Popper Variables
|
|
20
|
+
const ladderizedSelectPopperState = ref(false);
|
|
21
|
+
const ladderizedSelectRef = ref(null);
|
|
22
|
+
const isLadderizedSelectPopperDisabled = computed(() => disabled.value);
|
|
23
|
+
|
|
24
|
+
// Ladderized Select Model
|
|
25
|
+
const ladderizedSelectModel = useVModel(props, 'modelValue', emit);
|
|
26
|
+
const ladderizedSelectOptions = computed(() => options.value);
|
|
27
|
+
|
|
28
|
+
// Input Variables
|
|
29
|
+
const inputText = ref<string>('');
|
|
30
|
+
const isSearching = ref(false);
|
|
31
|
+
const wasCleared = ref(false);
|
|
32
|
+
|
|
33
|
+
const isLeafNode = (item: MenuListType): boolean => {
|
|
34
|
+
return !item.sublevel || item.sublevel.length === 0;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Helper to find the path to a selected value in the menu tree
|
|
38
|
+
const findPathToValue = (items: MenuListType[], value: string | number, path: string[] = []): string[] | null => {
|
|
39
|
+
for (const item of items) {
|
|
40
|
+
const newPath = [...path, item.text];
|
|
41
|
+
|
|
42
|
+
if (item.value === value) {
|
|
43
|
+
return newPath;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (item.sublevel) {
|
|
47
|
+
const result = findPathToValue(item.sublevel, value, newPath);
|
|
48
|
+
|
|
49
|
+
if (result) return result;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return null;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const handleSelectedLadderizedItem = (selectedItems: string[], selectedItem?: MenuListType) => {
|
|
57
|
+
wasCleared.value = false;
|
|
58
|
+
ladderizedSelectModel.value = selectedItems;
|
|
59
|
+
|
|
60
|
+
let itemToCheck = selectedItem;
|
|
61
|
+
|
|
62
|
+
// Fallback: if selectedItem is not provided, try to find it from the value
|
|
63
|
+
if (!itemToCheck && selectedItems.length > 0) {
|
|
64
|
+
const findItemByValue = (items: MenuListType[], value: string | number): MenuListType | undefined => {
|
|
65
|
+
for (const item of items) {
|
|
66
|
+
if (item.value === value) return item;
|
|
67
|
+
|
|
68
|
+
if (item.sublevel) {
|
|
69
|
+
const found = findItemByValue(item.sublevel, value);
|
|
70
|
+
|
|
71
|
+
if (found) return found;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return undefined;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
itemToCheck = findItemByValue(ladderizedSelectOptions.value, selectedItems[selectedItems.length - 1]);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (itemToCheck) {
|
|
82
|
+
const path = findPathToValue(ladderizedSelectOptions.value, itemToCheck.value);
|
|
83
|
+
|
|
84
|
+
inputText.value = path ? path.join(' > ') : itemToCheck.text || '';
|
|
85
|
+
|
|
86
|
+
if (isLeafNode(itemToCheck)) {
|
|
87
|
+
ladderizedSelectPopperState.value = false;
|
|
88
|
+
}
|
|
89
|
+
} else if (selectedItems.length === 0 && !wasCleared.value) {
|
|
90
|
+
inputText.value = '';
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const handleSearch = () => {
|
|
95
|
+
isSearching.value = true;
|
|
96
|
+
|
|
97
|
+
debouncedEmitSearch();
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const debouncedEmitSearch = useDebounceFn(() => {
|
|
101
|
+
// Optionally emit search event here if needed
|
|
102
|
+
}, 300);
|
|
103
|
+
|
|
104
|
+
const handleClear = () => {
|
|
105
|
+
wasCleared.value = true;
|
|
106
|
+
|
|
107
|
+
inputText.value = '';
|
|
108
|
+
|
|
109
|
+
emit('update:modelValue', []);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const handleOptionsToggle = () => {
|
|
113
|
+
ladderizedSelectPopperState.value = true;
|
|
114
|
+
|
|
115
|
+
isSearching.value = false;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Watch for changes in modelValue to update inputText
|
|
119
|
+
watch(
|
|
120
|
+
() => ladderizedSelectModel.value,
|
|
121
|
+
(newVal) => {
|
|
122
|
+
if (wasCleared.value) {
|
|
123
|
+
inputText.value = '';
|
|
124
|
+
wasCleared.value = false;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (Array.isArray(newVal) && newVal.length > 0) {
|
|
129
|
+
// Treat the array as a single path for ladderized select
|
|
130
|
+
let currentLevel = ladderizedSelectOptions.value;
|
|
131
|
+
|
|
132
|
+
const pathTexts: string[] = [];
|
|
133
|
+
|
|
134
|
+
for (const value of newVal) {
|
|
135
|
+
const found = currentLevel.find((item) => item.value === value);
|
|
136
|
+
if (!found) break;
|
|
137
|
+
pathTexts.push(found.text);
|
|
138
|
+
currentLevel = found.sublevel || [];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
inputText.value = pathTexts.join(' > ');
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
{ immediate: true },
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
onClickOutside(ladderizedSelectRef, () => {
|
|
148
|
+
ladderizedSelectPopperState.value = false;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
ladderizedClasses,
|
|
153
|
+
ladderizedSelectPopperState,
|
|
154
|
+
ladderizedSelectRef,
|
|
155
|
+
ladderizedSelectOptions,
|
|
156
|
+
isLadderizedSelectPopperDisabled,
|
|
157
|
+
ladderizedSelectModel,
|
|
158
|
+
inputText,
|
|
159
|
+
handleSelectedLadderizedItem,
|
|
160
|
+
handleSearch,
|
|
161
|
+
handleClear,
|
|
162
|
+
handleOptionsToggle,
|
|
163
|
+
};
|
|
164
|
+
};
|
|
@@ -25,38 +25,81 @@
|
|
|
25
25
|
>
|
|
26
26
|
<div ref="multiSelectRef">
|
|
27
27
|
<div @click="handleOptionsToggle">
|
|
28
|
-
<
|
|
29
|
-
:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
class="spr-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
28
|
+
<template v-if="props.chipped">
|
|
29
|
+
<div :class="multiSelectClasses.chippedInputTextBaseClasses">
|
|
30
|
+
<div ref="chippedInputTextRef" :class="multiSelectClasses.chippedInputTextClasses">
|
|
31
|
+
<div class="spr-h-auto spr-w-full">
|
|
32
|
+
<template v-if="multiSelectedListItems.length > 0">
|
|
33
|
+
<template v-for="item in multiSelectedListItems" :key="item.value">
|
|
34
|
+
<spr-chips
|
|
35
|
+
class="spr-m-1 spr-inline-block"
|
|
36
|
+
:label="String(item.text)"
|
|
37
|
+
closable
|
|
38
|
+
visible
|
|
39
|
+
@close="handleChippedRemoveItem(String(item.value))"
|
|
40
|
+
/>
|
|
41
|
+
</template>
|
|
42
|
+
</template>
|
|
43
|
+
<template v-else>
|
|
44
|
+
<span class="spr-placeholder spr-px-3 spr-text-gray-400">{{ props.placeholder }}</span>
|
|
45
|
+
</template>
|
|
46
|
+
</div>
|
|
47
|
+
<div :class="multiSelectClasses.chippedIconClasses">
|
|
48
|
+
<div class="spr-flex spr-items-center spr-gap-1">
|
|
49
|
+
<Icon
|
|
50
|
+
v-if="props.clearable && inputText"
|
|
51
|
+
class="spr-cursor-pointer"
|
|
52
|
+
icon="ph:x"
|
|
53
|
+
@click.stop="handleClear"
|
|
54
|
+
/>
|
|
55
|
+
<Icon icon="ph:caret-down" />
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
53
58
|
</div>
|
|
54
|
-
</
|
|
59
|
+
</div>
|
|
60
|
+
<div v-if="props.displayHelper" :class="multiSelectClasses.chippedHelperContainerClasses">
|
|
61
|
+
<div v-if="props.displayHelper" :class="multiSelectClasses.chippedHelperClasses">
|
|
62
|
+
<slot name="helperMessage">
|
|
63
|
+
<Icon v-if="props.helperIcon" :icon="props.helperIcon" width="20px" height="20px" />
|
|
64
|
+
<span>{{ props.helperText }}</span>
|
|
65
|
+
</slot>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</template>
|
|
69
|
+
<template v-else>
|
|
70
|
+
<spr-input
|
|
71
|
+
:id="`input-${props.id}`"
|
|
72
|
+
v-model="inputText"
|
|
73
|
+
:class="{
|
|
74
|
+
'spr-cursor-pointer': true,
|
|
75
|
+
}"
|
|
76
|
+
:placeholder="props.placeholder"
|
|
77
|
+
autocomplete="off"
|
|
78
|
+
:helper-text="props.helperText"
|
|
79
|
+
:helper-icon="props.helperIcon"
|
|
80
|
+
:display-helper="props.displayHelper"
|
|
81
|
+
:active="props.active"
|
|
82
|
+
:readonly="true"
|
|
83
|
+
:disabled="props.disabled"
|
|
84
|
+
:error="props.error"
|
|
85
|
+
>
|
|
86
|
+
<template #icon>
|
|
87
|
+
<div class="spr-flex spr-items-center spr-gap-1">
|
|
88
|
+
<Icon
|
|
89
|
+
v-if="props.clearable && inputText"
|
|
90
|
+
class="spr-cursor-pointer"
|
|
91
|
+
icon="ph:x"
|
|
92
|
+
@click.stop="handleClear"
|
|
93
|
+
/>
|
|
94
|
+
<Icon icon="ph:caret-down" />
|
|
95
|
+
</div>
|
|
96
|
+
</template>
|
|
55
97
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
98
|
+
<template #helperMessage>
|
|
99
|
+
<slot name="helperMessage" />
|
|
100
|
+
</template>
|
|
101
|
+
</spr-input>
|
|
102
|
+
</template>
|
|
60
103
|
|
|
61
104
|
<!-- Hidden Select for QA automation -->
|
|
62
105
|
<select v-if="multiSelectOptions && multiSelectOptions.length" v-model="multiSelectModel" multiple hidden>
|
|
@@ -106,6 +149,7 @@ import 'floating-vue/dist/style.css';
|
|
|
106
149
|
|
|
107
150
|
import SprInput from '../../input/input.vue';
|
|
108
151
|
import SprList from '../../list/list.vue';
|
|
152
|
+
import SprChips from '../../chips/chips.vue';
|
|
109
153
|
|
|
110
154
|
import { multiSelectPropTypes, multiSelectEmitTypes } from './select-multiple';
|
|
111
155
|
|
|
@@ -124,6 +168,7 @@ const {
|
|
|
124
168
|
inputText,
|
|
125
169
|
isMultiSelectPopperDisabled,
|
|
126
170
|
handleMultiSelectedItem,
|
|
171
|
+
handleChippedRemoveItem,
|
|
127
172
|
handleClear,
|
|
128
173
|
handleOptionsToggle,
|
|
129
174
|
} = useMultiSelect(props, emit);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ref, toRefs, computed, ComputedRef, onMounted, watch } from 'vue';
|
|
2
|
-
import { onClickOutside, useVModel } from '@vueuse/core';
|
|
2
|
+
import { onClickOutside, useVModel, useFocus } from '@vueuse/core';
|
|
3
3
|
|
|
4
4
|
import classNames from 'classnames';
|
|
5
5
|
|
|
@@ -10,10 +10,15 @@ import type { MenuListType } from '../../list/list';
|
|
|
10
10
|
interface MultiSelectClasses {
|
|
11
11
|
baseClasses: string;
|
|
12
12
|
labelClasses: string;
|
|
13
|
+
chippedInputTextBaseClasses: string;
|
|
14
|
+
chippedInputTextClasses: string;
|
|
15
|
+
chippedIconClasses: string;
|
|
16
|
+
chippedHelperContainerClasses: string;
|
|
17
|
+
chippedHelperClasses: string;
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
export const useMultiSelect = (props: MultiSelectPropTypes, emit: SetupContext<MultiSelectEmitTypes>['emit']) => {
|
|
16
|
-
const { displayText, options,
|
|
21
|
+
const { displayText, options, textField, valueField, active, disabled, error } = toRefs(props);
|
|
17
22
|
|
|
18
23
|
const multiSelectClasses: ComputedRef<MultiSelectClasses> = computed(() => {
|
|
19
24
|
const baseClasses = classNames('spr-flex spr-flex-col spr-gap-size-spacing-4xs');
|
|
@@ -22,9 +27,66 @@ export const useMultiSelect = (props: MultiSelectPropTypes, emit: SetupContext<M
|
|
|
22
27
|
'spr-text-color-on-fill-disabled': disabled.value,
|
|
23
28
|
});
|
|
24
29
|
|
|
30
|
+
const chippedInputTextBaseClasses = classNames(
|
|
31
|
+
'spr-relative spr-flex spr-items-center spr-min-h-9 spr-rounded-border-radius-md spr-border-[1.5px] spr-border-solid',
|
|
32
|
+
{
|
|
33
|
+
'spr-cursor-pointer': !disabled.value,
|
|
34
|
+
|
|
35
|
+
// Border State
|
|
36
|
+
'spr-border-color-weak': !focused.value && !error.value && !disabled.value && !active.value,
|
|
37
|
+
'spr-border-color-brand-base': !focused.value && active.value,
|
|
38
|
+
'spr-border-color-danger-base': !focused.value && error.value,
|
|
39
|
+
|
|
40
|
+
// Border State Focused
|
|
41
|
+
'focus: spr-border-kangkong-700': focused.value && !error.value && !disabled.value && !active.value,
|
|
42
|
+
'focus: spr-border-tomato-600': focused.value && error.value,
|
|
43
|
+
'focus: spr-border-white-100': focused.value && disabled.value,
|
|
44
|
+
|
|
45
|
+
// Disabled State
|
|
46
|
+
'spr-background-color-disabled spr-cursor-not-allowed spr-border-mushroom-100': disabled.value,
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const chippedInputTextClasses = classNames(
|
|
51
|
+
'spr-flex spr-gap-1 spr-justify-between spr-w-full spr-outline-none spr-ring-0 spr-border-none spr-rounded-border-radius-md spr-font-size-200',
|
|
52
|
+
'spr-font-size-200 [font-weight:inherit]',
|
|
53
|
+
'placeholder:spr-text-mushroom-300',
|
|
54
|
+
{
|
|
55
|
+
'spr-text-color-strong': !disabled.value,
|
|
56
|
+
|
|
57
|
+
// Disabled State
|
|
58
|
+
'spr-text-color-on-fill-disabled !spr-cursor-not-allowed': disabled.value,
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const chippedIconClasses = classNames(
|
|
63
|
+
'spr-flex spr-items-center spr-justify-center spr-h-inherit spr-px-2 [&>svg]:spr-min-h-4 [&>svg]:spr-min-w-4',
|
|
64
|
+
{
|
|
65
|
+
'spr-text-mushroom-300': !error.value,
|
|
66
|
+
'spr-text-tomato-600': error.value,
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const chippedHelperContainerClasses = classNames(
|
|
71
|
+
'spr-flex spr-flex-row spr-items-start spr-justify-between spr-w-full',
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const chippedHelperClasses = classNames(
|
|
75
|
+
'spr-body-sm-regular spr-flex spr-items-center spr-gap-size-spacing-5xs spr-flex-1',
|
|
76
|
+
{
|
|
77
|
+
'spr-text-color-danger-base': error.value,
|
|
78
|
+
'spr-text-color-supporting': !error.value,
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
|
|
25
82
|
return {
|
|
26
83
|
baseClasses,
|
|
27
84
|
labelClasses,
|
|
85
|
+
chippedInputTextBaseClasses,
|
|
86
|
+
chippedInputTextClasses,
|
|
87
|
+
chippedIconClasses,
|
|
88
|
+
chippedHelperContainerClasses,
|
|
89
|
+
chippedHelperClasses,
|
|
28
90
|
};
|
|
29
91
|
});
|
|
30
92
|
|
|
@@ -39,7 +101,9 @@ export const useMultiSelect = (props: MultiSelectPropTypes, emit: SetupContext<M
|
|
|
39
101
|
|
|
40
102
|
const inputText = ref<string | number>('');
|
|
41
103
|
const inputTextBackup = ref<string | number>('');
|
|
104
|
+
const chippedInputTextRef = ref(null);
|
|
42
105
|
|
|
106
|
+
const { focused } = useFocus(chippedInputTextRef);
|
|
43
107
|
/**
|
|
44
108
|
* Returns the normalized value of the model as an array for internal use.
|
|
45
109
|
*/
|
|
@@ -148,6 +212,42 @@ export const useMultiSelect = (props: MultiSelectPropTypes, emit: SetupContext<M
|
|
|
148
212
|
updateMultiSelectedItemsFromValue();
|
|
149
213
|
};
|
|
150
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Removes an item from the multi-select model by its value.
|
|
217
|
+
* Handles stringified objects and emits the updated model value.
|
|
218
|
+
*/
|
|
219
|
+
const handleChippedRemoveItem = (itemValue: string) => {
|
|
220
|
+
let currentValues = Array.isArray(multiSelectModel.value) ? [...multiSelectModel.value] : [multiSelectModel.value];
|
|
221
|
+
|
|
222
|
+
currentValues = currentValues.filter((val) => {
|
|
223
|
+
let valToCompare = val;
|
|
224
|
+
|
|
225
|
+
if (typeof valToCompare === 'string' && valToCompare.startsWith('{') && valToCompare.endsWith('}')) {
|
|
226
|
+
try {
|
|
227
|
+
valToCompare = JSON.parse(valToCompare);
|
|
228
|
+
} catch {
|
|
229
|
+
// ignore
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (typeof itemValue === 'string' && itemValue.startsWith('{') && itemValue.endsWith('}')) {
|
|
234
|
+
try {
|
|
235
|
+
itemValue = JSON.parse(itemValue);
|
|
236
|
+
} catch {
|
|
237
|
+
// ignore
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (typeof valToCompare === 'object' && typeof itemValue === 'object') {
|
|
242
|
+
return JSON.stringify(valToCompare) !== JSON.stringify(itemValue);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return valToCompare != itemValue;
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
emit('update:modelValue', currentValues);
|
|
249
|
+
};
|
|
250
|
+
|
|
151
251
|
/**
|
|
152
252
|
* Updates the selected items in the multi-select based on the current model value.
|
|
153
253
|
* Handles stringified objects and updates the input text accordingly.
|
|
@@ -271,6 +371,7 @@ export const useMultiSelect = (props: MultiSelectPropTypes, emit: SetupContext<M
|
|
|
271
371
|
inputText,
|
|
272
372
|
isMultiSelectPopperDisabled,
|
|
273
373
|
handleMultiSelectedItem,
|
|
374
|
+
handleChippedRemoveItem,
|
|
274
375
|
handleClear,
|
|
275
376
|
handleOptionsToggle,
|
|
276
377
|
};
|