summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/addrbook/content
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/mailnews/addrbook/content
parentInitial commit. (diff)
downloadthunderbird-upstream.tar.xz
thunderbird-upstream.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/mailnews/addrbook/content')
-rw-r--r--comm/mailnews/addrbook/content/abAddressBookNameDialog.js103
-rw-r--r--comm/mailnews/addrbook/content/abAddressBookNameDialog.xhtml51
-rw-r--r--comm/mailnews/addrbook/content/abCardDAVDialog.js229
-rw-r--r--comm/mailnews/addrbook/content/abCardDAVDialog.xhtml96
-rw-r--r--comm/mailnews/addrbook/content/abCardDAVProperties.js140
-rw-r--r--comm/mailnews/addrbook/content/abCardDAVProperties.xhtml93
-rw-r--r--comm/mailnews/addrbook/content/abDragDrop.js124
-rw-r--r--comm/mailnews/addrbook/content/abMailListDialog.js961
-rw-r--r--comm/mailnews/addrbook/content/abResultsPane.js482
-rw-r--r--comm/mailnews/addrbook/content/abView.js539
-rw-r--r--comm/mailnews/addrbook/content/map-list.js217
11 files changed, 3035 insertions, 0 deletions
diff --git a/comm/mailnews/addrbook/content/abAddressBookNameDialog.js b/comm/mailnews/addrbook/content/abAddressBookNameDialog.js
new file mode 100644
index 0000000000..6c921d9285
--- /dev/null
+++ b/comm/mailnews/addrbook/content/abAddressBookNameDialog.js
@@ -0,0 +1,103 @@
+/* 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/. */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var gOkButton;
+var gNameInput;
+var gDirectory = null;
+
+var kPersonalAddressbookURI = "jsaddrbook://abook.sqlite";
+var kCollectedAddressbookURI = "jsaddrbook://history.sqlite";
+var kAllDirectoryRoot = "moz-abdirectory://";
+
+window.addEventListener("DOMContentLoaded", abNameOnLoad);
+
+function abNameOnLoad() {
+ // Get the document elements.
+ gOkButton = document.querySelector("dialog").getButton("accept");
+ gNameInput = document.getElementById("name");
+
+ // look in arguments[0] for parameters to see if we have a directory or not
+ if (
+ "arguments" in window &&
+ window.arguments[0] &&
+ "selectedDirectory" in window.arguments[0]
+ ) {
+ gDirectory = window.arguments[0].selectedDirectory;
+ gNameInput.value = gDirectory.dirName;
+ }
+
+ // Work out the window title (if we have a directory specified, then it's a
+ // rename).
+ var bundle = document.getElementById("bundle_addressBook");
+
+ if (gDirectory) {
+ let oldListName = gDirectory.dirName;
+ document.title = bundle.getFormattedString("addressBookTitleEdit", [
+ oldListName,
+ ]);
+ } else {
+ document.title = bundle.getString("addressBookTitleNew");
+ }
+
+ if (
+ gDirectory &&
+ (gDirectory.URI == kCollectedAddressbookURI ||
+ gDirectory.URI == kPersonalAddressbookURI ||
+ gDirectory.URI == kAllDirectoryRoot + "?")
+ ) {
+ // Address book name is not editable, therefore disable the field and
+ // only have an ok button that doesn't do anything.
+ gNameInput.readOnly = true;
+ document.querySelector("dialog").buttons = "accept";
+ } else {
+ document.addEventListener("dialogaccept", abNameOKButton);
+ gNameInput.focus();
+ abNameDoOkEnabling();
+ }
+}
+
+function abNameOKButton(event) {
+ let newDirName = gNameInput.value.trim();
+
+ // Do not allow an already existing name.
+ if (
+ MailServices.ab.directoryNameExists(newDirName) &&
+ (!gDirectory || newDirName != gDirectory.dirName)
+ ) {
+ const kAlertTitle = document
+ .getElementById("bundle_addressBook")
+ .getString("duplicateNameTitle");
+ const kAlertText = document
+ .getElementById("bundle_addressBook")
+ .getFormattedString("duplicateNameText", [newDirName]);
+ Services.prompt.alert(window, kAlertTitle, kAlertText);
+ event.preventDefault();
+ return;
+ }
+
+ // Either create a new directory or update an existing one depending on what
+ // we were given when we started.
+ if (gDirectory) {
+ gDirectory.dirName = newDirName;
+ } else {
+ let dirPrefId = MailServices.ab.newAddressBook(
+ newDirName,
+ "",
+ Ci.nsIAbManager.JS_DIRECTORY_TYPE
+ );
+ let directory = MailServices.ab.getDirectoryFromId(dirPrefId);
+ window.arguments[0].newDirectoryUID = directory.UID;
+ if ("onNewDirectory" in window.arguments[0]) {
+ window.arguments[0].onNewDirectory(directory);
+ }
+ }
+}
+
+function abNameDoOkEnabling() {
+ gOkButton.disabled = gNameInput.value.trim() == "";
+}
diff --git a/comm/mailnews/addrbook/content/abAddressBookNameDialog.xhtml b/comm/mailnews/addrbook/content/abAddressBookNameDialog.xhtml
new file mode 100644
index 0000000000..3fc0d7d9e1
--- /dev/null
+++ b/comm/mailnews/addrbook/content/abAddressBookNameDialog.xhtml
@@ -0,0 +1,51 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/addressbook/abAddressBookNameDialog.dtd">
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ scrolling="false"
+ style="min-width: 540px; min-height: 210px"
+>
+ <head>
+ <title><!-- addressBookTitleEdit --></title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/addressbook/abAddressBookNameDialog.js"
+ ></script>
+ </head>
+ <body>
+ <xul:dialog>
+ <xul:stringbundle
+ id="bundle_addressBook"
+ src="chrome://messenger/locale/addressbook/addressBook.properties"
+ />
+
+ <div class="input-container">
+ <label id="nameLabel" for="name">&name.label;</label>
+ <input
+ id="name"
+ type="text"
+ class="input-inline"
+ oninput="abNameDoOkEnabling();"
+ />
+ </div>
+ </xul:dialog>
+ </body>
+</html>
diff --git a/comm/mailnews/addrbook/content/abCardDAVDialog.js b/comm/mailnews/addrbook/content/abCardDAVDialog.js
new file mode 100644
index 0000000000..8ccce89680
--- /dev/null
+++ b/comm/mailnews/addrbook/content/abCardDAVDialog.js
@@ -0,0 +1,229 @@
+/* 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/. */
+
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ CardDAVUtils: "resource:///modules/CardDAVUtils.jsm",
+ MailServices: "resource:///modules/MailServices.jsm",
+});
+
+var log = console.createInstance({
+ prefix: "carddav.setup",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "carddav.setup.loglevel",
+});
+
+var oAuth = null;
+var callbacks = null;
+var uiElements = {};
+var userContextId;
+
+window.addEventListener(
+ "DOMContentLoaded",
+ () => {
+ for (let id of [
+ "username",
+ "location",
+ "statusArea",
+ "statusImage",
+ "statusMessage",
+ "resultsArea",
+ "availableBooks",
+ ]) {
+ uiElements[id] = document.getElementById("carddav-" + id);
+ }
+ },
+ { once: true }
+);
+window.addEventListener(
+ "DOMContentLoaded",
+ async () => {
+ await document.l10n.translateRoots();
+ fillLocationPlaceholder();
+ setStatus();
+ },
+ { once: true }
+);
+
+/**
+ * Update the placeholder text for the network location field. If the username
+ * is a valid email address use the domain part of the username, otherwise use
+ * the default placeholder.
+ */
+function fillLocationPlaceholder() {
+ let parts = uiElements.username.value.split("@");
+ let domain = parts.length == 2 && parts[1] ? parts[1] : null;
+
+ if (domain) {
+ uiElements.location.setAttribute("placeholder", domain);
+ } else {
+ uiElements.location.setAttribute(
+ "placeholder",
+ uiElements.location.getAttribute("default-placeholder")
+ );
+ }
+}
+
+function handleCardDAVURLInput(event) {
+ changeCardDAVURL();
+}
+
+function changeCardDAVURL() {
+ uiElements.resultsArea.hidden = true;
+ setStatus();
+}
+
+function handleCardDAVURLBlur(event) {
+ if (
+ uiElements.location.validity.typeMismatch &&
+ !uiElements.location.value.match(/^https?:\/\//)
+ ) {
+ uiElements.location.value = `https://${uiElements.location.value}`;
+ }
+}
+
+async function check() {
+ // We might be accepting the dialog by pressing Enter in the URL input.
+ handleCardDAVURLBlur();
+
+ let username = uiElements.username.value;
+
+ if (!uiElements.location.validity.valid && !username.split("@")[1]) {
+ log.error(`Invalid URL: "${uiElements.location.value}"`);
+ return;
+ }
+
+ let url = uiElements.location.value || username.split("@")[1];
+ if (!url.match(/^https?:\/\//)) {
+ url = "https://" + url;
+ }
+
+ setStatus("loading", "carddav-loading");
+ while (uiElements.availableBooks.lastChild) {
+ uiElements.availableBooks.lastChild.remove();
+ }
+
+ let foundBooks;
+ try {
+ foundBooks = await CardDAVUtils.detectAddressBooks(
+ username,
+ undefined,
+ url,
+ true
+ );
+ } catch (ex) {
+ if (ex.result == Cr.NS_ERROR_NOT_AVAILABLE) {
+ setStatus("error", "carddav-known-incompatible", {
+ url: new URL(url).hostname,
+ });
+ } else {
+ log.error(ex);
+ setStatus("error", "carddav-connection-error");
+ }
+ return;
+ }
+
+ // Create a list of CardDAV directories that already exist.
+ let existing = [];
+ for (let d of MailServices.ab.directories) {
+ if (d.dirType == Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE) {
+ existing.push(d.getStringValue("carddav.url", ""));
+ }
+ }
+
+ // Display a checkbox for each directory that doesn't already exist.
+ let alreadyAdded = 0;
+ for (let book of foundBooks) {
+ if (existing.includes(book.url.href)) {
+ alreadyAdded++;
+ continue;
+ }
+ let checkbox = uiElements.availableBooks.appendChild(
+ document.createXULElement("checkbox")
+ );
+ checkbox.setAttribute("label", book.name);
+ checkbox.checked = true;
+ checkbox.value = book.url.href;
+ checkbox._book = book;
+ }
+
+ if (uiElements.availableBooks.childElementCount == 0) {
+ if (alreadyAdded > 0) {
+ setStatus("error", "carddav-already-added");
+ } else {
+ setStatus("error", "carddav-none-found");
+ }
+ } else {
+ uiElements.resultsArea.hidden = false;
+ setStatus();
+ }
+}
+
+function setStatus(status, message, args) {
+ uiElements.username.disabled = status == "loading";
+ uiElements.location.disabled = status == "loading";
+
+ switch (status) {
+ case "loading":
+ uiElements.statusImage.setAttribute(
+ "src",
+ "chrome://global/skin/icons/loading.png"
+ );
+ uiElements.statusImage.setAttribute(
+ "srcset",
+ "chrome://global/skin/icons/loading@2x.png 2x"
+ );
+ break;
+ case "error":
+ uiElements.statusImage.setAttribute(
+ "src",
+ "chrome://global/skin/icons/warning.svg"
+ );
+ uiElements.statusImage.removeAttribute("srcset");
+ break;
+ default:
+ uiElements.statusImage.removeAttribute("src");
+ uiElements.statusImage.removeAttribute("srcset");
+ break;
+ }
+
+ if (status) {
+ uiElements.statusArea.setAttribute("status", status);
+ document.l10n.setAttributes(uiElements.statusMessage, message, args);
+ } else {
+ uiElements.statusArea.removeAttribute("status");
+ uiElements.statusMessage.removeAttribute("data-l10n-id");
+ uiElements.statusMessage.textContent = "";
+ }
+
+ // Grow to fit the list of books. Uses `resizeBy` because it has special
+ // handling in SubDialog.jsm that the other resize functions don't have.
+ window.resizeBy(0, Math.min(250, uiElements.availableBooks.scrollHeight));
+ window.dispatchEvent(new CustomEvent("status-changed"));
+}
+
+window.addEventListener("dialogaccept", event => {
+ if (uiElements.resultsArea.hidden) {
+ event.preventDefault();
+ check();
+ return;
+ }
+
+ if (uiElements.availableBooks.childElementCount == 0) {
+ return;
+ }
+
+ for (let checkbox of uiElements.availableBooks.children) {
+ if (checkbox.checked) {
+ let book = checkbox._book.create();
+ if (window.arguments[0]) {
+ // Pass the UID of the book back to the opening window.
+ window.arguments[0].newDirectoryUID = book.UID;
+ }
+ }
+ }
+});
diff --git a/comm/mailnews/addrbook/content/abCardDAVDialog.xhtml b/comm/mailnews/addrbook/content/abCardDAVDialog.xhtml
new file mode 100644
index 0000000000..9e0fc5e1e4
--- /dev/null
+++ b/comm/mailnews/addrbook/content/abCardDAVDialog.xhtml
@@ -0,0 +1,96 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/shared/cardDAV.css" type="text/css"?>
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ scrolling="false"
+ style="min-width: 540px; min-height: 210px"
+>
+ <head>
+ <title data-l10n-id="carddav-window-title"></title>
+ <link rel="localization" href="branding/brand.ftl" />
+ <link rel="localization" href="messenger/addressbook/abCardDAVDialog.ftl" />
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/addressbook/abCardDAVDialog.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog
+ id="carddav-dialog"
+ data-l10n-id="carddav-dialog"
+ data-l10n-attrs="buttonlabelaccept,buttonaccesskeyaccept"
+ >
+ <html:div id="carddav-properties-table">
+ <html:div>
+ <label
+ control="carddav-username"
+ data-l10n-id="carddav-username-label"
+ data-l10n-attrs="value,accesskey"
+ />
+ </html:div>
+ <html:div class="input-container">
+ <html:input
+ id="carddav-username"
+ type="text"
+ oninput="fillLocationPlaceholder();"
+ />
+ </html:div>
+ <html:div>
+ <label
+ control="carddav-location"
+ data-l10n-id="carddav-location-label"
+ data-l10n-attrs="value,accesskey"
+ />
+ </html:div>
+ <html:div class="input-container">
+ <html:input
+ id="carddav-location"
+ type="url"
+ data-l10n-id="carddav-location"
+ data-l10n-attrs="default-placeholder"
+ required="required"
+ onblur="handleCardDAVURLBlur(event);"
+ oninput="handleCardDAVURLInput(event);"
+ />
+ </html:div>
+ </html:div>
+
+ <html:div id="carddav-statusArea">
+ <html:div id="carddav-statusContainer">
+ <html:img id="carddav-statusImage" alt="" />
+ <html:span id="carddav-statusMessage">&#160;</html:span>
+ <!-- Include 160 = nbsp, to make the element occupy the
+ full height, for at least one line. With a normal space,
+ it does not have sufficient height. -->
+ </html:div>
+ </html:div>
+
+ <vbox id="carddav-resultsArea" hidden="true" flex="1">
+ <label
+ id="carddav-availableBooksHeader"
+ data-l10n-id="carddav-available-books"
+ />
+ <vbox id="carddav-availableBooks" class="indent"></vbox>
+ </vbox>
+ </dialog>
+ </html:body>
+</html>
diff --git a/comm/mailnews/addrbook/content/abCardDAVProperties.js b/comm/mailnews/addrbook/content/abCardDAVProperties.js
new file mode 100644
index 0000000000..3b2bddf2c4
--- /dev/null
+++ b/comm/mailnews/addrbook/content/abCardDAVProperties.js
@@ -0,0 +1,140 @@
+/* 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/. */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var gDirectory = window.arguments[0].selectedDirectory;
+var gStringBundle,
+ gNameInput,
+ gURLInput,
+ gRefreshActiveInput,
+ gRefreshMenulist,
+ gReadOnlyInput,
+ gAcceptButton;
+
+window.addEventListener(
+ "DOMContentLoaded",
+ () => {
+ gStringBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/addressbook/addressBook.properties"
+ );
+ document.title = gStringBundle.formatStringFromName(
+ "addressBookTitleEdit",
+ [gDirectory.dirName]
+ );
+
+ gNameInput = document.getElementById("carddav-name");
+ gNameInput.value = gDirectory.dirName;
+ gNameInput.addEventListener("input", () => {
+ gAcceptButton.disabled = gNameInput.value.trim() == "";
+ });
+
+ gURLInput = document.getElementById("carddav-url");
+ gURLInput.value = gDirectory.getStringValue("carddav.url", "");
+
+ gRefreshActiveInput = document.getElementById("carddav-refreshActive");
+ gRefreshActiveInput.addEventListener(
+ "command",
+ () => (gRefreshMenulist.disabled = !gRefreshActiveInput.checked)
+ );
+
+ gRefreshMenulist = document.getElementById("carddav-refreshInterval");
+ initRefreshInterval();
+
+ gReadOnlyInput = document.getElementById("carddav-readOnly");
+ gReadOnlyInput.checked = gDirectory.readOnly;
+
+ gAcceptButton = document.querySelector("dialog").getButton("accept");
+ },
+ { once: true }
+);
+
+window.addEventListener("dialogaccept", event => {
+ let newDirName = gNameInput.value.trim();
+ let newSyncInterval = gRefreshActiveInput.checked
+ ? gRefreshMenulist.value
+ : 0;
+
+ if (newDirName != gDirectory.dirName) {
+ // Do not allow an already existing name.
+ if (MailServices.ab.directoryNameExists(newDirName)) {
+ let alertTitle = gStringBundle.GetStringFromName("duplicateNameTitle");
+ let alertText = gStringBundle.formatStringFromName("duplicateNameText", [
+ newDirName,
+ ]);
+ Services.prompt.alert(window, alertTitle, alertText);
+ event.preventDefault();
+ return;
+ }
+
+ gDirectory.dirName = newDirName;
+ }
+
+ if (newSyncInterval != gDirectory.getIntValue("carddav.syncinterval", -1)) {
+ gDirectory.setIntValue("carddav.syncinterval", newSyncInterval);
+ }
+
+ if (gReadOnlyInput.checked != gDirectory.readOnly) {
+ gDirectory.setBoolValue("readOnly", gReadOnlyInput.checked);
+ }
+});
+
+function initRefreshInterval() {
+ function createMenuItem(minutes) {
+ let menuitem = document.createXULElement("menuitem");
+ menuitem.setAttribute("value", minutes);
+ menuitem.setAttribute("data-l10n-attrs", "label");
+ if (minutes < 60) {
+ document.l10n.setAttributes(
+ menuitem,
+ "carddav-refreshinterval-minutes-value",
+ {
+ minutes,
+ }
+ );
+ } else {
+ document.l10n.setAttributes(
+ menuitem,
+ "carddav-refreshinterval-hours-value",
+ {
+ hours: minutes / 60,
+ }
+ );
+ }
+
+ gRefreshMenulist.menupopup.appendChild(menuitem);
+ if (refreshInterval == minutes) {
+ gRefreshMenulist.value = minutes;
+ foundValue = true;
+ }
+
+ return menuitem;
+ }
+
+ let refreshInterval = gDirectory.getIntValue("carddav.syncinterval", 30);
+ if (refreshInterval === null) {
+ refreshInterval = 30;
+ }
+
+ let foundValue = false;
+
+ for (let min of [1, 5, 15, 30, 60, 120, 240, 360, 720, 1440]) {
+ createMenuItem(min);
+ }
+
+ if (refreshInterval == 0) {
+ gRefreshMenulist.value = 30; // The default.
+ gRefreshMenulist.disabled = true;
+ foundValue = true;
+ } else {
+ gRefreshActiveInput.checked = true;
+ }
+
+ if (!foundValue) {
+ // Special menuitem in case the user changed the value in the config editor.
+ createMenuItem(refreshInterval);
+ }
+}
diff --git a/comm/mailnews/addrbook/content/abCardDAVProperties.xhtml b/comm/mailnews/addrbook/content/abCardDAVProperties.xhtml
new file mode 100644
index 0000000000..b611cbbf43
--- /dev/null
+++ b/comm/mailnews/addrbook/content/abCardDAVProperties.xhtml
@@ -0,0 +1,93 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/shared/cardDAV.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/addressbook/abAddressBookNameDialog.dtd">
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ width="540"
+ height="210"
+ scrolling="false"
+>
+ <head>
+ <title><!-- addressBookTitleEdit --></title>
+ <link
+ rel="localization"
+ href="messenger/addressbook/abCardDAVProperties.ftl"
+ />
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/addressbook/abCardDAVProperties.js"
+ ></script>
+ </head>
+ <body>
+ <xul:dialog id="carddav-properties-dialog">
+ <div id="carddav-properties-table">
+ <div>
+ <xul:label
+ control="carddav-name"
+ value="&name.label;"
+ accesskey="&name.accesskey;"
+ />
+ </div>
+ <div class="input-container">
+ <input id="carddav-name" type="text" class="input-inline" />
+ </div>
+ <div>
+ <xul:label
+ data-l10n-id="carddav-url-label"
+ data-l10n-attrs="value,accesskey"
+ control="carddav-url"
+ />
+ </div>
+ <div class="input-container">
+ <input
+ id="carddav-url"
+ type="url"
+ class="input-inline"
+ readonly="readonly"
+ />
+ </div>
+ <div id="carddav-refreshActive-cell">
+ <xul:checkbox
+ id="carddav-refreshActive"
+ data-l10n-id="carddav-refreshinterval-label"
+ data-l10n-attrs="label,accesskey"
+ control="carddav-refreshInterval"
+ />
+ </div>
+ <div id="carddav-refreshInterval-cell">
+ <xul:menulist id="carddav-refreshInterval">
+ <xul:menupopup>
+ <!-- This will be filled programmatically to reduce the number of needed strings -->
+ </xul:menupopup>
+ </xul:menulist>
+ </div>
+ <div></div>
+ <div>
+ <xul:checkbox
+ id="carddav-readOnly"
+ data-l10n-id="carddav-readonly-label"
+ data-l10n-attrs="label,accesskey"
+ />
+ </div>
+ </div>
+ </xul:dialog>
+ </body>
+</html>
diff --git a/comm/mailnews/addrbook/content/abDragDrop.js b/comm/mailnews/addrbook/content/abDragDrop.js
new file mode 100644
index 0000000000..56cbee0e08
--- /dev/null
+++ b/comm/mailnews/addrbook/content/abDragDrop.js
@@ -0,0 +1,124 @@
+/* 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-globals-from abResultsPane.js */
+
+// Returns the load context for the current window
+function getLoadContext() {
+ return window.docShell.QueryInterface(Ci.nsILoadContext);
+}
+
+var abFlavorDataProvider = {
+ QueryInterface: ChromeUtils.generateQI(["nsIFlavorDataProvider"]),
+
+ getFlavorData(aTransferable, aFlavor, aData) {
+ if (aFlavor == "application/x-moz-file-promise") {
+ var primitive = {};
+ aTransferable.getTransferData("text/vcard", primitive);
+ var vCard = primitive.value.QueryInterface(Ci.nsISupportsString).data;
+ aTransferable.getTransferData(
+ "application/x-moz-file-promise-dest-filename",
+ primitive
+ );
+ var leafName = primitive.value.QueryInterface(Ci.nsISupportsString).data;
+ aTransferable.getTransferData(
+ "application/x-moz-file-promise-dir",
+ primitive
+ );
+ var localFile = primitive.value.QueryInterface(Ci.nsIFile).clone();
+ localFile.append(leafName);
+
+ var ofStream = Cc[
+ "@mozilla.org/network/file-output-stream;1"
+ ].createInstance(Ci.nsIFileOutputStream);
+ ofStream.init(localFile, -1, -1, 0);
+ var converter = Cc[
+ "@mozilla.org/intl/converter-output-stream;1"
+ ].createInstance(Ci.nsIConverterOutputStream);
+ converter.init(ofStream, null);
+ converter.writeString(vCard);
+ converter.close();
+
+ aData.value = localFile;
+ }
+ },
+};
+
+let abResultsPaneObserver = {
+ onDragStart(event) {
+ let selectedRows = GetSelectedRows();
+
+ if (!selectedRows) {
+ return;
+ }
+
+ let selectedAddresses = GetSelectedAddresses();
+
+ event.dataTransfer.setData("moz/abcard", selectedRows);
+ event.dataTransfer.setData("moz/abcard", selectedRows);
+ event.dataTransfer.setData("text/x-moz-address", selectedAddresses);
+ event.dataTransfer.setData("text/plain", selectedAddresses);
+
+ let card = GetSelectedCard();
+ if (card && card.displayName && !card.isMailList) {
+ try {
+ // A card implementation may throw NS_ERROR_NOT_IMPLEMENTED.
+ // Don't break drag-and-drop if that happens.
+ let vCard = card.translateTo("vcard");
+ event.dataTransfer.setData("text/vcard", decodeURIComponent(vCard));
+ event.dataTransfer.setData(
+ "application/x-moz-file-promise-dest-filename",
+ `${card.displayName}.vcf`.replace(/(.{74}).*(.{10})$/u, "$1...$2")
+ );
+ event.dataTransfer.setData(
+ "application/x-moz-file-promise-url",
+ "data:text/vcard," + vCard
+ );
+ event.dataTransfer.setData(
+ "application/x-moz-file-promise",
+ abFlavorDataProvider
+ );
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+
+ event.dataTransfer.effectAllowed = "copyMove";
+ // a drag targeted at a tree should instead use the treechildren so that
+ // the current selection is used as the drag feedback
+ event.dataTransfer.addElement(event.target);
+ event.stopPropagation();
+ },
+};
+
+function DragAddressOverTargetControl(event) {
+ var dragSession = Cc["@mozilla.org/widget/dragservice;1"]
+ .getService(Ci.nsIDragService)
+ .getCurrentSession();
+
+ if (!dragSession.isDataFlavorSupported("text/x-moz-address")) {
+ return;
+ }
+
+ var trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+ Ci.nsITransferable
+ );
+ trans.init(getLoadContext());
+ trans.addDataFlavor("text/x-moz-address");
+
+ var canDrop = true;
+
+ for (var i = 0; i < dragSession.numDropItems; ++i) {
+ dragSession.getData(trans, i);
+ var dataObj = {};
+ var bestFlavor = {};
+ try {
+ trans.getAnyTransferData(bestFlavor, dataObj);
+ } catch (ex) {
+ canDrop = false;
+ break;
+ }
+ }
+ dragSession.canDrop = canDrop;
+}
diff --git a/comm/mailnews/addrbook/content/abMailListDialog.js b/comm/mailnews/addrbook/content/abMailListDialog.js
new file mode 100644
index 0000000000..6d512d3014
--- /dev/null
+++ b/comm/mailnews/addrbook/content/abMailListDialog.js
@@ -0,0 +1,961 @@
+/* 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-globals-from ../../../mail/components/addrbook/content/abCommon.js */
+/* import-globals-from ../../../mail/components/compose/content/addressingWidgetOverlay.js */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+top.MAX_RECIPIENTS = 1;
+
+var gListCard;
+var gEditList;
+var gOldListName = "";
+
+var gAWContentHeight = 0;
+var gAWRowHeight = 0;
+var gNumberOfCols = 0;
+
+var test_addresses_sequence = false;
+
+if (
+ Services.prefs.getPrefType("mail.debug.test_addresses_sequence") ==
+ Ci.nsIPrefBranch.PREF_BOOL
+) {
+ test_addresses_sequence = Services.prefs.getBoolPref(
+ "mail.debug.test_addresses_sequence"
+ );
+}
+
+try {
+ var gDragService = Cc["@mozilla.org/widget/dragservice;1"].getService(
+ Ci.nsIDragService
+ );
+} catch (e) {}
+
+// Returns the load context for the current window
+function getLoadContext() {
+ return window.docShell.QueryInterface(Ci.nsILoadContext);
+}
+
+function mailingListExists(listname) {
+ if (MailServices.ab.mailListNameExists(listname)) {
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/addressbook/addressBook.properties"
+ );
+ Services.prompt.alert(
+ window,
+ bundle.GetStringFromName("mailListNameExistsTitle"),
+ bundle.GetStringFromName("mailListNameExistsMessage")
+ );
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Get the new inputs from the create/edit mailing list dialog and use them to
+ * update the mailing list that was passed in as an argument.
+ *
+ * @param {nsIAbDirectory} mailList - The mailing list object to update. When
+ * creating a new list it will be newly created and empty.
+ * @param {boolean} isNewList - Whether we are populating a new list.
+ * @returns {boolean} - Whether the operation succeeded or not.
+ */
+function updateMailList(mailList, isNewList) {
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/addressbook/addressBook.properties"
+ );
+ let listname = document.getElementById("ListName").value.trim();
+
+ if (listname.length == 0) {
+ alert(bundle.GetStringFromName("emptyListName"));
+ return false;
+ }
+
+ if (listname.match(" ")) {
+ alert(bundle.GetStringFromName("badListNameSpaces"));
+ return false;
+ }
+
+ for (let char of ',;"<>') {
+ if (listname.includes(char)) {
+ alert(bundle.GetStringFromName("badListNameCharacters"));
+ return false;
+ }
+ }
+
+ let canonicalNewListName = listname.toLowerCase();
+ let canonicalOldListName = gOldListName.toLowerCase();
+ if (isNewList || canonicalOldListName != canonicalNewListName) {
+ if (mailingListExists(listname)) {
+ // After showing the "Mailing List Already Exists" error alert,
+ // focus ListName input field for user to choose a different name.
+ document.getElementById("ListName").focus();
+ return false;
+ }
+ }
+
+ mailList.isMailList = true;
+ mailList.dirName = listname;
+ mailList.listNickName = document.getElementById("ListNickName").value;
+ mailList.description = document.getElementById("ListDescription").value;
+
+ return true;
+}
+
+/**
+ * Updates the members of the mailing list.
+ *
+ * @param {nsIAbDirectory} mailList - The mailing list object to
+ * update. When creating a new list it will be newly created and empty.
+ * @param {nsIAbDirectory} parentDirectory - The address book containing the
+ * mailing list.
+ */
+function updateMailListMembers(mailList, parentDirectory) {
+ // Gather email address inputs into a single string (comma-separated).
+ let addresses = Array.from(
+ document.querySelectorAll(".textbox-addressingWidget"),
+ element => element.value
+ )
+ .filter(value => value.trim())
+ .join();
+
+ // Convert the addresses string into address objects.
+ let addressObjects =
+ MailServices.headerParser.makeFromDisplayAddress(addresses);
+ let existingCards = mailList.childCards;
+
+ // Work out which addresses need to be added...
+ let existingCardAddresses = existingCards.map(card => card.primaryEmail);
+ let addressObjectsToAdd = addressObjects.filter(
+ aObj => !existingCardAddresses.includes(aObj.email)
+ );
+
+ // ... and which need to be removed.
+ let addressObjectAddresses = addressObjects.map(aObj => aObj.email);
+ let cardsToRemove = existingCards.filter(
+ card => !addressObjectAddresses.includes(card.primaryEmail)
+ );
+
+ for (let { email, name } of addressObjectsToAdd) {
+ let card = parentDirectory.cardForEmailAddress(email);
+ if (!card) {
+ card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ card.primaryEmail = email;
+ card.displayName = name || email;
+ }
+ mailList.addCard(card);
+ }
+
+ if (cardsToRemove.length > 0) {
+ mailList.deleteCards(cardsToRemove);
+ }
+}
+
+function MailListOKButton(event) {
+ var popup = document.getElementById("abPopup");
+ if (popup) {
+ var uri = popup.getAttribute("value");
+
+ // FIX ME - hack to avoid crashing if no ab selected because of blank option bug from template
+ // should be able to just remove this if we are not seeing blank lines in the ab popup
+ if (!uri) {
+ event.preventDefault();
+ return; // don't close window
+ }
+ // -----
+
+ // Add mailing list to database
+ var mailList =
+ Cc["@mozilla.org/addressbook/directoryproperty;1"].createInstance();
+ mailList = mailList.QueryInterface(Ci.nsIAbDirectory);
+
+ if (updateMailList(mailList, true)) {
+ var parentDirectory = GetDirectoryFromURI(uri);
+ mailList = parentDirectory.addMailList(mailList);
+ updateMailListMembers(mailList, parentDirectory);
+ window.arguments[0].newListUID = mailList.UID;
+ window.arguments[0].newListURI = mailList.URI;
+ } else {
+ event.preventDefault();
+ }
+ }
+}
+
+function OnLoadNewMailList() {
+ var selectedAB = null;
+
+ if ("arguments" in window && window.arguments[0]) {
+ var abURI = window.arguments[0].selectedAB;
+ if (abURI && abURI != kAllDirectoryRoot + "?") {
+ var directory = GetDirectoryFromURI(abURI);
+ if (directory.isMailList) {
+ var parentURI = GetParentDirectoryFromMailingListURI(abURI);
+ if (parentURI) {
+ selectedAB = parentURI;
+ }
+ } else if (directory.readOnly) {
+ selectedAB = kPersonalAddressbookURI;
+ } else {
+ selectedAB = abURI;
+ }
+ }
+
+ let cards = window.arguments[0].cards;
+ if (cards && cards.length > 0) {
+ let listbox = document.getElementById("addressingWidget");
+ let newListBoxNode = listbox.cloneNode(false);
+ let templateNode = listbox.querySelector("richlistitem");
+
+ top.MAX_RECIPIENTS = 0;
+ for (let card of cards) {
+ let address = MailServices.headerParser
+ .makeMailboxObject(card.displayName, card.primaryEmail)
+ .toString();
+ SetInputValue(address, newListBoxNode, templateNode);
+ }
+ listbox.parentNode.replaceChild(newListBoxNode, listbox);
+ }
+ }
+
+ if (!selectedAB) {
+ selectedAB = kPersonalAddressbookURI;
+ }
+
+ // set popup with address book names
+ var abPopup = document.getElementById("abPopup");
+ abPopup.value = selectedAB;
+
+ AppendNewRowAndSetFocus();
+ awFitDummyRows(1);
+
+ if (AppConstants.MOZ_APP_NAME == "seamonkey") {
+ /* global awDocumentKeyPress */
+ document.addEventListener("keypress", awDocumentKeyPress, true);
+ }
+
+ // focus on first name
+ var listName = document.getElementById("ListName");
+ if (listName) {
+ setTimeout(
+ function (firstTextBox) {
+ firstTextBox.focus();
+ },
+ 0,
+ listName
+ );
+ }
+
+ let input = document.getElementById("addressCol1#1");
+ input.popup.addEventListener("click", () => {
+ awReturnHit(input);
+ });
+
+ document.addEventListener("dialogaccept", MailListOKButton);
+}
+
+function EditListOKButton(event) {
+ // edit mailing list in database
+ if (updateMailList(gEditList, false)) {
+ let parentURI = GetParentDirectoryFromMailingListURI(gEditList.URI);
+ let parentDirectory = GetDirectoryFromURI(parentURI);
+ updateMailListMembers(gEditList, parentDirectory);
+ if (gListCard) {
+ // modify the list card (for the results pane) from the mailing list
+ gListCard.displayName = gEditList.dirName;
+ gListCard.lastName = gEditList.dirName;
+ gListCard.setProperty("NickName", gEditList.listNickName);
+ gListCard.setProperty("Notes", gEditList.description);
+ }
+
+ gEditList.editMailListToDatabase(gListCard);
+
+ window.arguments[0].refresh = true;
+ return; // close the window
+ }
+ event.preventDefault();
+}
+
+function OnLoadEditList() {
+ gListCard = window.arguments[0].abCard;
+ var listUri = window.arguments[0].listURI;
+
+ gEditList = GetDirectoryFromURI(listUri);
+
+ document.getElementById("ListName").value = gEditList.dirName;
+ document.getElementById("ListNickName").value = gEditList.listNickName;
+ document.getElementById("ListDescription").value = gEditList.description;
+ gOldListName = gEditList.dirName;
+
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/addressbook/addressBook.properties"
+ );
+ document.title = bundle.formatStringFromName("mailingListTitleEdit", [
+ gOldListName,
+ ]);
+
+ let cards = gEditList.childCards;
+ if (cards.length > 0) {
+ let listbox = document.getElementById("addressingWidget");
+ let newListBoxNode = listbox.cloneNode(false);
+ let templateNode = listbox.querySelector("richlistitem");
+
+ top.MAX_RECIPIENTS = 0;
+ for (let card of cards) {
+ let address = MailServices.headerParser
+ .makeMailboxObject(card.displayName, card.primaryEmail)
+ .toString();
+ SetInputValue(address, newListBoxNode, templateNode);
+ }
+ listbox.parentNode.replaceChild(newListBoxNode, listbox);
+ }
+
+ // Is this directory read-only? If so, we now need to set all the fields to
+ // read-only.
+ if (gEditList.readOnly) {
+ const kMailListFields = ["ListName", "ListNickName", "ListDescription"];
+
+ for (let i = 0; i < kMailListFields.length; ++i) {
+ document.getElementById(kMailListFields[i]).readOnly = true;
+ }
+
+ document.querySelector("dialog").buttons = "accept";
+
+ // Getting a sane read-only implementation for the addressing widget would
+ // basically need a separate dialog. Given I'm not sure about the future of
+ // the mailing list dialog in its current state, let's just disable it
+ // completely.
+ document.getElementById("addressingWidget").disabled = true;
+ } else {
+ document.addEventListener("dialogaccept", EditListOKButton);
+ }
+
+ if (AppConstants.MOZ_APP_NAME == "seamonkey") {
+ document.addEventListener("keypress", awDocumentKeyPress, true);
+ }
+
+ // workaround for bug 118337 - for mailing lists that have more rows than fits inside
+ // the display, the value of the textbox inside the new row isn't inherited into the input -
+ // the first row then appears to be duplicated at the end although it is actually empty.
+ // see awAppendNewRow which copies first row and clears it
+ setTimeout(AppendLastRow, 0);
+
+ document.querySelectorAll(`input[is="autocomplete-input"]`).forEach(input => {
+ input.popup.addEventListener("click", () => {
+ awReturnHit(input);
+ });
+ });
+}
+
+function AppendLastRow() {
+ AppendNewRowAndSetFocus();
+ awFitDummyRows(1);
+
+ // focus on first name
+ let listName = document.getElementById("ListName");
+ if (listName) {
+ listName.focus();
+ }
+}
+
+function AppendNewRowAndSetFocus() {
+ let lastInput = awGetInputElement(top.MAX_RECIPIENTS);
+ if (lastInput && lastInput.value) {
+ awAppendNewRow(true);
+ } else {
+ awSetFocusTo(lastInput);
+ }
+}
+
+function SetInputValue(inputValue, parentNode, templateNode) {
+ top.MAX_RECIPIENTS++;
+
+ var newNode = templateNode.cloneNode(true);
+ parentNode.appendChild(newNode); // we need to insert the new node before we set the value of the select element!
+
+ var input = newNode.querySelector(`input[is="autocomplete-input"]`);
+ let label = newNode.querySelector(`label.person-icon`);
+ if (input) {
+ input.value = inputValue;
+ input.setAttribute("id", "addressCol1#" + top.MAX_RECIPIENTS);
+ label.setAttribute("for", "addressCol1#" + top.MAX_RECIPIENTS);
+ input.popup.addEventListener("click", () => {
+ awReturnHit(input);
+ });
+ }
+}
+
+function awClickEmptySpace(target, setFocus) {
+ if (target == null || target.localName != "hbox") {
+ return;
+ }
+
+ let lastInput = awGetInputElement(top.MAX_RECIPIENTS);
+
+ if (lastInput && lastInput.value) {
+ awAppendNewRow(setFocus);
+ } else if (setFocus) {
+ awSetFocusTo(lastInput);
+ }
+}
+
+function awReturnHit(inputElement) {
+ let row = awGetRowByInputElement(inputElement);
+ if (inputElement.value) {
+ let nextInput = awGetInputElement(row + 1);
+ if (!nextInput) {
+ awAppendNewRow(true);
+ } else {
+ awSetFocusTo(nextInput);
+ }
+ }
+}
+
+function awDeleteRow(rowToDelete) {
+ /* When we delete a row, we must reset the id of others row in order to not break the sequence */
+ var maxRecipients = top.MAX_RECIPIENTS;
+ awRemoveRow(rowToDelete);
+
+ var numberOfCols = awGetNumberOfCols();
+ for (var row = rowToDelete + 1; row <= maxRecipients; row++) {
+ for (var col = 1; col <= numberOfCols; col++) {
+ awGetElementByCol(row, col).setAttribute(
+ "id",
+ "addressCol" + col + "#" + (row - 1)
+ );
+ }
+ }
+
+ awTestRowSequence();
+}
+
+/**
+ * Append a new row.
+ *
+ * @param {boolean} setFocus - Whether to set the focus on the new row.
+ * @returns {Element?} The input element from the new row.
+ */
+function awAppendNewRow(setFocus) {
+ let body = document.getElementById("addressingWidget");
+ let listitem1 = awGetListItem(1);
+ let input;
+ let label;
+
+ if (body && listitem1) {
+ let nextDummy = awGetNextDummyRow();
+ let newNode = listitem1.cloneNode(true);
+ if (nextDummy) {
+ body.replaceChild(newNode, nextDummy);
+ } else {
+ body.appendChild(newNode);
+ }
+
+ top.MAX_RECIPIENTS++;
+
+ input = newNode.querySelector(`input[is="autocomplete-input"]`);
+ label = newNode.querySelector(`label.person-icon`);
+ if (input) {
+ input.value = "";
+ input.setAttribute("id", "addressCol1#" + top.MAX_RECIPIENTS);
+ label.setAttribute("for", "addressCol1#" + top.MAX_RECIPIENTS);
+ input.popup.addEventListener("click", () => {
+ awReturnHit(input);
+ });
+ }
+ // Focus the new input widget.
+ if (setFocus && input) {
+ awSetFocusTo(input);
+ }
+ }
+ return input;
+}
+
+// functions for accessing the elements in the addressing widget
+
+/**
+ * Returns the recipient inputbox for a row.
+ *
+ * @param {integer} row - Index of the recipient row to return. Starts at 1.
+ * @returns {Element} This returns the input element.
+ */
+function awGetInputElement(row) {
+ return document.getElementById("addressCol1#" + row);
+}
+
+function awGetElementByCol(row, col) {
+ var colID = "addressCol" + col + "#" + row;
+ return document.getElementById(colID);
+}
+
+function awGetListItem(row) {
+ var listbox = document.getElementById("addressingWidget");
+ if (listbox && row > 0) {
+ return listbox.getItemAtIndex(row - 1);
+ }
+
+ return null;
+}
+
+/**
+ * @param {Element} inputElement - The recipient input element.
+ * @returns {integer} The row index (starting from 1) where the input element
+ * is found. 0 if the element is not found.
+ */
+function awGetRowByInputElement(inputElement) {
+ if (!inputElement) {
+ return 0;
+ }
+
+ var listitem = inputElement.parentNode.parentNode;
+ return (
+ document.getElementById("addressingWidget").getIndexOfItem(listitem) + 1
+ );
+}
+
+function DragOverAddressListTree(event) {
+ var dragSession = gDragService.getCurrentSession();
+
+ // XXX add support for other flavors here
+ if (dragSession.isDataFlavorSupported("text/x-moz-address")) {
+ dragSession.canDrop = true;
+ }
+}
+
+function DropOnAddressListTree(event) {
+ let dragSession = gDragService.getCurrentSession();
+ let trans;
+
+ try {
+ trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+ Ci.nsITransferable
+ );
+ trans.init(getLoadContext());
+ trans.addDataFlavor("text/x-moz-address");
+ } catch (ex) {
+ return;
+ }
+
+ for (let i = 0; i < dragSession.numDropItems; ++i) {
+ dragSession.getData(trans, i);
+ let dataObj = {};
+ let bestFlavor = {};
+ trans.getAnyTransferData(bestFlavor, dataObj);
+ if (dataObj) {
+ dataObj = dataObj.value.QueryInterface(Ci.nsISupportsString);
+ }
+ if (!dataObj) {
+ continue;
+ }
+
+ // pull the URL out of the data object
+ let address = dataObj.data.substring(0, dataObj.length);
+ if (!address) {
+ continue;
+ }
+
+ DropListAddress(event.target, address);
+ }
+}
+
+function DropListAddress(target, address) {
+ // Set focus on a new available, visible row.
+ awClickEmptySpace(target, true);
+ if (top.MAX_RECIPIENTS == 0) {
+ top.MAX_RECIPIENTS = 1;
+ }
+
+ // Break apart the MIME-ready header address into individual addressees to
+ // add to the dialog.
+ let addresses = MailServices.headerParser.parseEncodedHeader(address);
+ for (let addr of addresses) {
+ let lastInput = awGetInputElement(top.MAX_RECIPIENTS);
+ lastInput.value = addr.toString();
+ awAppendNewRow(true);
+ }
+}
+
+/**
+ * Handles keypress events for the email address inputs (that auto-fill)
+ * in the Address Book Mailing List dialogs. When a comma-separated list of
+ * addresses is entered on one row, split them into one address per row. Only
+ * add a new blank row on "Enter" key. On "Tab" key focus moves to the "Cancel"
+ * button.
+ *
+ * @param {KeyboardEvent} event - The DOM keypress event.
+ * @param {Element} element - The element that triggered the keypress event.
+ */
+function awAbRecipientKeyPress(event, element) {
+ if (event.key != "Enter" && event.key != "Tab") {
+ return;
+ }
+
+ if (!element.value) {
+ if (event.key == "Enter") {
+ awReturnHit(element);
+ }
+ } else {
+ let inputElement = element;
+ let originalRow = awGetRowByInputElement(element);
+ let row;
+ let addresses = MailServices.headerParser.makeFromDisplayAddress(
+ element.value
+ );
+
+ if (addresses.length > 1) {
+ // Collect any existing addresses from the following rows so we don't
+ // simply overwrite them.
+ row = originalRow + 1;
+ inputElement = awGetInputElement(row);
+
+ while (inputElement) {
+ if (inputElement.value) {
+ addresses.push(inputElement.value);
+ inputElement.value = "";
+ }
+ row += 1;
+ inputElement = awGetInputElement(row);
+ }
+ }
+
+ // Insert the addresses, adding new rows if needed.
+ row = originalRow;
+ let needNewRows = false;
+
+ for (let address of addresses) {
+ if (needNewRows) {
+ inputElement = awAppendNewRow(false);
+ } else {
+ inputElement = awGetInputElement(row);
+ if (!inputElement) {
+ needNewRows = true;
+ inputElement = awAppendNewRow(false);
+ }
+ }
+
+ if (inputElement) {
+ inputElement.value = address;
+ }
+ row += 1;
+ }
+
+ if (event.key == "Enter") {
+ // Prevent the dialog from closing. "Enter" inserted a new row instead.
+ event.preventDefault();
+ awReturnHit(inputElement);
+ } else if (event.key == "Tab") {
+ // Focus the last row to let "Tab" move focus to the "Cancel" button.
+ let lastRow = row - 1;
+ awGetInputElement(lastRow).focus();
+ }
+ }
+}
+
+/**
+ * Handle keydown event on a recipient input.
+ * Enables recipient row deletion with DEL or BACKSPACE and
+ * recipient list navigation with cursor up/down.
+ *
+ * Note that the keydown event fires for ALL keys, so this may affect
+ * autocomplete as user enters a recipient text.
+ *
+ * @param {KeyboardEvent} event - The keydown event fired on a recipient input.
+ * @param {HTMLInputElement} inputElement - The recipient input element
+ * on which the event fired (textbox-addressingWidget).
+ */
+function awRecipientKeyDown(event, inputElement) {
+ switch (event.key) {
+ // Enable deletion of empty recipient rows.
+ case "Delete":
+ case "Backspace":
+ if (inputElement.value.length == 1 && event.repeat) {
+ // User is holding down Delete or Backspace to delete recipient text
+ // inline and is now deleting the last character: Set flag to
+ // temporarily block row deletion.
+ top.awRecipientInlineDelete = true;
+ }
+ if (!inputElement.value && !event.altKey) {
+ // When user presses DEL or BACKSPACE on an empty row, and it's not an
+ // ongoing inline deletion, and not ALT+BACKSPACE for input undo,
+ // we delete the row.
+ if (top.awRecipientInlineDelete && !event.repeat) {
+ // User has released and re-pressed Delete or Backspace key
+ // after holding them down to delete recipient text inline:
+ // unblock row deletion.
+ top.awRecipientInlineDelete = false;
+ }
+ if (!top.awRecipientInlineDelete) {
+ let deleteForward = event.key == "Delete";
+ awDeleteHit(inputElement, deleteForward);
+ }
+ }
+ break;
+
+ // Enable browsing the list of recipients up and down with cursor keys.
+ case "ArrowDown":
+ case "ArrowUp":
+ // Only browse recipients if the autocomplete popup is not open.
+ if (!inputElement.popupOpen) {
+ let row = awGetRowByInputElement(inputElement);
+ let down = event.key == "ArrowDown";
+ let noEdgeRow = down ? row < top.MAX_RECIPIENTS : row > 1;
+ if (noEdgeRow) {
+ let targetRow = down ? row + 1 : row - 1;
+ awSetFocusTo(awGetInputElement(targetRow));
+ }
+ }
+ break;
+ }
+}
+
+/**
+ * Delete recipient row (addressingWidgetItem) from UI.
+ *
+ * @param {HTMLInputElement} inputElement - The recipient input element.
+ * textbox-addressingWidget) whose parent row (addressingWidgetItem) will be
+ * deleted.
+ * @param {boolean} deleteForward - true: focus next row after deleting the row
+ * false: focus previous row after deleting the row
+ */
+function awDeleteHit(inputElement, deleteForward = false) {
+ let row = awGetRowByInputElement(inputElement);
+
+ // Don't delete the row if it's the last one remaining; just reset it.
+ if (top.MAX_RECIPIENTS <= 1) {
+ inputElement.value = "";
+ return;
+ }
+
+ // Set the focus to the input field of the next/previous row according to
+ // the direction of deleting if possible.
+ // Note: awSetFocusTo() is asynchronous, i.e. we'll focus after row removal.
+ if (
+ (!deleteForward && row > 1) ||
+ (deleteForward && row == top.MAX_RECIPIENTS)
+ ) {
+ // We're deleting backwards, but not the first row,
+ // or forwards on the last row: Focus previous row.
+ awSetFocusTo(awGetInputElement(row - 1));
+ } else {
+ // We're deleting forwards, but not the last row,
+ // or backwards on the first row: Focus next row.
+ awSetFocusTo(awGetInputElement(row + 1));
+ }
+
+ // Delete the row.
+ awDeleteRow(row);
+}
+
+function awTestRowSequence() {
+ /*
+ This function is for debug and testing purpose only, normal user should not run it!
+
+ Every time we insert or delete a row, we must be sure we didn't break the ID sequence of
+ the addressing widget rows. This function will run a quick test to see if the sequence still ok
+
+ You need to define the pref mail.debug.test_addresses_sequence to true in order to activate it
+ */
+
+ if (!test_addresses_sequence) {
+ return true;
+ }
+
+ // Debug code to verify the sequence is still good.
+
+ let listbox = document.getElementById("addressingWidget");
+ let listitems = listbox.itemChildren;
+ if (listitems.length >= top.MAX_RECIPIENTS) {
+ for (let i = 1; i <= listitems.length; i++) {
+ let item = listitems[i - 1];
+ let inputID = item
+ .querySelector(`input[is="autocomplete-input"]`)
+ .id.split("#")[1];
+ let menulist = item.querySelector("menulist");
+ // In some places like the mailing list dialog there is no menulist,
+ // and so no popupID that needs to be kept in sequence.
+ let popupID = menulist && menulist.id.split("#")[1];
+ if (inputID != i || (popupID && popupID != i)) {
+ dump(
+ `#ERROR: sequence broken at row ${i}, ` +
+ `inputID=${inputID}, popupID=${popupID}\n`
+ );
+ return false;
+ }
+ dump("---SEQUENCE OK---\n");
+ return true;
+ }
+ } else {
+ dump(
+ `#ERROR: listitems.length(${listitems.length}) < ` +
+ `top.MAX_RECIPIENTS(${top.MAX_RECIPIENTS})\n`
+ );
+ }
+
+ return false;
+}
+
+function awRemoveRow(row) {
+ awGetListItem(row).remove();
+ awFitDummyRows();
+
+ top.MAX_RECIPIENTS--;
+}
+
+function awGetNumberOfCols() {
+ if (gNumberOfCols == 0) {
+ var listbox = document.getElementById("addressingWidget");
+ var listCols = listbox.getElementsByTagName("treecol");
+ gNumberOfCols = listCols.length;
+ if (!gNumberOfCols) {
+ // If no cols defined, that means we have only one!
+ gNumberOfCols = 1;
+ }
+ }
+
+ return gNumberOfCols;
+}
+
+function awCreateDummyItem(aParent) {
+ var listbox = document.getElementById("addressingWidget");
+ var item = listbox.getItemAtIndex(0);
+
+ var titem = document.createXULElement("richlistitem");
+ titem.setAttribute("_isDummyRow", "true");
+ titem.setAttribute("class", "dummy-row");
+ titem.style.height = item.getBoundingClientRect().height + "px";
+
+ for (let i = 0; i < awGetNumberOfCols(); i++) {
+ let cell = awCreateDummyCell(titem);
+ if (item.children[i].hasAttribute("style")) {
+ cell.setAttribute("style", item.children[i].getAttribute("style"));
+ }
+ if (item.children[i].hasAttribute("flex")) {
+ cell.setAttribute("flex", item.children[i].getAttribute("flex"));
+ }
+ }
+
+ if (aParent) {
+ aParent.appendChild(titem);
+ }
+
+ return titem;
+}
+
+function awFitDummyRows() {
+ awCalcContentHeight();
+ awCreateOrRemoveDummyRows();
+}
+
+function awCreateOrRemoveDummyRows() {
+ let listbox = document.getElementById("addressingWidget");
+ let listboxHeight = listbox.getBoundingClientRect().height;
+
+ // remove rows to remove scrollbar
+ let kids = listbox.querySelectorAll("[_isDummyRow]");
+ for (
+ let i = kids.length - 1;
+ gAWContentHeight > listboxHeight && i >= 0;
+ --i
+ ) {
+ gAWContentHeight -= gAWRowHeight;
+ kids[i].remove();
+ }
+
+ // add rows to fill space
+ if (gAWRowHeight) {
+ while (gAWContentHeight + gAWRowHeight < listboxHeight) {
+ awCreateDummyItem(listbox);
+ gAWContentHeight += gAWRowHeight;
+ }
+ }
+}
+
+function awCalcContentHeight() {
+ var listbox = document.getElementById("addressingWidget");
+ var items = listbox.itemChildren;
+
+ gAWContentHeight = 0;
+ if (items.length > 0) {
+ // all rows are forced to a uniform height in xul listboxes, so
+ // find the first listitem with a boxObject and use it as precedent
+ var i = 0;
+ do {
+ gAWRowHeight = items[i].getBoundingClientRect().height;
+ ++i;
+ } while (i < items.length && !gAWRowHeight);
+ gAWContentHeight = gAWRowHeight * items.length;
+ }
+}
+
+/* ::::::::::: addressing widget dummy rows ::::::::::::::::: */
+
+function awCreateDummyCell(aParent) {
+ var cell = document.createXULElement("hbox");
+ cell.setAttribute("class", "addressingWidgetCell dummy-row-cell");
+ if (aParent) {
+ aParent.appendChild(cell);
+ }
+
+ return cell;
+}
+
+function awGetNextDummyRow() {
+ // gets the next row from the top down
+ return document.querySelector("#addressingWidget > [_isDummyRow]");
+}
+
+/**
+ * Set focus to the specified element, typically a recipient input element.
+ * We do this asynchronously to allow other processes like adding or removing rows
+ * to complete before shifting focus.
+ *
+ * @param {Element} element - The element to receive focus asynchronously.
+ */
+function awSetFocusTo(element) {
+ // Remember the (input) element to focus for asynchronous focusing, so that we
+ // play safe if this gets called again and the original element gets removed
+ // before we can focus it.
+ top.awInputToFocus = element;
+ setTimeout(_awSetFocusTo, 0);
+}
+
+function _awSetFocusTo() {
+ top.awInputToFocus.focus();
+}
+
+// returns null if abURI is not a mailing list URI
+function GetParentDirectoryFromMailingListURI(abURI) {
+ var abURIArr = abURI.split("/");
+ /*
+ Turn "jsaddrbook://abook.sqlite/MailList6"
+ into ["jsaddrbook:","","abook.sqlite","MailList6"],
+ then into "jsaddrbook://abook.sqlite".
+
+ Turn "moz-aboutlookdirectory:///<top dir ID>/<ML dir ID>"
+ into ["moz-aboutlookdirectory:","","","<top dir ID>","<ML dir ID>"],
+ and then into: "moz-aboutlookdirectory:///<top dir ID>".
+ */
+ if (
+ abURIArr.length == 4 &&
+ ["jsaddrbook:", "moz-abmdbdirectory:"].includes(abURIArr[0]) &&
+ abURIArr[3] != ""
+ ) {
+ return abURIArr[0] + "//" + abURIArr[2];
+ } else if (
+ abURIArr.length == 5 &&
+ abURIArr[0] == "moz-aboutlookdirectory:" &&
+ abURIArr[4] != ""
+ ) {
+ return abURIArr[0] + "///" + abURIArr[3];
+ }
+
+ return null;
+}
diff --git a/comm/mailnews/addrbook/content/abResultsPane.js b/comm/mailnews/addrbook/content/abResultsPane.js
new file mode 100644
index 0000000000..2a4bd99f3b
--- /dev/null
+++ b/comm/mailnews/addrbook/content/abResultsPane.js
@@ -0,0 +1,482 @@
+/* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 ; js-indent-level: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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-globals-from ../../../mail/base/content/utilityOverlay.js */
+/* import-globals-from ../../../mail/components/addrbook/content/abCommon.js */
+/* import-globals-from abView.js */
+
+/**
+ * Use of items in this file require:
+ *
+ * AbResultsPaneDoubleClick(card)
+ * Is called when the results pane is double-clicked, with the clicked card.
+ * GetAbViewListener()
+ * Called when creating a new view
+ */
+/* globals AbResultsPaneDoubleClick, GetAbViewListener */ // abContactsPane.js or abSearchDialog.js
+
+var kDefaultSortColumn = "GeneratedName";
+
+// List/card selections in the results pane.
+var kNothingSelected = 0;
+var kListsAndCards = 1;
+var kMultipleListsOnly = 2;
+var kSingleListOnly = 3;
+var kCardsOnly = 4;
+
+// Global Variables
+
+// Holds a reference to the "abResultsTree" document element. Initially
+// set up by SetAbView.
+var gAbResultsTree = null;
+// gAbView is the current value of gAbResultsTree.view, without passing
+// through XPCOM, so we can access extra functions if necessary.
+var gAbView = null;
+
+function SetAbView(aURI, aSearchQuery, aSearchString) {
+ // If we don't have a URI, just clear the view and leave everything else
+ // alone.
+ if (!aURI) {
+ if (gAbView) {
+ CloseAbView();
+ }
+ return;
+ }
+
+ // If we do have a URI, we want to allow updating the review even if the
+ // URI is the same, as the search results may be different.
+
+ var sortColumn = kDefaultSortColumn;
+ var sortDirection = kDefaultAscending;
+
+ if (!gAbResultsTree) {
+ gAbResultsTree = document.getElementById("abResultsTree");
+ gAbResultsTree.controllers.appendController(ResultsPaneController);
+ }
+
+ if (gAbView) {
+ sortColumn = gAbView.sortColumn;
+ sortDirection = gAbView.sortDirection;
+ } else {
+ if (gAbResultsTree.hasAttribute("sortCol")) {
+ sortColumn = gAbResultsTree.getAttribute("sortCol");
+ }
+ var sortColumnNode = document.getElementById(sortColumn);
+ if (sortColumnNode && sortColumnNode.hasAttribute("sortDirection")) {
+ sortDirection = sortColumnNode.getAttribute("sortDirection");
+ }
+ }
+
+ gAbView = gAbResultsTree.view = new ABView(
+ GetDirectoryFromURI(aURI),
+ aSearchQuery,
+ aSearchString,
+ GetAbViewListener(),
+ sortColumn,
+ sortDirection
+ ).QueryInterface(Ci.nsITreeView);
+ window.dispatchEvent(new CustomEvent("viewchange"));
+
+ UpdateSortIndicators(sortColumn, sortDirection);
+
+ // If the selected address book is LDAP and the search box is empty,
+ // inform the user of the empty results pane.
+ let abResultsTree = document.getElementById("abResultsTree");
+ let cardViewOuterBox = document.getElementById("CardViewOuterBox");
+ let blankResultsPaneMessageBox = document.getElementById(
+ "blankResultsPaneMessageBox"
+ );
+ if (aURI.startsWith("moz-abldapdirectory://") && !aSearchQuery) {
+ if (abResultsTree) {
+ abResultsTree.hidden = true;
+ }
+ if (cardViewOuterBox) {
+ cardViewOuterBox.hidden = true;
+ }
+ if (blankResultsPaneMessageBox) {
+ blankResultsPaneMessageBox.hidden = false;
+ }
+ } else {
+ if (abResultsTree) {
+ abResultsTree.hidden = false;
+ }
+ if (cardViewOuterBox) {
+ cardViewOuterBox.hidden = false;
+ }
+ if (blankResultsPaneMessageBox) {
+ blankResultsPaneMessageBox.hidden = true;
+ }
+ }
+}
+
+function CloseAbView() {
+ gAbView = null;
+ if (gAbResultsTree) {
+ gAbResultsTree.view = null;
+ }
+}
+
+function GetSelectedAddresses() {
+ return GetAddressesForCards(GetSelectedAbCards());
+}
+
+function GetNumSelectedCards() {
+ try {
+ return gAbView.selection.count;
+ } catch (ex) {}
+
+ // if something went wrong, return 0 for the count.
+ return 0;
+}
+
+function GetSelectedCardTypes() {
+ var cards = GetSelectedAbCards();
+ if (!cards) {
+ console.error("ERROR: GetSelectedCardTypes: |cards| is null.");
+ return kNothingSelected; // no view
+ }
+ var count = cards.length;
+ if (count == 0) {
+ // Nothing selected.
+ return kNothingSelected;
+ }
+
+ var mailingListCnt = 0;
+ var cardCnt = 0;
+ for (let i = 0; i < count; i++) {
+ // We can assume no values from GetSelectedAbCards will be null.
+ if (cards[i].isMailList) {
+ mailingListCnt++;
+ } else {
+ cardCnt++;
+ }
+ }
+
+ if (mailingListCnt == 0) {
+ return kCardsOnly;
+ }
+ if (cardCnt > 0) {
+ return kListsAndCards;
+ }
+ if (mailingListCnt == 1) {
+ return kSingleListOnly;
+ }
+ return kMultipleListsOnly;
+}
+
+// NOTE, will return -1 if more than one card selected, or no cards selected.
+function GetSelectedCardIndex() {
+ if (!gAbView) {
+ return -1;
+ }
+
+ var treeSelection = gAbView.selection;
+ if (treeSelection.getRangeCount() == 1) {
+ var start = {};
+ var end = {};
+ treeSelection.getRangeAt(0, start, end);
+ if (start.value == end.value) {
+ return start.value;
+ }
+ }
+
+ return -1;
+}
+
+// NOTE, returns the card if exactly one card is selected, null otherwise
+function GetSelectedCard() {
+ var index = GetSelectedCardIndex();
+ return index == -1 ? null : gAbView.getCardFromRow(index);
+}
+
+/**
+ * Return a (possibly empty) list of cards
+ *
+ * It pushes only non-null/empty element, if any, into the returned list.
+ */
+function GetSelectedAbCards() {
+ var abView = gAbView;
+
+ if (!abView?.selection) {
+ return [];
+ }
+
+ let cards = [];
+ var count = abView.selection.getRangeCount();
+ for (let i = 0; i < count; ++i) {
+ let start = {};
+ let end = {};
+
+ abView.selection.getRangeAt(i, start, end);
+
+ for (let j = start.value; j <= end.value; ++j) {
+ // avoid inserting null element into the list. GetRangeAt() may be buggy.
+ let tmp = abView.getCardFromRow(j);
+ if (tmp) {
+ cards.push(tmp);
+ }
+ }
+ }
+ return cards;
+}
+
+// XXX todo
+// an optimization might be to make this return
+// the selected ranges, which would be faster
+// when the user does large selections, but for now, let's keep it simple.
+function GetSelectedRows() {
+ var selectedRows = "";
+
+ if (!gAbView) {
+ return selectedRows;
+ }
+
+ var rangeCount = gAbView.selection.getRangeCount();
+ for (let i = 0; i < rangeCount; ++i) {
+ var start = {};
+ var end = {};
+ gAbView.selection.getRangeAt(i, start, end);
+ for (let j = start.value; j <= end.value; ++j) {
+ if (selectedRows) {
+ selectedRows += ",";
+ }
+ selectedRows += j;
+ }
+ }
+
+ return selectedRows;
+}
+
+function AbResultsPaneOnClick(event) {
+ // we only care about button 0 (left click) events
+ if (event.button != 0) {
+ return;
+ }
+
+ // all we need to worry about here is double clicks
+ // and column header clicks.
+ //
+ // we get in here for clicks on the "treecol" (headers)
+ // and the "scrollbarbutton" (scrollbar buttons)
+ // we don't want those events to cause a "double click"
+
+ var t = event.target;
+
+ if (t.localName == "treecol") {
+ var sortDirection;
+ var currentDirection = t.getAttribute("sortDirection");
+
+ // Revert the sort order. If none is set, use Ascending.
+ sortDirection =
+ currentDirection == kDefaultAscending
+ ? kDefaultDescending
+ : kDefaultAscending;
+
+ SortAndUpdateIndicators(t.id, sortDirection);
+ } else if (t.localName == "treechildren") {
+ // figure out what row the click was in
+ var row = gAbResultsTree.getRowAt(event.clientX, event.clientY);
+ if (row == -1) {
+ return;
+ }
+
+ if (event.detail == 2) {
+ AbResultsPaneDoubleClick(gAbView.getCardFromRow(row));
+ }
+ }
+}
+
+function SortAndUpdateIndicators(sortColumn, sortDirection) {
+ UpdateSortIndicators(sortColumn, sortDirection);
+
+ if (gAbView) {
+ gAbView.sortBy(sortColumn, sortDirection);
+ }
+}
+
+function UpdateSortIndicators(colID, sortDirection) {
+ var sortedColumn = null;
+
+ // set the sort indicator on the column we are sorted by
+ if (colID) {
+ sortedColumn = document.getElementById(colID);
+ if (sortedColumn) {
+ sortedColumn.setAttribute("sortDirection", sortDirection);
+ gAbResultsTree.setAttribute("sortCol", colID);
+ }
+ }
+
+ // remove the sort indicator from all the columns
+ // except the one we are sorted by
+ var currCol = gAbResultsTree.firstElementChild.firstElementChild;
+ while (currCol) {
+ if (currCol != sortedColumn && currCol.localName == "treecol") {
+ currCol.removeAttribute("sortDirection");
+ }
+ currCol = currCol.nextElementSibling;
+ }
+}
+
+// Controller object for Results Pane
+var ResultsPaneController = {
+ supportsCommand(command) {
+ switch (command) {
+ case "cmd_selectAll":
+ case "cmd_delete":
+ case "button_delete":
+ case "cmd_print":
+ case "cmd_printcard":
+ return true;
+ default:
+ return false;
+ }
+ },
+
+ isCommandEnabled(command) {
+ switch (command) {
+ case "cmd_selectAll":
+ return true;
+ case "cmd_delete":
+ case "button_delete": {
+ let numSelected;
+ let enabled = false;
+ if (gAbView && gAbView.selection) {
+ if (gAbView.directory) {
+ enabled = !gAbView.directory.readOnly;
+ } else {
+ enabled = true;
+ }
+ numSelected = gAbView.selection.count;
+ } else {
+ numSelected = 0;
+ }
+ enabled = enabled && numSelected > 0;
+
+ if (enabled && !gAbView?.directory) {
+ // Undefined gAbView.directory means "All Address Books" is selected.
+ // Disable the menu/button if any selected card is from a read only
+ // directory.
+ enabled = !GetSelectedAbCards().some(
+ card =>
+ MailServices.ab.getDirectoryFromUID(card.directoryUID).readOnly
+ );
+ }
+
+ if (command == "cmd_delete") {
+ switch (GetSelectedCardTypes()) {
+ case kSingleListOnly:
+ updateDeleteControls("valueList");
+ break;
+ case kMultipleListsOnly:
+ updateDeleteControls("valueLists");
+ break;
+ case kListsAndCards:
+ updateDeleteControls("valueItems");
+ break;
+ case kCardsOnly:
+ default:
+ updateDeleteControls(
+ numSelected < 2 ? "valueCard" : "valueCards"
+ );
+ }
+ }
+ return enabled;
+ }
+ case "cmd_print":
+ // cmd_print is currently only used in SeaMonkey.
+ // Prevent printing when we don't have an opener (browserDOMWindow is
+ // null).
+ let enabled = window.browserDOMWindow && GetNumSelectedCards() > 0;
+ document.querySelectorAll("[command=cmd_print]").forEach(e => {
+ e.disabled = !enabled;
+ });
+ return enabled;
+ case "cmd_printcard":
+ // Prevent printing when we don't have an opener (browserDOMWindow is
+ // null).
+ return window.browserDOMWindow && GetNumSelectedCards() > 0;
+ default:
+ return false;
+ }
+ },
+
+ doCommand(command) {
+ switch (command) {
+ case "cmd_selectAll":
+ if (gAbView) {
+ gAbView.selection.selectAll();
+ }
+ break;
+ case "cmd_delete":
+ case "button_delete":
+ AbDelete();
+ break;
+ }
+ },
+};
+
+function updateDeleteControls(
+ labelAttribute,
+ accessKeyAttribute = "accesskeyDefault"
+) {
+ goSetMenuValue("cmd_delete", labelAttribute);
+ goSetAccessKey("cmd_delete", accessKeyAttribute);
+
+ // The toolbar button doesn't update itself from the command. Do that now.
+ let button = document.getElementById("button-abdelete");
+ if (!button) {
+ return;
+ }
+
+ let command = document.getElementById("cmd_delete");
+ button.label = command.getAttribute("label");
+ button.setAttribute(
+ "tooltiptext",
+ button.getAttribute(
+ labelAttribute == "valueCardDAV" ? "tooltipCardDAV" : "tooltipDefault"
+ )
+ );
+}
+
+/**
+ * Generate a comma separated list of addresses from the given cards.
+ *
+ * @param {nsIAbCard[]} cards - The cards to get addresses for.
+ * @returns {string} A string of comma separated mailboxes.
+ */
+function GetAddressesForCards(cards) {
+ if (!cards) {
+ return "";
+ }
+
+ return cards
+ .map(makeMimeAddressFromCard)
+ .filter(addr => addr)
+ .join(",");
+}
+
+/**
+ * Make a MIME encoded string output of the card. This will make a difference
+ * e.g. in scenarios where non-ASCII is used in the mailbox, or when then
+ * display name include special characters such as comma.
+ *
+ * @param {nsIAbCard} card - The card to use.
+ * @returns {string} A MIME encoded mailbox representation of the card.
+ */
+function makeMimeAddressFromCard(card) {
+ if (!card) {
+ return "";
+ }
+
+ let email;
+ if (card.isMailList) {
+ let directory = GetDirectoryFromURI(card.mailListURI);
+ email = directory.description || card.displayName;
+ } else {
+ email = card.emailAddresses[0];
+ }
+ return MailServices.headerParser.makeMimeAddress(card.displayName, email);
+}
diff --git a/comm/mailnews/addrbook/content/abView.js b/comm/mailnews/addrbook/content/abView.js
new file mode 100644
index 0000000000..f5acb7d11c
--- /dev/null
+++ b/comm/mailnews/addrbook/content/abView.js
@@ -0,0 +1,539 @@
+/* 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/. */
+
+/* globals MailServices, PROTO_TREE_VIEW, Services */
+
+function ABView(
+ directory,
+ searchQuery,
+ searchString,
+ listener,
+ sortColumn,
+ sortDirection
+) {
+ this.__proto__.__proto__ = new PROTO_TREE_VIEW();
+ this.directory = directory;
+ this.listener = listener;
+
+ let directories = directory ? [directory] : MailServices.ab.directories;
+ if (searchQuery) {
+ searchQuery = searchQuery.replace(/^\?+/, "");
+ for (let dir of directories) {
+ dir.search(searchQuery, searchString, this);
+ }
+ } else {
+ for (let dir of directories) {
+ for (let card of dir.childCards) {
+ this._rowMap.push(new abViewCard(card, dir));
+ }
+ }
+ if (this.listener) {
+ this.listener.onCountChanged(this.rowCount);
+ }
+ }
+ this.sortBy(sortColumn, sortDirection);
+}
+ABView.nameFormat = Services.prefs.getIntPref(
+ "mail.addr_book.lastnamefirst",
+ 0
+);
+ABView.prototype = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsITreeView",
+ "nsIAbDirSearchListener",
+ "nsIObserver",
+ "nsISupportsWeakReference",
+ ]),
+
+ directory: null,
+ listener: null,
+ _notifications: [
+ "addrbook-directory-deleted",
+ "addrbook-directory-invalidated",
+ "addrbook-contact-created",
+ "addrbook-contact-updated",
+ "addrbook-contact-deleted",
+ "addrbook-list-created",
+ "addrbook-list-updated",
+ "addrbook-list-deleted",
+ "addrbook-list-member-added",
+ "addrbook-list-member-removed",
+ ],
+
+ sortColumn: "",
+ sortDirection: "",
+ collator: new Intl.Collator(undefined, { numeric: true }),
+
+ deleteSelectedCards() {
+ let directoryMap = new Map();
+ for (let i = 0; i < this.selection.getRangeCount(); i++) {
+ let start = {};
+ let finish = {};
+ this.selection.getRangeAt(i, start, finish);
+ for (let j = start.value; j <= finish.value; j++) {
+ let card = this.getCardFromRow(j);
+ let cardSet = directoryMap.get(card.directoryUID);
+ if (!cardSet) {
+ cardSet = new Set();
+ directoryMap.set(card.directoryUID, cardSet);
+ }
+ cardSet.add(card);
+ }
+ }
+
+ for (let [directoryUID, cardSet] of directoryMap) {
+ let directory;
+ if (this.directory && this.directory.isMailList) {
+ // Removes cards from the list instead of deleting them.
+ directory = this.directory;
+ } else {
+ directory = MailServices.ab.getDirectoryFromUID(directoryUID);
+ }
+
+ cardSet = [...cardSet];
+ directory.deleteCards(cardSet.filter(card => !card.isMailList));
+ for (let card of cardSet.filter(card => card.isMailList)) {
+ MailServices.ab.deleteAddressBook(card.mailListURI);
+ }
+ }
+ },
+ getCardFromRow(row) {
+ return this._rowMap[row] ? this._rowMap[row].card : null;
+ },
+ getDirectoryFromRow(row) {
+ return this._rowMap[row] ? this._rowMap[row].directory : null;
+ },
+ sortBy(sortColumn, sortDirection, resort) {
+ // Remember what was selected.
+ let selection = this.selection;
+ if (selection) {
+ for (let i = 0; i < this._rowMap.length; i++) {
+ this._rowMap[i].wasSelected = selection.isSelected(i);
+ this._rowMap[i].wasCurrent = selection.currentIndex == i;
+ }
+ }
+
+ // Do the sort.
+ if (sortColumn == this.sortColumn && !resort) {
+ if (sortDirection == this.sortDirection) {
+ return;
+ }
+ this._rowMap.reverse();
+ } else {
+ this._rowMap.sort((a, b) => {
+ let aText = a.getText(sortColumn);
+ let bText = b.getText(sortColumn);
+ if (sortDirection == "descending") {
+ return this.collator.compare(bText, aText);
+ }
+ return this.collator.compare(aText, bText);
+ });
+ }
+
+ // Restore what was selected.
+ if (selection) {
+ selection.selectEventsSuppressed = true;
+ for (let i = 0; i < this._rowMap.length; i++) {
+ if (this._rowMap[i].wasSelected != selection.isSelected(i)) {
+ selection.toggleSelect(i);
+ }
+ }
+ // Can't do this until updating the selection is finished.
+ for (let i = 0; i < this._rowMap.length; i++) {
+ if (this._rowMap[i].wasCurrent) {
+ selection.currentIndex = i;
+ break;
+ }
+ }
+ this.selectionChanged();
+ selection.selectEventsSuppressed = false;
+ }
+
+ if (this.tree) {
+ this.tree.invalidate();
+ }
+ this.sortColumn = sortColumn;
+ this.sortDirection = sortDirection;
+ },
+
+ // nsITreeView
+
+ selectionChanged() {
+ if (this.listener) {
+ this.listener.onSelectionChanged();
+ }
+ },
+ setTree(tree) {
+ this.tree = tree;
+ for (let topic of this._notifications) {
+ if (tree) {
+ Services.obs.addObserver(this, topic, true);
+ } else {
+ try {
+ Services.obs.removeObserver(this, topic);
+ } catch (ex) {
+ // `this` might not be a valid observer.
+ }
+ }
+ }
+ Services.prefs.addObserver("mail.addr_book.lastnamefirst", this, true);
+ },
+
+ // nsIAbDirSearchListener
+
+ onSearchFoundCard(card) {
+ // Instead of duplicating the insertion code below, just call it.
+ this.observe(card, "addrbook-contact-created", this.directory?.UID);
+ },
+ onSearchFinished(status, complete, secInfo, location) {
+ // Special handling for Bad Cert errors.
+ let offerCertException = false;
+ try {
+ // If code is not an NSS error, getErrorClass() will fail.
+ let nssErrorsService = Cc["@mozilla.org/nss_errors_service;1"].getService(
+ Ci.nsINSSErrorsService
+ );
+ let errorClass = nssErrorsService.getErrorClass(status);
+ if (errorClass == Ci.nsINSSErrorsService.ERROR_CLASS_BAD_CERT) {
+ offerCertException = true;
+ }
+ } catch (ex) {}
+
+ if (offerCertException) {
+ // Give the user the option of adding an exception for the bad cert.
+ let params = {
+ exceptionAdded: false,
+ securityInfo: secInfo,
+ prefetchCert: true,
+ location,
+ };
+ window.openDialog(
+ "chrome://pippki/content/exceptionDialog.xhtml",
+ "",
+ "chrome,centerscreen,modal",
+ params
+ );
+ // params.exceptionAdded will be set if the user added an exception.
+ }
+ },
+
+ // nsIObserver
+
+ observe(subject, topic, data) {
+ if (topic == "nsPref:changed") {
+ ABView.nameFormat = Services.prefs.getIntPref(
+ "mail.addr_book.lastnamefirst",
+ 0
+ );
+ for (let card of this._rowMap) {
+ delete card._getTextCache.GeneratedName;
+ }
+ if (this.tree) {
+ if (this.sortColumn == "GeneratedName") {
+ this.sortBy(this.sortColumn, this.sortDirection, true);
+ } else {
+ this.tree.invalidate(this.tree.columns.GeneratedName);
+ }
+ }
+ return;
+ }
+
+ if (this.directory && data && this.directory.UID != data) {
+ return;
+ }
+
+ // If we make it here, we're in the root directory, or the right directory.
+
+ switch (topic) {
+ case "addrbook-directory-deleted": {
+ if (this.directory) {
+ break;
+ }
+
+ subject.QueryInterface(Ci.nsIAbDirectory);
+ let scrollPosition = this.tree?.getFirstVisibleRow();
+ for (let i = this._rowMap.length - 1; i >= 0; i--) {
+ if (this._rowMap[i].directory.UID == subject.UID) {
+ this._rowMap.splice(i, 1);
+ if (this.tree) {
+ this.tree.rowCountChanged(i, -1);
+ }
+ }
+ }
+ if (this.listener) {
+ this.listener.onCountChanged(this.rowCount);
+ }
+ if (this.tree && scrollPosition !== null) {
+ this.tree.scrollToRow(scrollPosition);
+ }
+ break;
+ }
+ case "addrbook-directory-invalidated":
+ subject.QueryInterface(Ci.nsIAbDirectory);
+ if (subject == this.directory) {
+ this._rowMap.length = 0;
+ for (let card of this.directory.childCards) {
+ this._rowMap.push(new abViewCard(card, this.directory));
+ }
+ this.sortBy(this.sortColumn, this.sortDirection, true);
+ if (this.listener) {
+ this.listener.onCountChanged(this.rowCount);
+ }
+ }
+ break;
+ case "addrbook-list-created": {
+ let parentDir = MailServices.ab.getDirectoryFromUID(data);
+ // `subject` is an nsIAbDirectory, make it the matching card instead.
+ subject.QueryInterface(Ci.nsIAbDirectory);
+ for (let card of parentDir.childCards) {
+ if (card.UID == subject.UID) {
+ subject = card;
+ break;
+ }
+ }
+ }
+ // Falls through.
+ case "addrbook-list-member-added":
+ case "addrbook-contact-created":
+ if (topic == "addrbook-list-member-added" && !this.directory) {
+ break;
+ }
+
+ subject.QueryInterface(Ci.nsIAbCard);
+ let viewCard = new abViewCard(subject);
+ let sortText = viewCard.getText(this.sortColumn);
+ let addIndex = null;
+ for (let i = 0; addIndex === null && i < this._rowMap.length; i++) {
+ let comparison = this.collator.compare(
+ sortText,
+ this._rowMap[i].getText(this.sortColumn)
+ );
+ if (
+ (comparison < 0 && this.sortDirection == "ascending") ||
+ (comparison >= 0 && this.sortDirection == "descending")
+ ) {
+ addIndex = i;
+ }
+ }
+ if (addIndex === null) {
+ addIndex = this._rowMap.length;
+ }
+ this._rowMap.splice(addIndex, 0, viewCard);
+ if (this.tree) {
+ this.tree.rowCountChanged(addIndex, 1);
+ }
+ if (this.listener) {
+ this.listener.onCountChanged(this.rowCount);
+ }
+ break;
+
+ case "addrbook-list-updated": {
+ let parentDir = this.directory;
+ if (!parentDir) {
+ parentDir = MailServices.ab.getDirectoryFromUID(data);
+ }
+ // `subject` is an nsIAbDirectory, make it the matching card instead.
+ subject.QueryInterface(Ci.nsIAbDirectory);
+ for (let card of parentDir.childCards) {
+ if (card.UID == subject.UID) {
+ subject = card;
+ break;
+ }
+ }
+ }
+ // Falls through.
+ case "addrbook-contact-updated": {
+ subject.QueryInterface(Ci.nsIAbCard);
+ let needsSort = false;
+ for (let i = this._rowMap.length - 1; i >= 0; i--) {
+ if (
+ this._rowMap[i].card.equals(subject) &&
+ this._rowMap[i].card.directoryUID == subject.directoryUID
+ ) {
+ this._rowMap.splice(i, 1, new abViewCard(subject));
+ needsSort = true;
+ }
+ }
+ if (needsSort) {
+ this.sortBy(this.sortColumn, this.sortDirection, true);
+ }
+ break;
+ }
+
+ case "addrbook-list-deleted": {
+ subject.QueryInterface(Ci.nsIAbDirectory);
+ let scrollPosition = this.tree?.getFirstVisibleRow();
+ for (let i = this._rowMap.length - 1; i >= 0; i--) {
+ if (this._rowMap[i].card.UID == subject.UID) {
+ this._rowMap.splice(i, 1);
+ if (this.tree) {
+ this.tree.rowCountChanged(i, -1);
+ }
+ }
+ }
+ if (this.listener) {
+ this.listener.onCountChanged(this.rowCount);
+ }
+ if (this.tree && scrollPosition !== null) {
+ this.tree.scrollToRow(scrollPosition);
+ }
+ break;
+ }
+ case "addrbook-list-member-removed":
+ if (!this.directory) {
+ break;
+ }
+ // Falls through.
+ case "addrbook-contact-deleted": {
+ subject.QueryInterface(Ci.nsIAbCard);
+ let scrollPosition = this.tree?.getFirstVisibleRow();
+ for (let i = this._rowMap.length - 1; i >= 0; i--) {
+ if (
+ this._rowMap[i].card.equals(subject) &&
+ this._rowMap[i].card.directoryUID == subject.directoryUID
+ ) {
+ this._rowMap.splice(i, 1);
+ if (this.tree) {
+ this.tree.rowCountChanged(i, -1);
+ }
+ }
+ }
+ if (this.listener) {
+ this.listener.onCountChanged(this.rowCount);
+ }
+ if (this.tree && scrollPosition !== null) {
+ this.tree.scrollToRow(scrollPosition);
+ }
+ break;
+ }
+ }
+ },
+};
+
+/**
+ * Representation of a card, used as a table row in ABView.
+ *
+ * @param {nsIAbCard} card - contact or mailing list card for this row.
+ * @param {nsIAbDirectory} [directoryHint] - the directory containing card,
+ * if available (this is a performance optimization only).
+ */
+function abViewCard(card, directoryHint) {
+ this.card = card;
+ this._getTextCache = {};
+ if (directoryHint) {
+ this._directory = directoryHint;
+ } else {
+ this._directory = MailServices.ab.getDirectoryFromUID(
+ this.card.directoryUID
+ );
+ }
+}
+abViewCard.listFormatter = new Services.intl.ListFormat(
+ Services.appinfo.name == "xpcshell" ? "en-US" : undefined,
+ { type: "unit" }
+);
+abViewCard.prototype = {
+ _getText(columnID) {
+ try {
+ let { getProperty, supportsVCard, vCardProperties } = this.card;
+
+ if (this.card.isMailList) {
+ if (columnID == "GeneratedName") {
+ return this.card.displayName;
+ }
+ if (["NickName", "Notes"].includes(columnID)) {
+ return getProperty(columnID, "");
+ }
+ return "";
+ }
+
+ switch (columnID) {
+ case "addrbook":
+ case "Addrbook":
+ return this._directory.dirName;
+ case "GeneratedName":
+ return this.card.generateName(ABView.nameFormat);
+ case "_PhoneticName":
+ return this.card.generatePhoneticName(true);
+ case "ChatName":
+ return this.card.isMailList ? "" : this.card.generateChatName();
+ case "EmailAddresses":
+ return abViewCard.listFormatter.format(this.card.emailAddresses);
+ case "PhoneNumbers": {
+ let phoneNumbers;
+ if (supportsVCard) {
+ phoneNumbers = vCardProperties.getAllValues("tel");
+ } else {
+ phoneNumbers = [
+ getProperty("WorkPhone", ""),
+ getProperty("HomePhone", ""),
+ getProperty("CellularNumber", ""),
+ getProperty("FaxNumber", ""),
+ getProperty("PagerNumber", ""),
+ ];
+ }
+ return abViewCard.listFormatter.format(phoneNumbers.filter(Boolean));
+ }
+ case "JobTitle":
+ case "Title":
+ if (supportsVCard) {
+ return vCardProperties.getFirstValue("title");
+ }
+ return getProperty("JobTitle", "");
+ case "Department":
+ if (supportsVCard) {
+ let vCardValue = vCardProperties.getFirstValue("org");
+ if (Array.isArray(vCardValue)) {
+ return vCardValue[1] || "";
+ }
+ return "";
+ }
+ return getProperty(columnID, "");
+ case "Company":
+ case "Organization":
+ if (supportsVCard) {
+ let vCardValue = vCardProperties.getFirstValue("org");
+ if (Array.isArray(vCardValue)) {
+ return vCardValue[0] || "";
+ }
+ return vCardValue;
+ }
+ return getProperty("Company", "");
+ case "NickName":
+ if (supportsVCard) {
+ return vCardProperties.getFirstValue("nickname");
+ }
+ return getProperty(columnID, "");
+ default:
+ return getProperty(columnID, "");
+ }
+ } catch (ex) {
+ return "";
+ }
+ },
+ getText(columnID) {
+ if (!(columnID in this._getTextCache)) {
+ this._getTextCache[columnID] = this._getText(columnID)?.trim() ?? "";
+ }
+ return this._getTextCache[columnID];
+ },
+ get id() {
+ return this.card.UID;
+ },
+ get open() {
+ return false;
+ },
+ get level() {
+ return 0;
+ },
+ get children() {
+ return [];
+ },
+ getProperties() {
+ return this.card.isMailList ? "MailList" : "";
+ },
+ get directory() {
+ return this._directory;
+ },
+};
diff --git a/comm/mailnews/addrbook/content/map-list.js b/comm/mailnews/addrbook/content/map-list.js
new file mode 100644
index 0000000000..102cb09522
--- /dev/null
+++ b/comm/mailnews/addrbook/content/map-list.js
@@ -0,0 +1,217 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* global MozElements */
+
+// Wrap in a block to prevent leaking to window scope.
+{
+ /**
+ * The MozMapList widget behaves as a popup menu showing available map options
+ * for an address. It is a part of the card view in the addressbook.
+ *
+ * @augments {MozElements.MozMenuPopup}
+ */
+ class MozMapList extends MozElements.MozMenuPopup {
+ connectedCallback() {
+ if (this.delayConnectedCallback() || this.hasConnected) {
+ return;
+ }
+ this.setAttribute("is", "map-list");
+
+ this.addEventListener("command", event => {
+ this._chooseMapService(event.target);
+ event.stopPropagation();
+ });
+
+ this.addEventListener("popupshowing", event => {
+ this._listMapServices();
+ });
+
+ this._setWidgetDisabled(true);
+ }
+
+ get mapURL() {
+ return this._createMapItURL();
+ }
+
+ /**
+ * Initializes the necessary address data from an addressbook card.
+ *
+ * @param {nsIAbCard} card - The card to get the address data from.
+ * @param {string} addrPrefix - Card property prefix: "Home" or "Work",
+ * to make the map use either HomeAddress or WorkAddress.
+ */
+ initMapAddressFromCard(card, addrPrefix) {
+ let mapItURLFormat = this._getMapURLPref();
+ let doNotShowMap = !mapItURLFormat || !addrPrefix || !card;
+ this._setWidgetDisabled(doNotShowMap);
+ if (doNotShowMap) {
+ return;
+ }
+
+ this.address1 = card.getProperty(addrPrefix + "Address");
+ this.address2 = card.getProperty(addrPrefix + "Address2");
+ this.city = card.getProperty(addrPrefix + "City");
+ this._state = card.getProperty(addrPrefix + "State");
+ this.zip = card.getProperty(addrPrefix + "ZipCode");
+ this.country = card.getProperty(addrPrefix + "Country");
+ }
+
+ /**
+ * Sets the disabled/enabled state of the parent widget (e.g. a button).
+ */
+ _setWidgetDisabled(disabled) {
+ this.parentNode.disabled = disabled;
+ }
+
+ /**
+ * Returns the Map service URL from localized pref. Returns null if there
+ * is none at the given index.
+ *
+ * @param {integer} [index=0] - The index of the service to return.
+ * 0 is the default service.
+ */
+ _getMapURLPref(index = 0) {
+ let url = null;
+ if (!index) {
+ url = Services.prefs.getComplexValue(
+ "mail.addr_book.mapit_url.format",
+ Ci.nsIPrefLocalizedString
+ ).data;
+ } else {
+ try {
+ url = Services.prefs.getComplexValue(
+ "mail.addr_book.mapit_url." + index + ".format",
+ Ci.nsIPrefLocalizedString
+ ).data;
+ } catch (e) {}
+ }
+
+ return url;
+ }
+
+ /**
+ * Builds menuitem elements representing map services defined in prefs
+ * and attaches them to the specified button.
+ */
+ _listMapServices() {
+ let index = 1;
+ let itemFound = true;
+ let defaultFound = false;
+ const kUserIndex = 100;
+ let mapList = this;
+ while (mapList.hasChildNodes()) {
+ mapList.lastChild.remove();
+ }
+
+ let defaultUrl = this._getMapURLPref();
+
+ // Creates the menuitem with supplied data.
+ function addMapService(url, name) {
+ let item = document.createXULElement("menuitem");
+ item.setAttribute("url", url);
+ item.setAttribute("label", name);
+ item.setAttribute("type", "radio");
+ item.setAttribute("name", "mapit_service");
+ if (url == defaultUrl) {
+ item.setAttribute("checked", "true");
+ }
+ mapList.appendChild(item);
+ }
+
+ // Generates a useful generic name by cutting out only the host address.
+ function generateName(url) {
+ return new URL(url).hostname;
+ }
+
+ // Add all defined map services as menuitems.
+ while (itemFound) {
+ let urlName;
+ let urlTemplate = this._getMapURLPref(index);
+ if (!urlTemplate) {
+ itemFound = false;
+ } else {
+ // Name is not mandatory, generate one if not found.
+ try {
+ urlName = Services.prefs.getComplexValue(
+ "mail.addr_book.mapit_url." + index + ".name",
+ Ci.nsIPrefLocalizedString
+ ).data;
+ } catch (e) {
+ urlName = generateName(urlTemplate);
+ }
+ }
+ if (itemFound) {
+ addMapService(urlTemplate, urlName);
+ index++;
+ if (urlTemplate == defaultUrl) {
+ defaultFound = true;
+ }
+ } else if (index < kUserIndex) {
+ // After iterating the base region provided urls, check for user defined ones.
+ index = kUserIndex;
+ itemFound = true;
+ }
+ }
+ if (!defaultFound) {
+ // If user had put a customized map URL into mail.addr_book.mapit_url.format
+ // preserve it as a new map service named with the URL.
+ // 'index' now points to the first unused entry in prefs.
+ let defaultName = generateName(defaultUrl);
+ addMapService(defaultUrl, defaultName);
+ Services.prefs.setCharPref(
+ "mail.addr_book.mapit_url." + index + ".format",
+ defaultUrl
+ );
+ Services.prefs.setCharPref(
+ "mail.addr_book.mapit_url." + index + ".name",
+ defaultName
+ );
+ }
+ }
+
+ /**
+ * Save user selected mapping service.
+ *
+ * @param {Element} item - The chosen menuitem with map service.
+ */
+ _chooseMapService(item) {
+ // Save selected URL as the default.
+ let defaultUrl = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(
+ Ci.nsIPrefLocalizedString
+ );
+ defaultUrl.data = item.getAttribute("url");
+ Services.prefs.setComplexValue(
+ "mail.addr_book.mapit_url.format",
+ Ci.nsIPrefLocalizedString,
+ defaultUrl
+ );
+ }
+
+ /**
+ * Generate the map URL used to open the link on clicking the menulist button.
+ *
+ * @returns {urlFormat} - the map url generated from the address.
+ */
+ _createMapItURL() {
+ let urlFormat = this._getMapURLPref();
+ if (!urlFormat) {
+ return null;
+ }
+
+ urlFormat = urlFormat.replace("@A1", encodeURIComponent(this.address1));
+ urlFormat = urlFormat.replace("@A2", encodeURIComponent(this.address2));
+ urlFormat = urlFormat.replace("@CI", encodeURIComponent(this.city));
+ urlFormat = urlFormat.replace("@ST", encodeURIComponent(this._state));
+ urlFormat = urlFormat.replace("@ZI", encodeURIComponent(this.zip));
+ urlFormat = urlFormat.replace("@CO", encodeURIComponent(this.country));
+
+ return urlFormat;
+ }
+ }
+
+ customElements.define("map-list", MozMapList, { extends: "menupopup" });
+}