define(["lib/jquery", "lib/underscore", "common/util/dom_utils", "./scrollable", "ARCore/keyboards", "common/util/check", "common/util/binding_list"], function($, _, DOMUtils, Scrollable, Keyboards, check, BindingList) { "use strict"; // HTML for background var backdropHTML = " \ <div class=\"lens-modal-backdrop\"> \ <div class=\"lens-modal-backdrop-stripes\"></div> \ <div class=\"lens-modal-backdrop-circle\"></div> \ </div> \ "; // template for button modals var buttonTemplate = " \ <% if(title) { %> \ <h1><%= title %></h1> \ <% } %> \ \ <% if(text) { %> \ <p><%= text %></p> \ <% } %> \ \ <div class=\"lens-modal-buttons\"></div> \ "; var makeButtonModal = function(title, text, buttons, modal, buttonOrder, image) { if(!modal) { modal = Modal("50%"); } var content; var buttonContainer; var el = $(modal.el); if (buttonOrder && buttonOrder === "vertical"){ content = _.template(selectTemplate, { title: title, text: text, image: image, }); el.html(content); buttonContainer = el.find(".lens-modal-options"); if (el.css("height")){ buttonContainer.css("max-height", el.css("height")); } Scrollable(buttonContainer); } else{ content = _.template(buttonTemplate, { title: title, text: text, image: image, }); el.html(content); buttonContainer = el.find(".lens-modal-buttons"); } if(image){ $("<img>").attr("src", image) .appendTo(buttonContainer); } buttons.forEach(function(button) { $("<button />").text(button.text) .addClass(button.class) .click(button.callback) .appendTo(buttonContainer); }); return modal; }; // template for select modal var selectTemplate = " \ <% if(title) { %> \ <h1><%= title %></h1> \ <% } %> \ \ <% if(text) { %> \ <p><%= text %></p> \ <% } %> \ \ <div class=\"lens-modal-options\"></div> \ "; // template for text entry modal var textTemplate = " \ <div> \ <button class=\"lens-text-entry-cancel lens-iconify\">close</button> \ <input class=\"lens-text-entry-input\" type=\"text\" data-lens-ignore=\"true\" /> \ <button class=\"lens-text-entry-save lens-iconify\">ok</button> \ </div> \ <div class=\"lens-text-entry-keyboard\"> \ </div> \ "; // template for image modals var imageTemplate = " \ <% if(title) { %> \ <h1><%= title %></h1> \ <% } %> \ \ <% if(text) { %> \ <p><%= text %></p> \ <% } %> \ \ <div class=\"lens-modal-images\"></div> \ "; var makeImageModal = function(title, text, images, modal) { if(!modal) { modal = Modal("75%"); } var el = $(modal.el); var content = _.template(imageTemplate, { title: title, text: text }); el.html(content); var imageContainer = el.find(".lens-modal-images"); images.forEach(function(image) { $("<img>").attr("src", image.src) .addClass("image") .addClass(image.class) .click(image.callback) .appendTo(imageContainer); }); Scrollable(el); return modal; }; // returns a function that calls hides and destroys the modal, then // calls the callback, passing in arg var makeCallback = function(modal, callback, arg) { if(!callback) { callback = $.noop; } return function() { modal.hide(function() { modal.destroy(); callback(arg); }); }; }; // binds a handler to the transitionend event of an element that clears // this handler and calls the given callback var setTransitionCallback = function(el, callback) { if(!callback) { return; } $(el).on("webkitTransitionEnd.lens-setTransitionCallback", function() { $(el).off("webkitTransitionEnd.lens-setTransitionCallback"); callback(); }); }; /** * Creates a new Modal to display a DOM element. You generally won't need * this; use one of the factory functions to open a dialog of a particular * type. * * @param {String} [width] Width of the modal, in any CSS unit. * @param {String} [height] Height of the modal, in any CSS unit. * * @class The Modal Component allows you to open modal dialogs to display * a message or prompt for input. * * @property {Element} el The modal element that should be populated with * content * @memberOf Lens.Components */ var Modal = function(width, height) { check(width, check.Match.Optional(String)); check(height, check.Match.Optional(String)); /** @alias Lens.Components.Modal.prototype */ var self = {}; var backdrop = $($.parseHTML(backdropHTML)); var wrapper = $("<div class=\"lens-modal-wrapper\" />"); var modal = $("<div class=\"lens-modal\" />"); var showSubscribers = BindingList(); var hideSubscribers = BindingList(); var destroySubscribers = BindingList(); self.el = modal[0]; if(width) { modal.css("width", width); } if(height) { modal.css("height", height); } backdrop.hide().appendTo("body"); wrapper.hide().appendTo("body"); modal.appendTo(wrapper); /** * Shows the modal. * @param {Function} [onComplete] Called when the modal is fully shown. */ self.show = function(hideBackdrop, onComplete) { check(onComplete, check.Match.Optional(Function)); if (!hideBackdrop || hideBackdrop === null) { backdrop.show().css("opacity", 1); } setTransitionCallback(wrapper, onComplete); wrapper.show().css("transform", "scale(1)"); if(Lens.LAF.Scrollbars) { Lens.LAF.Scrollbars.disableBodyScroll(); } showSubscribers.callAll(self); }; /** * Hides the modal. * @param {Function} [onComplete] Called when the modal is fully hidden. */ self.hide = function(onComplete) { check(onComplete, check.Match.Optional(Function)); setTransitionCallback(backdrop, function() { backdrop.hide(); }); backdrop.css("opacity", 0); setTransitionCallback(wrapper, function() { wrapper.hide(); if(onComplete) { onComplete(); } }); wrapper.css("transform", "scale(0)"); if(Lens.LAF.Scrollbars) { Lens.LAF.Scrollbars.enableBodyScroll(); } hideSubscribers.callAll(self); }; /** * Removes the modal from the DOM. */ self.destroy = function() { wrapper.remove(); backdrop.remove(); destroySubscribers.callAll(self); }; /** * Registers a function to be called whenever a Modal is shown. * * @param {Function} callback * The function to call. Gets one {@link Modal} as an * argument. * * @return {Binding} A Binding that allows this handler to be cleared. */ self.shown = function(callback) { check(callback, Function); return showSubscribers.add(callback); }; /** * Registers a function to be called whenever a Modal is hidden. * * @param {Function} callback * The function to call. Gets a {@link Modal} as an argument. * * @return {Binding} A Binding that allows this handler to be cleared. */ self.hidden = function(callback){ check(callback, Function); return hideSubscribers.add(callback); }; /** * Registers a function to be called whenever a Modal is destroyed. * * @param {Function} callback * The function to call. Gets a {@link Modal} as an argument. * * @return {Binding} A Binding that allows this handler to be cleared. */ self.destroyed = function(callback){ check(callback, Function); return destroySubscribers.add(callback); }; Object.freeze(self); return self; }; _.extend(Modal, /** @lends Lens.Components.Modal */ { /** * Shows a modal dialog with a message and a button to dismiss * the modal. * * @param {Object} options * Object with options. Alternatively, a single string, in which * case this string will be used as options.text and all other * options will be left as the default. * * @param {String} options.text * Body text for the alert. * * @param {String} [options.title] * Title of the alert. Default: no title. * * @param {String} [options.width] * CSS width of the alert. Default: 50%. * * @param {String} [options.height] * CSS height of the alert. Default: None. * * @param {Boolean} [options.hideBackdrop] * Optional backdrop of the alert. Default: Shows backdrop. * * @param {Boolean} [options.hideBorder] * Optional border of the alert. Default: Shows border. * * @param {String} [options.buttonText] * Text for the button to dismiss the alert. * * @param {Function} [onDismiss] * Function to be called when the alert is dismissed. * * @return {Modal} * * @example * Modal.alert("foo"); * * @example * Modal.alert({ * title: "Notice", * text: "A thing happened", * buttonText: "Yay!" * }, function() { * // called when the alert is dismissed * }); */ alert: function(options, onDismiss) { check(options, check.Match.OneOf(String, { text: String, title: check.Match.Optional(String), buttonText: check.Match.Optional(String), width: check.Match.Optional(String), height: check.Match.Optional(String), hideBackdrop: check.Match.Optional(Boolean), hideBorder: check.Match.Optional(Boolean), })); check(onDismiss, check.Match.Optional(Function)); if (typeof options === "string") { options = {text: options}; } var modal = Modal("50%"); if (options.width){ $(modal.el).css("width", options.width); } if (options.height){ $(modal.el).css("height", options.height); } makeButtonModal(options.title, options.text, [ { text: "OK", class: "btn-primary", callback: makeCallback(modal, onDismiss) } ], modal); if (options.hideBorder) { $(modal.el).css("border",""); } if (options.hideBackdrop) { modal.show(true); } else { modal.show(); } return modal; }, /** * Shows a modal dialog with a message and affirmative and negative * buttons. * * @param {Object} options * Object with options. Alternatively, a single string, in which * case this string will be used as options.text and all other * options will be left as the default. * * @param {String} options.text * Body text for the alert. * * @param {String} [options.title] * Title of the alert. Default: no title. * * @param {String} [options.width] * CSS width of the alert. Default: 50%. * * @param {String} [options.height] * CSS height of the alert. Default: None. * * @param {Boolean} [options.hideBorder] * Optional border of the alert. Default: Shows border. * * @param {Boolean} [options.hideBackdrop] * Optional backdrop of the alert. Default: Shows backdrop. * * @param {String} [options.okText] * Text for the affirmative button. Defaults to "OK". * * @param {String} [options.cancelText] * Text for the negative button. Default to "Cancel". * * @param {Function} callback * Function to be called when the alert is dismissed. Will get a * single argument: true if the affirmative button was pressed, * or false if the negative button was pressed. * * @return {Modal} * * @example * Modal.confirm("foo", function(response) { * // response is true or false * }); * * @example * Modal.confirm({ * title: "Tell Me", * text: "Should I continue?", * okText: "sure!", * cancelText: "nope." * }, function(response) {}); */ confirm: function(options, callback) { check(options, check.Match.OneOf(String, { text: String, title: check.Match.Optional(String), width: check.Match.Optional(String), height: check.Match.Optional(String), hideBorder: check.Match.Optional(Boolean), hideBackdrop: check.Match.Optional(Boolean), okText: check.Match.Optional(String), cancelText: check.Match.Optional(String) })); check(callback, Function); if (typeof options === "string") { options = {text: options}; } var modal = Modal("50%"); if (options.width){ $(modal.el).css("width", options.width); } if (options.height){ $(modal.el).css("height", options.height); } makeButtonModal(options.title, options.text, [ { text: (options.cancelText || "Cancel"), class: "btn-danger", callback: makeCallback(modal, callback, false) }, { text: (options.okText || "OK"), class: "btn-primary", callback: makeCallback(modal, callback, true) } ], modal); if (options.hideBorder) { $(modal.el).css("border", "none"); } if (options.hideBackdrop) { modal.show(true); } else { modal.show(); } return modal; }, /** * Shows a modal dialog with a message and arbitrary buttons. * * @param {Object} options * Object with options. * * @param {Array<Object>} options.buttons * Array of buttons. Each button is specified as an object with * a "text" property (which specifies the text displayed on the * button), a "callback" property (which is called when the button * is pressed), and an optional "class" property (which specifies * a CSS class to give to the button, such as btn-primary). * * @param {String} [options.image] * Path to image for the alert. Default: no image. * * @param {String} [options.text] * Body text for the alert. Default: no text. * * @param {String} [options.title] * Title of the alert. Default: no title. * * @param {String} [options.width] * CSS width of the alert. Default: 50%. * * @param {String} [options.height] * CSS height of the alert. Default: None. * * @param {String} [options.buttonOrder] * Display order of buttons. "vertical" or "horizontal". Default: horizontal. * * @param {Boolean} [options.hideBorder] * Optional border of the alert. Default: Shows border. * * @param {Boolean} [options.hideBackdrop] * Optional backdrop of the alert. Default: Shows backdrop. * @return {Modal} * * @example * Modal.confirm("foo", function(response) { * // response is true or false * }); * * @example * Modal.buttons({ * title: "Which is best?", * text: "Pick a flavor:", * buttons: [ * {text: "vanilla", class: "white-button", callback: function(){}}, * {text: "chocolate", class: "brown-button", callback: function(){}}, * {text: "strawberry", class: "red-button", callback: function(){}} * ] * }) */ buttons: function(options) { check(options, { image: check.Match.Optional(String), text: check.Match.Optional(String), title: check.Match.Optional(String), width: check.Match.Optional(String), height: check.Match.Optional(String), buttonOrder: check.Match.Optional(String), hideBorder: check.Match.Optional(Boolean), hideBackdrop: check.Match.Optional(Boolean), buttons: [{ text: String, callback: Function, class: check.Match.Optional(String) }] }); if (options.buttonOrder) { if (!(options.buttonOrder==="vertical" || options.buttonOrder==="horizontal")) { throw check.Match.Error("Expected string 'vertical' or 'horizontal', got " + options.buttonOrder); } } // we need to make the button callbacks dismiss the modal var modal = Modal("50%"); if (options.width){ $(modal.el).css("width", options.width); } if (options.height){ $(modal.el).css("height", options.height); } options.buttons.forEach(function(button) { button.callback = makeCallback(modal, button.callback); }); if (options.buttonOrder && options.buttonOrder === "vertical"){ makeButtonModal(options.title, options.text, options.buttons, modal, "vertical", options.image); } else{ makeButtonModal(options.title, options.text, options.buttons, modal, null, options.image); } if (options.hideBorder) { $(modal.el).css("border", "none"); } if (options.hideBackdrop) { modal.show(true); } else { modal.show(); } return modal; }, /** * Shows a modal dialog with a selectable images. * * @param {Object} options * Object with options. * * @param {Array<Object>} options.images * Array of images. Each image is specified as an object with a * "src" property (which specifies the image that will be * displayed), a "callback" property (which is called when the * image is pressed and passed the "src" string of the image that * was pressed), and an optional "class" property (which specifies * a CSS class to give to the image). By default, the images are * scaled to fit a scrollable grid three images wide (each 300px by * 250px). Note: two classes are used to apply default styles, so * CSS styles defined with the "class" may need to be more specific * or use "!important" to override the default styling for that * image (ex. for width and height). See * http://www.vanseodesign.com/css/css-specificity-inheritance-cascaade/ * for an explanation of CSS precedence. * * @param {String} [options.text] * Body text for the modal. Default: no text. * * @param {String} [options.title] * Title of the modal. Default: no title. * * @param {String} [options.width] * CSS width of the alert. Default: 50%. * * @param {String} [options.height] * CSS height of the alert. Default: None. * * @param {Boolean} [options.hideBorder] * Optional border of the alert. Default: Shows border. * * @param {Boolean} [options.hideBackdrop] * Optional backdrop of the alert. Default: Shows backdrop. * @return {Modal} * * @example * Modal.images({title: "Image", * images: [{src: "/1.png", callback: function(src) { * alert("You selected image 1!"); * }); * * @example * Modal.images({ * title: "Image Select", * text: "Pick an image to insert:", * images: [ * {src: "/an-img.jpg", class: "big-img", callback: function(src){}}, * {src: "http://example.com/img.png", class: "med-img", callback: function(src){}}, * {src: "data:image/png;base64,iVBdAAA...", class: "small-img", callback: function(src){}} * ] * }) */ images: function(options) { check(options, { text: check.Match.Optional(String), title: check.Match.Optional(String), width: check.Match.Optional(String), height: check.Match.Optional(String), hideBorder: check.Match.Optional(Boolean), hideBackdrop: check.Match.Optional(Boolean), images: [{ src: String, callback: Function, class: check.Match.Optional(String) }] }); // amount modal will take up of the screen var modal = Modal("75%"); if (options.width){ $(modal.el).css("width", options.width); } if (options.height){ $(modal.el).css("height", options.height); } // calls given callback for each image with src on click options.images.forEach(function(image) { image.callback = makeCallback(modal, image.callback, image.src); }); // generate template instance with images inserted makeImageModal(options.title, options.text, options.images, modal); if (options.hideBorder) { $(modal.el).css("border", "none"); } if (options.hideBackdrop) { modal.show(true); } else { modal.show(); } return modal; }, /** * Shows a modal dialog with a message and a list of options. * * @param {Object} options * Object with options. * * @param {Array<String>} options.options * Array of selectable options. * * @param {String} [options.text] * Body text for the alert. Default: no text. * * @param {String} [options.title] * Title of the alert. Default: no title. * * @param {String} [options.width] * CSS width of the alert. Default: 50%. * * @param {String} [options.height] * CSS height of the alert. Default: None. * * @param {Boolean} [options.hideBorder] * Optional border of the alert. Default: Shows border. * * @param {Boolean} [options.hideBackdrop] * Optional backdrop of the alert. Default: Shows backdrop. * * @param {Function} callback * A callback to invoke with the selected option. * * @return {Modal} * * @example * Modal.select({ * title: "Where do you live?", * text: "Pick a country:", * options: ["US", "UK", "Ireland", "Canada", "Mexico", "France", "Germany"] * }, function(response) { }) */ select: function(options, callback) { check(options, check.Match.OneOf(String, { text: check.Match.Optional(String), title: check.Match.Optional(String), width: check.Match.Optional(String), height: check.Match.Optional(String), hideBorder: check.Match.Optional(Boolean), hideBackdrop: check.Match.Optional(Boolean), options: [String] })); check(callback, Function); var modal = Modal("50%"); var el = $(modal.el); if (options.width){ el.css("width", options.width); } if (options.height){ el.css("height", options.height); } var content = _.template(selectTemplate, { title: options.title, text: options.text }); el.html(content); var selectContainer = el.find(".lens-modal-options"); options.options.forEach(function(text) { $("<button />").text(text) .click(makeCallback(modal, callback, text)) .appendTo(selectContainer); }); Scrollable(selectContainer); if (options.hideBorder) { $(modal.el).css("border", "none"); } if (options.hideBackdrop) { modal.show(true); } else { modal.show(); } return modal; }, /** * Brings up the text entry modal. * * @param {function} initialText * The initial text in the text box. * * @param {function} callback * Called when the user is done entering text. If the user accepted * their text, gets the text as an argument. If the user cancelled, * gets initialText as an argument. */ textEntry: function(initialText, callback) { check(initialText, String); check(callback, Function); var modal = Modal("75%"); var el = $(modal.el); el.html(textTemplate); el.find(".lens-text-entry-cancel").click(function() { modal.hide(); modal.destroy(); callback(initialText); }); el.find(".lens-text-entry-save").click(function() { modal.hide(); modal.destroy(); callback(el.find(".lens-text-entry-input").val()); }); el.find(".lens-text-entry-input").val(initialText).blur(function() { el.find(".lens-text-entry-input").focus(); }); Keyboards.request(el.find(".lens-text-entry-keyboard")); modal.show(false, function() { el.find(".lens-text-entry-input").select(); }); return modal; } }); DOMUtils.addStylesheet("Components/modal.less"); Lens._addMember(Modal, "Modal", Lens.Components); return Modal; });