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