mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-05-18 05:05:36 +02:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user