/* DokuWiki MoaiEditor Scroll_to.js file Version : 0.5b (2026-05-08) Author : MoaiTools License : GPL 3 (http://www.gnu.org/licenses/gpl.html) */ /* This is the umbrella class for scrolling functions: - Scrolling the preview when the user scrolls the editor (keeping it synchronized). - Scrolling the editor when the user clicks an element on the preview. - Scrolling both editor and preview when the user clicks on the table of contents. - Scrolling both editor and preview when the user clicks on the go to top button. - Scrolling both editor and preview when the user clicks on the go to bottom button. */ MoaiEditor.Scroll = class { constructor(outer) { // Arguments this.outer = outer; // Variables this.clipboard = null; // Objects this.tools = new MoaiEditor.ScrollTools(); this.sync = new MoaiEditor.ScrollSync(this); this.left = new MoaiEditor.ScrollTo(); this.right = new MoaiEditor.ScrollTo(); } // ──────────────────────────────────── init() { this.sync.init(); } // ──────────────────────────────────── halt() { // Stop any scroll loop in progress before switching editors this.left.halt(); this.right.halt(); this.sync.scroller.engaged = false; } // ──────────────────────────────────── // Called whenever the 'ScrollTo' class finishes scrolling onScrollEnd() { // Wait for both sides to end if (!this.left.ended || !this.right.ended) return; // Flash elements var flashleft = null; // null:no flash, true:flash blue, false:flash red var flashright = null; if (this.scroll.type == 'click') { //this.left.success = false; // TODO: Test this condition flashright = this.left.success; if (this.left.success) flashleft = true; } else if (this.scroll.type == 'toc') { if (this.left.success) flashleft = true; if (this.right.success) flashright = true; } moaiEditor.editor.current.flash(flashleft, this.scroll); moaiEditor.preview.flash(flashright, this.scroll); // Hide autoscrolling visual indicator moaiEditor.layout.indicatorScrolling.style.opacity = '0'; // Re-enable external scroll synchronization this.sync.lastScroll = Math.round(moaiEditor.editor.current.scroll.top); moaiEditor.scroll.sync.disabled = false; //setTimeout(function(){}, 100); } // ──────────────────────────────────── toTop() { // Setup variables this.smoothScrollInit(); this.scroll = {type:'top'}; // Setup the callbacks this.left.getTargetScroll = function() {return 0;}; this.right.getTargetScroll = function() {return 0;}; // Start the process this.left.start(); this.right.start(); } // ──────────────────────────────────── toBottom() { // Setup variables this.smoothScrollInit(); this.scroll = {type:'bottom'}; // Setup the callbacks this.left.getTargetScroll = function() {return this.object.scroll.max;}; this.right.getTargetScroll = function() {return this.object.scroll.max;}; // Start the process this.left.start(); this.right.start(); } // ──────────────────────────────────── // Scrolls both panes when the user clicks on the table of contents toc(element) { // Exit if the match does not exist (can happen if the user edited the line before the preview updated) const match = this.findMatch(element); if (match === null) { return; } // Setup variables this.smoothScrollInit(); this.left.linenum = match.startline; this.right.element = element; this.scroll = {type:'toc', startline:match.startline, endline:match.endline, element:element}; // Setup the callbacks this.left.getTargetScroll = this.toc_getTargetScroll_left; this.right.getTargetScroll = this.toc_getTargetScroll_right; // Start the process this.left.start(); this.right.start(); } toc_getTargetScroll_left() { return this.object.getLineRect(this.linenum).top; } toc_getTargetScroll_right() { return moaiEditor.scroll.tools.getRectRelativeToParent(this.element).top; } // ──────────────────────────────────── // Scrolls the editor when the user clicks an element on the preview onClick(element) { // Exit if the match does not exist (can happen if the user edited the line before the preview updated) const match = this.findMatch(element); if (match === null) { moaiEditor.preview.flash(false, {element:element}); // TODO: Test this condition return; } // Current right position let pos = element.getBoundingClientRect(); let container = document.querySelector("#moaied__preview").getBoundingClientRect(); let right = (pos.top + pos.bottom)/2 - container.top; // Setup variables this.smoothScrollInit(); this.left.startline = match.startline; this.left.endline = match.endline; this.left.target = right; this.scroll = {type:'click', startline:match.startline, endline:match.endline, element:element}; // Setup the callback this.left.getTargetScroll = this.onClick_getTargetScroll; // Start the process this.left.start(); } onClick_getTargetScroll() { const editor = this.object; // Current left position let container = document.querySelector("#moaied__mirror").getBoundingClientRect(); let top = editor.getLineRect(this.startline, 'viewport').top; let bottom = editor.getLineRect(this.endline, 'viewport').bottom; let left = (top + bottom)/2 - container.top; // Calc final scroll let correction = left - this.target; // Return return editor.scroll.top + correction; } // ──────────────────────────────────── makeElementClickable(element) { // Exit if the element already has been made clickable if (element.classList.contains('moaied-preview-header')) return; // Add event listener element.addEventListener('click', function(){ moaiEditor.scroll.onClick(this); }); // Style the header const headerLvl = Number(element.tagName.substr(1,1)); element.classList.add('moaied-preview-header'); element.title = 'Click to scroll'; // Add icon image const h = 24 - 2*headerLvl; const m = 14 - 2*headerLvl; const img = moaiEditor.createHTML(''); img.style.setProperty('height', h+'px'); img.style.setProperty('margin-left', m+'px'); element.appendChild(img); } // ──────────────────────────────────── smoothScrollInit () { // In phones, scroll to the textarea side (call goRight before to prevent refresh bug) moaiEditor.layout.goRight(); // Setup variables this.left.smooth = true; this.right.smooth = true; this.left.object = moaiEditor.editor.current; this.right.object = moaiEditor.preview; } // ──────────────────────────────────── findMatch (element) { // Search in existing matches const match = moaiEditor.matches.find(element); if (match) return match; // TODO: Recalculate the matches and try again (needed if the user edited the matched line before a preview update) // The code below works right away, but needs testing before enabling it because it will run very seldom, // so a bug there could become hard to track. /* moaiEditor.matches.update(null,null); return moaiEditor.matches.find(element); */ // No matching line was found return null; } // ──────────────────────────────────── copy () { // Store scroll position before switching editors const editor = moaiEditor.editor.current; const scrollTop = editor.scroll.top; const n = editor.watcher.lines.length; for (var i=0; i= scrollTop) break; } var linenum = i; var fraction = (scrollTop-rect.top)/rect.height; fraction = Math.max(0, fraction); // Prevent negative number if gap between document top and first line this.clipboard = linenum + fraction; } paste () { // Restore scroll position after switching editors if (this.clipboard === null) return; // Setup variables this.left.float = this.clipboard; this.left.smooth = false; this.left.object = moaiEditor.editor.current; this.scroll = {type:'paste'}; // Setup the callback this.left.getTargetScroll = this.paste_getTargetScroll; // Start the process this.left.start(); } paste_getTargetScroll() { const linenum = Math.floor(this.float); const fraction = this.float % 1; const rect = this.object.getLineRect(linenum); const scrollTop = rect.top + rect.height * fraction; return scrollTop; } }; // End Class