1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 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) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * @overview
 26  * 
 27  */
 28 
 29 /**
 30  * Creates a new autocomplete list. The list isn't populated or displayed until some
 31  * autocompletion happens. Takes a data class and loader, so that when data is needed (it's
 32  * loaded lazily), the loader can be called on the data class.
 33  * @class
 34  * This class implements autocomplete functionality. It has two main parts: matching data based
 35  * on keystroke events, and displaying/managing the list of matches. This class is theoretically
 36  * neutral concerning the data that gets matched (as long as its class has an <code>autocompleteMatch()</code>
 37  * method), and the field that it's being called from.
 38  * 
 39  * The data class's <code>autocompleteMatch()</code> method should returns a list of matches, where each match is
 40  * an object with the following properties:
 41  * <table border="1" width="50%">
 42  * <tr><td width="15%">data</td><td>the object being matched</td></tr>
 43  * <tr><td>text</td><td>the text to display for this object in the list</td></tr>
 44  * <tr><td>[key1]</td><td>a string that may be used to replace the typed text</td></tr>
 45  * <tr><td>[keyN]</td><td>a string that may be used to replace the typed text</td></tr>
 46  * </table>
 47  * 
 48  * The calling client also specifies the key in the match result for the string that will be used
 49  * to replace the typed text (also called the "completion string"). For example, the completion 
 50  * string for matching contacts could be a full address, or just the email.
 51  * 
 52  * The client may provide additional key event handlers in the form of callbacks. If the callback
 53  * explicitly returns true or false, that's what the event handler will return.
 54  * 
 55  * A single autocomplete list view may handle several related input fields. With the "quick complete" feature, there
 56  * may be multiple outstanding autocomplete requests to the server. Each request is managed through a context which
 57  * has all the information needed to make the request and handle its results.
 58  * 
 59  * 
 60  * 
 61  * Using Autocomplete
 62  * 
 63  * Autocomplete kicks in after there is a pause in the typing (that pause has to be at least 300ms by default). Let's say that
 64  * you are entering addresses into the To: field while composing an email. You type a few characters and then pause:
 65  * 
 66  * 	dav
 67  * 
 68  * ZCS will ask the user for people whose name or email address matches "dav", and display the matches in a list that pops up.
 69  * The matches will be sorted with the people you email the most at the top. When you select a match, that person's address
 70  * will replace the search string ("dav") in the To: field. Typically the address will be in a bubble.
 71  * 
 72  * 	Davey Jones x
 73  * 
 74  * Quick Complete
 75  * 
 76  * Many times you will know which address you're looking for, and you will type enough characters so that they will appear at
 77  * the top of the matches, and then you type semicolon or a return to select them once the list has come up. If you know that
 78  * the address you want will appear at the top of the matches based on what you've typed, then there's a way to select it 
 79  * without waiting for the list to come up: just type a semicolon. For example, let's assume that I email Davey Jones a lot,
 80  * and I know that if I type "dav" he will be the first match. I can just type
 81  * 
 82  * 	dav;
 83  * 
 84  * and continue, whether that's adding more addresses, or moving on to the subject and body (done easily via the Tab key).
 85  * Autocompletion will happen in the background, and will automatically replace "dav;" with the first match from the list. If 
 86  * no matches are found, nothing changes. One way to think of the Quick Complete feature is as the autocomplete version of 
 87  * Google's "I'm Feeling Lucky", though in this case you have a much better idea of what the results are going to be. You 
 88  * don't have to wait for the list to appear in order to add the bubble. It gets added for you.
 89  * 
 90  * You can type in multiple Quick Complete strings, and they will all be handled. For example, I could type
 91  * 
 92  * 	dav;pb;ann;x;
 93  * 
 94  * and see bubbles pop up for Davey Jones, Phil Bates, Ann Miller, and Xavier Gold without any more action on my part. I could
 95  * even type "dav;" into the To: field, hit Tab to go to the Cc: field, type "pb;" there, and then Tab to the Subject: field,
 96  * and start writing my message.
 97  * 
 98  * One small limitation of Quick Complete is that the bubbles will pop up within a field in the order that the results come 
 99  * back, which may not match the order of the strings you typed in. You can drag the bubbles to rearrange them if you want.
100  * 
101  * Special Keys
102  * 
103  * There are a number of keys that have special meanings when you are working with an input field that supports autocomplete. 
104  * Most of them apply while the list of matches is showing, and are used to control selection of the match you want:
105  * 
106  * Return		Adds the selected address
107  * Tab		Adds the selected address
108  * ;		Adds the selected address
109  * ,		Adds the selected address (if enabled in Preferences/Address Book/Autocomplete)
110  * DownArrow	Selects the next address (hold to repeat)
111  * UpArrow		Selects the previous address (hold to repeat)
112  * Esc		Hides the list
113  * 
114  * A few keys have special meanings while the list is not showing:
115  * 
116  * Return		If the input contains an email address, turn it into a bubble
117  * Tab		Go to the next field
118  * Esc		If requests are pending (it will say "Autocompleting"), cancel them. If not, cancel compose.
119  * 
120  * 
121  * 
122  * @author Conrad Damon
123  *
124  * @param {Hash}	params			a hash of parameters:
125  * @param	{String}		matchValue			the name of field in match result to use for completion
126  * @param	{function}		dataClass			the class that has the data loader
127  * @param	{function}		dataLoader			a method of dataClass that returns data to match against
128  * @param	{DwtComposite}	parent				the control that created this list (defaults to shell)
129  * @param	{String}		className			the CSS class
130  * @param	{Array}			delims				the list of delimiters (which separate tokens such as addresses)
131  * @param	{Array}			delimCodes			the list of delimiter key codes
132  * @param	{String}		separator			the separator (gets added to the end of a match)
133  * @param	{AjxCallback}	compCallback		the callback into client to notify it that completion happened
134  * @param	{AjxCallback}	selectionCallback	the callback into client to notify it that selection from extended DL happened (passed from email.js, and accessed from ZmDLAutocompleteListView.prototype._doUpdate)
135  * @param	{AjxCallback}	keyDownCallback		the additional client ONKEYDOWN handler
136  * @param	{AjxCallback}	keyPressCallback	the additional client ONKEYPRESS handler
137  * @param	{AjxCallback}	keyUpCallback		the additional client ONKEYUP handler
138  * @param	{string}		contextId			ID from parent
139  * @param	{Hash}			options				the additional options for the data class
140  * @param	{function}		locationCallback	used to customize list location (optional)
141  * 
142  * @extends		DwtComposite
143  */
144 ZmAutocompleteListView = function(params) {
145 
146 	if (arguments.length == 0) {
147 		return;
148 	}
149 
150 	params.parent = params.parent || appCtxt.getShell();
151 	params.className = params.className || "ZmAutocompleteListView";
152 	params.posStyle = DwtControl.ABSOLUTE_STYLE;
153 	params.id = params.contextId ? DwtId.makeId(ZmId.WIDGET_AUTOCOMPLETE, params.contextId) :
154 								   this._htmlElId || Dwt.getNextId("ZmAutocompleteListView_");
155 	DBG.println("acid", "ID: " + params.id);
156 	DwtComposite.call(this, params);
157 
158 	this._dataClass = this._dataAPI = params.dataClass;
159 	this._dataLoader = params.dataLoader;
160 	this._dataLoaded = false;
161 	this._matchValue = params.matchValue;
162 	this._selectionCallback = params.selectionCallback;
163 	this._separator = (params.separator != null) ? params.separator : AjxEmailAddress.SEPARATOR;
164     this._options = params.options || {};
165 	this._locationCallback = params.locationCallback;
166 	this._autocompleteType = params.autocompleteType;
167 
168 	this._callbacks = {};
169 	for (var i = 0; i < ZmAutocompleteListView.CALLBACKS.length; i++) {
170 		this._setCallbacks(ZmAutocompleteListView.CALLBACKS[i], params);
171 	}
172 
173 	this._isDelim = AjxUtil.arrayAsHash(params.delims || ZmAutocompleteListView.DELIMS);
174 	this._isDelimCode = AjxUtil.arrayAsHash(params.delimCodes || ZmAutocompleteListView.DELIM_CODES);
175 	if (!params.delims && !params.delimCodes) {
176 		this._isDelim[','] = this._isDelimCode[188] = appCtxt.get(ZmSetting.AUTOCOMPLETE_ON_COMMA); 
177 		var listener = new AjxListener(this, this._settingChangeListener);
178 		var aoc = appCtxt.getSettings().getSetting(ZmSetting.AUTOCOMPLETE_ON_COMMA);
179 		if (aoc) {
180 			aoc.addChangeListener(listener);
181 		}
182 	}
183 
184     // mouse event handling
185 	this._setMouseEventHdlrs();
186 	this.addListener(DwtEvent.ONMOUSEDOWN, new AjxListener(this, this._mouseDownListener));
187 	this.addListener(DwtEvent.ONMOUSEOVER, new AjxListener(this, this._mouseOverListener));
188 	this._addSelectionListener(new AjxListener(this, this._listSelectionListener));
189 	this._outsideListener = new AjxListener(null, ZmAutocompleteListView._outsideMouseDownListener);
190 
191 	// only trigger matching after a sufficient pause
192 	this._acInterval = appCtxt.get(ZmSetting.AC_TIMER_INTERVAL);
193 	this._acActionId = {};	// per element
194 
195 	// for managing focus on Tab in Firefox
196 	if (AjxEnv.isGeckoBased) {
197 		this._focusAction = new AjxTimedAction(null, this._autocompleteFocus);
198 	}
199 
200 	this._origClass = "acRow";
201 	this._selClass = "acRow-selected";
202 	this._showLinkTextClass = "LinkText";
203 	this._hideLinkTextClass = "LinkText-hide";
204 	this._hideSelLinkTextClass = "LinkText-hide-selected";
205 
206 	this._contexts 			= {};	// key is element ID
207 	this._inputValue		= {};	// key is element ID
208 	
209 	this.setVisible(false);
210 	this.setScrollStyle(Dwt.SCROLL);
211 	this.reset();
212 };
213 
214 ZmAutocompleteListView.prototype = new DwtComposite;
215 ZmAutocompleteListView.prototype.constructor = ZmAutocompleteListView;
216 ZmAutocompleteListView.prototype.toString = function() { return "ZmAutocompleteListView"; };
217 
218 ZmAutocompleteListView.CB_ADDR_FOUND	= "addrFound";
219 ZmAutocompleteListView.CB_COMPLETION	= "comp";
220 ZmAutocompleteListView.CB_KEYDOWN		= "keyDown";
221 ZmAutocompleteListView.CB_KEYPRESS		= "keyPress";
222 ZmAutocompleteListView.CB_KEYUP			= "keyUp";
223 ZmAutocompleteListView.CALLBACKS = [
224 		ZmAutocompleteListView.CB_ADDR_FOUND,
225 		ZmAutocompleteListView.CB_COMPLETION,
226 		ZmAutocompleteListView.CB_KEYDOWN,
227 		ZmAutocompleteListView.CB_KEYPRESS,
228 		ZmAutocompleteListView.CB_KEYUP
229 ];
230 
231 // map of characters that are completion characters
232 ZmAutocompleteListView.DELIMS = [',', ';', '\n', '\r'];	// used when list is not showing
233 ZmAutocompleteListView.DELIM_CODES = [                  // used when list is showing
234     DwtKeyEvent.KEY_COMMA,
235     DwtKeyEvent.KEY_SEMICOLON,
236     DwtKeyEvent.KEY_SEMICOLON_1,
237     DwtKeyEvent.KEY_END_OF_TEXT,
238     DwtKeyEvent.KEY_RETURN
239 ];
240 
241 ZmAutocompleteListView.WAIT_ID = "wait";
242 
243 // for list selection with up/down arrows
244 ZmAutocompleteListView.NEXT = -1;
245 ZmAutocompleteListView.PREV = -2;
246 
247 // possible states of an autocomplete context
248 ZmAutocompleteListView.STATE_NEW		= "NEW";
249 ZmAutocompleteListView.STATE_REQUEST	= "REQUEST";
250 ZmAutocompleteListView.STATE_RESPONSE	= "RESPONSE";
251 ZmAutocompleteListView.STATE_DONE		= "DONE";
252 
253 
254 
255 
256 /**
257  * Handles the on key down event.
258  * 
259  * @param	{Event}	event		the event
260  */
261 ZmAutocompleteListView.onKeyDown =
262 function(ev) {
263 
264 	ev = DwtUiEvent.getEvent(ev);
265 	var key = DwtKeyEvent.getCharCode(ev);
266 	var result = true;
267 	var element = DwtUiEvent.getTargetWithProp(ev, "_aclvId");
268 	DBG.println("ac", ev.type.toUpperCase() + " in " + (element && element.id) + ": " + key);
269 	var aclv = element && DwtControl.ALL_BY_ID[element._aclvId];
270 	if (aclv) {
271 		// if the user types a single delimiting character with the list showing, do completion
272 		var isDelim = (!ev.shiftKey && (aclv._isDelimCode[key] || (key === DwtKeyEvent.KEY_TAB && aclv.getVisible())));
273 		var visible = aclv.getVisible();
274 		aclv._actionHandled = false;
275 		// DBG.println("ac", "key = " + key + ", isDelim: " + isDelim);
276 		if (visible && aclv.handleAction(key, isDelim, element)) {
277 			aclv._actionHandled = true;
278 			result = false;
279 		}
280 
281 		aclv._inputValue[element.id] = element.value;
282 		var cbResult = aclv._runCallbacks(ZmAutocompleteListView.CB_KEYDOWN, element && element.id, [ev, aclv, result, element]);
283 		// DBG.println("ac", ev.type.toUpperCase() + " cbResult: " + cbResult);
284 		result = (cbResult === true || cbResult === false) ? cbResult : result;
285 	}
286 	if (AjxEnv.isFirefox){
287 		ZmAutocompleteListView.clearTimer();
288 		ZmAutocompleteListView.timer =  new AjxTimedAction(this, ZmAutocompleteListView.onKeyUp, [ev]);
289 		AjxTimedAction.scheduleAction(ZmAutocompleteListView.timer, 300)
290 	}
291 	return ZmAutocompleteListView._echoKey(result, ev);
292 };
293 
294 /**
295  * Handles the on key press event.
296  * 
297  * @param	{Event}	event		the event
298  */
299 ZmAutocompleteListView.onKeyPress =
300 function(ev) {
301 	ev = DwtUiEvent.getEvent(ev);
302 	DwtKeyEvent.geckoCheck(ev);
303 	var result = true;
304 	var key = DwtKeyEvent.getCharCode(ev);
305 	var element = DwtUiEvent.getTargetWithProp(ev, "_aclvId");
306 	DBG.println("ac", ev.type.toUpperCase() + " in " + (element && element.id) + ": " + key);
307 	var aclv = element && DwtControl.ALL_BY_ID[element._aclvId];
308 	if (aclv) {
309 		if (aclv._actionHandled) {
310 			result = false;
311 		}
312 		var cbResult = aclv._runCallbacks(ZmAutocompleteListView.CB_KEYPRESS, element && element.id, [ev, aclv, result, element]);
313 		DBG.println("ac", ev.type.toUpperCase() + " cbResult: " + cbResult);
314 		result = (cbResult === true || cbResult === false) ? cbResult : true;
315 	}
316 
317 	return ZmAutocompleteListView._echoKey(result, ev);
318 };
319 
320 /**
321  * Handles the on key up event.
322  * 
323  * @param	{Event}	event		the event
324  */
325 ZmAutocompleteListView.onKeyUp =
326 function(ev) {
327 	ev = DwtUiEvent.getEvent(ev);
328 	var result = true;
329 	var key = DwtKeyEvent.getCharCode(ev);
330 	var element = DwtUiEvent.getTargetWithProp(ev, "_aclvId");
331 	DBG.println("ac", ev.type.toUpperCase() + " in " + (element && element.id) + ": " + key);
332 	var aclv = element && DwtControl.ALL_BY_ID[element._aclvId];
333 	if (aclv) {
334 		if (aclv._actionHandled) {
335 			result = false;
336 		}
337 		var result = ZmAutocompleteListView._onKeyUp(ev);
338 		var cbResult = aclv._runCallbacks(ZmAutocompleteListView.CB_KEYUP, element && element.id, [ev, aclv, result, element]);
339 		DBG.println("ac", ev.type.toUpperCase() + " cbResult: " + cbResult);
340 		result = (cbResult === true || cbResult === false) ? cbResult : result;
341 	}
342 	return ZmAutocompleteListView._echoKey(result, ev);
343 };
344 
345 /**
346  * "onkeyup" handler for performing autocompletion. The reason it's an "onkeyup" handler is that it's the only one
347  * that arrives after the input has been updated.
348  *
349  * @param ev		the key event
350  * 
351  * @private
352  */
353 ZmAutocompleteListView._onKeyUp =
354 function(ev) {
355 
356 	var element = DwtUiEvent.getTargetWithProp(ev, "_aclvId");
357 	if (!element) {
358 		return ZmAutocompleteListView._echoKey(true, ev);
359 	}
360 
361 	var aclv = DwtControl.ALL_BY_ID[element._aclvId];
362 	var key = DwtKeyEvent.getCharCode(ev);
363 	var value = element.value;
364 	var elId = element.id;
365 	DBG.println("ac", ev.type + " event, key = " + key + ", value = " + value);
366 	ev.inputChanged = (value != aclv._inputValue[elId]);
367 
368 	// reset timer on any address field key activity
369 	if (aclv._acActionId[elId] !== -1 && !DwtKeyMap.IS_MODIFIER[key] && key !== DwtKeyEvent.KEY_TAB) {
370 		DBG.println("ac", "canceling autocomplete");
371 		AjxTimedAction.cancelAction(aclv._acActionId[elId]);
372 		aclv._acActionId[elId] = -1;
373 	}
374 
375 	// ignore modifier keys (including Shift), or a key with a modifier that makes it nonprintable
376 	if (DwtKeyMap.IS_MODIFIER[key] || DwtKeyMapMgr.hasModifier(ev)) {
377 		return true;
378 	}
379 
380 	// if the input is empty, clear the list (if it's for this input)
381 	if (!value && aclv._currentContext && element == aclv._currentContext.element) {
382 		aclv.reset(element);
383 		return true;
384 	}
385 
386 	// a Return following an address turns it into a bubble
387 	if (DwtKeyEvent.IS_RETURN[key] && aclv._complete(element)) {
388 		return false;
389 	}
390 
391 	// skip if input value is not changed
392 	if (!ev.inputChanged) {
393 		return true;
394 	}
395 
396 	ZmAutocompleteListView.clearTimer();
397 
398 	// regular input, schedule autocomplete
399 	var ev1 = new DwtKeyEvent();
400 	DwtKeyEvent.copy(ev1, ev);
401 	ev1.aclv = aclv;
402 	ev1.element = element;
403 	DBG.println("ac", "scheduling autocomplete for: " + elId);
404 
405 	var aif = DwtControl.ALL_BY_ID[element._aifId];
406 	if (aif && aif._editMode) {
407 		return false;
408 	}
409 	
410 	var acAction = new AjxTimedAction(aclv, aclv._autocompleteAction, [ev1]);
411 	aclv._acActionId[elId] = AjxTimedAction.scheduleAction(acAction, aclv._acInterval);
412 	
413 	return true;
414 };
415 
416 ZmAutocompleteListView.clearTimer =
417 function(ev){
418     if (ZmAutocompleteListView.timer){
419         AjxTimedAction.cancelAction(ZmAutocompleteListView.timer)
420     }
421 };
422 
423 /**
424  * Invokes or prevents the browser's default behavior (which is to echo the typed key).
425  * 
426  * @param {Boolean}	echo	if <code>true</code>, echo the key
427  * @param {Event}	ev	the UI event
428  * 
429  * @private
430  */
431 ZmAutocompleteListView._echoKey =
432 function(echo, ev) {
433 	DwtUiEvent.setBehaviour(ev, !echo, echo);
434 	return echo;
435 };
436 
437 /**
438  * Hides list if there is a click elsewhere.
439  * 
440  * @private
441  */
442 ZmAutocompleteListView._outsideMouseDownListener =
443 function(ev, context) {
444 
445 	var curList = context && context.obj;
446 	if (curList) {
447 		DBG.println("out", "outside listener, cur " + curList.toString() + ": " + curList._htmlElId);
448 		curList.show(false);
449 		curList.setWaiting(false);
450 	}
451 };
452 
453 /**
454  * Sets the active account.
455  * 
456  * @param	{ZmAccount}		account		the account
457  */
458 ZmAutocompleteListView.prototype.setActiveAccount =
459 function(account) {
460 	this._activeAccount = account;
461 };
462 
463 /**
464  * Adds autocompletion to the given field by setting key event handlers.
465  *
466  * @param {Element}	element			an HTML element
467  * @param {string}	addrInputId		ID of ZmAddressInputField (for addr bubbles)
468  * 
469  * @private
470  */
471 ZmAutocompleteListView.prototype.handle =
472 function(element, addrInputId) {
473 	
474 	var elId = element.id = element.id || Dwt.getNextId();
475 	DBG.println("ac", "HANDLE " + elId);
476 	// TODO: use el id instead of expando
477 	element._aclvId = this._htmlElId;
478 	if (addrInputId) {
479 		element._aifId = addrInputId;
480 	}
481 	this._contexts[elId] = {};
482 	this._acActionId[elId] = -1;
483 	Dwt.setHandler(element, DwtEvent.ONKEYDOWN, ZmAutocompleteListView.onKeyDown);
484 	Dwt.setHandler(element, DwtEvent.ONKEYPRESS, ZmAutocompleteListView.onKeyPress);
485 	Dwt.setHandler(element, DwtEvent.ONKEYUP, ZmAutocompleteListView.onKeyUp);
486 	if (AjxEnv.isFirefox){
487 		// don't override the element input handler directly, as DwtControl uses
488 		// that for changing style, etc.
489 		var control = DwtControl.findControl(element);
490 
491 		if (control && control.getInputElement && control.getInputElement() === element) {
492 			control.addListener(DwtEvent.ONBLUR, ZmAutocompleteListView.clearTimer);
493 		} else {
494 			Dwt.setHandler(element, DwtEvent.ONBLUR, ZmAutocompleteListView.clearTimer);
495 		}
496 	}
497 	this.isActive = true;
498 };
499 
500 ZmAutocompleteListView.prototype.unhandle =
501 function(element) {
502 	DBG.println("ac", "UNHANDLE " + element.id);
503 	Dwt.clearHandler(element, DwtEvent.ONKEYDOWN);
504 	Dwt.clearHandler(element, DwtEvent.ONKEYPRESS);
505 	Dwt.clearHandler(element, DwtEvent.ONKEYUP);
506 	this.isActive = false;
507 };
508 
509 // Kicks off an autocomplete cycle, which scans the content of the given input and then
510 // handles the strings it finds, possible making requests to the data provider.
511 ZmAutocompleteListView.prototype.autocomplete =
512 function(element) {
513 
514 	if (this._dataLoader && !this._dataLoaded) {
515 		this._data = this._dataLoader.call(this._dataClass);
516 		this._dataAPI = this._data;
517 		this._dataLoaded = true;
518 	}
519 
520 	var results = this._parseInput(element);
521 	this._process(results, element);
522 };
523 
524 /**
525  * See if the text in the input is an address. If it is, complete it.
526  * 
527  * @param {Element}		element
528  * @return {boolean}	true if the value in the input was completed
529  */
530 ZmAutocompleteListView.prototype._complete =
531 function(element) {
532 
533 	var value = element.value;
534 	if (this._dataAPI.isComplete && this._dataAPI.isComplete(value)) {
535 		DBG.println("ac", "got a Return or Tab, found an addr: " + value);
536 		var result = this._parseInput(element)[0];
537 		var context = {
538 			element:	element,
539 			str:		result.str,
540 			isAddress:	true,
541 			isComplete:	result.isComplete,
542 			key:		this._getKey(result)
543 		}
544 		this._update(context);
545 		this.reset(element);
546 		return true;
547 	}
548 	return false;
549 };
550 
551 // Parses the content of the given input by splitting the text at delimiters. Returns a list of
552 // objects with information about each string it found.
553 ZmAutocompleteListView.prototype._parseInput =
554 function(element) {
555 
556 	DBG.println("ac", "parse input for element: " + element.id); 
557 	var results = [];
558 	var text = element && element.value;
559 	if (!text) {
560 		return results;
561 	}
562 	DBG.println("ac", "PARSE: " + text);
563 	var str = "";
564 	for (var i = 0; i < text.length; i++) {
565 		var c = text.charAt(i);
566 		if (c == ' ' && !str) { continue; }	// ignore leading space
567 		var isDelim = this._isDelim[c];
568 		if (isDelim || c == ' ') {
569 			// space counts as delim if bubbles are on and the space follows an address
570 			var str1 = (this._dataAPI.isComplete && this._dataAPI.isComplete(str, true));
571 			if (str1) {
572 				DBG.println("ac", "parse input found address: " + str);
573 				str1 = (str1 === true) ? str : str1;
574 				results.push({element:element, str:str1, isComplete:true, isAddress:true});
575 				str = "";
576 			}
577 			else if (c == ";") {
578 				// semicolon triggers Quick Complete
579 				results.push({element:element, str:str, isComplete:true});
580 				str = "";
581 			}
582 			else {
583 				// space typed, but not after an address so no special meaning
584 				str += c;
585 			}
586 		}
587 		else {
588 			str += c;
589 		}
590 	}
591 	if (str) {
592 		results.push({str:str, isComplete:false});
593 	}
594 
595 	return results;
596 };
597 
598 /**
599  * Look through the parsed contents of the input and make any needed autocomplete requests. If there is a 
600  * delimited email address, go ahead and handle it now. Also, make sure to cancel any requests that no
601  * longer match the contents of the input. This function will run only after a pause in the user's typing
602  * (via a setTimeout call), so existing contexts will be in either the REQUEST state or the DONE state.
603  */
604 ZmAutocompleteListView.prototype._process =
605 function(results, element) {
606 
607 	// for convenience, create a hash of current keys for this input
608 	var resultsHash = {};
609 	for (var i = 0; i < results.length; i++) {
610 		var key = this._getKey(results[i]);
611 		resultsHash[key] = true;
612 	}
613 	
614 	// cancel any outstanding requests for strings that are no longer in the input
615 	var pendingContextHash = {};
616 	var oldContexts = this._contexts[element.id];
617 	if (oldContexts && oldContexts.length) {
618 		for (var i = 0; i < oldContexts.length; i++) {
619 			var context = oldContexts[i];
620 			var key = context.key;
621 			if (key && context.reqId && context.state == ZmAutocompleteListView.STATE_REQUEST && !resultsHash[key]) {
622 				DBG.println("ac", "request for '" + context.str + "' no longer current, canceling req " + context.reqId);
623 				appCtxt.getAppController().cancelRequest(context.reqId);
624 				context.state = ZmAutocompleteListView.STATE_DONE;
625 				if (context.str == this._waitingStr) {
626 					this.setWaiting(false);
627 				}
628 			}
629 			else if (context.state == ZmAutocompleteListView.STATE_REQUEST) {
630 				pendingContextHash[context.key] = context;
631 			}
632 		}
633 	}
634 	
635 	// process the parsed content
636 	var newContexts = [];
637 	for (var i = 0; i < results.length; i++) {
638 		var result = results[i];
639 		var str = result.str;
640 		var key = this._getKey(result);
641 		var pendingContext = pendingContextHash[key];
642 		// see if we already have a pending request for this result; if so, leave it alone
643 		if (pendingContext) {
644 			DBG.println("ac", "PROCESS: propagate pending context for '" + str + "'");
645 			newContexts.push(pendingContext);
646 		}
647 		else {
648 			// add a new context
649 			DBG.println("ac", "PROCESS: add new context for '" + str + "', isComplete: " + result.isComplete);
650 			var context = {
651 				element:	element,
652 				str:		str,
653 				isComplete:	result.isComplete,
654 				key:		key,
655 				isAddress:	result.isAddress,
656 				state:		ZmAutocompleteListView.STATE_NEW
657 			}
658 			newContexts.push(context);
659 			if (result.isAddress) {
660 				// handle a completed email address now
661 				this._update(context);
662 			}
663 			else {
664 				// go get autocomplete results from the data provider
665 				this._autocomplete(context);
666 			}
667 		}
668 	}
669 	this._contexts[element.id] = newContexts;
670 };
671 
672 // Returns a key that combines the string with whether it's subject to Quick Complete
673 ZmAutocompleteListView.prototype._getKey =
674 function(context) {
675 	return context.str + (context.isComplete ? this._separator : "");
676 };
677 
678 /**
679  * Resets the visible state of the autocomplete list. The state-related properties are not
680  * per-element because there can only be one visible autocomplete list.
681  */
682 ZmAutocompleteListView.prototype.reset =
683 function(element) {
684 
685 	DBG.println("ac", "RESET");
686 	this._matches = null;
687 	this._selected = null;
688 
689 	this._matchHash			= {};
690 	this._forgetLink		= {};
691 	this._expandLink		= {};
692 
693 	this.show(false);
694 	if (this._memberListView) {
695 		this._memberListView.show(false);
696 	}
697 	this.setWaiting(false);
698 	
699 	if (element) {
700 		this._removeDoneRequests(element);
701 	}
702 };
703 
704 /**
705  * Checks the given key to see if it's used to control the autocomplete list in some way.
706  * If it does, the action is taken and the key won't be echoed into the input area.
707  *
708  * The following keys are action keys:
709  *	38 40		up/down arrows (list selection)
710  *	37 39		left/right arrows (dl expansion)
711  *	27			escape (hide list)
712  *
713  * The following keys are delimiters (trigger completion when list is up):
714  *	3 13		return
715  *	9			tab
716  *	59 186		semicolon
717  *	188			comma (depends on user pref)
718  *
719  * @param {int}		key			a numeric key code
720  * @param {boolean}	isDelim		true if a single delimiter key was typed
721  * @param {Element}	element		element key event happened in 
722  * 
723  * @private
724  */
725 ZmAutocompleteListView.prototype.handleAction = function(key, isDelim, element) {
726 
727 	DBG.println("ac", "autocomplete handleAction for key " + key + " / " + isDelim);
728 
729 	if (isDelim) {
730 		this._update();
731 	}
732     else if (key === DwtKeyEvent.KEY_ARROW_RIGHT) {
733 		// right arrow
734 		var dwttext = this._expandText && this._expandText[this._selected];
735 
736 		// if the caret is at the end of the input, expand a distribution list,
737 		// if possible
738 		if(!dwttext || Dwt.getSelectionStart(element) !== element.value.length) {
739 			return false;
740 		}
741 
742 		// fake a click
743 		dwttext.notifyListeners(DwtEvent.ONMOUSEDOWN);
744 
745 	}
746     else if (key === DwtKeyEvent.KEY_ARROW_UP || key === DwtKeyEvent.KEY_ARROW_DOWN) {
747 		// handle up and down arrow keys
748 		if (this.size() < 1) {
749 			return;
750 		}
751 		if (key === DwtKeyEvent.KEY_ARROW_DOWN) {
752 			this._setSelected(ZmAutocompleteListView.NEXT);
753 		}
754         else if (key === DwtKeyEvent.KEY_ARROW_UP) {
755 			this._setSelected(ZmAutocompleteListView.PREV);
756 		}
757 	}
758     else if (key === DwtKeyEvent.KEY_ESCAPE) {
759 		if (this.getVisible()) {
760 			this.reset(element); // ESC hides the list
761 		}
762 		else if (!this._cancelPendingRequests(element)) {
763 			return false;
764 		}
765 	}
766     else if (key === DwtKeyEvent.KEY_TAB) {
767 		this._popdown();
768 		return false;
769 	}
770     else {
771 		return false;
772 	}
773 	return true;
774 };
775 
776 // Cancels the XHR of any context in the REQUEST state.
777 ZmAutocompleteListView.prototype._cancelPendingRequests =
778 function(element) {
779 
780 	var foundOne = false;
781 	var contexts = this._contexts[element.id];
782 	if (contexts && contexts.length) {
783 		for (var i = 0; i < contexts.length; i++) {
784 			var context = contexts[i];
785 			if (context.state == ZmAutocompleteListView.STATE_REQUEST) {
786 				DBG.println("ac", "user-initiated cancel of request for '" + context.str + "', " + context.reqId);
787 				appCtxt.getAppController().cancelRequest(context.reqId);
788 				context.state = ZmAutocompleteListView.STATE_DONE;
789 				foundOne = true;
790 			}
791 		}
792 	}
793 	this.setWaiting(false);
794 	
795 	return foundOne;
796 };
797 
798 // Clean up contexts we are done with
799 ZmAutocompleteListView.prototype._removeDoneRequests =
800 function(element) {
801 
802 	var contexts = this._contexts[element.id];
803 	var newContexts = [];
804 	if (contexts && contexts.length) {
805 		for (var i = 0; i < contexts.length; i++) {
806 			var context = contexts[i];
807 			if (context.state == ZmAutocompleteListView.STATE_DONE) {
808 				newContexts.push(context);
809 			}
810 		}
811 	}
812 	this._contexts[element.id] = newContexts;
813 };
814 
815 /**
816  * Sets the waiting status.
817  * 
818  * @param	{Boolean}	on		if <code>true</code>, turn waiting "on"
819  * @param	{string}	str		string that pending request is for
820  * 
821  */
822 ZmAutocompleteListView.prototype.setWaiting =
823 function(on, str) {
824 
825 	if (!on && !this._waitingDiv) {
826 		return;
827 	}
828 
829 	var div = this._waitingDiv;
830 	if (!div) {
831 		div = this._waitingDiv = document.createElement("div");
832 		div.className = "acWaiting";
833 		var html = [], idx = 0;
834 		html[idx++] = "<table role='presentation' cellpadding=0 cellspacing=0 border=0>";
835 		html[idx++] = "<tr>";
836 		html[idx++] = "<td><div class='ImgSpinner'></div></td>";
837 		html[idx++] = "<td>" + ZmMsg.autocompleteWaiting + "</td>";
838 		html[idx++] = "</tr>";
839 		html[idx++] = "</table>";
840 		div.innerHTML = html.join("");
841 		Dwt.setPosition(div, Dwt.ABSOLUTE_STYLE);
842 		appCtxt.getShell().getHtmlElement().appendChild(div);
843 	}
844 
845 	if (on) {
846 		this._popdown();
847 		var loc = this._getDefaultLoc();
848 		Dwt.setLocation(div, loc.x, loc.y);
849 
850 		this._setLiveRegionText(ZmMsg.autocompleteWaiting);
851 	}
852 	this._waitingStr = on ? str : "";
853 
854 	Dwt.setZIndex(div, on ? Dwt.Z_DIALOG_MENU : Dwt.Z_HIDDEN);
855 	Dwt.setVisible(div, on);
856 };
857 
858 // Private methods
859 
860 /**
861  * Called as a timed action, after a sufficient pause in typing within an address field.
862  * 
863  * @private
864  */
865 ZmAutocompleteListView.prototype._autocompleteAction =
866 function(ev) {
867 	var aclv = ev.aclv;
868 	aclv._acActionId[ev.element.id] = -1; // so we don't try to cancel
869 	aclv.autocomplete(ev.element);
870 };
871 
872 /**
873  * Displays the current matches in a popup list, selecting the first.
874  *
875  * @param {Boolean}	show	if <code>true</code>, display the list
876  * @param {String}	loc		where to display the list
877  * 
878  */
879 ZmAutocompleteListView.prototype.show =
880 function(show, loc) {
881 
882 	if (show) {
883 		this.setWaiting(false);
884 		this._popup(loc);
885 	} else {
886 		this._popdown();
887 	}
888 };
889 
890 // Makes an autocomplete request to the data provider.
891 ZmAutocompleteListView.prototype._autocomplete =
892 function(context) {
893 
894 	var str = AjxStringUtil.trim(context.str);
895 	if (!str || !(this._dataAPI && this._dataAPI.autocompleteMatch)) {
896 		return;
897 	}
898 	DBG.println("ac", "autocomplete: " + context.str);
899 	
900 	this._currentContext = context;	// so we can figure out where to pop up the "waiting" indicator
901 	var respCallback = this._handleResponseAutocomplete.bind(this, context);
902 	context.state = ZmAutocompleteListView.STATE_REQUEST;
903 	context.reqId = this._dataAPI.autocompleteMatch(str, respCallback, this, this._options, this._activeAccount, this._autocompleteType);
904 	DBG.println("ac", "Request ID for " + context.element.id + " / '" + context.str + "': " + context.reqId);
905 };
906 
907 ZmAutocompleteListView.prototype._handleResponseAutocomplete =
908 function(context, list) {
909 
910 	context.state = ZmAutocompleteListView.STATE_RESPONSE;
911 
912 	if (list && list.length) {
913 		DBG.println("ac", "matches found for '" + context.str + "': " + list.length);
914 		context.list = list;
915 		if (context.isComplete) {
916 			// doing Quick Complete, go ahead and update with the first match
917 			DBG.println("ac", "performing quick completion for: " + context.str);
918 			this._update(context, list[0]);
919 		} else {
920 			// pop up the list of matches
921 			this._set(list, context);
922 			this._currentContext = context;
923 			this.show(true);
924 		}
925 	} else if (!context.isComplete) {
926 		this._popdown();
927 		this._showNoResults();
928 
929 		var msg = AjxMessageFormat.format(ZmMsg.autocompleteMatches, 0);
930 		this._setLiveRegionText(msg);
931 	}
932 };
933 
934 // Returns the field in the match that we show the user.
935 ZmAutocompleteListView.prototype._getCompletionValue =
936 function(match) {
937 	var value = "";
938 	if (this._matchValue instanceof Array) {
939 		for (var i = 0, len = this._matchValue.length; i < len; i++) {
940 			if (match[this._matchValue[i]]) {
941 				value = match[this._matchValue[i]];
942 				break;
943 			}
944 		}
945 	} else {
946 		value = match[this._matchValue] || "";
947 	}
948 	return value;
949 };
950 
951 // Updates the content of the input with the given match and adds a bubble
952 ZmAutocompleteListView.prototype._update =
953 function(context, match) {
954 
955 	context = context || this._currentContext;
956 	if (!context) {
957 		return;
958 	}
959 	match = match || this._matchHash[this._selected];
960 	
961 	if (match && match.needDerefGroup) {
962 		var contact = new ZmContact(match.groupId, {});
963 		var continuationCb = new AjxCallback(this, this._updateContinuation, [context, match]);
964 		var derefCallback = new AjxCallback(match, match.setContactGroupMembers, [match.groupId, continuationCb]);
965 		contact.load(derefCallback, null, null, true);
966 	}
967 	else {
968 		this._updateContinuation(context, match);
969 	}
970 };
971 
972 // continuation of _update
973 ZmAutocompleteListView.prototype._updateContinuation = 
974 function(context, match) {
975 
976 	var newText = "";
977 	var address = context.address = context.address || (context.isAddress && context.str) || (match && this._getCompletionValue(match));
978 	DBG.println("ac", "UPDATE: result for '" + context.str + "' is " + AjxStringUtil.htmlEncode(address));
979 
980 	var bubbleAdded = this._addBubble(context, match, context.isComplete);
981 	if (!bubbleAdded) {
982 		newText = address + this._separator;
983 	}
984 
985 	// figure out what the content of the input should now be
986 	var el = context.element;
987 	if (el) {
988 		// context.add means don't change the content (used by DL selection)
989 		if (!context.add) {
990 			// Parse the input again so we know what to replace. There is a race condition here, since the user
991 			// may have altered the content during the request. In that case, the altered content will not match
992 			// and get replaced, which is fine. Reparsing the input seems like a better option than trying to use
993 			// regexes.
994 			var results = this._parseInput(el);
995 			var newValue = "";
996 			for (var i = 0; i < results.length; i++) {
997 				var result = results[i];
998 				var key = this._getKey(result);
999 				// Compare el.value to key too. Edge case: user types complete email and presses enter
1000 				// before new autocomplete request is sent. In this case context.key is only a part of key and el.value.
1001 				// Bug 86577
1002 				if (context.key === key || el.value === key) {
1003 					newValue += newText;
1004 				}
1005 				else {
1006 					newValue += key;
1007 				}
1008 			}
1009 			if (bubbleAdded) {
1010 				newValue = AjxStringUtil.trim(newValue);
1011 			}
1012 			if (el.value !== newValue) {
1013 				el.value = newValue;
1014 			}
1015 		}
1016 		
1017 		if (!context.isComplete) {
1018 			// match was selected from visible list, refocus the input and clear the list
1019 			el.focus();
1020 			this.reset(el);
1021 		}
1022 	}
1023 	context.state = ZmAutocompleteListView.STATE_DONE;
1024 
1025 	this._runCallbacks(ZmAutocompleteListView.CB_COMPLETION, el && el.id, [address, el, match]);
1026 };
1027 
1028 // Adds a bubble. If we are adding it via Quick Complete, we don't want the input field to set
1029 // focus since the user may have tabbed into another input field.
1030 ZmAutocompleteListView.prototype._addBubble =
1031 function(context, match, noFocus) {
1032 
1033 	var el = context.element;
1034 	var addrInput = el && el._aifId && DwtControl.ALL_BY_ID[el._aifId];
1035 	if (addrInput) {
1036 		var bubbleCount = addrInput.getBubbleCount();
1037 
1038 		if (match && match.multipleAddresses) {
1039 			// mass complete (add all) from a DL
1040 			addrInput.addValue(context.address);
1041 		}
1042 		else {
1043 			var addedClass = this._dataAPI && this._dataAPI.getAddedBubbleClass && this._dataAPI.getAddedBubbleClass(context.str);
1044 			var bubbleParams = {
1045 				address:	context.address,
1046 				match:		match,
1047 				noFocus:	noFocus,
1048 				addClass:	addedClass,
1049 				noParse:	this._options.noBubbleParse
1050 			}
1051 			addrInput.addBubble(bubbleParams);
1052 		}
1053 
1054 		var msg = AjxMessageFormat.format(ZmMsg.autocompleteAddressesAdded,
1055 		                                  addrInput.getBubbleCount() - bubbleCount);
1056 		this._setLiveRegionText(msg);
1057 
1058 		el = addrInput._input;
1059 		// Input field loses focus along the way. Restore it when the stack is finished
1060 		if (AjxEnv.isIE) {
1061 			AjxTimedAction.scheduleAction(new AjxTimedAction(addrInput, addrInput.focus), 0);
1062 		}
1063 		return true;
1064 	}
1065 	else {
1066 		return false;
1067 	}
1068 };
1069 
1070 // Listeners
1071 
1072 // MOUSE_DOWN selects a match and performs an update. Note that we don't wait for
1073 // a corresponding MOUSE_UP event.
1074 ZmAutocompleteListView.prototype._mouseDownListener = 
1075 function(ev) {
1076 	ev = DwtUiEvent.getEvent(ev);
1077 	var row = DwtUiEvent.getTargetWithProp(ev, "id");
1078 	if (!row || !row.id || row.id.indexOf("Row") === -1) {
1079 		return;
1080 	}
1081 	if (ev.button == DwtMouseEvent.LEFT) {
1082 		this._setSelected(row.id);
1083 		if (this.isListenerRegistered(DwtEvent.SELECTION)) {
1084 	    	var selEv = DwtShell.selectionEvent;
1085 	    	DwtUiEvent.copy(selEv, ev);
1086 	    	selEv.detail = 0;
1087 	    	this.notifyListeners(DwtEvent.SELECTION, selEv);
1088 	    	return true;
1089 	    }		
1090 	}
1091 };
1092 
1093 // Mouse over selects a match
1094 ZmAutocompleteListView.prototype._mouseOverListener = 
1095 function(ev) {
1096 	ev = DwtUiEvent.getEvent(ev);
1097 	var row = Dwt.findAncestor(DwtUiEvent.getTarget(ev), "id");
1098 	if (row) {
1099 		this._setSelected(row.id);
1100 	}
1101 	return true;
1102 };
1103 
1104 // Seems like DwtComposite should define this method
1105 ZmAutocompleteListView.prototype._addSelectionListener = 
1106 function(listener) {
1107 	this._eventMgr.addListener(DwtEvent.SELECTION, listener);
1108 };
1109 
1110 ZmAutocompleteListView.prototype._listSelectionListener = 
1111 function(ev) {
1112 	this._update();
1113 };
1114 
1115 // Layout
1116 
1117 // Lazily create main table, since we may need it to show "Waiting..." row before
1118 // a call to _set() is made.
1119 ZmAutocompleteListView.prototype._getTable =
1120 function() {
1121 
1122 	var table = this._tableId && document.getElementById(this._tableId);
1123 	if (!table) {
1124 		var html = [], idx = 0;
1125 		this._tableId = this.getHTMLElId() + '_table';
1126 		html[idx++] = "<table role='presentation' id='" + this._tableId + "' cellpadding=0 cellspacing=0 border=0>";
1127 		html[idx++] = "</table>";
1128 		this.getHtmlElement().innerHTML = html.join("");
1129 		table = document.getElementById(this._tableId);
1130 	}
1131 	return table;
1132 };
1133 
1134 ZmAutocompleteListView.prototype._setLiveRegionText =
1135 function(text) {
1136 	// Lazily create accessibility live region
1137 	var id = this.getHTMLElId() + '_liveRegion';
1138 	var liveRegion = Dwt.byId(id);
1139 
1140 	if (!liveRegion) {
1141 		liveRegion = document.createElement('div');
1142 		liveRegion.id = id;
1143 		liveRegion.className = 'ScreenReaderOnly';
1144 		liveRegion.setAttribute('role', 'alert');
1145 		liveRegion.setAttribute('aria-label', ZmMsg.autocomplete);
1146 		liveRegion.setAttribute('aria-live', 'assertive');
1147 		liveRegion.setAttribute('aria-relevant', 'additions');
1148 		liveRegion.setAttribute('aria-atomic', true);
1149 		appCtxt.getShell().getHtmlElement().appendChild(liveRegion);
1150 	}
1151 
1152 	// Set the live region text content
1153 	Dwt.removeChildren(liveRegion);
1154 	if (text) {
1155 		var paragraph = document.createElement('p');
1156 		paragraph.appendChild(document.createTextNode(text));
1157 		liveRegion.appendChild(paragraph);
1158 	}
1159 };
1160 
1161 // Creates the list and its member elements based on the matches we have. Each match becomes a
1162 // row. The first match is automatically selected.
1163 ZmAutocompleteListView.prototype._set =
1164 function(list, context) {
1165 
1166 	this._removeAll();
1167 	var table = this._getTable();
1168 	this._matches = list;
1169 	var forgetEnabled = (this._options.supportForget !== false);
1170 	var expandEnabled = (this._options.supportExpand !== false);
1171 	var len = this._matches.length;
1172 	for (var i = 0; i < len; i++) {
1173 		var match = this._matches[i];
1174 		if (match && (match.text || match.icon)) {
1175 			var rowId = match.id = this._getId("Row", i);
1176 			this._matchHash[rowId] = match;
1177 			var row = table.insertRow(-1);
1178 			row.className = this._origClass;
1179 			row.id = rowId;
1180 			row.index = i;
1181 			var html = [], idx = 0;
1182 			var cell = row.insertCell(-1);
1183 			cell.className = "AutocompleteMatchIcon";
1184 			if (match.icon) {
1185 				cell.innerHTML = (match.icon.indexOf('Dwt') !== -1) ? ["<div class='", match.icon, "'></div>"].join("") :
1186 								 									 AjxImg.getImageHtml(match.icon);
1187 			} else {
1188 				cell.innerHTML = " ";
1189 			}
1190 			cell = row.insertCell(-1);
1191 			cell.innerHTML = match.text || " ";
1192 			if (forgetEnabled) {
1193 				this._insertLinkCell(this._forgetLink, row, rowId, this._getId("Forget", i), (match.score > 0));
1194 			}
1195 			if (expandEnabled) {
1196 				this._insertLinkCell(this._expandLink, row, rowId, this._getId("Expand", i), match.canExpand);
1197 			}
1198 		}
1199 	}
1200 	if (forgetEnabled) {
1201 		this._forgetText = {};
1202 		this._addLinks(this._forgetText, "Forget", ZmMsg.forget, ZmMsg.forgetTooltip, this._handleForgetLink, context);
1203 	}
1204 	if (expandEnabled) {
1205 		this._expandText = {};
1206 		this._addLinks(this._expandText, "Expand", ZmMsg.expand, ZmMsg.expandTooltip, this.expandDL, context);
1207 	}
1208 
1209 	var msg = AjxMessageFormat.format(ZmMsg.autocompleteMatches, len);
1210 	this._setLiveRegionText(msg);
1211 
1212 	AjxTimedAction.scheduleAction(new AjxTimedAction(this,
1213 		function() {
1214 			this._setSelected(this._getId("Row", 0));
1215 		}), 100);
1216 };
1217 
1218 ZmAutocompleteListView.prototype._showNoResults =
1219 function() {
1220 	// do nothing. Overload to show something.
1221 };
1222 
1223 ZmAutocompleteListView.prototype._insertLinkCell =
1224 function(hash, row, rowId, linkId, addLink) {
1225 	hash[rowId] = addLink ? linkId : null;
1226 	var cell = row.insertCell(-1);
1227 	cell.className = "Link";
1228 	cell.innerHTML = addLink ? "<a id='" + linkId + "'></a>" : "";
1229 };
1230 
1231 ZmAutocompleteListView.prototype._getId =
1232 function(type, num) {
1233 	return [this._htmlElId, "ac" + type, num].join("_");
1234 };
1235 
1236 // Add a DwtText to the link so it can have a tooltip.
1237 ZmAutocompleteListView.prototype._addLinks =
1238 function(textHash, idLabel, label, tooltip, handler, context) {
1239 
1240 	var len = this._matches.length;
1241 	for (var i = 0; i < len; i++) {
1242 		var match = this._matches[i];
1243 		var rowId = match.id = this._getId("Row", i);
1244 		var linkId = this._getId(idLabel, i);
1245 		var link = document.getElementById(linkId);
1246 		if (link) {
1247 			var textId = this._getId(idLabel + "Text", i);
1248 			var text = new DwtText({parent:this, className:this._hideLinkTextClass, id:textId});
1249 			textHash[rowId] = text;
1250 			text.isLinkText = true;
1251 			text.setText(label);
1252 			text.setToolTipContent(tooltip);
1253 			var listener = handler.bind(this, {email:match.email, textId:textId, rowId:rowId, element:context.element});
1254 			text.addListener(DwtEvent.ONMOUSEDOWN, listener);
1255 			text.reparentHtmlElement(link);
1256 		}
1257 	}
1258 };
1259 
1260 ZmAutocompleteListView.prototype._showLink =
1261 function(hash, textHash, rowId, show) {
1262 	var text = textHash && textHash[rowId];
1263 	if (text) {
1264 		text.setClassName(!show ? this._hideLinkTextClass :
1265 			hash[rowId] ? this._showLinkTextClass : this._hideSelLinkTextClass);
1266 	}
1267 };
1268 
1269 // Displays the list
1270 ZmAutocompleteListView.prototype._popup =
1271 function(loc) {
1272 
1273 	if (this.getVisible()) {
1274 		return;
1275 	}
1276 
1277 	loc = loc || this._getDefaultLoc();
1278 	var x = loc.x;
1279 	var y = loc.y;
1280 
1281 	var windowSize = this.shell.getSize();
1282 	var availHeight = windowSize.y - y;
1283 	var fullHeight = this.size() * this._getRowHeight();
1284 	this.setLocation(Dwt.LOC_NOWHERE, Dwt.LOC_NOWHERE);
1285 	this.setVisible(true);
1286 	var curSize = this.getSize();
1287 	if (availHeight < fullHeight) {
1288 	  //we are short add text to alert user to keep typing
1289       this._showMoreResultsText(availHeight);
1290       // if we don't fit, resize so we are scrollable
1291       this.setSize(Dwt.DEFAULT, availHeight - (AjxEnv.isIE ? 30 : 10));
1292 	   // see if we need to account for width of vertical scrollbar
1293 	  var div = this.getHtmlElement();
1294 	  if (div.clientWidth != div.scrollWidth) {
1295 			this.setSize(curSize.x + Dwt.SCROLLBAR_WIDTH, Dwt.DEFAULT);
1296 	  }
1297       
1298 	} else if (curSize.y < fullHeight) {
1299 		this.setSize(Dwt.CLEAR, fullHeight);
1300 	} else {
1301 		this.setSize(Dwt.CLEAR, Dwt.CLEAR);	// set back to auto-sizing
1302 	}
1303 
1304 	var newX = (x + curSize.x >= windowSize.x) ? windowSize.x - curSize.x : x;
1305 
1306 	DBG.println("ac", this.toString() + " popup at: " + newX + "," + y);
1307     this.setLocation(newX, y);
1308 	this.setVisible(true);
1309 	this.setZIndex(Dwt.Z_DIALOG_MENU);
1310 
1311 	var omem = appCtxt.getOutsideMouseEventMgr();
1312 	var omemParams = {
1313 		id:					"ZmAutocompleteListView",
1314 		obj:				this,
1315 		outsideListener:	this._outsideListener,
1316 		noWindowBlur:		appCtxt.get(ZmSetting.IS_DEV_SERVER)
1317 	}
1318 	omem.startListening(omemParams);
1319 };
1320 
1321 // returns a point with a location just below the input field
1322 ZmAutocompleteListView.prototype._getDefaultLoc = 
1323 function() {
1324 
1325 	if (this._locationCallback) {
1326 		return this._locationCallback();
1327 	}
1328 	
1329 	var el = this._currentContext && this._currentContext.element;
1330 	if (!el) {
1331 		return {};
1332 	}
1333 	
1334 	var elLoc = Dwt.getLocation(el);
1335 	var elSize = Dwt.getSize(el);
1336 	var x = elLoc.x;
1337 	var y = elLoc.y + elSize.y + 3;
1338 	DwtPoint.tmp.set(x, y);
1339 	return DwtPoint.tmp;
1340 };
1341 
1342 // Hides the list
1343 ZmAutocompleteListView.prototype._popdown = 
1344 function() {
1345 
1346 	if (!this.getVisible()) {
1347 		return;
1348 	}
1349 	DBG.println("out", "popdown " + this.toString() + ": " + this._htmlElId);
1350 
1351 	if (this._memberListView) {
1352 		this._memberListView._popdown();
1353 	}
1354 	
1355 	this.setZIndex(Dwt.Z_HIDDEN);
1356 	this.setVisible(false);
1357 	this._removeAll();
1358 	this._selected = null;
1359 
1360 	var omem = appCtxt.getOutsideMouseEventMgr();
1361 	omem.stopListening({id:"ZmAutocompleteListView", obj:this});
1362 };
1363 
1364 /*
1365     Display message to user that more results are available than fit in the current display
1366     @param {int}    availHeight available height of display
1367  */
1368 ZmAutocompleteListView.prototype._showMoreResultsText =
1369 function (availHeight){
1370     //over load for implementation
1371 };
1372 
1373 /**
1374  * Selects a match by changing its CSS class.
1375  *
1376  * @param	{string}	id		ID of row to select, or NEXT / PREV
1377  */
1378 ZmAutocompleteListView.prototype._setSelected =
1379 function(id) {
1380 
1381 	DBG.println("ac", "setting selected id to " + id);
1382 	var table = document.getElementById(this._tableId);
1383 	var rows = table && table.rows;
1384 	if (!(rows && rows.length)) {
1385 		return;
1386 	}
1387 
1388 	var len = rows.length;
1389 
1390 	// handle selection of next/prev via arrow keys
1391 	if (id == ZmAutocompleteListView.NEXT || id == ZmAutocompleteListView.PREV) {
1392 		id = this._getRowId(rows, id, len);
1393 		if (!id) {
1394 			return;
1395 		}
1396 	}
1397 
1398 	// make sure the ID matches one of our rows
1399 	var found = false;
1400 	for (var i = 0; i < len; i++) {
1401 		if (rows[i].id == id) {
1402 			found = true;
1403 			break;
1404 		}
1405 	}
1406 	if (!found) {
1407 		return;
1408 	}
1409 	
1410 	// select one row, deselect the rest
1411 	for (var i = 0; i < len; i++) {
1412 		var row = rows[i];
1413 		var curStyle = row.className;
1414 		if (row.id == id) {
1415 			row.className = this._selClass;
1416 		} else if (curStyle != this._origClass) {
1417 			row.className = this._origClass;
1418 		}
1419 	}
1420 
1421 	// links only shown for selected row
1422 	this._showLink(this._forgetLink, this._forgetText, this._selected, false);
1423 	this._showLink(this._forgetLink, this._forgetText, id, true);
1424 
1425 	this._showLink(this._expandLink, this._expandText, this._selected, false);
1426 	this._showLink(this._expandLink, this._expandText, id, true);
1427 
1428 	this._selected = id;
1429 
1430 	var match = this._matchHash[id];
1431 	var msg;
1432 
1433 	if (!match) {
1434 		msg = AjxStringUtil.convertHtml2Text(Dwt.byId(this._selected));
1435 	} else {
1436 		var msg = AjxMessageFormat.format(ZmMsg.autocompleteMatchText, [ match.name, match.email ]);
1437 		if (match.isGroup) {
1438 			msg = AjxMessageFormat.format(ZmMsg.autocompleteGroup, msg);
1439 		}
1440 		else if (match.isDL) {
1441 			msg = AjxMessageFormat.format(ZmMsg.autocompleteDL, msg);
1442 		}
1443 	}
1444 
1445 	this._setLiveRegionText(msg);
1446 };
1447 
1448 ZmAutocompleteListView.prototype._getRowId =
1449 function(rows, id, len) {
1450 
1451 	if (len < 1) {
1452 		return;
1453 	}
1454 
1455 	var idx = -1;
1456 	for (var i = 0; i < len; i++) {
1457 		if (rows[i].id == this._selected) {
1458 			idx = i;
1459 			break;
1460 		}
1461 	}
1462 	var newIdx = (id == ZmAutocompleteListView.PREV) ? idx - 1 : idx + 1;
1463 	if (newIdx == -1) {
1464 		newIdx = len - 1;
1465 	}
1466 	if (newIdx == len) {
1467 		newIdx = 0;
1468 	}
1469 	
1470 	if (newIdx >= 0 && newIdx < len) {
1471 		Dwt.scrollIntoView(rows[newIdx], this.getHtmlElement());
1472 		return rows[newIdx].id;
1473 	}
1474 	return null;
1475 };
1476 
1477 ZmAutocompleteListView.prototype._getRowHeight =
1478 function() {
1479 	if (!this._rowHeight) {
1480 		if (!this.getVisible()) {
1481 			this.setLocation(Dwt.LOC_NOWHERE, Dwt.LOC_NOWHERE);
1482 			this.setVisible(true);
1483 		}
1484 		var row = this._getTable().rows[0];
1485 		this._rowHeight = row && Dwt.getSize(row).y;
1486 	}
1487 	return this._rowHeight || 18;
1488 };
1489 
1490 
1491 
1492 // Miscellaneous
1493 
1494 // Clears the internal list of matches
1495 ZmAutocompleteListView.prototype._removeAll =
1496 function() {
1497 	this._matches = null;
1498 	var table = this._getTable();
1499 	for (var i = table.rows.length - 1; i >= 0; i--) {
1500 		var row = table.rows[i];
1501 		if (row != this._waitingRow) {
1502 			table.deleteRow(i);
1503 		}
1504 	}
1505 	this._removeLinks(this._forgetText);
1506 	this._removeLinks(this._expandText);
1507 };
1508 
1509 ZmAutocompleteListView.prototype._removeLinks =
1510 function(textHash) {
1511 	if (!textHash) {
1512 		return;
1513 	}
1514 	for (var id in textHash) {
1515 		var textCtrl = textHash[id];
1516 		if (textCtrl) {
1517 			textCtrl.dispose();
1518 		}
1519 	}
1520 };
1521 
1522 // Returns the number of matches
1523 ZmAutocompleteListView.prototype.size =
1524 function() {
1525 	return this._getTable().rows.length;
1526 };
1527 
1528 // Force focus to the input element (handle Tab in Firefox)
1529 ZmAutocompleteListView.prototype._autocompleteFocus =
1530 function(htmlEl) {
1531 	htmlEl.focus();
1532 };
1533 
1534 ZmAutocompleteListView.prototype._getAcListLoc =
1535 function(ev) {
1536 	var element = ev.element;
1537 	var loc = Dwt.getLocation(element);
1538 	var height = Dwt.getSize(element).y;
1539 	return (new DwtPoint(loc.x, loc.y + height));
1540 };
1541 
1542 ZmAutocompleteListView.prototype._settingChangeListener =
1543 function(ev) {
1544 	if (ev.type != ZmEvent.S_SETTING) {
1545 		return;
1546 	}
1547 	if (ev.source.id == ZmSetting.AUTOCOMPLETE_ON_COMMA) {
1548 		this._isDelim[','] = this._isDelimCode[188] = appCtxt.get(ZmSetting.AUTOCOMPLETE_ON_COMMA);
1549 	}
1550 };
1551 
1552 ZmAutocompleteListView.prototype._handleForgetLink =
1553 function(params) {
1554 	if (this._dataAPI.forget) {
1555 		this._dataAPI.forget(params.email, this._handleResponseForget.bind(this, params.email, params.rowId));
1556 	}
1557 };
1558 
1559 ZmAutocompleteListView.prototype._handleResponseForget =
1560 function(email, rowId) {
1561 	var row = document.getElementById(rowId);
1562 	if (row) {
1563 		row.parentNode.removeChild(row);
1564 		var msg = AjxMessageFormat.format(ZmMsg.forgetSummary, [email]);
1565 		appCtxt.setStatusMsg(msg);
1566 	}
1567 	appCtxt.clearAutocompleteCache(ZmAutocomplete.AC_TYPE_CONTACT);
1568 };
1569 
1570 /**
1571  * Displays a second popup list with the members of the given distribution list.
1572  *
1573  * @param {hash}			params				hash of params:
1574  * @param {string}			params.email		address of a distribution list
1575  * @param {string}			params.textId		ID of link text
1576  * @param {string}			params.rowId		ID or list view row
1577  * @param {DwtMouseEvent}	params.ev			mouse event
1578  * @param {DwtPoint}		params.loc			location to popup at; default is right of parent ACLV
1579  * @param {Element}			params.element		input element
1580  */
1581 ZmAutocompleteListView.prototype.expandDL =
1582 function(params) {
1583 
1584 	if (!this._dataAPI.expandDL) {
1585 		return;
1586 	}
1587 
1588 	var mlv = this._memberListView;
1589 	if (mlv && mlv.getVisible() && params.textId && this._curExpanded == params.textId) {
1590 		// User has clicked "Collapse" link
1591 		mlv.show(false);
1592 		this._curExpanded = null;
1593 		this._setExpandText(params.textId, false);
1594 	} else {
1595 		// User has clicked "Expand" link
1596 		if (mlv && mlv.getVisible()) {
1597 			// expanding a DL while another one is showing
1598 			this._setExpandText(this._curExpanded, false);
1599 			mlv.show(false);
1600 		}
1601 		var contactsApp = appCtxt.getApp(ZmApp.CONTACTS);
1602 		var contact = contactsApp.getContactByEmail(params.email);
1603 		if (!contact) {
1604 			contact = new ZmContact(null);
1605 			contact.initFromEmail(params.email);	// don't cache, since it's not a real contact (no ID)
1606 		}
1607 		contact.isDL = true;
1608 		if (params.textId && params.rowId) {
1609 			this._curExpanded = params.textId;
1610 			this._setExpandText(params.textId, true);
1611 		}
1612 		this._dataAPI.expandDL(contact, 0, this._handleResponseExpandDL.bind(this, contact, params));
1613 	}
1614 
1615 };
1616 
1617 ZmAutocompleteListView.prototype._handleResponseExpandDL =
1618 function(contact, params, matches) {
1619 
1620 	var mlv = this._memberListView;
1621 	if (!mlv) {
1622 		mlv = this._memberListView = new ZmDLAutocompleteListView({parent:appCtxt.getShell(), parentAclv:this,
1623                                                                    selectionCallback: this._selectionCallback,
1624                                                                    expandTextId: params.textId});
1625 	}
1626 	mlv._dlContact = contact;
1627 	mlv._dlBubbleId = params.textId;
1628 	mlv._set(matches, contact);
1629 
1630 	// default position is just to right of parent ac list
1631 	var loc = params.loc;
1632 	if (this.getVisible()) {
1633 		loc = this.getLocation();
1634 		loc.x += this.getSize().x;
1635 	}
1636 
1637 	mlv.show(true, loc);
1638 	if (!mlv._rowHeight) {
1639 		var table = document.getElementById(mlv._tableId);
1640 		if (table) {
1641 			mlv._rowHeight = Dwt.getSize(table.rows[0]).y;
1642 		}
1643 	}
1644 };
1645 
1646 ZmAutocompleteListView.prototype._setExpandText =
1647 function(textId, expanded) {
1648 	var textCtrl = DwtControl.fromElementId(textId);
1649 	if (textCtrl && textCtrl.setText) {
1650 		textCtrl.setText(expanded ? ZmMsg.collapse : ZmMsg.expand);
1651 	}
1652 };
1653 
1654 ZmAutocompleteListView.prototype._setCallbacks =
1655 function(type, params) {
1656 
1657 	var cbKey = type + "Callback";
1658 	var list = this._callbacks[type] = [];
1659 	if (params[cbKey]) {
1660 		list.push({callback:params[cbKey]});
1661 	}
1662 };
1663 
1664 /**
1665  * Adds a callback of the given type. In an input ID is provided, then the callback
1666  * will only be run if the event happened in that input.
1667  *
1668  * @param {constant}				type		autocomplete callback type (ZmAutocompleteListView.CB_*)
1669  * @param {AjxCallback|function}	callback	callback to add
1670  * @param {string}					inputId		DOM ID of an input element (optional)
1671  */
1672 ZmAutocompleteListView.prototype.addCallback =
1673 function(type, callback, inputId) {
1674 	this._callbacks[type].push({callback:callback, inputId:inputId});
1675 };
1676 
1677 ZmAutocompleteListView.prototype._runCallbacks =
1678 function(type, inputId, args) {
1679 
1680 	var result = null;
1681 	var list = this._callbacks[type];
1682 	if (list && list.length) {
1683 		for (var i = 0; i < list.length; i++) {
1684 			var cbObj = list[i];
1685 			if (inputId && cbObj.inputId && (inputId != cbObj.inputId)) { continue; }
1686 			var callback = cbObj.callback;
1687 			var r;
1688 			if (typeof(callback) == "function") {
1689 				r = callback.apply(callback, args);
1690 			}
1691 			else if (callback && callback.isAjxCallback) {
1692 				r = AjxCallback.prototype.run.apply(cbObj.callback, args);
1693 			}
1694 			if (r === true || r === false) {
1695 				result = (result == null) ? r : result && r;
1696 			}
1697 		}
1698 	}
1699 	return result;
1700 };
1701