Proyectos de Subversion Moodle

Rev

| 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
 
16
/**
17
 * JavaScript for managing multiple grade items for a quiz.
18
 *
19
 * @module     mod_quiz/edit_multiple_grades
20
 * @copyright  2023 The Open University
21
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22
 */
23
 
24
import {call as fetchMany} from 'core/ajax';
25
import MoodleConfig from 'core/config';
26
import {addIconToContainer} from 'core/loadingicon';
27
import Notification from 'core/notification';
28
import Pending from 'core/pending';
29
import {get_string as getString} from 'core/str';
30
import {render as renderTemplate} from 'core/templates';
31
import {replaceNode} from 'core/templates';
32
 
33
/**
34
 * @type {Object} selectors used in this code.
35
 */
36
const SELECTORS = {
37
    'addGradeItemButton': '#mod_quiz-add_grade_item',
38
    'autoSetupButton': '#mod_quiz-grades_auto_setup',
39
    'editingPageContents': '#edit_grading_page-contents',
40
    'gradeItemList': 'table#mod_quiz-grade-item-list',
41
    'gradeItemSelect': 'select[data-slot-id]',
42
    'gradeItemSelectId': (id) => 'select#grade-item-choice-' + id,
43
    'gradeItemTr': 'table#mod_quiz-grade-item-list tr[data-quiz-grade-item-id]',
44
    'inplaceEditable': 'span.inplaceeditable',
45
    'inplaceEditableOn': 'span.inplaceeditable.inplaceeditingon',
46
    'resetAllButton': '#mod_quiz-grades_reset_all',
47
    'slotList': 'table#mod_quiz-slot-list',
48
    'updateGradeItemLink': (id) => 'tr[data-quiz-grade-item-id="' + id + '"] .quickeditlink',
49
};
50
 
51
/**
52
 * Call the Ajax service to create a quiz grade item.
53
 *
54
 * @param {Number} quizId id of the quiz to update.
55
 * @returns {Promise<Object>} a promise that resolves to the template context required to re-render the page.
56
 */
57
const createGradeItem = (
58
    quizId,
59
) => callServiceAndReturnRenderingData({
60
    methodname: 'mod_quiz_create_grade_items',
61
    args: {
62
        quizid: quizId,
63
        quizgradeitems: [{name: ''}],
64
    }
65
});
66
 
67
/**
68
 * Call the Ajax service to update a quiz grade item.
69
 *
70
 * @param {Number} quizId id of the quiz to update.
71
 * @param {Number} gradeItemId id of the grade item to update.
72
 * @param {String} newName the new name to set.
73
 * @return {Promise} Promise that resolves to the context required to re-render the page.
74
 */
75
const updateGradeItem = (
76
    quizId,
77
    gradeItemId,
78
    newName
79
) => callServiceAndReturnRenderingData({
80
    methodname: 'mod_quiz_update_grade_items',
81
    args: {
82
        quizid: quizId,
83
        quizgradeitems: [{id: gradeItemId, name: newName}],
84
    }
85
});
86
 
87
/**
88
 * Call the Ajax service to delete a quiz grade item.
89
 *
90
 * @param {Number} quizId id of the quiz to update.
91
 * @param {Number} gradeItemId id of the grade item to delete.
92
 * @return {Promise} Promise that resolves to the context required to re-render the page.
93
 */
94
const deleteGradeItem = (
95
    quizId,
96
    gradeItemId
97
) => callServiceAndReturnRenderingData({
98
    methodname: 'mod_quiz_delete_grade_items',
99
    args: {
100
        quizid: quizId,
101
        quizgradeitems: [{id: gradeItemId}],
102
    }
103
});
104
 
105
/**
106
 * Call the Ajax service to update the quiz grade item used by a slot.
107
 *
108
 * @param {Number} quizId id of the quiz to update.
109
 * @param {Number} slotId id of the slot to update.
110
 * @param {Number|null} gradeItemId new grade item ot set, or null to un-set.
111
 * @return {Promise} Promise that resolves to the context required to re-render the page.
112
 */
113
const updateSlotGradeItem = (
114
    quizId,
115
    slotId,
116
    gradeItemId
117
) => callServiceAndReturnRenderingData({
118
    methodname: 'mod_quiz_update_slots',
119
    args: {
120
        quizid: quizId,
121
        slots: [{id: slotId, quizgradeitemid: gradeItemId}],
122
    }
123
});
124
 
125
/**
126
 * Call the Ajax service to setup one grade item for each quiz section.
127
 *
128
 * @param {Number} quizId id of the quiz to update.
129
 * @return {Promise} Promise that resolves to the context required to re-render the page.
130
 */
