editor: fix TOC active heading detection (#9413)

* Initial plan

* fix: compute heading offsets relative to editor root for correct TOC highlighting

Headings inside callout blocks had incorrect `offsetTop` values because
`offsetTop` is relative to the nearest positioned ancestor (the callout),
not the editor root. This caused the TOC to highlight wrong sections.

Fix: walk the `offsetParent` chain from the heading up to the editor
content element, accumulating `offsetTop` values to get the correct
absolute position within the editor.

Co-authored-by: thecodrr <7473959+thecodrr@users.noreply.github.com>

* editor: ignore callouts from toc

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thecodrr <7473959+thecodrr@users.noreply.github.com>
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
This commit is contained in:
Copilot
2026-03-02 09:34:53 +05:00
committed by GitHub
parent 9fc8e6b1e5
commit 26a9086d96

View File

@@ -33,6 +33,23 @@ const levelsMap: Record<string, number> = {
H6: 6
};
function getOffsetTopRelativeTo(
element: HTMLElement,
ancestor: HTMLElement
): number {
let top = 0;
let current: HTMLElement | null = element;
// Walk up the offsetParent chain until we reach ancestor (the editor root).
// This correctly handles elements nested inside positioned containers such as
// callout blocks, where `offsetTop` alone would only be relative to the
// nearest positioned parent instead of the editor root.
while (current && current !== ancestor) {
top += current.offsetTop;
current = current.offsetParent as HTMLElement | null;
}
return top;
}
export function getTableOfContents(content: HTMLElement) {
const tableOfContents: TOCItem[] = [];
let level = -1;
@@ -46,6 +63,9 @@ export function getTableOfContents(content: HTMLElement) {
const nodeName = heading.nodeName;
const currentHeading = levelsMap[nodeName];
const isInsideCallout = !!closestWithin(heading, ".callout", content);
if (isInsideCallout) continue;
level =
prevHeading < currentHeading
? level + 1
@@ -59,7 +79,7 @@ export function getTableOfContents(content: HTMLElement) {
level,
title,
id,
top: (heading as HTMLElement).offsetTop
top: getOffsetTopRelativeTo(heading, content)
});
}
return tableOfContents;
@@ -93,3 +113,16 @@ export function scrollIntoViewById(blockId: string, optionalStyles = "") {
);
}
}
function closestWithin(
element: Element,
selector: string,
boundary: Element
): Element | null {
let current: Element | null = element;
while (current && current !== boundary) {
if (current.matches(selector)) return current;
current = current.parentElement;
}
return null;
}