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 +0 -10
- package/dist/selectic.common.js +110 -22
- package/dist/selectic.esm.js +110 -22
- package/doc/breakingChanges.md +8 -0
- package/doc/changeIcons.md +8 -2
- package/package.json +1 -1
- package/src/ExtendedList.tsx +5 -5
- package/src/Icon.tsx +2 -2
- package/src/List.tsx +0 -4
- package/src/Store.tsx +161 -25
- package/src/index.tsx +2 -2
- package/src/tools.ts +13 -0
- package/test/Store/Store_creation.spec.js +12 -12
- package/test/Store/Store_props.spec.js +612 -595
- package/test/Store/changeIcons.spec.js +1 -1
- package/test/Store/commit.spec.js +56 -47
- package/test/Store/selectGroup.spec.js +278 -271
- package/test/Store/toggleSelectAll.spec.js +6 -1
- package/test/helper.js +4 -0
- package/test/tools.js +5 -1
- package/types/ExtendedList.d.ts +5 -5
- package/types/List.d.ts +0 -3
- package/types/Store.d.ts +15 -2
- package/types/index.d.ts +2 -2
- package/types/tools.d.ts +4 -0
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.
|
package/dist/selectic.common.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
949
|
-
const search =
|
|
950
|
-
const 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 =
|
|
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 =
|
|
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 +
|
|
1010
|
+
this.setFilteredOptions(this.buildGroupItems(allOptions), false, totalAllOptions + state.groups.size);
|
|
978
1011
|
const isPartial = vue.unref(this.isPartial);
|
|
979
|
-
if (isPartial &&
|
|
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
|
|
1068
|
-
|
|
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,
|
|
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(() =>
|
|
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(() =>
|
|
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 (
|
|
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;
|