Skip to content

Commit 7f098a3

Browse files
committed
fix: highlight only parts
1 parent 189155c commit 7f098a3

File tree

2 files changed

+87
-93
lines changed

2 files changed

+87
-93
lines changed

src/client/ConfigNavigationClient.js

Lines changed: 55 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -58,97 +58,87 @@ const closeOtherDetails = function(keepOpenSet) {
5858
};
5959

6060
const highlightActiveOnPageLink = function(hash) {
61-
const hashToUse = hash || location.hash;
62-
if (!hashToUse) {
61+
// Use the passed hash parameter, not location.hash (which doesn't update with pushState)
62+
if (!hash) {
6363
return;
6464
}
6565

66-
const activeHash = hashToUse.substring(1);
67-
const allLinks = document.querySelectorAll('a');
66+
const activeHash = hash.substring(1);
6867

69-
for (let i = 0; i < allLinks.length; i++) {
70-
const link = allLinks[i];
71-
link.classList.remove('active');
72-
73-
if (link.parentElement && link.parentElement.parentElement && link.parentElement.parentElement.tagName === 'UL') {
74-
link.parentElement.parentElement.classList.remove('active');
68+
const updateTOC = () => {
69+
// Remove active class from all TOC links
70+
const allLinks = document.querySelectorAll('.table-of-contents a');
71+
for (let i = 0; i < allLinks.length; i++) {
72+
allLinks[i].classList.remove('table-of-contents__link--active');
7573
}
76-
}
7774

78-
const activeLinks = document.querySelectorAll("a[href='#" + activeHash + "']");
79-
for (let i = 0; i < activeLinks.length; i++) {
80-
activeLinks[i].classList.add('active');
81-
}
75+
// Add active class to matching TOC links
76+
const activeLinks = document.querySelectorAll(".table-of-contents a[href='#" + activeHash + "']");
77+
for (let i = 0; i < activeLinks.length; i++) {
78+
activeLinks[i].classList.add('table-of-contents__link--active');
79+
}
80+
};
81+
82+
// Update immediately
83+
updateTOC();
84+
85+
// Keep updating after scroll ends to override Docusaurus IntersectionObserver
86+
let scrollEndTimer;
87+
const handleScrollEnd = () => {
88+
clearTimeout(scrollEndTimer);
89+
scrollEndTimer = setTimeout(() => {
90+
updateTOC();
91+
window.removeEventListener('scroll', handleScrollEnd);
92+
}, 50);
93+
};
94+
95+
window.addEventListener('scroll', handleScrollEnd);
96+
handleScrollEnd();
8297
};
8398

8499
const highlightDetailsOnActiveHash = function(activeHash, doNotOpen) {
85100
const activeAnchors = document.querySelectorAll(".anchor[id='" + activeHash + "']");
86101
const detailsElements = document.querySelectorAll('details');
87102
const activeSectionElements = document.querySelectorAll('.active-section');
88103

104+
// Clear all previous highlights
89105
for (let i = 0; i < activeSectionElements.length; i++) {
90106
activeSectionElements[i].classList.remove('active-section');
91107
}
92108

93-
for (let i = 0; i < activeAnchors.length; i++) {
94-
const headline = activeAnchors[i].parentElement;
95-
const headlineRank = activeAnchors[i].parentElement.nodeName.substr(1);
96-
let el = headline;
97-
98-
while (el) {
99-
if (el.tagName !== 'BR' && el.tagName !== 'HR') {
100-
el.classList.add('active-section');
101-
}
102-
el = el.nextElementSibling;
103-
104-
if (el) {
105-
const elRank = el.nodeName.substr(1);
106-
if (elRank > 0 && elRank <= headlineRank) {
107-
break;
108-
}
109-
}
110-
}
111-
}
112-
113109
for (let i = 0; i < detailsElements.length; i++) {
114110
detailsElements[i].classList.remove('active');
115111
}
116112

117-
if (activeAnchors.length > 0) {
118-
for (let i = 0; i < activeAnchors.length; i++) {
119-
let element = activeAnchors[i];
120-
121-
for (; element && element !== document; element = element.parentElement) {
122-
if (element.tagName === 'DETAILS') {
123-
element.classList.add('active');
124-
125-
if (!doNotOpen) {
126-
element.open = true;
127-
element.setAttribute('data-collapsed', 'false');
128-
const collapsibleContent = element.querySelector(':scope > div[style]');
129-
if (collapsibleContent) {
130-
collapsibleContent.style.display = 'block';
131-
collapsibleContent.style.overflow = 'visible';
132-
collapsibleContent.style.height = 'auto';
133-
}
134-
}
135-
}
136-
}
137-
}
113+
// Add active-section class only to the heading itself
114+
for (let i = 0; i < activeAnchors.length; i++) {
115+
const headline = activeAnchors[i].parentElement;
116+
headline.classList.add('active-section');
138117
}
139118

119+
// Find the target element and handle its details
140120
const targetElement = activeHash ? document.getElementById(activeHash) : null;
141121
if (targetElement) {
142122
const parentDetails = getParentDetailsElements(targetElement);
143123
const keepOpenSet = new Set(parentDetails);
144124

125+
// Close other unrelated details
145126
if (!doNotOpen) {
146127
closeOtherDetails(keepOpenSet);
147128
}
148129

130+
// Open parent details and mark only the DIRECT target detail as active
131+
let directTargetDetail = null;
149132
for (let i = 0; i < parentDetails.length; i++) {
150133
const element = parentDetails[i];
151-
element.classList.add('active');
134+
135+
// The first (innermost) detail that contains the target is the direct one
136+
if (i === 0) {
137+
directTargetDetail = element;
138+
element.classList.add('active');
139+
}
140+
141+
// Open all parent details
152142
if (!doNotOpen) {
153143
element.open = true;
154144
element.setAttribute('data-collapsed', 'false');
@@ -209,10 +199,10 @@ export function onRouteDidUpdate({ location, previousLocation }) {
209199
handleHashNavigation(location.hash);
210200

211201
if (targetEl) {
212-
setTimeout(() => {
213-
const y = targetEl.getBoundingClientRect().top + window.scrollY - 280;
202+
requestAnimationFrame(() => {
203+
const y = targetEl.getBoundingClientRect().top + window.scrollY - 100;
214204
window.scrollTo({ top: y, behavior: 'smooth' });
215-
}, 150);
205+
});
216206
}
217207
}
218208
});
@@ -229,18 +219,19 @@ export function onRouteDidUpdate({ location, previousLocation }) {
229219
handleHashNavigation(hash);
230220

231221
if (targetEl) {
232-
setTimeout(() => {
233-
const y = targetEl.getBoundingClientRect().top + window.scrollY - 280;
222+
requestAnimationFrame(() => {
223+
const y = targetEl.getBoundingClientRect().top + window.scrollY - 100;
234224
window.scrollTo({ top: y, behavior: 'smooth' });
235-
}, 150);
225+
});
236226
}
237227
});
238228

239229
// NOTE: hashchange only handles highlighting, NOT scrolling (to avoid race condition)
240230
// Scrolling is handled by DetailsClicksClient's universal click handler
241231
window.addEventListener('hashchange', function(e) {
232+
// Extract hash from newURL to handle pushState correctly
242233
const newHash = e.newURL ? e.newURL.split('#')[1] : '';
243-
const hashToUse = newHash ? '#' + newHash : location.hash;
234+
const hashToUse = newHash ? '#' + newHash : '';
244235

245236
if (!hashToUse) {
246237
return;

src/client/DetailsClicksClient.js

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -650,10 +650,9 @@ const preserveExpansionStates = ExecutionEnvironment.canUseDOM ? function(skipEv
650650
const newHash = anchorLink.getAttribute("href");
651651
const targetId = newHash.substring(1);
652652

653+
// Remove from all elements first
653654
document.querySelectorAll(".-contains-target-link").forEach(function(el) {
654-
if (el.querySelectorAll(':scope > summary a[href="' + newHash + '"]').length == 0) {
655-
el.classList.remove("-contains-target-link");
656-
}
655+
el.classList.remove("-contains-target-link");
657656
});
658657

659658
let query = location.search;
@@ -671,16 +670,11 @@ const preserveExpansionStates = ExecutionEnvironment.canUseDOM ? function(skipEv
671670
parentDetails.forEach(openDetailsElement);
672671

673672
// replaceState doesn't trigger hashchange, so we must scroll here
674-
setTimeout(() => {
673+
requestAnimationFrame(() => {
675674
const targetRect = targetEl.getBoundingClientRect();
676-
if (targetRect.top !== 0 || targetRect.left !== 0) {
677-
window.scroll({
678-
behavior: 'smooth',
679-
left: 0,
680-
top: window.scrollY + targetRect.top - 280
681-
});
682-
}
683-
}, 150);
675+
const y = window.scrollY + targetRect.top - 100;
676+
window.scrollTo({ top: y, behavior: 'smooth' });
677+
});
684678
} else if (!el.hasAttribute("open")) {
685679
anchorLink.parentElement.click();
686680
}
@@ -729,6 +723,8 @@ const preserveExpansionStates = ExecutionEnvironment.canUseDOM ? function(skipEv
729723
// Universal hash link handler (including TOC sidebar)
730724
// Fixes Docusaurus's buggy hash navigation and enables CSS :target highlighting
731725
if (ExecutionEnvironment.canUseDOM) {
726+
let scrollTimeout = null;
727+
732728
document.addEventListener('click', function(e) {
733729
let target = e.target;
734730
let anchor = null;
@@ -756,26 +752,33 @@ if (ExecutionEnvironment.canUseDOM) {
756752
closeOtherDetails(keepOpenSet);
757753
parentDetails.forEach(openDetailsElement);
758754

759-
setTimeout(() => {
760-
const currentScrollY = window.scrollY;
755+
// Update URL hash for history/bookmarking
756+
const oldURL = window.location.href;
757+
window.history.pushState(null, '', hash);
758+
const newURL = window.location.href;
761759

762-
// Setting location.hash triggers :target CSS for highlighting
763-
if (window.location.hash !== hash) {
764-
window.location.hash = hash;
765-
}
760+
// Trigger hashchange event manually for other listeners (including sidebar highlighting)
761+
window.dispatchEvent(new HashChangeEvent('hashchange', {
762+
oldURL: oldURL,
763+
newURL: newURL
764+
}));
766765

767-
// Cancel browser's automatic scroll
768-
window.scrollTo(0, currentScrollY);
766+
// Cancel any pending scroll
767+
if (scrollTimeout) {
768+
clearTimeout(scrollTimeout);
769+
}
769770

770-
// Do our own controlled scroll
771-
setTimeout(() => {
772-
const rect = targetEl.getBoundingClientRect();
773-
if (rect.top !== 0 || rect.left !== 0) {
774-
const y = window.scrollY + rect.top - 280;
775-
window.scrollTo({ top: y, behavior: 'smooth' });
776-
}
777-
}, 50);
778-
}, 150);
771+
// Smooth scroll to target with proper offset
772+
scrollTimeout = requestAnimationFrame(() => {
773+
const rect = targetEl.getBoundingClientRect();
774+
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
775+
const targetY = scrollTop + rect.top - 100;
776+
777+
window.scrollTo({
778+
top: targetY,
779+
behavior: 'smooth'
780+
});
781+
});
779782
}
780783
}
781784
}, true); // Capture phase

0 commit comments

Comments
 (0)