/** * jquery number plug-in 2.1.0 * copyright 2012, digital fusion * licensed under the mit license. * http://opensource.teamdf.com/license/ * * a jquery plugin which implements a permutation of phpjs.org's number_format to provide * simple number formatting, insertion, and as-you-type masking of a number. * * @author sam sehnert * @docs http://www.teamdf.com/web/jquery-number-format-redux/196/ */ (function($){ /** * method for selecting a range of characters in an input/textarea. * * @param int rangestart : where we want the selection to start. * @param int rangeend : where we want the selection to end. * * @return void; */ function setselectionrange( rangestart, rangeend ) { // check which way we need to define the text range. if( this.createtextrange ) { var range = this.createtextrange(); range.collapse( true ); range.movestart( 'character', rangestart ); range.moveend( 'character', rangeend-rangestart ); range.select(); } // alternate setselectionrange method for supporting browsers. else if( this.setselectionrange ) { this.focus(); this.setselectionrange( rangestart, rangeend ); } } /** * get the selection position for the given part. * * @param string part : options, 'start' or 'end'. the selection position to get. * * @return int : the index position of the selection part. */ function getselection( part ) { var pos = this.value.length; // work out the selection part. part = ( part.tolowercase() == 'start' ? 'start' : 'end' ); if( document.selection ){ // the current selection var range = document.selection.createrange(), stored_range, selectionstart, selectionend; // we'll use this as a 'dummy' stored_range = range.duplicate(); // select all text //stored_range.movetoelementtext( this ); stored_range.expand('textedit'); // now move 'dummy' end point to end point of original range stored_range.setendpoint( 'endtoend', range ); // now we can calculate start and end points selectionstart = stored_range.text.length - range.text.length; selectionend = selectionstart + range.text.length; return part == 'start' ? selectionstart : selectionend; } else if(typeof(this['selection'+part])!="undefined") { pos = this['selection'+part]; } return pos; } /** * substitutions for keydown keycodes. * allows conversion from e.which to ascii characters. */ var _keydown = { codes : { 188 : 44, 109 : 45, 190 : 46, 191 : 47, 192 : 96, 220 : 92, 222 : 39, 221 : 93, 219 : 91, 173 : 45, 187 : 61, //ie key codes 186 : 59, //ie key codes 189 : 45, //ie key codes 110 : 46 //ie key codes }, shifts : { 96 : "~", 49 : "!", 50 : "@", 51 : "#", 52 : "$", 53 : "%", 54 : "^", 55 : "&", 56 : "*", 57 : "(", 48 : ")", 45 : "_", 61 : "+", 91 : "{", 93 : "}", 92 : "|", 59 : ":", 39 : "\"", 44 : "<", 46 : ">", 47 : "?" } }; /** * jquery number formatter plugin. this will allow you to format numbers on an element. * * @params proxied for format_number method. * * @return : the jquery collection the method was called with. */ $.fn.number = function( number, decimals, dec_point, thousands_sep ){ // enter the default thousands separator, and the decimal placeholder. thousands_sep = (typeof thousands_sep === 'undefined') ? ',' : thousands_sep; dec_point = (typeof dec_point === 'undefined') ? '.' : dec_point; decimals = (typeof decimals === 'undefined' ) ? 0 : decimals; // work out the unicode character for the decimal placeholder. var u_dec = ('\\u'+('0000'+(dec_point.charcodeat(0).tostring(16))).slice(-4)), regex_dec_num = new regexp('[^'+u_dec+'0-9]','g'), regex_dec = new regexp(u_dec,'g'); // if we've specified to take the number from the target element, // we loop over the collection, and get the number. if( number === true ) { // if this element is a number, then we add a keyup if( this.is('input:text') ) { // return the jquery collection. return this.on({ /** * handles keyup events, re-formatting numbers. * * @param object e : the keyup event object.s * * @return void; */ 'keydown.format' : function(e){ // define variables used in the code below. var $this = $(this), data = $this.data('numformat'), code = (e.keycode ? e.keycode : e.which), chara = '', //unescape(e.originalevent.keyidentifier.replace('u+','%u')), start = getselection.apply(this,['start']), end = getselection.apply(this,['end']), val = '', setpos = false; // webkit (chrome & safari) on windows screws up the keyidentifier detection // for numpad characters. i've disabled this for now, because while keycode munging // below is hackish and ugly, it actually works cross browser & platform. // if( typeof e.originalevent.keyidentifier !== 'undefined' ) // { // chara = unescape(e.originalevent.keyidentifier.replace('u+','%u')); // } // else // { if (_keydown.codes.hasownproperty(code)) { code = _keydown.codes[code]; } if (!e.shiftkey && (code >= 65 && code <= 90)){ code += 32; } else if (!e.shiftkey && (code >= 69 && code <= 105)){ code -= 48; } else if (e.shiftkey && _keydown.shifts.hasownproperty(code)){ //get shifted keycode value chara = _keydown.shifts[code]; } if( chara == '' ) chara = string.fromcharcode(code); // } // stop executing if the user didn't type a number key, a decimal character, or backspace. if( code !== 8 && chara != dec_point && !chara.match(/[0-9]/) ) { // we need the original keycode now... var key = (e.keycode ? e.keycode : e.which); if( // allow control keys to go through... (delete, etc) key == 46 || key == 8 || key == 9 || key == 27 || key == 13 || // allow: ctrl+a, ctrl+r ( (key == 65 || key == 82 ) && ( e.ctrlkey || e.metakey ) === true ) || // allow: home, end, left, right ( (key >= 35 && key <= 39) ) ){ return; } // but prevent all other keys. e.preventdefault(); return false; } //console.log('continuing on: ', code, chara); // the whole lot has been selected, or if the field is empty, and the character if( ( start == 0 && end == this.value.length || $this.val() == 0 ) && !e.metakey && !e.ctrlkey && !e.altkey && chara.length === 1 && chara != 0 ) { // blank out the field, but only if the data object has already been instanciated. start = end = 1; this.value = ''; // reset the cursor position. data.init = (decimals>0?-1:0); data.c = (decimals>0?-(decimals+1):0); setselectionrange.apply(this, [0,0]); } // otherwise, we need to reset the caret position // based on the users selection. else { data.c = end-this.value.length; } // if the start position is before the decimal point, // and the user has typed a decimal point, we need to move the caret // past the decimal place. if( decimals > 0 && chara == dec_point && start == this.value.length-decimals-1 ) { data.c++; data.init = math.max(0,data.init); e.preventdefault(); // set the selection position. setpos = this.value.length+data.c; } // if the user is just typing the decimal place, // we simply ignore it. else if( chara == dec_point ) { data.init = math.max(0,data.init); e.preventdefault(); } // if hitting the delete key, and the cursor is behind a decimal place, // we simply move the cursor to the other side of the decimal place. else if( decimals > 0 && code == 8 && start == this.value.length-decimals ) { e.preventdefault(); data.c--; // set the selection position. setpos = this.value.length+data.c; } // if hitting the delete key, and the cursor is to the right of the decimal // (but not directly to the right) we replace the character preceeding the // caret with a 0. else if( decimals > 0 && code == 8 && start > this.value.length-decimals ) { if( this.value === '' ) return; // if the character preceeding is not already a 0, // replace it with one. if( this.value.slice(start-1, start) != '0' ) { val = this.value.slice(0, start-1) + '0' + this.value.slice(start); $this.val(val.replace(regex_dec_num,'').replace(regex_dec,dec_point)); } e.preventdefault(); data.c--; // set the selection position. setpos = this.value.length+data.c; } // if the delete key was pressed, and the character immediately // before the caret is a thousands_separator character, simply // step over it. else if( code == 8 && this.value.slice(start-1, start) == thousands_sep ) { e.preventdefault(); data.c--; // set the selection position. setpos = this.value.length+data.c; } // if the caret is to the right of the decimal place, and the user is entering a // number, remove the following character before putting in the new one. else if( decimals > 0 && start == end && this.value.length > decimals+1 && start > this.value.length-decimals-1 && isfinite(+chara) && !e.metakey && !e.ctrlkey && !e.altkey && chara.length === 1 ) { // if the character preceeding is not already a 0, // replace it with one. if( end === this.value.length ) { val = this.value.slice(0, start-1); } else { val = this.value.slice(0, start)+this.value.slice(start+1); } // reset the position. this.value = val; setpos = start; } // if we need to re-position the characters. if( setpos !== false ) { //console.log('setpos keydown: ', setpos ); setselectionrange.apply(this, [setpos, setpos]); } // store the data on the element. $this.data('numformat', data); }, /** * handles keyup events, re-formatting numbers. * * @param object e : the keyup event object.s * * @return void; */ 'keyup.format' : function(e){ // store these variables for use below. var $this = $(this), data = $this.data('numformat'), code = (e.keycode ? e.keycode : e.which), start = getselection.apply(this,['start']), setpos; // stop executing if the user didn't type a number key, a decimal, or a comma. if( this.value === '' || (code < 48 || code > 57) && (code < 96 || code > 105 ) && code !== 8 ) return; // re-format the textarea. $this.val($this.val()); if( decimals > 0 ) { // if we haven't marked this item as 'initialised' // then do so now. it means we should place the caret just // before the decimal. this will never be un-initialised before // the decimal character itself is entered. if( data.init < 1 ) { start = this.value.length-decimals-( data.init < 0 ? 1 : 0 ); data.c = start-this.value.length; data.init = 1; $this.data('numformat', data); } // increase the cursor position if the caret is to the right // of the decimal place, and the character pressed isn't the delete key. else if( start > this.value.length-decimals && code != 8 ) { data.c++; // store the data, now that it's changed. $this.data('numformat', data); } } //console.log( 'setting pos: ', start, decimals, this.value.length + data.c, this.value.length, data.c ); // set the selection position. setpos = this.value.length+data.c; setselectionrange.apply(this, [setpos, setpos]); }, /** * reformat when pasting into the field. * * @param object e : jquery event object. * * @return false : prevent default action. */ 'paste.format' : function(e){ // defint $this. it's used twice!. var $this = $(this), original = e.originalevent, val = null; // get the text content stream. if (window.clipboarddata && window.clipboarddata.getdata) { // ie val = window.clipboarddata.getdata('text'); } else if (original.clipboarddata && original.clipboarddata.getdata) { val = original.clipboarddata.getdata('text/plain'); } // do the reformat operation. $this.val(val); // stop the actual content from being pasted. e.preventdefault(); return false; } }) // loop each element (which isn't blank) and do the format. .each(function(){ var $this = $(this).data('numformat',{ c : -(decimals+1), decimals : decimals, thousands_sep : thousands_sep, dec_point : dec_point, regex_dec_num : regex_dec_num, regex_dec : regex_dec, init : false }); // return if the element is empty. if( this.value === '' ) return; // otherwise... format!! $this.val($this.val()); }); } else { // return the collection. return this.each(function(){ var $this = $(this), num = +$this.text().replace(regex_dec_num,'').replace(regex_dec,'.'); $this.number( !isfinite(num) ? 0 : +num, decimals, dec_point, thousands_sep ); }); } } // add this number to the element as text. return this.text( $.number.apply(window,arguments) ); }; // // create .val() hooks to get and set formatted numbers in inputs. // // we check if any hooks already exist, and cache // them in case we need to re-use them later on. var orighookget = null, orighookset = null; // check if a text valhook already exists. if( $.valhooks.text ) { // preserve the original valhook function // we'll call this for values we're not // explicitly handling. orighookget = $.valhooks.text.get; orighookset = $.valhooks.text.set; } else { // define an object for the new valhook. $.valhooks.text = {}; } /** * define the valhook to return normalised field data against an input * which has been tagged by the number formatter. * * @param object el : the raw dom element that we're getting the value from. * * @return mixed : returns the value that was written to the element as a * javascript number, or undefined to let jquery handle it normally. */ $.valhooks.text.get = function( el ){ // get the element, and its data. var $this = $(el), num, data = $this.data('numformat'); // does this element have our data field? if( !data ) { // check if the valhook function already existed if( $.isfunction( orighookget ) ) { // there was, so go ahead and call it return orighookget(el); } else { // no previous function, return undefined to have jquery // take care of retrieving the value return undefined; } } else { // remove formatting, and return as number. if( el.value === '' ) return ''; // convert to a number. num = +(el.value .replace( data.regex_dec_num, '' ) .replace( data.regex_dec, '.' )); // if we've got a finite number, return it. // otherwise, simply return 0. // return as a string... thats what we're // used to with .val() return ''+( isfinite( num ) ? num : 0 ); } }; /** * a valhook which formats a number when run against an input * which has been tagged by the number formatter. * * @param object el : the raw dom element (input element). * @param float : the number to set into the value field. * * @return mixed : returns the value that was written to the element, * or undefined to let jquery handle it normally. */ $.valhooks.text.set = function( el, val ) { // get the element, and its data. var $this = $(el), data = $this.data('numformat'); // does this element have our data field? if( !data ) { // check if the valhook function already existed if( $.isfunction( orighookset ) ) { // there was, so go ahead and call it return orighookset(el,val); } else { // no previous function, return undefined to have jquery // take care of retrieving the value return undefined; } } else { return el.value = $.number( val, data.decimals, data.dec_point, data.thousands_sep ) } }; /** * the (modified) excellent number formatting method from phpjs.org. * http://phpjs.org/functions/number_format/ * * @modified by sam sehnert (teamdf.com) * - don't redefine dec_point, thousands_sep... just overwrite with defaults. * - don't redefine decimals, just overwrite as numeric. * - generate regex for normalizing pre-formatted numbers. * * @param float number : the number you wish to format, or true to use the text contents * of the element as the number. please note that this won't work for * elements which have child nodes with text content. * @param int decimals : the number of decimal places that should be displayed. defaults to 0. * @param string dec_point : the character to use as a decimal point. defaults to '.'. * @param string thousands_sep : the character to use as a thousands separator. defaults to ','. * * @return string : the formatted number as a string. */ $.number = function( number, decimals, dec_point, thousands_sep ){ // set the default values here, instead so we can use them in the replace below. thousands_sep = (typeof thousands_sep === 'undefined') ? ',' : thousands_sep; dec_point = (typeof dec_point === 'undefined') ? '.' : dec_point; decimals = !isfinite(+decimals) ? 0 : math.abs(decimals); // work out the unicode representation for the decimal place. var u_dec = ('\\u'+('0000'+(dec_point.charcodeat(0).tostring(16))).slice(-4)); // fix the number, so that it's an actual number. number = (number + '') .replace(new regexp(u_dec,'g'),'.') .replace(new regexp('[^0-9+\-ee.]','g'),''); var n = !isfinite(+number) ? 0 : +number, s = '', tofixedfix = function (n, decimals) { var k = math.pow(10, decimals); return '' + math.round(n * k) / k; }; // fix for ie parsefloat(0.55).tofixed(0) = 0; s = (decimals ? tofixedfix(n, decimals) : '' + math.round(n)).split('.'); if (s[0].length > 3) { s[0] = s[0].replace(/\b(?=(?:\d{3})+(?!\d))/g, thousands_sep); } if ((s[1] || '').length < decimals) { s[1] = s[1] || ''; s[1] += new array(decimals - s[1].length + 1).join('0'); } return s.join(dec_point); } })(jquery);