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