116 lines
3.6 KiB
TypeScript
116 lines
3.6 KiB
TypeScript
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>
|
||
);
|
||
});
|