/* $Id: jquery.meiomask.js 42 2008-11-17 14:35:03Z fabiomcosta $ */
/**
 * @version 1.0.3 $Revision: 42 $
 * The MIT License
 * Copyright (c) 2008 Fabio M. Costa http://www.meiocodigo.com
 */
/**
 * jquery.meiomask.js
 * $URL: http://svn.assembla.com/svn/meiomask/jquery.meiomask.js $
 * @author: $Author: fabiomcosta $
 * @version 1.0.3 $Revision: 42 $
 * @lastchange: $Date: 2008-11-17 12:35:03 -0200 (Mon, 17 Nov 2008) $
 *
 * 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
 *
 * 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);
    
    $.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Ã§Ã‡Ã¡Ã Ã£Ã©Ã¨Ã­Ã¬Ã³Ã²ÃµÃºÃ¹Ã¼]/
            },
            
            // fixed chars to be used on the masks. You may change it for your needs!
            fixedChars : '[(),.:/ -]',
            
            // these keys will be ignored by the mask. You may add yours!
            // all these numbers where obtained on the keydown event
            ignoreKeys : [
                8,  //BKSPACE
                9,    //ALT
                13,    //CTRL
                16,    //SHIFT
                17,    //CTRL
                18,    //ALT
                //32,    //SPACE
                33,    //PGUP
                34,    //PGDOWN
                35,    //END
                36,    //HOME
                37,    //LEFT
                38,    //UP
                39,    //RIGHT
                40,    //DOWN
                45,    //INSERT
                46,    //DELETE
                91, //METAKEY Command key on Mac. '[' key haves this same keycode but on keypress... and this list is used on keydown. So '[' still works as it haves code 221 on keydown.
                116    //F5
            ],
            
            iphoneIgnoreKeys : [
                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
                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.
                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:
            // <input type="text" name="some_name" id="some_name" alt="msk" />
            masks : {
                'phone'                : { mask : '(99) 9999-9999' },
                'phone-us'            : { mask : '(999) 9999-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' },
                'cep'                : { mask : '99999-999' },
                'time'                : { mask : '29:69' },
                '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', defaultValue : '000' },
                'decimal-us'        : { mask : '99.999,999,999,999', type : 'reverse', defaultValue : '000' },
                'signed-decimal'    : { mask : '99,999.999.999.999', type : 'reverse', defaultValue : '+000' },
                'signed-decimal-us' : { mask : '99,999.999.999.999', type : 'reverse', defaultValue : '+000' }
            },
            
            init : function(){
                // if has not inited...
                if( !this.hasInit ){
                    var i;
                    this.ignore = false;
                    this.fixedCharsReg = new RegExp(this.fixedChars);
                    this.fixedCharsRegG = new RegExp(this.fixedChars,'g');
                    
                    // constructs number rules
                    for(i=0; i<=9; i++) this.rules[i] = new RegExp('[0-'+i+']');
                    
                    this.hasInit = true;
                }
            },
            
            set: function(el,options){
                
                var maskObj = this,
                    $el = $(el),
                    mlStr = 'maxLength';
                    
                this.init();
                
                return $el.each(function(){
                    
                    var $this = $(this),
                        o = $.extend({},maskObj.options),
                        attrValue = $this.attr(o.attr),
                        tmpMask = '',
                        // 'input' event fires on every keyboard event on the input
                        pasteEvent = maskObj.__getPasteEvent();
                        
                    // 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' ) 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,
                            mlValue = $this.attr(mlStr),
                            reverse = (o.type=='reverse');
                        
                        o = $.extend({},o,{
                            maxlength: mlValue,
                            maskArray : o.mask.split(''),
                            maskNonFixedCharsArray : o.mask.replace(maskObj.fixedCharsRegG,'').split(''),
                            defaultValue: defaultValue.split('')
                        });
                        
                        //sets text-align right for reverse masks
                        if(reverse) $this.css('text-align','right');
                        
                        // apply mask to the current value of the input
                        if($this.val()!='') $this.val( maskObj.string($this.val(),o) );
                            
                        // apply the default value of the mask to the input
                        else if(defaultValue!='') $this.val( maskObj.string(defaultValue,o) );
                        
                        $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',{func:maskObj._keyDown,thisObj:maskObj},maskObj._onMask)
                            .bind('keyup',{func:maskObj._keyUp,thisObj:maskObj},maskObj._onMask)
                            .bind('keypress',{func:maskObj._keyPress,thisObj:maskObj},maskObj._onMask)
                            .bind(pasteEvent,{func:maskObj._paste,thisObj:maskObj},maskObj._delayedOnMask);
                    }
                });
            },
            
            //unsets the mask from el
            unset : function(el){
                var $el = $(el),
                    _this = this;
                return $el.each(function(){
                    var $this = $(this);
                    if( $this.data('mask') ){
                        var maxLength = $this.data('mask').maxlength,
                            pasteEvent = _this.__getPasteEvent();
                            
                        if(maxLength != -1) $this.attr('maxLength',maxLength);
                        
                        $this.unbind('keydown',_this._onMask)
                            .unbind('keypress',_this._onMask)
                            .unbind('keyup',_this._onMask)
                            .unbind(pasteEvent,_this._delayedOnMask)
                            .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;
                        break;
                }
                var reverse = (o.type=='reverse');

                this._insertSignal(reverse,str,o);
                    
                return this.__maskArray(str.split(''),
                            o.mask.replace(this.fixedCharsRegG,'').split(''),
                            o.mask.split(''),
                            reverse,
                            o.defaultValue,
                            o.signal);
            },
            
            _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.value = o.$this.val();
                o.nKey = thisObj.__getKeyNumber(e);
                o.range = thisObj.__getRangePosition(o._this);
                o.valueArray = o.value.split('');
                o.data = o.$this.data('mask');
                o.reverse = (o.data.type=='reverse');
                return e.data.func.call(thisObj,e,o);
            },
            
            // the timeout is set because on ie we can't get the value from the input without it
            _delayedOnMask : function(e){
                e.type='paste';
                setTimeout(function(){ e.data.thisObj._onMask(e) },1);
            },
            
            _keyDown : function(e,o){
                // lets say keypress at desktop == keydown at iphone (theres no keypress at iphone)
                var arrayKeys = isIphone ? this.iphoneIgnoreKeys : this.ignoreKeys;
                this.ignore = ( $.inArray(o.nKey,arrayKeys) > -1 );
                if( this.ignore ) o.data.onValid.call(o._this,'',o.nKey);
                return isIphone ? this._keyPress(e,o) : true;
            },
            
            _keyUp : function(e,o){
                //9=TAB_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 && ($.browser.safari || $.browser.msie)) return true;
                return this._paste(e,o);
            },
            
            _paste : function(e,o){
                // changes the signal at the data obj from the input
                this._changeSignal(e.type,o);
                
                var $thisVal = this.__maskArray(
                    o.valueArray,
                    o.data.maskNonFixedCharsArray,
                    o.data.maskArray,
                    o.reverse,
                    o.data.defaultValue,
                    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);
                
                return true;

            },
            
            _keyPress: function(e,o){
                
                if( this.ignore || e.ctrlKey || e.metaKey || e.altKey ) return true;
                
                // changes the signal at the data obj from the input
                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(this.fixedCharsRegG,'').split(''),
                    // searches for fixed chars begining from the range start position, till it finds a non fixed
                    extraPos = this.__extraPositionsTill(rangeStart,maskArray);
                
                o.rsEp = rangeStart+extraPos;
                
                // if the rule for this character doesnt exist (value.length is bigger than mask.length)
                if( !this.rules[maskArray[o.rsEp]] ){
                    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.reverse,
                    o.data.defaultValue,
                    o.data.signal,
                    extraPos
                );
                
                o.$this.val( $thisVal );
                return (o.reverse)?this._keyPressReverse(e,o):this._keyPressFixed(e,o);
            },
            
            _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.rangeStart==0 && o.range.end==0) || o.rangeStart != o.range.end ) )
                    this.__setRange(o._this,o.value.length);
                return false;
            },
            
            _setMaskData : function($input,key,value){
                var data = $input.data('mask');
                data[key] = value;
                $input.data('mask',data);
            },
            
            // 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.substr(0,1):String.fromCharCode(o.nKey);
                    if( $.inArray(inputChar,this.signals) > -1 ){
                        if(inputChar=='+') inputChar='';
                        this._setMaskData(o.$this,'signal',inputChar);
                        o.data.signal = inputChar;
                    }
                }
            },
            
            // insert signal if any
            _insertSignal : function(reverse,str,o){
                if( reverse && o.defaultValue ){
                    if(typeof o.defaultValue == 'string') o.defaultValue = o.defaultValue.split('');
                    if($.inArray(o.defaultValue[0],this.signals) > -1){
                        var maybeASignal = str.substr(0,1);
                        o.signal = ($.inArray(maybeASignal,this.signals) > -1)?maybeASignal:o.defaultValue[0];
                        if(o.signal=='+') o.signal = '';
                        o.defaultValue.shift();
                    }
                }
            },
            
            // browsers like firefox2 and before and opera doenst have the onPaste event, but the paste feature can be done with the onInput event.
            __getPasteEvent : function(){
                return ( $.browser.opera || ( $.browser.mozilla && parseFloat($.browser.version.substr(0,3)) < 1.9 ))?'input':'paste';
            },
            
            __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,reverse,defaultValue,signal,extraPos){
                if(reverse) valueArray.reverse();
                valueArray = this.__removeInvalidChars(valueArray,maskNonFixedCharsArray);
                if(defaultValue) valueArray = this.__applyDefaultValue.call(valueArray,defaultValue);
                valueArray = this.__applyMask(valueArray,maskArray,extraPos);
                if(reverse){
                    valueArray.reverse();
                    if(!signal || signal=='+') signal='';
                    return signal+valueArray.join('').substring(valueArray.length-maskArray.length);
                }
                else
                    return valueArray.join('').substring(0,maskArray.length);
            },
            
            // 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[0]) this.pop();
                    else break;
                }
                // apply the default value
                for(i=0;i<defLen;i++) if(!this[i])
                    this[i] = defaultValue[i];
                    
                return this;
            },
            
            // Removes values that doesnt match the mask from the valueArray
            // Returns the array without the invalid chars.
            __removeInvalidChars : function(valueArray,maskNonFixedCharsArray){
                // removes invalid chars
                for(var i=0; i<valueArray.length; i++ ){
                    if( maskNonFixedCharsArray[i] &&
                        this.rules[maskNonFixedCharsArray[i]] &&
                        !this.rules[maskNonFixedCharsArray[i]].test(valueArray[i]) ){
                            valueArray.splice(i,1);
                            i--;
                    }
                }
                return valueArray;
            },
            
            // Apply the current input mask to the valueArray and returns it. 
            __applyMask : function(valueArray,maskArray,plus){
                if( typeof plus == 'undefined' ) plus = 0;
                // apply the current mask to the array of chars
                for(var i=0; i<valueArray.length+plus; i++ ){
                    if( maskArray[i] && this.fixedCharsReg.test(maskArray[i]) )
                        valueArray.splice(i,0,maskArray[i]);
                }
                return valueArray;
            },
            
            // searches for fixed chars begining from the range start position, till it finds a non fixed
            __extraPositionsTill : function(rangeStart,maskArray){
                var extraPos = 0;
                while( this.fixedCharsReg.test(maskArray[rangeStart]) ){
                    rangeStart++;
                    extraPos++;
                }
                return extraPos;
            },
            
            // 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/
            __getRangePosition : function(input){
                if (!$.browser.msie) return {start: input.selectionStart, end: input.selectionEnd};
                var pos = {start: 0, end: 0},
                    range = document.selection.createRange();
                //if (!range || range.parentElement() != input) return pos;
                pos.start = 0 - range.duplicate().moveStart('character', -100000);
                pos.end = pos.start + range.text.length;
                return pos;
            }
            
        }
    });
    
    $.fn.extend({
        setMask : function(options){
            $.invalid
            return $.mask.set(this,options);
        },
        unsetMask : function(){
            return $.mask.unset(this);
        }
    });
})(jQuery);
