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 35 this.bindKeyboard(this.root); 36 } 37 38 collapse() { 39 this.list.hidden = true; 40 this.list.setAttribute('inert', ''); 41 this.setExpanded(false); 42 this.root.classList.remove('is-transitioning'); 43 } 44 45 /** 46 * If width is set to auto, fix it to the calculated width + 20px 47 * We do this in order to avoid width shift on hover 48 * */ 49 setAutoWidth() { 50 const bonusWidth = 20; 51 52 const cs = getComputedStyle(this.root); 53 const declaredWidth = cs.getPropertyValue('--switcher-width').trim(); 54 55 if (declaredWidth === 'auto') { 56 const initialWidth = this.root.getBoundingClientRect().width; 57 58 this.root.style.setProperty('--switcher-width', (initialWidth + bonusWidth) + 'px'); 59 } 60 } 61 62 setExpanded(open) { 63 const val = String(!!open); 64 this.root.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.setExpanded(false); 96 this.list.hidden = true; 97 this.list.setAttribute('inert', ''); 98 this.root.classList.remove('is-transitioning'); 99 } else { 100 this.root.classList.add('is-transitioning'); 101 // Removing is-open triggers CSS max-height â 0 animation 102 this.setExpanded(false); 103 } 104 } 105 } 106 107 bindKeyboard(target) { 108 target.addEventListener('keydown', (e) => { 109 if (e.key === 'Enter' || e.key === ' ') { 110 e.preventDefault(); 111 this.setOpen(!this.isOpen, { source: e }); 112 } 113 if (e.key === 'Escape') { 114 this.setOpen(false, { source: e });
115 target.focus?.(); 116 } 117 }); 118 } 119} 120 121class ShortcodeSwitcher extends BaseSwitcher { 122 constructor(wrapper) { 123 const overlay = 124 wrapper.querySelector('.trp-language-switcher.trp-shortcode-overlay') 125 || [...wrapper.querySelectorAll('.trp-language-switcher')] 126 .find(el => el.classList.contains('trp-shortcode-overlay')); 127 128 // Overlay must be interactable; ensure no accidental hidden/inert from server 129 overlay.hidden = false; 130 overlay.removeAttribute('hidden'); 131 overlay.removeAttribute('inert'); 132 if ('inert' in overlay) overlay.inert = false; 133 134 super(overlay); 135 if (!this.root || !this.list) return; 136 137 // ARIA on overlay (focusable container) 138 this.root.setAttribute('role', 'listbox'); 139 this.root.setAttribute('aria-haspopup', 'listbox'); 140 this.root.setAttribute('aria-controls', this.list.id); 141 if (!this.root.hasAttribute('tabindex')) this.root.setAttribute('tabindex', '0'); 142 143 const isClickMode = 144 this.root.classList.contains('trp-open-on-click') || 145 wrapper.dataset.openMode === 'click' || 146 wrapper.classList.contains('trp-open-on-click'); 147 148 if (isClickMode) { 149 // Click anywhere on overlay EXCEPT inside the list 150 this.root.addEventListener('click', (e) => { 151 const inList = e.target.closest('.trp-switcher-dropdown-list'); 152 if (!inList) { 153 e.preventDefault(); 154 e.stopPropagation(); 155 this.setOpen(!this.isOpen, { source: e }); 156 } 157 }, true); 158 159 // Outside click closes 160 this.onDocClick = (evt) => { 161 if (!wrapper.contains(evt.target)) this.setOpen(false, { source: evt }); 162 }; 163 document.addEventListener('click', this.onDocClick, true); 164 165 // Focus leaving wrapper closes 166 wrapper.addEventListener('focusout', () => { 167 setTimeout(() => { 168 if (!wrapper.contains(document.activeElement)) { 169 this.setOpen(false, { source: 'keyboard' }); 170 } 171 }, 0); 172 }); 173 } else { 174 // Hover mode on overlay 175 this.root.addEventListener('mouseenter', (e) => this.setOpen(true, { source: e })); 176 this.root.addEventListener('mouseleave', (e) => this.setOpen(false, { source: e })); 177 } 178 } 179} 180 181class FloaterSwitcher extends BaseSwitcher { 182 constructor(el) { 183 super(el); 184 185 el.addEventListener('mouseenter', (e) => this.setOpen(true, { source: e })); 186 el.addEventListener('mouseleave', (e) => this.setOpen(false, { source: e })); 187 188 this.onDocClick = (evt) => { if (!el.contains(evt.target)) this.setOpen(false, { source: evt }); }; 189 document.addEventListener('click', this.onDocClick, true); 190 } 191} 192 193document.addEventListener('DOMContentLoaded', () => { 194 // Front-end or classic admin 195 initLanguageSwitchers(document); 196 197 // If no wrapper and we are in Gutenberg, watch for async SSR insert 198 if ( 199 inGutenberg() && 200 !getEditorDoc().querySelector('.trp-shortcode-switcher__wrapper') 201 ) { 202 observeWrapperUntilFound(); 203 } 204}); 205 206/** For shortcode switcher 207 * Mark the shortcodes that were initialized 208 * 209 * */ 210const TRP_BOUND = new WeakSet(); 211const mark = (el) => TRP_BOUND.add(el); 212const isMarked = (el) => TRP_BOUND.has(el); 213 214function inGutenberg() { 215 return document.body?.classList?.contains('block-editor-page') 216 || !!(window.wp?.data?.select?.('core/block-editor')); 217} 218 219// If editor uses an iframe canvas, work inside it 220function getEditorDoc() { 221 const ifr = document.querySelector('iframe[name="editor-canvas"], .editor-canvas__iframe'); 222 223 return (ifr && ifr.contentDocument) ? ifr.contentDocument : document; 224} 225 226function initLanguageSwitchers(root = document) { 227 root.querySelectorAll('.trp-language-switcher.trp-ls-dropdown:not(.trp-shortcode-switcher):not(.trp-opposite-language)') 228 .forEach(el => { if (!isMarked(el)) { mark(el); new FloaterSwitcher(el); } }); 229
230 root.querySelectorAll('.trp-shortcode-switcher__wrapper') 231 .forEach(wrapper => { 232 const overlay = wrapper.querySelector('.trp-language-switcher:not(.trp-opposite-button)'); 233 234 if (overlay && !isMarked(overlay)) { 235 mark(overlay); 236 new ShortcodeSwitcher(wrapper); 237 } 238 }); 239} 240 241function observeWrapperUntilFound() { 242 const edDoc = getEditorDoc(); 243 244 // If it already exists, init and stop. 245 if (edDoc.querySelector('.trp-shortcode-switcher__wrapper')) { 246 initLanguageSwitchers(edDoc); 247 248 return; 249 } 250 251 const mo = new MutationObserver((mutations) => { 252 for (const m of mutations) { 253 for (const n of m.addedNodes) { 254 if (!(n instanceof Element)) continue; 255 256 if ( 257 n.matches?.('.trp-shortcode-switcher__wrapper') || 258 n.querySelector?.('.trp-shortcode-switcher__wrapper') 259 ) { 260 initLanguageSwitchers(edDoc); 261 } 262 } 263 } 264 }); 265 266 mo.observe(edDoc, { childList: true, subtree: true }); 267} 268
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.