design-system-next 2.7.43 → 2.7.44
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 +3711 -3319
- 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/assets/styles/tailwind.css +21 -1
- package/src/components/input/input.ts +6 -2
- package/src/components/input/input.vue +11 -6
- package/src/components/input/use-input.ts +33 -3
- package/src/components/select/select-laderrized/select-laderrized.ts +122 -0
- package/src/components/select/select-laderrized/select-laderrized.vue +110 -0
- package/src/components/select/select-laderrized/use-select-laderrized.ts +499 -0
- package/src/components/select/select-multiple/select-multiple.ts +122 -0
- package/src/components/select/select-multiple/select-multiple.vue +110 -0
- package/src/components/select/select-multiple/use-select-multiple.ts +499 -0
- package/src/components/select/select.ts +105 -0
- package/src/components/select/select.vue +90 -0
- package/src/components/select/use-select.ts +445 -0
- package/src/examples/select-number-multi-select.vue +71 -0
- package/src/components/dropdown/__tests__/dropdown-fixes.spec.ts +0 -106
- package/src/components/dropdown/__tests__/dropdown-value-types.spec.ts +0 -213
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { PropType, ExtractPropTypes } from 'vue';
|
|
2
|
+
import type { MenuListType } from '../list/list';
|
|
3
|
+
|
|
4
|
+
export const definePropType = <T>(val: unknown): PropType<T> => val as PropType<T>;
|
|
5
|
+
|
|
6
|
+
const GROUPED_ITEMS_BY_TYPES = ['A-Z', 'Z-A'] as const;
|
|
7
|
+
|
|
8
|
+
const PLACEMENTS_TYPES = [
|
|
9
|
+
'auto',
|
|
10
|
+
'auto-start',
|
|
11
|
+
'auto-end',
|
|
12
|
+
'top',
|
|
13
|
+
'top-start',
|
|
14
|
+
'top-end',
|
|
15
|
+
'right',
|
|
16
|
+
'right-start',
|
|
17
|
+
'right-end',
|
|
18
|
+
'bottom',
|
|
19
|
+
'bottom-start',
|
|
20
|
+
'bottom-end',
|
|
21
|
+
'left',
|
|
22
|
+
'left-start',
|
|
23
|
+
'left-end',
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
const POPPER_STRATEGY_TYPES = ['fixed', 'absolute'] as const;
|
|
27
|
+
|
|
28
|
+
export const selectPropTypes = {
|
|
29
|
+
id: {
|
|
30
|
+
type: String,
|
|
31
|
+
required: true,
|
|
32
|
+
},
|
|
33
|
+
modelValue: {
|
|
34
|
+
type: [String, Number, Object, Array] as PropType<
|
|
35
|
+
string | number | Record<string, unknown> | (string | number | Record<string, unknown>)[]
|
|
36
|
+
>,
|
|
37
|
+
default: () => [],
|
|
38
|
+
},
|
|
39
|
+
menuList: {
|
|
40
|
+
type: Array as PropType<MenuListType[] | string[] | Record<string, unknown>[]>,
|
|
41
|
+
required: true,
|
|
42
|
+
default: [],
|
|
43
|
+
},
|
|
44
|
+
groupItemsBy: {
|
|
45
|
+
type: String as PropType<(typeof GROUPED_ITEMS_BY_TYPES)[number]>,
|
|
46
|
+
validator: (value: (typeof GROUPED_ITEMS_BY_TYPES)[number] | undefined) => {
|
|
47
|
+
return value === undefined || GROUPED_ITEMS_BY_TYPES.includes(value);
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
textField: {
|
|
51
|
+
type: String,
|
|
52
|
+
default: 'text',
|
|
53
|
+
description: 'Field name to use for display text when using dynamic object arrays',
|
|
54
|
+
},
|
|
55
|
+
valueField: {
|
|
56
|
+
type: String,
|
|
57
|
+
default: 'value',
|
|
58
|
+
description: 'Field name to use for value when using dynamic object arrays',
|
|
59
|
+
},
|
|
60
|
+
placeholder: {
|
|
61
|
+
type: String,
|
|
62
|
+
},
|
|
63
|
+
searchString: {
|
|
64
|
+
type: String,
|
|
65
|
+
default: '',
|
|
66
|
+
},
|
|
67
|
+
placement: {
|
|
68
|
+
type: String as PropType<(typeof PLACEMENTS_TYPES)[number]>,
|
|
69
|
+
validator: (value: (typeof PLACEMENTS_TYPES)[number]) => PLACEMENTS_TYPES.includes(value),
|
|
70
|
+
default: 'bottom',
|
|
71
|
+
},
|
|
72
|
+
popperStrategy: {
|
|
73
|
+
type: String,
|
|
74
|
+
validator: (value: 'fixed' | 'absolute') => POPPER_STRATEGY_TYPES.includes(value),
|
|
75
|
+
default: 'absolute',
|
|
76
|
+
},
|
|
77
|
+
popperWidth: {
|
|
78
|
+
type: String,
|
|
79
|
+
default: '100%',
|
|
80
|
+
},
|
|
81
|
+
width: {
|
|
82
|
+
type: String,
|
|
83
|
+
default: '100%',
|
|
84
|
+
},
|
|
85
|
+
wrapperPosition: {
|
|
86
|
+
type: String,
|
|
87
|
+
default: 'relative',
|
|
88
|
+
},
|
|
89
|
+
disabled: {
|
|
90
|
+
type: Boolean,
|
|
91
|
+
default: false,
|
|
92
|
+
},
|
|
93
|
+
readonly: {
|
|
94
|
+
type: Boolean,
|
|
95
|
+
default: false,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const selectEmitTypes = {
|
|
100
|
+
'infinite-scroll-trigger': Boolean,
|
|
101
|
+
'update:modelValue': (_value: unknown) => true, // Accept any type of value
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export type SelectPropTypes = ExtractPropTypes<typeof selectPropTypes>;
|
|
105
|
+
export type SelectEmitTypes = typeof selectEmitTypes;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Menu
|
|
3
|
+
:shown="selectPopperState"
|
|
4
|
+
aria-id="select-wrapper"
|
|
5
|
+
distance="4"
|
|
6
|
+
:placement="props.placement"
|
|
7
|
+
:triggers="[]"
|
|
8
|
+
:popper-hide-triggers="[]"
|
|
9
|
+
:auto-hide="false"
|
|
10
|
+
:disabled="isSelectPopperDisabled"
|
|
11
|
+
:container="`#${props.id}`"
|
|
12
|
+
:strategy="
|
|
13
|
+
props.popperStrategy === 'fixed' || props.popperStrategy === 'absolute' ? props.popperStrategy : 'absolute'
|
|
14
|
+
"
|
|
15
|
+
:delay="0"
|
|
16
|
+
:style="{
|
|
17
|
+
position: props.wrapperPosition,
|
|
18
|
+
width: props.width,
|
|
19
|
+
}"
|
|
20
|
+
>
|
|
21
|
+
<div @click="selectPopperState = true">
|
|
22
|
+
{{ selectedListItems }}
|
|
23
|
+
|
|
24
|
+
<spr-input
|
|
25
|
+
v-model="inputText"
|
|
26
|
+
label="Select Numbers"
|
|
27
|
+
:placeholder="props.placeholder"
|
|
28
|
+
:readonly="props.readonly"
|
|
29
|
+
:disabled="props.disabled"
|
|
30
|
+
autocomplete="off"
|
|
31
|
+
/>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div
|
|
35
|
+
:id="props.id"
|
|
36
|
+
:style="{
|
|
37
|
+
width: props.popperWidth,
|
|
38
|
+
}"
|
|
39
|
+
></div>
|
|
40
|
+
|
|
41
|
+
<template #popper>
|
|
42
|
+
<div
|
|
43
|
+
ref="selectRef"
|
|
44
|
+
class="spr-grid spr-max-h-[300px] spr-gap-0.5 spr-overflow-y-auto spr-overflow-x-hidden spr-p-2"
|
|
45
|
+
>
|
|
46
|
+
<template v-if="selectMenuList.length > 0">
|
|
47
|
+
<spr-list
|
|
48
|
+
v-model="selectedListItems"
|
|
49
|
+
:menu-list="selectMenuList"
|
|
50
|
+
:group-items-by="props.groupItemsBy"
|
|
51
|
+
:pre-selected-items="selectValue"
|
|
52
|
+
@update:model-value="handleSelectedItem"
|
|
53
|
+
/>
|
|
54
|
+
</template>
|
|
55
|
+
<template v-else>
|
|
56
|
+
<div class="spr-flex spr-items-center spr-justify-center spr-p-2 spr-text-center">
|
|
57
|
+
<span class="spr-body-sm-regular spr-m-0">No results found</span>
|
|
58
|
+
</div>
|
|
59
|
+
</template>
|
|
60
|
+
</div>
|
|
61
|
+
</template>
|
|
62
|
+
</Menu>
|
|
63
|
+
</template>
|
|
64
|
+
|
|
65
|
+
<script lang="ts" setup>
|
|
66
|
+
import { Menu } from 'floating-vue';
|
|
67
|
+
|
|
68
|
+
import 'floating-vue/dist/style.css';
|
|
69
|
+
|
|
70
|
+
import SprInput from '../input/input.vue';
|
|
71
|
+
import SprList from '../list/list.vue';
|
|
72
|
+
|
|
73
|
+
import { selectPropTypes, selectEmitTypes } from './select';
|
|
74
|
+
|
|
75
|
+
import { useSelect } from './use-select';
|
|
76
|
+
|
|
77
|
+
const props = defineProps(selectPropTypes);
|
|
78
|
+
const emit = defineEmits(selectEmitTypes);
|
|
79
|
+
|
|
80
|
+
const {
|
|
81
|
+
selectPopperState,
|
|
82
|
+
selectRef,
|
|
83
|
+
selectMenuList,
|
|
84
|
+
isSelectPopperDisabled,
|
|
85
|
+
selectedListItems,
|
|
86
|
+
handleSelectedItem,
|
|
87
|
+
selectValue,
|
|
88
|
+
inputText,
|
|
89
|
+
} = useSelect(props, emit);
|
|
90
|
+
</script>
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import { ref, toRefs, computed, onMounted, watch } from 'vue';
|
|
2
|
+
import { onClickOutside, useInfiniteScroll, useVModel } from '@vueuse/core';
|
|
3
|
+
|
|
4
|
+
import type { SetupContext } from 'vue';
|
|
5
|
+
import type { SelectPropTypes, SelectEmitTypes } from './select';
|
|
6
|
+
import type { MenuListType } from '../list/list';
|
|
7
|
+
|
|
8
|
+
export const useSelect = (props: SelectPropTypes, emit: SetupContext<SelectEmitTypes>['emit']) => {
|
|
9
|
+
const { menuList, searchString, disabled, textField, valueField } = toRefs(props);
|
|
10
|
+
|
|
11
|
+
const selectRef = ref<HTMLDivElement | null>(null);
|
|
12
|
+
const selectPopperState = ref<boolean>(false);
|
|
13
|
+
const isSelectPopperDisabled = computed(() => disabled.value);
|
|
14
|
+
|
|
15
|
+
const selectModel = useVModel(props, 'modelValue', emit);
|
|
16
|
+
const inputText = ref('');
|
|
17
|
+
const selectedListItems = ref<MenuListType[]>([]);
|
|
18
|
+
const selectMenuList = ref<MenuListType[]>([]);
|
|
19
|
+
|
|
20
|
+
// Normalized value for internal use - always an array
|
|
21
|
+
const normalizedValue = computed(() => {
|
|
22
|
+
// If already an array, use it
|
|
23
|
+
if (Array.isArray(selectModel.value)) {
|
|
24
|
+
return selectModel.value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// If not an array but has a value, make it a single-item array
|
|
28
|
+
if (selectModel.value !== undefined && selectModel.value !== null) {
|
|
29
|
+
return [selectModel.value];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Default empty array
|
|
33
|
+
return [];
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Compatibility layer for pre-selected items (List component expects string[] format)
|
|
37
|
+
const compatPreSelectedItems = computed(() => {
|
|
38
|
+
return selectModel.value !== undefined && selectModel.value !== null
|
|
39
|
+
? [
|
|
40
|
+
typeof selectModel.value === 'object'
|
|
41
|
+
? selectModel.value
|
|
42
|
+
: typeof selectModel.value === 'number'
|
|
43
|
+
? selectModel.value
|
|
44
|
+
: selectModel.value.toString(),
|
|
45
|
+
]
|
|
46
|
+
: [];
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const processMenuList = () => {
|
|
50
|
+
// Handle empty array or non-array values
|
|
51
|
+
if (!menuList.value || !Array.isArray(menuList.value) || menuList.value.length === 0) {
|
|
52
|
+
selectMenuList.value = [];
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const firstItem = menuList.value[0];
|
|
57
|
+
|
|
58
|
+
// Handle array of strings
|
|
59
|
+
if (typeof firstItem === 'string') {
|
|
60
|
+
selectMenuList.value = (menuList.value as string[]).map((item) => ({
|
|
61
|
+
text: item,
|
|
62
|
+
value: item,
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Handle array of numbers
|
|
69
|
+
if (typeof firstItem === 'number') {
|
|
70
|
+
selectMenuList.value = (menuList.value as number[]).map((item) => ({
|
|
71
|
+
text: item.toString(),
|
|
72
|
+
value: item, // Keep the value as a number instead of converting to string
|
|
73
|
+
}));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Handle array of objects with dynamic attributes
|
|
78
|
+
if (typeof firstItem === 'object' && firstItem !== null) {
|
|
79
|
+
// Check if it's already in MenuListType format
|
|
80
|
+
if ('text' in firstItem && 'value' in firstItem) {
|
|
81
|
+
selectMenuList.value = menuList.value as MenuListType[];
|
|
82
|
+
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Transform to MenuListType format using textField and valueField
|
|
87
|
+
selectMenuList.value = (menuList.value as Record<string, unknown>[]).map((item) => {
|
|
88
|
+
const displayText = item[textField.value] || 'Unnamed';
|
|
89
|
+
// Use the specified value field if available, otherwise use the entire object
|
|
90
|
+
const itemValue = valueField.value && item[valueField.value] !== undefined ? item[valueField.value] : item;
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
text: displayText,
|
|
94
|
+
value: typeof itemValue === 'object' ? JSON.stringify(itemValue) : itemValue.toString(),
|
|
95
|
+
_originalObject: item, // Store the original object for reference
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Default fallback
|
|
103
|
+
selectMenuList.value = menuList.value as MenuListType[];
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
watch(menuList, () => {
|
|
107
|
+
processMenuList();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const handleSearch = () => {
|
|
111
|
+
if (menuList.value && menuList.value.length === 0) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!multiSelect.value) {
|
|
116
|
+
singleSelectSearch();
|
|
117
|
+
} else {
|
|
118
|
+
// Process menu list for searching
|
|
119
|
+
processMenuList();
|
|
120
|
+
|
|
121
|
+
// Handle multi-select search - filter based on search string
|
|
122
|
+
if (searchString.value.trim() !== '') {
|
|
123
|
+
selectMenuList.value = getFilteredMenuList(selectMenuList.value);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const singleSelectSearch = () => {
|
|
129
|
+
if (props.ladderized) {
|
|
130
|
+
ladderizedSearch();
|
|
131
|
+
} else {
|
|
132
|
+
basicSearch();
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const basicSearch = () => {
|
|
137
|
+
// Process menu list first
|
|
138
|
+
processMenuList();
|
|
139
|
+
|
|
140
|
+
// Then filter based on search string
|
|
141
|
+
if (searchString.value.trim() !== '') {
|
|
142
|
+
selectMenuList.value = getFilteredMenuList(selectMenuList.value);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const ladderizedSearch = () => {
|
|
147
|
+
//revert to initial list if search string is empty or selectModel is not empty
|
|
148
|
+
if (searchString.value === '' || normalizedValue.value.length > 0) {
|
|
149
|
+
selectMenuList.value = [...menuList.value] as MenuListType[];
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const menuListSubLevels = getAllSublevelItems(menuList.value as MenuListType[]);
|
|
154
|
+
|
|
155
|
+
const filteredMenuList = getFilteredMenuList(menuList.value as MenuListType[]);
|
|
156
|
+
const filteredMenuListSubLevels = getFilteredMenuList(menuListSubLevels);
|
|
157
|
+
|
|
158
|
+
if (filteredMenuList.length > 0) {
|
|
159
|
+
//if there is a match at the top level of the menuList
|
|
160
|
+
selectMenuList.value = getAllSublevelItems(filteredMenuList);
|
|
161
|
+
} else if (filteredMenuListSubLevels.length > 0) {
|
|
162
|
+
//if there is a match at the 2nd level (sublevel of a menuList item) of the menuList
|
|
163
|
+
selectMenuList.value = filteredMenuListSubLevels;
|
|
164
|
+
} else {
|
|
165
|
+
selectMenuList.value = [];
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// compile sublevel items from menuList to a single array
|
|
170
|
+
// and add text and value of the parent item to all sublevel items as subtext and subvalue
|
|
171
|
+
const getAllSublevelItems = (menuList: MenuListType[]) => {
|
|
172
|
+
return menuList.reduce<MenuListType[]>((currentValue, currentItem) => {
|
|
173
|
+
if (currentItem.sublevel) {
|
|
174
|
+
const mappedSublevel = currentItem.sublevel.map((sublevelItem: MenuListType) => ({
|
|
175
|
+
...sublevelItem, //text and value of a sublevel item
|
|
176
|
+
subtext: currentItem.text, // text of parent of a sublevel item
|
|
177
|
+
subvalue: typeof currentItem.value === 'string' ? currentItem.value : String(currentItem.value), // value of parent of a sublevel item as string
|
|
178
|
+
}));
|
|
179
|
+
|
|
180
|
+
return [...currentValue, ...mappedSublevel];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return currentValue;
|
|
184
|
+
}, [] as MenuListType[]);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// filter list based on search string and menuList/sublevel texts
|
|
188
|
+
const getFilteredMenuList = (list: MenuListType[]) => {
|
|
189
|
+
return list.filter((item: MenuListType) => {
|
|
190
|
+
const searchTerm = searchString.value.toLowerCase().trim();
|
|
191
|
+
return item.text.toLowerCase().includes(searchTerm);
|
|
192
|
+
});
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
watch(searchString, () => {
|
|
196
|
+
handleSearch();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
onClickOutside(selectRef, () => {
|
|
200
|
+
selectPopperState.value = false;
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
useInfiniteScroll(
|
|
204
|
+
selectRef,
|
|
205
|
+
() => {
|
|
206
|
+
emit('infinite-scroll-trigger', true);
|
|
207
|
+
},
|
|
208
|
+
{ distance: 10 },
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// Handle selected item for simple list component
|
|
212
|
+
const handleSelectedItem = (selectedItems: MenuListType[]) => {
|
|
213
|
+
if (!props.ladderized) {
|
|
214
|
+
// Determine the type of value to emit based on the original data type and multiSelect
|
|
215
|
+
if (multiSelect.value) {
|
|
216
|
+
// For multi-select, always return an array
|
|
217
|
+
const values = selectedItems.map((item) => {
|
|
218
|
+
// If we stored the original object, use it
|
|
219
|
+
if ('_originalObject' in item) {
|
|
220
|
+
return item._originalObject;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// For simple types, handle value type conversion properly
|
|
224
|
+
const val = item.value;
|
|
225
|
+
|
|
226
|
+
// If it's already a number, keep it as a number
|
|
227
|
+
if (typeof val === 'number') {
|
|
228
|
+
return val;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// For strings that look like numbers, convert them
|
|
232
|
+
if (typeof val === 'string' && !isNaN(Number(val)) && val.trim() !== '') {
|
|
233
|
+
// Only convert if it looks like a proper number format
|
|
234
|
+
if (/^-?\d+(\.\d+)?$/.test(val)) {
|
|
235
|
+
return Number(val);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Return the original value for all other cases
|
|
240
|
+
return val;
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
selectModel.value = values as (string | number | Record<string, unknown>)[];
|
|
244
|
+
} else {
|
|
245
|
+
// For single-select
|
|
246
|
+
if (selectedItems.length === 0) {
|
|
247
|
+
selectModel.value = props.multiSelect ? [] : '';
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const item = selectedItems[0];
|
|
252
|
+
|
|
253
|
+
// If we stored the original object, use it
|
|
254
|
+
if ('_originalObject' in item) {
|
|
255
|
+
selectModel.value = item._originalObject as Record<string, unknown>;
|
|
256
|
+
} else {
|
|
257
|
+
// For simple types, return the value (try to convert number strings to numbers)
|
|
258
|
+
const val = item.value;
|
|
259
|
+
if (typeof val === 'string' && !isNaN(Number(val)) && val.trim() !== '') {
|
|
260
|
+
selectModel.value = Number(val);
|
|
261
|
+
} else {
|
|
262
|
+
selectModel.value = val;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
} else if (props.ladderized) {
|
|
267
|
+
if (props.searchString !== '') {
|
|
268
|
+
// generate select value if ladderized with search string
|
|
269
|
+
const subvalue = selectedItems[0]?.subvalue;
|
|
270
|
+
const value = selectedItems[0]?.value;
|
|
271
|
+
if (subvalue !== undefined && value !== undefined) {
|
|
272
|
+
selectModel.value = [subvalue, value] as [string, string | number];
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
// For regular ladderized select selection without search
|
|
276
|
+
if (selectedItems.length > 0) {
|
|
277
|
+
const item = selectedItems[0];
|
|
278
|
+
// Use the value directly for ladderized items
|
|
279
|
+
if (item && item.value) {
|
|
280
|
+
selectModel.value = item.value;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Always close select for single selection, regardless of value type
|
|
287
|
+
if (!multiSelect.value) {
|
|
288
|
+
setTimeout(() => {
|
|
289
|
+
selectPopperState.value = false;
|
|
290
|
+
}, 10);
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// Handle selected item for ladderized list component
|
|
295
|
+
const handleSelectedLadderizedItem = (selectedItems: string[]) => {
|
|
296
|
+
// Update the model value with the selected ladderized items
|
|
297
|
+
if (selectedItems.length > 0) {
|
|
298
|
+
selectModel.value = selectedItems;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// If item is from last sublevel, close the select
|
|
302
|
+
if (checkIfItemFromLastSublevel(selectedItems)) {
|
|
303
|
+
selectPopperState.value = false;
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const checkIfItemFromLastSublevel = (selectedItems: string[]) => {
|
|
308
|
+
let selectedItemsObject = selectMenuList.value;
|
|
309
|
+
|
|
310
|
+
// Traverse to the last item in the selectedItems array
|
|
311
|
+
selectedItems.forEach((selectedItem) => {
|
|
312
|
+
selectedItemsObject = selectedItemsObject.find((item) => selectedItem === item.value)?.sublevel ?? [];
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// If there is a sublevel, return false
|
|
316
|
+
if (selectedItemsObject.length > 0) {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return true;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// Update selected items when model value changes externally
|
|
324
|
+
const updateSelectedItemsFromValue = () => {
|
|
325
|
+
if (!selectMenuList.value.length) return;
|
|
326
|
+
|
|
327
|
+
const values = normalizedValue.value;
|
|
328
|
+
if (!values || !values.length) {
|
|
329
|
+
selectedListItems.value = [];
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Store both original values and string versions for flexible matching
|
|
334
|
+
const valueData = values.map((val) => {
|
|
335
|
+
if (val === undefined || val === null) return { original: '', string: '' };
|
|
336
|
+
|
|
337
|
+
// For objects, use JSON string representation
|
|
338
|
+
if (typeof val === 'object') {
|
|
339
|
+
return {
|
|
340
|
+
original: val,
|
|
341
|
+
string: JSON.stringify(val),
|
|
342
|
+
isObject: true,
|
|
343
|
+
id: 'id' in val ? val.id : undefined,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// For numbers and strings, keep original and string versions
|
|
348
|
+
return {
|
|
349
|
+
original: val,
|
|
350
|
+
string: val.toString(),
|
|
351
|
+
isObject: false,
|
|
352
|
+
};
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// Extract just string values for comparison
|
|
356
|
+
const valueStrings = valueData.map((v) => v.string);
|
|
357
|
+
|
|
358
|
+
if (props.ladderized) {
|
|
359
|
+
// Special handling for ladderized selects
|
|
360
|
+
if (Array.isArray(selectModel.value) && selectModel.value.length === 2) {
|
|
361
|
+
// Handle [subvalue, value] format used in ladderized selects with search
|
|
362
|
+
const subvalue = selectModel.value[0]?.toString() || '';
|
|
363
|
+
const value = selectModel.value[1]?.toString() || '';
|
|
364
|
+
|
|
365
|
+
selectedListItems.value = selectMenuList.value.filter((item) => {
|
|
366
|
+
return item.value === value && (!item.subvalue || item.subvalue === subvalue);
|
|
367
|
+
});
|
|
368
|
+
} else {
|
|
369
|
+
// Regular ladderized select value
|
|
370
|
+
selectedListItems.value = selectMenuList.value.filter((item) => {
|
|
371
|
+
// Convert both to strings for comparison or check direct equality for numbers
|
|
372
|
+
if (typeof item.value === 'number') {
|
|
373
|
+
return valueData.some((v) => v.original === item.value || v.string === String(item.value));
|
|
374
|
+
} else {
|
|
375
|
+
return valueStrings.includes(String(item.value));
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
} else {
|
|
380
|
+
// Regular Select value
|
|
381
|
+
selectedListItems.value = selectMenuList.value.filter((item) => {
|
|
382
|
+
// Handle objects with _originalObject property
|
|
383
|
+
if ('_originalObject' in item && item._originalObject) {
|
|
384
|
+
return valueData.some((v) => {
|
|
385
|
+
// If both are objects, compare by JSON string or by ID
|
|
386
|
+
if (v.isObject && typeof v.original === 'object') {
|
|
387
|
+
const originalObj = item._originalObject as Record<string, unknown>;
|
|
388
|
+
|
|
389
|
+
// First try direct equality comparison
|
|
390
|
+
if (v.original === originalObj) return true;
|
|
391
|
+
|
|
392
|
+
// Try JSON string comparison
|
|
393
|
+
const itemJson = JSON.stringify(originalObj);
|
|
394
|
+
if (v.string === itemJson) return true;
|
|
395
|
+
|
|
396
|
+
// Try ID-based comparison if both have ID fields
|
|
397
|
+
if (v.id !== undefined && 'id' in originalObj) {
|
|
398
|
+
return v.id === originalObj.id;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return false;
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Handle both numeric and string values correctly
|
|
406
|
+
if (typeof item.value === 'number') {
|
|
407
|
+
return valueData.some((v) => v.original === item.value || v.string === String(item.value));
|
|
408
|
+
} else {
|
|
409
|
+
return valueStrings.includes(String(item.value));
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
watch(selectModel, () => {
|
|
416
|
+
updateSelectedItemsFromValue();
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
watch(selectMenuList, () => {
|
|
420
|
+
updateSelectedItemsFromValue();
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
onMounted(() => {
|
|
424
|
+
processMenuList();
|
|
425
|
+
|
|
426
|
+
// Set initial selected items based on model value
|
|
427
|
+
if (normalizedValue.value.length > 0) {
|
|
428
|
+
updateSelectedItemsFromValue();
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
return {
|
|
433
|
+
selectPopperState,
|
|
434
|
+
selectRef,
|
|
435
|
+
inputText,
|
|
436
|
+
selectMenuList,
|
|
437
|
+
isSelectPopperDisabled,
|
|
438
|
+
selectedListItems,
|
|
439
|
+
handleSelectedItem,
|
|
440
|
+
handleSelectedLadderizedItem,
|
|
441
|
+
selectModel: compatPreSelectedItems, // Use compatible format for lists
|
|
442
|
+
removeCurrentLevelInBackLabel,
|
|
443
|
+
isLadderizedSearch,
|
|
444
|
+
};
|
|
445
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Example usage of select with primitive number values in multi-select mode
|
|
2
|
+
|
|
3
|
+
<template>
|
|
4
|
+
<div>
|
|
5
|
+
<h2>Multiple Number Values Demo</h2>
|
|
6
|
+
<p>Selected values: {{ displaySelection }}</p>
|
|
7
|
+
|
|
8
|
+
<div class="select-container">
|
|
9
|
+
<spr-select
|
|
10
|
+
id="number-multi-select"
|
|
11
|
+
v-model="selectedNumbers"
|
|
12
|
+
:menu-list="numberOptions"
|
|
13
|
+
multi-select
|
|
14
|
+
@update:model-value="handleSelectedItems"
|
|
15
|
+
>
|
|
16
|
+
<spr-input v-model="displayText" label="Select Numbers" readonly placeholder="Select numbers..." />
|
|
17
|
+
</spr-select>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div class="mt-4">
|
|
21
|
+
<h3>Current selection type:</h3>
|
|
22
|
+
<pre>{{ typeof selectedNumbers[0] }}</pre>
|
|
23
|
+
|
|
24
|
+
<h3>Current selection:</h3>
|
|
25
|
+
{{ selectedNumbers }}
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script setup>
|
|
31
|
+
import { ref, computed } from 'vue';
|
|
32
|
+
import SprInput from '@/components/input/input.vue';
|
|
33
|
+
import SprSelect from '@/components/select/select.vue';
|
|
34
|
+
|
|
35
|
+
// Define number options - raw number values
|
|
36
|
+
const numberOptions = [
|
|
37
|
+
{ text: 'One', value: 1 },
|
|
38
|
+
{ text: 'Two', value: 2 },
|
|
39
|
+
{ text: 'Three', value: 3 },
|
|
40
|
+
{ text: 'Four', value: 4 },
|
|
41
|
+
{ text: 'Five', value: 5 },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
// Track selected numbers
|
|
45
|
+
const selectedNumbers = ref([]);
|
|
46
|
+
const displayText = ref('');
|
|
47
|
+
|
|
48
|
+
// Display the selection summary
|
|
49
|
+
const displaySelection = computed(() => {
|
|
50
|
+
if (selectedNumbers.value.length === 0) {
|
|
51
|
+
return 'None';
|
|
52
|
+
}
|
|
53
|
+
return selectedNumbers.value.join(', ');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Handle selected items and update display text
|
|
57
|
+
const handleSelectedItems = (items) => {
|
|
58
|
+
// For multi-select, update display text to show selected items
|
|
59
|
+
const selectedTexts = items.map((itemValue) => {
|
|
60
|
+
// Find corresponding text for each selected value
|
|
61
|
+
const option = numberOptions.find((opt) => opt.value === itemValue);
|
|
62
|
+
return option ? option.text : itemValue;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Update the input display text
|
|
66
|
+
displayText.value = selectedTexts.join(', ');
|
|
67
|
+
|
|
68
|
+
console.log('Selected values:', items);
|
|
69
|
+
console.log('Type of first value:', typeof items[0]);
|
|
70
|
+
};
|
|
71
|
+
</script>
|