buefy 0.9.13 → 0.9.14

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 (230) hide show
  1. package/dist/buefy.css +1 -1
  2. package/dist/buefy.esm.js +167 -117
  3. package/dist/buefy.esm.min.js +2 -2
  4. package/dist/buefy.js +168 -116
  5. package/dist/buefy.min.css +1 -1
  6. package/dist/buefy.min.js +2 -2
  7. package/dist/cjs/autocomplete.js +5 -5
  8. package/dist/cjs/breadcrumb.js +1 -1
  9. package/dist/cjs/button.js +3 -3
  10. package/dist/cjs/carousel.js +4 -4
  11. package/dist/cjs/{chunk-c6fbc7b4.js → chunk-02406b6a.js} +2 -2
  12. package/dist/cjs/{chunk-d0f8ea39.js → chunk-0cc2e136.js} +6 -6
  13. package/dist/cjs/{chunk-114191ae.js → chunk-0d901f36.js} +2 -2
  14. package/dist/cjs/{chunk-9e4cf4c5.js → chunk-1061ac68.js} +1 -1
  15. package/dist/cjs/{chunk-2c7de785.js → chunk-1d2f05e0.js} +0 -0
  16. package/dist/cjs/{chunk-30670fac.js → chunk-2a2403f9.js} +78 -62
  17. package/dist/cjs/{chunk-d54e40f6.js → chunk-2cdb1a89.js} +2 -9
  18. package/dist/cjs/{chunk-34949503.js → chunk-4a3a5cf7.js} +1 -1
  19. package/dist/cjs/{chunk-fe2f57ee.js → chunk-4ebee779.js} +2 -2
  20. package/dist/cjs/{chunk-2062216d.js → chunk-5a7e385c.js} +1 -1
  21. package/dist/cjs/{chunk-f5106717.js → chunk-74fb31db.js} +3 -3
  22. package/dist/cjs/{chunk-7da0c017.js → chunk-a267720d.js} +21 -21
  23. package/dist/cjs/{chunk-a11294f9.js → chunk-a268cb3d.js} +2 -2
  24. package/dist/cjs/{chunk-c7b2aa4b.js → chunk-bb7da16a.js} +1 -1
  25. package/dist/cjs/{chunk-2ae50815.js → chunk-bebdaf0b.js} +3 -3
  26. package/dist/cjs/{chunk-9103eeda.js → chunk-c0adb618.js} +1 -1
  27. package/dist/cjs/{chunk-d120e215.js → chunk-d7d30e52.js} +1 -1
  28. package/dist/cjs/{chunk-fefd7b77.js → chunk-ddd15f05.js} +0 -0
  29. package/dist/cjs/{chunk-2911aa4b.js → chunk-e8dc6270.js} +14 -11
  30. package/dist/cjs/{chunk-3cc5d9a6.js → chunk-eb8d954b.js} +0 -0
  31. package/dist/cjs/{chunk-6cb902f8.js → chunk-f536c03f.js} +1 -1
  32. package/dist/cjs/{chunk-61023b09.js → chunk-fbf3566e.js} +2 -2
  33. package/dist/cjs/clockpicker.js +8 -8
  34. package/dist/cjs/config.js +1 -1
  35. package/dist/cjs/datepicker.js +9 -9
  36. package/dist/cjs/datetimepicker.js +11 -11
  37. package/dist/cjs/dialog.js +4 -4
  38. package/dist/cjs/dropdown.js +3 -3
  39. package/dist/cjs/field.js +2 -2
  40. package/dist/cjs/helpers.js +20 -0
  41. package/dist/cjs/icon.js +2 -2
  42. package/dist/cjs/image.js +2 -2
  43. package/dist/cjs/index.js +24 -22
  44. package/dist/cjs/input.js +4 -4
  45. package/dist/cjs/loading.js +2 -2
  46. package/dist/cjs/menu.js +2 -2
  47. package/dist/cjs/message.js +3 -3
  48. package/dist/cjs/modal.js +2 -2
  49. package/dist/cjs/navbar.js +3 -0
  50. package/dist/cjs/notification.js +4 -4
  51. package/dist/cjs/numberinput.js +4 -4
  52. package/dist/cjs/pagination.js +3 -3
  53. package/dist/cjs/progress.js +2 -2
  54. package/dist/cjs/rate.js +2 -2
  55. package/dist/cjs/select.js +4 -4
  56. package/dist/cjs/sidebar.js +1 -1
  57. package/dist/cjs/slider.js +2 -2
  58. package/dist/cjs/snackbar.js +4 -3
  59. package/dist/cjs/steps.js +5 -5
  60. package/dist/cjs/switch.js +1 -1
  61. package/dist/cjs/table.js +24 -22
  62. package/dist/cjs/tabs.js +7 -7
  63. package/dist/cjs/taginput.js +5 -5
  64. package/dist/cjs/timepicker.js +10 -10
  65. package/dist/cjs/toast.js +2 -2
  66. package/dist/cjs/tooltip.js +2 -2
  67. package/dist/cjs/upload.js +21 -7
  68. package/dist/components/autocomplete/index.js +78 -62
  69. package/dist/components/autocomplete/index.min.js +2 -2
  70. package/dist/components/breadcrumb/index.js +2 -2
  71. package/dist/components/breadcrumb/index.min.js +1 -1
  72. package/dist/components/button/index.js +2 -2
  73. package/dist/components/button/index.min.js +2 -2
  74. package/dist/components/carousel/index.js +2 -2
  75. package/dist/components/carousel/index.min.js +2 -2
  76. package/dist/components/checkbox/index.js +1 -1
  77. package/dist/components/checkbox/index.min.js +1 -1
  78. package/dist/components/clockpicker/index.js +22 -22
  79. package/dist/components/clockpicker/index.min.js +2 -2
  80. package/dist/components/collapse/index.js +1 -1
  81. package/dist/components/collapse/index.min.js +1 -1
  82. package/dist/components/datepicker/index.js +29 -26
  83. package/dist/components/datepicker/index.min.js +2 -2
  84. package/dist/components/datetimepicker/index.js +29 -26
  85. package/dist/components/datetimepicker/index.min.js +2 -2
  86. package/dist/components/dialog/index.js +2 -2
  87. package/dist/components/dialog/index.min.js +2 -2
  88. package/dist/components/dropdown/index.js +2 -2
  89. package/dist/components/dropdown/index.min.js +1 -1
  90. package/dist/components/field/index.js +22 -22
  91. package/dist/components/field/index.min.js +1 -1
  92. package/dist/components/icon/index.js +2 -2
  93. package/dist/components/icon/index.min.js +2 -2
  94. package/dist/components/image/index.js +2 -2
  95. package/dist/components/image/index.min.js +2 -2
  96. package/dist/components/input/index.js +2 -2
  97. package/dist/components/input/index.min.js +2 -2
  98. package/dist/components/loading/index.js +1 -1
  99. package/dist/components/loading/index.min.js +1 -1
  100. package/dist/components/menu/index.js +2 -2
  101. package/dist/components/menu/index.min.js +2 -2
  102. package/dist/components/message/index.js +2 -2
  103. package/dist/components/message/index.min.js +2 -2
  104. package/dist/components/modal/index.js +2 -2
  105. package/dist/components/modal/index.min.js +2 -2
  106. package/dist/components/navbar/index.js +4 -1
  107. package/dist/components/navbar/index.min.js +2 -2
  108. package/dist/components/notification/index.js +3 -10
  109. package/dist/components/notification/index.min.js +2 -2
  110. package/dist/components/numberinput/index.js +2 -2
  111. package/dist/components/numberinput/index.min.js +2 -2
  112. package/dist/components/pagination/index.js +2 -2
  113. package/dist/components/pagination/index.min.js +2 -2
  114. package/dist/components/progress/index.js +2 -2
  115. package/dist/components/progress/index.min.js +2 -2
  116. package/dist/components/radio/index.js +1 -1
  117. package/dist/components/radio/index.min.js +1 -1
  118. package/dist/components/rate/index.js +2 -2
  119. package/dist/components/rate/index.min.js +2 -2
  120. package/dist/components/select/index.js +2 -2
  121. package/dist/components/select/index.min.js +2 -2
  122. package/dist/components/sidebar/index.js +2 -2
  123. package/dist/components/sidebar/index.min.js +1 -1
  124. package/dist/components/skeleton/index.js +1 -1
  125. package/dist/components/skeleton/index.min.js +1 -1
  126. package/dist/components/slider/index.js +2 -2
  127. package/dist/components/slider/index.min.js +2 -2
  128. package/dist/components/snackbar/index.js +5 -11
  129. package/dist/components/snackbar/index.min.js +2 -2
  130. package/dist/components/steps/index.js +2 -2
  131. package/dist/components/steps/index.min.js +2 -2
  132. package/dist/components/switch/index.js +2 -2
  133. package/dist/components/switch/index.min.js +1 -1
  134. package/dist/components/table/index.js +36 -16
  135. package/dist/components/table/index.min.js +2 -2
  136. package/dist/components/tabs/index.js +4 -4
  137. package/dist/components/tabs/index.min.js +2 -2
  138. package/dist/components/tag/index.js +1 -1
  139. package/dist/components/tag/index.min.js +1 -1
  140. package/dist/components/taginput/index.js +78 -62
  141. package/dist/components/taginput/index.min.js +2 -2
  142. package/dist/components/timepicker/index.js +22 -22
  143. package/dist/components/timepicker/index.min.js +2 -2
  144. package/dist/components/toast/index.js +3 -10
  145. package/dist/components/toast/index.min.js +2 -2
  146. package/dist/components/tooltip/index.js +2 -2
  147. package/dist/components/tooltip/index.min.js +1 -1
  148. package/dist/components/upload/index.js +21 -7
  149. package/dist/components/upload/index.min.js +2 -2
  150. package/dist/esm/autocomplete.js +6 -6
  151. package/dist/esm/breadcrumb.js +1 -1
  152. package/dist/esm/button.js +4 -4
  153. package/dist/esm/carousel.js +4 -4
  154. package/dist/esm/{chunk-21fc0948.js → chunk-03f0ac1a.js} +6 -6
  155. package/dist/esm/{chunk-b0c0c6b0.js → chunk-0c4e4e90.js} +0 -0
  156. package/dist/esm/{chunk-ae8ab23a.js → chunk-113685dc.js} +3 -3
  157. package/dist/esm/{chunk-71a547bc.js → chunk-1b63211c.js} +1 -1
  158. package/dist/esm/{chunk-d7f92d97.js → chunk-1f41edb4.js} +1 -1
  159. package/dist/esm/{chunk-3773c62d.js → chunk-2229e354.js} +1 -1
  160. package/dist/esm/{chunk-83eb0d37.js → chunk-40f06d9c.js} +2 -2
  161. package/dist/esm/{chunk-75a5af93.js → chunk-590a6902.js} +1 -1
  162. package/dist/esm/{chunk-22e9f916.js → chunk-5f0c3fc4.js} +14 -11
  163. package/dist/esm/{chunk-6019fd7a.js → chunk-79ac4d01.js} +79 -63
  164. package/dist/esm/{chunk-8ed29c41.js → chunk-8cad1844.js} +1 -1
  165. package/dist/esm/{chunk-29ca0df8.js → chunk-91404fa9.js} +0 -0
  166. package/dist/esm/{chunk-b07e3182.js → chunk-97f201e0.js} +21 -21
  167. package/dist/esm/{chunk-c9c58d0c.js → chunk-9e0ae963.js} +0 -0
  168. package/dist/esm/{chunk-ece062a7.js → chunk-a8516afd.js} +2 -2
  169. package/dist/esm/{chunk-4b67a181.js → chunk-b66a83ce.js} +2 -2
  170. package/dist/esm/{chunk-9f7f7441.js → chunk-b99e83bd.js} +1 -1
  171. package/dist/esm/{chunk-18e8b067.js → chunk-bbf5d78a.js} +2 -2
  172. package/dist/esm/{chunk-8d0f95b8.js → chunk-e01e9ef0.js} +3 -3
  173. package/dist/esm/{chunk-799e084d.js → chunk-e7eb83d8.js} +2 -9
  174. package/dist/esm/{chunk-d92f0cd9.js → chunk-ea9bc877.js} +1 -1
  175. package/dist/esm/{chunk-e7c9b2cb.js → chunk-efec59b6.js} +2 -2
  176. package/dist/esm/clockpicker.js +8 -8
  177. package/dist/esm/config.js +1 -1
  178. package/dist/esm/datepicker.js +10 -10
  179. package/dist/esm/datetimepicker.js +11 -11
  180. package/dist/esm/dialog.js +4 -4
  181. package/dist/esm/dropdown.js +4 -4
  182. package/dist/esm/field.js +3 -3
  183. package/dist/esm/helpers.js +19 -1
  184. package/dist/esm/icon.js +3 -3
  185. package/dist/esm/image.js +3 -3
  186. package/dist/esm/index.js +23 -23
  187. package/dist/esm/input.js +5 -5
  188. package/dist/esm/loading.js +3 -3
  189. package/dist/esm/menu.js +2 -2
  190. package/dist/esm/message.js +3 -3
  191. package/dist/esm/modal.js +3 -3
  192. package/dist/esm/navbar.js +3 -0
  193. package/dist/esm/notification.js +4 -4
  194. package/dist/esm/numberinput.js +4 -4
  195. package/dist/esm/pagination.js +4 -4
  196. package/dist/esm/progress.js +2 -2
  197. package/dist/esm/rate.js +2 -2
  198. package/dist/esm/select.js +5 -5
  199. package/dist/esm/sidebar.js +1 -1
  200. package/dist/esm/slider.js +2 -2
  201. package/dist/esm/snackbar.js +4 -3
  202. package/dist/esm/steps.js +5 -5
  203. package/dist/esm/switch.js +1 -1
  204. package/dist/esm/table.js +25 -23
  205. package/dist/esm/tabs.js +7 -7
  206. package/dist/esm/taginput.js +5 -5
  207. package/dist/esm/timepicker.js +11 -11
  208. package/dist/esm/toast.js +2 -2
  209. package/dist/esm/tooltip.js +3 -3
  210. package/dist/esm/upload.js +21 -7
  211. package/dist/vetur/attributes.json +4 -0
  212. package/dist/vetur/tags.json +1 -0
  213. package/package.json +1 -1
  214. package/src/components/autocomplete/Autocomplete.vue +711 -701
  215. package/src/components/breadcrumb/__snapshots__/BreadcrumbItem.spec.js.snap +1 -5
  216. package/src/components/carousel/__snapshots__/CarouselList.spec.js.snap +48 -48
  217. package/src/components/datepicker/DatepickerTableRow.spec.js +26 -0
  218. package/src/components/datepicker/DatepickerTableRow.vue +4 -5
  219. package/src/components/field/Field.vue +271 -271
  220. package/src/components/navbar/NavbarDropdown.vue +4 -0
  221. package/src/components/snackbar/index.js +2 -1
  222. package/src/components/table/Table.spec.js +17 -3
  223. package/src/components/table/Table.vue +8 -9
  224. package/src/components/tabs/Tabs.vue +185 -183
  225. package/src/components/timepicker/__snapshots__/Timepicker.spec.js.snap +18 -47
  226. package/src/components/upload/Upload.vue +19 -7
  227. package/src/utils/NoticeMixin.js +1 -5
  228. package/src/utils/config.js +1 -1
  229. package/src/utils/helpers.js +16 -0
  230. package/types/components.d.ts +5 -0
