スティッキーユーザーカードのコンテンツテーマ

より洗練された user-card-contents テーマを作成し、いくつかのカスタムユーザーフィールドを追加しました。このカードを「新しいトピックを作成」コンポーネントと同様に、閉じるボタン付きのスティッキー(固定)表示にしたいと考えています。

要素を閉じる呼び出しを防止するために、user-card-contents.js を上書きする必要があると考えていますが、合っていますでしょうか?これをテーマ内にパッケージ化することは可能でしょうか?

よろしくお願いいたします。

こんにちは、Pete さん。Meta へようこそ :wave:

答えは短く「はい」です。変更がフロントエンドのみに影響する場合は、テーマやコンポーネント内で実装可能です。

「sticky」の意味について詳しく教えていただけますか?コンテンツに合わせてスクロールしながらも同じ位置に留まるようにしたいという意味であれば、それは CSS の変更になります。また、この変更はデスクトップとモバイルの両方に適用される予定ですか?

具体的にどの呼び出しを指しているのか詳しく説明していただけますか?(クリック時やスクロール時など)

閉じるボタンのマークアップは、ユーザーカードの Handlebars テンプレートに追加する必要があります。ボタンがクリックされた際のアクションを処理するロジックは、コンポーネントの .js ファイルに追加する必要があります。

完全なオーバーライドは必ずしも必要ないかもしれません。クラス内の特定のメソッドをオーバーライドするために使用できるフックがいくつかあります。もう少し具体的に何をしたいか教えていただければ、それについて詳しく共有できます。

ジョー、歓迎してくれてありがとう!

私が目指しているのは、card-contents-base.js のミックスインイベントハンドラー clickOutsideEventName がデスクトップでカードを閉じないようにすることです。その代わり、ユーザーがボタンをクリックして閉じるように強制したいと考えています。モバイルではおそらく異なるアプローチが必要になるでしょう。

このハンドルバーテンプレートは動作するようになりました。次は .js の仕組みを解明します :slight_smile:

TL;DR: これが求めているものだと思います

テーマ JS

api.modifyClass("component:user-card-contents", {
  didInsertElement() {
    this._super(...arguments);
    $("html").off(this.clickOutsideEventName);
  },
  actions: {
    closeCard() {
      this._close();
    }
  }
});

そして、テンプレートのどこかに以下を追加します。

{{d-button
  class="btn-flat"
  action=(action "closeCard")
  icon="times"
}}

詳細な説明

おそらく、すでにテーマの作成に取り組んでいるので、これらの多くは既にご存知でしょう。しかし、より広い層の読者のために、少し詳しく説明してみます。

最初にやるべきことは、ローカルまたは GitHub で検索することです。GitHub では、こちら のような結果が得られます。ほとんどの場合、検索語には複数の結果が存在するため、より具体的にするか、手動で結果をスキャンして目的に近いものを見つける必要があります。

さて、こうしてこのファイルにたどり着きました。

discourse/app/assets/javascripts/discourse/app/mixins/card-contents-base.js at e5dc843185feb268c277bb0ee4db9666d6452783 · discourse/discourse · GitHub

このファイルは Mixin です。なぜこれに触れるのかというと、Mixin はさまざまな場所で共有され得ることを認識しておく必要があるからです。この場合、ユーザーカードとグループカードの両方で使用されています。つまり、ここでの変更は両方に影響します。

ファイル内で検索すると、clickOutsideEventName が最初にここで定義されていることがわかります。

discourse/app/assets/javascripts/discourse/app/mixins/card-contents-base.js at e5dc843185feb268c277bb0ee4db9666d6452783 · discourse/discourse · GitHub

その後、プロパティとしてここで渡されます。

discourse/app/assets/javascripts/discourse/app/mixins/card-contents-base.js at e5dc843185feb268c277bb0ee4db9666d6452783 · discourse/discourse · GitHub

そして最終的にここで消費されます。

discourse/app/assets/javascripts/discourse/app/mixins/card-contents-base.js at e5dc843185feb268c277bb0ee4db9666d6452783 · discourse/discourse · GitHub

さて、これらすべてが何を意味するのかというと、すべてのコードが追加されている場所を見ると、didInsertElement の内部にあることに気づくでしょう。

discourse/app/assets/javascripts/discourse/app/mixins/card-contents-base.js at e5dc843185feb268c277bb0ee4db9666d6452783 · discourse/discourse · GitHub

Ember ガイド は以下のように述べています。

Ember は、didInsertElement() が呼び出される時点で、以下のことが保証されます。

  1. コンポーネントの要素が作成され、DOM に挿入されていること。
  2. コンポーネントの要素が、コンポーネントの this.element プロパティを通じてアクセス可能であること。

なぜこれが必要なのかというと、ユーザーカードとグループカードでは異なる mousedown ハンドラが必要だからです。少し遡ってみると、clickOutsideEventName で要素の ID が使用されていることに気づくでしょう。

これは前述のようにプロパティとして渡されます。

さて、これがあなたの作業とどう関係するかを見ていきましょう。

カードが外部をクリックしたときに閉じられないようにしようとしています。その方法を探してみましょう。思い出してください、clickOutsideEventName が最終的にここで消費されることを話し合いました。

要約すると、カードが挿入されたとき(最初のページ表示時)、HTML 要素に mousedown ハンドラが追加されます。その後、mousedown イベントのターゲットをチェックします。ターゲットがカード内にある場合は処理を中断し、カードの外にある場合は this._close() を呼び出してカードを閉じます。

_close() はここで定義されています。

discourse/app/assets/javascripts/discourse/app/mixins/card-contents-base.js at e5dc843185feb268c277bb0ee4db9666d6452783 · discourse/discourse · GitHub

これが、閉じるボタンを追加する際に呼び出す必要があるメソッドです。ただし、後でこれに戻ります。

さて、カードの外をクリックしても閉じないようにするには、この mousedown ハンドラを削除する必要があります。どのようにすればよいでしょうか?didInsertElement() を修正する必要があります。その方法は以下の通りです。

Discourse テーマは プラグイン API にアクセスでき、これには利用可能な多くのメソッドが含まれています。最も一般的に使用されるものについての詳細は こちら にあります。

思い出してください、作業しているファイルは Mixin です。Mixin は Ember クラスです。そのため、使用するメソッドは modifyClass です。

https://github.com/discourse/discourse/blob/master/app/assets/javascripts/discourse/app/lib/plugin-api.js#L121-L124

modifyClass を使用すると、クラスメソッドを追加、修正、または完全にオーバーライドできます。今回はメソッドの修正に焦点を当てます。

didInsertElement() を修正したいので、以下のようなことができます。

api.modifyClass('mixin:card-contents-base', {
  didInsertElement() {
    console.log("foo");
  }
});

試してみましょう…

うまくいきませんでした。

なぜでしょうか?実は、modifyClass メソッドは現在、Mixin の修正をサポートしていないようです(私がテストした限りでは)。なぜそうなのかを調べ、修正可能か確認するようにします。とりあえず、あなたがやりたいことに戻りましょう。

Mixin を修正できないので、行き詰まったのでしょうか?いいえ。もう少し深く掘り下げましょう。

前述のように、その Mixin はユーザー user-card-contentsgroup-card-contents の両方の Ember コンポーネント で使用されています(Mixin はコードの再利用を設計するため)。

では、user-card-contents コンポーネントを見てみましょう。

discourse/app/assets/javascripts/discourse/app/components/user-card-contents.js at e5dc843185feb268c277bb0ee4db9666d6452783 · discourse/discourse · GitHub

注意深く読むと、まず上記で議論した Mixin を ここで インポートし、その後、新しい Ember コンポーネントを作成して Mixin を渡していることがわかります。

これは何を意味するのでしょうか?user-card-contentsdidInsertElement() は実際には Mixin から継承されていることを意味します。group-card-contents についても同様です。

では、私たちはどこにいるのでしょうか?いいニュースと悪いニュースがあります。いいニュースは、user-card-contents に変更を加えても group-card-contents に影響を与えないようにできることです。悪いニュースは、変更を両方に適用したい場合、いくつかのコードを重複させる必要があることです。

modifyClass に戻り、user-card-contents で再度試してみましょう。以下のようなコードです。

api.modifyClass('component:user-card-contents', {
  didInsertElement() {
    console.log("foo");
  }
});

そして結果を見てみましょう…

できました :tada:

変更が登録され、コンソールで確認できますが、まだ完全ではありません。

さて、didInsertElement() を修正できるようになったので、あなたがやりたいことに戻りましょう。思い出してください、mousedown ハンドラはここで didInsertElement で定義されています。

discourse/app/assets/javascripts/discourse/app/mixins/card-contents-base.js at e5dc843185feb268c277bb0ee4db9666d6452783 · discourse/discourse · GitHub

では、どうすればよいでしょうか?実は、以下のように簡単です。

$("html").off(clickOutsideEventName)

これでうまくいくでしょうか?いいえ。以下のようにすると

api.modifyClass('component:user-card-contents', {
  didInsertElement() {
    $("html").off(clickOutsideEventName)
  }
});

何かが壊れてしまいます。なぜでしょうか?それは、メソッドの修正ではなく、そのコンポーネントの核心となる didInsertElement() メソッドを完全にオーバーライドしているからです。つまり、コアのコードは一切適用されません。

これをどう修正するか?実は、Ember には this._super(...arguments) という仕組みがあります。

