selectic 3.0.21 → 3.1.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.
Files changed (43) hide show
  1. package/dist/selectic.common.js +545 -67
  2. package/dist/selectic.esm.js +546 -69
  3. package/doc/changeIcons.md +118 -0
  4. package/doc/changeText.md +1 -1
  5. package/doc/domProperties.md +57 -19
  6. package/doc/extendedProperties.md +83 -72
  7. package/doc/main.md +2 -0
  8. package/doc/params.md +177 -112
  9. package/doc/properties.md +42 -0
  10. package/package.json +4 -4
  11. package/src/ExtendedList.tsx +53 -6
  12. package/src/Filter.tsx +11 -9
  13. package/src/Icon.tsx +199 -0
  14. package/src/List.tsx +12 -6
  15. package/src/MainInput.tsx +15 -11
  16. package/src/Store.tsx +290 -123
  17. package/src/css/selectic.css +24 -0
  18. package/src/icons/caret-down.tsx +21 -0
  19. package/src/icons/caret-up.tsx +21 -0
  20. package/src/icons/check.tsx +23 -0
  21. package/src/icons/question.tsx +21 -0
  22. package/src/icons/search.tsx +21 -0
  23. package/src/icons/spinner.tsx +21 -0
  24. package/src/icons/strikeThrough.tsx +21 -0
  25. package/src/icons/times.tsx +21 -0
  26. package/src/index.tsx +78 -37
  27. package/test/Store/Store_computed.spec.js +84 -0
  28. package/test/Store/changeIcons.spec.js +154 -0
  29. package/test/Store/selectGroup.spec.js +389 -0
  30. package/test/Store/selectItem.spec.js +100 -46
  31. package/test/helper.js +38 -34
  32. package/types/ExtendedList.d.ts +7 -2
  33. package/types/Icon.d.ts +25 -0
  34. package/types/Store.d.ts +142 -5
  35. package/types/icons/caret-down.d.ts +6 -0
  36. package/types/icons/caret-up.d.ts +6 -0
  37. package/types/icons/check.d.ts +6 -0
  38. package/types/icons/question.d.ts +6 -0
  39. package/types/icons/search.d.ts +6 -0
  40. package/types/icons/spinner.d.ts +6 -0
  41. package/types/icons/strikeThrough.d.ts +6 -0
  42. package/types/icons/times.d.ts +6 -0
  43. package/types/index.d.ts +74 -1
package/src/Store.tsx CHANGED
@@ -61,45 +61,45 @@ export type GetCallback = (_ids: OptionId[])
61
61
  export type FormatCallback = (_option: OptionItem) => OptionItem;
62
62
 
63
63
  export type SelectionOverflow =
64
- /* Items are reduced in width and an ellipsis is displayed in their name. */
64
+ /** Items are reduced in width and an ellipsis is displayed in their name. */
65
65
  'collapsed'
66
66
  /* The container extends in height in order to display all items. */
67
67
  | 'multiline';
68
68
 
69
69
  export type ListPosition =
70
- /* Display the list at bottom */
70
+ /** Display the list at bottom */
71
71
  'bottom'
72
- /* Display the list at bottom */
72
+ /** Display the list at bottom */
73
73
  | 'top'
74
- /* Display the list at bottom but if there is not enough space, display it at top */
74
+ /** Display the list at bottom but if there is not enough space, display it at top */
75
75
  | 'auto';
76
76
 
77
77
  export type HideFilter =
78
- /* Display or hide the filter panel */
78
+ /** Display or hide the filter panel */
79
79
  boolean
80
- /* The handler to open the filter panel is hidden only if there is less
80
+ /** The handler to open the filter panel is hidden only if there is less
81
81
  * than 10 options */
82
82
  | 'auto'
83
- /* The panel filter is always open */
83
+ /** The panel filter is always open */
84
84
  | 'open';
85
85
 
86
86
  export type SelectAllOption =
87
- /* Display the "select all" only when data are all fetched or allowRevert */
87
+ /** Display the "select all" only when data are all fetched or allowRevert */
88
88
  'auto'
89
- /* Always display the "select all" in mulitple mode. */
89
+ /** Always display the "select all" in mulitple mode. */
90
90
  | 'visible';
91
91
 
