/** * jquery.meio.mask.js * @author: fabiomcosta * @version: 1.1.14 * * 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($) { // https://github.com/jquery/jquery-migrate/blob/master/src/core.js#L50 if (!$.browser) { var uaMatch = function(ua) { ua = ua.toLowerCase(); var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || /(webkit)[ \/]([\w.]+)/.exec(ua) || /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || /(msie) ([\w.]+)/.exec(ua) || ua.indexOf('compatible') < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || []; return match[2] || '0'; }; $.browser = { mozilla: /mozilla/.test(navigator.userAgent.toLowerCase()) && !/webkit/.test(navigator.userAgent.toLowerCase()), webkit: /webkit/.test(navigator.userAgent.toLowerCase()), opera: /opera/.test(navigator.userAgent.toLowerCase()), msie: /msie/.test(navigator.userAgent.toLowerCase()), android: (navigator.userAgent.toLowerCase().indexOf('mozilla/5.0') > -1 && navigator.userAgent.toLowerCase().indexOf('android ') > -1 && navigator.userAgent.toLowerCase().indexOf('applewebkit') > -1), version: uaMatch(navigator.userAgent) }; } var isMobile = (window.orientation != null); // browsers like firefox2 and before and opera doesnt have the onPaste event, but the paste feature can be done with the onInput event. var pasteEvent = (($.browser.opera || ($.browser.mozilla && parseFloat($.browser.version.substr(0, 3)) < 1.9)) ? 'input' : 'paste'); // the timeout is set because we can't get the value from the input without it var pasteHandler = function(e) { e = $.event.fix(e || window.event); e.type = 'paste'; var el = e.target; setTimeout(function() { $.event.dispatch.call(el, e); }, 1); }; $.event.special.paste = { setup: function() { if (this.addEventListener) this.addEventListener(pasteEvent, pasteHandler, false); else if (this.attachEvent) this.attachEvent('on' + pasteEvent, pasteHandler); }, teardown: function() { if (this.removeEventListener) this.removeEventListener(pasteEvent, pasteHandler, false); else if (this.detachEvent) this.detachEvent('on' + pasteEvent, pasteHandler); } }; $.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' }, 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() {}, onFocus: function(input, evt) {}, onBlur: function(input, evt) {} }, // 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: { 'phone': { mask: '(99) 9999-9999' }, 'phone-us': { mask: '(999) 999-9999' }, 'cpf': { mask: '999.999.999-99' }, // cadastro nacional de pessoa fisica (kind of a brazillian ssn) '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 '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' }, // Usado para informar quilometragem. 'km': { mask: '999.999', type: 'reverse' } }, init: function() { // if has not inited... if (!this.hasInit) { var self = this, i, keyRep = 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, 10)); }); 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) { // prevents javascript automatic type convertion o.mask += ''; 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 kept when the mask is removed) 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('drop.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(); // trigger mask function dataObj.onFocus(this, e); }, _onBlur: function(e) { var $this = $(this), dataObj = $this.data('mask'); if (dataObj.inputFocusValue != $this.val() && !dataObj.changed) $this.trigger('change'); // trigger mask function dataObj.onBlur(this, e); }, _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); o.data = o.$this.data('mask'); if (o.$this.attr('readonly') || !o.data) { return true; } 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) && e.key); if (this.ignore) { var rep = this.keyRep[o.nKey]; o.data.onValid.call(o._this, rep || '', o.nKey); } return 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.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... 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); if (!o.repeat) { 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 because theres an unwanted behavior 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 form = input.form; if (form == null) { return null; } var formEls = form.elements, initialInputIndex = $.inArray(input, formEls) + 1, len = formEls.length, $input = null, i; // look for next input on the form of the pased input for (i = initialInputIndex; i < len; i++) { $input = $(formEls[i]); if (this.__isNextInput($input, selector)) { return $input; } } var forms = document.forms, initialFormIndex = $.inArray(input.form, forms) + 1, y, tmpFormEls, _len = forms.length; // look for the next forms for the next input for (y = initialFormIndex; y < _len; y++) { tmpFormEls = forms[y].elements; len = tmpFormEls.length; for (i = 0; i < len; 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 && !$.browser.android) 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);