227 lines
5.6 KiB
Vue
227 lines
5.6 KiB
Vue
<template>
|
|
<div class="phone-input-wrapper">
|
|
<v-label v-if="label" class="v-label mb-2">{{ label }}</v-label>
|
|
<div class="phone-input-container" :class="{ 'phone-input-container--error': hasError }">
|
|
<PhoneInput
|
|
v-model="phoneValue"
|
|
@update="handlePhoneUpdate"
|
|
:preferred-countries="['MC', 'FR', 'US', 'IT', 'CH', 'GB']"
|
|
:translations="{
|
|
phoneInput: {
|
|
placeholder: placeholder || 'Phone number'
|
|
}
|
|
}"
|
|
country-locale="en-US"
|
|
:auto-format="true"
|
|
:no-formatting-as-you-type="false"
|
|
class="custom-phone-input"
|
|
/>
|
|
</div>
|
|
<div v-if="errorMessage" class="error-message mt-1">
|
|
<span class="text-error text-caption">{{ errorMessage }}</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import PhoneInput from 'base-vue-phone-input';
|
|
|
|
interface Props {
|
|
modelValue?: string;
|
|
label?: string;
|
|
placeholder?: string;
|
|
error?: boolean;
|
|
errorMessage?: string;
|
|
required?: boolean;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
interface Emits {
|
|
(e: 'update:modelValue', value: string): void;
|
|
(e: 'phone-data', data: any): void;
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
modelValue: '',
|
|
placeholder: 'Phone number',
|
|
error: false,
|
|
required: false,
|
|
disabled: false
|
|
});
|
|
|
|
const emit = defineEmits<Emits>();
|
|
|
|
// Internal phone value
|
|
const phoneValue = ref(props.modelValue || '');
|
|
const phoneData = ref(null);
|
|
|
|
// Watch for external changes
|
|
watch(() => props.modelValue, (newValue) => {
|
|
if (newValue !== phoneValue.value) {
|
|
phoneValue.value = newValue || '';
|
|
}
|
|
});
|
|
|
|
// Handle phone input updates
|
|
const handlePhoneUpdate = (data: any) => {
|
|
phoneData.value = data;
|
|
|
|
// Emit the formatted international number or the raw input
|
|
const formattedPhone = data?.formatInternational || data?.e164 || phoneValue.value;
|
|
|
|
emit('update:modelValue', formattedPhone);
|
|
emit('phone-data', data);
|
|
};
|
|
|
|
// Watch phoneValue changes to emit updates
|
|
watch(phoneValue, (newValue) => {
|
|
if (!phoneData.value) {
|
|
// If no phone data yet, just emit the raw value
|
|
emit('update:modelValue', newValue);
|
|
}
|
|
});
|
|
|
|
const hasError = computed(() => {
|
|
return props.error || !!props.errorMessage;
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.phone-input-wrapper {
|
|
width: 100%;
|
|
}
|
|
|
|
.phone-input-container {
|
|
border: 2px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
|
border-radius: 4px;
|
|
transition: border-color 0.2s ease-in-out;
|
|
background: rgb(var(--v-theme-surface));
|
|
}
|
|
|
|
.phone-input-container:hover {
|
|
border-color: rgba(var(--v-theme-on-surface), 0.87);
|
|
}
|
|
|
|
.phone-input-container:focus-within {
|
|
border-color: rgb(var(--v-theme-primary));
|
|
border-width: 2px;
|
|
}
|
|
|
|
.phone-input-container--error {
|
|
border-color: rgb(var(--v-theme-error)) !important;
|
|
}
|
|
|
|
/* Style the phone input to match Vuetify */
|
|
.phone-input-container :deep(.phone-input) {
|
|
border: none !important;
|
|
outline: none !important;
|
|
background: transparent !important;
|
|
font-family: 'Roboto', sans-serif;
|
|
font-size: 16px;
|
|
padding: 12px 16px;
|
|
width: 100%;
|
|
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
|
}
|
|
|
|
.phone-input-container :deep(.phone-input input) {
|
|
border: none !important;
|
|
outline: none !important;
|
|
background: transparent !important;
|
|
padding: 0 !important;
|
|
margin: 0 !important;
|
|
font-family: inherit;
|
|
font-size: inherit;
|
|
color: inherit;
|
|
width: 100%;
|
|
}
|
|
|
|
/* Style the country selector */
|
|
.phone-input-container :deep(.country-selector) {
|
|
border: none !important;
|
|
background: transparent !important;
|
|
padding: 0 8px 0 0;
|
|
margin-right: 8px;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.phone-input-container :deep(.country-selector .selected-country) {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
cursor: pointer;
|
|
padding: 4px;
|
|
border-radius: 4px;
|
|
transition: background-color 0.2s ease;
|
|
}
|
|
|
|
.phone-input-container :deep(.country-selector .selected-country:hover) {
|
|
background-color: rgba(var(--v-theme-on-surface), 0.08);
|
|
}
|
|
|
|
/* Style the dropdown */
|
|
.phone-input-container :deep(.country-list) {
|
|
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
|
border-radius: 4px;
|
|
background: rgb(var(--v-theme-surface));
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.phone-input-container :deep(.country-list .country-option) {
|
|
padding: 8px 12px;
|
|
cursor: pointer;
|
|
transition: background-color 0.2s ease;
|
|
border-bottom: 1px solid rgba(var(--v-border-color), 0.12);
|
|
}
|
|
|
|
.phone-input-container :deep(.country-list .country-option:hover) {
|
|
background-color: rgba(var(--v-theme-primary), 0.08);
|
|
}
|
|
|
|
.phone-input-container :deep(.country-list .country-option:last-child) {
|
|
border-bottom: none;
|
|
}
|
|
|
|
/* Error styling */
|
|
.error-message {
|
|
min-height: 20px;
|
|
}
|
|
|
|
.text-error {
|
|
color: rgb(var(--v-theme-error)) !important;
|
|
}
|
|
|
|
/* Monaco/France flag priority styling */
|
|
.phone-input-container :deep(.country-option[data-country-code="MC"]) {
|
|
background-color: rgba(var(--v-theme-primary), 0.04);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.phone-input-container :deep(.country-option[data-country-code="FR"]) {
|
|
background-color: rgba(var(--v-theme-primary), 0.04);
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* Dark theme support */
|
|
@media (prefers-color-scheme: dark) {
|
|
.phone-input-container {
|
|
background: rgb(var(--v-theme-surface));
|
|
border-color: rgba(var(--v-theme-outline), 0.38);
|
|
}
|
|
|
|
.phone-input-container :deep(.country-list) {
|
|
background: rgb(var(--v-theme-surface));
|
|
border-color: rgba(var(--v-theme-outline), 0.38);
|
|
}
|
|
}
|
|
|
|
/* Responsive adjustments */
|
|
@media (max-width: 600px) {
|
|
.phone-input-container :deep(.phone-input) {
|
|
font-size: 16px; /* Prevent zoom on iOS */
|
|
}
|
|
}
|
|
</style>
|