@signal24/vue-foundation 4.24.1 → 4.25.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@signal24/vue-foundation",
3
3
  "type": "module",
4
- "version": "4.24.1",
4
+ "version": "4.25.0",
5
5
  "description": "Common components, directives, and helpers for Vue 3 apps",
6
6
  "module": "./dist/vue-foundation.es.js",
7
7
  "exports": {
@@ -1,5 +1,12 @@
1
1
  <template>
2
- <VfSmartSelect v-model="selectedItem" :options="computedOpts" :formatter="ezFormatter" :null-title="nullTitle" :placeholder="placeholder" />
2
+ <VfSmartSelect
3
+ v-model="selectedItem"
4
+ :options="computedOpts"
5
+ :formatter="ezFormatter"
6
+ :null-title="nullTitle"
7
+ :placeholder="placeholder"
8
+ :name="name"
9
+ />
3
10
  </template>
4
11
 
5
12
  <script lang="ts" setup generic="T extends string">
@@ -19,6 +26,7 @@ const props = defineProps<{
19
26
  placeholder?: string;
20
27
  options: { [key in T]: string } | T[];
21
28
  formatter?: (label: string, key: T) => string;
29
+ name?: string;
22
30
  }>();
23
31
 
24
32
  const computedOpts = computed(() => {
@@ -1,5 +1,6 @@
1
1
  export interface VfSmartSelectOptionDescriptor<T> {
2
2
  key: string | symbol;
3
+ group?: string;
3
4
  title: string;
4
5
  subtitle?: string | null;
5
6
  searchContent?: string;
@@ -9,31 +9,40 @@
9
9
  :class="{ nullable: !!nullTitle }"
10
10
  :placeholder="effectivePlaceholder"
11
11
  :required="required"
12
+ :name="name"
12
13
  data-1p-ignore
13
14
  @keydown="handleKeyDown"
14
15
  @focus="handleInputFocused"
15
16
  @blur="handleInputBlurred"
16
17
  />
17
- <div v-if="shouldDisplayOptions" ref="optionsContainer" class="vf-smart-select-options">
18
+ <div v-if="shouldDisplayOptions" ref="optionsContainer" class="vf-smart-select-options" :class="{ grouped: isGrouped }">
18
19
  <div v-if="!isLoaded" class="no-results">Loading...</div>
19
20
  <template v-else>
20
- <div
21
- v-for="option in effectiveOptions"
22
- :key="String(option.key)"
23
- class="option"
24
- :class="[highlightedOptionKey === option.key && 'highlighted', option.ref && classForOption?.(option.ref)]"
25
- @mousemove="handleOptionHover(option)"
26
- @mousedown="selectOption(option)"
27
- >
28
- <slot name="option" :option="option">
29
- <div class="title" v-html="option.title" />
30
- <div v-if="option.subtitle" class="subtitle" v-html="option.subtitle" />
31
- </slot>
32
- </div>
33
- <div v-if="!effectiveOptions.length && searchText" class="no-results">
34
- <slot name="no-results">
35
- {{ effectiveNoResultsText }}
36
- </slot>
21
+ <div v-for="group in groupedOptions" :key="group.groupTitle" class="group">
22
+ <div v-if="group.groupTitle" class="group-title">
23
+ <slot name="group" :group="group.groupTitle">
24
+ {{ group.groupTitle }}
25
+ </slot>
26
+ </div>
27
+
28
+ <div
29
+ v-for="option in group.options"
30
+ :key="option.key"
31
+ class="option"
32
+ :class="[highlightedOptionKey === option.key && 'highlighted', option.ref && classForOption?.(option.ref)]"
33
+ @mousemove="handleOptionHover(option)"
34
+ @mousedown="selectOption(option)"
35
+ >
36
+ <slot name="option" :option="option">
37
+ <div class="title" v-html="option.title" />
38
+ <div v-if="option.subtitle" class="subtitle" v-html="option.subtitle" />
39
+ </slot>
40
+ </div>
41
+ <div v-if="!effectiveOptions.length && searchText" class="no-results">
42
+ <slot name="no-results">
43
+ {{ effectiveNoResultsText }}
44
+ </slot>
45
+ </div>
37
46
  </div>
38
47
  </template>
39
48
  </div>
@@ -41,7 +50,7 @@
41
50
  </template>
42
51
 
43
52
  <script lang="ts" setup generic="T, V = T">
44
- import { debounce, isEqual } from 'lodash';
53
+ import { debounce, groupBy, isEqual, uniq } from 'lodash';
45
54
  import { computed, onMounted, type Ref, ref, watch } from 'vue';
46
55
 
47
56
  import { escapeHtml } from '../helpers/string';
@@ -68,6 +77,8 @@ const props = defineProps<{
68
77
  valueField?: keyof T;
69
78
  valueExtractor?: (option: T) => V;
70
79
  labelField?: keyof T;
80
+ groupField?: keyof T;
81
+ groupFormatter?: (option: T) => string;
71
82
  formatter?: (option: T) => string;
72
83
  subtitleFormatter?: (option: T) => string;
73
84
  classForOption?: (option: T) => string;
@@ -79,6 +90,7 @@ const props = defineProps<{
79
90
  required?: boolean;
80
91
  showCreateTextOnNewItem?: boolean;
81
92
  autoNext?: boolean;
93
+ name?: string;
82
94
  }>();
83
95
 
84
96
  const emit = defineEmits<{
@@ -127,6 +139,11 @@ const effectiveKeyExtractor = computed(() => {
127
139
  if (effectiveValueExtractor.value) return (option: T) => String(effectiveValueExtractor.value!(option));
128
140
  return null;
129
141
  });
142
+ const effectiveGroupFormatter = computed(() => {
143
+ if (props.groupFormatter) return props.groupFormatter;
144
+ if (props.groupField) return (option: T) => String(option[props.groupField!]);
145
+ return null;
146
+ });
130
147
  const effectiveFormatter = computed(() => {
131
148
  if (props.formatter) return props.formatter;
132
149
  if (props.labelField) return (option: T) => String(option[props.labelField!]);
@@ -134,9 +151,11 @@ const effectiveFormatter = computed(() => {
134
151
  });
135
152
 
136
153
  const allOptions = computed(() => [...effectivePrependOptions.value, ...loadedOptions.value, ...effectiveAppendOptions.value]);
154
+ const isGrouped = computed(() => !!(props.groupField || props.groupFormatter));
137
155
 
138
156
  const optionsDescriptors = computed(() => {
139
157
  return allOptions.value.map((option, index) => {
158
+ const group = effectiveGroupFormatter.value?.(option);
140
159
  const title = effectiveFormatter.value(option);
141
160
  const subtitle = props.subtitleFormatter?.(option);
142
161
  const strippedTitle = title ? title.trim().toLowerCase() : '';
@@ -158,6 +177,7 @@ const optionsDescriptors = computed(() => {
158
177
 
159
178
  return {
160
179
  key: effectiveKeyExtractor.value?.(option) ?? String(index),
180
+ group,
161
181
  title,
162
182
  subtitle,
163
183
  searchContent: searchContent.join(''),
@@ -206,6 +226,24 @@ const effectiveOptions = computed(() => {
206
226
  return options;
207
227
  });
208
228
 
229
+ const groupedOptions = computed(() => {
230
+ if (!effectiveOptions.value[0]?.group) {
231
+ return [
232
+ {
233
+ groupTitle: '',
234
+ options: effectiveOptions.value
235
+ }
236
+ ];
237
+ }
238
+
239
+ const groupTitles = uniq(effectiveOptions.value.map(option => option.group ?? ''));
240
+ const groupedOptions = groupBy(effectiveOptions.value, option => option.group);
241
+ return groupTitles.map(groupTitle => ({
242
+ groupTitle,
243
+ options: groupedOptions[groupTitle!]
244
+ }));
245
+ });
246
+
209
247
  // watch props
210
248
  watch(() => props.modelValue, handleValueChanged);
211
249
  watch(
@@ -591,6 +629,11 @@ function focusNextInput() {
591
629
  overflow: auto;
592
630
  z-index: 101;
593
631
 
632
+ .group-title {
633
+ padding: 5px 8px;
634
+ color: #999;
635
+ }
636
+
594
637
  .option,
595
638
  .no-results {
596
639
  padding: 5px 8px;
package/src/config.ts CHANGED
@@ -3,13 +3,15 @@ interface IOptions {
3
3
  errorHandler: (err: Error) => void;
4
4
  defaultDateFormat: string;
5
5
  defaultTimeFormat: string;
6
+ defaultCurrencyDivisor: number;
6
7
  }
7
8
 
8
9
  export const VfOptions: IOptions = {
9
10
  unhandledErrorSupportText: 'please contact support',
10
11
  errorHandler: err => console.error('Unhandled error:', err),
11
12
  defaultDateFormat: 'M/d/yy',
12
- defaultTimeFormat: 'H:mm'
13
+ defaultTimeFormat: 'H:mm',
14
+ defaultCurrencyDivisor: 1
13
15
  };
14
16
 
15
17
  export function configureVf(options: Partial<IOptions>) {
@@ -64,7 +64,7 @@ function desnake(value: string | null) {
64
64
  return value ? desnakeCase(value) : null;
65
65
  }
66
66
 
67
- function usCurrency(value: string | number, divisor = 1) {
67
+ function usCurrency(value: string | number, divisor?: number) {
68
68
  return formatUSCurrency(value, divisor);
69
69
  }
70
70
 
@@ -1,6 +1,8 @@
1
1
  import currency from 'currency.js';
2
2
  import { v4 as uuidv4 } from 'uuid';
3
3
 
4
+ import { VfOptions } from '@/config';
5
+
4
6
  // placing this here so we don't have to use the ESLint rule everywhere
5
7
  // eslint-disable-next-line vue/prefer-import-from-vue
6
8
  export { escapeHtml } from '@vue/shared';
@@ -19,8 +21,10 @@ export function formatPhone(value: string) {
19
21
  return '(' + cleanValue.substring(0, 3) + ') ' + cleanValue.substring(3, 6) + '-' + cleanValue.substring(6);
20
22
  }
21
23
 
22
- export function formatUSCurrency(value: string | number, divisor = 1) {
23
- return currency(value).divide(divisor).format();
24
+ export function formatUSCurrency(value: string | number, divisor?: number) {
25
+ return currency(value)
26
+ .divide(divisor ?? VfOptions.defaultCurrencyDivisor)
27
+ .format();
24
28
  }
25
29
 
26
30
  export function uuid() {