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