Тема фиксированного содержимого карточки пользователя

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

Этот файл представляет собой миксин. Почему я об этом упоминаю? Потому что нужно помнить, что миксины могут использоваться в самых разных местах. В данном случае он применяется как для карточек пользователей, так и для карточек групп. Следовательно, внесённые вами изменения затронут оба типа.

Если вы поищете в файле, то обнаружите, что 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 используется идентификатор элемента:

Что, как мы обсуждали выше, затем передаётся как свойство.

Теперь давайте перейдём к тому, как всё это связано с тем, что вы делаете.

Вы пытаетесь предотвратить закрытие карточек при клике пользователя вне их области. Давайте попробуем найти способ сделать это. Если вы помните, мы обсуждали, что clickOutsideEventName в конечном итоге используется здесь:

Короче говоря, это добавляет обработчик события mousedown к элементу HTML при вставке карточки (при первом просмотре страницы). Затем мы проверяем цель события mousedown. Если цель находится внутри карточки, мы прерываем действие. Если она вне карточки, мы закрываем её, вызывая this._close().

_close() определяется здесь:

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

И это то, что вам нужно будет вызывать при добавлении кнопки закрытия — но мы вернёмся к этому позже.

Теперь цель состоит в том, чтобы удалить этот обработчик mousedown, если вы хотите, чтобы клики вне карточки не закрывали её. Как это сделать? Нам нужно будет модифицировать didInsertElement(), и вот как это можно сделать.

Темы Discourse имеют доступ к API плагинов, который содержит множество методов, которые вы можете использовать. Более подробная информация о наиболее часто используемых из них доступна здесь.

Если вы помните, файл, с которым мы работаем, — это миксин. Миксин — это класс 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 в настоящее время не поддерживает изменение миксинов (насколько я проверял). Я отмечу это, чтобы разобраться, почему так происходит, и проверить, можно ли это исправить. Но пока давайте вернёмся к тому, что вы хотите сделать.

Что ж, мы не можем изменять миксины, значит, мы застряли, да? Нет. Давайте копнём чуть глубже.

Как я уже упоминал, этот миксин используется как для карточки пользователя user-card-contents, так и для карточки группы group-card-contents — компонентов Ember (снова потому, что миксины созданы для переиспользования кода).

Итак, давайте посмотрим на компонент user-card-contents здесь:

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

Если внимательно прочитать, можно заметить, что мы сначала импортируем обсуждаемый выше миксин здесь,

а затем создаём новый компонент Ember и передаём ему миксин.

Что это значит? Это означает, что didInsertElement() для user-card-contents фактически наследуется из миксина. То же самое можно сказать и о 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(), о котором мы говорили ранее, и для этого есть три шага:

  1. добавить действие closeCard (вы можете назвать его как угодно);
  2. добавить кнопку в шаблон карточки пользователя;
  3. вызывать это действие при клике на кнопку.

Я остановлюсь на карточках пользователей для простоты. Итак, мы добавляем это (вместе с тем, что мы уже обсуждали):

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

всё, что это делает, — это вызывает основной метод _close() всякий раз, когда срабатывает пользовательское действие closeCard.

Далее нам нужно добавить кнопку в шаблон карточки пользователя, что-то вроде этого:

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

Мои примерные результаты выглядят так:

И, конечно же, вы можете сделать что-то подобное для карточек групп, как я упоминал выше.

Я оставлю реализацию для карточек групп и мобильных устройств в качестве упражнения для вас, поскольку вы фактически будете использовать те же концепции, которые мы обсуждали выше, если захотите внести изменения в них, но, пожалуйста, дайте мне знать, если возникнут какие-либо проблемы.