1/** 2 * Event Kalender â Frontend JavaScript 3 * Version: 1.0.0 4 */ 5/* global precFrontend, FullCalendar */ 6(function ($) { 7 'use strict'; 8 9 /** 10 * Text aus einem Kalender-Datensatz sicher aufbereiten. 11 * 12 * WARUM DAS NOETIG IST: Die Werte (Titel, Ort, Kategorie) kommen als JSON 13 * vom Server und werden hier zu HTML zusammengesetzt. Wird ein solcher Wert 14 * ungeprueft in einen HTML-String geschrieben und dieser dann per innerHTML 15 * eingesetzt, fuehrt der Browser darin enthaltene Auszeichnung aus - das 16 * waere gespeichertes Cross-Site-Scripting, sichtbar fuer JEDEN Besucher. 17 * 18 * precText() liefert reinen Text: erst werden Entitaeten aufgeloest (damit 19 * "Kunst & Kultur" auch wirklich "Kunst & Kultur" heisst), danach wird 20 * das Ergebnis wieder maskiert. Uebrig bleibt genau der sichtbare Text. 21 */ 22 /** 23 * Symbol als SVG (seit v11.1.0) â kommt fertig vom Server (PREC_Symbole, feste Zeichenketten). 24 * Ersetzt die Emojis, die je Betriebssystem anders (Windows: schwarz-weiÃ) aussahen. 25 */ 26 function precSym(name) { 27 return (window.precFrontend && precFrontend.sym && precFrontend.sym[name]) ? precFrontend.sym[name] : ''; 28 } 29 30 /** 31 * Ortsteil-Auswahl an die gewählten Orte anpassen (seit v11.1.0). 32 * - Erscheint erst, wenn im Orte-Filter ein Ort gewählt ist â und nur, wenn es dort Ortsteile gibt. 33 * - Ohne Orte-Filter in der Leiste gilt der Ort des Kurzbefehls; ohne jeden Ort alle Ortsteile. 34 * - Mehrere Orte: Ortsteile je Ort gruppiert (optgroup). 35 * Rückgabe: true, wenn sich die Auswahl dadurch geändert hat (gewählter Ortsteil passt nicht mehr). 36 */ 37 function precOtSync($swrap) { 38 var $sel = $swrap.find('.prec-district-select').first(); 39 if (!$sel.length) return false; 40 var orte = $sel.data('orte') || {}, alle = $sel.data('alle') || {}; 41 if (typeof orte === 'string') { try { orte = JSON.parse(orte); } catch (e) { orte = {}; } } 42 if (typeof alle === 'string') { try { alle = JSON.parse(alle); } catch (e) { alle = {}; } } 43 var hatOrtFilter = ($sel.attr('data-ort-filter') === '1') && $swrap.find('.prec-loc-check').length > 0; 44 var gruppen = []; 45 if (hatOrtFilter) { 46 $swrap.find('.prec-loc-check:checked').each(function () { 47 var slugs = String($(this).val() || '').split(','), liste = {}; 48 slugs.forEach(function (sl) { var o = orte[$.trim(sl)] || {}; Object.keys(o).forEach(function (k) { liste[k] = (liste[k] || 0) + (+o[k] || 0); }); }); 49 if (Object.keys(liste).length) gruppen.push({ name: $(this).data('name') || '', liste: liste }); 50 }); 51 } else { 52 var fest = String($swrap.data('prec-locked-location') || '').split(',').map($.trim).filter(Boolean), liste = {}; 53 if (fest.length) { 54 fest.forEach(function (sl) { var o = orte[sl] || {}; Object.keys(o).forEach(function (k) { liste[k] = (liste[k] || 0) + (+o[k] || 0); }); }); 55 } else { liste = alle; } 56 if (Object.keys(liste).length) gruppen.push({ name: '', liste: liste }); 57 } 58 var vor = String($sel.data('vorauswahl') || ''), vorher = String($sel.val() || ''); 59 var sortiert = function (o) { return Object.keys(o).sort(function (a, b) { return a.localeCompare(b, 'de'); }); }; 60 $sel.empty().append($('<option>', { value: '', text: 'Alle Ortsteile' })); 61 var werte = {}; 62 gruppen.forEach(function (g) { 63 var $ziel = (gruppen.length > 1) ? $('<optgroup>', { label: g.name }).appendTo($sel) : $sel; 64 sortiert(g.liste).forEach(function (k) { werte[k] = 1; $ziel.append($('<option>', { value: k, text: k + ' (' + g.liste[k] + ')' })); }); 65 }); 66 // Vorgabe des Kurzbefehls nur behalten, solange kein anderer Ort gewählt ist (sonst leere Trefferliste) 67 if (vor && !werte[vor] && (!hatOrtFilter || !gruppen.length)) { $sel.append($('<option>', { value: vor, text: vor.replace(/\|/g, ' + ') })); werte[vor] = 1; } 68 var neu = werte[vorher] ? vorher : ''; 69 $sel.val(neu); 70 var $w = $sel.closest('.prec-district-wrap'); 71 var zeigen = gruppen.length > 0 || (!!vor && !!werte[vor]); 72 if (zeigen) { $w.removeAttr('hidden'); } else { $w.attr('hidden', 'hidden'); } 73 return neu !== vorher; 74 } 75 window.precOtSync = precOtSync; 76 77 function precText(wert) { 78 if (wert === null || wert === undefined) return ''; 79 var box = document.createElement('textarea'); 80 box.innerHTML = String(wert); // Entitaeten aufloesen 81 var klartext = box.value; 82 box.textContent = klartext; // wieder maskieren 83 return box.innerHTML; 84 } 85 86 /** Wert fuer ein HTML-Attribut absichern (Anfuehrungszeichen inbegriffen). */ 87 function precAttr(wert) { 88 return precText(wert).replace(/"/g, '"').replace(/'/g, '''); 89 } 90 91 'use strict'; 92 93 /* ==================================================================== 94 Namespace â MUSS vor document.ready definiert sein 95 ==================================================================== */ 96 var PREC = window.PREC = window.PREC || {}; 97 98 /* ==================================================================== 99 Kategorie-Filter 100 ==================================================================== */ 101 PREC.filter = { 102 103 activeCat: '*', 104 activeDays: 0, 105 dateFrom: '', 106 dateTo: '', 107 searchQuery: '', 108 109 init: function () { 110 var self = this; 111 112 // Kategorie-Buttons 113 $(document).on('click', '.prec-filter-btn', function (e) { 114 var $btn = $(this); 115 var $wrap = $btn.closest('.prec-shortcode-wrap'); 116 $btn.closest('.prec-filter-cats').find('.prec-filter-btn').removeClass('active'); 117 $btn.addClass('active'); 118 self.activeCat = $btn.data('filter'); 119 // Eine ueber Filter-Buttons gesetzte Kategorie verfaellt, sobald 120 // der Nutzer selbst in der Leiste waehlt - und mit ihr ALLE 121 // uebrigen Button-Sperren (Stadt, Datum, past, Ort, Suche). 122 // Sonst filtert der Button unsichtbar weiter mit, obwohl der 123 // Nutzer laengst selbst uebernommen hat (Mischzustand). 124 $wrap.data('prec-ext-category', ''); 125 var fbtnOrig = $wrap.data('prec-fbtn-orig'); 126 if (fbtnOrig) { 127 $wrap.data('prec-locked-date-from', fbtnOrig.von || ''); 128 $wrap.data('prec-locked-date-to', fbtnOrig.bis || ''); 129 $wrap.data('prec-locked-search', fbtnOrig.search || ''); 130 $wrap.data('prec-locked-venue', fbtnOrig.venue || ''); 131 $wrap.data('prec-locked-organizer', fbtnOrig.organizer || ''); 132 $wrap.data('prec-locked-city', fbtnOrig.city || ''); 133 $wrap.data('prec-locked-past', fbtnOrig.past || ''); 134 self.dateFrom = fbtnOrig.von || ''; 135 self.dateTo = fbtnOrig.bis || ''; 136 } 137 // Aktiven Filter-Button optisch zuruecksetzen. 138 $('.prec-fbtn-aktiv').removeClass('prec-fbtn-aktiv'); 139 140 // AJAX-Reload: Neue Events für gewählte Kategorie laden 141 var $layout = $wrap.find('div[class*="prec-layout-"]').first(); 142 if ($layout.length && typeof precFrontend !== 'undefined') { 143 var cat = self.activeCat === '*' ? '' : self.activeCat; 144 _precLoadEvents($layout, 1, null, cat); 145 } else { 146 // Fallback: Client-Side Filter 147 self.applyAll( $wrap ); 148 } 149 }); 150 151 // ââ Time-Dropdown (Schnellfilter) ââââââââââââââââââââââ 152 $(document).on('click', '#prec-time-dropdown-btn', function (e) { 153 e.stopPropagation(); 154 var $btn = $(this); 155 var $menu = $btn.siblings('#prec-time-dropdown-menu'); 156 var isOpen = $menu.hasClass('open'); 157 $('.prec-time-dropdown-menu').removeClass('open'); 158 $('[id="prec-time-dropdown-btn"]').attr('aria-expanded','false'); 159 if (!isOpen) { $menu.addClass('open'); $btn.attr('aria-expanded','true'); } 160 }); 161 $(document).on('click', function () { 162 $('.prec-time-dropdown-menu').removeClass('open'); 163 $('[id="prec-time-dropdown-btn"]').attr('aria-expanded','false'); 164 }); 165 $(document).on('click', '.prec-time-opt', function (e) { 166 e.stopPropagation(); 167 var $opt = $(this); 168 var $wrap = $opt.closest('.prec-shortcode-wrap'); 169 var days = parseInt($opt.data('days')) || 0; 170 var label = $opt.text().trim(); 171 $opt.closest('.prec-time-dropdown-menu').find('.prec-time-opt').removeClass('active'); 172 $opt.addClass('active'); 173 $opt.closest('.prec-time-dropdown-wrap').find('#prec-time-label') 174 .html(precSym('kalender') + ' ').append(document.createTextNode(label)); 175 // a11y (seit v7.0.5): aria-label des Buttons mit der aktuellen Auswahl aktualisieren 176 $opt.closest('.prec-time-dropdown-wrap').find('#prec-time-dropdown-btn') 177 .attr('aria-label', 'Zeitraum-Filter, aktuell: ' + label); 178 $opt.closest('.prec-time-dropdown-menu').removeClass('open'); 179 $opt.closest('.prec-time-dropdown-wrap').find('#prec-time-dropdown-btn') 180 .attr('aria-expanded','false').toggleClass('active', days !== 0); 181 self.activeDays = days; 182 self.dateFrom = ''; 183 self.dateTo = ''; 184 $wrap.find('#prec-date-from, #prec-date-to').val(''); 185 $wrap.find('#prec-daterange-display').val('').attr('placeholder','Zeitraum wählen'); 186 self.updateResetBtn($wrap); 187 var $layout = $wrap.find('div[class*="prec-layout-"]').first(); 188 if ($layout.length && typeof _precLoadEvents === 'function') { 189 _precLoadEvents($layout, 1); 190 } else { self.applyAll($wrap); } 191 }); 192 193 // ââ Daterange-Picker (Kalender-Popup) ââ 194 (function initDateRangePicker(){ 195 var drFrom = '', drTo = '', drMonth = new Date(); 196 var drMode = 'range'; // 'range' oder 'single' 197 var months_de = ['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'];
198 var days_de = ['Mo','Di','Mi','Do','Fr','Sa','So']; 199 200 function pad(n){ return n < 10 ? '0'+n : ''+n; } 201 function toISO(d){ return d.getFullYear()+'-'+pad(d.getMonth()+1)+'-'+pad(d.getDate()); } 202 function fromISO(s){ var p=s.split('-'); return new Date(+p[0],+p[1]-1,+p[2]); } 203 function fmtDE(s){ if(!s) return ''; var p=s.split('-'); return p[2]+'.'+p[1]+'.'+p[0]; } 204 205 function renderCalendar(){ 206 var $grid = $('#prec-dr-grid'); 207 if (!$grid.length) return; 208 var y = drMonth.getFullYear(), m = drMonth.getMonth(); 209 $('#prec-dr-month-label').text(months_de[m]+' '+y); 210 211 // Modus-Buttons aktualisieren 212 $('#prec-dr-mode-range').toggleClass('active', drMode==='range'); 213 $('#prec-dr-mode-single').toggleClass('active', drMode==='single'); 214 215 var html = ''; 216 days_de.forEach(function(d){ html += '<div style="font-weight:600;color:#6b7280;padding:4px 0;">'+d+'</div>'; }); 217 218 var first = new Date(y,m,1), last = new Date(y,m+1,0); 219 var startDay = (first.getDay()+6)%7; 220 var today = toISO(new Date()); 221 222 for(var i=0;i<startDay;i++) html += '<div></div>'; 223 224 for(var d=1;d<=last.getDate();d++){ 225 var iso = y+'-'+pad(m+1)+'-'+pad(d); 226 var cls = 'prec-dr-day'; 227 if(iso===drFrom || iso===drTo) cls += ' prec-dr-selected'; 228 else if(drFrom && drTo && iso>drFrom && iso<drTo) cls += ' prec-dr-range'; 229 if(iso===today) cls += ' prec-dr-today'; 230 html += '<div class="'+cls+'" data-date="'+iso+'" style="padding:6px 0;border-radius:6px;cursor:pointer;">'+d+'</div>'; 231 } 232 $grid.html(html); 233 } 234 235 // Ganzes Popup: stopPropagation damit Document-Handler es nicht schlieÃt 236 $(document).on('click', '#prec-daterange-popup', function(e){ 237 e.stopPropagation(); 238 }); 239 240 // Popup öffnen/schlieÃen 241 $(document).on('click', '#prec-daterange-display', function(e){ 242 e.stopPropagation(); 243 var $popup = $('#prec-daterange-popup'); 244 if($popup.is(':visible')){ $popup.hide(); return; } 245 if(drFrom) drMonth = fromISO(drFrom); 246 else drMonth = new Date(); 247 renderCalendar(); 248 $popup.show(); 249 }); 250 $(document).on('click', function(){ 251 $('#prec-daterange-popup').hide(); 252 }); 253 254 // Monat navigieren 255 $(document).on('click', '.prec-dr-nav', function(){ 256 drMonth.setMonth(drMonth.getMonth() + parseInt($(this).data('dir'))); 257 renderCalendar(); 258 }); 259 260 // Modus umschalten (Click) 261 $(document).on('click', '#prec-dr-mode-single', function(){ 262 drMode = 'single'; drTo = ''; 263 updateDisplay(); renderCalendar(); 264 }); 265 $(document).on('click', '#prec-dr-mode-range', function(){ 266 drMode = 'range'; 267 renderCalendar(); 268 }); 269 // a11y (seit v7.0.6): Mode-Switcher per Tastatur (Space/Enter) bedienen, 270 // damit Screenreader-Nutzer das role="button" auch wirklich nutzen können. 271 $(document).on('keydown', '#prec-dr-mode-single, #prec-dr-mode-range', function(e){ 272 if (e.key === ' ' || e.key === 'Enter' || e.keyCode === 32 || e.keyCode === 13) { 273 e.preventDefault(); 274 $(this).trigger('click'); 275 } 276 }); 277 278 // Tag klicken 279 $(document).on('click', '.prec-dr-day', function(){ 280 var iso = $(this).data('date'); 281 if(drMode === 'single'){ 282 drFrom = iso; drTo = ''; 283 updateDisplay();
284 renderCalendar(); 285 } else { 286 // Range-Modus 287 if(!drFrom || (drFrom && drTo)){ 288 drFrom = iso; drTo = ''; 289 } else { 290 if(iso < drFrom){ drTo = drFrom; drFrom = iso; } 291 else { drTo = iso; } 292 } 293 updateDisplay(); 294 renderCalendar(); 295 } 296 }); 297 298 function updateDisplay(){ 299 var $disp = $('#prec-daterange-display'); 300 if(drMode === 'single' && drFrom){ 301 $disp.val(fmtDE(drFrom)); 302 } else if(drFrom && drTo){ 303 $disp.val(fmtDE(drFrom)+' â '+fmtDE(drTo)); 304 } else if(drFrom){ 305 $disp.val(fmtDE(drFrom)+' â â¦'); 306 } else { 307 $disp.val('').attr('placeholder','Zeitraum wählen'); 308 } 309 } 310 311 // Anwenden 312 $(document).on('click', '#prec-dr-apply', function(){ 313 var $wrap = $(this).closest('.prec-shortcode-wrap'); 314 $wrap.find('#prec-date-from').val(drFrom); 315 $wrap.find('#prec-date-to').val(drTo || drFrom); 316 self.dateFrom = drFrom; 317 self.dateTo = drTo || drFrom; 318 self.activeDays = 0; 319 $wrap.find('.prec-time-opt').removeClass('active').first().addClass('active'); 320 $wrap.find('#prec-time-label').html(precSym('kalender') + ' Alle Termine'); 321 // a11y (seit v7.0.5): aria-label zurücksetzen 322 $wrap.find('#prec-time-dropdown-btn') 323 .attr('aria-label', 'Zeitraum-Filter, aktuell: Alle Termine') 324 .removeClass('active'); 325 self.updateResetBtn($wrap); 326 $('#prec-daterange-popup').hide(); 327 var $layout = $wrap.find('div[class*="prec-layout-"]').first(); 328 if($layout.length && typeof _precLoadEvents === 'function'){ 329 _precLoadEvents($layout, 1); 330 } 331 }); 332 333 // Zurücksetzen 334 $(document).on('click', '#prec-dr-clear', function(){ 335 drFrom = ''; drTo = ''; 336 updateDisplay(); 337 renderCalendar(); 338 var $wrap = $(this).closest('.prec-shortcode-wrap'); 339 $wrap.find('#prec-date-from, #prec-date-to').val(''); 340 self.dateFrom = ''; self.dateTo = ''; 341 }); 342 })(); 343 344 // CSS für Daterange-Picker 345 if(!$('#prec-dr-styles').length){ 346 $('head').append('<style id="prec-dr-styles">' 347 +'.prec-dr-day:hover{background:#e0f2fe !important;}' 348 +'.prec-dr-selected{background:#2271b1 !important;color:#fff !important;font-weight:600;}' 349 +'.prec-dr-range{background:#dbeafe !important;color:#1d4ed8 !important;}' 350 +'.prec-dr-today{font-weight:700;box-shadow:inset 0 0 0 2px #2271b1;}' 351 +'.prec-dr-mode{display:inline-block;padding:4px 10px;font-size:11px;border:1px solid #dcdcde;border-radius:4px;cursor:pointer;background:#f9fafb;color:#6b7280;}' 352 +'.prec-dr-mode.active{background:#2271b1;color:#fff;border-color:#2271b1;}' 353 +'.prec-dr-mode:hover:not(.active){background:#f3f4f6;}' 354 +'</style>'); 355 } 356 357 // ALLE Filter zurücksetzen 358 $(document).on('click', '#prec-reset-all', function () { 359 var $wrap = $(this).closest('.prec-shortcode-wrap'); 360 self.activeCat = '*'; 361 self.activeDays = 0; 362 self.dateFrom = ''; 363 self.dateTo = ''; 364 self.searchQuery = ''; 365 $wrap.find('#prec-date-from, #prec-date-to').val(''); 366 $wrap.find('#prec-daterange-display').val('').attr('placeholder','Zeitraum wählen'); 367 $wrap.find('#prec-event-search').val(''); 368 $wrap.find('.prec-time-btn').removeClass('active').first().addClass('active'); 369 // a11y (seit v7.0.6): Time-Dropdown-Label und sein aria-label zurücksetzen, 370 // damit Screenreader nach Reset nicht mehr die alte Auswahl ansagen. 371 $wrap.find('.prec-time-opt').removeClass('active').first().addClass('active'); 372 $wrap.find('#prec-time-label').html(precSym('kalender') + ' Alle Termine'); 373 $wrap.find('#prec-time-dropdown-btn') 374 .attr('aria-label', 'Zeitraum-Filter, aktuell: Alle Termine') 375 .removeClass('active'); 376 377 // Seit v6.9.7: Reset bringt den Filter zurück zum INITIAL-Zustand des Shortcodes 378 // â also zur Shortcode-Vorauswahl (location="cottbus", category="kultur"), 379 // NICHT zu âAlle Orte / Alle Kategorien". Das ist intuitiver: Reset = zurück 380 // zum Ausgangszustand, wie die Seite ursprünglich geladen wurde. 381 382 // Kategorie zurücksetzen 383 var preselectedCat = $wrap.data('prec-locked-category') || ''; 384 $wrap.find('.prec-filter-btn').removeClass('active'); 385 if (preselectedCat) { 386 var $preBtn = $wrap.find('.prec-filter-btn[data-filter="' + preselectedCat + '"]').first(); 387 if ($preBtn.length) { 388 $preBtn.addClass('active'); 389 } else { 390 $wrap.find('.prec-filter-btn[data-filter="*"]').first().addClass('active'); 391 } 392 } else { 393 $wrap.find('.prec-filter-btn[data-filter="*"]').first().addClass('active'); 394 } 395 396 // Standort zurücksetzen 397 var preselectedLoc = $wrap.data('prec-locked-location') || ''; 398 $wrap.find('.prec-loc-check').prop('checked', false); 399 if (preselectedLoc) { 400 var preselectedLocs = preselectedLoc.toString().split(',').map(function(s){return s.trim();}
400).filter(Boolean); 401 var checkedCount = 0; 402 preselectedLocs.forEach(function(slug){ 403 // seit v11.1.0: ein Eintrag kann mehrere gleichnamige Orte bündeln (data-slugs) 404 var $cb = $wrap.find('.prec-loc-check').filter(function(){ 405 return (' ' + ($(this).attr('data-slugs') || $(this).val()) + ' ').indexOf(' ' + slug + ' ') !== -1; 406 }).first(); 407 if ($cb.length && !$cb.prop('checked')) { $cb.prop('checked', true); checkedCount++; } 408 }); 409 $wrap.find('.prec-loc-check-all').prop('checked', checkedCount === 0); 410 // Button-Label aktualisieren 411 var $btn = $wrap.find('#prec-location-btn .prec-loc-label'); 412 var locText; 413 if (checkedCount === 0) { locText = 'Alle Orte'; } 414 else if (checkedCount === 1) { locText = $wrap.find('.prec-loc-check:checked').first().data('name') || preselectedLocs[0]; } 415 else { locText = checkedCount + ' Orte'; } 416 $btn.text(locText); 417 // a11y (seit v7.0.5): aria-label des Buttons synchron 418 $wrap.find('#prec-location-btn').attr('aria-label', 'Orte-Filter, aktuell: ' + locText); 419 } else { 420 $wrap.find('.prec-loc-check-all').prop('checked', true); 421 $wrap.find('#prec-location-btn .prec-loc-label').text('Alle Orte'); 422 $wrap.find('#prec-location-btn').attr('aria-label', 'Orte-Filter, aktuell: Alle Orte'); 423 } 424 $wrap.find('#prec-location-btn').css({'border-color':'#dcdcde','background':'#fff'}); 425 426 // Seit v10.9.0: Ortsteil + Non-Profit auf die Vorauswahl des Kurzbefehls zurück 427 // Seit v11.1.0: Liste zuerst an die (zurückgesetzten) Orte anpassen 428 $wrap.find('.prec-district-select').each(function () { $(this).val(''); }); 429 precOtSync($wrap); 430 $wrap.find('.prec-district-select').each(function () { $(this).val(($(this).data('vorauswahl') || '') + ''); }); 431 $wrap.find('.prec-np-check').each(function () { $(this).prop('checked', ($(this).data('vorauswahl') + '') === '1'); }); 432 433 $wrap.find('#prec-search-bar').slideUp(200); 434 self.updateResetBtn( $wrap ); 435 var $layout = $wrap.find('div[class*="prec-layout-"]').first(); 436 if ($layout.length && typeof _precLoadEvents === 'function') { 437 _precLoadEvents($layout, 1); 438 } else { 439 self.applyAll( $wrap ); 440 } 441 }); 442 443 // Ort-Filter 444 // ââ ORT-FILTER (Mehrfach-Checkboxen) ââââââââââââââââââââââââ 445 // Dropdown öffnen/schlieÃen 446 $(document).on('click', '#prec-location-btn', function (e) { 447 e.stopPropagation(); 448 var $dd = $(this).siblings('#prec-location-dropdown'); 449 var open = $dd.is(':visible'); 450 $('.prec-location-dropdown').hide(); 451 $('.prec-location-btn').attr('aria-expanded','false'); 452 if (!open) { 453 $dd.show(); 454 $(this).attr('aria-expanded','true'); 455 // Smart-Positioning: rechts ausrichten wenn kein Platz links 456 var ddRight = $dd.offset().left + $dd.outerWidth(); 457 if (ddRight > $(window).width() - 10) { 458 $dd.css({ left: 'auto', right: '0' }); 459 } else { 460 $dd.css({ left: '0', right: 'auto' }); 461 } 462 // Such-Feld fokussieren (seit v6.9.5) 463 var $search = $dd.find('.prec-loc-search'); 464 if ($search.length) { 465 setTimeout(function(){ $search.trigger('focus'); }, 50); 466 } 467 } 468 }); 469 470 // Such-Filter im Standort-Dropdown (seit v6.9.5) 471 // Filtert die Liste live nach dem eingegebenen Text â case-insensitive, 472 // matcht den Anfang oder beliebige Stelle im Ortsnamen. 473 // Seit v6.9.9: Bei aktiver Suche werden auch initial versteckte Orte 474 // (prec-loc-hidden-default) sichtbar â der User soll alle Orte durchsuchen können. 475 $(document).on('input', '.prec-loc-search', function(){ 476 var q = ($(this).val() || '').toLowerCase().trim(); 477 var $dd = $(this).closest('.prec-location-dropdown'); 478 var $items = $dd.find('.prec-loc-item').not('.prec-loc-all'); 479 var visibleCount = 0; 480 $items.each(function(){ 481 var name = ($(this).data('loc-name') || '').toString(); 482 var match = q === '' || name.indexOf(q) !== -1; 483 // Bei Suche: alle Orte berücksichtigen (auch initial versteckte) 484 if (q !== '') { 485 $(this).css('display', match ? 'flex' : 'none'); 486 } else { 487 // Suche leer: initial versteckte Orte wieder verstecken, 488 // auÃer der Toggle-Button wurde geklickt (Klasse entfernt) 489 var isHiddenByDefault = $(this).hasClass('prec-loc-hidden-default'); 490 $(this).css('display', isHiddenByDefault ? 'none' : 'flex'); 491 } 492 if (match && $(this).css('display') !== 'none') visibleCount++; 493 }); 494 // Toggle-Button bei aktiver Suche ausblenden 495 $dd.find('.prec-loc-show-all').toggle(q === ''); 496 $dd.find('.prec-loc-noresults').toggle(visibleCount === 0 && q !== ''); 497 }); 498 499 // Toggle-Button âAlle weiteren Orte anzeigen" (seit v6.9.9) 500 $(document).on('click', '.prec-loc-show-all', function(){ 501 var $dd = $(this).closest('.prec-location-dropdown'); 502 $dd.find('.prec-loc-hidden-default').css('display', 'flex').removeClass('prec-loc-hidden-default'); 503 $(this).hide(); 504 }); 505 506 // âAlle Orte" Checkbox-Click zeigt auch alle versteckten Orte 507 $(document).on('change', '.prec-loc-check-all', function(){ 508 if ($(this).is(':checked')) { 509 var $dd = $(this).closest('.prec-location-dropdown'); 510 $dd.find('.prec-loc-hidden-default').css('display', 'flex').removeClass('prec-loc-hidden-default'); 511 $dd.find('.prec-loc-show-all').hide(); 512 } 513 }); 514 515 // Esc-Taste schlieÃt Dropdown 516 $(document).on('keydown', '.prec-loc-search', function(e){ 517 if (e.key === 'Escape') { 518 $(this).val('').trigger('input'); 519 $(this).closest('.prec-location-dropdown').hide(); 520 $(this).closest('.prec-location-filter-wrap').find('.prec-location-btn').attr('aria-expanded','false'); 521 } 522 }); 523 $(document).on('click', function (e) { 524 if (!$(e.target).closest('.prec-location-filter-wrap').length) { 525 $('.prec-location-dropdown').hide(); 526 $('.prec-location-btn').attr('aria-expanded','false'); 527 } 528 }); 529 // "Alle Orte" Checkbox 530 $(document).on('change', '.prec-loc-check-all', function () { 531 var $fwrap = $(this).closest('.prec-location-filter-wrap'); 532 if ($(this).is(':checked')) $fwrap.find('.prec-loc-check').prop('checked', false); 533 precUpdateLocationFilter($fwrap.closest('.prec-shortcode-wrap')); 534 }); 535 // Einzel-Checkbox 536 $(document).on('change', '.prec-loc-check', function () { 537 var $fwrap = $(this).closest('.prec-location-filter-wrap'); 538 var any = $fwrap.find('.prec-loc-check:checked').length > 0; 539 $fwrap.find('.prec-loc-check-all').prop('checked', !any); 540 precUpdateLocationFilter($fwrap.closest('.prec-shortcode-wrap')); 541 }); 542 // Ortsteil-Auswahl und âNur Non-Profit" (seit v10.9.0) â kombinierbar mit allen übrigen Filtern 543 $(document).on('change', '.prec-district-select, .prec-np-check', function () { 544 var $swrap = $(this).closest('.prec-shortcode-wrap'); 545 if (!$swrap.length) return; 546 self.updateResetBtn($swrap); 547 var $layout = $swrap.find('div[class*="prec-layout-"]').first(); 548 if ($layout.length && typeof _precLoadEvents === 'function') _precLoadEvents($layout, 1); 549 }); 550 function precUpdateLocationFilter($swrap) { 551 var $btn = $swrap.find('#prec-location-btn'); 552 var checked = $swrap.find('.prec-loc-check:checked'); 553 var locText; 554 if (checked.length === 0) { 555 locText = 'Alle Orte'; 556 $btn.find('.prec-loc-label').text(locText); 557 $btn.attr('aria-expanded','false'); 558 } else if (checked.length === 1) { 559 locText = checked.first().data('name'); 560 $btn.find('.prec-loc-label').text(locText); 561 $btn.attr('aria-expanded','true'); 562 } else { 563 locText = checked.length + ' Orte'; 564 $btn.find('.prec-loc-label').text(locText); 565 $btn.attr('aria-expanded','true'); 566 } 567 // a11y (seit v7.0.5): aria-label des Buttons mit aktueller Auswahl 568 $btn.attr('aria-label', 'Orte-Filter, aktuell: ' + locText); 569 precOtSync($swrap); // seit v11.1.0: Ortsteile der gewählten Orte 570 self.updateResetBtn($swrap); 571 var $layout = $swrap.find('div[class*="prec-layout-"]').first(); 572 if ($layout.length && typeof _precLoadEvents === 'function') _precLoadEvents($layout, 1); 573 } 574 575 // Layout-Switcher 576 // Masonry auf Mobile (â¤480px) aus dem Layout-Switcher entfernen 577 function precAdjustLayoutSwitcher() { 578 var vw = window.innerWidth || document.documentElement.clientWidth; 579 var $sel = $('#prec-layout-switcher'); 580 if (!$sel.length) return; 581 if (vw <= 480) { 582 // Masonry-Option ausblenden (disabled + class) 583 $sel.find('option[value="masonry"]').prop('disabled', true).hide(); 584 // Falls gerade Masonry aktiv: auf Grid wechseln 585 if ($sel.val() === 'masonry') { 586 $sel.val('grid').trigger('change'); 587 } 588 } else { 589 $sel.find('option[value="masonry"]').prop('disabled', false).show(); 590 } 591 } 592 precAdjustLayoutSwitcher(); 593 $(window).on('resize', function() { precAdjustLayoutSwitcher(); }); 594 595 $(document).on('change', '#prec-layout-switcher', function () { 596 var newLayout = $(this).val(); 597 var $wrap = $(this).closest('.prec-shortcode-wrap'); 598 // Layout-Wrapper: nur DIV-Elemente mit prec-layout- Klasse (nicht das Select-Dropdown) 599 var $old = $wrap.find('div[class*="prec-layout-"]').first(); 600 if (!$old.length) return; 601 602 // Alte Layout-Klasse entfernen, neue setzen 603 var oldClass = ($old.attr('class').match(/prec-layout-[\w-]+/) || [])[0]; 604 if (oldClass) $old.removeClass(oldClass); 605 $old.addClass('prec-layout-' + newLayout); 606 607 // Container-Klasse austauschen 608 var containerMap = { 609 'grid': 'prec-events-grid', 'image-cards': 'prec-ic2-grid', 610 'color-cards': 'prec-cc-grid', 'image-grid': 'prec-ig-grid', 611 'color-list': 'prec-cl-list', 'list': 'prec-events-list', 612 'horizontal': 'prec-horizontal-list', 'masonry': 'prec-masonry-grid', 613 'timeline': 'prec-timeline' 614 }; 615 var $list = $old.find('.prec-events-grid, .prec-ic2-grid, .prec-cc-grid, .prec-ig-grid, .prec-cl-list, .prec-events-list, .prec-horizontal-list, .prec-masonry-grid, .prec-timeline').first(); 616 if ($list.length) { 617 // Alle Container-Klassen entfernen 618 for (var k in containerMap) $list.removeClass(containerMap[k]); 619 $list.addClass(containerMap[newLayout] || 'prec-events-grid'); 620 $list.removeAttr('style'); // Inline-Grid-Styles entfernen 621 } 622 623 // AJAX-Reload mit neuem Layout 624 _precLoadEvents($old, 1); 625 626 // Auswahl merken 627 try { localStorage.setItem('prec_user_layout', newLayout); } catch(e) {} 628 }); 629 630 // Suche: Toggle-Button (original) 631 $(document).on('click', '#prec-search-toggle', function () { 632 var $wrap = $(this).closest('.prec-shortcode-wrap') || $(this).closest('.prec-filter-bar-wrap').parent(); 633 var $bar = $wrap.find('#prec-search-bar'); 634 if ( !$bar.length ) $bar = $(this).closest('.prec-filter-bar-wrap').find('#prec-search-bar'); 635 if ( $bar.is(':visible') ) {
636 $bar.slideUp(200); 637 } else { 638 $bar.slideDown(300, function(){ $bar.find('#prec-event-search').focus(); }); 639 } 640 }); 641 642 // Suche: SchlieÃen 643 $(document).on('click', '#prec-search-close', function () { 644 var $wrap = $(this).closest('.prec-shortcode-wrap') || $(this).closest('.prec-filter-bar-wrap').parent(); 645 self.searchQuery = ''; 646 $(this).closest('.prec-search-bar').find('#prec-event-search').val(''); 647 $(this).closest('.prec-search-bar').slideUp(200); 648 self.updateResetBtn( $wrap ); 649 var $layout = $wrap.find('div[class*="prec-layout-"]').first(); 650 if ($layout.length && typeof _precLoadEvents === 'function') { 651 _precLoadEvents($layout, 1); 652 } else { 653 self.applyAll( $wrap ); 654 } 655 }); 656 657 // Suche: Live-Eingabe mit Debounce 658 var searchTimer; 659 $(document).on('input', '#prec-event-search', function () { 660 var $wrap = $(this).closest('.prec-shortcode-wrap') || $(this).closest('.prec-filter-bar-wrap').parent(); 661 self.searchQuery = $(this).val().toLowerCase().trim(); 662 self.updateResetBtn( $wrap ); 663 clearTimeout(searchTimer); 664 searchTimer = setTimeout(function() { 665 var $layout = $wrap.find('div[class*="prec-layout-"]').first(); 666 if ($layout.length && typeof _precLoadEvents === 'function') { 667 _precLoadEvents($layout, 1); 668 } else { 669 self.applyAll( $wrap ); 670 } 671 }, 400); 672 }); 673 674 // Kategorie-Overflow: "+X" Button wenn Buttons nicht in eine Zeile passen 675 var self = this; 676 // Erst nach kurzer Verzögerung prüfen (Layout muss gerendert sein) 677 setTimeout(function() { self.checkCatOverflow(); }, 150); 678 $(window).on('resize', function() { self.checkCatOverflow(); }); 679 $(document).on('click', '#prec-cat-more', function() { 680 var $inner = $('#prec-cat-inner'); 681 var isExpanded = $inner.hasClass('prec-cat-expanded'); 682 $inner.toggleClass('prec-cat-expanded'); 683 if (isExpanded) { 684 // Zuklappen 685 $(this).html('+' + $(this).data('hiddenCount')).removeClass('prec-cat-less'); 686 } else { 687 // Aufklappen 688 $(this).html('â').addClass('prec-cat-less'); 689 } 690 }); 691 }, 692 693 checkCatOverflow: function() { 694 var $inner = $('#prec-cat-inner'); 695 var $more = $('#prec-cat-more'); 696 if (!$inner.length) return; 697 698 // Reset 699 $inner.removeClass('prec-cat-expanded'); 700 $more.removeClass('prec-cat-less'); 701 $inner.find('.prec-filter-btn').show(); 702 $more.hide(); 703 704 var maxH = 42; // Eine Zeile (Button-Höhe + gap) 705 if ($inner[0].scrollHeight <= maxH) return; 706 707 // Buttons zählen die nicht sichtbar sind 708 var hidden = 0; 709 var btns = $inner.find('.prec-filter-btn'); 710 btns.each(function(i) { 711 if (i === 0) return; // "Alle" immer zeigen 712 if (this.offsetTop > btns.first()[0].offsetTop) { 713 hidden++; 714 } 715 }); 716 717 if (hidden > 0) { 718 $more.data('hiddenCount', hidden).html('+' + hidden).show(); 719 } 720 }, 721 722 updateResetBtn: function( $wrap ) { 723 var active = this.activeDays > 0 || !!this.dateFrom || !!this.dateTo || this.activeCat !== '*' || !!this.searchQuery; 724 // Seit v10.9.0: abweichender Ortsteil / Non-Profit zählt als aktiver Filter 725 if (!active && $wrap && $wrap.length) { 726 $wrap.find('.prec-district-select').each(function () { if (($(this).val() || '') !== (($(this).data('vorauswahl') || '') + '')) active = true; }); 727 $wrap.find('.prec-np-check').each(function () { if ($(this).is(':checked') !== (($(this).data('vorauswahl') + '') === '1')) active = true; }); 728 } 729 $wrap.find('#prec-reset-all')[ active ? 'show' : 'hide' ](); 730 }, 731 732 applyAll: function ( $wrap ) { 733 if ( !$wrap || !$wrap.length ) { 734 console.error('[EK] applyAll: $wrap leer oder nicht gefunden!'); 735 return; 736 } 737 var self = this; 738 // Server-seitiges Datum verwenden (WordPress-Zeitzone, kein UTC-Versatz) 739 var todayStr = (precFrontend && precFrontend.today) ? precFrontend.today : (function(){ 740 var d = new Date(); 741 return d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0'); 742 })(); 743 var todayParts = todayStr.split('-'); 744 var today = new Date(+todayParts[0], +todayParts[1]-1, +todayParts[2]); 745 today.setHours(0, 0, 0, 0); 746 747 var rangeFrom = null, rangeTo = null; 748 749 if ( self.dateFrom || self.dateTo ) { 750 if ( self.dateFrom ) rangeFrom = new Date( self.dateFrom + 'T00:00:00' ); 751 if ( self.dateTo ) rangeTo = new Date( self.dateTo + 'T23:59:59' ); 752 } else if ( self.activeDays === 1 ) { 753 rangeFrom = new Date( today ); 754 rangeTo = new Date( today ); 755 rangeTo.setHours(23, 59, 59, 0); 756 } else if ( self.activeDays > 1 ) { 757 rangeFrom = new Date( today ); 758 rangeTo = new Date( today ); 759 rangeTo.setDate( rangeTo.getDate() + self.activeDays - 1 ); 760 rangeTo.setHours(23, 59, 59, 0); 761 } 762 763 var anyVisible = false;
764 $wrap.find('.prec-event-card').each(function () { 765 var $card = $(this); 766 var categories = ' ' + ($card.data('categories') || $card.data('category') || '') + ' '; 767 var cardDate = $card.data('date') || ''; 768 var cardTime = $card.data('time') || '00:00'; 769 770 var catMatch = self.activeCat === '*' || categories.indexOf(' ' + self.activeCat + ' ') !== -1; 771 var dateMatch = true; 772 if ( cardDate && (rangeFrom || rangeTo) ) { 773 var dt = new Date( cardDate + 'T' + cardTime ); 774 if ( rangeFrom && dt < rangeFrom ) dateMatch = false; 775 if ( rangeTo && dt > rangeTo ) dateMatch = false; 776 } else if ( (rangeFrom || rangeTo) && !cardDate ) { 777 dateMatch = false; 778 } 779 780 // Textsuche 781 var textMatch = true; 782 if ( self.searchQuery ) { 783 var cardText = ($card.text() || '').toLowerCase(); 784 textMatch = cardText.indexOf( self.searchQuery ) !== -1; 785 } 786 787 var show = catMatch && dateMatch && textMatch; 788 $card[ show ? 'show' : 'hide' ](); 789 if (show) anyVisible = true; 790 }); 791 792 this.updateResetBtn( $wrap ); 793 $wrap.find('.prec-no-events-filtered').remove(); 794 if ( !anyVisible ) { 795 var msg = self.searchQuery 796 ? 'Keine Veranstaltungen für â' + precText(self.searchQuery) + '" gefunden.' 797 : (self.dateFrom || self.dateTo || self.activeDays > 0) 798 ? 'Keine Veranstaltungen im gewählten Zeitraum.' 799 : 'Keine Veranstaltungen in dieser Kategorie.'; 800 $wrap.find('.prec-events-wrap').append( 801 '<p class="prec-no-events prec-no-events-filtered">' + msg + '</p>' 802 ); 803 } 804 } 805 }; 806 807 /* ==================================================================== 808 Document ready â alle Module starten 809 ==================================================================== */ 810 $(function () { 811 // seit v11.1.0: Ortsteil-Auswahl an die vorausgewählten Orte anpassen (ohne Neuladen) 812 $('.prec-shortcode-wrap').each(function () { precOtSync($(this)); }); 813 if (PREC.filter) PREC.filter.init(); 814 if (PREC.catOverflow) PREC.catOverflow.init(); 815 if (PREC.registration) PREC.registration.init(); 816 if (PREC.sharing) PREC.sharing.init(); 817 if (PREC.slider) PREC.slider.init(); 818 if (PREC.sidebar) PREC.sidebar.init(); 819 if (PREC.countdown) PREC.countdown.init(); 820 if (PREC.calendar) PREC.calendar.init(); 821 if (PREC.map) PREC.map.init(); 822 if (PREC.gallery) PREC.gallery.init(); 823 }); 824 825 /* ==================================================================== 826 Kategorie-Overflow: wird von PREC.filter.checkCatOverflow gesteuert 827 ==================================================================== */ 828 PREC.catOverflow = { 829 init: function () { /* Deaktiviert â PREC.filter.init() übernimmt */ } 830 }; 831 832 PREC.sidebar = { 833 init: function () { 834 // Sidebar-Boxen: Immer offen, kein Toggle. 835 // Alle Inhalte flieÃen natürlich nach unten. 836 } 837 }; 838 839 PREC.countdown = { 840 init: function () { 841 // Legacy countdown (shortcode) 842 this._initSimple(); 843 // Flip clock countdown (sidebar box) 844 this._initFlip(); 845 }, 846 847 _initSimple: function () { 848 var els = document.querySelectorAll('.prec-countdown[data-target]'); 849 els.forEach(function (el) { 850 var target = new Date(el.getAttribute('data-target').replace(/-/g, '/')).getTime(); 851 if (isNaN(target)) return; 852 function update() { 853 var diff = target - Date.now(); 854 if (diff <= 0) { el.classList.add('prec-countdown-ended'); clearInterval(t); return; } 855 var d = Math.floor(diff / 86400000); 856 var h = Math.floor((diff % 86400000) / 3600000); 857 var m = Math.floor((diff % 3600000) / 60000); 858 var s = Math.floor((diff % 60000) / 1000); 859 var q = el.querySelector.bind(el); 860 if (q('.prec-cd-days')) q('.prec-cd-days').textContent = d; 861 if (q('.prec-cd-hours')) q('.prec-cd-hours').textContent = h < 10 ? '0' + h : h; 862 if (q('.prec-cd-minutes')) q('.prec-cd-minutes').textContent = m < 10 ? '0' + m : m; 863 if (q('.prec-cd-seconds')) q('.prec-cd-seconds').textContent = s < 10 ? '0' + s : s; 864 } 865 update(); 866 var t = setInterval(update, 1000); 867 }); 868 }, 869 870 _initFlip: function () { 871 var clocks = document.querySelectorAll('.prec-flip-clock[data-target]'); 872 clocks.forEach(function (clock) { 873 var target = new Date(clock.getAttribute('data-target').replace(/-/g, '/')).getTime(); 874 if (isNaN(target)) return; 875 876 var cards = {}; 877 clock.querySelectorAll('.prec-flip-card').forEach(function (c) { 878 cards[c.getAttribute('data-unit')] = { 879 el: c, 880 front: c.querySelector('.prec-flip-front span'), 881 back: c.querySelector('.prec-flip-back span'), 882 current: '--', 883 busy: false 884 }; 885 }); 886 887 function pad(n) { return n < 10 ? '0' + n : '' + n; } 888 889 function flipTo(card, newVal) { 890 if (card.current === newVal || card.busy) return; 891 card.busy = true; 892 893 // Neue Zahl auf die Rückseite schreiben 894 card.back.textContent = newVal; 895 896 // Flip-Animation starten 897 card.el.classList.add('prec-flipping'); 898 899 // Nach der CSS-Transition (800ms): aufräumen 900 setTimeout(function () { 901 card.el.classList.remove('prec-flipping'); 902 // Vorderseite übernimmt die neue Zahl 903 card.front.textContent = newVal; 904 card.current = newVal; 905 card.busy = false;
906 }, 820); 907 } 908 909 function update() { 910 var diff = target - Date.now(); 911 if (diff <= 0) { 912 Object.keys(cards).forEach(function (k) { flipTo(cards[k], '0'); }); 913 clearInterval(timer); 914 return; 915 } 916 var d = Math.floor(diff / 86400000); 917 var h = Math.floor((diff % 86400000) / 3600000); 918 var m = Math.floor((diff % 3600000) / 60000); 919 var s = Math.floor((diff % 60000) / 1000); 920 921 if (cards.days) flipTo(cards.days, '' + d); 922 if (cards.hours) flipTo(cards.hours, pad(h)); 923 if (cards.minutes) flipTo(cards.minutes, pad(m)); 924 if (cards.seconds) flipTo(cards.seconds, pad(s)); 925 } 926 927 // Initialwerte ohne Animation setzen 928 var diff0 = target - Date.now(); 929 if (diff0 > 0) { 930 var d0 = Math.floor(diff0 / 86400000); 931 var h0 = Math.floor((diff0 % 86400000) / 3600000); 932 var m0 = Math.floor((diff0 % 3600000) / 60000); 933 var s0 = Math.floor((diff0 % 60000) / 1000); 934 if (cards.days) { cards.days.front.textContent = '' + d0; cards.days.current = '' + d0; } 935 if (cards.hours) { cards.hours.front.textContent = pad(h0); cards.hours.current = pad(h0); } 936 if (cards.minutes) { cards.minutes.front.textContent = pad(m0); cards.minutes.current = pad(m0); } 937 if (cards.seconds) { cards.seconds.front.textContent = pad(s0); cards.seconds.current = pad(s0); } 938 } 939 940 var timer = setInterval(update, 1000); 941 }); 942 } 943 }; 944 945 PREC.registration = { 946 init: function () { 947 $(document).on('submit', '.prec-reg-form', function (e) { 948 e.preventDefault(); 949 var $form = $(this); 950 var $btn = $form.find('button[type="submit"]'); 951 var $msg = $form.find('.prec-form-message'); 952 953 $btn.prop('disabled', true).text('Wird gesendet â¦'); 954 $msg.html(''); 955 956 $.post(precFrontend.ajaxUrl, $form.serialize() + '&action=prec_register&nonce=' + precFrontend.nonce, function (res) { 957 if (res.success) { 958 $msg.html('<div class="prec-notice prec-notice-success">â ' + res.data.message + '</div>'); 959 $form[0].reset(); 960 } else { 961 $msg.html('<div class="prec-notice prec-notice-error">â ' + res.data.message + '</div>'); 962 } 963 }).fail(function () { 964 $msg.html('<div class="prec-notice prec-notice-error">â Ein Fehler ist aufgetreten.</div>'); 965 }).always(function () { 966 $btn.prop('disabled', false).text('Jetzt anmelden'); 967 }); 968 }); 969 } 970 }; 971 972 /* ==================================================================== 973 Social Sharing (copy link) 974 ==================================================================== */ 975 PREC.sharing = { 976 init: function () { 977 $(document).on('click', '.prec-share-copy', function (e) { 978 e.preventDefault(); 979 var url = $(this).attr('href') || window.location.href; 980 if (navigator.clipboard) { 981 navigator.clipboard.writeText(url).then(function () { 982 PREC.sharing.toast('Link kopiert! ð'); 983 }); 984 } else { 985 var ta = document.createElement('textarea'); 986 ta.value = url; 987 document.body.appendChild(ta); 988 ta.select(); 989 document.execCommand('copy'); 990 document.body.removeChild(ta); 991 PREC.sharing.toast('Link kopiert! ð'); 992 } 993 }); 994 }, 995 toast: function (msg) {
996 var $t = $('<div class="prec-toast">' + msg + '</div>').css({ 997 position: 'fixed', bottom: '24px', right: '24px', background: '#333', color: '#fff', 998 padding: '10px 20px', borderRadius: '8px', zIndex: 9999, fontSize: '14px', opacity: 0 999 }).appendTo('body').animate({ opacity: 1 }, 200); 1000 setTimeout(function () { $t.animate({ opacity: 0 }, 300, function () { $t.remove(); }); }, 2500); 1001 } 1002 }; 1003 1004 /* ==================================================================== 1005 FullCalendar 1006 ==================================================================== */ 1007 PREC.calendar = { 1008 init: function () { 1009 var $wrap = $('#prec-fullcalendar'); 1010 if (!$wrap.length) return; 1011 1012 if (typeof FullCalendar === 'undefined') { 1013 $(window).on('load', PREC.calendar.init); 1014 return; 1015 } 1016 1017 // Tooltip-Element 1018 var $tip = $('<div class="prec-cal-tooltip"></div>').appendTo('body').hide(); 1019 1020 var calendar = new FullCalendar.Calendar($wrap[0], { 1021 initialView: $wrap.data('view') === 'week' ? 'timeGridWeek' : 1022 $wrap.data('view') === 'day' ? 'timeGridDay' : 1023 $wrap.data('view') === 'agenda' ? 'listMonth' : 'dayGridMonth', 1024 locale: 'de', 1025 firstDay: 1, // Montag 1026 buttonText: { 1027 today: 'Heute', 1028 month: 'Monat', 1029 week: 'Woche', 1030 day: 'Tag', 1031 list: 'Liste' 1032 }, 1033 headerToolbar: { 1034 left: 'prev,next today', 1035 center: 'title', 1036 right: 'dayGridMonth,timeGridWeek,listMonth' 1037 }, 1038 height: 'auto', 1039 dayMaxEvents: 4, 1040 moreLinkText: function(n) { return '+' + n + ' weitere'; }, 1041 moreLinkClassNames: ['prec-cal-more-link'], 1042 events: { 1043 url: precFrontend.ajaxUrl, 1044 method: 'GET', 1045 extraParams: function () { 1046 return { 1047 action: 'prec_calendar_events', 1048 nonce: $wrap.data('nonce'), 1049 category: $wrap.data('category') || '', 1050 tag: $wrap.data('tag') || '', 1051 location: $wrap.data('location') || '', 1052 event_type: $wrap.data('event-type') || '', 1053 featured: $wrap.data('featured') || '', 1054 district: $wrap.attr('data-district') || '', // seit v10.9.0 1055 nonprofit: $wrap.attr('data-nonprofit') || '' 1056 }; 1057 } 1058 }, 1059 eventContent: function(arg) { 1060 var props = arg.event.extendedProps || {}; 1061 var time = props.time || ''; 1062 var title = arg.event.title || ''; 1063 var view = arg.view.type; 1064 1065 // Listenansicht: einfacher Text 1066 if (view === 'listMonth') { 1067 return { html: '<span class="prec-cal-list-title">' + precText(title) + '</span>' }; 1068 } 1069 1070 // Monats-/Wochenansicht: Design-Pill 1071 var html = '<div class="prec-cal-pill">'; 1072 if (time && !arg.event.allDay) { 1073 html += '<span class="prec-cal-pill-time">' + precText(time) + '</span>'; 1074 } 1075 html += '<span class="prec-cal-pill-title">' + precText(title) + '</span>'; 1076 html += '</div>'; 1077 return { html: html }; 1078 }, 1079 eventClick: function (info) { 1080 info.jsEvent.preventDefault(); 1081 $tip.hide(); 1082 if (info.event.url) window.location.href = info.event.url; 1083 }, 1084 eventDidMount: function (info) { 1085 var props = info.event.extendedProps || {}; 1086 var el = info.el; 1087 var cleanTitle = precText(info.event.title); 1088 1089 // Hover-Tooltip 1090 $(el).on('mouseenter', function(e) { 1091 var html = '<div class="prec-cal-tip-inner">'; 1092 if (props.thumb) { 1093 html += '<img class="prec-cal-tip-img" src="' + precAttr(props.thumb) + '" alt="">'; 1094 } 1095 html += '<div class="prec-cal-tip-body">'; 1096 html += '<strong class="prec-cal-tip-title">
1096' + cleanTitle + '</strong>'; 1097 if (props.time) { 1098 html += '<div class="prec-cal-tip-meta">ð ' + precText(props.time) + ' Uhr</div>'; 1099 } 1100 var loc = []; 1101 if (props.venue) loc.push(props.venue); 1102 if (props.city) loc.push(props.city); 1103 if (loc.length) { 1104 html += '<div class="prec-cal-tip-meta">ð ' + precText(loc.join(', ')) + '</div>'; 1105 } 1106 if (props.category) { 1107 html += '<div class="prec-cal-tip-cat"><span class="prec-cal-tip-cat-badge">' + precText(props.category) + '</span></div>'; 1108 } 1109 if (props.excerpt) { 1110 html += '<div class="prec-cal-tip-excerpt">' + precText(props.excerpt) + '</div>'; 1111 } 1112 html += '</div></div>'; 1113 $tip.html(html).show(); 1114 PREC.calendar._posTip(e, $tip); 1115 }).on('mousemove', function(e) { 1116 PREC.calendar._posTip(e, $tip); 1117 }).on('mouseleave', function() { 1118 $tip.hide(); 1119 }); 1120 }, 1121 loading: function (isLoading) { 1122 if (isLoading) { 1123 $wrap.find('.prec-calendar-loading').show(); 1124 } else { 1125 $wrap.find('.prec-calendar-loading').hide(); 1126 } 1127 } 1128 }); 1129 1130 calendar.render(); 1131 }, 1132 _posTip: function(e, $tip) { 1133 var x = e.pageX + 16; 1134 var y = e.pageY + 12; 1135 var tw = $tip.outerWidth(); 1136 var ww = $(window).width() + $(window).scrollLeft(); 1137 if (x + tw + 20 > ww) x = e.pageX - tw - 16; 1138 $tip.css({ left: x, top: y }); 1139 } 1140 }; 1141 1142 /* ==================================================================== 1143 Multi-marker map 1144 ==================================================================== */ 1145 PREC.map = { 1146 init: function () { 1147 var $maps = $('.prec-events-map'); 1148 if (!$maps.length) return; 1149 1150 $maps.each(function () { 1151 var $el = $(this); 1152 var markers = $el.data('markers') || []; 1153 var zoom = parseInt($el.data('zoom')) || 10; 1154 var lat = parseFloat($el.data('lat')); 1155 var lng = parseFloat($el.data('lng')); 1156 1157 if (!markers.length) return; 1158 1159 // Auto center 1160 if (!lat || !lng) { 1161 lat = parseFloat(markers[0].lat); 1162 lng = parseFloat(markers[0].lng); 1163 } 1164 1165 var provider = typeof precFrontend.mapProvider !== 'undefined' ? precFrontend.mapProvider : 'osm'; 1166 1167 if (provider === 'google' && typeof google !== 'undefined') { 1168 PREC.map.initGoogle($el[0], markers, lat, lng, zoom); 1169 } else if (typeof L !== 'undefined') { 1170 PREC.map.initLeaflet($el[0], markers, lat, lng, zoom); 1171 } 1172 }); 1173 }, 1174 1175 initLeaflet: function (el, markers, lat, lng, zoom) { 1176 var map = L.map(el, { scrollWheelZoom: false }).setView([lat, lng], zoom); 1177 L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { 1178 maxZoom: 19, 1179 attribution: '© OpenStreetMap' 1180 }).addTo(map); 1181 1182 markers.forEach(function (m) { 1183 // Titel und Datum werden hier zu HTML zusammengesetzt, das 1184 // Leaflet als Markup rendert - ohne Maskierung waere das eine 1185 // Einladung zu gespeichertem Cross-Site-Scripting. 1186 var popup = '<strong><a href="' + precAttr(m.url) + '">' + precText(m.title) + '</a></strong>'; 1187 if (m.date) popup += '<br><small>ð ' + precText(m.date) + '</small>'; 1188 L.marker([m.lat, m.lng]).addTo(map).bindPopup(popup); 1189 }); 1190 1191 if (markers.length > 1) { 1192 var bounds = L.latLngBounds(markers.map(function (m) { return [m.lat, m.lng]; })); 1193 map.fitBounds(bounds, { padding: [30, 30] }); 1194 } 1195 1196 // Strg+Scroll zum Zoomen (wie Google Maps cooperative) 1197 el.style.position = 'relative'; 1198 1199 // Dauerhafter dezenter Hinweis unten links 1200 var badge = document.createElement('div'); 1201 badge.innerHTML = '<span style="margin-right:5px;">â¨</span> Strg + Scrollen zum Zoomen'; 1202 badge.style.cssText = 'position:absolute;bottom:8px;left:8px;z-index:998;background:rgba(255,255,255,.92);color:#374151;font-size:11px;font-weight:500;padding:5px 10px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,.12);pointer-events:none;line-height:1.3;backdrop-filter:blur(4px);'; 1203 el.appendChild(badge); 1204 1205 // Vollbild-Overlay bei Scroll ohne Strg
1206 var overlay = document.createElement('div'); 1207 overlay.textContent = 'Strg + Scrollen zum Zoomen'; 1208 overlay.style.cssText = 'position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.4);color:#fff;font-size:15px;font-weight:600;z-index:999;pointer-events:none;opacity:0;transition:opacity .3s;border-radius:inherit;letter-spacing:.3px;'; 1209 el.appendChild(overlay); 1210 1211 var hideTimer; 1212 el.addEventListener('wheel', function(e){ 1213 if (e.ctrlKey || e.metaKey) { 1214 map.scrollWheelZoom.enable(); 1215 badge.style.opacity = '0'; 1216 } else { 1217 map.scrollWheelZoom.disable(); 1218 overlay.style.opacity = '1'; 1219 badge.style.opacity = '0'; 1220 clearTimeout(hideTimer); 1221 hideTimer = setTimeout(function(){ overlay.style.opacity = '0'; badge.style.opacity = '1'; }, 1200); 1222 } 1223 }, { passive: true }); 1224 el.addEventListener('mouseleave', function(){ map.scrollWheelZoom.disable(); overlay.style.opacity = '0'; badge.style.opacity = '1'; }); 1225 }, 1226 1227 initGoogle: function (el, markers, lat, lng, zoom) { 1228 var map = new google.maps.Map(el, { center: { lat: lat, lng: lng }, zoom: zoom, gestureHandling: 'cooperative' }); 1229 var bounds = new google.maps.LatLngBounds(); 1230 var infoWindow = new google.maps.InfoWindow(); 1231 1232 markers.forEach(function (m) { 1233 var marker = new google.maps.Marker({ 1234 position: { lat: m.lat, lng: m.lng }, 1235 map: map, 1236 title: m.title 1237 }); 1238 bounds.extend(marker.position); 1239 marker.addListener('click', function () { 1240 // Gleiche Begruendung wie beim Leaflet-Popup weiter oben. 1241 var content = '<strong><a href="' + precAttr(m.url) + '">' + precText(m.title) + '</a></strong>'; 1242 if (m.date) content += '<br><small>ð ' + precText(m.date) + '</small>'; 1243 infoWindow.setContent(content); 1244 infoWindow.open(map, marker); 1245 }); 1246 }); 1247 1248 if (markers.length > 1) map.fitBounds(bounds); 1249 } 1250 }; 1251 1252 /* ==================================================================== 1253 Lightbox â Professionelle Slider-Galerie 1254 ==================================================================== */ 1255 PREC.gallery = { 1256 $overlay: null, 1257 $img: null, 1258 $counter: null, 1259 items: [], 1260 current: 0, 1261 touchStartX: 0, 1262 1263 init: function () { 1264 var self = this; 1265 1266 // Click auf Gallery-Item â Lightbox öffnen 1267 $(document).on('click', '.prec-gallery-item[data-lightbox], a.prec-gallery-item', function (e) { 1268 e.preventDefault(); 1269 var $clicked = $(this); 1270 var group = $clicked.closest('.prec-gallery-grid, .prec-event-gallery'); 1271 1272 // Alle Bilder der Gruppe sammeln 1273 self.items = []; 1274 self.captions = []; // Bildautor je Bild (seit v10.3.1) 1275 group.find('a.prec-gallery-item, .prec-gallery-item[data-lightbox]').each(function () { 1276 self.items.push($(this).attr('href')); 1277 self.captions.push($(this).attr('data-caption') || ''); 1278 }); 1279 if (!self.items.length) { 1280 self.items.push($clicked.attr('href')); 1281 self.captions.push($clicked.attr('data-caption') || ''); 1282 } 1283 1284 // Index des geklickten Bildes 1285 self.current = self.items.indexOf($clicked.attr('href')); 1286 if (self.current < 0) self.current = 0; 1287 1288 self.open(); 1289 }); 1290 1291 // Keyboard 1292 $(document).on('keydown.precLightbox', function (e) { 1293 if (!self.$overlay || !self.$overlay.is(':visible')) return; 1294 if (e.key === 'Escape') self.close(); 1295 if (e.key === 'ArrowRight') self.next(); 1296 if (e.key === 'ArrowLeft') self.prev(); 1297 }); 1298 }, 1299 1300 build: function () { 1301 if (this.$overlay) return; 1302 var html = '<div class="prec-lb">' + 1303 '<div class="prec-lb-backdrop"></div>' + 1304 '<button class="prec-lb-close" aria-label="SchlieÃen"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>' + 1305 '<button class="prec-lb-nav prec-lb-prev" aria-label="Vorheriges Bild"><svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg></button>' + 1306 '<button class="prec-lb-nav prec-lb-next" aria-label="Nächstes Bild"><svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 6 15 12 9 18"/></svg></button>' + 1307 '<div class="prec-lb-stage">' + 1308 '<div class="prec-lb-spinner"></div>' + 1309 '<img class="prec-lb-img" src="" alt="">' + 1310 '</div>' + 1311 '<div class="prec-lb-footer">' + 1312 '<span class="prec-lb-caption"></span>' + 1313 '<span class="prec-lb-counter"></span>' + 1314 '</div>' + 1315 '</div>'; 1316
1317 this.$overlay = $(html).appendTo('body'); 1318 this.$img = this.$overlay.find('.prec-lb-img'); 1319 this.$counter = this.$overlay.find('.prec-lb-counter'); 1320 this.$caption = this.$overlay.find('.prec-lb-caption'); 1321 1322 var self = this; 1323 1324 // Close 1325 this.$overlay.find('.prec-lb-close').on('click', function () { self.close(); }); 1326 this.$overlay.find('.prec-lb-backdrop').on('click', function () { self.close(); }); 1327 1328 // Nav 1329 this.$overlay.find('.prec-lb-prev').on('click', function (e) { e.stopPropagation(); self.prev(); }); 1330 this.$overlay.find('.prec-lb-next').on('click', function (e) { e.stopPropagation(); self.next(); }); 1331 1332 // Touch/Swipe 1333 this.$overlay[0].addEventListener('touchstart', function (e) { 1334 self.touchStartX = e.changedTouches[0].screenX; 1335 }, { passive: true }); 1336 this.$overlay[0].addEventListener('touchend', function (e) { 1337 var diff = e.changedTouches[0].screenX - self.touchStartX; 1338 if (Math.abs(diff) > 50) { 1339 diff > 0 ? self.prev() : self.next(); 1340 } 1341 }, { passive: true }); 1342 }, 1343 1344 open: function () { 1345 this.build(); 1346 this.show(false); 1347 this.$overlay.addClass('prec-lb-visible'); 1348 $('body').css('overflow', 'hidden'); 1349 }, 1350 1351 close: function () { 1352 var self = this; 1353 this.$overlay.removeClass('prec-lb-visible'); 1354 $('body').css('overflow', ''); 1355 setTimeout(function () { 1356 self.$img.attr('src', ''); 1357 }, 300); 1358 }, 1359 1360 show: function (animate) { 1361 var self = this; 1362 var src = this.items[this.current]; 1363 if (!src) return; 1364 1365 // Counter + Bildautor 1366 this.$counter.text((this.current + 1) + ' / ' + this.items.length); 1367 if (this.$caption) this.$caption.text((this.captions && this.captions[this.current]) || ''); 1368 1369 // Nav Buttons 1370 this.$overlay.find('.prec-lb-prev').toggle(this.items.length > 1); 1371 this.$overlay.find('.prec-lb-next').toggle(this.items.length > 1); 1372 1373 // Loading state 1374 this.$overlay.addClass('prec-lb-loading'); 1375 if (animate) this.$img.addClass('prec-lb-img-fade'); 1376 1377 // Preload 1378 var img = new Image(); 1379 img.onload = function () { 1380 self.$img.attr('src', src); 1381 self.$overlay.removeClass('prec-lb-loading'); 1382 if (animate) { 1383 setTimeout(function () { self.$img.removeClass('prec-lb-img-fade'); }, 50); 1384 } 1385 }; 1386 img.onerror = function () { 1387 self.$overlay.removeClass('prec-lb-loading'); 1388 }; 1389 img.src = src; 1390 }, 1391 1392 next: function () { 1393 this.current = (this.current + 1) % this.items.length; 1394 this.show(true); 1395 }, 1396 1397 prev: function () { 1398 this.current = (this.current - 1 + this.items.length) % this.items.length; 1399 this.show(true); 1400 } 1401 }; 1402 1403 /* ------------------------------------------------------------------ */ 1404 /* Buchungssystem */ 1405 /* ------------------------------------------------------------------ */ 1406 PREC.booking = { 1407 init: function () { 1408 if (!$('.prec-booking-form').length) return; 1409 1410 $(document).on('change', '.prec-booking-date', function () { 1411 var $form = $(this).closest('.prec-booking-form'); 1412 var eventId = $form.data('event-id'); 1413 var date = $(this).val(); 1414 var $slotsWrap= $form.find('.prec-booking-slots'); 1415 var $grid = $form.find('.prec-slots-grid'); 1416 var $fields = $form.find('.prec-booking-fields'); 1417 1418 $grid.html('<p>Lade Zeitslotsâ¦</p>'); 1419 $slotsWrap.show(); 1420 $fields.hide(); 1421 1422 $.post(precFrontend.ajaxUrl, { 1423 action: 'prec_get_timeslots', 1424 nonce: precFrontend.nonce, 1425 event_id: eventId, 1426 date: date 1427 }, function (res) { 1428 $grid.empty(); 1429 if (res.success && res.data.length) { 1430 res.data.forEach(function (slot) { 1431 var $btn = $('<button type="button" class="prec-slot-btn">').text(slot.label).data('time', slot.time); 1432 if (slot.available === 0) $btn.prop('disabled', true).addClass('disabled'); 1433 $grid.append($btn); 1434 }); 1435 } else { 1436 $grid.html('<p>Keine Zeitslots verfügbar.</p>'); 1437 } 1438 }); 1439 }); 1440 1441 $(document).on('click', '.prec-slot-btn:not(.disabled)', function () { 1442 var $form = $(this).closest('.prec-booking-form'); 1443 $('.prec-slot-btn').removeClass('selected'); 1444 $(this).addClass('selected'); 1445 $form.data('selected-time', $(this).data('time')); 1446 $form.find('.prec-booking-fields').show(); 1447 }); 1448 1449 $(document).on('click', '.prec-book-submit', function () { 1450 var $form = $(this).closest('.prec-booking-form'); 1451 var $msg = $form.find('.prec-booking-message'); 1452 var $btn = $(this); 1453 var time = $form.data('selected-time'); 1454 1455 if (!time) { $msg.text('Bitte einen Zeitslot wählen.'); return; } 1456 1457 $btn.prop('disabled', true); 1458 $.post(precFrontend.ajaxUrl, { 1459 action: 'prec_book_timeslot', 1460 nonce: precFrontend.nonce, 1461 event_id: $form.data('event-id'), 1462 date: $form.find('[name=booking_date]').val(), 1463 time: time, 1464 name: $form.find('[name=booking_name]').val(), 1465 email: $form.find('[name=booking_email]').val(), 1466 phone: $form.find('[name=booking_phone]').val(), 1467 seats: $form.find('[name=booking_seats]').val() 1468 }, function (res) { 1469 $btn.prop('disabled', false); 1470 $msg.text(res.data.message || (res.success ? 'Erfolgreich!' : 'Fehler')); 1471 $msg.css('color', res.success ? 'green' : 'red'); 1472 }); 1473 }); 1474 } 1475 }; 1476 1477 // ========================================================= 1478 // AJAX PAGINATION 1479 // ========================================================= 1480 1481 /** 1482 * Externe Filter anwenden (seit v10.2.0, genutzt von den Filter-Buttons). 1483 * 1484 * Setzt exakt die Werte, die auch Filterleiste und Blaettern verwenden - 1485 * es gibt keinen zweiten Datenpfad. Ein leeres Objekt setzt alles zurueck. 1486 * Rueckgabe false, wenn auf der Seite keine Liste steht. 1487 */ 1488 PREC.applyExternalFilter = function (cfg) { 1489 cfg = cfg || {}; 1490 var $outer = $('.prec-shortcode-wrap').first(); 1491 // WICHTIG: mit div qualifizieren - [class*="prec-layout-"] traefe sonst 1492 // auch das Layout-Auswahlfeld (select.prec-layout-select) in der 1493 // Filterleiste, das im DOM VOR der Liste steht. 1494 var $layout = $outer.find('div[class*="prec-layout-"]').first(); 1495 if (!$layout.length) return false;
1496 1497 // Urzustand der Sperr-Attribute einmalig sichern, BEVOR der erste 1498 // Button sie ueberschreibt. Der Reset-Klick (leeres cfg) stellt so die 1499 // eigenen Shortcode-Attribute des Kalenders wieder her (venue=, city=, 1500 // date_from= usw.), statt sie pauschal zu loeschen. 1501 if (!$outer.data('prec-fbtn-orig')) { 1502 $outer.data('prec-fbtn-orig', { 1503 von: ($outer.data('prec-locked-date-from') || '') + '', 1504 bis: ($outer.data('prec-locked-date-to') || '') + '', 1505 search: ($outer.data('prec-locked-search') || '') + '', 1506 venue: ($outer.data('prec-locked-venue') || '') + '', 1507 organizer: ($outer.data('prec-locked-organizer') || '') + '', 1508 city: ($outer.data('prec-locked-city') || '') + '', 1509 past: ($outer.data('prec-locked-past') || '') + '', 1510 category: '' 1511 }); 1512 } 1513 var reset = !cfg.von && !cfg.bis && !cfg.search && !cfg.venue 1514 && !cfg.organizer && !cfg.city && !cfg.category && !cfg.past; 1515 var ziel = reset ? $outer.data('prec-fbtn-orig') : cfg; 1516 1517 if (PREC.filter) { 1518 PREC.filter.dateFrom = ziel.von || ''; 1519 PREC.filter.dateTo = ziel.bis || ''; 1520 PREC.filter.activeDays = 0; 1521 PREC.filter.searchQuery = ''; 1522 } 1523 // Sichtbare Suchfelder leeren - die Ladefunktion liest die Suche 1524 // direkt aus den Eingabefeldern, nicht nur aus dem Filter-Zustand. 1525 $outer.find('#prec-event-search, .prec-search-input, [data-prec-search]').val(''); 1526 $outer.data('prec-locked-date-from', ziel.von || ''); 1527 $outer.data('prec-locked-date-to', ziel.bis || ''); 1528 $outer.data('prec-locked-search', ziel.search || ''); 1529 $outer.data('prec-locked-venue', ziel.venue || ''); 1530 $outer.data('prec-locked-organizer', ziel.organizer || ''); 1531 $outer.data('prec-locked-city', ziel.city || ''); 1532 $outer.data('prec-locked-past', ziel.past || ''); 1533 1534 // Aktiven Kategorie-Knopf der Filterleiste optisch zuruecksetzen, 1535 // damit keine widerspruechliche Auswahl angezeigt wird. 1536 $outer.find('.prec-filter-btn.active').removeClass('active'); 1537 $outer.find('.prec-filter-btn[data-filter="*"]').addClass('active'); 1538 1539 // Kategorie am Wrapper merken: Die Ladefunktion liest die Kategorie 1540 // sonst nur aus der Filterleiste - beim Blaettern ginge die 1541 // Button-Kategorie verloren. 1542 $outer.data('prec-ext-category', ziel.category || ''); 1543 1544 _precLoadEvents($layout, 1, null, ziel.category || '', true); 1545 return true; 1546 }; 1547 1548 // Globale loadEvents Funktion (wird von Filtern und Pagination genutzt) 1549 var _precLoadEvents = function($wrap, page, perPage, category, scrollAfter) { 1550 if (typeof precFrontend === 'undefined') { 1551 console.error('[EK] precFrontend nicht definiert â AJAX nicht möglich'); 1552 return; 1553 } 1554 var layout = $wrap.attr('class').match(/prec-layout-([\w-]+)/); 1555 layout = layout ? layout[1] : 'grid'; 1556 1557 var $outer = $wrap.closest('.prec-shortcode-wrap'); 1558 1559 // Seit v6.9.7: Kategorie wird ausschlieÃlich aus dem aktiven Button gelesen. 1560 // Shortcode-Kategorie wird beim Initial-Render als active-Button gesetzt, ist 1561 // aber NICHT mehr hart gelockt â der User kann frei wechseln. 1562 // Das frühere data-prec-locked-category wird ignoriert (Backward-Compat: bleibt 1563 // im DOM, hat aber keine Wirkung mehr). 1564 var cat = category || ''; 1565 if (!cat) { 1566 var $activeBtn = $outer.find('.prec-filter-btn.active'); 1567 if ($activeBtn.length && $activeBtn.data('filter') !== '*') cat = $activeBtn.data('filter'); 1568 } 1569 if (!cat) cat = ($outer.find('[data-prec-category]').val() || $wrap.find('[data-prec-category]').val()) || ''; 1570 // Von einem Filter-Button gesetzte Kategorie (seit v10.2.0) - greift 1571 // nur, wenn weder Aufrufer noch Filterleiste eine Kategorie liefern. 1572 if (!cat) cat = ($outer.data('prec-ext-category') || '') + ''; 1573 1574 // ââ Filter-State-Fallback (seit v7.1.3, erweitert v7.1.6) âââââââââ 1575 // Drei Quellen werden in Reihenfolge geprüft: 1576 // (1) Filter-Bar im DOM (User-Interaktion, höchste Priorität) 1577 // (2) URL-Parameter (?s=â¦&prec_category=â¦) â von [prec_event_search] 1578 // (3) Shortcode-Attribute am Wrapper (data-prec-locked-*) â Shortcode-Defaults 1579 //
1580 // Wichtig: Nur wenn die Filter-Bar GAR NICHT im DOM ist, fällt JS auf 1581 // Shortcode-Defaults zurück. Wenn die Filter-Bar da ist, aber der User 1582 // alle Checkboxen abgehakt hat, soll "Alle" gelten â Shortcode-Default 1583 // greift dann NICHT, weil das User-Intent ist. 1584 function _precUrlParam(name) { 1585 try { 1586 var sp = new URLSearchParams(window.location.search); 1587 return sp.get(name) || ''; 1588 } catch (e) { return ''; } 1589 } 1590 1591 var search = ($outer.find('#prec-event-search').val() 1592 || $outer.find('.prec-search-input').val() 1593 || $outer.find('[data-prec-search]').val() 1594 || $wrap.find('[data-prec-search]').val() 1595 || _precUrlParam('s')) || ''; 1596 1597 // ââ LOCATION ââââââââââââââââââââââââââââââââââââââââââââââââ 1598 var $locChecks = $outer.find('.prec-loc-check'); 1599 var hasLocFilterBar = $locChecks.length > 0; 1600 var locationFromBar = (function(){ 1601 var slugs = []; 1602 $outer.find('.prec-loc-check:checked').each(function(){ slugs.push($(this).val()); }); 1603 return slugs.join(','); 1604 })(); 1605 1606 var location; 1607 if (hasLocFilterBar) { 1608 // User hat Hoheit â auch wenn alle abgehakt = "Alle Orte" 1609 location = locationFromBar; 1610 } else { 1611 // Keine Filter-Bar â Shortcode-Default oder URL 1612 location = ($outer.find('select[name="prec_location"]').val() 1613 || _precUrlParam('prec_location') 1614 || $outer.data('prec-locked-location') 1615 || $wrap.data('prec-locked-location') 1616 || '') + ''; 1617 } 1618 1619 // ââ KATEGORIE âââââââââââââââââââââââââââââââââââââââââââââââ 1620 var $catBtns = $outer.find('.prec-filter-btn'); 1621 if (!cat) { 1622 // Wenn Filter-Bar mit Buttons da ist und ein Button "*" (Alle) aktiv, 1623 // dann hat User explizit "Alle" gewählt â kein Shortcode-Fallback. 1624 // Wenn KEINE Filter-Bar im DOM â Shortcode-Default greift. 1625 if ($catBtns.length === 0) { 1626 cat = ($outer.find('select[name="prec_category"]').val() 1627 || _precUrlParam('prec_category') 1628 || $outer.data('prec-locked-category') 1629 || $wrap.data('prec-locked-category') 1630 || '') + ''; 1631 } else { 1632 // Filter-Bar vorhanden â entweder ein Button (auÃer "*") ist aktiv 1633 // (haben wir oben schon erfasst), oder "*" / nichts aktiv = "Alle" 1634 cat = ($outer.find('select[name="prec_category"]').val() 1635 || _precUrlParam('prec_category') 1636 || '') + ''; 1637 } 1638 } 1639 var limit = perPage || parseInt($outer.find('#prec-per-page').val(), 10) || parseInt($wrap.data('limit'), 10) || 20; 1640 var past = $wrap.data('past') || '0'; 1641 var $list = $wrap.find('.prec-events-grid, .prec-ic2-grid, .prec-cc-grid, .prec-ig-grid, .prec-cl-list, .prec-horizontal-list, .prec-masonry-grid, .prec-events-list, .prec-timeline, [data-prec-list]'); // [data-prec-list] seit v10.9.0: Editions-Layouts 1642 if (!$list.length) { 1643 // Grid-Container fehlt â erstellen mit korrektem Layout 1644 var containerMap = { 1645 'grid': 'prec-events-grid', 1646 'image-cards': 'prec-ic2-grid', 1647 'color-cards': 'prec-cc-grid', 1648 'image-grid': 'prec-ig-grid', 1649 'color-list': 'prec-cl-list', 1650 'list': 'prec-events-list', 1651 'horizontal': 'prec-horizontal-list', 1652 'masonry': 'prec-masonry-grid', 1653 'timeline': 'prec-timeline' 1654 }; 1655 var containerClass = containerMap[layout] || 'prec-events-grid'; 1656 $wrap.find('.prec-no-events, .prec-no-events-filtered').remove(); 1657 var $newList = $('<div>').addClass(containerClass); 1658 $wrap.prepend($newList); 1659 $list = $newList; 1660 } 1661 1662 // Datumsfilter: Von PREC.filter lesen 1663 var dateFrom = '', dateTo = ''; 1664 if (PREC.filter) { 1665 if (PREC.filter.dateFrom) { dateFrom = PREC.filter.dateFrom; } 1666 if (PREC.filter.dateTo) { dateTo = PREC.filter.dateTo; } 1667 if (!dateFrom && PREC.filter.activeDays > 0) { 1668 // precFrontend.today kommt vom Server (WordPress-Zeitzone) â kein UTC-Versatz 1669 var todayStr = (precFrontend && precFrontend.today) ? precFrontend.today : (function(){ 1670 var d = new Date(); 1671 return d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0'); 1672 })(); 1673 dateFrom = todayStr; 1674 if (PREC.filter.activeDays === 1) { 1675 dateTo = dateFrom; 1676 } else { 1677 var parts = todayStr.split('-'); 1678 var end = new Date(+parts[0], +parts[1]-1, +parts[2]); 1679 end.setDate(end.getDate() + PREC.filter.activeDays - 1); 1680 dateTo = end.getFullYear() + '-' + String(end.getMonth()+1).padStart(2,'0') + '-' + String(end.getDate()).padStart(2,'0'); 1681 } 1682 } 1683 } 1684 1685 // Date-Fallback aus URL (seit v7.1.3): wenn von [prec_event_search] 1686 // kommend, kann prec_date=2026-06-17 in der URL stehen. 1687 if (!dateFrom) { 1688 var urlDate = _precUrlParam('prec_date'); 1689 if (urlDate && /^\d{4}-\d{2}-\d{2}$/.test(urlDate)) { 1690 dateFrom = urlDate; 1691 dateTo = urlDate; 1692 } 1693 } 1694 1695 $wrap.addClass('prec-loading'); 1696 1697 // Skeleton-Platzhalter während des Ladens 1698 var skelCount = Math.min(limit, 6); 1699 var skelHtml = ''; 1700 for (var s = 0; s < skelCount; s++) skelHtml += '<div class="prec-skeleton"></div>'; 1701 $list.html(skelHtml); 1702 1703 // ââ Shortcode-Defaults (seit v7.1.7) ââââââââââââââââââââââââââââ 1704 // Alle weiteren Shortcode-Filter (auÃer location/category, die schon 1705 // oben behandelt sind) werden vom Wrapper gelesen und im AJAX-POST 1706 // mitgeschickt. Diese können vom User nicht über die Filter-Bar 1707 // verändert werden, sind also immer "locked" â bei jedem Pagination-, 1708 // Layout- und Kategorie-Reset-Klick werden sie respektiert. 1709 var lockedVenue = ($outer.data('prec-locked-venue') || '') + ''; 1710 var lockedTag = ($outer.data('prec-locked-tag') || '') + ''; 1711 var lockedOrganizer = ($outer.data('prec-locked-organizer') || '') + ''; 1712 var lockedEventType = ($outer.data('prec-locked-event-type') || '') + ''; 1713 var lockedFeatured = ($outer.data('prec-locked-featured') || '') + ''; 1714 var lockedOrderby = ($outer.data('prec-locked-orderby') || '') + ''; 1715 var lockedOrder = ($outer.data('prec-locked-order') || '') + ''; 1716 var lockedIds = ($outer.data('prec-locked-ids') || '') + ''; 1717 // seit v7.1.8: drei weitere persistente Shortcode-Defaults 1718 var lockedShowOffline = ($outer.data('prec-locked-show-offline') || '') + ''; 1719 var lockedGroupMultiday = ($outer.data('prec-locked-group-multiday') || '') + ''; 1720 var lockedSliderEffect = ($outer.data('prec-locked-slider-effect') || '') + ''; 1721 // Seit v9.8.3: Datumsbereich, Ausschluss und (fixe) Suche als Shortcode- 1722 // Defaults ebenfalls persistent â überleben Blättern/Filtern/Layoutwechsel. 1723 var lockedDateFrom = ($outer.data('prec-locked-date-from') || '') + ''; 1724 var lockedDateTo = ($outer.data('prec-locked-date-to') || '') + ''; 1725 var lockedExclude = ($outer.data('prec-locked-exclude') || '') + ''; 1726 var lockedSearch = ($outer.data('prec-locked-search') || '') + ''; 1727 // Seit v10.2.0 (Filter-Buttons): Stadt-Filter und Status-Vorgabe. 1728 var lockedCity = ($outer.data('prec-locked-city') || '') + ''; 1729 var lockedPast = ($outer.data('prec-locked-past') || '') + ''; 1730 // Seit v10.9.0: Ortsteil + âNur Non-Profit" â Filterleiste hat Vorrang, 1731 // ohne Bedienelement gilt die Vorgabe aus dem Kurzbefehl (district="â¦", nonprofit="1"). 1732 var $otSel = $outer.find('.prec-district-select').first(); 1733 var district = $otSel.length ? ($otSel.val() || '') : (($outer.data('prec-locked-district') || '') + ''); 1734 var $npChk = $outer.find('.prec-np-check').first(); 1735 var nonprofit = $npChk.length ? ($npChk.is(':checked') ? '1' : '') : (($outer.data('prec-locked-nonprofit') || '') + ''); 1736 1737 $.post(precFrontend.ajaxUrl, { 1738 action: 'prec_load_events', 1739 nonce: precFrontend.nonce, 1740 page: page, 1741 limit: limit, 1742 layout: layout, 1743 category: cat, 1744 location: location, 1745 search: search || lockedSearch, 1746 date_from: dateFrom || lockedDateFrom, 1747 date_to: dateTo || lockedDateTo, 1748 past: lockedPast !== '' ? lockedPast : past, 1749 city: lockedCity, 1750 district: district, 1751 nonprofit: nonprofit, 1752 pagination: (($outer.data('prec-locked-pagination') + '') === '0') ? '0' : '', // seit v10.9.0 1753 venue: lockedVenue, 1754 tag: lockedTag, 1755 organizer: lockedOrganizer, 1756 event_type: lockedEventType, 1757 featured: lockedFeatured, 1758 orderby: lockedOrderby, 1759 order: lockedOrder, 1760 ids: lockedIds, 1761 show_offline: lockedShowOffline, 1762 group_multiday: lockedGroupMultiday, 1763 slider_effect: lockedSliderEffect, 1764 exclude: lockedExclude 1765 }, function (res) { 1766 $wrap.removeClass('prec-loading'); 1767 if (res.success && res.data) { 1768 1769 // Slider: vollständiges Template (inkl. Wrapper + Inline-JS) ersetzen 1770 if (layout === 'slider') { 1771 var html = res.data.html || ''; 1772 // Wrapper finden und ersetzen 1773 var $sliderWrap = $wrap.find('.prec-slider-wrap'); 1774 if ($sliderWrap.length) { 1775 $sliderWrap.replaceWith(html); 1776 } else { 1777 $list.html(html); 1778 } 1779 // Inline-Scripts ausführen (jQuery .html() ignoriert sie) 1780 var $tmp = $('<div>').append($.parseHTML(html, document, true)); 1781 $tmp.find('script').each(function () { 1782 try { $.globalEval(this.text || this.textContent); } catch (e) {} 1783 }); 1784 if (res.data.pagination) $wrap.find('.prec-pagination').replaceWith(res.data.pagination); 1785 else $wrap.find('.prec-pagination').empty(); 1786 if (scrollAfter) $('html,body').animate({ scrollTop: $wrap.offset().top - 80 }, 300); 1787 return; 1788 } 1789 1790 // IMMER ersetzen - sonst blieben bei leerer Antwort die 1791 // Skeleton-Platzhalter dauerhaft stehen (seit v10.2.4). 1792 $list.html(res.data.html || ('<p class="prec-no-events">' 1793 + ((precFrontend.i18n && precFrontend.i18n.noEvents) || 'Keine Veranstaltungen gefunden.') 1794 + '</p>')); 1795 if (res.data.pagination) $wrap.find('.prec-pagination').replaceWith(res.data.pagination); 1796 else $wrap.find('.prec-pagination').empty(); 1797 1798 // Spalten nach AJAX wiederherstellen â liest immer direkt vom Picker 1799 var colLayouts = ['grid','color-cards','color-list','image-cards','image-grid','masonry']; 1800 if (colLayouts.indexOf(layout) !== -1) { 1801 var vw = window.innerWidth || document.documentElement.clientWidth; 1802 var $picker = $outer.find('#prec-cols-picker'); 1803 var cols = $picker.length ? (parseInt($picker.val()) || 3) : 3; 1804 1805 // Responsive Deckel (verhindert zu viele Spalten auf kleinen Screens) 1806 if (vw <= 480) cols = Math.min(cols, 1); 1807 else if (vw <= 768) cols = Math.min(cols, 2); 1808 1809 var gridVal = 'repeat(' + cols + ',1fr)'; 1810 $list.css({ 1811 'display': 'grid', 1812 'grid-template-columns': gridVal, 1813 'gap': '24px', 1814 'width': '100%' 1815 }); 1816 // CSS-Variable ebenfalls aktualisieren 1817 $wrap[0].style.setProperty('--prec-grid-cols', String(cols)); 1818 if ($list[0]) $list[0].style.setProperty('--prec-grid-cols', String(cols)); 1819 } 1820 1821 if (scrollAfter) $('html,body').animate({ scrollTop: $wrap.offset().top - 80 }, 300); 1822 } 1823 }).fail(function () { 1824 $wrap.removeClass('prec-loading'); 1825 console.error('[EK] AJAX load_events fehlgeschlagen'); 1826 }); 1827 }; 1828 1829 function initAjaxPagination() { 1830 // Klick auf Seitenzahl 1831 $(document).on('click', '.prec-page-btn', function (e) { 1832 e.preventDefault(); 1833 var $btn = $(this); 1834 var page = parseInt($btn.data('page'), 10); 1835 var $wrap = $btn.closest('[class*="prec-layout-"]'); 1836 if (!$wrap.length || !page) return; 1837 _precLoadEvents($wrap, page, null, null, true); 1838 }); 1839 1840 // Anzahl pro Seite geändert â Seite 1 neu laden 1841 $(document).on('change', '#prec-per-page', function () { 1842 var $outer = $(this).closest('.prec-shortcode-wrap'); 1843 var $wrap = $outer.find('div[class*="prec-layout-"]').first(); 1844 if (!$wrap.length) return; 1845 _precLoadEvents($wrap, 1, parseInt($(this).val(), 10), null, true); 1846 }); 1847 1848 // Spalten-Picker: setzt grid-template-columns direkt + CSS-Variable 1849 $(document).on('change', '#prec-cols-picker', function () { 1850 var cols = parseInt($(this).val(), 10); 1851 var $outer = $(this).closest('.prec-shortcode-wrap'); 1852 var $wrap = $outer.find('div[class*="prec-layout-"]').first(); 1853 if (!$wrap.length) return; 1854 var gridVal = 'repeat(' + cols + ', 1fr)'; 1855 // CSS-Variable auf Wrapper setzen 1856 $wrap[0].style.setProperty('--prec-grid-cols', String(cols)); 1857 // Grid-Template direkt auf alle Grid-Container setzen (zuverlässiger) 1858 $wrap.find('.prec-cc-grid,.prec-ic2-grid,.prec-ig-grid,.prec-events-grid').each(function(){ 1859 this.style.setProperty('--prec-grid-cols', String(cols)); 1860 this.style.gridTemplateColumns = gridVal; 1861 }); 1862 // Color-list (CSS-Grid) 1863 $wrap.find('.prec-cl-list').each(function(){ 1864 this.style.setProperty('--prec-grid-cols', String(cols)); 1865 this.style.gridTemplateColumns = gridVal; 1866 }); 1867 // Masonry (columns CSS property) 1868 $wrap.find('.prec-masonry-grid').each(function(){ 1869 this.style.columns = String(cols); 1870 }); 1871 }); 1872 1873 // Spalten-Picker bei Layout-Wechsel aktualisieren 1874 $(document).on('change', '#prec-layout-switcher', function () { 1875 var layout = $(this).val(); 1876 var colLayouts = ['grid','color-cards','image-cards','image-grid','color-list','masonry'];
1877 var $picker = $(this).closest('.prec-filter-controls').find('#prec-cols-picker'); 1878 if (colLayouts.indexOf(layout) === -1) { 1879 $picker.closest('label').hide(); 1880 } else { 1881 $picker.closest('label').show(); 1882 $picker.attr('data-layout', layout); 1883 1884 // Spalten aus Responsive-Einstellungen laden 1885 var vw = window.innerWidth || document.documentElement.clientWidth; 1886 var isPortrait2 = window.matchMedia ? window.matchMedia('(orientation: portrait)').matches : (window.innerHeight > window.innerWidth); 1887 var dev; 1888 if (vw <= 480) dev = 'mobile_port'; 1889 else if (vw <= 600) dev = isPortrait2 ? 'mobile_port' : 'mobile_land'; 1890 else if (vw <= 900) dev = isPortrait2 ? 'tablet_port' : 'mobile_land'; 1891 else if (vw <= 1024) dev = isPortrait2 ? 'tablet_port' : 'tablet_land'; 1892 else dev = 'desktop'; 1893 var resp = (typeof precFrontend !== 'undefined' && precFrontend.responsive) ? precFrontend.responsive : null; 1894 if (resp && resp[dev] && resp[dev].layoutCols && resp[dev].layoutCols[layout]) { 1895 var cols = parseInt(resp[dev].layoutCols[layout]); 1896 if (vw <= 480) cols = Math.min(cols, 1); 1897 if (vw <= 1023) cols = Math.min(cols, 2); 1898 $picker.val(String(cols)); 1899 } 1900 } 1901 }); 1902 } 1903 1904 // ========================================================= 1905 // LIVE FILTER & SEARCH 1906 // ========================================================= 1907 function initLiveFilter() { 1908 var filterTimer; 1909 1910 $(document).on('change', '[data-prec-category]', function () { 1911 triggerFilter($(this).closest('[class*="prec-layout-"]')); 1912 }); 1913 1914 $(document).on('input', '[data-prec-search]', function () { 1915 clearTimeout(filterTimer); 1916 var $wrap = $(this).closest('[class*="prec-layout-"]'); 1917 filterTimer = setTimeout(function () { triggerFilter($wrap); }, 400); 1918 }); 1919 1920 function triggerFilter($wrap) { 1921 if (!$wrap.length) return; 1922 var layout = ($wrap.attr('class').match(/prec-layout-([\w-]+)/) || [])[1] || 'grid'; 1923 var category = $wrap.find('[data-prec-category]').val() || ''; 1924 var search = $wrap.find('[data-prec-search]').val() || ''; 1925 var $list = $wrap.find('.prec-events-grid, .prec-ic2-grid, .prec-cc-grid, .prec-ig-grid, .prec-cl-list, .prec-horizontal-list, .prec-masonry-grid, .prec-events-list, .prec-timeline, [data-prec-list]'); // [data-prec-list] seit v10.9.0: Editions-Layouts 1926 if (!$list.length) $list = $wrap; 1927 1928 $wrap.addClass('prec-loading'); 1929 1930 $.post(precFrontend.ajaxUrl, { 1931 action: 'prec_load_events', 1932 nonce: precFrontend.nonce, 1933 page: 1, 1934 layout: layout, 1935 category: category, 1936 search: search 1937 }, function (res) { 1938 $wrap.removeClass('prec-loading'); 1939 if (res.success && res.data) { 1940 if (res.data.html) $list.html(res.data.html); 1941 if (res.data.pagination) $wrap.find('.prec-pagination').replaceWith(res.data.pagination); 1942 } 1943 }); 1944 } 1945 } 1946 1947 // ========================================================= 1948 // MASONRY REFLOW (nach Bild-Laden) 1949 // ========================================================= 1950 function initMasonry() { 1951 var $grid = $('[data-prec-masonry]'); 1952 if (!$grid.length) return; 1953 $grid.find('img').on('load', function () { 1954 // CSS columns reflow via display trick 1955 $grid.css('display','none').height(); // force reflow 1956 $grid.css('display',''); 1957 }); 1958 } 1959 1960 // ========================================================= 1961 // SHARE BUTTON â Link kopieren (Instagram) 1962 // ========================================================= 1963 function initCopyShare() { 1964 $(document).on('click', '.prec-share-copy', function (e) { 1965 e.preventDefault(); 1966 var url = $(this).data('url'); 1967 if (navigator.clipboard && url) { 1968 navigator.clipboard.writeText(url).then(function () { 1969 showToast(precFrontend.i18n && precFrontend.i18n.copied ? precFrontend.i18n.copied
1969: 'Link kopiert!'); 1970 }); 1971 } else if (url) { 1972 // Fallback 1973 var $t = $('<input>').val(url).appendTo('body').select(); 1974 document.execCommand('copy'); 1975 $t.remove(); 1976 showToast('Link kopiert!'); 1977 } 1978 }); 1979 } 1980 1981 // ========================================================= 1982 // TOAST NOTIFICATION 1983 // ========================================================= 1984 function showToast(message, type) { 1985 // Vorsorglich maskieren: Heute uebergeben alle Aufrufer feste Texte. 1986 // Kaeme spaeter ein dynamischer Wert dazu, waere die Luecke sonst 1987 // sofort da, ohne dass es jemand bemerkt. 1988 message = precText(message); 1989 var $toast = $('<div class="prec-toast prec-toast-' + (type || 'success') + '">' + message + '</div>'); 1990 $('body').append($toast); 1991 setTimeout(function () { $toast.addClass('prec-toast--visible'); }, 10); 1992 setTimeout(function () { 1993 $toast.removeClass('prec-toast--visible'); 1994 setTimeout(function () { $toast.remove(); }, 300); 1995 }, 3000); 1996 } 1997 1998 // ========================================================= 1999 // SCROLL-TO-TOP nach Formular-Submit 2000 // ========================================================= 2001 function initFormFeedback() { 2002 $(document).on('submit', '.prec-reg-form', function (e) { 2003 e.preventDefault(); 2004 var $form = $(this); 2005 var $btn = $form.find('[type=submit]'); 2006 var $msg = $form.find('.prec-form-message'); 2007 2008 $btn.prop('disabled', true).text($btn.data('loading') || 'Wird gesendetâ¦'); 2009 $msg.text('').removeClass('prec-msg-success prec-msg-error'); 2010 2011 $.post(precFrontend.ajaxUrl, $form.serialize(), function (res) { 2012 $btn.prop('disabled', false).text($btn.data('original') || $btn.attr('data-original') || 'Jetzt anmelden'); 2013 if (res.success) { 2014 $msg.addClass('prec-msg-success').text(res.data.message || 'Anmeldung erfolgreich!'); 2015 $form.find('input:not([type=hidden]), textarea, select').val(''); 2016 $('html,body').animate({ scrollTop: $msg.offset().top - 60 }, 400); 2017 } else { 2018 $msg.addClass('prec-msg-error').text((res.data && res.data.message) || 'Fehler. Bitte versuche es erneut.'); 2019 } 2020 }).fail(function () { 2021 $btn.prop('disabled', false); 2022 $msg.addClass('prec-msg-error').text('Verbindungsfehler. Bitte erneut versuchen.'); 2023 }); 2024 }); 2025 2026 // Store original button text 2027 $(document).on('mouseenter', '.prec-reg-form [type=submit]', function () { 2028 if (!$(this).attr('data-original')) $(this).attr('data-original', $(this).text()); 2029 }); 2030 } 2031 2032 // Init all 2033 initAjaxPagination(); 2034 initLiveFilter(); 2035 initMasonry(); 2036 initCopyShare(); 2037 2038 // ââ Spalten-Picker + Layout-Switcher: Responsive-Einstellungen anwenden ââ 2039 (function applyInitialCols() { 2040 var vw = window.innerWidth || document.documentElement.clientWidth; 2041 var resp = (typeof precFrontend !== 'undefined' && precFrontend.responsive) ? precFrontend.responsive : null; 2042 // Gerät + Orientierung bestimmen 2043 var isPortrait = window.matchMedia ? window.matchMedia('(orientation: portrait)').matches 2044 : (window.innerHeight > window.innerWidth); 2045 var dev; 2046 if (vw <= 480) dev = 'mobile_port'; 2047 else if (vw <= 600) dev = isPortrait ? 'mobile_port' : 'mobile_land'; 2048 else if (vw <= 900) dev = isPortrait ? 'tablet_port' : 'mobile_land'; 2049 else if (vw <= 1024) dev = isPortrait ? 'tablet_port' : 'tablet_land'; 2050 else dev = 'desktop'; 2051 var devCfg = resp ? resp[dev] : null; 2052 2053 // Layout-Switcher: nur erlaubte Layouts je Gerät anzeigen 2054 if (devCfg && devCfg.layouts && devCfg.layouts.length) { 2055 $('#prec-layout-switcher option').each(function(){ 2056 if (devCfg.layouts.indexOf($(this).val()) === -1) $(this).remove(); 2057 }); 2058 } 2059 2060 // Spalten für alle Shortcode-Wraps setzen 2061 $('.prec-shortcode-wrap').each(function () { 2062 var $outer = $(this); 2063 var $picker = $outer.find('#prec-cols-picker'); 2064 if (!$picker.length) return; 2065 2066 // Aktuelles Layout ermitteln 2067 var $layoutWrap = $outer.find('div[class*="prec-layout-"]').first(); 2068 var currentLayout = 'grid'; 2069 if ($layoutWrap.length) { 2070 var cls = $layoutWrap.attr('class') || ''; 2071 var m = cls.match(/prec-layout-([\w-]+)/); 2072 if (m) currentLayout = m[1]; 2073 } 2074 2075 // Spalten aus layoutCols holen, Fallback auf defaultCols 2076 var layoutCols = devCfg && devCfg.layoutCols ? devCfg.layoutCols : null; 2077 var cols = layoutCols && layoutCols[currentLayout] 2078 ? parseInt(layoutCols[currentLayout])
2079 : (devCfg ? (parseInt(devCfg.defaultCols) || 1) : (vw <= 480 ? 1 : vw <= 1023 ? 2 : 3)); 2080 2081 // Harte Limits 2082 if (vw <= 480) cols = Math.min(cols, 1); 2083 else if (vw <= 1023) cols = Math.min(cols, 2); 2084 2085 $picker.val(String(cols)); 2086 2087 if ($layoutWrap.length) { 2088 var gridVal = 'repeat(' + cols + ', 1fr)'; 2089 $layoutWrap[0].style.setProperty('--prec-grid-cols', String(cols)); 2090 $layoutWrap.find('.prec-cc-grid,.prec-ic2-grid,.prec-ig-grid,.prec-cl-list,.prec-events-grid').each(function () { 2091 this.style.setProperty('--prec-grid-cols', String(cols)); 2092 this.style.gridTemplateColumns = gridVal; 2093 }); 2094 } 2095 }); 2096 2097 // Resize â neu berechnen 2098 if (!applyInitialCols._bound) { 2099 applyInitialCols._bound = true; 2100 var rt; 2101 window.addEventListener('resize', function () { 2102 clearTimeout(rt); 2103 rt = setTimeout(applyInitialCols, 250); 2104 }); 2105 } 2106 })(); 2107 // Lightbox wird über PREC.gallery.init() gestartet 2108 initFormFeedback(); 2109 2110 // Preis-Text aufklappen/zuklappen 2111 $(document).on('click', '.prec-price-toggle', function(e) { 2112 e.preventDefault(); 2113 e.stopPropagation(); 2114 var $btn = $(this); 2115 var $price = $btn.siblings('.prec-price-truncated'); 2116 var expanded = $price.hasClass('prec-price-expanded'); 2117 $price.toggleClass('prec-price-expanded'); 2118 $btn.text(expanded ? 'ï¼ Mehr' : 'â Weniger'); 2119 $btn.attr('aria-expanded', !expanded); 2120 }); 2121 2122})(jQuery); 2123 2124/* ========================================================= 2125 LIGHTBOX + TOAST CSS (inline für Zero-Dependency) 2126 ========================================================= */ 2127(function () { 2128 var css = ` 2129 /* ââ Lightbox Container ââ */ 2130 .prec-lb { 2131 position:fixed; inset:0; z-index:100000; 2132 display:flex; align-items:center; justify-content:center; 2133 opacity:0; visibility:hidden; 2134 /* pointer-events:none ist entscheidend: Ohne dies blieb die 2135 geschlossene (unsichtbare) Lightbox als Klickfalle ueber der 2136 ganzen Seite liegen - Links darunter waren tot (bis v10.2.6). */ 2137 pointer-events:none !important; 2138 transition: opacity .35s ease, visibility .35s ease; 2139 } 2140 .prec-lb.prec-lb-visible { 2141 opacity:1; visibility:visible; 2142 pointer-events:auto !important; 2143 } 2144 2145 /* ââ Backdrop: Blur + dunkle Transparenz ââ */ 2146 .prec-lb-backdrop { 2147 position:absolute; inset:0; 2148 background: rgba(10,10,10,.75); 2149 backdrop-filter: blur(18px) saturate(120%); 2150 -webkit-backdrop-filter: blur(18px) saturate(120%); 2151 cursor:pointer; 2152 } 2153 2154 /* ââ Bühne (Bild-Container) ââ */ 2155 .prec-lb-stage { 2156 position:relative; z-index:2; 2157 display:flex; align-items:center; justify-content:center; 2158 max-width:92vw; max-height:85vh; 2159 } 2160 2161 /* ââ Bild ââ */ 2162 .prec-lb-img { 2163 max-width:92vw; max-height:85vh; 2164 border-radius:6px; 2165 box-shadow: 0 25px 80px rgba(0,0,0,.6), 0 0 0 1px rgba(255,255,255,.08); 2166 object-fit:contain; 2167 transition: opacity .25s ease, transform .25s ease; 2168 user-select:none; -webkit-user-drag:none; 2169 } 2170 .prec-lb-img-fade { 2171 opacity:0; transform:scale(.97); 2172 } 2173 2174 /* ââ Spinner ââ */ 2175 .prec-lb-spinner { 2176 position:absolute; width:36px; height:36px; 2177 border:3px solid rgba(255,255,255,.15); 2178 border-top-color:rgba(255,255,255,.8); 2179 border-radius:50%; 2180 animation: precSpin .7s linear infinite; 2181 opacity:0; transition:opacity .2s; 2182 } 2183 .prec-lb-loading .prec-lb-spinner { opacity:1; } 2184 @keyframes precSpin { to { transform:rotate(360deg); } } 2185 2186 /* ââ Close-Button ââ */ 2187 /* !important + feste SVG-Strichfarbe: Themes ueberschreiben gern 2188 button-Hintergrund und -Farbe (z.B. CD-Rot) - dann war das weisse 2189 Schliessen-X unsichtbar (rot auf rot). Seit v10.2.7 theme-fest. */ 2190 .prec-lb-close { 2191 position:absolute; top:16px; right:20px; z-index:10; 2192 width:44px !important; height:44px !important; 2193 min-width:0 !important; min-height:0 !important; 2194 padding:0 !important; margin:0 !important; 2195 display:flex !important; align-items:center; justify-content:center; 2196 background:rgba(0,0,0,.45) !important; 2197 backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px); 2198 border:1px solid rgba(255,255,255,.35) !important; 2199 border-radius:50% !important; color:#fff !important; cursor:pointer; 2200 box-shadow:none !important; text-indent:0 !important; 2201 transition: background .2s, transform .15s; 2202 -webkit-appearance:none; appearance:none; 2203 } 2204 .prec-lb-close:hover { 2205 background:rgba(0,0,0,.65) !important; transform:scale(1.08); 2206 } 2207 .prec-lb-close svg, .prec-lb-nav svg { 2208 display:block !important; width:24px; height:24px; 2209 stroke:#fff !important; fill:none !important; opacity:1 !important; 2210 } 2211 .prec-lb-nav svg { width:28px; height:28px; } 2212 2213 /* ââ Nav-Buttons ââ */ 2214 .prec-lb-nav { 2215 position:absolute; top:50%; z-index:10; 2216 transform:translateY(-50%); 2217 width:48px; height:48px; 2218 display:flex; align-items:center; justify-content:center; 2219 background:rgba(255,255,255,.08); 2220 backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px); 2221 border:1px solid rgba(255,255,255,.12); 2222 border-radius:50%; color:#fff; cursor:pointer; 2223 transition: background .2s, transform .15s, opacity .2s; 2224 opacity:.7; 2225 } 2226 .prec-lb-nav:hover { background:rgba(255,255,255,.18); opacity:1; transform:translateY(-50%) scale(1.08); }
2227 .prec-lb-prev { left:20px; } 2228 .prec-lb-next { right:20px; } 2229 2230 /* ââ Footer / Counter ââ */ 2231 .prec-lb-footer { 2232 position:absolute; bottom:20px; left:50%; transform:translateX(-50%); 2233 z-index:10; display:flex; align-items:center; gap:10px; max-width:92vw; 2234 } 2235 /* Bildautor in der Lightbox (seit v10.3.1) */ 2236 .prec-lb-caption:empty { display:none; } 2237 .prec-lb-caption { 2238 display:inline-block; padding:6px 14px; 2239 background:rgba(0,0,0,.45); border:1px solid rgba(255,255,255,.12); 2240 border-radius:20px; color:rgba(255,255,255,.9); 2241 font-size:12px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; 2242 } 2243 .prec-lb-counter { 2244 display:inline-block; 2245 padding:6px 16px; 2246 background:rgba(255,255,255,.1); 2247 backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px); 2248 border:1px solid rgba(255,255,255,.12); 2249 border-radius:20px; 2250 color:rgba(255,255,255,.85); 2251 font-size:13px; font-weight:500; letter-spacing:.5px; 2252 user-select:none; 2253 } 2254 2255 /* ââ Responsive ââ */ 2256 @media (max-width:768px) { 2257 .prec-lb-nav { width:40px; height:40px; } 2258 .prec-lb-prev { left:10px; } 2259 .prec-lb-next { right:10px; } 2260 .prec-lb-close { top:10px; right:12px; width:38px; height:38px; } 2261 .prec-lb-img { max-width:96vw; max-height:80vh; border-radius:4px; } 2262 } 2263 2264 /* ââ Toast (unverändert) ââ */ 2265 .prec-toast { 2266 position:fixed; bottom:24px; left:50%; transform:translateX(-50%) translateY(20px); 2267 background:#1a1a1a; color:#fff; padding:12px 24px; border-radius:8px; 2268 font-size:14px; font-weight:500; z-index:99998; opacity:0; 2269 transition:opacity .3s, transform .3s; pointer-events:none; white-space:nowrap; 2270 } 2271 .prec-toast--visible { opacity:1; transform:translateX(-50%) translateY(0); } 2272 .prec-toast-error { background:#d63638; } 2273 .prec-toast-success { background:#00a32a; } 2274 .prec-loading { opacity:.5; pointer-events:none; transition:opacity .2s; } 2275 .prec-msg-success { color:#00a32a; font-weight:600; margin-top:10px; display:block; } 2276 .prec-msg-error { color:#d63638; font-weight:600; margin-top:10px; display:block; } 2277 `; 2278 var style = document.createElement('style'); 2279 style.textContent = css; 2280 document.head.appendChild(style); 2281})(); 2282 2283/* ââ Ganze Event-Karte anklickbar (alle Layouts) ââ */ 2284(function(){ 2285 var selectors = [ 2286 '.prec-event-card[data-href]', 2287 '.prec-hcard[data-href]', 2288 '.prec-mcard[data-href]', 2289 '.prec-timeline-event[data-href]', 2290 '.prec-sb-item[data-href]' 2291 ].join(','); 2292 2293 document.addEventListener('click', function(e) { 2294 // Nicht wenn auf echten Link/Button geklickt 2295 if (e.target.closest('a, button, input, select, textarea')) return; 2296 var card = e.target.closest(selectors); 2297 if (!card) return; 2298 window.location.href = card.dataset.href; 2299 }); 2300})(); 2301 2302/* ââ Date-Picker: Klick überall im Feld öffnet Kalender ââ */ 2303(function(){ 2304 document.addEventListener('click', function(e) { 2305 var input = e.target.closest('input[type="date"]'); 2306 if (!input) return; 2307 if (typeof input.showPicker === 'function') { 2308 try { input.showPicker(); } catch(ex) {} 2309 } 2310 }); 2311})(); 2312 2313/* ===================================================================== 2314 PREC EVENT-SLIDER (seit v5.22.15) 2315 Initialisiert alle .prec-slider-Elemente auf der Seite mit: 2316 - Prev/Next-Navigation 2317 - Dots-Indikator (klickbar) 2318 - Optional Autoplay (data-autoplay="1", data-interval="5000") 2319 - Touch-Swipe-Support für Mobile 2320 - Responsive: 3/2/1 Cards je nach Viewport 2321 ===================================================================== */ 2322(function () { 2323 function visibleCount() { 2324 var w = window.innerWidth || document.documentElement.clientWidth; 2325 if (w <= 680) return 1; 2326 if (w <= 1024) return 2; 2327 return 3; 2328 } 2329 2330 function initSlider(root) { 2331 if (root.dataset.precSliderInit === '1') return; // Idempotenz 2332 root.dataset.precSliderInit = '1'; 2333 2334 var track = root.querySelector('.prec-slider-track'); 2335 var prev = root.querySelector('.prec-slider-prev'); 2336 var next = root.querySelector('.prec-slider-next'); 2337 var dots = root.querySelector('.prec-slider-dots'); 2338 if (!track) return; 2339 2340 var slides = Array.prototype.slice.call(track.children); 2341 var total = slides.length; 2342 if (total === 0) return; 2343 2344 // Container-Position absichern (für absolute Pfeil-Buttons) 2345 if (getComputedStyle(root).position === 'static') { 2346 root.style.position = 'relative'; 2347 } 2348 2349 // Card-Breiten als Inline-Style setzen â robust gegen 2350 // konkurrierende CSS-Regeln und Cache-Probleme. (seit v5.22.16) 2351 function applyCardWidths() { 2352 var pv = visibleCount(); 2353 var gap = pv === 1 ? 16 : 20; 2354 // Track-Gap synchronisieren 2355 track.style.gap = gap + 'px'; 2356 track.style.display = 'flex'; 2357 track.style.flexWrap = 'nowrap'; 2358 track.style.alignItems = 'stretch';
2359 // Card-Breite berechnen: ((100% - (n-1)*gap) / n) 2360 for (var i = 0; i < slides.length; i++) { 2361 var s = slides[i]; 2362 s.style.flex = '0 0 calc((100% - ' + ((pv - 1) * gap) + 'px) / ' + pv + ')'; 2363 s.style.maxWidth = 'calc((100% - ' + ((pv - 1) * gap) + 'px) / ' + pv + ')'; 2364 s.style.minWidth = '0'; 2365 s.style.boxSizing = 'border-box'; 2366 } 2367 } 2368 applyCardWidths(); 2369 2370 var index = 0; 2371 var perView = visibleCount(); 2372 var maxIndex = Math.max(0, total - perView); 2373 2374 function goTo(i, animate) { 2375 index = Math.max(0, Math.min(i, maxIndex)); 2376 if (!slides[index]) return; 2377 // Position des aktuellen Slides relativ zum Track-Anfang 2378 var offset = slides[index].offsetLeft; 2379 track.style.transition = animate === false ? 'none' : ''; 2380 track.style.transform = 'translateX(-' + offset + 'px)'; 2381 updateDots(); 2382 updateButtons(); 2383 } 2384 2385 function updateButtons() { 2386 if (prev) prev.disabled = index <= 0; 2387 if (next) next.disabled = index >= maxIndex; 2388 } 2389 2390 function buildDots() { 2391 if (!dots) return; 2392 dots.innerHTML = ''; 2393 var pages = maxIndex + 1; 2394 for (var i = 0; i < pages; i++) { 2395 var b = document.createElement('button'); 2396 b.type = 'button'; 2397 b.dataset.idx = i; 2398 b.setAttribute('aria-label', 'Zu Slide ' + (i + 1)); 2399 if (i === 0) b.className = 'is-active'; 2400 b.addEventListener('click', function () { 2401 goTo(parseInt(this.dataset.idx, 10), true); 2402 resetAutoplay(); 2403 }); 2404 dots.appendChild(b); 2405 } 2406 } 2407 2408 function updateDots() { 2409 if (!dots) return; 2410 var children = dots.children; 2411 for (var i = 0; i < children.length; i++) { 2412 children[i].classList.toggle('is-active', i === index); 2413 } 2414 } 2415 2416 // Autoplay 2417 var autoplay = root.dataset.autoplay === '1'; 2418 var interval = parseInt(root.dataset.interval, 10) || 5000; 2419 var timer = null; 2420 function startAutoplay() { 2421 if (!autoplay || maxIndex === 0) return; 2422 stopAutoplay(); 2423 timer = setInterval(function () { 2424 goTo(index >= maxIndex ? 0 : index + 1, true); 2425 }, interval); 2426 } 2427 function stopAutoplay() { 2428 if (timer) { clearInterval(timer); timer = null; } 2429 } 2430 function resetAutoplay() { stopAutoplay(); startAutoplay(); } 2431 2432 // Pfeil-Buttons 2433 if (prev) prev.addEventListener('click', function () { 2434 goTo(index - 1, true); resetAutoplay(); 2435 }); 2436 if (next) next.addEventListener('click', function () { 2437 goTo(index + 1, true); resetAutoplay(); 2438 }); 2439 2440 // Touch-Swipe für Mobile 2441 var touchStartX = 0; 2442 var touchEndX = 0; 2443 track.addEventListener('touchstart', function (e) { 2444 touchStartX = e.changedTouches[0].screenX; 2445 stopAutoplay(); 2446 }, { passive: true }); 2447 track.addEventListener('touchend', function (e) { 2448 touchEndX = e.changedTouches[0].screenX; 2449 var diff = touchEndX - touchStartX; 2450 if (Math.abs(diff) > 40) { 2451 goTo(index + (diff < 0 ? 1 : -1), true); 2452 } 2453 startAutoplay(); 2454 }, { passive: true }); 2455 2456 // Hover stoppt Autoplay (Desktop) 2457 root.addEventListener('mouseenter', stopAutoplay); 2458 root.addEventListener('mouseleave', startAutoplay); 2459 2460 // Resize: perView neu berechnen + Card-Breiten neu setzen 2461 var resizeTimeout = null; 2462 window.addEventListener('resize', function () { 2463 clearTimeout(resizeTimeout); 2464 resizeTimeout = setTimeout(function () { 2465 var newPerView = visibleCount(); 2466 applyCardWidths(); // Inline-Breiten neu berechnen 2467 if (newPerView !== perView) { 2468 perView = newPerView; 2469 maxIndex = Math.max(0, total - perView); 2470 buildDots(); 2471 if (index > maxIndex) index = maxIndex; 2472 goTo(index, false); 2473 } else { 2474 goTo(index, false); // bei reiner GröÃenänderung Position justieren 2475 } 2476 }, 150); 2477 }); 2478 2479 // Init 2480 buildDots(); 2481 goTo(0, false); 2482 startAutoplay(); 2483 } 2484 2485 function initAll() { 2486 var sliders = document.querySelectorAll('.prec-slider:not([data-prec-slider-init="1"])'); 2487 for (var i = 0; i < sliders.length; i++) { 2488 initSlider(sliders[i]); 2489 } 2490 } 2491 2492 if (document.readyState === 'loading') { 2493 document.addEventListener('DOMContentLoaded', initAll); 2494 } else { 2495 initAll(); 2496 } 2497 2498 // Falls der Slider via AJAX nachgeladen wird (Filter/Pagination): 2499 // erneut initialisieren â Idempotenz-Flag verhindert Doppel-Init. 2500 if (typeof jQuery !== 'undefined') { 2501 jQuery(document).on('prec:events-loaded', initAll); 2502 } 2503})();
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.