Bug 1019483 - (Part 1) Create interface to manage autofill profiles. r=MattN
MozReview-Commit-ID: KrGSPz7B108
--- a/browser/base/content/test/static/browser_all_files_referenced.js
+++ b/browser/base/content/test/static/browser_all_files_referenced.js
@@ -127,18 +127,16 @@ var whitelist = new Set([
{file: "chrome://browser/skin/customizableui/customize-illustration@2x.png",
platforms: ["linux", "win"]},
{file: "chrome://browser/skin/customizableui/info-icon-customizeTip@2x.png",
platforms: ["linux", "win"]},
{file: "chrome://browser/skin/customizableui/panelarrow-customizeTip@2x.png",
platforms: ["linux", "win"]},
// Bug 1320058
{file: "chrome://browser/skin/preferences/saveFile.png", platforms: ["win"]},
- // Bug 1348369
- {file: "chrome://formautofill/content/editProfile.xhtml"},
// Bug 1316187
{file: "chrome://global/content/customizeToolbar.xul"},
// Bug 1343837
{file: "chrome://global/content/findUtils.js"},
// Bug 1343843
{file: "chrome://global/content/url-classifier/unittests.xul"},
// Bug 1343839
{file: "chrome://global/locale/headsUpDisplay.properties"},
--- a/browser/extensions/formautofill/FormAutofillParent.jsm
+++ b/browser/extensions/formautofill/FormAutofillParent.jsm
@@ -71,17 +71,19 @@ FormAutofillParent.prototype = {
*/
init() {
log.debug("init");
let storePath = OS.Path.join(OS.Constants.Path.profileDir, PROFILE_JSON_FILE_NAME);
this._profileStore = new ProfileStorage(storePath);
this._profileStore.initialize();
Services.obs.addObserver(this, "advanced-pane-loaded", false);
+ Services.ppmm.addMessageListener("FormAutofill:GetProfiles", this);
Services.ppmm.addMessageListener("FormAutofill:SaveProfile", this);
+ Services.ppmm.addMessageListener("FormAutofill:RemoveProfiles", this);
// Observing the pref and storage changes
Services.prefs.addObserver(ENABLED_PREF, this, false);
Services.obs.addObserver(this, "formautofill-storage-changed", false);
// Force to trigger the onStatusChanged function for setting listeners properly
// while initizlization
this._setStatus(this._getStatus());
@@ -126,27 +128,20 @@ FormAutofillParent.prototype = {
default: {
throw new Error(`FormAutofillParent: Unexpected topic observed: ${topic}`);
}
}
},
/**
- * Add/remove message listener and broadcast the status to frames while the
- * form autofill status changed.
+ * Broadcast the status to frames when the form autofill status changes.
*/
_onStatusChanged() {
log.debug("_onStatusChanged: Status changed to", this._enabled);
- if (this._enabled) {
- Services.ppmm.addMessageListener("FormAutofill:GetProfiles", this);
- } else {
- Services.ppmm.removeMessageListener("FormAutofill:GetProfiles", this);
- }
-
Services.ppmm.broadcastAsyncMessage("FormAutofill:enabledStatus", this._enabled);
// Sync process data autofillEnabled to make sure the value up to date
// no matter when the new content process is initialized.
Services.ppmm.initialProcessData.autofillEnabled = this._enabled;
},
/**
* Query pref and storage status to determine the overall status for
@@ -188,16 +183,20 @@ FormAutofillParent.prototype = {
case "FormAutofill:SaveProfile": {
if (data.guid) {
this.getProfileStore().update(data.guid, data.profile);
} else {
this.getProfileStore().add(data.profile);
}
break;
}
+ case "FormAutofill:RemoveProfiles": {
+ data.guids.forEach(guid => this.getProfileStore().remove(guid));
+ break;
+ }
}
},
/**
* Returns the instance of ProfileStorage. To avoid syncing issues, anyone
* who needs to access the profile should request the instance by this instead
* of creating a new one.
*
@@ -215,16 +214,17 @@ FormAutofillParent.prototype = {
_uninit() {
if (this._profileStore) {
this._profileStore._saveImmediately();
this._profileStore = null;
}
Services.ppmm.removeMessageListener("FormAutofill:GetProfiles", this);
Services.ppmm.removeMessageListener("FormAutofill:SaveProfile", this);
+ Services.ppmm.removeMessageListener("FormAutofill:RemoveProfiles", this);
Services.obs.removeObserver(this, "advanced-pane-loaded");
Services.prefs.removeObserver(ENABLED_PREF, this);
},
/**
* Get the profile data from profile store and return profiles back to content process.
*
* @private
--- a/browser/extensions/formautofill/FormAutofillPreferences.jsm
+++ b/browser/extensions/formautofill/FormAutofillPreferences.jsm
@@ -8,16 +8,17 @@
"use strict";
this.EXPORTED_SYMBOLS = ["FormAutofillPreferences"];
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
const PREF_AUTOFILL_ENABLED = "browser.formautofill.enabled";
const BUNDLE_URI = "chrome://formautofill/locale/formautofill.properties";
+const MANAGE_PROFILES_URL = "chrome://formautofill/content/manageProfiles.xhtml";
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://formautofill/FormAutofillUtils.jsm");
this.log = null;
FormAutofillUtils.defineLazyLogGetter(this, this.EXPORTED_SYMBOLS[0]);
@@ -121,17 +122,17 @@ FormAutofillPreferences.prototype = {
switch (event.type) {
case "command": {
let target = event.target;
if (target == this.refs.enabledCheckbox) {
// Set preference directly instead of relying on <Preference>
Services.prefs.setBoolPref(PREF_AUTOFILL_ENABLED, target.checked);
} else if (target == this.refs.savedProfilesBtn) {
- // TODO: Open Saved Profiles dialog
+ target.ownerGlobal.gSubDialog.open(MANAGE_PROFILES_URL);
}
break;
}
}
},
/**
* Attach event listener
--- a/browser/extensions/formautofill/content/editProfile.css
+++ b/browser/extensions/formautofill/content/editProfile.css
@@ -1,14 +1,17 @@
/* 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/. */
body {
font-size: 1rem;
+ /* body needs padding until the edit profile dialog could be loaded as a
+ stacked subdialog */
+ padding: 2em;
}
form,
label,
div {
display: flex;
}
--- a/browser/extensions/formautofill/content/editProfile.xhtml
+++ b/browser/extensions/formautofill/content/editProfile.xhtml
@@ -1,16 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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/. -->
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Profile Autofill - Edit Profile</title>
+ <!-- common.css and dialog.css need to be included until this file can be
+ - loaded as a stacked subdialog. -->
+ <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" />
+ <link rel="stylesheet" href="chrome://browser/skin/preferences/in-content/dialog.css" />
<link rel="stylesheet" href="chrome://formautofill/content/editProfile.css" />
<script src="chrome://formautofill/content/editProfile.js"></script>
</head>
<body>
<form>
<label id="first-name-container">
<span>First Name</span>
<input id="first-name" type="text"/>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/content/manageProfiles.css
@@ -0,0 +1,60 @@
+/* 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/. */
+
+div {
+ display: flex;
+}
+
+button {
+ padding: 3px 2em;
+}
+
+fieldset {
+ margin: 0;
+ padding: 0;
+ border: none;
+}
+
+fieldset > legend {
+ box-sizing: border-box;
+ width: 100%;
+ padding: 0.4em 0.7em;
+ font-size: 0.9em;
+ color: #808080;
+ background-color: var(--in-content-box-background-hover);
+ border: 1px solid var(--in-content-box-border-color);
+ border-radius: 2px 2px 0 0;
+}
+
+option:nth-child(even) {
+ background-color: -moz-oddtreerow;
+}
+
+#profiles {
+ font-size: 0.85em;
+ width: 100%;
+ height: 16.6em;
+ border-top: none;
+ border-radius: 0 0 2px 2px;
+}
+
+#profiles > option {
+ padding-inline-start: 0.7em;
+}
+
+#controls-container {
+ flex: 0 1 100%;
+ justify-content: end;
+ font-size: 0.9em;
+ margin-top: 1em;
+}
+
+#remove {
+ margin-inline-start: 0;
+ margin-inline-end: auto;
+}
+
+#edit {
+ margin-inline-end: 0;
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/content/manageProfiles.js
@@ -0,0 +1,268 @@
+/* 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 {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+const EDIT_PROFILE_URL = "chrome://formautofill/content/editProfile.xhtml";
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://formautofill/FormAutofillUtils.jsm");
+
+this.log = null;
+FormAutofillUtils.defineLazyLogGetter(this, "manageProfiles");
+
+function ManageProfileDialog() {
+ window.addEventListener("DOMContentLoaded", this, {once: true});
+}
+
+ManageProfileDialog.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
+
+ _elements: {},
+
+ /**
+ * Count the number of "formautofill-storage-changed" events epected to
+ * receive to prevent repeatedly loading profiles.
+ * @type {number}
+ */
+ _pendingChangeCount: 0,
+
+ /**
+ * Get the selected options on the profiles element.
+ *
+ * @returns {array<DOMElement>}
+ */
+ get _selectedOptions() {
+ return Array.from(this._elements.profiles.selectedOptions);
+ },
+
+ init() {
+ this._elements = {
+ profiles: document.getElementById("profiles"),
+ controlsContainer: document.getElementById("controls-container"),
+ remove: document.getElementById("remove"),
+ add: document.getElementById("add"),
+ edit: document.getElementById("edit"),
+ };
+ this.attachEventListeners();
+ },
+
+ uninit() {
+ log.debug("uninit");
+ this.detachEventListeners();
+ this._elements = null;
+ },
+
+ /**
+ * Load profiles and render them.
+ *
+ * @returns {promise}
+ */
+ loadProfiles() {
+ return this.getProfiles().then(profiles => {
+ log.debug("profiles:", profiles);
+ this.renderProfileElements(profiles);
+ this.updateButtonsStates(this._selectedOptions.length);
+ });
+ },
+
+ /**
+ * Get profiles from storage.
+ *
+ * @returns {promise}
+ */
+ getProfiles() {
+ return new Promise(resolve => {
+ Services.cpmm.addMessageListener("FormAutofill:Profiles", function getResult(result) {
+ Services.cpmm.removeMessageListener("FormAutofill:Profiles", getResult);
+ resolve(result.data);
+ });
+ Services.cpmm.sendAsyncMessage("FormAutofill:GetProfiles", {});
+ });
+ },
+
+ /**
+ * Render the profiles onto the page while maintaining selected options if
+ * they still exist.
+ *
+ * @param {array<object>} profiles
+ */
+ renderProfileElements(profiles) {
+ let selectedGuids = this._selectedOptions.map(option => option.value);
+ this.clearProfileElements();
+ for (let profile of profiles) {
+ let option = new Option(this.getProfileLabel(profile),
+ profile.guid,
+ false,
+ selectedGuids.includes(profile.guid));
+ option.profile = profile;
+ this._elements.profiles.appendChild(option);
+ }
+ },
+
+ /**
+ * Remove all existing profile elements.
+ */
+ clearProfileElements() {
+ let parent = this._elements.profiles;
+ while (parent.lastChild) {
+ parent.removeChild(parent.lastChild);
+ }
+ },
+
+ /**
+ * Remove profiles by guids.
+ * Keep track of the number of "formautofill-storage-changed" events to
+ * ignore before loading profiles.
+ *
+ * @param {array<string>} guids
+ */
+ removeProfiles(guids) {
+ this._pendingChangeCount += guids.length - 1;
+ Services.cpmm.sendAsyncMessage("FormAutofill:RemoveProfiles", {guids});
+ },
+
+ /**
+ * Get profile display label. It should display up to two pieces of
+ * information, separated by a comma.
+ *
+ * @param {object} profile
+ * @returns {string}
+ */
+ getProfileLabel(profile) {
+ // TODO: Implement a smarter way for deciding what to display
+ // as option text. Possibly improve the algorithm in
+ // ProfileAutoCompleteResult.jsm and reuse it here.
+ const fieldOrder = [
+ "street-address", // Street address
+ "address-level2", // City/Town
+ "organization", // Company or organization name
+ "address-level1", // Province/State (Standardized code if possible)
+ "country", // Country
+ "postal-code", // Postal code
+ "tel", // Phone number
+ "email", // Email address
+ ];
+
+ let parts = [];
+ for (const fieldName of fieldOrder) {
+ let string = profile[fieldName];
+ if (string) {
+ parts.push(string);
+ }
+ if (parts.length == 2) {
+ break;
+ }
+ }
+ return parts.join(", ");
+ },
+
+ /**
+ * Open the edit profile dialog to create/edit a profile.
+ *
+ * @param {object} profile [optional]
+ */
+ openEditDialog(profile) {
+ window.openDialog(EDIT_PROFILE_URL, null,
+ "chrome,centerscreen,modal,width=600,height=370",
+ profile);
+ },
+
+ /**
+ * Enable/disable the Edit and Remove buttons based on number of selected
+ * options.
+ *
+ * @param {number} selectedCount
+ */
+ updateButtonsStates(selectedCount) {
+ log.debug("updateButtonsStates:", selectedCount);
+ if (selectedCount == 0) {
+ this._elements.edit.setAttribute("disabled", "disabled");
+ this._elements.remove.setAttribute("disabled", "disabled");
+ } else if (selectedCount == 1) {
+ this._elements.edit.removeAttribute("disabled");
+ this._elements.remove.removeAttribute("disabled");
+ } else if (selectedCount > 1) {
+ this._elements.edit.setAttribute("disabled", "disabled");
+ this._elements.remove.removeAttribute("disabled");
+ }
+ },
+
+ /**
+ * Handle events
+ *
+ * @param {DOMEvent} event
+ */
+ handleEvent(event) {
+ switch (event.type) {
+ case "DOMContentLoaded": {
+ this.init();
+ this.loadProfiles();
+ break;
+ }
+ case "click": {
+ this.handleClick(event);
+ break;
+ }
+ case "change": {
+ this.updateButtonsStates(this._selectedOptions.length);
+ break;
+ }
+ case "unload": {
+ this.uninit();
+ break;
+ }
+ }
+ },
+
+ /**
+ * Handle click events
+ *
+ * @param {DOMEvent} event
+ */
+ handleClick(event) {
+ if (event.target == this._elements.remove) {
+ this.removeProfiles(this._selectedOptions.map(option => option.value));
+ } else if (event.target == this._elements.add) {
+ this.openEditDialog();
+ } else if (event.target == this._elements.edit) {
+ this.openEditDialog(this._selectedOptions[0].profile);
+ }
+ },
+
+ observe(subject, topic, data) {
+ switch (topic) {
+ case "formautofill-storage-changed": {
+ if (this._pendingChangeCount) {
+ this._pendingChangeCount -= 1;
+ return;
+ }
+ this.loadProfiles();
+ }
+ }
+ },
+
+ /**
+ * Attach event listener
+ */
+ attachEventListeners() {
+ window.addEventListener("unload", this, {once: true});
+ this._elements.profiles.addEventListener("change", this);
+ this._elements.controlsContainer.addEventListener("click", this);
+ Services.obs.addObserver(this, "formautofill-storage-changed", false);
+ },
+
+ /**
+ * Remove event listener
+ */
+ detachEventListeners() {
+ this._elements.profiles.removeEventListener("change", this);
+ this._elements.controlsContainer.removeEventListener("click", this);
+ Services.obs.removeObserver(this, "formautofill-storage-changed");
+ },
+};
+
+new ManageProfileDialog();
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/content/manageProfiles.xhtml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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/. -->
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Profile Autofill - Manage Profiles</title>
+ <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" />
+ <link rel="stylesheet" href="chrome://formautofill/content/manageProfiles.css" />
+ <script src="chrome://formautofill/content/manageProfiles.js"></script>
+</head>
+<body>
+ <fieldset>
+ <legend>Profiles</legend>
+ <select id="profiles" size="9" multiple="multiple"/>
+ </fieldset>
+ <div id="controls-container">
+ <button id="remove" disabled="disabled">Remove</button>
+ <button id="add">Add</button>
+ <button id="edit" disabled="disabled">Edit</button>
+ </div>
+</body>
+</html>
\ No newline at end of file
--- a/toolkit/themes/shared/in-content/common.inc.css
+++ b/toolkit/themes/shared/in-content/common.inc.css
@@ -191,25 +191,25 @@ xul|menulist {
-moz-border-right-colors: none !important;
-moz-border-bottom-colors: none !important;
-moz-border-left-colors: none !important;
border-radius: 2px;
background-color: var(--in-content-page-background);
}
html|button:enabled:hover,
-html|select:enabled:hover,
+html|select:not([size]):not([multiple]):enabled:hover,
xul|button:not([disabled="true"]):hover,
xul|colorpicker[type="button"]:not([disabled="true"]):hover,
xul|menulist:not([disabled="true"]):hover {
background-color: var(--in-content-box-background-hover);
}
html|button:enabled:hover:active,
-html|select:enabled:hover:active,
+html|select:not([size]):not([multiple]):enabled:hover:active,
xul|button:not([disabled="true"]):hover:active,
xul|colorpicker[type="button"]:not([disabled="true"]):hover:active,
xul|menulist[open="true"]:not([disabled="true"]) {
background-color: var(--in-content-box-background-active);
}
html|button:disabled,
html|select:disabled,
@@ -706,34 +706,37 @@ xul|filefield + xul|button:-moz-locale-d
xul|textbox + xul|button,
xul|filefield + xul|button {
border-inline-start: none;
}
/* List boxes */
+html|select[size][multiple],
xul|richlistbox,
xul|listbox {
-moz-appearance: none;
margin-inline-start: 0;
background-color: var(--in-content-box-background);
border: 1px solid var(--in-content-box-border-color);
color: var(--in-content-text-color);
}
+html|select[size][multiple] > html|option,
xul|treechildren::-moz-tree-row,
xul|listbox xul|listitem {
padding: 0.3em;
margin: 0;
border: none;
border-radius: 0;
background-image: none;
}
+html|select[size][multiple] > html|option:hover,
xul|treechildren::-moz-tree-row(hover),
xul|listbox xul|listitem:hover {
background-color: var(--in-content-item-hover);
}
xul|treechildren::-moz-tree-row(selected),
xul|listbox xul|listitem[selected="true"] {
background-color: var(--in-content-item-selected);