Disable Predictive Scrolling - Mousewheel (OnScroll) Event fires too often (Touchpad) - javascript

I am executing Javascript onScroll.
My code works great with any normal computer mouse, but when I use my notebook's touchpad, I encounter the following situation:
my mouse fires (about 1 to 8) mousewheel events while the finger is moving the wheel.
my touchpad fires a lot more (~60) mousewheel events while the two fingers are touching the pad and continues to fire after my fingers are up in the air again.
I know this behavior from mobile touch devices. The Feature is called "Predictive Touch" - The Scrolling continues if your finger movement had enough acceleration before lifting it up.
I think the touchpad drivers are setting this "smooth scrolling" behavior.
To debug this case, I have used the following code:
/* Handle Mouse-Wheel Scrolling */
var lastChange = +new Date();
$(window).bind('mousewheel', function(e){
console.log("mw");
if(+new Date() - lastChange > 1000){
console.log("mw allowed");
if(e.originalEvent.wheelDelta > 0) {/*go to previous*/}
else{ /*go to next*/}
lastChange = +new Date();
}
return false;});
This is a simple code that "allows" a mouse-scrolling-event every second.
If I make a fast touchpad-scroll, the mousewheel event is fired ~300 times. The one-second-condition is letting 3 events happen. My fingers were on the touchpad for far less than a second.
With this test, I discovered that the mousewheel events are still fired (almost continuously for 3 seconds), even when my fingers are already off the touchpad.
Is there a Javascript function or a workaround / trick / hack to avoid this behavior?
Something like a onTouchEnd event for touchpads, maybe?

To achieve this, you'd have to distinguish between mouse scroll events and touchpad events, which is not (yet) possible using JavaScript. It was already asked in question How to capture touch pad input.
Pointer Events are currently in state of Editor's Draft and not yet supported by any browser. See also touch events docs on MDN.

EDIT: This doesn't appear to work for trackpads. Once they are widely supported, this could be implemented using Touch Events, specifically the Touch End event. By tracking when the finger leaves the trackpad, you can prevent the page scrolling at that particular point.
https://jsfiddle.net/gLkkb5z0/3/
(function(){
var special = jQuery.event.special,
uid1 = 'D' + (+new Date()),
uid2 = 'D' + (+new Date() + 1);
special.scrollstart = {
setup: function() {
var timer,
handler = function(evt) {
var _self = this,
_args = arguments;
if (timer) {
clearTimeout(timer);
} else {
evt.type = 'scrollstart';
jQuery.event.handle.apply(_self, _args);
}
timer = setTimeout( function(){
timer = null;
}, special.scrollstop.latency);
};
jQuery(this).bind('scroll', handler).data(uid1, handler);
},
teardown: function(){
jQuery(this).unbind( 'scroll', jQuery(this).data(uid1) );
}
};
special.scrollstop = {
latency: 300,
setup: function() {
var timer,
handler = function(evt) {
var _self = this,
_args = arguments;
if (timer) {
clearTimeout(timer);
}
timer = setTimeout( function(){
timer = null;
evt.type = 'scrollstop';
jQuery.event.handle.apply(_self, _args);
}, special.scrollstop.latency);
};
jQuery(this).bind('scroll', handler).data(uid2, handler);
},
teardown: function() {
jQuery(this).unbind( 'scroll', jQuery(this).data(uid2) );
}
};
})();
Demo
Taken from http://james.padolsey.com/javascript/special-scroll-events-for-jquery/

Related

"Wheel" event triggers more than once despite using throttle

