Detecting by How Much User Has Scrolled

Detecting by how much user has scrolled

Pure JavaScript uses scrollTop and scrollLeft:

var scrollLeft = (window.pageXOffset !== undefined) ? window.pageXOffset : (document.documentElement || document.body.parentNode || document.body).scrollLeft;
var scrollTop = (window.pageYOffset !== undefined) ? window.pageYOffset : (document.documentElement || document.body.parentNode || document.body).scrollTop;

https://developer.mozilla.org/en-US/docs/Web/API/Element.scrollTop

jQuery version:

var scrollLeft = $(window).scrollLeft() ;
var scrollTop = $(window).scrollTop() ;

What you need is this:

document.getElementById('enlargedImgWrapper').style.top = (scrollTop+30) + 'px';

Javascript: is it possible to determine how much user scrolls after reaching the end of a page?

If You need to keep track of the user activity after the bottom (or the top) of the page has been reached, beside the scroll event, You need to track the the wheel event. Moreover, on mobile, You need to track also touchstart and touchmove events.

Not all these events are normalized across browsers, so I did my own normalization function, which is more or less something like this:
var compulsivity = Math.log2(Math.max(scrollAmount, 0.01) * wheelAmount);

Below is a complete playground. You can test it in Chrome using the Mobile View of the Developer Tools, or in other browsers using the TouchEmulator.

function Tracker(page) {
this.page = page;
this.moveUp = 0;
this.moveDown = 0;
this.startTouches = {};
this.moveTouches = {};
this.lastScrollY = 0;
this.monitor = {};
this.startThreshold = 160;
this.moveThreshold = 10;
this.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
this.pullToRefresh = window.chrome || navigator.userAgent.match('CriOS');
this.amplitude = 16 / Math.log(2);
this.page.ownerDocument.addEventListener( 'onwheel' in document ? 'wheel' : 'onmousewheel' in document ? 'mousewheel' : 'DOMMouseScroll', this, { passive: true } );
/* The basic scroll event cannot be canceled, so it does not need to be set passive.*/
this.page.ownerDocument.addEventListener('scroll', this);
this.page.addEventListener('touchstart', this, { passive: true });
/* Maybe we need to cancel pullToRefresh */
this.page.addEventListener('touchmove', this, { passive: false });
return this;
}

Tracker.prototype.handleEvent = function (e) { /* handleEvent is built-in */
var winHeight = (this.iOS ? document.documentElement.clientHeight : window.innerHeight) | 0,
currScrollY = window.pageYOffset | 0,
amountScrollY = (this.lastScrollY - currScrollY) | 0,
elHeight = this.page.offsetHeight | 0,
elTop = -currScrollY, elBottom = winHeight - elHeight + currScrollY,
isTop = elTop >= 0, isBottom = elBottom >= 0;

switch (e.type) {
case 'wheel':
case 'onmousewheel':
case 'mousewheel':
case 'DOMMouseScroll':
var wheelDelta = e.wheelDelta ? e.wheelDelta : e.deltaY ? -e.deltaY : -e.detail,
wheelDir = (wheelDelta > 0) - (wheelDelta < 0),
wheelUp = wheelDir < 0, wheelDown = wheelDir > 0,
wheelAmount = 100 * wheelDir;

if (isTop && wheelDown) {
this.moveUp++;
this.moveDown = 0;
} else if (isBottom && wheelUp) {
this.moveUp = 0;
this.moveDown++;
} else {
this.moveUp = 0;
this.moveDown = 0;
}

var compulsivity = this.amplitude * Math.log(Math.max(this.moveUp, this.moveDown, 0.01) * wheelAmount* wheelDir);
this.monitor[e.type].track(wheelAmount, compulsivity);
break;
case 'scroll':
/* end of scroll event for iOS, start/end of scroll event for other browsers */
this.lastScrollY = currScrollY;
this.monitor[e.type].track(amountScrollY, 0);
break;
case 'touchstart':
var touches = [].slice.call(e.touches), i = touches.length;
while (i--) {
var touch = touches[i], id = touch.identifier;
this.startTouches[id] = touch;
this.moveTouches[id] = touch;
}
break;
case 'touchmove':
var touches = [].slice.call(e.touches), i = touches.length,
currTouches = {},
swipeUp = false, swipeDown = false,
currMoveY = 0, totalMoveY = 0;
while (i--) {
var touch = touches[i], id = touch.identifier;
currTouches[id] = touch;
if (id in this.moveTouches) {
currMoveY = this.moveTouches[id].screenY - touch.screenY;
}
if (id in this.startTouches) {
totalMoveY = this.startTouches[id].screenY - touch.screenY;
}
swipeUp = currMoveY > 0 || totalMoveY > 0;
swipeDown = currMoveY < 0 || totalMoveY < 0;
if (this.pullToRefresh && isTop && swipeDown && e.cancelable) {
e.preventDefault();
console.log('Reload prevented');
}
}
this.moveTouches = currTouches;
var moveDir = (totalMoveY > 0) - (totalMoveY < 0),
longSwipe = moveDir * totalMoveY > this.startThreshold,
shortSwipe = moveDir * totalMoveY > this.moveThreshold,
realSwipe = longSwipe || shortSwipe;

if (isTop && swipeDown) {
if (realSwipe) this.moveUp++;
this.moveDown = 0;
} else if (isBottom && swipeUp) {
this.moveUp = 0;
if (realSwipe) this.moveDown++;
} else {
this.moveUp = 0;
this.moveDown = 0;
}

var compulsivity = this.amplitude * Math.log(Math.max(this.moveUp, this.moveDown, 0.01) * moveDir * totalMoveY);
this.monitor[e.type].track(currMoveY, compulsivity);
break;
}
};

