简体   繁体   中英

Do mobile webkit browsers have a rounding issue in JS?

I'm trying to debug an issue with some javascript slider code on mobile browsers. It appears to be a rounding error which only occurs on mobile devices. The following code will work fine on the desktop (eg Chrome) but the increase button fails to work on higher values on the slider when viewed in Webkit on a smartphone eg iPhone iOS 5/6, Samsung S2 ICS.

Try this http://jsfiddle.net/codecowboy/mLpfu/ . Click the 'debug on mobile' button - its directly adjacent to the Run button top left (you need to be logged in to see this button). Enter the url generated into a browser on a smartphone (preferably a webkit browser on iPhone / Android).

Drag the slider to say 265, hit increase. Some values will let you hit increase, some won't. The higher the value, the worse the problem gets.

The code is using jQuery and the noUISlider plugin. The button click code is:

var btnIncrease= document.getElementById("increase");
        btnIncrease.addEventListener('click',function(e) {

            var slider = $("#noUiSlider");
            console.log(e);
            var value = slider.noUiSlider('value')[1]; //the 'value' method returns an array.
            console.log('value pre move '+value);
            value = value+1;
            slider.noUiSlider("move", { knob : 0, to:  parseInt(value,10) });

            console.log(slider.noUiSlider('value')[1]);


        });

Can anyone explain what is causing this? Could this be a Big / Little Endian issue? Or a bug in jQuery?

The above code calls the nouislider plugin, source here:

(function( $ ){

$.fn.noUiSlider = function( method, options ) {

    function neg(a){ return a<0; }
    function abs(a){ return Math.abs(a); }
    function roundTo(a,b) { return Math.round(a / b) * b; }
    function dup(a){ return jQuery.extend(true, {}, a); }

    var defaults, methods, helpers, options = options||[], functions, touch = ('ontouchstart' in document.documentElement);

    defaults = {

    /*
     * {handles}            Specifies the number of handles. (init)
     * [INT]                1, 2
     */
        'handles'   :       2,
    /*
     * {connect}            Whether to connect the middle bar to the handles. (init)
     * [MIXED]              "upper", "lower", false, true
     */
        'connect'   :       true,
    /*
     * {scale};             The values represented by the slider handles. (init,move,value)
     * [ARRAY]              [-+x,>x]
     */
        'scale'     :       [0,100],
    /*
     * {start}              The starting positions for the handles, mapped to {scale}. (init)
     * [ARRAY][INT]         [y>={scale[0]}, y=<{scale[1]}], integer in range.
     */
        'start'     :       [25,75],
    /*
     * {to}                 The position to move a handle to. (move)
     * [INT]                Any, but will be corrected to match z > {scale[0]} || _l, z < {scale[1]} || _u
     */
        'to'        :       0,
    /*
     * {handle}             The handle to move. (move)
     * [MIXED]              0,1,"lower","upper"
     */
        'handle'    :       0,
    /*
     * {change}             The function to be called on every change. (init)
     * [FUNCTION]           param [STRING]'move type'
     */
        'change'    :       '',
    /*
     * {end}                The function when a handle is no longer being changed. (init)
     * [FUNCTION]           param [STRING]'move type'
     */
        'end'       :       '',
    /*
     * {step}               Whether, and at what intervals, the slider should snap to a new position. Adheres to {scale} (init)
     * [MIXED]              <x, FALSE
     */
        'step'      :       false,
    /*
     * {save}               Whether a scale give to a function should become the default for the slider it is called on. (move,value)
     * [BOOLEAN]            true, false
     */
        'save'      :       false,
    /*
     * {click}              Whether the slider moves by clicking the bar
     * [BOOLEAN]            true, false
     */
        'click'     :       true

    };

    helpers = {

        scale:              function( a, b, c ){    
            var d = b[0],e = b[1];
            if(neg(d)){
                a=a+abs(d);
                e=e+abs(d);
            } else {
                a=a-d;
                e=e-d;
            }
            return (a*c)/e;
        },
        deScale:            function( a, b, c ){
            var d = b[0],e = b[1];
            e = neg(d) ? e + abs(d) : e - d;
            return ((a*e)/c) + d;                   
        },
        connect:            function( api ){

            if(api.connect){

                if(api.handles.length>1){
                    api.connect.css({'left':api.low.left(),'right':(api.slider.innerWidth()-api.up.left())});
                } else {
                    api.low ? api.connect.css({'left':api.low.left(),'right':0}) : api.connect.css({'left':0,'right':(api.slider.innerWidth()-api.up.left())});
                }

            }

        },
        left:               function(){
            return parseFloat($(this).css('left'));
        },
        call:               function( f, t, n ){
            if ( typeof(f) == "function" ){ f.call(t, n) }
        },
        bounce:             function( api, n, c, handle ){

            var go = false;

            if( handle.is( api.up ) ){

                if( api.low && n < api.low.left() ){

                    n = api.low.left();
                    go=true;

                }

            } else {

                if( api.up && n > api.up.left() ){

                    n = api.up.left();
                    go=true;

                }

            }

            if ( n > api.slider.innerWidth() ){

                n = api.slider.innerWidth()

                go=true;

            } else if( n < 0 ){

                n = 0;
                go=true;

            }

            return [n,go];

        }

    };

    methods = {

        init:               function(){

            return this.each( function(){

                /* variables */

                var s, slider, api;

                /* fill them */

                slider      = $(this).css('position','relative');
                api         = new Object();

                api.options = $.extend( defaults, options );
                s           = api.options;

                typeof s.start == 'object' ? 1 : s.start=[s.start];

                /* Available elements */

                api.slider  = slider;
                api.low     = $('<div class="noUi-handle noUi-lowerHandle"><div></div></div>');
                api.up      = $('<div class="noUi-handle noUi-upperHandle"><div></div></div>');
                api.connect = $('<div class="noUi-midBar"></div>');

                /* Append the middle bar */

                s.connect ? api.connect.appendTo(api.slider) : api.connect = false;

                /* Append the handles */

                // legacy rename
                if(s.knobs){
                    s.handles=s.knobs;
                }

                if ( s.handles === 1 ){

                    /*
                        This always looks weird:
                        Connect=lower, means activate upper, because the bar connects to 0.
                    */

                    if ( s.connect === true || s.connect === 'lower' ){

                        api.low     = false;
                        api.up      = api.up.appendTo(api.slider);
                        api.handles = [api.up];

                    } else if ( s.connect === 'upper' || !s.connect ) {

                        api.low     = api.low.prependTo(api.slider);
                        api.up      = false;
                        api.handles = [api.low];

                    }

                } else {

                    api.low     = api.low.prependTo(api.slider);
                    api.up      = api.up.appendTo(api.slider);
                    api.handles = [api.low, api.up];

                }

                if(api.low){ api.low.left = helpers.left; }
                if(api.up){ api.up.left = helpers.left; }

                api.slider.children().css('position','absolute');

                $.each( api.handles, function( index ){

                    $(this).css({
                        'left' : helpers.scale(s.start[index],api.options.scale,api.slider.innerWidth()),
                        'zIndex' : index + 1
                    }).children().bind(touch?'touchstart.noUi':'mousedown.noUi',functions.start);

                });

                if(s.click){
                    api.slider.click(functions.click).find('*:not(.noUi-midBar)').click(functions.flse);
                }

                helpers.connect(api);

                /* expose */
                api.options=s;
                api.slider.data('api',api);

            });

        },
        move:               function(){

            var api, bounce, to, handle, scale;

            api = dup($(this).data('api'));
            api.options = $.extend( api.options, options );

            // rename legacy 'knob'
            if(api.options.knob){
                api.options.handle = api.options.knob;
            }

            // flatten out the legacy 'lower/upper' options
            handle  = api.options.handle;
            handle  = api.handles[handle == 'lower' || handle == 0 || typeof handle == 'undefined' ? 0 : 1];
            bounce  = helpers.bounce(api, helpers.scale(api.options.to, api.options.scale, api.slider.innerWidth()), handle.left(), handle);

            handle.css('left',bounce[0]);

            if( (handle.is(api.up) && handle.left() == 0) || (handle.is(api.low) && handle.left() == api.slider.innerWidth()) ){
                handle.css('zIndex',parseInt(handle.css('zIndex'))+2);
            }

            if(options.save===true){
                api.options.scale = options.scale;
                $(this).data('api',api);
            }

            helpers.connect(api);
            helpers.call(api.options.change, api.slider, 'move');
            helpers.call(api.options.end, api.slider, 'move');

        },
        value:              function(){

            var val1, val2, api;

            api = dup($(this).data('api'));
            api.options = $.extend( api.options, options );

            val1    = api.low ? Math.round(helpers.deScale(api.low.left(), api.options.scale, api.slider.innerWidth()))  : false;
            val2    = api.up ? Math.round(helpers.deScale(api.up.left(), api.options.scale, api.slider.innerWidth()))  : false;

            if(options.save){
                api.options.scale = options.scale;
                $(this).data('api',api);
            }

            return [val1,val2];

        },
        api:                function(){
            return $(this).data('api');
        },
        disable:            function(){
            return this.each( function(){
                $(this).addClass('disabled');
            });
        },
        enable:             function(){
            return this.each( function(){
                $(this).removeClass('disabled');
            });
        }

    },

    functions = {

        start:              function( e ){

            if(! $(this).parent().parent().hasClass('disabled') ){

                e.preventDefault();
                $('body').bind( 'selectstart.noUi' , functions.flse);
                $(this).addClass('noUi-activeHandle');

                $(document).bind(touch?'touchmove.noUi':'mousemove.noUi', functions.move);

                touch?$(this).bind('touchend.noUi',functions.end):$(document).bind('mouseup.noUi', functions.end);

            }

        },
        move:               function( e ){

            var a,b,h,api,go = false,handle,bounce;

            h       = $('.noUi-activeHandle');
            api     = h.parent().parent().data('api');
            handle  = h.parent().is(api.low) ? api.low : api.up;
            a       = e.pageX - Math.round( api.slider.offset().left );

            // if there is no pageX on the event, it is probably touch, so get it there.
            if(isNaN(a)){
                a = e.originalEvent.touches[0].pageX - Math.round( api.slider.offset().left );
            }

            // a = p.nw  == New position 
            // b = p.cur == Old position

            b       = handle.left();
            bounce  = helpers.bounce(api, a, b, handle);
            a       = bounce[0];
            go      = bounce[1];

            if ( api.options.step && !go){

                // get values from options
                var v1 = api.options.scale[0], v2 = api.options.scale[1];

                // convert values to [0-X>0] range
                // edge case: both values negative;
                if( neg(v2) ){ 
                    v2 = abs( v1 - v2 );
                    v1 = 0;
                }
                    // handle all values
                    v2 = ( v2 + ( -1 * v1 ) );

                // converts step to the new range
                var con = helpers.scale( api.options.step, [0,v2], api.slider.innerWidth() );

                // if the current movement is bigger than step, set to step.
                if ( Math.abs( b - a ) >= con ){
                    a = a < b ? b-con : b+con;
                    go = true;
                }

            } else {
                go = true;
            }

            if(a===b){
                go=false;
            }

            if(go){

                handle.css('left',a);
                if( (handle.is(api.up) && handle.left() == 0) || (handle.is(api.low) && handle.left() == api.slider.innerWidth()) ){
                    handle.css('zIndex',parseInt(handle.css('zIndex'))+2);
                }
                helpers.connect(api);
                helpers.call(api.options.change, api.slider, 'slide');

            }

        },
        end:                function(){

            var handle, api;

            handle  = $('.noUi-activeHandle');
            api     = handle.parent().parent().data('api');

            $(document).add('body').add(handle.removeClass('noUi-activeHandle').parent()).unbind('.noUi');

            helpers.call(api.options.end, api.slider, 'slide');

        },
        click:              function( e ){

            if(! $(this).hasClass('disabled') ){

                var api = $(this).data('api');
                var s   = api.options;
                var c   = e.pageX - api.slider.offset().left;

                c = s.step ? roundTo(c,helpers.scale( s.step, s.scale, api.slider.innerWidth() )) : c;

                if( api.low && api.up ){
                    c < ((api.low.left()+api.up.left())/2) ? api.low.css("left", c) : api.up.css("left", c);
                } else {
                    api.handles[0].css('left',c);
                }

                helpers.connect(api);
                helpers.call(s.change, api.slider, 'click');
                helpers.call(s.end, api.slider, 'click');

            }

        },
        flse:               function(){
            return false;
        }

    }

    if ( methods[method] ) {
        return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 ));
    } else if ( typeof method === 'object' || ! method ) {
        return methods.init.apply( this, arguments );
    } else {
        $.error( 'No such method: ' +  method );
    }

};

})( jQuery );

The likely culprit (going off that source code) is within the move: function( e ) :

a = e.pageX - Math.round( api.slider.offset().left );
// if there is no pageX on the event, it is probably touch, so get it there.
if(isNaN(a)){
  a = e.originalEvent.touches[0].pageX - Math.round( api.slider.offset().left );
}
// a = p.nw  == New position

Assuming the Math libraries involved are equally accurate, and api.slider.offset().left doesn't vary between platforms, I'd do some logging from the source code at that point to determine the purpose and values of e.pageX / e.originalEvent.touches[0].pageX .

Another possibility is that the slider's left offset, which is just pulled from the DOM ( $('.noUi-activeHandle').parent().parent().data('api').slider.offset().left ) somehow varies in accuracy between platforms. Log that too, I guess.

Neither suggest an exponential grown in inaccuracy, though, so there might be something else.

javascript has indeed rounding issues. All numbers are internally floating points and are thus due to floating point rounding problems.

Try using http://silentmatt.com/biginteger/ to see if it fixes your problem.

Please check http://blog.greweb.fr/2013/01/be-careful-with-js-numbers/ for some more detailed information.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM