Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
YUI.add('moodle-mod_quiz-toolboxes', function (Y, NAME) {
2
 
3
/* eslint-disable no-unused-vars */
4
/**
5
 * Resource and activity toolbox class.
6
 *
7
 * This class is responsible for managing AJAX interactions with activities and resources
8
 * when viewing a course in editing mode.
9
 *
10
 * @module moodle-course-toolboxes
11
 * @namespace M.course.toolboxes
12
 */
13
 
14
// The CSS classes we use.
15
var CSS = {
16
        ACTIVITYINSTANCE: 'activityinstance',
17
        AVAILABILITYINFODIV: 'div.availabilityinfo',
18
        CONTENTWITHOUTLINK: 'contentwithoutlink',
19
        CONDITIONALHIDDEN: 'conditionalhidden',
20
        DIMCLASS: 'dimmed',
21
        DIMMEDTEXT: 'dimmed_text',
22
        EDITINSTRUCTIONS: 'editinstructions',
23
        EDITINGMAXMARK: 'editor_displayed',
24
        HIDE: 'hide',
25
        JOIN: 'page_join',
26
        MODINDENTCOUNT: 'mod-indent-',
27
        MODINDENTHUGE: 'mod-indent-huge',
28
        PAGE: 'page',
29
        SECTIONHIDDENCLASS: 'hidden',
30
        SECTIONIDPREFIX: 'section-',
31
        SELECTMULTIPLE: 'select-multiple',
32
        SLOT: 'slot',
33
        SHOW: 'editing_show',
34
        TITLEEDITOR: 'titleeditor'
35
    },
36
    // The CSS selectors we use.
37
    SELECTOR = {
38
        ACTIONAREA: '.actions',
39
        ACTIONLINKTEXT: '.actionlinktext',
40
        ACTIVITYACTION: 'a.cm-edit-action[data-action], a.editing_maxmark, a.editing_section, input.shuffle_questions',
41
        ACTIVITYFORM: 'span.instancemaxmarkcontainer form',
42
        ACTIVITYINSTANCE: '.' + CSS.ACTIVITYINSTANCE,
43
        SECTIONINSTANCE: '.sectioninstance',
44
        ACTIVITYLI: 'li.activity, li.section',
45
        ACTIVITYMAXMARK: 'input[name=maxmark]',
46
        COMMANDSPAN: '.commands',
47
        CONTENTAFTERLINK: 'div.contentafterlink',
48
        CONTENTWITHOUTLINK: 'div.contentwithoutlink',
49
        DELETESECTIONICON: 'a.editing_delete .icon',
50
        EDITMAXMARK: 'a.editing_maxmark',
51
        EDITSECTION: 'a.editing_section',
52
        EDITSECTIONICON: 'a.editing_section .icon',
53
        EDITSHUFFLEQUESTIONSACTION: 'input.cm-edit-action[data-action]',
54
        EDITSHUFFLEAREA: '.instanceshufflequestions .shuffle-progress',
55
        HIDE: 'a.editing_hide',
56
        HIGHLIGHT: 'a.editing_highlight',
57
        INSTANCENAME: 'span.instancename',
58
        INSTANCEMAXMARK: 'span.instancemaxmark',
59
        INSTANCESECTION: 'span.instancesection',
60
        INSTANCESECTIONAREA: 'div.section-heading',
61
        MODINDENTDIV: '.mod-indent',
62
        MODINDENTOUTER: '.mod-indent-outer',
63
        NUMQUESTIONS: '.numberofquestions',
64
        PAGECONTENT: 'div#page-content',
65
        PAGELI: 'li.page',
66
        SECTIONLI: 'li.section',
67
        SECTIONUL: 'ul.section',
68
        SECTIONFORM: '.instancesectioncontainer form',
69
        SECTIONINPUT: 'input[name=section]',
70
        SELECTMULTIPLEBUTTON: '#selectmultiplecommand',
71
        SELECTMULTIPLECANCELBUTTON: '#selectmultiplecancelcommand',
72
        SELECTMULTIPLECHECKBOX: '.select-multiple-checkbox',
73
        SELECTMULTIPLEDELETEBUTTON: '#selectmultipledeletecommand',
74
        SELECTALL: '#questionselectall',
75
        SHOW: 'a.' + CSS.SHOW,
76
        SLOTLI: 'li.slot',
77
        SUMMARKS: '.mod_quiz_summarks'
78
    },
79
    BODY = Y.one(document.body);
80
 
81
// Setup the basic namespace.
82
M.mod_quiz = M.mod_quiz || {};
83
 
84
/**
85
 * The toolbox class is a generic class which should never be directly
86
 * instantiated. Please extend it instead.
87
 *
88
 * @class toolbox
89
 * @constructor
90
 * @protected
91
 * @extends Base
92
 */
93
var TOOLBOX = function() {
94
    TOOLBOX.superclass.constructor.apply(this, arguments);
95
};
96
 
97
Y.extend(TOOLBOX, Y.Base, {
98
    /**
99
     * Send a request using the REST API
100
     *
101
     * @method send_request
102
     * @param {Object} data The data to submit with the AJAX request
103
     * @param {Node} [statusspinner] A statusspinner which may contain a section loader
104
     * @param {Function} success_callback The callback to use on success
105
     * @param {Object} [optionalconfig] Any additional configuration to submit
106
     * @chainable
107
     */
108
    send_request: function(data, statusspinner, success_callback, optionalconfig) {
109
        // Default data structure
110
        if (!data) {
111
            data = {};
112
        }
113
 
114
        // Handle any variables which we must pass back through to
115
        var pageparams = this.get('config').pageparams,
116
            varname;
117
        for (varname in pageparams) {
118
            data[varname] = pageparams[varname];
119
        }
120
 
121
        data.sesskey = M.cfg.sesskey;
122
        data.courseid = this.get('courseid');
123
        data.quizid = this.get('quizid');
124
 
125
        var uri = M.cfg.wwwroot + this.get('ajaxurl');
126
 
127
        // Define the configuration to send with the request
128
        var responsetext = [];
129
        var config = {
130
            method: 'POST',
131
            data: data,
132
            on: {
133
                success: function(tid, response) {
134
                    try {
135
                        responsetext = Y.JSON.parse(response.responseText);
136
                        if (responsetext.error) {
137
                            new M.core.ajaxException(responsetext);
138
                        }
139
                    } catch (e) {
140
                        // Ignore.
141
                    }
142
 
143
                    // Run the callback if we have one.
144
                    if (responsetext.hasOwnProperty('newsummarks')) {
145
                        Y.one(SELECTOR.SUMMARKS).setHTML(responsetext.newsummarks);
146
                    }
147
                    if (responsetext.hasOwnProperty('newnumquestions')) {
148
                        Y.one(SELECTOR.NUMQUESTIONS).setHTML(
149
                                M.util.get_string('numquestionsx', 'quiz', responsetext.newnumquestions)
150
                            );
151
                    }
152
                    if (success_callback) {
153
                        Y.bind(success_callback, this, responsetext)();
154
                    }
155
 
156
                    if (statusspinner) {
157
                        window.setTimeout(function() {
158
                            statusspinner.hide();
159
                        }, 400);
160
                    }
161
                },
162
                failure: function(tid, response) {
163
                    if (statusspinner) {
164
                        statusspinner.hide();
165
                    }
166
                    new M.core.ajaxException(response);
167
                }
168
            },
169
            context: this
170
        };
171
 
172
        // Apply optional config
173
        if (optionalconfig) {
174
            for (varname in optionalconfig) {
175
                config[varname] = optionalconfig[varname];
176
            }
177
        }
178
 
179
        if (statusspinner) {
180
            statusspinner.show();
181
        }
182
 
183
        // Send the request
184
        Y.io(uri, config);
185
        return this;
186
    }
187
},
188
{
189
    NAME: 'mod_quiz-toolbox',
190
    ATTRS: {
191
        /**
192
         * The ID of the Moodle Course being edited.
193
         *
194
         * @attribute courseid
195
         * @default 0
196
         * @type Number
197
         */
198
        courseid: {
199
            'value': 0
200
        },
201
 
202
        /**
203
         * The Moodle course format.
204
         *
205
         * @attribute format
206
         * @default 'topics'
207
         * @type String
208
         */
209
        quizid: {
210
            'value': 0
211
        },
212
        /**
213
         * The URL to use when submitting requests.
214
         * @attribute ajaxurl
215
         * @default null
216
         * @type String
217
         */
218
        ajaxurl: {
219
            'value': null
220
        },
221
        /**
222
         * Any additional configuration passed when creating the instance.
223
         *
224
         * @attribute config
225
         * @default {}
226
         * @type Object
227
         */
228
        config: {
229
            'value': {}
230
        }
231
    }
232
}
233
);
234
/**
235
 * Resource and activity toolbox class.
236
 *
237
 * This class is responsible for managing AJAX interactions with activities and resources
238
 * when viewing a quiz in editing mode.
239
 *
240
 * @module mod_quiz-resource-toolbox
241
 * @namespace M.mod_quiz.resource_toolbox
242
 */
243
 
244
/**
245
 * Resource and activity toolbox class.
246
 *
247
 * This is a class extending TOOLBOX containing code specific to resources
248
 *
249
 * This class is responsible for managing AJAX interactions with activities and resources
250
 * when viewing a quiz in editing mode.
251
 *
252
 * @class resources
253
 * @constructor
254
 * @extends M.course.toolboxes.toolbox
255
 */
256
var RESOURCETOOLBOX = function() {
257
    RESOURCETOOLBOX.superclass.constructor.apply(this, arguments);
258
};
259
 
260
Y.extend(RESOURCETOOLBOX, TOOLBOX, {
261
    /**
262
     * An Array of events added when editing a max mark field.
263
     * These should all be detached when editing is complete.
264
     *
265
     * @property editmaxmarkevents
266
     * @protected
267
     * @type Array
268
     * @protected
269
     */
270
    editmaxmarkevents: [],
271
 
272
    /**
273
     *
274
     */
275
    NODE_PAGE: 1,
276
    NODE_SLOT: 2,
277
    NODE_JOIN: 3,
278
 
279
    /**
280
     * Initialize the resource toolbox
281
     *
282
     * For each activity the commands are updated and a reference to the activity is attached.
283
     * This way it doesn't matter where the commands are going to called from they have a reference to the
284
     * activity that they relate to.
285
     * This is essential as some of the actions are displayed in an actionmenu which removes them from the
286
     * page flow.
287
     *
288
     * This function also creates a single event delegate to manage all AJAX actions for all activities on
289
     * the page.
290
     *
291
     * @method initializer
292
     * @protected
293
     */
294
    initializer: function() {
295
        M.mod_quiz.quizbase.register_module(this);
296
        Y.delegate('click', this.handle_data_action, BODY, SELECTOR.ACTIVITYACTION, this);
297
        Y.delegate('click', this.handle_data_action, BODY, SELECTOR.DEPENDENCY_LINK, this);
298
        this.initialise_select_multiple();
299
    },
300
 
301
    /**
302
     * Initialize the select multiple options
303
     *
304
     * Add actions to the buttons that enable multiple slots to be selected and managed at once.
305
     *
306
     * @method initialise_select_multiple
307
     * @protected
308
     */
309
    initialise_select_multiple: function() {
310
        // Click select multiple button to show the select all options.
311
        Y.one(SELECTOR.SELECTMULTIPLEBUTTON).on('click', function(e) {
312
            e.preventDefault();
313
            Y.one('body').addClass(CSS.SELECTMULTIPLE);
314
        });
315
 
316
        // Click cancel button to show the select all options.
317
        Y.one(SELECTOR.SELECTMULTIPLECANCELBUTTON).on('click', function(e) {
318
            e.preventDefault();
319
            Y.one('body').removeClass(CSS.SELECTMULTIPLE);
320
        });
321
 
322
        // Assign the delete method to the delete multiple button.
323
        Y.delegate('click', this.delete_multiple_action, BODY, SELECTOR.SELECTMULTIPLEDELETEBUTTON, this);
324
    },
325
 
326
    /**
327
     * Handles the delegation event. When this is fired someone has triggered an action.
328
     *
329
     * Note not all actions will result in an AJAX enhancement.
330
     *
331
     * @protected
332
     * @method handle_data_action
333
     * @param {EventFacade} ev The event that was triggered.
334
     * @returns {boolean}
335
     */
336
    handle_data_action: function(ev) {
337
        // We need to get the anchor element that triggered this event.
338
        var node = ev.target;
339
        if (!node.test('a')) {
340
            node = node.ancestor(SELECTOR.ACTIVITYACTION);
341
        }
342
 
343
        // From the anchor we can get both the activity (added during initialisation) and the action being
344
        // performed (added by the UI as a data attribute).
345
        var action = node.getData('action'),
346
            activity = node.ancestor(SELECTOR.ACTIVITYLI);
347
 
348
        if (!node.test('a') || !action || !activity) {
349
            // It wasn't a valid action node.
350
            return;
351
        }
352
 
353
        // Switch based upon the action and do the desired thing.
354
        switch (action) {
355
            case 'editmaxmark':
356
                // The user wishes to edit the maxmark of the resource.
357
                this.edit_maxmark(ev, node, activity, action);
358
                break;
359
            case 'delete':
360
                // The user is deleting the activity.
361
                this.delete_with_confirmation(ev, node, activity, action);
362
                break;
363
            case 'addpagebreak':
364
            case 'removepagebreak':
365
                // The user is adding or removing a page break.
366
                this.update_page_break(ev, node, activity, action);
367
                break;
368
            case 'adddependency':
369
            case 'removedependency':
370
                // The user is adding or removing a dependency between questions.
371
                this.update_dependency(ev, node, activity, action);
372
                break;
373
            default:
374
                // Nothing to do here!
375
                break;
376
        }
377
    },
378
 
379
    /**
380
     * Add a loading icon to the specified activity.
381
     *
382
     * The icon is added within the action area.
383
     *
384
     * @method add_spinner
385
     * @param {Node} activity The activity to add a loading icon to
386
     * @return {Node|null} The newly created icon, or null if the action area was not found.
387
     */
388
    add_spinner: function(activity) {
389
        var actionarea = activity.one(SELECTOR.ACTIONAREA);
390
        if (actionarea) {
391
            return M.util.add_spinner(Y, actionarea);
392
        }
393
        return null;
394
    },
395
 
396
    /**
397
     * Deletes the given activity or resource after confirmation.
398
     *
399
     * @protected
400
     * @method delete_with_confirmation
401
     * @param {EventFacade} ev The event that was fired.
402
     * @param {Node} button The button that triggered this action.
403
     * @param {Node} activity The activity node that this action will be performed on.
404
     */
405
    delete_with_confirmation: function(ev, button, activity) {
406
        // Prevent the default button action.
407
        ev.preventDefault();
408
 
409
        // Get the element we're working on.
410
        var element = activity;
411
        // Create confirm string (different if element has or does not have name)
412
        var qtypename = M.util.get_string(
413
            'pluginname',
414
            'qtype_' + element.getAttribute('class').match(/qtype_([^\s]*)/)[1]
415
        );
416
 
417
        // Create the confirmation dialogue.
418
        require(['core/notification'], function(Notification) {
419
            Notification.saveCancelPromise(
420
                M.util.get_string('confirm', 'moodle'),
421
                M.util.get_string('confirmremovequestion', 'quiz', qtypename),
422
                M.util.get_string('yes', 'moodle')
423
            ).then(function() {
424
                var spinner = this.add_spinner(element);
425
                var data = {
426
                    'class': 'resource',
427
                    'action': 'DELETE',
428
                    'id': Y.Moodle.mod_quiz.util.slot.getId(element)
429
                };
430
                this.send_request(data, spinner, function(response) {
431
                    if (response.deleted) {
432
                        // Actually remove the element.
433
                        Y.Moodle.mod_quiz.util.slot.remove(element);
434
                        this.reorganise_edit_page();
435
                        if (M.core.actionmenu && M.core.actionmenu.instance) {
436
                            M.core.actionmenu.instance.hideMenu(ev);
437
                        }
438
                    }
439
                });
440
 
441
                return;
442
            }.bind(this)).catch(function() {
443
                // User cancelled.
444
            });
445
        }.bind(this));
446
    },
447
 
448
    /**
449
     * Finds the section that would become empty if we remove the selected slots.
450
     *
451
     * @protected
452
     * @method find_sections_that_would_become_empty
453
     * @returns {String} The name of the first section found
454
     */
455
    find_sections_that_would_become_empty: function() {
456
        var section;
457
        var sectionnodes = Y.all(SELECTOR.SECTIONLI);
458
 
459
        if (sectionnodes.size() > 1) {
460
            sectionnodes.some(function(node) {
461
                var sectionname = node.one(SELECTOR.INSTANCESECTION).getContent();
462
                var checked = node.all(SELECTOR.SELECTMULTIPLECHECKBOX + ':checked');
463
                var unchecked = node.all(SELECTOR.SELECTMULTIPLECHECKBOX + ':not(:checked)');
464
 
465
                if (!checked.isEmpty() && unchecked.isEmpty()) {
466
                    section = sectionname;
467
                }
468
 
469
                return section;
470
            });
471
        }
472
 
473
        return section;
474
    },
475
 
476
    /**
477
     * Takes care of what needs to happen when the user clicks on the delete multiple button.
478
     *
479
     * @protected
480
     * @method delete_multiple_action
481
     * @param {EventFacade} ev The event that was fired.
482
     */
483
    delete_multiple_action: function(ev) {
484
        var problemsection = this.find_sections_that_would_become_empty();
485
 
486
        if (typeof problemsection !== 'undefined') {
487
            require(['core/notification'], function(Notification) {
488
                Notification.alert(
489
                    M.util.get_string('cannotremoveslots', 'quiz'),
490
                    M.util.get_string('cannotremoveallsectionslots', 'quiz', problemsection)
491
                );
492
            });
493
        } else {
494
            this.delete_multiple_with_confirmation(ev);
495
        }
496
    },
497
 
498
    /**
499
     * Deletes the given activities or resources after confirmation.
500
     *
501
     * @protected
502
     * @method delete_multiple_with_confirmation
503
     * @param {EventFacade} ev The event that was fired.
504
     */
505
    delete_multiple_with_confirmation: function(ev) {
506
        ev.preventDefault();
507
 
508
        var ids = '';
509
        var slots = [];
510
        Y.all(SELECTOR.SELECTMULTIPLECHECKBOX + ':checked').each(function(node) {
511
            var slot = Y.Moodle.mod_quiz.util.slot.getSlotFromComponent(node);
512
            ids += ids === '' ? '' : ',';
513
            ids += Y.Moodle.mod_quiz.util.slot.getId(slot);
514
            slots.push(slot);
515
        });
516
        var element = Y.one('div.mod-quiz-edit-content');
517
 
518
        // Do nothing if no slots are selected.
519
        if (!slots || !slots.length) {
520
            return;
521
        }
522
 
523
        require(['core/notification'], function(Notification) {
524
            Notification.saveCancelPromise(
525
                M.util.get_string('confirm', 'moodle'),
526
                M.util.get_string('areyousureremoveselected', 'quiz'),
527
                M.util.get_string('yes', 'moodle')
528
            ).then(function() {
529
                var spinner = this.add_spinner(element);
530
                var data = {
531
                    'class': 'resource',
532
                    field: 'deletemultiple',
533
                    ids: ids
534
                };
535
                // Delete items on server.
536
                this.send_request(data, spinner, function(response) {
537
                    // Delete locally if deleted on server.
538
                    if (response.deleted) {
539
                        // Actually remove the element.
540
                        Y.all(SELECTOR.SELECTMULTIPLECHECKBOX + ':checked').each(function(node) {
541
                            Y.Moodle.mod_quiz.util.slot.remove(node.ancestor('li.activity'));
542
                        });
543
                        // Update the page numbers and sections.
544
                        this.reorganise_edit_page();
545
 
546
                        // Remove the select multiple options.
547
                        Y.one('body').removeClass(CSS.SELECTMULTIPLE);
548
                    }
549
                });
550
 
551
                return;
552
            }.bind(this)).catch(function() {
553
                // User cancelled.
554
            });
555
        }.bind(this));
556
    },
557
 
558
    /**
559
     * Edit the maxmark for the resource
560
     *
561
     * @protected
562
     * @method edit_maxmark
563
     * @param {EventFacade} ev The event that was fired.
564
     * @param {Node} button The button that triggered this action.
565
     * @param {Node} activity The activity node that this action will be performed on.
566
     * @param {String} action The action that has been requested.
567
     * @return Boolean
568
     */
569
    edit_maxmark: function(ev, button, activity) {
570
        // Get the element we're working on
571
        var instancemaxmark = activity.one(SELECTOR.INSTANCEMAXMARK),
572
            instance = activity.one(SELECTOR.ACTIVITYINSTANCE),
573
            currentmaxmark = instancemaxmark.get('firstChild'),
574
            oldmaxmark = currentmaxmark.get('data'),
575
            maxmarktext = oldmaxmark,
576
            thisevent,
577
            anchor = instancemaxmark, // Grab the anchor so that we can swap it with the edit form.
578
            data = {
579
                'class': 'resource',
580
                'field': 'getmaxmark',
581
                'id': Y.Moodle.mod_quiz.util.slot.getId(activity)
582
            };
583
 
584
        // Prevent the default actions.
585
        ev.preventDefault();
586
 
587
        this.send_request(data, null, function(response) {
588
            if (M.core.actionmenu && M.core.actionmenu.instance) {
589
                M.core.actionmenu.instance.hideMenu(ev);
590
            }
591
 
592
            // Try to retrieve the existing string from the server.
593
            if (response.instancemaxmark) {
594
                maxmarktext = response.instancemaxmark;
595
            }
596
 
597
            // Create the editor and submit button.
598
            var editform = Y.Node.create('<form action="#" />');
599
            var editinstructions = Y.Node.create('<span class="' + CSS.EDITINSTRUCTIONS + '" id="id_editinstructions" />')
600
                .set('innerHTML', M.util.get_string('edittitleinstructions', 'moodle'));
601
            var editor = Y.Node.create('<input name="maxmark" type="text" class="' + CSS.TITLEEDITOR + '" />').setAttrs({
602
                'value': maxmarktext,
603
                'autocomplete': 'off',
604
                'aria-describedby': 'id_editinstructions',
605
                'maxLength': '12',
606
                'size': parseInt(this.get('config').questiondecimalpoints, 10) + 2
607
            });
608
 
609
            // Clear the existing content and put the editor in.
610
            editform.appendChild(editor);
611
            editform.setData('anchor', anchor);
612
            instance.insert(editinstructions, 'before');
613
            anchor.replace(editform);
614
 
615
            // We hide various components whilst editing:
616
            activity.addClass(CSS.EDITINGMAXMARK);
617
 
618
            // Focus and select the editor text.
619
            editor.focus().select();
620
 
621
            // Cancel the edit if we lose focus or the escape key is pressed.
622
            thisevent = editor.on('blur', this.edit_maxmark_cancel, this, activity, false);
623
            this.editmaxmarkevents.push(thisevent);
624
            thisevent = editor.on('key', this.edit_maxmark_cancel, 'esc', this, activity, true);
625
            this.editmaxmarkevents.push(thisevent);
626
 
627
            // Handle form submission.
628
            thisevent = editform.on('submit', this.edit_maxmark_submit, this, activity, oldmaxmark);
629
            this.editmaxmarkevents.push(thisevent);
630
        });
631
    },
632
 
633
    /**
634
     * Handles the submit event when editing the activity or resources maxmark.
635
     *
636
     * @protected
637
     * @method edit_maxmark_submit
638
     * @param {EventFacade} ev The event that triggered this.
639
     * @param {Node} activity The activity whose maxmark we are altering.
640
     * @param {String} originalmaxmark The original maxmark the activity or resource had.
641
     */
642
    edit_maxmark_submit: function(ev, activity, originalmaxmark) {
643
        // We don't actually want to submit anything.
644
        ev.preventDefault();
645
        var newmaxmark = Y.Lang.trim(activity.one(SELECTOR.ACTIVITYFORM + ' ' + SELECTOR.ACTIVITYMAXMARK).get('value'));
646
        var spinner = this.add_spinner(activity);
647
        this.edit_maxmark_clear(activity);
648
        activity.one(SELECTOR.INSTANCEMAXMARK).setContent(newmaxmark);
649
        if (newmaxmark !== null && newmaxmark !== "" && newmaxmark !== originalmaxmark) {
650
            var data = {
651
                'class': 'resource',
652
                'field': 'updatemaxmark',
653
                'maxmark': newmaxmark,
654
                'id': Y.Moodle.mod_quiz.util.slot.getId(activity)
655
            };
656
            this.send_request(data, spinner, function(response) {
657
                if (response.instancemaxmark) {
658
                    activity.one(SELECTOR.INSTANCEMAXMARK).setContent(response.instancemaxmark);
659
                }
660
            });
661
        }
662
    },
663
 
664
    /**
665
     * Handles the cancel event when editing the activity or resources maxmark.
666
     *
667
     * @protected
668
     * @method edit_maxmark_cancel
669
     * @param {EventFacade} ev The event that triggered this.
670
     * @param {Node} activity The activity whose maxmark we are altering.
671
     * @param {Boolean} preventdefault If true we should prevent the default action from occuring.
672
     */
673
    edit_maxmark_cancel: function(ev, activity, preventdefault) {
674
        if (preventdefault) {
675
            ev.preventDefault();
676
        }
677
        this.edit_maxmark_clear(activity);
678
    },
679
 
680
    /**
681
     * Handles clearing the editing UI and returning things to the original state they were in.
682
     *
683
     * @protected
684
     * @method edit_maxmark_clear
685
     * @param {Node} activity  The activity whose maxmark we were altering.
686
     */
687
    edit_maxmark_clear: function(activity) {
688
        // Detach all listen events to prevent duplicate triggers
689
        new Y.EventHandle(this.editmaxmarkevents).detach();
690
 
691
        var editform = activity.one(SELECTOR.ACTIVITYFORM),
692
            instructions = activity.one('#id_editinstructions');
693
        if (editform) {
694
            editform.replace(editform.getData('anchor'));
695
        }
696
        if (instructions) {
697
            instructions.remove();
698
        }
699
 
700
        // Remove the editing class again to revert the display.
701
        activity.removeClass(CSS.EDITINGMAXMARK);
702
 
703
        // Refocus the link which was clicked originally so the user can continue using keyboard nav.
704
        Y.later(100, this, function() {
705
            activity.one(SELECTOR.EDITMAXMARK).focus();
706
        });
707
 
708
        // TODO MDL-50768 This hack is to keep Behat happy until they release a version of
709
        // MinkSelenium2Driver that fixes
710
        // https://github.com/Behat/MinkSelenium2Driver/issues/80.
711
        if (!Y.one('input[name=maxmark')) {
712
            Y.one('body').append('<input type="text" name="maxmark" style="display: none">');
713
        }
714
    },
715
 
716
    /**
717
     * Joins or separates the given slot with the page of the previous slot. Reorders the pages of
718
     * the other slots
719
     *
720
     * @protected
721
     * @method update_page_break
722
     * @param {EventFacade} ev The event that was fired.
723
     * @param {Node} button The button that triggered this action.
724
     * @param {Node} activity The activity node that this action will be performed on.
725
     * @param {String} action The action, addpagebreak or removepagebreak.
726
     * @chainable
727
     */
728
    update_page_break: function(ev, button, activity, action) {
729
        // Prevent the default button action
730
        ev.preventDefault();
731
 
732
        var nextactivity = activity.next('li.activity.slot');
733
        var spinner = this.add_spinner(nextactivity);
734
        var value = action === 'removepagebreak' ? 1 : 2;
735
 
736
        var data = {
737
            'class': 'resource',
738
            'field': 'updatepagebreak',
739
            'id':    Y.Moodle.mod_quiz.util.slot.getId(nextactivity),
740
            'value': value
741
        };
742
 
743
        this.send_request(data, spinner, function(response) {
744
            if (response.slots) {
745
                if (action === 'addpagebreak') {
746
                    Y.Moodle.mod_quiz.util.page.add(activity);
747
                } else {
748
                    var page = activity.next(Y.Moodle.mod_quiz.util.page.SELECTORS.PAGE);
749
                    Y.Moodle.mod_quiz.util.page.remove(page, true);
750
                }
751
                this.reorganise_edit_page();
752
            }
753
        });
754
 
755
        return this;
756
    },
757
 
758
    /**
759
     * Updates a slot to either require the question in the previous slot to
760
     * have been answered, or not,
761
     *
762
     * @protected
763
     * @method update_page_break
764
     * @param {EventFacade} ev The event that was fired.
765
     * @param {Node} button The button that triggered this action.
766
     * @param {Node} activity The activity node that this action will be performed on.
767
     * @param {String} action The action, adddependency or removedependency.
768
     * @chainable
769
     */
770
    update_dependency: function(ev, button, activity, action) {
771
        // Prevent the default button action.
772
        ev.preventDefault();
773
        var spinner = this.add_spinner(activity);
774
 
775
        var data = {
776
            'class': 'resource',
777
            'field': 'updatedependency',
778
            'id':    Y.Moodle.mod_quiz.util.slot.getId(activity),
779
            'value': action === 'adddependency' ? 1 : 0
780
        };
781
 
782
        this.send_request(data, spinner, function(response) {
783
            if (response.hasOwnProperty('requireprevious')) {
784
                Y.Moodle.mod_quiz.util.slot.updateDependencyIcon(activity, response.requireprevious);
785
            }
786
        });
787
 
788
        return this;
789
    },
790
 
791
    /**
792
     * Reorganise the UI after every edit action.
793
     *
794
     * @protected
795
     * @method reorganise_edit_page
796
     */
797
    reorganise_edit_page: function() {
798
        Y.Moodle.mod_quiz.util.slot.reorderSlots();
799
        Y.Moodle.mod_quiz.util.slot.reorderPageBreaks();
800
        Y.Moodle.mod_quiz.util.page.reorderPages();
801
        Y.Moodle.mod_quiz.util.slot.updateOneSlotSections();
802
        Y.Moodle.mod_quiz.util.slot.updateAllDependencyIcons();
803
    },
804
 
805
    NAME: 'mod_quiz-resource-toolbox',
806
    ATTRS: {
807
        courseid: {
808
            'value': 0
809
        },
810
        quizid: {
811
            'value': 0
812
        }
813
    }
814
 
815
});
816
 
817
M.mod_quiz.resource_toolbox = null;
818
M.mod_quiz.init_resource_toolbox = function(config) {
819
    M.mod_quiz.resource_toolbox = new RESOURCETOOLBOX(config);
820
    return M.mod_quiz.resource_toolbox;
821
};
822
/**
823
 * Section toolbox class.
824
 *
825
 * This class is responsible for managing AJAX interactions with sections
826
 * when adding, editing, removing section headings.
827
 *
828
 * @module moodle-mod_quiz-toolboxes
829
 * @namespace M.mod_quiz.toolboxes
830
 */
831
 
832
/**
833
 * Section toolbox class.
834
 *
835
 * This class is responsible for managing AJAX interactions with sections
836
 * when adding, editing, removing section headings when editing a quiz.
837
 *
838
 * @class section
839
 * @constructor
840
 * @extends M.mod_quiz.toolboxes.toolbox
841
 */
842
var SECTIONTOOLBOX = function() {
843
    SECTIONTOOLBOX.superclass.constructor.apply(this, arguments);
844
};
845
 
846
Y.extend(SECTIONTOOLBOX, TOOLBOX, {
847
    /**
848
     * An Array of events added when editing a max mark field.
849
     * These should all be detached when editing is complete.
850
     *
851
     * @property editsectionevents
852
     * @protected
853
     * @type Array
854
     * @protected
855
     */
856
    editsectionevents: [],
857
 
858
    /**
859
     * Initialize the section toolboxes module.
860
     *
861
     * Updates all span.commands with relevant handlers and other required changes.
862
     *
863
     * @method initializer
864
     * @protected
865
     */
866
    initializer: function() {
867
        M.mod_quiz.quizbase.register_module(this);
868
 
869
        BODY.delegate('key', this.handle_data_action, 'down:enter', SELECTOR.ACTIVITYACTION, this);
870
        Y.delegate('click', this.handle_data_action, BODY, SELECTOR.ACTIVITYACTION, this);
871
        Y.delegate('change', this.handle_data_action, BODY, SELECTOR.EDITSHUFFLEQUESTIONSACTION, this);
872
    },
873
 
874
    /**
875
     * Handles the delegation event. When this is fired someone has triggered an action.
876
     *
877
     * Note not all actions will result in an AJAX enhancement.
878
     *
879
     * @protected
880
     * @method handle_data_action
881
     * @param {EventFacade} ev The event that was triggered.
882
     * @returns {boolean}
883
     */
884
    handle_data_action: function(ev) {
885
        // We need to get the anchor element that triggered this event.
886
        var node = ev.target;
887
        if (!node.test('a') && !node.test('input[data-action]')) {
888
            node = node.ancestor(SELECTOR.ACTIVITYACTION);
889
        }
890
 
891
        // From the anchor we can get both the activity (added during initialisation) and the action being
892
        // performed (added by the UI as a data attribute).
893
        var action = node.getData('action'),
894
            activity = node.ancestor(SELECTOR.ACTIVITYLI);
895
 
896
        if ((!node.test('a') && !node.test('input[data-action]')) || !action || !activity) {
897
            // It wasn't a valid action node.
898
            return;
899
        }
900
 
901
        // Switch based upon the action and do the desired thing.
902
        switch (action) {
903
            case 'edit_section_title':
904
                // The user wishes to edit the section headings.
905
                this.edit_section_title(ev, node, activity, action);
906
                break;
907
            case 'shuffle_questions':
908
                // The user wishes to edit the shuffle questions of the section (resource).
909
                this.edit_shuffle_questions(ev, node, activity, action);
910
                break;
911
            case 'deletesection':
912
                // The user is deleting the activity.
913
                this.delete_section_with_confirmation(ev, node, activity, action);
914
                break;
915
            default:
916
                // Nothing to do here!
917
                break;
918
        }
919
    },
920
 
921
    /**
922
     * Deletes the given section heading after confirmation.
923
     *
924
     * @protected
925
     * @method delete_section_with_confirmation
926
     * @param {EventFacade} ev The event that was fired.
927
     * @param {Node} button The button that triggered this action.
928
     * @param {Node} activity The activity node that this action will be performed on.
929
     * @chainable
930
     */
931
    delete_section_with_confirmation: function(ev, button, activity) {
932
        ev.preventDefault();
933
        require(['core/notification'], function(Notification) {
934
            Notification.saveCancelPromise(
935
                M.util.get_string('confirm', 'moodle'),
936
                M.util.get_string('confirmremovesectionheading', 'quiz', activity.getData('sectionname')),
937
                M.util.get_string('yes', 'moodle')
938
            ).then(function() {
939
                var spinner = M.util.add_spinner(Y, activity.one(SELECTOR.ACTIONAREA));
940
                var data = {
941
                    'class': 'section',
942
                    'action': 'DELETE',
943
                    'id': activity.get('id').replace('section-', '')
944
                };
945
                this.send_request(data, spinner, function(response) {
946
                    if (response.deleted) {
947
                        window.location.reload(true);
948
                    }
949
                });
950
 
951
                return;
952
            }.bind(this)).catch(function() {
953
                // User cancelled.
954
            });
955
        }.bind(this));
956
    },
957
 
958
    /**
959
     * Edit the edit section title for the section
960
     *
961
     * @protected
962
     * @method edit_section_title
963
     * @param {EventFacade} ev The event that was fired.
964
     * @param {Node} button The button that triggered this action.
965
     * @param {Node} activity The activity node that this action will be performed on.
966
     * @param {String} action The action that has been requested.
967
     * @return Boolean
968
     */
969
    edit_section_title: function(ev, button, activity) {
970
        // Get the element we're working on
971
        var activityid = activity.get('id').replace('section-', ''),
972
            instancesection = activity.one(SELECTOR.INSTANCESECTION),
973
            thisevent,
974
            anchor = instancesection, // Grab the anchor so that we can swap it with the edit form.
975
            data = {
976
                'class': 'section',
977
                'field': 'getsectiontitle',
978
                'id':    activityid
979
            };
980
 
981
        // Prevent the default actions.
982
        ev.preventDefault();
983
 
984
        this.send_request(data, null, function(response) {
985
            // Try to retrieve the existing string from the server.
986
            var oldtext = response.instancesection;
987
 
988
            // Create the editor and submit button.
989
            var editform = Y.Node.create('<form action="#" />');
990
            var editinstructions = Y.Node.create('<span class="' + CSS.EDITINSTRUCTIONS + '" id="id_editinstructions" />')
991
                .set('innerHTML', M.util.get_string('edittitleinstructions', 'moodle'));
992
            var editor = Y.Node.create('<input name="section" type="text" />').setAttrs({
993
                'value': oldtext,
994
                'autocomplete': 'off',
995
                'aria-describedby': 'id_editinstructions',
996
                'maxLength': '255' // This is the maxlength in DB.
997
            });
998
 
999
            // Clear the existing content and put the editor in.
1000
            editform.appendChild(editor);
1001
            editform.setData('anchor', anchor);
1002
            instancesection.insert(editinstructions, 'before');
1003
            anchor.replace(editform);
1004
 
1005
            // Focus and select the editor text.
1006
            editor.focus().select();
1007
            // Cancel the edit if we lose focus or the escape key is pressed.
1008
            thisevent = editor.on('blur', this.edit_section_title_cancel, this, activity, false);
1009
            this.editsectionevents.push(thisevent);
1010
            thisevent = editor.on('key', this.edit_section_title_cancel, 'esc', this, activity, true);
1011
            this.editsectionevents.push(thisevent);
1012
            // Handle form submission.
1013
            thisevent = editform.on('submit', this.edit_section_title_submit, this, activity, oldtext);
1014
            this.editsectionevents.push(thisevent);
1015
        });
1016
    },
1017
 
1018
    /**
1019
     * Handles the submit event when editing section heading.
1020
     *
1021
     * @protected
1022
     * @method edit_section_title_submiy
1023
     * @param {EventFacade} ev The event that triggered this.
1024
     * @param {Node} activity The activity whose maxmark we are altering.
1025
     * @param {String} oldtext The original maxmark the activity or resource had.
1026
     */
1027
    edit_section_title_submit: function(ev, activity, oldtext) {
1028
         // We don't actually want to submit anything.
1029
        ev.preventDefault();
1030
        var newtext = Y.Lang.trim(activity.one(SELECTOR.SECTIONFORM + ' ' + SELECTOR.SECTIONINPUT).get('value'));
1031
        var spinner = M.util.add_spinner(Y, activity.one(SELECTOR.INSTANCESECTIONAREA));
1032
        this.edit_section_title_clear(activity);
1033
        if (newtext !== null && newtext !== oldtext) {
1034
            var instancesection = activity.one(SELECTOR.INSTANCESECTION);
1035
            var instancesectiontext = newtext;
1036
            if (newtext.trim() === '') {
1037
                // Add a sr-only default section heading text to make sure we don't end up with an empty section heading.
1038
                instancesectiontext = M.util.get_string('sectionnoname', 'quiz');
1039
                instancesection.addClass('sr-only');
1040
            } else {
1041
                // Show the section heading when a non-empty value is set.
1042
                instancesection.removeClass('sr-only');
1043
            }
1044
            instancesection.setContent(instancesectiontext);
1045
 
1046
            var data = {
1047
                'class':      'section',
1048
                'field':      'updatesectiontitle',
1049
                'newheading': newtext,
1050
                'id':         activity.get('id').replace('section-', '')
1051
            };
1052
            this.send_request(data, spinner, function(response) {
1053
                if (response) {
1054
                    // Set the content of the section heading if for some reason the response is different from the new text.
1055
                    // e.g. filters were applied, the update failed, etc.
1056
                    if (newtext !== response.instancesection) {
1057
                        if (response.instancesection.trim() === '') {
1058
                            // Add a sr-only default section heading text.
1059
                            instancesectiontext = M.util.get_string('sectionnoname', 'quiz');
1060
                            instancesection.addClass('sr-only');
1061
                        } else {
1062
                            instancesectiontext = response.instancesection;
1063
                            // Show the section heading when a non-empty value is set.
1064
                            instancesection.removeClass('sr-only');
1065
                        }
1066
                        instancesection.setContent(instancesectiontext);
1067
                    }
1068
 
1069
                    activity.one(SELECTOR.EDITSECTIONICON).set('title',
1070
                            M.util.get_string('sectionheadingedit', 'quiz', response.instancesection));
1071
                    activity.one(SELECTOR.EDITSECTIONICON).set('alt',
1072
                            M.util.get_string('sectionheadingedit', 'quiz', response.instancesection));
1073
                    var deleteicon = activity.one(SELECTOR.DELETESECTIONICON);
1074
                    if (deleteicon) {
1075
                        deleteicon.set('title', M.util.get_string('sectionheadingremove', 'quiz', response.instancesection));
1076
                        deleteicon.set('alt', M.util.get_string('sectionheadingremove', 'quiz', response.instancesection));
1077
                    }
1078
                }
1079
            });
1080
        }
1081
    },
1082
 
1083
    /**
1084
     * Handles the cancel event when editing the activity or resources maxmark.
1085
     *
1086
     * @protected
1087
     * @method edit_maxmark_cancel
1088
     * @param {EventFacade} ev The event that triggered this.
1089
     * @param {Node} activity The activity whose maxmark we are altering.
1090
     * @param {Boolean} preventdefault If true we should prevent the default action from occuring.
1091
     */
1092
    edit_section_title_cancel: function(ev, activity, preventdefault) {
1093
        if (preventdefault) {
1094
            ev.preventDefault();
1095
        }
1096
        this.edit_section_title_clear(activity);
1097
    },
1098
 
1099
    /**
1100
     * Handles clearing the editing UI and returning things to the original state they were in.
1101
     *
1102
     * @protected
1103
     * @method edit_maxmark_clear
1104
     * @param {Node} activity  The activity whose maxmark we were altering.
1105
     */
1106
    edit_section_title_clear: function(activity) {
1107
        // Detach all listen events to prevent duplicate triggers
1108
        new Y.EventHandle(this.editsectionevents).detach();
1109
 
1110
        var editform = activity.one(SELECTOR.SECTIONFORM),
1111
            instructions = activity.one('#id_editinstructions');
1112
        if (editform) {
1113
            editform.replace(editform.getData('anchor'));
1114
        }
1115
        if (instructions) {
1116
            instructions.remove();
1117
        }
1118
 
1119
        // Refocus the link which was clicked originally so the user can continue using keyboard nav.
1120
        Y.later(100, this, function() {
1121
            activity.one(SELECTOR.EDITSECTION).focus();
1122
        });
1123
 
1124
        // This hack is to keep Behat happy until they release a version of
1125
        // MinkSelenium2Driver that fixes
1126
        // https://github.com/Behat/MinkSelenium2Driver/issues/80.
1127
        if (!Y.one('input[name=section]')) {
1128
            Y.one('body').append('<input type="text" name="section" style="display: none">');
1129
        }
1130
    },
1131
 
1132
    /**
1133
     * Edit the edit shuffle questions for the section
1134
     *
1135
     * @protected
1136
     * @method edit_shuffle_questions
1137
     * @param {EventFacade} ev The event that was fired.
1138
     * @param {Node} button The button that triggered this action.
1139
     * @param {Node} activity The activity node that this action will be performed on.
1140
     * @return Boolean
1141
     */
1142
    edit_shuffle_questions: function(ev, button, activity) {
1143
        var newvalue;
1144
        if (activity.one(SELECTOR.EDITSHUFFLEQUESTIONSACTION).get('checked')) {
1145
            newvalue = 1;
1146
            activity.addClass('shuffled');
1147
        } else {
1148
            newvalue = 0;
1149
            activity.removeClass('shuffled');
1150
        }
1151
 
1152
        // Prevent the default actions.
1153
        ev.preventDefault();
1154
 
1155
        // Get the element we're working on
1156
        var data = {
1157
            'class': 'section',
1158
            'field': 'updateshufflequestions',
1159
            'id': activity.get('id').replace('section-', ''),
1160
            'newshuffle': newvalue
1161
        };
1162
 
1163
        // Send request.
1164
        var spinner = M.util.add_spinner(Y, activity.one(SELECTOR.EDITSHUFFLEAREA));
1165
        this.send_request(data, spinner);
1166
    }
1167
 
1168
}, {
1169
    NAME: 'mod_quiz-section-toolbox',
1170
    ATTRS: {
1171
        courseid: {
1172
            'value': 0
1173
        },
1174
        quizid: {
1175
            'value': 0
1176
        }
1177
    }
1178
});
1179
 
1180
M.mod_quiz.init_section_toolbox = function(config) {
1181
    return new SECTIONTOOLBOX(config);
1182
};
1183
 
1184
 
1185
}, '@VERSION@', {
1186
    "requires": [
1187
        "base",
1188
        "node",
1189
        "event",
1190
        "event-key",
1191
        "io",
1192
        "moodle-mod_quiz-quizbase",
1193
        "moodle-mod_quiz-util-slot",
1194
        "moodle-core-notification-ajaxexception"
1195
    ]
1196
});