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
/* eslint camelcase: off */
16
 
17
/**
18
 * JavaScript library for the quiz module.
19
 *
20
 * @package    mod
21
 * @subpackage quiz
22
 * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
23
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24
 */
25
 
26
M.mod_quiz = M.mod_quiz || {};
27
 
28
M.mod_quiz.init_attempt_form = function(Y) {
29
    require(['core_question/question_engine'], function(qEngine) {
30
        qEngine.initForm('#responseform');
31
    });
32
    Y.on('submit', M.mod_quiz.timer.stop, '#responseform');
33
    require(['core_form/changechecker'], function(FormChangeChecker) {
34
        FormChangeChecker.watchFormById('responseform');
35
    });
36
};
37
 
38
M.mod_quiz.init_review_form = function(Y) {
39
    require(['core_question/question_engine'], function(qEngine) {
40
        qEngine.initForm('.questionflagsaveform');
41
    });
42
    Y.on('submit', function(e) { e.halt(); }, '.questionflagsaveform');
43
};
44
 
45
M.mod_quiz.init_comment_popup = function(Y) {
46
    // Add a close button to the window.
47
    var closebutton = Y.Node.create('<input type="button" class="btn btn-secondary" />');
48
    closebutton.set('value', M.util.get_string('cancel', 'moodle'));
49
    Y.one('#id_submitbutton').ancestor().append(closebutton);
50
    Y.on('click', function() { window.close() }, closebutton);
51
}
52
 
53
// Code for updating the countdown timer that is used on timed quizzes.
54
M.mod_quiz.timer = {
55
    // YUI object.
56
    Y: null,
57
 
58
    // Timestamp at which time runs out, according to the student's computer's clock.
59
    endtime: 0,
60
 
61
    // Is this a quiz preview?
62
    preview: 0,
63
 
64
    // This records the id of the timeout that updates the clock periodically,
65
    // so we can cancel.
66
    timeoutid: null,
67
 
68
    // Threshold for updating time remaining, in milliseconds.
69
    threshold: 3000,
70
 
71
    /**
72
     * @param Y the YUI object
73
     * @param start, the timer starting time, in seconds.
74
     * @param preview, is this a quiz preview?
75
     */
76
    init: function(Y, start, preview) {
77
        M.mod_quiz.timer.Y = Y;
78
        M.mod_quiz.timer.endtime = M.pageloadstarttime.getTime() + start*1000;
79
        M.mod_quiz.timer.preview = preview;
80
        M.mod_quiz.timer.update();
81
 
82
        Y.one('#quiz-timer-wrapper').setStyle('display', 'flex');
83
        require(['core_form/changechecker'], function(FormChangeChecker) {
84
            M.mod_quiz.timer.FormChangeChecker = FormChangeChecker;
85
        });
86
        Y.one('#toggle-timer').on('click', function() {
87
            M.mod_quiz.timer.toggleVisibility();
88
        });
89
 
90
        // We store the visibility as a user preference. If the value is not '1',
91
        // i. e. it is '0' or the item does not exist, the timer must be shown.
92
        require(['core_user/repository'], function(UserRepository) {
93
            UserRepository.getUserPreference('quiz_timerhidden')
94
                .then((response) => {
95
                    M.mod_quiz.timer.setVisibility(response !== '1', false);
96
                    return;
97
                })
98
                // If there is an error, we catch and ignore it, because (i) no matter what we do,
99
                // we do not have the stored value, so we will need to take a reasonable default
100
                // and (ii) the student who is currently taking the quiz is probably not interested
101
                // in the technical details why the fetch failed, even less, because they can hardly
102
                // do anything to solve the problem. However, we still log that there was an error
103
                // to leave a trace, e. g. for debugging.
104
                .catch((error) => {
105
                    M.mod_quiz.timer.setVisibility(true, false);
106
                    Y.log(error, 'error', 'moodle-mod_quiz');
107
                });
108
        });
109
    },
110
 
111
    /**
112
     * Toggle the timer's visibility.
113
     */
114
    toggleVisibility: function() {
115
        var Y = M.mod_quiz.timer.Y;
116
        var timer = Y.one('#quiz-time-left');
117
 
118
        // If the timer is currently hidden, the visibility should be set to true and vice versa.
119
        this.setVisibility(timer.getAttribute('hidden') === 'hidden');
120
    },
121
 
122
    /**
123
     * Set visibility of the timer.
124
     * @param visible whether the timer should be visible
125
     * @param updatePref whether the new status should be stored as a preference
126
     */
127
    setVisibility: function(visible, updatePref = true) {
128
        var Y = M.mod_quiz.timer.Y;
129
        var timer = Y.one('#quiz-time-left');
130
        var button = Y.one('#toggle-timer');
131
 
132
        if (visible) {
133
            button.setContent(M.util.get_string('hide', 'moodle'));
134
            timer.show();
135
        } else {
136
            button.setContent(M.util.get_string('show', 'moodle'));
137
            timer.hide();
138
        }
139
 
140
        // Only update the user preference if this has been requested.
141
        if (updatePref) {
142
            require(['core_user/repository'], function(UserRepository) {
143
                UserRepository.setUserPreference('quiz_timerhidden', (visible ? '0' : '1'));
144
            });
145
        }
146
 
147
    },
148
 
149
    /**
150
     * Stop the timer, if it is running.
151
     */
152
    stop: function(e) {
153
        if (M.mod_quiz.timer.timeoutid) {
154
            clearTimeout(M.mod_quiz.timer.timeoutid);
155
        }
156
    },
157
 
158
    /**
159
     * Function to convert a number between 0 and 99 to a two-digit string.
160
     */
161
    two_digit: function(num) {
162
        if (num < 10) {
163
            return '0' + num;
164
        } else {
165
            return num;
166
        }
167
    },
168
 
169
    // Function to update the clock with the current time left, and submit the quiz if necessary.
170
    update: function() {
171
        var Y = M.mod_quiz.timer.Y;
172
        var secondsleft = Math.floor((M.mod_quiz.timer.endtime - new Date().getTime())/1000);
173
 
174
        // If time has expired, set the hidden form field that says time has expired and submit
175
        if (secondsleft < 0) {
176
            M.mod_quiz.timer.stop(null);
177
            Y.one('#quiz-time-left').setContent(M.util.get_string('timesup', 'quiz'));
178
            var input = Y.one('input[name=timeup]');
179
            input.set('value', 1);
180
            var form = input.ancestor('form');
181
            if (form.one('input[name=finishattempt]')) {
182
                form.one('input[name=finishattempt]').set('value', 0);
183
            }
184
            M.mod_quiz.timer.FormChangeChecker.markFormSubmitted(input.getDOMNode());
185
            form.submit();
186
            return;
187
        }
188
 
189
        // If time has nearly expired, change the colour.
190
        if (secondsleft < 100) {
191
            Y.one('#quiz-timer').removeClass('timeleft' + (secondsleft + 2))
192
                    .removeClass('timeleft' + (secondsleft + 1))
193
                    .addClass('timeleft' + secondsleft);
194
 
195
            // From now on, the timer should be visible and should not be hideable anymore.
196
            // We use the second (optional) parameter in order to leave the user preference
197
            // unchanged.
198
            M.mod_quiz.timer.setVisibility(true, false);
199
            Y.one('#toggle-timer').setAttribute('disabled', true);
200
        }
201
 
202
        // Update the time display.
203
        var hours = Math.floor(secondsleft/3600);
204
        secondsleft -= hours*3600;
205
        var minutes = Math.floor(secondsleft/60);
206
        secondsleft -= minutes*60;
207
        var seconds = secondsleft;
208
        Y.one('#quiz-time-left').setContent(hours + ':' +
209
                M.mod_quiz.timer.two_digit(minutes) + ':' +
210
                M.mod_quiz.timer.two_digit(seconds));
211
 
212
        // Arrange for this method to be called again soon.
213
        M.mod_quiz.timer.timeoutid = setTimeout(M.mod_quiz.timer.update, 100);
214
    },
215
 
216
    // Allow the end time of the quiz to be updated.
217
    updateEndTime: function(timeleft) {
218
        var newtimeleft = new Date().getTime() + timeleft * 1000;
219
 
220
        // Timer might not have been initialized yet. We initialize it with
221
        // preview = 0, because it's better to take a preview for a real quiz
222
        // than to take a real quiz for a preview.
223
        if (M.mod_quiz.timer.Y === null) {
224
            M.mod_quiz.timer.init(window.Y, timeleft, 0);
225
        }
226
 
227
        // Only update if change is greater than the threshold, so the
228
        // time doesn't bounce around unnecessarily.
229
        if (Math.abs(newtimeleft - M.mod_quiz.timer.endtime) > M.mod_quiz.timer.threshold) {
230
            M.mod_quiz.timer.endtime = newtimeleft;
231
            M.mod_quiz.timer.update();
232
        }
233
    }
234
};
235
 
236
M.mod_quiz.filesUpload = {
237
    /**
238
     * YUI object.
239
     */
240
    Y: null,
241
 
242
    /**
243
     * Number of files uploading.
244
     */
245
    numberFilesUploading: 0,
246
 
247
    /**
248
     * Disable navigation block when uploading and enable navigation block when all files are uploaded.
249
     */
250
    disableNavPanel: function() {
251
        var quizNavigationBlock = document.getElementById('mod_quiz_navblock');
252
        if (quizNavigationBlock) {
253
            if (M.mod_quiz.filesUpload.numberFilesUploading) {
254
                quizNavigationBlock.classList.add('nav-disabled');
255
            } else {
256
                quizNavigationBlock.classList.remove('nav-disabled');
257
            }
258
        }
259
    }
260
};
261
 
262
M.mod_quiz.nav = M.mod_quiz.nav || {};
263
 
264
M.mod_quiz.nav.update_flag_state = function(attemptid, questionid, newstate) {
265
    var Y = M.mod_quiz.nav.Y;
266
    var navlink = Y.one('#quiznavbutton' + questionid);
267
    navlink.removeClass('flagged');
268
    if (newstate == 1) {
269
        navlink.addClass('flagged');
270
        navlink.one('.accesshide .flagstate').setContent(M.util.get_string('flagged', 'question'));
271
    } else {
272
        navlink.one('.accesshide .flagstate').setContent('');
273
    }
274
};
275
 
