新しい、または更新されたトピックの通知を固定する

こんにちは、

もし「新しいまたは更新されたトピック x 件を表示」というバーがヘッダーの下に固定され、トピック一覧をスクロールしている際にすぐに見えるようにすれば、役立つかもしれません。バーをクリックすると、新しいまたは更新されたトピックを読み込むためにトップへジャンプします。

この :arrow_down: のように固定部分はうまく機能しています。

#list-area .show-more.has-topics {
  position: sticky;
  top: var(--header-offset);
}

もう一つの部分(JS)は、トップへジャンプまたはスクロールするクリック機能になるはずですが、どのように実装するか、あるいは最適な方法がわかりません。

テンプレート discovery/topics.hbsdiscovery/categories.hbs の該当セクションを見つけました。<a href<a href="/"> に変更すれば動作するかもしれませんが(確信はありません、試していません)、これではロゴをクリックしたときと同じように毎回読み込まれてしまいます。

ご協力ありがとうございます :slightly_smiling_face:

その通りです。URL を追加すると、イベントを傍受するロジックがないため、ナビゲーションが発生してしまいます。

このリンクは Ember のアクションを呼び出しています。Discourse のすべてのアクションは拡張可能です。つまり、ある種の「カスタマイズフック」として機能します。では、アクションをどのように変更すればよいのでしょうか?まず、そのアクションが何をしているかを確認してみましょう。

Github またはローカルで、アクション名を検索します。アクションは常に JS ファイルで定義され、Handlebars で参照されます。定義を確認したいので、検索範囲を JS ファイルに限定します。

4 つのファイルが見つかります。どれを確認すべきでしょうか?discovery/topics.hbs テンプレートのカスタマイズを行いたいのであれば、discovery/topics.js を確認する必要があります。

このコードは役に立ちますか?いいえ。しかし、アクションがどこで定義されているかはわかりました。では、修正してみましょう。

discovery/topics.js は Ember クラスです。プラグイン API の modifyClass というメソッドを使って Ember クラスを変更できます(:winking_face_with_tongue:)。

https://github.com/discourse/discourse/blob/main/app/assets/javascripts/discourse/app/lib/plugin-api.js#L166-L195

変更したいクラスはすでにわかっています。それは discovery/topics です。このクラスがどのような種類のものであるかを知る必要があります。ファイルディレクトリを確認してみましょう。

discourse/app/controllers/discovery/topics.js

これはコントローラーです。

また、そのクラス内の showInserted アクションを変更したいこともわかっています。したがって、まずは以下のように記述します。

api.modifyClass("controller:discovery/topics", {
  pluginId: 'sticky-new-topics-banner',
  actions: {
    showInserted() {
      // ここに処理を追加
    }
  }
});

その後、「新しいトピック」バナーがクリックされたときにウィンドウをスクロールさせるための任意のコードを追加できます。私は以下のような実装を選びました。

const listControls = document.querySelector(".list-controls");
listControls.scrollIntoView();

scrollIntoView() については、こちらで詳しく読むことができます:scrollIntoView()

そして、そのコードをプラグイン API のメソッドに以下のように追加します。

api.modifyClass("controller:discovery/topics", {
  pluginId: 'sticky-new-topics-banner',
  actions: {
    showInserted() {
+     const listControls = document.querySelector(".list-controls");
+     listControls.scrollIntoView();
    }
  }
});

これで完了でしょうか?いいえ。これでは Discourse が壊れてしまいます。なぜなら、アクションを完全に上書きしているからです。リンクをクリックすると list-controls 要素にスクロールされますが、新しいトピックは読み込まれません。なぜでしょうか?コアのコードがもはや適用されないからです。つまり、以下のコードです。

では、これをどう修正すればよいのでしょうか?以下の単純な 1 行で解決できます。

this._super(...arguments);

コアのコードに単に追加したいだけなら、コアのコードをコピーする必要はありません。処理を行い、最後にその 1 行を追加するだけです。これにより、コアのコードが確実に適用されます。

api.modifyClass("controller:discovery/topics", {
  pluginId: 'sticky-new-topics-banner',
  actions: {
    showInserted() {
      const listControls = document.querySelector(".list-controls");
      listControls.scrollIntoView();
+
+     this._super(...arguments);
    }
  }
});

これをテストすると、ほとんどすべてがうまく機能しますが、一点だけ問題があります。ヘッダーが list-controls と重なってしまうことです。なぜでしょうか?ヘッダーが sticky に設定されているためです。

JS でこれを解決する方法はいくつかあります(高さを計算する、オフセットを取得する、Discourse のヘルパーをインポートするなど)。ここではそれらには深入りしません。

最も簡単な方法は CSS の scroll-margin-top を使用することです。詳しくは こちら をご覧ください。

したがって、以下を追加します。

.list-controls {
  scroll-margin-top: calc(var(--header-offset) * 2);
}

日本語で説明すると:リンクがクリックされたとき、list-controls のトップにスクロールしますが、ヘッダーの高さの 2 倍分だけ上余白を設けることで、ヘッダーとの重なりを防ぎ、若干の余白を持たせます。

では、これらすべてをまとめましょう。

共通ヘッダータブ

<script type="text/discourse-plugin" version="0.8">
api.modifyClass("controller:discovery/topics", {
  pluginId: "sticky-new-topics-banner",
  actions: {
    showInserted() {
      const listControls = document.querySelector(".list-controls");
      listControls.scrollIntoView();
      this._super(...arguments);
    }
  }
});
</script>

共通 CSS

#list-area {
  // モバイルではレイアウトが異なります
  .alert-info,
  .show-more.has-topics {
    position: sticky;
    // Safari ではプレフィックスなしだと問題が発生することがあります
    position: -webkit-sticky;
    top: var(--header-offset);
    // バナーはコンテンツの上に表示する必要があります
    z-index: z("header");
  }
}

.list-controls {
  scroll-margin-top: calc(var(--header-offset) * 2);
}

@Johani さん、ありがとうございます!とても助かりましたし、ついに多くのことが理解できたと思います。あなたの詳しい回答が本当に気に入っています。そこから多くのことを学べますし、仕組みもよくわかります。もちろん、完璧に機能しています。もう一度、ありがとうございます!:slightly_smiling_face: