PageSourceSearch

https://esimcenter.com/cdn/assets/js/multi-select.js?v=v1.3.3-buyctx20260826

js esimcenter.com collected 2026-10-02 01:28:32 UTC 10,280 bytes, 192 lines download raw bytes

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 { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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">&times;</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.