@signal24/vue-foundation 4.24.2 → 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.2",
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,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;
@@ -15,26 +15,34 @@
15
15
  @focus="handleInputFocused"
16
16
  @blur="handleInputBlurred"
17
17
  />
18
- <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 }">
19
19
  <div v-if="!isLoaded" class="no-results">Loading...</div>
20
20
  <template v-else>
21
- <div
22
- v-for="option in effectiveOptions"
23
- :key="String(option.key)"
24
- class="option"
25
- :class="[highlightedOptionKey === option.key && 'highlighted', option.ref && classForOption?.(option.ref)]"
26
- @mousemove="handleOptionHover(option)"
27
- @mousedown="selectOption(option)"
28
- >
29
- <slot name="option" :option="option">
30
- <div class="title" v-html="option.title" />
31
- <div v-if="option.subtitle" class="subtitle" v-html="option.subtitle" />
32
- </slot>
33
- </div>
34
- <div v-if="!effectiveOptions.length && searchText" class="no-results">
35
- <slot name="no-results">
36
- {{ effectiveNoResultsText }}
37
- </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>
38
46
  </div>
39
47
  </template>
40
48
  </div>
@@ -42,7 +50,7 @@
42
50
  </template>
43
51
 
44
52
  <script lang="ts" setup generic="T, V = T">
45
- import { debounce, isEqual } from 'lodash';
53
+ import { debounce, groupBy, isEqual, uniq } from 'lodash';
46
54
  import { computed, onMounted, type Ref, ref, watch } from 'vue';
47
55
 
48
56
  import { escapeHtml } from '../helpers/string';
@@ -69,6 +77,8 @@ const props = defineProps<{
69
77
  valueField?: keyof T;
70
78
  valueExtractor?: (option: T) => V;
71
79
  labelField?: keyof T;
80
+ groupField?: keyof T;
81
+ groupFormatter?: (option: T) => string;
72
82
  formatter?: (option: T) => string;
73
83
  subtitleFormatter?: (option: T) => string;
74
84
  classForOption?: (option: T) => string;
@@ -129,6 +139,11 @@ const effectiveKeyExtractor = computed(() => {
129
139
  if (effectiveValueExtractor.value) return (option: T) => String(effectiveValueExtractor.value!(option));
130
140
  return null;
131
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
+ });
132
147
  const effectiveFormatter = computed(() => {
133
148
  if (props.formatter) return props.formatter;
134
149
  if (props.labelField) return (option: T) => String(option[props.labelField!]);
@@ -136,9 +151,11 @@ const effectiveFormatter = computed(() => {
136
151
  });
137
152
 
138
153
  const allOptions = computed(() => [...effectivePrependOptions.value, ...loadedOptions.value, ...effectiveAppendOptions.value]);
154
+ const isGrouped = computed(() => !!(props.groupField || props.groupFormatter));
139
155
 
140
156
  const optionsDescriptors = computed(() => {
141
157
  return allOptions.value.map((option, index) => {
158
+ const group = effectiveGroupFormatter.value?.(option);
142
159
  const title = effectiveFormatter.value(option);
143
160
  const subtitle = props.subtitleFormatter?.(option);
144
161
  const strippedTitle = title ? title.trim().toLowerCase() : '';
@@ -160,6 +177,7 @@ const optionsDescriptors = computed(() => {
160
177
 
161
178
  return {
162
179
  key: effectiveKeyExtractor.value?.(option) ?? String(index),
180
+ group,
163
181
  title,
164
182
  subtitle,
165
183
  searchContent: searchContent.join(''),
@@ -208,6 +226,24 @@ const effectiveOptions = computed(() => {
208
226
  return options;
209
227
  });
210
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
+
211
247
  // watch props
212
248
  watch(() => props.modelValue, handleValueChanged);
213
249
  watch(
@@ -593,6 +629,11 @@ function focusNextInput() {
593
629
  overflow: auto;
594
630
  z-index: 101;
595
631
 
632
+ .group-title {
633
+ padding: 5px 8px;
634
+ color: #999;
635
+ }
636
+
596
637
  .option,
597
638
  .no-results {
598
639
  padding: 5px 8px;