1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. 5 * 6 * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at: https://www.zimbra.com/license 9 * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15 10 * have been added to cover use of software over a computer network and provide for limited attribution 11 * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B. 12 * 13 * Software distributed under the License is distributed on an "AS IS" basis, 14 * WITHOUT WARRANTY OF ANY KIND, either express or implied. 15 * See the License for the specific language governing rights and limitations under the License. 16 * The Original Code is Zimbra Open Source Web Client. 17 * The Initial Developer of the Original Code is Zimbra, Inc. All rights to the Original Code were 18 * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015. 19 * 20 * All portions of the code are Copyright (C) 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved. 21 * ***** END LICENSE BLOCK ***** 22 */ 23 24 /** 25 * Creates an address input field that shows addresses as bubbles. 26 * @constructor 27 * @class 28 * This class creates and manages a control for entering email addresses and displaying 29 * them in bubbles. An address's surrounding bubble can be used to remove it, or, if the 30 * address is a distribution list, expand it. They can be dragged to other address fields, 31 * or reordered within this one. They can be selected via a "rubber band" selection box. 32 * The bubbles support a few shortcuts. 33 * 34 * It is not a DwtInputField. If you don't want bubbles, use that (or a native INPUT) instead. 35 * 36 * @author Conrad Damon 37 * 38 * @param {hash} params hash of params: 39 * @param {ZmAutocompleteListView} autocompleteListView associated autocomplete control 40 * @param {string} inputId an explicit ID to use for the control's INPUT element 41 * @param {string} templateId custom template to use 42 * @param {string} type arbitrary type to uniquely identify this among others 43 * @param {boolean} strictMode if true (default), bubbles must contain valid addresses 44 * @param {AjxCallback|function} bubbleAddedCallback called when a bubble is added 45 * @param {AjxCallback|function} bubbleRemovedCallback called when a bubble is removed 46 * @param {AjxCallback|function} bubbleMenuCreatedCallback called when the action menu has been created 47 * @param {AjxCallback|function} bubbleMenuResetOperationsCallback called when the action menu has reset its operations 48 * @param {boolean} noOutsideListening don't worry about outside mouse clicks 49 */ 50 ZmAddressInputField = function(params) { 51 52 params.parent = params.parent || appCtxt.getShell(); 53 params.className = params.className || "addrBubbleContainer"; 54 DwtComposite.call(this, params); 55 56 this._initialize(params); 57 58 if (params.autocompleteListView) { 59 this.setAutocompleteListView(params.autocompleteListView); 60 } 61 62 this.type = params.type; 63 this._strictMode = (params.strictMode !== false); 64 this._noOutsideListening = params.noOutsideListening; 65 this._singleBubble = params.singleBubble; 66 67 this._bubbleAddedCallback = params.bubbleAddedCallback; 68 this._bubbleRemovedCallback = params.bubbleRemovedCallback; 69 this._bubbleMenuCreatedCallback = params.bubbleMenuCreatedCallback; 70 this._bubbleResetOperationsCallback = params.bubbleMenuResetOperationsCallback; 71 72 this._bubbleClassName = "addrBubble"; 73 74 this._bubbleList = new ZmAddressBubbleList({parent:this, separator:this._separator}); 75 this._bubbleList.addSelectionListener(this._selectionListener.bind(this)); 76 this._bubbleList.addActionListener(this._actionListener.bind(this)); 77 78 this._listeners = {}; 79 this._listeners[ZmOperation.DELETE] = this._deleteListener.bind(this); 80 this._listeners[ZmOperation.EDIT] = this._editListener.bind(this); 81 this._listeners[ZmOperation.EXPAND] = this._expandListener.bind(this); 82 this._listeners[ZmOperation.CONTACT] = this._contactListener.bind(this); 83 84 // drag-and-drop of bubbles 85 var dropTgt = new DwtDropTarget("ZmAddressBubble"); 86 dropTgt.markAsMultiple(); 87 dropTgt.addDropListener(this._dropListener.bind(this)); 88 this.setDropTarget(dropTgt); 89 90 // rubber-band selection of bubbles 91 this._setEventHdlrs([DwtEvent.ONMOUSEDOWN, DwtEvent.ONMOUSEMOVE, DwtEvent.ONMOUSEUP]); 92 var dragBox = new DwtDragBox(); 93 dragBox.addDragListener(this._dragBoxListener.bind(this)); 94 this.setDragBox(dragBox); 95 96 // Let this be a single tab stop, then manage focus among bubbles (if any) and the input using arrow keys 97 this.tabGroupMember = this; 98 99 this.addListener(DwtEvent.ONMOUSEDOWN, this._mouseDownListener); 100 this._reset(); 101 }; 102 103 ZmAddressInputField.prototype = new DwtComposite; 104 ZmAddressInputField.prototype.constructor = ZmAddressInputField; 105 106 ZmAddressInputField.prototype.isZmAddressInputField = true; 107 ZmAddressInputField.prototype.isInputControl = true; 108 //ZmAddressInputField.prototype.role = 'combobox'; 109 ZmAddressInputField.prototype.toString = function() { return "ZmAddressInputField"; }; 110 111 ZmAddressInputField.prototype.TEMPLATE = "share.Widgets#ZmAddressInputField"; 112 113 ZmAddressInputField.INPUT_EXTRA = 30; // extra width for the INPUT 114 ZmAddressInputField.INPUT_EXTRA_SMALL = 10; // edit mode 115 116 // tie a bubble SPAN to a widget that can handle clicks 117 ZmAddressInputField.BUBBLE_OBJ_ID = {}; 118 119 // several ZmAddressInputField's can share an action menu, so save context statically 120 ZmAddressInputField.menuContext = {}; 121 122 ZmAddressInputField.prototype.setAutocompleteListView = 123 function(aclv) { 124 this._aclv = aclv; 125 this._separator = (aclv._separator) || AjxEmailAddress.SEPARATOR; 126 aclv.addCallback(ZmAutocompleteListView.CB_KEYDOWN, this._keyDownCallback.bind(this), this._inputId); 127 aclv.addCallback(ZmAutocompleteListView.CB_KEYUP, this._keyUpCallback.bind(this), this._inputId); 128 }; 129 130 // Override since we normally want to add bubble before the INPUT, and not at the end. If we're 131 // leaving edit mode, we want to put the bubble back where it was via the index. 132 ZmAddressInputField.prototype.addChild = 133 function(child, index) { 134 135 DwtComposite.prototype.addChild.apply(this, arguments); 136 137 var el = child.getHtmlElement(); 138 if (this._input.parentNode == this._holder) { 139 var refElement; 140 if (index != null) { 141 var refBubble = this._getBubbleList().getBubble(index); 142 refElement = refBubble && refBubble.getHtmlElement(); 143 } 144 this._holder.insertBefore(el, refElement || this._input); 145 } else { 146 this._holder.appendChild(el); 147 } 148 }; 149 150 /** 151 * Creates a bubble for the given address and adds it into the holding area. If the address 152 * is a local group, it is expanded and the members are added individually. 153 * 154 * @param {hash} params hash of params: 155 * @param {string} address address text to go in the bubble 156 * @param {ZmAutocompleteMatch} match match object 157 * @param {ZmAddressBubble} bubble bubble to clone 158 * @param {int} index position (relative to bubbles, not elements) at which to add bubble 159 * @param {boolean} skipNotify if true, don't call bubbleAddedCallback 160 * @param {boolean} noFocus if true, don't focus input after bubble is added 161 * @param {string} addClass additional class name for bubble 162 * @param {boolean} noParse if true, do not parse content to see if it is an address 163 */ 164 ZmAddressInputField.prototype.addBubble = 165 function(params) { 166 167 params = params || {}; 168 if (!params.address && !params.bubble) { return; } 169 170 if (params.bubble) { 171 params.address = params.bubble.address; 172 params.match = params.bubble.match; 173 params.canExpand = params.bubble.canExpand; 174 } 175 params.parent = this; 176 params.addrInput = this; 177 params.parentId = this._htmlElId; 178 params.className = this._bubbleClassName ; 179 params.canRemove = true; 180 params.separator = this._separator; 181 params.type = this.type; 182 183 if (params.index == null && this._editModeIndex != null) { 184 params.index = this._getInsertionIndex(this._holder.childNodes[this._editModeIndex]); 185 } 186 187 var bubble, bubbleAdded = false; 188 189 // if it's a local group, expand it and add each address separately 190 var match = params.match; 191 if (match && match.isGroup && match.type == ZmAutocomplete.AC_TYPE_CONTACT) { 192 var addrs = AjxEmailAddress.split(params.address); 193 for (var i = 0, len = addrs.length; i < len; i++) { 194 params.id = params.addrObj = params.match = params.email = params.canExpand = null; 195 params.address = addrs[i]; 196 params.index = (params.index != null) ? params.index + i : null; 197 if (this._hasValidAddress(params)) { 198 this._addBubble(new ZmAddressBubble(params), params.index); 199 bubbleAdded = true; 200 } 201 } 202 } 203 else { 204 if (this._hasValidAddress(params)) { 205 bubble = new ZmAddressBubble(params); 206 this._addBubble(bubble, params.index, params.noFocus); 207 bubbleAdded = true; 208 } 209 else { 210 // if handed a non-address while in strict mode, append it to the INPUT and bail 211 var value = this._input.value; 212 var sep = value ? this._separator : ""; 213 this._setInputValue([value, sep, params.address].join("")); 214 } 215 } 216 217 if (bubbleAdded) { 218 this._holder.className = "addrBubbleHolder"; 219 if (this._bubbleAddedCallback && !params.skipNotify) { 220 this._bubbleAddedCallback.run(bubble, true); 221 } 222 this._leaveEditMode(); 223 return bubble; 224 } 225 }; 226 227 ZmAddressInputField.prototype._addBubble = 228 function(bubble, index, noFocus) { 229 230 if (!bubble || (this._singleBubble && this._numBubbles > 0)) { 231 return; 232 } 233 234 DBG.println("aif1", "ADD bubble: " + AjxStringUtil.htmlEncode(bubble.address)); 235 bubble.setDropTarget(this.getDropTarget()); 236 this._bubbleList.add(bubble, index); 237 this._numBubbles++; 238 239 var bubbleId = bubble._htmlElId; 240 this._bubble[bubbleId] = bubble; 241 this._addressHash[bubble.hashKey] = bubbleId; 242 243 if (!noFocus) { 244 this.focus(); 245 } 246 247 if (this._singleBubble) { 248 this._setInputEnabled(false); 249 } 250 }; 251 252 ZmAddressInputField.prototype.getAddressBubble = 253 function(email) { 254 return this._addressHash[email]; 255 }; 256 257 ZmAddressInputField.prototype._hasValidAddress = 258 function(params) { 259 if (!this._strictMode) { 260 return true; 261 } 262 var addr = (params.addrObj && params.addrObj.getAddress()) || params.address || (params.match && params.match.email); 263 return (Boolean(AjxEmailAddress.parse(addr))); 264 }; 265 266 /** 267 * Removes the bubble with the given ID from the holding area. 268 * 269 * @param {string} bubbleId ID of bubble to remove 270 * @param {boolean} skipNotify if true, don't call bubbleRemovedCallback 271 */ 272 ZmAddressInputField.prototype.removeBubble = 273 function(bubbleId, skipNotify) { 274 275 var bubble = DwtControl.fromElementId(bubbleId); 276 if (!bubble) { return; } 277 278 this._bubbleList.remove(bubble); 279 280 bubble.dispose(); 281 282 this._bubble[bubbleId] = null; 283 delete this._bubble[bubbleId]; 284 delete this._addressHash[bubble.hashKey]; 285 this._numBubbles--; 286 287 if (this._numBubbles == 0) { 288 this._holder.className = "addrBubbleHolder-empty"; 289 } 290 291 this._resizeInput(); 292 293 if (this._bubbleRemovedCallback && !skipNotify) { 294 this._bubbleRemovedCallback.run(bubble, false); 295 } 296 297 if (this._singleBubble && this._numBubbles === 0) { 298 this._setInputEnabled(true); 299 } 300 }; 301 302 /** 303 * Removes all bubbles from the holding area. 304 */ 305 ZmAddressInputField.prototype.clear = 306 function(skipNotify) { 307 for (var id in this._bubble) { 308 this.removeBubble(id, skipNotify); 309 } 310 this._reset(); 311 }; 312 313 /** 314 * Returns a string of concatenated bubble addresses. 315 */ 316 ZmAddressInputField.prototype.getValue = 317 function() { 318 var list = this.getAddresses(); 319 if (this._input.value) { 320 list.push(this._input.value); 321 } 322 return list.join(this._separator); 323 }; 324 325 /** 326 * Parses the given text into email addresses, and adds a bubble for each one 327 * that we don't already have. Since text is passed in, we don't recognize expandable DLs. 328 * A bubble may be added for a string even if it doesn't parse as an email address. 329 * 330 * @param {string} text email addresses 331 * @param {boolean} add if true, control is not cleared first 332 * @param {boolean} skipNotify if true, don't call bubbleAddedCallback 333 * @param {boolean} invokeAutocomplete if true, trigger autocomplete 334 * (useful in paste event when keyup/down events don't take place 335 */ 336 ZmAddressInputField.prototype.setValue = 337 function(text, add, skipNotify, invokeAutocomplete) { 338 339 if (!add) { 340 this.clear(); 341 } 342 if (!text) { 343 return; 344 } 345 346 var index = null; 347 if (this._editModeIndex != null) { 348 index = this._getInsertionIndex(this._holder.childNodes[this._editModeIndex]); 349 } 350 351 var addrs = AjxEmailAddress.parseEmailString(text); 352 var good, bad; 353 if (this.type === ZmId.SEARCH) { 354 // search field query isn't supposed to be validated for emails 355 good = addrs.all.getArray(); 356 bad = []; 357 // skip notify because we don't need to trigger search on text to bubble conversion 358 skipNotify = true; 359 } 360 else { 361 good = addrs.good.getArray(); 362 bad = addrs.bad.getArray(); 363 } 364 365 for (var i = 0; i < good.length; i++) { 366 var addr = good[i]; 367 if ((addr && !this._addressHash[addr.address])) { 368 this.addBubble({ 369 address: addr.toString(), 370 addrObj: addr, 371 index: (index != null) ? index + i : null, 372 skipNotify: skipNotify 373 }); 374 } 375 } 376 377 this._setInputValue(bad.length ? bad.join(this._separator) : ""); 378 if (invokeAutocomplete && bad.length) { 379 this._aclv.autocomplete(this.getInputElement()); 380 } 381 }; 382 383 /** 384 * Sets the value of the input without looking for email addresses. No bubbles will be added. 385 * 386 * @param {string} text new input content 387 */ 388 ZmAddressInputField.prototype.setInputValue = 389 function(text) { 390 this._input.value = text; 391 this._resizeInput(); 392 }; 393 394 /** 395 * Adds address(es) to the input. 396 * 397 * @param {string} text email addresses 398 * @param {boolean} skipNotify if true, don't call bubbleAddedCallback 399 */ 400 ZmAddressInputField.prototype.addValue = 401 function(text, skipNotify) { 402 this.setValue(text, true, skipNotify); 403 }; 404 405 /** 406 * Removes the selected bubble. If none are selected, selects the last one. 407 * 408 * @param {boolean} checkInput if true, make sure INPUT is empty 409 * 410 * @return {boolean} true if the delete selected or removed a bubble 411 */ 412 ZmAddressInputField.prototype.handleDelete = 413 function(checkInput) { 414 415 if (checkInput && this._input.value.length > 0) { 416 return false; 417 } 418 419 var sel = this.getSelection(); 420 if (sel.length) { 421 for (var i = 0, len = sel.length; i < len; i++) { 422 if (sel[i]) { 423 this.removeBubble(sel[i].id); 424 } 425 } 426 this.focus(); 427 return true; 428 } 429 else { 430 return this._selectBubbleBeforeInput(); 431 } 432 }; 433 434 // Selects the bubble to the left of the (empty) INPUT, if there is one. 435 ZmAddressInputField.prototype._selectBubbleBeforeInput = 436 function() { 437 438 if (!this._input.value) { 439 var index = this._getInputIndex(); 440 var span = (index > 0) && this._holder.childNodes[index - 1]; 441 var bubble = DwtControl.fromElement(span); 442 if (bubble) { 443 this.setSelected(bubble, true); 444 this.blur(); 445 appCtxt.getKeyboardMgr().grabFocus(bubble); 446 return true; 447 } 448 } 449 return false; 450 }; 451 452 /** 453 * Sets selection of the given bubble. 454 * 455 * @param {Element} bubble bubble to select 456 * @param {boolean} selected if true, select the bubble, otherwise deselect it 457 */ 458 ZmAddressInputField.prototype.setSelected = 459 function(bubble, selected) { 460 this._bubbleList.setSelected(bubble, selected); 461 }; 462 463 /** 464 * Returns a list of the currently selected bubbles. If a bubble has been selected via right-click, 465 * but is not part of the current left-click selection, only it will be returned. 466 * 467 * @param {ZmAddressBubble} bubble reference bubble 468 */ 469 ZmAddressInputField.prototype.getSelection = 470 function(bubble) { 471 return this._bubbleList.getSelection(bubble); 472 }; 473 474 ZmAddressInputField.prototype.getSelectionCount = 475 function(bubble) { 476 return this._bubbleList.getSelectionCount(bubble); 477 }; 478 479 ZmAddressInputField.prototype.deselectAll = 480 function() { 481 this._bubbleList.deselectAll(); 482 }; 483 484 ZmAddressInputField.prototype.preventSelection = 485 function(targetEl) { 486 return !(this._bubble[targetEl.id] || this.__isInputEl(targetEl)); 487 }; 488 489 /** 490 * Makes bubbles out of addresses in pasted text. 491 * 492 * @param ev 493 */ 494 ZmAddressInputField.onPaste = 495 function(ev) { 496 var addrInput = ZmAddressInputField._getAddrInputFromEvent(ev); 497 if (addrInput) { 498 // trigger autocomplete after paste to accommodate mouse click pastes 499 var invokeAutocomplete = true; 500 // give browser time to update input - easier than dealing with clipboard 501 // will also resize the INPUT 502 AjxTimedAction.scheduleAction( 503 new AjxTimedAction( 504 addrInput, 505 addrInput._checkInput, 506 [null, invokeAutocomplete] 507 ), 100 508 ); 509 } 510 }; 511 512 ZmAddressInputField.onCut = 513 function(ev) { 514 var addrInput = ZmAddressInputField._getAddrInputFromEvent(ev); 515 if (addrInput) { 516 addrInput._resizeInput(); 517 } 518 }; 519 520 /** 521 * Handle arrow up, arrow down for bubble holder 522 * 523 * @param ev 524 */ 525 ZmAddressInputField.onHolderKeyClick = 526 function(ev) { 527 ev = DwtUiEvent.getEvent(ev); 528 var key = DwtKeyEvent.getCharCode(ev); 529 if (key === DwtKeyEvent.KEY_ARROW_UP) { 530 if (this.clientHeight >= this.scrollHeight) { return; } 531 this.scrollTop = Math.max(this.scrollTop - this.clientHeight, 0); 532 DBG.println("aif", "this.scrollTop = " + this.scrollTop); 533 } 534 else if (key === DwtKeyEvent.KEY_ARROW_DOWN) { 535 if (this.clientHeight >= this.scrollHeight) { return; } 536 this.scrollTop = Math.min(this.scrollTop + this.clientHeight, this.scrollHeight - this.clientHeight); 537 DBG.println("aif", "this.scrollTop = " + this.scrollTop); 538 } 539 }; 540 541 // looks for valid addresses in the input, and converts them to bubbles 542 ZmAddressInputField.prototype._checkInput = 543 function(text, invokeAutocomplete) { 544 text = text || this._input.value; 545 DBG.println("aif", "CHECK input: " + AjxStringUtil.htmlEncode(text)); 546 if (text) { 547 this.setValue(text, true, false, invokeAutocomplete); 548 } 549 }; 550 551 // focus input when holder div is clicked 552 ZmAddressInputField.onHolderClick = 553 function(ev) { 554 DBG.println("aif", "ZmAddressInputField.onHolderClick"); 555 var addrInput = ZmAddressInputField._getAddrInputFromEvent(ev); 556 if (addrInput) { 557 addrInput.focus(); 558 559 // bug 85036: ensure caret visibility on IE by resetting the selection 560 var input = addrInput.getInputElement(); 561 Dwt.setSelectionRange(input, Dwt.getSelectionStart(input), 562 Dwt.getSelectionEnd(input)); 563 } 564 }; 565 566 /** 567 * Removes the bubble with the given ID from the holding area. 568 * 569 * @param {string} bubbleId ID of bubble to remove 570 * @param {boolean} skipNotify if true, don't call bubbleRemovedCallback 571 * 572 */ 573 ZmAddressInputField.removeBubble = 574 function(bubbleId, skipNotify) { 575 576 var bubble = document.getElementById(bubbleId); 577 DBG.println("aif", "REMOVE bubble: " + AjxStringUtil.htmlEncode(bubble.address)); 578 var parentId = bubble._aifId || ZmAddressInputField.BUBBLE_OBJ_ID[bubbleId]; 579 var addrInput = bubble && DwtControl.ALL_BY_ID[parentId]; 580 if (addrInput && addrInput.getEnabled()) { 581 addrInput.removeBubble(bubbleId, skipNotify); 582 addrInput.focus(); 583 } 584 }; 585 586 ZmAddressInputField.prototype.getInputElement = 587 function() { 588 return this._input; 589 }; 590 591 ZmAddressInputField.prototype._focus = function() { 592 this.setDisplayState(DwtControl.FOCUSED); 593 }; 594 595 ZmAddressInputField.prototype._blur = function() { 596 this.setDisplayState(DwtControl.NORMAL); 597 }; 598 599 ZmAddressInputField.prototype.setEnabled = 600 function(enabled) { 601 DwtControl.prototype.setEnabled.call(this, enabled); 602 this._input.disabled = !enabled; 603 }; 604 605 /** 606 * Enables or disables the input without affecting the bubbles. 607 * 608 * @param {boolean} enabled enable input if true, disable if false 609 */ 610 ZmAddressInputField.prototype._setInputEnabled = 611 function(enabled) { 612 this._input.disabled = !enabled; 613 }; 614 615 ZmAddressInputField.prototype._initialize = 616 function(params) { 617 618 this._holderId = Dwt.getNextId(); 619 this._inputId = params.inputId || Dwt.getNextId(); 620 this._label = params.label; 621 this._dragInsertionBarId = Dwt.getNextId(); 622 var data = { 623 inputTagName: AjxEnv.isIE || AjxEnv.isModernIE ? 'textarea' : 'input type="text" ', 624 holderId: this._holderId, 625 inputId: this._inputId, 626 label: this._label, 627 dragInsertionBarId: this._dragInsertionBarId 628 }; 629 this._createHtmlFromTemplate(params.templateId || this.TEMPLATE, data); 630 631 this._holder = document.getElementById(this._holderId); 632 this._holder._aifId = this._htmlElId; 633 this._input = document.getElementById(this._inputId); 634 this._input.supportsAutoComplete = true; 635 this._dragInsertionBar = document.getElementById(this._dragInsertionBarId); 636 637 Dwt.setHandler(this._holder, DwtEvent.ONCLICK, ZmAddressInputField.onHolderClick); 638 Dwt.setHandler(this._input, DwtEvent.ONCUT, ZmAddressInputField.onCut); 639 Dwt.setHandler(this._input, DwtEvent.ONPASTE, ZmAddressInputField.onPaste); 640 Dwt.setHandler(this._holder, DwtEvent.ONKEYDOWN, ZmAddressInputField.onHolderKeyClick); 641 642 this.setFocusElement(); // now that INPUT has been created 643 644 var args = {container:this._holder, threshold:10, amount:15, interval:5, id:this._holderId}; 645 this._dndScrollCallback = DwtControl._dndScrollCallback.bind(null, [args]); 646 this._dndScrollId = this._holderId; 647 }; 648 649 ZmAddressInputField.prototype._reset = 650 function() { 651 652 this._bubble = {}; // bubbles by bubble ID 653 this._addressHash = {}; // used addresses, so we can check for dupes 654 655 this._numBubbles = 0; 656 657 this._bubbleList.reset(); 658 659 this._editMode = false; 660 this._editModeIndex = this._editModeBubble = null; 661 662 this._dragInsertionBarIndex = null; // node index vertical bar indicating insertion point 663 664 this._holder.className = "addrBubbleHolder-empty"; 665 this._setInputValue(""); 666 }; 667 668 ZmAddressInputField.prototype.moveCursorToEnd = 669 function() { 670 Dwt.moveCursorToEnd(this._input); 671 }; 672 673 ZmAddressInputField.prototype._setInputValue = 674 function(value) { 675 DBG.println("aif", "SET input value to: " + AjxStringUtil.htmlEncode(value)); 676 this._input.value = value && value.replace(/\s+/g, ' '); 677 this._resizeInput(); 678 }; 679 680 // Handles key events that occur in the INPUT. 681 ZmAddressInputField.prototype._keyDownCallback = 682 function(ev, aclv) { 683 ev = DwtUiEvent.getEvent(ev); 684 var key = DwtKeyEvent.getCharCode(ev); 685 var propagate; 686 var clearInput = false; 687 688 if (DwtKeyMapMgr.hasModifier(ev) || ev.shiftKey) { 689 return propagate; 690 } 691 692 // Esc in edit mode restores the original address to the bubble 693 if (key === DwtKeyEvent.KEY_ESCAPE && this._editMode) { 694 DBG.println("aif", "_keyDownCallback found ESC key in edit mode"); 695 this._leaveEditMode(true); 696 propagate = false; // eat the event - eg don't let compose view catch Esc and pop the view 697 clearInput = true; 698 } 699 // Del removes selected bubbles, or selects last bubble if there is no input 700 else if (key === DwtKeyEvent.KEY_BACKSPACE) { 701 DBG.println("aif", "_keyDownCallback found DEL key"); 702 if (this.handleDelete(true)) { 703 propagate = false; 704 } 705 } 706 // Left arrow selects last bubble if there is no input 707 else if (key === DwtKeyEvent.KEY_ARROW_LEFT) { 708 DBG.println("aif", "_keyDownCallback found left arrow"); 709 if (this._selectBubbleBeforeInput()) { 710 propagate = false; 711 } 712 } 713 // Handle case where user is leaving edit while we're not in strict mode 714 // (in strict mode, aclv will call addrFoundCallback if it gets a Return) 715 else if (!this._strictMode && DwtKeyEvent.IS_RETURN[key]) { 716 DBG.println("aif", "_keyDownCallback found RETURN"); 717 var bubble = this._editMode && this._editModeBubble; 718 if (bubble && !bubble.addrObj) { 719 this._leaveEditMode(); 720 propagate = false; 721 clearInput = true; 722 } 723 } 724 725 if (clearInput && AjxEnv.isGeckoBased) { 726 AjxTimedAction.scheduleAction(new AjxTimedAction(this, this._setInputValue, [""]), 20); 727 } 728 729 return propagate; 730 }; 731 732 // need to do this on keyup, after character has appeared in the INPUT 733 ZmAddressInputField.prototype._keyUpCallback = 734 function(ev, aclv) { 735 if (!this._input.value && this._editMode) { 736 if (this._bubbleRemovedCallback) { 737 this._bubbleRemovedCallback.run(this._editModeBubble, false); 738 } 739 this._leaveEditMode(); 740 } 741 this._resizeInput(); 742 }; 743 744 ZmAddressInputField.prototype._selectionListener = 745 function(ev) { 746 747 var bubble = ev.item; 748 if (ev.detail == DwtEvent.ONDBLCLICK) { 749 // Double-clicking a bubble moves it into edit mode. It is replaced by the 750 // INPUT, which is moved to the bubble's position. The bubble's address fills 751 // the input and is selected. 752 this.setSelected(bubble, false); 753 this._checkInput(); 754 this._enterEditMode(bubble); 755 } 756 else { 757 this._resetOperations(); 758 } 759 }; 760 761 ZmAddressInputField.prototype._actionListener = 762 function(ev) { 763 764 var bubble = ev.item; 765 var menu = this.getActionMenu(); 766 ZmAddressInputField.menuContext.addrInput = this; 767 ZmAddressInputField.menuContext.event = ev; 768 ZmAddressInputField.menuContext.bubble = bubble; 769 770 DBG.println("aif", "right sel bubble: " + bubble.id); 771 this._resetOperations(); 772 773 var email = bubble.email; 774 var contactsApp = appCtxt.getApp(ZmApp.CONTACTS); 775 if (email && contactsApp) { 776 // first check if contact is cached, and no server call is needed 777 var contact = contactsApp.getContactByEmail(email); 778 if (contact) { 779 this._handleResponseGetContact(ev, contact); 780 } else { 781 menu.getOp(ZmOperation.CONTACT).setText(ZmMsg.loading); 782 var respCallback = this._handleResponseGetContact.bind(this, ev); 783 contactsApp.getContactByEmail(email, respCallback); 784 } 785 } 786 else { 787 var actionMenu = this.getActionMenu(); 788 actionMenu.getOp(ZmOperation.CONTACT).setVisible(false); 789 actionMenu.getOp(ZmOperation.EXPAND).setVisible(false); 790 791 this._setContactText(null); 792 menu.popup(0, ev.docX || bubble.getXW(), ev.docY || bubble.getYH()); 793 } 794 795 // if we are listening for outside mouse clicks, add the action menu to the elements 796 // defined as "inside" so that clicking a menu item doesn't call our outside listener 797 // and deselectAll before the menu listener does its thing 798 if (!this._noOutsideListening && (this.getSelectionCount() > 0)) { 799 var omem = appCtxt.getOutsideMouseEventMgr(); 800 var omemParams = { 801 id: "ZmAddressBubbleList", 802 obj: menu, 803 outsideListener: this.getOutsideListener() 804 } 805 DBG.println("aif", "ADD menu to outside listening " + this._input.id); 806 omem.startListening(omemParams); 807 } 808 }; 809 810 ZmAddressInputField.prototype.getOutsideListener = 811 function() { 812 return this._bubbleList ? this._bubbleList._outsideMouseListener.bind(this._bubbleList) : null; 813 }; 814 815 ZmAddressInputField.prototype.getActionMenu = 816 function() { 817 var menu = this._actionMenu || this.parent._bubbleActionMenu; 818 if (!menu) { 819 menu = this._actionMenu = this.parent._bubbleActionMenu = this._createActionMenu(); 820 } 821 return menu; 822 }; 823 824 ZmAddressInputField.prototype._createActionMenu = 825 function() { 826 827 DBG.println("aif", "create action menu for " + this._input.id); 828 var menuItems = this._getActionMenuOps(); 829 var menu = new ZmActionMenu({parent:this.shell, menuItems:menuItems}); 830 for (var i = 0; i < menuItems.length; i++) { 831 var menuItem = menuItems[i]; 832 if (this._listeners[menuItem]) { 833 menu.addSelectionListener(menuItem, this._listeners[menuItem]); 834 } 835 } 836 837 var copyMenuItem = menu.getOp(ZmOperation.COPY); 838 if (copyMenuItem) { 839 appCtxt.getClipboard().init(copyMenuItem, { 840 onMouseDown: this._clipCopy.bind(this), 841 onComplete: this._clipCopyComplete.bind(this) 842 }); 843 } 844 845 menu.addPopdownListener(this._menuPopdownListener.bind(this)); 846 847 if (this._bubbleMenuCreatedCallback) { 848 this._bubbleMenuCreatedCallback.run(this, menu); 849 } 850 851 return menu; 852 }; 853 854 ZmAddressInputField.prototype._resetOperations = 855 function() { 856 857 var menu = this.getActionMenu(); 858 if (menu) { 859 var sel = this.getSelection(); 860 var bubble = (sel.length == 1) ? sel[0] : null; 861 menu.enable(ZmOperation.DELETE, sel.length > 0); 862 menu.enable(ZmOperation.COPY, sel.length > 0); 863 menu.enable(ZmOperation.EDIT, Boolean(bubble)); 864 var email = bubble && bubble.email; 865 var ac = window.parentAppCtxt || window.appCtxt; 866 var isExpandableDl = ac.isExpandableDL(email); 867 menu.enable(ZmOperation.EXPAND, isExpandableDl); 868 //not sure this is %100 good, since isExpandableDL returns false also if EXPAND_DL_ENABLED setting is false. 869 //but I tried to do this in _setContactText by passing in the contact we get (using getContactByEmail) - but that contact somehow doesn't 870 //have isGal set or type "group" (the type is "contact"), thus isDistributionList returns null. Not sure what this inconsistency comes from. 871 872 //so this is messy and I just try to do the best with information - see the comment above - so I use isExpandableDl as indication of DL (sometimes it's false despite it being an expandable DL) 873 //and I also use isDL as another way to try to know if it's a DL (by trying to find the contact from the contactsApp cache - sometimes it's there, sometimes not (it's there 874 //after you go to the DL folder). 875 var contactsApp = appCtxt.getApp(ZmApp.CONTACTS); 876 var contact = contactsApp && contactsApp.getContactByEmail(email); 877 var isDL = contact && contact.isDistributionList(); 878 var canEdit = !(isDL || isExpandableDl) || (contact && contact.dlInfo && contact.dlInfo.isOwner); 879 menu.enable(ZmOperation.CONTACT, canEdit); 880 881 } 882 883 if (this._bubbleResetOperationsCallback) { 884 this._bubbleResetOperationsCallback.run(this, menu); 885 } 886 }; 887 888 ZmAddressInputField.prototype._getActionMenuOps = 889 function() { 890 891 var ops = [ZmOperation.DELETE]; 892 if (AjxClipboard.isSupported()) { 893 ops.push(ZmOperation.COPY); 894 }; 895 ops.push(ZmOperation.EDIT); 896 ops.push(ZmOperation.EXPAND); 897 ops.push(ZmOperation.CONTACT); 898 899 return ops; 900 }; 901 902 ZmAddressInputField.prototype._handleResponseGetContact = function(ev, contact) { 903 904 ZmAddressInputField.menuContext.contact = contact; 905 this._setContactText(contact); 906 var x = ev.docX > 0 ? ev.docX : ev.item.getXW(), 907 y = ev.docY > 0 ? ev.docY : ev.item.getYH(); 908 909 this.getActionMenu().popup(0, x, y); 910 }; 911 912 ZmAddressInputField.prototype._setContactText = 913 function(contact) { 914 ZmBaseController.setContactTextOnMenu(contact, this.getActionMenu()); 915 }; 916 917 ZmAddressInputField.prototype._deleteListener = 918 function() { 919 var addrInput = ZmAddressInputField.menuContext.addrInput; 920 var sel = addrInput && addrInput.getSelection(); 921 if (sel && sel.length) { 922 for (var i = 0; i < sel.length; i++) { 923 addrInput.removeBubble(sel[i].id); 924 } 925 } 926 }; 927 928 ZmAddressInputField.prototype._editListener = 929 function() { 930 var addrInput = ZmAddressInputField.menuContext.addrInput; 931 var bubble = ZmAddressInputField.menuContext.bubble; 932 if (addrInput && bubble) { 933 addrInput._enterEditMode(bubble); 934 } 935 }; 936 937 ZmAddressInputField.prototype._expandListener = 938 function() { 939 var addrInput = ZmAddressInputField.menuContext.addrInput; 940 var bubble = ZmAddressInputField.menuContext.bubble; 941 if (addrInput && bubble) { 942 ZmAddressBubble.expandBubble(bubble.id, bubble.email); 943 } 944 }; 945 946 /** 947 * If there's a contact for the participant, edit it, otherwise add it. 948 * 949 * @private 950 */ 951 ZmAddressInputField.prototype._contactListener = 952 function(ev) { 953 var addrInput = ZmAddressInputField.menuContext.addrInput; 954 if (addrInput) { 955 var loadCallback = addrInput._handleLoadContactListener.bind(addrInput); 956 AjxDispatcher.require(["ContactsCore", "Contacts"], false, loadCallback, null, true); 957 } 958 }; 959 960 /** 961 * @private 962 */ 963 ZmAddressInputField.prototype._handleLoadContactListener = 964 function() { 965 966 var ctlr = window.parentAppCtxt ? window.parentAppCtxt.getApp(ZmApp.CONTACTS).getContactController() : 967 AjxDispatcher.run("GetContactController"); 968 var contact = ZmAddressInputField.menuContext.contact; 969 if (contact) { 970 if (contact.isLoaded) { 971 ctlr.show(contact); 972 } else { 973 var callback = this._loadContactCallback.bind(this); 974 contact.load(callback); 975 } 976 } else { 977 var contact = new ZmContact(null); 978 var bubble = ZmAddressInputField.menuContext.bubble; 979 var email = bubble && bubble.email; 980 if (email) { 981 contact.initFromEmail(email); 982 ctlr.show(contact, true); 983 } 984 } 985 }; 986 987 ZmAddressInputField.prototype._loadContactCallback = 988 function(resp, contact) { 989 var ctlr = window.parentAppCtxt ? window.parentAppCtxt.getApp(ZmApp.CONTACTS).getContactController() : 990 AjxDispatcher.run("GetContactController"); 991 ctlr.show(contact); 992 }; 993 994 // Copies address text from the active bubble to the clipboard. 995 ZmAddressInputField.prototype._clipCopy = 996 function(clip) { 997 clip.setText(ZmAddressInputField.menuContext.bubble.address + this._separator); 998 }; 999 1000 ZmAddressInputField.prototype._clipCopyComplete = 1001 function(clip) { 1002 this._actionMenu.popdown(); 1003 }; 1004 1005 ZmAddressInputField.prototype._menuPopdownListener = 1006 function() { 1007 1008 var bubble = ZmAddressInputField.menuContext.bubble; 1009 if (bubble) { 1010 bubble.setClassName(this._bubbleClassName); 1011 } 1012 1013 if (!this._noOutsideListening && (this.getSelectionCount() > 0)) { 1014 DBG.println("aif", "REMOVE menu from outside listening " + this._input.id); 1015 var omem = appCtxt.getOutsideMouseEventMgr(); 1016 omem.stopListening({id:"ZmAddressInputField", obj:this.getActionMenu()}); 1017 } 1018 1019 // use a timer since popdown happens before listeners are called; alternatively, we could put the 1020 // code below at the end of every menu action listener 1021 AjxTimedAction.scheduleAction(new AjxTimedAction(this, 1022 function() { 1023 DBG.println("aif", "_menuPopdownListener"); 1024 ZmAddressInputField.menuContext = {}; 1025 this._bubbleList.clearRightSelection(); 1026 }), 10); 1027 }; 1028 1029 ZmAddressInputField.prototype._enterEditMode = 1030 function(bubble) { 1031 1032 DBG.println("aif", "ENTER edit mode"); 1033 if (this._editMode) { 1034 // user double-clicked a bubble while another bubble was being edited 1035 this._leaveEditMode(); 1036 } 1037 1038 this._editMode = true; 1039 this._editModeIndex = this._getBubbleIndex(bubble); 1040 DBG.println("aif", "MOVE input"); 1041 this._holder.insertBefore(this._input, bubble.getHtmlElement()); 1042 this.removeBubble(bubble.id, true); 1043 1044 this._editModeBubble = bubble; 1045 this._setInputValue(bubble.address); 1046 1047 // Chrome triggers BLUR after DBLCLICK, so use a timer to make sure select works 1048 AjxTimedAction.scheduleAction(new AjxTimedAction(this, 1049 function() { 1050 this.focus(); 1051 this._input.select(); 1052 }), 20); 1053 1054 if (this._singleBubble) { 1055 this._setInputEnabled(true); 1056 } 1057 }; 1058 1059 ZmAddressInputField.prototype._leaveEditMode = 1060 function(restore) { 1061 1062 DBG.println("aif", "LEAVE edit mode"); 1063 if (!this._editMode) { 1064 return; 1065 } 1066 1067 if (this._holder.lastChild != this._input) { 1068 this._holder.appendChild(this._input); 1069 } 1070 var bubble = restore && this._editModeBubble; 1071 this._checkInput(bubble && bubble.address); 1072 this.focus(); 1073 1074 this._editMode = false; 1075 this._editModeIndex = this._editModeBubble = null; 1076 DBG.println("aif", "input value: " + AjxStringUtil.htmlEncode(this._input.value)); 1077 }; 1078 1079 // size the input to a bit more than its current content 1080 ZmAddressInputField.prototype._resizeInput = 1081 function() { 1082 var val = AjxStringUtil.htmlEncode(this._input.value); 1083 var paddings = Dwt.getMargins(this._holder); 1084 var margins = Dwt.getMargins(this._input); 1085 var maxWidth = Dwt.getSize(this._holder).x - (this._input.offsetLeft + ((AjxEnv.isTrident) ? (margins.left + paddings.left) : 0) + paddings.right + margins.right + 1); 1086 maxWidth = Math.max(maxWidth, 3); //don't get too small - minimum 3 - if it gets negative, the cursor would not show up before starting to type (bug 84924) 1087 1088 var inputWidth = "100%"; 1089 if (this._input.supportsAutoComplete) { 1090 var inputFontSize = DwtCssStyle.getProperty(this._input, "font-size"); 1091 var strW = AjxStringUtil.getWidth(val, false, inputFontSize); 1092 if (AjxEnv.isWindows && (AjxEnv.isFirefox || AjxEnv.isSafari || AjxEnv.isChrome) ){ 1093 // FF/Win: fudge factor since string is longer in INPUT than when measured in SPAN 1094 strW = strW * 1.2; 1095 } 1096 var pad = this._editMode ? ZmAddressInputField.INPUT_EXTRA_SMALL : ZmAddressInputField.INPUT_EXTRA; 1097 inputWidth = Math.min(strW + pad, maxWidth); 1098 if (this._editMode) { 1099 inputWidth = Math.max(inputWidth, ZmAddressInputField.INPUT_EXTRA); 1100 } 1101 } 1102 Dwt.setSize(this._input, inputWidth, Dwt.DEFAULT); 1103 }; 1104 1105 ZmAddressInputField.prototype.hasFocus = 1106 function(ev) { 1107 return true; 1108 }; 1109 1110 ZmAddressInputField.prototype.getKeyMapName = 1111 function() { 1112 return ZmKeyMap.MAP_ADDRESS; 1113 }; 1114 1115 // invoked when at least one bubble is selected 1116 ZmAddressInputField.prototype.handleKeyAction = 1117 function(actionCode, ev) { 1118 1119 var selCount = this.getSelectionCount(); 1120 if (!selCount || this._editMode) { 1121 // it might be nicer to allow arrowing out of the field (eg right arrow when at end of input) to move to 1122 // another bubble or toolbar control, but getting the cursor position is not reliable 1123 ev.forcePropagate = true; 1124 return true; 1125 } 1126 DBG.println("aif", "handle shortcut: " + actionCode); 1127 1128 switch (actionCode) { 1129 1130 case DwtKeyMap.DELETE: 1131 this.handleDelete(); 1132 break; 1133 1134 case DwtKeyMap.SELECT_NEXT: 1135 if (selCount == 1) { 1136 this._selectAdjacentBubble(true); 1137 } 1138 break; 1139 1140 case DwtKeyMap.SELECT_PREV: 1141 if (selCount == 1) { 1142 this._selectAdjacentBubble(false); 1143 } 1144 break; 1145 1146 default: 1147 return false; 1148 } 1149 1150 return true; 1151 }; 1152 1153 // Returns an ordered list of bubbles 1154 ZmAddressInputField.prototype._getBubbleList = 1155 function() { 1156 1157 var list = []; 1158 var children = this._holder.childNodes; 1159 for (var i = 0; i < children.length; i++) { 1160 var id = children[i].id; 1161 if (id && this._bubble[id]) { 1162 var bubble = DwtControl.fromElementId(id); 1163 if (bubble) { 1164 list.push(bubble); 1165 } 1166 } 1167 } 1168 1169 this._bubbleList.set(list); 1170 return this._bubbleList; 1171 }; 1172 1173 ZmAddressInputField.prototype.getBubbleCount = 1174 function() { 1175 return this._getBubbleList().getArray().length; 1176 }; 1177 1178 // returns the index of the given bubble among all the holder's elements (not just bubbles) 1179 ZmAddressInputField.prototype._getBubbleIndex = 1180 function(bubble) { 1181 return AjxUtil.indexOf(this._holder.childNodes, bubble.getHtmlElement()); 1182 }; 1183 1184 // returns the index of the INPUT among all the holder's elements 1185 ZmAddressInputField.prototype._getInputIndex = 1186 function() { 1187 return AjxUtil.indexOf(this._holder.childNodes, this._input); 1188 }; 1189 1190 /** 1191 * Selects the next or previous bubble relative to the selected one. 1192 * 1193 * @param {boolean} next if true, select next bubble; otherwise select previous bubble 1194 */ 1195 ZmAddressInputField.prototype._selectAdjacentBubble = 1196 function(next) { 1197 1198 var sel = this.getSelection(); 1199 var bubble = sel && sel.length && sel[0]; 1200 if (!bubble) { return; } 1201 1202 var index = this._getBubbleIndex(bubble); 1203 index = next ? index + 1 : index - 1; 1204 var children = this._holder.childNodes; 1205 var el = (index >= 0 && index < children.length) && children[index]; 1206 if (el == this._dragInsertionBar) { 1207 index = next ? index + 1 : index - 1; 1208 el = (index >= 0 && index < children.length) && children[index]; 1209 } 1210 if (el) { 1211 if (el == this._input) { 1212 this.setSelected(bubble, false); 1213 this.focus(); 1214 } 1215 else { 1216 var newBubble = DwtControl.fromElement(el); 1217 if (newBubble) { 1218 this.setSelected(bubble, false); 1219 this.setSelected(newBubble, true); 1220 } 1221 } 1222 } 1223 }; 1224 1225 /** 1226 * Returns an ordered list of bubble addresses. 1227 * 1228 * @param {boolean} asObjects if true, return list of AjxEmailAddress 1229 */ 1230 ZmAddressInputField.prototype.getAddresses = 1231 function(asObjects) { 1232 1233 var addrs = []; 1234 var bubbles = this._getBubbleList().getArray(); 1235 var ac = window.parentAppCtxt || window.appCtxt; 1236 for (var i = 0; i < bubbles.length; i++) { 1237 var bubble = bubbles[i]; 1238 var addr = bubble.address; 1239 if (asObjects) { 1240 var addrObj = AjxEmailAddress.parse(addr) || new AjxEmailAddress("", null, addr); 1241 if (ac.isExpandableDL(bubble.email) || (bubble.match && bubble.match.isDL)) { 1242 addrObj.isGroup = true; 1243 addrObj.canExpand = true; 1244 } 1245 addrs.push(addrObj); 1246 } 1247 else { 1248 addrs.push(addr); 1249 } 1250 } 1251 return addrs; 1252 }; 1253 1254 ZmAddressInputField._getAddrInputFromEvent = 1255 function(ev) { 1256 var target = DwtUiEvent.getTarget(ev); 1257 return target && DwtControl.ALL_BY_ID[target._aifId]; 1258 }; 1259 1260 /** 1261 * Since both the input and each of its bubbles has a drop listener, the target object may be 1262 * either of those object types. Dropping is okay if we're over a different type of input, or if 1263 * we're reordering bubbles within the same input. 1264 */ 1265 ZmAddressInputField.prototype._dropListener = 1266 function(dragEv) { 1267 1268 var sel = dragEv.srcData && dragEv.srcData.selection; 1269 if (!(sel && sel.length)) { return; } 1270 1271 if (dragEv.action == DwtDropEvent.DRAG_ENTER) { 1272 DBG.println("aif", "DRAG_ENTER"); 1273 var targetObj = dragEv.uiEvent.dwtObj; 1274 var targetInput = targetObj.isAddressBubble ? targetObj.addrInput : targetObj; 1275 var dragBubble = sel[0]; 1276 if (dragBubble.type != this.type) { 1277 dragEv.doIt = true; 1278 } 1279 else if (targetInput._numBubbles <= 1) { 1280 dragEv.doIt = false; 1281 } 1282 if (dragEv.doIt && targetInput._numBubbles >= 1) { 1283 var idx = targetInput._getIndexFromEvent(dragEv.uiEvent); 1284 var bubbleIdx = targetInput._getBubbleIndex(dragBubble); 1285 DBG.println("aif", "idx: " + idx + ", bubbleIdx: " + bubbleIdx); 1286 if ((dragBubble.type == this.type) && (idx == bubbleIdx || idx == bubbleIdx + 1)) { 1287 dragEv.doIt = false; 1288 } 1289 else { 1290 this._setInsertionBar(idx); 1291 } 1292 } 1293 if (!dragEv.doIt) { 1294 this._setInsertionBar(null); 1295 } 1296 } 1297 else if (dragEv.action == DwtDropEvent.DRAG_LEAVE) { 1298 DBG.println("aif", "DRAG_LEAVE"); 1299 this._setInsertionBar(null); 1300 } 1301 else if (dragEv.action == DwtDropEvent.DRAG_DROP) { 1302 DBG.println("aif", "DRAG_DROP"); 1303 var sourceInput = dragEv.srcData.addrInput; 1304 var index = this._getInsertionIndex(this._dragInsertionBar); 1305 for (var i = 0; i < sel.length; i++) { 1306 var bubble = sel[i]; 1307 var id = bubble.id; 1308 this.addBubble({bubble:bubble, index:index + i}); 1309 sourceInput.removeBubble(id); 1310 } 1311 this._setInsertionBar(null); 1312 } 1313 }; 1314 1315 ZmAddressInputField.prototype._dragBoxListener = 1316 function(ev) { 1317 // Check if user is using scroll bar rather than trying to drag. 1318 if (ev && ev.srcControl && this._holder) { 1319 var scrollWidth = this._holder.scrollWidth; //returns width w/out scrollbar 1320 var scrollPos = scrollWidth + Dwt.getLocation(this._holder).x; 1321 var dBox = ev.srcControl.getDragBox(); 1322 if (dBox) { 1323 DBG.println("aif", "DRAG_BOX x =" + dBox.getStartX() + " scrollWidth = " + scrollWidth); 1324 if (dBox.getStartX() > scrollPos) { 1325 DBG.println("aif", "DRAG_BOX x =" + dBox.getStartX() + " scrollPos = " + scrollPos); 1326 return false; 1327 } 1328 } 1329 } 1330 1331 if (ev.action == DwtDragEvent.DRAG_INIT) { 1332 // okay to draw drag box if we have at least one bubble, and user isn't clicking in 1333 // the non-empty INPUT (might be trying to select text) 1334 return (this._numBubbles > 0 && (ev.target != this._input || this._input.value == "")); 1335 } 1336 else if (ev.action == DwtDragEvent.DRAG_START) { 1337 DBG.println("aif", "ZmAddressInputField DRAG_START"); 1338 this.deselectAll(); 1339 this.blur(); 1340 } 1341 else if (ev.action == DwtDragEvent.DRAG_MOVE) { 1342 // DBG.println("aif", "ZmAddressInputField DRAG_MOVE"); 1343 var box = this._dragSelectionBox; 1344 for (var id in this._bubble) { 1345 var bubble = this._bubble[id]; 1346 var span = bubble.getHtmlElement(); 1347 var sel = Dwt.doOverlap(box, span); 1348 if (sel != this._bubbleList.isSelected(bubble)) { 1349 this.setSelected(bubble, sel); 1350 appCtxt.getKeyboardMgr().grabFocus(bubble); 1351 } 1352 } 1353 } 1354 else if (ev.action == DwtDragEvent.DRAG_END) { 1355 DBG.println("aif", "ZmAddressInputField DRAG_END"); 1356 this._bubbleList._checkSelection(); 1357 if (AjxEnv.isWindows && (this.getSelectionCount() == 0)) { 1358 this.blur(); 1359 this.focus(); 1360 } 1361 } 1362 }; 1363 1364 ZmAddressInputField.prototype._mouseDownListener = 1365 function(ev) { 1366 // reset mouse event to propagate event to browser (allows focus on input when clicking on holder click) 1367 ev._stopPropagation = false; 1368 ev._returnValue = true; 1369 }; 1370 1371 // Returns insertion index (among all elements) based on event coordinates 1372 ZmAddressInputField.prototype._getIndexFromEvent = 1373 function(ev) { 1374 1375 var bubble, w, bx, idx; 1376 var bubble = (ev.dwtObj && ev.dwtObj.isAddressBubble) ? ev.dwtObj : null; 1377 if (bubble) { 1378 w = bubble.getSize().x; 1379 bx = ev.docX - bubble.getLocation().x; 1380 idx = this._getBubbleIndex(bubble); // TODO: cache? 1381 return (bx > (w / 2)) ? idx + 1 : idx; 1382 } 1383 else { 1384 idx = 0; 1385 var children = this._holder.childNodes; 1386 for (var i = 0; i < children.length; i++) { 1387 var id = children[i].id; 1388 bubble = id && this._bubble[id]; 1389 if (bubble) { 1390 w = bubble.getSize().x; 1391 bx = ev.docX - bubble.getLocation().x; 1392 if (bx < (w / 2)) { 1393 return idx; 1394 } 1395 else { 1396 idx++; 1397 } 1398 } 1399 else if (i < (children.length - 1)) { 1400 idx++; 1401 } 1402 } 1403 return idx; 1404 } 1405 }; 1406 1407 ZmAddressInputField.prototype._setInsertionBar = 1408 function(index) { 1409 1410 if (index == this._dragInsertionBarIndex) { return; } 1411 1412 var bar = this._dragInsertionBar; 1413 if (index != null) { 1414 bar.style.display = "inline"; 1415 var refElement = this._holder.childNodes[index]; 1416 if (refElement) { 1417 this._holder.insertBefore(bar, refElement); 1418 this._dragInsertionBarIndex = index; 1419 } 1420 } 1421 else { 1422 bar.style.display = "none"; 1423 this._dragInsertionBarIndex = null; 1424 } 1425 }; 1426 1427 ZmAddressInputField.prototype._getInsertionIndex = 1428 function(element) { 1429 1430 var bubbleIndex = 0; 1431 var children = this._holder.childNodes; 1432 for (var i = 0; i < children.length; i++) { 1433 var el = children[i]; 1434 if (el == element) { 1435 break; 1436 } 1437 else if (el && this._bubble[el.id]) { 1438 bubbleIndex++; 1439 } 1440 } 1441 return bubbleIndex; 1442 }; 1443 1444 1445 1446 1447 /** 1448 * Creates a bubble that contains an email address. 1449 * @constructor 1450 * @class 1451 * This class represents an object that allows various operations to be performed on an 1452 * email address within a compose or display context. 1453 * 1454 * @param {hash} params the hash of parameters: 1455 * @param {ZmAddressInputField} parent parent control 1456 * @param {string} id element ID for the bubble 1457 * @param {string} className CSS class for the bubble 1458 * @param {string} address email address to display in the bubble 1459 * @param {AjxEmailAddress} addrObj email address (alternative form) 1460 * @param {boolean} canRemove if true, an x will be provided to remove the address bubble 1461 * @param {boolean} canExpand if true, a + will be provided to expand the DL address 1462 * @param {string} separator address separator 1463 * 1464 * @extends DwtControl 1465 */ 1466 ZmAddressBubble = function(params) { 1467 1468 params = params || {}; 1469 params.id = this.id = params.id || Dwt.getNextId(); 1470 params.className = params.className || "addrBubble"; 1471 if (params.addClass) { 1472 params.className = [params.className, params.addClass].join(" "); 1473 } 1474 DwtControl.call(this, params); 1475 1476 this.type = params.type; 1477 this.isAddressBubble = true; 1478 1479 var addrInput = this.addrInput = params.addrInput; 1480 var match = this.match = params.match; 1481 var addrContent = !params.noParse && (params.address || (match && match.email)); 1482 var addrObj = this.addrObj = params.addrObj || (addrContent && AjxEmailAddress.parse(addrContent)); 1483 this.address = params.address || (addrObj && addrObj.toString()); 1484 this.email = params.email = params.email || (addrObj && addrObj.getAddress()) || ""; 1485 // text search bubbles won't have anything in the "email" field so we need to use "address" for hash lookup 1486 this.hashKey = this.type === ZmId.SEARCH ? this.address : this.email; 1487 var ac = window.parentAppCtxt || window.appCtxt; 1488 this.canExpand = params.canExpand = params.canExpand || ac.isExpandableDL(this.email); 1489 1490 this._createHtml(params); 1491 1492 this._setEventHdlrs([DwtEvent.ONCLICK, DwtEvent.ONDBLCLICK, 1493 DwtEvent.ONMOUSEOVER, DwtEvent.ONMOUSEOUT, 1494 DwtEvent.ONMOUSEDOWN, DwtEvent.ONMOUSEMOVE, DwtEvent.ONMOUSEUP]); 1495 this.addListener(DwtEvent.ONCLICK, this._clickListener.bind(this)); 1496 this.addListener(DwtEvent.ONDBLCLICK, this._dblClickListener.bind(this)); 1497 this.addListener(DwtEvent.ONMOUSEUP, this._mouseUpListener.bind(this)); 1498 1499 if (addrInput) { 1500 var dragSrc = new DwtDragSource(Dwt.DND_DROP_MOVE); 1501 dragSrc.addDragListener(this._dragListener.bind(this)); 1502 this.setDragSource(dragSrc); 1503 } 1504 1505 this._evtMgr = new AjxEventMgr(); 1506 this._selEv = new DwtSelectionEvent(true); 1507 }; 1508 1509 ZmAddressBubble.prototype = new DwtControl; 1510 ZmAddressBubble.prototype.constructor = ZmAddressBubble; 1511 1512 ZmAddressBubble.prototype.isZmAddressBubble = true; 1513 ZmAddressBubble.prototype.toString = function() { return "ZmAddressBubble"; }; 1514 ZmAddressBubble.prototype.isFocusable = true; 1515 1516 ZmAddressBubble.prototype._createElement = 1517 function() { 1518 return document.createElement("SPAN"); 1519 }; 1520 1521 ZmAddressBubble.prototype._createHtml = 1522 function(params) { 1523 1524 var el = this.getHtmlElement(); 1525 el.innerHTML = ZmAddressBubble.getContent(params); 1526 if (params.parentId) { 1527 el._aifId = params.parentId; 1528 } 1529 }; 1530 1531 /** 1532 * Returns HTML for the content of a bubble. 1533 * 1534 * @param {hash} params the hash of parameters: 1535 * @param {ZmAddressInputField} parent parent control 1536 * @param {string} id element ID for the bubble 1537 * @param {string} className CSS class for the bubble 1538 * @param {string} address email address to display in the bubble 1539 * @param {AjxEmailAddress} addrObj email address (alternative form) 1540 * @param {boolean} canRemove if true, an x will be provided to remove the address bubble 1541 * @param {boolean} canExpand if true, a + will be provided to expand the DL address 1542 * @param {boolean} noParse if true, do not parse content to see if it is an address 1543 */ 1544 ZmAddressBubble.getContent = 1545 function(params) { 1546 1547 var id = params.id; 1548 var addrObj = params.addrObj || (!params.noParse && AjxEmailAddress.parse(params.address)) || params.address || ZmMsg.unknown; 1549 var fullAddress = AjxStringUtil.htmlEncode(addrObj ? addrObj.toString() : params.address); 1550 var text = AjxStringUtil.htmlEncode(addrObj ? addrObj.toString(appCtxt.get(ZmSetting.SHORT_ADDRESS)) : params.address); 1551 1552 var expandLinkText = "", removeLinkText = "", addrStyle = ""; 1553 var style = "cursor:pointer;position:absolute;top:2px;"; 1554 1555 if (params.canExpand) { 1556 var addr = params.email || params.address; 1557 var expandLinkId = id + "_expand"; 1558 var expandLink = 'ZmAddressBubble.expandBubble("' + id + '","' + addr + '");'; 1559 var expStyle = style + "left:2px;"; 1560 var expandLinkText = AjxImg.getImageHtml("BubbleExpand", expStyle, "id='" + expandLinkId + "' onclick='" + expandLink + "'"); 1561 addrStyle += "padding-left:12px;"; 1562 } 1563 1564 if (params.canRemove) { 1565 var removeLinkId = id + "_remove"; 1566 var removeLink = 'ZmAddressInputField.removeBubble("' + id + '");'; 1567 var removeStyle = style + "right:2px;"; 1568 var removeLinkText = AjxImg.getImageHtml("BubbleDelete", removeStyle, "id='" + removeLinkId + "' onclick='" + removeLink + "'"); 1569 addrStyle += "padding-right:12px;"; 1570 } 1571 1572 var html = [], idx = 0; 1573 var addrStyleText = (params.canExpand || params.canRemove) ? " style='" + addrStyle + "'" : ""; 1574 html[idx++] = "<span" + addrStyleText + ">" + text + " </span>"; 1575 var addrText = html.join(""); 1576 1577 return expandLinkText + addrText + removeLinkText; 1578 }; 1579 1580 1581 /** 1582 * Gets the key map name. 1583 * 1584 * @return {string} the key map name 1585 */ 1586 ZmAddressBubble.prototype.getKeyMapName = 1587 function() { 1588 return DwtKeyMap.MAP_BUTTON; 1589 }; 1590 1591 /** 1592 * Handles a key action event. 1593 * 1594 * @param {constant} actionCode the action code (see {@link DwtKeyMap}) 1595 * @param {DwtEvent} ev the event 1596 * @return {boolean} <code>true</code> if the event is handled; <code>false</code> otherwise 1597 * @see DwtKeyMap 1598 */ 1599 ZmAddressBubble.prototype.handleKeyAction = function(actionCode, ev) { 1600 1601 if (!this.list || (this.addrInput && this.addrInput._editMode)) { 1602 return true; 1603 } 1604 1605 switch (actionCode) { 1606 case DwtKeyMap.SELECT: 1607 case DwtKeyMap.SUBMENU: 1608 this.list._itemActioned(ev, this); 1609 break; 1610 } 1611 1612 return true; 1613 }; 1614 1615 /** 1616 * Adds a selection listener. 1617 * 1618 * @param {AjxListener} listener the listener 1619 */ 1620 ZmAddressBubble.prototype.addSelectionListener = 1621 function(listener) { 1622 this._evtMgr.addListener(DwtEvent.SELECTION, listener); 1623 }; 1624 1625 /** 1626 * Removes a selection listener. 1627 * 1628 * @param {AjxListener} listener the listener 1629 */ 1630 ZmAddressBubble.prototype.removeSelectionListener = 1631 function(listener) { 1632 this._evtMgr.removeListener(DwtEvent.SELECTION, listener); 1633 }; 1634 1635 ZmAddressBubble.prototype._clickListener = 1636 function(ev) { 1637 if (this.list && this._dragging == DwtControl._NO_DRAG) { 1638 this.list._itemClicked(ev, this); 1639 } 1640 else if (this._evtMgr.isListenerRegistered(DwtEvent.SELECTION)) { 1641 DwtUiEvent.copy(this._selEv, ev); 1642 this._selEv.item = this; 1643 this._selEv.detail = DwtEvent.ONCLICK; 1644 this._evtMgr.notifyListeners(DwtEvent.SELECTION, this._selEv); 1645 } 1646 }; 1647 1648 ZmAddressBubble.prototype._dblClickListener = 1649 function(ev) { 1650 if (!this.list) { return; } 1651 this.list._itemDoubleClicked(ev, this); 1652 }; 1653 1654 ZmAddressBubble.prototype._mouseUpListener = 1655 function(ev) { 1656 if (!this.list) { return; } 1657 if (ev.button == DwtMouseEvent.RIGHT) { 1658 this.list._itemActioned(ev, this); 1659 } 1660 }; 1661 1662 ZmAddressBubble.prototype._getDragProxy = 1663 function(dragOp) { 1664 1665 var icon = document.createElement("div"); 1666 icon.className = this._className; 1667 Dwt.setPosition(icon, Dwt.ABSOLUTE_STYLE); 1668 var count = this.addrInput.getSelectionCount(this); 1669 var content; 1670 if (count == 1) { 1671 var addrObj = AjxEmailAddress.parse(this.address) || this.address || ZmMsg.unknown; 1672 content = AjxStringUtil.htmlEncode(addrObj ? addrObj.toString(appCtxt.get(ZmSetting.SHORT_ADDRESS)) : this.address); 1673 } 1674 else { 1675 content = AjxMessageFormat.format(ZmMsg.numAddresses, count); 1676 } 1677 icon.innerHTML = content; 1678 this.shell.getHtmlElement().appendChild(icon); 1679 Dwt.setZIndex(icon, Dwt.Z_DND); 1680 return icon; 1681 }; 1682 1683 ZmAddressBubble.prototype._dragListener = 1684 function(ev) { 1685 if (ev.action == DwtDragEvent.SET_DATA) { 1686 ev.srcData = {selection: this.addrInput.getSelection(this), 1687 addrInput: this.addrInput}; 1688 } 1689 }; 1690 1691 ZmAddressBubble.prototype._dragOver = 1692 function(ev) { 1693 this.addrInput._dragOver(ev); 1694 }; 1695 1696 /** 1697 * Gets the tool tip content. 1698 * 1699 * @param {Object} ev the hover event 1700 * @return {String} the tool tip content 1701 */ 1702 ZmAddressBubble.prototype.getToolTipContent = 1703 function(ev) { 1704 1705 var ttParams = {address:this.addrObj, ev:ev}; 1706 var ttCallback = new AjxCallback(this, 1707 function(callback) { 1708 appCtxt.getToolTipMgr().getToolTip(ZmToolTipMgr.PERSON, ttParams, callback); 1709 }); 1710 return {callback:ttCallback}; 1711 }; 1712 1713 // Bug 78359 - hack so that shortcuts work even though browser focus is on hidden textarea 1714 ZmAddressBubble.prototype.hasFocus = 1715 function() { 1716 return true; 1717 }; 1718 1719 /** 1720 * Expands the distribution list address of the bubble with the given ID. 1721 * 1722 * @param {string} bubbleId ID of bubble 1723 * @param {string} email address to expand 1724 */ 1725 ZmAddressBubble.expandBubble = function(bubbleId, email) { 1726 1727 var bubble = document.getElementById(bubbleId); 1728 if (bubble) { 1729 var parentId = bubble._aifId || ZmAddressInputField.BUBBLE_OBJ_ID[bubbleId]; 1730 var parent = bubble && DwtControl.ALL_BY_ID[parentId]; 1731 if (parent && parent.getEnabled() && parent._aclv) { 1732 var bubbleObj = DwtControl.fromElementId(bubbleId); 1733 if (bubbleObj) { 1734 var loc = bubbleObj.getLocation(); 1735 loc.y += bubbleObj.getSize().y + 2; 1736 parent._aclv.expandDL({ 1737 email: email, 1738 textId: bubbleObj._htmlElId, 1739 loc: loc, 1740 element: parent._input 1741 }); 1742 } 1743 } 1744 } 1745 }; 1746 1747 1748 1749 /** 1750 * Creates an empty bubble list. 1751 * @constructor 1752 * @class 1753 * This class manages selection events (click, double-click, and right-click) of a collection of bubbles, since 1754 * those events are typically meaningful within a group of bubbles. It maintains the visual state of the bubble 1755 * and notifies any listeners of the selection events. 1756 * 1757 * @param {hash} params hash of params: 1758 * @param {ZmAddressInputField} parent parent 1759 * @param {string} normalClass class for an unselected bubble 1760 * @param {string} selClass class for a selected bubble 1761 * @param {string} rightSelClass class for a right-clicked bubble 1762 */ 1763 ZmAddressBubbleList = function(params) { 1764 1765 params = params || {}; 1766 this.parent = params.parent; 1767 this._separator = params.separator || AjxEmailAddress.SEPARATOR; 1768 1769 this._normalClass = params.normalClass || "addrBubble"; 1770 this._selClass = params.selClass || this._normalClass + "-" + DwtCssStyle.SELECTED; 1771 this._actionClass = params.rightSelClass || this._normalClass + "-" + DwtCssStyle.ACTIONED; 1772 1773 this._evtMgr = new AjxEventMgr(); 1774 this._selEv = new DwtSelectionEvent(true); 1775 this._actionEv = new DwtListViewActionEvent(true); 1776 1777 this.reset(); 1778 }; 1779 1780 ZmAddressBubbleList.prototype.isZmAddressBubbleList = true; 1781 ZmAddressBubbleList.prototype.toString = function() { return "ZmAddressBubbleList"; }; 1782 1783 ZmAddressBubbleList.prototype.set = 1784 function(list) { 1785 1786 this._bubbleList = []; 1787 var selected = {}; 1788 this._numSelected = 0; 1789 for (var i = 0; i < list.length; i++) { 1790 var bubble = list[i]; 1791 this._bubbleList.push(bubble); 1792 if (this._selected[bubble.id]) { 1793 selected[bubble.id] = true; 1794 DBG.println("aif", "ZmAddressBubbleList::set - bubble selected: " + bubble.address); 1795 this._numSelected++; 1796 } 1797 } 1798 this._selected = selected; 1799 }; 1800 1801 ZmAddressBubbleList.prototype.getArray = 1802 function(list) { 1803 return this._bubbleList; 1804 }; 1805 1806 ZmAddressBubbleList.prototype.add = 1807 function(bubble, index) { 1808 AjxUtil.arrayAdd(this._bubbleList, bubble, index); 1809 bubble.list = this; 1810 }; 1811 1812 ZmAddressBubbleList.prototype.remove = 1813 function(bubble) { 1814 AjxUtil.arrayRemove(this._bubbleList, bubble); 1815 bubble.list = null; 1816 if (this._selected[bubble.id]) { 1817 this._numSelected--; 1818 this._selected[bubble.id] = false; 1819 this._checkSelection(); 1820 } 1821 if (bubble == this._rightSelBubble) { 1822 this._rightSelBubble = null; 1823 } 1824 bubble.dispose(); 1825 }; 1826 1827 ZmAddressBubbleList.prototype.clear = function() { 1828 while (this._bubbleList.length > 0) { 1829 this.remove(this._bubbleList[this._bubbleList.length - 1]); 1830 } 1831 }; 1832 1833 ZmAddressBubbleList.prototype.getBubble = 1834 function(index) { 1835 index = index || 0; 1836 return this._bubbleList[index]; 1837 }; 1838 1839 /** 1840 * Adds a selection listener. 1841 * 1842 * @param {AjxListener} listener the listener 1843 */ 1844 ZmAddressBubbleList.prototype.addSelectionListener = 1845 function(listener) { 1846 this._evtMgr.addListener(DwtEvent.SELECTION, listener); 1847 }; 1848 1849 /** 1850 * Removes a selection listener. 1851 * 1852 * @param {AjxListener} listener the listener 1853 */ 1854 ZmAddressBubbleList.prototype.removeSelectionListener = 1855 function(listener) { 1856 this._evtMgr.removeListener(DwtEvent.SELECTION, listener); 1857 }; 1858 1859 /** 1860 * Adds an action listener. 1861 * 1862 * @param {AjxListener} listener the listener 1863 */ 1864 ZmAddressBubbleList.prototype.addActionListener = 1865 function(listener) { 1866 this._evtMgr.addListener(DwtEvent.ACTION, listener); 1867 }; 1868 1869 /** 1870 * Removes an action listener. 1871 * 1872 * @param {AjxListener} listener the listener 1873 */ 1874 ZmAddressBubbleList.prototype.removeActionListener = 1875 function(listener) { 1876 this._evtMgr.removeListener(DwtEvent.ACTION, listener); 1877 }; 1878 1879 ZmAddressBubbleList.prototype._itemClicked = 1880 function(ev, bubble) { 1881 1882 if (ev.shiftKey) { 1883 if (this._lastSelectedId) { 1884 var select = false; 1885 for (var i = 0, len = this._bubbleList.length; i < len; i++) { 1886 var b = this._bubbleList[i]; 1887 if (b == bubble || b.id == this._lastSelectedId) { 1888 if (select) { 1889 this.setSelected(b, true); 1890 select = false; 1891 continue; 1892 } 1893 select = !select; 1894 } 1895 this.setSelected(b, select); 1896 } 1897 } 1898 } 1899 else if (ev.ctrlKey || ev.metaKey) { 1900 this.setSelected(bubble, !this._selected[bubble.id]); 1901 if (this._selected[bubble.id]) { 1902 this._lastSelectedId = bubble.id; 1903 } 1904 } 1905 else { 1906 var wasOnlyOneSelected = ((this.getSelectionCount() == 1) && this._selected[bubble.id]); 1907 this.deselectAll(); 1908 this.setSelected(bubble, !wasOnlyOneSelected); 1909 this._lastSelectedId = wasOnlyOneSelected ? null : bubble.id; 1910 } 1911 1912 if (this._evtMgr.isListenerRegistered(DwtEvent.SELECTION)) { 1913 DwtUiEvent.copy(this._selEv, ev); 1914 this._selEv.item = bubble; 1915 this._selEv.detail = DwtEvent.ONCLICK; 1916 this._evtMgr.notifyListeners(DwtEvent.SELECTION, this._selEv); 1917 } 1918 }; 1919 1920 ZmAddressBubbleList.prototype._itemDoubleClicked = 1921 function(ev, bubble) { 1922 1923 if (this._evtMgr.isListenerRegistered(DwtEvent.SELECTION)) { 1924 DwtUiEvent.copy(this._selEv, ev); 1925 this._selEv.item = bubble; 1926 this._selEv.detail = DwtEvent.ONDBLCLICK; 1927 this._evtMgr.notifyListeners(DwtEvent.SELECTION, this._selEv); 1928 } 1929 }; 1930 1931 ZmAddressBubbleList.prototype._itemActioned = 1932 function(ev, bubble) { 1933 1934 this._rightSelBubble = bubble; 1935 bubble.setClassName(this._actionClass); 1936 if (this._evtMgr.isListenerRegistered(DwtEvent.ACTION)) { 1937 DwtUiEvent.copy(this._actionEv, ev); 1938 this._actionEv.item = bubble; 1939 this._evtMgr.notifyListeners(DwtEvent.ACTION, this._actionEv); 1940 } 1941 }; 1942 1943 /** 1944 * Sets selection of the given bubble. 1945 * 1946 * @param {ZmAddressBubble} bubble bubble to select 1947 * @param {boolean} selected if true, select the bubble, otherwise deselect it 1948 */ 1949 ZmAddressBubbleList.prototype.setSelected = 1950 function(bubble, selected) { 1951 1952 if (!bubble) { return; } 1953 if (selected == Boolean(this._selected[bubble.id])) { return; } 1954 1955 this._selected[bubble.id] = selected; 1956 bubble.setClassName(selected ? this._selClass : this._normalClass); 1957 1958 this._numSelected = selected ? this._numSelected + 1 : this._numSelected - 1; 1959 DBG.println("aif", "**** selected: " + selected + ", " + bubble.email + ", num = " + this._numSelected); 1960 this._checkSelection(); 1961 }; 1962 1963 ZmAddressBubbleList.prototype.isSelected = 1964 function(bubble) { 1965 return Boolean(bubble && this._selected[bubble.id]); 1966 }; 1967 1968 /** 1969 * Returns a list of the currently selected bubbles. If a bubble has been selected via right-click, 1970 * but is not part of the current left-click selection, only it will be returned. 1971 * 1972 * @param {ZmAddressBubble} bubble reference bubble 1973 */ 1974 ZmAddressBubbleList.prototype.getSelection = 1975 function(bubble) { 1976 1977 var ref = bubble || this._rightSelBubble; 1978 var refIncluded = false; 1979 var sel = []; 1980 for (var i = 0; i < this._bubbleList.length; i++) { 1981 var bubble = this._bubbleList[i]; 1982 if (this._selected[bubble.id]) { 1983 sel.push(bubble); 1984 if (bubble == ref) { 1985 refIncluded = true; 1986 } 1987 } 1988 } 1989 sel = (ref && !refIncluded) ? [ref] : sel; 1990 DBG.println("aif", "getSelection, sel length: " + sel.length); 1991 1992 return sel; 1993 }; 1994 1995 ZmAddressBubbleList.prototype.getSelectionCount = 1996 function(bubble) { 1997 return bubble ? this.getSelection(bubble).length : this._numSelected; 1998 }; 1999 2000 ZmAddressBubbleList.prototype.deselectAll = 2001 function() { 2002 DBG.println("aif", "deselectAll"); 2003 var sel = this.getSelection(); 2004 for (var i = 0, len = sel.length; i < len; i++) { 2005 this.setSelected(sel[i], false); 2006 } 2007 this._selected = {}; 2008 this._numSelected = 0; 2009 }; 2010 2011 ZmAddressBubbleList.prototype.clearRightSelection = 2012 function() { 2013 this._rightSelBubble = null; 2014 }; 2015 2016 ZmAddressBubbleList.prototype.reset = 2017 function(list) { 2018 this._bubbleList = []; 2019 this._selected = {}; 2020 this._numSelected = 0; 2021 }; 2022 2023 ZmAddressBubbleList.prototype.size = 2024 function() { 2025 return this._bubbleList.length; 2026 }; 2027 2028 ZmAddressBubbleList.prototype.selectAddressText = 2029 function() { 2030 2031 var sel = this.getSelection(); 2032 var addrs = []; 2033 for (var i = 0; i < sel.length; i++) { 2034 addrs.push(sel[i].address); 2035 } 2036 var textarea = this._getTextarea(); 2037 textarea.value = addrs.join(this._separator) + this._separator; 2038 textarea.focus(); 2039 textarea.select(); 2040 }; 2041 2042 ZmAddressBubbleList.prototype._getTextarea = 2043 function() { 2044 // hidden textarea used for copying address text 2045 if (!ZmAddressBubbleList._textarea) { 2046 var el = ZmAddressBubbleList._textarea = document.createElement("textarea"); 2047 el.id = "abcb"; // address bubble clipboard 2048 el["data-hidden"] = "1"; 2049 appCtxt.getShell().getHtmlElement().appendChild(el); 2050 Dwt.setPosition(el, Dwt.ABSOLUTE_STYLE); 2051 Dwt.setLocation(el, Dwt.LOC_NOWHERE, Dwt.LOC_NOWHERE); 2052 } 2053 return ZmAddressBubbleList._textarea; 2054 }; 2055 2056 ZmAddressBubbleList.prototype._checkSelection = 2057 function() { 2058 2059 // don't mess with outside listening if we're selecting via rubber-banding 2060 if (this.parent && (this.parent._noOutsideListening || this.parent._dragging == DwtControl._DRAGGING)) { return; } 2061 2062 if (!this._listening && this._numSelected == 1) { 2063 var omem = appCtxt.getOutsideMouseEventMgr(); 2064 var omemParams = { 2065 id: "ZmAddressBubbleList", 2066 elementId: null, // all clicks call our listener 2067 outsideListener: this._outsideMouseListener.bind(this), 2068 noWindowBlur: appCtxt.get(ZmSetting.IS_DEV_SERVER) 2069 } 2070 DBG.println("aif", "START outside listening for bubbles"); 2071 omem.startListening(omemParams); 2072 this._listening = true; 2073 } 2074 else if (this._listening && this._numSelected == 0) { 2075 var omem = appCtxt.getOutsideMouseEventMgr(); 2076 DBG.println("aif", "STOP outside listening for bubbles"); 2077 var omemParams = { 2078 id: "ZmAddressBubbleList", 2079 elementId: null 2080 } 2081 omem.stopListening(omemParams); 2082 this._listening = false; 2083 } 2084 this.selectAddressText(); 2085 }; 2086 2087 ZmAddressBubbleList.prototype._outsideMouseListener = 2088 function(ev, context) { 2089 2090 // modified clicks control list selection, ignore them 2091 if (!ev.shiftKey && !ev.ctrlKey && !ev.metaKey) { 2092 this.deselectAll(); 2093 } 2094 }; 2095