igorescobar / jQuery-Mask-Plugin

A jQuery Plugin to make masks on form fields and HTML elements.

Home Page:http://igorescobar.github.io/jQuery-Mask-Plugin/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Android 2.3.2 and earlier issue

opened this issue · comments

Noticed on US-phone mask input. Plugin sets cursor before the digit on inserting special symbols like '(', ')' or '-'.

Hi @nextcaller this is a known issue (#102 #63). Its a problem with how android older versions fires those events. I'm not putting all my energy/attention on fixing this problem because I think this is an issue that might be gone as long as mobile devices evolve. The default Android 2.3.x browsers it's like the IE6 of mobile browsers. It's just a matter of time to this issue be gone for good.

@nextcaller do you know an % of how many android users actually use android 2.3.x?

I just figured that 81% percent of android's users are using android 4.0.0+. Just 19% are using 2.3 or older.
http://developer.android.com/about/dashboards/index.html

@nextcaller I think that in about one year or so this issue might be gone for good ;-)

The same issue occurs on HTC oneX android 4.0.2 native browser.

Same problem with v.1.5.7?

On Android HTC one it got fixed by applying "-webkit-user-modify:read-write-plaintext-only", But on Android 2.3.6 it occurs the same problem with an updated v.1.5.7

My main goal is not Android 2.x. I'm trying to fix the issue with Android 4.x. Give me a few more days.

Ok I will wait for the fix on Android 2.x

any solution?

Sorry, i don't offer decision to solve the problem, but i want to offer just close the issues as "won't fix" because http://caniuse.com/usage_table.php as we can see android 2.3 has very small part < 0.5%

@Areks I'm starting to think that there is no fix to this problem. It's a browser issue and there is not much to do in this case... but... I'm always open to suggestions. I want to fix it... I just don't know how.

ola Igor verifiquei outro problema nas mascaras elas estão dando problemas quando são editados os campos no sistema android alguma ideia do que seja

@clezeraragon Não faço ideia. Eu não tenho Android... fica difícil testar/debugar em cima desta plataforma.

