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