selectic 3.0.18 → 3.0.20

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/src/Store.tsx CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { reactive, watch, unref, computed, ComputedRef } from 'vue';
8
- import { convertToRegExp, assignObject, deepClone } from './tools';
8
+ import { convertToRegExp, assignObject, deepClone, compareOptions } from './tools';
9
9
 
10
10
  /* {{{ Types definitions */
11
11
 
@@ -346,6 +346,7 @@ interface Messages {
346
346
  moreSelectedItem: string;
347
347
  moreSelectedItems: string;
348
348
  unknownPropertyValue: string;
349
+ wrongQueryResult: string;
349
350
  }
350
351
 
351
352
  export type PartialMessages = { [K in keyof Messages]?: Messages[K] };
@@ -376,6 +377,7 @@ let messages: Messages = {
376
377
  moreSelectedItem: '+1 other',
377
378
  moreSelectedItems: '+%d others',
378
379
  unknownPropertyValue: 'property "%s" has incorrect values.',
380
+ wrongQueryResult: 'Query did not return all results.',
379
381
  };
380
382
 
381
383
  let closePreviousSelectic: undefined | voidCaller;
@@ -1165,7 +1167,7 @@ export default class SelecticStore {
1165
1167
  return childOptions;
1166
1168
  }
1167
1169
 
1168
- private buildAllOptions(keepFetched = false) {
1170
+ private buildAllOptions(keepFetched = false, stopFetch = false) {
1169
1171
  const allOptions: OptionValue[] = [];
1170
1172
  let listOptions: OptionValue[] = [];
1171
1173
  let elementOptions: OptionValue[] = [];
@@ -1242,13 +1244,26 @@ export default class SelecticStore {
1242
1244
  }
1243
1245
  }
1244
1246
 
1245
- this.state.filteredOptions = [];
1246
- this.state.totalFilteredOptions = Infinity;
1247
+ if (!stopFetch) {
1248
+ this.state.filteredOptions = [];
1249
+ this.state.totalFilteredOptions = Infinity;
1247
1250
 
1248
- this.buildFilteredOptions().then(() => {
1249
- /* XXX: To recompute for strict mode and auto-select */
1250
- this.assertCorrectValue();
1251
- });
1251
+ this.buildFilteredOptions().then(() => {
1252
+ /* XXX: To recompute for strict mode and auto-select */
1253
+ this.assertCorrectValue();
1254
+ });
1255
+ } else {
1256
+ /* Do not fetch again just build filteredOptions */
1257
+ const search = this.state.searchText;
1258
+ if (!search) {
1259
+ this.state.filteredOptions = this.buildGroupItems(allOptions);
1260
+ this.state.totalFilteredOptions = this.state.filteredOptions.length;
1261
+ return;
1262
+ }
1263
+ const options = this.filterOptions(allOptions, search);
1264
+ this.state.filteredOptions = options;
1265
+ this.state.totalFilteredOptions = this.state.filteredOptions.length;
1266
+ }
1252
1267
  }
1253
1268
 
