/** * jquery.meio.mask.js * @author: fabiomcosta * @version: 1.1.3 * * Created by Fabio M. Costa on 2008-09-16. Please report any bug at http://www.meiocodigo.com * * Copyright (c) 2008 Fabio M. Costa http://www.meiocodigo.com * * The MIT License (http://www.opensource.org/licenses/mit-license.php) * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ (function($){ var isIphone = (window.orientation != undefined), // browsers like firefox2 and before and opera doenst have the onPaste event, but the paste feature can be done with the onInput event. pasteEvent = (($.browser.opera || ($.browser.mozilla && parseFloat($.browser.version.substr(0,3)) < 1.9 ))? 'input': 'paste'); $.event.special.paste = { setup: function() { if(this.addEventListener) this.addEventListener(pasteEvent, pasteHandler, false); else if (this.attachEvent) this.attachEvent(pasteEvent, pasteHandler); }, teardown: function() { if(this.removeEventListener) this.removeEventListener(pasteEvent, pasteHandler, false); else if (this.detachEvent) this.detachEvent(pasteEvent, pasteHandler); } }; // the timeout is set because we can't get the value from the input without it function pasteHandler(e){ var self = this; e = $.event.fix(e || window.e); e.type = 'paste'; // Execute the right handlers by setting the event type to paste setTimeout(function(){ $.event.handle.call(self, e); }, 1); }; $.extend({ mask : { // the mask rules. You may add yours! // number rules will be overwritten rules : { 'z': /[a-z]/, 'Z': /[A-Z]/, 'a': /[a-zA-Z]/, '*': /[0-9a-zA-Z]/, '@': /[0-9a-zA-ZçÇáàãâéèêíìóòôõúùü]/ }, // these keys will be ignored by the mask. // all these numbers where obtained on the keydown event keyRepresentation : { 8 : 'backspace', 9 : 'tab', 13 : 'enter', 16 : 'shift', 17 : 'control', 18 : 'alt', 27 : 'esc', 33 : 'page up', 34 : 'page down', 35 : 'end', 36 : 'home', 37 : 'left', 38 : 'up', 39 : 'right', 40 : 'down', 45 : 'insert', 46 : 'delete', 116 : 'f5', 123 : 'f12', 224 : 'command' }, iphoneKeyRepresentation : { 10 : 'go', 127 : 'delete' }, signals : { '+' : '', '-' : '-' }, // default settings for the plugin options : { attr: 'alt', // an attr to look for the mask name or the mask itself mask: null, // the mask to be used on the input type: 'fixed', // the mask of this mask maxLength: -1, // the maxLength of the mask defaultValue: '', // the default value for this input signal: false, // this should not be set, to use signal at masks put the signal you want ('-' or '+') at the default value of this mask. // See the defined masks for a better understanding. textAlign: true, // use false to not use text-align on any mask (at least not by the plugin, you may apply it using css) selectCharsOnFocus: true, // select all chars from input on its focus autoTab: false, // auto focus the next form element when you type the mask completely setSize: false, // sets the input size based on the length of the mask (work with fixed and reverse masks only) fixedChars : '[(),.:/ -]', // fixed chars to be used on the masks. You may change it for your needs! onInvalid : function(){}, onValid : function(){}, onOverflow : function(){} }, // masks. You may add yours! // Ex: $.fn.setMask.masks.msk = {mask: '999'} // and then if the 'attr' options value is 'alt', your input should look like: // masks : { 'year' : { mask : '2999'}, 'phone' : { mask : '(99) 9999-9999' }, 'phone-us' : { mask : '(999) 999-9999' }, 'cpf' : { mask : '999.999.999-99' }, // cadastro nacional de pessoa fisica 'cnpj' : { mask : '99.999.999/9999-99' }, 'date' : { mask : '39/19/9999' }, //uk date 'date-us' : { mask : '19/39/9999' }, 'month-year' : { mask : '19/9999' }, 'cep' : { mask : '99999-999' }, 'time' : { mask : '29:59' }, 'cc' : { mask : '9999 9999 9999 9999' }, //credit card mask 'integer' : { mask : '999.999.999.999', type : 'reverse' }, 'decimal' : { mask : '99,999.999.999.999', type : 'reverse' }, 'decimal-us' : { mask : '99.999,999,999,999', type : 'reverse' }, 'signed-decimal' : { mask : '99,999.999.999.999', type : 'reverse' }, 'signed-decimal-us' : { mask : '99,999.999.999.999', type : 'reverse' } }, init : function(){ // if has not inited... if( !this.hasInit ){ var self = this, i, keyRep = (isIphone)? this.iphoneKeyRepresentation: this.keyRepresentation; this.ignore = false; // constructs number rules for(i=0; i<=9; i++) this.rules[i] = new RegExp('[0-'+i+']'); this.keyRep = keyRep; // ignore keys array creation for iphone or the normal ones this.ignoreKeys = []; $.each(keyRep,function(key){ self.ignoreKeys.push( parseInt(key) ); }); this.hasInit = true; } }, set: function(el,options){ var maskObj = this, $el = $(el), mlStr = 'maxLength'; options = options || {}; this.init(); return $el.each(function(){ if(options.attr) maskObj.options.attr = options.attr; var $this = $(this), o = $.extend({}, maskObj.options), attrValue = $this.attr(o.attr), tmpMask = ''; // then we look for the 'attr' option tmpMask = (typeof options == 'string')? options: (attrValue != '')? attrValue: null; if(tmpMask) o.mask = tmpMask; // then we see if it's a defined mask if(maskObj.masks[tmpMask]) o = $.extend(o, maskObj.masks[tmpMask]); // then it looks if the options is an object, if it is we will overwrite the actual options if(typeof options == 'object' && options.constructor != Array) o = $.extend(o, options); //then we look for some metadata on the input if($.metadata) o = $.extend(o, $this.metadata()); if(o.mask != null){ if($this.data('mask')) maskObj.unset($this); var defaultValue = o.defaultValue, reverse = (o.type=='reverse'), fixedCharsRegG = new RegExp(o.fixedChars, 'g'); if(o.maxLength == -1) o.maxLength = $this.attr(mlStr); o = $.extend({}, o,{ fixedCharsReg: new RegExp(o.fixedChars), fixedCharsRegG: fixedCharsRegG, maskArray: o.mask.split(''), maskNonFixedCharsArray: o.mask.replace(fixedCharsRegG, '').split('') }); //setSize option (this is not removed from the input (while removing the mask) since this would be kind of funky) if((o.type=='fixed' || reverse) && o.setSize && !$this.attr('size')) $this.attr('size', o.mask.length); //sets text-align right for reverse masks if(reverse && o.textAlign) $this.css('text-align', 'right'); if(this.value!='' || defaultValue!=''){ // apply mask to the current value of the input or to the default value var val = maskObj.string((this.value!='')? this.value: defaultValue, o); //setting defaultValue fixes the reset button from the form this.defaultValue = val; $this.val(val); } // compatibility patch for infinite mask, that is now repeat if(o.type=='infinite') o.type = 'repeat'; $this.data('mask', o); // removes the maxLength attribute (it will be set again if you use the unset method) $this.removeAttr(mlStr); // setting the input events $this.bind('keydown.mask', {func:maskObj._onKeyDown, thisObj:maskObj}, maskObj._onMask) .bind('keypress.mask', {func:maskObj._onKeyPress, thisObj:maskObj}, maskObj._onMask) .bind('keyup.mask', {func:maskObj._onKeyUp, thisObj:maskObj}, maskObj._onMask) .bind('paste.mask', {func:maskObj._onPaste, thisObj:maskObj}, maskObj._onMask) .bind('focus.mask', maskObj._onFocus) .bind('blur.mask', maskObj._onBlur) .bind('change.mask', maskObj._onChange); } }); }, //unsets the mask from el unset : function(el){ var $el = $(el); return $el.each(function(){ var $this = $(this); if($this.data('mask')){ var maxLength = $this.data('mask').maxLength; if(maxLength != -1) $this.attr('maxLength', maxLength); $this.unbind('.mask') .removeData('mask'); } }); }, //masks a string string : function(str, options){ this.init(); var o={}; if(typeof str != 'string') str = String(str); switch(typeof options){ case 'string': // then we see if it's a defined mask if(this.masks[options]) o = $.extend(o, this.masks[options]); else o.mask = options; break; case 'object': o = options; } if(!o.fixedChars) o.fixedChars = this.options.fixedChars; var fixedCharsReg = new RegExp(o.fixedChars), fixedCharsRegG = new RegExp(o.fixedChars, 'g'); // insert signal if any if( (o.type=='reverse') && o.defaultValue ){ if( typeof this.signals[o.defaultValue.charAt(0)] != 'undefined' ){ var maybeASignal = str.charAt(0); o.signal = (typeof this.signals[maybeASignal] != 'undefined') ? this.signals[maybeASignal] : this.signals[o.defaultValue.charAt(0)]; o.defaultValue = o.defaultValue.substring(1); } } return this.__maskArray(str.split(''), o.mask.replace(fixedCharsRegG, '').split(''), o.mask.split(''), o.type, o.maxLength, o.defaultValue, fixedCharsReg, o.signal); }, // all the 3 events below are here just to fix the change event on reversed masks. // It isn't fired in cases that the keypress event returns false (needed). _onFocus: function(e){ var $this = $(this), dataObj = $this.data('mask'); dataObj.inputFocusValue = $this.val(); dataObj.changed = false; if(dataObj.selectCharsOnFocus) $this.select(); }, _onBlur: function(e){ var $this = $(this), dataObj = $this.data('mask'); if(dataObj.inputFocusValue != $this.val() && !dataObj.changed) $this.trigger('change'); }, _onChange: function(e){ $(this).data('mask').changed = true; }, _onMask : function(e){ var thisObj = e.data.thisObj, o = {}; o._this = e.target; o.$this = $(o._this); // if the input is readonly it does nothing if(o.$this.attr('readonly')) return true; o.data = o.$this.data('mask'); o[o.data.type] = true; o.value = o.$this.val(); o.nKey = thisObj.__getKeyNumber(e); o.range = thisObj.__getRange(o._this); o.valueArray = o.value.split(''); return e.data.func.call(thisObj, e, o); }, _onKeyDown : function(e,o){ // lets say keypress at desktop == keydown at iphone (theres no keypress at iphone) this.ignore = $.inArray(o.nKey, this.ignoreKeys) > -1 || e.ctrlKey || e.metaKey || e.altKey; if(this.ignore){ var rep = this.keyRep[o.nKey]; o.data.onValid.call(o._this, rep? rep: '', o.nKey); } return isIphone ? this._keyPress(e, o) : true; }, _onKeyUp : function(e, o){ //9=TAB_KEY 16=SHIFT_KEY //this is a little bug, when you go to an input with tab key //it would remove the range selected by default, and that's not a desired behavior if(o.nKey==9 || o.nKey==16) return true; if(o.data.type=='repeat'){ this.__autoTab(o); return true; } return this._onPaste(e, o); }, _onPaste : function(e,o){ // changes the signal at the data obj from the input if(o.reverse) this.__changeSignal(e.type, o); var $thisVal = this.__maskArray( o.valueArray, o.data.maskNonFixedCharsArray, o.data.maskArray, o.data.type, o.data.maxLength, o.data.defaultValue, o.data.fixedCharsReg, o.data.signal ); o.$this.val( $thisVal ); // this makes the caret stay at first position when // the user removes all values in an input and the plugin adds the default value to it (if it haves one). if( !o.reverse && o.data.defaultValue.length && (o.range.start==o.range.end) ) this.__setRange(o._this, o.range.start, o.range.end); //fix so ie's and safari's caret won't go to the end of the input value. if( ($.browser.msie || $.browser.safari) && !o.reverse) this.__setRange(o._this,o.range.start,o.range.end); if(this.ignore) return true; this.__autoTab(o); return true; }, _onKeyPress: function(e, o){ if(this.ignore) return true; // changes the signal at the data obj from the input if(o.reverse) this.__changeSignal(e.type, o); var c = String.fromCharCode(o.nKey), rangeStart = o.range.start, rawValue = o.value, maskArray = o.data.maskArray; if(o.reverse){ // the input value from the range start to the value start var valueStart = rawValue.substr(0, rangeStart), // the input value from the range end to the value end valueEnd = rawValue.substr(o.range.end, rawValue.length); rawValue = valueStart+c+valueEnd; //necessary, if not decremented you will be able to input just the mask.length-1 if signal!='' //ex: mask:99,999.999.999 you will be able to input 99,999.999.99 if(o.data.signal && (rangeStart-o.data.signal.length > 0)) rangeStart-=o.data.signal.length; } var valueArray = rawValue.replace(o.data.fixedCharsRegG, '').split(''), // searches for fixed chars begining from the range start position, till it finds a non fixed extraPos = this.__extraPositionsTill(rangeStart, maskArray, o.data.fixedCharsReg); o.rsEp = rangeStart+extraPos; if(o.repeat) o.rsEp = 0; // if the rule for this character doesnt exist (value.length is bigger than mask.length) // added a verification for maxLength in the case of the repeat type mask if( !this.rules[maskArray[o.rsEp]] || (o.data.maxLength != -1 && valueArray.length >= o.data.maxLength && o.repeat)){ // auto focus on the next input of the current form o.data.onOverflow.call(o._this, c, o.nKey); return false; } // if the new character is not obeying the law... :P else if( !this.rules[maskArray[o.rsEp]].test( c ) ){ o.data.onInvalid.call(o._this, c, o.nKey); return false; } else o.data.onValid.call(o._this, c, o.nKey); var $thisVal = this.__maskArray( valueArray, o.data.maskNonFixedCharsArray, maskArray, o.data.type, o.data.maxLength, o.data.defaultValue, o.data.fixedCharsReg, o.data.signal, extraPos ); o.$this.val( $thisVal ); return (o.reverse)? this._keyPressReverse(e, o): (o.fixed)? this._keyPressFixed(e, o): true; }, _keyPressFixed: function(e, o){ if(o.range.start==o.range.end){ // the 0 thing is cause theres a particular behavior i wasnt liking when you put a default // value on a fixed mask and you select the value from the input the range would go to the // end of the string when you enter a char. with this it will overwrite the first char wich is a better behavior. // opera fix, cant have range value bigger than value length, i think it loops thought the input value... if((o.rsEp==0 && o.value.length==0) || o.rsEp < o.value.length) this.__setRange(o._this, o.rsEp, o.rsEp+1); } else this.__setRange(o._this, o.range.start, o.range.end); return true; }, _keyPressReverse: function(e, o){ //fix for ie //this bug was pointed by Pedro Martins //it fixes a strange behavior that ie was having after a char was inputted in a text input that //had its content selected by any range if($.browser.msie && ((o.range.start==0 && o.range.end==0) || o.range.start != o.range.end )) this.__setRange(o._this, o.value.length); return false; }, __autoTab: function(o){ if(o.data.autoTab && ( ( o.$this.val().length >= o.data.maskArray.length && !o.repeat ) || ( o.data.maxLength != -1 && o.valueArray.length >= o.data.maxLength && o.repeat ) ) ){ var nextEl = this.__getNextInput(o._this, o.data.autoTab); if(nextEl){ o.$this.trigger('blur'); nextEl.focus().select(); } } }, // changes the signal at the data obj from the input __changeSignal : function(eventType,o){ if(o.data.signal!==false){ var inputChar = (eventType=='paste')? o.value.charAt(0): String.fromCharCode(o.nKey); if( this.signals && (typeof this.signals[inputChar] != 'undefined') ){ o.data.signal = this.signals[inputChar]; } } }, __getKeyNumber : function(e){ return (e.charCode||e.keyCode||e.which); }, // this function is totaly specific to be used with this plugin, youll never need it // it gets the array representing an unmasked string and masks it depending on the type of the mask __maskArray : function(valueArray, maskNonFixedCharsArray, maskArray, type, maxlength, defaultValue, fixedCharsReg, signal, extraPos){ if(type == 'reverse') valueArray.reverse(); valueArray = this.__removeInvalidChars(valueArray, maskNonFixedCharsArray, type=='repeat'||type=='infinite'); if(defaultValue) valueArray = this.__applyDefaultValue.call(valueArray, defaultValue); valueArray = this.__applyMask(valueArray, maskArray, extraPos, fixedCharsReg); switch(type){ case 'reverse': valueArray.reverse(); return (signal || '')+valueArray.join('').substring(valueArray.length-maskArray.length); case 'infinite': case 'repeat': var joinedValue = valueArray.join(''); return (maxlength != -1 && valueArray.length >= maxlength)? joinedValue.substring(0, maxlength): joinedValue; default: return valueArray.join('').substring(0, maskArray.length); } return ''; }, // applyes the default value to the result string __applyDefaultValue : function(defaultValue){ var defLen = defaultValue.length,thisLen = this.length,i; //removes the leading chars for(i=thisLen-1;i>=0;i--){ if(this[i]==defaultValue.charAt(0)) this.pop(); else break; } // apply the default value for(i=0;i 0 || formEl.offsetHeight > 0) && formEl.nodeName != 'FIELDSET' && (selector === true || (typeof selector == 'string' && $formEl.is(selector))); }, // http://www.bazon.net/mishoo/articles.epl?art_id=1292 __setRange : function(input, start, end) { if(typeof end == 'undefined') end = start; if (input.setSelectionRange){ input.setSelectionRange(start, end); } else{ // assumed IE var range = input.createTextRange(); range.collapse(); range.moveStart('character', start); range.moveEnd('character', end - start); range.select(); } }, // adaptation from http://digitarald.de/project/autocompleter/ __getRange : function(input){ if (!$.browser.msie) return {start: input.selectionStart, end: input.selectionEnd}; var pos = {start: 0, end: 0}, range = document.selection.createRange(); pos.start = 0 - range.duplicate().moveStart('character', -100000); pos.end = pos.start + range.text.length; return pos; }, //deprecated unmaskedVal : function(el){ return $(el).val().replace($.mask.fixedCharsRegG, ''); } } }); $.fn.extend({ setMask : function(options){ //Se for IE, desabilita o botão direito do mouse no //campo com a máscara, para evitar o 'colar' if(navigator.appName == "Microsoft Internet Explorer") { $(this).mouseenter(function() {document.oncontextmenu=new Function("return false");}); $(this).mouseout(function() {document.oncontextmenu=new Function("return true");}); } //-- return $.mask.set(this, options); }, unsetMask : function(){ return $.mask.unset(this); }, //deprecated unmaskedVal : function(){ return $.mask.unmaskedVal(this[0]); } }); })(jQuery);