@@ -95,43 +95,6 @@ function addModeListener() {
95
95
} ) ;
96
96
}
97
97
98
- /*******************************************************************************
99
- * Right sidebar table of contents (TOC) interactivity
100
- */
101
- function setupPageTableOfContents ( ) {
102
- const pageToc = document . querySelector ( "#pst-page-toc-nav" ) ;
103
- pageToc . addEventListener ( "click" , ( event ) => {
104
- const clickedLink = event . target . closest ( ".nav-link" ) ;
105
- if ( ! clickedLink ) {
106
- return ;
107
- }
108
-
109
- // First, clear all the added classes and attributes
110
- // -----
111
- pageToc . querySelectorAll ( "a[aria-current]" ) . forEach ( ( el ) => {
112
- el . removeAttribute ( "aria-current" ) ;
113
- } ) ;
114
- pageToc . querySelectorAll ( ".active" ) . forEach ( ( el ) => {
115
- el . classList . remove ( "active" ) ;
116
- } ) ;
117
-
118
- // Then add the classes and attributes to where they should go now
119
- // -----
120
- clickedLink . setAttribute ( "aria-current" , "true" ) ;
121
- clickedLink . classList . add ( "active" ) ;
122
- // Find all parents (up to the TOC root) matching .toc-entry and add the
123
- // active class. This activates style rules that expand the TOC when the
124
- // user clicks a TOC entry that has nested entries.
125
- let parentElement = clickedLink . parentElement ;
126
- while ( parentElement && parentElement !== pageToc ) {
127
- if ( parentElement . matches ( ".toc-entry" ) ) {
128
- parentElement . classList . add ( "active" ) ;
129
- }
130
- parentElement = parentElement . parentElement ;
131
- }
132
- } ) ;
133
- }
134
-
135
98
/*******************************************************************************
136
99
* Scroll
137
100
*/
@@ -1020,6 +983,128 @@ async function fetchRevealBannersTogether() {
1020
983
} , 320 ) ;
1021
984
}
1022
985
986
+ /**
987
+ * Add the machinery needed to highlight elements in the TOC when scrolling.
988
+ *
989
+ */
990
+ async function addTOCScrollSpy ( ) {
991
+ const options = {
992
+ root : null , // Target the viewport
993
+ // Offset the rootMargin slightly so that intersections trigger _before_ headings begin to
994
+ // go offscreen
995
+ rootMargin : `${ - 2 * document . querySelector ( "header.bd-header" ) . getBoundingClientRect ( ) . bottom } px 0px 0px 0px` ,
996
+ threshold : 0 , // Trigger as soon as 1 pixel is visible
997
+ } ;
998
+
999
+ const pageToc = document . querySelector ( "#pst-page-toc-nav" ) ;
1000
+ const tocLinks = Array . from ( document . querySelectorAll ( "#pst-page-toc-nav a" ) ) ;
1001
+
1002
+ /**
1003
+ * Activate an element and its parent TOC dropdowns; deactivate
1004
+ * everything else in the TOC. Together with the theme CSS, this
1005
+ * highlights the given TOC entry.
1006
+ *
1007
+ * @param {HTMLElement } tocElement The TOC entry to be highlighted
1008
+ */
1009
+ function activate ( tocElement ) {
1010
+ // Deactivate all TOC links except the requested element
1011
+ tocLinks
1012
+ . filter ( ( el ) => el !== tocElement )
1013
+ . forEach ( ( el ) => {
1014
+ el . classList . remove ( "active" ) ;
1015
+ el . removeAttribute ( "aria-current" ) ;
1016
+ } ) ;
1017
+
1018
+ // Activate the requested element
1019
+ tocElement . classList . add ( "active" ) ;
1020
+ tocElement . setAttribute ( "aria-current" , "true" ) ;
1021
+
1022
+ // Travel up the DOM from the requested element, collecting the set of
1023
+ // all parent elements that need to be activated
1024
+ const parents = new Set ( ) ;
1025
+ let el = tocElement . parentElement ;
1026
+ while ( el && el !== pageToc ) {
1027
+ if ( el . matches ( ".toc-entry" ) ) {
1028
+ parents . add ( el ) ;
1029
+ }
1030
+ el = el . parentElement ;
1031
+ }
1032
+
1033
+ // Iterate over all child elements of the TOC, deactivating everything
1034
+ // that isn't a parent of the active node and activating the parents
1035
+ // of the active TOC entry
1036
+ pageToc . querySelectorAll ( ".toc-entry" ) . forEach ( ( el ) => {
1037
+ if ( parents . has ( el ) ) {
1038
+ el . classList . add ( "active" ) ;
1039
+ } else {
1040
+ el . classList . remove ( "active" ) ;
1041
+ }
1042
+ } ) ;
1043
+ }
1044
+
1045
+ /**
1046
+ * Get the heading in the article associated with a TOC entry.
1047
+ *
1048
+ * @param {HTMLElement } tocElement TOC DOM element to use to grab an article heading
1049
+ *
1050
+ * @returns The article heading that the TOC element links to
1051
+ */
1052
+ function getHeading ( tocElement ) {
1053
+ return document . querySelector (
1054
+ `${ tocElement . getAttribute ( "href" ) } > :is(h1,h2,h3,h4,h5,h6)` ,
1055
+ ) ;
1056
+ }
1057
+
1058
+ // Create a hashmap which stores the state of the headings. This object maps headings
1059
+ // in the article to TOC elements, along with information about whether they are
1060
+ // visible and the order in which they appear in the article.
1061
+ const headingState = new Map (
1062
+ Array . from ( tocLinks ) . map ( ( el , index ) => {
1063
+ return [
1064
+ getHeading ( el ) ,
1065
+ {
1066
+ tocElement : el ,
1067
+ visible : false ,
1068
+ index : index ,
1069
+ } ,
1070
+ ] ;
1071
+ } ) ,
1072
+ ) ;
1073
+
1074
+ /**
1075
+ *
1076
+ * @param {IntersectionObserverEntry } entries Objects containing threshold-crossing
1077
+ * event information
1078
+ *
1079
+ */
1080
+ function callback ( entries ) {
1081
+ // Update the state of the TOC headings
1082
+ entries . forEach ( ( entry ) => {
1083
+ headingState . get ( entry . target ) . visible = entry . isIntersecting ;
1084
+ } ) ;
1085
+
1086
+ // Sort the active headings by the order in which they appear in the TOC.
1087
+ const sorted = Array . from ( headingState . values ( ) )
1088
+ . filter ( ( { visible } ) => visible )
1089
+ . sort ( ( a , b ) => a . index > b . index ) ;
1090
+
1091
+ // If there are any visible results, activate the one _above_ the first visible
1092
+ // heading. This ensures that when a heading scrolls offscreen, the TOC entry
1093
+ // for that entry remains highlighted.
1094
+ //
1095
+ // If the first element is visible, just highlight the first entry in the TOC.
1096
+ if ( sorted . length > 0 ) {
1097
+ const idx = sorted [ 0 ] . index ;
1098
+ activate ( tocLinks [ idx > 0 ? idx - 1 : 0 ] ) ;
1099
+ }
1100
+ }
1101
+
1102
+ const observer = new IntersectionObserver ( callback , options ) ;
1103
+ tocLinks . forEach ( ( tocElement ) => {
1104
+ observer . observe ( getHeading ( tocElement ) ) ;
1105
+ } ) ;
1106
+ }
1107
+
1023
1108
/*******************************************************************************
1024
1109
* Call functions after document loading.
1025
1110
*/
@@ -1030,10 +1115,10 @@ documentReady(fetchRevealBannersTogether);
1030
1115
1031
1116
documentReady ( addModeListener ) ;
1032
1117
documentReady ( scrollToActive ) ;
1033
- documentReady ( setupPageTableOfContents ) ;
1034
1118
documentReady ( setupSearchButtons ) ;
1035
1119
documentReady ( setupSearchAsYouType ) ;
1036
1120
documentReady ( setupMobileSidebarKeyboardHandlers ) ;
1121
+ documentReady ( addTOCScrollSpy ) ;
1037
1122
1038
1123
// Determining whether an element has scrollable content depends on stylesheets,
1039
1124
// so we're checking for the "load" event rather than "DOMContentLoaded"
0 commit comments