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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - -
-
- -
-
-
- - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - { - 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(); - ]]> - - - - - - - - - - - - - - - - - - -
-
-
- - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - -
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" };