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