mirror of
https://github.com/discourse/discourse.git
synced 2025-01-31 11:36:05 +08:00
FEATURE: Move sticky avatars into core
This patch takes the small component we had for sticky avatars and adds it into our core code base. A small refactor has been made to have a `StickyAvatars` dedicated class.
This commit is contained in:
parent
c47a526371
commit
c2be7c65e8
|
@ -0,0 +1,10 @@
|
|||
import StickyAvatars from "discourse/lib/sticky-avatars";
|
||||
|
||||
export default {
|
||||
name: "sticky-avatars",
|
||||
after: "inject-objects",
|
||||
|
||||
initialize(container) {
|
||||
StickyAvatars.init(container);
|
||||
},
|
||||
};
|
110
app/assets/javascripts/discourse/app/lib/sticky-avatars.js
Normal file
110
app/assets/javascripts/discourse/app/lib/sticky-avatars.js
Normal file
|
@ -0,0 +1,110 @@
|
|||
import { addWidgetCleanCallback } from "discourse/components/mount-widget";
|
||||
import Site from "discourse/models/site";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import { schedule } from "@ember/runloop";
|
||||
|
||||
export default class StickyAvatars {
|
||||
stickyClass = "sticky-avatar";
|
||||
topicPostSelector = "#topic .post-stream .topic-post";
|
||||
intersectionObserver = null;
|
||||
direction = "⬇️";
|
||||
prevOffset = -1;
|
||||
|
||||
static init(container) {
|
||||
new this(container).init();
|
||||
}
|
||||
|
||||
constructor(container) {
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
init() {
|
||||
if (Site.currentProp("mobileView") || !("IntersectionObserver" in window)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const appEvents = this.container.lookup("service:app-events");
|
||||
appEvents.on("topic:current-post-scrolled", this._handlePostNodes);
|
||||
appEvents.on("topic:scrolled", this._handleScroll);
|
||||
appEvents.on("page:topic-loaded", this._initIntersectionObserver);
|
||||
|
||||
addWidgetCleanCallback("post-stream", this._clearIntersectionObserver);
|
||||
}
|
||||
|
||||
@bind
|
||||
_handleScroll(offset) {
|
||||
if (offset <= 0) {
|
||||
this.direction = "⬇️";
|
||||
document
|
||||
.querySelectorAll(`${this.topicPostSelector}.${this.stickyClass}`)
|
||||
.forEach((node) => node.classList.remove(this.stickyClass));
|
||||
} else if (offset > this.prevOffset) {
|
||||
this.direction = "⬇️";
|
||||
} else {
|
||||
this.direction = "⬆️";
|
||||
}
|
||||
this.prevOffset = offset;
|
||||
}
|
||||
|
||||
@bind
|
||||
_handlePostNodes() {
|
||||
this._clearIntersectionObserver();
|
||||
this._initIntersectionObserver();
|
||||
|
||||
schedule("afterRender", () => {
|
||||
document.querySelectorAll(this.topicPostSelector).forEach((postNode) => {
|
||||
this.intersectionObserver.observe(postNode);
|
||||
|
||||
const topicAvatarNode = postNode.querySelector(".topic-avatar");
|
||||
if (!topicAvatarNode || !postNode.querySelector("#post_1")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const topicMapNode = postNode.querySelector(".topic-map");
|
||||
if (!topicMapNode) {
|
||||
return;
|
||||
}
|
||||
topicAvatarNode.style.marginBottom = `${topicMapNode.clientHeight}px`;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@bind
|
||||
_initIntersectionObserver() {
|
||||
schedule("afterRender", () => {
|
||||
const headerOffset =
|
||||
parseInt(
|
||||
getComputedStyle(document.body).getPropertyValue("--header-offset"),
|
||||
10
|
||||
) || 0;
|
||||
const headerHeight = Math.max(headerOffset, 0);
|
||||
|
||||
this.intersectionObserver = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (!entry.isIntersecting || entry.intersectionRatio === 1) {
|
||||
entry.target.classList.remove(this.stickyClass);
|
||||
return;
|
||||
}
|
||||
|
||||
const postContentHeight = entry.target.querySelector(".contents")
|
||||
?.clientHeight;
|
||||
if (
|
||||
this.direction === "⬆️" ||
|
||||
postContentHeight > window.innerHeight - headerHeight
|
||||
) {
|
||||
entry.target.classList.add(this.stickyClass);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: [0.0, 1.0], rootMargin: `-${headerHeight}px 0px 0px 0px` }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@bind
|
||||
_clearIntersectionObserver() {
|
||||
this.intersectionObserver?.disconnect();
|
||||
this.intersectionObserver = null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
|
||||
import { test } from "qunit";
|
||||
import { find, scrollTo, visit, waitUntil } from "@ember/test-helpers";
|
||||
import { setupApplicationTest as EMBER_CLI_ENV } from "ember-qunit";
|
||||
|
||||
acceptance("Sticky Avatars", function (needs) {
|
||||
if (!EMBER_CLI_ENV) {
|
||||
return; // helpers not available in legacy env
|
||||
}
|
||||
|
||||
const container = document.getElementById("ember-testing-container");
|
||||
|
||||
needs.hooks.beforeEach(function () {
|
||||
container.scrollTop = 0;
|
||||
});
|
||||
|
||||
test("Adds sticky avatars when scrolling up", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
|
||||
await scrollTo(container, 0, 800);
|
||||
await scrollTo(container, 0, 700);
|
||||
|
||||
await waitUntil(() => find(".sticky-avatar"));
|
||||
assert.ok(
|
||||
find("#post_5").parentElement.classList.contains("sticky-avatar"),
|
||||
"Sticky avatar is applied"
|
||||
);
|
||||
});
|
||||
});
|
|
@ -769,6 +769,14 @@ span.highlighted {
|
|||
transition: visibility 1s, opacity ease-out 1s;
|
||||
}
|
||||
|
||||
.topic-post.sticky-avatar {
|
||||
.topic-avatar {
|
||||
position: sticky;
|
||||
top: calc(var(--header-offset) - 0.25em);
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablet (portrait) ----------- */
|
||||
|
||||
@media all and (max-width: 790px) {
|
||||
|
|
Loading…
Reference in New Issue
Block a user