i try to do something like this:
function throttle(fn, wait) {
var time = Date.now();
return function() {
if ((time + wait - Date.now()) < 0) {
fn();
time = Date.now();
}
}
}
function callback() {
//something
}
something.addEventListener("wheel", throttle(callback, 500));
When I use mousewheel it seems to work nice and triggers only once. The issue is when I use Macbook's touchpad this event triggers (depending on swipe's length) 1, 2 or 3 times at once. What's a problem?
Your code is fine, the problem is that when you "wheel" with a touchpad, you trigger the wheel event a lot, especially a lot of very small values.
For example, if you try to scroll this page with a touchpad, you will notice the smoothness of the scroll. That's because many events are fired with a degressive value.
A throttle is a good start but not enough. An upgrade would be to dismiss wheel events with a very small delta value, like this:
function throttle(fn, wait) {
var time = Date.now();
return function(event) {
// we dismiss every wheel event with deltaY less than 4
if (Math.abs(event.deltaY) < 4) return
if ((time + wait - Date.now()) < 0) {
fn(event);
time = Date.now();
}
}
}
function callback(event) {
// something
}
something.addEventListener("wheel", throttle(callback, 500));
It won't be "perfect" but close.
If you want a perfect result, some advanced maths is necessary. And when I mean advanced, I mean I would myself need one week or two full-time to implement it cleanly across all devices.
If you want to control the wheel to scroll from a screen A to a screen B, you should check out the css scroll snapping property.

Listening to the scroll direction only ONCE on devices with continuous scroll

We're trying to make a pseudo scrolling effect on the homepage of our website but we're running into an issue with the scrolling mechanics touchpads and other digital scrolling mice have (which have a continuing scrolling effect with an easing out timing function).
We want to be able to listen to the direction of the scroll (currently using .on('scroll mousewheel') ) to determine the direction of the scroll (therefore deciding if we should show the previous or next slide) but not listen to every scroll event as this would lead to a series of flashing, seizure-inducing flurry of changes (these being hidden/shown using javascript depending on the scroll value).
We currently have a setTimeout() function that waits 50ms per scroll event before executing the code that actually makes the changes, but this can lead to a longer wait than expected on the aforementioned devices due to their ability to continuously scroll on a swipe, thus continuously refreshing the 50ms wait. This also doesn't take into consideration the easing out scrolling that digital scroll wheels have that allow it to still fire more than once towards the end of the scroll.
So, in essence, we seem to be looking for one of the following:
Listening to the direction of the mousewheel direction without
firing the function every instance of that scroll.
Another workaround we haven't thought about yet.
Here's the current section of javascript related to this:
var timer;
$('html').on ('scroll mousewheel', function (e) {
if(timer) {
window.clearTimeout(timer);
}
timer = window.setTimeout(function() {
var delta = e.originalEvent.wheelDelta;
if((window.innerHeight + window.pageYOffset ) >= document.body.offsetHeight && delta < 0){
$(allSections[scrollNumber]).hide();
$(".cc-nav-circle").removeClass("active");
if(scrollNumber >= allSections.length - 1){
scrollNumber = 0;
} else {
scrollNumber++;
}
} else if($(window).scrollTop() === 0 && delta > 0){
$(allSections[scrollNumber]).hide();
$(".cc-nav-circle").removeClass("active");
if(scrollNumber <= 0){
scrollNumber = allSections.length - 1;
} else {
scrollNumber--;
}
}
$(allSections[scrollNumber]).show();
$(allCircles[scrollNumber]).addClass("active");
}, 50);
});
Here's the current in-progess version of this website: https://unink-marketing.squarespace.com/
Instead of delaying your scroll function until the user is done scrolling, you can scroll right away and just ignore further scroll events until the 50ms timer completes. See the updated code below
var timer;
var justScrolled = false;
$('html').on ('scroll mousewheel', function (e) {
if(timer) {
window.clearTimeout(timer);
}
timer = window.setTimeout(function() {
justScrolled = false;
}, 50);
if(justScrolled) {
return;
}
justScrolled = true;
// Do scroll stuff
});

Is there a way to prevent fastclick from firing “active” state on scroll?

