sprintify-ui 0.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 (176) hide show
  1. package/README.md +188 -0
  2. package/dist/types/src/components/BaseAlert.vue.d.ts +51 -0
  3. package/dist/types/src/components/BaseAutocomplete.vue.d.ts +268 -0
  4. package/dist/types/src/components/BaseAutocompleteFetch.vue.d.ts +273 -0
  5. package/dist/types/src/components/BaseAvatar.vue.d.ts +126 -0
  6. package/dist/types/src/components/BaseBadge.vue.d.ts +94 -0
  7. package/dist/types/src/components/BaseBelongsTo.vue.d.ts +268 -0
  8. package/dist/types/src/components/BaseBoolean.vue.d.ts +64 -0
  9. package/dist/types/src/components/BaseBreadcrumbs.vue.d.ts +66 -0
  10. package/dist/types/src/components/BaseButton.vue.d.ts +23 -0
  11. package/dist/types/src/components/BaseCard.vue.d.ts +74 -0
  12. package/dist/types/src/components/BaseCardRow.vue.d.ts +16 -0
  13. package/dist/types/src/components/BaseClipboard.vue.d.ts +74 -0
  14. package/dist/types/src/components/BaseContainer.vue.d.ts +34 -0
  15. package/dist/types/src/components/BaseCounter.vue.d.ts +125 -0
  16. package/dist/types/src/components/BaseDataIterator.vue.d.ts +345 -0
  17. package/dist/types/src/components/BaseDataTable.vue.d.ts +657 -0
  18. package/dist/types/src/components/BaseDataTableToggleColumns.vue.d.ts +1281 -0
  19. package/dist/types/src/components/BaseDatePicker.vue.d.ts +190 -0
  20. package/dist/types/src/components/BaseDateSelect.vue.d.ts +171 -0
  21. package/dist/types/src/components/BaseDescriptionList.vue.d.ts +48 -0
  22. package/dist/types/src/components/BaseDescriptionListItem.vue.d.ts +49 -0
  23. package/dist/types/src/components/BaseDialog.vue.d.ts +160 -0
  24. package/dist/types/src/components/BaseFilePicker.vue.d.ts +44 -0
  25. package/dist/types/src/components/BaseFileUploader.vue.d.ts +220 -0
  26. package/dist/types/src/components/BaseInput.vue.d.ts +209 -0
  27. package/dist/types/src/components/BaseInputLabel.vue.d.ts +31 -0
  28. package/dist/types/src/components/BaseLoadingCover.vue.d.ts +166 -0
  29. package/dist/types/src/components/BaseLoadingPage.vue.d.ts +2 -0
  30. package/dist/types/src/components/BaseMediaLibrary.vue.d.ts +269 -0
  31. package/dist/types/src/components/BaseMediaLibraryItem.vue.d.ts +75 -0
  32. package/dist/types/src/components/BaseMenu.vue.d.ts +117 -0
  33. package/dist/types/src/components/BaseMenuItem.vue.d.ts +147 -0
  34. package/dist/types/src/components/BaseModalCenter.vue.d.ts +141 -0
  35. package/dist/types/src/components/BaseModalSide.vue.d.ts +141 -0
  36. package/dist/types/src/components/BaseNavbar.vue.d.ts +79 -0
  37. package/dist/types/src/components/BaseNavbarItem.vue.d.ts +80 -0
  38. package/dist/types/src/components/BaseNavbarItemContent.vue.d.ts +127 -0
  39. package/dist/types/src/components/BasePagination.vue.d.ts +25 -0
  40. package/dist/types/src/components/BasePaginationSimple.vue.d.ts +25 -0
  41. package/dist/types/src/components/BasePanel.vue.d.ts +31 -0
  42. package/dist/types/src/components/BasePassword.vue.d.ts +66 -0
  43. package/dist/types/src/components/BaseProcessRing.vue.d.ts +36 -0
  44. package/dist/types/src/components/BaseReadMore.vue.d.ts +74 -0
  45. package/dist/types/src/components/BaseSelect.vue.d.ts +55 -0
  46. package/dist/types/src/components/BaseSideNavigation.vue.d.ts +48 -0
  47. package/dist/types/src/components/BaseSideNavigationItem.vue.d.ts +92 -0
  48. package/dist/types/src/components/BaseSkeleton.vue.d.ts +93 -0
  49. package/dist/types/src/components/BaseSpinner.vue.d.ts +2 -0
  50. package/dist/types/src/components/BaseSwitch.vue.d.ts +39 -0
  51. package/dist/types/src/components/BaseSystemAlert.vue.d.ts +141 -0
  52. package/dist/types/src/components/BaseTabItem.vue.d.ts +70 -0
  53. package/dist/types/src/components/BaseTable.vue.d.ts +467 -0
  54. package/dist/types/src/components/BaseTableColumn.vue.d.ts +164 -0
  55. package/dist/types/src/components/BaseTabs.vue.d.ts +48 -0
  56. package/dist/types/src/components/BaseTagAutocomplete.vue.d.ts +274 -0
  57. package/dist/types/src/components/BaseTagAutocompleteFetch.vue.d.ts +251 -0
  58. package/dist/types/src/components/BaseTextarea.vue.d.ts +228 -0
  59. package/dist/types/src/components/BaseTextareaAutoresize.vue.d.ts +44 -0
  60. package/dist/types/src/components/BaseTitle.vue.d.ts +45 -0
  61. package/dist/types/src/components/BaseWordCount.vue.d.ts +31 -0
  62. package/dist/types/src/components/SlotComponent.d.ts +43 -0
  63. package/dist/types/src/components/index.d.ts +2 -0
  64. package/dist/types/src/composables/breakpoints.d.ts +12 -0
  65. package/dist/types/src/composables/modal.d.ts +6 -0
  66. package/dist/types/src/constants/MyConstants.d.ts +1 -0
  67. package/dist/types/src/constants/index.d.ts +2 -0
  68. package/dist/types/src/index.d.ts +253 -0
  69. package/dist/types/src/types/Media.d.ts +8 -0
  70. package/dist/types/src/types/UploadedFile.d.ts +9 -0
  71. package/dist/types/src/types/User.d.ts +6 -0
  72. package/dist/types/src/types/types.d.ts +88 -0
  73. package/dist/types/src/utils/fileSizeFormat.d.ts +1 -0
  74. package/dist/types/src/utils/index.d.ts +4 -0
  75. package/dist/types/src/utils/scrollPreventer.d.ts +4 -0
  76. package/dist/types/src/utils/toHumanList.d.ts +1 -0
  77. package/package.json +99 -0
  78. package/src/assets/button.css +80 -0
  79. package/src/assets/form.css +15 -0
  80. package/src/assets/main.css +3 -0
  81. package/src/assets/pikaday.css +134 -0
  82. package/src/assets/tailwind.css +5 -0
  83. package/src/components/BaseAlert.stories.js +52 -0
  84. package/src/components/BaseAlert.vue +152 -0
  85. package/src/components/BaseAutocomplete.stories.js +127 -0
  86. package/src/components/BaseAutocomplete.vue +376 -0
  87. package/src/components/BaseAutocompleteFetch.stories.js +121 -0
  88. package/src/components/BaseAutocompleteFetch.vue +185 -0
  89. package/src/components/BaseAvatar.stories.js +39 -0
  90. package/src/components/BaseAvatar.vue +92 -0
  91. package/src/components/BaseBadge.stories.js +61 -0
  92. package/src/components/BaseBadge.vue +70 -0
  93. package/src/components/BaseBelongsTo.stories.js +130 -0
  94. package/src/components/BaseBelongsTo.vue +122 -0
  95. package/src/components/BaseBoolean.stories.js +35 -0
  96. package/src/components/BaseBoolean.vue +29 -0
  97. package/src/components/BaseBreadcrumbs.stories.js +45 -0
  98. package/src/components/BaseBreadcrumbs.vue +78 -0
  99. package/src/components/BaseButton.stories.js +80 -0
  100. package/src/components/BaseButton.vue +39 -0
  101. package/src/components/BaseCard.stories.js +61 -0
  102. package/src/components/BaseCard.vue +49 -0
  103. package/src/components/BaseCardRow.vue +34 -0
  104. package/src/components/BaseClipboard.stories.js +31 -0
  105. package/src/components/BaseClipboard.vue +96 -0
  106. package/src/components/BaseContainer.stories.js +34 -0
  107. package/src/components/BaseContainer.vue +50 -0
  108. package/src/components/BaseCounter.stories.js +32 -0
  109. package/src/components/BaseCounter.vue +72 -0
  110. package/src/components/BaseDataIterator.stories.js +90 -0
  111. package/src/components/BaseDataIterator.vue +658 -0
  112. package/src/components/BaseDataTable.stories.js +95 -0
  113. package/src/components/BaseDataTable.vue +489 -0
  114. package/src/components/BaseDataTableToggleColumns.vue +69 -0
  115. package/src/components/BaseDatePicker.stories.js +53 -0
  116. package/src/components/BaseDatePicker.vue +166 -0
  117. package/src/components/BaseDateSelect.vue +192 -0
  118. package/src/components/BaseDescriptionList.vue +11 -0
  119. package/src/components/BaseDescriptionListItem.vue +12 -0
  120. package/src/components/BaseDialog.vue +104 -0
  121. package/src/components/BaseFilePicker.vue +101 -0
  122. package/src/components/BaseFileUploader.vue +166 -0
  123. package/src/components/BaseInput.vue +82 -0
  124. package/src/components/BaseInputLabel.vue +26 -0
  125. package/src/components/BaseLoadingCover.vue +84 -0
  126. package/src/components/BaseLoadingPage.vue +19 -0
  127. package/src/components/BaseMediaLibrary.vue +281 -0
  128. package/src/components/BaseMediaLibraryItem.vue +92 -0
  129. package/src/components/BaseMenu.vue +114 -0
  130. package/src/components/BaseMenuItem.vue +93 -0
  131. package/src/components/BaseModalCenter.vue +107 -0
  132. package/src/components/BaseModalSide.vue +112 -0
  133. package/src/components/BaseNavbar.vue +72 -0
  134. package/src/components/BaseNavbarItem.vue +72 -0
  135. package/src/components/BaseNavbarItemContent.vue +57 -0
  136. package/src/components/BasePagination.vue +82 -0
  137. package/src/components/BasePaginationSimple.vue +60 -0
  138. package/src/components/BasePanel.vue +39 -0
  139. package/src/components/BasePassword.vue +73 -0
  140. package/src/components/BaseProcessRing.vue +56 -0
  141. package/src/components/BaseReadMore.vue +72 -0
  142. package/src/components/BaseSelect.vue +59 -0
  143. package/src/components/BaseSideNavigation.vue +7 -0
  144. package/src/components/BaseSideNavigationItem.vue +42 -0
  145. package/src/components/BaseSkeleton.vue +24 -0
  146. package/src/components/BaseSpinner.vue +47 -0
  147. package/src/components/BaseSwitch.vue +87 -0
  148. package/src/components/BaseSystemAlert.vue +86 -0
  149. package/src/components/BaseTabItem.vue +30 -0
  150. package/src/components/BaseTable.vue +781 -0
  151. package/src/components/BaseTableColumn.vue +109 -0
  152. package/src/components/BaseTabs.vue +12 -0
  153. package/src/components/BaseTagAutocomplete.vue +385 -0
  154. package/src/components/BaseTagAutocompleteFetch.vue +154 -0
  155. package/src/components/BaseTextarea.vue +73 -0
  156. package/src/components/BaseTextareaAutoresize.vue +117 -0
  157. package/src/components/BaseTitle.vue +80 -0
  158. package/src/components/BaseWordCount.vue +36 -0
  159. package/src/components/SlotComponent.ts +37 -0
  160. package/src/components/index.ts +5 -0
  161. package/src/composables/breakpoints.ts +6 -0
  162. package/src/composables/modal.ts +77 -0
  163. package/src/constants/MyConstants.ts +1 -0
  164. package/src/constants/index.ts +5 -0
  165. package/src/env.d.ts +15 -0
  166. package/src/index.ts +70 -0
  167. package/src/lang/en.json +56 -0
  168. package/src/lang/fr.json +56 -0
  169. package/src/types/Media.ts +9 -0
  170. package/src/types/UploadedFile.ts +10 -0
  171. package/src/types/User.ts +7 -0
  172. package/src/types/types.ts +112 -0
  173. package/src/utils/fileSizeFormat.ts +15 -0
  174. package/src/utils/index.ts +5 -0
  175. package/src/utils/scrollPreventer.ts +21 -0
  176. package/src/utils/toHumanList.ts +20 -0
@@ -0,0 +1,109 @@
1
+ <script lang="ts">
2
+ import { defineComponent, h, PropType } from 'vue';
3
+
4
+ export default defineComponent({
5
+ name: 'BaseTableColumn',
6
+ props: {
7
+ label: {
8
+ default: '',
9
+ type: String,
10
+ },
11
+ customKey: {
12
+ default: '',
13
+ type: [String, Number],
14
+ },
15
+ field: {
16
+ default: '',
17
+ type: String,
18
+ },
19
+ meta: {
20
+ default: undefined,
21
+ type: [Object, Array],
22
+ },
23
+ width: {
24
+ default: undefined,
25
+ type: Number,
26
+ },
27
+ numeric: {
28
+ default: false,
29
+ type: Boolean,
30
+ },
31
+ position: {
32
+ default: 'left',
33
+ type: String as PropType<'left' | 'center' | 'right'>,
34
+ },
35
+ searchable: {
36
+ default: false,
37
+ type: Boolean,
38
+ },
39
+ sortable: {
40
+ default: false,
41
+ type: Boolean,
42
+ },
43
+ visible: {
44
+ type: Boolean,
45
+ default: true,
46
+ },
47
+ alwaysVisible: {
48
+ type: Boolean,
49
+ default: false,
50
+ },
51
+ optional: {
52
+ type: Boolean,
53
+ default: false,
54
+ },
55
+ customSort: {
56
+ default: undefined,
57
+ type: Function,
58
+ },
59
+ customSearch: {
60
+ default: undefined,
61
+ type: Function,
62
+ },
63
+ /** Adds native attributes to th :th-attrs="(column)" => ({})" */
64
+ thAttrs: {
65
+ type: Function,
66
+ default: () => ({}),
67
+ },
68
+ /** Adds native attributes to td :td-attrs="(row, column)" => ({})" */
69
+ tdAttrs: {
70
+ type: Function,
71
+ default: () => ({}),
72
+ },
73
+ },
74
+ setup() {
75
+ const nextSequence = inject('nextSequence') as any;
76
+ const addColumn = inject('addColumn') as any;
77
+ const removeColumn = inject('removeColumn') as any;
78
+
79
+ return {
80
+ nextSequence,
81
+ addColumn,
82
+ removeColumn,
83
+ };
84
+ },
85
+ data() {
86
+ return {
87
+ newKey: undefined,
88
+ };
89
+ },
90
+ computed: {
91
+ style() {
92
+ return {
93
+ width: this.width ? this.width + 'px' : undefined,
94
+ };
95
+ },
96
+ },
97
+ created() {
98
+ this.newKey = this.nextSequence();
99
+ this.addColumn(this);
100
+ },
101
+ beforeUnmount() {
102
+ this.removeColumn(this);
103
+ },
104
+ render() {
105
+ // render-less
106
+ return h('span', { 'data-id': this.newKey }, this.label);
107
+ },
108
+ });
109
+ </script>
@@ -0,0 +1,12 @@
1
+ <template>
2
+ <div class="relative">
3
+ <div class="absolute bottom-0 left-0 h-[2px] w-full bg-slate-300" />
4
+ <div class="overflow-x-auto overflow-y-hidden">
5
+ <ul class="flex space-x-2 text-center text-sm font-medium">
6
+ <slot />
7
+ </ul>
8
+ </div>
9
+ </div>
10
+ </template>
11
+
12
+ <script lang="ts" setup></script>
@@ -0,0 +1,385 @@
1
+ <template>
2
+ <div>
3
+ <div class="min-h-[42px] rounded border border-slate-300 p-1">
4
+ <div class="-m-0.5 flex flex-wrap">
5
+ <div
6
+ v-for="selection in normalizedModelValue"
7
+ :key="selection.value"
8
+ class="p-0.5"
9
+ >
10
+ <div
11
+ class="flex items-stretch rounded border"
12
+ :class="selectionClass(selection)"
13
+ >
14
+ <div class="py-[5px] pl-3 pr-1 text-sm">
15
+ {{ selection.label }}
16
+ </div>
17
+ <button
18
+ type="button"
19
+ class="flex shrink-0 appearance-none items-center justify-center border-0 bg-transparent pl-1 pr-3 text-xs outline-none"
20
+ @click="dontLooseFocus($event, () => removeOption(selection))"
21
+ @mousedown="dontLooseFocus"
22
+ >
23
+
24
+ </button>
25
+ </div>
26
+ </div>
27
+ <div class="grow p-0.5">
28
+ <input
29
+ ref="input"
30
+ :value="keywords"
31
+ type="text"
32
+ :placeholder="$t('sui.select_an_item')"
33
+ class="h-[32px] w-full min-w-[50px] border-none p-0 pl-1 shadow-none outline-none focus:border-none focus:shadow-none focus:outline-none focus:ring-0"
34
+ :class="inputClass"
35
+ @focus="onTextFocus"
36
+ @blur="onTextBlur"
37
+ @input="onTextInput"
38
+ @keydown="onTextKeydown"
39
+ />
40
+ </div>
41
+ </div>
42
+ </div>
43
+
44
+ <div class="relative">
45
+ <div
46
+ v-show="showDropdown"
47
+ class="absolute top-1 z-[1] min-h-[100px] w-full overflow-hidden rounded border border-slate-300 bg-white shadow-md"
48
+ >
49
+ <ul ref="dropdown" class="max-h-[214px] w-full overflow-y-auto p-1">
50
+ <li
51
+ v-for="option in normalizedOptions"
52
+ :key="option.value"
53
+ class="block"
54
+ >
55
+ <button
56
+ class="block w-full cursor-pointer appearance-none rounded-sm border-none px-2 py-1 text-left text-sm focus:outline-none"
57
+ :class="optionClass(option)"
58
+ type="button"
59
+ tabindex="-1"
60
+ @click="dontLooseFocus($event, () => addOption(option))"
61
+ @mousedown="dontLooseFocus"
62
+ >
63
+ {{ option.label }}
64
+ </button>
65
+ </li>
66
+ </ul>
67
+
68
+ <slot v-if="normalizedOptions.length == 0" name="empty">
69
+ <div class="p-5 text-center text-slate-600">
70
+ {{ $t('sui.nothing_found') }}
71
+ </div>
72
+ </slot>
73
+
74
+ <BaseLoadingCover :model-value="loading" duration="duration-50" />
75
+ </div>
76
+ </div>
77
+ </div>
78
+ </template>
79
+
80
+ <script lang="ts" setup>
81
+ import { cloneDeep, get } from 'lodash';
82
+ import { PropType, Ref, ComputedRef } from 'vue';
83
+ import { NormalizedOption, Option, OptionValue } from '@/types/types';
84
+ import { useInfiniteScroll } from '@vueuse/core';
85
+ import { useNotificationsStore } from '../../stores/notifications';
86
+
87
+ const props = defineProps({
88
+ modelValue: {
89
+ required: true,
90
+ type: Array as PropType<Option[]>,
91
+ },
92
+ options: {
93
+ required: true,
94
+ type: Array as PropType<Option[]>,
95
+ },
96
+ labelKey: {
97
+ required: true,
98
+ type: String,
99
+ },
100
+ valueKey: {
101
+ required: true,
102
+ type: String,
103
+ },
104
+ inputClass: {
105
+ default: undefined,
106
+ type: String,
107
+ },
108
+ placeholder: {
109
+ default: undefined,
110
+ type: String,
111
+ },
112
+ loading: {
113
+ default: false,
114
+ type: Boolean,
115
+ },
116
+ required: {
117
+ default: false,
118
+ type: Boolean,
119
+ },
120
+ disabled: {
121
+ default: false,
122
+ type: Boolean,
123
+ },
124
+ min: {
125
+ default: undefined,
126
+ type: Number,
127
+ },
128
+ max: {
129
+ default: undefined,
130
+ type: Number,
131
+ },
132
+ });
133
+
134
+ const emit = defineEmits([
135
+ 'update:modelValue',
136
+ 'typing',
137
+ 'focus',
138
+ 'scrollBottom',
139
+ ]);
140
+
141
+ const i18n = useI18n();
142
+ const notifications = useNotificationsStore();
143
+
144
+ const timerId = ref(0);
145
+ const keywords = ref('');
146
+ const showDropdown = ref(false);
147
+ const inputElement = ref(null) as Ref<HTMLInputElement | null>;
148
+ const dropdown = ref(null) as Ref<HTMLElement | null>;
149
+ const selectionIndex = ref(0);
150
+ const selectionToDelete = ref(null) as Ref<NormalizedOption | null>;
151
+
152
+ onMounted(() => {
153
+ useInfiniteScroll(
154
+ dropdown.value,
155
+ () => {
156
+ emit('scrollBottom');
157
+ },
158
+ { distance: 10 }
159
+ );
160
+ });
161
+
162
+ const optionActive = computed(() => {
163
+ return (
164
+ normalizedOptions.value[
165
+ Math.min(selectionIndex.value, normalizedOptions.value.length - 1)
166
+ ] ?? null
167
+ );
168
+ });
169
+
170
+ const normalizedModelValue = computed(() => {
171
+ if (!props.modelValue) {
172
+ return [];
173
+ }
174
+ return props.modelValue.map((o) => {
175
+ return {
176
+ label: o[props.labelKey] as string,
177
+ value: o[props.valueKey] as string | number,
178
+ option: o,
179
+ };
180
+ });
181
+ }) as ComputedRef<NormalizedOption[]>;
182
+
183
+ const normalizedOptions = computed(() => {
184
+ return props.options
185
+ .map((option) => {
186
+ return {
187
+ label: option[props.labelKey] as string,
188
+ value: option[props.valueKey] as string | number,
189
+ option: option,
190
+ } as NormalizedOption;
191
+ })
192
+ .filter((option) => {
193
+ return !hasSelectedOption(option.value);
194
+ });
195
+ });
196
+
197
+ const optionValues = computed(() => {
198
+ return normalizedModelValue.value.map((o) => {
199
+ return o.value;
200
+ });
201
+ });
202
+
203
+ const hasSelectedOption = (value: OptionValue): boolean => {
204
+ return optionValues.value.includes(value);
205
+ };
206
+
207
+ const dontLooseFocus = (event: Event, next: null | (() => void) = null) => {
208
+ event.preventDefault();
209
+ inputElement.value?.focus();
210
+ if (next) {
211
+ next();
212
+ }
213
+ };
214
+
215
+ const onTextFocus = () => {
216
+ clearTimeout(timerId.value);
217
+ showDropdown.value = true;
218
+ emit('focus');
219
+ };
220
+
221
+ const onTextBlur = () => {
222
+ timerId.value = setTimeout(() => {
223
+ showDropdown.value = false;
224
+ }, 10);
225
+ };
226
+
227
+ const onTextInput = (event: Event) => {
228
+ selectionIndex.value = 0;
229
+ selectionToDelete.value = null;
230
+ setKeywords(get(event, 'target.value', '') + '');
231
+ dropdown.value?.scrollTo({
232
+ top: 0,
233
+ });
234
+ };
235
+
236
+ const onTextKeydown = (event: KeyboardEvent) => {
237
+ const key = event.key;
238
+
239
+ if (props.loading) {
240
+ return;
241
+ }
242
+
243
+ if (key === 'Backspace' && keywords.value == '') {
244
+ attemptRemoveLastSelection();
245
+ return;
246
+ }
247
+
248
+ if (key === 'ArrowDown') {
249
+ if (selectionIndex.value < props.options.length - 1) {
250
+ selectionIndex.value++;
251
+ } else {
252
+ selectionIndex.value = 0;
253
+ }
254
+ return;
255
+ }
256
+
257
+ if (key === 'ArrowUp') {
258
+ if (selectionIndex.value > 0) {
259
+ selectionIndex.value--;
260
+ } else {
261
+ selectionIndex.value = Math.max(0, props.options.length - 1);
262
+ }
263
+ return;
264
+ }
265
+
266
+ if (key === 'Enter') {
267
+ event.preventDefault();
268
+ if (optionActive.value) {
269
+ addOption(optionActive.value);
270
+ }
271
+ return;
272
+ }
273
+ };
274
+
275
+ const optionClass = (option: NormalizedOption) => {
276
+ if (normalizedModelValue.value.map((o) => o.value).includes(option.value)) {
277
+ return 'bg-blue-600 text-white';
278
+ }
279
+ if (optionActive.value && optionActive.value.value == option.value) {
280
+ return 'bg-slate-200';
281
+ }
282
+ return 'bg-white hover:bg-slate-200';
283
+ };
284
+
285
+ const selectionClass = (selection: NormalizedOption): string => {
286
+ if (
287
+ selectionToDelete.value &&
288
+ selectionToDelete.value.value == selection.value
289
+ ) {
290
+ return 'bg-red-200 border-red-300 text-red-800';
291
+ }
292
+ return 'bg-slate-200 border-slate-300';
293
+ };
294
+
295
+ const setKeywords = (input: string) => {
296
+ keywords.value = input;
297
+ emit('typing', input);
298
+ };
299
+
300
+ const toggleOption = (option: NormalizedOption) => {
301
+ if (hasSelectedOption(option.value)) {
302
+ removeOption(option);
303
+ } else {
304
+ addOption(option);
305
+ }
306
+ };
307
+
308
+ const addOption = (option: NormalizedOption) => {
309
+ if (props.max && normalizedModelValue.value.length >= props.max) {
310
+ notifications.push({
311
+ title: i18n.t('sui.whoops'),
312
+ text: i18n.t('sui.you_cannot_select_more_than_x_items', { x: props.max }),
313
+ color: 'warning',
314
+ });
315
+ return;
316
+ }
317
+
318
+ if (hasSelectedOption(option.value)) {
319
+ return;
320
+ }
321
+
322
+ beforeAddOption();
323
+
324
+ emitUpdate([...normalizedModelValue.value, option]);
325
+
326
+ setKeywords('');
327
+ };
328
+
329
+ const attemptRemoveLastSelection = () => {
330
+ const optionValueToDelete =
331
+ normalizedModelValue.value[normalizedModelValue.value.length - 1] ?? null;
332
+
333
+ if (
334
+ selectionToDelete.value &&
335
+ optionValueToDelete &&
336
+ optionValueToDelete.value == selectionToDelete.value.value
337
+ ) {
338
+ removeOption(selectionToDelete.value);
339
+ selectionToDelete.value = null;
340
+ return;
341
+ }
342
+
343
+ selectionToDelete.value =
344
+ normalizedModelValue.value.find((v) => {
345
+ return v.value === optionValueToDelete.value;
346
+ }) ?? null;
347
+ };
348
+
349
+ const removeOption = (option: NormalizedOption) => {
350
+ let newModelValue = cloneDeep(normalizedModelValue.value);
351
+ newModelValue = newModelValue.filter((v) => v.value != option.value);
352
+ emitUpdate(newModelValue);
353
+ };
354
+
355
+ const emitUpdate = (value: NormalizedOption[]) => {
356
+ emit(
357
+ 'update:modelValue',
358
+ value.map((v) => v.option)
359
+ );
360
+ afterUpdate();
361
+ };
362
+
363
+ const beforeAddOption = () => {
364
+ selectionToDelete.value = null;
365
+ clearInput();
366
+ };
367
+
368
+ const afterUpdate = () => {
369
+ validateSelectionIndex();
370
+ };
371
+
372
+ const clearInput = () => {
373
+ setKeywords('');
374
+ };
375
+
376
+ const validateSelectionIndex = () => {
377
+ // Wait for filteredOptions to update
378
+ nextTick(() => {
379
+ selectionIndex.value = Math.max(
380
+ 0,
381
+ Math.min(selectionIndex.value, normalizedOptions.value.length - 1)
382
+ );
383
+ });
384
+ };
385
+ </script>
@@ -0,0 +1,154 @@
1
+ <template>
2
+ <BaseTagAutocomplete
3
+ :loading="loading && page == 1"
4
+ :model-value="modelValue"
5
+ :disabled="disabled"
6
+ :placeholder="placeholder"
7
+ :options="options"
8
+ :value-key="valueKey"
9
+ :label-key="labelKey"
10
+ :input-class="inputClass"
11
+ :min="min"
12
+ :max="max"
13
+ @focus="onFocus"
14
+ @typing="onTyping"
15
+ @scroll-bottom="scrollBottom"
16
+ @update:model-value="$emit('update:modelValue', $event)"
17
+ >
18
+ <template #empty>
19
+ <div class="p-5 text-center text-slate-600">
20
+ <span v-if="firstSearch">
21
+ {{ $t('sui.nothing_found') }}
22
+ </span>
23
+ <span v-else>
24
+ {{ $t('sui.autocomplete_placeholder') }}
25
+ </span>
26
+ </div>
27
+ </template>
28
+ </BaseTagAutocomplete>
29
+ </template>
30
+
31
+ <script lang="ts" setup>
32
+ import { debounce } from 'lodash';
33
+ import { config } from 'src';
34
+ import { PropType, Ref } from 'vue';
35
+ import { Option } from '@/types/types';
36
+
37
+ const props = defineProps({
38
+ modelValue: {
39
+ required: true,
40
+ type: Array as PropType<Option[]>,
41
+ },
42
+ url: {
43
+ required: true,
44
+ type: String,
45
+ },
46
+ labelKey: {
47
+ required: true,
48
+ type: String,
49
+ },
50
+ valueKey: {
51
+ required: true,
52
+ type: String,
53
+ },
54
+ inputClass: {
55
+ default: undefined,
56
+ type: String,
57
+ },
58
+ placeholder: {
59
+ default: undefined,
60
+ type: String,
61
+ },
62
+ required: {
63
+ default: false,
64
+ type: Boolean,
65
+ },
66
+ disabled: {
67
+ default: false,
68
+ type: Boolean,
69
+ },
70
+ min: {
71
+ default: undefined,
72
+ type: Number,
73
+ },
74
+ max: {
75
+ default: undefined,
76
+ type: Number,
77
+ },
78
+ });
79
+
80
+ defineEmits(['update:modelValue', 'typing', 'focus', 'scrollBottom']);
81
+
82
+ const http = config.http;
83
+
84
+ const loading = ref(false);
85
+ const firstSearch = ref(false);
86
+ const reachedEnd = ref(false);
87
+ const keywords = ref('');
88
+ const page = ref(1);
89
+ const options = ref([]) as Ref<Option[]>;
90
+
91
+ const onTyping = (query: string) => {
92
+ page.value = 1;
93
+ reachedEnd.value = false;
94
+
95
+ if (keywords.value != query) {
96
+ keywords.value = query;
97
+ debouncedSearch();
98
+ }
99
+ };
100
+
101
+ const onFocus = () => {
102
+ if (firstSearch.value) {
103
+ return;
104
+ }
105
+
106
+ search();
107
+
108
+ firstSearch.value = true;
109
+ };
110
+
111
+ const scrollBottom = () => {
112
+ if (!reachedEnd.value) {
113
+ page.value++;
114
+ search();
115
+ }
116
+ };
117
+
118
+ const search = () => {
119
+ if (loading.value) {
120
+ return;
121
+ }
122
+
123
+ loading.value = true;
124
+ firstSearch.value = true;
125
+
126
+ http
127
+ .get(props.url, {
128
+ params: {
129
+ search: keywords.value,
130
+ page: page.value,
131
+ },
132
+ })
133
+ .then((response: any) => {
134
+ const data = response.data.data as Option[];
135
+
136
+ if (data.length == 0) {
137
+ reachedEnd.value = true;
138
+ }
139
+
140
+ if (page.value == 1) {
141
+ options.value = data;
142
+ } else {
143
+ options.value.push(...data);
144
+ }
145
+ })
146
+ .finally(() => {
147
+ loading.value = false;
148
+ });
149
+ };
150
+
151
+ const debouncedSearch = debounce(() => {
152
+ search();
153
+ }, 300);
154
+ </script>