637 lines
24 KiB
JavaScript
637 lines
24 KiB
JavaScript
/* offside-js 1.3.1 22-05-2016
|
|
* Minimal JavaScript kit without library dependencies to push things off-canvas using just class manipulation
|
|
* https://github.com/toomuchdesign/offside.git
|
|
*
|
|
* by Andrea Carraro
|
|
* Available under the MIT license
|
|
*/
|
|
|
|
;(function ( window, document, undefined ) {
|
|
|
|
/*
|
|
* The first time Offside is called, it creates a singleton-factory object
|
|
* and place it into "window.offside.factory".
|
|
*
|
|
* Offside factory serves the following purposes:
|
|
* - DOM initialization
|
|
* - Centralized Offside instances management and initialization
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
// Self-invoking function returning the object which contains
|
|
// the "getInstance" method used for initializing
|
|
// the Offside sigleton factory
|
|
var offside = (function () {
|
|
|
|
// Global Offside singleton-factory constructor
|
|
function initOffsideFactory( options ) {
|
|
|
|
// Utility functions
|
|
// Shared among factory and Offside instances, too
|
|
|
|
// Close all open Offsides.
|
|
// If an Offside instance id is provided, it just closes the matching instance instead
|
|
var closeAll = function( offsideId ) {
|
|
|
|
// Look for an open Offside id
|
|
if ( openOffsidesId.length > 0 ) {
|
|
|
|
// Close matching Offside instance if an ID is provided
|
|
if ( !isNaN( offsideId ) ) {
|
|
|
|
instantiatedOffsides[ offsideId ].close();
|
|
} else {
|
|
|
|
// Close all Offside instances
|
|
openOffsidesId.forEach( function( offsideId ) {
|
|
instantiatedOffsides[ offsideId ].close();
|
|
});
|
|
}
|
|
|
|
}
|
|
},
|
|
|
|
// Append a class to body in order to turn on elements CSS 3D transitions
|
|
// only when happens the first interation with an Offside instance.
|
|
// Otherwise we would see Offside instances being smoothly pushed
|
|
// out of the screen during DOM initialization.
|
|
turnOnCssTransitions = function() {
|
|
addClass( body, transitionsClass );
|
|
},
|
|
|
|
addClass = function( el, c ) {
|
|
if ( el.classList ) {
|
|
el.classList.add(c);
|
|
} else {
|
|
el.className = ( el.className + ' ' + c ).trim();
|
|
}
|
|
},
|
|
|
|
removeClass = function( el, c ) {
|
|
if ( el.classList ) {
|
|
el.classList.remove(c);
|
|
} else {
|
|
el.className = el.className.replace( new RegExp( '(^|\\b)' + c.split(' ').join('|') + '(\\b|$)', 'gi' ), ' ' );
|
|
}
|
|
},
|
|
|
|
addEvent = function( el, eventName, eventHandler ) {
|
|
el.addEventListener( eventName, eventHandler );
|
|
},
|
|
|
|
removeEvent = function( el, eventName, eventHandler ) {
|
|
el.removeEventListener( eventName, eventHandler );
|
|
},
|
|
|
|
// Return a collection (array) of DOM elements from:
|
|
// - A DOM element
|
|
// - An array of DOM elements
|
|
// - A string selector
|
|
getDomElements = function( els ) {
|
|
|
|
// "els" is a DOM element
|
|
// http://stackoverflow.com/a/120275/2902821
|
|
if( els instanceof HTMLElement ){
|
|
return [ els ];
|
|
}
|
|
|
|
// "els" is an array
|
|
else if( Array.isArray( els ) ) {
|
|
return els;
|
|
}
|
|
|
|
// "els" is a string
|
|
else if( typeof els === 'string' ) {
|
|
// Convert Nodelist into an array
|
|
// http://www.jstips.co/en/converting-a-node-list-to-an-array/
|
|
return Array.apply( null, document.querySelectorAll( els ) );
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
// Check if a value exists in an array. Returns:
|
|
// - array index if value exists
|
|
// - "false" if value is not found
|
|
// See: http://stackoverflow.com/a/5767357
|
|
isInArray = function( arr, value ) {
|
|
var index = arr.indexOf( value );
|
|
return index > -1 ? index : false;
|
|
};
|
|
|
|
// Offside.js factory initialization
|
|
|
|
var i,
|
|
factorySettings; // Offside factory private settings
|
|
|
|
// Default factory settings
|
|
factorySettings = {
|
|
|
|
slidingElementsSelector: '.offside-sliding-element', // String: Default sliding elements selectors ('#foo, #bar')
|
|
disableCss3dTransforms: false, // Disable CSS 3d Transforms support (for testing purposes)
|
|
debug: false, // Boolean: If true, print errors in console
|
|
};
|
|
|
|
// User defined factory settings
|
|
for ( i in options ) {
|
|
if ( factorySettings.hasOwnProperty( i ) ) {
|
|
factorySettings[i] = options[i];
|
|
}
|
|
}
|
|
|
|
// Private factory properties
|
|
var globalClass = 'offside-js', // Global Offside classes namespace
|
|
initClass = globalClass + '--init', // Class appended to body when Offside is intialized
|
|
slidingElementsClass = 'offside-sliding-element', // Class appended to sliding elements
|
|
transitionsClass = globalClass + '--interact', // Class appended to body when ready to turn on Offside CSS transitions (Added when first menu interaction happens)
|
|
instantiatedOffsides = [], // Array containing all instantiated offside elements
|
|
firstInteraction = true, // Keep track of first Offside interaction
|
|
has3d = factorySettings.disableCss3dTransforms ? false : _has3d(), // Browser supports CSS 3d Transforms
|
|
openOffsidesId = [], // Tracks opened Offside instances id's
|
|
body = document.body,
|
|
slidingElements = getDomElements( factorySettings.slidingElementsSelector ), // Sliding elements
|
|
debug = factorySettings.debug;
|
|
|
|
// Offside singleton-factory Dom initialization
|
|
// It's called just once on Offside singleton-factory init.
|
|
function _factoryDomInit() {
|
|
|
|
// Add class to sliding elements
|
|
slidingElements.forEach( function( item ) {
|
|
addClass( item, slidingElementsClass );
|
|
});
|
|
|
|
// DOM Fallbacks when CSS transform 3d not available
|
|
if ( !has3d ) {
|
|
// No CSS 3d Transform fallback
|
|
addClass( document.documentElement, 'no-csstransforms3d' ); //Adds Modernizr-like class to HTML element when CSS 3D Transforms not available
|
|
}
|
|
|
|
// Add init class to body
|
|
addClass( body, initClass );
|
|
}
|
|
|
|
// Private Offside factory methods
|
|
|
|
// Testing for CSS 3D Transform Support
|
|
// https://gist.github.com/lorenzopolidori/3794226
|
|
function _has3d() {
|
|
|
|
if ( !window.getComputedStyle ) {
|
|
return false;
|
|
}
|
|
|
|
var el = document.createElement('p'),
|
|
has3d,
|
|
transforms = {
|
|
'webkitTransform':'-webkit-transform',
|
|
'OTransform':'-o-transform',
|
|
'msTransform':'-ms-transform',
|
|
'MozTransform':'-moz-transform',
|
|
'transform':'transform'
|
|
};
|
|
|
|
// Add it to the body to get the computed style
|
|
document.body.insertBefore(el, null);
|
|
|
|
for( var t in transforms ){
|
|
if( el.style[t] !== undefined ){
|
|
el.style[t] = 'translate3d(1px,1px,1px)';
|
|
has3d = window.getComputedStyle(el).getPropertyValue(transforms[t]);
|
|
}
|
|
}
|
|
|
|
document.body.removeChild(el);
|
|
|
|
return ( has3d !== undefined && has3d.length > 0 && has3d !== 'none' );
|
|
}
|
|
|
|
// Offside constructor wrapper
|
|
// It supplies a wrapper around OffsideInstance constructor
|
|
// to prevent instance initialization when casted on a non-existing DOM element
|
|
// See: http://stackoverflow.com/a/8618792
|
|
function createOffsideInstance( el, options, offsideId ) {
|
|
|
|
// Check if provided element exists before using it to instantiate an Offside instance
|
|
var domEl = el !== undefined ?
|
|
getDomElements( el ) :
|
|
getDomElements( '.offside' );
|
|
|
|
// If provided el exists initialize an Offside instance, else return null
|
|
return domEl !== false ?
|
|
new OffsideInstance( domEl[0], options, offsideId ) :
|
|
null;
|
|
}
|
|
|
|
// Offside constructor
|
|
// Set up and initialize a new Offside instance
|
|
// Called by Offside factory "getOffsideInstance()" method
|
|
function OffsideInstance( domEl, options, offsideId ) {
|
|
|
|
var i,
|
|
offsideSettings;
|
|
|
|
// Default Offside instance settings
|
|
offsideSettings = {
|
|
|
|
buttonsSelector: '', // String: Offside toggle buttons selectors ('#foo, #bar')
|
|
slidingSide: 'left', // String: Offside element pushed on left or right
|
|
init: function(){}, // Function: After init callback
|
|
beforeOpen: function(){}, // Function: Before open callback
|
|
afterOpen: function(){}, // Function: After open callback
|
|
beforeClose: function(){}, // Function: Before close callback
|
|
afterClose: function(){}, // Function: After close callback
|
|
beforeDestroy: function(){}, // Function: After destroy callback
|
|
afterDestroy: function(){}, // Function: After destroy callback
|
|
};
|
|
|
|
// User defined Offside instance settings
|
|
for ( i in options ) {
|
|
if ( offsideSettings.hasOwnProperty( i ) ) {
|
|
offsideSettings[i] = options[i];
|
|
}
|
|
}
|
|
|
|
// Offside instance private properties
|
|
var offside = domEl, // Hello, I'm the Offside instance
|
|
offsideButtons = getDomElements( offsideSettings.buttonsSelector ), // Offside toggle buttons
|
|
slidingSide = offsideSettings.slidingSide,
|
|
offsideClass = 'offside', // Class added to Offside instance when initialized
|
|
offsideSideClass = offsideClass + '--' + slidingSide, // Class added to Offside instance when initialized (eg. offside offside--left)
|
|
offsideOpenClass = 'is-open', // Class appended to Offside instance when open
|
|
offsideBodyOpenClass = globalClass + '--' + 'is-open', // Class appended to body when an Offside instance is open (offside-js--is-open)
|
|
offsideBodyOpenSideClass = globalClass + '--is-' + slidingSide, // Class appended to body when Offside instance is open (eg. offside-js--is-left / offside-js--is-open)
|
|
id = offsideId || 0; // Offside instance id
|
|
|
|
// Offside instance private methods
|
|
|
|
var _toggleOffside = function() {
|
|
|
|
// Premise: Just 1 Offside instance at time can be open.
|
|
|
|
// Check currently toggling Offside status
|
|
isInArray( openOffsidesId, id ) === false ? _openOffside() : _closeOffside();
|
|
},
|
|
|
|
_openOffside = function() {
|
|
|
|
// beforeOpen callback
|
|
offsideSettings.beforeOpen();
|
|
|
|
// Turn on CSS transitions on first interaction with an Offside instance
|
|
if ( firstInteraction ) {
|
|
firstInteraction = false;
|
|
turnOnCssTransitions();
|
|
}
|
|
|
|
// If another Offside instance is already open,
|
|
// close it before going on
|
|
closeAll();
|
|
|
|
// Set global body active class for current Offside instance
|
|
addClass( body, offsideBodyOpenClass );
|
|
addClass( body, offsideBodyOpenSideClass );
|
|
|
|
// Add Offside instance open class
|
|
addClass( offside, offsideOpenClass );
|
|
|
|
// Update open Offside instances tracker
|
|
openOffsidesId.push( id );
|
|
|
|
// afterOpen callback
|
|
offsideSettings.afterOpen();
|
|
},
|
|
|
|
_closeOffside = function() {
|
|
|
|
// Proceed with closing stuff only if
|
|
// current Offside instance is listed among openOffsidesId array
|
|
var index = isInArray( openOffsidesId, id );
|
|
|
|
if ( index !== false ) {
|
|
|
|
// beforeClose callback
|
|
offsideSettings.beforeClose();
|
|
|
|
// Remove global body active class for current Offside instance
|
|
removeClass( body, offsideBodyOpenClass );
|
|
removeClass( body, offsideBodyOpenSideClass );
|
|
|
|
// Remove Offside instance open class
|
|
removeClass( offside, offsideOpenClass );
|
|
|
|
// Update open Offside instances tracker
|
|
openOffsidesId.splice( index, 1 );
|
|
|
|
// afterClose callback
|
|
offsideSettings.afterClose();
|
|
}
|
|
},
|
|
|
|
_closeAll = function() {
|
|
|
|
closeAll();
|
|
},
|
|
|
|
// Offside buttons click handler
|
|
_onButtonClick = function( e ) {
|
|
|
|
e.preventDefault();
|
|
_toggleOffside();
|
|
},
|
|
|
|
/*
|
|
// Get Offside instance unique ID
|
|
_getId = function() {
|
|
return id;
|
|
}
|
|
*/
|
|
|
|
// Set up and initialize a new Offside instance
|
|
_initOffside = function() {
|
|
|
|
if ( debug ) {
|
|
_checkElements();
|
|
}
|
|
|
|
// Append classes to Offside instance (.offside and .offside{slidingSide})
|
|
addClass( offside, offsideClass );
|
|
addClass( offside, offsideSideClass );
|
|
|
|
// Toggle Offside on click event
|
|
offsideButtons.forEach( function( item ) {
|
|
addEvent( item, 'click', _onButtonClick );
|
|
});
|
|
|
|
// Init callback
|
|
offsideSettings.init();
|
|
},
|
|
|
|
_destroyOffside = function() {
|
|
|
|
// beforeDestroy callback
|
|
offsideSettings.beforeDestroy();
|
|
|
|
// Close Offside intance before destroy
|
|
_closeOffside();
|
|
|
|
// Remove click event from Offside buttons
|
|
offsideButtons.forEach( function( item ) {
|
|
removeEvent( item, 'click', _onButtonClick );
|
|
});
|
|
|
|
// Remove classes appended on init phase
|
|
removeClass( offside, offsideClass );
|
|
removeClass( offside, offsideSideClass );
|
|
|
|
// Destroy Offside instance
|
|
delete instantiatedOffsides[id];
|
|
|
|
// afterDestroy callback
|
|
offsideSettings.afterDestroy();
|
|
},
|
|
|
|
// Fire console errors if DOM elements are missing
|
|
_checkElements = function() {
|
|
|
|
if ( !offside ) {
|
|
console.error( 'Offside alert: "offside" selector could not match any element' );
|
|
}
|
|
|
|
if ( !offsideButtons.length ) {
|
|
console.error( 'Offside alert: "buttonsSelector" selector could not match any element' );
|
|
}
|
|
};
|
|
|
|
// Offside instances public methods
|
|
this.toggle = function() {
|
|
_toggleOffside();
|
|
};
|
|
|
|
this.open = function() {
|
|
_openOffside();
|
|
};
|
|
|
|
this.close = function() {
|
|
_closeOffside();
|
|
};
|
|
|
|
this.closeAll = function() {
|
|
_closeAll();
|
|
};
|
|
|
|
this.destroy = function() {
|
|
_destroyOffside();
|
|
};
|
|
|
|
// Ok, init Offside instance
|
|
_initOffside();
|
|
|
|
} // OffsideInstance constructor end
|
|
|
|
|
|
// DOM initialization
|
|
_factoryDomInit();
|
|
|
|
// This is the actual returned Offside factory
|
|
return {
|
|
|
|
//Offside factory public methods
|
|
closeOpenOffside: function() {
|
|
closeAll();
|
|
},
|
|
|
|
// This is the method responsible for creating a new Offside instance
|
|
// and register it into "instantiatedOffsides" array
|
|
getOffsideInstance: function( el, options ) {
|
|
|
|
// Get length of instantiated Offsides array
|
|
var offsideId = instantiatedOffsides.length || 0,
|
|
|
|
// Instantiate new Offside instance
|
|
offsideInstance = createOffsideInstance( el, options, offsideId );
|
|
|
|
// If Offside instance is sccessfully created
|
|
if ( offsideInstance !== null ) {
|
|
|
|
// Push new instance into "instantiatedOffsides" array and return it
|
|
|
|
/*jshint -W093 */
|
|
return instantiatedOffsides[ offsideId ] = offsideInstance;
|
|
/*jshint +W093 */
|
|
}
|
|
|
|
|
|
}
|
|
|
|
};
|
|
|
|
} // initOffsideFactory() end
|
|
|
|
return {
|
|
|
|
// Get the Singleton instance if one exists
|
|
// or create one if it doesn't
|
|
getInstance: function ( el, options ) {
|
|
|
|
if ( !window.offside.factory ) {
|
|
window.offside.factory = initOffsideFactory( options );
|
|
}
|
|
|
|
return window.offside.factory.getOffsideInstance( el, options );
|
|
}
|
|
};
|
|
|
|
})();
|
|
|
|
// Store in window a reference to the Offside singleton factory
|
|
if ( typeof module !== 'undefined' && module.exports ) {
|
|
module.exports = offside.getInstance;
|
|
} else {
|
|
window.offside = offside.getInstance;
|
|
}
|
|
|
|
})( window, document );
|
|
|
|
/**
|
|
* Start GP.
|
|
*/
|
|
document.addEventListener( 'DOMContentLoaded', function() {
|
|
document.querySelector( '.slideout-navigation' ).style.display = '';
|
|
} );
|
|
|
|
var generateOffside = offside( '.slideout-navigation', {
|
|
slidingElementsSelector:'#slideout-container',
|
|
buttonsSelector: '.slideout-mobile .main-navigation .menu-toggle, .slideout-both .main-navigation .menu-toggle, .slideout-both .slideout-toggle, .slideout-desktop .slideout-toggle',
|
|
slidingSide: offSide.side,
|
|
beforeOpen: function() {
|
|
// Turn on visibility so we can transition nicely.
|
|
document.querySelector( '.slideout-navigation' ).style.visibility = 'visible';
|
|
},
|
|
afterOpen: function() {
|
|
document.documentElement.classList.add( 'slide-opened' );
|
|
document.body.classList.add( 'slide-opened' );
|
|
|
|
if ( document.body.classList.contains( 'dropdown-hover' ) ) {
|
|
var dropdownItems = document.querySelector( '.slideout-navigation' ).querySelectorAll( 'li.menu-item-has-children' );
|
|
for ( var i = 0; i < dropdownItems.length; i++ ) {
|
|
var spanToggle = dropdownItems[i].querySelector( 'span.dropdown-menu-toggle' );
|
|
|
|
if ( spanToggle ) {
|
|
spanToggle.setAttribute( 'tabindex', 0 );
|
|
spanToggle.setAttribute( 'role', 'button' );
|
|
spanToggle.setAttribute( 'aria-expanded', true );
|
|
}
|
|
}
|
|
}
|
|
|
|
document.querySelector( '.slideout-navigation' ).removeAttribute( 'aria-hidden' );
|
|
|
|
// Focus the first focusable element.
|
|
var focusable = document.querySelector( '.slideout-navigation' ).querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' );
|
|
|
|
if ( focusable ) {
|
|
setTimeout( function() {
|
|
focusable[0].focus();
|
|
}, 200 );
|
|
}
|
|
},
|
|
afterClose: function() {
|
|
var body = document.body,
|
|
nav = document.querySelectorAll( '.main-navigation' );
|
|
|
|
for ( var i = 0; i < nav.length; i++ ) {
|
|
if ( nav[i].classList.contains( 'toggled' ) ) {
|
|
nav[i].classList.remove( 'toggled' );
|
|
}
|
|
};
|
|
|
|
document.documentElement.classList.remove( 'slide-opened' );
|
|
body.classList.remove( 'slide-opened' );
|
|
|
|
if ( 'true' === document.querySelector( '.main-navigation .menu-toggle' ).getAttribute( 'aria-expanded' ) ) {
|
|
document.querySelector( '.main-navigation .menu-toggle' ).setAttribute( 'aria-expanded', false );
|
|
}
|
|
|
|
if ( body.classList.contains( 'dropdown-hover' ) ) {
|
|
var dropdownItems = document.querySelector( '.main-navigation:not(.slideout-navigation):not(.mobile-menu-control-wrapper)' ).querySelectorAll( 'li.menu-item-has-children' );
|
|
for ( var i = 0; i < dropdownItems.length; i++ ) {
|
|
var spanToggle = dropdownItems[i].querySelector( 'span.dropdown-menu-toggle' );
|
|
|
|
if ( spanToggle ) {
|
|
spanToggle.removeAttribute( 'tabindex' );
|
|
spanToggle.setAttribute( 'role', 'presentation' );
|
|
spanToggle.removeAttribute( 'aria-expanded' );
|
|
}
|
|
}
|
|
}
|
|
|
|
// Turn off visibility.
|
|
setTimeout( function() {
|
|
document.querySelector( '.slideout-navigation:not(.is-open)' ).style.visibility = '';
|
|
document.querySelector( '.slideout-navigation:not(.is-open)' ).setAttribute( 'aria-hidden', 'true' );
|
|
}, 500 );
|
|
|
|
// Focus our slideout toggle.
|
|
if ( window.document.documentElement.clientWidth <= 768 ) {
|
|
if ( body.classList.contains( 'slideout-mobile' ) || body.classList.contains( 'slideout-both' ) ) {
|
|
document.querySelectorAll( '.main-navigation:not(.slideout-navigation)' ).forEach( function( navigation ) {
|
|
if ( navigation && navigation.style.display !== 'none' ) {
|
|
navigation.querySelector( '.menu-toggle' ).focus();
|
|
}
|
|
} );
|
|
}
|
|
} else {
|
|
if ( body.classList.contains( 'slideout-desktop' ) || body.classList.contains( 'slideout-both' ) ) {
|
|
document.querySelectorAll( '.main-navigation:not(.slideout-navigation)' ).forEach( function( navigation ) {
|
|
if ( navigation && navigation.style.display !== 'none' ) {
|
|
navigation.querySelector( '.slideout-toggle a' ).focus();
|
|
}
|
|
} );
|
|
}
|
|
}
|
|
}
|
|
} );
|
|
|
|
var closeElements = document.querySelectorAll( '.slideout-overlay, .slideout-exit, .slider-exit a' );
|
|
for ( var i = 0; i < closeElements.length; i++ ) {
|
|
closeElements[i].addEventListener( 'click', function( e ) {
|
|
e.preventDefault();
|
|
generateOffside.close();
|
|
} );
|
|
};
|
|
|
|
|
|
/**
|
|
* Closes the slideout navigation if the link does something.
|
|
* eg. Goes to an anchor link
|
|
*
|
|
* @since 1.7
|
|
*/
|
|
var slideoutLinks = document.querySelectorAll( '.slideout-navigation ul a' );
|
|
|
|
var closeOffsideOnAction = function() {
|
|
var url = this.getAttribute( 'href' );
|
|
|
|
if ( '#' !== url && '' !== url && ! navigator.userAgent.match( /iemobile/i ) ) {
|
|
setTimeout( function() {
|
|
generateOffside.close();
|
|
}, 200 );
|
|
}
|
|
};
|
|
|
|
for ( var i = 0; i < slideoutLinks.length; i++ ) {
|
|
slideoutLinks[i].addEventListener( 'click', closeOffsideOnAction, false );
|
|
};
|
|
|
|
document.addEventListener( 'keyup', function( e ) {
|
|
if ( document.body.classList.contains( 'slide-opened' ) ) {
|
|
e = e || window.event;
|
|
|
|
if ( e.keyCode == 27 ) {
|
|
generateOffside.close();
|
|
}
|
|
}
|
|
} );
|