diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a3b469..d5fa73b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## HEAD +## 1.4.1 - 17.07.2015 * Add missing comma in bower.json * Add example for stackable modal diff --git a/download/modal.js b/download/modal.js new file mode 100644 index 0000000..563c469 --- /dev/null +++ b/download/modal.js @@ -0,0 +1,540 @@ +/*! + * 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)); diff --git a/download/modal.scss b/download/modal.scss new file mode 100644 index 0000000..ee25ae4 --- /dev/null +++ b/download/modal.scss @@ -0,0 +1,21 @@ +/** + * CSS Modal + * http://drublic.github.com/css-modal + * + * @author Hans Christian Reinl - @drublic + */ + +// Configuration variables +@import 'modal-config'; + +// Core operations +@import 'modal-core'; + +// Modal animations +@import 'modal-animations'; + +// Theme styles +@import 'modal-theme'; + +// Include the modal element with it's selector +@import 'modal-element'; diff --git a/package.json b/package.json index 4b72b29..b12ba51 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "css-modal", "title": "CSS Modal", "description": "Modals, built with pure CSS", - "version": "1.4.0", + "version": "1.4.1", "homepage": "https://github.com/drublic/css-modal", "author": { "name": "Hans Christian Reinl",