Source: Components/modal.js

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