I’m using FastClick on a page with large links because I want to bypass the 300ms delay for taps in mobile browsers. I have a “highlight” style for the links’ :active states and it is properly firing quickly, thanks to FastClick.
My problem is that – in Mobile Safari at least – it also fires while you’re tapping and swiping to scroll the page. This makes it feel like you can’t scroll the page without it thinking you’re trying to tap the links.
Is there a way to prevent it from firing when someone scrolls?
Maybe you can add the needsclick class to the body?
<body class="needsclick">
...
</body>
Just an idea :)
Nice question! +1
This problem has nothing to do with FastClick, but FastClick does make the solution to your problem more complex. So I will stick to pure JavaScript and raw Touch Events )
On a mobile touch device the implementation of the webview is significantly different from the desktop web browser for reasons specific to the platform.
One feature that is important in this case is momentum scrolling in the webview.
Momentum Scrolling is a hardware accelerated feature of the device. So when a scrollable area is touched on the screen, the hardware identifies the touch and attaches a 200 millisecond countdown timer to the scrollable area, that when triggered, puts the area into a hardware accelerated scroll. When the hardware is in the state it does not broadcast touch events because they are specific to the hardware accelerated scroll. The timer can be cancelled, and momentum scrolling prevented by using preventDefault on the touch event within the provided 200 milliseconds.
Here is how I approach this problem. I assume you are using native scroll i.e overflow:scroll.
The method is to attach a touchstart event to the element you want touchable.
Once the touchstart event has fired, the event handler attaches touchend, touchmove and touchcancel events to the target element. A setTimout timer is initiated that removes the newly added events after 140ms.
Here are some clips for my production code:
I have two event methods for adding and removing events from collections:
var eventify = (function () {
function eventify(type, el, callback, phase) {
phase = phase || false;
if ((el.constructor.toString().contains('NodeList')) || (el.constructor.toString().contains('HTMLCollection'))) {
[].forEach.call(el, function (element) {
if (!element.hasEvent(type)) {
element.addEvent(type);
HTMLElement.prototype.addEventListener.apply(element, [type, callback, phase]);
}
});
} else {
if (!el.hasEvent(type)) {
el.addEvent(type);
HTMLElement.prototype.addEventListener.apply(el, [type, callback, phase]);
}
}
return callback;
}
return eventify
})();
var uneventify = (function () {
function uneventify(type, el, callback) {
if ((el.constructor.toString().contains('NodeList')) || (el.constructor.toString().contains('HTMLCollection'))) {
[].forEach.call(el, function (element) {
if (element.hasEvent(type)) {
element.removeEvent(type);
HTMLElement.prototype.removeEventListener.apply(element, [type, callback]);
}
});
} else {
if (el.hasEvent(type)) {
el.removeEvent(type);
HTMLElement.prototype.removeEventListener.apply(el, [type, callback]);
}
}
}
return uneventify
})();
Then I have a tapify method for tap events on the scroller:
var tapify = (function () {
function tapify(el, callback) {
eventify('touchstart', el, function (e) {
var that = this;
var start = e.pageY;
var target = e.target;
function dynamicEvents() {
var endfn = eventify('touchend', target, function (evt) {
e.preventDefault();
e.stopImmediatePropagation();
evt.preventDefault();
evt.stopImmediatePropagation();
uneventify('touchmove', target, movefn);
uneventify('touchend', target, endfn);
uneventify('touchcancel', target, cancelfn);
callback && callback(target);
});
var cancelfn = eventify('touchcancel', target, function (evt) {
e.preventDefault();
e.stopImmediatePropagation();
evt.preventDefault();
evt.stopImmediatePropagation();
uneventify('touchmove', target, movefn);
uneventify('touchend', target, endfn);
uneventify('touchcancel', target, cancelfn);
callback && callback(target);
});
var movefn = eventify('touchmove', target, function (evt) {
var distance = start - evt.pageY;
if (distance > 20) {
uneventify('touchend', target, endfn);
uneventify('touchcancel', target, cancelfn);
uneventify('touchmove', el, movefn);
}
});
setTimeout(function () {
uneventify('touchmove', target, movefn);
uneventify('touchend', target, endfn);
uneventify('touchcancel', target, cancelfn);
}, 140);
}
if (global.isIos) setTimeout(function () {
dynamicEvents();
}, 60);
else dynamicEvents();
}, false);
}
return tapify;
})();
I use global.isIos to identify the target device. Android stops sending touch events to webview ater 200ms.
Then to attach the event to a element or a collection of elements, use :
tapify(document.querySelectorAll('button'), function (e) {
//your event handler here!!
});
Hope this helps

JavaScript touch event updates for force or radius

