1// Custom country picker: searchable dropdown with flag thumbnails. 2// Reveals on JS load; falls back to the native <select> when JS is off. 3 4(function () { 5 document.querySelectorAll('[data-country-picker]').forEach(init); 6 7 function init(root) { 8 var trigger = root.querySelector('.cp-trigger'); 9 var dropdown = root.querySelector('.cp-dropdown'); 10 var search = root.querySelector('.cp-search'); 11 var list = root.querySelector('.cp-list'); 12 var empty = root.querySelector('.cp-empty'); 13 var native = root.querySelector('.cp-native'); 14 var options = Array.from(root.querySelectorAll('.cp-option')); 15 var groups = Array.from(root.querySelectorAll('.cp-group')); 16 var triggerFlag = trigger.querySelector('.cp-flag'); 17 var triggerName = trigger.querySelector('.cp-name'); 18 19 // Pre-compute a diacritic-folded haystack per option so "oster" matches 20 // "Ãsterreich", "etats" matches "Ãtats-Unis", "turkei" matches "Türkei". 21 function fold(s) { 22 return (s || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase(); 23 } 24 options.forEach(function (o) { 25 o._haystack = fold(o.dataset.name) + ' ' + o.dataset.code; 26 }); 27 28 // Activate the custom UI (the native <select> stays in the DOM for form submission). 29 root.classList.add('is-ready'); 30 native.setAttribute('tabindex', '-1'); 31 32 var open = false; 33 var activeIndex = -1; 34 35 function placeholder() { 36 // First non-empty option in the native select gives us the localized placeholder 37 var ph = native.options[0] ? native.options[0].text : 'Select country'; 38 triggerFlag.removeAttribute('src'); 39 triggerFlag.style.display = 'none'; 40 triggerName.textContent = ph; 41 triggerName.classList.add('is-placeholder'); 42 } 43 44 function select(code, opts) { 45 opts = opts || {}; 46 var opt = options.find(function (o) { return o.dataset.code === code; }); 47 if (!opt) { placeholder(); return; } 48 native.value = code; 49 var flag = opt.dataset.flag; 50 if (flag) { 51 triggerFlag.src = flag; 52 triggerFlag.style.display = ''; 53 } 54 triggerName.textContent = opt.dataset.name; 55 triggerName.classList.remove('is-placeholder'); 56 options.forEach(function (o) { o.classList.toggle('is-selected', o === opt); }); 57 // Notify other listeners (pricing calculator carries country into the URL) 58 if (!opts.silent) native.dispatchEvent(new Event('change', { bubbles: true })); 59 } 60 61 function setOpen(v) { 62 open = v; 63 dropdown.hidden = !v; 64 trigger.setAttribute('aria-expanded', v ? 'true' : 'false'); 65 if (v) { 66 // Focus search; reset filter; jump active to selected (or first) 67 search.value = ''; 68 filter(''); 69 var selectedIdx = options.findIndex(function (o) { return o.classList.contains('is-selected'); }); 70 activeIndex = selectedIdx >= 0 ? selectedIdx : 0; 71 updateActive(); 72 scrollActiveIntoView(); 73 setTimeout(function () { search.focus(); }, 0); 74 } 75 } 76 77 function updateActive() { 78 options.forEach(function (o, i) { o.classList.toggle('is-active', i === activeIndex); }); 79 } 80 81 function scrollActiveIntoView() { 82 var el = options[activeIndex]; 83 if (!el || el.hidden) return; 84 var box = list.getBoundingClientRect(); 85 var elBox = el.getBoundingClientRect(); 86 if (elBox.top < box.top) list.scrollTop -= (box.top - elBox.top); 87 else if (elBox.bottom > box.bottom) list.scrollTop += (elBox.bottom - box.bottom); 88 } 89 90 function visible() { 91 return options.filter(function (o) { return !o.hidden; }); 92 } 93 94 function filter(q) { 95 q = fold(q.trim()); 96 var anyVisible = false; 97 options.forEach(function (o) { 98 var match = !q || o._haystack.indexOf(q) !== -1; 99 o.hidden = !match; 100 if (match) anyVisible = true; 101 }); 102 // Hide group headers whose group has no visible items 103 groups.forEach(function (g) { 104 var anyInGroup = false; 105 var n = g.nextElementSibling; 106 while (n && !n.classList.contains('cp-group')) { 107 if (n.classList.contains('cp-option') && !n.hidden) { anyInGroup = true; break; } 108 n = n.nextElementSibling; 109 } 110 g.hidden = !anyInGroup; 111 }); 112 empty.hidden = anyVisible; 113 // Move active to first visible 114 if (q) { 115 var first = options.findIndex(function (o) { return !o.hidden; }); 116 if (first >= 0) { activeIndex = first; updateActive(); } 117 } 118 } 119 120 function step(dir) { 121 var vis = visible(); 122 if (!vis.length) return; 123 var currentInVis = vis.indexOf(options[activeIndex]); 124 var nextInVis = (currentInVis + dir + vis.length) % vis.length; 125 activeIndex = options.indexOf(vis[nextInVis]); 126 updateActive(); 127 scrollActiveIntoView(); 128 } 129 130 // --- Wiring --- 131 trigger.addEventListener('click', function (e) { 132 e.preventDefault(); 133 setOpen(!open); 134 }); 135 136 search.addEventListener('input', function () { filter(search.value); }); 137 138 search.addEventListener('keydown', function (e) { 139 if (e.key === 'ArrowDown') { e.preventDefault(); step(1); } 140 else if (e.key === 'ArrowUp') { e.preventDefault(); step(-1); } 141 else if (e.key === 'Enter') { 142 e.preventDefault(); 143 var el = options[activeIndex]; 144 if (el && !el.hidden) { select(el.dataset.code); setOpen(false); trigger.focus(); } 145 } 146 else if (e.key === 'Escape') { e.preventDefault(); setOpen(false); trigger.focus(); } 147 }); 148 149 list.addEventListener('click', function (e) { 150 var el = e.target.closest('.cp-option'); 151 if (!el) return; 152 select(el.dataset.code); 153 setOpen(false); 154 trigger.focus(); 155 }); 156 157 document.addEventListener('click', function (e) { 158 if (open && !root.contains(e.target)) setOpen(false); 159 }); 160 161 // Allow Enter / ArrowDown on the trigger to open 162 trigger.addEventListener('keydown', function (e) { 163 if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') { 164 e.preventDefault(); 165 setOpen(true); 166 } 167 }); 168 169 // --- Preselection --- 170 var initialCode = preselect(); 171 if (initialCode) select(initialCode, { silent: true }); 172 else placeholder(); 173 174 function preselect() { 175 // 1) Explicit ?country= (e.g. carried over from pricing â contact handoff) 176 var url = new URL(window.location.href); 177 var fromUrl = url.searchParams.get('country'); 178 if (fromUrl && hasCode(fromUrl)) return fromUrl.toLowerCase(); 179 180 var nav = (navigator.language || (navigator.languages && navigator.languages[0]) || '').toLowerCase(); 181 var parts = nav.split('-'); 182 var siteLang = (document.documentElement.lang || '').toLowerCase(); 183 184 // 2) Browser region â only if its language matches the site's language. 185 // (e.g. de-AT visitor on /de/ â AT; en-US visitor on /de/ â fall through.) 186 if (parts[0] === siteLang && parts[1] && hasCode(parts[1])) return parts[1]; 187 188 // 3) Site language default 189 var hint = { de: 'de', en: 'us', fr: 'fr' }[siteLang]; 190 if (hint && hasCode(hint)) return hint; 191 192 // 4) Loose fallback to navigator region 193 if (parts[1] && hasCode(parts[1])) return parts[1]; 194 if (parts[0] && hasCode(parts[0])) return parts[0]; 195 196 return null; 197 } 198 199 function hasCode(code) { 200 return options.some(function (o) { return o.dataset.code === code.toLowerCase(); }); 201 } 202 } 203})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.