Seenginx/Seenginx/wwwroot/js/modal.js

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));