Modules (181)

Dialogs

Description

Utilities for creating and managing standard modal dialogs.

Dependencies

Variables

Public API

DIALOG_BTN_CANCEL

Dialog Buttons IDs

    var DIALOG_BTN_CANCEL           = "cancel",
        DIALOG_BTN_OK               = "ok",
        DIALOG_BTN_DONTSAVE         = "dontsave",
        DIALOG_BTN_SAVE_AS          = "save_as",
        DIALOG_CANCELED             = "_canceled",
        DIALOG_BTN_DOWNLOAD         = "download";
Public API

DIALOG_BTN_CLASS_PRIMARY

Dialog Buttons Class Names

    var DIALOG_BTN_CLASS_PRIMARY    = "primary",
        DIALOG_BTN_CLASS_NORMAL     = "",
        DIALOG_BTN_CLASS_LEFT       = "left";

zIndex

Type
number

The z-index used for the dialogs. Each new dialog increase this number by 2

    var zIndex = 1050;

Functions

Private

_dismissDialog

$dlg
$.Element
buttonId
string
    function _dismissDialog($dlg, buttonId) {
        $dlg.data("buttonId", buttonId);
        $dlg.modal("hide");
    }
Private

_handleTab

event
$.Event
$dlg
$.Element
    function _handleTab(event, $dlg) {
        var $inputs = $(":input:enabled, a", $dlg).filter(":visible");

        function stopEvent() {
            event.stopPropagation();
            event.preventDefault();
        }

        if ($(event.target).closest($dlg).length) {
            // If it's the first or last tabbable element, focus the last/first element
            if ((!event.shiftKey && event.target === $inputs[$inputs.length - 1]) ||
                    (event.shiftKey && event.target === $inputs[0])) {
                $inputs.filter(event.shiftKey ? ":last" : ":first").focus();
                stopEvent();

            // If there is no element to focus, don't let it focus outside of the dialog
            } else if (!$inputs.length) {
                stopEvent();
            }

        // If the focus left the dialog, focus the first element in the dialog
        } else {
            $inputs.first().focus();
            stopEvent();
        }
    }
Private

_hasButton

$dlg
$.Element
buttonId
string
Returns:
boolean
    function _hasButton($dlg, buttonId) {
        return ($dlg.find("[data-button-id='" + buttonId + "']").length > 0);
    }
Private

_keydownHook

Handles the keyDown event for the dialogs

e
$.Event
autoDismiss
boolean
Returns:
boolean
    var _keydownHook = function (e, autoDismiss) {
        var $primaryBtn     = this.find(".primary"),
            buttonId        = null,
            which           = String.fromCharCode(e.which),
            $focusedElement = this.find(".dialog-button:focus, a:focus");

        function stopEvent() {
            e.preventDefault();
            e.stopPropagation();
        }

        // There might be a textfield in the dialog's UI; don't want to mistake normal typing for dialog dismissal
        var inTextArea    = (e.target.tagName === "TEXTAREA"),
            inTypingField = inTextArea || ($(e.target).filter(":text, :password").length > 0);

        if (e.which === KeyEvent.DOM_VK_TAB) {
            // We don't want to stopEvent() in this case since we might want the default behavior.
            // _handleTab takes care of stopping/preventing default as necessary.
            _handleTab(e, this);
        } else if (e.which === KeyEvent.DOM_VK_ESCAPE) {
            buttonId = DIALOG_BTN_CANCEL;
        } else if (e.which === KeyEvent.DOM_VK_RETURN && (!inTextArea || e.ctrlKey)) {
            // Enter key in single-line text input always dismisses; in text area, only Ctrl+Enter dismisses
            // Click primary
            stopEvent();
            if (e.target.tagName === "BUTTON") {
                this.find(e.target).click();
            } else if (e.target.tagName !== "INPUT") {
                // If the target element is not BUTTON or INPUT, click the primary button
                // We're making an exception for INPUT element because of this issue: GH-11416
                $primaryBtn.click();
            }
        } else if (e.which === KeyEvent.DOM_VK_SPACE) {
            if ($focusedElement.length) {
                // Space bar on focused button or link
                stopEvent();
                $focusedElement.click();
            }
        } else if (brackets.platform === "mac") {
            // CMD+Backspace Don't Save
            if (e.metaKey && (e.which === KeyEvent.DOM_VK_BACK_SPACE)) {
                if (_hasButton(this, DIALOG_BTN_DONTSAVE)) {
                    buttonId = DIALOG_BTN_DONTSAVE;
                }
            // FIXME (issue #418) CMD+. Cancel swallowed by native shell
            } else if (e.metaKey && (e.which === KeyEvent.DOM_VK_PERIOD)) {
                buttonId = DIALOG_BTN_CANCEL;
            }
        } else { // if (brackets.platform === "win") {
            // 'N' Don't Save
            if (which === "N" && !inTypingField) {
                if (_hasButton(this, DIALOG_BTN_DONTSAVE)) {
                    buttonId = DIALOG_BTN_DONTSAVE;
                }
            }
        }

        if (buttonId) {
            stopEvent();
            _processButton(this, buttonId, autoDismiss);
        }

        // Stop any other global hooks from processing the event (but
        // allow it to continue bubbling if we haven't otherwise stopped it).
        return true;
    };
