// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Enhance the gradebook tree setup with various facilities.
*
* @module core_grades/edittree_index
* @copyright 2016 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import storage from 'core/localstorage';
import {addIconToContainer} from 'core/loadingicon';
import Notification from 'core/notification';
import Pending from 'core/pending';
const SELECTORS = {
CATEGORY_TOGGLE: '.toggle-category',
GRADEBOOK_SETUP_TABLE: '.setup-grades',
WEIGHT_OVERRIDE_CHECKBOX: '.weightoverride',
BULK_MOVE_SELECT: '#menumoveafter',
BULK_MOVE_INPUT: '#bulkmoveinput',
GRADEBOOK_SETUP_WRAPPER: '.gradetree-wrapper',
GRADEBOOK_SETUP_BOX: '.gradetreebox'
};
/**
* Register related event listeners.
*
* @method registerListenerEvents
* @param {int} courseId The ID of course.
* @param {int} userId The ID of the current logged user.
*/
const registerListenerEvents = (courseId, userId) => {
document.addEventListener('change', e => {
// Toggle the availability of the weight input field based on the changed state (checked/unchecked) of the
// related checkbox element.
if (e.target.matches(SELECTORS.WEIGHT_OVERRIDE_CHECKBOX)) {
toggleWeightInput(e.target);
}
// Submit the bulk move form when the selected option in the bulk move select element has been changed.
if (e.target.matches(SELECTORS.BULK_MOVE_SELECT)) {
submitBulkMoveForm(e.target);
}
});
const gradebookSetup = document.querySelector(SELECTORS.GRADEBOOK_SETUP_TABLE);
gradebookSetup.addEventListener('click', e => {
const toggle = e.target.closest(SELECTORS.CATEGORY_TOGGLE);
// Collapse or expand the grade category when the visibility toggle button is activated.
if (toggle) {
e.preventDefault();
toggleCategory(toggle, courseId, userId, true);
}
});
};
/**
* Toggle the weight input field based on its checkbox.
*
* @method toggleWeightInput
* @param {object} weightOverrideCheckbox The weight override checkbox element.
*/
const toggleWeightInput = (weightOverrideCheckbox) => {
const row = weightOverrideCheckbox.closest('tr');
const itemId = row.dataset.itemid;
const weightOverrideInput = row.querySelector(`input[name="weight_${itemId}"]`);
weightOverrideInput.disabled = !weightOverrideCheckbox.checked;
};
/**
* Submit the bulk move form.
*
* @method toggleWeightInput
* @param {object} bulkMoveSelect The bulk move select element.
*/
const submitBulkMoveForm = (bulkMoveSelect) => {
const form = bulkMoveSelect.closest('form');
const bulkMoveInput = form.querySelector(SELECTORS.BULK_MOVE_INPUT);
bulkMoveInput.value = 1;
form.submit();
};
/**
* Method that collapses all relevant grade categories based on the locally stored state of collapsed grade categories
* for a given user.
*
* @method collapseGradeCategories
* @param {int} courseId The ID of course.
* @param {int} userId The ID of the current logged user.
*/
const collapseGradeCategories = (courseId, userId) => {
const gradebookSetup = document.querySelector(SELECTORS.GRADEBOOK_SETUP_TABLE);
const storedCollapsedCategories = storage.get(`core_grade_collapsedgradecategories_${courseId}_${userId}`);
if (storedCollapsedCategories) {
// Fetch all grade categories that are locally stored as collapsed and re-apply the collapse action.
const collapsedCategories = JSON.parse(storedCollapsedCategories);
collapsedCategories.forEach((category) => {
const categoryToggleElement =
gradebookSetup.querySelector(`${SELECTORS.CATEGORY_TOGGLE}[data-category="${category}"`);
if (categoryToggleElement) {
toggleCategory(categoryToggleElement, courseId, userId, false);
}
});
}
};
/**
* Method that updates the locally stored state of collapsed grade categories based on a performed toggle action on a
* given grade category.
*
* @method updateCollapsedCategoriesStoredState
* @param {string} category The category to be added or removed from the collapsed grade categories local storage.
* @param {int} courseId The ID of course.
* @param {int} userId The ID of the current logged user.
* @param {boolean} isCollapsing Whether the category is being collapsed or not.
*/
const updateCollapsedCategoriesStoredState = (category, courseId, userId, isCollapsing) => {
const currentStoredCollapsedCategories = storage.get(`core_grade_collapsedgradecategories_${courseId}_${userId}`);
let collapsedCategories = currentStoredCollapsedCategories ?
JSON.parse(currentStoredCollapsedCategories) : [];
if (isCollapsing) {
collapsedCategories.push(category);
} else {
collapsedCategories = collapsedCategories.filter(cat => cat !== category);
}
storage.set(`core_grade_collapsedgradecategories_${courseId}_${userId}`, JSON.stringify(collapsedCategories));
};
/**
* Method that handles the grade category toggle action.
*
* @method toggleCategory
* @param {object} toggleElement The category toggle node that was clicked.
* @param {int} courseId The ID of course.
* @param {int} userId The ID of the current logged user.
* @param {boolean} storeCollapsedState Whether to store (local storage) the state of collapsed grade categories.
*/
const toggleCategory = (toggleElement, courseId, userId, storeCollapsedState) => {
const target = toggleElement.dataset.target;
const category = toggleElement.dataset.category;
// Whether the toggle action is collapsing the category or not.
const isCollapsing = toggleElement.getAttribute('aria-expanded') === "true";
const gradebookSetup = toggleElement.closest(SELECTORS.GRADEBOOK_SETUP_TABLE);
// Find all targeted 'children' rows of the toggled category.
const targetRows = gradebookSetup.querySelectorAll(target);
// Find the maximum grade cell in the grade category that is being collapsed/expanded.
const toggleElementRow = toggleElement.closest('tr');
const maxGradeCell = toggleElementRow.querySelector('.column-range');
if (isCollapsing) {
toggleElement.setAttribute('aria-expanded', 'false');
// Update the 'data-target' of the toggle category node to make sure that when we perform another toggle action
// to expand this category we only target rows which have been hidden by this category toggle action.
toggleElement.dataset.target = `[data-hidden-by='${category}']`;
if (maxGradeCell) {
const relatedCategoryAggregationRow = gradebookSetup.querySelector(`[data-aggregationforcategory='${category}']`);
maxGradeCell.innerHTML = relatedCategoryAggregationRow.querySelector('.column-range').innerHTML;
}
} else {
toggleElement.setAttribute('aria-expanded', 'true');
// Update the 'data-target' of the toggle category node to make sure that when we perform another toggle action
// to collapse this category we only target rows which are children of this category and are not currently hidden.
toggleElement.dataset.target = `.${category}[data-hidden='false']`;
if (maxGradeCell) {
maxGradeCell.innerHTML = '';
}
}
// If explicitly instructed, update accordingly the locally stored state of collapsed categories based on the
// toggle action performed on the given grade category.
if (storeCollapsedState) {
updateCollapsedCategoriesStoredState(category, courseId, userId, isCollapsing);
}
// Loop through all targeted child row elements and update the required data attributes to either hide or show
// them depending on the toggle action (collapsing or expanding).
targetRows.forEach((row) => {
if (isCollapsing) {
row.dataset.hidden = 'true';
row.dataset.hiddenBy = category;
} else {
row.dataset.hidden = 'false';
row.dataset.hiddenBy = '';
}
});
// Since the user report is presented in an HTML table, rowspans are used under each category to create a visual
// hierarchy between categories and grading items. When expanding or collapsing a category we need to also update
// (subtract or add) the rowspan values associated to each parent category row to preserve the correct visual
// hierarchy in the table.
updateParentCategoryRowspans(toggleElement, targetRows.length);
};
/**
* Method that updates the rowspan value of all 'parent' category rows of a given category node.
*
* @method updateParentCategoryRowspans
* @param {object} toggleElement The category toggle node that was clicked.
* @param {int} num The number we want to add or subtract from the rowspan value of the 'parent' category row elements.
*/
const updateParentCategoryRowspans = (toggleElement, num) => {
const gradebookSetup = toggleElement.closest(SELECTORS.GRADEBOOK_SETUP_TABLE);
// Get the row element which contains the category toggle node.
const rowElement = toggleElement.closest('tr');
// Loop through the class list of the toggle category row element.
// The list contains classes which identify all parent categories of the toggled category.
rowElement.classList.forEach((className) => {
// Find the toggle node of the 'parent' category that is identified by the given class name.
const parentCategoryToggleElement = gradebookSetup.querySelector(`[data-target=".${className}[data-hidden='false']"`);
if (parentCategoryToggleElement) {
// Get the row element which contains the parent category toggle node.
const categoryRowElement = parentCategoryToggleElement.closest('tr');
// Find the rowspan element associated to this parent category.
const categoryRowSpanElement = categoryRowElement.nextElementSibling.querySelector('[rowspan]');
// Depending on whether the toggle action has expanded or collapsed the category, either add or
// subtract from the 'parent' category rowspan.
if (toggleElement.getAttribute('aria-expanded') === "true") {
categoryRowSpanElement.rowSpan = categoryRowSpanElement.rowSpan + num;
} else { // The category has been collapsed.
categoryRowSpanElement.rowSpan = categoryRowSpanElement.rowSpan - num;
}
}
});
};
/**
* Initialize module.
*
* @method init
* @param {int} courseId The ID of course.
* @param {int} userId The ID of the current logged user.
*/
export const init = (courseId, userId) => {
const pendingPromise = new Pending();
const gradebookSetupBox = document.querySelector(SELECTORS.GRADEBOOK_SETUP_BOX);
// Display a loader while the relevant grade categories are being re-collapsed on page load (based on the locally
// stored state for the given user).
addIconToContainer(gradebookSetupBox).then((loader) => {
setTimeout(() => {
collapseGradeCategories(courseId, userId);
// Once the grade categories have been re-collapsed, remove the loader and display the Gradebook setup content.
loader.remove();
document.querySelector(SELECTORS.GRADEBOOK_SETUP_WRAPPER).classList.remove('d-none');
pendingPromise.resolve();
}, 150);
return;
}).fail(Notification.exception);
registerListenerEvents(courseId, userId);
};