@signal24/vue-foundation 3.6.0 → 3.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signal24/vue-foundation",
3
- "version": "3.6.0",
3
+ "version": "3.7.2",
4
4
  "description": "Common components, directives, and helpers for Vue 3 apps",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -37,6 +37,7 @@
37
37
 
38
38
  <script>
39
39
  import debounce from 'lodash/debounce';
40
+ import isEqual from 'lodash/isEqual';
40
41
 
41
42
  const nullSymbol = Symbol(null);
42
43
  const createSymbol = Symbol('create');
@@ -49,6 +50,8 @@ export default {
49
50
  props: [
50
51
  'modelValue',
51
52
  'options',
53
+ 'prependOptions',
54
+ 'appendOptions',
52
55
  'preload',
53
56
  'url',
54
57
  'urlParams',
@@ -71,7 +74,7 @@ export default {
71
74
  data() {
72
75
  return {
73
76
  isLoaded: false,
74
- resolvedOptions: [],
77
+ loadedOptions: [],
75
78
  isSearching: false,
76
79
  searchText: '',
77
80
  selectedOption: null,
@@ -83,12 +86,11 @@ export default {
83
86
  },
84
87
 
85
88
  computed: {
86
- hasManualOptionsObject() {
87
- return this.options && typeof this.options === 'object' && !Array.isArray(this.options);
88
- },
89
-
89
+ /**
90
+ * EFFECTIVE PROPS
91
+ */
90
92
  effectiveDisabled() {
91
- return this.disabled || !this.resolvedOptions;
93
+ return this.disabled || !this.loadedOptions;
92
94
  },
93
95
 
94
96
  effectivePlaceholder() {
@@ -97,47 +99,6 @@ export default {
97
99
  return this.placeholder || '';
98
100
  },
99
101
 
100
- effectiveOptions() {
101
- let options = [...this.decoratedOptions];
102
-
103
- if (this.isSearching) {
104
- const strippedSearchText = this.searchText.trim().toLowerCase();
105
-
106
- if (strippedSearchText.length) {
107
- options = options.filter(option => option.searchContent.includes(strippedSearchText));
108
-
109
- const escapedSearchText = this.searchText.escapeHtml().replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
110
- const searchRe = new RegExp(`(${escapedSearchText})`, 'ig');
111
-
112
- options = options.map(option => {
113
- option = { ...option };
114
- option.titleHtml = option.titleHtml.replace(searchRe, '<mark>$1</mark>');
115
- if (option.subtitleHtml)
116
- option.subtitleHtml = option.subtitleHtml.replace(searchRe, '<mark>$1</mark>');
117
- return option;
118
- });
119
-
120
- if (this.shouldShowCreateOption) {
121
- const hasExactMatch =
122
- options.find(option => option.searchContent === strippedSearchText) !== undefined;
123
- if (!hasExactMatch) {
124
- options.push({
125
- key: createSymbol,
126
- titleHtml: 'Create <strong>' + this.searchText.trim() + '</strong>...'
127
- });
128
- }
129
- }
130
- }
131
- } else if (this.nullTitle) {
132
- options.unshift({
133
- key: nullSymbol,
134
- titleHtml: this.nullTitle
135
- });
136
- }
137
-
138
- return options;
139
- },
140
-
141
102
  effectiveIdKey() {
142
103
  return this.idKey || 'id';
143
104
  },
@@ -148,7 +109,7 @@ export default {
148
109
 
149
110
  effectiveValueKey() {
150
111
  if (this.valueKey) return this.valueKey;
151
- if (this.hasManualOptionsObject) return this.effectiveIdKey;
112
+ if (this.options && !Array.isArray(this.options)) return this.effectiveIdKey;
152
113
  return undefined;
153
114
  },
154
115
 
@@ -156,17 +117,32 @@ export default {
156
117
  return this.noResultsText || 'No options match your search.';
157
118
  },
158
119
 
159
- resolvedOptionsArray() {
160
- return this.hasManualOptionsObject
161
- ? Object.entries(this.resolvedOptions).map(entry => ({
162
- [this.effectiveIdKey]: entry[0],
163
- [this.effectiveTitleKey]: entry[1]
164
- }))
165
- : this.resolvedOptions;
120
+ effectiveRemoteSearch() {
121
+ return this.$isPropTruthy(this.remoteSearch);
122
+ },
123
+
124
+ /**
125
+ * OPTIONS GENERATION
126
+ */
127
+
128
+ loadedOptionsArray() {
129
+ return this.arrayifyOptions(this.loadedOptions);
130
+ },
131
+
132
+ prependOptionsArray() {
133
+ return this.prependOptions ? this.arrayifyOptions(this.prependOptions) : [];
134
+ },
135
+
136
+ appendOptionsArray() {
137
+ return this.appendOptions ? this.arrayifyOptions(this.appendOptions) : [];
138
+ },
139
+
140
+ fullOptionsArray() {
141
+ return [...this.prependOptionsArray, ...this.loadedOptionsArray, ...this.appendOptionsArray];
166
142
  },
167
143
 
168
- decoratedOptions() {
169
- return this.resolvedOptionsArray.map((option, index) => {
144
+ optionsDescriptors() {
145
+ return this.fullOptionsArray.map((option, index) => {
170
146
  const title = this.getOptionTitle(option);
171
147
  const subtitle = this.getOptionSubtitle(option);
172
148
  const strippedTitle = title ? title.text.trim().toLowerCase() : '';
@@ -190,6 +166,47 @@ export default {
190
166
  ref: option
191
167
  };
192
168
  });
169
+ },
170
+
171
+ effectiveOptions() {
172
+ let options = [...this.optionsDescriptors];
173
+
174
+ if (this.isSearching) {
175
+ const strippedSearchText = this.searchText.trim().toLowerCase();
176
+
177
+ if (strippedSearchText.length) {
178
+ options = options.filter(option => option.searchContent.includes(strippedSearchText));
179
+
180
+ const escapedSearchText = this.searchText.escapeHtml().replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
181
+ const searchRe = new RegExp(`(${escapedSearchText})`, 'ig');
182
+
183
+ options = options.map(option => {
184
+ option = { ...option };
185
+ option.titleHtml = option.titleHtml.replace(searchRe, '<mark>$1</mark>');
186
+ if (option.subtitleHtml)
187
+ option.subtitleHtml = option.subtitleHtml.replace(searchRe, '<mark>$1</mark>');
188
+ return option;
189
+ });
190
+
191
+ if (this.shouldShowCreateOption) {
192
+ const hasExactMatch =
193
+ options.find(option => option.searchContent === strippedSearchText) !== undefined;
194
+ if (!hasExactMatch) {
195
+ options.push({
196
+ key: createSymbol,
197
+ titleHtml: 'Create <strong>' + this.searchText.trim() + '</strong>...'
198
+ });
199
+ }
200
+ }
201
+ }
202
+ } else if (this.nullTitle) {
203
+ options.unshift({
204
+ key: nullSymbol,
205
+ titleHtml: this.nullTitle
206
+ });
207
+ }
208
+
209
+ return options;
193
210
  }
194
211
  },
195
212
 
@@ -201,25 +218,37 @@ export default {
201
218
  },
202
219
 
203
220
  options() {
204
- this.resolvedOptions = this.options;
221
+ this.loadedOptions = this.options;
205
222
  },
206
223
 
207
224
  url() {
225
+ console.log('url changed');
208
226
  this.handleSourceUpdate();
209
227
  },
210
228
 
211
- urlParams() {
212
- this.handleSourceUpdate();
229
+ // we should probably solve this more consistently across the board,
230
+ // but for now: urlParams may be a hardcoded object in the parent, so
231
+ // on re-render, a new object literal may be created, which is *technically*
232
+ // a change that will fire this
233
+ urlParams(newValue, oldValue) {
234
+ if (!isEqual(oldValue, newValue)) {
235
+ this.handleSourceUpdate();
236
+ }
213
237
  },
214
238
 
215
239
  // data
216
240
 
217
- decoratedOptions() {
218
- this.shouldDisplayOptions && setTimeout(this.highlightInitialOption, 0);
241
+ optionsDescriptors() {
242
+ if (this.shouldDisplayOptions) {
243
+ setTimeout(this.highlightInitialOption, 0);
244
+ }
219
245
  },
220
246
 
221
247
  searchText() {
222
- if (this.isSearching && !this.searchText.trim().length) this.isSearching = false;
248
+ // don't disable searching here if it's remote search, as that will need to be done after the fetch
249
+ if (this.isSearching && !this.effectiveRemoteSearch && !this.searchText.trim().length) {
250
+ this.isSearching = false;
251
+ }
223
252
  },
224
253
 
225
254
  shouldDisplayOptions() {
@@ -245,51 +274,61 @@ export default {
245
274
  this.shouldShowCreateOption = this.$attrs['onCreateItem'] !== undefined;
246
275
 
247
276
  if (this.options) {
248
- this.resolvedOptions = this.options;
249
- // this.buildDecoratedOptions();
277
+ this.loadedOptions = this.options;
250
278
  this.isLoaded = true;
251
279
  } else if (this.$isPropTruthy(this.preload)) {
252
- this.performInitialLoad();
280
+ await this.loadRemoteOptions();
253
281
  }
254
282
 
255
283
  this.handleValueChanged();
256
284
 
257
285
  this.$watch('selectedOption', () => {
258
286
  const newValue =
259
- this.selectedOption && this.effectiveValueKey ? this.selectedOption[this.effectiveValueKey] : this.selectedOption;
260
- newValue === this.modelValue || this.$emit('update:modelValue', newValue);
287
+ this.selectedOption && this.effectiveValueKey
288
+ ? this.selectedOption[this.effectiveValueKey]
289
+ : this.selectedOption;
290
+ if (newValue !== this.modelValue) {
291
+ this.$emit('update:modelValue', newValue);
292
+ }
261
293
  });
294
+
295
+ if (this.effectiveRemoteSearch) {
296
+ this.$watch('searchText', debounce(this.reloadOptionsIfSearching, 250));
297
+ }
262
298
  },
263
299
 
264
300
  methods: {
265
- async performInitialLoad() {
301
+ async loadRemoteOptions() {
266
302
  await this.reloadOptions();
267
- this.$emit('optionsLoaded', this.resolvedOptions);
268
-
269
- if (this.$isPropTruthy(this.remoteSearch)) {
270
- this.$watch('searchText', debounce(this.reloadOptionsIfSearching, 250));
271
- }
303
+ this.$emit('optionsLoaded', this.loadedOptions);
272
304
  },
273
305
 
274
306
  handleSourceUpdate() {
307
+ console.log('source updated');
275
308
  if (this.preload) return this.reloadOptions();
276
309
  if (!this.isLoaded) return;
277
310
  this.isLoaded = false;
278
- this.resolvedOptions = [];
311
+ this.loadedOptions = [];
279
312
  },
280
313
 
281
314
  async reloadOptions() {
282
315
  let params = {};
283
316
  this.urlParams && Object.assign(params, this.urlParams);
284
- this.$isPropTruthy(this.remoteSearch) && this.searchText && (params.q = this.searchText);
317
+
318
+ if (this.effectiveRemoteSearch && this.isSearching && this.searchText) {
319
+ params.q = this.searchText;
320
+ }
285
321
 
286
322
  const result = await this.$http.get(this.url, { params: params });
287
- this.resolvedOptions = result.data;
323
+ this.loadedOptions = result.data;
288
324
  this.isLoaded = true;
289
325
  },
290
326
 
291
327
  reloadOptionsIfSearching() {
292
- this.isSearching && this.reloadOptions();
328
+ if (this.isSearching) {
329
+ this.reloadOptions();
330
+ this.isSearching = this.searchText.trim().length > 0;
331
+ }
293
332
  },
294
333
 
295
334
  handleKeyDown(e) {
@@ -331,7 +370,9 @@ export default {
331
370
  }
332
371
 
333
372
  if (e.key === 'Delete' || e.key === 'Backspace') {
334
- if (this.searchText.length > 1) this.isSearching = true;
373
+ if (this.searchText.length > 1) {
374
+ this.isSearching = true;
375
+ }
335
376
  return;
336
377
  }
337
378
 
@@ -363,7 +404,7 @@ export default {
363
404
  },
364
405
 
365
406
  handleOptionsDisplayed() {
366
- this.isLoaded || this.$isPropTruthy(this.preload) || this.performInitialLoad();
407
+ this.isLoaded || this.loadRemoteOptions();
367
408
  this.teleportOptionsContainer();
368
409
  this.optionsListId && this.$refs.optionsContainer.setAttribute('id', this.optionsListId);
369
410
  },
@@ -439,6 +480,8 @@ export default {
439
480
  },
440
481
 
441
482
  selectOption(option) {
483
+ this.isSearching = false;
484
+
442
485
  if (option.key == nullSymbol) {
443
486
  this.searchText = '';
444
487
  this.selectedOption = null;
@@ -450,7 +493,7 @@ export default {
450
493
  this.selectedOptionTitle = null;
451
494
  this.$emit('createItem', createText);
452
495
  } else {
453
- const selectedDecoratedOption = this.decoratedOptions.find(
496
+ const selectedDecoratedOption = this.optionsDescriptors.find(
454
497
  decoratedOption => decoratedOption.key == option.key
455
498
  );
456
499
  const realOption = selectedDecoratedOption.ref;
@@ -465,7 +508,7 @@ export default {
465
508
  handleValueChanged() {
466
509
  if (this.modelValue) {
467
510
  if (this.effectiveValueKey) {
468
- this.selectedOption = this.resolvedOptionsArray.find(
511
+ this.selectedOption = this.fullOptionsArray.find(
469
512
  option => option[this.effectiveValueKey] === this.modelValue
470
513
  );
471
514
  } else {
@@ -530,7 +573,16 @@ export default {
530
573
  },
531
574
 
532
575
  addRemoteOption(option) {
533
- this.resolvedOptions.push(option);
576
+ this.loadedOptions.push(option);
577
+ },
578
+
579
+ arrayifyOptions(options) {
580
+ return Array.isArray(options)
581
+ ? options
582
+ : Object.entries(options).map(entry => ({
583
+ [this.effectiveIdKey]: entry[0],
584
+ [this.effectiveTitleKey]: entry[1]
585
+ }));
534
586
  }
535
587
  }
536
588
  };