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

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

diff --git a/app/assets/javascripts/discourse/app/components/topic-navigation.js b/app/assets/javascripts/discourse/app/components/topic-navigation.js
index 77d1dee..aad6ca4 100644
--- a/app/assets/javascripts/discourse/app/components/topic-navigation.js
+++ b/app/assets/javascripts/discourse/app/components/topic-navigation.js
@@ -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,
diff --git a/app/assets/javascripts/discourse/app/components/topic-progress.js b/app/assets/javascripts/discourse/app/components/topic-progress.js
index a6fea0e..22661d0 100644
--- a/app/assets/javascripts/discourse/app/components/topic-progress.js
+++ b/app/assets/javascripts/discourse/app/components/topic-progress.js
@@ -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;
+  _startObserver() {
+    if ("IntersectionObserver" in window) {
+      this._topicBottomObserver = this._setupObserver();
+      this._topicBottomObserver.observe(
+        document.querySelector("#topic-bottom")
+      );
     }
+  },
 
-    const totalWidth = this._totalWidth;
-    const progressWidth = (this._streamPercentage || 0) * totalWidth;
-    const borderSize = progressWidth === totalWidth ? "0px" : "1px";
+  _setupObserver() {
+    const composerH =
+      document.querySelector("#reply-control")?.clientHeight || 0;
 
-    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);
-    }
+    return new IntersectionObserver(this._intersectionHandler, {
+      threshold: 0.1,
+      rootMargin: `0px 0px -${composerH}px 0px`,
+    });
   },
 
-  _dock() {
-    const $wrapper = $(this.element);
-    if (!$wrapper || $wrapper.length === 0) {
-      return;
+  _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();
     }
+  },
 
-    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);
+  @bind
+  _intersectionHandler(entries) {
+    if (entries[0].isIntersecting === true) {
+      this.set("docked", true);
     } else {
-      $wrapper.css("bottom", isDocked ? bottom : "");
+      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`);
+        }
+      }
     }
-
-    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,
-    });
   },
 
   click(e) {
-    if ($(e.target).closest("#topic-progress").length) {
+    if (e.target.closest("#topic-progress")) {
       this.send("toggleExpansion");
     }
   },

[... diff too long, it was truncated ...]

GitHub sha: 095421a1e129e870a5e7dfd0e78116dac05a7a31

This commit appears in #14698 which was approved by jjaffeux. It was merged by pmusaraj.

This commit has been mentioned on Discourse Meta. There might be relevant details there: