From e497d87fdcc6a72339844cc31fb2e7f9cee8b4ce Mon Sep 17 00:00:00 2001 From: Neill Magill Date: Wed, 7 May 2025 09:22:10 +0100 Subject: [PATCH] MDL-85391 course: Handle null course urls Course urls may be null when navigation is set to true. The return value from course_get_url being null was not being handled before this change, which could lead to an exception causing the page not to load. It also handles null urls in the course index in Boost so that they are not a link to what ever the current page is. If either the first or last section in a format did not a return a url under the Classic theme there would still be a link displayed with a link to the current page the user is on. After this change no link would be displayed in this case. --- .../build/local/courseindex/section.min.js | 2 +- .../local/courseindex/section.min.js.map | 2 +- .../amd/src/local/courseindex/section.js | 3 +- .../local/content/sectionnavigation.php | 6 ++-- .../output/local/content/sectionselector.php | 16 ++++++---- .../classes/output/local/state/section.php | 2 +- public/course/format/singleactivity/lib.php | 2 +- public/course/format/social/lib.php | 2 +- .../local/courseindex/section.mustache | 29 ++++++++++++++----- public/course/format/topics/lib.php | 2 +- public/course/format/weeks/lib.php | 2 +- public/course/lib.php | 2 +- 12 files changed, 45 insertions(+), 25 deletions(-) diff --git a/public/course/format/amd/build/local/courseindex/section.min.js b/public/course/format/amd/build/local/courseindex/section.min.js index 5bb626db405..df56a5096a7 100644 --- a/public/course/format/amd/build/local/courseindex/section.min.js +++ b/public/course/format/amd/build/local/courseindex/section.min.js @@ -8,6 +8,6 @@ define("core_courseformat/local/courseindex/section",["exports","core_courseform * @class core_courseformat/local/courseindex/section * @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,_sectiontitle=_interopRequireDefault(_sectiontitle),_dndsection=_interopRequireDefault(_dndsection),_log=_interopRequireDefault(_log);class Component extends _dndsection.default{create(){this.name="courseindex_section",this.selectors={SECTION:"[data-for='section']",SECTION_ITEM:"[data-for='section_item']",SECTION_TITLE:"[data-for='section_title']",CM_LAST:'[data-for="cm"]:last-child',DND_ALLOWED:"[data-courseindexdndallowed='true']"},this.classes={SECTIONHIDDEN:"dimmed",SECTIONCURRENT:"current",LOCKED:"editinprogress",RESTRICTIONS:"restrictions",PAGEITEM:"pageitem",OVERLAYBORDERS:"overlay-preview-borders"},this.id=this.element.dataset.id,this.isPageItem=!1}static init(target,selectors){let element=document.querySelector(target);return element||(_log.default.debug("Init component with id is deprecated, use a query selector instead."),element=document.getElementById(target)),new this({element:element,selectors:selectors})}stateReady(state){this.configState(state);const sectionItem=this.getElement(this.selectors.SECTION_ITEM);if(this.reactive.isEditing&&this.reactive.supportComponents&&document.querySelector(this.selectors.DND_ALLOWED)){const titleitem=new _sectiontitle.default({...this,element:sectionItem,fullregion:this.element});this.configDragDrop(titleitem)}const section=state.section.get(this.id);window.location.href==section.sectionurl.replace(/&/g,"&")&&(this.reactive.dispatch("setPageItem","section",this.id),sectionItem.scrollIntoView())}getWatchers(){return[{watch:"section[".concat(this.id,"]:deleted"),handler:this.remove},{watch:"section[".concat(this.id,"]:updated"),handler:this._refreshSection},{watch:"course.pageItem:updated",handler:this._refreshPageItem}]}getLastCm(){return this.getElement(this.selectors.CM_LAST)}_refreshSection(_ref){var _element$hasrestricti,_element$dragging,_element$locked;let{element:element}=_ref;const sectionItem=this.getElement(this.selectors.SECTION_ITEM);sectionItem.classList.toggle(this.classes.SECTIONHIDDEN,!element.visible),sectionItem.classList.toggle(this.classes.RESTRICTIONS,null!==(_element$hasrestricti=element.hasrestrictions)&&void 0!==_element$hasrestricti&&_element$hasrestricti),this.element.classList.toggle(this.classes.SECTIONCURRENT,element.current),this.element.classList.toggle(this.classes.DRAGGING,null!==(_element$dragging=element.dragging)&&void 0!==_element$dragging&&_element$dragging),this.element.classList.toggle(this.classes.LOCKED,null!==(_element$locked=element.locked)&&void 0!==_element$locked&&_element$locked),this.locked=element.locked,this.getElement(this.selectors.SECTION_TITLE).innerHTML=element.title}_refreshPageItem(_ref2){var _element$pageItem,_this$pageItem;let{element:element,state:state}=_ref2;if(!element.pageItem)return;const containsPageItem=this._isPageItemInThisSection(element.pageItem);if(!containsPageItem||this._isParentSectionIndexCollapsed(state))return this.pageItem=!1,void this.getElement(this.selectors.SECTION_ITEM).classList.remove(this.classes.PAGEITEM);!state.section.get(this.id).indexcollapsed||null!==(_element$pageItem=element.pageItem)&&void 0!==_element$pageItem&&_element$pageItem.isStatic?this.pageItem="section"==element.pageItem.type&&element.pageItem.id==this.id:this.pageItem=containsPageItem;this.getElement(this.selectors.SECTION_ITEM).classList.toggle(this.classes.PAGEITEM,null!==(_this$pageItem=this.pageItem)&&void 0!==_this$pageItem&&_this$pageItem),this.pageItem&&!this.reactive.isEditing&&this.element.scrollIntoView({block:"nearest"})}_isPageItemInThisSection(pageItem){if(pageItem.sectionId==this.id)return!0;return!!this.element.querySelector("".concat(this.selectors.SECTION,'[data-id="').concat(pageItem.sectionId,'"]'))}_isParentSectionIndexCollapsed(state){const parentElement=this.element.parentElement.closest(this.selectors.SECTION);if(!parentElement||!parentElement.dataset.id)return null;return!!state.section.get(parentElement.dataset.id).indexcollapsed}async addOverlay(){this.element.classList.add(this.classes.OVERLAYBORDERS)}removeOverlay(){this.element.classList.remove(this.classes.OVERLAYBORDERS)}}return _exports.default=Component,_exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_sectiontitle=_interopRequireDefault(_sectiontitle),_dndsection=_interopRequireDefault(_dndsection),_log=_interopRequireDefault(_log);class Component extends _dndsection.default{create(){this.name="courseindex_section",this.selectors={SECTION:"[data-for='section']",SECTION_ITEM:"[data-for='section_item']",SECTION_TITLE:"[data-for='section_title']",CM_LAST:'[data-for="cm"]:last-child',DND_ALLOWED:"[data-courseindexdndallowed='true']"},this.classes={SECTIONHIDDEN:"dimmed",SECTIONCURRENT:"current",LOCKED:"editinprogress",RESTRICTIONS:"restrictions",PAGEITEM:"pageitem",OVERLAYBORDERS:"overlay-preview-borders"},this.id=this.element.dataset.id,this.isPageItem=!1}static init(target,selectors){let element=document.querySelector(target);return element||(_log.default.debug("Init component with id is deprecated, use a query selector instead."),element=document.getElementById(target)),new this({element:element,selectors:selectors})}stateReady(state){var _section$sectionurl;this.configState(state);const sectionItem=this.getElement(this.selectors.SECTION_ITEM);if(this.reactive.isEditing&&this.reactive.supportComponents&&document.querySelector(this.selectors.DND_ALLOWED)){const titleitem=new _sectiontitle.default({...this,element:sectionItem,fullregion:this.element});this.configDragDrop(titleitem)}const section=state.section.get(this.id);window.location.href==(null===(_section$sectionurl=section.sectionurl)||void 0===_section$sectionurl?void 0:_section$sectionurl.replace(/&/g,"&"))&&(this.reactive.dispatch("setPageItem","section",this.id),sectionItem.scrollIntoView())}getWatchers(){return[{watch:"section[".concat(this.id,"]:deleted"),handler:this.remove},{watch:"section[".concat(this.id,"]:updated"),handler:this._refreshSection},{watch:"course.pageItem:updated",handler:this._refreshPageItem}]}getLastCm(){return this.getElement(this.selectors.CM_LAST)}_refreshSection(_ref){var _element$hasrestricti,_element$dragging,_element$locked;let{element:element}=_ref;const sectionItem=this.getElement(this.selectors.SECTION_ITEM);sectionItem.classList.toggle(this.classes.SECTIONHIDDEN,!element.visible),sectionItem.classList.toggle(this.classes.RESTRICTIONS,null!==(_element$hasrestricti=element.hasrestrictions)&&void 0!==_element$hasrestricti&&_element$hasrestricti),this.element.classList.toggle(this.classes.SECTIONCURRENT,element.current),this.element.classList.toggle(this.classes.DRAGGING,null!==(_element$dragging=element.dragging)&&void 0!==_element$dragging&&_element$dragging),this.element.classList.toggle(this.classes.LOCKED,null!==(_element$locked=element.locked)&&void 0!==_element$locked&&_element$locked),this.locked=element.locked,this.getElement(this.selectors.SECTION_TITLE).innerHTML=element.title}_refreshPageItem(_ref2){var _element$pageItem,_this$pageItem;let{element:element,state:state}=_ref2;if(!element.pageItem)return;const containsPageItem=this._isPageItemInThisSection(element.pageItem);if(!containsPageItem||this._isParentSectionIndexCollapsed(state))return this.pageItem=!1,void this.getElement(this.selectors.SECTION_ITEM).classList.remove(this.classes.PAGEITEM);!state.section.get(this.id).indexcollapsed||null!==(_element$pageItem=element.pageItem)&&void 0!==_element$pageItem&&_element$pageItem.isStatic?this.pageItem="section"==element.pageItem.type&&element.pageItem.id==this.id:this.pageItem=containsPageItem;this.getElement(this.selectors.SECTION_ITEM).classList.toggle(this.classes.PAGEITEM,null!==(_this$pageItem=this.pageItem)&&void 0!==_this$pageItem&&_this$pageItem),this.pageItem&&!this.reactive.isEditing&&this.element.scrollIntoView({block:"nearest"})}_isPageItemInThisSection(pageItem){if(pageItem.sectionId==this.id)return!0;return!!this.element.querySelector("".concat(this.selectors.SECTION,'[data-id="').concat(pageItem.sectionId,'"]'))}_isParentSectionIndexCollapsed(state){const parentElement=this.element.parentElement.closest(this.selectors.SECTION);if(!parentElement||!parentElement.dataset.id)return null;return!!state.section.get(parentElement.dataset.id).indexcollapsed}async addOverlay(){this.element.classList.add(this.classes.OVERLAYBORDERS)}removeOverlay(){this.element.classList.remove(this.classes.OVERLAYBORDERS)}}return _exports.default=Component,_exports.default})); //# sourceMappingURL=section.min.js.map \ No newline at end of file diff --git a/public/course/format/amd/build/local/courseindex/section.min.js.map b/public/course/format/amd/build/local/courseindex/section.min.js.map index 30643cbc8c6..2ac1284f61a 100644 --- a/public/course/format/amd/build/local/courseindex/section.min.js.map +++ b/public/course/format/amd/build/local/courseindex/section.min.js.map @@ -1 +1 @@ -{"version":3,"file":"section.min.js","sources":["../../../src/local/courseindex/section.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 section component.\n *\n * This component is used to control specific course section interactions like drag and drop.\n *\n * @module core_courseformat/local/courseindex/section\n * @class core_courseformat/local/courseindex/section\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport SectionTitle from 'core_courseformat/local/courseindex/sectiontitle';\nimport DndSection from 'core_courseformat/local/courseeditor/dndsection';\nimport log from \"core/log\";\n\nexport default class Component extends DndSection {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'courseindex_section';\n // Default query selectors.\n this.selectors = {\n SECTION: `[data-for='section']`,\n SECTION_ITEM: `[data-for='section_item']`,\n SECTION_TITLE: `[data-for='section_title']`,\n CM_LAST: `[data-for=\"cm\"]:last-child`,\n DND_ALLOWED: `[data-courseindexdndallowed='true']`,\n };\n // Default classes to toggle on refresh.\n this.classes = {\n SECTIONHIDDEN: 'dimmed',\n SECTIONCURRENT: 'current',\n LOCKED: 'editinprogress',\n RESTRICTIONS: 'restrictions',\n PAGEITEM: 'pageitem',\n OVERLAYBORDERS: 'overlay-preview-borders',\n };\n\n // We need our id to watch specific events.\n this.id = this.element.dataset.id;\n this.isPageItem = false;\n }\n\n /**\n * Static method to create a component instance form the mustahce template.\n *\n * @param {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 let element = document.querySelector(target);\n // TODO Remove this if condition as part of MDL-83851.\n if (!element) {\n log.debug('Init component with id is deprecated, use a query selector instead.');\n element = document.getElementById(target);\n }\n return new this({\n element,\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {Object} state the initial state\n */\n stateReady(state) {\n this.configState(state);\n const sectionItem = this.getElement(this.selectors.SECTION_ITEM);\n // Drag and drop is only available for components compatible course formats.\n if (this.reactive.isEditing && this.reactive.supportComponents && document.querySelector(this.selectors.DND_ALLOWED)) {\n // Init the inner dragable element passing the full section as affected region.\n const titleitem = new SectionTitle({\n ...this,\n element: sectionItem,\n fullregion: this.element,\n });\n this.configDragDrop(titleitem);\n }\n // Check if the current url is the section url.\n const section = state.section.get(this.id);\n if (window.location.href == section.sectionurl.replace(/&/g, \"&\")) {\n this.reactive.dispatch('setPageItem', 'section', this.id);\n sectionItem.scrollIntoView();\n }\n }\n\n /**\n * Component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n {watch: `section[${this.id}]:deleted`, handler: this.remove},\n {watch: `section[${this.id}]:updated`, handler: this._refreshSection},\n {watch: `course.pageItem:updated`, handler: this._refreshPageItem},\n ];\n }\n\n /**\n * Get the last CM element of that section.\n *\n * @returns {element|null}\n */\n getLastCm() {\n return this.getElement(this.selectors.CM_LAST);\n }\n\n /**\n * Update a course index section using the state information.\n *\n * @param {Object} param details the update details.\n * @param {Object} param.element the section element\n */\n _refreshSection({element}) {\n // Update classes.\n const sectionItem = this.getElement(this.selectors.SECTION_ITEM);\n sectionItem.classList.toggle(this.classes.SECTIONHIDDEN, !element.visible);\n sectionItem.classList.toggle(this.classes.RESTRICTIONS, element.hasrestrictions ?? false);\n this.element.classList.toggle(this.classes.SECTIONCURRENT, element.current);\n this.element.classList.toggle(this.classes.DRAGGING, element.dragging ?? false);\n this.element.classList.toggle(this.classes.LOCKED, element.locked ?? false);\n this.locked = element.locked;\n // Update title.\n this.getElement(this.selectors.SECTION_TITLE).innerHTML = element.title;\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) {\n return;\n }\n\n const containsPageItem = this._isPageItemInThisSection(element.pageItem);\n\n if (!containsPageItem || this._isParentSectionIndexCollapsed(state)) {\n this.pageItem = false;\n this.getElement(this.selectors.SECTION_ITEM).classList.remove(this.classes.PAGEITEM);\n return;\n }\n\n const section = state.section.get(this.id);\n if (section.indexcollapsed && !element.pageItem?.isStatic) {\n this.pageItem = containsPageItem;\n } else {\n this.pageItem = (element.pageItem.type == 'section' && element.pageItem.id == this.id);\n }\n const sectionItem = this.getElement(this.selectors.SECTION_ITEM);\n sectionItem.classList.toggle(this.classes.PAGEITEM, this.pageItem ?? false);\n if (this.pageItem && !this.reactive.isEditing) {\n this.element.scrollIntoView({block: \"nearest\"});\n }\n }\n\n /**\n * Check if the page item is inside this section.\n *\n * @private\n * @param {Object} pageItem\n * @param {Object} pageItem.sectionId the current page item section id.\n * @returns {boolean}\n */\n _isPageItemInThisSection(pageItem) {\n if (pageItem.sectionId == this.id) {\n return true;\n }\n // Check for any possible subsections.\n const subsection = this.element.querySelector(`${this.selectors.SECTION}[data-id=\"${pageItem.sectionId}\"]`);\n if (subsection) {\n return true;\n }\n return false;\n }\n\n /**\n * Check if the parent section index is collapsed.\n *\n * @private\n * @param {Object} state the current state\n * @returns {boolean|null} null if no parent section is found.\n */\n _isParentSectionIndexCollapsed(state) {\n const parentElement = this.element.parentElement.closest(this.selectors.SECTION);\n if (!parentElement || !parentElement.dataset.id) {\n return null;\n }\n const parentSection = state.section.get(parentElement.dataset.id);\n return !!parentSection.indexcollapsed;\n }\n\n /**\n * Overridden version of the component addOverlay async method.\n *\n * The course index is not compatible with overlay elements.\n */\n async addOverlay() {\n this.element.classList.add(this.classes.OVERLAYBORDERS);\n }\n\n /**\n * Overridden version of the component removeOverlay.\n *\n * The course index is not compatible with overlay elements.\n */\n removeOverlay() {\n this.element.classList.remove(this.classes.OVERLAYBORDERS);\n }\n}\n"],"names":["Component","DndSection","create","name","selectors","SECTION","SECTION_ITEM","SECTION_TITLE","CM_LAST","DND_ALLOWED","classes","SECTIONHIDDEN","SECTIONCURRENT","LOCKED","RESTRICTIONS","PAGEITEM","OVERLAYBORDERS","id","this","element","dataset","isPageItem","target","document","querySelector","debug","getElementById","stateReady","state","configState","sectionItem","getElement","reactive","isEditing","supportComponents","titleitem","SectionTitle","fullregion","configDragDrop","section","get","window","location","href","sectionurl","replace","dispatch","scrollIntoView","getWatchers","watch","handler","remove","_refreshSection","_refreshPageItem","getLastCm","classList","toggle","visible","hasrestrictions","current","DRAGGING","dragging","locked","innerHTML","title","pageItem","containsPageItem","_isPageItemInThisSection","_isParentSectionIndexCollapsed","indexcollapsed","_element$pageItem","isStatic","type","block","sectionId","parentElement","closest","add","removeOverlay"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"section.min.js","sources":["../../../src/local/courseindex/section.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 section component.\n *\n * This component is used to control specific course section interactions like drag and drop.\n *\n * @module core_courseformat/local/courseindex/section\n * @class core_courseformat/local/courseindex/section\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport SectionTitle from 'core_courseformat/local/courseindex/sectiontitle';\nimport DndSection from 'core_courseformat/local/courseeditor/dndsection';\nimport log from \"core/log\";\n\nexport default class Component extends DndSection {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'courseindex_section';\n // Default query selectors.\n this.selectors = {\n SECTION: `[data-for='section']`,\n SECTION_ITEM: `[data-for='section_item']`,\n SECTION_TITLE: `[data-for='section_title']`,\n CM_LAST: `[data-for=\"cm\"]:last-child`,\n DND_ALLOWED: `[data-courseindexdndallowed='true']`,\n };\n // Default classes to toggle on refresh.\n this.classes = {\n SECTIONHIDDEN: 'dimmed',\n SECTIONCURRENT: 'current',\n LOCKED: 'editinprogress',\n RESTRICTIONS: 'restrictions',\n PAGEITEM: 'pageitem',\n OVERLAYBORDERS: 'overlay-preview-borders',\n };\n\n // We need our id to watch specific events.\n this.id = this.element.dataset.id;\n this.isPageItem = false;\n }\n\n /**\n * Static method to create a component instance form the mustahce template.\n *\n * @param {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 let element = document.querySelector(target);\n // TODO Remove this if condition as part of MDL-83851.\n if (!element) {\n log.debug('Init component with id is deprecated, use a query selector instead.');\n element = document.getElementById(target);\n }\n return new this({\n element,\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {Object} state the initial state\n */\n stateReady(state) {\n this.configState(state);\n const sectionItem = this.getElement(this.selectors.SECTION_ITEM);\n // Drag and drop is only available for components compatible course formats.\n if (this.reactive.isEditing && this.reactive.supportComponents && document.querySelector(this.selectors.DND_ALLOWED)) {\n // Init the inner dragable element passing the full section as affected region.\n const titleitem = new SectionTitle({\n ...this,\n element: sectionItem,\n fullregion: this.element,\n });\n this.configDragDrop(titleitem);\n }\n // Check if the current url is the section url.\n const section = state.section.get(this.id);\n // Sections urls may be null.\n if (window.location.href == section.sectionurl?.replace(/&/g, \"&\")) {\n this.reactive.dispatch('setPageItem', 'section', this.id);\n sectionItem.scrollIntoView();\n }\n }\n\n /**\n * Component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n {watch: `section[${this.id}]:deleted`, handler: this.remove},\n {watch: `section[${this.id}]:updated`, handler: this._refreshSection},\n {watch: `course.pageItem:updated`, handler: this._refreshPageItem},\n ];\n }\n\n /**\n * Get the last CM element of that section.\n *\n * @returns {element|null}\n */\n getLastCm() {\n return this.getElement(this.selectors.CM_LAST);\n }\n\n /**\n * Update a course index section using the state information.\n *\n * @param {Object} param details the update details.\n * @param {Object} param.element the section element\n */\n _refreshSection({element}) {\n // Update classes.\n const sectionItem = this.getElement(this.selectors.SECTION_ITEM);\n sectionItem.classList.toggle(this.classes.SECTIONHIDDEN, !element.visible);\n sectionItem.classList.toggle(this.classes.RESTRICTIONS, element.hasrestrictions ?? false);\n this.element.classList.toggle(this.classes.SECTIONCURRENT, element.current);\n this.element.classList.toggle(this.classes.DRAGGING, element.dragging ?? false);\n this.element.classList.toggle(this.classes.LOCKED, element.locked ?? false);\n this.locked = element.locked;\n // Update title.\n this.getElement(this.selectors.SECTION_TITLE).innerHTML = element.title;\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) {\n return;\n }\n\n const containsPageItem = this._isPageItemInThisSection(element.pageItem);\n\n if (!containsPageItem || this._isParentSectionIndexCollapsed(state)) {\n this.pageItem = false;\n this.getElement(this.selectors.SECTION_ITEM).classList.remove(this.classes.PAGEITEM);\n return;\n }\n\n const section = state.section.get(this.id);\n if (section.indexcollapsed && !element.pageItem?.isStatic) {\n this.pageItem = containsPageItem;\n } else {\n this.pageItem = (element.pageItem.type == 'section' && element.pageItem.id == this.id);\n }\n const sectionItem = this.getElement(this.selectors.SECTION_ITEM);\n sectionItem.classList.toggle(this.classes.PAGEITEM, this.pageItem ?? false);\n if (this.pageItem && !this.reactive.isEditing) {\n this.element.scrollIntoView({block: \"nearest\"});\n }\n }\n\n /**\n * Check if the page item is inside this section.\n *\n * @private\n * @param {Object} pageItem\n * @param {Object} pageItem.sectionId the current page item section id.\n * @returns {boolean}\n */\n _isPageItemInThisSection(pageItem) {\n if (pageItem.sectionId == this.id) {\n return true;\n }\n // Check for any possible subsections.\n const subsection = this.element.querySelector(`${this.selectors.SECTION}[data-id=\"${pageItem.sectionId}\"]`);\n if (subsection) {\n return true;\n }\n return false;\n }\n\n /**\n * Check if the parent section index is collapsed.\n *\n * @private\n * @param {Object} state the current state\n * @returns {boolean|null} null if no parent section is found.\n */\n _isParentSectionIndexCollapsed(state) {\n const parentElement = this.element.parentElement.closest(this.selectors.SECTION);\n if (!parentElement || !parentElement.dataset.id) {\n return null;\n }\n const parentSection = state.section.get(parentElement.dataset.id);\n return !!parentSection.indexcollapsed;\n }\n\n /**\n * Overridden version of the component addOverlay async method.\n *\n * The course index is not compatible with overlay elements.\n */\n async addOverlay() {\n this.element.classList.add(this.classes.OVERLAYBORDERS);\n }\n\n /**\n * Overridden version of the component removeOverlay.\n *\n * The course index is not compatible with overlay elements.\n */\n removeOverlay() {\n this.element.classList.remove(this.classes.OVERLAYBORDERS);\n }\n}\n"],"names":["Component","DndSection","create","name","selectors","SECTION","SECTION_ITEM","SECTION_TITLE","CM_LAST","DND_ALLOWED","classes","SECTIONHIDDEN","SECTIONCURRENT","LOCKED","RESTRICTIONS","PAGEITEM","OVERLAYBORDERS","id","this","element","dataset","isPageItem","target","document","querySelector","debug","getElementById","stateReady","state","configState","sectionItem","getElement","reactive","isEditing","supportComponents","titleitem","SectionTitle","fullregion","configDragDrop","section","get","window","location","href","sectionurl","_section$sectionurl","replace","dispatch","scrollIntoView","getWatchers","watch","handler","remove","_refreshSection","_refreshPageItem","getLastCm","classList","toggle","visible","hasrestrictions","current","DRAGGING","dragging","locked","innerHTML","title","pageItem","containsPageItem","_isPageItemInThisSection","_isParentSectionIndexCollapsed","indexcollapsed","_element$pageItem","isStatic","type","block","sectionId","parentElement","closest","add","removeOverlay"],"mappings":"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"} \ No newline at end of file diff --git a/public/course/format/amd/src/local/courseindex/section.js b/public/course/format/amd/src/local/courseindex/section.js index f52fc88fc35..c432b99ecd1 100644 --- a/public/course/format/amd/src/local/courseindex/section.js +++ b/public/course/format/amd/src/local/courseindex/section.js @@ -99,7 +99,8 @@ export default class Component extends DndSection { } // Check if the current url is the section url. const section = state.section.get(this.id); - if (window.location.href == section.sectionurl.replace(/&/g, "&")) { + // Sections urls may be null. + if (window.location.href == section.sectionurl?.replace(/&/g, "&")) { this.reactive.dispatch('setPageItem', 'section', this.id); sectionItem.scrollIntoView(); } diff --git a/public/course/format/classes/output/local/content/sectionnavigation.php b/public/course/format/classes/output/local/content/sectionnavigation.php index 2b5639d2c9b..02f7059009c 100644 --- a/public/course/format/classes/output/local/content/sectionnavigation.php +++ b/public/course/format/classes/output/local/content/sectionnavigation.php @@ -101,7 +101,8 @@ class sectionnavigation implements named_templatable, renderable { } $data->previousname = get_section_name($course, $sections[$back]); $data->previousurl = course_get_url($course, $back, ['navigation' => true]); - $data->hasprevious = true; + // If there is no url for the section the link should not be displayed. + $data->hasprevious = !empty($data->previousurl); } $back--; } @@ -115,7 +116,8 @@ class sectionnavigation implements named_templatable, renderable { } $data->nextname = get_section_name($course, $sections[$forward]); $data->nexturl = course_get_url($course, $forward, ['navigation' => true]); - $data->hasnext = true; + // If there is no url for the section the link should not be displayed. + $data->hasnext = !empty($data->nexturl); } $forward++; } diff --git a/public/course/format/classes/output/local/content/sectionselector.php b/public/course/format/classes/output/local/content/sectionselector.php index 74a7fc36f80..7113d452159 100644 --- a/public/course/format/classes/output/local/content/sectionselector.php +++ b/public/course/format/classes/output/local/content/sectionselector.php @@ -120,7 +120,9 @@ class sectionselector implements named_templatable, renderable { nothing: ['' => get_string('jumpto')], ); // Disable the current section. - $select->set_option_disabled($disabledlink); + if (!is_null($disabledlink)) { + $select->set_option_disabled($disabledlink); + } $select->class = 'jumpmenu'; $select->formid = 'sectionmenu'; @@ -143,17 +145,19 @@ class sectionselector implements named_templatable, renderable { bool $indent = false ) { $url = $this->get_section_url($course, $section); - $indentation = $indent ? self::INDENTER : ''; - $this->sectionmenu[$url] = $indentation . $format->get_section_name($section); + if (!is_null($url)) { + $indentation = $indent ? self::INDENTER : ''; + $this->sectionmenu[$url] = $indentation . $format->get_section_name($section); + } } /** * Get the section url. * @param stdClass $course * @param section_info $section - * @return string + * @return string|null */ - private function get_section_url(stdClass $course, section_info $section): string { - return course_get_url($course, (object) $section, ['navigation' => true])->out(false); + private function get_section_url(stdClass $course, section_info $section): ?string { + return course_get_url($course, (object) $section, ['navigation' => true])?->out(false); } } diff --git a/public/course/format/classes/output/local/state/section.php b/public/course/format/classes/output/local/state/section.php index 0ca2e63bfb5..96355dc401f 100644 --- a/public/course/format/classes/output/local/state/section.php +++ b/public/course/format/classes/output/local/state/section.php @@ -83,7 +83,7 @@ class section implements renderable { 'rawtitle' => $section->name, 'cmlist' => [], 'visible' => !empty($section->visible), - 'sectionurl' => course_get_url($course, $section->section, ['navigation' => true])->out(false), + 'sectionurl' => course_get_url($course, $section->section, ['navigation' => true])?->out(false), 'current' => $format->is_section_current($section), 'indexcollapsed' => $indexcollapsed, 'contentcollapsed' => $contentcollapsed, diff --git a/public/course/format/singleactivity/lib.php b/public/course/format/singleactivity/lib.php index 8d831b749f7..a142102736b 100644 --- a/public/course/format/singleactivity/lib.php +++ b/public/course/format/singleactivity/lib.php @@ -41,7 +41,7 @@ class format_singleactivity extends core_courseformat\base implements core_cours * @param array $options options for view URL. At the moment core uses: * 'navigation' (bool) ignored by this format * 'sr' (int) ignored by this format - * @return null|moodle_url + * @return moodle_url */ public function get_view_url($section, $options = []) { return new moodle_url('/course/view.php', ['id' => $this->courseid]); diff --git a/public/course/format/social/lib.php b/public/course/format/social/lib.php index a620deacc58..55a0235d65d 100644 --- a/public/course/format/social/lib.php +++ b/public/course/format/social/lib.php @@ -43,7 +43,7 @@ class format_social extends core_courseformat\base { * @param array $options options for view URL. At the moment core uses: * 'navigation' (bool) ignored by this format * 'sr' (int) ignored by this format - * @return null|moodle_url + * @return moodle_url */ public function get_view_url($section, $options = array()) { return new moodle_url('/course/view.php', ['id' => $this->courseid]); diff --git a/public/course/format/templates/local/courseindex/section.mustache b/public/course/format/templates/local/courseindex/section.mustache index 41163cea732..e163fd3758b 100644 --- a/public/course/format/templates/local/courseindex/section.mustache +++ b/public/course/format/templates/local/courseindex/section.mustache @@ -91,14 +91,27 @@ {{#str}} collapse, core {{/str}} - - {{{title}}} - + {{! We display a link when there is a sectionurl, otherwise a span. The data-for attribute is required by JS. }} + {{#sectionurl}} + + {{{title}}} + + {{/sectionurl}} + {{^sectionurl}} + + {{{title}}} + + {{/sectionurl}} {{highlighted}} diff --git a/public/course/format/topics/lib.php b/public/course/format/topics/lib.php index 9a21437440e..f2279d46d7b 100644 --- a/public/course/format/topics/lib.php +++ b/public/course/format/topics/lib.php @@ -107,7 +107,7 @@ class format_topics extends core_courseformat\base { * @param array $options options for view URL. At the moment core uses: * 'navigation' (bool) if true and section not empty, the function returns section page; otherwise, it returns course page. * 'sr' (int) used by course formats to specify to which section to return - * @return null|moodle_url + * @return moodle_url */ public function get_view_url($section, $options = []) { $course = $this->get_course(); diff --git a/public/course/format/weeks/lib.php b/public/course/format/weeks/lib.php index 867b1cdb35d..3359399d4ee 100644 --- a/public/course/format/weeks/lib.php +++ b/public/course/format/weeks/lib.php @@ -124,7 +124,7 @@ class format_weeks extends core_courseformat\base { * @param array $options options for view URL. At the moment core uses: * 'navigation' (bool) if true and section not empty, the function returns section page; otherwise, it returns course page. * 'sr' (int) used by course formats to specify to which section to return - * @return null|moodle_url + * @return moodle_url */ public function get_view_url($section, $options = array()) { $course = $this->get_course(); diff --git a/public/course/lib.php b/public/course/lib.php index e2b60b6295b..ee5c8886a3d 100644 --- a/public/course/lib.php +++ b/public/course/lib.php @@ -2578,7 +2578,7 @@ function get_sorted_course_formats($enabledonly = false) { * @param array $options options for view URL. At the moment core uses: * 'navigation' (bool) if true and section has no separate page, the function returns null * 'sr' (int) used by multipage formats to specify to which section to return - * @return moodle_url The url of course + * @return moodle_url|null The url of course */ function course_get_url($courseorid, $section = null, $options = array()) { return course_get_format($courseorid)->get_view_url($section, $options);