92
92
  export interface SelecticStoreStateParams {
93
- /* Equivalent of <select>'s "multiple" attribute */
93
+ /** Equivalent of <select>'s "multiple" attribute */
94
94
  multiple?: boolean;
95
95
 
96
- /* Equivalent of <input>'s "placeholder" attribute */
96
+ /** Equivalent of <input>'s "placeholder" attribute */
97
97
  placeholder?: string;
98
98
 
99
- /* Hide filter component when enabled */
99
+ /** Hide filter component when enabled */
100
100
  hideFilter?: HideFilter;
101
101
 
102
- /* Allow to reverse selection.
102
+ /** Allow to reverse selection.
103
103
  * If true, parent should support the selectionIsExcluded property.
104
104
  * If false, the action is never available.
105
105
  * If undefined, the action is available only when it is not needed to
@@ -107,26 +107,26 @@ export interface SelecticStoreStateParams {
107
107
  */
108
108
  allowRevert?: boolean;
109
109
 
110
- /* Force the availability of the "select all" even if all data is not fetched yet. */
110
+ /** Force the availability of the "select all" even if all data is not fetched yet. */
111
111
  forceSelectAll?: SelectAllOption;
112
112
 
113
- /* Allow user to clear current selection */
113
+ /** Allow user to clear current selection */
114
114
  allowClearSelection?: boolean;
115
115
 
116
- /* Number of items to retrieve in fetch request (it is possible
116
+ /** Number of items to retrieve in fetch request (it is possible
117
117
  * to fetch more items at once if several pages are requested) */
118
118
  pageSize?: number;
119
119
 
120
- /* Select the first available option */
120
+ /** Select the first available option */
121
121
  autoSelect?: boolean;
122
122
 
123
- /* Disable the select if only one option is given and must be selected. */
123
+ /** Disable the select if only one option is given and must be selected. */
124
124
  autoDisabled?: boolean;
125
125
 
126
- /* Accept only values which are in options */
126
+ /** Accept only values which are in options */
127
127
  strictValue?: boolean;
128
128
 
129
- /* Define how the component should behave when selected items are too
129
+ /** Define how the component should behave when selected items are too
130
130
  * large for the container.
131
131
  * collapsed (default): Items are reduced in width and an ellipsis
132
132
  * is displayed in their name.
@@ -135,69 +135,80 @@ export interface SelecticStoreStateParams {
135
135
  */
136
136
  selectionOverflow?: SelectionOverflow;
137
137
 
138
- /* Called when item is displayed in the list. */
138
+ /** Called when item is displayed in the list. */
139
139
  formatOption?: FormatCallback;
140
140
 
141
- /* Called when item is displayed in the selection area. */
141
+ /** Called when item is displayed in the selection area. */
142
142
  formatSelection?: FormatCallback;
143
143
 
144
- /* Described behavior when options from several sources are set (static, dynamic, slots)
144
+ /** Described behavior when options from several sources are set (static, dynamic, slots)
145
145
  * It describe what to do (sort or force)
146
146
  * and the order (O → static options, D → dynamic options, E → slot elements)
147
147
  * Example: "sort-ODE"
148
148
  */
149
149
  optionBehavior?: string;
150
150
 
151
- /* Indicate where the list should be deployed */
151
+ /** Indicate where the list should be deployed */
152
152
  listPosition?: ListPosition;
153
153
 
154
- /* If true, the component is open at start */
154
+ /** If true, the component is open at start */
155
155
  isOpen?: boolean;
156
+
157
+ /** Avoid selecting all items when clicking on group's header */
158
+ disableGroupSelection?: boolean;
156
159
  }
157
160
 
