monacousa-portal/components/PhoneInputWrapper.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>