1/* ===================================================================== 2 * eSIM Center â multi-country select (shared) 3 * Trip planner: pick several countries -> /multi-country?c=tr,de,fr 4 * Uses the dataset the header already emits: window.countries, window.userLang, 5 * window.esimBase. No external deps. 6 * Exposes window.EsimMultiSelect { state + url + mount() }. 7 * ===================================================================== */ 8(function () { 9 function countries() { return (typeof window.countries === 'object' && window.countries) ? window.countries : {}; } 10 var TR = (window.userLang === 'tr'); // yalnızca SLUG seçimi için (TR'de coklu-ulke) 11 /* Görünen metinler â TEK KAYNAK: window.lang.multi (PHP l(), aktif dilde). 12 countN bir Åablon ('Search (%n)') â JS'te %n yerine sayı konur. EN fallback. */ 13 var _ML = (window.lang && window.lang.multi) ? window.lang.multi 14 : { search: 'Search', ph: 'Add a country â e.g. Turkey, Germany, Franceâ¦', add: 'Add', countN: 'Search (%n)', empty: 'Pick at least one country' }; 15 var L = { 16 search: _ML.search, ph: _ML.ph, add: _ML.add, empty: _ML.empty, 17 countN: function (n) { return String(_ML.countN).replace('%n', n); } 18 }; 19 20 var sel = []; // ordered list of slugs 21 22 function has(slug) { return sel.indexOf(slug) !== -1; } 23 function add(slug) { if (slug && countries()[slug] && !has(slug)) sel.push(slug); return MS; } 24 function remove(slug) { var i = sel.indexOf(slug); if (i !== -1) sel.splice(i, 1); return MS; } 25 function clear() { sel = []; return MS; } 26 function slugs() { return sel.slice(); } 27 function count() { return sel.length; } 28 function codes() { 29 var cs = countries(); 30 return sel.map(function (s) { return cs[s] ? cs[s].country_code : null; }).filter(Boolean); 31 } 32 function pageSlug() { return TR ? 'coklu-ulke' : 'multi-country'; } 33 function url() { 34 var base = (window.esimBase || ''); 35 return base + '/' + pageSlug() + '?c=' + encodeURIComponent(codes().join(',')); 36 } 37 /* Prefill from a list of lowercase country codes (used by the results page). */ 38 function setFromCodes(arr) { 39 clear(); 40 var cs = countries(); 41 (arr || []).forEach(function (code) { 42 code = String(code || '').toLowerCase().trim(); 43 if (!code) return; 44 for (var s in cs) { if (cs[s] && cs[s].country_code === code) { if (!has(s)) sel.push(s); break; } } 45 }); 46 return MS; 47 } 48 /* Prefill directly from slugs (used by header hand-off). */ 49 function setFromSlugs(arr) { clear(); (arr || []).forEach(function (s) { add(s); }); return MS; } 50 51 function esc(s) { 52 return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { 53 return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; 54 }); 55 } 56 // ---- Relevance ranking (single source of truth for country search) ---------- 57 // Plain substring-anywhere matching ranked Austria above Turkey for "tur" (its TR 58 // name "Avusturya" contains "tur"). Score tiers fix this: exact > prefix > 59 // word-prefix > substring, with the active-language (displayed) title weighted top. 60 function _norm(s) { return String(s == null ? '' : s).toLowerCase().trim(); } 61 function _fieldScore(field, q) { 62 var f = _norm(field); 63 if (!f) return 0; 64 if (f === q) return 100; // exact 65 if (f.indexOf(q) === 0) return 75; // starts with 66 var parts = f.split(/[\s\-\/(),.]+/); 67 for (var i = 0; i < parts.length; i++) { if (parts[i] && parts[i].indexOf(q) === 0) return 55; } // word starts with 68 if (f.indexOf(q) !== -1) return 25; // contains 69 return 0; 70 } 71 function score(q, v, slug) { 72 q = _norm(q); if (!q || !v) return 0; 73 var active = _fieldScore(v.title, q); // active-language (shown) title 74 if (active > 0) active += 5; // ...wins ties vs other fields 75 var other = Math.max(_fieldScore(v.title_tr, q), _fieldScore(v.title_en, q)); 76 return Math.max(active, other, _fieldScore(v.country_code, q), _fieldScore(slug, q)); 77 } 78 // Score every country, drop non-matches, sort by score then shorter/alpha name, 79 // optionally cap to `limit`. Returns [{slug, c, score}]. Does NOT skip selected 80 // countries â the caller decides (header shows them "Added", the widget hides them). 81 function rankCountries(q, cs, limit) { 82 q = _norm(q); if (!q) return []; 83 cs = cs || countries(); 84 var out = [], k; 85 for (k in cs) { var sc = score(q, cs[k], k); if (sc > 0) out.push({ slug: k, c: cs[k], score: sc }); } 86 out.sort(function (a, b) { 87 if (b.score !== a.score) return b.score - a.score; 88 var an = _norm((a.c && (a.c.title || a.c.title_en)) || a.slug); 89 var bn = _norm((b.c && (b.c.title || b.c.title_en)) || b.slug); 90 if (an.length !== bn.length) return an.length - bn.length; // shorter name first 91 return an < bn ? -1 : (an > bn ? 1 : 0); 92 }); 93 return (limit && limit > 0) ? out.slice(0, limit) : out; 94 } 95 function match(q) { 96 if (!_norm(q)) return []; 97 var out = [], ranked = rankCountries(q, countries(), 0); 98 for (var i = 0; i < ranked.length; i++) { 99 if (has(ranked[i].slug)) continue; // widget hides already-picked countries 100 out.push({ slug: ranked[i].slug, c: ranked[i].c }); 101 if (out.length >= 8) break; 102 } 103 return out; 104 } 105 106 var MS = { 107 has: has, add: add, remove: remove, clear: clear, slugs: slugs, codes: codes, 108 count: count, url: url, pageSlug: pageSlug, setFromCodes: setFromCodes, setFromSlugs: setFromSlugs, 109 labels: L, match: match, score: score, rankCountries: rankCountries, esc: esc 110 }; 111 112 /* ---- full standalone widget (used on the results page bar) ---- */ 113 MS.mount = function (rootSel, opts) { 114 opts = opts || {}; 115 var root = typeof rootSel === 'string' ? document.querySelector(rootSel) : rootSel; 116 if (!root) return; 117 if (root.getAttribute('data-ms-mounted')) return; // guard: mount once per element 118 root.setAttribute('data-ms-mounted', '1'); 119 if (!Object.keys(countries()).length) { try { console.warn('EsimMultiSelect: window.countries is empty â no suggestions will show'); } catch (e) {} } 120 121 root.innerHTML = 122 '<div class="ms-shell">' + 123 ' <div class="ms-inwrap">' + 124 ' <i class="fa-solid fa-magnifying-glass ms-ico"></i>' + 125 ' <input id="msInput" type="text" autocomplete="off" placeholder="' + esc(L.ph) + '" />' + 126 ' <div id="msDD" class="ms-dd"></div>' + 127 ' </div>' + 128 ' <button type="button" id="msGo" class="ms-go"><i class="fa-solid fa-magnifying-glass"></i> <span id="msGoLbl">' + esc(L.search) + '</span></button>' + 129 '</div>' + 130 '<div id="msChips" class="ms-chips"></div>'; 131 132 var I = root.querySelector('#msInput'); 133 var DD = root.querySelector('#msDD'); 134 var GO = root.querySelector('#msGo'); 135 var GL = root.querySelector('#msGoLbl'); 136 var CH = root.querySelector('#msChips'); 137 138 function renderChips() { 139 var cs = countries(); 140 CH.innerHTML = sel.map(function (s) { 141 var v = cs[s] || {}; var img = v.image ? '<img src="' + esc(v.image) + '" alt="" onerror="this.style.display=\'none\'">' : ''; 142 return '<span class="ms-chip" data-slug="' + esc(s) + '">' + img + esc(v.title || v.title_en || s) + 143 '<button type="button" class="ms-x" data-rm="' + esc(s) + '" aria-label="remove">×</button></span>'; 144 }).join(''); 145 GL.textContent = count() ? L.countN(count()) : L.search; 146 Array.prototype.forEach.call(CH.querySelectorAll('[data-rm]'), function (b) { 147 b.addEventListener('click', function () { remove(b.getAttribute('data-rm')); renderChips(); }); 148 }); 149 } 150 function renderDD() { 151 var res = match(I.value); 152 if (!I.value.trim() || !res.length) { DD.classList.remove('open'); DD.innerHTML = ''; return; } 153 DD.innerHTML = res.map(function (r) { 154 var v = r.c; var img = v.image ? '<img src="' + esc(v.image) + '" alt="" onerror="this.style.display=\'none\'">' : ''; 155 return '<button type="button" class="ms-opt" data-slug="' + esc(r.slug) + '">' + img + 156 '<span>' + esc(v.title || v.title_en || r.slug) + '</span><i class="fa-solid fa-plus"></i></button>'; 157 }).join(''); 158 DD.classList.add('open'); 159 Array.prototype.forEach.call(DD.querySelectorAll('[data-slug]'), function (b) { 160 b.addEventListener('mousedown', function (e) { e.preventDefault(); rememberSearch(b.getAttribute('data-slug')); add(b.getAttribute('data-slug')); I.value = ''; DD.classList.remove('open'); renderChips(); I.focus(); }); 161 }); 162 } 163 /* BUY_CONTEXT_v1 â kullanıcının YAZDIÄI arama terimini ve seçtiÄi ülkeyi sakla. 164 Alım genelde arama kutusunda deÄil, aramanın götürdüÄü ülke sayfasında olur; 165 bu yüzden deÄer sayfalar arası yaÅasın diye sessionStorage'a yazılır. 166 staff.js -> esimBuyContext() okur, _basket.context'e, oradan _sale.context'e gider. */ 167 function rememberSearch(slug) {
168 try { 169 var term = (I.value || '').trim(); 170 if (term) { sessionStorage.setItem('esimSearchTerm', term.slice(0, 60)); } 171 if (slug) { sessionStorage.setItem('esimSearchSlug', String(slug).slice(0, 40)); } 172 } catch (e) { /* storage kapalı â arama yine de çalıÅır */ } 173 } 174 175 function go() { if (count()) location = url(); else I.focus(); } 176 177 I.addEventListener('input', renderDD); 178 I.addEventListener('focus', renderDD); 179 document.addEventListener('mousedown', function (e) { if (!root.contains(e.target)) DD.classList.remove('open'); }); 180 I.addEventListener('keydown', function (e) { 181 if (e.key === 'Enter') { e.preventDefault(); var r = match(I.value); if (r.length) { rememberSearch(r[0].slug); add(r[0].slug); I.value = ''; DD.classList.remove('open'); renderChips(); } else go(); } 182 else if (e.key === 'Escape') { DD.classList.remove('open'); } 183 }); 184 GO.addEventListener('click', go); 185 186 if (Array.isArray(opts.preselectCodes)) setFromCodes(opts.preselectCodes); 187 else if (Array.isArray(opts.preselectSlugs)) setFromSlugs(opts.preselectSlugs); 188 renderChips(); 189 }; 190 191 window.EsimMultiSelect = MS; 192})();
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.