calendar/amd/src/calendar.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. * This module is the highest level module for the calendar. It is
  17. * responsible for initialising all of the components required for
  18. * the calendar to run. It also coordinates the interaction between
  19. * components by listening for and responding to different events
  20. * triggered within the calendar UI.
  21. *
  22. * @module core_calendar/calendar
  23. * @copyright 2017 Simey Lameze <simey@moodle.com>
  24. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  25. */
  26. define([
  27. 'jquery',
  28. 'core/templates',
  29. 'core/notification',
  30. 'core_calendar/repository',
  31. 'core_calendar/events',
  32. 'core_calendar/view_manager',
  33. 'core_calendar/crud',
  34. 'core_calendar/selectors',
  35. 'core/url',
  36. 'core/str',
  37. ],
  38. function(
  39. $,
  40. Templates,
  41. Notification,
  42. CalendarRepository,
  43. CalendarEvents,
  44. CalendarViewManager,
  45. CalendarCrud,
  46. CalendarSelectors,
  47. Url,
  48. Str,
  49. ) {
  50. var SELECTORS = {
  51. ROOT: "[data-region='calendar']",
  52. DAY: "[data-region='day']",
  53. NEW_EVENT_BUTTON: "[data-action='new-event-button']",
  54. DAY_CONTENT: "[data-region='day-content']",
  55. LOADING_ICON: '.loading-icon',
  56. VIEW_DAY_LINK: "[data-action='view-day-link']",
  57. CALENDAR_MONTH_WRAPPER: ".calendarwrapper",
  58. TODAY: '.today',
  59. DAY_NUMBER_CIRCLE: '.day-number-circle',
  60. DAY_NUMBER: '.day-number',
  61. SCREEN_READER_ANNOUNCEMENTS: '.calendar-announcements',
  62. CURRENT_MONTH: '.calendar-controls .current'
  63. };
  64. /**
  65. * Handler for the drag and drop move event. Provides a loading indicator
  66. * while the request is sent to the server to update the event start date.
  67. *
  68. * Triggers a eventMoved calendar javascript event if the event was successfully
  69. * updated.
  70. *
  71. * @param {event} e The calendar move event
  72. * @param {int} eventId The event id being moved
  73. * @param {object|null} originElement The jQuery element for where the event is moving from
  74. * @param {object} destinationElement The jQuery element for where the event is moving to
  75. */
  76. var handleMoveEvent = function(e, eventId, originElement, destinationElement) {
  77. var originTimestamp = null;
  78. var destinationTimestamp = destinationElement.attr('data-day-timestamp');
  79. if (originElement) {
  80. originTimestamp = originElement.attr('data-day-timestamp');
  81. }
  82. // If the event has actually changed day.
  83. if (!originElement || originTimestamp != destinationTimestamp) {
  84. Templates.render('core/loading', {})
  85. .then(function(html, js) {
  86. // First we show some loading icons in each of the days being affected.
  87. destinationElement.find(SELECTORS.DAY_CONTENT).addClass('hidden');
  88. Templates.appendNodeContents(destinationElement, html, js);
  89. if (originElement) {
  90. originElement.find(SELECTORS.DAY_CONTENT).addClass('hidden');
  91. Templates.appendNodeContents(originElement, html, js);
  92. }
  93. return;
  94. })
  95. .then(function() {
  96. // Send a request to the server to make the change.
  97. return CalendarRepository.updateEventStartDay(eventId, destinationTimestamp);
  98. })
  99. .then(function() {
  100. // If the update was successful then broadcast an event letting the calendar
  101. // know that an event has been moved.
  102. $('body').trigger(CalendarEvents.eventMoved, [eventId, originElement, destinationElement]);
  103. return;
  104. })
  105. .always(function() {
  106. // Always remove the loading icons regardless of whether the update
  107. // request was successful or not.
  108. var destinationLoadingElement = destinationElement.find(SELECTORS.LOADING_ICON);
  109. destinationElement.find(SELECTORS.DAY_CONTENT).removeClass('hidden');
  110. Templates.replaceNode(destinationLoadingElement, '', '');
  111. if (originElement) {
  112. var originLoadingElement = originElement.find(SELECTORS.LOADING_ICON);
  113. originElement.find(SELECTORS.DAY_CONTENT).removeClass('hidden');
  114. Templates.replaceNode(originLoadingElement, '', '');
  115. }
  116. return;
  117. })
  118. .catch(Notification.exception);
  119. }
  120. };
  121. /**
  122. * Listen to and handle any calendar events fired by the calendar UI.
  123. *
  124. * @method registerCalendarEventListeners
  125. * @param {object} root The calendar root element
  126. * @param {object} eventFormModalPromise A promise reolved with the event form modal
  127. */
  128. var registerCalendarEventListeners = function(root, eventFormModalPromise) {
  129. var body = $('body');
  130. body.on(CalendarEvents.created, function() {
  131. CalendarViewManager.reloadCurrentMonth(root);
  132. });
  133. body.on(CalendarEvents.deleted, function() {
  134. CalendarViewManager.reloadCurrentMonth(root);
  135. });
  136. body.on(CalendarEvents.updated, function() {
  137. CalendarViewManager.reloadCurrentMonth(root);
  138. });
  139. body.on(CalendarEvents.editActionEvent, function(e, url) {
  140. // Action events needs to be edit directly on the course module.
  141. window.location.assign(url);
  142. });
  143. // Handle the event fired by the drag and drop code.
  144. body.on(CalendarEvents.moveEvent, handleMoveEvent);
  145. // When an event is successfully moved we should updated the UI.
  146. body.on(CalendarEvents.eventMoved, function() {
  147. CalendarViewManager.reloadCurrentMonth(root);
  148. });
  149. // Announce the newly loaded month to screen readers.
  150. body.on(CalendarEvents.monthChanged, root, async function() {
  151. const monthName = body.find(SELECTORS.CURRENT_MONTH).text();
  152. const monthAnnoucement = await Str.get_string('newmonthannouncement', 'calendar', monthName);
  153. body.find(SELECTORS.SCREEN_READER_ANNOUNCEMENTS).html(monthAnnoucement);
  154. });
  155. CalendarCrud.registerEditListeners(root, eventFormModalPromise);
  156. };
  157. /**
  158. * Register event listeners for the module.
  159. *
  160. * @param {object} root The calendar root element
  161. * @param {boolean} isCalendarBlock - A flag indicating whether this is a calendar block.
  162. */
  163. var registerEventListeners = function(root, isCalendarBlock) {
  164. const viewingFullCalendar = document.getElementById(CalendarSelectors.fullCalendarView);
  165. // Listen the click on the day link to render the day view.
  166. root.on('click', SELECTORS.VIEW_DAY_LINK, function(e) {
  167. var dayLink = $(e.target).closest(SELECTORS.VIEW_DAY_LINK);
  168. var year = dayLink.data('year'),
  169. month = dayLink.data('month'),
  170. day = dayLink.data('day'),
  171. courseId = dayLink.data('courseid'),
  172. categoryId = dayLink.data('categoryid');
  173. const urlParams = {
  174. view: 'day',
  175. time: dayLink.data('timestamp'),
  176. course: courseId,
  177. };
  178. if (viewingFullCalendar) {
  179. // Construct the URL parameter string from the urlParams object.
  180. const urlParamString = Object.entries(urlParams)
  181. .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
  182. .join('&');
  183. CalendarViewManager.refreshDayContent(root, year, month, day, courseId, categoryId, root,
  184. 'core_calendar/calendar_day', isCalendarBlock).then(function() {
  185. e.preventDefault();
  186. // Update the URL if it's not calendar block.
  187. if (!isCalendarBlock) {
  188. CalendarViewManager.updateUrl('?' + urlParamString);
  189. }
  190. return;
  191. }).catch(Notification.exception);
  192. } else {
  193. window.location.assign(Url.relativeUrl('calendar/view.php', urlParams));
  194. }
  195. });
  196. root.on('change', CalendarSelectors.elements.courseSelector, function() {
  197. var selectElement = $(this);
  198. var courseId = selectElement.val();
  199. const courseName = $("option:selected", selectElement).text();
  200. CalendarViewManager.reloadCurrentMonth(root, courseId, null)
  201. .then(function() {
  202. // We need to get the selector again because the content has changed.
  203. return root.find(CalendarSelectors.elements.courseSelector).val(courseId);
  204. })
  205. .then(function() {
  206. CalendarViewManager.updateUrl('?view=month&course=' + courseId);
  207. CalendarViewManager.handleCourseChange(Number(courseId), courseName);
  208. return;
  209. })
  210. .catch(Notification.exception);
  211. });
  212. var eventFormPromise = CalendarCrud.registerEventFormModal(root),
  213. contextId = $(SELECTORS.CALENDAR_MONTH_WRAPPER).data('context-id');
  214. registerCalendarEventListeners(root, eventFormPromise);
  215. if (contextId) {
  216. // Bind click events to calendar days.
  217. root.on('click', SELECTORS.DAY, function(e) {
  218. var target = $(e.target);
  219. const displayingSmallBlockCalendar = root.parents('aside').data('blockregion') === 'side-pre';
  220. if (!viewingFullCalendar && displayingSmallBlockCalendar) {
  221. const dateContainer = target.closest(SELECTORS.DAY);
  222. const wrapper = target.closest(CalendarSelectors.wrapper);
  223. const courseId = wrapper.data('courseid');
  224. const params = {
  225. view: 'day',
  226. time: dateContainer.data('day-timestamp'),
  227. course: courseId,
  228. };
  229. window.location.assign(Url.relativeUrl('calendar/view.php', params));
  230. } else {
  231. const hasViewDayLink = target.closest(SELECTORS.VIEW_DAY_LINK).length;
  232. const shouldShowNewEventModal = !hasViewDayLink;
  233. if (shouldShowNewEventModal) {
  234. var startTime = $(this).attr('data-new-event-timestamp');
  235. eventFormPromise.then(function(modal) {
  236. var wrapper = target.closest(CalendarSelectors.wrapper);
  237. modal.setCourseId(wrapper.data('courseid'));
  238. var categoryId = wrapper.data('categoryid');
  239. if (typeof categoryId !== 'undefined') {
  240. modal.setCategoryId(categoryId);
  241. }
  242. modal.setContextId(wrapper.data('contextId'));
  243. modal.setStartTime(startTime);
  244. modal.show();
  245. return;
  246. }).catch(Notification.exception);
  247. }
  248. }
  249. e.preventDefault();
  250. });
  251. }
  252. };
  253. return {
  254. /**
  255. * Initializes the calendar view manager and registers event listeners.
  256. *
  257. * @param {HTMLElement} root - The root element where the calendar view manager and event listeners will be attached.
  258. * @param {boolean} [isCalendarBlock=false] - A flag indicating whether this is a calendar block.
  259. */
  260. init: function(root, isCalendarBlock = false) {
  261. root = $(root);
  262. CalendarViewManager.init(root, 'month', isCalendarBlock);
  263. registerEventListeners(root, isCalendarBlock);
  264. }
  265. };
  266. });