/* DokuWiki MoaiEditor Scroll_to.js file Version : 0.5 (May 5, 2026) Author : MoaiTools License : GPL 3 (http://www.gnu.org/licenses/gpl.html) */ /* This class will scroll smoothly to a desired point in the document while measuring the reported position of that point continously in order to make on-the-fly corrections to the scroll, while at the same time avoiding correcting too often due to most browsers stuttering badly if done so when 'scroll-behavior' is set to 'smooth' (Firefox being the exception). This is needed because oftentimes positions of DOM elements get reported inaccurately if that element is far away from the visible area. Therefore scrolling close to that element is needed before an accurate measurement can be obtained. Examples of innacurate position reporting can be observed in the preview (due to lazy loading of resources) and in CodeMirror (due to most lines in CodeMirror not being rendered before they are in the visible area). */ MoaiEditor.ScrollTo = class { constructor () { // Constants this.timeout = 4000; // Avoid getting stuck in an infinite loop if something goes wrong this.minUpdateTime = 300; // Avoid updating the scroll position to often to avoid stutter in browsers // Variables this.object = null; // Object which needs to be scrolled (requires these getters and setters: pointerEvents, scroll.top, scroll.smooth) this.smooth = true; // Smooth scroll can be disabled (but we still get the benefit of giving the the scroll time to converge) this.ended = true; // Flag indicating if the loop has ended this.success = false; this.startTime = null; // this.lastScroll = null; // Last measured scroll this.lastUpdateTime = null; // Last time the scroll was set this.sameScrollCount = null; // Count interations without scroll change this.targetScroll = null; // this.getTargetScroll = null; // Callback to update the target scroll this.i = 0; // Iteration counter } // ──────────────────────────────────── start() { // Remove flash element this.object.flash('remove'); // Initialize scroll variables this.targetScroll = this.getClampedTargetScroll(); this.lastScroll = Math.floor(this.object.scroll.top); // End inmediately if we are already at the correct position const scroll = Math.floor(this.object.scroll.top); const distance = Math.abs(scroll-this.targetScroll); if (distance < 3) { this.stop(true); return; } // Initialize time variables this.startTime = Date.now(); this.lastUpdateTime = Date.now(); this.sameScrollCount = 0; // Enable or disable smooth scroll on the DOM element this.object.scroll.smooth = this.smooth; // Set the scroll on the DOM element this.object.scroll.top = this.targetScroll; // Prevent users from interfering during scroll this.object.pointerEvents = false; // Prevent other processes from interfering during scroll (this also disables the mirror-to-textarea synchronization) moaiEditor.scroll.sync.disabled = true; // Show autoscrolling visual indicator (to facilitate bug tracking if the loop crashes) moaiEditor.layout.indicatorScrolling.style.opacity = '1'; // Start loop this.i = 0; this.ended = false; this.success = false; clearInterval(this.interval); this.interval = setInterval(this.onInterval.bind(this), 50); } // ──────────────────────────────────── halt() { // Stop when switching editors clearInterval(this.interval); } // ──────────────────────────────────── stop(reached) { // TODO: Prevent errors: make sure to end the loop if the user switches editors, the element disapears, or the lines get deleted this.ended = true; this.success = reached; clearInterval(this.interval); // Disable smooth scroll this.object.scroll.smooth = false; // Re-enable pointer events this.object.pointerEvents = true; // Callback moaiEditor.scroll.onScrollEnd(); } // ──────────────────────────────────── getClampedTargetScroll() { // Clamp the target scroll to handle out-of-bounds target values (else we will never reach the target scroll) // Also handle edge cases while scroll is in progress: // a) preview is updated and preview element gets removed // b) document gets edited and line number no longer exist try { var target = Math.floor(this.getTargetScroll()); target = Math.max(0, target); target = Math.min(target, this.object.scroll.max); return target; } catch ({name, message}) { clearInterval(this.interval); const errorString = "moaiEditor.scrollTo: "+name+": "+message; const knownError = ['TypeError'].includes(name); // Show known errors only in strict mode if (knownError) { if (moaiEditor.strict) console.warn(errorString); } // Always show unknown errors else { if (moaiEditor.strict) console.warn(errorString); else console.error(errorString); } return null; } } // ──────────────────────────────────── same(a, b) { // Compare aproximate (and not exact) scroll positions to know if they are the same, // because some browsers seem to not to be pixel-perfect at scrolling to a given position. if (Math.abs(a - b) < 2) return true; return false; } // ──────────────────────────────────── onInterval() { // Time variables this.i += 1; const now = Date.now(); const elapsed = now - this.startTime; const lastUpdateElapsed = now - this.lastUpdateTime; var minUpdateTime = 0; if (this.smooth) minUpdateTime = this.minUpdateTime; // Stop if too much time has elapsed if (elapsed > this.timeout) { this.stop(false); return; } // Get scroll values const scroll = Math.floor(this.object.scroll.top); const distance = Math.abs(scroll-this.targetScroll); const targetScroll = this.getClampedTargetScroll(); // Stop if the target scroll could not be obtained if (targetScroll === null) { clearInterval(this.interval); return; } // Detect if scroll has stopped this.sameScrollCount += 1; if (!this.same (scroll,this.lastScroll)) this.sameScrollCount = 0; // Determine minUpdateTime // Stop if we reached the intended scroll and enough time has passed const targetScrollReached = distance <= 2; const minFramesReached = this.sameScrollCount >= 3; const minTimeLastUpdateReached = lastUpdateElapsed > minUpdateTime+50; if (targetScrollReached && minFramesReached && minTimeLastUpdateReached) { this.stop(true); return; } // Correct scroll if needed and we are close to the previous destination (to avoid browser stutter) if (!this.same (targetScroll,this.targetScroll) && lastUpdateElapsed > minUpdateTime && distance < 600) { this.object.scroll.top = targetScroll; this.targetScroll = targetScroll; this.lastUpdateTime = now; } // Remember some values this.lastScroll = scroll; } }; // End Class