1/** 2 * Scroll-spy for the Yoast table of contents. 3 * 4 * A static list answers "what is on this page". This answers "where am I", 5 * which is the difference between a module that feels part of the page and one 6 * that feels dropped onto it. Yoast ships no such behaviour, so the theme adds 7 * it around the block's own markup â nothing here depends on Yoast internals 8 * beyond the anchors it already writes. 9 * 10 * Degrades to a plain list of links: without JS, or without IntersectionObserver, 11 * every link still jumps to its heading. Only the highlight is lost. 12 */ 13( function () { 14 'use strict'; 15 16 var toc = document.querySelector( '.yoast-table-of-contents' ); 17 18 if ( ! toc || ! ( 'IntersectionObserver' in window ) ) { 19 return; 20 } 21 22 /* 23 * Only same-document anchors, and only ones whose target actually exists. 24 * Yoast writes the list from the headings at save time, so a heading that 25 * was renamed or deleted afterwards leaves a link pointing at nothing â 26 * observing null would throw and take the whole highlight with it. 27 */ 28 var links = []; 29 30 Array.prototype.forEach.call( toc.querySelectorAll( 'a[href*="#"]' ), function ( link ) { 31 var id = link.hash ? link.hash.slice( 1 ) : ''; 32 33 if ( ! id ) { 34 return; 35 } 36 37 var target; 38 39 /* getElementById over a selector: Yoast's ids start with a digit often 40 enough (a heading like "2026 in review") that querySelector would 41 throw on the invalid selector. */ 42 try { 43 target = document.getElementById( decodeURIComponent( id ) ); 44 } catch ( e ) { 45 target = document.getElementById( id ); 46 } 47 48 if ( target ) { 49 links.push( { link: link, target: target } ); 50 } 51 } ); 52 53 if ( links.length < 2 ) { 54 /* One heading is not a table of contents, and highlighting the only 55 entry says nothing. */ 56 return; 57 } 58 59 var current = null; 60 61 function setCurrent( entry ) { 62 if ( entry === current ) { 63 return; 64 } 65 66 if ( current ) { 67 current.link.classList.remove( 'is-current' ); 68 current.link.removeAttribute( 'aria-current' ); 69 } 70 71 current = entry; 72 73 if ( current ) { 74 current.link.classList.add( 'is-current' ); 75 /* `location` rather than `true`: this marks the reader's position 76 within the page, not the current page in a set of pages. */ 77 current.link.setAttribute( 'aria-current', 'location' ); 78 } 79 } 80 81 /* 82 * Which heading counts as "current" is decided by position, not by what the 83 * observer happens to report. The observer is only a cheap way to be told 84 * that something moved â asking the DOM afterwards is what makes the answer 85 * correct when several headings share the viewport, or when none does 86 * because a long section fills it. 87 */ 88 function recompute() { 89 /* The sticky header covers the top of the viewport, so the line that 90 decides "have I reached this heading" sits below it. --sin-header-h is 91 published by header-scroll.js; the fallback matches its resting 92 height, and the same value is what scroll-margin-top uses. */ 93 var headerH = parseFloat( 94 getComputedStyle( document.documentElement ).getPropertyValue( '--sin-header-h' ) 95 ) || 84; 96 var line = headerH + 24; 97 var found = null; 98 99 for ( var i = 0; i < links.length; i++ ) { 100 if ( links[ i ].target.getBoundingClientRect().top <= line ) { 101 found = links[ i ]; 102 } else { 103 break; 104 } 105 } 106 107 /* 108 * Above the first heading, nothing is highlighted rather than the first 109 * link being highlighted pre-emptively â the reader has not reached it, 110 * and saying they have is worse than saying nothing. 111 * 112 * At the very bottom the last section can be too short to ever cross the 113 * line, so the page can end with the second-to-last link lit. Reaching 114 * the bottom means you are in the last section, whatever the arithmetic 115 * says. 116 */ 117 if ( window.innerHeight + window.scrollY >= document.body.offsetHeight - 2 ) { 118 found = links[ links.length - 1 ]; 119 } 120 121 setCurrent( found ); 122 } 123 124 var ticking = false; 125 126 function onScroll() { 127 if ( ticking ) { 128 return; 129 } 130 131 ticking = true; 132 window.requestAnimationFrame( function () { 133 recompute(); 134 ticking = false;
135 } ); 136 } 137 138 /* The observer fires on the crossings; the scroll listener covers the long 139 drifts between them. Both funnel into the same rAF-throttled recompute, so 140 the cost is one layout read per frame at most. */ 141 var observer = new IntersectionObserver( onScroll, { 142 threshold: [ 0, 1 ], 143 } ); 144 145 links.forEach( function ( entry ) { 146 observer.observe( entry.target ); 147 } ); 148 149 window.addEventListener( 'scroll', onScroll, { passive: true } ); 150 window.addEventListener( 'resize', onScroll, { passive: true } ); 151 152 recompute(); 153}() );
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.