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
            return;
198
        }
199
 
200
        this.delay = delay * 1000;
201
 
202
        this.form.delegate('valuechange', this.value_changed, this.SELECTORS.VALUE_CHANGE_ELEMENTS, this);
203
        this.form.delegate('change', this.value_changed, this.SELECTORS.CHANGE_ELEMENTS, this);
204
        this.form.on('submit', this.stop_autosaving, this);
205
 
206
        require(['core_form/events'], function(FormEvent) {
207
            window.addEventListener(FormEvent.eventTypes.uploadChanged, this.value_changed.bind(this));
208
        }.bind(this));
209
 
210
        this.init_tinymce(this.TINYMCE_DETECTION_REPEATS);
211
 
212
        this.save_hidden_field_values();
213
        this.watch_hidden_fields();
214
    },
215
 
216
    save_hidden_field_values: function() {
217
        this.form.all(this.SELECTORS.HIDDEN_INPUTS).each(function(hidden) {
218
            var name = hidden.get('name');
219
            if (!name) {
220
                return;
221
            }
222
            this.hidden_field_values[name] = hidden.get('value');
223
        }, this);
224
    },
225
 
226
    watch_hidden_fields: function() {
227
        this.detect_hidden_field_changes();
228
        Y.later(this.WATCH_HIDDEN_DELAY, this, this.watch_hidden_fields);
229
    },
230
 
231
    detect_hidden_field_changes: function() {
232
        this.form.all(this.SELECTORS.HIDDEN_INPUTS).each(function(hidden) {
233
            var name = hidden.get('name'),
234
                value = hidden.get('value');
235
            if (!name) {
236
                return;
237
            }
238
            if (!(name in this.hidden_field_values) || value !== this.hidden_field_values[name]) {
239
                this.hidden_field_values[name] = value;
240
                this.value_changed({target: hidden});
241
            }
242
        }, this);
243
    },
244
 
245
    /**
246
     * Initialise watching of TinyMCE specifically.
247
     *
248
     * Because TinyMCE might load slowly, and after us, we need to keep
249
     * trying, until we detect TinyMCE is there, or enough time has passed.
250
     * This is based on the TINYMCE_DETECTION_DELAY and
251
     * TINYMCE_DETECTION_REPEATS properties.
252
     *
253
     *
254
     * @method init_tinymce
255
     * @param {Number} repeatcount The number of attempts made so far.
256
     */
257
    init_tinymce: function(repeatcount) {
258
        if (typeof window.tinyMCE === 'undefined') {
259
            if (repeatcount > 0) {
260
                Y.later(this.TINYMCE_DETECTION_DELAY, this, this.init_tinymce, [repeatcount - 1]);
261
            } else {
262
            }
263
            return;
264
        }
265
 
266
        this.editor_change_handler = Y.bind(this.editor_changed, this);
267
        if (window.tinyMCE.onAddEditor) {
268
            window.tinyMCE.onAddEditor.add(Y.bind(this.init_tinymce_editor, this));
269
        } else if (window.tinyMCE.on) {
270
            var startSaveTimer = this.start_save_timer_if_necessary.bind(this);
271
            window.tinyMCE.on('AddEditor', function(event) {
272
                event.editor.on('Change Undo Redo keydown', startSaveTimer);
273
            });
274
            // One or more editors might already have been added, so we have to attach
275
            // the event handlers to these as well.
276
            window.tinyMCE.get().forEach(function(editor) {
277
                editor.on('Change Undo Redo keydown', startSaveTimer);
278
            });
279
         }
280
    },
281
 
282
    /**
283
     * Initialise watching of a specific TinyMCE editor.
284
     *
285
     * @method init_tinymce_editor
286
     * @param {EventFacade} e
287
     * @param {Object} editor The TinyMCE editor object
288
     */
289
    init_tinymce_editor: function(e, editor) {
290
        editor.onChange.add(this.editor_change_handler);
291
        editor.onRedo.add(this.editor_change_handler);
292
        editor.onUndo.add(this.editor_change_handler);
293
        editor.onKeyDown.add(this.editor_change_handler);
294
    },
295
 
296
    value_changed: function(e) {
297
        var name = e.target.getAttribute('name');
298
        if (name === 'thispage' || name === 'scrollpos' || (name && name.match(/_:flagged$/))) {
299
            return; // Not interesting.
300
        }
301
 
302
        // Fallback to the ID when the name is not present (in the case of content editable).
303
        name = name || '#' + e.target.getAttribute('id');
304
        this.start_save_timer_if_necessary();
305
    },
306
 
307
    editor_changed: function(editor) {
308
        this.start_save_timer_if_necessary();
309
    },
310
 
311
    start_save_timer_if_necessary: function() {
312
        this.dirty = true;
313
 
314
        if (this.delay_timer || this.save_transaction) {
315
            // Already counting down or daving.
316
            return;
317
        }
318
 
319
        this.start_save_timer();
320
    },
321
 
