Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

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