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
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="spr-font-main">
|
|
3
|
+
<div v-if="props.searchableMenu" class="spr-mb-3 spr-grid spr-gap-3">
|
|
4
|
+
<spr-input
|
|
5
|
+
v-model="searchText"
|
|
6
|
+
:placeholder="props.searchableMenuPlaceholder"
|
|
7
|
+
autocomplete="off"
|
|
8
|
+
@keyup="handleSearch"
|
|
9
|
+
/>
|
|
10
|
+
|
|
11
|
+
<div v-if="isParentMenu" class="spr-background-color-surface spr-h-[1px]"></div>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
3
14
|
<template v-if="props.groupItemsBy">
|
|
4
15
|
<div class="spr-grid spr-gap-2">
|
|
5
16
|
<div v-for="(list, listIndex) in groupedMenuList" :key="listIndex" class="spr-grid spr-gap-0.5">
|
|
@@ -46,11 +57,21 @@
|
|
|
46
57
|
@click="handleSelectedItem(item)"
|
|
47
58
|
>
|
|
48
59
|
<spr-checkbox v-if="props.multiSelect" :disabled="item.disabled" :checked="isItemSelected(item)" />
|
|
49
|
-
<div
|
|
60
|
+
<div
|
|
61
|
+
:class="[
|
|
62
|
+
'spr-flex spr-flex-auto spr-flex-col spr-justify-start',
|
|
63
|
+
{ 'spr-text-color-disabled': item.disabled },
|
|
64
|
+
]"
|
|
65
|
+
>
|
|
50
66
|
<span class="spr-text-left spr-text-xs">{{ item.text }}</span>
|
|
51
|
-
<span
|
|
52
|
-
item.subtext
|
|
53
|
-
|
|
67
|
+
<span
|
|
68
|
+
v-if="item.subtext"
|
|
69
|
+
:class="[
|
|
70
|
+
'spr-body-xs-regular spr-text-color-base spr-text-left',
|
|
71
|
+
{ 'spr-text-color-disabled': item.disabled },
|
|
72
|
+
]"
|
|
73
|
+
>{{ item.subtext }}</span
|
|
74
|
+
>
|
|
54
75
|
</div>
|
|
55
76
|
<Icon
|
|
56
77
|
v-if="isItemSelected(item) && !props.multiSelect"
|
|
@@ -73,13 +94,20 @@ import { Icon } from '@iconify/vue';
|
|
|
73
94
|
import { listPropTypes, listEmitTypes } from './list';
|
|
74
95
|
import { useList } from './use-list';
|
|
75
96
|
|
|
76
|
-
import SprCheckbox from '
|
|
97
|
+
import SprCheckbox from '@/components/checkbox/checkbox.vue';
|
|
98
|
+
import SprInput from '@/components/input/input.vue';
|
|
77
99
|
|
|
78
100
|
const props = defineProps(listPropTypes);
|
|
79
101
|
const emit = defineEmits(listEmitTypes);
|
|
80
102
|
|
|
81
|
-
const {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
103
|
+
const {
|
|
104
|
+
searchText,
|
|
105
|
+
localizedMenuList,
|
|
106
|
+
groupedMenuList,
|
|
107
|
+
isParentMenu,
|
|
108
|
+
isItemSelected,
|
|
109
|
+
getListItemClasses,
|
|
110
|
+
handleSearch,
|
|
111
|
+
handleSelectedItem,
|
|
112
|
+
} = useList(props, emit);
|
|
85
113
|
</script>
|
|
@@ -13,7 +13,7 @@ interface ListClasses {
|
|
|
13
13
|
export const useList = (props: ListPropTypes, emit: SetupContext<ListEmitTypes>['emit']) => {
|
|
14
14
|
const selectedItems = useVModel(props, 'modelValue', emit);
|
|
15
15
|
|
|
16
|
-
const { menuList, groupItemsBy, multiSelect, preSelectedItems } = toRefs(props);
|
|
16
|
+
const { menuList, menuLevel, groupItemsBy, multiSelect, preSelectedItems } = toRefs(props);
|
|
17
17
|
|
|
18
18
|
const listClasses: ComputedRef<ListClasses> = computed(() => {
|
|
19
19
|
const listItemClasses = classNames(
|
|
@@ -26,6 +26,8 @@ export const useList = (props: ListPropTypes, emit: SetupContext<ListEmitTypes>[
|
|
|
26
26
|
return { listItemClasses };
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
+
const searchText = ref<string>('');
|
|
30
|
+
|
|
29
31
|
const localizedMenuList = ref<MenuListType[]>([]);
|
|
30
32
|
const groupedMenuList = ref<GroupedMenuListType[]>([
|
|
31
33
|
{
|
|
@@ -35,66 +37,72 @@ export const useList = (props: ListPropTypes, emit: SetupContext<ListEmitTypes>[
|
|
|
35
37
|
]);
|
|
36
38
|
|
|
37
39
|
// #region - Helper Methods
|
|
40
|
+
const isParentMenu = computed(() => menuLevel.value === 0);
|
|
41
|
+
|
|
38
42
|
const isItemSelected = (item: MenuListType) => {
|
|
39
43
|
// First check standard selection via the selectedItems array
|
|
40
44
|
const directSelected = selectedItems.value.some((selectedItem) => {
|
|
41
45
|
// Compare both text and value properties to handle different value types
|
|
42
46
|
if (selectedItem.text === item.text) return true;
|
|
43
|
-
|
|
47
|
+
|
|
44
48
|
// Ensure comparison works for both string and number values
|
|
45
49
|
const selectedItemValue = selectedItem.value;
|
|
46
50
|
const itemValue = item.value;
|
|
47
|
-
|
|
51
|
+
|
|
48
52
|
// For primitives, use string comparison to handle number-string comparison properly
|
|
49
53
|
if (typeof selectedItemValue !== 'object' && typeof itemValue !== 'object') {
|
|
50
54
|
return String(selectedItemValue) === String(itemValue);
|
|
51
55
|
}
|
|
52
|
-
|
|
56
|
+
|
|
53
57
|
// For objects, use JSON.stringify for comparison (will match for equality)
|
|
54
|
-
if (
|
|
55
|
-
|
|
58
|
+
if (
|
|
59
|
+
typeof selectedItemValue === 'object' &&
|
|
60
|
+
selectedItemValue !== null &&
|
|
61
|
+
typeof itemValue === 'object' &&
|
|
62
|
+
itemValue !== null
|
|
63
|
+
) {
|
|
56
64
|
return JSON.stringify(selectedItemValue) === JSON.stringify(itemValue);
|
|
57
65
|
}
|
|
58
|
-
|
|
66
|
+
|
|
59
67
|
return false;
|
|
60
68
|
});
|
|
61
|
-
|
|
69
|
+
|
|
62
70
|
if (directSelected) return true;
|
|
63
|
-
|
|
71
|
+
|
|
64
72
|
// Additional check for objects stored in _originalObject property
|
|
65
73
|
if ('_originalObject' in item && item._originalObject && preSelectedItems.value?.length) {
|
|
66
|
-
return preSelectedItems.value.some(preSelectedValue => {
|
|
74
|
+
return preSelectedItems.value.some((preSelectedValue) => {
|
|
67
75
|
// Direct reference comparison (most accurate)
|
|
68
76
|
if (preSelectedValue === item._originalObject) {
|
|
69
77
|
return true;
|
|
70
78
|
}
|
|
71
|
-
|
|
79
|
+
|
|
72
80
|
// If both are objects, compare their serialized forms
|
|
73
81
|
if (typeof preSelectedValue === 'object' && preSelectedValue !== null) {
|
|
74
82
|
const originalObj = item._originalObject as Record<string, unknown>;
|
|
75
|
-
|
|
83
|
+
|
|
76
84
|
if (typeof originalObj === 'object') {
|
|
77
85
|
// First try comparing by ID for more reliable object comparison
|
|
78
86
|
if ('id' in preSelectedValue && 'id' in originalObj) {
|
|
79
87
|
return preSelectedValue.id === originalObj.id;
|
|
80
88
|
}
|
|
81
|
-
|
|
89
|
+
|
|
82
90
|
// Fallback to full object comparison
|
|
83
91
|
const valString = JSON.stringify(preSelectedValue);
|
|
84
92
|
const itemString = JSON.stringify(originalObj);
|
|
85
93
|
return valString === itemString;
|
|
86
94
|
}
|
|
87
|
-
|
|
95
|
+
|
|
88
96
|
// If object has an id field, check if it matches with the item value
|
|
89
97
|
if ('id' in preSelectedValue) {
|
|
90
98
|
return String(item.value).includes(String(preSelectedValue.id));
|
|
91
99
|
}
|
|
92
100
|
}
|
|
93
|
-
|
|
101
|
+
|
|
94
102
|
return false;
|
|
95
103
|
});
|
|
96
104
|
}
|
|
97
|
-
|
|
105
|
+
|
|
98
106
|
return false;
|
|
99
107
|
};
|
|
100
108
|
|
|
@@ -159,52 +167,48 @@ export const useList = (props: ListPropTypes, emit: SetupContext<ListEmitTypes>[
|
|
|
159
167
|
// For objects, check for matching _originalObject properties
|
|
160
168
|
if (typeof preSelectedItem === 'object' && preSelectedItem !== null) {
|
|
161
169
|
// Try to find an item with a matching _originalObject
|
|
162
|
-
const objectMatch = localizedMenuList.value.find(menuItem => {
|
|
170
|
+
const objectMatch = localizedMenuList.value.find((menuItem) => {
|
|
163
171
|
if (!menuItem._originalObject) return false;
|
|
164
|
-
|
|
172
|
+
|
|
165
173
|
// Compare serialized versions for deep equality
|
|
166
174
|
return JSON.stringify(menuItem._originalObject) === JSON.stringify(preSelectedItem);
|
|
167
175
|
});
|
|
168
|
-
|
|
176
|
+
|
|
169
177
|
if (objectMatch) return objectMatch;
|
|
170
|
-
|
|
178
|
+
|
|
171
179
|
// If no direct object match, try matching on ID if both have it
|
|
172
180
|
if ('id' in preSelectedItem) {
|
|
173
|
-
const idMatch = localizedMenuList.value.find(menuItem => {
|
|
181
|
+
const idMatch = localizedMenuList.value.find((menuItem) => {
|
|
174
182
|
if (menuItem._originalObject && 'id' in menuItem._originalObject) {
|
|
175
183
|
return menuItem._originalObject.id === preSelectedItem.id;
|
|
176
184
|
}
|
|
177
185
|
// Also check if the value field contains a stringified version that includes the id
|
|
178
186
|
return String(menuItem.value).includes(String(preSelectedItem.id));
|
|
179
187
|
});
|
|
180
|
-
|
|
188
|
+
|
|
181
189
|
if (idMatch) return idMatch;
|
|
182
190
|
}
|
|
183
191
|
}
|
|
184
|
-
|
|
192
|
+
|
|
185
193
|
// First try direct value comparison (for exact matches)
|
|
186
|
-
const directMatch = localizedMenuList.value.find(
|
|
187
|
-
(menuItem) => menuItem.value === preSelectedItem
|
|
188
|
-
);
|
|
194
|
+
const directMatch = localizedMenuList.value.find((menuItem) => menuItem.value === preSelectedItem);
|
|
189
195
|
if (directMatch) return directMatch;
|
|
190
|
-
|
|
196
|
+
|
|
191
197
|
// Special handling for number values in the preSelectedItems array
|
|
192
198
|
if (typeof preSelectedItem === 'number') {
|
|
193
199
|
// Find items that match the number value either directly or as a string
|
|
194
200
|
const numericMatch = localizedMenuList.value.find(
|
|
195
|
-
(menuItem) =>
|
|
201
|
+
(menuItem) =>
|
|
196
202
|
// Match if menuItem.value is the same number
|
|
197
203
|
(typeof menuItem.value === 'number' && menuItem.value === preSelectedItem) ||
|
|
198
204
|
// Match if menuItem.value is a string representation of the number
|
|
199
|
-
(typeof menuItem.value === 'string' && menuItem.value === String(preSelectedItem))
|
|
205
|
+
(typeof menuItem.value === 'string' && menuItem.value === String(preSelectedItem)),
|
|
200
206
|
);
|
|
201
207
|
if (numericMatch) return numericMatch;
|
|
202
208
|
}
|
|
203
|
-
|
|
209
|
+
|
|
204
210
|
// Then try string comparison for cases where types differ (string vs number)
|
|
205
|
-
return localizedMenuList.value.find(
|
|
206
|
-
(menuItem) => String(menuItem.value) === String(preSelectedItem)
|
|
207
|
-
);
|
|
211
|
+
return localizedMenuList.value.find((menuItem) => String(menuItem.value) === String(preSelectedItem));
|
|
208
212
|
})
|
|
209
213
|
.filter(Boolean) as MenuListType[];
|
|
210
214
|
|
|
@@ -229,50 +233,112 @@ export const useList = (props: ListPropTypes, emit: SetupContext<ListEmitTypes>[
|
|
|
229
233
|
const getListItemClasses = (item: MenuListType) => ({
|
|
230
234
|
[listClasses.value.listItemClasses]: !item.disabled,
|
|
231
235
|
'spr-background-color-single-active': isItemSelected(item) && !item.disabled,
|
|
232
|
-
'hover:spr-cursor-not-allowed spr-flex spr-cursor-pointer spr-items-center spr-gap-1.5 spr-rounded-lg spr-p-2':
|
|
236
|
+
'hover:spr-cursor-not-allowed spr-flex spr-cursor-pointer spr-items-center spr-gap-1.5 spr-rounded-lg spr-p-2':
|
|
237
|
+
item.disabled,
|
|
233
238
|
});
|
|
234
239
|
|
|
240
|
+
const handleSearch = () => {
|
|
241
|
+
const search = searchText.value.trim().toLowerCase();
|
|
242
|
+
|
|
243
|
+
if (!search) {
|
|
244
|
+
setMenuList();
|
|
245
|
+
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Filter items by text or subtext
|
|
250
|
+
const filtered = props.menuList.filter((item) => {
|
|
251
|
+
const textMatch = item.text.toLowerCase().includes(search);
|
|
252
|
+
const subtextMatch = item.subtext ? item.subtext.toLowerCase().includes(search) : false;
|
|
253
|
+
|
|
254
|
+
return textMatch || subtextMatch;
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
localizedMenuList.value = filtered;
|
|
258
|
+
|
|
259
|
+
// If grouping is enabled, regroup the filtered list
|
|
260
|
+
if (groupItemsBy?.value) {
|
|
261
|
+
groupedMenuList.value = [{ groupLabel: 'no-group', items: [] }];
|
|
262
|
+
if (groupItemsBy.value === 'default') {
|
|
263
|
+
filtered.forEach((item) => {
|
|
264
|
+
const groupKey = item.group || 'no-group';
|
|
265
|
+
|
|
266
|
+
if (groupedMenuList.value.some((g) => g.groupLabel === groupKey)) {
|
|
267
|
+
groupedMenuList.value.find((g) => g.groupLabel === groupKey)?.items.push(item);
|
|
268
|
+
} else {
|
|
269
|
+
groupedMenuList.value.push({ groupLabel: groupKey, items: [item] });
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
} else {
|
|
273
|
+
filtered
|
|
274
|
+
.sort((a, b) => {
|
|
275
|
+
if (groupItemsBy.value === 'A-Z') return a.text.localeCompare(b.text);
|
|
276
|
+
if (groupItemsBy.value === 'Z-A') return b.text.localeCompare(a.text);
|
|
277
|
+
|
|
278
|
+
return 0;
|
|
279
|
+
})
|
|
280
|
+
.forEach((item) => {
|
|
281
|
+
const firstCharacter = item.text.charAt(0);
|
|
282
|
+
const groupKey = /^\d/.test(firstCharacter) ? 'no-group' : firstCharacter.toUpperCase();
|
|
283
|
+
|
|
284
|
+
if (groupedMenuList.value.some((g) => g.groupLabel === groupKey)) {
|
|
285
|
+
groupedMenuList.value.find((g) => g.groupLabel === groupKey)?.items.push(item);
|
|
286
|
+
} else {
|
|
287
|
+
groupedMenuList.value.push({ groupLabel: groupKey, items: [item] });
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
235
294
|
const handleSelectedItem = (item: MenuListType) => {
|
|
236
|
-
if(item.disabled) return;
|
|
295
|
+
if (item.disabled) return;
|
|
237
296
|
|
|
238
297
|
if (multiSelect.value) {
|
|
239
298
|
// For multi-select, check if item is already selected
|
|
240
299
|
const index = selectedItems.value.findIndex((selectedItem: MenuListType) => {
|
|
241
300
|
// Compare text values first for simple match
|
|
242
301
|
if (selectedItem.text === item.text) return true;
|
|
243
|
-
|
|
302
|
+
|
|
244
303
|
// Compare primitive values with string conversion for compatibility
|
|
245
304
|
if (typeof selectedItem.value !== 'object' && typeof item.value !== 'object') {
|
|
246
305
|
return String(selectedItem.value) === String(item.value);
|
|
247
306
|
}
|
|
248
|
-
|
|
307
|
+
|
|
249
308
|
// For objects, compare their JSON string representations
|
|
250
|
-
if (
|
|
251
|
-
|
|
309
|
+
if (
|
|
310
|
+
typeof selectedItem.value === 'object' &&
|
|
311
|
+
selectedItem.value !== null &&
|
|
312
|
+
typeof item.value === 'object' &&
|
|
313
|
+
item.value !== null
|
|
314
|
+
) {
|
|
252
315
|
return JSON.stringify(selectedItem.value) === JSON.stringify(item.value);
|
|
253
316
|
}
|
|
254
|
-
|
|
317
|
+
|
|
255
318
|
// Compare _originalObject if available (most reliable for complex objects)
|
|
256
|
-
if (
|
|
257
|
-
|
|
258
|
-
|
|
319
|
+
if (
|
|
320
|
+
'_originalObject' in selectedItem &&
|
|
321
|
+
selectedItem._originalObject &&
|
|
322
|
+
'_originalObject' in item &&
|
|
323
|
+
item._originalObject
|
|
324
|
+
) {
|
|
259
325
|
// Direct reference equality check (fastest)
|
|
260
326
|
if (selectedItem._originalObject === item._originalObject) {
|
|
261
327
|
return true;
|
|
262
328
|
}
|
|
263
|
-
|
|
329
|
+
|
|
264
330
|
// ID-based comparison (reliable for objects with IDs)
|
|
265
331
|
const selectedObj = selectedItem._originalObject as Record<string, unknown>;
|
|
266
332
|
const itemObj = item._originalObject as Record<string, unknown>;
|
|
267
|
-
|
|
333
|
+
|
|
268
334
|
if ('id' in selectedObj && 'id' in itemObj) {
|
|
269
335
|
return selectedObj.id === itemObj.id;
|
|
270
336
|
}
|
|
271
|
-
|
|
337
|
+
|
|
272
338
|
// Full JSON comparison (most comprehensive but slower)
|
|
273
339
|
return JSON.stringify(selectedItem._originalObject) === JSON.stringify(item._originalObject);
|
|
274
340
|
}
|
|
275
|
-
|
|
341
|
+
|
|
276
342
|
return false;
|
|
277
343
|
});
|
|
278
344
|
|
|
@@ -306,11 +372,14 @@ export const useList = (props: ListPropTypes, emit: SetupContext<ListEmitTypes>[
|
|
|
306
372
|
});
|
|
307
373
|
|
|
308
374
|
return {
|
|
375
|
+
searchText,
|
|
309
376
|
listClasses,
|
|
310
377
|
localizedMenuList,
|
|
311
378
|
groupedMenuList,
|
|
379
|
+
isParentMenu,
|
|
312
380
|
isItemSelected,
|
|
313
381
|
getListItemClasses,
|
|
382
|
+
handleSearch,
|
|
314
383
|
handleSelectedItem,
|
|
315
384
|
};
|
|
316
385
|
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { PropType, ExtractPropTypes } from 'vue';
|
|
2
|
+
import type { MenuListType } from '@/components/list/list';
|
|
3
|
+
|
|
4
|
+
const PLACEMENTS_TYPES = [
|
|
5
|
+
'auto',
|
|
6
|
+
'auto-start',
|
|
7
|
+
'auto-end',
|
|
8
|
+
'top',
|
|
9
|
+
'top-start',
|
|
10
|
+
'top-end',
|
|
11
|
+
'right',
|
|
12
|
+
'right-start',
|
|
13
|
+
'right-end',
|
|
14
|
+
'bottom',
|
|
15
|
+
'bottom-start',
|
|
16
|
+
'bottom-end',
|
|
17
|
+
'left',
|
|
18
|
+
'left-start',
|
|
19
|
+
'left-end',
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
const POPPER_STRATEGY_TYPES = ['fixed', 'absolute'] as const;
|
|
23
|
+
|
|
24
|
+
export const selectLadderizedPropTypes = {
|
|
25
|
+
id: {
|
|
26
|
+
type: String,
|
|
27
|
+
required: true,
|
|
28
|
+
},
|
|
29
|
+
modelValue: {
|
|
30
|
+
type: Array as PropType<string[]>,
|
|
31
|
+
default: () => [],
|
|
32
|
+
},
|
|
33
|
+
menuList: {
|
|
34
|
+
type: Array as PropType<MenuListType[]>,
|
|
35
|
+
required: true,
|
|
36
|
+
default: () => [],
|
|
37
|
+
},
|
|
38
|
+
label: {
|
|
39
|
+
type: String,
|
|
40
|
+
default: '',
|
|
41
|
+
},
|
|
42
|
+
placeholder: {
|
|
43
|
+
type: String,
|
|
44
|
+
default: '',
|
|
45
|
+
},
|
|
46
|
+
helperText: {
|
|
47
|
+
type: String,
|
|
48
|
+
default: '',
|
|
49
|
+
},
|
|
50
|
+
helperIcon: {
|
|
51
|
+
type: String,
|
|
52
|
+
default: null,
|
|
53
|
+
},
|
|
54
|
+
displayHelper: {
|
|
55
|
+
type: Boolean,
|
|
56
|
+
default: false,
|
|
57
|
+
},
|
|
58
|
+
clearable: {
|
|
59
|
+
type: Boolean,
|
|
60
|
+
default: false,
|
|
61
|
+
},
|
|
62
|
+
searchableMenu: {
|
|
63
|
+
type: Boolean,
|
|
64
|
+
default: false,
|
|
65
|
+
},
|
|
66
|
+
searchableMenuPlaceholder: {
|
|
67
|
+
type: String,
|
|
68
|
+
default: 'Search...',
|
|
69
|
+
},
|
|
70
|
+
placement: {
|
|
71
|
+
type: String as PropType<(typeof PLACEMENTS_TYPES)[number]>,
|
|
72
|
+
validator: (value: (typeof PLACEMENTS_TYPES)[number]) => PLACEMENTS_TYPES.includes(value),
|
|
73
|
+
default: 'bottom',
|
|
74
|
+
},
|
|
75
|
+
wrapperPosition: {
|
|
76
|
+
type: String,
|
|
77
|
+
default: 'relative',
|
|
78
|
+
},
|
|
79
|
+
width: {
|
|
80
|
+
type: String,
|
|
81
|
+
default: '100%',
|
|
82
|
+
},
|
|
83
|
+
popperWidth: {
|
|
84
|
+
type: String,
|
|
85
|
+
default: '100%',
|
|
86
|
+
},
|
|
87
|
+
popperStrategy: {
|
|
88
|
+
type: String,
|
|
89
|
+
validator: (value: 'fixed' | 'absolute') => POPPER_STRATEGY_TYPES.includes(value),
|
|
90
|
+
default: 'absolute',
|
|
91
|
+
},
|
|
92
|
+
disabled: {
|
|
93
|
+
type: Boolean,
|
|
94
|
+
default: false,
|
|
95
|
+
},
|
|
96
|
+
removeCurrentLevelInBackLabel: {
|
|
97
|
+
type: Boolean,
|
|
98
|
+
default: false,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const selectLadderizedEmitTypes = {
|
|
103
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
104
|
+
'update:modelValue': (_value: unknown) => true,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export type SelectLadderizedPropTypes = ExtractPropTypes<typeof selectLadderizedPropTypes>;
|
|
108
|
+
export type SelectLadderizedEmitTypes = typeof selectLadderizedEmitTypes;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="ladderizedClasses.baseClasses">
|
|
3
|
+
<label v-if="props.label" :for="props.id" :class="ladderizedClasses.labelClasses">
|
|
4
|
+
{{ props.label }}
|
|
5
|
+
</label>
|
|
6
|
+
|
|
7
|
+
<Menu
|
|
8
|
+
:shown="ladderizedSelectPopperState"
|
|
9
|
+
aria-id="ladderized-select-wrapper"
|
|
10
|
+
distance="4"
|
|
11
|
+
:placement="props.placement"
|
|
12
|
+
:triggers="[]"
|
|
13
|
+
:popper-hide-triggers="[]"
|
|
14
|
+
:auto-hide="false"
|
|
15
|
+
:disabled="isLadderizedSelectPopperDisabled"
|
|
16
|
+
:container="'#ladderized-select-wrapper'"
|
|
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="spr-cursor-pointer"
|
|
30
|
+
:placeholder="props.placeholder"
|
|
31
|
+
autocomplete="off"
|
|
32
|
+
:helper-text="props.helperText"
|
|
33
|
+
:helper-icon="props.helperIcon"
|
|
34
|
+
:display-helper="props.displayHelper"
|
|
35
|
+
readonly
|
|
36
|
+
:disabled="props.disabled"
|
|
37
|
+
@keyup="handleSearch"
|
|
38
|
+
>
|
|
39
|
+
<template #icon>
|
|
40
|
+
<div class="spr-flex spr-items-center spr-gap-1">
|
|
41
|
+
<Icon
|
|
42
|
+
v-if="props.clearable && inputText"
|
|
43
|
+
class="spr-cursor-pointer"
|
|
44
|
+
icon="ph:x"
|
|
45
|
+
@click.stop="handleClear"
|
|
46
|
+
/>
|
|
47
|
+
<Icon icon="ph:caret-down" />
|
|
48
|
+
</div>
|
|
49
|
+
</template>
|
|
50
|
+
</spr-input>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div id="ladderized-select-wrapper" :style="{ width: props.popperWidth }"></div>
|
|
54
|
+
|
|
55
|
+
<template #popper>
|
|
56
|
+
<div
|
|
57
|
+
ref="ladderizedSelectRef"
|
|
58
|
+
class="spr-grid spr-max-h-[300px] spr-gap-0.5 spr-overflow-y-auto spr-overflow-x-hidden"
|
|
59
|
+
>
|
|
60
|
+
<template v-if="ladderizedSelectMenuList.length > 0">
|
|
61
|
+
<spr-ladderized-list
|
|
62
|
+
v-model="ladderizedSelectModel"
|
|
63
|
+
:ladderized="true"
|
|
64
|
+
:menu-list="ladderizedSelectMenuList"
|
|
65
|
+
:menu-level="ladderizedSelectModel.length"
|
|
66
|
+
:remove-current-level-in-back-label="props.removeCurrentLevelInBackLabel"
|
|
67
|
+
:searchable-menu="props.searchableMenu"
|
|
68
|
+
:searchable-menu-placeholder="props.searchableMenuPlaceholder"
|
|
69
|
+
@update:model-value="handleSelectedLadderizedItem"
|
|
70
|
+
/>
|
|
71
|
+
</template>
|
|
72
|
+
<template v-else>
|
|
73
|
+
<div class="spr-flex spr-items-center spr-justify-center spr-p-2 spr-text-center">
|
|
74
|
+
<span class="spr-body-sm-regular spr-m-0">No results found</span>
|
|
75
|
+
</div>
|
|
76
|
+
</template>
|
|
77
|
+
</div>
|
|
78
|
+
</template>
|
|
79
|
+
</Menu>
|
|
80
|
+
</div>
|
|
81
|
+
</template>
|
|
82
|
+
|
|
83
|
+
<script lang="ts" setup>
|
|
84
|
+
import { Menu } from 'floating-vue';
|
|
85
|
+
import { Icon } from '@iconify/vue';
|
|
86
|
+
|
|
87
|
+
import 'floating-vue/dist/style.css';
|
|
88
|
+
|
|
89
|
+
import SprInput from '@/components/input/input.vue';
|
|
90
|
+
import SprLadderizedList from '@/components/list/ladderized-list/ladderized-list.vue';
|
|
91
|
+
|
|
92
|
+
import { selectLadderizedPropTypes, selectLadderizedEmitTypes } from './select-ladderized';
|
|
93
|
+
import { useSelectLadderized } from './use-select-ladderized';
|
|
94
|
+
|
|
95
|
+
const props = defineProps(selectLadderizedPropTypes);
|
|
96
|
+
const emit = defineEmits(selectLadderizedEmitTypes);
|
|
97
|
+
|
|
98
|
+
const {
|
|
99
|
+
ladderizedClasses,
|
|
100
|
+
ladderizedSelectPopperState,
|
|
101
|
+
ladderizedSelectRef,
|
|
102
|
+
ladderizedSelectMenuList,
|
|
103
|
+
isLadderizedSelectPopperDisabled,
|
|
104
|
+
ladderizedSelectModel,
|
|
105
|
+
inputText,
|
|
106
|
+
handleSelectedLadderizedItem,
|
|
107
|
+
handleSearch,
|
|
108
|
+
handleClear,
|
|
109
|
+
handleMenuToggle,
|
|
110
|
+
} = useSelectLadderized(props, emit);
|
|
111
|
+
</script>
|