adobe/brackets

View on GitHub
src/widgets/ModalBar.js

Summary

Maintainability
A
2 hrs
Test Coverage
/*
 * 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.
 *
 */

/**
 * A "modal bar" component. This is a lightweight replacement for modal dialogs that
 * appears at the top of the editor area for operations like Find and Quick Open.
 */
define(function (require, exports, module) {
    "use strict";

    var MainViewManager  = require("view/MainViewManager"),
        EventDispatcher  = require("utils/EventDispatcher"),
        KeyEvent         = require("utils/KeyEvent"),
        AnimationUtils   = require("utils/AnimationUtils"),
        WorkspaceManager = require("view/WorkspaceManager");

    /**
     * Creates a modal bar whose contents are the given template.
     *
     * Dispatches one event:
     * - close - When the bar is closed, either via close() or via autoClose. After this event, the
     *     bar may remain visible and in the DOM while its closing animation is playing. However,
     *     by the time "close" is fired, the bar has been "popped out" of the layout and the
     *     editor scroll position has already been restored.
     *     Second argument is the reason for closing (one of ModalBar.CLOSE_*).
     *     Third argument is the Promise that close() will be returning.
     *
     * @constructor
     *
     * @param {string} template The HTML contents of the modal bar.
     * @param {boolean} autoClose If true, then close the dialog if the user hits Esc
     *      or if the bar loses focus.
     * @param {boolean} animate If true (the default), animate the dialog closed, otherwise
     *      close it immediately.
     */
    function ModalBar(template, autoClose, animate) {
        if (animate === undefined) {
            animate = true;
        }

        this._handleKeydown = this._handleKeydown.bind(this);
        this._handleFocusChange = this._handleFocusChange.bind(this);

        this._$root = $("<div class='modal-bar'/>")
            .html(template)
            .insertBefore("#editor-holder");

        if (animate) {
            this._$root.addClass("popout offscreen");
            // Forcing the renderer to do a layout, which will cause it to apply the transform for the "offscreen"
            // class, so it will animate when you remove the class.
            window.getComputedStyle(this._$root.get(0)).getPropertyValue("top");
            this._$root.removeClass("popout offscreen");
        }

        // If something *other* than an editor (like another modal bar) has focus, set the focus
        // to the editor here, before opening up the new modal bar. This ensures that the old
        // focused item has time to react and close before the new modal bar is opened.
        // See bugs #4287 and #3424
        MainViewManager.focusActivePane();

        if (autoClose) {
            this._autoClose = true;
            this._$root.on("keydown", this._handleKeydown);
            window.document.body.addEventListener("focusin", this._handleFocusChange, true);

            // Set focus to the first input field, or the first button if there is no input field.
            // TODO: remove this logic?
            var $firstInput = $("input[type='text']", this._$root).first();
            if ($firstInput.length > 0) {
                $firstInput.focus();
            } else {
                $("button", this._$root).first().focus();
            }
        }

        // Preserve scroll position of the current full editor across the editor refresh, adjusting for the
        // height of the modal bar so the code doesn't appear to shift if possible.
        MainViewManager.cacheScrollState(MainViewManager.ALL_PANES);
        WorkspaceManager.recomputeLayout();  // changes available ht for editor area
        MainViewManager.restoreAdjustedScrollState(MainViewManager.ALL_PANES, this.height());
    }
    EventDispatcher.makeEventDispatcher(ModalBar.prototype);

    /**
     * A jQuery object containing the root node of the ModalBar.
     */
    ModalBar.prototype._$root = null;

    /**
     * True if this ModalBar is set to autoclose.
     */
    ModalBar.prototype._autoClose = false;

    /**
     * Allows client code to block autoClose from closing the ModalBar: if set, this function is called whenever
     * autoClose would normally close the ModalBar. Returning true prevents the close from occurring. Programmatically
     * calling close() will still close the bar, however.
     * @type {?function():boolean}
     */
    ModalBar.prototype.isLockedOpen = null;

    ModalBar.CLOSE_ESCAPE = "escape";
    ModalBar.CLOSE_BLUR = "blur";
    ModalBar.CLOSE_API = "api";

    /**
     * @return {number} Height of the modal bar in pixels, if open.
     */
    ModalBar.prototype.height = function () {
        return this._$root.outerHeight();
    };

    /**
     * Prepares the ModalBar for closing by popping it out of the main flow and resizing/
     * rescrolling the Editor to maintain its current apparent code position. Useful if
     * you want to do that as a separate operation from actually animating the ModalBar
     * closed and removing it (for example, if you need to switch full editors in between).
     * If you don't call this explicitly, it will get called at the beginning of `close()`.
     *
     * @param {boolean=} restoreScrollPos If true (the default), adjust the scroll position
     *     of the editor to account for the ModalBar disappearing. If not set, the caller
     *     should do it immediately on return of this function (before the animation completes),
     *     because the editor will already have been resized.
     */
    ModalBar.prototype.prepareClose = function (restoreScrollPos) {
        if (restoreScrollPos === undefined) {
            restoreScrollPos = true;
        }

        this._$root.addClass("popout");

        // Since the modal bar has now an absolute position relative to the editor holder,
        // when there are html menus we need to adjust the top position
        if (!brackets.nativeMenus) {
            var top = $("#titlebar").outerHeight();
            this._$root.css("top", top + "px");
        }

        // Preserve scroll position of all visible views
        //  adjusting for the height of the modal bar so the code doesn't appear to shift if possible.
        var barHeight = this.height();
        if (restoreScrollPos) {
            MainViewManager.cacheScrollState(MainViewManager.ALL_PANES);
        }
        WorkspaceManager.recomputeLayout();  // changes available ht for editor area
        // restore scroll position of all views
        if (restoreScrollPos) {
            MainViewManager.restoreAdjustedScrollState(MainViewManager.ALL_PANES, -barHeight);
        }
    };

    /**
     * Closes the modal bar and returns focus to the active editor. Returns a promise that is
     * resolved when the bar is fully closed and the container is removed from the DOM.
     * @param {boolean=} restoreScrollPos If true (the default), adjust the scroll position
     *     of the editor to account for the ModalBar disappearing. If not set, the caller
     *     should do it immediately on return of this function (before the animation completes),
     *     because the editor will already have been resized. Note that this is ignored if
     *     `prepareClose()` was already called (you need to pass the parameter to that
     *     function if you call it first).
     * @param {boolean=} animate If true (the default), animate the closing of the ModalBar,
     *     otherwise close it immediately.
     * @param {string=} _reason For internal use only.
     * @return {$.Promise} promise resolved when close is finished
     */
    ModalBar.prototype.close = function (restoreScrollPos, animate, _reason) {
        var result = new $.Deferred(),
            self = this;

        if (restoreScrollPos === undefined) {
            restoreScrollPos = true;
        }
        if (animate === undefined) {
            animate = true;
        }

        // If someone hasn't already called `prepareClose()` to pop the ModalBar out of the flow
        // and resize the editor, then do that here.
        if (!this._$root.hasClass("popout")) {
            this.prepareClose(restoreScrollPos);
        }

        if (this._autoClose) {
            window.document.body.removeEventListener("focusin", this._handleFocusChange, true);
        }

        this.trigger("close", _reason, result);

        function doRemove() {
            self._$root.remove();
            result.resolve();
        }

        if (animate) {
            AnimationUtils.animateUsingClass(this._$root.get(0), "offscreen")
                .done(doRemove);
        } else {
            doRemove();
        }

        MainViewManager.focusActivePane();

        return result.promise();
    };

    /**
     * If autoClose is set, close the bar when Escape is pressed
     */
    ModalBar.prototype._handleKeydown = function (e) {
        if (e.keyCode === KeyEvent.DOM_VK_ESCAPE) {
            e.stopPropagation();
            e.preventDefault();
            this.close(undefined, undefined, ModalBar.CLOSE_ESCAPE);
        }
    };

    /**
     * If autoClose is set, detects when something other than the modal bar is getting focus and
     * dismisses the modal bar. DOM nodes with "attached-to" jQuery metadata referencing an element
     * within the ModalBar are allowed to take focus without closing it.
     */
    ModalBar.prototype._handleFocusChange = function (e) {
        if (this.isLockedOpen && this.isLockedOpen()) {
            return;
        }

        var effectiveElem = $(e.target).data("attached-to") || e.target;

        if (!$.contains(this._$root.get(0), effectiveElem)) {
            this.close(undefined, undefined, ModalBar.CLOSE_BLUR);
        }
    };

    /**
     * @return {jQueryObject} A jQuery object representing the root of the ModalBar.
     */
    ModalBar.prototype.getRoot = function () {
        return this._$root;
    };

    exports.ModalBar = ModalBar;
});