Index: chrome/browser/resources/options/password_manager_list.js |
diff --git a/chrome/browser/resources/options/password_manager_list.js b/chrome/browser/resources/options/password_manager_list.js |
index 78c9912b629381f11b1170e1d39995d8adb51f2f..c1b62de260ae7b9a539db59c148190577dd4a3e7 100644 |
--- a/chrome/browser/resources/options/password_manager_list.js |
+++ b/chrome/browser/resources/options/password_manager_list.js |
@@ -6,6 +6,8 @@ cr.define('options.passwordManager', function() { |
/** @const */ var ArrayDataModel = cr.ui.ArrayDataModel; |
/** @const */ var DeletableItemList = options.DeletableItemList; |
/** @const */ var DeletableItem = options.DeletableItem; |
+ /** @const */ var InlineEditableItemList = options.InlineEditableItemList; |
+ /** @const */ var InlineEditableItem = options.InlineEditableItem; |
/** @const */ var List = cr.ui.List; |
/** |
@@ -17,7 +19,7 @@ cr.define('options.passwordManager', function() { |
* @param {boolean} showPasswords If true, add a button to the element to |
* allow the user to reveal the saved password. |
* @constructor |
- * @extends {options.DeletableItem} |
+ * @extends {options.InlineEditableItem} |
*/ |
function PasswordListItem(dataModel, entry, showPasswords) { |
var el = cr.doc.createElement('div'); |
@@ -30,110 +32,248 @@ cr.define('options.passwordManager', function() { |
} |
PasswordListItem.prototype = { |
- __proto__: DeletableItem.prototype, |
+ __proto__: InlineEditableItem.prototype, |
/** @override */ |
decorate: function(showPasswords) { |
- DeletableItem.prototype.decorate.call(this); |
+ InlineEditableItem.prototype.decorate.call(this); |
- // The URL of the site. |
- var urlLabel = this.ownerDocument.createElement('div'); |
- urlLabel.classList.add('favicon-cell'); |
- urlLabel.classList.add('weakrtl'); |
- urlLabel.classList.add('url'); |
- urlLabel.setAttribute('title', this.url); |
- urlLabel.textContent = this.url; |
+ this.isPlaceholder = !this.url; |
- // The favicon URL is prefixed with "origin/", which essentially removes |
- // the URL path past the top-level domain and ensures that a scheme (e.g., |
- // http) is being used. This ensures that the favicon returned is the |
- // default favicon for the domain and that the URL has a scheme if none |
- // is present in the password manager. |
- urlLabel.style.backgroundImage = getFaviconImageSet( |
- 'origin/' + this.url, 16); |
- this.contentElement.appendChild(urlLabel); |
- |
- // The stored username. |
- var usernameLabel = this.ownerDocument.createElement('div'); |
- usernameLabel.className = 'name'; |
- usernameLabel.textContent = this.username; |
- usernameLabel.title = this.username; |
- this.contentElement.appendChild(usernameLabel); |
+ this.contentElement.appendChild(this.createUrlElement()); |
+ this.contentElement.appendChild(this.createUsernameElement()); |
- // The stored password. |
var passwordInputDiv = this.ownerDocument.createElement('div'); |
passwordInputDiv.className = 'password'; |
// The password input field. |
var passwordInput = this.ownerDocument.createElement('input'); |
passwordInput.type = 'password'; |
- passwordInput.className = 'inactive-password'; |
- passwordInput.readOnly = true; |
- passwordInput.value = showPasswords ? this.password : '********'; |
- passwordInputDiv.appendChild(passwordInput); |
+ if (!this.isPlaceholder) { |
+ passwordInput.className = 'inactive-password'; |
+ passwordInput.readOnly = true; |
+ passwordInput.value = showPasswords ? this.password : '********'; |
+ } |
this.passwordField = passwordInput; |
- // The show/hide button. |
- if (showPasswords) { |
- var button = this.ownerDocument.createElement('button'); |
- button.hidden = true; |
- button.className = 'list-inline-button custom-appearance'; |
- button.textContent = loadTimeData.getString('passwordShowButton'); |
- button.addEventListener('click', this.onClick_.bind(this), true); |
- button.addEventListener('mousedown', function(event) { |
+ // Makes the password input field editable. |
+ this.addEditField(passwordInput, null); |
+ |
+ // Keeps the password validity updated. |
+ this.updatePasswordValidity_(); |
+ passwordInput.addEventListener('input', function(event) { |
+ this.updatePasswordValidity_(); |
+ }.bind(this)); |
+ |
+ passwordInputDiv.appendChild(passwordInput); |
+ |
+ // The list-inline buttons. |
+ if (!this.isPlaceholder) { |
+ // The container of the list-inline buttons. |
+ var buttonsContainer = this.ownerDocument.createElement('div'); |
+ buttonsContainer.className = 'list-inline-buttons-container'; |
+ |
+ var mousedownEventHandler = function(event) { |
// Don't focus on this button by mousedown. |
event.preventDefault(); |
// Don't handle list item selection. It causes focus change. |
event.stopPropagation(); |
- }, false); |
- passwordInputDiv.appendChild(button); |
- this.passwordShowButton = button; |
+ }; |
+ |
+ // The overwrite button. |
+ var overwriteButton = this.ownerDocument.createElement('button'); |
+ overwriteButton.className = 'list-inline-button custom-appearance'; |
+ overwriteButton.textContent = loadTimeData.getString( |
+ 'passwordOverwriteButton'); |
+ overwriteButton.addEventListener( |
+ 'click', this.onClickOverwriteButton_.bind(this), true); |
+ overwriteButton.addEventListener( |
+ 'mousedown', mousedownEventHandler, false); |
+ buttonsContainer.appendChild(overwriteButton); |
+ this.passwordOverwriteButton = overwriteButton; |
+ |
+ // The show/hide button. |
+ if (showPasswords) { |
+ var button = this.ownerDocument.createElement('button'); |
+ button.className = 'list-inline-button custom-appearance'; |
+ button.textContent = loadTimeData.getString('passwordShowButton'); |
+ button.addEventListener( |
+ 'click', this.onClickShowButton_.bind(this), true); |
+ button.addEventListener('mousedown', mousedownEventHandler, false); |
+ buttonsContainer.appendChild(button); |
+ this.passwordShowButton = button; |
+ } |
+ |
+ passwordInputDiv.appendChild(buttonsContainer); |
} |
this.contentElement.appendChild(passwordInputDiv); |
+ |
+ // Adds the event listeners for editing. |
+ this.addEventListener('canceledit', this.resetInputs); |
+ this.addEventListener('commitedit', this.finishEdit); |
+ }, |
+ |
+ /** |
+ * Constructs and returns the URL element for this item. |
+ * @return {HTMLElement} The URL element. |
+ * @protected |
+ */ |
+ createUrlElement: function() { |
+ var urlEl = this.ownerDocument.createElement('div'); |
+ urlEl.className = 'favicon-cell weakrtl url'; |
+ urlEl.setAttribute('title', this.url); |
+ urlEl.textContent = this.url; |
+ |
+ // The favicon URL is prefixed with "origin/", which essentially removes |
+ // the URL path past the top-level domain and ensures that a scheme (e.g., |
+ // http) is being used. This ensures that the favicon returned is the |
+ // default favicon for the domain and that the URL has a scheme if none is |
+ // present in the password manager. |
+ urlEl.style.backgroundImage = getFaviconImageSet( |
+ 'origin/' + this.url, 16); |
+ return urlEl; |
+ }, |
+ |
+ /** |
+ * Constructs and returns the username element for this item. |
+ * @return {HTMLElement} The username element. |
+ * @protected |
+ */ |
+ createUsernameElement: function() { |
+ var usernameEl = this.ownerDocument.createElement('div'); |
+ usernameEl.className = 'name'; |
+ usernameEl.textContent = this.username; |
+ usernameEl.title = this.username; |
+ return usernameEl; |
}, |
/** @override */ |
selectionChanged: function() { |
- var input = this.passwordField; |
- var button = this.passwordShowButton; |
- // The button doesn't exist when passwords can't be shown. |
- if (!button) |
+ InlineEditableItem.prototype.selectionChanged.call(this); |
+ |
+ // Don't set 'inactive-password' class for the placeholder so that it |
+ // shows the background and the borders. |
+ if (this.isPlaceholder) |
return; |
- if (this.selected) { |
- input.classList.remove('inactive-password'); |
- button.hidden = false; |
- } else { |
- input.classList.add('inactive-password'); |
- button.hidden = true; |
- } |
+ this.passwordField.classList.toggle('inactive-password', !this.selected); |
+ }, |
+ |
+ /** @override */ |
+ get currentInputIsValid() { |
+ return !!this.passwordField.value; |
}, |
/** |
- * Reveals the plain text password of this entry. |
+ * Returns if the password has been edited. |
+ * @return {boolean} Whether the password has been edited. |
+ * @protected |
+ */ |
+ passwordHasBeenEdited: function() { |
+ return this.passwordField.value != this.password || this.overwriting; |
+ }, |
+ |
+ /** @override */ |
+ get hasBeenEdited() { |
+ return this.passwordHasBeenEdited(); |
+ }, |
+ |
+ /** |
+ * Reveals the plain text password of this entry. Never called for the Add |
+ * New Entry row. |
+ * @param {string} password The plain text password. |
*/ |
showPassword: function(password) { |
- this.passwordField.value = password; |
+ this.overwriting = false; |
+ this.password = password; |
+ this.setPasswordFieldValue_(password); |
this.passwordField.type = 'text'; |
+ this.passwordField.readOnly = false; |
+ this.passwordOverwriteButton.hidden = true; |
var button = this.passwordShowButton; |
if (button) |
button.textContent = loadTimeData.getString('passwordHideButton'); |
}, |
/** |
- * Hides the plain text password of this entry. |
+ * Hides the plain text password of this entry. Never called for the Add |
+ * New Entry row. |
+ * @private |
*/ |
- hidePassword: function() { |
+ hidePassword_: function() { |
this.passwordField.type = 'password'; |
+ this.passwordField.readOnly = true; |
+ this.passwordOverwriteButton.hidden = false; |
var button = this.passwordShowButton; |
if (button) |
button.textContent = loadTimeData.getString('passwordShowButton'); |
}, |
/** |
+ * Resets the input fields to their original values and states. |
+ * @protected |
+ */ |
+ resetInputs: function() { |
+ this.finishOverwriting_(); |
+ this.setPasswordFieldValue_(this.password); |
+ }, |
+ |
+ /** |
+ * Commits the new data to the browser. |
+ * @protected |
+ */ |
+ finishEdit: function() { |
+ this.password = this.passwordField.value; |
+ this.finishOverwriting_(); |
+ PasswordManager.updatePassword( |
+ this.getOriginalIndex_(), this.passwordField.value); |
+ }, |
+ |
+ /** |
+ * Called with the response of the browser, which indicates the validity of |
+ * the URL. |
+ * @param {string} url The URL. |
+ * @param {boolean} valid The validity of the URL. |
+ */ |
+ originValidityCheckComplete: function(url, valid) { |
+ assertNotReached(); |
+ }, |
+ |
+ /** |
+ * Updates the custom validity of the password input field. |
+ * @private |
+ */ |
+ updatePasswordValidity_: function() { |
+ this.passwordField.setCustomValidity(this.passwordField.value ? |
+ '' : loadTimeData.getString('editPasswordInvalidPasswordTooltip')); |
+ }, |
+ |
+ /** |
+ * Finishes password overwriting. |
+ * @private |
+ */ |
+ finishOverwriting_: function() { |
+ if (!this.overwriting) |
+ return; |
+ this.overwriting = false; |
+ this.passwordOverwriteButton.hidden = false; |
+ this.passwordField.readOnly = true; |
+ }, |
+ |
+ /** |
+ * Sets the value of the password input field. |
+ * @param {string} password The new value. |
+ * @private |
+ */ |
+ setPasswordFieldValue_: function(password) { |
+ this.passwordField.value = password; |
+ this.updatePasswordValidity_(); |
+ }, |
+ |
+ /** |
* Get the original index of this item in the data model. |
* @return {number} The index. |
* @private |
@@ -144,16 +284,34 @@ cr.define('options.passwordManager', function() { |
}, |
/** |
+ * Called when clicking the overwrite button. Allows the user to overwrite |
+ * the hidden password. |
+ * @param {Event} event The click event. |
+ * @private |
+ */ |
+ onClickOverwriteButton_: function(event) { |
+ this.overwriting = true; |
+ this.passwordOverwriteButton.hidden = true; |
+ |
+ this.setPasswordFieldValue_(''); |
+ this.passwordField.readOnly = false; |
+ this.passwordField.focus(); |
+ }, |
+ |
+ /** |
* On-click event handler. Swaps the type of the input field from password |
* to text and back. |
* @private |
*/ |
- onClick_: function(event) { |
+ onClickShowButton_: function(event) { |
+ // Prevents committing an edit. |
+ this.resetInputs(); |
+ |
if (this.passwordField.type == 'password') { |
// After the user is authenticated, showPassword() will be called. |
PasswordManager.requestShowPassword(this.getOriginalIndex_()); |
} else { |
- this.hidePassword(); |
+ this.hidePassword_(); |
} |
}, |
@@ -162,7 +320,7 @@ cr.define('options.passwordManager', function() { |
* @type {string} |
*/ |
get url() { |
- return this.dataItem[0]; |
+ return this.dataItem[0] || ''; |
}, |
set url(url) { |
this.dataItem[0] = url; |
@@ -173,7 +331,7 @@ cr.define('options.passwordManager', function() { |
* @type {string} |
*/ |
get username() { |
- return this.dataItem[1]; |
+ return this.dataItem[1] || ''; |
}, |
set username(username) { |
this.dataItem[1] = username; |
@@ -184,7 +342,7 @@ cr.define('options.passwordManager', function() { |
* @type {string} |
*/ |
get password() { |
- return this.dataItem[2]; |
+ return this.dataItem[2] || ''; |
}, |
set password(password) { |
this.dataItem[2] = password; |
@@ -192,6 +350,125 @@ cr.define('options.passwordManager', function() { |
}; |
/** |
+ * Creates a new passwords list item for the Add New Entry row. |
+ * @param {ArrayDataModel} dataModel The data model that contains this item. |
+ * @constructor |
+ * @extends {options.passwordManager.PasswordListItem} |
+ */ |
+ function PasswordAddRowListItem(dataModel) { |
+ var el = cr.doc.createElement('div'); |
+ el.dataItem = []; |
+ el.dataModel = dataModel; |
+ el.__proto__ = PasswordAddRowListItem.prototype; |
+ el.decorate(); |
+ |
+ return el; |
+ } |
+ |
+ PasswordAddRowListItem.prototype = { |
+ __proto__: PasswordListItem.prototype, |
+ |
+ /** @override */ |
+ decorate: function() { |
+ PasswordListItem.prototype.decorate.call(this, false); |
+ |
+ this.urlField.placeholder = loadTimeData.getString( |
+ 'newPasswordUrlFieldPlaceholder'); |
+ this.usernameField.placeholder = loadTimeData.getString( |
+ 'newPasswordUsernameFieldPlaceholder'); |
+ this.passwordField.placeholder = loadTimeData.getString( |
+ 'newPasswordPasswordFieldPlaceholder'); |
+ |
+ // Sets the validity of the URL initially. |
+ this.setUrlValid_(false); |
+ }, |
+ |
+ /** @override */ |
+ createUrlElement: function() { |
+ var urlEl = this.createEditableTextCell(''); |
+ urlEl.className += ' favicon-cell weakrtl url'; |
+ |
+ var urlField = urlEl.querySelector('input'); |
+ urlField.addEventListener('input', this.onUrlInput_.bind(this)); |
+ this.urlField = urlField; |
+ |
+ return urlEl; |
+ }, |
+ |
+ /** @override */ |
+ createUsernameElement: function() { |
+ var usernameEl = this.createEditableTextCell(''); |
+ usernameEl.className = 'name'; |
+ |
+ this.usernameField = usernameEl.querySelector('input'); |
+ |
+ return usernameEl; |
+ }, |
+ |
+ /** @override */ |
+ get currentInputIsValid() { |
+ return this.urlValidityKnown && this.urlIsValid && |
+ this.passwordField.value; |
+ }, |
+ |
+ /** @override */ |
+ get hasBeenEdited() { |
+ return this.urlField.value || this.usernameField.value || |
+ this.passwordHasBeenEdited(); |
+ }, |
+ |
+ /** @override */ |
+ resetInputs: function() { |
+ PasswordListItem.prototype.resetInputs.call(this); |
+ |
+ this.urlField.value = ''; |
+ this.usernameField.value = ''; |
+ |
+ this.setUrlValid_(false); |
+ }, |
+ |
+ /** @override */ |
+ finishEdit: function() { |
+ var newUrl = this.urlField.value; |
+ var newUsername = this.usernameField.value; |
+ var newPassword = this.passwordField.value; |
+ this.resetInputs(); |
+ |
+ PasswordManager.addPassword(newUrl, newUsername, newPassword); |
+ }, |
+ |
+ /** @override */ |
+ originValidityCheckComplete: function(url, valid) { |
+ if (url == this.urlField.value) |
+ this.setUrlValid_(valid); |
+ }, |
+ |
+ /** |
+ * Updates whether the URL in the input is valid. |
+ * @param {boolean} valid The validity of the URL. |
+ * @private |
+ */ |
+ setUrlValid_: function(valid) { |
+ this.urlIsValid = valid; |
+ this.urlValidityKnown = true; |
+ if (this.urlField) { |
+ this.urlField.setCustomValidity(valid ? |
+ '' : loadTimeData.getString('editPasswordInvalidUrlTooltip')); |
+ } |
+ }, |
+ |
+ /** |
+ * Called when inputting to a URL input. |
+ * @param {Event} event The input event. |
+ * @private |
+ */ |
+ onUrlInput_: function(event) { |
+ this.urlValidityKnown = false; |
+ PasswordManager.checkOriginValidityForAdding(this.urlField.value); |
+ }, |
+ }; |
+ |
+ /** |
* Creates a new PasswordExceptions list item. |
* @param {Array} entry A pair of the form [url, username]. |
* @constructor |
@@ -217,9 +494,7 @@ cr.define('options.passwordManager', function() { |
// The URL of the site. |
var urlLabel = this.ownerDocument.createElement('div'); |
- urlLabel.className = 'url'; |
- urlLabel.classList.add('favicon-cell'); |
- urlLabel.classList.add('weakrtl'); |
+ urlLabel.className = 'url favicon-cell weakrtl'; |
urlLabel.textContent = this.url; |
// The favicon URL is prefixed with "origin/", which essentially removes |
@@ -247,12 +522,12 @@ cr.define('options.passwordManager', function() { |
/** |
* Create a new passwords list. |
* @constructor |
- * @extends {options.DeletableItemList} |
+ * @extends {options.InlineEditableItemList} |
*/ |
var PasswordsList = cr.ui.define('list'); |
PasswordsList.prototype = { |
- __proto__: DeletableItemList.prototype, |
+ __proto__: InlineEditableItemList.prototype, |
/** |
* Whether passwords can be revealed or not. |
@@ -263,7 +538,7 @@ cr.define('options.passwordManager', function() { |
/** @override */ |
decorate: function() { |
- DeletableItemList.prototype.decorate.call(this); |
+ InlineEditableItemList.prototype.decorate.call(this); |
Preferences.getInstance().addEventListener( |
'profile.password_manager_allow_show_passwords', |
this.onPreferenceChanged_.bind(this)); |
@@ -284,6 +559,9 @@ cr.define('options.passwordManager', function() { |
* @param {Array} entry |
*/ |
createItem: function(entry) { |
+ if (!entry) |
+ return new PasswordAddRowListItem(this.dataModel); |
+ |
var showPasswords = this.showPasswords_; |
if (loadTimeData.getBoolean('disableShowPasswords')) |
@@ -343,6 +621,7 @@ cr.define('options.passwordManager', function() { |
return { |
PasswordListItem: PasswordListItem, |
+ PasswordAddRowListItem: PasswordAddRowListItem, |
PasswordExceptionsListItem: PasswordExceptionsListItem, |
PasswordsList: PasswordsList, |
PasswordExceptionsList: PasswordExceptionsList, |