On a responsive website where elements may shrink/expand according to the size of your viewport, the elements that was at the top of your viewport at the time may get pushed down/up as the elements above them get squished/expanded. Forcing you to scroll down/up again in order to get to the same area you were looking at.
This javascript code attempts to get rid of this problem by finding and updating the element at/near the top of the viewport on load and on scroll. Then it performs a scroll towards the current top element when the size of the viewport changes.
Based heavily on the answer I've received in my SO question for this, I got the following code in the end:
// Define detection area (where to pick element that we will lock on to)
var DETECT_TOP = 5,
DETECT_SIDE = 60,
DETECT_HEIGHT = 60;
// Step between each check in the detection box
// laggy if too small
var DETECT_STEP_X = 50,
DETECT_STEP_Y = 10;
// Cut-off is when the element we detected is sliced
// in-between by the top of the viewport
var CUTOFF_ADJUST = true;
var viewportWidth = $(window).width();
function Node () {
var that = this;
this.element = null;
// data
this.documentOffset = {
// jQuery element.offset().top is funny in old Safari
top: function () {
if (!that.element) { return 0; }
return that.element.offsetTop + that.element.offsetParent.offsetTop;
},
bottom: function () {
if (!that.element) { return 0; }
return this.top() + $(that.element).height();
}
};
this.viewportOffset = {
//offsets before and after resizing may be different in a responsive website
// (thinner <p> increases its height due to word-wrap, etc.)
oldTop: 0,
oldBottom: 0,
top: function () {
this.oldTop = that.documentOffset.top() - $(window).scrollTop();
return this.oldTop;
},
bottom: function () {
this.oldBottom = that.documentOffset.bottom() - $(window).scrollTop();
return this.oldBottom;
}
};
// Main functions
this.update = function () {
var sampleElement, timer;
clearTimeout(timer);
//
// Timeout stuff = fix for old IE
// https://stackoverflow.com/questions/8227345/elementfrompoint-returns-null-when-it-used-in-onscroll-event
//
timer = setTimeout(
function () {
for (var y = DETECT_TOP; y <= DETECT_TOP + DETECT_HEIGHT; y+= DETECT_STEP_Y) {
for (var x = DETECT_SIDE; x <= $(document).width() - DETECT_SIDE; x+= DETECT_STEP_X) {
//scans the detected box for an element
sampleElement = getElementAt(x, y);
// Change element criterions here
// e.g. we don't want it to pick underlying container div
// don't pick fixed sidebar, etc. etc.
if (sampleElement && sampleElement.children.length === 0) {
that.element = sampleElement;
that.updateOffsets();
return;
}
}
}
},
0
);
};
this.scrollTo = function (act) {
var scrollAmount = that.documentOffset.top();
//calculate scroll amount, then perform it
if (that.element) {
if (CUTOFF_ADJUST) {
if (that.wasBelowViewportTop()) {
scrollAmount -= that.viewportOffset.oldTop;
} else {
scrollAmount = that.documentOffset.bottom() - that.viewportOffset.oldBottom;
}
}
if (navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1) {
//
// Setting scrollTop() on Safari:
// $(document).scrollTop(scrollAmount) works only when expanding
// $("body").scrollTop(scrollAmount) works only when shrinking
//
if (act == "expanding") {
$(document).scrollTop(scrollAmount);
}
if (act == "shrinking") {
$("body").scrollTop(scrollAmount);
}
} else {
$("html, body").scrollTop(scrollAmount);
}
that.updateOffsets();
}
};
// helper functions
this.updateOffsets = function () {
that.viewportOffset.top();
that.viewportOffset.bottom();
};
this.wasBelowViewportTop = function () {
if (this.viewportOffset.oldTop >= 0) {
return true;
}
return false;
};
}
var elementFromPointFeatureCheck = false,
elementFromPointIsRelative = true;
function getElementAt (x, y) {
//
// Various fixes for document.elementFromPoint(x, y)
// null check is for IE8,
// !elementFromPointIsRelative is for browsers that uses x, y relative to document (some old Safari and Opera etc.)
// elementFromPointIsRelative is for browsers that uses x, y relative to viewport (default functionality)
//
// https://stackoverflow.com/questions/17953672/document-elementfrompoint-in-jquery/19363125#19363125
// http://www.zehnet.de/2010/11/19/document-elementfrompoint-a-jquery-solution/
// (see comment on zehnet)
//
var scroll, doc;
if (!document.elementFromPoint) {
return null;
}
if (!elementFromPointFeatureCheck) {
if ((scroll = $(window).scrollTop()) > 0) {
doc = document.elementFromPoint(0, scroll + $(window).height() -1);
if (doc !== null && doc.tagName == "HTML") {
doc = null;
}
elementFromPointIsRelative = (doc === null);
}
elementFromPointFeatureCheck = (scroll > 0);
}
if (!elementFromPointIsRelative) {
y += $(window).scrollTop();
}
return document.elementFromPoint(x, y);
}
That's the main definitions chunk of the code. Then I put it all together:
var detectedNode = new Node();
$(window)
.load(detectedNode.update)
.scroll(detectedNode.update)
.resize(function () {
var act = (viewportWidth <= $(window).width()) ? "expanding" : "shrinking";
viewportWidth = $(window).width();
detectedNode.scrollTo(act);
});
And everything is inside a jQuery(document).ready()
.
What are some changes I should make and is there any issues and/or best practices that I should be made aware of?
I think that the code is a little too cluttered with browser fixes, but I am not too sure how, or whether or not I should separate them to make the main part of the code more readable.
I am also rather new to oop, but I have a feeling that my Node object is doing more than it should. Although we scrollTo
the node, but it isn't the node that's doing the scrolling? But then what about the node update
method?
Demo
Blank mark-up for comparison.
jsbin Demo with the mark-up above.
Browser Support
It should at least work in recent versions of chrome, ff, ie, safari, and opera.
I am still trying to add fixes to make it work for slightly older versions like saf 4, opera 10 and/or beyond, but these aren't a priority.
Here's the quirksmode page containing the functions used in the code.