Skip to content

Android 2.3.2 and earlier issue #135

Closed

Description

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

Activity

  1. igorescobar commented on Apr 8, 2014

    @igorescobar
    Owner

    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.

  2. igorescobar commented on Apr 8, 2014

    @igorescobar
    Owner

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

  3. igorescobar commented on Apr 8, 2014

    @igorescobar
    Owner

    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

  4. igorescobar commented on Apr 8, 2014

    @igorescobar
    Owner

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

  5. sreeyashu commented on Apr 14, 2014

    @sreeyashu

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

  6. igorescobar commented on May 1, 2014

    @igorescobar
    Owner

    Same problem with v.1.5.7?

  7. sreeyashu commented on May 2, 2014

    @sreeyashu

    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

  8. igorescobar commented on May 5, 2014

    @igorescobar
    Owner

    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.

  9. sreeyashu commented on May 7, 2014

    @sreeyashu

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

  10. CavalcanteLeo commented on Oct 20, 2014

    @CavalcanteLeo

    any solution?

  11. Areks commented on Nov 14, 2014

    @Areks

    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%

  12. igorescobar commented on Nov 14, 2014

    @igorescobar
    Owner

    @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.

  13. clezeraragon commented on Mar 23, 2015

    @clezeraragon

    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

  14. igorescobar commented on Mar 23, 2015

    @igorescobar
    Owner

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

  15. 5 remaining items

  16. lelotnk commented on Apr 8, 2016

    @lelotnk

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

  17. aChudinov commented on May 19, 2016

    @aChudinov

    This bug also appears on modern UCWeb browsers

  18. igorescobar commented on May 19, 2016

    @igorescobar
    Owner

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

  19. CavalcanteLeo commented on May 19, 2016

    @CavalcanteLeo
  20. lucianoprevedello commented on May 19, 2016

    @lucianoprevedello

    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);
    

    }));
    `

  21. joaomelont commented on Jun 3, 2016

    @joaomelont

    @lucianoprevedello Thanks! It worked here...

  22. igorescobar commented on Nov 19, 2016

    @igorescobar
    Owner

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

  23. igorescobar commented on Nov 25, 2016

    @igorescobar
    Owner

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

  24. DiegoBuenoCoelho-zz commented on Dec 22, 2016

    @DiegoBuenoCoelho-zz

    @lucianoprevedello Thanks, worked here too :)

  25. igorescobar commented on Dec 22, 2016

    @igorescobar
    Owner
  26. igorescobar commented on Dec 23, 2016

    @igorescobar
    Owner

    @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.

  27. igorescobar commented on Dec 24, 2016

    @igorescobar
    Owner

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions