1/* DokuWiki MoaiEditor Scroll_to.js file 2 Version : 0.5 (May 5, 2026) 3 Author : MoaiTools <info@moaitools.org> 4 License : GPL 3 (http://www.gnu.org/licenses/gpl.html) */ 5 6/* This class will scroll smoothly to a desired point in the document while 7 measuring the reported position of that point continously in order to make 8 on-the-fly corrections to the scroll, while at the same time avoiding 9 correcting too often due to most browsers stuttering badly if done so 10 when 'scroll-behavior' is set to 'smooth' (Firefox being the exception). 11 12 This is needed because oftentimes positions of DOM elements get reported 13 inaccurately if that element is far away from the visible area. Therefore 14 scrolling close to that element is needed before an accurate measurement 15 can be obtained. 16 17 Examples of innacurate position reporting can be observed in the preview 18 (due to lazy loading of resources) and in CodeMirror (due to most lines 19 in CodeMirror not being rendered before they are in the visible area). 20*/ 21MoaiEditor.ScrollTo = class { 22 23 constructor () { 24 25 // Constants 26 this.timeout = 4000; // Avoid getting stuck in an infinite loop if something goes wrong 27 this.minUpdateTime = 300; // Avoid updating the scroll position to often to avoid stutter in browsers 28 29 // Variables 30 this.object = null; // Object which needs to be scrolled (requires these getters and setters: pointerEvents, scroll.top, scroll.smooth) 31 this.smooth = true; // Smooth scroll can be disabled (but we still get the benefit of giving the the scroll time to converge) 32 this.ended = true; // Flag indicating if the loop has ended 33 this.success = false; 34 this.startTime = null; // 35 this.lastScroll = null; // Last measured scroll 36 this.lastUpdateTime = null; // Last time the scroll was set 37 this.sameScrollCount = null; // Count interations without scroll change 38 this.targetScroll = null; // 39 this.getTargetScroll = null; // Callback to update the target scroll 40 this.i = 0; // Iteration counter 41 } 42 // ──────────────────────────────────── 43 start() { 44 // Remove flash element 45 this.object.flash('remove'); 46 // Initialize scroll variables 47 this.targetScroll = this.getClampedTargetScroll(); 48 this.lastScroll = Math.floor(this.object.scroll.top); 49 // End inmediately if we are already at the correct position 50 const scroll = Math.floor(this.object.scroll.top); 51 const distance = Math.abs(scroll-this.targetScroll); 52 if (distance < 3) { 53 this.stop(true); 54 return; 55 } 56 // Initialize time variables 57 this.startTime = Date.now(); 58 this.lastUpdateTime = Date.now(); 59 this.sameScrollCount = 0; 60 // Enable or disable smooth scroll on the DOM element 61 this.object.scroll.smooth = this.smooth; 62 // Set the scroll on the DOM element 63 this.object.scroll.top = this.targetScroll; 64 // Prevent users from interfering during scroll 65 this.object.pointerEvents = false; 66 // Prevent other processes from interfering during scroll (this also disables the mirror-to-textarea synchronization) 67 moaiEditor.scroll.sync.disabled = true; 68 // Show autoscrolling visual indicator (to facilitate bug tracking if the loop crashes) 69 moaiEditor.layout.indicatorScrolling.style.opacity = '1'; 70 // Start loop 71 this.i = 0; 72 this.ended = false; 73 this.success = false; 74 clearInterval(this.interval); 75 this.interval = setInterval(this.onInterval.bind(this), 50); 76 } 77 // ──────────────────────────────────── 78 halt() { 79 // Stop when switching editors 80 clearInterval(this.interval); 81 } 82 // ──────────────────────────────────── 83 stop(reached) { 84 // TODO: Prevent errors: make sure to end the loop if the user switches editors, the element disapears, or the lines get deleted 85 this.ended = true; 86 this.success = reached; 87 clearInterval(this.interval); 88 // Disable smooth scroll 89 this.object.scroll.smooth = false; 90 // Re-enable pointer events 91 this.object.pointerEvents = true; 92 // Callback 93 moaiEditor.scroll.onScrollEnd(); 94 } 95 // ──────────────────────────────────── 96 getClampedTargetScroll() { 97 // Clamp the target scroll to handle out-of-bounds target values (else we will never reach the target scroll) 98 // Also handle edge cases while scroll is in progress: 99 // a) preview is updated and preview element gets removed 100 // b) document gets edited and line number no longer exist 101 try { 102 var target = Math.floor(this.getTargetScroll()); 103 target = Math.max(0, target); 104 target = Math.min(target, this.object.scroll.max); 105 return target; 106 } catch ({name, message}) { 107 clearInterval(this.interval); 108 const errorString = "moaiEditor.scrollTo: "+name+": "+message; 109 const knownError = ['TypeError'].includes(name); 110 // Show known errors only in strict mode 111 if (knownError) { 112 if (moaiEditor.strict) 113 console.warn(errorString); 114 } 115 // Always show unknown errors 116 else { 117 if (moaiEditor.strict) 118 console.warn(errorString); 119 else 120 console.error(errorString); 121 } 122 return null; 123 } 124 } 125 // ──────────────────────────────────── 126 same(a, b) { 127 // Compare aproximate (and not exact) scroll positions to know if they are the same, 128 // because some browsers seem to not to be pixel-perfect at scrolling to a given position. 129 if (Math.abs(a - b) < 2) 130 return true; 131 return false; 132 } 133 // ──────────────────────────────────── 134 onInterval() { 135 136 // Time variables 137 this.i += 1; 138 const now = Date.now(); 139 const elapsed = now - this.startTime; 140 const lastUpdateElapsed = now - this.lastUpdateTime; 141 var minUpdateTime = 0; 142 if (this.smooth) 143 minUpdateTime = this.minUpdateTime; 144 145 // Stop if too much time has elapsed 146 if (elapsed > this.timeout) { 147 this.stop(false); 148 return; 149 } 150 // Get scroll values 151 const scroll = Math.floor(this.object.scroll.top); 152 const distance = Math.abs(scroll-this.targetScroll); 153 const targetScroll = this.getClampedTargetScroll(); 154 155 // Stop if the target scroll could not be obtained 156 if (targetScroll === null) { 157 clearInterval(this.interval); 158 return; 159 } 160 // Detect if scroll has stopped 161 this.sameScrollCount += 1; 162 if (!this.same (scroll,this.lastScroll)) 163 this.sameScrollCount = 0; 164 165 // Determine minUpdateTime 166 167 // Stop if we reached the intended scroll and enough time has passed 168 const targetScrollReached = distance <= 2; 169 const minFramesReached = this.sameScrollCount >= 3; 170 const minTimeLastUpdateReached = lastUpdateElapsed > minUpdateTime+50; 171 if (targetScrollReached && minFramesReached && minTimeLastUpdateReached) { 172 this.stop(true); 173 return; 174 } 175 // Correct scroll if needed and we are close to the previous destination (to avoid browser stutter) 176 if (!this.same (targetScroll,this.targetScroll) && lastUpdateElapsed > minUpdateTime && distance < 600) { 177 this.object.scroll.top = targetScroll; 178 this.targetScroll = targetScroll; 179 this.lastUpdateTime = now; 180 } 181 // Remember some values 182 this.lastScroll = scroll; 183 } 184}; // End Class 185 186 187 188 189 190 191 192