1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2006, 2007, 2008, 2009, 2010, 2013, 2014, 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, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 /**
 24  * Creates a spinner control.
 25  * @constructor
 26  * @class
 27  * Represents a entry field for entering numeric values.  Has 2 arrow buttons
 28  * that can be used to increment or decrement the current value with a step
 29  * that can be specified.
 30  *
 31  * <h4>CSS</h4>
 32  * <ul>
 33  * <li><code>DwtSpinner</code>              -- a table that contains the spinner elements
 34  * <li><code>DwtSpinner-inputCell</code>    -- the TD that holds the input field
 35  * <li><code>DwtSpinner-btnCell</code>      -- a DIV holding the 2 arrow buttons
 36  * <li><code>DwtSpinner-upBtn</code>        -- the DIV button for increment operation
 37  * <li><code>DwtSpinner-downBtn</code>      -- the DIV button for decrement operation
 38  * <li><code>DwtSpinner-up-pressed</code>   -- upBtn while pressed
 39  * <li><code>DwtSpinner-down-pressed</code> -- downBtn while pressed
 40  * <li><code>DwtSpinner-disabled</code>     -- the table gets this class added when the widget is disabled
 41  * </ul>
 42  * 
 43  * @param	{hash}	params		a hash of parameters
 44  * @param {DwtComposite} params.parent 	the parent widget
 45  * @param {string} params.className the class name for the containing DIV (see {@link DwtControl})
 46  * @param {string} params.posStyle 	the positioning style (see {@link DwtControl})
 47  * @param {number} params.max 	the maximum value
 48  * @param {number} params.min 	the minimum value
 49  * @param {number} params.size 	size of the input field, as in <code><input size="X"></code>
 50  * @param {number} params.value the original value of the input field
 51  * @param {number} params.maxLen the maximum length of the text in the input field
 52  * @param {number} params.step 	the amount to add or substract when the arrow buttons are pressed
 53  * @param {number} [params.decimals=0] Number of decimal digits.  Specify 0 to allow only
 54  *                 integers (default). Pass <code>null</code> to allow float numbers but
 55  *                 not enforce decimals.
 56  * @param {string} [params.align="right"] 	the align of the input field text (see <code>dwt.css</code>)
 57  *
 58  * @author Mihai Bazon
 59  * 
 60  * @extends DwtControl
 61  */
 62 DwtSpinner = function(params) {
 63 	if (arguments.length == 0) return;
 64 	DwtControl.call(this, { parent        : params.parent,
 65                                 className     : params.className,
 66                                 posStyle      : params.posStyle,
 67                                 parentElement : params.parentElement
 68                               });
 69 
 70 	// setup arguments
 71 	this._maxValue      = params.max  != null ? params.max  : null;
 72 	this._minValue      = params.min  != null ? params.min  : null;
 73 	this._fieldSize     = params.size != null ? params.size : 3;
 74 	this._origValue     = params.value     || 0;
 75 	this._maxLen        = params.maxLen    || null;
 76 	this._step          = params.step      || 1;
 77 	this._decimals      = 'decimals' in params ? params.decimals : 0;
 78 	this._align         = params.align     || null;
 79 
 80 	// timerFunc is a closure that gets called upon timeout when the user
 81 	// presses and holds the mouse button
 82 	this._timerFunc = AjxCallback.simpleClosure(this._timerFunc, this);
 83 
 84 	// upon click and hold we capture mouse events
 85 	this._btnPressCapture = new DwtMouseEventCapture({
 86 		targetObj:this,
 87 		id:"DwtSpinner",
 88 		mouseUpHdlr:AjxCallback.simpleClosure(this._stopCapture, this)
 89 	});
 90 
 91 	this._createElements();
 92 };
 93 
 94 DwtSpinner.prototype = new DwtControl;
 95 DwtSpinner.prototype.constructor = DwtSpinner;
 96 
 97 DwtSpinner.INIT_TIMER = 250;
 98 DwtSpinner.SLOW_TIMER = 125;
 99 DwtSpinner.FAST_TIMER = 33;
