Mypal/toolkit/components/satchel/AutoCompletePopup.jsm

318 lines
9.1 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, this handleEnter is only called when the user clicks on
* one of the items in the popup since the popup is rendered in the parent process.
* The real controller's handleEnter is called directly in the content process
* for other methods of completing a selection (e.g. using the tab or enter
* keys) since the field with focus is in that process.
*/
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");
}
},
}