2026-02-26 09:24:26 +08:00
|
|
|
|
import { customElement, noShadowDOM } from "solid-element";
|
|
|
|
|
|
import { createSignal, onCleanup } from "solid-js";
|
|
|
|
|
|
import { render } from "solid-js/web";
|
|
|
|
|
|
import { Article } from "./Article";
|
2026-02-26 15:40:58 +08:00
|
|
|
|
import { resolvePath } from "../utils/path";
|
2026-02-26 09:24:26 +08:00
|
|
|
|
|
|
|
|
|
|
customElement("md-link", {}, (props, { element }) => {
|
|
|
|
|
|
noShadowDOM();
|
|
|
|
|
|
|
|
|
|
|
|
const [showArticle, setShowArticle] = createSignal(false);
|
2026-02-26 17:44:58 +08:00
|
|
|
|
const [expanded, setExpanded] = createSignal(false);
|
2026-02-26 09:24:26 +08:00
|
|
|
|
let articleContainer: HTMLDivElement | undefined;
|
|
|
|
|
|
let disposeArticle: (() => void) | null = null;
|
2026-02-26 17:44:58 +08:00
|
|
|
|
let articleElement: HTMLElement | undefined;
|
2026-02-26 09:24:26 +08:00
|
|
|
|
|
2026-02-26 09:53:30 +08:00
|
|
|
|
// 从 element 的 textContent 获取链接目标(支持 path#section 语法)
|
2026-02-26 09:37:28 +08:00
|
|
|
|
const rawLinkSrc = element?.textContent?.trim() || "";
|
2026-02-26 15:40:58 +08:00
|
|
|
|
|
2026-02-26 09:53:30 +08:00
|
|
|
|
// 解析 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;
|
|
|
|
|
|
|
2026-02-26 09:47:21 +08:00
|
|
|
|
// 隐藏原始文本内容
|
|
|
|
|
|
if (element) {
|
|
|
|
|
|
element.textContent = "";
|
|
|
|
|
|
}
|
2026-02-26 09:37:28 +08:00
|
|
|
|
|
|
|
|
|
|
// 从父节点 article 的 data-src 获取当前 markdown 文件完整路径
|
|
|
|
|
|
const articleEl = element?.closest('article[data-src]');
|
|
|
|
|
|
const articlePath = articleEl?.getAttribute('data-src') || '';
|
|
|
|
|
|
|
2026-02-26 15:40:58 +08:00
|
|
|
|
// 解析相对路径
|
2026-02-26 09:53:30 +08:00
|
|
|
|
const linkSrc = resolvePath(articlePath, path);
|
2026-02-26 09:24:26 +08:00
|
|
|
|
|
|
|
|
|
|
// 查找包含此元素的 p 标签
|
|
|
|
|
|
const parentP = element?.closest("p");
|
|
|
|
|
|
|
|
|
|
|
|
const toggleArticle = () => {
|
|
|
|
|
|
if (!parentP) return;
|
|
|
|
|
|
|
|
|
|
|
|
if (showArticle()) {
|
2026-02-26 17:44:58 +08:00
|
|
|
|
// 隐藏文章 - 先折叠再移除
|
|
|
|
|
|
setExpanded(false);
|
2026-02-26 09:24:26 +08:00
|
|
|
|
if (articleContainer) {
|
2026-02-26 17:44:58 +08:00
|
|
|
|
articleContainer.style.height = '0';
|
|
|
|
|
|
articleContainer.style.opacity = '0';
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
if (disposeArticle) {
|
|
|
|
|
|
disposeArticle();
|
|
|
|
|
|
disposeArticle = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
articleContainer?.remove();
|
|
|
|
|
|
articleContainer = undefined;
|
|
|
|
|
|
articleElement = undefined;
|
|
|
|
|
|
}, 300);
|
2026-02-26 09:24:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
setShowArticle(false);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 显示文章
|
|
|
|
|
|
articleContainer = document.createElement("div");
|
|
|
|
|
|
articleContainer.classList.add("md-link-article");
|
2026-02-26 09:39:35 +08:00
|
|
|
|
articleContainer.classList.add("ml-4", "border-l-2", "border-gray-200", "pl-4");
|
2026-02-26 17:44:58 +08:00
|
|
|
|
articleContainer.style.height = '0';
|
|
|
|
|
|
articleContainer.style.opacity = '0';
|
|
|
|
|
|
articleContainer.style.overflow = 'hidden';
|
|
|
|
|
|
articleContainer.style.transition = 'height 0.3s ease, opacity 0.3s ease';
|
2026-02-26 09:24:26 +08:00
|
|
|
|
parentP.after(articleContainer);
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染 Article 组件
|
|
|
|
|
|
disposeArticle = render(() => (
|
2026-02-26 09:37:28 +08:00
|
|
|
|
<Article
|
2026-02-26 09:24:26 +08:00
|
|
|
|
src={linkSrc}
|
2026-02-26 09:53:30 +08:00
|
|
|
|
section={section}
|
2026-02-26 17:44:58 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}}
|
2026-02-26 09:24:26 +08:00
|
|
|
|
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"
|
|
|
|
|
|
>
|
2026-02-26 09:59:45 +08:00
|
|
|
|
{section || rawLinkSrc}
|
2026-02-26 09:24:26 +08:00
|
|
|
|
</a>
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|