calendar/amd/src/month_view_drag_drop.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. * A javascript module to handle calendar drag and drop in the calendar
  17. * month view.
  18. *
  19. * @module core_calendar/month_view_drag_drop
  20. * @copyright 2017 Ryan Wyllie <ryan@moodle.com>
  21. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  22. */
  23. define([
  24. 'jquery',
  25. 'core/notification',
  26. 'core/str',
  27. 'core_calendar/events',
  28. 'core_calendar/drag_drop_data_store'
  29. ],
  30. function(
  31. $,
  32. Notification,
  33. Str,
  34. CalendarEvents,
  35. DataStore
  36. ) {
  37. var SELECTORS = {
  38. ROOT: "[data-region='calendar']",
  39. DRAGGABLE: '[draggable="true"][data-region="event-item"]',
  40. DROP_ZONE: '[data-drop-zone="month-view-day"]',
  41. WEEK: '[data-region="month-view-week"]',
  42. };
  43. var INVALID_DROP_ZONE_CLASS = 'bg-faded';
  44. var INVALID_HOVER_CLASS = 'bg-danger text-white';
  45. var VALID_HOVER_CLASS = 'bg-primary text-white';
  46. var ALL_CLASSES = INVALID_DROP_ZONE_CLASS + ' ' + INVALID_HOVER_CLASS + ' ' + VALID_HOVER_CLASS;
  47. /* @var {bool} registered If the event listeners have been added */
  48. var registered = false;
  49. /**
  50. * Get the correct drop zone element from the given javascript
  51. * event.
  52. *
  53. * @param {event} e The javascript event
  54. * @return {object|null}
  55. */
  56. var getDropZoneFromEvent = function(e) {
  57. var dropZone = $(e.target).closest(SELECTORS.DROP_ZONE);
  58. return (dropZone.length) ? dropZone : null;
  59. };
  60. /**
  61. * Determine if the given dropzone element is within the acceptable
  62. * time range.
  63. *
  64. * The drop zone timestamp is midnight on that day so we should check
  65. * that the event's acceptable timestart value
  66. *
  67. * @param {object} dropZone The drop zone day from the calendar
  68. * @return {bool}
  69. */
  70. var isValidDropZone = function(dropZone) {
  71. var dropTimestamp = dropZone.attr('data-day-timestamp');
  72. var minTimestart = DataStore.getMinTimestart();
  73. var maxTimestart = DataStore.getMaxTimestart();
  74. if (minTimestart && minTimestart > dropTimestamp) {
  75. return false;
  76. }
  77. if (maxTimestart && maxTimestart < dropTimestamp) {
  78. return false;
  79. }
  80. return true;
  81. };
  82. /**
  83. * Get the error string to display for a given drop zone element
  84. * if it is invalid.
  85. *
  86. * @param {object} dropZone The drop zone day from the calendar
  87. * @return {string}
  88. */
  89. var getDropZoneError = function(dropZone) {
  90. var dropTimestamp = dropZone.attr('data-day-timestamp');
  91. var minTimestart = DataStore.getMinTimestart();
  92. var maxTimestart = DataStore.getMaxTimestart();
  93. if (minTimestart && minTimestart > dropTimestamp) {
  94. return DataStore.getMinError();
  95. }
  96. if (maxTimestart && maxTimestart < dropTimestamp) {
  97. return DataStore.getMaxError();
  98. }
  99. return null;
  100. };
  101. /**
  102. * Remove all of the styling from each of the drop zones in the calendar.
  103. */
  104. var clearAllDropZonesState = function() {
  105. $(SELECTORS.ROOT).find(SELECTORS.DROP_ZONE).each(function(index, dropZone) {
  106. dropZone = $(dropZone);
  107. dropZone.removeClass(ALL_CLASSES);
  108. });
  109. };
  110. /**
  111. * Update the hover state for the event in the calendar to reflect
  112. * which days the event will be moved to.
  113. *
  114. * If the drop zone is not being hovered then it will apply some
  115. * styling to reflect whether the drop zone is a valid or invalid
  116. * drop place for the current dragging event.
  117. *
  118. * This funciton supports events spanning multiple days and will
  119. * recurse to highlight (or remove highlight) each of the days
  120. * that the event will be moved to.
  121. *
  122. * For example: An event with a duration of 3 days will have
  123. * 3 days highlighted when it's dragged elsewhere in the calendar.
  124. * The current drag target and the 2 days following it (including
  125. * wrapping to the next week if necessary).
  126. *
  127. * @param {string|object} dropZone The drag target element
  128. * @param {bool} hovered If the target is hovered or not
  129. * @param {Number} count How many days to highlight (default to duration)
  130. */
  131. var updateHoverState = function(dropZone, hovered, count) {
  132. if (typeof count === 'undefined') {
  133. // This is how many days we need to highlight.
  134. count = DataStore.getDurationDays();
  135. }
  136. var valid = isValidDropZone(dropZone);
  137. dropZone.removeClass(ALL_CLASSES);
  138. if (hovered) {
  139. if (valid) {
  140. dropZone.addClass(VALID_HOVER_CLASS);
  141. } else {
  142. dropZone.addClass(INVALID_HOVER_CLASS);
  143. }
  144. } else {
  145. dropZone.removeClass(VALID_HOVER_CLASS + ' ' + INVALID_HOVER_CLASS);
  146. if (!valid) {
  147. dropZone.addClass(INVALID_DROP_ZONE_CLASS);
  148. }
  149. }
  150. count--;
  151. // If we've still got days to highlight then we should
  152. // find the next day.
  153. if (count > 0) {
  154. var nextDropZone = dropZone.next();
  155. // If there are no more days in this week then we
  156. // need to move down to the next week in the calendar.
  157. if (!nextDropZone.length) {
  158. var nextWeek = dropZone.closest(SELECTORS.WEEK).next();
  159. if (nextWeek.length) {
  160. nextDropZone = nextWeek.children(SELECTORS.DROP_ZONE).first();
  161. }
  162. }
  163. // If we found another day then let's recursively
  164. // update it's hover state.
  165. if (nextDropZone.length) {
  166. updateHoverState(nextDropZone, hovered, count);
  167. }
  168. }
  169. };
  170. /**
  171. * Find all of the calendar event drop zones in the calendar and update the display
  172. * for the user to indicate which zones are valid and invalid.
  173. */
  174. var updateAllDropZonesState = function() {
  175. $(SELECTORS.ROOT).find(SELECTORS.DROP_ZONE).each(function(index, dropZone) {
  176. dropZone = $(dropZone);
  177. if (!isValidDropZone(dropZone)) {
  178. updateHoverState(dropZone, false);
  179. }
  180. });
  181. };
  182. /**
  183. * Set up the module level variables to track which event is being
  184. * dragged and how many days it spans.
  185. *
  186. * @param {event} e The dragstart event
  187. */
  188. var dragstartHandler = function(e) {
  189. var target = $(e.target);
  190. var draggableElement = target.closest(SELECTORS.DRAGGABLE);
  191. if (!draggableElement.length) {
  192. return;
  193. }
  194. var eventElement = draggableElement.find('[data-event-id]');
  195. var eventId = eventElement.attr('data-event-id');
  196. var minTimestart = draggableElement.attr('data-min-day-timestamp');
  197. var maxTimestart = draggableElement.attr('data-max-day-timestamp');
  198. var minError = draggableElement.attr('data-min-day-error');
  199. var maxError = draggableElement.attr('data-max-day-error');
  200. var eventsSelector = SELECTORS.ROOT + ' [data-event-id="' + eventId + '"]';
  201. var duration = $(eventsSelector).length;
  202. DataStore.setEventId(eventId);
  203. DataStore.setDurationDays(duration);
  204. if (minTimestart) {
  205. DataStore.setMinTimestart(minTimestart);
  206. }
  207. if (maxTimestart) {
  208. DataStore.setMaxTimestart(maxTimestart);
  209. }
  210. if (minError) {
  211. DataStore.setMinError(minError);
  212. }
  213. if (maxError) {
  214. DataStore.setMaxError(maxError);
  215. }
  216. e.dataTransfer.effectAllowed = "move";
  217. e.dataTransfer.dropEffect = "move";
  218. // Firefox requires a value to be set here or the drag won't
  219. // work and the dragover handler won't fire.
  220. e.dataTransfer.setData('text/plain', eventId);
  221. e.dropEffect = "move";
  222. updateAllDropZonesState();
  223. };
  224. /**
  225. * Update the hover state of the target day element when
  226. * the user is dragging an event over it.
  227. *
  228. * This will add a visual indicator to the calendar UI to
  229. * indicate which day(s) the event will be moved to.
  230. *
  231. * @param {event} e The dragstart event
  232. */
  233. var dragoverHandler = function(e) {
  234. // Ignore dragging of non calendar events.
  235. if (!DataStore.hasEventId()) {
  236. return;
  237. }
  238. e.preventDefault();
  239. var dropZone = getDropZoneFromEvent(e);
  240. if (!dropZone) {
  241. return;
  242. }
  243. updateHoverState(dropZone, true);
  244. };
  245. /**
  246. * Update the hover state of the target day element that was
  247. * previously dragged over but has is no longer a drag target.
  248. *
  249. * This will remove the visual indicator from the calendar UI
  250. * that was added by the dragoverHandler.
  251. *
  252. * @param {event} e The dragstart event
  253. */
  254. var dragleaveHandler = function(e) {
  255. // Ignore dragging of non calendar events.
  256. if (!DataStore.hasEventId()) {
  257. return;
  258. }
  259. var dropZone = getDropZoneFromEvent(e);
  260. if (!dropZone) {
  261. return;
  262. }
  263. updateHoverState(dropZone, false);
  264. e.preventDefault();
  265. };
  266. /**
  267. * Determines the event element, origin day, and destination day
  268. * once the user drops the calendar event. These three bits of data
  269. * are provided as the payload to the "moveEvent" calendar javascript
  270. * event that is fired.
  271. *
  272. * This will remove the visual indicator from the calendar UI
  273. * that was added by the dragoverHandler.
  274. *
  275. * @param {event} e The dragstart event
  276. */
  277. var dropHandler = function(e) {
  278. // Ignore dragging of non calendar events.
  279. if (!DataStore.hasEventId()) {
  280. return;
  281. }
  282. var dropZone = getDropZoneFromEvent(e);
  283. if (!dropZone) {
  284. DataStore.clearAll();
  285. clearAllDropZonesState();
  286. return;
  287. }
  288. if (isValidDropZone(dropZone)) {
  289. var eventId = DataStore.getEventId();
  290. var eventElementSelector = SELECTORS.ROOT + ' [data-event-id="' + eventId + '"]';
  291. var eventElement = $(eventElementSelector);
  292. var origin = null;
  293. if (eventElement.length) {
  294. origin = eventElement.closest(SELECTORS.DROP_ZONE);
  295. }
  296. $('body').trigger(CalendarEvents.moveEvent, [eventId, origin, dropZone]);
  297. } else {
  298. // If the drop zone is not valid then there is not need for us to
  299. // try to process it. Instead we can just show an error to the user.
  300. var message = getDropZoneError(dropZone);
  301. Str.get_string('errorinvaliddate', 'calendar').then(function(string) {
  302. Notification.exception({
  303. name: string,
  304. message: message || string
  305. });
  306. });
  307. }
  308. DataStore.clearAll();
  309. clearAllDropZonesState();
  310. e.preventDefault();
  311. };
  312. /**
  313. * Clear the data store and remove the drag indicators from the UI
  314. * when the drag event has finished.
  315. */
  316. var dragendHandler = function() {
  317. DataStore.clearAll();
  318. clearAllDropZonesState();
  319. };
  320. /**
  321. * Re-render the drop zones in the new month to highlight
  322. * which areas are or aren't acceptable to drop the calendar
  323. * event.
  324. */
  325. var calendarMonthChangedHandler = function() {
  326. updateAllDropZonesState();
  327. };
  328. return {
  329. /**
  330. * Initialise the event handlers for the drag events.
  331. */
  332. init: function() {
  333. if (!registered) {
  334. // These handlers are only added the first time the module
  335. // is loaded because we don't want to have a new listener
  336. // added each time the "init" function is called otherwise we'll
  337. // end up with lots of stale handlers.
  338. document.addEventListener('dragstart', dragstartHandler, false);
  339. document.addEventListener('dragover', dragoverHandler, false);
  340. document.addEventListener('dragleave', dragleaveHandler, false);
  341. document.addEventListener('drop', dropHandler, false);
  342. document.addEventListener('dragend', dragendHandler, false);
  343. $('body').on(CalendarEvents.monthChanged, calendarMonthChangedHandler);
  344. registered = true;
  345. }
  346. },
  347. };
  348. });