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
 * Enhance the gradebook tree setup with various facilities.
18
 *
19
 * @module     core_grades/edittree_index
20
 * @copyright  2016 Andrew Nicols <andrew@nicols.co.uk>
21
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22
 */
23
 
24
import storage from 'core/localstorage';
25
import {addIconToContainer} from 'core/loadingicon';
26
import Notification from 'core/notification';
27
import Pending from 'core/pending';
28
 
29
const SELECTORS = {
30
    CATEGORY_TOGGLE: '.toggle-category',
31
    GRADEBOOK_SETUP_TABLE: '.setup-grades',
32
    WEIGHT_OVERRIDE_CHECKBOX: '.weightoverride',
33
    BULK_MOVE_SELECT: '#menumoveafter',
34
    BULK_MOVE_INPUT: '#bulkmoveinput',
35
    GRADEBOOK_SETUP_WRAPPER: '.gradetree-wrapper',
36
    GRADEBOOK_SETUP_BOX: '.gradetreebox'
37
};
38
 
39
/**
40
 * Register related event listeners.
41
 *
42
 * @method registerListenerEvents
43
 * @param {int} courseId The ID of course.
44
 * @param {int} userId The ID of the current logged user.
45
 */
46
const registerListenerEvents = (courseId, userId) => {
47
 
48
    document.addEventListener('change', e => {
49
        // Toggle the availability of the weight input field based on the changed state (checked/unchecked) of the
50
        // related checkbox element.
51
        if (e.target.matches(SELECTORS.WEIGHT_OVERRIDE_CHECKBOX)) {
52
            toggleWeightInput(e.target);
53
        }
54
        // Submit the bulk move form when the selected option in the bulk move select element has been changed.
55
        if (e.target.matches(SELECTORS.BULK_MOVE_SELECT)) {
56
            submitBulkMoveForm(e.target);
57
        }
58
    });
59
 
60
    const gradebookSetup = document.querySelector(SELECTORS.GRADEBOOK_SETUP_TABLE);
61
    gradebookSetup.addEventListener('click', e => {
62
        const toggle = e.target.closest(SELECTORS.CATEGORY_TOGGLE);
63
        // Collapse or expand the grade category when the visibility toggle button is activated.
64
        if (toggle) {
65
            e.preventDefault();
66
            toggleCategory(toggle, courseId, userId, true);
67
        }
68
    });
69
};
70
 
71
/**
72
 * Toggle the weight input field based on its checkbox.
73
 *
74
 * @method toggleWeightInput
75
 * @param {object} weightOverrideCheckbox The weight override checkbox element.
76
 */
77
const toggleWeightInput = (weightOverrideCheckbox) => {
78
    const row = weightOverrideCheckbox.closest('tr');
79
    const itemId = row.dataset.itemid;
80
    const weightOverrideInput = row.querySelector(`input[name="weight_${itemId}"]`);
81
    weightOverrideInput.disabled = !weightOverrideCheckbox.checked;
82
};
83
 
84
/**
85
 * Submit the bulk move form.
86
 *
87
 * @method toggleWeightInput
88
 * @param {object} bulkMoveSelect The bulk move select element.
89
 */
90
const submitBulkMoveForm = (bulkMoveSelect) => {
91
    const form = bulkMoveSelect.closest('form');
92
    const bulkMoveInput = form.querySelector(SELECTORS.BULK_MOVE_INPUT);
93
    bulkMoveInput.value = 1;
94
    form.submit();
95
};
96
 
97
/**
98
 * Method that collapses all relevant grade categories based on the locally stored state of collapsed grade categories
99
 * for a given user.
100
 *
101
 * @method collapseGradeCategories
102
 * @param {int} courseId The ID of course.
103
 * @param {int} userId The ID of the current logged user.
104
 */
