// 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/>.
/**
* Bulk selection auxiliar methods.
*
* @module core_courseformat/local/content/actions/bulkselection
* @class core_courseformat/local/content/actions/bulkselection
* @copyright 2023 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class BulkSelector {
/**
* The class constructor.
* @param {CourseEditor} courseEditor the original actions component.
*/
constructor(courseEditor) {
this.courseEditor = courseEditor;
this.selectors = {
BULKCMCHECKBOX: `[data-bulkcheckbox][data-action='toggleSelectionCm']`,
BULKSECTIONCHECKBOX: `[data-bulkcheckbox][data-action='toggleSelectionSection']`,
CONTENT: `#region-main`,
};
}
/**
* Process a new selection.
* @param {Number} id
* @param {String} elementType cm or section
* @param {Object} settings special selection settings
* @param {Boolean} settings.all if the action is over all elements of the same type
* @param {Boolean} settings.range if the action is over a range of elements
*/
processNewSelection(id, elementType, settings) {
const value = !this._isBulkSelected(id, elementType);
if (settings.all && settings.range) {
this.switchCurrentSelection();
return;
}
if (!this._isSelectable(id, elementType)) {
return;
}
if (settings.all) {
if (elementType == 'cm') {
this._updateBulkCmSiblings(id, value);
} else {
this._updateBulkSelectionAll(elementType, value);
}
return;
}
if (settings.range) {
this._updateBulkSelectionRange(id, elementType, value);
return;
}
this._updateBulkSelection([id], elementType, value);
}
/**
* Switch between section and cm selection.
*/
switchCurrentSelection() {
const bulk = this.courseEditor.get('bulk');
if (bulk.selectedType === '' || bulk.selection.length == 0) {
return;
}
const newSelectedType = (bulk.selectedType === 'section') ? 'cm' : 'section';
let newSelectedIds;
if (bulk.selectedType === 'section') {
newSelectedIds = this._getCmIdsFromSections(bulk.selection);
} else {
newSelectedIds = this._getSectionIdsFromCms(bulk.selection);
}
// Formats can display only a few activities of the section,
// We need to select on the activities present in the page.
const affectedIds = [];
newSelectedIds.forEach(newId => {
if (this._getSelector(newId, newSelectedType)) {
affectedIds.push(newId);
}
});
this.courseEditor.dispatch('bulkEnable', true);
if (affectedIds.length != 0) {
this._updateBulkSelection(affectedIds, newSelectedType, true);
}
}
/**
* Select all elements of the current type.
* @param {Boolean} value the wanted selected value
*/
selectAll(value) {
const bulk = this.courseEditor.get('bulk');
if (bulk.selectedType == '') {
return;
}
if (!value) {
this.courseEditor.dispatch('bulkEnable', true);
return;
}
const elementType = bulk.selectedType;
this._updateBulkSelectionAll(elementType, value);
}
/**
* Checks if all selectable elements are selected.
* @returns {Boolean} true if all are selected
*/
checkAllSelected() {
const bulk = this.courseEditor.get('bulk');
if (bulk.selectedType == '') {
return false;
}
return this._getContentCheckboxes(bulk.selectedType).every(bulkSelect => {
if (bulkSelect.disabled) {
return true;
}
// Some sections may not be selectale for bulk actions.
if (bulk.selectedType == 'section') {
const section = this.courseEditor.get('section', bulkSelect.dataset.id);
if (!section.bulkeditable) {
return true;
}
}
return bulk.selection.includes(bulkSelect.dataset.id);
});
}
/**
* Check if the id is part of the current bulk selection.
* @private
* @param {Number} id
* @param {String} elementType
* @returns {Boolean} if the element is present in the current selection.
*/
_isBulkSelected(id, elementType) {
const bulk = this.courseEditor.get('bulk');
if (bulk.selectedType !== elementType) {
return false;
}
return bulk.selection.includes(id);
}
/**
* Update the current bulk selection removing or adding Ids.
* @private
* @param {Number[]} ids the user selected element id
* @param {String} elementType cm or section
* @param {Boolean} value the wanted selected value
*/
_updateBulkSelection(ids, elementType, value) {
let mutation = elementType;
mutation += (value) ? 'Select' : 'Unselect';
this.courseEditor.dispatch(mutation, ids);
}
/**
* Get all content bulk selector checkboxes of one type (section/cm).
* @private
* @param {String} elementType section or cm
* @returns {HTMLElement[]} an array with all checkboxes
*/
_getContentCheckboxes(elementType) {
const selector = (elementType == 'cm') ? this.selectors.BULKCMCHECKBOX : this.selectors.BULKSECTIONCHECKBOX;
const checkboxes = document.querySelectorAll(`${this.selectors.CONTENT} ${selector}`);
// Converting to array because NodeList has less iteration methods.
return [...checkboxes];
}
/**
* Validate if an element is selectable in the current page.
* @private
* @param {Number} id the user selected element id
* @param {String} elementType cm or section
* @return {Boolean}
*/
_isSelectable(id, elementType) {
const bulkSelect = this._getSelector(id, elementType);
if (!bulkSelect || bulkSelect.disabled) {
return false;
}
return true;
}
/**
* Get as specific element checkbox.
* @private
* @param {Number} id
* @param {String} elementType cm or section
* @returns {HTMLElement|undefined}
*/
_getSelector(id, elementType) {
let selector = (elementType == 'cm') ? this.selectors.BULKCMCHECKBOX : this.selectors.BULKSECTIONCHECKBOX;
selector += `[data-id='${id}']`;
return document.querySelector(`${this.selectors.CONTENT} ${selector}`);
}
/**
* Update the current bulk selection when a user uses shift to select a range.
* @private
* @param {Number} id the user selected element id
* @param {String} elementType cm or section
* @param {Boolean} value the wanted selected value
*/
_updateBulkSelectionRange(id, elementType, value) {
const bulk = this.courseEditor.get('bulk');
let lastSelectedId = bulk.selection.at(-1);
if (bulk.selectedType !== elementType || lastSelectedId == id) {
this._updateBulkSelection([id], elementType, value);
return;
}
const affectedIds = [];
let found = 0;
this._getContentCheckboxes(elementType).every(bulkSelect => {
if (bulkSelect.disabled) {
return true;
}
if (elementType == 'section') {
const section = this.courseEditor.get('section', bulkSelect.dataset.id);
if (value && !section?.bulkeditable) {
return true;
}
}
if (bulkSelect.dataset.id == id || bulkSelect.dataset.id == lastSelectedId) {
found++;
}
if (found == 0) {
return true;
}
affectedIds.push(bulkSelect.dataset.id);
return found != 2;
});
this._updateBulkSelection(affectedIds, elementType, value);
}
/**
* Select or unselect all cm siblings.
* @private
* @param {Number} cmId the user selected element id
* @param {Boolean} value the wanted selected value
*/
_updateBulkCmSiblings(cmId, value) {
const bulk = this.courseEditor.get('bulk');
if (bulk.selectedType === 'section') {
return;
}
const cm = this.courseEditor.get('cm', cmId);
const section = this.courseEditor.get('section', cm.sectionid);
// Formats can display only a few activities of the section,
// We need to select on the activities selectable in the page.
const affectedIds = [];
section.cmlist.forEach(sectionCmId => {
if (this._isSelectable(sectionCmId, 'cm')) {
affectedIds.push(sectionCmId);
}
});
this._updateBulkSelection(affectedIds, 'cm', value);
}
/**
* Select or unselects al elements of the same type.
* @private
* @param {String} elementType section or cm
* @param {Boolean} value if the elements must be selected or unselected.
*/
_updateBulkSelectionAll(elementType, value) {
const affectedIds = [];
this._getContentCheckboxes(elementType).forEach(bulkSelect => {
if (bulkSelect.disabled) {
return;
}
if (elementType == 'section') {
const section = this.courseEditor.get('section', bulkSelect.dataset.id);
if (value && !section?.bulkeditable) {
return;
}
}
affectedIds.push(bulkSelect.dataset.id);
});
this._updateBulkSelection(affectedIds, elementType, value);
}
/**
* Get all cm ids from a specific section ids.
* @private
* @param {Number[]} sectionIds
* @returns {Number[]} the cm ids
*/
_getCmIdsFromSections(sectionIds) {
const result = [];
sectionIds.forEach(sectionId => {
const section = this.courseEditor.get('section', sectionId);
result.push(...section.cmlist);
});
return result;
}
/**
* Get all section ids containing a specific cm ids.
* @private
* @param {Number[]} cmIds
* @returns {Number[]} the section ids
*/
_getSectionIdsFromCms(cmIds) {
const result = new Set();
cmIds.forEach(cmId => {
const cm = this.courseEditor.get('cm', cmId);
if (cm.sectionnumber == 0) {
return;
}
result.add(cm.sectionid);
});
return [...result];
}
}
/**
* Process a bulk selection toggle action.
* @method
* @param {CourseEditor} courseEditor
* @param {HTMLElement} target the action element
* @param {Event} event
* @param {String} elementType cm or section
*/
export const toggleBulkSelectionAction = function(courseEditor, target, event, elementType) {
const id = target.dataset.id;
if (!id) {
return;
}
// When the action cames from a form element (checkbox) we should not preventDefault.
// If we do it the changechecker module will execute the state change twice.
if (target.dataset.preventDefault) {
event.preventDefault();
}
// Using shift or alt key can produce text selection.
document.getSelection().removeAllRanges();
const bulkSelector = new BulkSelector(courseEditor);
bulkSelector.processNewSelection(
id,
elementType,
{
range: event.shiftKey,
all: event.altKey,
}
);
};
/**
* Switch the current bulk selection.
* @method
* @param {CourseEditor} courseEditor
*/
export const switchBulkSelection = function(courseEditor) {
const bulkSelector = new BulkSelector(courseEditor);
bulkSelector.switchCurrentSelection();
};
/**
* Select/unselect all element of the selected type.
* @method
* @param {CourseEditor} courseEditor
* @param {Boolean} value if the elements must be selected or unselected.
*/
export const selectAllBulk = function(courseEditor, value) {
const bulkSelector = new BulkSelector(courseEditor);
bulkSelector.selectAll(value);
};
/**
* Check if all possible elements are selected.
* @method
* @param {CourseEditor} courseEditor
* @return {Boolean} if all elements of the current type are selected.
*/
export const checkAllBulkSelected = function(courseEditor) {
const bulkSelector = new BulkSelector(courseEditor);
return bulkSelector.checkAllSelected();
};