粘性用户卡片内容主题

我创建了一个更丰富的用户卡片内容主题,并添加了一些自定义用户字段。我希望让卡片保持固定,并添加一个关闭按钮,类似于“创建新主题”组件的实现方式。

我这样理解是否正确:我需要覆盖 user-card-contents.js 文件,以阻止关闭元素的调用?我能否将这部分内容打包到主题中?

谢谢!

嘿,Pete。欢迎来到 Meta :wave:

简短的回答是:可以。如果你的更改仅影响前端,那么可以在主题或组件中完成。

能否请你详细说明一下你说的“sticky”是什么意思?如果你是指希望它随内容滚动但保持在同一位置,那么这需要通过 CSS 来实现。另外,这项更改是否打算同时应用于桌面端和移动端?

能否请你具体描述一下你指的是哪个调用?(例如点击时、滚动时等)

关闭按钮的 HTML 结构需要添加到用户卡片的 Handlebars 模板中。处理按钮点击事件的逻辑则需要添加到组件的 .js 文件中。

可能不需要完全覆盖,有一些钩子(hooks)可以用来覆盖类中的特定方法。如果你能更详细地描述一下你想实现的功能,我可以分享更多关于这方面的信息。

谢谢你的欢迎,Joe!

我的目标是在桌面端阻止 card-contents-base.js 混入中的事件处理器 clickOutsideEventName 关闭卡片。相反,我希望强制用户点击按钮来关闭它。对于移动端,我可能需要采用不同的处理方式。

我已经让这个 Handlebars 模板运行起来了,接下来要解决的是 .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 事件处理程序。如果我们回顾一下,你现在会注意到元素的 ID 被用于 clickOutsideEventName

正如我们上面讨论的,它随后被作为属性传递。

现在,让我们看看这一切如何与你正在做的事情相关联。

你试图防止用户点击卡片外部时卡片被关闭。所以,让我们尝试找出一种方法来实现这一点。如果你还记得,我们讨论过 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");
  }
});

看看会发生什么……

voalá :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();
    }
  }
});

这一切所做的就是每当触发 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>

只是出于好奇,标签中的版本号在这里重要吗?另外,是否总是最好将这些内容放在 head_tag.html 而不是 header.html 中,还是说这其实并不重要?

谢谢!

@Johani 如果您愿意,我可以为此另开一个线程,但我正在尝试追踪客户端的 user-card 控制器,该控制器:

  • 在首次点击时加载用户卡片
  • 并在第二次点击时重定向到用户摘要

我的目标是移除第二次点击的功能。谢谢!