move implementation to a glimmer component

This commit is contained in:
Sam Saffron 2024-11-13 16:30:50 +11:00
parent 89749d267a
commit 097409ba20
No known key found for this signature in database
GPG Key ID: B9606168D2FFD9F5
6 changed files with 134 additions and 150 deletions

View File

@ -1,16 +1,5 @@
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.allowList(["div.markdown-tabs", "section", "section[data-selected]"]);
helper.registerPlugin((md) => {
const ruler = md.block.bbcode.ruler;
@ -21,10 +10,7 @@ function setupTabs(helper) {
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"]];
token.attrs = [["class", "markdown-tabs"]];
},
after(state) {
@ -44,61 +30,25 @@ function setupTabs(helper) {
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],
];
const section = state.push("tab_panel_open", "section", 1);
if (tab.selected) {
tabToken.attrs.push(["data-selected", ""]);
section.attrs = [["data-selected", ""]];
}
const linkToken = state.push("tab_link_open", "a", 1);
linkToken.attrs = [["data-tab-id", tabId]];
// Add tab name
state.push("tab_header_open", "h4", 1);
const textToken = state.push("text", "", 0);
textToken.content = tab.name;
textToken.content = tab.name.toString().trim();
state.push("tab_header_close", "h4", -1);
state.push("tab_link_close", "a", -1);
state.push("tab_button_close", "div", -1);
});
}
tab.content.forEach((token) => state.tokens.push(token));
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.push("tab_panel_close", "section", -1);
});
}
state.env.tabContent = null;
state.push("panels_close", "div", -1);
state.push("tab_close", "div", -1);
},
});

View File

@ -0,0 +1,62 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { fn } from "@ember/helper";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import { htmlSafe } from "@ember/template";
class Tab {
@tracked selected;
constructor(title, content, selected) {
this.title = title;
this.content = content;
this.selected = selected;
}
}
export default class MarkdownTabs extends Component {
@tracked tabs;
constructor() {
super(...arguments);
this.tabs = this.args.tabs.map((tab) => {
return new Tab(tab.title, tab.content, tab.selected);
});
}
headerClass(tab) {
return tab.selected
? "markdown-tabs__header selected"
: "markdown-tabs__header";
}
panelClass(tab) {
return tab.selected
? "markdown-tabs__panel selected"
: "markdown-tabs__panel";
}
@action
selectTab(tab, event) {
this.tabs.forEach((t) => (t.selected = t === tab));
event.preventDefault();
}
<template>
<div class="markdown-tabs__headers" role="tablist">
{{#each this.tabs as |tab|}}
<div class={{this.headerClass tab}}>
<a href {{on "click" (fn this.selectTab tab)}}>{{tab.title}}</a>
</div>
{{/each}}
</div>
<div class="markdown-tabs__panels">
{{#each this.tabs as |tab|}}
<div class={{this.panelClass tab}} role="tabpanel">
{{htmlSafe tab.content}}
</div>
{{/each}}
</div>
</template>
}

View File

@ -0,0 +1,38 @@
import MarkdownTabs from "discourse/components/markdown-tabs";
import { withPluginApi } from "discourse/lib/plugin-api";
function initializeMarkdownTabs(api) {
api.decorateCookedElement(
(elem, helper) => {
for (const tabsElement of [...elem.querySelectorAll(".markdown-tabs")]) {
const tabs = [...tabsElement.querySelectorAll("section")].map(
(section) => {
return {
title: section.querySelector("h4").textContent.trim(),
content: section.innerHTML
.replace(section.querySelector("h4").outerHTML, "")
.trim(),
selected: section.hasAttribute("data-selected"),
};
}
);
while (tabsElement.firstChild) {
tabsElement.removeChild(tabsElement.firstChild);
}
helper.renderGlimmer(tabsElement, <template>
<MarkdownTabs @tabs={{tabs}} />
</template>);
}
},
{ id: "discourse-tabs", onlyStream: false }
);
}
export default {
name: "discourse-tabs",
initialize() {
withPluginApi("0.8.7", initializeMarkdownTabs);
},
};

View File

@ -1,50 +0,0 @@
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);
},
};

View File

@ -1,17 +1,17 @@
.markdown-tabs {
margin: 1em 0;
.markdown-tabs-wrapper {
&__headers {
display: flex;
gap: 0.2em;
border-bottom: 2px solid var(--primary-low);
padding: 0 0.2em;
}
.markdown-tab {
&__header {
margin-bottom: -2px;
&[data-selected] {
&.selected {
a {
color: var(--tertiary);
font-weight: 500;
@ -36,15 +36,15 @@
}
}
.markdown-tab-panels {
&__panels {
padding: 0 0;
}
.markdown-tab-panel {
display: none;
&__panel {
display: none;
&[data-selected] {
display: block;
}
&.selected {
display: block;
}
}
}

View File

@ -2849,24 +2849,16 @@ HTML
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">
<section>
<h4>
First Tab</h4>
<p>Tab 1 <strong>content</strong></p>
</div>
<div class="markdown-tab-panel" data-tab-id="tab-1" data-selected="">
</section>
<section data-selected="">
<h4>
Second Tab</h4>
<p>Tab 2 content</p>
</div>
</div>
</section>
</div>
HTML
expect(html).to match_html(expected)
@ -2887,24 +2879,16 @@ HTML
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="">
<section>
<h4>
First Tab</h4>
<p>Tab 1 <strong>content</strong></p>
</div>
<div class="markdown-tab-panel" data-tab-id="tab-1">
</section>
<section>
<h4>
Second Tab</h4>
<p>Tab 2 content</p>
</div>
</div>
</section>
</div>
HTML
expect(html).to match_html(expected)