classcard-ui 0.2.1462 → 0.2.1464

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 (209) hide show
  1. package/README.md +24 -24
  2. package/dist/classcard-ui.common.js +148 -113
  3. package/dist/classcard-ui.common.js.map +1 -1
  4. package/dist/classcard-ui.css +1 -1
  5. package/dist/classcard-ui.umd.js +148 -113
  6. package/dist/classcard-ui.umd.js.map +1 -1
  7. package/dist/classcard-ui.umd.min.js +2 -2
  8. package/dist/classcard-ui.umd.min.js.map +1 -1
  9. package/package.json +83 -83
  10. package/src/App.vue +16 -16
  11. package/src/colorConfig.js +52 -52
  12. package/src/components/CAlertModal/CAlertModal.vue +179 -179
  13. package/src/components/CAlertModal/index.js +3 -3
  14. package/src/components/CAlerts/CAlerts.vue +97 -97
  15. package/src/components/CAlerts/index.js +2 -2
  16. package/src/components/CAnchorTabs/CAnchorTabs.vue +100 -100
  17. package/src/components/CAnchorTabs/index.js +2 -2
  18. package/src/components/CAnchorTag/CAnchorTag.vue +84 -84
  19. package/src/components/CAnchorTag/index.js +2 -2
  20. package/src/components/CAvatar/CAvatar.vue +230 -230
  21. package/src/components/CAvatar/index.js +2 -2
  22. package/src/components/CAvatarGroup/CAvatarGroup.vue +213 -213
  23. package/src/components/CAvatarGroup/index.js +2 -2
  24. package/src/components/CBasicTable/CBasicTable.vue +184 -184
  25. package/src/components/CBasicTable/index.js +2 -2
  26. package/src/components/CBreadcrumbs/CBreadcrumbs.vue +38 -38
  27. package/src/components/CBreadcrumbs/index.js +2 -2
  28. package/src/components/CButton/CButton.vue +239 -239
  29. package/src/components/CButton/index.js +2 -2
  30. package/src/components/CButtonGroup/CButtonGroup.vue +155 -155
  31. package/src/components/CButtonGroup/index.js +2 -2
  32. package/src/components/CButtonIcon/CButtonIcon.vue +166 -166
  33. package/src/components/CButtonIcon/index.js +2 -2
  34. package/src/components/CButtonLink/CButtonLink.vue +43 -43
  35. package/src/components/CButtonLink/index.js +2 -2
  36. package/src/components/CButtonSelect/CButtonSelect.vue +186 -186
  37. package/src/components/CButtonSelect/index.js +2 -2
  38. package/src/components/CButtonSelectBorder/CButtonSelectBorder.vue +265 -265
  39. package/src/components/CButtonSelectBorder/index.js +3 -3
  40. package/src/components/CButtonWithDropdown/CButtonWithDropdown.vue +152 -152
  41. package/src/components/CButtonWithDropdown/index.js +2 -2
  42. package/src/components/CCalendar/CCalendar.vue +443 -443
  43. package/src/components/CCalendar/index.js +3 -3
  44. package/src/components/CCard/CCard.vue +53 -53
  45. package/src/components/CCard/index.js +2 -2
  46. package/src/components/CCheckbox/CCheckbox.vue +200 -200
  47. package/src/components/CCheckbox/index.js +2 -2
  48. package/src/components/CCircularButton/CCircularButton.vue +57 -57
  49. package/src/components/CCircularButton/index.js +2 -2
  50. package/src/components/CCollapsibleSection/CCollapsibleSection.vue +121 -121
  51. package/src/components/CCollapsibleSection/index.js +2 -2
  52. package/src/components/CColorDots/CColorDots.vue +52 -52
  53. package/src/components/CColorDots/index.js +3 -3
  54. package/src/components/CConfirmActionModal/CConfirmActionModal.vue +198 -198
  55. package/src/components/CConfirmActionModal/index.js +3 -3
  56. package/src/components/CDatepicker/CDatepicker.vue +235 -235
  57. package/src/components/CDatepicker/index.js +2 -2
  58. package/src/components/CDualSelect/CDualSelect.vue +193 -193
  59. package/src/components/CDualSelect/index.js +2 -2
  60. package/src/components/CEditor/CEditor.vue +114 -114
  61. package/src/components/CEditor/index.js +2 -2
  62. package/src/components/CFormSectionHeading/CFormSectionHeading.vue +76 -76
  63. package/src/components/CFormSectionHeading/index.js +2 -2
  64. package/src/components/CGroupedFilterDropdown/CGroupedFilterDropdown.vue +253 -253
  65. package/src/components/CGroupedFilterDropdown/index.js +2 -2
  66. package/src/components/CGroupedSelect/CGroupedSelect.vue +366 -366
  67. package/src/components/CGroupedSelect/index.js +3 -3
  68. package/src/components/CIcon/CIcon.vue +112 -112
  69. package/src/components/CIcon/index.js +2 -2
  70. package/src/components/CIconDropdown/CIconDropdown.vue +191 -191
  71. package/src/components/CIconDropdown/index.js +2 -2
  72. package/src/components/CIconSelect/CIconSelect.vue +182 -182
  73. package/src/components/CIconSelect/index.js +3 -3
  74. package/src/components/CInput/CInput.vue +173 -173
  75. package/src/components/CInput/index.js +2 -2
  76. package/src/components/CInputAddon/CInputAddon.vue +297 -297
  77. package/src/components/CInputAddon/index.js +2 -2
  78. package/src/components/CInputEmail/CInputEmail.vue +107 -107
  79. package/src/components/CInputEmail/index.js +2 -2
  80. package/src/components/CInsetTabs/CInsetTabs.vue +134 -134
  81. package/src/components/CInsetTabs/index.js +3 -3
  82. package/src/components/CModalHeading/CModalHeading.vue +22 -22
  83. package/src/components/CModalHeading/index.js +2 -2
  84. package/src/components/CModuleHelpLinks/CModuleHelpLinks.vue +88 -88
  85. package/src/components/CModuleHelpLinks/index.js +3 -3
  86. package/src/components/CMultiselect/CMultiselect.vue +930 -930
  87. package/src/components/CMultiselect/index.js +2 -2
  88. package/src/components/CMultiselectr/CMultiselectr.vue +44 -44
  89. package/src/components/CMultiselectr/index.js +2 -2
  90. package/src/components/CPageHeading/CPageHeading.vue +83 -83
  91. package/src/components/CPageHeading/index.js +2 -2
  92. package/src/components/CPagination/CPagination.vue +239 -239
  93. package/src/components/CPagination/index.js +2 -2
  94. package/src/components/CPhoneNumber/CPhoneNumber.vue +213 -213
  95. package/src/components/CPhoneNumber/index.js +2 -2
  96. package/src/components/CProgress/CProgress.vue +91 -91
  97. package/src/components/CProgress/index.js +2 -2
  98. package/src/components/CRadio/CRadio.vue +197 -197
  99. package/src/components/CRadio/index.js +2 -2
  100. package/src/components/CRadioGroup/CRadioGroup.vue +96 -96
  101. package/src/components/CRadioGroup/index.js +2 -2
  102. package/src/components/CRangeSlider/CRangeSlider.vue +55 -55
  103. package/src/components/CRangeSlider/index.js +2 -2
  104. package/src/components/CReorderableStackedList/CReorderableStackedList.vue +94 -94
  105. package/src/components/CReorderableStackedList/index.js +2 -2
  106. package/src/components/CSelect/CSelect.vue +1165 -1165
  107. package/src/components/CSelect/index.js +2 -2
  108. package/src/components/CSmallTimeline/CSmallTimeline.vue +40 -40
  109. package/src/components/CSmallTimeline/index.js +2 -2
  110. package/src/components/CStackedList/CStackedList.vue +162 -162
  111. package/src/components/CStackedList/index.js +2 -2
  112. package/src/components/CStats/CStats.vue +187 -187
  113. package/src/components/CStats/index.js +2 -2
  114. package/src/components/CSwitch/CSwitch.vue +200 -200
  115. package/src/components/CSwitch/index.js +2 -2
  116. package/src/components/CTabLazy/CTabLazy.vue +83 -83
  117. package/src/components/CTabLazy/index.js +2 -2
  118. package/src/components/CTable/CTable.vue +1114 -1114
  119. package/src/components/CTable/index.js +2 -2
  120. package/src/components/CTabs/CTabs.vue +250 -250
  121. package/src/components/CTabs/index.js +2 -2
  122. package/src/components/CTag/CTag.vue +109 -109
  123. package/src/components/CTag/index.js +2 -2
  124. package/src/components/CTextarea/CTextarea.vue +118 -118
  125. package/src/components/CTextarea/index.js +2 -2
  126. package/src/components/CTimeline/CTimeline.vue +237 -237
  127. package/src/components/CTimeline/index.js +2 -2
  128. package/src/components/CToolTip/CToolTip.vue +108 -108
  129. package/src/components/CToolTip/index.js +3 -3
  130. package/src/components/CUpload/CUpload.vue +331 -331
  131. package/src/components/CUpload/index.js +2 -2
  132. package/src/components/NumberAnimator.vue +112 -112
  133. package/src/components/index.js +57 -57
  134. package/src/helper.js +8 -8
  135. package/src/icons.js +829 -827
  136. package/src/main.js +22 -22
  137. package/src/stories/CAlertModal.stories.js +30 -30
  138. package/src/stories/CAlerts.stories.js +37 -37
  139. package/src/stories/CAnchorTabs.stories.js +29 -29
  140. package/src/stories/CAnchorTag.stories.js +38 -38
  141. package/src/stories/CAvatar.stories.js +38 -38
  142. package/src/stories/CAvatarGroup.stories.js +136 -136
  143. package/src/stories/CBasicTable.stories.js +316 -316
  144. package/src/stories/CBreadcrumbs.stories.js +24 -24
  145. package/src/stories/CButton.stories.js +49 -49
  146. package/src/stories/CButtonGroup.stories.js +43 -43
  147. package/src/stories/CButtonIcon.stories.js +27 -27
  148. package/src/stories/CButtonLink.stories.js +24 -24
  149. package/src/stories/CButtonSelect.stories.js +44 -44
  150. package/src/stories/CButtonSelectBorder.stories.js +56 -56
  151. package/src/stories/CButtonWithDropdown.stories.js +41 -41
  152. package/src/stories/CCalendar.stories.js +16 -16
  153. package/src/stories/CCard.stories.js +30 -30
  154. package/src/stories/CCheckbox.stories.js +38 -38
  155. package/src/stories/CCircularButton.stories.js +29 -29
  156. package/src/stories/CCollapsibleSection.stories.js +29 -29
  157. package/src/stories/CColorDots.stories.js +37 -37
  158. package/src/stories/CConfirmActionModal.stories.js +60 -60
  159. package/src/stories/CDatepicker.stories.js +31 -31
  160. package/src/stories/CDualSelect.stories.js +29 -29
  161. package/src/stories/CEditor.stories.js +30 -30
  162. package/src/stories/CFormSectionHeading.stories.js +37 -37
  163. package/src/stories/CGroupedFilterDropdown.stories.js +176 -176
  164. package/src/stories/CGroupedSelect.stories.js +103 -103
  165. package/src/stories/CIcon.stories.js +31 -31
  166. package/src/stories/CIconDropdown.stories.js +52 -52
  167. package/src/stories/CIconSelect.stories.js +45 -45
  168. package/src/stories/CInput.stories.js +36 -36
  169. package/src/stories/CInputAddon.stories.js +37 -37
  170. package/src/stories/CInputEmail.stories.js +27 -27
  171. package/src/stories/CInsetTabs.stories.js +48 -48
  172. package/src/stories/CModalHeading.stories.js +25 -25
  173. package/src/stories/CModuleHelpLinks.stories.js +25 -25
  174. package/src/stories/CMultiselect.stories.js +136 -136
  175. package/src/stories/CMultiselectr.stories.js +23 -23
  176. package/src/stories/CPageHeading.stories.js +32 -32
  177. package/src/stories/CPagination.stories.js +30 -30
  178. package/src/stories/CPhoneNumber.stories.js +37 -37
  179. package/src/stories/CProgress.stories.js +23 -23
  180. package/src/stories/CRadio.stories.js +44 -44
  181. package/src/stories/CRadioGroup.stories.js +51 -51
  182. package/src/stories/CRangeSlider.stories.js +23 -23
  183. package/src/stories/CReorderableStackedList.stories.js +23 -23
  184. package/src/stories/CSelect.stories.js +157 -157
  185. package/src/stories/CSmallTimeline.stories.js +26 -26
  186. package/src/stories/CStackedList.stories.js +37 -37
  187. package/src/stories/CStats.stories.js +53 -53
  188. package/src/stories/CSwitch.stories.js +28 -28
  189. package/src/stories/CTabLazy.stories.js +42 -42
  190. package/src/stories/CTable.stories.js +203 -203
  191. package/src/stories/CTabs.stories.js +36 -36
  192. package/src/stories/CTag.stories.js +37 -37
  193. package/src/stories/CTextarea.stories.js +32 -32
  194. package/src/stories/CTimeline.stories.js +26 -26
  195. package/src/stories/CToolTip.stories.js +27 -27
  196. package/src/stories/CUpload.stories.js +36 -36
  197. package/src/stories/Introduction.stories.mdx +207 -207
  198. package/src/stories/Page.vue +88 -88
  199. package/src/stories/assets/code-brackets.svg +0 -0
  200. package/src/stories/assets/colors.svg +0 -0
  201. package/src/stories/assets/comments.svg +0 -0
  202. package/src/stories/assets/direction.svg +0 -0
  203. package/src/stories/assets/flow.svg +0 -0
  204. package/src/stories/assets/plugin.svg +0 -0
  205. package/src/stories/assets/repo.svg +0 -0
  206. package/src/stories/assets/stackalt.svg +0 -0
  207. package/src/stories/header.css +26 -26
  208. package/src/stories/page.css +69 -69
  209. package/src/stories/utils.css +32 -32
