Skip to content

Commit 209ca4a

Browse files
committed
fix: 修复左侧 toc 显示问题
1 parent 904d762 commit 209ca4a

2 files changed

Lines changed: 101 additions & 54 deletions

File tree

src/components/widgets/card-toc/CardTOC.astro

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ const { class: className, style } = Astro.props;
2828
class={"group " + (className || "")}
2929
style={style}
3030
>
31-
<div class="toc-scroll-container">
32-
<div class="toc-content" id="card-toc-content"></div>
31+
<div data-card-toc-root>
32+
<div class="toc-scroll-container">
33+
<div class="toc-content" data-card-toc-content></div>
34+
</div>
3335
</div>
3436
</WidgetLayout>
3537

@@ -51,76 +53,86 @@ const { class: className, style } = Astro.props;
5153
<script>
5254
import { TOCManager } from "@/utils/tocManager";
5355

54-
if (typeof window.CardTOC === "undefined") {
55-
window.CardTOC = {
56-
manager: null,
57-
};
58-
}
56+
const cardTocManagers = new WeakMap();
5957

60-
async function initCardTOC() {
61-
const tocContent = document.getElementById("card-toc-content");
62-
if (!tocContent) {
58+
/**
59+
* 为单个 card-toc 实例初始化目录。
60+
* 目录绑定当前组件自己的内容容器,避免左侧多断点并存时命中隐藏实例。
61+
*/
62+
function initCardTOC(root) {
63+
if (!(root instanceof HTMLElement)) {
6364
return;
6465
}
6566

66-
try {
67-
// Ensure window.CardTOC is initialized
68-
if (!window.CardTOC) {
69-
window.CardTOC = { manager: null };
70-
}
67+
const tocContent = root.querySelector("[data-card-toc-content]");
68+
if (!(tocContent instanceof HTMLElement)) {
69+
return;
70+
}
7171

72-
// Cleanup existing manager if present
73-
const existingManager = window.CardTOC.manager;
74-
if (existingManager && existingManager.cleanup) {
72+
try {
73+
const existingManager = cardTocManagers.get(root);
74+
if (
75+
existingManager &&
76+
typeof existingManager.cleanup === "function"
77+
) {
7578
existingManager.cleanup();
7679
}
7780

78-
// Create new manager
7981
const manager = new TOCManager({
80-
contentId: "card-toc-content",
81-
indicatorId: "card-active-indicator",
82+
contentElement: tocContent,
8283
maxLevel: 3,
8384
scrollOffset: 80,
8485
});
8586

86-
window.CardTOC.manager = manager;
87+
cardTocManagers.set(root, manager);
8788
manager.init();
8889
} catch (error) {
89-
console.error("Failed to load TOCManager for CardTOC:", error);
90+
if (typeof reportError === "function" && error instanceof Error) {
91+
reportError(error);
92+
}
9093
}
9194
}
9295

96+
/**
97+
* 页面内可能同时存在多个 card-toc 实例。
98+
* 统一批量初始化,确保可见和隐藏实例都各自绑定正确容器。
99+
*/
100+
function initAllCardTOCs() {
101+
document.querySelectorAll("[data-card-toc-root]").forEach((root) => {
102+
initCardTOC(root);
103+
});
104+
}
105+
93106
if (document.readyState === "loading") {
94-
document.addEventListener("DOMContentLoaded", initCardTOC);
107+
document.addEventListener("DOMContentLoaded", initAllCardTOCs);
95108
} else {
96-
initCardTOC();
109+
initAllCardTOCs();
97110
}
98111

99112
document.addEventListener("swup:contentReplaced", () => {
100-
setTimeout(initCardTOC, 100);
113+
setTimeout(initAllCardTOCs, 100);
101114
});
102115

103116
document.addEventListener("astro:page-load", () => {
104-
setTimeout(initCardTOC, 100);
117+
setTimeout(initAllCardTOCs, 100);
105118
});
106119

107120
document.addEventListener("astro:after-swap", () => {
108-
setTimeout(initCardTOC, 100);
121+
setTimeout(initAllCardTOCs, 100);
109122
});
110123

111124
window.addEventListener("popstate", () => {
112-
setTimeout(initCardTOC, 200);
125+
setTimeout(initAllCardTOCs, 200);
113126
});
114127

115128
window.addEventListener("hashchange", () => {
116129
if (!window.tocInternalNavigation) {
117-
setTimeout(initCardTOC, 100);
130+
setTimeout(initAllCardTOCs, 100);
118131
}
119132
window.tocInternalNavigation = false;
120133
});
121134

122135
document.addEventListener("password:decrypted", () => {
123-
setTimeout(initCardTOC, 200);
136+
setTimeout(initAllCardTOCs, 200);
124137
});
125138
</script>
126-

src/utils/tocManager.ts

Lines changed: 57 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import I18nKey from "../i18n/i18nKey";
88
import { i18n } from "../i18n/translation";
99

1010
export interface TOCConfig {
11-
contentId: string;
12-
indicatorId: string;
11+
contentId?: string;
12+
contentElement?: HTMLElement;
1313
maxLevel?: number;
1414
scrollOffset?: number;
1515
}
@@ -20,17 +20,43 @@ export class TOCManager {
2020
private minDepth = 10;
2121
private maxLevel: number;
2222
private scrollTimeout: number | null = null;
23-
private contentId: string;
24-
private indicatorId: string;
23+
private contentId: string | null;
24+
private contentElement: HTMLElement | null;
2525
private scrollOffset: number;
2626

2727
constructor(config: TOCConfig) {
28-
this.contentId = config.contentId;
29-
this.indicatorId = config.indicatorId;
28+
this.contentId = config.contentId ?? null;
29+
this.contentElement = config.contentElement ?? null;
3030
this.maxLevel = config.maxLevel || 3;
3131
this.scrollOffset = config.scrollOffset || 80;
3232
}
3333

34+
/**
35+
* 获取当前实例绑定的 TOC 容器。
36+
* 优先使用实例级元素引用,避免多实例时因为重复 id 命中错误节点。
37+
*/
38+
private getTOCContentElement(): HTMLElement | null {
39+
if (this.contentElement) {
40+
return this.contentElement;
41+
}
42+
43+
if (!this.contentId) {
44+
return null;
45+
}
46+
47+
return document.getElementById(this.contentId);
48+
}
49+
50+
/**
51+
* 获取当前实例内部的活动指示器。
52+
* 指示器限定在当前内容容器内查询,避免多个目录组件互相污染。
53+
*/
54+
private getIndicatorElement(): HTMLElement | null {
55+
return this.getTOCContentElement()?.querySelector(
56+
"[data-card-toc-indicator]",
57+
) as HTMLElement | null;
58+
}
59+
3460
private getContentContainer(): Element | null {
3561
return (
3662
document.querySelector(".custom-md") ||
@@ -177,19 +203,20 @@ export class TOCManager {
177203
`;
178204
});
179205

180-
tocHTML += `<div id="${this.indicatorId}" style="opacity: 0;" class="toc-active-indicator"></div>`;
206+
tocHTML +=
207+
'<div data-card-toc-indicator style="opacity: 0;" class="toc-active-indicator"></div>';
181208

182209
return tocHTML;
183210
}
184211

185212
public updateTOCContent(): void {
186-
const tocContent = document.getElementById(this.contentId);
187-
if (!tocContent) {return;}
213+
const tocContent = this.getTOCContentElement();
214+
if (!tocContent) {
215+
return;
216+
}
188217

189218
tocContent.innerHTML = this.generateTOCHTML();
190-
this.tocItems = Array.from(
191-
document.querySelectorAll(`#${this.contentId} a`),
192-
);
219+
this.tocItems = Array.from(tocContent.querySelectorAll("a"));
193220
}
194221

195222
private getVisibleHeadingIds(): string[] {
@@ -233,7 +260,9 @@ export class TOCManager {
233260
}
234261

235262
public updateActiveState(): void {
236-
if (!this.tocItems || this.tocItems.length === 0) {return;}
263+
if (!this.tocItems || this.tocItems.length === 0) {
264+
return;
265+
}
237266

238267
this.tocItems.forEach((item) => {
239268
item.classList.remove("visible");
@@ -254,16 +283,20 @@ export class TOCManager {
254283
}
255284

256285
private updateActiveIndicator(activeItems: HTMLElement[]): void {
257-
const indicator = document.getElementById(this.indicatorId);
258-
if (!indicator || !this.tocItems.length) {return;}
286+
const indicator = this.getIndicatorElement();
287+
if (!indicator || !this.tocItems.length) {
288+
return;
289+
}
259290

260291
if (activeItems.length === 0) {
261292
indicator.style.opacity = "0";
262293
return;
263294
}
264295

265-
const tocContent = document.getElementById(this.contentId);
266-
if (!tocContent) {return;}
296+
const tocContent = this.getTOCContentElement();
297+
if (!tocContent) {
298+
return;
299+
}
267300

268301
const contentRect = tocContent.getBoundingClientRect();
269302
const firstActive = activeItems[0];
@@ -285,12 +318,14 @@ export class TOCManager {
285318
}
286319

287320
private scrollToActiveItem(activeItem: HTMLElement): void {
288-
if (!activeItem) {return;}
321+
if (!activeItem) {
322+
return;
323+
}
289324

290-
const tocContainer = document
291-
.querySelector(`#${this.contentId}`)
292-
?.closest(".toc-scroll-container");
293-
if (!tocContainer) {return;}
325+
const tocContainer = activeItem.closest(".toc-scroll-container");
326+
if (!tocContainer) {
327+
return;
328+
}
294329

295330
if (this.scrollTimeout) {
296331
clearTimeout(this.scrollTimeout);

0 commit comments

Comments
 (0)