Files
2026-05-13 09:43:32 -04:00

323 lines
12 KiB
JavaScript

/*
* Spinbox plugin for jQuery
* http://www.softwareunity.com/jquery/JQuerySpinBtn/
*
* Adds bells and whistles to any ordinary textbox to
* make it look and feel like a SpinBox Control.
*
* Copyright (c) 2006-2010 Software Unity Ltd
* Dual licensed under the MIT and GPL licenses:
* http://www.softwareunity.com/jquery/MIT-LICENSE.txt
* http://www.softwareunity.com/jquery/GPL-LICENSE.txt
*
* Originally written by George Adamson, Software Unity (george.jquery@softwareunity.com) August 2006.
* Modifications made by Mark Gibson, (mgibson@designlinks.net) September 2006.
* Rewritten and enhanced by George Adamson, Software Unity (george.jquery@softwareunity.com) October 2009.
*
* Tested in IE6, Opera9, Firefox 3.5.3
* v1.0 11 Aug 2006 - George Adamson - First release
* v1.1 Aug 2006 - George Adamson - Minor enhancements
* v1.2 27 Sep 2006 - Mark Gibson - Major enhancements
* v1.3a 28 Sep 2006 - George Adamson - Minor enhancements
* v2.0 20 Oct 2009 - George Adamson - Major refactor with breaking changes to options.
Sample usage:
// Create group of settings to initialise spinbutton(s). (Optional)
var myOptions = {
min: 0, // Set lower limit or null for no limit.
max: 100, // Set upper limit or null for no limit.
step: 0.5, // Set increment size.
spinboxClass: "mySpinBoxClass", // CSS class to style the spinbutton. (Class also specifies url of the up/down button image.)
upClass: "mySpinUpClass", // CSS class for style when mouse over up button.
downClass: "mySpinDnClass" // CSS class for style when mouse over down button.
}
jQuery(document).ready(function($){
// Initialise INPUT elements as SpinBoxes: (passing options if desired)
$("INPUT.spinbox").spinbox(myOptions);
});
*/
(function($){
$.fn.spinbox = function(options){
// Tidy up when spinbox('destroy') is called:
if( options && typeof(options)==="String" && options==="destroy" )
return this.unbind(".spinbox")
.removeClass(options.spinboxClass)
.removeClass(options.upClass).removeClass(options.downClass)
.removeClass(options.upHoverClass).removeClass(options.downHoverClass)
.removeData("spinboxMin").removeData("spinboxMax").removeData("spinboxReset")
.removeData("spinboxStep").removeData("spinboxBigStep")
;
// Apply specified options or defaults:
var undefined;
options = $.extend( {}, $.fn.spinbox.defaults, options );
// Store min, max & reset values for each element: (Reset defaults to element's initial value if option undefined)
this.each(function(){
var $box = $(this),
min = classData( "spinboxMin", this ),
max = classData( "spinboxMax", this ),
step = classData( "spinboxStep", this ),
bigStep = classData( "spinboxBigStep", this ),
reset = classData( "spinboxReset", this ),
scale = classData( "spinboxScale", this ); // AKA: Number of decimal places.
if( min===undefined ) min = firstNum( $box.attr("min"), options.min );
if( max===undefined ) max = firstNum( $box.attr("max"), options.max );
if( !step ) step = firstNum( $box.attr("step"), options.step );
if( !bigStep ) bigStep = firstNum( $box.attr("bigStep"), options.bigStep );
if( reset===undefined ) reset = firstNum( options.reset, $box.val(), min, max );
if( scale===undefined ) scale = $box.data("spinboxScale"); scale = ( scale || isNum(scale) ) ? scale : options.scale;
// When scale option is true, auto derive the number of decimalPlaces to round to:
if( scale === true ) scale = ( step.toString().split(".")[1] || "" ).length;
$box.data( "spinboxMin", min );
$box.data( "spinboxMax", max );
$box.data( "spinboxStep", step );
$box.data( "spinboxBigStep", bigStep );
$box.data( "spinboxReset", reset );
$box.data( "spinboxScale", scale );
});
return this.each(function(){
// Flags used while mouse is being moved or pressed:
var hoverUp, hoverDown, delayId, repeatId;
var $box = $(this);
// Bind event handlers for the spinbox:
$box.bind("mousemove.spinbox",function(e){
// Change css class when mouse is over an up/down button:
var $box = $(this);
var offset = $box.offset(); // Calculate element left & top;
var middle = $box.height() / 2; // Calculate midpoint between top and bottom of element;
var hover = ( e.pageX > offset.left + $box.width() - options.buttonWidth );
hoverUp = hover && ( e.pageY <= offset.top + middle );
hoverDown = hover && ( e.pageY > offset.top + middle );
$box.toggleClass( options.upHoverClass, hoverUp )
.toggleClass( options.downHoverClass, hoverDown );
// TODO: Stop autorepeat when mouse moves away from button?
})
.bind("mouseout.spinbox",function(){
// Reset up/down buttons to their normal state when mouse moves away:
stopAutorepeat();
$(this).removeClass( [ options.upClass, options.downClass, options.upHoverClass, options.downHoverClass ].join(" ") );
hoverUp = hoverDown = null;
})
.bind("mousedown.spinbox",function(e){
// Update the spinbox value and set up autorepeat to start after a short delay:
if( hoverUp || hoverDown ){
$(this).toggleClass( options.upClass, hoverUp ).toggleClass( options.downClass, hoverDown );
adjustValue.apply( this, [e] );
startAutorepeat(this,e);
};
})
.bind("mouseup.spinbox", function(e){
stopAutorepeat();
$(this).removeClass(options.upClass).removeClass(options.downClass);
})
//.bind("dblclick", function(e) {
// if ($.browser.msie)
// adjustValue.apply( this, [e, options.step, 1] );
//})
.bind("keydown.spinbox",function(e){
// Filter key press by allowable options.keys if specified:
if( !options.keys ||
$.grep(options.keys, function(key){
return key === e.keyCode || ( key instanceof RegExp && key.test( String.fromCharCode(e.keyCode) ) );
}).length ){
// Define arry key codes and decide whether to use options.bigStep when Shift is pressed:
var $box = $(this);
var key = { up:38, down:40, pageUp:33, pageDown:34 };
var bigStep = $box.data("spinboxBigStep");
var step = e.shiftKey ? bigStep : $box.data("spinboxStep");
// Respond to up/down arrow keys and pageUp/pageDown:
switch(e.keyCode){
case key.up: adjustValue.apply( this, [e, step, 1] ); break;
case key.down: adjustValue.apply( this, [e, step, -1] ); break;
case key.pageUp: adjustValue.apply( this, [e, bigStep, 1] ); break;
case key.pageDown: adjustValue.apply( this, [e, bigStep, -1] ); break;
};
}
else
// Cancel event if keys filter was specified but pressed key is not on the list:
return !options.keys;
})
.bind("change.spinbox", function(e){
adjustValue.apply( this, [e,0] );
})
.addClass(options.spinboxClass);
// React to mousewheel if desired:
if( options.mousewheel ){
$(this).bind("mousewheel.spinbox DOMMouseScroll.spinbox", function(e){
var step = e.shiftKey ? $(this).data("spinboxBigStep") : $(this).data("spinboxStep");
// Respond to mouse wheel: (Allow for IE/Opera e.wheelDelta and W3C e.detail)
if ( e.detail < 0 || e.wheelDelta >= 120 )
adjustValue.apply( this, [e, step, 1] );
else if ( e.detail > 0 || e.wheelDelta <= -120 )
adjustValue.apply( this, [e, step, -1] );
return false;
});
}
// Initialise the current value, ensuring it is within min/max etc:
adjustValue.apply( this, [$.Event(),0] );
// Helper called in response to click or mousewheel etc to apply step change:
function adjustValue(e,step,direction){
var $box = $(this); if( options.ignore && $box.is(options.ignore) ) return;
step = firstNum( step, $box.data("spinboxStep"), 1 );
direction = direction || (hoverDown ? -1 : 1);
var oldVal = $box.val();
var val = firstNum( oldVal, $box.data("spinboxReset"), 0 );
var min = firstNum( $box.data("spinboxMin") );
var max = firstNum( $box.data("spinboxMax") );
var data = [val,step,min,max,direction,oldVal,options];
if( direction > 0 ) val = options.increment.apply( this,data ); else
if( direction < 0 ) val = options.decrement.apply( this,data );
if( isNum($box.data("spinboxScale")) && options.round ) val = options.round(val,$box.data("spinboxScale"));
if( isNum(min) ) val = Math.max(val,min);
if( isNum(max) ) val = Math.min(val,max);
var data = [val,step,min,max,direction,oldVal,options];
if( val != oldVal && $box.triggerHandler("beforeSpin",data) !== false ){
$box.val(val);
if( options.change ) $box.trigger("change",data);
$box.triggerHandler("spin",data);
};
};
// Helper to begin autorepeat when mouse is held down:
function startAutorepeat(elem,e){
stopAutorepeat();
// Start timer for initial delay:
delayId = window.setTimeout(function(){
adjustValue.apply( elem, [e] );
// Start timer for repeating:
repeatId = window.setInterval(function(){
adjustValue.apply( elem, [e] );
},options.repeat);
},options.delay);
};
// Helper to end autorepeat when mouse is released:
function stopAutorepeat(){
window.clearTimeout(delayId);
window.clearInterval(repeatId);
};
});
};
// Helper functions:
// More reliable alternative to isNaN and isFinite:
function isNum(num){
return !isNaN(parseFloat(num))
}
// Helper to return the first parameter that is a valid number:
function firstNum(args){
for( var i=0; i<arguments.length; i++ ){
if( isNum(arguments[i]) ) return Number( parseFloat(arguments[i]) );
};
return;
};
// Helper to extract settings stored in css class attribute string: (Eg: when class="spinbox spinboxStep0.5")
function classData(attr,elem){
var classes = $(elem||this).attr("class"),
match = new RegExp( "(\\b" + attr + ")(\\S*)" ).exec(classes),
lookupBoolean = { "true":true, "True":true, "false":false, "False":false };
// Convert "True" or "False" string to boolean:
if( match && match.length >= 3 && lookupBoolean[match[3]] !== undefined )
match[3] = lookupBoolean[ match[3] ];
return !match ? undefined : // attr not found in class string.
match.length >= 3 ? match[2] : // Specific value.
null; // attr specified but value deliberately blank.
};
$.fn.spinbox.defaults = {
min : 0, // Lower limit or null.
max : null, // Upper limit or null.
step : 1, // Size of standard increment.
bigStep : 10, // Size of increment when Shift key is held down or when pageUp/pagedown is pressed.
keys : [ /[0-9]/,9,13,8,46,33,34,37,38,39,40,109,188,190 ], // Array of regular expressions and/or char codes to restrict key input. Default: 0-9, Tab, Enter, Backspace, Delete, PageUp, PageDown, Left, Up, Right, Down, Minus, Comma, Dot.
ignore : "[readonly],[disabled]", // Spinbox will not respond on elements matching this CSS selector.
spinboxClass : 'spinbox-active', // Added to element when spinbox is initialised. Typically used to apply button image through css.
upClass : 'spinbox-up', // Added to element while mouse is depressed on the Up button.
downClass : 'spinbox-down', // Added to element while mouse is depressed on the Down button.
upHoverClass : 'spinbox-up-hover', // Added to element while mouse is over the Up button.
downHoverClass : 'spinbox-down-hover', // Added to element while mouse is over the Down button.
mousewheel : true, // When true, spinbox will react to mousehweel.
change : true, // When true, spinbox will trigger change event as it spins. (Otherwise change event is fired in the same ways as any normal textbox)
increment : function(val,step,min,max,options){ return val + step; }, // Custom function to calculate the value increment.
decrement : function(val,step,min,max,options){ return val - step; }, // Custom function to calculate the value decrement.
reset : null, // Value used when element value is invalid. Specify null to default to element's initial value;
delay : 500, // Initial delay before auto-repeat when mouse button held down. (Milliseconds)
repeat : 100, // Interval between auto-repeats when mouse button held down. (Milliseconds)
buttonWidth : 20, // Width of the button sprite image (so we can decide when mouse is over it).
scale : true, // Specify number of DP, or true to auto derive from options.step (Eg: when step is 0.125 then DP will be 3)
round : function round(num,dp) { // Specify your own decimalPlaces rounding function if you don't like this default one.
return Math.round( num * Math.pow(10,dp) ) / Math.pow(10,dp);
}
};
})(jQuery);