541 lines
14 KiB
JavaScript
541 lines
14 KiB
JavaScript
/*!
|
|
* CSS Modal
|
|
* http://drublic.github.com/css-modal
|
|
*
|
|
* @author Hans Christian Reinl - @drublic
|
|
*/
|
|
|
|
(function (global, $) {
|
|
|
|
'use strict';
|
|
|
|
/*
|
|
* Storage for functions and attributes
|
|
*/
|
|
var modal = {
|
|
|
|
activeElement: undefined, // Store for currently active element
|
|
lastActive: undefined, // Store for last active elemet
|
|
stackedElements: [], // Store for stacked elements
|
|
|
|
// All elements that can get focus, can be tabbed in a modal
|
|
tabbableElements: 'a[href], area[href], input:not([disabled]),' +
|
|
'select:not([disabled]), textarea:not([disabled]),' +
|
|
'button:not([disabled]), iframe, object, embed, *[tabindex],' +
|
|
'*[contenteditable]',
|
|
|
|
/*
|
|
* Polyfill addEventListener for IE8 (only very basic)
|
|
* @param event {string} event type
|
|
* @param element {Node} node to fire event on
|
|
* @param callback {function} gets fired if event is triggered
|
|
*/
|
|
on: function (event, elements, callback) {
|
|
var i = 0;
|
|
|
|
if (typeof event !== 'string') {
|
|
throw new Error('Type error: `event` has to be a string');
|
|
}
|
|
|
|
if (typeof callback !== 'function') {
|
|
throw new Error('Type error: `callback` has to be a function');
|
|
}
|
|
|
|
if (!elements) {
|
|
return;
|
|
}
|
|
|
|
// Make elements an array and attach event listeners
|
|
if (!elements.length) {
|
|
elements = [elements];
|
|
}
|
|
|
|
for (; i < elements.length; i++) {
|
|
|
|
// If jQuery is supported
|
|
if ($) {
|
|
$(elements[i]).on(event, callback);
|
|
|
|
// Default way to support events
|
|
} else if ('addEventListener' in elements[i]) {
|
|
elements[i].addEventListener(event, callback, false);
|
|
}
|
|
|
|
}
|
|
},
|
|
|
|
/*
|
|
* Convenience function to trigger event
|
|
* @param event {string} event type
|
|
* @param modal {string} id of modal that the event is triggered on
|
|
*/
|
|
trigger: function (event, modal) {
|
|
var eventTrigger;
|
|
var eventParams = {
|
|
detail: {
|
|
'modal': modal
|
|
}
|
|
};
|
|
|
|
// Use jQuery to fire the event if it is included
|
|
if ($) {
|
|
$(document).trigger(event, eventParams);
|
|
|
|
// Use createEvent if supported (that's mostly the case)
|
|
} else if (document.createEvent) {
|
|
eventTrigger = document.createEvent('CustomEvent');
|
|
|
|
eventTrigger.initCustomEvent(event, false, false, {
|
|
'modal': modal
|
|
});
|
|
|
|
document.dispatchEvent(eventTrigger);
|
|
|
|
// Use CustomEvents if supported
|
|
} else {
|
|
eventTrigger = new CustomEvent(event, eventParams);
|
|
|
|
document.dispatchEvent(eventTrigger);
|
|
}
|
|
},
|
|
|
|
/*
|
|
* Convenience function to add a class to an element
|
|
* @param element {Node} element to add class to
|
|
* @param className {string}
|
|
*/
|
|
addClass: function (element, className) {
|
|
if (element && !element.className.match(className)) {
|
|
element.className += ' ' + className;
|
|
}
|
|
},
|
|
|
|
/*
|
|
* Convenience function to remove a class from an element
|
|
* @param element {Node} element to remove class off
|
|
* @param className {string}
|
|
*/
|
|
removeClass: function (element, className) {
|
|
element.className = element.className.replace(className, '').replace(' ', ' ');
|
|
},
|
|
|
|
/**
|
|
* Convenience function to check if an element has a class
|
|
* @param {Node} element Element to check classname on
|
|
* @param {string} className Class name to check for
|
|
* @return {Boolean} true, if class is available on modal
|
|
*/
|
|
hasClass: function (element, className) {
|
|
return !!element.className.match(className);
|
|
},
|
|
|
|
/*
|
|
* Focus modal
|
|
*/
|
|
setFocus: function () {
|
|
if (modal.activeElement) {
|
|
|
|
// Set element with last focus
|
|
modal.lastActive = document.activeElement;
|
|
|
|
// New focussing
|
|
modal.activeElement.focus();
|
|
|
|
// Add handler to keep the focus
|
|
modal.keepFocus(modal.activeElement);
|
|
}
|
|
},
|
|
|
|
/*
|
|
* Unfocus
|
|
*/
|
|
removeFocus: function () {
|
|
if (modal.lastActive) {
|
|
modal.lastActive.focus();
|
|
}
|
|
},
|
|
|
|
/*
|
|
* Keep focus inside the modal
|
|
* @param element {node} element to keep focus in
|
|
*/
|
|
keepFocus: function (element) {
|
|
var allTabbableElements = [];
|
|
|
|
// Don't keep the focus if the browser is unable to support
|
|
// CSS3 selectors
|
|
try {
|
|
allTabbableElements = element.querySelectorAll(modal.tabbableElements);
|
|
} catch (ex) {
|
|
return;
|
|
}
|
|
|
|
var firstTabbableElement = modal.getFirstElementVisible(allTabbableElements);
|
|
var lastTabbableElement = modal.getLastElementVisible(allTabbableElements);
|
|
|
|
var focusHandler = function (event) {
|
|
var keyCode = event.which || event.keyCode;
|
|
|
|
// TAB pressed
|
|
if (keyCode !== 9) {
|
|
return;
|
|
}
|
|
|
|
// Polyfill to prevent the default behavior of events
|
|
event.preventDefault = event.preventDefault || function () {
|
|
event.returnValue = false;
|
|
};
|
|
|
|
// Move focus to first element that can be tabbed if Shift isn't used
|
|
if (event.target === lastTabbableElement && !event.shiftKey) {
|
|
event.preventDefault();
|
|
firstTabbableElement.focus();
|
|
|
|
// Move focus to last element that can be tabbed if Shift is used
|
|
} else if (event.target === firstTabbableElement && event.shiftKey) {
|
|
event.preventDefault();
|
|
lastTabbableElement.focus();
|
|
}
|
|
};
|
|
|
|
modal.on('keydown', element, focusHandler);
|
|
},
|
|
|
|
/*
|
|
* Return the first visible element of a nodeList
|
|
*
|
|
* @param nodeList The nodelist to parse
|
|
* @return {Node|null} Returns a specific node or null if no element found
|
|
*/
|
|
getFirstElementVisible: function (nodeList) {
|
|
var nodeListLength = nodeList.length;
|
|
|
|
// If the first item is not visible
|
|
if (!modal.isElementVisible(nodeList[0])) {
|
|
for (var i = 1; i < nodeListLength - 1; i++) {
|
|
|
|
// Iterate elements in the NodeList, return the first visible
|
|
if (modal.isElementVisible(nodeList[i])) {
|
|
return nodeList[i];
|
|
}
|
|
}
|
|
} else {
|
|
return nodeList[0];
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
/*
|
|
* Return the last visible element of a nodeList
|
|
*
|
|
* @param nodeList The nodelist to parse
|
|
* @return {Node|null} Returns a specific node or null if no element found
|
|
*/
|
|
getLastElementVisible: function (nodeList) {
|
|
var nodeListLength = nodeList.length;
|
|
var lastTabbableElement = nodeList[nodeListLength - 1];
|
|
|
|
// If the last item is not visible
|
|
if (!modal.isElementVisible(lastTabbableElement)) {
|
|
for (var i = nodeListLength - 1; i >= 0; i--) {
|
|
|
|
// Iterate elements in the NodeList, return the first visible
|
|
if (modal.isElementVisible(nodeList[i])) {
|
|
return nodeList[i];
|
|
}
|
|
}
|
|
} else {
|
|
return lastTabbableElement;
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
/*
|
|
* Convenience function to check if an element is visible
|
|
*
|
|
* Test idea taken from jQuery 1.3.2 source code
|
|
*
|
|
* @param element {Node} element to test
|
|
* @return {boolean} is the element visible or not
|
|
*/
|
|
isElementVisible: function (element) {
|
|
return !(element.offsetWidth === 0 && element.offsetHeight === 0);
|
|
},
|
|
|
|
/*
|
|
* Mark modal as active
|
|
* @param element {Node} element to set active
|
|
*/
|
|
setActive: function (element) {
|
|
modal.addClass(element, 'is-active');
|
|
modal.activeElement = element;
|
|
|
|
// Update aria-hidden
|
|
modal.activeElement.setAttribute('aria-hidden', 'false');
|
|
|
|
// Set the focus to the modal
|
|
modal.setFocus(element.id);
|
|
|
|
// Fire an event
|
|
modal.trigger('cssmodal:show', modal.activeElement);
|
|
},
|
|
|
|
/*
|
|
* Unset previous active modal
|
|
* @param isStacked {boolean} `true` if element is stacked above another
|
|
* @param shouldNotBeStacked {boolean} `true` if next element should be stacked
|
|
*/
|
|
unsetActive: function (isStacked, shouldNotBeStacked) {
|
|
modal.removeClass(document.documentElement, 'has-overlay');
|
|
|
|
if (modal.activeElement) {
|
|
modal.removeClass(modal.activeElement, 'is-active');
|
|
|
|
// Fire an event
|
|
modal.trigger('cssmodal:hide', modal.activeElement);
|
|
|
|
// Update aria-hidden
|
|
modal.activeElement.setAttribute('aria-hidden', 'true');
|
|
|
|
// Unfocus
|
|
modal.removeFocus();
|
|
|
|
// Make modal stacked if needed
|
|
if (isStacked && !shouldNotBeStacked) {
|
|
modal.stackModal(modal.activeElement);
|
|
}
|
|
|
|
// If there are any stacked elements
|
|
if (!isStacked && modal.stackedElements.length > 0) {
|
|
modal.unstackModal();
|
|
}
|
|
|
|
// Reset active element
|
|
modal.activeElement = null;
|
|
}
|
|
},
|
|
|
|
/*
|
|
* Stackable modal
|
|
* @param stackableModal {node} element to be stacked
|
|
*/
|
|
stackModal: function (stackableModal) {
|
|
modal.addClass(stackableModal, 'is-stacked');
|
|
|
|
// Set modal as stacked
|
|
modal.stackedElements.push(modal.activeElement);
|
|
},
|
|
|
|
/*
|
|
* Reactivate stacked modal
|
|
*/
|
|
unstackModal: function () {
|
|
var stackedCount = modal.stackedElements.length;
|
|
var lastStacked = modal.stackedElements[stackedCount - 1];
|
|
|
|
modal.removeClass(lastStacked, 'is-stacked');
|
|
|
|
// Set hash to modal, activates the modal automatically
|
|
global.location.hash = lastStacked.id;
|
|
|
|
// Remove modal from stackedElements array
|
|
modal.stackedElements.splice(stackedCount - 1, 1);
|
|
},
|
|
|
|
/*
|
|
* When displaying modal, prevent background from scrolling
|
|
* @param {Object} event The incoming hashChange event
|
|
* @return {void}
|
|
*/
|
|
mainHandler: function (event, noHash) {
|
|
var hash = global.location.hash.replace('#', '');
|
|
var index = 0;
|
|
var tmp = [];
|
|
var modalElement;
|
|
var modalChild;
|
|
|
|
// JS-only: no hash present
|
|
if (noHash) {
|
|
hash = event.currentTarget.getAttribute('href').replace('#', '');
|
|
}
|
|
|
|
modalElement = document.getElementById(hash);
|
|
|
|
// Check if the hash contains an index
|
|
if (hash.indexOf('/') !== -1) {
|
|
tmp = hash.split('/');
|
|
index = tmp.pop();
|
|
hash = tmp.join('/');
|
|
|
|
// Remove the index from the hash...
|
|
modalElement = document.getElementById(hash);
|
|
|
|
// ... and store the index as a number on the element to
|
|
// make it accessible for plugins
|
|
if (!modalElement) {
|
|
throw new Error('ReferenceError: element "' + hash + '" does not exist!');
|
|
}
|
|
|
|
modalElement.index = (1 * index);
|
|
}
|
|
|
|
// If the hash element exists
|
|
if (modalElement) {
|
|
|
|
// Polyfill to prevent the default behavior of events
|
|
try {
|
|
event.preventDefault();
|
|
} catch (ex) {
|
|
event.returnValue = false;
|
|
}
|
|
|
|
// Get first element in selected element
|
|
modalChild = modalElement.children[0];
|
|
|
|
// When we deal with a modal and body-class `has-overlay` is not set
|
|
if (modalChild && modalChild.className.match(/modal-inner/)) {
|
|
|
|
// Make previous element stackable if it is not the same modal
|
|
modal.unsetActive(
|
|
!modal.hasClass(modalElement, 'is-active'),
|
|
(modalElement.getAttribute('data-stackable') === 'false')
|
|
);
|
|
|
|
// Set an html class to prevent scrolling
|
|
modal.addClass(document.documentElement, 'has-overlay');
|
|
|
|
// Set scroll position for modal
|
|
modal._currentScrollPositionY = global.scrollY;
|
|
modal._currentScrollPositionX = global.scrollX;
|
|
|
|
// Mark the active element
|
|
modal.setActive(modalElement);
|
|
modal.activeElement._noHash = noHash;
|
|
}
|
|
} else {
|
|
|
|
// If activeElement is already defined, delete it
|
|
modal.unsetActive();
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Inject iframes
|
|
*/
|
|
injectIframes: function () {
|
|
var iframes = document.querySelectorAll('[data-iframe-src]');
|
|
var iframe;
|
|
var i = 0;
|
|
|
|
for (; i < iframes.length; i++) {
|
|
iframe = document.createElement('iframe');
|
|
|
|
iframe.src = iframes[i].getAttribute('data-iframe-src');
|
|
iframe.setAttribute('webkitallowfullscreen', true);
|
|
iframe.setAttribute('mozallowfullscreen', true);
|
|
iframe.setAttribute('allowfullscreen', true);
|
|
|
|
iframes[i].appendChild(iframe);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Listen to all relevant events
|
|
* @return {void}
|
|
*/
|
|
init: function () {
|
|
|
|
/*
|
|
* Hide overlay when ESC is pressed
|
|
*/
|
|
this.on('keyup', document, function (event) {
|
|
var hash = global.location.hash.replace('#', '');
|
|
|
|
// If key ESC is pressed
|
|
if (event.keyCode === 27) {
|
|
if (modal.activeElement && hash === modal.activeElement.id) {
|
|
global.location.hash = '!';
|
|
} else {
|
|
modal.unsetActive();
|
|
}
|
|
|
|
if (modal.lastActive) {
|
|
return false;
|
|
}
|
|
|
|
// Unfocus
|
|
modal.removeFocus();
|
|
}
|
|
}, false);
|
|
|
|
/**
|
|
* Trigger main handler on click if hash is deactivated
|
|
*/
|
|
this.on('click', document.querySelectorAll('[data-cssmodal-nohash]'), function (event) {
|
|
modal.mainHandler(event, true);
|
|
});
|
|
|
|
// And close modal without hash
|
|
this.on('click', document.querySelectorAll('.modal-close'), function (event) {
|
|
if (modal.activeElement._noHash){
|
|
modal.mainHandler(event, true);
|
|
}
|
|
});
|
|
|
|
/*
|
|
* Trigger main handler on load and hashchange
|
|
*/
|
|
this.on('hashchange', global, modal.mainHandler);
|
|
this.on('load', global, modal.mainHandler);
|
|
|
|
/**
|
|
* Prevent scrolling when modal is active
|
|
* @return {void}
|
|
*/
|
|
global.onscroll = global.onmousewheel = function () {
|
|
if (document.documentElement.className.match(/has-overlay/)) {
|
|
global.scrollTo(modal._currentScrollPositionX, modal._currentScrollPositionY);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Inject iframes
|
|
*/
|
|
modal.injectIframes();
|
|
}
|
|
};
|
|
|
|
/*
|
|
* AMD, module loader, global registration
|
|
*/
|
|
|
|
// Expose modal for loaders that implement the Node module pattern.
|
|
if (typeof module === 'object' && module && typeof module.exports === 'object') {
|
|
module.exports = modal;
|
|
|
|
// Register as an AMD module
|
|
} else if (typeof define === 'function' && define.amd) {
|
|
define('CSSModal', [], function () {
|
|
|
|
// We use jQuery if the browser doesn't support CustomEvents
|
|
if (!global.CustomEvent && !$) {
|
|
throw new Error('This browser doesn\'t support CustomEvent - please include jQuery.');
|
|
}
|
|
|
|
modal.init();
|
|
|
|
return modal;
|
|
});
|
|
|
|
// Export CSSModal into global space
|
|
} else if (typeof global === 'object' && typeof global.document === 'object') {
|
|
global.CSSModal = modal;
|
|
modal.init();
|
|
}
|
|
|
|
}(window, window.jQuery));
|