PageSourceSearch

https://souqmadar.com/js/locale-modal.js

js souqmadar.com collected 2026-10-02 01:43:55 UTC 21,738 bytes, 432 lines download raw bytes

1/**
2 * Locale Modal (LM) — Globe icon popup for Country / Language / Currency
3 *
4 * Works on ALL pages:
5 *   - Homepage: calls LZ.setCountry/setLanguage/setCurrency (live, no reload)
6 *   - Analysis pages: saves to localStorage + cookies, then reloads
7 *
8 * Data source: window.__LM_DATA__ injected server-side (countries, languages, currencies)
9 *
10 * Public API:
11 *   LM.open(opts)  — open the modal; opts.lockCountry = true disables the country field
12 *   LM.close()     — close the modal
13 *   LM.apply()     — apply selections and close
14 */
15(function () {
16  'use strict';
17  if (window.LM) return;
18
19  var LM = window.LM = {};
20  var _built = false;
21  var _lockCountry = false;
22  var _countries = [];          // CX-11A: full-world country list (code/flag/name)
23  var _selectedCountry = 'SA';  // CX-11A: state for the searchable country picker
24  var _noResultsText = 'No countries found';
25
26  // ── Fallback locale data (used if __LM_DATA__ not available) ───────────────
27  var _fallbackCountries = [
28    { code: 'SA', flag: '\uD83C\uDDF8\uD83C\uDDE6', name: '\u0627\u0644\u0633\u0639\u0648\u062F\u064A\u0629' },
29    { code: 'AE', flag: '\uD83C\uDDE6\uD83C\uDDEA', name: '\u0627\u0644\u0625\u0645\u0627\u0631\u0627\u062A' },
30    { code: 'EG', flag: '\uD83C\uDDEA\uD83C\uDDEC', name: '\u0645\u0635\u0631' },
31    { code: 'US', flag: '\uD83C\uDDFA\uD83C\uDDF8', name: 'United States' },
32    { code: 'GB', flag: '\uD83C\uDDEC\uD83C\uDDE7', name: 'United Kingdom' },
33    { code: 'IN', flag: '\uD83C\uDDEE\uD83C\uDDF3', name: 'India' },
34  ];
35  var _fallbackLanguages = [
36    { code: 'ar', labelFull: '\uD83C\uDF10 \u0639\u0631\u0628\u064A' },
37    { code: 'en', labelFull: '\uD83C\uDF10 English' },
38  ];
39  var _fallbackCurrencies = ['SAR', 'AED', 'EGP', 'USD', 'INR', 'EUR', 'GBP'];
40
41  // ── Read current locale values ──────────────────────────────────────────────
42  // PHASE UI-01 (completion) — language always comes from the single
43  // Presentation Context; only country/currency (out of PresentationContext's
44  // scope) still read LZ / the sm_lz_v1 bundle.
45  function _getCurrent() {
46    var country = 'SA', currency = 'SAR';
47    if (window.LZ) {
48      country  = LZ.country  || country;
49      currency = LZ.currency || currency;
50    } else {
51      try {
52        var s = JSON.parse(localStorage.getItem('sm_lz_v1') || '{}');
53        if (s.country_code)  country  = s.country_code;
54        if (s.currency_code) currency = s.currency_code;
55      } catch (_e) {}
56    }
57    return { country: country, language: window.PresentationContext.language, currency: currency };
58  }
59
60  // ── Build modal HTML (once) ─────────────────────────────────────────────────
61  function _build() {
62    if (_built) return;
63    _built = true;
64
65    var data       = window.__LM_DATA__ || {};
66    var countries  = (data.countries  && data.countries.length)  ? data.countries  : _fallbackCountries;
67    var languages  = (data.languages  && data.languages.length)  ? data.languages  : _fallbackLanguages;
68    var currencies = (data.currencies && data.currencies.length) ? data.currencies : _fallbackCurrencies;
69
70    var isAr = window.PresentationContext.language === 'ar';
71
72    var T = isAr ? {
73      title:       '\u0627\u0644\u0644\u063A\u0629 \u0648\u0627\u0644\u0639\u0645\u0644\u0629',
74      titleFull:   '\u0627\u0644\u062F\u0648\u0644\u0629 \u0648\u0627\u0644\u0644\u063A\u0629 \u0648\u0627\u0644\u0639\u0645\u0644\u0629',
75      country:     '\u0627\u0644\u062F\u0648\u0644\u0629',
76      lang:        '\u0627\u0644\u0644\u063A\u0629',
77      currency:    '\u0627\u0644\u0639\u0645\u0644\u0629',
78      apply:       '\u062A\u0637\u0628\u064A\u0642',
79      close:       '\u0625\u063A\u0644\u0627\u0642',
80      countryNote: '\u062B\u0627\u0628\u062A\u0629 \u2014 \u062A\u0645 \u062A\u062D\u062F\u064A\u062F\u0647\u0627 \u0639\u0646\u062F \u0628\u062F\u0621 \u0627\u0644\u0628\u062D\u062B',
81      searchPlaceholder: '\u0627\u0628\u062D\u062B \u0639\u0646 \u062F\u0648\u0644\u0629...',
82      noResults:   '\u0644\u0627 \u062A\u0648\u062C\u062F \u0646\u062A\u0627\u0626\u062C',
83    } : {
84      title:       'Language & Currency',
85      titleFull:   'Country, Language & Currency',
86      country:     'Country',
87      lang:        'Language',
88      currency:    'Currency',
89      apply:       'Apply',
90      close:       'Close',
91      countryNote: 'Fixed — set when the search started',
92      searchPlaceholder: 'Search for a country...',
93      noResults:   'No countries found',
94    };
95    _noResultsText = T.noResults;
96
97    // Inject CSS
98    var style = document.createElement('style');
99    style.textContent = [
100      '#lm-overlay{position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.5);display:none;',
101      'align-items:flex-end;justify-content:center;padding:0}',
102      '@media(min-width:480px){#lm-overlay{align-items:center;padding:16px}}',
103      '#lm-overlay.lm-open{display:flex}',
104      '#lm-modal{background:#fff;border-radius:20px 20px 0 0;width:100%;max-width:460px;',
105      'box-shadow:0 -8px 40px rgba(0,0,0,.2);overflow:hidden;font-family:inherit;',
106      'display:flex;flex-direction:column;max-height:92svh;max-height:92vh;',
107      'animation:lmSlideUp .22s ease}',
108      '@media(min-width:480px){#lm-modal{border-radius:20px;animation:lmFadeIn .18s ease;max-height:90vh}}',
109      '.lm-body{overflow-y:auto;flex:1 1 auto}',
110      '.lm-ft{flex-shrink:0}',
111      '@keyframes lmSlideUp{from{transform:translateY(100%)}to{transform:translateY(0)}}',
112      '@keyframes lmFadeIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}',
113      '.lm-hd{display:flex;align-items:center;justify-content:space-between;',
114      'padding:18px 20px 14px;border-bottom:1px solid #F1F5F9}',
115      '.lm-title{font-size:16px;font-weight:800;color:#0F172A}',
116      '.lm-close-btn{width:32px;height:32px;border-radius:50%;background:#F1F5F9;border:none;',
117      'cursor:pointer;font-size:16px;display:flex;align-items:center;justify-content:center;',
118      'color:#64748B;transition:background .15s}',
119      '.lm-close-btn:hover{background:#E2E8F0}',
120      '.lm-body{padding:20px;display:flex;flex-direction:column;gap:16px}',
121      '.lm-group{display:flex;flex-direction:column;gap:6px}',
122      '.lm-label{font-size:11px;font-weight:700;color:#94A3B8;text-transform:uppercase;letter-spacing:.07em;',
123      'display:flex;align-items:center;gap:6px}',
124      '.lm-lock-icon{font-size:11px}',
125      '.lm-select{width:100%;padding:11px 14px;border:1.5px solid #E2E8F0;border-radius:12px;',
126      'font-size:14px;font-weight:600;color:#0F172A;background:#F8FAFC;cursor:pointer;',
127      'outline:none;font-family:inherit;-webkit-appearance:none;appearance:none;',
128      'background-image:url("data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'12\' height=\'8\' viewBox=\'0 0 12 8\'%3E%3Cpath fill=\'%2394A3B8\' d=\'M6 8L0 0h12z\'/%3E%3C/svg%3E");',
129      'background-repeat:no-repeat;background-position:calc(100% - 14px) center}',
130      'html[dir=rtl] .lm-select{background-position:14px center;padding-right:14px;padding-left:36px}',
131      '.lm-select:focus{border-color:#F97316;box-shadow:0 0 0 3px rgba(249,115,22,.12);background:#fff}',
132      '.lm-select:disabled{opacity:1;cursor:not-allowed;color:#64748B;background:#F1F5F9;',
133      'border-color:#E2E8F0;background-image:none}',
134      '.lm-country-note{font-size:11px;color:#94A3B8;margin-top:2px;display:none}',
135      '.lm-country-note.lm-visible{display:block}',
136      /* CX-11A: searchable country picker */
137      '.lm-country-search-wrap{position:relative}',
138      '.lm-country-search{width:100%;padding:11px 14px;border:1.5px solid #E2E8F0;border-radius:12px;',
139      'font-size:14px;font-weight:600;color:#0F172A;background:#F8FAFC;outline:none;',
140      'font-family:inherit;box-sizing:border-box}',
141      '.lm-country-search:focus{border-color:#F97316;box-shadow:0 0 0 3px rgba(249,115,22,.12);background:#fff}',
142      '.lm-country-search:disabled{opacity:1;cursor:not-allowed;color:#64748B;background:#F1F5F9}',
143      '.lm-country-list{margin-top:6px;max-height:200px;overflow-y:auto;border:1.5px solid #E2E8F0;',
144      'border-radius:12px;background:#fff}',
145      '.lm-country-list.lm-hidden{display:none}',
146      '.lm-country-item{display:flex;align-items:center;gap:8px;padding:10px 14px;font-size:14px;',
147      'font-weight:600;color:#0F172A;cursor:pointer;transition:background .1s}',
148      '.lm-country-item:hover,.lm-country-item.lm-active{background:#FFF7ED}',
149      '.lm-country-item.lm-selected{background:#FFEDD5;color:#C2410C}',
150      '.lm-country-empty{padding:14px;text-align:center;font-size:13px;color:#94A3B8}',
151      '.lm-country-current{display:flex;align-items:center;gap:8px;padding:11px 14px;',
152      'border:1.5px solid #E2E8F0;border-radius:12px;background:#F8FAFC;font-size:14px;',
153      'font-weight:600;color:#0F172A;cursor:pointer}',
154      '.lm-country-current:hover{border-color:#CBD5E1}',
155      '.lm-country-current.lm-locked{cursor:not-allowed;color:#64748B}',
156      '.lm-ft{padding:16px 20px 20px;border-top:1px solid #F1F5F9}',
157      '.lm-apply-btn{width:100%;padding:14px;border:none;border-radius:12px;',
158      'background:#F97316;color:#fff;font-size:15px;font-weight:800;cursor:pointer;',
159      'letter-spacing:.02em;transition:background .15s;font-family:inherit}',
160      '.lm-apply-btn:hover{background:#E54B00}',
161      '.lm-apply-btn:active{background:#C73F00}',
162    ].join('');
163    document.head.appendChild(style);
164
165    _countries = countries;
166
167    var lOpts = languages.map(function (l) {
168      return '<option value="' + l.code + '">' + l.labelFull + '</option>';
169    }).join('');
170    var cyOpts = currencies.map(function (c) {
171      return '<option value="' + c + '">' + c + '</option>';
172    }).join('');
173
174    var dir = isAr ? 'rtl' : 'ltr';
175    var html = '<div id="lm-overlay" onclick="if(event.target===this)LM.close()" role="dialog" aria-modal="true" aria-label="' + T.titleFull + '">'
176      + '<div id="lm-modal" dir="' + dir + '">'
177      + '<div class="lm-hd">'
178      + '<span class="lm-title" id="lm-title">' + T.titleFull + '</span>'
179      + '<button class="lm-close-btn" onclick="LM.close()" aria-label="' + T.close + '">&#x2715;</button>'
180      + '</div>'
181      + '<div class="lm-body">'
182      + '<div class="lm-group" id="lm-country-group">'
183      + '<label class="lm-label"><span id="lm-country-lock-icon"></span>' + T.country + '</label>'
184      + '<div class="lm-country-search-wrap">'
185      + '<input type="text" id="lm-country-search" class="lm-country-search" placeholder="' + T.searchPlaceholder + '" autocomplete="off" />'
186      + '<div class="lm-country-list lm-hidden" id="lm-country-list" role="listbox"></div>'
187      + '</div>'
188      + '<div class="lm-country-note" id="lm-country-note">' + T.countryNote + '</div>'
189      + '</div>'
190      + '<div class="lm-group"><label class="lm-label">' + T.lang + '</label>'
191      + '<select id="lm-sel-lang" class="lm-select">' + lOpts + '</select></div>'
192      + '<div class="lm-group"><label class="lm-label">' + T.currency + '</label>'
193      + '<select id="lm-sel-currency" class="lm-select">' + cyOpts + '</select></div>'
194      + '</div>'
195      + '<div class="lm-ft"><button class="lm-apply-btn" onclick="LM.apply()">' + T.apply + '</button></div>'
196      + '</div></div>';
197
198    document.body.insertAdjacentHTML('beforeend', html);
199    _wireCountrySearch();
200  }
201
202  // ── CX-11A: searchable country picker ───────────────────────────────────────
203  function _escHtml(s) {
204    return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
205  }
206
207  function _findCountry(code) {
208    for (var i = 0; i < _countries.length; i++) {
209      if (_countries[i].code === code) return _countries[i];
210    }
211    return null;
212  }
213
214  function _renderCountryList(filterText) {
215    var listEl = document.getElementById('lm-country-list');
216    if (!listEl) return;
217    var q = (filterText || '').trim().toLowerCase();
218    var matches = !q ? _countries : _countries.filter(function (c) {
219      return c.name.toLowerCase().indexOf(q) !== -1 || c.code.toLowerCase().indexOf(q) !== -1;
220    });
221    if (!matches.length) {
222      listEl.innerHTML = '<div class="lm-country-empty">' + _escHtml(_noResultsText) + '</div>';
223      return;
224    }
225    listEl.innerHTML = matches.slice(0, 300).map(function (c) {
226      var sel = c.code === _selectedCountry ? ' lm-selected' : '';
227      return '<div class="lm-country-item' + sel + '" role="option" data-code="' + c.c
227ode + '">'
228        + c.flag + ' ' + _escHtml(c.name) + '</div>';
229    }).join('');
230  }
231
232  function _selectCountry(code) {
233    var c = _findCountry(code);
234    if (!c) return;
235    _selectedCountry = code;
236    var input = document.getElementById('lm-country-search');
237    if (input) input.value = c.flag + ' ' + c.name;
238    var listEl = document.getElementById('lm-country-list');
239    if (listEl) listEl.classList.add('lm-hidden');
240  }
241
242  function _wireCountrySearch() {
243    var input = document.getElementById('lm-country-search');
244    var listEl = document.getElementById('lm-country-list');
245    if (!input || !listEl) return;
246
247    input.addEventListener('focus', function () {
248      if (_lockCountry) return;
249      _renderCountryList(input.value === _currentDisplayValue() ? '' : input.value);
250      listEl.classList.remove('lm-hidden');
251    });
252    input.addEventListener('input', function () {
253      if (_lockCountry) return;
254      _renderCountryList(input.value);
255      listEl.classList.remove('lm-hidden');
256    });
257    // Delay hide so click on a list item registers before blur closes it.
258    input.addEventListener('blur', function () {
259      setTimeout(function () { listEl.classList.add('lm-hidden'); }, 150);
260    });
261    listEl.addEventListener('mousedown', function (e) {
262      var item = e.target.closest ? e.target.closest('.lm-country-item') : null;
263      if (!item) return;
264      e.preventDefault();
265      _selectCountry(item.getAttribute('data-code'));
266    });
267  }
268
269  function _currentDisplayValue() {
270    var c = _findCountry(_selectedCountry);
271    return c ? (c.flag + ' ' + c.name) : '';
272  }
273
274  // ── Apply lock state visually ───────────────────────────────────────────────
275  function _applyLockState() {
276    var cs        = document.getElementById('lm-country-search');
277    var listEl    = document.getElementById('lm-country-list');
278    var lockIcon  = document.getElementById('lm-country-lock-icon');
279    var lockNote  = document.getElementById('lm-country-note');
280    var titleEl   = document.getElementById('lm-title');
281    var isAr      = window.PresentationContext.language === 'ar';
282
283    if (cs) cs.disabled = _lockCountry;
284    if (_lockCountry && listEl) listEl.classList.add('lm-hidden');
285
286    if (lockIcon) lockIcon.textContent = _lockCountry ? '\uD83D\uDD12 ' : '';
287
288    if (lockNote) {
289      if (_lockCountry) lockNote.classList.add('lm-visible');
290      else lockNote.classList.remove('lm-visible');
291    }
292
293    if (titleEl) {
294      if (_lockCountry) {
295        titleEl.textContent = isAr ? '\u0627\u0644\u0644\u063A\u0629 \u0648\u0627\u0644\u0639\u0645\u0644\u0629' : 'Language & Currency';
296      } else {
297        titleEl.textContent = isAr ? '\u0627\u0644\u062F\u0648\u0644\u0629 \u0648\u0627\u0644\u0644\u063A\u0629 \u0648\u0627\u0644\u0639\u0645\u0644\u0629' : 'Country, Language & Currency';
298      }
299    }
300  }
301
302  // ── Public API ──────────────────────────────────────────────────────────────
303  LM.open = function (opts) {
304    opts = opts || {};
305    _lockCountry = !!opts.lockCountry;
306
307    _build();
308
309    var cur = _getCurrent();
310    var cs  = document.getElementById('lm-country-search');
311    var ls  = document.getElementById('lm-sel-lang');
312    var cy  = document.getElementById('lm-sel-currency');
313    _selectedCountry = cur.country;
314    if (cs) cs.value = _currentDisplayValue() || cur.country;
315    if (ls) ls.value = cur.language;
316    if (cy) cy.value = cur.currency;
317
318    _applyLockState();
319
320    var ov = document.getElementById('lm-overlay');
321    if (ov) { ov.classList.add('lm-open'); document.body.style.overflow = 'hidden'; }
322  };
323
324  LM.close = function () {
325    var ov = document.getElementById('lm-overlay');
326    if (ov) { ov.classList.remove('lm-open'); document.body.style.overflow = ''; }
327  };
328
329  LM.apply = function () {
330    var ls  = document.getElementById('lm-sel-lang');
331    var cy  = document.getElementById('lm-sel-currency');
332    // Country is ignored when locked
333    var country  = !_lockCountry ? _selectedCountry : null;
334    var language = ls  ? ls.value  : null;
335    var currency = cy  ? cy.value  : null;
336
337    if (window.LZ && typeof LZ.setLanguage === 'function') {
338      // Homepage: LZ handles live re-render without page reload.
339      // IMPORTANT: only call setCountry if the country actually changed.
340      // setCountry is async and resets _langManual, which causes it to overwrite
341      // the language cookie set by setLanguage (race condition).
342      var _currentCountry = LZ.country || null;
343      var _countryChanged = country && country !== _currentCountry;
344      if (language) LZ.setLanguage(language);
345      if (currency) LZ.setCurrency(currency);
346      if (_countryChanged) LZ.setCountry(country);
347      LM.updateButtons();
348      LM.close();
349    } else {
350      // Analysis pages: persist to localStorage + cookies, then reload
351      try {
352        var s = JSON.parse(localStorage.getItem('sm_lz_v1') || '{}');
353        if (country)  { s.country_code  = country;  s.manual = true; }
354        if (language) { s.language_code = language; s.lang_manual = true; }
355        if (currency) { s.currency_code = currency; s.curr_manual = true; }
356        localStorage.setItem('sm_lz_v1', JSON.stringify(s));
357      } catch (_e) {}
358      // PHASE UI-01 (completion): single write path for sm_lang persistence —
359      // see presentation-language-context.js. locale-modal.js is only ever
360      // loaded on pages that inject the context first (see server.js).
361      if (language) window.PresentationContext.setLanguage(language);
362      if (country) {
363        document.cookie = 'sm_country=' + country + '; path=/; max-age=31536000';
364      }
365      LM.close();
366      window.location.reload();
367    }
368  };
369
370  // ── Button sync — updates flag + country name in locale pill buttons ────────
371  function _codeToFlag(code) {
372    if (!code || code.length !== 2) return '🌐';
373    try {
374      return String.fromCodePoint(
375        0x1F1E6 + code.toUpperCase().charCodeAt(0) - 65,
376        0x1F1E6 + code.toUpperCase().charCodeAt(1) - 65
377      );
378    } catch (_e) { return '🌐'; }
379  }
380
381  var _COUNTRY_NAMES = {
382    SA:{ar:'السعودية',    en:'Saudi Arabia'},  AE:{ar:'الإمارات',   en:'UAE'},
383    EG:{ar:'مصر',         en:'Egypt'},         KW:{ar:'الكويت',     en:'Kuwait'},
384    QA:{ar:'قطر',         en:'Qatar'},         BH:{ar:'البحرين',    en:'Bahrain'},
385    OM:{ar:'عُمان',       en:'Oman'},          JO:{ar:'الأردن',     en:'Jordan'},
386    IQ:{ar:'العراق',      en:'Iraq'},          LB:{ar:'لبنان',      en:'Lebanon'},
387    YE:{ar:'اليمن',       en:'Yemen'},         IN:{ar:'الهند',      en:'India'},
388    US:{ar:'أمريكا',      en:'United States'}, GB:{ar:'بريطانيا',   en:'UK'},
389    CA:{ar:'كندا',        en:'Canada'},        AU:{ar:'أستراليا',   en:'Australia'},
390    TR:{ar:'تركيا',       en:'Turkey'},        DE:{ar:'ألمانيا',    en:'Germany'},
391    FR:{ar:'فرنسا',       en:'France'},        MA:{ar:'المغرب',     en:'Morocco'},
392    DZ:{ar:'الجزائر',     en:'Algeria'},       PK:{ar:'باكستان',    en:'Pakistan'},
393    ID:{ar:'إندونيسيا',   en:'Indonesia'},     PH:{ar:'الفلبين',    en:'Philippines'},
394    TH:{ar:'تايلاند',     en:'Thailand'},      ES:{ar:'إسبانيا',    en:'Spain'},
395    IT:{ar:'إيطاليا',     en:'Italy'},         PL:{ar:'بولندا',     en:'Poland'},
396    RU:{ar:'روسيا',       en:'Russia'}
397  };
398
399  function _codeToName(countryCode, langCode) {
400    var c = (countryCode || '').toUpperCase();
401    var l = (langCode || 'ar').toLowerCase();
402    // CX-11A: prefer the full-world list (already localized server-side) when available.
403    var full = _findCountry(c);
404    if (full && full.name) return full.name;
405    var rec = _COUNTRY_NAMES[c];
406    if (!rec) return c;
407    return l === 'ar' ? rec.ar : rec.en;
408  }
409
410  LM.updateButtons = function () {
411    var cur  = _getCurrent();
412    var flag = _codeToFlag(cur.country);
413    var name = _codeToName(cur.country, cur.language);
414    var lang = (cur.language || 'AR').toUpperCase();
415
416    // Update both locale token instances (homepage + analysis page)
417    [['hp2LocaleFlag','hp2LocaleName','hp2LocaleLang'],
418     ['aspLocaleFlag','aspLocaleName','aspLocaleLang']].forEach(function(ids) {
419      var elFlag    = document.getElementById(ids[0]);
420      var elCountry = document.getElementById(ids[1]);
421      var elLang    = document.getElementById(ids[2]);
422      if (elFlag)    elFlag.textContent    = flag;
423      if (elCountry) elCountry.textContent = name;
424      if (elLang)    elLang.textContent    = lang;
425    });
426  };
427
428  // Close on Escape key
429  document.addEventListener('keydown', function (e) {
430    if (e.key === 'Escape') LM.close();
431  });
432})();

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.