/* Copyright (c) 2004-2006, The Dojo Foundation All Rights Reserved. Licensed under the Academic Free License version 2.1 or above OR the modified BSD license. For more information on Dojo licensing, see: http://dojotoolkit.org/community/licensing.shtml */ dojo.provide("dojo.widget.Spinner"); dojo.require("dojo.io.*"); dojo.require("dojo.lfx.*"); dojo.require("dojo.html.*"); dojo.require("dojo.html.layout"); dojo.require("dojo.string"); dojo.require("dojo.widget.*"); dojo.require("dojo.widget.IntegerTextbox"); dojo.require("dojo.widget.RealNumberTextbox"); dojo.require("dojo.widget.DateTextbox"); dojo.require("dojo.experimental"); dojo.declare( "dojo.widget.Spinner", null, { // summary: Mixin for validation widgets with a spinner // description: This class basically (conceptually) extends dojo.widget.ValidationTextbox. // It modifies the template to have up/down arrows, and provides related handling code. _typamaticTimer: null, _typamaticFunction: null, _currentTimeout: this.defaultTimeout, _eventCount: 0, // defaultTimeout: Number // number of milliseconds before a held key or button becomes typematic defaultTimeout: 500, // timeoutChangeRate: Number // fraction of time used to change the typematic timer between events // 1.0 means that each typematic event fires at defaultTimeout intervals // < 1.0 means that each typematic event fires at an increasing faster rate timeoutChangeRate: 0.90, templatePath: dojo.uri.dojoUri("src/widget/templates/Spinner.html"), templateCssPath: dojo.uri.dojoUri("src/widget/templates/Spinner.css"), // incrementSrc: String // up arrow graphic URL incrementSrc: dojo.uri.dojoUri("src/widget/templates/images/spinnerIncrement.gif"), // decrementSrc: String // down arrow graphic URL decrementSrc: dojo.uri.dojoUri("src/widget/templates/images/spinnerDecrement.gif"), // does the keyboard related stuff _handleKeyEvents: function(/*Event*/ evt){ if(!evt.key){ return; } if(!evt.ctrlKey && !evt.altKey){ switch(evt.key){ case evt.KEY_DOWN_ARROW: dojo.event.browser.stopEvent(evt); this._downArrowPressed(evt); return; case evt.KEY_UP_ARROW: dojo.event.browser.stopEvent(evt); this._upArrowPressed(evt); return; } } this._eventCount++; }, _onSpinnerKeyUp: function(/*Event*/ evt){ this._arrowReleased(evt); this.onkeyup(evt); }, // reset button size; this function is called when the input area has changed size _resize: function(){ var inputSize = dojo.html.getBorderBox(this.textbox); this.buttonSize = { width: inputSize.height / 2, height: inputSize.height / 2 }; if(this.upArrowNode){ dojo.html.setMarginBox(this.upArrowNode, this.buttonSize); dojo.html.setMarginBox(this.downArrowNode, this.buttonSize); } }, _pressButton: function(/*DomNode*/ node){ node.style.borderWidth = "1px 0px 0px 1px"; node.style.borderStyle = "inset"; }, _releaseButton: function(/*DomNode*/ node){ node.style.borderWidth = "0px 1px 1px 0px"; node.style.borderStyle = "outset"; }, _arrowPressed: function(/*Event*/ evt, /*Number*/ direction){ var nodePressed = (direction == -1) ? this.downArrowNode : this.upArrowNode; var nodeReleased = (direction == +1) ? this.downArrowNode : this.upArrowNode; if(typeof evt != "number"){ if(this._typamaticTimer != null){ if(this._typamaticNode == nodePressed){ return; } dojo.lang.clearTimeout(this._typamaticTimer); } this._releaseButton(nodeReleased); this._eventCount++; this._typamaticTimer = null; this._currentTimeout = this.defaultTimeout; }else if (evt != this._eventCount){ this._releaseButton(nodePressed); return; } this._pressButton(nodePressed); this._setCursorX(this.adjustValue(direction,this._getCursorX())); this._typamaticNode = nodePressed; this._typamaticTimer = dojo.lang.setTimeout(this, "_arrowPressed", this._currentTimeout, this._eventCount, direction); this._currentTimeout = Math.round(this._currentTimeout * this.timeoutChangeRate); }, _downArrowPressed: function(/*Event*/ evt){ return this._arrowPressed(evt,-1); }, // IE sends these events when rapid clicking, mimic an extra single click _downArrowDoubleClicked: function(/*Event*/ evt){ var rc = this._downArrowPressed(evt); dojo.lang.setTimeout(this, "_arrowReleased", 50, null); return rc; }, _upArrowPressed: function(/*Event*/ evt){ return this._arrowPressed(evt,+1); }, // IE sends these events when rapid clicking, mimic an extra single click _upArrowDoubleClicked: function(/*Event*/ evt){ var rc = this._upArrowPressed(evt); dojo.lang.setTimeout(this, "_arrowReleased", 50, null); return rc; }, _arrowReleased: function(/*Event*/ evt){ this.textbox.focus(); if(evt != null && typeof evt == "object" && evt.keyCode && evt.keyCode != null){ var keyCode = evt.keyCode; var k = dojo.event.browser.keys; switch(keyCode){ case k.KEY_DOWN_ARROW: case k.KEY_UP_ARROW: dojo.event.browser.stopEvent(evt); break; } } this._releaseButton(this.upArrowNode); this._releaseButton(this.downArrowNode); this._eventCount++; if(this._typamaticTimer != null){ dojo.lang.clearTimeout(this._typamaticTimer); } this._typamaticTimer = null; this._currentTimeout = this.defaultTimeout; }, _mouseWheeled: function(/*Event*/ evt){ var scrollAmount = 0; if(typeof evt.wheelDelta == 'number'){ // IE scrollAmount = evt.wheelDelta; }else if (typeof evt.detail == 'number'){ // Mozilla+Firefox scrollAmount = -evt.detail; } if(scrollAmount > 0){ this._upArrowPressed(evt); this._arrowReleased(evt); }else if (scrollAmount < 0){ this._downArrowPressed(evt); this._arrowReleased(evt); } }, _discardEvent: function(/*Event*/ evt){ dojo.event.browser.stopEvent(evt); }, _getCursorX: function(){ var x = -1; try{ this.textbox.focus(); if (typeof this.textbox.selectionEnd == "number"){ x = this.textbox.selectionEnd; }else if (document.selection && document.selection.createRange){ var range = document.selection.createRange().duplicate(); if(range.parentElement() == this.textbox){ range.moveStart('textedit', -1); x = range.text.length; } } }catch(e){ /* squelch! */ } return x; }, _setCursorX: function(/*Number*/ x){ try{ this.textbox.focus(); if(!x){ x = 0; } if(typeof this.textbox.selectionEnd == "number"){ this.textbox.selectionEnd = x; }else if(this.textbox.createTextRange){ var range = this.textbox.createTextRange(); range.collapse(true); range.moveEnd('character', x); range.moveStart('character', x); range.select(); } }catch(e){ /* squelch! */ } }, _spinnerPostMixInProperties: function(/*Object*/ args, /*Object*/ frag){ // summary: the widget's postMixInProperties() method should call this method // set image size before instantiating template; // changing it aftwards doesn't work on FF var inputNode = this.getFragNodeRef(frag); var inputSize = dojo.html.getBorderBox(inputNode); this.buttonSize = { width: inputSize.height / 2 - 1, height: inputSize.height / 2 - 1}; }, _spinnerPostCreate: function(/*Object*/ args, /*Object*/ frag){ // summary: the widget's postCreate() method should call this method // extra listeners if(this.textbox.addEventListener){ // dojo.event.connect() doesn't seem to work with DOMMouseScroll this.textbox.addEventListener('DOMMouseScroll', dojo.lang.hitch(this, "_mouseWheeled"), false); // Mozilla + Firefox + Netscape }else{ dojo.event.connect(this.textbox, "onmousewheel", this, "_mouseWheeled"); // IE + Safari } //dojo.event.connect(window, "onchange", this, "_resize"); } } ); dojo.widget.defineWidget( "dojo.widget.IntegerSpinner", [dojo.widget.IntegerTextbox, dojo.widget.Spinner], { // summary: an IntegerTextbox with +/- buttons // delta: Number // increment amount delta: "1", postMixInProperties: function(/*Object*/ args, /*Object*/ frag){ dojo.widget.IntegerSpinner.superclass.postMixInProperties.apply(this, arguments); this._spinnerPostMixInProperties(args, frag); }, postCreate: function(/*Object*/ args, /*Object*/ frag){ dojo.widget.IntegerSpinner.superclass.postCreate.apply(this, arguments); this._spinnerPostCreate(args, frag); }, adjustValue: function(/*Number*/ direction, /*Number*/ x){ // sumary // spin the input field // direction < 0: spin down // direction > 0: spin up // direction = 0: revalidate existing value var val = this.getValue().replace(/[^\-+\d]/g, ""); if(val.length == 0){ return; } var num = Math.min(Math.max((parseInt(val)+(parseInt(this.delta) * direction)), (this.flags.min?this.flags.min:-Infinity)), (this.flags.max?this.flags.max:+Infinity)); val = num.toString(); if(num >= 0){ val = ((this.flags.signed == true)?'+':' ')+val; // make sure first char is a nondigit } if(this.flags.separator.length > 0){ for (var i=val.length-3; i > 1; i-=3){ val = val.substr(0,i)+this.flags.separator+val.substr(i); } } if(val.substr(0,1) == ' '){ val = val.substr(1); } // remove space this.setValue(val); return val.length; } }); dojo.widget.defineWidget( "dojo.widget.RealNumberSpinner", [dojo.widget.RealNumberTextbox, dojo.widget.Spinner], function(){ dojo.experimental("dojo.widget.RealNumberSpinner"); }, { // summary // A RealNumberTextbox with +/- buttons // delta: Number // amount that pushing a button changes the value? delta: "1e1", postMixInProperties: function(/*Object*/ args, /*Object*/ frag){ dojo.widget.RealNumberSpinner.superclass.postMixInProperties.apply(this, arguments); this._spinnerPostMixInProperties(args, frag); }, postCreate: function(/*Object*/ args, /*Object*/ frag){ dojo.widget.RealNumberSpinner.superclass.postCreate.apply(this, arguments); this._spinnerPostCreate(args, frag); }, adjustValue: function(/*Number*/ direction, /*Number*/ x){ var val = this.getValue().replace(/[^\-+\.eE\d]/g, ""); if(!val.length){ return; } var num = parseFloat(val); if(isNaN(num)){ return; } var delta = this.delta.split(/[eE]/); if(!delta.length){ delta = [1, 1]; }else{ delta[0] = parseFloat(delta[0].replace(/[^\-+\.\d]/g, "")); if(isNaN(delta[0])){ delta[0] = 1; } if(delta.length > 1){ delta[1] = parseInt(delta[1]); } if(isNaN(delta[1])){ delta[1] = 1; } } val = this.getValue().split(/[eE]/); if(!val.length){ return; } var numBase = parseFloat(val[0].replace(/[^\-+\.\d]/g, "")); if(val.length == 1){ var numExp = 0; }else{ var numExp = parseInt(val[1].replace(/[^\-+\d]/g, "")); } if(x <= val[0].length){ x = 0; numBase += delta[0] * direction; }else{ x = Number.MAX_VALUE; numExp += delta[1] * direction; if(this.flags.eSigned == false && numExp < 0){ numExp = 0; } } num = Math.min(Math.max((numBase * Math.pow(10,numExp)), (this.flags.min?this.flags.min:-Infinity)), (this.flags.max?this.flags.max:+Infinity)); if((this.flags.exponent == true || (this.flags.exponent != false && x != 0)) && num.toExponential){ if (isNaN(this.flags.places) || this.flags.places == Infinity){ val = num.toExponential(); }else{ val = num.toExponential(this.flags.places); } }else if(num.toFixed && num.toPrecision){ if(isNaN(this.flags.places) || this.flags.places == Infinity){ val = num.toPrecision((1/3).toString().length-1); }else{ val = num.toFixed(this.flags.places); } }else{ val = num.toString(); } if(num >= 0){ if(this.flags.signed == true){ val = '+' + val; } } val = val.split(/[eE]/); if(this.flags.separator.length > 0){ if(num >= 0 && val[0].substr(0,1) != '+'){ val[0] = ' ' + val[0]; // make sure first char is nondigit for easy algorithm } var i = val[0].lastIndexOf('.'); if(i >= 0){ i -= 3; }else{ i = val[0].length-3; } for (; i > 1; i-=3){ val[0] = val[0].substr(0,i)+this.flags.separator+val[0].substr(i); } if(val[0].substr(0,1) == ' '){ val[0] = val[0].substr(1); } // remove space } if(val.length > 1){ if((this.flags.eSigned == true)&&(val[1].substr(0,1) != '+')){ val[1] = '+' + val[1]; }else if((!this.flags.eSigned)&&(val[1].substr(0,1) == '+')){ val[1] = val[1].substr(1); }else if((!this.flags.eSigned)&&(val[1].substr(0,1) == '-')&&(num.toFixed && num.toPrecision)){ if(isNaN(this.flags.places)){ val[0] = num.toPrecision((1/3).toString().length-1); }else{ val[0] = num.toFixed(this.flags.places).toString(); } val[1] = "0"; } val[0] += 'e' + val[1]; } this.setValue(val[0]); if(x > val[0].length){ x = val[0].length; } return x; } }); dojo.widget.defineWidget( "dojo.widget.TimeSpinner", [dojo.widget.TimeTextbox, dojo.widget.Spinner], function(){ dojo.experimental("dojo.widget.TimeSpinner"); }, { postMixInProperties: function(/*Object*/ args, /*Object*/ frag){ dojo.widget.TimeSpinner.superclass.postMixInProperties.apply(this, arguments); this._spinnerPostMixInProperties(args, frag); }, postCreate: function(/*Object*/ args, /*Object*/ frag){ dojo.widget.TimeSpinner.superclass.postCreate.apply(this, arguments); this._spinnerPostCreate(args, frag); }, adjustValue: function(/*Number*/ direction, /*Number*/ x){ //FIXME: formatting should make use of dojo.date.format? var val = this.getValue(); var format = (this.flags.format && this.flags.format.search(/[Hhmst]/) >= 0) ? this.flags.format : "hh:mm:ss t"; if(direction == 0 || !val.length || !this.isValid()){ return; } if (!this.flags.amSymbol){ this.flags.amSymbol = "AM"; } if (!this.flags.pmSymbol){ this.flags.pmSymbol = "PM"; } var re = dojo.regexp.time(this.flags); var qualifiers = format.replace(/H/g,"h").replace(/[^hmst]/g,"").replace(/([hmst])\1/g,"$1"); var hourPos = qualifiers.indexOf('h') + 1; var minPos = qualifiers.indexOf('m') + 1; var secPos = qualifiers.indexOf('s') + 1; var ampmPos = qualifiers.indexOf('t') + 1; // tweak format to match the incoming data exactly to help find where the cursor is var cursorFormat = format; var ampm = ""; if (ampmPos > 0){ ampm = val.replace(new RegExp(re),"$"+ampmPos); cursorFormat = cursorFormat.replace(/t+/, ampm.replace(/./g,"t")); } var hour = 0; var deltaHour = 1; if (hourPos > 0){ hour = val.replace(new RegExp(re),"$"+hourPos); if (dojo.lang.isString(this.delta)){ deltaHour = this.delta.replace(new RegExp(re),"$"+hourPos); } if (isNaN(deltaHour)){ deltaHour = 1; } else { deltaHour = parseInt(deltaHour); } if (hour.length == 2){ cursorFormat = cursorFormat.replace(/([Hh])+/, "$1$1"); } else { cursorFormat = cursorFormat.replace(/([Hh])+/, "$1"); } if (isNaN(hour)){ hour = 0; } else { hour = parseInt(hour.replace(/^0(\d)/,"$1")); } } var min = 0; var deltaMin = 1; if (minPos > 0){ min = val.replace(new RegExp(re),"$"+minPos); if (dojo.lang.isString(this.delta)){ deltaMin = this.delta.replace(new RegExp(re),"$"+minPos); } if (isNaN(deltaMin)){ deltaMin = 1; } else { deltaMin = parseInt(deltaMin); } cursorFormat = cursorFormat.replace(/m+/, min.replace(/./g,"m")); if (isNaN(min)){ min = 0; } else { min = parseInt(min.replace(/^0(\d)/,"$1")); } } var sec = 0; var deltaSec = 1; if (secPos > 0){ sec = val.replace(new RegExp(re),"$"+secPos); if (dojo.lang.isString(this.delta)){ deltaSec = this.delta.replace(new RegExp(re),"$"+secPos); } if (isNaN(deltaSec)){ deltaSec = 1; } else { deltaSec = parseInt(deltaSec); } cursorFormat = cursorFormat.replace(/s+/, sec.replace(/./g,"s")); if (isNaN(sec)){ sec = 0; } else { sec = parseInt(sec.replace(/^0(\d)/,"$1")); } } if (isNaN(x) || x >= cursorFormat.length){ x = cursorFormat.length-1; } var cursorToken = cursorFormat.charAt(x); switch(cursorToken){ case 't': if (ampm == this.flags.amSymbol){ ampm = this.flags.pmSymbol; } else if (ampm == this.flags.pmSymbol){ ampm = this.flags.amSymbol; } break; default: if (hour >= 1 && hour < 12 && ampm == this.flags.pmSymbol){ hour += 12; } if (hour == 12 && ampm == this.flags.amSymbol){ hour = 0; } switch(cursorToken){ case 's': sec += deltaSec * direction; while (sec < 0){ min--; sec += 60; } while (sec >= 60){ min++; sec -= 60; } case 'm': if (cursorToken == 'm'){ min += deltaMin * direction; } while (min < 0){ hour--; min += 60; } while (min >= 60){ hour++; min -= 60; } case 'h': case 'H': if (cursorToken == 'h' || cursorToken == 'H'){ hour += deltaHour * direction; } while (hour < 0){ hour += 24; } while (hour >= 24){ hour -= 24; } break; default: // should never get here return; } if (hour >= 12){ ampm = this.flags.pmSymbol; if (format.indexOf('h') >= 0 && hour >= 13){ hour -= 12; } } else { ampm = this.flags.amSymbol; if (format.indexOf('h') >= 0 && hour == 0){ hour = 12; } } } cursorFormat = format; if (hour >= 0 && hour < 10 && format.search(/[hH]{2}/) >= 0){ hour = "0" + hour.toString(); } if (hour >= 10 && cursorFormat.search(/[hH]{2}/) < 0 ){ cursorFormat = cursorFormat.replace(/(h|H)/, "$1$1"); } if (min >= 0 && min < 10 && cursorFormat.search(/mm/) >= 0){ min = "0" + min.toString(); } if (min >= 10 && cursorFormat.search(/mm/) < 0 ){ cursorFormat = cursorFormat.replace(/m/, "$1$1"); } if (sec >= 0 && sec < 10 && cursorFormat.search(/ss/) >= 0){ sec = "0" + sec.toString(); } if (sec >= 10 && cursorFormat.search(/ss/) < 0 ){ cursorFormat = cursorFormat.replace(/s/, "$1$1"); } x = cursorFormat.indexOf(cursorToken); if (x == -1){ x = format.length; } format = format.replace(/[hH]+/, hour); format = format.replace(/m+/, min); format = format.replace(/s+/, sec); format = format.replace(/t/, ampm); this.setValue(format); if(x > format.length){ x = format.length; } return x; } });