@@ -1,1165 +1,1165 @@
1
- <template>
2
- <div>
3
- <div
4
- :class="`flex w-full items-center ${
5
- label ? 'justify-between' : 'justify-end'
6
- }`"
7
- >
8
- <div class="flex items-center" v-if="label">
9
- <!-- label of select field -->
10
- <label class="block text-sm font-medium text-gray-900">
11
- {{ label }}
12
- </label>
13
- <!-- asterisk sign to render if field is required -->
14
- <p v-if="isRequired" class="ml-1 text-red-600">*</p>
15
- </div>
16
- <button
17
- v-if="actionBtn"
18
- :id="getActionIDFn(actionBtn)"
19
- class="block text-sm font-medium text-indigo-800 hover:underline"
20
- @click="actionBtnEvent($event)"
21
- >
22
- {{ actionBtn }}
23
- </button>
24
- <span v-if="hint" class="text-sm text-gray-500">{{ hint }}</span>
25
- </div>
26
- <div :class="['relative', inputContainerClass]" ref="inputContainer">
27
- <div :class="label || actionBtn ? 'mt-1' : ''">
28
- <input
29
- ref="c-select-input"
30
- type="text"
31
- v-model="selectSearch"
32
- @click="handleInputClick"
33
- @focus="handleInputFocus"
34
- @input="search()"
35
- @keydown="handleKeydown"
36
- @keydown.enter.prevent="handleKeydown"
37
- aria-haspopup="listbox"
38
- aria-expanded="true"
39
- aria-labelledby="listbox-label"
40
- class="h-9 w-full cursor-pointer border py-2 pr-10 text-left text-sm"
41
- :class="[
42
- classes,
43
- !isValidate && 'border-red-300',
44
- inputClasses,
45
- customBorderRadius,
46
- toggleDropdown ? 'pl-10' : 'pl-3',
47
- ]"
48
- :disabled="isDisabled"
49
- autocomplete="off"
50
- @blur="handleElementBlur"
51
- :id="id"
52
- :style="inputStyle"
53
- />
54
- <button
55
- class="absolute top-2.5 right-10 z-100 cursor-pointer"
56
- :id="id + '_close_button'"
57
- v-if="showCloseButton && (value == null || value.length == 0)"
58
- @click.stop="handleCrossClick"
59
- >
60
- <c-icon
61
- name="close"
62
- type="outline-v2"
63
- class="h-4 w-4 text-gray-400"
64
- />
65
- </button>
66
- <div
67
- v-if="!toggleDropdown"
68
- class="pointer-events-none absolute inset-0 left-3 flex h-9 items-center overflow-hidden pr-10"
69
- >
70
- <div
71
- :class="['flex items-center gap-2 truncate', selectedOptionStyles]"
72
- >
73
- <div
74
- class="flex shrink-0"
75
- v-if="!addCheckBox && showImage && !selectSearch && value"
76
- >
77
- <c-avatar
78
- v-if="value.photo"
79
- size="extraextraextrasmall"
80
- :image="value.photo"
81
- :rounded="true"
82
- ></c-avatar>
83
- <c-avatar
84
- v-else
85
- size="extraextraextrasmall"
86
- :nameInitials="value.initials"
87
- :rounded="true"
88
- :isDynamicallyColored="coloredAvatars"
89
- ></c-avatar>
90
- </div>
91
- <div
92
- class="flex shrink-0"
93
- v-if="
94
- addCheckBox &&
95
- showImage &&
96
- !selectSearch &&
97
- selectedValuesArray &&
98
- selectedValuesArray.length &&
99
- selectedValuesArray[0].id
100
- "
101
- >
102
- <c-avatar
103
- v-if="selectedValuesArray[0].photo"
104
- size="extraextraextrasmall"
105
- :image="selectedValuesArray[0].photo"
106
- :rounded="true"
107
- ></c-avatar>
108
- <c-avatar
109
- v-else
110
- size="extraextraextrasmall"
111
- :nameInitials="selectedValuesArray[0].initials"
112
- :rounded="true"
113
- :isDynamicallyColored="coloredAvatars"
114
- ></c-avatar>
115
- </div>
116
- <c-icon
117
- v-if="icon && !showImage && !selectSearch"
118
- :class="icon.class"
119
- :name="icon.name"
120
- :type="icon.type"
121
- :viewBox="icon.viewBox"
122
- >
123
- </c-icon>
124
- <div
125
- v-if="addCheckBox"
126
- class="flex flex-1 items-center overflow-hidden text-sm"
127
- >
128
- <p
129
- class="block truncate"
130
- v-if="
131
- selectedValuesArray.length > 0 &&
132
- selectedValuesArray[0].id &&
133
- (!selectSearch || selectSearch == '')
134
- "
135
- >
136
- {{ selectedValuesArray[0][renderOptionName] }}
137
- </p>
138
- <p v-else>
139
- {{ !selectSearch || selectSearch == "" ? placeholder : null }}
140
- </p>
141
- <p
142
- v-if="
143
- selectedValuesArray.length > 1 &&
144
- (!selectSearch || selectSearch == '') &&
145
- selectedValuesArray[0].showExtra
146
- "
147
- class="ml-1 block"
148
- >
149
- +{{ selectedValuesArray.length - 1 }}
150
- </p>
151
- </div>
152
- <p
153
- v-else
154
- :class="[
155
- 'flex items-center gap-2 truncate text-sm',
156
- showImage ? 'ml-3' : '',
157
- selectedValueClass,
158
- ]"
159
- v-bind:style="{
160
- ...(shouldShowCustomFonts ? { fontFamily: selectedValue } : {}),
161
- }"
162
- >
163
- {{
164
- hasSelectedValue
165
- ? selectedValue
166
- : !selectSearch || selectSearch == ""
167
- ? placeholder
168
- : null
169
- }}
170
- <c-tag
171
- v-if="value && value.badge"
172
- :label="value.badge.label"
173
- :color="value.badge.color"
174
- class="shrink-0"
175
- ></c-tag>
176
- </p>
177
- </div>
178
- </div>
179
- <div
180
- v-if="toggleDropdown"
181
- class="pointer-events-none absolute inset-0 left-3 flex h-9 items-center overflow-hidden pr-10"
182
- >
183
- <div class="flex items-center gap-2">
184
- <c-icon
185
- name="search"
186
- type="outline"
187
- class="h-5 w-5 text-gray-400"
188
- ></c-icon>
189
- <p v-if="!selectSearch" class="text-sm text-gray-500">Search</p>
190
- </div>
191
- </div>
192
- <div class="pointer-events-none absolute top-2 right-3 flex">
193
- <div
194
- v-if="type == 'tertiary' ? showFocus : true"
195
- class="pointer-events-none right-0 flex items-center"
196
- >
197
- <c-icon
198
- name="chevron-down"
199
- type="solid"
200
- :class="[
201
- 'h-5 w-5 transition-transform duration-300',
202
- toggleDropdown ? 'rotate-180' : '',
203
- dropdownSelectorIconClass,
204
- ]"
205
- ></c-icon>
206
- </div>
207
- </div>
208
- </div>
209
- <transition
210
- enter-active-class="transition ease-out duration-100"
211
- enter-class="transform opacity-0 scale-95"
212
- enter-to-class="transform opacity-100 scale-100"
213
- leave-active-class="transition ease-in duration-75"
214
- leave-class="transform opacity-100 scale-100"
215
- leave-to-class="transform opacity-0 scale-95"
216
- >
217
- <div
218
- v-if="toggleDropdown && !isDisabled"
219
- :class="`${getDropdownPosition()} ${
220
- useSticky ? 'sticky sm:absolute' : 'absolute'
221
- } z-10 ${
222
- isFooter && shouldOpenAbove
223
- ? 'mb-12 max-h-60'
224
- : !isFooter && shouldOpenAbove
225
- ? 'mb-1 max-h-60'
226
- : 'mt-1 max-h-80 pb-20'
227
- } ${customWidth ? customWidth : 'w-full'}`"
228
- >
229
- <ul
230
- tabindex="-1"
231
- role="listbox"
232
- aria-labelledby="listbox-label"
233
- ref="optionsList"
234
- :class="`max-h-60 overflow-auto overscroll-contain bg-white py-1 text-sm ring-1 ring-gray-900 ring-opacity-5 focus:outline-none ${getDropdownPosition2()} ${getDropdownShadow()} ${
235
- isFooter ||
236
- (showCreateOption &&
237
- showCreateOptionAfterSearch &&
238
- selectSearch &&
239
- selectSearch.trim())
240
- ? 'rounded-t-md'
241
- : 'rounded-md'
242
- }`"
243
- @mousedown="handlePreventBlur"
244
- >
245
- <li
246
- v-if="addAction"
247
- @mousedown="actionEvent($event)"
248
- class="relative flex min-h-[36px] cursor-pointer select-none py-2 pl-3 pr-9 text-indigo-500 hover:bg-indigo-100 hover:text-indigo-700"
249
- >
250
- <c-icon
251
- type="outline"
252
- class="mr-1 h-5 w-5 text-indigo-400 group-hover:text-indigo-500"
253
- name="plus"
254
- ></c-icon>
255
- {{ addAction.label }}
256
- </li>
257
- <li v-if="headerSwitch" class="min-h-[36px] p-2 hover:bg-gray-100">
258
- <c-switch
259
- :label="headerSwitch.headerText"
260
- class="text-sm"
261
- direction="left"
262
- size="small"
263
- :value="headerSwitch.headerSwitchValue ? 1 : 0"
264
- @returnToggleValue="handleHeaderSwitch"
265
- :disabled="headerSwitch.headerSwitchDisable"
266
- ></c-switch>
267
- </li>
268
- <hr v-if="headerSwitch" class="my-1" />
269
- <!-- Select All Option -->
270
- <template
271
- v-if="
272
- (selectCheckboxes || addCheckBox) &&
273
- (organizedOptions.groups.length > 0 ||
274
- organizedOptions.selectedValues.length > 0 ||
275
- organizedOptions.unselectedValues.length > 0)
276
- "
277
- >
278
- <li
279
- @mousedown="handleSelect($event, allOption)"
280
- class="relative flex min-h-[36px] cursor-pointer select-none items-center gap-3 py-2 pl-3 pr-9 text-gray-700 hover:bg-gray-100"
281
- >
282
- <input
283
- type="checkbox"
284
- name="select-all"
285
- class="pointer-events-none h-4 w-4 cursor-pointer rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 disabled:opacity-50"
286
- :checked="selectAll"
287
- />
288
-
289
- <div class="flex items-center gap-2">
290
- <c-icon
291
- v-if="allOption && allOption.icon"
292
- :class="allOption.icon.class"
293
- :name="allOption.icon.name"
294
- :type="allOption.icon.type"
295
- :viewBox="allOption.icon.viewBox"
296
- ></c-icon>
297
- <span class="list-options block break-words font-normal">
298
- Select all
299
- </span>
300
- </div>
301
- </li>
302
- <hr class="my-1" />
303
- </template>
304
- <!-- Groups Section -->
305
- <li
306
- v-for="(option, index) in organizedOptions.groups"
307
- :key="`group-${option.id || index}`"
308
- id="listbox-option-group"
309
- role="option"
310
- :ref="`option-group-${String(index)}`"
311
- @mousedown="handleSelect($event, option)"
312
- :class="option.isDisabled ? 'pointer-events-none' : ''"
313
- >
314
- <span
315
- class="flex min-h-[36px] w-full items-center gap-3 px-3 py-2"
316
- :class="`group ${
317
- option.isDisabled ? 'custom-disabled-state' : ''
318
- } ${optionClasses} relative cursor-pointer select-none text-gray-700 hover:bg-gray-100`"
319
- >
320
- <div v-if="addCheckBox" class="flex h-5 items-center">
321
- <input
322
- type="checkbox"
323
- name="group"
324
- class="pointer-events-none h-4 w-4 cursor-pointer rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 disabled:opacity-50"
325
- :checked="isChecked(option)"
326
- />
327
- </div>
328
- <div class="flex items-center gap-2">
329
- <div v-if="option.showImage" class="flex-shrink-0">
330
- <c-avatar
331
- v-if="option.photo"
332
- size="extraextraextrasmall"
333
- :image="option.photo"
334
- :rounded="true"
335
- ></c-avatar>
336
- <c-avatar
337
- v-else
338
- size="extraextraextrasmall"
339
- :nameInitials="option.initials"
340
- :rounded="true"
341
- :isDynamicallyColored="coloredAvatars"
342
- ></c-avatar>
343
- </div>
344
- <div
345
- class="flex items-center justify-center rounded-full bg-gray-100 p-1"
346
- v-if="option.showIcon && option.icon"
347
- >
348
- <c-icon
349
- :class="option.icon.class"
350
- :name="option.icon.name"
351
- :type="option.icon.type"
352
- :viewBox="option.icon.viewBox"
353
- >
354
- </c-icon>
355
- </div>
356
- <span
357
- :class="[
358
- option.photo || option.initials ? '' : 'text-left',
359
- addCheckBox ? 'overflow-hidden' : '',
360
- ]"
361
- class="list-options block break-words font-normal"
362
- v-bind:style="{
363
- ...(shouldShowCustomFonts
364
- ? { fontFamily: option[renderOptionName] }
365
- : {}),
366
- }"
367
- >{{ option[renderOptionName] }}
368
- </span>
369
- </div>
370
- </span>
371
- </li>
372
- <hr
373
- v-if="
374
- organizedOptions.groups.length > 0 &&
375
- (organizedOptions.selectedValues.length > 0 ||
376
- organizedOptions.unselectedValues.length > 0)
377
- "
378
- class="my-1"
379
- />
380
- <!-- Selected values section (non-groups) -->
381
- <li
382
- v-for="(option, index) in organizedOptions.selectedValues"
383
- :key="`selected-value-${option.id || index}`"
384
- :id="`listbox-option-selected-value-${String(index)}`"
385
- role="option"
386
- :ref="`option-selected-value-${String(index)}`"
387
- @mousedown="handleSelect($event, option)"
388
- :class="option.isDisabled ? 'pointer-events-none' : ''"
389
- >
390
- <span
391
- class="flex min-h-[36px] w-full items-center gap-3 px-3 py-2"
392
- :class="`group ${
393
- option.isDisabled ? 'custom-disabled-state' : ''
394
- } ${optionClasses} relative cursor-pointer select-none text-gray-700 hover:bg-gray-100`"
395
- >
396
- <div v-if="addCheckBox" class="flex h-5 items-center">
397
- <input
398
- type="checkbox"
399
- name="value"
400
- class="pointer-events-none h-4 w-4 cursor-pointer rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 disabled:opacity-50"
401
- :checked="preservedOrder ? isChecked(option) : true"
402
- />
403
- </div>
404
- <div class="flex items-center gap-2">
405
- <div v-if="option.showImage" class="flex-shrink-0">
406
- <c-avatar
407
- v-if="option.photo"
408
- size="extraextraextrasmall"
409
- :image="option.photo"
410
- :rounded="true"
411
- ></c-avatar>
412
- <c-avatar
413
- v-else
414
- size="extraextraextrasmall"
415
- :nameInitials="option.initials"
416
- :rounded="true"
417
- :isDynamicallyColored="coloredAvatars"
418
- ></c-avatar>
419
- </div>
420
- <div
421
- class="flex items-center justify-center rounded-full bg-gray-100 p-1"
422
- v-if="option.showIcon && option.icon"
423
- >
424
- <c-icon
425
- :class="option.icon.class"
426
- :name="option.icon.name"
427
- :type="option.icon.type"
428
- :viewBox="option.icon.viewBox"
429
- >
430
- </c-icon>
431
- </div>
432
- <span
433
- :class="[
434
- option.photo || option.initials ? '' : 'text-left',
435
- addCheckBox ? 'overflow-hidden' : '',
436
- ]"
437
- class="list-options block break-words font-normal"
438
- v-bind:style="{
439
- ...(shouldShowCustomFonts
440
- ? { fontFamily: option[renderOptionName] }
441
- : {}),
442
- }"
443
- >{{ option[renderOptionName] }}
444
- </span>
445
- </div>
446
- </span>
447
- <hr v-if="option.isBorder" class="my-1" />
448
- </li>
449
- <hr
450
- v-if="
451
- organizedOptions.selectedValues.length > 0 &&
452
- organizedOptions.unselectedValues.length > 0
453
- "
454
- class="my-1"
455
- />
456
- <!-- Unselected values section -->
457
- <li
458
- v-for="(option, index) in organizedOptions.unselectedValues"
459
- :key="`unselected-value-${option.id || index}`"
460
- :id="`listbox-option-unselected-value-${String(index)}`"
461
- role="option"
462
- :ref="`option-unselected-value-${String(index)}`"
463
- @mousedown="handleSelect($event, option)"
464
- :class="option.isDisabled ? 'pointer-events-none' : ''"
465
- >
466
- <span
467
- class="flex min-h-[36px] w-full items-center gap-3 px-3 py-2"
468
- :class="`group ${
469
- option.isDisabled ? 'custom-disabled-state' : ''
470
- } ${optionClasses} relative cursor-pointer select-none text-gray-700 hover:bg-gray-100`"
471
- >
472
- <div v-if="addCheckBox" class="flex h-5 items-center">
473
- <input
474
- type="checkbox"
475
- name="value"
476
- class="pointer-events-none h-4 w-4 cursor-pointer rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 disabled:opacity-50"
477
- :checked="selectAll || isChecked(option)"
478
- />
479
- </div>
480
- <div class="flex items-center gap-2">
481
- <div v-if="option.showImage" class="flex-shrink-0">
482
- <c-avatar
483
- v-if="option.photo"
484
- size="extraextraextrasmall"
485
- :image="option.photo"
486
- :rounded="true"
487
- ></c-avatar>
488
- <c-avatar
489
- v-else
490
- size="extraextraextrasmall"
491
- :nameInitials="option.initials"
492
- :rounded="true"
493
- :isDynamicallyColored="coloredAvatars"
494
- ></c-avatar>
495
- </div>
496
- <div
497
- class="flex items-center justify-center rounded-full bg-gray-100 p-1"
498
- v-if="option.showIcon && option.icon"
499
- >
500
- <c-icon
501
- :class="option.icon.class"
502
- :name="option.icon.name"
503
- :type="option.icon.type"
504
- :viewBox="option.icon.viewBox"
505
- >
506
- </c-icon>
507
- </div>
508
- <span
509
- :class="[
510
- option.photo || option.initials ? '' : 'text-left',
511
- addCheckBox ? 'overflow-hidden' : '',
512
- ]"
513
- class="list-options flex items-center gap-2 break-words font-normal"
514
- v-bind:style="{
515
- ...(shouldShowCustomFonts
516
- ? { fontFamily: option[renderOptionName] }
517
- : {}),
518
- }"
519
- >{{ option[renderOptionName] }}
520
- <c-tag
521
- v-if="option.badge"
522
- :label="option.badge.label"
523
- :color="option.badge.color"
524
- class="shrink-0"
525
- ></c-tag>
526
- </span>
527
- </div>
528
- </span>
529
- <hr v-if="option.isBorder" class="my-1" />
530
- </li>
531
- <li
532
- v-if="
533
- organizedOptions.groups.length === 0 &&
534
- organizedOptions.selectedValues.length === 0 &&
535
- organizedOptions.unselectedValues.length === 0 &&
536
- selectSearch &&
537
- selectSearch.length > 0
538
- "
539
- class="px-3 py-2 text-sm text-gray-500"
540
- >
541
- No options found, try searching something else.
542
- </li>
543
- </ul>
544
- <div
545
- v-if="isFooter"
546
- class="group cursor-pointer rounded-b-md bg-gray-50 p-3 ring-1 ring-gray-900 ring-opacity-5"
547
- @mousedown="handleFooter"
548
- >
549
- <p class="text-sm text-indigo-700">
550
- {{ footerText }}
551
- </p>
552
- </div>
553
- <div
554
- v-if="
555
- showCreateOption &&
556
- showCreateOptionAfterSearch &&
557
- selectSearch &&
558
- selectSearch.trim()
559
- "
560
- class="sticky bottom-0 z-10 flex min-h-[44px] items-start gap-3 rounded-b-md bg-white px-3 py-2 shadow-lg ring-1 ring-gray-900 ring-opacity-5"
561
- >
562
- <span class="word-break flex-1 text-sm text-gray-700">{{
563
- selectSearch
564
- }}</span>
565
-
566
- <button
567
- type="button"
568
- :id="id + '_create_button'"
569
- @mousedown="handleCreateOption($event)"
570
- class="flex items-center gap-1 text-sm text-indigo-700"
571
- >
572
- <c-icon
573
- type="solid"
574
- name="plus-circle-solid-v2"
575
- class="h-5 w-5 text-indigo-600"
576
- viewBox="0 0 24 24"
577
- ></c-icon>
578
- Create
579
- </button>
580
- </div>
581
- </div>
582
- </transition>
583
- </div>
584
- <p
585
- v-if="helpText && isValidate == true"
586
- class="mt-2 text-left text-sm text-gray-500"
587
- >
588
- {{ helpText }}
589
- </p>
590
- <!-- validation error message -->
591
- <p
592
- v-if="!isValidate && errorMessage"
593
- class="mt-2 text-left text-sm text-red-600"
594
- >
595
- {{ errorMessage }}
596
- </p>
597
- </div>
598
- </template>
599
- <script>
600
- import { isNumber, isEmpty } from "lodash-es";
601
- import CIcon from "../CIcon/CIcon.vue";
602
- import { getActionID } from "../../helper";
603
- import CTag from '../CTag/CTag.vue';
604
- import CAvatar from '../CAvatar/CAvatar.vue';
605
- export default {
606
- name: "CSelect",
607
- components: {
608
- CIcon,
609
- CTag,
610
- CAvatar,
611
- },
612
- props: {
613
- // id of selectpicker
614
- id: {
615
- type: String,
616
- },
617
- // label of selectpicker
618
- label: {
619
- type: String,
620
- },
621
- // action button of selectpicker
622
- actionBtn: {
623
- type: String,
624
- default: null,
625
- },
626
- // placeholder in selectpicker
627
- placeholder: {
628
- type: String,
629
- },
630
- // list to render in dropdown
631
- options: {
632
- type: Array,
633
- required: true,
634
- },
635
- // show the avatars with different colors
636
- coloredAvatars: {
637
- type: Boolean,
638
- },
639
- // text below dropdown to describe more about the field
640
- helpText: {
641
- type: String,
642
- },
643
- // implement a fixed btn at the bottom of dropdown
644
- isFooter: {
645
- type: Boolean,
646
- },
647
- // style input field
648
- inputStyle: {
649
- type: String,
650
- },
651
- // footer btn text
652
- footerText: {
653
- type: String,
654
- },
655
- // text adjacent to label of dropdown to provide hint about field
656
- hint: { type: String },
657
- // to show if image is present along with dropdown option
658
- showImage: { type: Boolean },
659
- // whether the select field is mandatory or not
660
- isRequired: {
661
- type: Boolean,
662
- },
663
- // validation is passed or not
664
- isValidate: { type: Boolean, default: true },
665
- // will truncate the text in input field
666
- selectedOptionStyles: {
667
- type: String,
668
- },
669
- // validation error message
670
- errorMessage: {
671
- type: String,
672
- },
673
- // perform action on first option in dropdown
674
- addAction: { type: Object },
675
- // icons in dropdown list
676
- icon: { type: Object },
677
- // value to set as default option in dropdown
678
- value: { type: [Object, String, Array], default: null },
679
- // type of dropdown - gray,tertiary, blue or default
680
- type: {
681
- type: String,
682
- },
683
- // name of the field in array to that contains text to render in dropdown
684
- renderOptionName: {
685
- type: String,
686
- default: "option",
687
- },
688
- // whether to disable the selectpicker
689
- isDisabled: {
690
- type: Boolean,
691
- default: false,
692
- },
693
- inputClasses: {
694
- type: String,
695
- default: "",
696
- },
697
- optionClasses: {
698
- type: String,
699
- default: "",
700
- },
701
- // customised width for the dropdown
702
- customWidth: {
703
- type: String,
704
- default: null,
705
- },
706
- customBorderRadius: {
707
- type: String,
708
- default: "rounded-md",
709
- },
710
- addCheckBox: {
711
- type: Boolean,
712
- default: false,
713
- },
714
- selectAll: {
715
- type: Boolean,
716
- default: false,
717
- },
718
- selectCheckboxes: {
719
- type: Boolean,
720
- default: false,
721
- },
722
- headerSwitch: {
723
- type: Object,
724
- default: null,
725
- },
726
- useSticky: {
727
- type: Boolean,
728
- default: false,
729
- },
730
- showCloseButton: {
731
- type: Boolean,
732
- default: false,
733
- },
734
- showCreateOption: {
735
- type: Boolean,
736
- default: false,
737
- },
738
- shouldShowCustomFonts: {
739
- type: Boolean,
740
- default: false,
741
- },
742
- customFontNames: {
743
- type: Array,
744
- default: () => [],
745
- },
746
- inputContainerClass: {
747
- type: String,
748
- default: "",
749
- },
750
- },
751
- data() {
752
- return {
753
- toggleDropdown: false,
754
- selectedValue: null,
755
- showFocus: false,
756
- selectSearch: null,
757
- renderOptions: this.options,
758
- selectedValuesArray: [],
759
- isToggle: 0,
760
- shouldOpenAbove: false,
761
- // Frozen order while dropdown is open - prevents UI shift on selection
762
- preservedOrder: null,
763
- };
764
- },
765
- computed: {
766
- classes() {
767
- return {
768
- "text-gray-900 border border-gray-300 hover:bg-gray-200 bg-gray-100 focus:ring-2 focus:border-gray-200 focus:ring-gray-200 focus:ring-offset-2 shadow-sm":
769
- this.type == "gray",
770
- "bg-white border border-gray-300 hover:bg-indigo-100 text-indigo-600 hover:text-indigo-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-200":
771
- this.type == "tertiary",
772
- "bg-white text-indigo-700 border-none focus:outline-none":
773
- this.type == "indigo",
774
- "border border-gray-300 bg-white shadow-sm focus:outline-none focus:border-gray-300 focus:ring-indigo-600 focus:ring-2 focus:ring-offset-2":
775
- this.type == null,
776
- };
777
- },
778
- allOption() {
779
- if (
780
- this.options &&
781
- this.options.length > 0 &&
782
- this.options[0] &&
783
- this.options[0].id === "all"
784
- ) {
785
- return this.options[0];
786
- }
787
- return null;
788
- },
789
- organizedOptions() {
790
- if (!this.addCheckBox) {
791
- const filteredOptions = this.renderOptions.filter(
792
- (option) => option.id !== "all"
793
- );
794
- return {
795
- groups: [],
796
- selectedValues: [],
797
- unselectedValues: filteredOptions,
798
- };
799
- }
800
-
801
- // While dropdown is open and not searching, preserve order to prevent UI shift on selection
802
- if (
803
- this.preservedOrder &&
804
- (!this.selectSearch || this.selectSearch.trim() === "")
805
- ) {
806
- return this.preservedOrder;
807
- }
808
-
809
- return this.computeOrganizedOptions();
810
- },
811
- showCreateOptionAfterSearch() {
812
- if (!this.showCreateOption) return false;
813
-
814
- const term = (this.selectSearch || "").trim().toLowerCase();
815
- if (!term) return false;
816
-
817
- const hasExactMatch = this.options.some((option) => {
818
- const label = option[this.renderOptionName];
819
- return label && String(label).toLowerCase() === term;
820
- });
821
-
822
- return !hasExactMatch;
823
- },
824
- dropdownSelectorIconClass() {
825
- return this.type === "indigo" ? "text-indigo-600" : "text-gray-400";
826
- },
827
- hasSelectedValue() {
828
- const val = this.selectedValue;
829
- if (val === null || val === undefined || val === "") {
830
- return false;
831
- }
832
- if (typeof val === "object") {
833
- return !isEmpty(val);
834
- }
835
- return true;
836
- },
837
- selectedValueClass() {
838
- if (this.hasSelectedValue) {
839
- if (this.isDisabled) {
840
- return "text-gray-500";
841
- }
842
-
843
- if (this.type === "indigo") {
844
- return "text-indigo-600";
845
- }
846
- return "text-gray-700";
847
- }
848
- return "text-gray-500";
849
- },
850
- },
851
- methods: {
852
- isEmpty,
853
- getActionIDFn(name) {
854
- return getActionID(name, this.id);
855
- },
856
- checkAvailableSpace() {
857
- if (!this.$refs.inputContainer) return false;
858
-
859
- const container = this.$refs.inputContainer;
860
- const rect = container.getBoundingClientRect();
861
- const viewportHeight = window.innerHeight;
862
- const bottomSpace = viewportHeight - rect.bottom;
863
- const componentHeight = 320;
864
-
865
- // Check if bottom space is less than 80% the component height if there is no footer
866
- return (
867
- bottomSpace <
868
- (this.isFooter ? componentHeight : (componentHeight * 4) / 5)
869
- );
870
- },
871
- getDropdownPosition() {
872
- this.shouldOpenAbove = this.checkAvailableSpace();
873
- return this.shouldOpenAbove ? "bottom-full" : "top-full";
874
- },
875
- getDropdownPosition2() {
876
- return this.shouldOpenAbove ? "bottom-full" : "";
877
- },
878
- getDropdownShadow() {
879
- if (this.shouldOpenAbove) {
880
- return "shadow-[0_-10px_15px_-3px_rgba(0,0,0,0.1),0_-4px_6px_-4px_rgba(0,0,0,0.1)]";
881
- } else {
882
- return "shadow-lg";
883
- }
884
- },
885
- handleCrossClick(event) {
886
- event.preventDefault();
887
- this.$emit("hide-single-select-dropdown");
888
- },
889
- handleInputClick() {
890
- this.toggleDropdown = !this.toggleDropdown;
891
- if (!this.toggleDropdown) {
892
- this.selectSearch = "";
893
- this.$nextTick(() => {
894
- if (this.$refs["c-select-input"]) {
895
- this.$refs["c-select-input"].focus();
896
- }
897
- });
898
- }
899
- },
900
- handleInputFocus() {
901
- if (this.type == "tertiary") {
902
- this.showFocus = true;
903
- }
904
- },
905
- handleSelect(event, option) {
906
- this.selectSearch = null;
907
- this.selectedValue = option[this.renderOptionName];
908
- if (this.addCheckBox) {
909
- // Freeze current display order BEFORE updating selection to prevent UI shift
910
- const currentDisplay = this.organizedOptions;
911
- this.preservedOrder = {
912
- groups: [...currentDisplay.groups],
913
- selectedValues: [...currentDisplay.selectedValues],
914
- unselectedValues: [...currentDisplay.unselectedValues],
915
- };
916
- if (this.getIndex(option) == -1) {
917
- this.selectedValuesArray = [...this.selectedValuesArray, option];
918
- } else {
919
- this.selectedValuesArray = this.selectedValuesArray.filter(
920
- (item) => option.id !== item.id
921
- );
922
- }
923
- this.$emit("onChangeChecked", this.selectedValuesArray, option);
924
- }
925
-
926
- this.$emit("onChangeValue", option);
927
- if (!this.addCheckBox) {
928
- this.toggleDropdown = false;
929
- }
930
- this.type === "tertiary" ? (this.showFocus = false) : "";
931
- this.renderOptions = this.options;
932
- },
933
- handleFooter() {
934
- this.$emit("handleFooter");
935
- },
936
- actionEvent(event) {
937
- this.$emit("listAction", event);
938
- },
939
- actionBtnEvent(event) {
940
- this.$emit("listAction", event);
941
- },
942
- handleElementBlur() {
943
- this.toggleDropdown = false;
944
- if (this.addCheckBox) {
945
- this.$emit("onClickOutside", this.selectedValuesArray);
946
- }
947
- this.selectSearch = null;
948
- this.renderOptions = this.options;
949
- },
950
- handleHeaderSwitch(value) {
951
- this.$emit("handleHeaderSwitch", value);
952
- },
953
- //this prevents the blur to be called when the user clicks on the scrollbar
954
- handlePreventBlur(event) {
955
- event.preventDefault();
956
- },
957
- search() {
958
- this.selectedValue = null;
959
- // if the search term is empty space, then set the render options to empty array
960
- if (
961
- this.selectSearch !== null &&
962
- this.selectSearch.length > 0 &&
963
- this.selectSearch.trim() === ""
964
- ) {
965
- this.renderOptions = [];
966
- this.$emit("search-term", this.selectSearch);
967
- return;
968
- }
969
- if (
970
- !this.selectSearch ||
971
- this.selectSearch == "" ||
972
- this.selectSearch.trim() == ""
973
- ) {
974
- this.renderOptions = this.options;
975
- this.$emit("search-term", this.selectSearch);
976
- return;
977
- }
978
- let options = [...this.options];
979
- this.renderOptions = options.filter((option) => {
980
- isNumber(option[this.renderOptionName])
981
- ? (option[this.renderOptionName] =
982
- option[this.renderOptionName].toString())
983
- : "";
984
- const optionName = String(
985
- option[this.renderOptionName] || ""
986
- ).toLowerCase();
987
- const searchTerm = this.selectSearch.trim().toLowerCase();
988
- return optionName.includes(searchTerm);
989
- });
990
-
991
- if (this.renderOptions.length === 0) {
992
- this.$emit("search-term", this.selectSearch.trim());
993
- }
994
- },
995
- async scrollOptionIntoView(refName) {
996
- await this.$nextTick();
997
- const list = this.$refs.optionsList;
998
- const optionEl = (this.$refs[refName] || [])[0];
999
- if (!list || !optionEl) return;
1000
-
1001
- const top = optionEl.offsetTop;
1002
- const bottom = top + optionEl.offsetHeight;
1003
- const viewTop = list.scrollTop;
1004
- const viewBottom = viewTop + list.clientHeight;
1005
-
1006
- const target =
1007
- top < viewTop
1008
- ? top
1009
- : bottom > viewBottom
1010
- ? bottom - list.clientHeight
1011
- : viewTop;
1012
-
1013
- if (target !== viewTop) {
1014
- list.scrollTo({ top: target, behavior: "smooth" });
1015
- }
1016
- },
1017
- getIndex(option) {
1018
- return this.selectedValuesArray.findIndex((item) => item.id == option.id);
1019
- },
1020
- isChecked(option) {
1021
- return this.selectedValuesArray.findIndex(
1022
- (item) => item.id == option.id
1023
- ) == -1
1024
- ? false
1025
- : true;
1026
- },
1027
- handleCreateOption(event) {
1028
- event.preventDefault();
1029
- this.toggleDropdown = false;
1030
- this.$emit("handleCreateOption", {
1031
- event,
1032
- searchText: this.selectSearch.trim(),
1033
- });
1034
- this.selectSearch = null;
1035
- this.renderOptions = this.options;
1036
-
1037
- const cSelectInputRef = this.$refs["c-select-input"];
1038
- this.$nextTick(() => {
1039
- cSelectInputRef && cSelectInputRef.blur();
1040
- });
1041
- },
1042
- handleKeydown(event) {
1043
- if (event.key === "Enter") {
1044
- event.preventDefault();
1045
- event.stopPropagation();
1046
-
1047
- if (
1048
- this.showCreateOption &&
1049
- this.showCreateOptionAfterSearch &&
1050
- this.selectSearch &&
1051
- this.selectSearch.trim()
1052
- ) {
1053
- this.handleCreateOption(event);
1054
- }
1055
- }
1056
- },
1057
- async handleLoadFont() {
1058
- const customFontNames = this.customFontNames;
1059
- const googleParam = customFontNames
1060
- .map((f) => `family=${f.replace(/ /g, "+")}`)
1061
- .join("&");
1062
-
1063
- const link = document.createElement("link");
1064
- link.rel = "stylesheet";
1065
- link.href = `https://fonts.googleapis.com/css2?${googleParam}&display=swap`;
1066
- link.setAttribute("data-fonts", customFontNames.join(","));
1067
- document.head.appendChild(link);
1068
-
1069
- await document.fonts.ready;
1070
- },
1071
- isGroup(option) {
1072
- return option.type && option.type.includes("group");
1073
- },
1074
- computeOrganizedOptions() {
1075
- const selectedIds = new Set(
1076
- this.selectedValuesArray.map((item) => item.id)
1077
- );
1078
-
1079
- const groups = [];
1080
- const selectedValues = [];
1081
- const unselectedValues = [];
1082
-
1083
- // Filter out the "all" option from renderOptions
1084
- this.renderOptions
1085
- .filter((option) => option.id !== "all")
1086
- .forEach((option) => {
1087
- if (this.isGroup(option)) {
1088
- // Groups stay in their own section regardless of selection
1089
- groups.push(option);
1090
- } else {
1091
- // Values are separated by selection state
1092
- if (selectedIds.has(option.id)) {
1093
- selectedValues.push(option);
1094
- } else {
1095
- unselectedValues.push(option);
1096
- }
1097
- }
1098
- });
1099
-
1100
- // Sort by renderOptionName (guard against undefined a/b or missing renderOptionName)
1101
- const getSortKey = (item) =>
1102
- String((item && item[this.renderOptionName]) || "").toLowerCase();
1103
-
1104
- selectedValues.sort((a, b) => getSortKey(a).localeCompare(getSortKey(b)));
1105
- unselectedValues.sort((a, b) => getSortKey(a).localeCompare(getSortKey(b)));
1106
-
1107
- return {
1108
- groups,
1109
- selectedValues,
1110
- unselectedValues,
1111
- };
1112
- },
1113
- },
1114
- watch: {
1115
- value() {
1116
- this.selectedValue =
1117
- this.value !== null && this.value[this.renderOptionName]
1118
- ? this.value[this.renderOptionName]
1119
- : this.value;
1120
- if (this.addCheckBox) {
1121
- this.selectedValuesArray = this.value ? this.value : [];
1122
- }
1123
- },
1124
- options() {
1125
- this.renderOptions = this.options;
1126
- },
1127
- toggleDropdown(newValue) {
1128
- if (newValue) {
1129
- this.checkAvailableSpace();
1130
-
1131
- this.preservedOrder = null;
1132
-
1133
- const index = this.renderOptions.findIndex(
1134
- (option) => option[this.renderOptionName] === this.selectedValue
1135
- );
1136
- if (index !== -1) {
1137
- const refName = `option-${String(index)}`;
1138
- this.scrollOptionIntoView(refName);
1139
- }
1140
- } else {
1141
- this.preservedOrder = null;
1142
- }
1143
- },
1144
- },
1145
- mounted() {
1146
- this.selectedValue =
1147
- this.value !== null && this.value[this.renderOptionName]
1148
- ? this.value[this.renderOptionName]
1149
- : this.value;
1150
- if (this.addCheckBox) {
1151
- this.selectedValuesArray = this.value ? this.value : [];
1152
- }
1153
-
1154
- if (this.shouldShowCustomFonts && this.customFontNames.length > 0) {
1155
- this.handleLoadFont();
1156
- }
1157
- },
1158
- };
1159
- </script>
1160
-
1161
- <style scoped>
1162
- .word-break {
1163
- word-break: break-word;
1164
- }
1165
- </style>
1
+ <template>
2
+ <div>
3
+ <div
4
+ :class="`flex w-full items-center ${
5
+ label ? 'justify-between' : 'justify-end'
6
+ }`"
7
+ >
8
+ <div class="flex items-center" v-if="label">
9
+ <!-- label of select field -->
10
+ <label class="block text-sm font-medium text-gray-900">
11
+ {{ label }}
12
+ </label>
13
+ <!-- asterisk sign to render if field is required -->
14
+ <p v-if="isRequired" class="ml-1 text-red-600">*</p>
15
+ </div>
16
+ <button
17
+ v-if="actionBtn"
18
+ :id="getActionIDFn(actionBtn)"
19
+ class="block text-sm font-medium text-indigo-800 hover:underline"
20
+ @click="actionBtnEvent($event)"
21
+ >
22
+ {{ actionBtn }}
23
+ </button>
24
+ <span v-if="hint" class="text-sm text-gray-500">{{ hint }}</span>
25
+ </div>
26
+ <div :class="['relative', inputContainerClass]" ref="inputContainer">
27
+ <div :class="label || actionBtn ? 'mt-1' : ''">
28
+ <input
29
+ ref="c-select-input"
30
+ type="text"
31
+ v-model="selectSearch"
32
+ @click="handleInputClick"
33
+ @focus="handleInputFocus"
34
+ @input="search()"
35
+ @keydown="handleKeydown"
36
+ @keydown.enter.prevent="handleKeydown"
37
+ aria-haspopup="listbox"
38
+ aria-expanded="true"
39
+ aria-labelledby="listbox-label"
40
+ class="h-9 w-full cursor-pointer border py-2 pr-10 text-left text-sm"
41
+ :class="[
42
+ classes,
43
+ !isValidate && 'border-red-300',
44
+ inputClasses,
45
+ customBorderRadius,
46
+ toggleDropdown ? 'pl-10' : 'pl-3',
47
+ ]"
48
+ :disabled="isDisabled"
49
+ autocomplete="off"
50
+ @blur="handleElementBlur"
51
+ :id="id"
52
+ :style="inputStyle"
53
+ />
54
+ <button
55
+ class="absolute top-2.5 right-10 z-100 cursor-pointer"
56
+ :id="id + '_close_button'"
57
+ v-if="showCloseButton && (value == null || value.length == 0)"
58
+ @click.stop="handleCrossClick"
59
+ >
60
+ <c-icon
61
+ name="close"
62
+ type="outline-v2"
63
+ class="h-4 w-4 text-gray-400"
64
+ />
65
+ </button>
66
+ <div
67
+ v-if="!toggleDropdown"
68
+ class="pointer-events-none absolute inset-0 left-3 flex h-9 items-center overflow-hidden pr-10"
69
+ >
70
+ <div
71
+ :class="['flex items-center gap-2 truncate', selectedOptionStyles]"
72
+ >
73
+ <div
74
+ class="flex shrink-0"
75
+ v-if="!addCheckBox && showImage && !selectSearch && value"
76
+ >
77
+ <c-avatar
78
+ v-if="value.photo"
79
+ size="extraextraextrasmall"
80
+ :image="value.photo"
81
+ :rounded="true"
82
+ ></c-avatar>
83
+ <c-avatar
84
+ v-else
85
+ size="extraextraextrasmall"
86
+ :nameInitials="value.initials"
87
+ :rounded="true"
88
+ :isDynamicallyColored="coloredAvatars"
89
+ ></c-avatar>
90
+ </div>
91
+ <div
92
+ class="flex shrink-0"
93
+ v-if="
94
+ addCheckBox &&
95
+ showImage &&
96
+ !selectSearch &&
97
+ selectedValuesArray &&
98
+ selectedValuesArray.length &&
99
+ selectedValuesArray[0].id
100
+ "
101
+ >
102
+ <c-avatar
103
+ v-if="selectedValuesArray[0].photo"
104
+ size="extraextraextrasmall"
105
+ :image="selectedValuesArray[0].photo"
106
+ :rounded="true"
107
+ ></c-avatar>
108
+ <c-avatar
109
+ v-else
110
+ size="extraextraextrasmall"
111
+ :nameInitials="selectedValuesArray[0].initials"
112
+ :rounded="true"
113
+ :isDynamicallyColored="coloredAvatars"
114
+ ></c-avatar>
115
+ </div>
116
+ <c-icon
117
+ v-if="icon && !showImage && !selectSearch"
118
+ :class="icon.class"
119
+ :name="icon.name"
120
+ :type="icon.type"
121
+ :viewBox="icon.viewBox"
122
+ >
123
+ </c-icon>
124
+ <div
125
+ v-if="addCheckBox"
126
+ class="flex flex-1 items-center overflow-hidden text-sm"
127
+ >
128
+ <p
129
+ class="block truncate"
130
+ v-if="
131
+ selectedValuesArray.length > 0 &&
132
+ selectedValuesArray[0].id &&
133
+ (!selectSearch || selectSearch == '')
134
+ "
135
+ >
136
+ {{ selectedValuesArray[0][renderOptionName] }}
137
+ </p>
138
+ <p v-else>
139
+ {{ !selectSearch || selectSearch == "" ? placeholder : null }}
140
+ </p>
141
+ <p
142
+ v-if="
143
+ selectedValuesArray.length > 1 &&
144
+ (!selectSearch || selectSearch == '') &&
145
+ selectedValuesArray[0].showExtra
146
+ "
147
+ class="ml-1 block"
148
+ >
149
+ +{{ selectedValuesArray.length - 1 }}
150
+ </p>
151
+ </div>
152
+ <p
153
+ v-else
154
+ :class="[
155
+ 'flex items-center gap-2 truncate text-sm',
156
+ showImage ? 'ml-3' : '',
157
+ selectedValueClass,
158
+ ]"
159
+ v-bind:style="{
160
+ ...(shouldShowCustomFonts ? { fontFamily: selectedValue } : {}),
161
+ }"
162
+ >
163
+ {{
164
+ hasSelectedValue
165
+ ? selectedValue
166
+ : !selectSearch || selectSearch == ""
167
+ ? placeholder
168
+ : null
169
+ }}
170
+ <c-tag
171
+ v-if="value && value.badge"
172
+ :label="value.badge.label"
173
+ :color="value.badge.color"
174
+ class="shrink-0"
175
+ ></c-tag>
176
+ </p>
177
+ </div>
178
+ </div>
179
+ <div
180
+ v-if="toggleDropdown"
181
+ class="pointer-events-none absolute inset-0 left-3 flex h-9 items-center overflow-hidden pr-10"
182
+ >
183
+ <div class="flex items-center gap-2">
184
+ <c-icon
185
+ name="search"
186
+ type="outline"
187
+ class="h-5 w-5 text-gray-400"
188
+ ></c-icon>
189
+ <p v-if="!selectSearch" class="text-sm text-gray-500">Search</p>
190
+ </div>
191
+ </div>
192
+ <div class="pointer-events-none absolute top-2 right-3 flex">
193
+ <div
194
+ v-if="type == 'tertiary' ? showFocus : true"
195
+ class="pointer-events-none right-0 flex items-center"
196
+ >
197
+ <c-icon
198
+ name="chevron-down"
199
+ type="solid"
200
+ :class="[
201
+ 'h-5 w-5 transition-transform duration-300',
202
+ toggleDropdown ? 'rotate-180' : '',
203
+ dropdownSelectorIconClass,
204
+ ]"
205
+ ></c-icon>
206
+ </div>
207
+ </div>
208
+ </div>
209
+ <transition
210
+ enter-active-class="transition ease-out duration-100"
211
+ enter-class="transform opacity-0 scale-95"
212
+ enter-to-class="transform opacity-100 scale-100"
213
+ leave-active-class="transition ease-in duration-75"
214
+ leave-class="transform opacity-100 scale-100"
215
+ leave-to-class="transform opacity-0 scale-95"
216
+ >
217
+ <div
218
+ v-if="toggleDropdown && !isDisabled"
219
+ :class="`${getDropdownPosition()} ${
220
+ useSticky ? 'sticky sm:absolute' : 'absolute'
221
+ } z-10 ${
222
+ isFooter && shouldOpenAbove
223
+ ? 'mb-12 max-h-60'
224
+ : !isFooter && shouldOpenAbove
225
+ ? 'mb-1 max-h-60'
226
+ : 'mt-1 max-h-80 pb-20'
227
+ } ${customWidth ? customWidth : 'w-full'}`"
228
+ >
229
+ <ul
230
+ tabindex="-1"
231
+ role="listbox"
232
+ aria-labelledby="listbox-label"
233
+ ref="optionsList"
234
+ :class="`max-h-60 overflow-auto overscroll-contain bg-white py-1 text-sm ring-1 ring-gray-900 ring-opacity-5 focus:outline-none ${getDropdownPosition2()} ${getDropdownShadow()} ${
235
+ isFooter ||
236
+ (showCreateOption &&
237
+ showCreateOptionAfterSearch &&
238
+ selectSearch &&
239
+ selectSearch.trim())
240
+ ? 'rounded-t-md'
241
+ : 'rounded-md'
242
+ }`"
243
+ @mousedown="handlePreventBlur"
244
+ >
245
+ <li
246
+ v-if="addAction"
247
+ @mousedown="actionEvent($event)"
248
+ class="relative flex min-h-[36px] cursor-pointer select-none py-2 pl-3 pr-9 text-indigo-500 hover:bg-indigo-100 hover:text-indigo-700"
249
+ >
250
+ <c-icon
251
+ type="outline"
252
+ class="mr-1 h-5 w-5 text-indigo-400 group-hover:text-indigo-500"
253
+ name="plus"
254
+ ></c-icon>
255
+ {{ addAction.label }}
256
+ </li>
257
+ <li v-if="headerSwitch" class="min-h-[36px] p-2 hover:bg-gray-100">
258
+ <c-switch
259
+ :label="headerSwitch.headerText"
260
+ class="text-sm"
261
+ direction="left"
262
+ size="small"
263
+ :value="headerSwitch.headerSwitchValue ? 1 : 0"
264
+ @returnToggleValue="handleHeaderSwitch"
265
+ :disabled="headerSwitch.headerSwitchDisable"
266
+ ></c-switch>
267
+ </li>
268
+ <hr v-if="headerSwitch" class="my-1" />
269
+ <!-- Select All Option -->
270
+ <template
271
+ v-if="
272
+ (selectCheckboxes || addCheckBox) &&
273
+ (organizedOptions.groups.length > 0 ||
274
+ organizedOptions.selectedValues.length > 0 ||
275
+ organizedOptions.unselectedValues.length > 0)
276
+ "
277
+ >
278
+ <li
279
+ @mousedown="handleSelect($event, allOption)"
280
+ class="relative flex min-h-[36px] cursor-pointer select-none items-center gap-3 py-2 pl-3 pr-9 text-gray-700 hover:bg-gray-100"
281
+ >
282
+ <input
283
+ type="checkbox"
284
+ name="select-all"
285
+ class="pointer-events-none h-4 w-4 cursor-pointer rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 disabled:opacity-50"
286
+ :checked="selectAll"
287
+ />
288
+
289
+ <div class="flex items-center gap-2">
290
+ <c-icon
291
+ v-if="allOption && allOption.icon"
292
+ :class="allOption.icon.class"
293
+ :name="allOption.icon.name"
294
+ :type="allOption.icon.type"
295
+ :viewBox="allOption.icon.viewBox"
296
+ ></c-icon>
297
+ <span class="list-options block break-words font-normal">
298
+ Select all
299
+ </span>
300
+ </div>
301
+ </li>
302
+ <hr class="my-1" />
303
+ </template>
304
+ <!-- Groups Section -->
305
+ <li
306
+ v-for="(option, index) in organizedOptions.groups"
307
+ :key="`group-${option.id || index}`"
308
+ id="listbox-option-group"
309
+ role="option"
310
+ :ref="`option-group-${String(index)}`"
311
+ @mousedown="handleSelect($event, option)"
312
+ :class="option.isDisabled ? 'pointer-events-none' : ''"
313
+ >
314
+ <span
315
+ class="flex min-h-[36px] w-full items-center gap-3 px-3 py-2"
316
+ :class="`group ${
317
+ option.isDisabled ? 'custom-disabled-state' : ''
318
+ } ${optionClasses} relative cursor-pointer select-none text-gray-700 hover:bg-gray-100`"
319
+ >
320
+ <div v-if="addCheckBox" class="flex h-5 items-center">
321
+ <input
322
+ type="checkbox"
323
+ name="group"
324
+ class="pointer-events-none h-4 w-4 cursor-pointer rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 disabled:opacity-50"
325
+ :checked="isChecked(option)"
326
+ />
327
+ </div>
328
+ <div class="flex items-center gap-2">
329
+ <div v-if="option.showImage" class="flex-shrink-0">
330
+ <c-avatar
331
+ v-if="option.photo"
332
+ size="extraextraextrasmall"
333
+ :image="option.photo"
334
+ :rounded="true"
335
+ ></c-avatar>
336
+ <c-avatar
337
+ v-else
338
+ size="extraextraextrasmall"
339
+ :nameInitials="option.initials"
340
+ :rounded="true"
341
+ :isDynamicallyColored="coloredAvatars"
342
+ ></c-avatar>
343
+ </div>
344
+ <div
345
+ class="flex items-center justify-center rounded-full bg-gray-100 p-1"
346
+ v-if="option.showIcon && option.icon"
347
+ >
348
+ <c-icon
349
+ :class="option.icon.class"
350
+ :name="option.icon.name"
351
+ :type="option.icon.type"
352
+ :viewBox="option.icon.viewBox"
353
+ >
354
+ </c-icon>
355
+ </div>
356
+ <span
357
+ :class="[
358
+ option.photo || option.initials ? '' : 'text-left',
359
+ addCheckBox ? 'overflow-hidden' : '',
360
+ ]"
361
+ class="list-options block break-words font-normal"
362
+ v-bind:style="{
363
+ ...(shouldShowCustomFonts
364
+ ? { fontFamily: option[renderOptionName] }
365
+ : {}),
366
+ }"
367
+ >{{ option[renderOptionName] }}
368
+ </span>
369
+ </div>
370
+ </span>
371
+ </li>
372
+ <hr
373
+ v-if="
374
+ organizedOptions.groups.length > 0 &&
375
+ (organizedOptions.selectedValues.length > 0 ||
376
+ organizedOptions.unselectedValues.length > 0)
377
+ "
378
+ class="my-1"
379
+ />
380
+ <!-- Selected values section (non-groups) -->
381
+ <li
382
+ v-for="(option, index) in organizedOptions.selectedValues"
383
+ :key="`selected-value-${option.id || index}`"
384
+ :id="`listbox-option-selected-value-${String(index)}`"
385
+ role="option"
386
+ :ref="`option-selected-value-${String(index)}`"
387
+ @mousedown="handleSelect($event, option)"
388
+ :class="option.isDisabled ? 'pointer-events-none' : ''"
389
+ >
390
+ <span
391
+ class="flex min-h-[36px] w-full items-center gap-3 px-3 py-2"
392
+ :class="`group ${
393
+ option.isDisabled ? 'custom-disabled-state' : ''
394
+ } ${optionClasses} relative cursor-pointer select-none text-gray-700 hover:bg-gray-100`"
395
+ >
396
+ <div v-if="addCheckBox" class="flex h-5 items-center">
397
+ <input
398
+ type="checkbox"
399
+ name="value"
400
+ class="pointer-events-none h-4 w-4 cursor-pointer rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 disabled:opacity-50"
401
+ :checked="preservedOrder ? isChecked(option) : true"
402
+ />
403
+ </div>
404
+ <div class="flex items-center gap-2">
405
+ <div v-if="option.showImage" class="flex-shrink-0">
406
+ <c-avatar
407
+ v-if="option.photo"
408
+ size="extraextraextrasmall"
409
+ :image="option.photo"
410
+ :rounded="true"
411
+ ></c-avatar>
412
+ <c-avatar
413
+ v-else
414
+ size="extraextraextrasmall"
415
+ :nameInitials="option.initials"
416
+ :rounded="true"
417
+ :isDynamicallyColored="coloredAvatars"
418
+ ></c-avatar>
419
+ </div>
420
+ <div
421
+ class="flex items-center justify-center rounded-full bg-gray-100 p-1"
422
+ v-if="option.showIcon && option.icon"
423
+ >
424
+ <c-icon
425
+ :class="option.icon.class"
426
+ :name="option.icon.name"
427
+ :type="option.icon.type"
428
+ :viewBox="option.icon.viewBox"
429
+ >
430
+ </c-icon>
431
+ </div>
432
+ <span
433
+ :class="[
434
+ option.photo || option.initials ? '' : 'text-left',
435
+ addCheckBox ? 'overflow-hidden' : '',
436
+ ]"
437
+ class="list-options block break-words font-normal"
438
+ v-bind:style="{
439
+ ...(shouldShowCustomFonts
440
+ ? { fontFamily: option[renderOptionName] }
441
+ : {}),
442
+ }"
443
+ >{{ option[renderOptionName] }}
444
+ </span>
445
+ </div>
446
+ </span>
447
+ <hr v-if="option.isBorder" class="my-1" />
448
+ </li>
449
+ <hr
450
+ v-if="
451
+ organizedOptions.selectedValues.length > 0 &&
452
+ organizedOptions.unselectedValues.length > 0
453
+ "
454
+ class="my-1"
455
+ />
456
+ <!-- Unselected values section -->
457
+ <li
458
+ v-for="(option, index) in organizedOptions.unselectedValues"
459
+ :key="`unselected-value-${option.id || index}`"
460
+ :id="`listbox-option-unselected-value-${String(index)}`"
461
+ role="option"
462
+ :ref="`option-unselected-value-${String(index)}`"
463
+ @mousedown="handleSelect($event, option)"
464
+ :class="option.isDisabled ? 'pointer-events-none' : ''"
465
+ >
466
+ <span
467
+ class="flex min-h-[36px] w-full items-center gap-3 px-3 py-2"
468
+ :class="`group ${
469
+ option.isDisabled ? 'custom-disabled-state' : ''
470
+ } ${optionClasses} relative cursor-pointer select-none text-gray-700 hover:bg-gray-100`"
471
+ >
472
+ <div v-if="addCheckBox" class="flex h-5 items-center">
473
+ <input
474
+ type="checkbox"
475
+ name="value"
476
+ class="pointer-events-none h-4 w-4 cursor-pointer rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 disabled:opacity-50"
477
+ :checked="selectAll || isChecked(option)"
478
+ />
479
+ </div>
480
+ <div class="flex items-center gap-2">
481
+ <div v-if="option.showImage" class="flex-shrink-0">
482
+ <c-avatar
483
+ v-if="option.photo"
484
+ size="extraextraextrasmall"
485
+ :image="option.photo"
486
+ :rounded="true"
487
+ ></c-avatar>
488
+ <c-avatar
489
+ v-else
490
+ size="extraextraextrasmall"
491
+ :nameInitials="option.initials"
492
+ :rounded="true"
493
+ :isDynamicallyColored="coloredAvatars"
494
+ ></c-avatar>
495
+ </div>
496
+ <div
497
+ class="flex items-center justify-center rounded-full bg-gray-100 p-1"
498
+ v-if="option.showIcon && option.icon"
499
+ >
500
+ <c-icon
501
+ :class="option.icon.class"
502
+ :name="option.icon.name"
503
+ :type="option.icon.type"
504
+ :viewBox="option.icon.viewBox"
505
+ >
506
+ </c-icon>
507
+ </div>
508
+ <span
509
+ :class="[
510
+ option.photo || option.initials ? '' : 'text-left',
511
+ addCheckBox ? 'overflow-hidden' : '',
512
+ ]"
513
+ class="list-options flex items-center gap-2 break-words font-normal"
514
+ v-bind:style="{
515
+ ...(shouldShowCustomFonts
516
+ ? { fontFamily: option[renderOptionName] }
517
+ : {}),
518
+ }"
519
+ >{{ option[renderOptionName] }}
520
+ <c-tag
521
+ v-if="option.badge"
522
+ :label="option.badge.label"
523
+ :color="option.badge.color"
524
+ class="shrink-0"
525
+ ></c-tag>
526
+ </span>
527
+ </div>
528
+ </span>
529
+ <hr v-if="option.isBorder" class="my-1" />
530
+ </li>
531
+ <li
532
+ v-if="
533
+ organizedOptions.groups.length === 0 &&
534
+ organizedOptions.selectedValues.length === 0 &&
535
+ organizedOptions.unselectedValues.length === 0 &&
536
+ selectSearch &&
537
+ selectSearch.length > 0
538
+ "
539
+ class="px-3 py-2 text-sm text-gray-500"
540
+ >
541
+ No options found, try searching something else.
542
+ </li>
543
+ </ul>
544
+ <div
545
+ v-if="isFooter"
546
+ class="group cursor-pointer rounded-b-md bg-gray-50 p-3 ring-1 ring-gray-900 ring-opacity-5"
547
+ @mousedown="handleFooter"
548
+ >
549
+ <p class="text-sm text-indigo-700">
550
+ {{ footerText }}
551
+ </p>
552
+ </div>
553
+ <div
554
+ v-if="
555
+ showCreateOption &&
556
+ showCreateOptionAfterSearch &&
557
+ selectSearch &&
558
+ selectSearch.trim()
559
+ "
560
+ class="sticky bottom-0 z-10 flex min-h-[44px] items-start gap-3 rounded-b-md bg-white px-3 py-2 shadow-lg ring-1 ring-gray-900 ring-opacity-5"
561
+ >
562
+ <span class="word-break flex-1 text-sm text-gray-700">{{
563
+ selectSearch
564
+ }}</span>
565
+
566
+ <button
567
+ type="button"
568
+ :id="id + '_create_button'"
569
+ @mousedown="handleCreateOption($event)"
570
+ class="flex items-center gap-1 text-sm text-indigo-700"
571
+ >
572
+ <c-icon
573
+ type="solid"
574
+ name="plus-circle-solid-v2"
575
+ class="h-5 w-5 text-indigo-600"
576
+ viewBox="0 0 24 24"
577
+ ></c-icon>
578
+ Create
579
+ </button>
580
+ </div>
581
+ </div>
582
+ </transition>
583
+ </div>
584
+ <p
585
+ v-if="helpText && isValidate == true"
586
+ class="mt-2 text-left text-sm text-gray-500"
587
+ >
588
+ {{ helpText }}
589
+ </p>
590
+ <!-- validation error message -->
591
+ <p
592
+ v-if="!isValidate && errorMessage"
593
+ class="mt-2 text-left text-sm text-red-600"
594
+ >
595
+ {{ errorMessage }}
596
+ </p>
597
+ </div>
598
+ </template>
599
+ <script>
600
+ import { isNumber, isEmpty } from "lodash-es";
601
+ import CIcon from "../CIcon/CIcon.vue";
602
+ import { getActionID } from "../../helper";
603
+ import CTag from '../CTag/CTag.vue';
604
+ import CAvatar from '../CAvatar/CAvatar.vue';
605
+ export default {
606
+ name: "CSelect",
607
+ components: {
608
+ CIcon,
609
+ CTag,
610
+ CAvatar,
611
+ },
612
+ props: {
613
+ // id of selectpicker
614
+ id: {
615
+ type: String,
616
+ },
617
+ // label of selectpicker
618
+ label: {
619
+ type: String,
620
+ },
621
+ // action button of selectpicker
622
+ actionBtn: {
623
+ type: String,
624
+ default: null,
625
+ },
626
+ // placeholder in selectpicker
627
+ placeholder: {
628
+ type: String,
629
+ },
630
+ // list to render in dropdown
631
+ options: {
632
+ type: Array,
633
+ required: true,
634
+ },
635
+ // show the avatars with different colors
636
+ coloredAvatars: {
637
+ type: Boolean,
638
+ },
639
+ // text below dropdown to describe more about the field
640
+ helpText: {
641
+ type: String,
642
+ },
643
+ // implement a fixed btn at the bottom of dropdown
644
+ isFooter: {
645
+ type: Boolean,
646
+ },
647
+ // style input field
648
+ inputStyle: {
649
+ type: String,
650
+ },
651
+ // footer btn text
652
+ footerText: {
653
+ type: String,
654
+ },
655
+ // text adjacent to label of dropdown to provide hint about field
656
+ hint: { type: String },
657
+ // to show if image is present along with dropdown option
658
+ showImage: { type: Boolean },
659
+ // whether the select field is mandatory or not
660
+ isRequired: {
661
+ type: Boolean,
662
+ },
663
+ // validation is passed or not
664
+ isValidate: { type: Boolean, default: true },
665
+ // will truncate the text in input field
666
+ selectedOptionStyles: {
667
+ type: String,
668
+ },
669
+ // validation error message
670
+ errorMessage: {
671
+ type: String,
672
+ },
673
+ // perform action on first option in dropdown
674
+ addAction: { type: Object },
675
+ // icons in dropdown list
676
+ icon: { type: Object },
677
+ // value to set as default option in dropdown
678
+ value: { type: [Object, String, Array], default: null },
679
+ // type of dropdown - gray,tertiary, blue or default
680
+ type: {
681
+ type: String,
682
+ },
683
+ // name of the field in array to that contains text to render in dropdown
684
+ renderOptionName: {
685
+ type: String,
686
+ default: "option",
687
+ },
688
+ // whether to disable the selectpicker
689
+ isDisabled: {
690
+ type: Boolean,
691
+ default: false,
692
+ },
693
+ inputClasses: {
694
+ type: String,
695
+ default: "",
696
+ },
697
+ optionClasses: {
698
+ type: String,
699
+ default: "",
700
+ },
701
+ // customised width for the dropdown
702
+ customWidth: {
703
+ type: String,
704
+ default: null,
705
+ },
706
+ customBorderRadius: {
707
+ type: String,
708
+ default: "rounded-md",
709
+ },
710
+ addCheckBox: {
711
+ type: Boolean,
712
+ default: false,
713
+ },
714
+ selectAll: {
715
+ type: Boolean,
716
+ default: false,
717
+ },
718
+ selectCheckboxes: {
719
+ type: Boolean,
720
+ default: false,
721
+ },
722
+ headerSwitch: {
723
+ type: Object,
724
+ default: null,
725
+ },
726
+ useSticky: {
727
+ type: Boolean,
728
+ default: false,
729
+ },
730
+ showCloseButton: {
731
+ type: Boolean,
732
+ default: false,
733
+ },
734
+ showCreateOption: {
735
+ type: Boolean,
736
+ default: false,
737
+ },
738
+ shouldShowCustomFonts: {
739
+ type: Boolean,
740
+ default: false,
741
+ },
742
+ customFontNames: {
743
+ type: Array,
744
+ default: () => [],
745
+ },
746
+ inputContainerClass: {
747
+ type: String,
748
+ default: "",
749
+ },
750
+ },
751
+ data() {
752
+ return {
753
+ toggleDropdown: false,
754
+ selectedValue: null,
755
+ showFocus: false,
756
+ selectSearch: null,
757
+ renderOptions: this.options,
758
+ selectedValuesArray: [],
759
+ isToggle: 0,
760
+ shouldOpenAbove: false,
761
+ // Frozen order while dropdown is open - prevents UI shift on selection
762
+ preservedOrder: null,
763
+ };
764
+ },
765
+ computed: {
766
+ classes() {
767
+ return {
768
+ "text-gray-900 border border-gray-300 hover:bg-gray-200 bg-gray-100 focus:ring-2 focus:border-gray-200 focus:ring-gray-200 focus:ring-offset-2 shadow-sm":
769
+ this.type == "gray",
770
+ "bg-white border border-gray-300 hover:bg-indigo-100 text-indigo-600 hover:text-indigo-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-200":
771
+ this.type == "tertiary",
772
+ "bg-white text-indigo-700 border-none focus:outline-none":
773
+ this.type == "indigo",
774
+ "border border-gray-300 bg-white shadow-sm focus:outline-none focus:border-gray-300 focus:ring-indigo-600 focus:ring-2 focus:ring-offset-2":
775
+ this.type == null,
776
+ };
777
+ },
778
+ allOption() {
779
+ if (
780
+ this.options &&
781
+ this.options.length > 0 &&
782
+ this.options[0] &&
783
+ this.options[0].id === "all"
784
+ ) {
785
+ return this.options[0];
786
+ }
787
+ return null;
788
+ },
789
+ organizedOptions() {
790
+ if (!this.addCheckBox) {
791
+ const filteredOptions = this.renderOptions.filter(
792
+ (option) => option.id !== "all"
793
+ );
794
+ return {
795
+ groups: [],
796
+ selectedValues: [],
797
+ unselectedValues: filteredOptions,
798
+ };
799
+ }
800
+
801
+ // While dropdown is open and not searching, preserve order to prevent UI shift on selection
802
+ if (
803
+ this.preservedOrder &&
804
+ (!this.selectSearch || this.selectSearch.trim() === "")
805
+ ) {
806
+ return this.preservedOrder;
807
+ }
808
+
809
+ return this.computeOrganizedOptions();
810
+ },
811
+ showCreateOptionAfterSearch() {
812
+ if (!this.showCreateOption) return false;
813
+
814
+ const term = (this.selectSearch || "").trim().toLowerCase();
815
+ if (!term) return false;
816
+
817
+ const hasExactMatch = this.options.some((option) => {
818
+ const label = option[this.renderOptionName];
819
+ return label && String(label).toLowerCase() === term;
820
+ });
821
+
822
+ return !hasExactMatch;
823
+ },
824
+ dropdownSelectorIconClass() {
825
+ return this.type === "indigo" ? "text-indigo-600" : "text-gray-400";
826
+ },
827
+ hasSelectedValue() {
828
+ const val = this.selectedValue;
829
+ if (val === null || val === undefined || val === "") {
830
+ return false;
831
+ }
832
+ if (typeof val === "object") {
833
+ return !isEmpty(val);
834
+ }
835
+ return true;
836
+ },
837
+ selectedValueClass() {
838
+ if (this.hasSelectedValue) {
839
+ if (this.isDisabled) {
840
+ return "text-gray-500";
841
+ }
842
+
843
+ if (this.type === "indigo") {
844
+ return "text-indigo-600";
845
+ }
846
+ return "text-gray-700";
847
+ }
848
+ return "text-gray-500";
849
+ },
850
+ },
851
+ methods: {
852
+ isEmpty,
853
+ getActionIDFn(name) {
854
+ return getActionID(name, this.id);
855
+ },
856
+ checkAvailableSpace() {
857
+ if (!this.$refs.inputContainer) return false;
858
+
859
+ const container = this.$refs.inputContainer;
860
+ const rect = container.getBoundingClientRect();
861
+ const viewportHeight = window.innerHeight;
862
+ const bottomSpace = viewportHeight - rect.bottom;
863
+ const componentHeight = 320;
864
+
865
+ // Check if bottom space is less than 80% the component height if there is no footer
866
+ return (
867
+ bottomSpace <
868
+ (this.isFooter ? componentHeight : (componentHeight * 4) / 5)
869
+ );
870
+ },
871
+ getDropdownPosition() {
872
+ this.shouldOpenAbove = this.checkAvailableSpace();
873
+ return this.shouldOpenAbove ? "bottom-full" : "top-full";
874
+ },
875
+ getDropdownPosition2() {
876
+ return this.shouldOpenAbove ? "bottom-full" : "";
877
+ },
878
+ getDropdownShadow() {
879
+ if (this.shouldOpenAbove) {
880
+ return "shadow-[0_-10px_15px_-3px_rgba(0,0,0,0.1),0_-4px_6px_-4px_rgba(0,0,0,0.1)]";
881
+ } else {
882
+ return "shadow-lg";
883
+ }
884
+ },
885
+ handleCrossClick(event) {
886
+ event.preventDefault();
887
+ this.$emit("hide-single-select-dropdown");
888
+ },
889
+ handleInputClick() {
890
+ this.toggleDropdown = !this.toggleDropdown;
891
+ if (!this.toggleDropdown) {
892
+ this.selectSearch = "";
893
+ this.$nextTick(() => {
894
+ if (this.$refs["c-select-input"]) {
895
+ this.$refs["c-select-input"].focus();
896
+ }
897
+ });
898
+ }
899
+ },
900
+ handleInputFocus() {
901
+ if (this.type == "tertiary") {
902
+ this.showFocus = true;
903
+ }
904
+ },
905
+ handleSelect(event, option) {
906
+ this.selectSearch = null;
907
+ this.selectedValue = option[this.renderOptionName];
908
+ if (this.addCheckBox) {
909
+ // Freeze current display order BEFORE updating selection to prevent UI shift
910
+ const currentDisplay = this.organizedOptions;
911
+ this.preservedOrder = {
912
+ groups: [...currentDisplay.groups],
913
+ selectedValues: [...currentDisplay.selectedValues],
914
+ unselectedValues: [...currentDisplay.unselectedValues],
915
+ };
916
+ if (this.getIndex(option) == -1) {
917
+ this.selectedValuesArray = [...this.selectedValuesArray, option];
918
+ } else {
919
+ this.selectedValuesArray = this.selectedValuesArray.filter(
920
+ (item) => option.id !== item.id
921
+ );
922
+ }
923
+ this.$emit("onChangeChecked", this.selectedValuesArray, option);
924
+ }
925
+
926
+ this.$emit("onChangeValue", option);
927
+ if (!this.addCheckBox) {
928
+ this.toggleDropdown = false;
929
+ }
930
+ this.type === "tertiary" ? (this.showFocus = false) : "";
931
+ this.renderOptions = this.options;
932
+ },
933
+ handleFooter() {
934
+ this.$emit("handleFooter");
935
+ },
936
+ actionEvent(event) {
937
+ this.$emit("listAction", event);
938
+ },
939
+ actionBtnEvent(event) {
940
+ this.$emit("listAction", event);
941
+ },
942
+ handleElementBlur() {
943
+ this.toggleDropdown = false;
944
+ if (this.addCheckBox) {
945
+ this.$emit("onClickOutside", this.selectedValuesArray);
946
+ }
947
+ this.selectSearch = null;
948
+ this.renderOptions = this.options;
949
+ },
950
+ handleHeaderSwitch(value) {
951
+ this.$emit("handleHeaderSwitch", value);
952
+ },
953
+ //this prevents the blur to be called when the user clicks on the scrollbar
954
+ handlePreventBlur(event) {
955
+ event.preventDefault();
956
+ },
957
+ search() {
958
+ this.selectedValue = null;
959
+ // if the search term is empty space, then set the render options to empty array
960
+ if (
961
+ this.selectSearch !== null &&
962
+ this.selectSearch.length > 0 &&
963
+ this.selectSearch.trim() === ""
964
+ ) {
965
+ this.renderOptions = [];
966
+ this.$emit("search-term", this.selectSearch);
967
+ return;
968
+ }
969
+ if (
970
+ !this.selectSearch ||
971
+ this.selectSearch == "" ||
972
+ this.selectSearch.trim() == ""
973
+ ) {
974
+ this.renderOptions = this.options;
975
+ this.$emit("search-term", this.selectSearch);
976
+ return;
977
+ }
978
+ let options = [...this.options];
979
+ this.renderOptions = options.filter((option) => {
980
+ isNumber(option[this.renderOptionName])
981
+ ? (option[this.renderOptionName] =
982
+ option[this.renderOptionName].toString())
983
+ : "";
984
+ const optionName = String(
985
+ option[this.renderOptionName] || ""
986
+ ).toLowerCase();
987
+ const searchTerm = this.selectSearch.trim().toLowerCase();
988
+ return optionName.includes(searchTerm);
989
+ });
990
+
991
+ if (this.renderOptions.length === 0) {
992
+ this.$emit("search-term", this.selectSearch.trim());
993
+ }
994
+ },
995
+ async scrollOptionIntoView(refName) {
996
+ await this.$nextTick();
997
+ const list = this.$refs.optionsList;
998
+ const optionEl = (this.$refs[refName] || [])[0];
999
+ if (!list || !optionEl) return;
1000
+
1001
+ const top = optionEl.offsetTop;
1002
+ const bottom = top + optionEl.offsetHeight;
1003
+ const viewTop = list.scrollTop;
1004
+ const viewBottom = viewTop + list.clientHeight;
1005
+
1006
+ const target =
1007
+ top < viewTop
1008
+ ? top
1009
+ : bottom > viewBottom
1010
+ ? bottom - list.clientHeight
1011
+ : viewTop;
1012
+
1013
+ if (target !== viewTop) {
1014
+ list.scrollTo({ top: target, behavior: "smooth" });
1015
+ }
1016
+ },
1017
+ getIndex(option) {
1018
+ return this.selectedValuesArray.findIndex((item) => item.id == option.id);
1019
+ },
1020
+ isChecked(option) {
1021
+ return this.selectedValuesArray.findIndex(
1022
+ (item) => item.id == option.id
1023
+ ) == -1
1024
+ ? false
1025
+ : true;
1026
+ },
1027
+ handleCreateOption(event) {
1028
+ event.preventDefault();
1029
+ this.toggleDropdown = false;
1030
+ this.$emit("handleCreateOption", {
1031
+ event,
1032
+ searchText: this.selectSearch.trim(),
1033
+ });
1034
+ this.selectSearch = null;
1035
+ this.renderOptions = this.options;
1036
+
1037
+ const cSelectInputRef = this.$refs["c-select-input"];
1038
+ this.$nextTick(() => {
1039
+ cSelectInputRef && cSelectInputRef.blur();
1040
+ });
1041
+ },
1042
+ handleKeydown(event) {
1043
+ if (event.key === "Enter") {
1044
+ event.preventDefault();
1045
+ event.stopPropagation();
1046
+
1047
+ if (
1048
+ this.showCreateOption &&
1049
+ this.showCreateOptionAfterSearch &&
1050
+ this.selectSearch &&
1051
+ this.selectSearch.trim()
1052
+ ) {
1053
+ this.handleCreateOption(event);
1054
+ }
1055
+ }
1056
+ },
1057
+ async handleLoadFont() {
1058
+ const customFontNames = this.customFontNames;
1059
+ const googleParam = customFontNames
1060
+ .map((f) => `family=${f.replace(/ /g, "+")}`)
1061
+ .join("&");
1062
+
1063
+ const link = document.createElement("link");
1064
+ link.rel = "stylesheet";
1065
+ link.href = `https://fonts.googleapis.com/css2?${googleParam}&display=swap`;
1066
+ link.setAttribute("data-fonts", customFontNames.join(","));
1067
+ document.head.appendChild(link);
1068
+
1069
+ await document.fonts.ready;
1070
+ },
1071
+ isGroup(option) {
1072
+ return option.type && option.type.includes("group");
1073
+ },
1074
+ computeOrganizedOptions() {
1075
+ const selectedIds = new Set(
1076
+ this.selectedValuesArray.map((item) => item.id)
1077
+ );
1078
+
1079
+ const groups = [];
1080
+ const selectedValues = [];
1081
+ const unselectedValues = [];
1082
+
1083
+ // Filter out the "all" option from renderOptions
1084
+ this.renderOptions
1085
+ .filter((option) => option.id !== "all")
1086
+ .forEach((option) => {
1087
+ if (this.isGroup(option)) {
1088
+ // Groups stay in their own section regardless of selection
1089
+ groups.push(option);
1090
+ } else {
1091
+ // Values are separated by selection state
1092
+ if (selectedIds.has(option.id)) {
1093
+ selectedValues.push(option);
1094
+ } else {
1095
+ unselectedValues.push(option);
1096
+ }
1097
+ }
1098
+ });
1099
+
1100
+ // Sort by renderOptionName (guard against undefined a/b or missing renderOptionName)
1101
+ const getSortKey = (item) =>
1102
+ String((item && item[this.renderOptionName]) || "").toLowerCase();
1103
+
1104
+ selectedValues.sort((a, b) => getSortKey(a).localeCompare(getSortKey(b)));
1105
+ unselectedValues.sort((a, b) => getSortKey(a).localeCompare(getSortKey(b)));
1106
+
1107
+ return {
1108
+ groups,
1109
+ selectedValues,
1110
+ unselectedValues,
1111
+ };
1112
+ },
1113
+ },
1114
+ watch: {
1115
+ value() {
1116
+ this.selectedValue =
1117
+ this.value !== null && this.value[this.renderOptionName]
1118
+ ? this.value[this.renderOptionName]
1119
+ : this.value;
1120
+ if (this.addCheckBox) {
1121
+ this.selectedValuesArray = this.value ? this.value : [];
1122
+ }
1123
+ },
1124
+ options() {
1125
+ this.renderOptions = this.options;
1126
+ },
1127
+ toggleDropdown(newValue) {
1128
+ if (newValue) {
1129
+ this.checkAvailableSpace();
1130
+
1131
+ this.preservedOrder = null;
1132
+
1133
+ const index = this.renderOptions.findIndex(
1134
+ (option) => option[this.renderOptionName] === this.selectedValue
1135
+ );
1136
+ if (index !== -1) {
1137
+ const refName = `option-${String(index)}`;
1138
+ this.scrollOptionIntoView(refName);
1139
+ }
1140
+ } else {
1141
+ this.preservedOrder = null;
1142
+ }
1143
+ },
1144
+ },
1145
+ mounted() {
1146
+ this.selectedValue =
1147
+ this.value !== null && this.value[this.renderOptionName]
1148
+ ? this.value[this.renderOptionName]
1149
+ : this.value;
1150
+ if (this.addCheckBox) {
1151
+ this.selectedValuesArray = this.value ? this.value : [];
1152
+ }
1153
+
1154
+ if (this.shouldShowCustomFonts && this.customFontNames.length > 0) {
1155
+ this.handleLoadFont();
1156
+ }
1157
+ },
1158
+ };
1159
+ </script>
1160
+
1161
+ <style scoped>
1162
+ .word-break {
1163
+ word-break: break-word;
1164
+ }
1165
+ </style>