1/* DokuWiki MoaiEditor Scroll_sync.js file 2 Version : 0.5a (May 6, 2026) 3 Author : MoaiTools <info@moaitools.org> 4 License : GPL 3 (http://www.gnu.org/licenses/gpl.html) */ 5 6/* Scrolls the preview (keeping it synchronized with the editor). 7 8*/ 9MoaiEditor.ScrollSync = class { 10 11 constructor(outer) { 12 13 // Variables 14 this.disabled = false; // synchronization can be disabled temporarily when other processes want to take control 15 } 16 // ──────────────────────────────────── 17 init() { 18 this.scroller = new MoaiEditor.Scroller(this); 19 } 20 // ──────────────────────────────────── 21 get isEnabled() { 22 if (this.disabled || moaiEditor.buttons.autoscroll.mode == 'off' || !moaiEditor.layoutReady) 23 return false; 24 return true; 25 } 26 // ──────────────────────────────────── 27 // This function is called whenever the user scrolls the editor side 28 onScroll(event) { 29 30 // Exit if disabled by the user, by other process, or if the editor is not ready 31 if (!this.isEnabled) 32 return; 33 34 // Exit if the editor scroll has not changed (to avoid interfering right after another process has done scrolling) 35 const scroll = moaiEditor.editor.current.scroll.top; 36 const sameScroll = Math.abs(scroll - this.lastScroll) <= 2; 37 if (sameScroll) 38 return; 39 40 // Synchronize the scroll of the preview 41 const target = this.calcScroll().right; 42 if (target !== null) { 43 this.scroller.target = target; 44 this.scroller.engaged = true; 45 } 46 // Remember last scroll position 47 this.lastScroll = scroll; 48 } 49 // ──────────────────────────────────── 50 calcScroll() { 51 52 // There should be at least one match 53 var matches = moaiEditor.matches.matches; 54 if (matches.length == 0) 55 return null; 56 57 // Get the editor side scroll position 58 const leftScroll = moaiEditor.editor.current.scroll.top; 59 60 // Get the preview container top position relative to the screen 61 this.container = moaiEditor.preview.container; 62 this.containerTop = this.container.getBoundingClientRect().top; 63 64 // Get the scroll points above and below our current scroll position 65 var p1, p2, match; 66 for (var i = 0; i < matches.length; i++) { 67 if (matches[i].scroll === null) 68 continue; 69 match = matches[i]; 70 if (match.scroll.top > leftScroll) { 71 p1 = this.getPoint(i-1, 'bottom'); 72 p2 = this.getPoint(i, 'top'); 73 break; 74 } 75 if (match.scroll.bottom > leftScroll) { 76 p1 = this.getPoint(i, 'top'); 77 p2 = this.getPoint(i, 'bottom'); 78 break; 79 } 80 } 81 // Exit if no scroll points where found 82 if (match === undefined) { 83 return null; 84 } 85 86 87 // Build bottom scroll point if needed 88 if (p1 === undefined) { 89 p1 = this.getPoint(i-1, 'bottom'); 90 p2 = this.getPoint(i, 'top'); 91 } 92 93 // Get the normalized scroll progress between both scrollpoints on the editor side 94 const normalized = (leftScroll - p1.left) / (p2.left - p1.left); 95 96 // Compute the corresponding scroll on the preview side 97 const right = p1.right + normalized * (p2.right - p1.right); 98 99 // Debug line 100 const scroll = {p1:p1, p2:p2, right:right}; 101 this.debugLine(scroll); 102 103 // Return 104 return scroll; 105 } 106 // ──────────────────────────────────── 107 getPoint(i, side) { 108 var type, left, right; 109 var linenum = side.toUpperCase(); 110 // Build a TOP scrollpoint if needed 111 if (i < 0) { 112 type = 'TOP'; 113 left = 0; 114 right = 0; 115 // Build a BOTTOM scrollpoint if needed 116 } else if (i >= moaiEditor.matches.matches.length) { 117 type = 'BOTTOM'; 118 left = moaiEditor.editor.current.scroll.max; 119 right = this.container.scrollHeight; 120 // Get the scrollpoint from the match 121 } else { 122 const match = moaiEditor.matches.matches[i]; 123 type = match.type, 124 left = match.scroll[side], 125 right = match.handle.getBoundingClientRect()[side] - this.containerTop + this.container.scrollTop; 126 linenum = match.startline; 127 if (side == 'bottom') 128 linenum = match.endline; 129 } 130 // Pack and return 131 return {linenum:linenum, type:type, left:left, right:right, side:side}; 132 } 133 // ──────────────────────────────────── 134 debugLine(scroll) { 135 // Exit if debugline is not enabled 136 const line = document.querySelector("#moai__debug div:nth-child(2)"); 137 if (!line) return; 138 139 // Debug line 140 const p1 = scroll.p1; 141 const p2 = scroll.p2; 142 const txt1 = "<"+scroll.p1.type+"> "+p1.side+" L"+p1.linenum; 143 const txt2 = "<"+scroll.p2.type+"> "+p2.side+" L"+p2.linenum; 144 const text = txt1+" ⇔ "+txt2; 145 line.textContent = text; 146 } 147}; // End Class 148 149// ▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆ 150// ▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆ 151 152MoaiEditor.Scroller = class { 153 154 constructor(outer) { 155 this.outer = outer; 156 this.i = 0; 157 this.target = null; 158 this._engaged = false; 159 this.onFrame(); 160 } 161 get engaged() { 162 return this._engaged; 163 } 164 set engaged(boolean) { 165 this._engaged = boolean; 166 this.lastScroll = moaiEditor.preview.scroll.top; 167 } 168 onFrame() { 169 requestAnimationFrame(this.onFrame.bind(this)); 170 //this.i += 1; if (this.i % 3 !== 0) return; // Test other framerates 171 const dt = Date.now() - this.lastTime; 172 this.lastTime = Date.now(); 173 if (!this.engaged || !this.outer.isEnabled || this.target === null) 174 return; 175 // Disengage if the user scrolled the preview 176 const scroll = moaiEditor.preview.scroll.top; 177 const userScrolled = Math.abs(scroll - this.lastScroll) > 2; 178 if (userScrolled) { 179 this.engaged = false; 180 return; 181 } 182 // Scroll the preview 183 const distance = this.target - scroll; 184 const increment = 1*distance*dt/160; 185 moaiEditor.preview.scroll.top = scroll + increment; 186 this.lastScroll = scroll + increment; 187 } 188}; // End Class 189 190 191