@vc-shell/framework 1.0.38 → 1.0.39

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 (142) hide show
  1. package/components/atoms/vc-badge/vc-badge.stories.ts +27 -0
  2. package/components/atoms/vc-badge/vc-badge.vue +63 -0
  3. package/components/atoms/vc-button/vc-button.stories.ts +34 -0
  4. package/components/atoms/vc-button/vc-button.vue +219 -0
  5. package/components/atoms/vc-card/vc-card.vue +137 -0
  6. package/components/atoms/vc-checkbox/vc-checkbox.stories.ts +25 -0
  7. package/components/atoms/vc-checkbox/vc-checkbox.vue +130 -0
  8. package/components/atoms/vc-col/vc-col.vue +22 -0
  9. package/components/atoms/vc-container/vc-container.stories.ts +31 -0
  10. package/components/atoms/vc-container/vc-container.vue +220 -0
  11. package/components/atoms/vc-hint/vc-hint.stories.ts +23 -0
  12. package/components/atoms/vc-hint/vc-hint.vue +11 -0
  13. package/components/atoms/vc-icon/vc-icon.stories.ts +32 -0
  14. package/components/atoms/vc-icon/vc-icon.vue +36 -0
  15. package/components/atoms/vc-image/vc-image.stories.ts +40 -0
  16. package/components/atoms/vc-image/vc-image.vue +122 -0
  17. package/components/atoms/vc-info-row/vc-info-row.vue +42 -0
  18. package/components/atoms/vc-label/vc-label.stories.ts +23 -0
  19. package/components/atoms/vc-label/vc-label.vue +49 -0
  20. package/components/atoms/vc-link/vc-link.stories.ts +30 -0
  21. package/components/atoms/vc-link/vc-link.vue +46 -0
  22. package/components/atoms/vc-loading/vc-loading.vue +30 -0
  23. package/components/atoms/vc-progress/vc-progress.stories.ts +25 -0
  24. package/components/atoms/vc-progress/vc-progress.vue +65 -0
  25. package/components/atoms/vc-row/vc-row.vue +13 -0
  26. package/components/atoms/vc-status/vc-status.stories.ts +26 -0
  27. package/components/atoms/vc-status/vc-status.vue +78 -0
  28. package/components/atoms/vc-status-icon/vc-status-icon.vue +21 -0
  29. package/components/atoms/vc-switch/vc-switch.stories.ts +27 -0
  30. package/components/atoms/vc-switch/vc-switch.vue +100 -0
  31. package/components/atoms/vc-widget/vc-widget.vue +85 -0
  32. package/components/index.ts +43 -0
  33. package/components/molecules/vc-breadcrumbs/_internal/vc-breadcrumbs-item/vc-breadcrumbs-item.vue +103 -0
  34. package/components/molecules/vc-breadcrumbs/vc-breadcrumbs.stories.ts +39 -0
  35. package/components/molecules/vc-breadcrumbs/vc-breadcrumbs.vue +21 -0
  36. package/components/molecules/vc-editor/vc-editor.vue +117 -0
  37. package/components/molecules/vc-file-upload/vc-file-upload.vue +134 -0
  38. package/components/molecules/vc-form/vc-form.stories.ts +23 -0
  39. package/components/molecules/vc-form/vc-form.vue +5 -0
  40. package/components/molecules/vc-input/vc-input.stories.ts +26 -0
  41. package/components/molecules/vc-input/vc-input.vue +443 -0
  42. package/components/molecules/vc-multivalue/vc-multivalue.vue +447 -0
  43. package/components/molecules/vc-notification/vc-notification.vue +101 -0
  44. package/components/molecules/vc-pagination/vc-pagination.stories.ts +23 -0
  45. package/components/molecules/vc-pagination/vc-pagination.vue +169 -0
  46. package/components/molecules/vc-rating/vc-rating.stories.ts +23 -0
  47. package/components/molecules/vc-rating/vc-rating.vue +77 -0
  48. package/components/molecules/vc-select/vc-select.stories.ts +25 -0
  49. package/components/molecules/vc-select/vc-select.vue +402 -0
  50. package/components/molecules/vc-slider/vc-slider.vue +106 -0
  51. package/components/molecules/vc-textarea/vc-textarea.stories.ts +23 -0
  52. package/components/molecules/vc-textarea/vc-textarea.vue +155 -0
  53. package/components/organisms/vc-app/_internal/vc-app-bar/vc-app-bar.vue +146 -0
  54. package/components/organisms/vc-app/_internal/vc-app-menu/_internal/vc-app-menu-item/_internal/vc-app-menu-link.vue +148 -0
  55. package/components/organisms/vc-app/_internal/vc-app-menu/_internal/vc-app-menu-item/vc-app-menu-item.vue +157 -0
  56. package/components/organisms/vc-app/_internal/vc-app-menu/vc-app-menu.vue +110 -0
  57. package/components/organisms/vc-app/vc-app.stories.ts +75 -0
  58. package/components/organisms/vc-app/vc-app.vue +171 -0
  59. package/components/organisms/vc-blade/_internal/vc-blade-header/vc-blade-header.vue +126 -0
  60. package/components/organisms/vc-blade/_internal/vc-blade-toolbar/_internal/vc-blade-toolbar-button/vc-blade-toolbar-button.vue +223 -0
  61. package/components/organisms/vc-blade/_internal/vc-blade-toolbar/vc-blade-toolbar.vue +67 -0
  62. package/components/organisms/vc-blade/vc-blade.stories.ts +46 -0
  63. package/components/organisms/vc-blade/vc-blade.vue +87 -0
  64. package/components/organisms/vc-dynamic-property/vc-dynamic-property.vue +292 -0
  65. package/components/organisms/vc-gallery/_internal/vc-gallery-item/vc-gallery-item.vue +123 -0
  66. package/components/organisms/vc-gallery/_internal/vc-gallery-preview/vc-gallery-preview.vue +93 -0
  67. package/components/organisms/vc-gallery/vc-gallery.vue +186 -0
  68. package/components/organisms/vc-login-form/vc-login-form.stories.ts +55 -0
  69. package/components/organisms/vc-login-form/vc-login-form.vue +48 -0
  70. package/components/organisms/vc-popup/vc-popup.stories.ts +23 -0
  71. package/components/organisms/vc-popup/vc-popup.vue +97 -0
  72. package/components/organisms/vc-table/_internal/vc-table-cell/vc-table-cell.vue +113 -0
  73. package/components/organisms/vc-table/_internal/vc-table-counter/vc-table-counter.vue +29 -0
  74. package/components/organisms/vc-table/_internal/vc-table-filter/vc-table-filter.vue +152 -0
  75. package/components/organisms/vc-table/_internal/vc-table-mobile-item/vc-table-mobile-item.vue +272 -0
  76. package/components/organisms/vc-table/vc-table.stories.ts +99 -0
  77. package/components/organisms/vc-table/vc-table.vue +638 -0
  78. package/core/api/index.ts +1 -0
  79. package/core/api/platform.ts +8332 -0
  80. package/core/composables/index.ts +8 -0
  81. package/core/composables/useAutosave/index.ts +57 -0
  82. package/core/composables/useFunctions/debounce.ts +18 -0
  83. package/core/composables/useFunctions/delay.ts +7 -0
  84. package/core/composables/useFunctions/index.ts +21 -0
  85. package/core/composables/useFunctions/once.ts +14 -0
  86. package/core/composables/useFunctions/sleep.ts +4 -0
  87. package/core/composables/useFunctions/throttle.ts +17 -0
  88. package/core/composables/useI18n/index.ts +28 -0
  89. package/core/composables/useLogger/index.ts +24 -0
  90. package/core/composables/useNotifications/index.ts +116 -0
  91. package/core/composables/usePermissions/index.ts +32 -0
  92. package/core/composables/useSettings/index.ts +36 -0
  93. package/core/composables/useUser/index.ts +266 -0
  94. package/core/directives/autofocus/index.ts +9 -0
  95. package/core/directives/click-outside/index.ts +21 -0
  96. package/core/directives/index.ts +4 -0
  97. package/core/directives/loading/index.ts +28 -0
  98. package/core/directives/permissions/index.ts +20 -0
  99. package/core/plugins/index.ts +1 -0
  100. package/core/plugins/validation/index.ts +2 -0
  101. package/core/plugins/validation/rules.ts +196 -0
  102. package/core/types/index.ts +92 -0
  103. package/core/utilities/camelToSnake.ts +7 -0
  104. package/core/utilities/index.ts +1 -0
  105. package/dist/core/composables/useNotifications/index.d.ts +1 -1
  106. package/dist/core/composables/useNotifications/index.d.ts.map +1 -1
  107. package/dist/core/composables/useUser/index.d.ts +2 -2
  108. package/dist/core/composables/useUser/index.d.ts.map +1 -1
  109. package/dist/core/plugins/validation/index.d.ts.map +1 -1
  110. package/dist/core/types/index.d.ts +1 -1
  111. package/dist/core/types/index.d.ts.map +1 -1
  112. package/dist/framework.js +70 -97
  113. package/dist/framework.js.map +1 -1
  114. package/dist/shared/app-switcher/composables/useAppSwitcher/index.d.ts +1 -1
  115. package/dist/shared/app-switcher/composables/useAppSwitcher/index.d.ts.map +1 -1
  116. package/dist/shared/app-switcher/index.d.ts +2 -2
  117. package/dist/shared/app-switcher/index.d.ts.map +1 -1
  118. package/dist/shared/blade-navigation/composables/useBladeNavigation/index.d.ts +1 -1
  119. package/dist/shared/blade-navigation/composables/useBladeNavigation/index.d.ts.map +1 -1
  120. package/dist/shared/blade-navigation/types/index.d.ts +1 -1
  121. package/dist/shared/blade-navigation/types/index.d.ts.map +1 -1
  122. package/dist/style.css +1 -1
  123. package/dist/tsconfig.tsbuildinfo +1 -0
  124. package/dist/vite.config.d.ts.map +1 -1
  125. package/package.json +11 -8
  126. package/shared/app-switcher/components/index.ts +1 -0
  127. package/shared/app-switcher/components/vc-app-switcher/vc-app-switcher.vue +90 -0
  128. package/shared/app-switcher/composables/index.ts +1 -0
  129. package/shared/app-switcher/composables/useAppSwitcher/index.ts +54 -0
  130. package/shared/app-switcher/index.ts +14 -0
  131. package/shared/assets/components/assets-details/assets-details.vue +138 -0
  132. package/shared/assets/components/index.ts +1 -0
  133. package/shared/assets/index.ts +19 -0
  134. package/shared/assets/locales/en.json +29 -0
  135. package/shared/assets/locales/index.ts +2 -0
  136. package/shared/blade-navigation/components/index.ts +1 -0
  137. package/shared/blade-navigation/components/vc-blade-navigation/vc-blade-navigation.vue +84 -0
  138. package/shared/blade-navigation/composables/index.ts +1 -0
  139. package/shared/blade-navigation/composables/useBladeNavigation/index.ts +216 -0
  140. package/shared/blade-navigation/index.ts +15 -0
  141. package/shared/blade-navigation/types/index.ts +52 -0
  142. package/shared/index.ts +16 -0
