1/** 2 * SKP Unified Language Switcher â Production JS 3 * Dropdown interaction. Zero dependencies. 4 * 5 * v1.1 â Portal pattern for mobile bottom-sheet 6 * v1.2 â Cookie persistence: remembers selected language across page loads. 7 * GTranslate pages bypass WordPress so PHP can't detect Tier 2 8 * languages. Cookie + JS toggle update solves this. 9 * v1.3 â JS-constructed URLs: GTranslate mangles href attributes by 10 * prepending the current lang prefix. Option clicks now build 11 * clean URLs from scratch instead of following mangled hrefs. 12 * v1.4 â URL is authoritative for the displayed language. Previously a 13 * stale skp_lang=el cookie repainted the toggle to GR on English 14 * pages (visit /el/, strip prefix, land on / â EN flag "hiccuped" 15 * to GR). Non-prefixed path now always renders EN and clears any 16 * non-en cookie. Also removes the dead /el/ regex branch â 17 * detectLangFromURL() already handles el. 18 */ 19(function () { 20 'use strict'; 21 22 var MQ_MOBILE = '(max-width: 640px)'; 23 var COOKIE_NAME = 'skp_lang'; 24 var COOKIE_DAYS = 30; 25 var FLAG_BASE = 'https://flagcdn.com/w40/'; 26 var backdrop = null; 27 28 /* ============================================================ 29 * Language data (must match PHP arrays) 30 * ============================================================ */ 31 var LANG_MAP = { 32 en: 'gb', el: 'gr', fr: 'fr', de: 'de', it: 'it', nl: 'nl', 33 ro: 'ro', pt: 'pt', es: 'es', tr: 'tr', sv: 'se', ru: 'ru', 34 pl: 'pl', sl: 'si', af: 'za', sq: 'al', az: 'az', be: 'by', 35 bs: 'ba', bg: 'bg', hr: 'hr', cs: 'cz', da: 'dk', et: 'ee', 36 fi: 'fi', ka: 'ge', iw: 'il', hu: 'hu', ga: 'ie', ja: 'jp', 37 lv: 'lv', lt: 'lt', lb: 'lu', mk: 'mk', fa: 'ir', no: 'no', 38 sr: 'rs', sk: 'sk', uk: 'ua' 39 }; 40 41 /* ============================================================ 42 * Cookie helpers 43 * ============================================================ */ 44 function setCookie(code) { 45 var d = new Date(); 46 d.setTime(d.getTime() + COOKIE_DAYS * 86400000); 47 document.cookie = COOKIE_NAME + '=' + encodeURIComponent(code) + 48 ';expires=' + d.toUTCString() + 49 ';path=/;SameSite=Lax'; 50 } 51 52 function getCookie() { 53 var match = document.cookie.match(new RegExp('(?:^|;\\s*)' + COOKIE_NAME + '=([^;]*)')); 54 return match ? decodeURIComponent(match[1]) : null; 55 } 56 57 function clearCookie() { 58 document.cookie = COOKIE_NAME + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;SameSite=Lax'; 59 } 60 61 /* ============================================================ 62 * Detect current language from URL prefix 63 * Works on GTranslate proxy pages where PHP can't detect 64 * ============================================================ */ 65 function detectLangFromURL() { 66 var path = window.location.pathname.replace(/^\/+/, ''); 67 var seg = path.split('/')[0]; 68 if (seg && LANG_MAP[seg]) return seg; 69 return null; 70 } 71 72 /* ============================================================ 73 * Update toggle button flag + code on ALL visible switchers 74 * ============================================================ */ 75 function updateAllToggles(code) { 76 var flag = LANG_MAP[code]; 77 if (!flag) return; 78 79 document.querySelectorAll('.skp-lang').forEach(function (sw) { 80 var flagImg = sw.querySelector('.skp-lang__toggle .skp-flag'); 81 var codeSpan = sw.querySelector('.skp-lang__toggle .skp-lang__code'); 82 if (flagImg) flagImg.src = FLAG_BASE + flag + '.png'; 83 if (codeSpan) codeSpan.textContent = code.toUpperCase(); 84 85 // Update active state in dropdown options 86 sw.querySelectorAll('.skp-lang__option').forEach(function (opt) { 87 var isActive = opt.getAttribute('data-lang') === code; 88 opt.classList.toggle('skp-lang__option--active', isActive); 89 opt.setAttribute('aria-selected', isActive ? 'true' : 'false'); 90 // Add/remove checkmark 91 var check = opt.querySelector('.skp-lang__check'); 92 if (isActive && !check) { 93 var span = document.createElement('span'); 94 span.className = 'skp-lang__check'; 95 span.innerHTML = '✓'; 96 opt.appendChild(span); 97 } else if (!isActive && check) { 98 check.remove(); 99 } 100 }); 101 }); 102 } 103 104 /* ============================================================ 105 * Mobile helpers (portal pattern) 106 * ============================================================ */ 107 function isMobile() { 108 return window.matchMedia(MQ_MOBILE).matches; 109 } 110 111 function getBackdrop() { 112 if (!backdrop) { 113 backdrop = document.createElement('div'); 114 backdrop.className = 'skp-lang__backdrop'; 115 backdrop.addEventListener('click', function () { 116 document.querySelectorAll('.skp-lang[data-open="true"]').forEach(function (sw) { 117 sw.dataset.open = 'false'; 118 var t = sw.querySelector('.skp-lang__toggle'); 119 if (t) t.setAttribute('aria-expanded', 'false'); 120 }); 121 hideBackdrop(); 122 returnAllDropdowns(); 123 }); 124 } 125 return backdrop; 126 } 127 128 function showBackdrop() { 129 var bd = getBackdrop(); 130 if (!bd.parentElement) document.body.appendChild(bd); 131 bd.offsetHeight; 132 bd.classList.add('skp-lang__backdrop--active'); 133 } 134 135 function hideBackdrop() { 136 if (backdrop) { 137 backdrop.classList.remove('skp-lang__backdrop--active'); 138 setTimeout(function () { 139 if (backdrop && backdrop.parentElement && !backdrop.classList.contains('skp-lang__backdrop--active')) { 140 backdrop.parentElement.removeChild(backdrop); 141 } 142 }, 300); 143 } 144 } 145 146 function returnAllDropdowns() { 147 document.querySelectorAll('.skp-lang__dropdown[data-portaled]').forEach(function (dd) { 148 var switcherId = dd.getAttribute('data-portaled'); 149 var sw = document.querySelector('.skp-lang[data-switcher-id="' + switcherId + '"]'); 150 if (sw) { 151 dd.removeAttribute('data-portaled'); 152 dd.classList.remove('skp-lang__dropdown--portaled'); 153 sw.appendChild(dd); 154 } 155 }); 156 } 157 158 /* ============================================================ 159 * Init 160 * ============================================================ */
161 function init() { 162 // The URL is the single source of truth for the displayed language: 163 // every non-EN surface (WPML /el/, GTranslate /fr/ â¦) is path-prefixed, 164 // so a non-prefixed path IS the English page. The cookie only records 165 // the last choice â it must never repaint the toggle against the URL. 166 var urlLang = detectLangFromURL(); 167 var cookieLang = getCookie(); 168 var activeLang = urlLang || 'en'; 169 170 if (urlLang) { 171 setCookie(urlLang); 172 } else if (cookieLang && cookieLang !== 'en') { 173 // Stale cookie from a prefixed page (user stripped /el/, or 174 // clicked the logo from /fr/) â drop it so it can't leak later. 175 clearCookie(); 176 } 177 178 // Update toggle UI if language differs from PHP-rendered default 179 if (activeLang && activeLang !== 'en') { 180 updateAllToggles(activeLang); 181 } 182 183 // Init each switcher instance 184 document.querySelectorAll('.skp-lang').forEach(function (sw, idx) { 185 sw.setAttribute('data-switcher-id', 'sw-' + idx); 186 initSwitcher(sw); 187 }); 188 } 189 190 function initSwitcher(switcher) { 191 var toggle = switcher.querySelector('.skp-lang__toggle'); 192 var dropdown = switcher.querySelector('.skp-lang__dropdown'); 193 if (!toggle || !dropdown) return; 194 195 var switcherId = switcher.getAttribute('data-switcher-id'); 196 197 function portalToBody() { 198 if (!isMobile()) return; 199 dropdown.setAttribute('data-portaled', switcherId); 200 dropdown.classList.add('skp-lang__dropdown--portaled'); 201 document.body.appendChild(dropdown); 202 showBackdrop(); 203 document.body.classList.add('skp-lang-sheet-open'); 204 } 205 206 function returnToSwitcher() { 207 if (dropdown.hasAttribute('data-portaled')) { 208 dropdown.removeAttribute('data-portaled'); 209 dropdown.classList.remove('skp-lang__dropdown--portaled'); 210 switcher.appendChild(dropdown); 211 hideBackdrop(); 212 document.body.classList.remove('skp-lang-sheet-open'); 213 } 214 } 215 216 function open() { 217 document.querySelectorAll('.skp-lang[data-open="true"]').forEach(function (sw) { 218 if (sw !== switcher) { 219 sw.dataset.open = 'false'; 220 var t = sw.querySelector('.skp-lang__toggle'); 221 if (t) t.setAttribute('aria-expanded', 'false'); 222 } 223 }); 224 returnAllDropdowns(); 225 switcher.dataset.open = 'true'; 226 toggle.setAttribute('aria-expanded', 'true'); 227 portalToBody(); 228 } 229 230 function close() { 231 switcher.dataset.open = 'false'; 232 toggle.setAttribute('aria-expanded', 'false'); 233 returnToSwitcher(); 234 } 235 236 function isOpen() { 237 return switcher.dataset.open === 'true'; 238 } 239 240 // Toggle click 241 toggle.addEventListener('click', function (e) { 242 e.stopPropagation(); 243 isOpen() ? close() : open(); 244 }); 245 246 // Option click â construct clean URL (GTranslate mangles hrefs) 247 dropdown.addEventListener('click', function (e) { 248 var opt = e.target.closest('.skp-lang__option'); 249 if (!opt) return; 250 e.preventDefault(); 251 252 var targetLang = opt.getAttribute('data-lang'); 253 if (!targetLang) return; 254 255 setCookie(targetLang); 256 257 // Strip any current language prefix from path 258 var path = window.location.pathname; 259 var allCodes = Object.keys(LANG_MAP); 260 var stripRegex = new RegExp('^/(' + allCodes.join('|') + ')/'); 261 var cleanPath = path.replace(stripRegex, '/'); 262 263 // Build target URL 264 var targetUrl; 265 if (targetLang === 'en') { 266 // EN = no prefix (root) 267 targetUrl = cleanPath; 268 } else { 269 // EL (WPML) or Tier 2 (GTranslate) = /{lang}/path 270 targetUrl = '/' + targetLang + cleanPath; 271 } 272 273 close(); 274 window.location.href = targetUrl; 275 }); 276 277 // Close on outside click 278 document.addEventListener('click', function (e) { 279 if (!e.target.closest('.skp-lang') && !e.target.closest('.skp-lang__dropdown')) { 280 close(); 281 } 282 }); 283 284 // Keyboard: open 285 toggle.addEventListener('keydown', function (e) { 286 if (['ArrowDown', 'Enter', ' '].indexOf(e.key) !== -1) { 287 e.preventDefault(); 288 open(); 289 var first = dropdown.querySelector('.skp-lang__option'); 290 if (first) first.focus(); 291 } 292 }); 293 294 // Keyboard: navigate options 295 dropdown.addEventListener('keydown', function (e) { 296 var opts = Array.prototype.slice.call(dropdown.querySelectorAll('.skp-lang__option')); 297 var idx = opts.indexOf(document.activeElement); 298 299 if (e.key === 'ArrowDown') { 300 e.preventDefault(); 301 if (opts[idx + 1]) opts[idx + 1].focus(); 302 } else if (e.key === 'ArrowUp') { 303 e.preventDefault(); 304 if (idx > 0) opts[idx - 1].focus(); 305 else { close(); toggle.focus(); } 306 } else if (e.key === 'Escape') { 307 e.preventDefault(); 308 close(); 309 toggle.focus(); 310 } else if (e.key === 'Enter') { 311 e.preventDefault(); 312 if (document.activeElement) document.activeElement.click(); 313 } 314 }); 315 316 // Make options focusable 317 var options = dropdown.querySelectorAll('.skp-lang__option'); 318 for (var i = 0; i < options.length; i++) { 319 options[i].setAttribute('tabindex', '-1'); 320 } 321 } 322 323 // Boot 324 if (document.readyState === 'loading') { 325 document.addEventListener('DOMContentLoaded', init); 326 } else { 327 init(); 328 } 329})();
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.