276
M.mod_quiz.nav.init = function(Y) {
277
    M.mod_quiz.nav.Y = Y;
278
 
279
    Y.all('#quiznojswarning').remove();
280
 
281
    var form = Y.one('#responseform');
282
    if (form) {
283
        function nav_to_page(pageno) {
284
            Y.one('#followingpage').set('value', pageno);
285
 
286
            // Automatically submit the form. We do it this strange way because just
287
            // calling form.submit() does not run the form's submit event handlers.
288
            var submit = form.one('input[name="next"]');
289
            submit.set('name', '');
290
            submit.getDOMNode().click();
291
        };
292
 
293
        Y.delegate('click', function(e) {
294
            if (this.hasClass('thispage')) {
295
                return;
296
            }
297
 
298
            e.preventDefault();
299
 
300
            var pageidmatch = this.get('href').match(/page=(\d+)/);
301
            var pageno;
302
            if (pageidmatch) {
303
                pageno = pageidmatch[1];
304
            } else {
305
                pageno = 0;
306
            }
307
 
308
            var questionidmatch = this.get('href').match(/#question-(\d+)-(\d+)/);
309
            if (questionidmatch) {
310
                form.set('action', form.get('action') + questionidmatch[0]);
311
            }
312
 
313
            nav_to_page(pageno);
314
        }, document.body, '.qnbutton');
315
    }
316
 
317
    if (Y.one('a.endtestlink')) {
318
        Y.on('click', function(e) {
319
            e.preventDefault();
320
            nav_to_page(-1);
321
        }, 'a.endtestlink');
322
    }
323
 
324
    // Navigation buttons should be disabled when the files are uploading.
325
    require(['core_form/events'], function(formEvent) {
326
        document.addEventListener(formEvent.eventTypes.uploadStarted, function() {
327
            M.mod_quiz.filesUpload.numberFilesUploading++;
328
            M.mod_quiz.filesUpload.disableNavPanel();
329
        });
330
 
331
        document.addEventListener(formEvent.eventTypes.uploadCompleted, function() {
332
            M.mod_quiz.filesUpload.numberFilesUploading--;
333
            M.mod_quiz.filesUpload.disableNavPanel();
334
        });
335
    });
336
 
337
    if (M.core_question_flags) {
338
        M.core_question_flags.add_listener(M.mod_quiz.nav.update_flag_state);
339
    }
340
};
341
 
342
M.mod_quiz.secure_window = {
343
    init: function(Y) {
344
        if (window.location.href.substring(0, 4) == 'file') {
345
            window.location = 'about:blank';
346
        }
347
        Y.delegate('contextmenu', M.mod_quiz.secure_window.prevent, document, '*');
348
        Y.delegate('mousedown',   M.mod_quiz.secure_window.prevent_mouse, 'body', '*');
349
        Y.delegate('mouseup',     M.mod_quiz.secure_window.prevent_mouse, 'body', '*');
350
        Y.delegate('dragstart',   M.mod_quiz.secure_window.prevent, document, '*');
351
        Y.delegate('selectstart', M.mod_quiz.secure_window.prevent_selection, document, '*');
352
        Y.delegate('cut',         M.mod_quiz.secure_window.prevent, document, '*');
353
        Y.delegate('copy',        M.mod_quiz.secure_window.prevent, document, '*');
354
        Y.delegate('paste',       M.mod_quiz.secure_window.prevent, document, '*');
355
        Y.on('beforeprint', function() {
356
            Y.one(document.body).setStyle('display', 'none');
357
        }, window);
358
        Y.on('afterprint', function() {
359
            Y.one(document.body).setStyle('display', 'block');
360
        }, window);
361
        Y.on('key', M.mod_quiz.secure_window.prevent, '*', 'press:67,86,88+ctrl');
362
        Y.on('key', M.mod_quiz.secure_window.prevent, '*', 'up:67,86,88+ctrl');
363
        Y.on('key', M.mod_quiz.secure_window.prevent, '*', 'down:67,86,88+ctrl');
364
        Y.on('key', M.mod_quiz.secure_window.prevent, '*', 'press:67,86,88+meta');
365
        Y.on('key', M.mod_quiz.secure_window.prevent, '*', 'up:67,86,88+meta');
366
        Y.on('key', M.mod_quiz.secure_window.prevent, '*', 'down:67,86,88+meta');
367
    },
368
 
369
    is_content_editable: function(n) {
370
        if (n.test('[contenteditable=true]')) {
371
            return true;
372
        }
373
        n = n.get('parentNode');
374
        if (n === null) {
375
            return false;
376
        }
377
        return M.mod_quiz.secure_window.is_content_editable(n);
378
    },
379
 
380
    prevent_selection: function(e) {
381
        return false;
382
    },
383
 
384
    prevent: function(e) {
385
        alert(M.util.get_string('functiondisabledbysecuremode', 'quiz'));
386
        e.halt();
387
    },
388
 
389
    prevent_mouse: function(e) {
390
        if (e.button == 1 && /^(INPUT|TEXTAREA|BUTTON|SELECT|LABEL|A)$/i.test(e.target.get('tagName'))) {
391
            // Left click on a button or similar. No worries.
392
            return;
393
        }
394
        if (e.button == 1 && M.mod_quiz.secure_window.is_content_editable(e.target)) {
395
            // Left click in Atto or similar.
396
            return;
397
        }
398
        e.halt();
399
    },
400
 
401
    init_close_button: function(Y, url) {
402
        Y.on('click', function(e) {
403
            M.mod_quiz.secure_window.close(url, 0)
404
        }, '#secureclosebutton');
405
    },
406
 
407
    close: function(url, delay) {
408
        setTimeout(function() {
409
            if (window.opener) {
410
                window.opener.document.location.reload();
411
                window.close();
412
            } else {
413
                window.location.href = url;
414
            }
415
        }, delay*1000);
416
    }
417
};