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