423 lines
11 KiB
Vue
423 lines
11 KiB
Vue
<template>
|
|
<div class="premium-phone-wrapper">
|
|
<!-- Label -->
|
|
<v-label v-if="label" class="phone-label">
|
|
{{ label }}
|
|
<span v-if="required" class="text-error ml-1">*</span>
|
|
</v-label>
|
|
|
|
<!-- Vue Tel Input with Professional Styling -->
|
|
<div class="phone-input-container" :class="{
|
|
'phone-input-container--error': hasError,
|
|
'phone-input-container--disabled': disabled
|
|
}">
|
|
<vue-tel-input
|
|
v-model="phoneValue"
|
|
:preferred-countries="preferredCountries"
|
|
:only-countries="onlyCountries.length > 0 ? onlyCountries : undefined"
|
|
:ignored-countries="ignoredCountries"
|
|
:placeholder="placeholder"
|
|
:disabled="disabled"
|
|
:required="required"
|
|
:valid-characters-only="true"
|
|
:auto-default-country="false"
|
|
:default-country="defaultCountry"
|
|
:input-options="inputOptions"
|
|
:dropdown-options="dropdownOptions"
|
|
mode="international"
|
|
@on-input="handlePhoneInput"
|
|
@country-changed="handleCountryChange"
|
|
@validate="handleValidation"
|
|
:wrapper-classes="wrapperClasses"
|
|
:input-classes="inputClasses"
|
|
class="vue-tel-input-styled"
|
|
>
|
|
<!-- Custom arrow icon slot -->
|
|
<template #arrow-icon>
|
|
<v-icon size="16" class="dropdown-arrow">mdi-chevron-down</v-icon>
|
|
</template>
|
|
</vue-tel-input>
|
|
</div>
|
|
|
|
<!-- Error Message -->
|
|
<div v-if="errorMessage" class="error-message">
|
|
<v-icon size="16" color="error" class="mr-1">mdi-alert-circle-outline</v-icon>
|
|
<span class="text-error text-caption">{{ errorMessage }}</span>
|
|
</div>
|
|
|
|
<!-- Help Text -->
|
|
<div v-else-if="helpText" class="help-text">
|
|
<span class="text-medium-emphasis text-caption">{{ helpText }}</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { VueTelInput } from 'vue-tel-input';
|
|
import 'vue-tel-input/vue-tel-input.css';
|
|
|
|
interface Props {
|
|
modelValue?: string;
|
|
label?: string;
|
|
placeholder?: string;
|
|
error?: boolean;
|
|
errorMessage?: string;
|
|
helpText?: string;
|
|
required?: boolean;
|
|
disabled?: boolean;
|
|
defaultCountry?: string;
|
|
onlyCountries?: string[];
|
|
ignoredCountries?: string[];
|
|
}
|
|
|
|
interface PhoneObject {
|
|
number: string;
|
|
isValid: boolean;
|
|
country: {
|
|
name: string;
|
|
iso2: string;
|
|
dialCode: string;
|
|
priority: number;
|
|
areaCodes?: string[];
|
|
};
|
|
}
|
|
|
|
interface Emits {
|
|
(e: 'update:modelValue', value: string): void;
|
|
(e: 'phone-data', data: PhoneObject): void;
|
|
(e: 'country-changed', country: any): void;
|
|
(e: 'validate', data: PhoneObject): void;
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
modelValue: '',
|
|
placeholder: 'Phone number',
|
|
error: false,
|
|
required: false,
|
|
disabled: false,
|
|
defaultCountry: 'MC', // Default to Monaco
|
|
onlyCountries: () => [],
|
|
ignoredCountries: () => []
|
|
});
|
|
|
|
const emit = defineEmits<Emits>();
|
|
|
|
// Monaco and France prioritized countries
|
|
const preferredCountries = ['MC', 'FR', 'US', 'IT', 'CH', 'GB', 'DE', 'ES', 'CA'];
|
|
|
|
// Internal phone value
|
|
const phoneValue = ref(props.modelValue);
|
|
|
|
// Input and dropdown configuration
|
|
const inputOptions = {
|
|
placeholder: props.placeholder,
|
|
required: props.required,
|
|
disabled: props.disabled,
|
|
showDialCode: false,
|
|
tabindex: 0
|
|
};
|
|
|
|
const dropdownOptions = {
|
|
showDialCode: true,
|
|
tabindex: 0,
|
|
disabledDialCode: false
|
|
};
|
|
|
|
// Custom CSS classes
|
|
const wrapperClasses = 'premium-tel-wrapper';
|
|
const inputClasses = 'premium-tel-input';
|
|
|
|
// Computed
|
|
const hasError = computed(() => {
|
|
return props.error || !!props.errorMessage;
|
|
});
|
|
|
|
// Watch for external model value changes
|
|
watch(() => props.modelValue, (newValue) => {
|
|
if (newValue !== phoneValue.value) {
|
|
phoneValue.value = newValue || '';
|
|
}
|
|
});
|
|
|
|
// Watch internal value changes
|
|
watch(phoneValue, (newValue) => {
|
|
emit('update:modelValue', newValue || '');
|
|
});
|
|
|
|
// Event handlers
|
|
const handlePhoneInput = (phoneNumber: string, phoneObject: PhoneObject) => {
|
|
phoneValue.value = phoneNumber;
|
|
emit('update:modelValue', phoneNumber);
|
|
emit('phone-data', phoneObject);
|
|
};
|
|
|
|
const handleCountryChange = (country: any) => {
|
|
emit('country-changed', country);
|
|
};
|
|
|
|
const handleValidation = (phoneObject: PhoneObject) => {
|
|
emit('validate', phoneObject);
|
|
};
|
|
|
|
// Initialize with model value
|
|
onMounted(() => {
|
|
if (props.modelValue) {
|
|
phoneValue.value = props.modelValue;
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.premium-phone-wrapper {
|
|
width: 100%;
|
|
position: relative;
|
|
}
|
|
|
|
.phone-label {
|
|
display: block;
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.phone-input-container {
|
|
position: relative;
|
|
border-radius: 8px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.phone-input-container--error :deep(.vue-tel-input) {
|
|
border-color: rgb(var(--v-theme-error)) !important;
|
|
}
|
|
|
|
.phone-input-container--disabled {
|
|
opacity: 0.6;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* Vue Tel Input Styling */
|
|
.vue-tel-input-styled :deep(.vue-tel-input) {
|
|
border: 2px solid rgba(var(--v-theme-outline), 0.38);
|
|
border-radius: 8px;
|
|
background: rgb(var(--v-theme-surface));
|
|
transition: all 0.2s ease;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.vue-tel-input-styled :deep(.vue-tel-input:hover) {
|
|
border-color: rgba(var(--v-theme-on-surface), 0.6);
|
|
}
|
|
|
|
.vue-tel-input-styled :deep(.vue-tel-input:focus-within) {
|
|
border-color: rgb(var(--v-theme-primary));
|
|
box-shadow: 0 0 0 2px rgba(var(--v-theme-primary), 0.12);
|
|
}
|
|
|
|
/* Country Selection Styling */
|
|
.vue-tel-input-styled :deep(.country-selector) {
|
|
background: linear-gradient(135deg, rgba(var(--v-theme-primary), 0.04) 0%, rgba(var(--v-theme-primary), 0.08) 100%);
|
|
border-right: 1px solid rgba(var(--v-theme-outline), 0.12);
|
|
padding: 14px 12px;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
min-width: 80px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.vue-tel-input-styled :deep(.country-selector:hover) {
|
|
background: linear-gradient(135deg, rgba(var(--v-theme-primary), 0.08) 0%, rgba(var(--v-theme-primary), 0.16) 100%);
|
|
}
|
|
|
|
.vue-tel-input-styled :deep(.country-selector .iti-flag) {
|
|
width: 20px;
|
|
height: 15px;
|
|
border-radius: 2px;
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
|
|
}
|
|
|
|
.vue-tel-input-styled :deep(.country-selector .selection) {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-weight: 600;
|
|
font-size: 0.875rem;
|
|
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
|
}
|
|
|
|
/* Input Field Styling */
|
|
.vue-tel-input-styled :deep(.vue-tel-input input) {
|
|
border: none !important;
|
|
outline: none !important;
|
|
padding: 14px 16px;
|
|
font-size: 1rem;
|
|
font-weight: 500;
|
|
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
|
background: transparent;
|
|
flex: 1;
|
|
font-family: 'Roboto', sans-serif;
|
|
}
|
|
|
|
.vue-tel-input-styled :deep(.vue-tel-input input::placeholder) {
|
|
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
|
font-weight: 400;
|
|
}
|
|
|
|
/* Dropdown Styling - COMPACT AND BEAUTIFUL */
|
|
.vue-tel-input-styled :deep(.country-list) {
|
|
background: rgb(var(--v-theme-surface));
|
|
border: 1px solid rgba(var(--v-theme-outline), 0.24);
|
|
border-radius: 8px;
|
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
|
margin-top: 4px;
|
|
overflow: hidden;
|
|
backdrop-filter: blur(8px);
|
|
max-height: 240px !important; /* COMPACT HEIGHT */
|
|
scrollbar-width: thin;
|
|
scrollbar-color: rgba(var(--v-theme-primary), 0.3) transparent;
|
|
}
|
|
|
|
.vue-tel-input-styled :deep(.country-list::-webkit-scrollbar) {
|
|
width: 6px;
|
|
}
|
|
|
|
.vue-tel-input-styled :deep(.country-list::-webkit-scrollbar-track) {
|
|
background: transparent;
|
|
}
|
|
|
|
.vue-tel-input-styled :deep(.country-list::-webkit-scrollbar-thumb) {
|
|
background: rgba(var(--v-theme-primary), 0.3);
|
|
border-radius: 3px;
|
|
}
|
|
|
|
/* Country List Items - COMPACT AND CLEAN */
|
|
.vue-tel-input-styled :deep(.country-list li) {
|
|
padding: 10px 16px !important; /* COMPACT PADDING */
|
|
cursor: pointer;
|
|
transition: all 0.15s ease;
|
|
border-bottom: 1px solid rgba(var(--v-theme-outline), 0.06);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
font-size: 0.875rem !important; /* SMALLER FONT */
|
|
line-height: 1.2 !important;
|
|
}
|
|
|
|
.vue-tel-input-styled :deep(.country-list li:hover) {
|
|
background: rgba(var(--v-theme-primary), 0.08);
|
|
transform: translateX(2px);
|
|
}
|
|
|
|
.vue-tel-input-styled :deep(.country-list li.highlighted) {
|
|
background: rgba(var(--v-theme-primary), 0.12);
|
|
font-weight: 600;
|
|
color: rgb(var(--v-theme-primary));
|
|
}
|
|
|
|
.vue-tel-input-styled :deep(.country-list li.preferred) {
|
|
background: linear-gradient(90deg, rgba(var(--v-theme-primary), 0.04) 0%, transparent 100%);
|
|
border-left: 3px solid rgb(var(--v-theme-primary));
|
|
font-weight: 600;
|
|
}
|
|
|
|
.vue-tel-input-styled :deep(.country-list li:last-child) {
|
|
border-bottom: none;
|
|
}
|
|
|
|
/* Flag Styling in Dropdown */
|
|
.vue-tel-input-styled :deep(.country-list .iti-flag) {
|
|
width: 20px !important;
|
|
height: 15px !important;
|
|
border-radius: 2px;
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* Country Names and Codes */
|
|
.vue-tel-input-styled :deep(.country-list .country-name) {
|
|
flex: 1;
|
|
font-weight: 500;
|
|
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
|
}
|
|
|
|
.vue-tel-input-styled :deep(.country-list .dial-code) {
|
|
font-weight: 600;
|
|
font-size: 0.8125rem;
|
|
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
|
font-family: 'Roboto Mono', monospace;
|
|
}
|
|
|
|
/* Arrow Icon Styling */
|
|
.dropdown-arrow {
|
|
transition: transform 0.2s ease;
|
|
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
|
}
|
|
|
|
.vue-tel-input-styled :deep(.country-selector.open) .dropdown-arrow {
|
|
transform: rotate(180deg);
|
|
}
|
|
|
|
/* Error Message */
|
|
.error-message {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-top: 8px;
|
|
padding: 6px 12px;
|
|
background: rgba(var(--v-theme-error), 0.08);
|
|
border-radius: 6px;
|
|
border-left: 3px solid rgb(var(--v-theme-error));
|
|
}
|
|
|
|
/* Help Text */
|
|
.help-text {
|
|
margin-top: 6px;
|
|
padding: 0 12px;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
/* Responsive Design */
|
|
@media (max-width: 768px) {
|
|
.vue-tel-input-styled :deep(.country-selector) {
|
|
min-width: 70px;
|
|
padding: 12px 10px;
|
|
}
|
|
|
|
.vue-tel-input-styled :deep(.vue-tel-input input) {
|
|
padding: 12px 14px;
|
|
font-size: 0.9375rem;
|
|
}
|
|
|
|
.vue-tel-input-styled :deep(.country-list) {
|
|
max-height: 200px !important;
|
|
}
|
|
|
|
.vue-tel-input-styled :deep(.country-list li) {
|
|
padding: 8px 14px !important;
|
|
}
|
|
}
|
|
|
|
/* Dark Theme Support */
|
|
@media (prefers-color-scheme: dark) {
|
|
.vue-tel-input-styled :deep(.country-list) {
|
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
|
}
|
|
}
|
|
|
|
/* High Contrast Support */
|
|
@media (prefers-contrast: high) {
|
|
.vue-tel-input-styled :deep(.vue-tel-input) {
|
|
border-width: 3px;
|
|
}
|
|
}
|
|
|
|
/* Monaco and France Priority Visual Enhancement */
|
|
.vue-tel-input-styled :deep(.country-list li[data-country-code="mc"]),
|
|
.vue-tel-input-styled :deep(.country-list li[data-country-code="fr"]) {
|
|
background: linear-gradient(90deg, rgba(var(--v-theme-primary), 0.06) 0%, rgba(var(--v-theme-primary), 0.02) 100%);
|
|
border-left: 2px solid rgb(var(--v-theme-primary));
|
|
font-weight: 600;
|
|
}
|
|
</style>
|