mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 831693 - Experiment with auto completion in Inspector Searchbox, r=paul
--HG-- rename : browser/devtools/webconsole/AutocompletePopup.jsm => browser/devtools/shared/AutocompletePopup.jsm
This commit is contained in:
parent
0ed168173d
commit
3a413eb145
@ -24,6 +24,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "Highlighter",
|
|||||||
"resource:///modules/devtools/Highlighter.jsm");
|
"resource:///modules/devtools/Highlighter.jsm");
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "ToolSidebar",
|
XPCOMUtils.defineLazyModuleGetter(this, "ToolSidebar",
|
||||||
"resource:///modules/devtools/Sidebar.jsm");
|
"resource:///modules/devtools/Sidebar.jsm");
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "SelectorSearch",
|
||||||
|
"resource:///modules/devtools/SelectorSearch.jsm");
|
||||||
|
|
||||||
const LAYOUT_CHANGE_TIMER = 250;
|
const LAYOUT_CHANGE_TIMER = 250;
|
||||||
|
|
||||||
@ -62,16 +64,6 @@ InspectorPanel.prototype = {
|
|||||||
this.nodemenu.addEventListener("popupshowing", this._setupNodeMenu, true);
|
this.nodemenu.addEventListener("popupshowing", this._setupNodeMenu, true);
|
||||||
this.nodemenu.addEventListener("popuphiding", this._resetNodeMenu, true);
|
this.nodemenu.addEventListener("popuphiding", this._resetNodeMenu, true);
|
||||||
|
|
||||||
// Initialize the search related items
|
|
||||||
this.searchBox = this.panelDoc.getElementById("inspector-searchbox");
|
|
||||||
this._lastSearched = null;
|
|
||||||
this._searchResults = null;
|
|
||||||
this._searchIndex = 0;
|
|
||||||
this._onHTMLSearch = this._onHTMLSearch.bind(this);
|
|
||||||
this._onSearchKeypress = this._onSearchKeypress.bind(this);
|
|
||||||
this.searchBox.addEventListener("command", this._onHTMLSearch, true);
|
|
||||||
this.searchBox.addEventListener("keypress", this._onSearchKeypress, true);
|
|
||||||
|
|
||||||
// Create an empty selection
|
// Create an empty selection
|
||||||
this._selection = new Selection();
|
this._selection = new Selection();
|
||||||
this.onNewSelection = this.onNewSelection.bind(this);
|
this.onNewSelection = this.onNewSelection.bind(this);
|
||||||
@ -153,6 +145,7 @@ InspectorPanel.prototype = {
|
|||||||
deferred.resolve(this);
|
deferred.resolve(this);
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
|
|
||||||
|
this.setupSearchBox();
|
||||||
this.setupSidebar();
|
this.setupSidebar();
|
||||||
|
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
@ -195,6 +188,24 @@ InspectorPanel.prototype = {
|
|||||||
this.isDirty = true;
|
this.isDirty = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hooks the searchbar to show result and auto completion suggestions.
|
||||||
|
*/
|
||||||
|
setupSearchBox: function InspectorPanel_setupSearchBox() {
|
||||||
|
// Initiate the selectors search object.
|
||||||
|
let setNodeFunction = function(node) {
|
||||||
|
this.selection.setNode(node, "selectorsearch");
|
||||||
|
}.bind(this);
|
||||||
|
if (this.searchSuggestions) {
|
||||||
|
this.searchSuggestions.destroy();
|
||||||
|
this.searchSuggestions = null;
|
||||||
|
}
|
||||||
|
this.searchBox = this.panelDoc.getElementById("inspector-searchbox");
|
||||||
|
this.searchSuggestions = new SelectorSearch(this.browser.contentDocument,
|
||||||
|
this.searchBox,
|
||||||
|
setNodeFunction);
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the sidebar.
|
* Build the sidebar.
|
||||||
*/
|
*/
|
||||||
@ -250,6 +261,7 @@ InspectorPanel.prototype = {
|
|||||||
self.selection.setNode(newWindow.document.documentElement, "navigateaway");
|
self.selection.setNode(newWindow.document.documentElement, "navigateaway");
|
||||||
}
|
}
|
||||||
self._initMarkup();
|
self._initMarkup();
|
||||||
|
self.setupSearchBox();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newWindow.document.readyState == "loading") {
|
if (newWindow.document.readyState == "loading") {
|
||||||
@ -394,9 +406,8 @@ InspectorPanel.prototype = {
|
|||||||
|
|
||||||
this.nodemenu.removeEventListener("popupshowing", this._setupNodeMenu, true);
|
this.nodemenu.removeEventListener("popupshowing", this._setupNodeMenu, true);
|
||||||
this.nodemenu.removeEventListener("popuphiding", this._resetNodeMenu, true);
|
this.nodemenu.removeEventListener("popuphiding", this._resetNodeMenu, true);
|
||||||
this.searchBox.removeEventListener("command", this._onHTMLSearch, true);
|
|
||||||
this.searchBox.removeEventListener("keypress", this._onSearchKeypress, true);
|
|
||||||
this.breadcrumbs.destroy();
|
this.breadcrumbs.destroy();
|
||||||
|
this.searchSuggestions.destroy();
|
||||||
this.selection.off("new-node", this.onNewSelection);
|
this.selection.off("new-node", this.onNewSelection);
|
||||||
this.selection.off("before-new-node", this.onBeforeNewSelection);
|
this.selection.off("before-new-node", this.onBeforeNewSelection);
|
||||||
this.selection.off("detached", this.onDetached);
|
this.selection.off("detached", this.onDetached);
|
||||||
@ -408,81 +419,14 @@ InspectorPanel.prototype = {
|
|||||||
this.panelDoc = null;
|
this.panelDoc = null;
|
||||||
this.panelWin = null;
|
this.panelWin = null;
|
||||||
this.breadcrumbs = null;
|
this.breadcrumbs = null;
|
||||||
|
this.searchSuggestions = null;
|
||||||
this.lastNodemenuItem = null;
|
this.lastNodemenuItem = null;
|
||||||
this.nodemenu = null;
|
this.nodemenu = null;
|
||||||
this.searchBox = null;
|
|
||||||
this.highlighter = null;
|
this.highlighter = null;
|
||||||
|
|
||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* The command callback for the HTML search box. This function is
|
|
||||||
* automatically invoked as the user is typing.
|
|
||||||
*/
|
|
||||||
_onHTMLSearch: function InspectorPanel__onHTMLSearch() {
|
|
||||||
let query = this.searchBox.value;
|
|
||||||
if (query == this._lastSearched) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._lastSearched = query;
|
|
||||||
this._searchIndex = 0;
|
|
||||||
|
|
||||||
if (query.length == 0) {
|
|
||||||
this.searchBox.removeAttribute("filled");
|
|
||||||
this.searchBox.classList.remove("devtools-no-search-result");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.searchBox.setAttribute("filled", true);
|
|
||||||
this._searchResults = this.browser.contentDocument.querySelectorAll(query);
|
|
||||||
if (this._searchResults.length > 0) {
|
|
||||||
this.searchBox.classList.remove("devtools-no-search-result");
|
|
||||||
this.cancelLayoutChange();
|
|
||||||
this.selection.setNode(this._searchResults[0]);
|
|
||||||
} else {
|
|
||||||
this.searchBox.classList.add("devtools-no-search-result");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search for the search box value as a query selector.
|
|
||||||
*/
|
|
||||||
_onSearchKeypress: function InspectorPanel__onSearchKeypress(aEvent) {
|
|
||||||
let query = this.searchBox.value;
|
|
||||||
switch(aEvent.keyCode) {
|
|
||||||
case aEvent.DOM_VK_ENTER:
|
|
||||||
case aEvent.DOM_VK_RETURN:
|
|
||||||
if (query == this._lastSearched) {
|
|
||||||
this._searchIndex = (this._searchIndex + 1) % this._searchResults.length;
|
|
||||||
} else {
|
|
||||||
this._onHTMLSearch();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case aEvent.DOM_VK_UP:
|
|
||||||
if (--this._searchIndex < 0) {
|
|
||||||
this._searchIndex = this._searchResults.length - 1;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case aEvent.DOM_VK_DOWN:
|
|
||||||
this._searchIndex = (this._searchIndex + 1) % this._searchResults.length;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
aEvent.preventDefault();
|
|
||||||
aEvent.stopPropagation();
|
|
||||||
this.cancelLayoutChange();
|
|
||||||
if (this._searchResults.length > 0) {
|
|
||||||
this.selection.setNode(this._searchResults[this._searchIndex]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the node menu.
|
* Show the node menu.
|
||||||
*/
|
*/
|
||||||
|
549
browser/devtools/inspector/SelectorSearch.jsm
Normal file
549
browser/devtools/inspector/SelectorSearch.jsm
Normal file
@ -0,0 +1,549 @@
|
|||||||
|
/* 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 = Components.utils;
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "AutocompletePopup",
|
||||||
|
"resource:///modules/devtools/AutocompletePopup.jsm");
|
||||||
|
this.EXPORTED_SYMBOLS = ["SelectorSearch"];
|
||||||
|
|
||||||
|
// Maximum number of selector suggestions shown in the panel.
|
||||||
|
const MAX_SUGGESTIONS = 15;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts any input box on a page to a CSS selector search and suggestion box.
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
* @param nsIDOMDocument aContentDocument
|
||||||
|
* The content document which inspector is attached to.
|
||||||
|
* @param nsiInputElement aInputNode
|
||||||
|
* The input element to which the panel will be attached and from where
|
||||||
|
* search input will be taken.
|
||||||
|
* @param Function aCallback
|
||||||
|
* The method to callback when a search is available.
|
||||||
|
* This method is called with the matched node as the first argument.
|
||||||
|
*/
|
||||||
|
this.SelectorSearch = function(aContentDocument, aInputNode, aCallback) {
|
||||||
|
this.doc = aContentDocument;
|
||||||
|
this.callback = aCallback;
|
||||||
|
this.searchBox = aInputNode;
|
||||||
|
this.panelDoc = this.searchBox.ownerDocument;
|
||||||
|
|
||||||
|
// initialize variables.
|
||||||
|
this._lastSearched = null;
|
||||||
|
this._lastValidSearch = "";
|
||||||
|
this._lastToLastValidSearch = null;
|
||||||
|
this._searchResults = null;
|
||||||
|
this._searchSuggestions = {};
|
||||||
|
this._searchIndex = 0;
|
||||||
|
|
||||||
|
// bind!
|
||||||
|
this._showPopup = this._showPopup.bind(this);
|
||||||
|
this._onHTMLSearch = this._onHTMLSearch.bind(this);
|
||||||
|
this._onSearchKeypress = this._onSearchKeypress.bind(this);
|
||||||
|
this._onListBoxKeypress = this._onListBoxKeypress.bind(this);
|
||||||
|
|
||||||
|
// Options for the AutocompletePopup.
|
||||||
|
let options = {
|
||||||
|
panelId: "inspector-searchbox-panel",
|
||||||
|
listBoxId: "searchbox-panel-listbox",
|
||||||
|
fixedWidth: true,
|
||||||
|
autoSelect: true,
|
||||||
|
position: "before_start",
|
||||||
|
direction: "ltr",
|
||||||
|
onClick: this._onListBoxKeypress,
|
||||||
|
onKeypress: this._onListBoxKeypress,
|
||||||
|
};
|
||||||
|
this.searchPopup = new AutocompletePopup(this.panelDoc, options);
|
||||||
|
|
||||||
|
// event listeners.
|
||||||
|
this.searchBox.addEventListener("command", this._onHTMLSearch, true);
|
||||||
|
this.searchBox.addEventListener("keypress", this._onSearchKeypress, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.SelectorSearch.prototype = {
|
||||||
|
|
||||||
|
// The possible states of the query.
|
||||||
|
States: {
|
||||||
|
CLASS: "class",
|
||||||
|
ID: "id",
|
||||||
|
TAG: "tag",
|
||||||
|
},
|
||||||
|
|
||||||
|
// The current state of the query.
|
||||||
|
_state: null,
|
||||||
|
|
||||||
|
// The query corresponding to last state computation.
|
||||||
|
_lastStateCheckAt: null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the state of the query. State refers to whether the query
|
||||||
|
* currently requires a class suggestion, or a tag, or an Id suggestion.
|
||||||
|
* This getter will effectively compute the state by traversing the query
|
||||||
|
* character by character each time the query changes.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* '#f' requires an Id suggestion, so the state is States.ID
|
||||||
|
* 'div > .foo' requires class suggestion, so state is States.CLASS
|
||||||
|
*/
|
||||||
|
get state() {
|
||||||
|
if (!this.searchBox || !this.searchBox.value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let query = this.searchBox.value;
|
||||||
|
if (this._lastStateCheckAt == query) {
|
||||||
|
// If query is the same, return early.
|
||||||
|
return this._state;
|
||||||
|
}
|
||||||
|
this._lastStateCheckAt = query;
|
||||||
|
|
||||||
|
this._state = null;
|
||||||
|
let subQuery = "";
|
||||||
|
// Now we iterate over the query and decide the state character by character.
|
||||||
|
// The logic here is that while iterating, the state can go from one to
|
||||||
|
// another with some restrictions. Like, if the state is Class, then it can
|
||||||
|
// never go to Tag state without a space or '>' character; Or like, a Class
|
||||||
|
// state with only '.' cannot go to an Id state without any [a-zA-Z] after
|
||||||
|
// the '.' which means that '.#' is a selector matching a class name '#'.
|
||||||
|
// Similarily for '#.' which means a selctor matching an id '.'.
|
||||||
|
for (let i = 1; i <= query.length; i++) {
|
||||||
|
// Calculate the state.
|
||||||
|
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:
|
||||||
|
this._state = lastChar == "."
|
||||||
|
? this.States.CLASS
|
||||||
|
: lastChar == "#"
|
||||||
|
? this.States.ID
|
||||||
|
: 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 '.'.
|
||||||
|
this._state = (lastChar == " " || lastChar == ">")
|
||||||
|
? this.States.TAG
|
||||||
|
: lastChar == "#"
|
||||||
|
? this.States.ID
|
||||||
|
: 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 '#'.
|
||||||
|
this._state = (lastChar == " " || lastChar == ">")
|
||||||
|
? this.States.TAG
|
||||||
|
: lastChar == "."
|
||||||
|
? this.States.CLASS
|
||||||
|
: this.States.ID;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this._state;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes event listeners and cleans up references.
|
||||||
|
*/
|
||||||
|
destroy: function SelectorSearch_destroy() {
|
||||||
|
// event listeners.
|
||||||
|
this.searchBox.removeEventListener("command", this._onHTMLSearch, true);
|
||||||
|
this.searchBox.removeEventListener("keypress", this._onSearchKeypress, true);
|
||||||
|
this.searchPopup.destroy();
|
||||||
|
this.searchPopup = null;
|
||||||
|
this.searchBox = null;
|
||||||
|
this.doc = null;
|
||||||
|
this.panelDoc = null;
|
||||||
|
this._searchResults = null;
|
||||||
|
this._searchSuggestions = null;
|
||||||
|
this.callback = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The command callback for the input box. This function is automatically
|
||||||
|
* invoked as the user is typing if the input box type is search.
|
||||||
|
*/
|
||||||
|
_onHTMLSearch: function SelectorSearch__onHTMLSearch() {
|
||||||
|
let query = this.searchBox.value;
|
||||||
|
if (query == this._lastSearched) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._lastSearched = query;
|
||||||
|
this._searchIndex = 0;
|
||||||
|
|
||||||
|
if (query.length == 0) {
|
||||||
|
this._lastValidSearch = "";
|
||||||
|
this.searchBox.removeAttribute("filled");
|
||||||
|
this.searchBox.classList.remove("devtools-no-search-result");
|
||||||
|
if (this.searchPopup.isOpen) {
|
||||||
|
this.searchPopup.hidePopup();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.searchBox.setAttribute("filled", true);
|
||||||
|
try {
|
||||||
|
this._searchResults = this.doc.querySelectorAll(query);
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
this._searchResults = [];
|
||||||
|
}
|
||||||
|
if (this._searchResults.length > 0) {
|
||||||
|
this._lastValidSearch = query;
|
||||||
|
// Even though the selector matched atleast one node, there is still
|
||||||
|
// possibility of suggestions.
|
||||||
|
if (query.match(/[\s>+]$/)) {
|
||||||
|
// If the query has a space or '>' at the end, create a selector to match
|
||||||
|
// the children of the selector inside the search box by adding a '*'.
|
||||||
|
this._lastValidSearch += "*";
|
||||||
|
}
|
||||||
|
else if (query.match(/[\s>+][\.#a-zA-Z][\.#>\s+]*$/)) {
|
||||||
|
// If the query is a partial descendant selector which does not matches
|
||||||
|
// any node, remove the last incomplete part and add a '*' to match
|
||||||
|
// everything. For ex, convert 'foo > b' to 'foo > *' .
|
||||||
|
let lastPart = query.match(/[\s>+][\.#a-zA-Z][^>\s+]*$/)[0];
|
||||||
|
this._lastValidSearch = query.slice(0, -1 * lastPart.length + 1) + "*";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!query.slice(-1).match(/[\.#\s>+]/)) {
|
||||||
|
// Hide the popup if we have some matching nodes and the query is not
|
||||||
|
// ending with [.# >] which means that the selector is not at the
|
||||||
|
// beginning of a new class, tag or id.
|
||||||
|
if (this.searchPopup.isOpen) {
|
||||||
|
this.searchPopup.hidePopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.showSuggestions();
|
||||||
|
}
|
||||||
|
this.searchBox.classList.remove("devtools-no-search-result");
|
||||||
|
this.callback(this._searchResults[0]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (query.match(/[\s>+]$/)) {
|
||||||
|
this._lastValidSearch = query + "*";
|
||||||
|
}
|
||||||
|
else if (query.match(/[\s>+][\.#a-zA-Z][\.#>\s+]*$/)) {
|
||||||
|
let lastPart = query.match(/[\s+>][\.#a-zA-Z][^>\s+]*$/)[0];
|
||||||
|
this._lastValidSearch = query.slice(0, -1 * lastPart.length + 1) + "*";
|
||||||
|
}
|
||||||
|
this.searchBox.classList.add("devtools-no-search-result");
|
||||||
|
this.showSuggestions();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles keypresses inside the input box.
|
||||||
|
*/
|
||||||
|
_onSearchKeypress: function SelectorSearch__onSearchKeypress(aEvent) {
|
||||||
|
let query = this.searchBox.value;
|
||||||
|
switch(aEvent.keyCode) {
|
||||||
|
case aEvent.DOM_VK_ENTER:
|
||||||
|
case aEvent.DOM_VK_RETURN:
|
||||||
|
if (query == this._lastSearched) {
|
||||||
|
this._searchIndex = (this._searchIndex + 1) % this._searchResults.length;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this._onHTMLSearch();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case aEvent.DOM_VK_UP:
|
||||||
|
if (this.searchPopup.isOpen && this.searchPopup.itemCount > 0) {
|
||||||
|
this.searchPopup.focus();
|
||||||
|
if (this.searchPopup.selectedIndex == this.searchPopup.itemCount - 1) {
|
||||||
|
this.searchPopup.selectedIndex =
|
||||||
|
Math.max(0, this.searchPopup.itemCount - 2);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.searchPopup.selectedIndex = this.searchPopup.itemCount - 1;
|
||||||
|
}
|
||||||
|
this.searchBox.value = this.searchPopup.selectedItem.label;
|
||||||
|
}
|
||||||
|
else if (--this._searchIndex < 0) {
|
||||||
|
this._searchIndex = this._searchResults.length - 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case aEvent.DOM_VK_DOWN:
|
||||||
|
if (this.searchPopup.isOpen && this.searchPopup.itemCount > 0) {
|
||||||
|
this.searchPopup.focus();
|
||||||
|
this.searchPopup.selectedIndex = 0;
|
||||||
|
this.searchBox.value = this.searchPopup.selectedItem.label;
|
||||||
|
}
|
||||||
|
this._searchIndex = (this._searchIndex + 1) % this._searchResults.length;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case aEvent.DOM_VK_TAB:
|
||||||
|
if (this.searchPopup.isOpen &&
|
||||||
|
this.searchPopup.getItemAtIndex(this.searchPopup.itemCount - 1)
|
||||||
|
.preLabel == query) {
|
||||||
|
this.searchPopup.selectedIndex = this.searchPopup.itemCount - 1;
|
||||||
|
this.searchBox.value = this.searchPopup.selectedItem.label;
|
||||||
|
this._onHTMLSearch();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case aEvent.DOM_VK_BACK_SPACE:
|
||||||
|
case aEvent.DOM_VK_DELETE:
|
||||||
|
// need to throw away the lastValidSearch.
|
||||||
|
this._lastToLastValidSearch = null;
|
||||||
|
// This gets the most complete selector from the query. For ex.
|
||||||
|
// '.foo.ba' returns '.foo' , '#foo > .bar.baz' returns '#foo > .bar'
|
||||||
|
// '.foo +bar' returns '.foo +' and likewise.
|
||||||
|
this._lastValidSearch = (query.match(/(.*)[\.#][^\.# ]{0,}$/) ||
|
||||||
|
query.match(/(.*[\s>+])[a-zA-Z][^\.# ]{0,}$/) ||
|
||||||
|
["",""])[1];
|
||||||
|
return;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
aEvent.preventDefault();
|
||||||
|
aEvent.stopPropagation();
|
||||||
|
if (this._searchResults.length > 0) {
|
||||||
|
this.callback(this._searchResults[this._searchIndex]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles keypress and mouse click on the suggestions richlistbox.
|
||||||
|
*/
|
||||||
|
_onListBoxKeypress: function SelectorSearch__onListBoxKeypress(aEvent) {
|
||||||
|
switch(aEvent.keyCode || aEvent.button) {
|
||||||
|
case aEvent.DOM_VK_ENTER:
|
||||||
|
case aEvent.DOM_VK_RETURN:
|
||||||
|
case aEvent.DOM_VK_TAB:
|
||||||
|
case 0: // left mouse button
|
||||||
|
aEvent.stopPropagation();
|
||||||
|
aEvent.preventDefault();
|
||||||
|
this.searchBox.value = this.searchPopup.selectedItem.label;
|
||||||
|
this.searchBox.focus();
|
||||||
|
this._onHTMLSearch();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case aEvent.DOM_VK_UP:
|
||||||
|
if (this.searchPopup.selectedIndex == 0) {
|
||||||
|
this.searchPopup.selectedIndex = -1;
|
||||||
|
aEvent.stopPropagation();
|
||||||
|
aEvent.preventDefault();
|
||||||
|
this.searchBox.focus();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let index = this.searchPopup.selectedIndex;
|
||||||
|
this.searchBox.value = this.searchPopup.getItemAtIndex(index - 1).label;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case aEvent.DOM_VK_DOWN:
|
||||||
|
if (this.searchPopup.selectedIndex == this.searchPopup.itemCount - 1) {
|
||||||
|
this.searchPopup.selectedIndex = -1;
|
||||||
|
aEvent.stopPropagation();
|
||||||
|
aEvent.preventDefault();
|
||||||
|
this.searchBox.focus();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let index = this.searchPopup.selectedIndex;
|
||||||
|
this.searchBox.value = this.searchPopup.getItemAtIndex(index + 1).label;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case aEvent.DOM_VK_BACK_SPACE:
|
||||||
|
aEvent.stopPropagation();
|
||||||
|
aEvent.preventDefault();
|
||||||
|
this.searchBox.focus();
|
||||||
|
if (this.searchBox.selectionStart > 0) {
|
||||||
|
this.searchBox.value =
|
||||||
|
this.searchBox.value.substring(0, this.searchBox.selectionStart - 1);
|
||||||
|
}
|
||||||
|
this._lastToLastValidSearch = null;
|
||||||
|
let query = this.searchBox.value;
|
||||||
|
this._lastValidSearch = (query.match(/(.*)[\.#][^\.# ]{0,}$/) ||
|
||||||
|
query.match(/(.*[\s>+])[a-zA-Z][^\.# ]{0,}$/) ||
|
||||||
|
["",""])[1];
|
||||||
|
this._onHTMLSearch();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Populates the suggestions list and show the suggestion popup.
|
||||||
|
*/
|
||||||
|
_showPopup: function SelectorSearch__showPopup(aList, aFirstPart) {
|
||||||
|
// Sort alphabetically in increaseing order.
|
||||||
|
aList = aList.sort();
|
||||||
|
// Sort based on count= in decreasing order.
|
||||||
|
aList = aList.sort(function([a1,a2], [b1,b2]) {
|
||||||
|
return a2 < b2;
|
||||||
|
});
|
||||||
|
|
||||||
|
let total = 0;
|
||||||
|
let query = this.searchBox.value;
|
||||||
|
let toLowerCase = false;
|
||||||
|
let items = [];
|
||||||
|
// In case of tagNames, change the case to small.
|
||||||
|
if (query.match(/.*[\.#][^\.#]{0,}$/) == null) {
|
||||||
|
toLowerCase = true;
|
||||||
|
}
|
||||||
|
for (let [value, count] of aList) {
|
||||||
|
// for cases like 'div ' or 'div >' or 'div+'
|
||||||
|
if (query.match(/[\s>+]$/)) {
|
||||||
|
value = query + value;
|
||||||
|
}
|
||||||
|
// for cases like 'div #a' or 'div .a' or 'div > d' and likewise
|
||||||
|
else if (query.match(/[\s>+][\.#a-zA-Z][^\s>+\.#]*$/)) {
|
||||||
|
let lastPart = query.match(/[\s>+][\.#a-zA-Z][^>\s+\.#]*$/)[0];
|
||||||
|
value = query.slice(0, -1 * lastPart.length + 1) + value;
|
||||||
|
}
|
||||||
|
// for cases like 'div.class' or '#foo.bar' and likewise
|
||||||
|
else if (query.match(/[a-zA-Z][#\.][^#\.\s+>]*$/)) {
|
||||||
|
let lastPart = query.match(/[a-zA-Z][#\.][^#\.\s>+]*$/)[0];
|
||||||
|
value = query.slice(0, -1 * lastPart.length + 1) + value;
|
||||||
|
}
|
||||||
|
let item = {
|
||||||
|
preLabel: query,
|
||||||
|
label: value,
|
||||||
|
count: count
|
||||||
|
};
|
||||||
|
if (toLowerCase) {
|
||||||
|
item.label = value.toLowerCase();
|
||||||
|
}
|
||||||
|
items.unshift(item);
|
||||||
|
if (++total > MAX_SUGGESTIONS - 1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (total > 0) {
|
||||||
|
this.searchPopup.setItems(items);
|
||||||
|
this.searchPopup.openPopup(this.searchBox);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.searchPopup.hidePopup();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Suggests classes,ids and tags based on the user input as user types in the
|
||||||
|
* searchbox.
|
||||||
|
*/
|
||||||
|
showSuggestions: function SelectorSearch_showSuggestions() {
|
||||||
|
let query = this.searchBox.value;
|
||||||
|
if (this._lastValidSearch != "" &&
|
||||||
|
this._lastToLastValidSearch != this._lastValidSearch) {
|
||||||
|
this._searchSuggestions = {
|
||||||
|
ids: new Map(),
|
||||||
|
classes: new Map(),
|
||||||
|
tags: new Map(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let nodes = [];
|
||||||
|
try {
|
||||||
|
nodes = this.doc.querySelectorAll(this._lastValidSearch);
|
||||||
|
} catch (ex) {}
|
||||||
|
for (let node of nodes) {
|
||||||
|
this._searchSuggestions.ids.set(node.id, 1);
|
||||||
|
this._searchSuggestions.tags
|
||||||
|
.set(node.tagName,
|
||||||
|
(this._searchSuggestions.tags.get(node.tagName) || 0) + 1);
|
||||||
|
for (let className of node.classList) {
|
||||||
|
this._searchSuggestions.classes
|
||||||
|
.set(className,
|
||||||
|
(this._searchSuggestions.classes.get(className) || 0) + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._lastToLastValidSearch = this._lastValidSearch;
|
||||||
|
}
|
||||||
|
else if (this._lastToLastValidSearch != this._lastValidSearch) {
|
||||||
|
this._searchSuggestions = {
|
||||||
|
ids: new Map(),
|
||||||
|
classes: new Map(),
|
||||||
|
tags: new Map(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (query.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let nodes = null;
|
||||||
|
if (this.state == this.States.CLASS) {
|
||||||
|
nodes = this.doc.querySelectorAll("[class]");
|
||||||
|
for (let node of nodes) {
|
||||||
|
for (let className of node.classList) {
|
||||||
|
this._searchSuggestions.classes
|
||||||
|
.set(className,
|
||||||
|
(this._searchSuggestions.classes.get(className) || 0) + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (this.state == this.States.ID) {
|
||||||
|
nodes = this.doc.querySelectorAll("[id]");
|
||||||
|
for (let node of nodes) {
|
||||||
|
this._searchSuggestions.ids.set(node.id, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (this.state == this.States.TAG) {
|
||||||
|
nodes = this.doc.getElementsByTagName("*");
|
||||||
|
for (let node of nodes) {
|
||||||
|
this._searchSuggestions.tags
|
||||||
|
.set(node.tagName,
|
||||||
|
(this._searchSuggestions.tags.get(node.tagName) || 0) + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._lastToLastValidSearch = this._lastValidSearch;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter the suggestions based on search box value.
|
||||||
|
let result = [];
|
||||||
|
let firstPart = "";
|
||||||
|
if (this.state == this.States.TAG) {
|
||||||
|
// gets the tag that is being completed. For ex. 'div.foo > s' returns 's',
|
||||||
|
// 'di' returns 'di' and likewise.
|
||||||
|
firstPart = (query.match(/[\s>+]?([a-zA-Z]*)$/) || ["",query])[1];
|
||||||
|
for (let [tag, count] of this._searchSuggestions.tags) {
|
||||||
|
if (tag.toLowerCase().startsWith(firstPart.toLowerCase())) {
|
||||||
|
result.push([tag, count]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (this.state == this.States.CLASS) {
|
||||||
|
// gets the class that is being completed. For ex. '.foo.b' returns 'b'
|
||||||
|
firstPart = query.match(/\.([^\.]*)$/)[1];
|
||||||
|
for (let [className, count] of this._searchSuggestions.classes) {
|
||||||
|
if (className.startsWith(firstPart)) {
|
||||||
|
result.push(["." + className, count]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
firstPart = "." + firstPart;
|
||||||
|
}
|
||||||
|
else if (this.state == this.States.ID) {
|
||||||
|
// gets the id that is being completed. For ex. '.foo#b' returns 'b'
|
||||||
|
firstPart = query.match(/#([^#]*)$/)[1];
|
||||||
|
for (let [id, count] of this._searchSuggestions.ids) {
|
||||||
|
if (id.startsWith(firstPart)) {
|
||||||
|
result.push(["#" + id, 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
firstPart = "#" + firstPart;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._showPopup(result, firstPart);
|
||||||
|
},
|
||||||
|
};
|
@ -5,3 +5,58 @@
|
|||||||
#inspector-sidebar {
|
#inspector-sidebar {
|
||||||
min-width: 250px;
|
min-width: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#inspector-searchbox-panel {
|
||||||
|
-moz-appearance: none !important;
|
||||||
|
border: 1px solid hsl(210,24%,10%);
|
||||||
|
box-shadow: 0 1px 0 hsla(209,29%,72%,.25) inset;
|
||||||
|
background-color: transparent;
|
||||||
|
background-image: linear-gradient(to bottom, hsla(209,18%,18%,0.9), hsl(210,24%,16%));
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#searchbox-panel-listbox {
|
||||||
|
-moz-appearance: none !important;
|
||||||
|
background-color: rgba(0,0,0,0);
|
||||||
|
border-width: 0px !important;
|
||||||
|
width: 250px;
|
||||||
|
max-width: 250px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#searchbox-panel-listbox > richlistitem,
|
||||||
|
#searchbox-panel-listbox > richlistitem[selected] {
|
||||||
|
overflow-x: hidden;
|
||||||
|
width: 100%;
|
||||||
|
background-color: rgba(0,0,0,0);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#searchbox-panel-listbox:focus > richlistitem[selected],
|
||||||
|
#searchbox-panel-listbox > richlistitem:hover {
|
||||||
|
background-color: rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#searchbox-panel-listbox > richlistitem[selected] > .autocomplete-value {
|
||||||
|
color: hsl(200,100%,60%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#searchbox-panel-listbox > richlistitem > label {
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
#searchbox-panel-listbox > richlistitem > .initial-value {
|
||||||
|
max-width: 130px;
|
||||||
|
margin-left: 15px;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#searchbox-panel-listbox > richlistitem > .autocomplete-value {
|
||||||
|
max-width: 150px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 1px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#searchbox-panel-listbox > richlistitem > .autocomplete-count {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
@ -38,6 +38,10 @@ _BROWSER_FILES = \
|
|||||||
browser_inspector_bug_817558_delete_node.js \
|
browser_inspector_bug_817558_delete_node.js \
|
||||||
browser_inspector_bug_650804_search.js \
|
browser_inspector_bug_650804_search.js \
|
||||||
browser_inspector_bug_650804_search.html \
|
browser_inspector_bug_650804_search.html \
|
||||||
|
browser_inspector_bug_831693_input_suggestion.js \
|
||||||
|
browser_inspector_bug_831693_searchbox_panel_navigation.js \
|
||||||
|
browser_inspector_bug_831693_combinator_suggestions.js \
|
||||||
|
browser_inspector_bug_831693_search_suggestions.html \
|
||||||
browser_inspector_bug_840156_destroy_after_navigation.js \
|
browser_inspector_bug_840156_destroy_after_navigation.js \
|
||||||
browser_inspector_bug_835722_infobar_reappears.js \
|
browser_inspector_bug_835722_infobar_reappears.js \
|
||||||
head.js \
|
head.js \
|
||||||
|
@ -20,7 +20,7 @@ function test()
|
|||||||
["v", "d1", true],
|
["v", "d1", true],
|
||||||
["VK_DOWN", "d2", true],
|
["VK_DOWN", "d2", true],
|
||||||
["VK_ENTER", "d1", true],
|
["VK_ENTER", "d1", true],
|
||||||
[".", "d1", true],
|
[".", "d1", false],
|
||||||
["c", "d1", false],
|
["c", "d1", false],
|
||||||
["1", "d2", true],
|
["1", "d2", true],
|
||||||
["VK_DOWN", "d2", true],
|
["VK_DOWN", "d2", true],
|
||||||
@ -30,7 +30,7 @@ function test()
|
|||||||
["VK_BACK_SPACE", "d1", false],
|
["VK_BACK_SPACE", "d1", false],
|
||||||
["VK_BACK_SPACE", "d1", false],
|
["VK_BACK_SPACE", "d1", false],
|
||||||
["VK_BACK_SPACE", "d1", true],
|
["VK_BACK_SPACE", "d1", true],
|
||||||
[".", "d1", true],
|
[".", "d1", false],
|
||||||
["c", "d1", false],
|
["c", "d1", false],
|
||||||
["1", "d2", true],
|
["1", "d2", true],
|
||||||
["VK_DOWN", "s2", true],
|
["VK_DOWN", "s2", true],
|
||||||
|
@ -0,0 +1,113 @@
|
|||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
|
function test()
|
||||||
|
{
|
||||||
|
waitForExplicitFinish();
|
||||||
|
|
||||||
|
let inspector, searchBox, state, popup;
|
||||||
|
|
||||||
|
// The various states of the inspector: [key, suggestions array]
|
||||||
|
// [
|
||||||
|
// what key to press,
|
||||||
|
// suggestions array with count [
|
||||||
|
// [suggestion1, count1], [suggestion2] ...
|
||||||
|
// ] count can be left to represent 1
|
||||||
|
// ]
|
||||||
|
let keyStates = [
|
||||||
|
["d", [["div", 4]]],
|
||||||
|
["i", [["div", 4]]],
|
||||||
|
["v", []],
|
||||||
|
[" ", [["div div", 2], ["div span", 2]]],
|
||||||
|
[">", [["div >div", 2], ["div >span", 2]]],
|
||||||
|
["VK_BACK_SPACE", [["div div", 2], ["div span", 2]]],
|
||||||
|
["+", [["div +span"]]],
|
||||||
|
["VK_BACK_SPACE", [["div div", 2], ["div span", 2]]],
|
||||||
|
["VK_BACK_SPACE", []],
|
||||||
|
["VK_BACK_SPACE", [["div", 4]]],
|
||||||
|
["VK_BACK_SPACE", [["div", 4]]],
|
||||||
|
["VK_BACK_SPACE", []],
|
||||||
|
["p", []],
|
||||||
|
[" ", [["p strong"]]],
|
||||||
|
["+", [["p +button"], ["p +p"]]],
|
||||||
|
["b", [["p +button"]]],
|
||||||
|
["u", [["p +button"]]],
|
||||||
|
["t", [["p +button"]]],
|
||||||
|
["t", [["p +button"]]],
|
||||||
|
["o", [["p +button"]]],
|
||||||
|
["n", []],
|
||||||
|
["+", [["p +button+p"]]],
|
||||||
|
];
|
||||||
|
|
||||||
|
gBrowser.selectedTab = gBrowser.addTab();
|
||||||
|
gBrowser.selectedBrowser.addEventListener("load", function onload() {
|
||||||
|
gBrowser.selectedBrowser.removeEventListener("load", onload, true);
|
||||||
|
waitForFocus(setupTest, content);
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
content.location = "http://mochi.test:8888/browser/browser/devtools/inspector/test/browser_inspector_bug_831693_search_suggestions.html";
|
||||||
|
|
||||||
|
function $(id) {
|
||||||
|
if (id == null) return null;
|
||||||
|
return content.document.getElementById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupTest()
|
||||||
|
{
|
||||||
|
openInspector(startTest);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startTest(aInspector)
|
||||||
|
{
|
||||||
|
inspector = aInspector;
|
||||||
|
searchBox =
|
||||||
|
inspector.panelWin.document.getElementById("inspector-searchbox");
|
||||||
|
popup = inspector.searchSuggestions.searchPopup;
|
||||||
|
|
||||||
|
focusSearchBoxUsingShortcut(inspector.panelWin, function() {
|
||||||
|
searchBox.addEventListener("command", checkState, true);
|
||||||
|
checkStateAndMoveOn(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkStateAndMoveOn(index) {
|
||||||
|
if (index == keyStates.length) {
|
||||||
|
finishUp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let [key, suggestions] = keyStates[index];
|
||||||
|
state = index;
|
||||||
|
|
||||||
|
info("pressing key " + key + " to get suggestions " +
|
||||||
|
JSON.stringify(suggestions));
|
||||||
|
EventUtils.synthesizeKey(key, {}, inspector.panelWin);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkState(event) {
|
||||||
|
executeSoon(function() {
|
||||||
|
let [key, suggestions] = keyStates[state];
|
||||||
|
let actualSuggestions = popup.getItems();
|
||||||
|
is(popup._panel.state == "open" || popup._panel.state == "showing"
|
||||||
|
? actualSuggestions.length: 0, suggestions.length,
|
||||||
|
"There are expected number of suggestions at " + state + "th step.");
|
||||||
|
actualSuggestions = actualSuggestions.reverse();
|
||||||
|
for (let i = 0; i < suggestions.length; i++) {
|
||||||
|
is(suggestions[i][0], actualSuggestions[i].label,
|
||||||
|
"The suggestion at " + i + "th index for " + state +
|
||||||
|
"th step is correct.")
|
||||||
|
is(suggestions[i][1] || 1, actualSuggestions[i].count,
|
||||||
|
"The count for suggestion at " + i + "th index for " + state +
|
||||||
|
"th step is correct.")
|
||||||
|
}
|
||||||
|
checkStateAndMoveOn(state + 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishUp() {
|
||||||
|
searchBox = null;
|
||||||
|
popup = null;
|
||||||
|
gBrowser.removeCurrentTab();
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,115 @@
|
|||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
|
function test()
|
||||||
|
{
|
||||||
|
waitForExplicitFinish();
|
||||||
|
|
||||||
|
let inspector, searchBox, state, popup;
|
||||||
|
|
||||||
|
// The various states of the inspector: [key, suggestions array]
|
||||||
|
// [
|
||||||
|
// what key to press,
|
||||||
|
// suggestions array with count [
|
||||||
|
// [suggestion1, count1], [suggestion2] ...
|
||||||
|
// ] count can be left to represent 1
|
||||||
|
// ]
|
||||||
|
let keyStates = [
|
||||||
|
["d", [["div", 2]]],
|
||||||
|
["i", [["div", 2]]],
|
||||||
|
["v", []],
|
||||||
|
[".", [["div.c1"]]],
|
||||||
|
["VK_BACK_SPACE", []],
|
||||||
|
["#", [["div#d1"], ["div#d2"]]],
|
||||||
|
["VK_BACK_SPACE", []],
|
||||||
|
["VK_BACK_SPACE", [["div", 2]]],
|
||||||
|
["VK_BACK_SPACE", [["div", 2]]],
|
||||||
|
["VK_BACK_SPACE", []],
|
||||||
|
[".", [[".c1", 3], [".c2"]]],
|
||||||
|
["c", [[".c1", 3], [".c2"]]],
|
||||||
|
["2", []],
|
||||||
|
["VK_BACK_SPACE", [[".c1", 3], [".c2"]]],
|
||||||
|
["1", []],
|
||||||
|
["#", [["#d2"], ["#p1"], ["#s2"]]],
|
||||||
|
["VK_BACK_SPACE", []],
|
||||||
|
["VK_BACK_SPACE", [[".c1", 3], [".c2"]]],
|
||||||
|
["VK_BACK_SPACE", [[".c1", 3], [".c2"]]],
|
||||||
|
["VK_BACK_SPACE", []],
|
||||||
|
["#", [["#b1"], ["#d1"], ["#d2"], ["#p1"], ["#p2"], ["#p3"], ["#s1"], ["#s2"]]],
|
||||||
|
["p", [["#p1"], ["#p2"], ["#p3"]]],
|
||||||
|
["VK_BACK_SPACE", [["#b1"], ["#d1"], ["#d2"], ["#p1"], ["#p2"], ["#p3"], ["#s1"], ["#s2"]]],
|
||||||
|
["VK_BACK_SPACE", []],
|
||||||
|
];
|
||||||
|
|
||||||
|
gBrowser.selectedTab = gBrowser.addTab();
|
||||||
|
gBrowser.selectedBrowser.addEventListener("load", function onload() {
|
||||||
|
gBrowser.selectedBrowser.removeEventListener("load", onload, true);
|
||||||
|
waitForFocus(setupTest, content);
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
content.location = "http://mochi.test:8888/browser/browser/devtools/inspector/test/browser_inspector_bug_650804_search.html";
|
||||||
|
|
||||||
|
function $(id) {
|
||||||
|
if (id == null) return null;
|
||||||
|
return content.document.getElementById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupTest()
|
||||||
|
{
|
||||||
|
openInspector(startTest);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startTest(aInspector)
|
||||||
|
{
|
||||||
|
inspector = aInspector;
|
||||||
|
searchBox =
|
||||||
|
inspector.panelWin.document.getElementById("inspector-searchbox");
|
||||||
|
popup = inspector.searchSuggestions.searchPopup;
|
||||||
|
|
||||||
|
focusSearchBoxUsingShortcut(inspector.panelWin, function() {
|
||||||
|
searchBox.addEventListener("command", checkState, true);
|
||||||
|
checkStateAndMoveOn(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkStateAndMoveOn(index) {
|
||||||
|
if (index == keyStates.length) {
|
||||||
|
finishUp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let [key, suggestions] = keyStates[index];
|
||||||
|
state = index;
|
||||||
|
|
||||||
|
info("pressing key " + key + " to get suggestions " +
|
||||||
|
JSON.stringify(suggestions));
|
||||||
|
EventUtils.synthesizeKey(key, {}, inspector.panelWin);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkState(event) {
|
||||||
|
executeSoon(function() {
|
||||||
|
let [key, suggestions] = keyStates[state];
|
||||||
|
let actualSuggestions = popup.getItems();
|
||||||
|
is(popup._panel.state == "open" || popup._panel.state == "showing"
|
||||||
|
? actualSuggestions.length: 0, suggestions.length,
|
||||||
|
"There are expected number of suggestions at " + state + "th step.");
|
||||||
|
actualSuggestions = actualSuggestions.reverse();
|
||||||
|
for (let i = 0; i < suggestions.length; i++) {
|
||||||
|
is(suggestions[i][0], actualSuggestions[i].label,
|
||||||
|
"The suggestion at " + i + "th index for " + state +
|
||||||
|
"th step is correct.")
|
||||||
|
is(suggestions[i][1] || 1, actualSuggestions[i].count,
|
||||||
|
"The count for suggestion at " + i + "th index for " + state +
|
||||||
|
"th step is correct.")
|
||||||
|
}
|
||||||
|
checkStateAndMoveOn(state + 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishUp() {
|
||||||
|
searchBox = null;
|
||||||
|
popup = null;
|
||||||
|
gBrowser.removeCurrentTab();
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Inspector Search Box Test</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="d1">
|
||||||
|
<div class="l1">
|
||||||
|
<div id="d2" class="c1">Hello, I'm nested div</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span id="s1">Hello, I'm a span
|
||||||
|
<div class="l1">
|
||||||
|
<span>Hi I am a nested span</span>
|
||||||
|
<span class="s4">Hi I am a nested classed span</span>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
<span class="c1" id="s2">And me</span>
|
||||||
|
|
||||||
|
<p class="c1" id="p1">.someclass</p>
|
||||||
|
<p id="p2">#someid</p>
|
||||||
|
<button id="b1" disabled>button[disabled]</button>
|
||||||
|
<p id="p3" class="c2"><strong>p>strong</strong></p>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,125 @@
|
|||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
|
function test()
|
||||||
|
{
|
||||||
|
waitForExplicitFinish();
|
||||||
|
|
||||||
|
let inspector, searchBox, state, panel;
|
||||||
|
|
||||||
|
// The various states of the inspector: [key, query]
|
||||||
|
// [
|
||||||
|
// what key to press,
|
||||||
|
// what should be the text in the searchbox
|
||||||
|
// ]
|
||||||
|
let keyStates = [
|
||||||
|
["d", "d"],
|
||||||
|
["i", "di"],
|
||||||
|
["v", "div"],
|
||||||
|
[".", "div."],
|
||||||
|
["VK_UP", "div.c1"],
|
||||||
|
["VK_DOWN", "div.l1"],
|
||||||
|
["VK_DOWN", "div.l1"],
|
||||||
|
["VK_BACK_SPACE", "div.l"],
|
||||||
|
["VK_UP", "div.l1"],
|
||||||
|
["VK_UP", "div.l1"],
|
||||||
|
[" ", "div.l1 "],
|
||||||
|
["VK_UP", "div.l1 DIV"],
|
||||||
|
["VK_UP", "div.l1 DIV"],
|
||||||
|
[".", "div.l1 DIV."],
|
||||||
|
["VK_TAB", "div.l1 DIV.c1"],
|
||||||
|
["VK_BACK_SPACE", "div.l1 DIV.c"],
|
||||||
|
["VK_BACK_SPACE", "div.l1 DIV."],
|
||||||
|
["VK_BACK_SPACE", "div.l1 DIV"],
|
||||||
|
["VK_BACK_SPACE", "div.l1 DI"],
|
||||||
|
["VK_BACK_SPACE", "div.l1 D"],
|
||||||
|
["VK_BACK_SPACE", "div.l1 "],
|
||||||
|
["VK_UP", "div.l1 DIV"],
|
||||||
|
["VK_BACK_SPACE", "div.l1 DI"],
|
||||||
|
["VK_BACK_SPACE", "div.l1 D"],
|
||||||
|
["VK_BACK_SPACE", "div.l1 "],
|
||||||
|
["VK_UP", "div.l1 DIV"],
|
||||||
|
["VK_UP", "div.l1 DIV"],
|
||||||
|
["VK_TAB", "div.l1 DIV"],
|
||||||
|
["VK_BACK_SPACE", "div.l1 DI"],
|
||||||
|
["VK_BACK_SPACE", "div.l1 D"],
|
||||||
|
["VK_BACK_SPACE", "div.l1 "],
|
||||||
|
["VK_DOWN", "div.l1 DIV"],
|
||||||
|
["VK_DOWN", "div.l1 SPAN"],
|
||||||
|
["VK_DOWN", "div.l1 SPAN"],
|
||||||
|
["VK_BACK_SPACE", "div.l1 SPA"],
|
||||||
|
["VK_BACK_SPACE", "div.l1 SP"],
|
||||||
|
["VK_BACK_SPACE", "div.l1 S"],
|
||||||
|
["VK_BACK_SPACE", "div.l1 "],
|
||||||
|
["VK_BACK_SPACE", "div.l1"],
|
||||||
|
["VK_BACK_SPACE", "div.l"],
|
||||||
|
["VK_BACK_SPACE", "div."],
|
||||||
|
["VK_BACK_SPACE", "div"],
|
||||||
|
["VK_BACK_SPACE", "di"],
|
||||||
|
["VK_BACK_SPACE", "d"],
|
||||||
|
["VK_BACK_SPACE", ""],
|
||||||
|
];
|
||||||
|
|
||||||
|
gBrowser.selectedTab = gBrowser.addTab();
|
||||||
|
gBrowser.selectedBrowser.addEventListener("load", function onload() {
|
||||||
|
gBrowser.selectedBrowser.removeEventListener("load", onload, true);
|
||||||
|
waitForFocus(setupTest, content);
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
content.location = "http://mochi.test:8888/browser/browser/devtools/inspector/test/browser_inspector_bug_831693_search_suggestions.html";
|
||||||
|
|
||||||
|
function $(id) {
|
||||||
|
if (id == null) return null;
|
||||||
|
return content.document.getElementById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupTest()
|
||||||
|
{
|
||||||
|
openInspector(startTest);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startTest(aInspector)
|
||||||
|
{
|
||||||
|
inspector = aInspector;
|
||||||
|
searchBox =
|
||||||
|
inspector.panelWin.document.getElementById("inspector-searchbox");
|
||||||
|
panel = inspector.searchSuggestions.searchPopup._list;
|
||||||
|
|
||||||
|
focusSearchBoxUsingShortcut(inspector.panelWin, function() {
|
||||||
|
searchBox.addEventListener("keypress", checkState, true);
|
||||||
|
panel.addEventListener("keypress", checkState, true);
|
||||||
|
checkStateAndMoveOn(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkStateAndMoveOn(index) {
|
||||||
|
if (index == keyStates.length) {
|
||||||
|
finishUp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let [key, query] = keyStates[index];
|
||||||
|
state = index;
|
||||||
|
|
||||||
|
info("pressing key " + key + " to get searchbox value as " + query);
|
||||||
|
EventUtils.synthesizeKey(key, {}, inspector.panelWin);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkState(event) {
|
||||||
|
// Using setTimout as the "command" event fires at delay after keypress
|
||||||
|
window.setTimeout(function() {
|
||||||
|
let [key, query] = keyStates[state];
|
||||||
|
|
||||||
|
is(searchBox.value, query, "The suggestion at " + state + "th step on " +
|
||||||
|
"pressing " + key + " key is correct.")
|
||||||
|
checkStateAndMoveOn(state + 1);
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishUp() {
|
||||||
|
searchBox = null;
|
||||||
|
panel = null;
|
||||||
|
gBrowser.removeCurrentTab();
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
@ -7,19 +7,10 @@ const Cu = Components.utils;
|
|||||||
|
|
||||||
// The XUL and XHTML namespace.
|
// The XUL and XHTML namespace.
|
||||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||||
const XHTML_NS = "http://www.w3.org/1999/xhtml";
|
|
||||||
|
|
||||||
const HUD_STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
|
|
||||||
|
|
||||||
|
|
||||||
Cu.import("resource://gre/modules/Services.jsm");
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||||
|
|
||||||
XPCOMUtils.defineLazyGetter(this, "stringBundle", function () {
|
|
||||||
return Services.strings.createBundle(HUD_STRINGS_URI);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
this.EXPORTED_SYMBOLS = ["AutocompletePopup"];
|
this.EXPORTED_SYMBOLS = ["AutocompletePopup"];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -28,21 +19,50 @@ this.EXPORTED_SYMBOLS = ["AutocompletePopup"];
|
|||||||
* @constructor
|
* @constructor
|
||||||
* @param nsIDOMDocument aDocument
|
* @param nsIDOMDocument aDocument
|
||||||
* The document you want the popup attached to.
|
* The document you want the popup attached to.
|
||||||
|
* @param Object aOptions
|
||||||
|
* 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.
|
||||||
|
* - autoSelect {Boolean} Boolean to allow the first entry of the popup
|
||||||
|
* panel to be automatically selected when the popup shows.
|
||||||
|
* - fixedWidth {Boolean} Boolean to control dynamic width of the popup.
|
||||||
|
* - direction {String} The direction of the text in the panel. rtl or 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 the richlistitems.
|
||||||
*/
|
*/
|
||||||
this.AutocompletePopup = function AutocompletePopup(aDocument)
|
this.AutocompletePopup =
|
||||||
|
function AutocompletePopup(aDocument,
|
||||||
|
aOptions = {fixedWidth: false,
|
||||||
|
autoSelect: false,
|
||||||
|
position: "after_start",
|
||||||
|
panelId: "devtools_autoCompletePopup"})
|
||||||
{
|
{
|
||||||
this._document = aDocument;
|
this._document = aDocument;
|
||||||
|
|
||||||
|
this.fixedWidth = aOptions.fixedWidth;
|
||||||
|
this.autoSelect = aOptions.autoSelect;
|
||||||
|
this.position = aOptions.position;
|
||||||
|
this.direction = aOptions.direction;
|
||||||
|
|
||||||
|
this.onSelect = aOptions.onSelect;
|
||||||
|
this.onClick = aOptions.onClick;
|
||||||
|
this.onKeypress = aOptions.onKeypress;
|
||||||
|
|
||||||
|
let id = aOptions.panelId;
|
||||||
// Reuse the existing popup elements.
|
// Reuse the existing popup elements.
|
||||||
this._panel = this._document.getElementById("webConsole_autocompletePopup");
|
this._panel = this._document.getElementById(id);
|
||||||
if (!this._panel) {
|
if (!this._panel) {
|
||||||
this._panel = this._document.createElementNS(XUL_NS, "panel");
|
this._panel = this._document.createElementNS(XUL_NS, "panel");
|
||||||
this._panel.setAttribute("id", "webConsole_autocompletePopup");
|
this._panel.setAttribute("id", id);
|
||||||
this._panel.setAttribute("label",
|
this._panel.setAttribute("class", "devtools-autocomplete-popup");
|
||||||
stringBundle.GetStringFromName("Autocomplete.label"));
|
|
||||||
this._panel.setAttribute("noautofocus", "true");
|
this._panel.setAttribute("noautofocus", "true");
|
||||||
this._panel.setAttribute("ignorekeys", "true");
|
|
||||||
this._panel.setAttribute("level", "top");
|
this._panel.setAttribute("level", "top");
|
||||||
|
if (!aOptions.onKeypress) {
|
||||||
|
this._panel.setAttribute("ignorekeys", "true");
|
||||||
|
}
|
||||||
|
|
||||||
let mainPopupSet = this._document.getElementById("mainPopupSet");
|
let mainPopupSet = this._document.getElementById("mainPopupSet");
|
||||||
if (mainPopupSet) {
|
if (mainPopupSet) {
|
||||||
@ -51,38 +71,29 @@ this.AutocompletePopup = function AutocompletePopup(aDocument)
|
|||||||
else {
|
else {
|
||||||
this._document.documentElement.appendChild(this._panel);
|
this._document.documentElement.appendChild(this._panel);
|
||||||
}
|
}
|
||||||
|
this._list = null;
|
||||||
this._list = this._document.createElementNS(XUL_NS, "richlistbox");
|
|
||||||
this._list.flex = 1;
|
|
||||||
this._panel.appendChild(this._list);
|
|
||||||
|
|
||||||
// Open and hide the panel, so we initialize the API of the richlistbox.
|
|
||||||
this._panel.width = 1;
|
|
||||||
this._panel.height = 1;
|
|
||||||
this._panel.openPopup(null, "overlap", 0, 0, false, false);
|
|
||||||
this._panel.hidePopup();
|
|
||||||
this._panel.width = "";
|
|
||||||
this._panel.height = "";
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this._list = this._panel.firstChild;
|
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.popup, 0, 0);
|
||||||
|
this._panel.hidePopup();
|
||||||
}
|
}
|
||||||
|
|
||||||
AutocompletePopup.prototype = {
|
this._list.flex = 1;
|
||||||
_document: null,
|
this._list.setAttribute("seltype", "single");
|
||||||
_panel: null,
|
|
||||||
_list: null,
|
if (aOptions.listBoxId) {
|
||||||
|
this._list.setAttribute("id", aOptions.listBoxId);
|
||||||
|
}
|
||||||
|
this._list.setAttribute("class", "devtools-autocomplete-listbox");
|
||||||
|
|
||||||
/**
|
|
||||||
* Open the autocomplete popup panel.
|
|
||||||
*
|
|
||||||
* @param nsIDOMNode aAnchor
|
|
||||||
* Optional node to anchor the panel to.
|
|
||||||
*/
|
|
||||||
openPopup: function AP_openPopup(aAnchor)
|
|
||||||
{
|
|
||||||
this._panel.openPopup(aAnchor, "after_start", 0, 0, false, false);
|
|
||||||
|
|
||||||
if (this.onSelect) {
|
if (this.onSelect) {
|
||||||
this._list.addEventListener("select", this.onSelect, false);
|
this._list.addEventListener("select", this.onSelect, false);
|
||||||
@ -92,7 +103,37 @@ AutocompletePopup.prototype = {
|
|||||||
this._list.addEventListener("click", this.onClick, false);
|
this._list.addEventListener("click", this.onClick, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.onKeypress) {
|
||||||
|
this._list.addEventListener("keypress", this.onKeypress, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AutocompletePopup.prototype = {
|
||||||
|
_document: null,
|
||||||
|
_panel: null,
|
||||||
|
_list: null,
|
||||||
|
|
||||||
|
// Event handlers.
|
||||||
|
onSelect: null,
|
||||||
|
onClick: null,
|
||||||
|
onKeypress: null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the autocomplete popup panel.
|
||||||
|
*
|
||||||
|
* @param nsIDOMNode aAnchor
|
||||||
|
* Optional node to anchor the panel to.
|
||||||
|
*/
|
||||||
|
openPopup: function AP_openPopup(aAnchor)
|
||||||
|
{
|
||||||
|
this._panel.openPopup(aAnchor, this.position, 0, 0);
|
||||||
|
|
||||||
|
if (this.autoSelect) {
|
||||||
|
this.selectFirstItem();
|
||||||
|
}
|
||||||
|
if (!this.fixedWidth) {
|
||||||
this._updateSize();
|
this._updateSize();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -101,14 +142,6 @@ AutocompletePopup.prototype = {
|
|||||||
hidePopup: function AP_hidePopup()
|
hidePopup: function AP_hidePopup()
|
||||||
{
|
{
|
||||||
this._panel.hidePopup();
|
this._panel.hidePopup();
|
||||||
|
|
||||||
if (this.onSelect) {
|
|
||||||
this._list.removeEventListener("select", this.onSelect, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.onClick) {
|
|
||||||
this._list.removeEventListener("click", this.onClick, false);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -131,11 +164,35 @@ AutocompletePopup.prototype = {
|
|||||||
}
|
}
|
||||||
this.clearItems();
|
this.clearItems();
|
||||||
|
|
||||||
|
if (this.onSelect) {
|
||||||
|
this._list.removeEventListener("select", this.onSelect, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.onClick) {
|
||||||
|
this._list.removeEventListener("click", this.onClick, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.onKeypress) {
|
||||||
|
this._list.removeEventListener("keypress", this.onKeypress, false);
|
||||||
|
}
|
||||||
|
|
||||||
this._document = null;
|
this._document = null;
|
||||||
this._list = null;
|
this._list = null;
|
||||||
this._panel = null;
|
this._panel = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the autocomplete items array.
|
||||||
|
*
|
||||||
|
* @param Number aIndex The index of the item what is wanted.
|
||||||
|
*
|
||||||
|
* @return The autocomplete item at index aIndex.
|
||||||
|
*/
|
||||||
|
getItemAtIndex: function AP_getItemAtIndex(aIndex)
|
||||||
|
{
|
||||||
|
return this._list.getItemAtIndex(aIndex)._autocompleteItem;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the autocomplete items array.
|
* Get the autocomplete items array.
|
||||||
*
|
*
|
||||||
@ -166,9 +223,27 @@ AutocompletePopup.prototype = {
|
|||||||
|
|
||||||
// Make sure that the new content is properly fitted by the XUL richlistbox.
|
// Make sure that the new content is properly fitted by the XUL richlistbox.
|
||||||
if (this.isOpen) {
|
if (this.isOpen) {
|
||||||
// We need the timeout to allow the content to reflow. Attempting to
|
if (this.autoSelect) {
|
||||||
// update the richlistbox size too early does not work.
|
this.selectFirstItem();
|
||||||
this._document.defaultView.setTimeout(this._updateSize.bind(this), 1);
|
}
|
||||||
|
if (!this.fixedWidth) {
|
||||||
|
this._updateSize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects the first item of the richlistbox. Note that first item here is the
|
||||||
|
* item closes to the input element, which means that 0th index if position is
|
||||||
|
* below, and last index if position is above.
|
||||||
|
*/
|
||||||
|
selectFirstItem: function AP_selectFirstItem()
|
||||||
|
{
|
||||||
|
if (this.position.contains("before")) {
|
||||||
|
this.selectedIndex = this.itemCount - 1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.selectedIndex = 0;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -179,11 +254,23 @@ AutocompletePopup.prototype = {
|
|||||||
*/
|
*/
|
||||||
_updateSize: function AP__updateSize()
|
_updateSize: function AP__updateSize()
|
||||||
{
|
{
|
||||||
|
// We need the timeout to allow the content to reflow. Attempting to
|
||||||
|
// update the richlistbox size too early does not work.
|
||||||
|
this._document.defaultView.setTimeout(function() {
|
||||||
if (!this._panel) {
|
if (!this._panel) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._list.width = this._panel.clientWidth +
|
this._list.width = this._panel.clientWidth +
|
||||||
this._scrollbarWidth;
|
this._scrollbarWidth;
|
||||||
|
// Height change is required, otherwise the panel is drawn at an offset
|
||||||
|
// the first time.
|
||||||
|
this._list.height = this._panel.clientHeight;
|
||||||
|
// This brings the panel back at right position.
|
||||||
|
this._list.top = 0;
|
||||||
|
// Changing panel height might make the selected item out of view, so
|
||||||
|
// bring it back to view.
|
||||||
|
this._list.ensureIndexIsVisible(this._list.selectedIndex);
|
||||||
|
}.bind(this), 5);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -198,14 +285,16 @@ AutocompletePopup.prototype = {
|
|||||||
this._list.removeChild(this._list.firstChild);
|
this._list.removeChild(this._list.firstChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset the panel and list dimensions. New dimensions are calculated when a
|
if (!this.fixedWidth) {
|
||||||
// new set of items is added to the autocomplete popup.
|
// 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.width = "";
|
||||||
this._list.height = "";
|
this._list.height = "";
|
||||||
this._panel.width = "";
|
this._panel.width = "";
|
||||||
this._panel.height = "";
|
this._panel.height = "";
|
||||||
this._panel.top = "";
|
this._panel.top = "";
|
||||||
this._panel.left = "";
|
this._panel.left = "";
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -225,7 +314,9 @@ AutocompletePopup.prototype = {
|
|||||||
*/
|
*/
|
||||||
set selectedIndex(aIndex) {
|
set selectedIndex(aIndex) {
|
||||||
this._list.selectedIndex = aIndex;
|
this._list.selectedIndex = aIndex;
|
||||||
|
if (this.isOpen) {
|
||||||
this._list.ensureIndexIsVisible(this._list.selectedIndex);
|
this._list.ensureIndexIsVisible(this._list.selectedIndex);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -245,23 +336,51 @@ AutocompletePopup.prototype = {
|
|||||||
*/
|
*/
|
||||||
set selectedItem(aItem) {
|
set selectedItem(aItem) {
|
||||||
this._list.selectedItem = this._findListItem(aItem);
|
this._list.selectedItem = this._findListItem(aItem);
|
||||||
|
if (this.isOpen) {
|
||||||
this._list.ensureIndexIsVisible(this._list.selectedIndex);
|
this._list.ensureIndexIsVisible(this._list.selectedIndex);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Append an item into the autocomplete list.
|
* Append an item into the autocomplete list.
|
||||||
*
|
*
|
||||||
* @param object aItem
|
* @param object aItem
|
||||||
* The item you want appended to the list. The object must have a
|
* The item you want appended to the list.
|
||||||
* "label" property which is used as the displayed value.
|
* The item object can have the following properties:
|
||||||
|
* - label {String} Property which is used as the displayed value.
|
||||||
|
* - preLabel {String} [Optional] The String that will be displayed
|
||||||
|
* before the label indicating that this is the already
|
||||||
|
* 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 AP_appendItem(aItem)
|
appendItem: function AP_appendItem(aItem)
|
||||||
{
|
{
|
||||||
let description = this._document.createElementNS(XUL_NS, "description");
|
|
||||||
description.textContent = aItem.label;
|
|
||||||
|
|
||||||
let listItem = this._document.createElementNS(XUL_NS, "richlistitem");
|
let listItem = this._document.createElementNS(XUL_NS, "richlistitem");
|
||||||
listItem.appendChild(description);
|
if (this.direction) {
|
||||||
|
listItem.setAttribute("dir", this.direction);
|
||||||
|
}
|
||||||
|
let label = this._document.createElementNS(XUL_NS, "label");
|
||||||
|
label.setAttribute("value", aItem.label);
|
||||||
|
label.setAttribute("class", "autocomplete-value");
|
||||||
|
if (aItem.preLabel) {
|
||||||
|
let preDesc = this._document.createElementNS(XUL_NS, "label");
|
||||||
|
preDesc.setAttribute("value", aItem.preLabel);
|
||||||
|
preDesc.setAttribute("class", "initial-value");
|
||||||
|
listItem.appendChild(preDesc);
|
||||||
|
label.setAttribute("value", aItem.label.slice(aItem.preLabel.length));
|
||||||
|
}
|
||||||
|
listItem.appendChild(label);
|
||||||
|
if (aItem.count && aItem.count > 1) {
|
||||||
|
let countDesc = this._document.createElementNS(XUL_NS, "label");
|
||||||
|
countDesc.setAttribute("value", aItem.count);
|
||||||
|
countDesc.setAttribute("flex", "1");
|
||||||
|
countDesc.setAttribute("class", "autocomplete-count");
|
||||||
|
listItem.appendChild(countDesc);
|
||||||
|
}
|
||||||
listItem._autocompleteItem = aItem;
|
listItem._autocompleteItem = aItem;
|
||||||
|
|
||||||
this._list.appendChild(listItem);
|
this._list.appendChild(listItem);
|
||||||
@ -349,6 +468,14 @@ AutocompletePopup.prototype = {
|
|||||||
return this.selectedItem;
|
return this.selectedItem;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Focuses the richlistbox.
|
||||||
|
*/
|
||||||
|
focus: function AP_focus()
|
||||||
|
{
|
||||||
|
this._list.focus();
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine the scrollbar width in the current document.
|
* Determine the scrollbar width in the current document.
|
||||||
*
|
*
|
@ -14,7 +14,6 @@ EXTRA_JS_MODULES = \
|
|||||||
HUDService.jsm \
|
HUDService.jsm \
|
||||||
PropertyPanel.jsm \
|
PropertyPanel.jsm \
|
||||||
NetworkPanel.jsm \
|
NetworkPanel.jsm \
|
||||||
AutocompletePopup.jsm \
|
|
||||||
WebConsolePanel.jsm \
|
WebConsolePanel.jsm \
|
||||||
$(NULL)
|
$(NULL)
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "NetworkPanel",
|
|||||||
"resource:///modules/NetworkPanel.jsm");
|
"resource:///modules/NetworkPanel.jsm");
|
||||||
|
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "AutocompletePopup",
|
XPCOMUtils.defineLazyModuleGetter(this, "AutocompletePopup",
|
||||||
"resource:///modules/AutocompletePopup.jsm");
|
"resource:///modules/devtools/AutocompletePopup.jsm");
|
||||||
|
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils",
|
XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils",
|
||||||
"resource://gre/modules/devtools/WebConsoleUtils.jsm");
|
"resource://gre/modules/devtools/WebConsoleUtils.jsm");
|
||||||
@ -2736,9 +2736,15 @@ JSTerm.prototype = {
|
|||||||
init: function JST_init()
|
init: function JST_init()
|
||||||
{
|
{
|
||||||
let chromeDocument = this.hud.owner.chromeDocument;
|
let chromeDocument = this.hud.owner.chromeDocument;
|
||||||
this.autocompletePopup = new AutocompletePopup(chromeDocument);
|
let autocompleteOptions = {
|
||||||
this.autocompletePopup.onSelect = this.onAutocompleteSelect.bind(this);
|
onSelect: this.onAutocompleteSelect.bind(this),
|
||||||
this.autocompletePopup.onClick = this.acceptProposedCompletion.bind(this);
|
onClick: this.acceptProposedCompletion.bind(this),
|
||||||
|
panelId: "webConsole_autocompletePopup",
|
||||||
|
listBoxId: "webConsole_autocompletePopupListBox",
|
||||||
|
position: "before_start"
|
||||||
|
};
|
||||||
|
this.autocompletePopup = new AutocompletePopup(chromeDocument,
|
||||||
|
autocompleteOptions);
|
||||||
|
|
||||||
let doc = this.hud.document;
|
let doc = this.hud.document;
|
||||||
this.completeNode = doc.querySelector(".jsterm-complete-node");
|
this.completeNode = doc.querySelector(".jsterm-complete-node");
|
||||||
|
@ -110,10 +110,6 @@ scratchpad.linkText=Shift+RETURN - Open in Scratchpad
|
|||||||
# string
|
# string
|
||||||
gcliterm.instanceLabel=Instance of %S
|
gcliterm.instanceLabel=Instance of %S
|
||||||
|
|
||||||
# LOCALIZATION NOTE (Autocomplete.label):
|
|
||||||
# The autocomplete popup panel label/title.
|
|
||||||
Autocomplete.label=Autocomplete popup
|
|
||||||
|
|
||||||
# LOCALIZATION NOTE (stacktrace.anonymousFunction):
|
# LOCALIZATION NOTE (stacktrace.anonymousFunction):
|
||||||
# This string is used to display JavaScript functions that have no given name -
|
# This string is used to display JavaScript functions that have no given name -
|
||||||
# they are said to be anonymous. See stacktrace.outputMessage.
|
# they are said to be anonymous. See stacktrace.outputMessage.
|
||||||
|
Loading…
Reference in New Issue
Block a user