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