deployment-talks/slides/js/controllers/location.js

247 lines
6.1 KiB
JavaScript

/**
* 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();
}
}