--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -141,24 +141,17 @@
<panel type="autocomplete" id="PopupSearchAutoComplete" noautofocus="true" hidden="true"/>
<!-- for url bar autocomplete -->
<panel type="autocomplete-richlistbox"
id="PopupAutoCompleteRichResult"
noautofocus="true"
hidden="true"
flip="none"
- level="parent">
-#ifdef NIGHTLY_BUILD
- <hbox id="urlbar-search-footer" flex="1" align="stretch" pack="end">
- <button id="urlbar-search-settings" label="&changeSearchSettings.button;"
- oncommand="BrowserUITelemetry.countSearchSettingsEvent('urlbar'); openPreferences('paneSearch')"/>
- </hbox>
-#endif
- </panel>
+ level="parent"/>
<!-- for select dropdowns. The menupopup is what shows the list of options,
and the popuponly menulist makes things like the menuactive attributes
work correctly on the menupopup. ContentSelectDropdown expects the
popuponly menulist to be its immediate parent. -->
<menulist popuponly="true" id="ContentSelectDropdown" hidden="true">
<menupopup rolluponmousewheel="true"
activateontab="true"
@@ -705,17 +698,17 @@
completeselectedindex="true"
shrinkdelay="250"
tabscrolling="true"
showcommentcolumn="true"
showimagecolumn="true"
enablehistory="true"
maxrows="10"
newlines="stripsurroundingwhitespace"
- ontextentered="this.handleCommand(param);"
+ ontextentered="this.handleCommand(...args);"
ontextreverted="return this.handleRevert();"
pageproxystate="invalid"
onfocus="document.getElementById('identity-box').style.MozUserFocus= 'normal'"
onblur="setTimeout(() => { document.getElementById('identity-box').style.MozUserFocus = ''; }, 0);">
<box id="notification-popup-box" hidden="true" align="center">
<image id="default-notification-icon" class="notification-anchor-icon" role="button"
aria-label="&urlbar.defaultNotificationAnchor.label;"/>
<image id="geo-notification-icon" class="notification-anchor-icon geo-icon" role="button"
--- a/browser/base/content/test/urlbar/browser.ini
+++ b/browser/base/content/test/urlbar/browser.ini
@@ -51,16 +51,17 @@ support-files =
authenticate.sjs
[browser_urlbarDecode.js]
[browser_urlbarDelete.js]
[browser_urlbarEnter.js]
[browser_urlbarEnterAfterMouseOver.js]
skip-if = os == "linux" # Bug 1073339 - Investigate autocomplete test unreliability on Linux/e10s
[browser_urlbarHashChangeProxyState.js]
[browser_urlbarKeepStateAcrossTabSwitches.js]
+[browser_urlbarOneOffs.js]
[browser_urlbarPrivateBrowsingWindowChange.js]
[browser_urlbarRevert.js]
[browser_urlbarSearchSingleWordNotification.js]
[browser_urlbarSearchSuggestions.js]
support-files =
searchSuggestionEngine.xml
searchSuggestionEngine.sjs
[browser_urlbarSearchSuggestionsNotification.js]
--- a/browser/base/content/test/urlbar/browser_autocomplete_autoselect.js
+++ b/browser/base/content/test/urlbar/browser_autocomplete_autoselect.js
@@ -1,16 +1,31 @@
function repeat(limit, func) {
for (let i = 0; i < limit; i++) {
func(i);
}
}
function is_selected(index) {
is(gURLBar.popup.richlistbox.selectedIndex, index, `Item ${index + 1} should be selected`);
+
+ // This is true because although both the listbox and the one-offs can have
+ // selections, the test doesn't check that.
+ is(gURLBar.popup.oneOffSearchButtons.selectedButton, null,
+ "A result is selected, so the one-offs should not have a selection");
+}
+
+function is_selected_one_off(index) {
+ is(gURLBar.popup.oneOffSearchButtons.selectedButtonIndex, index,
+ "Expected one-off button should be selected");
+
+ // This is true because although both the listbox and the one-offs can have
+ // selections, the test doesn't check that.
+ is(gURLBar.popup.richlistbox.selectedIndex, -1,
+ "A one-off is selected, so the listbox should not have a selection");
}
add_task(function*() {
let maxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults");
registerCleanupFunction(function* () {
yield PlacesTestUtils.clearHistory();
});
@@ -32,22 +47,37 @@ add_task(function*() {
is(results.length, maxResults,
"Should get maxResults=" + maxResults + " results");
is_selected(0);
info("Key Down to select the next item");
EventUtils.synthesizeKey("VK_DOWN", {});
is_selected(1);
- info("Key Down maxResults times should wrap around all the way around");
- repeat(maxResults, () => EventUtils.synthesizeKey("VK_DOWN", {}));
+ info("Key Down maxResults-1 times should select the first one-off");
+ repeat(maxResults - 1, () => EventUtils.synthesizeKey("VK_DOWN", {}));
+ is_selected_one_off(0);
+
+ info("Key Down numButtons-1 should select the last one-off");
+ repeat(gURLBar.popup.oneOffSearchButtons.numButtons - 1,
+ () => EventUtils.synthesizeKey("VK_DOWN", {}));
+ is_selected_one_off(gURLBar.popup.oneOffSearchButtons.numButtons - 1);
+
+ info("Key Down twice more should select the second result");
+ repeat(2, () => EventUtils.synthesizeKey("VK_DOWN", {}));
is_selected(1);
- info("Key Up maxResults times should wrap around the other way");
- repeat(maxResults, () => EventUtils.synthesizeKey("VK_UP", {}));
+ info("Key Down maxResults + numButtons times should wrap around");
+ repeat(maxResults + gURLBar.popup.oneOffSearchButtons.numButtons,
+ () => EventUtils.synthesizeKey("VK_DOWN", {}));
+ is_selected(1);
+
+ info("Key Up maxResults + numButtons times should wrap around the other way");
+ repeat(maxResults + gURLBar.popup.oneOffSearchButtons.numButtons,
+ () => EventUtils.synthesizeKey("VK_UP", {}));
is_selected(1);
info("Page Up will go up the list, but not wrap");
EventUtils.synthesizeKey("VK_PAGE_UP", {})
is_selected(0);
info("Page Up again will wrap around to the end of the list");
EventUtils.synthesizeKey("VK_PAGE_UP", {})
--- a/browser/base/content/test/urlbar/browser_autocomplete_edit_completed.js
+++ b/browser/base/content/test/urlbar/browser_autocomplete_edit_completed.js
@@ -26,20 +26,23 @@ add_task(function*() {
let nextValue = gURLBar.controller.getFinalCompleteValueAt(nextIndex);
is(list.selectedIndex, nextIndex, "The next item is selected.");
is(gURLBar.value, nextValue, "The selected URL is completed.");
info("Press backspace");
EventUtils.synthesizeKey("VK_BACK_SPACE", {});
yield promiseSearchComplete();
- let editedValue = gURLBar.value;
+ let editedValue = gURLBar.textValue;
is(list.selectedIndex, initialIndex, "The initial index is selected again.");
isnot(editedValue, nextValue, "The URL has changed.");
+ let docLoad = waitForDocLoadAndStopIt("http://" + editedValue);
+
info("Press return to load edited URL.");
EventUtils.synthesizeKey("VK_RETURN", {});
yield Promise.all([
promisePopupHidden(gURLBar.popup),
- waitForDocLoadAndStopIt("http://" + editedValue)]);
+ docLoad,
+ ]);
gBrowser.removeTab(gBrowser.selectedTab);
});
--- a/browser/base/content/test/urlbar/browser_bug1070778.js
+++ b/browser/base/content/test/urlbar/browser_bug1070778.js
@@ -38,17 +38,17 @@ add_task(function*() {
is_selected(1);
// Re-select keyword item
EventUtils.synthesizeKey("VK_UP", {});
is_selected(0);
EventUtils.synthesizeKey("b", {});
yield promiseSearchComplete();
- is(gURLBar.value, "keyword ab", "urlbar should have expected input");
+ is(gURLBar.textValue, "keyword ab", "urlbar should have expected input");
let result = gURLBar.popup.richlistbox.firstChild;
isnot(result, null, "Should have first item");
let uri = NetUtil.newURI(result.getAttribute("url"));
is(uri.spec, makeActionURI("keyword", {url: "http://example.com/?q=ab", input: "keyword ab"}).spec, "Expect correct url");
EventUtils.synthesizeKey("VK_ESCAPE", {});
yield promisePopupHidden(gURLBar.popup);
--- a/browser/base/content/test/urlbar/browser_tabMatchesInAwesomebar_perwindowpb.js
+++ b/browser/base/content/test/urlbar/browser_tabMatchesInAwesomebar_perwindowpb.js
@@ -65,17 +65,17 @@ function* runTest(aSourceWindow, aDestWi
}
let awaitTabSwitch;
if (aExpectSwitch) {
awaitTabSwitch = BrowserTestUtils.removeTab(testTab, {dontRemove: true})
}
// Execute the selected action.
- controller.handleEnter(true);
+ controller.handleEnter(true, null);
info("sent Enter command to the controller");
if (aExpectSwitch) {
// If we expect a tab switch then the current tab
// will be closed and we switch to the other tab.
yield awaitTabSwitch;
} else {
// If we don't expect a tab switch then wait for the tab to load.
--- a/browser/base/content/test/urlbar/browser_urlbarAutoFillTrimURLs.js
+++ b/browser/base/content/test/urlbar/browser_urlbarAutoFillTrimURLs.js
@@ -39,18 +39,18 @@ function continue_test() {
info(`Testing with input: ${aTyped}`);
gURLBar.inputField.value = aTyped.substr(0, aTyped.length - 1);
gURLBar.focus();
gURLBar.selectionStart = aTyped.length - 1;
gURLBar.selectionEnd = aTyped.length - 1;
EventUtils.synthesizeKey(aTyped.substr(-1), {});
waitForSearchComplete(function () {
- info(`Got value: ${gURLBar.value}`);
- is(gURLBar.value, aExpected, "Autofilled value is as expected");
+ info(`Got value: ${gURLBar.textValue}`);
+ is(gURLBar.textValue, aExpected, "Autofilled value is as expected");
aCallback();
});
}
test_autoFill("http://", "http://", function () {
test_autoFill("http://au", "http://autofilltrimurl.com/", function () {
test_autoFill("http://www.autofilltrimurl.com", "http://www.autofilltrimurl.com/", function () {
// Now ensure selecting from the popup correctly trims.
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbarOneOffs.js
@@ -0,0 +1,228 @@
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+
+let gMaxResults;
+
+add_task(function* init() {
+ gMaxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults");
+
+ // Add a search suggestion engine and move it to the front so that it appears
+ // as the first one-off.
+ let engine = yield promiseNewSearchEngine(TEST_ENGINE_BASENAME);
+ Services.search.moveEngine(engine, 0);
+
+ registerCleanupFunction(function* () {
+ yield hidePopup();
+ yield PlacesTestUtils.clearHistory();
+ });
+
+ yield PlacesTestUtils.clearHistory();
+
+ let visits = [];
+ for (let i = 0; i < gMaxResults; i++) {
+ visits.push({
+ uri: makeURI("http://example.com/browser_urlbarOneOffs.js/?" + i),
+ // TYPED so that the visit shows up when the urlbar's drop-down arrow is
+ // pressed.
+ transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ });
+ }
+ yield PlacesTestUtils.addVisits(visits);
+});
+
+// Keys up and down through the history panel, i.e., the panel that's shown when
+// there's no text in the textbox.
+add_task(function* history() {
+ gURLBar.focus();
+ EventUtils.synthesizeKey("VK_DOWN", {})
+ yield promisePopupShown(gURLBar.popup);
+
+ assertState(-1, -1, "");
+
+ // Key down through each result.
+ for (let i = 0; i < gMaxResults; i++) {
+ EventUtils.synthesizeKey("VK_DOWN", {})
+ assertState(i, -1,
+ "example.com/browser_urlbarOneOffs.js/?" + (gMaxResults - i - 1));
+ }
+
+ // Key down through each one-off.
+ for (let i = 0; i < gURLBar.popup.oneOffSearchButtons.numButtons; i++) {
+ EventUtils.synthesizeKey("VK_DOWN", {})
+ assertState(-1, i, "");
+ }
+
+ // Key down once more. Nothing should be selected.
+ EventUtils.synthesizeKey("VK_DOWN", {})
+ assertState(-1, -1, "");
+
+ // Once more. The first result should be selected.
+ EventUtils.synthesizeKey("VK_DOWN", {})
+ assertState(0, -1,
+ "example.com/browser_urlbarOneOffs.js/?" + (gMaxResults - 1));
+
+ // Now key up. Nothing should be selected again.
+ EventUtils.synthesizeKey("VK_UP", {})
+ assertState(-1, -1, "");
+
+ // Key up through each one-off.
+ for (let i = gURLBar.popup.oneOffSearchButtons.numButtons - 1; i >= 0; i--) {
+ EventUtils.synthesizeKey("VK_UP", {})
+ assertState(-1, i, "");
+ }
+
+ // Key up through each result.
+ for (let i = gMaxResults - 1; i >= 0; i--) {
+ EventUtils.synthesizeKey("VK_UP", {})
+ assertState(i, -1,
+ "example.com/browser_urlbarOneOffs.js/?" + (gMaxResults - i - 1));
+ }
+
+ // Key up once more. Nothing should be selected.
+ EventUtils.synthesizeKey("VK_UP", {})
+ assertState(-1, -1, "");
+
+ yield hidePopup();
+});
+
+// Keys up and down through the non-history panel, i.e., the panel that's shown
+// when you type something in the textbox.
+add_task(function* typedValue() {
+ // Use a typed value that returns the visits added above but that doesn't
+ // trigger autofill since that would complicate the test.
+ let typedValue = "browser_urlbarOneOffs";
+ yield promiseAutocompleteResultPopup(typedValue, window, true);
+
+ assertState(0, -1, typedValue);
+
+ // Key down through each result. The first result is already selected, which
+ // is why gMaxResults - 1 is the correct number of times to do this.
+ for (let i = 0; i < gMaxResults - 1; i++) {
+ EventUtils.synthesizeKey("VK_DOWN", {})
+ // i starts at zero so that the textValue passed to assertState is correct.
+ // But that means that i + 1 is the expected selected index, since initially
+ // (when this loop starts) the first result is selected.
+ assertState(i + 1, -1,
+ "example.com/browser_urlbarOneOffs.js/?" + (gMaxResults - i - 1));
+ }
+
+ // Key down through each one-off.
+ for (let i = 0; i < gURLBar.popup.oneOffSearchButtons.numButtons; i++) {
+ EventUtils.synthesizeKey("VK_DOWN", {})
+ assertState(-1, i, typedValue);
+ }
+
+ // Key down once more. The selection should wrap around to the first result.
+ EventUtils.synthesizeKey("VK_DOWN", {})
+ assertState(0, -1, typedValue);
+
+ // Now key up. The selection should wrap back around to the one-offs. Key
+ // up through all the one-offs.
+ for (let i = gURLBar.popup.oneOffSearchButtons.numButtons - 1; i >= 0; i--) {
+ EventUtils.synthesizeKey("VK_UP", {})
+ assertState(-1, i, typedValue);
+ }
+
+ // Key up through each non-heuristic result.
+ for (let i = gMaxResults - 2; i >= 0; i--) {
+ EventUtils.synthesizeKey("VK_UP", {})
+ assertState(i + 1, -1,
+ "example.com/browser_urlbarOneOffs.js/?" + (gMaxResults - i - 1));
+ }
+
+ // Key up once more. The heuristic result should be selected.
+ EventUtils.synthesizeKey("VK_UP", {})
+ assertState(0, -1, typedValue);
+
+ yield hidePopup();
+});
+
+// Checks that "Search with Current Search Engine" items are updated to "Search
+// with One-Off Engine" when a one-off is selected.
+add_task(function* searchWith() {
+ let typedValue = "foo";
+ yield promiseAutocompleteResultPopup(typedValue);
+
+ assertState(0, -1, typedValue);
+
+ let item = gURLBar.popup.richlistbox.firstChild;
+ Assert.equal(item._actionText.textContent,
+ "Search with " + Services.search.currentEngine.name,
+ "Sanity check: first result's action text");
+
+ // Tab to the first one-off. Now the first result and the first one-off
+ // should both be selected.
+ EventUtils.synthesizeKey("VK_TAB", {})
+ assertState(0, 0, typedValue);
+
+ let engineName = gURLBar.popup.oneOffSearchButtons.selectedButton.engine.name;
+ Assert.notEqual(engineName, Services.search.currentEngine.name,
+ "Sanity check: First one-off engine should not be " +
+ "the current engine");
+ Assert.equal(item._actionText.textContent,
+ "Search with " + engineName,
+ "First result's action text should be updated");
+
+ yield hidePopup();
+});
+
+// Clicks a one-off.
+add_task(function* oneOffClick() {
+ gBrowser.selectedTab = gBrowser.addTab();
+
+ let typedValue = "foo";
+ yield promiseAutocompleteResultPopup(typedValue);
+
+ assertState(0, -1, typedValue);
+
+ let oneOffs = gURLBar.popup.oneOffSearchButtons.getSelectableButtons(true);
+ let resultsPromise = promiseSearchResultsLoaded();
+ EventUtils.synthesizeMouseAtCenter(oneOffs[0], {});
+ yield resultsPromise;
+
+ gBrowser.removeTab(gBrowser.selectedTab);
+});
+
+// Presses the Return key when a one-off is selected.
+add_task(function* oneOffReturn() {
+ gBrowser.selectedTab = gBrowser.addTab();
+
+ let typedValue = "foo";
+ yield promiseAutocompleteResultPopup(typedValue, window, true);
+
+ assertState(0, -1, typedValue);
+
+ // Tab to select the first one-off.
+ EventUtils.synthesizeKey("VK_TAB", {})
+ assertState(0, 0, typedValue);
+
+ let resultsPromise = promiseSearchResultsLoaded();
+ EventUtils.synthesizeKey("VK_RETURN", {})
+ yield resultsPromise;
+
+ gBrowser.removeTab(gBrowser.selectedTab);
+});
+
+
+function assertState(result, oneOff, textValue=undefined) {
+ Assert.equal(gURLBar.popup.selectedIndex, result,
+ "Expected result should be selected");
+ Assert.equal(gURLBar.popup.oneOffSearchButtons.selectedButtonIndex, oneOff,
+ "Expected one-off should be selected");
+ if (textValue !== undefined) {
+ Assert.equal(gURLBar.textValue, textValue, "Expected textValue");
+ }
+}
+
+function* hidePopup() {
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ yield promisePopupHidden(gURLBar.popup);
+}
+
+function promiseSearchResultsLoaded() {
+ let tab = gBrowser.selectedTab;
+ return promiseTabLoadEvent(tab).then(() => {
+ Assert.equal(tab.linkedBrowser.currentURI.spec,
+ "http://mochi.test:8888/",
+ 'Expected "search results" page loaded');
+ });
+}
--- a/browser/base/content/test/urlbar/browser_urlbarSearchSuggestions.js
+++ b/browser/base/content/test/urlbar/browser_urlbarSearchSuggestions.js
@@ -20,17 +20,20 @@ add_task(function* prepare() {
Assert.ok(!gURLBar.popup.popupOpen, "popup should be closed");
});
});
add_task(function* clickSuggestion() {
gBrowser.selectedTab = gBrowser.addTab();
gURLBar.focus();
yield promiseAutocompleteResultPopup("foo");
- let [idx, suggestion] = yield promiseFirstSuggestion();
+ let [idx, suggestion, engineName] = yield promiseFirstSuggestion();
+ Assert.equal(engineName,
+ "browser_searchSuggestionEngine%20searchSuggestionEngine.xml",
+ "Expected suggestion engine");
let item = gURLBar.popup.richlistbox.getItemAtIndex(idx);
let loadPromise = promiseTabLoaded(gBrowser.selectedTab);
item.click();
yield loadPromise;
let uri = Services.search.currentEngine.getSubmission(suggestion).uri;
Assert.ok(uri.equals(gBrowser.currentURI),
"The search results page should have loaded");
gBrowser.removeTab(gBrowser.selectedTab);
@@ -42,24 +45,24 @@ function getFirstSuggestion() {
let present = false;
for (let i = 0; i < matchCount; i++) {
let url = controller.getValueAt(i);
let mozActionMatch = url.match(/^moz-action:([^,]+),(.*)$/);
if (mozActionMatch) {
let [, type, paramStr] = mozActionMatch;
let params = JSON.parse(paramStr);
if (type == "searchengine" && "searchSuggestion" in params) {
- return [i, params.searchSuggestion];
+ return [i, params.searchSuggestion, params.engineName];
}
}
}
return [-1, null];
}
function promiseFirstSuggestion() {
return new Promise(resolve => {
- let pair;
+ let tuple;
waitForCondition(() => {
- pair = getFirstSuggestion();
- return pair[0] >= 0;
- }, () => resolve(pair));
+ tuple = getFirstSuggestion();
+ return tuple[0] >= 0;
+ }, () => resolve(tuple));
});
}
--- a/browser/base/content/test/urlbar/browser_urlbarStop.js
+++ b/browser/base/content/test/urlbar/browser_urlbarStop.js
@@ -15,17 +15,16 @@ add_task(function* () {
gBrowser.selectedTab = gBrowser.addTab("about:blank");
is(gURLBar.textValue, "", "location bar is empty");
yield typeAndSubmitAndStop(badURL);
is(gURLBar.textValue, gURLBar.trimValue(badURL), "location bar reflects stopped page in an empty tab");
gBrowser.removeCurrentTab();
});
-function typeAndSubmitAndStop(url) {
- gBrowser.userTypedValue = url;
- URLBarSetURI();
+function* typeAndSubmitAndStop(url) {
+ yield promiseAutocompleteResultPopup(url, window, true);
is(gURLBar.textValue, gURLBar.trimValue(url), "location bar reflects loading page");
let promise = waitForDocLoadAndStopIt(url, gBrowser.selectedBrowser, false);
gURLBar.handleCommand();
- return promise;
+ yield promise;
}
--- a/browser/base/content/test/urlbar/browser_urlbar_autoFill_backspaced.js
+++ b/browser/base/content/test/urlbar/browser_urlbar_autoFill_backspaced.js
@@ -2,23 +2,23 @@
* confirm the remaining value.
*/
function* test_autocomplete(data) {
let {desc, typed, autofilled, modified, keys, action, onAutoFill} = data;
info(desc);
yield promiseAutocompleteResultPopup(typed);
- is(gURLBar.value, autofilled, "autofilled value is as expected");
+ is(gURLBar.textValue, autofilled, "autofilled value is as expected");
if (onAutoFill)
onAutoFill()
keys.forEach(key => EventUtils.synthesizeKey(key, {}));
- is(gURLBar.value, modified, "backspaced value is as expected");
+ is(gURLBar.textValue, modified, "backspaced value is as expected");
yield promiseSearchComplete();
ok(gURLBar.popup.richlistbox.children.length > 0, "Should get at least 1 result");
let result = gURLBar.popup.richlistbox.children[0];
let type = result.getAttribute("type");
let types = type.split(/\s+/);
ok(types.indexOf(action) >= 0, `The type attribute "${type}" includes the expected action "${action}"`);
--- a/browser/base/content/test/urlbar/head.js
+++ b/browser/base/content/test/urlbar/head.js
@@ -322,20 +322,28 @@ function promiseSearchComplete(win = win
Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH;
}
// Wait until there are at least two matches.
return new Promise(resolve => waitForCondition(searchIsComplete, resolve));
});
}
-function promiseAutocompleteResultPopup(inputText, win = window) {
+function promiseAutocompleteResultPopup(inputText,
+ win = window,
+ fireInputEvent = false) {
waitForFocus(() => {
win.gURLBar.focus();
win.gURLBar.value = inputText;
+ if (fireInputEvent) {
+ // This is necessary to get the urlbar to set gBrowser.userTypedValue.
+ let event = document.createEvent("Events");
+ event.initEvent("input", true, true);
+ win.gURLBar.dispatchEvent(event);
+ }
win.gURLBar.controller.startSearch(inputText);
}, win);
return promiseSearchComplete(win);
}
function promiseNewSearchEngine(basename) {
return new Promise((resolve, reject) => {
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -192,17 +192,21 @@ file, You can obtain one at http://mozil
case KeyEvent.DOM_VK_LEFT:
case KeyEvent.DOM_VK_RIGHT:
case KeyEvent.DOM_VK_HOME:
// Reset the selected index so that nsAutoCompleteController
// simply closes the popup without trying to fill anything.
this.popup.selectedIndex = -1;
break;
}
-
+ if (this.popup.popupOpen &&
+ !this.popup.disableKeyNavigation &&
+ this.popup.handleKeyPress(aEvent)) {
+ return true;
+ }
return this.handleKeyPress(aEvent);
]]></body>
</method>
<field name="_mayTrimURLs">true</field>
<method name="trimValue">
<parameter name="aURL"/>
<body><![CDATA[
@@ -326,143 +330,212 @@ file, You can obtain one at http://mozil
}
// tell widget to revert to last typed text only if the user
// was scrolling when they hit escape
return !isScrolling;
]]></body>
</method>
- <method name="handleCommand">
- <parameter name="aTriggeringEvent"/>
- <body><![CDATA[
- if (aTriggeringEvent instanceof MouseEvent && aTriggeringEvent.button == 2)
- return; // Do nothing for right clicks
-
- var url = this.value;
- var mayInheritPrincipal = false;
- var postData = null;
-
- let action = this._parseActionUrl(this._value);
- let lastLocationChange = gBrowser.selectedBrowser.lastLocationChange;
+ <!--
+ This is ultimately called by the autocomplete controller as the result
+ of handleEnter when the Return key is pressed in the textbox. Since
+ onPopupClick also calls handleEnter, this is also called as a result in
+ that case.
- let matchLastLocationChange = true;
- if (action) {
- if (action.type == "switchtab") {
- url = action.params.url;
- if (this.hasAttribute("actiontype")) {
- this.handleRevert();
- let prevTab = gBrowser.selectedTab;
- if (switchToTabHavingURI(url) && isTabEmpty(prevTab)) {
- gBrowser.removeTab(prevTab);
- }
- return;
- }
- } else if (action.type == "remotetab") {
- url = action.params.url;
- } else if (action.type == "keyword") {
- url = action.params.url;
- } else if (action.type == "searchengine") {
- [url, postData] = this._parseAndRecordSearchEngineAction(action);
- } else if (action.type == "visiturl") {
- url = action.params.url;
- }
- continueOperation.call(this);
+ @param event
+ The event that triggered the command.
+ @param openUILinkWhere
+ Optional. The "where" to pass to openUILinkIn. This method
+ computes the appropriate "where" given the event, but you can
+ use this to override it.
+ @param openUILinkParams
+ Optional. The parameters to pass to openUILinkIn. As with
+ "where", this method computes the appropriate parameters, but
+ any parameters you supply here will override those.
+ -->
+ <method name="handleCommand">
+ <parameter name="event"/>
+ <parameter name="selectedPopupIndex"/>
+ <parameter name="openUILinkWhere"/>
+ <parameter name="openUILinkParams"/>
+ <body><![CDATA[
+ let isMouseEvent = event instanceof MouseEvent;
+ if (isMouseEvent && event.button == 2) {
+ // Do nothing for right clicks.
+ return;
}
- else {
- this._canonizeURL(aTriggeringEvent, response => {
- [url, postData, mayInheritPrincipal] = response;
- if (url) {
- matchLastLocationChange = (lastLocationChange ==
- gBrowser.selectedBrowser.lastLocationChange);
- continueOperation.call(this);
- }
- });
+
+ let where = "current";
+ // If the current tab is empty, ignore Alt+Enter (just reuse this tab)
+ let altEnter = !isMouseEvent &&
+ event &&
+ event.altKey &&
+ !isTabEmpty(gBrowser.selectedTab);
+ if (isMouseEvent || altEnter) {
+ // Use the standard UI link behaviors for clicks or Alt+Enter
+ where = isMouseEvent ? whereToOpenLink(event, false, false) : "tab";
}
- function continueOperation()
- {
- this.value = url;
- gBrowser.userTypedValue = url;
- if (gInitialPages.includes(url)) {
- gBrowser.selectedBrowser.initialPageLoadedFromURLBar = url;
+ // For the URL to load, prefer the url attribute of the selected
+ // result over the input's value. The reason is that if there is a
+ // selected result and it's a searchengine action, then its url will
+ // have been updated for the currently selected one-off search engine.
+ // Note that when this method is called, the popup may be closed,
+ // which is why this method has a selectedPopupIndex param.
+ let url;
+ if (typeof(selectedPopupIndex) != "number" ||
+ selectedPopupIndex < 0) {
+ selectedPopupIndex = this.popup.selectedIndex;
+ }
+ if (selectedPopupIndex >= 0) {
+ let item = this.popup.richlistbox.children[selectedPopupIndex];
+ if (item) {
+ url = item.getAttribute("url");
}
- try {
- addToUrlbarHistory(url);
- } catch (ex) {
- // Things may go wrong when adding url to session history,
- // but don't let that interfere with the loading of the url.
- Cu.reportError(ex);
- }
+ }
+ url = url || this.value;
+
+ let mayInheritPrincipal = false;
+ let postData = null;
+ let lastLocationChange = gBrowser.selectedBrowser.lastLocationChange;
+ let matchLastLocationChange = true;
- let loadCurrent = () => {
- try {
- openUILinkIn(url, "current", {
- allowThirdPartyFixup: true,
- indicateErrorPageLoad: true,
- disallowInheritPrincipal: !mayInheritPrincipal,
- allowPinnedTabHostChange: true,
- postData: postData,
- allowPopups: url.startsWith("javascript:"),
- });
- } catch (ex) {
- // This load can throw an exception in certain cases, which means
- // we'll want to replace the URL with the loaded URL:
- if (ex.result != Cr.NS_ERROR_LOAD_SHOWED_ERRORPAGE) {
+let action = this._parseActionUrl(url);
+ if (action) {
+ switch (action.type) {
+ case "visiturl":
+ case "keyword":
+ case "remotetab":
+ url = action.params.url;
+ break;
+ case "switchtab":
+ url = action.params.url;
+ if (this.hasAttribute("actiontype")) {
this.handleRevert();
+ let prevTab = gBrowser.selectedTab;
+ if (switchToTabHavingURI(url) && isTabEmpty(prevTab)) {
+ gBrowser.removeTab(prevTab);
+ }
+ return;
}
- }
+ break;
+ case "searchengine":
+ [url, postData] =
+ this._parseAndRecordSearchEngineAction(action, event, where,
+ openUILinkParams);
+ break;
+ }
+ this._loadURL(url, postData, where, openUILinkParams,
+ matchLastLocationChange, mayInheritPrincipal);
+ return;
+ }
+
+ this._canonizeURL(event, response => {
+ [url, postData, mayInheritPrincipal] = response;
+ if (url) {
+ matchLastLocationChange =
+ lastLocationChange ==
+ gBrowser.selectedBrowser.lastLocationChange;
+ this._loadURL(url, postData, where, openUILinkParams,
+ matchLastLocationChange, mayInheritPrincipal);
+ }
+ });
+ ]]></body>
+ </method>
- // Ensure the start of the URL is visible for UX reasons:
- this.selectionStart = this.selectionEnd = 0;
- };
+ <method name="_loadURL">
+ <parameter name="url"/>
+ <parameter name="postData"/>
+ <parameter name="openUILinkWhere"/>
+ <parameter name="openUILinkParams"/>
+ <parameter name="matchLastLocationChange"/>
+ <parameter name="mayInheritPrincipal"/>
+ <body><![CDATA[
+ this.value = url;
+ gBrowser.userTypedValue = url;
+ if (gInitialPages.includes(url)) {
+ gBrowser.selectedBrowser.initialPageLoadedFromURLBar = url;
+ }
+ try {
+ addToUrlbarHistory(url);
+ } catch (ex) {
+ // Things may go wrong when adding url to session history,
+ // but don't let that interfere with the loading of the url.
+ Cu.reportError(ex);
+ }
- // Focus the content area before triggering loads, since if the load
- // occurs in a new tab, we want focus to be restored to the content
- // area when the current tab is re-selected.
- gBrowser.selectedBrowser.focus();
-
- let isMouseEvent = aTriggeringEvent instanceof MouseEvent;
-
- // If the current tab is empty, ignore Alt+Enter (just reuse this tab)
- let altEnter = !isMouseEvent && aTriggeringEvent &&
- aTriggeringEvent.altKey && !isTabEmpty(gBrowser.selectedTab);
+ let current = openUILinkWhere == "current";
- if (isMouseEvent || altEnter) {
- // Use the standard UI link behaviors for clicks or Alt+Enter
- let where = "tab";
- if (isMouseEvent)
- where = whereToOpenLink(aTriggeringEvent, false, false);
+ let params;
+ if (current) {
+ params = {
+ postData: postData,
+ allowThirdPartyFixup: true,
+ indicateErrorPageLoad: true,
+ disallowInheritPrincipal: true,
+ allowPinnedTabHostChange: true,
+ disallowInheritPrincipal: !mayInheritPrincipal,
+ allowPopups: url.startsWith("javascript:"),
+ };
+ } else {
+ params = {
+ postData: postData,
+ allowThirdPartyFixup: true,
+ initiatingDoc: document,
+ };
+ }
+
+ if (openUILinkParams) {
+ for (let key in openUILinkParams) {
+ params[key] = openUILinkParams[key];
+ }
+ }
- if (where == "current") {
- if (matchLastLocationChange) {
- loadCurrent();
- }
- } else {
- this.handleRevert();
- let params = { allowThirdPartyFixup: true,
- postData: postData,
- initiatingDoc: document };
- openUILinkIn(url, where, params);
- }
- } else {
- if (matchLastLocationChange) {
- loadCurrent();
- }
+ // Focus the content area before triggering loads, since if the load
+ // occurs in a new tab, we want focus to be restored to the content
+ // area when the current tab is re-selected.
+ gBrowser.selectedBrowser.focus();
+
+ if (current && !matchLastLocationChange) {
+ return;
+ }
+
+ if (!current) {
+ this.handleRevert();
+ }
+
+ try {
+ openUILinkIn(url, openUILinkWhere, params);
+ } catch (ex) {
+ // This load can throw an exception in certain cases, which means
+ // we'll want to replace the URL with the loaded URL:
+ if (ex.result != Cr.NS_ERROR_LOAD_SHOWED_ERRORPAGE) {
+ this.handleRevert();
}
}
+
+ if (current) {
+ // Ensure the start of the URL is visible for UX reasons:
+ this.selectionStart = this.selectionEnd = 0;
+ }
]]></body>
</method>
<method name="_parseAndRecordSearchEngineAction">
<parameter name="action"/>
+ <parameter name="event"/>
+ <parameter name="openUILinkWhere"/>
+ <parameter name="openUILinkParams"/>
<body><![CDATA[
let engine =
Services.search.getEngineByName(action.params.engineName);
BrowserSearch.recordSearchInTelemetry(engine, "urlbar");
+ this.popup.oneOffSearchButtons.recordTelemetry(event, openUILinkWhere,
+ openUILinkParams);
let query = action.params.searchSuggestion ||
action.params.searchQuery;
let submission = engine.getSubmission(query, null, "keyword");
return [submission.uri.spec, submission.postData];
]]></body>
</method>
<method name="_canonizeURL">
@@ -898,33 +971,34 @@ file, You can obtain one at http://mozil
this.gotResultForCurrentQuery = false;
this.mController.handleText();
}
this.resetActionType();
]]></body>
</method>
<method name="handleEnter">
+ <parameter name="event"/>
<body><![CDATA[
// We need to ensure we're using a selected autocomplete result.
// A result should automatically be selected by default,
// however autocomplete is async and therefore we may not
// have a result set relating to the current input yet. If that
// happens, we need to mark that when the first result does get added,
// it needs to be handled as if enter was pressed with that first
// result selected.
// If anything other than the default (first) result is selected, then
// it must have been manually selected by the human. We let this
// explicit choice be used, even if it may be related to a previous
// input.
// However, if the default result is automatically selected, we
// ensure that it corresponds to the current input.
if (this.popup.selectedIndex != 0 || this.gotResultForCurrentQuery) {
- return this.mController.handleEnter(false);
+ return this.mController.handleEnter(false, event);
}
this.handleEnterWhenGotResult = true;
return true;
]]></body>
</method>
@@ -1094,17 +1168,17 @@ file, You can obtain one at http://mozil
index: controller.selection.currentIndex,
kind: "mouse"
};
}
// Check for unmodified left-click, and use default behavior
if (aEvent.button == 0 && !aEvent.shiftKey && !aEvent.ctrlKey &&
!aEvent.altKey && !aEvent.metaKey) {
- controller.handleEnter(true);
+ controller.handleEnter(true, aEvent);
return;
}
// Check for middle-click or modified clicks on the search bar
if (popupForSearchBar) {
// Handle search bar popup clicks
var search = controller.getValueAt(this.selectedIndex);
@@ -1131,16 +1205,21 @@ file, You can obtain one at http://mozil
}
]]></body>
</method>
</implementation>
</binding>
<binding id="urlbar-rich-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-rich-result-popup">
+ <resources>
+ <stylesheet src="chrome://browser/content/search/searchbarBindings.css"/>
+ <stylesheet src="chrome://browser/skin/searchbar.css"/>
+ </resources>
+
<content ignorekeys="true" level="top" consumeoutsideclicks="never"
aria-owns="richlistbox">
<xul:hbox anonid="search-suggestions-notification"
align="center"
role="alert"
aria-describedby="search-suggestions-notification-text">
<xul:description flex="1">
&urlbar.searchSuggestionsNotification.question;
@@ -1165,16 +1244,19 @@ file, You can obtain one at http://mozil
label="&urlbar.searchSuggestionsNotification.enable;"
accesskey="&urlbar.searchSuggestionsNotification.enable.accesskey;"
onclick="document.getBindingParent(this).dismissSearchSuggestionsNotification(true);"/>
</xul:hbox>
<xul:richlistbox anonid="richlistbox" class="autocomplete-richlistbox"
flex="1"/>
<xul:hbox anonid="footer">
<children/>
+ <xul:vbox anonid="one-off-search-buttons"
+ class="search-one-offs"
+ flex="1"/>
</xul:hbox>
</content>
<implementation>
<field name="_maxResults">0</field>
<field name="_bundle" readonly="true">
Cc["@mozilla.org/intl/stringbundle;1"].
@@ -1187,16 +1269,21 @@ file, You can obtain one at http://mozil
this, "anonid", "search-suggestions-notification"
);
</field>
<field name="footer" readonly="true">
document.getAnonymousElementByAttribute(this, "anonid", "footer");
</field>
+ <field name="oneOffSearchButtons" readonly="true">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "one-off-search-buttons");
+ </field>
+
<method name="openSearchSuggestionsNotificationLearnMoreURL">
<body><![CDATA[
let url = Services.urlFormatter.formatURL(
Services.prefs.getCharPref("app.support.baseURL") + "suggestions"
);
openUILinkIn(url, "tab");
]]></body>
</method>
@@ -1315,16 +1402,19 @@ file, You can obtain one at http://mozil
<body><![CDATA[
if (this.mPopupOpen) {
return;
}
this.mInput = aInput;
this.selectedIndex = this._isFirstResultHeuristic ? 0 : -1;
this.view = aInput.controller.QueryInterface(Components.interfaces.nsITreeView);
+ this.oneOffSearchButtons.popup = this;
+ this.oneOffSearchButtons.textbox = this.input;
+ this.oneOffSearchButtons.telemetryID = "urlbar";
this.invalidate();
var rect = window.document.documentElement.getBoundingClientRect();
var width = rect.right - rect.left;
this.setAttribute("width", width);
// Adjust the direction of the autocomplete popup list based on the textbox direction, bug 649840
var popupDirection = aElement.ownerDocument.defaultView.getComputedStyle(aElement).direction;
@@ -1469,72 +1559,78 @@ file, You can obtain one at http://mozil
resolve();
};
this.addEventListener("transitionend", onTransitionEnd, true);
});
]]>
</body>
</method>
- <method name="onPopupClick">
- <parameter name="aEvent"/>
- <body>
- <![CDATA[
- // Ignore right-clicks
- if (aEvent.button == 2)
- return;
-
- var controller = this.view.QueryInterface(Components.interfaces.nsIAutoCompleteController);
-
- // Check for unmodified left-click, and use default behavior
- if (aEvent.button == 0 && !aEvent.shiftKey && !aEvent.ctrlKey &&
- !aEvent.altKey && !aEvent.metaKey) {
- controller.handleEnter(true);
- return;
- }
-
- // Check for middle-click or modified clicks on the URL bar
- if (gURLBar && this.mInput == gURLBar) {
- var url = controller.getValueAt(this.selectedIndex);
- var options = {};
-
- // close the autocomplete popup and revert the entered address
- this.closePopup();
- controller.handleEscape();
-
- // Check if this is meant to be an action
- let action = this.mInput._parseActionUrl(url);
- if (action) {
- // TODO (bug 1054816): Centralise the implementation of actions
- // into a JS module.
- switch (action.type) {
- case "switchtab": // Fall through.
- case "keyword": // Fall through.
- case "visiturl": {
- url = action.params.url;
- break;
- }
- case "searchengine": {
- [url, options.postData] =
- this.input._parseAndRecordSearchEngineAction(action);
- break;
- }
- default: {
- return;
- }
+ <method name="_visuallySelectedOneOffChanged">
+ <body><![CDATA[
+ // Update all searchengine result items to use the newly selected
+ // engine.
+ for (let item of this.richlistbox.childNodes) {
+ if (item.collapsed) {
+ break;
+ }
+ let url = item.getAttribute("url");
+ if (url) {
+ let action = item._parseActionUrl(url);
+ if (action && action.type == "searchengine") {
+ item._adjustAcItem();
}
}
+ }
+ ]]></body>
+ </method>
- // respect the usual clicking subtleties
- openUILink(url, aEvent, options);
- }
- ]]>
- </body>
+ <!-- This handles keypress changes to the selection among the one-off
+ search buttons and between the one-offs and the listbox. It returns
+ true if the keypress was consumed and false if not. -->
+ <method name="handleKeyPress">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ this.oneOffSearchButtons.handleKeyPress(aEvent, this._matchCount,
+ !this._isFirstResultHeuristic,
+ gBrowser.userTypedValue);
+ return aEvent.defaultPrevented;
+ ]]></body>
</method>
+ <!-- This is called when a one-off is clicked and when "search in new tab"
+ is selected from a one-off context menu. -->
+ <method name="handleOneOffSearch">
+ <parameter name="event"/>
+ <parameter name="engine"/>
+ <parameter name="where"/>
+ <parameter name="params"/>
+ <body><![CDATA[
+ this.input.handleCommand(event, -1, where, params);
+ ]]></body>
+ </method>
+
+ <!-- Result listitems call this to determine which search engine they
+ should show in their labels and include in their url attributes. -->
+ <property name="overrideSearchEngineName" readonly="true">
+ <getter><![CDATA[
+ // When building the popup, autocomplete reuses an item at index i if
+ // that item's url attribute matches the controller's value at index
+ // i, but only if overrideSearchEngineName matches the engine in the
+ // url attribute. To absolutely avoid reusing items that shouldn't be
+ // reused, always return a non-null name here by falling back to the
+ // current engine.
+ let engine =
+ (this.oneOffSearchButtons.visuallySelectedButton &&
+ this.oneOffSearchButtons.visuallySelectedButton.engine) ||
+ Services.search.currentEngine;
+ return engine ? engine.name : null;
+ ]]></getter>
+ </property>
+
<method name="createResultLabel">
<parameter name="item"/>
<parameter name="proposedLabel"/>
<body>
<![CDATA[
let parts = [proposedLabel];
let action = this.mInput._parseActionUrl(item.getAttribute("url"));
@@ -1585,25 +1681,29 @@ file, You can obtain one at http://mozil
this.selectedIndex = 0;
this.richlistbox.suppressMenuItemEvent = false;
this._ignoreNextSelect = false;
}
this.input.gotResultForCurrentQuery = true;
if (this.input.handleEnterWhenGotResult) {
this.input.handleEnterWhenGotResult = false;
- this.input.mController.handleEnter(false);
+ this.input.mController.handleEnter(false, null);
}
]]>
</body>
</method>
</implementation>
<handlers>
+ <handler event="OneOffsVisuallySelectedButtonChanged"><![CDATA[
+ this._visuallySelectedOneOffChanged();
+ ]]></handler>
+
<handler event="mousedown"><![CDATA[
// Required to make the xul:label.text-link elements in the search
// suggestions notification work correctly when clicked on Linux.
// This is copied from the mousedown handler in
// browser-search-autocomplete-result-popup, which apparently had a
// similar problem.
event.preventDefault();
]]></handler>
--- a/browser/components/search/content/search.xml
+++ b/browser/components/search/content/search.xml
@@ -327,20 +327,18 @@
]]></body>
</method>
<method name="handleSearchCommand">
<parameter name="aEvent"/>
<parameter name="aEngine"/>
<parameter name="aForceNewTab"/>
<body><![CDATA[
- var textBox = this._textbox;
- var textValue = textBox.value;
-
var where = "current";
+ let params;
// Open ctrl/cmd clicks on one-off buttons in a new background tab.
if (aEvent && aEvent.originalTarget.getAttribute("anonid") == "search-go-button") {
if (aEvent.button == 2)
return;
where = whereToOpenLink(aEvent, false, true);
}
else if (aForceNewTab) {
@@ -349,63 +347,78 @@
where += "-background";
}
else {
var newTabPref = Services.prefs.getBoolPref("browser.search.openintab");
if (((aEvent instanceof KeyboardEvent) && aEvent.altKey) ^ newTabPref)
where = "tab";
if ((aEvent instanceof MouseEvent) &&
(aEvent.button == 1 || aEvent.getModifierState("Accel"))) {
- where = "tab-background";
+ where = "tab";
+ params = {
+ inBackground: true,
+ };
}
}
+ this.handleSearchCommandWhere(aEvent, aEngine, where, params);
+ ]]></body>
+ </method>
+
+ <method name="handleSearchCommandWhere">
+ <parameter name="aEvent"/>
+ <parameter name="aEngine"/>
+ <parameter name="aWhere"/>
+ <parameter name="aParams"/>
+ <body><![CDATA[
+ var textBox = this._textbox;
+ var textValue = textBox.value;
+
let selection = this.telemetrySearchDetails;
- this.doSearch(textValue, where, aEngine);
+ this.doSearch(textValue, aWhere, aEngine);
if (!selection || (selection.index == -1)) {
- let source = "unknown";
- let type = "unknown";
- let target = aEvent.originalTarget;
- if (aEvent instanceof KeyboardEvent) {
- type = "key";
- if (this._textbox.selectedButton) {
- source = "oneoff";
+ let recorded = this.textbox.popup.oneOffButtons.recordTelemetry(
+ aEvent, aWhere, aParams
+ );
+ if (!recorded) {
+ let source = "unknown";
+ let type = "unknown";
+ let target = aEvent.originalTarget;
+ if (aEvent instanceof KeyboardEvent) {
+ type = "key";
+ } else if (aEvent instanceof MouseEvent) {
+ type = "mouse";
+ if (target.classList.contains("search-panel-header") ||
+ target.parentNode.classList.contains("search-panel-header")) {
+ source = "header";
+ }
+ } else if (aEvent instanceof XULCommandEvent) {
+ if (target.getAttribute("anonid") == "paste-and-search") {
+ source = "paste";
+ }
}
- } else if (aEvent instanceof MouseEvent) {
- type = "mouse";
- if (target.classList.contains("searchbar-engine-one-off-item")) {
- source = "oneoff";
- } else if (target.classList.contains("search-panel-header") ||
- target.parentNode.classList.contains("search-panel-header")) {
- source = "header";
+ if (!aEngine) {
+ aEngine = this.currentEngine;
}
- } else if (aEvent instanceof XULCommandEvent) {
- if (target.getAttribute("anonid") == "paste-and-search") {
- source = "paste";
- } else if (target.getAttribute("anonid") == "search-one-offs-context-open-in-new-tab") {
- source = "oneoff-context";
- }
+ BrowserSearch.recordOneoffSearchInTelemetry(aEngine, source, type,
+ aWhere);
}
-
- if (!aEngine) {
- aEngine = this.currentEngine;
- }
- BrowserSearch.recordOneoffSearchInTelemetry(aEngine, source, type, where);
}
- if (where == "tab-background")
+ if (aWhere == "tab" && aParams && aParams.inBackground)
this.focus();
]]></body>
</method>
<method name="doSearch">
<parameter name="aData"/>
<parameter name="aWhere"/>
<parameter name="aEngine"/>
+ <parameter name="aParams"/>
<body><![CDATA[
var textBox = this._textbox;
// Save the current value in the form history
if (aData && !PrivateBrowsingUtils.isWindowPrivate(window) && this.FormHistory.enabled) {
this.FormHistory.update(
{ op : "bump",
fieldname : textBox.getAttribute("autocompletesearchparam"),
@@ -421,21 +434,23 @@
this.telemetrySearchDetails = null;
if (telemetrySearchDetails && telemetrySearchDetails.index == -1) {
telemetrySearchDetails = null;
}
BrowserSearch.recordSearchInTelemetry(engine, "searchbar", telemetrySearchDetails);
// null parameter below specifies HTML response for search
let params = {
postData: submission.postData,
- inBackground: aWhere == "tab-background"
};
- openUILinkIn(submission.uri.spec,
- aWhere == "tab-background" ? "tab" : aWhere,
- params);
+ if (aParams) {
+ for (let key in aParams) {
+ params[key] = aParams[key];
+ }
+ }
+ openUILinkIn(submission.uri.spec, aWhere, params);
]]></body>
</method>
</implementation>
<handlers>
<handler event="command"><![CDATA[
const target = event.originalTarget;
if (target.engine) {
@@ -640,16 +655,26 @@
we can override just the getter. If that proves to be the case, the setter
can be removed.
-->
<property name="searchParam"
onget="return this.getAttribute('autocompletesearchparam') +
(PrivateBrowsingUtils.isWindowPrivate(window) ? '|private' : '');"
onset="this.setAttribute('autocompletesearchparam', val); return val;"/>
+ <!-- This is implemented so that when textbox.value is set directly (e.g.,
+ by tests), the one-off query is updated. -->
+ <method name="onBeforeValueSet">
+ <parameter name="aValue"/>
+ <body><![CDATA[
+ this.popup.oneOffButtons.query = aValue;
+ return aValue;
+ ]]></body>
+ </method>
+
<!--
This method overrides the autocomplete binding's openPopup (essentially
duplicating the logic from the autocomplete popup binding's
openAutocompletePopup method), modifying it so that the popup is aligned with
the inner textbox, but sized to not extend beyond the search bar border.
-->
<method name="openPopup">
<body><![CDATA[
@@ -740,199 +765,43 @@
this._selectionDetails = null;
}
document.getBindingParent(this).handleSearchCommand(evt, engine);
this.mEnterEvent = null;
]]></body>
</method>
- <field name="_selectedButton"/>
- <property name="selectedButton" onget="return this._selectedButton;">
+ <property name="selectedButton">
+ <getter><![CDATA[
+ return this.popup.oneOffButtons.selectedButton;
+ ]]></getter>
<setter><![CDATA[
- this._changeVisuallySelectedButton(val, true);
+ return this.popup.oneOffButtons.selectedButton = val;
]]></setter>
</property>
- <method name="_changeVisuallySelectedButton">
- <parameter name="val"/>
- <parameter name="aUpdateLogicallySelectedButton"/>
- <body><![CDATA[
- let list = this.getSelectableButtons();
- let visuallySelectedButton = list.find(button => {
- return button.getAttribute("selected") == "true"
- });
- if (visuallySelectedButton)
- visuallySelectedButton.removeAttribute("selected");
-
- let textbox = document.getBindingParent(this).textbox;
- let header =
- document.getAnonymousElementByAttribute(this.popup, "anonid",
- "search-panel-one-offs-header");
- // Avoid selecting dummy buttons.
- if (val && !val.classList.contains("dummy")) {
- val.setAttribute("selected", "true");
- if (aUpdateLogicallySelectedButton)
- this._selectedButton = val;
- if (val.classList.contains("searchbar-engine-one-off-item")) {
- let headerEngineText =
- document.getAnonymousElementByAttribute(this.popup, "anonid",
- "searchbar-oneoffheader-engine");
- header.selectedIndex = 2;
- headerEngineText.value = val.engine.name;
- }
- else {
- header.selectedIndex = textbox.value ? 1 : 0;
- }
- this.setAttribute("aria-activedescendant", val.id);
- } else {
- header.selectedIndex = textbox.value ? 1 : 0;
- this.removeAttribute("aria-activedescendant");
- if (aUpdateLogicallySelectedButton)
- this._selectedButton = null;
- }
- ]]></body>
- </method>
-
- <method name="getSelectableButtons">
- <parameter name="aCycleEngines"/>
- <body><![CDATA[
- let buttons = [];
- let oneOff = document.getAnonymousElementByAttribute(this.popup, "anonid",
- "search-panel-one-offs");
- for (oneOff = oneOff.firstChild; oneOff; oneOff = oneOff.nextSibling) {
- if (oneOff.classList.contains("dummy"))
- break;
- buttons.push(oneOff);
- }
-
- if (aCycleEngines)
- return buttons;
-
- let addEngine =
- document.getAnonymousElementByAttribute(this.popup, "anonid", "add-engines");
- for (addEngine = addEngine.firstChild; addEngine; addEngine = addEngine.nextSibling)
- buttons.push(addEngine);
-
- buttons.push(document.getAnonymousElementByAttribute(this.popup, "anonid",
- "search-settings"));
- return buttons;
- ]]></body>
- </method>
-
- <method name="advanceSelection">
- <parameter name="aForward"/>
- <parameter name="aSkipSuggestions"/>
- <parameter name="aCycleEngines"/>
- <body><![CDATA[
- let popup = this.popup;
- let list = document.getAnonymousElementByAttribute(popup, "anonid",
- "search-panel-one-offs");
- let selectedButton = this.selectedButton;
- let buttons = this.getSelectableButtons(aCycleEngines);
-
- let suggestionsHidden;
- if (!aSkipSuggestions) {
- let suggestions = document.getAnonymousElementByAttribute(popup, "anonid", "tree");
- suggestionsHidden = suggestions.getAttribute("collapsed") == "true";
- aSkipSuggestions = suggestionsHidden;
- }
-
- // If the last suggestion is selected, DOWN selects the first button.
- if (!aSkipSuggestions && aForward &&
- popup.selectedIndex + 1 == popup.view.rowCount) {
- this.selectedButton = buttons[0];
- return false;
- }
-
- // If a one-off is selected and no suggestion is selected (or we skip them)
- if (selectedButton && (popup.selectedIndex == -1 || aSkipSuggestions)) {
- // cycle through one-off buttons.
- let index = buttons.indexOf(selectedButton);
- if (aForward)
- ++index;
- else
- --index;
- if (index >= 0 && index < buttons.length)
- this.selectedButton = buttons[index];
- else
- this.selectedButton = null;
-
- if (this.selectedButton || aCycleEngines || suggestionsHidden)
- return true;
-
- // Set the selectedIndex to something that will make
- // handleKeyNavigation (called by autocomplete.xml's onKeyPress
- // method) reset the text field value to what the user typed.
- // Doesn't work when aSkipSuggestions=true, see bug 1124747.
- if (aForward)
- popup.selectedIndex = popup.view.rowCount - 1;
- else
- popup.selectedIndex = popup.view.rowCount;
- return false;
- }
-
- if (!selectedButton) {
- // If no selection, select the first button or ...
- if (aForward && aSkipSuggestions) {
- this.selectedButton = buttons[0];
- return true;
- }
-
- if (!aForward && (aCycleEngines || suggestionsHidden ||
- (!aSkipSuggestions && popup.selectedIndex == -1))) {
- // the last button.
- this.selectedButton = buttons[buttons.length - 1];
- return true;
- }
- }
-
- return false;
- ]]></body>
- </method>
<method name="handleKeyboardNavigation">
<parameter name="aEvent"/>
<body><![CDATA[
let popup = this.popup;
if (!popup.popupOpen)
return;
- let list = document.getAnonymousElementByAttribute(popup, "anonid",
- "search-panel-one-offs");
- if (!list) // remove this check when removing the old search UI.
- return;
-
// accel + up/down changes the default engine and shouldn't affect
// the selection on the one-off buttons.
if (aEvent.getModifierState("Accel"))
return;
- let stopEvent = false;
-
- // Alt + up/down is very similar to (shift +) tab but differs in that
- // it loops through the list, whereas tab will move the focus out.
- if (aEvent.altKey &&
- (aEvent.keyCode == KeyEvent.DOM_VK_DOWN ||
- aEvent.keyCode == KeyEvent.DOM_VK_UP)) {
- stopEvent =
- this.advanceSelection(aEvent.keyCode == KeyEvent.DOM_VK_DOWN,
- true, true);
- }
- else if (aEvent.keyCode == KeyEvent.DOM_VK_DOWN ||
- aEvent.keyCode == KeyEvent.DOM_VK_UP) {
- stopEvent = this.advanceSelection(aEvent.keyCode == KeyEvent.DOM_VK_DOWN);
- }
- else if (aEvent.keyCode == KeyEvent.DOM_VK_TAB) {
- stopEvent = this.advanceSelection(!aEvent.shiftKey, true);
- }
-
- if (stopEvent) {
- aEvent.preventDefault();
- aEvent.stopPropagation();
- }
+ let suggestions =
+ document.getAnonymousElementByAttribute(popup, "anonid", "tree");
+ let suggestionsHidden =
+ suggestions.getAttribute("collapsed") == "true";
+ let numItems = suggestionsHidden ? 0 : this.popup.view.rowCount;
+ this.popup.oneOffButtons.handleKeyPress(aEvent, numItems, true);
]]></body>
</method>
<!-- nsIController -->
<field name="searchbarController" readonly="true"><![CDATA[({
_self: this,
supportsCommand: function(aCommand) {
return aCommand == "cmd_clearhistory" ||
@@ -962,16 +831,22 @@
default:
// do nothing with unrecognized command
}
}
})]]></field>
</implementation>
<handlers>
+ <handler event="input"><![CDATA[
+ if (!this.value) {
+ this.popup.removeAttribute("showonlysettings");
+ }
+ ]]></handler>
+
<handler event="keypress" phase="capturing"
action="return this.handleKeyboardNavigation(event);"/>
<handler event="keypress" keycode="VK_UP" modifiers="accel"
phase="capturing"
action="document.getBindingParent(this).selectEngine(event, false);"/>
<handler event="keypress" keycode="VK_DOWN" modifiers="accel"
@@ -1008,91 +883,60 @@
]]>
</handler>
</handlers>
</binding>
<binding id="browser-search-autocomplete-result-popup" extends="chrome://browser/content/urlbarBindings.xml#browser-autocomplete-result-popup">
<resources>
+ <stylesheet src="chrome://browser/content/search/searchbarBindings.css"/>
<stylesheet src="chrome://browser/skin/searchbar.css"/>
</resources>
- <content ignorekeys="true" level="top" consumeoutsideclicks="never" context="_child">
+ <content ignorekeys="true" level="top" consumeoutsideclicks="never">
<xul:hbox anonid="searchbar-engine" xbl:inherits="showonlysettings"
class="search-panel-header search-panel-current-engine">
<xul:image class="searchbar-engine-image" xbl:inherits="src"/>
<xul:label anonid="searchbar-engine-name" flex="1" crop="end"
role="presentation"/>
</xul:hbox>
<xul:tree anonid="tree" flex="1"
class="autocomplete-tree plain search-panel-tree"
hidecolumnpicker="true" seltype="single">
<xul:treecols anonid="treecols">
<xul:treecol id="treecolAutoCompleteValue" class="autocomplete-treecol" flex="1" overflow="true"/>
</xul:treecols>
<xul:treechildren class="autocomplete-treebody"/>
</xul:tree>
- <xul:deck anonid="search-panel-one-offs-header"
- selectedIndex="0"
- class="search-panel-header search-panel-current-input">
- <xul:label anonid="searchbar-oneoffheader-search" value="&searchWithHeader.label;"/>
- <xul:hbox anonid="search-panel-searchforwith"
- class="search-panel-current-input">
- <xul:label anonid="searchbar-oneoffheader-before" value="&searchFor.label;"/>
- <xul:label anonid="searchbar-oneoffheader-searchtext" flex="1" crop="end" class="search-panel-input-value"/>
- <xul:label anonid="searchbar-oneoffheader-after" flex="10000" value="&searchWith.label;"/>
- </xul:hbox>
- <xul:hbox anonid="search-panel-searchonengine"
- class="search-panel-current-input">
- <xul:label anonid="searchbar-oneoffheader-beforeengine" value="&search.label;"/>
- <xul:label anonid="searchbar-oneoffheader-engine" flex="1" crop="end"
- class="search-panel-input-value"/>
- <xul:label anonid="searchbar-oneoffheader-afterengine" flex="10000"
- value="&searchAfter.label;"/>
- </xul:hbox>
- </xul:deck>
- <xul:description anonid="search-panel-one-offs"
- role="group"
- class="search-panel-one-offs"/>
- <xul:vbox anonid="add-engines"/>
- <xul:button anonid="search-settings"
- oncommand="showSettings();"
- class="search-setting-button search-panel-header"
- label="&changeSearchSettings.button;"/>
- <xul:menupopup anonid="search-one-offs-context-menu">
- <xul:menuitem anonid="search-one-offs-context-open-in-new-tab"
- label="&searchInNewTab.label;"
- accesskey="&searchInNewTab.accesskey;"/>
- <xul:menuitem anonid="search-one-offs-context-set-default"
- label="&searchSetAsDefault.label;"
- accesskey="&searchSetAsDefault.accesskey;"/>
- </xul:menupopup>
+ <xul:vbox anonid="search-one-off-buttons" class="search-one-offs"/>
</content>
<implementation>
<!-- Popup rollup is triggered by native events before the mousedown event
reaches the DOM. The will be set to true by the popuphiding event and
false after the mousedown event has been triggered to detect what
caused rollup. -->
<field name="_isHiding">false</field>
- <!-- When a context menu is opened on a one-off button, this is set to the
- engine of that button for use with the context menu actions. -->
- <field name="_contextEngine">null</field>
<field name="_bundle">null</field>
<property name="bundle" readonly="true">
<getter>
<![CDATA[
if (!this._bundle) {
const kBundleURI = "chrome://browser/locale/search.properties";
this._bundle = Services.strings.createBundle(kBundleURI);
}
return this._bundle;
]]>
</getter>
</property>
+ <field name="oneOffButtons" readonly="true">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "search-one-off-buttons");
+ </field>
+
<method name="updateHeader">
<body><![CDATA[
let currentEngine = Services.search.currentEngine;
let uri = currentEngine.iconURI;
if (uri) {
this.setAttribute("src", uri.spec);
}
else {
@@ -1105,66 +949,56 @@
[currentEngine.name], 1);
document.getAnonymousElementByAttribute(this, "anonid", "searchbar-engine-name")
.setAttribute("value", headerText);
document.getAnonymousElementByAttribute(this, "anonid", "searchbar-engine")
.engine = currentEngine;
]]></body>
</method>
- <method name="showSettings">
+ <!-- This is called when a one-off is clicked and when "search in new tab"
+ is selected from a one-off context menu. -->
+ <method name="handleOneOffSearch">
+ <parameter name="event"/>
+ <parameter name="engine"/>
+ <parameter name="where"/>
+ <parameter name="params"/>
<body><![CDATA[
- BrowserUITelemetry.countSearchSettingsEvent("searchbar");
- openPreferences("paneSearch");
- // If the preference tab was already selected, the panel doesn't
- // close itself automatically.
- BrowserSearch.searchBar._textbox.closePopup();
+ let searchbar = document.getElementById("searchbar");
+ searchbar.handleSearchCommandWhere(event, engine, where, params);
]]></body>
</method>
+ </implementation>
- <constructor><![CDATA[
- // Prevent popup events from the context menu from reaching the autocomplete
- // binding (or other listeners).
- let menu = document.getAnonymousElementByAttribute(this, "anonid", "search-one-offs-context-menu");
- let listener = aEvent => aEvent.stopPropagation();
- menu.addEventListener("popupshowing", listener);
- menu.addEventListener("popuphiding", listener);
- menu.addEventListener("popupshown", aEvent => {
- this._ignoreMouseEvents = true;
- aEvent.stopPropagation();
- });
- menu.addEventListener("popuphidden", aEvent => {
- this._ignoreMouseEvents = false;
- aEvent.stopPropagation();
- });
- ]]></constructor>
- </implementation>
<handlers>
- <handler event="popuphidden"><![CDATA[
- Services.tm.mainThread.dispatch(function() {
- document.getElementById("searchbar").textbox.selectedButton = null;
- }, Ci.nsIThread.DISPATCH_NORMAL);
- this._contextEngine = null;
- ]]></handler>
-
- <handler event="contextmenu"><![CDATA[
- let target = event.originalTarget;
- // Prevent the context menu from appearing except on the one off buttons.
- if (!target.classList.contains("searchbar-engine-one-off-item") ||
- target.classList.contains("dummy")) {
- event.preventDefault();
- return;
- }
- document.getAnonymousElementByAttribute(this, "anonid", "search-one-offs-context-set-default")
- .setAttribute("disabled", target.engine == Services.search.currentEngine);
-
- this._contextEngine = target.engine;
+ <handler event="OneOffsEngineInstalled"><![CDATA[
+ BrowserSearch.searchBar.openSuggestionsPanel();
]]></handler>
<handler event="popupshowing"><![CDATA[
+ if (!this.oneOffButtons.popup) {
+ // The panel width only spans to the textbox size, but we also want it
+ // to include the magnifier icon's width.
+ let ltr = getComputedStyle(this).direction == "ltr";
+ let magnifierWidth = parseInt(getComputedStyle(this)[
+ ltr ? "marginLeft" : "marginRight"
+ ]) * -1;
+ // Ensure the panel is wide enough to fit at least 3 engines.
+ let minWidth = Math.max(
+ parseInt(this.width) + magnifierWidth,
+ this.oneOffButtons.buttonWidth * 3
+ );
+ this.style.minWidth = minWidth + "px";
+
+ // Set popup after setting the minWidth since it builds the buttons.
+ this.oneOffButtons.popup = this;
+ this.oneOffButtons.textbox = this.input;
+ this.oneOffButtons.telemetryID = "searchbar";
+ }
+
// First handle deciding if we are showing the reduced version of the
// popup containing only the preferences button. We do this if the
// glass icon has been clicked if the text field is empty.
let searchbar = document.getElementById("searchbar");
let tree = document.getAnonymousElementByAttribute(this, "anonid",
"tree")
if (searchbar.hasAttribute("showonlysettings")) {
searchbar.removeAttribute("showonlysettings");
@@ -1180,190 +1014,869 @@
// The autocomplete binding itself will take care of uncollapsing later,
// if we currently have no rows but end up having some in the future
// when the search string changes
tree.collapsed = !tree.view || !tree.view.rowCount;
}
// Show the current default engine in the top header of the panel.
this.updateHeader();
+ ]]></handler>
- // Update the 'Search for <keywords> with:" header.
- let headerSearchText =
- document.getAnonymousElementByAttribute(this, "anonid",
- "searchbar-oneoffheader-searchtext");
- let headerPanel =
- document.getAnonymousElementByAttribute(this, "anonid",
- "search-panel-one-offs-header");
- let list = document.getAnonymousElementByAttribute(this, "anonid",
- "search-panel-one-offs");
- let textbox = searchbar.textbox;
- let self = this;
- let inputHandler = function() {
- headerSearchText.setAttribute("value", textbox.value);
+ <handler event="popuphiding"><![CDATA[
+ this._isHiding = true;
+ setTimeout(() => {
+ this._isHiding = false;
+ }, 0);
+ ]]></handler>
+ </handlers>
+
+ </binding>
+
+ <!-- Used for additional open search providers in the search panel. -->
+ <binding id="addengine-icon" extends="xul:box">
+ <content>
+ <xul:image class="addengine-icon" xbl:inherits="src"/>
+ <xul:image class="addengine-badge"/>
+ </content>
+ </binding>
+
+
+ <binding id="search-one-offs">
+
+ <content context="_child">
+ <xul:deck anonid="search-panel-one-offs-header"
+ selectedIndex="0"
+ class="search-panel-header search-panel-current-input">
+ <xul:label anonid="searchbar-oneoffheader-search"
+ value="&searchWithHeader.label;"/>
+ <xul:hbox anonid="search-panel-searchforwith"
+ class="search-panel-current-input">
+ <xul:label anonid="searchbar-oneoffheader-before"
+ value="&searchFor.label;"/>
+ <xul:label anonid="searchbar-oneoffheader-searchtext"
+ class="search-panel-input-value"
+ flex="1"
+ crop="end"/>
+ <xul:label anonid="searchbar-oneoffheader-after"
+ flex="10000"
+ value="&searchWith.label;"/>
+ </xul:hbox>
+ <xul:hbox anonid="search-panel-searchonengine"
+ class="search-panel-current-input">
+ <xul:label anonid="searchbar-oneoffheader-beforeengine"
+ value="&search.label;"/>
+ <xul:label anonid="searchbar-oneoffheader-engine"
+ class="search-panel-input-value"
+ flex="1"
+ crop="end"/>
+ <xul:label anonid="searchbar-oneoffheader-afterengine"
+ flex="10000"
+ value="&searchAfter.label;"/>
+ </xul:hbox>
+ </xul:deck>
+ <xul:description anonid="search-panel-one-offs"
+ role="group"
+ class="search-panel-one-offs"/>
+ <xul:vbox anonid="add-engines"/>
+ <xul:button anonid="search-settings"
+ oncommand="showSettings();"
+ class="search-setting-button search-panel-header"
+ label="&changeSearchSettings.button;"/>
+ <xul:menupopup anonid="search-one-offs-context-menu">
+ <xul:menuitem anonid="search-one-offs-context-open-in-new-tab"
+ label="&searchInNewTab.label;"
+ accesskey="&searchInNewTab.accesskey;"/>
+ <xul:menuitem anonid="search-one-offs-context-set-default"
+ label="&searchSetAsDefault.label;"
+ accesskey="&searchSetAsDefault.accesskey;"/>
+ </xul:menupopup>
+ </content>
+
+ <implementation implements="nsIDOMEventListener">
+
+ <!-- Width in pixels of the one-off buttons. -->
+ <property name="buttonWidth" readonly="true">
+ <getter><![CDATA[
+ // 49px is the min-width of each search engine button,
+ // adapt this const when changing the css.
+ // It's actually 48px + 1px of right border.
+ return 49;
+ ]]></getter>
+ </property>
+
+ <!-- The popup that contains the one-offs. This is required, so it should
+ never be null or undefined, except possibly before the one-offs are
+ used. -->
+ <property name="popup">
+ <getter><![CDATA[
+ return this._popup;
+ ]]></getter>
+ <setter><![CDATA[
+ if (this._popup != val) {
+ let events = [
+ "popupshowing",
+ "popuphidden",
+ ];
+ if (this._popup) {
+ for (let event of events) {
+ this._popup.removeEventListener(event, this);
+ }
+ }
+ if (val) {
+ for (let event of events) {
+ val.addEventListener(event, this);
+ }
+ }
+ this._popup = val;
+
+ // If the popup is already open, rebuild the one-offs now. The
+ // popup may be opening, so check that the state is not closed
+ // instead of checking popupOpen.
+ if (val.state != "closed") {
+ this._rebuild();
+ }
+ }
+ return val;
+ ]]></setter>
+ </property>
+ <field name="_popup"><![CDATA[
+ null
+ ]]></field>
+
+ <!-- The textbox associated with the one-offs. May be null/undefined, and
+ in that case you should update the query property manually. -->
+ <property name="textbox">
+ <getter><![CDATA[
+ return this._textbox;
+ ]]></getter>
+ <setter><![CDATA[
+ if (this._textbox != val) {
+ if (this._textbox) {
+ this._textbox.removeEventListener("input", this);
+ }
+ if (val) {
+ val.addEventListener("input", this);
+ }
+ this._textbox = val;
+ }
+ ]]></setter>
+ </property>
+ <field name="_textbox"><![CDATA[
+ null
+ ]]></field>
+
+ <!-- Set this to a string that identifies your one-offs consumer. It'll
+ be appended to telemetry recorded with recordTelemetry(). -->
+ <field name="telemetryID"><![CDATA[
+ ""
+ ]]></field>
+
+ <!-- The query string currently shown in the one-offs. If the textbox
+ property is non-null, then this is automatically updated on
+ input. -->
+ <property name="query">
+ <getter><![CDATA[
+ return this._query;
+ ]]></getter>
+ <setter><![CDATA[
+ this._query = val;
+ if (this.popup && this.popup.popupOpen) {
+ this._updateAfterQueryChanged();
+ }
+ return val;
+ ]]></setter>
+ </property>
+ <field name="_query"><![CDATA[
+ ""
+ ]]></field>
+
+ <!-- The selected one-off, a xul:button, including the add-engine button
+ and the search-settings button. Null if no one-off is selected. -->
+ <property name="selectedButton">
+ <getter><![CDATA[
+ return this._selectedButton;
+ ]]></getter>
+ <setter><![CDATA[
+ this._changeVisuallySelectedButton(val, true);
+ return val;
+ ]]></setter>
+ </property>
+ <field name="_selectedButton"><![CDATA[
+ null
+ ]]></field>
+
+ <!-- The index of the selected one-off, including the add-engine button
+ and the search-settings button. -1 if no one-off is selected. -->
+ <property name="selectedButtonIndex">
+ <getter><![CDATA[
+ let buttons = this.getSelectableButtons(true);
+ for (let i = 0; i < buttons.length; i++) {
+ if (buttons[i] == this._selectedButton) {
+ return i;
+ }
+ }
+ return -1;
+ ]]></getter>
+ <setter><![CDATA[
+ let buttons = this.getSelectableButtons(true);
+ this.selectedButton = buttons[val];
+ return val;
+ ]]></setter>
+ </property>
+
+ <!-- The visually selected one-off is the same as the selected one-off
+ unless a one-off is moused over. In that case, the visually selected
+ one-off is the moused-over one-off, which may be different from the
+ selected one-off. The visually selected one-off is always the one
+ that is visually highlighted. Includes the add-engine button and the
+ search-settings button. A xul:button. -->
+ <property name="visuallySelectedButton" readonly="true">
+ <getter><![CDATA[
+ return this.getSelectableButtons(true).find(button => {
+ return button.getAttribute("selected") == "true";
+ });
+ ]]></getter>
+ </property>
+
+ <!-- The number of one-offs, including the add-engine button (if shown)
+ and the search-settings button. -->
+ <property name="numButtons" readonly="true">
+ <getter><![CDATA[
+ return this.getSelectableButtons(true).length;
+ ]]></getter>
+ </property>
+
+ <property name="bundle" readonly="true">
+ <getter><![CDATA[
+ if (!this._bundle) {
+ const kBundleURI = "chrome://browser/locale/search.properties";
+ this._bundle = Services.strings.createBundle(kBundleURI);
+ }
+ return this._bundle;
+ ]]></getter>
+ </property>
+ <field name="_bundle"><![CDATA[
+ null
+ ]]></field>
+
+ <!-- When a context menu is opened on a one-off button, this is set to the
+ engine of that button for use with the context menu actions. -->
+ <field name="_contextEngine"><![CDATA[
+ null
+ ]]></field>
+
+ <constructor><![CDATA[
+ // Prevent popup events from the context menu from reaching the autocomplete
+ // binding (or other listeners).
+ let menu = document.getAnonymousElementByAttribute(this, "anonid", "search-one-offs-context-menu");
+ let listener = aEvent => aEvent.stopPropagation();
+ menu.addEventListener("popupshowing", listener);
+ menu.addEventListener("popuphiding", listener);
+ menu.addEventListener("popupshown", aEvent => {
+ this._ignoreMouseEvents = true;
+ aEvent.stopPropagation();
+ });
+ menu.addEventListener("popuphidden", aEvent => {
+ this._ignoreMouseEvents = false;
+ aEvent.stopPropagation();
+ });
+ ]]></constructor>
+
+ <!-- This handles events outside the one-off buttons, like on the popup
+ and textbox. -->
+ <method name="handleEvent">
+ <parameter name="event"/>
+ <body><![CDATA[
+ switch (event.type) {
+ case "input":
+ // The urlbar's value property can be a moz-action URI, but we want
+ // the value that the user sees, which is textValue. So see if the
+ // textbox has a textValue property, and use it if so.
+ this.query = typeof(event.target.textValue) == "string" ?
+ event.target.textValue :
+ event.target.value;
+ break;
+ case "popupshowing":
+ this._rebuild();
+ break;
+ case "popuphidden":
+ Services.tm.mainThread.dispatch(() => {
+ this.selectedButton = null;
+ }, Ci.nsIThread.DISPATCH_NORMAL);
+ this._contextEngine = null;
+ break;
+ }
+ ]]></body>
+ </method>
+
+ <method name="showSettings">
+ <body><![CDATA[
+ BrowserUITelemetry.countSearchSettingsEvent(this.telemetryID);
+ openPreferences("paneSearch");
+ // If the preference tab was already selected, the panel doesn't
+ // close itself automatically.
+ this.popup.hidePopup();
+ ]]></body>
+ </method>
+
+ <!-- Updates the parts of the UI that show the query string. -->
+ <method name="_updateAfterQueryChanged">
+ <body><![CDATA[
+ let headerSearchText =
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "searchbar-oneoffheader-searchtext");
+ let headerPanel =
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "search-panel-one-offs-header");
+ let list = document.getAnonymousElementByAttribute(this, "anonid",
+ "search-panel-one-offs");
+ headerSearchText.setAttribute("value", this.query);
let groupText;
let isOneOffSelected =
this.selectedButton &&
this.selectedButton.classList.contains("searchbar-engine-one-off-item");
// Typing de-selects the settings or opensearch buttons at the bottom
// of the search panel, as typing shows the user intends to search.
if (this.selectedButton && !isOneOffSelected)
this.selectedButton = null;
- if (textbox.value) {
- self.removeAttribute("showonlysettings");
+ if (this.query) {
groupText = headerSearchText.previousSibling.value +
'"' + headerSearchText.value + '"' +
headerSearchText.nextSibling.value;
if (!isOneOffSelected)
headerPanel.selectedIndex = 1;
}
else {
let noSearchHeader =
- document.getAnonymousElementByAttribute(self, "anonid",
+ document.getAnonymousElementByAttribute(this, "anonid",
"searchbar-oneoffheader-search");
groupText = noSearchHeader.value;
if (!isOneOffSelected)
headerPanel.selectedIndex = 0;
}
list.setAttribute("aria-label", groupText);
- };
- textbox.addEventListener("input", inputHandler);
- this.addEventListener("popuphiding", function hiding() {
- textbox.removeEventListener("input", inputHandler);
- this.removeEventListener("popuphiding", hiding);
- });
- inputHandler();
+ ]]></body>
+ </method>
+
+ <!-- Builds all the UI. -->
+ <method name="_rebuild">
+ <body><![CDATA[
+ // Update the 'Search for <keywords> with:" header.
+ this._updateAfterQueryChanged();
+
+ let list = document.getAnonymousElementByAttribute(this, "anonid",
+ "search-panel-one-offs");
+
+ // Handle opensearch items. This needs to be done before building the
+ // list of one off providers, as that code will return early if all the
+ // alternative engines are hidden.
+ let addEngineList =
+ document.getAnonymousElementByAttribute(this, "anonid", "add-engines");
+ while (addEngineList.firstChild)
+ addEngineList.firstChild.remove();
+
+ const kXULNS =
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+ let addEngines = gBrowser.selectedBrowser.engines;
+ if (addEngines && addEngines.length > 0) {
+ for (let engine of addEngines) {
+ let button = document.createElementNS(kXULNS, "button");
+ let label = this.bundle.formatStringFromName("cmd_addFoundEngine",
+ [engine.title], 1);
+ button.id = "searchbar-add-engine-" + engine.title.replace(/ /g, '-');
+ button.setAttribute("class", "addengine-item");
+ button.setAttribute("label", label);
+ button.setAttribute("pack", "start");
+
+ button.setAttribute("crop", "end");
+ button.setAttribute("tooltiptext", engine.uri);
+ button.setAttribute("uri", engine.uri);
+ if (engine.icon) {
+ button.setAttribute("image", engine.icon);
+ }
+ button.setAttribute("title", engine.title);
+ addEngineList.appendChild(button);
+ }
+ }
+
+ // Finally, build the list of one-off buttons.
+ while (list.firstChild)
+ list.firstChild.remove();
+
+ let Preferences =
+ Cu.import("resource://gre/modules/Preferences.jsm", {}).Preferences;
+ let pref = Preferences.get("browser.search.hiddenOneOffs");
+ let hiddenList = pref ? pref.split(",") : [];
- // Handle opensearch items. This needs to be done before building the
- // list of one off providers, as that code will return early if all the
- // alternative engines are hidden.
- let addEngineList =
- document.getAnonymousElementByAttribute(this, "anonid", "add-engines");
- while (addEngineList.firstChild)
- addEngineList.firstChild.remove();
+ let currentEngineName = Services.search.currentEngine.name;
+ let engines = Services.search.getVisibleEngines()
+ .filter(e => e.name != currentEngineName &&
+ hiddenList.indexOf(e.name) == -1);
+
+ let header = document.getAnonymousElementByAttribute(this, "anonid",
+ "search-panel-one-offs-header")
+ // header is a xul:deck so collapsed doesn't work on it, see bug 589569.
+ header.hidden = list.collapsed = !engines.length;
- const kXULNS =
- "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ if (!engines.length)
+ return;
+
+ let panel = this.popup;
+ let panelWidth = parseInt(panel.clientWidth);
+ // The + 1 is because the last button doesn't have a right border.
+ let enginesPerRow = Math.floor((panelWidth + 1) / this.buttonWidth);
+ let buttonWidth = Math.floor(panelWidth / enginesPerRow);
+ // There will be an emtpy area of:
+ // panelWidth - enginesPerRow * buttonWidth px
+ // at the end of each row.
- let addEngines = gBrowser.selectedBrowser.engines;
- if (addEngines && addEngines.length > 0) {
- for (let engine of addEngines) {
+ // If the <description> tag with the list of search engines doesn't have
+ // a fixed height, the panel will be sized incorrectly, causing the bottom
+ // of the suggestion <tree> to be hidden.
+ let rowCount = Math.ceil(engines.length / enginesPerRow);
+ let height = rowCount * 33; // 32px per row, 1px border.
+ list.setAttribute("height", height + "px");
+
+ // Ensure we can refer to the settings button by ID:
+ let settingsEl = document.getAnonymousElementByAttribute(this, "anonid", "search-settings");
+ settingsEl.id = this.id + "-anon-search-settings";
+
+ let dummyItems = enginesPerRow - (engines.length % enginesPerRow || enginesPerRow);
+ for (let i = 0; i < engines.length; ++i) {
+ let engine = engines[i];
let button = document.createElementNS(kXULNS, "button");
- let label = this.bundle.formatStringFromName("cmd_addFoundEngine",
- [engine.title], 1);
- button.id = "searchbar-add-engine-" + engine.title.replace(/ /g, '-');
- button.setAttribute("class", "addengine-item");
- button.setAttribute("label", label);
- button.setAttribute("pack", "start");
+ button.id = "searchbar-engine-one-off-item-" + engine.name.replace(/ /g, '-');
+ let uri = "chrome://browser/skin/search-engine-placeholder.png";
+ if (engine.iconURI) {
+ uri = engine.iconURI.spec;
+ }
+ button.setAttribute("image", uri);
+ button.setAttribute("class", "searchbar-engine-one-off-item");
+ button.setAttribute("tooltiptext", engine.name);
+ button.setAttribute("width", buttonWidth);
+ button.engine = engine;
+
+ if ((i + 1) % enginesPerRow == 0)
+ button.classList.add("last-of-row");
+
+ if (i >= engines.length + dummyItems - enginesPerRow)
+ button.classList.add("last-row");
+
+ list.appendChild(button);
+ }
- button.setAttribute("crop", "end");
- button.setAttribute("tooltiptext", engine.uri);
- button.setAttribute("uri", engine.uri);
- if (engine.icon) {
- button.setAttribute("image", engine.icon);
+ while (dummyItems) {
+ let button = document.createElementNS(kXULNS, "button");
+ button.setAttribute("class", "searchbar-engine-one-off-item dummy last-row");
+ button.setAttribute("width", buttonWidth);
+
+ if (!--dummyItems)
+ button.classList.add("last-of-row");
+
+ list.appendChild(button);
+ }
+ ]]></body>
+ </method>
+
+ <method name="_changeVisuallySelectedButton">
+ <parameter name="val"/>
+ <parameter name="aUpdateLogicallySelectedButton"/>
+ <body><![CDATA[
+ let visuallySelectedButton = this.visuallySelectedButton;
+ if (visuallySelectedButton)
+ visuallySelectedButton.removeAttribute("selected");
+
+ let header =
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "search-panel-one-offs-header");
+ // Avoid selecting dummy buttons.
+ if (val && !val.classList.contains("dummy")) {
+ val.setAttribute("selected", "true");
+ if (val.classList.contains("searchbar-engine-one-off-item")) {
+ let headerEngineText =
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "searchbar-oneoffheader-engine");
+ header.selectedIndex = 2;
+ headerEngineText.value = val.engine.name;
}
- button.setAttribute("title", engine.title);
- addEngineList.appendChild(button);
+ else {
+ header.selectedIndex = this.query ? 1 : 0;
+ }
+ this.setAttribute("aria-activedescendant", val.id);
+ } else {
+ val = null;
+ header.selectedIndex = this.query ? 1 : 0;
+ this.removeAttribute("aria-activedescendant");
}
- }
+
+ if (aUpdateLogicallySelectedButton)
+ this._selectedButton = val;
- // Finally, build the list of one-off buttons.
- while (list.firstChild)
- list.firstChild.remove();
+ let event = document.createEvent("Events");
+ event.initEvent("OneOffsVisuallySelectedButtonChanged", true, false);
+ this.dispatchEvent(event);
+ ]]></body>
+ </method>
- let Preferences =
- Cu.import("resource://gre/modules/Preferences.jsm", {}).Preferences;
- let pref = Preferences.get("browser.search.hiddenOneOffs");
- let hiddenList = pref ? pref.split(",") : [];
+ <method name="getSelectableButtons">
+ <parameter name="aIncludeSettingsButton"/>
+ <body><![CDATA[
+ let buttons = [];
+ let oneOff = document.getAnonymousElementByAttribute(this, "anonid",
+ "search-panel-one-offs");
+ for (oneOff = oneOff.firstChild; oneOff; oneOff = oneOff.nextSibling) {
+ if (oneOff.classList.contains("dummy"))
+ break;
+ buttons.push(oneOff);
+ }
+
+ if (!aIncludeSettingsButton)
+ return buttons;
+
+ let addEngine =
+ document.getAnonymousElementByAttribute(this, "anonid", "add-engines");
+ for (addEngine = addEngine.firstChild; addEngine; addEngine = addEngine.nextSibling)
+ buttons.push(addEngine);
+
+ buttons.push(document.getAnonymousElementByAttribute(this, "anonid",
+ "search-settings"));
+ return buttons;
+ ]]></body>
+ </method>
- let currentEngineName = Services.search.currentEngine.name;
- let engines = Services.search.getVisibleEngines()
- .filter(e => e.name != currentEngineName &&
- hiddenList.indexOf(e.name) == -1);
+ <method name="handleSearchCommand">
+ <parameter name="aEvent"/>
+ <parameter name="aEngine"/>
+ <parameter name="aForceNewTab"/>
+ <body><![CDATA[
+ let where = "current";
+ let params;
- let header = document.getAnonymousElementByAttribute(this, "anonid",
- "search-panel-one-offs-header")
- // header is a xul:deck so collapsed doesn't work on it, see bug 589569.
- header.hidden = list.collapsed = !engines.length;
+ // Open ctrl/cmd clicks on one-off buttons in a new background tab.
+ if (aForceNewTab) {
+ where = "tab";
+ if (Services.prefs.getBoolPref("browser.tabs.loadInBackground")) {
+ params = {
+ inBackground: true,
+ };
+ }
+ }
+ else {
+ var newTabPref = Services.prefs.getBoolPref("browser.search.openintab");
+ if (((aEvent instanceof KeyboardEvent) && aEvent.altKey) ^ newTabPref)
+ where = "tab";
+ if ((aEvent instanceof MouseEvent) &&
+ (aEvent.button == 1 || aEvent.getModifierState("Accel"))) {
+ where = "tab";
+ params = {
+ inBackground: true,
+ };
+ }
+ }
+
+ this.popup.handleOneOffSearch(aEvent, aEngine, where, params);
+ ]]></body>
+ </method>
+
+ <!--
+ Increments or decrements the index of the currently selected one-off.
- // 49px is the min-width of each search engine button,
- // adapt this const when changing the css.
- // It's actually 48px + 1px of right border.
- const ENGINE_WIDTH = 49;
- let panel = document.getElementById("PopupSearchAutoComplete");
- // The panel width only spans to the textbox size, but we also want it
- // to include the magnifier icon's width.
- let ltr = getComputedStyle(this).direction == "ltr";
- let magnifierWidth = parseInt(getComputedStyle(panel)[
- ltr ? "marginLeft" : "marginRight"
- ]) * -1;
- let minWidth = parseInt(panel.width) + magnifierWidth;
- if (engines.length) {
- // Ensure the panel is wide enough to fit at least 3 engines.
- minWidth = Math.max(minWidth, ENGINE_WIDTH * 3);
- }
- panel.style.minWidth = minWidth + "px";
+ @param aForward
+ If true, the index is incremented, and if false, the index is
+ decremented.
+ @param aWrapAround
+ This has a couple of effects, depending on whether there is
+ currently a selection.
+ (1) If true and the last one-off is currently selected,
+ incrementing the index will cause the selection to be cleared and
+ this method to return true. Calling advanceSelection again after
+ that (again with aForward=true) will select the first one-off.
+ Likewise if decrementing the index when the first one-off is
+ selected, except in the opposite direction of course.
+ (2) If true and there currently is no selection, decrementing the
+ index will cause the last one-off to become selected and this
+ method to return true. Only the aForward=false case is affected
+ because it is always the case that if aForward=true and there
+ currently is no selection, the first one-off becomes selected and
+ this method returns true.
+ @param aIncludeSettingsButton
+ If true, the search-settings button is included. If false, it's
+ ignored.
+ @return True if the selection can continue to advance after this method
+ returns and false if not.
+ -->
+ <method name="advanceSelection">
+ <parameter name="aForward"/>
+ <parameter name="aWrapAround"/>
+ <parameter name="aIncludeSettingsButton"/>
+ <body><![CDATA[
+ let popup = this.popup;
+ let list = document.getAnonymousElementByAttribute(popup, "anonid",
+ "search-panel-one-offs");
+ let selectedButton = this.selectedButton;
+ let buttons = this.getSelectableButtons(aIncludeSettingsButton);
+
+ if (selectedButton) {
+ // cycle through one-off buttons.
+ let index = buttons.indexOf(selectedButton);
+ if (aForward)
+ ++index;
+ else
+ --index;
+
+ if (index >= 0 && index < buttons.length)
+ this.selectedButton = buttons[index];
+ else
+ this.selectedButton = null;
+
+ if (this.selectedButton || aWrapAround)
+ return true;
+
+ return false;
+ }
+
+ if (!selectedButton) {
+ // If no selection, select the first button or ...
+ if (aForward) {
+ this.selectedButton = buttons[0];
+ return true;
+ }
- if (!engines.length)
- return;
+ if (!aForward && aWrapAround) {
+ // the last button.
+ this.selectedButton = buttons[buttons.length - 1];
+ return true;
+ }
+ }
+
+ return false;
+ ]]></body>
+ </method>
+
+ <!--
+ This handles key presses specific to the one-off buttons like Tab and
+ Alt-Up/Down, and Up/Down keys within the buttons. Since one-off buttons
+ are always used in conjunction with a list of some sort (in this.popup),
+ it also handles Up/Down keys that cross the boundaries between list
+ items and the one-off buttons.
- let panelWidth = parseInt(panel.clientWidth);
- // The + 1 is because the last button doesn't have a right border.
- let enginesPerRow = Math.floor((panelWidth + 1) / ENGINE_WIDTH);
- let buttonWidth = Math.floor(panelWidth / enginesPerRow);
- // There will be an emtpy area of:
- // panelWidth - enginesPerRow * buttonWidth px
- // at the end of each row.
+ @param event
+ The key event.
+ @param numListItems
+ The number of items in the list. The reason that this is a
+ parameter at all is that the list may contain items at the end
+ that should be ignored, depending on the consumer. That's true
+ for the urlbar for example.
+ @param allowEmptySelection
+ Pass true if it's OK that neither the list nor the one-off
+ buttons contains a selection. Pass false if either the list or
+ the one-off buttons (or both) should always contain a selection.
+ @param textboxUserValue
+ When the last list item is selected and the user presses Down,
+ the first one-off becomes selected and the textbox value is
+ restored to the value that the user typed. Pass that value here.
+ However, if you pass true for allowEmptySelection, you don't need
+ to pass anything for this parameter. (Pass undefined or null.)
+ @return True if this method handled the keypress and false if not. If
+ false, then you should let the autocomplete controller handle
+ the keypress. The value of event.defaultPrevented will be the
+ same as this return value.
+ -->
+ <method name="handleKeyPress">
+ <parameter name="event"/>
+ <parameter name="numListItems"/>
+ <parameter name="allowEmptySelection"/>
+ <parameter name="textboxUserValue"/>
+ <body><![CDATA[
+ let stopEvent = false;
- // If the <description> tag with the list of search engines doesn't have
- // a fixed height, the panel will be sized incorrectly, causing the bottom
- // of the suggestion <tree> to be hidden.
- let rowCount = Math.ceil(engines.length / enginesPerRow);
- let height = rowCount * 33; // 32px per row, 1px border.
- list.setAttribute("height", height + "px");
+ // Tab cycles through the one-offs and moves the focus out at the end.
+ if (event.keyCode == KeyEvent.DOM_VK_TAB) {
+ stopEvent = this.advanceSelection(!event.shiftKey, false, true);
+ }
+
+ // Alt + up/down is very similar to (shift +) tab but differs in that
+ // it loops through the list, whereas tab will move the focus out.
+ else if (event.altKey &&
+ (event.keyCode == KeyEvent.DOM_VK_DOWN ||
+ event.keyCode == KeyEvent.DOM_VK_UP)) {
+ stopEvent =
+ this.advanceSelection(event.keyCode == KeyEvent.DOM_VK_DOWN,
+ true, false);
+ }
- // Ensure we can refer to the settings button by ID:
- let settingsEl = document.getAnonymousElementByAttribute(this, "anonid", "search-settings");
- settingsEl.id = this.id + "-anon-search-settings";
+ else if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_UP) {
+ if (numListItems > 0) {
+ if (this.popup.selectedIndex > 0) {
+ // The autocomplete controller should handle this case.
+ } else if (this.popup.selectedIndex == 0) {
+ if (!allowEmptySelection) {
+ // Wrap around the selection to the last one-off.
+ this.selectedButton = null;
+ stopEvent = this.advanceSelection(false, true, true);
+ if (stopEvent) {
+ this.popup.selectedIndex = -1;
+ }
+ }
+ } else if (this.firstButtonSelected(true)) {
+ this.selectedButton = null;
+ } else {
+ stopEvent = this.advanceSelection(false, true, true);
+ }
+ } else {
+ stopEvent = this.advanceSelection(false, true, true);
+ }
+ }
- let dummyItems = enginesPerRow - (engines.length % enginesPerRow || enginesPerRow);
- for (let i = 0; i < engines.length; ++i) {
- let engine = engines[i];
- let button = document.createElementNS(kXULNS, "button");
- button.id = "searchbar-engine-one-off-item-" + engine.name.replace(/ /g, '-');
- let uri = "chrome://browser/skin/search-engine-placeholder.png";
- if (engine.iconURI) {
- uri = engine.iconURI.spec;
+ else if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_DOWN) {
+ if (numListItems > 0) {
+ if (this.popup.selectedIndex >= 0 &&
+ this.popup.selectedIndex < numListItems - 1) {
+ // The autocomplete controller should handle this case.
+ } else if (this.popup.selectedIndex == numListItems - 1) {
+ this.selectedButton = null;
+ stopEvent = this.advanceSelection(true, true, true);
+ if (stopEvent) {
+ stopEvent = !allowEmptySelection;
+ if (this.textbox && typeof(textboxUserValue) == "string") {
+ this.textbox.value = textboxUserValue;
+ }
+ if (!allowEmptySelection) {
+ this.popup.selectedIndex = -1;
+ }
+ }
+ } else if (this.lastButtonSelected(true)) {
+ this.selectedButton = null;
+ stopEvent = allowEmptySelection;
+ } else if (this.selectedButton) {
+ stopEvent = this.advanceSelection(true, true, true);
+ } else {
+ // The autocomplete controller should handle this case.
+ }
+ } else {
+ stopEvent = this.advanceSelection(true, true, true);
+ }
}
- button.setAttribute("image", uri);
- button.setAttribute("class", "searchbar-engine-one-off-item");
- button.setAttribute("tooltiptext", engine.name);
- button.setAttribute("width", buttonWidth);
- button.engine = engine;
+
+ if (stopEvent) {
+ event.preventDefault();
+ event.stopPropagation();
+ return true;
+ }
+ return false;
+ ]]></body>
+ </method>
+
+ <!--
+ Returns true if the first one-off is selected.
+
+ @param aIncludeSettingsButton
+ If true, the search-settings button is included. If false, it's
+ ignored.
+ @return True if the first button is selected.
+ -->
+ <method name="firstButtonSelected">
+ <parameter name="aIncludeSettingsButton"/>
+ <body><![CDATA[
+ if (!this.selectedButton) {
+ return false;
+ }
+ let buttons = this.getSelectableButtons(aIncludeSettingsButton);
+ return buttons[0] == this.selectedButton;
+ ]]></body>
+ </method>
+
+ <!--
+ Returns true if the last one-off is selected.
- if ((i + 1) % enginesPerRow == 0)
- button.classList.add("last-of-row");
+ @param aIncludeSettingsButton
+ If true, the search-settings button is included. If false, it's
+ ignored.
+ @return True if the last button is selected.
+ -->
+ <method name="lastButtonSelected">
+ <parameter name="aIncludeSettingsButton"/>
+ <body><![CDATA[
+ if (!this.selectedButton) {
+ return false;
+ }
+ let buttons = this.getSelectableButtons(aIncludeSettingsButton);
+ return buttons[buttons.length - 1] == this.selectedButton;
+ ]]></body>
+ </method>
- if (i >= engines.length + dummyItems - enginesPerRow)
- button.classList.add("last-row");
+ <!--
+ If the given event is related to the one-offs, this method records
+ one-off telemetry for it. this.telemetryID will be appended to the
+ computed source, so make sure you set that first.
- list.appendChild(button);
- }
+ @param aEvent
+ An event, like a click on a one-off button.
+ @param aOpenUILinkWhere
+ The "where" passed to openUILink.
+ @param aOpenUILinkParams
+ The "params" passed to openUILink.
+ @return True if telemetry was recorded and false if not.
+ -->
+ <method name="recordTelemetry">
+ <parameter name="aEvent"/>
+ <parameter name="aOpenUILinkWhere"/>
+ <parameter name="aOpenUILinkParams"/>
+ <body><![CDATA[
+ if (!aEvent) {
+ return;
+ }
- while (dummyItems) {
- let button = document.createElementNS(kXULNS, "button");
- button.setAttribute("class", "searchbar-engine-one-off-item dummy last-row");
- button.setAttribute("width", buttonWidth);
+ let source = null;
+ let type = "unknown";
+ let engine = null;
+ let target = aEvent.originalTarget;
- if (!--dummyItems)
- button.classList.add("last-of-row");
+ if (aEvent instanceof KeyboardEvent) {
+ type = "key";
+ if (this.selectedButton) {
+ source = "oneoff";
+ engine = this.selectedButton.engine;
+ }
+ } else if (aEvent instanceof MouseEvent) {
+ type = "mouse";
+ if (target.classList.contains("searchbar-engine-one-off-item")) {
+ source = "oneoff";
+ engine = target.engine;
+ }
+ } else if ((aEvent instanceof XULCommandEvent) &&
+ target.getAttribute("anonid") ==
+ "search-one-offs-context-open-in-new-tab") {
+ source = "oneoff-context";
+ engine = this._contextEngine;
+ }
- list.appendChild(button);
- }
- ]]></handler>
+ if (!source) {
+ return false;
+ }
+
+ if (this.telemetryID) {
+ source += "-" + this.telemetryID;
+ }
+
+ let tabBackground = aOpenUILinkWhere == "tab" &&
+ aOpenUILinkParams &&
+ aOpenUILinkParams.inBackground;
+ let where = tabBackground ? "tab-background" : aOpenUILinkWhere;
+ BrowserSearch.recordOneoffSearchInTelemetry(engine, source, type,
+ where);
+ return true;
+ ]]></body>
+ </method>
+
+ </implementation>
+
+ <handlers>
<handler event="mousedown"><![CDATA[
// Required to receive click events from the buttons on Linux.
event.preventDefault();
]]></handler>
<handler event="mouseover"><![CDATA[
let target = event.originalTarget;
@@ -1373,40 +1886,38 @@
// Ignore mouse events when the context menu is open.
if (this._ignoreMouseEvents)
return;
if ((target.classList.contains("searchbar-engine-one-off-item") &&
!target.classList.contains("dummy")) ||
target.classList.contains("addengine-item") ||
target.classList.contains("search-setting-button")) {
- let textbox = document.getElementById("searchbar").textbox;
- textbox._changeVisuallySelectedButton(target);
+ this._changeVisuallySelectedButton(target);
}
]]></handler>
<handler event="mouseout"><![CDATA[
let target = event.originalTarget;
if (target.localName != "button") {
return;
}
// Don't deselect the current button if the context menu is open.
if (this._ignoreMouseEvents)
return;
- let textbox = document.getElementById("searchbar").textbox;
// Unfortunately this will fire before mouseover hits another item.
// If this button is selected, we replace that selection only if
// we're not moving to a different one-off item:
if (target.getAttribute("selected") == "true" &&
(!event.relatedTarget ||
!event.relatedTarget.classList.contains("searchbar-engine-one-off-item") ||
event.relatedTarget.classList.contains("dummy"))) {
- textbox._changeVisuallySelectedButton(textbox.selectedButton);
+ this._changeVisuallySelectedButton(this.selectedButton);
}
]]></handler>
<handler event="click"><![CDATA[
if (event.button == 2)
return; // ignore right clicks.
let button = event.originalTarget;
@@ -1416,30 +1927,38 @@
return;
// For some reason, if the context menu had been opened prior to the
// click, the suggestions popup won't be closed after loading the search
// in the current tab - so we hide it manually. Some focusing magic
// that happens when a search is loaded ensures that the popup is opened
// again if it needs to be, so we don't need to worry about which cases
// require manual hiding.
- this.hidePopup();
+ this.popup.hidePopup();
- let searchbar = document.getElementById("searchbar");
- searchbar.handleSearchCommand(event, engine);
+ // Make sure the clicked button is selected. In practice this should
+ // always be the case since you have to mouse over the button to click
+ // it, and the mouseover handler selects the button. So mostly this is
+ // for tests.
+ this.selectedButton = button;
+
+ this.handleSearchCommand(event, engine);
]]></handler>
<handler event="command"><![CDATA[
let target = event.originalTarget;
if (target.classList.contains("addengine-item")) {
- // On success, hide and reshow the panel to show the new engine.
+ // On success, hide the panel and tell event listeners to reshow it to
+ // show the new engine.
let installCallback = {
- onSuccess: function(engine) {
- event.target.hidePopup();
- BrowserSearch.searchBar.openSuggestionsPanel();
+ onSuccess: engine => {
+ this.popup.hidePopup();
+ let event = document.createEvent("Events");
+ event.initEvent("OneOffsEngineInstalled", true, false);
+ this.dispatchEvent(event);
},
onError: function(errorCode) {
if (errorCode != Ci.nsISearchInstallCallback.ERROR_DUPLICATE_ENGINE) {
// Download error is shown by the search service
return;
}
const kSearchBundleURI = "chrome://global/locale/search/search.properties";
let searchBundle = Services.strings.createBundle(kSearchBundleURI);
@@ -1456,18 +1975,17 @@
}
}
Services.search.addEngine(target.getAttribute("uri"), null,
target.getAttribute("image"), false,
installCallback);
}
let anonid = target.getAttribute("anonid");
if (anonid == "search-one-offs-context-open-in-new-tab") {
- let searchbar = document.getElementById("searchbar");
- searchbar.handleSearchCommand(event, this._contextEngine, true);
+ this.handleSearchCommand(event, this._contextEngine, true);
}
if (anonid == "search-one-offs-context-set-default") {
let currentEngine = Services.search.currentEngine;
// Make the target button of the context menu reflect the current
// search engine first. Doing this as opposed to rebuilding all the
// one-off buttons avoids flicker.
let button = document.getElementById("searchbar-engine-one-off-item-" +
@@ -1479,26 +1997,26 @@
button.setAttribute("image", uri);
button.setAttribute("tooltiptext", currentEngine.name);
button.engine = currentEngine;
Services.search.currentEngine = this._contextEngine;
}
]]></handler>
- <handler event="popuphiding"><![CDATA[
- this._isHiding = true;
- setTimeout(() => {
- this._isHiding = false;
- }, 0);
+ <handler event="contextmenu"><![CDATA[
+ let target = event.originalTarget;
+ // Prevent the context menu from appearing except on the one off buttons.
+ if (!target.classList.contains("searchbar-engine-one-off-item") ||
+ target.classList.contains("dummy")) {
+ event.preventDefault();
+ return;
+ }
+ document.getAnonymousElementByAttribute(this, "anonid", "search-one-offs-context-set-default")
+ .setAttribute("disabled", target.engine == Services.search.currentEngine);
+
+ this._contextEngine = target.engine;
]]></handler>
</handlers>
- </binding>
- <!-- Used for additional open search providers in the search panel. -->
- <binding id="addengine-icon" extends="xul:box">
- <content>
- <xul:image class="addengine-icon" xbl:inherits="src"/>
- <xul:image class="addengine-badge"/>
- </content>
</binding>
</bindings>
--- a/browser/components/search/content/searchbarBindings.css
+++ b/browser/components/search/content/searchbarBindings.css
@@ -2,8 +2,12 @@
* 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/. */
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
.searchbar-textbox {
-moz-binding: url("chrome://browser/content/search/search.xml#searchbar-textbox");
}
+
+.search-one-offs {
+ -moz-binding: url("chrome://browser/content/search/search.xml#search-one-offs");
+}
--- a/browser/components/search/test/browser_hiddenOneOffs_diacritics.js
+++ b/browser/components/search/test/browser_hiddenOneOffs_diacritics.js
@@ -12,19 +12,23 @@ const searchIcon = document.getAnonymous
const diacritic_engine = "Foo \u2661";
var Preferences =
Cu.import("resource://gre/modules/Preferences.jsm", {}).Preferences;
// Get an array of the one-off buttons.
function getOneOffs() {
let oneOffs = [];
- let oneOff =
+ let oneOffsContainer =
document.getAnonymousElementByAttribute(searchPopup, "anonid",
+ "search-one-off-buttons");
+ let oneOff =
+ document.getAnonymousElementByAttribute(oneOffsContainer, "anonid",
"search-panel-one-offs");
+
for (oneOff = oneOff.firstChild; oneOff; oneOff = oneOff.nextSibling) {
if (oneOff.classList.contains("dummy"))
break;
oneOffs.push(oneOff);
}
return oneOffs;
}
--- a/browser/components/search/test/browser_oneOffHeader.js
+++ b/browser/components/search/test/browser_oneOffHeader.js
@@ -5,21 +5,25 @@
const isMac = ("nsILocalFileMac" in Ci);
const searchbar = document.getElementById("searchbar");
const textbox = searchbar._textbox;
const searchPopup = document.getElementById("PopupSearchAutoComplete");
const searchIcon = document.getAnonymousElementByAttribute(searchbar, "anonid",
"searchbar-search-button");
+
+const oneOffsContainer =
+ document.getAnonymousElementByAttribute(searchPopup, "anonid",
+ "search-one-off-buttons");
const searchSettings =
- document.getAnonymousElementByAttribute(searchPopup, "anonid",
+ document.getAnonymousElementByAttribute(oneOffsContainer, "anonid",
"search-settings");
var header =
- document.getAnonymousElementByAttribute(searchPopup, "anonid",
+ document.getAnonymousElementByAttribute(oneOffsContainer, "anonid",
"search-panel-one-offs-header");
function getHeaderText() {
let headerChild = header.selectedPanel;
while (headerChild.hasChildNodes()) {
headerChild = headerChild.firstChild;
}
let headerStrings = [];
for (let label = headerChild; label; label = label.nextSibling) {
@@ -27,17 +31,17 @@ function getHeaderText() {
}
return headerStrings.join("");
}
// Get an array of the one-off buttons.
function getOneOffs() {
let oneOffs = [];
let oneOff =
- document.getAnonymousElementByAttribute(searchPopup, "anonid",
+ document.getAnonymousElementByAttribute(oneOffsContainer, "anonid",
"search-panel-one-offs");
for (oneOff = oneOff.firstChild; oneOff; oneOff = oneOff.nextSibling) {
if (oneOff.classList.contains("dummy"))
break;
oneOffs.push(oneOff);
}
return oneOffs;
--- a/browser/components/search/test/browser_searchbar_keyboard_navigation.js
+++ b/browser/components/search/test/browser_searchbar_keyboard_navigation.js
@@ -1,36 +1,39 @@
// Tests that keyboard navigation in the search panel works as designed.
const searchbar = document.getElementById("searchbar");
const textbox = searchbar._textbox;
const searchPopup = document.getElementById("PopupSearchAutoComplete");
+const oneOffsContainer =
+ document.getAnonymousElementByAttribute(searchPopup, "anonid",
+ "search-one-off-buttons");
const kValues = ["foo1", "foo2", "foo3"];
const kUserValue = "foo";
// Get an array of the one-off buttons.
function getOneOffs() {
let oneOffs = [];
- let oneOff = document.getAnonymousElementByAttribute(searchPopup, "anonid",
+ let oneOff = document.getAnonymousElementByAttribute(oneOffsContainer, "anonid",
"search-panel-one-offs");
for (oneOff = oneOff.firstChild; oneOff; oneOff = oneOff.nextSibling) {
if (oneOff.classList.contains("dummy"))
break;
oneOffs.push(oneOff);
}
return oneOffs;
}
function getOpenSearchItems() {
let os = [];
let addEngineList =
- document.getAnonymousElementByAttribute(searchPopup, "anonid",
+ document.getAnonymousElementByAttribute(oneOffsContainer, "anonid",
"add-engines");
for (let item = addEngineList.firstChild; item; item = item.nextSibling)
os.push(item);
return os;
}
add_task(function* init() {
--- a/browser/components/search/test/browser_searchbar_smallpanel_keyboard_navigation.js
+++ b/browser/components/search/test/browser_searchbar_smallpanel_keyboard_navigation.js
@@ -1,37 +1,40 @@
// Tests that keyboard navigation in the search panel works as designed.
const searchbar = document.getElementById("searchbar");
const textbox = searchbar._textbox;
const searchPopup = document.getElementById("PopupSearchAutoComplete");
+const oneOffsContainer =
+ document.getAnonymousElementByAttribute(searchPopup, "anonid",
+ "search-one-off-buttons");
const searchIcon = document.getAnonymousElementByAttribute(searchbar, "anonid",
"searchbar-search-button");
const kValues = ["foo1", "foo2", "foo3"];
// Get an array of the one-off buttons.
function getOneOffs() {
let oneOffs = [];
- let oneOff = document.getAnonymousElementByAttribute(searchPopup, "anonid",
+ let oneOff = document.getAnonymousElementByAttribute(oneOffsContainer, "anonid",
"search-panel-one-offs");
for (oneOff = oneOff.firstChild; oneOff; oneOff = oneOff.nextSibling) {
if (oneOff.classList.contains("dummy"))
break;
oneOffs.push(oneOff);
}
return oneOffs;
}
function getOpenSearchItems() {
let os = [];
let addEngineList =
- document.getAnonymousElementByAttribute(searchPopup, "anonid",
+ document.getAnonymousElementByAttribute(oneOffsContainer, "anonid",
"add-engines");
for (let item = addEngineList.firstChild; item; item = item.nextSibling)
os.push(item);
return os;
}
add_task(function* init() {
--- a/toolkit/components/autocomplete/nsAutoCompleteController.cpp
+++ b/toolkit/components/autocomplete/nsAutoCompleteController.cpp
@@ -284,40 +284,42 @@ nsAutoCompleteController::HandleText()
}
StartSearches();
return NS_OK;
}
NS_IMETHODIMP
-nsAutoCompleteController::HandleEnter(bool aIsPopupSelection, bool *_retval)
+nsAutoCompleteController::HandleEnter(bool aIsPopupSelection,
+ nsIDOMEvent *aEvent,
+ bool *_retval)
{
*_retval = false;
if (!mInput)
return NS_OK;
nsCOMPtr<nsIAutoCompleteInput> input(mInput);
+ int32_t selectedIndex = -1;
// allow the event through unless there is something selected in the popup
input->GetPopupOpen(_retval);
if (*_retval) {
nsCOMPtr<nsIAutoCompletePopup> popup;
input->GetPopup(getter_AddRefs(popup));
if (popup) {
- int32_t selectedIndex;
popup->GetSelectedIndex(&selectedIndex);
*_retval = selectedIndex >= 0;
}
}
// Stop the search, and handle the enter.
StopSearch();
- EnterMatch(aIsPopupSelection);
+ EnterMatch(aIsPopupSelection, aEvent, selectedIndex);
return NS_OK;
}
NS_IMETHODIMP
nsAutoCompleteController::HandleEscape(bool *_retval)
{
*_retval = false;
@@ -383,17 +385,17 @@ nsAutoCompleteController::HandleEndCompo
mCompositionState = eCompositionState_Committing;
return NS_OK;
}
NS_IMETHODIMP
nsAutoCompleteController::HandleTab()
{
bool cancel;
- return HandleEnter(false, &cancel);
+ return HandleEnter(false, nullptr, &cancel);
}
NS_IMETHODIMP
nsAutoCompleteController::HandleKeyNavigation(uint32_t aKey, bool *_retval)
{
// By default, don't cancel the event
*_retval = false;
@@ -1351,17 +1353,19 @@ nsAutoCompleteController::ClearSearchTim
if (mTimer) {
mTimer->Cancel();
mTimer = nullptr;
}
return NS_OK;
}
nsresult
-nsAutoCompleteController::EnterMatch(bool aIsPopupSelection)
+nsAutoCompleteController::EnterMatch(bool aIsPopupSelection,
+ nsIDOMEvent *aEvent,
+ int32_t aSelectedPopupIndex)
{
nsCOMPtr<nsIAutoCompleteInput> input(mInput);
nsCOMPtr<nsIAutoCompletePopup> popup;
input->GetPopup(getter_AddRefs(popup));
NS_ENSURE_TRUE(popup != nullptr, NS_ERROR_FAILURE);
bool forceComplete;
input->GetForceComplete(&forceComplete);
@@ -1487,17 +1491,17 @@ nsAutoCompleteController::EnterMatch(boo
input->SelectTextRange(value.Length(), value.Length());
mSearchString = value;
}
obsSvc->NotifyObservers(input, "autocomplete-did-enter-text", nullptr);
ClosePopup();
bool cancel;
- input->OnTextEntered(&cancel);
+ input->OnTextEntered(aEvent, aSelectedPopupIndex, &cancel);
return NS_OK;
}
nsresult
nsAutoCompleteController::RevertTextValue()
{
// StopSearch() can call PostSearchCleanup() which might result
--- a/toolkit/components/autocomplete/nsAutoCompleteController.h
+++ b/toolkit/components/autocomplete/nsAutoCompleteController.h
@@ -50,17 +50,19 @@ protected:
nsresult ClearSearchTimer();
void MaybeCompletePlaceholder();
void HandleSearchResult(nsIAutoCompleteSearch *aSearch,
nsIAutoCompleteResult *aResult);
nsresult ProcessResult(int32_t aSearchIndex, nsIAutoCompleteResult *aResult);
nsresult PostSearchCleanup();
- nsresult EnterMatch(bool aIsPopupSelection);
+ nsresult EnterMatch(bool aIsPopupSelection,
+ nsIDOMEvent *aEvent,
+ int32_t aSelectedPopupIndex);
nsresult RevertTextValue();
nsresult CompleteDefaultIndex(int32_t aResultIndex);
nsresult CompleteValue(nsString &aValue);
nsresult GetResultAt(int32_t aIndex, nsIAutoCompleteResult** aResult,
int32_t* aRowIndex);
nsresult GetResultValueAt(int32_t aIndex, bool aGetFinalValue,
--- a/toolkit/components/autocomplete/nsIAutoCompleteController.idl
+++ b/toolkit/components/autocomplete/nsIAutoCompleteController.idl
@@ -1,17 +1,18 @@
/* 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/. */
#include "nsISupports.idl"
interface nsIAutoCompleteInput;
+interface nsIDOMEvent;
-[scriptable, uuid(ff9f8465-204a-47a6-b3c9-0628b3856684)]
+[scriptable, uuid(c6dcd364-99a8-48a9-9611-77e6a18ac9f3)]
interface nsIAutoCompleteController : nsISupports
{
/*
* Possible values for the searchStatus attribute
*/
const unsigned short STATUS_NONE = 1;
const unsigned short STATUS_SEARCHING = 2;
const unsigned short STATUS_COMPLETE_NO_MATCH = 3;
@@ -58,19 +59,26 @@ interface nsIAutoCompleteController : ns
void handleText();
/*
* Notify the controller that the user wishes to enter the current text. If
* aIsPopupSelection is true, then a selection was made from the popup, so
* fill this value into the input field before continuing. If false, just
* use the current value of the input field.
*
+ * @param aIsPopupSelection
+ * Pass true if the selection was made from the popup.
+ * @param aEvent
+ * Optional. The event that triggered the enter, like a key event if
+ * the user pressed the Return key or a click event if the user clicked
+ * a popup item.
* @return True if the controller wishes to prevent event propagation and default event
*/
- boolean handleEnter(in boolean aIsPopupSelection);
+ boolean handleEnter(in boolean aIsPopupSelection,
+ in nsIDOMEvent aEvent);
/*
* Notify the controller that the user wishes to revert autocomplete
*
* @return True if the controller wishes to prevent event propagation and default event
*/
boolean handleEscape();
--- a/toolkit/components/autocomplete/nsIAutoCompleteInput.idl
+++ b/toolkit/components/autocomplete/nsIAutoCompleteInput.idl
@@ -2,17 +2,17 @@
* 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/. */
#include "nsISupports.idl"
#include "nsIAutoCompleteController.idl"
interface nsIAutoCompletePopup;
-[scriptable, uuid(B068E70F-F82C-4C12-AD87-82E271C5C180)]
+[scriptable, uuid(d2f4b70b-e679-4295-9f91-a1ca57da7fca)]
interface nsIAutoCompleteInput : nsISupports
{
/*
* The result view that will be used to display results
*/
readonly attribute nsIAutoCompletePopup popup;
/*
@@ -119,19 +119,25 @@ interface nsIAutoCompleteInput : nsISupp
/*
* Notification that the search concluded successfully
*/
void onSearchComplete();
/*
* Notification that the user selected and entered a result item
*
+ * @param aEvent
+ * The event that triggered the enter. Will be null if it's unknown.
+ * @param aSelectedPopupIndex
+ * The index in the popup that was selected when the enter was made.
+ * -1 if unknown.
* @return True if the user wishes to prevent the enter
*/
- boolean onTextEntered();
+ boolean onTextEntered(in nsIDOMEvent aEvent,
+ in long aSelectedPopupIndex);
/*
* Notification that the user cancelled the autocomplete session
*
* @return True if the user wishes to prevent the revert
*/
boolean onTextReverted();
--- a/toolkit/components/autocomplete/tests/unit/test_completeDefaultIndex_casing.js
+++ b/toolkit/components/autocomplete/tests/unit/test_completeDefaultIndex_casing.js
@@ -25,17 +25,17 @@ add_test(function test_keyNavigation() {
aController.handleKeyNavigation(Ci.nsIDOMKeyEvent.DOM_VK_RIGHT);
do_check_eq(aController.input.textValue, "mozilla");
});
});
add_test(function test_handleEnter() {
doSearch("MOZ", "mozilla", function(aController) {
do_check_eq(aController.input.textValue, "MOZilla");
- aController.handleEnter(false);
+ aController.handleEnter(false, null);
do_check_eq(aController.input.textValue, "mozilla");
});
});
function doSearch(aSearchString, aResultValue, aOnCompleteCallback) {
let search = new AutoCompleteSearchBase("search",
new AutoCompleteResult([ "mozilla", "toolkit" ], 0));
registerAutoCompleteSearch(search);
--- a/toolkit/components/autocomplete/tests/unit/test_finalCompleteValue.js
+++ b/toolkit/components/autocomplete/tests/unit/test_finalCompleteValue.js
@@ -11,17 +11,17 @@ function AutoCompleteInput(aSearches) {
AutoCompleteInput.prototype = Object.create(AutoCompleteInputBase.prototype);
add_test(function test_handleEnter_mouse() {
doSearch("moz", "mozilla.com", "http://www.mozilla.com", function(aController) {
do_check_eq(aController.input.textValue, "moz");
do_check_eq(aController.getFinalCompleteValueAt(0), "http://www.mozilla.com");
// Keyboard interaction is tested by test_finalCompleteValueSelectedIndex.js
// so here just test popup selection.
- aController.handleEnter(true);
+ aController.handleEnter(true, null);
do_check_eq(aController.input.textValue, "http://www.mozilla.com");
});
});
function doSearch(aSearchString, aResultValue, aFinalCompleteValue, aOnCompleteCallback) {
let search = new AutoCompleteSearchBase(
"search",
new AutoCompleteResult([ aResultValue ], [ aFinalCompleteValue ])
--- a/toolkit/components/autocomplete/tests/unit/test_finalCompleteValueSelectedIndex.js
+++ b/toolkit/components/autocomplete/tests/unit/test_finalCompleteValueSelectedIndex.js
@@ -36,17 +36,17 @@ add_test(function test_handleEnter() {
Assert.equal(aController.input.popup.selectedIndex, 0);
aController.handleKeyNavigation(Ci.nsIDOMKeyEvent.DOM_VK_DOWN);
Assert.equal(aController.input.popup.selectedIndex, 1);
// Simulate mouse interaction changing selectedIndex
// ie NOT keyboard interaction:
aController.input.popup.selectedIndex = 0;
- aController.handleEnter(false);
+ aController.handleEnter(false, null);
// Verify that the keyboard-selected thing got inserted,
// and not the mouse selection:
Assert.equal(aController.input.textValue, "http://www.mozilla.org");
});
// Then the case where we do not:
doSearch("moz", results, function(aController) {
Assert.equal(aController.input.textValue, "moz");
@@ -56,17 +56,17 @@ add_test(function test_handleEnter() {
Assert.equal(aController.input.popup.selectedIndex, 0);
aController.input.popupOpen = true;
// Simulate mouse interaction changing selectedIndex
// ie NOT keyboard interaction:
aController.input.popup.selectedIndex = 1;
Assert.equal(selectByWasCalled, false);
Assert.equal(aController.input.popup.selectedIndex, 1);
- aController.handleEnter(false);
+ aController.handleEnter(false, null);
// Verify that the input stayed the same, because no selection was made
// with the keyboard:
Assert.equal(aController.input.textValue, "moz");
});
});
function doSearch(aSearchString, aResults, aOnCompleteCallback) {
selectByWasCalled = false;
--- a/toolkit/components/autocomplete/tests/unit/test_finalCompleteValue_defaultIndex.js
+++ b/toolkit/components/autocomplete/tests/unit/test_finalCompleteValue_defaultIndex.js
@@ -20,17 +20,17 @@ add_test(function test_handleEnter() {
];
doSearch("moz", results, controller => {
let input = controller.input;
Assert.equal(input.textValue, "mozilla.com");
Assert.equal(controller.getFinalCompleteValueAt(0), results[0][1]);
Assert.equal(controller.getFinalCompleteValueAt(1), results[1][1]);
Assert.equal(input.popup.selectedIndex, 0);
- controller.handleEnter(false);
+ controller.handleEnter(false, null);
// Verify that the keyboard-selected thing got inserted,
// and not the mouse selection:
Assert.equal(controller.input.textValue, "https://www.mozilla.com");
});
});
function doSearch(aSearchString, aResults, aOnCompleteCallback) {
let search = new AutoCompleteSearchBase(
--- a/toolkit/components/autocomplete/tests/unit/test_finalCompleteValue_forceComplete.js
+++ b/toolkit/components/autocomplete/tests/unit/test_finalCompleteValue_forceComplete.js
@@ -19,56 +19,56 @@ function run_test() {
run_next_test();
}
add_test(function test_handleEnterWithDirectMatchCompleteSelectedIndex() {
doSearch("moz", "mozilla.com", "http://www.mozilla.com",
{ forceComplete: true, completeSelectedIndex: true }, function(aController) {
do_check_eq(aController.input.textValue, "moz");
do_check_eq(aController.getFinalCompleteValueAt(0), "http://www.mozilla.com");
- aController.handleEnter(false);
+ aController.handleEnter(false, null);
// After enter the final complete value should be shown in the input.
do_check_eq(aController.input.textValue, "http://www.mozilla.com");
});
});
add_test(function test_handleEnterWithDirectMatch() {
doSearch("mozilla", "mozilla.com", "http://www.mozilla.com",
{ forceComplete: true, completeDefaultIndex: true }, function(aController) {
// Should autocomplete the search string to a suggestion.
do_check_eq(aController.input.textValue, "mozilla.com");
do_check_eq(aController.getFinalCompleteValueAt(0), "http://www.mozilla.com");
- aController.handleEnter(false);
+ aController.handleEnter(false, null);
// After enter the final complete value should be shown in the input.
do_check_eq(aController.input.textValue, "http://www.mozilla.com");
});
});
add_test(function test_handleEnterWithNoMatch() {
doSearch("mozilla", "mozilla.com", "http://www.mozilla.com",
{ forceComplete: true, completeDefaultIndex: true }, function(aController) {
// Should autocomplete the search string to a suggestion.
do_check_eq(aController.input.textValue, "mozilla.com");
do_check_eq(aController.getFinalCompleteValueAt(0), "http://www.mozilla.com");
// Now input something that does not match...
aController.input.textValue = "mozillax";
// ... and confirm. We don't want one of the values from the previous
// results to be taken, since what's now in the input field doesn't match.
- aController.handleEnter(false);
+ aController.handleEnter(false, null);
do_check_eq(aController.input.textValue, "mozillax");
});
});
add_test(function test_handleEnterWithIndirectMatch() {
doSearch("com", "mozilla.com", "http://www.mozilla.com",
{ forceComplete: true, completeDefaultIndex: true }, function(aController) {
// Should autocomplete the search string to a suggestion.
do_check_eq(aController.input.textValue, "com >> mozilla.com");
do_check_eq(aController.getFinalCompleteValueAt(0), "http://www.mozilla.com");
- aController.handleEnter(false);
+ aController.handleEnter(false, null);
// After enter the final complete value from the suggestion should be shown
// in the input.
do_check_eq(aController.input.textValue, "http://www.mozilla.com");
});
});
function doSearch(aSearchString, aResultValue, aFinalCompleteValue,
aInputProps, aOnCompleteCallback) {
--- a/toolkit/components/autocomplete/tests/unit/test_finalDefaultCompleteValue.js
+++ b/toolkit/components/autocomplete/tests/unit/test_finalDefaultCompleteValue.js
@@ -26,17 +26,17 @@ add_test(function test_keyNavigation() {
aController.handleKeyNavigation(Ci.nsIDOMKeyEvent.DOM_VK_RIGHT);
do_check_eq(aController.input.textValue, "mozilla.com");
});
});
add_test(function test_handleEnter() {
doSearch("moz", "mozilla.com", "http://www.mozilla.com", function(aController) {
do_check_eq(aController.input.textValue, "mozilla.com");
- aController.handleEnter(false);
+ aController.handleEnter(false, null);
do_check_eq(aController.input.textValue, "http://www.mozilla.com");
});
});
function doSearch(aSearchString, aResultValue, aFinalCompleteValue, aOnCompleteCallback) {
let search = new AutoCompleteSearchBase(
"search",
new AutoCompleteResult([ aResultValue ], [ aFinalCompleteValue ])
--- a/toolkit/components/autocomplete/tests/unit/test_hiddenResult.js
+++ b/toolkit/components/autocomplete/tests/unit/test_hiddenResult.js
@@ -57,17 +57,17 @@ function run_test() {
input.onSearchComplete = function() {
// Hidden results should still be able to do inline autocomplete
do_check_eq(input.textValue, "mozillaTest1");
// Now, let's fill the textbox with the first result of the popup.
// The first search is marked as hidden, so we must always get the
// second search.
- controller.handleEnter(true);
+ controller.handleEnter(true, null);
do_check_eq(input.textValue, "mozillaTest2");
// Only one item in the popup.
do_check_eq(controller.matchCount, 1);
// Unregister searches
unregisterAutoCompleteSearch(searchNormal);
unregisterAutoCompleteSearch(searchTypeAhead);
--- a/toolkit/components/autocomplete/tests/unit/test_popupSelectionVsDefaultCompleteValue.js
+++ b/toolkit/components/autocomplete/tests/unit/test_popupSelectionVsDefaultCompleteValue.js
@@ -25,17 +25,17 @@ AutoCompleteInput.prototype = Object.cre
function run_test() {
run_next_test();
}
add_test(function test_handleEnter() {
doSearch("moz", function(aController) {
do_check_eq(aController.input.textValue, "mozilla.com");
- aController.handleEnter(true);
+ aController.handleEnter(true, null);
do_check_eq(aController.input.textValue, "mozilla.org");
});
});
function doSearch(aSearchString, aOnCompleteCallback) {
let typeAheadSearch = new AutoCompleteSearchBase(
"typeAheadSearch",
new AutoCompleteTypeAheadResult([ "mozilla.com" ], [ "http://www.mozilla.com" ])
--- a/toolkit/components/autocomplete/tests/unit/test_stopSearch.js
+++ b/toolkit/components/autocomplete/tests/unit/test_stopSearch.js
@@ -125,17 +125,17 @@ var gTests = [
controller.handleText();
},
function(controller) {
print("handleEscape");
controller.handleEscape();
},
function(controller) {
print("handleEnter");
- controller.handleEnter(false);
+ controller.handleEnter(false, null);
},
function(controller) {
print("handleTab");
controller.handleTab();
},
function(controller) {
print("handleKeyNavigation");
--- a/toolkit/components/places/PlacesUtils.jsm
+++ b/toolkit/components/places/PlacesUtils.jsm
@@ -322,16 +322,33 @@ this.PlacesUtils = {
return bundle.formatStringFromName(key, params, params.length);
},
getString: function PU_getString(key) {
return bundle.GetStringFromName(key);
},
/**
+ * Makes a moz-action URI for the given action and set of parameters.
+ *
+ * @param type
+ * The action type.
+ * @param params
+ * A JS object of action params.
+ * @returns A moz-action URI as a string.
+ */
+ mozActionURI(type, params) {
+ let encodedParams = {};
+ for (let key in params) {
+ encodedParams[key] = encodeURIComponent(params[key]);
+ }
+ return "moz-action:" + type + "," + JSON.stringify(encodedParams);
+ },
+
+ /**
* Determines whether or not a ResultNode is a Bookmark folder.
* @param aNode
* A result node
* @returns true if the node is a Bookmark folder, false otherwise
*/
nodeIsFolder: function PU_nodeIsFolder(aNode) {
return (aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER ||
aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT);
--- a/toolkit/components/places/UnifiedComplete.js
+++ b/toolkit/components/places/UnifiedComplete.js
@@ -564,34 +564,16 @@ function stripHttpAndTrim(spec) {
}
if (spec.endsWith("/")) {
spec = spec.slice(0, -1);
}
return spec;
}
/**
- * Make a moz-action: URL for a given action and set of parameters.
- *
- * @param action
- * Name of the action
- * @param params
- * Object, whose keys are parameter names and values are the
- * corresponding parameter values.
- * @return String representation of the built moz-action: URL
- */
-function makeActionURL(action, params) {
- let encodedParams = {};
- for (let key in params) {
- encodedParams[key] = encodeURIComponent(params[key]);
- }
- return "moz-action:" + action + "," + JSON.stringify(encodedParams);
-}
-
-/**
* Returns the key to be used for a URL in a map for the purposes of removing
* duplicate entries - any 2 URLs that should be considered the same should
* return the same key. For some moz-action URLs this will unwrap the params
* and return a key based on the wrapped URL.
*/
function makeKeyForURL(actionUrl) {
// At this stage we only consider moz-action URLs.
if (!actionUrl.startsWith("moz-action:")) {
@@ -1120,18 +1102,20 @@ Search.prototype = {
if (queryIndex != -1) {
queryString = searchString.substring(queryIndex + 1);
}
// We need to escape the parameters as if they were the query in a URL
queryString = encodeURIComponent(queryString).replace(/%20/g, "+");
let escapedURL = entry.url.href.replace("%s", queryString);
let style = (this._enableActions ? "action " : "") + "keyword";
- let actionURL = makeActionURL("keyword", { url: escapedURL,
- input: this._originalSearchString });
+ let actionURL = PlacesUtils.mozActionURI("keyword", {
+ url: escapedURL,
+ input: this._originalSearchString,
+ });
let value = this._enableActions ? actionURL : escapedURL;
// The title will end up being "host: queryString"
let comment = entry.url.host;
this._addMatch({ value, comment, style, frecency: FRECENCY_DEFAULT });
return true;
},
@@ -1219,17 +1203,17 @@ Search.prototype = {
input: suggestion || this._originalSearchString,
searchQuery: query,
};
if (suggestion)
actionURLParams.searchSuggestion = suggestion;
if (match.engineAlias) {
actionURLParams.alias = match.engineAlias;
}
- let value = makeActionURL("searchengine", actionURLParams);
+ let value = PlacesUtils.mozActionURI("searchengine", actionURLParams);
this._addMatch({
value: value,
comment: match.engineName,
icon: match.iconUrl,
style: "action searchengine",
frecency: FRECENCY_DEFAULT,
remote: !!suggestion
@@ -1251,17 +1235,17 @@ Search.prototype = {
} else {
icon = PlacesUtils.favicons
.getFaviconLinkForIcon(NetUtil.newURI(icon)).spec;
}
let match = {
// We include the deviceName in the action URL so we can render it in
// the URLBar.
- value: makeActionURL("remotetab", { url, deviceName }),
+ value: PlacesUtils.mozActionURI("remotetab", { url, deviceName }),
comment: title || url,
style: "action remotetab",
// we want frecency > FRECENCY_DEFAULT so it doesn't get pushed out
// by "remote" matches.
frecency: FRECENCY_DEFAULT + 1,
icon,
}
this._addMatch(match);
@@ -1311,17 +1295,17 @@ Search.prototype = {
// getFixupURIInfo() escaped the URI, so it may not be pretty. Embed the
// escaped URL in the action URI since that URL should be "canonical". But
// pass the pretty, unescaped URL as the match comment, since it's likely
// to be displayed to the user, and in any case the front-end should not
// rely on it being canonical.
let escapedURL = uri.spec;
let displayURL = textURIService.unEscapeURIForUI("UTF-8", uri.spec);
- let value = makeActionURL("visiturl", {
+ let value = PlacesUtils.mozActionURI("visiturl", {
url: escapedURL,
input: this._originalSearchString,
});
let match = {
value: value,
comment: displayURL,
style: "action visiturl",
@@ -1380,17 +1364,17 @@ Search.prototype = {
// when searching for "Firefox".
let terms = parseResult.terms.toLowerCase();
if (this._searchTokens.length > 0 &&
this._searchTokens.every(token => !terms.includes(token))) {
return;
}
// Turn the match into a searchengine action with a favicon.
- match.value = makeActionURL("searchengine", {
+ match.value = PlacesUtils.mozActionURI("searchengine", {
engineName: parseResult.engineName,
input: parseResult.terms,
searchQuery: parseResult.terms,
});
match.comment = parseResult.engineName;
match.icon = match.icon || match.iconUrl;
match.style = "action searchengine favicon";
},
@@ -1552,17 +1536,17 @@ Search.prototype = {
let tags = row.getResultByIndex(QUERYINDEX_TAGS) || "";
let frecency = row.getResultByIndex(QUERYINDEX_FRECENCY);
// If actions are enabled and the page is open, add only the switch-to-tab
// result. Otherwise, add the normal result.
let url = escapedURL;
let action = null;
if (this._enableActions && openPageCount > 0 && this.hasBehavior("openpage")) {
- url = makeActionURL("switchtab", {url: escapedURL});
+ url = PlacesUtils.mozActionURI("switchtab", {url: escapedURL});
action = "switchtab";
}
// Always prefer the bookmark title unless it is empty
let title = bookmarkTitle || historyTitle;
// We will always prefer to show tags if we have them.
let showTags = !!tags;
--- a/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
+++ b/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
@@ -250,17 +250,17 @@ function* check_autocomplete(test) {
if (test.autofilled) {
// Check the autoFilled result.
Assert.equal(input.textValue, test.autofilled,
"Autofilled value is correct");
// Now force completion and check correct casing of the result.
// This ensures the controller is able to do its magic case-preserving
// stuff and correct replacement of the user's casing with result's one.
- controller.handleEnter(false);
+ controller.handleEnter(false, null);
Assert.equal(input.textValue, test.completed,
"Completed value is correct");
}
}
var addBookmark = Task.async(function* (aBookmarkObj) {
Assert.ok(!!aBookmarkObj.uri, "Bookmark object contains an uri");
let parentId = aBookmarkObj.parentId ? aBookmarkObj.parentId
--- a/toolkit/components/satchel/nsFormFillController.cpp
+++ b/toolkit/components/satchel/nsFormFillController.cpp
@@ -552,17 +552,19 @@ nsFormFillController::OnSearchBegin()
NS_IMETHODIMP
nsFormFillController::OnSearchComplete()
{
return NS_OK;
}
NS_IMETHODIMP
-nsFormFillController::OnTextEntered(bool* aPrevent)
+nsFormFillController::OnTextEntered(nsIDOMEvent* aEvent,
+ int32_t aSelectedPopupIndex,
+ bool* aPrevent)
{
NS_ENSURE_ARG(aPrevent);
NS_ENSURE_TRUE(mFocusedInput, NS_OK);
// Fire off a DOMAutoComplete event
nsCOMPtr<nsIDOMDocument> domDoc;
nsCOMPtr<nsIDOMElement> element = do_QueryInterface(mFocusedInput);
element->GetOwnerDocument(getter_AddRefs(domDoc));
NS_ENSURE_STATE(domDoc);
@@ -1003,17 +1005,17 @@ nsFormFillController::KeyPress(nsIDOMEve
case nsIDOMKeyEvent::DOM_VK_ESCAPE:
mController->HandleEscape(&cancel);
break;
case nsIDOMKeyEvent::DOM_VK_TAB:
mController->HandleTab();
cancel = false;
break;
case nsIDOMKeyEvent::DOM_VK_RETURN:
- mController->HandleEnter(false, &cancel);
+ mController->HandleEnter(false, aEvent, &cancel);
break;
}
if (cancel) {
aEvent->PreventDefault();
// Don't let the page see the RETURN event when the popup is open
// (indicated by cancel=true) so sites don't manually submit forms
// (e.g. via submit.click()) without the autocompleted value being filled.
--- a/toolkit/content/browser-child.js
+++ b/toolkit/content/browser-child.js
@@ -585,17 +585,17 @@ var AutoCompletePopup = {
this._input = null;
this._popupOpen = false;
addMessageListener("FormAutoComplete:HandleEnter", message => {
this.selectedIndex = message.data.selectedIndex;
let controller = Components.classes["@mozilla.org/autocomplete/controller;1"].
getService(Components.interfaces.nsIAutoCompleteController);
- controller.handleEnter(message.data.isPopupSelection);
+ controller.handleEnter(message.data.isPopupSelection, null);
});
addEventListener("unload", function() {
AutoCompletePopup.destroy();
});
},
destroy: function() {
--- a/toolkit/content/widgets/autocomplete.xml
+++ b/toolkit/content/widgets/autocomplete.xml
@@ -39,17 +39,16 @@
<children includes="toolbarbutton"/>
</content>
<implementation implements="nsIAutoCompleteInput, nsIDOMXULMenuListElement">
<field name="mController">null</field>
<field name="mSearchNames">null</field>
<field name="mIgnoreInput">false</field>
- <field name="mEnterEvent">null</field>
<field name="_searchBeginHandler">null</field>
<field name="_searchCompleteHandler">null</field>
<field name="_textEnteredHandler">null</field>
<field name="_textRevertedHandler">null</field>
<constructor><![CDATA[
this.mController = Components.classes["@mozilla.org/autocomplete/controller;1"].
@@ -227,21 +226,23 @@
}
if (this._searchCompleteHandler)
this._searchCompleteHandler();
]]></body>
</method>
<method name="onTextEntered">
+ <parameter name="event"/>
+ <parameter name="selectedPopupIndex"/>
<body><![CDATA[
let rv = false;
- if (this._textEnteredHandler)
- rv = this._textEnteredHandler(this.mEnterEvent);
- this.mEnterEvent = null;
+ if (this._textEnteredHandler) {
+ rv = this._textEnteredHandler(event, selectedPopupIndex);
+ }
return rv;
]]></body>
</method>
<method name="onTextReverted">
<body><![CDATA[
if (this._textRevertedHandler)
return this._textRevertedHandler();
@@ -411,19 +412,23 @@
]]></body>
</method>
<!-- ::::::::::::: event dispatching ::::::::::::: -->
<method name="initEventHandler">
<parameter name="aEventType"/>
<body><![CDATA[
- let handlerString = this.getAttribute("on" + aEventType);
- if (handlerString) {
- return (new Function("eventType", "param", handlerString)).bind(this, aEventType);
+ let handlerCode = this.getAttribute("on" + aEventType);
+ if (handlerCode) {
+ return (...args) => {
+ let fn = (new Function("eventType", "args", handlerCode))
+ .bind(this, aEventType, args);
+ return fn();
+ };
}
return null;
]]></body>
</method>
<!-- ::::::::::::: key handling ::::::::::::: -->
<field name="_selectionDetails">null</field>
@@ -489,24 +494,23 @@
cancel = this.mController.handleEscape();
break;
case KeyEvent.DOM_VK_RETURN:
if (AppConstants.platform == "macosx") {
// Prevent the default action, since it will beep on Mac
if (aEvent.metaKey)
aEvent.preventDefault();
}
- this.mEnterEvent = aEvent;
if (this.mController.selection) {
this._selectionDetails = {
index: this.mController.selection.currentIndex,
kind: "key"
};
}
- cancel = this.handleEnter();
+ cancel = this.handleEnter(aEvent);
break;
case KeyEvent.DOM_VK_DELETE:
if (AppConstants.platform == "macosx" && !aEvent.shiftKey) {
break;
}
cancel = this.handleDelete();
break;
case KeyEvent.DOM_VK_BACK_SPACE:
@@ -531,18 +535,19 @@
aEvent.preventDefault();
}
return true;
]]></body>
</method>
<method name="handleEnter">
+ <parameter name="event"/>
<body><![CDATA[
- return this.mController.handleEnter(false);
+ return this.mController.handleEnter(false, event || null);
]]></body>
</method>
<method name="handleDelete">
<body><![CDATA[
return this.mController.handleDelete();
]]></body>
</method>
@@ -642,17 +647,17 @@
for (let i = 0; i < this.mController.matchCount; i++) {
let matchVal = this.mController.getFinalCompleteValueAt(i);
if (matchVal.toLowerCase() == filledVal) {
this.popup.selectedIndex = i;
break;
}
}
}
- this.mController.handleEnter(false);
+ this.mController.handleEnter(false, null);
}
if (!this.ignoreBlurWhileSearching)
this.detachController();
}
]]></handler>
</handlers>
</binding>
@@ -941,17 +946,17 @@ extends="chrome://global/content/binding
return aIndex;
]]></body>
</method>
<method name="onPopupClick">
<parameter name="aEvent"/>
<body><![CDATA[
var controller = this.view.QueryInterface(Components.interfaces.nsIAutoCompleteController);
- controller.handleEnter(true);
+ controller.handleEnter(true, aEvent);
]]></body>
</method>
</implementation>
<handlers>
<handler event="popupshowing"><![CDATA[
// If normalMaxRows wasn't already set by the input, then set it here
// so that we restore the correct number when the popup is hidden.
@@ -1248,40 +1253,49 @@ extends="chrome://global/content/binding
// due to new results, but only when: the item is the same, *OR*
// we are about to replace the currently mouse-selected item, to
// avoid surprising the user.
let iface = Components.interfaces.nsIAutoCompletePopup;
if (item.getAttribute("text") == trimmedSearchString &&
invalidateReason == iface.INVALIDATE_REASON_NEW_RESULT &&
(item.getAttribute("url") == url ||
this.richlistbox.mouseSelectedIndex === this._currentIndex)) {
- item.collapsed = false;
- // Call adjustSiteIconStart only after setting collapsed=false.
- // The calculations it does may be wrong otherwise.
- item.adjustSiteIconStart(this._siteIconStart);
- // The popup may have changed size between now and the last time
- // the item was shown, so always handle over/underflow.
- item.handleOverUnderflow();
- this._currentIndex++;
- continue;
+ // Additionally, if the item is a searchengine action, then it
+ // should only be reused if the engine name is the same as the
+ // popup's override engine name, if any.
+ let action = item._parseActionUrl(url);
+ if (!action ||
+ action.type != "searchengine" ||
+ !this.overrideSearchEngineName ||
+ action.params.engineName == this.overrideSearchEngineName) {
+ item.collapsed = false;
+ // Call adjustSiteIconStart only after setting collapsed=
+ // false. The calculations it does may be wrong otherwise.
+ item.adjustSiteIconStart(this._siteIconStart);
+ // The popup may have changed size between now and the last
+ // time the item was shown, so always handle over/underflow.
+ item.handleOverUnderflow();
+ this._currentIndex++;
+ continue;
+ }
}
}
else {
// need to create a new item
item = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "richlistitem");
item.setAttribute("dir", this.style.direction);
}
// set these attributes before we set the class
// so that we can use them from the constructor
let iconURI = controller.getImageAt(this._currentIndex);
item.setAttribute("image", iconURI);
item.setAttribute("url", url);
item.setAttribute("title", controller.getCommentAt(this._currentIndex));
- item.setAttribute("type", controller.getStyleAt(this._currentIndex));
+ item.setAttribute("originaltype", controller.getStyleAt(this._currentIndex));
item.setAttribute("text", trimmedSearchString);
if (this._currentIndex < existingItemsCount) {
// re-use the existing item
item._adjustAcItem();
item.collapsed = false;
}
else {
@@ -1846,17 +1860,17 @@ extends="chrome://global/content/binding
}
let title = this.getAttribute("title");
let displayUrl;
let originalUrl = this.getAttribute("url");
let emphasiseUrl = true;
- let type = this.getAttribute("type");
+ let type = this.getAttribute("originaltype");
let types = new Set(type.split(/\s+/));
let initialTypes = new Set(types);
// Remove types that should ultimately not be in the `type` string.
types.delete("action");
types.delete("autofill");
types.delete("heuristic");
type = [...types][0] || "";
@@ -1893,16 +1907,27 @@ extends="chrome://global/content/binding
emphasiseUrl = false;
// The order here is not localizable, we default to appending
// "- Search with Engine" to the search string, to be able to
// properly generate emphasis pairs. That said, no localization
// changed the order while it was possible, so doesn't look like
// there's a strong need for that.
let {engineName, searchSuggestion, searchQuery} = action.params;
+
+ // Override the engine name if the popup defines an override.
+ let override = popup.overrideSearchEngineName;
+ if (override && override != engineName) {
+ engineName = override;
+ action.params.engineName = override;
+ let newURL =
+ PlacesUtils.mozActionURI(action.type, action.params);
+ this.setAttribute("url", newURL);
+ }
+
let engineStr =
this._stringBundle.formatStringFromName("searchWithEngine",
[engineName], 1);
this._setUpDescription(this._actionText, engineStr, true);
// Make the title by generating an array of pairs and its
// corresponding interpolation string (e.g., "%1$S") to pass to
// _generateEmphasisPairs.
--- a/xpfe/components/autocomplete/resources/content/autocomplete.xml
+++ b/xpfe/components/autocomplete/resources/content/autocomplete.xml
@@ -1575,17 +1575,17 @@
<handlers>
<handler event="mouseout" action="this.popup.selectedIndex = -1;"/>
<handler event="mouseup"><![CDATA[
var rc = this.parentNode.treeBoxObject.getRowAt(event.clientX, event.clientY);
if (rc != -1) {
this.popup.selectedIndex = rc;
- this.popup.view.handleEnter(true);
+ this.popup.view.handleEnter(true, null);
}
]]></handler>
<handler event="mousemove"><![CDATA[
if (Date.now() - this.mLastMoveTime > 30) {
var rc = this.parentNode.treeBoxObject.getRowAt(event.clientX, event.clientY);
if (rc != -1 && rc != this.popup.selectedIndex)
this.popup.selectedIndex = rc;