function Monitor(events) {
this.ctx = null;
this.cont = null;
this.events = events;
this.values = [];
this.average = 0;
this.lastDrawTime = 0;
this.inertiaDuration = 200;
return this;
}

Monitor.prototype.showOn = function (container) {
var cv = document.createElement('canvas');
this.ctx = cv.getContext('2d');
this.cont = document.getElementById(container);
cv.width = this.cont.offsetWidth;
cv.height = this.cont.offsetHeight;
cv.style.top = 0;
cv.style.left = 0;
cv.style.zIndex = -1;
cv.style.position = 'absolute';
cv.style.backgroundColor = '#000';
this.cont.appendChild(cv);
var self = this;
window.addEventListener('resize', function () {
var cv = self.ctx.canvas, cont = self.cont;
cv.width = cont.offsetWidth;
cv.height = cont.offsetHeight;
});
return this;
};

Monitor.prototype.track = function (value, average) {
this.average = average;
if (this.values.push(value) > this.ctx.canvas.width) this.values.shift();
if (value) this.lastDrawTime = new Date().getTime();
};

Monitor.prototype.draw = function () {
if (this.ctx) {
var cv = this.ctx.canvas, w = cv.width, h = cv.height;
var i = this.values.length, x = w | 0, y = (0.5 * h) | 0;
cv.style.backgroundColor = 'rgb(' + this.average + ', 0, 0)';
this.ctx.clearRect(0, 0, w, h);
this.ctx.strokeStyle = '#00ffff';
this.ctx.lineWidth = 1;
this.ctx.beginPath();
while (i--) {
x -= 4;
if (x < 0) break;
this.ctx.moveTo(x, y);
this.ctx.lineTo(x + 1, y);
this.ctx.lineTo(x + 1, y - this.values[i]);
}
this.ctx.stroke();
var elapsed = new Date().getTime() - this.lastDrawTime;
/* cool down */
this.average = this.average > 0 ? (this.average * 0.9) | 0 : 0;
if (elapsed > this.inertiaDuration) {
this.track(0, this.average);
}
}
var self = this;
setTimeout(function () {
self.draw();
}, 100);
};

Monitor.prototype.connectTo = function (tracker) {
var events = this.events.split(' '), i = events.length;
while (i--) {
tracker.monitor[events[i]] = this;
}
this.draw();
return this;
};

function loadSomeData(target) {
$.ajax({
url: 'https://jsonplaceholder.typicode.com/users',
method: 'GET',
crossDomain: true,
dataType: 'json',
success: function (users) {
var html = '', $ul = $(target).find('ul');
$.each(users, function (i, user) {
var item = '<li><a class="ui-alt-icon ui-nodisc-icon">';
item += '<h2>' + user.name + '</h2>';
item += '<p><strong>' + user.company.name + '</strong></p>';
item += '<p>' + user.address.zipcode + ', ' + user.address.city + '</p>';
item += '<p>' + user.phone + '</p>';
item += '<p>' + user.email + '</p>';
item += '<p class="ui-body-inherit ui-li-aside ui-li-count"><strong>' + user.id + '</strong></p>';
item += '</a></li>';
html += item;
});
$ul.append(html).listview('refresh');
},
});
}

