selectic 3.1.3 → 3.1.5
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/dist/selectic.common.js +106 -62
- package/dist/selectic.esm.js +106 -62
- package/package.json +1 -1
- package/src/ExtendedList.tsx +40 -42
- package/src/List.tsx +6 -3
- package/src/Store.tsx +2 -2
- package/src/index.tsx +1 -1
- package/src/tools.ts +85 -23
- package/test/Tools/tools.spec.js +151 -0
- package/types/ExtendedList.d.ts +1 -1
- package/types/List.d.ts +1 -1
- package/types/tools.d.ts +10 -4
package/dist/selectic.common.js
CHANGED
|
@@ -111,32 +111,76 @@ function assignObject(obj, ...sourceObjects) {
|
|
|
111
111
|
}
|
|
112
112
|
return result;
|
|
113
113
|
}
|
|
114
|
-
/**
|
|
115
|
-
*
|
|
114
|
+
/**
|
|
115
|
+
* Ckeck whether a value is primitive.
|
|
116
|
+
* @returns true if val is primitive and false otherwise.
|
|
117
|
+
*/
|
|
118
|
+
function isPrimitive(val) {
|
|
119
|
+
/* The value null is treated explicitly because in JavaScript
|
|
120
|
+
* `typeof null === 'object'` is evaluated to `true`.
|
|
121
|
+
*/
|
|
122
|
+
return val === null || (typeof val !== 'object' && typeof val !== 'function');
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Performs a deep comparison between two objects to determine if they
|
|
126
|
+
* should be considered equal.
|
|
127
|
+
*
|
|
128
|
+
* @param objA object to compare to objB.
|
|
129
|
+
* @param objB object to compare to objA.
|
|
130
|
+
* @param attributes list of attributes to not compare.
|
|
131
|
+
* @param refs internal reference to object to avoid cyclic references
|
|
132
|
+
* @returns true if objA should be considered equal to objB.
|
|
116
133
|
*/
|
|
117
|
-
function
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
134
|
+
function isDeepEqual(objA, objB, ignoreAttributes = [], refs = new WeakMap()) {
|
|
135
|
+
objA = vue.unref(objA);
|
|
136
|
+
objB = vue.unref(objB);
|
|
137
|
+
/* For primitive types */
|
|
138
|
+
if (isPrimitive(objA)) {
|
|
139
|
+
return isPrimitive(objB) && Object.is(objA, objB);
|
|
140
|
+
}
|
|
141
|
+
/* For functions (follow the behavior of _.isEqual and compare functions
|
|
142
|
+
* by reference). */
|
|
143
|
+
if (typeof objA === 'function') {
|
|
144
|
+
return typeof objB === 'function' && objA === objB;
|
|
145
|
+
}
|
|
146
|
+
/* For circular references */
|
|
147
|
+
if (refs.has(objA)) {
|
|
148
|
+
return refs.get(objA) === objB;
|
|
149
|
+
}
|
|
150
|
+
refs.set(objA, objB);
|
|
151
|
+
/* For objects */
|
|
152
|
+
if (typeof objA === 'object') {
|
|
153
|
+
if (typeof objB !== 'object') {
|
|
125
154
|
return false;
|
|
126
155
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
156
|
+
/* For arrays */
|
|
157
|
+
if (Array.isArray(objA)) {
|
|
158
|
+
return Array.isArray(objB) &&
|
|
159
|
+
objA.length === objB.length &&
|
|
160
|
+
!objA.some((val, idx) => !isDeepEqual(val, objB[idx], ignoreAttributes, refs));
|
|
161
|
+
}
|
|
162
|
+
/* For RegExp */
|
|
163
|
+
if (objA instanceof RegExp) {
|
|
164
|
+
return objB instanceof RegExp &&
|
|
165
|
+
objA.source === objB.source &&
|
|
166
|
+
objA.flags === objB.flags;
|
|
167
|
+
}
|
|
168
|
+
/* For Date */
|
|
169
|
+
if (objA instanceof Date) {
|
|
170
|
+
return objB instanceof Date && objA.getTime() === objB.getTime();
|
|
171
|
+
}
|
|
172
|
+
/* This should be an object */
|
|
173
|
+
const aRec = objA;
|
|
174
|
+
const bRec = objB;
|
|
175
|
+
const aKeys = Object.keys(aRec).filter((key) => !ignoreAttributes.includes(key));
|
|
176
|
+
const bKeys = Object.keys(bRec).filter((key) => !ignoreAttributes.includes(key));
|
|
177
|
+
const differentKeyFound = aKeys.some((key) => {
|
|
178
|
+
return !bKeys.includes(key) ||
|
|
179
|
+
!isDeepEqual(aRec[key], bRec[key], ignoreAttributes, refs);
|
|
138
180
|
});
|
|
139
|
-
|
|
181
|
+
return aKeys.length === bKeys.length && !differentKeyFound;
|
|
182
|
+
}
|
|
183
|
+
return true;
|
|
140
184
|
}
|
|
141
185
|
let displayLog = false;
|
|
142
186
|
function debug(fName, step, ...args) {
|
|
@@ -1190,7 +1234,7 @@ class SelecticStore {
|
|
|
1190
1234
|
/* update cache */
|
|
1191
1235
|
state.totalDynOptions = total;
|
|
1192
1236
|
const old = state.dynOptions.splice(offset, result.length, ...result);
|
|
1193
|
-
if (
|
|
1237
|
+
if (isDeepEqual(old, result)) {
|
|
1194
1238
|
/* Added options are the same as previous ones.
|
|
1195
1239
|
* Stop fetching to avoid infinite loop
|
|
1196
1240
|
*/
|
|
@@ -2452,8 +2496,10 @@ let List = class List extends vtyx.Vue {
|
|
|
2452
2496
|
onOffsetChange() {
|
|
2453
2497
|
this.checkOffset();
|
|
2454
2498
|
}
|
|
2455
|
-
onFilteredOptionsChange() {
|
|
2456
|
-
|
|
2499
|
+
onFilteredOptionsChange(oldVal, newVal) {
|
|
2500
|
+
if (!isDeepEqual(oldVal, newVal)) {
|
|
2501
|
+
this.checkOffset();
|
|
2502
|
+
}
|
|
2457
2503
|
}
|
|
2458
2504
|
onGroupIdChange() {
|
|
2459
2505
|
this.$emit('groupId', this.groupId);
|
|
@@ -2566,42 +2612,6 @@ let ExtendedList = class ExtendedList extends vtyx.Vue {
|
|
|
2566
2612
|
}
|
|
2567
2613
|
return '';
|
|
2568
2614
|
}
|
|
2569
|
-
get onKeyDown() {
|
|
2570
|
-
return (evt) => {
|
|
2571
|
-
const key = evt.key;
|
|
2572
|
-
if (key === 'Escape') {
|
|
2573
|
-
this.store.commit('isOpen', false);
|
|
2574
|
-
}
|
|
2575
|
-
else if (key === 'Enter') {
|
|
2576
|
-
const index = this.store.state.activeItemIdx;
|
|
2577
|
-
if (index !== -1) {
|
|
2578
|
-
const item = this.store.state.filteredOptions[index];
|
|
2579
|
-
if (!item.disabled && !item.isGroup) {
|
|
2580
|
-
this.store.selectItem(item.id);
|
|
2581
|
-
}
|
|
2582
|
-
}
|
|
2583
|
-
evt.stopPropagation();
|
|
2584
|
-
evt.preventDefault();
|
|
2585
|
-
}
|
|
2586
|
-
else if (key === 'ArrowUp') {
|
|
2587
|
-
const index = this.store.state.activeItemIdx;
|
|
2588
|
-
if (index > 0) {
|
|
2589
|
-
this.store.commit('activeItemIdx', index - 1);
|
|
2590
|
-
}
|
|
2591
|
-
evt.stopPropagation();
|
|
2592
|
-
evt.preventDefault();
|
|
2593
|
-
}
|
|
2594
|
-
else if (key === 'ArrowDown') {
|
|
2595
|
-
const index = this.store.state.activeItemIdx;
|
|
2596
|
-
const max = this.store.state.totalFilteredOptions - 1;
|
|
2597
|
-
if (index < max) {
|
|
2598
|
-
this.store.commit('activeItemIdx', index + 1);
|
|
2599
|
-
}
|
|
2600
|
-
evt.stopPropagation();
|
|
2601
|
-
evt.preventDefault();
|
|
2602
|
-
}
|
|
2603
|
-
};
|
|
2604
|
-
}
|
|
2605
2615
|
get bestPosition() {
|
|
2606
2616
|
const windowHeight = window.innerHeight;
|
|
2607
2617
|
const isFullyEstimated = this.isFullyEstimated;
|
|
@@ -2717,6 +2727,40 @@ let ExtendedList = class ExtendedList extends vtyx.Vue {
|
|
|
2717
2727
|
clickHeaderGroup() {
|
|
2718
2728
|
this.store.selectGroup(this.topGroupId, !this.topGroupSelected);
|
|
2719
2729
|
}
|
|
2730
|
+
onKeyDown(evt) {
|
|
2731
|
+
const key = evt.key;
|
|
2732
|
+
if (key === 'Escape') {
|
|
2733
|
+
this.store.commit('isOpen', false);
|
|
2734
|
+
}
|
|
2735
|
+
else if (key === 'Enter') {
|
|
2736
|
+
const index = this.store.state.activeItemIdx;
|
|
2737
|
+
if (index !== -1) {
|
|
2738
|
+
const item = this.store.state.filteredOptions[index];
|
|
2739
|
+
if (!item.disabled && !item.isGroup) {
|
|
2740
|
+
this.store.selectItem(item.id);
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
evt.stopPropagation();
|
|
2744
|
+
evt.preventDefault();
|
|
2745
|
+
}
|
|
2746
|
+
else if (key === 'ArrowUp') {
|
|
2747
|
+
const index = this.store.state.activeItemIdx;
|
|
2748
|
+
if (index > 0) {
|
|
2749
|
+
this.store.commit('activeItemIdx', index - 1);
|
|
2750
|
+
}
|
|
2751
|
+
evt.stopPropagation();
|
|
2752
|
+
evt.preventDefault();
|
|
2753
|
+
}
|
|
2754
|
+
else if (key === 'ArrowDown') {
|
|
2755
|
+
const index = this.store.state.activeItemIdx;
|
|
2756
|
+
const max = this.store.state.totalFilteredOptions - 1;
|
|
2757
|
+
if (index < max) {
|
|
2758
|
+
this.store.commit('activeItemIdx', index + 1);
|
|
2759
|
+
}
|
|
2760
|
+
evt.stopPropagation();
|
|
2761
|
+
evt.preventDefault();
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2720
2764
|
/* }}} */
|
|
2721
2765
|
/* {{{ Life cycles */
|
|
2722
2766
|
mounted() {
|
|
@@ -3015,7 +3059,7 @@ let Selectic = class Selectic extends vtyx.Vue {
|
|
|
3015
3059
|
else {
|
|
3016
3060
|
this.removeListeners();
|
|
3017
3061
|
if (state.status.hasChanged) {
|
|
3018
|
-
this
|
|
3062
|
+
this.emit('change', this.getValue(), state.selectionIsExcluded);
|
|
3019
3063
|
this.store.resetChange();
|
|
3020
3064
|
}
|
|
3021
3065
|
this.emit('close');
|
package/dist/selectic.esm.js
CHANGED
|
@@ -107,32 +107,76 @@ function assignObject(obj, ...sourceObjects) {
|
|
|
107
107
|
}
|
|
108
108
|
return result;
|
|
109
109
|
}
|
|
110
|
-
/**
|
|
111
|
-
*
|
|
110
|
+
/**
|
|
111
|
+
* Ckeck whether a value is primitive.
|
|
112
|
+
* @returns true if val is primitive and false otherwise.
|
|
113
|
+
*/
|
|
114
|
+
function isPrimitive(val) {
|
|
115
|
+
/* The value null is treated explicitly because in JavaScript
|
|
116
|
+
* `typeof null === 'object'` is evaluated to `true`.
|
|
117
|
+
*/
|
|
118
|
+
return val === null || (typeof val !== 'object' && typeof val !== 'function');
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Performs a deep comparison between two objects to determine if they
|
|
122
|
+
* should be considered equal.
|
|
123
|
+
*
|
|
124
|
+
* @param objA object to compare to objB.
|
|
125
|
+
* @param objB object to compare to objA.
|
|
126
|
+
* @param attributes list of attributes to not compare.
|
|
127
|
+
* @param refs internal reference to object to avoid cyclic references
|
|
128
|
+
* @returns true if objA should be considered equal to objB.
|
|
112
129
|
*/
|
|
113
|
-
function
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
130
|
+
function isDeepEqual(objA, objB, ignoreAttributes = [], refs = new WeakMap()) {
|
|
131
|
+
objA = unref(objA);
|
|
132
|
+
objB = unref(objB);
|
|
133
|
+
/* For primitive types */
|
|
134
|
+
if (isPrimitive(objA)) {
|
|
135
|
+
return isPrimitive(objB) && Object.is(objA, objB);
|
|
136
|
+
}
|
|
137
|
+
/* For functions (follow the behavior of _.isEqual and compare functions
|
|
138
|
+
* by reference). */
|
|
139
|
+
if (typeof objA === 'function') {
|
|
140
|
+
return typeof objB === 'function' && objA === objB;
|
|
141
|
+
}
|
|
142
|
+
/* For circular references */
|
|
143
|
+
if (refs.has(objA)) {
|
|
144
|
+
return refs.get(objA) === objB;
|
|
145
|
+
}
|
|
146
|
+
refs.set(objA, objB);
|
|
147
|
+
/* For objects */
|
|
148
|
+
if (typeof objA === 'object') {
|
|
149
|
+
if (typeof objB !== 'object') {
|
|
121
150
|
return false;
|
|
122
151
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
152
|
+
/* For arrays */
|
|
153
|
+
if (Array.isArray(objA)) {
|
|
154
|
+
return Array.isArray(objB) &&
|
|
155
|
+
objA.length === objB.length &&
|
|
156
|
+
!objA.some((val, idx) => !isDeepEqual(val, objB[idx], ignoreAttributes, refs));
|
|
157
|
+
}
|
|
158
|
+
/* For RegExp */
|
|
159
|
+
if (objA instanceof RegExp) {
|
|
160
|
+
return objB instanceof RegExp &&
|
|
161
|
+
objA.source === objB.source &&
|
|
162
|
+
objA.flags === objB.flags;
|
|
163
|
+
}
|
|
164
|
+
/* For Date */
|
|
165
|
+
if (objA instanceof Date) {
|
|
166
|
+
return objB instanceof Date && objA.getTime() === objB.getTime();
|
|
167
|
+
}
|
|
168
|
+
/* This should be an object */
|
|
169
|
+
const aRec = objA;
|
|
170
|
+
const bRec = objB;
|
|
171
|
+
const aKeys = Object.keys(aRec).filter((key) => !ignoreAttributes.includes(key));
|
|
172
|
+
const bKeys = Object.keys(bRec).filter((key) => !ignoreAttributes.includes(key));
|
|
173
|
+
const differentKeyFound = aKeys.some((key) => {
|
|
174
|
+
return !bKeys.includes(key) ||
|
|
175
|
+
!isDeepEqual(aRec[key], bRec[key], ignoreAttributes, refs);
|
|
134
176
|
});
|
|
135
|
-
|
|
177
|
+
return aKeys.length === bKeys.length && !differentKeyFound;
|
|
178
|
+
}
|
|
179
|
+
return true;
|
|
136
180
|
}
|
|
137
181
|
let displayLog = false;
|
|
138
182
|
function debug(fName, step, ...args) {
|
|
@@ -1186,7 +1230,7 @@ class SelecticStore {
|
|
|
1186
1230
|
/* update cache */
|
|
1187
1231
|
state.totalDynOptions = total;
|
|
1188
1232
|
const old = state.dynOptions.splice(offset, result.length, ...result);
|
|
1189
|
-
if (
|
|
1233
|
+
if (isDeepEqual(old, result)) {
|
|
1190
1234
|
/* Added options are the same as previous ones.
|
|
1191
1235
|
* Stop fetching to avoid infinite loop
|
|
1192
1236
|
*/
|
|
@@ -2448,8 +2492,10 @@ let List = class List extends Vue {
|
|
|
2448
2492
|
onOffsetChange() {
|
|
2449
2493
|
this.checkOffset();
|
|
2450
2494
|
}
|
|
2451
|
-
onFilteredOptionsChange() {
|
|
2452
|
-
|
|
2495
|
+
onFilteredOptionsChange(oldVal, newVal) {
|
|
2496
|
+
if (!isDeepEqual(oldVal, newVal)) {
|
|
2497
|
+
this.checkOffset();
|
|
2498
|
+
}
|
|
2453
2499
|
}
|
|
2454
2500
|
onGroupIdChange() {
|
|
2455
2501
|
this.$emit('groupId', this.groupId);
|
|
@@ -2562,42 +2608,6 @@ let ExtendedList = class ExtendedList extends Vue {
|
|
|
2562
2608
|
}
|
|
2563
2609
|
return '';
|
|
2564
2610
|
}
|
|
2565
|
-
get onKeyDown() {
|
|
2566
|
-
return (evt) => {
|
|
2567
|
-
const key = evt.key;
|
|
2568
|
-
if (key === 'Escape') {
|
|
2569
|
-
this.store.commit('isOpen', false);
|
|
2570
|
-
}
|
|
2571
|
-
else if (key === 'Enter') {
|
|
2572
|
-
const index = this.store.state.activeItemIdx;
|
|
2573
|
-
if (index !== -1) {
|
|
2574
|
-
const item = this.store.state.filteredOptions[index];
|
|
2575
|
-
if (!item.disabled && !item.isGroup) {
|
|
2576
|
-
this.store.selectItem(item.id);
|
|
2577
|
-
}
|
|
2578
|
-
}
|
|
2579
|
-
evt.stopPropagation();
|
|
2580
|
-
evt.preventDefault();
|
|
2581
|
-
}
|
|
2582
|
-
else if (key === 'ArrowUp') {
|
|
2583
|
-
const index = this.store.state.activeItemIdx;
|
|
2584
|
-
if (index > 0) {
|
|
2585
|
-
this.store.commit('activeItemIdx', index - 1);
|
|
2586
|
-
}
|
|
2587
|
-
evt.stopPropagation();
|
|
2588
|
-
evt.preventDefault();
|
|
2589
|
-
}
|
|
2590
|
-
else if (key === 'ArrowDown') {
|
|
2591
|
-
const index = this.store.state.activeItemIdx;
|
|
2592
|
-
const max = this.store.state.totalFilteredOptions - 1;
|
|
2593
|
-
if (index < max) {
|
|
2594
|
-
this.store.commit('activeItemIdx', index + 1);
|
|
2595
|
-
}
|
|
2596
|
-
evt.stopPropagation();
|
|
2597
|
-
evt.preventDefault();
|
|
2598
|
-
}
|
|
2599
|
-
};
|
|
2600
|
-
}
|
|
2601
2611
|
get bestPosition() {
|
|
2602
2612
|
const windowHeight = window.innerHeight;
|
|
2603
2613
|
const isFullyEstimated = this.isFullyEstimated;
|
|
@@ -2713,6 +2723,40 @@ let ExtendedList = class ExtendedList extends Vue {
|
|
|
2713
2723
|
clickHeaderGroup() {
|
|
2714
2724
|
this.store.selectGroup(this.topGroupId, !this.topGroupSelected);
|
|
2715
2725
|
}
|
|
2726
|
+
onKeyDown(evt) {
|
|
2727
|
+
const key = evt.key;
|
|
2728
|
+
if (key === 'Escape') {
|
|
2729
|
+
this.store.commit('isOpen', false);
|
|
2730
|
+
}
|
|
2731
|
+
else if (key === 'Enter') {
|
|
2732
|
+
const index = this.store.state.activeItemIdx;
|
|
2733
|
+
if (index !== -1) {
|
|
2734
|
+
const item = this.store.state.filteredOptions[index];
|
|
2735
|
+
if (!item.disabled && !item.isGroup) {
|
|
2736
|
+
this.store.selectItem(item.id);
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
evt.stopPropagation();
|
|
2740
|
+
evt.preventDefault();
|
|
2741
|
+
}
|
|
2742
|
+
else if (key === 'ArrowUp') {
|
|
2743
|
+
const index = this.store.state.activeItemIdx;
|
|
2744
|
+
if (index > 0) {
|
|
2745
|
+
this.store.commit('activeItemIdx', index - 1);
|
|
2746
|
+
}
|
|
2747
|
+
evt.stopPropagation();
|
|
2748
|
+
evt.preventDefault();
|
|
2749
|
+
}
|
|
2750
|
+
else if (key === 'ArrowDown') {
|
|
2751
|
+
const index = this.store.state.activeItemIdx;
|
|
2752
|
+
const max = this.store.state.totalFilteredOptions - 1;
|
|
2753
|
+
if (index < max) {
|
|
2754
|
+
this.store.commit('activeItemIdx', index + 1);
|
|
2755
|
+
}
|
|
2756
|
+
evt.stopPropagation();
|
|
2757
|
+
evt.preventDefault();
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2716
2760
|
/* }}} */
|
|
2717
2761
|
/* {{{ Life cycles */
|
|
2718
2762
|
mounted() {
|
|
@@ -3011,7 +3055,7 @@ let Selectic = class Selectic extends Vue {
|
|
|
3011
3055
|
else {
|
|
3012
3056
|
this.removeListeners();
|
|
3013
3057
|
if (state.status.hasChanged) {
|
|
3014
|
-
this
|
|
3058
|
+
this.emit('change', this.getValue(), state.selectionIsExcluded);
|
|
3015
3059
|
this.store.resetChange();
|
|
3016
3060
|
}
|
|
3017
3061
|
this.emit('close');
|
package/package.json
CHANGED
package/src/ExtendedList.tsx
CHANGED
|
@@ -97,48 +97,6 @@ export default class ExtendedList extends Vue<Props> {
|
|
|
97
97
|
return '';
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
get onKeyDown() {
|
|
101
|
-
return (evt: KeyboardEvent) => {
|
|
102
|
-
const key = evt.key;
|
|
103
|
-
|
|
104
|
-
if (key === 'Escape') {
|
|
105
|
-
this.store.commit('isOpen', false);
|
|
106
|
-
} else
|
|
107
|
-
if (key === 'Enter') {
|
|
108
|
-
const index = this.store.state.activeItemIdx;
|
|
109
|
-
|
|
110
|
-
if (index !== -1) {
|
|
111
|
-
const item = this.store.state.filteredOptions[index];
|
|
112
|
-
|
|
113
|
-
if (!item.disabled && !item.isGroup) {
|
|
114
|
-
this.store.selectItem(item.id);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
evt.stopPropagation();
|
|
118
|
-
evt.preventDefault();
|
|
119
|
-
} else
|
|
120
|
-
if (key === 'ArrowUp') {
|
|
121
|
-
const index = this.store.state.activeItemIdx;
|
|
122
|
-
|
|
123
|
-
if (index > 0) {
|
|
124
|
-
this.store.commit('activeItemIdx', index - 1);
|
|
125
|
-
}
|
|
126
|
-
evt.stopPropagation();
|
|
127
|
-
evt.preventDefault();
|
|
128
|
-
} else
|
|
129
|
-
if (key === 'ArrowDown') {
|
|
130
|
-
const index = this.store.state.activeItemIdx;
|
|
131
|
-
const max = this.store.state.totalFilteredOptions - 1;
|
|
132
|
-
|
|
133
|
-
if (index < max) {
|
|
134
|
-
this.store.commit('activeItemIdx', index + 1);
|
|
135
|
-
}
|
|
136
|
-
evt.stopPropagation();
|
|
137
|
-
evt.preventDefault();
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
100
|
get bestPosition(): 'top' | 'bottom' {
|
|
143
101
|
const windowHeight = window.innerHeight;
|
|
144
102
|
const isFullyEstimated = this.isFullyEstimated;
|
|
@@ -288,6 +246,46 @@ export default class ExtendedList extends Vue<Props> {
|
|
|
288
246
|
this.store.selectGroup(this.topGroupId, !this.topGroupSelected);
|
|
289
247
|
}
|
|
290
248
|
|
|
249
|
+
private onKeyDown(evt: KeyboardEvent) {
|
|
250
|
+
const key = evt.key;
|
|
251
|
+
|
|
252
|
+
if (key === 'Escape') {
|
|
253
|
+
this.store.commit('isOpen', false);
|
|
254
|
+
} else
|
|
255
|
+
if (key === 'Enter') {
|
|
256
|
+
const index = this.store.state.activeItemIdx;
|
|
257
|
+
|
|
258
|
+
if (index !== -1) {
|
|
259
|
+
const item = this.store.state.filteredOptions[index];
|
|
260
|
+
|
|
261
|
+
if (!item.disabled && !item.isGroup) {
|
|
262
|
+
this.store.selectItem(item.id);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
evt.stopPropagation();
|
|
266
|
+
evt.preventDefault();
|
|
267
|
+
} else
|
|
268
|
+
if (key === 'ArrowUp') {
|
|
269
|
+
const index = this.store.state.activeItemIdx;
|
|
270
|
+
|
|
271
|
+
if (index > 0) {
|
|
272
|
+
this.store.commit('activeItemIdx', index - 1);
|
|
273
|
+
}
|
|
274
|
+
evt.stopPropagation();
|
|
275
|
+
evt.preventDefault();
|
|
276
|
+
} else
|
|
277
|
+
if (key === 'ArrowDown') {
|
|
278
|
+
const index = this.store.state.activeItemIdx;
|
|
279
|
+
const max = this.store.state.totalFilteredOptions - 1;
|
|
280
|
+
|
|
281
|
+
if (index < max) {
|
|
282
|
+
this.store.commit('activeItemIdx', index + 1);
|
|
283
|
+
}
|
|
284
|
+
evt.stopPropagation();
|
|
285
|
+
evt.preventDefault();
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
291
289
|
/* }}} */
|
|
292
290
|
/* {{{ Life cycles */
|
|
293
291
|
|
package/src/List.tsx
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* It handles interactions with these items.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {Vue, Component, Prop, Watch, h} from 'vtyx';
|
|
6
|
+
import { Vue, Component, Prop, Watch, h } from 'vtyx';
|
|
7
7
|
import { unref } from 'vue';
|
|
8
8
|
|
|
9
9
|
import Store, {
|
|
@@ -11,6 +11,7 @@ import Store, {
|
|
|
11
11
|
OptionId,
|
|
12
12
|
} from './Store';
|
|
13
13
|
import Icon from './Icon';
|
|
14
|
+
import { isDeepEqual } from './tools';
|
|
14
15
|
|
|
15
16
|
export interface Props {
|
|
16
17
|
store: Store;
|
|
@@ -222,8 +223,10 @@ export default class List extends Vue<Props> {
|
|
|
222
223
|
}
|
|
223
224
|
|
|
224
225
|
@Watch('filteredOptions', { deep: true })
|
|
225
|
-
public onFilteredOptionsChange() {
|
|
226
|
-
|
|
226
|
+
public onFilteredOptionsChange(oldVal: OptionItem[], newVal: OptionItem[]) {
|
|
227
|
+
if (!isDeepEqual(oldVal, newVal)) {
|
|
228
|
+
this.checkOffset();
|
|
229
|
+
}
|
|
227
230
|
}
|
|
228
231
|
|
|
229
232
|
@Watch('groupId')
|
package/src/Store.tsx
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { reactive, watch, unref, computed, ComputedRef } from 'vue';
|
|
8
8
|
import {
|
|
9
9
|
assignObject,
|
|
10
|
-
|
|
10
|
+
isDeepEqual,
|
|
11
11
|
convertToRegExp,
|
|
12
12
|
debug,
|
|
13
13
|
deepClone,
|
|
@@ -1704,7 +1704,7 @@ export default class SelecticStore {
|
|
|
1704
1704
|
state.totalDynOptions = total;
|
|
1705
1705
|
const old = state.dynOptions.splice(offset, result.length, ...result);
|
|
1706
1706
|
|
|
1707
|
-
if (
|
|
1707
|
+
if (isDeepEqual(old, result)) {
|
|
1708
1708
|
/* Added options are the same as previous ones.
|
|
1709
1709
|
* Stop fetching to avoid infinite loop
|
|
1710
1710
|
*/
|
package/src/index.tsx
CHANGED
|
@@ -560,7 +560,7 @@ export default class Selectic extends Vue<Props> {
|
|
|
560
560
|
} else {
|
|
561
561
|
this.removeListeners();
|
|
562
562
|
if (state.status.hasChanged) {
|
|
563
|
-
this
|
|
563
|
+
this.emit('change', this.getValue(), state.selectionIsExcluded);
|
|
564
564
|
this.store.resetChange();
|
|
565
565
|
}
|
|
566
566
|
this.emit('close');
|
package/src/tools.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { unref } from 'vue';
|
|
2
|
-
import { OptionValue } from './Store';
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* Clone the object and its inner properties.
|
|
@@ -8,7 +7,11 @@ import { OptionValue } from './Store';
|
|
|
8
7
|
* @param refs internal reference to object to avoid cyclic references
|
|
9
8
|
* @returns a copy of obj
|
|
10
9
|
*/
|
|
11
|
-
export function deepClone<T = any>(
|
|
10
|
+
export function deepClone<T = any>(
|
|
11
|
+
origObject: T,
|
|
12
|
+
ignoreAttributes: string[] = [],
|
|
13
|
+
refs: WeakMap<any, any> = new WeakMap()
|
|
14
|
+
): T {
|
|
12
15
|
const obj = unref(origObject);
|
|
13
16
|
|
|
14
17
|
/* For circular references */
|
|
@@ -88,34 +91,93 @@ export function assignObject<T>(obj: Partial<T>, ...sourceObjects: Array<Partial
|
|
|
88
91
|
return result as T;
|
|
89
92
|
}
|
|
90
93
|
|
|
91
|
-
/**
|
|
92
|
-
*
|
|
94
|
+
/**
|
|
95
|
+
* Ckeck whether a value is primitive.
|
|
96
|
+
* @returns true if val is primitive and false otherwise.
|
|
97
|
+
*/
|
|
98
|
+
function isPrimitive<T = any>(val: T): boolean {
|
|
99
|
+
/* The value null is treated explicitly because in JavaScript
|
|
100
|
+
* `typeof null === 'object'` is evaluated to `true`.
|
|
101
|
+
*/
|
|
102
|
+
return val === null || (typeof val !== 'object' && typeof val !== 'function');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Performs a deep comparison between two objects to determine if they
|
|
107
|
+
* should be considered equal.
|
|
108
|
+
*
|
|
109
|
+
* @param objA object to compare to objB.
|
|
110
|
+
* @param objB object to compare to objA.
|
|
111
|
+
* @param attributes list of attributes to not compare.
|
|
112
|
+
* @param refs internal reference to object to avoid cyclic references
|
|
113
|
+
* @returns true if objA should be considered equal to objB.
|
|
93
114
|
*/
|
|
94
|
-
export function
|
|
95
|
-
|
|
96
|
-
|
|
115
|
+
export function isDeepEqual<T = any>(
|
|
116
|
+
objA: T,
|
|
117
|
+
objB: T,
|
|
118
|
+
ignoreAttributes: string[] = [],
|
|
119
|
+
refs: WeakMap<any, any> = new WeakMap()
|
|
120
|
+
): boolean {
|
|
121
|
+
objA = unref(objA);
|
|
122
|
+
objB = unref(objB);
|
|
123
|
+
|
|
124
|
+
/* For primitive types */
|
|
125
|
+
if (isPrimitive(objA)) {
|
|
126
|
+
return isPrimitive(objB) && Object.is(objA, objB);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* For functions (follow the behavior of _.isEqual and compare functions
|
|
130
|
+
* by reference). */
|
|
131
|
+
if (typeof objA === 'function') {
|
|
132
|
+
return typeof objB === 'function' && objA === objB;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* For circular references */
|
|
136
|
+
if (refs.has(objA)) {
|
|
137
|
+
return refs.get(objA) === objB;
|
|
97
138
|
}
|
|
139
|
+
refs.set(objA, objB);
|
|
98
140
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (keys.length !== Object.keys(newOption).length) {
|
|
141
|
+
/* For objects */
|
|
142
|
+
if (typeof objA === 'object') {
|
|
143
|
+
if (typeof objB !== 'object') {
|
|
103
144
|
return false;
|
|
104
145
|
}
|
|
105
|
-
return keys.every((optionKey) => {
|
|
106
|
-
const key = optionKey as keyof OptionValue;
|
|
107
|
-
const oldValue = oldOption[key];
|
|
108
|
-
const newValue = newOption[key];
|
|
109
146
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
147
|
+
/* For arrays */
|
|
148
|
+
if (Array.isArray(objA)) {
|
|
149
|
+
return Array.isArray(objB) &&
|
|
150
|
+
objA.length === objB.length &&
|
|
151
|
+
!objA.some((val, idx) => !isDeepEqual(val, (objB as unknown[])[idx], ignoreAttributes, refs));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* For RegExp */
|
|
155
|
+
if (objA instanceof RegExp) {
|
|
156
|
+
return objB instanceof RegExp &&
|
|
157
|
+
objA.source === objB.source &&
|
|
158
|
+
objA.flags === objB.flags;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* For Date */
|
|
162
|
+
if (objA instanceof Date) {
|
|
163
|
+
return objB instanceof Date && objA.getTime() === objB.getTime();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* This should be an object */
|
|
167
|
+
const aRec = objA as Record<string, any>;
|
|
168
|
+
const bRec = objB as Record<string, any>;
|
|
169
|
+
const aKeys = Object.keys(aRec).filter((key) => !ignoreAttributes.includes(key));
|
|
170
|
+
const bKeys = Object.keys(bRec).filter((key) => !ignoreAttributes.includes(key));
|
|
171
|
+
|
|
172
|
+
const differentKeyFound = aKeys.some((key) => {
|
|
173
|
+
return !bKeys.includes(key) ||
|
|
174
|
+
!isDeepEqual(aRec[key], bRec[key], ignoreAttributes, refs);
|
|
117
175
|
});
|
|
118
|
-
|
|
176
|
+
|
|
177
|
+
return aKeys.length === bKeys.length && !differentKeyFound;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return true;
|
|
119
181
|
}
|
|
120
182
|
|
|
121
183
|
let displayLog = false;
|
package/test/Tools/tools.spec.js
CHANGED
|
@@ -5,6 +5,7 @@ const {
|
|
|
5
5
|
assignObject,
|
|
6
6
|
convertToRegExp,
|
|
7
7
|
deepClone,
|
|
8
|
+
isDeepEqual,
|
|
8
9
|
} = toolFile;
|
|
9
10
|
|
|
10
11
|
tape.test('assignObject()', (st) => {
|
|
@@ -402,3 +403,153 @@ tape.test('deepClone()', (st) => {
|
|
|
402
403
|
tst.end();
|
|
403
404
|
});
|
|
404
405
|
});
|
|
406
|
+
|
|
407
|
+
tape.test('isDeepEqual()', (st) => {
|
|
408
|
+
const fn = () => {};
|
|
409
|
+
const obj = {
|
|
410
|
+
a: 1,
|
|
411
|
+
b: 'b',
|
|
412
|
+
c: false,
|
|
413
|
+
d: undefined,
|
|
414
|
+
e: null,
|
|
415
|
+
f: {},
|
|
416
|
+
g: fn,
|
|
417
|
+
spe1: NaN,
|
|
418
|
+
spe2: Infinity,
|
|
419
|
+
spe3: '',
|
|
420
|
+
spe4: [],
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
st.test('should compare simple objects', (tst) => {
|
|
424
|
+
const objA = obj;
|
|
425
|
+
const objB = deepClone(obj);
|
|
426
|
+
|
|
427
|
+
tst.is(isDeepEqual(objB, objA), true, 'should be equal');
|
|
428
|
+
|
|
429
|
+
objB.a = objB.a.toString();
|
|
430
|
+
tst.is(isDeepEqual(objA, objB), false, 'should not be equal');
|
|
431
|
+
tst.end();
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
st.test('should compare nested objects', (tst) => {
|
|
435
|
+
const deep1 = obj;
|
|
436
|
+
const deep2 = {
|
|
437
|
+
deep: deep1,
|
|
438
|
+
added: 'a value',
|
|
439
|
+
};
|
|
440
|
+
const objA = {
|
|
441
|
+
d: deep2,
|
|
442
|
+
}
|
|
443
|
+
const objB = deepClone(objA);
|
|
444
|
+
|
|
445
|
+
tst.is(isDeepEqual(objA, objB), true, 'should be equal');
|
|
446
|
+
tst.end();
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
st.test('with arrays', (tst) => {
|
|
450
|
+
const nestedArray1 = [
|
|
451
|
+
obj,
|
|
452
|
+
{ a: 'value' },
|
|
453
|
+
null,
|
|
454
|
+
undefined,
|
|
455
|
+
42,
|
|
456
|
+
'value',
|
|
457
|
+
];
|
|
458
|
+
const nestedArray2 = deepClone(nestedArray1);
|
|
459
|
+
const arrayA = [
|
|
460
|
+
{
|
|
461
|
+
deep: nestedArray1,
|
|
462
|
+
},
|
|
463
|
+
nestedArray2,
|
|
464
|
+
42, 'value', null, undefined, [[]],
|
|
465
|
+
];
|
|
466
|
+
const arrayB = deepClone(arrayA);
|
|
467
|
+
|
|
468
|
+
tst.is(isDeepEqual(arrayA, arrayB), true, 'should be equal');
|
|
469
|
+
tst.end();
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
st.test('with RegExp', (tst) => {
|
|
473
|
+
const regA1 = /hello?/gi;
|
|
474
|
+
const regA2 = /.* [aA]+?/;
|
|
475
|
+
|
|
476
|
+
const regB1 = deepClone(regA1);
|
|
477
|
+
const regB2 = {rgx: regA2};
|
|
478
|
+
|
|
479
|
+
tst.is(isDeepEqual(regA1, regB1), true, 'should be equal');
|
|
480
|
+
tst.is(isDeepEqual(regA2, regB2), false, 'should not be equal');
|
|
481
|
+
tst.end();
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
st.test('with Date', (tst) => {
|
|
485
|
+
const dateA = new Date('1996-06-27');
|
|
486
|
+
const dateB = new Date('1996-06-27');
|
|
487
|
+
|
|
488
|
+
tst.is(isDeepEqual(dateA, dateB), true, 'should be equal');
|
|
489
|
+
tst.end();
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
st.test('with functions', (tst) => {
|
|
493
|
+
const fn1 = fn;
|
|
494
|
+
const fn2 = () => {};
|
|
495
|
+
|
|
496
|
+
tst.is(isDeepEqual(fn1, fn), true, 'should be equal');
|
|
497
|
+
tst.is(isDeepEqual(fn2, fn), false, 'should not be equal');
|
|
498
|
+
tst.end();
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
st.test('with primitives', (tst) => {
|
|
502
|
+
tst.is(isDeepEqual(10%3, 1), true, 'should be equal');
|
|
503
|
+
tst.is(isDeepEqual('Bingo'[0], 'B'), true, 'should be equal');
|
|
504
|
+
tst.is(isDeepEqual(!true, false), true, 'should be equal');
|
|
505
|
+
tst.is(isDeepEqual(obj.e, null), true, 'should be equal');
|
|
506
|
+
tst.is(isDeepEqual(obj.d, undefined), true, 'should be equal');
|
|
507
|
+
tst.end();
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
st.test('with circular references', (tst) => {
|
|
511
|
+
const obj1 = {
|
|
512
|
+
a: 'a',
|
|
513
|
+
}
|
|
514
|
+
const obj2 = {
|
|
515
|
+
b: 'b',
|
|
516
|
+
}
|
|
517
|
+
obj1.child = obj1;
|
|
518
|
+
obj1.sibling = obj2;
|
|
519
|
+
obj2.sibling = obj1;
|
|
520
|
+
|
|
521
|
+
const objA = [obj1, obj2];
|
|
522
|
+
const objB = deepClone(objA);
|
|
523
|
+
|
|
524
|
+
tst.is(isDeepEqual(objA, objB), true, 'should be equal');
|
|
525
|
+
tst.end();
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
st.test('can ignore some attributes', (tst) => {
|
|
529
|
+
const noCompare1 = {
|
|
530
|
+
ref: 1,
|
|
531
|
+
};
|
|
532
|
+
const noCompare2 = new Set(['alpha', 'omega']);
|
|
533
|
+
const noCompare3 = new Map([[1, 'alpha'], [22, 'omega']]);
|
|
534
|
+
const deep1 = {
|
|
535
|
+
a: 'alpha',
|
|
536
|
+
noCompare: noCompare3,
|
|
537
|
+
};
|
|
538
|
+
const objA = {
|
|
539
|
+
id: 'ref',
|
|
540
|
+
noCopy: noCompare1,
|
|
541
|
+
nop: noCompare2,
|
|
542
|
+
not: 42,
|
|
543
|
+
deep: deep1,
|
|
544
|
+
};
|
|
545
|
+
const objB = deepClone(objA);
|
|
546
|
+
objB.not = 24;
|
|
547
|
+
|
|
548
|
+
const result1 = isDeepEqual(objA, objB, ['noCopy', 'nothing', 'nop', 'not']);
|
|
549
|
+
const result2 = isDeepEqual(objA, objB, ['noCopy', 'nothing', 'nop']);
|
|
550
|
+
|
|
551
|
+
tst.is(result1, true, 'should be equal');
|
|
552
|
+
tst.is(result2, false, 'should not be equal');
|
|
553
|
+
tst.end();
|
|
554
|
+
});
|
|
555
|
+
});
|
package/types/ExtendedList.d.ts
CHANGED
|
@@ -26,7 +26,6 @@ export default class ExtendedList extends Vue<Props> {
|
|
|
26
26
|
get searching(): boolean;
|
|
27
27
|
get errorMessage(): string;
|
|
28
28
|
get infoMessage(): string;
|
|
29
|
-
get onKeyDown(): (evt: KeyboardEvent) => void;
|
|
30
29
|
get bestPosition(): 'top' | 'bottom';
|
|
31
30
|
get position(): 'top' | 'bottom';
|
|
32
31
|
get horizontalStyle(): string;
|
|
@@ -39,6 +38,7 @@ export default class ExtendedList extends Vue<Props> {
|
|
|
39
38
|
private getGroup;
|
|
40
39
|
private computeListSize;
|
|
41
40
|
private clickHeaderGroup;
|
|
41
|
+
private onKeyDown;
|
|
42
42
|
mounted(): void;
|
|
43
43
|
unmounted(): void;
|
|
44
44
|
render(): h.JSX.Element;
|
package/types/List.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ export default class List extends Vue<Props> {
|
|
|
30
30
|
private onMouseOver;
|
|
31
31
|
onIndexChange(): void;
|
|
32
32
|
onOffsetChange(): void;
|
|
33
|
-
onFilteredOptionsChange(): void;
|
|
33
|
+
onFilteredOptionsChange(oldVal: OptionItem[], newVal: OptionItem[]): void;
|
|
34
34
|
onGroupIdChange(): void;
|
|
35
35
|
mounted(): void;
|
|
36
36
|
render(): h.JSX.Element;
|
package/types/tools.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { OptionValue } from './Store';
|
|
2
1
|
/**
|
|
3
2
|
* Clone the object and its inner properties.
|
|
4
3
|
* @param obj The object to be clone.
|
|
@@ -21,10 +20,17 @@ export declare function deepClone<T = any>(origObject: T, ignoreAttributes?: str
|
|
|
21
20
|
export declare function convertToRegExp(name: string, flag?: string): RegExp;
|
|
22
21
|
/** Does the same as Object.assign but does not replace if value is undefined */
|
|
23
22
|
export declare function assignObject<T>(obj: Partial<T>, ...sourceObjects: Array<Partial<T>>): T;
|
|
24
|
-
/**
|
|
25
|
-
*
|
|
23
|
+
/**
|
|
24
|
+
* Performs a deep comparison between two objects to determine if they
|
|
25
|
+
* should be considered equal.
|
|
26
|
+
*
|
|
27
|
+
* @param objA object to compare to objB.
|
|
28
|
+
* @param objB object to compare to objA.
|
|
29
|
+
* @param attributes list of attributes to not compare.
|
|
30
|
+
* @param refs internal reference to object to avoid cyclic references
|
|
31
|
+
* @returns true if objA should be considered equal to objB.
|
|
26
32
|
*/
|
|
27
|
-
export declare function
|
|
33
|
+
export declare function isDeepEqual<T = any>(objA: T, objB: T, ignoreAttributes?: string[], refs?: WeakMap<any, any>): boolean;
|
|
28
34
|
export declare function debug(fName: string, step: string, ...args: any[]): void;
|
|
29
35
|
export declare namespace debug {
|
|
30
36
|
var enable: (display: boolean) => void;
|