1$ = jQuery; 2 3/** 4 * callback function for url generator button 5 */ 6function callback__urlGeneratorButton_eventscalendar_2(button){ 7 eventscalendar.proccessFilter(button, 'generateURL'); 8} 9 10/** 11 * 12 * callback function for search component 13 * 14 * @param {*} searchForm The search form 15 */ 16function callback__searchComponent_eventscalendar_2(searchForm){ 17 // get the search input in the search form 18 let inputElm = searchForm.querySelector('input'); 19 20 eventscalendar.proccessFilter(inputElm, inputElm.value); 21} 22 23/** 24 * 25 * callback function for datepicker component 26 * 27 * @param {*} elm The element 28 * @param {*} selected 29 * 30 * @returns void 31 */ 32function callback__datepickerComponent_eventscalendar_2(elm, selected){ 33 eventscalendar.proccessFilter(elm, selected); 34} 35 36/** 37 * 38 * callback function for select component 39 * 40 * @param {*} elm The element 41 * @param {*} selected 42 * 43 * @returns void 44 */ 45function callback__selectComponent_eventscalendar_2(elm, selected){ 46 eventscalendar.proccessFilter(elm, selected); 47} 48 49/** 50 * Reset the filter 51 * 52 * @returns void 53 */ 54function EventsResetFilter(){ 55 eventscalendar.resetFilter(); 56} 57 58/** 59 * Download ICS file with current filter settings 60 * 61 * @param {HTMLElement} button The button element 62 * @returns void 63 */ 64function downloadEventsIcs(button) { 65 // Check if eventscalendar object is available 66 if (typeof eventscalendar === 'undefined' || !eventscalendar.state) { 67 console.error('Fehler: Kalender nicht initialisiert'); 68 return; 69 } 70 71 // Get the REST API url 72 var baseUrl = window.location.protocol + '//' + window.location.host; 73 var apiUrl = baseUrl + '/wp-json/flib/v1/events'; 74 75 // Show loading state 76 button.disabled = true; 77 // Store original content before changing it 78 if (!button.getAttribute('data-original-content')) { 79 button.setAttribute('data-original-content', button.innerHTML); 80 } 81 button.innerHTML = '<span>Wird erstellt...</span>'; 82 83 try { 84 // Build POST data for REST API with proper date format conversion 85 var postData = { 86 format: 'ics', 87 from: convertDateToRestFormat(eventscalendar.state.filter.startdate), 88 to: convertDateToRestFormat(eventscalendar.state.filter.enddate), 89 category: eventscalendar.state.combinedCategories ? eventscalendar.state.combinedCategories.join(',') : '', 90 place: Array.isArray(eventscalendar.state.filter.place) ? eventscalendar.state.filter.place.join(',') : (eventscalendar.state.filter.place || ''), 91 source: eventscalendar.state.filter.source || 'events-system' 92 }; 93 94 // Create a temporary form for file download 95 var tempForm = document.createElement('form'); 96 tempForm.method = 'POST'; 97 tempForm.action = apiUrl; 98 tempForm.style.display = 'none'; 99 100 // Convert POST data to form fields 101 for (var key in postData) { 102 if (postData[key] !== null && postData[key] !== '') { 103 var input = document.createElement('input'); 104 input.type = 'hidden'; 105 input.name = key; 106 input.value = postData[key]; 107 tempForm.appendChild(input); 108 } 109 } 110 111 // Add form to DOM, submit, and remove 112 document.body.appendChild(tempForm); 113 tempForm.submit(); 114 document.body.removeChild(tempForm); 115 } catch (error) { 116 console.error('Fehler beim Erstellen der ICS-Datei:', error); 117 } 118 119 // Reset button state 120 setTimeout(function() { 121 button.disabled = false; 122 // Restore original button content 123 var originalContent = button.getAttribute('data-original-content'); 124 if (originalContent) { 125 button.innerHTML = originalContent; 126 } else { 127 button.innerHTML = '<span>Kalender abonnieren</span>'; 128 } 129 }, 2000); 130} 131 132/** 133 * Convert date from DD.MM.YYYY format to YYYY-MM-DD HH:MM format for REST API 134 * 135 * @param {string} dateStr - Date string in DD.MM.YYYY format or null 136 * @returns {string} - Date string in YYYY-MM-DD HH:MM format or null 137 */ 138function convertDateToRestFormat(dateStr) { 139 if (!dateStr || dateStr === 'null' || dateStr === null) { 140 return null; 141 } 142 143 // Check if date is in DD.MM.YYYY format 144 var datePattern = /^(\d{1,2})\.(\d{1,2})\.(\d{4})$/; 145 var match = dateStr.match(datePattern); 146 147 if (match) { 148 var day = match[1].padStart(2, '0');
149 var month = match[2].padStart(2, '0'); 150 var year = match[3]; 151 152 // Convert to YYYY-MM-DD 00:00 format 153 return year + '-' + month + '-' + day + ' 00:00'; 154 } 155 156 // If not in expected format, return as-is 157 return dateStr; 158} 159 160/** 161 * 162 * Switch the page 163 * 164 * @param {*} item The item to switch to 165 * @param {*} page The page number 166 * 167 * @returns void 168 */ 169function switchPage(item, page){ 170 eventscalendar.state.currentPage = page; 171 eventscalendar.refresh() 172} 173 174/** 175 * 176 * Switch the tab 177 * 178 * @param {*} type The type to switch to 179 * 180 * @returns void 181 */ 182function switchTab(type){ 183 document.querySelectorAll('.tab-button').forEach(function(elm){ 184 elm.classList.add('disabled') 185 elm.disabled = false 186 }) 187 document.querySelector('[data-tab="'+type+'"]').disabled = true 188 document.querySelector('[data-tab="'+type+'"]').classList.remove('disabled') 189 eventscalendar.state.filter.source = type 190 191 eventscalendar.refresh() 192} 193 194/** 195 * 196 * Resize events 197 * 198 */ 199window.addEventListener('resize', function() { 200 resizeMobileHiddenParts(); 201}); 202 203/** 204 * 205 * Toggle the info box 206 * 207 * @param {*} event The event 208 * @param {*} button The button 209 * 210 * @returns void 211 */ 212function toggleInfoBox(event, button) { 213 event.preventDefault(); 214 event.stopPropagation(); 215 216 if (!button) return; 217 218 let infoBox = button.closest('.infobox'); 219 if (!infoBox) return; 220 221 let hiddenMobilePart = infoBox.querySelector('.mobile-hidden'); 222 if (!hiddenMobilePart) return; 223 224 infoBox.classList.toggle('mobile-toggled-visible'); 225 226 if (infoBox.classList.contains('mobile-toggled-visible')) { 227 // Get the height of the ul inside the hidden part 228 let hiddenMobilePartHeight = hiddenMobilePart.scrollHeight; 229 230 // Set the height of the hidden part to the height of the ul 231 hiddenMobilePart.style.height = hiddenMobilePartHeight + 'px'; 232 } else { 233 hiddenMobilePart.style.height = '0px'; 234 } 235} 236 237/** 238 * 239 * Resize mobile hidden parts 240 * 241 * returns void 242 */ 243function resizeMobileHiddenParts() { 244 let hiddenParts = document.querySelectorAll('.infobox.mobile-toggled-visible .mobile-hidden'); 245 if (!hiddenParts) return; 246 247 hiddenParts.forEach(function(hiddenPart) { 248 hiddenPart.style.height = 'auto'; 249 let hiddenPartHeight = hiddenPart.scrollHeight; 250 hiddenPart.style.height = hiddenPartHeight + 'px'; 251 }); 252} 253 254/** 255 * Eventscalendar JS Class 256 */ 257class Eventscalendar{ 258 259 constructor(){ 260 this.state = { 261 "action": "getEvents", 262 "allow_refresh": true, 263 "generateURL": false, 264 "filter": { 265 "search": '', 266 "source": null, 267 "startdate": null, 268 "enddate": null, 269 "category": [], 270 "place": [], 271 "radius": null, 272 "extraCategories": [] 273 }, 274 'currentPage': 1, 275 'combinedCategories': [] 276 } 277 } 278 279 /** 280 * 281 * Proccess the filter 282 * 283 * @param {*} elm The element 284 * @param {*} selected The selected value 285 */ 286 proccessFilter(elm, selected) { 287 switch (elm.dataset.name) { 288 case 'search': 289 this.state.filter.search = elm.value || ''; 290 break; 291 case 'startdate': 292 this.state.filter.startdate = elm.value || null; 293 break; 294 case 'enddate': 295 this.state.filter.enddate = elm.value || null; 296 break; 297 case 'category': 298 this.state.filter.category = selected; 299 break; 300 case 'place': 301 this.state.filter.place = selected; 302 break; 303 case 'radius': 304 this.state.filter.radius = selected || null; 305 break; 306 default: 307 // Check if it's an extra category filter 308 if (elm.dataset.repeaterFilter === "true") { 309 const name = elm.dataset.name; 310 // normalize to array and remove placeholders 311 const values = (Array.isArray(selected) ? selected : [selected]).filter(v =>
311 v && v !== 'keine-auswahl'); 312 313 if (!Array.isArray(this.state.filter.extraCategories)) { 314 this.state.filter.extraCategories = []; 315 } 316 317 // find existing entry by name and update, else push a new one 318 const idx = this.state.filter.extraCategories.findIndex(ec => ec && ec.name === name); 319 if (idx >= 0) { 320 this.state.filter.extraCategories[idx].values = values; 321 } else { 322 this.state.filter.extraCategories.push({ name: name, values: values }); 323 } 324 } 325 break; 326 } 327 328 // Build combined categories (main + extra) for the API call only 329 const extraCats = Array.isArray(this.state.filter.extraCategories) 330 ? this.state.filter.extraCategories.flatMap(ec => Array.isArray(ec.values) ? ec.values : []) 331 : []; 332 333 const combined = [ 334 ...(Array.isArray(this.state.filter.category) ? this.state.filter.category : (this.state.filter.category ? [this.state.filter.category] : [])), 335 ...extraCats, 336 ]; 337 338 // Save only into separate state; do not overwrite the raw select state 339 this.state.combinedCategories = Array.from(new Set(combined)).filter(v => v && v !== 'keine-auswahl'); 340 341 // Reset the current page to 1 342 this.state.currentPage = 1; 343 344 // If selected is 'generateURL', set the state of generateURL to true 345 if (selected === 'generateURL') { 346 this.state.generateURL = true; 347 } else { 348 this.state.generateURL = false; 349 } 350 351 // Refresh the UI 352 this.refresh(); 353 } 354 355 /** 356 * Reset the filter 357 * 358 * @returns void 359 */ 360 resetFilter(){ 361 362 // Get reference to the filter wrapper of the events calendar 363 const filterWrapper = document.querySelector('.eventscalendar-wrapper .filter-wrapper'); 364 365 //block refresh 366 this.state.allow_refresh = false; 367 368 // Get the default settings 369 var refreshWrapper = document.getElementById('eventscalendar-refresh-wrapper'); 370 var defaults = refreshWrapper.dataset.defaults 371 defaults = JSON.parse(defaults) 372 373 this.state.filter.startdate = null 374 this.state.filter.enddate = null 375 this.state.filter.category = defaults.category 376 this.state.filter.place = [] 377 this.state.filter.radius = null 378 this.state.currentPage = 1 379 this.state.filter.search = '' 380 381 // check if extra categories are present, and reset them (now an array) 382 if (this.state.filter.extraCategories) { 383 this.state.filter.extraCategories = []; 384 } 385 386 // recompute combined categories from raw select values 387 const _combinedAfterReset = [ 388 ...(Array.isArray(this.state.filter.category) ? this.state.filter.category : (this.state.filter.category ? [this.state.filter.category] : [])) 389 ]; 390 this.state.combinedCategories = Array.from(new Set(_combinedAfterReset)).filter(v => v && v !== 'keine-auswahl'); 391 392 const startDateElement = filterWrapper.querySelector('[data-name="startdate"]'); 393 if (startDateElement) { 394 startDateElement.value = this.formatDate(Date()); 395 } 396 397 const endDateElement = filterWrapper.querySelector('[data-name="enddate"]'); 398 if (endDateElement) { 399 endDateElement.value = ''; 400 } 401 402 const placeElement = filterWrapper.querySelector('[data-name="place"]'); 403 if (placeElement && placeElement.slim) { 404 placeElement.slim.setSelected('keine-auswahl'); 405 // if the element contains the data-sourceurl attribute setData to 'keine-auswahl' and 'Keine Auswahl' 406 if (placeElement.dataset.sourceurl) { 407 placeElement.slim.setData([ 408 { text: 'Keine Auswahl', value: 'keine-auswahl', placeholder: 'Keine Auswahl'}, 409 ]); 410 } 411 } 412 413 const radiusElement = filterWrapper.querySelector('[data-name="radius"]'); 414 if (radiusElement && radiusElement.slim) { 415 radiusElement.slim.setSelected(''); 416 } 417 418 const categoryElement = filterWrapper.querySelector('[data-name="category"]'); 419 if (categoryElement && categoryElement.slim) { 420 categoryElement.slim.setSelected(defaults.category); 421 } 422 423 const categoryRepeaterElements = filterWrapper.querySelectorAll('[data-repeater-filter="true"]'); 424 if (categoryRepeaterElements) {
425 categoryRepeaterElements.forEach(function (categoryRepeaterElement) { 426 // set back to empty 427 categoryRepeaterElement.slim.setSelected('keine-auswahl'); 428 }); 429 } 430 431 const searchElement = filterWrapper.querySelector('[data-name="search"]'); 432 if (searchElement) { 433 searchElement.value = ''; 434 } 435 436 // Reset the current page to 1 437 this.state.currentPage = 1; 438 439 //unblock refresh 440 this.state.allow_refresh = true; 441 442 // wait a bit before refreshing 443 this.refresh() 444 } 445 446 /** 447 * 448 * Refresh the events 449 * 450 * @returns void 451 */ 452 refresh(){ 453 454 if(!this.state.allow_refresh){ 455 history.pushState(this.state, '') 456 return; 457 } 458 459 // Get the ajax url because the script is not localized, see /inc/api/config.php 460 var baseUrl = window.location.protocol + '//' + window.location.host; 461 var ajaxurl = baseUrl + '/wp-admin/admin-ajax.php'; 462 463 //get element settings 464 var refreshWrapper = document.getElementById('eventscalendar-refresh-wrapper'); 465 var section = refreshWrapper.closest('section'); 466 467 var form_data = new FormData(); 468 form_data.append('action', this.state.action); 469 form_data.append('startdate', this.state.filter.startdate); 470 form_data.append('enddate', this.state.filter.enddate); 471 form_data.append('category', this.state.combinedCategories); 472 form_data.append('place', this.state.filter.place); 473 form_data.append('radius', this.state.filter.radius); 474 form_data.append('settings', refreshWrapper.dataset.settings); 475 form_data.append('page', this.state.currentPage); 476 form_data.append('source', this.state.filter.source); 477 form_data.append('search', this.state.filter.search); 478 form_data.append('generateURL', this.state.generateURL); 479 480 // Get the ajax url because the script is not localized, see /inc/api/config.php 481 var baseUrl = window.location.protocol + '//' + window.location.host; 482 var ajaxurl = baseUrl + '/wp-admin/admin-ajax.php'; 483 484 $.ajax({ 485 url: ajaxurl, 486 type: 'POST', 487 contentType: false, 488 processData: false, 489 data: form_data, 490 beforeSend: function(xhr) { 491 document.querySelector('.events-loading').style.display = 'flex'; 492 refreshWrapper.style.display = 'none'; 493 494 // Jump to section 495 if (section) { 496 let sectionAnchor = section.querySelector('.section-anchor'); 497 let sectionTop = sectionAnchor.getBoundingClientRect().top + window.scrollY; 498 window.scrollTo({ 499 top: sectionTop, 500 behavior: 'smooth' 501 }); 502 } 503 }, 504 success: function(response) { 505 console.log(response); 506 refreshWrapper.innerHTML = response.data.html; 507 508 509 // Get the .calendar-subscription button and change it to the url 510 const icsButton = document.querySelector('.calendar-subscription-button'); 511 if (icsButton && response.data.url) { 512 eventscalendar.copyUrlToClipboard(icsButton, response.data.url); 513 } 514 515 // Reset generateURL back to false after any request 516 eventscalendar.state.generateURL = false; 517 518 history.pushState(eventscalendar.state, ''); 519 } 520 }) 521 .done(function() { 522 document.querySelector('.events-loading').style.display = 'none'; 523 refreshWrapper.style.display = 'block'; 524 svgVisible(); 525 }) 526 } 527 528 formatDate(date) { 529 date = new Date(date) 530 var format = ('0' + date.getDate()).slice(-2)+'.'+('0' + (date.getMonth()+1)).slice(-2) +'.'+date.getFullYear(); 531 return format; 532 } 533 534 /** 535 * Copy URL to clipboard and update button text 536 * 537 * @param {HTMLElement} button - The calendar subscription button 538 * @param {string} url - The URL to copy to clipboard 539 * @returns void 540 */
541 copyUrlToClipboard(button, url) { 542 // Copy URL to clipboard 543 navigator.clipboard.writeText(url).then(function() { 544 const span = button.querySelector('span'); 545 const originalText = span.innerText; 546 const originalColor = button.style.backgroundColor; 547 548 // Show the info text 549 const infoText = document.querySelector('.calendar-subscription-info'); 550 if (infoText) { 551 infoText.style.display = 'block'; 552 } 553 554 // Replace icon with check 555 const svg = button.querySelector('svg'); 556 if (svg) { 557 const checkPath = 'M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z'; 558 svg.innerHTML = '<path fill="currentColor" d="' + checkPath + '"></path>'; 559 svg.setAttribute('viewBox', '0 0 512 512'); 560 } 561 562 // Change button appearance 563 button.style.backgroundColor = '#28a745'; 564 span.innerText = 'Link in die Zwischenablage kopiert'; 565 566 // Reset after 3 seconds 567 setTimeout(function() { 568 span.innerText = originalText; 569 button.style.backgroundColor = originalColor; 570 }, 3000); 571 }); 572 } 573} 574 575/** 576 * Intit Eventscalendar Object 577 */ 578 const eventscalendar = new Eventscalendar(); 579 580// add dom content loaded event 581document.addEventListener('DOMContentLoaded', function(){ 582 let activeTab = 0 583 document.querySelectorAll('.tab-button').forEach(function(elm, index){ 584 if(activeTab != index){ 585 elm.classList.add('disabled') 586 } 587 else{ 588 elm.disabled = true 589 } 590 }); 591 592 var defaults = document.getElementById('eventscalendar-refresh-wrapper').dataset.defaults; 593 defaults = JSON.parse(defaults); 594 595 eventscalendar.state.filter.category = defaults.category; 596 eventscalendar.state.filter.source = defaults.source; 597 598 const _initCombined = Array.isArray(defaults.category) ? defaults.category : (defaults.category ? [defaults.category] : []); 599 eventscalendar.state.combinedCategories = Array.from(new Set(_initCombined)).filter(v => v && v !== 'keine-auswahl'); 600});
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.