No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

app-calendar.js 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. /**
  2. * App Calendar
  3. */
  4. /**
  5. * ! If both start and end dates are same Full calendar will nullify the end date value.
  6. * ! Full calendar will end the event on a day before at 12:00:00AM thus, event won't extend to the end date.
  7. * ! We are getting events from a separate file named app-calendar-events.js. You can add or remove events from there.
  8. *
  9. **/
  10. 'use strict';
  11. document.addEventListener('DOMContentLoaded', function () {
  12. const direction = isRtl ? 'rtl' : 'ltr';
  13. (function () {
  14. // DOM Elements
  15. const calendarEl = document.getElementById('calendar');
  16. const appCalendarSidebar = document.querySelector('.app-calendar-sidebar');
  17. const addEventSidebar = document.getElementById('addEventSidebar');
  18. const appOverlay = document.querySelector('.app-overlay');
  19. const offcanvasTitle = document.querySelector('.offcanvas-title');
  20. const btnToggleSidebar = document.querySelector('.btn-toggle-sidebar');
  21. const btnSubmit = document.getElementById('addEventBtn');
  22. const btnDeleteEvent = document.querySelector('.btn-delete-event');
  23. const btnCancel = document.querySelector('.btn-cancel');
  24. const eventTitle = document.getElementById('eventTitle');
  25. const eventStartDate = document.getElementById('eventStartDate');
  26. const eventEndDate = document.getElementById('eventEndDate');
  27. const eventUrl = document.getElementById('eventURL');
  28. const eventLocation = document.getElementById('eventLocation');
  29. const eventDescription = document.getElementById('eventDescription');
  30. const allDaySwitch = document.querySelector('.allDay-switch');
  31. const selectAll = document.querySelector('.select-all');
  32. const filterInputs = Array.from(document.querySelectorAll('.input-filter'));
  33. const inlineCalendar = document.querySelector('.inline-calendar');
  34. // Calendar settings
  35. const calendarColors = {
  36. Business: 'primary',
  37. Holiday: 'success',
  38. Personal: 'danger',
  39. Family: 'warning',
  40. ETC: 'info'
  41. };
  42. // External jQuery Elements
  43. const eventLabel = $('#eventLabel'); // ! Using jQuery vars due to select2 jQuery dependency
  44. const eventGuests = $('#eventGuests'); // ! Using jQuery vars due to select2 jQuery dependency
  45. // Event Data
  46. let currentEvents = events; // Assuming events are imported from app-calendar-events.js
  47. let isFormValid = false;
  48. let eventToUpdate = null;
  49. let inlineCalInstance = null;
  50. // Offcanvas Instance
  51. const bsAddEventSidebar = new bootstrap.Offcanvas(addEventSidebar);
  52. //! TODO: Update Event label and guest code to JS once select removes jQuery dependency
  53. // Initialize Select2 with custom templates
  54. if (eventLabel.length) {
  55. function renderBadges(option) {
  56. if (!option.id) {
  57. return option.text;
  58. }
  59. var $badge =
  60. "<span class='badge badge-dot bg-" + $(option.element).data('label') + " me-2'> " + '</span>' + option.text;
  61. return $badge;
  62. }
  63. eventLabel.wrap('<div class="position-relative"></div>').select2({
  64. placeholder: 'Select value',
  65. dropdownParent: eventLabel.parent(),
  66. templateResult: renderBadges,
  67. templateSelection: renderBadges,
  68. minimumResultsForSearch: -1,
  69. escapeMarkup: function (es) {
  70. return es;
  71. }
  72. });
  73. }
  74. // Render guest avatars
  75. if (eventGuests.length) {
  76. function renderGuestAvatar(option) {
  77. if (!option.id) return option.text;
  78. return `
  79. <div class='d-flex flex-wrap align-items-center'>
  80. <div class='avatar avatar-xs me-2'>
  81. <img src='${assetsPath}img/avatars/${$(option.element).data('avatar')}'
  82. alt='avatar' class='rounded-circle' />
  83. </div>
  84. ${option.text}
  85. </div>`;
  86. }
  87. eventGuests.wrap('<div class="position-relative"></div>').select2({
  88. placeholder: 'Select value',
  89. dropdownParent: eventGuests.parent(),
  90. closeOnSelect: false,
  91. templateResult: renderGuestAvatar,
  92. templateSelection: renderGuestAvatar,
  93. escapeMarkup: function (es) {
  94. return es;
  95. }
  96. });
  97. }
  98. // Event start (flatpicker)
  99. if (eventStartDate) {
  100. var start = eventStartDate.flatpickr({
  101. monthSelectorType: 'static',
  102. static: true,
  103. enableTime: true,
  104. altFormat: 'Y-m-dTH:i:S',
  105. onReady: function (selectedDates, dateStr, instance) {
  106. if (instance.isMobile) {
  107. instance.mobileInput.setAttribute('step', null);
  108. }
  109. }
  110. });
  111. }
  112. // Event end (flatpicker)
  113. if (eventEndDate) {
  114. var end = eventEndDate.flatpickr({
  115. monthSelectorType: 'static',
  116. static: true,
  117. enableTime: true,
  118. altFormat: 'Y-m-dTH:i:S',
  119. onReady: function (selectedDates, dateStr, instance) {
  120. if (instance.isMobile) {
  121. instance.mobileInput.setAttribute('step', null);
  122. }
  123. }
  124. });
  125. }
  126. // Inline sidebar calendar (flatpicker)
  127. if (inlineCalendar) {
  128. inlineCalInstance = inlineCalendar.flatpickr({
  129. monthSelectorType: 'static',
  130. static: true,
  131. inline: true
  132. });
  133. }
  134. // Event click function
  135. function eventClick(info) {
  136. eventToUpdate = info.event;
  137. if (eventToUpdate.url) {
  138. info.jsEvent.preventDefault();
  139. window.open(eventToUpdate.url, '_blank');
  140. }
  141. bsAddEventSidebar.show();
  142. // For update event set offcanvas title text: Update Event
  143. if (offcanvasTitle) {
  144. offcanvasTitle.innerHTML = 'Update Event';
  145. }
  146. btnSubmit.innerHTML = 'Update';
  147. btnSubmit.classList.add('btn-update-event');
  148. btnSubmit.classList.remove('btn-add-event');
  149. btnDeleteEvent.classList.remove('d-none');
  150. eventTitle.value = eventToUpdate.title;
  151. start.setDate(eventToUpdate.start, true, 'Y-m-d');
  152. eventToUpdate.allDay === true ? (allDaySwitch.checked = true) : (allDaySwitch.checked = false);
  153. eventToUpdate.end !== null
  154. ? end.setDate(eventToUpdate.end, true, 'Y-m-d')
  155. : end.setDate(eventToUpdate.start, true, 'Y-m-d');
  156. eventLabel.val(eventToUpdate.extendedProps.calendar).trigger('change');
  157. eventToUpdate.extendedProps.location !== undefined
  158. ? (eventLocation.value = eventToUpdate.extendedProps.location)
  159. : null;
  160. eventToUpdate.extendedProps.guests !== undefined
  161. ? eventGuests.val(eventToUpdate.extendedProps.guests).trigger('change')
  162. : null;
  163. eventToUpdate.extendedProps.description !== undefined
  164. ? (eventDescription.value = eventToUpdate.extendedProps.description)
  165. : null;
  166. }
  167. // Modify sidebar toggler
  168. function modifyToggler() {
  169. const fcSidebarToggleButton = document.querySelector('.fc-sidebarToggle-button');
  170. fcSidebarToggleButton.classList.remove('fc-button-primary');
  171. fcSidebarToggleButton.classList.add('d-lg-none', 'd-inline-block', 'ps-0');
  172. while (fcSidebarToggleButton.firstChild) {
  173. fcSidebarToggleButton.firstChild.remove();
  174. }
  175. fcSidebarToggleButton.setAttribute('data-bs-toggle', 'sidebar');
  176. fcSidebarToggleButton.setAttribute('data-overlay', '');
  177. fcSidebarToggleButton.setAttribute('data-target', '#app-calendar-sidebar');
  178. fcSidebarToggleButton.insertAdjacentHTML(
  179. 'beforeend',
  180. '<i class="icon-base bx bx-menu icon-lg text-heading"></i>'
  181. );
  182. }
  183. // Filter events by calender
  184. function selectedCalendars() {
  185. let selected = [],
  186. filterInputChecked = [].slice.call(document.querySelectorAll('.input-filter:checked'));
  187. filterInputChecked.forEach(item => {
  188. selected.push(item.getAttribute('data-value'));
  189. });
  190. return selected;
  191. }
  192. // --------------------------------------------------------------------------------------------------
  193. // AXIOS: fetchEvents
  194. // * This will be called by fullCalendar to fetch events. Also this can be used to refetch events.
  195. // --------------------------------------------------------------------------------------------------
  196. function fetchEvents(info, successCallback) {
  197. let calendars = selectedCalendars();
  198. // We are reading event object from app-calendar-events.js file directly by including that file above app-calendar file.
  199. // You should make an API call, look into above commented API call for reference
  200. let selectedEvents = currentEvents.filter(function (event) {
  201. return calendars.includes(event.extendedProps.calendar.toLowerCase());
  202. });
  203. // if (selectedEvents.length > 0) {
  204. successCallback(selectedEvents);
  205. // }
  206. }
  207. // Init FullCalendar
  208. // ------------------------------------------------
  209. let calendar = new Calendar(calendarEl, {
  210. initialView: 'dayGridMonth',
  211. events: fetchEvents,
  212. plugins: [dayGridPlugin, interactionPlugin, listPlugin, timegridPlugin],
  213. editable: true,
  214. dragScroll: true,
  215. dayMaxEvents: 2,
  216. eventResizableFromStart: true,
  217. customButtons: {
  218. sidebarToggle: {
  219. text: 'Sidebar'
  220. }
  221. },
  222. headerToolbar: {
  223. start: 'sidebarToggle, prev,next, title',
  224. end: 'dayGridMonth,timeGridWeek,timeGridDay,listMonth'
  225. },
  226. direction: direction,
  227. initialDate: new Date(),
  228. navLinks: true, // can click day/week names to navigate views
  229. eventClassNames: function ({ event: calendarEvent }) {
  230. const colorName = calendarColors[calendarEvent._def.extendedProps.calendar];
  231. // Background Color
  232. return ['bg-label-' + colorName];
  233. },
  234. dateClick: function (info) {
  235. let date = moment(info.date).format('YYYY-MM-DD');
  236. resetValues();
  237. bsAddEventSidebar.show();
  238. // For new event set offcanvas title text: Add Event
  239. if (offcanvasTitle) {
  240. offcanvasTitle.innerHTML = 'Add Event';
  241. }
  242. btnSubmit.innerHTML = 'Add';
  243. btnSubmit.classList.remove('btn-update-event');
  244. btnSubmit.classList.add('btn-add-event');
  245. btnDeleteEvent.classList.add('d-none');
  246. eventStartDate.value = date;
  247. eventEndDate.value = date;
  248. },
  249. eventClick: function (info) {
  250. eventClick(info);
  251. },
  252. datesSet: function () {
  253. modifyToggler();
  254. },
  255. viewDidMount: function () {
  256. modifyToggler();
  257. }
  258. });
  259. // Render calendar
  260. calendar.render();
  261. // Modify sidebar toggler
  262. modifyToggler();
  263. const eventForm = document.getElementById('eventForm');
  264. const fv = FormValidation.formValidation(eventForm, {
  265. fields: {
  266. eventTitle: {
  267. validators: {
  268. notEmpty: {
  269. message: 'Please enter event title '
  270. }
  271. }
  272. },
  273. eventStartDate: {
  274. validators: {
  275. notEmpty: {
  276. message: 'Please enter start date '
  277. }
  278. }
  279. },
  280. eventEndDate: {
  281. validators: {
  282. notEmpty: {
  283. message: 'Please enter end date '
  284. }
  285. }
  286. }
  287. },
  288. plugins: {
  289. trigger: new FormValidation.plugins.Trigger(),
  290. bootstrap5: new FormValidation.plugins.Bootstrap5({
  291. // Use this for enabling/changing valid/invalid class
  292. eleValidClass: '',
  293. rowSelector: function (field, ele) {
  294. // field is the field name & ele is the field element
  295. return '.form-control-validation';
  296. }
  297. }),
  298. submitButton: new FormValidation.plugins.SubmitButton(),
  299. // Submit the form when all fields are valid
  300. // defaultSubmit: new FormValidation.plugins.DefaultSubmit(),
  301. autoFocus: new FormValidation.plugins.AutoFocus()
  302. }
  303. })
  304. .on('core.form.valid', function () {
  305. // Jump to the next step when all fields in the current step are valid
  306. isFormValid = true;
  307. })
  308. .on('core.form.invalid', function () {
  309. // if fields are invalid
  310. isFormValid = false;
  311. });
  312. // Sidebar Toggle Btn
  313. if (btnToggleSidebar) {
  314. btnToggleSidebar.addEventListener('click', e => {
  315. btnCancel.classList.remove('d-none');
  316. });
  317. }
  318. // Add Event
  319. // ------------------------------------------------
  320. function addEvent(eventData) {
  321. // ? Add new event data to current events object and refetch it to display on calender
  322. // ? You can write below code to AJAX call success response
  323. currentEvents.push(eventData);
  324. calendar.refetchEvents();
  325. // ? To add event directly to calender (won't update currentEvents object)
  326. // calendar.addEvent(eventData);
  327. }
  328. // Update Event
  329. // ------------------------------------------------
  330. function updateEvent(eventData) {
  331. // ? Update existing event data to current events object and refetch it to display on calender
  332. // ? You can write below code to AJAX call success response
  333. eventData.id = parseInt(eventData.id);
  334. currentEvents[currentEvents.findIndex(el => el.id === eventData.id)] = eventData; // Update event by id
  335. calendar.refetchEvents();
  336. // ? To update event directly to calender (won't update currentEvents object)
  337. // let propsToUpdate = ['id', 'title', 'url'];
  338. // let extendedPropsToUpdate = ['calendar', 'guests', 'location', 'description'];
  339. // updateEventInCalendar(eventData, propsToUpdate, extendedPropsToUpdate);
  340. }
  341. // Remove Event
  342. // ------------------------------------------------
  343. function removeEvent(eventId) {
  344. // ? Delete existing event data to current events object and refetch it to display on calender
  345. // ? You can write below code to AJAX call success response
  346. currentEvents = currentEvents.filter(function (event) {
  347. return event.id != eventId;
  348. });
  349. calendar.refetchEvents();
  350. // ? To delete event directly to calender (won't update currentEvents object)
  351. // removeEventInCalendar(eventId);
  352. }
  353. // (Update Event In Calendar (UI Only)
  354. // ------------------------------------------------
  355. const updateEventInCalendar = (updatedEventData, propsToUpdate, extendedPropsToUpdate) => {
  356. const existingEvent = calendar.getEventById(updatedEventData.id);
  357. // --- Set event properties except date related ----- //
  358. // ? Docs: https://fullcalendar.io/docs/Event-setProp
  359. // dateRelatedProps => ['start', 'end', 'allDay']
  360. // eslint-disable-next-line no-plusplus
  361. for (var index = 0; index < propsToUpdate.length; index++) {
  362. var propName = propsToUpdate[index];
  363. existingEvent.setProp(propName, updatedEventData[propName]);
  364. }
  365. // --- Set date related props ----- //
  366. // ? Docs: https://fullcalendar.io/docs/Event-setDates
  367. existingEvent.setDates(updatedEventData.start, updatedEventData.end, {
  368. allDay: updatedEventData.allDay
  369. });
  370. // --- Set event's extendedProps ----- //
  371. // ? Docs: https://fullcalendar.io/docs/Event-setExtendedProp
  372. // eslint-disable-next-line no-plusplus
  373. for (var index = 0; index < extendedPropsToUpdate.length; index++) {
  374. var propName = extendedPropsToUpdate[index];
  375. existingEvent.setExtendedProp(propName, updatedEventData.extendedProps[propName]);
  376. }
  377. };
  378. // Remove Event In Calendar (UI Only)
  379. // ------------------------------------------------
  380. function removeEventInCalendar(eventId) {
  381. calendar.getEventById(eventId).remove();
  382. }
  383. // Add new event
  384. // ------------------------------------------------
  385. btnSubmit.addEventListener('click', e => {
  386. if (btnSubmit.classList.contains('btn-add-event')) {
  387. if (isFormValid) {
  388. let newEvent = {
  389. id: calendar.getEvents().length + 1,
  390. title: eventTitle.value,
  391. start: eventStartDate.value,
  392. end: eventEndDate.value,
  393. startStr: eventStartDate.value,
  394. endStr: eventEndDate.value,
  395. display: 'block',
  396. extendedProps: {
  397. location: eventLocation.value,
  398. guests: eventGuests.val(),
  399. calendar: eventLabel.val(),
  400. description: eventDescription.value
  401. }
  402. };
  403. if (eventUrl.value) {
  404. newEvent.url = eventUrl.value;
  405. }
  406. if (allDaySwitch.checked) {
  407. newEvent.allDay = true;
  408. }
  409. addEvent(newEvent);
  410. bsAddEventSidebar.hide();
  411. }
  412. } else {
  413. // Update event
  414. // ------------------------------------------------
  415. if (isFormValid) {
  416. let eventData = {
  417. id: eventToUpdate.id,
  418. title: eventTitle.value,
  419. start: eventStartDate.value,
  420. end: eventEndDate.value,
  421. url: eventUrl.value,
  422. extendedProps: {
  423. location: eventLocation.value,
  424. guests: eventGuests.val(),
  425. calendar: eventLabel.val(),
  426. description: eventDescription.value
  427. },
  428. display: 'block',
  429. allDay: allDaySwitch.checked ? true : false
  430. };
  431. updateEvent(eventData);
  432. bsAddEventSidebar.hide();
  433. }
  434. }
  435. });
  436. // Call removeEvent function
  437. btnDeleteEvent.addEventListener('click', e => {
  438. removeEvent(parseInt(eventToUpdate.id));
  439. // eventToUpdate.remove();
  440. bsAddEventSidebar.hide();
  441. });
  442. // Reset event form inputs values
  443. // ------------------------------------------------
  444. function resetValues() {
  445. eventEndDate.value = '';
  446. eventUrl.value = '';
  447. eventStartDate.value = '';
  448. eventTitle.value = '';
  449. eventLocation.value = '';
  450. allDaySwitch.checked = false;
  451. eventGuests.val('').trigger('change');
  452. eventDescription.value = '';
  453. }
  454. // When modal hides reset input values
  455. addEventSidebar.addEventListener('hidden.bs.offcanvas', function () {
  456. resetValues();
  457. });
  458. // Hide left sidebar if the right sidebar is open
  459. btnToggleSidebar.addEventListener('click', e => {
  460. if (offcanvasTitle) {
  461. offcanvasTitle.innerHTML = 'Add Event';
  462. }
  463. btnSubmit.innerHTML = 'Add';
  464. btnSubmit.classList.remove('btn-update-event');
  465. btnSubmit.classList.add('btn-add-event');
  466. btnDeleteEvent.classList.add('d-none');
  467. appCalendarSidebar.classList.remove('show');
  468. appOverlay.classList.remove('show');
  469. });
  470. // Calender filter functionality
  471. // ------------------------------------------------
  472. if (selectAll) {
  473. selectAll.addEventListener('click', e => {
  474. if (e.currentTarget.checked) {
  475. document.querySelectorAll('.input-filter').forEach(c => (c.checked = 1));
  476. } else {
  477. document.querySelectorAll('.input-filter').forEach(c => (c.checked = 0));
  478. }
  479. calendar.refetchEvents();
  480. });
  481. }
  482. if (filterInputs) {
  483. filterInputs.forEach(item => {
  484. item.addEventListener('click', () => {
  485. document.querySelectorAll('.input-filter:checked').length < document.querySelectorAll('.input-filter').length
  486. ? (selectAll.checked = false)
  487. : (selectAll.checked = true);
  488. calendar.refetchEvents();
  489. });
  490. });
  491. }
  492. // Jump to date on sidebar(inline) calendar change
  493. inlineCalInstance.config.onChange.push(function (date) {
  494. calendar.changeView(calendar.view.type, moment(date[0]).format('YYYY-MM-DD'));
  495. modifyToggler();
  496. appCalendarSidebar.classList.remove('show');
  497. appOverlay.classList.remove('show');
  498. });
  499. })();
  500. });