/** * Reads and writes the URL based on reveal.js' current state. */ export default class Location { constructor( Reveal ) { this.Reveal = Reveal; // Delays updates to the URL due to a Chrome thumbnailer bug this.writeURLTimeout = 0; this.onWindowHashChange = this.onWindowHashChange.bind( this ); } bind() { window.addEventListener( 'hashchange', this.onWindowHashChange, false ); } unbind() { window.removeEventListener( 'hashchange', this.onWindowHashChange, false ); } /** * Reads the current URL (hash) and navigates accordingly. */ readURL() { let config = this.Reveal.getConfig(); let indices = this.Reveal.getIndices(); let currentSlide = this.Reveal.getCurrentSlide(); let hash = window.location.hash; // Attempt to parse the hash as either an index or name let bits = hash.slice( 2 ).split( '/' ), name = hash.replace( /#\/?/gi, '' ); // 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 element; 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 { element = document.getElementById( decodeURIComponent( name ) ); } catch ( error ) { } // Ensure that we're not already on a slide with the same name let isSameNameAsCurrentSlide = currentSlide ? currentSlide.getAttribute( 'id' ) === name : false; if( element ) { // If the slide exists and is not the current slide... if ( !isSameNameAsCurrentSlide || typeof f !== 'undefined' ) { // ...find the position of the named slide and navigate to it let slideIndices = this.Reveal.getIndices( element ); this.Reveal.slide( slideIndices.h, slideIndices.v, f ); } } // If the slide doesn't exist, navigate to the current slide else { this.Reveal.slide( indices.h || 0, indices.v || 0 ); } } else { let hashIndexBase = config.hashOneBasedIndex ? 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; } } if( h !== indices.h || v !== indices.v || f !== undefined ) { this.Reveal.slide( h, v, f ); } } } /** * 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 avaialble. 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 === '/' ) { window.history.replaceState( null, null, window.location.pathname + window.location.search ); } else { window.history.replaceState( null, null, '#' + 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 ); // } } } /** * 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(); } }