diff options
Diffstat (limited to 'toolkit/components/satchel/AutoCompletePopup.jsm')
-rw-r--r-- | toolkit/components/satchel/AutoCompletePopup.jsm | 317 |
1 files changed, 317 insertions, 0 deletions
diff --git a/toolkit/components/satchel/AutoCompletePopup.jsm b/toolkit/components/satchel/AutoCompletePopup.jsm new file mode 100644 index 000000000..7604e7bd5 --- /dev/null +++ b/toolkit/components/satchel/AutoCompletePopup.jsm @@ -0,0 +1,317 @@ +/* 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"); + } + }, +} |