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