@@ -0,0 +1,169 @@
1
+ <template>
2
+ <div class="vc-pagination flex">
3
+ <!-- To first page chevron -->
4
+ <div
5
+ class="vc-pagination__item"
6
+ :class="{
7
+ 'vc-pagination__item_disabled': currentPage === 1,
8
+ }"
9
+ @click="currentPage !== 1 && $emit('itemClick', 1)"
10
+ >
11
+ <VcIcon size="xs" icon="fas fa-angle-double-left"></VcIcon>
12
+ </div>
13
+
14
+ <!-- To previous page arrow -->
15
+ <div
16
+ class="vc-pagination__item"
17
+ :class="{
18
+ 'vc-pagination__item_disabled': currentPage === 1,
19
+ }"
20
+ @click="currentPage !== 1 && $emit('itemClick', currentPage - 1)"
21
+ >
22
+ <VcIcon size="xs" icon="fas fa-arrow-left"></VcIcon>
23
+ </div>
24
+
25
+ <template v-if="expanded && $isDesktop.value">
26
+ <!-- To preprevious page with number -->
27
+ <div
28
+ v-if="currentPage > 2"
29
+ class="vc-pagination__item"
30
+ @click="$emit('itemClick', currentPage - 2)"
31
+ >
32
+ {{ currentPage - 2 }}
33
+ </div>
34
+
35
+ <!-- To previous page with number -->
36
+ <div
37
+ v-if="currentPage > 1"
38
+ class="vc-pagination__item"
39
+ @click="$emit('itemClick', currentPage - 1)"
40
+ >
41
+ {{ currentPage - 1 }}
42
+ </div>
43
+ </template>
44
+
45
+ <!-- Current page -->
46
+ <div class="vc-pagination__item vc-pagination__item_current">
47
+ {{ currentPage }}
48
+ </div>
49
+
50
+ <template v-if="expanded && $isDesktop.value">
51
+ <!-- To next page with number -->
52
+ <div
53
+ v-if="currentPage < pages"
54
+ class="vc-pagination__item"
55
+ @click="$emit('itemClick', currentPage + 1)"
56
+ >
57
+ {{ currentPage + 1 }}
58
+ </div>
59
+
60
+ <!-- To postnext page with number -->
61
+ <div
62
+ v-if="currentPage < pages - 1"
63
+ class="vc-pagination__item"
64
+ @click="$emit('itemClick', currentPage + 2)"
65
+ >
66
+ {{ currentPage + 2 }}
67
+ </div>
68
+ </template>
69
+
70
+ <!-- To next page arrow -->
71
+ <div
72
+ class="vc-pagination__item"
73
+ :class="{
74
+ 'vc-pagination__item_disabled': currentPage === pages,
75
+ }"
76
+ @click="currentPage !== pages && $emit('itemClick', currentPage + 1)"
77
+ >
78
+ <VcIcon size="xs" icon="fas fa-arrow-right"></VcIcon>
79
+ </div>
80
+
81
+ <!-- To last page chevron -->
82
+ <div
83
+ class="vc-pagination__item"
84
+ :class="{
85
+ 'vc-pagination__item_disabled': currentPage === pages,
86
+ }"
87
+ @click="currentPage !== pages && $emit('itemClick', pages)"
88
+ >
89
+ <VcIcon size="xs" icon="fas fa-angle-double-right"></VcIcon>
90
+ </div>
91
+ </div>
92
+ </template>
93
+
94
+ <script lang="ts" setup>
95
+ import { VcIcon } from "@/components";
96
+
97
+ defineProps({
98
+ expanded: {
99
+ type: Boolean,
100
+ default: false,
101
+ },
102
+
103
+ pages: {
104
+ type: Number,
105
+ default: 1,
106
+ },
107
+
108
+ currentPage: {
109
+ type: Number,
110
+ default: 1,
111
+ },
112
+ });
113
+
114
+ defineEmits(["itemClick"]);
115
+ </script>
116
+
117
+ <style lang="scss">
118
+ :root {
119
+ --pagination-item-width: 30px;
120
+ --pagination-item-height: 30px;
121
+ --pagination-item-color: #000000;
122
+ --pagination-item-color-hover: #000000;
123
+ --pagination-item-color-current: #ffffff;
124
+ --pagination-item-color-disabled: #9c9c9c;
125
+ --pagination-item-background-color: #ffffff;
126
+ --pagination-item-background-color-hover: #dfeef9;
127
+ --pagination-item-background-color-current: #43b0e6;
128
+ --pagination-item-background-color-disabled: #ffffff;
129
+ --pagination-item-border-radius: 3px;
130
+ --pagination-item-border-color: #eaecf2;
131
+ --pagination-item-border-color-hover: #eaecf2;
132
+ --pagination-item-border-color-current: #eaecf2;
133
+ --pagination-item-border-color-disabled: #eaecf2;
134
+ }
135
+
136
+ .vc-pagination {
137
+ &__item {
138
+ @apply flex items-center justify-center w-[var(--pagination-item-width)]
139
+ h-[var(--pagination-item-height)]
140
+ bg-[color:var(--pagination-item-background-color)]
141
+ border border-solid border-[color:var(--pagination-item-border-color)]
142
+ rounded-[var(--pagination-item-border-radius)]
143
+ text-[color:var(--pagination-item-color)]
144
+ box-border cursor-pointer
145
+ transition duration-200
146
+ mr-3 select-none last:mr-0
147
+ hover:bg-[color:var(--pagination-item-background-color-hover)]
148
+ hover:text-[color:var(--pagination-item-color-hover)]
149
+ hover:border hover:border-solid
150
+ hover:border-[color:var(--pagination-item-border-color-hover)];
151
+
152
+ &_current,
153
+ &_current:hover {
154
+ @apply bg-[color:var(--pagination-item-background-color-current)]
155
+ text-[color:var(--pagination-item-color-current)]
156
+ border border-solid border-[color:var(--pagination-item-border-color-current)]
157
+ cursor-auto;
158
+ }
159
+
160
+ &_disabled,
161
+ &_disabled:hover {
162
+ @apply bg-[color:var(--pagination-item-background-color-disabled)]
163
+ text-[color:var(--pagination-item-color-disabled)]
164
+ border border-solid border-[color:var(--pagination-item-border-color-disabled)]
165
+ cursor-auto;
166
+ }
167
+ }
168
+ }
169
+ </style>
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Rating component.
3
+ * @author Aleksandr Vishnyakov <av@virtoway.com>
4
+ */
5
+ import { Story } from "@storybook/vue3";
6
+ import VcRating from "./vc-rating.vue";
7
+
8
+ export default {
9
+ title: "atoms/vc-rating",
10
+ component: VcRating,
11
+ };
12
+
13
+ const Template: Story = (args) => ({
14
+ components: { VcRating },
15
+ setup() {
16
+ return { args };
17
+ },
18
+ template: '<vc-rating v-bind="args"></vc-rating>',
19
+ });
20
+
21
+ export const Rating = Template.bind({});
22
+ Rating.storyName = "vc-rating";
23
+ Rating.args = {};
@@ -0,0 +1,77 @@
1
+ <template>
2
+ <div class="vc-rating">
3
+ <!-- Rating label -->
4
+ <VcLabel v-if="label" class="mb-2">
5
+ <span>{{ label }}</span>
6
+ <template v-if="tooltip" v-slot:tooltip>
7
+ <span v-html="tooltip"></span>
8
+ </template>
9
+ </VcLabel>
10
+
11
+ <!-- Rating icon -->
12
+ <template v-if="rating">
13
+ <template v-if="variant == 'stars'">
14
+ <VcIcon
15
+ v-for="index in rating"
16
+ :key="index"
17
+ icon="fas fa-star"
18
+ class="vc-rating__icon"
19
+ ></VcIcon>
20
+ <VcIcon
21
+ v-for="index in max - rating"
22
+ :key="index"
23
+ icon="far fa-star"
24
+ class="vc-rating__icon"
25
+ ></VcIcon>
26
+ </template>
27
+ <template v-else>
28
+ <VcIcon
29
+ v-if="variant == 'star-and-text'"
30
+ icon="fas fa-star"
31
+ class="vc-rating__icon"
32
+ ></VcIcon>
33
+ <span class="vc-rating__rating">{{ rating }}/{{ max }}</span>
34
+ <slot name="details"></slot>
35
+ </template>
36
+ </template>
37
+ <template v-else>
38
+ <span class="vc-rating__placeholder"></span>
39
+ </template>
40
+ </div>
41
+ </template>
42
+
43
+ <script lang="ts" setup>
44
+ import { VcIcon, VcLabel } from "@/components";
45
+
46
+ export interface Props {
47
+ label?: string;
48
+ placeholder?: string;
49
+ tooltip?: string;
50
+ rating: number | undefined;
51
+ max?: number;
52
+ variant?: "stars" | "star-and-text" | "text";
53
+ }
54
+
55
+ withDefaults(defineProps<Props>(), { max: 5, variant: "stars" });
56
+ </script>
57
+
58
+ <style lang="scss">
59
+ :root {
60
+ --rating-placeholder-color: #a5a5a5;
61
+ }
62
+
63
+ .vc-rating {
64
+ @apply align-middle;
65
+
66
+ &__placeholder {
67
+ @apply text-[color:var(--rating-placeholder-color)];
68
+ }
69
+ &__icon {
70
+ @apply text-[color:var(--special-color)] mr-1;
71
+ font-size: inherit;
72
+ }
73
+ &__rating {
74
+ @apply mr-1;
75
+ }
76
+ }
77
+ </style>
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Select component.
3
+ * @author Iurii A Taranov <me@flanker72.ru>
4
+ */
5
+ import { Story } from "@storybook/vue3";
6
+ import VcSelect from "./vc-select.vue";
7
+
8
+ export default {
9
+ title: "molecules/vc-select",
10
+ component: VcSelect,
11
+ };
12
+
13
+ const Template: Story = (args) => ({
14
+ components: { VcSelect },
15
+ setup() {
16
+ return { args };
17
+ },
18
+ template: '<vc-select v-bind="args"></vc-select>',
19
+ });
20
+
21
+ export const Select = Template.bind({});
22
+ Select.storyName = "vc-select";
23
+ Select.args = {
24
+ options: [{ id: 1, title: "Option" }],
25
+ };
@@ -0,0 +1,402 @@
1
+ <template>
2
+ <div
3
+ class="vc-select box-border"
4
+ :class="{
5
+ 'vc-select_opened': isOpened,
6
+ 'vc-select_error': errorMessage,
7
+ 'vc-select_disabled': isDisabled,
8
+ }"
9
+ >
10
+ <!-- Select label -->
11
+ <VcLabel v-if="label" class="mb-2" :required="isRequired">
12
+ <span>{{ label }}</span>
13
+ <template v-if="tooltip" v-slot:tooltip>
14
+ <span v-html="tooltip"></span>
15
+ </template>
16
+ </VcLabel>
17
+
18
+ <!-- Select field -->
19
+ <div
20
+ class="vc-select__field-wrapper relative box-border border border-solid border-[color:var(--select-border-color)] rounded-[var(--select-border-radius)] bg-[color:var(--select-background-color)] flex items-stretch"
21
+ ref="inputFieldWrapRef"
22
+ >
23
+ <div
24
+ class="w-full appearance-none border-none outline-none min-h-[var(--select-height)] px-3 flex items-center w-full box-border box-border cursor-pointer invalid:text-[color:var(--select-placeholder-color)] overflow-hidden"
25
+ @click.stop="toggleDropdown"
26
+ ref="dropdownToggleRef"
27
+ >
28
+ <div v-if="!selectedItem" class="text-[#a5a5a5]">
29
+ {{ placeholder }}
30
+ </div>
31
+ <slot
32
+ v-else-if="$slots['selectedItem']"
33
+ name="selectedItem"
34
+ :item="selectedItem"
35
+ ></slot>
36
+ <slot v-else name="item" :item="selectedItem">
37
+ {{ selectedItem[displayProperty] }}
38
+ </slot>
39
+ </div>
40
+
41
+ <!-- Select chevron -->
42
+ <div
43
+ v-if="!isDisabled"
44
+ class="vc-select__chevron absolute right-0 top-0 h-full cursor-pointer px-3 flex items-center text-[color:var(--select-chevron-color)] hover:text-[color:var(--select-chevron-color-hover)]"
45
+ @click.stop="toggleDropdown"
46
+ >
47
+ <VcIcon size="s" icon="fas fa-chevron-down"></VcIcon>
48
+ </div>
49
+ <teleport to="#app">
50
+ <div
51
+ v-if="isOpened"
52
+ class="flex flex-col box-border max-h-[300px] z-10 overflow-hidden absolute bg-[color:var(--select-background-color)] border border-solid border-[color:var(--select-border-color)] border-t-[color:var(--select-background-color)] rounded-b-[var(--select-border-radius)] p-2"
53
+ ref="dropdownRef"
54
+ v-click-outside="closeDropdown"
55
+ >
56
+ <input
57
+ v-if="isSearchable"
58
+ ref="search"
59
+ class="w-full box-border border border-solid border-[#eaecf2] rounded-[4px] h-[32px] leading-[32px] outline-none mb-3 px-2"
60
+ @input="onSearch"
61
+ />
62
+
63
+ <VcContainer :no-padding="true">
64
+ <div
65
+ class="flex items-center min-h-[36px] px-2 rounded-[3px] cursor-pointer hover:bg-[#eff7fc]"
66
+ v-for="(item, i) in options"
67
+ :key="i"
68
+ @click="onItemSelect(item)"
69
+ >
70
+ <slot name="item" :item="item">{{ item[displayProperty] }}</slot>
71
+ </div>
72
+ <div v-show="hasNextPage" ref="load" class="text-center">
73
+ Loading...
74
+ </div>
75
+ </VcContainer>
76
+ </div>
77
+ </teleport>
78
+
79
+ <!-- Input clear button -->
80
+ <div
81
+ v-if="clearable && modelValue && !isDisabled"
82
+ class="vc-select__clear"
83
+ @click="onReset"
84
+ >
85
+ <VcIcon size="s" icon="fas fa-times"></VcIcon>
86
+ </div>
87
+ </div>
88
+
89
+ <slot v-if="errorMessage" name="error">
90
+ <VcHint class="text-[color:var(--select-border-color-error)] mt-1">
91
+ {{ errorMessage }}
92
+ </VcHint>
93
+ </slot>
94
+ </div>
95
+ </template>
96
+
97
+ <script lang="ts" setup>
98
+ import { nextTick, ref, computed, watch, getCurrentInstance } from "vue";
99
+ import { VcIcon, VcLabel, VcContainer } from "@/components";
100
+ import { createPopper, Instance, State } from "@popperjs/core";
101
+ import { clickOutside as vClickOutside } from "@/core/directives";
102
+
103
+ const props = defineProps({
104
+ modelValue: {
105
+ type: [String, Number],
106
+ default: undefined,
107
+ },
108
+
109
+ placeholder: {
110
+ type: String,
111
+ default: "Click to select...",
112
+ },
113
+
114
+ options: {
115
+ type: Array,
116
+ default: () => [],
117
+ },
118
+
119
+ isRequired: {
120
+ type: Boolean,
121
+ default: false,
122
+ },
123
+
124
+ isDisabled: {
125
+ type: Boolean,
126
+ default: false,
127
+ },
128
+
129
+ isSearchable: {
130
+ type: Boolean,
131
+ default: false,
132
+ },
133
+
134
+ label: {
135
+ type: String,
136
+ default: undefined,
137
+ },
138
+
139
+ tooltip: {
140
+ type: String,
141
+ default: undefined,
142
+ },
143
+
144
+ keyProperty: {
145
+ type: String,
146
+ default: "id",
147
+ },
148
+
149
+ displayProperty: {
150
+ type: String,
151
+ default: "title",
152
+ },
153
+
154
+ initialItem: {
155
+ type: Object,
156
+ default: undefined,
157
+ },
158
+
159
+ name: {
160
+ type: String,
161
+ default: "Field",
162
+ },
163
+
164
+ clearable: {
165
+ type: Boolean,
166
+ default: true,
167
+ },
168
+
169
+ optionsTotal: {
170
+ type: Number,
171
+ default: 0,
172
+ },
173
+
174
+ onInfiniteScroll: {
175
+ type: Function,
176
+ default: undefined,
177
+ },
178
+
179
+ errorMessage: {
180
+ type: String,
181
+ default: undefined
182
+ }
183
+ });
184
+
185
+ const emit = defineEmits(["update:modelValue", "change", "close", "search"]);
186
+
187
+ const instance = getCurrentInstance();
188
+ const isOpened = ref(false);
189
+ const search = ref();
190
+ const popper = ref<Instance>();
191
+ const dropdownToggleRef = ref();
192
+ const dropdownRef = ref();
193
+ const inputFieldWrapRef = ref();
194
+ const load = ref();
195
+ const observer = new IntersectionObserver(infiniteScroll);
196
+
197
+ const selectedItem = computed(
198
+ () =>
199
+ (props.options as Record<string, unknown>[])?.find(
200
+ (item) => item[props.keyProperty] === props.modelValue
201
+ ) || props.initialItem
202
+ );
203
+
204
+ const hasNextPage = computed(() => {
205
+ return props.options.length < props.optionsTotal;
206
+ });
207
+
208
+ watch(
209
+ () => props.modelValue,
210
+ (value) => {
211
+ emit("update:modelValue", value);
212
+ }
213
+ );
214
+
215
+ function closeDropdown() {
216
+ observer.disconnect();
217
+ isOpened.value = false;
218
+ popper.value?.destroy();
219
+ inputFieldWrapRef.value.style.borderRadius = "var(--select-border-radius)";
220
+ emit("close");
221
+ }
222
+
223
+ async function toggleDropdown() {
224
+ if (!props.isDisabled) {
225
+ if (isOpened.value) {
226
+ closeDropdown();
227
+ } else {
228
+ isOpened.value = true;
229
+ if (hasNextPage.value) {
230
+ await nextTick();
231
+ observer.observe(load.value);
232
+ }
233
+ await nextTick(() => {
234
+ search?.value?.focus();
235
+ popper.value = createPopper(
236
+ dropdownToggleRef.value,
237
+ dropdownRef.value,
238
+ {
239
+ placement: "bottom",
240
+ modifiers: [
241
+ {
242
+ name: "flip",
243
+ options: {
244
+ fallbackPlacements: ["top", "bottom"],
245
+ },
246
+ },
247
+ {
248
+ name: "preventOverflow",
249
+ options: {
250
+ mainAxis: false,
251
+ },
252
+ },
253
+ {
254
+ name: "sameWidthChangeBorders",
255
+ enabled: true,
256
+ phase: "beforeWrite",
257
+ requires: ["computeStyles"],
258
+ fn: ({ state }: { state: State }) => {
259
+ const placement = state.placement;
260
+ if (placement === "top") {
261
+ state.styles.popper.borderTop =
262
+ "1px solid var(--select-border-color)";
263
+ state.styles.popper.borderBottom =
264
+ "1px solid var(--select-background-color)";
265
+ state.styles.popper.borderRadius =
266
+ "var(--select-border-radius) var(--select-border-radius) 0 0";
267
+ inputFieldWrapRef.value.style.borderRadius =
268
+ "0 0 var(--select-border-radius) var(--select-border-radius)";
269
+ } else {
270
+ state.styles.popper.borderBottom =
271
+ "1px solid var(--select-border-color)";
272
+ state.styles.popper.borderTop =
273
+ "1px solid var(--select-background-color)";
274
+ state.styles.popper.borderRadius =
275
+ "0 0 var(--select-border-radius) var(--select-border-radius)";
276
+
277
+ if (inputFieldWrapRef.value) {
278
+ inputFieldWrapRef.value.style.borderRadius =
279
+ "var(--select-border-radius) var(--select-border-radius) 0 0";
280
+ }
281
+ }
282
+ state.styles.popper.width = `${
283
+ state.rects.reference.width + 2
284
+ }px`;
285
+ },
286
+ effect: ({ state }: { state: State }) => {
287
+ const ref = state.elements.reference as HTMLElement;
288
+ const placement = state.placement;
289
+ if (placement === "top") {
290
+ state.elements.popper.style.borderTop =
291
+ "1px solid var(--select-border-color)";
292
+ state.elements.popper.style.borderBottom =
293
+ "1px solid var(--select-background-color)";
294
+ state.elements.popper.style.borderRadius =
295
+ "var(--select-border-radius) var(--select-border-radius) 0 0";
296
+ inputFieldWrapRef.value.style.borderRadius =
297
+ "0 0 var(--select-border-radius) var(--select-border-radius)";
298
+ } else {
299
+ state.elements.popper.style.borderBottom =
300
+ "1px solid var(--select-border-color)";
301
+ state.elements.popper.style.borderTop =
302
+ "1px solid var(--select-background-color)";
303
+ state.elements.popper.style.borderRadius =
304
+ "0 0 var(--select-border-radius) var(--select-border-radius)";
305
+
306
+ if (inputFieldWrapRef.value) {
307
+ inputFieldWrapRef.value.style.borderRadius =
308
+ "var(--select-border-radius) var(--select-border-radius) 0 0";
309
+ }
310
+ }
311
+ state.elements.popper.style.width = `${
312
+ ref.offsetWidth + 2
313
+ }px`;
314
+ },
315
+ },
316
+ {
317
+ name: "offset",
318
+ options: {
319
+ offset: [0, 0],
320
+ },
321
+ },
322
+ ],
323
+ }
324
+ );
325
+ });
326
+ }
327
+ }
328
+ }
329
+
330
+ function onItemSelect(item: { [x: string]: string }) {
331
+ emit("update:modelValue", item[props.keyProperty]);
332
+ emit("change", item[props.keyProperty]);
333
+ emit("close");
334
+ isOpened.value = false;
335
+ }
336
+
337
+ function onSearch(event: InputEvent) {
338
+ emit("search", (event.target as HTMLInputElement).value);
339
+ }
340
+
341
+ // Handle input event to propertly reset value and emit changes
342
+ function onReset() {
343
+ emit("update:modelValue", "");
344
+ }
345
+
346
+ async function infiniteScroll([
347
+ { isIntersecting, target },
348
+ ]: IntersectionObserverEntry[]) {
349
+ if (
350
+ isIntersecting &&
351
+ props.onInfiniteScroll &&
352
+ typeof props.onInfiniteScroll === "function"
353
+ ) {
354
+ const ul = (target as HTMLElement).offsetParent as Element;
355
+ const scrollTop = (target as HTMLElement).offsetParent?.scrollTop;
356
+ await props.onInfiniteScroll();
357
+ await nextTick();
358
+ ul.scrollTop = scrollTop as number;
359
+ }
360
+ }
361
+ </script>
362
+
363
+ <style lang="scss">
364
+ :root {
365
+ --select-height: 38px;
366
+ --select-border-radius: 3px;
367
+ --select-border-color: #d3dbe9;
368
+ --select-border-color-error: #f14e4e;
369
+ --select-background-color: #ffffff;
370
+ --select-background-color-disabled: #fafafa;
371
+ --select-placeholder-color: #a5a5a5;
372
+ --select-chevron-color: #43b0e6;
373
+ --select-chevron-color-hover: #319ed4;
374
+ }
375
+
376
+ .vc-select {
377
+ &_disabled &__field-wrapper,
378
+ &_disabled &__field {
379
+ @apply bg-[color:var(--select-background-color-disabled)] text-[#424242];
380
+ }
381
+
382
+ &_error &__field-wrapper {
383
+ @apply border border-solid border-[color:var(--select-border-color-error)];
384
+ }
385
+
386
+ &_disabled &__field {
387
+ @apply cursor-auto;
388
+ }
389
+
390
+ &_opened &__chevron {
391
+ @apply rotate-180;
392
+ }
393
+
394
+ &_opened &__field-wrapper {
395
+ @apply rounded-t-[var(--select-border-radius)];
396
+ }
397
+
398
+ &__clear {
399
+ @apply cursor-pointer text-[color:var(--input-clear-color)] hover:text-[color:var(--input-clear-color-hover)] pr-10 pl-3 flex items-center;
400
+ }
401
+ }
402
+ </style>