1/* DokuWiki MoaiEditor Ajax.js file 2 Author : MoaiTools <info@moaitools.org> 3 License : GPL 3 (http://www.gnu.org/licenses/gpl.html) */ 4 5/* This class handles the AJAX requests to the server needed to update the preview. 6*/ 7MoaiEditor.Ajax = class { 8 9 constructor(outer) { 10 11 // Arguments 12 this.outer = outer; 13 14 // Variables 15 this.requestOngoing = false; 16 17 // Objects 18 this.timer = new MoaiEditor.LivePreviewTimer(this); // Live preview mechanism 19 } 20 21 onPreviewBtnClick() { 22 if (!this.requestOngoing) 23 this.request(); 24 } 25 26 request() { 27 28 // Exit if the editor is not enabled and ready 29 if (!moaiEditor.layoutReady) 30 return; 31 32 // Exit if there is an ongoing ajax request 33 if (this.requestOngoing) 34 return; 35 36 // Get the payload 37 const payload = moaiEditor.dirty.onRequest(); 38 39 // Exit if no changes have been made to the text 40 if (payload === null) 41 return; 42 43 // Make the ajax request 44 const url = DOKU_BASE + 'lib/exe/ajax.php'; 45 const rsptype = 'text'; 46 jQuery.post(url, payload, this.onResponse.bind(this), rsptype).fail(this.onFail.bind(this)); 47 48 // Disable the preview button 49 moaiEditor.buttons.preview.mode = 'loading'; 50 51 // Show visual cue of preview-in-progress 52 document.getElementById('moaied__preview').classList.add('moaied-preview-in-progress'); 53 54 // Record the start time 55 this.requestStart = Date.now(); 56 57 // Prevent a new request before this one is finished 58 this.requestOngoing = true; 59 } 60 61 onResponse(data, status) { 62 // Process the ajax response 63 if (status == 'success') { 64 this.domStart = Date.now(); 65 // Unpack response 66 const id = data.substring(0, 5); 67 const html = data.substring(5); 68 // On ID mismatch 69 if (!moaiEditor.dirty.onResponse(id, html)) { 70 this.onFail(null, null, 'ID mismatch in AJAX response.'); 71 this.timer.onFail(); 72 return; 73 } 74 // Record time taken by HTTP request (for live-preview responsiveness adjustment) 75 const elapsed = Date.now() - this.requestStart; 76 this.timer.elapsed1.update(elapsed); 77 // Record time taken by DOM update and other synchronous tasks (for live-preview responsiveness adjustment) 78 requestAnimationFrame(() => { 79 const elapsed = Date.now()-this.domStart; 80 this.timer.elapsed2.update(elapsed); 81 this.timer.onSuccess(); 82 }); 83 // Enable the preview button 84 moaiEditor.buttons.preview.mode = 'normal'; 85 // Remove visual cue of preview-in-progress 86 document.getElementById('moaied__preview').classList.remove('moaied-preview-in-progress'); 87 // Show error 88 } else { 89 console.warn("MoaiEditor Plugin :: Error in the preview request."); 90 moaied.buttons.preview.mode = 'error'; 91 } 92 // Allow a new request 93 this.requestOngoing = false; 94 } 95 96 onFail(xhr, status, error) { 97 this.timer.onFail(); 98 this.requestOngoing = false; 99 moaiEditor.buttons.preview.mode = 'error'; 100 console.warn("MoaiEditor Plugin :: Error in the preview request : "+error); 101 } 102}; // End Class 103 104// ▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆ 105// ▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆ 106 107/* This class triggers automatic preview updates whenever some time 108 has passed since the user stopped typing. It will make previews 109 more often if the server requests and DOM updates are quicker 110 and it will wait longer if they are slower. 111 112 The DOM update is an synchronous (blocking) process which will 113 freeze the browser while it is happening, so this class will 114 take this time into account a lot more than the asynchronous 115 (non blocking) request time which does not interfere with 116 normal user interaction. 117 118 Observations on DOM update time: 119 120 a) The DOM update time can be quite noticeable in very large 121 documents (7K lines) even if the changed part is relatively 122 small. Maybe a whole document reflow is being triggered. 123 b) Firefox has been observed to be several times slower than 124 Chrome when partially updating a preview in a very large 125 document. Testing in different browsers is necessary. 126 127 TODO: check if 'isolation' or another technique can be used to improve 128 performance during partial preview updates in big documents by telling 129 the browser to not touch the unchaged parts. 130 */ 131MoaiEditor.LivePreviewTimer = class { 132 133 constructor (outer) { 134 135 // Arguments 136 this.outer = outer; 137 138 // Objects 139 this.elapsed1 = new MoaiEditor.MovingAverage('Request', 3); // Time taken by HTTP request (non blocking) 140 this.elapsed2 = new MoaiEditor.MovingAverage('Blocking', 4); // Time taken by DOM update (blocking) 141 142 // Variables 143 this.settings = {debug:{request:true, blocking:true}}; 144 this.delay = 500; 145 this.lastTextChange = Date.now(); 146 147 // Interval 148 this.interval = setInterval(this.onInterval.bind(this), 200); 149 } 150 151 reset() { 152 // Called whenever the user makes text changes 153 this.lastTextChange = Date.now(); 154 } 155 156 onInterval() { 157 158 // Return if the editor is not enabled and ready 159 if (!moaiEditor.layoutReady) 160 return; 161 162 // Make AJAX request if the conditions are met 163 const elapsed = Date.now() - this.lastTextChange; 164 if (moaiEditor.buttons.livepreview.mode == 'on') 165 if (moaiEditor.dirty.state.current.changed) 166 if (elapsed > this.delay) 167 this.outer.request(); 168 } 169 170 onSuccess() { 171 // Adjust the responsiveness of the live preview (considering blocking and non-blocking time) 172 const request = this.elapsed1.avg; 173 const dom = this.elapsed2.avg; 174 this.delay = Math.max(400, request/2 + 4*dom); 175 // Debug 176 if (this.settings.debug.request) 177 this.elapsed1.debug(); 178 if (this.settings.debug.blocking) 179 this.elapsed2.debug(); 180 } 181 182 onFail() { 183 this.delay = Math.min(this.delay*2, 30*1000); 184 this.lastTextChange = Date.now(); 185 } 186}; // End Class 187 188// ▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆ 189// ▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆ 190 191MoaiEditor.MovingAverage = class { 192 193 constructor (label, div) { 194 this.val = []; 195 this.avg = 100; 196 this.div = div; 197 this.label = label; 198 } 199 update (value) { 200 this.val.push(value); 201 if (this.val.length > 4) 202 this.val.shift(); 203 this.avg = Math.round(this.val.reduce((sum, currentValue) => sum + currentValue, 0) / this.val.length); 204 } 205 debug () { 206 const line = document.querySelector("#moai__debug div:nth-child("+this.div+")"); 207 if (!line) return; 208 const text = this.label+" avg:"+this.avg+" "+JSON.stringify(this.val); 209 line.innerHTML = text; 210 } 211}; // End Class 212 213 214