105
const collapseGradeCategories = (courseId, userId) => {
106
    const gradebookSetup = document.querySelector(SELECTORS.GRADEBOOK_SETUP_TABLE);
107
    const storedCollapsedCategories = storage.get(`core_grade_collapsedgradecategories_${courseId}_${userId}`);
108
 
109
    if (storedCollapsedCategories) {
110
        // Fetch all grade categories that are locally stored as collapsed and re-apply the collapse action.
111
        const collapsedCategories = JSON.parse(storedCollapsedCategories);
112
 
113
        collapsedCategories.forEach((category) => {
114
            const categoryToggleElement =
115
                gradebookSetup.querySelector(`${SELECTORS.CATEGORY_TOGGLE}[data-category="${category}"`);
116
            if (categoryToggleElement) {
117
                toggleCategory(categoryToggleElement, courseId, userId, false);
118
            }
119
        });
120
    }
121
};
122
 
123
/**
124
 * Method that updates the locally stored state of collapsed grade categories based on a performed toggle action on a
125
 * given grade category.
126
 *
127
 * @method updateCollapsedCategoriesStoredState
128
 * @param {string} category The category to be added or removed from the collapsed grade categories local storage.
129
 * @param {int} courseId The ID of course.
130
 * @param {int} userId The ID of the current logged user.
131
 * @param {boolean} isCollapsing Whether the category is being collapsed or not.
132
 */
133
const updateCollapsedCategoriesStoredState = (category, courseId, userId, isCollapsing) => {
134
    const currentStoredCollapsedCategories = storage.get(`core_grade_collapsedgradecategories_${courseId}_${userId}`);
135
    let collapsedCategories = currentStoredCollapsedCategories ?
136
        JSON.parse(currentStoredCollapsedCategories) : [];
137
 
138
    if (isCollapsing) {
139
        collapsedCategories.push(category);
140
    } else {
141
        collapsedCategories = collapsedCategories.filter(cat => cat !== category);
142
    }
143
    storage.set(`core_grade_collapsedgradecategories_${courseId}_${userId}`, JSON.stringify(collapsedCategories));
144
};
145
 
146
/**
147
 * Method that handles the grade category toggle action.
148
 *
149
 * @method toggleCategory
150
 * @param {object} toggleElement The category toggle node that was clicked.
151
 * @param {int} courseId The ID of course.
152
 * @param {int} userId The ID of the current logged user.
153
 * @param {boolean} storeCollapsedState Whether to store (local storage) the state of collapsed grade categories.
154
 */
155
const toggleCategory = (toggleElement, courseId, userId, storeCollapsedState) => {
156
    const target = toggleElement.dataset.target;
157
    const category = toggleElement.dataset.category;
158
    // Whether the toggle action is collapsing the category or not.
159
    const isCollapsing = toggleElement.getAttribute('aria-expanded') === "true";
160
    const gradebookSetup = toggleElement.closest(SELECTORS.GRADEBOOK_SETUP_TABLE);
161
    // Find all targeted 'children' rows of the toggled category.
162
    const targetRows = gradebookSetup.querySelectorAll(target);
163
    // Find the maximum grade cell in the grade category that is being collapsed/expanded.
164
    const toggleElementRow = toggleElement.closest('tr');
165
    const maxGradeCell = toggleElementRow.querySelector('.column-range');
166
 
167
    if (isCollapsing) {
168
        toggleElement.setAttribute('aria-expanded', 'false');
169
        // Update the 'data-target' of the toggle category node to make sure that when we perform another toggle action
170
        // to expand this category we only target rows which have been hidden by this category toggle action.
171
        toggleElement.dataset.target = `[data-hidden-by='${category}']`;
172
        if (maxGradeCell) {
173
            const relatedCategoryAggregationRow = gradebookSetup.querySelector(`[data-aggregationforcategory='${category}']`);
174
            maxGradeCell.innerHTML = relatedCategoryAggregationRow.querySelector('.column-range').innerHTML;
175
        }
176
    } else {
177
        toggleElement.setAttribute('aria-expanded', 'true');
178
        // Update the 'data-target' of the toggle category node to make sure that when we perform another toggle action
179
        // to collapse this category we only target rows which are children of this category and are not currently hidden.
180
        toggleElement.dataset.target = `.${category}[data-hidden='false']`;
181
        if (maxGradeCell) {
182
            maxGradeCell.innerHTML = '';
183
        }
184
    }
185
    // If explicitly instructed, update accordingly the locally stored state of collapsed categories based on the
186
    // toggle action performed on the given grade category.
187
    if (storeCollapsedState) {
188
        updateCollapsedCategoriesStoredState(category, courseId, userId, isCollapsing);
189
    }
190
 
191
    // Loop through all targeted child row elements and update the required data attributes to either hide or show
192
    // them depending on the toggle action (collapsing or expanding).
193
    targetRows.forEach((row) => {
194
        if (isCollapsing) {
195
            row.dataset.hidden = 'true';
196
            row.dataset.hiddenBy = category;
197
        } else {
198
            row.dataset.hidden = 'false';
199
            row.dataset.hiddenBy = '';
200
        }
201
    });
202
 
203
    // Since the user report is presented in an HTML table, rowspans are used under each category to create a visual
204
    // hierarchy between categories and grading items. When expanding or collapsing a category we need to also update
205
    // (subtract or add) the rowspan values associated to each parent category row to preserve the correct visual
206
    // hierarchy in the table.
207
    updateParentCategoryRowspans(toggleElement, targetRows.length);
208
};
209
 
