Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Android 2.3.2 and earlier issue #135

Closed
nextcaller opened this issue Apr 8, 2014 · 31 comments
Closed

Android 2.3.2 and earlier issue #135

nextcaller opened this issue Apr 8, 2014 · 31 comments
Labels

Comments

@nextcaller
Copy link

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

@igorescobar igorescobar added the bug label Apr 8, 2014
@igorescobar
Copy link
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.

@igorescobar
Copy link
Owner

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

@igorescobar
Copy link
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

@igorescobar
Copy link
Owner

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

@sreeyashu
Copy link

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

@igorescobar
Copy link
Owner

Same problem with v.1.5.7?

@sreeyashu
Copy link

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

@igorescobar
Copy link
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.

@sreeyashu
Copy link

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

@CavalcanteLeo
Copy link

any solution?

@Areks
Copy link

Areks commented Nov 14, 2014

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%

@igorescobar
Copy link
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.

This was referenced Mar 18, 2015
@clezeraragon
Copy link

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

@igorescobar
Copy link
Owner

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

@ThiagoDosSantos
Copy link

This bug still remains :(

@nextcaller
Copy link
Author

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

@ThiagoDosSantos
Copy link

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.

@elijahpaul
Copy link

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

@igorescobar
Copy link
Owner

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

@lelotnk
Copy link

lelotnk commented Apr 8, 2016

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

@aChudinov
Copy link

This bug also appears on modern UCWeb browsers

@igorescobar
Copy link
Owner

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

@CavalcanteLeo
Copy link

try https://www.genymotion.com/

@lucianoprevedello
Copy link

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

}));
`

@joaomelont
Copy link

@lucianoprevedello Thanks! It worked here...

@igorescobar
Copy link
Owner

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

@igorescobar
Copy link
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.

@DiegoBuenoCoelho-zz
Copy link

@lucianoprevedello Thanks, worked here too :)

@igorescobar
Copy link
Owner

@igorescobar
Copy link
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.

@igorescobar igorescobar reopened this Dec 23, 2016
@igorescobar
Copy link
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
Labels
Projects
None yet
Development

No branches or pull requests