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