1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 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) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * Creates a control that allows the user to select items from a list, and
 26  * places the selected items in another list.
 27  * @constructor
 28  * @class
 29  * This class creates and manages a control that lets the user
 30  * select items from a list. Two lists are maintained, one with items to select 
 31  * from, and one that contains the selected items. Between them are buttons 
 32  * to shuffle items back and forth between the two lists.
 33  * <p>
 34  * There are two types of buttons: one or more transfer buttons move items from
 35  * the source list to the target list, and the remove button moves items from the
 36  * target list to the source list. The client can specify its transfer buttons.
 37  * If no specification is given, there will be a single transfer button called 
 38  * "Add".</p>
 39  * <p>
 40  * The parent must implement search(columnItem, ascending) if column sorting
 41  * is supported. It should also create a subclass of {@link DwtChooser} which returns
 42  * the appropriate source and target list views, themselves subclasses of
 43  * {@link DwtChooserListView}. Those subclasses must implement _getHeaderList() and
 44  * _createItemHtml(item).</p>
 45  * <p>
 46  * There are two different layout styles, horizontal (with the list views at the
 47  * left and right) and vertical (with the list views at the top and bottom). There
 48  * are two different selection styles, single and multiple, which control how many
 49  * items may appear in the target list view. There are two different transfer modes:
 50  * one where items are copied between lists, and one where they're moved.</p>
 51  *
 52  * @author Conrad Damon
 53  *
 54  * @param	{hash}		params		a hash of parameters
 55  * @param {DwtComposite}	params.parent			the containing widget
 56  * @param {string}	params.className		the CSS class
 57  * @param {string}	params.slvClassName	the CSS class for source list view
 58  * @param {string}	params.tlvClassName	the CSS class for target list view
 59  * @param {array}	params.buttonInfo		the id/label pairs for transfer buttons
 60  * @param {DwtChooser.HORIZ_STYLE|DwtChooser.VERT_STYLE}	params.layoutStyle	the layout style (vertical or horizontal)
 61  * @param {DwtChooser.SINGLE_SELECT|DwtChooser.MULTI_SELECT}	params.selectStyle	the multi-select (default) or single-select
 62  * @param {constant}	params.mode			the items are moved or copied
 63  * @param {boolean}	params.noDuplicates	if <code>true</code>, prevent duplicates in target list
 64  * @param {number}	params.singleHeight	the height of list view for single select style
 65  * @param {number}	params.listSize		the list width (if {@link DwtChooser.HORIZ_STYLE}) or height (if {@link DwtChooser.VERT_STYLE})
 66  * @param {boolean}	params.sourceEmptyOk	if <code>true</code>, do not show "No Results" in source list view
 67  * @param {boolean}	params.allButtons		if <code>true</code>, offer "Add All" and "Remove All" buttons
 68  * @param {boolean}	params.hasTextField	if <code>true</code>, create a text field for user input
 69  * 
 70  * @extends		DwtComposite
 71  */
 72 DwtChooser = function(params) {
 73 
 74 	if (arguments.length == 0) return;
 75 	DwtComposite.call(this, params.parent, params.className);
 76 
 77 	this._slvClassName = params.slvClassName;
 78 	this._tlvClassName = params.tlvClassName;
 79 	this._layoutStyle = params.layoutStyle ? params.layoutStyle : DwtChooser.HORIZ_STYLE;
 80 	this._selectStyle = params.selectStyle ? params.selectStyle : DwtChooser.MULTI_SELECT;
 81 	this._mode = params.listStyle ? params.listStyle : DwtChooser.MODE_MOVE;
 82 	this._noDuplicates = (params.noDuplicates !== false);
 83 	this._singleHeight = params.singleHeight ? params.singleHeight : 45; // 45 = header row + row with icon
 84 	this._listSize = params.listSize;
 85 	this._sourceEmptyOk = params.sourceEmptyOk;
 86 	this._allButtons = params.allButtons;
 87 	this._hasTextField = params.hasTextField;
 88 
 89 	this._handleButtonInfo(params.buttonInfo);
 90 	this._mode = params.mode ? params.mode :
 91 						this._hasMultiButtons ? DwtChooser.MODE_COPY : DwtChooser.MODE_MOVE;
 92 
 93 	this._createHtml();
 94 	this._initialize();
 95 	
 96 	var parentSz = params.parent.getSize();
 97 	var listWidth = params.listWidth || parentSz.x;
 98 	var listHeight = params.listHeight || parentSz.y;
 99 	if (listWidth && listHeight) {
100 		this.resize(listWidth, listHeight);
101 	}
102 
103 };
104 
105 DwtChooser.prototype = new DwtComposite;
106 DwtChooser.prototype.constructor = DwtChooser;
107 DwtChooser.prototype.isFocusable = true;
108 DwtChooser.prototype.role = 'listbox';
109 
110 // Consts
111 
112 // layout style
113 /**
114  * Defines a "horizontal" layout style.
115  */
116 DwtChooser.HORIZ_STYLE	= 1;
117 /**
118  * Defines a "vertical" layout style.
119  */
120 DwtChooser.VERT_STYLE	= 2;
121 
122 // number of items target list can hold
123 /**
124  * Defines a "single" select.
125  */
126 DwtChooser.SINGLE_SELECT	= 1;
127 /**
128  * Defines a "multi" select.
129  */
130 DwtChooser.MULTI_SELECT		= 2;
131 
132 // what happens to source items during transfer
133 DwtChooser.MODE_COPY	= 1;
134 DwtChooser.MODE_MOVE	= 2;
135 
136 DwtChooser.REMOVE_BTN_ID = "__remove__";
137 DwtChooser.ADD_ALL_BTN_ID = "__addAll__";
138 DwtChooser.REMOVE_ALL_BTN_ID = "__removeAll__";
139 
140 DwtChooser.prototype.toString = 
141 function() {
142 	return "DwtChooser";
143 };
144 
145 /**
146  * Sets the given list view with the given list. Defaults to source view.
147  *
148  * @param {AjxVector|Array|Object|Hash}	items			a list of items or hash of lists
149  * @param {DwtChooserListView.SOURCE|DwtChooserListView.TARGET}	view			the view to set
150  * @param {boolean}	clearOtherView	if <code>true</code>, clear out other view
151  */
152 DwtChooser.prototype.setItems =
153 function(items, view, clearOtherView) {
154 	view = view ? view : DwtChooserListView.SOURCE;
155 	this._reset(view);
156 	this.addItems(items, view, true);
157 	this._selectFirst(view);
158 	if (clearOtherView) {
159 		this._reset((view == DwtChooserListView.SOURCE) ? DwtChooserListView.TARGET : DwtChooserListView.SOURCE);
160 	}
161 };	
162 	
163 /**
164  * Gets a copy of the items in the given list. If that's the target list and 
165  * there are multiple transfer buttons, then a hash with a vector for each one
166  * is returned. Otherwise, a single vector is returned. Defaults to target view.
167  *
168  * @param {DwtChooserListView.SOURCE|DwtChooserListView.TARGET}	view			the view to set
169  * @return	{AjxVector|array|Object|hash}		the item(s)
170  */
171 DwtChooser.prototype.getItems =
172 function(view) {
173 	view = view ? view : DwtChooserListView.TARGET;
174 	if (view == DwtChooserListView.SOURCE) {
175 		return this.sourceListView.getList().clone();
176 	} else {	
177 		if (this._hasMultiButtons) {
178 			var data = {};
179 			for (var i in this._data) {
180 				data[i] = this._data[i].clone();
181 			}
182 			return data;
183 		} else {
184 			return this._data[this._buttonInfo[0].id].clone();
185 		}
186 	}
187 };
188 
189 /**
190  * Adds items to the given list view.
191  *
192  * @param {AjxVector|array|Object|hash}	items			a list of items or hash of lists
193  * @param {DwtChooserListView.SOURCE|DwtChooserListView.TARGET}	view			the view to set
194  * @param {boolean}	skipNotify	if <code>true</code>, do not notify listeners
195  * @param {string}	id			the button ID
196  */
197 DwtChooser.prototype.addItems =
198 function(items, view, skipNotify, id) {
199 	view = view ? view : DwtChooserListView.SOURCE;
200 	var list = (items instanceof AjxVector) ? items.getArray() : (items instanceof Array) ? items : [items];
201 	if (view == DwtChooserListView.SOURCE) {
202 		for (var i = 0; i < list.length; i++) {
203 			this._addToSource(list[i], null, skipNotify);
204 		}
205 	} else {
206 		var data;
207 		if (this._selectStyle == DwtChooser.SINGLE_SELECT) {
208 			this.targetListView._resetList();
209 			list = (list.length > 0) ? [list[0]] : list;
210 		}
211 		for (var i = 0; i < list.length; i++) {
212 			this._addToTarget(list[i], id, skipNotify);
213 //			if (this._selectStyle == DwtChooser.SINGLE_SELECT) {
214 //				return;
215 //			}
216 		}
217 	}
218 	if (view == DwtChooserListView.SOURCE) {
219 		var list = this.sourceListView.getList();
220 		this._sourceSize = list ? list.size() : 0;
221 	}
222 };
223 
224 /**
225  * Removes items from the given list view.
226  *
227  * @param {AjxVector|array|Object|hash}	list			a list of items or hash of lists
228  * @param {DwtChooserListView.SOURCE|DwtChooserListView.TARGET}	view			the view to set
229  * @param {boolean}	skipNotify	if <code>true</code>, do not notify listeners
230  */
231 DwtChooser.prototype.removeItems =
232 function(list, view, skipNotify) {
233 	list = (list instanceof AjxVector) ? list.getArray() : (list instanceof Array) ? list : [list];
234 	for (var i = 0; i < list.length; i++) {
235 		(view == DwtChooserListView.SOURCE) ? this._removeFromSource(list[i], skipNotify) : this._removeFromTarget(list[i], skipNotify);
236 	}
237 };
238 
239 /**
240  * Moves or copies items from the source list to the target list, paying attention
241  * to current mode.
242  *
243  * @param {AjxVector|array|Object|hash}	list			a list of items or hash of lists
244  * @param {string}	id			the ID of the transfer button that was used
245  * @param {boolean}	skipNotify	if <code>true</code>, do not notify listeners
246  */
247 DwtChooser.prototype.transfer =
248 function(list, id, skipNotify) {
249 	id = id ? id : this._activeButtonId;
250 	this._setActiveButton(id);
251 	if (this._mode == DwtChooser.MODE_MOVE) {
252 		if (this._selectStyle == DwtChooser.SINGLE_SELECT) {
253 			var tlist = this.targetListView.getList();
254 			if (tlist && tlist.size()) {
255 				this.remove(tlist, true);
256 			}
257 		}
258 		this.removeItems(list, DwtChooserListView.SOURCE, true);
259 	}
260 	this.addItems(list, DwtChooserListView.TARGET, skipNotify);
261 	this.sourceListView.deselectAll();
262 };
263 
264 /**
265  * Removes items from target list, paying attention to current mode. Also handles button state.
266  *
267  * @param {AjxVector|array|Object|hash}	list			a list of items or hash of lists
268  * @param {boolean}	skipNotify	if <code>true</code>, do not notify listeners
269  */
270 DwtChooser.prototype.remove =
271 function(list, skipNotify) {
272 	list = (list instanceof AjxVector) ? list.getArray() : (list instanceof Array) ? list : [list];
273 	if (this._mode == DwtChooser.MODE_MOVE) {
274 		for (var i = 0; i < list.length; i++) {
275 			var index = this._getInsertionIndex(this.sourceListView, list[i]);
276 			this.sourceListView.addItem(list[i], index, true);
277 		}
278 		this._sourceSize = list ? list.length : 0;
279 	}
280 	this.removeItems(list, DwtChooserListView.TARGET);
281 };
282 
283 /**
284  * Sets the select style to the given style. Performs a resize
285  * in order to adjust the layout, and changes the label on the transfer button if it's
286  * the default one.
287  *
288  * @param {DwtChooser.SINGLE_SELECT|DwtChooser.MULTI_SELECT}	style		the style single or multiple select
289  * @param {boolean}	noResize	if <code>true</code>, do not perform resize
290  */
291 DwtChooser.prototype.setSelectStyle =
292 function(style, noResize) {
293 	if (style == this._selectStyle) return;
294 	
295 	this._selectStyle = style;
296 	if (this._defLabel) {
297 		var button = this._button[this._buttonInfo[0].id];
298 		button.setText((style == DwtChooser.SINGLE_SELECT) ? AjxMsg.select : AjxMsg.add);
299 	}
300 	if (!noResize) {
301 		var curSz = this.getSize();
302 		this.resize(curSz.x, curSz.y);
303 	}
304 	
305 	// "Add All" and "Remove All" buttons only shown if MULTI_SELECT
306 	if (this._allButtons) {
307 		this._addAllButton.setVisible(style == DwtChooser.MULTI_SELECT);
308 		this._removeAllButton.setVisible(style == DwtChooser.MULTI_SELECT);
309 		this._enableButtons();
310 	}
311 
312 	// if we're going from multi to single, preserve only the first target item
313 	if (style == DwtChooser.SINGLE_SELECT) {
314 		var list = this.targetListView.getList();
315 		var a = list ? list.clone().getArray() : null;
316 		if (a && a.length) {
317 			this._reset(DwtChooserListView.TARGET);
318 			this.addItems(a[0], DwtChooserListView.TARGET, true);
319 			this.targetListView.deselectAll();
320 			if (a.length > 1 && this._mode == DwtChooser.MODE_MOVE) {
321 				this.addItems(a.slice(1), DwtChooserListView.SOURCE, true);
322 			}
323 			this._enableButtons();
324 		}
325 	}
326 	
327 	this.sourceListView.setMultiSelect(style == DwtChooser.MULTI_SELECT);
328 	this.targetListView.setMultiSelect(style == DwtChooser.MULTI_SELECT);
329 };
330 
331 /**
332  * Resets one or both list views, and the buttons. Defaults to resetting both list views.
333  *
334  * @param {DwtChooser.SINGLE_SELECT|DwtChooser.MULTI_SELECT}	style		the style single or multiple select
335  */
336 DwtChooser.prototype.reset =
337 function(view) {
338 	this._reset(view);
339 	this._setActiveButton(this._buttonInfo[0].id); // make first button active by default
340 	this._enableButtons();
341 	if (this._hasTextField) {
342 		this._textField.setValue("");
343 	}
344 };
345 
346 /**
347  * Resets one or both list views. Defaults to resetting both list views.
348  *
349  * @param {DwtChooser.SINGLE_SELECT|DwtChooser.MULTI_SELECT}	style		the style single or multiple select
350  * 
351  * @private
352  */
353 DwtChooser.prototype._reset =
354 function(view) {
355 	// clear out source list view and related data
356 	if (!view || view == DwtChooserListView.SOURCE) {
357 		this.sourceListView._resetList();
358 	}
359 
360 	// clear out target list view and related data
361 	if (!view || view == DwtChooserListView.TARGET) {
362 		this.targetListView._resetList();
363 		for (var i in this._data) {
364 			this._data[i].removeAll();
365 		}
366 	}
367 };
368 
369 /**
370  * Adds a state change listener.
371  *
372  * @param {AjxListener}		listener	a listener
373  */
374 DwtChooser.prototype.addStateChangeListener = 
375 function(listener) {
376 	this.targetListView.addStateChangeListener(listener);
377 };
378 
379 /**
380  * Removes a state change listener.
381  *
382  * @param {AjxListener}		listener	a listener
383  */
384 DwtChooser.prototype.removeStateChangeListener = 
385 function(listener) {
386 	this.targetListView.removeStateChangeListener(listener);
387 };
388 
389 /**
390  * Gets the source <code><divgt;</code> that contains the source list view.
391  * 
392  * @return	{Element}		the element
393  */
394 DwtChooser.prototype.getSourceListView = 
395 function() {
396 	return document.getElementById(this._sourceListViewDivId);
397 };
398 
399 /**
400  * Gets the source <code><div></code> that contains the buttons
401  * 
402  * @return	{Element}		the element
403  */
404 DwtChooser.prototype.getButtons = 
405 function() {
406 	return document.getElementById(this._buttonsDivId);
407 };
408 
409 /**
410  * Gets the source <code><div></code> that contains the target list view.
411  * 
412  * @return	{Element}		the element
413  */
414 DwtChooser.prototype.getTargetListView = 
415 function() {
416 	return document.getElementById(this._targetListViewDivId);
417 };
418 
419 /**
420  * Gets the text input field.
421  * 
422  * @return	{DwtInputField}		the text input field
423  */
424 DwtChooser.prototype.getTextField =
425 function() {
426 	return this._textField;
427 };
428 
429 /**
430  * Creates the HTML framework, with placeholders for elements which are created later.
431  * 
432  * @private
433  */
434 DwtChooser.prototype._createHtml = 
435 function() {
436 
437 	this._sourceListViewDivId	= Dwt.getNextId();
438 	this._targetListViewDivId	= Dwt.getNextId();
439 	this._buttonsDivId			= Dwt.getNextId();
440 	this._removeButtonDivId		= Dwt.getNextId();
441 	if (this._allButtons) {
442 		this._addAllButtonDivId		= Dwt.getNextId();
443 		this._removeAllButtonDivId	= Dwt.getNextId();
444 	}
445 	if (this._hasTextField) {
446 		this._textFieldTdId = Dwt.getNextId();
447 	}
448 
449 	var html = [];
450 	var idx = 0;
451 	
452 	if (this._layoutStyle == DwtChooser.HORIZ_STYLE) {
453 		// start new table for list views
454 		html[idx++] = "<table>";
455 		html[idx++] = "<tr>";
456 
457 		// source list
458 		html[idx++] = "<td id='";
459 		html[idx++] = this._sourceListViewDivId;
460 		html[idx++] = "'></td>";
461 
462 		// transfer buttons
463 		html[idx++] = "<td valign='middle' id='";
464 		html[idx++] = this._buttonsDivId;
465 		html[idx++] = "'>";
466 		if (this._allButtons) {
467 			html[idx++] = "<div id='";
468 			html[idx++] = this._addAllButtonDivId;
469 			html[idx++] = "'></div><br>";
470 		}
471 		for (var i = 0; i < this._buttonInfo.length; i++) {
472 			var id = this._buttonInfo[i].id;
473 			html[idx++] = "<div id='";
474 			html[idx++] = this._buttonDivId[id];
475 			html[idx++] = "'></div><br>";
476 		}
477 		// remove button
478 		html[idx++] = "<br><div id='";
479 		html[idx++] = this._removeButtonDivId;
480 		html[idx++] = "'></div>";
481 		if (this._allButtons) {
482 			html[idx++] = "<br><div id='";
483 			html[idx++] = this._removeAllButtonDivId;
484 			html[idx++] = "'></div><br>";
485 		}
486 		html[idx++] = "</td>";
487 
488 		// target list
489 		html[idx++] = "<td id='";
490 		html[idx++] = this._targetListViewDivId;
491 		html[idx++] = "'></td>";
492 
493 		html[idx++] = "</tr>";
494 		
495 		if (this._hasTextField) {
496 			html[idx++] = "<tr><td>";
497 			html[idx++] = "<table width=100%><tr><td style='white-space:nowrap; width:1%'>";
498 			html[idx++] = AjxMsg.add;
499 			html[idx++] = ":</td><td id='";
500 			html[idx++] = this._textFieldTdId;
501 			html[idx++] = "'></td></tr></table>";
502 			html[idx++] = "</td><td> </td><td> </td></tr>";
503 		}
504 
505 		html[idx++] = "</table>";
506 	} else {
507 		// source list
508 		html[idx++] = "<div id='";
509 		html[idx++] = this._sourceListViewDivId;
510 		html[idx++] = "'></div>";
511 
512 		// transfer buttons
513 		html[idx++] = "<div align='center' id='";
514 		html[idx++] = this._buttonsDivId;
515 		html[idx++] = "'>";
516 		html[idx++] = "<table class='ZPropertySheet' cellspacing='6'><tr>";
517 		if (this._allButtons) {
518 			html[idx++] = "<td id='";
519 			html[idx++] = this._addAllButtonDivId;
520 			html[idx++] = "'></td>";
521 		}
522 		for (var i = 0; i < this._buttonInfo.length; i++) {
523 			var id = this._buttonInfo[i].id;
524 			html[idx++] = "<td id='";
525 			html[idx++] = this._buttonDivId[id];
526 			html[idx++] = "'></td>";
527 		}
528 		// remove button
529 		html[idx++] = "<td id='";
530 		html[idx++] = this._removeButtonDivId;
531 		html[idx++] = "'></td>";
532 		if (this._allButtons) {
533 			html[idx++] = "<td id='";
534 			html[idx++] = this._removeAllButtonDivId;
535 			html[idx++] = "'></td>";
536 		}
537 		html[idx++] = "</tr></table></div>";
538 
539 		// target list
540 		html[idx++] = "<div id='";
541 		html[idx++] = this._targetListViewDivId;
542 		html[idx++] = "'></div>";
543 	}
544 
545 	this.getHtmlElement().innerHTML = html.join("");
546 };
547 
548 /*
549 * Takes button info and sets up various bits of internal data for later use.
550 */
551 DwtChooser.prototype._handleButtonInfo =
552 function(buttonInfo) {
553 
554 	if (!buttonInfo) {
555 		this._defLabel = (this._selectStyle == DwtChooser.SINGLE_SELECT) ? AjxMsg.select : AjxMsg.add;
556 		buttonInfo = [ { label: this._defLabel } ];
557 	}
558 	this._buttonInfo = buttonInfo;
559 
560 	// create IDs for button elements and their containers
561 	this._buttonDivId = {};
562 	this._buttonId = {};
563 	if (this._buttonInfo.length == 1) {
564 		if (!this._buttonInfo[0].id) {
565 			this._buttonInfo[0].id = Dwt.getNextId("DwtChooserButtonInfo_");
566 		}
567 		this._activeButtonId = this._buttonInfo[0].id;
568 	}
569 	for (var i = 0; i < this._buttonInfo.length; i++) {
570 		var id = this._buttonInfo[i].id;
571 		this._buttonDivId[id] = Dwt.getNextId("DwtChooserButtonDiv_");
572 		this._buttonId[id] = Dwt.getNextId("DwtChooserButton_");
573 	}
574 	this._hasMultiButtons = (this._buttonInfo.length > 1);
575 };
576 
577 /**
578  * Creates and places elements into the DOM.
579  * 
580  * @private
581  */
582 DwtChooser.prototype._initialize =
583 function() {
584 
585 	// create and add transfer buttons
586 	var buttonListener = new AjxListener(this, this._transferButtonListener);
587 	this._button = {};
588 	this._buttonIndex = {};
589 	this._data = {};
590 	for (var i = 0; i < this._buttonInfo.length; i++) {
591 		var id = this._buttonInfo[i].id;
592 		this._button[id] = this._setupButton(id, this._buttonId[id], this._buttonDivId[id], this._buttonInfo[i].label);
593 		this._button[id].addSelectionListener(buttonListener);
594 		this._buttonIndex[id] = i;
595 		this._data[id] = new AjxVector();
596 	}
597 
598 	// create and add source list view
599 	this.sourceListView = this._createSourceListView();
600 	this._addListView(this.sourceListView, this._sourceListViewDivId);
601 	this.sourceListView.addSelectionListener(new AjxListener(this, this._sourceListener));
602 
603 	// create and add target list view
604 	this.targetListView = this._createTargetListView();
605 	this._addListView(this.targetListView, this._targetListViewDivId);
606 	this.targetListView.addSelectionListener(new AjxListener(this, this._targetListener));
607 
608 	// create and add the remove button
609 	this._removeButtonId = Dwt.getNextId("DwtChooserRemoveButton_");
610 	this._removeButton = this._setupButton(DwtChooser.REMOVE_BTN_ID, this._removeButtonId, this._removeButtonDivId, AjxMsg.remove);
611 	this._removeButton.addSelectionListener(new AjxListener(this, this._removeButtonListener));
612 
613 	if (this._allButtons) {
614 		// create and add "Add All" and "Remove All" buttons
615 		this._addAllButtonId = Dwt.getNextId();
616 		this._addAllButton = this._setupButton(DwtChooser.ADD_ALL_BTN_ID, this._addAllButtonId, this._addAllButtonDivId, AjxMsg.addAll);
617 		this._addAllButton.addSelectionListener(new AjxListener(this, this._addAllButtonListener));
618 		this._removeAllButtonId = Dwt.getNextId();
619 		this._removeAllButton = this._setupButton(DwtChooser.REMOVE_ALL_BTN_ID, this._removeAllButtonId, this._removeAllButtonDivId, AjxMsg.removeAll);
620 		this._removeAllButton.addSelectionListener(new AjxListener(this, this._removeAllButtonListener));
621 		if (this._selectStyle == DwtChooser.SINGLE_SELECT) {
622 			this._addAllButton.setVisible(false);
623 			this._removeAllButton.setVisible(false);
624 		}
625 	}
626 
627 	if (this._hasTextField) {
628 		var params = {parent: this, type: DwtInputField.STRING};
629 		this._textField = new DwtInputField(params);
630 		this._textField.reparentHtmlElement(this._textFieldTdId);
631 		this._textField._chooser = this;
632 		this._textField.setHandler(DwtEvent.ONKEYUP, DwtChooser._onKeyUp);
633 		Dwt.setSize(this._textField.getInputElement(), "100%", Dwt.DEFAULT);
634 	}
635 
636 	if (this._selectStyle == DwtChooser.SINGLE_SELECT) {
637 		this.sourceListView.setMultiSelect(false);
638 		this.targetListView.setMultiSelect(false);
639 	}
640 };
641 
642 DwtChooser.prototype.getTabGroupMember =
643 function() {
644 	var tg = new DwtTabGroup(this.toString());
645 	tg.addMember(this.sourceListView);
646 	for (var i = 0; i < this._buttonInfo.length; i++) {
647 		tg.addMember(this._button[this._buttonInfo[i].id]);
648 	}
649 	tg.addMember(this._removeButton);
650 	if (this._addAllButton) {
651 		tg.addMember(this._addAllButton);
652 		tg.addMember(this._removeAllButton);
653 	}
654 	if (this._hasTextField) {
655 		tg.addMember(this._textField);
656 	}
657 	tg.addMember(this.targetListView);
658 	return tg;
659 };
660 
661 /**
662  * Returns a source list view object.
663  * 
664  * @private
665  */
666 DwtChooser.prototype._createSourceListView =
667 function() {
668 	return new DwtChooserListView(this, DwtChooserListView.SOURCE, this._slvClassName);
669 };
670 
671 /**
672  * Returns a target list view object.
673  * 
674  * @private
675  */
676 DwtChooser.prototype._createTargetListView =
677 function() {
678 	return new DwtChooserListView(this, DwtChooserListView.TARGET, this._tlvClassName);
679 };
680 
681 /**
682  * Adds a list view into the DOM and sets its size to fit in its container.
683  *
684  * @param listView		[DwtChooserListView]	the list view
685  * @param listViewDivId	[string]				ID of container DIV
686  * 
687  * @private
688  */
689 DwtChooser.prototype._addListView =
690 function(listView, listViewDivId) {
691 	var listDiv = document.getElementById(listViewDivId);
692  	listDiv.appendChild(listView.getHtmlElement());
693 	listView.setUI(null, true); // renders headers and empty list
694 	listView._initialized = true;
695 };
696 
697 /**
698  * Sizes the list views based on the given available width and height.
699  *
700  * @param {number}	width	the width (in pixels)
701  * @param {number}	height	the height (in pixels)
702  */
703 DwtChooser.prototype.resize =
704 function(width, height) {
705 	if (!width || !height) return;
706 	if (width == Dwt.DEFAULT && height == Dwt.DEFAULT) return;
707 
708 	var buttonsDiv = document.getElementById(this._buttonsDivId);
709 	var btnSz = Dwt.getSize(buttonsDiv);
710 	var w, sh, th;
711 	if (this._layoutStyle == DwtChooser.HORIZ_STYLE) {
712 		w = this._listSize ? this._listSize : (width == Dwt.DEFAULT) ? width : Math.floor(((width - btnSz.x) / 2) - 12);
713 		sh = th = height;
714 	} else {
715 		w = width;
716 		if (this._selectStyle == DwtChooser.SINGLE_SELECT) {
717 			sh = this._listSize ? this._listSize : (height == Dwt.DEFAULT) ? height : height - btnSz.y - this._singleHeight - 30;
718 			th = (height == Dwt.DEFAULT) ? height : height - btnSz.y - sh - 30;
719 		} else {
720 			sh = th = this._listSize ? this._listSize : (height == Dwt.DEFAULT) ? height : Math.floor(((height - btnSz.y) / 2) - 12);
721 		}
722 	}
723 	this.sourceListView.setSize((w == Dwt.DEFAULT) ? w : w+2, sh);
724 	this.targetListView.setSize((w == Dwt.DEFAULT) ? w : w+2, th);
725 };
726 
727 /**
728  * Creates a transfer or remove button.
729  *
730  * @param {string}	id					the button ID
731  * @param {string}	buttonId			the ID of button element
732  * @param {string}	buttonDivId		the ID of DIV that contains button
733  * @param {string}	label				the button text
734  * 
735  * @private
736  */
737 DwtChooser.prototype._setupButton =
738 function(id, buttonId, buttonDivId, label) {
739 	var button = new DwtButton({parent:this, id:buttonId});
740 	button.setText(label);
741 	button.id = buttonId;
742 	button._buttonId = id;
743 
744 	var buttonDiv = document.getElementById(buttonDivId);
745 	buttonDiv.appendChild(button.getHtmlElement());
746 
747 	return button;
748 };
749 
750 // Listeners
751 
752 /**
753  * Single-click selects an item, double-click adds selected items to target list.
754  *
755  * @param {DwtEvent}	ev		the click event
756  * 
757  * @private
758  */
759 DwtChooser.prototype._sourceListener =
760 function(ev) {
761 	if (ev.detail == DwtListView.ITEM_DBL_CLICKED) {
762 		// double-click performs transfer
763 		this.transfer(this.sourceListView.getSelection(), this._activeButtonId);
764 		this.sourceListView.deselectAll();
765 	} else if (this._activeButtonId == DwtChooser.REMOVE_BTN_ID) {
766 		// single-click activates appropriate transfer button if needed
767 		var id = this._lastActiveTransferButtonId ? this._lastActiveTransferButtonId : this._buttonInfo[0].id;
768 		this._setActiveButton(id);
769 	}
770 	this.targetListView.deselectAll();
771 	this._enableButtons();
772 };
773 
774 /**
775  * Single-click selects an item, double-click removes it from the target list.
776  *
777  * @param {DwtEvent}		ev		the click event
778  * 
779  * @private
780  */
781 DwtChooser.prototype._targetListener =
782 function(ev) {
783 	if (ev.detail == DwtListView.ITEM_DBL_CLICKED) {
784 		this.remove(this.targetListView.getSelection());
785 	} else {
786 		this._setActiveButton(DwtChooser.REMOVE_BTN_ID);
787 		this.sourceListView.deselectAll();
788 		this._enableButtons();
789 	}
790 };
791 
792 /**
793  * Clicking a transfer button moves selected items to the target list.
794  *
795  * @param {DwtEvent}		ev		the click event
796  * 
797  * @private
798  */
799 DwtChooser.prototype._transferButtonListener =
800 function(ev) {
801 	var button = DwtControl.getTargetControl(ev);
802 	var id = button._buttonId;
803 	var sel = this.sourceListView.getSelection();
804 	if (sel && sel.length) {
805 		this.transfer(sel, id);
806 		this._enableButtons();
807 	} else {
808 		var email = this._getEmailFromText();
809 		if (email) {
810 			this.transfer([email], id);
811 		} else {
812 			this._setActiveButton(id);
813 		}
814 	}
815 };
816 
817 /**
818  * Clicking the remove button removes selected items from the target list.
819  *
820  * @param {DwtEvent}		ev		the click event
821  * 
822  * @private
823  */
824 DwtChooser.prototype._removeButtonListener =
825 function(ev) {
826 	this.remove(this.targetListView.getSelection());
827 	var list = this.targetListView.getList();
828 	if (list && list.size()) {
829 		this._selectFirst(DwtChooserListView.TARGET);
830 	} else {
831 		this._enableButtons();
832 	}
833 };
834 
835 /**
836  * Populates the target list with all items.
837  *
838  * @param {DwtEvent}		ev		the click event
839  * 
840  * @private
841  */
842 DwtChooser.prototype._addAllButtonListener =
843 function(ev) {
844 	this.transfer(this.sourceListView.getList().clone());
845 	this._selectFirst(DwtChooserListView.TARGET);
846 };
847 
848 /**
849  * Clears the target list.
850  *
851  * @param {DwtEvent}		ev		the click event
852  * 
853  * @private
854  */
855 DwtChooser.prototype._removeAllButtonListener =
856 function(ev) {
857 	this.remove(this.targetListView.getList().clone());
858 	this._selectFirst(DwtChooserListView.SOURCE);
859 };
860 
861 
862 
863 // Miscellaneous methods
864 
865 /**
866  * Enable/disable buttons as appropriate.
867  *
868  * @private
869  */
870 DwtChooser.prototype._enableButtons =
871 function(sForce, tForce) {
872 	var sourceList = this.sourceListView.getList();
873 	var targetList = this.targetListView.getList();
874 	var sourceEnabled = (sForce || (this.sourceListView.getSelectionCount() > 0));
875 	for (var i = 0; i < this._buttonInfo.length; i++) {
876 		var id = this._buttonInfo[i].id;
877 		this._button[id].setEnabled(sourceEnabled);
878 	}
879 	var targetEnabled = (tForce || (this.targetListView.getSelectionCount() > 0));
880 	this._removeButton.setEnabled(targetEnabled);
881 
882 	if (this._allButtons && (this._selectStyle == DwtChooser.MULTI_SELECT)) {
883 		var sourceSize = sourceList ? sourceList.size() : 0;
884 		var targetSize = targetList ? targetList.size() : 0;
885 		this._addAllButton.setEnabled(sourceSize > 0);
886 		this._removeAllButton.setEnabled(targetSize > 0);
887 	}
888 };
889 
890 /**
891  * Selects the first item in the given list view.
892  *
893  * @param {constant}	view	the source or target
894  * 
895  * @private
896  */
897 DwtChooser.prototype._selectFirst =
898 function(view, index) {
899 	var listView = (view == DwtChooserListView.SOURCE) ? this.sourceListView : this.targetListView;
900 	var list = listView.getList();
901 	if (list && list.size() > 0) {
902 		listView.setSelection(list.get(0));
903 	}
904 };
905 
906 /**
907  * Makes a button "active" (the default for double-clicks). Done by
908  * manipulating the style class. The active/non-active class is set as the
909  * "_origClassName" so that activation/triggering still work. This only
910  * applies if there are multiple transfer buttons.
911  *
912  * @param {string}	id		the ID of button to make active
913  * 
914  * @private
915  */
916 DwtChooser.prototype._setActiveButton =
917 function(id) {
918 	if (!this._hasMultiButtons) {
919 		return;
920 	}
921 	if (id != this._activeButtonId) {
922 		var buttonId = (this._activeButtonId == DwtChooser.REMOVE_BTN_ID) ? this._removeButtonId : this._buttonId[this._activeButtonId];
923 		if (buttonId) {
924 			var oldButton = DwtControl.findControl(document.getElementById(buttonId));
925 			if (oldButton) {
926 				oldButton.setDisplayState(DwtControl.NORMAL);
927 			}
928 		}
929 		buttonId = (id == DwtChooser.REMOVE_BTN_ID) ? this._removeButtonId : this._buttonId[id];
930 		var button = DwtControl.findControl(document.getElementById(buttonId));
931 		if (button) {
932 			button.setDisplayState(DwtControl.DEFAULT);
933 		}
934 		this._activeButtonId = id;
935 		if (id != DwtChooser.REMOVE_BTN_ID) {
936 			this._lastActiveTransferButtonId = id;
937 		}
938 	}
939 };
940 
941 /**
942  * Returns true if the list contains the item. Default implementation is identity.
943  *
944  * @param {Object}	item	the item
945  * @param {AjxVector}	list	the list to check against
946  * 
947  * @private
948  */
949 DwtChooser.prototype._isDuplicate =
950 function(item, list) {
951 	return list.contains(item);
952 };
953 
954 /**
955  * Adds an item to the end of the source list.
956  *
957  * @param {Object}	item		the item to add
958  * @param {boolean}	skipNotify	if <code>true</code>, don't notify listeners
959  * 
960  * @private
961  */
962 DwtChooser.prototype._addToSource =
963 function(item, index, skipNotify) {
964 	if (!item) return;
965 	if (!item._chooserIndex) {
966 		var list = this.sourceListView.getList();
967 		item._chooserIndex = list ? list.size() + 1 : 1;
968 	}
969 	this.sourceListView.addItem(item, index, skipNotify);
970 };
971 
972 /**
973  * Adds an item to the target list. If there are multiple transfer buttons, it keeps
974  * the items grouped depending on which button was used to move them.
975  *
976  * @param {Object}	item		the item to add
977  * @param {string}	id			the ID of the transfer button that was used
978  * @param {boolean}	skipNotify	if <code>true</code>, don't notify listeners
979  * 
980  * @private
981  */
982 DwtChooser.prototype._addToTarget =
983 function(item, id, skipNotify) {
984 	if (!item) return;
985 	id = id ? id : this._activeButtonId;
986 	if (this._noDuplicates && this._data[id] && this._isDuplicate(item, this._data[id])) {
987 		return;
988 	}
989 
990 	// item is being added to target list with multiple transfer buttons,
991 	// so we need to clone it on second and subsequent transfers
992 	var list = this.targetListView.getList();
993 	if (list && list.contains(item) && item.clone) {
994 		var newItem = item.clone();
995 		newItem.id = Dwt.getNextId();
996 		item = newItem;
997 	}
998 
999 	var idx = null;
1000 	if (this._hasMultiButtons) {
1001 		// get a list of all the items in order
1002 		list = [];
1003 		for (var i = 0; i < this._buttonInfo.length; i++) {
1004 			list = list.concat(this._data[this._buttonInfo[i].id].getArray());
1005 		}
1006 		// find the first item with a higher button index
1007 		var buttonIdx = this._buttonIndex[id];
1008 		for (idx = 0; idx < list.length; idx++) {
1009 			var testButtonIdx = this._buttonIndex[list[idx]._buttonId];
1010 			if (testButtonIdx > buttonIdx) {
1011 				break;
1012 			}
1013 		}
1014 	}
1015 
1016 	item._buttonId = id;
1017 	item.id = item.id || Dwt.getNextId();
1018 	this._data[id].add(item);
1019 	this.targetListView.addItem(item, idx, skipNotify);
1020 };
1021 
1022 /**
1023  * Removes an item from the source list.
1024  *
1025  * @param {Object}	item		the item to remove
1026  * @param {boolean}	skipNotify	if <code>true</code>, don't notify listeners
1027  * 
1028  * @private
1029  */
1030 DwtChooser.prototype._removeFromSource =
1031 function(item, skipNotify) {
1032 	if (!item) return;
1033 	var list = this.sourceListView.getList();
1034 	if (!list) return;
1035 	if (!list.contains(item)) return;
1036 
1037 	this.sourceListView.removeItem(item, skipNotify);
1038 };
1039 
1040 /**
1041  * Removes an item from the target list.
1042  *
1043  * @param {Object}	item		the item to remove
1044  * @param {boolean}	skipNotify	if <code>true</code>, don't notify listeners
1045  * 
1046  * @private
1047  */
1048 DwtChooser.prototype._removeFromTarget =
1049 function(item, skipNotify) {
1050 	if (!item) return;
1051 	var list = this.targetListView.getList();
1052 	if (!list) return;
1053 	if (!list.contains(item)) return;
1054 
1055 	this._data[item._buttonId].remove(item);
1056 	this.targetListView.removeItem(item, skipNotify);
1057 };
1058 
1059 DwtChooser.prototype._getInsertionIndex =
1060 function(view, item) {
1061 	var list = view.getList();
1062 	if (!list) return null;
1063 	var a = list.getArray();
1064 	for (var i = 0; i < a.length; i++) {
1065 		if (item._chooserIndex && a[i]._chooserIndex && (a[i]._chooserIndex >= item._chooserIndex)) {
1066 			return i;
1067 		}
1068 	}
1069 	return null;
1070 };
1071 
1072 DwtChooser.prototype._getEmailFromText =
1073 function() {
1074 	if (this._hasTextField) {
1075 		var text = this._textField.getValue();
1076 		var email = AjxEmailAddress.parse(text);
1077 		if (email) {
1078 			email.id = Dwt.getNextId();
1079 			return email;
1080 		}
1081 	}
1082 };
1083 
1084 DwtChooser._onKeyUp =
1085 function(ev) {
1086 	var el = DwtUiEvent.getTarget(ev);
1087 	var obj = DwtControl.findControl(el);	// DwtInputField
1088 	var chooser = obj._chooser;
1089 	var key = DwtKeyEvent.getCharCode(ev);
1090 	if (DwtKeyEvent.IS_RETURN[key]) {
1091 		var email = chooser._getEmailFromText();
1092 		if (email) {
1093 			chooser.transfer([email], chooser._activeButtonId);
1094 			el.value = "";
1095 		}
1096 	}
1097 	chooser._enableButtons(el.value.length);
1098 };
1099 
1100 /**
1101  * Creates a chooser list view.
1102  * @constructor
1103  * @class
1104  * This base class represents a list view which contains items that can be transferred from it
1105  * (source) or to it (target). Subclasses should implement  _getHeaderList(),
1106  * _sortColumn(), and _createItemHtml().
1107  *
1108  * @param {hash}	params		a hash of parameters
1109  * @param {DwtComposite}      params.parent		the containing widget
1110  * @param {constant}      params.type			the source or target
1111  * @param {string}      params.className		the CSS class
1112  * @param {constant}      params.view			the context for use in creating IDs
1113  * 
1114  * @extends		DwtListView
1115  */
1116 DwtChooserListView = function(params) {
1117 	
1118 	if (arguments.length == 0) return;
1119 	params = Dwt.getParams(arguments, DwtChooserListView.PARAMS);
1120 	params.className = params.className || "DwtChooserListView";
1121 	params.headerList = this._getHeaderList(parent);
1122 	DwtListView.call(this, params);
1123 
1124 	this.type = params.type;
1125 	this._chooserParent = params.parent.parent;
1126 
1127     // create a drag source so that dragging a column header will trigger mouse capture
1128     this._dragSrc = new DwtDragSource(Dwt.DND_DROP_MOVE);
1129     this.setDragSource(this._dragSrc);
1130 };
1131 
1132 DwtChooserListView.PARAMS = ["parent", "type", "className", "view"];
1133 
1134 /**
1135  * Defines the "source" list view type.
1136  */
1137 DwtChooserListView.SOURCE = 1;
1138 /**
1139  * Defines the "target" list view type.
1140  */
1141 DwtChooserListView.TARGET = 2;
1142 
1143 DwtChooserListView.prototype = new DwtListView;
1144 DwtChooserListView.prototype.constructor = DwtChooserListView;
1145 
1146 DwtChooserListView.prototype._getHeaderList = function() {};
1147 
1148 DwtChooserListView.prototype.toString = 
1149 function() {
1150 	return "DwtChooserListView";
1151 };
1152 
1153 /*
1154 * Override to handle empty results set. Always omit the "No Results" message if
1155 * this is a target list view, or if we've been told to ignore it in the source view.
1156 */
1157 DwtChooserListView.prototype.setUI =
1158 function(defaultColumnSort, noResultsOk) {
1159 	noResultsOk = noResultsOk ? noResultsOk : ((this.type == DwtChooserListView.TARGET) ||
1160 												this.parent._sourceEmptyOk);
1161 	DwtListView.prototype.setUI.call(this, defaultColumnSort, noResultsOk);
1162 };
1163 
1164 /**
1165  * DwtListView override to ignore right-clicks in list view.
1166  *
1167  * @param {Element}	clickedEl		the element that was clicked
1168  * @param {DwtEvent}	ev				the click event
1169  * 
1170  * @private
1171  */
1172 DwtChooserListView.prototype._itemClicked = 
1173 function(clickedEl, ev) {
1174 	// Ignore right-clicks, we don't support action menus
1175 	if (!ev.shiftKey && !ev.ctrlKey && ev.button == DwtMouseEvent.RIGHT) {
1176 		return;
1177 	} else {
1178 		DwtListView.prototype._itemClicked.call(this, clickedEl, ev);
1179 	}
1180 };
1181 
1182 /**
1183  * Called when a column header has been clicked.
1184  *
1185  * @param {string}	columnItem		the ID for column that was clicked
1186  * @param {boolean}	ascending		if <code>true</code>, sort in ascending order
1187  * 
1188  * @private
1189  */
1190 DwtChooserListView.prototype._sortColumn = 
1191 function(columnItem, ascending) {
1192 	this._chooserParent.search(columnItem, ascending);
1193 };
1194 
1195 DwtChooserListView.prototype._getHeaderSashLocation =
1196 function() {
1197 
1198 	var el = this.getHtmlElement();
1199 	if (Dwt.getPosition(el) == Dwt.ABSOLUTE_STYLE) {
1200 		return DwtListView.prototype._getHeaderSashLocation.call(this);
1201 	}
1202 
1203 	var thisLoc = Dwt.toWindow(el, 0, 0);
1204 	var contLoc = Dwt.toWindow(this._chooserParent.getHtmlElement(), 0, 0);
1205 	if (!this._tmpPoint) {
1206 		this._tmpPoint = new DwtPoint();
1207 	}
1208 	this._tmpPoint.x = thisLoc.x - contLoc.x;
1209 	this._tmpPoint.y = thisLoc.y - contLoc.y;
1210 
1211 	return this._tmpPoint;
1212 };
1213