158
161
  export interface Props {
159
- /* Selected value */
162
+ /** Selected value */
160
163
  value?: SelectedValue | null;
161
164
 
162
- /* If true, the value represents the ones we don't want to select */
165
+ /** If true, the value represents the ones we don't want to select */
163
166
  selectionIsExcluded?: boolean;
164
167
 
165
- /* Equivalent of "disabled" Select's attribute */
168
+ /** Equivalent of "disabled" Select's attribute */
166
169
  disabled?: boolean;
167
170
 
168
- /* List of options to display */
171
+ /** List of options to display */
169
172
  options?: OptionProp[] | null;
170
173
 
171
- /* List of options to display from child elements */
174
+ /** List of options to display from child elements */
172
175
  childOptions?: OptionValue[];
173
176
 
174
- /* Define groups which will be used by items */
177
+ /** Define groups which will be used by items */
175
178
  groups?: GroupValue[];
176
179
 
177
- /* Overwrite default texts */
180
+ /** Overwrite default texts */
178
181
  texts?: PartialMessages | null;
179
182
 
180
- /* Keep this component open if another Selectic component opens */
183
+ /** Overwrite default icons */
184
+ icons?: PartialIcons | null;
185
+
186
+ /** Overwrite default icon family */
187
+ iconFamily?: IconFamily | null;
188
+
189
+ /** Keep this component open if another Selectic component opens */
181
190
  keepOpenWithOtherSelectic?: boolean;
182
191
 
183
- /* Selectic configuration */
192
+ /** Selectic configuration */
184
193
  params?: SelecticStoreStateParams;
185
194
 
186
- /* Method to call to fetch extra data */
195
+ /** Method to call to fetch extra data */
187
196
  fetchCallback?: FetchCallback | null;
188
197
 
189
- /* Method to call to get specific item */
198
+ /** Method to call to get specific item */
190
199
  getItemsCallback?: GetCallback | null;
191
200
  }
192
201
 
193
202
  type InternalProps = MandateProps<Props>;
194
203
 
