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:
parent
769388b8ba
commit
bec76f937c
|
@ -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 };
|
|
@ -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");
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue