selectic 3.1.1 → 3.1.2

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/README.md CHANGED
@@ -112,13 +112,3 @@ Run unitary tests:
112
112
  ```console
113
113
  $ npm run test
114
114
  ```
115
-
116
- ## Migration information
117
-
118
- ### From 3.0 to 3.1
119
-
120
- Selectic no more depends on Font-awesome. It embeds its own icons (from Material Design Icons).
121
-
122
- It is still possible to use Font-awesome icons (or from any other libraries).
123
-
124
- Read [the documentation section related to changing icons](./doc/changeIcons.md) for more information on how to handle them.
@@ -138,12 +138,25 @@ function compareOptions(oldOptions, newOptions) {
138
138
  });
139
139
  });
140
140
  }
141
+ let displayLog = false;
142
+ function debug(fName, step, ...args) {
143
+ if (!displayLog) {
144
+ return;
145
+ }
146
+ console.log('--%s-- [%s]', fName, step, ...args);
147
+ }
148
+ /** Enable logs for debugging */
149
+ debug.enable = (display) => {
150
+ displayLog = display;
151
+ };
141
152
 
142
153
  /* File Purpose:
143
154
  * It keeps and computes all states at a single place.
144
155
  * Every inner components of Selectic should communicate with this file to
145
156
  * change or to get states.
146
157
  */
158
+ /* For debugging */
159
+ debug.enable(false);
147
160
  /* }}} */
148
161
  /* {{{ Static */
149
162
  function changeTexts$1(texts) {
@@ -178,12 +191,19 @@ let messages = {
178
191
  let defaultFamilyIcon = 'selectic';
179
192
  let icons = {};
180
193
  let closePreviousSelectic;
194
+ /**
195
+ * Time to wait before considering there is no other requests.
196
+ * This time is await only if there is already a requested request.
197
+ */
198
+ const DEBOUNCE_REQUEST = 250;
181
199
  /* }}} */
182
200
  let uid = 0;
183
201
  class SelecticStore {
184
202
  constructor(props = {}) {
185
203
  /* Do not need reactivity */
186
204
  this.requestId = 0;
205
+ this.requestSearchId = 0; /* Used for search request */
206
+ this.isRequesting = false;
187
207
  this._uid = ++uid;
188
208
  /* {{{ Props */
189
209
  const defaultProps = {
@@ -299,6 +319,7 @@ class SelecticStore {
299
319
  this.data.cacheItem.clear();
300
320
  this.setAutomaticClose();
301
321
  this.commit('isOpen', false);
322
+ this.clearDisplay();
302
323
  this.buildAllOptions(true);
303
324
  this.buildSelectedOptions();
304
325
  }, { deep: true });
@@ -389,6 +410,7 @@ class SelecticStore {
389
410
  /* {{{ public methods */
390
411
  commit(name, value) {
391
412
  const oldValue = this.state[name];
413
+ debug('commit', 'start', name, value, 'oldValue:', oldValue);
392
414
  if (oldValue === value) {
393
415
  return;
394
416
  }
@@ -397,8 +419,7 @@ class SelecticStore {
397
419
  case 'searchText':
398
420
  this.state.offsetItem = 0;
399
421
  this.state.activeItemIdx = -1;
400
- this.state.filteredOptions = [];
401
- this.state.totalFilteredOptions = Infinity;
422
+ this.clearDisplay();
402
423
  if (value) {
403
424
  this.buildFilteredOptions();
404
425
  }
@@ -446,6 +467,7 @@ class SelecticStore {
446
467
  }
447
468
  break;
448
469
  }
470
+ debug('commit', '(done)', name);
449
471
  }
450
472
  setAutomaticChange() {
451
473
  this.state.status.automaticChange = true;
@@ -631,14 +653,14 @@ class SelecticStore {
631
653
  this.state.status.errorMessage = '';
632
654
  }
633
655
  clearCache(forceReset = false) {
656
+ debug('clearCache', 'start', forceReset);
634
657
  const isPartial = vue.unref(this.isPartial);
635
658
  const total = isPartial ? Infinity : 0;
636
659
  this.data.cacheItem.clear();
637
660
  this.state.allOptions = [];
638
661
  this.state.totalAllOptions = total;
639
662
  this.state.totalDynOptions = total;
640
- this.state.filteredOptions = [];
641
- this.state.totalFilteredOptions = Infinity;
663
+ this.clearDisplay();
642
664
  this.state.status.errorMessage = '';
643
665
  this.state.status.hasChanged = false;
644
666
  if (forceReset) {
@@ -770,6 +792,13 @@ class SelecticStore {
770
792
  this.checkAutoSelect();
771
793
  }
772
794
  }
795
+ /** Reset the display cache in order to rebuild it */
796
+ clearDisplay() {
797
+ debug('clearDisplay', 'start');
798
+ this.state.filteredOptions = [];
799
+ this.state.totalFilteredOptions = Infinity;
800
+ }
801
+ /** rebuild the state filteredOptions to normalize their values */
773
802
  updateFilteredOptions() {
774
803
  if (!this.data.doNotUpdate) {
775
804
  this.state.filteredOptions = this.buildItems(this.state.filteredOptions);
@@ -819,7 +848,7 @@ class SelecticStore {
819
848
  });
820
849
  return listOptions;
821
850
  }
822
- /* This method is for the computed property elementOptions */
851
+ /** This method is for the computed property elementOptions */
823
852
  getElementOptions() {
824
853
  const options = deepClone(this.props.childOptions, ['data']);
825
854
  const childOptions = [];
@@ -850,7 +879,9 @@ class SelecticStore {
850
879
  });
851
880
  return childOptions;
852
881
  }
882
+ /** Generate the list of all options by combining the 3 option lists */
853
883
  buildAllOptions(keepFetched = false, stopFetch = false) {
884
+ debug('buildAllOptions', 'start', 'keepFetched', keepFetched, 'stopFetch', stopFetch);
854
885
  const allOptions = [];
855
886
  let listOptions = [];
856
887
  let elementOptions = [];
@@ -922,8 +953,6 @@ class SelecticStore {
922
953
  }
923
954
  }
924
955
  if (!stopFetch) {
925
- this.state.filteredOptions = [];
926
- this.state.totalFilteredOptions = Infinity;
927
956
  this.buildFilteredOptions().then(() => {
928
957
  /* XXX: To recompute for strict mode and auto-select */
929
958
  this.assertCorrectValue();
@@ -939,18 +968,21 @@ class SelecticStore {
939
968
  const options = this.filterOptions(allOptions, search);
940
969
  this.setFilteredOptions(options);
941
970
  }
971
+ debug('buildAllOptions', 'end', 'allOptions:', this.state.allOptions.length, 'totalAllOptions:', this.state.totalAllOptions);
942
972
  }
943
973
  async buildFilteredOptions() {
944
- if (!this.state.isOpen) {
974
+ const state = this.state;
975
+ if (!state.isOpen) {
945
976
  /* Do not try to fetch anything while the select is not open */
946
977
  return;
947
978
  }
948
- const allOptions = this.state.allOptions;
949
- const search = this.state.searchText;
950
- const totalAllOptions = this.state.totalAllOptions;
979
+ const allOptions = state.allOptions;
980
+ const search = state.searchText;
981
+ const totalAllOptions = state.totalAllOptions;
951
982
  const allOptionsLength = allOptions.length;
952
- let filteredOptionsLength = this.state.filteredOptions.length;
983
+ let filteredOptionsLength = state.filteredOptions.length;
953
984
  const hasAllItems = vue.unref(this.hasAllItems);
985
+ debug('buildFilteredOptions', 'start', 'hasAllItems:', hasAllItems, 'allOptions', allOptions.length, 'search:', search, 'filteredOptionsLength:', filteredOptionsLength);
954
986
  if (hasAllItems) {
955
987
  /* Everything has already been fetched and stored in filteredOptions */
956
988
  return;
@@ -967,16 +999,17 @@ class SelecticStore {
967
999
  return;
968
1000
  }
969
1001
  /* When we only have partial options */
970
- const offsetItem = this.state.offsetItem;
1002
+ const offsetItem = state.offsetItem;
971
1003
  const marginSize = vue.unref(this.marginSize);
972
1004
  const endIndex = offsetItem + marginSize;
1005
+ debug('buildFilteredOptions', 'partial options', 'offsetItem:', offsetItem, 'marginSize:', marginSize, 'filteredOptionsLength', filteredOptionsLength);
973
1006
  if (endIndex <= filteredOptionsLength) {
974
1007
  return;
975
1008
  }
976
1009
  if (!search && endIndex <= allOptionsLength) {
977
- this.setFilteredOptions(this.buildGroupItems(allOptions), false, totalAllOptions + this.state.groups.size);
1010
+ this.setFilteredOptions(this.buildGroupItems(allOptions), false, totalAllOptions + state.groups.size);
978
1011
  const isPartial = vue.unref(this.isPartial);
979
- if (isPartial && this.state.totalDynOptions === Infinity) {
1012
+ if (isPartial && state.totalDynOptions === Infinity) {
980
1013
  this.fetchData();
981
1014
  }
982
1015
  return;
@@ -989,6 +1022,7 @@ class SelecticStore {
989
1022
  return;
990
1023
  }
991
1024
  }
1025
+ debug('buildFilteredOptions', 'end', '(will call fetchData)', this.state.filteredOptions.length);
992
1026
  await this.fetchData();
993
1027
  }
994
1028
  async buildSelectedOptions() {
@@ -1042,6 +1076,39 @@ class SelecticStore {
1042
1076
  state.selectedOptions = items[0];
1043
1077
  }
1044
1078
  }
1079
+ async fetchRequest(fetchCallback, search, offset, limit) {
1080
+ const searchRqId = ++this.requestSearchId;
1081
+ if (!search) {
1082
+ ++this.requestId;
1083
+ }
1084
+ const requestId = this.requestId;
1085
+ debug('fetchRequest', 'start', 'search:', search, 'offset:', offset, 'limit:', limit, 'requestId:', requestId, 'requestSearchId:', searchRqId, 'isRequesting:', this.isRequesting);
1086
+ if (this.isRequesting) {
1087
+ debug('fetchRequest', `await ${DEBOUNCE_REQUEST}ms`);
1088
+ /* debounce the call to avoid sending too much requests */
1089
+ await new Promise((resolve) => {
1090
+ setTimeout(resolve, DEBOUNCE_REQUEST);
1091
+ });
1092
+ /* Check if there are other requested requests, in such case drop this one */
1093
+ if (requestId !== this.requestId || (search && searchRqId !== this.requestSearchId)) {
1094
+ debug('fetchRequest', '××deprecated××', requestId, searchRqId);
1095
+ return false;
1096
+ }
1097
+ }
1098
+ this.isRequesting = true;
1099
+ const response = await fetchCallback(search, offset, limit);
1100
+ /* Check if request is obsolete */
1101
+ if (requestId !== this.requestId || (search && searchRqId !== this.requestSearchId)) {
1102
+ debug('fetchRequest', '×××deprecated×××', requestId, searchRqId);
1103
+ return false;
1104
+ }
1105
+ this.isRequesting = false;
1106
+ const deprecated = searchRqId !== this.requestSearchId;
1107
+ debug('fetchRequest', 'end', response.result.length, response.total, deprecated);
1108
+ return Object.assign(Object.assign({}, response), {
1109
+ /* this is to fulfill the cache */
1110
+ deprecated: deprecated });
1111
+ }
1045
1112
  async fetchData() {
1046
1113
  const state = this.state;
1047
1114
  const labels = this.data.labels;
@@ -1063,9 +1130,14 @@ class SelecticStore {
1063
1130
  const offset = filteredOptionsLength - this.nbGroups(state.filteredOptions) - dynOffset;
1064
1131
  const nbItems = endIndex - offset;
1065
1132
  const limit = Math.ceil(nbItems / pageSize) * pageSize;
1133
+ debug('fetchData', 'start', 'search:', search, 'offset:', offset, 'limit:', limit);
1066
1134
  try {
1067
- const requestId = ++this.requestId;
1068
- const { total: rTotal, result } = await fetchCallback(search, offset, limit);
1135
+ const response = await this.fetchRequest(fetchCallback, search, offset, limit);
1136
+ if (!response) {
1137
+ debug('fetchData', '×× deprecated ××', search, offset, limit);
1138
+ return;
1139
+ }
1140
+ const { total: rTotal, result, deprecated } = response;
1069
1141
  let total = rTotal;
1070
1142
  let errorMessage = '';
1071
1143
  /* Assert result is correctly formatted */
@@ -1083,11 +1155,12 @@ class SelecticStore {
1083
1155
  if (!search) {
1084
1156
  /* update cache */
1085
1157
  state.totalDynOptions = total;
1086
- const old = state.dynOptions.splice(offset, limit, ...result);
1158
+ const old = state.dynOptions.splice(offset, result.length, ...result);
1087
1159
  if (compareOptions(old, result)) {
1088
1160
  /* Added options are the same as previous ones.
1089
1161
  * Stop fetching to avoid infinite loop
1090
1162
  */
1163
+ debug('fetchData', 'no new values');
1091
1164
  if (!vue.unref(this.hasFetchedAllItems)) {
1092
1165
  /* Display error if all items are not fetch
1093
1166
  * We can have the case where old value and result
@@ -1095,14 +1168,21 @@ class SelecticStore {
1095
1168
  * total is 0 */
1096
1169
  errorMessage = labels.wrongQueryResult;
1097
1170
  }
1098
- setTimeout(() => this.buildAllOptions(true, true), 0);
1171
+ setTimeout(() => {
1172
+ debug('fetchData', 'before buildAllOptions (stopped)', 'offsetItem:', this.state.offsetItem, 'allOptions:', this.state.allOptions.length);
1173
+ this.buildAllOptions(true, true);
1174
+ }, 0);
1099
1175
  }
1100
1176
  else {
1101
- setTimeout(() => this.buildAllOptions(true), 0);
1177
+ setTimeout(() => {
1178
+ debug('fetchData', 'before buildAllOptions', 'offsetItem:', this.state.offsetItem, 'allOptions:', this.state.allOptions.length);
1179
+ this.buildAllOptions(true);
1180
+ }, 0);
1102
1181
  }
1103
1182
  }
1104
- /* Check request is not obsolete */
1105
- if (requestId !== this.requestId) {
1183
+ /* Check request (without search) is not obsolete */
1184
+ if (deprecated) {
1185
+ debug('fetchData', '××× deprecated ×××', search, offset, limit);
1106
1186
  return;
1107
1187
  }
1108
1188
  if (!search) {
@@ -1131,14 +1211,17 @@ class SelecticStore {
1131
1211
  }
1132
1212
  catch (e) {
1133
1213
  state.status.errorMessage = e.message;
1214
+ debug('fetchData', 'error', e.message);
1134
1215
  if (!search) {
1135
1216
  state.totalDynOptions = 0;
1136
1217
  this.buildAllOptions(true, true);
1137
1218
  }
1138
1219
  }
1139
1220
  this.state.status.searching = false;
1221
+ debug('fetchData', 'end');
1140
1222
  }
1141
1223
  filterOptions(options, search) {
1224
+ debug('filterOptions', 'start', 'options:', options.length, 'search:', search);
1142
1225
  if (!search) {
1143
1226
  return this.buildGroupItems(options);
1144
1227
  }
@@ -1149,6 +1232,7 @@ class SelecticStore {
1149
1232
  addStaticFilteredOptions(fromDynamic = false) {
1150
1233
  const search = this.state.searchText;
1151
1234
  let continueLoop = fromDynamic;
1235
+ debug('addStaticFilteredOptions', 'start', 'fromDynamic:', fromDynamic, 'optionBehaviorOperation:', this.state.optionBehaviorOperation);
1152
1236
  if (this.state.optionBehaviorOperation !== 'sort') {
1153
1237
  return;
1154
1238
  }
@@ -1174,6 +1258,7 @@ class SelecticStore {
1174
1258
  }
1175
1259
  this.setFilteredOptions(options, true);
1176
1260
  }
1261
+ debug('addStaticFilteredOptions', 'end');
1177
1262
  }
1178
1263
  buildSelectedItems(ids) {
1179
1264
  return this.buildItems(ids.map((id) => {
@@ -1215,6 +1300,7 @@ class SelecticStore {
1215
1300
  }
1216
1301
  buildGroupItems(options, previousItem) {
1217
1302
  let previousGroupId = previousItem && previousItem.group;
1303
+ debug('buildGroupItems', 'start', 'options:', options.length, 'previousGroupId:', previousGroupId);
1218
1304
  const list = this.buildItems(options).reduce((items, item) => {
1219
1305
  if (item.group !== previousGroupId) {
1220
1306
  const groupId = item.group;
@@ -1231,6 +1317,7 @@ class SelecticStore {
1231
1317
  items.push(item);
1232
1318
  return items;
1233
1319
  }, []);
1320
+ debug('buildGroupItems', 'end', list.length);
1234
1321
  return list;
1235
1322
  }
1236
1323
  buildOptionBehavior(optionBehavior, state) {
@@ -1348,6 +1435,7 @@ class SelecticStore {
1348
1435
  }
1349
1436
  /** assign new value to the filteredOptions and apply change depending on it */
1350
1437
  setFilteredOptions(options, add = false, length = 0) {
1438
+ debug('setFilteredOptions', 'start', 'options:', options.length, 'add', add, 'length', length);
1351
1439
  if (!add) {
1352
1440
  this.state.filteredOptions = options;
1353
1441
  this.state.totalFilteredOptions = length || options.length;