1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 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) 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 combo box.
 26  * @constructor
 27  * @class
 28  * This class represents a combo box.
 29  *
 30  * @author Dave Comfort
 31  * 
 32  * @param {hash}	params		a hash of parameters
 33  * @param {DwtComposite}      parent		the parent widget
 34  * @param {boolean}	useLabel		Set to true if the value should be shown in a DwtLabel. Defaults to false, showing it in a DwtInputField.
 35  * @param {hash}	inputParams		params for the input (see {@link DwtInputField} or {@link DwtLabel})
 36  * @param {string}      className		the CSS class
 37  * @param {constant}      posStyle		the positioning style (see {@link DwtControl})
 38  * @param {int}     maxRows         The number of maxRows needed in drop down(see {@link DwtMenu})
 39  * @param {constant} layout         The layout of the drop down(see {@link DwtMenu})
 40  * @param {boolean} autoScroll    Set to true if auto scroll to the input text is needed. Defaults to false.
 41  * 
 42  * @extends		DwtComposite
 43  */
 44 DwtComboBox = function(params) {
 45     if (arguments.length == 0) { return; }
 46 	params = Dwt.getParams(arguments, DwtComboBox.PARAMS);
 47     params.className = params.className || "DwtComboBox";
 48     DwtComposite.call(this, params);
 49     
 50     this.input = null;
 51 	this._menu = null;
 52     this._button = null;
 53     
 54     this._textToValue = {}; // Map of text strings to their values.
 55     this._valueToText = {};
 56     this._valueToItem = {};
 57 	this._size = 0;
 58 
 59     this._hasMenuCallback = true;
 60 	this._menuItemListenerObj = new AjxListener(this, this._menuItemListener);
 61 
 62     this._inputParams = params.inputParams;
 63 	this._useLabel = Boolean(params.useLabel);
 64 	this._maxRows = params.maxRows;
 65 	this._layout = params.layout;
 66 	this._autoScroll = params.autoScroll || false;
 67     this._createHtml();
 68 };
 69 
 70 DwtComboBox.PARAMS = ["parent", "inputParams", "className", "posStyle", "dialog"];
 71 
 72 DwtComboBox.prototype = new DwtComposite;
 73 DwtComboBox.prototype.constructor = DwtComboBox;
 74 
 75 DwtComboBox.prototype.isDwtComboBox = true;
 76 DwtComboBox.prototype.toString = function() { return "DwtComboBox"; };
 77 
 78 //
 79 // Data
 80 //
 81 
 82 DwtComboBox.prototype.TEMPLATE = "dwt.Widgets#DwtComboBox";
 83 
 84 //
 85 // Public methods
 86 //
 87 
 88 DwtComboBox.prototype.getTabGroupMember = function() {
 89 	return this._tabGroup;
 90 };
 91 
 92 /**
 93  * Adds the change listener.
 94  * 
 95  * @param	{AjxListener}	listener		the listener
 96  */
 97 DwtComboBox.prototype.addChangeListener = function(listener) {
 98 	this.addListener(DwtEvent.ONCHANGE, listener);
 99 };