Private

_processButton

$dlg
$.Element
The dialog element to be dismissed.
buttonId
string
The ID of the button that was clicked.
autoDismiss
boolean
Whether to autodismiss the dialog on a button click.
    function _processButton($dlg, buttonId, autoDismiss) {
        if (autoDismiss) {
            _dismissDialog($dlg, buttonId);
        } else {
            $dlg.triggerHandler("buttonClick", buttonId);
        }
    }
Public API

addLinkTooltips

Ensures that all <a> tags with a URL have a tooltip showing the same URL

elementOrDialog
non-nullable
jQueryObject, Dialog
Dialog intance, or root of other DOM tree to add tooltips to
    function addLinkTooltips(elementOrDialog) {
        var $element;
        if (elementOrDialog.getElement) {
            $element = elementOrDialog.getElement().find(".dialog-message");
        } else {
            $element = elementOrDialog;
        }
        $element.find("a").each(function (index, elem) {
            var $elem = $(elem);
            var url = $elem.attr("href");
            if (url && url !== "#" && !$elem.attr("title")) {
                $elem.attr("title", url);
            }
        });
    }

    window.addEventListener("resize", setDialogMaxSize);

    exports.DIALOG_BTN_CANCEL            = DIALOG_BTN_CANCEL;
    exports.DIALOG_BTN_OK                = DIALOG_BTN_OK;
    exports.DIALOG_BTN_DONTSAVE          = DIALOG_BTN_DONTSAVE;
    exports.DIALOG_BTN_SAVE_AS           = DIALOG_BTN_SAVE_AS;
    exports.DIALOG_CANCELED              = DIALOG_CANCELED;
    exports.DIALOG_BTN_DOWNLOAD          = DIALOG_BTN_DOWNLOAD;

    exports.DIALOG_BTN_CLASS_PRIMARY     = DIALOG_BTN_CLASS_PRIMARY;
    exports.DIALOG_BTN_CLASS_NORMAL      = DIALOG_BTN_CLASS_NORMAL;
    exports.DIALOG_BTN_CLASS_LEFT        = DIALOG_BTN_CLASS_LEFT;

    exports.showModalDialog              = showModalDialog;
    exports.showModalDialogUsingTemplate = showModalDialogUsingTemplate;
    exports.cancelModalDialogIfOpen      = cancelModalDialogIfOpen;
    exports.addLinkTooltips              = addLinkTooltips;
});
Public API

cancelModalDialogIfOpen

Immediately closes any dialog instances with the given class. The dialog callback for each instance will be called with the special buttonId DIALOG_CANCELED (note: callback is run asynchronously).

dlgClass
string
The class name identifier for the dialog.
buttonId
optional
string
The button id to use when closing the dialog. Defaults to DIALOG_CANCELED
    function cancelModalDialogIfOpen(dlgClass, buttonId) {
        $("." + dlgClass + ".instance").each(function () {
            if ($(this).is(":visible")) {   // Bootstrap breaks if try to hide dialog that's already hidden
                _dismissDialog($(this), buttonId || DIALOG_CANCELED);
            }
        });
    }

setDialogMaxSize

Don't allow dialog to exceed viewport size

    function setDialogMaxSize() {
        var maxWidth, maxHeight,
            $dlgs = $(".modal-inner-wrapper > .instance");

        // Verify 1 or more modal dialogs are showing
        if ($dlgs.length > 0) {
            maxWidth  = $("body").width();
            maxHeight = $("body").height();

            $dlgs.css({
                "max-width":  maxWidth,
                "max-height": maxHeight,
                "overflow":   "auto"
            });
        }
    }
Public API

showModalDialog

Creates a new general purpose modal dialog using the default template and the template variables given as parameters as described.

dlgClass
string
A class name identifier for the dialog. Typically one of DefaultDialogs.*
title
optional
string
The title of the dialog. Can contain HTML markup. Defaults to "".
message
optional
string
The message to display in the dialog. Can contain HTML markup. Defaults to "".
buttons
optional
Array.<{className: string, id: string, text: string}>
An array of buttons where each button has a class, id and text property. The id is used in "data-button-id". Defaults to a single Ok button. Typically className is one of DIALOG_BTN_CLASS_*, id is one of DIALOG_BTN_*
autoDismiss
optional
boolean
Whether to automatically dismiss the dialog when one of the buttons is clicked. Default true. If false, you'll need to manually handle button clicks and the Esc key, and dismiss the dialog yourself when ready by calling `close()` on the returned dialog.
Returns:
Dialog
    function showModalDialog(dlgClass, title, message, buttons, autoDismiss) {
        var templateVars = {
            dlgClass: dlgClass,
            title:    title   || "",
            message:  message || "",
            buttons:  buttons || [{ className: DIALOG_BTN_CLASS_PRIMARY, id: DIALOG_BTN_OK, text: Strings.OK }]
        };
        var template = Mustache.render(DialogTemplate, templateVars);

        return showModalDialogUsingTemplate(template, autoDismiss);
    }
