1document.addEventListener("DOMContentLoaded", function(event) { 2 function trpClearWooCartFragments(){ 3 4 // clear WooCommerce cart fragments when switching language 5 var trp_language_switcher_urls = document.querySelectorAll(".trp-language-switcher-container a:not(.trp-ls-disabled-language), .trp-language-item:not(.trp-language-item__current)"); 6 7 for (var i = 0; i < trp_language_switcher_urls.length; i++) { 8 trp_language_switcher_urls[i].addEventListener("click", function(){ 9 if ( typeof wc_cart_fragments_params !== 'undefined' && typeof wc_cart_fragments_params.fragment_name !== 'undefined' ) { 10 window.sessionStorage.removeItem(wc_cart_fragments_params.fragment_name); 11 } 12 }); 13 } 14 } 15 16 trpClearWooCartFragments(); 17}); 18; 19class BaseSwitcher { 20 constructor(rootEl) { 21 this.root = rootEl; 22 this.list = rootEl?.querySelector('.trp-switcher-dropdown-list') || null; 23 this.isOpen = false; 24 this._pendingFocusOnOpen = false; 25 if (!this.root || !this.list) return; 26 27 if (!this.list.id) { 28 this.list.id = `trp-list-${Math.random().toString(36).slice(2, 9)}`; 29 } 30 31 // Single transitionend handler to drop .is-transitioning and finalize hidden/inert 32 this._onTe = (e) => { 33 if (e.target !== this.list || e.propertyName !== 'max-height') return; 34 35 this.root.classList.remove('is-transitioning'); 36 37 if (!this.isOpen) { 38 this.list.hidden = true; 39 this.list.setAttribute('inert', ''); 40 } else if (this._pendingFocusOnOpen) { 41 this._pendingFocusOnOpen = false; 42 const first = this.list.querySelector( 43 '[role="option"], a, button, [tabindex]:not([tabindex="-1"])' 44 ); 45 first?.focus?.({ preventScroll: true }); 46 } 47 }; 48 this.list.addEventListener('transitionend', this._onTe); 49 50 this.collapse(); 51 this.setAutoWidth(); 52 this.bindKeyboard(); 53 } 54 55 /** 56 * Returns true if the list has a non-zero transition duration (for any property). 57 * We use this to decide whether to rely on `transitionend` or fall back to sync behavior. 58 */ 59 _hasAnimatedTransition() { 60 if (!this.list) return false; 61 62 const cs = getComputedStyle(this.list); 63 const durationsRaw = cs.transitionDuration || ''; 64 65 if (!durationsRaw) return false; 66 67 const durations = durationsRaw 68 .split(',') 69 .map(str => parseFloat(str) || 0); 70 71 return durations.some(d => d > 0); 72 } 73 74 collapse() { 75 this.list.hidden = true; 76 this.list.setAttribute('inert', ''); 77 this.setExpanded(false); 78 this.root.classList.remove('is-transitioning'); 79 } 80 81 /** 82 * If width is set to auto, fix it to the calculated width + 20px 83 * We do this in order to avoid width shift on hover 84 * */ 85 setAutoWidth() { 86 const bonusWidth = 10; 87 88 const cs = getComputedStyle(this.root); 89 const declaredWidth = cs.getPropertyValue('--switcher-width').trim(); 90 91 if (declaredWidth === 'auto' && this.root.querySelector('.trp-language-item-name')) { // In case trp-language-item-name is not present, we are in flags only mode - so no auto width sizing is needed 92 const initialWidth = this.root.getBoundingClientRect().width; 93 94 this.root.style.setProperty('--switcher-width', (initialWidth + bonusWidth) + 'px'); 95 } 96 } 97 98 setExpanded(open) { 99 const trigger = this.root.querySelector('.trp-language-item__current[role="button"]'); 100 const val = String( !!open ); 101 trigger?.setAttribute('aria-expanded', val); 102 this.root.classList.toggle('is-open', !!open); 103 } 104 105 setOpen( open, { source = null } = {} ) { 106 if ( !this.root || !this.list || open === this.isOpen ) return; 107 108 const prefersReduced = window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches;
109 const hasTransition = !prefersReduced && this._hasAnimatedTransition(); 110 111 this.isOpen = open; 112 113 // No transitions (0s duration) OR reduced motion: do everything synchronously, 114 if ( !hasTransition ) { 115 if ( open ) { 116 this.list.hidden = false; 117 this.list.removeAttribute( 'inert' ); 118 this.setExpanded( true ); 119 120 this._pendingFocusOnOpen = ( source?.type === 'keydown' ); 121 if ( this._pendingFocusOnOpen ) { 122 this._pendingFocusOnOpen = false; 123 const first = this.list.querySelector( 124 '[role="option"], a, button, [tabindex]:not([tabindex="-1"])' 125 ); 126 first?.focus?.({ preventScroll: true }); 127 } 128 } else { 129 this.setExpanded( false ); 130 this.list.hidden = true; 131 this.list.setAttribute( 'inert', '' ); 132 this._pendingFocusOnOpen = false; 133 } 134 return; 135 } 136 137 // Animated path: rely on transitionend to remove .is-transitioning 138 if ( open ) { 139 // Must be visible for CSS transition to run 140 this.list.hidden = false; 141 this.list.removeAttribute( 'inert' ); 142 143 this._pendingFocusOnOpen = ( source?.type === 'keydown' ); 144 145 this.root.classList.add( 'is-transitioning' ); 146 // Next frame so browser registers pre-open (max-height: 0) state 147 requestAnimationFrame( () => this.setExpanded( true ) ); 148 } else { 149 this.root.classList.add( 'is-transitioning' ); 150 this.setExpanded( false ); 151 } 152 } 153 154 bindKeyboard() { 155 const trigger = this.root.querySelector('.trp-language-item__current[role="button"]'); 156 if ( !trigger ) return; 157 158 trigger.addEventListener('keydown', (e) => { 159 const inList = !!e.target.closest?.('.trp-switcher-dropdown-list'); 160 161 if ( e.key === 'Enter' || e.key === ' ' ) { 162 e.preventDefault(); 163 this.setOpen(!this.isOpen, { source: e }); 164 return; 165 } 166 167 if ( e.key === 'ArrowDown' && !this.isOpen ) { 168 e.preventDefault(); 169 this.setOpen(true, { source: e }); 170 } 171 172 if ( e.key === 'Escape' ) { 173 this.setOpen(false, { source: e }); 174 trigger.focus?.(); 175 } 176 }); 177 } 178} 179 180class ShortcodeSwitcher extends BaseSwitcher { 181 constructor(wrapper) { 182 const overlay = 183 wrapper.querySelector('.trp-language-switcher.trp-shortcode-overlay') 184 || [...wrapper.querySelectorAll('.trp-language-switcher')] 185 .find(el => el.classList.contains('trp-shortcode-overlay')); 186 187 // Overlay must be interactable; ensure no accidental hidden/inert from server 188 overlay.hidden = false; 189 overlay.removeAttribute('hidden'); 190 overlay.removeAttribute('inert'); 191 if ('inert' in overlay) overlay.inert = false; 192 193 super(overlay); 194 195 if (!this.root || !this.list) return; 196 197 const control = this.root.querySelector('.trp-language-item__current[role="button"]'); 198 if (control && this.list && !control.hasAttribute('aria-controls')) { 199 control.setAttribute('aria-controls', this.list.id); 200 } 201 202 const isClickMode = 203 this.root.classList.contains('trp-open-on-click') || 204 wrapper.dataset.openMode === 'click' || 205 wrapper.classList.contains('trp-open-on-click'); 206 207 if (isClickMode) { 208 // Click anywhere on overlay EXCEPT inside the list 209 this.root.addEventListener('click', (e) => { 210 const inList = e.target.closest('.trp-switcher-dropdown-list'); 211 if (!inList) { 212 e.preventDefault(); 213 e.stopPropagation(); 214 this.setOpen(!this.isOpen, { source: e }); 215 } 216 }, true); 217 218 // Outside click closes 219 this.onDocClick = (evt) => { 220 if (!wrapper.contains(evt.target)) this.setOpen(false, { source: evt }); 221 }; 222 document.addEventListener('click', this.onDocClick, true); 223 224 // Focus leaving wrapper closes 225 wrapper.addEventListener('focusout', () => { 226 setTimeout(() => { 227 if (!wrapper.contains(document.activeElement)) { 228 this.setOpen(false, { source: 'keyboard' }); 229 } 230 }, 0); 231 }); 232 } else { 233 // Hover mode on overlay 234 this.root.addEventListener('mouseenter', (e) => this.setOpen(true, { source: e })); 235 this.root.addEventListener('mouseleave', (e) => this.setOpen(false, { source: e })); 236 } 237 } 238} 239 240class FloaterSwitcher extends BaseSwitcher { 241 constructor(el) { 242 super(el); 243 244 el.addEventListener('mouseenter', (e) => this.setOpen(true, { source: e })); 245 el.addEventListener('mouseleave', (e) => this.setOpen(false, { source: e })); 246 247 this.onDocClick = (evt) => { if (!el.contains(evt.target)) this.setOpen(false, { source: evt }); }; 248 document.addEventListener('click', this.onDocClick, true); 249 } 250} 251 252document.addEventListener('DOMContentLoaded', () => { 253 // Front-end or classic admin 254 initLanguageSwitchers(document); 255 256 // If no wrapper and we are in Gutenberg, watch for async SSR insert 257 if ( 258 inGutenberg() &&
259 !getEditorDoc().querySelector(WRAPPER) 260 ) { 261 observeWrapperUntilFound(); 262 } 263 264 if ( !inGutenberg() ) 265 observeShortcodeSwitcher(); 266}); 267 268/** For shortcode switcher 269 * Mark the shortcodes that were initialized 270 * */ 271const TRP_BOUND = new WeakSet(); 272const mark = (el) => TRP_BOUND.add(el); 273const isMarked = (el) => TRP_BOUND.has(el); 274 275const WRAPPER = '.trp-shortcode-switcher__wrapper'; 276const OVERLAY = '.trp-language-switcher:not(.trp-opposite-button):not(.trp-ald-language-selector)'; 277 278// Helpers 279function inGutenberg() { 280 return document.body?.classList?.contains('block-editor-page') 281 || !!(window.wp?.data?.select?.('core/block-editor')); 282} 283 284// If editor uses an iframe canvas, work inside it 285function getEditorDoc() { 286 const ifr = document.querySelector('iframe[name="editor-canvas"], .editor-canvas__iframe'); 287 288 return (ifr && ifr.contentDocument) ? ifr.contentDocument : document; 289} 290 291function initLanguageSwitchers(root = document) { 292 const floater = root.querySelector( 293 '.trp-language-switcher.trp-ls-dropdown:not(.trp-shortcode-switcher):not(.trp-opposite-language)' 294 ); 295 296 if (floater) 297 new FloaterSwitcher(floater); 298 299 root.querySelectorAll(WRAPPER) 300 .forEach(wrapper => { 301 const overlay = wrapper.querySelector(OVERLAY); 302 303 if (overlay && !isMarked(overlay)) { 304 mark(overlay); 305 new ShortcodeSwitcher(wrapper); 306 } 307 }); 308} 309 310/** 311 * Observes the document for dynamically inserted shortcode switchers and initializes them automatically when detected. 312 */ 313function observeShortcodeSwitcher() { 314 const initWrapper = ( wrapper ) => { 315 if ( !wrapper ) 316 return; 317 318 const overlay = wrapper.querySelector( OVERLAY ); 319 320 if ( !overlay || isMarked( overlay ) ) 321 return; 322 323 mark( overlay ); 324 325 new ShortcodeSwitcher( wrapper ); 326 } 327 328 const mo = new MutationObserver( ( mutations ) => { 329 for ( const m of mutations ) { 330 for ( const n of m.addedNodes ) { 331 if ( n.nodeType !== 1 ) 332 continue; 333 334 if ( n.matches?.( WRAPPER ) ) 335 initWrapper( n ); 336 337 n.querySelectorAll?.( WRAPPER ).forEach( initWrapper ); 338 } 339 } 340 }); 341 342 mo.observe( document, { childList: true, subtree: true } ); 343} 344 345/** 346 * Observe Gutenberg for the shortcode wrapper being inserted asynchronously. 347 * 348 * Supports both Blocks API v2 (no editor iframe; wrapper appears in the outer document) 349 * and Blocks API v3 (editor content rendered inside an iframe canvas). 350 * 351 * Strategy: 352 * 1) Check the current editor document for `.trp-shortcode-switcher__wrapper` and init immediately. 353 * 2) If an editor canvas iframe exists, watch its document (and reattach on iframe load) for the wrapper. 354 * 3) If no iframe yet, watch the outer document for either the iframe (v3) or the wrapper itself (v2). 355 * 356 * Initialization is performed once per context to avoid duplicate bindings. 357 */ 358function observeWrapperUntilFound() { 359 // If wrapper already exists in current editor doc, init 360 const edDoc = getEditorDoc(); 361 const existing = edDoc.querySelector(WRAPPER); 362 363 if ( existing ) { 364 initLanguageSwitchers( edDoc ); 365 return; 366 } 367 368 // Helper to locate the editor canvas iframe in the OUTER document 369 const findCanvasIframe = () => document.querySelector('iframe[name="editor-canvas"], .editor-canvas__iframe'); 370 371 // If iframe is already present in the outer doc, start watching inside it 372 const iframeNow = findCanvasIframe(); 373 if ( iframeNow ) { 374 watchIframe( iframeNow ); 375 return; 376 } 377 378 // Otherwise, observe the OUTER document until the iframe appears 379 const outerMO = new MutationObserver( ( mutations ) => { 380 for ( const m of mutations ) { 381 for ( const n of m.addedNodes ) { 382 if ( n.nodeType !== 1 ) continue; 383 384 const iframe = 385 n.matches?.('iframe[name="editor-canvas"], .editor-canvas__iframe') 386 ? n 387 : n.querySelector?.('iframe[name="editor-canvas"], .editor-canvas__iframe'); 388 389 if ( iframe ) { 390 outerMO.disconnect(); 391 watchIframe( iframe ); 392 return; 393 } 394 395 // Also catch shortcode wrapper added directly to the outer document (API v2, no iframe) 396 const wrapper = 397 n.matches?.(WRAPPER) 398 ? n 399 : n.querySelector?.(WRAPPER); 400 401 if ( wrapper ) { 402 outerMO.disconnect(); 403 initLanguageSwitchers( document ); 404 return; 405 } 406 407 } 408 } 409 } ); 410 outerMO.observe( document, { childList: true, subtree: true } ); 411 412 function watchIframe( iframe ) { 413 // Try immediately (some builds inject srcdoc synchronously) 414 tryAttachInside(); 415 416 // Also on load/navigate (Gutenberg may reload the canvas) 417 iframe.addEventListener( 'load', tryAttachInside ); 418 419 function tryAttachInside() { 420 let doc; 421 try { 422 doc = iframe.contentDocument || iframe.contentWindow?.document; 423 } catch (e) { 424 console.warn('Cannot access iframe content due to cross-origin restrictions', e); 425 return; 426 } 427 if ( !doc ) return; 428 429 // If wrapper is already there, init once and stop.
430 const hit = doc.querySelector(WRAPPER); 431 if ( hit ) { 432 initLanguageSwitchers( doc ); 433 return; 434 } 435 436 // Observe INSIDE the iframe until wrapper appears 437 const innerMO = new MutationObserver( ( muts ) => { 438 for ( const mm of muts ) { 439 for ( const nn of mm.addedNodes ) { 440 if ( nn.nodeType !== 1 ) continue; 441 if ( 442 nn.matches?.(WRAPPER) || 443 nn.querySelector?.(WRAPPER) 444 ) { 445 innerMO.disconnect(); 446 initLanguageSwitchers( doc ); 447 return; 448 } 449 } 450 } 451 if ( doc.querySelector(WRAPPER) ) { 452 innerMO.disconnect(); 453 initLanguageSwitchers( doc ); 454 } 455 } ); 456 457 innerMO.observe( doc, { childList: true, subtree: true } ); 458 } 459 } 460};
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.