322
    start_save_timer: function() {
323
        this.cancel_delay();
324
        this.delay_timer = Y.later(this.delay, this, this.save_changes);
325
    },
326
 
327
    cancel_delay: function() {
328
        if (this.delay_timer && this.delay_timer !== true) {
329
            this.delay_timer.cancel();
330
        }
331
        this.delay_timer = null;
332
    },
333
 
334
    save_changes: function() {
335
        this.cancel_delay();
336
        this.dirty = false;
337
 
338
        if (this.is_time_nearly_over()) {
339
            this.stop_autosaving();
340
            return;
341
        }
342
 
343
        if (typeof window.tinyMCE !== 'undefined') {
344
            window.tinyMCE.triggerSave();
345
        }
346
 
347
        // YUI io.form incorrectly (in my opinion) sends the value of all submit
348
        // buttons in the ajax request. We don't want any submit buttons.
349
        // Therefore, temporarily change the type.
350
        // (Yes, this is a nasty hack. One day this will be re-written as AMD, hopefully).
351
        var allsubmitbuttons = this.form.all('input[type=submit], button[type=submit]');
352
        allsubmitbuttons.setAttribute('type', 'button');
353
 
354
        this.save_transaction = Y.io(this.AUTOSAVE_HANDLER, {
355
            method:  'POST',
356
            form:    {id: this.form},
357
            on:      {
358
                success: this.save_done,
359
                failure: this.save_failed
360
            },
361
            context: this
362
        });
363
 
364
        // Change the button types back.
365
        allsubmitbuttons.setAttribute('type', 'submit');
366
    },
367
 
368
    save_done: function(transactionid, response) {
369
        var autosavedata = JSON.parse(response.responseText);
370
        if (autosavedata.status !== 'OK') {
371
            // Because IIS is useless, Moodle can't send proper HTTP response
372
            // codes, so we have to detect failures manually.
373
            this.save_failed(transactionid, response);
374
            return;
375
        }
376
 
377
        if (typeof autosavedata.timeleft !== 'undefined') {
378
            M.mod_quiz.timer.updateEndTime(autosavedata.timeleft);
379
        }
380
 
381
        this.update_saved_time_display();
382
 
383
        this.save_transaction = null;
384
 
385
        if (this.dirty) {
386
            this.start_save_timer();
387
        }
388
 
389
        if (this.savefailures > 0) {
390
            Y.one(this.SELECTORS.CONNECTION_ERROR).hide();
391
            Y.one(this.SELECTORS.CONNECTION_OK).show();
392
            this.savefailures = this.FIRST_SUCCESSFUL_SAVE;
393
        } else if (this.savefailures === this.FIRST_SUCCESSFUL_SAVE) {
394
            Y.one(this.SELECTORS.CONNECTION_OK).hide();
395
            this.savefailures = 0;
396
        }
397
    },
398
 
399
    save_failed: function() {
400
        this.save_transaction = null;
401
 
402
        // We want to retry soon.
403
        this.start_save_timer();
404
 
405
        this.savefailures = Math.max(1, this.savefailures + 1);
406
        if (this.savefailures === this.FAILURES_BEFORE_NOTIFY) {
407
            Y.one(this.SELECTORS.CONNECTION_ERROR).show();
408
            Y.one(this.SELECTORS.CONNECTION_OK).hide();
409
        }
410
    },
411
 
412
    /**
413
     * Inform the user that their answers have been saved.
414
     *
415
     * @method update_saved_time_display
416
     */
417
    update_saved_time_display: function() {
418
        // We fetch the current language's preferred time format from the language pack.
11 efrain 419
        require(['core/user_date', 'core/notification'], function(UserDate, Notification) {
420
            UserDate.get([{
421
                timestamp: Math.floor(Date.now() / 1000),
422
                format: M.util.get_string('strftimedatetimeshortaccurate', 'langconfig'),
423
            }]).then(function(dateStrs) {
424
                var infoDiv = Y.one('#mod_quiz_navblock .othernav .autosave_info');
425
                infoDiv.set('text', M.util.get_string('lastautosave', 'quiz', dateStrs[0]));
426
                infoDiv.show();
427
                return;
428
            }).catch(Notification.exception);
429
        });
1 efrain 430
    },
431
 
432
    is_time_nearly_over: function() {
433
        return M.mod_quiz.timer && M.mod_quiz.timer.endtime &&
434
                (new Date().getTime() + 2 * this.delay) > M.mod_quiz.timer.endtime;
435
    },
436
 
437
    stop_autosaving: function() {
438
        this.cancel_delay();
439
        this.delay_timer = true;
440
        if (this.save_transaction) {
441
            this.save_transaction.abort();
442
        }
443
    }
444
};
445
 
446
 
447
}, '@VERSION@', {
448
    "requires": [
449
        "base",
450
        "node",
451
        "event",
452
        "event-valuechange",
453
        "node-event-delegate",
454
        "io-form",
455
        "datatype-date-format"
456
    ]
457
});