/* DokuWiki MoaiEditor Ajax.js file Version : 0.5 (May 5, 2026) Author : MoaiTools License : GPL 3 (http://www.gnu.org/licenses/gpl.html) */ /* This class handles the AJAX requests to the server needed to update the preview. */ MoaiEditor.Ajax = class { constructor(outer) { // Arguments this.outer = outer; // Variables this.requestOngoing = false; // Objects this.timer = new MoaiEditor.LivePreviewTimer(this); // Live preview mechanism } // ──────────────────────────────────── onPreviewBtnClick() { if (!this.requestOngoing) this.request(); } // ──────────────────────────────────── request() { // Exit if the editor is not enabled and ready if (!moaiEditor.layoutReady) return; // Exit if there is an ongoing ajax request if (this.requestOngoing) return; // Get the payload const payload = moaiEditor.dirty.onRequest(); // Exit if no changes have been made to the text if (payload === null) return; // Make the ajax request const url = 'lib/plugins/moaieditor/ajax.php'; const rsptype = 'text'; jQuery.post(url, payload, this.onResponse.bind(this), rsptype).fail(this.onFail.bind(this)); // Disable the preview button moaiEditor.buttons.preview.mode = 'loading'; // Show visual cue of preview-in-progress document.getElementById('moaied__preview').classList.add('moaied-preview-in-progress'); // Record the start time this.requestStart = Date.now(); // Prevent a new request before this one is finished this.requestOngoing = true; } // ──────────────────────────────────── onResponse(data, status) { // Process the ajax response if (status == 'success') { this.domStart = Date.now(); // Unpack response const id = data.substring(0, 5); const md5 = data.substring(5, 37); const html = data.substring(37); // On MD5 mismatch if (md5 !== MD5.generate(html)) { this.onFail(null, null, 'MD5 mismatch in AJAX response.'); this.timer.onFail(); return; } // On ID mismatch if (!moaiEditor.dirty.onResponse(id, html)) { this.onFail(null, null, 'ID mismatch in AJAX response.'); this.timer.onFail(); return; } // Record time taken by HTTP request (for live-preview responsiveness adjustment) const elapsed = Date.now() - this.requestStart; this.timer.elapsed1.update(elapsed); // Record time taken by DOM update and other synchronous tasks (for live-preview responsiveness adjustment) requestAnimationFrame(() => { const elapsed = Date.now()-this.domStart; this.timer.elapsed2.update(elapsed); this.timer.onSuccess(); }); // Enable the preview button moaiEditor.buttons.preview.mode = 'normal'; // Remove visual cue of preview-in-progress document.getElementById('moaied__preview').classList.remove('moaied-preview-in-progress'); // Show error } else { console.warn("MoaiEditor Plugin :: Error in the preview request."); moaied.buttons.preview.mode = 'error'; } // Allow a new request this.requestOngoing = false; } // ──────────────────────────────────── onFail(xhr, status, error) { this.timer.onFail(); this.requestOngoing = false; moaiEditor.buttons.preview.mode = 'error'; console.warn("MoaiEditor Plugin :: Error in the preview request : "+error); } }; // End Class // ▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆ // ▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆ /* This class triggers automatic preview updates whenever some time has passed since the user stopped typing. It will make previews more often if the server requests and DOM updates are quicker and it will wait longer if they are slower. The DOM update is an synchronous (blocking) process which will freeze the browser while it is happening, so this class will take this time into account a lot more than the asynchronous (non blocking) request time which does not interfere with normal user interaction. Observations on DOM update time: a) The DOM update time can be quite noticeable in very large documents (7K lines) even if the changed part is relatively small. Maybe a whole document reflow is being triggered. b) Firefox has been observed to be several times slower than Chrome when partially updating a preview in a very large document. Testing in different browsers is necessary. TODO: check if 'isolation' or another technique can be used to improve performance during partial preview updates in big documents by telling the browser to not touch the unchaged parts. */ MoaiEditor.LivePreviewTimer = class { constructor (outer) { // Arguments this.outer = outer; // Objects this.elapsed1 = new MoaiEditor.MovingAverage('Request', 3); // Time taken by HTTP request (non blocking) this.elapsed2 = new MoaiEditor.MovingAverage('Blocking', 4); // Time taken by DOM update (blocking) // Variables this.settings = {debug:{request:true, blocking:true}}; this.delay = 500; this.lastTextChange = Date.now(); // Interval this.interval = setInterval(this.onInterval.bind(this), 200); } // ──────────────────────────────────── reset() { // Called whenever the user makes text changes this.lastTextChange = Date.now(); } // ──────────────────────────────────── onInterval() { // Return if the editor is not enabled and ready if (!moaiEditor.layoutReady) return; // Make AJAX request if the conditions are met const elapsed = Date.now() - this.lastTextChange; if (moaiEditor.buttons.livepreview.mode == 'on') if (moaiEditor.dirty.state.current.changed) if (elapsed > this.delay) this.outer.request(); } // ──────────────────────────────────── onSuccess() { // Adjust the responsiveness of the live preview (considering blocking and non-blocking time) const request = this.elapsed1.avg; const dom = this.elapsed2.avg; this.delay = Math.max(400, request/2 + 4*dom); // Debug if (this.settings.debug.request) this.elapsed1.debug(); if (this.settings.debug.blocking) this.elapsed2.debug(); } // ──────────────────────────────────── onFail() { this.delay = Math.min(this.delay*2, 30*1000); this.lastTextChange = Date.now(); } }; // End Class // ▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆ // ▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆ MoaiEditor.MovingAverage = class { constructor (label, div) { this.val = []; this.avg = 100; this.div = div; this.label = label; } update (value) { this.val.push(value); if (this.val.length > 4) this.val.shift(); this.avg = Math.round(this.val.reduce((sum, currentValue) => sum + currentValue, 0) / this.val.length); } debug () { const line = document.querySelector("#moai__debug div:nth-child("+this.div+")"); if (!line) return; const text = this.label+" avg:"+this.avg+" "+JSON.stringify(this.val); line.innerHTML = text; } }; // End Class