This bug still remains :(

Can you detail the exact bug and step by step to recreate? I will open a
ticket with our team to address.

On Fri, Aug 28, 2015 at 2:01 PM, ThiagoDosSantos notifications@github.com
wrote:

This bug still remains :(


Reply to this email directly or view it on GitHub
#135 (comment)
.

As you said, plugin sets cursor before the digit when using mask. Actually, the bug is not from the plugin itself, it's from android mobile. A workaround for this is to change the input type to "tel". It solved my problem when dealing with number masks, like date, money, etc.

I'm working with credit card number inputs, and changing the input type to 'tel' solved the cursor issue for me too.

Anyone if the issue could validate the latest version? Maybe the use of the input event could help.

the same issue occurs on Galaxy Note 3 - Android 5 - Google Chrome.
tested on http://igorescobar.github.io/jQuery-Mask-Plugin/

This bug also appears on modern UCWeb browsers

The force is with you guys. I don´t even have an Android to try to fix it. Any help would be appreciated.

I made a small change to my need:

`/**

  • jquery.mask.js
  • @Version: v1.14.0
  • @author: Igor Escobar
    *
  • Created by Igor Escobar on 2012-03-10. Please report any bug at http://blog.igorescobar.com
    *
  • Copyright (c) 2012 Igor Escobar http://blog.igorescobar.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.
    */

/* jshint laxbreak: true /
/
global define, jQuery, Zepto */

'use strict';

// UMD (Universal Module Definition) patterns for JavaScript modules that work everywhere.
// https://github.com/umdjs/umd/blob/master/jqueryPluginCommonjs.js
(function (factory) {

if (typeof define === 'function' && define.amd) {
    define(['jquery'], factory);
} else if (typeof exports === 'object') {
    module.exports = factory(require('jquery'));
} else {
    factory(jQuery || Zepto);
}

}(function ($) {

var Mask = function (el, mask, options) {

    var p = {
        invalid: [],
        getCaret: function () {
            try {
                var sel,
                    pos = 0,
                    ctrl = el.get(0),
                    dSel = document.selection,
                    cSelStart = ctrl.selectionStart;

                // IE Support
                if (dSel && navigator.appVersion.indexOf('MSIE 10') === -1) {
                    sel = dSel.createRange();
                    sel.moveStart('character', -p.val().length);
                    pos = sel.text.length;
                }
                // Firefox support
                else if (cSelStart || cSelStart === '0') {
                    pos = cSelStart;
                }

                return pos;
            } catch (e) {}
        },
        setCaret: function(pos) {
            try {
                if (el.is(':focus')) {
                    var range, ctrl = el.get(0);

                    // Firefox, WebKit, etc..
                    if (ctrl.setSelectionRange) {
                        ctrl.focus();
                        ctrl.setSelectionRange(pos, pos);
                    } else { // IE
                        range = ctrl.createTextRange();
                        range.collapse(true);
                        range.moveEnd('character', pos);
                        range.moveStart('character', pos);
                        range.select();
                    }
                }
            } catch (e) {}
        },
        events: function() {
            el
            .on('keydown.mask', function(e) {
                el.data('mask-keycode', e.keyCode || e.which);
                // added by Luciano
                var th = $(this);
                var ua = navigator.userAgent.toLowerCase();
                var isAndroid = ua.indexOf("android") > -1;
                if(isAndroid) {                    
                    setTimeout(function() {
                        var strLength= th.val().length;
                        th.focus();
                        th[0].setSelectionRange(strLength, strLength);
                    }, 100);
                }
            })
            .on($.jMaskGlobals.useInput ? 'input.mask' : 'keyup.mask', p.behaviour)
            .on('paste.mask drop.mask', function() {
                    setTimeout(function() {
                        el.keydown().keyup();
                    }, 100);                    
            })
            .on('change.mask', function(){
                el.data('changed', true);
            })
            .on('blur.mask', function(){
                if (oldValue !== p.val() && !el.data('changed')) {
                    el.trigger('change');
                }
                el.data('changed', false);
            })
            // it's very important that this callback remains in this position
            // otherwhise oldValue it's going to work buggy
            .on('blur.mask', function() {
                oldValue = p.val();
            })
            // select all text on focus
            .on('focus.mask', function (e) {
                if (options.selectOnFocus === true) {
                    $(e.target).select();
                }
            })
            // clear the value if it not complete the mask
            .on('focusout.mask', function() {
                if (options.clearIfNotMatch && !regexMask.test(p.val())) {
                   p.val('');
               }
            });
        },
        getRegexMask: function() {
            var maskChunks = [], translation, pattern, optional, recursive, oRecursive, r;

            for (var i = 0; i < mask.length; i++) {
                translation = jMask.translation[mask.charAt(i)];

                if (translation) {

                    pattern = translation.pattern.toString().replace(/.{1}$|^.{1}/g, '');
                    optional = translation.optional;
                    recursive = translation.recursive;

                    if (recursive) {
                        maskChunks.push(mask.charAt(i));
                        oRecursive = {digit: mask.charAt(i), pattern: pattern};
                    } else {
                        maskChunks.push(!optional && !recursive ? pattern : (pattern + '?'));
                    }

                } else {
                    maskChunks.push(mask.charAt(i).replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'));
                }
            }

            r = maskChunks.join('');

            if (oRecursive) {
                r = r.replace(new RegExp('(' + oRecursive.digit + '(.*' + oRecursive.digit + ')?)'), '($1)?')
                     .replace(new RegExp(oRecursive.digit, 'g'), oRecursive.pattern);
            }

            return new RegExp(r);
        },
        destroyEvents: function() {
            el.off(['input', 'keydown', 'keyup', 'paste', 'drop', 'blur', 'focusout', ''].join('.mask '));
        },
        val: function(v) {
            var isInput = el.is('input'),
                method = isInput ? 'val' : 'text',
                r;

            if (arguments.length > 0) {
                if (el[method]() !== v) {
                    el[method](v);
                }
                r = el;
            } else {
                r = el[method]();
            }

            return r;
        },
        getMCharsBeforeCount: function(index, onCleanVal) {
            for (var count = 0, i = 0, maskL = mask.length; i < maskL && i < index; i++) {
                if (!jMask.translation[mask.charAt(i)]) {
                    index = onCleanVal ? index + 1 : index;
                    count++;
                }
            }
            return count;
        },
        caretPos: function (originalCaretPos, oldLength, newLength, maskDif) {
            var translation = jMask.translation[mask.charAt(Math.min(originalCaretPos - 1, mask.length - 1))];

            return !translation ? p.caretPos(originalCaretPos + 1, oldLength, newLength, maskDif)
                                : Math.min(originalCaretPos + newLength - oldLength - maskDif, newLength);
        },
        behaviour: function(e) {
            e = e || window.event;
            p.invalid = [];

            var keyCode = el.data('mask-keycode');

            if ($.inArray(keyCode, jMask.byPassKeys) === -1) {
                var caretPos    = p.getCaret(),
                    currVal     = p.val(),
                    currValL    = currVal.length,
                    newVal      = p.getMasked(),
                    newValL     = newVal.length,
                    maskDif     = p.getMCharsBeforeCount(newValL - 1) - p.getMCharsBeforeCount(currValL - 1),
                    changeCaret = caretPos < currValL;

                p.val(newVal);

                if (changeCaret) {
                    // Avoid adjusting caret on backspace or delete
                    if (!(keyCode === 8 || keyCode === 46)) {
                        caretPos = p.caretPos(caretPos, currValL, newValL, maskDif);
                    }
                    p.setCaret(caretPos);
                }

                return p.callbacks(e);
            }
        },
        getMasked: function(skipMaskChars, val) {
            var buf = [],
                value = val === undefined ? p.val() : val + '',
                m = 0, maskLen = mask.length,
                v = 0, valLen = value.length,
                offset = 1, addMethod = 'push',
                resetPos = -1,
                lastMaskChar,
                check;

            if (options.reverse) {
                addMethod = 'unshift';
                offset = -1;
                lastMaskChar = 0;
                m = maskLen - 1;
                v = valLen - 1;
                check = function () {
                    return m > -1 && v > -1;
                };
            } else {
                lastMaskChar = maskLen - 1;
                check = function () {
                    return m < maskLen && v < valLen;
                };
            }

            while (check()) {
                var maskDigit = mask.charAt(m),
                    valDigit = value.charAt(v),
                    translation = jMask.translation[maskDigit];

                if (translation) {
                    if (valDigit.match(translation.pattern)) {
                        buf[addMethod](valDigit);
                         if (translation.recursive) {
                            if (resetPos === -1) {
                                resetPos = m;
                            } else if (m === lastMaskChar) {
                                m = resetPos - offset;
                            }

                            if (lastMaskChar === resetPos) {
                                m -= offset;
                            }
                        }
                        m += offset;
                    } else if (translation.optional) {
                        m += offset;
                        v -= offset;
                    } else if (translation.fallback) {
                        buf[addMethod](translation.fallback);
                        m += offset;
                        v -= offset;
                    } else {
                      p.invalid.push({p: v, v: valDigit, e: translation.pattern});
                    }
                    v += offset;
                } else {
                    if (!skipMaskChars) {
                        buf[addMethod](maskDigit);
                    }

                    if (valDigit === maskDigit) {
                        v += offset;
                    }

                    m += offset;
                }
            }

            var lastMaskCharDigit = mask.charAt(lastMaskChar);
            if (maskLen === valLen + 1 && !jMask.translation[lastMaskCharDigit]) {
                buf.push(lastMaskCharDigit);
            }

            return buf.join('');
        },
        callbacks: function (e) {
            var val = p.val(),
                changed = val !== oldValue,
                defaultArgs = [val, e, el, options],
                callback = function(name, criteria, args) {
                    if (typeof options[name] === 'function' && criteria) {
                        options[name].apply(this, args);
                    }
                };

            callback('onChange', changed === true, defaultArgs);
            callback('onKeyPress', changed === true, defaultArgs);
            callback('onComplete', val.length === mask.length, defaultArgs);
            callback('onInvalid', p.invalid.length > 0, [val, e, el, p.invalid, options]);
        }
    };

    el = $(el);
    var jMask = this, oldValue = p.val(), regexMask;

    mask = typeof mask === 'function' ? mask(p.val(), undefined, el,  options) : mask;


    // public methods
    jMask.mask = mask;
    jMask.options = options;
    jMask.remove = function() {
        var caret = p.getCaret();
        p.destroyEvents();
        p.val(jMask.getCleanVal());
        p.setCaret(caret - p.getMCharsBeforeCount(caret));
        return el;
    };

    // get value without mask
    jMask.getCleanVal = function() {
       return p.getMasked(true);
    };

    // get masked value without the value being in the input or element
    jMask.getMaskedVal = function(val) {
       return p.getMasked(false, val);
    };

   jMask.init = function(onlyMask) {
        onlyMask = onlyMask || false;
        options = options || {};

        jMask.clearIfNotMatch  = $.jMaskGlobals.clearIfNotMatch;
        jMask.byPassKeys       = $.jMaskGlobals.byPassKeys;
        jMask.translation      = $.extend({}, $.jMaskGlobals.translation, options.translation);

        jMask = $.extend(true, {}, jMask, options);

        regexMask = p.getRegexMask();

        if (onlyMask === false) {

            if (options.placeholder) {
                el.attr('placeholder' , options.placeholder);
            }

            // this is necessary, otherwise if the user submit the form
            // and then press the "back" button, the autocomplete will erase
            // the data. Works fine on IE9+, FF, Opera, Safari.
            if (el.data('mask')) {
              el.attr('autocomplete', 'off');
            }

            p.destroyEvents();
            p.events();

            var caret = p.getCaret();
            p.val(p.getMasked());
            p.setCaret(caret + p.getMCharsBeforeCount(caret, true));

        } else {
            p.events();
            p.val(p.getMasked());
        }
    };

    jMask.init(!el.is('input'));
};

$.maskWatchers = {};
var HTMLAttributes = function () {
    var input = $(this),
        options = {},
        prefix = 'data-mask-',
        mask = input.attr('data-mask');

    if (input.attr(prefix + 'reverse')) {
        options.reverse = true;
    }

    if (input.attr(prefix + 'clearifnotmatch')) {
        options.clearIfNotMatch = true;
    }

    if (input.attr(prefix + 'selectonfocus') === 'true') {
       options.selectOnFocus = true;
    }

    if (notSameMaskObject(input, mask, options)) {
        return input.data('mask', new Mask(this, mask, options));
    }
},
notSameMaskObject = function(field, mask, options) {
    options = options || {};
    var maskObject = $(field).data('mask'),
        stringify = JSON.stringify,
        value = $(field).val() || $(field).text();
    try {
        if (typeof mask === 'function') {
            mask = mask(value);
        }
        return typeof maskObject !== 'object' || stringify(maskObject.options) !== stringify(options) || maskObject.mask !== mask;
    } catch (e) {}
},
eventSupported = function(eventName) {
    var el = document.createElement('div'), isSupported;

    eventName = 'on' + eventName;
    isSupported = (eventName in el);

    if ( !isSupported ) {
        el.setAttribute(eventName, 'return;');
        isSupported = typeof el[eventName] === 'function';
    }
    el = null;

    return isSupported;
};

$.fn.mask = function(mask, options) {
    options = options || {};
    var selector = this.selector,
        globals = $.jMaskGlobals,
        interval = globals.watchInterval,
        watchInputs = options.watchInputs || globals.watchInputs,
        maskFunction = function() {
            if (notSameMaskObject(this, mask, options)) {
                return $(this).data('mask', new Mask(this, mask, options));

            }
        };

    $(this).each(maskFunction);

    if (selector && selector !== '' && watchInputs) {
        clearInterval($.maskWatchers[selector]);
        $.maskWatchers[selector] = setInterval(function(){
            $(document).find(selector).each(maskFunction);
        }, interval);
    }
    return this;
};

$.fn.masked = function(val) {
    return this.data('mask').getMaskedVal(val);
};

$.fn.unmask = function() {
    clearInterval($.maskWatchers[this.selector]);
    delete $.maskWatchers[this.selector];
    return this.each(function() {
        var dataMask = $(this).data('mask');
        if (dataMask) {
            dataMask.remove().removeData('mask');
        }
    });
};

$.fn.cleanVal = function() {
    return this.data('mask').getCleanVal();
};

$.applyDataMask = function(selector) {
    selector = selector || $.jMaskGlobals.maskElements;
    var $selector = (selector instanceof $) ? selector : $(selector);
    $selector.filter($.jMaskGlobals.dataMaskAttr).each(HTMLAttributes);
};

var globals = {
    maskElements: 'input,td,span,div',
    dataMaskAttr: '*[data-mask]',
    dataMask: true,
    watchInterval: 300,
    watchInputs: true,
    useInput: eventSupported('input'),
    watchDataMask: false,
    byPassKeys: [9, 16, 17, 18, 36, 37, 38, 39, 40, 91],
    translation: {
        '0': {pattern: /\d/},
        '9': {pattern: /\d/, optional: true},
        '#': {pattern: /\d/, recursive: true},
        'A': {pattern: /[a-zA-Z0-9]/},
        'S': {pattern: /[a-zA-Z]/}
    }
};

$.jMaskGlobals = $.jMaskGlobals || {};
globals = $.jMaskGlobals = $.extend(true, {}, globals, $.jMaskGlobals);

// looking for inputs with data-mask attribute
if (globals.dataMask) {
    $.applyDataMask();
}

setInterval(function() {
    if ($.jMaskGlobals.watchDataMask) {
        $.applyDataMask();
    }
}, globals.watchInterval);

}));
`

@lucianoprevedello Thanks! It worked here...

Can you guys give this a try and let me know?
#464

Guys... lets all talk into the PR #464. I'm going to close this so we can organize this talk in only one thread.

@lucianoprevedello Thanks, worked here too :)

@DieguitoBueno @joaomelont be careful. this batch works only on the scenario when the user keeps type without erasing nothing on the middle. If you type something and then go to the middle of the text and change something things will get messy... I really need an Android in which I could reproduce this... It's really hard to debug this without having the device.

Guys, I believe this is now fixed. Please, upgrade your jQuery Mask Plugin.