dialogs/dialogs.js

/**
 * @copyright 2015 Tridium, Inc. All Rights Reserved.
 * @author Gareth Johnson
 */

/*jshint browser: true */
/*global define */

define("dialogs", [
        "jquery",
        "Promise",
        "underscore",
        "lex!js",
        "hbs!nmodule/js/rc/dialogs/dialogs",
        "hbs!nmodule/js/rc/dialogs/loader",
        "css!nmodule/js/rc/dialogs/dialogs"], function (
        $,
        Promise,
        _,
        lexicons,
        template,
        loaderTemplate) {

  "use strict";
        
  var defaultLoadImg = loaderTemplate(),
      exports,
      lex = lexicons[0],
      openDialogs = [],
      resizeHandler,
      unknownErr = 'Unknown error';
      
  /**
   * This is a convenience method used for working with functions that take
   * an Object Literal as an argument.
   * 
   * This method always ensures an Object is returned so its properties can be
   * further validated.
   *
   * @inner
   */
  function objectify(obj, propName) {
    if (!(obj === undefined || obj === null)) {
      if (obj.constructor === Object) {
        return obj;
      }
      else if (typeof propName === "string") {
        var o = {};
        o[propName] = obj;
        return o;
      }
    }
    return {};
  }
  
  /**
   * A default button handler that returns a resolved promise.
   *
   * @inner
   *
   * @returns {Promise}
   */
  function defaultButtonHandler() {
    return Promise.resolve();
  }
  
  /**
   * Define a default value for possibly undefined variables.
   *
   * @inner
   *
   * @param val The value to be tested.
   * @param defVal The default value to be returned if the value is undefined.
   * @returns The default value if the value is undefined.
   */
  function def(val, defVal) {
    return val === undefined ? defVal : val;
  }
    
  ////////////////////////////////////////////////////////////////
  // Button DOM
  ////////////////////////////////////////////////////////////////
   
  /**
   * Return true if the Button's DOM Element is in a state
   * where it can be clicked (i.e. it's not hidden or disabled).
   *
   * @inner
   *
   * @param  {jQuery} buttonJq
   * @return {Boolean} true if the button can be clicked.
   */
  function canClick(buttonJq) {
    return buttonJq &&
           !$("button", buttonJq).attr("disabled") &&
           buttonJq.css("display") !== "none";
  }

  /**
   * Set the button to be enabled/disabled.
   *
   * @inner
   *
   * @param {jQuery} buttonJq The DOM for the button.
   * @param {Boolean} enable
   */
  function setButtonEnable(buttonJq, enable) {
    var b = $("button", buttonJq);
    if (enable) {
      b.removeAttr("disabled");  
    }
    else {
      b.attr("disabled", "disabled");
    }
  }

  /**
   * Show or hide a button.
   *
   * @inner
   *
   * @param {jQuery} buttonJq The button's DOM.
   * @param {boolean} show
   */
  function setButtonShow(buttonJq, show) {
    if (show) {
      buttonJq.show();
    }
    else {
      buttonJq.hide();
    }
  }

  /**
   * Set the data on the button's DOM.
   *
   * @inner
   *
   * @param {jQuery} buttonJq The button's DOM.
   * @param {Object} data
   */
  function setButtonData(buttonJq, data) {
    $("button", buttonJq).data("js-dialog-button", data);
  }

  /**
   * Return the button object for the button name.
   *
   * @inner
   *
   * @param  {Dialog} dlg The Dialog instance.
   * @param  {String} name The name of the button.
   * @return {Object} the button data.
   */
  function findButton(dlg, name) {
    var buttons = dlg.$params.buttons,
        i;

    for (i = 0; i < buttons.length; ++i) {
      if (buttons[i].name === name) {
        return buttons[i];
      }
    }

    throw new Error("Unable to find dialog button: " + name);
  }

  /**
   * Find the button DOM under the dialog's DOM via its name.
   *
   * @inner
   *
   * @param  {jQuery} dialogJq The Dialog's DOM object.
   * @param  {String} name      The name of the button to look up.
   * @return {jQuery}           The button's DOM object.
   */
  function findButtonDom(dialogJq, name) {
    return $(".js-dialog-button-" + name, dialogJq).parent();
  }
  
  ////////////////////////////////////////////////////////////////
  // DOM Creation
  ////////////////////////////////////////////////////////////////
   

  /**
   * Initialize the DOM for the button.
   *
   * @inner
   *
   * @param {Dialog} dlg
   * @param {jQuery} buttonJq
   * @param {Object} buttonData
   */
  function initButtonDom(dlg, buttonJq, buttonData) {
    var name = buttonData.name,
        displayName = buttonData.displayName || name,
        handlerArray = buttonData.handlerArray,
        hide = buttonData.hide,
        disable = buttonData.disable;

    buttonData.jq = buttonJq;

    // Ensure the button name is always defined.
    if (typeof name !== "string") {
      throw new Error("Dialog button has no name!");
    }
        
    buttonJq.click(function () {
      var args = [dlg].concat(Array.prototype.slice.call(arguments)),
          i,
          promises = [],
          retVal;

      if (!canClick(buttonJq)) {
        return;
      }
      
      try {
        for (i = 0; i < handlerArray.length; ++i) {
          try {
            retVal = handlerArray[i].apply(this, args);
          } catch (e) {
            return dlg.close(name, /*fail*/ e || new Error());
          }

          // If a handler returns false then this means we need to use a
          // rejected promise.
          if (retVal === false) {
            retVal = Promise.reject();
          }

          // When the button is clicked invoke the handler and hold any promises returned
          promises.push(retVal);
        }
        
        // Close the dialog once all the promises have been resolved
        Promise.all(promises)
          .then(function () {
            dlg.close(name);
          })
          .catch(_.noop);
      }
      finally {
        $(window).trigger("jsdialog:buttonpressed",
                          [dlg, name, displayName, handlerArray]);
      }
    });

    if (disable) {
      setButtonEnable(buttonJq, /*enable*/false);
    }

    if (hide) {
      setButtonShow(buttonJq, /*show*/false);
    }

    setButtonData(buttonJq, buttonData);
  } 

  /**
   * Returns a deferred promise object that has properties for
   * the resolve and reject methods as well as a property for the original
   * promise reference.
   *
   * @inner
   *
   * @return {Object} An object that has resolve, reject and promise properties.
   */
  function deferred() {
    var resolve = null,
        reject = null,
        promise = new Promise(function (res, rej) {
          resolve = res;
          reject = rej;
        });

    return {
      resolve: resolve,
      reject: reject,
      promise: promise
    };
  }
   
  /**
   * Initialize the DOM for the Dialog box.
   *
   * @inner
   *
   * @param  {Dialog} dlg
   */
  function initDom(dlg) {
     var dialogJq,
         params = dlg.$params,
         buttons = params.buttons,
         i,
         bt,
         templateData = {
           title: params.title,
           content: typeof params.content === "function" ? "<span></span>" : params.content,
           loading: params.loading,
           buttons: buttons,
           fade: params.fade
         };
      
    if (params.cancel) {
      buttons.push({
        name: "cancel",
        displayName: lex.get("dialogs.cancel"),
        handler: params.cancel,
        esc: true
      });
    }
    
    if (params.no) {
      buttons.unshift({
        name: "no",
        displayName: lex.get("dialogs.no"),
        handler: params.no
      });
    }
    
    if (params.yes) {
      buttons.unshift({
        name: "yes",
        displayName: lex.get("dialogs.yes"),
        handler: params.yes
      });
    }
    
    if (params.ok) {
      buttons.unshift({
        name: "ok",
        displayName: lex.get("dialogs.ok"),
        handler: params.ok
      });
    }
         
    dialogJq = $(template(templateData));    
    findMainDialogElement(dialogJq).data("js-dialog", dlg);

    // Initialize the buttons
    for (i = 0; i < buttons.length; ++i) {
      bt = buttons[i];
      bt.handlerArray = [bt.handler || defaultButtonHandler];

      initButtonDom(dlg, 
                    findButtonDom(dialogJq, bt.name), 
                    bt);
    }

    dlg.$dialogJq = dialogJq;
  }

  function findMainDialogElement(dialogJq) {
    return dialogJq.children('.js-dialog');
  }

  function findContentWrapperElement(dialogJq) {
    return findMainDialogElement(dialogJq).children('.js-dialog-content-wrapper');
  }

  function findContentElement(dialogJq) {
    return findContentWrapperElement(dialogJq).children('.js-dialog-content');
  }

  function findButtonsElement(dialogJq) {
    return findMainDialogElement(dialogJq).children('.js-dialog-button-content');
  }

  function findHeaderElement(dialogJq) {
    return findMainDialogElement(dialogJq).children('.js-dialog-header');
  }

  ////////////////////////////////////////////////////////////////
  // Dialog
  ////////////////////////////////////////////////////////////////
   
  /**
   * A class for a Dialog box.
   * 
   * An instance of a Dialog can be accessed indirectly by use of 
   * one of the showXxx methods.
   *
   * @class
   * @inner
   * @public
   * @memberOf module:dialogs
   *
   * @example
   *   <caption>Show a basic simple OK Dialog box</caption>
   *   dialogs.showOk("Here's a nice OK dialog box!");
   */   
  var Dialog = function Dialog(params) {
    var that = this;
    
    params = objectify(params, "content");
    
    // Set up default dialog parameters
    params.title = def(params.title, "");
    params.content = def(params.content, "");
    params.buttonNames = params.buttonNames || {};
    params.buttons = params.buttons || [];
    params.parent = params.parent || $("body");
    params.private = params.private || false;

    that.$params = params;
    that.$closed = false;
    that.$hidden = false;
    that.$dialogJq = null;
    
    // Inner deferred promise. This promise will be resolved
    // when the dialog is closed.
    that.$df = deferred();
        
    // Create the DOM but don't attach it.
    initDom(that);
  };

  function cancelDelayId(dlg) {
    if (dlg.$delayId) {
      clearTimeout(dlg.$delayId);
    }
  }

  /**
   * When a dialog is shown or the window is resized, the content wrapper
   * element needs to have its max-height set to prevent the dialog buttons
   * from being scrolled off the bottom of the page.
   *
   * @inner
   * @param {Dialog} dlg
   */
  function layout(dlg) {
    if (!resizeHandler) {
      resizeHandler = _.throttle(function () {
        exports.each(function (i, dlg) { layout(dlg); });
      }, 1000 / 60);
      $(window).on('resize', resizeHandler);
    }

    var dialogJq = dlg.jq(),
        wrapper = findContentWrapperElement(dialogJq),
        mainElement = findMainDialogElement(dialogJq),
        header = findHeaderElement(dialogJq),
        buttons = findButtonsElement(dialogJq),
        // getComputedStyle excludes borders and padding
        containerHeight = window.getComputedStyle(dialogJq[0], null).getPropertyValue('height'),
        // container will be taller than dialog when enlarging window, vice versa when shrinking
        maxHeight = Math.max(mainElement.innerHeight(), parseInt(containerHeight, 10)),
        headerHeight = header.outerHeight() || 0,
        buttonsHeight = buttons.outerHeight() || 0,
        height = maxHeight - (headerHeight + buttonsHeight);

    wrapper.css('max-height', height);
    
    var layoutParam = dlg.$params.layout;
    if (typeof layoutParam === 'function') {
      layoutParam(dlg);
    }
  }
  
  /**
   * Show the Dialog.
   * 
   * @returns {module:dialogs~Dialog}
   *
   * @example
   *   <caption>Create a blank dialog box and then show it.</caption>
   *   dialogs.make("A dialog box with no buttons!")
   *          .show();
   */
  Dialog.prototype.show = function show() {
    var that = this,
        dialogJq = that.$dialogJq,
        webDlg;
    
    cancelDelayId(that);

    // Ensure the dialog's first button has focus.
    function autoFocus() {
      $(".js-dialog-button-content button:first", webDlg).focus();
    }

    //Sets the background opacity to screen the contents when the dialog is showing
    function setPrivate() {
      if (that.$params.private) {
        dialogJq.addClass("js-dialog-container-private");
      }
    }

    if (!that.$closed) {
      // Lazily attach the dialog to the DOM
      if (dialogJq.parent().length === 0) {         
        openDialogs.push(that);
        that.$params.parent.append(dialogJq);
        $(window).trigger("jsdialog:created", [that]);
      }
      
      that.$hidden = false;

      if (that.$params.fade) {
        dialogJq.stop(/*clearQueue*/true, /*jumpToEnd*/true);
        dialogJq.fadeIn("fast", autoFocus);
      } else {
        dialogJq.show();
        setPrivate();
        autoFocus();
      }
      layout(that);
    }
    
    $(window).trigger("jsdialog:show", [that]);
    
    return that;
  };
  
  /**
   * Hide the Dialog without closing it. The preferred method
   * to use is close.
   *
   * @returns {module:dialogs~Dialog}
   *
   * @example
   * <caption>
   *   Hide a dialog box after 2 seconds
   * </caption>
   * dialogs.showYesNo("Meeting alert! Do you want to be reminded in 10 seconds?")
   *        .yes(function (dialog) {
   *          dialog.hide();
   *            setTimeout(function () {
   *            dialog.show();
   *          }, 10000);
   *          return false;
   *        });
   */
  Dialog.prototype.hide = function hide() {
    var that = this,
        dialogJq = that.$dialogJq;

    cancelDelayId(that);

    if (!that.$closed) {
      that.$hidden = true;   

      if (that.$params.fade) {
        dialogJq.stop(/*clearQueue*/true, /*jumpToEnd*/true);
        dialogJq.fadeOut("fast");
      }
      else {
        dialogJq.hide();
      }

      
      $(window).trigger("jsdialog:hide", [that]);
    }
    
    return that;
  };
  
  function removeFromOpenDialogs(dlg) {
    // Remove the dialog from the array
    var i;
    for (i = 0; i < openDialogs.length; ++i) {
      if (openDialogs[i] === dlg) {
        openDialogs.splice(i, 1);
        break;
      }
    }
  }
  
  /**
   * Close the Dialog. This will remove the Dialog box from the screen.
   *
   * @param {String} [name] The name of the button used to close the dialog box.
   * This parameter is designed to be called from the Dialog JS framework itself.
   * @param {*} [fail] optional failure reason. If truthy, the dialog's promise
   * will be rejected with this failure reason; otherwise, the promise will be
   * resolved.
   * @returns {module:dialogs~Dialog}
   *
   * @example
   * <caption>
   *   Open a Dialog and close it after 2 seconds
   * </caption>
   * var dlg = dialogs.showOk("A notification");
   *
   * setTimeout(function () {
   *   dlg.close();
   * }, 2000);
   */
  Dialog.prototype.close = function close(name, fail) {
    var that = this,
        dialogJq = that.$dialogJq;

    name = name || "cancel";
        
    that.$closed = true;

    cancelDelayId(that);
        
    if (dialogJq) {
      dialogJq.stop(/*clearQueue*/true, /*jumpToEnd*/true);
      if (that.$params.fade) {
        dialogJq.fadeOut("fast", function () {
          dialogJq.remove();
        });
      } else {
        dialogJq.remove();
      }
      removeFromOpenDialogs(that);
      $(window).trigger("jsdialog:close", [that]);
    }
    
    // Once closed, resolve the promise with the specified name
    if (!fail) {
      that.$df.resolve([that, name]);
    }
    else {
      that.$df.reject([that, name, fail]);
    }
    
    return that;
  };
  
  /**
   * Move the Dialog to the front.
   *
   * @returns {module:dialogs~Dialog}
   */
  Dialog.prototype.toFront = function toFront() {
    var that = this,
        dialogJq = that.$dialogJq;

    if (dialogJq && !that.$closed && openDialogs.length > 1) {
      findMainDialogElement(dialogJq).stop(/*clearQueue*/true, /*jumpToEnd*/true);
      removeFromOpenDialogs(that);
      openDialogs.push(that);
      that.$params.parent.append(dialogJq);
    }
    
    return that;
  };
  
  /**
   * Move the Dialog to the back.
   *
   * @returns {module:dialogs~Dialog}
   */
  Dialog.prototype.toBack = function toBack() {
    var that = this,
        dialogJq = that.$dialogJq;
    if (dialogJq &&
        !that.$closed &&
        openDialogs.length > 1 &&
        openDialogs[1].$dialogJq) {
      findMainDialogElement(dialogJq).stop(/*clearQueue*/true, /*jumpToEnd*/true);
      removeFromOpenDialogs(that);
      openDialogs.unshift(that);
      dialogJq.insertBefore(openDialogs[1].$dialogJq);
    }
    
    return that;
  };
  
  /**
   * Return true if the Dialog is closed and removed from the DOM.
   *
   * @returns {Boolean} Return true if the Dialog has been closed.
   */
  Dialog.prototype.isClosed = function isClosed() {
    return this.$closed;
  };
  
  /**
   * Return true if the Dialog is hidden.
   *
   * @returns {Dialog} Return true if the Dialog has been hidden.
   */
  Dialog.prototype.isHidden = function isHidden() { 
    return this.$hidden;
  };

  /**
   * Add a callback handler for a button via its name. This callback
   * handler will be invoked when the button is clicked.
   *
   * Any handler function can return a jQuery Deferred Promise. This
   * can control when and if the Dialog box closes after the handler
   * has been invoked. It should be noted that multiple handlers
   * can be registered on a button.
   *
   * * If the handlers return nothing, the Dialog will be closed after
   * all the Handlers have been invoked.
   * * If one or more handlers return a jQuery Deferred Promise, the Dialog
   * will only close after all the jQuery Promises have been resolved.
   * * If one of the Deferred Promises is rejected, the Dialog will not close.
   *
   * @param {String} name The name of the button to register the handler on.
   * @param {Function} handler The handler of the function to be
   * invoked when the button is clicked. When invoked, the first argument of the
   * handler is the Dialog instance.
   * @returns {module:dialogs~Dialog}
   *
   * @see module:dialogs~Dialog#ok
   * @see module:dialogs~Dialog#cancel
   * @see module:dialogs~Dialog#yes
   * @see module:dialogs~Dialog#no
   *
   * @example
   * <caption>
   *   Register a function be to be called when the 'foo' button
   *   is clicked.
   * </caption>
   * dialogs.show({
   *   content: "Show some stuff",
   *   buttons: [
   *     name: "foo",
   *     handler: function () {
   *       alert("First annoying alert!");
   *     }
   *   ]
   * }).on("foo", function () {
   *   alert("This will also be called when foo button is clicked.");
   * });
   */
  Dialog.prototype.on = function on(name, handler) {
    findButton(this, name).handlerArray.push(handler);
    return this;
  };

  /**
   * Click one of the Dialog's buttons.
   * 
   * @example
   * <caption>
   *   Show a Dialog with an OK button and click it 2 seconds later
   * </caption>
   * var dlg = dialogs.showOk("This is an OK Dialog")
   * setTimeout(function () {
   *   dlg.click("ok")
   * }, 2000);   
   * 
   * @param  {String} name The name of the Dialog button to click.
   * @returns {module:dialogs~Dialog}
   */
  Dialog.prototype.click = function click(name) {
    var bt = findButton(this, name);
    if (canClick(bt.jq)) {
      bt.jq.click();
    }
    return this;
  };

  function onButton(dlg, name, handler) {
    if (handler) {
      return dlg.on(name, handler);
    }
    else {
      return dlg.click(name);
    }
  }
    
  /**
   * Add a 'ok' handler to the Dialog or if no handler is specified,
   * simulate clicking the Dialog's 'ok' button.
   *
   * @see module:dialogs~Dialog#on
   * 
   * @param {Function} [handler] if specified, the handler to be invoked
   * when the Dialog's 'ok' button is clicked. The first argument of the
   * function callback is the Dialog instance.
   * @returns {module:dialogs~Dialog}
   */
  Dialog.prototype.ok = function ok(handler) {
    return onButton(this, "ok", handler);
  };
  
  /**
   * Add a 'cancel' handler to the Dialog or if no handler is specified,
   * simulate clicking the Dialog's 'cancel' button.
   *
   * @see module:dialogs~Dialog#on
   * 
   * @param {Function} [handler] if specified, the handler to be invoked
   * when the Dialog's 'cancel' button is clicked. The first argument of the
   * function callback is the Dialog instance.
   * @returns {module:dialogs~Dialog}
   */
  Dialog.prototype.cancel = function cancel(handler) {
    return onButton(this, "cancel", handler);
  };
  
  /**
   * Add a 'yes' handler to the Dialog or if no handler is specified,
   * simulate clicking the Dialog's 'yes' button.
   *
   * @see module:dialogs~Dialog#on
   * 
   * @param {Function} [handler] if specified, the handler to be invoked
   * when the Dialog's 'yes' button is clicked. The first argument of the
   * function callback is the Dialog instance.
   * @returns {module:dialogs~Dialog}
   */
  Dialog.prototype.yes = function yes(handler) {
    return onButton(this, "yes", handler);
  };
  
  /**
   * Add a 'no' handler to the Dialog or if no handler is specified,
   * simulate clicking the Dialog's 'no' button. The first argument of the
   * function callback is the Dialog instance.
   *
   * @see module:dialogs~Dialog#on
   * 
   * @param {Function} [handler] if specified, the handler to be invoked
   * when the Dialog's 'no' button is clicked.
   * @returns {module:dialogs~Dialog}
   */
  Dialog.prototype.no = function no(handler) {
    return onButton(this, "no", handler);
  };
  
  /**
   * Return the button DOM for the given name.
   *
   * @param {String} name The name of the button.
   * @returns {jQuery} the Button's jQuery DOM object or null if nothing found.
   */
  Dialog.prototype.buttonJq = function buttonJq(name) {
    return findButton(this, name).jq;
  };
  
  /**
   * Disable a button.
   *
   * @param {String} name The name of the button.
   * @returns {module:dialogs~Dialog}
   */
  Dialog.prototype.disableButton = function disableButton(name) {
    setButtonEnable(this.buttonJq(name), /*enable*/false);
    return this;
  };
  
  /**
   * Enable a button.
   *
   * @param {String} name The name of the button.
   * @returns {module:dialogs~Dialog}
   */
  Dialog.prototype.enableButton = function enableButton(name) {
    setButtonEnable(this.buttonJq(name), /*enable*/true);
    return this;
  };
  
  /**
   * Show a button.
   *
   * @param {String} name The name of the button.
   * @returns {module:dialogs~Dialog}
   */
  Dialog.prototype.showButton = function showButton(name) {
    setButtonShow(this.buttonJq(name), /*show*/true);
    return this;
  };
  
  /**
   * Hide a button.
   *
   * @param {String} name The name of the button.
   * @returns {module:dialogs~Dialog}
   */
  Dialog.prototype.hideButton = function hideButton(name) {
    setButtonShow(this.buttonJq(name), /*show*/false);
    return this;
  };
  
  /**
   * Return the internal jQuery wrapped DOM element for the entire Dialog.
   *
   * @returns {jQuery} the Dialog's jQuery DOM object.
   */
  Dialog.prototype.jq = function jq() {
    return this.$dialogJq;
  };

  /**
   * If this dialog has content, return the jQuery DOM wrapper
   * for the Content.
   * 
   * @returns {jQuery} The DOM wrapper for the content. This wrapper will
   * be empty if the dialog is shown with no content.
   */
  Dialog.prototype.content = function () {
    return findContentElement(this.$dialogJq);
  };

  /**
   * Return a promise for the dialog that will be resolved when the dialog closes.
   * This is useful when wanting to use dialogs in a promise chain when creating a user interface.
   * 
   * @returns {Promise} The promise to be resolved.
   */
  Dialog.prototype.promise = function () {
    return this.$df.promise;
  };
  
  /**
   * An Object that defines a Button.
   *
   * @typedef {Object} module:dialogs~Button
   * @inner
   * @public
   *
   * @see module:dialogs~Dialog#on
   * 
   * @property {String} [name] A button's unique name so it can be identified.
   * @property {String} [displayName] The button's display name. This name will used
   * on the button. If not specified, the button's name will be used instead.
   * @property {Function} [handler] A function that will be invoked when the button is
   * clicked. Returning false will keep the dialog from closing after this handler
   * has been invoked.
   * @property {Boolean} [esc] If true, this handler will be invoked when the user
   * hits the escape key.
   */

  ////////////////////////////////////////////////////////////////
  // Exports
  ////////////////////////////////////////////////////////////////


  /**
   * Create stunning, modal Dialog boxes in JavaScript.
   *
   * This is a UI library used to create dynamic, modal Dialog boxes in your
   * browser.
   *
   * @example
   * <caption>
   *   A simple modal OK dialog box.
   * </caption>
   * dialogs.showOk("Some Dialog Box Content")
   *        .ok(function () {
   *          console.log("The OK button has been clicked");
   *        });
   *
   * @example
   * <caption>
   *   A Dialog box with a title and some HTML content.
   * </caption>
   * dialogs.showOk({
   *          title: "The Dialog Box's Title!",
   *          content: "&lt;p&gt;Some HTML Content&lt;/p&gt;"
   *        })
   *        .ok(function () {
   *          console.log("The OK button has been clicked");
   *        });
   *
   *  @example
   *  <caption>
   *    A simple Yes, No, Cancel dialog box.
   *  </caption>
   *  dialogs.showYesNoCancel("Would you like some tea with that?")
   *         .yes(function () {
   *           console.log("The user clicked Yes");
   *         })
   *         .no(function() {
   *           console.log("The user clicked No");
   *         })
   *         .cancel(function () {
   *           console.log("The user clicked Cancel");
   *         });
   *
   * @example
   * <caption>
   *   Show a loading dialog box and have it close after the AJAX call has finished.
   * </caption>
   * dialogs.showLoading(0, $.ajax(uri, options));
   *
   * @example
   * <caption>
   *   Use promises to show a loading dialog box and then pop up another dialog.
   * </caption>
   * var dlg = dialogs.showLoading();
   * // After 2 seconds, close the loading box.
   * setTimeout(function () {
   *   dlg.close();
   * }, 2000);
   * dlg.promise().spread(function (dlg, buttonClicked) {
   *   // Prints 'ok'
   *   console.log(buttonClicked);
   *      
   *   dialogs.showOk("The foobar has finished loading!");
   * });
   *
   * @example
   * <caption>
   *   Show a dialog. Have the content dynamically created by
   *   passing in a function for the content.
   * </caption>
   * dialogs.show(function(dlg, jq) {
   *   jq.html("&lt;div&gt;I love Niagara 4!&lt;/div&gt;");
   * });
   *
   * @example
   * <caption>
   *   Show a dialog. Have the content dynamically created
   *   by passing in a function for the content. The dialog
   *   will only show when the return promise has been resolved.
   * </caption>
   * dialogs.show(function(dlg, jq) {
   *   return Promise.resolve($.ajax("/myajax")
   *     .then(function (response) {
   *       jq.html("The answer is..." + JSON.parse(response).answer);
   *     });
   * });
   * 
   * @example
   * <caption>
   *   A Dialog BOX with background privacy setting.
   * </caption>
   * dialogs.showOk({
   *          title: "The Dialog Box's Title!",
   *          content: "&lt;p&gt;Some HTML Content&lt;/p&gt;",
   *          private: true //ensures background contents are screened when the dialog is showing
   *        })
   *        .ok(function () {
   *          console.log("The OK button has been clicked");
   *        });
   *
   *
   * @module dialogs
   * @requires jquery
   * @requires lex!js
   * @requires css!nmodule/js/rc/dialogs/dialogs
   */
  exports = {
    
    /**
     * Create a Dialog box and return it.
     * 
     * Please note, this will not show the Dialog box but just return 
     * an instance of a new one.
     *
     * This method can take either an Object Literal for parameters or a 
     * singular String argument for the Dialog's Content.
     *
     * @see module:dialogs~Button
     *
     * @param {Object|String|Function} params parameters for launching a Dialog,
     * or directly, the `content` to be shown. 
     * @param {String|Function} [params.content] the Dialog's content.
     * Please note, this can be HTML. This can also be a function used to
     * generate the content dynamically. The callback function is passed the
     * Dialog instance and content jQuery element as parameters. The callback
     * can return a promise that when resolved will show the dialog box. By
     * default, if a promise is returned, a loading dialog box will appear.
     * @param {String} [params.title] the Dialog's title. This should not contain HTML as any HTML will be escaped.
     * @param {Boolean} [params.fade] if true, the Dialog box will fade in
     * quickly.
     * @param {Function} [params.layout] an optional callback function to be
     * called when the dialog lays itself out. It will receive the
     * {@link module:dialogs~Dialog dialog} instance as the first parameter.
     * If your content needs to perform some logic to lay itself out when the
     * dialog changes dimensions, use this callback to do so.
     * @param {Function} [params.ok] handler to be invoked when the 'OK' button
     * is clicked. By defining this handler, this will also cause an 'OK' button
     * to be added to the Dialog box.
     * @param {Function} [params.cancel] handler to be invoked when the 'Cancel'
     * button is clicked. By defining this handler, this will also cause a
     * 'Cancel' button to be added to the Dialog box.
     * @param {Function} [params.yes] handler to be invoked when the 'Yes'
     * button is clicked. By defining this handler, this will also cause a 'Yes'
     * button to be added to the Dialog box.
     * @param {Function} [params.no] handler to be invoked when the 'No' button
     * is clicked. By defining this handler, this will also cause a 'No' button
     * to be added to the Dialog box.
     * @param {Array.<module:dialogs~Button>} [params.buttons] an array of additional
     * buttons.
     * @param {jQuery} [params.parent] A parent jQuery wrapped DOM element to
     * attach the dialog to. If not specified, the dialog is attached to the
     * HTML document's body element.
     * @param {Boolean} [params.private] Defaults to false. If true, the background 
     * contents are not visible when the dialog is showing.
     * @returns {module:dialogs~Dialog}
     *
     * @example
     * <caption>Make a dialog box and show it</caption>
     * dialogs.make("A dialog with no buttons")
     *        .show();
     */
    make: function make(params) {
      return new Dialog(params);
    },
  
    /**
     * Create and show a Dialog box. Shortcut for `dialogs.make(params).show()`.
     *
     * @see module:dialogs.make
     * 
     * @param {Object} params parameters for launching a Dialog. See
     * {@link module:dialogs.make}.
     * 
     * @returns {module:dialogs~Dialog}
     *
     * @example
     * <caption>
     *   Show a simple Dialog box.
     * </caption>
     * dialogs.show({
     *   title: "Dialog",
     *   content: "Hey this is a Dialog!",
     *   ok: function () {
     *     console.log("The OK button has been clicked");
     *   }
     * });
     *
     * dialogs.show(function(dlg, jq) {
     *   jq.html("&lt;div&gt;I love Niagara 4!&lt;/div&gt;");
     * });
     *
     * dialogs.show(function(dlg, jq) {
     *   return Promise.resolve($.ajax("/myajax")
     *     .then(function (response) {
     *       jq.html("The answer is..." + JSON.parse(response).answer);
     *     });
     * });
     */
    show: function show(params) {
      params = objectify(params, "content");

      var func,
          loading = false,
          dlg,
          promise;

      if (typeof params.content === "function") {
        func = params.content;

        // When a callback is used to generate the content, the loading dialog is on
        // by default if the callback returns a promise.
        loading = params.loading || params.loading === undefined;
        delete params.loading;
      }

      dlg = exports.make(params);

      if (func) {
        promise = func(dlg, dlg.content());

        // If the callback function returns a promise then 
        // make sure the dialog is shown when it resolves.
        if (promise && typeof promise.then === "function") {
          promise.then(function () {
            dlg.show();
          });

          // If a promise is returned and we can show the loading dialog
          // then show it.
          if (loading) {
            exports.showLoading(params.delay || 0, promise);
          }
        }
        else {
          dlg.show();
        }
      }
      else {
        dlg.show();
      }
      return dlg;
    },

    /**
     * Iterate through each Dialog box.
     *
     * @param {Function} func called for each Dialog box whereby 
     * the first argument is index of the Dialog box with the second
     * being the Dialog instance.
     */
    each: function each(func) {
      $.each(openDialogs.slice(0), func);
    },

    /**
     * Return the number of Open Dialogs (includes hidden).
     * 
     * @return {Number} the number of open Dialogs.
     */
    size: function size() {
      return openDialogs.length;
    },
      
    /**
     * Creates and shows a Dialog box with an OK button.
     *
     * @param {Object} params parameters for launching a Dialog. See
     * {@link module:dialogs.make}.
     *
     * @see module:dialogs.make
     * @returns {module:dialogs~Dialog}
     */
    showOk: function showOk(params) {
      params = objectify(params, "content");
      params.ok = params.ok || defaultButtonHandler;
      return exports.show(params);
    },

  
    /**
     * Creates and shows a Dialog box with OK and Cancel buttons.
     *
     * @param {Object} params parameters for launching a Dialog. See
     * {@link module:dialogs.make}.
     *
     * @see module:dialogs.make
     * @returns {module:dialogs~Dialog}
     */
    showOkCancel: function showOkCancel(params) {
      params = objectify(params, "content");
      params.ok = params.ok || defaultButtonHandler;
      params.cancel = params.cancel || defaultButtonHandler;
      return exports.show(params);
    },
  
    /**
     * Creates and shows a Dialog box with a Cancel button.
     *
     * @param {Object} params parameters for launching a Dialog. See
     * {@link module:dialogs.make}.
     *
     * @see module:dialogs.make
     * @returns {module:dialogs~Dialog}
     */
    showCancel: function showCancel(params) {
      params = objectify(params, "content");
      params.cancel = params.cancel || defaultButtonHandler;
      return exports.show(params);
    },
  
    /**
     * Creates and shows a Dialog box with Yes and No buttons.
     *
     * @param {Object} params parameters for launching a Dialog. See
     * {@link module:dialogs.make}.
     *
     * @see module:dialogs.make
     * @returns {module:dialogs~Dialog}
     */
    showYesNo: function showYesNo(params) {
      params = objectify(params, "content");
      params.yes = params.yes || defaultButtonHandler;
      params.no = params.no || defaultButtonHandler;
      return exports.show(params);
    },
  
     /**
     * Creates and shows a Dialog box with Yes, No and Cancel buttons.
     *
      * @param {Object} params parameters for launching a Dialog. See
      * {@link module:dialogs.make}.
      *
      * @see module:dialogs.make
      * @returns {module:dialogs~Dialog}
     */
    showYesNoCancel: function showYesNoCancel(params) {
      params = objectify(params, "content");
      params.yes = params.yes || defaultButtonHandler;
      params.no = params.no || defaultButtonHandler;
      params.cancel = params.cancel || defaultButtonHandler;
      return exports.show(params);
    },
    
    /**
     * Creates and shows a Loading Dialog box.
     *
     * @param {Number} [delay] An optional delay (in milliseconds) before the
     * Dialog box appears. By default, there is no delay in showing the Dialog.
     * @param {Promise} [promise] An optional Promise that can be passed
     * into the loading Dialog box. If defined, the loading Dialog box will 
     * automatically close after the promise has completed.
     * @param {jQuery} [parent] The DOM parent to attach the dialog too. If not specified, 
     * the dialog is attached to the HTML page's body.
     * @param {String} [loaderImg] an alternate loader image to use.
     * @returns {module:dialogs~Dialog}
     *
     * @example
     * <caption>
     *   Show a loading Dialog box if an AJAX call takes longer than
     *   half a second to complete.
     * </caption>
     *
     * // Make an AJAX call that may take longer that half a second.
     * var promise = $.ajax({
     *   url: "test.html",
     *   context: document.body
     * }).done(function() {
     *   $(this).addClass("done");
     * });
     *
     * // If the AJAX call takes longer that half a second, display a
     * // Loading Dialog box.
     * dialogs.showLoading(500, promise);
     */
    showLoading: function showLoading(delay, promise, parent, loaderImg) {

      loaderImg = loaderImg || defaultLoadImg;

      // Ensure the image is loaded before we display the loading dialog
      var img,
          dlg = exports.make({
            title: lex.get("dialogs.loading"),
            content: loaderImg,
            parent: parent,
            loading: true,
            fade: true
          });

      delay = delay || 0;

      img = $(".js-dialog-loading-img", dlg.$dialogJq)[0];
      
      img.onload = function () {
        if (!dlg.$closed && !dlg.$hidden) {
          // Lazily initialize the content once the image has loaded...
          dlg.$delayId = setTimeout(function () {
            dlg.show();
          }, delay);
        }
      };

      // If a promise is passed in then automatically close the 
      // dialog box when it's resolved or rejected.
      if (promise) {
        promise.then(function () {
          dlg.close();
        }, function (err) {
          dlg.close("cancel", err || unknownErr);
        });
      }
      
      img.src = loaderImg;
      return dlg;
    }
  };
  
  ////////////////////////////////////////////////////////////////
  // Handle key presses
  ////////////////////////////////////////////////////////////////
  
  // Handle any key presses
  $(function handleKeyPresses() {
    $(document).bind("keyup", function (e) {
      var esc = e.keyCode === /*esc key*/27,
          buttons,
          bt,
          i,
          x;
      
      if (esc && openDialogs.length > 0) {
        for (i = openDialogs.length - 1; i >= 0; --i) {
          // Find the first dialog that's not hidden or closed.
          if (openDialogs[i].$dialogJq &&
              !openDialogs[i].isClosed() &&
              !openDialogs[i].isHidden()) {
              
            buttons = openDialogs[i].$params.buttons;
            
            if (esc) {
              // Invoke any buttons that handle an escape press
              for (x = 0; x < buttons.length; ++x) {
                bt = buttons[x];
                if (bt.esc && canClick(bt.jq)) {
                  bt.jq.click();
                }
              }
            }
            break;
          }
        }
      }
    });
  });
    
  return exports;
});