我正在尝试调试移动浏览器上的一些 javascript 滑块代码的问题。它似乎是仅在移动设备上发生的舍入误差。以下代码在桌面(例如 Chrome)上可以正常工作,但在智能手机(例如 iPhone iOS 5/6、Samsung S2 ICS)上的 Webkit 中查看时,增加按钮无法在滑块上的较高值上工作。
试试这个http://jsfiddle.net/codecowboy/mLpfu/。单击“在移动设备上调试”按钮 - 它与左上方的“运行”按钮直接相邻(您需要登录才能看到此按钮)。将生成的 url 输入智能手机上的浏览器(最好是 iPhone / Android 上的 webkit 浏览器)。
拖动滑块说 265,点击增加。有些值会让你命中增加,有些则不会。值越高,问题越严重。
该代码使用 jQuery 和 noUISlider 插件。按钮点击代码为:
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]);
});
谁能解释这是什么原因造成的?这可能是一个大/小端问题吗?还是 jQuery 中的错误?
上面的代码调用了 nouislider 插件,源码在这里:
(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 );