100 
101 /**
102  * Removes the change listener.
103  * 
104  * @param	{AjxListener}	listener		the listener
105  */
106 DwtComboBox.prototype.removeChangeListener = function(listener) {
107 	this.removeListener(DwtEvent.ONCHANGE, listener);
108 };
109 
110 /**
111  * Adds an entry to the combo box list.
112  * 
113  * @param {string}	text		the user-visible text for the entry
114  * @param {string}	value		the value for the entry
115  * @param {boolean}	selected	if <code>true</code>, the entry is selected
116  */
117 DwtComboBox.prototype.add =
118 function(text, value, selected) {
119 	this._textToValue[text] = value;
120     this._valueToText[value] = text;
121     if (!this._hasMenuCallback) {
122 		var menu = this._button.getMenu();
123     	this._createMenuItem(menu, text);
124 	}
125 	if (selected) {
126 		this.setText(text);
127 	}
128 	this._size++;
129 	this._updateButton();
130 };
131 
132 /**
133  * Removes the specified value from the list.
134  *
135  * @param	{string}	value		the value
136  */
137 DwtComboBox.prototype.remove = function(value) {
138     var item = this._valueToItem[value];
139     if (item) {
140         this._button.getMenu().removeChild(item);
141         var text = this._valueToText[value];
142         delete this._textToValue[text];
143         delete this._valueToText[value];
144         delete this._valueToItem[value];
145         if (this.getText() == text) {
146             this.setText("");
147         }
148 		this._size--;
149 		this._updateButton();
150 	}
151 };
152 
153 /**
154  * Removes all the items in the list.
155  * 
156  */
157 DwtComboBox.prototype.removeAll = function() {
158     this._button.setMenu(new AjxCallback(this, this._createMenu), true);
159     this._hasMenuCallback = true;
160 
161     this._textToValue = {};
162     this._valueToText = {};
163     this._valueToItem = {};
164 	this._size = 0;
165 	this._updateButton();
166 };
167 
168 /**
169  * Gets the value of the currently selected entry. If the entry
170  * is one that was not added via the add method (that is, if it was
171  * typed in by the user) then <code>null</code> is returned.
172  * 
173  * @return	{string}	the value
174  */
175 DwtComboBox.prototype.getValue =
176 function() {
177 	var text = this.getText();
178 	return this._textToValue[text];
179 };
180 
181 /**
182  * Sets the value.
183  * 
184  * @param	{string}	value		the value
185  */
186 DwtComboBox.prototype.setValue = function(value) {
187 	var text = this._valueToText[value];
188 	this.setText(text || value);
189 };
190 
191 /**
192  * Gets the text of the currently selected entry.
193  * 
194  * @return	{string}	the text
195  */
196 DwtComboBox.prototype.getText =
197 function() {
198 	return this._useLabel ? this.input.getText() : this.input.getValue();
199 };
200 
201 /**
202  * Sets the selected text.
203  * 
204  * @param	{string}	text		the text
205  */
206 DwtComboBox.prototype.setText =
207 function(text) {
208 	if (this._useLabel)
209 		this.input.setText(text);
210 	else
211 		this.input.setValue(text);
212 };
213 
214 DwtComboBox.prototype.setEnabled =
215 function(enabled) {
216 	if (enabled != this._enabled) {
217 		DwtComposite.prototype.setEnabled.call(this, enabled);
218 		this.input.setEnabled(enabled);
219 		this._button.setEnabled(enabled);
220     }
221 };
222 
223 DwtComboBox.prototype.focus = function() {
224     return this.input.focus();
225 };
226 
227 DwtComboBox.prototype.popdown = function() {
228 	if (this._menu)
229 		this._menu.popdown();
230 };
231 
232 //
233 // Protected methods
234 //
235 
236 DwtComboBox.prototype._createMenu =
237 function() {
238 	var params = {parent:this};
239 	if (this._maxRows) {
240 		params.maxRows = this._maxRows;
241 	}
242 	if (this._layout) {
243 		params.layout = this._layout;
244 	}
245     var menu = this._menu = new DwtMenu(params);
246     for (var i in this._textToValue) {
247     	var item = this._createMenuItem(menu, i);
248         var value = this._textToValue[i];
249         this._valueToItem[value] = item;
250     }
251 	this._hasMenuCallback = false;
252 	return menu;
253 };
254 
255 DwtComboBox.prototype._createMenuItem =
256 function(menu, text) {
257 	var item = new DwtMenuItem({parent:menu});
258 	item.setText(text);
259 	item.addSelectionListener(this._menuItemListenerObj);
260 	if (!this._menuWidth) {
261 		this._menuWidth = this.getW() - 10; // 10 is some fudge factor that lines up the menu right.
262 	}
263     item.getHtmlElement().style.minWidth = this._menuWidth;
264     return item;
265 };
266 
267 DwtComboBox.prototype._menuItemListener =
268 function(ev) {
269 	var menuItem = ev.dwtObj;
270 	var ovalue = this.getText();
271 	var nvalue = menuItem.getText();
272 	this.setText(nvalue);
273 	this._menu.popdown();
274 
275 	// notify our listeners
276 	var event = DwtUiEvent.getEvent(ev);
277 	event._args = { selectObj: this, newValue: nvalue, oldValue: ovalue };
278 	this.notifyListeners(DwtEvent.ONCHANGE, event);
279 
280 	if (!this._useLabel) {
281 	    var input = this.input.getInputElement();
282 	    input.focus();
283 	    input.select();
284 	}
285 };
286 
287 DwtComboBox.prototype._handleKeyDown = function(ev) {
288 	var keycode = DwtKeyEvent.getCharCode(ev);
289 
290 	this.__ovalue = this.getText();
291 
292 	return true;
293 };
294 
295 DwtComboBox.prototype._handleKeyUp = function(ev) {
296 	// propagate event to DwtInputField
297 	DwtInputField._keyUpHdlr(ev);
298 	// notify our listeners
299 	var event = DwtUiEvent.getEvent(ev);
300 	var newValue = this.getText();
301 	event._args = { selectObj: this, newValue: newValue, oldValue: this.__ovalue };
302 	this.notifyListeners(DwtEvent.ONCHANGE, event);
303 	if (this._menu && this._autoScroll && newValue != this.__ovalue) {
304 		//if auto scroll is on then scroll to the index which starts with
305 		//the value in input field
306 		var index = 0;
307 		for (var text in this._textToValue) {
308 			if (text.indexOf(newValue) == 0) {
309 				this._menu.scrollToIndex(index);
310 				break;
311 			}
312 			index++;
313 		}
314 	}
315 	return true;
316 };
317 
318 DwtComboBox.prototype._updateButton =
319 function() {
320 	this._button.setVisible(this._size > 0);
321 };
322 
323 DwtComboBox.prototype._createHtml = function(templateId) {
324     var data = { id: this._htmlElId };
325     this._createHtmlFromTemplate(templateId || this.TEMPLATE, data);
326 };
327 
328 DwtComboBox.prototype._createHtmlFromTemplate = function(templateId, data) {
329     DwtComposite.prototype._createHtmlFromTemplate.call(this, templateId, data);
330 
331 	var inputParams = this._inputParams || {};
332 	inputParams.parent = this;
333 	inputParams.size = inputParams.size || 40;
334 	delete this._inputParams;
335     
336 	this.input = (this._useLabel ?
337 	              new DwtLabel(inputParams) : new DwtInputField(inputParams));
338     this.input.replaceElement(data.id + "_input");
339 	this.input.setHandler(DwtEvent.ONKEYDOWN, AjxCallback.simpleClosure(this._handleKeyDown, this));
340 	this.input.setHandler(DwtEvent.ONKEYUP, AjxCallback.simpleClosure(this._handleKeyUp, this));
341 
342     this._button = new DwtComboBoxButton({parent:this});
343 	this._button.setMenu(new AjxListener(this, this._createMenu), true);
344     this._button.replaceElement(data.id + "_button");
345 	this._updateButton();
346 
347 	this._tabGroup = new DwtTabGroup(this._htmlElId);
348 	this._tabGroup.addMember(this.input);
349 	this._tabGroup.addMember(this._button);
350 };
351 
352 /**
353  * The input field inherits the id for accessibility purposes.
354  * 
355  * @private
356  */
357 DwtComboBox.prototype._replaceElementHook =
358 function(oel, nel, inheritClass, inheritStyle) {
359 	DwtComposite.prototype._replaceElementHook.apply(this, arguments);
360 	// set input settings
361 	if (oel.size) {
362 		var el = (this._useLabel ?
363 		          this.input.getHtmlElement() :
364 		          this.input.getInputElement());
365 		el.size = oel.size;
366 	}
367 	if (oel.title) {
368 		this.input.setHint(oel.title);
369 	}
370 };
371 
372 //
373 // Classes
374 //
375 
376 /**
377  * DwtComboBoxButton: Stylizable button just for use in combo boxes.
378  * 
379  * @param {hash}	params		a hash of parameters
380  * @param {DwtComposite}       params.parent		the parent widget
381  * @param	{string}       params.className		the CSS class
382  * 
383  * @extends		DwtButton
384  * @private
385  */
386 DwtComboBoxButton = function(params) {
387 	params = Dwt.getParams(arguments, DwtComboBoxButton.PARAMS);
388 	params.posStyle = Dwt.RELATIVE_STYLE;
389 	DwtButton.call(this, params);
390 }
391 
392 DwtComboBoxButton.prototype = new DwtButton;
393 DwtComboBoxButton.prototype.constructor = DwtComboBoxButton;
394 
395 DwtComboBoxButton.prototype.toString =
396 function() {
397     return "DwtComboBoxButton";
398 };
399 
400 DwtComboBoxButton.PARAMS = ["parent", "className"];
401 
402 // Data
403 
404 DwtComboBoxButton.prototype.TEMPLATE = "dwt.Widgets#DwtComboBoxButton"
405 
406