@signal24/vue-foundation 3.8.0 → 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.
Files changed (122) hide show
  1. package/.eslintrc.cjs +35 -0
  2. package/.prettierrc.json +4 -2
  3. package/dist/src/components/ajax-select.vue.d.ts +21 -0
  4. package/dist/src/components/alert-helpers.d.ts +8 -0
  5. package/dist/src/components/alert-modal.vue.d.ts +27 -0
  6. package/dist/src/components/ez-smart-select.vue.d.ts +27 -0
  7. package/dist/src/components/index.d.ts +8 -0
  8. package/dist/src/components/modal-container.d.ts +33 -0
  9. package/dist/src/components/modal.vue.d.ts +34 -0
  10. package/dist/src/components/smart-select.vue.d.ts +121 -0
  11. package/dist/src/config.d.ts +8 -0
  12. package/dist/src/directives/autofocus.d.ts +2 -0
  13. package/dist/src/directives/confirm-button.d.ts +2 -0
  14. package/dist/src/directives/date-input.d.ts +2 -0
  15. package/dist/src/directives/datetime.d.ts +2 -0
  16. package/dist/src/directives/disabled.d.ts +2 -0
  17. package/dist/src/directives/duration.d.ts +2 -0
  18. package/dist/src/directives/index.d.ts +24 -0
  19. package/dist/src/directives/infinite-scroll.d.ts +3 -0
  20. package/dist/src/directives/readonly.d.ts +2 -0
  21. package/dist/src/directives/tooltip.d.ts +41 -0
  22. package/dist/src/filters/index.d.ts +39 -0
  23. package/dist/src/helpers/array.d.ts +3 -0
  24. package/dist/src/helpers/context-menu.d.ts +13 -0
  25. package/dist/src/helpers/delay.d.ts +2 -0
  26. package/dist/src/helpers/error.d.ts +7 -0
  27. package/dist/src/helpers/index.d.ts +9 -0
  28. package/dist/src/helpers/mask.d.ts +15 -0
  29. package/dist/src/helpers/number.d.ts +1 -0
  30. package/dist/src/helpers/object.d.ts +2 -0
  31. package/dist/src/helpers/openapi.d.ts +34 -0
  32. package/dist/src/helpers/string.d.ts +5 -0
  33. package/dist/src/hooks/index.d.ts +2 -0
  34. package/dist/src/hooks/infinite-scroll.d.ts +30 -0
  35. package/dist/src/hooks/resize-watcher.d.ts +1 -0
  36. package/dist/src/index.d.ts +8 -0
  37. package/dist/src/types.d.ts +14 -0
  38. package/dist/src/vite-plugins/index.d.ts +1 -0
  39. package/dist/src/vite-plugins/index.js +2 -0
  40. package/dist/src/vite-plugins/vite-openapi-plugin.d.ts +4 -0
  41. package/dist/src/vite-plugins/vite-openapi-plugin.js +58 -0
  42. package/dist/vue-foundation.css +1 -0
  43. package/dist/vue-foundation.es.js +1129 -0
  44. package/package.json +44 -16
  45. package/src/components/ajax-select.vue +44 -23
  46. package/src/components/alert-helpers.ts +45 -0
  47. package/src/components/alert-modal.vue +68 -0
  48. package/src/components/ez-smart-select.vue +51 -0
  49. package/src/components/index.ts +10 -0
  50. package/src/components/modal-container.ts +131 -0
  51. package/src/components/modal.vue +44 -129
  52. package/src/components/smart-select.vue +196 -243
  53. package/src/config.ts +15 -0
  54. package/src/directives/autofocus.ts +20 -0
  55. package/src/directives/confirm-button.ts +50 -0
  56. package/src/directives/date-input.ts +19 -0
  57. package/src/directives/datetime.ts +48 -0
  58. package/src/directives/disabled.ts +30 -0
  59. package/src/directives/duration.ts +79 -0
  60. package/src/directives/index.ts +37 -0
  61. package/src/directives/infinite-scroll.ts +9 -0
  62. package/src/directives/readonly.ts +15 -0
  63. package/src/directives/tooltip.ts +190 -0
  64. package/src/filters/index.ts +79 -0
  65. package/src/helpers/array.ts +7 -0
  66. package/src/helpers/context-menu.ts +108 -0
  67. package/src/helpers/delay.ts +2 -0
  68. package/src/helpers/error.ts +41 -0
  69. package/src/helpers/index.ts +9 -0
  70. package/src/helpers/mask.ts +105 -0
  71. package/src/helpers/number.ts +3 -0
  72. package/src/helpers/object.ts +19 -0
  73. package/src/helpers/openapi.ts +82 -0
  74. package/src/helpers/string.ts +27 -0
  75. package/src/hooks/index.ts +2 -0
  76. package/src/hooks/infinite-scroll.ts +107 -0
  77. package/src/hooks/resize-watcher.ts +8 -0
  78. package/src/index.ts +14 -0
  79. package/src/types.ts +14 -0
  80. package/src/vite-plugins/index.ts +2 -0
  81. package/src/vite-plugins/vite-openapi-plugin.ts +71 -0
  82. package/tsconfig.app.json +22 -0
  83. package/tsconfig.json +14 -0
  84. package/tsconfig.node.json +9 -0
  85. package/tsconfig.vite-plugins.json +10 -0
  86. package/tsconfig.vitest.json +9 -0
  87. package/vite.config.js +37 -35
  88. package/vitest.config.js +17 -0
  89. package/.eslintrc.js +0 -16
  90. package/CHANGES.md +0 -13
  91. package/postcss.config.cjs +0 -5
  92. package/src/app.js +0 -25
  93. package/src/components/alert.vue +0 -130
  94. package/src/components/index.js +0 -12
  95. package/src/config.js +0 -11
  96. package/src/directives/autofocus.js +0 -17
  97. package/src/directives/confirm-button.js +0 -40
  98. package/src/directives/date-input.js +0 -18
  99. package/src/directives/datetime.js +0 -46
  100. package/src/directives/disabled.js +0 -28
  101. package/src/directives/duration.js +0 -72
  102. package/src/directives/index.js +0 -10
  103. package/src/directives/infinite-scroll.js +0 -17
  104. package/src/directives/readonly.js +0 -17
  105. package/src/directives/tooltip.js +0 -178
  106. package/src/directives/user-text.js +0 -11
  107. package/src/filters/index.js +0 -82
  108. package/src/helpers/array.js +0 -99
  109. package/src/helpers/context-menu.js +0 -66
  110. package/src/helpers/delay.js +0 -3
  111. package/src/helpers/error.js +0 -36
  112. package/src/helpers/http.js +0 -44
  113. package/src/helpers/index.js +0 -9
  114. package/src/helpers/mask.js +0 -90
  115. package/src/helpers/number.js +0 -6
  116. package/src/helpers/string.js +0 -36
  117. package/src/helpers/vue.js +0 -5
  118. package/src/index.js +0 -33
  119. package/src/plugins/index.js +0 -10
  120. package/src/plugins/infinite-scroll/hook.js +0 -30
  121. package/src/plugins/infinite-scroll.js +0 -100
  122. 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.titleHtml" />
