DEV: drops jquery from scrolling-post-stream (#15313)

Note that this commit also introduces a `domUtils` helper to handle most complex operations in vanilla JS compared to using jQuery.
This commit is contained in:
Joffrey JAFFEUX 2021-12-17 14:52:42 +01:00 committed by GitHub
parent 769388b8ba
commit bec76f937c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 137 additions and 52 deletions

View File

@ -0,0 +1,21 @@
function offset(element) {
// note that getBoundingClientRect forces a reflow.
// When used in critical performance conditions
// you might want to move to more involved solution
// such as implementing an IntersectionObserver and
// using its boundingClientRect property
const rect = element.getBoundingClientRect();
return {
top: rect.top + window.scrollY,
left: rect.left + window.scrollX,
};
}
function position(element) {
return {
top: element.offsetTop,
left: element.offsetLeft,
};
}
export default { offset, position };

View File

@ -1,5 +1,5 @@
import { cloak, uncloak } from "discourse/widgets/post-stream";
import { next, scheduleOnce } from "@ember/runloop";
import { schedule, scheduleOnce } from "@ember/runloop";
import DiscourseURL from "discourse/lib/url";
import MountWidget from "discourse/components/mount-widget";
import discourseDebounce from "discourse-common/lib/debounce";
@ -7,18 +7,20 @@ import { isWorkaroundActive } from "discourse/lib/safari-hacks";
import offsetCalculator from "discourse/lib/offset-calculator";
import { inject as service } from "@ember/service";
import { bind } from "discourse-common/utils/decorators";
import domUtils from "discourse-common/utils/dom-utils";
const DEBOUNCE_DELAY = 50;
function findTopView($posts, viewportTop, postsWrapperTop, min, max) {
function findTopView(posts, viewportTop, postsWrapperTop, min, max) {
if (max < min) {
return min;
}
while (max > min) {
const mid = Math.floor((min + max) / 2);
const $post = $($posts[mid]);
const viewBottom = $post.offset().top - postsWrapperTop + $post.height();
const post = posts.item(mid);
const viewBottom =
domUtils.offset(post).top - postsWrapperTop + post.clientHeight;
if (viewBottom > viewportTop) {
max = mid - 1;
@ -57,20 +59,20 @@ export default MountWidget.extend({
},
beforePatch() {
const $body = $(document);
this.prevHeight = $body.height();
this.prevScrollTop = $body.scrollTop();
this.prevHeight = document.body.clientHeight;
this.prevScrollTop = document.body.scrollTop;
},
afterPatch() {
const $body = $(document);
const height = $body.height();
const scrollTop = $body.scrollTop();
const height = document.body.clientHeight;
// This hack is for when swapping out many cloaked views at once
// when using keyboard navigation. It could suddenly move the scroll
if (this.prevHeight === height && scrollTop !== this.prevScrollTop) {
$body.scrollTop(this.prevScrollTop);
if (
this.prevHeight === height &&
document.body.scrollTop !== this.prevScrollTop
) {
document.body.scroll({ left: 0, top: this.prevScrollTop });
}
},
@ -97,31 +99,30 @@ export default MountWidget.extend({
return;
}
const $w = $(window);
const windowHeight = window.innerHeight ? window.innerHeight : $w.height();
const windowHeight = window.innerHeight;
const slack = Math.round(windowHeight * 5);
const onscreen = [];
const nearby = [];
const windowTop = $w.scrollTop();
const postsWrapperTop = $(".posts-wrapper").offset().top;
const $posts = $(
this.element.querySelectorAll(".onscreen-post, .cloaked-post")
const windowTop = document.documentElement.scrollTop;
const postsWrapperTop = domUtils.offset(
document.querySelector(".posts-wrapper")
).top;
const postsNodes = this.element.querySelectorAll(
".onscreen-post, .cloaked-post"
);
const viewportTop = windowTop - slack;
const topView = findTopView(
$posts,
postsNodes,
viewportTop,
postsWrapperTop,
0,
$posts.length - 1
postsNodes.length - 1
);
let windowBottom = windowTop + windowHeight;
let viewportBottom = windowBottom + slack;
const bodyHeight = $("body").height();
const bodyHeight = document.body.clientHeight;
if (windowBottom > bodyHeight) {
windowBottom = bodyHeight;
}
@ -148,16 +149,15 @@ export default MountWidget.extend({
let allAbove = true;
let bottomView = topView;
let lastBottom = 0;
while (bottomView < $posts.length) {
const post = $posts[bottomView];
const $post = $(post);
while (bottomView < postsNodes.length) {
const post = postsNodes.item(bottomView);
if (!$post) {
if (!post) {
break;
}
const viewTop = $post.offset().top;
const postHeight = $post.outerHeight(true);
const viewTop = domUtils.offset(post).top;
const postHeight = post.clientHeight;
const viewBottom = Math.ceil(viewTop + postHeight);
allAbove = allAbove && viewTop < topCheck;
@ -199,28 +199,30 @@ export default MountWidget.extend({
const first = posts.objectAt(onscreen[0]);
if (this._topVisible !== first) {
this._topVisible = first;
const $body = $("body");
const elem = $posts[onscreen[0]];
const elem = postsNodes.item(onscreen[0]);
const elemId = elem.id;
const $elem = $(elem);
const elemPos = $elem.position();
const distToElement = elemPos ? $body.scrollTop() - elemPos.top : 0;
const elemPos = domUtils.position(elem);
const distToElement = elemPos
? document.body.scrollTop - elemPos.top
: 0;
const topRefresh = () => {
refresh(() => {
const $refreshedElem = $(`#${elemId}`);
const refreshedElem = document.getElementById(elemId);
// Quickly going back might mean the element is destroyed
const position = $refreshedElem.position();
const position = domUtils.position(refreshedElem);
if (position && position.top) {
let whereY = position.top + distToElement;
$("html, body").scrollTop(whereY);
document.documentElement.scroll({ top: whereY, left: 0 });
// This seems weird, but somewhat infrequently a rerender
// will cause the browser to scroll to the top of the document
// in Chrome. This makes sure the scroll works correctly if that
// happens.
next(() => $("html, body").scrollTop(whereY));
schedule("afterRender", () => {
document.documentElement.scroll({ top: whereY, left: 0 });
});
}
});
};
@ -292,7 +294,7 @@ export default MountWidget.extend({
_posted(staged) {
this.queueRerender(() => {
if (staged) {
const postNumber = staged.get("post_number");
const postNumber = staged.post_number;
DiscourseURL.jumpToPost(postNumber, { skipIfOnScreen: true });
}
});
@ -340,18 +342,17 @@ export default MountWidget.extend({
this.appEvents.on("post-stream:posted", this, "_posted");
$(this.element).on(
"mouseenter.post-stream",
"button.widget-button",
(e) => {
$("button.widget-button").removeClass("d-hover");
$(e.target).addClass("d-hover");
}
this.element.addEventListener(
"mouseenter",
this._handleWidgetButtonHoverState,
true
);
$(this.element).on("mouseleave.post-stream", "button.widget-button", () => {
$("button.widget-button").removeClass("d-hover");
});
this.element.addEventListener(
"mouseleave",
this._removeWidgetButtonHoverState,
true
);
this.appEvents.on("post-stream:refresh", this, "_refresh");
@ -365,12 +366,36 @@ export default MountWidget.extend({
willDestroyElement() {
this._super(...arguments);
document.removeEventListener("touchmove", this._debouncedScroll);
window.removeEventListener("scroll", this._debouncedScroll);
this.appEvents.off("post-stream:refresh", this, "_debouncedScroll");
$(this.element).off("mouseenter.post-stream");
$(this.element).off("mouseleave.post-stream");
this.element.removeEventListener(
"mouseenter",
this._handleWidgetButtonHoverState
);
this.element.removeEventListener(
"mouseleave",
this._removeWidgetButtonHoverState
);
this.appEvents.off("post-stream:refresh", this, "_refresh");
this.appEvents.off("post-stream:posted", this, "_posted");
},
_handleWidgetButtonHoverState(event) {
if (event.target.classList.contains("widget-button")) {
document
.querySelectorAll("button.widget-button")
.forEach((widgetButton) => {
widgetButton.classList.remove("d-hover");
});
event.target.classList.add("d-hover");
}
},
_removeWidgetButtonHoverState() {
document.querySelectorAll("button.widget-button").forEach((button) => {
button.classList.remove("d-hover");
});
},
});

View File

@ -0,0 +1,39 @@
import componentTest, {
setupRenderingTest,
} from "discourse/tests/helpers/component-test";
import { discourseModule } from "discourse/tests/helpers/qunit-helpers";
import hbs from "htmlbars-inline-precompile";
import domUtils from "discourse-common/utils/dom-utils";
discourseModule("utils:dom-utils", function (hooks) {
setupRenderingTest(hooks);
componentTest("offset", {
template: hbs`{{d-button translatedLabel="baz"}}`,
async test(assert) {
const element = document.querySelector(".btn");
const offset = domUtils.offset(element);
const rect = element.getBoundingClientRect();
assert.deepEqual(offset, {
top: rect.top + window.scrollY,
left: rect.left + window.scrollX,
});
},
});
componentTest("position", {
template: hbs`{{d-button translatedLabel="baz"}}`,
async test(assert) {
const element = document.querySelector(".btn");
const position = domUtils.position(element);
assert.deepEqual(position, {
top: element.offsetTop,
left: element.offsetLeft,
});
},
});
});