REFACTOR: `LockOn` class (#10428)
Mostly de-jQuery-ification. This refactor tries to closely preserve the original behavior. Changes: * Store the interval inside the class (allows using `clearLock()` on `LockOn` objects) * Extract the interval function to a separate method * Math.max result is never undefined (per MDN: "[Return value] The largest of the given numbers. If at least one of the arguments cannot be converted to a number, NaN is returned.") * Replace jQuery's `offset()` * Private methods be private * Native `scrollTop` (jQuery's just a wrapper for this) * `addEventListener`/`removeEventListener`
This commit is contained in:
parent
2008ecd68e
commit
7a8442435c
|
@ -19,8 +19,15 @@ import { minimumOffset } from "discourse/lib/offset-calculator";
|
||||||
// 2. give up on the scrollbar and implement it ourselves (something that will happen)
|
// 2. give up on the scrollbar and implement it ourselves (something that will happen)
|
||||||
|
|
||||||
const LOCK_DURATION_MS = 1000;
|
const LOCK_DURATION_MS = 1000;
|
||||||
const SCROLL_EVENTS =
|
const SCROLL_EVENTS = [
|
||||||
"scroll.lock-on touchmove.lock-on mousedown.lock-on wheel.lock-on DOMMouseScroll.lock-on mousewheel.lock-on keyup.lock-on";
|
"scroll",
|
||||||
|
"touchmove",
|
||||||
|
"mousedown",
|
||||||
|
"wheel",
|
||||||
|
"DOMMouseScroll",
|
||||||
|
"mousewheel",
|
||||||
|
"keyup"
|
||||||
|
];
|
||||||
const SCROLL_TYPES = ["mousedown", "mousewheel", "touchmove", "wheel"];
|
const SCROLL_TYPES = ["mousedown", "mousewheel", "touchmove", "wheel"];
|
||||||
|
|
||||||
function within(threshold, x, y) {
|
function within(threshold, x, y) {
|
||||||
|
@ -31,59 +38,95 @@ export default class LockOn {
|
||||||
constructor(selector, options) {
|
constructor(selector, options) {
|
||||||
this.selector = selector;
|
this.selector = selector;
|
||||||
this.options = options || {};
|
this.options = options || {};
|
||||||
|
this._boundScrollListener = this._scrollListener.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
elementTop() {
|
elementTop() {
|
||||||
const $selected = $(this.selector);
|
const element = document.querySelector(this.selector);
|
||||||
if ($selected.length && $selected.offset && $selected.offset()) {
|
if (!element) {
|
||||||
return $selected.offset().top - minimumOffset();
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { top } = element.getBoundingClientRect();
|
||||||
|
const offset = top + window.scrollY;
|
||||||
|
|
||||||
|
return offset - minimumOffset();
|
||||||
}
|
}
|
||||||
|
|
||||||
clearLock(interval) {
|
clearLock() {
|
||||||
$("body, html").off(SCROLL_EVENTS);
|
this._removeListener();
|
||||||
clearInterval(interval);
|
clearInterval(this.interval);
|
||||||
|
|
||||||
if (this.options.finished) {
|
if (this.options.finished) {
|
||||||
this.options.finished();
|
this.options.finished();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lock() {
|
lock() {
|
||||||
const startedAt = Date.now();
|
this.startedAt = Date.now();
|
||||||
let previousTop = this.elementTop();
|
this.previousTop = this.elementTop();
|
||||||
previousTop && $(window).scrollTop(previousTop);
|
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
if (this.previousTop) {
|
||||||
const elementTop = this.elementTop();
|
window.scrollTo(window.pageXOffset, this.previousTop);
|
||||||
if (!previousTop && !elementTop) {
|
}
|
||||||
// we can't find the element yet, wait a little bit more
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const top = Math.max(0, elementTop);
|
this.interval = setInterval(() => this._performLocking(), 50);
|
||||||
const scrollTop = $(window).scrollTop();
|
|
||||||
|
|
||||||
if (typeof top === "undefined" || isNaN(top)) {
|
this._removeListener();
|
||||||
return this.clearLock(interval);
|
this._addListener();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!within(4, top, previousTop) || !within(4, scrollTop, top)) {
|
_scrollListener(event) {
|
||||||
$(window).scrollTop(top);
|
if (event.which > 0 || SCROLL_TYPES.includes(event.type)) {
|
||||||
previousTop = top;
|
this.clearLock();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Stop after a little while
|
_addListener() {
|
||||||
if (Date.now() - startedAt > LOCK_DURATION_MS) {
|
const body = document.querySelector("body");
|
||||||
return this.clearLock(interval);
|
const html = document.querySelector("html");
|
||||||
}
|
|
||||||
}, 50);
|
|
||||||
|
|
||||||
$("body, html")
|
SCROLL_EVENTS.forEach(event => {
|
||||||
.off(SCROLL_EVENTS)
|
body.addEventListener(event, this._boundScrollListener);
|
||||||
.on(SCROLL_EVENTS, e => {
|
html.addEventListener(event, this._boundScrollListener);
|
||||||
if (e.which > 0 || SCROLL_TYPES.includes(e.type)) {
|
});
|
||||||
this.clearLock(interval);
|
}
|
||||||
}
|
|
||||||
});
|
_removeListener() {
|
||||||
|
const body = document.querySelector("body");
|
||||||
|
const html = document.querySelector("html");
|
||||||
|
|
||||||
|
SCROLL_EVENTS.forEach(event => {
|
||||||
|
body.removeEventListener(event, this._boundScrollListener);
|
||||||
|
html.removeEventListener(event, this._boundScrollListener);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_performLocking() {
|
||||||
|
const elementTop = this.elementTop();
|
||||||
|
|
||||||
|
// If we can't find the element yet, wait a little bit more
|
||||||
|
if (!this.previousTop && !elementTop) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const top = Math.max(0, elementTop);
|
||||||
|
|
||||||
|
if (isNaN(top)) {
|
||||||
|
return this.clearLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!within(4, top, this.previousTop) ||
|
||||||
|
!within(4, window.scrollTop, top)
|
||||||
|
) {
|
||||||
|
window.scrollTo(window.pageXOffset, top);
|
||||||
|
this.previousTop = top;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop after a little while
|
||||||
|
if (Date.now() - this.startedAt > LOCK_DURATION_MS) {
|
||||||
|
return this.clearLock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue