1/* DokuWiki MoaiEditor Scroll.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/* This is the umbrella class for scrolling functions: 7 - Scrolling the preview when the user scrolls the editor (keeping it synchronized). 8 - Scrolling the editor when the user clicks an element on the preview. 9 - Scrolling both editor and preview when the user clicks on the table of contents. 10 - Scrolling both editor and preview when the user clicks on the go to top button. 11 - Scrolling both editor and preview when the user clicks on the go to bottom button. 12*/ 13MoaiEditor.Scroll = class { 14 15 constructor(outer) { 16 17 // Arguments 18 this.outer = outer; 19 20 // Variables 21 this.clipboard = null; 22 23 // Objects 24 this.tools = new MoaiEditor.ScrollTools(); 25 this.sync = new MoaiEditor.ScrollSync(this); 26 this.left = new MoaiEditor.ScrollTo(); 27 this.right = new MoaiEditor.ScrollTo(); 28 } 29 // ──────────────────────────────────── 30 init() { 31 this.sync.init(); 32 } 33 // ──────────────────────────────────── 34 halt() { 35 // Stop any scroll loop in progress before switching editors 36 this.left.halt(); 37 this.right.halt(); 38 this.sync.scroller.engaged = false; 39 } 40 // ──────────────────────────────────── 41 // Called whenever the 'ScrollTo' class finishes scrolling 42 onScrollEnd() { 43 // Wait for both sides to end 44 if (!this.left.ended || !this.right.ended) 45 return; 46 // Flash elements 47 var flashleft = null; // null:no flash, true:flash blue, false:flash red 48 var flashright = null; 49 if (this.scroll.type == 'click') { 50 //this.left.success = false; // TODO: Test this condition 51 flashright = this.left.success; 52 if (this.left.success) 53 flashleft = true; 54 } 55 else if (this.scroll.type == 'toc') { 56 if (this.left.success) 57 flashleft = true; 58 if (this.right.success) 59 flashright = true; 60 } 61 moaiEditor.editor.current.flash(flashleft, this.scroll); 62 moaiEditor.preview.flash(flashright, this.scroll); 63 // Hide autoscrolling visual indicator 64 moaiEditor.layout.indicatorScrolling.style.opacity = '0'; 65 // Re-enable external scroll synchronization 66 this.sync.lastScroll = Math.round(moaiEditor.editor.current.scroll.top); 67 moaiEditor.scroll.sync.disabled = false; 68 //setTimeout(function(){}, 100); 69 70 } 71 // ──────────────────────────────────── 72 toTop() { 73 // Setup variables 74 this.smoothScrollInit(); 75 this.scroll = {type:'top'}; 76 // Setup the callbacks 77 this.left.getTargetScroll = function() {return 0;}; 78 this.right.getTargetScroll = function() {return 0;}; 79 // Start the process 80 this.left.start(); 81 this.right.start(); 82 } 83 // ──────────────────────────────────── 84 toBottom() { 85 // Setup variables 86 this.smoothScrollInit(); 87 this.scroll = {type:'bottom'}; 88 // Setup the callbacks 89 this.left.getTargetScroll = function() {return this.object.scroll.max;}; 90 this.right.getTargetScroll = function() {return this.object.scroll.max;}; 91 // Start the process 92 this.left.start(); 93 this.right.start(); 94 } 95 // ──────────────────────────────────── 96 // Scrolls both panes when the user clicks on the table of contents 97 toc(element) { 98 // Exit if the match does not exist (can happen if the user edited the line before the preview updated) 99 const match = this.findMatch(element); 100 if (match === null) { 101 return; 102 } 103 // Setup variables 104 this.smoothScrollInit(); 105 this.left.linenum = match.startline; 106 this.right.element = element; 107 this.scroll = {type:'toc', startline:match.startline, endline:match.endline, element:element}; 108 // Setup the callbacks 109 this.left.getTargetScroll = this.toc_getTargetScroll_left; 110 this.right.getTargetScroll = this.toc_getTargetScroll_right; 111 // Start the process 112 this.left.start(); 113 this.right.start(); 114 } 115 toc_getTargetScroll_left() { 116 return this.object.getLineRect(this.linenum).top; 117 } 118 toc_getTargetScroll_right() { 119 return moaiEditor.scroll.tools.getRectRelativeToParent(this.element).top; 120 } 121 // ──────────────────────────────────── 122 // Scrolls the editor when the user clicks an element on the preview 123 onClick(element) { 124 // Exit if the match does not exist (can happen if the user edited the line before the preview updated) 125 const match = this.findMatch(element); 126 if (match === null) { 127 moaiEditor.preview.flash(false, {element:element}); // TODO: Test this condition 128 return; 129 } 130 // Current right position 131 let pos = element.getBoundingClientRect(); 132 let container = document.querySelector("#moaied__preview").getBoundingClientRect(); 133 let right = (pos.top + pos.bottom)/2 - container.top; 134 // Setup variables 135 this.smoothScrollInit(); 136 this.left.startline = match.startline; 137 this.left.endline = match.endline; 138 this.left.target = right; 139 this.scroll = {type:'click', startline:match.startline, endline:match.endline, element:element}; 140 // Setup the callback 141 this.left.getTargetScroll = this.onClick_getTargetScroll; 142 // Start the process 143 this.left.start(); 144 } 145 onClick_getTargetScroll() { 146 const editor = this.object; 147 // Current left position 148 let container = document.querySelector("#moaied__mirror").getBoundingClientRect(); 149 let top = editor.getLineRect(this.startline, 'viewport').top; 150 let bottom = editor.getLineRect(this.endline, 'viewport').bottom; 151 let left = (top + bottom)/2 - container.top; 152 // Calc final scroll 153 let correction = left - this.target; 154 // Return 155 return editor.scroll.top + correction; 156 } 157 // ──────────────────────────────────── 158 makeElementClickable(element) { 159 160 // Exit if the element already has been made clickable 161 if (element.classList.contains('moaied-preview-header')) 162 return; 163 164 // Add event listener 165 element.addEventListener('click', function(){ moaiEditor.scroll.onClick(this); }); 166 167 // Style the header 168 const headerLvl = Number(element.tagName.substr(1,1)); 169 element.classList.add('moaied-preview-header'); 170 element.title = 'Click to scroll'; 171 172 // Add icon image 173 const h = 24 - 2*headerLvl; 174 const m = 14 - 2*headerLvl; 175 const img = moaiEditor.createHTML('<img class="moaied-scroller-icon" src="lib/plugins/moaieditor/icons/sp_aim.png">'); 176 img.style.setProperty('height', h+'px'); 177 img.style.setProperty('margin-left', m+'px'); 178 element.appendChild(img); 179 } 180 // ──────────────────────────────────── 181 smoothScrollInit () { 182 // In phones, scroll to the textarea side (call goRight before to prevent refresh bug) 183 moaiEditor.layout.goRight(); 184 // Setup variables 185 this.left.smooth = true; 186 this.right.smooth = true; 187 this.left.object = moaiEditor.editor.current; 188 this.right.object = moaiEditor.preview; 189 } 190 // ──────────────────────────────────── 191 findMatch (element) { 192 193 // Search in existing matches 194 const match = moaiEditor.matches.find(element); 195 if (match) 196 return match; 197 198 // TODO: Recalculate the matches and try again (needed if the user edited the matched line before a preview update) 199 // The code below works right away, but needs testing before enabling it because it will run very seldom, 200 // so a bug there could become hard to track. 201 /* 202 moaiEditor.matches.update(null,null); 203 return moaiEditor.matches.find(element); 204 */ 205 206 // No matching line was found 207 return null; 208 } 209 // ──────────────────────────────────── 210 copy () { 211 // Store scroll position before switching editors 212 const editor = moaiEditor.editor.current; 213 const scrollTop = editor.scroll.top; 214 const n = editor.watcher.lines.length; 215 for (var i=0; i<n; i++) { 216 var rect = editor.getLineRect(i); 217 if (rect.bottom >= scrollTop) 218 break; 219 } 220 var linenum = i; 221 var fraction = (scrollTop-rect.top)/rect.height; 222 fraction = Math.max(0, fraction); // Prevent negative number if gap between document top and first line 223 this.clipboard = linenum + fraction; 224 } 225 paste () { 226 // Restore scroll position after switching editors 227 if (this.clipboard === null) 228 return; 229 // Setup variables 230 this.left.float = this.clipboard; 231 this.left.smooth = false; 232 this.left.object = moaiEditor.editor.current; 233 this.scroll = {type:'paste'}; 234 // Setup the callback 235 this.left.getTargetScroll = this.paste_getTargetScroll; 236 // Start the process 237 this.left.start(); 238 } 239 paste_getTargetScroll() { 240 const linenum = Math.floor(this.float); 241 const fraction = this.float % 1; 242 const rect = this.object.getLineRect(linenum); 243 const scrollTop = rect.top + rect.height * fraction; 244 return scrollTop; 245 } 246}; // End Class 247 248 249