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