210
/**
211
 * Method that updates the rowspan value of all 'parent' category rows of a given category node.
212
 *
213
 * @method updateParentCategoryRowspans
214
 * @param {object} toggleElement The category toggle node that was clicked.
215
 * @param {int} num The number we want to add or subtract from the rowspan value of the 'parent' category row elements.
216
 */
217
const updateParentCategoryRowspans = (toggleElement, num) => {
218
    const gradebookSetup = toggleElement.closest(SELECTORS.GRADEBOOK_SETUP_TABLE);
219
    // Get the row element which contains the category toggle node.
220
    const rowElement = toggleElement.closest('tr');
221
 
222
    // Loop through the class list of the toggle category row element.
223
    // The list contains classes which identify all parent categories of the toggled category.
224
    rowElement.classList.forEach((className) => {
225
        // Find the toggle node of the 'parent' category that is identified by the given class name.
226
        const parentCategoryToggleElement = gradebookSetup.querySelector(`[data-target=".${className}[data-hidden='false']"`);
227
        if (parentCategoryToggleElement) {
228
            // Get the row element which contains the parent category toggle node.
229
            const categoryRowElement = parentCategoryToggleElement.closest('tr');
230
            // Find the rowspan element associated to this parent category.
231
            const categoryRowSpanElement = categoryRowElement.nextElementSibling.querySelector('[rowspan]');
232
 
233
            // Depending on whether the toggle action has expanded or collapsed the category, either add or
234
            // subtract from the 'parent' category rowspan.
235
            if (toggleElement.getAttribute('aria-expanded') === "true") {
236
                categoryRowSpanElement.rowSpan = categoryRowSpanElement.rowSpan + num;
237
            } else { // The category has been collapsed.
238
                categoryRowSpanElement.rowSpan = categoryRowSpanElement.rowSpan - num;
239
            }
240
        }
241
    });
242
};
243
 
244
/**
245
 * Initialize module.
246
 *
247
 * @method init
248
 * @param {int} courseId The ID of course.
249
 * @param {int} userId The ID of the current logged user.
250
 */
251
export const init = (courseId, userId) => {
252
    const pendingPromise = new Pending();
253
    const gradebookSetupBox = document.querySelector(SELECTORS.GRADEBOOK_SETUP_BOX);
254
    // Display a loader while the relevant grade categories are being re-collapsed on page load (based on the locally
255
    // stored state for the given user).
256
    addIconToContainer(gradebookSetupBox).then((loader) => {
257
        setTimeout(() => {
258
            collapseGradeCategories(courseId, userId);
259
            // Once the grade categories have been re-collapsed, remove the loader and display the Gradebook setup content.
260
            loader.remove();
261
            document.querySelector(SELECTORS.GRADEBOOK_SETUP_WRAPPER).classList.remove('d-none');
262
            pendingPromise.resolve();
263
        }, 150);
264
        return;
265
    }).fail(Notification.exception);
266
 
267
    registerListenerEvents(courseId, userId);
268
};