PageSourceSearch

https://www.sridianti.com/wp-content/themes/blogsite/assets/js/index.js

js sridianti.com collected 2026-10-02 13:22:42 UTC 30,588 bytes, 818 lines download raw bytes

1
2/*  -----------------------------------------------------------------------------------------------
3    Namespace
4--------------------------------------------------------------------------------------------------- */
5
6var blogsite = blogsite || {};
7
8// Set a default value for scrolled.
9blogsite.scrolled = 0;
10
11// polyfill closest
12// https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
13if ( ! Element.prototype.closest ) {
14    Element.prototype.closest = function( s ) {
15        var el = this;
16
17        do {
18            if ( el.matches( s ) ) {
19                return el;
20            }
21
22            el = el.parentElement || el.parentNode;
23        } while ( el !== null && el.nodeType === 1 );
24
25        return null;
26    };
27}
28
29// polyfill forEach
30// https://developer.mozilla.org/en-US/docs/Web/API/NodeList/forEach#Polyfill
31if ( window.NodeList && ! NodeList.prototype.forEach ) {
32    NodeList.prototype.forEach = function( callback, thisArg ) {
33        var i;
34        var len = this.length;
35
36        thisArg = thisArg || window;
37
38        for ( i = 0; i < len; i++ ) {
39            callback.call( thisArg, this[ i ], i, this );
40        }
41    };
42}
43
44// event "polyfill"
45blogsite.createEvent = function( eventName ) {
46    var event;
47    if ( typeof window.Event === 'function' ) {
48        event = new Event( eventName );
49    } else {
50        event = document.createEvent( 'Event' );
51        event.initEvent( eventName, true, false );
52    }
53    return event;
54};
55
56// matches "polyfill"
57// https://developer.mozilla.org/es/docs/Web/API/Element/matches
58if ( ! Element.prototype.matches ) {
59    Element.prototype.matches =
60        Element.prototype.matchesSelector ||
61        Element.prototype.mozMatchesSelector ||
62        Element.prototype.msMatchesSelector ||
63        Element.prototype.oMatchesSelector ||
64        Element.prototype.webkitMatchesSelector ||
65        function( s ) {
66            var matches = ( this.document || this.ownerDocument ).querySelectorAll( s ),
67                i = matches.length;
68            while ( --i >= 0 && matches.item( i ) !== this ) {}
69            return i > -1;
70        };
71}
72
73// Add a class to the body for when touch is enabled for browsers that don't support media queries
74// for interaction media features. Adapted from <https://codepen.io/Ferie/pen/vQOMmO>.
75blogsite.touchEnabled = {
76
77    init: function() {
78        var matchMedia = function() {
79            // Include the 'heartz' as a way to have a non-matching MQ to help terminate the join. See <https://git.io/vznFH>.
80            var prefixes = [ '-webkit-', '-moz-', '-o-', '-ms-' ];
81            var query = [ '(', prefixes.join( 'touch-enabled),(' ), 'heartz', ')' ].join( '' );
82            return window.matchMedia && window.matchMedia( query ).matches;
83        };
84
85        if ( ( 'ontouchstart' in window ) || ( window.DocumentTouch && document instanceof window.DocumentTouch ) || matchMedia() ) {
86            document.body.classList.add( 'touch-enabled' );
87        }
88    }
89}; // blogsite.touchEnabled
90
91/*  -----------------------------------------------------------------------------------------------
92    Cover Modals
93--------------------------------------------------------------------------------------------------- */
94
95blogsite.coverModals = {
96
97    init: function() {
98        if ( document.querySelector( '.cover-modal' ) ) {
99            // Handle cover modals when they're toggled.
100            this.onToggle();
101
102            // When toggled, untoggle if visitor clicks on the wrapping element of the modal.
103            this.outsideUntoggle();
104
105            // Close on escape key press.
106            this.closeOnEscape();
107
108            // Hide and show modals before and after their animations have played out.
109            this.hideAndShowModals();
110        }
111    },
112
113    // Handle cover modals when they're toggled.
114    onToggle: function() {
115        document.querySelectorAll( '.cover-modal' ).forEach( function( element ) {
116            element.addEventListener( 'toggled', function( event ) {
117                var modal = event.target,
118                    body = document.body;
119
120                if ( modal.classList.contains( 'active' ) ) {
121                    body.classList.add( 'showing-modal' );
122                } else {
123                    body.classList.remove( 'showing-modal' );
124                    body.classList.add( 'hiding-modal' );
125
126                    // Remove the hiding class after a delay, when animations have been run.
127                    setTimeout( function() {
128                        body.classList.remove( 'hiding-modal' );
129                    }, 500 );
130                }
131            } );
132        } );
133    },
134
135    // Close modal on outside click.
136    outsideUntoggle: function() {
137        document.addEventListener( 'click', function( event ) {
138            var target = event.target;
139            var modal = document.querySelector( '.cover-modal.active' );
140
141            // if target onclick is <a> with # within the href attribute
142            if ( event.target.tagName.toLowerCase() === 'a' && event.target.hash.includes( '#' ) && modal !== null ) {
143                // untoggle the modal
144                this.untoggleModal( modal );
145                // wait 550 and scroll to the anchor
146                setTimeout( function() {
147                    var anchor = document.getElementById( event.target.hash.slice( 1 ) );
148                    anchor.scrollIntoView();
149                }, 550 );
150            }
151
152            if ( target === modal ) {
153                this.untoggleModal( target );
154            }
155        }.bind( this ) );
156    },
157
158    // Close modal on escape key press.
159    closeOnEscape: function() {
160        document.addEventListener( 'keydown', function( event ) {
161            if ( event.keyCode === 27 ) {
162                event.preventDefault();
163                document.querySelectorAll( '.cover-modal.active' ).forEach( function( element ) {
164                    this.untoggleModal( element );
165                }.bind( this ) );
166            }
167        }.bind( this ) );
168    },
169
170    // Hide and show modals before and after their animations have played out.
171    hideAndShowModals: function() {
172        var _doc = document,
173            _win = window,
174            modals = _doc.querySelectorAll( '.cover-modal' ),
175            htmlStyle = _doc.documentElement.style,
176            adminBar = _doc.querySelector( '#wpadminbar' );
177
178        function getAdminBarHeight( negativeValue ) {
179            var height,
180                currentScroll = _win.pageYOffset;
181
182            if ( adminBar ) {
183                height = currentScroll + adminBar.getBoundingClientRect().height;
184
185                return negativeValue ? -height : height;
186            }
187
188            return currentScroll === 0 ? 0 : -currentScroll;
189        }
190
191        function htmlStyles() {
192            var overflow = _win.innerHeight > _doc.documentElement.getBoundingClientRect().height;
193
194            return {
195                'overflow-y': overflow ? 'hidden' : 'scroll',
196                position: 'fixed',
197                width: '100%',
198                top: getAdminBarHeight( true ) + 'px',
199                left: 0
200            };
201        }
202
203        // Show the modal.
204        modals.forEach( function( modal ) {
205            modal.addEventListener( 'toggle-target-before-inactive', function( event ) {
206                var styles = htmlStyles(),
207                    offsetY = _win.pageYOffset,
208                    paddingTop = ( Math.abs( getAdminBarHeight() ) - offsetY ) + 'px',
209                    mQuery = _win.matchMedia( '(max-width: 600px)' );
210
211                if ( event.target !== modal ) {
212                    return;
213                }
214
215                Object.keys( styles ).forEach( function( styleKey ) {
216                    htmlStyle.setProperty( styleKey, styles[ styleKey ] );
217                } );
218
219                _win.blogsite.scrolled = parseInt( styles.top, 10 );
220
221                if ( adminBar ) {
222                    _doc.body.style.setProperty( 'padding-top', paddingTop );
223
224                    if ( mQuery.matches ) {
225                        if ( offsetY >= getAdminBarHeight() ) {
226                            modal.style.setProperty( 'top', 0 );
227                        } else {
228                            modal.style.setProperty( 'top', ( getAdminBarHeight() - offsetY ) + 'px' );
229                        }
230                    }
231                }
232
233                modal.classList.add( 'show-modal' );
234            } );
235
236            // Hide the modal after a delay, so animations have time to play out.
237            modal.addEventListener( 'toggle-target-after-inactive', function( event ) {
238                if ( event.target !== modal ) {
239                    return;
240                }
241
242                setTimeout( function() {
243                    var clickedEl = blogsite.toggles.clickedEl;
244
245                    modal.classList.remove( 'show-modal' );
246
247                    Object.keys( htmlStyles() ).forEach( function( styleKey ) {
248                        htmlStyle.removeProperty( styleKey );
249                    } );
250
251                    if ( adminBar ) {
252                        _doc.body.style.removeProperty( 'padding-top' );
253                        modal.style.removeProperty( 'top' );
254                    }
255
256                    if ( clickedEl !== false ) {
257                        clickedEl.focus();
258                        clickedEl = false;
259                    }
260
261                    _win.scrollTo( 0, Math.abs( _win.blogsite.scrolled + getAdminBarHeight() ) );
262
263                    _win.blogsite.scrolled = 0;
264                }, 500 );
265            } );
266        } );
267    },
268
269    // Untoggle a modal.
270    untoggleModal: function( modal ) {
271        var modalTargetClass,
272            modalToggle = false;
273
274        // If the modal has specified the string (ID or class) used by toggles to target it, untoggle the toggles with that target string.
275        // The modal-target-string must match the string toggles use to target the modal.
276        if ( modal.dataset.modalTargetString ) {
277            modalTargetClass = modal.dataset.modalTargetString;
278
279            modalToggle = document.querySelector( '*[data-toggle-target="' + modalTargetClass + '"]' );
280        }
281
282        // If a modal toggle exists, trigger it so all of the toggle options are included.
283        if ( modalToggle ) {
284            modalToggle.click();
285
286            // If one doesn't exist, just hide the modal.
287        } else {
288            modal.classList.remove( 'active' );
289        }
290    }
291
292}; // blogsite.coverModals
293
294/*  -----------------------------------------------------------------------------------------------
295    Intrinsic Ratio Embeds
296--------------------------------------------------------------------------------------------------- */
297
298blogsite.intrinsicRatioVideos = {
299
300    init: function() {
301        this.makeFit();
302
303        window.addEventListener( 'resize', function() {
304            this.makeFit();
305        }.bind( this ) );
306    },
307
308    makeFit: function() {
309        document.querySelectorAll( 'iframe, object, video' ).forEach( function( video ) {
310            var ratio, iTargetWidth,
311                container = video.parentNode;
312
313            // Skip videos we want to ignore.
314            if ( video.classList.contains( 'intrinsic-ignore' ) || video.parentNode.classList.contains( 'intrinsic-ignore' ) ) {
315                return true;
316            }
317
318            if ( ! video.dataset.origwidth ) {
319                // Get the video element proportions.
320                video.setAttribute( 'data-origwidth', video.width );
321                video.setAttribute( 'data-origheight', video.height );
322            }
323
324            iTargetWidth = container.offsetWidth;
325
326            // Get ratio from proportions.
327            ratio = iTargetWidth / video.dataset.origwidth;
328
329            // Scale based on ratio, thus retaining proportions.
330            video.style.width = iTargetWidth + 'px';
331            video.style.height = ( video.dataset.origheight * ratio ) + 'px';
332        } );
333    }
334
335}; // blogsite.instrinsicRatioVideos
336
337/*  -----------------------------------------------------------------------------------------------
338    Modal Menu
339--------------------------------------------------------------------------------------------------- */
340blogsite.modalMenu = {
341
342    init: function() {
343        // If the current menu item is in a sub level, expand all the levels higher up on load.
344        this.expandLevel();
345        this.keepFocusInModal();
346    },
347
348    expandLevel: function() {
349        var modalMenus = document.querySelectorAll( '.modal-menu' );
350
351        modalMenus.forEach( function( modalMenu ) {
352            var activeMenuItem = modalMenu.querySelector( '.current-menu-item' );
353
354            if ( activeMenuItem ) {
355                blogsiteFindParents( activeMenuItem, 'li' ).forEach( function( element ) {
356                    var subMenuToggle = element.querySelector( '.sub-menu-toggle' );
357                    if ( subMenuToggle ) {
358                        blogsite.toggles.performToggle( subMenuToggle, true );
359                    }
360                } );
361            }
362        } );
363    },
364
365    keepFocusInModal: function() {
366        var _doc = document;
367
368        _doc.addEventListener( 'keydown', function( event ) {
369            var toggleTarget, modal, selectors, elements, menuType, bottomMenu, activeEl, lastEl, firstEl, tabKey, shift
369Key,
370                clickedEl = blogsite.toggles.clickedEl;
371
372            if ( clickedEl && _doc.body.classList.contains( 'showing-modal' ) ) {
373                toggleTarget = clickedEl.dataset.toggleTarget;
374                selectors = 'input, a, button';
375                modal = _doc.querySelector( toggleTarget );
376
377                elements = modal.querySelectorAll( selectors );
378                elements = Array.prototype.slice.call( elements );
379
380                if ( '.menu-modal' === toggleTarget ) {
381                    menuType = window.matchMedia( '(min-width: 1000px)' ).matches;
382                    menuType = menuType ? '.expanded-menu' : '.mobile-menu';
383
384                    elements = elements.filter( function( element ) {
385                        return null !== element.closest( menuType ) && null !== element.offsetParent;
386                    } );
387
388                    elements.unshift( _doc.querySelector( '.close-nav-toggle' ) );
389
390                    bottomMenu = _doc.querySelector( '.menu-bottom > nav' );
391
392                    if ( bottomMenu ) {
393                        bottomMenu.querySelectorAll( selectors ).forEach( function( element ) {
394                            elements.push( element );
395                        } );
396                    }
397                }
398
399                lastEl = elements[ elements.length - 1 ];
400                firstEl = elements[0];
401                activeEl = _doc.activeElement;
402                tabKey = event.keyCode === 9;
403                shiftKey = event.shiftKey;
404
405                if ( ! shiftKey && tabKey && lastEl === activeEl ) {
406                    event.preventDefault();
407                    firstEl.focus();
408                }
409
410                if ( shiftKey && tabKey && firstEl === activeEl ) {
411                    event.preventDefault();
412                    lastEl.focus();
413                }
414            }
415        } );
416    }
417}; // blogsite.modalMenu
418
419/*  -----------------------------------------------------------------------------------------------
420    Primary Menu
421--------------------------------------------------------------------------------------------------- */
422
423blogsite.primaryMenu = {
424
425    init: function() {
426        this.focusMenuWithChildren();
427    },
428
429    // The focusMenuWithChildren() function implements Keyboard Navigation in the Primary Menu
430    // by adding the '.focus' class to all 'li.menu-item-has-children' when the focus is on the 'a' element.
431    focusMenuWithChildren: function() {
432        // Get all the link elements within the primary menu.
433        var links, i, len,
434            menu = document.querySelector( '.primary-menu-wrapper' );
435
436        if ( ! menu ) {
437            return false;
438        }
439
440        links = menu.getElementsByTagName( 'a' );
441
442        // Each time a menu link is focused or blurred, toggle focus.
443        for ( i = 0, len = links.length; i < len; i++ ) {
444            links[i].addEventListener( 'focus', toggleFocus, true );
445            links[i].addEventListener( 'blur', toggleFocus, true );
446        }
447
448        //Sets or removes the .focus class on an element.
449        function toggleFocus() {
450            var self = this;
451
452            // Move up through the ancestors of the current link until we hit .primary-menu.
453            while ( -1 === self.className.indexOf( 'primary-menu' ) ) {
454                // On li elements toggle the class .focus.
455                if ( 'li' === self.tagName.toLowerCase() ) {
456                    if ( -1 !== self.className.indexOf( 'focus' ) ) {
457                        self.className = self.className.replace( ' focus', '' );
458                    } else {
459                        self.className += ' focus';
460                    }
461                }
462                self = self.parentElement;
463            }
464        }
465    }
466}; // blogsite.primaryMenu
467
468/*  -----------------------------------------------------------------------------------------------
469    Toggles
470--------------------------------------------------------------------------------------------------- */
471
472blogsite.toggles = {
473
474    clickedEl: false,
475
476    init: function() {
477        // Do the toggle.
478        this.toggle();
479
480        // Check for toggle/untoggle on resize.
481        this.resizeCheck();
482
483        // Check for untoggle on escape key press.
484        this.untoggleOnEscapeKeyPress();
485    },
486
487    performToggle: function( element, instantly ) {
488        var target, timeOutTime, classToToggle,
489            self = this,
490            _doc = document,
491            // Get our targets.
492            toggle = element,
493            targetString = toggle.dataset.toggleTarget,
494            activeClass = 'active';
495
496        // Elements to focus after modals are closed.
497        if ( ! _doc.querySelectorAll( '.show-modal' ).length ) {
498            self.clickedEl = _doc.activeElement;
499        }
500
501        if ( targetString === 'next' ) {
502            target = toggle.nextSibling;
503        } else {
504            target = _doc.querySelector( targetString );
505        }
506
507        // Trigger events on the toggle targets before they are toggled.
508        if ( target.classList.contains( activeClass ) ) {
509            target.dispatchEvent( blogsite.createEvent( 'toggle-target-before-active' ) );
510        } else {
511            target.dispatchEvent( blogsite.createEvent( 'toggle-target-before-inactive' ) );
512        }
513
514        // Get the class to toggle, if specified.
515        classToToggle = toggle.dataset.classToToggle ? toggle.dataset.classToToggle : activeClass;
516
517        // For cover modals, set a short timeout duration so the class animations have time to play out.
518        timeOutTime = 0;
519
520        if ( target.classList.contains( 'cover-modal' ) ) {
521            timeOutTime = 10;
522        }
523
524        setTimeout( function() {
525            var focusElement,
526                subMenued = target.classList.contains( 'sub-menu' ),
527                newTarget = subMenued ? toggle.closest( '.menu-item' ).querySelector( '.sub-menu' ) : target,
528                duration = toggle.dataset.toggleDuration;
529
530            // Toggle the target of the clicked toggle.
531            if ( toggle.dataset.toggleType === 'slidetoggle' && ! instantly && duration !== '0' ) {
532                blogsiteMenuToggle( newTarget, duration );
533            } else {
534                newTarget.classList.toggle( classToToggle );
535            }
536
537            // If the toggle target is 'next', only give the clicked toggle the active class.
538            if ( targetString === 'next' ) {
539                toggle.classList.toggle( activeClass );
540            } else if ( target.classList.contains( 'sub-menu' ) ) {
541                toggle.classList.toggle( activeClass );
542            } else {
543                // If not, toggle all toggles with this toggle target.
544                _doc.querySelector( '*[data-toggle-target="' + targetString + '"]' ).classList.toggle( activeClass );
545            }
546
547            // Toggle aria-expanded on the toggle.
548            blogsiteToggleAttribute( toggle, 'aria-expanded', 'true', 'false' );
549
550            if ( self.clickedEl && -1 !== toggle.getAttribute( 'class' ).indexOf( 'close-' ) ) {
551                blogsiteToggleAttribute( self.clickedEl, 'aria-expanded', 'true', 'false' );
552            }
553
554            // Toggle body class.
555            if ( toggle.dataset.toggleBodyClass ) {
556                _doc.body.classList.toggle( toggle.dataset.toggleBodyClass );
557            }
558
559            // Check whether to set focus.
560            if ( toggle.dataset.setFocus ) {
561                focusElement = _doc.querySelector( toggle.dataset.setFocus );
562
563                if ( focusElement ) {
564                    if ( target.classList.contains( activeClass ) ) {
565                        focusElement.focus();
566                    } else {
567                        focusElement.blur();
568                    }
569                }
570            }
571
572            // Trigger the toggled event on the toggle target.
573            target.dispatchEvent( blogsite.createEvent( 'toggled' ) );
574
575            // Trigger events on the toggle targets after they are toggled.
576            if ( target.classList.contains( activeClass ) ) {
577                target.dispatchEvent( blogsite.createEvent( 'toggle-target-after-active' ) );
578            } else {
579                target.dispatchEvent( blogsite.createEvent( 'toggle-target-after-inactive' ) );
580            }
581        }, timeOutTime );
582    },
583
584    // Do the toggle.
585    toggle: function() {
586        var self = this;
587
588        document.querySelectorAll( '*[data-toggle-target]' ).forEach( function( element ) {
589            element.addEventListener( 'click', function( event ) {
590                event.preventDefault();
591                self.performToggle( element );
592            } );
593        } );
594    },
595
596    // Check for toggle/untoggle on screen resize.
597    resizeCheck: function() {
598        if ( document.querySelectorAll( '*[data-untoggle-above], *[data-untoggle-below], *[data-toggle-above], *[data-toggle-below]' ).length ) {
599            window.addEventListener( 'resize', function() {
600                var winWidth = window.innerWidth,
601                    toggles = document.querySelectorAll( '.toggle' );
602
603                toggles.forEach( function( toggle ) {
604                    var unToggleAbove = toggle.dataset.untoggleAbove,
605                        unToggleBelow = toggle.dataset.untoggleBelow,
606                        toggleAbove = toggle.dataset.toggleAbove,
607                        toggleBelow = toggle.dataset.toggleBelow;
608
609                    // If no width comparison is set, continue.
610                    if ( ! unToggleAbove && ! unToggleBelow && ! toggleAbove && ! toggleBelow ) {
611                        return;
612                    }
613
614                    // If the toggle width comparison is true, toggle the toggle.
615                    if (
616                        ( ( ( unToggleAbove && winWidth > unToggleAbove ) ||
617                            ( unToggleBelow && winWidth < unToggleBelow ) ) &&
618                            toggle.classList.contains( 'active' ) ) ||
619                        ( ( ( toggleAbove && winWidth > toggleAbove ) ||
620                            ( toggleBelow && winWidth < toggleBelow ) ) &&
621                            ! toggle.classList.contains( 'active' ) )
622                    ) {
623                        toggle.click();
624                    }
625                } );
626            } );
627        }
628    },
629
630    // Close toggle on escape key press.
631    untoggleOnEscapeKeyPress: function() {
632        document.addEventListener( 'keyup', function( event ) {
633            if ( event.key === 'Escape' ) {
634                document.querySelectorAll( '*[data-untoggle-on-escape].active' ).forEach( function( element ) {
635                    if ( element.classList.contains( 'active' ) ) {
636                        element.click();
637                    }
638                } );
639            }
640        } );
641    }
642
643}; // blogsite.toggles
644
645/**
646 * Is the DOM ready?
647 *
648 * This implementation is coming from https://gomakethings.com/a-native-javascript-equivalent-of-jquerys-ready-method/
649 *
650 * @param {Function} fn Callback function to run.
651 */
652function blogsiteDomReady( fn ) {
653    if ( typeof fn !== 'function' ) {
654        return;
655    }
656
657    if ( document.readyState === 'interactive' || document.readyState === 'complete' ) {
658        return fn();
659    }
660
661    document.addEventListener( 'DOMContentLoaded', fn, false );
662}
663
664blogsiteDomReady( function() {
665    blogsite.toggles.init();              // Handle toggles.
666    blogsite.coverModals.init();          // Handle cover modals.
667    blogsite.intrinsicRatioVideos.init(); // Retain aspect ratio of videos on window resize.
668    blogsite.modalMenu.init();            // Modal Menu.
669    blogsite.primaryMenu.init();          // Primary Menu.
670    blogsite.touchEnabled.init();         // Add class to body if device is touch-enabled.
671} );
672
673/*  -----------------------------------------------------------------------------------------------
674    Helper functions
675--------------------------------------------------------------------------------------------------- */
676
677/* Toggle an attribute ----------------------- */
678
679function blogsiteToggleAttribute( element, attribute, trueVal, falseVal ) {
680    if ( trueVal === undefined ) {
681        trueVal = true;
682    }
683    if ( falseVal === undefined ) {
684        falseVal = false;
685    }
686    if ( element.getAttribute( attribute ) !== trueVal ) {
687        element.setAttribute( attribute, trueVal );
688    } else {
689        element.setAttribute( attribute, falseVal );
690    }
691}
692
693/**
694 * Toggle a menu item on or off.
695 *
696 * @param {HTMLElement} target
697 * @param {number} duration
698 */
699function blogsiteMenuToggle( target, duration ) {
700    var initialParentHeight, finalParentHeight, menu, menuItems, transitionListener,
701        initialPositions = [],
702        finalPositions = [];
703
704    if ( ! target ) {
705        return;
706    }
707
708    menu = target.closest( '.menu-wrapper' );
709
710    // Step 1: look at the initial positions of every menu item.
711    menuItems = menu.querySelectorAll( '.menu-item' );
712
713    menuItems.forEach( function( menuItem, index ) {
714        initialPositions[ index ] = { x: menuItem.offsetLeft, y: menuItem.offsetTop };
715    } );
716    initialParentHeight = target.parentElement.offsetHeight;
717
718    target.classList.add( 'toggling-target' );
719
720    // Step 2: toggle target menu item and look at the final positions of every menu item.
721    target.classList.toggle( 'active' );
722
723    menuItems.forEach( function( menuItem, index ) {
724        finalPositions[ index ] = { x: menuItem.offsetLeft, y: menuItem.offsetTop };
725    } );
726    finalParentHeight = target.parentElement.offsetHeight;
727
728    // Step 3: close target menu item again.
729    // The whole process happens without giving the browser a chance to render, so it's invisible.
730    target.classList.toggle( 'active' );
731
732    /*
733     * Step 4: prepare animation.
734     * Position all the items with absolute offsets, at the same starting position.
735     * Shouldn't result in any visual changes if done right.
736     */
737    menu.classList.add( 'is-toggling' );
738    target.classList.toggle( 'active' );
739    menuItems.forEach( function( menuItem, index ) {
740        var initialPosition = initialPositions[ index ];
741        if ( initialPosition.y === 0 && menuItem.parentElement === target ) {
742            initialPosition.y = initialParentHeight;
743        }
744        menuItem.style.transform = 'translate(' + initialPosition.x + 'px, ' + initialPosition.y + 'px)';
745    } );
746
747    /*
748     * The double rAF is unfortunately needed, since we're toggling CSS classes, and
749     * the only way to ensure layout completion here across browsers is to wait twice.
750     * This just delays the start of the animation by 2 frames and is thus not an issue.
751     */
752    requestAnimationFrame( function() {
753        requestAnimationFrame( function() {
754            /*
755             * Step 5: start animation by moving everything to final position.
756             * All the layout work has already happened, while we were preparing for the animation.
757             * The animation now runs entirely in CSS, using cheap CSS properties (opacity and transform)
758             * that don't trigger the layout or paint stages.
759             */
760            menu.classList.add( 'is-animating' );
761            menuItems.forEach( function( menuItem, index ) {
762                var finalPosition = finalPositions[ index ];
763                if ( finalPosition.y === 0 && menuItem.parentElement === target ) {
764                    finalPosition.y = finalParentHeight;
765                }
766                if ( duration !== undefined ) {
767                    menuItem.style.transitionDuration = duration + 'ms';
768                }
769                menuItem.style.transform = 'translate(' + finalPosition.x + 'px, ' + finalPosition.y + 'px)';
770            } );
771            if ( duration !== undefined ) {
772                target.style.transitionDuration = duration + 'ms';
773            }
774        } );
775
776        // Step 6: finish toggling.
777        // Remove all transient classes when the animation ends.
778        transitionListener = function() {
779            menu.classList.remove( 'is-animating' );
780            menu.classList.remove( 'is-toggling' );
781            target.classList.remove( 'toggling-target' );
782            menuItems.forEach( function( menuItem ) {
783                menuItem.style.transform = '';
784                menuItem.style.transitionDuration = '';
785            } );
786            target.style.transitionDuration = '';
787            target.removeEventListener( 'transitionend', transitionListener );
788        };
789
790        target.addEventListener( 'transitionend', transitionListener );
791    } );
792}
793
794/**
795 * Traverses the DOM up to find elements matching the query.
796 *
797 * @param {HTMLElement} target
798 * @param {string} query
799 * @return {NodeList} parents matching query
800 */
801function blogsiteFindParents( target, query ) {
802    var parents = [];
803
804    // Recursively go up the DOM adding matches to the parents array.
805    function traverse( item ) {
806        var parent = item.parentNode;
807        if ( parent instanceof HTMLElement ) {
808            if ( parent.matches( query ) ) {
809                parents.push( parent );
810            }
811            traverse( parent );
812        }
813    }
814
815    traverse( target );
816
817    return parents;
818}

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.