Bug 1266456 - part9: use HTMLTooltip for autocomplete-popup;r=bgrins

Modify the devtools autocomplete-popup to rely on a HTMLTooltip instance
instead of a XUL panel.

Other than the straightforward migration to HTML, the main difference with
the new implementation is that the richlistbox has now been replace with a
simple HTML list element. The former XUL widget used to be able to take the
focus from the input it was linked to.

This is no longer the case. Most autocomplete users were always keeping the
focus in the input, except for the inspector-search, which was moving the
focus back and forth between the input and the autocomplete's richlistbox.
Now the focus is always in the input. A practical example to illustrate how
this changes the UX: before when the user had the focus on the first element
of the list, pressing "DOWN" would keep the element selected but visually move
the focus in the input. Now the selection simply cycles to the next item.

Even though this introduces a difference in behaviour compared to the previous
implementation, it makes the inspector search UX consistent with the other
autocomplete widgets used in devtools.

Another difference is about the display for the inspector-search. The position
of the autocomplete popup used to be above the input. This is now impossible to
achieve because the search input is at the top of the toolbox and the HTML tooltip
can not exceed the limits of the toolbox.

For this #2 issue, either we manage to use XUL panel wrappers, in which case, the
autocomplete will be displayed as it used to. Or we can invert the order in which
items are inserted and explicitly ask for the autocomplete to be displayed below the
input. I prefered not to change this here in order to make the code change easier to
understand, but it should be addressed in a follow-up.

MozReview-Commit-ID: jH9aXm9Jvz

--HG--
extra : rebase_source : 57267be0d214ed807f3152838c4123400ab7b7e3
This commit is contained in:
Julian Descottes 2016-07-07 16:32:42 +02:00
Родитель 74d5a93a8f
Коммит d03c3018a5
11 изменённых файлов: 353 добавлений и 420 удалений

Просмотреть файл

@ -699,7 +699,6 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
// 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.

Просмотреть файл

