;(function ($) { "use strict"; /** * Remodal settings */ var pluginName = "remodal", defaults = { hashTracking: true, closeOnConfirm: true, closeOnCancel: true }; /** * Special plugin object for instances. * @type {Object} */ $[pluginName] = { lookup: [] }; var current, // current modal scrollTop; // scroll position /** * Get transition duration in ms * @return {Number} */ var getTransitionDuration = function ($elem) { var duration = $elem.css("transition-duration") || $elem.css("-webkit-transition-duration") || $elem.css("-moz-transition-duration") || $elem.css("-o-transition-duration") || $elem.css("-ms-transition-duration") || 0; var delay = $elem.css("transition-delay") || $elem.css("-webkit-transition-delay") || $elem.css("-moz-transition-delay") || $elem.css("-o-transition-delay") || $elem.css("-ms-transition-delay") || 0; return (parseFloat(duration) + parseFloat(delay)) * 1000; }; /** * Get a scrollbar width * @return {Number} */ var getScrollbarWidth = function () { if ($(document.body).height() <= $(window).height()) { return 0; } var outer = document.createElement("div"); outer.style.visibility = "hidden"; outer.style.width = "100px"; document.body.appendChild(outer); var widthNoScroll = outer.offsetWidth; // force scrollbars outer.style.overflow = "scroll"; // add innerdiv var inner = document.createElement("div"); inner.style.width = "100%"; outer.appendChild(inner); var widthWithScroll = inner.offsetWidth; // remove divs outer.parentNode.removeChild(outer); return widthNoScroll - widthWithScroll; }; /** * Lock screen */ var lockScreen = function () { $(document.body).css("", "+=" + getScrollbarWidth()); $("html, body").addClass(pluginName + "_lock"); }; /** * Unlock screen */ var unlockScreen = function () { $(document.body).css("", "-=" + getScrollbarWidth()); $("html, body").removeClass(pluginName + "_lock"); }; /** * Parse string with options * @param str * @returns {Object} */ var parseOptions = function (str) { var obj = {}, clearedStr, arr; // remove spaces before and after delimiters clearedStr = str.replace(/\s*:\s*/g, ":").replace(/\s*,\s*/g, ","); // parse string arr = clearedStr.split(","); var i, len, val; for (i = 0, len = arr.length; i < len; i++) { arr[i] = arr[i].split(":"); val = arr[i][1]; // convert string value if it is like a boolean if (typeof val === "string" || val instanceof String) { val = val === "true" || (val === "false" ? false : val); } // convert 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; }; /** * Remodal constructor */ function Remodal(modal, options) { this.settings = $.extend({}, defaults, options); this.modal = modal; this.buildDOM(); this.addEventListeners(); this.index = $[pluginName].lookup.push(this) - 1; this.busy = false; } /** * Build required DOM */ Remodal.prototype.buildDOM = function () { this.body = $(document.body); this.bg = $("." + pluginName + "-bg"); this.modalClose = $("").addClass(pluginName + "-close"); this.overlay = $("
").addClass(pluginName + "-overlay"); if (!this.modal.hasClass(pluginName)) { this.modal.addClass(pluginName); } this.modal.css("visibility", "visible"); this.modal.append(this.modalClose); this.overlay.append(this.modal); this.body.append(this.overlay); this.confirm = this.modal.find("." + pluginName + "-confirm"); this.cancel = this.modal.find("." + pluginName + "-cancel"); var tdOverlay = getTransitionDuration(this.overlay), tdModal = getTransitionDuration(this.modal), tdBg = getTransitionDuration(this.bg); this.td = tdModal > tdOverlay ? tdModal : tdOverlay; this.td = tdBg > this.td ? tdBg : this.td; }; /** * Add event listeners to the current modal window */ Remodal.prototype.addEventListeners = function () { var self = this; this.modalClose.bind("click." + pluginName, function (e) { e.preventDefault(); self.close(); }); this.cancel.bind("click." + pluginName, function (e) { e.preventDefault(); self.modal.trigger("cancel"); if (self.settings.closeOnCancel) { self.close(); } }); this.confirm.bind("click." + pluginName, function (e) { e.preventDefault(); self.modal.trigger("confirm"); if (self.settings.closeOnConfirm) { self.close(); } }); $(document).bind("keyup." + pluginName, function (e) { if (e.keyCode === 27) { self.close(); } }); this.overlay.bind("click." + pluginName, function (e) { var $target = $(e.target); if (!$target.hasClass(pluginName + "-overlay")) { return; } self.close(); }); }; /** * Open modal window */ Remodal.prototype.open = function () { // check if animation is complete if (this.busy) { return; } this.busy = true; this.modal.trigger("open"); var id = this.modal.attr("data-" + pluginName + "-id"); if (id && this.settings.hashTracking) { scrollTop = $(window).scrollTop(); location.hash = id; } if (current && current !== this) { current.overlay.hide(); current.body.removeClass(pluginName + "_active"); } current = this; lockScreen(); this.overlay.show(); var self = this; setTimeout(function () { self.body.addClass(pluginName + "_active"); setTimeout(function () { self.busy = false; self.modal.trigger("opened"); }, self.td + 50); }, 25); }; /** * Close modal window */ Remodal.prototype.close = function () { // check if animation is complete if (this.busy) { return; } this.busy = true; this.modal.trigger("close"); if (this.settings.hashTracking && this.modal.attr("data-" + pluginName + "-id") === location.hash.substr(1)) { location.hash = ""; $(window).scrollTop(scrollTop); } this.body.removeClass(pluginName + "_active"); var self = this; setTimeout(function () { self.overlay.hide(); unlockScreen(); self.busy = false; self.modal.trigger("closed"); }, self.td + 50); }; if ($) { $.fn[pluginName] = function (opts) { var instance; this.each(function (i, e) { var $e = $(e); if ($e.data(pluginName) == null) { instance = new Remodal($e, opts); $e.data(pluginName, instance.index); if (instance.settings.hashTracking && $e.attr("data-" + pluginName + "-id") === location.hash.substr(1)) { instance.open(); } } }); return instance; }; } $(document).ready(function () { /** * data-remodal-target opens a modal window with a special id without hash change. */ $(document).on("click", "[data-" + pluginName + "-target]", function (e) { e.preventDefault(); var elem = e.currentTarget, id = elem.getAttribute("data-" + pluginName + "-target"), $target = $("[data-" + pluginName + "-id=" + id + "]"); $[pluginName].lookup[$target.data(pluginName)].open(); }); /** * Auto initialization of modal windows. * They should have the 'remodal' class attribute. * Also you can pass params into the modal throw the data-remodal-options attribute. * data-remodal-options must be a valid JSON string. */ $(document).find("." + pluginName).each(function (i, container) { var $container = $(container), options = $container.data(pluginName + "-options"); if (!options) { options = {}; } else if (typeof options === "string" || options instanceof String) { options = parseOptions(options); } $container[pluginName](options); }); }); /** * Hashchange handling to show a modal with a special id. */ var hashHandler = function (e, closeOnEmptyHash) { var id = location.hash.replace("#", ""); if (typeof closeOnEmptyHash === "undefined") { closeOnEmptyHash = true; } if (!id) { if (closeOnEmptyHash) { // check if we have currently opened modal and animation is complete if (current && !current.busy && current.settings.hashTracking) { current.close(); } } } else { var $elem; // Catch syntax error if your hash is bad try { $elem = $("[data-" + pluginName + "-id=" + id.replace(new RegExp("/", "g"), "\\/") + "]"); } catch (e) {} if ($elem && $elem.length) { var instance = $[pluginName].lookup[$elem.data(pluginName)]; if (instance && instance.settings.hashTracking) { instance.open(); } } } }; $(window).bind("hashchange." + pluginName, hashHandler); })(window.jQuery || window.Zepto);