198 lines
6.1 KiB
Vue
198 lines
6.1 KiB
Vue
|
|
<template>
|
||
|
|
<div class="phone-input-wrapper">
|
||
|
|
<v-text-field
|
||
|
|
:model-value="displayValue"
|
||
|
|
@update:model-value="updatePhone"
|
||
|
|
:label="label"
|
||
|
|
:variant="variant"
|
||
|
|
:density="density"
|
||
|
|
:rules="rules"
|
||
|
|
:required="required"
|
||
|
|
:disabled="disabled"
|
||
|
|
:readonly="readonly"
|
||
|
|
:clearable="clearable"
|
||
|
|
:hide-details="hideDetails"
|
||
|
|
:error-messages="errorMessages"
|
||
|
|
>
|
||
|
|
<template #prepend-inner>
|
||
|
|
<v-menu
|
||
|
|
v-model="countryMenu"
|
||
|
|
:close-on-content-click="true"
|
||
|
|
location="bottom"
|
||
|
|
>
|
||
|
|
<template v-slot:activator="{ props }">
|
||
|
|
<v-btn
|
||
|
|
v-bind="props"
|
||
|
|
variant="text"
|
||
|
|
size="small"
|
||
|
|
class="country-selector"
|
||
|
|
:disabled="disabled || readonly"
|
||
|
|
>
|
||
|
|
<span class="country-flag">{{ selectedCountry.flag }}</span>
|
||
|
|
<span class="country-code">{{ selectedCountry.dialCode }}</span>
|
||
|
|
<v-icon end size="small">mdi-menu-down</v-icon>
|
||
|
|
</v-btn>
|
||
|
|
</template>
|
||
|
|
<v-list density="compact" max-height="300">
|
||
|
|
<v-list-item
|
||
|
|
v-for="country in filteredCountries"
|
||
|
|
:key="country.iso2"
|
||
|
|
@click="selectCountry(country)"
|
||
|
|
:title="`${country.flag} ${country.name} (${country.dialCode})`"
|
||
|
|
>
|
||
|
|
</v-list-item>
|
||
|
|
</v-list>
|
||
|
|
</v-menu>
|
||
|
|
</template>
|
||
|
|
</v-text-field>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup lang="ts">
|
||
|
|
import { ref, computed, watch } from 'vue';
|
||
|
|
|
||
|
|
interface Country {
|
||
|
|
name: string;
|
||
|
|
iso2: string;
|
||
|
|
dialCode: string;
|
||
|
|
flag: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface Props {
|
||
|
|
modelValue?: string;
|
||
|
|
label?: string;
|
||
|
|
variant?: 'flat' | 'elevated' | 'tonal' | 'outlined' | 'plain' | 'underlined' | 'solo' | 'solo-inverted' | 'solo-filled';
|
||
|
|
density?: 'default' | 'comfortable' | 'compact';
|
||
|
|
rules?: any[];
|
||
|
|
required?: boolean;
|
||
|
|
disabled?: boolean;
|
||
|
|
readonly?: boolean;
|
||
|
|
clearable?: boolean;
|
||
|
|
hideDetails?: boolean | 'auto';
|
||
|
|
errorMessages?: string | string[];
|
||
|
|
defaultCountry?: string;
|
||
|
|
preferredCountries?: string[];
|
||
|
|
}
|
||
|
|
|
||
|
|
const props = withDefaults(defineProps<Props>(), {
|
||
|
|
label: 'Phone Number',
|
||
|
|
variant: 'outlined',
|
||
|
|
density: 'comfortable',
|
||
|
|
defaultCountry: 'US',
|
||
|
|
preferredCountries: () => ['US', 'FR', 'ES', 'PT', 'GB']
|
||
|
|
});
|
||
|
|
|
||
|
|
const emit = defineEmits<{
|
||
|
|
'update:modelValue': [value: string];
|
||
|
|
}>();
|
||
|
|
|
||
|
|
// Countries data
|
||
|
|
const countries: Country[] = [
|
||
|
|
{ name: 'United States', iso2: 'US', dialCode: '+1', flag: '🇺🇸' },
|
||
|
|
{ name: 'United Kingdom', iso2: 'GB', dialCode: '+44', flag: '🇬🇧' },
|
||
|
|
{ name: 'France', iso2: 'FR', dialCode: '+33', flag: '🇫🇷' },
|
||
|
|
{ name: 'Spain', iso2: 'ES', dialCode: '+34', flag: '🇪🇸' },
|
||
|
|
{ name: 'Portugal', iso2: 'PT', dialCode: '+351', flag: '🇵🇹' },
|
||
|
|
{ name: 'Germany', iso2: 'DE', dialCode: '+49', flag: '🇩🇪' },
|
||
|
|
{ name: 'Italy', iso2: 'IT', dialCode: '+39', flag: '🇮🇹' },
|
||
|
|
{ name: 'Netherlands', iso2: 'NL', dialCode: '+31', flag: '🇳🇱' },
|
||
|
|
{ name: 'Belgium', iso2: 'BE', dialCode: '+32', flag: '🇧🇪' },
|
||
|
|
{ name: 'Switzerland', iso2: 'CH', dialCode: '+41', flag: '🇨🇭' },
|
||
|
|
{ name: 'Austria', iso2: 'AT', dialCode: '+43', flag: '🇦🇹' },
|
||
|
|
{ name: 'Sweden', iso2: 'SE', dialCode: '+46', flag: '🇸🇪' },
|
||
|
|
{ name: 'Norway', iso2: 'NO', dialCode: '+47', flag: '🇳🇴' },
|
||
|
|
{ name: 'Denmark', iso2: 'DK', dialCode: '+45', flag: '🇩🇰' },
|
||
|
|
{ name: 'Finland', iso2: 'FI', dialCode: '+358', flag: '🇫🇮' },
|
||
|
|
{ name: 'Poland', iso2: 'PL', dialCode: '+48', flag: '🇵🇱' },
|
||
|
|
{ name: 'Greece', iso2: 'GR', dialCode: '+30', flag: '🇬🇷' },
|
||
|
|
{ name: 'Ireland', iso2: 'IE', dialCode: '+353', flag: '🇮🇪' },
|
||
|
|
{ name: 'Canada', iso2: 'CA', dialCode: '+1', flag: '🇨🇦' },
|
||
|
|
{ name: 'Australia', iso2: 'AU', dialCode: '+61', flag: '🇦🇺' },
|
||
|
|
{ name: 'New Zealand', iso2: 'NZ', dialCode: '+64', flag: '🇳🇿' },
|
||
|
|
{ name: 'Japan', iso2: 'JP', dialCode: '+81', flag: '🇯🇵' },
|
||
|
|
{ name: 'China', iso2: 'CN', dialCode: '+86', flag: '🇨🇳' },
|
||
|
|
{ name: 'India', iso2: 'IN', dialCode: '+91', flag: '🇮🇳' },
|
||
|
|
{ name: 'Brazil', iso2: 'BR', dialCode: '+55', flag: '🇧🇷' },
|
||
|
|
{ name: 'Mexico', iso2: 'MX', dialCode: '+52', flag: '🇲🇽' },
|
||
|
|
{ name: 'South Africa', iso2: 'ZA', dialCode: '+27', flag: '🇿🇦' },
|
||
|
|
];
|
||
|
|
|
||
|
|
const countryMenu = ref(false);
|
||
|
|
const localNumber = ref('');
|
||
|
|
const selectedCountry = ref<Country>(
|
||
|
|
countries.find(c => c.iso2 === props.defaultCountry) || countries[0]
|
||
|
|
);
|
||
|
|
|
||
|
|
// Sort countries with preferred ones first
|
||
|
|
const filteredCountries = computed(() => {
|
||
|
|
const preferred = countries.filter(c => props.preferredCountries.includes(c.iso2));
|
||
|
|
const others = countries.filter(c => !props.preferredCountries.includes(c.iso2));
|
||
|
|
return [...preferred, ...others];
|
||
|
|
});
|
||
|
|
|
||
|
|
// Display value (local number without country code)
|
||
|
|
const displayValue = computed(() => localNumber.value);
|
||
|
|
|
||
|
|
// Parse initial value
|
||
|
|
watch(() => props.modelValue, (newValue) => {
|
||
|
|
if (newValue) {
|
||
|
|
// Find matching country code
|
||
|
|
const country = countries.find(c => newValue.startsWith(c.dialCode));
|
||
|
|
if (country) {
|
||
|
|
selectedCountry.value = country;
|
||
|
|
localNumber.value = newValue.slice(country.dialCode.length);
|
||
|
|
} else {
|
||
|
|
// Default to current country if no match
|
||
|
|
localNumber.value = newValue;
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
localNumber.value = '';
|
||
|
|
}
|
||
|
|
}, { immediate: true });
|
||
|
|
|
||
|
|
// Update phone number
|
||
|
|
const updatePhone = (value: string) => {
|
||
|
|
localNumber.value = value;
|
||
|
|
if (value) {
|
||
|
|
emit('update:modelValue', selectedCountry.value.dialCode + value);
|
||
|
|
} else {
|
||
|
|
emit('update:modelValue', '');
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Select country
|
||
|
|
const selectCountry = (country: Country) => {
|
||
|
|
selectedCountry.value = country;
|
||
|
|
countryMenu.value = false;
|
||
|
|
if (localNumber.value) {
|
||
|
|
emit('update:modelValue', country.dialCode + localNumber.value);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style scoped>
|
||
|
|
.phone-input-wrapper {
|
||
|
|
width: 100%;
|
||
|
|
}
|
||
|
|
|
||
|
|
.country-selector {
|
||
|
|
padding: 0 8px;
|
||
|
|
min-width: auto;
|
||
|
|
height: auto;
|
||
|
|
}
|
||
|
|
|
||
|
|
.country-flag {
|
||
|
|
font-size: 1.2rem;
|
||
|
|
margin-right: 4px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.country-code {
|
||
|
|
font-size: 0.875rem;
|
||
|
|
margin-right: 4px;
|
||
|
|
}
|
||
|
|
|
||
|
|
:deep(.v-field__prepend-inner) {
|
||
|
|
padding-top: 0 !important;
|
||
|
|
}
|
||
|
|
</style>
|