1/* DokuWiki MoaiEditor Scroll.js file 2 Version : 0.5 (May 5, 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 center position 131 let pos = element.getBoundingClientRect(); 132 let right = (pos.top + pos.bottom)/2; 133 // Setup variables 134 this.smoothScrollInit(); 135 this.left.startline = match.startline; 136 this.left.endline = match.endline; 137 this.left.target = right; 138 this.scroll = {type:'click', startline:match.startline, endline:match.endline, element:element}; 139 // Setup the callback 140 this.left.getTargetScroll = this.onClick_getTargetScroll; 141 // Start the process 142 this.left.start(); 143 } 144 onClick_getTargetScroll() { 145 const editor = this.object; 146 // Current left center position 147 let top = editor.getLineRect(this.startline, 'viewport').top; 148 let bottom = editor.getLineRect(this.endline, 'viewport').bottom; 149 let left = (top + bottom)/2; 150 // Calc final scroll 151 let correction = left - this.target; 152 // Return 153 return editor.scroll.top + correction; 154 } 155 // ──────────────────────────────────── 156 makeElementClickable(element) { 157 158 // Exit if the element already has been made clickable 159 if (element.classList.contains('moaied-preview-header')) 160 return; 161 162 // Add event listener 163 element.addEventListener('click', function(){ moaiEditor.scroll.onClick(this); }); 164 165 // Style the header 166 const headerLvl = Number(element.tagName.substr(1,1)); 167 element.classList.add('moaied-preview-header'); 168 element.title = 'Click to scroll'; 169 170 // Add icon image 171 const h = 24 - 2*headerLvl; 172 const m = 14 - 2*headerLvl; 173 const img = moaiEditor.createHTML('<img class="moaied-scroller-icon" src="lib/plugins/moaieditor/icons/sp_aim.png">'); 174 img.style.setProperty('height', h+'px'); 175 img.style.setProperty('margin-left', m+'px'); 176 element.appendChild(img); 177 } 178 // ──────────────────────────────────── 179 smoothScrollInit () { 180 // In phones, scroll to the textarea side (call goRight before to prevent refresh bug) 181 moaiEditor.layout.goRight(); 182 // Setup variables 183 this.left.smooth = true; 184 this.right.smooth = true; 185 this.left.object = moaiEditor.editor.current; 186 this.right.object = moaiEditor.preview; 187 } 188 // ──────────────────────────────────── 189 findMatch (element) { 190 191 // Search in existing matches 192 const match = moaiEditor.matches.find(element); 193 if (match) 194 return match; 195 196 // TODO: Recalculate the matches and try again (needed if the user edited the matched line before a preview update) 197 // The code below works right away, but needs testing before enabling it because it will run very seldom, 198 // so a bug there could become hard to track. 199 /* 200 moaiEditor.matches.update(null,null); 201 return moaiEditor.matches.find(element); 202 */ 203 204 // No matching line was found 205 return null; 206 } 207 // ──────────────────────────────────── 208 copy () { 209 // Store scroll position before switching editors 210 const editor = moaiEditor.editor.current; 211 const scrollTop = editor.scroll.top; 212 const n = editor.watcher.lines.length; 213 for (var i=0; i<n; i++) { 214 var rect = editor.getLineRect(i); 215 if (rect.bottom >= scrollTop) 216 break; 217 } 218 var linenum = i; 219 var fraction = (scrollTop-rect.top)/rect.height; 220 fraction = Math.max(0, fraction); // Prevent negative number if gap between document top and first line 221 this.clipboard = linenum + fraction; 222 } 223 paste () { 224 // Restore scroll position after switching editors 225 if (this.clipboard === null) 226 return; 227 // Setup variables 228 this.left.float = this.clipboard; 229 this.left.smooth = false; 230 this.left.object = moaiEditor.editor.current; 231 this.scroll = {type:'paste'}; 232 // Setup the callback 233 this.left.getTargetScroll = this.paste_getTargetScroll; 234 // Start the process 235 this.left.start(); 236 } 237 paste_getTargetScroll() { 238 const linenum = Math.floor(this.float); 239 const fraction = this.float % 1; 240 const rect = this.object.getLineRect(linenum); 241 const scrollTop = rect.top + rect.height * fraction; 242 return scrollTop; 243 } 244}; // End Class 245 246 247