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:
Jarek Radosz 2020-08-13 16:43:05 +02:00 committed by GitHub
parent 2008ecd68e
commit 7a8442435c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 81 additions and 38 deletions

View File

@ -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();
}
} }
} }