mirror of
https://github.com/discourse/discourse.git
synced 2024-11-25 09:42:07 +08:00
FEATURE: core support for tabbed interface inside markdown
This is useful for AI features and documentation. Adds support for: ``` [tabs] [tab="tab name"] content **here** [/tab] [tab="tab2 name"] other content **here** [/tab] [/tabs] ``` Optionally you can select the default tab using: ``` [tabs] [tab="tab name"] content **here** [/tab] [tab="tab2 name" selected] other content **here** [/tab] [/tabs] ```
This commit is contained in:
parent
57f4176b57
commit
89749d267a
|
@ -16,6 +16,7 @@ import * as onebox from "./onebox";
|
|||
import * as paragraph from "./paragraph";
|
||||
import * as quotes from "./quotes";
|
||||
import * as table from "./table";
|
||||
import * as tabs from "./tabs";
|
||||
import * as textPostProcess from "./text-post-process";
|
||||
import * as uploadProtocol from "./upload-protocol";
|
||||
import * as watchedWords from "./watched-words";
|
||||
|
@ -42,6 +43,7 @@ export default [
|
|||
feature("bbcode-inline", bbcodeInline),
|
||||
feature("bbcode-block", bbcodeBlock),
|
||||
feature("anchor", anchor),
|
||||
feature("tabs", tabs),
|
||||
];
|
||||
|
||||
function feature(id, { setup, priority = 0 }) {
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
const WRAP_CLASS = "markdown-tabs";
|
||||
|
||||
function setupTabs(helper) {
|
||||
helper.allowList([
|
||||
"div.markdown-tabs",
|
||||
"div.markdown-tabs-wrapper",
|
||||
"div.markdown-tab",
|
||||
"div.markdown-tab-panels",
|
||||
"div.markdown-tab-panel",
|
||||
"a[data-tab-id]",
|
||||
"div[data-tab-id]",
|
||||
"div[data-selected]",
|
||||
]);
|
||||
|
||||
helper.registerPlugin((md) => {
|
||||
const ruler = md.block.bbcode.ruler;
|
||||
|
||||
ruler.push("tabs", {
|
||||
tag: "tabs",
|
||||
before(state) {
|
||||
state.env.tabContent = [];
|
||||
|
||||
const token = state.push("tab_open", "div", 1);
|
||||
token.attrs = [["class", WRAP_CLASS]];
|
||||
|
||||
const wrapperToken = state.push("tab_wrapper_open", "div", 1);
|
||||
wrapperToken.attrs = [["class", "markdown-tabs-wrapper"]];
|
||||
},
|
||||
|
||||
after(state) {
|
||||
if (state.env.tabContent) {
|
||||
// force selection of 1 tab only
|
||||
let selected = false;
|
||||
state.env.tabContent.forEach((tab) => {
|
||||
if (tab.selected) {
|
||||
if (selected) {
|
||||
tab.selected = false;
|
||||
} else {
|
||||
selected = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!selected) {
|
||||
state.env.tabContent[0].selected = true;
|
||||
}
|
||||
|
||||
let index = 0;
|
||||
state.env.tabContent.forEach((tab) => {
|
||||
const tabId = `tab-${index}`;
|
||||
index++;
|
||||
|
||||
tab.id = tabId;
|
||||
|
||||
const tabToken = state.push("tab_button_open", "div", 1);
|
||||
tabToken.attrs = [
|
||||
["class", "markdown-tab"],
|
||||
["data-tab-id", tabId],
|
||||
];
|
||||
if (tab.selected) {
|
||||
tabToken.attrs.push(["data-selected", ""]);
|
||||
}
|
||||
|
||||
const linkToken = state.push("tab_link_open", "a", 1);
|
||||
linkToken.attrs = [["data-tab-id", tabId]];
|
||||
|
||||
// Add tab name
|
||||
const textToken = state.push("text", "", 0);
|
||||
textToken.content = tab.name;
|
||||
|
||||
state.push("tab_link_close", "a", -1);
|
||||
state.push("tab_button_close", "div", -1);
|
||||
});
|
||||
}
|
||||
|
||||
state.push("tab_wrapper_close", "div", -1);
|
||||
state.push("panels_open", "div", 1).attrs = [
|
||||
["class", "markdown-tab-panels"],
|
||||
];
|
||||
|
||||
if (state.env.tabContent) {
|
||||
state.env.tabContent.forEach((tab) => {
|
||||
const panelToken = state.push("tab_panel_open", "div", 1);
|
||||
panelToken.attrs = [
|
||||
["class", "markdown-tab-panel"],
|
||||
["data-tab-id", tab.id],
|
||||
];
|
||||
if (tab.selected) {
|
||||
panelToken.attrs.push(["data-selected", ""]);
|
||||
}
|
||||
|
||||
if (tab.content) {
|
||||
tab.content.forEach((token) => state.tokens.push(token));
|
||||
}
|
||||
|
||||
state.push("tab_panel_close", "div", -1);
|
||||
});
|
||||
}
|
||||
|
||||
state.env.tabContent = null;
|
||||
|
||||
state.push("panels_close", "div", -1);
|
||||
state.push("tab_close", "div", -1);
|
||||
},
|
||||
});
|
||||
|
||||
ruler.push("tab", {
|
||||
tag: "tab",
|
||||
replace(state, tagInfo, content) {
|
||||
const attrs = tagInfo.attrs;
|
||||
const name = attrs.name || attrs._default || "";
|
||||
const selected = attrs.selected === "" || attrs.selected === "true";
|
||||
|
||||
const tokens = [];
|
||||
state.md.block.parse(content, state.md, state.env, tokens);
|
||||
|
||||
state.env.tabContent.push({
|
||||
selected,
|
||||
name,
|
||||
content: tokens,
|
||||
startPos: state.tokens.length,
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function setup(helper) {
|
||||
setupTabs(helper);
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||
|
||||
function initializeMarkdownTabs(api) {
|
||||
api.decorateCooked(
|
||||
($elem) => {
|
||||
const tabs = $elem[0].querySelectorAll(".markdown-tabs");
|
||||
if (!tabs.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
tabs.forEach((tabContainer) => {
|
||||
const tabButtons = tabContainer.querySelectorAll(".markdown-tab");
|
||||
const panels = tabContainer.querySelectorAll(".markdown-tab-panel");
|
||||
|
||||
tabButtons.forEach((tab) => {
|
||||
tab.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (tab.hasAttribute("data-selected")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tabId = tab.getAttribute("data-tab-id");
|
||||
|
||||
// Remove selected state from all tabs and panels
|
||||
tabButtons.forEach((t) => t.removeAttribute("data-selected"));
|
||||
panels.forEach((p) => p.removeAttribute("data-selected"));
|
||||
|
||||
// Set selected state for clicked tab and its panel
|
||||
tab.setAttribute("data-selected", "");
|
||||
const panel = tabContainer.querySelector(
|
||||
`.markdown-tab-panel[data-tab-id="${tabId}"]`
|
||||
);
|
||||
if (panel) {
|
||||
panel.setAttribute("data-selected", "");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
{ id: "discourse-tabs" }
|
||||
);
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "discourse-tabs",
|
||||
initialize() {
|
||||
withPluginApi("0.8.7", initializeMarkdownTabs);
|
||||
},
|
||||
};
|
|
@ -33,6 +33,7 @@
|
|||
@import "login";
|
||||
@import "magnific-popup";
|
||||
@import "menu-panel";
|
||||
@import "markdown-tabs";
|
||||
@import "modal";
|
||||
@import "new-user";
|
||||
@import "not-found";
|
||||
|
|
50
app/assets/stylesheets/common/base/markdown-tabs.scss
Normal file
50
app/assets/stylesheets/common/base/markdown-tabs.scss
Normal file
|
@ -0,0 +1,50 @@
|
|||
.markdown-tabs {
|
||||
margin: 1em 0;
|
||||
|
||||
.markdown-tabs-wrapper {
|
||||
display: flex;
|
||||
gap: 0.2em;
|
||||
border-bottom: 2px solid var(--primary-low);
|
||||
padding: 0 0.2em;
|
||||
}
|
||||
|
||||
.markdown-tab {
|
||||
margin-bottom: -2px;
|
||||
|
||||
&[data-selected] {
|
||||
a {
|
||||
color: var(--tertiary);
|
||||
font-weight: 500;
|
||||
border-bottom: 2px solid var(--tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover:not([data-selected]) {
|
||||
a {
|
||||
color: var(--primary);
|
||||
background: var(--primary-very-low);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
padding: 0.5em 1em;
|
||||
color: var(--primary-medium);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-tab-panels {
|
||||
padding: 0 0;
|
||||
|
||||
.markdown-tab-panel {
|
||||
display: none;
|
||||
|
||||
&[data-selected] {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2832,4 +2832,82 @@ HTML
|
|||
expect(doc.to_html).to eq(html_with_thumbnail)
|
||||
end
|
||||
end
|
||||
|
||||
describe "markdown tabs" do
|
||||
it "supports overriding default tab in markdown" do
|
||||
md = <<~MD
|
||||
[tabs]
|
||||
[tab="First Tab"]
|
||||
Tab 1 **content**
|
||||
[/tab]
|
||||
[tab="Second Tab" selected]
|
||||
Tab 2 content
|
||||
[/tab]
|
||||
[/tabs]
|
||||
MD
|
||||
|
||||
html = PrettyText.cook(md)
|
||||
expected = <<~HTML
|
||||
<div class="markdown-tabs">
|
||||
<div class="markdown-tabs-wrapper">
|
||||
<div class="markdown-tab" data-tab-id="tab-0">
|
||||
<a data-tab-id="tab-0">
|
||||
First Tab</a>
|
||||
</div>
|
||||
<div class="markdown-tab" data-tab-id="tab-1" data-selected="">
|
||||
<a data-tab-id="tab-1">
|
||||
Second Tab</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="markdown-tab-panels">
|
||||
<div class="markdown-tab-panel" data-tab-id="tab-0">
|
||||
<p>Tab 1 <strong>content</strong></p>
|
||||
</div>
|
||||
<div class="markdown-tab-panel" data-tab-id="tab-1" data-selected="">
|
||||
<p>Tab 2 content</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
HTML
|
||||
expect(html).to match_html(expected)
|
||||
end
|
||||
|
||||
it "supports tabs markup" do
|
||||
md = <<~MD
|
||||
[tabs]
|
||||
[tab="First Tab"]
|
||||
Tab 1 **content**
|
||||
[/tab]
|
||||
[tab="Second Tab"]
|
||||
Tab 2 content
|
||||
[/tab]
|
||||
[/tabs]
|
||||
MD
|
||||
|
||||
html = PrettyText.cook(md)
|
||||
expected = <<~HTML
|
||||
<div class="markdown-tabs">
|
||||
<div class="markdown-tabs-wrapper">
|
||||
<div class="markdown-tab" data-tab-id="tab-0" data-selected="">
|
||||
<a data-tab-id="tab-0">
|
||||
First Tab</a>
|
||||
</div>
|
||||
<div class="markdown-tab" data-tab-id="tab-1">
|
||||
<a data-tab-id="tab-1">
|
||||
Second Tab</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="markdown-tab-panels">
|
||||
<div class="markdown-tab-panel" data-tab-id="tab-0" data-selected="">
|
||||
<p>Tab 1 <strong>content</strong></p>
|
||||
</div>
|
||||
<div class="markdown-tab-panel" data-tab-id="tab-1">
|
||||
<p>Tab 2 content</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
HTML
|
||||
expect(html).to match_html(expected)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue
Block a user