PageSourceSearch

https://bigid.com/wp-content/plugins/translatepress-multilingual/a…trp-frontend-language-switcher.js?ver=3.3.4

js bigid.com collected 2026-09-24 08:28:04 UTC 16,102 bytes, 442 lines download raw bytes

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):not(.trp-ald-language-selector)';
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(OVERLAY);
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.