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,362 @@
|
|
|
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 { SelectPropTypes, SelectEmitTypes } from './select';
|
|
8
|
+
import type { MenuListType } from '../list/list';
|
|
9
|
+
|
|
10
|
+
interface SelectClasses {
|
|
11
|
+
baseClasses: string;
|
|
12
|
+
labelClasses: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const useSelect = (props: SelectPropTypes, emit: SetupContext<SelectEmitTypes>['emit']) => {
|
|
16
|
+
const { displayText, menuList, disabled, textField, valueField, disabledLocalSearch } = toRefs(props);
|
|
17
|
+
|
|
18
|
+
const selectClasses: ComputedRef<SelectClasses> = 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 selectRef = ref<HTMLDivElement | null>(null);
|
|
33
|
+
const selectPopperState = ref<boolean>(false);
|
|
34
|
+
const isSelectPopperDisabled = computed(() => disabled.value);
|
|
35
|
+
|
|
36
|
+
// Select Variables
|
|
37
|
+
const selectModel = useVModel(props, 'modelValue', emit);
|
|
38
|
+
const selectedListItems = ref<MenuListType[]>();
|
|
39
|
+
const selectMenuList = 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
|
+
selectPopperState.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(selectModel.value)) {
|
|
57
|
+
return selectModel.value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// If not an array but has a value, make it a single-item array
|
|
61
|
+
if (selectModel.value !== undefined && selectModel.value !== null) {
|
|
62
|
+
return [selectModel.value];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Default empty array
|
|
66
|
+
return [];
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Compatibility layer for pre-selected items (List component expects string[] format)
|
|
70
|
+
const compatPreSelectedItems = computed(() => {
|
|
71
|
+
if (selectModel.value === undefined || selectModel.value === null) return [];
|
|
72
|
+
|
|
73
|
+
if (Array.isArray(selectModel.value)) {
|
|
74
|
+
return selectModel.value;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return [selectModel.value];
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const processMenuList = () => {
|
|
81
|
+
// Handle empty array or non-array values
|
|
82
|
+
if (!menuList.value || !Array.isArray(menuList.value) || menuList.value.length === 0) {
|
|
83
|
+
selectMenuList.value = [];
|
|
84
|
+
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const firstItem = menuList.value[0];
|
|
89
|
+
|
|
90
|
+
// Handle array of strings
|
|
91
|
+
if (typeof firstItem === 'string') {
|
|
92
|
+
selectMenuList.value = (menuList.value as string[]).map((item) => ({
|
|
93
|
+
text: item,
|
|
94
|
+
value: item,
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Handle array of numbers
|
|
101
|
+
if (typeof firstItem === 'number') {
|
|
102
|
+
selectMenuList.value = (menuList.value as number[]).map((item) => ({
|
|
103
|
+
text: item.toString(),
|
|
104
|
+
value: item, // Keep the value as a number instead of converting to string
|
|
105
|
+
}));
|
|
106
|
+
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Handle array of objects with dynamic attributes
|
|
111
|
+
if (typeof firstItem === 'object' && firstItem !== null) {
|
|
112
|
+
// Check if it's already in MenuListType format
|
|
113
|
+
if ('text' in firstItem && 'value' in firstItem) {
|
|
114
|
+
selectMenuList.value = menuList.value as MenuListType[];
|
|
115
|
+
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Transform to MenuListType format using textField and valueField
|
|
120
|
+
selectMenuList.value = (menuList.value as Record<string, unknown>[]).map((item) => {
|
|
121
|
+
const displayText = item[textField.value] || 'Unnamed';
|
|
122
|
+
// Use the specified value field if available, otherwise use the entire object
|
|
123
|
+
const itemValue = valueField.value && item[valueField.value] !== undefined ? item[valueField.value] : item;
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
text: displayText,
|
|
127
|
+
value: typeof itemValue === 'object' ? JSON.stringify(itemValue) : itemValue.toString(),
|
|
128
|
+
_originalObject: item, // Store the original object for reference
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
selectMenuList.value = menuList.value as MenuListType[];
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const filteredSelectMenuList = computed(() => {
|
|
139
|
+
if (disabledLocalSearch.value) {
|
|
140
|
+
return selectMenuList.value;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const query = inputText.value.toString().toLowerCase().trim();
|
|
144
|
+
|
|
145
|
+
if (!query) return selectMenuList.value;
|
|
146
|
+
|
|
147
|
+
return selectMenuList.value.filter((item) => item.text?.toString().toLowerCase().includes(query));
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
watch(menuList, () => {
|
|
151
|
+
processMenuList();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Search handler: always emit search-string, but only filter locally if local search is enabled
|
|
155
|
+
const handleSearch = () => {
|
|
156
|
+
isSearching.value = true;
|
|
157
|
+
|
|
158
|
+
debouncedEmitSearch();
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const debouncedEmitSearch = useDebounceFn(() => {
|
|
162
|
+
emit('search-string', inputText.value);
|
|
163
|
+
}, 300);
|
|
164
|
+
|
|
165
|
+
onClickOutside(selectRef, () => {
|
|
166
|
+
selectPopperState.value = false;
|
|
167
|
+
|
|
168
|
+
// If user was searching, restore inputText from backup
|
|
169
|
+
if (isSearching.value) {
|
|
170
|
+
inputText.value = inputTextBackup.value;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
isSearching.value = false;
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
useInfiniteScroll(
|
|
177
|
+
selectRef,
|
|
178
|
+
() => {
|
|
179
|
+
emit('infinite-scroll-trigger', true);
|
|
180
|
+
},
|
|
181
|
+
{ distance: 10 },
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// Handle selected item for simple list component
|
|
185
|
+
const handleSelectedItem = (selectedItems: MenuListType[]) => {
|
|
186
|
+
if (selectedItems.length === 0) {
|
|
187
|
+
selectModel.value = '';
|
|
188
|
+
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
hasUserSelected.value = true; // User has now made a selection
|
|
193
|
+
|
|
194
|
+
const item = selectedItems[0];
|
|
195
|
+
|
|
196
|
+
// If we stored the original object, use it
|
|
197
|
+
if ('_originalObject' in item) {
|
|
198
|
+
selectModel.value = item._originalObject as Record<string, unknown>;
|
|
199
|
+
} else {
|
|
200
|
+
// For simple types, return the value (try to convert number strings to numbers)
|
|
201
|
+
const itemValue = item.value;
|
|
202
|
+
const itemText = item.text || '';
|
|
203
|
+
|
|
204
|
+
if (typeof itemValue === 'string' && !isNaN(Number(itemValue)) && itemValue.trim() !== '') {
|
|
205
|
+
selectModel.value = Number(itemValue);
|
|
206
|
+
} else {
|
|
207
|
+
selectModel.value = itemValue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
inputText.value = itemText;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Clone inputText to backup after selection
|
|
214
|
+
inputTextBackup.value = inputText.value;
|
|
215
|
+
|
|
216
|
+
// Always close select for single selection
|
|
217
|
+
setTimeout(() => {
|
|
218
|
+
selectPopperState.value = false;
|
|
219
|
+
}, 10);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// Update selected items when model value changes externally
|
|
223
|
+
const updateSelectedItemsFromValue = () => {
|
|
224
|
+
if (!selectMenuList.value.length) return;
|
|
225
|
+
|
|
226
|
+
const values = normalizedValue.value;
|
|
227
|
+
|
|
228
|
+
if (!values || !values.length) {
|
|
229
|
+
selectedListItems.value = [];
|
|
230
|
+
|
|
231
|
+
// Only set displayText if user hasn't typed anything
|
|
232
|
+
if (
|
|
233
|
+
displayText.value &&
|
|
234
|
+
!hasUserSelected.value &&
|
|
235
|
+
!isSearching.value &&
|
|
236
|
+
(!inputText.value || inputText.value === '')
|
|
237
|
+
) {
|
|
238
|
+
inputText.value = displayText.value;
|
|
239
|
+
|
|
240
|
+
// Clone displayText to backup if present
|
|
241
|
+
inputTextBackup.value = displayText.value;
|
|
242
|
+
} else if (!hasUserSelected.value && (!inputText.value || inputText.value === '')) {
|
|
243
|
+
inputText.value = '';
|
|
244
|
+
inputTextBackup.value = '';
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Store both original values and string versions for flexible matching
|
|
251
|
+
const valueData = values.map((val) => {
|
|
252
|
+
if (val === undefined || val === null) return { original: '', string: '' };
|
|
253
|
+
|
|
254
|
+
// For objects, use JSON string representation
|
|
255
|
+
if (typeof val === 'object') {
|
|
256
|
+
return {
|
|
257
|
+
original: val,
|
|
258
|
+
string: JSON.stringify(val),
|
|
259
|
+
isObject: true,
|
|
260
|
+
id: 'id' in val ? val.id : undefined,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// For numbers and strings, keep original and string versions
|
|
265
|
+
return {
|
|
266
|
+
original: val,
|
|
267
|
+
string: val.toString(),
|
|
268
|
+
isObject: false,
|
|
269
|
+
};
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Extract just string values for comparison
|
|
273
|
+
const valueStrings = valueData.map((v) => v.string);
|
|
274
|
+
|
|
275
|
+
selectedListItems.value = selectMenuList.value.filter((item) => {
|
|
276
|
+
// Handle objects with _originalObject property
|
|
277
|
+
if ('_originalObject' in item && item._originalObject) {
|
|
278
|
+
return valueData.some((v) => {
|
|
279
|
+
// If both are objects, compare by JSON string or by ID
|
|
280
|
+
if (v.isObject && typeof v.original === 'object') {
|
|
281
|
+
const originalObj = item._originalObject as Record<string, unknown>;
|
|
282
|
+
|
|
283
|
+
if (v.original === originalObj) return true;
|
|
284
|
+
|
|
285
|
+
const itemJson = JSON.stringify(originalObj);
|
|
286
|
+
|
|
287
|
+
if (v.string === itemJson) return true;
|
|
288
|
+
|
|
289
|
+
if (v.id !== undefined && 'id' in originalObj) {
|
|
290
|
+
return v.id === originalObj.id;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return false;
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Handle both numeric and string values correctly
|
|
298
|
+
if (typeof item.value === 'number') {
|
|
299
|
+
return valueData.some((v) => v.original === item.value || v.string === String(item.value));
|
|
300
|
+
} else {
|
|
301
|
+
return valueStrings.includes(String(item.value));
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Only update inputText if not searching
|
|
306
|
+
if (!isSearching.value) {
|
|
307
|
+
inputText.value = selectedListItems.value.map((item) => item.text).join(', ');
|
|
308
|
+
|
|
309
|
+
// Only use displayText.value if user hasn't selected anything yet
|
|
310
|
+
if (displayText.value && !hasUserSelected.value && (!inputText.value || inputText.value === '')) {
|
|
311
|
+
inputText.value = displayText.value;
|
|
312
|
+
inputTextBackup.value = displayText.value;
|
|
313
|
+
} else {
|
|
314
|
+
// Always update backup to match inputText if not searching
|
|
315
|
+
inputTextBackup.value = inputText.value;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const handleClear = () => {
|
|
321
|
+
emit('update:modelValue', '');
|
|
322
|
+
|
|
323
|
+
inputText.value = '';
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
watch(selectModel, () => {
|
|
327
|
+
updateSelectedItemsFromValue();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
watch(selectMenuList, () => {
|
|
331
|
+
updateSelectedItemsFromValue();
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
onMounted(() => {
|
|
335
|
+
processMenuList();
|
|
336
|
+
|
|
337
|
+
// Set initial selected items based on model value
|
|
338
|
+
if (normalizedValue.value.length > 0) {
|
|
339
|
+
updateSelectedItemsFromValue();
|
|
340
|
+
} else if (displayText.value) {
|
|
341
|
+
inputText.value = displayText.value;
|
|
342
|
+
inputTextBackup.value = displayText.value;
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
selectClasses,
|
|
348
|
+
selectPopperState,
|
|
349
|
+
handleMenuToggle,
|
|
350
|
+
selectRef,
|
|
351
|
+
selectModel: compatPreSelectedItems, // Use compatible format for lists
|
|
352
|
+
selectMenuList,
|
|
353
|
+
filteredSelectMenuList,
|
|
354
|
+
selectedListItems,
|
|
355
|
+
inputText,
|
|
356
|
+
isSelectPopperDisabled,
|
|
357
|
+
isSearching,
|
|
358
|
+
handleSelectedItem,
|
|
359
|
+
handleSearch,
|
|
360
|
+
handleClear,
|
|
361
|
+
};
|
|
362
|
+
};
|