Bug 1019483 - (Part 1) Create interface to manage autofill profiles. r=MattN

MozReview-Commit-ID: KrGSPz7B108

--HG--
extra : rebase_source : 29d93d82b5240420121024ec9fb7afea69d5600b
This commit is contained in:
Scott Wu 2017-03-06 15:56:51 +08:00
Родитель f55fc8d7c5
Коммит cf289cb4ce
9 изменённых файлов: 373 добавлений и 13 удалений

Просмотреть файл

@ -132,8 +132,6 @@ var whitelist = new Set([
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

Просмотреть файл

@ -76,7 +76,9 @@ FormAutofillParent.prototype = {
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);
@ -131,17 +133,10 @@ FormAutofillParent.prototype = {
},
/**
* 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.
@ -193,6 +188,10 @@ FormAutofillParent.prototype = {
}
break;
}
case "FormAutofill:RemoveProfiles": {
data.guids.forEach(guid => this.getProfileStore().remove(guid));
break;
}
}
},
@ -220,6 +219,7 @@ FormAutofillParent.prototype = {
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);
},

Просмотреть файл

@ -13,6 +13,7 @@ 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");
@ -126,7 +127,7 @@ FormAutofillPreferences.prototype = {
// 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;
}

Просмотреть файл

@ -4,6 +4,9 @@
body {
font-size: 1rem;
/* body needs padding until the edit profile dialog could be loaded as a
stacked subdialog */
padding: 2em;
}
form,

Просмотреть файл

@ -6,6 +6,10 @@
<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>

Просмотреть файл

@ -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;
}

Просмотреть файл

@ -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();

Просмотреть файл

@ -0,0 +1,23 @@
<?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://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>

Просмотреть файл

@ -196,7 +196,7 @@ xul|menulist {
}
html|button:enabled:hover,
html|select:enabled:hover,
html|select:not([size][multiple]):enabled:hover,
xul|button:not([disabled="true"]):hover,
xul|colorpicker[type="button"]:not([disabled="true"]):hover,
xul|menulist:not([disabled="true"]):hover {
@ -204,7 +204,7 @@ xul|menulist:not([disabled="true"]):hover {
}
html|button:enabled:hover:active,
html|select:enabled:hover:active,
html|select:not([size][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"]) {
@ -711,6 +711,7 @@ xul|filefield + xul|button {
/* List boxes */
html|select[size][multiple],
xul|richlistbox,
xul|listbox {
-moz-appearance: none;
@ -720,6 +721,7 @@ xul|listbox {
color: var(--in-content-text-color);
}
html|select[size][multiple] > html|option,
xul|treechildren::-moz-tree-row,
xul|listbox xul|listitem {
padding: 0.3em;
@ -729,6 +731,7 @@ xul|listbox xul|listitem {
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);