如果您访问 /tags/category-slug/tag-name 并点击 <kbd>新建主题</kbd> 按钮,作曲器将预设为该标签,具体说明如下:
这太棒了。但现在我(以及至少另一位用户)希望能够在访问 /c/cat-slug/cat-id 时,通过默认标签来设置此行为。似乎主题组件应该能够针对该按钮,对其进行修改,或者隐藏它并添加一个新按钮(就在那里有一个插件出口,我刚才看到过,但现在找不到了)。
有人能给个提示吗?
如果您访问 /tags/category-slug/tag-name 并点击 <kbd>新建主题</kbd> 按钮,作曲器将预设为该标签,具体说明如下:
这太棒了。但现在我(以及至少另一位用户)希望能够在访问 /c/cat-slug/cat-id 时,通过默认标签来设置此行为。似乎主题组件应该能够针对该按钮,对其进行修改,或者隐藏它并添加一个新按钮(就在那里有一个插件出口,我刚才看到过,但现在找不到了)。
有人能给个提示吗?
它是应该仅针对某一特定类别工作,还是需要支持为多个类别设置“默认标签”,且每个类别的标签各不相同?
我设想可以设置一个默认标签用于几个类别。我大概能做到这一点,但不知道在哪里或如何修改“创建主题”按钮,使其包含默认标签。
TL;DR 直接跳到可运行的代码 这里
当你查看包含 + 新建主题 的页面时,可以通过检查器查看其 HTML。
如果你这样做,会注意到它有一个 id。
HTML 元素上的 id 应该是唯一的,也就是说……同一视图中的两个元素不能共享同一个 HTML id 属性。因此,这足以让我们开始。
如果我在 Github 上搜索 "create-topic",会看到以下内容…
Repository search results · GitHub
注意左侧的过滤器。
我知道我想追踪该按钮的 HTML,因为我在尝试追踪它触发的操作。由于 Handlebars 模板常用于此类场景,所以我选择 Handlebars;然后我看到了这个。
Repository search results · GitHub
那里只有一个结果,所以我们很幸运。如果有更多结果,你可以采取一些措施进一步缩小列表范围,但这超出了本主题的范围。
那么,让我们检查该文件。
然后你会看到按钮的操作设置如下:
action=action
嗯……这不太有帮助……那现在怎么办?
当你看到 action=action 时,意味着该操作是从父模板传递给组件的。
让我们试着找出哪些模板包含该组件。因此,我们前往 Github 并按模板中的使用方式搜索组件名称。对于这个例子,我们会使用类似 "{{create-topic-button" 的内容。
注意,我只添加了 {{COMPONENT_NAME 而跳过了其余部分。我们不知道传递给它的其他参数,所以我们需要一个通用搜索。
结果如下:
Repository search results · GitHub
我们得到两个结果……其中一个在 styleguide 插件中,所以我们直接忽略它。另一个在核心代码中。那么,让我们看看它是什么样子的:
{{create-topic-button
canCreateTopic=canCreateTopic
action=(action "clickCreateTopicButton")
disabled=createTopicButtonDisabled
label=createTopicLabel
btnClass=createTopicClass
canCreateTopicOnTag=canCreateTopicOnTag
}}
啊……我们越来越接近了。现在你看到按钮的操作是:
action=(action "clickCreateTopicButton")
现在我们需要弄清楚该操作做了什么。因此,我们搜索该操作的名称。然后过滤到 .js 文件,因为我们现在想在该组件的 js 文件中查看该操作的定义。
Repository search results · GitHub
同样,我们只得到一个结果,所以让我们看看它。
看起来该操作执行以下两种操作之一:如果类别是只读的且用户还没有草稿,则显示警报。否则,它调用 createTopic() 方法。
我们对后者感兴趣,所以让我们看看它。
如果你在该文件中搜索 createTopic()(使用文件内搜索,而不是 Github)……你会注意到只有一个引用。怎么回事?这个组件是如何调用一个未定义的方法的?
好吧,答案在文件的更上方。
这是什么意思?
我不想在这里花太多时间,但 Ember 使用类。可以把类想象成可重用的代码包。上面高亮的那一行意味着:
获取 Ember 的 Component 包,将 FilterModeMixin 包添加到其中,并允许我添加更多方法或覆盖一些现有方法,从而为我的应用程序创建一个新的 Ember 组件。
那么,现在让我们回到我们要追踪的操作。
clickCreateTopicButton() {
if (this.categoryReadOnlyBanner && !this.hasDraft) {
bootbox.alert(this.categoryReadOnlyBanner);
} else {
this.createTopic();
}
},
它调用了 this.createTopic()。这不是默认的 Ember 组件方法。它是 Discourse 的自定义方法,所以它必须来自 FilterModeMixin。什么是 FilterModeMixin?嗯……它在文件顶部定义。
import FilterModeMixin from "discourse/mixins/filter-mode";
所以,我想我们得去那里看看。
暂停一下,在该文件中进行 createTopic() 的 inline 搜索。我是认真的。停止阅读并去做。我会等……别作弊……我盯着你呢:eyes。
好的。你搜索了,但没有结果。现在怎么办?
我上面描述的只是传递内容的一种方法。如果你找不到想要的东西,退一步,尝试不同的方法。
那么,让我们回顾一下……我们现在在哪里?在我们卡住之前,我们正在查看 d-navigation 组件的 JS 文件。让我们看看它的模板。
同样,我们使用 "{{COMPONENT_NAME" 进行搜索。
Repository search results · GitHub
这给了我们四个结果……
这重要吗?也许吧。在这个案例中重要吗?不重要。我们只是想弄清楚 createTopic() 来自哪里或它是什么。所以,我们就用第一个结果吧。
看看这个……
createTopic=(route-action "createTopic")
太好了……更多的术语……因为大家都喜欢这个
说真的,让我们谈谈路由操作。它们是什么?嗯,它们是路由……操作?即在路由上定义的操作。为什么它们很好?因为 Discourse 中的路由可以嵌套。
这样看:
- route-1
- route-1-1
- route-1-2
- route-1-3
如果我有一个共享组件,需要在路由 111、112 和 113 上工作,并且需要不同的参数,如果我只在所有路由中使用同一个组件并传递相同的操作,然后在每个路由中根据需要修改它,难道不是更容易吗?
这就是路由操作的作用。
好的,让我们回到问题。我们当时正在查看 navigation/default 组件中的:
createTopic=(route-action "createTopic")
现在我们需要找出路由是什么,以检查该路由操作做了什么。
你想修改 /c/cat-slug/cat-id 页面上的新建主题按钮的行为。所以,让我们访问其中一个页面。例如:http://localhost:4200/c/meta/6
这是什么路由?除非你对 Discourse 非常熟悉,否则你可能无法判断。那么,现在怎么办?
这时,你浏览器的 Ember 扩展就派上用场了。
在这里安装(如果你还没有的话)。我会等。
(链接是一个 Github 仓库,但描述中包含不同浏览器的扩展链接)
好的,现在你安装了它,再次访问该页面 /c/cat-slug/cat-id 并查看扩展页面。
加载完成后,点击“Routes”,然后切换“Current Route only”
啊哈……看看这个。我们现在知道自己在哪个路由上了。我们在 discovery.category 上。
但这还不是全部……它是:
application > discovery > discovery.category
记住,路由是嵌套的。那么,现在怎么办?
我通常从最顶层开始。在这个例子中,就是 application 路由。找到该路由的文件,并搜索看操作是否在那里定义。
结果发现不是……所以我们向下移动到嵌套树中的 discovery 路由。
在那里搜索……然后…… bingo!
好的,现在我们知道:
createTopic=(route-action "createTopic")
指的是什么。所以让我们看看那个操作。
createTopic() {
if (this.get("currentUser.has_topic_draft")) {
this.openTopicDraft();
} else {
this.openComposer(this.controllerFor("discovery/topics"));
}
},
看起来它执行以下两种操作之一:如果用户有草稿,则打开它。否则,它调用带有参数的 openComposer()。接下来是什么?嗯,你现在应该已经知道答案了。我们需要找出 openComposer() 来自哪里或它做了什么。
所以我们在文件中搜索 openComposer() 并且……当然我们没有得到任何结果。该路由中没有名为 openComposer() 的方法。
接下来是什么?还记得关于 Ember 类的那部分吗?让我们试试那个。
我们在路由文件的顶部有:
这意味着该路由继承了 DiscourseRoute “包”中的所有方法,以及 OpenComposer “包”中定义的方法。
openComposer 更可能是我们想要的,所以让我们看看它。不过在此之前……我们需要看看 openComposer 在该文件中是如何定义的。
import OpenComposer from "discourse/mixins/open-composer";
看看 URL。它不是 Ember 组件。它不是路由;也不是模型。它是一个 mixin。什么是 mixin?非常非常简短的回答……它是一组可重用的函数。
你在 mixin 中定义这些:
add(number) {
return number + 1
}
substract(number) {
return number - 1
}
然后将 mixin 添加到你的 Ember 组件中,然后你可以这样做:
// 起始值为 1
myMethod () {
this.add(value) // 返回 2
this.substract(value) // 返回 0
}
那么,这与我们要做的有什么关系?
嗯,这里的 open-composer:
import OpenComposer from "discourse/mixins/open-composer";
是一个 mixin。该 mixin 中的一个方法是 OpenComposer()。
如果你对此感到困惑也没关系。它们有相同的名字——除了一个以大写字母开头,这表明它是一个类。
它们的意思不同。
要理解这一点,你需要知道(在这种情况下),你给导入模块起的名字并不重要,只要它们作为“default”导出即可。
解释这一点超出了本主题的范围。你只需要知道:
这里的 OpenComposer
和这里的 openComposer()
不是同一个东西。
好的……让我们回顾一下。
新建主题按钮 HTML id < 新建主题按钮操作 < d-navigation 组件操作 < discovery 路由操作 < OpenComposer mixin < openComposer() 方法
所以……这就是你在该路由上点击 + 新建主题 按钮时最终被调用的方法。
openComposer(controller) {
let categoryId = controller.get("category.id");
if (
categoryId &&
controller.category.isUncategorizedCategory &&
!this.siteSettings.allow_uncategorized_topics
) {
categoryId = null;
}
this.controllerFor("composer").open({
prioritizedCategoryId: categoryId,
topicCategoryId: categoryId,
action: Composer.CREATE_TOPIC,
draftKey: controller.get("model.draft_key") || Composer.NEW_TOPIC_KEY,
draftSequence: controller.get("model.draft_sequence") || 0,
});
},
那么,让我们回到你的问题。
我们已经确定了如何在 /c/cat-slug/cat-id 上找出该按钮的操作,但这似乎与你访问 /tags/category-slug/tag-name 时发生的情况不同,而这正是你想做的。
那么下一步是什么?让我们看看该路由如何处理 createTopic() 操作。
嗯……你会注意到它处理该操作的方式不同。
对于 /c/cat-slug/cat-id,它看起来像这样:
createTopic() {
if (this.get("currentUser.has_topic_draft")) {
this.openTopicDraft();
} else {
this.openComposer(this.controllerFor("discovery/topics"));
}
},
对于 /tags/category-slug/tag-name,它看起来像这样:
createTopic() {
if (this.get("currentUser.has_topic_draft")) {
this.openTopicDraft();
} else {
const controller = this.controllerFor("tag.show");
const composerController = this.controllerFor("composer");
composerController
.open({
categoryId: controller.get("category.id"),
action: Composer.CREATE_TOPIC,
draftKey: Composer.NEW_TOPIC_KEY
})
.then(() => {
// 预填充标签输入框
if (composerController.canEditTags && controller.get("model.id")) {
const composerModel = this.controllerFor("composer").get("model");
composerModel.set(
"tags",
[
controller.get("model.id"),
...makeArray(controller.additionalTags)
].filter(Boolean)
);
}
});
}
}
这个差异基本上就是你在这里询问的内容。
所以,你所要做的就是……修改 discovery 路由中的 createTopic() 操作,使其像 tag-show 路由中的那样工作。那么该怎么做呢?
还记得我们说过 Ember 使用类吗?是的,我们得再次回到那里。
插件 API 允许你通过此方法修改 Ember 类。
那么,我们要修改的是什么?discovery 路由……因为……记住,当你在像 /c/cat-slug/cat-id 这样的页面上时,createTopic() 就在那里定义。
我们从这里开始:
api.modifyClass("route:discovery", {
pluginId: "prefill-composer-tags",
actions: {
createTopic() {
console.log("fires");
}
}
});
这做了什么?它破坏了 + 新建主题 按钮;但它告诉我们我们走在正确的方向上。如果你尝试添加上面的代码片段,你会注意到点击按钮不再打开编辑器,而只是向控制台打印一条消息。这是一件好事,因为我们已经瞄准了正确的类和正确的操作——route:discovery 和 createTopic()。
那么,接下来是什么?嗯,还记得 /tags/category-slug/tag-name 上的按钮正好做了我们想要的事情吗?所以,让我们从该路由复制代码——并添加所需的导入。
const Composer = require("discourse/models/composer");
const { makeArray } = require("discourse-common/lib/helpers");
api.modifyClass("route:discovery", {
pluginId: "prefill-composer-tags",
actions: {
createTopic() {
if (this.get("currentUser.has_topic_draft")) {
this.openTopicDraft();
} else {
const controller = this.controllerFor("tag.show");
const composerController = this.controllerFor("composer");
composerController
.open({
categoryId: controller.get("category.id"),
action: Composer.CREATE_TOPIC,
draftKey: Composer.NEW_TOPIC_KEY
})
.then(() => {
// 预填充标签输入框
if (composerController.canEditTags && controller.get("model.id")) {
const composerModel = this.controllerFor("composer").get("model");
composerModel.set(
"tags",
[
controller.get("model.id"),
...makeArray(controller.additionalTags)
].filter(Boolean)
);
}
});
}
}
}
});
这会起作用吗?不会,但我们已经接近一步了。为什么不起作用?因为编辑器打开时添加的标签未定义。为什么?因为它们是从 tag.show 控制器加载的——这不是我们想要的。让我们修改代码,使其与我们所在的路由一起工作。
不过,在此之前,我们需要某种索引来存储我们想要的默认标签。让我们使用一个新对象,如下所示:
// category-slug: [DEFAULT_TAGS_ARRAY]
const defaultTagIndex = {
// 单个单词的 slug
meta: ["a", "b", "c"],
core: ["g", "h"],
// 带连字符的 slug
["general-chat"]: ["d", "e", "f"]
};
这基本上意味着:如果编辑器在 meta 类别页面上打开,则添加标签 “a,b,c”。
如果编辑器在 core 类别页面上打开,则添加标签 “g,h”,以此类推。
现在有了这个,我们可以修改操作,使其看起来像这样。
const Composer = require("discourse/models/composer");
const { makeArray } = require("discourse-common/lib/helpers");
// category-slug: [DEFAULT_TAGS_ARRAY]
const defaultTagIndex = {
// 单个单词的 slug
meta: ["a", "b", "c"],
core: ["g", "h"],
// 带连字符的 slug
["general-chat"]: ["d", "e", "f"]
};
api.modifyClass("route:discovery", {
pluginId: "prefill-composer-tags",
actions: {
createTopic() {
try {
const hasDraft = this.currentUser?.has_topic_draft;
if (hasDraft) {
this._super(...arguments);
return;
} else {
const controller = this.controllerFor("discovery/topics");
const composerController = this.controllerFor("composer");
const categoryId = controller.category?.id;
const categorySlug = controller.category?.slug;
if (!categoryId) {
this._super(...arguments);
return;
}
composerController
.open({
categoryId: categoryId,
action: Composer.CREATE_TOPIC,
draftKey: Composer.NEW_TOPIC_KEY
})
.then(() => {
// 预填充标签输入框
if (composerController.canEditTags && categoryId) {
const composerModel = composerController.model;
composerModel.set(
"tags",
makeArray(defaultTagIndex[categorySlug]).filter(Boolean)
);
}
});
}
} catch {
this._super(...arguments);
return;
}
}
}
});
注意:
我将所有内容包裹在 try…catch 块中。如果代码失败,我们执行 this._super(...arguments)。
如果你熟悉 Ember,你就会知道 this._super(...arguments) 的作用。如果不熟悉,这里有一个简单的解释。我们正在覆盖 createTopic(),所以如果覆盖因错误而失败——也许核心代码已更新——则回退到核心中定义的方法,见 此处。
如果用户有新主题草稿,我们只需回退到 this._super(...arguments) 并让核心代码按原样处理。
这就足够了。你现在需要添加的只是通过主题设置创建默认标签索引的方法。