design-system-next 2.7.45 → 2.8.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/dist/design-system-next.js +7001 -5891
- 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 +23 -1
- package/src/components/list/ladderized-list/ladderized-list.ts +12 -0
- package/src/components/list/ladderized-list/ladderized-list.vue +6 -0
- package/src/components/list/list.ts +15 -3
- package/src/components/list/list.vue +37 -9
- package/src/components/list/use-list.ts +116 -47
- package/src/components/select/select-ladderized/select-ladderized.ts +108 -0
- package/src/components/select/select-ladderized/select-ladderized.vue +111 -0
- package/src/components/select/select-ladderized/use-select-ladderized.ts +165 -0
- package/src/components/select/select-multiple/select-multiple.ts +135 -0
- package/src/components/select/select-multiple/select-multiple.vue +171 -0
- package/src/components/select/select-multiple/use-select-multiple.ts +367 -0
- package/src/components/select/select.ts +135 -0
- package/src/components/select/select.vue +168 -0
- package/src/components/select/use-select.ts +362 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { ref, toRefs, computed, ComputedRef, onMounted, watch } from 'vue';
|
|
2
|
+
import { onClickOutside, useInfiniteScroll, useVModel, useDebounceFn } from '@vueuse/core';
|
|
3
|
+
|
|
4
|
+
import classNames from 'classnames';
|
|
5
|
+
|
|
6
|
+
import type { SetupContext } from 'vue';
|
|
7
|
+
import type { MultiSelectPropTypes, MultiSelectEmitTypes } from './select-multiple';
|
|
8
|
+
import type { MenuListType } from '../../list/list';
|
|
9
|
+
|
|
10
|
+
interface MultiSelectClasses {
|
|
11
|
+
baseClasses: string;
|
|
12
|
+
labelClasses: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const useMultiSelect = (props: MultiSelectPropTypes, emit: SetupContext<MultiSelectEmitTypes>['emit']) => {
|
|
16
|
+
const { displayText, menuList, disabled, textField, valueField, disabledLocalSearch } = toRefs(props);
|
|
17
|
+
|
|
18
|
+
const multiSelectClasses: ComputedRef<MultiSelectClasses> = computed(() => {
|
|
19
|
+
const baseClasses = classNames('spr-flex spr-flex-col spr-gap-size-spacing-4xs');
|
|
20
|
+
|
|
21
|
+
const labelClasses = classNames('spr-body-sm-regular spr-text-color-strong spr-block', {
|
|
22
|
+
'spr-text-color-on-fill-disabled': disabled.value,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
baseClasses,
|
|
27
|
+
labelClasses,
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Popper Variables
|
|
32
|
+
const multiSelectRef = ref<HTMLDivElement | null>(null);
|
|
33
|
+
const multiSelectPopperState = ref<boolean>(false);
|
|
34
|
+
const isMultiSelectPopperDisabled = computed(() => disabled.value);
|
|
35
|
+
|
|
36
|
+
// Multi-Select Variables
|
|
37
|
+
const multiSelectModel = useVModel(props, 'modelValue', emit);
|
|
38
|
+
const multiSelectedListItems = ref<MenuListType[]>();
|
|
39
|
+
const multiSelectMenuList = ref<MenuListType[]>([]);
|
|
40
|
+
const hasUserSelected = ref(false);
|
|
41
|
+
|
|
42
|
+
// Input Text Variables
|
|
43
|
+
const inputText = ref<string | number>('');
|
|
44
|
+
const inputTextBackup = ref<string | number>('');
|
|
45
|
+
const isSearching = ref<boolean>(false);
|
|
46
|
+
|
|
47
|
+
const handleMenuToggle = () => {
|
|
48
|
+
multiSelectPopperState.value = true;
|
|
49
|
+
|
|
50
|
+
isSearching.value = false;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Normalized value for internal use - always an array
|
|
54
|
+
const normalizedValue = computed(() => {
|
|
55
|
+
// If already an array, use it
|
|
56
|
+
if (Array.isArray(multiSelectModel.value)) {
|
|
57
|
+
return multiSelectModel.value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// If not an array but has a value, make it a single-item array
|
|
61
|
+
if (multiSelectModel.value !== undefined && multiSelectModel.value !== null) {
|
|
62
|
+
return [multiSelectModel.value];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Default empty array
|
|
66
|
+
return [];
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const processMenuList = () => {
|
|
70
|
+
// Handle empty array or non-array values
|
|
71
|
+
if (!menuList.value || !Array.isArray(menuList.value) || menuList.value.length === 0) {
|
|
72
|
+
multiSelectMenuList.value = [];
|
|
73
|
+
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const firstItem = menuList.value[0];
|
|
78
|
+
|
|
79
|
+
// Handle array of strings
|
|
80
|
+
if (typeof firstItem === 'string') {
|
|
81
|
+
multiSelectMenuList.value = (menuList.value as string[]).map((item) => ({
|
|
82
|
+
text: item,
|
|
83
|
+
value: item,
|
|
84
|
+
}));
|
|
85
|
+
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Handle array of numbers
|
|
90
|
+
if (typeof firstItem === 'number') {
|
|
91
|
+
multiSelectMenuList.value = (menuList.value as number[]).map((item) => ({
|
|
92
|
+
text: item.toString(),
|
|
93
|
+
value: item, // Keep the value as a number instead of converting to string
|
|
94
|
+
}));
|
|
95
|
+
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Handle array of objects with dynamic attributes
|
|
100
|
+
if (typeof firstItem === 'object' && firstItem !== null) {
|
|
101
|
+
// Check if it's already in MenuListType format
|
|
102
|
+
if ('text' in firstItem && 'value' in firstItem) {
|
|
103
|
+
multiSelectMenuList.value = menuList.value as MenuListType[];
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
// Transform to MenuListType format using textField and valueField
|
|
107
|
+
multiSelectMenuList.value = (menuList.value as Record<string, unknown>[]).map((item) => {
|
|
108
|
+
// Ensure displayText is a string
|
|
109
|
+
const displayText = item[textField.value] !== undefined ? String(item[textField.value]) : 'Unnamed';
|
|
110
|
+
// Use the specified value field if available, otherwise use the entire object
|
|
111
|
+
let itemValue = valueField.value && item[valueField.value] !== undefined ? item[valueField.value] : item;
|
|
112
|
+
// If itemValue is undefined, fallback to empty string
|
|
113
|
+
if (itemValue === undefined) itemValue = '';
|
|
114
|
+
return {
|
|
115
|
+
text: displayText,
|
|
116
|
+
value: typeof itemValue === 'object' ? JSON.stringify(itemValue) : String(itemValue),
|
|
117
|
+
_originalObject: item, // Store the original object for reference
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
multiSelectMenuList.value = menuList.value as MenuListType[];
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const filteredMultiSelectMenuList = computed(() => {
|
|
127
|
+
if (disabledLocalSearch.value) {
|
|
128
|
+
return multiSelectMenuList.value;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const query = inputText.value.toString().toLowerCase().trim();
|
|
132
|
+
|
|
133
|
+
if (!query) return multiSelectMenuList.value;
|
|
134
|
+
|
|
135
|
+
return multiSelectMenuList.value.filter((item) => item.text?.toString().toLowerCase().includes(query));
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
watch(menuList, () => {
|
|
139
|
+
processMenuList();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Search handler: always emit search-string, but only filter locally if local search is enabled
|
|
143
|
+
const handleSearch = () => {
|
|
144
|
+
isSearching.value = true;
|
|
145
|
+
|
|
146
|
+
debouncedEmitSearch();
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const debouncedEmitSearch = useDebounceFn(() => {
|
|
150
|
+
emit('search-string', inputText.value);
|
|
151
|
+
}, 300);
|
|
152
|
+
|
|
153
|
+
onClickOutside(multiSelectRef, () => {
|
|
154
|
+
multiSelectPopperState.value = false;
|
|
155
|
+
// If user was searching, restore inputText from backup
|
|
156
|
+
if (isSearching.value) {
|
|
157
|
+
inputText.value = inputTextBackup.value;
|
|
158
|
+
}
|
|
159
|
+
isSearching.value = false;
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
useInfiniteScroll(
|
|
163
|
+
multiSelectRef,
|
|
164
|
+
() => {
|
|
165
|
+
emit('infinite-scroll-trigger', true);
|
|
166
|
+
},
|
|
167
|
+
{ distance: 10 },
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
// Handle multi-selected item for simple list component
|
|
171
|
+
const handleMultiSelectedItem = (multiSelectedItems: MenuListType[]) => {
|
|
172
|
+
// Get the last clicked item (assuming spr-list emits the full array in order)
|
|
173
|
+
const lastClicked = multiSelectedItems[multiSelectedItems.length - 1];
|
|
174
|
+
if (!lastClicked) return;
|
|
175
|
+
|
|
176
|
+
// Normalize value for comparison
|
|
177
|
+
let lastValue: string | number | Record<string, unknown>;
|
|
178
|
+
if ('_originalObject' in lastClicked) {
|
|
179
|
+
lastValue = lastClicked._originalObject ?? lastClicked.value;
|
|
180
|
+
} else if (typeof lastClicked.value === 'number') {
|
|
181
|
+
lastValue = lastClicked.value;
|
|
182
|
+
} else if (
|
|
183
|
+
typeof lastClicked.value === 'string' &&
|
|
184
|
+
!isNaN(Number(lastClicked.value)) &&
|
|
185
|
+
lastClicked.value.trim() !== '' &&
|
|
186
|
+
/^-?\d+(\.\d+)?$/.test(lastClicked.value)
|
|
187
|
+
) {
|
|
188
|
+
lastValue = Number(lastClicked.value);
|
|
189
|
+
} else {
|
|
190
|
+
lastValue = lastClicked.value;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Always normalize current selection to an array
|
|
194
|
+
let current: (string | number | Record<string, unknown>)[] = [];
|
|
195
|
+
if (Array.isArray(multiSelectModel.value)) {
|
|
196
|
+
current = [...multiSelectModel.value];
|
|
197
|
+
} else if (
|
|
198
|
+
multiSelectModel.value !== undefined &&
|
|
199
|
+
multiSelectModel.value !== null &&
|
|
200
|
+
multiSelectModel.value !== ''
|
|
201
|
+
) {
|
|
202
|
+
current = [multiSelectModel.value];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Find if already selected (deep compare for objects, strict for primitives)
|
|
206
|
+
const isSelected = current.some((sel) => {
|
|
207
|
+
if (typeof sel === 'object' && typeof lastValue === 'object') {
|
|
208
|
+
return JSON.stringify(sel) === JSON.stringify(lastValue);
|
|
209
|
+
}
|
|
210
|
+
return sel === lastValue;
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
if (isSelected) {
|
|
214
|
+
// Remove from selection
|
|
215
|
+
current = current.filter((sel) => {
|
|
216
|
+
if (typeof sel === 'object' && typeof lastValue === 'object') {
|
|
217
|
+
return JSON.stringify(sel) !== JSON.stringify(lastValue);
|
|
218
|
+
}
|
|
219
|
+
return sel !== lastValue;
|
|
220
|
+
});
|
|
221
|
+
} else {
|
|
222
|
+
// Add to selection
|
|
223
|
+
current.push(lastValue);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
hasUserSelected.value = true;
|
|
227
|
+
multiSelectModel.value = current;
|
|
228
|
+
multiSelectPopperState.value = true;
|
|
229
|
+
// Clone inputText to backup after selection
|
|
230
|
+
inputTextBackup.value = inputText.value;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// Update multi-selected items when model value changes externally
|
|
234
|
+
const updateMultiSelectedItemsFromValue = () => {
|
|
235
|
+
if (!multiSelectMenuList.value.length) return;
|
|
236
|
+
|
|
237
|
+
const values = normalizedValue.value;
|
|
238
|
+
|
|
239
|
+
if (!values || !values.length) {
|
|
240
|
+
multiSelectedListItems.value = [];
|
|
241
|
+
|
|
242
|
+
// Always clear inputText and backup if nothing is selected
|
|
243
|
+
inputText.value = '';
|
|
244
|
+
inputTextBackup.value = '';
|
|
245
|
+
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Store both original values and string versions for flexible matching
|
|
250
|
+
const valueData = values.map((val) => {
|
|
251
|
+
if (val === undefined || val === null) return { original: '', string: '' };
|
|
252
|
+
|
|
253
|
+
// For objects, use JSON string representation
|
|
254
|
+
if (typeof val === 'object') {
|
|
255
|
+
return {
|
|
256
|
+
original: val,
|
|
257
|
+
string: JSON.stringify(val),
|
|
258
|
+
isObject: true,
|
|
259
|
+
id: 'id' in val ? val.id : undefined,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// For numbers and strings, keep original and string versions
|
|
264
|
+
return {
|
|
265
|
+
original: val,
|
|
266
|
+
string: val.toString(),
|
|
267
|
+
isObject: false,
|
|
268
|
+
};
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Extract just string values for comparison
|
|
272
|
+
const valueStrings = valueData.map((v) => v.string);
|
|
273
|
+
|
|
274
|
+
multiSelectedListItems.value = multiSelectMenuList.value.filter((item) => {
|
|
275
|
+
// Handle objects with _originalObject property
|
|
276
|
+
if ('_originalObject' in item && item._originalObject) {
|
|
277
|
+
return valueData.some((v) => {
|
|
278
|
+
// If both are objects, compare by JSON string or by ID
|
|
279
|
+
if (v.isObject && typeof v.original === 'object') {
|
|
280
|
+
const originalObj = item._originalObject as Record<string, unknown>;
|
|
281
|
+
|
|
282
|
+
if (v.original === originalObj) return true;
|
|
283
|
+
|
|
284
|
+
const itemJson = JSON.stringify(originalObj);
|
|
285
|
+
|
|
286
|
+
if (v.string === itemJson) return true;
|
|
287
|
+
|
|
288
|
+
if (v.id !== undefined && 'id' in originalObj) {
|
|
289
|
+
return v.id === originalObj.id;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return false;
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Handle both numeric and string values correctly
|
|
297
|
+
if (typeof item.value === 'number') {
|
|
298
|
+
return valueData.some((v) => v.original === item.value || v.string === String(item.value));
|
|
299
|
+
} else {
|
|
300
|
+
return valueStrings.includes(String(item.value));
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Only update inputText if not searching
|
|
305
|
+
if (!isSearching.value) {
|
|
306
|
+
if (multiSelectedListItems.value.length > 3) {
|
|
307
|
+
inputText.value = `${multiSelectedListItems.value.length} items selected`;
|
|
308
|
+
} else {
|
|
309
|
+
inputText.value = multiSelectedListItems.value.map((item) => item.text).join(', ');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Only use displayText.value if user hasn't selected anything yet
|
|
313
|
+
if (displayText.value && !hasUserSelected.value && (!inputText.value || inputText.value === '')) {
|
|
314
|
+
inputText.value = displayText.value;
|
|
315
|
+
inputTextBackup.value = displayText.value;
|
|
316
|
+
} else {
|
|
317
|
+
// Always update backup to match inputText if not searching
|
|
318
|
+
inputTextBackup.value = inputText.value;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const handleClear = () => {
|
|
324
|
+
emit('update:modelValue', '');
|
|
325
|
+
|
|
326
|
+
inputText.value = '';
|
|
327
|
+
|
|
328
|
+
multiSelectPopperState.value = false;
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
watch(multiSelectModel, () => {
|
|
332
|
+
updateMultiSelectedItemsFromValue();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
watch(multiSelectMenuList, () => {
|
|
336
|
+
updateMultiSelectedItemsFromValue();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
onMounted(() => {
|
|
340
|
+
processMenuList();
|
|
341
|
+
|
|
342
|
+
// Set initial multi-selected items based on model value
|
|
343
|
+
if (normalizedValue.value.length > 0) {
|
|
344
|
+
updateMultiSelectedItemsFromValue();
|
|
345
|
+
} else if (displayText.value) {
|
|
346
|
+
inputText.value = displayText.value;
|
|
347
|
+
inputTextBackup.value = displayText.value;
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
multiSelectClasses,
|
|
353
|
+
multiSelectPopperState,
|
|
354
|
+
multiSelectRef,
|
|
355
|
+
multiSelectModel,
|
|
356
|
+
multiSelectMenuList,
|
|
357
|
+
filteredMultiSelectMenuList,
|
|
358
|
+
multiSelectedListItems,
|
|
359
|
+
inputText,
|
|
360
|
+
isMultiSelectPopperDisabled,
|
|
361
|
+
isSearching,
|
|
362
|
+
handleMultiSelectedItem,
|
|
363
|
+
handleSearch,
|
|
364
|
+
handleClear,
|
|
365
|
+
handleMenuToggle,
|
|
366
|
+
};
|
|
367
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
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
|
+
label: {
|
|
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
|
+
displayText: {
|
|
90
|
+
type: String,
|
|
91
|
+
default: '',
|
|
92
|
+
},
|
|
93
|
+
displayHelper: {
|
|
94
|
+
type: Boolean,
|
|
95
|
+
default: false,
|
|
96
|
+
},
|
|
97
|
+
helperIcon: {
|
|
98
|
+
type: String,
|
|
99
|
+
default: null,
|
|
100
|
+
},
|
|
101
|
+
helperText: {
|
|
102
|
+
type: String,
|
|
103
|
+
default: '',
|
|
104
|
+
},
|
|
105
|
+
disabled: {
|
|
106
|
+
type: Boolean,
|
|
107
|
+
default: false,
|
|
108
|
+
},
|
|
109
|
+
clearable: {
|
|
110
|
+
type: Boolean,
|
|
111
|
+
default: false,
|
|
112
|
+
},
|
|
113
|
+
searchable: {
|
|
114
|
+
type: Boolean,
|
|
115
|
+
default: false,
|
|
116
|
+
},
|
|
117
|
+
disabledLocalSearch: {
|
|
118
|
+
type: Boolean,
|
|
119
|
+
default: false,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const selectEmitTypes = {
|
|
124
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
125
|
+
'update:modelValue': (_value: unknown) => true,
|
|
126
|
+
|
|
127
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
128
|
+
'infinite-scroll-trigger': (_triggered: boolean) => true,
|
|
129
|
+
|
|
130
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
131
|
+
'search-string': (_search: string | number) => true,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export type SelectPropTypes = ExtractPropTypes<typeof selectPropTypes>;
|
|
135
|
+
export type SelectEmitTypes = typeof selectEmitTypes;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="selectClasses.baseClasses">
|
|
3
|
+
<label v-if="props.label" :for="id" :class="selectClasses.labelClasses">
|
|
4
|
+
{{ props.label }}
|
|
5
|
+
</label>
|
|
6
|
+
|
|
7
|
+
<Menu
|
|
8
|
+
:shown="selectPopperState"
|
|
9
|
+
aria-id="select-wrapper"
|
|
10
|
+
distance="4"
|
|
11
|
+
:placement="props.placement"
|
|
12
|
+
:triggers="[]"
|
|
13
|
+
:popper-hide-triggers="[]"
|
|
14
|
+
:auto-hide="false"
|
|
15
|
+
:disabled="isSelectPopperDisabled"
|
|
16
|
+
:container="`#${props.id}`"
|
|
17
|
+
:strategy="
|
|
18
|
+
props.popperStrategy === 'fixed' || props.popperStrategy === 'absolute' ? props.popperStrategy : 'absolute'
|
|
19
|
+
"
|
|
20
|
+
:delay="0"
|
|
21
|
+
:style="{
|
|
22
|
+
position: props.wrapperPosition,
|
|
23
|
+
width: props.width,
|
|
24
|
+
}"
|
|
25
|
+
>
|
|
26
|
+
<div @click="handleMenuToggle">
|
|
27
|
+
<spr-input
|
|
28
|
+
v-model="inputText"
|
|
29
|
+
:class="{
|
|
30
|
+
'spr-cursor-pointer': !props.searchable,
|
|
31
|
+
}"
|
|
32
|
+
:placeholder="props.placeholder"
|
|
33
|
+
:readonly="!props.searchable"
|
|
34
|
+
:disabled="props.disabled"
|
|
35
|
+
autocomplete="off"
|
|
36
|
+
:helper-text="props.helperText"
|
|
37
|
+
:helper-icon="props.helperIcon"
|
|
38
|
+
:display-helper="props.displayHelper"
|
|
39
|
+
@keyup="handleSearch"
|
|
40
|
+
>
|
|
41
|
+
<template #icon>
|
|
42
|
+
<div class="spr-flex spr-items-center spr-gap-1">
|
|
43
|
+
<Icon
|
|
44
|
+
v-if="props.clearable && inputText"
|
|
45
|
+
class="spr-cursor-pointer"
|
|
46
|
+
icon="ph:x"
|
|
47
|
+
@click.stop="handleClear"
|
|
48
|
+
/>
|
|
49
|
+
<Icon icon="ph:caret-down" />
|
|
50
|
+
</div>
|
|
51
|
+
</template>
|
|
52
|
+
</spr-input>
|
|
53
|
+
|
|
54
|
+
<select
|
|
55
|
+
v-if="selectMenuList && selectMenuList.length"
|
|
56
|
+
:value="Array.isArray(selectModel) ? selectModel[0] : selectModel"
|
|
57
|
+
data-testid="qa-hidden-select"
|
|
58
|
+
tabindex="-1"
|
|
59
|
+
aria-hidden="true"
|
|
60
|
+
hidden
|
|
61
|
+
>
|
|
62
|
+
<option v-for="item in selectMenuList" :key="item.value" :value="item.value">
|
|
63
|
+
{{ item.text }}
|
|
64
|
+
</option>
|
|
65
|
+
</select>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<div
|
|
69
|
+
:id="props.id"
|
|
70
|
+
:style="{
|
|
71
|
+
width: props.popperWidth,
|
|
72
|
+
}"
|
|
73
|
+
></div>
|
|
74
|
+
|
|
75
|
+
<template #popper>
|
|
76
|
+
<div
|
|
77
|
+
ref="selectRef"
|
|
78
|
+
class="spr-grid spr-max-h-[300px] spr-gap-0.5 spr-overflow-y-auto spr-overflow-x-hidden spr-p-2"
|
|
79
|
+
>
|
|
80
|
+
<template v-if="isSearching">
|
|
81
|
+
<template v-if="!props.disabledLocalSearch">
|
|
82
|
+
<template v-if="filteredSelectMenuList.length > 0">
|
|
83
|
+
<spr-list
|
|
84
|
+
v-model="selectedListItems"
|
|
85
|
+
:menu-list="filteredSelectMenuList"
|
|
86
|
+
:group-items-by="props.groupItemsBy"
|
|
87
|
+
:pre-selected-items="Array.isArray(selectModel) ? selectModel.flat() : [selectModel]"
|
|
88
|
+
@update:model-value="handleSelectedItem"
|
|
89
|
+
/>
|
|
90
|
+
</template>
|
|
91
|
+
<template v-else>
|
|
92
|
+
<div class="spr-flex spr-items-center spr-justify-center spr-p-2 spr-text-center">
|
|
93
|
+
<span class="spr-body-sm-regular spr-m-0">No results found</span>
|
|
94
|
+
</div>
|
|
95
|
+
</template>
|
|
96
|
+
</template>
|
|
97
|
+
<template v-else>
|
|
98
|
+
<template v-if="selectMenuList.length > 0">
|
|
99
|
+
<spr-list
|
|
100
|
+
v-model="selectedListItems"
|
|
101
|
+
:menu-list="selectMenuList"
|
|
102
|
+
:group-items-by="props.groupItemsBy"
|
|
103
|
+
:pre-selected-items="Array.isArray(selectModel) ? selectModel.flat() : [selectModel]"
|
|
104
|
+
@update:model-value="handleSelectedItem"
|
|
105
|
+
/>
|
|
106
|
+
</template>
|
|
107
|
+
<template v-else>
|
|
108
|
+
<div class="spr-flex spr-items-center spr-justify-center spr-p-2 spr-text-center">
|
|
109
|
+
<span class="spr-body-sm-regular spr-m-0">No results found</span>
|
|
110
|
+
</div>
|
|
111
|
+
</template>
|
|
112
|
+
</template>
|
|
113
|
+
</template>
|
|
114
|
+
<template v-else>
|
|
115
|
+
<template v-if="selectMenuList.length > 0">
|
|
116
|
+
<spr-list
|
|
117
|
+
v-model="selectedListItems"
|
|
118
|
+
:menu-list="selectMenuList"
|
|
119
|
+
:group-items-by="props.groupItemsBy"
|
|
120
|
+
:pre-selected-items="Array.isArray(selectModel) ? selectModel.flat() : [selectModel]"
|
|
121
|
+
@update:model-value="handleSelectedItem"
|
|
122
|
+
/>
|
|
123
|
+
</template>
|
|
124
|
+
<template v-else>
|
|
125
|
+
<div class="spr-flex spr-items-center spr-justify-center spr-p-2 spr-text-center">
|
|
126
|
+
<span class="spr-body-sm-regular spr-m-0">No results found</span>
|
|
127
|
+
</div>
|
|
128
|
+
</template>
|
|
129
|
+
</template>
|
|
130
|
+
</div>
|
|
131
|
+
</template>
|
|
132
|
+
</Menu>
|
|
133
|
+
</div>
|
|
134
|
+
</template>
|
|
135
|
+
|
|
136
|
+
<script lang="ts" setup>
|
|
137
|
+
import { Menu } from 'floating-vue';
|
|
138
|
+
import { Icon } from '@iconify/vue';
|
|
139
|
+
|
|
140
|
+
import 'floating-vue/dist/style.css';
|
|
141
|
+
|
|
142
|
+
import SprInput from '../input/input.vue';
|
|
143
|
+
import SprList from '../list/list.vue';
|
|
144
|
+
|
|
145
|
+
import { selectPropTypes, selectEmitTypes } from './select';
|
|
146
|
+
|
|
147
|
+
import { useSelect } from './use-select';
|
|
148
|
+
|
|
149
|
+
const props = defineProps(selectPropTypes);
|
|
150
|
+
const emit = defineEmits(selectEmitTypes);
|
|
151
|
+
|
|
152
|
+
const {
|
|
153
|
+
selectClasses,
|
|
154
|
+
selectPopperState,
|
|
155
|
+
selectRef,
|
|
156
|
+
selectModel,
|
|
157
|
+
selectMenuList,
|
|
158
|
+
filteredSelectMenuList,
|
|
159
|
+
selectedListItems,
|
|
160
|
+
inputText,
|
|
161
|
+
isSelectPopperDisabled,
|
|
162
|
+
isSearching,
|
|
163
|
+
handleSelectedItem,
|
|
164
|
+
handleSearch,
|
|
165
|
+
handleClear,
|
|
166
|
+
handleMenuToggle,
|
|
167
|
+
} = useSelect(props, emit);
|
|
168
|
+
</script>
|