summaryrefslogtreecommitdiff
path: root/public
diff options
context:
space:
mode:
authorpolo <ordipolo@gmx.fr>2026-01-29 22:51:19 +0100
committerpolo <ordipolo@gmx.fr>2026-01-29 22:51:19 +0100
commite3a42c8342bba0db15e2ca9a78911121e5d539da (patch)
treedf2b669452ba82774d741a4b9e48948b8dc45a0a /public
parentf007bac3b9172711dc0fcca1306270ab99dbd8a4 (diff)
downloadfullcalendar-master.tar.gz
fullcalendar-master.tar.bz2
fullcalendar-master.zip
classe CalendarModalView, contrôles getElementOrThrow et assertElementTypeHEADmaster
Diffstat (limited to 'public')
-rw-r--r--public/css/calendar.css8
-rw-r--r--public/index.php11
-rw-r--r--public/js/CalendarModalView.js120
-rw-r--r--public/js/calendar.js110
-rw-r--r--public/js/calendar_admin.js410
-rw-r--r--public/js/main.js16
6 files changed, 370 insertions, 305 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
21if($_SESSION['admin'] === true){ 23if($_SESSION['admin']){
22 echo '<script src="js/calendar_admin.js"></script>' . "\n"; 24 echo '<script src="js/calendar_admin.js"></script>' . "\n";
23} 25}
24else{ 26else{
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
3class 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, "&amp;") // & en premier
115 .replace(/</g, "&lt;")
116 .replace(/>/g, "&gt;")
117 .replace(/"/g, "&quot;")
118 .replace(/'/g, "&#039;");
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
3document.addEventListener('DOMContentLoaded', function(){ 3document.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
3document.addEventListener('DOMContentLoaded', function(){ 3document.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
4function 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)
12function assertElementType(elem, ctor) {
13 if (!(elem instanceof ctor)) {
14 throw new Error(`type attendu: ${ctor.name}, type obtenu: ${elem.tagName}`);
15 }
16}