diff --git a/browser/extensions/formautofill/FormAutofillParent.jsm b/browser/extensions/formautofill/FormAutofillParent.jsm
index 478079b3eb06..5f5e56ee6875 100644
--- a/browser/extensions/formautofill/FormAutofillParent.jsm
+++ b/browser/extensions/formautofill/FormAutofillParent.jsm
@@ -125,8 +125,29 @@ FormAutofillParent.prototype = {
Services.ppmm.addMessageListener("FormAutofill:GetDecryptedString", this);
Services.prefs.addObserver(ENABLED_AUTOFILL_CREDITCARDS_PREF, this);
}
+
+ for (let win of Services.wm.getEnumerator("navigator:browser")) {
+ this.injectElements(win.document);
+ }
+ Services.wm.addListener(this);
},
+ injectElements(doc) {
+ Services.scriptloader.loadSubScript("chrome://formautofill/content/customElements.js",
+ doc.ownerGlobal);
+ },
+
+ onOpenWindow(xulWindow) {
+ const win = xulWindow.docShell.domWindow;
+ win.addEventListener("load", () => {
+ if (win.document.documentElement.getAttribute("windowtype") == "navigator:browser") {
+ this.injectElements(win.document);
+ }
+ }, {once: true});
+ },
+
+ onCloseWindow() {},
+
observe(subject, topic, data) {
log.debug("observe:", topic, "with data:", data);
switch (topic) {
@@ -280,6 +301,7 @@ FormAutofillParent.prototype = {
Services.ppmm.removeMessageListener("FormAutofill:RemoveAddresses", this);
Services.obs.removeObserver(this, "privacy-pane-loaded");
Services.prefs.removeObserver(ENABLED_AUTOFILL_ADDRESSES_PREF, this);
+ Services.wm.removeListener(this);
if (FormAutofill.isAutofillCreditCardsAvailable) {
Services.ppmm.removeMessageListener("FormAutofill:SaveCreditCard", this);
diff --git a/browser/extensions/formautofill/content/customElements.js b/browser/extensions/formautofill/content/customElements.js
new file mode 100644
index 000000000000..2e30870b4d9b
--- /dev/null
+++ b/browser/extensions/formautofill/content/customElements.js
@@ -0,0 +1,357 @@
+/* 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/. */
+
+// This file is loaded into the browser window scope.
+/* eslint-env mozilla/browser-window */
+/* eslint-disable mozilla/balanced-listeners */ // Not relevant since the document gets unloaded.
+
+"use strict";
+
+// Wrap in a block to prevent leaking to window scope.
+(() => {
+ const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+ class MozAutocompleteProfileListitemBase extends MozElements.MozRichlistitem {
+ constructor() {
+ super();
+
+ /**
+ * For form autofill, we want to unify the selection no matter by
+ * keyboard navigation or mouseover in order not to confuse user which
+ * profile preview is being shown. This field is set to true to indicate
+ * that selectedIndex of popup should be changed while mouseover item
+ */
+ this.selectedByMouseOver = true;
+ }
+
+ get _stringBundle() {
+ if (!this.__stringBundle) {
+ this.__stringBundle = Services.strings.createBundle(
+ "chrome://formautofill/locale/formautofill.properties"
+ );
+ }
+ return this.__stringBundle;
+ }
+
+ _cleanup() {
+ this.removeAttribute("formautofillattached");
+ if (this._itemBox) {
+ this._itemBox.removeAttribute("size");
+ }
+ }
+
+ _onOverflow() {}
+
+ _onUnderflow() {}
+
+ handleOverUnderflow() {}
+
+ _adjustAutofillItemLayout() {
+ let outerBoxRect = this.parentNode.getBoundingClientRect();
+
+ // Make item fit in popup as XUL box could not constrain
+ // item's width
+ this._itemBox.style.width = outerBoxRect.width + "px";
+ // Use two-lines layout when width is smaller than 150px or
+ // 185px if an image precedes the label.
+ let oneLineMinRequiredWidth = this.getAttribute("ac-image") ? 185 : 150;
+
+ if (outerBoxRect.width <= oneLineMinRequiredWidth) {
+ this._itemBox.setAttribute("size", "small");
+ } else {
+ this._itemBox.removeAttribute("size");
+ }
+ }
+ }
+
+ MozElements.MozAutocompleteProfileListitem = class MozAutocompleteProfileListitem extends MozAutocompleteProfileListitemBase {
+ connectedCallback() {
+ if (this.delayConnectedCallback()) {
+ return;
+ }
+
+ this.textContent = "";
+
+ this.appendChild(MozXULElement.parseXULToFragment(`
+
+ `));
+
+ this._itemBox = this.querySelector(".autofill-item-box");
+ this._labelAffix = this.querySelector(".profile-label-affix");
+ this._label = this.querySelector(".profile-label");
+ this._comment = this.querySelector(".profile-comment");
+
+ this._updateAttributes();
+ this._adjustAcItem();
+ }
+
+ static get observedAttributes() {
+ return [
+ "ac-image",
+ ];
+ }
+
+ attributeChangedCallback(name, oldValue, newValue) {
+ if (this.isConnectedAndReady && name == "ac-image" && oldValue != newValue) {
+ this._updateAttributes();
+ }
+ }
+
+ _updateAttributes() {
+ this.inheritAttribute(this._itemBox, "ac-image");
+ }
+
+ set selected(val) {
+ if (val) {
+ this.setAttribute("selected", "true");
+ } else {
+ this.removeAttribute("selected");
+ }
+
+ let {AutoCompletePopup} =
+ ChromeUtils.import("resource://gre/modules/AutoCompletePopup.jsm");
+ AutoCompletePopup.sendMessageToBrowser("FormAutofill:PreviewProfile");
+
+ return val;
+ }
+
+ get selected() {
+ return this.getAttribute("selected") == "true";
+ }
+
+ _adjustAcItem() {
+ this._adjustAutofillItemLayout();
+ this.setAttribute("formautofillattached", "true");
+ this._itemBox.style.setProperty("--primary-icon", `url(${this.getAttribute("ac-image")})`);
+
+ let {primaryAffix, primary, secondary} = JSON.parse(this.getAttribute("ac-value"));
+
+ this._labelAffix.textContent = primaryAffix;
+ this._label.textContent = primary;
+ this._comment.textContent = secondary;
+ }
+ };
+
+ customElements.define(
+ "autocomplete-profile-listitem",
+ MozElements.MozAutocompleteProfileListitem,
+ {extends: "richlistitem"}
+ );
+
+ class MozAutocompleteProfileListitemFooter extends MozAutocompleteProfileListitemBase {
+ constructor() {
+ super();
+
+ this.addEventListener("click", (event) => {
+ if (event.button != 0) {
+ return;
+ }
+
+ if (this._warningTextBox.contains(event.originalTarget)) {
+ return;
+ }
+
+ window.openPreferences("privacy-form-autofill", {origin: "autofillFooter"});
+ });
+ }
+
+ connectedCallback() {
+ if (this.delayConnectedCallback()) {
+ return;
+ }
+
+ this.textContent = "";
+ this.appendChild(MozXULElement.parseXULToFragment(`
+
+ `));
+
+ this._itemBox = this.querySelector(".autofill-footer");
+ this._optionButton = this.querySelector(".autofill-button");
+ this._warningTextBox = this.querySelector(".autofill-warning");
+
+ /**
+ * A handler for updating warning message once selectedIndex has been changed.
+ *
+ * There're three different states of warning message:
+ * 1. None of addresses were selected: We show all the categories intersection of fields in the
+ * form and fields in the results.
+ * 2. An address was selested: Show the additional categories that will also be filled.
+ * 3. An address was selected, but the focused category is the same as the only one category: Only show
+ * the exact category that we're going to fill in.
+ *
+ * @private
+ * @param {string[]} data.categories
+ * The categories of all the fields contained in the selected address.
+ */
+ this._updateWarningNote = ({data} = {}) => {
+ let categories = (data && data.categories) ? data.categories : this._allFieldCategories;
+ // If the length of categories is 1, that means all the fillable fields are in the same
+ // category. We will change the way to inform user according to this flag. When the value
+ // is true, we show "Also autofills ...", otherwise, show "Autofills ..." only.
+ let hasExtraCategories = categories.length > 1;
+ // Show the categories in certain order to conform with the spec.
+ let orderedCategoryList = [{id: "address", l10nId: "category.address"},
+ {id: "name", l10nId: "category.name"},
+ {id: "organization", l10nId: "category.organization2"},
+ {id: "tel", l10nId: "category.tel"},
+ {id: "email", l10nId: "category.email"},
+ ];
+ let showCategories = hasExtraCategories ?
+ orderedCategoryList.filter(category => categories.includes(category.id) && category.id != this._focusedCategory) :
+ [orderedCategoryList.find(category => category.id == this._focusedCategory)];
+
+ let separator = this._stringBundle.GetStringFromName("fieldNameSeparator");
+ let warningTextTmplKey = hasExtraCategories ? "phishingWarningMessage" : "phishingWarningMessage2";
+ let categoriesText = showCategories.map(category => this._stringBundle.GetStringFromName(category.l10nId)).join(separator);
+
+ this._warningTextBox.textContent = this._stringBundle.formatStringFromName(warningTextTmplKey, [categoriesText], 1);
+ this.parentNode.parentNode.adjustHeight();
+ };
+
+ this._adjustAcItem();
+ }
+
+ _onCollapse() {
+ /* global messageManager */
+ if (this.showWarningText) {
+ messageManager.removeMessageListener(
+ "FormAutofill:UpdateWarningMessage", this._updateWarningNote
+ );
+ }
+ this._itemBox.removeAttribute("no-warning");
+ }
+
+ _adjustAcItem() {
+ /* global Cu */
+ this._adjustAutofillItemLayout();
+ this.setAttribute("formautofillattached", "true");
+
+ let {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm", {});
+ // TODO: The "Short" suffix is pointless now as normal version string is no longer needed,
+ // we should consider removing the suffix if possible when the next time locale change.
+ let buttonTextBundleKey = AppConstants.platform == "macosx" ?
+ "autocompleteFooterOptionOSXShort" : "autocompleteFooterOptionShort";
+ let buttonText = this._stringBundle.GetStringFromName(buttonTextBundleKey);
+ this._optionButton.textContent = buttonText;
+
+ let value = JSON.parse(this.getAttribute("ac-value"));
+
+ this._allFieldCategories = value.categories;
+ this._focusedCategory = value.focusedCategory;
+ this.showWarningText = this._allFieldCategories && this._focusedCategory;
+
+ if (this.showWarningText) {
+ messageManager.addMessageListener("FormAutofill:UpdateWarningMessage", this._updateWarningNote);
+ this._updateWarningNote();
+ } else {
+ this._itemBox.setAttribute("no-warning", "true");
+ }
+ }
+ }
+
+ customElements.define(
+ "autocomplete-profile-listitem-footer",
+ MozAutocompleteProfileListitemFooter,
+ {extends: "richlistitem"}
+ );
+
+ class MozAutocompleteCreditcardInsecureField extends MozAutocompleteProfileListitemBase {
+ connectedCallback() {
+ if (this.delayConnectedCallback()) {
+ return;
+ }
+ this.textContent = "";
+ this.appendChild(MozXULElement.parseXULToFragment(`
+
+ `));
+
+ this._itemBox = this.querySelector(".autofill-insecure-item");
+
+ this._adjustAcItem();
+ }
+
+ set selected(val) {
+ // Make this item unselectable since we see this item as a pure message.
+ return false;
+ }
+
+ get selected() {
+ return this.getAttribute("selected") == "true";
+ }
+
+ _adjustAcItem() {
+ this._adjustAutofillItemLayout();
+ this.setAttribute("formautofillattached", "true");
+
+ let value = this.getAttribute("ac-value");
+ this._itemBox.textContent = value;
+ }
+ }
+
+ customElements.define(
+ "autocomplete-creditcard-insecure-field",
+ MozAutocompleteCreditcardInsecureField,
+ {extends: "richlistitem"}
+ );
+
+ class MozAutocompleteProfileListitemClearButton extends MozAutocompleteProfileListitemBase {
+ constructor() {
+ super();
+
+ this.addEventListener("click", (event) => {
+ if (event.button != 0) {
+ return;
+ }
+
+ let {AutoCompletePopup} =
+ ChromeUtils.import("resource://gre/modules/AutoCompletePopup.jsm");
+ AutoCompletePopup.sendMessageToBrowser("FormAutofill:ClearForm");
+ });
+ }
+
+ connectedCallback() {
+ if (this.delayConnectedCallback()) {
+ return;
+ }
+
+ this.textContent = "";
+ this.appendChild(MozXULElement.parseXULToFragment(`
+
+ `));
+
+ this._itemBox = this.querySelector(".autofill-item-box");
+ this._clearBtn = this.querySelector(".autofill-button");
+
+ this._adjustAcItem();
+ }
+
+ _adjustAcItem() {
+ this._adjustAutofillItemLayout();
+ this.setAttribute("formautofillattached", "true");
+
+ let clearFormBtnLabel =
+ this._stringBundle.GetStringFromName("clearFormBtnLabel2");
+ this._clearBtn.textContent = clearFormBtnLabel;
+ }
+ }
+
+ customElements.define(
+ "autocomplete-profile-listitem-clear-button",
+ MozAutocompleteProfileListitemClearButton,
+ {extends: "richlistitem"}
+ );
+})();
diff --git a/browser/extensions/formautofill/content/formautofill.css b/browser/extensions/formautofill/content/formautofill.css
index d0e4eb7c4c46..857edfc9d9d6 100644
--- a/browser/extensions/formautofill/content/formautofill.css
+++ b/browser/extensions/formautofill/content/formautofill.css
@@ -11,23 +11,9 @@
padding: 0;
height: auto;
min-height: auto;
+ -moz-binding: none;
}
-#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-profile"] {
- -moz-binding: url("chrome://formautofill/content/formautofill.xml#autocomplete-profile-listitem");
-}
-
-#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-footer"] {
- -moz-binding: url("chrome://formautofill/content/formautofill.xml#autocomplete-profile-listitem-footer");
-}
-
-#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-insecureWarning"] {
- -moz-binding: url("chrome://formautofill/content/formautofill.xml#autocomplete-creditcard-insecure-field");
-}
-
-#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-clear-button"] {
- -moz-binding: url("chrome://formautofill/content/formautofill.xml#autocomplete-profile-listitem-clear-button");
-}
/* Treat @collpased="true" as display: none similar to how it is for XUL elements.
* https://developer.mozilla.org/en-US/docs/Web/CSS/visibility#Values */
#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-profile"][collapsed="true"],
diff --git a/browser/extensions/formautofill/content/formautofill.xml b/browser/extensions/formautofill/content/formautofill.xml
deleted file mode 100644
index 1a99e61d133e..000000000000
--- a/browser/extensions/formautofill/content/formautofill.xml
+++ /dev/null
@@ -1,354 +0,0 @@
-
-
-
-
-
-
-
-
-
-
- true
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/toolkit/content/widgets/autocomplete.xml b/toolkit/content/widgets/autocomplete.xml
index a0d5cc87692a..c6410339a20a 100644
--- a/toolkit/content/widgets/autocomplete.xml
+++ b/toolkit/content/widgets/autocomplete.xml
@@ -1060,10 +1060,16 @@
let options = null;
switch (style) {
case "autofill-profile":
+ options = { is: "autocomplete-profile-listitem" };
+ break;
case "autofill-footer":
+ options = { is: "autocomplete-profile-listitem-footer" };
+ break;
case "autofill-clear-button":
+ options = { is: "autocomplete-profile-listitem-clear-button" };
+ break;
case "autofill-insecureWarning":
- // implemented via XBL bindings, no CE for them
+ options = { is: "autocomplete-creditcard-insecure-field" };
break;
case "insecureWarning":
options = { is: "autocomplete-richlistitem-insecure-warning" };