Is there a way to disable or detect that wheel events are from the "inertia" setting on a Mac?
I'd like to be able to tell the difference between real events and the others...or disable that kind of scrolling for a particular page.
I found a solution that works really well for this. Below is some pasted code from my project. It basically comes down to this logic:
A scroll event is from a human when ANY ONE of these conditions are true:
The direction is the other way around than the last one
More than 50 milliseconds passed since the last scroll event (picked 100ms to be sure)
The delta value is at least as high as the previous one
Since Mac spams scroll events with descreasing delta's to the browser every 20ms when inertial scrolling is enabled, this is a pretty failsafe way. I've never had it fail on me at least. Just checking the time since the last scroll won't work because a user won't be able to scroll again if the "virtual freewheel" is still running even though they haven't scrolled for 3 seconds.
this.minScrollWheelInterval = 100; // minimum milliseconds between scrolls
this.animSpeed = 300;
this.lastScrollWheelTimestamp = 0;
this.lastScrollWheelDelta = 0;
this.animating = false;
document.addEventListener('wheel',
(e) => {
const now = Date.now();
const rapidSuccession = now - this.lastScrollWheelTimestamp < this.minScrollWheelInterval;
const otherDirection = (this.lastScrollWheelDelta > 0) !== (e.deltaY > 0);
const speedDecrease = Math.abs(e.deltaY) < Math.abs(this.lastScrollWheelDelta);
const isHuman = otherDirection || !rapidSuccession || !speedDecrease;
if (isHuman && !this.animating) {
this.animating = true; // current animation starting: future animations blocked
$('.something').stop().animate( // perform some animation like moving to the next/previous page
{property: value},
this.animSpeed,
() => {this.animating = false} // animation finished: ready for next animation
)
}
this.lastScrollWheelTimestamp = now;
this.lastScrollWheelDelta = e.deltaY;
},
{passive: true}
);
There's one caveat by the way: Mac also has acceleration on the scrolling, i.e.: at first, the delta value is higher for each successive event. It seems like this does not last more than 100ms or so though. So if whatever action/animation you are firing as a result of the scroll event lasts at least 100ms and blocks all other actions/animations in the meantime, this is never a problem.
Yes and no.
You can use touchdown/up, and scroll as events to look for the page moving about but those won't trigger if the OS is doing an inertial scroll. Fun, right?
One thing that you can continually detect, however, is window.pageYOffset. That value will keep changing while an inertial scroll is happening but won't throw an event. So you can come up with a set of timers to keep checking for an inertial scroll and keep running itself until the page has stopped moving.
Tricky stuff, but it should work.
Oh how is this issue killing me :/
I'm in the process of creating "endless" scrolling large file viewer.
To make situation worse, this editor is embedded in page that has its own scroll bar, because its bigger than one screen.
U use overflow-x scroll for horizontal scroll, but for vertical scroll i need current line highlighter (as seen in most modern IDEs) so i'm using jquery mousewheel plugin, and scrolling moving content for line height up or down.
It works perfectly on ubuntu/chrome but on MacOS Lion/Chrome sometimes, and just sometimes,
when you scroll, it doesn't prevent default scroll on the editor element, and event propagates "up" and page it self starts to scroll.
I cant even describe how much annoying that is.
As for inertial scroll it self, i successfully reduced it with two timers
var self = this;
// mouse wheel events
$('#editorWrapper').mousewheel(function (event, delta, deltax, deltay) {
self._thisScroll = new Date().getTime();
//
//this is entirely because of Inertial scrolling feature on mac os :(
//
if((self._thisScroll - self._lastScroll) > 5){
//
//
// NOW i do actual moving of content
//
//
self._lastScroll = new Date().getTime();
}
5ms is value i found to have most natural feel on my MacBook Pro, and you have to scroll mouse wheel really fast to catch one of those..
Even still, sometimes on Mac listener on mac wrapper doesn't prevent default, and page scrolls down.
Well, (I might be wrong), I think that the "inertia" settings on the Mac are all computed by the system itself, the browser, or any program for that matter would just think that the user is scrolling quickly, rather than slowing down.
I'm not sure about other browsers, but the following event fires during inertial scroll on my Chrome, FF, Safari (mac):
var mousewheelevt=(/Firefox/i.test(navigator.userAgent))? "DOMMouseScroll" : "mousewheel";
function scrollEE (e) {
console.log(e);
}
window.addEventListener(mousewheelevt, scrollEE, false);
I had a big problem with an object animating based on scroll position after the scroll had completed, and the inertial scroll was really messing me around. I ended up calculating the velocity to determine how long the inertial scroll would last and used that to wait before animating.
var currentY = 0;
var previousY = 0;
var lastKnownVelocity = 0;
var velocityRating = 1;
function calculateVelocity() {
previousY = currentY;
currentY = $('#content').scrollTop();
lastKnownVelocity = previousY - currentY;
if (lastKnownVelocity > 20 || lastKnownVelocity < -20) {
velocityRating = 5;
} else {
velocityRating = 1;
}
}
$('#content').scroll(function () {
// get velocity while scrolling...
calculateVelocity();
// wait until finished scrolling...
clearTimeout($.data(this, 'scrollTimer'));
$.data(this, 'scrollTimer', setTimeout(function() {
// do your animation
$('#content').animate({scrollTop: snapPoint}, 300);
}, 300*velocityRating)); // short or long duration...
});
There's a library that solves this problem.
https://github.com/d4nyll/lethargy
After installing it, use it like this:
var lethargy = new Lethargy();
$(window).bind('mousewheel DOMMouseScroll wheel MozMousePixelScroll', function(e){
if(lethargy.check(e) === false) {
console.log('stopping zoom event from inertia')
e.preventDefault()
e.stopPropagation();
}
console.log('Continue with zoom event from intent')
});
Following your instructions with my type of code, I had to set timeout of 270ms on each action activated by scroll to get it all smooth, so if anyone is using something similar to me here is my example if not ignore it, hope it will help you.
//Event action
function scrollOnClick(height) {
$('html').animate({
scrollTop: height
}, 'fast');
return false;
};
// Scroll on PC
let timer = false;
$(window).on('mousewheel', function (event) {
if(timer != true) {
var heightWindow = $(window).height();
var heightCurrent = $(window).scrollTop();
if (event.originalEvent.wheelDelta >= 1) {
if (heightWindow >= heightCurrent) {
timer= true;
scrollOnClick(0)
setTimeout(function (){
timer = false;
},270);
}
} else {
if (heightCurrent < heightWindow) {
timer= true;
scrollOnClick(heightWindow)
setTimeout(function (){
timer = false;
},270);
}
}
}
});
Related
I'm using the following event listener to detect mouse wheel and scroll direction:
window.addEventListener('wheel', ({ deltaY }) => {
console.log(deltaY);
if (deltaY > 0) scrollDown();
else if (deltaY < 0) scrollUp();
});
The following happens here:
2 finger touch pad scroll on Macbook triggers the event handler
deltaY keeps logging due to the scroll accelerometer
scrollDown() or scrollUp() keep firing until accelerometer stops
I only want to fire scrollUp and scrollDown once per user interaction. I therefore need to detect a new mouse scroll event, not every mouse scroll events. Is this possible?
I did try a timeout to detect if deltaY was still changing due to the accelerometer, but this wasn't sufficient because if it was still changing, a second user interaction did not trigger scrollUp or scrollDown.
Here's a CodePen of what I'm trying to achieve: https://codepen.io/anon/pen/dQmPNN
It's very close to the required functionality, but if you hammer the mouse wheel hard on the first slide, then try to scroll to the next one immediately, the timeout solution locks it so you have to wait another second or so until the timeout completes and you can continue scrolling.
This is old but I found it when looking for an answer to pretty much the same problem.
I solved the problem for my purposes, so here's my solution in case it helps anyone else.
The problem is really to define what counts as one continuous action. Without something more concrete to work with, it's just a question of timing. It's the time between events that's the key - so the algorithm is to keep accumulating events until there's a certain gap between them. All that then remains is to figure out how big the allowed gap should be, which is solution specific. That's then the maximum delay after the user stops scrolling until they get feedback. My optimum is a quarter of a second, I'm using that as a default in the below.
Below is my JavaScript, I'm attaching the event to a div with the id 'wheelTestDiv' using jQuery but it works the same with the window object, as in the question.
It's worth noting that the below looks for any onWheel event but only tracks the Y axis. If you need more axes, or specifically only want to count events towards the timer when there's a change in deltaY, you'll need to change the code appropriately.
Also worth noting, if you don't need the flexibility of tracking events against different DOM objects, you could refactor the class to have static methods and properties, so there would be no need to create a global object variable. If you do need to track against different DOM objects (I do), then you may need multiple instances of the class.
"use strict";
class MouseWheelAggregater {
// Pass in the callback function and optionally, the maximum allowed pause
constructor(func, maxPause) {
this.maxAllowedPause = (maxPause) ? maxPause : 250; // millis
this.last = Date.now();
this.cummulativeDeltaY = 0;
this.timer;
this.eventFunction = func;
}
set maxPause(pauseTime) {
this.maxAllowedPause = pauseTime;
}
eventIn(e) {
var elapsed = Date.now() - this.last;
this.last = Date.now();
if ((this.cummulativeDeltaY === 0) || (elapsed < this.maxAllowedPause)) {
// Either a new action, or continuing a previous action with little
// time since the last movement
this.cummulativeDeltaY += e.originalEvent.deltaY;
if (this.timer !== undefined) clearTimeout(this.timer);
this.timer = setTimeout(this.fireAggregateEvent.bind(this),
this.maxAllowedPause);
} else {
// just in case some long-running process makes things happen out of
// order
this.fireAggregateEvent();
}
}
fireAggregateEvent() {
// Clean up and pass the delta to the callback
if (this.timer !== undefined) clearTimeout(this.timer);
var newDeltaY = this.cummulativeDeltaY;
this.cummulativeDeltaY = 0;
this.timer = undefined;
// Use a local variable during the call, so that class properties can
// be reset before the call. In case there's an error.
this.eventFunction(newDeltaY);
}
}
// Create a new MouseWheelAggregater object and pass in the callback function,
// to call each time a continuous action is complete.
// In this case, just log the net movement to the console.
var mwa = new MouseWheelAggregater((deltaY) => {
console.log(deltaY);
});
// Each time a mouse wheel event is fired, pass it into the class.
$(function () {
$("#wheelTestDiv").on('wheel', (e) => mwa.eventIn(e));
});
Web page ...
<!DOCTYPE html>
<html>
<head>
<title>Mouse over test</title>
<script src="/mouseWheelEventManager.js"></script>
</head>
<body>
<div id="wheelTestDiv" style="margin: 50px;">Wheel over here</div>
</body>
</html>
Have you tried breaking this out into functions with a flag to check if an interaction has occurred?
For example:
// Create a global variable which will keep track of userInteraction
let shouldScroll = true;
// add the event listener, and call the function when triggered
window.addEventListener('wheel', () => myFunction());
//Create a trigger function, checking if shouldScroll is true or false.
myFunction(){
shouldScroll ? (
if (deltaY > 0) scrollDown();
else if (deltaY < 0) scrollUp();
// Change back to false to prevent further scrolling.
shouldScroll = false;
) : return;
}
/* call this function when user interaction occurs
and you want to allow scrolling function again.. */
userInteraction(){
// set to true to allow scrolling
shouldScroll = true;
}
We can avoid such a situation by delay execution and removing the events in between the delay, refer below example and have added 1000ms as delay which can be modified based on your requirements.
let scrollPage = (deltaY)=>{
console.log(deltaY);
if (deltaY > 0) scrollDown();
else if (deltaY < 0) scrollUp();
};
var delayReg;
window.addEventListener('wheel', ({ deltaY }) => {
clearTimeout(delayReg);
delayReg = setTimeout(scrollPage.bind(deltaY),1000);
});
I've got a series of rectangles drawn on a canvas and use a scroll event listener to move the boxes up and down.
I'm trying to add in some validation so that the boxes cannot be scrolled past a certain point.
Due to the acceleration, the scroll values don't always increment by 1, so when scrolling quickly, sometimes my validation kicks in too early.
Any ideas how to solve this?
So in my event listener I have:
lScroll += e.deltaY;
if (lScroll > 0) {
canScroll = false;
lScroll = 0;
} else {
canScroll = true;
}
https://jsfiddle.net/kr85k3us/3/
Please check if this is working for you: https://jsfiddle.net/kr85k3us/4/
I tested it and it should work, but perhaps you can move your mousewheel faster :)
if (!canScroll && lScroll == 0) {
var first = Object.keys(boxes)[0];
if (boxes[first]['y'] < 10) {
var delta = 10 - boxes[first]['y'];
Object.keys(boxes).forEach(function(k){ boxes[k]['y'] += delta; });
}
}
This is the piece of code I added. If you can't scroll and lScroll is 0, it means that we reached the top. Next, I check if the first box is where it should be. If not (i.e. boxes[first]['y'] < 10) then it adjusts all the boxes' y position.
I'm trying to make a long page of images scroll up and down infinitely (for an exhibition).
This is what I've been working with (code I found here, so helpful!):
https://jsfiddle.net/p7r73tke/
It's mostly working ok for what I want, but I need more control over speed and pause.
How can I make the pause at the top longer?
is there a way to make it pause randomly for ~1 second ?
does anyone know of an easier way to do what I'm thinking of? maybe as samuel-liew suggests, javascript is not the best solution for the problem
thank u thank u!
function scrollpage() {
function f() {
window.scrollTo(0, i); //idk
if (status == 0) {
i = i + 50; //scroll speed top to bottom?
if (i >= Height) {
status = 30; //idk?
}
} else {
i = i - 10; //scroll speed bottom to top?
if (i <= 1) { // if you don't want continue scroll then remove this if condition
status = 0; //idk
}
}
setTimeout(f, 0.01); //idk
}
f();
}
var Height = 15000; //doc height input manually
var i = 1, //idk
j = Height,
status = 0; //idk
scrollpage();
(I'm new and tender to JavaScript, as you can see in the comments)
Thanks for any help!
jQuery solution:
var speed = 10000; // 10000 = 10 seconds
var doScroll = function() {
var direction = $(window).scrollTop() != 0 ? 0 : $(document).height() - $(window).height();
$('html, body').animate({ scrollTop: direction }, speed, 'linear');
}
doScroll(); // once on page load
setInterval(doScroll, speed + 10); // once every X ms
DEMO: http://jsfiddle.net/samliew/k35tbgau/
JavaScript:
Sorry, I do not recommend pure JavaScript for this as you have to take into account:
Cross-browser issues with getting window height, document height, and current scroll position
Recalculating the scroll speed based on content height every time the browser is resized
Programming an animation function
Keeping track of intervals and timeouts, and when you need to clear them
Direction/state of scroll
Taking into consideration if user manually scrolls the scrollbar
Probably lots more...
I am trying to detect a scroll on my page using JavaScript. So that I can change classes and attributes of some elements when user has scrolled certain amount of page. This is my JS function:
function detectScroll() {
var header = document.querySelector(".headerOrig"),
header_height = getComputedStyle(header).height.split('px')[0],
fix_class = "changeColor";
if( window.pageYOffset > header_height ) {
header.classList.add(fix_class);
}
if( window.pageYOffset < header_height ) {
header.classList.remove(fix_class);
}
var change = window.setInterval(detectScroll, 5000);
}
and I am calling it when the page is loaded:
<body onload="detectScroll();">
However, I have this problem - I need to set up a really small interval so that the function gets called and the class is changed immediately. BUT then the page freezes and everything except the JS function works very slowly.
Is there any better way of achieving this in JavaScript?
Thanks for any advice/suggestion.
You are going to want to change a couple things. First, we can use onscroll instead of an interval. But you are also going to want to cache as much as possible to reduce the amount of calculations on your scroll. Even further, you should use requestAnimationFrame (or simply "debounce" in general for older browsers -- see the link). This ensures your work only happens when the browser is planning on repainting. For instance, while the user scrolls the actual scroll event may fire dozens of times but the page only repaints once. You only care about that single repaint and if we can avoid doing work for the other X times it will be all the more smoother:
// Get our header and its height and store them once
// (This assumes height is not changing with the class change).
var header = document.querySelector(".headerOrig");
var header_height = getComputedStyle(header).height.split('px')[0];
var fix_class = "changeColor";
// This is a simple boolean we will use to determine if we are
// waiting to check or not (in between animation frames).
var waitingtoCheck = false;
function checkHeaderHeight() {
if (window.pageYOffset > header_height) {
header.classList.add(fix_class);
}
if (window.pageYOffset < header_height) {
header.classList.remove(fix_class);
}
// Set waitingtoCheck to false so we will request again
// on the next scroll event.
waitingtoCheck = false;
}
function onWindowScroll() {
// If we aren't currently waiting to check on the next
// animation frame, then let's request it.
if (waitingtoCheck === false) {
waitingtoCheck = true;
window.requestAnimationFrame(checkHeaderHeight);
}
}
// Add the window scroll listener
window.addEventListener("scroll", onWindowScroll);
use onscroll instead of onload so you don't need to call the function with an interval.
Your dedectScroll function will be triggered automatically when any scroll appers if you use onscroll
<body onscroll="detectScroll();">
Your function is adding an interval recursively, you should add an event listener to the scroll event this way :
function detectScroll() {
var header = document.querySelector(".headerOrig"),
header_height = getComputedStyle(header).height.split('px')[0],
fix_class = "changeColor";
if( window.pageYOffset > header_height ) {
header.classList.add(fix_class);
}
if( window.pageYOffset < header_height ) {
header.classList.remove(fix_class);
}
}
window.addEventListener("scroll",detectScroll);
I have created a newsfeed. The feed switches every 2 seconds. You can also manually switch left/right, or click the panel from the squares at the bottom. The switching between slides is down using jQuery UI Slide.
Right now, if you are in the middle of a slide, and you click left/right/squares, then another slide occurs on top of the existing, still going slide and the whole system is messed up.
How can I prevent other actions occurring if a slide/switch is already in progress?
This is my code:
$(document).ready(function(){
newsfeedTimer = setInterval(newsfeed, displayDuration);
// Manual change of feed (LEFT)
$('#newsfeeds_wrapper > .left').click(function(event){
event.stopPropagation();
feedLeft();
clearInterval(newsfeedTimer);
newsfeedTimer = setInterval(newsfeed, displayDuration);
});
// Very similar code for feed right
// Ignore the other method of switching (if it works for above, I can implement it for this one)
});
function newsfeed() {
feedRight();
}
// Feed to the Right
// jump is used to jump multiple newsfeed instead of one at a time
function feedRight(jump)
{
jump = typeof jump !== 'undefined' ? jump : 1;
var current = $('.newsfeed:first');
var next = $('.newsfeed:nth(' + jump + ')');
current.hide('slide',{duration: transitionDuration}, function(){
// Append as many needed
for( var i = 0; i < jump; i++ ) {
$('.newsfeed:first').appendTo('#newsfeeds');
}
next.show('slide',{direction : 'right' , duration: transitionDuration});
}
I don't want to stop() an animation! I want to disable changing the slides IF there is animation happening!!
without seeing the full breadth of the code, I am shooting myself in the foot here. But here is a direction I would take it. You could also have two functions, one to bind, another to unbind. When animation is initiated, you unbind the left/right controls. When stopped, you bind. Or, set a global variable... ala.
var config = {'inProgress': false};
$('#newsfeeds_wrapper > .left').click(function(event){
if(!config.inProgress){
event.stopPropagation();
feedLeft();
clearInterval(newsfeedTimer);
newsfeedTimer = setInterval(newsfeed, displayDuration);
}
});
in your animation function. Seems like when you cut/paste, some of the code is lost, so lets just assume some animation.
when you enter your animation functions, set config.inProgress = true;
function feedRight(jump)
{
config.inProgress = true;
// removed your code, but just using for simplicity sake
// added a callback
next.show('slide',{direction : 'right' , duration: transitionDuration},
function() {
// Animation complete. Set inProgress to false
config.inProgress = false;
});
)
}