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
|
<Article
|
||||||
class="prose text-black prose-sm max-w-full flex-1"
|
class="prose text-black prose-sm max-w-full flex-1"
|
||||||
src={currentPath()}
|
src={currentPath()}
|
||||||
onDom={(dom) =>
|
onDom={(dom) => addRevealedClasses(dom, location.pathname)}
|
||||||
addRevealedClasses(dom, stream.revealedPaths[currentPath()])
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -61,9 +61,6 @@ const SidebarContent: Component<SidebarContentProps> = (props) => {
|
||||||
if (node.children?.some((child) => !isHeadingHidden(child))) return false;
|
if (node.children?.some((child) => !isHeadingHidden(child))) return false;
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
createEffect(() => {
|
|
||||||
console.log(currentFileHeadings(), revealedHeadings());
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col h-full">
|
<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.
|
* Walk every element in `root` and tag it as revealed or concealed.
|
||||||
*
|
*
|
||||||
* Headings (h1–h6) update a current-heading tracker. When a higher-level
|
* Two-pass approach:
|
||||||
* heading is encountered (e.g. h2 after h1), all sub-headings at deeper
|
* 1. Collect all headings with their ancestor chain.
|
||||||
* levels are cleared. Each non-heading element receives a `revealed` or
|
* 2. Cascade revealed status upward — if a heading is revealed, all its
|
||||||
* `concealed` CSS class depending on whether any of its current heading
|
* parent headings are marked revealed as well.
|
||||||
* ancestors is present in the `revealed` set.
|
* 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(
|
export function addRevealedClasses(root: HTMLDivElement, path: string) {
|
||||||
root: HTMLDivElement,
|
if (!state.connected) return;
|
||||||
revealed: Set<string>,
|
if (state.myRole === "gm") return;
|
||||||
) {
|
|
||||||
|
const normalized = normalizePath(path);
|
||||||
|
const revealed = state.revealedPaths[normalized];
|
||||||
if (!revealed) return;
|
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 HEADING_TAGS = new Set(["H1", "H2", "H3", "H4", "H5", "H6"]);
|
||||||
|
|
||||||
const walk = (el: Element) => {
|
// ---- Pass 1: collect heading hierarchy ----
|
||||||
const tag = el.tagName.toUpperCase();
|
interface HeadingEntry {
|
||||||
|
el: Element;
|
||||||
if (HEADING_TAGS.has(tag)) {
|
level: number;
|
||||||
const level = Number(tag.charAt(1)); // 1-6
|
text: string;
|
||||||
const text = el.textContent?.trim() ?? "";
|
/** Texts of all ancestor headings (shallowest first) */
|
||||||
|
parents: string[];
|
||||||
// Update current heading at this level
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Start walk from root's children (skip the container itself)
|
|
||||||
for (const child of root.children) {
|
|
||||||
walk(child);
|
|
||||||
}
|
}
|
||||||
|
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));
|
||||||
|
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;
|
||||||
|
for (let l = level + 1; l <= 6; l++) delete cur[l];
|
||||||
|
}
|
||||||
|
for (const child of el.children) collect(child);
|
||||||
|
};
|
||||||
|
for (const child of root.children) collect(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 / */
|
/** Normalize a path for lookup: strip .md, leading ./ or / */
|
||||||
|
|
|
||||||
|
|
@ -138,3 +138,14 @@ icon.big .icon-label-stroke {
|
||||||
.col-5 {
|
.col-5 {
|
||||||
@apply lg:flex-5;
|
@apply lg:flex-5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* concealed / revealed */
|
||||||
|
|
||||||
|
.concealed {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.revealed,
|
||||||
|
.concealed .revealed {
|
||||||
|
opacity: unset;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue