hakimel/reveal.js

View on GitHub
js/controllers/location.js

Summary

Maintainability
C
7 hrs
Test Coverage
/**
 * Reads and writes the URL based on reveal.js' current state.
 */
export default class Location {

    // The minimum number of milliseconds that must pass between
    // calls to history.replaceState
    MAX_REPLACE_STATE_FREQUENCY = 1000

    constructor( Reveal ) {

        this.Reveal = Reveal;

        // Delays updates to the URL due to a Chrome thumbnailer bug
        this.writeURLTimeout = 0;

        this.replaceStateTimestamp = 0;

        this.onWindowHashChange = this.onWindowHashChange.bind( this );

    }

    bind() {

        window.addEventListener( 'hashchange', this.onWindowHashChange, false );

    }

    unbind() {

        window.removeEventListener( 'hashchange', this.onWindowHashChange, false );

    }

    /**
     * Returns the slide indices for the given hash link.
     *
     * @param {string} [hash] the hash string that we want to
     * find the indices for
     *
     * @returns slide indices or null
     */
    getIndicesFromHash( hash=window.location.hash, options={} ) {

        // Attempt to parse the hash as either an index or name
        let name = hash.replace( /^#\/?/, '' );
        let bits = name.split( '/' );

        // If the first bit is not fully numeric and there is a name we
        // can assume that this is a named link
        if( !/^[0-9]*$/.test( bits[0] ) && name.length ) {
            let slide;

            let f;

            // Parse named links with fragments (#/named-link/2)
            if( /\/[-\d]+$/g.test( name ) ) {
                f = parseInt( name.split( '/' ).pop(), 10 );
                f = isNaN(f) ? undefined : f;
                name = name.split( '/' ).shift();
            }

            // Ensure the named link is a valid HTML ID attribute
            try {
                slide = document
                    .getElementById( decodeURIComponent( name ) )
                    .closest('.slides section');
            }
            catch ( error ) { }

            if( slide ) {
                return { ...this.Reveal.getIndices( slide ), f };
            }
        }
        else {
            const config = this.Reveal.getConfig();
            let hashIndexBase = config.hashOneBasedIndex || options.oneBasedIndex ? 1 : 0;

            // Read the index components of the hash
            let h = ( parseInt( bits[0], 10 ) - hashIndexBase ) || 0,
                v = ( parseInt( bits[1], 10 ) - hashIndexBase ) || 0,
                f;

            if( config.fragmentInURL ) {
                f = parseInt( bits[2], 10 );
                if( isNaN( f ) ) {
                    f = undefined;
                }
            }

            return { h, v, f };
        }

        // The hash couldn't be parsed or no matching named link was found
        return null

    }

    /**
     * Reads the current URL (hash) and navigates accordingly.
     */
    readURL() {

        const currentIndices = this.Reveal.getIndices();
        const newIndices = this.getIndicesFromHash();

        if( newIndices ) {
            if( ( newIndices.h !== currentIndices.h || newIndices.v !== currentIndices.v || newIndices.f !== undefined ) ) {
                    this.Reveal.slide( newIndices.h, newIndices.v, newIndices.f );
            }
        }
        // If no new indices are available, we're trying to navigate to
        // a slide hash that does not exist
        else {
            this.Reveal.slide( currentIndices.h || 0, currentIndices.v || 0 );
        }

    }

    /**
     * Updates the page URL (hash) to reflect the current
     * state.
     *
     * @param {number} delay The time in ms to wait before
     * writing the hash
     */
    writeURL( delay ) {

        let config = this.Reveal.getConfig();
        let currentSlide = this.Reveal.getCurrentSlide();

        // Make sure there's never more than one timeout running
        clearTimeout( this.writeURLTimeout );

        // If a delay is specified, timeout this call
        if( typeof delay === 'number' ) {
            this.writeURLTimeout = setTimeout( this.writeURL, delay );
        }
        else if( currentSlide ) {

            let hash = this.getHash();

            // If we're configured to push to history OR the history
            // API is not available.
            if( config.history ) {
                window.location.hash = hash;
            }
            // If we're configured to reflect the current slide in the
            // URL without pushing to history.
            else if( config.hash ) {
                // If the hash is empty, don't add it to the URL
                if( hash === '/' ) {
                    this.debouncedReplaceState( window.location.pathname + window.location.search );
                }
                else {
                    this.debouncedReplaceState( '#' + hash );
                }
            }
            // UPDATE: The below nuking of all hash changes breaks
            // anchors on pages where reveal.js is running. Removed
            // in 4.0. Why was it here in the first place? ¯\_(ツ)_/¯
            //
            // If history and hash are both disabled, a hash may still
            // be added to the URL by clicking on a href with a hash
            // target. Counter this by always removing the hash.
            // else {
            //     window.history.replaceState( null, null, window.location.pathname + window.location.search );
            // }

        }

    }

    replaceState( url ) {

        window.history.replaceState( null, null, url );
        this.replaceStateTimestamp = Date.now();

    }

    debouncedReplaceState( url ) {

        clearTimeout( this.replaceStateTimeout );

        if( Date.now() - this.replaceStateTimestamp > this.MAX_REPLACE_STATE_FREQUENCY ) {
            this.replaceState( url );
        }
        else {
            this.replaceStateTimeout = setTimeout( () => this.replaceState( url ), this.MAX_REPLACE_STATE_FREQUENCY );
        }

    }

    /**
     * Return a hash URL that will resolve to the given slide location.
     *
     * @param {HTMLElement} [slide=currentSlide] The slide to link to
     */
    getHash( slide ) {

        let url = '/';

        // Attempt to create a named link based on the slide's ID
        let s = slide || this.Reveal.getCurrentSlide();
        let id = s ? s.getAttribute( 'id' ) : null;
        if( id ) {
            id = encodeURIComponent( id );
        }

        let index = this.Reveal.getIndices( slide );
        if( !this.Reveal.getConfig().fragmentInURL ) {
            index.f = undefined;
        }

        // If the current slide has an ID, use that as a named link,
        // but we don't support named links with a fragment index
        if( typeof id === 'string' && id.length ) {
            url = '/' + id;

            // If there is also a fragment, append that at the end
            // of the named link, like: #/named-link/2
            if( index.f >= 0 ) url += '/' + index.f;
        }
        // Otherwise use the /h/v index
        else {
            let hashIndexBase = this.Reveal.getConfig().hashOneBasedIndex ? 1 : 0;
            if( index.h > 0 || index.v > 0 || index.f >= 0 ) url += index.h + hashIndexBase;
            if( index.v > 0 || index.f >= 0 ) url += '/' + (index.v + hashIndexBase );
            if( index.f >= 0 ) url += '/' + index.f;
        }

        return url;

    }

    /**
     * Handler for the window level 'hashchange' event.
     *
     * @param {object} [event]
     */
    onWindowHashChange( event ) {

        this.readURL();

    }

}