src/editor/MultiRangeInlineEditor.js
/*
* 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 InlineTextEditor
/**
* An inline editor for displaying and editing multiple text ranges. Each range corresponds to a
* contiguous set of lines in a file.
*
* In the current implementation, only one range is visible at a time. A list on the right side
* of the editor allows the user to select which range is visible.
*
* This module does not dispatch any events.
*/
define(function (require, exports, module) {
"use strict";
var _ = require("thirdparty/lodash");
// Load dependent modules
var TextRange = require("document/TextRange").TextRange,
InlineTextEditor = require("editor/InlineTextEditor").InlineTextEditor,
EditorManager = require("editor/EditorManager"),
FileUtils = require("file/FileUtils"),
PreferencesManager = require("preferences/PreferencesManager"),
ProjectManager = require("project/ProjectManager"),
Commands = require("command/Commands"),
Strings = require("strings"),
CommandManager = require("command/CommandManager");
var _prevMatchCmd, _nextMatchCmd;
/**
* Remove trailing "px" from a style size value.
* @param {!JQuery} $target Element in DOM
* @param {!string} styleName Style name to query
* @return {number} Style value converted from string to number, removing "px" units
*/
function _parseStyleSize($target, styleName) {
return parseInt($target.css(styleName), 10);
}
/** Returns a 'context' object for getting/setting project-specific preferences */
function _getPrefsContext() {
var projectRoot = ProjectManager.getProjectRoot(); // note: null during unit tests!
return { location : { scope: "user", layer: "project", layerID: projectRoot && projectRoot.fullPath } };
}
/**
* Stores one search result: its source file, line range, etc. plus the DOM node representing it
* in the results list.
* @constructor
*/
function SearchResultItem(rangeResult) {
this.name = rangeResult.name;
this.textRange = new TextRange(rangeResult.document, rangeResult.lineStart, rangeResult.lineEnd);
// this.$listItem is assigned in load()
}
SearchResultItem.prototype.name = null;
SearchResultItem.prototype.textRange = null;
SearchResultItem.prototype.$listItem = null;
function _updateRangeLabel(listItem, range, labelCB) {
if (labelCB) {
range.name = labelCB(range.textRange);
}
var text = _.escape(range.name) + " <span class='related-file'>:" + (range.textRange.startLine + 1) + "</span>";
listItem.html(text);
listItem.attr("title", listItem.text());
}
/**
* @constructor
* @param {Array.<{name:String,document:Document,lineStart:number,lineEnd:number}>} ranges The text
* ranges to display. Results within the same file are expected to be contiguous in this array.
* @param {?function(): $.Promise} messageCB Optional; returns a promise resolved with a message to
* show when no matches are available. The message should be already-escaped HTML.
* @param {?function(range): string} labelCB Optional; returns an updated label string for the given
* range. Called when we detect that the content of a range has changed. The label is plain
* text, not HTML.
* @param {?function(!File, !File):number} fileComparator Optional comparison function for sorting
* the results list (based on range.document.file). Defaults to FileUtils.comparePaths().
* @extends {InlineTextEditor}
*/
function MultiRangeInlineEditor(ranges, messageCB, labelCB, fileComparator) {
InlineTextEditor.call(this);
// Store the results to show in the range list. This creates TextRanges bound to the Document,
// which will stay up to date automatically (but we must be sure to detach them later)
this._ranges = ranges.map(function (rangeResult) {
return new SearchResultItem(rangeResult);
});
this._messageCB = messageCB;
this._labelCB = labelCB;
this._selectedRangeIndex = -1;
this._collapsedFiles = {};
// Set up list sort order
this._fileComparator = fileComparator || function defaultComparator(file1, file2) {
return FileUtils.comparePaths(file1.fullPath, file2.fullPath);
};
this._ranges.sort(function (result1, result2) {
return this._fileComparator(result1.textRange.document.file, result2.textRange.document.file);
}.bind(this));
}
MultiRangeInlineEditor.prototype = Object.create(InlineTextEditor.prototype);
MultiRangeInlineEditor.prototype.constructor = MultiRangeInlineEditor;
MultiRangeInlineEditor.prototype.parentClass = InlineTextEditor.prototype;
MultiRangeInlineEditor.prototype.$messageDiv = null;
MultiRangeInlineEditor.prototype.$relatedContainer = null;
MultiRangeInlineEditor.prototype.$related = null;
MultiRangeInlineEditor.prototype.$selectedMarker = null;
/** Includes all the _ranges[i].$listItem items, as well as section headers */
MultiRangeInlineEditor.prototype.$rangeList = null;
/**
* List of search results. Section headers are not represented in this list (they are implied before each group of
* of consecutive results from the same Document).
* @type {!Array.<SearchResultItem>}
*/
MultiRangeInlineEditor.prototype._ranges = null;
/** Index into this._ranges - indices do not include section headers */
MultiRangeInlineEditor.prototype._selectedRangeIndex = null;
/**
* Map from fullPath to true if collapsed. May not agree with preferences, in cases where multiple inline editors make
* concurrent changes.
* @type {!Object.<string, boolean>}
*/
MultiRangeInlineEditor.prototype._collapsedFiles = null;
MultiRangeInlineEditor.prototype._messageCB = null;
MultiRangeInlineEditor.prototype._labelCB = null;
MultiRangeInlineEditor.prototype._fileComparator = null;
/** @type {!Object.<string, jQueryObject>} Map from fullPath to section header DOM node */
MultiRangeInlineEditor.prototype._$headers = null;
/**
* @private
* Add a new result item <li> to the range list UI ($rangeList) and saves it in range.$listItem
* @param {SearchResultItem} range The range to add.
*/
MultiRangeInlineEditor.prototype._createListItem = function (range) {
var self = this,
$rangeItem = $("<li/>");
// Attach filename for unit test use
$rangeItem.data("filename", range.textRange.document.file.name);
$rangeItem.appendTo(this.$rangeList);
_updateRangeLabel($rangeItem, range);
$rangeItem.mousedown(function () {
self.setSelectedIndex(self._ranges.indexOf(range));
});
range.$listItem = $rangeItem;
};
/** Collapses/expands a file section in the range list UI */
MultiRangeInlineEditor.prototype._toggleSection = function (fullPath, duringInit) {
var $headerItem = this._$headers[fullPath];
var $disclosureIcon = $headerItem.find(".disclosure-triangle");
var isCollapsing = $disclosureIcon.hasClass("expanded");
$disclosureIcon.toggleClass("expanded");
$headerItem.nextUntil(".section-header").toggle(!isCollapsing); // explicit visibility arg, since during load() jQ doesn't think nodes are visible
// Update instance-specific state...
this._collapsedFiles[fullPath] = isCollapsing;
// ...AND persist as per-project view state
if (!duringInit) {
var setting = PreferencesManager.getViewState("inlineEditor.collapsedFiles", _getPrefsContext()) || {};
if (isCollapsing) {
setting[fullPath] = true;
} else {
delete setting[fullPath];
}
PreferencesManager.setViewState("inlineEditor.collapsedFiles", setting, _getPrefsContext());
}
// Show/hide selection indicator if selection was in collapsed section
this._updateSelectedMarker(false);
// Changing height of rule list may change ht of overall editor
this._ruleListHeightChanged();
// If user expands collapsed section and nothing selected yet, select first result in this section
if (this._selectedRangeIndex === -1 && !isCollapsing && !duringInit) {
var index = _.findIndex(this._ranges, function (resultItem) {
return resultItem.textRange.document.file.fullPath === fullPath;
});
this.setSelectedIndex(index);
}
};
/** Adds a file section header <li> to the range list UI ($rangeList) and adds it to the this._$headers map */
MultiRangeInlineEditor.prototype._createHeaderItem = function (doc) {
var $headerItem = $("<li class='section-header'><span class='disclosure-triangle expanded'/><span class='filename'>" + _.escape(doc.file.name) + "</span></li>")
.attr("title", ProjectManager.makeProjectRelativeIfPossible(doc.file.fullPath))
.appendTo(this.$rangeList);
$headerItem.click(function () {
this._toggleSection(doc.file.fullPath);
}.bind(this));
this._$headers[doc.file.fullPath] = $headerItem;
};
/** Refresh the contents of $rangeList */
MultiRangeInlineEditor.prototype._renderList = function () {
this.$rangeList.empty();
this._$headers = {};
var self = this,
lastSectionDoc,
numItemsInSection = 0;
// After seeing all results for a given file, update its header with total # of results
function finalizeSection() {
if (lastSectionDoc) {
self._$headers[lastSectionDoc.file.fullPath].append(" (" + numItemsInSection + ")");
if (self._collapsedFiles[lastSectionDoc.file.fullPath]) {
self._toggleSection(lastSectionDoc.file.fullPath, true);
}
}
}
this._ranges.forEach(function (resultItem) {
if (lastSectionDoc !== resultItem.textRange.document) {
// Finalize previous section
finalizeSection();
// Initialize new section
lastSectionDoc = resultItem.textRange.document;
numItemsInSection = 0;
// Create filename header for new section
this._createHeaderItem(lastSectionDoc);
}
numItemsInSection++;
this._createListItem(resultItem);
}, this);
// Finalize last section
finalizeSection();
};
/**
* @override
* @param {!Editor} hostEditor Outer Editor instance that inline editor will sit within.
*
*/
MultiRangeInlineEditor.prototype.load = function (hostEditor) {
MultiRangeInlineEditor.prototype.parentClass.load.apply(this, arguments);
// Create the message area
this.$messageDiv = $("<div/>")
.addClass("inline-editor-message");
// Prevent touch scroll events from bubbling up to the parent editor.
this.$editorHolder.on("mousewheel.MultiRangeInlineEditor", function (e) {
e.stopPropagation();
});
// Outer container for border-left and scrolling
this.$relatedContainer = $("<div/>").addClass("related-container");
// List "selection" highlight
this.$selectedMarker = $("<div/>").appendTo(this.$relatedContainer).addClass("selection");
// Inner container
this.$related = $("<div/>").appendTo(this.$relatedContainer).addClass("related");
// Range list
this.$rangeList = $("<ul/>").appendTo(this.$related);
// Determine which sections are initially collapsed (the actual collapsing happens after onAdded(),
// because jQuery.hide() requires the computed value of 'display' to work properly)
var toCollapse = PreferencesManager.getViewState("inlineEditor.collapsedFiles", _getPrefsContext()) || {};
Object.keys(toCollapse).forEach(function (fullPath) {
this._collapsedFiles[fullPath] = true;
}.bind(this));
// Render list & section headers (matching collapsed state set above)
this._renderList();
if (this._ranges.length > 1) { // attach to main container
this.$wrapper.before(this.$relatedContainer);
}
// Add TextRange listeners to update UI as text changes
var self = this;
this._ranges.forEach(function (range, index) {
// Update list item as TextRange changes
range.textRange.on("change", function () {
_updateRangeLabel(range.$listItem, range);
}).on("contentChange", function () {
_updateRangeLabel(range.$listItem, range, self._labelCB);
});
// If TextRange lost sync, remove it from the list (and close the widget if no other ranges are left)
range.textRange.on("lostSync", function () {
self._removeRange(range);
});
});
// Initial selection is the first non-collapsed result item
var indexToSelect = _.findIndex(this._ranges, function (range) {
return !this._collapsedFiles[range.textRange.document.file.fullPath];
}.bind(this));
if (this._ranges.length === 1 && indexToSelect === -1) {
// If no right-hand rule list shown, select the one result even if it's in a collapsed file (since no way to expand)
indexToSelect = 0;
}
if (indexToSelect !== -1) {
// select the first visible range
this.setSelectedIndex(indexToSelect);
} else {
// force the message div to show
this.setSelectedIndex(-1);
}
// Listen for clicks directly on us, so we can set focus back to the editor
var clickHandler = this._onClick.bind(this);
this.$htmlContent.on("click.MultiRangeInlineEditor", clickHandler);
// Also handle mouseup in case the user drags a little bit
this.$htmlContent.on("mouseup.MultiRangeInlineEditor", clickHandler);
// Update the rule list navigation menu items when we gain/lose focus.
this.$htmlContent
.on("focusin.MultiRangeInlineEditor", this._updateCommands.bind(this))
.on("focusout.MultiRangeInlineEditor", this._updateCommands.bind(this));
};
/**
* @private
* Updates the enablement for the rule list navigation commands.
*/
MultiRangeInlineEditor.prototype._updateCommands = function () {
var enabled = (this.hasFocus() && this._ranges.length > 1);
_prevMatchCmd.setEnabled(enabled && this._selectedRangeIndex > 0);
_nextMatchCmd.setEnabled(enabled && this._selectedRangeIndex !== -1 && this._selectedRangeIndex < this._ranges.length - 1);
};
/**
* @override
*/
MultiRangeInlineEditor.prototype.onAdded = function () {
// Set the initial position of the selected marker now that we're laid out.
this._updateSelectedMarker(false);
// Call super
MultiRangeInlineEditor.prototype.parentClass.onAdded.apply(this, arguments);
// Initially size the inline widget (calls sizeInlineWidgetToContents())
this._ruleListHeightChanged();
this._updateCommands();
};
/**
* Specify the range that is shown in the editor.
*
* @param {!number} index The index of the range to select, or -1 to deselect all. Index into this._ranges,
* so section headers are not included in the sequence.
* @param {boolean} force Whether to re-select the item even if we think it's already selected
* (used if the range list has changed).
*/
MultiRangeInlineEditor.prototype.setSelectedIndex = function (index, force) {
var newIndex = Math.min(Math.max(-1, index), this._ranges.length - 1),
self = this;
if (!force && newIndex !== -1 && newIndex === this._selectedRangeIndex) {
return;
}
// Remove selected class(es)
var $previousItem = (this._selectedRangeIndex >= 0) ? this._ranges[this._selectedRangeIndex].$listItem : null;
if ($previousItem) {
$previousItem.removeClass("selected");
}
// Clear our listeners on the previous editor since it'll be destroyed in setInlineContent().
if (this.editor) {
this.editor.off(".MultiRangeInlineEditor");
}
this._selectedRangeIndex = newIndex;
if (newIndex === -1) {
// show the message div
this.setInlineContent(null);
var hasHiddenMatches = this._ranges.length > 0;
if (hasHiddenMatches) {
this.$messageDiv.text(Strings.INLINE_EDITOR_HIDDEN_MATCHES);
} else if (this._messageCB) {
this._messageCB(hasHiddenMatches).done(function (msg) {
self.$messageDiv.html(msg);
});
} else {
this.$messageDiv.text(Strings.INLINE_EDITOR_NO_MATCHES);
}
this.$htmlContent.append(this.$messageDiv);
this.sizeInlineWidgetToContents();
} else {
this.$messageDiv.remove();
var range = this._getSelectedRange();
range.$listItem.addClass("selected");
// Add new editor
this.setInlineContent(range.textRange.document, range.textRange.startLine, range.textRange.endLine);
this.editor.focus();
this._updateEditorMinHeight();
this.editor.refresh();
// Ensure the cursor position is visible in the host editor as the user is arrowing around.
this.editor.on("cursorActivity.MultiRangeInlineEditor", this._ensureCursorVisible.bind(this));
// ensureVisibility is set to false because we don't want to scroll the main editor when the user selects a view
this.sizeInlineWidgetToContents();
this._updateSelectedMarker(true);
}
this._updateCommands();
};
/**
* Ensures that the editor's min-height is set so it never gets shorter than the rule list.
* This is necessary to make sure the editor's horizontal scrollbar stays at the bottom of the
* widget.
*/
MultiRangeInlineEditor.prototype._updateEditorMinHeight = function () {
if (!this.editor) {
return;
}
// Set the scroller's min-height to the natural height of the rule list, so the editor
// always stays at least as tall as the rule list.
var ruleListNaturalHeight = this.$related.outerHeight(),
headerHeight = $(".inline-editor-header", this.$htmlContent).outerHeight();
// If the widget isn't fully loaded yet, bail--we'll get called again in onAdded().
if (!ruleListNaturalHeight || !headerHeight) {
return;
}
// We have to set this on the scroller instead of the wrapper because:
// * we want the wrapper's actual height to remain "auto"
// * if we set a min-height on the wrapper, the scroller's height: 100% doesn't
// respect it (height: 100% doesn't seem to work properly with min-height on the parent)
$(this.editor.getScrollerElement())
.css("min-height", (ruleListNaturalHeight - headerHeight) + "px");
};
/** Update inline widget height to reflect changed rule-list height */
MultiRangeInlineEditor.prototype._ruleListHeightChanged = function () {
// Editor's min height depends on rule list height
this._updateEditorMinHeight();
// Overall widget height may have changed too
this.sizeInlineWidgetToContents();
};
MultiRangeInlineEditor.prototype._removeRange = function (range) {
// If this is the last range, just close the whole widget
if (this._ranges.length <= 1) {
this.close();
return; // note: the dispose() that would normally happen below is covered by close()
}
// Now we know there is at least one other range -> found out which one this is
var index = this._ranges.indexOf(range);
// If the range to be removed is the selected one, first switch to another one
if (index === this._selectedRangeIndex) {
// If possible, select the one below, else select the one above
if (index + 1 < this._ranges.length) {
this.setSelectedIndex(index + 1);
} else {
this.setSelectedIndex(index - 1);
}
}
// Now we can remove this range
range.textRange.dispose();
this._ranges.splice(index, 1);
// Re-render list & section headers
this._renderList();
// Move selection highlight if deletion affected its position
if (index < this._selectedRangeIndex) {
this._selectedRangeIndex--;
this._updateSelectedMarker(true);
}
if (this._ranges.length === 1) {
this.$relatedContainer.remove();
// Refresh the height of the inline editor since we remove
// the entire selector list.
if (this.editor) {
this.editor.refresh();
}
}
this._updateCommands();
};
/**
* Adds a new range to the inline editor and selects it. The range will be inserted
* immediately below the last range for the same document, or at the end of the list
* if there are no other ranges for that document.
* @param {string} name The label for the new range.
* @param {Document} doc The document the range is in.
* @param {number} lineStart The starting line of the range, 0-based, inclusive.
* @param {number} lineEnd The ending line of the range, 0-based, inclusive.
*/
MultiRangeInlineEditor.prototype.addAndSelectRange = function (name, doc, lineStart, lineEnd) {
var newRange = new SearchResultItem({
name: name,
document: doc,
lineStart: lineStart,
lineEnd: lineEnd
}),
i;
// Insert the new range after the last range from the same doc, or at the
// end of the list.
for (i = 0; i < this._ranges.length; i++) {
if (this._fileComparator(this._ranges[i].textRange.document.file, doc.file) > 0) {
break;
}
}
this._ranges.splice(i, 0, newRange);
// Update rule list display
this._renderList();
// Ensure rule list is visible if there are now multiple results
if (this._ranges.length > 1 && !this.$relatedContainer.parent().length) {
this.$wrapper.before(this.$relatedContainer);
}
// If added rule is in a collapsed item, expand it for clarity
if (this._collapsedFiles[doc.file.fullPath]) {
this._toggleSection(doc.file.fullPath);
}
// Select new range, showing it in the editor
this.setSelectedIndex(i, true); // force, since i might be same as before
this._updateCommands();
};
MultiRangeInlineEditor.prototype._updateSelectedMarker = function (animate) {
// If no selection or selection is in a collapsed section, just hide the marker
if (this._selectedRangeIndex < 0 || this._collapsedFiles[this._getSelectedRange().textRange.document.file.fullPath]) {
this.$selectedMarker.hide();
return;
}
var $rangeItem = this._ranges[this._selectedRangeIndex].$listItem;
// scroll the selection to the rangeItem
var containerHeight = this.$relatedContainer.height(),
itemTop = $rangeItem.position().top,
scrollTop = this.$relatedContainer.scrollTop();
this.$selectedMarker
.show()
.toggleClass("animate", animate)
.css("top", itemTop)
.height($rangeItem.outerHeight());
if (containerHeight <= 0) {
return;
}
var paddingTop = _parseStyleSize($rangeItem.parent(), "paddingTop");
if ((itemTop - paddingTop) < scrollTop) {
this.$relatedContainer.scrollTop(itemTop - paddingTop);
} else {
var itemBottom = itemTop + $rangeItem.height() + _parseStyleSize($rangeItem.parent(), "paddingBottom");
if (itemBottom > (scrollTop + containerHeight)) {
this.$relatedContainer.scrollTop(itemBottom - containerHeight);
}
}
};
/**
* Called any time inline is closed, whether manually (via closeThisInline()) or automatically
*/
MultiRangeInlineEditor.prototype.onClosed = function () {
// Superclass onClosed() destroys editor
MultiRangeInlineEditor.prototype.parentClass.onClosed.apply(this, arguments);
// de-ref all the Documents in the search results
this._ranges.forEach(function (searchResult) {
searchResult.textRange.dispose();
});
// Remove event handlers
this.$htmlContent.off(".MultiRangeInlineEditor");
this.$editorHolder.off(".MultiRangeInlineEditor");
};
/**
* Prevent clicks in the dead areas of the inlineWidget from changing the focus and insertion point in the editor.
* This is done by detecting clicks in the inlineWidget that are not inside the editor or the range list and
* restoring focus and the insertion point.
*/
MultiRangeInlineEditor.prototype._onClick = function (event) {
if (!this.editor) {
return;
}
var childEditor = this.editor,
editorRoot = childEditor.getRootElement(),
editorPos = $(editorRoot).offset();
function containsClick($parent) {
return $parent.find(event.target).length > 0 || $parent[0] === event.target;
}
// Ignore clicks in editor and clicks on filename link
// Check clicks on filename link in the context of the current inline widget.
if (!containsClick($(editorRoot)) && !containsClick($(".filename", this.$htmlContent))) {
childEditor.focus();
// Only set the cursor if the click isn't in the range list.
if (!containsClick(this.$relatedContainer)) {
if (event.pageY < editorPos.top) {
childEditor.setCursorPos(0, 0);
} else if (event.pageY > editorPos.top + $(editorRoot).height()) {
var lastLine = childEditor.getLastVisibleLine();
childEditor.setCursorPos(lastLine, childEditor.document.getLine(lastLine).length);
}
}
}
};
/**
* Based on the position of the cursor in the inline editor, determine whether we need to change the
* vertical scroll position of the host editor to ensure that the cursor is visible.
*/
MultiRangeInlineEditor.prototype._ensureCursorVisible = function () {
if (!this.editor) {
return;
}
if ($.contains(this.editor.getRootElement(), window.document.activeElement)) {
var hostScrollPos = this.hostEditor.getScrollPos(),
cursorCoords = this.editor._codeMirror.cursorCoords();
// Vertically, we want to set the scroll position relative to the overall host editor, not
// the lineSpace of the widget itself. We don't want to modify the horizontal scroll position.
var scrollerTop = this.hostEditor.getVirtualScrollAreaTop();
this.hostEditor._codeMirror.scrollIntoView({
left: hostScrollPos.x,
top: cursorCoords.top - scrollerTop,
right: hostScrollPos.x,
bottom: cursorCoords.bottom - scrollerTop
});
}
};
/**
* Overwrite InlineTextEditor's _onLostContent to do nothing if the document's file is deleted
* (deletes are handled via TextRange's lostSync).
*/
MultiRangeInlineEditor.prototype._onLostContent = function (event, cause) {
// Ignore when the editor's content got lost due to a deleted file
if (cause && cause.type === "deleted") { return; }
// Else yield to the parent's implementation
return MultiRangeInlineEditor.prototype.parentClass._onLostContent.apply(this, arguments);
};
/**
* @return {Array.<SearchResultItem>}
*/
MultiRangeInlineEditor.prototype._getRanges = function () {
return this._ranges;
};
/**
* @return {!SearchResultItem}
*/
MultiRangeInlineEditor.prototype._getSelectedRange = function () {
return this._selectedRangeIndex >= 0 ? this._ranges[this._selectedRangeIndex] : null;
};
/**
* Move the selection up or down, skipping any collapsed groups. If selection is currently IN a
* collapsed group, we expand it first so that other items in the same file are eligible.
*/
MultiRangeInlineEditor.prototype._selectNextPrev = function (dir) {
if (this._selectedRangeIndex === -1) {
return;
}
// Traverse up or down the list until we find an item eligible for selection
var origDoc = this._ranges[this._selectedRangeIndex].textRange.document,
i;
for (i = this._selectedRangeIndex + dir; i >= 0 && i < this._ranges.length; i += dir) {
var doc = this._ranges[i].textRange.document;
// If first candidate is in same collapsed group as current selection, expand it
if (doc === origDoc && this._collapsedFiles[doc.file.fullPath]) {
this._toggleSection(doc.file.fullPath);
}
// Only consider expanded groups now
if (!this._collapsedFiles[doc.file.fullPath]) {
this.setSelectedIndex(i);
return;
}
}
// If we got here, we couldn't find any eligible item - so do nothing. Happens if selection is
// already the first/last item, or if all remaining items above/below the selection are collapsed.
};
/**
* Display the next range in the range list
*/
MultiRangeInlineEditor.prototype._selectNextRange = function () {
this._selectNextPrev(1);
};
/**
* Display the previous range in the range list
*/
MultiRangeInlineEditor.prototype._selectPreviousRange = function () {
this._selectNextPrev(-1);
};
/**
* Sizes the inline widget height to be the maximum between the range list height and the editor height
* @override
*/
MultiRangeInlineEditor.prototype.sizeInlineWidgetToContents = function () {
// Size the code mirror editors height to the editor content
MultiRangeInlineEditor.prototype.parentClass.sizeInlineWidgetToContents.call(this);
// Size the widget height to the max between the editor/message content and the related ranges list
var widgetHeight = Math.max(this.$related.height(),
this.$header.outerHeight() +
(this._selectedRangeIndex === -1 ? this.$messageDiv.outerHeight() : this.$editorHolder.height()));
if (widgetHeight) {
this.hostEditor.setInlineWidgetHeight(this, widgetHeight, false);
}
};
/**
* Called when the editor containing the inline is made visible. Updates UI based on
* state that might have changed while the editor was hidden.
*/
MultiRangeInlineEditor.prototype.onParentShown = function () {
MultiRangeInlineEditor.prototype.parentClass.onParentShown.apply(this, arguments);
this._updateSelectedMarker(false);
};
/**
* Refreshes the height of the inline editor and all child editors.
* @override
*/
MultiRangeInlineEditor.prototype.refresh = function () {
MultiRangeInlineEditor.prototype.parentClass.refresh.apply(this, arguments);
this.sizeInlineWidgetToContents();
if (this.editor) {
this.editor.refresh();
}
};
/**
* Returns the currently focused MultiRangeInlineEditor.
* @return {MultiRangeInlineEditor}
*/
function getFocusedMultiRangeInlineEditor() {
var focusedWidget = EditorManager.getFocusedInlineWidget();
if (focusedWidget instanceof MultiRangeInlineEditor) {
return focusedWidget;
} else {
return null;
}
}
/**
* Previous Range command handler
*/
function _previousRange() {
var focusedMultiRangeInlineEditor = getFocusedMultiRangeInlineEditor();
if (focusedMultiRangeInlineEditor) {
focusedMultiRangeInlineEditor._selectPreviousRange();
}
}
/**
* Next Range command handler
*/
function _nextRange() {
var focusedMultiRangeInlineEditor = getFocusedMultiRangeInlineEditor();
if (focusedMultiRangeInlineEditor) {
focusedMultiRangeInlineEditor._selectNextRange();
}
}
_prevMatchCmd = CommandManager.register(Strings.CMD_QUICK_EDIT_PREV_MATCH, Commands.QUICK_EDIT_PREV_MATCH, _previousRange);
_prevMatchCmd.setEnabled(false);
_nextMatchCmd = CommandManager.register(Strings.CMD_QUICK_EDIT_NEXT_MATCH, Commands.QUICK_EDIT_NEXT_MATCH, _nextRange);
_nextMatchCmd.setEnabled(false);
exports.MultiRangeInlineEditor = MultiRangeInlineEditor;
exports.getFocusedMultiRangeInlineEditor = getFocusedMultiRangeInlineEditor;
});