@@ -988,62 +988,57 @@ async function fetchRevealBannersTogether() {
988
988
*
989
989
*/
990
990
async function addTOCScrollSpy ( ) {
991
+ // Intersection observer options
991
992
const options = {
992
- root : null , // Target the viewport
993
- // Offset the rootMargin slightly so that intersections trigger _before_ headings begin to
994
- // go offscreen. Need to account for the height of the top menu bar which is sticky, and
995
- // obscures the top part of the viewport. The factor of -1 is to move the location where the
996
- // intersection triggers downward below this element; a factor of 2 was found to give
997
- // good results in testing. If this is only -1 (the exact size of the top menu bar),
998
- // the intersection doesn't trigger until the heading is scrolled off the top of the screen
999
- // behind the menu bar, which also means that when clicking on a link in the TOC doesn't
1000
- // trigger the intersection (and therefore doesn't highlight the correct heading).
1001
- rootMargin : `${ - 2 * document . querySelector ( "header.bd-header" ) . getBoundingClientRect ( ) . bottom } px 0px 0px 0px` ,
1002
- threshold : 0 , // Trigger as soon as it becomes visible or invisible
993
+ root : null ,
994
+ rootMargin : `0px 0px -70% 0px` , // Use -70% for the bottom margin to so that intersection events happen in only the top third of the viewport
995
+ threshold : 1 , // Trigger once the heading becomes fully visible within the area described by the root margin
1003
996
} ;
1004
997
998
+ // Right sidebar table of contents container
1005
999
const pageToc = document . querySelector ( "#pst-page-toc-nav" ) ;
1006
- const tocLinks = Array . from ( document . querySelectorAll ( "#pst-page-toc-nav a" ) ) ;
1000
+
1001
+ // The table of contents is a list of .toc-entry items each of which contains
1002
+ // a link and possibly a nested list representing one level deeper in the
1003
+ // table of contents.
1004
+ const tocEntries = Array . from ( pageToc . querySelectorAll ( ".toc-entry" ) ) ;
1005
+ const tocLinks = Array . from ( pageToc . querySelectorAll ( "a" ) ) ;
1006
+
1007
+ // When the website visitor clicks a link in the TOC, we want that link to be
1008
+ // highlighted/activated, NOT whichever TOC link the intersection observer
1009
+ // callback would otherwise highlight, so we turn off the observer and turn it
1010
+ // back on later.
1011
+ let disableObserver = false ;
1012
+ pageToc . addEventListener ( "click" , ( event ) => {
1013
+ disableObserver = true ;
1014
+ const clickedTocLink = tocLinks . find ( ( el ) => el . contains ( event . target ) ) ;
1015
+ activate ( clickedTocLink ) ;
1016
+ setTimeout ( ( ) => {
1017
+ // Give the page ample time to finish scrolling, then re-enable the
1018
+ // intersection observer.
1019
+ disableObserver = false ;
1020
+ } , 1500 ) ;
1021
+ } ) ;
1007
1022
1008
1023
/**
1009
- * Activate an element and its parent TOC dropdowns ; deactivate
1010
- * everything else in the TOC. Together with the theme CSS, this
1011
- * highlights the given TOC entry.
1024
+ * Activate an element and its chain of ancestor TOC entries ; deactivate
1025
+ * everything else in the TOC. Together with the theme CSS, this unfolds
1026
+ * the TOC out to the given entry and highlights that entry.
1012
1027
*
1013
- * @param {HTMLElement } tocElement The TOC entry to be highlighted
1028
+ * @param {HTMLElement } tocLink The TOC entry to be highlighted
1014
1029
*/
1015
- function activate ( tocElement ) {
1016
- // Deactivate all TOC links except the requested element
1017
- tocLinks
1018
- . filter ( ( el ) => el !== tocElement )
1019
- . forEach ( ( el ) => {
1030
+ function activate ( tocLink ) {
1031
+ tocLinks . forEach ( ( el ) => {
1032
+ if ( el === tocLink ) {
1033
+ el . classList . add ( "active" ) ;
1034
+ el . setAttribute ( "aria-current" , true ) ;
1035
+ } else {
1020
1036
el . classList . remove ( "active" ) ;
1021
1037
el . removeAttribute ( "aria-current" ) ;
1022
- } ) ;
1023
-
1024
- // Activate the requested element
1025
- tocElement . classList . add ( "active" ) ;
1026
- tocElement . setAttribute ( "aria-current" , "true" ) ;
1027
-
1028
- // Travel up the DOM from the requested element, collecting the set of
1029
- // all parent elements that need to be activated. These are the collapsible
1030
- // <li> elements that can hold nested child headings.
1031
- const parents = new Set ( ) ;
1032
- let el = tocElement . parentElement ;
1033
- while ( el && el !== pageToc ) {
1034
- if ( el . matches ( ".toc-entry" ) ) {
1035
- parents . add ( el ) ;
1036
1038
}
1037
- el = el . parentElement ;
1038
- }
1039
-
1040
- // Iterate over all child elements of the TOC, deactivating everything
1041
- // that isn't a parent of the active node and activating the parents
1042
- // of the active TOC entry. This closes all collapsible <li> elements
1043
- // of which the active element is not a direct descendent, and activates
1044
- // those which are.
1045
- pageToc . querySelectorAll ( ".toc-entry" ) . forEach ( ( el ) => {
1046
- if ( parents . has ( el ) ) {
1039
+ } ) ;
1040
+ tocEntries . forEach ( ( el ) => {
1041
+ if ( el . contains ( tocLink ) ) {
1047
1042
el . classList . add ( "active" ) ;
1048
1043
} else {
1049
1044
el . classList . remove ( "active" ) ;
@@ -1052,61 +1047,43 @@ async function addTOCScrollSpy() {
1052
1047
}
1053
1048
1054
1049
/**
1055
- * Get the heading in the article associated with a TOC entry.
1050
+ * Get the heading in the article associated with the link in the table of contents
1056
1051
*
1057
- * @param {HTMLElement } tocElement TOC DOM element to use to grab an article heading
1052
+ * @param {HTMLElement } tocLink TOC DOM element to use to grab an article heading
1058
1053
*
1059
1054
* @returns The article heading that the TOC element links to
1060
1055
*/
1061
- function getHeading ( tocElement ) {
1056
+ function getHeading ( tocLink ) {
1062
1057
return document . querySelector (
1063
- `${ tocElement . getAttribute ( "href" ) } > :is(h1,h2,h3,h4,h5,h6)` ,
1058
+ `${ tocLink . getAttribute ( "href" ) } > :is(h1,h2,h3,h4,h5,h6)` ,
1064
1059
) ;
1065
1060
}
1066
1061
1067
- // Create a hashmap which stores the state of the headings. This object maps headings
1068
- // in the article to TOC elements, along with information about whether they are
1069
- // visible and the order in which they appear in the article.
1070
- const headingState = new Map (
1071
- Array . from ( tocLinks ) . map ( ( el ) => {
1072
- return [
1073
- getHeading ( el ) ,
1074
- {
1075
- tocElement : el ,
1076
- visible : false ,
1077
- } ,
1078
- ] ;
1079
- } ) ,
1080
- ) ;
1062
+ // Map article headings to their associated TOC links
1063
+ const tocLinksByHeading = new Map ( ) ;
1064
+ tocLinks . forEach ( ( link ) => tocLinksByHeading . set ( getHeading ( link ) , link ) ) ;
1081
1065
1082
1066
/**
1083
1067
*
1084
- * @param {IntersectionObserverEntry } entries Objects containing threshold-crossing
1068
+ * @param {IntersectionObserverEntry[] } entries Objects containing threshold-crossing
1085
1069
* event information
1086
1070
*
1087
1071
*/
1088
1072
function callback ( entries ) {
1089
- // Update the state of the TOC headings
1090
- entries . forEach ( ( entry ) => {
1091
- headingState . get ( entry . target ) . visible = entry . isIntersecting ;
1092
- } ) ;
1093
-
1094
- // If there are any visible results, activate the one _above_ the first visible
1095
- // heading. This ensures that when a heading scrolls offscreen, the TOC entry
1096
- // for that entry remains highlighted.
1097
- //
1098
- // If the first element is visible, just highlight the first entry in the TOC.
1099
- const visible = Array . from ( headingState . values ( ) ) . filter (
1100
- ( { visible } ) => visible ,
1101
- ) ;
1102
- if ( visible . length > 0 ) {
1103
- const indexAbove = Math . max ( sorted [ 0 ] . index - 1 , 0 ) ;
1104
- activate ( tocLinks [ indexAbove ] ) ;
1073
+ if ( disableObserver ) {
1074
+ return ;
1075
+ }
1076
+ const entry = entries . filter ( ( entry ) => entry . isIntersecting ) . pop ( ) ;
1077
+ if ( ! entry ) {
1078
+ return ;
1105
1079
}
1080
+ const heading = entry . target ;
1081
+ const tocLink = tocLinksByHeading . get ( heading ) ;
1082
+ activate ( tocLink ) ;
1106
1083
}
1107
1084
1108
1085
const observer = new IntersectionObserver ( callback , options ) ;
1109
- headingState . forEach ( ( _ , heading ) => {
1086
+ tocLinksByHeading . keys ( ) . forEach ( ( heading ) => {
1110
1087
observer . observe ( heading ) ;
1111
1088
} ) ;
1112
1089
}
0 commit comments