195
204
  export interface Data {
196
- /* Number of items displayed in a page (before scrolling) */
205
+ /** Number of items displayed in a page (before scrolling) */
197
206
  itemsPerPage: number;
198
207
 
199
208
  labels: Messages;
200
- /* used to avoid checking and updating table while doing batch stuff */
209
+ icons: PartialIcons;
210
+ iconFamily: IconFamily;
211
+ /** used to avoid checking and updating table while doing batch stuff */
201
212
  doNotUpdate: boolean;
202
213
  cacheItem: Map<OptionId, OptionValue>;
203
214
  activeOrder: OptionBehaviorOrder;
@@ -205,28 +216,28 @@ export interface Data {
205
216
  }
206
217
 
207
218
  export interface SelecticStoreState {
208
- /* The current selected values */
219
+ /** The current selected values */
209
220
  internalValue: SelectedValue;
210
221
 
211
- /* If true, user wants to choose the opposite selection */
222
+ /** If true, user wants to choose the opposite selection */
212
223
  selectionIsExcluded: boolean;
213
224
 
214
- /* If true, several value can be selected */
225
+ /** If true, several value can be selected */
215
226
  multiple: boolean;
216
227
 
217
- /* If true, no change can be done by user */
228
+ /** If true, no change can be done by user */
218
229
  disabled: boolean;
219
230
 
220
- /* Define the default text to display when there is no selection */
231
+ /** Define the default text to display when there is no selection */
221
232
  placeholder: string;
222
233
 
223
- /* If true, filters and controls are hidden */
234
+ /** If true, filters and controls are hidden */
224
235
  hideFilter: boolean;
225
236
 
226
- /* If true, the filter panel is always open */
237
+ /** If true, the filter panel is always open */
227
238
  keepFilterOpen: boolean;
228
239
 
229
- /* Allow to reverse selection.
240
+ /** Allow to reverse selection.
230
241
  * If true, parent should support the selectionIsExcluded property.
231
242
  * If false, the action is never available.
232
243
  * If undefined, the action is available only when it is not needed to
@@ -234,101 +245,134 @@ export interface SelecticStoreState {
234
245
  */
235
246
  allowRevert?: boolean;
236
247
 
237
- /* If true, user can clear current selection
248
+ /** If true, user can clear current selection
238
249
  * (if false, it is still possible to clear it programmatically) */
239
250
  allowClearSelection: boolean;
240
251
 
241
- /* If false, do not select the first available option even if value is mandatory */
252
+ /** If false, do not select the first available option even if value is mandatory */
242
253
  autoSelect: boolean;
243
254
 
244
- /* If true, Selectic is disabled if there is only one mandatory option. */
255
+ /** If true, Selectic is disabled if there is only one mandatory option. */
245
256
  autoDisabled: boolean;
246
257
 
247
- /* If true, only values which are in options are accepted. */
258
+ /** If true, only values which are in options are accepted. */
248
259
  strictValue: boolean;
249
260
 
250
- /* Define how to behave when selected items are too large for container. */
261
+ /** Define how to behave when selected items are too large for container. */
251
262
  selectionOverflow: SelectionOverflow;
252
263
 
253
- /* If true, the list is displayed */
264
+ /** If true, the list is displayed */
254
265
  isOpen: boolean;
255
266
 
256
- /* Text entered by user to look for options */
267
+ /** Text entered by user to look for options */
257
268
  searchText: string;
258
269
 
259
- /* Contains all known options */
270
+ /** Contains all known options */
260
271
  allOptions: OptionValue[];
261
272
 
262
- /* Contains all fetched dynamic options */
273
+ /** Contains all fetched dynamic options */
263
274
  dynOptions: OptionValue[];
264
275
 
265
- /* Contains options which should be displayed */
276
+ /** Contains options which should be displayed */
266
277
  filteredOptions: OptionItem[];
267
278
 
268
- /* Contains options which are selected */
279
+ /** Contains options which are selected */
269
280
  selectedOptions: OptionItem | OptionItem[] | null;
270
281
 
271
- /* The total number of all options (static + dynamic + elements) without any filter */
282
+ /** The total number of all options (static + dynamic + elements) without any filter */
272
283
  totalAllOptions: number;
273
284
 
274
- /* The total number of options which can be fetched (without any filter) */
285
+ /** The total number of options which can be fetched (without any filter) */
275
286
  totalDynOptions: number;
276
287
 
277
- /* The total number of options which should be displayed (filter is applied) */
288
+ /** The total number of options which should be displayed (filter is applied) */
278
289
  totalFilteredOptions: number;
279
290
 
280
- /* Description of groups (optGroup) */
291
+ /** Description of groups (optGroup) */
281
292
  groups: Map<OptionId, string>;
282
293
 
283
- /* Starting index of options which are displayed */
294
+ /** Starting index of options which are displayed */
284
295
  offsetItem: number;
285
296
 
286
- /* Index of active item */
297
+ /** Index of active item */
287
298
  activeItemIdx: number;
288
299
 
289
- /* Number of items to fetch per page */
300
+ /** Number of items to fetch per page */
290
301
  pageSize: number;
291
302
 
292
- /* Called when item is displayed in the list. */
303
+ /** Called when item is displayed in the list. */
293
304
  formatOption?: FormatCallback;
294
305
 
295
- /* Called when item is displayed in the selection area. */
306
+ /** Called when item is displayed in the selection area. */
296
307
  formatSelection?: FormatCallback;
297
308
 
298
- /* Operation to apply when there are several sources */
309
+ /** Operation to apply when there are several sources */
299
310
  optionBehaviorOperation: OptionBehaviorOperation;
300
311
 
301
- /* Order of sources options */
312
+ /** Order of sources options */
302
313
  optionBehaviorOrder: OptionBehaviorOrder[];
303
314
 
304
- /* Indicate where the list should be deployed */
315
+ /** Indicate where the list should be deployed */
305
316
  listPosition: ListPosition;
306
317
 
307
- /* If true, the "select All" is still available even if all data are not fetched yet. */
318
+ /** If true, the "select All" is still available even if all data are not fetched yet. */
308
319
  forceSelectAll: SelectAllOption;
309
320
 
310
- /* Inner status which should be modified only by store */
321
+ /** Avoid selecting all items when clicking on group's header */
322
+ disableGroupSelection: boolean;
323
+
324
+ /** Inner status which should be modified only by store */
311
325
  status: {
312
- /* If true, a search is currently done */
326
+ /** If true, a search is currently done */
313
327
  searching: boolean;
314
328
 
315
- /* If not empty, an error happens */
329
+ /** If not empty, an error happens */
316
330
  errorMessage: string;
317
331
 
318
- /* If true it means that all options are selected */
332
+ /** If true it means that all options are selected */
319
333
  areAllSelected: boolean;
320
334
 
321
- /* If true, a change has been done by user */
335
+ /** If true, a change has been done by user */
322
336
  hasChanged: boolean;
323
337
 
324
- /* If true, it means the current change has been done automatically by Selectic */
338
+ /** If true, it means the current change has been done automatically by Selectic */
325
339
  automaticChange: boolean;
326
340
 
327
- /* If true, it means the current close has been done automatically by Selectic */
341
+ /** If true, it means the current close has been done automatically by Selectic */
328
342
  automaticClose: boolean;
329
343
  };
330
344
  }
331
345
 
346
+ export type IconFamily = ''
347
+ | 'selectic'
348
+ | 'font-awesome-4'
349
+ | 'font-awesome-5'
350
+ | 'font-awesome-6'
351
+ | 'raw'
352
+ | `prefix:${string}`
353
+ ;
354
+
355
+ export type IconKey =
356
+ | 'caret-down'
357
+ | 'caret-up'
358
+ | 'check'
359
+ | 'search'
360
+ | 'spinner'
361
+ | 'strikethrough'
362
+ | 'times'
363
+ | 'question'
364
+ | 'spin'
365
+ ;
366
+
367
+ export type IconValue =
368
+ | `selectic:${IconKey}${'' | ':spin'}`
369
+ | `raw:${string}`
370
+ | `current:${IconKey}${'' | ':spin'}`
371
+ | string
372
+ ;
373
+ export type Icons = Record<IconKey, IconValue>;
374
+ export type PartialIcons = { [K in IconKey]?: Icons[K] };
375
+
332
376
  interface Messages {
333
377
  noFetchMethod: string;
334
378
  searchPlaceholder: string;
@@ -358,6 +402,14 @@ export function changeTexts(texts: PartialMessages) {
358
402
  messages = Object.assign(messages, texts);
359
403
  }
360
404
 
405
+ export function changeIcons(newIcons: PartialIcons, newFamilyIcon?: IconFamily) {
406
+ icons = Object.assign(icons, newIcons);
407
+
408
+ if (newFamilyIcon) {
409
+ defaultFamilyIcon = newFamilyIcon;
410
+ }
411
+ }
412
+
361
413
  /* }}} */
362
414
 
363
415
  let messages: Messages = {
@@ -380,6 +432,10 @@ let messages: Messages = {
380
432
  wrongQueryResult: 'Query did not return all results.',
381
433
  };
382
434
 
435
+ let defaultFamilyIcon: IconFamily = 'selectic';
436
+ let icons: PartialIcons = {
437
+ };
438
+
383
439
  let closePreviousSelectic: undefined | voidCaller;
384
440
 
385
441
  /* }}} */
@@ -402,9 +458,12 @@ export default class SelecticStore {
402
458
  /* }}} */
403
459
  /* {{{ computed */
404
460
 
405
- /* Number of item to pre-display */
461
+ /** Number of item to pre-display */
406
462
  public marginSize: ComputedRef<number>;
407
463
 
464
+ /** If true, it is possible to click on group to select all items inside */
465
+ public allowGroupSelection: ComputedRef<boolean>;
466
+
408
467
  public isPartial: ComputedRef<boolean>;
409
468
  public hasAllItems: ComputedRef<boolean>;
410
469
  public hasFetchedAllItems: ComputedRef<boolean>;
@@ -428,6 +487,8 @@ export default class SelecticStore {
428
487
  childOptions: [],
429
488
  groups: [],
430
489
  texts: null,
490
+ icons: null,
491
+ iconFamily: null,
431
492
  params: {},
432
493
  fetchCallback: null,
433
494
  getItemsCallback: null,
@@ -440,51 +501,52 @@ export default class SelecticStore {
440
501
  /* {{{ data */
441
502
 
442
503
  this.state = reactive<SelecticStoreState>({
443
- multiple: false,
444
- disabled: false,
445
- placeholder: '',
446
- hideFilter: false,
447
- keepFilterOpen: false,
448
- allowRevert: undefined,
504
+ activeItemIdx: -1,
505
+ allOptions: [],
449
506
  allowClearSelection: false,
450
- autoSelect: true,
507
+ allowRevert: undefined,
451
508
  autoDisabled: true,
452
- strictValue: false,
453
- selectionOverflow: 'collapsed',
454
-
509
+ autoSelect: true,
510
+ disabled: false,
511
+ disableGroupSelection: false,
512
+ dynOptions: [],
513
+ filteredOptions: [],
514
+ forceSelectAll: 'auto',
515
+ groups: new Map(),
516
+ hideFilter: false,
455
517
  internalValue: null,
456
518
  isOpen: false,
519
+ keepFilterOpen: false,
520
+ listPosition: 'auto',
521
+ multiple: false,
522
+ offsetItem: 0,
523
+ optionBehaviorOperation: 'sort',
524
+ optionBehaviorOrder: ['O', 'D', 'E'],
525
+ pageSize: 100,
526
+ placeholder: '',
457
527
  searchText: '',
458
- selectionIsExcluded: false,
459
- forceSelectAll: 'auto',
460
- allOptions: [],
461
- dynOptions: [],
462
- filteredOptions: [],
463
528
  selectedOptions: null,
529
+ selectionIsExcluded: false,
530
+ selectionOverflow: 'collapsed',
531
+ strictValue: false,
464
532
  totalAllOptions: Infinity,
465
533
  totalDynOptions: Infinity,
466
534
  totalFilteredOptions: Infinity,
467
- groups: new Map(),
468
- offsetItem: 0,
469
- activeItemIdx: -1,
470
- pageSize: 100,
471
- listPosition: 'auto',
472
-
473
- optionBehaviorOperation: 'sort',
474
- optionBehaviorOrder: ['O', 'D', 'E'],
475
535
 
476
536
  status: {
477
- searching: false,
478
- errorMessage: '',
479
537
  areAllSelected: false,
480
- hasChanged: false,
481
538
  automaticChange: false,
482
539
  automaticClose: false,
540
+ errorMessage: '',
541
+ hasChanged: false,
542
+ searching: false,
483
543
  },
484
544
  });
485
545
 
486
546
  this.data = reactive({
487
547
  labels: Object.assign({}, messages),
548
+ icons: Object.assign({}, icons),
549
+ iconFamily: defaultFamilyIcon,
488
550
  itemsPerPage: 10,
489
551
  doNotUpdate: false,
490
552
  cacheItem: new Map(),
@@ -539,6 +601,10 @@ export default class SelecticStore {
539
601
  return this.getElementOptions();
540
602
  });
541
603
 
604
+ this.allowGroupSelection = computed(() => {
605
+ return this.state.multiple && !this.isPartial.value && !this.state.disableGroupSelection;
606
+ });
607
+
542
608
  /* }}} */
543
609
  /* {{{ watch */
544
610
 
@@ -585,6 +651,11 @@ export default class SelecticStore {
585
651
 
586
652
  watch(() => this.state.internalValue, () => {
587
653
  this.buildSelectedOptions();
654
+ /* If there is only one item, and the previous selected value was
655
+ * different, then if we change it to the only available item we
656
+ * should disable Selectic (user has no more choice).
657
+ * This is why it is needed to check autoDisabled here. */
658
+ this.checkAutoDisabled();
588
659
  }, { deep: true });
589
660
 
590
661
  watch(() => this.state.allOptions, () => {
@@ -641,6 +712,9 @@ export default class SelecticStore {
641
712
  if (this.props.texts) {
642
713
  this.changeTexts(this.props.texts);
643
714
  }
715
+ if (this.props.icons || this.props.iconFamily) {
716
+ this.changeIcons(this.props.icons, this.props.iconFamily);
717
+ }
644
718
 
645
719
  this.addGroups(this.props.groups);
646
720
  this.assertValueType();
@@ -779,19 +853,47 @@ export default class SelecticStore {
779
853
  return this.buildSelectedItems(ids);
780
854
  }
781
855
 
782
- public selectItem(id: OptionId, selected?: boolean, keepOpen = false) {
856
+ public selectGroup(id: OptionId, itemsSelected: boolean) {
857
+ const state = this.state;
858
+
859
+ if (!unref(this.allowGroupSelection)) {
860
+ return;
861
+ }
862
+
863
+ const selectItem = this.selectItem.bind(this);
864
+ let hasChanged = false;
865
+ this.data.doNotUpdate = true;
866
+ const items = state.filteredOptions.filter((item) => {
867
+ const isInGroup = item.group === id && !item.exclusive && !item.disabled;
868
+
869
+ if (isInGroup) {
870
+ hasChanged = selectItem(item.id, itemsSelected, true) || hasChanged;
871
+ }
872
+
873
+ return isInGroup;
874
+ });
875
+ this.data.doNotUpdate = false;
876
+
877
+ if (hasChanged && items.length) {
878
+ this.updateFilteredOptions();
879
+ }
880
+
881
+ return;
882
+ }
883
+
884
+ public selectItem(id: OptionId, selected?: boolean, keepOpen = false): boolean {
783
885
  const state = this.state;
784
886
  let hasChanged = false;
785
887
  const item = state.allOptions.find((opt) => opt.id === id);
786
888
 
787
889
  /* Check that item is not disabled */
788
890
  if (item?.disabled) {
789
- return;
891
+ return hasChanged;
790
892
  }
791
893
 
792
894
  if (state.strictValue && !this.hasValue(id)) {
793
895
  /* reject invalid values */
794
- return;
896
+ return hasChanged;
795
897
  }
796
898
 
797
899
  if (state.multiple) {
@@ -844,12 +946,12 @@ export default class SelecticStore {
844
946
 
845
947
  if (!selected) {
846
948
  if (id !== oldValue) {
847
- return;
949
+ return hasChanged;
848
950
  }
849
951
  id = null;
850
952
  } else
851
953
  if (id === oldValue) {
852
- return;
954
+ return hasChanged;
853
955
  }
854
956
 
855
957
  if (keepOpen) {
@@ -863,6 +965,8 @@ export default class SelecticStore {
863
965
  if (hasChanged) {
864
966
  state.status.hasChanged = true;
865
967
  }
968
+
969
+ return hasChanged;
866
970
  }
867
971
 
868
972
  public toggleSelectAll() {
@@ -950,6 +1054,16 @@ export default class SelecticStore {
950
1054
  this.data.labels = Object.assign({}, this.data.labels, texts);
951
1055
  }
952
1056
 
1057
+ public changeIcons(icons: PartialIcons | null, family?: IconFamily | null) {
1058
+ if (icons) {
1059
+ this.data.icons = Object.assign({}, this.data.icons, icons);
1060
+ }
1061
+
1062
+ if (typeof family === 'string') {
1063
+ this.data.iconFamily = family;
1064
+ }
1065
+ }
1066
+
953
1067
  /* }}} */
954
1068
  /* {{{ private methods */
955
1069
 
@@ -1073,6 +1187,7 @@ export default class SelecticStore {
1073
1187
  if (!this.data.doNotUpdate) {
1074
1188
  this.state.filteredOptions = this.buildItems(this.state.filteredOptions);
1075
1189
  this.buildSelectedOptions();
1190
+ this.updateGroupSelection();
1076
1191
  }
1077
1192
  }
1078
1193
 
@@ -1082,7 +1197,7 @@ export default class SelecticStore {
1082
1197
  });
1083
1198
  }
1084
1199
 
1085
- /* This method is for the computed property listOptions */
1200
+ /** This method is for the computed property listOptions */
1086
1201
  private getListOptions(): OptionValue[] {
1087
1202
  const options = deepClone(this.props.options, ['data']);
1088
1203
  const listOptions: OptionValue[] = [];
@@ -1256,13 +1371,11 @@ export default class SelecticStore {
1256
1371
  /* Do not fetch again just build filteredOptions */
1257
1372
  const search = this.state.searchText;
1258
1373
  if (!search) {
1259
- this.state.filteredOptions = this.buildGroupItems(allOptions);
1260
- this.state.totalFilteredOptions = this.state.filteredOptions.length;
1374
+ this.setFilteredOptions(this.buildGroupItems(allOptions));
1261
1375
  return;
1262
1376
  }
1263
1377
  const options = this.filterOptions(allOptions, search);
1264
- this.state.filteredOptions = options;
1265
- this.state.totalFilteredOptions = this.state.filteredOptions.length;
1378
+ this.setFilteredOptions(options);
1266
1379
  }
1267
1380
  }
1268
1381
 
@@ -1288,14 +1401,12 @@ export default class SelecticStore {
1288
1401
  /* Check if all options have been fetched */
1289
1402
  if (hasFetchedAllItems) {
1290
1403
  if (!search) {
1291
- this.state.filteredOptions = this.buildGroupItems(allOptions);
1292
- this.state.totalFilteredOptions = this.state.filteredOptions.length;
1404
+ this.setFilteredOptions(this.buildGroupItems(allOptions));
1293
1405
  return;
1294
1406
  }
1295
1407
 
1296
1408
  const options = this.filterOptions(allOptions, search);
1297
- this.state.filteredOptions = options;
1298
- this.state.totalFilteredOptions = this.state.filteredOptions.length;
1409
+ this.setFilteredOptions(options);
1299
1410
  return;
1300
1411
  }
1301
1412
 
@@ -1310,8 +1421,7 @@ export default class SelecticStore {
1310
1421
  }
1311
1422
 
1312
1423
  if (!search && endIndex <= allOptionsLength) {
1313
- this.state.filteredOptions = this.buildGroupItems(allOptions);
1314
- this.state.totalFilteredOptions = totalAllOptions + this.state.groups.size;
1424
+ this.setFilteredOptions(this.buildGroupItems(allOptions), false, totalAllOptions + this.state.groups.size);
1315
1425
  const isPartial = unref(this.isPartial);
1316
1426
  if (isPartial && this.state.totalDynOptions === Infinity) {
1317
1427
  this.fetchData();
@@ -1447,7 +1557,7 @@ export default class SelecticStore {
1447
1557
  /* Added options are the same as previous ones.
1448
1558
  * Stop fetching to avoid infinite loop
1449
1559
  */
1450
- if (!this.hasFetchedAllItems) {
1560
+ if (!unref(this.hasFetchedAllItems)) {
1451
1561
  /* Display error if all items are not fetch
1452
1562
  * We can have the case where old value and result
1453
1563
  * are the same but total is correct when the
@@ -1472,6 +1582,10 @@ export default class SelecticStore {
1472
1582
  const options = this.buildGroupItems(result, previousItem);
1473
1583
  const nbGroups1 = this.nbGroups(options);
1474
1584
 
1585
+ /* replace existing options by what have been received
1586
+ * or add received options.
1587
+ * This allow to manage requests received in different orders.
1588
+ */
1475
1589
  state.filteredOptions.splice(offset + dynOffset, limit + nbGroups1, ...options);
1476
1590
  }
1477
1591
 
@@ -1481,6 +1595,7 @@ export default class SelecticStore {
1481
1595
  }
1482
1596
 
1483
1597
  state.totalFilteredOptions = total + nbGroups + dynOffset;
1598
+ this.updateGroupSelection();
1484
1599
 
1485
1600
  if (search && state.totalFilteredOptions <= state.filteredOptions.length) {
1486
1601
  this.addStaticFilteredOptions(true);
@@ -1538,8 +1653,7 @@ export default class SelecticStore {
1538
1653
  options = this.filterOptions(unref(this.elementOptions), search);
1539
1654
  break;
1540
1655
  }
1541
- this.state.filteredOptions.push(...options);
1542
- this.state.totalFilteredOptions += options.length;
1656
+ this.setFilteredOptions(options, true);
1543
1657
  }
1544
1658
  }
1545
1659
 
@@ -1715,6 +1829,59 @@ export default class SelecticStore {
1715
1829
  }
1716
1830
  }
1717
1831
 
1832
+ /** update group item, to mark them as selected if needed */
1833
+ private updateGroupSelection() {
1834
+ const state = this.state;
1835
+
1836
+ if (!unref(this.allowGroupSelection)) {
1837
+ return;
1838
+ }
1839
+
1840
+ const filteredOptions = state.filteredOptions;;
1841
+ const groupIdx = new Map<OptionId, number>();
1842
+ const groupAllSelected = new Map<OptionId, boolean>();
1843
+ const groupNbItem = new Map<OptionId, number>();
1844
+
1845
+ filteredOptions.forEach((option, idx) => {
1846
+ const groupId = option.group;
1847
+
1848
+ if (option.isGroup) {
1849
+ const id = option.id;
1850
+ groupIdx.set(id, idx);
1851
+ groupAllSelected.set(id, true);
1852
+ } else
1853
+ if (groupId !== undefined) {
1854
+ if (option.disabled || option.exclusive) {
1855
+ return;
1856
+ }
1857
+
1858
+ groupNbItem.set(groupId, (groupNbItem.get(groupId) || 0) + 1)
1859
+
1860
+ if (!option.selected) {
1861
+ groupAllSelected.set(groupId, false);
1862
+ }
1863
+ }
1864
+ });
1865
+
1866
+ for (const [id, idx] of groupIdx.entries()) {
1867
+ const group = filteredOptions[idx];
1868
+ group.selected = !!(groupAllSelected.get(id) && groupNbItem.get(id));
1869
+ }
1870
+ }
1871
+
1872
+ /** assign new value to the filteredOptions and apply change depending on it */
1873
+ private setFilteredOptions(options: OptionItem[], add = false, length = 0) {
1874
+ if (!add) {
1875
+ this.state.filteredOptions = options;
1876
+ this.state.totalFilteredOptions = length || options.length;
1877
+ } else {
1878
+ this.state.filteredOptions.push(...options);
1879
+ this.state.totalFilteredOptions += length || options.length;
1880
+ }
1881
+
1882
+ this.updateGroupSelection();
1883
+ }
1884
+
1718
1885
  /* }}} */
1719
1886
  /* }}} */
1720
1887