calendar/amd/src/modal_event_form.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. * Contain the logic for the quick add or update event modal.
  17. *
  18. * @module core_calendar/modal_event_form
  19. * @copyright 2017 Ryan Wyllie <ryan@moodle.com>
  20. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  21. */
  22. import $ from 'jquery';
  23. import * as CustomEvents from 'core/custom_interaction_events';
  24. import Modal from 'core/modal';
  25. import * as FormEvents from 'core_form/events';
  26. import CalendarEvents from './events';
  27. import * as Str from 'core/str';
  28. import * as Notification from 'core/notification';
  29. import * as Fragment from 'core/fragment';
  30. import * as Repository from 'core_calendar/repository';
  31. const SELECTORS = {
  32. SAVE_BUTTON: '[data-action="save"]',
  33. LOADING_ICON_CONTAINER: '[data-region="loading-icon-container"]',
  34. };
  35. export default class ModalEventForm extends Modal {
  36. static TYPE = 'core_calendar-modal_event_form';
  37. static TEMPLATE = 'calendar/modal_event_form';
  38. /**
  39. * Constructor for the Modal.
  40. *
  41. * @param {object} root The root jQuery element for the modal
  42. */
  43. constructor(root) {
  44. super(root);
  45. this.eventId = null;
  46. this.startTime = null;
  47. this.courseId = null;
  48. this.categoryId = null;
  49. this.contextId = null;
  50. this.reloadingBody = false;
  51. this.reloadingTitle = false;
  52. this.saveButton = this.getFooter().find(SELECTORS.SAVE_BUTTON);
  53. }
  54. configure(modalConfig) {
  55. modalConfig.large = true;
  56. super.configure(modalConfig);
  57. }
  58. /**
  59. * Set the context id to the given value.
  60. *
  61. * @method setContextId
  62. * @param {Number} id The event id
  63. */
  64. setContextId(id) {
  65. this.contextId = id;
  66. }
  67. /**
  68. * Retrieve the current context id, if any.
  69. *
  70. * @method getContextId
  71. * @return {Number|null} The event id
  72. */
  73. getContextId() {
  74. return this.contextId;
  75. }
  76. /**
  77. * Set the course id to the given value.
  78. *
  79. * @method setCourseId
  80. * @param {Number} id The event id
  81. */
  82. setCourseId(id) {
  83. this.courseId = id;
  84. }
  85. /**
  86. * Retrieve the current course id, if any.
  87. *
  88. * @method getCourseId
  89. * @return {Number|null} The event id
  90. */
  91. getCourseId() {
  92. return this.courseId;
  93. }
  94. /**
  95. * Set the category id to the given value.
  96. *
  97. * @method setCategoryId
  98. * @param {Number} id The event id
  99. */
  100. setCategoryId(id) {
  101. this.categoryId = id;
  102. }
  103. /**
  104. * Retrieve the current category id, if any.
  105. *
  106. * @method getCategoryId
  107. * @return {Number|null} The event id
  108. */
  109. getCategoryId() {
  110. return this.categoryId;
  111. }
  112. /**
  113. * Check if the modal has an course id.
  114. *
  115. * @method hasCourseId
  116. * @return {bool}
  117. */
  118. hasCourseId() {
  119. return this.courseId !== null;
  120. }
  121. /**
  122. * Check if the modal has an category id.
  123. *
  124. * @method hasCategoryId
  125. * @return {bool}
  126. */
  127. hasCategoryId() {
  128. return this.categoryId !== null;
  129. }
  130. /**
  131. * Set the event id to the given value.
  132. *
  133. * @method setEventId
  134. * @param {Number} id The event id
  135. */
  136. setEventId(id) {
  137. this.eventId = id;
  138. }
  139. /**
  140. * Retrieve the current event id, if any.
  141. *
  142. * @method getEventId
  143. * @return {Number|null} The event id
  144. */
  145. getEventId() {
  146. return this.eventId;
  147. }
  148. /**
  149. * Check if the modal has an event id.
  150. *
  151. * @method hasEventId
  152. * @return {bool}
  153. */
  154. hasEventId() {
  155. return this.eventId !== null;
  156. }
  157. /**
  158. * Set the start time to the given value.
  159. *
  160. * @method setStartTime
  161. * @param {Number} time The start time
  162. */
  163. setStartTime(time) {
  164. this.startTime = time;
  165. }
  166. /**
  167. * Retrieve the current start time, if any.
  168. *
  169. * @method getStartTime
  170. * @return {Number|null} The start time
  171. */
  172. getStartTime() {
  173. return this.startTime;
  174. }
  175. /**
  176. * Check if the modal has start time.
  177. *
  178. * @method hasStartTime
  179. * @return {bool}
  180. */
  181. hasStartTime() {
  182. return this.startTime !== null;
  183. }
  184. /**
  185. * Get the form element from the modal.
  186. *
  187. * @method getForm
  188. * @return {object}
  189. */
  190. getForm() {
  191. return this.getBody().find('form');
  192. }
  193. /**
  194. * Disable the buttons in the footer.
  195. *
  196. * @method disableButtons
  197. */
  198. disableButtons() {
  199. this.saveButton.prop('disabled', true);
  200. }
  201. /**
  202. * Enable the buttons in the footer.
  203. *
  204. * @method enableButtons
  205. */
  206. enableButtons() {
  207. this.saveButton.prop('disabled', false);
  208. }
  209. /**
  210. * Reload the title for the modal to the appropriate value
  211. * depending on whether we are creating a new event or
  212. * editing an existing event.
  213. *
  214. * @method reloadTitleContent
  215. * @return {object} A promise resolved with the new title text
  216. */
  217. reloadTitleContent() {
  218. if (this.reloadingTitle) {
  219. return this.titlePromise;
  220. }
  221. this.reloadingTitle = true;
  222. if (this.hasEventId()) {
  223. this.titlePromise = Str.get_string('editevent', 'calendar');
  224. } else {
  225. this.titlePromise = Str.get_string('newevent', 'calendar');
  226. }
  227. this.titlePromise.then((string) => {
  228. this.setTitle(string);
  229. return string;
  230. })
  231. .catch(Notification.exception)
  232. .always(() => {
  233. this.reloadingTitle = false;
  234. return;
  235. });
  236. return this.titlePromise;
  237. }
  238. /**
  239. * Send a request to the server to get the event_form in a fragment
  240. * and render the result in the body of the modal.
  241. *
  242. * If serialised form data is provided then it will be sent in the
  243. * request to the server to have the form rendered with the data. This
  244. * is used when the form had a server side error and we need the server
  245. * to re-render it for us to display the error to the user.
  246. *
  247. * @method reloadBodyContent
  248. * @param {string} formData The serialised form data
  249. * @return {object} A promise resolved with the fragment html and js from
  250. */
  251. reloadBodyContent(formData) {
  252. if (this.reloadingBody) {
  253. return this.bodyPromise;
  254. }
  255. this.reloadingBody = true;
  256. this.disableButtons();
  257. const args = {};
  258. if (this.hasEventId()) {
  259. args.eventid = this.getEventId();
  260. }
  261. if (this.hasStartTime()) {
  262. args.starttime = this.getStartTime();
  263. }
  264. if (this.hasCourseId()) {
  265. args.courseid = this.getCourseId();
  266. }
  267. if (this.hasCategoryId()) {
  268. args.categoryid = this.getCategoryId();
  269. }
  270. if (typeof formData !== 'undefined') {
  271. args.formdata = formData;
  272. }
  273. this.bodyPromise = Fragment.loadFragment('calendar', 'event_form', this.getContextId(), args);
  274. this.setBody(this.bodyPromise);
  275. this.bodyPromise.then(() => {
  276. this.enableButtons();
  277. return;
  278. })
  279. .catch(Notification.exception)
  280. .always(() => {
  281. this.reloadingBody = false;
  282. return;
  283. });
  284. return this.bodyPromise;
  285. }
  286. /**
  287. * Reload both the title and body content.
  288. *
  289. * @method reloadAllContent
  290. * @return {object} promise
  291. */
  292. reloadAllContent() {
  293. return $.when(this.reloadTitleContent(), this.reloadBodyContent());
  294. }
  295. /**
  296. * Kick off a reload the modal content before showing it. This
  297. * is to allow us to re-use the same modal for creating and
  298. * editing different events within the page.
  299. *
  300. * We do the reload when showing the modal rather than hiding it
  301. * to save a request to the server if the user closes the modal
  302. * and never re-opens it.
  303. *
  304. * @method show
  305. */
  306. show() {
  307. this.reloadAllContent();
  308. super.show(this);
  309. }
  310. /**
  311. * Clear the event id from the modal when it's closed so
  312. * that it is loaded fresh next time it's displayed.
  313. *
  314. * The event id will be set by the calling code if it wants
  315. * to edit a specific event.
  316. *
  317. * @method hide
  318. */
  319. hide() {
  320. super.hide(this);
  321. this.setEventId(null);
  322. this.setStartTime(null);
  323. this.setCourseId(null);
  324. this.setCategoryId(null);
  325. }
  326. /**
  327. * Get the serialised form data.
  328. *
  329. * @method getFormData
  330. * @return {string} serialised form data
  331. */
  332. getFormData() {
  333. return this.getForm().serialize();
  334. }
  335. /**
  336. * Send the form data to the server to create or update
  337. * an event.
  338. *
  339. * If there is a server side validation error then we re-request the
  340. * rendered form (with the data) from the server in order to get the
  341. * server side errors to display.
  342. *
  343. * On success the modal is hidden and the page is reloaded so that the
  344. * new event will display.
  345. *
  346. * @method save
  347. * @return {object} A promise
  348. */
  349. save() {
  350. const loadingContainer = this.saveButton.find(SELECTORS.LOADING_ICON_CONTAINER);
  351. // Now the change events have run, see if there are any "invalid" form fields.
  352. const invalid = this.getForm().find('[aria-invalid="true"]');
  353. // If we found invalid fields, focus on the first one and do not submit via ajax.
  354. if (invalid.length) {
  355. invalid.first().focus();
  356. return Promise.resolve();
  357. }
  358. loadingContainer.removeClass('hidden');
  359. this.disableButtons();
  360. const formData = this.getFormData();
  361. // Send the form data to the server for processing.
  362. return Repository.submitCreateUpdateForm(formData)
  363. .then((response) => {
  364. if (response.validationerror) {
  365. // If there was a server side validation error then
  366. // we need to re-request the rendered form from the server
  367. // in order to display the error for the user.
  368. this.reloadBodyContent(formData);
  369. return;
  370. } else {
  371. // Check whether this was a new event or not.
  372. // The hide function unsets the form data so grab this before the hide.
  373. const isExisting = this.hasEventId();
  374. // No problemo! Our work here is done.
  375. this.hide();
  376. // Trigger the appropriate calendar event so that the view can be updated.
  377. if (isExisting) {
  378. $('body').trigger(CalendarEvents.updated, [response.event]);
  379. } else {
  380. $('body').trigger(CalendarEvents.created, [response.event]);
  381. }
  382. }
  383. return;
  384. })
  385. .catch(Notification.exception)
  386. .always(() => {
  387. // Regardless of success or error we should always stop
  388. // the loading icon and re-enable the buttons.
  389. loadingContainer.addClass('hidden');
  390. this.enableButtons();
  391. return;
  392. });
  393. }
  394. /**
  395. * Set up all of the event handling for the modal.
  396. *
  397. * @method registerEventListeners
  398. * @fires event:uploadStarted
  399. * @fires event:formSubmittedByJavascript
  400. */
  401. registerEventListeners() {
  402. // Apply parent event listeners.
  403. super.registerEventListeners(this);
  404. // When the user clicks the save button we trigger the form submission. We need to
  405. // trigger an actual submission because there is some JS code in the form that is
  406. // listening for this event and doing some stuff (e.g. saving draft areas etc).
  407. this.getModal().on(CustomEvents.events.activate, SELECTORS.SAVE_BUTTON, (e, data) => {
  408. this.getForm().submit();
  409. data.originalEvent.preventDefault();
  410. e.stopPropagation();
  411. });
  412. // Catch the submit event before it is actually processed by the browser and
  413. // prevent the submission. We'll take it from here.
  414. this.getModal().on('submit', (e) => {
  415. FormEvents.notifyFormSubmittedByJavascript(this.getForm()[0]);
  416. this.save();
  417. // Stop the form from actually submitting and prevent it's
  418. // propagation because we have already handled the event.
  419. e.preventDefault();
  420. e.stopPropagation();
  421. });
  422. }
  423. }
  424. ModalEventForm.registerModalType();