$(document)
.on('pagecreate', '#page-list', function (e) {
$("[data-role='header'], [data-role='footer']").toolbar({ theme: 'a', position: 'fixed', tapToggle: false });
loadSomeData(e.target);
})
.on('pageshow', '#page-list', function (e, ui) {
var tracker = $.data(this, 'mobile-page', new Tracker(this));
new Monitor('touchstart touchmove').connectTo(tracker).showOn('header');
new Monitor('scroll wheel mousewheel DOMMouseScroll').connectTo(tracker).showOn('footer');
});
.ui-page {
touch-action: none;
}
h1, h2, h3, h4, h5, h6, p {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* JQM no frills */
.ui-btn,
.ui-title,
.ui-btn:hover,
.ui-btn:focus,
.ui-btn:active,
.ui-btn:visited {
text-shadow: none !important;
}
* {
-webkit-box-shadow: none !important;
-moz-box-shadow: none !important;
box-shadow: none !important;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Compulsivity</title>
<meta name="description" content="Compulsivity" />
<meta name="HandheldFriendly" content="True" />
<meta name="MobileOptimized" content="320" />
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, width=device-width, minimal-ui shrink-to-fit=no" />
<meta http-equiv="cleartype" content="on" />
<!-- Add to homescreen for Chrome on Android -->
<meta name="mobile-web-app-capable" content="yes" />
<!-- For iOS web apps. Delete if not needed. -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="Compulsivity" />
<link rel="stylesheet" href="https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.css" />
<!--
<script type="application/javascript" src="lib/touch-emulator.js"></script>
<script> TouchEmulator(); </script>
-->
<script type="application/javascript" src="https://cdn.jsdelivr.net/npm/jquery@2.2.4/dist/jquery.min.js"></script>
<script type="application/javascript" src="https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.js"></script>
</head>
<body>
<div id="header" data-role="header"><h4 style="color: #fff">Compulsivity</h4></div>
<div id="page-list" data-role="page">
<div data-role="content" role="main">
<ul data-role="listview" data-filter="true" data-inset="true"></ul>
</div>
</div>
<div id="footer" data-role="footer"><h4 style="color: #fff">Scroll</h4></div>
</body>
</html>

Detecting if a user has scrolled away from the top

I'd hate to argue with the creator of jQuery but having a brilliant mind doesn't necessarily mean you're always right of course. Using a delay on scroll might ruin any animation based on the event. I've spent a ridiculous amount of time investigating the scroll event and making plugins and demos based on it and it seems somewhat of an urban myth that it triggers very often of most devices.

Here's a small demo to test the amount - the maximum seems to be the display refresh rate :

Codepen

Unless the browser has smooth scrolling enabled, mousewheeling or panning will only result in a single event getting triggered. And even if that's not the case, most machines are very well equipped to handle as many calls as the frame rate will allow - provided the rest of the web page is built well.

That last bit is the biggest issue, browsers can handle quite a bit of script at small intervals but what could slow down execution most is badly written markup. The biggest bottlenecks are therefore often repaints. This can be well inspected when using developer tools.

Some examples of that are having big blocks of fixed position elements and animations that aren't based on transform but use pixel values instead. Both trigger unnecessary repaints which have a huge impact on performance. These problems aren't directly related to how often scroll fires.

Using fixed position elements, repaints on webkit related browsers can be prevented with this hack :

.fixed {
-webkit-backface-visibility: hidden;
}

This creates a separate stacking order, making the content independent of it's surroundings and preventing the complete page from being repainted. Firefox seems to do this by default but unfortunately I haven't found a property yet that will work with IE so it's best to avoid these kinds of elements in any case, especially if their size is a large portion of the viewport.

The following was not yet implemented at the time of writing of the article mentioned.

And that would be requestAnimationFrame which now has very good browser support. With this, functions can be executed in a way that's not forced on the browser. So if there are complex functions inside the scroll handler, it would be a very good idea to use this.

Another optimisation is to use flagging and not run anything unless it needs to. A helpful tool in this can be to add classes and check for their presence. Here's the approach I generally use :

$(function() {

var flag, modern = window.requestAnimationFrame;

$(window).scroll(function() {

if (!$(this).scrollTop()) {

if (flag) {
if (modern) requestAnimationFrame(doSomething);
else doSomething();
flag = false;
}
}
else if (!flag) {

if (modern) requestAnimationFrame(doMore);
else doMore();
flag = true;
}
});

function doSomething() {

// special magic
}

function doMore() {

// other shenanigans
}
});

And an example of how toggling a class could be used to determine a boolean :

$(function() {

var modern = window.requestAnimationFrame;

$(window).scroll(function() {

var flag = $('#element').hasClass('myclass');

if (!$(this).scrollTop()) {

if (flag) {
if (modern) requestAnimationFrame(doSomething);
else doSomething();
$('#element').removeClass('myclass');
}
}
else if (!flag) {

if (modern) requestAnimationFrame(doMore);
else doMore();
$('#element').addClass('myclass');
}
});

function doSomething() {

// special magic
}

function doMore() {

// other shenanigans
}
});

If it doesn't interfere with any desired functionality, debouncing the event can be a good approach of course. It can be as simple as :

$(function() {

var doit, modern = window.requestAnimationFrame;

$(window).scroll(function() {

clearTimeout(doit);

doit = setTimeout(function() {

if (modern) requestAnimationFrame(doSomething);
else doSomething();

}, 50);
});

function doSomething() {

// stuff going on
}
});

Below in the comments, Kaiido had a few valid points. Apart from having to raise the scope of the variable that defines the timeout, this approach may not be too useful in practice because it will only execute a function after scrolling has finished. I'd like to refer to this interesting article, leading to the conclusion that what would be more effective than debouncing here is throttling - making sure the events only run the function a maximum amount per time unit, closer to what was proposed in the question. Ben Alman made a nice little plugin for this (note it was written in 2010 already).

Example

I've managed to extract only the part needed for throttling, it can still be used in a similar way :

$(window).scroll($.restrain(50, someFunction));

Where the first argument is the amount of time within which only a single event will execute the second parameter - the callback function. Here's the underlying code :

(function(window, undefined) {

var $ = window.jQuery;

$.restrain = function(delay, callback) {

var executed = 0;

function moduleWrap() {

var elapsed = Date.now()-executed;

function runIt() {

executed = Date.now();
callback.apply(this, arguments);
}

if (elapsed > delay) runIt();
}

return moduleWrap;
};
})(this);

What will happen inside the function to be executed can still slow down the process of course. One example of what should always be avoided is using .css() to set style and placing any calculations inside it. This is quite detrimental to performance.

Has to be mentioned as well that the flagging code makes more sense when toggling at a certain point down the page instead of at the top because that position won't fire multiple times. So the check on the flag itself there could be left out in the scope of the question.

Doing this right, it would make sense to also check the scroll position on page load and toggle on the basis of that. Opera is particularly stubborn with this - it resumes the cached position after page load. So a small timeout is best suited :

$(window).on('load', function() {

setTimeout(function() {

// check scroll position and set flag

}, 20);
});

In general I'd say - don't avoid using the scroll event, it can provide very nice effects on a web page. Just be vigilant of how it all sticks together.

Detect if user is scrolling

this works:

window.onscroll = function (e) {  
// called when the window is scrolled.
}

edit:

you said this is a function in a TimeInterval..

Try doing it like so:

userHasScrolled = false;
window.onscroll = function (e)
{
userHasScrolled = true;
}

then inside your Interval insert this:

if(userHasScrolled)
{
//do your code here
userHasScrolled = false;
}

Check if a user has scrolled to the bottom (not just the window, but any element)

Use the .scroll() event on window, like this:

$(window).scroll(function() {
if($(window).scrollTop() + $(window).height() == $(document).height()) {
alert("bottom!");
}
});

You can test it here, this takes the top scroll of the window, so how much it's scrolled down, adds the height of the visible window and checks if that equals the height of the overall content (document). If you wanted to instead check if the user is near the bottom, it'd look something like this:

$(window).scroll(function() {
if($(window).scrollTop() + $(window).height() > $(document).height() - 100) {
alert("near bottom!");
}
});

You can test that version here, just adjust that 100 to whatever pixel from the bottom you want to trigger on.

How to get the number of pixels a user has scrolled down the page?

You can do this using .scroll() and .scrollTop().

 $(window).scroll(function() {
// use the value from $(window).scrollTop();
});

See also this question.



Related Topics



Leave a reply



Submit