forked from mirrors/gecko-dev
Differential Revision: https://phabricator.services.mozilla.com/D57846 --HG-- extra : moz-landing-system : lando
759 lines
24 KiB
JavaScript
759 lines
24 KiB
JavaScript
/* 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/. */
|
|
|
|
import { recordTelemetryEvent } from "../aboutLoginsUtils.js";
|
|
|
|
export default class LoginItem extends HTMLElement {
|
|
/**
|
|
* The number of milliseconds to display the "Copied" success message
|
|
* before reverting to the normal "Copy" button.
|
|
*/
|
|
static get COPY_BUTTON_RESET_TIMEOUT() {
|
|
return 5000;
|
|
}
|
|
|
|
constructor() {
|
|
super();
|
|
this._login = {};
|
|
this._error = null;
|
|
this._copyUsernameTimeoutId = 0;
|
|
this._copyPasswordTimeoutId = 0;
|
|
}
|
|
|
|
connectedCallback() {
|
|
if (this.shadowRoot) {
|
|
this.render();
|
|
return;
|
|
}
|
|
|
|
let loginItemTemplate = document.querySelector("#login-item-template");
|
|
let shadowRoot = this.attachShadow({ mode: "open" });
|
|
document.l10n.connectRoot(shadowRoot);
|
|
shadowRoot.appendChild(loginItemTemplate.content.cloneNode(true));
|
|
|
|
this._cancelButton = this.shadowRoot.querySelector(".cancel-button");
|
|
this._confirmDeleteDialog = document.querySelector("confirm-delete-dialog");
|
|
this._copyPasswordButton = this.shadowRoot.querySelector(
|
|
".copy-password-button"
|
|
);
|
|
this._copyUsernameButton = this.shadowRoot.querySelector(
|
|
".copy-username-button"
|
|
);
|
|
this._deleteButton = this.shadowRoot.querySelector(".delete-button");
|
|
this._editButton = this.shadowRoot.querySelector(".edit-button");
|
|
this._errorMessage = this.shadowRoot.querySelector(".error-message");
|
|
this._errorMessageLink = this._errorMessage.querySelector(
|
|
".error-message-link"
|
|
);
|
|
this._errorMessageText = this._errorMessage.querySelector(
|
|
".error-message-text"
|
|
);
|
|
this._form = this.shadowRoot.querySelector("form");
|
|
this._originInput = this.shadowRoot.querySelector("input[name='origin']");
|
|
this._usernameInput = this.shadowRoot.querySelector(
|
|
"input[name='username']"
|
|
);
|
|
this._passwordInput = this.shadowRoot.querySelector(
|
|
"input[name='password']"
|
|
);
|
|
this._revealCheckbox = this.shadowRoot.querySelector(
|
|
".reveal-password-checkbox"
|
|
);
|
|
this._saveChangesButton = this.shadowRoot.querySelector(
|
|
".save-changes-button"
|
|
);
|
|
this._favicon = this.shadowRoot.querySelector(".login-item-favicon");
|
|
this._faviconWrapper = this.shadowRoot.querySelector(
|
|
".login-item-favicon-wrapper"
|
|
);
|
|
this._title = this.shadowRoot.querySelector(".login-item-title");
|
|
this._timeCreated = this.shadowRoot.querySelector(".time-created");
|
|
this._timeChanged = this.shadowRoot.querySelector(".time-changed");
|
|
this._timeUsed = this.shadowRoot.querySelector(".time-used");
|
|
this._breachAlert = this.shadowRoot.querySelector(".breach-alert");
|
|
this._breachAlertLink = this._breachAlert.querySelector(
|
|
".breach-alert-link"
|
|
);
|
|
this._dismissBreachAlert = this.shadowRoot.querySelector(
|
|
".dismiss-breach-alert"
|
|
);
|
|
|
|
this.render();
|
|
|
|
this._breachAlertLink.addEventListener("click", this);
|
|
this._cancelButton.addEventListener("click", this);
|
|
this._copyPasswordButton.addEventListener("click", this);
|
|
this._copyUsernameButton.addEventListener("click", this);
|
|
this._deleteButton.addEventListener("click", this);
|
|
this._dismissBreachAlert.addEventListener("click", this);
|
|
this._editButton.addEventListener("click", this);
|
|
this._errorMessageLink.addEventListener("click", this);
|
|
this._form.addEventListener("submit", this);
|
|
this._originInput.addEventListener("blur", this);
|
|
this._originInput.addEventListener("click", this);
|
|
this._originInput.addEventListener("mousedown", this, true);
|
|
this._originInput.addEventListener("auxclick", this);
|
|
this._revealCheckbox.addEventListener("click", this);
|
|
window.addEventListener("AboutLoginsInitialLoginSelected", this);
|
|
window.addEventListener("AboutLoginsLoadInitialFavicon", this);
|
|
window.addEventListener("AboutLoginsLoginSelected", this);
|
|
window.addEventListener("AboutLoginsShowBlankLogin", this);
|
|
}
|
|
|
|
focus() {
|
|
if (!this._breachAlert.hidden) {
|
|
this._breachAlertLink.focus();
|
|
} else if (!this._editButton.disabled) {
|
|
this._editButton.focus();
|
|
} else if (!this._deleteButton.disabled) {
|
|
this._deleteButton.focus();
|
|
} else {
|
|
this._originInput.focus();
|
|
}
|
|
}
|
|
|
|
async render() {
|
|
if (this._error) {
|
|
if (this._error.errorMessage.includes("This login already exists")) {
|
|
document.l10n.setAttributes(
|
|
this._errorMessageLink,
|
|
"about-logins-error-message-duplicate-login-with-link",
|
|
{
|
|
loginTitle: this._error.login.title,
|
|
}
|
|
);
|
|
this._errorMessageLink.dataset.errorGuid = this._error.existingLoginGuid;
|
|
this._errorMessageText.hidden = true;
|
|
this._errorMessageLink.hidden = false;
|
|
} else {
|
|
this._errorMessageText.hidden = false;
|
|
this._errorMessageLink.hidden = true;
|
|
}
|
|
}
|
|
this._errorMessage.hidden = !this._error;
|
|
|
|
this._breachAlert.hidden =
|
|
!this._breachesMap || !this._breachesMap.has(this._login.guid);
|
|
if (!this._breachAlert.hidden) {
|
|
const breachDetails = this._breachesMap.get(this._login.guid);
|
|
this._breachAlertLink.href = breachDetails.breachAlertURL;
|
|
}
|
|
document.l10n.setAttributes(this._timeCreated, "login-item-time-created", {
|
|
timeCreated: this._login.timeCreated || "",
|
|
});
|
|
document.l10n.setAttributes(this._timeChanged, "login-item-time-changed", {
|
|
timeChanged: this._login.timePasswordChanged || "",
|
|
});
|
|
document.l10n.setAttributes(this._timeUsed, "login-item-time-used", {
|
|
timeUsed: this._login.timeLastUsed || "",
|
|
});
|
|
|
|
if (this._login.faviconDataURI) {
|
|
this._faviconWrapper.classList.add("hide-default-favicon");
|
|
this._favicon.src = this._login.faviconDataURI;
|
|
this._favicon.hidden = false;
|
|
} else {
|
|
// reset the src and alt attributes if the currently selected favicon doesn't have a favicon
|
|
this._favicon.src = "";
|
|
this._favicon.hidden = true;
|
|
this._faviconWrapper.classList.remove("hide-default-favicon");
|
|
}
|
|
|
|
this._title.textContent = this._login.title;
|
|
this._title.title = this._login.title;
|
|
this._originInput.defaultValue = this._login.origin || "";
|
|
this._usernameInput.defaultValue = this._login.username || "";
|
|
if (this._login.password) {
|
|
// We use .value instead of .defaultValue since the latter updates the
|
|
// content attribute making the password easily viewable with Inspect
|
|
// Element even when Master Password is enabled. This is only run when
|
|
// the password is non-empty since setting the field to an empty value
|
|
// would mark the field as 'dirty' for form validation and thus trigger
|
|
// the error styling since the password field is 'required'.
|
|
this._passwordInput.value = this._login.password;
|
|
}
|
|
|
|
if (this.dataset.editing) {
|
|
this._usernameInput.removeAttribute("data-l10n-id");
|
|
this._usernameInput.placeholder = "";
|
|
} else {
|
|
document.l10n.setAttributes(
|
|
this._usernameInput,
|
|
"about-logins-login-item-username"
|
|
);
|
|
}
|
|
this._copyUsernameButton.disabled = !this._login.username;
|
|
document.l10n.setAttributes(
|
|
this._saveChangesButton,
|
|
this.dataset.isNewLogin
|
|
? "login-item-save-new-button"
|
|
: "login-item-save-changes-button"
|
|
);
|
|
this._updatePasswordRevealState();
|
|
}
|
|
|
|
setBreaches(breachesByLoginGUID) {
|
|
this._breachesMap = breachesByLoginGUID;
|
|
this.render();
|
|
}
|
|
|
|
updateBreaches(breachesByLoginGUID) {
|
|
if (!this._breachesMap) {
|
|
this._breachesMap = new Map();
|
|
}
|
|
for (const [guid, breach] of [...breachesByLoginGUID]) {
|
|
this._breachesMap.set(guid, breach);
|
|
}
|
|
this.setBreaches(this._breachesMap);
|
|
}
|
|
|
|
dismissBreachAlert() {
|
|
document.dispatchEvent(
|
|
new CustomEvent("AboutLoginsDismissBreachAlert", {
|
|
bubbles: true,
|
|
detail: this._login,
|
|
})
|
|
);
|
|
this._recordTelemetryEvent({
|
|
object: "existing_login",
|
|
method: "dismiss_breach_alert",
|
|
});
|
|
}
|
|
|
|
showLoginItemError(error) {
|
|
this._error = error;
|
|
this.render();
|
|
}
|
|
|
|
async handleEvent(event) {
|
|
switch (event.type) {
|
|
case "AboutLoginsInitialLoginSelected": {
|
|
this.setLogin(event.detail, { skipFocusChange: true });
|
|
break;
|
|
}
|
|
case "AboutLoginsLoadInitialFavicon": {
|
|
this.render();
|
|
break;
|
|
}
|
|
case "AboutLoginsLoginSelected": {
|
|
this.confirmPendingChangesOnEvent(event, event.detail);
|
|
break;
|
|
}
|
|
case "AboutLoginsShowBlankLogin": {
|
|
this.confirmPendingChangesOnEvent(event, {});
|
|
break;
|
|
}
|
|
case "auxclick": {
|
|
if (event.button == 1) {
|
|
this._handleOriginClick();
|
|
}
|
|
break;
|
|
}
|
|
case "blur": {
|
|
// Add https:// prefix if one was not provided.
|
|
let originValue = this._originInput.value.trim();
|
|
if (!originValue) {
|
|
return;
|
|
}
|
|
if (!originValue.match(/:\/\//)) {
|
|
this._originInput.value = "https://" + originValue;
|
|
}
|
|
break;
|
|
}
|
|
case "click": {
|
|
let classList = event.currentTarget.classList;
|
|
if (classList.contains("reveal-password-checkbox")) {
|
|
if (this._revealCheckbox.checked && !this.dataset.isNewLogin) {
|
|
let masterPasswordAuth = await new Promise(resolve => {
|
|
window.AboutLoginsUtils.promptForMasterPassword(resolve);
|
|
});
|
|
if (!masterPasswordAuth) {
|
|
this._revealCheckbox.checked = false;
|
|
return;
|
|
}
|
|
}
|
|
this._updatePasswordRevealState();
|
|
|
|
let method = this._revealCheckbox.checked ? "show" : "hide";
|
|
this._recordTelemetryEvent({ object: "password", method });
|
|
return;
|
|
}
|
|
|
|
if (classList.contains("cancel-button")) {
|
|
let wasExistingLogin = !!this._login.guid;
|
|
if (wasExistingLogin) {
|
|
if (this.hasPendingChanges()) {
|
|
this.showConfirmationDialog("discard-changes", () => {
|
|
this.setLogin(this._login);
|
|
});
|
|
} else {
|
|
this.setLogin(this._login);
|
|
}
|
|
} else if (!this.hasPendingChanges()) {
|
|
window.dispatchEvent(new CustomEvent("AboutLoginsClearSelection"));
|
|
this._recordTelemetryEvent({
|
|
object: "new_login",
|
|
method: "cancel",
|
|
});
|
|
} else {
|
|
this.showConfirmationDialog("discard-changes", () => {
|
|
window.dispatchEvent(
|
|
new CustomEvent("AboutLoginsClearSelection")
|
|
);
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
if (
|
|
classList.contains("copy-password-button") ||
|
|
classList.contains("copy-username-button")
|
|
) {
|
|
let copyButton = event.currentTarget;
|
|
let otherCopyButton =
|
|
copyButton == this._copyPasswordButton
|
|
? this._copyUsernameButton
|
|
: this._copyPasswordButton;
|
|
if (copyButton.dataset.copyLoginProperty == "password") {
|
|
let masterPasswordAuth = await new Promise(resolve => {
|
|
window.AboutLoginsUtils.promptForMasterPassword(resolve);
|
|
});
|
|
if (!masterPasswordAuth) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
copyButton.disabled = true;
|
|
copyButton.dataset.copied = true;
|
|
let propertyToCopy = this._login[
|
|
copyButton.dataset.copyLoginProperty
|
|
];
|
|
document.dispatchEvent(
|
|
new CustomEvent("AboutLoginsCopyLoginDetail", {
|
|
bubbles: true,
|
|
detail: propertyToCopy,
|
|
})
|
|
);
|
|
otherCopyButton.disabled = false;
|
|
delete otherCopyButton.dataset.copied;
|
|
clearTimeout(this._copyUsernameTimeoutId);
|
|
clearTimeout(this._copyPasswordTimeoutId);
|
|
let timeoutId = setTimeout(() => {
|
|
copyButton.disabled = false;
|
|
delete copyButton.dataset.copied;
|
|
}, LoginItem.COPY_BUTTON_RESET_TIMEOUT);
|
|
if (copyButton.dataset.copyLoginProperty == "password") {
|
|
this._copyPasswordTimeoutId = timeoutId;
|
|
} else {
|
|
this._copyUsernameTimeoutId = timeoutId;
|
|
}
|
|
|
|
this._recordTelemetryEvent({
|
|
object: copyButton.dataset.telemetryObject,
|
|
method: "copy",
|
|
});
|
|
return;
|
|
}
|
|
if (classList.contains("delete-button")) {
|
|
this.showConfirmationDialog("delete", () => {
|
|
document.dispatchEvent(
|
|
new CustomEvent("AboutLoginsDeleteLogin", {
|
|
bubbles: true,
|
|
detail: this._login,
|
|
})
|
|
);
|
|
});
|
|
return;
|
|
}
|
|
if (classList.contains("dismiss-breach-alert")) {
|
|
this.dismissBreachAlert();
|
|
return;
|
|
}
|
|
if (classList.contains("edit-button")) {
|
|
this._toggleEditing();
|
|
this.render();
|
|
|
|
this._recordTelemetryEvent({
|
|
object: "existing_login",
|
|
method: "edit",
|
|
});
|
|
return;
|
|
}
|
|
if (
|
|
event.target.dataset.l10nName == "duplicate-link" &&
|
|
event.currentTarget.dataset.errorGuid
|
|
) {
|
|
let existingDuplicateLogin = {
|
|
guid: event.currentTarget.dataset.errorGuid,
|
|
};
|
|
window.dispatchEvent(
|
|
new CustomEvent("AboutLoginsLoginSelected", {
|
|
detail: existingDuplicateLogin,
|
|
cancelable: true,
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
if (classList.contains("origin-input")) {
|
|
this._handleOriginClick();
|
|
}
|
|
if (classList.contains("breach-alert-link")) {
|
|
this._recordTelemetryEvent({
|
|
object: "existing_login",
|
|
method: "learn_more_breach",
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
case "submit": {
|
|
// Prevent page navigation form submit behavior.
|
|
event.preventDefault();
|
|
if (!this._isFormValid({ reportErrors: true })) {
|
|
return;
|
|
}
|
|
if (!this.hasPendingChanges()) {
|
|
this._toggleEditing(false);
|
|
return;
|
|
}
|
|
let loginUpdates = this._loginFromForm();
|
|
if (this._login.guid) {
|
|
loginUpdates.guid = this._login.guid;
|
|
document.dispatchEvent(
|
|
new CustomEvent("AboutLoginsUpdateLogin", {
|
|
bubbles: true,
|
|
detail: loginUpdates,
|
|
})
|
|
);
|
|
|
|
this._recordTelemetryEvent({
|
|
object: "existing_login",
|
|
method: "save",
|
|
});
|
|
} else {
|
|
document.dispatchEvent(
|
|
new CustomEvent("AboutLoginsCreateLogin", {
|
|
bubbles: true,
|
|
detail: loginUpdates,
|
|
})
|
|
);
|
|
|
|
this._recordTelemetryEvent({ object: "new_login", method: "save" });
|
|
}
|
|
break;
|
|
}
|
|
case "mousedown": {
|
|
// No AutoScroll when middle clicking on origin input.
|
|
if (event.currentTarget == this._originInput && event.button == 1) {
|
|
event.preventDefault();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper to show the "Discard changes" confirmation dialog and delay the
|
|
* received event after confirmation.
|
|
* @param {object} event The event to be delayed.
|
|
* @param {object} login The login to be shown on confirmation.
|
|
*/
|
|
confirmPendingChangesOnEvent(event, login) {
|
|
if (this.hasPendingChanges()) {
|
|
event.preventDefault();
|
|
this.showConfirmationDialog("discard-changes", () => {
|
|
// Clear any pending changes
|
|
this.setLogin(login);
|
|
|
|
window.dispatchEvent(
|
|
new CustomEvent(event.type, {
|
|
detail: login,
|
|
cancelable: false,
|
|
})
|
|
);
|
|
});
|
|
} else {
|
|
this.setLogin(login);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Shows a confirmation dialog.
|
|
* @param {string} type The type of confirmation dialog to display.
|
|
* @param {boolean} onConfirm Optional, the function to execute when the confirm button is clicked.
|
|
*/
|
|
showConfirmationDialog(type, onConfirm = () => {}) {
|
|
const dialog = document.querySelector("confirmation-dialog");
|
|
let options;
|
|
switch (type) {
|
|
case "delete": {
|
|
options = {
|
|
title: "about-logins-confirm-remove-dialog-title",
|
|
message: "confirm-delete-dialog-message",
|
|
confirmButtonLabel:
|
|
"about-logins-confirm-remove-dialog-confirm-button",
|
|
};
|
|
break;
|
|
}
|
|
case "discard-changes": {
|
|
options = {
|
|
title: "confirm-discard-changes-dialog-title",
|
|
message: "confirm-discard-changes-dialog-message",
|
|
confirmButtonLabel: "confirm-discard-changes-dialog-confirm-button",
|
|
};
|
|
break;
|
|
}
|
|
}
|
|
let wasExistingLogin = !!this._login.guid;
|
|
let method = type == "delete" ? "delete" : "cancel";
|
|
let dialogPromise = dialog.show(options);
|
|
dialogPromise.then(
|
|
() => {
|
|
try {
|
|
onConfirm();
|
|
} catch (ex) {}
|
|
this._recordTelemetryEvent({
|
|
object: wasExistingLogin ? "existing_login" : "new_login",
|
|
method,
|
|
});
|
|
},
|
|
() => {}
|
|
);
|
|
return dialogPromise;
|
|
}
|
|
|
|
hasPendingChanges() {
|
|
let valuesChanged = !window.AboutLoginsUtils.doLoginsMatch(
|
|
Object.assign({ username: "", password: "", origin: "" }, this._login),
|
|
this._loginFromForm()
|
|
);
|
|
|
|
return this.dataset.editing && valuesChanged;
|
|
}
|
|
|
|
/**
|
|
* @param {login} login The login that should be displayed. The login object is
|
|
* a plain JS object representation of nsILoginInfo/nsILoginMetaInfo.
|
|
* @param {boolean} skipFocusChange Optional, if present and set to true, the Edit button of the
|
|
* login will not get focus automatically. This is used to prevent
|
|
* stealing focus from the search filter upon page load.
|
|
*/
|
|
setLogin(login, { skipFocusChange } = {}) {
|
|
this._login = login;
|
|
this._error = null;
|
|
|
|
this._form.reset();
|
|
|
|
if (login.guid) {
|
|
delete this.dataset.isNewLogin;
|
|
} else {
|
|
this.dataset.isNewLogin = true;
|
|
}
|
|
document.documentElement.classList.toggle("login-selected", login.guid);
|
|
this._toggleEditing(!login.guid);
|
|
|
|
this._revealCheckbox.checked = false;
|
|
|
|
clearTimeout(this._copyUsernameTimeoutId);
|
|
clearTimeout(this._copyPasswordTimeoutId);
|
|
for (let copyButton of [
|
|
this._copyUsernameButton,
|
|
this._copyPasswordButton,
|
|
]) {
|
|
copyButton.disabled = false;
|
|
delete copyButton.dataset.copied;
|
|
}
|
|
|
|
if (!skipFocusChange) {
|
|
this._editButton.focus();
|
|
}
|
|
this.render();
|
|
}
|
|
|
|
/**
|
|
* Updates the view if the login argument matches the login currently
|
|
* displayed.
|
|
*
|
|
* @param {login} login The login that was added to storage. The login object is
|
|
* a plain JS object representation of nsILoginInfo/nsILoginMetaInfo.
|
|
*/
|
|
loginAdded(login) {
|
|
if (
|
|
this._login.guid ||
|
|
!window.AboutLoginsUtils.doLoginsMatch(login, this._loginFromForm())
|
|
) {
|
|
return;
|
|
}
|
|
|
|
this.setLogin(login);
|
|
this.dispatchEvent(
|
|
new CustomEvent("AboutLoginsLoginSelected", {
|
|
bubbles: true,
|
|
composed: true,
|
|
detail: login,
|
|
})
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Updates the view if the login argument matches the login currently
|
|
* displayed.
|
|
*
|
|
* @param {login} login The login that was modified in storage. The login object is
|
|
* a plain JS object representation of nsILoginInfo/nsILoginMetaInfo.
|
|
*/
|
|
loginModified(login) {
|
|
if (this._login.guid != login.guid) {
|
|
return;
|
|
}
|
|
|
|
// Restore faviconDataURI on modified login
|
|
if (this._login.faviconDataURI && this._login.origin == login.origin) {
|
|
login.faviconDataURI = this._login.faviconDataURI;
|
|
}
|
|
|
|
let valuesChanged =
|
|
this.dataset.editing &&
|
|
!window.AboutLoginsUtils.doLoginsMatch(login, this._loginFromForm());
|
|
if (valuesChanged) {
|
|
this.showConfirmationDialog("discard-changes", () => {
|
|
this.setLogin(login);
|
|
});
|
|
} else {
|
|
this.setLogin(login);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clears the displayed login if the argument matches the currently
|
|
* displayed login.
|
|
*
|
|
* @param {login} login The login that was removed from storage. The login object is
|
|
* a plain JS object representation of nsILoginInfo/nsILoginMetaInfo.
|
|
*/
|
|
loginRemoved(login) {
|
|
if (login.guid != this._login.guid) {
|
|
return;
|
|
}
|
|
|
|
this.setLogin({}, { skipFocusChange: true });
|
|
this._toggleEditing(false);
|
|
}
|
|
|
|
_handleOriginClick() {
|
|
document.dispatchEvent(
|
|
new CustomEvent("AboutLoginsOpenSite", {
|
|
bubbles: true,
|
|
detail: this._login,
|
|
})
|
|
);
|
|
|
|
this._recordTelemetryEvent({
|
|
object: "existing_login",
|
|
method: "open_site",
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Checks that the edit/new-login form has valid values present for their
|
|
* respective required fields.
|
|
*
|
|
* @param {boolean} reportErrors If true, validation errors will be reported
|
|
* to the user.
|
|
*/
|
|
_isFormValid({ reportErrors } = {}) {
|
|
let fields = [this._passwordInput];
|
|
if (this.dataset.isNewLogin) {
|
|
fields.push(this._originInput);
|
|
}
|
|
let valid = true;
|
|
// Check validity on all required fields so each field will get :invalid styling
|
|
// if applicable.
|
|
for (let field of fields) {
|
|
if (reportErrors) {
|
|
valid &= field.reportValidity();
|
|
} else {
|
|
valid &= field.checkValidity();
|
|
}
|
|
}
|
|
return valid;
|
|
}
|
|
|
|
_loginFromForm() {
|
|
return Object.assign({}, this._login, {
|
|
username: this._usernameInput.value.trim(),
|
|
password: this._passwordInput.value,
|
|
origin:
|
|
window.AboutLoginsUtils.getLoginOrigin(this._originInput.value) || "",
|
|
});
|
|
}
|
|
|
|
_recordTelemetryEvent(eventObject) {
|
|
const extra = eventObject.hasOwnProperty("extra") ? eventObject.extra : {};
|
|
if (this._breachesMap && this._breachesMap.has(this._login.guid)) {
|
|
Object.assign(extra, { breached: "true" });
|
|
eventObject.extra = extra;
|
|
}
|
|
recordTelemetryEvent(eventObject);
|
|
}
|
|
|
|
/**
|
|
* Toggles the login-item view from editing to non-editing mode.
|
|
*
|
|
* @param {boolean} force When true puts the form in 'edit' mode, otherwise
|
|
* puts the form in read-only mode.
|
|
*/
|
|
_toggleEditing(force) {
|
|
let shouldEdit = force !== undefined ? force : !this.dataset.editing;
|
|
|
|
if (!shouldEdit) {
|
|
delete this.dataset.isNewLogin;
|
|
}
|
|
|
|
if (shouldEdit) {
|
|
this._passwordInput.style.removeProperty("width");
|
|
} else {
|
|
// Need to set a shorter width than -moz-available so the reveal checkbox
|
|
// will still appear next to the password.
|
|
this._passwordInput.style.width =
|
|
(this._login.password || "").length + "ch";
|
|
}
|
|
|
|
this._deleteButton.disabled = this.dataset.isNewLogin;
|
|
this._editButton.disabled = shouldEdit;
|
|
let inputTabIndex = !shouldEdit ? -1 : 0;
|
|
this._originInput.readOnly = !this.dataset.isNewLogin;
|
|
this._originInput.tabIndex = inputTabIndex;
|
|
this._usernameInput.readOnly = !shouldEdit;
|
|
this._usernameInput.tabIndex = inputTabIndex;
|
|
this._passwordInput.readOnly = !shouldEdit;
|
|
this._passwordInput.tabIndex = inputTabIndex;
|
|
if (shouldEdit) {
|
|
this.dataset.editing = true;
|
|
this._originInput.focus();
|
|
} else {
|
|
delete this.dataset.editing;
|
|
// Only reset the reveal checkbox when exiting 'edit' mode
|
|
this._revealCheckbox.checked = false;
|
|
}
|
|
}
|
|
|
|
_updatePasswordRevealState() {
|
|
if (
|
|
window.AboutLoginsUtils &&
|
|
window.AboutLoginsUtils.passwordRevealVisible === false
|
|
) {
|
|
this._revealCheckbox.hidden = true;
|
|
return;
|
|
}
|
|
|
|
let titleId = this._revealCheckbox.checked
|
|
? "login-item-password-reveal-checkbox-hide"
|
|
: "login-item-password-reveal-checkbox-show";
|
|
document.l10n.setAttributes(this._revealCheckbox, titleId);
|
|
|
|
let { checked } = this._revealCheckbox;
|
|
let inputType = checked ? "text" : "password";
|
|
this._passwordInput.type = inputType;
|
|
}
|
|
}
|
|
customElements.define("login-item", LoginItem);
|