felixarntz/theme-boilerplate

View on GitHub
assets/src/js/theme/sticky.js

Summary

Maintainability
B
6 hrs
Test Coverage
/**
 * File sticky.js.
 *
 * Handles stickiness of components which should always be visible by fixing them at the
 * top or bottom of the screen.
 */

class Sticky {
    constructor( options ) {
        let selectors;

        this.stickToTopSelectors = [];
        this.stickToBottomSelectors = [];

        this.options = options || {};

        selectors = Object.keys( this.options );
        selectors.forEach( selector => {
            switch ( true ) {
                case 'top' === this.options[ selector ]:
                    this.stickToTopSelectors.push( selector );
                    break;
                case 'bottom' === this.options[ selector ]:
                    this.stickToBottomSelectors.push( selector );
                    break;
            }
        });

        this.pageWrap                = document.getElementById( 'page' );
        this.stickToTopContainers    = this.stickToTopSelectors
            .map( selector => document.querySelector( selector ) )
            .filter( container => container )
            .sort( ( a, b ) => a.offsetTop < b.offsetTop ? -1 : 1 );
        this.stickToBottomContainers = this.stickToBottomSelectors
            .map( selector => document.querySelector( selector ) )
            .filter( container => container )
            .sort( ( a, b ) => a.offsetTop < b.offsetTop ? -1 : 1 )
            .reverse();
    }

    initialize() {
        const context = this;

        this.initializeHeaderSiteBranding();

        if ( ! this.stickToTopContainers.length && ! this.stickToBottomContainers.length ) {
            return;
        }

        this.stickToTopOffsets    = this.stickToTopContainers.map( container => container.offsetTop );
        this.stickToBottomOffsets = this.stickToBottomContainers.map( container => container.offsetTop + container.offsetHeight );

        function checkStickyContainers() {
            context.checkStickyContainers();
        }

        this.checkStickyContainers();
        window.addEventListener( 'scroll', checkStickyContainers );
        window.addEventListener( 'resize', checkStickyContainers );
    }

    initializeHeaderSiteBranding() {
        const headerSiteBranding = document.querySelector( '.site-custom-header .site-branding' );
        const offset             = headerSiteBranding ? ( headerSiteBranding.offsetTop ? headerSiteBranding.offsetTop : ( headerSiteBranding.offsetParent ? headerSiteBranding.offsetParent.offsetTop : 0 ) ) : 0;

        if ( ! headerSiteBranding ) {
            return;
        }

        function checkHeaderSiteBranding() {
            if ( window.scrollY >= offset ) {
                document.body.classList.add( 'scrolled-past-header-site-branding' );
                return;
            }

            document.body.classList.remove( 'scrolled-past-header-site-branding' );
        }

        checkHeaderSiteBranding();
        window.addEventListener( 'scroll', checkHeaderSiteBranding );
        window.addEventListener( 'resize', checkHeaderSiteBranding );
    }

    checkStickyContainers() {
        const pageWrap      = this.pageWrap;
        const topOffsets    = this.stickToTopOffsets;
        const bottomOffsets = this.stickToBottomOffsets;

        let toolbarOffset = this.getToolbarOffset();
        let topOffset     = 0;
        let bottomOffset  = 0;

        pageWrap.style.paddingTop = `${topOffset}px`;

        this.stickToTopContainers.forEach( ( container, index ) => {
            if ( window.scrollY >= topOffsets[ index ] - topOffset ) {
                container.style.top = `${( toolbarOffset + topOffset )}px`;
                if ( ! container.classList.contains( 'is-sticky' ) ) {
                    container.classList.add( 'is-sticky', 'is-sticky-top' );
                }

                topOffset += container.offsetHeight;
            } else {
                if ( container.classList.contains( 'is-sticky' ) ) {
                    container.classList.remove( 'is-sticky', 'is-sticky-top' );
                }
                container.style.top = 'auto';
            }

            pageWrap.style.paddingTop = `${topOffset}px`;
        });

        pageWrap.style.paddingBottom = `${( topOffset + bottomOffset )}px`;

        this.stickToBottomContainers.forEach( ( container, index ) => {
            if ( window.scrollY <= bottomOffsets[ index ] ) {
                container.style.bottom = `${bottomOffset}px`;
                if ( ! container.classList.contains( 'is-sticky' ) ) {
                    container.classList.add( 'is-sticky', 'is-sticky-bottom' );
                }

                bottomOffset += container.offsetHeight;
            } else {
                if ( container.classList.contains( 'is-sticky' ) ) {
                    container.classList.remove( 'is-sticky', 'is-sticky-bottom' );
                }
                container.style.bottom = 'auto';
            }

            pageWrap.style.paddingBottom = `${( topOffset + bottomOffset )}px`;
        });
    }

    getToolbarOffset() {
        const toolbar = document.getElementById( 'wpadminbar' );

        if ( ! toolbar ) {
            if ( document.body.classList.contains( 'admin-bar' ) ) {

                // If the toolbar is not yet rendered but will be shortly, fall back to WordPress defaults.
                if ( document.body.clientWidth <= 782 ) {
                    return 46;
                }
                return 32;
            }

            return 0;
        }

        return toolbar.offsetHeight;
    }

    addRemoveStickyContainer( selector, location, remove ) {
        const container = document.querySelector( selector );
        let selectorsHandle, containersHandle, offsetsHandle, exists;
        if ( 'top' === location ) {
            selectorsHandle  = 'stickToTopSelectors';
            containersHandle = 'stickToTopContainers';
            offsetsHandle    = 'stickToTopOffsets';
        } else if ( 'bottom' === location ) {
            selectorsHandle  = 'stickToBottomSelectors';
            containersHandle = 'stickToBottomContainers';
            offsetsHandle    = 'stickToBottomOffsets';
        }

        if ( ! container || ! selectorsHandle || ! containersHandle || ! offsetsHandle ) {
            return;
        }

        exists = this[ selectorsHandle ].includes( selector );
        if ( ! remove && exists || remove && ! exists ) {
            return;
        }

        this.pageWrap.style.removeProperty( 'padding-top' );
        this[ containersHandle ].forEach( container => {
            container.classList.remove( 'is-sticky', 'is-sticky-top', 'is-sticky-bottom' );
            container.style.removeProperty( 'top' );
            container.style.removeProperty( 'bottom' );
        });

        if ( remove ) {
            this[ selectorsHandle ].splice( this[ selectorsHandle ].findIndex( item => item === selector ), 1 );
            this[ containersHandle ].splice( this[ containersHandle ].findIndex( item => item === container ), 1 );
        } else {
            this[ selectorsHandle ].push( selector );
            this[ containersHandle ].push( container );
        }

        this[ containersHandle ] = this[ containersHandle ].sort( ( a, b ) => a.offsetTop < b.offsetTop ? -1 : 1 );
        if ( 'bottom' === location ) {
            this[ containersHandle ] = this[ containersHandle ].reverse();
        }

        this[ offsetsHandle ] = this[ containersHandle ].map( container => container.offsetTop + ( 'bottom' === location ? container.offsetHeight : 0 ) );

        this.checkStickyContainers();
    }
}

export default Sticky;