adobe/brackets

View on GitHub
src/editor/InlineTextEditor.js

Summary

Maintainability
A
1 hr
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.
 *
 */


// FUTURE: Merge part (or all) of this class with MultiRangeInlineEditor

define(function (require, exports, module) {
    "use strict";

    // Load dependent modules
    var CodeMirror          = require("thirdparty/CodeMirror/lib/codemirror"),
        EventDispatcher     = require("utils/EventDispatcher"),
        DocumentManager     = require("document/DocumentManager"),
        EditorManager       = require("editor/EditorManager"),
        CommandManager      = require("command/CommandManager"),
        Commands            = require("command/Commands"),
        InlineWidget        = require("editor/InlineWidget").InlineWidget,
        KeyEvent            = require("utils/KeyEvent");

    /**
     * Shows or hides the dirty indicator
     * @private
     */
    function _showDirtyIndicator($indicatorDiv, isDirty) {
        // Show or hide the dirty indicator by adjusting
        // the width of the div.
        $indicatorDiv.css("width", isDirty ? 16 : 0);
    }

    /**
     * Respond to dirty flag change event. If the dirty flag is associated with an inline editor,
     * show (or hide) the dirty indicator.
     * @private
     */
    function _dirtyFlagChangeHandler(event, doc) {
        var $dirtyIndicators = $(".inline-text-editor .dirty-indicator"),
            $indicator;

        $dirtyIndicators.each(function (index, indicator) {
            $indicator = $(this);
            if ($indicator.data("fullPath") === doc.file.fullPath) {
                _showDirtyIndicator($indicator, doc.isDirty);
            }
        });
    }

    /**
     * @constructor
     * @extends {InlineWidget}
     */
    function InlineTextEditor() {
        InlineWidget.call(this);

        this.editor = null;

        // We need to set this as a capture handler so CodeMirror doesn't handle Esc before we see it.
        this.handleKeyDown = this.handleKeyDown.bind(this);
        this.htmlContent.addEventListener("keydown", this.handleKeyDown, true);
    }
    InlineTextEditor.prototype = Object.create(InlineWidget.prototype);
    InlineTextEditor.prototype.constructor = InlineTextEditor;
    InlineTextEditor.prototype.parentClass = InlineWidget.prototype;

    InlineTextEditor.prototype.$wrapper = null;
    /** @type {Editor} */
    InlineTextEditor.prototype.editor = null;
    InlineTextEditor.prototype.$editorHolder = null;
    InlineTextEditor.prototype.$header = null;
    InlineTextEditor.prototype.$filename = null;

   /**
     * Given a host editor and its inline editors, find the widest gutter and make all the others match
     * @param {!Editor} hostEditor Host editor containing all the inline editors to sync
     * @private
     */
    function _syncGutterWidths(hostEditor) {
        var allHostedEditors = EditorManager.getInlineEditors(hostEditor);

        // add the host itself to the list too
        allHostedEditors.push(hostEditor);

        var maxWidth = 0;
        allHostedEditors.forEach(function (editor) {
            var $gutter = $(editor._codeMirror.getGutterElement()).find(".CodeMirror-linenumbers");
            $gutter.css("min-width", "");
            var curWidth = $gutter.width();
            if (curWidth > maxWidth) {
                maxWidth = curWidth;
            }
        });

        if (allHostedEditors.length === 1) {
            //There's only the host, just refresh the gutter
            allHostedEditors[0]._codeMirror.setOption("gutters", allHostedEditors[0]._codeMirror.getOption("gutters"));
            return;
        }

        maxWidth = maxWidth + "px";
        allHostedEditors.forEach(function (editor) {
            $(editor._codeMirror.getGutterElement()).find(".CodeMirror-linenumbers").css("min-width", maxWidth);

            // Force CodeMirror to refresh the gutter
            editor._codeMirror.setOption("gutters", editor._codeMirror.getOption("gutters"));
        });
    }

    /**
     * Called any time inline was closed, whether manually (via close()) or automatically
     */
    InlineTextEditor.prototype.onClosed = function () {
        InlineTextEditor.prototype.parentClass.onClosed.apply(this, arguments);

        _syncGutterWidths(this.hostEditor);

        // Destroy the inline editor.
        this.setInlineContent(null);
        this.htmlContent.removeEventListener("keydown", this.handleKeyDown, true);
    };

    /**
     * Update the inline editor's height when the number of lines change. The
     * base implementation of this method does nothing.
     */
    InlineTextEditor.prototype.sizeInlineWidgetToContents = function () {
        // brackets_codemirror_overrides.css adds height:auto to CodeMirror
        // Inline editors themselves do not need to be sized, but layouts like
        // the one used in CSSInlineEditor do need some manual layout.
    };

    /**
     * Some tasks have to wait until we've been parented into the outer editor
     * @param {string} the inline ID that is generated by CodeMirror after the widget that holds the inline
     *  editor is constructed and added to the DOM
     */
    InlineTextEditor.prototype.onAdded = function () {
        var self = this;

        InlineTextEditor.prototype.parentClass.onAdded.apply(this, arguments);

        if (this.editor) {
            this.editor.refresh();
        }

        // Update display of inline editors when the hostEditor signals a redraw
        CodeMirror.on(this.info, "redraw", function () {
            // At the point where we get the redraw, CodeMirror might not yet have actually
            // re-added the widget to the DOM. This is filed as https://github.com/codemirror/CodeMirror/issues/1226.
            // For now, we can work around it by doing the refresh on a setTimeout().
            window.setTimeout(function () {
                if (self.editor) {
                    self.editor.refresh();
                }
            }, 0);
        });

        _syncGutterWidths(this.hostEditor);

        if (this.editor) {
            this.editor.focus();
        }
    };

    /**
     * @return {?Editor} If an Editor within this inline editor has focus, returns it. Otherwise returns null.
     */
    InlineTextEditor.prototype.getFocusedEditor = function () {
        if (this.editor && this.editor.hasFocus()) {
            return this.editor;
        }
        return null;
    };

    /**
     * @private
     * Make sure that if we want to handle Esc to cancel a multiple selection, we don't let it bubble
     * up to InlineWidget, which will close the edit.
     */
    InlineTextEditor.prototype.handleKeyDown = function (e) {
        if (e.keyCode === KeyEvent.DOM_VK_ESCAPE && this.editor && this.editor.getSelections().length > 1) {
            CodeMirror.commands.singleSelection(this.editor._codeMirror);
            e.stopImmediatePropagation();
        }
    };

    /**
     * Sets the document and range to show in the inline editor, or null to destroy the current editor and leave
     * the content blank.
     * @param {Document} doc The document to show, or null to show nothing
     * @param {number} startLine The first line of text in `doc` to show in inline editor. Ignored if doc is null.
     * @param {number} endLine The last line of text in `doc` to show in inline editor. Ignored if doc is null.
     */
    InlineTextEditor.prototype.setInlineContent = function (doc, startLine, endLine) {
        var self = this;

        // Destroy the previous editor if we had one and clear out the filename info.
        if (this.editor) {
            this.editor.off(".InlineTextEditor");
            this.editor.destroy(); // remove from DOM and release ref on Document
            this.editor = null;
            this.$filename.off(".InlineTextEditor")
                .removeAttr("title");
            this.$filename.html("");
        }

        if (!doc) {
            return;
        }

        var range = {
            startLine: startLine,
            endLine: endLine
        };

        // dirty indicator, with file path stored on it
        var $dirtyIndicatorDiv = $("<div/>")
            .addClass("dirty-indicator")
            .html("&bull;")
            .width(0); // initialize indicator as hidden
        $dirtyIndicatorDiv.data("fullPath", doc.file.fullPath);

        this.$lineNumber = $("<span class='line-number'/>");

        // update contents of filename link
        this.$filename.append($dirtyIndicatorDiv)
            .append(doc.file.name + " : ")
            .append(this.$lineNumber)
            .attr("title", doc.file.fullPath);

        // clicking filename jumps to full editor view
        this.$filename.on("click.InlineTextEditor", function () {
            CommandManager.execute(Commands.FILE_OPEN, { fullPath: doc.file.fullPath })
                .done(function () {
                    EditorManager.getCurrentFullEditor().setCursorPos(startLine, 0, true);
                });
        });

        var inlineInfo = EditorManager.createInlineEditorForDocument(doc, range, this.$editorHolder.get(0));
        this.editor = inlineInfo.editor;

        // Init line number display
        this._updateLineRange(inlineInfo.editor);

        // Always update the widget height when an inline editor completes a
        // display update
        this.editor.on("update.InlineTextEditor", function (event, editor) {
            self.sizeInlineWidgetToContents();
        });

        // Size editor to content whenever text changes (via edits here or any
        // other view of the doc: Editor fires "change" any time its text
        // changes, regardless of origin)
        this.editor.on("change.InlineTextEditor", function (event, editor) {
            if (self.hostEditor.isFullyVisible()) {
                self.sizeInlineWidgetToContents();
                self._updateLineRange(editor);
            }
        });

        // If Document's file is deleted, or Editor loses sync with Document, delegate to this._onLostContent()
        this.editor.on("lostContent.InlineTextEditor", function () {
            self._onLostContent.apply(self, arguments);
        });

        // set dirty indicator state
        _showDirtyIndicator($dirtyIndicatorDiv, doc.isDirty);
    };

    /**
     * Updates start line display.
     * @param {Editor} editor
     */
    InlineTextEditor.prototype._updateLineRange = function (editor) {
        this._startLine = editor.getFirstVisibleLine();
        this._endLine = editor.getLastVisibleLine();
        this._lineCount = this._endLine - this._startLine;

        this.$lineNumber.text(this._startLine + 1);
    };

    /**
     * @param {Editor} hostEditor
     */
    InlineTextEditor.prototype.load = function (hostEditor) {
        InlineTextEditor.prototype.parentClass.load.apply(this, arguments);

        // We don't create the actual editor here--that will happen the first time
        // setInlineContent() is called.
        this.$wrapper = $("<div/>").addClass("inline-text-editor").appendTo(this.$htmlContent);
        this.$header = $("<div/>").addClass("inline-editor-header").appendTo(this.$wrapper);
        this.$filename = $("<a/>").addClass("filename").appendTo(this.$header);
        this.$editorHolder = $("<div/>").addClass("inline-editor-holder").appendTo(this.$wrapper);
    };

    /**
     * Called when the editor containing the inline is made visible.
     */
    InlineTextEditor.prototype.onParentShown = function () {
        InlineTextEditor.prototype.parentClass.onParentShown.apply(this, arguments);

        // Refresh line number display and codemirror line number gutter
        if (this.editor) {
            this._updateLineRange(this.editor);
            this.editor.refresh();
        }

        // We need to call this explicitly whenever the host editor is reshown
        this.sizeInlineWidgetToContents();
    };

    /**
     * If Document's file is deleted, or Editor loses sync with Document, just close
     */
    InlineTextEditor.prototype._onLostContent = function () {
        // Note: this closes the entire inline widget if any one Editor loses sync. This seems
        // better than leaving it open but suddenly removing one rule from the result list.
        this.close();
    };


    // Consolidate all dirty document updates
    // Due to circular dependencies, not safe to call on() directly
    EventDispatcher.on_duringInit(DocumentManager, "dirtyFlagChange", _dirtyFlagChangeHandler);

    exports.InlineTextEditor = InlineTextEditor;

});