1
0
Fork 0
firefox/browser/components/profiles/content/edit-profile-card.mjs
Daniel Baumann 5e9a113729
Adding upstream version 140.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-25 09:37:52 +02:00

478 lines
12 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/. */
/* eslint-env mozilla/remote-page */
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
import { html, ifDefined } from "chrome://global/content/vendor/lit.all.mjs";
/**
* Like DeferredTask but usable from content.
*/
class Debounce {
timeout = null;
#callback = null;
#timeoutId = null;
constructor(callback, timeout) {
this.#callback = callback;
this.timeout = timeout;
this.#timeoutId = null;
}
#trigger() {
this.#timeoutId = null;
this.#callback();
}
arm() {
this.disarm();
this.#timeoutId = setTimeout(() => this.#trigger(), this.timeout);
}
disarm() {
if (this.isArmed) {
clearTimeout(this.#timeoutId);
this.#timeoutId = null;
}
}
finalize() {
if (this.isArmed) {
this.disarm();
this.#callback();
}
}
get isArmed() {
return this.#timeoutId !== null;
}
}
// eslint-disable-next-line import/no-unassigned-import
import "chrome://global/content/elements/moz-card.mjs";
// eslint-disable-next-line import/no-unassigned-import
import "chrome://global/content/elements/moz-button.mjs";
// eslint-disable-next-line import/no-unassigned-import
import "chrome://global/content/elements/moz-button-group.mjs";
// eslint-disable-next-line import/no-unassigned-import
import "chrome://browser/content/profiles/avatar.mjs";
// eslint-disable-next-line import/no-unassigned-import
import "chrome://browser/content/profiles/profiles-theme-card.mjs";
// eslint-disable-next-line import/no-unassigned-import
import "chrome://browser/content/profiles/profiles-group.mjs";
const SAVE_NAME_TIMEOUT = 2000;
const SAVED_MESSAGE_TIMEOUT = 5000;
/**
* Element used for updating a profile's name, theme, and avatar.
*/
export class EditProfileCard extends MozLitElement {
static properties = {
profile: { type: Object },
profiles: { type: Array },
themes: { type: Array },
};
static queries = {
mozCard: "moz-card",
nameInput: "#profile-name",
errorMessage: "#error-message",
savedMessage: "#saved-message",
deleteButton: "#delete-button",
doneButton: "#done-button",
moreThemesLink: "#more-themes",
headerAvatar: "#header-avatar",
avatarsPicker: "#avatars",
themesPicker: "#themes",
};
updateNameDebouncer = null;
clearSavedMessageTimer = null;
get avatars() {
return this.avatarsPicker.childElements;
}
get themeCards() {
return this.themesPicker.childElements;
}
constructor() {
super();
this.updateNameDebouncer = new Debounce(
() => this.updateName(),
SAVE_NAME_TIMEOUT
);
this.clearSavedMessageTimer = new Debounce(
() => this.hideSavedMessage(),
SAVED_MESSAGE_TIMEOUT
);
}
connectedCallback() {
super.connectedCallback();
window.addEventListener("beforeunload", this);
window.addEventListener("pagehide", this);
this.init().then(() => (this.initialized = true));
}
async init() {
if (this.initialized) {
return;
}
let { currentProfile, profiles, themes, isInAutomation } =
await RPMSendQuery("Profiles:GetEditProfileContent");
if (isInAutomation) {
this.updateNameDebouncer.timeout = 50;
}
this.profile = currentProfile;
this.profiles = profiles;
this.themes = themes;
this.setFavicon();
}
async getUpdateComplete() {
const result = await super.getUpdateComplete();
await Promise.all(
Array.from(this.themeCards).map(card => card.updateComplete)
);
await this.mozCard.updateComplete;
return result;
}
setFavicon() {
let favicon = document.getElementById("favicon");
favicon.href = `chrome://browser/content/profiles/assets/16_${this.profile.avatar}.svg`;
}
getAvatarL10nId(value) {
switch (value) {
case "book":
return "book-avatar";
case "briefcase":
return "briefcase-avatar";
case "flower":
return "flower-avatar";
case "heart":
return "heart-avatar";
case "shopping":
return "shopping-avatar";
case "star":
return "star-avatar";
}
return "";
}
handleEvent(event) {
switch (event.type) {
case "beforeunload": {
let newName = this.nameInput.value.trim();
if (newName === "") {
this.showErrorMessage("edit-profile-page-no-name");
event.preventDefault();
} else {
this.updateNameDebouncer.finalize();
}
break;
}
case "pagehide": {
RPMSendAsyncMessage("Profiles:PageHide");
}
}
}
updated() {
super.updated();
if (!this.profile) {
return;
}
let { themeFg, themeBg } = this.profile;
this.headerAvatar.style.fill = themeBg;
this.headerAvatar.style.stroke = themeFg;
}
updateName() {
this.updateNameDebouncer.disarm();
this.showSavedMessage();
let newName = this.nameInput.value.trim();
if (!newName) {
return;
}
this.profile.name = newName;
RPMSendAsyncMessage("Profiles:UpdateProfileName", this.profile);
}
async updateTheme(newThemeId) {
if (newThemeId === this.profile.themeId) {
return;
}
let theme = await RPMSendQuery("Profiles:UpdateProfileTheme", newThemeId);
this.profile.themeId = theme.themeId;
this.profile.themeFg = theme.themeFg;
this.profile.themeBg = theme.themeBg;
this.requestUpdate();
}
async updateAvatar(newAvatar) {
if (newAvatar === this.profile.avatar) {
return;
}
this.profile.avatar = newAvatar;
RPMSendAsyncMessage("Profiles:UpdateProfileAvatar", this.profile);
this.requestUpdate();
this.setFavicon();
}
isDuplicateName(newName) {
return !!this.profiles.find(
p => p.id !== this.profile.id && p.name === newName
);
}
async handleInputEvent() {
this.hideSavedMessage();
let newName = this.nameInput.value.trim();
if (newName === "") {
this.showErrorMessage("edit-profile-page-no-name");
} else if (this.isDuplicateName(newName)) {
this.showErrorMessage("edit-profile-page-duplicate-name");
} else {
this.hideErrorMessage();
this.updateNameDebouncer.arm();
}
}
showErrorMessage(l10nId) {
this.updateNameDebouncer.disarm();
document.l10n.setAttributes(this.errorMessage, l10nId);
this.errorMessage.parentElement.hidden = false;
this.nameInput.setCustomValidity("invalid");
}
hideErrorMessage() {
this.errorMessage.parentElement.hidden = true;
this.nameInput.setCustomValidity("");
}
showSavedMessage() {
this.savedMessage.parentElement.hidden = false;
this.clearSavedMessageTimer.arm();
}
hideSavedMessage() {
this.savedMessage.parentElement.hidden = true;
this.clearSavedMessageTimer.disarm();
}
headerTemplate() {
return html`<h1
id="profile-header"
data-l10n-id="edit-profile-page-header"
></h1>`;
}
nameInputTemplate() {
return html`<input
type="text"
id="profile-name"
size="64"
aria-errormessage="error-message"
value=${this.profile.name}
@input=${this.handleInputEvent}
/>`;
}
profilesNameTemplate() {
return html`<div id="profile-name-area">
<label
data-l10n-id="edit-profile-page-profile-name-label"
for="profile-name"
></label>
${this.nameInputTemplate()}
<div class="message-parent">
<span class="message" hidden
><img
class="message-icon"
id="error-icon"
src="chrome://global/skin/icons/info.svg"
/>
<span id="error-message" role="alert"></span>
</span>
<span class="message" hidden
><img
class="message-icon"
id="saved-icon"
src="chrome://global/skin/icons/check-filled.svg"
/>
<span
id="saved-message"
data-l10n-id="edit-profile-page-profile-saved"
></span>
</span>
</div>
</div>`;
}
themesTemplate() {
if (!this.themes) {
return null;
}
return html`<profiles-group
id="themes"
value=${this.profile.themeId}
data-l10n-id="edit-profile-page-theme-header-2"
name="theme"
@click=${this.handleThemeClick}
>
${this.themes.map(
t =>
html`<profiles-group-item
l10nId=${ifDefined(t.dataL10nId)}
name=${ifDefined(t.name)}
value=${t.id}
>
<profiles-theme-card
.theme=${t}
value=${t.id}
></profiles-theme-card>
</profiles-group-item>`
)}
</profiles-group>`;
}
handleThemeClick() {
this.updateTheme(this.themesPicker.value);
}
avatarsTemplate() {
let avatars = ["book", "briefcase", "flower", "heart", "shopping", "star"];
return html`<profiles-group
value=${this.profile.avatar}
data-l10n-id="edit-profile-page-avatar-header-2"
name="avatar"
id="avatars"
@click=${this.handleAvatarClick}
>${avatars.map(
avatar =>
html`<profiles-group-item
l10nId=${this.getAvatarL10nId(avatar)}
value=${avatar}
><profiles-avatar value=${avatar}></profiles-avatar
></profiles-group-item>`
)}</profiles-group
>`;
}
handleAvatarClick() {
this.updateAvatar(this.avatarsPicker.value);
}
onDeleteClick() {
window.removeEventListener("beforeunload", this);
RPMSendAsyncMessage("Profiles:OpenDeletePage");
}
onDoneClick() {
let newName = this.nameInput.value.trim();
if (newName === "") {
this.showErrorMessage("edit-profile-page-no-name");
} else if (this.isDuplicateName(newName)) {
this.showErrorMessage("edit-profile-page-duplicate-name");
} else {
this.updateNameDebouncer.finalize();
// Remove the pagehide listener early to prevent double-counting the
// profiles.existing.closed Glean event.
window.removeEventListener("pagehide", this);
RPMSendAsyncMessage("Profiles:CloseProfileTab");
}
}
onMoreThemesClick() {
// Include the starting URI because the page will navigate before the
// event is asynchronously handled by Glean code in the parent actor.
RPMSendAsyncMessage("Profiles:MoreThemes", {
source: window.location.href,
});
}
buttonsTemplate() {
return html`<moz-button
id="delete-button"
data-l10n-id="edit-profile-page-delete-button"
@click=${this.onDeleteClick}
></moz-button>
<moz-button
id="done-button"
data-l10n-id="new-profile-page-done-button"
@click=${this.onDoneClick}
type="primary"
></moz-button>`;
}
render() {
if (!this.profile) {
return null;
}
return html`<link
rel="stylesheet"
href="chrome://browser/content/profiles/edit-profile-card.css"
/>
<link
rel="stylesheet"
href="chrome://global/skin/in-content/common.css"
/>
<moz-card
><div id="edit-profile-card" aria-labelledby="profile-header">
<img
id="header-avatar"
data-l10n-id=${this.profile.avatarL10nId}
src="chrome://browser/content/profiles/assets/80_${this.profile
.avatar}.svg"
/>
<div id="profile-content">
${this.headerTemplate()}${this.profilesNameTemplate()}
${this.themesTemplate()}
<a
id="more-themes"
href="https://addons.mozilla.org/firefox/themes/"
target="_blank"
@click=${this.onMoreThemesClick}
data-l10n-id="edit-profile-page-explore-themes"
></a>
${this.avatarsTemplate()}
<moz-button-group>${this.buttonsTemplate()}</moz-button-group>
</div>
</div></moz-card
>`;
}
}
customElements.define("edit-profile-card", EditProfileCard);