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