summaryrefslogtreecommitdiffstats
path: root/comm/suite/mailnews/components/compose/content/addressingWidgetOverlay.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/suite/mailnews/components/compose/content/addressingWidgetOverlay.js')
-rw-r--r--comm/suite/mailnews/components/compose/content/addressingWidgetOverlay.js1167
1 files changed, 1167 insertions, 0 deletions
diff --git a/comm/suite/mailnews/components/compose/content/addressingWidgetOverlay.js b/comm/suite/mailnews/components/compose/content/addressingWidgetOverlay.js
new file mode 100644
index 0000000000..38cd1f5ecc
--- /dev/null
+++ b/comm/suite/mailnews/components/compose/content/addressingWidgetOverlay.js
@@ -0,0 +1,1167 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const {MailServices} = ChromeUtils.import("resource:///modules/MailServices.jsm");
+
+top.MAX_RECIPIENTS = 1; /* for the initial listitem created in the XUL */
+
+var inputElementType = "";
+var selectElementType = "";
+var selectElementIndexTable = null;
+
+var gNumberOfCols = 0;
+
+var gDragService = Cc["@mozilla.org/widget/dragservice;1"]
+ .getService(Ci.nsIDragService);
+
+/**
+ * global variable inherited from MsgComposeCommands.js
+ *
+ var gMsgCompose;
+ */
+
+function awGetMaxRecipients()
+{
+ return top.MAX_RECIPIENTS;
+}
+
+function awGetNumberOfCols()
+{
+ if (gNumberOfCols == 0)
+ {
+ var listbox = document.getElementById('addressingWidget');
+ var listCols = listbox.getElementsByTagName('listcol');
+ gNumberOfCols = listCols.length;
+ if (!gNumberOfCols)
+ gNumberOfCols = 1; /* if no cols defined, that means we have only one! */
+ }
+
+ return gNumberOfCols;
+}
+
+function awInputElementName()
+{
+ if (inputElementType == "")
+ inputElementType = document.getElementById("addressCol2#1").localName;
+ return inputElementType;
+}
+
+function awSelectElementName()
+{
+ if (selectElementType == "")
+ selectElementType = document.getElementById("addressCol1#1").localName;
+ return selectElementType;
+}
+
+// TODO: replace awGetSelectItemIndex with recipient type index constants
+
+function awGetSelectItemIndex(itemData)
+{
+ if (selectElementIndexTable == null)
+ {
+ selectElementIndexTable = new Object();
+ var selectElem = document.getElementById("addressCol1#1");
+ for (var i = 0; i < selectElem.childNodes[0].childNodes.length; i ++)
+ {
+ var aData = selectElem.childNodes[0].childNodes[i].getAttribute("value");
+ selectElementIndexTable[aData] = i;
+ }
+ }
+ return selectElementIndexTable[itemData];
+}
+
+function Recipients2CompFields(msgCompFields)
+{
+ if (!msgCompFields) {
+ throw new Error("Message Compose Error: msgCompFields is null (ExtractRecipients)");
+ return;
+ }
+
+ var i = 1;
+ var addrTo = "";
+ var addrCc = "";
+ var addrBcc = "";
+ var addrReply = "";
+ var addrNg = "";
+ var addrFollow = "";
+ var to_Sep = "";
+ var cc_Sep = "";
+ var bcc_Sep = "";
+ var reply_Sep = "";
+ var ng_Sep = "";
+ var follow_Sep = "";
+
+ var recipientType;
+ var inputField;
+ var fieldValue;
+ var recipient;
+ while ((inputField = awGetInputElement(i)))
+ {
+ fieldValue = inputField.value;
+
+ if (fieldValue != "")
+ {
+ recipientType = awGetPopupElement(i).value;
+ recipient = null;
+
+ switch (recipientType)
+ {
+ case "addr_to" :
+ case "addr_cc" :
+ case "addr_bcc" :
+ case "addr_reply" :
+ try {
+ let headerParser = MailServices.headerParser;
+ recipient =
+ headerParser.makeFromDisplayAddress(fieldValue)
+ .map(fullValue => headerParser.makeMimeAddress(
+ fullValue.name,
+ fullValue.email))
+ .join(", ");
+ } catch (ex) {
+ recipient = fieldValue;
+ }
+ break;
+ }
+
+ switch (recipientType)
+ {
+ case "addr_to" : addrTo += to_Sep + recipient; to_Sep = ","; break;
+ case "addr_cc" : addrCc += cc_Sep + recipient; cc_Sep = ","; break;
+ case "addr_bcc" : addrBcc += bcc_Sep + recipient; bcc_Sep = ","; break;
+ case "addr_reply" : addrReply += reply_Sep + recipient; reply_Sep = ","; break;
+ case "addr_newsgroups" : addrNg += ng_Sep + fieldValue; ng_Sep = ","; break;
+ case "addr_followup" : addrFollow += follow_Sep + fieldValue; follow_Sep = ","; break;
+ case "addr_other":
+ let headerName = awGetPopupElement(i).label;
+ headerName = headerName.substring(0, headerName.indexOf(':'));
+ msgCompFields.setRawHeader(headerName, fieldValue, null);
+ break;
+ }
+ }
+ i ++;
+ }
+
+ msgCompFields.to = addrTo;
+ msgCompFields.cc = addrCc;
+ msgCompFields.bcc = addrBcc;
+ msgCompFields.replyTo = addrReply;
+ msgCompFields.newsgroups = addrNg;
+ msgCompFields.followupTo = addrFollow;
+}
+
+function CompFields2Recipients(msgCompFields)
+{
+ if (msgCompFields) {
+ var listbox = document.getElementById('addressingWidget');
+ var newListBoxNode = listbox.cloneNode(false);
+ var listBoxColsClone = listbox.firstChild.cloneNode(true);
+ newListBoxNode.appendChild(listBoxColsClone);
+ let templateNode = listbox.querySelector("listitem");
+ // dump("replacing child in comp fields 2 recips \n");
+ listbox.parentNode.replaceChild(newListBoxNode, listbox);
+
+ top.MAX_RECIPIENTS = 0;
+ var msgReplyTo = msgCompFields.replyTo;
+ var msgTo = msgCompFields.to;
+ var msgCC = msgCompFields.cc;
+ var msgBCC = msgCompFields.bcc;
+ var msgNewsgroups = msgCompFields.newsgroups;
+ var msgFollowupTo = msgCompFields.followupTo;
+ var havePrimaryRecipient = false;
+ if (msgReplyTo)
+ awSetInputAndPopupFromArray(msgCompFields.splitRecipients(msgReplyTo, false),
+ "addr_reply", newListBoxNode, templateNode);
+ if (msgTo) {
+ var rcp = msgCompFields.splitRecipients(msgTo, false);
+ if (rcp.length)
+ {
+ awSetInputAndPopupFromArray(rcp, "addr_to", newListBoxNode, templateNode);
+ havePrimaryRecipient = true;
+ }
+ }
+ if (msgCC)
+ awSetInputAndPopupFromArray(msgCompFields.splitRecipients(msgCC, false),
+ "addr_cc", newListBoxNode, templateNode);
+ if (msgBCC)
+ awSetInputAndPopupFromArray(msgCompFields.splitRecipients(msgBCC, false),
+ "addr_bcc", newListBoxNode, templateNode);
+ if (msgNewsgroups) {
+ awSetInputAndPopup(msgNewsgroups, "addr_newsgroups", newListBoxNode, templateNode);
+ havePrimaryRecipient = true;
+ }
+ if(msgFollowupTo)
+ awSetInputAndPopup(msgFollowupTo, "addr_followup", newListBoxNode, templateNode);
+
+ // If it's a new message, we need to add an extra empty recipient.
+ if (!havePrimaryRecipient)
+ _awSetInputAndPopup("", "addr_to", newListBoxNode, templateNode);
+ awFitDummyRows(2);
+
+ // CompFields2Recipients is called whenever a user replies or edits an existing message.
+ // We want to add all of the recipients for this message to the ignore list for spell check
+ let currentAddress = gCurrentIdentity ? gCurrentIdentity.fullAddress : "";
+ addRecipientsToIgnoreList([currentAddress,msgTo,msgCC,msgBCC].filter(adr => adr).join(", "));
+ }
+}
+
+function awSetInputAndPopupId(inputElem, popupElem, rowNumber)
+{
+ popupElem.id = "addressCol1#" + rowNumber;
+ inputElem.id = "addressCol2#" + rowNumber;
+ inputElem.setAttribute("aria-labelledby", popupElem.id);
+}
+
+function awSetInputAndPopupValue(inputElem, inputValue, popupElem, popupValue, rowNumber)
+{
+ inputElem.value = inputValue.trimLeft();
+
+ popupElem.selectedItem = popupElem.childNodes[0].childNodes[awGetSelectItemIndex(popupValue)];
+
+ if (rowNumber >= 0)
+ awSetInputAndPopupId(inputElem, popupElem, rowNumber);
+
+ _awSetAutoComplete(popupElem, inputElem);
+
+ onRecipientsChanged(true);
+}
+
+function _awSetInputAndPopup(inputValue, popupValue, 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.getElementsByTagName(awInputElementName());
+ var select = newNode.getElementsByTagName(awSelectElementName());
+
+ if (input && input.length == 1 && select && select.length == 1)
+ awSetInputAndPopupValue(input[0], inputValue, select[0], popupValue, top.MAX_RECIPIENTS)
+}
+
+function awSetInputAndPopup(inputValue, popupValue, parentNode, templateNode)
+{
+ if ( inputValue && popupValue )
+ {
+ var addressArray = inputValue.split(",");
+
+ for ( var index = 0; index < addressArray.length; index++ )
+ _awSetInputAndPopup(addressArray[index], popupValue, parentNode, templateNode);
+ }
+}
+
+function awSetInputAndPopupFromArray(inputArray, popupValue, parentNode, templateNode)
+{
+ if (popupValue)
+ {
+ for (let recipient of inputArray)
+ _awSetInputAndPopup(recipient, popupValue, parentNode, templateNode);
+ }
+}
+
+function awRemoveRecipients(msgCompFields, recipientType, recipientsList)
+{
+ if (!msgCompFields)
+ return;
+
+ var recipientArray = msgCompFields.splitRecipients(recipientsList, false);
+
+ for (var index = 0; index < recipientArray.length; index++)
+ for (var row = 1; row <= top.MAX_RECIPIENTS; row ++)
+ {
+ var popup = awGetPopupElement(row);
+ if (popup.value == recipientType) {
+ var input = awGetInputElement(row);
+ if (input.value == recipientArray[index])
+ {
+ awSetInputAndPopupValue(input, "", popup, "addr_to", -1);
+ break;
+ }
+ }
+ }
+}
+
+function awAddRecipients(msgCompFields, recipientType, recipientsList)
+{
+ if (!msgCompFields)
+ return;
+
+ var recipientArray = msgCompFields.splitRecipients(recipientsList, false);
+
+ for (var index = 0; index < recipientArray.length; index++)
+ awAddRecipient(recipientType, recipientArray[index]);
+}
+
+// this was broken out of awAddRecipients so it can be re-used...adds a new row matching recipientType and
+// drops in the single address.
+function awAddRecipient(recipientType, address)
+{
+ for (var row = 1; row <= top.MAX_RECIPIENTS; row ++)
+ {
+ if (awGetInputElement(row).value == "")
+ break;
+ }
+
+ if (row > top.MAX_RECIPIENTS)
+ awAppendNewRow(false);
+
+ awSetInputAndPopupValue(awGetInputElement(row), address, awGetPopupElement(row), recipientType, row);
+
+ /* be sure we still have an empty row left at the end */
+ if (row == top.MAX_RECIPIENTS)
+ {
+ awAppendNewRow(true);
+ awSetInputAndPopupValue(awGetInputElement(top.MAX_RECIPIENTS), "", awGetPopupElement(top.MAX_RECIPIENTS), recipientType, top.MAX_RECIPIENTS);
+ }
+
+ // add the recipient to our spell check ignore list
+ addRecipientsToIgnoreList(address);
+}
+
+function awTestRowSequence()
+{
+ /*
+ This function is for debug and testing purpose only, normal users should not run it!
+
+ Everytime 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
+ */
+
+ var test_sequence;
+ if (Services.prefs.getPrefType("mail.debug.test_addresses_sequence") == Ci.nsIPrefBranch.PREF_BOOL)
+ test_sequence = Services.prefs.getBoolPref("mail.debug.test_addresses_sequence");
+ if (!test_sequence)
+ return true;
+
+ /* debug code to verify the sequence still good */
+
+ var listbox = document.getElementById('addressingWidget');
+ var listitems = listbox.getElementsByTagName('listitem');
+ if (listitems.length >= top.MAX_RECIPIENTS )
+ {
+ for (var i = 1; i <= listitems.length; i ++)
+ {
+ var item = listitems [i - 1];
+ let inputID = item.querySelector(awInputElementName()).id.split("#")[1];
+ let popupID = item.querySelector(awSelectElementName()).id.split("#")[1];
+ if (inputID != i || 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 awCleanupRows()
+{
+ var maxRecipients = top.MAX_RECIPIENTS;
+ var rowID = 1;
+
+ for (var row = 1; row <= maxRecipients; row ++)
+ {
+ var inputElem = awGetInputElement(row);
+ if (inputElem.value == "" && row < maxRecipients)
+ awRemoveRow(awGetRowByInputElement(inputElem));
+ else
+ {
+ awSetInputAndPopupId(inputElem, awGetPopupElement(row), rowID);
+ rowID ++;
+ }
+ }
+
+ awTestRowSequence();
+}
+
+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);
+
+ // assume 2 column update (input and popup)
+ for (var row = rowToDelete + 1; row <= maxRecipients; row ++)
+ awSetInputAndPopupId(awGetInputElement(row), awGetPopupElement(row), (row-1));
+
+ awTestRowSequence();
+}
+
+function awClickEmptySpace(target, setFocus)
+{
+ if (target == null ||
+ (target.localName != "listboxbody" &&
+ target.localName != "listcell" &&
+ target.localName != "listitem"))
+ 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);
+ let nextInput = awGetInputElement(row+1);
+
+ if ( !nextInput )
+ {
+ if ( inputElement.value )
+ awAppendNewRow(true);
+ else // No address entered, switch to Subject field
+ {
+ var subjectField = document.getElementById( 'msgSubject' );
+ subjectField.select();
+ subjectField.focus();
+ }
+ }
+ else
+ {
+ nextInput.select();
+ awSetFocusTo(nextInput);
+ }
+
+ // be sure to add the recipient to our ignore list
+ // when the user hits enter in an autocomplete widget...
+ addRecipientsToIgnoreList(inputElement.value);
+}
+
+function awDeleteHit(inputElement)
+{
+ let row = awGetRowByInputElement(inputElement);
+
+ /* 1. don't delete the row if it's the last one remaining, just reset it! */
+ if (top.MAX_RECIPIENTS <= 1)
+ {
+ inputElement.value = "";
+ return;
+ }
+
+ /* 2. Set the focus to the previous field if possible */
+ // Note: awSetFocusTo() is asynchronous, i.e. we'll focus after row removal.
+ if (row > 1)
+ awSetFocusTo(awGetInputElement(row - 1))
+ else
+ awSetFocusTo(awGetInputElement(2))
+
+ /* 3. Delete the row */
+ awDeleteRow(row);
+}
+
+function awAppendNewRow(setFocus)
+{
+ var listbox = document.getElementById('addressingWidget');
+ var listitem1 = awGetListItem(1);
+
+ if ( listbox && listitem1 )
+ {
+ var lastRecipientType = awGetPopupElement(top.MAX_RECIPIENTS).value;
+
+ var nextDummy = awGetNextDummyRow();
+ var newNode = listitem1.cloneNode(true);
+ if (nextDummy)
+ listbox.replaceChild(newNode, nextDummy);
+ else
+ listbox.appendChild(newNode);
+
+ top.MAX_RECIPIENTS++;
+
+ var input = newNode.getElementsByTagName(awInputElementName());
+ if ( input && input.length == 1 )
+ {
+ input[0].value = "";
+
+ // We always clone the first row. The problem is that the first row
+ // could be focused. When we clone that row, we end up with a cloned
+ // XUL textbox that has a focused attribute set. Therefore we think
+ // we're focused and don't properly refocus. The best solution to this
+ // would be to clone a template row that didn't really have any presentation,
+ // rather than using the real visible first row of the listbox.
+ //
+ // For now we'll just put in a hack that ensures the focused attribute
+ // is never copied when the node is cloned.
+ if (input[0].getAttribute('focused') != '')
+ input[0].removeAttribute('focused');
+ }
+ var select = newNode.getElementsByTagName(awSelectElementName());
+ if ( select && select.length == 1 )
+ {
+ // It only makes sense to clone some field types; others
+ // should not be cloned, since it just makes the user have
+ // to go to the trouble of selecting something else. In such
+ // cases let's default to 'To' (a reasonable default since
+ // we already default to 'To' on the first dummy field of
+ // a new message).
+ switch (lastRecipientType)
+ {
+ case "addr_reply":
+ case "addr_other":
+ select[0].selectedIndex = awGetSelectItemIndex("addr_to");
+ break;
+ case "addr_followup":
+ select[0].selectedIndex = awGetSelectItemIndex("addr_newsgroups");
+ break;
+ default:
+ // e.g. "addr_to","addr_cc","addr_bcc","addr_newsgroups":
+ select[0].selectedIndex = awGetSelectItemIndex(lastRecipientType);
+ }
+
+ awSetInputAndPopupId(input[0], select[0], top.MAX_RECIPIENTS);
+
+ if (input)
+ _awSetAutoComplete(select[0], input[0]);
+ }
+
+ // Focus the new input widget.
+ if (setFocus && input[0] )
+ awSetFocusTo(input[0]);
+ }
+}
+
+// functions for accessing the elements in the addressing widget
+
+/**
+ * Returns the recipient type popup for a row.
+ *
+ * @param row Index of the recipient row to return. Starts at 1.
+ * @return This returns the menulist (not its child menupopup), despite the
+ * function name.
+ */
+function awGetPopupElement(row)
+{
+ return document.getElementById("addressCol1#" + row);
+}
+
+/**
+ * Returns the recipient inputbox for a row.
+ *
+ * @param row Index of the recipient row to return. Starts at 1.
+ * @return This returns the textbox element.
+ */
+function awGetInputElement(row)
+{
+ return document.getElementById("addressCol2#" + 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)
+ {
+ var listitems = listbox.getElementsByTagName('listitem');
+ if ( listitems && listitems.length >= row )
+ return listitems[row-1];
+ }
+ return 0;
+}
+
+function awGetRowByInputElement(inputElement)
+{
+ var row = 0;
+ if (inputElement) {
+ var listitem = inputElement.parentNode.parentNode;
+ while (listitem) {
+ if (listitem.localName == "listitem")
+ ++row;
+ listitem = listitem.previousSibling;
+ }
+ }
+ return row;
+}
+
+
+// Copy Node - copy this node and insert ahead of the (before) node. Append to end if before=0
+function awCopyNode(node, parentNode, beforeNode)
+{
+ var newNode = node.cloneNode(true);
+
+ if ( beforeNode )
+ parentNode.insertBefore(newNode, beforeNode);
+ else
+ parentNode.appendChild(newNode);
+
+ return newNode;
+}
+
+// remove row
+
+function awRemoveRow(row)
+{
+ awGetListItem(row).remove();
+ awFitDummyRows();
+
+ top.MAX_RECIPIENTS --;
+}
+
+/**
+ * Set focus to the specified element, typically a recipient input element.
+ * We do this asynchronusly to allow other processes like adding or removing rows
+ * to complete before shifting focus.
+ *
+ * @param 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();
+}
+
+// Deprecated - use awSetFocusTo() instead.
+// ### TODO: This function should be removed if we're sure addons aren't using it.
+function awSetFocus(row, inputElement) {
+ awSetFocusTo(inputElement);
+}
+
+function awTabFromRecipient(element, event) {
+ var row = awGetRowByInputElement(element);
+ if (!event.shiftKey && row < top.MAX_RECIPIENTS) {
+ var listBoxRow = row - 1; // listbox row indices are 0-based, ours are 1-based.
+ var listBox = document.getElementById("addressingWidget");
+ listBox.listBoxObject.ensureIndexIsVisible(listBoxRow + 1);
+ }
+
+ // be sure to add the recipient to our ignore list
+ // when the user tabs out of an autocomplete line...
+ addRecipientsToIgnoreList(element.value);
+}
+
+function awTabFromMenulist(element, event)
+{
+ var row = awGetRowByInputElement(element);
+ if (event.shiftKey && row > 1) {
+ var listBoxRow = row - 1; // listbox row indices are 0-based, ours are 1-based.
+ var listBox = document.getElementById("addressingWidget");
+ listBox.listBoxObject.ensureIndexIsVisible(listBoxRow - 1);
+ }
+}
+
+function awGetNumberOfRecipients()
+{
+ return top.MAX_RECIPIENTS;
+}
+
+function DropOnAddressingTarget(event, onWidget) {
+ let dragSession = gDragService.getCurrentSession();
+
+ let trans = Cc["@mozilla.org/widget/transferable;1"]
+ .createInstance(Ci.nsITransferable);
+ trans.init(getLoadContext());
+ trans.addDataFlavor("text/x-moz-address");
+
+ let added = false;
+ for (let i = 0; i < dragSession.numDropItems; ++i) {
+ dragSession.getData(trans, i);
+ let dataObj = {};
+ let bestFlavor = {};
+ let len = {};
+
+ // Ensure we catch any empty data that may have slipped through.
+ try {
+ trans.getAnyTransferData(bestFlavor, dataObj, len);
+ } catch(ex) {
+ continue;
+ }
+ if (dataObj) {
+ dataObj = dataObj.value.QueryInterface(Ci.nsISupportsString);
+ }
+ if (!dataObj) {
+ continue;
+ }
+
+ // Pull the address out of the data object.
+ let address = dataObj.data.substring(0, len.value);
+ if (!address) {
+ continue;
+ }
+
+ if (onWidget) {
+ // Break down and add each address.
+ parseAndAddAddresses(address,
+ awGetPopupElement(top.MAX_RECIPIENTS).value);
+ } else {
+ // Add address into the bucket.
+ DropRecipient(address);
+ }
+ added = true;
+ }
+
+ // We added at least one address during the drop.
+ // Disable the default handler and stop propagating the event
+ // to avoid data being dropped twice.
+ if (added) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+}
+
+function _awSetAutoComplete(selectElem, inputElem)
+{
+ let params = JSON.parse(inputElem.getAttribute('autocompletesearchparam'));
+ params.type = selectElem.value;
+ inputElem.setAttribute('autocompletesearchparam', JSON.stringify(params));
+}
+
+function awSetAutoComplete(rowNumber)
+{
+ var inputElem = awGetInputElement(rowNumber);
+ var selectElem = awGetPopupElement(rowNumber);
+ _awSetAutoComplete(selectElem, inputElem)
+}
+
+function awRecipientTextCommand(userAction, element)
+{
+ if (userAction == "typing" || userAction == "scrolling")
+ awReturnHit(element);
+}
+
+// Called when an autocomplete session item is selected and the status of
+// the session it was selected from is nsIAutoCompleteStatus::failureItems.
+//
+// As of this writing, the only way that can happen is when an LDAP
+// autocomplete session returns an error to be displayed to the user.
+//
+// There are hardcoded messages in here, but these are just fallbacks for
+// when string bundles have already failed us.
+//
+function awRecipientErrorCommand(errItem, element)
+{
+ // remove the angle brackets from the general error message to construct
+ // the title for the alert. someday we'll pass this info using a real
+ // exception object, and then this code can go away.
+ //
+ var generalErrString;
+ if (errItem.value != "") {
+ generalErrString = errItem.value.slice(1, errItem.value.length-1);
+ } else {
+ generalErrString = "Unknown LDAP server problem encountered";
+ }
+
+ // try and get the string of the specific error to contruct the complete
+ // err msg, otherwise fall back to something generic. This message is
+ // handed to us as an nsISupportsString in the param slot of the
+ // autocomplete error item, by agreement documented in
+ // nsILDAPAutoCompFormatter.idl
+ //
+ var specificErrString = "";
+ try {
+ var specificError = errItem.param.QueryInterface(Ci.nsISupportsString);
+ specificErrString = specificError.data;
+ } catch (ex) {
+ }
+ if (specificErrString == "") {
+ specificErrString = "Internal error";
+ }
+
+ Services.prompt.alert(window, generalErrString, specificErrString);
+}
+
+function awRecipientKeyPress(event, element)
+{
+ switch(event.key) {
+ case "ArrowUp":
+ awArrowHit(element, -1);
+ break;
+ case "ArrowDown":
+ awArrowHit(element, 1);
+ break;
+ case "Enter":
+ case "Tab":
+ // if the user text contains a comma or a line return, ignore
+ if (element.value.includes(',')) {
+ var addresses = element.value;
+ element.value = ""; // clear out the current line so we don't try to autocomplete it..
+ parseAndAddAddresses(addresses, awGetPopupElement(awGetRowByInputElement(element)).value);
+ }
+ else if (event.key == "Tab")
+ awTabFromRecipient(element, event);
+
+ break;
+ }
+}
+
+function awArrowHit(inputElement, direction)
+{
+ var row = awGetRowByInputElement(inputElement) + direction;
+ if (row) {
+ var nextInput = awGetInputElement(row);
+
+ if (nextInput)
+ awSetFocusTo(nextInput);
+ else if (inputElement.value)
+ awAppendNewRow(true);
+ }
+}
+
+function awRecipientKeyDown(event, element)
+{
+ switch(event.key) {
+ case "Delete":
+ case "Backspace":
+ /* do not query directly the value of the text field else the autocomplete widget could potentially
+ alter it value while doing some internal cleanup, instead, query the value through the first child
+ */
+ if (!element.value)
+ awDeleteHit(element);
+
+ //We need to stop the event else the listbox will receive it and the function
+ //awKeyDown will be executed!
+ event.stopPropagation();
+ break;
+ }
+}
+
+function awKeyDown(event, listboxElement)
+{
+ switch(event.key) {
+ case "Delete":
+ case "Backspace":
+ /* Warning, the listboxElement.selectedItems will change everytime we delete a row */
+ var length = listboxElement.selectedCount;
+ for (var i = 1; i <= length; i++) {
+ var inputs = listboxElement.selectedItem.getElementsByTagName(awInputElementName());
+ if (inputs && inputs.length == 1)
+ awDeleteHit(inputs[0]);
+ }
+ break;
+ }
+}
+
+function awMenulistKeyPress(event, element)
+{
+ switch(event.key) {
+ case "Tab":
+ awTabFromMenulist(element, event);
+ break;
+ }
+}
+
+/* ::::::::::: addressing widget dummy rows ::::::::::::::::: */
+
+var gAWContentHeight = 0;
+var gAWRowHeight = 0;
+
+function awFitDummyRows()
+{
+ awCalcContentHeight();
+ awCreateOrRemoveDummyRows();
+}
+
+function awCreateOrRemoveDummyRows()
+{
+ var listbox = document.getElementById("addressingWidget");
+ var listboxHeight = listbox.boxObject.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.getElementsByTagName("listitem");
+
+ 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].boxObject.height;
+ ++i;
+ } while (i < items.length && !gAWRowHeight);
+ gAWContentHeight = gAWRowHeight*items.length;
+ }
+}
+
+function awCreateDummyItem(aParent)
+{
+ var titem = document.createElement("listitem");
+ titem.setAttribute("_isDummyRow", "true");
+ titem.setAttribute("class", "dummy-row");
+
+ for (var i = awGetNumberOfCols(); i > 0; i--)
+ awCreateDummyCell(titem);
+
+ if (aParent)
+ aParent.appendChild(titem);
+
+ return titem;
+}
+
+function awCreateDummyCell(aParent)
+{
+ var cell = document.createElement("listcell");
+ 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]');
+}
+
+function awSizerListen()
+{
+ // when splitter is clicked, fill in necessary dummy rows each time the mouse is moved
+ awCalcContentHeight(); // precalculate
+ document.addEventListener("mousemove", awSizerMouseMove, true);
+ document.addEventListener("mouseup", awSizerMouseUp);
+}
+
+function awSizerMouseMove()
+{
+ awCreateOrRemoveDummyRows(2);
+}
+
+function awSizerMouseUp()
+{
+ document.removeEventListener("mousemove", awSizerMouseMove);
+ document.removeEventListener("mouseup", awSizerMouseUp);
+}
+
+function awSizerResized(aSplitter)
+{
+ // set the height on the listbox rather than on the toolbox
+ var listbox = document.getElementById("addressingWidget");
+ listbox.height = listbox.boxObject.height;
+ // remove all the heights set on the splitter's previous siblings
+ for (let sib = aSplitter.previousSibling; sib; sib = sib.previousSibling)
+ sib.removeAttribute("height");
+}
+
+function awDocumentKeyPress(event)
+{
+ try {
+ var id = event.target.id;
+ if (id.startsWith('addressCol1'))
+ awRecipientKeyPress(event, event.target);
+ } catch (e) { }
+}
+
+function awRecipientInputCommand(event, inputElement)
+{
+ gContentChanged=true;
+ setupAutocomplete();
+}
+
+// Given an arbitrary block of text like a comma delimited list of names or a names separated by spaces,
+// we will try to autocomplete each of the names and then take the FIRST match for each name, adding it the
+// addressing widget on the compose window.
+
+var gAutomatedAutoCompleteListener = null;
+
+function parseAndAddAddresses(addressText, recipientType)
+{
+ // strip any leading >> characters inserted by the autocomplete widget
+ var strippedAddresses = addressText.replace(/.* >> /, "");
+
+ var addresses = MailServices.headerParser
+ .makeFromDisplayAddress(strippedAddresses);
+
+ if (addresses.length)
+ {
+ // we need to set up our own autocomplete session and search for results
+
+ setupAutocomplete(); // be safe, make sure we are setup
+ if (!gAutomatedAutoCompleteListener)
+ gAutomatedAutoCompleteListener = new AutomatedAutoCompleteHandler();
+
+ gAutomatedAutoCompleteListener.init(addresses.map(addr => addr.toString()),
+ addresses.length, recipientType);
+ }
+}
+
+function AutomatedAutoCompleteHandler()
+{
+}
+
+// state driven self contained object which will autocomplete a block of addresses without any UI.
+// force picks the first match and adds it to the addressing widget, then goes on to the next
+// name to complete.
+
+AutomatedAutoCompleteHandler.prototype =
+{
+ param: this,
+ sessionName: null,
+ namesToComplete: {},
+ numNamesToComplete: 0,
+ indexIntoNames: 0,
+
+ numSessionsToSearch: 0,
+ numSessionsSearched: 0,
+ recipientType: null,
+ searchResults: null,
+
+ init:function(namesToComplete, numNamesToComplete, recipientType)
+ {
+ this.indexIntoNames = 0;
+ this.numNamesToComplete = numNamesToComplete;
+ this.namesToComplete = namesToComplete;
+
+ this.recipientType = recipientType;
+
+ // set up the auto complete sessions to use
+ setupAutocomplete();
+ this.autoCompleteNextAddress();
+ },
+
+ autoCompleteNextAddress:function()
+ {
+ this.numSessionsToSearch = 0;
+ this.numSessionsSearched = 0;
+ this.searchResults = new Array;
+
+ if (this.indexIntoNames < this.numNamesToComplete && this.namesToComplete[this.indexIntoNames])
+ {
+ /* XXX This is used to work, until switching to the new toolkit broke it
+ We should fix it see bug 456550.
+ if (!this.namesToComplete[this.indexIntoNames].includes('@')) // don't autocomplete if address has an @ sign in it
+ {
+ // make sure total session count is updated before we kick off ANY actual searches
+ if (gAutocompleteSession)
+ this.numSessionsToSearch++;
+
+ if (gLDAPSession && gCurrentAutocompleteDirectory)
+ this.numSessionsToSearch++;
+
+ if (gAutocompleteSession)
+ {
+ gAutocompleteSession.onAutoComplete(this.namesToComplete[this.indexIntoNames], null, this);
+ // AB searches are actually synchronous. So by the time we get here we have already looked up results.
+
+ // if we WERE going to also do an LDAP lookup, then check to see if we have a valid match in the AB, if we do
+ // don't bother with the LDAP search too just return
+
+ if (gLDAPSession && gCurrentAutocompleteDirectory && this.searchResults[0] && this.searchResults[0].defaultItemIndex != -1)
+ {
+ this.processAllResults();
+ return;
+ }
+ }
+
+ if (gLDAPSession && gCurrentAutocompleteDirectory)
+ gLDAPSession.onStartLookup(this.namesToComplete[this.indexIntoNames], null, this);
+ }
+ */
+
+ if (!this.numSessionsToSearch)
+ this.processAllResults(); // ldap and ab are turned off, so leave text alone
+ }
+ },
+
+ onStatus:function(aStatus)
+ {
+ return;
+ },
+
+ onAutoComplete: function(aResults, aStatus)
+ {
+ // store the results until all sessions are done and have reported in
+ if (aResults)
+ this.searchResults[this.numSessionsSearched] = aResults;
+
+ this.numSessionsSearched++; // bump our counter
+
+ if (this.numSessionsToSearch <= this.numSessionsSearched)
+ setTimeout('gAutomatedAutoCompleteListener.processAllResults()', 0); // we are all done
+ },
+
+ processAllResults: function()
+ {
+ // Take the first result and add it to the compose window
+ var addressToAdd;
+
+ // loop through the results looking for the non default case (default case is the address book with only one match, the default domain)
+ var sessionIndex;
+
+ var searchResultsForSession;
+
+ for (sessionIndex in this.searchResults)
+ {
+ searchResultsForSession = this.searchResults[sessionIndex];
+ if (searchResultsForSession && searchResultsForSession.defaultItemIndex > -1)
+ {
+ addressToAdd = searchResultsForSession.items
+ .queryElementAt(searchResultsForSession.defaultItemIndex,
+ Ci.nsIAutoCompleteItem).value;
+ break;
+ }
+ }
+
+ // still no match? loop through looking for the -1 default index
+ if (!addressToAdd)
+ {
+ for (sessionIndex in this.searchResults)
+ {
+ searchResultsForSession = this.searchResults[sessionIndex];
+ if (searchResultsForSession && searchResultsForSession.defaultItemIndex == -1)
+ {
+ addressToAdd = searchResultsForSession.items
+ .queryElementAt(0, Ci.nsIAutoCompleteItem).value;
+ break;
+ }
+ }
+ }
+
+ // no matches anywhere...just use what we were given
+ if (!addressToAdd)
+ addressToAdd = this.namesToComplete[this.indexIntoNames];
+
+ // that will automatically set the focus on a new available row, and make sure it is visible
+ awAddRecipient(this.recipientType ? this.recipientType : "addr_to", addressToAdd);
+
+ this.indexIntoNames++;
+ this.autoCompleteNextAddress();
+ },
+
+ QueryInterface : function(iid)
+ {
+ if (iid.equals(Ci.nsIAutoCompleteListener) ||
+ iid.equals(Ci.nsISupports))
+ return this;
+ throw Cr.NS_NOINTERFACE;
+ }
+}