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.
@@ -111,32 +111,76 @@ function assignObject(obj, ...sourceObjects) {
111
111
  }
112
112
  return result;
113
113
  }
114
- /** Compare 2 list of options.
115
- * @returns true if there are no difference
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 compareOptions(oldOptions, newOptions) {
118
- if (oldOptions.length !== newOptions.length) {
119
- return false;
120
- }
121
- return oldOptions.every((oldOption, idx) => {
122
- const newOption = newOptions[idx];
123
- const keys = Object.keys(oldOption);
124
- if (keys.length !== Object.keys(newOption).length) {
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
- return keys.every((optionKey) => {
128
- const key = optionKey;
129
- const oldValue = oldOption[key];
130
- const newValue = newOption[key];
131
- if (key === 'options') {
132
- return compareOptions(oldValue, newValue);
133
- }
134
- if (key === 'data') {
135
- return JSON.stringify(oldValue) === JSON.stringify(newValue);
136
- }
137
- return oldValue === newValue;
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 (compareOptions(old, result)) {
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
- this.checkOffset();
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.$emit('change', this.getValue(), state.selectionIsExcluded, this);
3062
+ this.emit('change', this.getValue(), state.selectionIsExcluded);
3019
3063
  this.store.resetChange();
3020
3064
  }
3021
3065
  this.emit('close');
@@ -107,32 +107,76 @@ function assignObject(obj, ...sourceObjects) {
107
107
  }
108
108
  return result;
109
109
  }
110
- /** Compare 2 list of options.
111
- * @returns true if there are no difference
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 compareOptions(oldOptions, newOptions) {
114
- if (oldOptions.length !== newOptions.length) {
115
- return false;
116
- }
117
- return oldOptions.every((oldOption, idx) => {
118
- const newOption = newOptions[idx];
119
- const keys = Object.keys(oldOption);
120
- if (keys.length !== Object.keys(newOption).length) {
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
- return keys.every((optionKey) => {
124
- const key = optionKey;
125
- const oldValue = oldOption[key];
126
- const newValue = newOption[key];
127
- if (key === 'options') {
128
- return compareOptions(oldValue, newValue);
129
- }
130
- if (key === 'data') {
131
- return JSON.stringify(oldValue) === JSON.stringify(newValue);
132
- }
133
- return oldValue === newValue;
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 (compareOptions(old, result)) {
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
- this.checkOffset();
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.$emit('change', this.getValue(), state.selectionIsExcluded, 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "selectic",
3
- "version": "3.1.3",
3
+ "version": "3.1.5",
4
4
  "description": "Smart Select for VueJS 3.x",
5
5
  "main": "dist/selectic.common.js",
6
6
  "module": "dist/selectic.esm.js",
@@ -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
- this.checkOffset();
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
- compareOptions,
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 (compareOptions(old, result)) {
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.$emit('change', this.getValue(), state.selectionIsExcluded, 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>(origObject: T, ignoreAttributes: string[] = [], refs: WeakMap<any, any> = new WeakMap()): T {
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
- /** Compare 2 list of options.
92
- * @returns true if there are no difference
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 compareOptions(oldOptions: OptionValue[], newOptions: OptionValue[]): boolean {
95
- if (oldOptions.length !== newOptions.length) {
96
- return false;
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
- return oldOptions.every((oldOption, idx) => {
100
- const newOption = newOptions[idx];
101
- const keys = Object.keys(oldOption);
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
- 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;
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;
@@ -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
+ });
@@ -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
- /** Compare 2 list of options.
25
- * @returns true if there are no difference
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 compareOptions(oldOptions: OptionValue[], newOptions: OptionValue[]): boolean;
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;