﻿/**
* 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: true, // 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:
            // <input type="text" name="some_name" id="some_name" alt="msk" />
            masks: {
                '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' },
                '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', 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 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 < defLen; i++) if (!this[i])
                    this[i] = defaultValue.charAt(i);

                return this;
            },

            // Removes values that doesnt match the mask from the valueArray
            // Returns the array without the invalid chars.
            __removeInvalidChars: function(valueArray, maskNonFixedCharsArray, repeatType) {
                // removes invalid chars
                for (var i = 0, y = 0; i < valueArray.length; i++) {
                    if (maskNonFixedCharsArray[y] &&
						this.rules[maskNonFixedCharsArray[y]] &&
						!this.rules[maskNonFixedCharsArray[y]].test(valueArray[i])) {
                        valueArray.splice(i, 1);
                        if (!repeatType) y--;
                        i--;
                    }
                    if (!repeatType) y++;
                }
                return valueArray;
            },

            // Apply the current input mask to the valueArray and returns it. 
            __applyMask: function(valueArray, maskArray, plus, fixedCharsReg) {
                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] && 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, fixedCharsReg) {
                var extraPos = 0;
                while (fixedCharsReg.test(maskArray[rangeStart++])) {
                    extraPos++;
                }
                return extraPos;
            },

            __getNextInput: function(input, selector) {
                var formEls = input.form.elements,
					initialInputIndex = $.inArray(input, formEls) + 1,
					$input = null,
					i;
                // look for next input on the form of the pased input
                for (i = initialInputIndex; i < formEls.length; i++) {
                    $input = $(formEls[i]);
                    if (this.__isNextInput($input, selector))
                        return $input;
                }

                var forms = document.forms,
					initialFormIndex = $.inArray(input.form, forms) + 1,
					y, tmpFormEls = null;
                // look for the next forms for the next input
                for (y = initialFormIndex; y < forms.length; y++) {
                    tmpFormEls = forms[y].elements;
                    for (i = 0; i < tmpFormEls.length; i++) {
                        $input = $(tmpFormEls[i]);
                        if (this.__isNextInput($input, selector))
                            return $input;
                    }
                }
                return null;
            },

            __isNextInput: function($formEl, selector) {
                var formEl = $formEl.get(0);
                return formEl
					&& (formEl.offsetWidth > 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) {
            return $.mask.set(this, options);
        },
        unsetMask: function() {
            return $.mask.unset(this);
        },
        //deprecated
        unmaskedVal: function() {
            return $.mask.unmaskedVal(this[0]);
        }
    });
})(jQuery);

