mirror of
https://github.com/moodle/moodle.git
synced 2025-01-19 14:27:22 +01:00
MDL-76895 core_courseformat: add fast bulk selections
This commit is contained in:
parent
f7a8df253b
commit
0436605df5
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
12
course/format/amd/build/local/content/actions/bulkselection.min.js
vendored
Normal file
12
course/format/amd/build/local/content/actions/bulkselection.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -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
@ -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
@ -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');
|
||||
}
|
||||
|
||||
/**
|
||||
|
385
course/format/amd/src/local/content/actions/bulkselection.js
Normal file
385
course/format/amd/src/local/content/actions/bulkselection.js
Normal 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();
|
||||
};
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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.
|
||||
*
|
||||
|
Loading…
x
Reference in New Issue
Block a user