sprintify-ui 0.0.31 → 0.0.33

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.
@@ -78,10 +78,7 @@
78
78
  <slot
79
79
  name="option"
80
80
  :option="option.option"
81
- :selected="
82
- normalizedModelValue &&
83
- normalizedModelValue.value == option.value
84
- "
81
+ :selected="isSelected(option)"
85
82
  :active="optionActive && optionActive.value == option.value"
86
83
  >
87
84
  <div
@@ -125,20 +122,16 @@
125
122
 
126
123
  <script lang="ts" setup>
127
124
  import { get } from 'lodash';
128
- import { PropType, Ref } from 'vue';
129
- import {
130
- NormalizedOption,
131
- Option,
132
- Selection,
133
- NormalizedSelection,
134
- } from '@/types/types';
125
+ import { PropType, Ref, ComputedRef } from 'vue';
126
+ import { NormalizedOption, Option } from '@/types/types';
135
127
  import { useInfiniteScroll, useMutationObserver } from '@vueuse/core';
136
128
  import BaseSkeleton from './BaseSkeleton.vue';
129
+ import { useHasOptions } from '@/composables/hasOptions';
137
130
 
138
131
  const props = defineProps({
139
132
  modelValue: {
140
133
  default: undefined,
141
- type: [Object, null] as PropType<Selection | undefined>,
134
+ type: [Object, null] as PropType<Option | null | undefined>,
142
135
  },
143
136
  options: {
144
137
  required: true,
@@ -193,6 +186,19 @@ const selectionIndex = ref(0);
193
186
  const inputElement = ref(null) as Ref<HTMLInputElement | null>;
194
187
  const dropdown = ref(null) as Ref<HTMLElement | null>;
195
188
 
189
+ const hasOptions = useHasOptions(
190
+ computed(() => props.modelValue),
191
+ computed(() => props.options),
192
+ computed(() => props.labelKey),
193
+ computed(() => props.valueKey),
194
+ computed(() => false)
195
+ );
196
+
197
+ const isSelected = hasOptions.isSelected;
198
+ const normalizedOptions = hasOptions.normalizedOptions;
199
+ const normalizedModelValue =
200
+ hasOptions.normalizedModelValue as ComputedRef<NormalizedOption | null>;
201
+
196
202
  onMounted(() => {
197
203
  useInfiniteScroll(
198
204
  dropdown.value,
@@ -211,17 +217,7 @@ const optionActive = computed(() => {
211
217
  );
212
218
  });
213
219
 
214
- const normalizedModelValue = computed(() => {
215
- if (!props.modelValue) {
216
- return null;
217
- }
218
- return {
219
- label: props.modelValue[props.labelKey] as string,
220
- value: props.modelValue[props.valueKey] as string | number,
221
- option: props.modelValue,
222
- };
223
- });
224
-
220
+ // Update the keywords when the model value changes
225
221
  watch(
226
222
  () => normalizedModelValue.value,
227
223
  () => {
@@ -234,16 +230,6 @@ watch(
234
230
  { immediate: true }
235
231
  );
236
232
 
237
- const normalizedOptions = computed(() => {
238
- return props.options.map((option) => {
239
- return {
240
- label: option[props.labelKey] as string,
241
- value: option[props.valueKey] as string | number,
242
- option: option,
243
- } as NormalizedOption;
244
- });
245
- });
246
-
247
233
  const filteredNormalizedOptions = computed((): NormalizedOption[] => {
248
234
  return normalizedOptions.value.filter((option) => {
249
235
  if (props.filter !== undefined) {
@@ -276,6 +262,8 @@ const onTextFocus = () => {
276
262
  emit('focus');
277
263
  };
278
264
 
265
+ // If keywords is changed but no new selection was made,
266
+ // update keywords to original value
279
267
  const onTextBlur = () => {
280
268
  timerId.value = setTimeout(() => {
281
269
  showDropdown.value = false;
@@ -305,7 +293,7 @@ const onTextKeydown = (event: KeyboardEvent) => {
305
293
  }
306
294
 
307
295
  if (key === 'ArrowDown') {
308
- if (selectionIndex.value < props.options.length - 1) {
296
+ if (selectionIndex.value < filteredNormalizedOptions.value.length - 1) {
309
297
  selectionIndex.value++;
310
298
  } else {
311
299
  selectionIndex.value = 0;
@@ -317,7 +305,10 @@ const onTextKeydown = (event: KeyboardEvent) => {
317
305
  if (selectionIndex.value > 0) {
318
306
  selectionIndex.value--;
319
307
  } else {
320
- selectionIndex.value = Math.max(0, props.options.length - 1);
308
+ selectionIndex.value = Math.max(
309
+ 0,
310
+ filteredNormalizedOptions.value.length - 1
311
+ );
321
312
  }
322
313
  return;
323
314
  }
@@ -333,9 +324,7 @@ const onTextKeydown = (event: KeyboardEvent) => {
333
324
 
334
325
  const optionClass = (option: NormalizedOption) => {
335
326
  const active = optionActive.value && optionActive.value.value == option.value;
336
- const selected =
337
- normalizedModelValue.value &&
338
- normalizedModelValue.value.value == option.value;
327
+ const selected = isSelected(option);
339
328
 
340
329
  if (selected) {
341
330
  if (active) {
@@ -359,12 +348,12 @@ const clear = () => {
359
348
  inputElement.value?.focus();
360
349
  };
361
350
 
362
- const onSelect = (normalizedModelValue: NormalizedSelection) => {
351
+ const onSelect = (normalizedModelValue: Option | null | undefined) => {
363
352
  update(normalizedModelValue);
364
353
  inputElement.value?.blur();
365
354
  };
366
355
 
367
- const update = (normalizedSelection: NormalizedSelection) => {
356
+ const update = (normalizedSelection: Option | null | undefined) => {
368
357
  const selection = normalizedSelection ? normalizedSelection.option : null;
369
358
  if (normalizedSelection) {
370
359
  setKeywordsWithoutEvent(normalizedSelection.label);
@@ -61,14 +61,14 @@
61
61
  import { config } from '../';
62
62
  import { debounce } from 'lodash';
63
63
  import { PropType, Ref } from 'vue';
64
- import { Option, Selection } from '@/types/types';
64
+ import { Option } from '@/types/types';
65
65
  import { RouteLocationRaw } from 'vue-router';
66
66
  import BaseAutocomplete from './BaseAutocomplete.vue';
67
67
 
68
68
  const props = defineProps({
69
69
  modelValue: {
70
70
  default: undefined,
71
- type: [Object, null] as PropType<Selection | undefined>,
71
+ type: [Object, null] as PropType<Option | null | undefined>,
72
72
  },
73
73
  url: {
74
74
  required: true,
@@ -24,12 +24,12 @@
24
24
  </template>
25
25
 
26
26
  <script lang="ts" setup>
27
- import { Selection } from '@/types/types';
28
27
  import { PropType } from 'vue';
29
28
  import { RouteLocationRaw } from 'vue-router';
30
29
  import { AxiosResponse } from 'axios';
31
30
  import { config } from '@/index';
32
31
  import BaseAutocompleteFetch from './BaseAutocompleteFetch.vue';
32
+ import { Option } from '@/types/types';
33
33
 
34
34
  const props = defineProps({
35
35
  modelValue: {
@@ -72,7 +72,7 @@ const props = defineProps({
72
72
  },
73
73
  currentModel: {
74
74
  default: null,
75
- type: [Object, null] as PropType<Selection>,
75
+ type: [Object, null] as PropType<Option | null>,
76
76
  },
77
77
  createNewUrl: {
78
78
  default: '',
@@ -120,7 +120,7 @@ watch(
120
120
  { immediate: true }
121
121
  );
122
122
 
123
- function onUpdate(newModel: Selection) {
123
+ function onUpdate(newModel: Option | null) {
124
124
  if (!newModel) {
125
125
  model.value = null;
126
126
  emit('update:modelValue', null);
@@ -0,0 +1,87 @@
1
+ import BaseButtonGroup from './BaseButtonGroup.vue';
2
+
3
+ export default {
4
+ title: 'Form/BaseButtonGroup',
5
+ component: BaseButtonGroup,
6
+ argTypes: {},
7
+ args: {
8
+ labelKey: 'label',
9
+ valueKey: 'value',
10
+ options: [
11
+ { label: 'Dark Vader', value: 'dark_vader' },
12
+ { label: 'Darth Maul', value: 'darth_maul' },
13
+ { label: 'Dark Sidious', value: 'dark_sidious' },
14
+ { label: 'Obi Wan Kenobi', value: 'obiwan' },
15
+ { label: 'Anakin Skywalker', value: 'anakin' },
16
+ { label: 'Mace Windu', value: 'windu' },
17
+ ],
18
+ },
19
+ decorators: [() => ({ template: '<div class="mb-36"><story/></div>' })],
20
+ };
21
+
22
+ const Template = (args) => ({
23
+ components: { BaseButtonGroup },
24
+ setup() {
25
+ const value = ref(null);
26
+ return { args, value };
27
+ },
28
+ template: `
29
+ <BaseButtonGroup v-model="value" v-bind="args"></BaseButtonGroup>
30
+ <p class="mt-5 text-sm">Value: <span class="bg-slate-200 font-mono px-1 py-px rounded">{{ value ?? 'NULL' }}</span></p>
31
+ `,
32
+ });
33
+
34
+ export const Single = Template.bind({});
35
+ Single.args = {};
36
+
37
+ export const SingleRequired = Template.bind({});
38
+ SingleRequired.args = {
39
+ required: true,
40
+ };
41
+
42
+ export const Multiple = Template.bind({});
43
+ Multiple.args = {
44
+ multiple: true,
45
+ };
46
+
47
+ export const Disabled = Template.bind({});
48
+ Disabled.args = {
49
+ disabled: true,
50
+ modelValue: { label: 'Dark Maul', value: 'darth_maul' },
51
+ };
52
+
53
+ export const SlotOption = (args) => ({
54
+ components: { BaseButtonGroup },
55
+ setup() {
56
+ const value = ref(null);
57
+
58
+ const options = [
59
+ { label: 'Red', value: 'red' },
60
+ { label: 'Blue', value: 'blue' },
61
+ { label: 'Green', value: 'green' },
62
+ { label: 'Black', value: 'black' },
63
+ { label: 'Gray', value: 'gray' },
64
+ ];
65
+
66
+ return { value, options, args };
67
+ },
68
+ template: `
69
+ <BaseButtonGroup
70
+ v-bind="args"
71
+ v-model="value"
72
+ :options="options"
73
+ >
74
+ <template #option="{ option, selected, onSelect }">
75
+ <button
76
+ class="btn btn-xs flex items-center space-x-1 font-semibold"
77
+ :class="[selected ? 'btn-black' : '']"
78
+ type="button"
79
+ @click="onSelect(option)"
80
+ >
81
+ <div class="w-3 h-3 rounded" :style="{ backgroundColor: option.value }"></div>
82
+ <div>{{ option.label }}</div>
83
+ </button>
84
+ </template>
85
+ </BaseButtonGroup>
86
+ `,
87
+ });
@@ -0,0 +1,134 @@
1
+ <template>
2
+ <div class="flex flex-wrap" :style="{ margin: '-' + spacing }">
3
+ <div
4
+ v-for="option in normalizedOptions"
5
+ :key="option.value"
6
+ :style="{ padding: spacing }"
7
+ >
8
+ <slot
9
+ name="option"
10
+ :selected="isSelected(option)"
11
+ :on-select="onSelect"
12
+ :option="option"
13
+ >
14
+ <button
15
+ :type="buttonType"
16
+ :disabled="disabled"
17
+ :class="[
18
+ buttonClass,
19
+ isSelected(option) ? buttonActiveClass : buttonInactiveClass,
20
+ ]"
21
+ @click="onSelect(option)"
22
+ >
23
+ {{ option.label }}
24
+ </button>
25
+ </slot>
26
+ </div>
27
+ </div>
28
+ </template>
29
+
30
+ <script lang="ts" setup>
31
+ import { PropType } from 'vue';
32
+ import { NormalizedOption, Option } from '@/types/types';
33
+ import { cloneDeep, isArray, isObject } from 'lodash';
34
+ import { useHasOptions } from '@/composables/hasOptions';
35
+
36
+ const props = defineProps({
37
+ modelValue: {
38
+ default: undefined,
39
+ type: [Array, String, Number, null] as PropType<
40
+ Option[] | Option | undefined
41
+ >,
42
+ },
43
+ required: {
44
+ default: false,
45
+ type: Boolean,
46
+ },
47
+ disabled: {
48
+ default: false,
49
+ type: Boolean,
50
+ },
51
+ buttonType: {
52
+ default: 'button',
53
+ type: String as PropType<'button' | 'submit'>,
54
+ },
55
+ buttonClass: {
56
+ default: 'btn btn-sm',
57
+ type: String,
58
+ },
59
+ buttonActiveClass: {
60
+ default: 'btn-primary',
61
+ type: String,
62
+ },
63
+ buttonInactiveClass: {
64
+ default: '',
65
+ type: String,
66
+ },
67
+ spacing: {
68
+ default: '0.15rem',
69
+ type: String,
70
+ },
71
+ options: {
72
+ required: true,
73
+ type: Array as PropType<Option[]>,
74
+ },
75
+ labelKey: {
76
+ required: true,
77
+ type: String,
78
+ },
79
+ valueKey: {
80
+ required: true,
81
+ type: String,
82
+ },
83
+ multiple: {
84
+ default: false,
85
+ type: Boolean,
86
+ },
87
+ });
88
+
89
+ const emit = defineEmits(['update:modelValue']);
90
+
91
+ const { normalizedOptions, normalizedModelValue, isSelected } = useHasOptions(
92
+ computed(() => props.modelValue),
93
+ computed(() => props.options),
94
+ computed(() => props.labelKey),
95
+ computed(() => props.valueKey),
96
+ computed(() => props.multiple)
97
+ );
98
+
99
+ function onSelect(option: NormalizedOption) {
100
+ if (props.multiple) {
101
+ let newModalValue = [] as NormalizedOption[];
102
+
103
+ if (isArray(normalizedModelValue.value)) {
104
+ newModalValue = cloneDeep(normalizedModelValue.value);
105
+ }
106
+
107
+ const exists = newModalValue.find((o) => o.value == option.value);
108
+
109
+ if (exists) {
110
+ newModalValue = newModalValue.filter((o) => o.value != option.value);
111
+ } else {
112
+ newModalValue.push(option);
113
+ }
114
+
115
+ emit(
116
+ 'update:modelValue',
117
+ newModalValue.map((o) => o.option)
118
+ );
119
+ } else {
120
+ if (!props.required) {
121
+ if (
122
+ !isArray(normalizedModelValue.value) &&
123
+ option.value == normalizedModelValue.value?.value
124
+ ) {
125
+ emit('update:modelValue', null);
126
+ return;
127
+ }
128
+ }
129
+
130
+ const newOption = option.option;
131
+ emit('update:modelValue', newOption);
132
+ }
133
+ }
134
+ </script>
@@ -64,16 +64,14 @@ export const SlotOption = (args) => ({
64
64
  v-model="value"
65
65
  :options="options"
66
66
  >
67
- <template #option="{ option, active, selected }">
67
+ <template #option="{ option, active }">
68
68
  <div
69
69
  class="rounded px-2 font-semibold py-1 text-sm"
70
70
  :class="{
71
- 'hover:bg-slate-100': !active && !selected,
72
- 'bg-slate-200 hover:bg-slate-300': active && !selected,
73
- 'bg-blue-500 text-white hover:bg-blue-600': !active && selected,
74
- 'bg-blue-600 text-white hover:bg-blue-700': active && selected,
71
+ 'hover:bg-slate-100': !active,
72
+ 'bg-slate-200 hover:bg-slate-300': active,
75
73
  }"
76
- :style="{ color: selected ? '' : option.value }"
74
+ :style="{ color: option.value }"
77
75
  >
78
76
  {{ option.label }}
79
77
  </div>
@@ -82,7 +82,6 @@
82
82
  <slot
83
83
  name="option"
84
84
  :option="option.option"
85
- :selected="optionValues.includes(option.value)"
86
85
  :active="optionActive && optionActive.value == option.value"
87
86
  >
88
87
  <div
@@ -127,10 +126,11 @@
127
126
  <script lang="ts" setup>
128
127
  import { cloneDeep, get } from 'lodash';
129
128
  import { PropType, Ref, ComputedRef } from 'vue';
130
- import { NormalizedOption, Option, OptionValue } from '@/types/types';
129
+ import { NormalizedOption, Option } from '@/types/types';
131
130
  import { useInfiniteScroll, useMutationObserver } from '@vueuse/core';
132
131
  import { useNotificationsStore } from '@/stores/notifications';
133
132
  import BaseSkeleton from './BaseSkeleton.vue';
133
+ import { useHasOptions } from '@/composables/hasOptions';
134
134
 
135
135
  const props = defineProps({
136
136
  modelValue: {
@@ -194,9 +194,23 @@ const keywords = ref('');
194
194
  const showDropdown = ref(false);
195
195
  const inputElement = ref(null) as Ref<HTMLInputElement | null>;
196
196
  const dropdown = ref(null) as Ref<HTMLElement | null>;
197
- const selectionIndex = ref(0);
197
+ const activeIndex = ref(0);
198
198
  const selectionToDelete = ref(null) as Ref<NormalizedOption | null>;
199
199
 
200
+ const hasOptions = useHasOptions(
201
+ computed(() => props.modelValue),
202
+ computed(() => props.options),
203
+ computed(() => props.labelKey),
204
+ computed(() => props.valueKey),
205
+ computed(() => true)
206
+ );
207
+
208
+ const isSelected = hasOptions.isSelected;
209
+ const normalizedOptions = hasOptions.normalizedOptions;
210
+ const normalizedModelValue = hasOptions.normalizedModelValue as ComputedRef<
211
+ NormalizedOption[]
212
+ >;
213
+
200
214
  onMounted(() => {
201
215
  useInfiniteScroll(
202
216
  dropdown.value,
@@ -210,34 +224,11 @@ onMounted(() => {
210
224
  const optionActive = computed(() => {
211
225
  return (
212
226
  filteredNormalizedOptions.value[
213
- Math.min(selectionIndex.value, filteredNormalizedOptions.value.length - 1)
227
+ Math.min(activeIndex.value, filteredNormalizedOptions.value.length - 1)
214
228
  ] ?? null
215
229
  );
216
230
  });
217
231
 
218
- const normalizedModelValue = computed(() => {
219
- if (!props.modelValue) {
220
- return [];
221
- }
222
- return props.modelValue.map((o) => {
223
- return {
224
- label: o[props.labelKey] as string,
225
- value: o[props.valueKey] as string | number,
226
- option: o,
227
- };
228
- });
229
- }) as ComputedRef<NormalizedOption[]>;
230
-
231
- const normalizedOptions = computed(() => {
232
- return props.options.map((option) => {
233
- return {
234
- label: option[props.labelKey] as string,
235
- value: option[props.valueKey] as string | number,
236
- option: option,
237
- } as NormalizedOption;
238
- });
239
- });
240
-
241
232
  const filteredNormalizedOptions = computed((): NormalizedOption[] => {
242
233
  return normalizedOptions.value
243
234
  .filter((option) => {
@@ -250,20 +241,10 @@ const filteredNormalizedOptions = computed((): NormalizedOption[] => {
250
241
  return option.label.toLowerCase().includes(keywords.value.toLowerCase());
251
242
  })
252
243
  .filter((option) => {
253
- return !hasSelectedOption(option.value);
244
+ return !isSelected(option);
254
245
  });
255
246
  });
256
247
 
257
- const optionValues = computed(() => {
258
- return normalizedModelValue.value.map((o) => {
259
- return o.value;
260
- });
261
- });
262
-
263
- const hasSelectedOption = (value: OptionValue): boolean => {
264
- return optionValues.value.includes(value);
265
- };
266
-
267
248
  function preventUnfocus(elements: HTMLElement[]) {
268
249
  elements.forEach((e) => {
269
250
  e.removeEventListener('mousedown', dontLooseFocus);
@@ -294,7 +275,7 @@ const onTextBlur = () => {
294
275
  };
295
276
 
296
277
  const onTextInput = (event: Event) => {
297
- selectionIndex.value = 0;
278
+ activeIndex.value = 0;
298
279
  selectionToDelete.value = null;
299
280
  setKeywords(get(event, 'target.value', '') + '');
300
281
  dropdown.value?.scrollTo({
@@ -315,19 +296,22 @@ const onTextKeydown = (event: KeyboardEvent) => {
315
296
  }
316
297
 
317
298
  if (key === 'ArrowDown') {
318
- if (selectionIndex.value < props.options.length - 1) {
319
- selectionIndex.value++;
299
+ if (activeIndex.value < filteredNormalizedOptions.value.length - 1) {
300
+ activeIndex.value++;
320
301
  } else {
321
- selectionIndex.value = 0;
302
+ activeIndex.value = 0;
322
303
  }
323
304
  return;
324
305
  }
325
306
 
326
307
  if (key === 'ArrowUp') {
327
- if (selectionIndex.value > 0) {
328
- selectionIndex.value--;
308
+ if (activeIndex.value > 0) {
309
+ activeIndex.value--;
329
310
  } else {
330
- selectionIndex.value = Math.max(0, props.options.length - 1);
311
+ activeIndex.value = Math.max(
312
+ 0,
313
+ filteredNormalizedOptions.value.length - 1
314
+ );
331
315
  }
332
316
  return;
333
317
  }
@@ -343,9 +327,7 @@ const onTextKeydown = (event: KeyboardEvent) => {
343
327
 
344
328
  const optionClass = (option: NormalizedOption) => {
345
329
  const active = optionActive.value && optionActive.value.value == option.value;
346
- const selected =
347
- normalizedModelValue.value &&
348
- normalizedModelValue.value.map((o) => o.value).includes(option.value);
330
+ const selected = isSelected(option);
349
331
 
350
332
  if (selected) {
351
333
  if (active) {
@@ -389,7 +371,7 @@ const addOption = (option: NormalizedOption) => {
389
371
  return;
390
372
  }
391
373
 
392
- if (hasSelectedOption(option.value)) {
374
+ if (isSelected(option)) {
393
375
  return;
394
376
  }
395
377
 
@@ -440,19 +422,19 @@ const beforeAddOption = () => {
440
422
  };
441
423
 
442
424
  const afterUpdate = () => {
443
- validateSelectionIndex();
425
+ validateActiveIndex();
444
426
  };
445
427
 
446
428
  const clearInput = () => {
447
429
  setKeywords('');
448
430
  };
449
431
 
450
- const validateSelectionIndex = () => {
432
+ const validateActiveIndex = () => {
451
433
  // Wait for filteredOptions to update
452
434
  nextTick(() => {
453
- selectionIndex.value = Math.max(
435
+ activeIndex.value = Math.max(
454
436
  0,
455
- Math.min(selectionIndex.value, normalizedOptions.value.length - 1)
437
+ Math.min(activeIndex.value, filteredNormalizedOptions.value.length - 1)
456
438
  );
457
439
  });
458
440
  };
@@ -11,6 +11,7 @@ import BaseBelongsTo from './BaseBelongsTo.vue';
11
11
  import BaseBoolean from './BaseBoolean.vue';
12
12
  import BaseBreadcrumbs from './BaseBreadcrumbs.vue';
13
13
  import BaseButton from './BaseButton.vue';
14
+ import BaseButtonGroup from './BaseButtonGroup.vue';
14
15
  import BaseCard from './BaseCard.vue';
15
16
  import BaseCardRow from './BaseCardRow.vue';
16
17
  import BaseCharacterCounter from './BaseCharacterCounter.vue';
@@ -80,6 +81,7 @@ export {
80
81
  BaseBoolean,
81
82
  BaseBreadcrumbs,
82
83
  BaseButton,
84
+ BaseButtonGroup,
83
85
  BaseCard,
84
86
  BaseCardRow,
85
87
  BaseCharacterCounter,