ttrpg-tools/src/components/md-link.tsx

116 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { customElement, noShadowDOM } from "solid-element";
import { createSignal, onCleanup } from "solid-js";
import { render } from "solid-js/web";
import { Article } from "./Article";
import { resolvePath } from "./utils/path";
customElement("md-link", {}, (props, { element }) => {
noShadowDOM();
const [showArticle, setShowArticle] = createSignal(false);
const [expanded, setExpanded] = createSignal(false);
let articleContainer: HTMLDivElement | undefined;
let disposeArticle: (() => void) | null = null;
let articleElement: HTMLElement | null | undefined;
// 从 element 的 textContent 获取链接目标(支持 path#section 语法)
const rawLinkSrc = element?.textContent?.trim() || "";
// 解析 section支持 path#section 语法)
const hashIndex = rawLinkSrc.indexOf('#');
const path = hashIndex >= 0 ? rawLinkSrc.slice(0, hashIndex) : rawLinkSrc;
const section = hashIndex >= 0 ? rawLinkSrc.slice(hashIndex + 1) : undefined;
// 隐藏原始文本内容
if (element) {
element.textContent = "";
}
// 从父节点 article 的 data-src 获取当前 markdown 文件完整路径
const articleEl = element?.closest('article[data-src]');
const articlePath = articleEl?.getAttribute('data-src') || '';
// 解析相对路径
const linkSrc = resolvePath(articlePath, path);
// 查找包含此元素的 p 标签
const parentP = element?.closest("p");
const toggleArticle = () => {
if (!parentP) return;
if (showArticle()) {
// 隐藏文章 - 先折叠再移除
setExpanded(false);
if (articleContainer) {
articleContainer.style.height = '0';
articleContainer.style.opacity = '0';
setTimeout(() => {
if (disposeArticle) {
disposeArticle();
disposeArticle = null;
}
articleContainer?.remove();
articleContainer = undefined;
articleElement = undefined;
}, 300);
}
setShowArticle(false);
} else {
// 显示文章
articleContainer = document.createElement("div");
articleContainer.classList.add("md-link-article");
articleContainer.classList.add("ml-4", "border-l-2", "border-gray-200", "pl-4");
articleContainer.style.height = '0';
articleContainer.style.opacity = '0';
articleContainer.style.overflow = 'hidden';
articleContainer.style.transition = 'height 0.3s ease, opacity 0.3s ease';
parentP.after(articleContainer);
// 渲染 Article 组件
disposeArticle = render(() => (
<Article
src={linkSrc}
section={section}
class="article-animate"
onLoaded={() => {
// 内容加载完成后,获取实际高度并展开
requestAnimationFrame(() => {
articleElement = articleContainer?.querySelector('article[data-src]');
if (articleElement) {
const height = articleElement.scrollHeight;
articleContainer!.style.height = `${height}px`;
articleContainer!.style.opacity = '1';
setExpanded(true);
}
});
}}
onError={(err) => console.error("Article error:", err)}
/>
), articleContainer);
setShowArticle(true);
}
};
onCleanup(() => {
if (disposeArticle) {
disposeArticle();
disposeArticle = null;
}
});
return (
<a
href="#"
onClick={(e) => {
e.preventDefault();
toggleArticle();
}}
class="text-blue-600 hover:text-blue-800 hover:underline"
>
{section || rawLinkSrc}
</a>
);
});