Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
/**
2
 * Resource and activity toolbox class.
3
 *
4
 * This class is responsible for managing AJAX interactions with activities and resources
5
 * when viewing a quiz in editing mode.
6
 *
7
 * @module mod_quiz-resource-toolbox
8
 * @namespace M.mod_quiz.resource_toolbox
9
 */
10
 
11
/**
12
 * Resource and activity toolbox class.
13
 *
14
 * This is a class extending TOOLBOX containing code specific to resources
15
 *
16
 * This class is responsible for managing AJAX interactions with activities and resources
17
 * when viewing a quiz in editing mode.
18
 *
19
 * @class resources
20
 * @constructor
21
 * @extends M.course.toolboxes.toolbox
22
 */
23
var RESOURCETOOLBOX = function() {
24
    RESOURCETOOLBOX.superclass.constructor.apply(this, arguments);
25
};
26
 
27
Y.extend(RESOURCETOOLBOX, TOOLBOX, {
28
    /**
29
     * An Array of events added when editing a max mark field.
30
     * These should all be detached when editing is complete.
31
     *
32
     * @property editmaxmarkevents
33
     * @protected
34
     * @type Array
35
     * @protected
36
     */
37
    editmaxmarkevents: [],
38
 
39
    /**
40
     *
41
     */
42
    NODE_PAGE: 1,
43
    NODE_SLOT: 2,
44
    NODE_JOIN: 3,
45
 
46
    /**
47
     * Initialize the resource toolbox
48
     *
49
     * For each activity the commands are updated and a reference to the activity is attached.
50
     * This way it doesn't matter where the commands are going to called from they have a reference to the
51
     * activity that they relate to.
52
     * This is essential as some of the actions are displayed in an actionmenu which removes them from the
53
     * page flow.
54
     *
55
     * This function also creates a single event delegate to manage all AJAX actions for all activities on
56
     * the page.
57
     *
58
     * @method initializer
59
     * @protected
60
     */
61
    initializer: function() {
62
        M.mod_quiz.quizbase.register_module(this);
63
        Y.delegate('click', this.handle_data_action, BODY, SELECTOR.ACTIVITYACTION, this);
64
        Y.delegate('click', this.handle_data_action, BODY, SELECTOR.DEPENDENCY_LINK, this);
65
        this.initialise_select_multiple();
66
    },
67
 
68
    /**
69
     * Initialize the select multiple options
70
     *
71
     * Add actions to the buttons that enable multiple slots to be selected and managed at once.
72
     *
73
     * @method initialise_select_multiple
74
     * @protected
75
     */
76
    initialise_select_multiple: function() {
77
        // Click select multiple button to show the select all options.
78
        Y.one(SELECTOR.SELECTMULTIPLEBUTTON).on('click', function(e) {
79
            e.preventDefault();
80
            Y.one('body').addClass(CSS.SELECTMULTIPLE);
81
        });
82
 
83
        // Click cancel button to show the select all options.
84
        Y.one(SELECTOR.SELECTMULTIPLECANCELBUTTON).on('click', function(e) {
85
            e.preventDefault();
86
            Y.one('body').removeClass(CSS.SELECTMULTIPLE);
87
        });
88
 
89
        // Assign the delete method to the delete multiple button.
90
        Y.delegate('click', this.delete_multiple_action, BODY, SELECTOR.SELECTMULTIPLEDELETEBUTTON, this);
91
    },
92
 
93
    /**
94
     * Handles the delegation event. When this is fired someone has triggered an action.
95
     *
96
     * Note not all actions will result in an AJAX enhancement.
97
     *
98
     * @protected
99
     * @method handle_data_action
100
     * @param {EventFacade} ev The event that was triggered.
101
     * @returns {boolean}
102
     */
103
    handle_data_action: function(ev) {
104
        // We need to get the anchor element that triggered this event.
105
        var node = ev.target;
106
        if (!node.test('a')) {
107
            node = node.ancestor(SELECTOR.ACTIVITYACTION);
108
        }
109
 
110
        // From the anchor we can get both the activity (added during initialisation) and the action being
111
        // performed (added by the UI as a data attribute).
112
        var action = node.getData('action'),
113
            activity = node.ancestor(SELECTOR.ACTIVITYLI);
114
 
115
        if (!node.test('a') || !action || !activity) {
116
            // It wasn't a valid action node.
117
            return;
118
        }
119
 
120
        // Switch based upon the action and do the desired thing.
121
        switch (action) {
122
            case 'editmaxmark':
123
                // The user wishes to edit the maxmark of the resource.
124
                this.edit_maxmark(ev, node, activity, action);
125
                break;
126
            case 'delete':
127
                // The user is deleting the activity.
128
                this.delete_with_confirmation(ev, node, activity, action);
129
                break;
130
            case 'addpagebreak':
131
            case 'removepagebreak':
132
                // The user is adding or removing a page break.
133
                this.update_page_break(ev, node, activity, action);
134
                break;
135
            case 'adddependency':
136
            case 'removedependency':
137
                // The user is adding or removing a dependency between questions.
138
                this.update_dependency(ev, node, activity, action);
139
                break;
140
            default:
141
                // Nothing to do here!
142
                break;
143
        }
144
    },
145
 
146
    /**
147
     * Add a loading icon to the specified activity.
148
     *
149
     * The icon is added within the action area.
150
     *
151
     * @method add_spinner
152
     * @param {Node} activity The activity to add a loading icon to
153
     * @return {Node|null} The newly created icon, or null if the action area was not found.
154
     */
155
    add_spinner: function(activity) {
156
        var actionarea = activity.one(SELECTOR.ACTIONAREA);
157
        if (actionarea) {
158
            return M.util.add_spinner(Y, actionarea);
159
        }
160
        return null;
161
    },
162
 
163
    /**
164
     * Deletes the given activity or resource after confirmation.
165
     *
166
     * @protected
167
     * @method delete_with_confirmation
168
     * @param {EventFacade} ev The event that was fired.
169
     * @param {Node} button The button that triggered this action.
170
     * @param {Node} activity The activity node that this action will be performed on.
171
     */
172
    delete_with_confirmation: function(ev, button, activity) {
173
        // Prevent the default button action.
174
        ev.preventDefault();
175
 
176
        // Get the element we're working on.
177
        var element = activity;
178
        // Create confirm string (different if element has or does not have name)
179
        var qtypename = M.util.get_string(
180
            'pluginname',
181
            'qtype_' + element.getAttribute('class').match(/qtype_([^\s]*)/)[1]
182
        );
183
 
184
        // Create the confirmation dialogue.
185
        require(['core/notification'], function(Notification) {
186
            Notification.saveCancelPromise(
187
                M.util.get_string('confirm', 'moodle'),
188
                M.util.get_string('confirmremovequestion', 'quiz', qtypename),
189
                M.util.get_string('yes', 'moodle')
190
            ).then(function() {
191
                var spinner = this.add_spinner(element);
192
                var data = {
193
                    'class': 'resource',
194
                    'action': 'DELETE',
195
                    'id': Y.Moodle.mod_quiz.util.slot.getId(element)
196
                };
197
                this.send_request(data, spinner, function(response) {
198
                    if (response.deleted) {
199
                        // Actually remove the element.
200
                        Y.Moodle.mod_quiz.util.slot.remove(element);
201
                        this.reorganise_edit_page();
202
                        if (M.core.actionmenu && M.core.actionmenu.instance) {
203
                            M.core.actionmenu.instance.hideMenu(ev);
204
                        }
205
                    }
206
                });
207
 
208
                return;
209
            }.bind(this)).catch(function() {
210
                // User cancelled.
211
            });
212
        }.bind(this));
213
    },
214
 
215
    /**
216
     * Finds the section that would become empty if we remove the selected slots.
217
     *
218
     * @protected
219
     * @method find_sections_that_would_become_empty
220
     * @returns {String} The name of the first section found
221
     */
222
    find_sections_that_would_become_empty: function() {
223
        var section;
224
        var sectionnodes = Y.all(SELECTOR.SECTIONLI);
225
 
226
        if (sectionnodes.size() > 1) {
227
            sectionnodes.some(function(node) {
228
                var sectionname = node.one(SELECTOR.INSTANCESECTION).getContent();
229
                var checked = node.all(SELECTOR.SELECTMULTIPLECHECKBOX + ':checked');
230
                var unchecked = node.all(SELECTOR.SELECTMULTIPLECHECKBOX + ':not(:checked)');
231
 
232
                if (!checked.isEmpty() && unchecked.isEmpty()) {
233
                    section = sectionname;
234
                }
235
 
236
                return section;
237
            });
238
        }
239
 
240
        return section;
241
    },
242
 
243
    /**
244
     * Takes care of what needs to happen when the user clicks on the delete multiple button.
245
     *
246
     * @protected
247
     * @method delete_multiple_action
248
     * @param {EventFacade} ev The event that was fired.
249
     */
250
    delete_multiple_action: function(ev) {
251
        var problemsection = this.find_sections_that_would_become_empty();
252
 
253
        if (typeof problemsection !== 'undefined') {
254
            require(['core/notification'], function(Notification) {
255
                Notification.alert(
256
                    M.util.get_string('cannotremoveslots', 'quiz'),
257
                    M.util.get_string('cannotremoveallsectionslots', 'quiz', problemsection)
258
                );
259
            });
260
        } else {
261
            this.delete_multiple_with_confirmation(ev);
262
        }
263
    },
264
 
265
    /**
266
     * Deletes the given activities or resources after confirmation.
267
     *
268
     * @protected
269
     * @method delete_multiple_with_confirmation
270
     * @param {EventFacade} ev The event that was fired.
271
     */
272
    delete_multiple_with_confirmation: function(ev) {
273
        ev.preventDefault();
274
 
275
        var ids = '';
276
        var slots = [];
277
        Y.all(SELECTOR.SELECTMULTIPLECHECKBOX + ':checked').each(function(node) {
278
            var slot = Y.Moodle.mod_quiz.util.slot.getSlotFromComponent(node);
279
            ids += ids === '' ? '' : ',';
280
            ids += Y.Moodle.mod_quiz.util.slot.getId(slot);
281
            slots.push(slot);
282
        });
283
        var element = Y.one('div.mod-quiz-edit-content');
284
 
285
        // Do nothing if no slots are selected.
286
        if (!slots || !slots.length) {
287
            return;
288
        }
289
 
290
        require(['core/notification'], function(Notification) {
291
            Notification.saveCancelPromise(
292
                M.util.get_string('confirm', 'moodle'),
293
                M.util.get_string('areyousureremoveselected', 'quiz'),
294
                M.util.get_string('yes', 'moodle')
295
            ).then(function() {
296
                var spinner = this.add_spinner(element);
297
                var data = {
298
                    'class': 'resource',
299
                    field: 'deletemultiple',
300
                    ids: ids
301
                };
302
                // Delete items on server.
303
                this.send_request(data, spinner, function(response) {
304
                    // Delete locally if deleted on server.
305
                    if (response.deleted) {
306
                        // Actually remove the element.
307
                        Y.all(SELECTOR.SELECTMULTIPLECHECKBOX + ':checked').each(function(node) {
308
                            Y.Moodle.mod_quiz.util.slot.remove(node.ancestor('li.activity'));
309
                        });
310
                        // Update the page numbers and sections.
311
                        this.reorganise_edit_page();
312
 
313
                        // Remove the select multiple options.
314
                        Y.one('body').removeClass(CSS.SELECTMULTIPLE);
315
                    }
316
                });
317
 
318
                return;
319
            }.bind(this)).catch(function() {
320
                // User cancelled.
321
            });
322
        }.bind(this));
323
    },
324
 
325
    /**
326
     * Edit the maxmark for the resource
327
     *
328
     * @protected
329
     * @method edit_maxmark
330
     * @param {EventFacade} ev The event that was fired.
331
     * @param {Node} button The button that triggered this action.
332
     * @param {Node} activity The activity node that this action will be performed on.
333
     * @param {String} action The action that has been requested.
334
     * @return Boolean
335
     */
336
    edit_maxmark: function(ev, button, activity) {
337
        // Get the element we're working on
338
        var instancemaxmark = activity.one(SELECTOR.INSTANCEMAXMARK),
339
            instance = activity.one(SELECTOR.ACTIVITYINSTANCE),
340
            currentmaxmark = instancemaxmark.get('firstChild'),
341
            oldmaxmark = currentmaxmark.get('data'),
342
            maxmarktext = oldmaxmark,
343
            thisevent,
344
            anchor = instancemaxmark, // Grab the anchor so that we can swap it with the edit form.
345
            data = {
346
                'class': 'resource',
347
                'field': 'getmaxmark',
348
                'id': Y.Moodle.mod_quiz.util.slot.getId(activity)
349
            };
350
 
351
        // Prevent the default actions.
352
        ev.preventDefault();
353
 
354
        this.send_request(data, null, function(response) {
355
            if (M.core.actionmenu && M.core.actionmenu.instance) {
356
                M.core.actionmenu.instance.hideMenu(ev);
357
            }
358
 
359
            // Try to retrieve the existing string from the server.
360
            if (response.instancemaxmark) {
361
                maxmarktext = response.instancemaxmark;
362
            }
363
 
364
            // Create the editor and submit button.
365
            var editform = Y.Node.create('<form action="#" />');
366
            var editinstructions = Y.Node.create('<span class="' + CSS.EDITINSTRUCTIONS + '" id="id_editinstructions" />')
367
                .set('innerHTML', M.util.get_string('edittitleinstructions', 'moodle'));
368
            var editor = Y.Node.create('<input name="maxmark" type="text" class="' + CSS.TITLEEDITOR + '" />').setAttrs({
369
                'value': maxmarktext,
370
                'autocomplete': 'off',
371
                'aria-describedby': 'id_editinstructions',
372
                'maxLength': '12',
373
                'size': parseInt(this.get('config').questiondecimalpoints, 10) + 2
374
            });
375
 
376
            // Clear the existing content and put the editor in.
377
            editform.appendChild(editor);
378
            editform.setData('anchor', anchor);
379
            instance.insert(editinstructions, 'before');
380
            anchor.replace(editform);
381
 
382
            // We hide various components whilst editing:
383
            activity.addClass(CSS.EDITINGMAXMARK);
384
 
385
            // Focus and select the editor text.
386
            editor.focus().select();
387
 
388
            // Cancel the edit if we lose focus or the escape key is pressed.
389
            thisevent = editor.on('blur', this.edit_maxmark_cancel, this, activity, false);
390
            this.editmaxmarkevents.push(thisevent);
391
            thisevent = editor.on('key', this.edit_maxmark_cancel, 'esc', this, activity, true);
392
            this.editmaxmarkevents.push(thisevent);
393
 
394
            // Handle form submission.
395
            thisevent = editform.on('submit', this.edit_maxmark_submit, this, activity, oldmaxmark);
396
            this.editmaxmarkevents.push(thisevent);
397
        });
398
    },
399
 
400
    /**
401
     * Handles the submit event when editing the activity or resources maxmark.
402
     *
403
     * @protected
404
     * @method edit_maxmark_submit
405
     * @param {EventFacade} ev The event that triggered this.
406
     * @param {Node} activity The activity whose maxmark we are altering.
407
     * @param {String} originalmaxmark The original maxmark the activity or resource had.
408
     */
409
    edit_maxmark_submit: function(ev, activity, originalmaxmark) {
410
        // We don't actually want to submit anything.
411
        ev.preventDefault();
412
        var newmaxmark = Y.Lang.trim(activity.one(SELECTOR.ACTIVITYFORM + ' ' + SELECTOR.ACTIVITYMAXMARK).get('value'));
413
        var spinner = this.add_spinner(activity);
414
        this.edit_maxmark_clear(activity);
415
        activity.one(SELECTOR.INSTANCEMAXMARK).setContent(newmaxmark);
416
        if (newmaxmark !== null && newmaxmark !== "" && newmaxmark !== originalmaxmark) {
417
            var data = {
418
                'class': 'resource',
419
                'field': 'updatemaxmark',
420
                'maxmark': newmaxmark,
421
                'id': Y.Moodle.mod_quiz.util.slot.getId(activity)
422
            };
423
            this.send_request(data, spinner, function(response) {
424
                if (response.instancemaxmark) {
425
                    activity.one(SELECTOR.INSTANCEMAXMARK).setContent(response.instancemaxmark);
426
                }
427
            });
428
        }
429
    },
430
 
431
    /**
432
     * Handles the cancel event when editing the activity or resources maxmark.
433
     *
434
     * @protected
435
     * @method edit_maxmark_cancel
436
     * @param {EventFacade} ev The event that triggered this.
437
     * @param {Node} activity The activity whose maxmark we are altering.
438
     * @param {Boolean} preventdefault If true we should prevent the default action from occuring.
439
     */
440
    edit_maxmark_cancel: function(ev, activity, preventdefault) {
441
        if (preventdefault) {
442
            ev.preventDefault();
443
        }
444
        this.edit_maxmark_clear(activity);
445
    },
446
 
447
    /**
448
     * Handles clearing the editing UI and returning things to the original state they were in.
449
     *
450
     * @protected
451
     * @method edit_maxmark_clear
452
     * @param {Node} activity  The activity whose maxmark we were altering.
453
     */
454
    edit_maxmark_clear: function(activity) {
455
        // Detach all listen events to prevent duplicate triggers
456
        new Y.EventHandle(this.editmaxmarkevents).detach();
457
 
458
        var editform = activity.one(SELECTOR.ACTIVITYFORM),
459
            instructions = activity.one('#id_editinstructions');
460
        if (editform) {
461
            editform.replace(editform.getData('anchor'));
462
        }
463
        if (instructions) {
464
            instructions.remove();
465
        }
466
 
467
        // Remove the editing class again to revert the display.
468
        activity.removeClass(CSS.EDITINGMAXMARK);
469
 
470
        // Refocus the link which was clicked originally so the user can continue using keyboard nav.
471
        Y.later(100, this, function() {
472
            activity.one(SELECTOR.EDITMAXMARK).focus();
473
        });
474
 
475
        // TODO MDL-50768 This hack is to keep Behat happy until they release a version of
476
        // MinkSelenium2Driver that fixes
477
        // https://github.com/Behat/MinkSelenium2Driver/issues/80.
478
        if (!Y.one('input[name=maxmark')) {
479
            Y.one('body').append('<input type="text" name="maxmark" style="display: none">');
480
        }
481
    },
482
 
483
    /**
484
     * Joins or separates the given slot with the page of the previous slot. Reorders the pages of
485
     * the other slots
486
     *
487
     * @protected
488
     * @method update_page_break
489
     * @param {EventFacade} ev The event that was fired.
490
     * @param {Node} button The button that triggered this action.
491
     * @param {Node} activity The activity node that this action will be performed on.
492
     * @param {String} action The action, addpagebreak or removepagebreak.
493
     * @chainable
494
     */
495
    update_page_break: function(ev, button, activity, action) {
496
        // Prevent the default button action
497
        ev.preventDefault();
498
 
499
        var nextactivity = activity.next('li.activity.slot');
500
        var spinner = this.add_spinner(nextactivity);
501
        var value = action === 'removepagebreak' ? 1 : 2;
502
 
503
        var data = {
504
            'class': 'resource',
505
            'field': 'updatepagebreak',
506
            'id':    Y.Moodle.mod_quiz.util.slot.getId(nextactivity),
507
            'value': value
508
        };
509
 
510
        this.send_request(data, spinner, function(response) {
511
            if (response.slots) {
512
                if (action === 'addpagebreak') {
513
                    Y.Moodle.mod_quiz.util.page.add(activity);
514
                } else {
515
                    var page = activity.next(Y.Moodle.mod_quiz.util.page.SELECTORS.PAGE);
516
                    Y.Moodle.mod_quiz.util.page.remove(page, true);
517
                }
518
                this.reorganise_edit_page();
519
            }
520
        });
521
 
522
        return this;
523
    },
524
 
525
    /**
526
     * Updates a slot to either require the question in the previous slot to
527
     * have been answered, or not,
528
     *
529
     * @protected
530
     * @method update_page_break
531
     * @param {EventFacade} ev The event that was fired.
532
     * @param {Node} button The button that triggered this action.
533
     * @param {Node} activity The activity node that this action will be performed on.
534
     * @param {String} action The action, adddependency or removedependency.
535
     * @chainable
536
     */
537
    update_dependency: function(ev, button, activity, action) {
538
        // Prevent the default button action.
539
        ev.preventDefault();
540
        var spinner = this.add_spinner(activity);
541
 
542
        var data = {
543
            'class': 'resource',
544
            'field': 'updatedependency',
545
            'id':    Y.Moodle.mod_quiz.util.slot.getId(activity),
546
            'value': action === 'adddependency' ? 1 : 0
547
        };
548
 
549
        this.send_request(data, spinner, function(response) {
550
            if (response.hasOwnProperty('requireprevious')) {
551
                Y.Moodle.mod_quiz.util.slot.updateDependencyIcon(activity, response.requireprevious);
552
            }
553
        });
554
 
555
        return this;
556
    },
557
 
558
    /**
559
     * Reorganise the UI after every edit action.
560
     *
561
     * @protected
562
     * @method reorganise_edit_page
563
     */
564
    reorganise_edit_page: function() {
565
        Y.Moodle.mod_quiz.util.slot.reorderSlots();
566
        Y.Moodle.mod_quiz.util.slot.reorderPageBreaks();
567
        Y.Moodle.mod_quiz.util.page.reorderPages();
568
        Y.Moodle.mod_quiz.util.slot.updateOneSlotSections();
569
        Y.Moodle.mod_quiz.util.slot.updateAllDependencyIcons();
570
    },
571
 
572
    NAME: 'mod_quiz-resource-toolbox',
573
    ATTRS: {
574
        courseid: {
575
            'value': 0
576
        },
577
        quizid: {
578
            'value': 0
579
        }
580
    }
581
 
582
});
583
 
584
M.mod_quiz.resource_toolbox = null;
585
M.mod_quiz.init_resource_toolbox = function(config) {
586
    M.mod_quiz.resource_toolbox = new RESOURCETOOLBOX(config);
587
    return M.mod_quiz.resource_toolbox;
588
};