@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 +1 -1
- package/src/components/smart-select.vue +135 -83
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
/**
|
|
90
|
+
* EFFECTIVE PROPS
|
|
91
|
+
*/
|
|
90
92
|
effectiveDisabled() {
|
|
91
|
-
return this.disabled || !this.
|
|
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.
|
|
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
|
-
|
|
160
|
-
return this.
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
169
|
-
return this.
|
|
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.
|
|
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
|
-
|
|
212
|
-
|
|
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
|
-
|
|
218
|
-
|
|
241
|
+
optionsDescriptors() {
|
|
242
|
+
if (this.shouldDisplayOptions) {
|
|
243
|
+
setTimeout(this.highlightInitialOption, 0);
|
|
244
|
+
}
|
|
219
245
|
},
|
|
220
246
|
|
|
221
247
|
searchText() {
|
|
222
|
-
if
|
|
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.
|
|
249
|
-
// this.buildDecoratedOptions();
|
|
277
|
+
this.loadedOptions = this.options;
|
|
250
278
|
this.isLoaded = true;
|
|
251
279
|
} else if (this.$isPropTruthy(this.preload)) {
|
|
252
|
-
this.
|
|
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
|
|
260
|
-
|
|
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
|
|
301
|
+
async loadRemoteOptions() {
|
|
266
302
|
await this.reloadOptions();
|
|
267
|
-
this.$emit('optionsLoaded', this.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
323
|
+
this.loadedOptions = result.data;
|
|
288
324
|
this.isLoaded = true;
|
|
289
325
|
},
|
|
290
326
|
|
|
291
327
|
reloadOptionsIfSearching() {
|
|
292
|
-
this.isSearching
|
|
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)
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
};
|