PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 2.4.8
Booking for Appointments and Events Calendar – Amelia v2.4.8
2.4.8 2.4.7 2.4.6 2.4.5 2.4.4 2.4.3 2.4.2 2.4.1 2.4 trunk 1.2.1 1.2.10 1.2.11 1.2.12 1.2.13 1.2.14 1.2.15 1.2.16 1.2.17 1.2.18 1.2.19 1.2.2 1.2.20 1.2.21 1.2.22 1.2.23 1.2.24 1.2.25 1.2.26 1.2.27 1.2.28 1.2.29 1.2.3 1.2.30 1.2.31 1.2.32 1.2.33 1.2.34 1.2.35 1.2.36 1.2.37 1.2.38 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 2.0 2.0.1 2.0.2 2.1 2.1.1 2.1.2 2.1.3 2.2 2.2.1 2.3
ameliabooking / v3 / src / views / public / Parts / DotsPopup.vue
ameliabooking / v3 / src / views / public / Parts Last commit date
Congratulations 1 month ago Payment 3 days ago Skeletons 1 month ago BackLink.vue 1 month ago BookingSkeleton.vue 1 month ago Calendar.vue 3 days ago DotsPopup.vue 1 month ago RecurringSetup.vue 1 month ago
DotsPopup.vue
398 lines
1 <template>
2 <el-popover
3 :visible="editPopVisible"
4 :persistent="false"
5 :show-arrow="false"
6 :width="'auto'"
7 :popper-class="'am-cc__popper'"
8 :popper-style="cssVars"
9 trigger="click"
10 >
11 <template #reference>
12 <AmButton
13 ref="menuTriggerRef"
14 class="am-cc__edit-btn"
15 :class="props.referenceClass"
16 category="secondary"
17 type="text"
18 size="mini"
19 icon="dots-vertical"
20 :icon-only="true"
21 :aria-label="triggerAriaLabel"
22 aria-haspopup="menu"
23 :aria-expanded="editPopVisible ? 'true' : 'false'"
24 @click="toggleMenu"
25 @keydown="onMenuTriggerKeydown"
26 />
27 </template>
28 <div
29 ref="menuRef"
30 v-click-outside="close"
31 class="am-cc__edit"
32 role="menu"
33 @keydown="onMenuKeydown"
34 >
35 <!-- Edit -->
36 <AmButton
37 v-if="props.haveEdit"
38 class="am-cc__edit-item"
39 category="secondary"
40 type="text"
41 prefix="edit"
42 role="menuitem"
43 :aria-label="amLabels.edit"
44 @click="editItem"
45 >
46 <span class="am-cc__edit-text">
47 {{ amLabels.edit }}
48 </span>
49 </AmButton>
50 <!-- /Edit -->
51
52 <!-- View -->
53 <AmButton
54 v-if="props.haveView"
55 class="am-cc__edit-item"
56 category="secondary"
57 type="text"
58 prefix="search"
59 role="menuitem"
60 :aria-label="amLabels.view"
61 @click="editItem"
62 >
63 <span class="am-cc__edit-text">
64 {{ amLabels.view }}
65 </span>
66 </AmButton>
67 <!-- /View -->
68
69 <!-- Delete -->
70 <AmButton
71 v-if="props.haveDelete"
72 class="am-cc__edit-item am-delete"
73 category="secondary"
74 type="text"
75 prefix="clearable"
76 role="menuitem"
77 :aria-label="amLabels.delete"
78 @click="removeItem"
79 >
80 <span class="am-cc__edit-text">
81 {{ amLabels.delete }}
82 </span>
83 </AmButton>
84 <!-- /Delete -->
85
86 <!-- Duplicate -->
87 <AmButton
88 v-if="props.haveDuplicate"
89 class="am-cc__edit-item am-clone"
90 category="secondary"
91 type="text"
92 prefix="clearable"
93 role="menuitem"
94 :aria-label="amLabels.duplicate"
95 @click="duplicateItem"
96 >
97 <span class="am-cc__edit-text">
98 {{ amLabels.duplicate }}
99 </span>
100 </AmButton>
101 <!-- /Duplicate -->
102 </div>
103 </el-popover>
104 </template>
105
106 <script setup>
107 // * Import from Vue
108 import { ref, inject, computed, unref, nextTick } from 'vue'
109
110 // * Dedicated components
111 import { ClickOutside as vClickOutside } from 'element-plus'
112 import { useColorTransparency } from '../../../assets/js/common/colorManipulation'
113 import AmButton from '../../_components/button/AmButton.vue'
114
115 // * Component properties
116 let props = defineProps({
117 index: {
118 type: Number,
119 default: 0,
120 },
121 referenceClass: {
122 type: String,
123 default: '',
124 },
125 haveDuplicate: {
126 type: Boolean,
127 default: false,
128 },
129 haveDelete: {
130 type: Boolean,
131 default: true,
132 },
133 haveEdit: {
134 type: Boolean,
135 default: true,
136 },
137 haveView: {
138 type: Boolean,
139 default: false,
140 },
141 triggerAriaLabel: {
142 type: String,
143 default: '',
144 },
145 })
146
147 // * Components emits
148 let emits = defineEmits(['remove', 'edit', 'duplicate'])
149
150 // * Labels
151 let amLabels = inject('amLabels')
152
153 let menuTriggerRef = ref(null)
154 let menuRef = ref(null)
155 let editPopVisible = ref(false)
156
157 const triggerAriaLabel = computed(() => {
158 const L = unref(amLabels)
159 return props.triggerAriaLabel || L?.actions || L?.edit || ''
160 })
161
162 function toggleMenu(e) {
163 e.stopPropagation()
164 editPopVisible.value = !editPopVisible.value
165 }
166
167 function onMenuTriggerKeydown(e) {
168 const openKeys = ['Enter', ' ', 'Spacebar', 'ArrowDown']
169
170 if (openKeys.includes(e.key)) {
171 e.preventDefault()
172 e.stopPropagation()
173 editPopVisible.value = true
174 focusFirstMenuItem()
175 }
176
177 if (e.key === 'Escape') {
178 e.preventDefault()
179 e.stopPropagation()
180 close(true)
181 }
182 }
183
184 function getVisibleMenuItems(container = null) {
185 const menuRoot = container || menuRef.value
186
187 if (!menuRoot) {
188 return []
189 }
190
191 return Array.from(menuRoot.querySelectorAll('.am-cc__edit-item')).filter(
192 (item) => !item.disabled && item.offsetParent !== null,
193 )
194 }
195
196 function focusFirstMenuItem() {
197 nextTick(() => {
198 const firstItem = getVisibleMenuItems(menuRef.value)[0]
199
200 if (firstItem) {
201 firstItem.focus()
202 }
203 })
204 }
205
206 function focusMenuTrigger() {
207 nextTick(() => {
208 const trigger = menuTriggerRef.value?.$el || menuTriggerRef.value
209
210 if (trigger && typeof trigger.focus === 'function') {
211 trigger.focus()
212 }
213 })
214 }
215
216 function onMenuKeydown(e) {
217 const menuItems = getVisibleMenuItems(e.currentTarget)
218
219 if (!menuItems.length) {
220 return
221 }
222
223 const currentIndex = menuItems.indexOf(document.activeElement)
224
225 if (e.key === 'Escape') {
226 e.preventDefault()
227 e.stopPropagation()
228 close(true)
229 return
230 }
231
232 if (e.key === 'Tab') {
233 close()
234 return
235 }
236
237 if (e.key === 'Home') {
238 e.preventDefault()
239 menuItems[0].focus()
240 return
241 }
242
243 if (e.key === 'End') {
244 e.preventDefault()
245 menuItems[menuItems.length - 1].focus()
246 return
247 }
248
249 if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
250 e.preventDefault()
251
252 const step = e.key === 'ArrowDown' ? 1 : -1
253 const fallbackIndex = e.key === 'ArrowDown' ? 0 : menuItems.length - 1
254 const nextIndex =
255 currentIndex === -1
256 ? fallbackIndex
257 : (currentIndex + step + menuItems.length) % menuItems.length
258
259 menuItems[nextIndex].focus()
260 }
261 }
262
263 function editItem() {
264 emits('edit', props.index)
265
266 close()
267 }
268
269 function removeItem() {
270 emits('remove', props.index)
271
272 close()
273 }
274
275 function duplicateItem() {
276 emits('duplicate', props.index)
277
278 close()
279 }
280
281 function close(focusTrigger = false) {
282 editPopVisible.value = false
283
284 if (focusTrigger) {
285 focusMenuTrigger()
286 }
287 }
288
289 /*************
290 * Customize *
291 *************/
292 // * Fonts
293 let amFonts = inject('amFonts')
294
295 // * Colors block
296 let amColors = inject('amColors')
297
298 let cssVars = computed(() => {
299 return {
300 '--am-c-cc-error': amColors.value.colorError,
301 '--am-c-cc-error-op15': useColorTransparency(amColors.value.colorError, 0.15),
302 '--am-c-cc-bgr': amColors.value.colorMainBgr,
303 '--am-c-cc-text': amColors.value.colorMainText,
304 '--am-c-cc-text-op10': useColorTransparency(amColors.value.colorMainText, 0.1),
305 '--am-c-cc-text-op15': useColorTransparency(amColors.value.colorMainText, 0.15),
306 '--am-c-cc-primary': amColors.value.colorPrimary,
307 '--am-font-family': amFonts.value.fontFamily,
308 }
309 })
310 </script>
311
312 <script>
313 export default {
314 name: 'DotsPopup',
315 }
316 </script>
317
318 <style lang="scss">
319 @mixin am-dots-popup {
320 .am-cc {
321 &__edit-btn {
322 cursor: pointer;
323
324 &.am-button:focus-visible {
325 outline: 2px solid var(--am-c-cc-primary);
326 outline-offset: 2px;
327 }
328 }
329 }
330 }
331
332 @mixin dots-popup-popper {
333 .am-cc {
334 &__edit {
335 display: flex;
336 flex-direction: column;
337 align-items: flex-start;
338
339 &-item {
340 background: transparent;
341 color: var(--am-c-cc-text);
342 padding: 4px;
343 justify-content: flex-start;
344
345 &:hover {
346 background-color: var(--am-c-cc-text-op15);
347 }
348
349 &.am-delete {
350 --am-c-cc-text: var(--am-c-cc-error);
351
352 &:hover {
353 background-color: var(--am-c-cc-error-op15);
354 }
355 }
356
357 span[class^='am-icon']^='am-icon'] {
358 font-size: 24px;
359 color: inherit;
360 margin: 0 4px 0 0;
361 }
362
363 &.am-button {
364 width: 100%;
365 border: none;
366 }
367 }
368
369 &-text {
370 font-size: 14px;
371 line-height: 1.7142857;
372 color: inherit;
373 }
374 }
375 }
376
377 &.am-cc__popper {
378 padding: 6px 4px;
379 background-color: var(--am-c-cc-bgr);
380 border-color: var(--am-c-cc-bgr);
381 box-shadow: 0 2px 12px 0 var(--am-c-cc-text-op10);
382
383 * {
384 font-family: var(--am-font-family), sans-serif;
385 box-sizing: border-box;
386 }
387 }
388 }
389
390 .amelia-v2-booking #amelia-container {
391 @include am-dots-popup;
392 }
393
394 .el-popper.el-popover {
395 @include dots-popup-popper;
396 }
397 </style>
398