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:
+27
-10
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user