Files
NexHome/templates/components/phone_input.html
T
2026-06-11 15:05:08 +08:00

150 lines
7.3 KiB
HTML

{# Phone input component with country code selector, flags, dynamic placeholder, and auto-formatting.
Usage: {% include "components/phone_input.html" %}
Expects: name, value (optional), id (optional)
#}
<div class="phone-input-wrapper" id="phoneWrapper">
<label for="contact_phone_input" class="block text-sm font-medium text-gray-700 mb-1">Contact Phone</label>
<div class="flex rounded-lg border border-gray-300 overflow-hidden focus-within:ring-2 focus-within:ring-accent focus-within:border-transparent transition">
<!-- Country selector button -->
<button type="button" id="phoneCountryBtn"
class="flex items-center gap-1.5 px-3 bg-gray-50 border-r border-gray-300 hover:bg-gray-100 transition text-sm flex-shrink-0"
onclick="togglePhoneDropdown()">
<span id="phoneFlag" class="text-lg leading-none">🇺🇸</span>
<span id="phoneDialCode" class="text-gray-700 font-medium">+1</span>
<svg class="w-3 h-3 text-gray-500 ml-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
<!-- Hidden input for actual phone value (with dial code) -->
<input type="hidden" name="contact_phone" id="phoneHidden" value="{{ phone_value|default('') }}">
<!-- Visible phone display -->
<input type="tel" id="phoneDisplay"
class="flex-1 px-3 py-2.5 outline-none bg-white text-sm min-w-0"
placeholder="(555) 123-4567"
autocomplete="tel"
oninput="formatPhoneInput(this)"
onfocus="formatPhoneInput(this)">
</div>
<!-- Dropdown -->
<div id="phoneDropdown"
class="hidden absolute z-50 mt-1 w-72 bg-white border border-gray-200 rounded-xl shadow-xl max-h-64 overflow-y-auto">
</div>
</div>
<script>
(function() {
const COUNTRIES = [
{ code: 'US', flag: '🇺🇸', dial: '+1', name: 'United States', format: '(###) ###-####', digitCount: 10 },
{ code: 'CA', flag: '🇨🇦', dial: '+1', name: 'Canada', format: '(###) ###-####', digitCount: 10 },
{ code: 'GB', flag: '🇬🇧', dial: '+44', name: 'United Kingdom', format: '#### ######', digitCount: 10 },
{ code: 'AU', flag: '🇦🇺', dial: '+61', name: 'Australia', format: '#### ### ###', digitCount: 9 },
{ code: 'CN', flag: '🇨🇳', dial: '+86', name: 'China', format: '### #### ####', digitCount: 11 },
{ code: 'JP', flag: '🇯🇵', dial: '+81', name: 'Japan', format: '##-####-####', digitCount: 10 },
{ code: 'KR', flag: '🇰🇷', dial: '+82', name: 'South Korea', format: '##-####-####', digitCount: 10 },
{ code: 'DE', flag: '🇩🇪', dial: '+49', name: 'Germany', format: '### ########', digitCount: 11 },
{ code: 'FR', flag: '🇫🇷', dial: '+33', name: 'France', format: '# ## ## ## ##', digitCount: 9 },
{ code: 'IN', flag: '🇮🇳', dial: '+91', name: 'India', format: '##### #####', digitCount: 10 },
{ code: 'BR', flag: '🇧🇷', dial: '+55', name: 'Brazil', format: '## #####-####', digitCount: 11 },
{ code: 'MX', flag: '🇲🇽', dial: '+52', name: 'Mexico', format: '## #### ####', digitCount: 10 },
{ code: 'SG', flag: '🇸🇬', dial: '+65', name: 'Singapore', format: '#### ####', digitCount: 8 },
{ code: 'HK', flag: '🇭🇰', dial: '+852', name: 'Hong Kong(China)', format: '#### ####', digitCount: 8 },
{ code: 'TW', flag: '🇹🇼', dial: '+886', name: 'Taiwan(China)', format: '## ### ####', digitCount: 9 },
];
let selectedCountry = COUNTRIES[0]; // Default: US
let rawDigits = '';
// Parse initial value
const hiddenInput = document.getElementById('phoneHidden');
const displayInput = document.getElementById('phoneDisplay');
if (hiddenInput.value) {
for (const c of COUNTRIES) {
if (hiddenInput.value.startsWith(c.dial)) {
selectedCountry = c;
rawDigits = hiddenInput.value.replace(c.dial, '').replace(/\D/g, '');
break;
}
}
if (!rawDigits && hiddenInput.value) {
rawDigits = hiddenInput.value.replace(/\D/g, '');
}
}
function updateDisplay() {
document.getElementById('phoneFlag').textContent = selectedCountry.flag;
document.getElementById('phoneDialCode').textContent = selectedCountry.dial;
displayInput.placeholder = formatDigits('', selectedCountry);
displayInput.value = formatDigits(rawDigits, selectedCountry);
hiddenInput.value = rawDigits.length > 0 ? selectedCountry.dial + rawDigits : '';
}
function formatDigits(digits, country) {
if (!digits) return '';
let result = '';
let di = 0;
for (let i = 0; i < country.format.length && di < digits.length; i++) {
if (country.format[i] === '#') {
result += digits[di++];
} else {
result += country.format[i];
}
}
return result;
}
window.formatPhoneInput = function(input) {
const cursorPos = input.selectionStart;
const prevLength = input.value.length;
rawDigits = input.value.replace(/\D/g, '').substring(0, selectedCountry.digitCount);
updateDisplay();
// Restore approximate cursor position
const newLength = input.value.length;
const newPos = cursorPos + (newLength - prevLength);
input.setSelectionRange(newPos, newPos);
};
window.togglePhoneDropdown = function() {
const dd = document.getElementById('phoneDropdown');
dd.classList.toggle('hidden');
if (!dd.classList.contains('hidden')) {
renderDropdown();
}
};
window.selectCountry = function(code) {
selectedCountry = COUNTRIES.find(c => c.code === code);
rawDigits = rawDigits.substring(0, selectedCountry.digitCount);
updateDisplay();
document.getElementById('phoneDropdown').classList.add('hidden');
};
function renderDropdown() {
const dd = document.getElementById('phoneDropdown');
dd.innerHTML = COUNTRIES.map(c => `
<button type="button"
onclick="selectCountry('${c.code}')"
class="w-full flex items-center gap-3 px-3 py-2.5 hover:bg-accent/5 transition text-left
${c.code === selectedCountry.code ? 'bg-accent/10' : ''}">
<span class="text-xl">${c.flag}</span>
<span class="text-sm font-medium text-gray-700">${c.name}</span>
<span class="text-sm text-gray-500 ml-auto">${c.dial}</span>
</button>
`).join('');
}
// Close dropdown on outside click
document.addEventListener('click', function(e) {
const wrapper = document.getElementById('phoneWrapper');
if (!wrapper.contains(e.target)) {
document.getElementById('phoneDropdown').classList.add('hidden');
}
});
// Initial render
updateDisplay();
})();
</script>