course/format/amd/src/local/courseindex/cm.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 cm component.
  17. *
  18. * This component is used to control specific course modules interactions like drag and drop.
  19. *
  20. * @module core_courseformat/local/courseindex/cm
  21. * @class core_courseformat/local/courseindex/cm
  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 DndCmItem from 'core_courseformat/local/courseeditor/dndcmitem';
  26. import Templates from 'core/templates';
  27. import Prefetch from 'core/prefetch';
  28. import Config from 'core/config';
  29. import Pending from "core/pending";
  30. import log from "core/log";
  31. // Prefetch the completion icons template.
  32. const completionTemplate = 'core_courseformat/local/courseindex/cmcompletion';
  33. Prefetch.prefetchTemplate(completionTemplate);
  34. export default class Component extends DndCmItem {
  35. /**
  36. * Constructor hook.
  37. */
  38. create() {
  39. // Optional component name for debugging.
  40. this.name = 'courseindex_cm';
  41. // Default query selectors.
  42. this.selectors = {
  43. CM_NAME: `[data-for='cm_name']`,
  44. CM_COMPLETION: `[data-for='cm_completion']`,
  45. };
  46. // Default classes to toggle on refresh.
  47. this.classes = {
  48. CMHIDDEN: 'dimmed',
  49. LOCKED: 'editinprogress',
  50. RESTRICTIONS: 'restrictions',
  51. PAGEITEM: 'pageitem',
  52. INDENTED: 'indented',
  53. };
  54. // We need our id to watch specific events.
  55. this.id = this.element.dataset.id;
  56. }
  57. /**
  58. * Static method to create a component instance form the mustache template.
  59. *
  60. * @param {element|string} target the DOM main element or its ID
  61. * @param {object} selectors optional css selector overrides
  62. * @return {Component}
  63. */
  64. static init(target, selectors) {
  65. let element = document.querySelector(target);
  66. // TODO Remove this if condition as part of MDL-83851.
  67. if (!element) {
  68. log.debug('Init component with id is deprecated, use a query selector instead.');
  69. element = document.getElementById(target);
  70. }
  71. return new this({
  72. element,
  73. selectors,
  74. });
  75. }
  76. /**
  77. * Initial state ready method.
  78. *
  79. * @param {Object} state the course state.
  80. */
  81. stateReady(state) {
  82. this.configDragDrop(this.id);
  83. const cm = state.cm.get(this.id);
  84. const course = state.course;
  85. // Refresh completion icon.
  86. this._refreshCompletion({
  87. state,
  88. element: cm,
  89. });
  90. const url = new URL(window.location.href);
  91. const anchor = url.hash.replace('#', '');
  92. // Check if the current url is the cm url.
  93. if (window.location.href == cm.url
  94. || (window.location.href.includes(course.baseurl) && anchor == cm.anchor)
  95. ) {
  96. this.element.scrollIntoView({block: "center"});
  97. }
  98. // Check if this we are displaying this activity page.
  99. if (Config.contextid != Config.courseContextId && Config.contextInstanceId == this.id) {
  100. this.reactive.dispatch('setPageItem', 'cm', this.id, true);
  101. this.element.scrollIntoView({block: "center"});
  102. }
  103. // Add anchor logic if the element is not user visible or the element hasn't URL.
  104. if (!cm.uservisible || !cm.url) {
  105. const element = this.getElement(this.selectors.CM_NAME);
  106. this.addEventListener(
  107. element,
  108. 'click',
  109. this._activityAnchor,
  110. );
  111. // If the element is not user visible we also need to update the anchor link including the section page.
  112. if (!document.getElementById(cm.anchor)) {
  113. element.setAttribute('href', this._getActivitySectionURL(cm));
  114. }
  115. }
  116. }
  117. /**
  118. * Component watchers.
  119. *
  120. * @returns {Array} of watchers
  121. */
  122. getWatchers() {
  123. return [
  124. {watch: `cm[${this.id}]:deleted`, handler: this.remove},
  125. {watch: `cm[${this.id}]:updated`, handler: this._refreshCm},
  126. {watch: `cm[${this.id}].completionstate:updated`, handler: this._refreshCompletion},
  127. {watch: `course.pageItem:updated`, handler: this._refreshPageItem},
  128. ];
  129. }
  130. /**
  131. * Update a course index cm using the state information.
  132. *
  133. * @param {object} param
  134. * @param {Object} param.element details the update details.
  135. */
  136. _refreshCm({element}) {
  137. // Update classes.
  138. this.element.classList.toggle(this.classes.CMHIDDEN, !element.visible);
  139. this.getElement(this.selectors.CM_NAME).innerHTML = element.name;
  140. this.element.classList.toggle(this.classes.DRAGGING, element.dragging ?? false);
  141. this.element.classList.toggle(this.classes.LOCKED, element.locked ?? false);
  142. this.element.classList.toggle(this.classes.RESTRICTIONS, element.hascmrestrictions ?? false);
  143. this.element.classList.toggle(this.classes.INDENTED, element.indent);
  144. this.locked = element.locked;
  145. }
  146. /**
  147. * Handle a page item update.
  148. *
  149. * @param {Object} details the update details
  150. * @param {Object} details.element the course state data.
  151. */
  152. _refreshPageItem({element}) {
  153. if (!element.pageItem) {
  154. return;
  155. }
  156. const isPageId = (element.pageItem.type == 'cm' && element.pageItem.id == this.id);
  157. this.element.classList.toggle(this.classes.PAGEITEM, isPageId);
  158. if (isPageId && !this.reactive.isEditing) {
  159. this.element.scrollIntoView({block: "nearest"});
  160. }
  161. }
  162. /**
  163. * Update the activity completion icon.
  164. *
  165. * @param {Object} details the update details
  166. * @param {Object} details.state the state data
  167. * @param {Object} details.element the element data
  168. */
  169. async _refreshCompletion({state, element}) {
  170. // No completion icons are displayed in edit mode.
  171. if (this.reactive.isEditing || !element.istrackeduser) {
  172. return;
  173. }
  174. // Check if the completion value has changed.
  175. const completionElement = this.getElement(this.selectors.CM_COMPLETION);
  176. if (!completionElement || completionElement.dataset.value == element.completionstate) {
  177. return;
  178. }
  179. // Collect section information from the state.
  180. const exporter = this.reactive.getExporter();
  181. const data = exporter.cmCompletion(state, element);
  182. const {html, js} = await Templates.renderForPromise(completionTemplate, data);
  183. Templates.replaceNode(completionElement, html, js);
  184. }
  185. /**
  186. * The activity anchor event.
  187. *
  188. * @param {Event} event
  189. */
  190. _activityAnchor(event) {
  191. const cm = this.reactive.get('cm', this.id);
  192. // If the user cannot access the element but the element is present in the page
  193. // the new url should be an anchor link.
  194. const element = document.getElementById(cm.anchor);
  195. if (element) {
  196. // Make sure the section is expanded.
  197. this.reactive.dispatch('sectionContentCollapsed', [cm.sectionid], false);
  198. // Marc the element as page item once the event is handled.
  199. const pendingAnchor = new Pending(`courseformat/activity:openAnchor`);
  200. setTimeout(() => {
  201. this.reactive.dispatch('setPageItem', 'cm', cm.id);
  202. pendingAnchor.resolve();
  203. }, 50);
  204. return;
  205. }
  206. // If the element is not present in the page we need to go to the specific section.
  207. event.preventDefault();
  208. window.location = this._getActivitySectionURL(cm);
  209. }
  210. /**
  211. * Get the anchor link in section page for the cm.
  212. *
  213. * @param {Object} cm the course module data.
  214. * @return {String} the anchor link.
  215. */
  216. _getActivitySectionURL(cm) {
  217. let section = this.reactive.get('section', cm.sectionid);
  218. // If the section is delegated get its parent section if it has one.
  219. if (section.component && section.parentsectionid) {
  220. section = this.reactive.get('section', section.parentsectionid);
  221. }
  222. if (!section) {
  223. return '#';
  224. }
  225. const sectionurl = section.sectionurl.split("#")[0];
  226. return `${sectionurl}#${cm.anchor}`;
  227. }
  228. }