refactor: improve heading revelation logic
Rewrite `addRevealedClasses` to use a two-pass approach that correctly cascades revealed status from child headings to parent headings. This ensures that if a sub-heading is revealed, its parent headings are also treated as revealed, maintaining proper visibility hierarchy.
This commit is contained in:
parent
ced9a4f8f2
commit
574d17f201
|
|
@ -126,9 +126,7 @@ const App: Component = () => {
|
|||
<Article
|
||||
class="prose text-black prose-sm max-w-full flex-1"
|
||||
src={currentPath()}
|
||||
onDom={(dom) =>
|
||||
addRevealedClasses(dom, stream.revealedPaths[currentPath()])
|
||||
}
|
||||
onDom={(dom) => addRevealedClasses(dom, location.pathname)}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -61,9 +61,6 @@ const SidebarContent: Component<SidebarContentProps> = (props) => {
|
|||
if (node.children?.some((child) => !isHeadingHidden(child))) return false;
|
||||
return true;
|
||||
};
|
||||
createEffect(() => {
|
||||
console.log(currentFileHeadings(), revealedHeadings());
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="flex flex-col h-full">
|
||||
|
|
|
|||
|
|
@ -630,49 +630,83 @@ export function isPathRevealed(path: string, section?: string): boolean {
|
|||
/**
|
||||
* Walk every element in `root` and tag it as revealed or concealed.
|
||||
*
|
||||
* Headings (h1–h6) update a current-heading tracker. When a higher-level
|
||||
* heading is encountered (e.g. h2 after h1), all sub-headings at deeper
|
||||
* levels are cleared. Each non-heading element receives a `revealed` or
|
||||
* `concealed` CSS class depending on whether any of its current heading
|
||||
* ancestors is present in the `revealed` set.
|
||||
* Two-pass approach:
|
||||
* 1. Collect all headings with their ancestor chain.
|
||||
* 2. Cascade revealed status upward — if a heading is revealed, all its
|
||||
* parent headings are marked revealed as well.
|
||||
* 3. Walk the tree again, applying `revealed` or `concealed` classes to
|
||||
* every element (headings included) based on whether any current
|
||||
* heading ancestor is in the cascaded revealed set.
|
||||
*/
|
||||
export function addRevealedClasses(
|
||||
root: HTMLDivElement,
|
||||
revealed: Set<string>,
|
||||
) {
|
||||
export function addRevealedClasses(root: HTMLDivElement, path: string) {
|
||||
if (!state.connected) return;
|
||||
if (state.myRole === "gm") return;
|
||||
|
||||
const normalized = normalizePath(path);
|
||||
const revealed = state.revealedPaths[normalized];
|
||||
if (!revealed) return;
|
||||
// Current heading text for each level (h1..h6, index 1..6)
|
||||
const cur: Record<number, string> = {};
|
||||
|
||||
const HEADING_TAGS = new Set(["H1", "H2", "H3", "H4", "H5", "H6"]);
|
||||
|
||||
const walk = (el: Element) => {
|
||||
// ---- Pass 1: collect heading hierarchy ----
|
||||
interface HeadingEntry {
|
||||
el: Element;
|
||||
level: number;
|
||||
text: string;
|
||||
/** Texts of all ancestor headings (shallowest first) */
|
||||
parents: string[];
|
||||
}
|
||||
const headings: HeadingEntry[] = [];
|
||||
const cur: Record<number, string> = {};
|
||||
|
||||
const collect = (el: Element) => {
|
||||
const tag = el.tagName.toUpperCase();
|
||||
|
||||
if (HEADING_TAGS.has(tag)) {
|
||||
const level = Number(tag.charAt(1)); // 1-6
|
||||
const text = el.textContent?.trim() ?? "";
|
||||
|
||||
// Update current heading at this level
|
||||
const level = Number(tag.charAt(1));
|
||||
const text = el.id || el.textContent?.trim() || "";
|
||||
const parents: string[] = [];
|
||||
for (let l = 1; l < level; l++) {
|
||||
if (cur[l]) parents.push(cur[l]);
|
||||
}
|
||||
headings.push({ el, level, text, parents });
|
||||
cur[level] = text;
|
||||
// Clear all deeper heading levels
|
||||
for (let l = level + 1; l <= 6; l++) delete cur[l];
|
||||
} else {
|
||||
// Non-heading: check if any current heading ancestor is revealed
|
||||
const isRevealed = Object.values(cur).some((h) => revealed.has(h));
|
||||
el.classList.add(isRevealed ? "revealed" : "concealed");
|
||||
}
|
||||
|
||||
// Recurse into children
|
||||
for (const child of el.children) {
|
||||
walk(child);
|
||||
}
|
||||
for (const child of el.children) collect(child);
|
||||
};
|
||||
for (const child of root.children) collect(child);
|
||||
|
||||
// Start walk from root's children (skip the container itself)
|
||||
for (const child of root.children) {
|
||||
walk(child);
|
||||
// ---- Cascade: if a heading is revealed, its parents are too ----
|
||||
const revealedSet = new Set<string>();
|
||||
for (const h of headings) {
|
||||
if (revealed.has(h.text) || revealedSet.has(h.text)) {
|
||||
revealedSet.add(h.text);
|
||||
for (const p of h.parents) revealedSet.add(p);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Pass 2: apply classes using the cascaded set ----
|
||||
const stack: string[] = [];
|
||||
const apply = (el: Element) => {
|
||||
let pushed = false;
|
||||
for (const child of el.children) {
|
||||
const tag = child.tagName.toUpperCase();
|
||||
if (HEADING_TAGS.has(tag)) {
|
||||
if (pushed) stack.pop();
|
||||
const text = child.id || child.textContent?.trim() || "";
|
||||
stack.push(text);
|
||||
pushed = true;
|
||||
}
|
||||
|
||||
const current = stack.length > 0 ? stack[stack.length - 1] : null;
|
||||
const isRevealed = current !== null && revealedSet.has(current);
|
||||
child.classList.add(isRevealed ? "revealed" : "concealed");
|
||||
|
||||
apply(child);
|
||||
}
|
||||
if (pushed) stack.pop();
|
||||
};
|
||||
apply(root);
|
||||
}
|
||||
|
||||
/** Normalize a path for lookup: strip .md, leading ./ or / */
|
||||
|
|
|
|||
|
|
@ -138,3 +138,14 @@ icon.big .icon-label-stroke {
|
|||
.col-5 {
|
||||
@apply lg:flex-5;
|
||||
}
|
||||
|
||||
/* concealed / revealed */
|
||||
|
||||
.concealed {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.revealed,
|
||||
.concealed .revealed {
|
||||
opacity: unset;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue