PageRenderTime 85ms CodeModel.GetById 38ms RepoModel.GetById 1ms app.codeStats 0ms

/thunderbird-14.0/comm-release/mail/components/compose/content/addressingWidgetOverlay.js

#
JavaScript | 1240 lines | 914 code | 187 blank | 139 comment | 189 complexity | cde6dd70e75cc649f4404d734c69807c MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, JSON, LGPL-3.0, AGPL-1.0, CC-BY-SA-3.0, LGPL-2.1, BSD-3-Clause, GPL-2.0, Apache-2.0, 0BSD, MIT
  1. # -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
  2. # ***** BEGIN LICENSE BLOCK *****
  3. # Version: MPL 1.1/GPL 2.0/LGPL 2.1
  4. #
  5. # The contents of this file are subject to the Mozilla Public License Version
  6. # 1.1 (the "License"); you may not use this file except in compliance with
  7. # the License. You may obtain a copy of the License at
  8. # http://www.mozilla.org/MPL/
  9. #
  10. # Software distributed under the License is distributed on an "AS IS" basis,
  11. # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  12. # for the specific language governing rights and limitations under the
  13. # License.
  14. #
  15. # The Original Code is Mozilla Communicator client code, released
  16. # March 31, 1998.
  17. #
  18. # The Initial Developer of the Original Code is
  19. # Netscape Communications Corporation.
  20. # Portions created by the Initial Developer are Copyright (C) 1998-1999
  21. # the Initial Developer. All Rights Reserved.
  22. #
  23. # Contributor(s):
  24. #
  25. # Alternatively, the contents of this file may be used under the terms of
  26. # either the GNU General Public License Version 2 or later (the "GPL"), or
  27. # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  28. # in which case the provisions of the GPL or the LGPL are applicable instead
  29. # of those above. If you wish to allow use of your version of this file only
  30. # under the terms of either the GPL or the LGPL, and not to allow others to
  31. # use your version of this file under the terms of the MPL, indicate your
  32. # decision by deleting the provisions above and replace them with the notice
  33. # and other provisions required by the GPL or the LGPL. If you do not delete
  34. # the provisions above, a recipient may use your version of this file under
  35. # the terms of any one of the MPL, the GPL or the LGPL.
  36. #
  37. # ***** END LICENSE BLOCK *****
  38. top.MAX_RECIPIENTS = 1; /* for the initial listitem created in the XUL */
  39. var inputElementType = "";
  40. var selectElementType = "";
  41. var selectElementIndexTable = null;
  42. var gNumberOfCols = 0;
  43. var gDragService = Components.classes["@mozilla.org/widget/dragservice;1"].getService();
  44. gDragService = gDragService.QueryInterface(Components.interfaces.nsIDragService);
  45. var gMimeHeaderParser = null;
  46. var test_addresses_sequence = false;
  47. try {
  48. if (sPrefs)
  49. test_addresses_sequence = sPrefs.getBoolPref("mail.debug.test_addresses_sequence");
  50. }
  51. catch (ex) {}
  52. function awGetMaxRecipients()
  53. {
  54. return top.MAX_RECIPIENTS;
  55. }
  56. function awGetNumberOfCols()
  57. {
  58. if (gNumberOfCols == 0)
  59. {
  60. var listbox = document.getElementById('addressingWidget');
  61. var listCols = listbox.getElementsByTagName('listcol');
  62. gNumberOfCols = listCols.length;
  63. if (!gNumberOfCols)
  64. gNumberOfCols = 1; /* if no cols defined, that means we have only one! */
  65. }
  66. return gNumberOfCols;
  67. }
  68. function awInputElementName()
  69. {
  70. if (inputElementType == "")
  71. inputElementType = document.getElementById("addressCol2#1").localName;
  72. return inputElementType;
  73. }
  74. function awSelectElementName()
  75. {
  76. if (selectElementType == "")
  77. selectElementType = document.getElementById("addressCol1#1").localName;
  78. return selectElementType;
  79. }
  80. // TODO: replace awGetSelectItemIndex with recipient type index constants
  81. function awGetSelectItemIndex(itemData)
  82. {
  83. if (selectElementIndexTable == null)
  84. {
  85. selectElementIndexTable = new Object();
  86. var selectElem = document.getElementById("addressCol1#1");
  87. for (var i = 0; i < selectElem.childNodes[0].childNodes.length; i ++)
  88. {
  89. var aData = selectElem.childNodes[0].childNodes[i].getAttribute("value");
  90. selectElementIndexTable[aData] = i;
  91. }
  92. }
  93. return selectElementIndexTable[itemData];
  94. }
  95. function Recipients2CompFields(msgCompFields)
  96. {
  97. if (msgCompFields)
  98. {
  99. var i = 1;
  100. var addrTo = "";
  101. var addrCc = "";
  102. var addrBcc = "";
  103. var addrReply = "";
  104. var addrNg = "";
  105. var addrFollow = "";
  106. var addrOther = "";
  107. var to_Sep = "";
  108. var cc_Sep = "";
  109. var bcc_Sep = "";
  110. var reply_Sep = "";
  111. var ng_Sep = "";
  112. var follow_Sep = "";
  113. gMimeHeaderParser = Components.classes["@mozilla.org/messenger/headerparser;1"].getService(Components.interfaces.nsIMsgHeaderParser);
  114. var recipientType;
  115. var inputField;
  116. var fieldValue;
  117. var recipient;
  118. while ((inputField = awGetInputElement(i)))
  119. {
  120. fieldValue = inputField.value;
  121. if (fieldValue == null)
  122. fieldValue = inputField.getAttribute("value");
  123. if (fieldValue != "")
  124. {
  125. recipientType = awGetPopupElement(i).selectedItem.getAttribute("value");
  126. recipient = null;
  127. switch (recipientType)
  128. {
  129. case "addr_to" :
  130. case "addr_cc" :
  131. case "addr_bcc" :
  132. case "addr_reply" :
  133. try {
  134. recipient = gMimeHeaderParser.reformatUnquotedAddresses(fieldValue);
  135. } catch (ex) {recipient = fieldValue;}
  136. break;
  137. }
  138. switch (recipientType)
  139. {
  140. case "addr_to" : addrTo += to_Sep + recipient; to_Sep = ","; break;
  141. case "addr_cc" : addrCc += cc_Sep + recipient; cc_Sep = ","; break;
  142. case "addr_bcc" : addrBcc += bcc_Sep + recipient; bcc_Sep = ","; break;
  143. case "addr_reply" : addrReply += reply_Sep + recipient; reply_Sep = ","; break;
  144. case "addr_newsgroups" : addrNg += ng_Sep + fieldValue; ng_Sep = ","; break;
  145. case "addr_followup" : addrFollow += follow_Sep + fieldValue; follow_Sep = ","; break;
  146. // do CRLF, same as PUSH_NEWLINE() in nsMsgSend.h / nsMsgCompUtils.cpp
  147. // see bug #195965
  148. case "addr_other" : addrOther += awGetPopupElement(i).selectedItem.getAttribute("label") + " " + fieldValue + "\r\n";break;
  149. }
  150. }
  151. i ++;
  152. }
  153. msgCompFields.to = addrTo;
  154. msgCompFields.cc = addrCc;
  155. msgCompFields.bcc = addrBcc;
  156. msgCompFields.replyTo = addrReply;
  157. msgCompFields.newsgroups = addrNg;
  158. msgCompFields.followupTo = addrFollow;
  159. msgCompFields.otherRandomHeaders = addrOther;
  160. gMimeHeaderParser = null;
  161. }
  162. else
  163. dump("Message Compose Error: msgCompFields is null (ExtractRecipients)");
  164. }
  165. function CompFields2Recipients(msgCompFields)
  166. {
  167. if (msgCompFields) {
  168. gMimeHeaderParser = Components.classes["@mozilla.org/messenger/headerparser;1"].getService(Components.interfaces.nsIMsgHeaderParser);
  169. var listbox = document.getElementById('addressingWidget');
  170. var newListBoxNode = listbox.cloneNode(false);
  171. var listBoxColsClone = listbox.firstChild.cloneNode(true);
  172. newListBoxNode.appendChild(listBoxColsClone);
  173. var templateNode = listbox.getElementsByTagName("listitem")[0];
  174. // dump("replacing child in comp fields 2 recips \n");
  175. listbox.parentNode.replaceChild(newListBoxNode, listbox);
  176. top.MAX_RECIPIENTS = 0;
  177. var msgReplyTo = msgCompFields.replyTo;
  178. var msgTo = msgCompFields.to;
  179. var msgCC = msgCompFields.cc;
  180. var msgBCC = msgCompFields.bcc;
  181. var msgRandomHeaders = msgCompFields.otherRandomHeaders;
  182. var msgNewsgroups = msgCompFields.newsgroups;
  183. var msgFollowupTo = msgCompFields.followupTo;
  184. var havePrimaryRecipient = false;
  185. if(msgReplyTo)
  186. awSetInputAndPopupFromArray(msgCompFields.splitRecipients(msgReplyTo, false, {}),
  187. "addr_reply", newListBoxNode, templateNode);
  188. if(msgTo)
  189. {
  190. var rcp = msgCompFields.splitRecipients(msgTo, false, {});
  191. if (rcp.length)
  192. {
  193. awSetInputAndPopupFromArray(rcp, "addr_to", newListBoxNode, templateNode);
  194. havePrimaryRecipient = true;
  195. }
  196. }
  197. if(msgCC)
  198. awSetInputAndPopupFromArray(msgCompFields.splitRecipients(msgCC, false, {}),
  199. "addr_cc", newListBoxNode, templateNode);
  200. if(msgBCC)
  201. awSetInputAndPopupFromArray(msgCompFields.splitRecipients(msgBCC, false, {}),
  202. "addr_bcc", newListBoxNode, templateNode);
  203. if(msgRandomHeaders)
  204. awSetInputAndPopup(msgRandomHeaders, "addr_other", newListBoxNode, templateNode);
  205. if(msgNewsgroups)
  206. {
  207. awSetInputAndPopup(msgNewsgroups, "addr_newsgroups", newListBoxNode, templateNode);
  208. havePrimaryRecipient = true;
  209. }
  210. if(msgFollowupTo)
  211. awSetInputAndPopup(msgFollowupTo, "addr_followup", newListBoxNode, templateNode);
  212. // If it's a new message, we need to add an extra empty recipient.
  213. if (!havePrimaryRecipient)
  214. _awSetInputAndPopup("", "addr_to", newListBoxNode, templateNode);
  215. awFitDummyRows(2);
  216. // CompFields2Recipients is called whenever a user replies or edits an existing message. We want to
  217. // add all of the recipients for this message to the ignore list for spell check
  218. addRecipientsToIgnoreList((gCurrentIdentity ? gCurrentIdentity.identityName + ', ' : '') + msgTo + ', ' + msgCC + ', ' + msgBCC);
  219. gMimeHeaderParser = null; //Release the mime parser
  220. }
  221. }
  222. function awSetInputAndPopupId(inputElem, popupElem, rowNumber)
  223. {
  224. popupElem.id = "addressCol1#" + rowNumber;
  225. inputElem.id = "addressCol2#" + rowNumber;
  226. inputElem.setAttribute("aria-labelledby", popupElem.id);
  227. }
  228. function awSetInputAndPopupValue(inputElem, inputValue, popupElem, popupValue, rowNumber)
  229. {
  230. // remove leading spaces
  231. while (inputValue && inputValue[0] == " " )
  232. inputValue = inputValue.substring(1, inputValue.length);
  233. inputElem.setAttribute("value", inputValue);
  234. inputElem.value = inputValue;
  235. popupElem.selectedItem = popupElem.childNodes[0].childNodes[awGetSelectItemIndex(popupValue)];
  236. if (rowNumber >= 0)
  237. awSetInputAndPopupId(inputElem, popupElem, rowNumber);
  238. _awSetAutoComplete(popupElem, inputElem);
  239. }
  240. function _awSetInputAndPopup(inputValue, popupValue, parentNode, templateNode)
  241. {
  242. top.MAX_RECIPIENTS++;
  243. var newNode = templateNode.cloneNode(true);
  244. parentNode.appendChild(newNode); // we need to insert the new node before we set the value of the select element!
  245. var input = newNode.getElementsByTagName(awInputElementName());
  246. var select = newNode.getElementsByTagName(awSelectElementName());
  247. if (input && input.length == 1 && select && select.length == 1)
  248. awSetInputAndPopupValue(input[0], inputValue, select[0], popupValue, top.MAX_RECIPIENTS)
  249. }
  250. function awSetInputAndPopup(inputValue, popupValue, parentNode, templateNode)
  251. {
  252. if ( inputValue && popupValue )
  253. {
  254. var addressArray = inputValue.split(",");
  255. for ( var index = 0; index < addressArray.length; index++ )
  256. _awSetInputAndPopup(addressArray[index], popupValue, parentNode, templateNode);
  257. }
  258. }
  259. function awSetInputAndPopupFromArray(inputArray, popupValue, parentNode, templateNode)
  260. {
  261. if (popupValue)
  262. {
  263. var recipient;
  264. for (var index = 0; index < inputArray.length; index++)
  265. {
  266. recipient = null;
  267. if (gMimeHeaderParser)
  268. try {
  269. recipient =
  270. gMimeHeaderParser.unquotePhraseOrAddrWString(inputArray[index], true);
  271. } catch (ex) {};
  272. if (!recipient)
  273. recipient = inputArray[index];
  274. _awSetInputAndPopup(recipient, popupValue, parentNode, templateNode);
  275. }
  276. }
  277. }
  278. function awRemoveRecipients(msgCompFields, recipientType, recipientsList)
  279. {
  280. if (!msgCompFields || !recipientsList)
  281. return;
  282. var recipientArray = msgCompFields.splitRecipients(recipientsList, false, {});
  283. for (var index = 0; index < recipientArray.length; index++)
  284. for (var row = 1; row <= top.MAX_RECIPIENTS; row ++)
  285. {
  286. var popup = awGetPopupElement(row);
  287. if (popup.selectedItem.getAttribute("value") == recipientType)
  288. {
  289. var input = awGetInputElement(row);
  290. if (input.value == recipientArray[index])
  291. {
  292. awSetInputAndPopupValue(input, "", popup, "addr_to", -1);
  293. break;
  294. }
  295. }
  296. }
  297. }
  298. function awAddRecipients(msgCompFields, recipientType, recipientsList)
  299. {
  300. if (!msgCompFields || !recipientsList)
  301. return;
  302. var recipientArray = msgCompFields.splitRecipients(recipientsList, false, {});
  303. for (var index = 0; index < recipientArray.length; index++)
  304. awAddRecipient(recipientType, recipientArray[index]);
  305. }
  306. // this was broken out of awAddRecipients so it can be re-used...adds a new row matching recipientType and
  307. // drops in the single address.
  308. function awAddRecipient(recipientType, address)
  309. {
  310. for (var row = 1; row <= top.MAX_RECIPIENTS; row ++)
  311. {
  312. if (awGetInputElement(row).value == "")
  313. break;
  314. }
  315. if (row > top.MAX_RECIPIENTS)
  316. awAppendNewRow(false);
  317. awSetInputAndPopupValue(awGetInputElement(row), address, awGetPopupElement(row), recipientType, row);
  318. /* be sure we still have an empty row left at the end */
  319. if (row == top.MAX_RECIPIENTS)
  320. {
  321. awAppendNewRow(true);
  322. awSetInputAndPopupValue(awGetInputElement(top.MAX_RECIPIENTS), "", awGetPopupElement(top.MAX_RECIPIENTS), "addr_to", top.MAX_RECIPIENTS);
  323. }
  324. // add the recipient to our spell check ignore list
  325. addRecipientsToIgnoreList(address);
  326. }
  327. function awTestRowSequence()
  328. {
  329. /*
  330. This function is for debug and testing purpose only, normal user should not run it!
  331. Everytime we insert or delete a row, we must be sure we didn't break the ID sequence of
  332. the addressing widget rows. This function will run a quick test to see if the sequence still ok
  333. You need to define the pref mail.debug.test_addresses_sequence to true in order to activate it
  334. */
  335. if (! test_addresses_sequence)
  336. return true;
  337. /* debug code to verify the sequence still good */
  338. var listbox = document.getElementById('addressingWidget');
  339. var listitems = listbox.getElementsByTagName('listitem');
  340. if (listitems.length >= top.MAX_RECIPIENTS )
  341. {
  342. for (var i = 1; i <= listitems.length; i ++)
  343. {
  344. var item = listitems [i - 1];
  345. var inputID = item.getElementsByTagName(awInputElementName())[0].getAttribute("id").split("#")[1];
  346. var popupID = item.getElementsByTagName(awSelectElementName())[0].getAttribute("id").split("#")[1];
  347. if (inputID != i || popupID != i)
  348. {
  349. dump("#ERROR: sequence broken at row " + i + ", inputID=" + inputID + ", popupID=" + popupID + "\n");
  350. return false;
  351. }
  352. dump("---SEQUENCE OK---\n");
  353. return true;
  354. }
  355. }
  356. else
  357. dump("#ERROR: listitems.length(" + listitems.length + ") < top.MAX_RECIPIENTS(" + top.MAX_RECIPIENTS + ")\n");
  358. return false;
  359. }
  360. function awResetAllRows()
  361. {
  362. var maxRecipients = top.MAX_RECIPIENTS;
  363. for (var row = 1; row <= maxRecipients ; row ++)
  364. {
  365. awGetInputElement(row).value = "";
  366. awGetPopupElement(row).selectedIndex = 0;
  367. }
  368. }
  369. function awCleanupRows()
  370. {
  371. var maxRecipients = top.MAX_RECIPIENTS;
  372. var rowID = 1;
  373. for (var row = 1; row <= maxRecipients; row ++)
  374. {
  375. var inputElem = awGetInputElement(row);
  376. if (inputElem.value == "" && row < maxRecipients)
  377. awRemoveRow(awGetRowByInputElement(inputElem));
  378. else
  379. {
  380. awSetInputAndPopupId(inputElem, awGetPopupElement(row), rowID);
  381. rowID ++;
  382. }
  383. }
  384. awTestRowSequence();
  385. }
  386. function awDeleteRow(rowToDelete)
  387. {
  388. /* When we delete a row, we must reset the id of others row in order to not break the sequence */
  389. var maxRecipients = top.MAX_RECIPIENTS;
  390. awRemoveRow(rowToDelete);
  391. // assume 2 column update (input and popup)
  392. for (var row = rowToDelete + 1; row <= maxRecipients; row ++)
  393. awSetInputAndPopupId(awGetInputElement(row), awGetPopupElement(row), (row-1));
  394. awTestRowSequence();
  395. }
  396. function awClickEmptySpace(target, setFocus)
  397. {
  398. if (target == null ||
  399. (target.localName != "listboxbody" &&
  400. target.localName != "listcell" &&
  401. target.localName != "listitem"))
  402. return;
  403. var lastInput = awGetInputElement(top.MAX_RECIPIENTS);
  404. if ( lastInput && lastInput.value )
  405. awAppendNewRow(setFocus);
  406. else
  407. if (setFocus)
  408. awSetFocus(top.MAX_RECIPIENTS, lastInput);
  409. }
  410. function awReturnHit(inputElement)
  411. {
  412. var row = awGetRowByInputElement(inputElement);
  413. var nextInput = awGetInputElement(row+1);
  414. if ( !nextInput )
  415. {
  416. if ( inputElement.value )
  417. awAppendNewRow(true);
  418. else // No address entered, switch to Subject field
  419. {
  420. var subjectField = document.getElementById( 'msgSubject' );
  421. subjectField.select();
  422. subjectField.focus();
  423. }
  424. }
  425. else
  426. {
  427. nextInput.select();
  428. awSetFocus(row+1, nextInput);
  429. }
  430. // be sure to add the user add recipient to our ignore list
  431. // when the user hits enter in an autocomplete widget...
  432. addRecipientsToIgnoreList(inputElement.value);
  433. }
  434. function awDeleteHit(inputElement)
  435. {
  436. var row = awGetRowByInputElement(inputElement);
  437. /* 1. don't delete the row if it's the last one remaining, just reset it! */
  438. if (top.MAX_RECIPIENTS <= 1)
  439. {
  440. inputElement.value = "";
  441. return;
  442. }
  443. /* 2. Set the focus to the previous field if possible */
  444. if (row > 1)
  445. awSetFocus(row - 1, awGetInputElement(row - 1))
  446. else
  447. awSetFocus(1, awGetInputElement(2)) /* We have to cheat a little bit because the focus will */
  448. /* be set asynchronusly after we delete the current row, */
  449. /* therefore the row number still the same! */
  450. /* 3. Delete the row */
  451. awDeleteRow(row);
  452. }
  453. function awInputChanged(inputElement)
  454. {
  455. //Do we need to add a new row?
  456. var lastInput = awGetInputElement(top.MAX_RECIPIENTS);
  457. if ( lastInput && lastInput.value && !top.doNotCreateANewRow)
  458. awAppendNewRow(false);
  459. top.doNotCreateANewRow = false;
  460. }
  461. function awAppendNewRow(setFocus)
  462. {
  463. var listbox = document.getElementById('addressingWidget');
  464. var listitem1 = awGetListItem(1);
  465. if ( listbox && listitem1 )
  466. {
  467. var lastRecipientType = awGetPopupElement(top.MAX_RECIPIENTS).selectedItem.getAttribute("value");
  468. var nextDummy = awGetNextDummyRow();
  469. var newNode = listitem1.cloneNode(true);
  470. if (nextDummy)
  471. listbox.replaceChild(newNode, nextDummy);
  472. else
  473. listbox.appendChild(newNode);
  474. top.MAX_RECIPIENTS++;
  475. var input = newNode.getElementsByTagName(awInputElementName());
  476. if ( input && input.length == 1 )
  477. {
  478. input[0].setAttribute("value", "");
  479. //this copies the autocomplete sessions list from recipient#1
  480. input[0].syncSessions(document.getElementById('addressCol2#1'));
  481. // also clone the showCommentColumn setting
  482. //
  483. input[0].showCommentColumn =
  484. document.getElementById("addressCol2#1").showCommentColumn;
  485. // We always clone the first row. The problem is that the first row
  486. // could be focused. When we clone that row, we end up with a cloned
  487. // XUL textbox that has a focused attribute set. Therefore we think
  488. // we're focused and don't properly refocus. The best solution to this
  489. // would be to clone a template row that didn't really have any presentation,
  490. // rather than using the real visible first row of the listbox.
  491. //
  492. // For now we'll just put in a hack that ensures the focused attribute
  493. // is never copied when the node is cloned.
  494. if (input[0].getAttribute('focused') != '')
  495. input[0].removeAttribute('focused');
  496. }
  497. var select = newNode.getElementsByTagName(awSelectElementName());
  498. if ( select && select.length == 1 )
  499. {
  500. // It only makes sense to clone some field types; others
  501. // should not be cloned, since it just makes the user have
  502. // to go to the trouble of selecting something else. In such
  503. // cases let's default to 'To' (a reasonable default since
  504. // we already default to 'To' on the first dummy field of
  505. // a new message).
  506. switch (lastRecipientType)
  507. {
  508. case "addr_reply":
  509. case "addr_other":
  510. select[0].selectedIndex = awGetSelectItemIndex("addr_to");
  511. break;
  512. case "addr_followup":
  513. select[0].selectedIndex = awGetSelectItemIndex("addr_newsgroups");
  514. break;
  515. default:
  516. // e.g. "addr_to","addr_cc","addr_bcc","addr_newsgroups":
  517. select[0].selectedIndex = awGetSelectItemIndex(lastRecipientType);
  518. }
  519. awSetInputAndPopupId(input[0], select[0], top.MAX_RECIPIENTS);
  520. if (input)
  521. _awSetAutoComplete(select[0], input[0]);
  522. }
  523. // focus on new input widget
  524. if (setFocus && input[0] )
  525. awSetFocus(top.MAX_RECIPIENTS, input[0]);
  526. }
  527. }
  528. // functions for accessing the elements in the addressing widget
  529. function awGetPopupElement(row)
  530. {
  531. return document.getElementById("addressCol1#" + row);
  532. }
  533. function awGetInputElement(row)
  534. {
  535. return document.getElementById("addressCol2#" + row);
  536. }
  537. function awGetElementByCol(row, col)
  538. {
  539. var colID = "addressCol" + col + "#" + row;
  540. return document.getElementById(colID);
  541. }
  542. function awGetListItem(row)
  543. {
  544. var listbox = document.getElementById('addressingWidget');
  545. if ( listbox && row > 0)
  546. {
  547. var listitems = listbox.getElementsByTagName('listitem');
  548. if ( listitems && listitems.length >= row )
  549. return listitems[row-1];
  550. }
  551. return 0;
  552. }
  553. function awGetRowByInputElement(inputElement)
  554. {
  555. var row = 0;
  556. if (inputElement) {
  557. var listitem = inputElement.parentNode.parentNode;
  558. while (listitem) {
  559. if (listitem.localName == "listitem")
  560. ++row;
  561. listitem = listitem.previousSibling;
  562. }
  563. }
  564. return row;
  565. }
  566. // Copy Node - copy this node and insert ahead of the (before) node. Append to end if before=0
  567. function awCopyNode(node, parentNode, beforeNode)
  568. {
  569. var newNode = node.cloneNode(true);
  570. if ( beforeNode )
  571. parentNode.insertBefore(newNode, beforeNode);
  572. else
  573. parentNode.appendChild(newNode);
  574. return newNode;
  575. }
  576. // remove row
  577. function awRemoveRow(row)
  578. {
  579. var listbox = document.getElementById('addressingWidget');
  580. awRemoveNodeAndChildren(listbox, awGetListItem(row));
  581. awFitDummyRows();
  582. top.MAX_RECIPIENTS --;
  583. }
  584. function awRemoveNodeAndChildren(parent, nodeToRemove)
  585. {
  586. nodeToRemove.parentNode.removeChild(nodeToRemove);
  587. }
  588. function awSetFocus(row, inputElement)
  589. {
  590. top.awRow = row;
  591. top.awInputElement = inputElement;
  592. setTimeout(_awSetFocus, 0);
  593. }
  594. function _awSetFocus()
  595. {
  596. var listbox = document.getElementById('addressingWidget');
  597. var theNewRow = awGetListItem(top.awRow);
  598. // Warning: firstVisibleRow is zero base but top.awRow is one base!
  599. var firstVisibleRow = listbox.getIndexOfFirstVisibleRow();
  600. var numOfVisibleRows = listbox.getNumberOfVisibleRows();
  601. // Do we need to scroll in order to see the selected row?
  602. if (top.awRow <= firstVisibleRow)
  603. listbox.scrollToIndex(top.awRow - 1);
  604. else if (top.awRow - 1 >= (firstVisibleRow + numOfVisibleRows))
  605. listbox.scrollToIndex(top.awRow - numOfVisibleRows);
  606. top.awInputElement.focus();
  607. }
  608. function awTabFromRecipient(element, event)
  609. {
  610. // If we are the last element in the listbox, we don't want to create a new row.
  611. if (element == awGetInputElement(top.MAX_RECIPIENTS))
  612. top.doNotCreateANewRow = true;
  613. var row = awGetRowByInputElement(element);
  614. if (!event.shiftKey && row < top.MAX_RECIPIENTS) {
  615. var listBoxRow = row - 1; // listbox row indices are 0-based, ours are 1-based.
  616. var listBox = document.getElementById("addressingWidget");
  617. listBox.listBoxObject.ensureIndexIsVisible(listBoxRow + 1);
  618. }
  619. // be sure to add the user add recipient to our ignore list
  620. // when the user tabs out of an autocomplete line...
  621. addRecipientsToIgnoreList(element.value);
  622. }
  623. function awTabFromMenulist(element, event)
  624. {
  625. var row = awGetRowByInputElement(element);
  626. if (event.shiftKey && row > 1) {
  627. var listBoxRow = row - 1; // listbox row indices are 0-based, ours are 1-based.
  628. var listBox = document.getElementById("addressingWidget");
  629. listBox.listBoxObject.ensureIndexIsVisible(listBoxRow - 1);
  630. }
  631. }
  632. function awGetNumberOfRecipients()
  633. {
  634. return top.MAX_RECIPIENTS;
  635. }
  636. function DragOverAddressingWidget(event)
  637. {
  638. var validFlavor = false;
  639. var dragSession = dragSession = gDragService.getCurrentSession();
  640. if (dragSession.isDataFlavorSupported("text/x-moz-address"))
  641. validFlavor = true;
  642. if (validFlavor)
  643. dragSession.canDrop = true;
  644. }
  645. function DropOnAddressingWidget(event)
  646. {
  647. var dragSession = gDragService.getCurrentSession();
  648. var trans = Components.classes["@mozilla.org/widget/transferable;1"].createInstance(Components.interfaces.nsITransferable);
  649. trans.addDataFlavor("text/x-moz-address");
  650. for ( var i = 0; i < dragSession.numDropItems; ++i )
  651. {
  652. dragSession.getData ( trans, i );
  653. var dataObj = new Object();
  654. var bestFlavor = new Object();
  655. var len = new Object();
  656. trans.getAnyTransferData ( bestFlavor, dataObj, len );
  657. if ( dataObj )
  658. dataObj = dataObj.value.QueryInterface(Components.interfaces.nsISupportsString);
  659. if ( !dataObj )
  660. continue;
  661. // pull the address out of the data object
  662. var address = dataObj.data.substring(0, len.value);
  663. if (!address)
  664. continue;
  665. DropRecipient(event.target, address);
  666. }
  667. }
  668. function DropRecipient(target, recipient)
  669. {
  670. // break down and add each address
  671. return parseAndAddAddresses(recipient, awGetPopupElement(top.MAX_RECIPIENTS).selectedItem.getAttribute("value"));
  672. }
  673. function _awSetAutoComplete(selectElem, inputElem)
  674. {
  675. inputElem.disableAutoComplete = selectElem.value == 'addr_newsgroups' ||
  676. selectElem.value == 'addr_followup' ||
  677. selectElem.value == 'addr_other';
  678. }
  679. function awSetAutoComplete(rowNumber)
  680. {
  681. var inputElem = awGetInputElement(rowNumber);
  682. var selectElem = awGetPopupElement(rowNumber);
  683. _awSetAutoComplete(selectElem, inputElem)
  684. }
  685. function awRecipientTextCommand(userAction, element)
  686. {
  687. if (userAction == "typing" || userAction == "scrolling")
  688. awReturnHit(element);
  689. }
  690. // Called when an autocomplete session item is selected and the status of
  691. // the session it was selected from is nsIAutoCompleteStatus::failureItems.
  692. //
  693. // As of this writing, the only way that can happen is when an LDAP
  694. // autocomplete session returns an error to be displayed to the user.
  695. //
  696. // There are hardcoded messages in here, but these are just fallbacks for
  697. // when string bundles have already failed us.
  698. //
  699. function awRecipientErrorCommand(errItem, element)
  700. {
  701. // remove the angle brackets from the general error message to construct
  702. // the title for the alert. someday we'll pass this info using a real
  703. // exception object, and then this code can go away.
  704. //
  705. var generalErrString;
  706. if (errItem.value != "")
  707. generalErrString = errItem.value.slice(1, errItem.value.length-1);
  708. else
  709. generalErrString = "Unknown LDAP server problem encountered";
  710. // try and get the string of the specific error to contruct the complete
  711. // err msg, otherwise fall back to something generic. This message is
  712. // handed to us as an nsISupportsString in the param slot of the
  713. // autocomplete error item, by agreement documented in
  714. // nsILDAPAutoCompFormatter.idl
  715. //
  716. var specificErrString = "";
  717. try
  718. {
  719. var specificError = errItem.param.QueryInterface(Components.interfaces.nsISupportsString);
  720. specificErrString = specificError.data;
  721. } catch (ex)
  722. {}
  723. if (specificErrString == "")
  724. specificErrString = "Internal error";
  725. gPromptService.alert(window, generalErrString, specificErrString);
  726. }
  727. function awRecipientKeyPress(event, element)
  728. {
  729. switch(event.keyCode) {
  730. case KeyEvent.DOM_VK_UP:
  731. awArrowHit(element, -1);
  732. break;
  733. case KeyEvent.DOM_VK_DOWN:
  734. awArrowHit(element, 1);
  735. break;
  736. case KeyEvent.DOM_VK_RETURN:
  737. case KeyEvent.DOM_VK_TAB:
  738. // if the user text contains a comma or a line return, ignore
  739. if (element.value.search(',') != -1)
  740. {
  741. var addresses = element.value;
  742. element.value = ""; // clear out the current line so we don't try to autocomplete it..
  743. parseAndAddAddresses(addresses, awGetPopupElement(awGetRowByInputElement(element)).selectedItem.getAttribute("value"));
  744. }
  745. else if (event.keyCode == KeyEvent.DOM_VK_TAB)
  746. awTabFromRecipient(element, event);
  747. break;
  748. }
  749. }
  750. function awArrowHit(inputElement, direction)
  751. {
  752. var row = awGetRowByInputElement(inputElement) + direction;
  753. if (row) {
  754. var nextInput = awGetInputElement(row);
  755. if (nextInput)
  756. awSetFocus(row, nextInput);
  757. else if (inputElement.value)
  758. awAppendNewRow(true);
  759. }
  760. }
  761. function awRecipientKeyDown(event, element)
  762. {
  763. switch(event.keyCode) {
  764. case KeyEvent.DOM_VK_DELETE:
  765. case KeyEvent.DOM_VK_BACK_SPACE:
  766. /* do not query directly the value of the text field else the autocomplete widget could potentially
  767. alter it value while doing some internal cleanup, instead, query the value through the first child
  768. */
  769. if (!element.value)
  770. awDeleteHit(element);
  771. // We need to stop the event else the listbox will receive it and the
  772. // function awKeyDown will be executed!
  773. event.stopPropagation();
  774. break;
  775. }
  776. }
  777. function awKeyDown(event, listboxElement)
  778. {
  779. switch(event.keyCode) {
  780. case KeyEvent.DOM_VK_DELETE:
  781. case KeyEvent.DOM_VK_BACK_SPACE:
  782. /* Warning, the listboxElement.selectedItems will change everytime we delete a row */
  783. var selItems = listboxElement.selectedItems;
  784. var length = listboxElement.selectedItems.length;
  785. for (var i = 1; i <= length; i++) {
  786. var inputs = listboxElement.selectedItems[0].getElementsByTagName(awInputElementName());
  787. if (inputs && inputs.length == 1)
  788. awDeleteHit(inputs[0]);
  789. }
  790. break;
  791. }
  792. }
  793. function awMenulistKeyPress(event, element)
  794. {
  795. switch(event.keyCode) {
  796. case KeyEvent.DOM_VK_TAB:
  797. awTabFromMenulist(element, event);
  798. break;
  799. }
  800. }
  801. /* ::::::::::: addressing widget dummy rows ::::::::::::::::: */
  802. var gAWContentHeight = 0;
  803. var gAWRowHeight = 0;
  804. function awFitDummyRows()
  805. {
  806. awCalcContentHeight();
  807. awCreateOrRemoveDummyRows();
  808. }
  809. function awCreateOrRemoveDummyRows()
  810. {
  811. var listbox = document.getElementById("addressingWidget");
  812. var listboxHeight = listbox.boxObject.height;
  813. // remove rows to remove scrollbar
  814. var kids = listbox.childNodes;
  815. for (var i = kids.length-1; gAWContentHeight > listboxHeight && i >= 0; --i) {
  816. if (kids[i].hasAttribute("_isDummyRow")) {
  817. gAWContentHeight -= gAWRowHeight;
  818. listbox.removeChild(kids[i]);
  819. }
  820. }
  821. // add rows to fill space
  822. if (gAWRowHeight) {
  823. while (gAWContentHeight+gAWRowHeight < listboxHeight) {
  824. awCreateDummyItem(listbox);
  825. gAWContentHeight += gAWRowHeight;
  826. }
  827. }
  828. }
  829. function awCalcContentHeight()
  830. {
  831. var listbox = document.getElementById("addressingWidget");
  832. var items = listbox.getElementsByTagName("listitem");
  833. gAWContentHeight = 0;
  834. if (items.length > 0) {
  835. // all rows are forced to a uniform height in xul listboxes, so
  836. // find the first listitem with a boxObject and use it as precedent
  837. var i = 0;
  838. do {
  839. gAWRowHeight = items[i].boxObject.height;
  840. ++i;
  841. } while (i < items.length && !gAWRowHeight);
  842. gAWContentHeight = gAWRowHeight*items.length;
  843. }
  844. }
  845. function awCreateDummyItem(aParent)
  846. {
  847. var titem = document.createElement("listitem");
  848. titem.setAttribute("_isDummyRow", "true");
  849. titem.setAttribute("class", "dummy-row");
  850. for (var i = awGetNumberOfCols(); i > 0; i--)
  851. awCreateDummyCell(titem);
  852. if (aParent)
  853. aParent.appendChild(titem);
  854. return titem;
  855. }
  856. function awCreateDummyCell(aParent)
  857. {
  858. var cell = document.createElement("listcell");
  859. cell.setAttribute("class", "addressingWidgetCell dummy-row-cell");
  860. if (aParent)
  861. aParent.appendChild(cell);
  862. return cell;
  863. }
  864. function awGetNextDummyRow()
  865. {
  866. // gets the next row from the top down
  867. var listbox = document.getElementById("addressingWidget");
  868. var kids = listbox.childNodes;
  869. for (var i = 0; i < kids.length; ++i) {
  870. if (kids[i].hasAttribute("_isDummyRow"))
  871. return kids[i];
  872. }
  873. return null;
  874. }
  875. function awSizerListen()
  876. {
  877. // when splitter is clicked, fill in necessary dummy rows each time the mouse is moved
  878. awCalcContentHeight(); // precalculate
  879. document.addEventListener("mousemove", awSizerMouseMove, true);
  880. document.addEventListener("mouseup", awSizerMouseUp, false);
  881. }
  882. function awSizerMouseMove()
  883. {
  884. awCreateOrRemoveDummyRows(2);
  885. }
  886. function awSizerMouseUp()
  887. {
  888. document.removeEventListener("mousemove", awSizerMouseMove, true);
  889. document.removeEventListener("mouseup", awSizerMouseUp, false);
  890. }
  891. function awDocumentKeyPress(event)
  892. {
  893. try {
  894. var id = event.target.id;
  895. if (id.substr(0, 11) == 'addressCol1')
  896. awMenulistKeyPress(event, event.target);
  897. } catch (e) { }
  898. }
  899. function awRecipientInputCommand(event, inputElement)
  900. {
  901. gContentChanged=true;
  902. setupAutocomplete();
  903. }
  904. // Given an arbitrary block of text like a comma delimited list of names or a names separated by spaces,
  905. // we will try to autocomplete each of the names and then take the FIRST match for each name, adding it the
  906. // addressing widget on the compose window.
  907. var gAutomatedAutoCompleteListener = null;
  908. function parseAndAddAddresses(addressText, recipientType)
  909. {
  910. // strip any leading >> characters inserted by the autocomplete widget
  911. var strippedAddresses = addressText.replace(/.* >> /, "");
  912. var hdrParser = Components.classes["@mozilla.org/messenger/headerparser;1"].getService(Components.interfaces.nsIMsgHeaderParser);
  913. var addresses = {};
  914. var names = {};
  915. var fullNames = {};
  916. var numAddresses = hdrParser.parseHeadersWithArray(strippedAddresses, addresses, names, fullNames);
  917. if (numAddresses > 0)
  918. {
  919. // we need to set up our own autocomplete session and search for results
  920. setupAutocomplete(); // be safe, make sure we are setup
  921. if (!gAutomatedAutoCompleteListener)
  922. gAutomatedAutoCompleteListener = new AutomatedAutoCompleteHandler();
  923. gAutomatedAutoCompleteListener.init(fullNames.value, numAddresses, recipientType);
  924. }
  925. }
  926. function AutomatedAutoCompleteHandler()
  927. {
  928. }
  929. // state driven self contained object which will autocomplete a block of addresses without any UI.
  930. // force picks the first match and adds it to the addressing widget, then goes on to the next
  931. // name to complete.
  932. AutomatedAutoCompleteHandler.prototype =
  933. {
  934. param: this,
  935. sessionName: null,
  936. namesToComplete: {},
  937. numNamesToComplete: 0,
  938. indexIntoNames: 0,
  939. numSessionsToSearch: 0,
  940. numSessionsSearched: 0,
  941. recipientType: null,
  942. searchResults: null,
  943. init:function(namesToComplete, numNamesToComplete, recipientType)
  944. {
  945. this.indexIntoNames = 0;
  946. this.numNamesToComplete = numNamesToComplete;
  947. this.namesToComplete = namesToComplete;
  948. this.recipientType = recipientType;
  949. // set up the auto complete sessions to use
  950. setupAutocomplete();
  951. this.autoCompleteNextAddress();
  952. },
  953. autoCompleteNextAddress:function()
  954. {
  955. this.numSessionsToSearch = 0;
  956. this.numSessionsSearched = 0;
  957. this.searchResults = new Array;
  958. if (this.indexIntoNames < this.numNamesToComplete && this.namesToComplete[this.indexIntoNames])
  959. {
  960. /* XXX This is used to work, until switching to the new toolkit broke it
  961. We should fix it see bug 456550.
  962. if (this.namesToComplete[this.indexIntoNames].search('@') == -1) // don't autocomplete if address has an @ sign in it
  963. {
  964. // make sure total session count is updated before we kick off ANY actual searches
  965. if (gAutocompleteSession)
  966. this.numSessionsToSearch++;
  967. if (gLDAPSession && gCurrentAutocompleteDirectory)
  968. this.numSessionsToSearch++;
  969. if (gAutocompleteSession)
  970. {
  971. gAutocompleteSession.onAutoComplete(this.namesToComplete[this.indexIntoNames], null, this);
  972. // AB searches are actually synchronous. So by the time we get here we have already looked up results.
  973. // 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
  974. // don't bother with the LDAP search too just return
  975. if (gLDAPSession && gCurrentAutocompleteDirectory && this.searchResults[0] && this.searchResults[0].defaultItemIndex != -1)
  976. {
  977. this.processAllResults();
  978. return;
  979. }
  980. }
  981. if (gLDAPSession && gCurrentAutocompleteDirectory)
  982. gLDAPSession.onStartLookup(this.namesToComplete[this.indexIntoNames], null, this);
  983. }
  984. */
  985. if (!this.numSessionsToSearch)
  986. this.processAllResults(); // ldap and ab are turned off, so leave text alone
  987. }
  988. },
  989. onStatus:function(aStatus)
  990. {
  991. return;
  992. },
  993. onAutoComplete: function(aResults, aStatus)
  994. {
  995. // store the results until all sessions are done and have reported in
  996. if (aResults)
  997. this.searchResults[this.numSessionsSearched] = aResults;
  998. this.numSessionsSearched++; // bump our counter
  999. if (this.numSessionsToSearch <= this.numSessionsSearched)
  1000. setTimeout('gAutomatedAutoCompleteListener.processAllResults()', 0); // we are all done
  1001. },
  1002. processAllResults: function()
  1003. {
  1004. // Take the first result and add it to the compose window
  1005. var addressToAdd;
  1006. // loop through the results looking for the non default case (default case is the address book with only one match, the default domain)
  1007. var sessionIndex;
  1008. var searchResultsForSession;
  1009. for (sessionIndex in this.searchResults)
  1010. {
  1011. searchResultsForSession = this.searchResults[sessionIndex];
  1012. if (searchResultsForSession && searchResultsForSession.defaultItemIndex > -1)
  1013. {
  1014. addressToAdd = searchResultsForSession.items.QueryElementAt(searchResultsForSession.defaultItemIndex, Components.interfaces.nsIAutoCompleteItem).value;
  1015. break;
  1016. }
  1017. }
  1018. // still no match? loop through looking for the -1 default index
  1019. if (!addressToAdd)
  1020. {
  1021. for (sessionIndex in this.searchResults)
  1022. {
  1023. searchResultsForSession = this.searchResults[sessionIndex];
  1024. if (searchResultsForSession && searchResultsForSession.defaultItemIndex == -1)
  1025. {
  1026. addressToAdd = searchResultsForSession.items.QueryElementAt(0, Components.interfaces.nsIAutoCompleteItem).value;
  1027. break;
  1028. }
  1029. }
  1030. }
  1031. // no matches anywhere...just use what we were given
  1032. if (!addressToAdd)
  1033. addressToAdd = this.namesToComplete[this.indexIntoNames];
  1034. // that will automatically set the focus on a new available row, and make sure it is visible
  1035. awAddRecipient(this.recipientType ? this.recipientType : "addr_to", addressToAdd);
  1036. this.indexIntoNames++;
  1037. this.autoCompleteNextAddress();
  1038. },
  1039. QueryInterface : function(iid)
  1040. {
  1041. if (iid.equals(Components.interfaces.nsIAutoCompleteListener) ||
  1042. iid.equals(Components.interfaces.nsISupports))
  1043. return this;
  1044. throw Components.results.NS_NOINTERFACE;
  1045. }
  1046. }