/* * Remodal - v1.1.0 * Responsive, lightweight, fast, synchronized with CSS animations, fully customizable modal window plugin with declarative configuration and hash tracking. * http://vodkabears.github.io/remodal/ * * Made by Ilya Makarov * Under MIT License */ !(function(root, factory) { if (typeof define === 'function' && define.amd) { define(['jquery'], function($) { return factory(root, $); }); } else if (typeof exports === 'object') { factory(root, require('jquery')); } else { factory(root, root.jQuery || root.Zepto); } })(this, function(global, $) { 'use strict'; /** * Name of the plugin * @private * @const * @type {String} */ var PLUGIN_NAME = 'remodal'; /** * Namespace for CSS and events * @private * @const * @type {String} */ var NAMESPACE = global.REMODAL_GLOBALS && global.REMODAL_GLOBALS.NAMESPACE || PLUGIN_NAME; /** * Animationstart event with vendor prefixes * @private * @const * @type {String} */ var ANIMATIONSTART_EVENTS = $.map( ['animationstart', 'webkitAnimationStart', 'MSAnimationStart', 'oAnimationStart'], function(eventName) { return eventName + '.' + NAMESPACE; } ).join(' '); /** * Animationend event with vendor prefixes * @private * @const * @type {String} */ var ANIMATIONEND_EVENTS = $.map( ['animationend', 'webkitAnimationEnd', 'MSAnimationEnd', 'oAnimationEnd'], function(eventName) { return eventName + '.' + NAMESPACE; } ).join(' '); /** * Default settings * @private * @const * @type {Object} */ var DEFAULTS = $.extend({ hashTracking: true, closeOnConfirm: true, closeOnCancel: true, closeOnEscape: true, closeOnOutsideClick: true, modifier: '', appendTo: null }, global.REMODAL_GLOBALS && global.REMODAL_GLOBALS.DEFAULTS); /** * States of the Remodal * @private * @const * @enum {String} */ var STATES = { CLOSING: 'closing', CLOSED: 'closed', OPENING: 'opening', OPENED: 'opened' }; /** * Reasons of the state change. * @private * @const * @enum {String} */ var STATE_CHANGE_REASONS = { CONFIRMATION: 'confirmation', CANCELLATION: 'cancellation' }; /** * Is animation supported? * @private * @const * @type {Boolean} */ var IS_ANIMATION = (function() { var style = document.createElement('div').style; return style.animationName !== undefined || style.WebkitAnimationName !== undefined || style.MozAnimationName !== undefined || style.msAnimationName !== undefined || style.OAnimationName !== undefined; })(); /** * Is iOS? * @private * @const * @type {Boolean} */ var IS_IOS = /iPad|iPhone|iPod/.test(navigator.platform); /** * Current modal * @private * @type {Remodal} */ var current; /** * Scrollbar position * @private * @type {Number} */ var scrollTop; /** * Returns an animation duration * @private * @param {jQuery} $elem * @returns {Number} */ function getAnimationDuration($elem) { if ( IS_ANIMATION && $elem.css('animation-name') === 'none' && $elem.css('-webkit-animation-name') === 'none' && $elem.css('-moz-animation-name') === 'none' && $elem.css('-o-animation-name') === 'none' && $elem.css('-ms-animation-name') === 'none' ) { return 0; } var duration = $elem.css('animation-duration') || $elem.css('-webkit-animation-duration') || $elem.css('-moz-animation-duration') || $elem.css('-o-animation-duration') || $elem.css('-ms-animation-duration') || '0s'; var delay = $elem.css('animation-delay') || $elem.css('-webkit-animation-delay') || $elem.css('-moz-animation-delay') || $elem.css('-o-animation-delay') || $elem.css('-ms-animation-delay') || '0s'; var iterationCount = $elem.css('animation-iteration-count') || $elem.css('-webkit-animation-iteration-count') || $elem.css('-moz-animation-iteration-count') || $elem.css('-o-animation-iteration-count') || $elem.css('-ms-animation-iteration-count') || '1'; var max; var len; var num; var i; duration = duration.split(', '); delay = delay.split(', '); iterationCount = iterationCount.split(', '); // The 'duration' size is the same as the 'delay' size for (i = 0, len = duration.length, max = Number.NEGATIVE_INFINITY; i < len; i++) { num = parseFloat(duration[i]) * parseInt(iterationCount[i], 10) + parseFloat(delay[i]); if (num > max) { max = num; } } return max; } /** * Returns a scrollbar width * @private * @returns {Number} */ function getScrollbarWidth() { if ($(document.body).height() <= $(window).height()) { return 0; } var outer = document.createElement('div'); var inner = document.createElement('div'); var widthNoScroll; var widthWithScroll; outer.style.visibility = 'hidden'; outer.style.width = '100px'; document.body.appendChild(outer); widthNoScroll = outer.offsetWidth; // Force scrollbars outer.style.overflow = 'scroll'; // Add inner div inner.style.width = '100%'; outer.appendChild(inner); widthWithScroll = inner.offsetWidth; // Remove divs outer.parentNode.removeChild(outer); return widthNoScroll - widthWithScroll; } /** * Locks the screen * @private */ function lockScreen() { if (IS_IOS) { return; } var $html = $('html'); var lockedClass = namespacify('is-locked'); var paddingRight; var $body; if (!$html.hasClass(lockedClass)) { $body = $(document.body); // Zepto does not support '-=', '+=' in the `css` method paddingRight = parseInt($body.css('padding-right'), 10) + getScrollbarWidth(); $body.css('padding-right', paddingRight + 'px'); $html.addClass(lockedClass); } } /** * Unlocks the screen * @private */ function unlockScreen() { if (IS_IOS) { return; } var $html = $('html'); var lockedClass = namespacify('is-locked'); var paddingRight; var $body; if ($html.hasClass(lockedClass)) { $body = $(document.body); // Zepto does not support '-=', '+=' in the `css` method paddingRight = parseInt($body.css('padding-right'), 10) - getScrollbarWidth(); $body.css('padding-right', paddingRight + 'px'); $html.removeClass(lockedClass); } } /** * Sets a state for an instance * @private * @param {Remodal} instance * @param {STATES} state * @param {Boolean} isSilent If true, Remodal does not trigger events * @param {String} Reason of a state change. */ function setState(instance, state, isSilent, reason) { var newState = namespacify('is', state); var allStates = [namespacify('is', STATES.CLOSING), namespacify('is', STATES.OPENING), namespacify('is', STATES.CLOSED), namespacify('is', STATES.OPENED)].join(' '); instance.$bg .removeClass(allStates) .addClass(newState); instance.$overlay .removeClass(allStates) .addClass(newState); instance.$wrapper .removeClass(allStates) .addClass(newState); instance.$modal .removeClass(allStates) .addClass(newState); instance.state = state; !isSilent && instance.$modal.trigger({ type: state, reason: reason }, [{ reason: reason }]); } /** * Synchronizes with the animation * @param {Function} doBeforeAnimation * @param {Function} doAfterAnimation * @param {Remodal} instance */ function syncWithAnimation(doBeforeAnimation, doAfterAnimation, instance) { var runningAnimationsCount = 0; var handleAnimationStart = function(e) { if (e.target !== this) { return; } runningAnimationsCount++; }; var handleAnimationEnd = function(e) { if (e.target !== this) { return; } if (--runningAnimationsCount === 0) { // Remove event listeners $.each(['$bg', '$overlay', '$wrapper', '$modal'], function(index, elemName) { instance[elemName].off(ANIMATIONSTART_EVENTS + ' ' + ANIMATIONEND_EVENTS); }); doAfterAnimation(); } }; $.each(['$bg', '$overlay', '$wrapper', '$modal'], function(index, elemName) { instance[elemName] .on(ANIMATIONSTART_EVENTS, handleAnimationStart) .on(ANIMATIONEND_EVENTS, handleAnimationEnd); }); doBeforeAnimation(); // If the animation is not supported by a browser or its duration is 0 if ( getAnimationDuration(instance.$bg) === 0 && getAnimationDuration(instance.$overlay) === 0 && getAnimationDuration(instance.$wrapper) === 0 && getAnimationDuration(instance.$modal) === 0 ) { // Remove event listeners $.each(['$bg', '$overlay', '$wrapper', '$modal'], function(index, elemName) { instance[elemName].off(ANIMATIONSTART_EVENTS + ' ' + ANIMATIONEND_EVENTS); }); doAfterAnimation(); } } /** * Closes immediately * @private * @param {Remodal} instance */ function halt(instance) { if (instance.state === STATES.CLOSED) { return; } $.each(['$bg', '$overlay', '$wrapper', '$modal'], function(index, elemName) { instance[elemName].off(ANIMATIONSTART_EVENTS + ' ' + ANIMATIONEND_EVENTS); }); instance.$bg.removeClass(instance.settings.modifier); instance.$overlay.removeClass(instance.settings.modifier).hide(); instance.$wrapper.hide(); unlockScreen(); setState(instance, STATES.CLOSED, true); } /** * Parses a string with options * @private * @param str * @returns {Object} */ function parseOptions(str) { var obj = {}; var arr; var len; var val; var i; // Remove spaces before and after delimiters str = str.replace(/\s*:\s*/g, ':').replace(/\s*,\s*/g, ','); // Parse a string arr = str.split(','); for (i = 0, len = arr.length; i < len; i++) { arr[i] = arr[i].split(':'); val = arr[i][1]; // Convert a string value if it is like a boolean if (typeof val === 'string' || val instanceof String) { val = val === 'true' || (val === 'false' ? false : val); } // Convert a string value if it is like a number if (typeof val === 'string' || val instanceof String) { val = !isNaN(val) ? +val : val; } obj[arr[i][0]] = val; } return obj; } /** * Generates a string separated by dashes and prefixed with NAMESPACE * @private * @param {...String} * @returns {String} */ function namespacify() { var result = NAMESPACE; for (var i = 0; i < arguments.length; ++i) { result += '-' + arguments[i]; } return result; } /** * Handles the hashchange event * @private * @listens hashchange */ function handleHashChangeEvent() { var id = location.hash.replace('#', ''); var instance; var $elem; if (!id) { // Check if we have currently opened modal and animation was completed if (current && current.state === STATES.OPENED && current.settings.hashTracking) { current.close(); } } else { // Catch syntax error if your hash is bad try { $elem = $( '[data-' + PLUGIN_NAME + '-id="' + id + '"]' ); } catch (err) {} if ($elem && $elem.length) { instance = $[PLUGIN_NAME].lookup[$elem.data(PLUGIN_NAME)]; if (instance && instance.settings.hashTracking) { instance.open(); } } } } /** * Remodal constructor * @constructor * @param {jQuery} $modal * @param {Object} options */ function Remodal($modal, options) { var $body = $(document.body); var $appendTo = $body; var remodal = this; remodal.settings = $.extend({}, DEFAULTS, options); remodal.index = $[PLUGIN_NAME].lookup.push(remodal) - 1; remodal.state = STATES.CLOSED; remodal.$overlay = $('.' + namespacify('overlay')); if (remodal.settings.appendTo !== null && remodal.settings.appendTo.length) { $appendTo = $(remodal.settings.appendTo); } if (!remodal.$overlay.length) { remodal.$overlay = $('