course/format/amd/src/local/courseindex/section.js

  1. // This file is part of Moodle - http://moodle.org/
  2. //
  3. // Moodle is free software: you can redistribute it and/or modify
  4. // it under the terms of the GNU General Public License as published by
  5. // the Free Software Foundation, either version 3 of the License, or
  6. // (at your option) any later version.
  7. //
  8. // Moodle is distributed in the hope that it will be useful,
  9. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. // GNU General Public License for more details.
  12. //
  13. // You should have received a copy of the GNU General Public License
  14. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  15. /**
  16. * Course index section component.
  17. *
  18. * This component is used to control specific course section interactions like drag and drop.
  19. *
  20. * @module core_courseformat/local/courseindex/section
  21. * @class core_courseformat/local/courseindex/section
  22. * @copyright 2021 Ferran Recio <ferran@moodle.com>
  23. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24. */
  25. import SectionTitle from 'core_courseformat/local/courseindex/sectiontitle';
  26. import DndSection from 'core_courseformat/local/courseeditor/dndsection';
  27. import log from "core/log";
  28. export default class Component extends DndSection {
  29. /**
  30. * Constructor hook.
  31. */
  32. create() {
  33. // Optional component name for debugging.
  34. this.name = 'courseindex_section';
  35. // Default query selectors.
  36. this.selectors = {
  37. SECTION: `[data-for='section']`,
  38. SECTION_ITEM: `[data-for='section_item']`,
  39. SECTION_TITLE: `[data-for='section_title']`,
  40. CM_LAST: `[data-for="cm"]:last-child`,
  41. };
  42. // Default classes to toggle on refresh.
  43. this.classes = {
  44. SECTIONHIDDEN: 'dimmed',
  45. SECTIONCURRENT: 'current',
  46. LOCKED: 'editinprogress',
  47. RESTRICTIONS: 'restrictions',
  48. PAGEITEM: 'pageitem',
  49. OVERLAYBORDERS: 'overlay-preview-borders',
  50. };
  51. // We need our id to watch specific events.
  52. this.id = this.element.dataset.id;
  53. this.isPageItem = false;
  54. }
  55. /**
  56. * Static method to create a component instance form the mustahce template.
  57. *
  58. * @param {string} target the DOM main element or its ID
  59. * @param {object} selectors optional css selector overrides
  60. * @return {Component}
  61. */
  62. static init(target, selectors) {
  63. let element = document.querySelector(target);
  64. // TODO Remove this if condition as part of MDL-83851.
  65. if (!element) {
  66. log.debug('Init component with id is deprecated, use a query selector instead.');
  67. element = document.getElementById(target);
  68. }
  69. return new this({
  70. element,
  71. selectors,
  72. });
  73. }
  74. /**
  75. * Initial state ready method.
  76. *
  77. * @param {Object} state the initial state
  78. */
  79. stateReady(state) {
  80. this.configState(state);
  81. const sectionItem = this.getElement(this.selectors.SECTION_ITEM);
  82. // Drag and drop is only available for components compatible course formats.
  83. if (this.reactive.isEditing && this.reactive.supportComponents) {
  84. // Init the inner dragable element passing the full section as affected region.
  85. const titleitem = new SectionTitle({
  86. ...this,
  87. element: sectionItem,
  88. fullregion: this.element,
  89. });
  90. this.configDragDrop(titleitem);
  91. }
  92. // Check if the current url is the section url.
  93. const section = state.section.get(this.id);
  94. if (window.location.href == section.sectionurl.replace(/&amp;/g, "&")) {
  95. this.reactive.dispatch('setPageItem', 'section', this.id);
  96. sectionItem.scrollIntoView();
  97. }
  98. }
  99. /**
  100. * Component watchers.
  101. *
  102. * @returns {Array} of watchers
  103. */
  104. getWatchers() {
  105. return [
  106. {watch: `section[${this.id}]:deleted`, handler: this.remove},
  107. {watch: `section[${this.id}]:updated`, handler: this._refreshSection},
  108. {watch: `course.pageItem:updated`, handler: this._refreshPageItem},
  109. ];
  110. }
  111. /**
  112. * Get the last CM element of that section.
  113. *
  114. * @returns {element|null}
  115. */
  116. getLastCm() {
  117. return this.getElement(this.selectors.CM_LAST);
  118. }
  119. /**
  120. * Update a course index section using the state information.
  121. *
  122. * @param {Object} param details the update details.
  123. * @param {Object} param.element the section element
  124. */
  125. _refreshSection({element}) {
  126. // Update classes.
  127. const sectionItem = this.getElement(this.selectors.SECTION_ITEM);
  128. sectionItem.classList.toggle(this.classes.SECTIONHIDDEN, !element.visible);
  129. sectionItem.classList.toggle(this.classes.RESTRICTIONS, element.hasrestrictions ?? false);
  130. this.element.classList.toggle(this.classes.SECTIONCURRENT, element.current);
  131. this.element.classList.toggle(this.classes.DRAGGING, element.dragging ?? false);
  132. this.element.classList.toggle(this.classes.LOCKED, element.locked ?? false);
  133. this.locked = element.locked;
  134. // Update title.
  135. this.getElement(this.selectors.SECTION_TITLE).innerHTML = element.title;
  136. }
  137. /**
  138. * Handle a page item update.
  139. *
  140. * @param {Object} details the update details
  141. * @param {Object} details.state the state data.
  142. * @param {Object} details.element the course state data.
  143. */
  144. _refreshPageItem({element, state}) {
  145. if (!element.pageItem) {
  146. return;
  147. }
  148. const containsPageItem = this._isPageItemInThisSection(element.pageItem);
  149. if (!containsPageItem || this._isParentSectionIndexCollapsed(state)) {
  150. this.pageItem = false;
  151. this.getElement(this.selectors.SECTION_ITEM).classList.remove(this.classes.PAGEITEM);
  152. return;
  153. }
  154. const section = state.section.get(this.id);
  155. if (section.indexcollapsed && !element.pageItem?.isStatic) {
  156. this.pageItem = containsPageItem;
  157. } else {
  158. this.pageItem = (element.pageItem.type == 'section' && element.pageItem.id == this.id);
  159. }
  160. const sectionItem = this.getElement(this.selectors.SECTION_ITEM);
  161. sectionItem.classList.toggle(this.classes.PAGEITEM, this.pageItem ?? false);
  162. if (this.pageItem && !this.reactive.isEditing) {
  163. this.element.scrollIntoView({block: "nearest"});
  164. }
  165. }
  166. /**
  167. * Check if the page item is inside this section.
  168. *
  169. * @private
  170. * @param {Object} pageItem
  171. * @param {Object} pageItem.sectionId the current page item section id.
  172. * @returns {boolean}
  173. */
  174. _isPageItemInThisSection(pageItem) {
  175. if (pageItem.sectionId == this.id) {
  176. return true;
  177. }
  178. // Check for any possible subsections.
  179. const subsection = this.element.querySelector(`${this.selectors.SECTION}[data-id="${pageItem.sectionId}"]`);
  180. if (subsection) {
  181. return true;
  182. }
  183. return false;
  184. }
  185. /**
  186. * Check if the parent section index is collapsed.
  187. *
  188. * @private
  189. * @param {Object} state the current state
  190. * @returns {boolean|null} null if no parent section is found.
  191. */
  192. _isParentSectionIndexCollapsed(state) {
  193. const parentElement = this.element.parentElement.closest(this.selectors.SECTION);
  194. if (!parentElement || !parentElement.dataset.id) {
  195. return null;
  196. }
  197. const parentSection = state.section.get(parentElement.dataset.id);
  198. return !!parentSection.indexcollapsed;
  199. }
  200. /**
  201. * Overridden version of the component addOverlay async method.
  202. *
  203. * The course index is not compatible with overlay elements.
  204. */
  205. async addOverlay() {
  206. this.element.classList.add(this.classes.OVERLAYBORDERS);
  207. }
  208. /**
  209. * Overridden version of the component removeOverlay.
  210. *
  211. * The course index is not compatible with overlay elements.
  212. */
  213. removeOverlay() {
  214. this.element.classList.remove(this.classes.OVERLAYBORDERS);
  215. }
  216. }