1254
1269
  private async buildFilteredOptions() {
@@ -1407,6 +1422,7 @@ export default class SelecticStore {
1407
1422
  const requestId = ++this.requestId;
1408
1423
  const {total: rTotal, result} = await fetchCallback(search, offset, limit);
1409
1424
  let total = rTotal;
1425
+ let errorMessage = '';
1410
1426
 
1411
1427
  /* Assert result is correctly formatted */
1412
1428
  if (!Array.isArray(result)) {
@@ -1426,8 +1442,16 @@ export default class SelecticStore {
1426
1442
  if (!search) {
1427
1443
  /* update cache */
1428
1444
  state.totalDynOptions = total;
1429
- state.dynOptions.splice(offset, limit, ...result);
1430
- setTimeout(() => this.buildAllOptions(true), 0);
1445
+ const old = state.dynOptions.splice(offset, limit, ...result);
1446
+ if (compareOptions(old, result)) {
1447
+ /* Added options are the same as previous ones.
1448
+ * Stop fetching to avoid infinite loop
1449
+ */
1450
+ errorMessage = labels.wrongQueryResult;
1451
+ setTimeout(() => this.buildAllOptions(true, true), 0);
1452
+ } else {
1453
+ setTimeout(() => this.buildAllOptions(true), 0);
1454
+ }
1431
1455
  }
1432
1456
 
1433
1457
  /* Check request is not obsolete */
@@ -1456,12 +1480,12 @@ export default class SelecticStore {
1456
1480
  this.addStaticFilteredOptions(true);
1457
1481
  }
1458
1482
 
1459
- state.status.errorMessage = '';
1483
+ state.status.errorMessage = errorMessage;
1460
1484
  } catch (e) {
1461
1485
  state.status.errorMessage = (e as Error).message;
1462
1486
  if (!search) {
1463
1487
  state.totalDynOptions = 0;
1464
- this.buildAllOptions(true);
1488
+ this.buildAllOptions(true, true);
1465
1489
  }
1466
1490
  }
1467
1491
 
@@ -184,13 +184,21 @@
184
184
 
185
185
  .selectic__extended-list {
186
186
  position: fixed;
187
+ top: var(--top-position, 0);
187
188
  z-index: 2000;
188
189
  height: auto;
190
+ max-height: var(--availableSpace);
189
191
  background-color: var(--selectic-bg, #ffffff);
190
192
  box-shadow: 2px 5px 12px 0px #888888;
191
193
  border-radius: 0 0 4px 4px;
192
194
  padding: 0;
195
+ width: var(--list-width, 200px);
193
196
  min-width: 200px;
197
+ display: grid;
198
+ grid-template-rows: minmax(0, max-content) 1fr;
199
+ }
200
+ .selectic__extended-list.selectic-position-top {
201
+ box-shadow: 2px -3px 12px 0px #888888;
194
202
  }
195
203
  .selectic__extended-list__list-container{
196
204
  overflow: auto;
package/src/tools.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { unref } from 'vue';
2
+ import { OptionValue } from './Store';
2
3
 
3
4
  /**
4
5
  * Clone the object and its inner properties.
@@ -86,3 +87,33 @@ export function assignObject<T>(obj: Partial<T>, ...sourceObjects: Array<Partial
86
87
  }
87
88
  return result as T;
88
89
  }
90
+
91
+ /** Compare 2 list of options.
92
+ * @returns true if there are no difference
93
+ */
94
+ export function compareOptions(oldOptions: OptionValue[], newOptions: OptionValue[]): boolean {
95
+ if (oldOptions.length !== newOptions.length) {
96
+ return false;
97
+ }
98
+
99
+ return oldOptions.every((oldOption, idx) => {
100
+ const newOption = newOptions[idx];
101
+ const keys = Object.keys(oldOption);
102
+ if (keys.length !== Object.keys(newOption).length) {
103
+ return false;
104
+ }
105
+ return keys.every((optionKey) => {
106
+ const key = optionKey as keyof OptionValue;
107
+ const oldValue = oldOption[key];
108
+ const newValue = newOption[key];
109
+
110
+ if (key === 'options') {
111
+ return compareOptions(oldValue, newValue);
112
+ }
113
+ if (key === 'data') {
114
+ return JSON.stringify(oldValue) === JSON.stringify(newValue);
115
+ }
116
+ return oldValue === newValue;
117
+ });
118
+ });
119
+ }
@@ -1387,7 +1387,7 @@ tape.test('Store creation', (subT) => {
1387
1387
  command1.fetch();
1388
1388
  await _.nextVueTick(store1, spy1.promise);
1389
1389
 
1390
- t.is(store1.state.filteredOptions.length, 3);
1390
+ t.is(store1.state.filteredOptions.length, 3, 'should contain the groups');
1391
1391
 
1392
1392
  const command2 = {};
1393
1393
  const spy2 = {};
@@ -748,6 +748,186 @@ tape.test('commit()', (st) => {
748
748
  t.true(toHaveBeenCalledWith(spy, ['', 100, 200]));
749
749
  t.deepEqual(store.state.allOptions.length, 300);
750
750
  t.deepEqual(store.state.offsetItem, 180);
751
+ t.is(store.state.status.errorMessage, '');
752
+
753
+ t.end();
754
+ });
755
+
756
+ sTest.test('should fetch only once', async (t) => {
757
+ const command = {};
758
+ let nbTry = 0;
759
+
760
+ const store = new Store({
761
+ fetchCallback: buildFetchCb({ total: 500, command }),
762
+ });
763
+ command.usage = 0;
764
+ store.commit('isOpen', true);
765
+
766
+ nbTry = 0;
767
+ while (nbTry !== command.usage && nbTry < 10) {
768
+ nbTry = command.usage;
769
+ command.fetch();
770
+ await _.nextVueTick(store, command.promise);
771
+ }
772
+
773
+ t.is(command.usage, 1, 'the fetch is done only once to get first page');
774
+ t.is(store.state.status.errorMessage, '');
775
+
776
+ command.usage = 0;
777
+ store.commit('offsetItem', 180);
778
+ nbTry = 0;
779
+ while (nbTry !== command.usage && nbTry < 10) {
780
+ nbTry = command.usage;
781
+ command.fetch();
782
+ await _.nextVueTick(store, command.promise);
783
+ }
784
+
785
+ t.is(command.usage, 1, 'fetch missing options only once');
786
+ t.is(store.state.status.errorMessage, '');
787
+
788
+ t.deepEqual(store.state.allOptions.length, 300);
789
+
790
+ t.end();
791
+ });
792
+
793
+ sTest.test('should fetch several time if needed', async (t) => {
794
+ const command = {};
795
+ let nbTry = 0;
796
+
797
+ const store = new Store({
798
+ fetchCallback: buildFetchCb({ total: 500, command }),
799
+ });
800
+ command.usage = 0;
801
+ store.commit('isOpen', true);
802
+
803
+ command.interceptResult = (result) => {
804
+ switch (command.usage) {
805
+ case 1:
806
+ /* returns only some of the first options */
807
+ result.result = result.result.slice(0, 15);
808
+ break;
809
+ case 2:
810
+ /* returns only some options */
811
+ result.result = result.result.slice(0, 25);
812
+ default:
813
+ /* do not change the result */
814
+ }
815
+ return result;
816
+ };
817
+
818
+ nbTry = 0;
819
+ while (nbTry !== command.usage && nbTry < 10) {
820
+ nbTry = command.usage;
821
+ command.fetch();
822
+ await _.nextVueTick(store, command.promise);
823
+ }
824
+
825
+ t.is(command.usage > 1, true, 'should fetch first options with several fetch');
826
+ t.is(command.usage < 10, true, 'should have stop fetching by itself');
827
+ t.is(store.state.allOptions.length >= 50, true, 'should have retrieve the first options');
828
+ t.is(store.state.status.errorMessage, '');
829
+
830
+ command.usage = 0;
831
+ store.commit('offsetItem', 180);
832
+ nbTry = 0;
833
+ while (nbTry !== command.usage && nbTry < 10) {
834
+ nbTry = command.usage;
835
+ command.fetch();
836
+ await _.nextVueTick(store, command.promise);
837
+ }
838
+
839
+ t.is(command.usage > 1, true, 'should fetch missing options with several fetch');
840
+ t.is(command.usage < 10, true, 'should have stop fetching by itself');
841
+
842
+ t.deepEqual(store.state.allOptions.length, 300);
843
+ t.is(store.state.status.errorMessage, '');
844
+
845
+ t.end();
846
+ });
847
+
848
+ sTest.test('should stop fetching with wrong result: similar results', async (t) => {
849
+ const command = {};
850
+ let nbTry = 0;
851
+
852
+ const store = new Store({
853
+ fetchCallback: buildFetchCb({ total: 500, command }),
854
+ });
855
+ command.usage = 0;
856
+ store.commit('isOpen', true);
857
+
858
+ command.interceptResult = (result) => {
859
+ /* Returns only the 2 first options */
860
+ result.result = result.result.slice(0, 2);
861
+
862
+ return result;
863
+ };
864
+
865
+ nbTry = 0;
866
+ while (nbTry !== command.usage && nbTry < 10) {
867
+ nbTry = command.usage;
868
+ command.fetch();
869
+ await _.nextVueTick(store, command.promise);
870
+ }
871
+
872
+ t.is(command.usage > 1, true, 'should have try to fetch several times');
873
+ t.is(command.usage < 10, true, 'should have stop fetching');
874
+ t.is(store.state.status.errorMessage, store.data.labels.wrongQueryResult);
875
+
876
+ command.usage = 0;
877
+ store.commit('offsetItem', 180);
878
+ nbTry = 0;
879
+ while (nbTry !== command.usage && nbTry < 10) {
880
+ nbTry = command.usage;
881
+ command.fetch();
882
+ await _.nextVueTick(store, command.promise);
883
+ }
884
+
885
+ t.is(command.usage < 10, true, 'should have stop fetching');
886
+ t.is(store.state.status.errorMessage, store.data.labels.wrongQueryResult);
887
+
888
+ t.end();
889
+ });
890
+
891
+ sTest.test('should stop fetching with wrong result: wrong total', async (t) => {
892
+ const command = {};
893
+ let nbTry = 0;
894
+
895
+ const store = new Store({
896
+ fetchCallback: buildFetchCb({ total: 20, command }),
897
+ });
898
+ command.usage = 0;
899
+ store.commit('isOpen', true);
900
+
901
+ command.interceptResult = (result) => {
902
+ result.total = 50;
903
+
904
+ return result;
905
+ };
906
+
907
+ nbTry = 0;
908
+ while (nbTry !== command.usage && nbTry < 10) {
909
+ nbTry = command.usage;
910
+ command.fetch();
911
+ await _.nextVueTick(store, command.promise);
912
+ }
913
+
914
+ t.is(command.usage > 1, true, 'should have try to fetch several times');
915
+ t.is(command.usage < 10, true, 'should have stop fetching');
916
+ t.is(store.state.status.errorMessage, store.data.labels.wrongQueryResult);
917
+
918
+ command.usage = 0;
919
+ store.commit('offsetItem', 30);
920
+ nbTry = 0;
921
+ while (nbTry !== command.usage && nbTry < 10) {
922
+ nbTry = command.usage;
923
+ command.fetch();
924
+ await _.nextVueTick(store, command.promise);
925
+ }
926
+
927
+ t.is(command.usage < 10, true, 'should have stop fetching');
928
+ t.is(store.state.status.errorMessage, store.data.labels.wrongQueryResult);
929
+
930
+ t.deepEqual(store.state.allOptions.length, 20);
751
931
 
752
932
  t.end();
753
933
  });
package/test/helper.js CHANGED
@@ -101,6 +101,7 @@ function buildFetchCb({
101
101
  command.reject = () => {};
102
102
  command.promise = () => {};
103
103
  command.fetch = () => {};
104
+ command.usage = 0;
104
105
  }
105
106
 
106
107
  return (search, offset, limit) => {
@@ -128,10 +129,16 @@ function buildFetchCb({
128
129
  });
129
130
 
130
131
  function resolveFetch() {
131
- resolve({
132
+ let result = {
132
133
  total,
133
134
  result: getOptions(nb, prefix, offset, groupName),
134
- });
135
+ };
136
+
137
+ if (command && command.interceptResult) {
138
+ result = command.interceptResult(result);
139
+ }
140
+
141
+ resolve(result);
135
142
  }
136
143
 
137
144
  if (command) {
@@ -145,6 +152,7 @@ function buildFetchCb({
145
152
 
146
153
  if (command) {
147
154
  command.promise = fetchPromise;
155
+ command.usage++;
148
156
  }
149
157
  spy.promise = fetchPromise;
150
158
 
@@ -18,12 +18,16 @@ export default class ExtendedList extends Vue<Props> {
18
18
  private topGroup;
19
19
  private listHeight;
20
20
  private listWidth;
21
+ private availableSpace;
22
+ /** check if the height of the box has been completely estimated. */
23
+ get isFullyEstimated(): boolean;
21
24
  get searchingLabel(): string;
22
25
  get searching(): boolean;
23
26
  get errorMessage(): string;
24
27
  get infoMessage(): string;
25
28
  get onKeyDown(): (evt: KeyboardEvent) => void;
26
29
  get bestPosition(): 'top' | 'bottom';
30
+ get position(): 'top' | 'bottom';
27
31
  get horizontalStyle(): string;
28
32
  get positionStyle(): string;
29
33
  onFilteredOptionsChange(): void;
@@ -11,13 +11,16 @@ export default class MainInput extends Vue<Props> {
11
11
  private store;
12
12
  private id;
13
13
  private nbHiddenItems;
14
+ private domObserver;
14
15
  get isDisabled(): boolean;
15
16
  get hasValue(): boolean;
16
17
  get displayPlaceholder(): boolean;
17
18
  get canBeCleared(): boolean;
18
19
  get showClearAll(): boolean;
19
20
  get clearedLabel(): string;
20
- get singleSelectedItem(): string | false;
21
+ get singleSelectedItem(): undefined | OptionItem;
22
+ get singleSelectedItemText(): string;
23
+ get singleSelectedItemTitle(): string;
21
24
  get singleStyle(): string | undefined;
22
25
  get selecticId(): string | undefined;
23
26
  get isSelectionReversed(): boolean;
@@ -31,7 +34,10 @@ export default class MainInput extends Vue<Props> {
31
34
  private selectItem;
32
35
  private clearSelection;
33
36
  private computeSize;
37
+ private closeObserver;
38
+ private createObserver;
34
39
  onInternalChange(): void;
35
40
  updated(): void;
41
+ beforeUnmount(): void;
36
42
  render(): h.JSX.Element;
37
43
  }
package/types/Store.d.ts CHANGED
@@ -139,6 +139,7 @@ interface Messages {
139
139
  moreSelectedItem: string;
140
140
  moreSelectedItems: string;
141
141
  unknownPropertyValue: string;
142
+ wrongQueryResult: string;
142
143
  }
143
144
  export declare type PartialMessages = {
144
145
  [K in keyof Messages]?: Messages[K];
package/types/tools.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { OptionValue } from './Store';
1
2
  /**
2
3
  * Clone the object and its inner properties.
3
4
  * @param obj The object to be clone.
@@ -20,3 +21,7 @@ export declare function deepClone<T = any>(origObject: T, ignoreAttributes?: str
20
21
  export declare function convertToRegExp(name: string, flag?: string): RegExp;
21
22
  /** Does the same as Object.assign but does not replace if value is undefined */
22
23
  export declare function assignObject<T>(obj: Partial<T>, ...sourceObjects: Array<Partial<T>>): T;
24
+ /** Compare 2 list of options.
25
+ * @returns true if there are no difference
26
+ */
27
+ export declare function compareOptions(oldOptions: OptionValue[], newOptions: OptionValue[]): boolean;