refactor: introduce RevealManager and Article DOM context

Decouple the reveal logic from the `Article` component by introducing
a `RevealManager` component and an `ArticleDomCtx`. This allows the
reveal logic to reactively manage DOM injections and classes without
polluting the `Article` component's props or lifecycle.

Additionally, implements a robust `cleanupInjections` mechanism to
ensure that injected buttons and styling artifacts are properly removed
during navigation, role changes, or disconnections.
This commit is contained in:
2026-07-07 22:19:04 +08:00
parent 1ef2560faa
commit aa326f38a0
5 changed files with 117 additions and 19 deletions
+27 -10
View File
@@ -1,10 +1,14 @@
import {
Component,
ParentProps,
createContext,
useContext,
createEffect,
onCleanup,
Show,
createResource,
createMemo,
createSignal,
} from "solid-js";
import { parseMarkdown } from "../markdown";
import { extractSection } from "../data-loader";
@@ -21,7 +25,19 @@ export interface ArticleProps {
onError?: (error: Error) => void;
class?: string; // 额外的 class 用于样式控制
scrollToHash?: boolean; // 是否自动滚动到 hash
onDom?: (dom: HTMLDivElement) => void;
}
// ---------------------------------------------------------------------------
// Article DOM context lets child components react to the content container
// ---------------------------------------------------------------------------
const ArticleDomCtx = createContext<() => HTMLDivElement | undefined>();
/** Access the article's content DOM element from a child component. */
export function useArticleDom(): () => HTMLDivElement | undefined {
const ctx = useContext(ArticleDomCtx);
if (!ctx) throw new Error("useArticleDom must be used inside an <Article>");
return ctx;
}
async function fetchArticleContent(params: {
@@ -59,13 +75,13 @@ function scrollToHash(hash: string) {
* Article 组件
* 用于将特定 src 位置的 md 文件显示为 markdown 文章
*/
export const Article: Component<ArticleProps> = (props) => {
export const Article: Component<ArticleProps & ParentProps> = (props) => {
const location = useLocation();
const [content, { refetch }] = createResource(
() => ({ src: props.src, section: props.section }),
fetchArticleContent,
);
let innerDiv!: HTMLDivElement;
const [contentDom, setContentDom] = createSignal<HTMLDivElement>();
// 解析 iconPath,默认为 "./assets",空字符串表示禁用
const iconPrefix = createMemo(() => {
@@ -82,8 +98,6 @@ export const Article: Component<ArticleProps> = (props) => {
// 内容渲染后检查 hash 并滚动
scrollToHash(location.hash);
props.onDom?.(innerDiv);
}
});
@@ -103,11 +117,14 @@ export const Article: Component<ArticleProps> = (props) => {
<div class="text-red-500">{content.error?.message}</div>
</Show>
<Show when={!content.loading && !content.error && content()}>
<div
class="relative"
ref={innerDiv}
innerHTML={parseMarkdown(content()!, iconPrefix())}
/>
<ArticleDomCtx.Provider value={contentDom}>
<div
class="relative"
ref={setContentDom}
innerHTML={parseMarkdown(content()!, iconPrefix())}
/>
{props.children}
</ArticleDomCtx.Provider>
</Show>
</article>
);