Public API

showModalDialogUsingTemplate

Creates a new modal dialog from a given template. The template can either be a string or a jQuery object representing a DOM node that is not in the current DOM.

template
string
A string template or jQuery object to use as the dialog HTML.
autoDismiss
optional
boolean
Whether to automatically dismiss the dialog when one of the buttons is clicked. Default true. If false, you'll need to manually handle button clicks and the Esc key, and dismiss the dialog yourself when ready by calling `close()` on the returned dialog.
Returns:
Dialog
    function showModalDialogUsingTemplate(template, autoDismiss) {
        if (autoDismiss === undefined) {
            autoDismiss = true;
        }

        $("body").append("<div class='modal-wrapper'><div class='modal-inner-wrapper'></div></div>");

        var result  = new $.Deferred(),
            promise = result.promise(),
            $dlg    = $(template)
                .addClass("instance")
                .appendTo(".modal-inner-wrapper:last");

        // Don't allow dialog to exceed viewport size
        setDialogMaxSize();

        // Save the dialog promise for unit tests
        $dlg.data("promise", promise);

        var keydownHook = function (e) {
            return _keydownHook.call($dlg, e, autoDismiss);
        };

        // Store current focus
        var lastFocus = window.document.activeElement;

        // Pipe dialog-closing notification back to client code
        $dlg.one("hidden", function () {
            var buttonId = $dlg.data("buttonId");
            if (!buttonId) {    // buttonId will be undefined if closed via Bootstrap's "x" button
                buttonId = DIALOG_BTN_CANCEL;
            }

            // Let call stack return before notifying that dialog has closed; this avoids issue #191
            // if the handler we're triggering might show another dialog (as long as there's no
            // fade-out animation)
            window.setTimeout(function () {
                result.resolve(buttonId);
            }, 0);

            // Remove the dialog instance from the DOM.
            $dlg.remove();

            // Remove our global keydown handler.
            KeyBindingManager.removeGlobalKeydownHook(keydownHook);

            // Restore previous focus
            if (lastFocus) {
                lastFocus.focus();    
            }

            //Remove wrapper
            $(".modal-wrapper:last").remove();
        }).one("shown", function () {
            var $primaryBtn = $dlg.find(".primary:enabled"),
                $otherBtn   = $dlg.find(".modal-footer .dialog-button:enabled:eq(0)");

            // Set focus to the primary button, to any other button, or to the dialog depending
            // if there are buttons
            if ($primaryBtn.length) {
                $primaryBtn.focus();
            } else if ($otherBtn.length) {
                $otherBtn.focus();
            } else {
                window.document.activeElement.blur();
            }

            // Push our global keydown handler onto the global stack of handlers.
            KeyBindingManager.addGlobalKeydownHook(keydownHook);
        });

        // Click handler for buttons
        $dlg.one("click", ".dialog-button", function (e) {
            _processButton($dlg, $(this).attr("data-button-id"), autoDismiss);
        });

        // Run the dialog
        $dlg
            .modal({
                backdrop: "static",
                show:     true,
                selector: ".modal-inner-wrapper:last",
                keyboard: false // handle the ESC key ourselves so we can deal with nested dialogs
            })
            // Updates the z-index of the modal dialog and the backdrop
            .css("z-index", zIndex + 1)
            .next()
            .css("z-index", zIndex);

        zIndex += 2;

        return (new Dialog($dlg, promise));
    }

Classes

Constructor

Dialog

$dlg
$.Element
The dialog jQuery element
promise
$.Promise
A promise that will be resolved with the ID of the clicked button when the dialog is dismissed. Never rejected.
    function Dialog($dlg, promise) {
        this._$dlg    = $dlg;
        this._promise = promise;
    }

Methods

close

Closes the dialog if is visible

    Dialog.prototype.close = function () {
        if (this._$dlg.is(":visible")) {   // Bootstrap breaks if try to hide dialog that's already hidden
            _dismissDialog(this._$dlg, DIALOG_CANCELED);
        }
    };

done

Adds a done callback to the dialog promise

    Dialog.prototype.done = function (callback) {
        this._promise.done(callback);
    };

getElement

The dialog jQuery element

    Dialog.prototype.getElement = function () {
        return this._$dlg;
    };

getPromise

The dialog promise

    Dialog.prototype.getPromise = function () {
        return this._promise;
    };