これは何をするのでしょうか?クラスメソッドに既に存在するコードに加えて、コードを追加または先頭に挿入することを可能にします。例えば、以下のようにすると

api.modifyClass('component:user-card-contents', {
  didInsertElement() {
    // 追加したいコード
    $("html").off(clickOutsideEventName);
    // コアからのコード
    this._super(...arguments);
  }
});

追加したいコードは、以下のすべてよりも先に実行されます。

discourse/app/assets/javascripts/discourse/app/mixins/card-contents-base.js at e5dc843185feb268c277bb0ee4db9666d6452783 · discourse/discourse · GitHub

ただし、以下のようにすると…

api.modifyClass('component:user-card-contents', {
  didInsertElement() {
    // コアからのコード
    this._super(...arguments);
    // 追加したいコード
    $("html").off(clickOutsideEventName);
  }
});

あなたのコードは、以下のすべての実行後に実行されます。

discourse/app/assets/javascripts/discourse/app/mixins/card-contents-base.js at e5dc843185feb268c277bb0ee4db9666d6452783 · discourse/discourse · GitHub

これは、コアの変更に対してテーマを堅牢に保つ素晴らしい方法です。コアのコードがまず実行され、その後であなたのコードが実行されます。では、再度試してみましょう。

api.modifyClass('component:user-card-contents', {
  didInsertElement() {
    // コアからのコード
    this._super(...arguments);
    // 追加したいコード
    $("html").off(clickOutsideEventName);
  }
});

そして…

なぜこのようなことが起こるのでしょうか?異なるコードコンテキストが原因です。Ember コンポーネントファイルでは、clickOutsideEventName が消費される時点で既に定義されています。あなたのテーマは異なるファイルにあるため、clickOutsideEventName はそこで定義されていません。

これをどう修正するか?これを覚えていますか?

clickOutsideEventName はコンポーネントのプロパティなので、this.clickOutsideEventName を使用すればうまくいくはずです。試してみましょう。

api.modifyClass('component:user-card-contents', {
  didInsertElement() {
    // コアからのコード
    this._super(...arguments);
    // 追加したいコード
    $("html").off(this.clickOutsideEventName);
  },
});

確かに動作しました :tada:

ユーザーカードはエラーなく開き、外部をクリックしても何も起こらなくなりました。

グループカードでも同じことができます。

api.modifyClass('component:group-card-contents', {
  didInsertElement() {
    // コアからのコード
    this._super(...arguments);
    // 追加したいコード
    $("html").off(this.clickOutsideEventName);
  },
});

残っているのは、閉じるボタンを前述の _close() メソッドに接続することです。これには 3 つのステップがあります。

  1. closeCard アクションを追加する(好きな名前にできます)
  2. ユーザーカードテンプレートにボタンを追加する
  3. ボタンがクリックされたときにそのアクションを呼び出す

簡単にするために、ユーザーカードに焦点を当てます。したがって、以下を追加します(既に議論した内容も含めて)。

api.modifyClass("component:user-card-contents", {
  didInsertElement() {
    this._super(...arguments);
    $("html").off(this.clickOutsideEventName);
  },
  // 新しい部分
  actions: {
    closeCard() {
      this._close();
    }
  }
});

これは、closeCard カスタムアクションがトリガーされるたびに、コアの _close() メソッドを呼び出すだけです。

次に、ユーザーカードテンプレートにボタンを追加します。以下のようなコードです。

{{d-button
  class="btn-flat"
  action=(action "closeCard")
  icon="times"
}}

私の簡易的な結果は以下の通りです。

もちろん、前述のようにグループカードでも同様のことができます。

グループカードとモバイルの実装は、あなたへの課題として残しておきます。これらに変更を加える場合、上記で議論した全く同じ概念を使用することになるためです。何か問題が発生した場合は、お気軽にお知らせください。

どうもありがとうございます。素晴らしいチュートリアルになりました!本当に助かりました。プラグイン API の存在は知りませんでした。

上記で最初にはっきりしなかった点として、テーマの JS 変更を script タグで囲み、プラグインの common/head_tag.html に配置する必要があります。

<script type="text/discourse-plugin" version="0.2">
</script>

単なる好奇心ですが、タグ内のバージョン番号はここで重要でしょうか?また、これらを header.html ではなく常に head_tag.html に配置するのが最善でしょうか、それともあまり関係ないのでしょうか?

ありがとうございます!

@Johani 必要であれば別のスレッドを作成することもできますが、現在はクライアント側の user-card コントローラーを特定しようとしています。このコントローラーは以下の動作を行います:

  • 最初のクリック時に user-card を読み込む
  • 2 回目のクリック時に user-summary にリダイレクトする

私の目的は、2 回目のクリックによる機能を削除することです。よろしくお願いします!