diff --git a/src/editor/CSSInlineEditor.js b/src/editor/CSSInlineEditor.js
index 01b2dca766d..644a94a2fe0 100644
--- a/src/editor/CSSInlineEditor.js
+++ b/src/editor/CSSInlineEditor.js
@@ -23,16 +23,25 @@
/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
-/*global define, $, CodeMirror, window */
+/*global define, $, CodeMirror, window, Mustache */
define(function (require, exports, module) {
"use strict";
// Load dependent modules
var CSSUtils = require("language/CSSUtils"),
+ DocumentManager = require("document/DocumentManager"),
+ DropdownEventHandler = require("utils/DropdownEventHandler").DropdownEventHandler,
EditorManager = require("editor/EditorManager"),
+ Editor = require("editor/Editor").Editor,
+ FileIndexManager = require("project/FileIndexManager"),
HTMLUtils = require("language/HTMLUtils"),
- MultiRangeInlineEditor = require("editor/MultiRangeInlineEditor").MultiRangeInlineEditor;
+ Menus = require("command/Menus"),
+ MultiRangeInlineEditor = require("editor/MultiRangeInlineEditor").MultiRangeInlineEditor,
+ PopUpManager = require("widgets/PopUpManager"),
+ Strings = require("strings");
+
+ var StylesheetsMenuTemplate = require("text!htmlContent/stylesheets-menu.html");
/**
* Given a position in an HTML editor, returns the relevant selector for the attribute/tag
@@ -78,6 +87,33 @@ define(function (require, exports, module) {
return selectorName;
}
+ /**
+ * @private
+ * Create the list of stylesheets in the dropdown menu.
+ * @return {string} The html content
+ */
+ function _renderList(cssFileInfos) {
+ var templateVars = {
+ styleSheetList : cssFileInfos
+ };
+
+ return Mustache.render(StylesheetsMenuTemplate, templateVars);
+ }
+
+ /**
+ * @private
+ * Add a new rule for the given selector to the given document, then add the rule to the
+ * given inline editor.
+ * @param {string} selectorName The selector to create a rule for.
+ * @param {MultiRangeInlineEditor} inlineEditor The inline editor to display the new rule in.
+ * @param {Document} styleDoc The document the rule should be inserted in.
+ */
+ function _addRule(selectorName, inlineEditor, styleDoc) {
+ var newRuleInfo = CSSUtils.addRuleToDocument(styleDoc, selectorName, Editor.getUseTabChar(), Editor.getSpaceUnits());
+ inlineEditor.addAndSelectRange(selectorName, styleDoc, newRuleInfo.range.from.line, newRuleInfo.range.to.line);
+ inlineEditor.editor.setCursorPos(newRuleInfo.pos.line, newRuleInfo.pos.ch);
+ }
+
/**
* This function is registered with EditManager as an inline editor provider. It creates a CSSInlineEditor
* when cursor is on an HTML tag name, class attribute, or id attribute, find associated
@@ -89,6 +125,7 @@ define(function (require, exports, module) {
* or null if we're not going to provide anything.
*/
function htmlToCSSProvider(hostEditor, pos) {
+
// Only provide a CSS editor when cursor is in HTML content
if (hostEditor.getLanguageForSelection().getId() !== "html") {
return null;
@@ -107,19 +144,126 @@ define(function (require, exports, module) {
return null;
}
- var result = new $.Deferred();
+ var result = new $.Deferred(),
+ cssInlineEditor,
+ cssFileInfos = [],
+ $newRuleButton,
+ $dropdown,
+ $dropdownItem,
+ dropdownEventHandler;
+
+ /**
+ * @private
+ * Close the dropdown externally to dropdown, which ultimately calls the
+ * _cleanupDropdown callback.
+ */
+ function _closeDropdown() {
+ if (dropdownEventHandler) {
+ dropdownEventHandler.close();
+ }
+ }
+
+ /**
+ * @private
+ * Remove the various event handlers that close the dropdown. This is called by the
+ * PopUpManager when the dropdown is closed.
+ */
+ function _cleanupDropdown() {
+ $("html").off("click", _closeDropdown);
+ dropdownEventHandler = null;
+ $dropdown = null;
+
+ EditorManager.focusEditor();
+ }
+
+ /**
+ * @private
+ * Callback when item from dropdown list is selected
+ * @param {jQueryObject} $link The `a` element selected with mouse or keyboard
+ */
+ function _onSelect($link) {
+ var path = $link.data("path");
+
+ if (path) {
+ DocumentManager.getDocumentForPath(path).done(function (styleDoc) {
+ _addRule(selectorName, cssInlineEditor, styleDoc);
+ });
+ }
+ }
+
+ /**
+ * @private
+ * Show or hide the stylesheets dropdown.
+ */
+ function _showDropdown() {
+ Menus.closeAll();
+
+ $dropdown = $(_renderList(cssFileInfos));
+
+ var toggleOffset = $newRuleButton.offset();
+ $dropdown
+ .css({
+ left: toggleOffset.left,
+ top: toggleOffset.top + $newRuleButton.outerHeight()
+ })
+ .appendTo($("body"));
+
+ $("html").on("click", _closeDropdown);
+
+ dropdownEventHandler = new DropdownEventHandler($dropdown, _onSelect, _cleanupDropdown);
+ dropdownEventHandler.open();
+
+ $dropdown.focus();
+ }
+
+ /**
+ * @private
+ * Checks to see if there are any stylesheets in the project, and returns the appropriate
+ * "no rules"/"no stylesheets" message accordingly.
+ * @return {$.Promise} a promise that is resolved with the message to show. Never rejected.
+ */
+ function _getNoRulesMsg() {
+ var result = new $.Deferred();
+ FileIndexManager.getFileInfoList("css").done(function (fileInfos) {
+ result.resolve(fileInfos.length ? Strings.CSS_QUICK_EDIT_NO_MATCHES : Strings.CSS_QUICK_EDIT_NO_STYLESHEETS);
+ });
+ return result;
+ }
CSSUtils.findMatchingRules(selectorName, hostEditor.document)
.done(function (rules) {
- if (rules && rules.length > 0) {
- var cssInlineEditor = new MultiRangeInlineEditor(rules);
- cssInlineEditor.load(hostEditor);
-
- result.resolve(cssInlineEditor);
- } else {
- // No matching rules were found.
- result.reject();
- }
+ cssInlineEditor = new MultiRangeInlineEditor(rules || [], _getNoRulesMsg);
+ cssInlineEditor.load(hostEditor);
+
+ var $header = $(".inline-editor-header", cssInlineEditor.$htmlContent);
+ $newRuleButton = $("")
+ .text(Strings.BUTTON_NEW_RULE)
+ .on("click", function (e) {
+ if (!$newRuleButton.hasClass("disabled")) {
+ // toggle dropdown
+ if ($dropdown) {
+ _closeDropdown();
+ } else {
+ _showDropdown();
+ }
+ }
+ e.stopPropagation();
+ });
+ $header.append($newRuleButton);
+
+ result.resolve(cssInlineEditor);
+
+ // Now that dialog has been built, collect list of stylesheets
+ FileIndexManager.getFileInfoList("css")
+ .done(function (fileInfos) {
+ cssFileInfos = fileInfos;
+
+ // "New Rule" button is disabled by default and gets enabled
+ // here if there are any stylesheets in project
+ if (cssFileInfos.length > 0) {
+ $newRuleButton.removeClass("disabled");
+ }
+ });
})
.fail(function () {
console.log("Error in findMatchingRules()");
diff --git a/src/editor/EditorManager.js b/src/editor/EditorManager.js
index 4e92d036ae1..3c39e9ce1c9 100644
--- a/src/editor/EditorManager.js
+++ b/src/editor/EditorManager.js
@@ -298,8 +298,8 @@ define(function (require, exports, module) {
if (hostEditor) {
hostEditor.getInlineWidgets().forEach(function (widget) {
- if (widget instanceof InlineTextEditor) {
- inlineEditors = inlineEditors.concat(widget.editors);
+ if (widget instanceof InlineTextEditor && widget.editor) {
+ inlineEditors.push(widget.editor);
}
});
}
@@ -343,8 +343,14 @@ define(function (require, exports, module) {
* @return {{content:DOMElement, editor:Editor}}
*/
function createInlineEditorForDocument(doc, range, inlineContent) {
- // Create the Editor
+ // Hide the container for the editor before creating it so that CodeMirror doesn't do extra work
+ // when initializing the document. When we construct the editor, we have to set its text and then
+ // set the (small) visible range that we show in the editor. If the editor is visible, CM has to
+ // render a large portion of the document before setting the visible range. By hiding the editor
+ // first and showing it after the visible range is set, we avoid that initial render.
+ $(inlineContent).hide();
var inlineEditor = _createEditorForDocument(doc, false, inlineContent, range);
+ $(inlineContent).show();
return { content: inlineContent, editor: inlineEditor };
}
diff --git a/src/editor/InlineTextEditor.js b/src/editor/InlineTextEditor.js
index 2f6ea3c3e84..6aefd3b86a8 100644
--- a/src/editor/InlineTextEditor.js
+++ b/src/editor/InlineTextEditor.js
@@ -61,7 +61,7 @@ define(function (require, exports, module) {
* @private
*/
function _dirtyFlagChangeHandler(event, doc) {
- var $dirtyIndicators = $(".inlineEditorHolder .dirty-indicator"),
+ var $dirtyIndicators = $(".inline-editor-holder .dirty-indicator"),
$indicator;
$dirtyIndicators.each(function (index, indicator) {
@@ -79,14 +79,18 @@ define(function (require, exports, module) {
function InlineTextEditor() {
InlineWidget.call(this);
- /* @type {Array.<{Editor}>}*/
- this.editors = [];
+ /* @type {Editor}*/
+ this.editor = null;
}
InlineTextEditor.prototype = Object.create(InlineWidget.prototype);
InlineTextEditor.prototype.constructor = InlineTextEditor;
InlineTextEditor.prototype.parentClass = InlineWidget.prototype;
- InlineTextEditor.prototype.editors = null;
+ InlineTextEditor.prototype.$wrapper = null;
+ 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
@@ -132,10 +136,8 @@ define(function (require, exports, module) {
_syncGutterWidths(this.hostEditor);
- this.editors.forEach(function (editor) {
- $(editor).off(".InlineTextEditor");
- editor.destroy(); //release ref on Document
- });
+ // Destroy the inline editor.
+ this.setInlineContent(null);
};
/**
@@ -159,9 +161,9 @@ define(function (require, exports, module) {
InlineTextEditor.prototype.parentClass.onAdded.apply(this, arguments);
- this.editors.forEach(function (editor) {
- editor.refresh();
- });
+ if (this.editor) {
+ this.editor.refresh();
+ }
// Update display of inline editors when the hostEditor signals a redraw
CodeMirror.on(this.info, "redraw", function () {
@@ -169,50 +171,59 @@ define(function (require, exports, module) {
// re-added the widget to the DOM. This is filed as https://github.com/marijnh/CodeMirror/issues/1226.
// For now, we can work around it by doing the refresh on a setTimeout().
window.setTimeout(function () {
- self.editors[0].refresh();
+ if (self.editor) {
+ self.editor.refresh();
+ }
}, 0);
});
_syncGutterWidths(this.hostEditor);
- this.editors[0].focus();
+ 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 () {
- var focusedI = CollectionUtils.indexOf(this.editors, function (editor) {
- return editor.hasFocus();
- });
- return this.editors[focusedI]; // returns undefined if -1, which works
+ if (this.editor && this.editor.hasFocus()) {
+ return this.editor;
+ }
+ return null;
};
/**
- *
- * @param {Document} doc
- * @param {number} startLine of text to show in inline editor
- * @param {number} endLine of text to show in inline editor
- * @param {HTMLDivElement} container container to hold the inline editor
+ * 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.createInlineEditorFromText = function (doc, startLine, endLine, container) {
+ 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
};
- // root container holding header & editor
- var $wrapperDiv = $("
");
- var wrapperDiv = $wrapperDiv[0];
-
- // header containing filename, dirty indicator, line number
- var $header = $("").addClass("inline-editor-header");
-
- var $filenameInfo = $("").addClass("filename");
-
// dirty indicator, with file path stored on it
var $dirtyIndicatorDiv = $("")
.addClass("dirty-indicator")
@@ -221,42 +232,36 @@ define(function (require, exports, module) {
this.$lineNumber = $("");
- // wrap filename & line number in clickable link with tooltip
- $filenameInfo.append($dirtyIndicatorDiv)
+ // 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
- $filenameInfo.click(function () {
+ this.$filename.on("click.InlineTextEditor", function () {
CommandManager.execute(Commands.FILE_OPEN, { fullPath: doc.file.fullPath })
.done(function () {
EditorManager.getCurrentFullEditor().setCursorPos(startLine, 0, true);
});
});
- $header.append($filenameInfo);
- $wrapperDiv.append($header);
+ var inlineInfo = EditorManager.createInlineEditorForDocument(doc, range, this.$editorHolder.get(0));
+ this.editor = inlineInfo.editor;
- // Create actual Editor instance
- var inlineInfo = EditorManager.createInlineEditorForDocument(doc, range, wrapperDiv);
- this.editors.push(inlineInfo.editor);
- container.appendChild(wrapperDiv);
-
// Init line number display
- this._updateLineRange = this._updateLineRange.bind(this);
this._updateLineRange(inlineInfo.editor);
// Always update the widget height when an inline editor completes a
// display update
- $(inlineInfo.editor).on("update.InlineTextEditor", function (event, editor) {
+ $(this.editor).on("update.InlineTextEditor", function (event, editor) {
self.sizeInlineWidgetToContents(true);
});
// 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)
- $(inlineInfo.editor).on("change.InlineTextEditor", function (event, editor) {
+ $(this.editor).on("change.InlineTextEditor", function (event, editor) {
if (self.hostEditor.isFullyVisible()) {
self.sizeInlineWidgetToContents(true);
self._updateLineRange(editor);
@@ -264,7 +269,7 @@ define(function (require, exports, module) {
});
// If Document's file is deleted, or Editor loses sync with Document, delegate to this._onLostContent()
- $(inlineInfo.editor).on("lostContent.InlineTextEditor", function () {
+ $(this.editor).on("lostContent.InlineTextEditor", function () {
self._onLostContent.apply(self, arguments);
});
@@ -296,24 +301,25 @@ define(function (require, exports, module) {
InlineTextEditor.prototype.load = function (hostEditor) {
InlineTextEditor.prototype.parentClass.load.apply(this, arguments);
- // TODO: incomplete impelementation. It's not clear yet if InlineTextEditor
- // will fuction as an abstract class or as generic inline editor implementation
- // that just shows a range of text. See CSSInlineEditor.css for an implementation of load()
+ // We don't create the actual editor here--that will happen the first time
+ // setInlineContent() is called.
+ this.$wrapper = $("").appendTo(this.$htmlContent);
+ this.$header = $("").addClass("inline-editor-header").appendTo(this.$wrapper);
+ this.$filename = $("").addClass("filename").appendTo(this.$header);
+ this.$editorHolder = $("").addClass("inline-editor-holder").appendTo(this.$wrapper);
};
/**
* Called when the editor containing the inline is made visible.
*/
InlineTextEditor.prototype.onParentShown = function () {
- var self = this;
-
InlineTextEditor.prototype.parentClass.onParentShown.apply(this, arguments);
// Refresh line number display and codemirror line number gutter
- this.editors.forEach(function (editor) {
- self._updateLineRange(editor);
- editor.refresh();
- });
+ if (this.editor) {
+ this._updateLineRange(this.editor);
+ this.editor.refresh();
+ }
// We need to call this explicitly whenever the host editor is reshown
this.sizeInlineWidgetToContents(true);
diff --git a/src/editor/MultiRangeInlineEditor.js b/src/editor/MultiRangeInlineEditor.js
index f1396e77c31..d5dc307a839 100644
--- a/src/editor/MultiRangeInlineEditor.js
+++ b/src/editor/MultiRangeInlineEditor.js
@@ -83,9 +83,11 @@ define(function (require, exports, module) {
/**
* @constructor
* @param {Array.<{name:String,document:Document,lineStart:number,lineEnd:number}>} ranges The text ranges to display.
+ * @param {function(): $.Promise} messageCB An optional callback that returns a promise that will be resolved with a message to show
+ * when no matches are available.
* @extends {InlineTextEditor}
*/
- function MultiRangeInlineEditor(ranges) {
+ function MultiRangeInlineEditor(ranges, messageCB) {
InlineTextEditor.call(this);
// Store the results to show in the range list. This creates TextRanges bound to the Document,
@@ -93,6 +95,7 @@ define(function (require, exports, module) {
this._ranges = ranges.map(function (rangeResult) {
return new SearchResultItem(rangeResult);
});
+ this._messageCB = messageCB;
this._selectedRangeIndex = -1;
}
@@ -100,14 +103,43 @@ define(function (require, exports, module) {
MultiRangeInlineEditor.prototype.constructor = MultiRangeInlineEditor;
MultiRangeInlineEditor.prototype.parentClass = InlineTextEditor.prototype;
- MultiRangeInlineEditor.prototype.$editorsDiv = null;
+ MultiRangeInlineEditor.prototype.$messageDiv = null;
MultiRangeInlineEditor.prototype.$relatedContainer = null;
MultiRangeInlineEditor.prototype.$related = null;
MultiRangeInlineEditor.prototype.$selectedMarker = null;
+ MultiRangeInlineEditor.prototype.$rangeList = null;
/** @type {Array.} */
MultiRangeInlineEditor.prototype._ranges = null;
MultiRangeInlineEditor.prototype._selectedRangeIndex = null;
+ MultiRangeInlineEditor.prototype._messageCB = null;
+
+ /**
+ * @private
+ * Add a new range to the range list UI.
+ * @param {SearchResultItem} range The range to add.
+ */
+ MultiRangeInlineEditor.prototype._createListItem = function (range) {
+ var self = this,
+ $rangeItem = $("").appendTo(this.$rangeList);
+
+ _updateRangeLabel($rangeItem, range);
+ $rangeItem.mousedown(function () {
+ self.setSelectedIndex(self._ranges.indexOf(range));
+ });
+
+ range.$listItem = $rangeItem;
+
+ // Update list item as TextRange changes
+ $(range.textRange).on("change", function () {
+ _updateRangeLabel($rangeItem, range);
+ });
+
+ // 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);
+ });
+ };
/**
* @override
@@ -117,60 +149,43 @@ define(function (require, exports, module) {
MultiRangeInlineEditor.prototype.load = function (hostEditor) {
MultiRangeInlineEditor.prototype.parentClass.load.apply(this, arguments);
- // Container to hold all editors
- var self = this;
-
- // Create DOM to hold editors and related list
- this.$editorsDiv = $(window.document.createElement("div")).addClass("inlineEditorHolder");
+ // Create the message area
+ this.$messageDiv = $("")
+ .addClass("inline-editor-message");
// Prevent touch scroll events from bubbling up to the parent editor.
- this.$editorsDiv.on("mousewheel.MultiRangeInlineEditor", function (e) {
+ this.$editorHolder.on("mousewheel.MultiRangeInlineEditor", function (e) {
e.stopPropagation();
});
// Outer container for border-left and scrolling
- this.$relatedContainer = $(window.document.createElement("div")).addClass("related-container");
+ this.$relatedContainer = $("").addClass("related-container");
// List "selection" highlight
- this.$selectedMarker = $(window.document.createElement("div")).appendTo(this.$relatedContainer).addClass("selection");
+ this.$selectedMarker = $("").appendTo(this.$relatedContainer).addClass("selection");
// Inner container
- this.$related = $(window.document.createElement("div")).appendTo(this.$relatedContainer).addClass("related");
+ this.$related = $("").appendTo(this.$relatedContainer).addClass("related");
// Range list
- var $rangeList = $(window.document.createElement("ul")).appendTo(this.$related);
+ this.$rangeList = $("
").appendTo(this.$related);
// create range list & add listeners for range textrange changes
var rangeItemText;
- this._ranges.forEach(function (range) {
- // Create list item UI
- var $rangeItem = $(window.document.createElement("li")).appendTo($rangeList);
- _updateRangeLabel($rangeItem, range);
- $rangeItem.mousedown(function () {
- self.setSelectedIndex(self._ranges.indexOf(range));
- });
-
- range.$listItem = $rangeItem;
-
- // Update list item as TextRange changes
- $(range.textRange).on("change", function () {
- _updateRangeLabel($rangeItem, range);
- });
-
- // 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);
- });
- });
-
- // select the first range
- self.setSelectedIndex(0);
+ this._ranges.forEach(this._createListItem, this);
if (this._ranges.length > 1) { // attach to main container
- this.$htmlContent.append(this.$relatedContainer);
+ this.$wrapper.before(this.$relatedContainer);
}
- this.$htmlContent.append(this.$editorsDiv);
+ if (this._ranges.length) {
+ // select the first range
+ this.setSelectedIndex(0);
+ } 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);
@@ -200,58 +215,72 @@ define(function (require, exports, module) {
// Editor must be at least as tall as the related list
this._updateEditorMinHeight();
+
+ // Set the initial inline widget height
+ this.sizeInlineWidgetToContents(true, false);
};
/**
* Specify the range that is shown in the editor.
*
- * @param {!number} index The index of the range to select.
+ * @param {!number} index The index of the range to select, or -1 to deselect all.
+ * @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) {
- var newIndex = Math.min(Math.max(0, index), this._ranges.length - 1),
+ MultiRangeInlineEditor.prototype.setSelectedIndex = function (index, force) {
+ var newIndex = Math.min(Math.max(-1, index), this._ranges.length - 1),
self = this;
- if (newIndex === this._selectedRangeIndex) {
+ 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");
}
- this._selectedRangeIndex = newIndex;
-
- var $rangeItem = this._ranges[this._selectedRangeIndex].$listItem;
-
- this._ranges[this._selectedRangeIndex].$listItem.addClass("selected");
-
- // Remove previous editors
- this.editors.forEach(function (editor) {
- $(self.editors[0]).off(".MultiRangeInlineEditor");
- editor.destroy(); //release ref on Document
- });
-
- this.editors = [];
- this.$editorsDiv.children().remove();
-
- // Add new editor
- var range = this._getSelectedRange();
- this.createInlineEditorFromText(range.textRange.document, range.textRange.startLine, range.textRange.endLine, this.$editorsDiv.get(0));
- this.editors[0].focus();
+ // Clear our listeners on the previous editor since it'll be destroyed in setInlineContent().
+ if (this.editor) {
+ $(this.editor).off(".MultiRangeInlineEditor");
+ }
- this._updateEditorMinHeight();
- this.editors[0].refresh();
+ this._selectedRangeIndex = newIndex;
- // Ensure the cursor position is visible in the host editor as the user is arrowing around.
- $(this.editors[0]).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(true, false);
-
- this._updateSelectedMarker();
+ if (newIndex === -1) {
+ // show the message div
+ this.setInlineContent(null);
+ if (this._messageCB) {
+ this._messageCB().done(function (msg) {
+ self.$messageDiv.html(msg);
+ });
+ } else {
+ this.$messageDiv.text(Strings.INLINE_EDITOR_NO_MATCHES);
+ }
+ this.$htmlContent.append(this.$messageDiv);
+ this.sizeInlineWidgetToContents(true, false);
+ } 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(true, false);
+
+ this._updateSelectedMarker();
+ }
};
/**
@@ -260,6 +289,10 @@ define(function (require, exports, module) {
* 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(),
@@ -274,7 +307,7 @@ define(function (require, exports, module) {
// * 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.editors[0].getScrollerElement())
+ $(this.editor.getScrollerElement())
.css("min-height", (ruleListNaturalHeight - headerHeight) + "px");
};
@@ -309,8 +342,56 @@ define(function (require, exports, module) {
this._updateSelectedMarker();
}
};
+
+ /**
+ * 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 = this._ranges.length - 1; i >= 0; i--) {
+ if (this._ranges[i].textRange.document === doc) {
+ break;
+ }
+ }
+ if (i === -1) {
+ i = this._ranges.length;
+ } else {
+ i++;
+ }
+ this._ranges.splice(i, 0, newRange);
+
+ // Add the new range to the UI and select it. This should load the associated range
+ // into the editor.
+ this._createListItem(newRange);
+ this.setSelectedIndex(i, true);
+
+ // Ensure that the rule list becomes visible if it wasn't already and we have
+ // more than one rule.
+ if (this._ranges.length > 1 && !this.$relatedContainer.parent().length) {
+ this.$wrapper.before(this.$relatedContainer);
+ }
+ };
MultiRangeInlineEditor.prototype._updateSelectedMarker = function () {
+ if (this._selectedRangeIndex < 0) {
+ return new $.Deferred().resolve().promise();
+ }
+
var result = new $.Deferred(),
$rangeItem = this._ranges[this._selectedRangeIndex].$listItem;
@@ -360,7 +441,7 @@ define(function (require, exports, module) {
// Remove event handlers
this.$htmlContent.off(".MultiRangeInlineEditor");
- this.$editorsDiv.off(".MultiRangeInlineEditor");
+ this.$editorHolder.off(".MultiRangeInlineEditor");
};
/**
@@ -369,7 +450,11 @@ define(function (require, exports, module) {
* restoring focus and the insertion point.
*/
MultiRangeInlineEditor.prototype._onClick = function (event) {
- var childEditor = this.editors[0],
+ if (!this.editor) {
+ return;
+ }
+
+ var childEditor = this.editor,
editorRoot = childEditor.getRootElement(),
editorPos = $(editorRoot).offset();
@@ -378,7 +463,7 @@ define(function (require, exports, module) {
}
// Ignore clicks in editor and clicks on filename link
- if (!containsClick($(editorRoot)) && !containsClick($(".filename", this.$editorsDiv))) {
+ if (!containsClick($(editorRoot)) && !containsClick($(".filename", this.$editorHolder))) {
childEditor.focus();
// Only set the cursor if the click isn't in the range list.
if (!containsClick(this.$relatedContainer)) {
@@ -397,9 +482,13 @@ define(function (require, exports, module) {
* vertical scroll position of the host editor to ensure that the cursor is visible.
*/
MultiRangeInlineEditor.prototype._ensureCursorVisible = function () {
- if ($.contains(this.editors[0].getRootElement(), window.document.activeElement)) {
+ if (!this.editor) {
+ return;
+ }
+
+ if ($.contains(this.editor.getRootElement(), window.document.activeElement)) {
var hostScrollPos = this.hostEditor.getScrollPos(),
- cursorCoords = this.editors[0]._codeMirror.cursorCoords();
+ 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.
@@ -435,21 +524,25 @@ define(function (require, exports, module) {
* @return {!SearchResultItem}
*/
MultiRangeInlineEditor.prototype._getSelectedRange = function () {
- return this._ranges[this._selectedRangeIndex];
+ return this._selectedRangeIndex >= 0 ? this._ranges[this._selectedRangeIndex] : null;
};
/**
* Display the next range in the range list
*/
MultiRangeInlineEditor.prototype._selectNextRange = function () {
- this.setSelectedIndex(this._selectedRangeIndex + 1);
+ if (this._selectedRangeIndex < this._ranges.length - 1) {
+ this.setSelectedIndex(this._selectedRangeIndex + 1);
+ }
};
/**
* Display the previous range in the range list
*/
MultiRangeInlineEditor.prototype._selectPreviousRange = function () {
- this.setSelectedIndex(this._selectedRangeIndex - 1);
+ if (this._selectedRangeIndex > 0) {
+ this.setSelectedIndex(this._selectedRangeIndex - 1);
+ }
};
/**
@@ -463,8 +556,10 @@ define(function (require, exports, module) {
// We use "call" rather than "apply" here since ensureVisibility was an argument added just for this override.
MultiRangeInlineEditor.prototype.parentClass.sizeInlineWidgetToContents.call(this, force);
- // Size the widget height to the max between the editor content and the related ranges list
- var widgetHeight = Math.max(this.$related.height(), this.$editorsDiv.height());
+ // 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, ensureVisibility);
@@ -478,9 +573,9 @@ define(function (require, exports, module) {
MultiRangeInlineEditor.prototype.refresh = function () {
MultiRangeInlineEditor.prototype.parentClass.refresh.apply(this, arguments);
this.sizeInlineWidgetToContents(true);
- this.editors.forEach(function (editor) {
- editor.refresh();
- });
+ if (this.editor) {
+ this.editor.refresh();
+ }
};
/**
diff --git a/src/extensions/default/JavaScriptQuickEdit/unittests.js b/src/extensions/default/JavaScriptQuickEdit/unittests.js
index 6cd8e927a19..a10416b89f4 100644
--- a/src/extensions/default/JavaScriptQuickEdit/unittests.js
+++ b/src/extensions/default/JavaScriptQuickEdit/unittests.js
@@ -241,7 +241,7 @@ define(function (require, exports, module) {
runs(function () {
var inlineWidget = EditorManager.getCurrentFullEditor().getInlineWidgets()[0];
- var inlinePos = inlineWidget.editors[0].getCursorPos();
+ var inlinePos = inlineWidget.editor.getCursorPos();
// verify cursor position in inline editor
expect(inlinePos).toEqual(this.infos["test1inline.js"].offsets[0]);
@@ -253,7 +253,7 @@ define(function (require, exports, module) {
runs(function () {
var inlineWidget = EditorManager.getCurrentFullEditor().getInlineWidgets()[0];
- var inlinePos = inlineWidget.editors[0].getCursorPos();
+ var inlinePos = inlineWidget.editor.getCursorPos();
// verify cursor position in inline editor
expect(inlinePos).toEqual(this.infos["test1inline.js"].offsets[1]);
@@ -265,7 +265,7 @@ define(function (require, exports, module) {
runs(function () {
var inlineWidget = EditorManager.getCurrentFullEditor().getInlineWidgets()[0];
- var inlinePos = inlineWidget.editors[0].getCursorPos();
+ var inlinePos = inlineWidget.editor.getCursorPos();
// verify cursor position in inline editor
expect(inlinePos).toEqual(this.infos["test1inline.js"].offsets[2]);
diff --git a/src/extensions/default/QuickView/main.js b/src/extensions/default/QuickView/main.js
index 3c1fcd48766..4909384388f 100644
--- a/src/extensions/default/QuickView/main.js
+++ b/src/extensions/default/QuickView/main.js
@@ -562,11 +562,11 @@ define(function (require, exports, module) {
editor;
for (i = 0; i < inlines.length; i++) {
- var $inlineDiv = inlines[i].$editorsDiv, // see MultiRangeInlineEditor
- $otherDiv = inlines[i].$htmlContent;
+ var $inlineEditorRoot = inlines[i].editor && $(inlines[i].editor.getRootElement()), // see MultiRangeInlineEditor
+ $otherDiv = inlines[i].$htmlContent;
- if ($inlineDiv && divContainsMouse($inlineDiv, event)) {
- editor = inlines[i].editors[0];
+ if ($inlineEditorRoot && divContainsMouse($inlineEditorRoot, event)) {
+ editor = inlines[i].editor;
break;
} else if ($otherDiv && divContainsMouse($otherDiv, event)) {
// Mouse inside unsupported inline editor like Quick Docs or Color Editor
diff --git a/src/htmlContent/stylesheets-menu.html b/src/htmlContent/stylesheets-menu.html
new file mode 100644
index 00000000000..010d331a5d4
--- /dev/null
+++ b/src/htmlContent/stylesheets-menu.html
@@ -0,0 +1,9 @@
+