PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 2.4.6
Booking for Appointments and Events Calendar – Amelia v2.4.6
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 / _components / input-phone / AmInputPhone.vue
ameliabooking / v3 / src / views / _components / input-phone Last commit date
AmInputPhone.vue 3 weeks ago
AmInputPhone.vue
616 lines
1 <template>
2 <div class="am-input-phone-wrapper">
3 <MazInputPhoneNumber
4 :id="id"
5 v-model="model"
6 v-model:country-code="countryCode"
7 v-bind="forwardedAttrs"
8 class="am-input-phone"
9 :class="inputClasses"
10 :label="label"
11 :preferred-countries="preferredCountries"
12 :ignored-countries="ignoredCountries"
13 :only-countries="onlyCountries"
14 :translations="translations"
15 :list-position="listPosition"
16 :color="color"
17 :size="size"
18 :hide-flags="hideFlags"
19 :disabled="disabled"
20 :required="required"
21 :placeholder="placeholder"
22 :example="example"
23 :search="search"
24 :search-threshold="searchThreshold"
25 :use-browser-locale="useBrowserLocale"
26 :fetch-country="fetchCountry"
27 :hide-country-select="hideCountrySelect"
28 :show-code-in-list="showCodeInList"
29 :custom-countries-list="customCountriesList"
30 :auto-format="autoFormat"
31 :country-locale="countryLocale"
32 :validation-error="validationError"
33 :validation-success="validationSuccess"
34 :success="success"
35 :error="error"
36 :display-country-name="displayCountryName"
37 :block="block"
38 :orientation="orientation"
39 :country-select-attributes="countrySelectAttributes"
40 :phone-input-attributes="resolvedPhoneInputAttributes"
41 :style="cssVars"
42 @country-code="handleCountryCode"
43 @data="handlePhoneData"
44 >
45 <template v-if="$slots['no-results']" #no-results>
46 <span class="am-input-phone-no-results">
47 <slot name="no-results" />
48 </span>
49 </template>
50
51 <template v-if="$slots['selector-flag']" #selector-flag="slotProps">
52 <slot name="selector-flag" v-bind="slotProps" />
53 </template>
54
55 <template v-if="$slots['country-list-flag']" #country-list-flag="slotProps">
56 <slot name="country-list-flag" v-bind="slotProps" />
57 </template>
58 </MazInputPhoneNumber>
59 </div>
60 </template>
61
62 <script setup>
63 import { MazInputPhoneNumber } from 'maz-ui/components'
64 import { computed, inject, ref, useAttrs, onBeforeUnmount, watch } from 'vue'
65 import { useColorTransparency } from '@/assets/js/common/colorManipulation'
66 import { getInternationalPhoneValue } from '@/assets/js/common/phone'
67
68 /**
69 * Component Props
70 */
71 const props = defineProps({
72 modelValue: {
73 type: String,
74 },
75 countryCode: {
76 type: String,
77 default: undefined,
78 },
79 id: {
80 type: String,
81 default: '',
82 },
83 name: {
84 type: String,
85 default: '',
86 },
87 disabled: {
88 type: Boolean,
89 default: false,
90 },
91 placeholder: {
92 type: String,
93 default: 'Enter phone',
94 },
95 label: {
96 type: String,
97 default: undefined,
98 },
99 preferredCountries: {
100 type: Array,
101 default: undefined,
102 },
103 ignoredCountries: {
104 type: Array,
105 default: undefined,
106 },
107 onlyCountries: {
108 type: Array,
109 default: undefined,
110 },
111 translations: {
112 type: Object,
113 default: undefined,
114 },
115 listPosition: {
116 type: String,
117 default: undefined,
118 },
119 color: {
120 type: String,
121 default: undefined,
122 },
123 size: {
124 type: String,
125 default: 'md',
126 },
127 hideFlags: {
128 type: Boolean,
129 default: false,
130 },
131 required: {
132 type: Boolean,
133 default: false,
134 },
135 example: {
136 type: Boolean,
137 default: true,
138 },
139 search: {
140 type: Boolean,
141 default: true,
142 },
143 searchThreshold: {
144 type: Number,
145 default: 0.75,
146 },
147 useBrowserLocale: {
148 type: Boolean,
149 default: true,
150 },
151 fetchCountry: {
152 type: Boolean,
153 default: false,
154 },
155 hideCountrySelect: {
156 type: Boolean,
157 default: false,
158 },
159 showCodeInList: {
160 type: Boolean,
161 default: true,
162 },
163 customCountriesList: {
164 type: Object,
165 default: undefined,
166 },
167 autoFormat: {
168 type: [String, Boolean],
169 default: 'blur',
170 },
171 countryLocale: {
172 type: String,
173 default: undefined,
174 },
175 validationError: {
176 type: Boolean,
177 default: false,
178 },
179 validationSuccess: {
180 type: Boolean,
181 default: true,
182 },
183 success: {
184 type: Boolean,
185 default: false,
186 },
187 error: {
188 type: Boolean,
189 default: false,
190 },
191 displayCountryName: {
192 type: Boolean,
193 default: false,
194 },
195 block: {
196 type: Boolean,
197 default: false,
198 },
199 orientation: {
200 type: String,
201 default: 'row',
202 },
203 countrySelectAttributes: {
204 type: Object,
205 default: undefined,
206 },
207 phoneInputAttributes: {
208 type: Object,
209 default: undefined,
210 },
211 })
212
213 /**
214 * Component Emits
215 * */
216 const emits = defineEmits(['update:modelValue', 'country-code', 'update:countryCode', 'data'])
217
218 const attrs = useAttrs()
219
220 const isRtl = computed(
221 () => typeof document !== 'undefined' && document.documentElement?.dir === 'rtl',
222 )
223
224 /**
225 * Component model
226 */
227 const hasLegacyName = computed(() => typeof props.name === 'string' && props.name.trim() !== '')
228
229 const resolvedPhoneInputAttributes = computed(() => {
230 const hasPhoneInputAttributes = !!props.phoneInputAttributes
231
232 const resolvedAttributes = {
233 autocomplete: 'tel',
234 inputmode: 'tel',
235 ...props.phoneInputAttributes,
236 }
237
238 if (!hasPhoneInputAttributes && !hasLegacyName.value) {
239 resolvedAttributes.name = 'phone'
240 }
241
242 if (hasLegacyName.value && props.phoneInputAttributes?.name === undefined) {
243 resolvedAttributes.name = props.name
244 }
245
246 return resolvedAttributes
247 })
248
249 const forwardedAttrs = computed(() => attrs)
250
251 const inputClasses = computed(() => ({
252 'am-rtl': isRtl.value,
253 }))
254
255 const lastPhoneData = ref(null)
256
257 const model = computed({
258 get: () => props.modelValue,
259 set: (val) => {
260 emits(
261 'update:modelValue',
262 getInternationalPhoneValue(val, lastPhoneData.value, props.countryCode),
263 )
264 },
265 })
266
267 const countryCode = computed({
268 get: () => props.countryCode,
269 set: (val) => {
270 emits('update:countryCode', val)
271 },
272 })
273
274 function handleCountryCode(val) {
275 emits('country-code', val)
276 }
277
278 function handlePhoneData(val) {
279 lastPhoneData.value = val
280
281 emits('data', val)
282
283 const internationalValue = getInternationalPhoneValue(
284 val?.phoneNumber ?? val?.e164 ?? props.modelValue,
285 val,
286 props.countryCode,
287 )
288
289 if (internationalValue !== props.modelValue) {
290 emits('update:modelValue', internationalValue)
291 }
292 }
293
294 const amColors = inject('amColors')
295 const cssVars = computed(() => {
296 return {
297 '--am-c-ph-drop-text-op10': useColorTransparency(amColors.value.colorDropText, 0.1),
298 }
299 })
300
301 const cssDropVars = computed(() => {
302 return {
303 '--am-c-phone-primary': amColors.value.colorPrimary,
304 '--am-c-phone-primary-op01': useColorTransparency(amColors.value.colorPrimary, 0.1),
305 '--am-c-phone-bgr': amColors.value.colorDropBgr,
306 '--am-c-phone-text': amColors.value.colorDropText,
307 '--am-c-phone-text-op-60': useColorTransparency(amColors.value.colorDropText, 0.6),
308 '--am-c-phone-border': amColors.value.colorDropText,
309 '--am-c-phone-hover': useColorTransparency(amColors.value.colorDropText, 0.1),
310 '--am-c-phone-input': useColorTransparency(amColors.value.colorDropText, 0.3),
311 '--am-c-scroll-op30': useColorTransparency(amColors.value.colorPrimary, 0.3),
312 '--am-c-scroll-op10': useColorTransparency(amColors.value.colorPrimary, 0.1),
313 }
314 })
315
316 const instanceDropKeys = new Set()
317
318 function applyDropdownVars(vars) {
319 if (typeof document === 'undefined') {
320 return
321 }
322
323 Object.entries(vars).forEach(([key, value]) => {
324 instanceDropKeys.add(key)
325 document.documentElement.style.setProperty(key, value)
326 })
327 }
328
329 function clearDropdownVars() {
330 if (typeof document === 'undefined') {
331 return
332 }
333
334 instanceDropKeys.forEach((key) => {
335 document.documentElement.style.removeProperty(key)
336 })
337
338 instanceDropKeys.clear()
339 }
340
341 const stopDropVarSync = watch(
342 cssDropVars,
343 (vars) => {
344 instanceDropKeys.clear()
345 Object.keys(vars).forEach((key) => {
346 instanceDropKeys.add(key)
347 })
348
349 applyDropdownVars(vars)
350 },
351 { immediate: true },
352 )
353
354 onBeforeUnmount(() => {
355 stopDropVarSync()
356 clearDropdownVars()
357 })
358 </script>
359
360 <script>
361 export default {
362 inheritAttrs: false,
363 }
364 </script>
365
366 <style lang="scss">
367 @mixin am-phone-input-block {
368 .am-input-phone {
369 width: 100%;
370
371 &-wrapper {
372 width: 100%;
373 }
374
375 // RTL support for v4 component
376 &.am-rtl {
377 direction: rtl;
378
379 &.m-input-phone-number {
380 .m-select-country {
381 .m-input-wrapper.--border:first-child {
382 border-radius: 0 6px 6px 0;
383 padding: 0 12px 0 0;
384 }
385 }
386
387 .m-phone-input {
388 .m-input-wrapper.--border:first-child {
389 border-radius: 6px 0 0 6px;
390 padding: 0 12px 0 0;
391 }
392 }
393 }
394 }
395
396 &.m-input-phone-number {
397 // am - amelia
398 // c - color
399 // ph - phone
400 // inp - input
401 // bgr - background
402 --am-c-ph-inp-bgr: var(--am-c-inp-bgr);
403 --am-c-ph-inp-border: var(--am-c-inp-border);
404 --am-c-ph-inp-text: var(--am-c-inp-text);
405 --am-c-ph-inp-placeholder: var(--am-c-inp-placeholder);
406
407 width: 100%;
408 display: flex;
409 align-items: stretch;
410
411 // Global reset under #amelia-container sets border: 0 on divs; restore Maz field chrome.
412 .m-input-wrapper {
413 &.--border {
414 background-color: var(--am-c-ph-inp-bgr, hsl(0 0% 100%));
415 border: none;
416 box-shadow: 0 0 0 1px var(--am-c-ph-inp-border, hsl(220 13.04% 90.98%));
417 border-radius: 6px;
418 padding: 0;
419 font-size: 15px;
420 line-height: 1.5;
421
422 &:focus-within {
423 --am-c-ph-inp-border: var(--am-c-primary, hsl(220 13.04% 90.98%));
424 }
425
426 // First input (country code) - make it narrower
427 &:first-child {
428 padding: 0 0 0 12px;
429 box-sizing: border-box;
430 }
431
432 &.maz-border-destructive {
433 --am-c-ph-inp-border: var(--am-c-error, #ef4444);
434 }
435 }
436
437 input {
438 width: 100%;
439 border: none;
440 background: transparent;
441 padding: 0;
442 font-size: var(--am-fs-inp, 15px);
443 font-weight: 400;
444 line-height: 24px;
445 color: var(--am-c-ph-inp-text, #333);
446
447 &::placeholder {
448 color: var(--am-c-ph-inp-placeholder, #999);
449 }
450
451 &:focus {
452 outline: none;
453 }
454 }
455
456 button {
457 background: transparent;
458 border: none;
459 cursor: pointer;
460 padding: 0 6px;
461 display: flex;
462 align-items: center;
463 justify-content: center;
464 color: var(--am-c-ph-inp-text, #333);
465
466 svg path {
467 color: var(--am-c-ph-inp-text, #333);
468 }
469
470 &:hover {
471 opacity: 0.7;
472 }
473 }
474
475 &-input {
476 height: 40px;
477 }
478 }
479
480 .m-phone-input {
481 width: 100%;
482 min-width: unset;
483
484 .m-input-wrapper {
485 border-radius: 0 6px 6px 0;
486 }
487 }
488
489 .m-select-country {
490 width: unset !important;
491
492 .m-input-wrapper {
493 border-radius: 6px 0 0 6px;
494
495 &-input {
496 width: 0;
497 height: 0;
498 overflow: hidden;
499 }
500
501 img {
502 width: 20px;
503 height: 20px;
504 border-radius: 50%;
505 }
506 }
507 }
508
509 .m-popover-container,
510 .m-select-list {
511 background-color: hsl(var(--maz-background, 0 0% 100%));
512 }
513 }
514 }
515 }
516
517 .m-popover-panel.--surface {
518 background-color: transparent !important;
519 border-radius: 6px !important;
520 border-color: transparent !important;
521 color: transparent !important;
522 z-index: 100000000 !important;
523
524 .m-select-list {
525 background-color: var(--am-c-phone-bgr, hsl(var(--maz-background, 0 0% 100%)));
526 color: var(--am-c-phone-text, inherit);
527 border-radius: 6px;
528
529 &__scroll-wrapper {
530 scrollbar-width: thin;
531 scrollbar-color: var(--am-c-scroll-op30) var(--am-c-scroll-op10);
532
533 &::-webkit-scrollbar {
534 width: 6px;
535 }
536
537 &::-webkit-scrollbar-thumb {
538 border-radius: 6px;
539 background: var(--am-c-scroll-op30);
540 }
541
542 &::-webkit-scrollbar-track {
543 border-radius: 6px;
544 background: var(--am-c-scroll-op10);
545 }
546 }
547
548 .m-select-list__search-input {
549 .m-input-wrapper {
550 background-color: transparent;
551 border-color: var(--am-c-phone-border, hsl(var(--maz-border, 0 0% 0%)));
552 border-radius: 6px;
553
554 svg path {
555 color: var(--am-c-phone-text-op-60, hsl(var(--maz-text, 0 0% 0%)));
556 }
557
558 &.maz-border-primary {
559 border-color: var(--am-c-phone-primary, hsl(220 13.04% 90.98%));
560 }
561
562 input {
563 color: var(--am-c-phone-text, hsl(var(--maz-text, 0 0% 0%)));
564
565 &::placeholder {
566 color: var(--am-c-phone-text-op-60, hsl(var(--maz-text, 0 0% 0%)));
567 }
568 }
569 }
570 }
571
572 .m-select-list-item {
573 &:hover,
574 &:focus {
575 background-color: var(--am-c-phone-hover, hsl(0 0% 0% / 0.05));
576 }
577
578 &.--is-selected {
579 background-color: var(--am-c-phone-primary-op01, hsl(220 13.04% 90.98% / 0.1));
580 color: var(--am-c-phone-primary, hsl(220 13.04% 90.98%));
581
582 &:focus {
583 outline-color: var(--am-c-phone-primary, hsl(220 13.04% 90.98%));
584 }
585 }
586
587 .m-input-phone-number__country-list-code {
588 color: var(--am-c-phone-text-op-60, hsl(var(--maz-text, 0 0% 0%)));
589 }
590 }
591
592 .am-input-phone-no-results {
593 display: flex;
594 align-items: center;
595 justify-content: center;
596 color: var(--am-c-phone-text, hsl(var(--maz-text, 0 0% 0%)));
597 font-size: 14px;
598 font-weight: 400;
599 line-height: 1.5;
600 padding: 0 12px;
601 margin: 4px 0 8px;
602 }
603 }
604 }
605
606 // Public
607 .amelia-v2-booking #amelia-container {
608 @include am-phone-input-block;
609 }
610
611 // Admin
612 #amelia-app-backend-new {
613 @include am-phone-input-block;
614 }
615 </style>
616