@signal24/vue-foundation 4.16.1 → 4.17.1
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/demo/components/demo-root.vue +21 -0
- package/demo/components/demo-vf-smart-select.vue +28 -0
- package/demo/index.html +14 -0
- package/demo/index.ts +10 -0
- package/demo/vite.config.ts +23 -0
- package/dist/demo/components/demo-root.vue.d.ts +2 -0
- package/dist/demo/components/demo-vf-smart-select.vue.d.ts +2 -0
- package/dist/demo/index.d.ts +1 -0
- package/dist/demo/vite.config.d.ts +2 -0
- package/dist/src/components/index.d.ts +5 -5
- package/dist/src/components/overlay-anchor.vue.d.ts +6 -8
- package/dist/src/components/overlay-container.d.ts +1 -1
- package/dist/src/components/toast-helpers.d.ts +1 -1
- package/dist/src/components/vf-ajax-select.vue.d.ts +26 -0
- package/dist/src/components/{alert-modal.vue.d.ts → vf-alert-modal.vue.d.ts} +1 -1
- package/dist/src/components/{ez-smart-select.vue.d.ts → vf-ez-smart-select.vue.d.ts} +14 -6
- package/dist/src/components/{modal.vue.d.ts → vf-modal.vue.d.ts} +9 -11
- package/dist/src/components/vf-smart-select.vue.d.ts +47 -0
- package/dist/src/components/{toast.vue.d.ts → vf-toast.vue.d.ts} +1 -1
- package/dist/vue-foundation.es.js +828 -894
- package/eslint.config.mjs +67 -0
- package/package.json +14 -12
- package/src/components/alert-helpers.ts +1 -1
- package/src/components/index.ts +5 -5
- package/src/components/overlay-container.ts +5 -1
- package/src/components/toast-helpers.ts +1 -1
- package/src/components/vf-ajax-select.vue +61 -0
- package/src/components/{alert-modal.vue → vf-alert-modal.vue} +5 -5
- package/src/components/{ez-smart-select.vue → vf-ez-smart-select.vue} +12 -8
- package/src/components/{modal.vue → vf-modal.vue} +3 -3
- package/src/components/vf-smart-select.vue +587 -0
- package/src/directives/duration.ts +3 -3
- package/src/filters/index.ts +1 -0
- package/src/helpers/array.ts +1 -0
- package/src/helpers/error.ts +4 -0
- package/src/helpers/object.ts +1 -0
- package/tsconfig.app.json +1 -1
- package/tsconfig.node.json +1 -1
- package/tsconfig.vitest.json +2 -2
- package/.eslintrc.cjs +0 -35
- package/dist/src/components/ajax-select.vue.d.ts +0 -19
- package/dist/src/components/smart-select.vue.d.ts +0 -115
- package/src/components/ajax-select.vue +0 -75
- package/src/components/smart-select.vue +0 -609
- /package/src/components/{toast.vue → vf-toast.vue} +0 -0
|
@@ -1,609 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="vf-smart-select" :class="{ disabled: effectiveDisabled, open: shouldDisplayOptions }">
|
|
3
|
-
<input
|
|
4
|
-
v-model="searchText"
|
|
5
|
-
ref="searchField"
|
|
6
|
-
type="text"
|
|
7
|
-
:class="{ nullable: !!nullTitle }"
|
|
8
|
-
@keydown="handleKeyDown"
|
|
9
|
-
:placeholder="effectivePlaceholder"
|
|
10
|
-
v-disabled="effectiveDisabled"
|
|
11
|
-
@focus="handleInputFocused"
|
|
12
|
-
@blur="handleInputBlurred"
|
|
13
|
-
:required="required"
|
|
14
|
-
data-1p-ignore
|
|
15
|
-
/>
|
|
16
|
-
<div v-if="shouldDisplayOptions" ref="optionsContainer" class="vf-smart-select-options">
|
|
17
|
-
<div v-if="!isLoaded" class="no-results">Loading...</div>
|
|
18
|
-
<template v-else>
|
|
19
|
-
<div
|
|
20
|
-
v-for="option in effectiveOptions"
|
|
21
|
-
:key="String(option.key)"
|
|
22
|
-
class="option"
|
|
23
|
-
:class="{
|
|
24
|
-
highlighted: highlightedOptionKey === option.key
|
|
25
|
-
}"
|
|
26
|
-
@mousemove="handleOptionHover(option)"
|
|
27
|
-
@mousedown="selectOption(option)"
|
|
28
|
-
>
|
|
29
|
-
<div class="title" v-html="option.title" />
|
|
30
|
-
<div v-if="option.subtitle" class="subtitle" v-html="option.subtitle" />
|
|
31
|
-
</div>
|
|
32
|
-
<div v-if="!effectiveOptions.length && searchText" class="no-results">
|
|
33
|
-
{{ effectiveNoResultsText }}
|
|
34
|
-
</div>
|
|
35
|
-
</template>
|
|
36
|
-
</div>
|
|
37
|
-
</div>
|
|
38
|
-
</template>
|
|
39
|
-
|
|
40
|
-
<script lang="ts">
|
|
41
|
-
import { debounce, isEqual } from 'lodash';
|
|
42
|
-
import type { PropType } from 'vue';
|
|
43
|
-
|
|
44
|
-
import { escapeHtml } from '../helpers/string';
|
|
45
|
-
|
|
46
|
-
const NullSymbol = Symbol('null');
|
|
47
|
-
const CreateSymbol = Symbol('create');
|
|
48
|
-
|
|
49
|
-
const VALID_KEYS = `\`1234567890-=[]\\;',./~!@#$%^&*()_+{}|:"<>?qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM`;
|
|
50
|
-
|
|
51
|
-
// todo: make type safe when Vue alpha is released
|
|
52
|
-
|
|
53
|
-
export type GenericObject = { [key: string]: any };
|
|
54
|
-
export interface OptionDescriptor {
|
|
55
|
-
key: string | Symbol;
|
|
56
|
-
title: string;
|
|
57
|
-
subtitle?: string | null;
|
|
58
|
-
searchContent?: string;
|
|
59
|
-
ref?: GenericObject;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export default {
|
|
63
|
-
props: {
|
|
64
|
-
modelValue: {
|
|
65
|
-
type: null as unknown as PropType<any>,
|
|
66
|
-
default: null
|
|
67
|
-
},
|
|
68
|
-
loadOptions: Function as PropType<(searchText: string | null) => Promise<GenericObject[]>>,
|
|
69
|
-
options: Object as PropType<GenericObject[]>,
|
|
70
|
-
prependOptions: Object as PropType<GenericObject[]>,
|
|
71
|
-
appendOptions: Object as PropType<GenericObject[]>,
|
|
72
|
-
onCreateItem: Function as PropType<(searchText: string) => void>,
|
|
73
|
-
preload: Boolean as PropType<boolean>,
|
|
74
|
-
remoteSearch: Boolean as PropType<boolean>,
|
|
75
|
-
searchFields: Object as PropType<string[]>,
|
|
76
|
-
placeholder: String as PropType<string>,
|
|
77
|
-
keyExtractor: Function as PropType<(option: any) => string | symbol>,
|
|
78
|
-
valueExtractor: Function as PropType<(option: any) => any>,
|
|
79
|
-
formatter: {
|
|
80
|
-
type: Function as PropType<(option: any) => string>,
|
|
81
|
-
required: true
|
|
82
|
-
},
|
|
83
|
-
subtitleFormatter: Function as PropType<(option: any) => string>,
|
|
84
|
-
nullTitle: String as PropType<string>,
|
|
85
|
-
noResultsText: String as PropType<string>,
|
|
86
|
-
disabled: Boolean as PropType<boolean>,
|
|
87
|
-
optionsListId: String as PropType<string>,
|
|
88
|
-
debug: Boolean as PropType<boolean>,
|
|
89
|
-
required: Boolean as PropType<boolean>
|
|
90
|
-
},
|
|
91
|
-
|
|
92
|
-
emits: {
|
|
93
|
-
optionsLoaded: Object as (options: any[]) => void,
|
|
94
|
-
'update:modelValue': Object as (value: any) => void
|
|
95
|
-
},
|
|
96
|
-
|
|
97
|
-
data() {
|
|
98
|
-
return {
|
|
99
|
-
isLoading: false,
|
|
100
|
-
isLoaded: false,
|
|
101
|
-
loadedOptions: [],
|
|
102
|
-
isSearching: false,
|
|
103
|
-
searchText: '',
|
|
104
|
-
selectedOption: null,
|
|
105
|
-
selectedOptionTitle: null,
|
|
106
|
-
shouldDisplayOptions: false,
|
|
107
|
-
highlightedOptionKey: null,
|
|
108
|
-
shouldShowCreateOption: false
|
|
109
|
-
} as {
|
|
110
|
-
isLoading: boolean;
|
|
111
|
-
isLoaded: boolean;
|
|
112
|
-
loadedOptions: GenericObject[];
|
|
113
|
-
isSearching: boolean;
|
|
114
|
-
searchText: string;
|
|
115
|
-
selectedOption: GenericObject | null;
|
|
116
|
-
selectedOptionTitle: string | null;
|
|
117
|
-
shouldDisplayOptions: boolean;
|
|
118
|
-
highlightedOptionKey: string | Symbol | null;
|
|
119
|
-
shouldShowCreateOption: boolean;
|
|
120
|
-
};
|
|
121
|
-
},
|
|
122
|
-
|
|
123
|
-
computed: {
|
|
124
|
-
/**
|
|
125
|
-
* EFFECTIVE PROPS
|
|
126
|
-
*/
|
|
127
|
-
effectivePrependOptions() {
|
|
128
|
-
return this.prependOptions ?? [];
|
|
129
|
-
},
|
|
130
|
-
|
|
131
|
-
effectiveAppendOptions() {
|
|
132
|
-
return this.appendOptions ?? [];
|
|
133
|
-
},
|
|
134
|
-
|
|
135
|
-
effectiveDisabled() {
|
|
136
|
-
return !!this.disabled; // there was another condition here but it didn't make sense
|
|
137
|
-
},
|
|
138
|
-
|
|
139
|
-
effectivePlaceholder() {
|
|
140
|
-
if (!this.isLoaded && this.preload) return 'Loading...';
|
|
141
|
-
if (this.nullTitle) return this.nullTitle;
|
|
142
|
-
return this.placeholder || '';
|
|
143
|
-
},
|
|
144
|
-
|
|
145
|
-
effectiveNoResultsText() {
|
|
146
|
-
return this.noResultsText || 'No options match your search.';
|
|
147
|
-
},
|
|
148
|
-
|
|
149
|
-
effectiveKeyExtractor() {
|
|
150
|
-
return this.keyExtractor ?? this.valueExtractor;
|
|
151
|
-
},
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* OPTIONS GENERATION
|
|
155
|
-
*/
|
|
156
|
-
|
|
157
|
-
allOptions() {
|
|
158
|
-
return [...this.effectivePrependOptions, ...this.loadedOptions, ...this.effectiveAppendOptions];
|
|
159
|
-
},
|
|
160
|
-
|
|
161
|
-
optionsDescriptors() {
|
|
162
|
-
return this.allOptions.map((option, index) => {
|
|
163
|
-
const title = this.formatter!(option);
|
|
164
|
-
const subtitle = this.subtitleFormatter?.(option);
|
|
165
|
-
const strippedTitle = title ? title.trim().toLowerCase() : '';
|
|
166
|
-
const strippedSubtitle = subtitle ? subtitle.trim().toLowerCase() : '';
|
|
167
|
-
|
|
168
|
-
const searchContent = [];
|
|
169
|
-
if (this.searchFields) {
|
|
170
|
-
this.searchFields.forEach(field => {
|
|
171
|
-
option[field] && searchContent.push(String(option[field]).toLowerCase());
|
|
172
|
-
});
|
|
173
|
-
} else {
|
|
174
|
-
searchContent.push(strippedTitle);
|
|
175
|
-
strippedSubtitle && searchContent.push(strippedSubtitle);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
// eslint-disable-next-line vue/no-use-computed-property-like-method
|
|
180
|
-
key: this.effectiveKeyExtractor?.(option) ?? String(index),
|
|
181
|
-
title,
|
|
182
|
-
subtitle,
|
|
183
|
-
searchContent: searchContent.join(''),
|
|
184
|
-
ref: option
|
|
185
|
-
} as OptionDescriptor;
|
|
186
|
-
});
|
|
187
|
-
},
|
|
188
|
-
|
|
189
|
-
effectiveOptions() {
|
|
190
|
-
let options = [...this.optionsDescriptors];
|
|
191
|
-
|
|
192
|
-
if (this.isSearching) {
|
|
193
|
-
const strippedSearchText = this.searchText.trim().toLowerCase();
|
|
194
|
-
|
|
195
|
-
if (strippedSearchText.length) {
|
|
196
|
-
options = options.filter(option => option.searchContent!.includes(strippedSearchText));
|
|
197
|
-
|
|
198
|
-
const escapedSearchText = escapeHtml(this.searchText).replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
199
|
-
const searchRe = new RegExp(`(${escapedSearchText})`, 'ig');
|
|
200
|
-
|
|
201
|
-
options = options.map(option => ({
|
|
202
|
-
...option,
|
|
203
|
-
title: option.title.replace(searchRe, '<mark>$1</mark>'),
|
|
204
|
-
subtitle: option.subtitle?.replace(searchRe, '<mark>$1</mark>')
|
|
205
|
-
}));
|
|
206
|
-
|
|
207
|
-
if (this.shouldShowCreateOption) {
|
|
208
|
-
const hasExactMatch = options.find(option => option.searchContent === strippedSearchText) !== undefined;
|
|
209
|
-
if (!hasExactMatch) {
|
|
210
|
-
options.push({
|
|
211
|
-
key: CreateSymbol,
|
|
212
|
-
title: 'Create <strong>' + this.searchText.trim() + '</strong>...'
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
} else if (this.nullTitle) {
|
|
218
|
-
options.unshift({
|
|
219
|
-
key: NullSymbol,
|
|
220
|
-
title: this.nullTitle
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return options;
|
|
225
|
-
}
|
|
226
|
-
},
|
|
227
|
-
|
|
228
|
-
watch: {
|
|
229
|
-
// props
|
|
230
|
-
|
|
231
|
-
modelValue() {
|
|
232
|
-
this.handleValueChanged();
|
|
233
|
-
},
|
|
234
|
-
|
|
235
|
-
options() {
|
|
236
|
-
this.loadedOptions = this.options ?? [];
|
|
237
|
-
this.isLoaded = true;
|
|
238
|
-
},
|
|
239
|
-
|
|
240
|
-
// data
|
|
241
|
-
|
|
242
|
-
optionsDescriptors() {
|
|
243
|
-
if (this.shouldDisplayOptions) {
|
|
244
|
-
setTimeout(this.highlightInitialOption, 0);
|
|
245
|
-
}
|
|
246
|
-
},
|
|
247
|
-
|
|
248
|
-
searchText() {
|
|
249
|
-
// don't disable searching here if it's remote search, as that will need to be done after the fetch
|
|
250
|
-
if (this.isSearching && !this.remoteSearch && !this.searchText.trim().length) {
|
|
251
|
-
this.isSearching = false;
|
|
252
|
-
}
|
|
253
|
-
},
|
|
254
|
-
|
|
255
|
-
shouldDisplayOptions() {
|
|
256
|
-
if (this.shouldDisplayOptions) {
|
|
257
|
-
setTimeout(this.handleOptionsDisplayed, 0);
|
|
258
|
-
} else {
|
|
259
|
-
this.isSearching = false;
|
|
260
|
-
this.searchText = this.selectedOptionTitle || '';
|
|
261
|
-
|
|
262
|
-
if (this.$refs.optionsContainer) {
|
|
263
|
-
(this.$refs.optionsContainer as HTMLElement).style.visibility = 'hidden';
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
},
|
|
267
|
-
|
|
268
|
-
effectiveOptions() {
|
|
269
|
-
if (this.modelValue && !this.selectedOption) {
|
|
270
|
-
this.handleValueChanged();
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (this.highlightedOptionKey && !this.effectiveOptions.find(option => option.key == this.highlightedOptionKey)) {
|
|
274
|
-
this.highlightedOptionKey = this.effectiveOptions[0]?.key ?? NullSymbol;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
},
|
|
278
|
-
|
|
279
|
-
async mounted() {
|
|
280
|
-
this.shouldShowCreateOption = this.onCreateItem !== undefined;
|
|
281
|
-
|
|
282
|
-
if (this.options) {
|
|
283
|
-
this.loadedOptions = this.options;
|
|
284
|
-
this.isLoaded = true;
|
|
285
|
-
} else if (this.preload) {
|
|
286
|
-
await this.loadRemoteOptions();
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
this.handleValueChanged();
|
|
290
|
-
|
|
291
|
-
this.$watch('selectedOption', () => {
|
|
292
|
-
if (this.selectedOption !== this.modelValue) {
|
|
293
|
-
this.$emit(
|
|
294
|
-
'update:modelValue',
|
|
295
|
-
this.selectedOption && this.valueExtractor ? this.valueExtractor(this.selectedOption) : this.selectedOption
|
|
296
|
-
);
|
|
297
|
-
}
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
if (this.remoteSearch) {
|
|
301
|
-
this.$watch('searchText', debounce(this.reloadOptionsIfSearching, 250));
|
|
302
|
-
}
|
|
303
|
-
},
|
|
304
|
-
|
|
305
|
-
methods: {
|
|
306
|
-
async loadRemoteOptions() {
|
|
307
|
-
await this.reloadOptions();
|
|
308
|
-
this.loadedOptions && this.$emit('optionsLoaded', this.loadedOptions);
|
|
309
|
-
},
|
|
310
|
-
|
|
311
|
-
async reloadOptions() {
|
|
312
|
-
const searchText = this.remoteSearch && this.isSearching && this.searchText ? this.searchText : null;
|
|
313
|
-
this.isLoading = true;
|
|
314
|
-
this.loadedOptions = (await this.loadOptions?.(searchText)) ?? [];
|
|
315
|
-
this.isLoading = false;
|
|
316
|
-
this.isLoaded = true;
|
|
317
|
-
},
|
|
318
|
-
|
|
319
|
-
reloadOptionsIfSearching() {
|
|
320
|
-
if (this.isSearching) {
|
|
321
|
-
this.reloadOptions();
|
|
322
|
-
this.isSearching = this.searchText.trim().length > 0;
|
|
323
|
-
}
|
|
324
|
-
},
|
|
325
|
-
|
|
326
|
-
handleKeyDown(e: KeyboardEvent) {
|
|
327
|
-
if (e.key == 'Escape') {
|
|
328
|
-
e.stopPropagation();
|
|
329
|
-
(e.target as any).blur();
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
if (e.key == 'ArrowLeft' || e.key == 'ArrowRight') return;
|
|
334
|
-
if (e.key == 'Tab') return;
|
|
335
|
-
|
|
336
|
-
if (!this.isLoaded) {
|
|
337
|
-
this.isSearching || e.preventDefault();
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
if (e.key == 'ArrowUp' || e.key == 'ArrowDown') {
|
|
342
|
-
e.preventDefault();
|
|
343
|
-
return this.incrementHighlightedOption(e.key == 'ArrowUp' ? -1 : 1);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (e.key == 'PageUp' || e.key == 'PageDown') {
|
|
347
|
-
e.preventDefault();
|
|
348
|
-
return this.incrementHighlightedOption(e.key == 'PageUp' ? -10 : 10);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
if (e.key == 'Home' || e.key == 'End') {
|
|
352
|
-
e.preventDefault();
|
|
353
|
-
return this.incrementHighlightedOption(e.key == 'Home' ? -Number.MAX_SAFE_INTEGER : Number.MAX_SAFE_INTEGER);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
if (e.key == 'Enter') {
|
|
357
|
-
e.preventDefault();
|
|
358
|
-
const highlightedOption = this.effectiveOptions.find(option => option.key == this.highlightedOptionKey);
|
|
359
|
-
if (highlightedOption) return this.selectOption(highlightedOption);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
if (e.key === 'Delete' || e.key === 'Backspace') {
|
|
363
|
-
if (this.searchText.length > 1) {
|
|
364
|
-
this.isSearching = true;
|
|
365
|
-
}
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
if (!e.metaKey && VALID_KEYS.includes(e.key)) {
|
|
370
|
-
this.isSearching = true;
|
|
371
|
-
}
|
|
372
|
-
},
|
|
373
|
-
|
|
374
|
-
handleInputFocused() {
|
|
375
|
-
this.setHighlightedOptionKey();
|
|
376
|
-
this.shouldDisplayOptions = true;
|
|
377
|
-
},
|
|
378
|
-
|
|
379
|
-
setHighlightedOptionKey(useFirstItemAsFallback?: boolean) {
|
|
380
|
-
if (this.selectedOption) {
|
|
381
|
-
this.highlightedOptionKey = this.getOptionKey(this.selectedOption);
|
|
382
|
-
} else if (useFirstItemAsFallback) {
|
|
383
|
-
this.highlightedOptionKey = this.effectiveOptions?.[0].key ?? NullSymbol;
|
|
384
|
-
} else if (this.nullTitle) {
|
|
385
|
-
this.highlightedOptionKey = NullSymbol;
|
|
386
|
-
}
|
|
387
|
-
},
|
|
388
|
-
|
|
389
|
-
getOptionKey(option: GenericObject): string | Symbol {
|
|
390
|
-
if (this.effectiveKeyExtractor) {
|
|
391
|
-
// eslint-disable-next-line vue/no-use-computed-property-like-method
|
|
392
|
-
return this.effectiveKeyExtractor(this.selectedOption)!;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
return this.getOptionDescriptor(option)?.key ?? '';
|
|
396
|
-
},
|
|
397
|
-
|
|
398
|
-
getOptionDescriptor(option: GenericObject) {
|
|
399
|
-
const matchedRef = this.effectiveOptions.find(o => o.ref === option);
|
|
400
|
-
if (matchedRef) {
|
|
401
|
-
return matchedRef;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// didn't find an object match, so we'll try a content match. a couple reasons:
|
|
405
|
-
// 1) the initial selection may have come from an owning object and the object as a whole may differ from the full content list
|
|
406
|
-
// 2) for reasons I've yet to determine, the prepend options, although they are wrapped by proxies and have identical content,
|
|
407
|
-
// are not the same proxy object as selectedOption once assigned -- even though the loaded data *is* the same. I've tried
|
|
408
|
-
// setting them as reactive using the same method (via data props rather than computed) and it didn't change anything.
|
|
409
|
-
// therefore, falling back to an isEqual check here when there's no equal object
|
|
410
|
-
const matcher = this.keyExtractor ? (a: GenericObject, b: GenericObject) => this.keyExtractor!(a) === this.keyExtractor!(b) : isEqual;
|
|
411
|
-
const matchedObj = this.effectiveOptions.find(o => matcher(o.ref!, option));
|
|
412
|
-
if (matchedObj) {
|
|
413
|
-
return matchedObj;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
return null;
|
|
417
|
-
},
|
|
418
|
-
|
|
419
|
-
handleInputBlurred() {
|
|
420
|
-
if (this.debug) return;
|
|
421
|
-
|
|
422
|
-
if (!this.searchText.length && this.nullTitle) {
|
|
423
|
-
this.selectedOption = null;
|
|
424
|
-
this.selectedOptionTitle = null;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
this.shouldDisplayOptions = false;
|
|
428
|
-
},
|
|
429
|
-
|
|
430
|
-
handleOptionsDisplayed() {
|
|
431
|
-
this.isLoaded || this.loadRemoteOptions();
|
|
432
|
-
this.optionsListId && (this.$refs.optionsContainer as HTMLElement).setAttribute('id', this.optionsListId);
|
|
433
|
-
this.teleportOptionsContainer();
|
|
434
|
-
},
|
|
435
|
-
|
|
436
|
-
teleportOptionsContainer() {
|
|
437
|
-
const elRect = this.$el.getBoundingClientRect();
|
|
438
|
-
const targetTop = elRect.y + elRect.height + 2;
|
|
439
|
-
const targetLeft = elRect.x;
|
|
440
|
-
|
|
441
|
-
const optionsEl = this.$refs.optionsContainer as HTMLElement;
|
|
442
|
-
const styles = window.getComputedStyle(this.$el);
|
|
443
|
-
|
|
444
|
-
for (let key in styles) {
|
|
445
|
-
if (!/^(font|text)/.test(key)) continue;
|
|
446
|
-
optionsEl.style[key] = styles[key];
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
optionsEl.style.top = targetTop + 'px';
|
|
450
|
-
optionsEl.style.left = targetLeft + 'px';
|
|
451
|
-
optionsEl.style.minWidth = elRect.width + 'px';
|
|
452
|
-
|
|
453
|
-
if (!styles.maxHeight || styles.maxHeight == 'none') {
|
|
454
|
-
const maxHeight = window.innerHeight - targetTop - 12;
|
|
455
|
-
optionsEl.style.maxHeight = maxHeight + 'px';
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
optionsEl.style.visibility = 'visible';
|
|
459
|
-
|
|
460
|
-
document.body.appendChild(optionsEl);
|
|
461
|
-
|
|
462
|
-
setTimeout(this.highlightInitialOption, 0);
|
|
463
|
-
},
|
|
464
|
-
|
|
465
|
-
highlightInitialOption() {
|
|
466
|
-
if (!this.isLoaded) return;
|
|
467
|
-
if (!this.highlightedOptionKey) return;
|
|
468
|
-
const highlightedOptionIdx = this.effectiveOptions.findIndex(option => option.key == this.highlightedOptionKey);
|
|
469
|
-
const containerEl = this.$refs.optionsContainer as HTMLElement;
|
|
470
|
-
const highlightedOptionEl = containerEl.querySelectorAll('.option')[highlightedOptionIdx] as HTMLElement;
|
|
471
|
-
containerEl.scrollTop = highlightedOptionEl.offsetTop;
|
|
472
|
-
},
|
|
473
|
-
|
|
474
|
-
handleOptionHover(option: OptionDescriptor) {
|
|
475
|
-
this.highlightedOptionKey = option ? option.key : null;
|
|
476
|
-
},
|
|
477
|
-
|
|
478
|
-
incrementHighlightedOption(increment: number) {
|
|
479
|
-
const highlightedOptionIdx = this.effectiveOptions.findIndex(option => option.key == this.highlightedOptionKey);
|
|
480
|
-
let targetOptionIdx = highlightedOptionIdx + increment;
|
|
481
|
-
|
|
482
|
-
if (targetOptionIdx < 0) targetOptionIdx = 0;
|
|
483
|
-
else if (targetOptionIdx >= this.effectiveOptions.length) targetOptionIdx = this.effectiveOptions.length - 1;
|
|
484
|
-
|
|
485
|
-
if (highlightedOptionIdx == targetOptionIdx) return;
|
|
486
|
-
|
|
487
|
-
this.highlightedOptionKey = this.effectiveOptions[targetOptionIdx].key;
|
|
488
|
-
|
|
489
|
-
const containerEl = this.$refs.optionsContainer as HTMLElement;
|
|
490
|
-
const targetOptionEl = containerEl.querySelectorAll('.option')[targetOptionIdx] as HTMLElement;
|
|
491
|
-
|
|
492
|
-
if (targetOptionEl.offsetTop < containerEl.scrollTop) {
|
|
493
|
-
containerEl.scrollTop = targetOptionEl.offsetTop;
|
|
494
|
-
} else if (targetOptionEl.offsetTop + targetOptionEl.offsetHeight > containerEl.scrollTop + containerEl.clientHeight) {
|
|
495
|
-
containerEl.scrollTop = targetOptionEl.offsetTop + targetOptionEl.offsetHeight - containerEl.clientHeight;
|
|
496
|
-
}
|
|
497
|
-
},
|
|
498
|
-
|
|
499
|
-
selectOption(option: OptionDescriptor) {
|
|
500
|
-
this.isSearching = false;
|
|
501
|
-
|
|
502
|
-
if (option.key == NullSymbol) {
|
|
503
|
-
this.searchText = '';
|
|
504
|
-
this.selectedOption = null;
|
|
505
|
-
this.selectedOptionTitle = null;
|
|
506
|
-
} else if (option.key === CreateSymbol) {
|
|
507
|
-
const createText = this.searchText.trim();
|
|
508
|
-
this.searchText = '';
|
|
509
|
-
this.selectedOption = null;
|
|
510
|
-
this.selectedOptionTitle = null;
|
|
511
|
-
this.onCreateItem?.(createText);
|
|
512
|
-
} else {
|
|
513
|
-
const selectedDecoratedOption = this.optionsDescriptors.find(decoratedOption => decoratedOption.key == option.key);
|
|
514
|
-
const realOption = selectedDecoratedOption!.ref;
|
|
515
|
-
this.selectedOption = realOption!;
|
|
516
|
-
this.selectedOptionTitle = this.formatter!(realOption!);
|
|
517
|
-
this.searchText = this.selectedOptionTitle || '';
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
(this.$refs.searchField as HTMLElement).blur();
|
|
521
|
-
},
|
|
522
|
-
|
|
523
|
-
handleValueChanged() {
|
|
524
|
-
if (this.modelValue) {
|
|
525
|
-
this.selectedOption = this.valueExtractor ? this.allOptions.find(o => this.modelValue === this.valueExtractor!(o)) : this.modelValue;
|
|
526
|
-
this.selectedOptionTitle = this.selectedOption ? this.formatter!(this.selectedOption) : null;
|
|
527
|
-
this.searchText = this.selectedOptionTitle || '';
|
|
528
|
-
} else {
|
|
529
|
-
this.selectedOption = null;
|
|
530
|
-
this.selectedOptionTitle = null;
|
|
531
|
-
this.searchText = '';
|
|
532
|
-
}
|
|
533
|
-
},
|
|
534
|
-
|
|
535
|
-
addRemoteOption(option: GenericObject) {
|
|
536
|
-
this.loadedOptions.unshift(option);
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
};
|
|
540
|
-
</script>
|
|
541
|
-
|
|
542
|
-
<style lang="scss">
|
|
543
|
-
.vf-smart-select {
|
|
544
|
-
position: relative;
|
|
545
|
-
|
|
546
|
-
input {
|
|
547
|
-
width: 100%;
|
|
548
|
-
padding-right: 24px !important;
|
|
549
|
-
|
|
550
|
-
&.nullable::placeholder {
|
|
551
|
-
color: #000;
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
&:after {
|
|
556
|
-
content: ' ';
|
|
557
|
-
display: block;
|
|
558
|
-
position: absolute;
|
|
559
|
-
top: 50%;
|
|
560
|
-
right: 8px;
|
|
561
|
-
margin-top: -3px;
|
|
562
|
-
width: 0;
|
|
563
|
-
height: 0;
|
|
564
|
-
border-style: solid;
|
|
565
|
-
border-width: 5px 5px 0 5px;
|
|
566
|
-
border-color: #333333 transparent transparent transparent;
|
|
567
|
-
pointer-events: none;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
&.open:after {
|
|
571
|
-
margin-top: -4px;
|
|
572
|
-
border-width: 0 5px 5px 5px;
|
|
573
|
-
border-color: transparent transparent #333333 transparent;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
&:not(.disabled) {
|
|
577
|
-
input {
|
|
578
|
-
cursor: pointer;
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
&.disabled:after {
|
|
583
|
-
opacity: 0.4;
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
.vf-smart-select-options {
|
|
588
|
-
visibility: hidden;
|
|
589
|
-
position: absolute;
|
|
590
|
-
min-height: 20px;
|
|
591
|
-
border: 1px solid #e8e8e8;
|
|
592
|
-
background: white;
|
|
593
|
-
overflow: auto;
|
|
594
|
-
z-index: 101;
|
|
595
|
-
|
|
596
|
-
.option,
|
|
597
|
-
.no-results {
|
|
598
|
-
padding: 5px 8px;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
.option {
|
|
602
|
-
cursor: pointer;
|
|
603
|
-
|
|
604
|
-
&.highlighted {
|
|
605
|
-
background-color: #f5f5f5;
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
</style>
|
|
File without changes
|