90 lines
2.8 KiB
JavaScript
90 lines
2.8 KiB
JavaScript
import { minimumOffset } from "discourse/lib/offset-calculator";
|
|
|
|
// Dear traveller, you are entering a zone where we are at war with the browser.
|
|
// The browser is insisting on positioning scrollTop per the location it was in
|
|
// the past, we are insisting on it being where we want it to be.
|
|
// The hack is just to keep trying over and over to position the scrollbar (up to 1 second).
|
|
//
|
|
// The root cause is that a "refresh" on a topic page will almost never be at the
|
|
// same position it was in the past, the URL points to the post at the top of the
|
|
// page, so a refresh will try to bring that post into view causing drift.
|
|
//
|
|
// Additionally if you loaded multiple batches of posts, on refresh they will not
|
|
// be loaded.
|
|
//
|
|
// This hack leads to a slight jerky experience, however other workarounds are more
|
|
// complex, the 2 options we have are
|
|
//
|
|
// 1. onbeforeunload ensure we are scrolled to the right spot
|
|
// 2. give up on the scrollbar and implement it ourselves (something that will happen)
|
|
|
|
const LOCK_DURATION_MS = 1000;
|
|
const SCROLL_EVENTS =
|
|
"scroll.lock-on touchmove.lock-on mousedown.lock-on wheel.lock-on DOMMouseScroll.lock-on mousewheel.lock-on keyup.lock-on";
|
|
const SCROLL_TYPES = ["mousedown", "mousewheel", "touchmove", "wheel"];
|
|
|
|
function within(threshold, x, y) {
|
|
return Math.abs(x - y) < threshold;
|
|
}
|
|
|
|
export default class LockOn {
|
|
constructor(selector, options) {
|
|
this.selector = selector;
|
|
this.options = options || {};
|
|
}
|
|
|
|
elementTop() {
|
|
const $selected = $(this.selector);
|
|
if ($selected.length && $selected.offset && $selected.offset()) {
|
|
return $selected.offset().top - minimumOffset();
|
|
}
|
|
}
|
|
|
|
clearLock(interval) {
|
|
$("body, html").off(SCROLL_EVENTS);
|
|
clearInterval(interval);
|
|
if (this.options.finished) {
|
|
this.options.finished();
|
|
}
|
|
}
|
|
|
|
lock() {
|
|
const startedAt = new Date().getTime();
|
|
let previousTop = this.elementTop();
|
|
previousTop && $(window).scrollTop(previousTop);
|
|
|
|
const interval = setInterval(() => {
|
|
const elementTop = this.elementTop();
|
|
if (!previousTop && !elementTop) {
|
|
// we can't find the element yet, wait a little bit more
|
|
return;
|
|
}
|
|
|
|
const top = Math.max(0, elementTop);
|
|
const scrollTop = $(window).scrollTop();
|
|
|
|
if (typeof top === "undefined" || isNaN(top)) {
|
|
return this.clearLock(interval);
|
|
}
|
|
|
|
if (!within(4, top, previousTop) || !within(4, scrollTop, top)) {
|
|
$(window).scrollTop(top);
|
|
previousTop = top;
|
|
}
|
|
|
|
// Stop after a little while
|
|
if (new Date().getTime() - startedAt > LOCK_DURATION_MS) {
|
|
return this.clearLock(interval);
|
|
}
|
|
}, 50);
|
|
|
|
$("body, html")
|
|
.off(SCROLL_EVENTS)
|
|
.on(SCROLL_EVENTS, e => {
|
|
if (e.which > 0 || SCROLL_TYPES.includes(e.type)) {
|
|
this.clearLock(interval);
|
|
}
|
|
});
|
|
}
|
|
}
|