@ -79,7 +79,7 @@ InspectorSearch.prototype = {
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;
}
@ -142,21 +142,19 @@ function SelectorAutocompleter(inspector, inputNode) {
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);
@ -231,11 +229,11 @@ SelectorAutocompleter.prototype = {
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;
@ -246,11 +244,11 @@ SelectorAutocompleter.prototype = {
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;
@ -262,11 +260,11 @@ SelectorAutocompleter.prototype = {
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;
@ -275,13 +273,13 @@ SelectorAutocompleter.prototype = {
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;
@ -311,20 +309,17 @@ SelectorAutocompleter.prototype = {
* 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.
@ -335,12 +330,10 @@ SelectorAutocompleter.prototype = {
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);
} else {
if (popup.selectedIndex === 0) {
popup.selectedIndex = popup.itemCount - 1;
} else {
popup.selectedIndex--;
}
this.searchBox.value = popup.selectedItem.label;
}
@ -348,12 +341,21 @@ SelectorAutocompleter.prototype = {
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;
}
@ -364,59 +366,17 @@ SelectorAutocompleter.prototype = {
},
/**
* 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;
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;
_onSearchPopupClick: function (event) {
let selectedItem = this.searchPopup.selectedItem;
if (selectedItem) {
this.searchBox.value = selectedItem.label;
}
this.emit("processing-done");
this.hidePopup();
event.preventDefault();
event.stopPropagation();
},
/**
@ -430,6 +390,9 @@ SelectorAutocompleter.prototype = {
/**
* 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;
@ -473,21 +436,24 @@ SelectorAutocompleter.prototype = {
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;
},
/**
@ -534,7 +500,7 @@ SelectorAutocompleter.prototype = {
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) {
@ -550,7 +516,9 @@ SelectorAutocompleter.prototype = {
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;

Просмотреть файл

@ -23,7 +23,6 @@ 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";
@ -109,12 +108,9 @@ function MarkupView(inspector, frame, controllerWindow) {
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);

Просмотреть файл

@ -222,7 +222,7 @@ function CssRuleView(inspector, document, store, pageStyle) {
autoSelect: true,
theme: "auto"
};
this.popup = new AutocompletePopup(this.styleDocument, options);
this.popup = new AutocompletePopup(inspector._toolbox, options);
this._showEmpty();

Просмотреть файл

@ -5,135 +5,106 @@
"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;
}
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();
}
// 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");
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.
*
@ -151,13 +122,19 @@ AutocompletePopup.prototype = {
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");
});
},
/**
@ -167,30 +144,29 @@ AutocompletePopup.prototype = {
* 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._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();
},
/**
@ -204,19 +180,17 @@ AutocompletePopup.prototype = {
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;
},
/**
@ -228,7 +202,7 @@ AutocompletePopup.prototype = {
* @return {Object} The autocomplete item at index index.
*/
getItemAtIndex: function (index) {
return this._list.getItemAtIndex(index)._autocompleteItem;
return this.items[index];
},
/**
@ -237,13 +211,8 @@ AutocompletePopup.prototype = {
* @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);
},
/**
@ -258,30 +227,24 @@ AutocompletePopup.prototype = {
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;
@ -292,26 +255,32 @@ AutocompletePopup.prototype = {
* 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));
}
},
/**
* 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");
_scrollElementIntoViewIfNeeded: function (element) {
let quads = element.getBoxQuads({relativeTo: this._tooltip.panel});
if (!quads || !quads[0]) {
return;
}
// Focus this for accessibility so users know about the selected item.
this._document.activeElement.setAttribute("aria-activedescendant",
this._list.selectedItem.id);
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);
}
},
/**
@ -320,22 +289,10 @@ AutocompletePopup.prototype = {
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();
},
/**
@ -344,7 +301,7 @@ AutocompletePopup.prototype = {
* @type {Number}
*/
get selectedIndex() {
return this._list.selectedIndex;
return this._selectedIndex;
},
/**
@ -354,11 +311,24 @@ AutocompletePopup.prototype = {
* 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;
if (this.isOpen && item && this.onSelectCallback) {
// Call the user-defined select callback if defined.
this.onSelectCallback();
}
this._updateAriaActiveDescendant();
},
/**
@ -366,8 +336,7 @@ AutocompletePopup.prototype = {
* @type Object
*/
get selectedItem() {
return this._list.selectedItem ?
this._list.selectedItem._autocompleteItem : null;
return this.items[this._selectedIndex];
},
/**
@ -377,11 +346,10 @@ AutocompletePopup.prototype = {
* 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();
},
/**
@ -401,54 +369,36 @@ AutocompletePopup.prototype = {
* 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);
},
/**
@ -458,11 +408,26 @@ AutocompletePopup.prototype = {
* 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;
}
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);
}
this._list.removeChild(listItem);
},
/**
@ -470,7 +435,7 @@ AutocompletePopup.prototype = {
* @type {Number}
*/
get itemCount() {
return this._list.childNodes.length;
return this.items.length;
},
/**
@ -478,8 +443,14 @@ AutocompletePopup.prototype = {
*
* @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;
},
/**
@ -489,12 +460,11 @@ AutocompletePopup.prototype = {
* 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;
},
@ -508,7 +478,7 @@ AutocompletePopup.prototype = {
if (this.selectedIndex > 0) {
this.selectedIndex--;
} else {
this.selectedIndex = this.itemCount - 1;
this.selectedIndex = this.items.length - 1;
}
return this.selectedItem;
@ -522,11 +492,8 @@ AutocompletePopup.prototype = {
* 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;
},
@ -538,20 +505,11 @@ AutocompletePopup.prototype = {
* 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.
*
@ -567,11 +525,25 @@ AutocompletePopup.prototype = {
* - 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;
},
};

Просмотреть файл

@ -33,7 +33,6 @@ const CONTENT_TYPES = {
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.
@ -989,13 +988,13 @@ InplaceEditor.prototype = {
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
@ -1021,13 +1020,13 @@ InplaceEditor.prototype = {
// 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();
},
@ -1174,7 +1173,6 @@ InplaceEditor.prototype = {
* 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);
},
@ -1184,7 +1182,6 @@ InplaceEditor.prototype = {
* popup.
*/
_hideAutocompletePopup: function () {
this.popup._panel.classList.remove(AUTOCOMPLETE_POPUP_CLASSNAME);
this.popup.off("popup-click", this._onAutocompletePopupClick);
this.popup.hidePopup();
},

Просмотреть файл

@ -420,6 +420,7 @@ HTMLTooltip.prototype = {
hide: Task.async(function* () {
this.doc.defaultView.clearTimeout(this.attachEventsTimer);
if (!this.isVisible()) {
this.emit("hidden");
return;
}
@ -500,7 +501,7 @@ HTMLTooltip.prototype = {
}
// 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.

Просмотреть файл

@ -5,11 +5,8 @@
"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",
@ -18,9 +15,6 @@ const CM_TERN_SCRIPTS = [
const autocompleteMap = new WeakMap();
// A simple way to give each popup its own panelId.
let autocompleteCounter = 0;
/**
* Prepares an editor instance for autocompletion.
*/
@ -124,23 +118,21 @@ function initializeAutoCompletion(ctx, options = {}) {
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 => {
@ -217,30 +209,33 @@ function autoComplete({ ed, cm }) {
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();
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.
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);
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");
});
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.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));
}
/**

Просмотреть файл

@ -45,6 +45,16 @@
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;
@ -55,50 +65,50 @@
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;
}
.devtools-autocomplete-listbox.dark-theme > richlistitem[selected],
.devtools-autocomplete-listbox.dark-theme > richlistitem: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 {
color: hsl(208,100%,60%);
}
.devtools-autocomplete-listbox.dark-theme > richlistitem[selected] > label {
color: #eee;
}
.devtools-autocomplete-listbox.dark-theme > richlistitem > label {
color: #ccc;
}
.devtools-autocomplete-listbox > richlistitem > .initial-value,
.devtools-autocomplete-listbox > richlistitem > .autocomplete-value {
margin: 0;
padding: 1px 0;
}
.devtools-autocomplete-listbox > richlistitem > .autocomplete-count {
.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 .autocomplete-selected > .autocomplete-value,
.devtools-autocomplete-listbox:focus.dark-theme .autocomplete-selected > .initial-value {
color: hsl(208,100%,60%);
}
.devtools-autocomplete-listbox.dark-theme .autocomplete-selected > span {
color: #eee;
}
.devtools-autocomplete-listbox.dark-theme .autocomplete-item > span {
color: #ccc;
}
.devtools-autocomplete-listbox .autocomplete-item > .initial-value,
.devtools-autocomplete-listbox .autocomplete-item > .autocomplete-value {
margin: 0;
padding: 0;
cursor: default;
}
.devtools-autocomplete-listbox .autocomplete-item > .autocomplete-count {
text-align: end;
}
@ -132,22 +142,22 @@
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;
}

Просмотреть файл

@ -107,6 +107,7 @@
background: transparent;
pointer-events: none;
overflow: hidden;
filter: drop-shadow(0 3px 4px var(--theme-tooltip-shadow));
}
.tooltip-xul-wrapper {
@ -144,11 +145,7 @@
/* 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;
}

Просмотреть файл

@ -246,17 +246,21 @@ JSTerm.prototype = {
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");
@ -1701,12 +1705,6 @@ JSTerm.prototype = {
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);