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