1/** 2 * Comments anchor scrolling (NAVIGA-667) 3 * 4 * The comment icons at the top and at the bottom of an article are plain 5 * anchors pointing at #coral_thread. The Coral thread, the paywall and the ads 6 * above them are loaded asynchronously, so the layout is still growing when the 7 * browser performs the native anchor jump - the page ends up scrolled short of 8 * the comments. This script takes over those jumps and keeps correcting the 9 * scroll position for a short while, until the page has settled. 10 */ 11(function () { 12 var TARGET_ID = "coral_thread"; 13 var ANCHOR_SELECTOR = 'a[href^="#' + TARGET_ID + '"]'; 14 var SETTLE_TIME = 5000; // How long the position is corrected after a jump (ms) 15 var CHECK_INTERVAL = 100; // How often the position is re-checked (ms) 16 var TOLERANCE = 2; // Do not re-scroll for differences smaller than this (px) 17 var MARGIN = 8; // Breathing room between the sticky header and the target (px) 18 19 var correctionTimer = null; 20 var previousScrollBehavior = ""; 21 var expectedPosition = -1; 22 23 /** 24 * Height of the sticky/fixed elements that cover the top of the viewport once 25 * the page is scrolled, so that the target does not end up underneath them. 26 * @returns {number} Offset in pixels 27 */ 28 function getStickyOffset() { 29 var elements = document.querySelectorAll(".sticky-top, .fixed-top"); 30 var offset = 0; 31 32 for (var i = 0; i < elements.length; i++) { 33 var style = window.getComputedStyle(elements[i]); 34 var position = style.position; 35 if (position !== "sticky" && position !== "-webkit-sticky" && position !== "fixed") { 36 continue; 37 } 38 if (style.display === "none" || style.visibility === "hidden") continue; 39 40 // Where the element ends up once it is pinned - it is not pinned yet when 41 // the user is still at the top of the page, so its current position in the 42 // viewport cannot be used. 43 var top = parseFloat(style.top); 44 if (isNaN(top) || top < 0) continue; 45 46 var height = elements[i].getBoundingClientRect().height; 47 if (height > 0) { 48 offset = Math.max(offset, top + height); 49 } 50 } 51 52 return offset; 53 } 54 55 /** 56 * Document position the target element should be scrolled to. 57 * @param {Element} target Element to scroll to 58 * @returns {number} Scroll position in pixels 59 */ 60 function getTargetPosition(target) { 61 var top = 62 window.pageYOffset + 63 target.getBoundingClientRect().top - 64 getStickyOffset() - 65 MARGIN; 66 67 // Never ask for more than the page can actually scroll, otherwise the 68 // correction below would keep firing against a clamped position. 69 var maxScroll = Math.max( 70 0, 71 document.documentElement.scrollHeight - window.innerHeight 72 ); 73 74 return Math.min(Math.max(0, top), maxScroll); 75 } 76 77 /** 78 * Stop correcting the scroll position and restore smooth scrolling. 79 */ 80 function stopCorrecting() { 81 if (!correctionTimer) return; 82 83 clearInterval(correctionTimer); 84 correctionTimer = null; 85 expectedPosition = -1; 86 document.documentElement.style.scrollBehavior = previousScrollBehavior; 87 } 88 89 /** 90 * Scroll the window and remember where we put it, so that scrolling done by 91 * the user can be told apart from our own corrections. 92 * @param {number} position Scroll position in pixels 93 */ 94 function scrollToPosition(position) { 95 window.scrollTo(0, position); 96 expectedPosition = window.pageYOffset; 97 } 98 99 /** 100 * Scroll to the target and keep it in place while the page is still growing. 101 * @param {Element} target Element to scroll to 102 */ 103 function scrollToTarget(target) { 104 stopCorrecting(); 105 106 // The site sets `html { scroll-behavior: smooth }`, which would animate every 107 // correction below and leave them restarting each other. Jump instantly. 108 previousScrollBehavior = document.documentElement.style.scrollBehavior; 109 document.documentElement.style.scrollBehavior = "auto"; 110 111 var remaining = SETTLE_TIME; 112 correctionTimer = setInterval(function () { 113 remaining -= CHECK_INTERVAL; 114 if (remaining <= 0 || !document.body.contains(target)) { 115 stopCorrecting(); 116 return; 117 } 118 119 var position = getTargetPosition(target); 120 if (Math.abs(window.pageYOffset - position) > TOLERANCE) { 121 scrollToPosition(position); 122 } 123 }, CHECK_INTERVAL); 124 125 scrollToPosition(getTargetPosition(target)); 126 } 127 128 /** 129 * Any scrolling we did not ask for means the user has taken over - back off. 130 * A plain scroll listener is used on purpose: wheel and touch events that 131 * happen on top of the cross-origin Coral iframe never reach this document. 132 */ 133 function onScroll() { 134 if (!correctionTimer) return; 135 if (Math.abs(window.pageYOffset - expectedPosition) > TOLERANCE) { 136 stopCorrecting(); 137 } 138 } 139 140 /** 141 * Handle a click on a comment anchor. 142 * @param {Event} event Click event 143 */ 144 function onClick(event) { 145 if (event.defaultPrevented) return; 146 if (event.button) return; // Not a plain left click 147 if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; 148 if (!event.target || !event.target.closest) return; 149 150 var link = event.target.closest(ANCHOR_SELECTOR); 151 if (!link) return; 152 153 var target = document.getElementById(TARGET_ID); 154 if (!target) return; // Comments are not on the page - let the browser decide. 155 156 event.preventDefault(); 157 158 // Add a history entry just like the native anchor would, so that Back takes 159 // the reader to where they were before the jump. Carry the existing state 160 // along - the SSO referer fix in custom-placeholder-top stores 161 // originalReferer there and reads it back later. 162 if ( 163 window.history && 164 window.history.pushState && 165 window.location.hash !== "#" + TARGET_ID 166 ) { 167 window.history.pushState( 168 window.history.state, 169 document.title, 170 "#" + TARGET_ID 171 ); 172 } 173 174 scrollToTarget(target); 175 } 176 177 // Nothing to do on pages without comment icons. 178 if (document.querySelector(ANCHOR_SELECTOR)) { 179 document.addEventListener("click", onClick); 180 window.addEventListener("scroll", onScroll, { passive: true }); 181 } 182})();
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.