From 7b04638c5261bd2b2ea3f505bdcd612c96587efa Mon Sep 17 00:00:00 2001 From: Stefan Topfstedt Date: Mon, 12 Jun 2023 09:07:38 -0700 Subject: [PATCH] MDL-71212 core_course: adds controls to the course index drawer. --- .../tests/behat/course_competencies.feature | 2 +- badges/tests/behat/criteria_activity.feature | 2 +- .../activity_completion_criteria.feature | 6 +- .../build/local/courseeditor/mutations.min.js | 2 +- .../local/courseeditor/mutations.min.js.map | 2 +- .../amd/src/local/courseeditor/mutations.js | 11 +++ .../tests/behat/course_courseindex.feature | 29 ++++++ .../behat/courseindex_keyboardnav.feature | 3 + lang/en/courseformat.php | 1 + .../build/local/reactive/statemanager.min.js | 2 +- .../local/reactive/statemanager.min.js.map | 2 +- lib/amd/src/local/reactive/statemanager.js | 15 ++++ mod/assign/tests/behat/hide_grader.feature | 2 +- .../tests/behat/remove_submission.feature | 2 +- .../tests/behat/set_availability.feature | 14 +-- .../tests/behat/guest_access.feature | 2 +- .../behat/workshop_late_submission.feature | 2 +- .../tests/behat/customfield_question.feature | 2 +- .../customfield_question_visbility.feature | 2 +- .../tests/behat/breakdown_by_activity.feature | 2 +- .../build/courseindexdrawercontrols.min.js | 11 +++ .../courseindexdrawercontrols.min.js.map | 1 + theme/boost/amd/build/drawers.min.js | 2 +- theme/boost/amd/build/drawers.min.js.map | 2 +- .../amd/src/courseindexdrawercontrols.js | 89 +++++++++++++++++++ theme/boost/amd/src/drawers.js | 13 ++- .../courseindexdrawercontrols.mustache | 57 ++++++++++++ theme/boost/templates/drawer.mustache | 3 + theme/boost/templates/drawers.mustache | 3 + 29 files changed, 258 insertions(+), 28 deletions(-) create mode 100644 theme/boost/amd/build/courseindexdrawercontrols.min.js create mode 100644 theme/boost/amd/build/courseindexdrawercontrols.min.js.map create mode 100644 theme/boost/amd/src/courseindexdrawercontrols.js create mode 100644 theme/boost/templates/courseindexdrawercontrols.mustache diff --git a/admin/tool/lp/tests/behat/course_competencies.feature b/admin/tool/lp/tests/behat/course_competencies.feature index 11dc3e4aa9c..44a15161c70 100644 --- a/admin/tool/lp/tests/behat/course_competencies.feature +++ b/admin/tool/lp/tests/behat/course_competencies.feature @@ -35,7 +35,7 @@ Feature: See the competencies for an activity on the course competencies page. And I select "Test-Comp2" of the competency tree And I click on "Add" "button" in the "Competency picker" "dialogue" And I am on the PageName1 "page activity editing" page - And I follow "Expand all" + And I click on "Expand all" "link" in the "region-main" "region" And I set the field "Course competencies" to "Test-Comp1" And I press "Save and return to course" And I log out diff --git a/badges/tests/behat/criteria_activity.feature b/badges/tests/behat/criteria_activity.feature index b4ecdb1226d..015ee0950b0 100644 --- a/badges/tests/behat/criteria_activity.feature +++ b/badges/tests/behat/criteria_activity.feature @@ -83,7 +83,7 @@ Feature: Award badges based on activity completion And I follow "Course Badge" And I select "Criteria" from the "jump" singleselect And I set the field "type" to "Activity completion" - And I click on "Expand all" "link" + And I click on "Expand all" "link" in the "region-main" "region" And I set the field "Quiz - Test quiz name" to "1" And I set the field "" to "1" And I press "Save" diff --git a/completion/tests/behat/activity_completion_criteria.feature b/completion/tests/behat/activity_completion_criteria.feature index 5b7f173de18..446bc7f897d 100644 --- a/completion/tests/behat/activity_completion_criteria.feature +++ b/completion/tests/behat/activity_completion_criteria.feature @@ -30,7 +30,7 @@ Feature: Allow to mark course as completed without cron for activity completion And I set the field "Enable completion tracking" to "Yes" And I click on "Save and display" "button" And I am on the "Test assignment name" "assign activity editing" page - And I follow "Expand all" + And I click on "Expand all" "link" in the "region-main" "region" And I set the field "Completion tracking" to "Show activity as complete when conditions are met" And I set the field "completionusegrade" to "1" And I press "Save and return to course" @@ -42,7 +42,7 @@ Feature: Allow to mark course as completed without cron for activity completion @javascript Scenario: Update course completion when student marks activity as complete Given I am on the "Test assignment name" "assign activity editing" page logged in as teacher1 - And I follow "Expand all" + And I click on "Expand all" "link" in the "region-main" "region" And I set the field "Completion tracking" to "Students can manually mark the activity as completed" And I press "Save and return to course" When I am on the "Completion course" course page logged in as student1 @@ -72,7 +72,7 @@ Feature: Allow to mark course as completed without cron for activity completion | name | Test assignment name2 | | idnumber | assign2 | And I am on the "Test assignment name2" "assign activity editing" page logged in as admin - And I follow "Expand all" + And I click on "Expand all" "link" in the "region-main" "region" And I set the field "Completion tracking" to "Show activity as complete when conditions are met" And I set the field "completionusegrade" to "1" And I press "Save and return to course" diff --git a/course/format/amd/build/local/courseeditor/mutations.min.js b/course/format/amd/build/local/courseeditor/mutations.min.js index bce3a2c2be9..ad95e389383 100644 --- a/course/format/amd/build/local/courseeditor/mutations.min.js +++ b/course/format/amd/build/local/courseeditor/mutations.min.js @@ -6,6 +6,6 @@ define("core_courseformat/local/courseeditor/mutations",["exports","core/ajax"], * @class core_courseformat/local/courseeditor/mutations * @copyright 2021 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};return _exports.default=class{async _callEditWebservice(action,courseId,ids,targetSectionId,targetCmId){const args={action:action,courseid:courseId,ids:ids};targetSectionId&&(args.targetsectionid=targetSectionId),targetCmId&&(args.targetcmid=targetCmId);let ajaxresult=await _ajax.default.call([{methodname:"core_courseformat_update_course",args:args}])[0];return JSON.parse(ajaxresult)}async _sectionBasicAction(stateManager,action,sectionIds,targetSectionId,targetCmId){const course=stateManager.get("course");this.sectionLock(stateManager,sectionIds,!0);const updates=await this._callEditWebservice(action,course.id,sectionIds,targetSectionId,targetCmId);this.bulkReset(stateManager),stateManager.processUpdates(updates),this.sectionLock(stateManager,sectionIds,!1)}async _cmBasicAction(stateManager,action,cmIds,targetSectionId,targetCmId){const course=stateManager.get("course");this.cmLock(stateManager,cmIds,!0);const updates=await this._callEditWebservice(action,course.id,cmIds,targetSectionId,targetCmId);this.bulkReset(stateManager),stateManager.processUpdates(updates),this.cmLock(stateManager,cmIds,!1)}init(stateManager){stateManager.addUpdateTypes({prepareFields:this._prepareFields})}_prepareFields(stateManager,updateName,fields){return fields.locked=!1,fields}async sectionHide(stateManager,sectionIds){await this._sectionBasicAction(stateManager,"section_hide",sectionIds)}async sectionShow(stateManager,sectionIds){await this._sectionBasicAction(stateManager,"section_show",sectionIds)}async cmShow(stateManager,cmIds){await this._cmBasicAction(stateManager,"cm_show",cmIds)}async cmHide(stateManager,cmIds){await this._cmBasicAction(stateManager,"cm_hide",cmIds)}async cmStealth(stateManager,cmIds){await this._cmBasicAction(stateManager,"cm_stealth",cmIds)}async cmDuplicate(stateManager,cmIds,targetSectionId,targetCmId){const course=stateManager.get("course"),sectionIds=new Set;targetSectionId?sectionIds.add(targetSectionId):cmIds.forEach((cmId=>{const cm=stateManager.get("cm",cmId);sectionIds.add(cm.sectionid)})),this.sectionLock(stateManager,Array.from(sectionIds),!0);const updates=await this._callEditWebservice("cm_duplicate",course.id,cmIds,targetSectionId,targetCmId);this.bulkReset(stateManager),stateManager.processUpdates(updates),this.sectionLock(stateManager,Array.from(sectionIds),!1)}async cmMove(stateManager,cmids,targetSectionId,targetCmId){if(!targetSectionId&&!targetCmId)throw new Error("Mutation cmMove requires targetSectionId or targetCmId");const course=stateManager.get("course");this.cmLock(stateManager,cmids,!0);const updates=await this._callEditWebservice("cm_move",course.id,cmids,targetSectionId,targetCmId);this.bulkReset(stateManager),stateManager.processUpdates(updates),this.cmLock(stateManager,cmids,!1)}async sectionMove(stateManager,sectionIds,targetSectionId){if(!targetSectionId)throw new Error("Mutation sectionMove requires targetSectionId");const course=stateManager.get("course");this.sectionLock(stateManager,sectionIds,!0);const updates=await this._callEditWebservice("section_move",course.id,sectionIds,targetSectionId);this.bulkReset(stateManager),stateManager.processUpdates(updates),this.sectionLock(stateManager,sectionIds,!1)}async sectionMoveAfter(stateManager,sectionIds,targetSectionId){if(!targetSectionId)throw new Error("Mutation sectionMoveAfter requires targetSectionId");const course=stateManager.get("course");this.sectionLock(stateManager,sectionIds,!0);const updates=await this._callEditWebservice("section_move_after",course.id,sectionIds,targetSectionId);this.bulkReset(stateManager),stateManager.processUpdates(updates),this.sectionLock(stateManager,sectionIds,!1)}async addSection(stateManager,targetSectionId){targetSectionId||(targetSectionId=0);const course=stateManager.get("course"),updates=await this._callEditWebservice("section_add",course.id,[],targetSectionId);stateManager.processUpdates(updates)}async sectionDelete(stateManager,sectionIds){const course=stateManager.get("course"),updates=await this._callEditWebservice("section_delete",course.id,sectionIds);this.bulkReset(stateManager),stateManager.processUpdates(updates)}async cmDelete(stateManager,cmIds){const course=stateManager.get("course");this.cmLock(stateManager,cmIds,!0);const updates=await this._callEditWebservice("cm_delete",course.id,cmIds);this.bulkReset(stateManager),this.cmLock(stateManager,cmIds,!1),stateManager.processUpdates(updates)}cmDrag(stateManager,cmIds,dragValue){this.setPageItem(stateManager),this._setElementsValue(stateManager,"cm",cmIds,"dragging",dragValue)}sectionDrag(stateManager,sectionIds,dragValue){this.setPageItem(stateManager),this._setElementsValue(stateManager,"section",sectionIds,"dragging",dragValue)}cmCompletion(stateManager,cmIds,complete){const newValue=complete?1:0;this._setElementsValue(stateManager,"cm",cmIds,"completionstate",newValue)}async cmMoveRight(stateManager,cmIds){await this._cmBasicAction(stateManager,"cm_moveright",cmIds)}async cmMoveLeft(stateManager,cmIds){await this._cmBasicAction(stateManager,"cm_moveleft",cmIds)}async cmNoGroups(stateManager,cmIds){await this._cmBasicAction(stateManager,"cm_nogroups",cmIds)}async cmVisibleGroups(stateManager,cmIds){await this._cmBasicAction(stateManager,"cm_visiblegroups",cmIds)}async cmSeparateGroups(stateManager,cmIds){await this._cmBasicAction(stateManager,"cm_separategroups",cmIds)}cmLock(stateManager,cmIds,lockValue){this._setElementsValue(stateManager,"cm",cmIds,"locked",lockValue)}sectionLock(stateManager,sectionIds,lockValue){this._setElementsValue(stateManager,"section",sectionIds,"locked",lockValue)}_setElementsValue(stateManager,name,ids,fieldName,newValue){stateManager.setReadOnly(!1),ids.forEach((id=>{const element=stateManager.get(name,id);element&&(element[fieldName]=newValue)})),stateManager.setReadOnly(!0)}setPageItem(stateManager,type,id,isStatic){let newPageItem;if(void 0!==type&&(newPageItem=stateManager.get(type,id),!newPageItem))return;stateManager.setReadOnly(!1);const course=stateManager.get("course");course.pageItem=null,newPageItem&&(course.pageItem={id:id,type:type,sectionId:"section"==type?newPageItem.id:newPageItem.sectionid,isStatic:isStatic}),stateManager.setReadOnly(!0)}unlockAll(stateManager){const state=stateManager.state;stateManager.setReadOnly(!1),state.section.forEach((section=>{section.locked=!1})),state.cm.forEach((cm=>{cm.locked=!1})),stateManager.setReadOnly(!0)}async sectionIndexCollapsed(stateManager,sectionIds,collapsed){const collapsedIds=this._updateStateSectionPreference(stateManager,"indexcollapsed",sectionIds,collapsed);if(!collapsedIds)return;const course=stateManager.get("course");await this._callEditWebservice("section_index_collapsed",course.id,collapsedIds)}async sectionContentCollapsed(stateManager,sectionIds,collapsed){const collapsedIds=this._updateStateSectionPreference(stateManager,"contentcollapsed",sectionIds,collapsed);if(!collapsedIds)return;const course=stateManager.get("course");await this._callEditWebservice("section_content_collapsed",course.id,collapsedIds)}_updateStateSectionPreference(stateManager,preferenceName,sectionIds,preferenceValue){stateManager.setReadOnly(!1);const affectedSections=new Set;if(sectionIds.forEach((sectionId=>{const section=stateManager.get("section",sectionId);if(void 0===section)return null;const newValue=null!=preferenceValue?preferenceValue:section[preferenceName];section[preferenceName]!=newValue&&(section[preferenceName]=newValue,affectedSections.add(section.id))})),stateManager.setReadOnly(!0),0==affectedSections.size)return null;const collapsedSectionIds=[];return stateManager.state.section.forEach((section=>{section[preferenceName]&&collapsedSectionIds.push(section.id)})),collapsedSectionIds}bulkEnable(stateManager,enabled){const state=stateManager.state;stateManager.setReadOnly(!1),state.bulk.enabled=enabled,state.bulk.selectedType="",state.bulk.selection=[],stateManager.setReadOnly(!0)}bulkReset(stateManager){const state=stateManager.state;stateManager.setReadOnly(!1),state.bulk.selectedType="",state.bulk.selection=[],stateManager.setReadOnly(!0)}cmSelect(stateManager,cmIds){this._addIdsToSelection(stateManager,"cm",cmIds)}cmUnselect(stateManager,cmIds){this._removeIdsFromSelection(stateManager,"cm",cmIds)}sectionSelect(stateManager,sectionIds){this._addIdsToSelection(stateManager,"section",sectionIds)}sectionUnselect(stateManager,sectionIds){this._removeIdsFromSelection(stateManager,"section",sectionIds)}_addIdsToSelection(stateManager,typeName,ids){const bulk=stateManager.state.bulk;if(null==bulk||!bulk.enabled)throw new Error("Bulk is not enabled");if(""!==(null==bulk?void 0:bulk.selectedType)&&(null==bulk?void 0:bulk.selectedType)!==typeName)throw new Error("Cannot add ".concat(typeName," to the current selection"));ids=ids.map((value=>value.toString())),stateManager.setReadOnly(!1),bulk.selectedType=typeName;const newSelection=new Set([...bulk.selection,...ids]);bulk.selection=[...newSelection],stateManager.setReadOnly(!0)}_removeIdsFromSelection(stateManager,typeName,ids){const bulk=stateManager.state.bulk;if(null==bulk||!bulk.enabled)throw new Error("Bulk is not enabled");if(""!==(null==bulk?void 0:bulk.selectedType)&&(null==bulk?void 0:bulk.selectedType)!==typeName)throw new Error("Cannot remove ".concat(typeName," from the current selection"));ids=ids.map((value=>value.toString())),stateManager.setReadOnly(!1);const IdsToFilter=new Set(ids);bulk.selection=bulk.selection.filter((current=>!IdsToFilter.has(current))),0===bulk.selection.length&&(bulk.selectedType=""),stateManager.setReadOnly(!0)}async cmState(stateManager,cmids){this.cmLock(stateManager,cmids,!0);const course=stateManager.get("course"),updates=await this._callEditWebservice("cm_state",course.id,cmids);stateManager.processUpdates(updates),this.cmLock(stateManager,cmids,!1)}async sectionState(stateManager,sectionIds){this.sectionLock(stateManager,sectionIds,!0);const course=stateManager.get("course"),updates=await this._callEditWebservice("section_state",course.id,sectionIds);stateManager.processUpdates(updates),this.sectionLock(stateManager,sectionIds,!1)}async courseState(stateManager){const course=stateManager.get("course"),updates=await this._callEditWebservice("course_state",course.id);stateManager.processUpdates(updates)}},_exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};return _exports.default=class{async _callEditWebservice(action,courseId,ids,targetSectionId,targetCmId){const args={action:action,courseid:courseId,ids:ids};targetSectionId&&(args.targetsectionid=targetSectionId),targetCmId&&(args.targetcmid=targetCmId);let ajaxresult=await _ajax.default.call([{methodname:"core_courseformat_update_course",args:args}])[0];return JSON.parse(ajaxresult)}async _sectionBasicAction(stateManager,action,sectionIds,targetSectionId,targetCmId){const course=stateManager.get("course");this.sectionLock(stateManager,sectionIds,!0);const updates=await this._callEditWebservice(action,course.id,sectionIds,targetSectionId,targetCmId);this.bulkReset(stateManager),stateManager.processUpdates(updates),this.sectionLock(stateManager,sectionIds,!1)}async _cmBasicAction(stateManager,action,cmIds,targetSectionId,targetCmId){const course=stateManager.get("course");this.cmLock(stateManager,cmIds,!0);const updates=await this._callEditWebservice(action,course.id,cmIds,targetSectionId,targetCmId);this.bulkReset(stateManager),stateManager.processUpdates(updates),this.cmLock(stateManager,cmIds,!1)}init(stateManager){stateManager.addUpdateTypes({prepareFields:this._prepareFields})}_prepareFields(stateManager,updateName,fields){return fields.locked=!1,fields}async sectionHide(stateManager,sectionIds){await this._sectionBasicAction(stateManager,"section_hide",sectionIds)}async sectionShow(stateManager,sectionIds){await this._sectionBasicAction(stateManager,"section_show",sectionIds)}async cmShow(stateManager,cmIds){await this._cmBasicAction(stateManager,"cm_show",cmIds)}async cmHide(stateManager,cmIds){await this._cmBasicAction(stateManager,"cm_hide",cmIds)}async cmStealth(stateManager,cmIds){await this._cmBasicAction(stateManager,"cm_stealth",cmIds)}async cmDuplicate(stateManager,cmIds,targetSectionId,targetCmId){const course=stateManager.get("course"),sectionIds=new Set;targetSectionId?sectionIds.add(targetSectionId):cmIds.forEach((cmId=>{const cm=stateManager.get("cm",cmId);sectionIds.add(cm.sectionid)})),this.sectionLock(stateManager,Array.from(sectionIds),!0);const updates=await this._callEditWebservice("cm_duplicate",course.id,cmIds,targetSectionId,targetCmId);this.bulkReset(stateManager),stateManager.processUpdates(updates),this.sectionLock(stateManager,Array.from(sectionIds),!1)}async cmMove(stateManager,cmids,targetSectionId,targetCmId){if(!targetSectionId&&!targetCmId)throw new Error("Mutation cmMove requires targetSectionId or targetCmId");const course=stateManager.get("course");this.cmLock(stateManager,cmids,!0);const updates=await this._callEditWebservice("cm_move",course.id,cmids,targetSectionId,targetCmId);this.bulkReset(stateManager),stateManager.processUpdates(updates),this.cmLock(stateManager,cmids,!1)}async sectionMove(stateManager,sectionIds,targetSectionId){if(!targetSectionId)throw new Error("Mutation sectionMove requires targetSectionId");const course=stateManager.get("course");this.sectionLock(stateManager,sectionIds,!0);const updates=await this._callEditWebservice("section_move",course.id,sectionIds,targetSectionId);this.bulkReset(stateManager),stateManager.processUpdates(updates),this.sectionLock(stateManager,sectionIds,!1)}async sectionMoveAfter(stateManager,sectionIds,targetSectionId){if(!targetSectionId)throw new Error("Mutation sectionMoveAfter requires targetSectionId");const course=stateManager.get("course");this.sectionLock(stateManager,sectionIds,!0);const updates=await this._callEditWebservice("section_move_after",course.id,sectionIds,targetSectionId);this.bulkReset(stateManager),stateManager.processUpdates(updates),this.sectionLock(stateManager,sectionIds,!1)}async addSection(stateManager,targetSectionId){targetSectionId||(targetSectionId=0);const course=stateManager.get("course"),updates=await this._callEditWebservice("section_add",course.id,[],targetSectionId);stateManager.processUpdates(updates)}async sectionDelete(stateManager,sectionIds){const course=stateManager.get("course"),updates=await this._callEditWebservice("section_delete",course.id,sectionIds);this.bulkReset(stateManager),stateManager.processUpdates(updates)}async cmDelete(stateManager,cmIds){const course=stateManager.get("course");this.cmLock(stateManager,cmIds,!0);const updates=await this._callEditWebservice("cm_delete",course.id,cmIds);this.bulkReset(stateManager),this.cmLock(stateManager,cmIds,!1),stateManager.processUpdates(updates)}cmDrag(stateManager,cmIds,dragValue){this.setPageItem(stateManager),this._setElementsValue(stateManager,"cm",cmIds,"dragging",dragValue)}sectionDrag(stateManager,sectionIds,dragValue){this.setPageItem(stateManager),this._setElementsValue(stateManager,"section",sectionIds,"dragging",dragValue)}cmCompletion(stateManager,cmIds,complete){const newValue=complete?1:0;this._setElementsValue(stateManager,"cm",cmIds,"completionstate",newValue)}async cmMoveRight(stateManager,cmIds){await this._cmBasicAction(stateManager,"cm_moveright",cmIds)}async cmMoveLeft(stateManager,cmIds){await this._cmBasicAction(stateManager,"cm_moveleft",cmIds)}async cmNoGroups(stateManager,cmIds){await this._cmBasicAction(stateManager,"cm_nogroups",cmIds)}async cmVisibleGroups(stateManager,cmIds){await this._cmBasicAction(stateManager,"cm_visiblegroups",cmIds)}async cmSeparateGroups(stateManager,cmIds){await this._cmBasicAction(stateManager,"cm_separategroups",cmIds)}cmLock(stateManager,cmIds,lockValue){this._setElementsValue(stateManager,"cm",cmIds,"locked",lockValue)}sectionLock(stateManager,sectionIds,lockValue){this._setElementsValue(stateManager,"section",sectionIds,"locked",lockValue)}_setElementsValue(stateManager,name,ids,fieldName,newValue){stateManager.setReadOnly(!1),ids.forEach((id=>{const element=stateManager.get(name,id);element&&(element[fieldName]=newValue)})),stateManager.setReadOnly(!0)}setPageItem(stateManager,type,id,isStatic){let newPageItem;if(void 0!==type&&(newPageItem=stateManager.get(type,id),!newPageItem))return;stateManager.setReadOnly(!1);const course=stateManager.get("course");course.pageItem=null,newPageItem&&(course.pageItem={id:id,type:type,sectionId:"section"==type?newPageItem.id:newPageItem.sectionid,isStatic:isStatic}),stateManager.setReadOnly(!0)}unlockAll(stateManager){const state=stateManager.state;stateManager.setReadOnly(!1),state.section.forEach((section=>{section.locked=!1})),state.cm.forEach((cm=>{cm.locked=!1})),stateManager.setReadOnly(!0)}async sectionIndexCollapsed(stateManager,sectionIds,collapsed){const collapsedIds=this._updateStateSectionPreference(stateManager,"indexcollapsed",sectionIds,collapsed);if(!collapsedIds)return;const course=stateManager.get("course");await this._callEditWebservice("section_index_collapsed",course.id,collapsedIds)}async allSectionsIndexCollapsed(stateManager,collapsed){const sectionIds=stateManager.getIds("section");this.sectionIndexCollapsed(stateManager,sectionIds,collapsed)}async sectionContentCollapsed(stateManager,sectionIds,collapsed){const collapsedIds=this._updateStateSectionPreference(stateManager,"contentcollapsed",sectionIds,collapsed);if(!collapsedIds)return;const course=stateManager.get("course");await this._callEditWebservice("section_content_collapsed",course.id,collapsedIds)}_updateStateSectionPreference(stateManager,preferenceName,sectionIds,preferenceValue){stateManager.setReadOnly(!1);const affectedSections=new Set;if(sectionIds.forEach((sectionId=>{const section=stateManager.get("section",sectionId);if(void 0===section)return null;const newValue=null!=preferenceValue?preferenceValue:section[preferenceName];section[preferenceName]!=newValue&&(section[preferenceName]=newValue,affectedSections.add(section.id))})),stateManager.setReadOnly(!0),0==affectedSections.size)return null;const collapsedSectionIds=[];return stateManager.state.section.forEach((section=>{section[preferenceName]&&collapsedSectionIds.push(section.id)})),collapsedSectionIds}bulkEnable(stateManager,enabled){const state=stateManager.state;stateManager.setReadOnly(!1),state.bulk.enabled=enabled,state.bulk.selectedType="",state.bulk.selection=[],stateManager.setReadOnly(!0)}bulkReset(stateManager){const state=stateManager.state;stateManager.setReadOnly(!1),state.bulk.selectedType="",state.bulk.selection=[],stateManager.setReadOnly(!0)}cmSelect(stateManager,cmIds){this._addIdsToSelection(stateManager,"cm",cmIds)}cmUnselect(stateManager,cmIds){this._removeIdsFromSelection(stateManager,"cm",cmIds)}sectionSelect(stateManager,sectionIds){this._addIdsToSelection(stateManager,"section",sectionIds)}sectionUnselect(stateManager,sectionIds){this._removeIdsFromSelection(stateManager,"section",sectionIds)}_addIdsToSelection(stateManager,typeName,ids){const bulk=stateManager.state.bulk;if(null==bulk||!bulk.enabled)throw new Error("Bulk is not enabled");if(""!==(null==bulk?void 0:bulk.selectedType)&&(null==bulk?void 0:bulk.selectedType)!==typeName)throw new Error("Cannot add ".concat(typeName," to the current selection"));ids=ids.map((value=>value.toString())),stateManager.setReadOnly(!1),bulk.selectedType=typeName;const newSelection=new Set([...bulk.selection,...ids]);bulk.selection=[...newSelection],stateManager.setReadOnly(!0)}_removeIdsFromSelection(stateManager,typeName,ids){const bulk=stateManager.state.bulk;if(null==bulk||!bulk.enabled)throw new Error("Bulk is not enabled");if(""!==(null==bulk?void 0:bulk.selectedType)&&(null==bulk?void 0:bulk.selectedType)!==typeName)throw new Error("Cannot remove ".concat(typeName," from the current selection"));ids=ids.map((value=>value.toString())),stateManager.setReadOnly(!1);const IdsToFilter=new Set(ids);bulk.selection=bulk.selection.filter((current=>!IdsToFilter.has(current))),0===bulk.selection.length&&(bulk.selectedType=""),stateManager.setReadOnly(!0)}async cmState(stateManager,cmids){this.cmLock(stateManager,cmids,!0);const course=stateManager.get("course"),updates=await this._callEditWebservice("cm_state",course.id,cmids);stateManager.processUpdates(updates),this.cmLock(stateManager,cmids,!1)}async sectionState(stateManager,sectionIds){this.sectionLock(stateManager,sectionIds,!0);const course=stateManager.get("course"),updates=await this._callEditWebservice("section_state",course.id,sectionIds);stateManager.processUpdates(updates),this.sectionLock(stateManager,sectionIds,!1)}async courseState(stateManager){const course=stateManager.get("course"),updates=await this._callEditWebservice("course_state",course.id);stateManager.processUpdates(updates)}},_exports.default})); //# sourceMappingURL=mutations.min.js.map \ No newline at end of file diff --git a/course/format/amd/build/local/courseeditor/mutations.min.js.map b/course/format/amd/build/local/courseeditor/mutations.min.js.map index 2dccbf42579..4f21b4da543 100644 --- a/course/format/amd/build/local/courseeditor/mutations.min.js.map +++ b/course/format/amd/build/local/courseeditor/mutations.min.js.map @@ -1 +1 @@ -{"version":3,"file":"mutations.min.js","sources":["../../../src/local/courseeditor/mutations.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\nimport ajax from 'core/ajax';\n\n/**\n * Default mutation manager\n *\n * @module core_courseformat/local/courseeditor/mutations\n * @class core_courseformat/local/courseeditor/mutations\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class {\n\n // All course editor mutations for Moodle 4.0 will be located in this file.\n\n /**\n * Private method to call core_courseformat_update_course webservice.\n *\n * @method _callEditWebservice\n * @param {string} action\n * @param {number} courseId\n * @param {array} ids\n * @param {number} targetSectionId optional target section id (for moving actions)\n * @param {number} targetCmId optional target cm id (for moving actions)\n */\n async _callEditWebservice(action, courseId, ids, targetSectionId, targetCmId) {\n const args = {\n action,\n courseid: courseId,\n ids,\n };\n if (targetSectionId) {\n args.targetsectionid = targetSectionId;\n }\n if (targetCmId) {\n args.targetcmid = targetCmId;\n }\n let ajaxresult = await ajax.call([{\n methodname: 'core_courseformat_update_course',\n args,\n }])[0];\n return JSON.parse(ajaxresult);\n }\n\n /**\n * Execute a basic section state action.\n * @param {StateManager} stateManager the current state manager\n * @param {string} action the action name\n * @param {array} sectionIds the section ids\n * @param {number} targetSectionId optional target section id (for moving actions)\n * @param {number} targetCmId optional target cm id (for moving actions)\n */\n async _sectionBasicAction(stateManager, action, sectionIds, targetSectionId, targetCmId) {\n const course = stateManager.get('course');\n this.sectionLock(stateManager, sectionIds, true);\n const updates = await this._callEditWebservice(\n action,\n course.id,\n sectionIds,\n targetSectionId,\n targetCmId\n );\n this.bulkReset(stateManager);\n stateManager.processUpdates(updates);\n this.sectionLock(stateManager, sectionIds, false);\n }\n\n /**\n * Execute a basic course module state action.\n * @param {StateManager} stateManager the current state manager\n * @param {string} action the action name\n * @param {array} cmIds the cm ids\n * @param {number} targetSectionId optional target section id (for moving actions)\n * @param {number} targetCmId optional target cm id (for moving actions)\n */\n async _cmBasicAction(stateManager, action, cmIds, targetSectionId, targetCmId) {\n const course = stateManager.get('course');\n this.cmLock(stateManager, cmIds, true);\n const updates = await this._callEditWebservice(\n action,\n course.id,\n cmIds,\n targetSectionId,\n targetCmId\n );\n this.bulkReset(stateManager);\n stateManager.processUpdates(updates);\n this.cmLock(stateManager, cmIds, false);\n }\n\n /**\n * Mutation module initialize.\n *\n * The reactive instance will execute this method when addMutations or setMutation is invoked.\n *\n * @param {StateManager} stateManager the state manager\n */\n init(stateManager) {\n // Add a method to prepare the fields when some update is comming from the server.\n stateManager.addUpdateTypes({\n prepareFields: this._prepareFields,\n });\n }\n\n /**\n * Add default values to state elements.\n *\n * This method is called every time a webservice returns a update state message.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n * @returns {Object} final fields data\n */\n _prepareFields(stateManager, updateName, fields) {\n // Any update should unlock the element.\n fields.locked = false;\n return fields;\n }\n\n /**\n * Hides sections.\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of section ids\n */\n async sectionHide(stateManager, sectionIds) {\n await this._sectionBasicAction(stateManager, 'section_hide', sectionIds);\n }\n\n /**\n * Show sections.\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of section ids\n */\n async sectionShow(stateManager, sectionIds) {\n await this._sectionBasicAction(stateManager, 'section_show', sectionIds);\n }\n\n /**\n * Show cms.\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of cm ids\n */\n async cmShow(stateManager, cmIds) {\n await this._cmBasicAction(stateManager, 'cm_show', cmIds);\n }\n\n /**\n * Hide cms.\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of cm ids\n */\n async cmHide(stateManager, cmIds) {\n await this._cmBasicAction(stateManager, 'cm_hide', cmIds);\n }\n\n /**\n * Stealth cms.\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of cm ids\n */\n async cmStealth(stateManager, cmIds) {\n await this._cmBasicAction(stateManager, 'cm_stealth', cmIds);\n }\n\n /**\n * Duplicate course modules\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of course modules ids\n * @param {number|undefined} targetSectionId the optional target sectionId\n * @param {number|undefined} targetCmId the target course module id\n */\n async cmDuplicate(stateManager, cmIds, targetSectionId, targetCmId) {\n const course = stateManager.get('course');\n // Lock all target sections.\n const sectionIds = new Set();\n if (targetSectionId) {\n sectionIds.add(targetSectionId);\n } else {\n cmIds.forEach((cmId) => {\n const cm = stateManager.get('cm', cmId);\n sectionIds.add(cm.sectionid);\n });\n }\n this.sectionLock(stateManager, Array.from(sectionIds), true);\n\n const updates = await this._callEditWebservice('cm_duplicate', course.id, cmIds, targetSectionId, targetCmId);\n this.bulkReset(stateManager);\n stateManager.processUpdates(updates);\n\n this.sectionLock(stateManager, Array.from(sectionIds), false);\n }\n\n /**\n * Move course modules to specific course location.\n *\n * Note that one of targetSectionId or targetCmId should be provided in order to identify the\n * new location:\n * - targetCmId: the activities will be located avobe the target cm. The targetSectionId\n * value will be ignored in this case.\n * - targetSectionId: the activities will be appended to the section. In this case\n * targetSectionId should not be present.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmids the list of cm ids to move\n * @param {number} targetSectionId the target section id\n * @param {number} targetCmId the target course module id\n */\n async cmMove(stateManager, cmids, targetSectionId, targetCmId) {\n if (!targetSectionId && !targetCmId) {\n throw new Error(`Mutation cmMove requires targetSectionId or targetCmId`);\n }\n const course = stateManager.get('course');\n this.cmLock(stateManager, cmids, true);\n const updates = await this._callEditWebservice('cm_move', course.id, cmids, targetSectionId, targetCmId);\n this.bulkReset(stateManager);\n stateManager.processUpdates(updates);\n this.cmLock(stateManager, cmids, false);\n }\n\n /**\n * Move course modules to specific course location.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of section ids to move\n * @param {number} targetSectionId the target section id\n */\n async sectionMove(stateManager, sectionIds, targetSectionId) {\n if (!targetSectionId) {\n throw new Error(`Mutation sectionMove requires targetSectionId`);\n }\n const course = stateManager.get('course');\n this.sectionLock(stateManager, sectionIds, true);\n const updates = await this._callEditWebservice('section_move', course.id, sectionIds, targetSectionId);\n this.bulkReset(stateManager);\n stateManager.processUpdates(updates);\n this.sectionLock(stateManager, sectionIds, false);\n }\n\n /**\n * Move course modules after a specific course location.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of section ids to move\n * @param {number} targetSectionId the target section id\n */\n async sectionMoveAfter(stateManager, sectionIds, targetSectionId) {\n if (!targetSectionId) {\n throw new Error(`Mutation sectionMoveAfter requires targetSectionId`);\n }\n const course = stateManager.get('course');\n this.sectionLock(stateManager, sectionIds, true);\n const updates = await this._callEditWebservice('section_move_after', course.id, sectionIds, targetSectionId);\n this.bulkReset(stateManager);\n stateManager.processUpdates(updates);\n this.sectionLock(stateManager, sectionIds, false);\n }\n\n /**\n * Add a new section to a specific course location.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {number} targetSectionId optional the target section id\n */\n async addSection(stateManager, targetSectionId) {\n if (!targetSectionId) {\n targetSectionId = 0;\n }\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('section_add', course.id, [], targetSectionId);\n stateManager.processUpdates(updates);\n }\n\n /**\n * Delete sections.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of course modules ids\n */\n async sectionDelete(stateManager, sectionIds) {\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('section_delete', course.id, sectionIds);\n this.bulkReset(stateManager);\n stateManager.processUpdates(updates);\n }\n\n /**\n * Delete cms.\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of section ids\n */\n async cmDelete(stateManager, cmIds) {\n const course = stateManager.get('course');\n this.cmLock(stateManager, cmIds, true);\n const updates = await this._callEditWebservice('cm_delete', course.id, cmIds);\n this.bulkReset(stateManager);\n this.cmLock(stateManager, cmIds, false);\n stateManager.processUpdates(updates);\n }\n\n /**\n * Mark or unmark course modules as dragging.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of course modules ids\n * @param {bool} dragValue the new dragging value\n */\n cmDrag(stateManager, cmIds, dragValue) {\n this.setPageItem(stateManager);\n this._setElementsValue(stateManager, 'cm', cmIds, 'dragging', dragValue);\n }\n\n /**\n * Mark or unmark course sections as dragging.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of section ids\n * @param {bool} dragValue the new dragging value\n */\n sectionDrag(stateManager, sectionIds, dragValue) {\n this.setPageItem(stateManager);\n this._setElementsValue(stateManager, 'section', sectionIds, 'dragging', dragValue);\n }\n\n /**\n * Mark or unmark course modules as complete.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of course modules ids\n * @param {bool} complete the new completion value\n */\n cmCompletion(stateManager, cmIds, complete) {\n const newValue = (complete) ? 1 : 0;\n this._setElementsValue(stateManager, 'cm', cmIds, 'completionstate', newValue);\n }\n\n /**\n * Move cms to the right: indent = 1.\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of cm ids\n */\n async cmMoveRight(stateManager, cmIds) {\n await this._cmBasicAction(stateManager, 'cm_moveright', cmIds);\n }\n\n /**\n * Move cms to the left: indent = 0.\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of cm ids\n */\n async cmMoveLeft(stateManager, cmIds) {\n await this._cmBasicAction(stateManager, 'cm_moveleft', cmIds);\n }\n\n /**\n * Set cms group mode to NOGROUPS.\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of cm ids\n */\n async cmNoGroups(stateManager, cmIds) {\n await this._cmBasicAction(stateManager, 'cm_nogroups', cmIds);\n }\n\n /**\n * Set cms group mode to VISIBLEGROUPS.\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of cm ids\n */\n async cmVisibleGroups(stateManager, cmIds) {\n await this._cmBasicAction(stateManager, 'cm_visiblegroups', cmIds);\n }\n\n /**\n * Set cms group mode to SEPARATEGROUPS.\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of cm ids\n */\n async cmSeparateGroups(stateManager, cmIds) {\n await this._cmBasicAction(stateManager, 'cm_separategroups', cmIds);\n }\n\n /**\n * Lock or unlock course modules.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of course modules ids\n * @param {bool} lockValue the new locked value\n */\n cmLock(stateManager, cmIds, lockValue) {\n this._setElementsValue(stateManager, 'cm', cmIds, 'locked', lockValue);\n }\n\n /**\n * Lock or unlock course sections.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of section ids\n * @param {bool} lockValue the new locked value\n */\n sectionLock(stateManager, sectionIds, lockValue) {\n this._setElementsValue(stateManager, 'section', sectionIds, 'locked', lockValue);\n }\n\n _setElementsValue(stateManager, name, ids, fieldName, newValue) {\n stateManager.setReadOnly(false);\n ids.forEach((id) => {\n const element = stateManager.get(name, id);\n if (element) {\n element[fieldName] = newValue;\n }\n });\n stateManager.setReadOnly(true);\n }\n\n /**\n * Set the page current item.\n *\n * Only one element of the course state can be the page item at a time.\n *\n * There are several actions that can alter the page current item. For example, when the user is in an activity\n * page, the page item is always the activity one. However, in a course page, when the user scrolls to an element,\n * this element get the page item.\n *\n * If the page item is static means that it is not meant to change. This is important because\n * static page items has some special logic. For example, if a cm is the static page item\n * and it is inside a collapsed section, the course index will expand the section to make it visible.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {String|undefined} type the element type (section or cm). Undefined will remove the current page item.\n * @param {Number|undefined} id the element id\n * @param {boolean|undefined} isStatic if the page item is static\n */\n setPageItem(stateManager, type, id, isStatic) {\n let newPageItem;\n if (type !== undefined) {\n newPageItem = stateManager.get(type, id);\n if (!newPageItem) {\n return;\n }\n }\n stateManager.setReadOnly(false);\n // Remove the current page item.\n const course = stateManager.get('course');\n course.pageItem = null;\n // Save the new page item.\n if (newPageItem) {\n course.pageItem = {\n id,\n type,\n sectionId: (type == 'section') ? newPageItem.id : newPageItem.sectionid,\n isStatic,\n };\n }\n stateManager.setReadOnly(true);\n }\n\n /**\n * Unlock all course elements.\n *\n * @param {StateManager} stateManager the current state manager\n */\n unlockAll(stateManager) {\n const state = stateManager.state;\n stateManager.setReadOnly(false);\n state.section.forEach((section) => {\n section.locked = false;\n });\n state.cm.forEach((cm) => {\n cm.locked = false;\n });\n stateManager.setReadOnly(true);\n }\n\n /**\n * Update the course index collapsed attribute of some sections.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the affected section ids\n * @param {boolean} collapsed the new collapsed value\n */\n async sectionIndexCollapsed(stateManager, sectionIds, collapsed) {\n const collapsedIds = this._updateStateSectionPreference(stateManager, 'indexcollapsed', sectionIds, collapsed);\n if (!collapsedIds) {\n return;\n }\n const course = stateManager.get('course');\n await this._callEditWebservice('section_index_collapsed', course.id, collapsedIds);\n }\n\n /**\n * Update the course content collapsed attribute of some sections.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the affected section ids\n * @param {boolean} collapsed the new collapsed value\n */\n async sectionContentCollapsed(stateManager, sectionIds, collapsed) {\n const collapsedIds = this._updateStateSectionPreference(stateManager, 'contentcollapsed', sectionIds, collapsed);\n if (!collapsedIds) {\n return;\n }\n const course = stateManager.get('course');\n await this._callEditWebservice('section_content_collapsed', course.id, collapsedIds);\n }\n\n /**\n * Private batch update for a section preference attribute.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {string} preferenceName the preference name\n * @param {array} sectionIds the affected section ids\n * @param {boolean} preferenceValue the new preferenceValue value\n * @return {Number[]|null} sections ids with the preference value true or null if no update is required\n */\n _updateStateSectionPreference(stateManager, preferenceName, sectionIds, preferenceValue) {\n stateManager.setReadOnly(false);\n const affectedSections = new Set();\n // Check if we need to update preferences.\n sectionIds.forEach(sectionId => {\n const section = stateManager.get('section', sectionId);\n if (section === undefined) {\n return null;\n }\n const newValue = preferenceValue ?? section[preferenceName];\n if (section[preferenceName] != newValue) {\n section[preferenceName] = newValue;\n affectedSections.add(section.id);\n }\n });\n stateManager.setReadOnly(true);\n if (affectedSections.size == 0) {\n return null;\n }\n // Get all collapsed section ids.\n const collapsedSectionIds = [];\n const state = stateManager.state;\n state.section.forEach(section => {\n if (section[preferenceName]) {\n collapsedSectionIds.push(section.id);\n }\n });\n return collapsedSectionIds;\n }\n\n /**\n * Enable/disable bulk editing.\n *\n * Note: reenabling the bulk will clean the current selection.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {Boolean} enabled the new bulk state.\n */\n bulkEnable(stateManager, enabled) {\n const state = stateManager.state;\n stateManager.setReadOnly(false);\n state.bulk.enabled = enabled;\n state.bulk.selectedType = '';\n state.bulk.selection = [];\n stateManager.setReadOnly(true);\n }\n\n /**\n * Reset the current selection.\n * @param {StateManager} stateManager the current state manager\n */\n bulkReset(stateManager) {\n const state = stateManager.state;\n stateManager.setReadOnly(false);\n state.bulk.selectedType = '';\n state.bulk.selection = [];\n stateManager.setReadOnly(true);\n }\n\n /**\n * Select a list of cms.\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of cm ids\n */\n cmSelect(stateManager, cmIds) {\n this._addIdsToSelection(stateManager, 'cm', cmIds);\n }\n\n /**\n * Unselect a list of cms.\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of cm ids\n */\n cmUnselect(stateManager, cmIds) {\n this._removeIdsFromSelection(stateManager, 'cm', cmIds);\n }\n\n /**\n * Select a list of sections.\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of cm ids\n */\n sectionSelect(stateManager, sectionIds) {\n this._addIdsToSelection(stateManager, 'section', sectionIds);\n }\n\n /**\n * Unselect a list of sections.\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of cm ids\n */\n sectionUnselect(stateManager, sectionIds) {\n this._removeIdsFromSelection(stateManager, 'section', sectionIds);\n }\n\n /**\n * Add some ids to the current bulk selection.\n * @param {StateManager} stateManager the current state manager\n * @param {String} typeName the type name (section/cm)\n * @param {array} ids the list of ids\n */\n _addIdsToSelection(stateManager, typeName, ids) {\n const bulk = stateManager.state.bulk;\n if (!bulk?.enabled) {\n throw new Error(`Bulk is not enabled`);\n }\n if (bulk?.selectedType !== \"\" && bulk?.selectedType !== typeName) {\n throw new Error(`Cannot add ${typeName} to the current selection`);\n }\n\n // Stored ids are strings for compatability with HTML data attributes.\n ids = ids.map(value => value.toString());\n\n stateManager.setReadOnly(false);\n bulk.selectedType = typeName;\n const newSelection = new Set([...bulk.selection, ...ids]);\n bulk.selection = [...newSelection];\n stateManager.setReadOnly(true);\n }\n\n /**\n * Remove some ids to the current bulk selection.\n *\n * The method resets the selection type if the current selection is empty.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {String} typeName the type name (section/cm)\n * @param {array} ids the list of ids\n */\n _removeIdsFromSelection(stateManager, typeName, ids) {\n const bulk = stateManager.state.bulk;\n if (!bulk?.enabled) {\n throw new Error(`Bulk is not enabled`);\n }\n if (bulk?.selectedType !== \"\" && bulk?.selectedType !== typeName) {\n throw new Error(`Cannot remove ${typeName} from the current selection`);\n }\n\n // Stored ids are strings for compatability with HTML data attributes.\n ids = ids.map(value => value.toString());\n\n stateManager.setReadOnly(false);\n const IdsToFilter = new Set(ids);\n bulk.selection = bulk.selection.filter(current => !IdsToFilter.has(current));\n if (bulk.selection.length === 0) {\n bulk.selectedType = '';\n }\n stateManager.setReadOnly(true);\n }\n\n /**\n * Get updated state data related to some cm ids.\n *\n * @method cmState\n * @param {StateManager} stateManager the current state\n * @param {array} cmids the list of cm ids to update\n */\n async cmState(stateManager, cmids) {\n this.cmLock(stateManager, cmids, true);\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('cm_state', course.id, cmids);\n stateManager.processUpdates(updates);\n this.cmLock(stateManager, cmids, false);\n }\n\n /**\n * Get updated state data related to some section ids.\n *\n * @method sectionState\n * @param {StateManager} stateManager the current state\n * @param {array} sectionIds the list of section ids to update\n */\n async sectionState(stateManager, sectionIds) {\n this.sectionLock(stateManager, sectionIds, true);\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('section_state', course.id, sectionIds);\n stateManager.processUpdates(updates);\n this.sectionLock(stateManager, sectionIds, false);\n }\n\n /**\n * Get the full updated state data of the course.\n *\n * @param {StateManager} stateManager the current state\n */\n async courseState(stateManager) {\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('course_state', course.id);\n stateManager.processUpdates(updates);\n }\n\n}\n"],"names":["action","courseId","ids","targetSectionId","targetCmId","args","courseid","targetsectionid","targetcmid","ajaxresult","ajax","call","methodname","JSON","parse","stateManager","sectionIds","course","get","sectionLock","updates","this","_callEditWebservice","id","bulkReset","processUpdates","cmIds","cmLock","init","addUpdateTypes","prepareFields","_prepareFields","updateName","fields","locked","_sectionBasicAction","_cmBasicAction","Set","add","forEach","cmId","cm","sectionid","Array","from","cmids","Error","cmDrag","dragValue","setPageItem","_setElementsValue","sectionDrag","cmCompletion","complete","newValue","lockValue","name","fieldName","setReadOnly","element","type","isStatic","newPageItem","undefined","pageItem","sectionId","unlockAll","state","section","collapsed","collapsedIds","_updateStateSectionPreference","preferenceName","preferenceValue","affectedSections","size","collapsedSectionIds","push","bulkEnable","enabled","bulk","selectedType","selection","cmSelect","_addIdsToSelection","cmUnselect","_removeIdsFromSelection","sectionSelect","sectionUnselect","typeName","map","value","toString","newSelection","IdsToFilter","filter","current","has","length"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"mutations.min.js","sources":["../../../src/local/courseeditor/mutations.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\nimport ajax from 'core/ajax';\n\n/**\n * Default mutation manager\n *\n * @module core_courseformat/local/courseeditor/mutations\n * @class core_courseformat/local/courseeditor/mutations\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class {\n\n // All course editor mutations for Moodle 4.0 will be located in this file.\n\n /**\n * Private method to call core_courseformat_update_course webservice.\n *\n * @method _callEditWebservice\n * @param {string} action\n * @param {number} courseId\n * @param {array} ids\n * @param {number} targetSectionId optional target section id (for moving actions)\n * @param {number} targetCmId optional target cm id (for moving actions)\n */\n async _callEditWebservice(action, courseId, ids, targetSectionId, targetCmId) {\n const args = {\n action,\n courseid: courseId,\n ids,\n };\n if (targetSectionId) {\n args.targetsectionid = targetSectionId;\n }\n if (targetCmId) {\n args.targetcmid = targetCmId;\n }\n let ajaxresult = await ajax.call([{\n methodname: 'core_courseformat_update_course',\n args,\n }])[0];\n return JSON.parse(ajaxresult);\n }\n\n /**\n * Execute a basic section state action.\n * @param {StateManager} stateManager the current state manager\n * @param {string} action the action name\n * @param {array} sectionIds the section ids\n * @param {number} targetSectionId optional target section id (for moving actions)\n * @param {number} targetCmId optional target cm id (for moving actions)\n */\n async _sectionBasicAction(stateManager, action, sectionIds, targetSectionId, targetCmId) {\n const course = stateManager.get('course');\n this.sectionLock(stateManager, sectionIds, true);\n const updates = await this._callEditWebservice(\n action,\n course.id,\n sectionIds,\n targetSectionId,\n targetCmId\n );\n this.bulkReset(stateManager);\n stateManager.processUpdates(updates);\n this.sectionLock(stateManager, sectionIds, false);\n }\n\n /**\n * Execute a basic course module state action.\n * @param {StateManager} stateManager the current state manager\n * @param {string} action the action name\n * @param {array} cmIds the cm ids\n * @param {number} targetSectionId optional target section id (for moving actions)\n * @param {number} targetCmId optional target cm id (for moving actions)\n */\n async _cmBasicAction(stateManager, action, cmIds, targetSectionId, targetCmId) {\n const course = stateManager.get('course');\n this.cmLock(stateManager, cmIds, true);\n const updates = await this._callEditWebservice(\n action,\n course.id,\n cmIds,\n targetSectionId,\n targetCmId\n );\n this.bulkReset(stateManager);\n stateManager.processUpdates(updates);\n this.cmLock(stateManager, cmIds, false);\n }\n\n /**\n * Mutation module initialize.\n *\n * The reactive instance will execute this method when addMutations or setMutation is invoked.\n *\n * @param {StateManager} stateManager the state manager\n */\n init(stateManager) {\n // Add a method to prepare the fields when some update is comming from the server.\n stateManager.addUpdateTypes({\n prepareFields: this._prepareFields,\n });\n }\n\n /**\n * Add default values to state elements.\n *\n * This method is called every time a webservice returns a update state message.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n * @returns {Object} final fields data\n */\n _prepareFields(stateManager, updateName, fields) {\n // Any update should unlock the element.\n fields.locked = false;\n return fields;\n }\n\n /**\n * Hides sections.\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of section ids\n */\n async sectionHide(stateManager, sectionIds) {\n await this._sectionBasicAction(stateManager, 'section_hide', sectionIds);\n }\n\n /**\n * Show sections.\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of section ids\n */\n async sectionShow(stateManager, sectionIds) {\n await this._sectionBasicAction(stateManager, 'section_show', sectionIds);\n }\n\n /**\n * Show cms.\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of cm ids\n */\n async cmShow(stateManager, cmIds) {\n await this._cmBasicAction(stateManager, 'cm_show', cmIds);\n }\n\n /**\n * Hide cms.\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of cm ids\n */\n async cmHide(stateManager, cmIds) {\n await this._cmBasicAction(stateManager, 'cm_hide', cmIds);\n }\n\n /**\n * Stealth cms.\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of cm ids\n */\n async cmStealth(stateManager, cmIds) {\n await this._cmBasicAction(stateManager, 'cm_stealth', cmIds);\n }\n\n /**\n * Duplicate course modules\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of course modules ids\n * @param {number|undefined} targetSectionId the optional target sectionId\n * @param {number|undefined} targetCmId the target course module id\n */\n async cmDuplicate(stateManager, cmIds, targetSectionId, targetCmId) {\n const course = stateManager.get('course');\n // Lock all target sections.\n const sectionIds = new Set();\n if (targetSectionId) {\n sectionIds.add(targetSectionId);\n } else {\n cmIds.forEach((cmId) => {\n const cm = stateManager.get('cm', cmId);\n sectionIds.add(cm.sectionid);\n });\n }\n this.sectionLock(stateManager, Array.from(sectionIds), true);\n\n const updates = await this._callEditWebservice('cm_duplicate', course.id, cmIds, targetSectionId, targetCmId);\n this.bulkReset(stateManager);\n stateManager.processUpdates(updates);\n\n this.sectionLock(stateManager, Array.from(sectionIds), false);\n }\n\n /**\n * Move course modules to specific course location.\n *\n * Note that one of targetSectionId or targetCmId should be provided in order to identify the\n * new location:\n * - targetCmId: the activities will be located avobe the target cm. The targetSectionId\n * value will be ignored in this case.\n * - targetSectionId: the activities will be appended to the section. In this case\n * targetSectionId should not be present.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmids the list of cm ids to move\n * @param {number} targetSectionId the target section id\n * @param {number} targetCmId the target course module id\n */\n async cmMove(stateManager, cmids, targetSectionId, targetCmId) {\n if (!targetSectionId && !targetCmId) {\n throw new Error(`Mutation cmMove requires targetSectionId or targetCmId`);\n }\n const course = stateManager.get('course');\n this.cmLock(stateManager, cmids, true);\n const updates = await this._callEditWebservice('cm_move', course.id, cmids, targetSectionId, targetCmId);\n this.bulkReset(stateManager);\n stateManager.processUpdates(updates);\n this.cmLock(stateManager, cmids, false);\n }\n\n /**\n * Move course modules to specific course location.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of section ids to move\n * @param {number} targetSectionId the target section id\n */\n async sectionMove(stateManager, sectionIds, targetSectionId) {\n if (!targetSectionId) {\n throw new Error(`Mutation sectionMove requires targetSectionId`);\n }\n const course = stateManager.get('course');\n this.sectionLock(stateManager, sectionIds, true);\n const updates = await this._callEditWebservice('section_move', course.id, sectionIds, targetSectionId);\n this.bulkReset(stateManager);\n stateManager.processUpdates(updates);\n this.sectionLock(stateManager, sectionIds, false);\n }\n\n /**\n * Move course modules after a specific course location.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of section ids to move\n * @param {number} targetSectionId the target section id\n */\n async sectionMoveAfter(stateManager, sectionIds, targetSectionId) {\n if (!targetSectionId) {\n throw new Error(`Mutation sectionMoveAfter requires targetSectionId`);\n }\n const course = stateManager.get('course');\n this.sectionLock(stateManager, sectionIds, true);\n const updates = await this._callEditWebservice('section_move_after', course.id, sectionIds, targetSectionId);\n this.bulkReset(stateManager);\n stateManager.processUpdates(updates);\n this.sectionLock(stateManager, sectionIds, false);\n }\n\n /**\n * Add a new section to a specific course location.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {number} targetSectionId optional the target section id\n */\n async addSection(stateManager, targetSectionId) {\n if (!targetSectionId) {\n targetSectionId = 0;\n }\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('section_add', course.id, [], targetSectionId);\n stateManager.processUpdates(updates);\n }\n\n /**\n * Delete sections.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of course modules ids\n */\n async sectionDelete(stateManager, sectionIds) {\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('section_delete', course.id, sectionIds);\n this.bulkReset(stateManager);\n stateManager.processUpdates(updates);\n }\n\n /**\n * Delete cms.\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of section ids\n */\n async cmDelete(stateManager, cmIds) {\n const course = stateManager.get('course');\n this.cmLock(stateManager, cmIds, true);\n const updates = await this._callEditWebservice('cm_delete', course.id, cmIds);\n this.bulkReset(stateManager);\n this.cmLock(stateManager, cmIds, false);\n stateManager.processUpdates(updates);\n }\n\n /**\n * Mark or unmark course modules as dragging.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of course modules ids\n * @param {bool} dragValue the new dragging value\n */\n cmDrag(stateManager, cmIds, dragValue) {\n this.setPageItem(stateManager);\n this._setElementsValue(stateManager, 'cm', cmIds, 'dragging', dragValue);\n }\n\n /**\n * Mark or unmark course sections as dragging.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of section ids\n * @param {bool} dragValue the new dragging value\n */\n sectionDrag(stateManager, sectionIds, dragValue) {\n this.setPageItem(stateManager);\n this._setElementsValue(stateManager, 'section', sectionIds, 'dragging', dragValue);\n }\n\n /**\n * Mark or unmark course modules as complete.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of course modules ids\n * @param {bool} complete the new completion value\n */\n cmCompletion(stateManager, cmIds, complete) {\n const newValue = (complete) ? 1 : 0;\n this._setElementsValue(stateManager, 'cm', cmIds, 'completionstate', newValue);\n }\n\n /**\n * Move cms to the right: indent = 1.\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of cm ids\n */\n async cmMoveRight(stateManager, cmIds) {\n await this._cmBasicAction(stateManager, 'cm_moveright', cmIds);\n }\n\n /**\n * Move cms to the left: indent = 0.\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of cm ids\n */\n async cmMoveLeft(stateManager, cmIds) {\n await this._cmBasicAction(stateManager, 'cm_moveleft', cmIds);\n }\n\n /**\n * Set cms group mode to NOGROUPS.\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of cm ids\n */\n async cmNoGroups(stateManager, cmIds) {\n await this._cmBasicAction(stateManager, 'cm_nogroups', cmIds);\n }\n\n /**\n * Set cms group mode to VISIBLEGROUPS.\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of cm ids\n */\n async cmVisibleGroups(stateManager, cmIds) {\n await this._cmBasicAction(stateManager, 'cm_visiblegroups', cmIds);\n }\n\n /**\n * Set cms group mode to SEPARATEGROUPS.\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of cm ids\n */\n async cmSeparateGroups(stateManager, cmIds) {\n await this._cmBasicAction(stateManager, 'cm_separategroups', cmIds);\n }\n\n /**\n * Lock or unlock course modules.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of course modules ids\n * @param {bool} lockValue the new locked value\n */\n cmLock(stateManager, cmIds, lockValue) {\n this._setElementsValue(stateManager, 'cm', cmIds, 'locked', lockValue);\n }\n\n /**\n * Lock or unlock course sections.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of section ids\n * @param {bool} lockValue the new locked value\n */\n sectionLock(stateManager, sectionIds, lockValue) {\n this._setElementsValue(stateManager, 'section', sectionIds, 'locked', lockValue);\n }\n\n _setElementsValue(stateManager, name, ids, fieldName, newValue) {\n stateManager.setReadOnly(false);\n ids.forEach((id) => {\n const element = stateManager.get(name, id);\n if (element) {\n element[fieldName] = newValue;\n }\n });\n stateManager.setReadOnly(true);\n }\n\n /**\n * Set the page current item.\n *\n * Only one element of the course state can be the page item at a time.\n *\n * There are several actions that can alter the page current item. For example, when the user is in an activity\n * page, the page item is always the activity one. However, in a course page, when the user scrolls to an element,\n * this element get the page item.\n *\n * If the page item is static means that it is not meant to change. This is important because\n * static page items has some special logic. For example, if a cm is the static page item\n * and it is inside a collapsed section, the course index will expand the section to make it visible.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {String|undefined} type the element type (section or cm). Undefined will remove the current page item.\n * @param {Number|undefined} id the element id\n * @param {boolean|undefined} isStatic if the page item is static\n */\n setPageItem(stateManager, type, id, isStatic) {\n let newPageItem;\n if (type !== undefined) {\n newPageItem = stateManager.get(type, id);\n if (!newPageItem) {\n return;\n }\n }\n stateManager.setReadOnly(false);\n // Remove the current page item.\n const course = stateManager.get('course');\n course.pageItem = null;\n // Save the new page item.\n if (newPageItem) {\n course.pageItem = {\n id,\n type,\n sectionId: (type == 'section') ? newPageItem.id : newPageItem.sectionid,\n isStatic,\n };\n }\n stateManager.setReadOnly(true);\n }\n\n /**\n * Unlock all course elements.\n *\n * @param {StateManager} stateManager the current state manager\n */\n unlockAll(stateManager) {\n const state = stateManager.state;\n stateManager.setReadOnly(false);\n state.section.forEach((section) => {\n section.locked = false;\n });\n state.cm.forEach((cm) => {\n cm.locked = false;\n });\n stateManager.setReadOnly(true);\n }\n\n /**\n * Update the course index collapsed attribute of some sections.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the affected section ids\n * @param {boolean} collapsed the new collapsed value\n */\n async sectionIndexCollapsed(stateManager, sectionIds, collapsed) {\n const collapsedIds = this._updateStateSectionPreference(stateManager, 'indexcollapsed', sectionIds, collapsed);\n if (!collapsedIds) {\n return;\n }\n const course = stateManager.get('course');\n await this._callEditWebservice('section_index_collapsed', course.id, collapsedIds);\n }\n\n /**\n * Update the course index collapsed attribute of all sections.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {boolean} collapsed the new collapsed value\n */\n async allSectionsIndexCollapsed(stateManager, collapsed) {\n const sectionIds = stateManager.getIds('section');\n this.sectionIndexCollapsed(stateManager, sectionIds, collapsed);\n }\n\n /**\n * Update the course content collapsed attribute of some sections.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the affected section ids\n * @param {boolean} collapsed the new collapsed value\n */\n async sectionContentCollapsed(stateManager, sectionIds, collapsed) {\n const collapsedIds = this._updateStateSectionPreference(stateManager, 'contentcollapsed', sectionIds, collapsed);\n if (!collapsedIds) {\n return;\n }\n const course = stateManager.get('course');\n await this._callEditWebservice('section_content_collapsed', course.id, collapsedIds);\n }\n\n /**\n * Private batch update for a section preference attribute.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {string} preferenceName the preference name\n * @param {array} sectionIds the affected section ids\n * @param {boolean} preferenceValue the new preferenceValue value\n * @return {Number[]|null} sections ids with the preference value true or null if no update is required\n */\n _updateStateSectionPreference(stateManager, preferenceName, sectionIds, preferenceValue) {\n stateManager.setReadOnly(false);\n const affectedSections = new Set();\n // Check if we need to update preferences.\n sectionIds.forEach(sectionId => {\n const section = stateManager.get('section', sectionId);\n if (section === undefined) {\n return null;\n }\n const newValue = preferenceValue ?? section[preferenceName];\n if (section[preferenceName] != newValue) {\n section[preferenceName] = newValue;\n affectedSections.add(section.id);\n }\n });\n stateManager.setReadOnly(true);\n if (affectedSections.size == 0) {\n return null;\n }\n // Get all collapsed section ids.\n const collapsedSectionIds = [];\n const state = stateManager.state;\n state.section.forEach(section => {\n if (section[preferenceName]) {\n collapsedSectionIds.push(section.id);\n }\n });\n return collapsedSectionIds;\n }\n\n /**\n * Enable/disable bulk editing.\n *\n * Note: reenabling the bulk will clean the current selection.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {Boolean} enabled the new bulk state.\n */\n bulkEnable(stateManager, enabled) {\n const state = stateManager.state;\n stateManager.setReadOnly(false);\n state.bulk.enabled = enabled;\n state.bulk.selectedType = '';\n state.bulk.selection = [];\n stateManager.setReadOnly(true);\n }\n\n /**\n * Reset the current selection.\n * @param {StateManager} stateManager the current state manager\n */\n bulkReset(stateManager) {\n const state = stateManager.state;\n stateManager.setReadOnly(false);\n state.bulk.selectedType = '';\n state.bulk.selection = [];\n stateManager.setReadOnly(true);\n }\n\n /**\n * Select a list of cms.\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of cm ids\n */\n cmSelect(stateManager, cmIds) {\n this._addIdsToSelection(stateManager, 'cm', cmIds);\n }\n\n /**\n * Unselect a list of cms.\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of cm ids\n */\n cmUnselect(stateManager, cmIds) {\n this._removeIdsFromSelection(stateManager, 'cm', cmIds);\n }\n\n /**\n * Select a list of sections.\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of cm ids\n */\n sectionSelect(stateManager, sectionIds) {\n this._addIdsToSelection(stateManager, 'section', sectionIds);\n }\n\n /**\n * Unselect a list of sections.\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of cm ids\n */\n sectionUnselect(stateManager, sectionIds) {\n this._removeIdsFromSelection(stateManager, 'section', sectionIds);\n }\n\n /**\n * Add some ids to the current bulk selection.\n * @param {StateManager} stateManager the current state manager\n * @param {String} typeName the type name (section/cm)\n * @param {array} ids the list of ids\n */\n _addIdsToSelection(stateManager, typeName, ids) {\n const bulk = stateManager.state.bulk;\n if (!bulk?.enabled) {\n throw new Error(`Bulk is not enabled`);\n }\n if (bulk?.selectedType !== \"\" && bulk?.selectedType !== typeName) {\n throw new Error(`Cannot add ${typeName} to the current selection`);\n }\n\n // Stored ids are strings for compatability with HTML data attributes.\n ids = ids.map(value => value.toString());\n\n stateManager.setReadOnly(false);\n bulk.selectedType = typeName;\n const newSelection = new Set([...bulk.selection, ...ids]);\n bulk.selection = [...newSelection];\n stateManager.setReadOnly(true);\n }\n\n /**\n * Remove some ids to the current bulk selection.\n *\n * The method resets the selection type if the current selection is empty.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {String} typeName the type name (section/cm)\n * @param {array} ids the list of ids\n */\n _removeIdsFromSelection(stateManager, typeName, ids) {\n const bulk = stateManager.state.bulk;\n if (!bulk?.enabled) {\n throw new Error(`Bulk is not enabled`);\n }\n if (bulk?.selectedType !== \"\" && bulk?.selectedType !== typeName) {\n throw new Error(`Cannot remove ${typeName} from the current selection`);\n }\n\n // Stored ids are strings for compatability with HTML data attributes.\n ids = ids.map(value => value.toString());\n\n stateManager.setReadOnly(false);\n const IdsToFilter = new Set(ids);\n bulk.selection = bulk.selection.filter(current => !IdsToFilter.has(current));\n if (bulk.selection.length === 0) {\n bulk.selectedType = '';\n }\n stateManager.setReadOnly(true);\n }\n\n /**\n * Get updated state data related to some cm ids.\n *\n * @method cmState\n * @param {StateManager} stateManager the current state\n * @param {array} cmids the list of cm ids to update\n */\n async cmState(stateManager, cmids) {\n this.cmLock(stateManager, cmids, true);\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('cm_state', course.id, cmids);\n stateManager.processUpdates(updates);\n this.cmLock(stateManager, cmids, false);\n }\n\n /**\n * Get updated state data related to some section ids.\n *\n * @method sectionState\n * @param {StateManager} stateManager the current state\n * @param {array} sectionIds the list of section ids to update\n */\n async sectionState(stateManager, sectionIds) {\n this.sectionLock(stateManager, sectionIds, true);\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('section_state', course.id, sectionIds);\n stateManager.processUpdates(updates);\n this.sectionLock(stateManager, sectionIds, false);\n }\n\n /**\n * Get the full updated state data of the course.\n *\n * @param {StateManager} stateManager the current state\n */\n async courseState(stateManager) {\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('course_state', course.id);\n stateManager.processUpdates(updates);\n }\n\n}\n"],"names":["action","courseId","ids","targetSectionId","targetCmId","args","courseid","targetsectionid","targetcmid","ajaxresult","ajax","call","methodname","JSON","parse","stateManager","sectionIds","course","get","sectionLock","updates","this","_callEditWebservice","id","bulkReset","processUpdates","cmIds","cmLock","init","addUpdateTypes","prepareFields","_prepareFields","updateName","fields","locked","_sectionBasicAction","_cmBasicAction","Set","add","forEach","cmId","cm","sectionid","Array","from","cmids","Error","cmDrag","dragValue","setPageItem","_setElementsValue","sectionDrag","cmCompletion","complete","newValue","lockValue","name","fieldName","setReadOnly","element","type","isStatic","newPageItem","undefined","pageItem","sectionId","unlockAll","state","section","collapsed","collapsedIds","_updateStateSectionPreference","getIds","sectionIndexCollapsed","preferenceName","preferenceValue","affectedSections","size","collapsedSectionIds","push","bulkEnable","enabled","bulk","selectedType","selection","cmSelect","_addIdsToSelection","cmUnselect","_removeIdsFromSelection","sectionSelect","sectionUnselect","typeName","map","value","toString","newSelection","IdsToFilter","filter","current","has","length"],"mappings":"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"} \ No newline at end of file diff --git a/course/format/amd/src/local/courseeditor/mutations.js b/course/format/amd/src/local/courseeditor/mutations.js index 712b14c283c..49e33447f96 100644 --- a/course/format/amd/src/local/courseeditor/mutations.js +++ b/course/format/amd/src/local/courseeditor/mutations.js @@ -501,6 +501,17 @@ export default class { await this._callEditWebservice('section_index_collapsed', course.id, collapsedIds); } + /** + * Update the course index collapsed attribute of all sections. + * + * @param {StateManager} stateManager the current state manager + * @param {boolean} collapsed the new collapsed value + */ + async allSectionsIndexCollapsed(stateManager, collapsed) { + const sectionIds = stateManager.getIds('section'); + this.sectionIndexCollapsed(stateManager, sectionIds, collapsed); + } + /** * Update the course content collapsed attribute of some sections. * diff --git a/course/format/tests/behat/course_courseindex.feature b/course/format/tests/behat/course_courseindex.feature index 1498e35356c..2045b1e07cf 100644 --- a/course/format/tests/behat/course_courseindex.feature +++ b/course/format/tests/behat/course_courseindex.feature @@ -186,6 +186,35 @@ Feature: Course index depending on role And I should see "Topic 3" in the "courseindex-content" "region" And I should see "Activity sample 3" in the "courseindex-content" "region" + @javascript + Scenario: Course index toggling all sections + When I am on the "Course 1" course page logged in as teacher1 + # Sections should be opened by default. + Then I should see "Topic 1" in the "courseindex-content" "region" + And I should see "Activity sample 1" in the "courseindex-content" "region" + And I should see "Topic 2" in the "courseindex-content" "region" + And I should see "Activity sample 2" in the "courseindex-content" "region" + And I should see "Topic 3" in the "courseindex-content" "region" + And I should see "Activity sample 3" in the "courseindex-content" "region" + # Collapse all sections + And I click on "Course index options" "button" in the "#courseindexdrawercontrols" "css_element" + And I click on "Collapse all" "link" in the "#courseindexdrawercontrols" "css_element" + And I should see "Topic 1" in the "courseindex-content" "region" + And I should not see "Activity sample 1" in the "courseindex-content" "region" + And I should see "Topic 2" in the "courseindex-content" "region" + And I should not see "Activity sample 2" in the "courseindex-content" "region" + And I should see "Topic 3" in the "courseindex-content" "region" + And I should not see "Activity sample 3" in the "courseindex-content" "region" + # Expand all sections + And I click on "Course index options" "button" in the "#courseindexdrawercontrols" "css_element" + And I click on "Expand all" "link" in the "#courseindexdrawercontrols" "css_element" + And I should see "Topic 1" in the "courseindex-content" "region" + And I should see "Activity sample 1" in the "courseindex-content" "region" + And I should see "Topic 2" in the "courseindex-content" "region" + And I should see "Activity sample 2" in the "courseindex-content" "region" + And I should see "Topic 3" in the "courseindex-content" "region" + And I should see "Activity sample 3" in the "courseindex-content" "region" + @javascript Scenario: Course index section preferences When I am on the "C1" "Course" page logged in as "teacher1" diff --git a/course/format/tests/behat/courseindex_keyboardnav.feature b/course/format/tests/behat/courseindex_keyboardnav.feature index 7f6d629c951..40684d8b60c 100644 --- a/course/format/tests/behat/courseindex_keyboardnav.feature +++ b/course/format/tests/behat/courseindex_keyboardnav.feature @@ -23,11 +23,13 @@ Feature: Verify that courseindex is usable with the keyboard And I should see "Topic 1" in the "courseindex-content" "region" And the focused element is "[data-preference='drawer-open-index'] .drawertoggle" "css_element" And I press the tab key + And I press the tab key And the focused element is ".courseindex-section" "css_element" @javascript Scenario: General focus on open course index. When I press the shift tab key + And I press the shift tab key And the focused element is "[data-preference='drawer-open-index'] .drawertoggle" "css_element" And I press enter Then I should not see "Topic 1" in the "courseindex-content" "region" @@ -35,6 +37,7 @@ Feature: Verify that courseindex is usable with the keyboard @javascript @accessibility Scenario: Course index should be accessible. When I press the shift tab key + And I press the shift tab key And I press enter Then the page should meet accessibility standards with "wcag143" extra tests And I press enter diff --git a/lang/en/courseformat.php b/lang/en/courseformat.php index 41e5e4400b2..2d100a91eec 100644 --- a/lang/en/courseformat.php +++ b/lang/en/courseformat.php @@ -45,6 +45,7 @@ $string['cmmove_info'] = 'Move {$a} after'; $string['cmsmove_title'] = 'Move selected activities'; $string['cmsmove_info'] = 'Move {$a} activities after'; $string['courseindex'] = 'Course index'; +$string['courseindexoptions'] = 'Course index options'; $string['nobulkaction'] = 'No bulk actions available'; $string['preference:coursesectionspreferences'] = 'Section user preferences for course {$a}'; $string['privacy:metadata:preference:coursesectionspreferences'] = 'Section user preferences like collapsed and expanded.'; diff --git a/lib/amd/build/local/reactive/statemanager.min.js b/lib/amd/build/local/reactive/statemanager.min.js index 015e34f9c35..91877fba6a9 100644 --- a/lib/amd/build/local/reactive/statemanager.min.js +++ b/lib/amd/build/local/reactive/statemanager.min.js @@ -15,6 +15,6 @@ define("core/local/reactive/statemanager",["exports"],(function(_exports){Object * @copyright 2021 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class{constructor(dispatchEvent,target){this.dispatchEvent=dispatchEvent,this.target=null!=target?target:document,this.readonly=!1,this.eventsToPublish=[],this.updateTypes={create:this.defaultCreate.bind(this),update:this.defaultUpdate.bind(this),delete:this.defaultDelete.bind(this),put:this.defaultPut.bind(this),override:this.defaultOverride.bind(this),remove:this.defaultRemove.bind(this),prepareFields:this.defaultPrepareFields.bind(this)},this.initialPromise=new Promise((resolve=>{this.target.addEventListener("state:loaded",(event=>{resolve(event.detail.state)}))}))}setInitialState(initialState){if(void 0!==this.state)throw Error("Initial state can only be initialized ones");const state=new Proxy({},new Handler("state",this,!0));for(const[prop,propValue]of Object.entries(initialState))state[prop]=propValue;this.state=state,this.readonly=!0,this.dispatchEvent({action:"state:loaded",state:this.state},this.target)}getInitialPromise(){return this.initialPromise}setReadOnly(readonly){this.readonly=readonly;let mode="off";this.readonly&&(mode="on",this._publishEvents()),this.dispatchEvent({action:"readmode:".concat(mode),state:this.state,element:null},this.target)}addUpdateTypes(newFunctions){for(const[updateType,updateFunction]of Object.entries(newFunctions))"function"==typeof updateFunction&&(this.updateTypes[updateType]=updateFunction.bind(newFunctions))}processUpdates(updates,updateTypes){if(!Array.isArray(updates))throw Error("State updates must be an array");this.setReadOnly(!1),updates.forEach((update=>{if(void 0===update.name)throw Error("Missing state update name");this.processUpdate(update.name,update.action,update.fields,updateTypes)})),this.setReadOnly(!0)}processUpdate(updateName,action,fields,updateTypes){var _action,_updateTypes$action,_updateTypes$prepareF;if(!fields)throw Error("Missing state update fields");void 0===updateTypes&&(updateTypes={});const method=null!==(_updateTypes$action=updateTypes[action=null!==(_action=action)&&void 0!==_action?_action:"update"])&&void 0!==_updateTypes$action?_updateTypes$action:this.updateTypes[action];if(void 0===method)throw Error("Unkown update action ".concat(action));method(this,updateName,(null!==(_updateTypes$prepareF=updateTypes.prepareFields)&&void 0!==_updateTypes$prepareF?_updateTypes$prepareF:this.updateTypes.prepareFields)(this,updateName,fields))}defaultPrepareFields(stateManager,updateName,fields){return fields}defaultCreate(stateManager,updateName,fields){let state=stateManager.state;state[updateName]instanceof StateMap?state[updateName].add(fields):state[updateName]=fields}defaultDelete(stateManager,updateName,fields){if(!stateManager.get(updateName,fields.id))throw Error("Inexistent ".concat(updateName," ").concat(fields.id));let state=stateManager.state;state[updateName]instanceof StateMap?state[updateName].delete(fields.id):delete state[updateName]}defaultRemove(stateManager,updateName,fields){if(!stateManager.get(updateName,fields.id))return;let state=stateManager.state;state[updateName]instanceof StateMap?state[updateName].delete(fields.id):delete state[updateName]}defaultUpdate(stateManager,updateName,fields){let current=stateManager.get(updateName,fields.id);if(!current)throw Error("Inexistent ".concat(updateName," ").concat(fields.id));for(const[fieldName,fieldValue]of Object.entries(fields))current[fieldName]=fieldValue}defaultPut(stateManager,updateName,fields){let current=stateManager.get(updateName,fields.id);if(current)for(const[fieldName,fieldValue]of Object.entries(fields))current[fieldName]=fieldValue;else{let state=stateManager.state;if(state[updateName]instanceof StateMap)return void state[updateName].add(fields);state[updateName]=fields}}defaultOverride(stateManager,updateName,fields){let current=stateManager.get(updateName,fields.id);if(current){for(const[fieldName]of Object.entries(current))void 0===fields[fieldName]&&delete current[fieldName];for(const[fieldName,fieldValue]of Object.entries(fields))current[fieldName]=fieldValue}else{let state=stateManager.state;if(state[updateName]instanceof StateMap)return void state[updateName].add(fields);state[updateName]=fields}}get(name,id){const state=this.state;let current=state[name];if(current instanceof StateMap){if(void 0===id)throw Error("Missing id for ".concat(name," state update"));current=state[name].get(id)}return current}registerStateAction(field,prop,action,data){let parentAction="updated";null!==prop?this.eventsToPublish.push({eventName:"".concat(field,".").concat(prop,":").concat(action),eventData:data,action:action}):parentAction=action,void 0!==data.id&&(null!==prop&&this.eventsToPublish.push({eventName:"".concat(field,"[").concat(data.id,"].").concat(prop,":").concat(action),eventData:data,action:action}),this.eventsToPublish.push({eventName:"".concat(field,"[").concat(data.id,"]:").concat(parentAction),eventData:data,action:parentAction})),this.eventsToPublish.push({eventName:"".concat(field,":").concat(parentAction),eventData:data,action:parentAction}),this.eventsToPublish.push({eventName:"state:updated",eventData:data,action:"updated"})}_publishEvents(){const fieldChanges=this.eventsToPublish;this.eventsToPublish=[],this.dispatchEvent({action:"transaction:start",state:this.state,element:null,changes:fieldChanges},this.target),fieldChanges.sort(((a,b)=>{var _weights$a$action,_weights$b$action;const weights={created:0,updated:1,deleted:2},aweight=null!==(_weights$a$action=weights[a.action])&&void 0!==_weights$a$action?_weights$a$action:0,bweight=null!==(_weights$b$action=weights[b.action])&&void 0!==_weights$b$action?_weights$b$action:0;return aweight===bweight?a.eventName.length-b.eventName.length:aweight-bweight}));let publishedEvents=new Set;fieldChanges.forEach((event=>{var _event$eventData$id;const eventkey="".concat(event.eventName,".").concat(null!==(_event$eventData$id=event.eventData.id)&&void 0!==_event$eventData$id?_event$eventData$id:0);publishedEvents.has(eventkey)||(this.dispatchEvent({action:event.eventName,state:this.state,element:event.eventData},this.target),publishedEvents.add(eventkey))})),this.dispatchEvent({action:"transaction:end",state:this.state,element:null},this.target)}};class Handler{constructor(name,stateManager,proxyValues){this.name=name,this.stateManager=stateManager,this.proxyValues=null!=proxyValues&&proxyValues}set(obj,prop,value,receiver){if(this.stateManager.readonly)throw new Error("State locked. Use mutations to change ".concat(prop," value in ").concat(this.name,"."));if(JSON.stringify(obj[prop])===JSON.stringify(value))return!0;const action=void 0!==obj[prop]?"updated":"created";return this.proxyValues?Array.isArray(value)?obj[prop]=new StateMap(prop,this.stateManager).loadValues(value):obj[prop]=new Proxy(value,new Handler(prop,this.stateManager)):obj[prop]=value,void 0===this.stateManager.state||this.stateManager.registerStateAction(this.name,prop,action,receiver),!0}deleteProperty(obj,prop){if(this.stateManager.readonly)throw new Error("State locked. Use mutations to delete ".concat(prop," in ").concat(this.name,"."));return prop in obj&&(delete obj[prop],this.stateManager.registerStateAction(this.name,prop,"deleted",obj)),!0}}class StateMap extends Map{constructor(name,stateManager,iterable){super(iterable),this.name=name,this.stateManager=stateManager}set(key,value){if(this.stateManager.readonly)throw new Error("State locked. Use mutations to change ".concat(key," value in ").concat(this.name,"."));if(key=this.normalizeKey(key),this.checkValue(value),null==key)throw Error("State lists keys cannot be null or undefined");if(this.normalizeKey(value.id)!==key)throw new Error("State error: ".concat(this.name," list element ID (").concat(value.id,") and key (").concat(key,") mismatch"));const action=super.has(key)?"updated":"created",result=super.set(key,new Proxy(value,new Handler(this.name,this.stateManager)));return void 0===this.stateManager.state||this.stateManager.registerStateAction(this.name,null,action,super.get(key)),result}checkValue(value){if(void 0===value.id)throw Error("State lists elements must contain at least an id attribute")}normalizeKey(key){return String(key).valueOf()}add(value){return this.checkValue(value),this.set(value.id,value)}get(key){return super.get(this.normalizeKey(key))}has(key){return super.has(this.normalizeKey(key))}delete(key){if(key=this.normalizeKey(key),this.stateManager.readonly)throw new Error("State locked. Use mutations to change ".concat(key," value in ").concat(this.name,"."));const previous=super.get(key),result=super.delete(key);return result?(this.stateManager.registerStateAction(this.name,null,"deleted",previous),result):result}toJSON(){let result=[];return this.forEach((value=>{result.push(value)})),result}loadValues(values){return values.forEach((data=>{this.checkValue(data);let key=data.id,newvalue=new Proxy(data,new Handler(this.name,this.stateManager));this.set(key,newvalue)})),this}}return _exports.default})); +class{constructor(dispatchEvent,target){this.dispatchEvent=dispatchEvent,this.target=null!=target?target:document,this.readonly=!1,this.eventsToPublish=[],this.updateTypes={create:this.defaultCreate.bind(this),update:this.defaultUpdate.bind(this),delete:this.defaultDelete.bind(this),put:this.defaultPut.bind(this),override:this.defaultOverride.bind(this),remove:this.defaultRemove.bind(this),prepareFields:this.defaultPrepareFields.bind(this)},this.initialPromise=new Promise((resolve=>{this.target.addEventListener("state:loaded",(event=>{resolve(event.detail.state)}))}))}setInitialState(initialState){if(void 0!==this.state)throw Error("Initial state can only be initialized ones");const state=new Proxy({},new Handler("state",this,!0));for(const[prop,propValue]of Object.entries(initialState))state[prop]=propValue;this.state=state,this.readonly=!0,this.dispatchEvent({action:"state:loaded",state:this.state},this.target)}getInitialPromise(){return this.initialPromise}setReadOnly(readonly){this.readonly=readonly;let mode="off";this.readonly&&(mode="on",this._publishEvents()),this.dispatchEvent({action:"readmode:".concat(mode),state:this.state,element:null},this.target)}addUpdateTypes(newFunctions){for(const[updateType,updateFunction]of Object.entries(newFunctions))"function"==typeof updateFunction&&(this.updateTypes[updateType]=updateFunction.bind(newFunctions))}processUpdates(updates,updateTypes){if(!Array.isArray(updates))throw Error("State updates must be an array");this.setReadOnly(!1),updates.forEach((update=>{if(void 0===update.name)throw Error("Missing state update name");this.processUpdate(update.name,update.action,update.fields,updateTypes)})),this.setReadOnly(!0)}processUpdate(updateName,action,fields,updateTypes){var _action,_updateTypes$action,_updateTypes$prepareF;if(!fields)throw Error("Missing state update fields");void 0===updateTypes&&(updateTypes={});const method=null!==(_updateTypes$action=updateTypes[action=null!==(_action=action)&&void 0!==_action?_action:"update"])&&void 0!==_updateTypes$action?_updateTypes$action:this.updateTypes[action];if(void 0===method)throw Error("Unkown update action ".concat(action));method(this,updateName,(null!==(_updateTypes$prepareF=updateTypes.prepareFields)&&void 0!==_updateTypes$prepareF?_updateTypes$prepareF:this.updateTypes.prepareFields)(this,updateName,fields))}defaultPrepareFields(stateManager,updateName,fields){return fields}defaultCreate(stateManager,updateName,fields){let state=stateManager.state;state[updateName]instanceof StateMap?state[updateName].add(fields):state[updateName]=fields}defaultDelete(stateManager,updateName,fields){if(!stateManager.get(updateName,fields.id))throw Error("Inexistent ".concat(updateName," ").concat(fields.id));let state=stateManager.state;state[updateName]instanceof StateMap?state[updateName].delete(fields.id):delete state[updateName]}defaultRemove(stateManager,updateName,fields){if(!stateManager.get(updateName,fields.id))return;let state=stateManager.state;state[updateName]instanceof StateMap?state[updateName].delete(fields.id):delete state[updateName]}defaultUpdate(stateManager,updateName,fields){let current=stateManager.get(updateName,fields.id);if(!current)throw Error("Inexistent ".concat(updateName," ").concat(fields.id));for(const[fieldName,fieldValue]of Object.entries(fields))current[fieldName]=fieldValue}defaultPut(stateManager,updateName,fields){let current=stateManager.get(updateName,fields.id);if(current)for(const[fieldName,fieldValue]of Object.entries(fields))current[fieldName]=fieldValue;else{let state=stateManager.state;if(state[updateName]instanceof StateMap)return void state[updateName].add(fields);state[updateName]=fields}}defaultOverride(stateManager,updateName,fields){let current=stateManager.get(updateName,fields.id);if(current){for(const[fieldName]of Object.entries(current))void 0===fields[fieldName]&&delete current[fieldName];for(const[fieldName,fieldValue]of Object.entries(fields))current[fieldName]=fieldValue}else{let state=stateManager.state;if(state[updateName]instanceof StateMap)return void state[updateName].add(fields);state[updateName]=fields}}get(name,id){const state=this.state;let current=state[name];if(current instanceof StateMap){if(void 0===id)throw Error("Missing id for ".concat(name," state update"));current=state[name].get(id)}return current}getIds(name){const state=this.state;if(!(state[name]instanceof StateMap))throw Error("".concat(name," is not an instance of StateMap"));return[...state[name].keys()]}registerStateAction(field,prop,action,data){let parentAction="updated";null!==prop?this.eventsToPublish.push({eventName:"".concat(field,".").concat(prop,":").concat(action),eventData:data,action:action}):parentAction=action,void 0!==data.id&&(null!==prop&&this.eventsToPublish.push({eventName:"".concat(field,"[").concat(data.id,"].").concat(prop,":").concat(action),eventData:data,action:action}),this.eventsToPublish.push({eventName:"".concat(field,"[").concat(data.id,"]:").concat(parentAction),eventData:data,action:parentAction})),this.eventsToPublish.push({eventName:"".concat(field,":").concat(parentAction),eventData:data,action:parentAction}),this.eventsToPublish.push({eventName:"state:updated",eventData:data,action:"updated"})}_publishEvents(){const fieldChanges=this.eventsToPublish;this.eventsToPublish=[],this.dispatchEvent({action:"transaction:start",state:this.state,element:null,changes:fieldChanges},this.target),fieldChanges.sort(((a,b)=>{var _weights$a$action,_weights$b$action;const weights={created:0,updated:1,deleted:2},aweight=null!==(_weights$a$action=weights[a.action])&&void 0!==_weights$a$action?_weights$a$action:0,bweight=null!==(_weights$b$action=weights[b.action])&&void 0!==_weights$b$action?_weights$b$action:0;return aweight===bweight?a.eventName.length-b.eventName.length:aweight-bweight}));let publishedEvents=new Set;fieldChanges.forEach((event=>{var _event$eventData$id;const eventkey="".concat(event.eventName,".").concat(null!==(_event$eventData$id=event.eventData.id)&&void 0!==_event$eventData$id?_event$eventData$id:0);publishedEvents.has(eventkey)||(this.dispatchEvent({action:event.eventName,state:this.state,element:event.eventData},this.target),publishedEvents.add(eventkey))})),this.dispatchEvent({action:"transaction:end",state:this.state,element:null},this.target)}};class Handler{constructor(name,stateManager,proxyValues){this.name=name,this.stateManager=stateManager,this.proxyValues=null!=proxyValues&&proxyValues}set(obj,prop,value,receiver){if(this.stateManager.readonly)throw new Error("State locked. Use mutations to change ".concat(prop," value in ").concat(this.name,"."));if(JSON.stringify(obj[prop])===JSON.stringify(value))return!0;const action=void 0!==obj[prop]?"updated":"created";return this.proxyValues?Array.isArray(value)?obj[prop]=new StateMap(prop,this.stateManager).loadValues(value):obj[prop]=new Proxy(value,new Handler(prop,this.stateManager)):obj[prop]=value,void 0===this.stateManager.state||this.stateManager.registerStateAction(this.name,prop,action,receiver),!0}deleteProperty(obj,prop){if(this.stateManager.readonly)throw new Error("State locked. Use mutations to delete ".concat(prop," in ").concat(this.name,"."));return prop in obj&&(delete obj[prop],this.stateManager.registerStateAction(this.name,prop,"deleted",obj)),!0}}class StateMap extends Map{constructor(name,stateManager,iterable){super(iterable),this.name=name,this.stateManager=stateManager}set(key,value){if(this.stateManager.readonly)throw new Error("State locked. Use mutations to change ".concat(key," value in ").concat(this.name,"."));if(key=this.normalizeKey(key),this.checkValue(value),null==key)throw Error("State lists keys cannot be null or undefined");if(this.normalizeKey(value.id)!==key)throw new Error("State error: ".concat(this.name," list element ID (").concat(value.id,") and key (").concat(key,") mismatch"));const action=super.has(key)?"updated":"created",result=super.set(key,new Proxy(value,new Handler(this.name,this.stateManager)));return void 0===this.stateManager.state||this.stateManager.registerStateAction(this.name,null,action,super.get(key)),result}checkValue(value){if(void 0===value.id)throw Error("State lists elements must contain at least an id attribute")}normalizeKey(key){return String(key).valueOf()}add(value){return this.checkValue(value),this.set(value.id,value)}get(key){return super.get(this.normalizeKey(key))}has(key){return super.has(this.normalizeKey(key))}delete(key){if(key=this.normalizeKey(key),this.stateManager.readonly)throw new Error("State locked. Use mutations to change ".concat(key," value in ").concat(this.name,"."));const previous=super.get(key),result=super.delete(key);return result?(this.stateManager.registerStateAction(this.name,null,"deleted",previous),result):result}toJSON(){let result=[];return this.forEach((value=>{result.push(value)})),result}loadValues(values){return values.forEach((data=>{this.checkValue(data);let key=data.id,newvalue=new Proxy(data,new Handler(this.name,this.stateManager));this.set(key,newvalue)})),this}}return _exports.default})); //# sourceMappingURL=statemanager.min.js.map \ No newline at end of file diff --git a/lib/amd/build/local/reactive/statemanager.min.js.map b/lib/amd/build/local/reactive/statemanager.min.js.map index 9310b33768b..9d0d2db271e 100644 --- a/lib/amd/build/local/reactive/statemanager.min.js.map +++ b/lib/amd/build/local/reactive/statemanager.min.js.map @@ -1 +1 @@ -{"version":3,"file":"statemanager.min.js","sources":["../../../src/local/reactive/statemanager.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Reactive simple state manager.\n *\n * The state manager contains the state data, trigger update events and\n * can lock and unlock the state data.\n *\n * This file contains the three main elements of the state manager:\n * - State manager: the public class to alter the state, dispatch events and process update messages.\n * - Proxy handler: a private class to keep track of the state object changes.\n * - StateMap class: a private class extending Map class that triggers event when a state list is modifed.\n *\n * @module core/local/reactive/statemanager\n * @class StateManager\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * State manager class.\n *\n * This class handle the reactive state and ensure only valid mutations can modify the state.\n * It also provide methods to apply batch state update messages (see processUpdates function doc\n * for more details on update messages).\n *\n * Implementing a deep state manager is complex and will require many frontend resources. To keep\n * the state fast and simple, the state can ONLY store two kind of data:\n * - Object with attributes\n * - Sets of objects with id attributes.\n *\n * This is an example of a valid state:\n *\n * {\n * course: {\n * name: 'course name',\n * shortname: 'courseshort',\n * sectionlist: [21, 34]\n * },\n * sections: [\n * {id: 21, name: 'Topic 1', visible: true},\n * {id: 34, name: 'Topic 2', visible: false,\n * ],\n * }\n *\n * The following cases are NOT allowed at a state ROOT level (throws an exception if they are assigned):\n * - Simple values (strings, boolean...).\n * - Arrays of simple values.\n * - Array of objects without ID attribute (all arrays will be converted to maps and requires an ID).\n *\n * Thanks to those limitations it can simplify the state update messages and the event names. If You\n * need to store simple data, just group them in an object.\n *\n * To grant any state change triggers the proper events, the class uses two private structures:\n * - proxy handler: any object stored in the state is proxied using this class.\n * - StateMap class: any object set in the state will be converted to StateMap using the\n * objects id attribute.\n */\nexport default class StateManager {\n\n /**\n * Create a basic reactive state store.\n *\n * The state manager is meant to work with native JS events. To ensure each reactive module can use\n * it in its own way, the parent element must provide a valid event dispatcher function and an optional\n * DOM element to anchor the event.\n *\n * @param {function} dispatchEvent the function to dispatch the custom event when the state changes.\n * @param {element} target the state changed custom event target (document if none provided)\n */\n constructor(dispatchEvent, target) {\n\n // The dispatch event function.\n /** @package */\n this.dispatchEvent = dispatchEvent;\n\n // The DOM container to trigger events.\n /** @package */\n this.target = target ?? document;\n\n // State can be altered freely until initial state is set.\n /** @package */\n this.readonly = false;\n\n // List of state changes pending to be published as events.\n /** @package */\n this.eventsToPublish = [];\n\n // The update state types functions.\n /** @package */\n this.updateTypes = {\n \"create\": this.defaultCreate.bind(this),\n \"update\": this.defaultUpdate.bind(this),\n \"delete\": this.defaultDelete.bind(this),\n \"put\": this.defaultPut.bind(this),\n \"override\": this.defaultOverride.bind(this),\n \"remove\": this.defaultRemove.bind(this),\n \"prepareFields\": this.defaultPrepareFields.bind(this),\n };\n\n // The state_loaded event is special because it only happens one but all components\n // may react to that state, even if they are registered after the setIinitialState.\n // For these reason we use a promise for that event.\n this.initialPromise = new Promise((resolve) => {\n const initialStateDone = (event) => {\n resolve(event.detail.state);\n };\n this.target.addEventListener('state:loaded', initialStateDone);\n });\n }\n\n /**\n * Loads the initial state.\n *\n * Note this method will trigger a state changed event with \"state:loaded\" actionname.\n *\n * The state mode will be set to read only when the initial state is loaded.\n *\n * @param {object} initialState\n */\n setInitialState(initialState) {\n\n if (this.state !== undefined) {\n throw Error('Initial state can only be initialized ones');\n }\n\n // Create the state object.\n const state = new Proxy({}, new Handler('state', this, true));\n for (const [prop, propValue] of Object.entries(initialState)) {\n state[prop] = propValue;\n }\n this.state = state;\n\n // When the state is loaded we can lock it to prevent illegal changes.\n this.readonly = true;\n\n this.dispatchEvent({\n action: 'state:loaded',\n state: this.state,\n }, this.target);\n }\n\n /**\n * Generate a promise that will be resolved when the initial state is loaded.\n *\n * In most cases the final state will be loaded using an ajax call. This is the reason\n * why states manager are created unlocked and won't be reactive until the initial state is set.\n *\n * @return {Promise} the resulting promise\n */\n getInitialPromise() {\n return this.initialPromise;\n }\n\n /**\n * Locks or unlocks the state to prevent illegal updates.\n *\n * Mutations use this method to modify the state. Once the state is updated, they must\n * block again the state.\n *\n * All changes done while the state is writable will be registered using registerStateAction.\n * When the state is set again to read only the method will trigger _publishEvents to communicate\n * changes to all watchers.\n *\n * @param {bool} readonly if the state is in read only mode enabled\n */\n setReadOnly(readonly) {\n\n this.readonly = readonly;\n\n let mode = 'off';\n\n // When the state is in readonly again is time to publish all pending events.\n if (this.readonly) {\n mode = 'on';\n this._publishEvents();\n }\n\n // Dispatch a read only event.\n this.dispatchEvent({\n action: `readmode:${mode}`,\n state: this.state,\n element: null,\n }, this.target);\n }\n\n /**\n * Add methods to process update state messages.\n *\n * The state manager provide a default update, create and delete methods. However,\n * some applications may require to override the default methods or even add new ones\n * like \"refresh\" or \"error\".\n *\n * @param {Object} newFunctions the new update types functions.\n */\n addUpdateTypes(newFunctions) {\n for (const [updateType, updateFunction] of Object.entries(newFunctions)) {\n if (typeof updateFunction === 'function') {\n this.updateTypes[updateType] = updateFunction.bind(newFunctions);\n }\n }\n }\n\n /**\n * Process a state updates array and do all the necessary changes.\n *\n * Note this method unlocks the state while it is executing and relocks it\n * when finishes.\n *\n * @param {array} updates\n * @param {Object} updateTypes optional functions to override the default update types.\n */\n processUpdates(updates, updateTypes) {\n if (!Array.isArray(updates)) {\n throw Error('State updates must be an array');\n }\n this.setReadOnly(false);\n updates.forEach((update) => {\n if (update.name === undefined) {\n throw Error('Missing state update name');\n }\n this.processUpdate(\n update.name,\n update.action,\n update.fields,\n updateTypes\n );\n });\n this.setReadOnly(true);\n }\n\n /**\n * Process a single state update.\n *\n * Note this method will not lock or unlock the state by itself.\n *\n * @param {string} updateName the state element to update\n * @param {string} action to action to perform\n * @param {object} fields the new data\n * @param {Object} updateTypes optional functions to override the default update types.\n */\n processUpdate(updateName, action, fields, updateTypes) {\n\n if (!fields) {\n throw Error('Missing state update fields');\n }\n\n if (updateTypes === undefined) {\n updateTypes = {};\n }\n\n action = action ?? 'update';\n\n const method = updateTypes[action] ?? this.updateTypes[action];\n\n if (method === undefined) {\n throw Error(`Unkown update action ${action}`);\n }\n\n // Some state data may require some cooking before sending to the\n // state. Reactive instances can overrdide the default fieldDefaults\n // method to add extra logic to all updates.\n const prepareFields = updateTypes.prepareFields ?? this.updateTypes.prepareFields;\n\n method(this, updateName, prepareFields(this, updateName, fields));\n }\n\n /**\n * Prepare fields for processing.\n *\n * This method is used to add default values or calculations from the frontend side.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n * @returns {Object} final fields data\n */\n defaultPrepareFields(stateManager, updateName, fields) {\n return fields;\n }\n\n\n /**\n * Process a create state message.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n */\n defaultCreate(stateManager, updateName, fields) {\n\n let state = stateManager.state;\n\n // Create can be applied only to lists, not to objects.\n if (state[updateName] instanceof StateMap) {\n state[updateName].add(fields);\n return;\n }\n state[updateName] = fields;\n }\n\n /**\n * Process a delete state message.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n */\n defaultDelete(stateManager, updateName, fields) {\n\n // Get the current value.\n let current = stateManager.get(updateName, fields.id);\n if (!current) {\n throw Error(`Inexistent ${updateName} ${fields.id}`);\n }\n\n // Process deletion.\n let state = stateManager.state;\n\n if (state[updateName] instanceof StateMap) {\n state[updateName].delete(fields.id);\n return;\n }\n delete state[updateName];\n }\n\n /**\n * Process a remove state message.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n */\n defaultRemove(stateManager, updateName, fields) {\n\n // Get the current value.\n let current = stateManager.get(updateName, fields.id);\n if (!current) {\n return;\n }\n\n // Process deletion.\n let state = stateManager.state;\n\n if (state[updateName] instanceof StateMap) {\n state[updateName].delete(fields.id);\n return;\n }\n delete state[updateName];\n }\n\n /**\n * Process a update state message.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n */\n defaultUpdate(stateManager, updateName, fields) {\n\n // Get the current value.\n let current = stateManager.get(updateName, fields.id);\n if (!current) {\n throw Error(`Inexistent ${updateName} ${fields.id}`);\n }\n\n // Execute updates.\n for (const [fieldName, fieldValue] of Object.entries(fields)) {\n current[fieldName] = fieldValue;\n }\n }\n\n /**\n * Process a put state message.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n */\n defaultPut(stateManager, updateName, fields) {\n\n // Get the current value.\n let current = stateManager.get(updateName, fields.id);\n if (current) {\n // Update attributes.\n for (const [fieldName, fieldValue] of Object.entries(fields)) {\n current[fieldName] = fieldValue;\n }\n } else {\n // Create new object.\n let state = stateManager.state;\n if (state[updateName] instanceof StateMap) {\n state[updateName].add(fields);\n return;\n }\n state[updateName] = fields;\n }\n }\n\n /**\n * Process an override state message.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n */\n defaultOverride(stateManager, updateName, fields) {\n\n // Get the current value.\n let current = stateManager.get(updateName, fields.id);\n if (current) {\n // Remove any unnecessary fields.\n for (const [fieldName] of Object.entries(current)) {\n if (fields[fieldName] === undefined) {\n delete current[fieldName];\n }\n }\n // Update field.\n for (const [fieldName, fieldValue] of Object.entries(fields)) {\n current[fieldName] = fieldValue;\n }\n } else {\n // Create the element if not exists.\n let state = stateManager.state;\n if (state[updateName] instanceof StateMap) {\n state[updateName].add(fields);\n return;\n }\n state[updateName] = fields;\n }\n }\n\n /**\n * Get an element from the state or form an alternative state object.\n *\n * The altstate param is used by external update functions that gets the current\n * state as param.\n *\n * @param {String} name the state object name\n * @param {*} id and object id for state maps.\n * @return {Object|undefined} the state object found\n */\n get(name, id) {\n const state = this.state;\n\n let current = state[name];\n if (current instanceof StateMap) {\n if (id === undefined) {\n throw Error(`Missing id for ${name} state update`);\n }\n current = state[name].get(id);\n }\n\n return current;\n }\n\n /**\n * Register a state modification and generate the necessary events.\n *\n * This method is used mainly by proxy helpers to dispatch state change event.\n * However, mutations can use it to inform components about non reactive changes\n * in the state (only the two first levels of the state are reactive).\n *\n * Each action can produce several events:\n * - The specific attribute updated, created or deleter (example: \"cm.visible:updated\")\n * - The general state object updated, created or deleted (example: \"cm:updated\")\n * - If the element has an ID attribute, the specific event with id (example: \"cm[42].visible:updated\")\n * - If the element has an ID attribute, the general event with id (example: \"cm[42]:updated\")\n * - A generic state update event \"state:update\"\n *\n * @param {string} field the affected state field name\n * @param {string|null} prop the affecter field property (null if affect the full object)\n * @param {string} action the action done (created/updated/deleted)\n * @param {*} data the affected data\n */\n registerStateAction(field, prop, action, data) {\n\n let parentAction = 'updated';\n\n if (prop !== null) {\n this.eventsToPublish.push({\n eventName: `${field}.${prop}:${action}`,\n eventData: data,\n action,\n });\n } else {\n parentAction = action;\n }\n\n // Trigger extra events if the element has an ID attribute.\n if (data.id !== undefined) {\n if (prop !== null) {\n this.eventsToPublish.push({\n eventName: `${field}[${data.id}].${prop}:${action}`,\n eventData: data,\n action,\n });\n }\n this.eventsToPublish.push({\n eventName: `${field}[${data.id}]:${parentAction}`,\n eventData: data,\n action: parentAction,\n });\n }\n\n // Register the general change.\n this.eventsToPublish.push({\n eventName: `${field}:${parentAction}`,\n eventData: data,\n action: parentAction,\n });\n\n // Register state updated event.\n this.eventsToPublish.push({\n eventName: `state:updated`,\n eventData: data,\n action: 'updated',\n });\n }\n\n /**\n * Internal method to publish events.\n *\n * This is a private method, it will be invoked when the state is set back to read only mode.\n */\n _publishEvents() {\n const fieldChanges = this.eventsToPublish;\n this.eventsToPublish = [];\n\n // Dispatch a transaction start event.\n this.dispatchEvent({\n action: 'transaction:start',\n state: this.state,\n element: null,\n changes: fieldChanges,\n }, this.target);\n\n // State changes can be registered in any order. However it will avoid many\n // components errors if they are sorted to have creations-updates-deletes in case\n // some component needs to create or destroy DOM elements before updating them.\n fieldChanges.sort((a, b) => {\n const weights = {\n created: 0,\n updated: 1,\n deleted: 2,\n };\n const aweight = weights[a.action] ?? 0;\n const bweight = weights[b.action] ?? 0;\n // In case both have the same weight, the eventName length decide.\n if (aweight === bweight) {\n return a.eventName.length - b.eventName.length;\n }\n return aweight - bweight;\n });\n\n // List of the published events to prevent redundancies.\n let publishedEvents = new Set();\n\n fieldChanges.forEach((event) => {\n\n const eventkey = `${event.eventName}.${event.eventData.id ?? 0}`;\n\n if (!publishedEvents.has(eventkey)) {\n this.dispatchEvent({\n action: event.eventName,\n state: this.state,\n element: event.eventData\n }, this.target);\n\n publishedEvents.add(eventkey);\n }\n });\n\n // Dispatch a transaction end event.\n this.dispatchEvent({\n action: 'transaction:end',\n state: this.state,\n element: null,\n }, this.target);\n }\n}\n\n// Proxy helpers.\n\n/**\n * The proxy handler.\n *\n * This class will inform any value change directly to the state manager.\n *\n * The proxied variable will throw an error if it is altered when the state manager is\n * in read only mode.\n */\nclass Handler {\n\n /**\n * Class constructor.\n *\n * @param {string} name the variable name used for identify triggered actions\n * @param {StateManager} stateManager the state manager object\n * @param {boolean} proxyValues if new values must be proxied (used only at state root level)\n */\n constructor(name, stateManager, proxyValues) {\n this.name = name;\n this.stateManager = stateManager;\n this.proxyValues = proxyValues ?? false;\n }\n\n /**\n * Set trap to trigger events when the state changes.\n *\n * @param {object} obj the source object (not proxied)\n * @param {string} prop the attribute to set\n * @param {*} value the value to save\n * @param {*} receiver the proxied element to be attached to events\n * @returns {boolean} if the value is set\n */\n set(obj, prop, value, receiver) {\n\n // Only mutations should be able to set state values.\n if (this.stateManager.readonly) {\n throw new Error(`State locked. Use mutations to change ${prop} value in ${this.name}.`);\n }\n\n // Check any data change.\n if (JSON.stringify(obj[prop]) === JSON.stringify(value)) {\n return true;\n }\n\n const action = (obj[prop] !== undefined) ? 'updated' : 'created';\n\n // Proxy value if necessary (used at state root level).\n if (this.proxyValues) {\n if (Array.isArray(value)) {\n obj[prop] = new StateMap(prop, this.stateManager).loadValues(value);\n } else {\n obj[prop] = new Proxy(value, new Handler(prop, this.stateManager));\n }\n } else {\n obj[prop] = value;\n }\n\n // If the state is not ready yet means the initial state is not yet loaded.\n if (this.stateManager.state === undefined) {\n return true;\n }\n\n this.stateManager.registerStateAction(this.name, prop, action, receiver);\n\n return true;\n }\n\n /**\n * Delete property trap to trigger state change events.\n *\n * @param {*} obj the affected object (not proxied)\n * @param {*} prop the prop to delete\n * @returns {boolean} if prop is deleted\n */\n deleteProperty(obj, prop) {\n // Only mutations should be able to set state values.\n if (this.stateManager.readonly) {\n throw new Error(`State locked. Use mutations to delete ${prop} in ${this.name}.`);\n }\n if (prop in obj) {\n\n delete obj[prop];\n\n this.stateManager.registerStateAction(this.name, prop, 'deleted', obj);\n }\n return true;\n }\n}\n\n/**\n * Class to add events dispatching to the JS Map class.\n *\n * When the state has a list of objects (with IDs) it will be converted into a StateMap.\n * StateMap is used almost in the same way as a regular JS map. Because all elements have an\n * id attribute, it has some specific methods:\n * - add: a convenient method to add an element without specifying the key (\"id\" attribute will be used as a key).\n * - loadValues: to add many elements at once wihout specifying keys (\"id\" attribute will be used).\n *\n * Apart, the main difference between regular Map and MapState is that this one will inform any change to the\n * state manager.\n */\nclass StateMap extends Map {\n\n /**\n * Create a reactive Map.\n *\n * @param {string} name the property name\n * @param {StateManager} stateManager the state manager\n * @param {iterable} iterable an iterable object to create the Map\n */\n constructor(name, stateManager, iterable) {\n // We don't have any \"this\" until be call super.\n super(iterable);\n this.name = name;\n this.stateManager = stateManager;\n }\n\n /**\n * Set an element into the map.\n *\n * Each value needs it's own id attribute. Objects without id will be rejected.\n * The function will throw an error if the value id and the key are not the same.\n *\n * @param {*} key the key to store\n * @param {*} value the value to store\n * @returns {Map} the resulting Map object\n */\n set(key, value) {\n\n // Only mutations should be able to set state values.\n if (this.stateManager.readonly) {\n throw new Error(`State locked. Use mutations to change ${key} value in ${this.name}.`);\n }\n\n // Normalize keys as string to prevent json decoding errors.\n key = this.normalizeKey(key);\n\n this.checkValue(value);\n\n if (key === undefined || key === null) {\n throw Error('State lists keys cannot be null or undefined');\n }\n\n // ID is mandatory and should be the same as the key.\n if (this.normalizeKey(value.id) !== key) {\n throw new Error(`State error: ${this.name} list element ID (${value.id}) and key (${key}) mismatch`);\n }\n\n const action = (super.has(key)) ? 'updated' : 'created';\n\n // Save proxied data into the list.\n const result = super.set(key, new Proxy(value, new Handler(this.name, this.stateManager)));\n\n // If the state is not ready yet means the initial state is not yet loaded.\n if (this.stateManager.state === undefined) {\n return result;\n }\n\n this.stateManager.registerStateAction(this.name, null, action, super.get(key));\n\n return result;\n }\n\n /**\n * Check if a value is valid to be stored in a a State List.\n *\n * Only objects with id attribute can be stored in State lists.\n *\n * This method throws an error if the value is not valid.\n *\n * @param {object} value (with ID)\n */\n checkValue(value) {\n if (!typeof value === 'object' && value !== null) {\n throw Error('State lists can contain objects only');\n }\n\n if (value.id === undefined) {\n throw Error('State lists elements must contain at least an id attribute');\n }\n }\n\n /**\n * Return a normalized key value for state map.\n *\n * Regular maps uses strict key comparissons but state maps are indexed by ID.JSON conversions\n * and webservices sometimes do unexpected types conversions so we convert any integer key to string.\n *\n * @param {*} key the provided key\n * @returns {string}\n */\n normalizeKey(key) {\n return String(key).valueOf();\n }\n\n /**\n * Insert a new element int a list.\n *\n * Each value needs it's own id attribute. Objects withouts id will be rejected.\n *\n * @param {object} value the value to add (needs an id attribute)\n * @returns {Map} the resulting Map object\n */\n add(value) {\n this.checkValue(value);\n return this.set(value.id, value);\n }\n\n /**\n * Return a state map element.\n *\n * @param {*} key the element id\n * @return {Object}\n */\n get(key) {\n return super.get(this.normalizeKey(key));\n }\n\n /**\n * Check whether an element with the specified key exists or not.\n *\n * @param {*} key the key to find\n * @return {boolean}\n */\n has(key) {\n return super.has(this.normalizeKey(key));\n }\n\n /**\n * Delete an element from the map.\n *\n * @param {*} key\n * @returns {boolean}\n */\n delete(key) {\n // State maps uses only string keys to avoid strict comparisons.\n key = this.normalizeKey(key);\n\n // Only mutations should be able to set state values.\n if (this.stateManager.readonly) {\n throw new Error(`State locked. Use mutations to change ${key} value in ${this.name}.`);\n }\n\n const previous = super.get(key);\n\n const result = super.delete(key);\n if (!result) {\n return result;\n }\n\n this.stateManager.registerStateAction(this.name, null, 'deleted', previous);\n\n return result;\n }\n\n /**\n * Return a suitable structure for JSON conversion.\n *\n * This function is needed because new values are compared in JSON. StateMap has Private\n * attributes which cannot be stringified (like this.stateManager which will produce an\n * infinite recursivity).\n *\n * @returns {array}\n */\n toJSON() {\n let result = [];\n this.forEach((value) => {\n result.push(value);\n });\n return result;\n }\n\n /**\n * Insert a full list of values using the id attributes as keys.\n *\n * This method is used mainly to initialize the list. Note each element is indexed by its \"id\" attribute.\n * This is a basic restriction of StateMap. All elements need an id attribute, otherwise it won't be saved.\n *\n * @param {iterable} values the values to load\n * @returns {StateMap} return the this value\n */\n loadValues(values) {\n values.forEach((data) => {\n this.checkValue(data);\n let key = data.id;\n let newvalue = new Proxy(data, new Handler(this.name, this.stateManager));\n this.set(key, newvalue);\n });\n return this;\n }\n}\n"],"names":["constructor","dispatchEvent","target","document","readonly","eventsToPublish","updateTypes","this","defaultCreate","bind","defaultUpdate","defaultDelete","defaultPut","defaultOverride","defaultRemove","defaultPrepareFields","initialPromise","Promise","resolve","addEventListener","event","detail","state","setInitialState","initialState","undefined","Error","Proxy","Handler","prop","propValue","Object","entries","action","getInitialPromise","setReadOnly","mode","_publishEvents","element","addUpdateTypes","newFunctions","updateType","updateFunction","processUpdates","updates","Array","isArray","forEach","update","name","processUpdate","fields","updateName","method","prepareFields","stateManager","StateMap","add","get","id","delete","current","fieldName","fieldValue","registerStateAction","field","data","parentAction","push","eventName","eventData","fieldChanges","changes","sort","a","b","weights","created","updated","deleted","aweight","bweight","length","publishedEvents","Set","eventkey","has","proxyValues","set","obj","value","receiver","JSON","stringify","loadValues","deleteProperty","Map","iterable","key","normalizeKey","checkValue","super","result","String","valueOf","previous","toJSON","values","newvalue"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"statemanager.min.js","sources":["../../../src/local/reactive/statemanager.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Reactive simple state manager.\n *\n * The state manager contains the state data, trigger update events and\n * can lock and unlock the state data.\n *\n * This file contains the three main elements of the state manager:\n * - State manager: the public class to alter the state, dispatch events and process update messages.\n * - Proxy handler: a private class to keep track of the state object changes.\n * - StateMap class: a private class extending Map class that triggers event when a state list is modifed.\n *\n * @module core/local/reactive/statemanager\n * @class StateManager\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * State manager class.\n *\n * This class handle the reactive state and ensure only valid mutations can modify the state.\n * It also provide methods to apply batch state update messages (see processUpdates function doc\n * for more details on update messages).\n *\n * Implementing a deep state manager is complex and will require many frontend resources. To keep\n * the state fast and simple, the state can ONLY store two kind of data:\n * - Object with attributes\n * - Sets of objects with id attributes.\n *\n * This is an example of a valid state:\n *\n * {\n * course: {\n * name: 'course name',\n * shortname: 'courseshort',\n * sectionlist: [21, 34]\n * },\n * sections: [\n * {id: 21, name: 'Topic 1', visible: true},\n * {id: 34, name: 'Topic 2', visible: false,\n * ],\n * }\n *\n * The following cases are NOT allowed at a state ROOT level (throws an exception if they are assigned):\n * - Simple values (strings, boolean...).\n * - Arrays of simple values.\n * - Array of objects without ID attribute (all arrays will be converted to maps and requires an ID).\n *\n * Thanks to those limitations it can simplify the state update messages and the event names. If You\n * need to store simple data, just group them in an object.\n *\n * To grant any state change triggers the proper events, the class uses two private structures:\n * - proxy handler: any object stored in the state is proxied using this class.\n * - StateMap class: any object set in the state will be converted to StateMap using the\n * objects id attribute.\n */\nexport default class StateManager {\n\n /**\n * Create a basic reactive state store.\n *\n * The state manager is meant to work with native JS events. To ensure each reactive module can use\n * it in its own way, the parent element must provide a valid event dispatcher function and an optional\n * DOM element to anchor the event.\n *\n * @param {function} dispatchEvent the function to dispatch the custom event when the state changes.\n * @param {element} target the state changed custom event target (document if none provided)\n */\n constructor(dispatchEvent, target) {\n\n // The dispatch event function.\n /** @package */\n this.dispatchEvent = dispatchEvent;\n\n // The DOM container to trigger events.\n /** @package */\n this.target = target ?? document;\n\n // State can be altered freely until initial state is set.\n /** @package */\n this.readonly = false;\n\n // List of state changes pending to be published as events.\n /** @package */\n this.eventsToPublish = [];\n\n // The update state types functions.\n /** @package */\n this.updateTypes = {\n \"create\": this.defaultCreate.bind(this),\n \"update\": this.defaultUpdate.bind(this),\n \"delete\": this.defaultDelete.bind(this),\n \"put\": this.defaultPut.bind(this),\n \"override\": this.defaultOverride.bind(this),\n \"remove\": this.defaultRemove.bind(this),\n \"prepareFields\": this.defaultPrepareFields.bind(this),\n };\n\n // The state_loaded event is special because it only happens one but all components\n // may react to that state, even if they are registered after the setIinitialState.\n // For these reason we use a promise for that event.\n this.initialPromise = new Promise((resolve) => {\n const initialStateDone = (event) => {\n resolve(event.detail.state);\n };\n this.target.addEventListener('state:loaded', initialStateDone);\n });\n }\n\n /**\n * Loads the initial state.\n *\n * Note this method will trigger a state changed event with \"state:loaded\" actionname.\n *\n * The state mode will be set to read only when the initial state is loaded.\n *\n * @param {object} initialState\n */\n setInitialState(initialState) {\n\n if (this.state !== undefined) {\n throw Error('Initial state can only be initialized ones');\n }\n\n // Create the state object.\n const state = new Proxy({}, new Handler('state', this, true));\n for (const [prop, propValue] of Object.entries(initialState)) {\n state[prop] = propValue;\n }\n this.state = state;\n\n // When the state is loaded we can lock it to prevent illegal changes.\n this.readonly = true;\n\n this.dispatchEvent({\n action: 'state:loaded',\n state: this.state,\n }, this.target);\n }\n\n /**\n * Generate a promise that will be resolved when the initial state is loaded.\n *\n * In most cases the final state will be loaded using an ajax call. This is the reason\n * why states manager are created unlocked and won't be reactive until the initial state is set.\n *\n * @return {Promise} the resulting promise\n */\n getInitialPromise() {\n return this.initialPromise;\n }\n\n /**\n * Locks or unlocks the state to prevent illegal updates.\n *\n * Mutations use this method to modify the state. Once the state is updated, they must\n * block again the state.\n *\n * All changes done while the state is writable will be registered using registerStateAction.\n * When the state is set again to read only the method will trigger _publishEvents to communicate\n * changes to all watchers.\n *\n * @param {bool} readonly if the state is in read only mode enabled\n */\n setReadOnly(readonly) {\n\n this.readonly = readonly;\n\n let mode = 'off';\n\n // When the state is in readonly again is time to publish all pending events.\n if (this.readonly) {\n mode = 'on';\n this._publishEvents();\n }\n\n // Dispatch a read only event.\n this.dispatchEvent({\n action: `readmode:${mode}`,\n state: this.state,\n element: null,\n }, this.target);\n }\n\n /**\n * Add methods to process update state messages.\n *\n * The state manager provide a default update, create and delete methods. However,\n * some applications may require to override the default methods or even add new ones\n * like \"refresh\" or \"error\".\n *\n * @param {Object} newFunctions the new update types functions.\n */\n addUpdateTypes(newFunctions) {\n for (const [updateType, updateFunction] of Object.entries(newFunctions)) {\n if (typeof updateFunction === 'function') {\n this.updateTypes[updateType] = updateFunction.bind(newFunctions);\n }\n }\n }\n\n /**\n * Process a state updates array and do all the necessary changes.\n *\n * Note this method unlocks the state while it is executing and relocks it\n * when finishes.\n *\n * @param {array} updates\n * @param {Object} updateTypes optional functions to override the default update types.\n */\n processUpdates(updates, updateTypes) {\n if (!Array.isArray(updates)) {\n throw Error('State updates must be an array');\n }\n this.setReadOnly(false);\n updates.forEach((update) => {\n if (update.name === undefined) {\n throw Error('Missing state update name');\n }\n this.processUpdate(\n update.name,\n update.action,\n update.fields,\n updateTypes\n );\n });\n this.setReadOnly(true);\n }\n\n /**\n * Process a single state update.\n *\n * Note this method will not lock or unlock the state by itself.\n *\n * @param {string} updateName the state element to update\n * @param {string} action to action to perform\n * @param {object} fields the new data\n * @param {Object} updateTypes optional functions to override the default update types.\n */\n processUpdate(updateName, action, fields, updateTypes) {\n\n if (!fields) {\n throw Error('Missing state update fields');\n }\n\n if (updateTypes === undefined) {\n updateTypes = {};\n }\n\n action = action ?? 'update';\n\n const method = updateTypes[action] ?? this.updateTypes[action];\n\n if (method === undefined) {\n throw Error(`Unkown update action ${action}`);\n }\n\n // Some state data may require some cooking before sending to the\n // state. Reactive instances can overrdide the default fieldDefaults\n // method to add extra logic to all updates.\n const prepareFields = updateTypes.prepareFields ?? this.updateTypes.prepareFields;\n\n method(this, updateName, prepareFields(this, updateName, fields));\n }\n\n /**\n * Prepare fields for processing.\n *\n * This method is used to add default values or calculations from the frontend side.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n * @returns {Object} final fields data\n */\n defaultPrepareFields(stateManager, updateName, fields) {\n return fields;\n }\n\n\n /**\n * Process a create state message.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n */\n defaultCreate(stateManager, updateName, fields) {\n\n let state = stateManager.state;\n\n // Create can be applied only to lists, not to objects.\n if (state[updateName] instanceof StateMap) {\n state[updateName].add(fields);\n return;\n }\n state[updateName] = fields;\n }\n\n /**\n * Process a delete state message.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n */\n defaultDelete(stateManager, updateName, fields) {\n\n // Get the current value.\n let current = stateManager.get(updateName, fields.id);\n if (!current) {\n throw Error(`Inexistent ${updateName} ${fields.id}`);\n }\n\n // Process deletion.\n let state = stateManager.state;\n\n if (state[updateName] instanceof StateMap) {\n state[updateName].delete(fields.id);\n return;\n }\n delete state[updateName];\n }\n\n /**\n * Process a remove state message.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n */\n defaultRemove(stateManager, updateName, fields) {\n\n // Get the current value.\n let current = stateManager.get(updateName, fields.id);\n if (!current) {\n return;\n }\n\n // Process deletion.\n let state = stateManager.state;\n\n if (state[updateName] instanceof StateMap) {\n state[updateName].delete(fields.id);\n return;\n }\n delete state[updateName];\n }\n\n /**\n * Process a update state message.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n */\n defaultUpdate(stateManager, updateName, fields) {\n\n // Get the current value.\n let current = stateManager.get(updateName, fields.id);\n if (!current) {\n throw Error(`Inexistent ${updateName} ${fields.id}`);\n }\n\n // Execute updates.\n for (const [fieldName, fieldValue] of Object.entries(fields)) {\n current[fieldName] = fieldValue;\n }\n }\n\n /**\n * Process a put state message.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n */\n defaultPut(stateManager, updateName, fields) {\n\n // Get the current value.\n let current = stateManager.get(updateName, fields.id);\n if (current) {\n // Update attributes.\n for (const [fieldName, fieldValue] of Object.entries(fields)) {\n current[fieldName] = fieldValue;\n }\n } else {\n // Create new object.\n let state = stateManager.state;\n if (state[updateName] instanceof StateMap) {\n state[updateName].add(fields);\n return;\n }\n state[updateName] = fields;\n }\n }\n\n /**\n * Process an override state message.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n */\n defaultOverride(stateManager, updateName, fields) {\n\n // Get the current value.\n let current = stateManager.get(updateName, fields.id);\n if (current) {\n // Remove any unnecessary fields.\n for (const [fieldName] of Object.entries(current)) {\n if (fields[fieldName] === undefined) {\n delete current[fieldName];\n }\n }\n // Update field.\n for (const [fieldName, fieldValue] of Object.entries(fields)) {\n current[fieldName] = fieldValue;\n }\n } else {\n // Create the element if not exists.\n let state = stateManager.state;\n if (state[updateName] instanceof StateMap) {\n state[updateName].add(fields);\n return;\n }\n state[updateName] = fields;\n }\n }\n\n /**\n * Get an element from the state or form an alternative state object.\n *\n * The altstate param is used by external update functions that gets the current\n * state as param.\n *\n * @param {String} name the state object name\n * @param {*} id and object id for state maps.\n * @return {Object|undefined} the state object found\n */\n get(name, id) {\n const state = this.state;\n\n let current = state[name];\n if (current instanceof StateMap) {\n if (id === undefined) {\n throw Error(`Missing id for ${name} state update`);\n }\n current = state[name].get(id);\n }\n\n return current;\n }\n\n /**\n * Get all element ids from the given state.\n *\n * @param {String} name the state object name\n * @return {Array} the element ids.\n */\n getIds(name) {\n const state = this.state;\n const current = state[name];\n if (!(current instanceof StateMap)) {\n throw Error(`${name} is not an instance of StateMap`);\n }\n return [...state[name].keys()];\n }\n\n /**\n * Register a state modification and generate the necessary events.\n *\n * This method is used mainly by proxy helpers to dispatch state change event.\n * However, mutations can use it to inform components about non reactive changes\n * in the state (only the two first levels of the state are reactive).\n *\n * Each action can produce several events:\n * - The specific attribute updated, created or deleter (example: \"cm.visible:updated\")\n * - The general state object updated, created or deleted (example: \"cm:updated\")\n * - If the element has an ID attribute, the specific event with id (example: \"cm[42].visible:updated\")\n * - If the element has an ID attribute, the general event with id (example: \"cm[42]:updated\")\n * - A generic state update event \"state:update\"\n *\n * @param {string} field the affected state field name\n * @param {string|null} prop the affecter field property (null if affect the full object)\n * @param {string} action the action done (created/updated/deleted)\n * @param {*} data the affected data\n */\n registerStateAction(field, prop, action, data) {\n\n let parentAction = 'updated';\n\n if (prop !== null) {\n this.eventsToPublish.push({\n eventName: `${field}.${prop}:${action}`,\n eventData: data,\n action,\n });\n } else {\n parentAction = action;\n }\n\n // Trigger extra events if the element has an ID attribute.\n if (data.id !== undefined) {\n if (prop !== null) {\n this.eventsToPublish.push({\n eventName: `${field}[${data.id}].${prop}:${action}`,\n eventData: data,\n action,\n });\n }\n this.eventsToPublish.push({\n eventName: `${field}[${data.id}]:${parentAction}`,\n eventData: data,\n action: parentAction,\n });\n }\n\n // Register the general change.\n this.eventsToPublish.push({\n eventName: `${field}:${parentAction}`,\n eventData: data,\n action: parentAction,\n });\n\n // Register state updated event.\n this.eventsToPublish.push({\n eventName: `state:updated`,\n eventData: data,\n action: 'updated',\n });\n }\n\n /**\n * Internal method to publish events.\n *\n * This is a private method, it will be invoked when the state is set back to read only mode.\n */\n _publishEvents() {\n const fieldChanges = this.eventsToPublish;\n this.eventsToPublish = [];\n\n // Dispatch a transaction start event.\n this.dispatchEvent({\n action: 'transaction:start',\n state: this.state,\n element: null,\n changes: fieldChanges,\n }, this.target);\n\n // State changes can be registered in any order. However it will avoid many\n // components errors if they are sorted to have creations-updates-deletes in case\n // some component needs to create or destroy DOM elements before updating them.\n fieldChanges.sort((a, b) => {\n const weights = {\n created: 0,\n updated: 1,\n deleted: 2,\n };\n const aweight = weights[a.action] ?? 0;\n const bweight = weights[b.action] ?? 0;\n // In case both have the same weight, the eventName length decide.\n if (aweight === bweight) {\n return a.eventName.length - b.eventName.length;\n }\n return aweight - bweight;\n });\n\n // List of the published events to prevent redundancies.\n let publishedEvents = new Set();\n\n fieldChanges.forEach((event) => {\n\n const eventkey = `${event.eventName}.${event.eventData.id ?? 0}`;\n\n if (!publishedEvents.has(eventkey)) {\n this.dispatchEvent({\n action: event.eventName,\n state: this.state,\n element: event.eventData\n }, this.target);\n\n publishedEvents.add(eventkey);\n }\n });\n\n // Dispatch a transaction end event.\n this.dispatchEvent({\n action: 'transaction:end',\n state: this.state,\n element: null,\n }, this.target);\n }\n}\n\n// Proxy helpers.\n\n/**\n * The proxy handler.\n *\n * This class will inform any value change directly to the state manager.\n *\n * The proxied variable will throw an error if it is altered when the state manager is\n * in read only mode.\n */\nclass Handler {\n\n /**\n * Class constructor.\n *\n * @param {string} name the variable name used for identify triggered actions\n * @param {StateManager} stateManager the state manager object\n * @param {boolean} proxyValues if new values must be proxied (used only at state root level)\n */\n constructor(name, stateManager, proxyValues) {\n this.name = name;\n this.stateManager = stateManager;\n this.proxyValues = proxyValues ?? false;\n }\n\n /**\n * Set trap to trigger events when the state changes.\n *\n * @param {object} obj the source object (not proxied)\n * @param {string} prop the attribute to set\n * @param {*} value the value to save\n * @param {*} receiver the proxied element to be attached to events\n * @returns {boolean} if the value is set\n */\n set(obj, prop, value, receiver) {\n\n // Only mutations should be able to set state values.\n if (this.stateManager.readonly) {\n throw new Error(`State locked. Use mutations to change ${prop} value in ${this.name}.`);\n }\n\n // Check any data change.\n if (JSON.stringify(obj[prop]) === JSON.stringify(value)) {\n return true;\n }\n\n const action = (obj[prop] !== undefined) ? 'updated' : 'created';\n\n // Proxy value if necessary (used at state root level).\n if (this.proxyValues) {\n if (Array.isArray(value)) {\n obj[prop] = new StateMap(prop, this.stateManager).loadValues(value);\n } else {\n obj[prop] = new Proxy(value, new Handler(prop, this.stateManager));\n }\n } else {\n obj[prop] = value;\n }\n\n // If the state is not ready yet means the initial state is not yet loaded.\n if (this.stateManager.state === undefined) {\n return true;\n }\n\n this.stateManager.registerStateAction(this.name, prop, action, receiver);\n\n return true;\n }\n\n /**\n * Delete property trap to trigger state change events.\n *\n * @param {*} obj the affected object (not proxied)\n * @param {*} prop the prop to delete\n * @returns {boolean} if prop is deleted\n */\n deleteProperty(obj, prop) {\n // Only mutations should be able to set state values.\n if (this.stateManager.readonly) {\n throw new Error(`State locked. Use mutations to delete ${prop} in ${this.name}.`);\n }\n if (prop in obj) {\n\n delete obj[prop];\n\n this.stateManager.registerStateAction(this.name, prop, 'deleted', obj);\n }\n return true;\n }\n}\n\n/**\n * Class to add events dispatching to the JS Map class.\n *\n * When the state has a list of objects (with IDs) it will be converted into a StateMap.\n * StateMap is used almost in the same way as a regular JS map. Because all elements have an\n * id attribute, it has some specific methods:\n * - add: a convenient method to add an element without specifying the key (\"id\" attribute will be used as a key).\n * - loadValues: to add many elements at once wihout specifying keys (\"id\" attribute will be used).\n *\n * Apart, the main difference between regular Map and MapState is that this one will inform any change to the\n * state manager.\n */\nclass StateMap extends Map {\n\n /**\n * Create a reactive Map.\n *\n * @param {string} name the property name\n * @param {StateManager} stateManager the state manager\n * @param {iterable} iterable an iterable object to create the Map\n */\n constructor(name, stateManager, iterable) {\n // We don't have any \"this\" until be call super.\n super(iterable);\n this.name = name;\n this.stateManager = stateManager;\n }\n\n /**\n * Set an element into the map.\n *\n * Each value needs it's own id attribute. Objects without id will be rejected.\n * The function will throw an error if the value id and the key are not the same.\n *\n * @param {*} key the key to store\n * @param {*} value the value to store\n * @returns {Map} the resulting Map object\n */\n set(key, value) {\n\n // Only mutations should be able to set state values.\n if (this.stateManager.readonly) {\n throw new Error(`State locked. Use mutations to change ${key} value in ${this.name}.`);\n }\n\n // Normalize keys as string to prevent json decoding errors.\n key = this.normalizeKey(key);\n\n this.checkValue(value);\n\n if (key === undefined || key === null) {\n throw Error('State lists keys cannot be null or undefined');\n }\n\n // ID is mandatory and should be the same as the key.\n if (this.normalizeKey(value.id) !== key) {\n throw new Error(`State error: ${this.name} list element ID (${value.id}) and key (${key}) mismatch`);\n }\n\n const action = (super.has(key)) ? 'updated' : 'created';\n\n // Save proxied data into the list.\n const result = super.set(key, new Proxy(value, new Handler(this.name, this.stateManager)));\n\n // If the state is not ready yet means the initial state is not yet loaded.\n if (this.stateManager.state === undefined) {\n return result;\n }\n\n this.stateManager.registerStateAction(this.name, null, action, super.get(key));\n\n return result;\n }\n\n /**\n * Check if a value is valid to be stored in a a State List.\n *\n * Only objects with id attribute can be stored in State lists.\n *\n * This method throws an error if the value is not valid.\n *\n * @param {object} value (with ID)\n */\n checkValue(value) {\n if (!typeof value === 'object' && value !== null) {\n throw Error('State lists can contain objects only');\n }\n\n if (value.id === undefined) {\n throw Error('State lists elements must contain at least an id attribute');\n }\n }\n\n /**\n * Return a normalized key value for state map.\n *\n * Regular maps uses strict key comparissons but state maps are indexed by ID.JSON conversions\n * and webservices sometimes do unexpected types conversions so we convert any integer key to string.\n *\n * @param {*} key the provided key\n * @returns {string}\n */\n normalizeKey(key) {\n return String(key).valueOf();\n }\n\n /**\n * Insert a new element int a list.\n *\n * Each value needs it's own id attribute. Objects withouts id will be rejected.\n *\n * @param {object} value the value to add (needs an id attribute)\n * @returns {Map} the resulting Map object\n */\n add(value) {\n this.checkValue(value);\n return this.set(value.id, value);\n }\n\n /**\n * Return a state map element.\n *\n * @param {*} key the element id\n * @return {Object}\n */\n get(key) {\n return super.get(this.normalizeKey(key));\n }\n\n /**\n * Check whether an element with the specified key exists or not.\n *\n * @param {*} key the key to find\n * @return {boolean}\n */\n has(key) {\n return super.has(this.normalizeKey(key));\n }\n\n /**\n * Delete an element from the map.\n *\n * @param {*} key\n * @returns {boolean}\n */\n delete(key) {\n // State maps uses only string keys to avoid strict comparisons.\n key = this.normalizeKey(key);\n\n // Only mutations should be able to set state values.\n if (this.stateManager.readonly) {\n throw new Error(`State locked. Use mutations to change ${key} value in ${this.name}.`);\n }\n\n const previous = super.get(key);\n\n const result = super.delete(key);\n if (!result) {\n return result;\n }\n\n this.stateManager.registerStateAction(this.name, null, 'deleted', previous);\n\n return result;\n }\n\n /**\n * Return a suitable structure for JSON conversion.\n *\n * This function is needed because new values are compared in JSON. StateMap has Private\n * attributes which cannot be stringified (like this.stateManager which will produce an\n * infinite recursivity).\n *\n * @returns {array}\n */\n toJSON() {\n let result = [];\n this.forEach((value) => {\n result.push(value);\n });\n return result;\n }\n\n /**\n * Insert a full list of values using the id attributes as keys.\n *\n * This method is used mainly to initialize the list. Note each element is indexed by its \"id\" attribute.\n * This is a basic restriction of StateMap. All elements need an id attribute, otherwise it won't be saved.\n *\n * @param {iterable} values the values to load\n * @returns {StateMap} return the this value\n */\n loadValues(values) {\n values.forEach((data) => {\n this.checkValue(data);\n let key = data.id;\n let newvalue = new Proxy(data, new Handler(this.name, this.stateManager));\n this.set(key, newvalue);\n });\n return this;\n }\n}\n"],"names":["constructor","dispatchEvent","target","document","readonly","eventsToPublish","updateTypes","this","defaultCreate","bind","defaultUpdate","defaultDelete","defaultPut","defaultOverride","defaultRemove","defaultPrepareFields","initialPromise","Promise","resolve","addEventListener","event","detail","state","setInitialState","initialState","undefined","Error","Proxy","Handler","prop","propValue","Object","entries","action","getInitialPromise","setReadOnly","mode","_publishEvents","element","addUpdateTypes","newFunctions","updateType","updateFunction","processUpdates","updates","Array","isArray","forEach","update","name","processUpdate","fields","updateName","method","prepareFields","stateManager","StateMap","add","get","id","delete","current","fieldName","fieldValue","getIds","keys","registerStateAction","field","data","parentAction","push","eventName","eventData","fieldChanges","changes","sort","a","b","weights","created","updated","deleted","aweight","bweight","length","publishedEvents","Set","eventkey","has","proxyValues","set","obj","value","receiver","JSON","stringify","loadValues","deleteProperty","Map","iterable","key","normalizeKey","checkValue","super","result","String","valueOf","previous","toJSON","values","newvalue"],"mappings":"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"} \ No newline at end of file diff --git a/lib/amd/src/local/reactive/statemanager.js b/lib/amd/src/local/reactive/statemanager.js index a8bcbae2962..99266b44afa 100644 --- a/lib/amd/src/local/reactive/statemanager.js +++ b/lib/amd/src/local/reactive/statemanager.js @@ -467,6 +467,21 @@ export default class StateManager { return current; } + /** + * Get all element ids from the given state. + * + * @param {String} name the state object name + * @return {Array} the element ids. + */ + getIds(name) { + const state = this.state; + const current = state[name]; + if (!(current instanceof StateMap)) { + throw Error(`${name} is not an instance of StateMap`); + } + return [...state[name].keys()]; + } + /** * Register a state modification and generate the necessary events. * diff --git a/mod/assign/tests/behat/hide_grader.feature b/mod/assign/tests/behat/hide_grader.feature index 13cbfb4aa88..02c0db80cfa 100644 --- a/mod/assign/tests/behat/hide_grader.feature +++ b/mod/assign/tests/behat/hide_grader.feature @@ -59,7 +59,7 @@ Feature: Hide grader identities identity from students # Enable the hidden grader option Given I am on the "Test assignment name" Activity page logged in as teacher1 And I navigate to "Settings" in current page administration - And I follow "Expand all" + And I click on "Expand all" "link" in the "region-main" "region" And I set the field "Hide grader identity from students" to "1" And I press "Save and return to course" And I log out diff --git a/mod/assign/tests/behat/remove_submission.feature b/mod/assign/tests/behat/remove_submission.feature index d3fcf4b0cb5..f2749aa77ba 100644 --- a/mod/assign/tests/behat/remove_submission.feature +++ b/mod/assign/tests/behat/remove_submission.feature @@ -132,7 +132,7 @@ Feature: Remove a submission | gradingduedate_enabled | 0 | And I am on the "Test assignment with time limit" Activity page logged in as admin And I navigate to "Settings" in current page administration - And I follow "Expand all" + And I click on "Expand all" "link" in the "region-main" "region" # Set 'Time limit' to 5 seconds. And I set the field "timelimit[enabled]" to "1" And I set the field "timelimit[number]" to "5" diff --git a/mod/assign/tests/behat/set_availability.feature b/mod/assign/tests/behat/set_availability.feature index c667ef275af..aff4da8a1e3 100644 --- a/mod/assign/tests/behat/set_availability.feature +++ b/mod/assign/tests/behat/set_availability.feature @@ -47,7 +47,7 @@ Feature: Set availability dates for an assignment Scenario: Student cannot submit an assignment prior to the 'allow submissions from' date Given I am on the "Assignment name" Activity page logged in as teacher1 And I navigate to "Settings" in current page administration - And I follow "Expand all" + And I click on "Expand all" "link" in the "region-main" "region" # Set 'Allow submissions from' to tomorrow at noon. And I set the field "Allow submissions from" to "##tomorrow noon##" And I press "Save and return to course" @@ -79,7 +79,7 @@ Feature: Set availability dates for an assignment Scenario: Student can submit an assignment before the due date Given I am on the "Assignment name" Activity page logged in as teacher1 And I navigate to "Settings" in current page administration - And I follow "Expand all" + And I click on "Expand all" "link" in the "region-main" "region" # Set 'Allow submissions from' to now. And I set the field "Allow submissions from" to "##now##" # Set 'Due date' to 2 days 5 hours 30 minutes in the future. @@ -107,7 +107,7 @@ Feature: Set availability dates for an assignment Scenario: Student can submit an assignment after the due date and the submission is marked as late Given I am on the "Assignment name" Activity page logged in as teacher1 And I navigate to "Settings" in current page administration - And I follow "Expand all" + And I click on "Expand all" "link" in the "region-main" "region" # Set 'Allow submissions from' to 3 days ago. And I set the field "Allow submissions from" to "##3 days ago##" # Set 'Due date' to 2 days 5 hours 30 minutes ago. @@ -144,7 +144,7 @@ Feature: Set availability dates for an assignment And I log out And I am on the "Assignment name" Activity page logged in as teacher1 And I navigate to "Settings" in current page administration - And I follow "Expand all" + And I click on "Expand all" "link" in the "region-main" "region" # Set 'Time limit' to 20 seconds. And I set the field "timelimit[enabled]" to "1" And I set the field "timelimit[number]" to "20" @@ -173,7 +173,7 @@ Feature: Set availability dates for an assignment And I log out And I am on the "Assignment name" Activity page logged in as teacher1 And I navigate to "Settings" in current page administration - And I follow "Expand all" + And I click on "Expand all" "link" in the "region-main" "region" # Set 'Time limit' to 5 seconds. And I set the field "timelimit[enabled]" to "1" And I set the field "timelimit[number]" to "5" @@ -198,7 +198,7 @@ Feature: Set availability dates for an assignment Scenario: Student cannot submit an assignment after the cut-off date Given I am on the "Assignment name" Activity page logged in as teacher1 And I navigate to "Settings" in current page administration - And I follow "Expand all" + And I click on "Expand all" "link" in the "region-main" "region" # Set 'Allow submissions from' to 3 days ago. And I set the field "Allow submissions from" to "##3 days ago##" # Set 'Due date' to 2 days 5 hours 30 minutes ago. @@ -222,7 +222,7 @@ Feature: Set availability dates for an assignment Scenario: Late submission will be calculated only when the student starts the assignment Given I am on the "Test late assignment with time limit" Activity page logged in as admin And I navigate to "Settings" in current page administration - And I follow "Expand all" + And I click on "Expand all" "link" in the "region-main" "region" # Set 'Time limit' to 5 seconds. And I set the field "timelimit[enabled]" to "1" And I set the field "timelimit[number]" to "5" diff --git a/mod/bigbluebuttonbn/tests/behat/guest_access.feature b/mod/bigbluebuttonbn/tests/behat/guest_access.feature index e6ae8c10752..d27adc9874f 100644 --- a/mod/bigbluebuttonbn/tests/behat/guest_access.feature +++ b/mod/bigbluebuttonbn/tests/behat/guest_access.feature @@ -30,7 +30,7 @@ Feature: Guest access allows external users to connect to a meeting | bigbluebuttonbn_guestaccess_enabled | 1 | When I am on the "RoomRecordings" "bigbluebuttonbn activity editing" page logged in as "admin" Then I should see "Guest access" - Then I click on "Expand all" "link" + And I click on "Expand all" "link" in the "region-main" "region" Then I should see "Allow guest access" And I should not see "Meeting link" And I should not see "Meeting password" diff --git a/mod/workshop/tests/behat/workshop_late_submission.feature b/mod/workshop/tests/behat/workshop_late_submission.feature index f73086cc1ef..6cfa7400c22 100644 --- a/mod/workshop/tests/behat/workshop_late_submission.feature +++ b/mod/workshop/tests/behat/workshop_late_submission.feature @@ -43,7 +43,7 @@ Feature: Workshop 'Late submissions are allowed' task When I am on the "TestWorkshop1" "workshop activity" page logged in as teacher1 Then I should see "Late submissions are allowed" And I navigate to "Settings" in current page administration - And I follow "Expand all" + And I click on "Expand all" "link" in the "region-main" "region" And I set the field "id_submissionend_day" to "1" And I set the field "id_submissionend_month" to "January" And I set the field "id_submissionend_year" to "2018" diff --git a/question/bank/customfields/tests/behat/customfield_question.feature b/question/bank/customfields/tests/behat/customfield_question.feature index 660f3f70ca7..969f70085d4 100644 --- a/question/bank/customfields/tests/behat/customfield_question.feature +++ b/question/bank/customfields/tests/behat/customfield_question.feature @@ -42,7 +42,7 @@ Feature: A teacher can edit question with custom fields Scenario: Edit a previously created question and see the custom field in the overview table and in the question preview. When I choose "Edit question" action for "First question" in the question bank And I should see "Category for test" - And I click on "Expand all" "link" + And I click on "Expand all" "link" in the "region-main" "region" And I should see "Field 1" And I set the following fields to these values: | Field 1 | custom field text | diff --git a/question/bank/customfields/tests/behat/customfield_question_visbility.feature b/question/bank/customfields/tests/behat/customfield_question_visbility.feature index def18fe2a2c..8bcdab19b9a 100644 --- a/question/bank/customfields/tests/behat/customfield_question_visbility.feature +++ b/question/bank/customfields/tests/behat/customfield_question_visbility.feature @@ -43,7 +43,7 @@ Feature: The visibility of question custom fields control where they are display Scenario: Display custom question fields to teachers based on their visibility. When I choose "Edit question" action for "First question" in the question bank And I should see "Category for test" - And I click on "Expand all" "link" + And I click on "Expand all" "link" in the "region-main" "region" And I should see "Field 1" And I should see "Field 2" And I should see "Field 3" diff --git a/report/competency/tests/behat/breakdown_by_activity.feature b/report/competency/tests/behat/breakdown_by_activity.feature index d61e0749bf6..fb7746f2b5d 100644 --- a/report/competency/tests/behat/breakdown_by_activity.feature +++ b/report/competency/tests/behat/breakdown_by_activity.feature @@ -42,7 +42,7 @@ Feature: See the competencies for an activity And I select "Test-Comp2" of the competency tree And I click on "Add" "button" in the "Competency picker" "dialogue" And I am on the PageName1 "page activity editing" page - And I follow "Expand all" + And I click on "Expand all" "link" in the "region-main" "region" And I set the field "Course competencies" to "Test-Comp1" And I press "Save and return to course" diff --git a/theme/boost/amd/build/courseindexdrawercontrols.min.js b/theme/boost/amd/build/courseindexdrawercontrols.min.js new file mode 100644 index 00000000000..9f6c0a32cc4 --- /dev/null +++ b/theme/boost/amd/build/courseindexdrawercontrols.min.js @@ -0,0 +1,11 @@ +define("theme_boost/courseindexdrawercontrols",["exports","core/reactive","core_courseformat/courseeditor"],(function(_exports,_reactive,_courseeditor){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0; +/** + * Controls for the course index drawer, such as expand-all/collapse-all sections. + * + * @module theme_boost/courseindexdrawercontrols + * @copyright 2023 Stefan Topfstedt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class Component extends _reactive.BaseComponent{create(){this.name="courseindexdrawercontrols",this.selectors={COLLAPSEALL:'[data-action="collapseallcourseindexsections"]',EXPANDALL:'[data-action="expandallcourseindexsections"]'}}static init(target,selectors){return new Component({element:document.getElementById(target),reactive:(0,_courseeditor.getCurrentCourseEditor)(),selectors:selectors})}stateReady(){const expandAllBtn=this.getElement(this.selectors.EXPANDALL);expandAllBtn&&this.addEventListener(expandAllBtn,"click",this._expandAllSections);const collapseAllBtn=this.getElement(this.selectors.COLLAPSEALL);collapseAllBtn&&this.addEventListener(collapseAllBtn,"click",this._collapseAllSections)}_collapseAllSections(){this._toggleAllSections(!0)}_expandAllSections(){this._toggleAllSections(!1)}_toggleAllSections(expandOrCollapse){this.reactive.dispatch("allSectionsIndexCollapsed",expandOrCollapse)}}return _exports.default=Component,_exports.default})); + +//# sourceMappingURL=courseindexdrawercontrols.min.js.map \ No newline at end of file diff --git a/theme/boost/amd/build/courseindexdrawercontrols.min.js.map b/theme/boost/amd/build/courseindexdrawercontrols.min.js.map new file mode 100644 index 00000000000..379a0e47662 --- /dev/null +++ b/theme/boost/amd/build/courseindexdrawercontrols.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"courseindexdrawercontrols.min.js","sources":["../src/courseindexdrawercontrols.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Controls for the course index drawer, such as expand-all/collapse-all sections.\n *\n * @module theme_boost/courseindexdrawercontrols\n * @copyright 2023 Stefan Topfstedt\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport {BaseComponent} from 'core/reactive';\nimport {getCurrentCourseEditor} from 'core_courseformat/courseeditor';\n\nexport default class Component extends BaseComponent {\n\n create() {\n this.name = 'courseindexdrawercontrols';\n this.selectors = {\n COLLAPSEALL: `[data-action=\"collapseallcourseindexsections\"]`,\n EXPANDALL: `[data-action=\"expandallcourseindexsections\"]`,\n };\n }\n\n /**\n * @param {element|string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new Component({\n element: document.getElementById(target),\n reactive: getCurrentCourseEditor(),\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n */\n stateReady() {\n // Attach the on-click event handlers to the expand-all and collapse-all buttons, if present.\n const expandAllBtn = this.getElement(this.selectors.EXPANDALL);\n if (expandAllBtn) {\n this.addEventListener(expandAllBtn, 'click', this._expandAllSections);\n\n }\n const collapseAllBtn = this.getElement(this.selectors.COLLAPSEALL);\n if (collapseAllBtn) {\n this.addEventListener(collapseAllBtn, 'click', this._collapseAllSections);\n }\n }\n\n /**\n * On-click event handler for the collapse-all button.\n * @private\n */\n _collapseAllSections() {\n this._toggleAllSections(true);\n }\n\n /**\n * On-click event handler for the expand-all button.\n * @private\n */\n _expandAllSections() {\n this._toggleAllSections(false);\n }\n\n /**\n * Collapses or expands all sections in the course index.\n * @param {boolean} expandOrCollapse set to TRUE to collapse all, and FALSE to expand all.\n * @private\n */\n _toggleAllSections(expandOrCollapse) {\n this.reactive.dispatch('allSectionsIndexCollapsed', expandOrCollapse);\n }\n}\n"],"names":["Component","BaseComponent","create","name","selectors","COLLAPSEALL","EXPANDALL","target","element","document","getElementById","reactive","stateReady","expandAllBtn","this","getElement","addEventListener","_expandAllSections","collapseAllBtn","_collapseAllSections","_toggleAllSections","expandOrCollapse","dispatch"],"mappings":";;;;;;;;MAyBqBA,kBAAkBC,wBAErCC,cACOC,KAAO,iCACPC,UAAY,CACfC,6DACAC,sEASQC,OAAQH,kBACX,IAAIJ,UAAU,CACnBQ,QAASC,SAASC,eAAeH,QACjCI,UAAU,0CACVP,UAAAA,YAOJQ,mBAEQC,aAAeC,KAAKC,WAAWD,KAAKV,UAAUE,WAChDO,mBACGG,iBAAiBH,aAAc,QAASC,KAAKG,0BAG9CC,eAAiBJ,KAAKC,WAAWD,KAAKV,UAAUC,aAClDa,qBACGF,iBAAiBE,eAAgB,QAASJ,KAAKK,sBAQxDA,4BACOC,oBAAmB,GAO1BH,0BACOG,oBAAmB,GAQ1BA,mBAAmBC,uBACZV,SAASW,SAAS,4BAA6BD"} \ No newline at end of file diff --git a/theme/boost/amd/build/drawers.min.js b/theme/boost/amd/build/drawers.min.js index 2bdb9344b33..abc7da4b594 100644 --- a/theme/boost/amd/build/drawers.min.js +++ b/theme/boost/amd/build/drawers.min.js @@ -1,3 +1,3 @@ -define("theme_boost/drawers",["exports","core/modal_backdrop","core/templates","core/aria","core/event_dispatcher","core/utils","core/pagehelpers","core/pending","jquery"],(function(_exports,_modal_backdrop,_templates,Aria,_event_dispatcher,_utils,_pagehelpers,_pending,_jquery){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal_backdrop=_interopRequireDefault(_modal_backdrop),_templates=_interopRequireDefault(_templates),Aria=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Aria),_pending=_interopRequireDefault(_pending),_jquery=_interopRequireDefault(_jquery);let backdropPromise=null;const drawerMap=new Map,SELECTORS_BUTTONS='[data-toggler="drawers"]',SELECTORS_CLOSEBTN='[data-toggler="drawers"][data-action="closedrawer"]',SELECTORS_OPENBTN='[data-toggler="drawers"][data-action="opendrawer"]',SELECTORS_TOGGLEBTN='[data-toggler="drawers"][data-action="toggle"]',SELECTORS_DRAWERS='[data-region="fixed-drawer"]',SELECTORS_DRAWERCONTENT=".drawercontent",SELECTORS_PAGECONTENT="#page-content",CLASSES_SCROLLED="scrolled",CLASSES_SHOW="show",CLASSES_NOTINITIALISED="not-initialized",getDrawerZIndex=()=>{const drawer=document.querySelector(SELECTORS_DRAWERS);return drawer?parseInt(window.getComputedStyle(drawer).zIndex,10):null},getBackdrop=()=>(backdropPromise||(backdropPromise=_templates.default.render("core/modal_backdrop",{}).then((html=>new _modal_backdrop.default(html))).then((modalBackdrop=>(getDrawerZIndex()&&modalBackdrop.setZIndex(getDrawerZIndex()-1),modalBackdrop.getAttachmentPoint().get(0).addEventListener("click",(e=>{e.preventDefault(),Drawers.closeAllDrawers()})),modalBackdrop))).catch()),backdropPromise),getDrawerOpenButton=drawerId=>{let openButton=document.querySelector("".concat(SELECTORS_OPENBTN,'[data-target="').concat(drawerId,'"]'));return openButton||(openButton=document.querySelector("".concat(SELECTORS_TOGGLEBTN,'[data-target="').concat(drawerId,'"]'))),openButton},disableDrawerTooltips=drawerNode=>{[drawerNode.querySelector(SELECTORS_CLOSEBTN),getDrawerOpenButton(drawerNode.id)].forEach((button=>{button&&disableButtonTooltip(button)}))},disableButtonTooltip=(button,enableOnBlur)=>{button.hasAttribute("data-original-title")?((0,_jquery.default)(button).tooltip("disable"),button.setAttribute("title",button.dataset.originalTitle)):(button.dataset.disabledToggle=button.dataset.toggle,button.removeAttribute("data-toggle")),enableOnBlur&&(button.dataset.restoreTooltipOnBlur=!0)},enableButtonTooltip=button=>{button.hasAttribute("data-original-title")?((0,_jquery.default)(button).tooltip("enable"),button.removeAttribute("title")):button.dataset.disabledToggle&&(button.dataset.toggle=button.dataset.disabledToggle,(0,_jquery.default)(button).tooltip()),delete button.dataset.restoreTooltipOnBlur};class Drawers{constructor(drawerNode){_defineProperty(this,"drawerNode",null),_defineProperty(this,"boundingRect",null),void 0===drawerNode.dataset.behatFakeDrawer&&(this.drawerNode=drawerNode,(0,_pagehelpers.isSmall)()&&this.closeDrawer({focusOnOpenButton:!1,updatePreferences:!1}),this.drawerNode.classList.contains(CLASSES_SHOW)?this.openDrawer({focusOnCloseButton:!1}):1==this.drawerNode.dataset.forceopen?(0,_pagehelpers.isSmall)()||this.openDrawer({focusOnCloseButton:!1}):Aria.hide(this.drawerNode),(0,_pagehelpers.isSmall)()&&disableDrawerTooltips(this.drawerNode),(drawerNode=>{const content=drawerNode.querySelector(SELECTORS_DRAWERCONTENT);content&&content.addEventListener("scroll",(()=>{drawerNode.classList.toggle(CLASSES_SCROLLED,0!=content.scrollTop)}))})(this.drawerNode),drawerMap.set(drawerNode,this),drawerNode.classList.remove(CLASSES_NOTINITIALISED))}get isOpen(){return this.drawerNode.classList.contains(CLASSES_SHOW)}get closeOnResize(){return!!parseInt(this.drawerNode.dataset.closeOnResize)}static getDrawerInstanceForNode(drawerNode){return drawerMap.has(drawerNode)||new Drawers(drawerNode),drawerMap.get(drawerNode)}dispatchEvent(eventname){let cancelable=arguments.length>1&&void 0!==arguments[1]&&arguments[1];return(0,_event_dispatcher.dispatchEvent)(eventname,{drawerInstance:this},this.drawerNode,{cancelable:cancelable})}openDrawer(){var _this$drawerNode$quer;let{focusOnCloseButton:focusOnCloseButton=!0}=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};const pendingPromise=new _pending.default("theme_boost/drawers:open");if(this.dispatchEvent(Drawers.eventTypes.drawerShow,!0).defaultPrevented)return;null===(_this$drawerNode$quer=this.drawerNode.querySelector(SELECTORS_CLOSEBTN))||void 0===_this$drawerNode$quer||_this$drawerNode$quer.classList.toggle("hidden",!0);let openButton=getDrawerOpenButton(this.drawerNode.id);var _jQuery;openButton&&openButton.hasAttribute("data-original-title")&&(null===(_jQuery=(0,_jquery.default)(openButton))||void 0===_jQuery||_jQuery.tooltip("hide"));Aria.unhide(this.drawerNode),this.drawerNode.classList.add(CLASSES_SHOW);const preference=this.drawerNode.dataset.preference;preference&&!(0,_pagehelpers.isSmall)()&&1!=this.drawerNode.dataset.forceopen&&M.util.set_user_preference(preference,!0);const state=this.drawerNode.dataset.state;if(state){document.getElementById("page").classList.add(state)}this.boundingRect=this.drawerNode.getBoundingClientRect(),(0,_pagehelpers.isSmall)()&&getBackdrop().then((backdrop=>{backdrop.show();return document.getElementById("page").style.overflow="hidden",backdrop})).catch();const closeButton=this.drawerNode.querySelector(SELECTORS_CLOSEBTN);focusOnCloseButton&&closeButton&&disableButtonTooltip(closeButton,!0),setTimeout((()=>{closeButton.classList.toggle("hidden",!1),focusOnCloseButton&&closeButton.focus(),pendingPromise.resolve()}),300),this.dispatchEvent(Drawers.eventTypes.drawerShown)}closeDrawer(){let{focusOnOpenButton:focusOnOpenButton=!0,updatePreferences:updatePreferences=!0}=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};const pendingPromise=new _pending.default("theme_boost/drawers:close");if(this.dispatchEvent(Drawers.eventTypes.drawerHide,!0).defaultPrevented)return;const closeButton=this.drawerNode.querySelector(SELECTORS_CLOSEBTN);var _jQuery2;(null==closeButton||closeButton.classList.toggle("hidden",!0),closeButton.hasAttribute("data-original-title"))&&(null===(_jQuery2=(0,_jquery.default)(closeButton))||void 0===_jQuery2||_jQuery2.tooltip("hide"));const preference=this.drawerNode.dataset.preference;preference&&updatePreferences&&!(0,_pagehelpers.isSmall)()&&M.util.set_user_preference(preference,!1);const state=this.drawerNode.dataset.state;if(state){document.getElementById("page").classList.remove(state)}Aria.hide(this.drawerNode),this.drawerNode.classList.remove(CLASSES_SHOW),getBackdrop().then((backdrop=>{if(backdrop.hide(),(0,_pagehelpers.isSmall)()){document.getElementById("page").style.overflow="visible"}return backdrop})).catch();let openButton=getDrawerOpenButton(this.drawerNode.id);openButton&&disableButtonTooltip(openButton,!0),setTimeout((()=>{openButton&&focusOnOpenButton&&openButton.focus(),pendingPromise.resolve()}),300),this.dispatchEvent(Drawers.eventTypes.drawerHidden)}toggleVisibility(){this.drawerNode.classList.contains(CLASSES_SHOW)?this.closeDrawer():this.openDrawer()}displace(scrollPosition){var _this$drawerNode$data;let displace=scrollPosition,openButton=getDrawerOpenButton(this.drawerNode.id);if(0===scrollPosition)return this.drawerNode.style.transform="",void(openButton&&(openButton.style.transform=""));const state=null===(_this$drawerNode$data=this.drawerNode.dataset)||void 0===_this$drawerNode$data?void 0:_this$drawerNode$data.state,drawrWidth=this.drawerNode.offsetWidth;let scrollThreshold=drawrWidth,direction=-1;"show-drawer-right"===state&&(direction=1,scrollThreshold=20),scrollPosition>scrollThreshold&&(displace=drawrWidth+20),displace*=direction;const transform="translateX(".concat(displace,"px)");openButton&&(openButton.style.transform=transform),this.drawerNode.style.transform=transform}preventOverlap(currentFocus){if(!this.isOpen)return;const drawrWidth=this.drawerNode.offsetWidth,element=currentFocus.getBoundingClientRect(),drawer=this.boundingRect;element.right+20>drawer.left&&element.left-20{drawerInstance.closeDrawer()}))}static closeOtherDrawers(comparisonInstance){drawerMap.forEach((drawerInstance=>{drawerInstance!==comparisonInstance&&drawerInstance.closeDrawer()}))}static preventCoveringFocusedElement(){const currentFocus=document.activeElement,pagecontent=document.querySelector(SELECTORS_PAGECONTENT);currentFocus&&null!=pagecontent&&pagecontent.contains(currentFocus)?drawerMap.forEach((drawerInstance=>{drawerInstance.preventOverlap(currentFocus)})):Drawers.displaceDrawers(window.scrollX)}static displaceDrawers(displace){drawerMap.forEach((drawerInstance=>{drawerInstance.displace(displace)}))}}_exports.default=Drawers,_defineProperty(Drawers,"eventTypes",{drawerShow:"theme_boost/drawers:show",drawerShown:"theme_boost/drawers:shown",drawerHide:"theme_boost/drawers:hide",drawerHidden:"theme_boost/drawers:hidden"});const setLastUsedToggle=toggleButton=>{toggleButton.dataset.target&&(document.querySelectorAll("".concat(SELECTORS_BUTTONS,'[data-target="').concat(toggleButton.dataset.target,'"]')).forEach((btn=>{btn.dataset.lastused=!1})),toggleButton.dataset.lastused=!0)};(()=>{document.addEventListener("click",(e=>{const toggleButton=e.target.closest(SELECTORS_TOGGLEBTN);if(toggleButton&&toggleButton.dataset.target){e.preventDefault();const targetDrawer=document.getElementById(toggleButton.dataset.target),drawerInstance=Drawers.getDrawerInstanceForNode(targetDrawer);setLastUsedToggle(toggleButton),drawerInstance.toggleVisibility()}const openDrawerButton=e.target.closest(SELECTORS_OPENBTN);if(openDrawerButton&&openDrawerButton.dataset.target){e.preventDefault();const targetDrawer=document.getElementById(openDrawerButton.dataset.target),drawerInstance=Drawers.getDrawerInstanceForNode(targetDrawer);setLastUsedToggle(toggleButton),drawerInstance.openDrawer()}const closeDrawerButton=e.target.closest(SELECTORS_CLOSEBTN);if(closeDrawerButton&&closeDrawerButton.dataset.target){e.preventDefault();const targetDrawer=document.getElementById(closeDrawerButton.dataset.target);Drawers.getDrawerInstanceForNode(targetDrawer).closeDrawer(),(target=>{const lastUsedButton=document.querySelector("".concat(SELECTORS_BUTTONS,'[data-target="').concat(target,'"][data-lastused="true"'));lastUsedButton&&lastUsedButton.focus()})(closeDrawerButton.dataset.target)}})),document.addEventListener(Drawers.eventTypes.drawerShow,(e=>{(0,_pagehelpers.isLarge)()||Drawers.closeOtherDrawers(e.detail.drawerInstance)}));const btnSelector="".concat(SELECTORS_TOGGLEBTN,", ").concat(SELECTORS_OPENBTN,", ").concat(SELECTORS_CLOSEBTN);document.addEventListener("focusout",(e=>{const button=e.target.closest(btnSelector);void 0!==(null==button?void 0:button.dataset.restoreTooltipOnBlur)&&enableButtonTooltip(button)}));document.addEventListener("scroll",(()=>{const body=document.querySelector("body");window.scrollY>=window.innerHeight?body.classList.add(CLASSES_SCROLLED):body.classList.remove(CLASSES_SCROLLED),Drawers.displaceDrawers(window.scrollX)}));const preventOverlap=(0,_utils.debounce)(Drawers.preventCoveringFocusedElement,100);document.addEventListener("focusin",preventOverlap),document.addEventListener("focusout",preventOverlap),window.addEventListener("resize",(0,_utils.debounce)((()=>{if((0,_pagehelpers.isSmall)()){let anyOpen=!1;drawerMap.forEach((drawerInstance=>{disableDrawerTooltips(drawerInstance.drawerNode),drawerInstance.isOpen&&(drawerInstance.closeOnResize?drawerInstance.closeDrawer():anyOpen=!0)})),anyOpen&&getBackdrop().then((backdrop=>backdrop.show())).catch()}else drawerMap.forEach((drawerInstance=>{var drawerNode;[(drawerNode=drawerInstance.drawerNode).querySelector(SELECTORS_CLOSEBTN),getDrawerOpenButton(drawerNode.id)].forEach((button=>{button&&enableButtonTooltip(button)}))})),getBackdrop().then((backdrop=>backdrop.hide())).catch()}),400))})();return document.querySelectorAll(SELECTORS_DRAWERS).forEach((drawerNode=>Drawers.getDrawerInstanceForNode(drawerNode))),_exports.default})); +define("theme_boost/drawers",["exports","core/modal_backdrop","core/templates","core/aria","core/event_dispatcher","core/utils","core/pagehelpers","core/pending","jquery"],(function(_exports,_modal_backdrop,_templates,Aria,_event_dispatcher,_utils,_pagehelpers,_pending,_jquery){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal_backdrop=_interopRequireDefault(_modal_backdrop),_templates=_interopRequireDefault(_templates),Aria=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Aria),_pending=_interopRequireDefault(_pending),_jquery=_interopRequireDefault(_jquery);let backdropPromise=null;const drawerMap=new Map,SELECTORS_BUTTONS='[data-toggler="drawers"]',SELECTORS_CLOSEBTN='[data-toggler="drawers"][data-action="closedrawer"]',SELECTORS_OPENBTN='[data-toggler="drawers"][data-action="opendrawer"]',SELECTORS_TOGGLEBTN='[data-toggler="drawers"][data-action="toggle"]',SELECTORS_DRAWERS='[data-region="fixed-drawer"]',SELECTORS_DRAWERCONTENT=".drawercontent",SELECTORS_PAGECONTENT="#page-content",SELECTORS_HEADERCONTENT=".drawerheadercontent",CLASSES_SCROLLED="scrolled",CLASSES_SHOW="show",CLASSES_NOTINITIALISED="not-initialized",getDrawerZIndex=()=>{const drawer=document.querySelector(SELECTORS_DRAWERS);return drawer?parseInt(window.getComputedStyle(drawer).zIndex,10):null},getBackdrop=()=>(backdropPromise||(backdropPromise=_templates.default.render("core/modal_backdrop",{}).then((html=>new _modal_backdrop.default(html))).then((modalBackdrop=>(getDrawerZIndex()&&modalBackdrop.setZIndex(getDrawerZIndex()-1),modalBackdrop.getAttachmentPoint().get(0).addEventListener("click",(e=>{e.preventDefault(),Drawers.closeAllDrawers()})),modalBackdrop))).catch()),backdropPromise),getDrawerOpenButton=drawerId=>{let openButton=document.querySelector("".concat(SELECTORS_OPENBTN,'[data-target="').concat(drawerId,'"]'));return openButton||(openButton=document.querySelector("".concat(SELECTORS_TOGGLEBTN,'[data-target="').concat(drawerId,'"]'))),openButton},disableDrawerTooltips=drawerNode=>{[drawerNode.querySelector(SELECTORS_CLOSEBTN),getDrawerOpenButton(drawerNode.id)].forEach((button=>{button&&disableButtonTooltip(button)}))},disableButtonTooltip=(button,enableOnBlur)=>{button.hasAttribute("data-original-title")?((0,_jquery.default)(button).tooltip("disable"),button.setAttribute("title",button.dataset.originalTitle)):(button.dataset.disabledToggle=button.dataset.toggle,button.removeAttribute("data-toggle")),enableOnBlur&&(button.dataset.restoreTooltipOnBlur=!0)},enableButtonTooltip=button=>{button.hasAttribute("data-original-title")?((0,_jquery.default)(button).tooltip("enable"),button.removeAttribute("title")):button.dataset.disabledToggle&&(button.dataset.toggle=button.dataset.disabledToggle,(0,_jquery.default)(button).tooltip()),delete button.dataset.restoreTooltipOnBlur};class Drawers{constructor(drawerNode){_defineProperty(this,"drawerNode",null),_defineProperty(this,"boundingRect",null),void 0===drawerNode.dataset.behatFakeDrawer&&(this.drawerNode=drawerNode,(0,_pagehelpers.isSmall)()&&this.closeDrawer({focusOnOpenButton:!1,updatePreferences:!1}),this.drawerNode.classList.contains(CLASSES_SHOW)?this.openDrawer({focusOnCloseButton:!1}):1==this.drawerNode.dataset.forceopen?(0,_pagehelpers.isSmall)()||this.openDrawer({focusOnCloseButton:!1}):Aria.hide(this.drawerNode),(0,_pagehelpers.isSmall)()&&disableDrawerTooltips(this.drawerNode),(drawerNode=>{const content=drawerNode.querySelector(SELECTORS_DRAWERCONTENT);content&&content.addEventListener("scroll",(()=>{drawerNode.classList.toggle(CLASSES_SCROLLED,0!=content.scrollTop)}))})(this.drawerNode),drawerMap.set(drawerNode,this),drawerNode.classList.remove(CLASSES_NOTINITIALISED))}get isOpen(){return this.drawerNode.classList.contains(CLASSES_SHOW)}get closeOnResize(){return!!parseInt(this.drawerNode.dataset.closeOnResize)}static getDrawerInstanceForNode(drawerNode){return drawerMap.has(drawerNode)||new Drawers(drawerNode),drawerMap.get(drawerNode)}dispatchEvent(eventname){let cancelable=arguments.length>1&&void 0!==arguments[1]&&arguments[1];return(0,_event_dispatcher.dispatchEvent)(eventname,{drawerInstance:this},this.drawerNode,{cancelable:cancelable})}openDrawer(){var _this$drawerNode$quer,_this$drawerNode$quer2;let{focusOnCloseButton:focusOnCloseButton=!0}=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};const pendingPromise=new _pending.default("theme_boost/drawers:open");if(this.dispatchEvent(Drawers.eventTypes.drawerShow,!0).defaultPrevented)return;null===(_this$drawerNode$quer=this.drawerNode.querySelector(SELECTORS_CLOSEBTN))||void 0===_this$drawerNode$quer||_this$drawerNode$quer.classList.toggle("hidden",!0),null===(_this$drawerNode$quer2=this.drawerNode.querySelector(SELECTORS_HEADERCONTENT))||void 0===_this$drawerNode$quer2||_this$drawerNode$quer2.classList.toggle("hidden",!0);let openButton=getDrawerOpenButton(this.drawerNode.id);var _jQuery;openButton&&openButton.hasAttribute("data-original-title")&&(null===(_jQuery=(0,_jquery.default)(openButton))||void 0===_jQuery||_jQuery.tooltip("hide"));Aria.unhide(this.drawerNode),this.drawerNode.classList.add(CLASSES_SHOW);const preference=this.drawerNode.dataset.preference;preference&&!(0,_pagehelpers.isSmall)()&&1!=this.drawerNode.dataset.forceopen&&M.util.set_user_preference(preference,!0);const state=this.drawerNode.dataset.state;if(state){document.getElementById("page").classList.add(state)}this.boundingRect=this.drawerNode.getBoundingClientRect(),(0,_pagehelpers.isSmall)()&&getBackdrop().then((backdrop=>{backdrop.show();return document.getElementById("page").style.overflow="hidden",backdrop})).catch();const closeButton=this.drawerNode.querySelector(SELECTORS_CLOSEBTN),headerContent=this.drawerNode.querySelector(SELECTORS_HEADERCONTENT);focusOnCloseButton&&closeButton&&disableButtonTooltip(closeButton,!0),setTimeout((()=>{closeButton.classList.toggle("hidden",!1),headerContent.classList.toggle("hidden",!1),focusOnCloseButton&&closeButton.focus(),pendingPromise.resolve()}),300),this.dispatchEvent(Drawers.eventTypes.drawerShown)}closeDrawer(){let{focusOnOpenButton:focusOnOpenButton=!0,updatePreferences:updatePreferences=!0}=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};const pendingPromise=new _pending.default("theme_boost/drawers:close");if(this.dispatchEvent(Drawers.eventTypes.drawerHide,!0).defaultPrevented)return;const closeButton=this.drawerNode.querySelector(SELECTORS_CLOSEBTN);null==closeButton||closeButton.classList.toggle("hidden",!0);const headerContent=this.drawerNode.querySelector(SELECTORS_HEADERCONTENT);var _jQuery2;(null==headerContent||headerContent.classList.toggle("hidden",!0),closeButton.hasAttribute("data-original-title"))&&(null===(_jQuery2=(0,_jquery.default)(closeButton))||void 0===_jQuery2||_jQuery2.tooltip("hide"));const preference=this.drawerNode.dataset.preference;preference&&updatePreferences&&!(0,_pagehelpers.isSmall)()&&M.util.set_user_preference(preference,!1);const state=this.drawerNode.dataset.state;if(state){document.getElementById("page").classList.remove(state)}Aria.hide(this.drawerNode),this.drawerNode.classList.remove(CLASSES_SHOW),getBackdrop().then((backdrop=>{if(backdrop.hide(),(0,_pagehelpers.isSmall)()){document.getElementById("page").style.overflow="visible"}return backdrop})).catch();let openButton=getDrawerOpenButton(this.drawerNode.id);openButton&&disableButtonTooltip(openButton,!0),setTimeout((()=>{openButton&&focusOnOpenButton&&openButton.focus(),pendingPromise.resolve()}),300),this.dispatchEvent(Drawers.eventTypes.drawerHidden)}toggleVisibility(){this.drawerNode.classList.contains(CLASSES_SHOW)?this.closeDrawer():this.openDrawer()}displace(scrollPosition){var _this$drawerNode$data;let displace=scrollPosition,openButton=getDrawerOpenButton(this.drawerNode.id);if(0===scrollPosition)return this.drawerNode.style.transform="",void(openButton&&(openButton.style.transform=""));const state=null===(_this$drawerNode$data=this.drawerNode.dataset)||void 0===_this$drawerNode$data?void 0:_this$drawerNode$data.state,drawrWidth=this.drawerNode.offsetWidth;let scrollThreshold=drawrWidth,direction=-1;"show-drawer-right"===state&&(direction=1,scrollThreshold=20),scrollPosition>scrollThreshold&&(displace=drawrWidth+20),displace*=direction;const transform="translateX(".concat(displace,"px)");openButton&&(openButton.style.transform=transform),this.drawerNode.style.transform=transform}preventOverlap(currentFocus){if(!this.isOpen)return;const drawrWidth=this.drawerNode.offsetWidth,element=currentFocus.getBoundingClientRect(),drawer=this.boundingRect;element.right+20>drawer.left&&element.left-20{drawerInstance.closeDrawer()}))}static closeOtherDrawers(comparisonInstance){drawerMap.forEach((drawerInstance=>{drawerInstance!==comparisonInstance&&drawerInstance.closeDrawer()}))}static preventCoveringFocusedElement(){const currentFocus=document.activeElement,pagecontent=document.querySelector(SELECTORS_PAGECONTENT);currentFocus&&null!=pagecontent&&pagecontent.contains(currentFocus)?drawerMap.forEach((drawerInstance=>{drawerInstance.preventOverlap(currentFocus)})):Drawers.displaceDrawers(window.scrollX)}static displaceDrawers(displace){drawerMap.forEach((drawerInstance=>{drawerInstance.displace(displace)}))}}_exports.default=Drawers,_defineProperty(Drawers,"eventTypes",{drawerShow:"theme_boost/drawers:show",drawerShown:"theme_boost/drawers:shown",drawerHide:"theme_boost/drawers:hide",drawerHidden:"theme_boost/drawers:hidden"});const setLastUsedToggle=toggleButton=>{toggleButton.dataset.target&&(document.querySelectorAll("".concat(SELECTORS_BUTTONS,'[data-target="').concat(toggleButton.dataset.target,'"]')).forEach((btn=>{btn.dataset.lastused=!1})),toggleButton.dataset.lastused=!0)};(()=>{document.addEventListener("click",(e=>{const toggleButton=e.target.closest(SELECTORS_TOGGLEBTN);if(toggleButton&&toggleButton.dataset.target){e.preventDefault();const targetDrawer=document.getElementById(toggleButton.dataset.target),drawerInstance=Drawers.getDrawerInstanceForNode(targetDrawer);setLastUsedToggle(toggleButton),drawerInstance.toggleVisibility()}const openDrawerButton=e.target.closest(SELECTORS_OPENBTN);if(openDrawerButton&&openDrawerButton.dataset.target){e.preventDefault();const targetDrawer=document.getElementById(openDrawerButton.dataset.target),drawerInstance=Drawers.getDrawerInstanceForNode(targetDrawer);setLastUsedToggle(toggleButton),drawerInstance.openDrawer()}const closeDrawerButton=e.target.closest(SELECTORS_CLOSEBTN);if(closeDrawerButton&&closeDrawerButton.dataset.target){e.preventDefault();const targetDrawer=document.getElementById(closeDrawerButton.dataset.target);Drawers.getDrawerInstanceForNode(targetDrawer).closeDrawer(),(target=>{const lastUsedButton=document.querySelector("".concat(SELECTORS_BUTTONS,'[data-target="').concat(target,'"][data-lastused="true"'));lastUsedButton&&lastUsedButton.focus()})(closeDrawerButton.dataset.target)}})),document.addEventListener(Drawers.eventTypes.drawerShow,(e=>{(0,_pagehelpers.isLarge)()||Drawers.closeOtherDrawers(e.detail.drawerInstance)}));const btnSelector="".concat(SELECTORS_TOGGLEBTN,", ").concat(SELECTORS_OPENBTN,", ").concat(SELECTORS_CLOSEBTN);document.addEventListener("focusout",(e=>{const button=e.target.closest(btnSelector);void 0!==(null==button?void 0:button.dataset.restoreTooltipOnBlur)&&enableButtonTooltip(button)}));document.addEventListener("scroll",(()=>{const body=document.querySelector("body");window.scrollY>=window.innerHeight?body.classList.add(CLASSES_SCROLLED):body.classList.remove(CLASSES_SCROLLED),Drawers.displaceDrawers(window.scrollX)}));const preventOverlap=(0,_utils.debounce)(Drawers.preventCoveringFocusedElement,100);document.addEventListener("focusin",preventOverlap),document.addEventListener("focusout",preventOverlap),window.addEventListener("resize",(0,_utils.debounce)((()=>{if((0,_pagehelpers.isSmall)()){let anyOpen=!1;drawerMap.forEach((drawerInstance=>{disableDrawerTooltips(drawerInstance.drawerNode),drawerInstance.isOpen&&(drawerInstance.closeOnResize?drawerInstance.closeDrawer():anyOpen=!0)})),anyOpen&&getBackdrop().then((backdrop=>backdrop.show())).catch()}else drawerMap.forEach((drawerInstance=>{var drawerNode;[(drawerNode=drawerInstance.drawerNode).querySelector(SELECTORS_CLOSEBTN),getDrawerOpenButton(drawerNode.id)].forEach((button=>{button&&enableButtonTooltip(button)}))})),getBackdrop().then((backdrop=>backdrop.hide())).catch()}),400))})();return document.querySelectorAll(SELECTORS_DRAWERS).forEach((drawerNode=>Drawers.getDrawerInstanceForNode(drawerNode))),_exports.default})); //# sourceMappingURL=drawers.min.js.map \ No newline at end of file diff --git a/theme/boost/amd/build/drawers.min.js.map b/theme/boost/amd/build/drawers.min.js.map index c46be7fe01c..0adb7091c1a 100644 --- a/theme/boost/amd/build/drawers.min.js.map +++ b/theme/boost/amd/build/drawers.min.js.map @@ -1 +1 @@ -{"version":3,"file":"drawers.min.js","sources":["../src/drawers.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Toggling the visibility of the secondary navigation on mobile.\n *\n * @module theme_boost/drawers\n * @copyright 2021 Bas Brands\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport ModalBackdrop from 'core/modal_backdrop';\nimport Templates from 'core/templates';\nimport * as Aria from 'core/aria';\nimport {dispatchEvent} from 'core/event_dispatcher';\nimport {debounce} from 'core/utils';\nimport {isSmall, isLarge} from 'core/pagehelpers';\nimport Pending from 'core/pending';\n// The jQuery module is only used for interacting with Boostrap 4. It can we removed when MDL-71979 is integrated.\nimport jQuery from 'jquery';\n\nlet backdropPromise = null;\n\nconst drawerMap = new Map();\n\nconst SELECTORS = {\n BUTTONS: '[data-toggler=\"drawers\"]',\n CLOSEBTN: '[data-toggler=\"drawers\"][data-action=\"closedrawer\"]',\n OPENBTN: '[data-toggler=\"drawers\"][data-action=\"opendrawer\"]',\n TOGGLEBTN: '[data-toggler=\"drawers\"][data-action=\"toggle\"]',\n DRAWERS: '[data-region=\"fixed-drawer\"]',\n DRAWERCONTENT: '.drawercontent',\n PAGECONTENT: '#page-content',\n};\n\nconst CLASSES = {\n SCROLLED: 'scrolled',\n SHOW: 'show',\n NOTINITIALISED: 'not-initialized',\n};\n\n/**\n * Pixel thresshold to auto-hide drawers.\n *\n * @type {Number}\n */\nconst THRESHOLD = 20;\n\n/**\n * Try to get the drawer z-index from the page content.\n *\n * @returns {Number|null} the z-index of the drawer.\n * @private\n */\nconst getDrawerZIndex = () => {\n const drawer = document.querySelector(SELECTORS.DRAWERS);\n if (!drawer) {\n return null;\n }\n return parseInt(window.getComputedStyle(drawer).zIndex, 10);\n};\n\n/**\n * Add a backdrop to the page.\n *\n * @returns {Promise} rendering of modal backdrop.\n * @private\n */\nconst getBackdrop = () => {\n if (!backdropPromise) {\n backdropPromise = Templates.render('core/modal_backdrop', {})\n .then(html => new ModalBackdrop(html))\n .then(modalBackdrop => {\n const drawerZindex = getDrawerZIndex();\n if (drawerZindex) {\n modalBackdrop.setZIndex(getDrawerZIndex() - 1);\n }\n modalBackdrop.getAttachmentPoint().get(0).addEventListener('click', e => {\n e.preventDefault();\n Drawers.closeAllDrawers();\n });\n return modalBackdrop;\n })\n .catch();\n }\n return backdropPromise;\n};\n\n/**\n * Get the button element to open a specific drawer.\n *\n * @param {String} drawerId the drawer element Id\n * @return {HTMLElement|undefined} the open button element\n * @private\n */\nconst getDrawerOpenButton = (drawerId) => {\n let openButton = document.querySelector(`${SELECTORS.OPENBTN}[data-target=\"${drawerId}\"]`);\n if (!openButton) {\n openButton = document.querySelector(`${SELECTORS.TOGGLEBTN}[data-target=\"${drawerId}\"]`);\n }\n return openButton;\n};\n\n/**\n * Disable drawer tooltips.\n *\n * @param {HTMLElement} drawerNode the drawer main node\n * @private\n */\nconst disableDrawerTooltips = (drawerNode) => {\n const buttons = [\n drawerNode.querySelector(SELECTORS.CLOSEBTN),\n getDrawerOpenButton(drawerNode.id),\n ];\n buttons.forEach(button => {\n if (!button) {\n return;\n }\n disableButtonTooltip(button);\n });\n};\n\n/**\n * Disable the button tooltips.\n *\n * @param {HTMLElement} button the button element\n * @param {boolean} enableOnBlur if the tooltip must be re-enabled on blur.\n * @private\n */\nconst disableButtonTooltip = (button, enableOnBlur) => {\n if (button.hasAttribute('data-original-title')) {\n // The jQuery is still used in Boostrap 4. It can we removed when MDL-71979 is integrated.\n jQuery(button).tooltip('disable');\n button.setAttribute('title', button.dataset.originalTitle);\n } else {\n button.dataset.disabledToggle = button.dataset.toggle;\n button.removeAttribute('data-toggle');\n }\n if (enableOnBlur) {\n button.dataset.restoreTooltipOnBlur = true;\n }\n};\n\n/**\n * Enable drawer tooltips.\n *\n * @param {HTMLElement} drawerNode the drawer main node\n * @private\n */\nconst enableDrawerTooltips = (drawerNode) => {\n const buttons = [\n drawerNode.querySelector(SELECTORS.CLOSEBTN),\n getDrawerOpenButton(drawerNode.id),\n ];\n buttons.forEach(button => {\n if (!button) {\n return;\n }\n enableButtonTooltip(button);\n });\n};\n\n/**\n * Enable the button tooltips.\n *\n * @param {HTMLElement} button the button element\n * @private\n */\nconst enableButtonTooltip = (button) => {\n // The jQuery is still used in Boostrap 4. It can we removed when MDL-71979 is integrated.\n if (button.hasAttribute('data-original-title')) {\n jQuery(button).tooltip('enable');\n button.removeAttribute('title');\n } else if (button.dataset.disabledToggle) {\n button.dataset.toggle = button.dataset.disabledToggle;\n jQuery(button).tooltip();\n }\n delete button.dataset.restoreTooltipOnBlur;\n};\n\n/**\n * Add scroll listeners to a drawer element.\n *\n * @param {HTMLElement} drawerNode the drawer main node\n * @private\n */\nconst addInnerScrollListener = (drawerNode) => {\n const content = drawerNode.querySelector(SELECTORS.DRAWERCONTENT);\n if (!content) {\n return;\n }\n content.addEventListener(\"scroll\", () => {\n drawerNode.classList.toggle(\n CLASSES.SCROLLED,\n content.scrollTop != 0\n );\n });\n};\n\n/**\n * The Drawers class is used to control on-screen drawer elements.\n *\n * It handles opening, and closing of drawer elements, as well as more detailed behaviours such as closing a drawer when\n * another drawer is opened, and supports closing a drawer when the screen is resized.\n *\n * Drawers are instantiated on page load, and can also be toggled lazily when toggling any drawer toggle, open button,\n * or close button.\n *\n * A range of show and hide events are also dispatched as detailed in the class\n * {@link module:theme_boost/drawers#eventTypes eventTypes} object.\n *\n * @example Standard usage\n *\n * // The module just needs to be included to add drawer support.\n * import 'theme_boost/drawers';\n *\n * @example Manually open or close any drawer\n *\n * import Drawers from 'theme_boost/drawers';\n *\n * const myDrawer = Drawers.getDrawerInstanceForNode(document.querySelector('.myDrawerNode');\n * myDrawer.closeDrawer();\n *\n * @example Listen to the before show event and cancel it\n *\n * import Drawers from 'theme_boost/drawers';\n *\n * document.addEventListener(Drawers.eventTypes.drawerShow, e => {\n * // The drawer which will be shown.\n * window.console.log(e.target);\n *\n * // The instance of the Drawers class for this drawer.\n * window.console.log(e.detail.drawerInstance);\n *\n * // Prevent this drawer from being shown.\n * e.preventDefault();\n * });\n *\n * @example Listen to the shown event\n *\n * document.addEventListener(Drawers.eventTypes.drawerShown, e => {\n * // The drawer which was shown.\n * window.console.log(e.target);\n *\n * // The instance of the Drawers class for this drawer.\n * window.console.log(e.detail.drawerInstance);\n * });\n */\nexport default class Drawers {\n /**\n * The underlying HTMLElement which is controlled.\n */\n drawerNode = null;\n\n /**\n * The drawer page bounding box dimensions.\n * @var {DOMRect} boundingRect\n */\n boundingRect = null;\n\n constructor(drawerNode) {\n // Some behat tests may use fake drawer divs to test components in drawers.\n if (drawerNode.dataset.behatFakeDrawer !== undefined) {\n return;\n }\n\n this.drawerNode = drawerNode;\n\n if (isSmall()) {\n this.closeDrawer({focusOnOpenButton: false, updatePreferences: false});\n }\n\n if (this.drawerNode.classList.contains(CLASSES.SHOW)) {\n this.openDrawer({focusOnCloseButton: false});\n } else if (this.drawerNode.dataset.forceopen == 1) {\n if (!isSmall()) {\n this.openDrawer({focusOnCloseButton: false});\n }\n } else {\n Aria.hide(this.drawerNode);\n }\n\n // Disable tooltips in small screens.\n if (isSmall()) {\n disableDrawerTooltips(this.drawerNode);\n }\n\n addInnerScrollListener(this.drawerNode);\n\n drawerMap.set(drawerNode, this);\n\n drawerNode.classList.remove(CLASSES.NOTINITIALISED);\n }\n\n /**\n * Whether the drawer is open.\n *\n * @returns {boolean}\n */\n get isOpen() {\n return this.drawerNode.classList.contains(CLASSES.SHOW);\n }\n\n /**\n * Whether the drawer should close when the window is resized\n *\n * @returns {boolean}\n */\n get closeOnResize() {\n return !!parseInt(this.drawerNode.dataset.closeOnResize);\n }\n\n /**\n * The list of event types.\n *\n * @static\n * @property {String} drawerShow See {@link event:theme_boost/drawers:show}\n * @property {String} drawerShown See {@link event:theme_boost/drawers:shown}\n * @property {String} drawerHide See {@link event:theme_boost/drawers:hide}\n * @property {String} drawerHidden See {@link event:theme_boost/drawers:hidden}\n */\n static eventTypes = {\n /**\n * An event triggered before a drawer is shown.\n *\n * @event theme_boost/drawers:show\n * @type {CustomEvent}\n * @property {HTMLElement} target The drawer that will be opened.\n */\n drawerShow: 'theme_boost/drawers:show',\n\n /**\n * An event triggered after a drawer is shown.\n *\n * @event theme_boost/drawers:shown\n * @type {CustomEvent}\n * @property {HTMLElement} target The drawer that was be opened.\n */\n drawerShown: 'theme_boost/drawers:shown',\n\n /**\n * An event triggered before a drawer is hidden.\n *\n * @event theme_boost/drawers:hide\n * @type {CustomEvent}\n * @property {HTMLElement} target The drawer that will be hidden.\n */\n drawerHide: 'theme_boost/drawers:hide',\n\n /**\n * An event triggered after a drawer is hidden.\n *\n * @event theme_boost/drawers:hidden\n * @type {CustomEvent}\n * @property {HTMLElement} target The drawer that was be hidden.\n */\n drawerHidden: 'theme_boost/drawers:hidden',\n };\n\n\n /**\n * Get the drawer instance for the specified node\n *\n * @param {HTMLElement} drawerNode\n * @returns {module:theme_boost/drawers}\n */\n static getDrawerInstanceForNode(drawerNode) {\n if (!drawerMap.has(drawerNode)) {\n new Drawers(drawerNode);\n }\n\n return drawerMap.get(drawerNode);\n }\n\n /**\n * Dispatch a drawer event.\n *\n * @param {string} eventname the event name\n * @param {boolean} cancelable if the event is cancelable\n * @returns {CustomEvent} the resulting custom event\n */\n dispatchEvent(eventname, cancelable = false) {\n return dispatchEvent(\n eventname,\n {\n drawerInstance: this,\n },\n this.drawerNode,\n {\n cancelable,\n }\n );\n }\n\n /**\n * Open the drawer.\n *\n * By default, openDrawer sets the page focus to the close drawer button. However, when a drawer is open at page\n * load, this represents an accessibility problem as the initial focus changes without any user interaction. The\n * focusOnCloseButton parameter can be set to false to prevent this behaviour.\n *\n * @param {object} args\n * @param {boolean} [args.focusOnCloseButton=true] Whether to alter page focus when opening the drawer\n */\n openDrawer({focusOnCloseButton = true} = {}) {\n\n const pendingPromise = new Pending('theme_boost/drawers:open');\n const showEvent = this.dispatchEvent(Drawers.eventTypes.drawerShow, true);\n if (showEvent.defaultPrevented) {\n return;\n }\n\n // Hide close button while the drawer is showing to prevent glitchy effects.\n this.drawerNode.querySelector(SELECTORS.CLOSEBTN)?.classList.toggle('hidden', true);\n\n // Remove open tooltip if still visible.\n let openButton = getDrawerOpenButton(this.drawerNode.id);\n if (openButton && openButton.hasAttribute('data-original-title')) {\n // The jQuery is still used in Boostrap 4. It can we removed when MDL-71979 is integrated.\n jQuery(openButton)?.tooltip('hide');\n }\n\n Aria.unhide(this.drawerNode);\n this.drawerNode.classList.add(CLASSES.SHOW);\n\n const preference = this.drawerNode.dataset.preference;\n if (preference && !isSmall() && (this.drawerNode.dataset.forceopen != 1)) {\n M.util.set_user_preference(preference, true);\n }\n\n const state = this.drawerNode.dataset.state;\n if (state) {\n const page = document.getElementById('page');\n page.classList.add(state);\n }\n\n this.boundingRect = this.drawerNode.getBoundingClientRect();\n\n if (isSmall()) {\n getBackdrop().then(backdrop => {\n backdrop.show();\n\n const pageWrapper = document.getElementById('page');\n pageWrapper.style.overflow = 'hidden';\n return backdrop;\n })\n .catch();\n }\n\n // Show close button once the drawer is fully opened.\n const closeButton = this.drawerNode.querySelector(SELECTORS.CLOSEBTN);\n if (focusOnCloseButton && closeButton) {\n disableButtonTooltip(closeButton, true);\n }\n setTimeout(() => {\n closeButton.classList.toggle('hidden', false);\n if (focusOnCloseButton) {\n closeButton.focus();\n }\n pendingPromise.resolve();\n }, 300);\n\n this.dispatchEvent(Drawers.eventTypes.drawerShown);\n }\n\n /**\n * Close the drawer.\n *\n * @param {object} args\n * @param {boolean} [args.focusOnOpenButton=true] Whether to alter page focus when opening the drawer\n * @param {boolean} [args.updatePreferences=true] Whether to update the user prewference\n */\n closeDrawer({focusOnOpenButton = true, updatePreferences = true} = {}) {\n\n const pendingPromise = new Pending('theme_boost/drawers:close');\n\n const hideEvent = this.dispatchEvent(Drawers.eventTypes.drawerHide, true);\n if (hideEvent.defaultPrevented) {\n return;\n }\n\n // Hide close button while the drawer is hiding to prevent glitchy effects.\n const closeButton = this.drawerNode.querySelector(SELECTORS.CLOSEBTN);\n closeButton?.classList.toggle('hidden', true);\n // Remove the close button tooltip if visible.\n if (closeButton.hasAttribute('data-original-title')) {\n // The jQuery is still used in Boostrap 4. It can we removed when MDL-71979 is integrated.\n jQuery(closeButton)?.tooltip('hide');\n }\n\n const preference = this.drawerNode.dataset.preference;\n if (preference && updatePreferences && !isSmall()) {\n M.util.set_user_preference(preference, false);\n }\n\n const state = this.drawerNode.dataset.state;\n if (state) {\n const page = document.getElementById('page');\n page.classList.remove(state);\n }\n\n Aria.hide(this.drawerNode);\n this.drawerNode.classList.remove(CLASSES.SHOW);\n\n getBackdrop().then(backdrop => {\n backdrop.hide();\n\n if (isSmall()) {\n const pageWrapper = document.getElementById('page');\n pageWrapper.style.overflow = 'visible';\n }\n return backdrop;\n })\n .catch();\n\n // Move focus to the open drawer (or toggler) button once the drawer is hidden.\n let openButton = getDrawerOpenButton(this.drawerNode.id);\n if (openButton) {\n disableButtonTooltip(openButton, true);\n }\n setTimeout(() => {\n if (openButton && focusOnOpenButton) {\n openButton.focus();\n }\n pendingPromise.resolve();\n }, 300);\n\n this.dispatchEvent(Drawers.eventTypes.drawerHidden);\n }\n\n /**\n * Toggle visibility of the drawer.\n */\n toggleVisibility() {\n if (this.drawerNode.classList.contains(CLASSES.SHOW)) {\n this.closeDrawer();\n } else {\n this.openDrawer();\n }\n }\n\n /**\n * Displaces the drawer outsite the page.\n *\n * @param {Number} scrollPosition the page current scroll position\n */\n displace(scrollPosition) {\n let displace = scrollPosition;\n let openButton = getDrawerOpenButton(this.drawerNode.id);\n if (scrollPosition === 0) {\n this.drawerNode.style.transform = '';\n if (openButton) {\n openButton.style.transform = '';\n }\n return;\n }\n const state = this.drawerNode.dataset?.state;\n const drawrWidth = this.drawerNode.offsetWidth;\n let scrollThreshold = drawrWidth;\n let direction = -1;\n if (state === 'show-drawer-right') {\n direction = 1;\n scrollThreshold = THRESHOLD;\n }\n if (scrollPosition > scrollThreshold) {\n displace = drawrWidth + THRESHOLD;\n }\n displace *= direction;\n const transform = `translateX(${displace}px)`;\n if (openButton) {\n openButton.style.transform = transform;\n }\n this.drawerNode.style.transform = transform;\n }\n\n /**\n * Prevent drawer from overlapping an element.\n *\n * @param {HTMLElement} currentFocus\n */\n preventOverlap(currentFocus) {\n if (!this.isOpen) {\n return;\n }\n const drawrWidth = this.drawerNode.offsetWidth;\n const element = currentFocus.getBoundingClientRect();\n const drawer = this.boundingRect;\n const overlapping = (\n (element.right + THRESHOLD) > drawer.left &&\n (element.left - THRESHOLD) < drawer.right\n );\n if (overlapping) {\n // Force drawer to displace out of the page.\n this.displace(drawrWidth + 1);\n } else {\n // Reset drawer displacement.\n this.displace(window.scrollX);\n }\n }\n\n /**\n * Close all drawers.\n */\n static closeAllDrawers() {\n drawerMap.forEach(drawerInstance => {\n drawerInstance.closeDrawer();\n });\n }\n\n /**\n * Close all drawers except for the specified drawer.\n *\n * @param {module:theme_boost/drawers} comparisonInstance\n */\n static closeOtherDrawers(comparisonInstance) {\n drawerMap.forEach(drawerInstance => {\n if (drawerInstance === comparisonInstance) {\n return;\n }\n\n drawerInstance.closeDrawer();\n });\n }\n\n /**\n * Prevent drawers from covering the focused element.\n */\n static preventCoveringFocusedElement() {\n const currentFocus = document.activeElement;\n // Focus on page layout elements should be ignored.\n const pagecontent = document.querySelector(SELECTORS.PAGECONTENT);\n if (!currentFocus || !pagecontent?.contains(currentFocus)) {\n Drawers.displaceDrawers(window.scrollX);\n return;\n }\n drawerMap.forEach(drawerInstance => {\n drawerInstance.preventOverlap(currentFocus);\n });\n }\n\n /**\n * Prevent drawer from covering the content when the page content covers the full page.\n *\n * @param {Number} displace\n */\n static displaceDrawers(displace) {\n drawerMap.forEach(drawerInstance => {\n drawerInstance.displace(displace);\n });\n }\n}\n\n/**\n * Set the last used attribute for the last used toggle button for a drawer.\n *\n * @param {object} toggleButton The clicked button.\n */\nconst setLastUsedToggle = (toggleButton) => {\n if (toggleButton.dataset.target) {\n document.querySelectorAll(`${SELECTORS.BUTTONS}[data-target=\"${toggleButton.dataset.target}\"]`)\n .forEach(btn => {\n btn.dataset.lastused = false;\n });\n toggleButton.dataset.lastused = true;\n }\n};\n\n/**\n * Set the focus to the last used button to open this drawer.\n * @param {string} target The drawer target.\n */\nconst focusLastUsedToggle = (target) => {\n const lastUsedButton = document.querySelector(`${SELECTORS.BUTTONS}[data-target=\"${target}\"][data-lastused=\"true\"`);\n if (lastUsedButton) {\n lastUsedButton.focus();\n }\n};\n\n/**\n * Register the event listeners for the drawer.\n *\n * @private\n */\nconst registerListeners = () => {\n // Listen for show/hide events.\n document.addEventListener('click', e => {\n const toggleButton = e.target.closest(SELECTORS.TOGGLEBTN);\n if (toggleButton && toggleButton.dataset.target) {\n e.preventDefault();\n const targetDrawer = document.getElementById(toggleButton.dataset.target);\n const drawerInstance = Drawers.getDrawerInstanceForNode(targetDrawer);\n setLastUsedToggle(toggleButton);\n\n drawerInstance.toggleVisibility();\n }\n\n const openDrawerButton = e.target.closest(SELECTORS.OPENBTN);\n if (openDrawerButton && openDrawerButton.dataset.target) {\n e.preventDefault();\n const targetDrawer = document.getElementById(openDrawerButton.dataset.target);\n const drawerInstance = Drawers.getDrawerInstanceForNode(targetDrawer);\n setLastUsedToggle(toggleButton);\n\n drawerInstance.openDrawer();\n }\n\n const closeDrawerButton = e.target.closest(SELECTORS.CLOSEBTN);\n if (closeDrawerButton && closeDrawerButton.dataset.target) {\n e.preventDefault();\n const targetDrawer = document.getElementById(closeDrawerButton.dataset.target);\n const drawerInstance = Drawers.getDrawerInstanceForNode(targetDrawer);\n\n drawerInstance.closeDrawer();\n focusLastUsedToggle(closeDrawerButton.dataset.target);\n }\n });\n\n // Close drawer when another drawer opens.\n document.addEventListener(Drawers.eventTypes.drawerShow, e => {\n if (isLarge()) {\n return;\n }\n Drawers.closeOtherDrawers(e.detail.drawerInstance);\n });\n\n // Tooglers and openers blur listeners.\n const btnSelector = `${SELECTORS.TOGGLEBTN}, ${SELECTORS.OPENBTN}, ${SELECTORS.CLOSEBTN}`;\n document.addEventListener('focusout', (e) => {\n const button = e.target.closest(btnSelector);\n if (button?.dataset.restoreTooltipOnBlur !== undefined) {\n enableButtonTooltip(button);\n }\n });\n\n const closeOnResizeListener = () => {\n if (isSmall()) {\n let anyOpen = false;\n drawerMap.forEach(drawerInstance => {\n disableDrawerTooltips(drawerInstance.drawerNode);\n if (drawerInstance.isOpen) {\n if (drawerInstance.closeOnResize) {\n drawerInstance.closeDrawer();\n } else {\n anyOpen = true;\n }\n }\n });\n\n if (anyOpen) {\n getBackdrop().then(backdrop => backdrop.show()).catch();\n }\n } else {\n drawerMap.forEach(drawerInstance => {\n enableDrawerTooltips(drawerInstance.drawerNode);\n });\n getBackdrop().then(backdrop => backdrop.hide()).catch();\n }\n };\n\n document.addEventListener('scroll', () => {\n const body = document.querySelector('body');\n if (window.scrollY >= window.innerHeight) {\n body.classList.add(CLASSES.SCROLLED);\n } else {\n body.classList.remove(CLASSES.SCROLLED);\n }\n // Horizontal scroll listener to displace the drawers to prevent covering\n // any possible sticky content.\n Drawers.displaceDrawers(window.scrollX);\n });\n\n const preventOverlap = debounce(Drawers.preventCoveringFocusedElement, 100);\n document.addEventListener('focusin', preventOverlap);\n document.addEventListener('focusout', preventOverlap);\n\n window.addEventListener('resize', debounce(closeOnResizeListener, 400));\n};\n\nregisterListeners();\n\nconst drawers = document.querySelectorAll(SELECTORS.DRAWERS);\ndrawers.forEach(drawerNode => Drawers.getDrawerInstanceForNode(drawerNode));\n"],"names":["backdropPromise","drawerMap","Map","SELECTORS","CLASSES","getDrawerZIndex","drawer","document","querySelector","parseInt","window","getComputedStyle","zIndex","getBackdrop","Templates","render","then","html","ModalBackdrop","modalBackdrop","setZIndex","getAttachmentPoint","get","addEventListener","e","preventDefault","Drawers","closeAllDrawers","catch","getDrawerOpenButton","drawerId","openButton","disableDrawerTooltips","drawerNode","id","forEach","button","disableButtonTooltip","enableOnBlur","hasAttribute","tooltip","setAttribute","dataset","originalTitle","disabledToggle","toggle","removeAttribute","restoreTooltipOnBlur","enableButtonTooltip","constructor","undefined","behatFakeDrawer","closeDrawer","focusOnOpenButton","updatePreferences","this","classList","contains","openDrawer","focusOnCloseButton","forceopen","Aria","hide","content","scrollTop","addInnerScrollListener","set","remove","isOpen","closeOnResize","has","dispatchEvent","eventname","cancelable","drawerInstance","pendingPromise","Pending","eventTypes","drawerShow","defaultPrevented","unhide","add","preference","M","util","set_user_preference","state","getElementById","boundingRect","getBoundingClientRect","backdrop","show","style","overflow","closeButton","setTimeout","focus","resolve","drawerShown","drawerHide","drawerHidden","toggleVisibility","displace","scrollPosition","transform","_this$drawerNode$data","drawrWidth","offsetWidth","scrollThreshold","direction","preventOverlap","currentFocus","element","right","left","scrollX","comparisonInstance","activeElement","pagecontent","displaceDrawers","setLastUsedToggle","toggleButton","target","querySelectorAll","btn","lastused","closest","targetDrawer","getDrawerInstanceForNode","openDrawerButton","closeDrawerButton","lastUsedButton","focusLastUsedToggle","closeOtherDrawers","detail","btnSelector","body","scrollY","innerHeight","preventCoveringFocusedElement","anyOpen","registerListeners"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"drawers.min.js","sources":["../src/drawers.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Toggling the visibility of the secondary navigation on mobile.\n *\n * @module theme_boost/drawers\n * @copyright 2021 Bas Brands\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport ModalBackdrop from 'core/modal_backdrop';\nimport Templates from 'core/templates';\nimport * as Aria from 'core/aria';\nimport {dispatchEvent} from 'core/event_dispatcher';\nimport {debounce} from 'core/utils';\nimport {isSmall, isLarge} from 'core/pagehelpers';\nimport Pending from 'core/pending';\n// The jQuery module is only used for interacting with Boostrap 4. It can we removed when MDL-71979 is integrated.\nimport jQuery from 'jquery';\n\nlet backdropPromise = null;\n\nconst drawerMap = new Map();\n\nconst SELECTORS = {\n BUTTONS: '[data-toggler=\"drawers\"]',\n CLOSEBTN: '[data-toggler=\"drawers\"][data-action=\"closedrawer\"]',\n OPENBTN: '[data-toggler=\"drawers\"][data-action=\"opendrawer\"]',\n TOGGLEBTN: '[data-toggler=\"drawers\"][data-action=\"toggle\"]',\n DRAWERS: '[data-region=\"fixed-drawer\"]',\n DRAWERCONTENT: '.drawercontent',\n PAGECONTENT: '#page-content',\n HEADERCONTENT: '.drawerheadercontent',\n};\n\nconst CLASSES = {\n SCROLLED: 'scrolled',\n SHOW: 'show',\n NOTINITIALISED: 'not-initialized',\n};\n\n/**\n * Pixel thresshold to auto-hide drawers.\n *\n * @type {Number}\n */\nconst THRESHOLD = 20;\n\n/**\n * Try to get the drawer z-index from the page content.\n *\n * @returns {Number|null} the z-index of the drawer.\n * @private\n */\nconst getDrawerZIndex = () => {\n const drawer = document.querySelector(SELECTORS.DRAWERS);\n if (!drawer) {\n return null;\n }\n return parseInt(window.getComputedStyle(drawer).zIndex, 10);\n};\n\n/**\n * Add a backdrop to the page.\n *\n * @returns {Promise} rendering of modal backdrop.\n * @private\n */\nconst getBackdrop = () => {\n if (!backdropPromise) {\n backdropPromise = Templates.render('core/modal_backdrop', {})\n .then(html => new ModalBackdrop(html))\n .then(modalBackdrop => {\n const drawerZindex = getDrawerZIndex();\n if (drawerZindex) {\n modalBackdrop.setZIndex(getDrawerZIndex() - 1);\n }\n modalBackdrop.getAttachmentPoint().get(0).addEventListener('click', e => {\n e.preventDefault();\n Drawers.closeAllDrawers();\n });\n return modalBackdrop;\n })\n .catch();\n }\n return backdropPromise;\n};\n\n/**\n * Get the button element to open a specific drawer.\n *\n * @param {String} drawerId the drawer element Id\n * @return {HTMLElement|undefined} the open button element\n * @private\n */\nconst getDrawerOpenButton = (drawerId) => {\n let openButton = document.querySelector(`${SELECTORS.OPENBTN}[data-target=\"${drawerId}\"]`);\n if (!openButton) {\n openButton = document.querySelector(`${SELECTORS.TOGGLEBTN}[data-target=\"${drawerId}\"]`);\n }\n return openButton;\n};\n\n/**\n * Disable drawer tooltips.\n *\n * @param {HTMLElement} drawerNode the drawer main node\n * @private\n */\nconst disableDrawerTooltips = (drawerNode) => {\n const buttons = [\n drawerNode.querySelector(SELECTORS.CLOSEBTN),\n getDrawerOpenButton(drawerNode.id),\n ];\n buttons.forEach(button => {\n if (!button) {\n return;\n }\n disableButtonTooltip(button);\n });\n};\n\n/**\n * Disable the button tooltips.\n *\n * @param {HTMLElement} button the button element\n * @param {boolean} enableOnBlur if the tooltip must be re-enabled on blur.\n * @private\n */\nconst disableButtonTooltip = (button, enableOnBlur) => {\n if (button.hasAttribute('data-original-title')) {\n // The jQuery is still used in Boostrap 4. It can we removed when MDL-71979 is integrated.\n jQuery(button).tooltip('disable');\n button.setAttribute('title', button.dataset.originalTitle);\n } else {\n button.dataset.disabledToggle = button.dataset.toggle;\n button.removeAttribute('data-toggle');\n }\n if (enableOnBlur) {\n button.dataset.restoreTooltipOnBlur = true;\n }\n};\n\n/**\n * Enable drawer tooltips.\n *\n * @param {HTMLElement} drawerNode the drawer main node\n * @private\n */\nconst enableDrawerTooltips = (drawerNode) => {\n const buttons = [\n drawerNode.querySelector(SELECTORS.CLOSEBTN),\n getDrawerOpenButton(drawerNode.id),\n ];\n buttons.forEach(button => {\n if (!button) {\n return;\n }\n enableButtonTooltip(button);\n });\n};\n\n/**\n * Enable the button tooltips.\n *\n * @param {HTMLElement} button the button element\n * @private\n */\nconst enableButtonTooltip = (button) => {\n // The jQuery is still used in Boostrap 4. It can we removed when MDL-71979 is integrated.\n if (button.hasAttribute('data-original-title')) {\n jQuery(button).tooltip('enable');\n button.removeAttribute('title');\n } else if (button.dataset.disabledToggle) {\n button.dataset.toggle = button.dataset.disabledToggle;\n jQuery(button).tooltip();\n }\n delete button.dataset.restoreTooltipOnBlur;\n};\n\n/**\n * Add scroll listeners to a drawer element.\n *\n * @param {HTMLElement} drawerNode the drawer main node\n * @private\n */\nconst addInnerScrollListener = (drawerNode) => {\n const content = drawerNode.querySelector(SELECTORS.DRAWERCONTENT);\n if (!content) {\n return;\n }\n content.addEventListener(\"scroll\", () => {\n drawerNode.classList.toggle(\n CLASSES.SCROLLED,\n content.scrollTop != 0\n );\n });\n};\n\n/**\n * The Drawers class is used to control on-screen drawer elements.\n *\n * It handles opening, and closing of drawer elements, as well as more detailed behaviours such as closing a drawer when\n * another drawer is opened, and supports closing a drawer when the screen is resized.\n *\n * Drawers are instantiated on page load, and can also be toggled lazily when toggling any drawer toggle, open button,\n * or close button.\n *\n * A range of show and hide events are also dispatched as detailed in the class\n * {@link module:theme_boost/drawers#eventTypes eventTypes} object.\n *\n * @example Standard usage\n *\n * // The module just needs to be included to add drawer support.\n * import 'theme_boost/drawers';\n *\n * @example Manually open or close any drawer\n *\n * import Drawers from 'theme_boost/drawers';\n *\n * const myDrawer = Drawers.getDrawerInstanceForNode(document.querySelector('.myDrawerNode');\n * myDrawer.closeDrawer();\n *\n * @example Listen to the before show event and cancel it\n *\n * import Drawers from 'theme_boost/drawers';\n *\n * document.addEventListener(Drawers.eventTypes.drawerShow, e => {\n * // The drawer which will be shown.\n * window.console.log(e.target);\n *\n * // The instance of the Drawers class for this drawer.\n * window.console.log(e.detail.drawerInstance);\n *\n * // Prevent this drawer from being shown.\n * e.preventDefault();\n * });\n *\n * @example Listen to the shown event\n *\n * document.addEventListener(Drawers.eventTypes.drawerShown, e => {\n * // The drawer which was shown.\n * window.console.log(e.target);\n *\n * // The instance of the Drawers class for this drawer.\n * window.console.log(e.detail.drawerInstance);\n * });\n */\nexport default class Drawers {\n /**\n * The underlying HTMLElement which is controlled.\n */\n drawerNode = null;\n\n /**\n * The drawer page bounding box dimensions.\n * @var {DOMRect} boundingRect\n */\n boundingRect = null;\n\n constructor(drawerNode) {\n // Some behat tests may use fake drawer divs to test components in drawers.\n if (drawerNode.dataset.behatFakeDrawer !== undefined) {\n return;\n }\n\n this.drawerNode = drawerNode;\n\n if (isSmall()) {\n this.closeDrawer({focusOnOpenButton: false, updatePreferences: false});\n }\n\n if (this.drawerNode.classList.contains(CLASSES.SHOW)) {\n this.openDrawer({focusOnCloseButton: false});\n } else if (this.drawerNode.dataset.forceopen == 1) {\n if (!isSmall()) {\n this.openDrawer({focusOnCloseButton: false});\n }\n } else {\n Aria.hide(this.drawerNode);\n }\n\n // Disable tooltips in small screens.\n if (isSmall()) {\n disableDrawerTooltips(this.drawerNode);\n }\n\n addInnerScrollListener(this.drawerNode);\n\n drawerMap.set(drawerNode, this);\n\n drawerNode.classList.remove(CLASSES.NOTINITIALISED);\n }\n\n /**\n * Whether the drawer is open.\n *\n * @returns {boolean}\n */\n get isOpen() {\n return this.drawerNode.classList.contains(CLASSES.SHOW);\n }\n\n /**\n * Whether the drawer should close when the window is resized\n *\n * @returns {boolean}\n */\n get closeOnResize() {\n return !!parseInt(this.drawerNode.dataset.closeOnResize);\n }\n\n /**\n * The list of event types.\n *\n * @static\n * @property {String} drawerShow See {@link event:theme_boost/drawers:show}\n * @property {String} drawerShown See {@link event:theme_boost/drawers:shown}\n * @property {String} drawerHide See {@link event:theme_boost/drawers:hide}\n * @property {String} drawerHidden See {@link event:theme_boost/drawers:hidden}\n */\n static eventTypes = {\n /**\n * An event triggered before a drawer is shown.\n *\n * @event theme_boost/drawers:show\n * @type {CustomEvent}\n * @property {HTMLElement} target The drawer that will be opened.\n */\n drawerShow: 'theme_boost/drawers:show',\n\n /**\n * An event triggered after a drawer is shown.\n *\n * @event theme_boost/drawers:shown\n * @type {CustomEvent}\n * @property {HTMLElement} target The drawer that was be opened.\n */\n drawerShown: 'theme_boost/drawers:shown',\n\n /**\n * An event triggered before a drawer is hidden.\n *\n * @event theme_boost/drawers:hide\n * @type {CustomEvent}\n * @property {HTMLElement} target The drawer that will be hidden.\n */\n drawerHide: 'theme_boost/drawers:hide',\n\n /**\n * An event triggered after a drawer is hidden.\n *\n * @event theme_boost/drawers:hidden\n * @type {CustomEvent}\n * @property {HTMLElement} target The drawer that was be hidden.\n */\n drawerHidden: 'theme_boost/drawers:hidden',\n };\n\n\n /**\n * Get the drawer instance for the specified node\n *\n * @param {HTMLElement} drawerNode\n * @returns {module:theme_boost/drawers}\n */\n static getDrawerInstanceForNode(drawerNode) {\n if (!drawerMap.has(drawerNode)) {\n new Drawers(drawerNode);\n }\n\n return drawerMap.get(drawerNode);\n }\n\n /**\n * Dispatch a drawer event.\n *\n * @param {string} eventname the event name\n * @param {boolean} cancelable if the event is cancelable\n * @returns {CustomEvent} the resulting custom event\n */\n dispatchEvent(eventname, cancelable = false) {\n return dispatchEvent(\n eventname,\n {\n drawerInstance: this,\n },\n this.drawerNode,\n {\n cancelable,\n }\n );\n }\n\n /**\n * Open the drawer.\n *\n * By default, openDrawer sets the page focus to the close drawer button. However, when a drawer is open at page\n * load, this represents an accessibility problem as the initial focus changes without any user interaction. The\n * focusOnCloseButton parameter can be set to false to prevent this behaviour.\n *\n * @param {object} args\n * @param {boolean} [args.focusOnCloseButton=true] Whether to alter page focus when opening the drawer\n */\n openDrawer({focusOnCloseButton = true} = {}) {\n\n const pendingPromise = new Pending('theme_boost/drawers:open');\n const showEvent = this.dispatchEvent(Drawers.eventTypes.drawerShow, true);\n if (showEvent.defaultPrevented) {\n return;\n }\n\n // Hide close button and header content while the drawer is showing to prevent glitchy effects.\n this.drawerNode.querySelector(SELECTORS.CLOSEBTN)?.classList.toggle('hidden', true);\n this.drawerNode.querySelector(SELECTORS.HEADERCONTENT)?.classList.toggle('hidden', true);\n\n\n // Remove open tooltip if still visible.\n let openButton = getDrawerOpenButton(this.drawerNode.id);\n if (openButton && openButton.hasAttribute('data-original-title')) {\n // The jQuery is still used in Boostrap 4. It can we removed when MDL-71979 is integrated.\n jQuery(openButton)?.tooltip('hide');\n }\n\n Aria.unhide(this.drawerNode);\n this.drawerNode.classList.add(CLASSES.SHOW);\n\n const preference = this.drawerNode.dataset.preference;\n if (preference && !isSmall() && (this.drawerNode.dataset.forceopen != 1)) {\n M.util.set_user_preference(preference, true);\n }\n\n const state = this.drawerNode.dataset.state;\n if (state) {\n const page = document.getElementById('page');\n page.classList.add(state);\n }\n\n this.boundingRect = this.drawerNode.getBoundingClientRect();\n\n if (isSmall()) {\n getBackdrop().then(backdrop => {\n backdrop.show();\n\n const pageWrapper = document.getElementById('page');\n pageWrapper.style.overflow = 'hidden';\n return backdrop;\n })\n .catch();\n }\n\n // Show close button and header content once the drawer is fully opened.\n const closeButton = this.drawerNode.querySelector(SELECTORS.CLOSEBTN);\n const headerContent = this.drawerNode.querySelector(SELECTORS.HEADERCONTENT);\n if (focusOnCloseButton && closeButton) {\n disableButtonTooltip(closeButton, true);\n }\n setTimeout(() => {\n closeButton.classList.toggle('hidden', false);\n headerContent.classList.toggle('hidden', false);\n if (focusOnCloseButton) {\n closeButton.focus();\n }\n pendingPromise.resolve();\n }, 300);\n\n this.dispatchEvent(Drawers.eventTypes.drawerShown);\n }\n\n /**\n * Close the drawer.\n *\n * @param {object} args\n * @param {boolean} [args.focusOnOpenButton=true] Whether to alter page focus when opening the drawer\n * @param {boolean} [args.updatePreferences=true] Whether to update the user prewference\n */\n closeDrawer({focusOnOpenButton = true, updatePreferences = true} = {}) {\n\n const pendingPromise = new Pending('theme_boost/drawers:close');\n\n const hideEvent = this.dispatchEvent(Drawers.eventTypes.drawerHide, true);\n if (hideEvent.defaultPrevented) {\n return;\n }\n\n // Hide close button and header content while the drawer is hiding to prevent glitchy effects.\n const closeButton = this.drawerNode.querySelector(SELECTORS.CLOSEBTN);\n closeButton?.classList.toggle('hidden', true);\n const headerContent = this.drawerNode.querySelector(SELECTORS.HEADERCONTENT);\n headerContent?.classList.toggle('hidden', true);\n // Remove the close button tooltip if visible.\n if (closeButton.hasAttribute('data-original-title')) {\n // The jQuery is still used in Boostrap 4. It can we removed when MDL-71979 is integrated.\n jQuery(closeButton)?.tooltip('hide');\n }\n\n const preference = this.drawerNode.dataset.preference;\n if (preference && updatePreferences && !isSmall()) {\n M.util.set_user_preference(preference, false);\n }\n\n const state = this.drawerNode.dataset.state;\n if (state) {\n const page = document.getElementById('page');\n page.classList.remove(state);\n }\n\n Aria.hide(this.drawerNode);\n this.drawerNode.classList.remove(CLASSES.SHOW);\n\n getBackdrop().then(backdrop => {\n backdrop.hide();\n\n if (isSmall()) {\n const pageWrapper = document.getElementById('page');\n pageWrapper.style.overflow = 'visible';\n }\n return backdrop;\n })\n .catch();\n\n // Move focus to the open drawer (or toggler) button once the drawer is hidden.\n let openButton = getDrawerOpenButton(this.drawerNode.id);\n if (openButton) {\n disableButtonTooltip(openButton, true);\n }\n setTimeout(() => {\n if (openButton && focusOnOpenButton) {\n openButton.focus();\n }\n pendingPromise.resolve();\n }, 300);\n\n this.dispatchEvent(Drawers.eventTypes.drawerHidden);\n }\n\n /**\n * Toggle visibility of the drawer.\n */\n toggleVisibility() {\n if (this.drawerNode.classList.contains(CLASSES.SHOW)) {\n this.closeDrawer();\n } else {\n this.openDrawer();\n }\n }\n\n /**\n * Displaces the drawer outsite the page.\n *\n * @param {Number} scrollPosition the page current scroll position\n */\n displace(scrollPosition) {\n let displace = scrollPosition;\n let openButton = getDrawerOpenButton(this.drawerNode.id);\n if (scrollPosition === 0) {\n this.drawerNode.style.transform = '';\n if (openButton) {\n openButton.style.transform = '';\n }\n return;\n }\n const state = this.drawerNode.dataset?.state;\n const drawrWidth = this.drawerNode.offsetWidth;\n let scrollThreshold = drawrWidth;\n let direction = -1;\n if (state === 'show-drawer-right') {\n direction = 1;\n scrollThreshold = THRESHOLD;\n }\n if (scrollPosition > scrollThreshold) {\n displace = drawrWidth + THRESHOLD;\n }\n displace *= direction;\n const transform = `translateX(${displace}px)`;\n if (openButton) {\n openButton.style.transform = transform;\n }\n this.drawerNode.style.transform = transform;\n }\n\n /**\n * Prevent drawer from overlapping an element.\n *\n * @param {HTMLElement} currentFocus\n */\n preventOverlap(currentFocus) {\n if (!this.isOpen) {\n return;\n }\n const drawrWidth = this.drawerNode.offsetWidth;\n const element = currentFocus.getBoundingClientRect();\n const drawer = this.boundingRect;\n const overlapping = (\n (element.right + THRESHOLD) > drawer.left &&\n (element.left - THRESHOLD) < drawer.right\n );\n if (overlapping) {\n // Force drawer to displace out of the page.\n this.displace(drawrWidth + 1);\n } else {\n // Reset drawer displacement.\n this.displace(window.scrollX);\n }\n }\n\n /**\n * Close all drawers.\n */\n static closeAllDrawers() {\n drawerMap.forEach(drawerInstance => {\n drawerInstance.closeDrawer();\n });\n }\n\n /**\n * Close all drawers except for the specified drawer.\n *\n * @param {module:theme_boost/drawers} comparisonInstance\n */\n static closeOtherDrawers(comparisonInstance) {\n drawerMap.forEach(drawerInstance => {\n if (drawerInstance === comparisonInstance) {\n return;\n }\n\n drawerInstance.closeDrawer();\n });\n }\n\n /**\n * Prevent drawers from covering the focused element.\n */\n static preventCoveringFocusedElement() {\n const currentFocus = document.activeElement;\n // Focus on page layout elements should be ignored.\n const pagecontent = document.querySelector(SELECTORS.PAGECONTENT);\n if (!currentFocus || !pagecontent?.contains(currentFocus)) {\n Drawers.displaceDrawers(window.scrollX);\n return;\n }\n drawerMap.forEach(drawerInstance => {\n drawerInstance.preventOverlap(currentFocus);\n });\n }\n\n /**\n * Prevent drawer from covering the content when the page content covers the full page.\n *\n * @param {Number} displace\n */\n static displaceDrawers(displace) {\n drawerMap.forEach(drawerInstance => {\n drawerInstance.displace(displace);\n });\n }\n}\n\n/**\n * Set the last used attribute for the last used toggle button for a drawer.\n *\n * @param {object} toggleButton The clicked button.\n */\nconst setLastUsedToggle = (toggleButton) => {\n if (toggleButton.dataset.target) {\n document.querySelectorAll(`${SELECTORS.BUTTONS}[data-target=\"${toggleButton.dataset.target}\"]`)\n .forEach(btn => {\n btn.dataset.lastused = false;\n });\n toggleButton.dataset.lastused = true;\n }\n};\n\n/**\n * Set the focus to the last used button to open this drawer.\n * @param {string} target The drawer target.\n */\nconst focusLastUsedToggle = (target) => {\n const lastUsedButton = document.querySelector(`${SELECTORS.BUTTONS}[data-target=\"${target}\"][data-lastused=\"true\"`);\n if (lastUsedButton) {\n lastUsedButton.focus();\n }\n};\n\n/**\n * Register the event listeners for the drawer.\n *\n * @private\n */\nconst registerListeners = () => {\n // Listen for show/hide events.\n document.addEventListener('click', e => {\n const toggleButton = e.target.closest(SELECTORS.TOGGLEBTN);\n if (toggleButton && toggleButton.dataset.target) {\n e.preventDefault();\n const targetDrawer = document.getElementById(toggleButton.dataset.target);\n const drawerInstance = Drawers.getDrawerInstanceForNode(targetDrawer);\n setLastUsedToggle(toggleButton);\n\n drawerInstance.toggleVisibility();\n }\n\n const openDrawerButton = e.target.closest(SELECTORS.OPENBTN);\n if (openDrawerButton && openDrawerButton.dataset.target) {\n e.preventDefault();\n const targetDrawer = document.getElementById(openDrawerButton.dataset.target);\n const drawerInstance = Drawers.getDrawerInstanceForNode(targetDrawer);\n setLastUsedToggle(toggleButton);\n\n drawerInstance.openDrawer();\n }\n\n const closeDrawerButton = e.target.closest(SELECTORS.CLOSEBTN);\n if (closeDrawerButton && closeDrawerButton.dataset.target) {\n e.preventDefault();\n const targetDrawer = document.getElementById(closeDrawerButton.dataset.target);\n const drawerInstance = Drawers.getDrawerInstanceForNode(targetDrawer);\n\n drawerInstance.closeDrawer();\n focusLastUsedToggle(closeDrawerButton.dataset.target);\n }\n });\n\n // Close drawer when another drawer opens.\n document.addEventListener(Drawers.eventTypes.drawerShow, e => {\n if (isLarge()) {\n return;\n }\n Drawers.closeOtherDrawers(e.detail.drawerInstance);\n });\n\n // Tooglers and openers blur listeners.\n const btnSelector = `${SELECTORS.TOGGLEBTN}, ${SELECTORS.OPENBTN}, ${SELECTORS.CLOSEBTN}`;\n document.addEventListener('focusout', (e) => {\n const button = e.target.closest(btnSelector);\n if (button?.dataset.restoreTooltipOnBlur !== undefined) {\n enableButtonTooltip(button);\n }\n });\n\n const closeOnResizeListener = () => {\n if (isSmall()) {\n let anyOpen = false;\n drawerMap.forEach(drawerInstance => {\n disableDrawerTooltips(drawerInstance.drawerNode);\n if (drawerInstance.isOpen) {\n if (drawerInstance.closeOnResize) {\n drawerInstance.closeDrawer();\n } else {\n anyOpen = true;\n }\n }\n });\n\n if (anyOpen) {\n getBackdrop().then(backdrop => backdrop.show()).catch();\n }\n } else {\n drawerMap.forEach(drawerInstance => {\n enableDrawerTooltips(drawerInstance.drawerNode);\n });\n getBackdrop().then(backdrop => backdrop.hide()).catch();\n }\n };\n\n document.addEventListener('scroll', () => {\n const body = document.querySelector('body');\n if (window.scrollY >= window.innerHeight) {\n body.classList.add(CLASSES.SCROLLED);\n } else {\n body.classList.remove(CLASSES.SCROLLED);\n }\n // Horizontal scroll listener to displace the drawers to prevent covering\n // any possible sticky content.\n Drawers.displaceDrawers(window.scrollX);\n });\n\n const preventOverlap = debounce(Drawers.preventCoveringFocusedElement, 100);\n document.addEventListener('focusin', preventOverlap);\n document.addEventListener('focusout', preventOverlap);\n\n window.addEventListener('resize', debounce(closeOnResizeListener, 400));\n};\n\nregisterListeners();\n\nconst drawers = document.querySelectorAll(SELECTORS.DRAWERS);\ndrawers.forEach(drawerNode => Drawers.getDrawerInstanceForNode(drawerNode));\n"],"names":["backdropPromise","drawerMap","Map","SELECTORS","CLASSES","getDrawerZIndex","drawer","document","querySelector","parseInt","window","getComputedStyle","zIndex","getBackdrop","Templates","render","then","html","ModalBackdrop","modalBackdrop","setZIndex","getAttachmentPoint","get","addEventListener","e","preventDefault","Drawers","closeAllDrawers","catch","getDrawerOpenButton","drawerId","openButton","disableDrawerTooltips","drawerNode","id","forEach","button","disableButtonTooltip","enableOnBlur","hasAttribute","tooltip","setAttribute","dataset","originalTitle","disabledToggle","toggle","removeAttribute","restoreTooltipOnBlur","enableButtonTooltip","constructor","undefined","behatFakeDrawer","closeDrawer","focusOnOpenButton","updatePreferences","this","classList","contains","openDrawer","focusOnCloseButton","forceopen","Aria","hide","content","scrollTop","addInnerScrollListener","set","remove","isOpen","closeOnResize","has","dispatchEvent","eventname","cancelable","drawerInstance","pendingPromise","Pending","eventTypes","drawerShow","defaultPrevented","unhide","add","preference","M","util","set_user_preference","state","getElementById","boundingRect","getBoundingClientRect","backdrop","show","style","overflow","closeButton","headerContent","setTimeout","focus","resolve","drawerShown","drawerHide","drawerHidden","toggleVisibility","displace","scrollPosition","transform","_this$drawerNode$data","drawrWidth","offsetWidth","scrollThreshold","direction","preventOverlap","currentFocus","element","right","left","scrollX","comparisonInstance","activeElement","pagecontent","displaceDrawers","setLastUsedToggle","toggleButton","target","querySelectorAll","btn","lastused","closest","targetDrawer","getDrawerInstanceForNode","openDrawerButton","closeDrawerButton","lastUsedButton","focusLastUsedToggle","closeOtherDrawers","detail","btnSelector","body","scrollY","innerHeight","preventCoveringFocusedElement","anyOpen","registerListeners"],"mappings":"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"} \ No newline at end of file diff --git a/theme/boost/amd/src/courseindexdrawercontrols.js b/theme/boost/amd/src/courseindexdrawercontrols.js new file mode 100644 index 00000000000..464c99e5691 --- /dev/null +++ b/theme/boost/amd/src/courseindexdrawercontrols.js @@ -0,0 +1,89 @@ +// 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 . + +/** + * Controls for the course index drawer, such as expand-all/collapse-all sections. + * + * @module theme_boost/courseindexdrawercontrols + * @copyright 2023 Stefan Topfstedt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +import {BaseComponent} from 'core/reactive'; +import {getCurrentCourseEditor} from 'core_courseformat/courseeditor'; + +export default class Component extends BaseComponent { + + create() { + this.name = 'courseindexdrawercontrols'; + this.selectors = { + COLLAPSEALL: `[data-action="collapseallcourseindexsections"]`, + EXPANDALL: `[data-action="expandallcourseindexsections"]`, + }; + } + + /** + * @param {element|string} target the DOM main element or its ID + * @param {object} selectors optional css selector overrides + * @return {Component} + */ + static init(target, selectors) { + return new Component({ + element: document.getElementById(target), + reactive: getCurrentCourseEditor(), + selectors, + }); + } + + /** + * Initial state ready method. + */ + stateReady() { + // Attach the on-click event handlers to the expand-all and collapse-all buttons, if present. + const expandAllBtn = this.getElement(this.selectors.EXPANDALL); + if (expandAllBtn) { + this.addEventListener(expandAllBtn, 'click', this._expandAllSections); + + } + const collapseAllBtn = this.getElement(this.selectors.COLLAPSEALL); + if (collapseAllBtn) { + this.addEventListener(collapseAllBtn, 'click', this._collapseAllSections); + } + } + + /** + * On-click event handler for the collapse-all button. + * @private + */ + _collapseAllSections() { + this._toggleAllSections(true); + } + + /** + * On-click event handler for the expand-all button. + * @private + */ + _expandAllSections() { + this._toggleAllSections(false); + } + + /** + * Collapses or expands all sections in the course index. + * @param {boolean} expandOrCollapse set to TRUE to collapse all, and FALSE to expand all. + * @private + */ + _toggleAllSections(expandOrCollapse) { + this.reactive.dispatch('allSectionsIndexCollapsed', expandOrCollapse); + } +} diff --git a/theme/boost/amd/src/drawers.js b/theme/boost/amd/src/drawers.js index d81429dab28..86680acfb89 100644 --- a/theme/boost/amd/src/drawers.js +++ b/theme/boost/amd/src/drawers.js @@ -42,6 +42,7 @@ const SELECTORS = { DRAWERS: '[data-region="fixed-drawer"]', DRAWERCONTENT: '.drawercontent', PAGECONTENT: '#page-content', + HEADERCONTENT: '.drawerheadercontent', }; const CLASSES = { @@ -421,8 +422,10 @@ export default class Drawers { return; } - // Hide close button while the drawer is showing to prevent glitchy effects. + // Hide close button and header content while the drawer is showing to prevent glitchy effects. this.drawerNode.querySelector(SELECTORS.CLOSEBTN)?.classList.toggle('hidden', true); + this.drawerNode.querySelector(SELECTORS.HEADERCONTENT)?.classList.toggle('hidden', true); + // Remove open tooltip if still visible. let openButton = getDrawerOpenButton(this.drawerNode.id); @@ -458,13 +461,15 @@ export default class Drawers { .catch(); } - // Show close button once the drawer is fully opened. + // Show close button and header content once the drawer is fully opened. const closeButton = this.drawerNode.querySelector(SELECTORS.CLOSEBTN); + const headerContent = this.drawerNode.querySelector(SELECTORS.HEADERCONTENT); if (focusOnCloseButton && closeButton) { disableButtonTooltip(closeButton, true); } setTimeout(() => { closeButton.classList.toggle('hidden', false); + headerContent.classList.toggle('hidden', false); if (focusOnCloseButton) { closeButton.focus(); } @@ -490,9 +495,11 @@ export default class Drawers { return; } - // Hide close button while the drawer is hiding to prevent glitchy effects. + // Hide close button and header content while the drawer is hiding to prevent glitchy effects. const closeButton = this.drawerNode.querySelector(SELECTORS.CLOSEBTN); closeButton?.classList.toggle('hidden', true); + const headerContent = this.drawerNode.querySelector(SELECTORS.HEADERCONTENT); + headerContent?.classList.toggle('hidden', true); // Remove the close button tooltip if visible. if (closeButton.hasAttribute('data-original-title')) { // The jQuery is still used in Boostrap 4. It can we removed when MDL-71979 is integrated. diff --git a/theme/boost/templates/courseindexdrawercontrols.mustache b/theme/boost/templates/courseindexdrawercontrols.mustache new file mode 100644 index 00000000000..8ac8f8bdbb7 --- /dev/null +++ b/theme/boost/templates/courseindexdrawercontrols.mustache @@ -0,0 +1,57 @@ +{{! + 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 . +}} +{{! + @template theme_boost/courseindexdrawercontrols + + This template will render "expand-all" and "collapse-all" buttons for the course index drawer. + + Example context (json): + { + } +}} + +{{#js}} + require(['theme_boost/courseindexdrawercontrols'], function(component) { + component.init('courseindexdrawercontrols'); + }); +{{/js}} diff --git a/theme/boost/templates/drawer.mustache b/theme/boost/templates/drawer.mustache index 5f944f3097d..24c75d779c6 100644 --- a/theme/boost/templates/drawer.mustache +++ b/theme/boost/templates/drawer.mustache @@ -47,6 +47,9 @@ > {{#pix}} e/cancel, core {{/pix}} +
{{$drawercontent}}{{/drawercontent}} diff --git a/theme/boost/templates/drawers.mustache b/theme/boost/templates/drawers.mustache index 3f1c7b185be..6c79bca70f4 100644 --- a/theme/boost/templates/drawers.mustache +++ b/theme/boost/templates/drawers.mustache @@ -61,6 +61,9 @@ {{#courseindex}} {{< theme_boost/drawer }} {{$id}}theme_boost-drawers-courseindex{{/id}} + {{$drawerheadercontent}} + {{> theme_boost/courseindexdrawercontrols}} + {{/drawerheadercontent}} {{$drawerclasses}}drawer drawer-left {{#courseindexopen}}show{{/courseindexopen}}{{/drawerclasses}} {{$drawercontent}} {{{courseindex}}}