MDL-76895 core_courseformat: add fast bulk selections

This commit is contained in:
Ferran Recio 2023-03-01 16:51:28 +01:00
parent f7a8df253b
commit 0436605df5
14 changed files with 525 additions and 86 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
define("core_courseformat/local/content/bulkedittools",["exports","core/reactive","core/sticky-footer","core_courseformat/courseeditor","core/str","core/pending","core/prefetch"],(function(_exports,_reactive,_stickyFooter,_courseeditor,_str,_pending,_prefetch){var obj;
define("core_courseformat/local/content/bulkedittools",["exports","core/reactive","core/sticky-footer","core_courseformat/courseeditor","core/str","core/pending","core/prefetch","core_courseformat/local/content/actions/bulkselection"],(function(_exports,_reactive,_stickyFooter,_courseeditor,_str,_pending,_prefetch,_bulkselection){var obj;
/**
* The bulk editor tools bar.
*
@ -6,6 +6,6 @@ define("core_courseformat/local/content/bulkedittools",["exports","core/reactive
* @class core_courseformat/local/content/bulkedittools
* @copyright 2023 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_pending=(obj=_pending)&&obj.__esModule?obj:{default:obj},(0,_prefetch.prefetchStrings)("core_courseformat",["bulkselection"]);class Component extends _reactive.BaseComponent{create(){this.name="bulk_editor_tools",this.selectors={ACTIONS:'[data-for="bulkaction"]',ACTIONTOOL:'[data-for="bulkactions"] li',CANCEL:'[data-for="bulkcancel"]',COUNT:"[data-for='bulkcount']",SELECTABLE:"[data-bulkcheckbox][data-is-selectable]",SELECTALL:'[data-for="selectall"]',BULKBTN:'[data-for="enableBulk"]'},this.classes={HIDE:"d-none",DISABLED:"disabled"}}static init(target,selectors){return new this({element:document.querySelector(target),reactive:(0,_courseeditor.getCurrentCourseEditor)(),selectors:selectors})}stateReady(){const cancelBtn=this.getElement(this.selectors.CANCEL);cancelBtn&&this.addEventListener(cancelBtn,"click",this._cancelBulk);const selectAll=this.getElement(this.selectors.SELECTALL);selectAll&&this.addEventListener(selectAll,"change",this._selectAllClick)}getWatchers(){return[{watch:"bulk.enabled:updated",handler:this._refreshEnabled},{watch:"bulk:updated",handler:this._refreshTools}]}_refreshEnabled(_ref){let{element:element}=_ref;element.enabled?(0,_stickyFooter.enableStickyFooter)():(0,_stickyFooter.disableStickyFooter)()}_refreshTools(param){this._refreshSelectCount(param),this._refreshSelectAll(param),this._refreshActions(param)}async _refreshSelectCount(_ref2){let{element:bulk}=_ref2;const stringName=bulk.selection.length>1?"bulkselection_plural":"bulkselection",selectedCount=await(0,_str.get_string)(stringName,"core_courseformat",bulk.selection.length),selectedElement=this.getElement(this.selectors.COUNT);selectedElement&&(selectedElement.innerHTML=selectedCount)}_refreshSelectAll(_ref3){let{element:bulk}=_ref3;const selectall=this.getElement(this.selectors.SELECTALL);if(!selectall)return;if(""===bulk.selectedType)return selectall.checked=!1,void(selectall.disabled=!0);selectall.disabled=!1;const maxSelection=document.querySelectorAll(this.selectors.SELECTABLE).length;selectall.checked=bulk.selection.length==maxSelection}_refreshActions(_ref4){let{element:bulk}=_ref4;const displayType="section"==bulk.selectedType?"section":"cm",enabled=""!==bulk.selectedType;this.getElements(this.selectors.ACTIONS).forEach((action=>{action.classList.toggle(this.classes.DISABLED,!enabled),action.tabIndex=enabled?0:-1;const actionTool=action.closest(this.selectors.ACTIONTOOL),isHidden=action.dataset.bulk!=displayType;null==actionTool||actionTool.classList.toggle(this.classes.HIDE,isHidden)}))}_cancelBulk(){const pending=new _pending.default("courseformat/content:bulktoggle_off");this.reactive.dispatch("bulkEnable",!1),setTimeout((()=>{var _document$querySelect;null===(_document$querySelect=document.querySelector(this.selectors.BULKBTN))||void 0===_document$querySelect||_document$querySelect.focus(),pending.resolve()}),150)}_selectAllClick(event){const target=event.target,bulk=this.reactive.get("bulk");""!==bulk.selectedType&&(target.checked?this._handleSelectAll(bulk):this._handleUnselectAll())}_handleUnselectAll(){const pending=new _pending.default("courseformat/content:bulktUnselectAll");this.reactive.dispatch("bulkEnable",!0),setTimeout((()=>{var _document$querySelect2;null===(_document$querySelect2=document.querySelector(this.selectors.SELECTABLE))||void 0===_document$querySelect2||_document$querySelect2.focus(),pending.resolve()}),150)}_handleSelectAll(bulk){const selectableIds=[],selectables=document.querySelectorAll(this.selectors.SELECTABLE);if(0==selectables.length)return;selectables.forEach((selectable=>{selectableIds.push(selectable.dataset.id)}));const mutation="cm"===bulk.selectedType?"cmSelect":"sectionSelect";this.reactive.dispatch(mutation,selectableIds)}}return _exports.default=Component,_exports.default}));
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_pending=(obj=_pending)&&obj.__esModule?obj:{default:obj},(0,_prefetch.prefetchStrings)("core_courseformat",["bulkselection"]);class Component extends _reactive.BaseComponent{create(){this.name="bulk_editor_tools",this.selectors={ACTIONS:'[data-for="bulkaction"]',ACTIONTOOL:'[data-for="bulkactions"] li',CANCEL:'[data-for="bulkcancel"]',COUNT:"[data-for='bulkcount']",SELECTABLE:"[data-bulkcheckbox][data-is-selectable]",SELECTALL:'[data-for="selectall"]',BULKBTN:'[data-for="enableBulk"]'},this.classes={HIDE:"d-none",DISABLED:"disabled"}}static init(target,selectors){return new this({element:document.querySelector(target),reactive:(0,_courseeditor.getCurrentCourseEditor)(),selectors:selectors})}stateReady(){const cancelBtn=this.getElement(this.selectors.CANCEL);cancelBtn&&this.addEventListener(cancelBtn,"click",this._cancelBulk);const selectAll=this.getElement(this.selectors.SELECTALL);selectAll&&this.addEventListener(selectAll,"click",this._selectAllClick)}getWatchers(){return[{watch:"bulk.enabled:updated",handler:this._refreshEnabled},{watch:"bulk:updated",handler:this._refreshTools}]}_refreshEnabled(_ref){let{element:element}=_ref;element.enabled?(0,_stickyFooter.enableStickyFooter)():(0,_stickyFooter.disableStickyFooter)()}_refreshTools(param){this._refreshSelectCount(param),this._refreshSelectAll(param),this._refreshActions(param)}async _refreshSelectCount(_ref2){let{element:bulk}=_ref2;const stringName=bulk.selection.length>1?"bulkselection_plural":"bulkselection",selectedCount=await(0,_str.get_string)(stringName,"core_courseformat",bulk.selection.length),selectedElement=this.getElement(this.selectors.COUNT);selectedElement&&(selectedElement.innerHTML=selectedCount)}_refreshSelectAll(_ref3){let{element:bulk}=_ref3;const selectall=this.getElement(this.selectors.SELECTALL);if(!selectall)return;selectall.disabled=""===bulk.selectedType;const pending=new _pending.default("courseformat/bulktools:refreshSelectAll");setTimeout((()=>{selectall.checked=(0,_bulkselection.checkAllBulkSelected)(this.reactive),pending.resolve()}),100)}_refreshActions(_ref4){let{element:bulk}=_ref4;const displayType="section"==bulk.selectedType?"section":"cm",enabled=""!==bulk.selectedType;this.getElements(this.selectors.ACTIONS).forEach((action=>{action.classList.toggle(this.classes.DISABLED,!enabled),action.tabIndex=enabled?0:-1;const actionTool=action.closest(this.selectors.ACTIONTOOL),isHidden=action.dataset.bulk!=displayType;null==actionTool||actionTool.classList.toggle(this.classes.HIDE,isHidden)}))}_cancelBulk(){const pending=new _pending.default("courseformat/content:bulktoggle_off");this.reactive.dispatch("bulkEnable",!1),setTimeout((()=>{var _document$querySelect;null===(_document$querySelect=document.querySelector(this.selectors.BULKBTN))||void 0===_document$querySelect||_document$querySelect.focus(),pending.resolve()}),150)}_selectAllClick(event){event.preventDefault(),event.altKey?(0,_bulkselection.switchBulkSelection)(this.reactive):(0,_bulkselection.checkAllBulkSelected)(this.reactive)?this._handleUnselectAll():(0,_bulkselection.selectAllBulk)(this.reactive,!0)}_handleUnselectAll(){const pending=new _pending.default("courseformat/content:bulktUnselectAll");(0,_bulkselection.selectAllBulk)(this.reactive,!1),setTimeout((()=>{var _document$querySelect2;null===(_document$querySelect2=document.querySelector(this.selectors.SELECTABLE))||void 0===_document$querySelect2||_document$querySelect2.focus(),pending.resolve()}),150)}}return _exports.default=Component,_exports.default}));
//# sourceMappingURL=bulkedittools.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -8,6 +8,6 @@ define("core_courseformat/local/content/section/cmitem",["exports","core_coursef
* @class core_courseformat/local/content/section/cmitem
* @copyright 2021 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_dndcmitem=(obj=_dndcmitem)&&obj.__esModule?obj:{default:obj};class _default extends _dndcmitem.default{create(){this.name="content_section_cmitem",this.selectors={BULKSELECT:"[data-for='cmBulkSelect']",BULKCHECKBOX:"[data-bulkcheckbox]",CARD:".activity-item",DRAGICON:".editing_move",INPLACEEDITABLE:"[data-inplaceeditablelink]"},this.classes={LOCKED:"editinprogress",HIDE:"d-none",SELECTED:"selected"},this.id=this.element.dataset.id}stateReady(state){var _this$getElement;this.configDragDrop(this.id),null===(_this$getElement=this.getElement(this.selectors.DRAGICON))||void 0===_this$getElement||_this$getElement.classList.add(this.classes.DRAGICON),this._refreshBulk({state:state}),this.addEventListener(this.element,"click",this._handleBulkModeClick)}getWatchers(){return[{watch:"cm[".concat(this.id,"]:deleted"),handler:this.unregister},{watch:"cm[".concat(this.id,"]:updated"),handler:this._refreshCm},{watch:"bulk:updated",handler:this._refreshBulk}]}_refreshCm(_ref){var _element$dragging,_element$locked;let{element:element}=_ref;this.element.classList.toggle(this.classes.DRAGGING,null!==(_element$dragging=element.dragging)&&void 0!==_element$dragging&&_element$dragging),this.element.classList.toggle(this.classes.LOCKED,null!==(_element$locked=element.locked)&&void 0!==_element$locked&&_element$locked),this.locked=element.locked}_refreshBulk(_ref2){var _this$getElement2;let{state:state}=_ref2;const bulk=state.bulk;this.setDraggable(!bulk.enabled),null===(_this$getElement2=this.getElement(this.selectors.BULKSELECT))||void 0===_this$getElement2||_this$getElement2.classList.toggle(this.classes.HIDE,!bulk.enabled);const disabled=!this._isCmBulkEnabled(bulk),selected=this._isSelected(bulk);this._refreshActivityCard(bulk,selected),this._setCheckboxValue(selected,disabled)}_refreshActivityCard(bulk,selected){var _this$getElement3,_this$getElement4;null===(_this$getElement3=this.getElement(this.selectors.INPLACEEDITABLE))||void 0===_this$getElement3||_this$getElement3.classList.toggle(this.classes.HIDE,bulk.enabled),null===(_this$getElement4=this.getElement(this.selectors.CARD))||void 0===_this$getElement4||_this$getElement4.classList.toggle(this.classes.SELECTED,selected),this.element.classList.toggle(this.classes.SELECTED,selected)}_setCheckboxValue(checked,disabled){const checkbox=this.getElement(this.selectors.BULKCHECKBOX);checkbox&&(checkbox.checked=checked,checkbox.disabled=disabled,disabled?checkbox.removeAttribute("data-is-selectable"):checkbox.dataset.isSelectable=1)}_handleBulkModeClick(event){if(event.target.closest(this.selectors.BULKSELECT))return;const bulk=this.reactive.get("bulk");if(!this._isCmBulkEnabled(bulk))return;event.preventDefault();const mutation=this._isSelected(bulk)?"cmUnselect":"cmSelect";this.reactive.dispatch(mutation,[this.id])}_isCmBulkEnabled(bulk){return!!bulk.enabled&&(""===bulk.selectedType||"cm"===bulk.selectedType)}_isSelected(bulk){return"cm"===bulk.selectedType&&bulk.selection.includes(this.id)}}return _exports.default=_default,_exports.default}));
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_dndcmitem=(obj=_dndcmitem)&&obj.__esModule?obj:{default:obj};class _default extends _dndcmitem.default{create(){this.name="content_section_cmitem",this.selectors={BULKSELECT:"[data-for='cmBulkSelect']",BULKCHECKBOX:"[data-bulkcheckbox]",CARD:".activity-item",DRAGICON:".editing_move",INPLACEEDITABLE:"[data-inplaceeditablelink]"},this.classes={LOCKED:"editinprogress",HIDE:"d-none",SELECTED:"selected"},this.id=this.element.dataset.id}stateReady(state){var _this$getElement;this.configDragDrop(this.id),null===(_this$getElement=this.getElement(this.selectors.DRAGICON))||void 0===_this$getElement||_this$getElement.classList.add(this.classes.DRAGICON),this._refreshBulk({state:state})}getWatchers(){return[{watch:"cm[".concat(this.id,"]:deleted"),handler:this.unregister},{watch:"cm[".concat(this.id,"]:updated"),handler:this._refreshCm},{watch:"bulk:updated",handler:this._refreshBulk}]}_refreshCm(_ref){var _element$dragging,_element$locked;let{element:element}=_ref;this.element.classList.toggle(this.classes.DRAGGING,null!==(_element$dragging=element.dragging)&&void 0!==_element$dragging&&_element$dragging),this.element.classList.toggle(this.classes.LOCKED,null!==(_element$locked=element.locked)&&void 0!==_element$locked&&_element$locked),this.locked=element.locked}_refreshBulk(_ref2){var _this$getElement2;let{state:state}=_ref2;const bulk=state.bulk;this.setDraggable(!bulk.enabled),bulk.enabled?(this.element.dataset.action="toggleSelectionCm",this.element.dataset.preventDefault=1):(this.element.removeAttribute("data-action"),this.element.removeAttribute("data-preventDefault")),null===(_this$getElement2=this.getElement(this.selectors.BULKSELECT))||void 0===_this$getElement2||_this$getElement2.classList.toggle(this.classes.HIDE,!bulk.enabled);const disabled=!this._isCmBulkEnabled(bulk),selected=this._isSelected(bulk);this._refreshActivityCard(bulk,selected),this._setCheckboxValue(selected,disabled)}_refreshActivityCard(bulk,selected){var _this$getElement3,_this$getElement4;null===(_this$getElement3=this.getElement(this.selectors.INPLACEEDITABLE))||void 0===_this$getElement3||_this$getElement3.classList.toggle(this.classes.HIDE,bulk.enabled),null===(_this$getElement4=this.getElement(this.selectors.CARD))||void 0===_this$getElement4||_this$getElement4.classList.toggle(this.classes.SELECTED,selected),this.element.classList.toggle(this.classes.SELECTED,selected)}_setCheckboxValue(checked,disabled){const checkbox=this.getElement(this.selectors.BULKCHECKBOX);checkbox&&(checkbox.checked=checked,checkbox.disabled=disabled,disabled?checkbox.removeAttribute("data-is-selectable"):checkbox.dataset.isSelectable=1)}_isCmBulkEnabled(bulk){return!!bulk.enabled&&(""===bulk.selectedType||"cm"===bulk.selectedType)}_isSelected(bulk){return"cm"===bulk.selectedType&&bulk.selection.includes(this.id)}}return _exports.default=_default,_exports.default}));
//# sourceMappingURL=cmitem.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -32,6 +32,7 @@ import Templates from 'core/templates';
import {prefetchStrings} from 'core/prefetch';
import {get_string as getString} from 'core/str';
import {getFirst} from 'core/normalise';
import {toggleBulkSelectionAction} from 'core_courseformat/local/content/actions/bulkselection';
import * as CourseEvents from 'core_course/events';
import Pending from 'core/pending';
import ContentTree from 'core_courseformat/local/courseeditor/contenttree';
@ -456,30 +457,20 @@ export default class extends BaseComponent {
* Handle a toggle cm selection.
*
* @param {Element} target the dispatch action element
* @param {Event} event the triggered event
*/
async _requestToggleSelectionCm(target) {
const cmId = target.dataset.id;
if (!cmId) {
return;
}
const value = target.checked ?? false;
const mutation = (value) ? 'cmSelect' : 'cmUnselect';
this.reactive.dispatch(mutation, [cmId]);
async _requestToggleSelectionCm(target, event) {
toggleBulkSelectionAction(this.reactive, target, event, 'cm');
}
/**
* Handle a toggle section selection.
*
* @param {Element} target the dispatch action element
* @param {Event} event the triggered event
*/
async _requestToggleSelectionSection(target) {
const sectionId = target.dataset.id;
if (!sectionId) {
return;
}
const value = target.checked ?? false;
const mutation = (value) ? 'sectionSelect' : 'sectionUnselect';
this.reactive.dispatch(mutation, [sectionId]);
async _requestToggleSelectionSection(target, event) {
toggleBulkSelectionAction(this.reactive, target, event, 'section');
}
/**

View File

@ -0,0 +1,385 @@
// 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 false;
}
// Section zero is never selectale for bulk actions.
if (bulk.selectedType == 'section') {
const section = this.courseEditor.get('section', bulkSelect.dataset.id);
if (section.number == 0) {
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 (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 (section?.number == 0) {
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();
};

View File

@ -28,6 +28,11 @@ import {getCurrentCourseEditor} from 'core_courseformat/courseeditor';
import {get_string as getString} from 'core/str';
import Pending from 'core/pending';
import {prefetchStrings} from 'core/prefetch';
import {
selectAllBulk,
switchBulkSelection,
checkAllBulkSelected
} from 'core_courseformat/local/content/actions/bulkselection';
// Load global strings.
prefetchStrings(
@ -85,7 +90,7 @@ export default class Component extends BaseComponent {
}
const selectAll = this.getElement(this.selectors.SELECTALL);
if (selectAll) {
this.addEventListener(selectAll, 'change', this._selectAllClick);
this.addEventListener(selectAll, 'click', this._selectAllClick);
}
}
@ -154,15 +159,17 @@ export default class Component extends BaseComponent {
if (!selectall) {
return;
}
if (bulk.selectedType === '') {
selectall.checked = false;
selectall.disabled = true;
return;
}
selectall.disabled = false;
const maxSelection = document.querySelectorAll(this.selectors.SELECTABLE).length;
selectall.checked = (bulk.selection.length == maxSelection);
selectall.disabled = (bulk.selectedType === '');
// The changechecker module can prevent the checkbox form changing it's value.
// To avoid that we leave the sniffer to act before changing the value.
const pending = new Pending(`courseformat/bulktools:refreshSelectAll`);
setTimeout(
() => {
selectall.checked = checkAllBulkSelected(this.reactive);
pending.resolve();
},
100
);
}
/**
@ -199,20 +206,20 @@ export default class Component extends BaseComponent {
}
/**
* Select all elements click handler.
* Handle special select all cases.
* @param {Event} event
*/
_selectAllClick(event) {
const target = event.target;
const bulk = this.reactive.get('bulk');
if (bulk.selectedType === '') {
event.preventDefault();
if (event.altKey) {
switchBulkSelection(this.reactive);
return;
}
if (!target.checked) {
if (checkAllBulkSelected(this.reactive)) {
this._handleUnselectAll();
return;
}
this._handleSelectAll(bulk);
selectAllBulk(this.reactive, true);
}
/**
@ -220,30 +227,11 @@ export default class Component extends BaseComponent {
*/
_handleUnselectAll() {
const pending = new Pending(`courseformat/content:bulktUnselectAll`);
// Re-enable bulk will clean the selection and the selection type.
this.reactive.dispatch('bulkEnable', true);
selectAllBulk(this.reactive, false);
// Wait for a while and focus on the first checkbox.
setTimeout(() => {
document.querySelector(this.selectors.SELECTABLE)?.focus();
pending.resolve();
}, 150);
}
/**
* Process a select all selectable elements.
* @param {Object} bulk the state bulk data
* @param {String} bulk.selectedType the current selected type (section/cm)
*/
_handleSelectAll(bulk) {
const selectableIds = [];
const selectables = document.querySelectorAll(this.selectors.SELECTABLE);
if (selectables.length == 0) {
return;
}
selectables.forEach(selectable => {
selectableIds.push(selectable.dataset.id);
});
const mutation = (bulk.selectedType === 'cm') ? 'cmSelect' : 'sectionSelect';
this.reactive.dispatch(mutation, selectableIds);
}
}

View File

@ -60,7 +60,6 @@ export default class extends DndCmItem {
this.configDragDrop(this.id);
this.getElement(this.selectors.DRAGICON)?.classList.add(this.classes.DRAGICON);
this._refreshBulk({state});
this.addEventListener(this.element, 'click', this._handleBulkModeClick);
}
/**
@ -99,6 +98,14 @@ export default class extends DndCmItem {
const bulk = state.bulk;
// For now, dragging elements in bulk is not possible.
this.setDraggable(!bulk.enabled);
// Convert the card into an active element in bulk mode.
if (bulk.enabled) {
this.element.dataset.action = 'toggleSelectionCm';
this.element.dataset.preventDefault = 1;
} else {
this.element.removeAttribute('data-action');
this.element.removeAttribute('data-preventDefault');
}
this.getElement(this.selectors.BULKSELECT)?.classList.toggle(this.classes.HIDE, !bulk.enabled);
@ -140,28 +147,6 @@ export default class extends DndCmItem {
}
}
/**
* Handle the activity card click in bulk mode.
* @param {Event} event the click event
*/
_handleBulkModeClick(event) {
const selectElement = event.target.closest(this.selectors.BULKSELECT);
if (selectElement) {
// The select element checkbox execute a normal content action as
// any regular action button. This is because the chengechecker module
// is sniffing any form element and will with the checked value
// changing it twice.
return;
}
const bulk = this.reactive.get('bulk');
if (!this._isCmBulkEnabled(bulk)) {
return;
}
event.preventDefault();
const mutation = (this._isSelected(bulk)) ? 'cmUnselect' : 'cmSelect';
this.reactive.dispatch(mutation, [this.id]);
}
/**
* Check if cm bulk selection is available.
* @param {Object} bulk the current state bulk attribute

View File

@ -9,7 +9,7 @@ Feature: Bulk activity and section selection.
| fullname | Course 1 |
| shortname | C1 |
| category | 0 |
| numsections | 2 |
| numsections | 4 |
And the following "activities" exist:
| activity | name | intro | course | idnumber | section |
| assign | Activity sample 1 | Test assignment description | C1 | sample1 | 1 |
@ -84,13 +84,15 @@ Feature: Bulk activity and section selection.
And I should see "1 selected" in the "sticky-footer" "region"
And the "Select all" "checkbox" should be enabled
When I click on "Select all" "checkbox" in the "sticky-footer" "region"
Then I should see "2 selected" in the "sticky-footer" "region"
Then I should see "4 selected" in the "sticky-footer" "region"
Scenario: Click on a select all with all sections selected unselects all sections
Given I click on "Bulk edit" "button"
And I click on "Select topic Topic 1" "checkbox"
And I click on "Select topic Topic 2" "checkbox"
And I should see "2 selected" in the "sticky-footer" "region"
And I click on "Select topic Topic 3" "checkbox"
And I click on "Select topic Topic 4" "checkbox"
And I should see "4 selected" in the "sticky-footer" "region"
And the "Select all" "checkbox" should be enabled
When I click on "Select all" "checkbox" in the "sticky-footer" "region"
Then I should see "0 selected" in the "sticky-footer" "region"
@ -116,3 +118,29 @@ Feature: Bulk activity and section selection.
And I should see "2 selected" in the "sticky-footer" "region"
Then I click on "Activity sample 1" "link" in the "Topic 1" "section"
And I should see "1 selected" in the "sticky-footer" "region"
Scenario: Select a range of activities using shift
Given I click on "Bulk edit" "button"
And I should see "0 selected" in the "sticky-footer" "region"
When I click on "Activity sample 1" "link" in the "Topic 1" "section"
And I shift click on "Activity sample 3" "link" in the "Topic 2" "section"
Then I should see "3 selected" in the "sticky-footer" "region"
Scenario: Select all activities in a section using alt
Given I click on "Bulk edit" "button"
And I should see "0 selected" in the "sticky-footer" "region"
When I alt click on "Activity sample 3" "link" in the "Topic 2" "section"
Then I should see "2 selected" in the "sticky-footer" "region"
Scenario: Select a range of sections using shift
Given I click on "Bulk edit" "button"
And I should see "0 selected" in the "sticky-footer" "region"
When I click on "Select topic Topic 1" "checkbox"
And I shift click on "Select topic Topic 3" "checkbox" in the "page" "region"
Then I should see "3 selected" in the "sticky-footer" "region"
Scenario: Select all section with alt click
Given I click on "Bulk edit" "button"
And I should see "0 selected" in the "sticky-footer" "region"
When I alt click on "Select topic Topic 3" "checkbox" in the "page" "region"
And I should see "4 selected" in the "sticky-footer" "region"

View File

@ -464,6 +464,55 @@ class behat_general extends behat_base {
$node->click();
}
/**
* Click on the element with some modifier key pressed (alt, shift, meta or control).
*
* It is important to note that not all HTML elements are compatible with this step because
* the webdriver limitations. For example, alt click on checkboxes with a visible label will
* produce a normal checkbox click without the modifier.
*
* @When I :modifier click on :element :selectortype in the :nodeelement :nodeselectortype
* @param string $modifier the extra modifier to press (for example, alt+shift or shift)
* @param string $element Element we look for
* @param string $selectortype The type of what we look for
* @param string $nodeelement Element we look in
* @param string $nodeselectortype The type of selector where we look in
*/
public function i_key_click_on_in_the($modifier, $element, $selectortype, $nodeelement, $nodeselectortype) {
behat_base::require_javascript_in_session($this->getSession());
$key = null;
switch (strtoupper(trim($modifier))) {
case '':
break;
case 'SHIFT':
$key = behat_keys::SHIFT;
break;
case 'CTRL':
$key = behat_keys::CONTROL;
break;
case 'ALT':
$key = behat_keys::ALT;
break;
case 'META':
$key = behat_keys::META;
break;
default:
throw new \coding_exception("Unknown modifier key '$modifier'}");
}
$node = $this->get_node_in_container($selectortype, $element, $nodeselectortype, $nodeelement);
$this->ensure_node_is_visible($node);
// KeyUP and KeyDown require the element to be displayed in the current window.
$this->execute_js_on_node($node, '{{ELEMENT}}.scrollIntoView();');
$node->keyDown($key);
$node->click();
// Any click action can move the scroll. Ensure the element is still displayed.
$this->execute_js_on_node($node, '{{ELEMENT}}.scrollIntoView();');
$node->keyUp($key);
}
/**
* Drags and drops the specified element to the specified container. This step does not work in all the browsers, consider it experimental.
*