28
- <div v-if="option.subtitleHtml" class="subtitle" v-html="option.subtitleHtml" />
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/debounce';
40
- import isEqual from 'lodash/isEqual';
38
+ <script lang="ts">
39
+ import { debounce, isEqual } from 'lodash';
40
+ import type { PropType } from 'vue';
41
41
 
42
- const nullSymbol = Symbol(null);
43
- const createSymbol = Symbol('create');
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
- emits: ['optionsLoaded', 'createItem', 'update:modelValue'],
49
-
50
- props: [
51
- 'modelValue',
52
- 'options',
53
- 'prependOptions',
54
- 'appendOptions',
55
- 'preload',
56
- 'url',
57
- 'urlParams',
58
- 'remoteSearch',
59
- 'searchFields',
60
- 'placeholder',
61
- 'valueKey',
62
- 'idKey',
63
- 'titleKey',
64
- 'titleFormatter',
65
- 'subtitleKey',
66
- 'subtitleFormatter',
67
- 'nullTitle',
68
- 'noResultsText',
69
- 'disabled',
70
- 'optionsListId',
71
- 'debug'
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
- effectiveDisabled() {
93
- return this.disabled || !this.loadedOptions;
125
+ effectivePrependOptions() {
126
+ return this.prependOptions ?? [];
94
127
  },
95
128
 
96
- effectivePlaceholder() {
97
- if (!this.isLoaded && this.$isPropTruthy(this.preload)) return 'Loading...';
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
- effectiveTitleKey() {
107
- return this.titleKey || 'name';
133
+ effectiveDisabled() {
134
+ return !!this.disabled; // there was another condition here but it didn't make sense
108
135
  },
109
136
 
110
- effectiveValueKey() {
111
- if (this.valueKey) return this.valueKey;
112
- if (this.options && !Array.isArray(this.options)) return this.effectiveIdKey;
113
- return undefined;
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
- effectiveRemoteSearch() {
121
- return this.$isPropTruthy(this.remoteSearch);
147
+ effectiveKeyExtractor() {
148
+ return this.keyExtractor ?? this.valueExtractor;
122
149
  },
123
150
 
124
151
  /**
125
152
  * OPTIONS GENERATION
126
153
  */
127
154
 
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];
155
+ allOptions() {
156
+ return [...this.effectivePrependOptions, ...this.loadedOptions, ...this.effectiveAppendOptions];
142
157
  },
143
158
 
144
159
  optionsDescriptors() {
145
- return this.fullOptionsArray.map((option, index) => {
146
- const title = this.getOptionTitle(option);
147
- const subtitle = this.getOptionSubtitle(option);
148
- const strippedTitle = title ? title.text.trim().toLowerCase() : '';
149
- const strippedSubtitle = subtitle ? subtitle.text.trim().toLowerCase() : '';
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
- let searchContent = [];
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
- key: typeof option == 'object' ? option[this.effectiveIdKey] || index : option,
163
- titleHtml: title.html,
164
- subtitleHtml: subtitle?.html,
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.includes(strippedSearchText));
194
+ options = options.filter(option => option.searchContent!.includes(strippedSearchText));
179
195
 
180
- const escapedSearchText = this.searchText.escapeHtml().replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
196
+ const escapedSearchText = escapeHtml(this.searchText).replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
181
197
  const searchRe = new RegExp(`(${escapedSearchText})`, 'ig');
182
198
 
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
- });
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: createSymbol,
197
- titleHtml: 'Create <strong>' + this.searchText.trim() + '</strong>...'
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: nullSymbol,
205
- titleHtml: this.nullTitle
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.effectiveRemoteSearch && !this.searchText.trim().length) {
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
- !this.highlightedOptionKey ||
266
- !this.effectiveOptions.find(option => option.key == this.highlightedOptionKey)
267
- ) {
268
- this.highlightedOptionKey = this.effectiveOptions.length ? this.effectiveOptions[0].key : null;
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.$isPropTruthy(this.preload)) {
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
- const 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);
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.effectiveRemoteSearch) {
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
- let params = {};
316
- this.urlParams && Object.assign(params, this.urlParams);
317
-
318
- if (this.effectiveRemoteSearch && this.isSearching && this.searchText) {
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
- if (this.selectedOption)
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.$isPropTruthy(this.debug)) return;
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
- option => option.key == this.highlightedOptionKey
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 == nullSymbol) {
503
+ if (option.key == NullSymbol) {
486
504
  this.searchText = '';
487
505
  this.selectedOption = null;
488
506
  this.selectedOptionTitle = null;
489
- } else if (option.key === createSymbol) {
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
- decoratedOption => decoratedOption.key == option.key
498
- );
499
- const realOption = selectedDecoratedOption.ref;
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
- if (this.effectiveValueKey) {
511
- this.selectedOption = this.fullOptionsArray.find(
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
- getOptionTitle(option) {
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;