design-system-next 2.8.0 → 2.8.1

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.
Binary file
@@ -1,7 +1,7 @@
1
1
  declare const _default: {
2
2
  "name": "design-system-next",
3
3
  "private": false,
4
- "version": "2.8.0",
4
+ "version": "2.8.1",
5
5
  "main": "./dist/design-system-next.js",
6
6
  "module": "./dist/design-system-next.js",
7
7
  "repository": {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "design-system-next",
3
3
  "private": false,
4
- "version": "2.8.0",
4
+ "version": "2.8.1",
5
5
  "main": "./dist/design-system-next.js",
6
6
  "module": "./dist/design-system-next.js",
7
7
  "repository": {
@@ -1,4 +1,4 @@
1
- import { computed, onBeforeMount, ref, toRefs } from 'vue';
1
+ import { computed, onBeforeMount, ref, toRefs, watch } from 'vue';
2
2
  import { useVModel } from '@vueuse/core';
3
3
 
4
4
  import { LadderizedListPropTypes, LadderizedListEmitTypes } from './ladderized-list';
@@ -146,6 +146,16 @@ export const useLadderizedList = (
146
146
  }
147
147
  };
148
148
 
149
+ // Watch for modelValue changes and reset selectedListItem if cleared
150
+ watch(
151
+ () => props.modelValue,
152
+ (newVal) => {
153
+ if (!newVal || (Array.isArray(newVal) && newVal.length === 0)) {
154
+ selectedListItem.value = [];
155
+ }
156
+ },
157
+ );
158
+
149
159
  onBeforeMount(() => {
150
160
  activeList.value = menuList.value;
151
161
  initializeMenuList();
@@ -124,9 +124,6 @@ export const multiSelectEmitTypes = {
124
124
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
125
125
  'update:modelValue': (_value: unknown) => true,
126
126
 
127
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
128
- 'infinite-scroll-trigger': (_triggered: boolean) => true,
129
-
130
127
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
131
128
  'search-string': (_search: string | number) => true,
132
129
  };
@@ -1,5 +1,5 @@
1
1
  import { ref, toRefs, computed, ComputedRef, onMounted, watch } from 'vue';
2
- import { onClickOutside, useInfiniteScroll, useVModel, useDebounceFn } from '@vueuse/core';
2
+ import { onClickOutside, useVModel, useDebounceFn } from '@vueuse/core';
3
3
 
4
4
  import classNames from 'classnames';
5
5
 
@@ -28,46 +28,46 @@ export const useMultiSelect = (props: MultiSelectPropTypes, emit: SetupContext<M
28
28
  };
29
29
  });
30
30
 
31
- // Popper Variables
32
31
  const multiSelectRef = ref<HTMLDivElement | null>(null);
33
32
  const multiSelectPopperState = ref<boolean>(false);
34
33
  const isMultiSelectPopperDisabled = computed(() => disabled.value);
35
34
 
36
- // Multi-Select Variables
37
35
  const multiSelectModel = useVModel(props, 'modelValue', emit);
38
- const multiSelectedListItems = ref<MenuListType[]>();
36
+ const multiSelectedListItems = ref<MenuListType[]>([]);
39
37
  const multiSelectMenuList = ref<MenuListType[]>([]);
40
38
  const hasUserSelected = ref(false);
41
39
 
42
- // Input Text Variables
43
40
  const inputText = ref<string | number>('');
44
41
  const inputTextBackup = ref<string | number>('');
45
42
  const isSearching = ref<boolean>(false);
46
43
 
44
+ /**
45
+ * Opens the multi-select dropdown menu and resets the searching state.
46
+ */
47
47
  const handleMenuToggle = () => {
48
48
  multiSelectPopperState.value = true;
49
-
50
49
  isSearching.value = false;
51
50
  };
52
51
 
53
- // Normalized value for internal use - always an array
52
+ /**
53
+ * Returns the normalized value of the model as an array for internal use.
54
+ */
54
55
  const normalizedValue = computed(() => {
55
- // If already an array, use it
56
56
  if (Array.isArray(multiSelectModel.value)) {
57
57
  return multiSelectModel.value;
58
58
  }
59
59
 
60
- // If not an array but has a value, make it a single-item array
61
60
  if (multiSelectModel.value !== undefined && multiSelectModel.value !== null) {
62
61
  return [multiSelectModel.value];
63
62
  }
64
63
 
65
- // Default empty array
66
64
  return [];
67
65
  });
68
66
 
67
+ /**
68
+ * Processes the menuList prop and normalizes it into MenuListType[] for the dropdown.
69
+ */
69
70
  const processMenuList = () => {
70
- // Handle empty array or non-array values
71
71
  if (!menuList.value || !Array.isArray(menuList.value) || menuList.value.length === 0) {
72
72
  multiSelectMenuList.value = [];
73
73
 
@@ -76,7 +76,6 @@ export const useMultiSelect = (props: MultiSelectPropTypes, emit: SetupContext<M
76
76
 
77
77
  const firstItem = menuList.value[0];
78
78
 
79
- // Handle array of strings
80
79
  if (typeof firstItem === 'string') {
81
80
  multiSelectMenuList.value = (menuList.value as string[]).map((item) => ({
82
81
  text: item,
@@ -86,43 +85,47 @@ export const useMultiSelect = (props: MultiSelectPropTypes, emit: SetupContext<M
86
85
  return;
87
86
  }
88
87
 
89
- // Handle array of numbers
90
88
  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
- }));
89
+ multiSelectMenuList.value = (menuList.value as Array<number | string | Record<string, unknown>>)
90
+ .filter((item): item is number => typeof item === 'number')
91
+ .map((item) => ({
92
+ text: item.toString(),
93
+ value: item,
94
+ }));
95
95
 
96
96
  return;
97
97
  }
98
98
 
99
- // Handle array of objects with dynamic attributes
100
99
  if (typeof firstItem === 'object' && firstItem !== null) {
101
- // Check if it's already in MenuListType format
102
100
  if ('text' in firstItem && 'value' in firstItem) {
103
101
  multiSelectMenuList.value = menuList.value as MenuListType[];
102
+
104
103
  return;
105
104
  }
106
- // Transform to MenuListType format using textField and valueField
105
+
107
106
  multiSelectMenuList.value = (menuList.value as Record<string, unknown>[]).map((item) => {
108
- // Ensure displayText is a string
109
107
  const displayText = item[textField.value] !== undefined ? String(item[textField.value]) : 'Unnamed';
110
- // Use the specified value field if available, otherwise use the entire object
108
+
111
109
  let itemValue = valueField.value && item[valueField.value] !== undefined ? item[valueField.value] : item;
112
- // If itemValue is undefined, fallback to empty string
110
+
113
111
  if (itemValue === undefined) itemValue = '';
112
+
114
113
  return {
115
114
  text: displayText,
116
115
  value: typeof itemValue === 'object' ? JSON.stringify(itemValue) : String(itemValue),
117
- _originalObject: item, // Store the original object for reference
116
+ _originalObject: item,
118
117
  };
119
118
  });
119
+
120
120
  return;
121
121
  }
122
122
 
123
123
  multiSelectMenuList.value = menuList.value as MenuListType[];
124
124
  };
125
125
 
126
+ /**
127
+ * Returns the filtered menu list based on the search input, or the full list if local search is disabled.
128
+ */
126
129
  const filteredMultiSelectMenuList = computed(() => {
127
130
  if (disabledLocalSearch.value) {
128
131
  return multiSelectMenuList.value;
@@ -135,102 +138,59 @@ export const useMultiSelect = (props: MultiSelectPropTypes, emit: SetupContext<M
135
138
  return multiSelectMenuList.value.filter((item) => item.text?.toString().toLowerCase().includes(query));
136
139
  });
137
140
 
138
- watch(menuList, () => {
139
- processMenuList();
140
- });
141
-
142
- // Search handler: always emit search-string, but only filter locally if local search is enabled
141
+ /**
142
+ * Handles the search input and emits the search-string event (debounced).
143
+ */
143
144
  const handleSearch = () => {
144
145
  isSearching.value = true;
145
146
 
146
147
  debouncedEmitSearch();
147
148
  };
148
149
 
150
+ /**
151
+ * Debounced function to emit the search-string event for remote search.
152
+ */
149
153
  const debouncedEmitSearch = useDebounceFn(() => {
150
154
  emit('search-string', inputText.value);
151
155
  }, 300);
152
156
 
157
+ /**
158
+ * Handles closing the dropdown when clicking outside, restoring input text if searching.
159
+ */
153
160
  onClickOutside(multiSelectRef, () => {
154
161
  multiSelectPopperState.value = false;
155
- // If user was searching, restore inputText from backup
156
162
  if (isSearching.value) {
157
163
  inputText.value = inputTextBackup.value;
158
164
  }
159
165
  isSearching.value = false;
160
166
  });
161
167
 
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
168
+ /**
169
+ * Handles selection changes from the dropdown and updates the model value.
170
+ * Converts stringified objects back to objects if needed.
171
+ */
171
172
  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);
173
+ const selectedValues = multiSelectedItems.map((item) => {
174
+ if (typeof item.value === 'string' && item.value.startsWith('{') && item.value.endsWith('}')) {
175
+ try {
176
+ return JSON.parse(item.value);
177
+ } catch {
178
+ return item.value;
179
+ }
209
180
  }
210
- return sel === lastValue;
181
+ return item.value;
211
182
  });
212
183
 
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
184
  hasUserSelected.value = true;
227
- multiSelectModel.value = current;
185
+ multiSelectModel.value = selectedValues;
228
186
  multiSelectPopperState.value = true;
229
- // Clone inputText to backup after selection
230
187
  inputTextBackup.value = inputText.value;
231
188
  };
232
189
 
233
- // Update multi-selected items when model value changes externally
190
+ /**
191
+ * Updates the selected items in the dropdown based on the current model value.
192
+ * Handles stringified objects and updates the input text accordingly.
193
+ */
234
194
  const updateMultiSelectedItemsFromValue = () => {
235
195
  if (!multiSelectMenuList.value.length) return;
236
196
 
@@ -238,70 +198,41 @@ export const useMultiSelect = (props: MultiSelectPropTypes, emit: SetupContext<M
238
198
 
239
199
  if (!values || !values.length) {
240
200
  multiSelectedListItems.value = [];
241
-
242
- // Always clear inputText and backup if nothing is selected
243
201
  inputText.value = '';
244
202
  inputTextBackup.value = '';
245
203
 
246
204
  return;
247
205
  }
248
206
 
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
207
  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;
208
+ return values.some((val) => {
209
+ let itemVal = item.value;
210
+ let valToCompare = val;
211
+
212
+ if (typeof itemVal === 'string' && itemVal.startsWith('{') && itemVal.endsWith('}')) {
213
+ try {
214
+ itemVal = JSON.parse(itemVal);
215
+ } catch {
216
+ // ignore
217
+ }
218
+ }
287
219
 
288
- if (v.id !== undefined && 'id' in originalObj) {
289
- return v.id === originalObj.id;
290
- }
220
+ if (typeof valToCompare === 'string' && valToCompare.startsWith('{') && valToCompare.endsWith('}')) {
221
+ try {
222
+ valToCompare = JSON.parse(valToCompare);
223
+ } catch {
224
+ // ignore
291
225
  }
292
- return false;
293
- });
294
- }
226
+ }
295
227
 
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
- }
228
+ if (typeof itemVal === 'object' && typeof valToCompare === 'object') {
229
+ return JSON.stringify(itemVal) === JSON.stringify(valToCompare);
230
+ }
231
+
232
+ return itemVal == valToCompare;
233
+ });
302
234
  });
