diff options
| author | polo <ordipolo@gmx.fr> | 2026-01-29 22:51:19 +0100 |
|---|---|---|
| committer | polo <ordipolo@gmx.fr> | 2026-01-29 22:51:19 +0100 |
| commit | e3a42c8342bba0db15e2ca9a78911121e5d539da (patch) | |
| tree | df2b669452ba82774d741a4b9e48948b8dc45a0a | |
| parent | f007bac3b9172711dc0fcca1306270ab99dbd8a4 (diff) | |
| download | fullcalendar-e3a42c8342bba0db15e2ca9a78911121e5d539da.tar.gz fullcalendar-e3a42c8342bba0db15e2ca9a78911121e5d539da.tar.bz2 fullcalendar-e3a42c8342bba0db15e2ca9a78911121e5d539da.zip | |
| -rw-r--r-- | public/css/calendar.css | 8 | ||||
| -rw-r--r-- | public/index.php | 11 | ||||
| -rw-r--r-- | public/js/CalendarModalView.js | 120 | ||||
| -rw-r--r-- | public/js/calendar.js | 110 | ||||
| -rw-r--r-- | public/js/calendar_admin.js | 410 | ||||
| -rw-r--r-- | public/js/main.js | 16 | ||||
| -rw-r--r-- | src/controller/calendar.php | 27 |
7 files changed, 388 insertions, 314 deletions
diff --git a/public/css/calendar.css b/public/css/calendar.css index b24fe5b..c96b91e 100644 --- a/public/css/calendar.css +++ b/public/css/calendar.css | |||
| @@ -1,7 +1,8 @@ | |||
| 1 | .event{ | 1 | #event{ |
| 2 | border: 2px double; | 2 | border: 2px double; |
| 3 | border-radius: 5px; | 3 | border-radius: 5px; |
| 4 | width: max-content; | 4 | max-width: 250px; |
| 5 | /*width: max-content;*/ | ||
| 5 | padding: 5px; | 6 | padding: 5px; |
| 6 | } | 7 | } |
| 7 | #calendar_zone{ | 8 | #calendar_zone{ |
| @@ -33,6 +34,9 @@ td .fc-timegrid-axis{ | |||
| 33 | .fc-day-other{ | 34 | .fc-day-other{ |
| 34 | background-color: #f0f0f0; | 35 | background-color: #f0f0f0; |
| 35 | } | 36 | } |
| 37 | .fc-daygrid-day:not(.fc-day-other, .fc-day-today){ | ||
| 38 | background-color: #ffffff; | ||
| 39 | } | ||
| 36 | .fc-daygrid-day-top{ | 40 | .fc-daygrid-day-top{ |
| 37 | justify-content: center; | 41 | justify-content: center; |
| 38 | } | 42 | } |
diff --git a/public/index.php b/public/index.php index 540e8ab..a721db5 100644 --- a/public/index.php +++ b/public/index.php | |||
| @@ -11,6 +11,8 @@ require '../src/controller/calendar.php'; | |||
| 11 | <meta name="viewport" content="width=device-width"> | 11 | <meta name="viewport" content="width=device-width"> |
| 12 | <link rel="stylesheet" href="css/calendar.css"> | 12 | <link rel="stylesheet" href="css/calendar.css"> |
| 13 | <!-- <link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/fullcalendar/main.min.css' /> --> | 13 | <!-- <link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/fullcalendar/main.min.css' /> --> |
| 14 | <script src="js/main.js"></script> | ||
| 15 | <script src="js/CalendarModalView.js"></script> | ||
| 14 | <script src='js/fullcalendar/packages/core/index.global.min.js'></script> | 16 | <script src='js/fullcalendar/packages/core/index.global.min.js'></script> |
| 15 | <script src='js/fullcalendar/packages/daygrid/index.global.min.js'></script> | 17 | <script src='js/fullcalendar/packages/daygrid/index.global.min.js'></script> |
| 16 | <script src='js/fullcalendar/packages/timegrid/index.global.min.js'></script> | 18 | <script src='js/fullcalendar/packages/timegrid/index.global.min.js'></script> |
| @@ -18,15 +20,14 @@ require '../src/controller/calendar.php'; | |||
| 18 | <script src='js/fullcalendar/packages/interaction/index.global.min.js'></script> | 20 | <script src='js/fullcalendar/packages/interaction/index.global.min.js'></script> |
| 19 | <script src='js/fullcalendar/packages/core/locales/fr.global.min.js'></script> | 21 | <script src='js/fullcalendar/packages/core/locales/fr.global.min.js'></script> |
| 20 | <?php | 22 | <?php |
| 21 | if($_SESSION['admin'] === true){ | 23 | if($_SESSION['admin']){ |
| 22 | echo '<script src="js/calendar_admin.js"></script>' . "\n"; | 24 | echo '<script src="js/calendar_admin.js"></script>' . "\n"; |
| 23 | } | 25 | } |
| 24 | else{ | 26 | else{ |
| 25 | echo '<script src="js/calendar.js"></script>' . "\n"; | 27 | echo '<script src="js/calendar.js"></script>' . "\n"; |
| 26 | } | 28 | } |
| 27 | ?> | 29 | ?> |
| 28 | </head> | 30 | </head> |
| 29 | |||
| 30 | <body> | 31 | <body> |
| 31 | <div id="calendar_zone"> | 32 | <div id="calendar_zone"> |
| 32 | <div id="calendar"></div> | 33 | <div id="calendar"></div> |
diff --git a/public/js/CalendarModalView.js b/public/js/CalendarModalView.js new file mode 100644 index 0000000..a7afba9 --- /dev/null +++ b/public/js/CalendarModalView.js | |||
| @@ -0,0 +1,120 @@ | |||
| 1 | // js/CalendarModalView.js | ||
| 2 | |||
| 3 | class CalendarModalView { | ||
| 4 | constructor(data) { | ||
| 5 | this.modal = ''; | ||
| 6 | if (data.mode !== 'show' && data.mode !== 'new' && data.mode !== 'edit') { | ||
| 7 | throw new Error("unknown mode in CalendarModal's constructor"); | ||
| 8 | } | ||
| 9 | this.data = data; | ||
| 10 | } | ||
| 11 | getView() { | ||
| 12 | switch (this.data.mode) { | ||
| 13 | case 'show': | ||
| 14 | this.makeShowView(); | ||
| 15 | break; | ||
| 16 | case 'new': | ||
| 17 | case 'edit': | ||
| 18 | this.makeEditView(); | ||
| 19 | break; | ||
| 20 | default: | ||
| 21 | throw new Error("unknown mode set in CalendarModal"); | ||
| 22 | } | ||
| 23 | return this.modal; | ||
| 24 | } | ||
| 25 | makeShowView() { | ||
| 26 | if (this.data.mode !== 'show') { | ||
| 27 | throw new Error(''); | ||
| 28 | } | ||
| 29 | if (this.data.all_day) { | ||
| 30 | this.data.end.setDate(this.data.end.getDate() - 1); // jour de fin modifié pour ne pas faire bizarre pour l'utilisateur | ||
| 31 | } | ||
| 32 | this.modal += `<div id="event" style="border-color: ` + this.escapeHtml(this.data.color) + `;"> | ||
| 33 | <h3>` + this.escapeHtml(this.data.title) + `</h3> | ||
| 34 | <p><i>` + this.escapeHtml(this.data.description) + `</i></p>`; | ||
| 35 | // allDay un jour | ||
| 36 | if (this.data.all_day && (this.data.start.getTime() === this.data.end.getTime())) { // comparaison des timestamps | ||
| 37 | this.modal += `<p>le ` + this.escapeHtml(this.displayDate(this.data.start)) + `</p>`; // affichage simplifié évènement d'un jour | ||
| 38 | } | ||
| 39 | // allDay plusieurs jours | ||
| 40 | else if (this.data.all_day) { | ||
| 41 | this.modal += `<p>du ` + this.escapeHtml(this.displayDate(this.data.start)) + `<br> | ||
| 42 | au ` + this.escapeHtml(this.displayDate(this.data.end)) + `</p>`; | ||
| 43 | } | ||
| 44 | // non allDay | ||
| 45 | else { | ||
| 46 | this.modal += `<p>du ` + this.escapeHtml(this.displayDate(this.data.start)) + `<br> | ||
| 47 | à ` + this.escapeHtml(this.displayHour(this.data.start)) + `</p> | ||
| 48 | <p>au ` + this.escapeHtml(this.displayDate(this.data.end)) + `<br> | ||
| 49 | à ` + this.escapeHtml(this.displayHour(this.data.end)) + `</p>`; | ||
| 50 | } | ||
| 51 | this.modal += `<button class="event_close_button">Fermer</button> | ||
| 52 | </div>`; | ||
| 53 | } | ||
| 54 | displayDate(date) { | ||
| 55 | return date.getDate().toString().padStart(2, '0') + '/' + (date.getMonth() + 1).toString().padStart(2, '0') + '/' + date.getFullYear(); | ||
| 56 | } | ||
| 57 | displayHour(date) { | ||
| 58 | return date.getHours().toString().padStart(2, '0') + 'h' + date.getMinutes().toString().padStart(2, '0'); | ||
| 59 | } | ||
| 60 | makeEditView() { | ||
| 61 | if (this.data.mode === 'show') { | ||
| 62 | throw new Error(''); | ||
| 63 | } | ||
| 64 | if (this.data.all_day) { | ||
| 65 | this.data.end.setDate(this.data.end.getDate() - 1); // jour de fin modifié pour ne pas faire bizarre pour l'utilisateur | ||
| 66 | } | ||
| 67 | const title = this.data.mode === 'edit' ? this.data.title : ''; | ||
| 68 | const id = this.data.mode === 'edit' ? this.data.id : ''; | ||
| 69 | const description = this.data.mode === 'edit' ? this.data.description : ''; | ||
| 70 | const color = this.data.mode === 'edit' ? this.data.color : '#3788D8'; // bleu par défaut | ||
| 71 | this.modal += `<div id="event" style="border-color: ` + this.escapeHtml(color) + `;"> | ||
| 72 | <div class="event_title_box"> | ||
| 73 | <h2>` + (this.data.mode === 'edit' ? "Modifier un" : "Nouvel") + ` évènement</h2> | ||
| 74 | </div> | ||
| 75 | <div class=""> | ||
| 76 | <label for="event_title">Nom</label> | ||
| 77 | <input type="text" id="event_title" value="` + this.escapeHtml(title !== null && title !== void 0 ? title : '') + `"> | ||
| 78 | <input type="hidden" id="event_id" value="` + this.escapeHtml(id !== null && id !== void 0 ? id : '') + `"> | ||
| 79 | </div> | ||
| 80 | <div class=""> | ||
| 81 | <label for="event_description">Description</label> | ||
| 82 | <textarea id="event_description" cols="27">` + this.escapeHtml(description !== null && description !== void 0 ? description : '') + `</textarea> | ||
| 83 | </div> | ||
| 84 | <div class=""> | ||
| 85 | <input type="checkbox" id="event_all_day" class="event_all_day" ` + (this.data.all_day ? 'checked' : '') + `> | ||
| 86 | <label for="event_all_day">Journée entière</label> | ||
| 87 | </div> | ||
| 88 | <div class=""> | ||
| 89 | <label for="event_start">Début</label> | ||
| 90 | <input type="` + this.escapeHtml(this.data.input_type) + `" id="event_start" value="` + this.escapeHtml(this.makeInputValue(this.data.start)) + `"> | ||
| 91 | </div> | ||
| 92 | <div class=""> | ||
| 93 | <label for="event_end">Fin</label> | ||
| 94 | <input type="` + this.escapeHtml(this.data.input_type) + `" id="event_end" value="` + this.escapeHtml(this.makeInputValue(this.data.end)) + `"> | ||
| 95 | </div> | ||
| 96 | <div class=""> | ||
| 97 | <label for="event_color">Couleur</label> | ||
| 98 | <input type="color" id="event_color" value="` + this.escapeHtml(color) + `"> | ||
| 99 | </div>` | ||
| 100 | + (this.data.mode === 'edit' | ||
| 101 | ? '<button class="submit_update_event">Modifier</button><button class="delete_event">Supprimer</button>' | ||
| 102 | : '<button class="submit_new_event">Enregistrer</button>') | ||
| 103 | + `<button class="event_close_button">Annuler</button> | ||
| 104 | </div>`; | ||
| 105 | } | ||
| 106 | makeInputValue(date) { | ||
| 107 | return date.getFullYear() + '-' + (date.getMonth() + 1).toString().padStart(2, '0') + '-' + date.getDate().toString().padStart(2, '0') | ||
| 108 | + (this.data.all_day ? '' : 'T' + date.getHours().toString().padStart(2, '0') + ':' + date.getMinutes().toString().padStart(2, '0')); | ||
| 109 | } | ||
| 110 | escapeHtml(html) { | ||
| 111 | if (html === null || html === undefined) | ||
| 112 | return ''; | ||
| 113 | return String(html) | ||
| 114 | .replace(/&/g, "&") // & en premier | ||
| 115 | .replace(/</g, "<") | ||
| 116 | .replace(/>/g, ">") | ||
| 117 | .replace(/"/g, """) | ||
| 118 | .replace(/'/g, "'"); | ||
| 119 | } | ||
| 120 | } | ||
diff --git a/public/js/calendar.js b/public/js/calendar.js index a092632..ff1a401 100644 --- a/public/js/calendar.js +++ b/public/js/calendar.js | |||
| @@ -1,115 +1,97 @@ | |||
| 1 | // js/calendar.js | 1 | // js/calendar.js |
| 2 | 2 | ||
| 3 | document.addEventListener('DOMContentLoaded', function(){ | 3 | document.addEventListener('DOMContentLoaded', function () { |
| 4 | const calendarEl = document.getElementById('calendar'); | 4 | const calendarEl = getElementOrThrow('calendar'); |
| 5 | const modal = getElementOrThrow('event_modal'); | ||
| 5 | let selected_start_string = null; | 6 | let selected_start_string = null; |
| 6 | 7 | const calendar = new FullCalendar.Calendar(calendarEl, { | |
| 7 | const calendar = new FullCalendar.Calendar(calendarEl,{ | ||
| 8 | editable: true, | 8 | editable: true, |
| 9 | locale: 'fr', | 9 | locale: 'fr', |
| 10 | //timeZone: 'local', // à modifier pour être à l'heure d'un autre pays | 10 | //timeZone: 'local', // à modifier pour être à l'heure d'un autre pays |
| 11 | initialView: 'dayGridMonth', | 11 | initialView: 'dayGridMonth', |
| 12 | headerToolbar:{ | 12 | headerToolbar: { |
| 13 | left: 'prev,next today', | 13 | left: 'prev,next today', |
| 14 | center: 'title', | 14 | center: 'title', |
| 15 | right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek' | 15 | //right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek' |
| 16 | //right: 'dayGridMonth,timeGridWeek' | 16 | right: 'dayGridMonth,timeGridWeek,listWeek' |
| 17 | }, | 17 | }, |
| 18 | slotMinWidth: 70, | 18 | slotMinWidth: 70, |
| 19 | defaultAllDay: false, | 19 | defaultAllDay: false, |
| 20 | |||
| 21 | // numéros de semaine | 20 | // numéros de semaine |
| 22 | //weekNumbers: true, | 21 | //weekNumbers: true, |
| 23 | //weekText: 's', | 22 | //weekText: 's', |
| 24 | |||
| 25 | // vue mois | 23 | // vue mois |
| 26 | contentHeight: 600, // après initialisation: calendar.setOption('contentHeight', 650); | 24 | contentHeight: 600, // après initialisation: calendar.setOption('contentHeight', 650); |
| 27 | //aspectRatio: 1.5, // après initialisation: calendar.setOption('aspectRatio', 1.8); | 25 | //aspectRatio: 1.5, // après initialisation: calendar.setOption('aspectRatio', 1.8); |
| 28 | // pour recalculer la taille au redimensionnement du parent, exécuter: calendar.updateSize() | 26 | // pour recalculer la taille au redimensionnement du parent, exécuter: calendar.updateSize() |
| 29 | stickyHeaderDates: true, // garder les en-tête de colonnes lors du scroll | 27 | stickyHeaderDates: true, // garder les en-tête de colonnes lors du scroll |
| 30 | fixedWeekCount: false, // avec false, affiche 4, 5 ou 6 semaines selon le mois | 28 | fixedWeekCount: false, // avec false, affiche 4, 5 ou 6 semaines selon le mois |
| 31 | selectable: true, // sélection de jours multiples | 29 | selectable: true, // sélection de jours en cliquant dessus |
| 30 | longPressDelay: 300, // 1000ms par défaut | ||
| 32 | navLinks: true, // numéros de jour et de semaines clicables | 31 | navLinks: true, // numéros de jour et de semaines clicables |
| 33 | |||
| 34 | // vue semaine | 32 | // vue semaine |
| 35 | slotEventOverlap: true, // superposition (limitée) de deux évènements simultanés | 33 | slotEventOverlap: true, // superposition (limitée) de deux évènements simultanés |
| 36 | allDayContent: 'Journée', // texte dans la case "toute la journée" | 34 | allDayContent: 'Journée', // texte dans la case "toute la journée" |
| 37 | nowIndicator: true, // barre rouge pour maintenant | 35 | nowIndicator: true, // barre rouge pour maintenant |
| 38 | |||
| 39 | // params en plus: https://fullcalendar.io/docs/events-json-feed | 36 | // params en plus: https://fullcalendar.io/docs/events-json-feed |
| 40 | events: 'index.php?action=get_events', // fichier PHP qui retourne les événements | 37 | events: 'index.php?action=get_events', // fichier PHP qui retourne les événements |
| 41 | 38 | select: function (info) { | |
| 42 | select: function(info){ | ||
| 43 | selected_start_string = info.startStr; // variable "globale" | 39 | selected_start_string = info.startStr; // variable "globale" |
| 44 | hideModal(); | 40 | hideModal(); |
| 45 | }, | 41 | }, |
| 46 | //unselect: function(event, view){}, | 42 | // sélection d'une date simple sur mobile, évite des problèmes de conflit avec eventClick |
| 47 | 43 | dateClick: function (info) { | |
| 48 | eventClick: function(info){ | 44 | if (window.matchMedia('(pointer: coarse)').matches) { |
| 49 | const aside = document.querySelector('aside'); | 45 | const end = new Date(info.date.getTime()); |
| 50 | const checked = info.event.allDay ? 'checked' : ''; | 46 | calendar.view.type === 'dayGridMonth' ? end.setDate(end.getDate() + 1) : end.setMinutes(end.getMinutes() + 30); |
| 51 | 47 | // vue date: la fin est une date exclue | |
| 52 | // change des objets Date en chaînes compatibles avec les input | 48 | // vue semaine: durée de 30min par défaut |
| 53 | function formatDate(date){ | 49 | calendar.select(info.date, end); // appeler select() avec un seul paramètre ne marche pas avec la vue "mois" |
| 54 | return date.getFullYear() + '-' + (date.getMonth() + 1).toString().padStart(2, '0') + '-' + date.getDate().toString().padStart(2, '0') | ||
| 55 | + (info.event.allDay ? '' : 'T' + date.getHours().toString().padStart(2, '0') + ':' + date.getMinutes().toString().padStart(2, '0')); | ||
| 56 | } | ||
| 57 | function minusOneDay(date){ | ||
| 58 | date.setDate(date.getDate() - 1); // jour de fin modifié pour ne pas faire bizarre pour l'utilisateur | ||
| 59 | return date; | ||
| 60 | } | ||
| 61 | |||
| 62 | const start = formatDate(info.event.start); | ||
| 63 | const start_date = start.split('T')[0]; | ||
| 64 | const start_hour = (info.event.allDay ? '' : '<br>à ' + start.split('T')[1]).replace(":", "h"); | ||
| 65 | const formated_start = start_date.split('-')[2] + '/' + start_date.split('-')[1] + '/' + start_date.split('-')[0] + start_hour; | ||
| 66 | const end = formatDate(info.event.allDay ? minusOneDay(info.event.end) : info.event.end, info.event.allDay); | ||
| 67 | const end_date = end.split('T')[0]; | ||
| 68 | const end_hour = (info.event.allDay ? '' : '<br>à ' + end.split('T')[1]).replace(":", "h"); | ||
| 69 | const formated_end = end_date.split('-')[2] + '/' + end_date.split('-')[1] + '/' + end_date.split('-')[0] + end_hour; | ||
| 70 | |||
| 71 | let aside_content = `<div class="event" style="border-color: ` + info.event.backgroundColor +`;"> | ||
| 72 | <h3>` + info.event.title + `</h3> | ||
| 73 | <p><i>` + info.event.extendedProps.description + `</i></p>`; | ||
| 74 | if(checked && (formated_start === formated_end)){ // affichage simplifié évènement d'un jour | ||
| 75 | aside_content = aside_content + `<p>le ` + formated_start + `</p> | ||
| 76 | </div>`; | ||
| 77 | } | 50 | } |
| 78 | else{ | 51 | }, |
| 79 | aside_content = aside_content + `<p>du ` + formated_start + `</p> | 52 | //unselect: function(event, view){}, |
| 80 | <p>au ` + formated_end + `</p> | 53 | eventClick: function (info) { |
| 81 | </div>`; | 54 | if (!info.event.start || !info.event.end) { |
| 55 | throw new Error('modale non conforme'); | ||
| 82 | } | 56 | } |
| 83 | 57 | const modal_view = new CalendarModalView({ | |
| 84 | aside.innerHTML = aside_content; | 58 | mode: 'show', |
| 59 | title: info.event.title, | ||
| 60 | description: info.event.extendedProps.description, | ||
| 61 | color: info.event.backgroundColor, | ||
| 62 | all_day: info.event.allDay, | ||
| 63 | start: info.event.start, | ||
| 64 | end: info.event.end | ||
| 65 | }); | ||
| 66 | modal.innerHTML = modal_view.getView(); | ||
| 85 | calendar.updateSize(); | 67 | calendar.updateSize(); |
| 86 | }, | 68 | }, |
| 87 | viewDidMount: function(info){ // déclenché lorsque qu'une nouvelle vue est chargée (mois, semaine...) | 69 | viewDidMount: function (_info) { |
| 88 | if(selected_start_string){ | 70 | hideModal(); |
| 71 | if (selected_start_string) { | ||
| 89 | calendar.gotoDate(new Date(selected_start_string)); | 72 | calendar.gotoDate(new Date(selected_start_string)); |
| 90 | } | 73 | } |
| 91 | }, | 74 | }, |
| 92 | //datesSet: function(info){}, // déclenché lorsque des dates affichées sont chargées (= comme viewDidMount + changement de date) | 75 | //datesSet: function(info){}, // déclenché lorsque des dates affichées sont chargées (= comme viewDidMount + changement de date) |
| 93 | }); | 76 | }); |
| 94 | 77 | function hideModal() { | |
| 95 | function hideModal(){ | 78 | modal.innerHTML = ''; |
| 96 | const aside = document.querySelector('aside'); | ||
| 97 | aside.innerHTML = ''; | ||
| 98 | calendar.updateSize(); | 79 | calendar.updateSize(); |
| 99 | } | 80 | } |
| 100 | 81 | document.addEventListener('keydown', function (event) { | |
| 101 | document.addEventListener('keydown', function(event){ | 82 | if (event.key === 'Escape') { |
| 102 | if(event.key === 'Escape') { | ||
| 103 | hideModal(); | 83 | hideModal(); |
| 104 | } | 84 | } |
| 105 | }); | 85 | }); |
| 106 | |||
| 107 | // technique de la délégation d'événements pour utiliser un bouton ajouté dynamiquement | 86 | // technique de la délégation d'événements pour utiliser un bouton ajouté dynamiquement |
| 108 | document.addEventListener('click', function(event){ | 87 | document.addEventListener('click', function (event) { |
| 109 | if(event.target.classList.contains('event_close_button')){ | 88 | if (!event.target) { |
| 89 | throw new Error('évènement click non conforme'); | ||
| 90 | } | ||
| 91 | assertElementType(event.target, HTMLElement); | ||
| 92 | if (event.target.classList.contains('event_close_button')) { | ||
| 110 | hideModal(); | 93 | hideModal(); |
| 111 | } | 94 | } |
| 112 | }); | 95 | }); |
| 113 | |||
| 114 | calendar.render(); | 96 | calendar.render(); |
| 115 | }); \ No newline at end of file | 97 | }); \ No newline at end of file |
diff --git a/public/js/calendar_admin.js b/public/js/calendar_admin.js index a99b069..df8e19d 100644 --- a/public/js/calendar_admin.js +++ b/public/js/calendar_admin.js | |||
| @@ -1,220 +1,164 @@ | |||
| 1 | // js/calendar_admin.js | 1 | // js/calendar_admin.js |
| 2 | 2 | ||
| 3 | document.addEventListener('DOMContentLoaded', function(){ | 3 | document.addEventListener('DOMContentLoaded', function () { |
| 4 | const calendarEl = document.getElementById('calendar'); | 4 | const calendarEl = getElementOrThrow('calendar'); |
| 5 | const modal = getElementOrThrow('event_modal'); | ||
| 5 | let selected_start_string = null; | 6 | let selected_start_string = null; |
| 6 | let event_selected = false; // pour event.remove() | 7 | let event_selected = false; // pour event.remove() |
| 7 | 8 | const calendar = new FullCalendar.Calendar(calendarEl, { | |
| 8 | const calendar = new FullCalendar.Calendar(calendarEl,{ | ||
| 9 | editable: true, | 9 | editable: true, |
| 10 | locale: 'fr', | 10 | locale: 'fr', |
| 11 | //timeZone: 'local', // à modifier pour être à l'heure d'un autre pays | 11 | //timeZone: 'local', // à modifier pour être à l'heure d'un autre pays |
| 12 | initialView: 'dayGridMonth', | 12 | initialView: 'dayGridMonth', |
| 13 | headerToolbar:{ | 13 | headerToolbar: { |
| 14 | left: 'prev,next today', | 14 | left: 'prev,next today', |
| 15 | center: 'title', | 15 | center: 'title', |
| 16 | right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek' | 16 | //right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek' |
| 17 | //right: 'dayGridMonth,timeGridWeek' | 17 | right: 'dayGridMonth,timeGridWeek,listWeek' |
| 18 | }, | 18 | }, |
| 19 | slotMinWidth: 70, | 19 | slotMinWidth: 70, |
| 20 | defaultAllDay: false, | 20 | defaultAllDay: false, |
| 21 | |||
| 22 | // numéros de semaine | 21 | // numéros de semaine |
| 23 | //weekNumbers: true, | 22 | //weekNumbers: true, |
| 24 | //weekText: 's', | 23 | //weekText: 's', |
| 25 | |||
| 26 | // vue mois | 24 | // vue mois |
| 27 | contentHeight: 600, // après initialisation: calendar.setOption('contentHeight', 650); | 25 | contentHeight: 600, // après initialisation: calendar.setOption('contentHeight', 650); |
| 28 | //aspectRatio: 1.5, // après initialisation: calendar.setOption('aspectRatio', 1.8); | 26 | //aspectRatio: 1.5, // après initialisation: calendar.setOption('aspectRatio', 1.8); |
| 29 | // pour recalculer la taille au redimensionnement du parent, exécuter: calendar.updateSize() | 27 | // pour recalculer la taille au redimensionnement du parent, exécuter: calendar.updateSize() |
| 30 | stickyHeaderDates: true, // garder les en-tête de colonnes lors du scroll | 28 | stickyHeaderDates: true, // garder les en-tête de colonnes lors du scroll |
| 31 | fixedWeekCount: false, // avec false, affiche 4, 5 ou 6 semaines selon le mois | 29 | fixedWeekCount: false, // avec false, affiche 4, 5 ou 6 semaines selon le mois |
| 32 | selectable: true, // sélection de jours multiples | 30 | selectable: true, // sélection de jours en cliquant dessus |
| 31 | longPressDelay: 300, // 1000ms par défaut | ||
| 33 | navLinks: true, // numéros de jour et de semaines clicables | 32 | navLinks: true, // numéros de jour et de semaines clicables |
| 34 | |||
| 35 | // vue semaine | 33 | // vue semaine |
| 36 | slotEventOverlap: true, // superposition (limitée) de deux évènements simultanés | 34 | slotEventOverlap: true, // superposition (limitée) de deux évènements simultanés |
| 37 | allDayContent: 'Journée', // texte dans la case "toute la journée" | 35 | allDayContent: 'Journée', // texte dans la case "toute la journée" |
| 38 | nowIndicator: true, // barre rouge pour maintenant | 36 | nowIndicator: true, // barre rouge pour maintenant |
| 39 | |||
| 40 | // params en plus: https://fullcalendar.io/docs/events-json-feed | 37 | // params en plus: https://fullcalendar.io/docs/events-json-feed |
| 41 | events: 'index.php?action=get_events', // fichier PHP qui retourne les événements | 38 | events: 'index.php?action=get_events', // fichier PHP qui retourne les événements |
| 42 | 39 | select: function (info) { | |
| 43 | select: function(info){ | ||
| 44 | selected_start_string = info.startStr; // variable "globale" | 40 | selected_start_string = info.startStr; // variable "globale" |
| 45 | event_selected = false; | 41 | event_selected = false; |
| 46 | const aside = document.querySelector('aside'); | 42 | let all_day = false; |
| 47 | let checked = ''; | 43 | let input_type = 'datetime-local'; // input datetime-local = date + heure |
| 48 | let input = 'datetime-local'; | 44 | const start = new Date(info.startStr); |
| 49 | |||
| 50 | // on veut des chaines de la forme 2025-05-20T07:05 | ||
| 51 | // il faut retirer les secondes et le fuseau horaire du format ISO, c'est chiant | ||
| 52 | // on enverra par contre une chaine ISO au serveur pour avoir un enregistrement correct | ||
| 53 | |||
| 54 | let start_value; | ||
| 55 | let end_value; | ||
| 56 | const end = new Date(info.endStr); | 45 | const end = new Date(info.endStr); |
| 57 | 46 | if (calendar.view.type == 'dayGridMonth') { | |
| 58 | if(calendar.view.type == 'dayGridMonth'){ | ||
| 59 | start_value = info.startStr + 'T10:00'; | ||
| 60 | end.setDate(end.getDate() - 1); // jour de fin modifié pour ne pas faire bizarre pour l'utilisateur | 47 | end.setDate(end.getDate() - 1); // jour de fin modifié pour ne pas faire bizarre pour l'utilisateur |
| 61 | end.setHours(11); | 48 | end.setHours(start.getTime() === end.getTime() ? 11 : 10); |
| 62 | end_value = end.toISOString().split('T')[0] + 'T11:00'; | 49 | start.setHours(10); |
| 63 | } | 50 | } |
| 64 | else if(calendar.view.type == 'timeGridWeek' || calendar.view.type == 'timeGridDay'){ | 51 | else if (calendar.view.type == 'timeGridWeek' && info.startStr.split("T").length === 1) { // clic sur la ligne "Journée" => évènement allDay |
| 65 | const start_array = info.startStr.split("T"); | 52 | all_day = true; |
| 66 | const end_array = info.endStr.split("T"); | 53 | input_type = 'date'; |
| 67 | |||
| 68 | // clic sur la ligne "Journée", = 'dayGridMonth' | ||
| 69 | if(start_array.length == 1){ | ||
| 70 | checked = 'checked'; | ||
| 71 | input = 'date'; | ||
| 72 | start_value = info.startStr; | ||
| 73 | end.setDate(end.getDate() - 1); | ||
| 74 | end_value = end.toISOString().split('T')[0]; | ||
| 75 | } | ||
| 76 | else if(start_array.length == 2){ | ||
| 77 | start_value = start_array[0] + "T" + start_array[1].substr(0,5); // format 2025-06-12T10:00 | ||
| 78 | end_value = end_array[0] + "T" + end_array[1].substr(0,5); | ||
| 79 | } | ||
| 80 | } | 54 | } |
| 81 | 55 | //else if(calendar.view.type == 'timeGridDay'){} | |
| 82 | const aside_content = `<div class="form_event"> | 56 | const modal_view = new CalendarModalView({ |
| 83 | <div class="event_title_box"> | 57 | mode: 'new', |
| 84 | <h2>Nouvel évènement</h2> | 58 | all_day: all_day, |
| 85 | </div> | 59 | input_type: input_type, |
| 86 | <div class=""> | 60 | start: start, |
| 87 | <label for="event_title">Nom</label> | 61 | end: end |
| 88 | <input type="text" id="event_title"> | 62 | }); |
| 89 | </div> | 63 | modal.innerHTML = modal_view.getView(); |
| 90 | <div class=""> | ||
| 91 | <label for="event_description">Description</label> | ||
| 92 | <textarea id="event_description" cols="27"></textarea> | ||
| 93 | </div> | ||
| 94 | <div class=""> | ||
| 95 | <input type="checkbox" id="event_all_day" class="event_all_day" ` + checked + `> | ||
| 96 | <label for="event_all_day">Journée entière</label> | ||
| 97 | </div> | ||
| 98 | <div class=""> | ||
| 99 | <label for="event_start">Début</label> | ||
| 100 | <input type="` + input + `" id="event_start" value="` + start_value + `"> | ||
| 101 | </div> | ||
| 102 | <div class=""> | ||
| 103 | <label for="event_end">Fin</label> | ||
| 104 | <input type="` + input + `" id="event_end" value="` + end_value + `"> | ||
| 105 | </div> | ||
| 106 | <div class=""> | ||
| 107 | <label for="event_color">Couleur</label> | ||
| 108 | <input type="color" id="event_color" value="#3788D8"> | ||
| 109 | </div> | ||
| 110 | <button class="submit_new_event">Enregistrer</button> | ||
| 111 | <button class="event_close_button">Annuler</button> | ||
| 112 | </div>`; | ||
| 113 | aside.innerHTML = aside_content; | ||
| 114 | calendar.updateSize(); | 64 | calendar.updateSize(); |
| 115 | }, | 65 | }, |
| 66 | // sélection d'une date simple sur mobile, évite des problèmes de conflit avec eventClick | ||
| 67 | dateClick: function (info) { | ||
| 68 | if (window.matchMedia('(pointer: coarse)').matches) { | ||
| 69 | const end = new Date(info.date); | ||
| 70 | calendar.view.type === 'dayGridMonth' ? end.setDate(end.getDate() + 1) : end.setMinutes(end.getMinutes() + 30); | ||
| 71 | // vue date: la fin est une date exclue | ||
| 72 | // vue semaine: durée de 30min par défaut | ||
| 73 | const local_end_date_array = end.toLocaleDateString().split('/'); | ||
| 74 | let local_end_str = local_end_date_array[2] + '-' + local_end_date_array[1] + '-' + local_end_date_array[0]; | ||
| 75 | if (calendar.view.type != 'dayGridMonth') { | ||
| 76 | local_end_str += 'T' + end.toLocaleTimeString(); | ||
| 77 | } | ||
| 78 | // on a besoin de deux chaînes représentant l'heure locale pour obtenir le même objet "info" qu'avec l'évènement select | ||
| 79 | // la vue "dayGridMonth" nécessite deux paramètres, par sécurité on en enverra toujours deux | ||
| 80 | calendar.select(info.dateStr, local_end_str); | ||
| 81 | } | ||
| 82 | }, | ||
| 116 | //unselect: function(event, view){}, | 83 | //unselect: function(event, view){}, |
| 117 | eventClick: function(info){ | 84 | eventClick: function (info) { |
| 118 | event_selected = true; // variable "globale" | 85 | event_selected = true; // variable "globale" |
| 119 | const aside = document.querySelector('aside'); | 86 | if (!info.event.start || !info.event.end) { |
| 120 | const checked = info.event.allDay ? 'checked' : ''; | 87 | throw new Error("info.event.start ou info.event.end est null"); |
| 121 | const input = info.event.allDay ? 'date' : 'datetime-local'; | ||
| 122 | |||
| 123 | // change des objets Date en chaînes compatibles avec les input | ||
| 124 | function formatDate(date){ | ||
| 125 | return date.getFullYear() + '-' + (date.getMonth() + 1).toString().padStart(2, '0') + '-' + date.getDate().toString().padStart(2, '0') | ||
| 126 | + (info.event.allDay ? '' : 'T' + date.getHours().toString().padStart(2, '0') + ':' + date.getMinutes().toString().padStart(2, '0')); | ||
| 127 | } | ||
| 128 | function minusOneDay(date){ | ||
| 129 | date.setDate(date.getDate() - 1); // jour de fin modifié pour ne pas faire bizarre pour l'utilisateur | ||
| 130 | return date; | ||
| 131 | } | 88 | } |
| 132 | 89 | const modal_view = new CalendarModalView({ | |
| 133 | const formated_start = formatDate(info.event.start); | 90 | mode: 'edit', |
| 134 | const formated_end = formatDate(info.event.allDay ? minusOneDay(info.event.end) : info.event.end, info.event.allDay); | 91 | input_type: info.event.allDay ? 'date' : 'datetime-local', |
| 135 | 92 | id: info.event.id, | |
| 136 | const aside_content = `<div class="form_event"> | 93 | title: info.event.title, |
| 137 | <div class="event_title_box"> | 94 | description: info.event.extendedProps.description, |
| 138 | <h2>Modifier un évènement</h2> | 95 | color: info.event.backgroundColor, |
| 139 | </div> | 96 | all_day: info.event.allDay, |
| 140 | <div class=""> | 97 | start: info.event.start, |
| 141 | <label for="event_title">Nom</label> | 98 | end: info.event.end |
| 142 | <input type="text" id="event_title" value="` + info.event.title + `"> | 99 | }); |
| 143 | <input type="hidden" id="event_id" value="` + info.event.id + `"> | 100 | modal.innerHTML = modal_view.getView(); |
| 144 | </div> | ||
| 145 | <div class=""> | ||
| 146 | <label for="event_description">Description</label> | ||
| 147 | <textarea id="event_description" cols="27">` + info.event.extendedProps.description + `</textarea> | ||
| 148 | </div> | ||
| 149 | <div class=""> | ||
| 150 | <input type="checkbox" id="event_all_day" class="event_all_day" ` + checked + `> | ||
| 151 | <label for="event_all_day">Journée entière</label> | ||
| 152 | </div> | ||
| 153 | <div class=""> | ||
| 154 | <label for="event_start">Début</label> | ||
| 155 | <input type="` + input + `" id="event_start" value="` + formated_start + `"> | ||
| 156 | </div> | ||
| 157 | <div class=""> | ||
| 158 | <label for="event_end">Fin</label> | ||
| 159 | <input type="` + input + `" id="event_end" value="` + formated_end + `"> | ||
| 160 | </div> | ||
| 161 | <div class=""> | ||
| 162 | <label for="event_color">Couleur</label> | ||
| 163 | <input type="color" id="event_color" value="` + info.event.backgroundColor + `"> | ||
| 164 | </div> | ||
| 165 | <button class="submit_update_event">Modifier</button> | ||
| 166 | <button class="event_close_button">Annuler</button> | ||
| 167 | <button class="delete_event">Supprimer</button> | ||
| 168 | </div>`; | ||
| 169 | aside.innerHTML = aside_content; | ||
| 170 | calendar.updateSize(); | 101 | calendar.updateSize(); |
| 171 | }, | 102 | }, |
| 172 | viewDidMount: function(info){ // déclenché lorsque qu'une nouvelle vue est chargée (mois, semaine...) | 103 | viewDidMount: function (_info) { |
| 173 | if(selected_start_string){ | 104 | hideModal(); |
| 105 | if (selected_start_string) { | ||
| 174 | calendar.gotoDate(new Date(selected_start_string)); | 106 | calendar.gotoDate(new Date(selected_start_string)); |
| 175 | } | 107 | } |
| 176 | }, | 108 | }, |
| 177 | //datesSet: function(info){}, // déclenché lorsque des dates affichées sont chargées (= comme viewDidMount + changement de date) | 109 | //datesSet: function(info){}, // déclenché lorsque des dates affichées sont chargées (= comme viewDidMount + changement de date) |
| 178 | }); | 110 | }); |
| 179 | 111 | function hideModal() { | |
| 180 | function hideModal(){ | ||
| 181 | const aside = document.querySelector('aside'); | ||
| 182 | event_selected = false; | 112 | event_selected = false; |
| 183 | aside.innerHTML = ''; | 113 | modal.innerHTML = ''; |
| 184 | calendar.updateSize(); | 114 | calendar.updateSize(); |
| 185 | } | 115 | } |
| 186 | 116 | function submitEvent(new_event) { | |
| 187 | function submitEvent(new_event){ | 117 | const event_title_input = getElementOrThrow('event_title'); |
| 188 | const event_title = document.getElementById('event_title').value; | 118 | const event_description_input = getElementOrThrow('event_description'); |
| 189 | const event_description = document.getElementById('event_description').value; | 119 | const event_all_day_input = getElementOrThrow('event_all_day'); |
| 190 | const event_all_day = document.getElementById('event_all_day').checked; | 120 | const event_start_input = getElementOrThrow('event_start'); |
| 191 | let event_start = document.getElementById('event_start').value; | 121 | const event_end_input = getElementOrThrow('event_end'); |
| 192 | let event_end = document.getElementById('event_end').value; | 122 | const event_color_input = getElementOrThrow('event_color'); |
| 193 | const event_color = document.getElementById('event_color').value; // #3788d8 par défaut | 123 | assertElementType(event_title_input, HTMLInputElement); |
| 194 | const event_id = new_event ? '' : document.getElementById('event_id').value; | 124 | assertElementType(event_description_input, HTMLTextAreaElement); |
| 195 | 125 | assertElementType(event_all_day_input, HTMLInputElement); | |
| 196 | if(event_title.length !== 0 && event_start.length !== 0 && event_end.length !== 0 && event_color.length !== 0 | 126 | assertElementType(event_start_input, HTMLInputElement); |
| 197 | && (new_event || event_id.length !== 0)) | 127 | assertElementType(event_end_input, HTMLInputElement); |
| 198 | { | 128 | assertElementType(event_color_input, HTMLInputElement); |
| 199 | if(event_all_day){ | 129 | const event_title = event_title_input.value; |
| 130 | const event_description = event_description_input.value; | ||
| 131 | const event_all_day = event_all_day_input.checked; | ||
| 132 | let event_start = new Date(event_start_input.value); | ||
| 133 | let event_end = new Date(event_end_input.value); | ||
| 134 | const event_color = event_color_input.value; // #3788d8 par défaut | ||
| 135 | let event_id = ''; | ||
| 136 | if (!new_event) { | ||
| 137 | //const event_id_input: HTMLElement|null = document.getElementById('event_id'); | ||
| 138 | const event_id_input = getElementOrThrow('event_id'); | ||
| 139 | assertElementType(event_id_input, HTMLInputElement); | ||
| 140 | event_id = event_id_input.value; | ||
| 141 | } | ||
| 142 | // contrôle de saisie | ||
| 143 | if (event_title.length !== 0 && event_start_input.value.length !== 0 && event_end_input.value.length !== 0 && event_color.length !== 0 | ||
| 144 | && (new_event || event_id.length !== 0)) { | ||
| 145 | if (event_all_day) { | ||
| 200 | // on remet le jour de fin exclu | 146 | // on remet le jour de fin exclu |
| 201 | const tmp_object = new Date(event_end); | 147 | event_end.setDate(event_end.getDate() + 1); |
| 202 | tmp_object.setDate(tmp_object.getDate() + 1); | ||
| 203 | event_end = tmp_object.toISOString().split('T')[0]; | ||
| 204 | } | 148 | } |
| 205 | else{ | 149 | else { |
| 206 | event_start = new Date(event_start).toISOString(); | 150 | event_start = new Date(event_start); |
| 207 | event_end = new Date(event_end).toISOString(); | 151 | event_end = new Date(event_end); |
| 208 | } | 152 | } |
| 209 | console.log(event_end); | 153 | // contrôle date/heure de fin après le début |
| 210 | 154 | if (event_start > event_end || (!event_all_day && event_start == event_end)) { | |
| 211 | if(event_start > event_end || (!event_all_day && event_start == event_end)){ | 155 | console.log("Erreur: la fin de l'évènement doit se situer après son début."); |
| 212 | return; | 156 | return; |
| 213 | } | 157 | } |
| 214 | |||
| 215 | // création | 158 | // création |
| 216 | if(new_event){ | 159 | if (new_event) { |
| 217 | const event = { | 160 | const event = { |
| 161 | id: '', | ||
| 218 | title: event_title, | 162 | title: event_title, |
| 219 | description: event_description, | 163 | description: event_description, |
| 220 | allDay: event_all_day, | 164 | allDay: event_all_day, |
| @@ -222,7 +166,6 @@ document.addEventListener('DOMContentLoaded', function(){ | |||
| 222 | end: event_end, | 166 | end: event_end, |
| 223 | color: event_color | 167 | color: event_color |
| 224 | }; | 168 | }; |
| 225 | |||
| 226 | fetch('index.php?action=new_event', { | 169 | fetch('index.php?action=new_event', { |
| 227 | method: 'POST', | 170 | method: 'POST', |
| 228 | headers: { | 171 | headers: { |
| @@ -230,146 +173,145 @@ document.addEventListener('DOMContentLoaded', function(){ | |||
| 230 | }, | 173 | }, |
| 231 | body: JSON.stringify(event), | 174 | body: JSON.stringify(event), |
| 232 | }) | 175 | }) |
| 233 | .then(response => response.json()) | 176 | .then(response => response.json()) |
| 234 | .then(data => { | 177 | .then(data => { |
| 235 | if(data.success){ | 178 | if (data.success) { |
| 236 | event.id = data.id; | 179 | event.id = data.id; |
| 237 | calendar.addEvent(event); | 180 | calendar.addEvent(event); |
| 238 | hideModal(); | 181 | hideModal(); |
| 239 | } | 182 | } |
| 240 | }) | 183 | }) |
| 241 | .catch((error) => { | 184 | .catch((error) => { |
| 242 | console.error('Error:', error); | 185 | console.error('Error:', error); |
| 243 | }); | 186 | }); |
| 244 | |||
| 245 | } | 187 | } |
| 246 | // modification | 188 | // modification |
| 247 | else{ | 189 | else { |
| 248 | const event = calendar.getEventById(event_id); | 190 | const event = calendar.getEventById(event_id); |
| 249 | 191 | if (!event) { | |
| 250 | if(event){ | 192 | throw new Error("Événement non trouvé !"); |
| 251 | const event_copy = { | 193 | } |
| 252 | id: parseInt(event.id), | 194 | const event_copy = { |
| 253 | description: event_description, | 195 | id: parseInt(event.id), |
| 254 | title: event_title, | 196 | description: event_description, |
| 255 | allDay: event_all_day, | 197 | title: event_title, |
| 256 | start: event_start, | 198 | allDay: event_all_day, |
| 257 | end: event_end, | 199 | start: event_start, |
| 258 | color: event_color | 200 | end: event_end, |
| 259 | }; | 201 | color: event_color |
| 260 | 202 | }; | |
| 261 | fetch('index.php?action=update_event', { | 203 | fetch('index.php?action=update_event', { |
| 262 | method: 'POST', | 204 | method: 'POST', |
| 263 | headers: { | 205 | headers: { |
| 264 | 'Content-Type': 'application/json', | 206 | 'Content-Type': 'application/json', |
| 265 | }, | 207 | }, |
| 266 | body: JSON.stringify(event_copy), | 208 | body: JSON.stringify(event_copy), |
| 267 | }) | 209 | }) |
| 268 | .then(response => response.json()) | 210 | .then(response => response.json()) |
| 269 | .then(data => { | 211 | .then(data => { |
| 270 | if(data.success){ | 212 | if (data.success) { |
| 271 | event.setProp('title', event_title); | 213 | event.setProp('title', event_title); |
| 272 | event.setExtendedProp('description', event_description); | 214 | event.setExtendedProp('description', event_description); |
| 273 | event.setAllDay(event_all_day); | 215 | event.setAllDay(event_all_day); |
| 274 | event.setStart(event_start); | 216 | event.setStart(event_start); |
| 275 | event.setEnd(event_end); | 217 | event.setEnd(event_end); |
| 276 | event.setProp('color', event_color); | 218 | event.setProp('color', event_color); |
| 277 | hideModal(); | 219 | hideModal(); |
| 278 | } | 220 | } |
| 279 | }) | 221 | }) |
| 280 | .catch((error) => { | 222 | .catch((error) => { |
| 281 | console.error('Error:', error); | 223 | console.error('Error:', error); |
| 282 | }); | 224 | }); |
| 283 | } | ||
| 284 | else{ | ||
| 285 | console.log("Événement non trouvé !"); | ||
| 286 | } | ||
| 287 | } | 225 | } |
| 288 | } | 226 | } |
| 289 | else{ | 227 | else { |
| 290 | // notif input vide | 228 | // notif input vide |
| 291 | console.log('erreur: input vide'); | 229 | console.log('erreur: input vide'); |
| 292 | } | 230 | } |
| 293 | } | 231 | } |
| 294 | 232 | function checkAllDay() { | |
| 295 | function checkAllDay(){ | 233 | const event_start_input = getElementOrThrow('event_start'); |
| 296 | const event_start_input = document.getElementById('event_start'); | 234 | const event_end_input = getElementOrThrow('event_end'); |
| 297 | const event_end_input = document.getElementById('event_end'); | 235 | const event_all_day = getElementOrThrow('event_all_day'); |
| 298 | 236 | assertElementType(event_start_input, HTMLInputElement); | |
| 237 | assertElementType(event_end_input, HTMLInputElement); | ||
| 238 | assertElementType(event_all_day, HTMLInputElement); | ||
| 299 | const start = event_start_input.value; | 239 | const start = event_start_input.value; |
| 300 | const end = event_end_input.value; | 240 | const end = event_end_input.value; |
| 301 | 241 | if (event_all_day.checked) { | |
| 302 | if(document.getElementById('event_all_day').checked){ | ||
| 303 | event_start_input.type = 'date'; | 242 | event_start_input.type = 'date'; |
| 304 | event_end_input.type = 'date'; | 243 | event_end_input.type = 'date'; |
| 305 | |||
| 306 | event_start_input.value = start.split('T')[0]; | 244 | event_start_input.value = start.split('T')[0]; |
| 307 | event_end_input.value = end.split('T')[0]; | 245 | event_end_input.value = end.split('T')[0]; |
| 308 | } | 246 | } |
| 309 | else{ | 247 | else { |
| 310 | event_start_input.type = 'datetime-local'; | 248 | event_start_input.type = 'datetime-local'; |
| 311 | event_end_input.type = 'datetime-local'; | 249 | event_end_input.type = 'datetime-local'; |
| 312 | |||
| 313 | event_start_input.value = start + 'T10:00'; | 250 | event_start_input.value = start + 'T10:00'; |
| 314 | event_end_input.value = end + 'T11:00'; | 251 | event_end_input.value = end + 'T11:00'; |
| 315 | } | 252 | } |
| 316 | } | 253 | } |
| 317 | function removeEvent(){ | 254 | function removeEvent() { |
| 318 | if(!confirm("Voulez-vous vraiment supprimer cet évènement du calendrier?")){ | 255 | if (!confirm("Voulez-vous vraiment supprimer cet évènement du calendrier?")) { |
| 319 | return; | 256 | return; |
| 320 | } | 257 | } |
| 321 | const event_id = document.getElementById('event_id').value; | 258 | const event_tag = getElementOrThrow('event_id'); // cible input hidden |
| 259 | assertElementType(event_tag, HTMLInputElement); | ||
| 260 | const event_id = event_tag.value; | ||
| 322 | const event = calendar.getEventById(event_id); | 261 | const event = calendar.getEventById(event_id); |
| 323 | 262 | if (!event) { | |
| 263 | throw new Error("Événement non trouvé !"); | ||
| 264 | } | ||
| 324 | fetch('index.php?action=remove_event', { | 265 | fetch('index.php?action=remove_event', { |
| 325 | method: 'POST', | 266 | method: 'POST', |
| 326 | headers: { | 267 | headers: { |
| 327 | 'Content-Type': 'application/json', | 268 | 'Content-Type': 'application/json', |
| 328 | }, | 269 | }, |
| 329 | body: JSON.stringify({'id': event_id}), | 270 | body: JSON.stringify({ 'id': event_id }), |
| 330 | }) | 271 | }) |
| 331 | .then(response => response.json()) | 272 | .then(response => response.json()) |
| 332 | .then(data => { | 273 | .then(data => { |
| 333 | if(data.success){ | 274 | if (data.success) { |
| 334 | event.remove(); | 275 | event.remove(); |
| 335 | hideModal(); | 276 | hideModal(); |
| 336 | } | 277 | } |
| 337 | }) | 278 | }) |
| 338 | .catch((error) => { | 279 | .catch((error) => { |
| 339 | console.error('Error:', error); | 280 | console.error('Error:', error); |
| 340 | }); | 281 | }); |
| 341 | event_selected = false; | 282 | event_selected = false; |
| 342 | } | 283 | } |
| 343 | |||
| 344 | // touches de clavier | 284 | // touches de clavier |
| 345 | document.addEventListener('keydown', function(event){ | 285 | document.addEventListener('keydown', function (event) { |
| 346 | if(event.key === 'Escape'){ | 286 | if (event.key === 'Escape') { |
| 347 | hideModal(); | 287 | hideModal(); |
| 348 | } | 288 | } |
| 349 | else if(event.key === 'Delete' && event_selected === true){ | 289 | else if (event.key === 'Delete' && event_selected === true) { |
| 350 | removeEvent(); | 290 | removeEvent(); |
| 351 | } | 291 | } |
| 352 | }); | 292 | }); |
| 353 | |||
| 354 | // boutons dans la "modale" | 293 | // boutons dans la "modale" |
| 355 | // technique de la délégation d'événements pour utiliser un bouton ajouté dynamiquement | 294 | // technique de la délégation d'événements pour utiliser un bouton ajouté dynamiquement |
| 356 | document.addEventListener('click', function(event){ | 295 | document.addEventListener('click', function (event) { |
| 357 | if(event.target.classList.contains('event_close_button')){ | 296 | if (!event.target) { |
| 297 | throw new Error('évènement click non conforme'); | ||
| 298 | } | ||
| 299 | assertElementType(event.target, HTMLElement); | ||
| 300 | if (event.target.classList.contains('event_close_button')) { | ||
| 358 | hideModal(); | 301 | hideModal(); |
| 359 | } | 302 | } |
| 360 | else if(event.target.classList.contains('event_all_day')){ | 303 | else if (event.target.classList.contains('event_all_day')) { |
| 361 | checkAllDay(); | 304 | checkAllDay(); |
| 362 | } | 305 | } |
| 363 | else if(event.target.classList.contains('submit_new_event')){ | 306 | else if (event.target.classList.contains('submit_new_event')) { |
| 364 | submitEvent(true); | 307 | submitEvent(true); |
| 365 | } | 308 | } |
| 366 | else if(event.target.classList.contains('submit_update_event')){ | 309 | else if (event.target.classList.contains('submit_update_event')) { |
| 367 | submitEvent(false); | 310 | submitEvent(false); |
| 368 | } | 311 | } |
| 369 | else if(event.target.classList.contains('delete_event')){ | 312 | else if (event.target.classList.contains('delete_event')) { |
| 370 | removeEvent(); | 313 | removeEvent(); |
| 371 | } | 314 | } |
| 372 | }); | 315 | }); |
| 373 | |||
| 374 | calendar.render(); | 316 | calendar.render(); |
| 375 | }); \ No newline at end of file | 317 | }); \ No newline at end of file |
diff --git a/public/js/main.js b/public/js/main.js new file mode 100644 index 0000000..50eedf6 --- /dev/null +++ b/public/js/main.js | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | // js/main.js | ||
| 2 | |||
| 3 | // réussite de getElementById | ||
| 4 | function getElementOrThrow(id) { | ||
| 5 | const elem = document.getElementById(id); | ||
| 6 | if (!elem) { | ||
| 7 | throw new Error("l'élément d'id: " + id + " non trouvé"); | ||
| 8 | } | ||
| 9 | return elem; | ||
| 10 | } | ||
| 11 | // l'erreur attribut "value" non trouvé devient l'élement attrapé n'a pas le bon type (c'est un peu plus clair) | ||
| 12 | function assertElementType(elem, ctor) { | ||
| 13 | if (!(elem instanceof ctor)) { | ||
| 14 | throw new Error(`type attendu: ${ctor.name}, type obtenu: ${elem.tagName}`); | ||
| 15 | } | ||
| 16 | } | ||
diff --git a/src/controller/calendar.php b/src/controller/calendar.php index c15141a..47dc8b7 100644 --- a/src/controller/calendar.php +++ b/src/controller/calendar.php | |||
| @@ -47,9 +47,9 @@ if($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['action']) && $_GET['acti | |||
| 47 | [ | 47 | [ |
| 48 | 'id' => 1, | 48 | 'id' => 1, |
| 49 | 'title' => 'Évènement1', | 49 | 'title' => 'Évènement1', |
| 50 | 'description' => 'blablabla', | 50 | 'description' => 'non allDay quelques heures', |
| 51 | 'start' => '2025-06-03T05:00:00Z', // Z indique que l'heure est en UTC | 51 | 'start' => '2026-01-03T05:00:00Z', // Z indique que l'heure est en UTC |
| 52 | 'end' => '2025-06-03T09:00:00Z', | 52 | 'end' => '2026-01-03T09:00:00Z', |
| 53 | 'allDay' => false, | 53 | 'allDay' => false, |
| 54 | 'color' => '#ffa500', // couleur hexa, éviter les couleurs CSS qui ne fonctionnent pas dans value="" en HTML | 54 | 'color' => '#ffa500', // couleur hexa, éviter les couleurs CSS qui ne fonctionnent pas dans value="" en HTML |
| 55 | //'url' => 'https://dev.nageurs-bigoudens.fr', // comportement: https://fullcalendar.io/docs/eventClick | 55 | //'url' => 'https://dev.nageurs-bigoudens.fr', // comportement: https://fullcalendar.io/docs/eventClick |
| @@ -57,21 +57,30 @@ if($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['action']) && $_GET['acti | |||
| 57 | [ | 57 | [ |
| 58 | 'id' => 2, | 58 | 'id' => 2, |
| 59 | 'title' => 'Évènement2', | 59 | 'title' => 'Évènement2', |
| 60 | 'description' => 'truc machin', | 60 | 'description' => 'non allDay sur plusieurs jours', |
| 61 | 'start' => '2025-06-06T08:00:00Z', | 61 | 'start' => '2026-01-06T08:00:00Z', |
| 62 | 'end' => '2025-06-07T08:00:00Z', | 62 | 'end' => '2026-01-07T08:00:00Z', |
| 63 | 'allDay' => false, | 63 | 'allDay' => false, |
| 64 | 'color' => '#e01b24', | 64 | 'color' => '#e01b24', |
| 65 | ], | 65 | ], |
| 66 | [ | 66 | [ |
| 67 | 'id' => 3, | 67 | 'id' => 3, |
| 68 | 'title' => 'Évènement3', | 68 | 'title' => 'Évènement3', |
| 69 | 'description' => 'ça va chier', | 69 | 'description' => 'allDay un jour', |
| 70 | 'start' => '2025-06-08', | 70 | 'start' => '2026-01-08', |
| 71 | 'end' => '2025-06-09', | 71 | 'end' => '2026-01-09', |
| 72 | 'allDay' => true, // pas d'heure | 72 | 'allDay' => true, // pas d'heure |
| 73 | 'color' => '#008000', | 73 | 'color' => '#008000', |
| 74 | ], | 74 | ], |
| 75 | [ | ||
| 76 | 'id' => 4, | ||
| 77 | 'title' => 'Évènement4', | ||
| 78 | 'description' => 'allDay plusieurs jours', | ||
| 79 | 'start' => '2026-01-21', | ||
| 80 | 'end' => '2026-01-23', | ||
| 81 | 'allDay' => true, // pas d'heure | ||
| 82 | 'color' => '#0080f0', | ||
| 83 | ], | ||
| 75 | ]; | 84 | ]; |
| 76 | 85 | ||
| 77 | header('Content-Type: application/json'); | 86 | header('Content-Type: application/json'); |
