Proyectos de Subversion Moodle

Rev

Ir a la última revisión | | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
// This file is part of Moodle - http://moodle.org/
2
//
3
// Moodle is free software: you can redistribute it and/or modify
4
// it under the terms of the GNU General Public License as published by
5
// the Free Software Foundation, either version 3 of the License, or
6
// (at your option) any later version.
7
//
8
// Moodle is distributed in the hope that it will be useful,
9
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
// GNU General Public License for more details.
12
//
13
// You should have received a copy of the GNU General Public License
14
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
15
 
16
 
17
/**
18
 * Auto-save functionality for during quiz attempts.
19
 *
20
 * @module moodle-mod_quiz-autosave
21
 */
22
 
23
/**
24
 * Auto-save functionality for during quiz attempts.
25
 *
26
 * @class M.mod_quiz.autosave
27
 */
28
 
29
M.mod_quiz = M.mod_quiz || {};
30
M.mod_quiz.autosave = {
31
    /**
32
     * The amount of time (in milliseconds) to wait between TinyMCE detections.
33
     *
34
     * @property TINYMCE_DETECTION_DELAY
35
     * @type Number
36
     * @default 500
37
     * @private
38
     */
39
    TINYMCE_DETECTION_DELAY:  500,
40
 
41
    /**
42
     * The number of times to try redetecting TinyMCE.
43
     *
44
     * @property TINYMCE_DETECTION_REPEATS
45
     * @type Number
46
     * @default 20
47
     * @private
48
     */
49
    TINYMCE_DETECTION_REPEATS: 20,
50
 
51
    /**
52
     * The delay (in milliseconds) between checking hidden input fields.
53
     *
54
     * @property WATCH_HIDDEN_DELAY
55
     * @type Number
56
     * @default 1000
57
     * @private
58
     */
59
    WATCH_HIDDEN_DELAY:      1000,
60
 
61
    /**
62
     * The number of failures to ignore before notifying the user.
63
     *
64
     * @property FAILURES_BEFORE_NOTIFY
65
     * @type Number
66
     * @default 1
67
     * @private
68
     */
69
    FAILURES_BEFORE_NOTIFY:     1,
70
 
71
    /**
72
     * The value to use when resetting the successful save counter.
73
     *
74
     * @property FIRST_SUCCESSFUL_SAVE
75
     * @static
76
     * @type Number
77
     * @default -1
78
     * @private
79
     */
80
    FIRST_SUCCESSFUL_SAVE:     -1,
81
 
82
    /**
83
     * The selectors used throughout this class.
84
     *
85
     * @property SELECTORS
86
     * @private
87
     * @type Object
88
     * @static
89
     */
90
    SELECTORS: {
91
        QUIZ_FORM:             '#responseform',
92
        VALUE_CHANGE_ELEMENTS: 'input, textarea, [contenteditable="true"]',
93
        CHANGE_ELEMENTS:       'input, select',
94
        HIDDEN_INPUTS:         'input[type=hidden]',
95
        CONNECTION_ERROR:      '#connection-error',
96
        CONNECTION_OK:         '#connection-ok'
97
    },
98
 
99
    /**
100
     * The script which handles the autosaves.
101
     *
102
     * @property AUTOSAVE_HANDLER
103
     * @type String
104
     * @default M.cfg.wwwroot + '/mod/quiz/autosave.ajax.php'
105
     * @private
106
     */
107
    AUTOSAVE_HANDLER: M.cfg.wwwroot + '/mod/quiz/autosave.ajax.php',
108
 
109
    /**
110
     * The delay (in milliseconds) between a change being made, and it being auto-saved.
111
     *
112
     * @property delay
113
     * @type Number
114
     * @default 120000
115
     * @private
116
     */
117
    delay: 120000,
118
 
119
    /**
120
     * A Node reference to the form we are monitoring.
121
     *
122
     * @property form
123
     * @type Node
124
     * @default null
125
     */
126
    form: null,
127
 
128
    /**
129
     * Whether the form has been modified since the last save started.
130
     *
131
     * @property dirty
132
     * @type boolean
133
     * @default false
134
     */
135
    dirty: false,
136
 
137
    /**
138
     * Timer object for the delay between form modifaction and the save starting.
139
     *
140
     * @property delay_timer
141
     * @type Object
142
     * @default null
143
     * @private
144
     */
145
    delay_timer: null,
146
 
147
    /**
148
     * Y.io transaction for the save ajax request.
149
     *
150
     * @property save_transaction
151
     * @type object
152
     * @default null
153
     */
154
    save_transaction: null,
155
 
156
    /**
157
     * Failed saves count.
158
     *
159
     * @property savefailures
160
     * @type Number
161
     * @default 0
162
     * @private
163
     */
164
    savefailures: 0,
165
 
166
    /**
167
     * Properly bound key change handler.
168
     *
169
     * @property editor_change_handler
170
     * @type EventHandle
171
     * @default null
172
     * @private
173
     */
174
    editor_change_handler: null,
175
 
176
    /**
177
     * Record of the value of all the hidden fields, last time they were checked.
178
     *
179
     * @property hidden_field_values
180
     * @type Object
181
     * @default {}
182
     */
183
    hidden_field_values: {},
184
 
185
    /**
186
     * Initialise the autosave code.
187
     *
188
     * @method init
189
     * @param {Number} delay the delay, in seconds, between a change being detected, and
190
     * a save happening.
191
     */
192
    init: function(delay) {
193
        this.form = Y.one(this.SELECTORS.QUIZ_FORM);
194
        if (!this.form) {
195
            Y.log('No response form found. Why did you try to set up autosave?', 'debug', 'moodle-mod_quiz-autosave');
196
            return;
197
        }
198
 
199
        this.delay = delay * 1000;
200
 
201
        this.form.delegate('valuechange', this.value_changed, this.SELECTORS.VALUE_CHANGE_ELEMENTS, this);
202
        this.form.delegate('change', this.value_changed, this.SELECTORS.CHANGE_ELEMENTS, this);
203
        this.form.on('submit', this.stop_autosaving, this);
204
 
205
        require(['core_form/events'], function(FormEvent) {
206
            window.addEventListener(FormEvent.eventTypes.uploadChanged, this.value_changed.bind(this));
207
        }.bind(this));
208
 
209
        this.init_tinymce(this.TINYMCE_DETECTION_REPEATS);
210
 
211
        this.save_hidden_field_values();
212
        this.watch_hidden_fields();
213
    },
214
 
215
    save_hidden_field_values: function() {
216
        this.form.all(this.SELECTORS.HIDDEN_INPUTS).each(function(hidden) {
217
            var name = hidden.get('name');
218
            if (!name) {
219
                return;
220
            }
221
            this.hidden_field_values[name] = hidden.get('value');
222
        }, this);
223
    },
224
 
225
    watch_hidden_fields: function() {
226
        this.detect_hidden_field_changes();
227
        Y.later(this.WATCH_HIDDEN_DELAY, this, this.watch_hidden_fields);
228
    },
229
 
230
    detect_hidden_field_changes: function() {
231
        this.form.all(this.SELECTORS.HIDDEN_INPUTS).each(function(hidden) {
232
            var name = hidden.get('name'),
233
                value = hidden.get('value');
234
            if (!name) {
235
                return;
236
            }
237
            if (!(name in this.hidden_field_values) || value !== this.hidden_field_values[name]) {
238
                this.hidden_field_values[name] = value;
239
                this.value_changed({target: hidden});
240
            }
241
        }, this);
242
    },
243
 
244
    /**
245
     * Initialise watching of TinyMCE specifically.
246
     *
247
     * Because TinyMCE might load slowly, and after us, we need to keep
248
     * trying, until we detect TinyMCE is there, or enough time has passed.
249
     * This is based on the TINYMCE_DETECTION_DELAY and
250
     * TINYMCE_DETECTION_REPEATS properties.
251
     *
252
     *
253
     * @method init_tinymce
254
     * @param {Number} repeatcount The number of attempts made so far.
255
     */
256
    init_tinymce: function(repeatcount) {
257
        if (typeof window.tinyMCE === 'undefined') {
258
            if (repeatcount > 0) {
259
                Y.later(this.TINYMCE_DETECTION_DELAY, this, this.init_tinymce, [repeatcount - 1]);
260
            } else {
261
                Y.log('Gave up looking for TinyMCE.', 'debug', 'moodle-mod_quiz-autosave');
262
            }
263
            return;
264
        }
265
 
266
        Y.log('Found TinyMCE.', 'debug', 'moodle-mod_quiz-autosave');
267
        this.editor_change_handler = Y.bind(this.editor_changed, this);
268
        if (window.tinyMCE.onAddEditor) {
269
            window.tinyMCE.onAddEditor.add(Y.bind(this.init_tinymce_editor, this));
270
        } else if (window.tinyMCE.on) {
271
            var startSaveTimer = this.start_save_timer_if_necessary.bind(this);
272
            window.tinyMCE.on('AddEditor', function(event) {
273
                event.editor.on('Change Undo Redo keydown', startSaveTimer);
274
            });
275
            // One or more editors might already have been added, so we have to attach
276
            // the event handlers to these as well.
277
            window.tinyMCE.get().forEach(function(editor) {
278
                editor.on('Change Undo Redo keydown', startSaveTimer);
279
            });
280
         }
281
    },
282
 
283
    /**
284
     * Initialise watching of a specific TinyMCE editor.
285
     *
286
     * @method init_tinymce_editor
287
     * @param {EventFacade} e
288
     * @param {Object} editor The TinyMCE editor object
289
     */
290
    init_tinymce_editor: function(e, editor) {
291
        Y.log('Found TinyMCE editor ' + editor.id + '.', 'debug', 'moodle-mod_quiz-autosave');
292
        editor.onChange.add(this.editor_change_handler);
293
        editor.onRedo.add(this.editor_change_handler);
294
        editor.onUndo.add(this.editor_change_handler);
295
        editor.onKeyDown.add(this.editor_change_handler);
296
    },
297
 
298
    value_changed: function(e) {
299
        var name = e.target.getAttribute('name');
300
        if (name === 'thispage' || name === 'scrollpos' || (name && name.match(/_:flagged$/))) {
301
            return; // Not interesting.
302
        }
303
 
304
        // Fallback to the ID when the name is not present (in the case of content editable).
305
        name = name || '#' + e.target.getAttribute('id');
306
        Y.log('Detected a value change in element ' + name + '.', 'debug', 'moodle-mod_quiz-autosave');
307
        this.start_save_timer_if_necessary();
308
    },
309
 
310
    editor_changed: function(editor) {
311
        Y.log('Detected a value change in editor ' + editor.id + '.', 'debug', 'moodle-mod_quiz-autosave');
312
        this.start_save_timer_if_necessary();
313
    },
314
 
315
    start_save_timer_if_necessary: function() {
316
        this.dirty = true;
317
 
318
        if (this.delay_timer || this.save_transaction) {
319
            // Already counting down or daving.
320
            return;
321
        }
322
 
323
        this.start_save_timer();
324
    },
325
 
326
    start_save_timer: function() {
327
        this.cancel_delay();
328
        this.delay_timer = Y.later(this.delay, this, this.save_changes);
329
    },
330
 
331
    cancel_delay: function() {
332
        if (this.delay_timer && this.delay_timer !== true) {
333
            this.delay_timer.cancel();
334
        }
335
        this.delay_timer = null;
336
    },
337
 
338
    save_changes: function() {
339
        this.cancel_delay();
340
        this.dirty = false;
341
 
342
        if (this.is_time_nearly_over()) {
343
            Y.log('No more saving, time is nearly over.', 'debug', 'moodle-mod_quiz-autosave');
344
            this.stop_autosaving();
345
            return;
346
        }
347
 
348
        Y.log('Doing a save.', 'debug', 'moodle-mod_quiz-autosave');
349
        if (typeof window.tinyMCE !== 'undefined') {
350
            window.tinyMCE.triggerSave();
351
        }
352
 
353
        // YUI io.form incorrectly (in my opinion) sends the value of all submit
354
        // buttons in the ajax request. We don't want any submit buttons.
355
        // Therefore, temporarily change the type.
356
        // (Yes, this is a nasty hack. One day this will be re-written as AMD, hopefully).
357
        var allsubmitbuttons = this.form.all('input[type=submit], button[type=submit]');
358
        allsubmitbuttons.setAttribute('type', 'button');
359
 
360
        this.save_transaction = Y.io(this.AUTOSAVE_HANDLER, {
361
            method:  'POST',
362
            form:    {id: this.form},
363
            on:      {
364
                success: this.save_done,
365
                failure: this.save_failed
366
            },
367
            context: this
368
        });
369
 
370
        // Change the button types back.
371
        allsubmitbuttons.setAttribute('type', 'submit');
372
    },
373
 
374
    save_done: function(transactionid, response) {
375
        var autosavedata = JSON.parse(response.responseText);
376
        if (autosavedata.status !== 'OK') {
377
            // Because IIS is useless, Moodle can't send proper HTTP response
378
            // codes, so we have to detect failures manually.
379
            this.save_failed(transactionid, response);
380
            return;
381
        }
382
 
383
        if (typeof autosavedata.timeleft !== 'undefined') {
384
            Y.log('Updating timer: ' + autosavedata.timeleft + ' seconds remain.', 'debug', 'moodle-mod_quiz-timer');
385
            M.mod_quiz.timer.updateEndTime(autosavedata.timeleft);
386
        }
387
 
388
        this.update_saved_time_display();
389
 
390
        Y.log('Save completed.', 'debug', 'moodle-mod_quiz-autosave');
391
        this.save_transaction = null;
392
 
393
        if (this.dirty) {
394
            Y.log('Dirty after save.', 'debug', 'moodle-mod_quiz-autosave');
395
            this.start_save_timer();
396
        }
397
 
398
        if (this.savefailures > 0) {
399
            Y.one(this.SELECTORS.CONNECTION_ERROR).hide();
400
            Y.one(this.SELECTORS.CONNECTION_OK).show();
401
            this.savefailures = this.FIRST_SUCCESSFUL_SAVE;
402
        } else if (this.savefailures === this.FIRST_SUCCESSFUL_SAVE) {
403
            Y.one(this.SELECTORS.CONNECTION_OK).hide();
404
            this.savefailures = 0;
405
        }
406
    },
407
 
408
    save_failed: function() {
409
        Y.log('Save failed.', 'debug', 'moodle-mod_quiz-autosave');
410
        this.save_transaction = null;
411
 
412
        // We want to retry soon.
413
        this.start_save_timer();
414
 
415
        this.savefailures = Math.max(1, this.savefailures + 1);
416
        if (this.savefailures === this.FAILURES_BEFORE_NOTIFY) {
417
            Y.one(this.SELECTORS.CONNECTION_ERROR).show();
418
            Y.one(this.SELECTORS.CONNECTION_OK).hide();
419
        }
420
    },
421
 
422
    /**
423
     * Inform the user that their answers have been saved.
424
     *
425
     * @method update_saved_time_display
426
     */
427
    update_saved_time_display: function() {
428
        // We fetch the current language's preferred time format from the language pack.
429
        var timeFormat = M.util.get_string('strftimedatetimeshortaccurate', 'langconfig');
430
        var message = M.util.get_string('lastautosave', 'quiz', Y.Date.format(new Date(), {'format': timeFormat}));
431
 
432
        var infoDiv = Y.one('#mod_quiz_navblock .othernav .autosave_info');
433
        infoDiv.set('text', message);
434
        infoDiv.show();
435
    },
436
 
437
    is_time_nearly_over: function() {
438
        return M.mod_quiz.timer && M.mod_quiz.timer.endtime &&
439
                (new Date().getTime() + 2 * this.delay) > M.mod_quiz.timer.endtime;
440
    },
441
 
442
    stop_autosaving: function() {
443
        this.cancel_delay();
444
        this.delay_timer = true;
445
        if (this.save_transaction) {
446
            this.save_transaction.abort();
447
        }
448
    }
449
};