303
235
 
304
- // Only update inputText if not searching
305
236
  if (!isSearching.value) {
306
237
  if (multiSelectedListItems.value.length > 3) {
307
238
  inputText.value = `${multiSelectedListItems.value.length} items selected`;
@@ -309,19 +240,20 @@ export const useMultiSelect = (props: MultiSelectPropTypes, emit: SetupContext<M
309
240
  inputText.value = multiSelectedListItems.value.map((item) => item.text).join(', ');
310
241
  }
311
242
 
312
- // Only use displayText.value if user hasn't selected anything yet
313
243
  if (displayText.value && !hasUserSelected.value && (!inputText.value || inputText.value === '')) {
314
244
  inputText.value = displayText.value;
315
245
  inputTextBackup.value = displayText.value;
316
- } else {
317
- // Always update backup to match inputText if not searching
246
+ } else if (hasUserSelected.value) {
318
247
  inputTextBackup.value = inputText.value;
319
248
  }
320
249
  }
321
250
  };
322
251
 
252
+ /**
253
+ * Clears the selection and input text, and closes the dropdown.
254
+ */
323
255
  const handleClear = () => {
324
- emit('update:modelValue', '');
256
+ emit('update:modelValue', []);
325
257
 
326
258
  inputText.value = '';
327
259
 
@@ -339,7 +271,6 @@ export const useMultiSelect = (props: MultiSelectPropTypes, emit: SetupContext<M
339
271
  onMounted(() => {
340
272
  processMenuList();
341
273
 
342
- // Set initial multi-selected items based on model value
343
274
  if (normalizedValue.value.length > 0) {
344
275
  updateMultiSelectedItemsFromValue();
345
276
  } else if (displayText.value) {