JavaScript touch events contain properties for radius and force. Unfortunately it appears that events aren't generated when either property changes. Events are only triggered for things like touch start, move or end. Can anyone think of a way to get more updates on change of touch size?
Currently to get radius updates I have to wiggle my finger to trigger the touch move event, but I would prefer a software solution.
I had the same issue and then discovered this blog post: http://blog.framerjs.com/posts/prototyping-3D-touch-interactions.html
In a nutshell, we need to use touchstart event to capture the touch event, assign the event to a variable and then use setInterval to get the force value:
var el = document.getElementById('myElement');
var currTouch = null;
var currTouchInterval = null;
attachListeners = function () {
el.addEventListener('touchstart', enterForceTouch, false);
el.addEventListener('touchend', exitForceTouch, false);
}
enterForceTouch = function (evt) {
evt.preventDefault();
currTouch = evt;
currTouchInterval = setInterval(updateForceTouch, 10); // 100 times per second.
}
updateForceTouch = function() {
if (currTouch) {
console.log(currTouch.touches[0].force); // Log our current force value.
}
}
exitForceTouch = function (evt) {
evt.preventDefault();
currTouch = null;
clearInterval(currTouchInterval);
}
attachListeners();

Timed long press in Javascript within jQuery phonegap app

There is a good example for doing long press in Javascript here: Long Press in JavaScript?
But it does not provide for knowing the duration of the press.
If I want to do different things based on the length of the press I cant use the pattern in that post.
I was trying to do something similar by saving current time in a variable on('mousedown')
and then calculating the time difference on('mouseup').
this works fine within a normal Javasript page in a "normal" browser.
However within my phonegap app something happens,
looks like the mouseup event is not being called if the finger is kept on the screen for a long duration (say 5 sec..).
Is this some native mobile browser behavior? Can I override it somehow?
I am using plain jQuery not jQuery mobile.
Any ideas anyone?
You could have a look at how the taphold and vmouseup (handleTouchEnd() line 752) events are implemented in jQuery mobile source code.
Since it is already tested and implemented I'd suggest to use jquery mobile instead of jquery and modify (since it already handles all the 'quirks' related each mobile browser), and change the code as you need.
You can check the time to identify Click or Long Press [jQuery]
function AddButtonEventListener() {
try {
var mousedowntime;
var presstime;
$("button[id$='" + buttonID + "']").mousedown(function() {
var d = new Date();
mousedowntime = d.getTime();
});
$("button[id$='" + buttonID + "']").mouseup(function() {
var d = new Date();
presstime = d.getTime() - mousedowntime;
if (presstime > 999/*You can decide the time*/) {
//Do_Action_Long_Press_Event();
}
else {
//Do_Action_Click_Event();
}
});
}
catch (err) {
alert(err.message);
}
}
Note that this solution is usefull if you do not use jQuery Mobile for some reason.
I used the article Fast Touch Event Handling and just added a piece of code
$.event.special.tap = {
distanceThreshold: 10,
timeThreshold: 350,
setup: function () {
var self = this,
$self = $(self);
// Bind touch start
$self.on('touchstart', function (startEvent) {
// Save the target element of the start event
var target = startEvent.target,
touchStart = startEvent.originalEvent.touches[0],
startX = touchStart.pageX,
startY = touchStart.pageY,
threshold = $.event.special.tap.distanceThreshold,
timeout,
expired = false;
function timerFired() {
expired = true;
}
function removeTapHandler() {
clearTimeout(timeout);
$self.off('touchmove', moveHandler).off('touchend', tapHandler).off('touchcancel', removeTapHandler);
};
function tapHandler(endEvent) {
removeTapHandler();
if (target == endEvent.target) {
if (expired) {
$.event.simulate('longtap', self, endEvent);
} else {
$.event.simulate('tap', self, endEvent);
}
}
};
// Remove tap and move handlers if the touch moves too far
function moveHandler(moveEvent) {
var touchMove = moveEvent.originalEvent.touches[0],
moveX = touchMove.pageX,
moveY = touchMove.pageY;
if (Math.abs(moveX - startX) > threshold || Math.abs(moveY - startY) > threshold) {
removeTapHandler();
}
};
// Remove the tap and move handlers if the timeout expires
timeout = setTimeout(timerFired, $.event.special.tap.timeThreshold);
// When a touch starts, bind a touch end and touch move handler
$self.on('touchmove', moveHandler).on('touchend', tapHandler).on('touchcancel', removeTapHandler);
});
}
};
So, now I have a tap and a longtap events

Categories