summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/addrbook/content/abMailListDialog.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mailnews/addrbook/content/abMailListDialog.js')
-rw-r--r--comm/mailnews/addrbook/content/abMailListDialog.js961
1 files changed, 961 insertions, 0 deletions
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;
+}