/
githubmirror
/
brackets
Обзор
Документация
Войти
/
githubmirror
/
brackets
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
release-1.12
src/widgets/Dialogs.js
460 строк
17 KB
Sahil Dua
Fix #11416: Enter key behaviour on Extension Manager Search Box (#12839)
18 окт 2016, 19:55
18 окт 2016, 19:55
ab1396b
Код
Авторство
О чём код?
/* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /** * Utilities for creating and managing standard modal dialogs. */ define(function (require, exports, module) { "use strict"; require("utils/Global"); var KeyBindingManager = require("command/KeyBindingManager"), KeyEvent = require("utils/KeyEvent"), Strings = require("strings"), DialogTemplate = require("text!htmlContent/dialog-template.html"), Mustache = require("thirdparty/mustache/mustache"); /** * Dialog Buttons IDs * @const {string} */ 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"; /** * Dialog Buttons Class Names * @const {string} */ var DIALOG_BTN_CLASS_PRIMARY = "primary", DIALOG_BTN_CLASS_NORMAL = "", DIALOG_BTN_CLASS_LEFT = "left"; /** * The z-index used for the dialogs. Each new dialog increase this number by 2 * @type {number} */ var zIndex = 1050; /** * @private * Dismises a modal dialog * @param {$.Element} $dlg * @param {string} buttonId */ function _dismissDialog($dlg, buttonId) { $dlg.data("buttonId", buttonId); $dlg.modal("hide"); } /** * @private * If autoDismiss is true, then dismisses the dialog. Otherwise just raises an event that the * given button was clicked. * @param {$.Element} $dlg The dialog element to be dismissed. * @param {string} buttonId The ID of the button that was clicked. * @param {boolean} autoDismiss Whether to autodismiss the dialog on a button click. */ function _processButton($dlg, buttonId, autoDismiss) { if (autoDismiss) { _dismissDialog($dlg, buttonId); } else { $dlg.triggerHandler("buttonClick", buttonId); } } /** * @private * Returns true if the modal dialog has a button with the given ID * @param {$.Element} $dlg * @param {string} buttonId * @return {boolean} */ function _hasButton($dlg, buttonId) { return ($dlg.find("[data-button-id='" + buttonId + "']").length > 0); } /** * @private * Handles the use of Tab so that it stays inside the Dialog * @param {$.Event} event * @param {$.Element} $dlg */ 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(); } } /** * Handles the keyDown event for the dialogs * @param {$.Event} e * @param {boolean} autoDismiss * @return {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; }; /** * @constructor * @private * * @param {$.Element} $dlg The dialog jQuery element * @param {$.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; } /** * The dialog jQuery element * @type {$.Element} */ Dialog.prototype.getElement = function () { return this._$dlg; }; /** * The dialog promise * @type {$.Promise} */ Dialog.prototype.getPromise = function () { return this._promise; }; /** * 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); } }; /** * Adds a done callback to the dialog promise */ Dialog.prototype.done = function (callback) { this._promise.done(callback); }; /** * 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" }); } } /** * 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. * * @param {string} template A string template or jQuery object to use as the dialog HTML. * @param {boolean=} autoDismiss 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. * @return {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)); } /** * Creates a new general purpose modal dialog using the default template and the template variables given * as parameters as described. * * @param {string} dlgClass A class name identifier for the dialog. Typically one of DefaultDialogs.* * @param {string=} title The title of the dialog. Can contain HTML markup. Defaults to "". * @param {string=} message The message to display in the dialog. Can contain HTML markup. Defaults to "". * @param {Array.<{className: string, id: string, text: string}>=} buttons 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_* * @param {boolean=} autoDismiss 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. * @return {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); } /** * 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). * @param {string} dlgClass The class name identifier for the dialog. * @param {string=} buttonId 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); } }); } /** * Ensures that all <a> tags with a URL have a tooltip showing the same URL * @param {!jQueryObject|Dialog} elementOrDialog 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; });