(function($){ var $chk = function(obj){ return !!(obj || obj === 0); }; /** * Masks an input with a pattern * @class * @requires jQuery * @name jQuery.iMask * @param {Object} options * @param {String} options.type (number|fixed) * @param {String} options.mask Mask using 9,a,x notation * @param {String} [options.maskEmptyChr=' '] * @param {String} [options.validNumbers='1234567890'] * @param {String} [options.validAlphas='abcdefghijklmnopqrstuvwxyz'] * @param {String} [options.validAlphaNums='abcdefghijklmnopqrstuvwxyz1234567890'] * @param {Number} [options.groupDigits=3] * @param {Number} [options.decDigits=2] * @param {String} [options.currencySymbol] * @param {String} [options.groupSymbol=','] * @param {String} [options.decSymbol='.'] * @param {Boolean} [options.showMask=true] * @param {Boolean} [options.stripMask=false] * @param {Function} [options.sanity] * @param {Object} [options.number] Override options for when validating numbers only * @param {Boolean} [options.number.stripMask=false] * @param {Boolean} [options.number.showMask=false] */ var iMask = function(){ this.initialize.apply(this, arguments); }; iMask.prototype = { std_options: { maskEmptyChr : ' ', validNumbers : "1234567890", validAlphas : "abcdefghijklmnopqrstuvwxyz", validAlphaNums : "abcdefghijklmnopqrstuvwxyz1234567890", groupDigits : 3, decDigits : 2, currencySymbol : '', groupSymbol : ',', decSymbol : '.', showMask : true, stripMask : false, lastFocus : 0, number : { stripMask : false, showMask : false } }, initialize: function(node, options) { this.node = node; this.domNode = node[0]; this.setOptions(options); var self = this; if(options.type == "number") this.node.css("text-align", "right"); this.node .bind( "mousedown click", function(ev){ ev.stopPropagation(); ev.preventDefault(); } ) .bind( "mouseup", function(){ self.onMouseUp .apply(self, arguments); } ) .bind( "keydown", function(){ self.onKeyDown .apply(self, arguments); } ) .bind( "keypress", function(){ self.onKeyPress.apply(self, arguments); } ) .bind( "focus", function(){ self.onFocus .apply(self, arguments); } ) //.bind( "blur", function(){ self.onBlur .apply(self, arguments); } ); }, setOptions: function(options) { this.options = $.extend({}, this.std_options, this.std_options[options.type] || {}, options); }, isFixed : function(){ return this.options.type == 'fixed'; }, isNumber : function(){ return this.options.type == 'number'; }, onMouseUp: function( ev ) { ev.stopPropagation(); ev.preventDefault(); if( this.isFixed() ) { var p = this.getSelectionStart(); this.setSelection(p, (p + 1)); } else if(this.isNumber() ) { this.setEnd(); } }, onKeyDown: function(ev) { if(ev.ctrlKey || ev.altKey || ev.metaKey) { return; } else if(ev.which == 13) { // enter this.node.blur(); this.submitForm(this.node); } else if(!(ev.which == 9)) { // tab if(this.options.type == "fixed") { ev.preventDefault(); var p = this.getSelectionStart(); switch(ev.which) { case 8: // Backspace this.updateSelection( this.options.maskEmptyChr ); this.selectPrevious(); break; case 36: // Home this.selectFirst(); break; case 35: // End this.selectLast(); break; case 37: // Left case 38: // Up this.selectPrevious(); break; case 39: // Right case 40: // Down this.selectNext(); break; case 46: // Delete this.updateSelection( this.options.maskEmptyChr ); this.selectNext(); break; default: var chr = this.chrFromEv(ev); if( this.isViableInput( p, chr ) ) { this.updateSelection( ev.shiftKey ? chr.toUpperCase() : chr ); this.node.trigger("valid", ev, this.node); this.selectNext(); } else { this.node.trigger("invalid", ev, this.node); } break; } } else if(this.options.type == "number") { switch(ev.which) { case 35: // END case 36: // HOME case 37: // LEFT case 38: // UP case 39: // RIGHT case 40: // DOWN break; case 8: // backspace case 46: // delete var self = this; setTimeout(function(){ self.formatNumber(); }, 1); break; default: ev.preventDefault(); var chr = this.chrFromEv( ev ); if( this.isViableInput( p, chr ) ) { var range = new Range( this ) , val = this.sanityTest( range.replaceWith( chr ) ); if(val !== false){ this.updateSelection( chr ); this.formatNumber(); } this.node.trigger( "valid", ev, this.node ); } else { this.node.trigger( "invalid", ev, this.node ); } break; } } } }, allowKeys : { 8 : 1 // backspace , 9 : 1 // tab , 13 : 1 // enter , 35 : 1 // end , 36 : 1 // home , 37 : 1 // left , 38 : 1 // up , 39 : 1 // right , 40 : 1 // down , 46 : 1 // delete }, onKeyPress: function(ev) { var key = ev.which || ev.keyCode; if( !( this.allowKeys[ key ] ) && !(ev.ctrlKey || ev.altKey || ev.metaKey) ) { ev.preventDefault(); ev.stopPropagation(); } }, onFocus: function(ev) { ev.stopPropagation(); ev.preventDefault(); this.options.showMask && (this.domNode.value = this.wearMask(this.domNode.value)); this.sanityTest( this.domNode.value ); var self = this; setTimeout( function(){ self[ self.options.type === "fixed" ? 'selectFirst' : 'setEnd' ](); }, 1 ); }, onBlur: function(ev) { // ev.stopPropagation(); // ev.preventDefault(); // // if(this.options.stripMask) // this.domNode.value = this.stripMask(); }, selectAll: function() { this.setSelection(0, this.domNode.value.length); }, selectFirst: function() { for(var i = 0, len = this.options.mask.length; i < len; i++) { if(this.isInputPosition(i)) { this.setSelection(i, (i + 1)); return; } } }, selectLast: function() { for(var i = (this.options.mask.length - 1); i >= 0; i--) { if(this.isInputPosition(i)) { this.setSelection(i, (i + 1)); return; } } }, selectPrevious: function(p) { if( !$chk(p) ){ p = this.getSelectionStart(); } if(p <= 0) { this.selectFirst(); } else { if(this.isInputPosition(p - 1)) { this.setSelection(p - 1, p); } else { this.selectPrevious(p - 1); } } }, selectNext: function(p) { if( !$chk(p) ){ p = this.getSelectionEnd(); } if( this.isNumber() ){ this.setSelection( p+1, p+1 ); return; } if( p >= this.options.mask.length) { this.selectLast(); } else { if(this.isInputPosition(p)) { this.setSelection(p, (p + 1)); } else { this.selectNext(p + 1); } } }, setSelection: function( a, b ) { a = a.valueOf(); if( !b && a.splice ){ b = a[1]; a = a[0]; } if(this.domNode.setSelectionRange) { this.domNode.focus(); this.domNode.setSelectionRange(a, b); } else if(this.domNode.createTextRange) { var r = this.domNode.createTextRange(); r.collapse(); r.moveStart("character", a); r.moveEnd("character", (b - a)); r.select(); } }, updateSelection: function( chr ) { var value = this.domNode.value , range = new Range( this ) , output = range.replaceWith( chr ); this.domNode.value = output; if( range[0] === range[1] ){ this.setSelection( range[0] + 1, range[0] + 1 ); }else{ this.setSelection( range ); } }, setEnd: function() { var len = this.domNode.value.length; this.setSelection(len, len); }, getSelectionRange : function(){ return [ this.getSelectionStart(), this.getSelectionEnd() ]; }, getSelectionStart: function() { var p = 0, n = this.domNode.selectionStart; if( n ) { if( typeof( n ) == "number" ){ p = n; } } else if( document.selection ){ var r = document.selection.createRange().duplicate(); r.moveEnd( "character", this.domNode.value.length ); p = this.domNode.value.lastIndexOf( r.text ); if( r.text == "" ){ p = this.domNode.value.length; } } return p; }, getSelectionEnd: function() { var p = 0, n = this.domNode.selectionEnd; if( n ) { if( typeof( n ) == "number"){ p = n; } } else if( document.selection ){ var r = document.selection.createRange().duplicate(); r.moveStart( "character", -this.domNode.value.length ); p = r.text.length; } return p; }, isInputPosition: function(p) { var mask = this.options.mask.toLowerCase(); var chr = mask.charAt(p); return !!~"9ax".indexOf(chr); }, sanityTest: function( str, p ){ var sanity = this.options.sanity; if(sanity instanceof RegExp){ return sanity.test(str); }else if($.isFunction(sanity)){ var ret = sanity(str, p); if(typeof(ret) == 'boolean'){ return ret; }else if(typeof(ret) != 'undefined'){ if( this.isFixed() ){ var p = this.getSelectionStart(); this.domNode.value = this.wearMask( ret ); this.setSelection( p, p+1 ); this.selectNext(); }else if( this.isNumber() ){ var range = new Range( this ); this.domNode.value = ret; this.setSelection( range ); this.formatNumber(); } return false; } } }, isViableInput: function() { return this[ this.isFixed() ? 'isViableFixedInput' : 'isViableNumericInput' ].apply( this, arguments ); }, isViableFixedInput : function( p, chr ){ var mask = this.options.mask.toLowerCase(); var chMask = mask.charAt(p); var val = this.domNode.value.split(''); val.splice( p, 1, chr ); val = val.join(''); var ret = this.sanityTest( val, p ); if(typeof(ret) == 'boolean'){ return ret; } if(({ '9' : this.options.validNumbers, 'a' : this.options.validAlphas, 'x' : this.options.validAlphaNums }[chMask] || '').indexOf(chr) >= 0){ return true; } return false; }, isViableNumericInput : function( p, chr ){ return !!~this.options.validNumbers.indexOf( chr ); }, wearMask: function(str) { var mask = this.options.mask.toLowerCase() , output = "" , chrSets = { '9' : 'validNumbers' , 'a' : 'validAlphas' , 'x' : 'validAlphaNums' }; for(var i = 0, u = 0, len = mask.length; i < len; i++) { switch(mask.charAt(i)) { case '9': case 'a': case 'x': output += ((this.options[ chrSets[ mask.charAt(i) ] ].indexOf( str.charAt(u).toLowerCase() ) >= 0) && ( str.charAt(u) != "")) ? str.charAt( u++ ) : this.options.maskEmptyChr; break; default: output += mask.charAt(i); if( str.charAt(u) == mask.charAt(i) ){ u++; } break; } } return output; }, stripMask: function() { var value = this.domNode.value; if("" == value) return ""; var output = ""; if( this.isFixed() ) { for(var i = 0, len = value.length; i < len; i++) { if((value.charAt(i) != this.options.maskEmptyChr) && (this.isInputPosition(i))) {output += value.charAt(i);} } } else if( this.isNumber() ) { for(var i = 0, len = value.length; i < len; i++) { if(this.options.validNumbers.indexOf(value.charAt(i)) >= 0) {output += value.charAt(i);} } } return output; }, chrFromEv: function(ev) { var chr = '', key = ev.which; if(key >= 96 && key <= 105){ key -= 48; } // shift number-pad numbers to corresponding character codes chr = String.fromCharCode(key).toLowerCase(); // key pressed as a lowercase string return chr; }, formatNumber: function() { // stripLeadingZeros var olen = this.domNode.value.length , str2 = this.stripMask() , str1 = str2.replace( /^0+/, '' ) , range = new Range(this); // wearLeadingZeros str2 = str1; str1 = ""; for(var len = str2.length, i = this.options.decDigits; len <= i; len++) { str1 += "0"; } str1 += str2; // decimalSymbol str2 = str1.substr(str1.length - this.options.decDigits) str1 = str1.substring(0, (str1.length - this.options.decDigits)) // groupSymbols var re = new RegExp("(\\d+)(\\d{"+ this.options.groupDigits +"})"); while(re.test(str1)) { str1 = str1.replace(re, "$1"+ this.options.groupSymbol +"$2"); } this.domNode.value = this.options.currencySymbol + str1 + this.options.decSymbol + str2; this.setSelection( range ); }, getObjForm: function() { return this.node.getClosest('form'); }, submitForm: function() { //var form = this.getObjForm(); //form.trigger('submit'); } }; function Range( obj ){ this.range = obj.getSelectionRange(); this.len = obj.domNode.value.length this.obj = obj; this['0'] = this.range[0]; this['1'] = this.range[1]; } Range.prototype = { valueOf : function(){ var len = this.len - this.obj.domNode.value.length; return [ this.range[0] - len, this.range[1] - len ]; }, replaceWith : function( str ){ var val = this.obj.domNode.value , range = this.valueOf(); return val.substr( 0, range[0] ) + str + val.substr( range[1] ); } }; $.fn.iMask = function(options){ this.each(function(){ if(this._imask) { // don't re-initialize, just reset the options dynamically this._imask.setOptions(options); } else { this._imask = new iMask($(this), options); } }); }; })(jQuery);