summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/addrbook/content/vcard-edit
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /comm/mail/components/addrbook/content/vcard-edit
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/mail/components/addrbook/content/vcard-edit')
-rw-r--r--comm/mail/components/addrbook/content/vcard-edit/adr.mjs149
-rw-r--r--comm/mail/components/addrbook/content/vcard-edit/custom.mjs60
-rw-r--r--comm/mail/components/addrbook/content/vcard-edit/edit.mjs1094
-rw-r--r--comm/mail/components/addrbook/content/vcard-edit/email.mjs135
-rw-r--r--comm/mail/components/addrbook/content/vcard-edit/fn.mjs71
-rw-r--r--comm/mail/components/addrbook/content/vcard-edit/id-gen.mjs12
-rw-r--r--comm/mail/components/addrbook/content/vcard-edit/impp.mjs97
-rw-r--r--comm/mail/components/addrbook/content/vcard-edit/n.mjs186
-rw-r--r--comm/mail/components/addrbook/content/vcard-edit/nickname.mjs59
-rw-r--r--comm/mail/components/addrbook/content/vcard-edit/note.mjs82
-rw-r--r--comm/mail/components/addrbook/content/vcard-edit/org.mjs197
-rw-r--r--comm/mail/components/addrbook/content/vcard-edit/special-date.mjs269
-rw-r--r--comm/mail/components/addrbook/content/vcard-edit/tel.mjs83
-rw-r--r--comm/mail/components/addrbook/content/vcard-edit/tz.mjs86
-rw-r--r--comm/mail/components/addrbook/content/vcard-edit/url.mjs89
-rw-r--r--comm/mail/components/addrbook/content/vcard-edit/vCardTemplates.inc.xhtml398
16 files changed, 3067 insertions, 0 deletions
diff --git a/comm/mail/components/addrbook/content/vcard-edit/adr.mjs b/comm/mail/components/addrbook/content/vcard-edit/adr.mjs
new file mode 100644
index 0000000000..2f395173f3
--- /dev/null
+++ b/comm/mail/components/addrbook/content/vcard-edit/adr.mjs
@@ -0,0 +1,149 @@
+/* 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 { vCardIdGen } from "./id-gen.mjs";
+
+const lazy = {};
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "VCardPropertyEntry",
+ "resource:///modules/VCardUtils.jsm"
+);
+
+/**
+ * @implements {VCardPropertyEntryView}
+ * @see RFC6350 ADR
+ */
+export class VCardAdrComponent extends HTMLElement {
+ /** @type {VCardPropertyEntry} */
+ vCardPropertyEntry;
+
+ static newVCardPropertyEntry() {
+ return new lazy.VCardPropertyEntry("adr", {}, "text", [
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ ]);
+ }
+
+ connectedCallback() {
+ if (this.hasConnected) {
+ return;
+ }
+ this.hasConnected = true;
+
+ let template = document.getElementById("template-vcard-edit-adr");
+ let clonedTemplate = template.content.cloneNode(true);
+ this.appendChild(clonedTemplate);
+
+ this.streetEl = this.querySelector('textarea[name="street"]');
+ this.assignIds(this.streetEl, this.querySelector('label[for="street"]'));
+ this.streetEl.addEventListener("input", () => {
+ this.resizeStreetEl();
+ });
+
+ this.localityEl = this.querySelector('input[name="locality"]');
+ this.assignIds(
+ this.localityEl,
+ this.querySelector('label[for="locality"]')
+ );
+
+ this.regionEl = this.querySelector('input[name="region"]');
+ this.assignIds(this.regionEl, this.querySelector('label[for="region"]'));
+
+ this.codeEl = this.querySelector('input[name="code"]');
+ this.assignIds(this.regionEl, this.querySelector('label[for="code"]'));
+
+ this.countryEl = this.querySelector('input[name="country"]');
+ this.assignIds(this.countryEl, this.querySelector('label[for="country"]'));
+
+ // Create the adr type selection.
+ this.vCardType = this.querySelector("vcard-type");
+ this.vCardType.createTypeSelection(this.vCardPropertyEntry, {
+ createLabel: true,
+ });
+
+ this.fromVCardPropertyEntryToUI();
+
+ this.querySelector(".remove-property-button").addEventListener(
+ "click",
+ () => {
+ this.dispatchEvent(
+ new CustomEvent("vcard-remove-property", { bubbles: true })
+ );
+ this.remove();
+ }
+ );
+ }
+
+ fromVCardPropertyEntryToUI() {
+ if (Array.isArray(this.vCardPropertyEntry.value[2])) {
+ this.streetEl.value = this.vCardPropertyEntry.value[2].join("\n");
+ } else {
+ this.streetEl.value = this.vCardPropertyEntry.value[2] || "";
+ }
+ // Per RFC 6350, post office box and extended address SHOULD be empty.
+ let pobox = this.vCardPropertyEntry.value[0] || "";
+ let extendedAddr = this.vCardPropertyEntry.value[1] || "";
+ if (extendedAddr) {
+ this.streetEl.value = this.streetEl.value + "\n" + extendedAddr.trim();
+ delete this.vCardPropertyEntry.value[1];
+ }
+ if (pobox) {
+ this.streetEl.value = pobox.trim() + "\n" + this.streetEl.value;
+ delete this.vCardPropertyEntry.value[0];
+ }
+
+ this.resizeStreetEl();
+ this.localityEl.value = this.vCardPropertyEntry.value[3] || "";
+ this.regionEl.value = this.vCardPropertyEntry.value[4] || "";
+ this.codeEl.value = this.vCardPropertyEntry.value[5] || "";
+ this.countryEl.value = this.vCardPropertyEntry.value[6] || "";
+ }
+
+ fromUIToVCardPropertyEntry() {
+ let streetValue = this.streetEl.value || "";
+ streetValue = streetValue.trim();
+ if (streetValue.includes("\n")) {
+ streetValue = streetValue.replaceAll("\r", "");
+ streetValue = streetValue.split("\n");
+ }
+
+ this.vCardPropertyEntry.value = [
+ "",
+ "",
+ streetValue,
+ this.localityEl.value || "",
+ this.regionEl.value || "",
+ this.codeEl.value || "",
+ this.countryEl.value || "",
+ ];
+ }
+
+ valueIsEmpty() {
+ return [
+ this.streetEl,
+ this.localityEl,
+ this.regionEl,
+ this.codeEl,
+ this.countryEl,
+ ].every(e => !e.value);
+ }
+
+ assignIds(inputEl, labelEl) {
+ let labelInputId = vCardIdGen.next().value;
+ inputEl.id = labelInputId;
+ labelEl.htmlFor = labelInputId;
+ }
+
+ resizeStreetEl() {
+ this.streetEl.rows = Math.max(1, this.streetEl.value.split("\n").length);
+ }
+}
+
+customElements.define("vcard-adr", VCardAdrComponent);
diff --git a/comm/mail/components/addrbook/content/vcard-edit/custom.mjs b/comm/mail/components/addrbook/content/vcard-edit/custom.mjs
new file mode 100644
index 0000000000..bcdb1f6531
--- /dev/null
+++ b/comm/mail/components/addrbook/content/vcard-edit/custom.mjs
@@ -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/. */
+
+import { vCardIdGen } from "./id-gen.mjs";
+
+export class VCardCustomComponent extends HTMLElement {
+ /** @type {VCardPropertyEntry[]} */
+ vCardPropertyEntries = null;
+ /** @type {HTMLInputElement[]} */
+ inputEls = null;
+
+ connectedCallback() {
+ if (this.hasConnected) {
+ return;
+ }
+ this.hasConnected = true;
+
+ let template = document.getElementById("template-vcard-edit-custom");
+ let clonedTemplate = template.content.cloneNode(true);
+ this.appendChild(clonedTemplate);
+
+ this.inputEls = this.querySelectorAll("input");
+ let labelEls = this.querySelectorAll("label");
+ for (let i = 0; i < 4; i++) {
+ let inputId = vCardIdGen.next().value;
+ document.l10n.setAttributes(
+ labelEls[i],
+ `about-addressbook-entry-name-custom${i + 1}`
+ );
+ labelEls[i].htmlFor = inputId;
+ this.inputEls[i].id = inputId;
+ }
+ this.fromVCardPropertyEntryToUI();
+ this.querySelector(".remove-property-button").addEventListener(
+ "click",
+ () => {
+ document.getElementById("vcard-add-custom").hidden = false;
+ this.dispatchEvent(
+ new CustomEvent("vcard-remove-property", { bubbles: true })
+ );
+ this.remove();
+ }
+ );
+ }
+
+ fromVCardPropertyEntryToUI() {
+ for (let i = 0; i < 4; i++) {
+ this.inputEls[i].value = this.vCardPropertyEntries[i].value;
+ }
+ }
+
+ fromUIToVCardPropertyEntry() {
+ for (let i = 0; i < 4; i++) {
+ this.vCardPropertyEntries[i].value = this.inputEls[i].value;
+ }
+ }
+}
+
+customElements.define("vcard-custom", VCardCustomComponent);
diff --git a/comm/mail/components/addrbook/content/vcard-edit/edit.mjs b/comm/mail/components/addrbook/content/vcard-edit/edit.mjs
new file mode 100644
index 0000000000..90463e33bb
--- /dev/null
+++ b/comm/mail/components/addrbook/content/vcard-edit/edit.mjs
@@ -0,0 +1,1094 @@
+/* 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 { vCardIdGen } from "./id-gen.mjs";
+import { VCardAdrComponent } from "./adr.mjs";
+import { VCardCustomComponent } from "./custom.mjs";
+import { VCardEmailComponent } from "./email.mjs";
+import { VCardIMPPComponent } from "./impp.mjs";
+import { VCardNComponent } from "./n.mjs";
+import { VCardFNComponent } from "./fn.mjs";
+import { VCardNickNameComponent } from "./nickname.mjs";
+import { VCardNoteComponent } from "./note.mjs";
+import {
+ VCardOrgComponent,
+ VCardRoleComponent,
+ VCardTitleComponent,
+} from "./org.mjs";
+import { VCardSpecialDateComponent } from "./special-date.mjs";
+import { VCardTelComponent } from "./tel.mjs";
+import { VCardTZComponent } from "./tz.mjs";
+import { VCardURLComponent } from "./url.mjs";
+
+const lazy = {};
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "VCardProperties",
+ "resource:///modules/VCardUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "VCardPropertyEntry",
+ "resource:///modules/VCardUtils.jsm"
+);
+
+class VCardEdit extends HTMLElement {
+ constructor() {
+ super();
+
+ this.contactNameHeading = document.getElementById("editContactHeadingName");
+ this.contactNickNameHeading = document.getElementById(
+ "editContactHeadingNickName"
+ );
+ this.contactEmailHeading = document.getElementById(
+ "editContactHeadingEmail"
+ );
+ }
+
+ connectedCallback() {
+ if (this.isConnected) {
+ this.updateView();
+
+ this.addEventListener("vcard-remove-property", e => {
+ if (e.target.vCardPropertyEntries) {
+ for (let entry of e.target.vCardPropertyEntries) {
+ this.vCardProperties.removeEntry(entry);
+ }
+ } else {
+ this.vCardProperties.removeEntry(e.target.vCardPropertyEntry);
+ }
+
+ // Move the focus to the first available valid element of the fieldset.
+ let sibling =
+ e.target.nextElementSibling || e.target.previousElementSibling;
+ // If we got a button, focus it since it's the "add row" button.
+ if (sibling?.type == "button") {
+ sibling.focus();
+ return;
+ }
+
+ // Otherwise we have a row field, so try to find a focusable element.
+ if (sibling && this.moveFocusIntoElement(sibling)) {
+ return;
+ }
+
+ // If we reach this point, the markup was unpredictable and we should
+ // move the focus to a valid element to avoid focus lost.
+ e.target
+ .closest("fieldset")
+ .querySelector(".add-property-button")
+ .focus();
+ });
+ }
+ }
+
+ disconnectedCallback() {
+ this.replaceChildren();
+ }
+
+ get vCardString() {
+ return this._vCardProperties.toVCard();
+ }
+
+ set vCardString(value) {
+ if (value) {
+ try {
+ this.vCardProperties = lazy.VCardProperties.fromVCard(value);
+ return;
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+ this.vCardProperties = new lazy.VCardProperties("4.0");
+ }
+
+ get vCardProperties() {
+ return this._vCardProperties;
+ }
+
+ set vCardProperties(value) {
+ this._vCardProperties = value;
+ // If no n property is present set one.
+ if (!this._vCardProperties.getFirstEntry("n")) {
+ this._vCardProperties.addEntry(VCardNComponent.newVCardPropertyEntry());
+ }
+ // If no fn property is present set one.
+ if (!this._vCardProperties.getFirstEntry("fn")) {
+ this._vCardProperties.addEntry(VCardFNComponent.newVCardPropertyEntry());
+ }
+ // If no nickname property is present set one.
+ if (!this._vCardProperties.getFirstEntry("nickname")) {
+ this._vCardProperties.addEntry(
+ VCardNickNameComponent.newVCardPropertyEntry()
+ );
+ }
+ // If no email property is present set one.
+ if (!this._vCardProperties.getFirstEntry("email")) {
+ let emailEntry = VCardEmailComponent.newVCardPropertyEntry();
+ emailEntry.params.pref = "1"; // Set as default email.
+ this._vCardProperties.addEntry(emailEntry);
+ }
+ // If one of the organizational properties is present,
+ // make sure they all are.
+ let title = this._vCardProperties.getFirstEntry("title");
+ let role = this._vCardProperties.getFirstEntry("role");
+ let org = this._vCardProperties.getFirstEntry("org");
+ if (title || role || org) {
+ if (!title) {
+ this._vCardProperties.addEntry(
+ VCardTitleComponent.newVCardPropertyEntry()
+ );
+ }
+ if (!role) {
+ this._vCardProperties.addEntry(
+ VCardRoleComponent.newVCardPropertyEntry()
+ );
+ }
+ if (!org) {
+ this._vCardProperties.addEntry(
+ VCardOrgComponent.newVCardPropertyEntry()
+ );
+ }
+ }
+
+ for (let i = 1; i <= 4; i++) {
+ if (!this._vCardProperties.getFirstEntry(`x-custom${i}`)) {
+ this._vCardProperties.addEntry(
+ new lazy.VCardPropertyEntry(`x-custom${i}`, {}, "text", "")
+ );
+ }
+ }
+
+ this.updateView();
+ }
+
+ updateView() {
+ // Create new DOM and replacing other vCardProperties.
+ let template = document.getElementById("template-addr-book-edit");
+ let clonedTemplate = template.content.cloneNode(true);
+ // Making the next two calls in one go causes a console error to be logged.
+ this.replaceChildren();
+ this.append(clonedTemplate);
+
+ if (!this.vCardProperties) {
+ return;
+ }
+
+ this.addFieldsetActions();
+
+ // Insert the vCard property entries.
+ for (let vCardPropertyEntry of this.vCardProperties.entries) {
+ this.insertVCardElement(vCardPropertyEntry, false);
+ }
+
+ let customProperties = ["x-custom1", "x-custom2", "x-custom3", "x-custom4"];
+ if (customProperties.some(key => this.vCardProperties.getFirstValue(key))) {
+ // If one of these properties has a value, display all of them.
+ let customFieldset = this.querySelector("#addr-book-edit-custom");
+ let customEl =
+ customFieldset.querySelector("vcard-custom") ||
+ new VCardCustomComponent();
+ customEl.vCardPropertyEntries = customProperties.map(key =>
+ this._vCardProperties.getFirstEntry(key)
+ );
+ let addCustom = document.getElementById("vcard-add-custom");
+ customFieldset.insertBefore(customEl, addCustom);
+ addCustom.hidden = true;
+ }
+
+ let nameEl = this.querySelector("vcard-n");
+ this.firstName = nameEl.firstNameEl.querySelector("input");
+ this.lastName = nameEl.lastNameEl.querySelector("input");
+ this.prefixName = nameEl.prefixEl.querySelector("input");
+ this.middleName = nameEl.middleNameEl.querySelector("input");
+ this.suffixName = nameEl.suffixEl.querySelector("input");
+ this.displayName = this.querySelector("vcard-fn").displayEl;
+
+ [
+ this.firstName,
+ this.lastName,
+ this.prefixName,
+ this.middleName,
+ this.suffixName,
+ this.displayName,
+ ].forEach(element => {
+ element.addEventListener("input", event =>
+ this.generateContactName(event)
+ );
+ });
+
+ // Only set the strings and define this selector if we're inside the
+ // address book edit panel.
+ if (document.getElementById("detailsPane")) {
+ this.preferDisplayName = this.querySelector("vcard-fn").preferDisplayEl;
+ document.l10n.setAttributes(
+ this.preferDisplayName.closest(".vcard-checkbox").querySelector("span"),
+ "about-addressbook-prefer-display-name"
+ );
+ }
+
+ this.nickName = this.querySelector("vcard-nickname").nickNameEl;
+ this.nickName.addEventListener("input", () => this.updateNickName());
+
+ if (this.vCardProperties) {
+ this.toggleDefaultEmailView();
+ this.checkForBdayOccurrences();
+ }
+
+ this.updateNickName();
+ this.updateEmailHeading();
+ this.generateContactName();
+ }
+
+ /**
+ * Update the contact name to reflect the users' choice.
+ *
+ * @param {?Event} event - The DOM event if we have one.
+ */
+ async generateContactName(event = null) {
+ // Don't generate any preview if the contact name element is not available,
+ // which it might happen since this component is used in other areas outside
+ // the address book UI.
+ if (!this.contactNameHeading) {
+ return;
+ }
+
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/addressbook/addressBook.properties"
+ );
+ let result = "";
+ let pref = Services.prefs.getIntPref("mail.addr_book.lastnamefirst");
+ switch (pref) {
+ case Ci.nsIAbCard.GENERATE_DISPLAY_NAME:
+ result = this.buildDefaultName();
+ break;
+
+ case Ci.nsIAbCard.GENERATE_LAST_FIRST_ORDER:
+ if (this.lastName.value) {
+ result = bundle.formatStringFromName("lastFirstFormat", [
+ this.lastName.value,
+ [
+ this.prefixName.value,
+ this.firstName.value,
+ this.middleName.value,
+ this.suffixName.value,
+ ]
+ .filter(Boolean)
+ .join(" "),
+ ]);
+ } else {
+ // Get the generic name if we don't have a last name.
+ result = this.buildDefaultName();
+ }
+ break;
+
+ default:
+ result = bundle.formatStringFromName("firstLastFormat", [
+ [this.prefixName.value, this.firstName.value, this.middleName.value]
+ .filter(Boolean)
+ .join(" "),
+ [this.lastName.value, this.suffixName.value]
+ .filter(Boolean)
+ .join(" "),
+ ]);
+ break;
+ }
+
+ if (result == "" || result == ", ") {
+ // We don't have anything to show as a contact name, so let's find the
+ // default email and show that, if we have it, otherwise pass an empty
+ // string to remove any leftover data.
+ let email = this.getDefaultEmail();
+ result = email ? email.split("@", 1)[0] : "";
+ }
+
+ this.contactNameHeading.textContent = result;
+ this.fillDisplayName(event);
+ }
+
+ /**
+ * Returns the name to show for this contact if the display name is available
+ * or it generates one from the available N data.
+ *
+ * @returns {string} - The name to show for this contact.
+ */
+ buildDefaultName() {
+ return this.displayName.isDirty
+ ? this.displayName.value
+ : [
+ this.prefixName.value,
+ this.firstName.value,
+ this.middleName.value,
+ this.lastName.value,
+ this.suffixName.value,
+ ]
+ .filter(Boolean)
+ .join(" ");
+ }
+
+ /**
+ * Update the nickname value of the contact header.
+ */
+ updateNickName() {
+ // Don't generate any preview if the contact nickname element is not
+ // available, which it might happen since this component is used in other
+ // areas outside the address book UI.
+ if (!this.contactNickNameHeading) {
+ return;
+ }
+
+ let value = this.nickName.value.trim();
+ this.contactNickNameHeading.hidden = !value;
+ this.contactNickNameHeading.textContent = value;
+ }
+
+ /**
+ * Update the email value of the contact header.
+ *
+ * @param {?string} email - The email value the user is currently typing.
+ */
+ updateEmailHeading(email = null) {
+ // Don't generate any preview if the contact nickname email is not
+ // available, which it might happen since this component is used in other
+ // areas outside the address book UI.
+ if (!this.contactEmailHeading) {
+ return;
+ }
+
+ // If no email string was passed, it means this method was called when the
+ // view or edit pane refreshes, therefore we need to fetch the correct
+ // default email address.
+ let value = email ?? this.getDefaultEmail();
+ this.contactEmailHeading.hidden = !value;
+ this.contactEmailHeading.textContent = value;
+ }
+
+ /**
+ * Find the default email used for this contact.
+ *
+ * @returns {VCardEmailComponent}
+ */
+ getDefaultEmail() {
+ let emails = document.getElementById("vcard-email").children;
+ if (emails.length == 1) {
+ return emails[0].emailEl.value;
+ }
+
+ let defaultEmail = [...emails].find(
+ el => el.vCardPropertyEntry.params.pref === "1"
+ );
+
+ // If no email is marked as preferred, use the first one.
+ if (!defaultEmail) {
+ defaultEmail = emails[0];
+ }
+
+ return defaultEmail.emailEl.value;
+ }
+
+ /**
+ * Auto fill the display name only if the pref is set, the user is not
+ * editing the display name field, and the field was never edited.
+ * The intention is to prefill while entering a new contact. Don't fill
+ * if we don't have a proper default name to show, but only a placeholder.
+ *
+ * @param {?Event} event - The DOM event if we have one.
+ */
+ fillDisplayName(event = null) {
+ if (
+ Services.prefs.getBoolPref("mail.addr_book.displayName.autoGeneration") &&
+ event?.originalTarget.id != "vCardDisplayName" &&
+ !this.displayName.isDirty &&
+ this.buildDefaultName()
+ ) {
+ this.displayName.value = this.contactNameHeading.textContent;
+ }
+ }
+
+ /**
+ * Inserts a custom element for a {VCardPropertyEntry}
+ *
+ * - Assigns rich data (not bind to a html attribute) and therefore
+ * the reference.
+ * - Inserts the element in the form at the correct position.
+ *
+ * @param {VCardPropertyEntry} entry
+ * @param {boolean} addEntry Adds the entry to the vCardProperties.
+ * @returns {VCardPropertyEntryView | undefined}
+ */
+ insertVCardElement(entry, addEntry) {
+ // Add the entry to the vCardProperty data.
+ if (addEntry) {
+ this.vCardProperties.addEntry(entry);
+ }
+
+ let fieldset;
+ let addButton;
+ switch (entry.name) {
+ case "n":
+ let n = new VCardNComponent();
+ n.vCardPropertyEntry = entry;
+ fieldset = document.getElementById("addr-book-edit-n");
+ let displayNicknameContainer = this.querySelector(
+ "#addr-book-edit-n .addr-book-edit-display-nickname"
+ );
+ fieldset.insertBefore(n, displayNicknameContainer);
+ return n;
+ case "fn":
+ let fn = new VCardFNComponent();
+ fn.vCardPropertyEntry = entry;
+ fieldset = this.querySelector(
+ "#addr-book-edit-n .addr-book-edit-display-nickname"
+ );
+ fieldset.insertBefore(fn, fieldset.firstElementChild);
+ return fn;
+ case "nickname":
+ let nickname = new VCardNickNameComponent();
+ nickname.vCardPropertyEntry = entry;
+ fieldset = this.querySelector(
+ "#addr-book-edit-n .addr-book-edit-display-nickname"
+ );
+ fieldset.insertBefore(
+ nickname,
+ fieldset.firstElementChild?.nextElementSibling
+ );
+ return nickname;
+ case "email":
+ let email = document.createElement("tr", { is: "vcard-email" });
+ email.vCardPropertyEntry = entry;
+ document.getElementById("vcard-email").appendChild(email);
+ return email;
+ case "url":
+ let url = new VCardURLComponent();
+ url.vCardPropertyEntry = entry;
+ fieldset = this.querySelector("#addr-book-edit-url");
+ addButton = document.getElementById("vcard-add-url");
+ fieldset.insertBefore(url, addButton);
+ return url;
+ case "tel":
+ let tel = new VCardTelComponent();
+ tel.vCardPropertyEntry = entry;
+ fieldset = this.querySelector("#addr-book-edit-tel");
+ addButton = document.getElementById("vcard-add-tel");
+ fieldset.insertBefore(tel, addButton);
+ return tel;
+ case "tz":
+ let tz = new VCardTZComponent();
+ tz.vCardPropertyEntry = entry;
+ fieldset = this.querySelector("#addr-book-edit-tz");
+ addButton = document.getElementById("vcard-add-tz");
+ fieldset.insertBefore(tz, addButton);
+ addButton.hidden = true;
+ return tz;
+ case "impp":
+ let impp = new VCardIMPPComponent();
+ impp.vCardPropertyEntry = entry;
+ fieldset = this.querySelector("#addr-book-edit-impp");
+ addButton = document.getElementById("vcard-add-impp");
+ fieldset.insertBefore(impp, addButton);
+ return impp;
+ case "anniversary":
+ let anniversary = new VCardSpecialDateComponent();
+ anniversary.vCardPropertyEntry = entry;
+ fieldset = this.querySelector("#addr-book-edit-bday-anniversary");
+ addButton = document.getElementById("vcard-add-bday-anniversary");
+ fieldset.insertBefore(anniversary, addButton);
+ return anniversary;
+ case "bday":
+ let bday = new VCardSpecialDateComponent();
+ bday.vCardPropertyEntry = entry;
+ fieldset = this.querySelector("#addr-book-edit-bday-anniversary");
+ addButton = document.getElementById("vcard-add-bday-anniversary");
+ fieldset.insertBefore(bday, addButton);
+ return bday;
+ case "adr":
+ let address = new VCardAdrComponent();
+ address.vCardPropertyEntry = entry;
+ fieldset = this.querySelector("#addr-book-edit-address");
+ addButton = document.getElementById("vcard-add-adr");
+ fieldset.insertBefore(address, addButton);
+ return address;
+ case "note":
+ let note = new VCardNoteComponent();
+ note.vCardPropertyEntry = entry;
+ fieldset = this.querySelector("#addr-book-edit-note");
+ addButton = document.getElementById("vcard-add-note");
+ fieldset.insertBefore(note, addButton);
+ // Only one note is allowed via UI.
+ addButton.hidden = true;
+ return note;
+ case "title":
+ let title = new VCardTitleComponent();
+ title.vCardPropertyEntry = entry;
+ fieldset = this.querySelector("#addr-book-edit-org");
+ addButton = document.getElementById("vcard-add-org");
+ fieldset.insertBefore(
+ title,
+ fieldset.querySelector("vcard-role, vcard-org, #vcard-add-org")
+ );
+ this.querySelector(
+ "#addr-book-edit-org .remove-property-button"
+ ).hidden = false;
+ // Only one title is allowed via UI.
+ addButton.hidden = true;
+ return title;
+ case "role":
+ let role = new VCardRoleComponent();
+ role.vCardPropertyEntry = entry;
+ fieldset = this.querySelector("#addr-book-edit-org");
+ addButton = document.getElementById("vcard-add-org");
+ fieldset.insertBefore(
+ role,
+ fieldset.querySelector("vcard-org, #vcard-add-org")
+ );
+ this.querySelector(
+ "#addr-book-edit-org .remove-property-button"
+ ).hidden = false;
+ // Only one role is allowed via UI.
+ addButton.hidden = true;
+ return role;
+ case "org":
+ let org = new VCardOrgComponent();
+ org.vCardPropertyEntry = entry;
+ fieldset = this.querySelector("#addr-book-edit-org");
+ addButton = document.getElementById("vcard-add-org");
+ fieldset.insertBefore(org, addButton);
+ this.querySelector(
+ "#addr-book-edit-org .remove-property-button"
+ ).hidden = false;
+ // Only one org is allowed via UI.
+ addButton.hidden = true;
+ return org;
+ default:
+ return undefined;
+ }
+ }
+
+ /**
+ * Creates a VCardPropertyEntry with a matching
+ * name to the vCard spec.
+ *
+ * @param {string} entryName - A name which should be a vCard spec property.
+ * @returns {VCardPropertyEntry | undefined}
+ */
+ static createVCardProperty(entryName) {
+ switch (entryName) {
+ case "n":
+ return VCardNComponent.newVCardPropertyEntry();
+ case "fn":
+ return VCardFNComponent.newVCardPropertyEntry();
+ case "nickname":
+ return VCardNickNameComponent.newVCardPropertyEntry();
+ case "email":
+ return VCardEmailComponent.newVCardPropertyEntry();
+ case "url":
+ return VCardURLComponent.newVCardPropertyEntry();
+ case "tel":
+ return VCardTelComponent.newVCardPropertyEntry();
+ case "tz":
+ return VCardTZComponent.newVCardPropertyEntry();
+ case "impp":
+ return VCardIMPPComponent.newVCardPropertyEntry();
+ case "bday":
+ return VCardSpecialDateComponent.newBdayVCardPropertyEntry();
+ case "anniversary":
+ return VCardSpecialDateComponent.newAnniversaryVCardPropertyEntry();
+ case "adr":
+ return VCardAdrComponent.newVCardPropertyEntry();
+ case "note":
+ return VCardNoteComponent.newVCardPropertyEntry();
+ case "title":
+ return VCardTitleComponent.newVCardPropertyEntry();
+ case "role":
+ return VCardRoleComponent.newVCardPropertyEntry();
+ case "org":
+ return VCardOrgComponent.newVCardPropertyEntry();
+ default:
+ return undefined;
+ }
+ }
+
+ /**
+ * Mutates the referenced vCardPropertyEntry(s).
+ * If the value of a VCardPropertyEntry is empty, the entry gets
+ * removed from the vCardProperty.
+ */
+ saveVCard() {
+ for (let node of [
+ ...this.querySelectorAll("vcard-adr"),
+ ...this.querySelectorAll("vcard-custom"),
+ ...document.getElementById("vcard-email").children,
+ ...this.querySelectorAll("vcard-fn"),
+ ...this.querySelectorAll("vcard-impp"),
+ ...this.querySelectorAll("vcard-n"),
+ ...this.querySelectorAll("vcard-nickname"),
+ ...this.querySelectorAll("vcard-note"),
+ ...this.querySelectorAll("vcard-org"),
+ ...this.querySelectorAll("vcard-role"),
+ ...this.querySelectorAll("vcard-title"),
+ ...this.querySelectorAll("vcard-special-date"),
+ ...this.querySelectorAll("vcard-tel"),
+ ...this.querySelectorAll("vcard-tz"),
+ ...this.querySelectorAll("vcard-url"),
+ ]) {
+ if (typeof node.fromUIToVCardPropertyEntry === "function") {
+ node.fromUIToVCardPropertyEntry();
+ }
+
+ // Filter out empty fields.
+ if (typeof node.valueIsEmpty === "function" && node.valueIsEmpty()) {
+ this.vCardProperties.removeEntry(node.vCardPropertyEntry);
+ }
+ }
+
+ // If no email has a pref value of 1, set it to the first email.
+ let emailEntries = this.vCardProperties.getAllEntries("email");
+ if (
+ emailEntries.length >= 1 &&
+ emailEntries.every(entry => entry.params.pref !== "1")
+ ) {
+ emailEntries[0].params.pref = "1";
+ }
+
+ for (let i = 1; i <= 4; i++) {
+ let entry = this._vCardProperties.getFirstEntry(`x-custom${i}`);
+ if (entry && !entry.value) {
+ this._vCardProperties.removeEntry(entry);
+ }
+ }
+ }
+
+ /**
+ * Move focus into the form.
+ */
+ setFocus() {
+ this.querySelector("vcard-n input:not([hidden])").focus();
+ }
+
+ /**
+ * Move focus to the first visible form element below the given element.
+ *
+ * @param {Element} element - The element to move focus into.
+ * @returns {boolean} - If the focus was moved into the element.
+ */
+ moveFocusIntoElement(element) {
+ for (let child of element.querySelectorAll(
+ "select,input,textarea,button"
+ )) {
+ // Make sure it is visible.
+ if (child.clientWidth != 0 && child.clientHeight != 0) {
+ child.focus();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Add buttons and further actions of the groupings for vCard property
+ * entries.
+ */
+ addFieldsetActions() {
+ // Add email button.
+ let addEmail = document.getElementById("vcard-add-email");
+ this.registerAddButton(addEmail, "email", () => {
+ this.toggleDefaultEmailView();
+ });
+
+ // Add listener to update the email written in the contact header.
+ this.addEventListener("vcard-email-default-changed", event => {
+ this.updateEmailHeading(
+ event.target.querySelector('input[type="email"]').value
+ );
+ });
+
+ // Add listener to be sure that only one checkbox from the emails is ticked.
+ this.addEventListener("vcard-email-default-checkbox", event => {
+ // Show the newly selected default email in the contact header.
+ this.updateEmailHeading(
+ event.target.querySelector('input[type="email"]').value
+ );
+ for (let vCardEmailComponent of document.getElementById("vcard-email")
+ .children) {
+ if (event.target !== vCardEmailComponent) {
+ vCardEmailComponent.checkboxEl.checked = false;
+ }
+ }
+ });
+
+ // Handling the VCardPropertyEntry change with the select.
+ let specialDatesFieldset = document.getElementById(
+ "addr-book-edit-bday-anniversary"
+ );
+ specialDatesFieldset.addEventListener(
+ "vcard-bday-anniversary-change",
+ event => {
+ let newVCardPropertyEntry = new lazy.VCardPropertyEntry(
+ event.detail.name,
+ event.target.vCardPropertyEntry.params,
+ event.target.vCardPropertyEntry.type,
+ event.target.vCardPropertyEntry.value
+ );
+ this.vCardProperties.removeEntry(event.target.vCardPropertyEntry);
+ event.target.vCardPropertyEntry = newVCardPropertyEntry;
+ this.vCardProperties.addEntry(newVCardPropertyEntry);
+ this.checkForBdayOccurrences();
+ }
+ );
+
+ // Add special date button.
+ let addSpecialDate = document.getElementById("vcard-add-bday-anniversary");
+ addSpecialDate.addEventListener("click", e => {
+ let newVCardProperty;
+ if (!this.vCardProperties.getFirstEntry("bday")) {
+ newVCardProperty = VCardEdit.createVCardProperty("bday");
+ } else {
+ newVCardProperty = VCardEdit.createVCardProperty("anniversary");
+ }
+ let el = this.insertVCardElement(newVCardProperty, true);
+ this.checkForBdayOccurrences();
+ this.moveFocusIntoElement(el);
+ });
+
+ // Organizational Properties.
+ let addOrg = document.getElementById("vcard-add-org");
+ addOrg.addEventListener("click", event => {
+ let title = VCardEdit.createVCardProperty("title");
+ let role = VCardEdit.createVCardProperty("role");
+ let org = VCardEdit.createVCardProperty("org");
+
+ let titleEl = this.insertVCardElement(title, true);
+ this.insertVCardElement(role, true);
+ this.insertVCardElement(org, true);
+
+ this.moveFocusIntoElement(titleEl);
+ addOrg.hidden = true;
+ });
+
+ let addAddress = document.getElementById("vcard-add-adr");
+ this.registerAddButton(addAddress, "adr");
+
+ let addURL = document.getElementById("vcard-add-url");
+ this.registerAddButton(addURL, "url");
+
+ let addTel = document.getElementById("vcard-add-tel");
+ this.registerAddButton(addTel, "tel");
+
+ let addTZ = document.getElementById("vcard-add-tz");
+ this.registerAddButton(addTZ, "tz", () => {
+ addTZ.hidden = true;
+ });
+
+ let addIMPP = document.getElementById("vcard-add-impp");
+ this.registerAddButton(addIMPP, "impp");
+
+ let addNote = document.getElementById("vcard-add-note");
+ this.registerAddButton(addNote, "note", () => {
+ addNote.hidden = true;
+ });
+
+ let addCustom = document.getElementById("vcard-add-custom");
+ addCustom.addEventListener("click", event => {
+ let el = new VCardCustomComponent();
+
+ // When the custom properties are deleted and added again ensure that
+ // the properties are set.
+ for (let i = 1; i <= 4; i++) {
+ if (!this._vCardProperties.getFirstEntry(`x-custom${i}`)) {
+ this._vCardProperties.addEntry(
+ new lazy.VCardPropertyEntry(`x-custom${i}`, {}, "text", "")
+ );
+ }
+ }
+
+ el.vCardPropertyEntries = [
+ this._vCardProperties.getFirstEntry("x-custom1"),
+ this._vCardProperties.getFirstEntry("x-custom2"),
+ this._vCardProperties.getFirstEntry("x-custom3"),
+ this._vCardProperties.getFirstEntry("x-custom4"),
+ ];
+ addCustom.parentNode.insertBefore(el, addCustom);
+
+ this.moveFocusIntoElement(el);
+ addCustom.hidden = true;
+ });
+
+ // Delete button for Organization Properties. This property has multiple
+ // fields, so we should dispatch the remove event only once after everything
+ // has been removed.
+ this.querySelector(
+ "#addr-book-edit-org .remove-property-button"
+ ).addEventListener("click", event => {
+ this.querySelector("vcard-title").remove();
+ this.querySelector("vcard-role").remove();
+ let org = this.querySelector("vcard-org");
+ // Reveal the "Add" button so we can focus it.
+ document.getElementById("vcard-add-org").hidden = false;
+ // Dispatch the event before removing the element so we can handle focus.
+ org.dispatchEvent(
+ new CustomEvent("vcard-remove-property", { bubbles: true })
+ );
+ org.remove();
+ event.target.hidden = true;
+ });
+ }
+
+ /**
+ * Registers a click event for addButton which creates a new vCardProperty
+ * and inserts it.
+ *
+ * @param {HTMLButtonElement} addButton
+ * @param {string} VCardPropertyName RFC6350 vCard property name.
+ * @param {(vCardElement) => {}} callback For further refinement.
+ * Like different focus instead of an input field.
+ */
+ registerAddButton(addButton, VCardPropertyName, callback) {
+ addButton.addEventListener("click", event => {
+ let newVCardProperty = VCardEdit.createVCardProperty(VCardPropertyName);
+ let el = this.insertVCardElement(newVCardProperty, true);
+
+ this.moveFocusIntoElement(el);
+ if (callback) {
+ callback(el);
+ }
+ });
+ }
+
+ /**
+ * If one BDAY vCardPropertyEntry is present disable
+ * the option to change an Anniversary to a BDAY.
+ *
+ * @see VCardSpecialDateComponent
+ */
+ checkForBdayOccurrences() {
+ let bdayOccurrence = this.vCardProperties.getFirstEntry("bday");
+ this.querySelectorAll("vcard-special-date").forEach(specialDate => {
+ specialDate.birthdayAvailability({ hasBday: !!bdayOccurrence });
+ });
+ }
+
+ /**
+ * Hide the default checkbox if we only have one email field.
+ */
+ toggleDefaultEmailView() {
+ let hideDefault =
+ document.getElementById("vcard-email").children.length <= 1;
+ let defaultColumn = this.querySelector(".default-column");
+ if (defaultColumn) {
+ defaultColumn.hidden = hideDefault;
+ }
+ document.getElementById("addr-book-edit-email-default").hidden =
+ hideDefault;
+
+ // Add class to position legend absolute.
+ document
+ .getElementById("addr-book-edit-email")
+ .classList.toggle("default-table-header", !hideDefault);
+ }
+
+ /**
+ * Validate the form with the minimum required data to save or update a
+ * contact. We can't use the built-in checkValidity() since our fields
+ * are not handled properly by the form element.
+ *
+ * @returns {boolean} - If the form is valid or not.
+ */
+ checkMinimumRequirements() {
+ let hasEmail = [...document.getElementById("vcard-email").children].find(
+ s => {
+ let field = s.querySelector(`input[type="email"]`);
+ return field.value.trim() && field.checkValidity();
+ }
+ );
+ let hasOrg = [...this.querySelectorAll("vcard-org")].find(n =>
+ n.orgEl.value.trim()
+ );
+
+ return (
+ this.firstName.value.trim() ||
+ this.lastName.value.trim() ||
+ this.displayName.value.trim() ||
+ hasEmail ||
+ hasOrg
+ );
+ }
+
+ /**
+ * Validate the special date fields making sure that we have a valid
+ * DATE-AND-OR-TIME. See date, date-noreduc.
+ * That is, valid if any of the fields are valid, but the combination of
+ * only year and day is not valid.
+ *
+ * @returns {boolean} - True all created special date fields are valid.
+ * @see https://datatracker.ietf.org/doc/html/rfc6350#section-4.3.4
+ */
+ validateDates() {
+ for (let field of document.querySelectorAll("vcard-special-date")) {
+ let y = field.querySelector(`input[type="number"][name="year"]`);
+ let m = field.querySelector(`select[name="month"]`);
+ let d = field.querySelector(`select[name="day"]`);
+ if (!y.checkValidity()) {
+ y.focus();
+ return false;
+ }
+ if (y.value && d.value && !m.value) {
+ m.required = true;
+ m.focus();
+ return false;
+ }
+ }
+ return true;
+ }
+}
+customElements.define("vcard-edit", VCardEdit);
+
+/**
+ * Responsible for the type selection of a vCard property.
+ *
+ * Couples the given vCardPropertyEntry with a <select> element.
+ * This is safe because contact editing always creates a new contact, even
+ * when an existing contact is selected for editing.
+ *
+ * @see RFC6350 TYPE
+ */
+class VCardTypeSelectionComponent extends HTMLElement {
+ /**
+ * The select element created by this custom element.
+ *
+ * @type {HTMLSelectElement}
+ */
+ selectEl;
+
+ /**
+ * Initializes the type selector elements to control the given
+ * vCardPropertyEntry.
+ *
+ * @param {VCardPropertyEntry} vCardPropertyEntry - The VCardPropertyEntry
+ * this element should control.
+ * @param {boolean} [options.createLabel] - Whether a Type label should be
+ * created for the selectEl element. If this is not `true`, then the label
+ * for the selectEl should be provided through some other means, such as the
+ * labelledBy property.
+ * @param {string} [options.labelledBy] - Optional `id` of the element that
+ * should label the selectEl element (through aria-labelledby).
+ * @param {string} [options.propertyType] - Specifies the set of types that
+ * should be available and shown for the corresponding property. Set as
+ * "tel" to use the set of telephone types. Otherwise defaults to only using
+ * the `home`, `work` and `(None)` types.
+ */
+ createTypeSelection(vCardPropertyEntry, options) {
+ let template;
+ let types;
+ switch (options.propertyType) {
+ case "tel":
+ types = ["work", "home", "cell", "fax", "pager"];
+ template = document.getElementById("template-vcard-edit-type-tel");
+ break;
+ default:
+ types = ["work", "home"];
+ template = document.getElementById("template-vcard-edit-type");
+ break;
+ }
+
+ let clonedTemplate = template.content.cloneNode(true);
+ this.replaceChildren(clonedTemplate);
+
+ this.selectEl = this.querySelector("select");
+ let selectId = vCardIdGen.next().value;
+ this.selectEl.id = selectId;
+
+ // Just abandon any values we don't have UI for. We don't have any way to
+ // know whether to keep them or not, and they're very rarely used.
+ let paramsType = vCardPropertyEntry.params.type;
+ // toLowerCase is called because other vCard sources are saving the type
+ // in upper case. E.g. from Google.
+ if (Array.isArray(paramsType)) {
+ let lowerCaseTypes = paramsType.map(type => type.toLowerCase());
+ this.selectEl.value = lowerCaseTypes.find(t => types.includes(t)) || "";
+ } else if (paramsType && types.includes(paramsType.toLowerCase())) {
+ this.selectEl.value = paramsType.toLowerCase();
+ }
+
+ // Change the value on the vCardPropertyEntry.
+ this.selectEl.addEventListener("change", e => {
+ if (this.selectEl.value) {
+ vCardPropertyEntry.params.type = this.selectEl.value;
+ } else {
+ delete vCardPropertyEntry.params.type;
+ }
+ });
+
+ // Set an aria-labelledyby on the select.
+ if (options.labelledBy) {
+ if (!document.getElementById(options.labelledBy)) {
+ throw new Error(`No such label element with id ${options.labelledBy}`);
+ }
+ this.querySelector("select").setAttribute(
+ "aria-labelledby",
+ options.labelledBy
+ );
+ }
+
+ // Create a label element for the select.
+ if (options.createLabel) {
+ let labelEl = document.createElement("label");
+ labelEl.htmlFor = selectId;
+ labelEl.setAttribute("data-l10n-id", "vcard-entry-type-label");
+ labelEl.classList.add("screen-reader-only");
+ this.insertBefore(labelEl, this.selectEl);
+ }
+ }
+}
+
+customElements.define("vcard-type", VCardTypeSelectionComponent);
+
+/**
+ * Interface for vCard Fields in the edit view.
+ *
+ * @interface VCardPropertyEntryView
+ */
+
+/**
+ * Getter/Setter for rich data do not use HTMLAttributes for this.
+ * Keep the reference intact through vCardProperties for proper saving.
+ *
+ * @property
+ * @name VCardPropertyEntryView#vCardPropertyEntry
+ */
+
+/**
+ * fromUIToVCardPropertyEntry should directly change data with the reference
+ * through vCardPropertyEntry.
+ * It's there for an action to read the user input values into the
+ * vCardPropertyEntry.
+ *
+ * @function
+ * @name VCardPropertyEntryView#fromUIToVCardPropertyEntry
+ * @returns {void}
+ */
+
+/**
+ * Updates the UI accordingly to the vCardPropertyEntry.
+ *
+ * @function
+ * @name VCardPropertyEntryView#fromVCardPropertyEntryToUI
+ * @returns {void}
+ */
+
+/**
+ * Checks if the value of VCardPropertyEntry is empty.
+ *
+ * @function
+ * @name VCardPropertyEntryView#valueIsEmpty
+ * @returns {boolean}
+ */
+
+/**
+ * Creates a new VCardPropertyEntry for usage in the a new Field.
+ *
+ * @function
+ * @name VCardPropertyEntryView#newVCardPropertyEntry
+ * @static
+ * @returns {VCardPropertyEntry}
+ */
diff --git a/comm/mail/components/addrbook/content/vcard-edit/email.mjs b/comm/mail/components/addrbook/content/vcard-edit/email.mjs
new file mode 100644
index 0000000000..751399ac6c
--- /dev/null
+++ b/comm/mail/components/addrbook/content/vcard-edit/email.mjs
@@ -0,0 +1,135 @@
+/* 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/. */
+
+const lazy = {};
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "VCardPropertyEntry",
+ "resource:///modules/VCardUtils.jsm"
+);
+
+/**
+ * @implements {VCardPropertyEntryView}
+ * @see RFC6350 EMAIL
+ */
+export class VCardEmailComponent extends HTMLTableRowElement {
+ /** @type {VCardPropertyEntry} */
+ vCardPropertyEntry;
+
+ /** @type {HTMLInputElement} */
+ emailEl;
+ /** @type {HTMLInputElement} */
+ checkboxEl;
+
+ static newVCardPropertyEntry() {
+ return new lazy.VCardPropertyEntry("email", {}, "text", "");
+ }
+
+ connectedCallback() {
+ if (this.hasConnected) {
+ return;
+ }
+ this.hasConnected = true;
+
+ let template = document.getElementById("template-vcard-edit-email");
+ let clonedTemplate = template.content.cloneNode(true);
+ this.appendChild(clonedTemplate);
+
+ this.emailEl = this.querySelector('input[type="email"]');
+ this.checkboxEl = this.querySelector('input[type="checkbox"]');
+
+ this.emailEl.addEventListener("input", () => {
+ // Dispatch the event only if this field is the currently selected
+ // default/preferred email address.
+ if (this.checkboxEl.checked) {
+ this.dispatchEvent(VCardEmailComponent.EmailEvent());
+ }
+ });
+
+ // Uncheck the checkbox of other VCardEmailComponents if this one is
+ // checked.
+ this.checkboxEl.addEventListener("change", event => {
+ if (event.target.checked === true) {
+ this.dispatchEvent(VCardEmailComponent.CheckboxEvent());
+ }
+ });
+
+ // Create the email type selection.
+ this.vCardType = this.querySelector("vcard-type");
+ this.vCardType.createTypeSelection(this.vCardPropertyEntry, {
+ labelledBy: "addr-book-edit-email-type",
+ });
+
+ this.querySelector(".remove-property-button").addEventListener(
+ "click",
+ () => {
+ this.dispatchEvent(
+ new CustomEvent("vcard-remove-property", { bubbles: true })
+ );
+ this.remove();
+ document.querySelector("vcard-edit").toggleDefaultEmailView();
+ }
+ );
+
+ this.fromVCardPropertyEntryToUI();
+ }
+
+ fromVCardPropertyEntryToUI() {
+ this.emailEl.value = this.vCardPropertyEntry.value;
+
+ let pref = this.vCardPropertyEntry.params.pref;
+ if (pref === "1") {
+ this.checkboxEl.checked = true;
+ }
+ }
+
+ fromUIToVCardPropertyEntry() {
+ this.vCardPropertyEntry.value = this.emailEl.value;
+
+ if (this.checkboxEl.checked) {
+ this.vCardPropertyEntry.params.pref = "1";
+ } else if (
+ this.vCardPropertyEntry.params.pref &&
+ this.vCardPropertyEntry.params.pref === "1"
+ ) {
+ // Only delete the pref if a pref of 1 is set and the checkbox is not
+ // checked. The pref mechanic is not fully supported yet. Leave all other
+ // prefs untouched.
+ delete this.vCardPropertyEntry.params.pref;
+ }
+ }
+
+ valueIsEmpty() {
+ return this.vCardPropertyEntry.value === "";
+ }
+
+ /**
+ * This event is fired when the checkbox is checked and we need to uncheck the
+ * other checkboxes from each VCardEmailComponent.
+ * FIXME: This should be a radio button part of radiogroup.
+ *
+ * @returns {CustomEvent}
+ */
+ static CheckboxEvent() {
+ return new CustomEvent("vcard-email-default-checkbox", {
+ detail: {},
+ bubbles: true,
+ });
+ }
+
+ /**
+ * This event is fired when the value of an email input field is changed. The
+ * event is fired only if the current email si set as default/preferred.
+ *
+ * @returns {CustomEvent}
+ */
+ static EmailEvent() {
+ return new CustomEvent("vcard-email-default-changed", {
+ detail: {},
+ bubbles: true,
+ });
+ }
+}
+
+customElements.define("vcard-email", VCardEmailComponent, { extends: "tr" });
diff --git a/comm/mail/components/addrbook/content/vcard-edit/fn.mjs b/comm/mail/components/addrbook/content/vcard-edit/fn.mjs
new file mode 100644
index 0000000000..446a262f28
--- /dev/null
+++ b/comm/mail/components/addrbook/content/vcard-edit/fn.mjs
@@ -0,0 +1,71 @@
+/* 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/. */
+
+const lazy = {};
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "VCardPropertyEntry",
+ "resource:///modules/VCardUtils.jsm"
+);
+
+/**
+ * @implements {VCardPropertyEntryView}
+ * @see RFC6350 FN
+ */
+export class VCardFNComponent extends HTMLElement {
+ /** @type {VCardPropertyEntry} */
+ vCardPropertyEntry;
+
+ /** @type {HTMLElement} */
+ displayEl;
+ /** @type {HTMLElement} */
+ preferDisplayEl;
+
+ static newVCardPropertyEntry() {
+ return new lazy.VCardPropertyEntry("fn", {}, "text", "");
+ }
+
+ constructor() {
+ super();
+ let template = document.getElementById("template-vcard-edit-fn");
+ let clonedTemplate = template.content.cloneNode(true);
+ this.appendChild(clonedTemplate);
+ }
+
+ connectedCallback() {
+ if (this.isConnected) {
+ this.displayEl = this.querySelector("#vCardDisplayName");
+ this.displayEl.addEventListener(
+ "input",
+ () => {
+ this.displayEl.isDirty = true;
+ },
+ { once: true }
+ );
+ this.preferDisplayEl = this.querySelector("#vCardPreferDisplayName");
+ this.fromVCardPropertyEntryToUI();
+ }
+ }
+
+ disconnectedCallback() {
+ if (!this.isConnected) {
+ this.displayEl = null;
+ this.vCardPropertyEntry = null;
+ }
+ }
+
+ fromVCardPropertyEntryToUI() {
+ this.displayEl.value = this.vCardPropertyEntry.value;
+ this.displayEl.isDirty = !!this.displayEl.value.trim();
+ }
+
+ fromUIToVCardPropertyEntry() {
+ this.vCardPropertyEntry.value = this.displayEl.value;
+ }
+
+ valueIsEmpty() {
+ return this.vCardPropertyEntry.value === "";
+ }
+}
+customElements.define("vcard-fn", VCardFNComponent);
diff --git a/comm/mail/components/addrbook/content/vcard-edit/id-gen.mjs b/comm/mail/components/addrbook/content/vcard-edit/id-gen.mjs
new file mode 100644
index 0000000000..b4ce37bfda
--- /dev/null
+++ b/comm/mail/components/addrbook/content/vcard-edit/id-gen.mjs
@@ -0,0 +1,12 @@
+/* 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/. */
+
+function* vCardHtmlIdGen() {
+ let internalId = 0;
+ while (true) {
+ yield `vcard-id-${internalId++}`;
+ }
+}
+
+export let vCardIdGen = vCardHtmlIdGen();
diff --git a/comm/mail/components/addrbook/content/vcard-edit/impp.mjs b/comm/mail/components/addrbook/content/vcard-edit/impp.mjs
new file mode 100644
index 0000000000..232925942e
--- /dev/null
+++ b/comm/mail/components/addrbook/content/vcard-edit/impp.mjs
@@ -0,0 +1,97 @@
+/* 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 { vCardIdGen } from "./id-gen.mjs";
+
+const lazy = {};
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "VCardPropertyEntry",
+ "resource:///modules/VCardUtils.jsm"
+);
+
+/**
+ * @implements {VCardPropertyEntryView}
+ * @see RFC6350 IMPP
+ */
+export class VCardIMPPComponent extends HTMLElement {
+ /** @type {VCardPropertyEntry} */
+ vCardPropertyEntry;
+
+ /** @type {HTMLInputElement} */
+ imppEl;
+ /** @type {HTMLSelectElement} */
+ protocolEl;
+
+ static newVCardPropertyEntry() {
+ return new lazy.VCardPropertyEntry("impp", {}, "uri", "");
+ }
+
+ connectedCallback() {
+ if (this.hasConnected) {
+ return;
+ }
+ this.hasConnected = true;
+
+ let template = document.getElementById("template-vcard-edit-impp");
+ this.appendChild(template.content.cloneNode(true));
+
+ this.imppEl = this.querySelector('input[name="impp"]');
+ document.l10n
+ .formatValue("vcard-impp-input-title")
+ .then(t => (this.imppEl.title = t));
+
+ this.protocolEl = this.querySelector('select[name="protocol"]');
+ this.protocolEl.id = vCardIdGen.next().value;
+
+ let protocolLabel = this.querySelector('label[for="protocol"]');
+ protocolLabel.htmlFor = this.protocolEl.id;
+
+ this.protocolEl.addEventListener("change", event => {
+ let entered = this.imppEl.value.split(":", 1)[0]?.toLowerCase();
+ if (entered) {
+ this.protocolEl.value =
+ [...this.protocolEl.options].find(o => o.value.startsWith(entered))
+ ?.value || "";
+ }
+ this.imppEl.placeholder = this.protocolEl.value;
+ this.imppEl.pattern = this.protocolEl.selectedOptions[0].dataset.pattern;
+ });
+
+ this.imppEl.id = vCardIdGen.next().value;
+ let imppLabel = this.querySelector('label[for="impp"]');
+ imppLabel.htmlFor = this.imppEl.id;
+ document.l10n.setAttributes(imppLabel, "vcard-impp-label");
+ this.imppEl.addEventListener("change", event => {
+ this.protocolEl.dispatchEvent(new CustomEvent("change"));
+ });
+
+ this.querySelector(".remove-property-button").addEventListener(
+ "click",
+ () => {
+ this.dispatchEvent(
+ new CustomEvent("vcard-remove-property", { bubbles: true })
+ );
+ this.remove();
+ }
+ );
+
+ this.fromVCardPropertyEntryToUI();
+ this.imppEl.dispatchEvent(new CustomEvent("change"));
+ }
+
+ fromVCardPropertyEntryToUI() {
+ this.imppEl.value = this.vCardPropertyEntry.value;
+ }
+
+ fromUIToVCardPropertyEntry() {
+ this.vCardPropertyEntry.value = this.imppEl.value;
+ }
+
+ valueIsEmpty() {
+ return this.vCardPropertyEntry.value === "";
+ }
+}
+
+customElements.define("vcard-impp", VCardIMPPComponent);
diff --git a/comm/mail/components/addrbook/content/vcard-edit/n.mjs b/comm/mail/components/addrbook/content/vcard-edit/n.mjs
new file mode 100644
index 0000000000..ae5d386d93
--- /dev/null
+++ b/comm/mail/components/addrbook/content/vcard-edit/n.mjs
@@ -0,0 +1,186 @@
+/* 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/. */
+
+const lazy = {};
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "VCardPropertyEntry",
+ "resource:///modules/VCardUtils.jsm"
+);
+
+/**
+ * @implements {VCardPropertyEntryView}
+ * @see RFC6350 N
+ */
+export class VCardNComponent extends HTMLElement {
+ /** @type {VCardPropertyEntry} */
+ vCardPropertyEntry;
+
+ /** @type {HTMLElement} */
+ prefixEl;
+ /** @type {HTMLElement} */
+ firstNameEl;
+ /** @type {HTMLElement} */
+ middleNameEl;
+ /** @type {HTMLElement} */
+ lastNameEl;
+ /** @type {HTMLElement} */
+ suffixEl;
+
+ constructor() {
+ super();
+ let template = document.getElementById("template-vcard-edit-n");
+ let clonedTemplate = template.content.cloneNode(true);
+ this.appendChild(clonedTemplate);
+ }
+
+ connectedCallback() {
+ if (this.isConnected) {
+ this.registerListComponents();
+ this.fromVCardPropertyEntryToUI();
+ this.sortAsOrder();
+ }
+ }
+
+ static newVCardPropertyEntry() {
+ return new lazy.VCardPropertyEntry("n", {}, "text", ["", "", "", "", ""]);
+ }
+
+ /**
+ * Assigns the vCardPropertyEntry values to the individual
+ * NListComponentText elements.
+ *
+ * @TODO sort-as param should be used for the order.
+ * The use-case is that not every language has the order of
+ * prefix, firstName, middleName, lastName, suffix.
+ * Aswell that the user is able to change the sorting as he like
+ * on a per contact base.
+ */
+ sortAsOrder() {
+ if (!this.vCardPropertyEntry.params["sort-as"]) {
+ // eslint-disable-next-line no-useless-return
+ return;
+ }
+ /**
+ * @TODO
+ * The sort-as DOM Mutation
+ */
+ }
+
+ fromVCardPropertyEntryToUI() {
+ let prefixVal = this.vCardPropertyEntry.value[3] || "";
+ let prefixInput = this.prefixEl.querySelector("input");
+ prefixInput.value = prefixVal;
+ if (prefixVal) {
+ this.prefixEl.querySelector("button").hidden = true;
+ } else {
+ this.prefixEl.classList.add("hasButton");
+ this.prefixEl.querySelector("label").hidden = true;
+ prefixInput.hidden = true;
+ }
+
+ // First Name is always shown.
+ this.firstNameEl.querySelector("input").value =
+ this.vCardPropertyEntry.value[1] || "";
+
+ let middleNameVal = this.vCardPropertyEntry.value[2] || "";
+ let middleNameInput = this.middleNameEl.querySelector("input");
+ middleNameInput.value = middleNameVal;
+ if (middleNameVal) {
+ this.middleNameEl.querySelector("button").hidden = true;
+ } else {
+ this.middleNameEl.classList.add("hasButton");
+ this.middleNameEl.querySelector("label").hidden = true;
+ middleNameInput.hidden = true;
+ }
+
+ // Last Name is always shown.
+ this.lastNameEl.querySelector("input").value =
+ this.vCardPropertyEntry.value[0] || "";
+
+ let suffixVal = this.vCardPropertyEntry.value[4] || "";
+ let suffixInput = this.suffixEl.querySelector("input");
+ suffixInput.value = suffixVal;
+ if (suffixVal) {
+ this.suffixEl.querySelector("button").hidden = true;
+ } else {
+ this.suffixEl.classList.add("hasButton");
+ this.suffixEl.querySelector("label").hidden = true;
+ suffixInput.hidden = true;
+ }
+ }
+
+ fromUIToVCardPropertyEntry() {
+ this.vCardPropertyEntry.value = [
+ this.lastNameEl.querySelector("input").value,
+ this.firstNameEl.querySelector("input").value,
+ this.middleNameEl.querySelector("input").value,
+ this.prefixEl.querySelector("input").value,
+ this.suffixEl.querySelector("input").value,
+ ];
+ }
+
+ valueIsEmpty() {
+ let noEmptyStrings = [
+ this.prefixEl,
+ this.firstNameEl,
+ this.middleNameEl,
+ this.lastNameEl,
+ this.suffixEl,
+ ].filter(node => {
+ return node.querySelector("input").value !== "";
+ });
+ return noEmptyStrings.length === 0;
+ }
+
+ registerListComponents() {
+ this.prefixEl = this.querySelector("#n-list-component-prefix");
+ let prefixInput = this.prefixEl.querySelector("input");
+ let prefixButton = this.prefixEl.querySelector("button");
+ prefixButton.addEventListener("click", e => {
+ this.prefixEl.querySelector("label").hidden = false;
+ prefixInput.hidden = false;
+ prefixButton.hidden = true;
+ this.prefixEl.classList.remove("hasButton");
+ prefixInput.focus();
+ });
+
+ this.firstNameEl = this.querySelector("#n-list-component-firstname");
+
+ this.middleNameEl = this.querySelector("#n-list-component-middlename");
+ let middleNameInput = this.middleNameEl.querySelector("input");
+ let middleNameButton = this.middleNameEl.querySelector("button");
+ middleNameButton.addEventListener("click", e => {
+ this.middleNameEl.querySelector("label").hidden = false;
+ middleNameInput.hidden = false;
+ middleNameButton.hidden = true;
+ this.middleNameEl.classList.remove("hasButton");
+ middleNameInput.focus();
+ });
+
+ this.lastNameEl = this.querySelector("#n-list-component-lastname");
+
+ this.suffixEl = this.querySelector("#n-list-component-suffix");
+ let suffixInput = this.suffixEl.querySelector("input");
+ let suffixButton = this.suffixEl.querySelector("button");
+ suffixButton.addEventListener("click", e => {
+ this.suffixEl.querySelector("label").hidden = false;
+ suffixInput.hidden = false;
+ suffixButton.hidden = true;
+ this.suffixEl.classList.remove("hasButton");
+ suffixInput.focus();
+ });
+ }
+
+ disconnectedCallback() {
+ if (!this.isConnected) {
+ this.prefixEl = null;
+ this.firstNameEl = null;
+ this.middleNameEl = null;
+ this.lastNameEl = null;
+ this.suffixEl = null;
+ }
+ }
+}
+customElements.define("vcard-n", VCardNComponent);
diff --git a/comm/mail/components/addrbook/content/vcard-edit/nickname.mjs b/comm/mail/components/addrbook/content/vcard-edit/nickname.mjs
new file mode 100644
index 0000000000..3622b28997
--- /dev/null
+++ b/comm/mail/components/addrbook/content/vcard-edit/nickname.mjs
@@ -0,0 +1,59 @@
+/* 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/. */
+
+const lazy = {};
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "VCardPropertyEntry",
+ "resource:///modules/VCardUtils.jsm"
+);
+
+/**
+ * @implements {VCardPropertyEntryView}
+ * @see RFC6350 NICKNAME
+ */
+export class VCardNickNameComponent extends HTMLElement {
+ /** @type {VCardPropertyEntry} */
+ vCardPropertyEntry;
+ /** @type {HTMLElement} */
+ nickNameEl;
+
+ constructor() {
+ super();
+ let template = document.getElementById("template-vcard-edit-nickname");
+ let clonedTemplate = template.content.cloneNode(true);
+ this.appendChild(clonedTemplate);
+ }
+
+ static newVCardPropertyEntry() {
+ return new lazy.VCardPropertyEntry("nickname", {}, "text", "");
+ }
+
+ connectedCallback() {
+ if (this.isConnected) {
+ this.nickNameEl = this.querySelector("#vCardNickName");
+ this.fromVCardPropertyEntryToUI();
+ }
+ }
+
+ disconnectedCallback() {
+ if (!this.isConnected) {
+ this.nickNameEl = null;
+ this.vCardPropertyEntry = null;
+ }
+ }
+
+ fromVCardPropertyEntryToUI() {
+ this.nickNameEl.value = this.vCardPropertyEntry.value;
+ }
+
+ fromUIToVCardPropertyEntry() {
+ this.vCardPropertyEntry.value = this.nickNameEl.value;
+ }
+
+ valueIsEmpty() {
+ return this.vCardPropertyEntry.value === "";
+ }
+}
+customElements.define("vcard-nickname", VCardNickNameComponent);
diff --git a/comm/mail/components/addrbook/content/vcard-edit/note.mjs b/comm/mail/components/addrbook/content/vcard-edit/note.mjs
new file mode 100644
index 0000000000..f78f4a16d8
--- /dev/null
+++ b/comm/mail/components/addrbook/content/vcard-edit/note.mjs
@@ -0,0 +1,82 @@
+/* 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/. */
+
+const lazy = {};
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "VCardPropertyEntry",
+ "resource:///modules/VCardUtils.jsm"
+);
+
+/**
+ * @implements {VCardPropertyEntryView}
+ * @see RFC6350 Note
+ */
+export class VCardNoteComponent extends HTMLElement {
+ /** @type {VCardPropertyEntry} */
+ vCardPropertyEntry;
+
+ /** @type {HTMLTextAreaElement} */
+ textAreaEl;
+
+ static newVCardPropertyEntry() {
+ return new lazy.VCardPropertyEntry("note", {}, "text", "");
+ }
+
+ constructor() {
+ super();
+ let template = document.getElementById("template-vcard-edit-note");
+ let clonedTemplate = template.content.cloneNode(true);
+ this.appendChild(clonedTemplate);
+ }
+
+ connectedCallback() {
+ if (this.isConnected) {
+ this.textAreaEl = this.querySelector("textarea");
+ this.textAreaEl.addEventListener("input", () => {
+ this.resizeTextAreaEl();
+ });
+ this.querySelector(".remove-property-button").addEventListener(
+ "click",
+ () => {
+ document.getElementById("vcard-add-note").hidden = false;
+ this.dispatchEvent(
+ new CustomEvent("vcard-remove-property", { bubbles: true })
+ );
+ this.remove();
+ }
+ );
+ this.fromVCardPropertyEntryToUI();
+ }
+ }
+
+ disconnectedCallback() {
+ if (!this.isConnected) {
+ this.textAreaEl = null;
+ this.vCardPropertyEntry = null;
+ }
+ }
+
+ fromVCardPropertyEntryToUI() {
+ this.textAreaEl.value = this.vCardPropertyEntry.value;
+ this.resizeTextAreaEl();
+ }
+
+ fromUIToVCardPropertyEntry() {
+ this.vCardPropertyEntry.value = this.textAreaEl.value;
+ }
+
+ valueIsEmpty() {
+ return this.vCardPropertyEntry.value === "";
+ }
+
+ resizeTextAreaEl() {
+ this.textAreaEl.rows = Math.min(
+ 15,
+ Math.max(5, this.textAreaEl.value.split("\n").length)
+ );
+ }
+}
+
+customElements.define("vcard-note", VCardNoteComponent);
diff --git a/comm/mail/components/addrbook/content/vcard-edit/org.mjs b/comm/mail/components/addrbook/content/vcard-edit/org.mjs
new file mode 100644
index 0000000000..fb788c3043
--- /dev/null
+++ b/comm/mail/components/addrbook/content/vcard-edit/org.mjs
@@ -0,0 +1,197 @@
+/* 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 { vCardIdGen } from "./id-gen.mjs";
+
+const lazy = {};
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "VCardPropertyEntry",
+ "resource:///modules/VCardUtils.jsm"
+);
+
+/**
+ * @implements {VCardPropertyEntryView}
+ * @see RFC6350 TITLE
+ */
+export class VCardTitleComponent extends HTMLElement {
+ /** @type {VCardPropertyEntry} */
+ vCardPropertyEntry;
+
+ /** @type {HTMLInputElement} */
+ titleEl;
+
+ static newVCardPropertyEntry() {
+ return new lazy.VCardPropertyEntry("title", {}, "text", "");
+ }
+
+ constructor() {
+ super();
+ let template = document.getElementById("template-vcard-edit-title");
+ let clonedTemplate = template.content.cloneNode(true);
+ this.appendChild(clonedTemplate);
+ }
+
+ connectedCallback() {
+ if (this.isConnected) {
+ this.titleEl = this.querySelector('input[name="title"]');
+ this.assignIds(this.titleEl, this.querySelector('label[for="title"]'));
+
+ this.fromVCardPropertyEntryToUI();
+ }
+ }
+
+ disconnectedCallback() {
+ if (!this.isConnected) {
+ this.vCardPropertyEntry = null;
+ this.titleEl = null;
+ }
+ }
+
+ fromVCardPropertyEntryToUI() {
+ this.titleEl.value = this.vCardPropertyEntry.value || "";
+ }
+
+ fromUIToVCardPropertyEntry() {
+ this.vCardPropertyEntry.value = this.titleEl.value;
+ }
+
+ valueIsEmpty() {
+ return this.vCardPropertyEntry.value === "";
+ }
+
+ assignIds(inputEl, labelEl) {
+ let labelInputId = vCardIdGen.next().value;
+ inputEl.id = labelInputId;
+ labelEl.htmlFor = labelInputId;
+ }
+}
+customElements.define("vcard-title", VCardTitleComponent);
+
+/**
+ * @implements {VCardPropertyEntryView}
+ * @see RFC6350 ROLE
+ */
+export class VCardRoleComponent extends HTMLElement {
+ /** @type {VCardPropertyEntry} */
+ vCardPropertyEntry;
+
+ /** @type {HTMLInputElement} */
+ roleEl;
+
+ static newVCardPropertyEntry() {
+ return new lazy.VCardPropertyEntry("role", {}, "text", "");
+ }
+
+ constructor() {
+ super();
+ let template = document.getElementById("template-vcard-edit-role");
+ let clonedTemplate = template.content.cloneNode(true);
+ this.appendChild(clonedTemplate);
+ }
+
+ connectedCallback() {
+ if (this.isConnected) {
+ this.roleEl = this.querySelector('input[name="role"]');
+ this.assignIds(this.roleEl, this.querySelector('label[for="role"]'));
+
+ this.fromVCardPropertyEntryToUI();
+ }
+ }
+
+ disconnectedCallback() {
+ if (!this.isConnected) {
+ this.vCardPropertyEntry = null;
+ this.roleEl = null;
+ }
+ }
+
+ fromVCardPropertyEntryToUI() {
+ this.roleEl.value = this.vCardPropertyEntry.value || "";
+ }
+
+ fromUIToVCardPropertyEntry() {
+ this.vCardPropertyEntry.value = this.roleEl.value;
+ }
+
+ valueIsEmpty() {
+ return this.vCardPropertyEntry.value === "";
+ }
+
+ assignIds(inputEl, labelEl) {
+ let labelInputId = vCardIdGen.next().value;
+ inputEl.id = labelInputId;
+ labelEl.htmlFor = labelInputId;
+ }
+}
+customElements.define("vcard-role", VCardRoleComponent);
+
+/**
+ * @implements {VCardPropertyEntryView}
+ * @see RFC6350 ORG
+ */
+export class VCardOrgComponent extends HTMLElement {
+ /** @type {VCardPropertyEntry} */
+ vCardPropertyEntry;
+ /** @type {HTMLInputElement} */
+ orgEl;
+ /** @type {HTMLInputElement} */
+ unitEl;
+
+ static newVCardPropertyEntry() {
+ return new lazy.VCardPropertyEntry("org", {}, "text", ["", ""]);
+ }
+
+ connectedCallback() {
+ if (this.hasConnected) {
+ return;
+ }
+ this.hasConnected = true;
+
+ let template = document.getElementById("template-vcard-edit-org");
+ let clonedTemplate = template.content.cloneNode(true);
+ this.appendChild(clonedTemplate);
+
+ this.orgEl = this.querySelector('input[name="org"]');
+ this.orgEl.id = vCardIdGen.next().value;
+ this.querySelector('label[for="org"]').htmlFor = this.orgEl.id;
+
+ this.unitEl = this.querySelector('input[name="orgUnit"]');
+ this.unitEl.id = vCardIdGen.next().value;
+ this.querySelector('label[for="orgUnit"]').htmlFor = this.unitEl.id;
+
+ this.fromVCardPropertyEntryToUI();
+ }
+
+ fromVCardPropertyEntryToUI() {
+ let values = this.vCardPropertyEntry.value;
+ if (!values) {
+ this.orgEl.value = "";
+ this.unitEl.value = "";
+ return;
+ }
+ if (!Array.isArray(values)) {
+ values = [values];
+ }
+ this.orgEl.value = values.shift() || "";
+ // In case data had more levels of units, just pull them together.
+ this.unitEl.value = values.join(", ");
+ }
+
+ fromUIToVCardPropertyEntry() {
+ this.vCardPropertyEntry.value = [this.orgEl.value.trim()];
+ if (this.unitEl.value.trim()) {
+ this.vCardPropertyEntry.value.push(this.unitEl.value.trim());
+ }
+ }
+
+ valueIsEmpty() {
+ return (
+ !this.vCardPropertyEntry.value ||
+ (Array.isArray(this.vCardPropertyEntry.value) &&
+ this.vCardPropertyEntry.value.every(v => v === ""))
+ );
+ }
+}
+customElements.define("vcard-org", VCardOrgComponent);
diff --git a/comm/mail/components/addrbook/content/vcard-edit/special-date.mjs b/comm/mail/components/addrbook/content/vcard-edit/special-date.mjs
new file mode 100644
index 0000000000..17c7df493b
--- /dev/null
+++ b/comm/mail/components/addrbook/content/vcard-edit/special-date.mjs
@@ -0,0 +1,269 @@
+/* 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 { vCardIdGen } from "./id-gen.mjs";
+
+const lazy = {};
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "VCardPropertyEntry",
+ "resource:///modules/VCardUtils.jsm"
+);
+
+const { ICAL } = ChromeUtils.import("resource:///modules/calendar/Ical.jsm");
+
+/**
+ * ANNIVERSARY and BDAY both have a cardinality of
+ * 1 ("Exactly one instance per vCard MAY be present.").
+ *
+ * For Anniversary we changed the cardinality to
+ * ("One or more instances per vCard MAY be present.")".
+ *
+ * @implements {VCardPropertyEntryView}
+ * @see RFC6350 ANNIVERSARY and BDAY
+ */
+export class VCardSpecialDateComponent extends HTMLElement {
+ /** @type {VCardPropertyEntry} */
+ vCardPropertyEntry;
+
+ /** @type {HTMLSelectElement} */
+ selectEl;
+ /** @type {HTMLInputElement} */
+ year;
+ /** @type {HTMLSelectElement} */
+ month;
+ /** @type {HTMLSelectElement} */
+ day;
+
+ /**
+ * Object containing the available days for each month.
+ *
+ * @type {object}
+ */
+ monthDays = {
+ 1: 31,
+ 2: 28,
+ 3: 31,
+ 4: 30,
+ 5: 31,
+ 6: 30,
+ 7: 31,
+ 8: 31,
+ 9: 30,
+ 10: 31,
+ 11: 30,
+ 12: 31,
+ };
+
+ static newAnniversaryVCardPropertyEntry() {
+ return new lazy.VCardPropertyEntry("anniversary", {}, "date", "");
+ }
+
+ static newBdayVCardPropertyEntry() {
+ return new lazy.VCardPropertyEntry("bday", {}, "date", "");
+ }
+
+ connectedCallback() {
+ if (this.hasConnected) {
+ return;
+ }
+ this.hasConnected = true;
+
+ let template = document.getElementById(
+ "template-vcard-edit-bday-anniversary"
+ );
+ let clonedTemplate = template.content.cloneNode(true);
+ this.appendChild(clonedTemplate);
+
+ this.selectEl = this.querySelector(".vcard-type-selection");
+ let selectId = vCardIdGen.next().value;
+ this.selectEl.id = selectId;
+ this.querySelector(".vcard-type-label").htmlFor = selectId;
+
+ this.selectEl.addEventListener("change", event => {
+ this.dispatchEvent(
+ VCardSpecialDateComponent.ChangeVCardPropertyEntryEvent(
+ event.target.value
+ )
+ );
+ });
+
+ this.month = this.querySelector("#month");
+ let monthId = vCardIdGen.next().value;
+ this.month.id = monthId;
+ this.querySelector('label[for="month"]').htmlFor = monthId;
+ this.month.addEventListener("change", () => {
+ this.fillDayOptions();
+ });
+
+ this.day = this.querySelector("#day");
+ let dayId = vCardIdGen.next().value;
+ this.day.id = dayId;
+ this.querySelector('label[for="day"]').htmlFor = dayId;
+
+ this.year = this.querySelector("#year");
+ let yearId = vCardIdGen.next().value;
+ this.year.id = yearId;
+ this.querySelector('label[for="year"]').htmlFor = yearId;
+ this.year.addEventListener("input", () => {
+ this.fillDayOptions();
+ });
+
+ document.l10n.formatValues([{ id: "vcard-date-year" }]).then(yearLabel => {
+ this.year.placeholder = yearLabel;
+ });
+
+ this.querySelector(".remove-property-button").addEventListener(
+ "click",
+ () => {
+ this.dispatchEvent(
+ new CustomEvent("vcard-remove-property", { bubbles: true })
+ );
+ this.remove();
+ }
+ );
+
+ this.fillMonthOptions();
+ this.fromVCardPropertyEntryToUI();
+ }
+
+ fromVCardPropertyEntryToUI() {
+ this.selectEl.value = this.vCardPropertyEntry.name;
+ if (this.vCardPropertyEntry.type === "text") {
+ // TODO: support of text type for special-date
+ this.hidden = true;
+ return;
+ }
+ // Default value is date-and-or-time.
+ let dateValue;
+ try {
+ dateValue = ICAL.VCardTime.fromDateAndOrTimeString(
+ this.vCardPropertyEntry.value || "",
+ "date-and-or-time"
+ );
+ } catch (ex) {
+ console.error(ex);
+ }
+ // Always set the month first since that controls the available days.
+ this.month.value = dateValue?.month || "";
+ this.fillDayOptions();
+ this.day.value = dateValue?.day || "";
+ this.year.value = dateValue?.year || "";
+ }
+
+ fromUIToVCardPropertyEntry() {
+ if (this.vCardPropertyEntry.type === "text") {
+ // TODO: support of text type for special-date
+ return;
+ }
+ // Default value is date-and-or-time.
+ let dateValue = new ICAL.VCardTime({}, null, "date");
+ // Set the properties directly instead of using the VCardTime
+ // constructor argument, which causes null values to become 0.
+ dateValue.year = this.year.value ? Number(this.year.value) : null;
+ dateValue.month = this.month.value ? Number(this.month.value) : null;
+ dateValue.day = this.day.value ? Number(this.day.value) : null;
+ this.vCardPropertyEntry.value = dateValue.toString();
+ }
+
+ valueIsEmpty() {
+ return !this.year.value && !this.month.value && !this.day.value;
+ }
+
+ /**
+ * @param {"bday" | "anniversary"} entryName
+ * @returns {CustomEvent}
+ */
+ static ChangeVCardPropertyEntryEvent(entryName) {
+ return new CustomEvent("vcard-bday-anniversary-change", {
+ detail: {
+ name: entryName,
+ },
+ bubbles: true,
+ });
+ }
+
+ /**
+ * Check if the specified year is a leap year in order to add or remove the
+ * extra day to February.
+ *
+ * @returns {boolean} True if the currently specified year is a leap year,
+ * or if no valid year value is available.
+ */
+ isLeapYear() {
+ // If the year is empty, we can't know if it's a leap year so must assume
+ // it is. Otherwise year-less dates can't show Feb 29.
+ if (!this.year.checkValidity() || this.year.value === "") {
+ return true;
+ }
+
+ let year = parseInt(this.year.value);
+ return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
+ }
+
+ fillMonthOptions() {
+ let formatter = Intl.DateTimeFormat(undefined, { month: "long" });
+ for (let m = 1; m <= 12; m++) {
+ let option = document.createElement("option");
+ option.setAttribute("value", m);
+ option.setAttribute("label", formatter.format(new Date(2000, m - 1, 2)));
+ this.month.appendChild(option);
+ }
+ }
+
+ /**
+ * Update the Day select element to reflect the available days of the selected
+ * month.
+ */
+ fillDayOptions() {
+ let prevDay = 0;
+ // Save the previously selected day if we have one.
+ if (this.day.childNodes.length > 1) {
+ prevDay = this.day.value;
+ }
+
+ // Always clear old options.
+ let defaultOption = document.createElement("option");
+ defaultOption.value = "";
+ document.l10n
+ .formatValues([{ id: "vcard-date-day" }])
+ .then(([dayLabel]) => {
+ defaultOption.textContent = dayLabel;
+ });
+ this.day.replaceChildren(defaultOption);
+
+ let monthValue = this.month.value || 1;
+ // Add a day to February if this is a leap year and we're in February.
+ if (monthValue === "2") {
+ this.monthDays["2"] = this.isLeapYear() ? 29 : 28;
+ }
+
+ let formatter = Intl.DateTimeFormat(undefined, { day: "numeric" });
+ for (let d = 1; d <= this.monthDays[monthValue]; d++) {
+ let option = document.createElement("option");
+ option.setAttribute("value", d);
+ option.setAttribute("label", formatter.format(new Date(2000, 0, d)));
+ this.day.appendChild(option);
+ }
+ // Reset the previously selected day, if it's available in the currently
+ // selected month.
+ this.day.value = prevDay <= this.monthDays[monthValue] ? prevDay : "";
+ }
+
+ /**
+ * @param {boolean} options.hasBday
+ */
+ birthdayAvailability(options) {
+ if (this.vCardPropertyEntry.name === "bday") {
+ return;
+ }
+ Array.from(this.selectEl.options).forEach(option => {
+ if (option.value === "bday") {
+ option.disabled = options.hasBday;
+ }
+ });
+ }
+}
+
+customElements.define("vcard-special-date", VCardSpecialDateComponent);
diff --git a/comm/mail/components/addrbook/content/vcard-edit/tel.mjs b/comm/mail/components/addrbook/content/vcard-edit/tel.mjs
new file mode 100644
index 0000000000..a5eb30c6d5
--- /dev/null
+++ b/comm/mail/components/addrbook/content/vcard-edit/tel.mjs
@@ -0,0 +1,83 @@
+/* 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 { vCardIdGen } from "./id-gen.mjs";
+
+const lazy = {};
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "VCardPropertyEntry",
+ "resource:///modules/VCardUtils.jsm"
+);
+
+/**
+ * @implements {VCardPropertyEntryView}
+ * @see RFC6350 TEL
+ *
+ * @TODO missing type-param-tel support.
+ * "text, voice, video, textphone"
+ */
+export class VCardTelComponent extends HTMLElement {
+ /** @type {VCardPropertyEntry} */
+ vCardPropertyEntry;
+
+ /** @type {HTMLInputElement} */
+ inputElement;
+
+ static newVCardPropertyEntry() {
+ return new lazy.VCardPropertyEntry("tel", {}, "text", "");
+ }
+
+ connectedCallback() {
+ if (this.hasConnected) {
+ return;
+ }
+ this.hasConnected = true;
+
+ let template = document.getElementById("template-vcard-edit-tel");
+ let clonedTemplate = template.content.cloneNode(true);
+ this.appendChild(clonedTemplate);
+
+ this.inputElement = this.querySelector('input[type="text"]');
+ let urlId = vCardIdGen.next().value;
+ this.inputElement.id = urlId;
+ let urlLabel = this.querySelector('label[for="text"]');
+ urlLabel.htmlFor = urlId;
+ document.l10n.setAttributes(urlLabel, "vcard-tel-label");
+ this.inputElement.type = "tel";
+
+ // Create the tel type selection.
+ this.vCardType = this.querySelector("vcard-type");
+ this.vCardType.createTypeSelection(this.vCardPropertyEntry, {
+ createLabel: true,
+ propertyType: "tel",
+ });
+
+ this.querySelector(".remove-property-button").addEventListener(
+ "click",
+ () => {
+ this.dispatchEvent(
+ new CustomEvent("vcard-remove-property", { bubbles: true })
+ );
+ this.remove();
+ }
+ );
+
+ this.fromVCardPropertyEntryToUI();
+ }
+
+ fromVCardPropertyEntryToUI() {
+ this.inputElement.value = this.vCardPropertyEntry.value;
+ }
+
+ fromUIToVCardPropertyEntry() {
+ this.vCardPropertyEntry.value = this.inputElement.value;
+ }
+
+ valueIsEmpty() {
+ return this.vCardPropertyEntry.value === "";
+ }
+}
+
+customElements.define("vcard-tel", VCardTelComponent);
diff --git a/comm/mail/components/addrbook/content/vcard-edit/tz.mjs b/comm/mail/components/addrbook/content/vcard-edit/tz.mjs
new file mode 100644
index 0000000000..cf77114db6
--- /dev/null
+++ b/comm/mail/components/addrbook/content/vcard-edit/tz.mjs
@@ -0,0 +1,86 @@
+/* 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/. */
+
+const lazy = {};
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "cal",
+ "resource:///modules/calendar/calUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "VCardPropertyEntry",
+ "resource:///modules/VCardUtils.jsm"
+);
+
+/**
+ * @implements {VCardPropertyEntryView}
+ * @see RFC6350 URL
+ */
+export class VCardTZComponent extends HTMLElement {
+ /** @type {VCardPropertyEntry} */
+ vCardPropertyEntry;
+
+ /** @type {HTMLSelectElement} */
+ selectEl;
+
+ static newVCardPropertyEntry() {
+ return new lazy.VCardPropertyEntry("tz", {}, "text", "");
+ }
+
+ constructor() {
+ super();
+ let template = document.getElementById("template-vcard-edit-tz");
+ let clonedTemplate = template.content.cloneNode(true);
+ this.appendChild(clonedTemplate);
+ }
+
+ connectedCallback() {
+ if (this.isConnected) {
+ this.selectEl = this.querySelector("select");
+ for (let tzid of lazy.cal.timezoneService.timezoneIds) {
+ let option = this.selectEl.appendChild(
+ document.createElement("option")
+ );
+ option.value = tzid;
+ option.textContent =
+ lazy.cal.timezoneService.getTimezone(tzid).displayName;
+ }
+
+ this.querySelector(".remove-property-button").addEventListener(
+ "click",
+ () => {
+ document.getElementById("vcard-add-tz").hidden = false;
+ this.dispatchEvent(
+ new CustomEvent("vcard-remove-property", { bubbles: true })
+ );
+ this.remove();
+ }
+ );
+
+ this.fromVCardPropertyEntryToUI();
+ }
+ }
+
+ disconnectedCallback() {
+ if (!this.isConnected) {
+ this.selectEl = null;
+ this.vCardPropertyEntry = null;
+ }
+ }
+
+ fromVCardPropertyEntryToUI() {
+ this.selectEl.value = this.vCardPropertyEntry.value;
+ }
+
+ fromUIToVCardPropertyEntry() {
+ this.vCardPropertyEntry.value = this.selectEl.value;
+ }
+
+ valueIsEmpty() {
+ return this.vCardPropertyEntry.value === "";
+ }
+}
+
+customElements.define("vcard-tz", VCardTZComponent);
diff --git a/comm/mail/components/addrbook/content/vcard-edit/url.mjs b/comm/mail/components/addrbook/content/vcard-edit/url.mjs
new file mode 100644
index 0000000000..98a1b42951
--- /dev/null
+++ b/comm/mail/components/addrbook/content/vcard-edit/url.mjs
@@ -0,0 +1,89 @@
+/* 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 { vCardIdGen } from "./id-gen.mjs";
+
+const lazy = {};
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "VCardPropertyEntry",
+ "resource:///modules/VCardUtils.jsm"
+);
+
+/**
+ * @implements {VCardPropertyEntryView}
+ * @see RFC6350 URL
+ */
+export class VCardURLComponent extends HTMLElement {
+ /** @type {VCardPropertyEntry} */
+ vCardPropertyEntry;
+
+ /** @type {HTMLInputElement} */
+ urlEl;
+
+ static newVCardPropertyEntry() {
+ return new lazy.VCardPropertyEntry("url", {}, "uri", "");
+ }
+
+ connectedCallback() {
+ if (this.hasConnected) {
+ return;
+ }
+ this.hasConnected = true;
+
+ let template = document.getElementById("template-vcard-edit-type-text");
+ let clonedTemplate = template.content.cloneNode(true);
+ this.appendChild(clonedTemplate);
+
+ this.urlEl = this.querySelector('input[type="text"]');
+ let urlId = vCardIdGen.next().value;
+ this.urlEl.id = urlId;
+ let urlLabel = this.querySelector('label[for="text"]');
+ urlLabel.htmlFor = urlId;
+ this.urlEl.type = "url";
+ document.l10n.setAttributes(urlLabel, "vcard-url-label");
+
+ this.urlEl.addEventListener("input", () => {
+ // Auto add https:// if the url is missing scheme.
+ if (
+ this.urlEl.value.length > "https://".length &&
+ !/^https?:\/\//.test(this.urlEl.value)
+ ) {
+ this.urlEl.value = "https://" + this.urlEl.value;
+ }
+ });
+
+ // Create the url type selection.
+ this.vCardType = this.querySelector("vcard-type");
+ this.vCardType.createTypeSelection(this.vCardPropertyEntry, {
+ createLabel: true,
+ });
+
+ this.querySelector(".remove-property-button").addEventListener(
+ "click",
+ () => {
+ this.dispatchEvent(
+ new CustomEvent("vcard-remove-property", { bubbles: true })
+ );
+ this.remove();
+ }
+ );
+
+ this.fromVCardPropertyEntryToUI();
+ }
+
+ fromVCardPropertyEntryToUI() {
+ this.urlEl.value = this.vCardPropertyEntry.value;
+ }
+
+ fromUIToVCardPropertyEntry() {
+ this.vCardPropertyEntry.value = this.urlEl.value;
+ }
+
+ valueIsEmpty() {
+ return this.vCardPropertyEntry.value === "";
+ }
+}
+
+customElements.define("vcard-url", VCardURLComponent);
diff --git a/comm/mail/components/addrbook/content/vcard-edit/vCardTemplates.inc.xhtml b/comm/mail/components/addrbook/content/vcard-edit/vCardTemplates.inc.xhtml
new file mode 100644
index 0000000000..56d53f57f1
--- /dev/null
+++ b/comm/mail/components/addrbook/content/vcard-edit/vCardTemplates.inc.xhtml
@@ -0,0 +1,398 @@
+# 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/.
+
+<!-- Styles -->
+<link rel="stylesheet" href="chrome://messenger/skin/vcard.css" />
+
+<!-- Scripts -->
+<script type="module" src="chrome://messenger/content/addressbook/edit/edit.mjs"></script>
+
+<!-- Localization -->
+<link rel="localization" href="messenger/addressbook/vcard.ftl" />
+
+<!-- Edit View -->
+<template id="template-addr-book-edit">
+ <!-- Name -->
+ <fieldset id="addr-book-edit-n" class="addr-book-edit-fieldset fieldset-reset">
+ <legend class="screen-reader-only" data-l10n-id="vcard-name-header"/>
+ <div class="addr-book-edit-display-nickname">
+ </div>
+ </fieldset>
+ <fieldset id="addr-book-edit-email" class="addr-book-edit-fieldset fieldset-reset">
+ <legend data-l10n-id="vcard-email-header"/>
+ <table>
+ <thead>
+ <tr>
+ <th id="addr-book-edit-email-type" scope="col">
+ <!-- NOTE: We use the <span> so we can apply the screen-reader-only
+ - class to the <span> rather than the <th> element. If we apply
+ - the class to the <th> element directly it causes problems with
+ - Orca's "browse mode" table navigation. See bug 1776644. -->
+ <span class="screen-reader-only"
+ data-l10n-id="vcard-entry-type-label">
+ </span>
+ </th>
+ <th id="addr-book-edit-email-label" scope="col">
+ <span class="screen-reader-only"
+ data-l10n-id="vcard-email-label">
+ </span>
+ </th>
+ <th id="addr-book-edit-email-default" scope="col">
+ <span data-l10n-id="vcard-primary-email-label"></span>
+ </th>
+ </tr>
+ </thead>
+ <tbody id="vcard-email"></tbody>
+ </table>
+ <button id="vcard-add-email"
+ data-l10n-id="vcard-email-add"
+ class="addr-book-edit-fieldset-button add-property-button icon-button"
+ type="button"></button>
+ </fieldset>
+ <!-- URL -->
+ <fieldset id="addr-book-edit-url" class="addr-book-edit-fieldset fieldset-reset">
+ <legend data-l10n-id="vcard-url-header"/>
+ <button id="vcard-add-url"
+ data-l10n-id="vcard-url-add"
+ class="addr-book-edit-fieldset-button add-property-button icon-button"
+ type="button"></button>
+ </fieldset>
+ <!-- Address -->
+ <fieldset id="addr-book-edit-address" class="addr-book-edit-fieldset fieldset-reset">
+ <legend data-l10n-id="vcard-adr-header"/>
+ <button id="vcard-add-adr"
+ data-l10n-id="vcard-adr-add"
+ class="addr-book-edit-fieldset-button add-property-button icon-button"
+ type="button"></button>
+ </fieldset>
+ <!-- Tel -->
+ <fieldset id="addr-book-edit-tel" class="addr-book-edit-fieldset fieldset-reset">
+ <legend data-l10n-id="vcard-tel-header"/>
+ <button id="vcard-add-tel"
+ data-l10n-id="vcard-tel-add"
+ class="addr-book-edit-fieldset-button add-property-button icon-button"
+ type="button"></button>
+ </fieldset>
+ <!-- Time Zone -->
+ <fieldset id="addr-book-edit-tz" class="addr-book-edit-fieldset fieldset-reset">
+ <legend data-l10n-id="vcard-tz-header"/>
+ <button id="vcard-add-tz"
+ data-l10n-id="vcard-tz-add"
+ class="addr-book-edit-fieldset-button add-property-button icon-button"
+ type="button"></button>
+ </fieldset>
+ <!-- IMPP (Chat) -->
+ <fieldset id="addr-book-edit-impp" class="addr-book-edit-fieldset fieldset-reset">
+ <legend data-l10n-id="vcard-impp2-header"/>
+ <button id="vcard-add-impp"
+ data-l10n-id="vcard-impp-add"
+ class="addr-book-edit-fieldset-button add-property-button icon-button"
+ type="button"></button>
+ </fieldset>
+ <!-- Birthday and Anniversary (Special dates) -->
+ <fieldset id="addr-book-edit-bday-anniversary" class="addr-book-edit-fieldset fieldset-reset">
+ <legend data-l10n-id="vcard-bday-anniversary-header"/>
+ <button id="vcard-add-bday-anniversary"
+ data-l10n-id="vcard-bday-anniversary-add"
+ class="addr-book-edit-fieldset-button add-property-button icon-button"
+ type="button"></button>
+ </fieldset>
+ <!-- Notes -->
+ <fieldset id="addr-book-edit-note" class="addr-book-edit-fieldset fieldset-reset">
+ <legend data-l10n-id="vcard-note-header"/>
+ <button id="vcard-add-note"
+ data-l10n-id="vcard-note-add"
+ class="addr-book-edit-fieldset-button add-property-button icon-button"
+ type="button"></button>
+ </fieldset>
+ <!-- Organization Info -->
+ <fieldset id="addr-book-edit-org" class="addr-book-edit-fieldset fieldset-reset">
+ <legend data-l10n-id="vcard-org-header"/>
+ <button id="vcard-add-org"
+ data-l10n-id="vcard-org-add"
+ class="addr-book-edit-fieldset-button add-property-button icon-button"
+ type="button"></button>
+ <button type="button"
+ class="addr-book-edit-fieldset-button remove-property-button icon-button"
+ data-l10n-id="vcard-remove-button"
+ hidden="hidden"></button>
+ </fieldset>
+ <!-- Custom -->
+ <fieldset id="addr-book-edit-custom" class="addr-book-edit-fieldset fieldset-reset">
+ <legend data-l10n-id="vcard-custom-header"/>
+ <button id="vcard-add-custom"
+ data-l10n-id="vcard-custom-add"
+ class="addr-book-edit-fieldset-button add-property-button icon-button"
+ type="button"></button>
+ </fieldset>
+</template>
+
+<!-- Individual fields -->
+
+<!-- N field -->
+<template id="template-vcard-edit-n">
+ <div id="n-list-component-prefix" class="n-list-component">
+ <label for="vcard-n-prefix" data-l10n-id="vcard-n-prefix" />
+ <input id="vcard-n-prefix"
+ type="text"
+ autocomplete="off" />
+ <button class="primary" data-l10n-id="vcard-n-add-prefix"
+ type="button">
+ <img src="chrome://global/skin/icons/add.svg" alt="" />
+ </button>
+ </div>
+ <div id="n-list-component-firstname" class="n-list-component">
+ <label for="vcard-n-firstname" data-l10n-id="vcard-n-firstname" />
+ <input id="vcard-n-firstname"
+ type="text"
+ autocomplete="off" />
+ </div>
+ <div id="n-list-component-middlename" class="n-list-component">
+ <label for="vcard-n-middlename" data-l10n-id="vcard-n-middlename" />
+ <input id="vcard-n-middlename"
+ type="text"
+ autocomplete="off" />
+ <button class="primary" data-l10n-id="vcard-n-add-middlename"
+ type="button">
+ <img src="chrome://global/skin/icons/add.svg" alt="" />
+ </button>
+ </div>
+ <div id="n-list-component-lastname" class="n-list-component">
+ <label for="vcard-n-lastname" data-l10n-id="vcard-n-lastname" />
+ <input id="vcard-n-lastname"
+ type="text"
+ autocomplete="off" />
+ </div>
+ <div id="n-list-component-suffix" class="n-list-component">
+ <label for="vcard-n-suffix" data-l10n-id="vcard-n-suffix" />
+ <button class="primary" data-l10n-id="vcard-n-add-suffix"
+ type="button">
+ <img src="chrome://global/skin/icons/add.svg" alt="" />
+ </button>
+ <input id="vcard-n-suffix"
+ type="text"
+ autocomplete="off" />
+ </div>
+</template>
+
+<!-- FN field. -->
+<template id="template-vcard-edit-fn">
+ <label for="vCardDisplayName" data-l10n-id="vcard-displayname"></label>
+ <input id="vCardDisplayName" type="text"/>
+ <label id="vCardDisplayNameCheckbox" class="vcard-checkbox">
+ <!-- There is no l10n ID on this element because the vCard edit form is
+ also used in other sections that don't use this checkbox and don't have
+ access to the fluent string. The string is added when needed by the
+ address book edit.js file. -->
+ <input type="checkbox" id="vCardPreferDisplayName" checked="checked" />
+ <!-- SPAN element needed for fluent string. -->
+ <span></span>
+ </label>
+</template>
+
+<!-- NICKNAME field. -->
+<template id="template-vcard-edit-nickname">
+ <label for="vCardNickName" data-l10n-id="vcard-nickname"></label>
+ <input id="vCardNickName" type="text"/>
+</template>
+
+<!-- Email -->
+<template id="template-vcard-edit-email">
+ <td>
+ <vcard-type></vcard-type>
+ </td>
+ <td class="email-column">
+ <input type="email"
+ aria-labelledby="addr-book-edit-email-label" />
+ </td>
+ <td class="default-column">
+ <input type="checkbox"
+ aria-labelledby="addr-book-edit-email-default" />
+ </td>
+ <td>
+ <button type="button"
+ class="addr-book-edit-fieldset-button remove-property-button icon-button"
+ data-l10n-id="vcard-remove-button-title"></button>
+ </td>
+</template>
+
+<!-- Phone -->
+<template id="template-vcard-edit-tel">
+ <vcard-type></vcard-type>
+ <label class="screen-reader-only" for="text"/>
+ <input type="text"/>
+ <button type="button"
+ class="addr-book-edit-fieldset-button remove-property-button icon-button"
+ data-l10n-id="vcard-remove-button-title"></button>
+</template>
+
+<!-- Field with type and text -->
+<template id="template-vcard-edit-type-text">
+ <vcard-type></vcard-type>
+ <label class="screen-reader-only" for="text"/>
+ <input type="text"/>
+ <button type="button"
+ class="addr-book-edit-fieldset-button remove-property-button icon-button"
+ data-l10n-id="vcard-remove-button-title"></button>
+</template>
+
+<!-- Time Zone -->
+<template id="template-vcard-edit-tz">
+ <select>
+ <option value=""></option>
+ </select>
+ <button type="button"
+ class="addr-book-edit-fieldset-button remove-property-button icon-button"
+ data-l10n-id="vcard-remove-button"></button>
+</template>
+
+<!-- IMPP -->
+<template id="template-vcard-edit-impp">
+ <label class="screen-reader-only" for="protocol" data-l10n-id="vcard-impp-select"></label>
+ <select name="protocol" class="vcard-type-selection">
+ <option value="matrix:u/john:example.org" data-pattern="matrix:.+/.+:.+">Matrix</option>
+ <option value="xmpp:john@example.org" data-pattern="xmpp:.+@.+">XMPP</option>
+ <option value="ircs://irc.example.org/john,isuser" data-pattern="ircs?://.+/.+,.+">IRC</option>
+ <option value="sip:1-555-123-4567@voip.example.org" data-pattern="sip:.+@.+">SIP</option>
+ <option value="skype:johndoe" data-pattern="skype:[A-Za-z\d\-\._]{6,32}">Skype</option>
+ <option value="" data-l10n-id="vcard-impp-option-other" data-pattern="..+:..+"></option>
+ </select>
+ <label class="screen-reader-only" for="impp" data-l10n-id="vcard-impp-input-label"></label>
+ <input type="text" name="impp" pattern="..+:..+" />
+ <button type="button"
+ class="addr-book-edit-fieldset-button remove-property-button icon-button"
+ data-l10n-id="vcard-remove-button-title"></button>
+</template>
+
+<!-- Birthday and Anniversary -->
+<template id="template-vcard-edit-bday-anniversary">
+ <label class="vcard-type-label screen-reader-only"
+ data-l10n-id="vcard-entry-type-label"></label>
+ <select class="vcard-type-selection">
+ <option value="bday" data-l10n-id="vcard-bday-label" selected="selected"/>
+ <option value="anniversary" data-l10n-id="vcard-anniversary-label"/>
+ </select>
+
+ <div class="vcard-year-month-day-container">
+ <label class="screen-reader-only" for="year" data-l10n-id="vcard-date-year"></label>
+ <input id="year" name="year" type="number" min="1000" max="9999" pattern="[0-9]{4}" class="size5" />
+
+ <label class="screen-reader-only" for="month" data-l10n-id="vcard-date-month"></label>
+ <select id="month" name="month" class="vcard-month-select">
+ <option value="" data-l10n-id="vcard-date-month" selected="selected"></option>
+ </select>
+
+ <label class="screen-reader-only" for="day" data-l10n-id="vcard-date-day"></label>
+ <select id="day" name="day" class="vcard-day-select">
+ <option value="" data-l10n-id="vcard-date-day" selected="selected"></option>
+ </select>
+
+ <button type="button"
+ class="addr-book-edit-fieldset-button remove-property-button icon-button"
+ data-l10n-id="vcard-remove-button-title"></button>
+ </div>
+</template>
+
+<!-- Address -->
+<template id="template-vcard-edit-adr">
+ <fieldset class="fieldset-grid fieldset-reset">
+ <legend class="screen-reader-only" data-l10n-id="vcard-adr-label"/>
+ <vcard-type></vcard-type>
+ <div class="vcard-adr-inputs">
+ <label for="street" data-l10n-id="vcard-adr-street"/>
+ <textarea name="street"></textarea>
+ </div>
+ <div class="vcard-adr-inputs">
+ <label for="locality" data-l10n-id="vcard-adr-locality"/>
+ <input type="text" name="locality"/>
+ </div>
+ <div class="vcard-adr-inputs">
+ <label for="region" data-l10n-id="vcard-adr-region"/>
+ <input type="text" name="region"/>
+ </div>
+ <div class="vcard-adr-inputs">
+ <label for="code" data-l10n-id="vcard-adr-code"/>
+ <input type="text" name="code"/>
+ </div>
+ <div class="vcard-adr-inputs">
+ <label for="country" data-l10n-id="vcard-adr-country"/>
+ <input type="text" name="country"/>
+ </div>
+ </fieldset>
+ <button type="button"
+ class="addr-book-edit-fieldset-button remove-property-button icon-button"
+ data-l10n-id="vcard-remove-button"></button>
+</template>
+
+<!-- Notes -->
+<template id="template-vcard-edit-note">
+ <textarea></textarea>
+ <button type="button"
+ class="addr-book-edit-fieldset-button remove-property-button icon-button"
+ data-l10n-id="vcard-remove-button"></button>
+</template>
+
+<!-- Organization Info -->
+<template id="template-vcard-edit-title">
+ <div class="vcard-adr-inputs">
+ <label for="title" data-l10n-id="vcard-org-title"/>
+ <input type="text" data-l10n-id="vcard-org-title-input" name="title" />
+ </div>
+</template>
+<template id="template-vcard-edit-role">
+ <div class="vcard-adr-inputs">
+ <label for="role" data-l10n-id="vcard-org-role"/>
+ <input type="text" data-l10n-id="vcard-org-role-input" name="role" />
+ </div>
+</template>
+<template id="template-vcard-edit-org">
+ <div class="vcard-adr-inputs">
+ <label for="org" data-l10n-id="vcard-org-org" />
+ <input type="text" name="org" data-l10n-id="vcard-org-org-input" />
+ <label for="orgUnit" data-l10n-id="vcard-org-org-unit" class="screen-reader-only"/>
+ <input type="text" name="orgUnit" data-l10n-id="vcard-org-org-unit-input" />
+ </div>
+</template>
+
+<!-- Custom -->
+<template id="template-vcard-edit-custom">
+ <div class="vcard-adr-inputs">
+ <label for="custom1"/>
+ <input type="text"/>
+ </div>
+ <div class="vcard-adr-inputs">
+ <label for="custom2"/>
+ <input type="text"/>
+ </div>
+ <div class="vcard-adr-inputs">
+ <label for="custom3"/>
+ <input type="text"/>
+ </div>
+ <div class="vcard-adr-inputs">
+ <label for="custom4"/>
+ <input type="text"/>
+ </div>
+ <button type="button"
+ class="addr-book-edit-fieldset-button remove-property-button icon-button"
+ data-l10n-id="vcard-remove-button"></button>
+</template>
+
+<template id="template-vcard-edit-type">
+ <select class="vcard-type-selection">
+ <option value="work" data-l10n-id="vcard-entry-type-work"/>
+ <option value="home" data-l10n-id="vcard-entry-type-home"/>
+ <option value="" data-l10n-id="vcard-entry-type-none" selected="selected"/>
+ </select>
+</template>
+
+<template id="template-vcard-edit-type-tel">
+ <select class="vcard-type-selection">
+ <option value="work" data-l10n-id="vcard-entry-type-work"/>
+ <option value="home" data-l10n-id="vcard-entry-type-home"/>
+ <option value="cell" data-l10n-id="vcard-entry-type-cell"/>
+ <option value="fax" data-l10n-id="vcard-entry-type-fax"/>
+ <option value="pager" data-l10n-id="vcard-entry-type-pager"/>
+ <option value="" data-l10n-id="vcard-entry-type-none" selected="selected"/>
+ </select>
+</template>