﻿/**
* jquery.meiomask.js
*
* @version 1.0.1
* 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($) {

    $.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!
            keys: {
                BKSPACE: 8,
                TAB: 9,
                ENTER: 13,
                SHIFT: 16,
                CTRL: 17,
                ALT: 18,
                SPACE: 32,
                PGUP: 33,
                PGDOWN: 34,
                END: 35,
                HOME: 36,
                LEFT: 37,
                UP: 38,
                RIGHT: 39,
                DOWN: 40,
                INSERT: 45,
                DELETE: 46,
                METAKEY: 91, // 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, at least at firefox.
                F5: 116

            },

            // default settings
            options: {
                attr: 'title' // an attr to look for the mask name or the mask itself
            },

            // masks. You may add yours!
            // Ex: $.fn.setMask.masks.msk = {mask: '999'}
            // and then if the 'attr' options value is 'alt', your input shoul look like:
            // <input type="text" name="some_name" id="some_name" alt="msk" />
            masks: {
                'TELEFONE': { mask: '(99) 9999-9999' },
                'CPF': { mask: '999.999.999-99' },
                'CNPJ': { mask: '99.999.999/9999-99' },
                'DATA': { mask: '99/99/9999' },
                'CEP': { mask: '99.999-999' },
                'HORA': { mask: '29:59' },
                'INTEIRO': { mask: '999.999.999.999', type: 'reverse' },
                'INTEIRO(2_CASAS)': { mask: '99', type: 'reverse' },
                'INTEIRO(3_CASAS)': { mask: '999', type: 'reverse' },
                'INTEIRO(4_CASAS)': { mask: '999.9', type: 'reverse' },
                'INTEIRO(8_CASAS)': { mask: '999.999.99', type: 'reverse' },
                'NUMERICO': { mask: '99,999.999.999.999', type: 'reverse' },
                'GUID': { mask: '********-****-****-****-************' },
                'MOEDA_REAL': { mask: '99,999.999.999.999', type: 'reverse' }
            },

            init: function(options) {
                var i;
                this.ignore = false;
                this.fixedCharsReg = new RegExp(this.fixedChars);
                this.fixedCharsRegG = new RegExp(this.fixedChars, 'g');
                this.ignoreArray = new Array();
                this.setOptions(options);
                // constructs number rules
                for (i = 0; i <= 9; i++) {
                    this.rules[i] = new RegExp('[0-' + i + ']');
                }
                //we gonna ignore these keys while using the mask
                for (i in this.keys) {
                    this.ignoreArray.push(this.keys[i]);
                }
                this.hasInit = true;
            },

            setOptions: function(options) {
                this.options = $.extend({}, this.options, options);
            },

            set: function(el, options) {

                var maskObj = this,
					$el = $(el),
					mlStr = 'maxlength';
                // if has not inited...
                // you could call init before the first setMask, what is best for your purpose
                if (this.hasInit)
                    this.setOptions(options);
                else
                    this.init(options);

                return $el.each(function() {

                    var $this = $(this),
						attrValue = $this.attr(maskObj.options.attr),
						tmpMask = '',
						pasteEvent,
						o = {
                        mask: null,
                        type: 'fixed'
                    };

                    // then we look for the 'attr' option
                    tmpMask = (typeof options == 'string') ? options : (attrValue != '') ? attrValue : null;
                    o.mask = tmpMask;
                    // then we see if it's a defined mask
                    if ((maskObj.masks[tmpMask])) o = $.extend(o, maskObj.masks[tmpMask]);
                    // then we look for some metadata on the input
                    if (($.metadata)) o = $.extend(o, $this.metadata());
                    var mask = o.mask;
                    if (mask != null) {
                        maskObj.unset($this);
                        $this.data('mask', {
                            maxlength: $this.attr(mlStr),
                            mask: mask,
                            maskArray: mask.split(''),
                            maskNonFixedCharsArray: mask.replace(maskObj.fixedCharsRegG, '').split(''),
                            type: o.type
                        });

                        // in safari, chrome and ie, if the attr maxlength is not
                        // set it will have a very high value... we dont need maxlength
                        // to be set so if its set by the user we set it to a very high value.
                        // removing the attribute in IE will set maxlength to a null string, thats why we don't remove it
                        if ($.browser.safari) {
                            if ($this.attr(mlStr) < 524288) { //$this is the maxlength value while its not set (webkit)
                                $this.removeAttr(mlStr);
                            }
                        }
                        else if ($.browser.msie) {
                            if ($this.attr(mlStr) < 2147483647) { //$this is the maxlength value while its not set (ie)
                                $this.attr(mlStr, 2147483647);
                            }
                        }
                        else {
                            // in mozilla and opera if maxlength is not set it will have value -1.
                            // Removing the attribute if it is set will solve the problem
                            if ($this.attr(mlStr) > -1) {
                                $this.removeAttr(mlStr);
                            }
                        }
                        if (o.type == 'reverse') {
                            $this.css('text-align', 'right');
                        }

                        $this.bind('keydown', { funcFixed: maskObj._keyDown, funcReverse: maskObj._keyDown, thisObj: maskObj }, maskObj._onMask);
                        $this.bind('keyup', { funcFixed: maskObj._keyUp, funcReverse: maskObj._keyUp, thisObj: maskObj }, maskObj._onMask);
                        $this.bind('keypress', { funcFixed: maskObj._keyPressFixed, funcReverse: maskObj._keyPressReverse, thisObj: maskObj }, maskObj._onMask);
                        // 'input' event fires on every keyboard event on the input
                        pasteEvent = ($.browser.opera || ($.browser.mozilla && $.browser.version.substr(0, 3) == '1.8')) ? 'input' : 'paste';
                        $this.bind(pasteEvent, { funcFixed: maskObj._paste, funcReverse: maskObj._paste, thisObj: maskObj }, maskObj._delayedOnMask);
                    }
                });
            },

            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;
                        if (maxLength == -1)
                            $this.removeAttr('maxlength');
                        else
                            $this.attr('maxlength', maxLength);
                        $this.unbind('keydown', _this._onMask).unbind('keypress', _this._onMask).unbind('keyup', _this._onMask);
                        if ($.browser.opera || ($.browser.mozilla && $.browser.version.substr(0, 3) == '1.8')) {
                            $this.unbind('input', _this._delayedOnMask);
                        }
                        else {
                            $this.unbind('paste', _this._delayedOnMask);
                        }
                        $this.removeData('mask');
                    }
                });
            },

            _onMask: function(e) {
                var thisObj = e.data.thisObj,
					o = {};
                o._this = e.target;
                o.$this = $(o._this);
                o.value = o.$this.val();
                o.valueArray = o.value.split('');
                o.data = o.$this.data('mask');
                o.nKey = thisObj.__getKeyNumber(e);
                o.caret = thisObj.__getRangePosition(o._this);
                o.reverse = (o.data.type == 'reverse');
                if (o.reverse) {
                    return e.data.funcReverse.call(thisObj, e, o);
                } else {
                    return e.data.funcFixed.call(thisObj, e, o);
                }
                return true;
            },
            // the timeout is set because on ie we can't get the value from the input without it
            _delayedOnMask: function(e) {
                setTimeout(function() { e.data.thisObj._onMask(e) }, 1);
            },

            _keyDown: function(e, o) {
                if ($.inArray(o.nKey, this.ignoreArray) > -1) {
                    this.ignore = true;
                }
                else {
                    this.ignore = false;
                }
                return true;
            },

            _keyUp: function(e, o) {
                switch (o.data.type) {
                    case 'reverse':
                        if (o.nKey == this.keys.BKSPACE || o.nKey == this.keys.DELETE) {
                            this.ignore = false;
                            this._keyPressReverse(e, o);
                        }
                        else {
                            this._paste(e, o);
                        }
                        break;
                    case 'fixed':
                        this._paste(e, o);
                        break;
                }
                return true;
            },

            _paste: function(e, o) {
                var valueArray = o.valueArray;
                valueArray = this.__removeInvalidChars(valueArray, o.data.maskNonFixedCharsArray);
                if (o.reverse) valueArray.reverse();
                valueArray = this.__applyMask(valueArray, o.data.maskArray);
                if (o.reverse) {
                    valueArray.reverse();
                    valueArray = valueArray.join('').substring(valueArray.length - o.data.mask.length);
                }
                else
                    valueArray = valueArray.join('').substring(0, o.data.mask.length);
                o.$this.val(valueArray);
                this.__setRange(o._this, o.caret.start, o.caret.end);
                return true;
            },

            _keyPressReverse: function(e, o) {

                if (this.ignore) return true;
                if (e.ctrlKey || e.metaKey || e.altKey) return true;

                var c = String.fromCharCode(o.nKey),
					caretStart = o.caret.start,
					rawValue = o.value,
                // the input value from the range start to the value start
					valueStart = rawValue.substr(0, caretStart),
                // the input value from the range end to the value end
					valueEnd = rawValue.substr(o.caret.end, rawValue.length),
					value = (valueStart + c + valueEnd).replace(this.fixedCharsRegG, ''),
					valueArray = value.split(''),
                // searches for fixed chars begining from the caret start position, till it finds a non fixed
					extraPos = this.__extraPositionsTill(caretStart, o.data.maskArray);

                if (!this.rules[o.data.maskArray[caretStart + extraPos]] ||
					!this.rules[o.data.maskArray[caretStart + extraPos]].test(c) &&
					(o.nKey != this.keys.BKSPACE && o.nKey != this.keys.DELETE)) //so it can remask after a backspace or delete
                    return false;

                valueArray = this.__removeInvalidChars(valueArray, o.data.maskNonFixedCharsArray);
                valueArray.reverse();
                valueArray = this.__applyMask(valueArray, o.data.maskArray);
                valueArray.reverse();
                o.$this.val(valueArray.join('').substring(valueArray.length - o.data.mask.length));
                return false;
            },

            _keyPressFixed: function(e, o) {

                if (this.ignore) return true;
                if (e.ctrlKey || e.metaKey || e.altKey) return true;

                var c = String.fromCharCode(o.nKey),
					rawValue = o.value,
					value = rawValue.replace(this.fixedCharsRegG, ''),
					valueArray = value.split(''),
					caretStart = o.caret.start,
					extraPos = this.__extraPositionsTill(caretStart, o.data.maskArray);

                if (!this.rules[o.data.maskArray[caretStart + extraPos]] ||
					!this.rules[o.data.maskArray[caretStart + extraPos]].test(c))
                    return false;

                valueArray = this.__removeInvalidChars(valueArray, o.data.maskNonFixedCharsArray);
                valueArray = this.__applyMask(valueArray, o.data.maskArray, extraPos);

                o.$this.val(valueArray.join('').substr(0, o.data.mask.length));

                if (caretStart == o.caret.end) {
                    if (caretStart + extraPos < rawValue.length)//opera fix, cant have caret value bigger than value length
                        this.__setRange(o._this, caretStart + extraPos, caretStart + extraPos + 1);
                }
                else
                    this.__setRange(o._this, caretStart, o.caret.end);

                return true;
            },

            __getKeyNumber: function(e) {
                return (e.charCode || e.keyCode || e.which);
            },

            // 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 (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 (this.fixedCharsReg.test(maskArray[i]))
                        valueArray.splice(i, 0, maskArray[i]);
                }
                return valueArray;
            },

            // searches for fixed chars begining from the caret start position, till it finds a non fixed
            __extraPositionsTill: function(caretStart, maskArray) {
                var extraPos = 0;
                while (this.fixedCharsReg.test(maskArray[caretStart])) {
                    caretStart++;
                    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();
                }
            },

            // a comment at http://www.bazon.net/mishoo/articles.epl?art_id=1292
            __getRangePosition: function(input) {
                var result = { start: 0, end: 0 };
                if (input.setSelectionRange) {
                    result.start = input.selectionStart;
                    result.end = input.selectionEnd;
                }
                else if (document.selection && document.selection.createRange) {
                    var range = document.selection.createRange();
                    var r2 = range.duplicate();
                    result.start = 0 - r2.moveStart('character', -100000);
                    result.end = result.start + range.text.length;
                }
                return result;
            }


        }
    });

    $.fn.setMask = function(options) {
        return $.mask.set(this, options);
    };

    $.fn.unsetMask = function() {
        return $.mask.unset(this);
    };

})(jQuery);