131
const autoSetupGradeItems = (
132
    quizId
133
) => callServiceAndReturnRenderingData({
134
    methodname: 'mod_quiz_create_grade_item_per_section',
135
    args: {
136
        quizid: quizId
137
    }
138
});
139
 
140
/**
141
 * Make a web service call, and also call mod_quiz_get_edit_grading_page_data to get the date to re-render the page.
142
 *
143
 * @param {Object} methodCall a web service call to pass to fetchMany. Must include methodCall.args.quizid.
144
 * @returns {Promise<Object>} a promise that resolves to the template context required to re-render the page.
145
 */
146
const callServiceAndReturnRenderingData = (methodCall) => callServicesAndReturnRenderingData([methodCall]);
147
 
148
/**
149
 * Make a web service call, and also call mod_quiz_get_edit_grading_page_data to get the date to re-render the page.
150
 *
151
 * @param {Object[]} methodCalls web service calls to pass to fetchMany. Must include methodCalls[0].args.quizid.
152
 * @returns {Promise<Object>} a promise that resolves to the template context required to re-render the page.
153
 */
154
const callServicesAndReturnRenderingData = (methodCalls) => {
155
    methodCalls.push({
156
            methodname: 'mod_quiz_get_edit_grading_page_data',
157
            args: {
158
                quizid: methodCalls[0].args.quizid,
159
            }
160
        });
161
    return Promise.all(fetchMany(methodCalls))
162
    .then(results => JSON.parse(results.at(-1)));
163
};
164
 
165
/**
166
 * Handle click events on the delete icon.
167
 *
168
 * @param {Event} e click event.
169
 */
170
const handleGradeItemDelete = (e) => {
171
    e.preventDefault();
172
    const pending = new Pending('delete-quiz-grade-item');
173
 
174
    const tableCell = e.target.closest('td');
175
    addIconToContainer(tableCell, pending);
176
 
177
    const tableRow = tableCell.closest('tr');
178
    const quizId = tableRow.closest('table').dataset.quizId;
179
    const gradeItemId = tableRow.dataset.quizGradeItemId;
180
 
181
    let nextItemToFocus;
182
    if (tableRow.nextElementSibling) {
183
        nextItemToFocus = SELECTORS.updateGradeItemLink(tableRow.nextElementSibling.dataset.quizGradeItemId);
184
    } else {
185
        nextItemToFocus = SELECTORS.addGradeItemButton;
186
    }
187
 
188
    deleteGradeItem(quizId, gradeItemId)
189
        .then(reRenderPage)
190
        .then(() => {
191
            pending.resolve();
192
            document.querySelector(nextItemToFocus).focus();
193
        })
194
        .catch(Notification.exception);
195
};
196
 
197
/**
198
 *
199
 * @param {HTMLElement} editableSpan the editable to turn off.
200
 */
201
const stopEditingGadeItem = (editableSpan) => {
202
    editableSpan.innerHTML = editableSpan.dataset.oldContent;
203
    delete editableSpan.dataset.oldContent;
204
 
205
    editableSpan.classList.remove('inplaceeditingon');
206
    editableSpan.querySelector('[data-action-edit]').focus();
207
};
208
 
209
/**
210
 * Handle click events on the start rename icon.
211
 *
212
 * @param {Event} e click event.
213
 */
214
const handleGradeItemEditStart = (e) => {
215
    e.preventDefault();
216
    const pending = new Pending('edit-quiz-grade-item-start');
217
    const editableSpan = e.target.closest(SELECTORS.inplaceEditable);
218
 
219
    document.querySelectorAll(SELECTORS.inplaceEditableOn).forEach(stopEditingGadeItem);
220
 
221
    editableSpan.dataset.oldContent = editableSpan.innerHTML;
222
    getString('edittitleinstructions')
223
        .then((instructions) => {
224
            const uniqueId = 'gi-edit-input-' + editableSpan.closest('tr').dataset.quizGradeItemId;
225
            editableSpan.innerHTML = '<span class="editinstructions">' + instructions + '</span>' +
226
                    '<label class="sr-only" for="' + uniqueId + '">' + editableSpan.dataset.editLabel + '</label>' +
227
                    '<input type="text" id="' + uniqueId + '" value="' + editableSpan.dataset.rawName +
228
                            '" class="ignoredirty form-control w-100">';
229
 
230
            const inputElement = editableSpan.querySelector('input');
231
            inputElement.focus();
232
            inputElement.select();
233
            editableSpan.classList.add('inplaceeditingon');
234
            pending.resolve();
235
            return null;
236
        })
237
        .catch(Notification.exception);
238
};
239
 
240
/**
241
 * Handle key down in the editable.
242
 *
243
 * @param {Event} e key event.
244
 */
245
const handleGradeItemKeyDown = (e) => {
246
    if (e.keyCode !== 13) {
247
        return;
248
    }
249
 
250
    const editableSpan = e.target.closest(SELECTORS.inplaceEditableOn);
251
 
252
    // Check this click is on a relevant element.
253
    if (!editableSpan || !editableSpan.closest(SELECTORS.gradeItemList)) {
254
        return;
255
    }
256
 
257
    e.preventDefault();
258
    const pending = new Pending('edit-quiz-grade-item-save');
259
 
260
    const newName = editableSpan.querySelector('input').value;
261
    const tableCell = e.target.closest('th');
262
    addIconToContainer(tableCell);
263
 
264
    const tableRow = tableCell.closest('tr');
265
    const quizId = tableRow.closest('table').dataset.quizId;
266
    const gradeItemId = tableRow.dataset.quizGradeItemId;
267
 
268
    updateGradeItem(quizId, gradeItemId, newName)
269
        .then(reRenderPage)
270
        .then(() => {
271
            pending.resolve();
272
            document.querySelector(SELECTORS.updateGradeItemLink(gradeItemId)).focus({'focusVisible': true});
273
        })
274
        .catch(Notification.exception);
275
};
276
 
277
/**
278
 * Replace the contents of the page with the page re-rendered from the provided data, once that promise resolves.
279
 *
280
 * @param {Object} editGradingPageData the template context data required to re-render the page.
281
 * @returns {Promise<void>} a promise that will resolve when the page is updated.
282
 */
283
const reRenderPage = (editGradingPageData) =>
284
    renderTemplate('mod_quiz/edit_grading_page', editGradingPageData)
285
        .then((html, js) => replaceNode(document.querySelector(SELECTORS.editingPageContents), html, js || ''));
286
 
287
/**
288
 * Handle key up in the editable.
289
 *
290
 * @param {Event} e key event.
291
 */
292
const handleGradeItemKeyUp = (e) => {
293
    if (e.keyCode !== 27) {
294
        return;
295
    }
296
 
297
    const editableSpan = e.target.closest(SELECTORS.inplaceEditableOn);
298
 
299
    // Check this click is on a relevant element.
300
    if (!editableSpan || !editableSpan.closest(SELECTORS.gradeItemList)) {
301
        return;
302
    }
303
 
304
    e.preventDefault();
305
    stopEditingGadeItem(editableSpan);
306
};
307
 
308
/**
309
 * Handle focus out of the editable.
310
 *
311
 * @param {Event} e event.
312
 */
313
const handleGradeItemFocusOut = (e) => {
314
    if (MoodleConfig.behatsiterunning) {
315
        // Behat triggers focusout too often so ignore.
316
        return;
317
    }
318
 
319
    const editableSpan = e.target.closest(SELECTORS.inplaceEditableOn);
320
 
321
    // Check this click is on a relevant element.
322
    if (!editableSpan || !editableSpan.closest(SELECTORS.gradeItemList)) {
323
        return;
324
    }
325
 
326
    e.preventDefault();
327
    stopEditingGadeItem(editableSpan);
328
};
329
 
330
/**
331
 * Handle when the selected grade item for a slot is changed.
332
 *
333
 * @param {Event} e event.
334
 */
335
const handleSlotGradeItemChanged = (e) => {
336
    const select = e.target.closest(SELECTORS.gradeItemSelect);
337
 
338
    // Check this click is on a relevant element.
339
    if (!select || !select.closest(SELECTORS.slotList)) {
340
        return;
341
    }
342
 
343
    e.preventDefault();
344
    const pending = new Pending('edit-slot-grade-item-updated');
345
 
346
    const slotId = select.dataset.slotId;
347
    const newGradeItemId = select.value ? select.value : null;
348
    const tableCell = e.target.closest('td');
349
    addIconToContainer(tableCell, pending);
350
 
351
    const quizId = tableCell.closest('table').dataset.quizId;
352
 
353
    updateSlotGradeItem(quizId, slotId, newGradeItemId)
354
        .then(reRenderPage)
355
        .then(() => {
356
            pending.resolve();
357
            document.querySelector(SELECTORS.gradeItemSelectId(slotId)).focus();
358
        })
359
        .catch(Notification.exception);
360
};
361
 
362
/**
363
 * Handle clicks in the table the shows the grade items.
364
 *
365
 * @param {Event} e click event.
366
 */
367
const handleGradeItemClick = (e) => {
368
    const link = e.target.closest('a');
369
 
370
    // Check this click is on a relevant element.
371
    if (!link || !link.closest(SELECTORS.gradeItemList)) {
372
        return;
373
    }
374
 
375
    if (link.dataset.actionDelete) {
376
        handleGradeItemDelete(e);
377
    }
378
 
379
    if (link.dataset.actionEdit) {
380
        handleGradeItemEditStart(e);
381
    }
382
};
383
 
384
/**
385
 * Handle clicks on the buttons.
386
 *
387
 * @param {Event} e click event.
388
 */
389
 
390
const handleButtonClick = (e) => {
391
    if (e.target.closest(SELECTORS.addGradeItemButton)) {
392
        handleAddGradeItemClick(e);
393
    }
394
    if (e.target.closest(SELECTORS.autoSetupButton)) {
395
        handleAutoSetup(e);
396
    }
397
    if (e.target.closest(SELECTORS.resetAllButton)) {
398
        handleResetAllClick(e);
399
    }
400
};
401
 
402
/**
403
 * Handle clicks on the 'Add grade item' button.
404
 *
405
 * @param {Event} e click event.
406
 */
407
const handleAddGradeItemClick = (e) => {
408
    e.preventDefault();
409
    const pending = new Pending('create-quiz-grade-item');
410
    addIconToContainer(e.target.parentNode, pending);
411
 
412
    const quizId = e.target.dataset.quizId;
413
 
414
    createGradeItem(quizId)
415
        .then(reRenderPage)
416
        .then(() => {
417
            pending.resolve();
418
            document.querySelector(SELECTORS.addGradeItemButton).focus();
419
        })
420
        .catch(Notification.exception);
421
};
422
 
423
/**
424
 * Handle clicks on the reset button - show a confirmation.
425
 *
426
 * @param {Event} e click event.
427
 */
428
const handleAutoSetup = (e) => {
429
    e.preventDefault();
430
    const pending = new Pending('setup-quiz-grade-items');
431
 
432
    const quizId = e.target.dataset.quizId;
433
 
434
    autoSetupGradeItems(quizId)
435
        .then(reRenderPage)
436
        .then(() => {
437
            pending.resolve();
438
            document.querySelector(SELECTORS.resetAllButton).focus();
439
        })
440
        .catch(Notification.exception);
441
};
442
 
443
/**
444
 * Handle clicks on the reset button - show a confirmation.
445
 *
446
 * @param {Event} e click event.
447
 */
448
const handleResetAllClick = (e) => {
449
    e.preventDefault();
450
    const button = e.target;
451
 
452
    Notification.deleteCancelPromise(
453
        getString('gradeitemsremoveallconfirm', 'quiz'),
454
        getString('gradeitemsremoveallmessage', 'quiz'),
455
        getString('reset'),
456
        button
457
    ).then(() => reallyResetAll(button))
458
    .catch(() => button.focus());
459
};
460
 
461
/**
462
 * Really reset all if the confirmation is OKed.
463
 *
464
 * @param {HTMLElement} button the reset button.
465
 */
466
const reallyResetAll = (button) => {
467
    const pending = new Pending('reset-quiz-grading');
468
    addIconToContainer(button.parentNode, pending);
469
 
470
    const quizId = button.dataset.quizId;
471
 
472
    let methodCalls = [];
473
 
474
    // Call to clear any assignments of grade items to slots (if required).
475
    const slotResets = [...document.querySelectorAll(SELECTORS.gradeItemSelect)].map(
476
            (select) => ({
477
                id: select.dataset.slotId,
478
                quizgradeitemid: 0,
479
            }));
480
    if (slotResets.length) {
481
        methodCalls.push({
482
            methodname: 'mod_quiz_update_slots',
483
            args: {
484
                quizid: quizId,
485
                slots: slotResets
486
            }
487
        });
488
    }
489
 
490
    // Request to delete all the grade items.
491
    methodCalls.push({
492
        methodname: 'mod_quiz_delete_grade_items',
493
        args: {
494
            quizid: quizId,
495
            quizgradeitems: [...document.querySelectorAll(SELECTORS.gradeItemTr)].map((tr) => {
496
                return {id: tr.dataset.quizGradeItemId};
497
            })
498
        }
499
    });
500
 
501
    callServicesAndReturnRenderingData(methodCalls)
502
        .then(reRenderPage)
503
        .then(() => {
504
            pending.resolve();
505
            document.querySelector(SELECTORS.addGradeItemButton).focus();
506
        })
507
        .catch(Notification.exception);
508
};
509
 
510
/**
511
 * Replace the container with a new version.
512
 */
513
const registerEventListeners = () => {
514
    document.body.addEventListener('click', handleGradeItemClick);
515
    document.body.addEventListener('keydown', handleGradeItemKeyDown);
516
    document.body.addEventListener('keyup', handleGradeItemKeyUp);
517
    document.body.addEventListener('focusout', handleGradeItemFocusOut);
518
 
519
    document.body.addEventListener('click', handleButtonClick);
520
 
521
    document.body.addEventListener('change', handleSlotGradeItemChanged);
522
};
523
 
524
/**
525
 * Entry point.
526
 */
527
export const init = () => {
528
    registerEventListeners();
529
};