Initial Commit
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
{# 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>
|
||||
Reference in New Issue
Block a user