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