REFACTOR: Use IntersectionObserver to calculate topic progress position (#14698)

This commit is contained in:
Penar Musaraj 2021-10-29 09:23:15 -04:00 committed by GitHub
parent 19c9b892dc
commit 095421a1e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 175 additions and 286 deletions

View File

@ -13,6 +13,10 @@ const MIN_WIDTH_TIMELINE = 924,
MIN_HEIGHT_TIMELINE = 325;
export default Component.extend(PanEvents, {
classNameBindings: [
"info.topicProgressExpanded:topic-progress-expanded",
"info.renderTimeline:render-timeline",
],
composerOpen: null,
info: null,
isPanning: false,

View File

@ -1,4 +1,4 @@
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import discourseComputed, { bind } from "discourse-common/utils/decorators";
import Component from "@ember/component";
import I18n from "I18n";
import { alias } from "@ember/object/computed";
@ -68,128 +68,100 @@ export default Component.extend({
return readPos < stream.length - 1 && readPos > position;
},
@observes("postStream.stream.[]")
_updateBar() {
scheduleOnce("afterRender", this, this._updateProgressBar);
},
_topicScrolled(event) {
if (this.docked) {
this.set("progressPosition", this.get("postStream.filteredPostsCount"));
this._streamPercentage = 1.0;
this.setProperties({
progressPosition: this.get("postStream.filteredPostsCount"),
_streamPercentage: 100,
});
} else {
this.set("progressPosition", event.postIndex);
this._streamPercentage = event.percent;
this.setProperties({
progressPosition: event.postIndex,
_streamPercentage: (event.percent * 100).toFixed(2),
});
}
},
this._updateBar();
@discourseComputed("_streamPercentage")
progressStyle(_streamPercentage) {
return `--progress-bg-width: ${_streamPercentage || 0}%`;
},
didInsertElement() {
this._super(...arguments);
this.appEvents
.on("composer:will-open", this, this._dock)
.on("composer:resized", this, this._dock)
.on("composer:closed", this, this._dock)
.on("topic:scrolled", this, this._dock)
.on("composer:resized", this, this._composerEvent)
.on("topic:current-post-scrolled", this, this._topicScrolled);
const prevEvent = this.prevEvent;
if (prevEvent) {
scheduleOnce("afterRender", this, this._topicScrolled, prevEvent);
} else {
scheduleOnce("afterRender", this, this._updateProgressBar);
if (this.prevEvent) {
scheduleOnce("afterRender", this, this._topicScrolled, this.prevEvent);
}
scheduleOnce("afterRender", this, this._dock);
scheduleOnce("afterRender", this, this._startObserver);
},
willDestroyElement() {
this._super(...arguments);
this._topicBottomObserver?.disconnect();
this.appEvents
.off("composer:will-open", this, this._dock)
.off("composer:resized", this, this._dock)
.off("composer:closed", this, this._dock)
.off("topic:scrolled", this, this._dock)
.off("composer:resized", this, this._composerEvent)
.off("topic:current-post-scrolled", this, this._topicScrolled);
},
_updateProgressBar() {
if (this.isDestroyed || this.isDestroying) {
return;
}
const $topicProgress = $(this.element.querySelector("#topic-progress"));
// speeds up stuff, bypass jquery slowness and extra checks
if (!this._totalWidth) {
this._totalWidth = $topicProgress[0].offsetWidth;
}
// Only show percentage once we have one
if (!this._streamPercentage) {
return;
}
const totalWidth = this._totalWidth;
const progressWidth = (this._streamPercentage || 0) * totalWidth;
const borderSize = progressWidth === totalWidth ? "0px" : "1px";
const $bg = $topicProgress.find(".bg");
if ($bg.length === 0) {
const style = `border-right-width: ${borderSize}; width: ${progressWidth}px`;
$topicProgress.append(`<div class='bg' style="${style}">&nbsp;</div>`);
} else {
$bg.css("border-right-width", borderSize).width(progressWidth - 2);
_startObserver() {
if ("IntersectionObserver" in window) {
this._topicBottomObserver = this._setupObserver();
this._topicBottomObserver.observe(
document.querySelector("#topic-bottom")
);
}
},
_dock() {
const $wrapper = $(this.element);
if (!$wrapper || $wrapper.length === 0) {
return;
}
_setupObserver() {
const composerH =
document.querySelector("#reply-control")?.clientHeight || 0;
const $html = $("html");
const offset = window.pageYOffset || $html.scrollTop();
const maximumOffset = $("#topic-bottom").offset().top;
const windowHeight = $(window).height();
let composerHeight = $("#reply-control").height() || 0;
const isDocked = offset >= maximumOffset - windowHeight + composerHeight;
let bottom = $("body").height() - maximumOffset;
const $iPadFooterNav = $(".footer-nav-ipad .footer-nav");
if ($iPadFooterNav && $iPadFooterNav.length > 0) {
bottom += $iPadFooterNav.outerHeight();
}
const draftComposerHeight = 40;
if (composerHeight > 0) {
const $iPhoneFooterNav = $(".footer-nav-visible .footer-nav");
const $replyDraft = $("#reply-control.draft");
if ($iPhoneFooterNav.outerHeight() && $replyDraft.outerHeight()) {
composerHeight =
$replyDraft.outerHeight() + $iPhoneFooterNav.outerHeight();
}
$wrapper.css("bottom", isDocked ? bottom : composerHeight);
} else {
$wrapper.css("bottom", isDocked ? bottom : "");
}
this.set("docked", isDocked);
$wrapper.css(
"margin-bottom",
!isDocked && composerHeight > draftComposerHeight ? "0px" : ""
);
this.appEvents.trigger("topic-progress:docked-status-changed", {
docked: isDocked,
element: this.element,
return new IntersectionObserver(this._intersectionHandler, {
threshold: 0.1,
rootMargin: `0px 0px -${composerH}px 0px`,
});
},
_composerEvent() {
// reinitializing needed to account for composer height
// might be no longer necessary if IntersectionObserver API supports dynamic rootMargin
// see https://github.com/w3c/IntersectionObserver/issues/428
if ("IntersectionObserver" in window) {
this._topicBottomObserver?.disconnect();
this._startObserver();
}
},
@bind
_intersectionHandler(entries) {
if (entries[0].isIntersecting === true) {
this.set("docked", true);
} else {
if (entries[0].boundingClientRect.top > 0) {
this.set("docked", false);
const wrapper = document.querySelector("#topic-progress-wrapper");
const composerH =
document.querySelector("#reply-control")?.clientHeight || 0;
if (composerH === 0) {
const filteredPostsHeight =
document.querySelector(".posts-filtered-notice")?.clientHeight || 0;
filteredPostsHeight === 0
? wrapper.style.removeProperty("bottom")
: wrapper.style.setProperty("bottom", `${filteredPostsHeight}px`);
} else {
wrapper.style.setProperty("bottom", `${composerH}px`);
}
}
}
},
click(e) {
if ($(e.target).closest("#topic-progress").length) {
if (e.target.closest("#topic-progress")) {
this.send("toggleExpansion");
}
},

View File

@ -8,7 +8,7 @@
</div>
{{/if}}
<nav title={{i18n "topic.progress.title"}} class={{if hideProgress "hidden"}} id="topic-progress">
<nav title={{i18n "topic.progress.title"}} class={{if hideProgress "hidden"}} id="topic-progress" style={{html-safe progressStyle}}>
<div class="nums">
<h4>{{progressPosition}}</h4>
<span class={{if hugeNumberOfPosts "hidden"}}>
@ -16,4 +16,5 @@
<h4>{{postStream.filteredPostsCount}}</h4>
</span>
</div>
<div class="bg"></div>
</nav>

View File

@ -26,7 +26,7 @@ createWidget("admin-menu-button", {
});
createWidget("topic-admin-menu-button", {
tagName: "span",
tagName: "span.topic-admin-menu-button",
buildKey: () => "topic-admin-menu-button",
defaultState() {

View File

@ -1261,7 +1261,7 @@ a.mention-group {
}
.topic-admin-menu-button-container {
display: inline-flex;
> span {
.topic-admin-menu-button {
display: flex; // to make this button match siblings behavior, all of its parents need to be flex
}
}

View File

@ -1,3 +1,5 @@
$topic-progress-height: 42px;
@keyframes button-jump-up {
0% {
margin-bottom: -60px;
@ -40,9 +42,8 @@
}
// timeline
@media screen and (min-width: 925px) {
// at 925px viewport width and above the timeline is visible (see topic-navigation.js)
.topic-navigation {
.topic-navigation {
&.render-timeline {
grid-area: timeline;
align-self: start;
@include sticky;
@ -50,30 +51,28 @@
margin-left: 1em;
z-index: z("timeline");
}
&:not(.render-timeline) {
// span all columns of grid layout so RTL can go as far left as possible
grid-column: 1/-1;
// save the space to avoid jumping when child gets fixed-positioned
min-height: $topic-progress-height;
}
&.topic-progress-expanded {
z-index: z("fullscreen");
}
}
// progress bar
@media screen and (max-width: 924px) {
// at 924px viewport width and below the progress bar is visible (see topic-navigation.js)
grid-template-areas: "posts posts";
.timeline-container:not(.timeline-fullscreen) {
display: none; // hiding this because sometimes the JS switch lags and causes layout issues
}
.timeline-container {
.timeline-scroller-content {
position: relative;
}
.timeline-container .timeline-scroller-content {
position: relative;
}
}
}
@media screen and (max-width: 924px) {
.post-stream {
// make space for the topic progress bar to dock
padding-bottom: 2em;
}
}
.progress-back-container {
z-index: z("dropdown");
margin-right: 0;
@ -91,8 +90,14 @@
}
#topic-progress-wrapper {
position: fixed;
bottom: 0px;
transition: bottom 0.1s, margin-bottom 0.1s;
right: 10px;
margin: 0 auto;
display: flex;
right: 9px; // 8px padding on #main-outlet + 1px right border
justify-content: flex-end;
z-index: z("timeline");
.topic-admin-menu-button-container {
display: flex;
> span {
@ -109,11 +114,63 @@
0
); // iOS11 Rendering bug https://meta.discourse.org/t/wrench-menu-not-disappearing-on-ios/94297
}
&.docked {
position: initial;
.topic-admin-popup-menu.right-side {
bottom: -150px; // Prevents menu from being too high when a topic is very short
}
}
body:not(.footer-nav-visible) & {
bottom: env(safe-area-inset-bottom);
}
&:not(.docked) {
@media screen and (min-width: $reply-area-max-width) {
// position to right side of composer
right: 50%;
margin-right: calc(#{$reply-area-max-width} / 2 * -1);
}
}
}
#topic-progress-wrapper.docked {
.topic-admin-popup-menu.right-side {
bottom: -150px; // Prevents menu from being too high when a topic is very short
#topic-progress {
position: relative;
background-color: var(--secondary);
color: var(--tertiary);
border: 1px solid var(--tertiary-low);
width: 145px;
height: $topic-progress-height;
/* as a big ol' click target, don't let text inside be selected */
@include unselectable;
.nums {
position: relative;
top: 12px;
width: 100%;
text-align: center;
z-index: z("base");
}
h4 {
display: inline;
font-size: $font-up-2;
line-height: $line-height-small;
}
.d-icon {
position: absolute;
right: 8px;
bottom: 9px;
z-index: z("base");
}
.bg {
position: absolute;
top: 0;
bottom: 0;
width: var(--progress-bg-width, 0);
background-color: var(--tertiary-low);
transition: width 0.75s;
}
}

View File

@ -9,19 +9,18 @@ body.footer-nav-visible {
padding-bottom: $footer-nav-height + 15;
}
#topic-progress-wrapper,
#reply-control.draft {
bottom: $footer-nav-height;
}
#reply-control.draft {
margin-bottom: env(safe-area-inset-bottom);
padding-bottom: 0px;
}
#topic-progress-wrapper:not(.docked) {
margin-bottom: calc(#{$footer-nav-height} + env(safe-area-inset-bottom));
}
.posts-filtered-notice {
transition: all linear 0.1s;
bottom: $footer-nav-height + 15;
bottom: calc(#{$footer-nav-height} + env(safe-area-inset-bottom));
}
}

View File

@ -29,7 +29,7 @@
}
.topic-timeline {
.timeline-footer-controls {
display: inherit;
display: flex;
}
}
.timeline-controls {
@ -119,17 +119,19 @@
bottom: 20px;
left: 10px;
button,
.btn-group {
margin-bottom: 0;
margin-right: 15px;
vertical-align: top;
.topic-notifications-button {
margin-right: 0.5em;
}
.widget-component-connector {
vertical-align: top;
.jump-to-post {
margin-bottom: 0.5em;
}
.topic-admin-menu-button {
display: flex;
}
}
.timeline-scrollarea-wrapper {
display: table-cell;
padding-right: 1.5em;
@ -140,7 +142,7 @@
border-right-style: solid;
border-right-width: 1px;
max-width: 120px;
float: right;
margin-top: 2em;
.timeline-scroller {
position: relative;

View File

@ -613,12 +613,14 @@ blockquote {
box-sizing: border-box;
}
.topic-area > .loading-container {
.topic-area > .loading-container,
.topic-navigation:not(.render-timeline) {
// loader needs to be same width as posts
width: calc(
#{$topic-avatar-width} + #{$topic-body-width} +
(#{$topic-body-width-padding} * 2)
);
max-width: 100%;
@media all and (max-width: 790px) {
// 32px is (left + right padding * 2) from .wrap in common/base/discourse.scss
max-width: calc(100vw - 32px);

View File

@ -58,34 +58,6 @@
}
}
#topic-progress-wrapper {
position: fixed;
bottom: 0;
left: 0;
margin: 0 auto;
max-width: $reply-area-max-width;
display: flex;
justify-content: flex-end;
z-index: z("timeline");
// max-width + bottom + left/right makes this element take up the whole width
// albeit as a transparent row, but we disable pointer-events to allow user to
// interact with visible elements at bottom of viewport
pointer-events: none;
> * {
// and then we reset for its children
pointer-events: auto;
}
&.docked {
position: absolute;
bottom: -70px;
}
html.rtl & {
justify-content: flex-start;
right: 0;
left: 2em;
}
}
#topic-progress-expanded {
border: 1px solid var(--primary-low);
padding: 5px;
@ -128,55 +100,6 @@
}
}
#topic-progress {
position: relative;
left: 340px;
&.hidden {
display: none;
}
background-color: var(--secondary);
color: var(--tertiary);
border: 1px solid var(--tertiary-low);
border-bottom: none;
width: 145px;
height: 34px;
/* as a big ol' click target, don't let text inside be selected */
@include unselectable;
&:hover {
cursor: pointer;
}
.nums {
position: relative;
top: 9px;
width: 100%;
text-align: center;
z-index: z("base");
}
.d-icon {
position: absolute;
right: 8px;
bottom: 9px;
z-index: z("base");
}
h4 {
display: inline;
font-size: $font-up-2;
line-height: $line-height-small;
}
.bg {
position: absolute;
top: 0;
bottom: 0;
width: 0;
max-width: 145px;
border-right: 1px solid var(--tertiary-low);
background-color: var(--tertiary-low);
transition: width 0.75s;
}
}
#topic-filter {
background-color: var(--highlight-medium);
padding: 8px;
@ -187,6 +110,10 @@
z-index: z("dropdown");
}
#topic-progress:hover {
cursor: pointer;
}
#topic-progress,
#topic-progress-expanded {
right: 0;
@ -200,10 +127,6 @@
}
@media all and (max-width: 485px) {
#topic-progress-wrapper.docked {
display: none;
}
#topic-footer-main-buttons {
max-width: 100%;
}

View File

@ -433,16 +433,16 @@ span.highlighted {
}
.posts-filtered-notice {
padding-right: 10em;
padding-right: 8.5em;
padding-bottom: unquote("max(1em, env(safe-area-inset-bottom))");
flex-wrap: wrap;
justify-content: flex-start;
padding-bottom: unquote("max(0.75em, env(safe-area-inset-bottom))");
margin: 1em -9px;
z-index: 101;
.filtered-replies-show-all {
position: absolute;
right: 2em;
right: 1em;
}
.filtered-replies-viewing {

View File

@ -31,26 +31,6 @@
top: -100%; // above parent container + some extra space
}
#topic-progress-wrapper {
position: fixed;
right: 10px; // match 10px padding on .wrap
bottom: 0;
z-index: z("timeline");
&:not(.docked) {
margin-bottom: env(safe-area-inset-bottom);
}
html.rtl & {
/**
* This should be the other way around, but it has to be "wrong" here
* because our RTL CSS is generated using the `rtlit` gem which flips
* left to right and right to left, so this will be corrected when it
* goes through rtlit.
*/
left: unset;
right: 1em;
}
}
#topic-progress-expanded {
border: 1px solid var(--primary-low);
padding: 5px;
@ -92,49 +72,6 @@
}
}
#topic-progress {
position: relative;
&.hidden {
display: none;
}
background-color: var(--secondary);
color: var(--tertiary);
border: 1px solid var(--tertiary-low);
width: 145px;
height: 42px;
/* as a big ol' click target, don't let text inside be selected */
@include unselectable;
.nums {
position: relative;
top: 12px;
width: 100%;
text-align: center;
z-index: z("base");
}
h4 {
display: inline;
font-size: $font-up-2;
line-height: $line-height-small;
}
.d-icon {
position: absolute;
right: 8px;
bottom: 9px;
z-index: z("base");
}
.bg {
position: absolute;
top: 0;
bottom: 0;
width: 0;
border-right: 1px solid var(--tertiary-low);
background-color: var(--tertiary-low);
transition: width 0.75s;
}
}
.topic-error {
padding: 18px;
width: 90%;
@ -151,14 +88,6 @@
}
}
#topic-progress-wrapper.docked {
position: absolute;
}
.topic-post:last-of-type {
padding-bottom: 40px;
}
sup sup,
sub sup,
sup sub,