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:
Loïc Guitaut 2021-10-27 14:41:29 +02:00 committed by Loïc Guitaut
parent c47a526371
commit c2be7c65e8
4 changed files with 157 additions and 0 deletions

View File

@ -0,0 +1,10 @@
import StickyAvatars from "discourse/lib/sticky-avatars";
export default {
name: "sticky-avatars",
after: "inject-objects",
initialize(container) {
StickyAvatars.init(container);
},
};

View 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;
}
}

View File

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

View File

@ -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) {