diff --git a/course/format/amd/build/local/courseindex/courseindex.min.js b/course/format/amd/build/local/courseindex/courseindex.min.js index 9ad6f2fe452..42b1db1b693 100644 --- a/course/format/amd/build/local/courseindex/courseindex.min.js +++ b/course/format/amd/build/local/courseindex/courseindex.min.js @@ -6,6 +6,6 @@ define("core_courseformat/local/courseindex/courseindex",["exports","core/reacti * @class core_courseformat/local/courseindex/courseindex * @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,_jquery=_interopRequireDefault(_jquery),_contenttree=_interopRequireDefault(_contenttree);class Component extends _reactive.BaseComponent{create(){this.name="courseindex",this.selectors={SECTION:"[data-for='section']",SECTION_CMLIST:"[data-for='cmlist']",CM:"[data-for='cm']",TOGGLER:'[data-action="togglecourseindexsection"]',COLLAPSE:'[data-toggle="collapse"]',DRAWER:".drawer"},this.classes={SECTIONHIDDEN:"dimmed",CMHIDDEN:"dimmed",SECTIONCURRENT:"current",COLLAPSED:"collapsed",SHOW:"show"},this.sections={},this.cms={}}static init(target,selectors){return new this({element:document.getElementById(target),reactive:(0,_courseeditor.getCurrentCourseEditor)(),selectors:selectors})}stateReady(state){this.addEventListener(this.element,"click",this._sectionTogglers);this.getElements(this.selectors.SECTION).forEach((section=>{this.sections[section.dataset.id]=section}));this.getElements(this.selectors.CM).forEach((cm=>{this.cms[cm.dataset.id]=cm})),this._expandPageCmSectionIfNecessary(state),this._refreshPageItem({element:state.course,state:state}),this.contentTree=new _contenttree.default(this.element,this.selectors,this.reactive.isEditing)}getWatchers(){return[{watch:"section.indexcollapsed:updated",handler:this._refreshSectionCollapsed},{watch:"cm:created",handler:this._createCm},{watch:"cm:deleted",handler:this._deleteCm},{watch:"section:created",handler:this._createSection},{watch:"section:deleted",handler:this._deleteSection},{watch:"course.pageItem:created",handler:this._refreshPageItem},{watch:"course.pageItem:updated",handler:this._refreshPageItem},{watch:"course.sectionlist:updated",handler:this._refreshCourseSectionlist},{watch:"section.cmlist:updated",handler:this._refreshSectionCmlist}]}_sectionTogglers(event){const sectionlink=event.target.closest(this.selectors.TOGGLER),isChevron=event.target.closest(this.selectors.COLLAPSE);if(sectionlink||isChevron){var _toggler$classList$co;const section=event.target.closest(this.selectors.SECTION),toggler=section.querySelector(this.selectors.COLLAPSE),isCollapsed=null!==(_toggler$classList$co=null==toggler?void 0:toggler.classList.contains(this.classes.COLLAPSED))&&void 0!==_toggler$classList$co&&_toggler$classList$co,sectionId=section.getAttribute("data-id");sectionlink&&!isCollapsed||this.reactive.dispatch("sectionIndexCollapsed",[sectionId],!isCollapsed)}}_refreshSectionCollapsed(_ref){var _toggler$classList$co2;let{element:element}=_ref;const target=this.getElement(this.selectors.SECTION,element.id);if(!target)throw new Error("Unkown section with ID ".concat(element.id));const toggler=target.querySelector(this.selectors.COLLAPSE),isCollapsed=null!==(_toggler$classList$co2=null==toggler?void 0:toggler.classList.contains(this.classes.COLLAPSED))&&void 0!==_toggler$classList$co2&&_toggler$classList$co2;element.indexcollapsed!==isCollapsed&&this._expandSectionNode(element)}_expandSectionNode(element,forceValue){var _toggler$dataset$targ;const toggler=this.getElement(this.selectors.SECTION,element.id).querySelector(this.selectors.COLLAPSE);let collapsibleId=null!==(_toggler$dataset$targ=toggler.dataset.target)&&void 0!==_toggler$dataset$targ?_toggler$dataset$targ:toggler.getAttribute("href");if(!collapsibleId)return;collapsibleId=collapsibleId.replace("#","");const collapsible=document.getElementById(collapsibleId);if(!collapsible)return;void 0===forceValue&&(forceValue=!element.indexcollapsed);const togglerValue=forceValue?"show":"hide";(0,_jquery.default)(collapsible).collapse(togglerValue)}_refreshPageItem(_ref2){var _element$pageItem;let{element:element,state:state}=_ref2;if(null==element||null===(_element$pageItem=element.pageItem)||void 0===_element$pageItem||!_element$pageItem.isStatic||"cm"!=element.pageItem.type)return;const section=state.section.get(element.pageItem.sectionId);section.indexcollapsed&&(this._expandSectionNode(section,!0),setTimeout((()=>{var _this$cms$element$pag;return null===(_this$cms$element$pag=this.cms[element.pageItem.id])||void 0===_this$cms$element$pag?void 0:_this$cms$element$pag.scrollIntoView({block:"nearest"})}),250))}_expandPageCmSectionIfNecessary(state){const pageCmInfo=this.reactive.getPageAnchorCmInfo();pageCmInfo&&this._expandSectionNode(state.section.get(pageCmInfo.sectionid),!0)}async _createCm(_ref3){let{state:state,element:element}=_ref3;const fakeelement=document.createElement("li");fakeelement.classList.add("bg-pulse-grey","w-100"),fakeelement.innerHTML=" ",this.cms[element.id]=fakeelement,this._refreshSectionCmlist({state:state,element:state.section.get(element.sectionid)});const data=this.reactive.getExporter().cm(state,element),newelement=(await this.renderComponent(fakeelement,"core_courseformat/local/courseindex/cm",data)).getElement();this.cms[element.id]=newelement,fakeelement.parentNode.replaceChild(newelement,fakeelement)}async _createSection(_ref4){let{state:state,element:element}=_ref4;const fakeelement=document.createElement("div");fakeelement.classList.add("bg-pulse-grey","w-100"),fakeelement.innerHTML=" ",this.sections[element.id]=fakeelement,this._refreshCourseSectionlist({state:state,element:state.course});const data=this.reactive.getExporter().section(state,element),newelement=(await this.renderComponent(fakeelement,"core_courseformat/local/courseindex/section",data)).getElement();this.sections[element.id]=newelement,fakeelement.parentNode.replaceChild(newelement,fakeelement)}_refreshSectionCmlist(_ref5){var _element$cmlist;let{element:element}=_ref5;const cmlist=null!==(_element$cmlist=element.cmlist)&&void 0!==_element$cmlist?_element$cmlist:[],listparent=this.getElement(this.selectors.SECTION_CMLIST,element.id);this._fixOrder(listparent,cmlist,this.cms)}_refreshCourseSectionlist(_ref6){let{state:state}=_ref6;const sectionlist=this.reactive.getExporter().listedSectionIds(state);this._fixOrder(this.element,sectionlist,this.sections)}_fixOrder(container,neworder,allitems){if(!neworder.length)return container.classList.add("hidden"),void(container.innerHTML="");for(container.classList.remove("hidden"),neworder.forEach(((itemid,index)=>{const item=allitems[itemid],currentitem=container.children[index];void 0!==currentitem?currentitem!==item&&item&&container.insertBefore(item,currentitem):container.append(item)}));container.children.length>neworder.length;)container.removeChild(container.lastChild)}_deleteCm(_ref7){let{element:element}=_ref7;delete this.cms[element.id]}_deleteSection(_ref8){let{element:element}=_ref8;delete this.sections[element.id]}}return _exports.default=Component,_exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_jquery=_interopRequireDefault(_jquery),_contenttree=_interopRequireDefault(_contenttree);class Component extends _reactive.BaseComponent{create(){this.name="courseindex",this.selectors={SECTION:"[data-for='section']",SECTION_CMLIST:"[data-for='cmlist']",CM:"[data-for='cm']",TOGGLER:'[data-action="togglecourseindexsection"]',COLLAPSE:'[data-toggle="collapse"]',DRAWER:".drawer"},this.classes={SECTIONHIDDEN:"dimmed",CMHIDDEN:"dimmed",SECTIONCURRENT:"current",COLLAPSED:"collapsed",SHOW:"show"},this.sections={},this.cms={}}static init(target,selectors){return new this({element:document.getElementById(target),reactive:(0,_courseeditor.getCurrentCourseEditor)(),selectors:selectors})}stateReady(state){this.addEventListener(this.element,"click",this._sectionTogglers);this.getElements(this.selectors.SECTION).forEach((section=>{this.sections[section.dataset.id]=section}));this.getElements(this.selectors.CM).forEach((cm=>{this.cms[cm.dataset.id]=cm})),this._expandPageCmSectionIfNecessary(state),this._refreshPageItem({element:state.course,state:state}),this.contentTree=new _contenttree.default(this.element,this.selectors,this.reactive.isEditing)}getWatchers(){return[{watch:"section.indexcollapsed:updated",handler:this._refreshSectionCollapsed},{watch:"cm:created",handler:this._createCm},{watch:"cm:deleted",handler:this._deleteCm},{watch:"section:created",handler:this._createSection},{watch:"section:deleted",handler:this._deleteSection},{watch:"course.pageItem:created",handler:this._refreshPageItem},{watch:"course.pageItem:updated",handler:this._refreshPageItem},{watch:"course.sectionlist:updated",handler:this._refreshCourseSectionlist},{watch:"section.cmlist:updated",handler:this._refreshSectionCmlist}]}_sectionTogglers(event){const sectionlink=event.target.closest(this.selectors.TOGGLER),isChevron=event.target.closest(this.selectors.COLLAPSE);if(sectionlink||isChevron){var _toggler$classList$co;const section=event.target.closest(this.selectors.SECTION),toggler=section.querySelector(this.selectors.COLLAPSE),isCollapsed=null!==(_toggler$classList$co=null==toggler?void 0:toggler.classList.contains(this.classes.COLLAPSED))&&void 0!==_toggler$classList$co&&_toggler$classList$co,sectionId=section.getAttribute("data-id");sectionlink&&!isCollapsed||this.reactive.dispatch("sectionIndexCollapsed",[sectionId],!isCollapsed)}}_refreshSectionCollapsed(_ref){var _toggler$classList$co2;let{element:element}=_ref;const target=this.getElement(this.selectors.SECTION,element.id);if(!target)throw new Error("Unkown section with ID ".concat(element.id));const toggler=target.querySelector(this.selectors.COLLAPSE),isCollapsed=null!==(_toggler$classList$co2=null==toggler?void 0:toggler.classList.contains(this.classes.COLLAPSED))&&void 0!==_toggler$classList$co2&&_toggler$classList$co2;element.indexcollapsed!==isCollapsed&&this._expandSectionNode(element)}_expandSectionNode(element,forceValue){var _toggler$dataset$targ;const toggler=this.getElement(this.selectors.SECTION,element.id).querySelector(this.selectors.COLLAPSE);let collapsibleId=null!==(_toggler$dataset$targ=toggler.dataset.target)&&void 0!==_toggler$dataset$targ?_toggler$dataset$targ:toggler.getAttribute("href");if(!collapsibleId)return;collapsibleId=collapsibleId.replace("#","");const collapsible=document.getElementById(collapsibleId);if(!collapsible)return;void 0===forceValue&&(forceValue=!element.indexcollapsed);const togglerValue=forceValue?"show":"hide";(0,_jquery.default)(collapsible).collapse(togglerValue)}_refreshPageItem(_ref2){var _element$pageItem;let{element:element,state:state}=_ref2;if(null==element||null===(_element$pageItem=element.pageItem)||void 0===_element$pageItem||!_element$pageItem.isStatic||"cm"!=element.pageItem.type)return;const section=state.section.get(element.pageItem.sectionId);section.indexcollapsed&&(this._expandSectionNode(section,!0),setTimeout((()=>{var _this$cms$element$pag;return null===(_this$cms$element$pag=this.cms[element.pageItem.id])||void 0===_this$cms$element$pag?void 0:_this$cms$element$pag.scrollIntoView({block:"nearest"})}),250))}_expandPageCmSectionIfNecessary(state){const pageCmInfo=this.reactive.getPageAnchorCmInfo();pageCmInfo&&this._expandSectionNode(state.section.get(pageCmInfo.sectionid),!0)}async _createCm(_ref3){let{state:state,element:element}=_ref3;const fakeelement=document.createElement("li");fakeelement.classList.add("bg-pulse-grey","w-100"),fakeelement.innerHTML=" ",this.cms[element.id]=fakeelement,this._refreshSectionCmlist({state:state,element:state.section.get(element.sectionid)});const data=this.reactive.getExporter().cm(state,element),newelement=(await this.renderComponent(fakeelement,"core_courseformat/local/courseindex/cm",data)).getElement();this.cms[element.id]=newelement,fakeelement.parentNode.replaceChild(newelement,fakeelement)}async _createSection(_ref4){let{state:state,element:element}=_ref4;const fakeelement=document.createElement("div");fakeelement.classList.add("bg-pulse-grey","w-100"),fakeelement.innerHTML=" ",this.sections[element.id]=fakeelement,this._refreshCourseSectionlist({state:state,element:state.course});const data=this.reactive.getExporter().section(state,element),newelement=(await this.renderComponent(fakeelement,"core_courseformat/local/courseindex/section",data)).getElement();this.sections[element.id]=newelement,fakeelement.parentNode.replaceChild(newelement,fakeelement)}_refreshSectionCmlist(_ref5){var _element$cmlist;let{element:element}=_ref5;const cmlist=null!==(_element$cmlist=element.cmlist)&&void 0!==_element$cmlist?_element$cmlist:[],listparent=this.getElement(this.selectors.SECTION_CMLIST,element.id);listparent&&this._fixOrder(listparent,cmlist,this.cms)}_refreshCourseSectionlist(_ref6){let{state:state}=_ref6;const sectionlist=this.reactive.getExporter().listedSectionIds(state);this._fixOrder(this.element,sectionlist,this.sections)}_fixOrder(container,neworder,allitems){if(!neworder.length)return container.classList.add("hidden"),void(container.innerHTML="");for(container.classList.remove("hidden"),neworder.forEach(((itemid,index)=>{const item=allitems[itemid],currentitem=container.children[index];void 0!==currentitem?currentitem!==item&&item&&container.insertBefore(item,currentitem):container.append(item)}));container.children.length>neworder.length;)container.removeChild(container.lastChild)}_deleteCm(_ref7){let{element:element}=_ref7;delete this.cms[element.id]}_deleteSection(_ref8){let{element:element}=_ref8;delete this.sections[element.id]}}return _exports.default=Component,_exports.default})); //# sourceMappingURL=courseindex.min.js.map \ No newline at end of file diff --git a/course/format/amd/build/local/courseindex/courseindex.min.js.map b/course/format/amd/build/local/courseindex/courseindex.min.js.map index 9301b660f0b..eb08b2c7116 100644 --- a/course/format/amd/build/local/courseindex/courseindex.min.js.map +++ b/course/format/amd/build/local/courseindex/courseindex.min.js.map @@ -1 +1 @@ -{"version":3,"file":"courseindex.min.js","sources":["../../../src/local/courseindex/courseindex.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 * Course index main component.\n *\n * @module core_courseformat/local/courseindex/courseindex\n * @class core_courseformat/local/courseindex/courseindex\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {getCurrentCourseEditor} from 'core_courseformat/courseeditor';\nimport jQuery from 'jquery';\nimport ContentTree from 'core_courseformat/local/courseeditor/contenttree';\n\nexport default class Component extends BaseComponent {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'courseindex';\n // Default query selectors.\n this.selectors = {\n SECTION: `[data-for='section']`,\n SECTION_CMLIST: `[data-for='cmlist']`,\n CM: `[data-for='cm']`,\n TOGGLER: `[data-action=\"togglecourseindexsection\"]`,\n COLLAPSE: `[data-toggle=\"collapse\"]`,\n DRAWER: `.drawer`,\n };\n // Default classes to toggle on refresh.\n this.classes = {\n SECTIONHIDDEN: 'dimmed',\n CMHIDDEN: 'dimmed',\n SECTIONCURRENT: 'current',\n COLLAPSED: `collapsed`,\n SHOW: `show`,\n };\n // Arrays to keep cms and sections elements.\n this.sections = {};\n this.cms = {};\n }\n\n /**\n * Static method to create a component instance form the mustache template.\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 this({\n element: document.getElementById(target),\n reactive: getCurrentCourseEditor(),\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {Object} state the state data\n */\n stateReady(state) {\n // Activate section togglers.\n this.addEventListener(this.element, 'click', this._sectionTogglers);\n\n // Get cms and sections elements.\n const sections = this.getElements(this.selectors.SECTION);\n sections.forEach((section) => {\n this.sections[section.dataset.id] = section;\n });\n const cms = this.getElements(this.selectors.CM);\n cms.forEach((cm) => {\n this.cms[cm.dataset.id] = cm;\n });\n\n this._expandPageCmSectionIfNecessary(state);\n this._refreshPageItem({element: state.course, state});\n\n // Configure Aria Tree.\n this.contentTree = new ContentTree(this.element, this.selectors, this.reactive.isEditing);\n }\n\n getWatchers() {\n return [\n {watch: `section.indexcollapsed:updated`, handler: this._refreshSectionCollapsed},\n {watch: `cm:created`, handler: this._createCm},\n {watch: `cm:deleted`, handler: this._deleteCm},\n {watch: `section:created`, handler: this._createSection},\n {watch: `section:deleted`, handler: this._deleteSection},\n {watch: `course.pageItem:created`, handler: this._refreshPageItem},\n {watch: `course.pageItem:updated`, handler: this._refreshPageItem},\n // Sections and cm sorting.\n {watch: `course.sectionlist:updated`, handler: this._refreshCourseSectionlist},\n {watch: `section.cmlist:updated`, handler: this._refreshSectionCmlist},\n ];\n }\n\n /**\n * Setup sections toggler.\n *\n * Toggler click is delegated to the main course index element because new sections can\n * appear at any moment and this way we prevent accidental double bindings.\n *\n * @param {Event} event the triggered event\n */\n _sectionTogglers(event) {\n const sectionlink = event.target.closest(this.selectors.TOGGLER);\n const isChevron = event.target.closest(this.selectors.COLLAPSE);\n\n if (sectionlink || isChevron) {\n\n const section = event.target.closest(this.selectors.SECTION);\n const toggler = section.querySelector(this.selectors.COLLAPSE);\n const isCollapsed = toggler?.classList.contains(this.classes.COLLAPSED) ?? false;\n\n // Update the state.\n const sectionId = section.getAttribute('data-id');\n if (!sectionlink || isCollapsed) {\n this.reactive.dispatch(\n 'sectionIndexCollapsed',\n [sectionId],\n !isCollapsed\n );\n }\n }\n }\n\n /**\n * Update section collapsed.\n *\n * @param {object} args\n * @param {object} args.element The leement to be expanded\n */\n _refreshSectionCollapsed({element}) {\n const target = this.getElement(this.selectors.SECTION, element.id);\n if (!target) {\n throw new Error(`Unkown section with ID ${element.id}`);\n }\n // Check if it is already done.\n const toggler = target.querySelector(this.selectors.COLLAPSE);\n const isCollapsed = toggler?.classList.contains(this.classes.COLLAPSED) ?? false;\n\n if (element.indexcollapsed !== isCollapsed) {\n this._expandSectionNode(element);\n }\n }\n\n /**\n * Expand a section node.\n *\n * By default the method will use element.indexcollapsed to decide if the\n * section is opened or closed. However, using forceValue it is possible\n * to open or close a section independant from the indexcollapsed attribute.\n *\n * @param {Object} element the course module state element\n * @param {boolean} forceValue optional forced expanded value\n */\n _expandSectionNode(element, forceValue) {\n const target = this.getElement(this.selectors.SECTION, element.id);\n const toggler = target.querySelector(this.selectors.COLLAPSE);\n let collapsibleId = toggler.dataset.target ?? toggler.getAttribute(\"href\");\n if (!collapsibleId) {\n return;\n }\n collapsibleId = collapsibleId.replace('#', '');\n const collapsible = document.getElementById(collapsibleId);\n if (!collapsible) {\n return;\n }\n\n if (forceValue === undefined) {\n forceValue = (element.indexcollapsed) ? false : true;\n }\n\n // Course index is based on Bootstrap 4 collapsibles. To collapse them we need jQuery to\n // interact with collapsibles methods. Hopefully, this will change in Bootstrap 5 because\n // it does not require jQuery anymore (when MDL-71979 is integrated).\n const togglerValue = (forceValue) ? 'show' : 'hide';\n jQuery(collapsible).collapse(togglerValue);\n }\n\n /**\n * Handle a page item update.\n *\n * @param {Object} details the update details\n * @param {Object} details.state the state data.\n * @param {Object} details.element the course state data.\n */\n _refreshPageItem({element, state}) {\n if (!element?.pageItem?.isStatic || element.pageItem.type != 'cm') {\n return;\n }\n // Check if we need to uncollapse the section and scroll to the element.\n const section = state.section.get(element.pageItem.sectionId);\n if (section.indexcollapsed) {\n this._expandSectionNode(section, true);\n setTimeout(\n () => this.cms[element.pageItem.id]?.scrollIntoView({block: \"nearest\"}),\n 250\n );\n }\n }\n\n /**\n * Expand a section if the current page is a section's cm.\n *\n * @private\n * @param {Object} state the course state.\n */\n _expandPageCmSectionIfNecessary(state) {\n const pageCmInfo = this.reactive.getPageAnchorCmInfo();\n if (!pageCmInfo) {\n return;\n }\n this._expandSectionNode(state.section.get(pageCmInfo.sectionid), true);\n }\n\n /**\n * Create a newcm instance.\n *\n * @param {object} param\n * @param {Object} param.state\n * @param {Object} param.element\n */\n async _createCm({state, element}) {\n // Create a fake node while the component is loading.\n const fakeelement = document.createElement('li');\n fakeelement.classList.add('bg-pulse-grey', 'w-100');\n fakeelement.innerHTML = ' ';\n this.cms[element.id] = fakeelement;\n // Place the fake node on the correct position.\n this._refreshSectionCmlist({\n state,\n element: state.section.get(element.sectionid),\n });\n // Collect render data.\n const exporter = this.reactive.getExporter();\n const data = exporter.cm(state, element);\n // Create the new content.\n const newcomponent = await this.renderComponent(fakeelement, 'core_courseformat/local/courseindex/cm', data);\n // Replace the fake node with the real content.\n const newelement = newcomponent.getElement();\n this.cms[element.id] = newelement;\n fakeelement.parentNode.replaceChild(newelement, fakeelement);\n }\n\n /**\n * Create a new section instance.\n *\n * @param {Object} details the update details.\n * @param {Object} details.state the state data.\n * @param {Object} details.element the element data.\n */\n async _createSection({state, element}) {\n // Create a fake node while the component is loading.\n const fakeelement = document.createElement('div');\n fakeelement.classList.add('bg-pulse-grey', 'w-100');\n fakeelement.innerHTML = ' ';\n this.sections[element.id] = fakeelement;\n // Place the fake node on the correct position.\n this._refreshCourseSectionlist({\n state,\n element: state.course,\n });\n // Collect render data.\n const exporter = this.reactive.getExporter();\n const data = exporter.section(state, element);\n // Create the new content.\n const newcomponent = await this.renderComponent(fakeelement, 'core_courseformat/local/courseindex/section', data);\n // Replace the fake node with the real content.\n const newelement = newcomponent.getElement();\n this.sections[element.id] = newelement;\n fakeelement.parentNode.replaceChild(newelement, fakeelement);\n }\n\n /**\n * Refresh a section cm list.\n *\n * @param {object} param\n * @param {Object} param.element\n */\n _refreshSectionCmlist({element}) {\n const cmlist = element.cmlist ?? [];\n const listparent = this.getElement(this.selectors.SECTION_CMLIST, element.id);\n this._fixOrder(listparent, cmlist, this.cms);\n }\n\n /**\n * Refresh the section list.\n *\n * @param {object} param\n * @param {Object} param.state\n */\n _refreshCourseSectionlist({state}) {\n const sectionlist = this.reactive.getExporter().listedSectionIds(state);\n this._fixOrder(this.element, sectionlist, this.sections);\n }\n\n /**\n * Fix/reorder the section or cms order.\n *\n * @param {Element} container the HTML element to reorder.\n * @param {Array} neworder an array with the ids order\n * @param {Array} allitems the list of html elements that can be placed in the container\n */\n _fixOrder(container, neworder, allitems) {\n\n // Empty lists should not be visible.\n if (!neworder.length) {\n container.classList.add('hidden');\n container.innerHTML = '';\n return;\n }\n\n // Grant the list is visible (in case it was empty).\n container.classList.remove('hidden');\n\n // Move the elements in order at the beginning of the list.\n neworder.forEach((itemid, index) => {\n const item = allitems[itemid];\n // Get the current element at that position.\n const currentitem = container.children[index];\n if (currentitem === undefined) {\n container.append(item);\n return;\n }\n if (currentitem !== item && item) {\n container.insertBefore(item, currentitem);\n }\n });\n // Remove the remaining elements.\n while (container.children.length > neworder.length) {\n container.removeChild(container.lastChild);\n }\n }\n\n /**\n * Remove a cm from the list.\n *\n * The actual DOM element removal is delegated to the cm component.\n *\n * @param {object} param\n * @param {Object} param.element\n */\n _deleteCm({element}) {\n delete this.cms[element.id];\n }\n\n /**\n * Remove a section from the list.\n *\n * The actual DOM element removal is delegated to the section component.\n *\n * @param {Object} details the update details.\n * @param {Object} details.element the element data.\n */\n _deleteSection({element}) {\n delete this.sections[element.id];\n }\n}\n"],"names":["Component","BaseComponent","create","name","selectors","SECTION","SECTION_CMLIST","CM","TOGGLER","COLLAPSE","DRAWER","classes","SECTIONHIDDEN","CMHIDDEN","SECTIONCURRENT","COLLAPSED","SHOW","sections","cms","target","this","element","document","getElementById","reactive","stateReady","state","addEventListener","_sectionTogglers","getElements","forEach","section","dataset","id","cm","_expandPageCmSectionIfNecessary","_refreshPageItem","course","contentTree","ContentTree","isEditing","getWatchers","watch","handler","_refreshSectionCollapsed","_createCm","_deleteCm","_createSection","_deleteSection","_refreshCourseSectionlist","_refreshSectionCmlist","event","sectionlink","closest","isChevron","toggler","querySelector","isCollapsed","classList","contains","sectionId","getAttribute","dispatch","getElement","Error","indexcollapsed","_expandSectionNode","forceValue","collapsibleId","replace","collapsible","undefined","togglerValue","collapse","pageItem","_element$pageItem","isStatic","type","get","setTimeout","_this$cms$element$pag","scrollIntoView","block","pageCmInfo","getPageAnchorCmInfo","sectionid","fakeelement","createElement","add","innerHTML","data","getExporter","newelement","renderComponent","parentNode","replaceChild","cmlist","listparent","_fixOrder","sectionlist","listedSectionIds","container","neworder","allitems","length","remove","itemid","index","item","currentitem","children","insertBefore","append","removeChild","lastChild"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"courseindex.min.js","sources":["../../../src/local/courseindex/courseindex.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 * Course index main component.\n *\n * @module core_courseformat/local/courseindex/courseindex\n * @class core_courseformat/local/courseindex/courseindex\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {getCurrentCourseEditor} from 'core_courseformat/courseeditor';\nimport jQuery from 'jquery';\nimport ContentTree from 'core_courseformat/local/courseeditor/contenttree';\n\nexport default class Component extends BaseComponent {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'courseindex';\n // Default query selectors.\n this.selectors = {\n SECTION: `[data-for='section']`,\n SECTION_CMLIST: `[data-for='cmlist']`,\n CM: `[data-for='cm']`,\n TOGGLER: `[data-action=\"togglecourseindexsection\"]`,\n COLLAPSE: `[data-toggle=\"collapse\"]`,\n DRAWER: `.drawer`,\n };\n // Default classes to toggle on refresh.\n this.classes = {\n SECTIONHIDDEN: 'dimmed',\n CMHIDDEN: 'dimmed',\n SECTIONCURRENT: 'current',\n COLLAPSED: `collapsed`,\n SHOW: `show`,\n };\n // Arrays to keep cms and sections elements.\n this.sections = {};\n this.cms = {};\n }\n\n /**\n * Static method to create a component instance form the mustache template.\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 this({\n element: document.getElementById(target),\n reactive: getCurrentCourseEditor(),\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {Object} state the state data\n */\n stateReady(state) {\n // Activate section togglers.\n this.addEventListener(this.element, 'click', this._sectionTogglers);\n\n // Get cms and sections elements.\n const sections = this.getElements(this.selectors.SECTION);\n sections.forEach((section) => {\n this.sections[section.dataset.id] = section;\n });\n const cms = this.getElements(this.selectors.CM);\n cms.forEach((cm) => {\n this.cms[cm.dataset.id] = cm;\n });\n\n this._expandPageCmSectionIfNecessary(state);\n this._refreshPageItem({element: state.course, state});\n\n // Configure Aria Tree.\n this.contentTree = new ContentTree(this.element, this.selectors, this.reactive.isEditing);\n }\n\n getWatchers() {\n return [\n {watch: `section.indexcollapsed:updated`, handler: this._refreshSectionCollapsed},\n {watch: `cm:created`, handler: this._createCm},\n {watch: `cm:deleted`, handler: this._deleteCm},\n {watch: `section:created`, handler: this._createSection},\n {watch: `section:deleted`, handler: this._deleteSection},\n {watch: `course.pageItem:created`, handler: this._refreshPageItem},\n {watch: `course.pageItem:updated`, handler: this._refreshPageItem},\n // Sections and cm sorting.\n {watch: `course.sectionlist:updated`, handler: this._refreshCourseSectionlist},\n {watch: `section.cmlist:updated`, handler: this._refreshSectionCmlist},\n ];\n }\n\n /**\n * Setup sections toggler.\n *\n * Toggler click is delegated to the main course index element because new sections can\n * appear at any moment and this way we prevent accidental double bindings.\n *\n * @param {Event} event the triggered event\n */\n _sectionTogglers(event) {\n const sectionlink = event.target.closest(this.selectors.TOGGLER);\n const isChevron = event.target.closest(this.selectors.COLLAPSE);\n\n if (sectionlink || isChevron) {\n\n const section = event.target.closest(this.selectors.SECTION);\n const toggler = section.querySelector(this.selectors.COLLAPSE);\n const isCollapsed = toggler?.classList.contains(this.classes.COLLAPSED) ?? false;\n\n // Update the state.\n const sectionId = section.getAttribute('data-id');\n if (!sectionlink || isCollapsed) {\n this.reactive.dispatch(\n 'sectionIndexCollapsed',\n [sectionId],\n !isCollapsed\n );\n }\n }\n }\n\n /**\n * Update section collapsed.\n *\n * @param {object} args\n * @param {object} args.element The leement to be expanded\n */\n _refreshSectionCollapsed({element}) {\n const target = this.getElement(this.selectors.SECTION, element.id);\n if (!target) {\n throw new Error(`Unkown section with ID ${element.id}`);\n }\n // Check if it is already done.\n const toggler = target.querySelector(this.selectors.COLLAPSE);\n const isCollapsed = toggler?.classList.contains(this.classes.COLLAPSED) ?? false;\n\n if (element.indexcollapsed !== isCollapsed) {\n this._expandSectionNode(element);\n }\n }\n\n /**\n * Expand a section node.\n *\n * By default the method will use element.indexcollapsed to decide if the\n * section is opened or closed. However, using forceValue it is possible\n * to open or close a section independant from the indexcollapsed attribute.\n *\n * @param {Object} element the course module state element\n * @param {boolean} forceValue optional forced expanded value\n */\n _expandSectionNode(element, forceValue) {\n const target = this.getElement(this.selectors.SECTION, element.id);\n const toggler = target.querySelector(this.selectors.COLLAPSE);\n let collapsibleId = toggler.dataset.target ?? toggler.getAttribute(\"href\");\n if (!collapsibleId) {\n return;\n }\n collapsibleId = collapsibleId.replace('#', '');\n const collapsible = document.getElementById(collapsibleId);\n if (!collapsible) {\n return;\n }\n\n if (forceValue === undefined) {\n forceValue = (element.indexcollapsed) ? false : true;\n }\n\n // Course index is based on Bootstrap 4 collapsibles. To collapse them we need jQuery to\n // interact with collapsibles methods. Hopefully, this will change in Bootstrap 5 because\n // it does not require jQuery anymore (when MDL-71979 is integrated).\n const togglerValue = (forceValue) ? 'show' : 'hide';\n jQuery(collapsible).collapse(togglerValue);\n }\n\n /**\n * Handle a page item update.\n *\n * @param {Object} details the update details\n * @param {Object} details.state the state data.\n * @param {Object} details.element the course state data.\n */\n _refreshPageItem({element, state}) {\n if (!element?.pageItem?.isStatic || element.pageItem.type != 'cm') {\n return;\n }\n // Check if we need to uncollapse the section and scroll to the element.\n const section = state.section.get(element.pageItem.sectionId);\n if (section.indexcollapsed) {\n this._expandSectionNode(section, true);\n setTimeout(\n () => this.cms[element.pageItem.id]?.scrollIntoView({block: \"nearest\"}),\n 250\n );\n }\n }\n\n /**\n * Expand a section if the current page is a section's cm.\n *\n * @private\n * @param {Object} state the course state.\n */\n _expandPageCmSectionIfNecessary(state) {\n const pageCmInfo = this.reactive.getPageAnchorCmInfo();\n if (!pageCmInfo) {\n return;\n }\n this._expandSectionNode(state.section.get(pageCmInfo.sectionid), true);\n }\n\n /**\n * Create a newcm instance.\n *\n * @param {object} param\n * @param {Object} param.state\n * @param {Object} param.element\n */\n async _createCm({state, element}) {\n // Create a fake node while the component is loading.\n const fakeelement = document.createElement('li');\n fakeelement.classList.add('bg-pulse-grey', 'w-100');\n fakeelement.innerHTML = ' ';\n this.cms[element.id] = fakeelement;\n // Place the fake node on the correct position.\n this._refreshSectionCmlist({\n state,\n element: state.section.get(element.sectionid),\n });\n // Collect render data.\n const exporter = this.reactive.getExporter();\n const data = exporter.cm(state, element);\n // Create the new content.\n const newcomponent = await this.renderComponent(fakeelement, 'core_courseformat/local/courseindex/cm', data);\n // Replace the fake node with the real content.\n const newelement = newcomponent.getElement();\n this.cms[element.id] = newelement;\n fakeelement.parentNode.replaceChild(newelement, fakeelement);\n }\n\n /**\n * Create a new section instance.\n *\n * @param {Object} details the update details.\n * @param {Object} details.state the state data.\n * @param {Object} details.element the element data.\n */\n async _createSection({state, element}) {\n // Create a fake node while the component is loading.\n const fakeelement = document.createElement('div');\n fakeelement.classList.add('bg-pulse-grey', 'w-100');\n fakeelement.innerHTML = ' ';\n this.sections[element.id] = fakeelement;\n // Place the fake node on the correct position.\n this._refreshCourseSectionlist({\n state,\n element: state.course,\n });\n // Collect render data.\n const exporter = this.reactive.getExporter();\n const data = exporter.section(state, element);\n // Create the new content.\n const newcomponent = await this.renderComponent(fakeelement, 'core_courseformat/local/courseindex/section', data);\n // Replace the fake node with the real content.\n const newelement = newcomponent.getElement();\n this.sections[element.id] = newelement;\n fakeelement.parentNode.replaceChild(newelement, fakeelement);\n }\n\n /**\n * Refresh a section cm list.\n *\n * @param {object} param\n * @param {Object} param.element\n */\n _refreshSectionCmlist({element}) {\n const cmlist = element.cmlist ?? [];\n const listparent = this.getElement(this.selectors.SECTION_CMLIST, element.id);\n if (!listparent) {\n return;\n }\n this._fixOrder(listparent, cmlist, this.cms);\n }\n\n /**\n * Refresh the section list.\n *\n * @param {object} param\n * @param {Object} param.state\n */\n _refreshCourseSectionlist({state}) {\n const sectionlist = this.reactive.getExporter().listedSectionIds(state);\n this._fixOrder(this.element, sectionlist, this.sections);\n }\n\n /**\n * Fix/reorder the section or cms order.\n *\n * @param {Element} container the HTML element to reorder.\n * @param {Array} neworder an array with the ids order\n * @param {Array} allitems the list of html elements that can be placed in the container\n */\n _fixOrder(container, neworder, allitems) {\n\n // Empty lists should not be visible.\n if (!neworder.length) {\n container.classList.add('hidden');\n container.innerHTML = '';\n return;\n }\n\n // Grant the list is visible (in case it was empty).\n container.classList.remove('hidden');\n\n // Move the elements in order at the beginning of the list.\n neworder.forEach((itemid, index) => {\n const item = allitems[itemid];\n // Get the current element at that position.\n const currentitem = container.children[index];\n if (currentitem === undefined) {\n container.append(item);\n return;\n }\n if (currentitem !== item && item) {\n container.insertBefore(item, currentitem);\n }\n });\n // Remove the remaining elements.\n while (container.children.length > neworder.length) {\n container.removeChild(container.lastChild);\n }\n }\n\n /**\n * Remove a cm from the list.\n *\n * The actual DOM element removal is delegated to the cm component.\n *\n * @param {object} param\n * @param {Object} param.element\n */\n _deleteCm({element}) {\n delete this.cms[element.id];\n }\n\n /**\n * Remove a section from the list.\n *\n * The actual DOM element removal is delegated to the section component.\n *\n * @param {Object} details the update details.\n * @param {Object} details.element the element data.\n */\n _deleteSection({element}) {\n delete this.sections[element.id];\n }\n}\n"],"names":["Component","BaseComponent","create","name","selectors","SECTION","SECTION_CMLIST","CM","TOGGLER","COLLAPSE","DRAWER","classes","SECTIONHIDDEN","CMHIDDEN","SECTIONCURRENT","COLLAPSED","SHOW","sections","cms","target","this","element","document","getElementById","reactive","stateReady","state","addEventListener","_sectionTogglers","getElements","forEach","section","dataset","id","cm","_expandPageCmSectionIfNecessary","_refreshPageItem","course","contentTree","ContentTree","isEditing","getWatchers","watch","handler","_refreshSectionCollapsed","_createCm","_deleteCm","_createSection","_deleteSection","_refreshCourseSectionlist","_refreshSectionCmlist","event","sectionlink","closest","isChevron","toggler","querySelector","isCollapsed","classList","contains","sectionId","getAttribute","dispatch","getElement","Error","indexcollapsed","_expandSectionNode","forceValue","collapsibleId","replace","collapsible","undefined","togglerValue","collapse","pageItem","_element$pageItem","isStatic","type","get","setTimeout","_this$cms$element$pag","scrollIntoView","block","pageCmInfo","getPageAnchorCmInfo","sectionid","fakeelement","createElement","add","innerHTML","data","getExporter","newelement","renderComponent","parentNode","replaceChild","cmlist","listparent","_fixOrder","sectionlist","listedSectionIds","container","neworder","allitems","length","remove","itemid","index","item","currentitem","children","insertBefore","append","removeChild","lastChild"],"mappings":"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"} \ No newline at end of file diff --git a/course/format/amd/src/local/courseindex/courseindex.js b/course/format/amd/src/local/courseindex/courseindex.js index e070de2eed9..b2f2e9c16a0 100644 --- a/course/format/amd/src/local/courseindex/courseindex.js +++ b/course/format/amd/src/local/courseindex/courseindex.js @@ -300,6 +300,9 @@ export default class Component extends BaseComponent { _refreshSectionCmlist({element}) { const cmlist = element.cmlist ?? []; const listparent = this.getElement(this.selectors.SECTION_CMLIST, element.id); + if (!listparent) { + return; + } this._fixOrder(listparent, cmlist, this.cms); } diff --git a/course/format/classes/output/local/content.php b/course/format/classes/output/local/content.php index b5121a5a2a7..25cf37cde4a 100644 --- a/course/format/classes/output/local/content.php +++ b/course/format/classes/output/local/content.php @@ -19,6 +19,7 @@ namespace core_courseformat\output\local; use core\output\named_templatable; use core_courseformat\base as course_format; use course_modinfo; +use section_info; use renderable; /** @@ -142,7 +143,7 @@ class content implements named_templatable, renderable { /** * Export sections array data. * - * @param renderer_base $output typically, the renderer that's calling this function + * @param \renderer_base $output typically, the renderer that's calling this function * @return array data context for a mustache template */ protected function export_sections(\renderer_base $output): array { @@ -154,7 +155,6 @@ class content implements named_templatable, renderable { // Generate section list. $sections = []; $stealthsections = []; - $numsections = $format->get_last_section_number(); foreach ($this->get_sections_to_display($modinfo) as $sectionnum => $thissection) { // The course/view.php check the section existence but the output can be called // from other parts so we need to check it. @@ -163,9 +163,14 @@ class content implements named_templatable, renderable { format_string($course->fullname)); } + if (!$format->is_section_visible($thissection)) { + continue; + } + + /** @var \core_courseformat\output\local\content\section $section */ $section = new $this->sectionclass($format, $thissection); - if ($sectionnum > $numsections) { + if ($section->is_stealth()) { // Activities inside this section are 'orphaned', this section will be printed as 'stealth' below. if (!empty($modinfo->sections[$sectionnum])) { $stealthsections[] = $section->export_for_template($output); @@ -173,10 +178,6 @@ class content implements named_templatable, renderable { continue; } - if (!$format->is_section_visible($thissection)) { - continue; - } - $sections[] = $section->export_for_template($output); } if (!empty($stealthsections)) { diff --git a/course/format/classes/output/local/content/section.php b/course/format/classes/output/local/content/section.php index cb96d5f8be3..0f6928b5145 100644 --- a/course/format/classes/output/local/content/section.php +++ b/course/format/classes/output/local/content/section.php @@ -95,9 +95,7 @@ class section implements named_templatable, renderable { $this->format = $format; $this->section = $section; - if ($section->section > $format->get_last_section_number()) { - $this->isstealth = true; - } + $this->isstealth = $section->is_orphan(); // Load output classes names from format. $this->headerclass = $format->get_output_classname('content\\section\\header'); @@ -110,6 +108,15 @@ class section implements named_templatable, renderable { $this->visibilityclass = $format->get_output_classname('content\\section\\visibility'); } + /** + * Check if the section is considered stealth. + * + * @return bool + */ + public function is_stealth(): bool { + return $this->isstealth; + } + /** * Hide the section title. * diff --git a/course/format/classes/output/local/content/section/controlmenu.php b/course/format/classes/output/local/content/section/controlmenu.php index 7b18968d5fa..13e72a6315f 100644 --- a/course/format/classes/output/local/content/section/controlmenu.php +++ b/course/format/classes/output/local/content/section/controlmenu.php @@ -98,7 +98,7 @@ class controlmenu extends basecontrolmenu { $usecomponents = $format->supports_components(); $coursecontext = context_course::instance($course->id); $numsections = $format->get_last_section_number(); - $isstealth = $section->section > $numsections; + $isstealth = $section->is_orphan(); $baseurl = course_get_url($course, $sectionreturn); $baseurl->param('sesskey', sesskey()); diff --git a/course/format/templates/local/content/section/controlmenu.mustache b/course/format/templates/local/content/section/controlmenu.mustache index dc7a6829c07..bfc09fa86a1 100644 --- a/course/format/templates/local/content/section/controlmenu.mustache +++ b/course/format/templates/local/content/section/controlmenu.mustache @@ -26,7 +26,7 @@ } }} {{#hasmenu}} -
+
{{{menu}}}
{{/hasmenu}} diff --git a/course/format/tests/behat/behat_courseformat.php b/course/format/tests/behat/behat_courseformat.php index 51d5b011e2b..71198a4cae3 100644 --- a/course/format/tests/behat/behat_courseformat.php +++ b/course/format/tests/behat/behat_courseformat.php @@ -50,6 +50,9 @@ class behat_courseformat extends behat_base { new behat_component_named_selector('Section visibility', [ ".//*[@data-sectionname=%locator%]//*[@data-region='sectionvisibility']", ]), + new behat_component_named_selector('Section actions menu', [ + ".//*[@data-sectionname=%locator%]//*[@data-for='section_title']//*[@data-region='sectionactionsmmenu']", + ]), ]; } } diff --git a/course/format/topics/classes/output/courseformat/content/section/controlmenu.php b/course/format/topics/classes/output/courseformat/content/section/controlmenu.php index 8a0bcbf08f7..fc427ba73a1 100644 --- a/course/format/topics/classes/output/courseformat/content/section/controlmenu.php +++ b/course/format/topics/classes/output/courseformat/content/section/controlmenu.php @@ -55,12 +55,16 @@ class controlmenu extends controlmenu_base { $section = $this->section; $coursecontext = $format->get_context(); - $controls = []; - if ($section->section && has_capability('moodle/course:setcurrentsection', $coursecontext)) { - $controls['highlight'] = $this->get_highlight_control(); + $parentcontrols = parent::section_control_items(); + + if ($section->is_orphan() || !$section->section) { + return $parentcontrols; } - $parentcontrols = parent::section_control_items(); + $controls = []; + if (has_capability('moodle/course:setcurrentsection', $coursecontext)) { + $controls['highlight'] = $this->get_highlight_control(); + } // If the edit key exists, we are going to insert our controls after it. if (array_key_exists("edit", $parentcontrols)) { diff --git a/mod/subsection/tests/behat/subsection_disabled_plugin.feature b/mod/subsection/tests/behat/subsection_disabled_plugin.feature new file mode 100644 index 00000000000..cc844ebebd1 --- /dev/null +++ b/mod/subsection/tests/behat/subsection_disabled_plugin.feature @@ -0,0 +1,131 @@ +@mod @mod_subsection +Feature: Courses should not lose subsection contents when mod_subsection is disabled + In order to disable subsections + As an admin + Courses needs to be usable with mod_subsection disabled + + Background: + Given I enable "subsection" "mod" plugin + And the following "users" exist: + | username | firstname | lastname | + | teacher1 | Teacher | 1 | + | student1 | Student | 1 | + And the following "courses" exist: + | fullname | shortname | category | numsections | initsections | + | Course 1 | C1 | 0 | 2 | 1 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + And the following "activities" exist: + | activity | name | course | idnumber | section | + | forum | Activity Sample | C1 | forum1 | 1 | + | subsection | Subsection1 | C1 | Subsection1 | 1 | + | data | Subactivity | C1 | data1 | 3 | + And I disable "subsection" "mod" plugin + + Scenario: Teachers should see subsections as orphaned + When I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I should see "Activity Sample" in the "Section 1" "section" + And I should not see "Subsection1" in the "Section 1" "section" + And I should see "Subsection1" in the "page-content" "region" + Then I should see "Subactivity" in the "Subsection1" "section" + And "Section 2" "section" should appear before "Subsection1" "section" + And "Edit settings" "link" should not exist in the "Subsection1" "core_courseformat > Section actions menu" + And "Duplicate" "link" should not exist in the "Subsection1" "core_courseformat > Section actions menu" + And "Hide" "link" should not exist in the "Subsection1" "core_courseformat > Section actions menu" + And "Move" "link" should not exist in the "Subsection1" "core_courseformat > Section actions menu" + And "View" "link" should exist in the "Subsection1" "core_courseformat > Section actions menu" + And "Delete" "link" should exist in the "Subsection1" "core_courseformat > Section actions menu" + + Scenario: Students should not see orphaned subsections + When I log in as "student1" + And I am on "Course 1" course homepage + Then I should see "Activity Sample" in the "page-content" "region" + And I should not see "Subsection1" in the "page-content" "region" + And I should not see "Subactivity" in the "page-content" "region" + + Scenario: Enabling again subsections should show the course as before + Given I log in as "teacher1" + And I am on "Course 1" course homepage + And I should see "Activity Sample" in the "Section 1" "section" + And I should not see "Subsection1" in the "Section 1" "section" + And I should see "Subsection1" in the "page-content" "region" + And I should see "Subactivity" in the "Subsection1" "section" + And "Section 2" "section" should appear before "Subsection1" "section" + When I enable "subsection" "mod" plugin + And I am on "Course 1" course homepage with editing mode on + Then I should see "Activity Sample" in the "Section 1" "section" + And I should see "Subsection1" in the "Section 1" "section" + And I should see "Subactivity" in the "Subsection1" "section" + And "Subsection1" "section" should appear before "Section 2" "section" + And "Edit settings" "link" should exist in the "Subsection1" "core_courseformat > Section actions menu" + And "Duplicate" "link" should not exist in the "Subsection1" "core_courseformat > Section actions menu" + And "Hide" "link" should exist in the "Subsection1" "core_courseformat > Section actions menu" + And "Move" "link" should exist in the "Subsection1" "core_courseformat > Section actions menu" + And "View" "link" should exist in the "Subsection1" "core_courseformat > Section actions menu" + And "Delete" "link" should exist in the "Subsection1" "core_courseformat > Section actions menu" + + @javascript + Scenario: Deleting the subsections with mod_subsection disabled should not break the course + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + # Perform teacher actions instead of fast steps to validate delete + # an orphaned subsection does not break the course. + And I click on "Edit" "icon" in the "Subsection1" "core_courseformat > Section actions menu" + When I choose "Delete" in the open action menu + And I click on "Delete" "button" in the "Delete section?" "dialogue" + Then I enable "subsection" "mod" plugin + And I am on "Course 1" course homepage + And I should see "Activity Sample" in the "Section 1" "section" + And I should see "Subsection1" in the "page-content" "region" + And I should not see "Subactivity" in the "page-content" "region" + + @javascript + Scenario: Access restrictions are restored when a subsection is deleted while mod_subsection is disabled + Given I enable "subsection" "mod" plugin + And the following "groups" exist: + | course | name | idnumber | + | C1 | G1 | GI1 | + And the following "group members" exist: + | user | group | + | student1 | GI1 | + And the following "groupings" exist: + | name | course | idnumber | + | GX1 | C1 | GXI1 | + And the following "grouping groups" exist: + | grouping | group | + | GXI1 | GI1 | + And I log in as "teacher1" + And I am on the "C1 > Subsection1" "course > section settings" page + And I set the following fields to these values: + | Access restrictions | Grouping: GX1 | + And I press "Save changes" + And I should see "Not available unless: You belong to a group in GX1" + And I disable "subsection" "mod" plugin + When I am on the "C1 > Subsection1" "course > section" page + And I turn editing mode on + And I click on "Edit" "icon" in the "[data-region='header-actions-container']" "css_element" + And I choose "Delete" in the open action menu + And I click on "Delete" "button" in the "Delete section?" "dialogue" + And I enable "subsection" "mod" plugin + Then I am on "Course 1" course homepage + And I should see "Not available unless: You belong to a group in GX1" + + @javascript + Scenario: Visibility is restored when a subsection is deleted while mod_subsection is disabled + Given I enable "subsection" "mod" plugin + And the following "activities" exist: + | activity | name | course | idnumber | section | visible | + | subsection | Subsection2 | C1 | Subsection2 | 1 | 0 | + And I log in as "teacher1" + And I disable "subsection" "mod" plugin + When I am on the "C1 > Subsection2" "course > section" page + And I turn editing mode on + And I click on "Edit" "icon" in the "[data-region='header-actions-container']" "css_element" + And I choose "Delete" in the open action menu + And I click on "Delete" "button" in the "Delete section?" "dialogue" + And I enable "subsection" "mod" plugin + Then I am on "Course 1" course homepage + And I should see "Hidden from students" diff --git a/theme/boost/scss/moodle/course.scss b/theme/boost/scss/moodle/course.scss index 911805ded70..8e163792fb4 100644 --- a/theme/boost/scss/moodle/course.scss +++ b/theme/boost/scss/moodle/course.scss @@ -1150,7 +1150,8 @@ $divider-hover-color: $primary !default; @include border-radius($activity-border-radius); } - &.hidden { + &.hidden, + &.orphaned { .section-item { background-color: $gray-100; } diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index 4761954be70..d0a575354e4 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -29065,7 +29065,7 @@ span.editinstructions .alert-link { border: 1px solid #dee2e6; border-radius: 1rem; } -.course-section.hidden .section-item { +.course-section.hidden .section-item, .course-section.orphaned .section-item { background-color: #f8f9fa; } .course-section .sectionname > a { diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index 13d2c61ba6b..0f3aa9ad816 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -29065,7 +29065,7 @@ span.editinstructions .alert-link { border: 1px solid #dee2e6; border-radius: 1rem; } -.course-section.hidden .section-item { +.course-section.hidden .section-item, .course-section.orphaned .section-item { background-color: #f8f9fa; } .course-section .sectionname > a {