https://www.monlet.fr/assets/controllers/agenda_modal_controller-0848064f29058dbaaa96fad6dde160ff.js
1import {Controller} from "@hotwired/stimulus"; 2import moment from "moment-timezone"; 3import {errorSwal, successSwal, warningSwal} from 'swal-utils'; 4import { trans } from 'translator'; 5 6export default class extends Controller { 7 static values = { 8 stationSlug: String 9 } 10 11 static targets = [ 12 'title', 13 'body', 14 'dateDebut', 15 'dateFin', 16 'dateNotification', 17 'notificationContainer', 18 'evenementForm', 19 'submitButton', 20 'fullDay', 21 'dateFinContainer', 22 'recurrenceMois', 23 'recurrenceRepetition', 24 ]; 25 26 connect() { 27 28 this._handleFocusIn = (event) => { 29 // Si la cible du focus est dans une boîte de dialogue CKEditor, 30 // on empêche Bootstrap de récupérer le focus. 31 if (event.target.closest('.cke_dialog')) { 32 event.stopPropagation(); 33 } 34 }; 35 this.initializeFullDayUI(); 36 this.initializeCkeditorImageWarning(); 37 // true = phase de capture, important pour passer AVANT Bootstrap 38 document.addEventListener('focusin', this._handleFocusIn, true); 39 } 40 41 disconnect() { 42 if (this._handleFocusIn) { 43 document.removeEventListener('focusin', this._handleFocusIn, true); 44 this._handleFocusIn = null; 45 } 46 } 47 48 openModal(event) { 49 const {title, body} = event.detail; 50 51 // Injecter le titre et le formulaire dans la modale 52 this.titleTarget.innerText = title; 53 this.bodyTarget.innerHTML = body; 54 55 const scripts = this.bodyTarget.querySelectorAll('script'); 56 scripts.forEach(oldScript => { 57 const newScript = document.createElement('script'); 58 [...oldScript.attributes].forEach(attr => 59 newScript.setAttribute(attr.name, attr.value) 60 ); 61 newScript.textContent = oldScript.textContent; 62 oldScript.replaceWith(newScript); 63 }); 64 65 this.initializeFullDayUI(); 66 this.initializeCkeditorImageWarning(); 67 // Afficher la modale 68 const modal = new bootstrap.Modal(this.element); 69 modal.show(); 70 71 } 72 73 onDateSelect(event) { 74 const {date, datepicker} = event.detail; 75 76 if (datepicker === this.dateDebutTarget) { 77 this.handleDateDebutChange(date); 78 } else if (datepicker === this.dateFinTarget) { 79 this.handleDateFinChange(date); 80 } 81 } 82 83 afficheNotification(event) { 84 const selectedChoice = event.params.choice; 85 switch (selectedChoice) { 86 case 'notifPersonnalise': 87 this.handleNotificationPersonnalise(); 88 break; 89 case 'notifMaintenant': 90 this.handleNotificationMaintenant(); 91 break; 92 case 'notifJamais': 93 default: 94 this.handleNotificationJamais(); 95 break; 96 } 97 } 98 99 handleDateDebutChange(dateDebut) { 100 const dateFinPicker = this.dateFinTarget.datepickerInstance; 101 const datefin = dateFinPicker.selectedDates[0] 102 103 if (datefin && dateDebut > datefin) { 104 const newDateFin = new Date(dateDebut.getTime() + 60 * 60 * 1000); // + 1heure 105 dateFinPicker.selectDate(newDateFin, {silent: false, updateTime: true}); 106 } 107 108 const checked = this.fullDayTarget.checked; 109 110 this.updateRecurrenceMoisLabels(dateDebut); 111 } 112 113 onRecurrenceRepetitionChange() { 114 const dateDebutPicker = this.dateDebutTarget?.datepickerInstance; 115 this.updateRecurrenceMoisLabels(dateDebutPicker?.selectedDates[0]); 116 } 117 118 updateRecurrenceMoisLabels(dateDebut) { 119 if (!this.hasRecurrenceMoisTarget || !dateDebut) { 120 return; 121 } 122 123 const recurrenceRepetition = this.hasRecurrenceRepetitionTarget ? this.recurrenceRepetitionTarget.value : ''; 124 125 const url = Routing.generate('app_agenda_ajax_mois', {station_slug: this.stationSlugValue}); 126 const data = new URLSearchParams({ 127 dateDebut: moment(dateDebut).format('YYYY-MM-DD HH:mm'), 128 recurrenceRepetition: recurrenceRepetition 129 }); 130 131 fetch(url, {method: 'POST', body: data}) 132 .then((response) => { 133 if (!response.ok) { 134 throw new Error(response.statusText); 135 } 136 return response.json(); 137 }) 138 .then((labels) => this.applyRecurrenceMoisLabels(labels)) 139 .catch((error) => console.error(error)); 140 } 141 142 applyRecurrenceMoisLabels(labels) { 143 const select = this.recurrenceMoisTarget; 144 145 [['0', labels.numero], ['1', labels.jour], ['2', labels.dernier]].forEac
145h(([value, label]) => { 146 const option = select.querySelector(`option[value="${value}"]`); 147 if (option) { 148 option.textContent = label; 149 } 150 }); 151 } 152 153 handleDateFinChange(dateFin) { 154 const dateNotificationPicker = this.dateNotificationTarget.datepickerInstance; 155 const dateNotification = dateNotificationPicker.selectedDates[0] 156 157 if (dateFin && dateNotification && dateNotification > dateFin) { 158 dateNotificationPicker.selectDate(dateFin, {silent: false}); 159 } 160 dateNotificationPicker.update({ 161 maxDate: dateFin 162 }, {silent: false}); 163 164 const dateFinPicker = this.dateFinTarget.datepickerInstance; 165 166 const checked = this.fullDayTarget.checked; 167 168 if (checked) { 169 const end = new Date(dateFinPicker.selectedDates[0]); 170 171 end.setHours(23, 59, 59, 59); 172 // end.setDate(start.getDate() + 1); 173 dateFinPicker.selectDate(end, {silent: false, updateTime: true}); 174 } 175 } 176 177 handleNotificationPersonnalise() { 178 const date = this.dateDebutTarget.datepickerInstance.selectedDates[0]; 179 this.dateNotificationTarget.datepickerInstance.selectDate(date, {silent: true, updateTime: true}) 180 this.notificationContainerTarget.classList.remove('d-none'); 181 } 182 183 handleNotificationMaintenant() { 184 const date = moment.tz(this.dateNotificationTarget.usertimezone).toDate() 185 this.dateNotificationTarget.datepickerInstance.selectDate(date, {silent: true, updateTime: true}) 186 this.notificationContainerTarget.classList.add('d-none'); 187 } 188 189 handleNotificationJamais() { 190 this.dateNotificationTarget.datepickerInstance.clear({silent: true}); 191 this.notificationContainerTarget.classList.add('d-none'); 192 } 193 194 toggleFullDay() { 195 if (!this.hasFullDayTarget) { 196 return; 197 } 198 199 const checked = this.fullDayTarget.checked; 200 201 const dateDebutPicker = this.dateDebutTarget.datepickerInstance; 202 const dateFinPicker = this.dateFinTarget.datepickerInstance; 203 204 if (!dateDebutPicker || !dateFinPicker) { 205 return; 206 } 207 208 if (checked) { 209 210 const start = dateDebutPicker.selectedDates[0]; 211 const end = dateFinPicker.selectedDates[0]; 212 213 start.setHours(0, 0, 0, 0); 214 end.setHours(23, 59, 59, 59); 215 216 dateDebutPicker.selectDate(start, {silent: true, updateTime: true}); 217 dateFinPicker.selectDate(end, {silent: true, updateTime: true}); 218 219 try { 220 dateDebutPicker.update({timepicker: false}, true); 221 } 222 catch (e) {} 223 try { 224 dateFinPicker.update({timepicker: false}, true); 225 } 226 catch (e) {} 227 } 228 if (!checked) { 229 230 dateDebutPicker.update({timepicker: true}, true); 231 232 dateFinPicker.update({timepicker: true}, true); 233 } 234 } 235 236 initializeFullDayUI(retryCount = 10) { 237 if (!this.hasFullDayTarget || !this.fullDayTarget.checked) { 238 return; 239 } 240 const dateDebutPicker = this.dateDebutTarget?.datepickerInstance; 241 const dateFinPicker = this.dateFinTarget?.datepickerInstance; 242 243 if (!dateDebutPicker || !dateFinPicker) { 244 if (retryCount > 0) { 245 setTimeout(() => this.initializeFullDayUI(retryCount - 1), 30); 246 } 247 return; 248 } 249 250 const checked = this.fullDayTarget.checked; 251 252 if (checked) { 253 const start = dateDebutPicker.selectedDates[0]; 254 const end = dateFinPicker.selectedDates[0]; 255 if(start.getDate() === end.getDate() - 1) { 256 end.setMinutes(end.getMinutes() - 1); 257 258 dateFinPicker.selectDate(end, {silent: true, updateTime: true}); 259 } 260 try { 261 dateDebutPicker.update({timepicker: false}, true); 262 } catch (e) { 263 } 264 try { 265 dateFinPicker.update({timepicker: false}, true); 266 } catch (e) { 267 } 268 269 } 270 } 271 272 normalizeBeforeFormFlowSubmit(event) { 273 this.normalizeDatesBeforeSubmit(); 274 } 275 276 async onSubmit(event) { 277 const form = this.evenementFormTarget; 278 279 if (window.CKEDITOR) {
280 Object.values(CKEDITOR.instances).forEach(instance => instance.updateElement()); 281 // ou, plus ciblé : 282 // CKEDITOR.instances.qsmart_illiwapbundle_evenement_description?.updateElement(); 283 } 284 285 if (form.checkValidity()) { 286 const url = form.getAttribute('action'); 287 this.normalizeDatesBeforeSubmit(); 288 const data = new URLSearchParams(new FormData(form)); 289 290 this.submitButtonTarget.disabled = true; 291 292 try { 293 const response = await fetch(url, { 294 method: 'POST', 295 body: data, 296 }); 297 298 if (!response.ok) { 299 throw new Error(response.statusText); 300 } 301 302 document.dispatchEvent(new CustomEvent('calendar:refresh')); 303 const modal = bootstrap.Modal.getInstance(this.element); 304 modal.hide(); 305 306 const message = url.includes("create") ? trans('agenda.swal.evenement_success_create') : trans('agenda.swal.evenement_success_edit'); 307 successSwal(message); 308 } catch (error) { 309 errorSwal(trans('agenda.swal.evenement_error_create'), error.message); 310 this.submitButtonTarget.disabled = false; 311 } 312 } else { 313 form.reportValidity(); 314 this.submitButtonTarget.disabled = false; 315 } 316 } 317 318 normalizeDatesBeforeSubmit() { 319 // Chercher les éléments dans le DOM au lieu d'utiliser les targets 320 const dateDebutElement = this.element.querySelector('[data-agenda-modal-target="dateDebut"]'); 321 const dateFinElement = this.element.querySelector('[data-agenda-modal-target="dateFin"]'); 322 const fullDayElement = this.element.querySelector('[data-agenda-modal-target="fullDay"]'); 323 324 const fields = [ 325 dateDebutElement, 326 dateFinElement, 327 ]; 328 329 const checked = fullDayElement && fullDayElement.checked; 330 331 if (checked) { 332 const dateFinPicker = dateFinElement.datepickerInstance; 333 const end = dateFinPicker.selectedDates[0]; 334 const dateDebutPicker = dateDebutElement.datepickerInstance; 335 const start = dateDebutPicker.selectedDates[0]; 336 337 if (start.getDate() === end.getDate()) { 338 end.setHours(0, 0, 0, 0); 339 end.setDate(end.getDate() + 1); 340 // this.dateFinTarget.value = moment(end).format('YYYY-MM-DD HH:mm'); 341 dateFinPicker.selectDate(end, {silent: true, updateTime: true}); 342 } 343 } 344 fields.forEach((input) => { 345 if (!input || !input.datepickerInstance) { 346 return; 347 348 } 349 const picker = input.datepickerInstance; 350 351 const date = picker.selectedDates[0]; 352 if (!date) { 353 return; 354 } 355 356 const dateFormatAttr = input.getAttribute('data-datepicker-dateFormat') || 'YYYY-MM-DD'; 357 const timeFormatAttr = input.getAttribute('data-datepicker-timeFormat') || 'HH:mm'; 358 const separator = input.getAttribute('data-datepicker-dateTimeSeparator') || ' '; 359 360 const backendFormat = `${dateFormatAttr.toUpperCase()}${separator}${timeFormatAttr}`; 361 362 input.value = moment(date).format(backendFormat); 363 }); 364 } 365 366 async clicDeleteEvenement(event) { 367 const confirmed = await warningSwal({ 368 title: trans('agenda.swal.evenement_warning_delete_title'), 369 text: trans('agenda.swal.evenement_warning_delete'), 370 confirmButtonText: trans('agenda.swal.evenement_warning_delete_button'), 371 cancelButtonText: trans('agenda.swal.evenement_warning_cancel_button') 372 }); 373 374 if (confirmed) { 375 this.deleteEvenement(event.params.eventId); 376 } 377 } 378 379 deleteEvenement(eventId) { 380 const url = Routing.generate('app_agenda_delete', { 381 id: eventId, 382 station_slug: this.stationSlugValue 383 }); 384 fetch(url, {method: 'POST'}) 385 .then(response => { 386 if (!response.ok) { 387 throw new Error(response.statusText); 388 } 389 successSwal(trans('agenda.swal.evenement_success_delete')); 390 document.dispatchEvent(new CustomEvent('calendar:refresh')); 391 const modal = bootstrap.Modal.getInstance(this.element); 392 modal.hide(); 393 }) 394 .catch(err => { 395 console.error(err); 396 errorSwal(err.message); 397 }); 398 } 399 400 duplicateEvenement(event) { 401 const url = Routing.generate('app_agenda_clone', { 402 id: event.params.eventId, 403 station_slug: this.stationSlugValue 404 }); 405 406 const currentModal = bootstrap.Modal.getInstance(this.element); 407 if (currentModal) { 408 currentModal.hide(); 409 } 410 411 fetch(url) 412 .then(response => { 413 if (!response.ok) { 414 throw Error(response.statusText) 415 } 416 return response.text() 417 }) 418 .then(htmlContent => { 419 const modalEvent = new CustomEvent('modal:open', { 420 detail: { 421 title: trans('agenda.index.duplicate_title'), 422 body: htmlContent 423 } 424 }); 425 window.dispatchEvent(modalEvent); 426 }).catch(function () { 427 errorSwal(trans('agenda.new.duplicate_error')) 428 }); 429 } 430 431 initializeCkeditorImageWarning() { 432 if (typeof CKEDITOR === 'undefined') { 433 return; 434 } 435 436 const editorName = 'evenement_description'; 437 const controller = this; 438 439 function attachImageListener(editor) { 440 if (!editor) { 441 return; 442 } 443 444 let hasShownWarning = false;
445 446 editor.on('change', function () { 447 448 if (hasShownWarning) { 449 return; 450 } 451 452 const data = editor.getData() || ''; 453 if (data.indexOf('<img') !== -1) { 454 controller.showImageCopyrightWarning(); 455 hasShownWarning = true; 456 } 457 }); 458 } 459 460 const existingEditor = CKEDITOR.instances[editorName]; 461 462 if (existingEditor && existingEditor.status === 'ready') { 463 attachImageListener(existingEditor); 464 } else { 465 CKEDITOR.on('instanceReady', function (event) { 466 if (event.editor.name === editorName) { 467 attachImageListener(event.editor); 468 } 469 }); 470 } 471 } 472 473 showImageCopyrightWarning() { 474 if (typeof Swal === 'undefined') { 475 return; 476 } 477 478 const swalOptions = { 479 title: trans('avertissement_soumission_image_titre'), 480 html: ` 481 <div> 482 <p id="swal-text" class="mb-0">${trans('avertissement_soumission_image_1')}</p> 483 <div class="collapse mt-0" id="collapseFullText"> 484 <p>${trans('avertissement_soumission_image_2')}</p> 485 </div> 486 <a id="toggleLink" class="btn btn-link" data-toggle="collapse" href="#collapseFullText" role="button" aria-expanded="false" aria-controls="collapseFullText"> 487 <i id="chevronIcon" class="fa fa-chevron-down"></i> 488 </a> 489 </div> 490 `, 491 icon: "info", 492 confirmButtonColor: '#9FBA00', 493 willOpen: () => { 494 const chevronIcon = document.getElementById('chevronIcon'); 495 const toggleLink = document.getElementById('toggleLink'); 496 if (!chevronIcon || !toggleLink) { 497 return; 498 } 499 500 toggleLink.addEventListener('click', () => { 501 const fullText = document.getElementById('collapseFullText'); 502 if (!fullText) { 503 return; 504 } 505 fullText.classList.toggle('show'); 506 chevronIcon.classList.toggle('fa-chevron-down'); 507 chevronIcon.classList.toggle('fa-chevron-up'); 508 }); 509 }, 510 }; 511 512 Swal.fire(swalOptions); 513 } 514}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.