The opening of the AutoCompletePopup will always start from content, but closing the popup can occur in the parent (for example, if the user switches focus from the browser), or in content (the user hits Esc, for example, which tells the parent to close the popup). This relationship between content and the popup has been true for a while, but the patch in bug 1294502 didn't account for it. In particular, before this patch, it was possible for AutoCompletePopup in browser-content.js and AutoCompletePopup.jsm to get out of sync on whether or not the popup is open. Mainly, this is because the parent wasn't telling the content that the popup had hidden if the hide was initialized by the parent. The other reason, was because the popupOpen state in browser-content.js was being set immediately, instead of waiting for the parent to report that the popup had indeed opened or closed. MozReview-Commit-ID: CRkg49lP1Hd
315 lines
8.9 KiB
JavaScript
315 lines
8.9 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
|
|
const Cc = Components.classes;
|
|
const Ci = Components.interfaces;
|
|
const Cu = Components.utils;
|
|
|
|
this.EXPORTED_SYMBOLS = [ "AutoCompletePopup" ];
|
|
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
|
|
|
// AutoCompleteResultView is an abstraction around a list of results
|
|
// we got back up from browser-content.js. It implements enough of
|
|
// nsIAutoCompleteController and nsIAutoCompleteInput to make the
|
|
// richlistbox popup work.
|
|
var AutoCompleteResultView = {
|
|
// nsISupports
|
|
QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteController,
|
|
Ci.nsIAutoCompleteInput]),
|
|
|
|
// Private variables
|
|
results: [],
|
|
|
|
// nsIAutoCompleteController
|
|
get matchCount() {
|
|
return this.results.length;
|
|
},
|
|
|
|
getValueAt(index) {
|
|
return this.results[index].value;
|
|
},
|
|
|
|
getLabelAt(index) {
|
|
// Unused by richlist autocomplete - see getCommentAt.
|
|
return "";
|
|
},
|
|
|
|
getCommentAt(index) {
|
|
// The richlist autocomplete popup uses comment for its main
|
|
// display of an item, which is why we're returning the label
|
|
// here instead.
|
|
return this.results[index].label;
|
|
},
|
|
|
|
getStyleAt(index) {
|
|
return this.results[index].style;
|
|
},
|
|
|
|
getImageAt(index) {
|
|
return this.results[index].image;
|
|
},
|
|
|
|
handleEnter: function(aIsPopupSelection) {
|
|
AutoCompletePopup.handleEnter(aIsPopupSelection);
|
|
},
|
|
|
|
stopSearch: function() {},
|
|
|
|
searchString: "",
|
|
|
|
// nsIAutoCompleteInput
|
|
get controller() {
|
|
return this;
|
|
},
|
|
|
|
get popup() {
|
|
return null;
|
|
},
|
|
|
|
_focus() {
|
|
AutoCompletePopup.requestFocus();
|
|
},
|
|
|
|
// Internal JS-only API
|
|
clearResults: function() {
|
|
this.results = [];
|
|
},
|
|
|
|
setResults: function(results) {
|
|
this.results = results;
|
|
},
|
|
};
|
|
|
|
this.AutoCompletePopup = {
|
|
MESSAGES: [
|
|
"FormAutoComplete:SelectBy",
|
|
"FormAutoComplete:GetSelectedIndex",
|
|
"FormAutoComplete:SetSelectedIndex",
|
|
"FormAutoComplete:MaybeOpenPopup",
|
|
"FormAutoComplete:ClosePopup",
|
|
"FormAutoComplete:Disconnect",
|
|
"FormAutoComplete:RemoveEntry",
|
|
"FormAutoComplete:Invalidate",
|
|
],
|
|
|
|
init: function() {
|
|
for (let msg of this.MESSAGES) {
|
|
Services.mm.addMessageListener(msg, this);
|
|
}
|
|
},
|
|
|
|
uninit: function() {
|
|
for (let msg of this.MESSAGES) {
|
|
Services.mm.removeMessageListener(msg, this);
|
|
}
|
|
},
|
|
|
|
handleEvent: function(evt) {
|
|
switch (evt.type) {
|
|
case "popupshowing": {
|
|
this.sendMessageToBrowser("FormAutoComplete:PopupOpened");
|
|
break;
|
|
}
|
|
|
|
case "popuphidden": {
|
|
AutoCompleteResultView.clearResults();
|
|
this.sendMessageToBrowser("FormAutoComplete:PopupClosed");
|
|
// adjustHeight clears the height from the popup so that
|
|
// we don't have a big shrink effect if we closed with a
|
|
// large list, and then open on a small one.
|
|
this.openedPopup.adjustHeight();
|
|
this.openedPopup = null;
|
|
this.weakBrowser = null;
|
|
evt.target.removeEventListener("popuphidden", this);
|
|
evt.target.removeEventListener("popupshowing", this);
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
// Along with being called internally by the receiveMessage handler,
|
|
// this function is also called directly by the login manager, which
|
|
// uses a single message to fill in the autocomplete results. See
|
|
// "RemoteLogins:autoCompleteLogins".
|
|
showPopupWithResults: function({ browser, rect, dir, results }) {
|
|
if (!results.length || this.openedPopup) {
|
|
// We shouldn't ever be showing an empty popup, and if we
|
|
// already have a popup open, the old one needs to close before
|
|
// we consider opening a new one.
|
|
return;
|
|
}
|
|
|
|
let window = browser.ownerDocument.defaultView;
|
|
let tabbrowser = window.gBrowser;
|
|
if (Services.focus.activeWindow != window ||
|
|
tabbrowser.selectedBrowser != browser) {
|
|
// We were sent a message from a window or tab that went into the
|
|
// background, so we'll ignore it for now.
|
|
return;
|
|
}
|
|
|
|
this.weakBrowser = Cu.getWeakReference(browser);
|
|
this.openedPopup = browser.autoCompletePopup;
|
|
this.openedPopup.hidden = false;
|
|
// don't allow the popup to become overly narrow
|
|
this.openedPopup.setAttribute("width", Math.max(100, rect.width));
|
|
this.openedPopup.style.direction = dir;
|
|
|
|
AutoCompleteResultView.setResults(results);
|
|
this.openedPopup.view = AutoCompleteResultView;
|
|
this.openedPopup.selectedIndex = -1;
|
|
|
|
if (results.length) {
|
|
// Reset fields that were set from the last time the search popup was open
|
|
this.openedPopup.mInput = AutoCompleteResultView;
|
|
this.openedPopup.showCommentColumn = false;
|
|
this.openedPopup.showImageColumn = false;
|
|
this.openedPopup.addEventListener("popuphidden", this);
|
|
this.openedPopup.addEventListener("popupshowing", this);
|
|
this.openedPopup.openPopupAtScreenRect("after_start", rect.left, rect.top,
|
|
rect.width, rect.height, false,
|
|
false);
|
|
this.openedPopup.invalidate();
|
|
} else {
|
|
this.closePopup();
|
|
}
|
|
},
|
|
|
|
invalidate(results) {
|
|
if (!this.openedPopup) {
|
|
return;
|
|
}
|
|
|
|
if (!results.length) {
|
|
this.closePopup();
|
|
} else {
|
|
AutoCompleteResultView.setResults(results);
|
|
this.openedPopup.invalidate();
|
|
}
|
|
},
|
|
|
|
closePopup() {
|
|
if (this.openedPopup) {
|
|
// Note that hidePopup() closes the popup immediately,
|
|
// so popuphiding or popuphidden events will be fired
|
|
// and handled during this call.
|
|
this.openedPopup.hidePopup();
|
|
}
|
|
},
|
|
|
|
removeLogin(login) {
|
|
Services.logins.removeLogin(login);
|
|
},
|
|
|
|
receiveMessage: function(message) {
|
|
if (!message.target.autoCompletePopup) {
|
|
// Returning false to pacify ESLint, but this return value is
|
|
// ignored by the messaging infrastructure.
|
|
return false;
|
|
}
|
|
|
|
switch (message.name) {
|
|
case "FormAutoComplete:SelectBy": {
|
|
if (this.openedPopup) {
|
|
this.openedPopup.selectBy(message.data.reverse, message.data.page);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case "FormAutoComplete:GetSelectedIndex": {
|
|
if (this.openedPopup) {
|
|
return this.openedPopup.selectedIndex;
|
|
}
|
|
// If the popup was closed, then the selection
|
|
// has not changed.
|
|
return -1;
|
|
}
|
|
|
|
case "FormAutoComplete:SetSelectedIndex": {
|
|
let { index } = message.data;
|
|
if (this.openedPopup) {
|
|
this.openedPopup.selectedIndex = index;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case "FormAutoComplete:MaybeOpenPopup": {
|
|
let { results, rect, dir } = message.data;
|
|
this.showPopupWithResults({ browser: message.target, rect, dir,
|
|
results });
|
|
break;
|
|
}
|
|
|
|
case "FormAutoComplete:Invalidate": {
|
|
let { results } = message.data;
|
|
this.invalidate(results);
|
|
break;
|
|
}
|
|
|
|
case "FormAutoComplete:ClosePopup": {
|
|
this.closePopup();
|
|
break;
|
|
}
|
|
|
|
case "FormAutoComplete:Disconnect": {
|
|
// The controller stopped controlling the current input, so clear
|
|
// any cached data. This is necessary cause otherwise we'd clear data
|
|
// only when starting a new search, but the next input could not support
|
|
// autocomplete and it would end up inheriting the existing data.
|
|
AutoCompleteResultView.clearResults();
|
|
break;
|
|
}
|
|
}
|
|
// Returning false to pacify ESLint, but this return value is
|
|
// ignored by the messaging infrastructure.
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Despite its name, handleEnter is what is called when the
|
|
* user clicks on one of the items in the popup.
|
|
*/
|
|
handleEnter(aIsPopupSelection) {
|
|
if (this.openedPopup) {
|
|
this.sendMessageToBrowser("FormAutoComplete:HandleEnter", {
|
|
selectedIndex: this.openedPopup.selectedIndex,
|
|
isPopupSelection: aIsPopupSelection,
|
|
});
|
|
}
|
|
},
|
|
|
|
/**
|
|
* If a browser exists that AutoCompletePopup knows about,
|
|
* sends it a message. Otherwise, this is a no-op.
|
|
*
|
|
* @param {string} msgName
|
|
* The name of the message to send.
|
|
* @param {object} data
|
|
* The optional data to send with the message.
|
|
*/
|
|
sendMessageToBrowser(msgName, data) {
|
|
let browser = this.weakBrowser ? this.weakBrowser.get()
|
|
: null;
|
|
if (browser) {
|
|
browser.messageManager.sendAsyncMessage(msgName, data);
|
|
}
|
|
},
|
|
|
|
stopSearch: function() {},
|
|
|
|
/**
|
|
* Sends a message to the browser requesting that the input
|
|
* that the AutoCompletePopup is open for be focused.
|
|
*/
|
|
requestFocus: function() {
|
|
if (this.openedPopup) {
|
|
this.sendMessageToBrowser("FormAutoComplete:Focus");
|
|
}
|
|
},
|
|
}
|