From dbbae02ae7427b3b908603a1bbae10134259ef7d Mon Sep 17 00:00:00 2001 From: Brian Grinstead Date: Tue, 10 Nov 2015 21:48:57 -0800 Subject: [PATCH 01/56] Bug 835896 - Make inspector searchbox also provide results matching text or attributes in the page;r=pbrosset --- devtools/client/framework/selection.js | 6 + devtools/client/inspector/inspector-panel.js | 72 ++-- ...selector-search.js => inspector-search.js} | 389 ++++++++---------- devtools/client/inspector/inspector.xul | 3 +- devtools/client/inspector/moz.build | 2 +- .../test/browser_inspector_search-01.js | 89 ++-- .../test/browser_inspector_search-02.js | 43 +- .../test/browser_inspector_search-03.js | 30 +- .../test/browser_inspector_search-04.js | 40 +- .../test/browser_inspector_search-05.js | 75 ++-- .../test/browser_inspector_search-06.js | 9 +- .../test/browser_inspector_search-reserved.js | 38 +- devtools/client/locales/en-US/inspector.dtd | 5 +- devtools/client/themes/inspector.css | 4 + devtools/server/actors/inspector.js | 140 ++++++- devtools/server/actors/utils/moz.build | 3 +- devtools/server/actors/utils/walker-search.js | 278 +++++++++++++ devtools/server/tests/mochitest/chrome.ini | 3 + .../mochitest/inspector-search-data.html | 52 +++ .../test_inspector-search-front.html | 220 ++++++++++ .../mochitest/test_inspector-search.html | 300 ++++++++++++++ 21 files changed, 1409 insertions(+), 392 deletions(-) rename devtools/client/inspector/{selector-search.js => inspector-search.js} (60%) create mode 100644 devtools/server/actors/utils/walker-search.js create mode 100644 devtools/server/tests/mochitest/inspector-search-data.html create mode 100644 devtools/server/tests/mochitest/test_inspector-search-front.html create mode 100644 devtools/server/tests/mochitest/test_inspector-search.html diff --git a/devtools/client/framework/selection.js b/devtools/client/framework/selection.js index 9eb29977662..cd0e3c41461 100644 --- a/devtools/client/framework/selection.js +++ b/devtools/client/framework/selection.js @@ -162,6 +162,12 @@ Selection.prototype = { setNodeFront: function(value, reason="unknown") { this.reason = reason; + // If a singleTextChild text node is being set, then set it's parent instead. + let parentNode = value && value.parentNode(); + if (value && parentNode && parentNode.singleTextChild === value) { + value = parentNode; + } + // We used to return here if the node had not changed but we now need to // set the node even if it is already set otherwise it is not possible to // e.g. highlight the same node twice. diff --git a/devtools/client/inspector/inspector-panel.js b/devtools/client/inspector/inspector-panel.js index c8aad6a807e..67bcdbeedcb 100644 --- a/devtools/client/inspector/inspector-panel.js +++ b/devtools/client/inspector/inspector-panel.js @@ -16,7 +16,7 @@ var {HostType} = require("devtools/client/framework/toolbox").Toolbox; loader.lazyGetter(this, "MarkupView", () => require("devtools/client/markupview/markup-view").MarkupView); loader.lazyGetter(this, "HTMLBreadcrumbs", () => require("devtools/client/inspector/breadcrumbs").HTMLBreadcrumbs); loader.lazyGetter(this, "ToolSidebar", () => require("devtools/client/framework/sidebar").ToolSidebar); -loader.lazyGetter(this, "SelectorSearch", () => require("devtools/client/inspector/selector-search").SelectorSearch); +loader.lazyGetter(this, "InspectorSearch", () => require("devtools/client/inspector/inspector-search").InspectorSearch); loader.lazyGetter(this, "strings", () => { return Services.strings.createBundle("chrome://devtools/locale/inspector.properties"); @@ -78,7 +78,20 @@ function InspectorPanel(iframeWindow, toolbox) { this.panelWin.inspector = this; this.nodeMenuTriggerInfo = null; + this._onBeforeNavigate = this._onBeforeNavigate.bind(this); + this.onNewRoot = this.onNewRoot.bind(this); + this._setupNodeMenu = this._setupNodeMenu.bind(this); + this._resetNodeMenu = this._resetNodeMenu.bind(this); + this._updateSearchResultsLabel = this._updateSearchResultsLabel.bind(this); + this.onNewSelection = this.onNewSelection.bind(this); + this.onBeforeNewSelection = this.onBeforeNewSelection.bind(this); + this.onDetached = this.onDetached.bind(this); + this.onToolboxHostChanged = this.onToolboxHostChanged.bind(this); + this.scheduleLayoutChange = this.scheduleLayoutChange.bind(this); + this.onPaneToggleButtonClicked = this.onPaneToggleButtonClicked.bind(this); + this._onMarkupFrameLoad = this._onMarkupFrameLoad.bind(this); + this._target.on("will-navigate", this._onBeforeNavigate); EventEmitter.decorate(this); @@ -139,31 +152,23 @@ InspectorPanel.prototype = { _deferredOpen: function(defaultSelection) { let deferred = promise.defer(); - this.onNewRoot = this.onNewRoot.bind(this); this.walker.on("new-root", this.onNewRoot); this.nodemenu = this.panelDoc.getElementById("inspector-node-popup"); this.lastNodemenuItem = this.nodemenu.lastChild; - this._setupNodeMenu = this._setupNodeMenu.bind(this); - this._resetNodeMenu = this._resetNodeMenu.bind(this); this.nodemenu.addEventListener("popupshowing", this._setupNodeMenu, true); this.nodemenu.addEventListener("popuphiding", this._resetNodeMenu, true); - this.onNewSelection = this.onNewSelection.bind(this); this.selection.on("new-node-front", this.onNewSelection); - this.onBeforeNewSelection = this.onBeforeNewSelection.bind(this); this.selection.on("before-new-node-front", this.onBeforeNewSelection); - this.onDetached = this.onDetached.bind(this); this.selection.on("detached-front", this.onDetached); this.breadcrumbs = new HTMLBreadcrumbs(this); - this.onToolboxHostChanged = this.onToolboxHostChanged.bind(this); this._toolbox.on("host-changed", this.onToolboxHostChanged); if (this.target.isLocalTab) { this.browser = this.target.tab.linkedBrowser; - this.scheduleLayoutChange = this.scheduleLayoutChange.bind(this); this.browser.addEventListener("resize", this.scheduleLayoutChange, true); // Show a warning when the debugger is paused. @@ -309,13 +314,31 @@ InspectorPanel.prototype = { * Hooks the searchbar to show result and auto completion suggestions. */ setupSearchBox: function() { - // Initiate the selectors search object. - if (this.searchSuggestions) { - this.searchSuggestions.destroy(); - this.searchSuggestions = null; - } this.searchBox = this.panelDoc.getElementById("inspector-searchbox"); - this.searchSuggestions = new SelectorSearch(this, this.searchBox); + this.searchResultsLabel = this.panelDoc.getElementById("inspector-searchlabel"); + + this.search = new InspectorSearch(this, this.searchBox); + this.search.on("search-cleared", this._updateSearchResultsLabel); + this.search.on("search-result", this._updateSearchResultsLabel); + }, + + get searchSuggestions() { + return this.search.autocompleter; + }, + + _updateSearchResultsLabel: function(event, result) { + let str = ""; + if (event !== "search-cleared") { + if (result) { + str = strings.formatStringFromName( + "inspector.searchResultsCount2", + [result.resultsIndex + 1, result.resultsLength], 2); + } else { + str = strings.GetStringFromName("inspector.searchResultsNone"); + } + } + + this.searchResultsLabel.textContent = str; }, /** @@ -369,7 +392,6 @@ InspectorPanel.prototype = { */ setupSidebarToggle: function() { this._paneToggleButton = this.panelDoc.getElementById("inspector-pane-toggle"); - this.onPaneToggleButtonClicked = this.onPaneToggleButtonClicked.bind(this); this._paneToggleButton.addEventListener("mousedown", this.onPaneToggleButtonClicked); this.updatePaneToggleButton(); @@ -399,7 +421,6 @@ InspectorPanel.prototype = { return; } this.markup.expandNode(this.selection.nodeFront); - this.setupSearchBox(); this.emit("new-root"); }); }; @@ -590,8 +611,6 @@ InspectorPanel.prototype = { this._paneToggleButton.removeEventListener("mousedown", this.onPaneToggleButtonClicked); this._paneToggleButton = null; - this.searchSuggestions.destroy(); - this.searchBox = null; this.selection.off("new-node-front", this.onNewSelection); this.selection.off("before-new-node", this.onBeforeNewSelection); this.selection.off("before-new-node-front", this.onBeforeNewSelection); @@ -602,10 +621,12 @@ InspectorPanel.prototype = { this.panelDoc = null; this.panelWin = null; this.breadcrumbs = null; - this.searchSuggestions = null; this.lastNodemenuItem = null; this.nodemenu = null; this._toolbox = null; + this.search.destroy(); + this.search = null; + this.searchBox = null; this._panelDestroyer = promise.all([ sidebarDestroyer, @@ -914,8 +935,7 @@ InspectorPanel.prototype = { this._markupFrame.setAttribute("context", "inspector-node-popup"); // This is needed to enable tooltips inside the iframe document. - this._boundMarkupFrameLoad = this._onMarkupFrameLoad.bind(this); - this._markupFrame.addEventListener("load", this._boundMarkupFrameLoad, true); + this._markupFrame.addEventListener("load", this._onMarkupFrameLoad, true); this._markupBox.setAttribute("collapsed", true); this._markupBox.appendChild(this._markupFrame); @@ -924,8 +944,7 @@ InspectorPanel.prototype = { }, _onMarkupFrameLoad: function() { - this._markupFrame.removeEventListener("load", this._boundMarkupFrameLoad, true); - delete this._boundMarkupFrameLoad; + this._markupFrame.removeEventListener("load", this._onMarkupFrameLoad, true); this._markupFrame.contentWindow.focus(); @@ -940,9 +959,8 @@ InspectorPanel.prototype = { _destroyMarkup: function() { let destroyPromise; - if (this._boundMarkupFrameLoad) { - this._markupFrame.removeEventListener("load", this._boundMarkupFrameLoad, true); - this._boundMarkupFrameLoad = null; + if (this._markupFrame) { + this._markupFrame.removeEventListener("load", this._onMarkupFrameLoad, true); } if (this.markup) { diff --git a/devtools/client/inspector/selector-search.js b/devtools/client/inspector/inspector-search.js similarity index 60% rename from devtools/client/inspector/selector-search.js rename to devtools/client/inspector/inspector-search.js index eb1e3dddb51..13d9effc4e0 100644 --- a/devtools/client/inspector/selector-search.js +++ b/devtools/client/inspector/inspector-search.js @@ -4,7 +4,7 @@ "use strict"; -const { Cu } = require("chrome"); +const {Cu, Ci} = require("chrome"); const promise = require("promise"); loader.lazyGetter(this, "EventEmitter", () => require("devtools/shared/event-emitter")); @@ -13,6 +13,108 @@ loader.lazyGetter(this, "AutocompletePopup", () => require("devtools/client/shar // Maximum number of selector suggestions shown in the panel. const MAX_SUGGESTIONS = 15; + +/** + * Converts any input field into a document search box. + * + * @param {InspectorPanel} inspector The InspectorPanel whose `walker` attribute + * should be used for document traversal. + * @param {DOMNode} input The input element to which the panel will be attached + * and from where search input will be taken. + * + * Emits the following events: + * - search-cleared: when the search box is emptied + * - search-result: when a search is made and a result is selected + */ +function InspectorSearch(inspector, input) { + this.inspector = inspector; + this.searchBox = input; + this._lastSearched = null; + + this._onKeyDown = this._onKeyDown.bind(this); + this._onCommand = this._onCommand.bind(this); + this.searchBox.addEventListener("keydown", this._onKeyDown, true); + this.searchBox.addEventListener("command", this._onCommand, true); + + // For testing, we need to be able to wait for the most recent node request + // to finish. Tests can watch this promise for that. + this._lastQuery = promise.resolve(null); + + this.autocompleter = new SelectorAutocompleter(inspector, input); + EventEmitter.decorate(this); +} + +exports.InspectorSearch = InspectorSearch; + +InspectorSearch.prototype = { + get walker() { + return this.inspector.walker; + }, + + destroy: function() { + this.searchBox.removeEventListener("keydown", this._onKeyDown, true); + this.searchBox.removeEventListener("command", this._onCommand, true); + this.searchBox = null; + this.autocompleter.destroy(); + }, + + _onSearch: function(reverse = false) { + this.doFullTextSearch(this.searchBox.value, reverse) + .catch(e => console.error(e)); + }, + + doFullTextSearch: Task.async(function*(query, reverse) { + let lastSearched = this._lastSearched; + this._lastSearched = query; + + if (query.length === 0) { + this.searchBox.classList.remove("devtools-no-search-result"); + if (!lastSearched || lastSearched.length > 0) { + this.emit("search-cleared"); + } + return; + } + + let res = yield this.walker.search(query, { reverse }); + + // Value has changed since we started this request, we're done. + if (query != this.searchBox.value) { + return; + } + + if (res) { + this.inspector.selection.setNodeFront(res.node, "inspectorsearch"); + this.searchBox.classList.remove("devtools-no-search-result"); + + res.query = query; + this.emit("search-result", res); + } else { + this.searchBox.classList.add("devtools-no-search-result"); + this.emit("search-result"); + } + }), + + _onCommand: function() { + if (this.searchBox.value.length === 0) { + this._onSearch(); + } + }, + + _onKeyDown: function(event) { + if (this.searchBox.value.length === 0) { + this.searchBox.removeAttribute("filled"); + } else { + this.searchBox.setAttribute("filled", true); + } + if (event.keyCode === event.DOM_VK_RETURN) { + this._onSearch(); + } if (event.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_G && event.metaKey) { + this._onSearch(event.shiftKey); + event.preventDefault(); + } + } +}; + /** * Converts any input box on a page to a CSS selector search and suggestion box. * @@ -21,29 +123,19 @@ const MAX_SUGGESTIONS = 15; * search or not. * * @constructor - * @param InspectorPanel aInspector + * @param InspectorPanel inspector * The InspectorPanel whose `walker` attribute should be used for * document traversal. - * @param nsiInputElement aInputNode + * @param nsiInputElement inputNode * The input element to which the panel will be attached and from where * search input will be taken. */ -function SelectorSearch(aInspector, aInputNode) { - this.inspector = aInspector; - this.searchBox = aInputNode; +function SelectorAutocompleter(inspector, inputNode) { + this.inspector = inspector; + this.searchBox = inputNode; 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.showSuggestions = this.showSuggestions.bind(this); this._onSearchKeypress = this._onSearchKeypress.bind(this); this._onListBoxKeypress = this._onListBoxKeypress.bind(this); this._onMarkupMutation = this._onMarkupMutation.bind(this); @@ -61,8 +153,7 @@ function SelectorSearch(aInspector, aInputNode) { }; this.searchPopup = new AutocompletePopup(this.panelDoc, options); - // event listeners. - this.searchBox.addEventListener("command", this._onHTMLSearch, true); + this.searchBox.addEventListener("input", this.showSuggestions, true); this.searchBox.addEventListener("keypress", this._onSearchKeypress, true); this.inspector.on("markupmutation", this._onMarkupMutation); @@ -72,9 +163,9 @@ function SelectorSearch(aInspector, aInputNode) { EventEmitter.decorate(this); } -exports.SelectorSearch = SelectorSearch; +exports.SelectorAutocompleter = SelectorAutocompleter; -SelectorSearch.prototype = { +SelectorAutocompleter.prototype = { get walker() { return this.inspector.walker; }, @@ -169,142 +260,33 @@ SelectorSearch.prototype = { * Removes event listeners and cleans up references. */ destroy: function() { - // event listeners. - this.searchBox.removeEventListener("command", this._onHTMLSearch, true); + this.searchBox.removeEventListener("input", this.showSuggestions, true); this.searchBox.removeEventListener("keypress", this._onSearchKeypress, true); this.inspector.off("markupmutation", this._onMarkupMutation); this.searchPopup.destroy(); this.searchPopup = null; this.searchBox = null; this.panelDoc = null; - this._searchResults = null; - this._searchSuggestions = null; - }, - - _selectResult: function(index) { - return this._searchResults.item(index).then(node => { - this.inspector.selection.setNodeFront(node, "selectorsearch"); - }); - }, - - _queryNodes: Task.async(function*(query) { - if (typeof this.hasMultiFrameSearch === "undefined") { - let target = this.inspector.toolbox.target; - this.hasMultiFrameSearch = yield target.actorHasMethod("domwalker", - "multiFrameQuerySelectorAll"); - } - - if (this.hasMultiFrameSearch) { - return yield this.walker.multiFrameQuerySelectorAll(query); - } else { - return yield this.walker.querySelectorAll(this.walker.rootNode, query); - } - }), - - /** - * 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() { - let query = this.searchBox.value; - if (query == this._lastSearched) { - this.emit("processing-done"); - return; - } - this._lastSearched = query; - this._searchResults = []; - 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(); - } - this.emit("processing-done"); - return; - } - - this.searchBox.setAttribute("filled", true); - let queryList = null; - - this._lastQuery = this._queryNodes(query).then(list => { - return list; - }, (err) => { - // Failures are ok here, just use a null item list; - return null; - }).then(queryList => { - // Value has changed since we started this request, we're done. - if (query != this.searchBox.value) { - if (queryList) { - queryList.release(); - } - return promise.reject(null); - } - - this._searchResults = queryList || []; - if (this._searchResults && 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(); - } - this.searchBox.classList.remove("devtools-no-search-result"); - - return this._selectResult(0); - } - return this._selectResult(0).then(() => { - this.searchBox.classList.remove("devtools-no-search-result"); - }).then(() => this.showSuggestions()); - } - 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"); - return this.showSuggestions(); - }).then(() => this.emit("processing-done"), Cu.reportError); }, /** * Handles keypresses inside the input box. */ - _onSearchKeypress: function(aEvent) { + _onSearchKeypress: function(event) { let query = this.searchBox.value; - switch(aEvent.keyCode) { - case aEvent.DOM_VK_RETURN: - if (query == this._lastSearched && this._searchResults) { - this._searchIndex = (this._searchIndex + 1) % this._searchResults.length; - } - else { - this._onHTMLSearch(); - return; + switch(event.keyCode) { + case event.DOM_VK_RETURN: + case event.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.hidePopup(); } break; - case aEvent.DOM_VK_UP: + case event.DOM_VK_UP: if (this.searchPopup.isOpen && this.searchPopup.itemCount > 0) { this.searchPopup.focus(); if (this.searchPopup.selectedIndex == this.searchPopup.itemCount - 1) { @@ -316,76 +298,45 @@ SelectorSearch.prototype = { } this.searchBox.value = this.searchPopup.selectedItem.label; } - else if (--this._searchIndex < 0) { - this._searchIndex = this._searchResults.length - 1; - } break; - case aEvent.DOM_VK_DOWN: + case event.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 && this._searchResults.length > 0) { - this._lastQuery = this._selectResult(this._searchIndex).then(() => this.emit("processing-done")); - } - else { - this.emit("processing-done"); - } + event.preventDefault(); + event.stopPropagation(); + this.emit("processing-done"); }, /** * Handles keypress and mouse click on the suggestions richlistbox. */ - _onListBoxKeypress: function(aEvent) { - switch(aEvent.keyCode || aEvent.button) { - case aEvent.DOM_VK_RETURN: - case aEvent.DOM_VK_TAB: + _onListBoxKeypress: function(event) { + switch(event.keyCode || event.button) { + case event.DOM_VK_RETURN: + case event.DOM_VK_TAB: case 0: // left mouse button - aEvent.stopPropagation(); - aEvent.preventDefault(); + event.stopPropagation(); + event.preventDefault(); this.searchBox.value = this.searchPopup.selectedItem.label; this.searchBox.focus(); - this._onHTMLSearch(); + this.hidePopup(); break; - case aEvent.DOM_VK_UP: + case event.DOM_VK_UP: if (this.searchPopup.selectedIndex == 0) { this.searchPopup.selectedIndex = -1; - aEvent.stopPropagation(); - aEvent.preventDefault(); + event.stopPropagation(); + event.preventDefault(); this.searchBox.focus(); } else { @@ -394,11 +345,11 @@ SelectorSearch.prototype = { } break; - case aEvent.DOM_VK_DOWN: + case event.DOM_VK_DOWN: if (this.searchPopup.selectedIndex == this.searchPopup.itemCount - 1) { this.searchPopup.selectedIndex = -1; - aEvent.stopPropagation(); - aEvent.preventDefault(); + event.stopPropagation(); + event.preventDefault(); this.searchBox.focus(); } else { @@ -407,20 +358,15 @@ SelectorSearch.prototype = { } break; - case aEvent.DOM_VK_BACK_SPACE: - aEvent.stopPropagation(); - aEvent.preventDefault(); + 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._lastToLastValidSearch = null; - let query = this.searchBox.value; - this._lastValidSearch = (query.match(/(.*)[\.#][^\.# ]{0,}$/) || - query.match(/(.*[\s>+])[a-zA-Z][^\.# ]{0,}$/) || - ["",""])[1]; - this._onHTMLSearch(); + this.hidePopup(); break; } this.emit("processing-done"); @@ -438,12 +384,12 @@ SelectorSearch.prototype = { /** * Populates the suggestions list and show the suggestion popup. */ - _showPopup: function(aList, aFirstPart, aState) { + _showPopup: function(list, firstPart, aState) { let total = 0; let query = this.searchBox.value; let items = []; - for (let [value, count, state] of aList) { + for (let [value, /*count*/, state] of list) { // for cases like 'div ' or 'div >' or 'div+' if (query.match(/[\s>+]$/)) { value = query + value; @@ -461,8 +407,7 @@ SelectorSearch.prototype = { let item = { preLabel: query, - label: value, - count: count + label: value }; // In case of tagNames, change the case to small @@ -489,6 +434,16 @@ SelectorSearch.prototype = { this.searchPopup.openPopup(this.searchBox); } else { + this.hidePopup(); + } + }, + + + /** + * Hide the suggestion popup if necessary. + */ + hidePopup: function() { + if (this.searchPopup.isOpen) { this.searchPopup.hidePopup(); } }, @@ -502,18 +457,18 @@ SelectorSearch.prototype = { let state = this.state; let firstPart = ""; - if (state == this.States.TAG) { + if (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]; query = query.slice(0, query.length - firstPart.length); } - else if (state == this.States.CLASS) { + else if (state === this.States.CLASS) { // gets the class that is being completed. For ex. '.foo.b' returns 'b' firstPart = query.match(/\.([^\.]*)$/)[1]; query = query.slice(0, query.length - firstPart.length - 1); } - else if (state == this.States.ID) { + else if (state === this.States.ID) { // gets the id that is being completed. For ex. '.foo#b' returns 'b' firstPart = query.match(/#([^#]*)$/)[1]; query = query.slice(0, query.length - firstPart.length - 1); @@ -524,23 +479,31 @@ SelectorSearch.prototype = { query += "*"; } - this._currentSuggesting = query; - return this.walker.getSuggestionsForQuery(query, firstPart, state).then(result => { - if (this._currentSuggesting != result.query) { + this._lastQuery = this.walker.getSuggestionsForQuery(query, firstPart, state).then(result => { + this.emit("processing-done"); + if (result.query !== query) { // This means that this response is for a previous request and the user // as since typed something extra leading to a new request. return; } - this._lastToLastValidSearch = this._lastValidSearch; - if (state == this.States.CLASS) { + if (state === this.States.CLASS) { firstPart = "." + firstPart; - } - else if (state == this.States.ID) { + } else if (state === this.States.ID) { firstPart = "#" + firstPart; } + // If there is a single tag match and it's what the user typed, then + // don't need to show a popup. + if (result.suggestions.length === 1 && + result.suggestions[0][0] === firstPart) { + result.suggestions = []; + } + + this._showPopup(result.suggestions, firstPart, state); }); + + return this._lastQuery; } }; diff --git a/devtools/client/inspector/inspector.xul b/devtools/client/inspector/inspector.xul index ed0b67c053b..0b9ee9d90dd 100644 --- a/devtools/client/inspector/inspector.xul +++ b/devtools/client/inspector/inspector.xul @@ -154,11 +154,12 @@ class="breadcrumbs-widget-container" flex="1" orient="horizontal" clicktoscroll="true"/> + + placeholder="&inspectorSearchHTML.label3;"/> diff --git a/devtools/client/inspector/moz.build b/devtools/client/inspector/moz.build index cd39e95c0e1..f110b16c81b 100644 --- a/devtools/client/inspector/moz.build +++ b/devtools/client/inspector/moz.build @@ -6,7 +6,7 @@ DevToolsModules( 'breadcrumbs.js', 'inspector-commands.js', 'inspector-panel.js', - 'selector-search.js' + 'inspector-search.js' ) BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] diff --git a/devtools/client/inspector/test/browser_inspector_search-01.js b/devtools/client/inspector/test/browser_inspector_search-01.js index 1e74be3ab78..968b10ec75a 100644 --- a/devtools/client/inspector/test/browser_inspector_search-01.js +++ b/devtools/client/inspector/test/browser_inspector_search-01.js @@ -8,11 +8,6 @@ const TEST_URL = TEST_URL_ROOT + "doc_inspector_search.html"; -// Indexes of the keys in the KEY_STATES array that should listen to "keypress" -// event instead of "command". These are keys that don't change the content of -// the search field and thus don't trigger command event. -const LISTEN_KEYPRESS = [3,4,8,18,19,20,21,22]; - // The various states of the inspector: [key, id, isValid] // [ // what key to press, @@ -20,35 +15,45 @@ const LISTEN_KEYPRESS = [3,4,8,18,19,20,21,22]; // is the searched text valid selector // ] const KEY_STATES = [ - ["d", "b1", false], - ["i", "b1", false], - ["v", "d1", true], - ["VK_DOWN", "d2", true], // keypress - ["VK_RETURN", "d1", true], //keypress - [".", "d1", false], - ["c", "d1", false], - ["1", "d2", true], - ["VK_DOWN", "d2", true], // keypress - ["VK_BACK_SPACE", "d2", false], - ["VK_BACK_SPACE", "d2", false], - ["VK_BACK_SPACE", "d1", true], - ["VK_BACK_SPACE", "d1", false], - ["VK_BACK_SPACE", "d1", false], - ["VK_BACK_SPACE", "d1", true], - [".", "d1", false], - ["c", "d1", false], - ["1", "d2", true], - ["VK_DOWN", "s2", true], // keypress - ["VK_DOWN", "p1", true], // kepress - ["VK_UP", "s2", true], // keypress - ["VK_UP", "d2", true], // keypress - ["VK_UP", "p1", true], - ["VK_BACK_SPACE", "p1", false], - ["2", "p3", true], - ["VK_BACK_SPACE", "p3", false], - ["VK_BACK_SPACE", "p3", false], - ["VK_BACK_SPACE", "p3", true], - ["r", "p3", false], + ["#", "b1", true], // # + ["d", "b1", true], // #d + ["1", "b1", true], // #d1 + ["VK_RETURN", "d1", true], // #d1 + ["VK_BACK_SPACE", "d1", true], // #d + ["2", "d1", true], // #d2 + ["VK_RETURN", "d2", true], // #d2 + ["2", "d2", true], // #d22 + ["VK_RETURN", "d2", false], // #d22 + ["VK_BACK_SPACE", "d2", false], // #d2 + ["VK_RETURN", "d2", true], // #d2 + ["VK_BACK_SPACE", "d2", true], // #d + ["1", "d2", true], // #d1 + ["VK_RETURN", "d1", true], // #d1 + ["VK_BACK_SPACE", "d1", true], // #d + ["VK_BACK_SPACE", "d1", true], // # + ["VK_BACK_SPACE", "d1", true], // + ["d", "d1", true], // d + ["i", "d1", true], // di + ["v", "d1", true], // div + [".", "d1", true], // div. + ["c", "d1", true], // div.c + ["VK_UP", "d1", true], // div.c1 + ["VK_TAB", "d1", true], // div.c1 + ["VK_RETURN", "d2", true], // div.c1 + ["VK_BACK_SPACE", "d2", true], // div.c + ["VK_BACK_SPACE", "d2", true], // div. + ["VK_BACK_SPACE", "d2", true], // div + ["VK_BACK_SPACE", "d2", true], // di + ["VK_BACK_SPACE", "d2", true], // d + ["VK_BACK_SPACE", "d2", true], // + [".", "d2", true], // . + ["c", "d2", true], // .c + ["1", "d2", true], // .c1 + ["VK_RETURN", "d2", true], // .c1 + ["VK_RETURN", "s2", true], // .c1 + ["VK_RETURN", "p1", true], // .c1 + ["P", "p1", true], // .c1P + ["VK_RETURN", "p1", false], // .c1P ]; add_task(function* () { @@ -60,14 +65,18 @@ add_task(function* () { let index = 0; for (let [ key, id, isValid ] of KEY_STATES) { - let event = (LISTEN_KEYPRESS.indexOf(index) !== -1) ? "keypress" : "command"; - let eventHandled = once(searchBox, event, true); - - info(index + ": Pressing key " + key + " to get id " + id); + info(index + ": Pressing key " + key + " to get id " + id + "."); + let done = inspector.searchSuggestions.once("processing-done"); EventUtils.synthesizeKey(key, {}, inspector.panelWin); - yield eventHandled; + yield done; + info("Got processing-done event"); - info("Got " + event + " event. Waiting for search query to complete"); + if (key === "VK_RETURN") { + info ("Waiting for " + (isValid ? "NO " : "") + "results"); + yield inspector.search.once("search-result"); + } + + info("Waiting for search query to complete"); yield inspector.searchSuggestions._lastQuery; info(inspector.selection.nodeFront.id + " is selected with text " + diff --git a/devtools/client/inspector/test/browser_inspector_search-02.js b/devtools/client/inspector/test/browser_inspector_search-02.js index 8e1c06ad129..5edec48d00c 100644 --- a/devtools/client/inspector/test/browser_inspector_search-02.js +++ b/devtools/client/inspector/test/browser_inspector_search-02.js @@ -16,14 +16,14 @@ const TEST_DATA = [ { key: "d", suggestions: [ - {label: "div", count: 4}, - {label: "#d1", count: 1}, - {label: "#d2", count: 1} + {label: "div"}, + {label: "#d1"}, + {label: "#d2"} ] }, { key: "i", - suggestions: [{label: "div", count: 4}] + suggestions: [{label: "div"}] }, { key: "v", @@ -32,22 +32,22 @@ const TEST_DATA = [ { key: " ", suggestions: [ - {label: "div div", count: 2}, - {label: "div span", count: 2} + {label: "div div"}, + {label: "div span"} ] }, { key: ">", suggestions: [ - {label: "div >div", count: 2}, - {label: "div >span", count: 2} + {label: "div >div"}, + {label: "div >span"} ] }, { key: "VK_BACK_SPACE", suggestions: [ - {label: "div div", count: 2 }, - {label: "div span", count: 2} + {label: "div div"}, + {label: "div span"} ] }, { @@ -57,8 +57,8 @@ const TEST_DATA = [ { key: "VK_BACK_SPACE", suggestions: [ - {label: "div div", count: 2 }, - {label: "div span", count: 2} + {label: "div div"}, + {label: "div span"} ] }, { @@ -67,14 +67,14 @@ const TEST_DATA = [ }, { key: "VK_BACK_SPACE", - suggestions: [{label: "div", count: 4}] + suggestions: [{label: "div"}] }, { key: "VK_BACK_SPACE", suggestions: [ - {label: "div", count: 4}, - {label: "#d1", count: 1}, - {label: "#d2", count: 1} + {label: "div"}, + {label: "#d1"}, + {label: "#d2"} ] }, { @@ -83,7 +83,12 @@ const TEST_DATA = [ }, { key: "p", - suggestions: [] + suggestions: [ + {label: "p"}, + {label: "#p1"}, + {label: "#p2"}, + {label: "#p3"}, + ] }, { key: " ", @@ -153,14 +158,12 @@ add_task(function* () { for (let i = 0; i < suggestions.length; i++) { is(actualSuggestions[i].label, suggestions[i].label, "The suggestion at " + i + "th index is correct."); - is(actualSuggestions[i].count, suggestions[i].count || 1, - "The count for suggestion at " + i + "th index is correct."); } } }); function formatSuggestions(suggestions) { return "[" + suggestions - .map(s => "'" + s.label + "' (" + (s.count || 1) + ")") + .map(s => "'" + s.label + "'") .join(", ") + "]"; } diff --git a/devtools/client/inspector/test/browser_inspector_search-03.js b/devtools/client/inspector/test/browser_inspector_search-03.js index 657a7e6bbb4..b2bfc355f4a 100644 --- a/devtools/client/inspector/test/browser_inspector_search-03.js +++ b/devtools/client/inspector/test/browser_inspector_search-03.js @@ -16,14 +16,14 @@ var TEST_DATA = [ { key: "d", suggestions: [ - {label: "div", count: 2}, - {label: "#d1", count: 1}, - {label: "#d2", count: 1} + {label: "div"}, + {label: "#d1"}, + {label: "#d2"} ] }, { key: "i", - suggestions: [{label: "div", count: 2}] + suggestions: [{label: "div"}] }, { key: "v", @@ -50,14 +50,14 @@ var TEST_DATA = [ }, { key: "VK_BACK_SPACE", - suggestions: [{label: "div", count: 2}] + suggestions: [{label: "div"}] }, { key: "VK_BACK_SPACE", suggestions: [ - {label: "div", count: 2}, - {label: "#d1", count: 1}, - {label: "#d2", count: 1} + {label: "div"}, + {label: "#d1"}, + {label: "#d2"} ] }, { @@ -67,14 +67,14 @@ var TEST_DATA = [ { key: ".", suggestions: [ - {label: ".c1", count: 3}, + {label: ".c1"}, {label: ".c2"} ] }, { key: "c", suggestions: [ - {label: ".c1", count: 3}, + {label: ".c1"}, {label: ".c2"} ] }, @@ -85,7 +85,7 @@ var TEST_DATA = [ { key: "VK_BACK_SPACE", suggestions: [ - {label: ".c1", count: 3}, + {label: ".c1"}, {label: ".c2"} ] }, @@ -108,14 +108,14 @@ var TEST_DATA = [ { key: "VK_BACK_SPACE", suggestions: [ - {label: ".c1", count: 3}, + {label: ".c1"}, {label: ".c2"} ] }, { key: "VK_BACK_SPACE", suggestions: [ - {label: ".c1", count: 3}, + {label: ".c1"}, {label: ".c2"} ] }, @@ -189,14 +189,12 @@ add_task(function* () { for (let i = 0; i < suggestions.length; i++) { is(actualSuggestions[i].label, suggestions[i].label, "The suggestion at " + i + "th index is correct."); - is(actualSuggestions[i].count, suggestions[i].count || 1, - "The count for suggestion at " + i + "th index is correct."); } } }); function formatSuggestions(suggestions) { return "[" + suggestions - .map(s => "'" + s.label + "' (" + (s.count || 1) + ")") + .map(s => "'" + s.label + "'") .join(", ") + "]"; } diff --git a/devtools/client/inspector/test/browser_inspector_search-04.js b/devtools/client/inspector/test/browser_inspector_search-04.js index a8af585248b..0f81a8809e7 100644 --- a/devtools/client/inspector/test/browser_inspector_search-04.js +++ b/devtools/client/inspector/test/browser_inspector_search-04.js @@ -19,14 +19,14 @@ var TEST_DATA = [ { key: "d", suggestions: [ - {label: "div", count: 5}, - {label: "#d1", count: 2}, - {label: "#d2", count: 2} + {label: "div"}, + {label: "#d1"}, + {label: "#d2"} ] }, { key: "i", - suggestions: [{label: "div", count: 5}] + suggestions: [{label: "div"}] }, { key: "v", @@ -34,14 +34,14 @@ var TEST_DATA = [ }, { key: "VK_BACK_SPACE", - suggestions: [{label: "div", count: 5}] + suggestions: [{label: "div"}] }, { key: "VK_BACK_SPACE", suggestions: [ - {label: "div", count: 5}, - {label: "#d1", count: 2}, - {label: "#d2", count: 2} + {label: "div"}, + {label: "#d1"}, + {label: "#d2"} ] }, { @@ -51,8 +51,8 @@ var TEST_DATA = [ { key: ".", suggestions: [ - {label: ".c1", count: 7}, - {label: ".c2", count: 3} + {label: ".c1"}, + {label: ".c2"} ] }, { @@ -62,14 +62,14 @@ var TEST_DATA = [ { key: "#", suggestions: [ - {label: "#b1", count: 2}, - {label: "#d1", count: 2}, - {label: "#d2", count: 2}, - {label: "#p1", count: 2}, - {label: "#p2", count: 2}, - {label: "#p3", count: 2}, - {label: "#s1", count: 2}, - {label: "#s2", count: 2} + {label: "#b1"}, + {label: "#d1"}, + {label: "#d2"}, + {label: "#p1"}, + {label: "#p2"}, + {label: "#p3"}, + {label: "#s1"}, + {label: "#s2"} ] }, ]; @@ -100,14 +100,12 @@ add_task(function* () { for (let i = 0; i < suggestions.length; i++) { is(actualSuggestions[i].label, suggestions[i].label, "The suggestion at " + i + "th index is correct."); - is(actualSuggestions[i].count, suggestions[i].count || 1, - "The count for suggestion at " + i + "th index is correct."); } } }); function formatSuggestions(suggestions) { return "[" + suggestions - .map(s => "'" + s.label + "' (" + (s.count || 1) + ")") + .map(s => "'" + s.label + "'") .join(", ") + "]"; } diff --git a/devtools/client/inspector/test/browser_inspector_search-05.js b/devtools/client/inspector/test/browser_inspector_search-05.js index b0b43012399..834be64b2ce 100644 --- a/devtools/client/inspector/test/browser_inspector_search-05.js +++ b/devtools/client/inspector/test/browser_inspector_search-05.js @@ -11,11 +11,14 @@ const TEST_URL = "data:text/html;charset=utf-8," + "" + ""; + TEST_URL_ROOT + IFRAME_SRC + "\">" + + ""; add_task(function* () { let {inspector} = yield openInspectorForURL(TEST_URL); - let {walker} = inspector; let searchBox = inspector.searchBox; let popup = inspector.searchSuggestions.searchPopup; @@ -24,41 +27,65 @@ add_task(function* () { yield focusSearchBoxUsingShortcut(inspector.panelWin); info("Enter # to search for all ids"); - let command = once(searchBox, "command"); + let processingDone = once(inspector.searchSuggestions, "processing-done"); EventUtils.synthesizeKey("#", {}, inspector.panelWin); - yield command; + yield processingDone; info("Wait for search query to complete"); yield inspector.searchSuggestions._lastQuery; - info("Press tab to fill the search input with the first suggestion and " + - "expect a new selection"); - let onSelect = inspector.once("inspector-updated"); + info("Press tab to fill the search input with the first suggestion"); + processingDone = once(inspector.searchSuggestions, "processing-done"); EventUtils.synthesizeKey("VK_TAB", {}, inspector.panelWin); + yield processingDone; + + info("Press enter and expect a new selection"); + let onSelect = inspector.once("inspector-updated"); + EventUtils.synthesizeKey("VK_RETURN", {}, inspector.panelWin); yield onSelect; - let node = inspector.selection.nodeFront; - ok(node.id, "b1", "The selected node is #b1"); - ok(node.tagName.toLowerCase(), "button", - "The selected node is