100 
101 DwtSpinner.prototype._createElements = function() {
102 	var div = this.getHtmlElement();
103 	var id = Dwt.getNextId();
104 	this._idField = id;
105 	this._idUpButton = id + "-up";
106 	this._idDownButton = id + "-down";
107 	var html = [ "<table class='DwtSpinner' cellspacing='0' cellpadding='0'>",
108 		     "<tr><td rowspan='2' class='DwtSpinner-inputCell'>", "<input id='", id, "' autocomplete='off' />", "</td>",
109 		     "<td unselectable id='", this._idUpButton, "' class='DwtSpinner-upBtn'><div class='ImgUpArrowSmall'> </div></td>",
110 		     "</tr><tr>",
111 		     "<td unselectable id='", this._idDownButton, "' class='DwtSpinner-downBtn'><div class='ImgDownArrowSmall'> </div></td>",
112 		     "</tr></table>" ];
113 
114 
115 // 		     "<td><div class='DwtSpinner-btnCell'>",
116 // 		     "<div unselectable class='DwtSpinner-upBtn' id='", this._idUpButton, "'><div class='ImgUpArrowSmall'> </div></div>",
117 // 		     "<div unselectable class='DwtSpinner-downBtn' id='", this._idDownButton, "'><div class='ImgDownArrowSmall'> </div></div>",
118 // 		     "</div></td></tr></table>" ];
119 	div.innerHTML = html.join("");
120 
121 	var b1 = this._getUpButton();
122 	b1.onmousedown = AjxCallback.simpleClosure(this._btnPressed, this, "Up");
123 	var b2 = this._getDownButton();
124 	b2.onmousedown = AjxCallback.simpleClosure(this._btnPressed, this, "Down");
125 // 	if (AjxEnv.isIE) {
126 // 		b1.ondblclick = b1.onmousedown;
127 // 		b2.ondblclick = b2.onmousedown;
128 //	}
129 // 	if (AjxEnv.isIE && b1.offsetHeight == 1) {
130 // 		// we must correct button heights for IE
131 // 		div = b1.parentNode;
132 // 		var td = div.parentNode;
133 // 		div.style.height = td.offsetHeight + "px";
134 // // 		b1.style.height = b2.style.height = td.offsetHeight / 2 + "px";
135 // // 		b2.style.top = "";
136 // // 		b2.style.bottom = "0px";
137 // 	}
138 	var input = this.getInputElement();
139 	if (this._maxLen)
140 		input.maxLength = this._maxLen;
141 	if (this._fieldSize)
142 		input.size = this._fieldSize;
143 	if (this._align)
144 		input.style.textAlign = this._align;
145 	if (this._origValue != null)
146 		this.setValue(this._origValue);
147 
148 	input.onblur = AjxCallback.simpleClosure(this.setValue, this, null);
149 	input[(AjxEnv.isIE || AjxEnv.isOpera) ? "onkeydown" : "onkeypress"]
150 		= AjxCallback.simpleClosure(this.__onKeyPress, this);
151 };
152 
153 DwtSpinner.prototype._getValidValue = function(val) {
154 	var n = parseFloat(val);
155 	if (isNaN(n) || n == null)
156 		n = this._lastValidValue; // note that this may be string
157 	if (n == null)
158 		n = this._minValue || 0;
159 	if (this._minValue != null && n < this._minValue)
160 		n = this._minValue;
161 	if (this._maxValue != null && n > this._maxValue)
162 		n = this._maxValue;
163 	// make sure it's a number
164 	n = parseFloat(n);
165 	if (this._decimals != null)
166 		n = n.toFixed(this._decimals);
167 	this._lastValidValue = n;
168 	return n;
169 };
170 
171 /**
172  * Gets the input element.
173  * 
174  * @return	{Element}	the element
175  */
176 DwtSpinner.prototype.getInputElement = function() {
177 	return document.getElementById(this._idField);
178 };
179 
180 DwtSpinner.prototype._getUpButton = function() {
181 	return document.getElementById(this._idUpButton);
182 };
183 
184 DwtSpinner.prototype._getDownButton = function() {
185 	return document.getElementById(this._idDownButton);
186 };
187 
188 DwtSpinner.prototype._getButton = function(direction) {
189 	switch (direction) {
190 	    case "Up"   : return this._getUpButton();
191 	    case "Down" : return this._getDownButton();
192 	}
193 };
194 
195 DwtSpinner.prototype._setBtnState = function(dir, disabled) {
196 	var btn = this._getButton(dir);
197 	if (disabled) {
198 		Dwt.addClass(btn, "DwtSpinner-" + dir + "-disabled");
199 		btn.firstChild.className = "Img" + dir + "ArrowSmallDis";
200 	} else {
201 		Dwt.delClass(btn, "DwtSpinner-" + dir + "-disabled");
202 		btn.firstChild.className = "Img" + dir + "ArrowSmall";
203 	}
204 };
205 
206 /**
207  * Gets the value.
208  * 
209  * @return	{number}	the value
210  */
211 DwtSpinner.prototype.getValue = function() {
212 	return parseFloat(this._getValidValue(this.getInputElement().value));
213 };
214 
215 /**
216  * Sets the value.
217  * 
218  * @param	{number}	val		the value
219  */
220 DwtSpinner.prototype.setValue = function(val) {
221 	if (val == null)
222 		val = this.getInputElement().value;
223 	val = this._getValidValue(val);
224 	this.getInputElement().value = val;
225 	val = parseFloat(val);
226 	this._setBtnState("Down", this._minValue != null && this._minValue == val);
227 	this._setBtnState("Up", this._maxValue != null && this._maxValue == val);
228 };
229 
230 DwtSpinner.prototype.setEnabled = function(enabled) {
231 	DwtControl.prototype.setEnabled.call(this, enabled);
232 	this.getInputElement().disabled = !enabled;
233 	var table = this.getHtmlElement().firstChild;
234 	if (!enabled)
235 		Dwt.addClass(table, "DwtSpinner-disabled");
236 	else
237 		Dwt.delClass(table, "DwtSpinner-disabled");
238 };
239 
240 DwtSpinner.prototype._rotateVal = function(direction) {
241 	var val = this.getValue();
242 	switch (direction) {
243 	    case "Up"   : val += this._step; break;
244 	    case "Down" : val -= this._step; break;
245 	}
246 	this.setValue(val);
247 };
248 
249 DwtSpinner.prototype._btnPressed = function(direction) {
250 	if (!this.getEnabled())
251 		return;
252 	Dwt.addClass(this._getButton(direction), "DwtSpinner-" + direction + "-pressed");
253 	this._direction = direction;
254 	this._rotateVal(direction);
255 	this._btnPressCapture.capture();
256 	this._timerSteps = 0;
257 	this._timer = setTimeout(this._timerFunc, DwtSpinner.INIT_TIMER);
258 };
259 
260 DwtSpinner.prototype._timerFunc = function() {
261 	var v1 = this.getValue();
262 	this._rotateVal(this._direction);
263 	var v2 = this.getValue();
264 	this._timerSteps++;
265 	var timeout = this._timerSteps > 4 ? DwtSpinner.FAST_TIMER : DwtSpinner.SLOW_TIMER;
266 	if (v1 != v2)
267 		this._timer = setTimeout(this._timerFunc, timeout);
268 	else
269 		this._stopCapture();
270 };
271 
272 DwtSpinner.prototype._stopCapture = function() {
273 	if (this._timer)
274 		clearTimeout(this._timer);
275 	this._timer = null;
276 	this._timerSteps = null;
277 	var direction = this._direction;
278 	Dwt.delClass(this._getButton(direction), "DwtSpinner-" + direction + "-pressed");
279 	this._direction = null;
280 	this._btnPressCapture.release();
281 	var input = this.getInputElement();
282 	input.focus();
283 	Dwt.setSelectionRange(input, 0, input.value.length);
284 };
285 
286 DwtSpinner.prototype.__onKeyPress = function(ev) {
287 	if (AjxEnv.isIE)
288 		ev = window.event;
289 	var dir = null;
290 	switch (ev.keyCode) {
291 	    case 38:
292 		dir = "Up";
293 		break;
294 	    case 40:
295 		dir = "Down";
296 		break;
297 	}
298 	if (dir) {
299 		this._rotateVal(dir);
300 		var input = this.getInputElement();
301 		Dwt.setSelectionRange(input, 0, input.value.length);
302 	}
303 };
304 
305 DwtSpinner.prototype.focus = function() {
306 	this.getInputElement().focus();
307 };
308 
309 DwtSpinner.prototype.select = function() {
310 	var input = this.getInputElement();
311 	input.focus();
312 	Dwt.setSelectionRange(input, 0, input.value.length);
313 };
314