@@ -1,701 +1,711 @@
1
- <template>
2
- <div class="autocomplete control" :class="{ 'is-expanded': expanded }">
3
- <b-input
4
- v-model="newValue"
5
- ref="input"
6
- :type="type"
7
- :size="size"
8
- :loading="loading"
9
- :rounded="rounded"
10
- :icon="icon"
11
- :icon-right="newIconRight"
12
- :icon-right-clickable="newIconRightClickable"
13
- :icon-pack="iconPack"
14
- :maxlength="maxlength"
15
- :autocomplete="newAutocomplete"
16
- :use-html5-validation="false"
17
- :aria-autocomplete="ariaAutocomplete"
18
- v-bind="$attrs"
19
- @input="onInput"
20
- @focus="focused"
21
- @blur="onBlur"
22
- @keydown.native="keydown"
23
- @keydown.native.up.prevent="keyArrows('up')"
24
- @keydown.native.down.prevent="keyArrows('down')"
25
- @icon-right-click="rightIconClick"
26
- @icon-click="(event) => $emit('icon-click', event)"
27
- />
28
-
29
- <transition name="fade">
30
- <div
31
- class="dropdown-menu"
32
- :class="{ 'is-opened-top': isOpenedTop && !appendToBody }"
33
- :style="style"
34
- v-show="isActive && (!isEmpty || hasEmptySlot || hasHeaderSlot)"
35
- ref="dropdown"
36
- >
37
- <div
38
- class="dropdown-content"
39
- v-show="isActive"
40
- :style="contentStyle">
41
- <div
42
- v-if="hasHeaderSlot"
43
- class="dropdown-item dropdown-header"
44
- role="button"
45
- tabindex="0"
46
- :class="{ 'is-hovered': headerHovered }"
47
- @click="selectHeaderOrFoterByClick($event, 'header')"
48
- >
49
- <slot name="header" />
50
- </div>
51
- <template v-for="(element, groupindex) in computedData">
52
- <div
53
- v-if="element.group"
54
- :key="groupindex + 'group'"
55
- class="dropdown-item">
56
- <slot
57
- v-if="hasGroupSlot"
58
- name="group"
59
- :group="element.group"
60
- :index="groupindex" />
61
- <span class="has-text-weight-bold" v-else>
62
- {{ element.group }}
63
- </span>
64
- </div>
65
- <a
66
- v-for="(option, index) in element.items"
67
- :key="groupindex + ':' + index"
68
- class="dropdown-item"
69
- role="button"
70
- tabindex="0"
71
- :class="{ 'is-hovered': option === hovered }"
72
- @click.stop="setSelected(option, !keepOpen, $event)"
73
- >
74
- <slot
75
- v-if="hasDefaultSlot"
76
- :option="option"
77
- :index="index" />
78
- <span v-else>
79
- {{ getValue(option, true) }}
80
- </span>
81
- </a>
82
- </template>
83
- <div
84
- v-if="isEmpty && hasEmptySlot"
85
- class="dropdown-item is-disabled">
86
- <slot name="empty" />
87
- </div>
88
- <div
89
- v-if="hasFooterSlot"
90
- class="dropdown-item dropdown-footer"
91
- role="button"
92
- tabindex="0"
93
- :class="{ 'is-hovered': footerHovered }"
94
- @click="selectHeaderOrFoterByClick($event, 'footer')"
95
- >
96
- <slot name="footer" />
97
- </div>
98
- </div>
99
- </div>
100
- </transition>
101
- </div>
102
- </template>
103
-
104
- <script>
105
- import {
106
- getValueByPath,
107
- removeElement,
108
- createAbsoluteElement,
109
- isCustomElement,
110
- toCssWidth
111
- } from '../../utils/helpers'
112
- import FormElementMixin from '../../utils/FormElementMixin'
113
- import Input from '../input/Input'
114
-
115
- export default {
116
- name: 'BAutocomplete',
117
- components: {
118
- [Input.name]: Input
119
- },
120
- mixins: [FormElementMixin],
121
- inheritAttrs: false,
122
- props: {
123
- value: [Number, String],
124
- data: {
125
- type: Array,
126
- default: () => []
127
- },
128
- field: {
129
- type: String,
130
- default: 'value'
131
- },
132
- keepFirst: Boolean,
133
- clearOnSelect: Boolean,
134
- openOnFocus: Boolean,
135
- customFormatter: Function,
136
- checkInfiniteScroll: Boolean,
137
- keepOpen: Boolean,
138
- selectOnClickOutside: Boolean,
139
- clearable: Boolean,
140
- maxHeight: [String, Number],
141
- dropdownPosition: {
142
- type: String,
143
- default: 'auto'
144
- },
145
- groupField: String,
146
- groupOptions: String,
147
- iconRight: String,
148
- iconRightClickable: Boolean,
149
- appendToBody: Boolean,
150
- type: {
151
- type: String,
152
- default: 'text'
153
- },
154
- confirmKeys: {
155
- type: Array,
156
- default: () => ['Tab', 'Enter']
157
- },
158
- selectableHeader: Boolean,
159
- selectableFooter: Boolean
160
- },
161
- data() {
162
- return {
163
- selected: null,
164
- hovered: null,
165
- headerHovered: null,
166
- footerHovered: null,
167
- isActive: false,
168
- newValue: this.value,
169
- newAutocomplete: this.autocomplete || 'off',
170
- ariaAutocomplete: this.keepFirst ? 'both' : 'list',
171
- isListInViewportVertically: true,
172
- hasFocus: false,
173
- style: {},
174
- _isAutocomplete: true,
175
- _elementRef: 'input',
176
- _bodyEl: undefined // Used to append to body
177
- }
178
- },
179
- computed: {
180
- computedData() {
181
- if (this.groupField) {
182
- if (this.groupOptions) {
183
- const newData = []
184
- this.data.forEach((option) => {
185
- const group = getValueByPath(option, this.groupField)
186
- const items = getValueByPath(option, this.groupOptions)
187
- newData.push({ group, items })
188
- })
189
- return newData
190
- } else {
191
- const tmp = {}
192
- this.data.forEach((option) => {
193
- const group = getValueByPath(option, this.groupField)
194
- if (!tmp[group]) tmp[group] = []
195
- tmp[group].push(option)
196
- })
197
- const newData = []
198
- Object.keys(tmp).forEach((group) => {
199
- newData.push({ group, items: tmp[group] })
200
- })
201
- return newData
202
- }
203
- }
204
- return [{ items: this.data }]
205
- },
206
- isEmpty() {
207
- if (!this.computedData) return true
208
- return !this.computedData.some((element) => element.items && element.items.length)
209
- },
210
- /**
211
- * White-listed items to not close when clicked.
212
- * Add input, dropdown and all children.
213
- */
214
- whiteList() {
215
- const whiteList = []
216
- whiteList.push(this.$refs.input.$el.querySelector('input'))
217
- whiteList.push(this.$refs.dropdown)
218
- // Add all children from dropdown
219
- if (this.$refs.dropdown !== undefined) {
220
- const children = this.$refs.dropdown.querySelectorAll('*')
221
- for (const child of children) {
222
- whiteList.push(child)
223
- }
224
- }
225
- if (this.$parent.$data._isTaginput) {
226
- // Add taginput container
227
- whiteList.push(this.$parent.$el)
228
- // Add .tag and .delete
229
- const tagInputChildren = this.$parent.$el.querySelectorAll('*')
230
- for (const tagInputChild of tagInputChildren) {
231
- whiteList.push(tagInputChild)
232
- }
233
- }
234
-
235
- return whiteList
236
- },
237
-
238
- /**
239
- * Check if exists default slot
240
- */
241
- hasDefaultSlot() {
242
- return !!this.$scopedSlots.default
243
- },
244
-
245
- /**
246
- * Check if exists group slot
247
- */
248
- hasGroupSlot() {
249
- return !!this.$scopedSlots.group
250
- },
251
-
252
- /**
253
- * Check if exists "empty" slot
254
- */
255
- hasEmptySlot() {
256
- return !!this.$slots.empty
257
- },
258
-
259
- /**
260
- * Check if exists "header" slot
261
- */
262
- hasHeaderSlot() {
263
- return !!this.$slots.header
264
- },
265
-
266
- /**
267
- * Check if exists "footer" slot
268
- */
269
- hasFooterSlot() {
270
- return !!this.$slots.footer
271
- },
272
-
273
- /**
274
- * Apply dropdownPosition property
275
- */
276
- isOpenedTop() {
277
- return (
278
- this.dropdownPosition === 'top' ||
279
- (this.dropdownPosition === 'auto' && !this.isListInViewportVertically)
280
- )
281
- },
282
-
283
- newIconRight() {
284
- if (this.clearable && this.newValue) {
285
- return 'close-circle'
286
- }
287
- return this.iconRight
288
- },
289
-
290
- newIconRightClickable() {
291
- if (this.clearable) {
292
- return true
293
- }
294
- return this.iconRightClickable
295
- },
296
-
297
- contentStyle() {
298
- return {
299
- maxHeight: toCssWidth(this.maxHeight)
300
- }
301
- }
302
- },
303
- watch: {
304
- /**
305
- * When dropdown is toggled, check the visibility to know when
306
- * to open upwards.
307
- */
308
- isActive(active) {
309
- if (this.dropdownPosition === 'auto') {
310
- if (active) {
311
- this.calcDropdownInViewportVertical()
312
- } else {
313
- // Timeout to wait for the animation to finish before recalculating
314
- setTimeout(() => {
315
- this.calcDropdownInViewportVertical()
316
- }, 100)
317
- }
318
- }
319
- },
320
-
321
- /**
322
- * When updating input's value
323
- * 1. Emit changes
324
- * 2. If value isn't the same as selected, set null
325
- * 3. Close dropdown if value is clear or else open it
326
- */
327
- newValue(value) {
328
- this.$emit('input', value)
329
- // Check if selected is invalid
330
- const currentValue = this.getValue(this.selected)
331
- if (currentValue && currentValue !== value) {
332
- this.setSelected(null, false)
333
- }
334
- // Close dropdown if input is clear or else open it
335
- if (this.hasFocus && (!this.openOnFocus || value)) {
336
- this.isActive = !!value
337
- }
338
- },
339
-
340
- /**
341
- * When v-model is changed:
342
- * 1. Update internal value.
343
- * 2. If it's invalid, validate again.
344
- */
345
- value(value) {
346
- this.newValue = value
347
- },
348
-
349
- /**
350
- * Select first option if "keep-first
351
- */
352
- data() {
353
- // Keep first option always pre-selected
354
- if (this.keepFirst) {
355
- this.$nextTick(() => {
356
- if (this.isActive) {
357
- this.selectFirstOption(this.computedData)
358
- } else {
359
- this.setHovered(null)
360
- }
361
- })
362
- }
363
- }
364
- },
365
- methods: {
366
- /**
367
- * Set which option is currently hovered.
368
- */
369
- setHovered(option) {
370
- if (option === undefined) return
371
-
372
- this.hovered = option
373
- },
374
-
375
- /**
376
- * Set which option is currently selected, update v-model,
377
- * update input value and close dropdown.
378
- */
379
- setSelected(option, closeDropdown = true, event = undefined) {
380
- if (option === undefined) return
381
- this.selected = option
382
- this.$emit('select', this.selected, event)
383
- if (this.selected !== null) {
384
- if (this.clearOnSelect) {
385
- const input = this.$refs.input
386
- input.newValue = ''
387
- input.$refs.input.value = ''
388
- } else {
389
- this.newValue = this.getValue(this.selected)
390
- }
391
- this.setHovered(null)
392
- }
393
- closeDropdown && this.$nextTick(() => {
394
- this.isActive = false
395
- })
396
- this.checkValidity()
397
- },
398
-
399
- /**
400
- * Select first option
401
- */
402
- selectFirstOption(computedData) {
403
- this.$nextTick(() => {
404
- const nonEmptyElements = computedData.filter(
405
- (element) => element.items && element.items.length
406
- )
407
- if (nonEmptyElements.length) {
408
- const option = nonEmptyElements[0].items[0]
409
- this.setHovered(option)
410
- } else {
411
- this.setHovered(null)
412
- }
413
- })
414
- },
415
-
416
- keydown(event) {
417
- const { key } = event // cannot destructure preventDefault (https://stackoverflow.com/a/49616808/2774496)
418
- // prevent emit submit event
419
- if (key === 'Enter') event.preventDefault()
420
- // Close dropdown on Tab & no hovered
421
- if (key === 'Escape' || key === 'Tab') {
422
- this.isActive = false
423
- }
424
-
425
- if (this.confirmKeys.indexOf(key) >= 0) {
426
- // If adding by comma, don't add the comma to the input
427
- if (key === ',') event.preventDefault()
428
- // Close dropdown on select by Tab
429
- const closeDropdown = !this.keepOpen || key === 'Tab'
430
- if (this.hovered === null) {
431
- // header and footer uses headerHovered && footerHovered. If header or footer
432
- // was selected then fire event otherwise just return so a value isn't selected
433
- this.checkIfHeaderOrFooterSelected(event, null, closeDropdown)
434
- return
435
- }
436
- this.setSelected(this.hovered, closeDropdown, event)
437
- }
438
- },
439
-
440
- selectHeaderOrFoterByClick(event, origin) {
441
- this.checkIfHeaderOrFooterSelected(event, {origin: origin})
442
- },
443
-
444
- /**
445
- * Check if header or footer was selected.
446
- */
447
- checkIfHeaderOrFooterSelected(event, triggerClick, closeDropdown = true) {
448
- if (this.selectableHeader && (this.headerHovered || (triggerClick && triggerClick.origin === 'header'))) {
449
- this.$emit('select-header', event)
450
- this.headerHovered = false
451
- if (triggerClick) this.setHovered(null)
452
- if (closeDropdown) this.isActive = false
453
- }
454
- if (this.selectableFooter && (this.footerHovered || (triggerClick && triggerClick.origin === 'footer'))) {
455
- this.$emit('select-footer', event)
456
- this.footerHovered = false
457
- if (triggerClick) this.setHovered(null)
458
- if (closeDropdown) this.isActive = false
459
- }
460
- },
461
-
462
- /**
463
- * Close dropdown if clicked outside.
464
- */
465
- clickedOutside(event) {
466
- const target = isCustomElement(this) ? event.composedPath()[0] : event.target
467
- if (!this.hasFocus && this.whiteList.indexOf(target) < 0) {
468
- if (this.keepFirst && this.hovered && this.selectOnClickOutside) {
469
- this.setSelected(this.hovered, true)
470
- } else {
471
- this.isActive = false
472
- }
473
- }
474
- },
475
-
476
- /**
477
- * Return display text for the input.
478
- * If object, get value from path, or else just the value.
479
- */
480
- getValue(option) {
481
- if (option === null) return
482
-
483
- if (typeof this.customFormatter !== 'undefined') {
484
- return this.customFormatter(option)
485
- }
486
- return typeof option === 'object' ? getValueByPath(option, this.field) : option
487
- },
488
-
489
- /**
490
- * Check if the scroll list inside the dropdown
491
- * reached it's end.
492
- */
493
- checkIfReachedTheEndOfScroll(list) {
494
- if (list.clientHeight !== list.scrollHeight &&
495
- list.scrollTop + list.clientHeight >= list.scrollHeight
496
- ) {
497
- this.$emit('infinite-scroll')
498
- }
499
- },
500
-
501
- /**
502
- * Calculate if the dropdown is vertically visible when activated,
503
- * otherwise it is openened upwards.
504
- */
505
- calcDropdownInViewportVertical() {
506
- this.$nextTick(() => {
507
- /**
508
- * this.$refs.dropdown may be undefined
509
- * when Autocomplete is conditional rendered
510
- */
511
- if (this.$refs.dropdown === undefined) return
512
-
513
- const rect = this.$refs.dropdown.getBoundingClientRect()
514
-
515
- this.isListInViewportVertically = rect.top >= 0 &&
516
- rect.bottom <= (window.innerHeight || document.documentElement.clientHeight)
517
- if (this.appendToBody) {
518
- this.updateAppendToBody()
519
- }
520
- })
521
- },
522
-
523
- /**
524
- * Arrows keys listener.
525
- * If dropdown is active, set hovered option, or else just open.
526
- */
527
- keyArrows(direction) {
528
- const sum = direction === 'down' ? 1 : -1
529
- if (this.isActive) {
530
- const data = this.computedData.map(
531
- (d) => d.items).reduce((a, b) => ([...a, ...b]), [])
532
- if (this.hasHeaderSlot && this.selectableHeader) {
533
- data.unshift(undefined)
534
- }
535
- if (this.hasFooterSlot && this.selectableFooter) {
536
- data.push(undefined)
537
- }
538
-
539
- let index
540
- if (this.headerHovered) {
541
- index = 0 + sum
542
- } else if (this.footerHovered) {
543
- index = (data.length - 1) + sum
544
- } else {
545
- index = data.indexOf(this.hovered) + sum
546
- }
547
-
548
- index = index > data.length - 1 ? data.length - 1 : index
549
- index = index < 0 ? 0 : index
550
-
551
- this.footerHovered = false
552
- this.headerHovered = false
553
- this.setHovered(data[index] !== undefined ? data[index] : null)
554
- if (this.hasFooterSlot && this.selectableFooter && index === data.length - 1) {
555
- this.footerHovered = true
556
- }
557
- if (this.hasHeaderSlot && this.selectableHeader && index === 0) {
558
- this.headerHovered = true
559
- }
560
-
561
- const list = this.$refs.dropdown.querySelector('.dropdown-content')
562
- let querySelectorText = 'a.dropdown-item:not(.is-disabled)'
563
- if (this.hasHeaderSlot && this.selectableHeader) {
564
- querySelectorText += ',div.dropdown-header'
565
- }
566
- if (this.hasFooterSlot && this.selectableFooter) {
567
- querySelectorText += ',div.dropdown-footer'
568
- }
569
- const element = list.querySelectorAll(querySelectorText)[index]
570
-
571
- if (!element) return
572
-
573
- const visMin = list.scrollTop
574
- const visMax = list.scrollTop + list.clientHeight - element.clientHeight
575
-
576
- if (element.offsetTop < visMin) {
577
- list.scrollTop = element.offsetTop
578
- } else if (element.offsetTop >= visMax) {
579
- list.scrollTop = element.offsetTop - list.clientHeight + element.clientHeight
580
- }
581
- } else {
582
- this.isActive = true
583
- }
584
- },
585
-
586
- /**
587
- * Focus listener.
588
- * If value is the same as selected, select all text.
589
- */
590
- focused(event) {
591
- if (this.getValue(this.selected) === this.newValue) {
592
- this.$el.querySelector('input').select()
593
- }
594
- if (this.openOnFocus) {
595
- this.isActive = true
596
- if (this.keepFirst) {
597
- // If open on focus, update the hovered
598
- this.selectFirstOption(this.computedData)
599
- }
600
- }
601
- this.hasFocus = true
602
- this.$emit('focus', event)
603
- },
604
-
605
- /**
606
- * Blur listener.
607
- */
608
- onBlur(event) {
609
- this.hasFocus = false
610
- this.$emit('blur', event)
611
- },
612
- onInput() {
613
- const currentValue = this.getValue(this.selected)
614
- if (currentValue && currentValue === this.newValue) return
615
- this.$emit('typing', this.newValue)
616
- this.checkValidity()
617
- },
618
- rightIconClick(event) {
619
- if (this.clearable) {
620
- this.newValue = ''
621
- this.setSelected(null, false)
622
- if (this.openOnFocus) {
623
- this.$refs.input.$el.focus()
624
- }
625
- } else {
626
- this.$emit('icon-right-click', event)
627
- }
628
- },
629
- checkValidity() {
630
- if (this.useHtml5Validation) {
631
- this.$nextTick(() => {
632
- this.checkHtml5Validity()
633
- })
634
- }
635
- },
636
- updateAppendToBody() {
637
- const dropdownMenu = this.$refs.dropdown
638
- const trigger = this.$refs.input.$el
639
- if (dropdownMenu && trigger) {
640
- // update wrapper dropdown
641
- const root = this.$data._bodyEl
642
- root.classList.forEach((item) => root.classList.remove(item))
643
- root.classList.add('autocomplete')
644
- root.classList.add('control')
645
- if (this.expandend) {
646
- root.classList.add('is-expandend')
647
- }
648
- const rect = trigger.getBoundingClientRect()
649
- let top = rect.top + window.scrollY
650
- let left = rect.left + window.scrollX
651
- if (!this.isOpenedTop) {
652
- top += trigger.clientHeight
653
- } else {
654
- top -= dropdownMenu.clientHeight
655
- }
656
- this.style = {
657
- position: 'absolute',
658
- top: `${top}px`,
659
- left: `${left}px`,
660
- width: `${trigger.clientWidth}px`,
661
- maxWidth: `${trigger.clientWidth}px`,
662
- zIndex: '99'
663
- }
664
- }
665
- }
666
- },
667
- created() {
668
- if (typeof window !== 'undefined') {
669
- document.addEventListener('click', this.clickedOutside)
670
- if (this.dropdownPosition === 'auto') { window.addEventListener('resize', this.calcDropdownInViewportVertical) }
671
- }
672
- },
673
- mounted() {
674
- if (this.checkInfiniteScroll &&
675
- this.$refs.dropdown && this.$refs.dropdown.querySelector('.dropdown-content')
676
- ) {
677
- const list = this.$refs.dropdown.querySelector('.dropdown-content')
678
- list.addEventListener('scroll', () => this.checkIfReachedTheEndOfScroll(list))
679
- }
680
- if (this.appendToBody) {
681
- this.$data._bodyEl = createAbsoluteElement(this.$refs.dropdown)
682
- this.updateAppendToBody()
683
- }
684
- },
685
- beforeDestroy() {
686
- if (typeof window !== 'undefined') {
687
- document.removeEventListener('click', this.clickedOutside)
688
- if (this.dropdownPosition === 'auto') { window.removeEventListener('resize', this.calcDropdownInViewportVertical) }
689
- }
690
- if (this.checkInfiniteScroll &&
691
- this.$refs.dropdown && this.$refs.dropdown.querySelector('.dropdown-content')
692
- ) {
693
- const list = this.$refs.dropdown.querySelector('.dropdown-content')
694
- list.removeEventListener('scroll', this.checkIfReachedTheEndOfScroll)
695
- }
696
- if (this.appendToBody) {
697
- removeElement(this.$data._bodyEl)
698
- }
699
- }
700
- }
701
- </script>
1
+ <template>
2
+ <div class="autocomplete control" :class="{ 'is-expanded': expanded }">
3
+ <b-input
4
+ v-model="newValue"
5
+ ref="input"
6
+ :type="type"
7
+ :size="size"
8
+ :loading="loading"
9
+ :rounded="rounded"
10
+ :icon="icon"
11
+ :icon-right="newIconRight"
12
+ :icon-right-clickable="newIconRightClickable"
13
+ :icon-pack="iconPack"
14
+ :maxlength="maxlength"
15
+ :autocomplete="newAutocomplete"
16
+ :use-html5-validation="false"
17
+ :aria-autocomplete="ariaAutocomplete"
18
+ v-bind="$attrs"
19
+ @input="onInput"
20
+ @focus="focused"
21
+ @blur="onBlur"
22
+ @keydown.native="keydown"
23
+ @keydown.native.up.prevent="keyArrows('up')"
24
+ @keydown.native.down.prevent="keyArrows('down')"
25
+ @icon-right-click="rightIconClick"
26
+ @icon-click="(event) => $emit('icon-click', event)"
27
+ />
28
+
29
+ <transition name="fade">
30
+ <div
31
+ class="dropdown-menu"
32
+ :class="{ 'is-opened-top': isOpenedTop && !appendToBody }"
33
+ :style="style"
34
+ v-show="isActive && (!isEmpty || hasEmptySlot || hasHeaderSlot)"
35
+ ref="dropdown"
36
+ >
37
+ <div
38
+ class="dropdown-content"
39
+ v-show="isActive"
40
+ :style="contentStyle">
41
+ <div
42
+ v-if="hasHeaderSlot"
43
+ class="dropdown-item dropdown-header"
44
+ role="button"
45
+ tabindex="0"
46
+ :class="{ 'is-hovered': headerHovered }"
47
+ @click="selectHeaderOrFoterByClick($event, 'header')"
48
+ >
49
+ <slot name="header" />
50
+ </div>
51
+ <template v-for="(element, groupindex) in computedData">
52
+ <div
53
+ v-if="element.group"
54
+ :key="groupindex + 'group'"
55
+ class="dropdown-item">
56
+ <slot
57
+ v-if="hasGroupSlot"
58
+ name="group"
59
+ :group="element.group"
60
+ :index="groupindex" />
61
+ <span class="has-text-weight-bold" v-else>
62
+ {{ element.group }}
63
+ </span>
64
+ </div>
65
+ <a
66
+ v-for="(option, index) in element.items"
67
+ :key="groupindex + ':' + index"
68
+ class="dropdown-item"
69
+ role="button"
70
+ tabindex="0"
71
+ :class="{ 'is-hovered': option === hovered }"
72
+ @click.stop="setSelected(option, !keepOpen, $event)"
73
+ >
74
+ <slot
75
+ v-if="hasDefaultSlot"
76
+ :option="option"
77
+ :index="index" />
78
+ <span v-else>
79
+ {{ getValue(option, true) }}
80
+ </span>
81
+ </a>
82
+ </template>
83
+ <div
84
+ v-if="isEmpty && hasEmptySlot"
85
+ class="dropdown-item is-disabled">
86
+ <slot name="empty" />
87
+ </div>
88
+ <div
89
+ v-if="hasFooterSlot"
90
+ class="dropdown-item dropdown-footer"
91
+ role="button"
92
+ tabindex="0"
93
+ :class="{ 'is-hovered': footerHovered }"
94
+ @click="selectHeaderOrFoterByClick($event, 'footer')"
95
+ >
96
+ <slot name="footer" />
97
+ </div>
98
+ </div>
99
+ </div>
100
+ </transition>
101
+ </div>
102
+ </template>
103
+
104
+ <script>
105
+ import {
106
+ getValueByPath,
107
+ removeElement,
108
+ createAbsoluteElement,
109
+ isCustomElement,
110
+ toCssWidth
111
+ } from '../../utils/helpers'
112
+ import FormElementMixin from '../../utils/FormElementMixin'
113
+ import Input from '../input/Input'
114
+
115
+ export default {
116
+ name: 'BAutocomplete',
117
+ components: {
118
+ [Input.name]: Input
119
+ },
120
+ mixins: [FormElementMixin],
121
+ inheritAttrs: false,
122
+ props: {
123
+ value: [Number, String],
124
+ data: {
125
+ type: Array,
126
+ default: () => []
127
+ },
128
+ field: {
129
+ type: String,
130
+ default: 'value'
131
+ },
132
+ keepFirst: Boolean,
133
+ clearOnSelect: Boolean,
134
+ openOnFocus: Boolean,
135
+ customFormatter: Function,
136
+ checkInfiniteScroll: Boolean,
137
+ keepOpen: Boolean,
138
+ selectOnClickOutside: Boolean,
139
+ clearable: Boolean,
140
+ maxHeight: [String, Number],
141
+ dropdownPosition: {
142
+ type: String,
143
+ default: 'auto'
144
+ },
145
+ groupField: String,
146
+ groupOptions: String,
147
+ iconRight: String,
148
+ iconRightClickable: Boolean,
149
+ appendToBody: Boolean,
150
+ type: {
151
+ type: String,
152
+ default: 'text'
153
+ },
154
+ confirmKeys: {
155
+ type: Array,
156
+ default: () => ['Tab', 'Enter']
157
+ },
158
+ selectableHeader: Boolean,
159
+ selectableFooter: Boolean
160
+ },
161
+ data() {
162
+ return {
163
+ selected: null,
164
+ hovered: null,
165
+ headerHovered: null,
166
+ footerHovered: null,
167
+ isActive: false,
168
+ newValue: this.value,
169
+ newAutocomplete: this.autocomplete || 'off',
170
+ ariaAutocomplete: this.keepFirst ? 'both' : 'list',
171
+ isListInViewportVertically: true,
172
+ hasFocus: false,
173
+ style: {},
174
+ _isAutocomplete: true,
175
+ _elementRef: 'input',
176
+ _bodyEl: undefined // Used to append to body
177
+ }
178
+ },
179
+ computed: {
180
+ computedData() {
181
+ if (this.groupField) {
182
+ if (this.groupOptions) {
183
+ const newData = []
184
+ this.data.forEach((option) => {
185
+ const group = getValueByPath(option, this.groupField)
186
+ const items = getValueByPath(option, this.groupOptions)
187
+ newData.push({ group, items })
188
+ })
189
+ return newData
190
+ } else {
191
+ const tmp = {}
192
+ this.data.forEach((option) => {
193
+ const group = getValueByPath(option, this.groupField)
194
+ if (!tmp[group]) tmp[group] = []
195
+ tmp[group].push(option)
196
+ })
197
+ const newData = []
198
+ Object.keys(tmp).forEach((group) => {
199
+ newData.push({ group, items: tmp[group] })
200
+ })
201
+ return newData
202
+ }
203
+ }
204
+ return [{ items: this.data }]
205
+ },
206
+ isEmpty() {
207
+ if (!this.computedData) return true
208
+ return !this.computedData.some((element) => element.items && element.items.length)
209
+ },
210
+ /**
211
+ * White-listed items to not close when clicked.
212
+ * Add input, dropdown and all children.
213
+ */
214
+ whiteList() {
215
+ const whiteList = []
216
+ whiteList.push(this.$refs.input.$el.querySelector('input'))
217
+ whiteList.push(this.$refs.dropdown)
218
+ // Add all children from dropdown
219
+ if (this.$refs.dropdown !== undefined) {
220
+ const children = this.$refs.dropdown.querySelectorAll('*')
221
+ for (const child of children) {
222
+ whiteList.push(child)
223
+ }
224
+ }
225
+ if (this.$parent.$data._isTaginput) {
226
+ // Add taginput container
227
+ whiteList.push(this.$parent.$el)
228
+ // Add .tag and .delete
229
+ const tagInputChildren = this.$parent.$el.querySelectorAll('*')
230
+ for (const tagInputChild of tagInputChildren) {
231
+ whiteList.push(tagInputChild)
232
+ }
233
+ }
234
+
235
+ return whiteList
236
+ },
237
+
238
+ /**
239
+ * Check if exists default slot
240
+ */
241
+ hasDefaultSlot() {
242
+ return !!this.$scopedSlots.default
243
+ },
244
+
245
+ /**
246
+ * Check if exists group slot
247
+ */
248
+ hasGroupSlot() {
249
+ return !!this.$scopedSlots.group
250
+ },
251
+
252
+ /**
253
+ * Check if exists "empty" slot
254
+ */
255
+ hasEmptySlot() {
256
+ return !!this.$slots.empty
257
+ },
258
+
259
+ /**
260
+ * Check if exists "header" slot
261
+ */
262
+ hasHeaderSlot() {
263
+ return !!this.$slots.header
264
+ },
265
+
266
+ /**
267
+ * Check if exists "footer" slot
268
+ */
269
+ hasFooterSlot() {
270
+ return !!this.$slots.footer
271
+ },
272
+
273
+ /**
274
+ * Apply dropdownPosition property
275
+ */
276
+ isOpenedTop() {
277
+ return (
278
+ this.dropdownPosition === 'top' ||
279
+ (this.dropdownPosition === 'auto' && !this.isListInViewportVertically)
280
+ )
281
+ },
282
+
283
+ newIconRight() {
284
+ if (this.clearable && this.newValue) {
285
+ return 'close-circle'
286
+ }
287
+ return this.iconRight
288
+ },
289
+
290
+ newIconRightClickable() {
291
+ if (this.clearable) {
292
+ return true
293
+ }
294
+ return this.iconRightClickable
295
+ },
296
+
297
+ contentStyle() {
298
+ return {
299
+ maxHeight: toCssWidth(this.maxHeight)
300
+ }
301
+ }
302
+ },
303
+ watch: {
304
+ /**
305
+ * When dropdown is toggled, check the visibility to know when
306
+ * to open upwards.
307
+ */
308
+ isActive(active) {
309
+ if (this.dropdownPosition === 'auto') {
310
+ if (active) {
311
+ this.calcDropdownInViewportVertical()
312
+ } else {
313
+ // Timeout to wait for the animation to finish before recalculating
314
+ setTimeout(() => {
315
+ this.calcDropdownInViewportVertical()
316
+ }, 100)
317
+ }
318
+ }
319
+ },
320
+
321
+ /**
322
+ * When updating input's value
323
+ * 1. Emit changes
324
+ * 2. If value isn't the same as selected, set null
325
+ * 3. Close dropdown if value is clear or else open it
326
+ */
327
+ newValue(value) {
328
+ this.$emit('input', value)
329
+ // Check if selected is invalid
330
+ const currentValue = this.getValue(this.selected)
331
+ if (currentValue && currentValue !== value) {
332
+ this.setSelected(null, false)
333
+ }
334
+ // Close dropdown if input is clear or else open it
335
+ if (this.hasFocus && (!this.openOnFocus || value)) {
336
+ this.isActive = !!value
337
+ }
338
+ },
339
+
340
+ /**
341
+ * When v-model is changed:
342
+ * 1. Update internal value.
343
+ * 2. If it's invalid, validate again.
344
+ */
345
+ value(value) {
346
+ this.newValue = value
347
+ },
348
+
349
+ /**
350
+ * Select first option if "keep-first
351
+ */
352
+ data() {
353
+ // Keep first option always pre-selected
354
+ if (this.keepFirst) {
355
+ this.$nextTick(() => {
356
+ if (this.isActive) {
357
+ this.selectFirstOption(this.computedData)
358
+ } else {
359
+ this.setHovered(null)
360
+ }
361
+ })
362
+ } else {
363
+ if (this.hovered) {
364
+ // reset hovered if list doesn't contain it
365
+ const hoveredValue = this.getValue(this.hovered)
366
+ const data = this.computedData.map((d) => d.items)
367
+ .reduce((a, b) => ([...a, ...b]), [])
368
+ if (!data.some((d) => this.getValue(d) === hoveredValue)) {
369
+ this.setHovered(null)
370
+ }
371
+ }
372
+ }
373
+ }
374
+ },
375
+ methods: {
376
+ /**
377
+ * Set which option is currently hovered.
378
+ */
379
+ setHovered(option) {
380
+ if (option === undefined) return
381
+
382
+ this.hovered = option
383
+ },
384
+
385
+ /**
386
+ * Set which option is currently selected, update v-model,
387
+ * update input value and close dropdown.
388
+ */
389
+ setSelected(option, closeDropdown = true, event = undefined) {
390
+ if (option === undefined) return
391
+ this.selected = option
392
+ this.$emit('select', this.selected, event)
393
+ if (this.selected !== null) {
394
+ if (this.clearOnSelect) {
395
+ const input = this.$refs.input
396
+ input.newValue = ''
397
+ input.$refs.input.value = ''
398
+ } else {
399
+ this.newValue = this.getValue(this.selected)
400
+ }
401
+ this.setHovered(null)
402
+ }
403
+ closeDropdown && this.$nextTick(() => {
404
+ this.isActive = false
405
+ })
406
+ this.checkValidity()
407
+ },
408
+
409
+ /**
410
+ * Select first option
411
+ */
412
+ selectFirstOption(computedData) {
413
+ this.$nextTick(() => {
414
+ const nonEmptyElements = computedData.filter(
415
+ (element) => element.items && element.items.length
416
+ )
417
+ if (nonEmptyElements.length) {
418
+ const option = nonEmptyElements[0].items[0]
419
+ this.setHovered(option)
420
+ } else {
421
+ this.setHovered(null)
422
+ }
423
+ })
424
+ },
425
+
426
+ keydown(event) {
427
+ const { key } = event // cannot destructure preventDefault (https://stackoverflow.com/a/49616808/2774496)
428
+ // prevent emit submit event
429
+ if (key === 'Enter') event.preventDefault()
430
+ // Close dropdown on Tab & no hovered
431
+ if (key === 'Escape' || key === 'Tab') {
432
+ this.isActive = false
433
+ }
434
+
435
+ if (this.confirmKeys.indexOf(key) >= 0) {
436
+ // If adding by comma, don't add the comma to the input
437
+ if (key === ',') event.preventDefault()
438
+ // Close dropdown on select by Tab
439
+ const closeDropdown = !this.keepOpen || key === 'Tab'
440
+ if (this.hovered === null) {
441
+ // header and footer uses headerHovered && footerHovered. If header or footer
442
+ // was selected then fire event otherwise just return so a value isn't selected
443
+ this.checkIfHeaderOrFooterSelected(event, null, closeDropdown)
444
+ return
445
+ }
446
+ this.setSelected(this.hovered, closeDropdown, event)
447
+ }
448
+ },
449
+
450
+ selectHeaderOrFoterByClick(event, origin) {
451
+ this.checkIfHeaderOrFooterSelected(event, {origin: origin})
452
+ },
453
+
454
+ /**
455
+ * Check if header or footer was selected.
456
+ */
457
+ checkIfHeaderOrFooterSelected(event, triggerClick, closeDropdown = true) {
458
+ if (this.selectableHeader && (this.headerHovered || (triggerClick && triggerClick.origin === 'header'))) {
459
+ this.$emit('select-header', event)
460
+ this.headerHovered = false
461
+ if (triggerClick) this.setHovered(null)
462
+ if (closeDropdown) this.isActive = false
463
+ }
464
+ if (this.selectableFooter && (this.footerHovered || (triggerClick && triggerClick.origin === 'footer'))) {
465
+ this.$emit('select-footer', event)
466
+ this.footerHovered = false
467
+ if (triggerClick) this.setHovered(null)
468
+ if (closeDropdown) this.isActive = false
469
+ }
470
+ },
471
+
472
+ /**
473
+ * Close dropdown if clicked outside.
474
+ */
475
+ clickedOutside(event) {
476
+ const target = isCustomElement(this) ? event.composedPath()[0] : event.target
477
+ if (!this.hasFocus && this.whiteList.indexOf(target) < 0) {
478
+ if (this.keepFirst && this.hovered && this.selectOnClickOutside) {
479
+ this.setSelected(this.hovered, true)
480
+ } else {
481
+ this.isActive = false
482
+ }
483
+ }
484
+ },
485
+
486
+ /**
487
+ * Return display text for the input.
488
+ * If object, get value from path, or else just the value.
489
+ */
490
+ getValue(option) {
491
+ if (option === null) return
492
+
493
+ if (typeof this.customFormatter !== 'undefined') {
494
+ return this.customFormatter(option)
495
+ }
496
+ return typeof option === 'object' ? getValueByPath(option, this.field) : option
497
+ },
498
+
499
+ /**
500
+ * Check if the scroll list inside the dropdown
501
+ * reached it's end.
502
+ */
503
+ checkIfReachedTheEndOfScroll(list) {
504
+ if (list.clientHeight !== list.scrollHeight &&
505
+ list.scrollTop + list.clientHeight >= list.scrollHeight
506
+ ) {
507
+ this.$emit('infinite-scroll')
508
+ }
509
+ },
510
+
511
+ /**
512
+ * Calculate if the dropdown is vertically visible when activated,
513
+ * otherwise it is openened upwards.
514
+ */
515
+ calcDropdownInViewportVertical() {
516
+ this.$nextTick(() => {
517
+ /**
518
+ * this.$refs.dropdown may be undefined
519
+ * when Autocomplete is conditional rendered
520
+ */
521
+ if (this.$refs.dropdown === undefined) return
522
+
523
+ const rect = this.$refs.dropdown.getBoundingClientRect()
524
+
525
+ this.isListInViewportVertically = rect.top >= 0 &&
526
+ rect.bottom <= (window.innerHeight || document.documentElement.clientHeight)
527
+ if (this.appendToBody) {
528
+ this.updateAppendToBody()
529
+ }
530
+ })
531
+ },
532
+
533
+ /**
534
+ * Arrows keys listener.
535
+ * If dropdown is active, set hovered option, or else just open.
536
+ */
537
+ keyArrows(direction) {
538
+ const sum = direction === 'down' ? 1 : -1
539
+ if (this.isActive) {
540
+ const data = this.computedData.map(
541
+ (d) => d.items).reduce((a, b) => ([...a, ...b]), [])
542
+ if (this.hasHeaderSlot && this.selectableHeader) {
543
+ data.unshift(undefined)
544
+ }
545
+ if (this.hasFooterSlot && this.selectableFooter) {
546
+ data.push(undefined)
547
+ }
548
+
549
+ let index
550
+ if (this.headerHovered) {
551
+ index = 0 + sum
552
+ } else if (this.footerHovered) {
553
+ index = (data.length - 1) + sum
554
+ } else {
555
+ index = data.indexOf(this.hovered) + sum
556
+ }
557
+
558
+ index = index > data.length - 1 ? data.length - 1 : index
559
+ index = index < 0 ? 0 : index
560
+
561
+ this.footerHovered = false
562
+ this.headerHovered = false
563
+ this.setHovered(data[index] !== undefined ? data[index] : null)
564
+ if (this.hasFooterSlot && this.selectableFooter && index === data.length - 1) {
565
+ this.footerHovered = true
566
+ }
567
+ if (this.hasHeaderSlot && this.selectableHeader && index === 0) {
568
+ this.headerHovered = true
569
+ }
570
+
571
+ const list = this.$refs.dropdown.querySelector('.dropdown-content')
572
+ let querySelectorText = 'a.dropdown-item:not(.is-disabled)'
573
+ if (this.hasHeaderSlot && this.selectableHeader) {
574
+ querySelectorText += ',div.dropdown-header'
575
+ }
576
+ if (this.hasFooterSlot && this.selectableFooter) {
577
+ querySelectorText += ',div.dropdown-footer'
578
+ }
579
+ const element = list.querySelectorAll(querySelectorText)[index]
580
+
581
+ if (!element) return
582
+
583
+ const visMin = list.scrollTop
584
+ const visMax = list.scrollTop + list.clientHeight - element.clientHeight
585
+
586
+ if (element.offsetTop < visMin) {
587
+ list.scrollTop = element.offsetTop
588
+ } else if (element.offsetTop >= visMax) {
589
+ list.scrollTop = element.offsetTop - list.clientHeight + element.clientHeight
590
+ }
591
+ } else {
592
+ this.isActive = true
593
+ }
594
+ },
595
+
596
+ /**
597
+ * Focus listener.
598
+ * If value is the same as selected, select all text.
599
+ */
600
+ focused(event) {
601
+ if (this.getValue(this.selected) === this.newValue) {
602
+ this.$el.querySelector('input').select()
603
+ }
604
+ if (this.openOnFocus) {
605
+ this.isActive = true
606
+ if (this.keepFirst) {
607
+ // If open on focus, update the hovered
608
+ this.selectFirstOption(this.computedData)
609
+ }
610
+ }
611
+ this.hasFocus = true
612
+ this.$emit('focus', event)
613
+ },
614
+
615
+ /**
616
+ * Blur listener.
617
+ */
618
+ onBlur(event) {
619
+ this.hasFocus = false
620
+ this.$emit('blur', event)
621
+ },
622
+ onInput() {
623
+ const currentValue = this.getValue(this.selected)
624
+ if (currentValue && currentValue === this.newValue) return
625
+ this.$emit('typing', this.newValue)
626
+ this.checkValidity()
627
+ },
628
+ rightIconClick(event) {
629
+ if (this.clearable) {
630
+ this.newValue = ''
631
+ this.setSelected(null, false)
632
+ if (this.openOnFocus) {
633
+ this.$refs.input.$el.focus()
634
+ }
635
+ } else {
636
+ this.$emit('icon-right-click', event)
637
+ }
638
+ },
639
+ checkValidity() {
640
+ if (this.useHtml5Validation) {
641
+ this.$nextTick(() => {
642
+ this.checkHtml5Validity()
643
+ })
644
+ }
645
+ },
646
+ updateAppendToBody() {
647
+ const dropdownMenu = this.$refs.dropdown
648
+ const trigger = this.$refs.input.$el
649
+ if (dropdownMenu && trigger) {
650
+ // update wrapper dropdown
651
+ const root = this.$data._bodyEl
652
+ root.classList.forEach((item) => root.classList.remove(item))
653
+ root.classList.add('autocomplete')
654
+ root.classList.add('control')
655
+ if (this.expandend) {
656
+ root.classList.add('is-expandend')
657
+ }
658
+ const rect = trigger.getBoundingClientRect()
659
+ let top = rect.top + window.scrollY
660
+ let left = rect.left + window.scrollX
661
+ if (!this.isOpenedTop) {
662
+ top += trigger.clientHeight
663
+ } else {
664
+ top -= dropdownMenu.clientHeight
665
+ }
666
+ this.style = {
667
+ position: 'absolute',
668
+ top: `${top}px`,
669
+ left: `${left}px`,
670
+ width: `${trigger.clientWidth}px`,
671
+ maxWidth: `${trigger.clientWidth}px`,
672
+ zIndex: '99'
673
+ }
674
+ }
675
+ }
676
+ },
677
+ created() {
678
+ if (typeof window !== 'undefined') {
679
+ document.addEventListener('click', this.clickedOutside)
680
+ if (this.dropdownPosition === 'auto') { window.addEventListener('resize', this.calcDropdownInViewportVertical) }
681
+ }
682
+ },
683
+ mounted() {
684
+ if (this.checkInfiniteScroll &&
685
+ this.$refs.dropdown && this.$refs.dropdown.querySelector('.dropdown-content')
686
+ ) {
687
+ const list = this.$refs.dropdown.querySelector('.dropdown-content')
688
+ list.addEventListener('scroll', () => this.checkIfReachedTheEndOfScroll(list))
689
+ }
690
+ if (this.appendToBody) {
691
+ this.$data._bodyEl = createAbsoluteElement(this.$refs.dropdown)
692
+ this.updateAppendToBody()
693
+ }
694
+ },
695
+ beforeDestroy() {
696
+ if (typeof window !== 'undefined') {
697
+ document.removeEventListener('click', this.clickedOutside)
698
+ if (this.dropdownPosition === 'auto') { window.removeEventListener('resize', this.calcDropdownInViewportVertical) }
699
+ }
700
+ if (this.checkInfiniteScroll &&
701
+ this.$refs.dropdown && this.$refs.dropdown.querySelector('.dropdown-content')
702
+ ) {
703
+ const list = this.$refs.dropdown.querySelector('.dropdown-content')
704
+ list.removeEventListener('scroll', this.checkIfReachedTheEndOfScroll)
705
+ }
706
+ if (this.appendToBody) {
707
+ removeElement(this.$data._bodyEl)
708
+ }
709
+ }
710
+ }
711
+ </script>