--- a/devtools/client/debugger/content/views/sources-view.js
+++ b/devtools/client/debugger/content/views/sources-view.js
@@ -694,17 +694,16 @@ SourcesView.prototype = Heritage.extend(
let bp = getBreakpoint(this.getState(), attachment);
let expr = (bp ? (bp.condition || "") : "");
let cbPanel = this._cbPanel;
// Update the conditional expression textbox. If no expression was
// previously set, revert to using an empty string by default.
this._cbTextbox.value = expr;
-
function openPopup() {
// Show the conditional expression panel. The popup arrow should be pointing
// at the line number node in the breakpoint item view.
cbPanel.hidden = false;
cbPanel.openPopup(breakpointItem.attachment.view.lineNumber,
BREAKPOINT_CONDITIONAL_POPUP_POSITION,
BREAKPOINT_CONDITIONAL_POPUP_OFFSET_X,
BREAKPOINT_CONDITIONAL_POPUP_OFFSET_Y);
--- a/devtools/client/inspector/inspector-search.js
+++ b/devtools/client/inspector/inspector-search.js
@@ -74,17 +74,17 @@ InspectorSearch.prototype = {
this.emit("search-cleared");
}
return;
}
let res = yield this.walker.search(query, { reverse });
// Value has changed since we started this request, we're done.
- if (query != this.searchBox.value) {
+ if (query !== this.searchBox.value) {
return;
}
if (res) {
this.inspector.selection.setNodeFront(res.node, "inspectorsearch");
this.searchBox.classList.remove("devtools-no-search-result");
res.query = query;
@@ -137,31 +137,29 @@ InspectorSearch.prototype = {
*/
function SelectorAutocompleter(inspector, inputNode) {
this.inspector = inspector;
this.searchBox = inputNode;
this.panelDoc = this.searchBox.ownerDocument;
this.showSuggestions = this.showSuggestions.bind(this);
this._onSearchKeypress = this._onSearchKeypress.bind(this);
- this._onListBoxKeypress = this._onListBoxKeypress.bind(this);
+ this._onSearchPopupClick = this._onSearchPopupClick.bind(this);
this._onMarkupMutation = this._onMarkupMutation.bind(this);
// Options for the AutocompletePopup.
let options = {
- panelId: "inspector-searchbox-panel",
- listBoxId: "searchbox-panel-listbox",
+ listId: "searchbox-panel-listbox",
autoSelect: true,
- position: "before_start",
- direction: "ltr",
+ position: "top",
theme: "auto",
- onClick: this._onListBoxKeypress,
- onKeypress: this._onListBoxKeypress
+ onClick: this._onSearchPopupClick,
};
- this.searchPopup = new AutocompletePopup(this.panelDoc, options);
+
+ this.searchPopup = new AutocompletePopup(inspector._toolbox, options);
this.searchBox.addEventListener("input", this.showSuggestions, true);
this.searchBox.addEventListener("keypress", this._onSearchKeypress, true);
this.inspector.on("markupmutation", this._onMarkupMutation);
// For testing, we need to be able to wait for the most recent node request
// to finish. Tests can watch this promise for that.
this._lastQuery = promise.resolve(null);
@@ -226,67 +224,67 @@ SelectorAutocompleter.prototype = {
subQuery = query.slice(0, i);
let [secondLastChar, lastChar] = subQuery.slice(-2);
switch (this._state) {
case null:
// This will happen only in the first iteration of the for loop.
lastChar = secondLastChar;
case this.States.TAG: // eslint-disable-line
- if (lastChar == ".") {
+ if (lastChar === ".") {
this._state = this.States.CLASS;
- } else if (lastChar == "#") {
+ } else if (lastChar === "#") {
this._state = this.States.ID;
- } else if (lastChar == "[") {
+ } else if (lastChar === "[") {
this._state = this.States.ATTRIBUTE;
} else {
this._state = this.States.TAG;
}
break;
case this.States.CLASS:
if (subQuery.match(/[\.]+[^\.]*$/)[0].length > 2) {
// Checks whether the subQuery has atleast one [a-zA-Z] after the
// '.'.
- if (lastChar == " " || lastChar == ">") {
+ if (lastChar === " " || lastChar === ">") {
this._state = this.States.TAG;
- } else if (lastChar == "#") {
+ } else if (lastChar === "#") {
this._state = this.States.ID;
- } else if (lastChar == "[") {
+ } else if (lastChar === "[") {
this._state = this.States.ATTRIBUTE;
} else {
this._state = this.States.CLASS;
}
}
break;
case this.States.ID:
if (subQuery.match(/[#]+[^#]*$/)[0].length > 2) {
// Checks whether the subQuery has atleast one [a-zA-Z] after the
// '#'.
- if (lastChar == " " || lastChar == ">") {
+ if (lastChar === " " || lastChar === ">") {
this._state = this.States.TAG;
- } else if (lastChar == ".") {
+ } else if (lastChar === ".") {
this._state = this.States.CLASS;
- } else if (lastChar == "[") {
+ } else if (lastChar === "[") {
this._state = this.States.ATTRIBUTE;
} else {
this._state = this.States.ID;
}
}
break;
case this.States.ATTRIBUTE:
- if (subQuery.match(/[\[][^\]]+[\]]/) != null) {
+ if (subQuery.match(/[\[][^\]]+[\]]/) !== null) {
// Checks whether the subQuery has at least one ']' after the '['.
- if (lastChar == " " || lastChar == ">") {
+ if (lastChar === " " || lastChar === ">") {
this._state = this.States.TAG;
- } else if (lastChar == ".") {
+ } else if (lastChar === ".") {
this._state = this.States.CLASS;
- } else if (lastChar == "#") {
+ } else if (lastChar === "#") {
this._state = this.States.ID;
} else {
this._state = this.States.ATTRIBUTE;
}
}
break;
}
}
@@ -306,135 +304,100 @@ SelectorAutocompleter.prototype = {
this.searchBox = null;
this.panelDoc = null;
},
/**
* Handles keypresses inside the input box.
*/
_onSearchKeypress: function (event) {
- let query = this.searchBox.value;
let popup = this.searchPopup;
switch (event.keyCode) {
case event.DOM_VK_RETURN:
case event.DOM_VK_TAB:
- if (popup.isOpen &&
- popup.getItemAtIndex(popup.itemCount - 1)
- .preLabel == query) {
- popup.selectedIndex = popup.itemCount - 1;
- this.searchBox.value = popup.selectedItem.label;
+ if (popup.isOpen) {
+ if (popup.selectedItem) {
+ this.searchBox.value = popup.selectedItem.label;
+ }
this.hidePopup();
- } else if (!popup.isOpen &&
- event.keyCode === event.DOM_VK_TAB) {
+ } else if (!popup.isOpen) {
// When tab is pressed with focus on searchbox and closed popup,
// do not prevent the default to avoid a keyboard trap and move focus
// to next/previous element.
this.emit("processing-done");
return;
}
break;
case event.DOM_VK_UP:
if (popup.isOpen && popup.itemCount > 0) {
- popup.focus();
- if (popup.selectedIndex == popup.itemCount - 1) {
- popup.selectedIndex =
- Math.max(0, popup.itemCount - 2);
+ if (popup.selectedIndex === 0) {
+ popup.selectedIndex = popup.itemCount - 1;
} else {
- popup.selectedIndex = popup.itemCount - 1;
+ popup.selectedIndex--;
}
this.searchBox.value = popup.selectedItem.label;
}
break;
case event.DOM_VK_DOWN:
if (popup.isOpen && popup.itemCount > 0) {
- popup.focus();
- popup.selectedIndex = 0;
+ if (popup.selectedIndex === popup.itemCount - 1) {
+ popup.selectedIndex = 0;
+ } else {
+ popup.selectedIndex++;
+ }
this.searchBox.value = popup.selectedItem.label;
}
break;
+ case event.DOM_VK_ESCAPE:
+ if (popup.isOpen) {
+ this.hidePopup();
+ }
+ break;
+
default:
return;
}
event.preventDefault();
event.stopPropagation();
this.emit("processing-done");
},
/**
- * Handles keypress and mouse click on the suggestions richlistbox.
+ * Handles click events from the autocomplete popup.
*/
- _onListBoxKeypress: function (event) {
- let popup = this.searchPopup;
-
- switch (event.keyCode || event.button) {
- case event.DOM_VK_RETURN:
- case event.DOM_VK_TAB:
- case 0:
- // left mouse button
- event.stopPropagation();
- event.preventDefault();
- this.searchBox.value = popup.selectedItem.label;
- this.searchBox.focus();
- this.hidePopup();
- break;
+ _onSearchPopupClick: function (event) {
+ let selectedItem = this.searchPopup.selectedItem;
+ if (selectedItem) {
+ this.searchBox.value = selectedItem.label;
+ }
+ this.hidePopup();
- case event.DOM_VK_UP:
- if (popup.selectedIndex == 0) {
- popup.selectedIndex = -1;
- event.stopPropagation();
- event.preventDefault();
- this.searchBox.focus();
- } else {
- let index = popup.selectedIndex;
- this.searchBox.value = popup.getItemAtIndex(index - 1).label;
- }
- break;
-
- case event.DOM_VK_DOWN:
- if (popup.selectedIndex == popup.itemCount - 1) {
- popup.selectedIndex = -1;
- event.stopPropagation();
- event.preventDefault();
- this.searchBox.focus();
- } else {
- let index = popup.selectedIndex;
- this.searchBox.value = popup.getItemAtIndex(index + 1).label;
- }
- break;
-
- case event.DOM_VK_BACK_SPACE:
- event.stopPropagation();
- event.preventDefault();
- this.searchBox.focus();
- if (this.searchBox.selectionStart > 0) {
- this.searchBox.value = this.searchBox.value.substring(0,
- this.searchBox.selectionStart - 1);
- }
- this.hidePopup();
- break;
- }
- this.emit("processing-done");
+ event.preventDefault();
+ event.stopPropagation();
},
/**
* Reset previous search results on markup-mutations to make sure we search
* again after nodes have been added/removed/changed.
*/
_onMarkupMutation: function () {
this._searchResults = null;
this._lastSearched = null;
},
/**
* Populates the suggestions list and show the suggestion popup.
+ *
+ * @return {Promise} promise that will resolve when the autocomplete popup is fully
+ * displayed or hidden.
*/
_showPopup: function (list, firstPart, popupState) {
let total = 0;
let query = this.searchBox.value;
let items = [];
for (let [value, , state] of list) {
if (query.match(/[\s>+]$/)) {
@@ -468,31 +431,34 @@ SelectorAutocompleter.prototype = {
item.preLabel = "#" + item.preLabel;
}
items.unshift(item);
if (++total > MAX_SUGGESTIONS - 1) {
break;
}
}
+
if (total > 0) {
+ let onPopupOpened = this.searchPopup.once("popup-opened");
this.searchPopup.setItems(items);
this.searchPopup.openPopup(this.searchBox);
- } else {
- this.hidePopup();
+ return onPopupOpened;
}
+
+ return this.hidePopup();
},
/**
* Hide the suggestion popup if necessary.
*/
hidePopup: function () {
- if (this.searchPopup.isOpen) {
- this.searchPopup.hidePopup();
- }
+ let onPopupClosed = this.searchPopup.once("popup-closed");
+ this.searchPopup.hidePopup();
+ return onPopupClosed;
},
/**
* Suggests classes,ids and tags based on the user input as user types in the
* searchbox.
*/
showSuggestions: function () {
let query = this.searchBox.value;
@@ -529,30 +495,32 @@ SelectorAutocompleter.prototype = {
let suggestionsPromise = this.walker.getSuggestionsForQuery(
query, firstPart, state);
this._lastQuery = suggestionsPromise.then(result => {
this.emit("processing-done");
if (result.query !== query) {
// This means that this response is for a previous request and the user
// as since typed something extra leading to a new request.
- return;
+ return promise.resolve(null);
}
if (state === this.States.CLASS) {
firstPart = "." + firstPart;
} else if (state === this.States.ID) {
firstPart = "#" + firstPart;
}
// If there is a single tag match and it's what the user typed, then
// don't need to show a popup.
if (result.suggestions.length === 1 &&
result.suggestions[0][0] === firstPart) {
result.suggestions = [];
}
- this._showPopup(result.suggestions, firstPart, state);
+ // Wait for the autocomplete-popup to fire its popup-opened event, to make sure
+ // the autoSelect item has been selected.
+ return this._showPopup(result.suggestions, firstPart, state);
});
return;
}
};
--- a/devtools/client/inspector/markup/markup.js
+++ b/devtools/client/inspector/markup/markup.js
@@ -18,17 +18,16 @@ const NEW_SELECTION_HIGHLIGHTER_TIMER =
const DRAG_DROP_AUTOSCROLL_EDGE_MAX_DISTANCE = 50;
const DRAG_DROP_AUTOSCROLL_EDGE_RATIO = 0.1;
const DRAG_DROP_MIN_AUTOSCROLL_SPEED = 2;
const DRAG_DROP_MAX_AUTOSCROLL_SPEED = 8;
const DRAG_DROP_MIN_INITIAL_DISTANCE = 10;
const DRAG_DROP_HEIGHT_TO_SPEED = 500;
const DRAG_DROP_HEIGHT_TO_SPEED_MIN = 0.5;
const DRAG_DROP_HEIGHT_TO_SPEED_MAX = 1;
-const AUTOCOMPLETE_POPUP_PANEL_ID = "markupview_autoCompletePopup";
const ATTR_COLLAPSE_ENABLED_PREF = "devtools.markup.collapseAttributes";
const ATTR_COLLAPSE_LENGTH_PREF = "devtools.markup.collapseAttributeLength";
const PREVIEW_MAX_DIM_PREF = "devtools.inspector.imagePreviewTooltipSize";
// Contains only void (without end tag) HTML elements
const HTML_VOID_ELEMENTS = [ "area", "base", "br", "col", "command", "embed",
"hr", "img", "input", "keygen", "link", "meta", "param", "source",
"track", "wbr" ];
@@ -104,22 +103,19 @@ function MarkupView(inspector, frame, co
Services.prefs.getBoolPref(ATTR_COLLAPSE_ENABLED_PREF);
this.collapseAttributeLength =
Services.prefs.getIntPref(ATTR_COLLAPSE_LENGTH_PREF);
// Creating the popup to be used to show CSS suggestions.
let options = {
autoSelect: true,
theme: "auto",
- // panelId option prevents the markupView autocomplete popup from
- // sharing XUL elements with other views, such as ruleView (see Bug 1191093)
- panelId: AUTOCOMPLETE_POPUP_PANEL_ID
};
- this.popup = new AutocompletePopup(this.doc.defaultView.parent.document,
- options);
+
+ this.popup = new AutocompletePopup(inspector._toolbox, options);
this.undo = new UndoStack();
this.undo.installController(controllerWindow);
this._containers = new Map();
// Binding functions that need to be called in scope.
this._mutationObserver = this._mutationObserver.bind(this);
--- a/devtools/client/inspector/rules/rules.js
+++ b/devtools/client/inspector/rules/rules.js
@@ -217,17 +217,17 @@ function CssRuleView(inspector, document
this.showUserAgentStyles = Services.prefs.getBoolPref(PREF_UA_STYLES);
this.enableMdnDocsTooltip =
Services.prefs.getBoolPref(PREF_ENABLE_MDN_DOCS_TOOLTIP);
let options = {
autoSelect: true,
theme: "auto"
};
- this.popup = new AutocompletePopup(this.styleDocument, options);
+ this.popup = new AutocompletePopup(inspector._toolbox, options);
this._showEmpty();
this._contextmenu = new StyleInspectorMenu(this, { isRuleView: true });
// Add the tooltips and highlighters to the view
this.tooltips = new overlays.TooltipsOverlay(this);
this.tooltips.addToView();
--- a/devtools/client/shared/autocomplete-popup.js
+++ b/devtools/client/shared/autocomplete-popup.js
@@ -1,144 +1,115 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
-const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const HTML_NS = "http://www.w3.org/1999/xhtml";
const Services = require("Services");
const {gDevTools} = require("devtools/client/framework/devtools");
-const events = require("devtools/shared/event-emitter");
+const {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip");
+const EventEmitter = require("devtools/shared/event-emitter");
+let itemIdCounter = 0;
/**
* Autocomplete popup UI implementation.
*
* @constructor
- * @param {nsIDOMDocument} document
- * The document you want the popup attached to.
+ * @param {Toolbox} toolbox
+ * The devtools toolbox required to instanciate the HTMLTooltip.
* @param {Object} options
* An object consiting any of the following options:
- * - panelId {String} The id for the popup panel.
- * - listBoxId {String} The id for the richlistbox inside the panel.
- * - position {String} The position for the popup panel.
- * - theme {String} String related to the theme of the popup.
+ * - listId {String} The id for the list <LI> element.
+ * - position {String} The position for the tooltip ("top" or "bottom").
+ * - theme {String} String related to the theme of the popup
* - autoSelect {Boolean} Boolean to allow the first entry of the popup
- * panel to be automatically selected when the popup shows.
- * - direction {String} The direction of the text in the panel. rtl / ltr
- * - onSelect {String} The select event handler for the richlistbox
- * - onClick {String} The click event handler for the richlistbox.
- * - onKeypress {String} The keypress event handler for richlistitems.
+ * panel to be automatically selected when the popup shows.
+ * - onSelect {String} Callback called when the selected index is updated.
+ * - onClick {String} Callback called when the autocomplete popup receives a click
+ * event. The selectedIndex will already be updated if need be.
*/
-function AutocompletePopup(document, options = {}) {
- this._document = document;
+function AutocompletePopup(toolbox, options = {}) {
+ EventEmitter.decorate(this);
+
+ this._document = toolbox.doc;
this.autoSelect = options.autoSelect || false;
- this.position = options.position || "after_start";
- this.direction = options.direction || "ltr";
+ this.position = options.position || "bottom";
+ let theme = options.theme || "dark";
this.onSelectCallback = options.onSelect;
this.onClickCallback = options.onClick;
- this.onKeypressCallback = options.onKeypress;
- let id = options.panelId || "devtools_autoCompletePopup";
- let theme = options.theme || "dark";
// If theme is auto, use the devtools.theme pref
- if (theme == "auto") {
+ if (theme === "auto") {
theme = Services.prefs.getCharPref("devtools.theme");
this.autoThemeEnabled = true;
// Setup theme change listener.
this._handleThemeChange = this._handleThemeChange.bind(this);
gDevTools.on("pref-changed", this._handleThemeChange);
}
- // Reuse the existing popup elements.
- this._panel = this._document.getElementById(id);
- if (!this._panel) {
- this._panel = this._document.createElementNS(XUL_NS, "panel");
- this._panel.setAttribute("id", id);
- this._panel.className = "devtools-autocomplete-popup devtools-monospace "
- + theme + "-theme";
-
- this._panel.setAttribute("noautofocus", "true");
- this._panel.setAttribute("level", "top");
- if (!options.onKeypress) {
- this._panel.setAttribute("ignorekeys", "true");
- }
- // Stop this appearing as an alert to accessibility.
- this._panel.setAttribute("role", "presentation");
- let mainPopupSet = this._document.getElementById("mainPopupSet");
- if (mainPopupSet) {
- mainPopupSet.appendChild(this._panel);
- } else {
- this._document.documentElement.appendChild(this._panel);
- }
- } else {
- this._list = this._panel.firstChild;
- }
+ // Create HTMLTooltip instance
+ this._tooltip = new HTMLTooltip(toolbox);
+ this._tooltip.panel.classList.add(
+ "devtools-autocomplete-popup",
+ "devtools-monospace",
+ theme + "-theme");
+ // Stop this appearing as an alert to accessibility.
+ this._tooltip.panel.setAttribute("role", "presentation");
- if (!this._list) {
- this._list = this._document.createElementNS(XUL_NS, "richlistbox");
- this._panel.appendChild(this._list);
-
- // Open and hide the panel, so we initialize the API of the richlistbox.
- this._panel.openPopup(null, this.position, 0, 0);
- this._panel.hidePopup();
- }
-
+ this._list = this._document.createElementNS(HTML_NS, "ul");
this._list.setAttribute("flex", "1");
this._list.setAttribute("seltype", "single");
- if (options.listBoxId) {
- this._list.setAttribute("id", options.listBoxId);
+ if (options.listId) {
+ this._list.setAttribute("id", options.listId);
}
this._list.className = "devtools-autocomplete-listbox " + theme + "-theme";
- this.onSelect = this.onSelect.bind(this);
+ this._tooltip.setContent(this._list);
+
this.onClick = this.onClick.bind(this);
- this.onKeypress = this.onKeypress.bind(this);
- this._list.addEventListener("select", this.onSelect, false);
this._list.addEventListener("click", this.onClick, false);
- this._list.addEventListener("keypress", this.onKeypress, false);
- this._itemIdCounter = 0;
+ // Array of raw autocomplete items
+ this.items = [];
+ // Map of autocompleteItem to HTMLElement
+ this.elements = new WeakMap();
- events.decorate(this);
+ this.selectedIndex = -1;
}
exports.AutocompletePopup = AutocompletePopup;
AutocompletePopup.prototype = {
_document: null,
- _panel: null,
+ _tooltip: null,
_list: null,
- __scrollbarWidth: null,
- // Event handlers.
onSelect: function (e) {
- this.emit("popup-select");
if (this.onSelectCallback) {
this.onSelectCallback(e);
}
},
onClick: function (e) {
+ let item = e.target.closest(".autocomplete-item");
+ if (item && typeof item.dataset.index !== "undefined") {
+ this.selectedIndex = parseInt(item.dataset.index, 10);
+ }
+
this.emit("popup-click");
if (this.onClickCallback) {
this.onClickCallback(e);
}
},
- onKeypress: function (e) {
- this.emit("popup-keypress");
- if (this.onKeypressCallback) {
- this.onKeypressCallback(e);
- }
- },
-
/**
* Open the autocomplete popup panel.
*
* @param {nsIDOMNode} anchor
* Optional node to anchor the panel to.
* @param {Number} xOffset
* Horizontal offset in pixels from the left of the node to the left
* of the popup.
@@ -146,247 +117,278 @@ AutocompletePopup.prototype = {
* Vertical offset in pixels from the top of the node to the starting
* of the popup.
* @param {Number} index
* The position of item to select.
*/
openPopup: function (anchor, xOffset = 0, yOffset = 0, index) {
this.__maxLabelLength = -1;
this._updateSize();
- this._panel.openPopup(anchor, this.position, xOffset, yOffset);
+ this._tooltip.show(anchor, {
+ x: xOffset,
+ y: yOffset,
+ position: this.position,
+ });
- if (this.autoSelect) {
- this.selectItemAtIndex(index);
- }
+ this._tooltip.once("shown", () => {
+ if (this.autoSelect) {
+ this.selectItemAtIndex(index);
+ }
- this.emit("popup-opened");
+ this.emit("popup-opened");
+ });
},
/**
* Select item at the provided index.
*
* @param {Number} index
* The position of the item to select.
*/
selectItemAtIndex: function (index) {
- if (typeof index != "number") {
+ if (typeof index !== "number") {
// If no index was provided, select the item closest to the input.
- let isAboveInput = this.position.includes("before");
+ let isAboveInput = this.position === "top";
index = isAboveInput ? this.itemCount - 1 : 0;
}
this.selectedIndex = index;
- this._list.ensureIndexIsVisible(this._list.selectedIndex);
},
/**
* Hide the autocomplete popup panel.
*/
hidePopup: function () {
// Return accessibility focus to the input.
- this._document.activeElement.removeAttribute("aria-activedescendant");
- this._panel.hidePopup();
+ this._findActiveElement().removeAttribute("aria-activedescendant");
+ this._tooltip.once("hidden", () => {
+ this.emit("popup-closed");
+ });
+ this._tooltip.hide();
},
/**
* Check if the autocomplete popup is open.
*/
get isOpen() {
- return this._panel &&
- (this._panel.state == "open" || this._panel.state == "showing");
+ return this._tooltip && this._tooltip.isVisible();
},
/**
* Destroy the object instance. Please note that the panel DOM elements remain
* in the DOM, because they might still be in use by other instances of the
* same code. It is the responsability of the client code to perform DOM
* cleanup.
*/
destroy: function () {
if (this.isOpen) {
this.hidePopup();
}
- this._list.removeEventListener("select", this.onSelect, false);
this._list.removeEventListener("click", this.onClick, false);
- this._list.removeEventListener("keypress", this.onKeypress, false);
if (this.autoThemeEnabled) {
gDevTools.off("pref-changed", this._handleThemeChange);
}
this._list.remove();
- this._panel.remove();
+ this._tooltip.destroy();
this._document = null;
this._list = null;
- this._panel = null;
+ this._tooltip = null;
},
/**
* Get the autocomplete items array.
*
* @param {Number} index
* The index of the item what is wanted.
*
* @return {Object} The autocomplete item at index index.
*/
getItemAtIndex: function (index) {
- return this._list.getItemAtIndex(index)._autocompleteItem;
+ return this.items[index];
},
/**
* Get the autocomplete items array.
*
* @return {Array} The array of autocomplete items.
*/
getItems: function () {
- let items = [];
-
- Array.forEach(this._list.childNodes, function (item) {
- items.push(item._autocompleteItem);
- });
-
- return items;
+ // Return a copy of the array to avoid side effects from the caller code.
+ return this.items.slice(0);
},
/**
* Set the autocomplete items list, in one go.
*
* @param {Array} items
* The list of items you want displayed in the popup list.
* @param {Number} index
* The position of the item to select.
*/
setItems: function (items, index) {
this.clearItems();
items.forEach(this.appendItem, this);
- // Make sure that the new content is properly fitted by the XUL richlistbox.
- if (this.isOpen) {
- if (this.autoSelect) {
- this.selectItemAtIndex(index);
- }
- this._updateSize();
+ if (this.isOpen && this.autoSelect) {
+ this.selectItemAtIndex(index);
}
},
__maxLabelLength: -1,
get _maxLabelLength() {
- if (this.__maxLabelLength != -1) {
+ if (this.__maxLabelLength !== -1) {
return this.__maxLabelLength;
}
let max = 0;
- for (let i = 0; i < this._list.childNodes.length; i++) {
- let item = this._list.childNodes[i]._autocompleteItem;
- let str = item.label;
- if (item.count) {
- str += (item.count + "");
+ for (let {label, count} of this.items) {
+ if (count) {
+ label += count + "";
}
- max = Math.max(str.length, max);
+ max = Math.max(label.length, max);
}
this.__maxLabelLength = max;
return this.__maxLabelLength;
},
/**
* Update the panel size to fit the content.
*/
_updateSize: function () {
- if (!this._panel) {
+ if (!this._tooltip) {
return;
}
this._list.style.width = (this._maxLabelLength + 3) + "ch";
- this._list.ensureIndexIsVisible(this._list.selectedIndex);
+ let selectedItem = this.selectedItem;
+ if (selectedItem) {
+ this._scrollElementIntoViewIfNeeded(this.elements.get(selectedItem));
+ }
+ },
+
+ _scrollElementIntoViewIfNeeded: function (element) {
+ let quads = element.getBoxQuads({relativeTo: this._tooltip.panel});
+ if (!quads || !quads[0]) {
+ return;
+ }
+
+ let {top, height} = quads[0].bounds;
+ let containerHeight = this._tooltip.panel.getBoundingClientRect().height;
+ if (top < 0) {
+ // Element is above container.
+ element.scrollIntoView(true);
+ } else if ((top + height) > containerHeight) {
+ // Element is beloew container.
+ element.scrollIntoView(false);
+ }
},
/**
* Update accessibility appropriately when the selected item is changed.
*/
_updateAriaActiveDescendant: function () {
- if (!this._list.selectedItem) {
- // Return accessibility focus to the input.
- this._document.activeElement.removeAttribute("aria-activedescendant");
+ let activeElement = this._findActiveElement();
+ if (!activeElement) {
return;
}
- // Focus this for accessibility so users know about the selected item.
- this._document.activeElement.setAttribute("aria-activedescendant",
- this._list.selectedItem.id);
+
+ if (this.selectedItem) {
+ // Focus this for accessibility so users know about the selected item.
+ let selectedElement = this.elements.get(this.selectedItem);
+ activeElement.setAttribute("aria-activedescendant", selectedElement.id);
+ } else {
+ // Return accessibility focus to the input.
+ activeElement.removeAttribute("aria-activedescendant");
+ }
+ },
+
+ /**
+ * Find the active element if it belongs in a child document of the autocomplete
+ * document.
+ */
+ _findActiveElement: function () {
+ let activeElement = this._document.activeElement;
+ while (activeElement && activeElement.contentDocument) {
+ activeElement = activeElement.contentDocument.activeElement;
+ }
+ return activeElement;
},
/**
* Clear all the items from the autocomplete list.
*/
clearItems: function () {
// Reset the selectedIndex to -1 before clearing the list
this.selectedIndex = -1;
-
- while (this._list.hasChildNodes()) {
- this._list.removeChild(this._list.firstChild);
- }
-
+ this._list.innerHTML = "";
this.__maxLabelLength = -1;
-
- // Reset the panel and list dimensions. New dimensions are calculated when
- // a new set of items is added to the autocomplete popup.
- this._list.width = "";
- this._list.style.width = "";
- this._list.height = "";
- this._panel.width = "";
- this._panel.height = "";
- this._panel.top = "";
- this._panel.left = "";
+ this.items = [];
+ this.elements = new WeakMap();
},
/**
* Getter for the index of the selected item.
*
* @type {Number}
*/
get selectedIndex() {
- return this._list.selectedIndex;
+ return this._selectedIndex;
},
/**
* Setter for the selected index.
*
* @param {Number} index
* The number (index) of the item you want to select in the list.
*/
set selectedIndex(index) {
- this._list.selectedIndex = index;
- if (this.isOpen && this._list.ensureIndexIsVisible) {
- this._list.ensureIndexIsVisible(this._list.selectedIndex);
+ let previousSelected = this._list.querySelector(".autocomplete-selected");
+ if (previousSelected) {
+ previousSelected.classList.remove("autocomplete-selected");
}
+
+ let item = this.items[index];
+ if (this.isOpen && item) {
+ let element = this.elements.get(item);
+
+ element.classList.add("autocomplete-selected");
+ this._scrollElementIntoViewIfNeeded(element);
+ }
+ this._selectedIndex = index;
this._updateAriaActiveDescendant();
+
+ if (this.isOpen && item && this.onSelectCallback) {
+ // Call the user-defined select callback if defined.
+ this.onSelectCallback();
+ }
},
/**
* Getter for the selected item.
* @type Object
*/
get selectedItem() {
- return this._list.selectedItem ?
- this._list.selectedItem._autocompleteItem : null;
+ return this.items[this._selectedIndex];
},
/**
* Setter for the selected item.
*
* @param {Object} item
* The object you want selected in the list.
*/
set selectedItem(item) {
- this._list.selectedItem = this._findListItem(item);
- if (this.isOpen) {
- this._list.ensureIndexIsVisible(this._list.selectedIndex);
+ let index = this.items.indexOf(item);
+ if (index !== -1 && this.isOpen) {
+ this.selectedIndex = index;
}
- this._updateAriaActiveDescendant();
},
/**
* Append an item into the autocomplete list.
*
* @param {Object} item
* The item you want appended to the list.
* The item object can have the following properties:
@@ -396,182 +398,186 @@ AutocompletePopup.prototype = {
* present text in the input box, and label is the text
* that will be auto completed. When this property is
* present, |preLabel.length| starting characters will be
* removed from label.
* - count {Number} [Optional] The number to represent the count of
* autocompleted label.
*/
appendItem: function (item) {
- let listItem = this._document.createElementNS(XUL_NS, "richlistitem");
+ let listItem = this._document.createElementNS(HTML_NS, "li");
// Items must have an id for accessibility.
- listItem.id = this._panel.id + "_item_" + this._itemIdCounter++;
+ listItem.setAttribute("id", "autocomplete-item-" + itemIdCounter++);
+ listItem.className = "autocomplete-item";
+ listItem.setAttribute("data-index", this.items.length);
if (this.direction) {
listItem.setAttribute("dir", this.direction);
}
- let label = this._document.createElementNS(XUL_NS, "label");
- label.setAttribute("value", item.label);
- label.setAttribute("class", "autocomplete-value");
+ let label = this._document.createElementNS(HTML_NS, "span");
+ label.textContent = item.label;
+ label.className = "autocomplete-value";
if (item.preLabel) {
- let preDesc = this._document.createElementNS(XUL_NS, "label");
- preDesc.setAttribute("value", item.preLabel);
- preDesc.setAttribute("class", "initial-value");
+ let preDesc = this._document.createElementNS(HTML_NS, "span");
+ preDesc.textContent = item.preLabel;
+ preDesc.className = "initial-value";
listItem.appendChild(preDesc);
- label.setAttribute("value", item.label.slice(item.preLabel.length));
+ label.textContent = item.label.slice(item.preLabel.length);
}
listItem.appendChild(label);
if (item.count && item.count > 1) {
- let countDesc = this._document.createElementNS(XUL_NS, "label");
- countDesc.setAttribute("value", item.count);
+ let countDesc = this._document.createElementNS(HTML_NS, "span");
+ countDesc.textContent = item.count;
countDesc.setAttribute("flex", "1");
- countDesc.setAttribute("class", "autocomplete-count");
+ countDesc.className = "autocomplete-count";
listItem.appendChild(countDesc);
}
- listItem._autocompleteItem = item;
this._list.appendChild(listItem);
- },
-
- /**
- * Find the richlistitem element that belongs to an item.
- *
- * @private
- *
- * @param {Object} item
- * The object you want found in the list.
- *
- * @return {nsIDOMNode} The nsIDOMNode that belongs to the given item object.
- * This node is the richlistitem element. Can be null.
- */
- _findListItem: function (item) {
- for (let i = 0; i < this._list.childNodes.length; i++) {
- let child = this._list.childNodes[i];
- if (child._autocompleteItem == item) {
- return child;
- }
- }
- return null;
+ this.items.push(item);
+ this.elements.set(item, listItem);
},
/**
* Remove an item from the popup list.
*
* @param {Object} item
* The item you want removed.
*/
removeItem: function (item) {
- let listItem = this._findListItem(item);
- if (!listItem) {
- throw new Error("Item not found!");
+ if (!this.items.includes(item)) {
+ return;
}
- this._list.removeChild(listItem);
+
+ let itemIndex = this.items.indexOf(item);
+ let selectedIndex = this.selectedIndex;
+
+ // Remove autocomplete item.
+ this.items.splice(itemIndex, 1);
+
+ // Remove corresponding DOM element from the elements WeakMap and from the DOM.
+ let elementToRemove = this.elements.get(item);
+ this.elements.delete(elementToRemove);
+ elementToRemove.remove();
+
+ if (itemIndex <= selectedIndex) {
+ // If the removed item index was before or equal to the selected index, shift the
+ // selected index by 1.
+ this.selectedIndex = Math.max(0, selectedIndex - 1);
+ }
},
/**
* Getter for the number of items in the popup.
* @type {Number}
*/
get itemCount() {
- return this._list.childNodes.length;
+ return this.items.length;
},
/**
* Getter for the height of each item in the list.
*
* @type {Number}
*/
- get _itemHeight() {
- return this._list.selectedItem.clientHeight;
+ get _itemsPerPane() {
+ if (this.items.length) {
+ let listHeight = this._tooltip.panel.clientHeight;
+ let element = this.elements.get(this.items[0]);
+ let elementHeight = element.getBoundingClientRect().height;
+ return Math.floor(listHeight / elementHeight);
+ }
+ return 0;
},
/**
* Select the next item in the list.
*
* @return {Object}
* The newly selected item object.
*/
selectNextItem: function () {
- if (this.selectedIndex < (this.itemCount - 1)) {
+ if (this.selectedIndex < (this.items.length - 1)) {
this.selectedIndex++;
} else {
this.selectedIndex = 0;
}
-
return this.selectedItem;
},
/**
* Select the previous item in the list.
*
* @return {Object}
* The newly-selected item object.
*/
selectPreviousItem: function () {
if (this.selectedIndex > 0) {
this.selectedIndex--;
} else {
- this.selectedIndex = this.itemCount - 1;
+ this.selectedIndex = this.items.length - 1;
}
return this.selectedItem;
},
/**
* Select the top-most item in the next page of items or
* the last item in the list.
*
* @return {Object}
* The newly-selected item object.
*/
selectNextPageItem: function () {
- let itemsPerPane = Math.floor(this._list.scrollHeight / this._itemHeight);
- let nextPageIndex = this.selectedIndex + itemsPerPane + 1;
- this.selectedIndex = nextPageIndex > this.itemCount - 1 ?
- this.itemCount - 1 : nextPageIndex;
-
+ let nextPageIndex = this.selectedIndex + this._itemsPerPane + 1;
+ this.selectedIndex = Math.min(nextPageIndex, this.itemCount - 1);
return this.selectedItem;
},
/**
* Select the bottom-most item in the previous page of items,
* or the first item in the list.
*
* @return {Object}
* The newly-selected item object.
*/
selectPreviousPageItem: function () {
- let itemsPerPane = Math.floor(this._list.scrollHeight / this._itemHeight);
- let prevPageIndex = this.selectedIndex - itemsPerPane - 1;
- this.selectedIndex = prevPageIndex < 0 ? 0 : prevPageIndex;
-
+ let prevPageIndex = this.selectedIndex - this._itemsPerPane - 1;
+ this.selectedIndex = Math.max(prevPageIndex, 0);
return this.selectedItem;
},
/**
- * Focuses the richlistbox.
- */
- focus: function () {
- this._list.focus();
- },
-
- /**
* Manages theme switching for the popup based on the devtools.theme pref.
*
* @private
*
* @param {String} event
* The name of the event. In this case, "pref-changed".
* @param {Object} data
* An object passed by the emitter of the event. In this case, the
* object consists of three properties:
* - pref {String} The name of the preference that was modified.
* - newValue {Object} The new value of the preference.
* - oldValue {Object} The old value of the preference.
*/
_handleThemeChange: function (event, data) {
- if (data.pref == "devtools.theme") {
- this._panel.classList.toggle(data.oldValue + "-theme", false);
- this._panel.classList.toggle(data.newValue + "-theme", true);
+ if (data.pref === "devtools.theme") {
+ this._tooltip.panel.classList.toggle(data.oldValue + "-theme", false);
+ this._tooltip.panel.classList.toggle(data.newValue + "-theme", true);
this._list.classList.toggle(data.oldValue + "-theme", false);
this._list.classList.toggle(data.newValue + "-theme", true);
}
},
+
+ /**
+ * Used by tests.
+ */
+ get _panel() {
+ return this._tooltip.panel;
+ },
+
+ /**
+ * Used by tests.
+ */
+ get _window() {
+ return this._document.defaultView;
+ },
};
--- a/devtools/client/shared/inplace-editor.js
+++ b/devtools/client/shared/inplace-editor.js
@@ -28,17 +28,16 @@ const Services = require("Services");
const HTML_NS = "http://www.w3.org/1999/xhtml";
const CONTENT_TYPES = {
PLAIN_TEXT: 0,
CSS_VALUE: 1,
CSS_MIXED: 2,
CSS_PROPERTY: 3,
};
-const AUTOCOMPLETE_POPUP_CLASSNAME = "inplace-editor-autocomplete-popup";
// The limit of 500 autocomplete suggestions should not be reached but is kept
// for safety.
const MAX_POPUP_ENTRIES = 500;
const FOCUS_FORWARD = Ci.nsIFocusManager.MOVEFOCUS_FORWARD;
const FOCUS_BACKWARD = Ci.nsIFocusManager.MOVEFOCUS_BACKWARD;
@@ -984,23 +983,23 @@ InplaceEditor.prototype = {
_onAutocompletePopupClick: function () {
this._acceptPopupSuggestion();
},
_acceptPopupSuggestion: function () {
let label, preLabel;
if (this._selectedIndex === undefined) {
- ({label, preLabel} =
- this.popup.getItemAtIndex(this.popup.selectedIndex));
+ ({label, preLabel} = this.popup.getItemAtIndex(this.popup.selectedIndex));
} else {
({label, preLabel} = this.popup.getItemAtIndex(this._selectedIndex));
}
let input = this.input;
+
let pre = "";
// CSS_MIXED needs special treatment here to make it so that
// multiple presses of tab will cycle through completions, but
// without selecting the completed text. However, this same
// special treatment will do the wrong thing for other editing
// styles.
if (input.selectionStart < input.selectionEnd ||
@@ -1016,23 +1015,23 @@ InplaceEditor.prototype = {
let toComplete = item.label.slice(item.preLabel.length);
input.value = pre + toComplete + post;
input.setSelectionRange(pre.length + toComplete.length,
pre.length + toComplete.length);
this._updateSize();
// Wait for the popup to hide and then focus input async otherwise it does
// not work.
let onPopupHidden = () => {
- this.popup._panel.removeEventListener("popuphidden", onPopupHidden);
+ this.popup.off("popup-closed", onPopupHidden);
this.doc.defaultView.setTimeout(()=> {
input.focus();
this.emit("after-suggest");
}, 0);
};
- this.popup._panel.addEventListener("popuphidden", onPopupHidden);
+ this.popup.on("popup-closed", onPopupHidden);
this._hideAutocompletePopup();
},
/**
* Handle the input field's keypress event.
*/
_onKeyPress: function (event) {
let prevent = false;
@@ -1169,27 +1168,25 @@ InplaceEditor.prototype = {
*
* @param {Number} offset
* X-offset relative to the input starting edge.
* @param {Number} selectedIndex
* The index of the item that should be selected. Use -1 to have no
* item selected.
*/
_openAutocompletePopup: function (offset, selectedIndex) {
- this.popup._panel.classList.add(AUTOCOMPLETE_POPUP_CLASSNAME);
this.popup.on("popup-click", this._onAutocompletePopupClick);
this.popup.openPopup(this.input, offset, 0, selectedIndex);
},
/**
* Remove the custom classname and click handler and close the autocomplete
* popup.
*/
_hideAutocompletePopup: function () {
- this.popup._panel.classList.remove(AUTOCOMPLETE_POPUP_CLASSNAME);
this.popup.off("popup-click", this._onAutocompletePopupClick);
this.popup.hidePopup();
},
/**
* Get the increment/decrement step to use for the provided key event.
*/
_getIncrement: function (event) {
--- a/devtools/client/shared/widgets/HTMLTooltip.js
+++ b/devtools/client/shared/widgets/HTMLTooltip.js
@@ -415,16 +415,17 @@ HTMLTooltip.prototype = {
/**
* Hide the current tooltip. The event "hidden" will be fired when the tooltip
* is hidden.
*/
hide: Task.async(function* () {
this.doc.defaultView.clearTimeout(this.attachEventsTimer);
if (!this.isVisible()) {
+ this.emit("hidden");
return;
}
this.topWindow.removeEventListener("click", this._onClick, true);
this.container.classList.remove("tooltip-visible");
if (this.useXulWrapper) {
yield this._hideXulWrapper();
}
@@ -495,17 +496,17 @@ HTMLTooltip.prototype = {
let win = node.ownerDocument.defaultView;
// Check if the tooltip panel contains the node if they live in the same document.
if (win === tooltipWindow) {
return this.panel.contains(node);
}
// Check if the node window is in the tooltip container.
- while (win.parent && win.parent != win) {
+ while (win.parent && win.parent !== win) {
if (win.parent === tooltipWindow) {
// If the parent window is the tooltip window, check if the tooltip contains
// the current frame element.
return this.panel.contains(win.frameElement);
}
win = win.parent;
}
--- a/devtools/client/sourceeditor/autocomplete.js
+++ b/devtools/client/sourceeditor/autocomplete.js
@@ -1,31 +1,25 @@
/* vim:set ts=2 sw=2 sts=2 et tw=80:
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
-const { Cu } = require("chrome");
-const CSSCompleter =
- require("devtools/client/sourceeditor/css-autocompleter");
-const { AutocompletePopup } =
- require("devtools/client/shared/autocomplete-popup");
+const CSSCompleter = require("devtools/client/sourceeditor/css-autocompleter");
+const { AutocompletePopup } = require("devtools/client/shared/autocomplete-popup");
const CM_TERN_SCRIPTS = [
"chrome://devtools/content/sourceeditor/codemirror/addon/tern/tern.js",
"chrome://devtools/content/sourceeditor/codemirror/addon/hint/show-hint.js"
];
const autocompleteMap = new WeakMap();
-// A simple way to give each popup its own panelId.
-let autocompleteCounter = 0;
-
/**
* Prepares an editor instance for autocompletion.
*/
function initializeAutoCompletion(ctx, options = {}) {
let { cm, ed, Editor } = ctx;
if (autocompleteMap.has(ed)) {
return;
}
@@ -119,33 +113,31 @@ function initializeAutoCompletion(ctx, o
return false;
}
if (!autocompleteState.suggestionInsertedOnce && popup.selectedItem) {
autocompleteMap.get(ed).insertingSuggestion = true;
insertPopupItem(ed, popup.selectedItem);
}
+ popup.once("popup-closed", () => {
+ // This event is used in tests.
+ ed.emit("popup-hidden");
+ });
popup.hidePopup();
- // This event is used in tests.
- ed.emit("popup-hidden");
return true;
}
// Give each popup a new name to avoid sharing the elements.
- let panelId = "devtools_sourceEditorCompletePopup" + autocompleteCounter;
- ++autocompleteCounter;
- let popup = new AutocompletePopup(win.parent.document, {
- position: "after_start",
- fixedWidth: true,
+ let popup = new AutocompletePopup({ doc: win.parent.document }, {
+ position: "bottom",
theme: "auto",
autoSelect: true,
- onClick: insertSelectedPopupItem,
- panelId: panelId
+ onClick: insertSelectedPopupItem
});
let cycle = reverse => {
if (popup && popup.isOpen) {
cycleSuggestions(ed, reverse == true);
return null;
}
@@ -212,40 +204,43 @@ function autoComplete({ ed, cm }) {
let autocompleteOpts = autocompleteMap.get(ed);
let { completer, popup } = autocompleteOpts;
if (!completer || autocompleteOpts.insertingSuggestion ||
autocompleteOpts.doNotAutocomplete) {
autocompleteOpts.insertingSuggestion = false;
return;
}
let cur = ed.getCursor();
- completer.complete(cm.getRange({line: 0, ch: 0}, cur), cur)
- .then(suggestions => {
- if (!suggestions || !suggestions.length ||
- suggestions[0].preLabel == null) {
- autocompleteOpts.suggestionInsertedOnce = false;
- popup.hidePopup();
+ completer.complete(cm.getRange({line: 0, ch: 0}, cur), cur).then(suggestions => {
+ if (!suggestions || !suggestions.length || suggestions[0].preLabel == null) {
+ autocompleteOpts.suggestionInsertedOnce = false;
+ popup.once("popup-closed", () => {
+ // This event is used in tests.
ed.emit("after-suggest");
- return;
- }
- // The cursor is at the end of the currently entered part of the token,
- // like "backgr|" but we need to open the popup at the beginning of the
- // character "b". Thus we need to calculate the width of the entered part
- // of the token ("backgr" here). 4 comes from the popup's left padding.
+ });
+ popup.hidePopup();
+ return;
+ }
+ // The cursor is at the end of the currently entered part of the token,
+ // like "backgr|" but we need to open the popup at the beginning of the
+ // character "b". Thus we need to calculate the width of the entered part
+ // of the token ("backgr" here). 4 comes from the popup's left padding.
- let cursorElement =
- cm.display.cursorDiv.querySelector(".CodeMirror-cursor");
- let left = suggestions[0].preLabel.length * cm.defaultCharWidth() + 4;
- popup.hidePopup();
- popup.setItems(suggestions);
- popup.openPopup(cursorElement, -1 * left, 0);
- autocompleteOpts.suggestionInsertedOnce = false;
+ let cursorElement = cm.display.cursorDiv.querySelector(".CodeMirror-cursor");
+ let left = suggestions[0].preLabel.length * cm.defaultCharWidth() + 4;
+ popup.hidePopup();
+ popup.setItems(suggestions);
+
+ popup.once("popup-opened", () => {
// This event is used in tests.
ed.emit("after-suggest");
- }).then(null, e => console.error(e));
+ });
+ popup.openPopup(cursorElement, -1 * left, 0);
+ autocompleteOpts.suggestionInsertedOnce = false;
+ }).then(null, e => console.error(e));
}
/**
* Inserts a popup item into the current cursor location
* in the editor.
*/
function insertPopupItem(ed, popupItem) {
let {preLabel, text} = popupItem;
--- a/devtools/client/themes/common.css
+++ b/devtools/client/themes/common.css
@@ -40,70 +40,80 @@
-moz-appearance: none !important;
box-shadow: 0 1px 0 hsla(209,29%,72%,.25) inset;
background-color: transparent;
border-radius: 3px;
overflow-x: hidden;
max-height: 20rem;
}
+/* Reset list styles. */
+.devtools-autocomplete-popup ul {
+ list-style: none;
+}
+
+.devtools-autocomplete-popup ul,
+.devtools-autocomplete-popup li {
+ margin: 0;
+}
+
:root[platform="linux"] .devtools-autocomplete-popup {
/* Root font size is bigger on Linux, adjust rem-based values. */
max-height: 16rem;
}
.devtools-autocomplete-listbox {
-moz-appearance: none !important;
background-color: transparent;
border-width: 0px !important;
margin: 0;
+ padding: 2px;
}
.devtools-autocomplete-listbox > scrollbox {
padding: 2px;
}
-.inplace-editor-autocomplete-popup .devtools-autocomplete-listbox {
- /* Inplace editor closes the autocomplete popup on blur, the autocomplete
- popup should not steal the focus here.*/
- -moz-user-focus: ignore;
-}
-
-.devtools-autocomplete-listbox > richlistitem,
-.devtools-autocomplete-listbox > richlistitem[selected] {
+.devtools-autocomplete-listbox .autocomplete-item {
width: 100%;
background-color: transparent;
border-radius: 4px;
+ padding: 1px 0;
}
-.devtools-autocomplete-listbox.dark-theme > richlistitem[selected],
-.devtools-autocomplete-listbox.dark-theme > richlistitem:hover {
+.devtools-autocomplete-listbox .autocomplete-selected {
+ background-color: rgba(0,0,0,0.2);
+}
+
+.devtools-autocomplete-listbox.dark-theme .autocomplete-selected,
+.devtools-autocomplete-listbox.dark-theme .autocomplete-item:hover {
background-color: rgba(0,0,0,0.5);
}
-.devtools-autocomplete-listbox.dark-theme > richlistitem[selected] > .autocomplete-value,
-.devtools-autocomplete-listbox:focus.dark-theme > richlistitem[selected] > .initial-value {
+.devtools-autocomplete-listbox.dark-theme .autocomplete-selected > .autocomplete-value,
+.devtools-autocomplete-listbox:focus.dark-theme .autocomplete-selected > .initial-value {
color: hsl(208,100%,60%);
}
-.devtools-autocomplete-listbox.dark-theme > richlistitem[selected] > label {
+.devtools-autocomplete-listbox.dark-theme .autocomplete-selected > span {
color: #eee;
}
-.devtools-autocomplete-listbox.dark-theme > richlistitem > label {
+.devtools-autocomplete-listbox.dark-theme .autocomplete-item > span {
color: #ccc;
}
-.devtools-autocomplete-listbox > richlistitem > .initial-value,
-.devtools-autocomplete-listbox > richlistitem > .autocomplete-value {
+.devtools-autocomplete-listbox .autocomplete-item > .initial-value,
+.devtools-autocomplete-listbox .autocomplete-item > .autocomplete-value {
margin: 0;
- padding: 1px 0;
+ padding: 0;
+ cursor: default;
}
-.devtools-autocomplete-listbox > richlistitem > .autocomplete-count {
+.devtools-autocomplete-listbox .autocomplete-item > .autocomplete-count {
text-align: end;
}
/* Rest of the dark and light theme */
.devtools-autocomplete-popup,
.theme-dark .CodeMirror-hints,
.theme-dark .CodeMirror-Tern-tooltip {
@@ -127,32 +137,32 @@
border-radius: 5px;
font-size: var(--theme-autompletion-font-size);
}
.devtools-autocomplete-popup.firebug-theme {
background: var(--theme-body-background);
}
-.devtools-autocomplete-listbox.firebug-theme > richlistitem[selected],
-.devtools-autocomplete-listbox.firebug-theme > richlistitem:hover,
-.devtools-autocomplete-listbox.light-theme > richlistitem[selected],
-.devtools-autocomplete-listbox.light-theme > richlistitem:hover {
+.devtools-autocomplete-listbox.firebug-theme .autocomplete-selected,
+.devtools-autocomplete-listbox.firebug-theme .autocomplete-item:hover,
+.devtools-autocomplete-listbox.light-theme .autocomplete-selected,
+.devtools-autocomplete-listbox.light-theme .autocomplete-item:hover {
background-color: rgba(128,128,128,0.3);
}
-.devtools-autocomplete-listbox.firebug-theme > richlistitem[selected] > .autocomplete-value,
-.devtools-autocomplete-listbox:focus.firebug-theme > richlistitem[selected] > .initial-value,
-.devtools-autocomplete-listbox.light-theme > richlistitem[selected] > .autocomplete-value,
-.devtools-autocomplete-listbox:focus.light-theme > richlistitem[selected] > .initial-value {
+.devtools-autocomplete-listbox.firebug-theme .autocomplete-selected > .autocomplete-value,
+.devtools-autocomplete-listbox:focus.firebug-theme .autocomplete-selected > .initial-value,
+.devtools-autocomplete-listbox.light-theme .autocomplete-selected > .autocomplete-value,
+.devtools-autocomplete-listbox:focus.light-theme .autocomplete-selected > .initial-value {
color: #222;
}
-.devtools-autocomplete-listbox.firebug-theme > richlistitem > label,
-.devtools-autocomplete-listbox.light-theme > richlistitem > label {
+.devtools-autocomplete-listbox.firebug-theme .autocomplete-item > span,
+.devtools-autocomplete-listbox.light-theme .autocomplete-item > span {
color: #666;
}
/* links to source code, like displaying `myfile.js:45` */
.devtools-source-link {
font-family: var(--monospace-font-family);
color: var(--theme-highlight-blue);
--- a/devtools/client/themes/tooltips.css
+++ b/devtools/client/themes/tooltips.css
@@ -102,16 +102,17 @@
.tooltip-container {
display: none;
position: fixed;
z-index: 9999;
display: none;
background: transparent;
pointer-events: none;
overflow: hidden;
+ filter: drop-shadow(0 3px 4px var(--theme-tooltip-shadow));
}
.tooltip-xul-wrapper {
-moz-appearance: none;
background: transparent;
overflow: visible;
border-style: none;
}
@@ -139,21 +140,17 @@
.tooltip-hidden {
display: flex;
visibility: hidden;
}
/* Tooltip : arrow style */
-.tooltip-container[type="arrow"] {
- filter: drop-shadow(0 3px 4px var(--theme-tooltip-shadow));
-}
-
-.tooltip-xul-wrapper .tooltip-container[type="arrow"] {
+.tooltip-xul-wrapper .tooltip-container {
/* When displayed in a XUL panel the drop shadow would be abruptly cut by the panel */
filter: none;
}
.tooltip-container[type="arrow"] > .tooltip-panel {
position: relative;
flex-grow: 0;
min-height: 10px;
--- a/devtools/client/webconsole/jsterm.js
+++ b/devtools/client/webconsole/jsterm.js
@@ -241,27 +241,31 @@ JSTerm.prototype = {
/**
* Initialize the JSTerminal UI.
*/
init: function () {
let autocompleteOptions = {
onSelect: this.onAutocompleteSelect.bind(this),
onClick: this.acceptProposedCompletion.bind(this),
- panelId: "webConsole_autocompletePopup",
- listBoxId: "webConsole_autocompletePopupListBox",
- position: "before_start",
+ listId: "webConsole_autocompletePopupListBox",
+ position: "top",
theme: "auto",
- direction: "ltr",
autoSelect: true
};
- this.autocompletePopup = new AutocompletePopup(this.hud.document,
- autocompleteOptions);
let doc = this.hud.document;
+
+ let toolbox = gDevTools.getToolbox(this.hud.owner.target);
+ if (!toolbox) {
+ // In some cases (e.g. Browser Console), there is no toolbox.
+ toolbox = { doc };
+ }
+ this.autocompletePopup = new AutocompletePopup(toolbox, autocompleteOptions);
+
let inputContainer = doc.querySelector(".jsterm-input-container");
this.completeNode = doc.querySelector(".jsterm-complete-node");
this.inputNode = doc.querySelector(".jsterm-input-node");
if (this.hud.owner._browserConsole &&
!Services.prefs.getBoolPref("devtools.chrome.enabled")) {
inputContainer.style.display = "none";
} else {
@@ -1696,22 +1700,16 @@ JSTerm.prototype = {
this._sidebarDestroy();
this.clearCompletion();
this.clearOutput();
this.autocompletePopup.destroy();
this.autocompletePopup = null;
- let popup = this.hud.owner.chromeWindow.document
- .getElementById("webConsole_autocompletePopup");
- if (popup) {
- popup.parentNode.removeChild(popup);
- }
-
if (this._onPaste) {
this.inputNode.removeEventListener("paste", this._onPaste, false);
this.inputNode.removeEventListener("drop", this._onPaste, false);
this._onPaste = null;
}
this.inputNode.removeEventListener("keypress", this._keyPress, false);
this.inputNode.removeEventListener("input", this._inputEventHandler, false);