Changeset 3476291
- Timestamp:
- 03/06/2026 11:05:10 AM (3 weeks ago)
- Location:
- luzid-content-scheduler/trunk
- Files:
-
- 6 edited
-
assets/css/luzid.css (modified) (1 diff)
-
assets/js/luzid-admin.js (modified) (5 diffs)
-
howto-de.php (modified) (6 diffs)
-
howto-en.php (modified) (1 diff)
-
luzid-content-scheduler.php (modified) (46 diffs)
-
readme.txt (modified) (5 diffs)
Legend:
- Unmodified
- Added
- Removed
-
luzid-content-scheduler/trunk/assets/css/luzid.css
r3456038 r3476291 1126 1126 /* Recurring rule validation (admin only) */ 1127 1127 .lcs-row--invalid td { background: #fff8e5; } 1128 1129 /* ======================================== 1130 Card-based Layout for Termine/Wiederholungen 1131 ======================================== */ 1132 1133 .lcs-cards-repeater { 1134 display: flex; 1135 flex-direction: column; 1136 gap: 16px; 1137 } 1138 1139 .lcs-card { 1140 background: #f9f9f9; 1141 border: 1px solid #dcdcde; 1142 border-radius: 4px; 1143 padding: 16px; 1144 } 1145 1146 .lcs-card__row { 1147 display: flex; 1148 flex-wrap: wrap; 1149 gap: 12px; 1150 align-items: flex-end; 1151 } 1152 1153 .lcs-card__row + .lcs-card__row { 1154 margin-top: 12px; 1155 padding-top: 12px; 1156 border-top: 1px solid #e5e5e5; 1157 } 1158 1159 .lcs-card__field { 1160 display: flex; 1161 flex-direction: column; 1162 gap: 4px; 1163 } 1164 1165 .lcs-card__field label { 1166 font-size: 11px; 1167 font-weight: 600; 1168 color: #646970; 1169 text-transform: uppercase; 1170 letter-spacing: 0.5px; 1171 } 1172 1173 .lcs-card__field input[type="date"], 1174 .lcs-card__field input[type="time"] { 1175 width: 130px; 1176 min-width: 130px; 1177 } 1178 1179 .lcs-card__field input[type="text"] { 1180 min-width: 180px; 1181 } 1182 1183 .lcs-card__field select { 1184 min-width: 80px; 1185 } 1186 1187 .lcs-card__field--wide { 1188 flex: 1; 1189 min-width: 200px; 1190 } 1191 1192 .lcs-card__field--wide input[type="text"] { 1193 width: 100%; 1194 } 1195 1196 .lcs-card__actions { 1197 display: flex; 1198 align-items: flex-end; 1199 padding-bottom: 2px; 1200 } 1201 1202 /* Wiederholungen: Zwei-Zeilen-Layout */ 1203 .lcs-recurring-card { 1204 background: #f9f9f9; 1205 border: 1px solid #dcdcde; 1206 border-radius: 4px; 1207 padding: 16px; 1208 margin-bottom: 12px; 1209 } 1210 1211 .lcs-recurring-card__row1 { 1212 display: flex; 1213 flex-wrap: wrap; 1214 gap: 12px; 1215 align-items: center; 1216 margin-bottom: 12px; 1217 } 1218 1219 .lcs-recurring-card__row2 { 1220 display: flex; 1221 flex-wrap: wrap; 1222 gap: 12px; 1223 align-items: flex-end; 1224 padding-top: 12px; 1225 border-top: 1px solid #e5e5e5; 1226 } 1227 1228 .lcs-recurring-card__field { 1229 display: flex; 1230 flex-direction: column; 1231 gap: 4px; 1232 } 1233 1234 .lcs-recurring-card__field label { 1235 font-size: 11px; 1236 font-weight: 600; 1237 color: #646970; 1238 text-transform: uppercase; 1239 letter-spacing: 0.5px; 1240 } 1241 1242 .lcs-recurring-card__field--type { 1243 min-width: 140px; 1244 } 1245 1246 .lcs-recurring-card__field--details { 1247 flex: 1; 1248 min-width: 200px; 1249 } 1250 1251 .lcs-recurring-card__field--time input[type="time"] { 1252 width: 100px; 1253 } 1254 1255 .lcs-recurring-card__field--text { 1256 flex: 1; 1257 min-width: 150px; 1258 } 1259 1260 .lcs-recurring-card__field--text input { 1261 width: 100%; 1262 } 1263 1264 .lcs-recurring-card__field--period { 1265 display: flex; 1266 gap: 8px; 1267 align-items: center; 1268 } 1269 1270 .lcs-recurring-card__field--period input[type="date"] { 1271 width: 130px; 1272 } 1273 1274 .lcs-recurring-card__field--period .lcs-help-inline { 1275 font-size: 12px; 1276 color: #646970; 1277 } 1278 1279 .lcs-recurring-card__actions { 1280 display: flex; 1281 align-items: flex-end; 1282 padding-bottom: 2px; 1283 } 1284 1285 /* Inline inputs spacing fix */ 1286 .lcs-inline-inputs { 1287 display: inline-flex; 1288 flex-wrap: wrap; 1289 gap: 8px; 1290 align-items: center; 1291 } 1292 1293 .lcs-inline-inputs .lcs-input { 1294 margin: 0; 1295 } 1296 1297 .lcs-inline-inputs .lcs-select { 1298 margin: 0; 1299 } 1300 1301 /* Smaller inputs for compact layouts */ 1302 .lcs-input--small { 1303 padding: 4px 8px; 1304 font-size: 13px; 1305 } 1306 1307 .lcs-select--small { 1308 padding: 4px 8px; 1309 font-size: 13px; 1310 } 1311 1312 /* Warning box for incomplete rules */ 1313 .lcs-rec-inline-warning { 1314 margin-bottom: 8px; 1315 padding: 8px 10px; 1316 background: #fff8e5; 1317 border: 1px solid #dba617; 1318 border-radius: 4px; 1319 color: #6b5a00; 1320 font-size: 13px; 1321 } 1322 1323 .lcs-rec-inline-warning .dashicons { 1324 margin-right: 6px; 1325 vertical-align: middle; 1326 } -
luzid-content-scheduler/trunk/assets/js/luzid-admin.js
r3456038 r3476291 192 192 if (!container) return; 193 193 194 var rowsWrap = container.querySelector('[data-lcs-rows]'); 194 // Support both table-based (tbody with data-lcs-rows) and card-based (.lcs-cards-repeater) layouts 195 var rowsWrap = container.querySelector('[data-lcs-rows]') || container; 195 196 var tpl = container.querySelector('template[data-lcs-template]'); 196 if (! rowsWrap || !tpl) return;197 if (!tpl) return; 197 198 198 199 container.addEventListener('click', function (e) { … … 201 202 e.preventDefault(); 202 203 var html = tpl.innerHTML; 203 var idx = rowsWrap.querySelectorAll('[data-lcs-row]').length;204 var idx = container.querySelectorAll('[data-lcs-row]').length; 204 205 html = html.replace(/__i__/g, String(idx)); 205 var tmp = document.createElement('tbody'); 206 207 // Create temporary container based on layout type 208 var isCardLayout = container.classList.contains('lcs-cards-repeater'); 209 var tmp = document.createElement(isCardLayout ? 'div' : 'tbody'); 206 210 tmp.innerHTML = html; 207 // Append all rows from template 208 Array.prototype.slice.call(tmp.children).forEach(function (tr) { 209 rowsWrap.appendChild(tr); 211 212 // Append all rows/cards from template 213 Array.prototype.slice.call(tmp.children).forEach(function (el) { 214 if (isCardLayout) { 215 // Insert before the template element 216 tpl.parentNode.insertBefore(el, tpl); 217 } else { 218 rowsWrap.appendChild(el); 219 } 210 220 }); 211 221 // Update recurring row visibility if needed 212 initRecurringTypeToggle( rowsWrap);222 initRecurringTypeToggle(container); 213 223 // Native inputs (date/time) need no additional initialization 214 224 return; … … 226 236 227 237 function initRecurringTypeToggle(root) { 228 // root can be tbody or document238 // root can be tbody, div, or document 229 239 var scope = root || document; 230 240 … … 235 245 row.querySelectorAll('[data-details]').forEach(function (el) { 236 246 var isActive = (el.getAttribute('data-details') === type); 237 el.style.display = isActive ? ' inline-flex' : 'none';247 el.style.display = isActive ? '' : 'none'; 238 248 // Wichtig: Inputs in versteckten Bereichen deaktivieren, damit keine doppelten POST-Werte 239 249 // (z.B. weekday in weekly + nth_weekday_month) den gespeicherten Wert überschreiben. … … 242 252 }); 243 253 }); 244 var common = row.querySelector('.lcs-rec-common');245 if(common) {246 common.style.display = type ? ' inline-flex' : 'none';254 // Show/hide all elements with lcs-rec-common class (can be multiple) 255 row.querySelectorAll('.lcs-rec-common').forEach(function (common) { 256 common.style.display = type ? '' : 'none'; 247 257 common.querySelectorAll('input, select, textarea').forEach(function (inp) { 248 258 inp.disabled = !type; 249 259 }); 250 } 251 } 252 253 scope.querySelectorAll('tr[data-lcs-row]').forEach(applyRow); 260 }); 261 } 262 263 // Support both tr[data-lcs-row] (table layout) and div[data-lcs-row] (card layout) 264 scope.querySelectorAll('[data-lcs-row]').forEach(applyRow); 254 265 255 266 scope.addEventListener('change', function (e) { 256 267 var sel = e.target.closest('.lcs-rec-type'); 257 268 if (!sel) return; 258 var row = sel.closest(' tr[data-lcs-row]');269 var row = sel.closest('[data-lcs-row]'); 259 270 if (row) applyRow(row); 260 271 }); -
luzid-content-scheduler/trunk/howto-de.php
r3456038 r3476291 5 5 <div class="luzid-howto"> 6 6 <div class="lcs-howto-section"> 7 <h3>Worum geht ’s?</h3>8 <p>Der <strong>Luzid Content Scheduler</strong> steuert die Sichtbarkeit von Frontend-Contentblöcken (z. B. Banner, Alerts, Divs) anhand von <strong>Terminen</strong>, <strong>Wiederholungen</strong> und <strong>Ausnahmen</strong> – ohne dass du dafür Code schreiben musst.</p>7 <h3>Worum geht's?</h3> 8 <p>Der <strong>Luzid Content Scheduler</strong> steuert die Sichtbarkeit von Frontend-Contentblöcken (z. B. Banner, Alerts, Divs) anhand von <strong>Terminen</strong>, <strong>Wiederholungen</strong> und <strong>Ausnahmen</strong> – ohne dass du dafür Code schreiben musst.</p> 9 9 <div class="lcs-callout"> 10 10 <p><strong>Merksatz:</strong> Der Block ist sichtbar, wenn <strong>mindestens eine Regel passt</strong> (ODER-Logik). <strong>Ausnahmen</strong> überschreiben alles und verhindern die Anzeige.</p> … … 15 15 <h3>Schnellstart in 3 Schritten</h3> 16 16 <ol> 17 <li><strong>Scheduler anlegen</strong> (Tab „Setup “) und speichern. Dadurch wird der <em>Slug</em> erzeugt.</li>18 <li>Deinem Frontend-Block die CSS-Klasse <code>.luzid-cs-<slug></code> geben (siehe Abschnitt „CSS-Klasse in Pagebuildern “).</li>17 <li><strong>Scheduler anlegen</strong> (Tab „Setup") und speichern. Dadurch wird der <em>Slug</em> erzeugt.</li> 18 <li>Deinem Frontend-Block die CSS-Klasse <code>.luzid-cs-<slug></code> geben (siehe Abschnitt „CSS-Klasse in Pagebuildern").</li> 19 19 <li>Regeln definieren (Termine/Wiederholungen/Ausnahmen). Optional Event-Offset aktivieren.</li> 20 20 </ol> … … 22 22 23 23 <div class="lcs-howto-section"> 24 <h3>Tab „Setup “</h3>24 <h3>Tab „Setup"</h3> 25 25 <p>Hier vergibst du den <strong>Namen</strong> des Schedulers. Beim Speichern wird daraus automatisch die stabile CSS-Klasse/der Slug erstellt. Änderst du später den Namen, ändert sich auch der Slug.</p> 26 26 <div class="lcs-callout"> 27 27 <p><strong>CSS-Klasse:</strong> Diese fügst du an den Frontend-Block an, den du steuern möchtest.</p> 28 <p><strong>Shortcode:</strong> Gibt das nächste Event-Datum dynamisch aus, z. B. <code>[luzid_cs slug="wartung" opt="long"]</code></p> 29 </div> 30 </div> 31 32 <div class="lcs-howto-section"> 33 <h3>Tab „Termine“</h3> 34 <p>Termine definieren <strong>einzelne Tage</strong> oder <strong>Zeiträume</strong>, in denen Content angezeigt werden kann.</p> 35 <ul> 36 <li><strong>Von</strong> gesetzt, <strong>Bis</strong> leer → gilt nur dieser Tag.</li> 37 <li><strong>Von</strong> und <strong>Bis</strong> gesetzt → gilt als Zeitraum.</li> 38 <li><strong>Präfix</strong> + <strong>Zeit</strong> steuern den Zeitpunkt: <em>ab</em>, <em>bis</em>, <em>um</em>.</li> 39 </ul> 40 </div> 41 42 <div class="lcs-howto-section"> 43 <h3>Tab „Wiederholungen“</h3> 44 <p>Wiederholungen sind Regeln wie „jeden Donnerstag“ oder „jeden 1. Donnerstag im Monat“. Mehrere Regeln werden per <strong>ODER</strong> kombiniert.</p> 28 <p><strong>Shortcode:</strong> Gibt das nächste Event-Datum dynamisch aus, z. B. <code>[luzid_cs slug="wartung" date="long"]</code></p> 29 <p><strong>Event-Tabelle:</strong> Aktiviere „In Event-Tabelle einbeziehen", um Events dieses Schedulers im <code>[luzid_cs_eventtable]</code> Shortcode anzuzeigen.</p> 30 </div> 31 </div> 32 33 <div class="lcs-howto-section"> 34 <h3>Tab „Termine"</h3> 35 <p>Termine definieren <strong>einzelne Tage</strong>, an denen Content angezeigt werden kann.</p> 36 <ul> 37 <li><strong>Datum</strong>: Der Tag des Events.</li> 38 <li><strong>Präfix 1</strong> + <strong>Zeit von</strong>: Startzeit mit optionalem Präfix (ab, bis, um, von).</li> 39 <li><strong>Präfix 2</strong> + <strong>Zeit bis</strong>: Endzeit mit optionalem Präfix (bis, –).</li> 40 <li><strong>Eventtext</strong>: Optionaler Text, der im Shortcode ausgegeben werden kann.</li> 41 </ul> 42 </div> 43 44 <div class="lcs-howto-section"> 45 <h3>Tab „Wiederholungen"</h3> 46 <p>Wiederholungen sind Regeln wie „jeden Donnerstag", „jeden 1. Donnerstag im Monat" oder „jährlich am 14. Februar". Mehrere Regeln werden per <strong>ODER</strong> kombiniert.</p> 45 47 <div class="lcs-callout"> 46 48 <p><strong>Beispiel:</strong> Du willst Content <em>jeden Montag</em> und zusätzlich <em>am 1. des Monats</em> anzeigen → lege zwei Regeln an. Sobald eine Regel passt, ist der Block sichtbar.</p> 47 49 </div> 48 <p>Jede Regel kann einen eigenen <strong>Regel-Zeitraum</strong> (von/bis) haben, der nur diese Regel einschränkt.</p> 49 </div> 50 51 <div class="lcs-howto-section"> 52 <h3>Tab „Ausnahmen“</h3> 50 <p><strong>Typen:</strong></p> 51 <ul> 52 <li><strong>Wöchentlich</strong>: Jeden bestimmten Wochentag.</li> 53 <li><strong>Monatlich</strong>: An einem bestimmten Tag im Monat (z.B. jeden 15.).</li> 54 <li><strong>Wochentag im Monat</strong>: Z.B. „1. Donnerstag" oder „letzter Freitag" im Monat.</li> 55 <li><strong>Jährlich</strong>: An einem bestimmten Datum jedes Jahr (z.B. 14. Februar).</li> 56 </ul> 57 <p>Jede Regel kann einen eigenen <strong>Regel-Zeitraum</strong> (Gültig von/bis) haben, der nur diese Regel einschränkt.</p> 58 </div> 59 60 <div class="lcs-howto-section"> 61 <h3>Tab „Ausnahmen"</h3> 53 62 <p>Ausnahmen überschreiben alle anderen Regeln. An den definierten Ausnahmen wird <strong>kein Content</strong> angezeigt.</p> 54 63 <ul> … … 57 66 </ul> 58 67 <div class="lcs-callout lcs-callout--warn"> 59 <p><strong>Wichtig:</strong> Wenn „jetzt“ in einer Ausnahme liegt, bleibt der Block immer unsichtbar – auch wenn Termine oder Wiederholungen passen.</p> 60 </div> 61 </div> 62 63 <div class="lcs-howto-section"> 64 <h3>Tab „Event-Offset“</h3> 65 <p>Der Event-Offset erweitert das Sichtbarkeitsfenster <strong>vor</strong> bzw. <strong>nach</strong> dem berechneten Event. Der Offset ändert <strong>nicht</strong> die Text-Ausgabe des Shortcodes, sondern nur die Sichtbarkeit.</p> 66 <ul> 67 <li><strong>Tage vorher</strong> + <strong>Startzeit</strong>: ab wann der Block vor dem Event eingeblendet wird.</li> 68 <li><strong>Tage nachher</strong> + <strong>Endzeit</strong>: wann die Sichtbarkeit endet (bei Zeiträumen bezogen auf den letzten Tag).</li> 69 </ul> 70 </div> 71 72 <div class="lcs-howto-section"> 73 <h3>Tab „Vorschau“</h3> 68 <p><strong>Wichtig:</strong> Wenn „jetzt" in einer Ausnahme liegt, bleibt der Block immer unsichtbar – auch wenn Termine oder Wiederholungen passen.</p> 69 </div> 70 </div> 71 72 <div class="lcs-howto-section"> 73 <h3>Tab „Event-Klassen"</h3> 74 <p>Event-Klassen ermöglichen dir, <strong>mehrere unabhängige Sichtbarkeitszeiträume</strong> für einen Scheduler zu definieren. Jede Klasse erzeugt eine eigene CSS-Klasse mit individuellen Offset-Einstellungen.</p> 75 76 <div class="lcs-callout"> 77 <p><strong>Anwendungsbeispiel:</strong> Du hast ein Event am 15. April. Mit Event-Klassen kannst du:</p> 78 <ul style="margin: 10px 0;"> 79 <li>Ein <strong>Popup</strong> 4 Tage vorher einblenden</li> 80 <li>Den <strong>Event-Content</strong> (mit Bildern) 2 Wochen vorher zeigen</li> 81 <li>Einen <strong>Menüeintrag</strong> 11 Tage vorher aktivieren</li> 82 </ul> 83 <p>Jeder dieser Inhalte hat seine eigene CSS-Klasse und wird unabhängig voneinander sichtbar.</p> 84 </div> 85 86 <h4>Standard-Klasse</h4> 87 <p>Die erste Klasse heißt immer „Standard" und kann nicht gelöscht oder umbenannt werden. Sie erzeugt die Haupt-CSS-Klasse:</p> 88 <p><code>.luzid-cs-<schedulername></code></p> 89 90 <h4>Zusätzliche Klassen</h4> 91 <p>Klicke auf „+ Klasse hinzufügen", um weitere Klassen anzulegen. Jede zusätzliche Klasse erhält einen eigenen Namen und erzeugt eine erweiterte CSS-Klasse:</p> 92 <p><code>.luzid-cs-<schedulername>-<klassenname></code></p> 93 94 <h4>Offset-Einstellungen pro Klasse</h4> 95 <ul> 96 <li><strong>Tage vorher</strong>: Ab wie vielen Tagen vor dem Event wird der Content sichtbar?</li> 97 <li><strong>Startzeit</strong>: Ab welcher Uhrzeit beginnt die Sichtbarkeit? (leer = 00:00 Uhr)</li> 98 <li><strong>Tage nachher</strong>: Wie viele Tage nach dem Event bleibt der Content sichtbar?</li> 99 <li><strong>Endzeit</strong>: Bis zu welcher Uhrzeit endet die Sichtbarkeit? (leer = Event-Zeitpunkt)</li> 100 </ul> 101 102 <h4>Vorschau</h4> 103 <p>Unter jeder Klasse siehst du eine Live-Vorschau des Sichtbarkeitszeitraums basierend auf dem nächsten berechneten Event.</p> 104 105 <div class="lcs-callout"> 106 <p><strong>Beispiel-Konfiguration:</strong></p> 107 <table class="lcs-table lcs-table--compact"> 108 <thead> 109 <tr><th>Klasse</th><th>CSS-Klasse</th><th>Tage vorher</th><th>Verwendung</th></tr> 110 </thead> 111 <tbody> 112 <tr><td>Standard</td><td><code>.luzid-cs-krimidinner</code></td><td>14</td><td>Hauptinhalt mit Bildern</td></tr> 113 <tr><td>popup</td><td><code>.luzid-cs-krimidinner-popup</code></td><td>4</td><td>Ankündigungs-Popup</td></tr> 114 <tr><td>menu</td><td><code>.luzid-cs-krimidinner-menu</code></td><td>11</td><td>Menüeintrag</td></tr> 115 </tbody> 116 </table> 117 </div> 118 </div> 119 120 <div class="lcs-howto-section"> 121 <h3>Tab „Vorschau"</h3> 74 122 <p>Mit der Vorschau kannst du dir die nächsten berechneten Events (inkl. Einblendungszeitpunkt und Shortcode-Text) anzeigen lassen – praktisch zum Testen deiner Regeln.</p> 75 123 </div> … … 77 125 <div class="lcs-howto-section"> 78 126 <h3>CSS-Klasse in WordPress & Pagebuildern</h3> 79 <p class="lcs-text"><em>Hinweis:</em> Frühere Plugin-Versionen nutzten das Präfix <code>luzid-cs-</code>. Diese Klasse wird weiterhin unterstützt – empfohlen ist aber <code>luzid-cs-</code>.</p> 80 <div class="lcs-callout"> 81 <p><strong>Gutenberg:</strong> Block auswählen → „Erweitert“ → „Zusätzliche CSS-Klasse(n)“ → <code>luzid-cs-<slug></code> eintragen (ohne Punkt).</p> 82 <p><strong>Elementor:</strong> Widget auswählen → „Erweitert“ → „CSS-Klassen“ → <code>luzid-cs-<slug></code> eintragen.</p> 83 <p><strong>Divi/andere Builder:</strong> Im Bereich „CSS-Klasse“ oder „Custom CSS Class“ den gleichen Wert eintragen.</p> 127 <div class="lcs-callout"> 128 <p><strong>Gutenberg:</strong> Block auswählen → „Erweitert" → „Zusätzliche CSS-Klasse(n)" → <code>luzid-cs-<slug></code> eintragen (ohne Punkt).</p> 129 <p><strong>Elementor:</strong> Widget auswählen → „Erweitert" → „CSS-Klassen" → <code>luzid-cs-<slug></code> eintragen.</p> 130 <p><strong>Divi/andere Builder:</strong> Im Bereich „CSS-Klasse" oder „Custom CSS Class" den gleichen Wert eintragen.</p> 84 131 </div> 85 132 <p>Wichtig: Die Klasse wird <strong>ohne führenden Punkt</strong> im Builder eingetragen. Der Punkt <code>.</code> ist nur in CSS-Schreibweise relevant.</p> … … 87 134 88 135 <div class="lcs-howto-section"> 89 <h3>Shortcode-Ausgabe</h3> 90 <p>Mit dem Shortcode gibst du das nächste Event oder eine Liste kommender Events im Frontend aus.</p> 91 92 <p><strong>Formate (<code>opt</code>):</strong></p> 93 <ul> 94 <li><code>opt="short"</code> → <em>01.01.2026</em></li> 95 <li><code>opt="date"</code> → <em>Donnerstag, 01.01.2026</em></li> 96 <li><code>opt="time"</code> → <em>ab 16:00 Uhr</em></li> 97 <li><code>opt="long"</code> → <em>Donnerstag, 01.01.2026 ab 16:00 Uhr</em></li> 98 </ul> 99 100 <p><strong>Parameter:</strong></p> 101 <ul> 102 <li><code>list="true|false"</code> (Standard: <code>false</code>) → gibt <code>count</code> Events als Liste aus (je Zeile ein Event, getrennt durch <code><br></code>).</li> 103 <li><code>count="n"</code> (n > 0, Standard: 10, Max.: 200) → Anzahl Events bei <code>list="true"</code>.</li> 104 <li><code>text="true|false"</code> (Standard: <code>false</code>) → hängt den optionalen <strong>Eventtext</strong> an (aus Termine/Wiederholungen).</li> 105 <li><code>sep="…"</code> (Standard: <code>-</code>) → Separator zwischen <code><opt></code> und Eventtext. Tipp: Zeilenumbruch mit <code>sep="<br>"</code>.</li> 106 <li><code>timeoffset="…"</code> (Minuten, auch negativ) → verschiebt nur die <em>Ausgabe</em> (nicht die Logik).</li> 107 <li><code>lang="de|en"</code> → Sprache für die Ausgabe erzwingen (leer = aktuelle UI).</li> 108 </ul> 109 110 <div class="lcs-callout"> 111 <p><strong>Beispiel:</strong> <code>[luzid_cs slug="wartung" opt="long"]</code></p> 112 <p><strong>Beispiel (Liste + Text):</strong> <code>[luzid_cs slug="krimidinner" opt="short" list="true" count="5" text="true" sep=" | "]</code></p> 113 <p><strong>Beispiel (Text in neuer Zeile):</strong> <code>[luzid_cs slug="krimidinner" opt="date" text="true" sep="<br>"]</code></p> 114 </div> 115 116 <p><strong>CSS-Klassen für eigenes Styling (externe CSS):</strong><br> 117 Die Ausgabe ist in Spans gekapselt, damit du Datum/Separator/Text unterschiedlich formatieren kannst:</p> 136 <h3>Shortcode: Einzelnes Event</h3> 137 <p>Mit dem Shortcode <code>[luzid_cs]</code> gibst du das nächste Event oder eine Liste kommender Events im Frontend aus.</p> 138 139 <p><strong>Basis-Syntax:</strong> <code>[luzid_cs slug="dein-slug"]</code></p> 140 141 <h4>Parameter-Übersicht</h4> 142 <table class="lcs-table lcs-table--compact"> 143 <thead> 144 <tr><th>Parameter</th><th>Standard</th><th>Beschreibung</th></tr> 145 </thead> 146 <tbody> 147 <tr><td><code>slug</code></td><td><em>(Pflicht)</em></td><td>Scheduler-Slug</td></tr> 148 <tr><td><code>date</code></td><td><em>(leer)</em></td><td>Datumsformat: <code>short</code>, <code>medium</code>, <code>long</code>, <code>full</code></td></tr> 149 <tr><td><code>time</code></td><td><em>(leer)</em></td><td>Zeitformat: <code>auto</code>, <code>raw</code>, <code>prefix</code>, <code>range</code>, <code>range_long</code></td></tr> 150 <tr><td><code>list</code></td><td><code>false</code></td><td>Liste ausgeben: <code>true</code> oder <code>false</code></td></tr> 151 <tr><td><code>count</code></td><td><code>10</code></td><td>Anzahl Events bei <code>list="true"</code> (max. 200)</td></tr> 152 <tr><td><code>text</code></td><td><code>false</code></td><td>Eventtext anhängen: <code>true</code> oder <code>false</code></td></tr> 153 <tr><td><code>sep1</code></td><td><code> </code> (Leerzeichen)</td><td>Trennzeichen zwischen Datum und Zeit</td></tr> 154 <tr><td><code>sep2</code></td><td><code> </code> (Leerzeichen)</td><td>Trennzeichen zwischen Zeit und Text</td></tr> 155 <tr><td><code>sep3</code></td><td><em>(leer)</em></td><td>Trennzeichen zwischen Listenelementen</td></tr> 156 <tr><td><code>timeoffset</code></td><td><code>0</code></td><td>Zeitverschiebung in Minuten (auch negativ)</td></tr> 157 <tr><td><code>lang</code></td><td><em>(aktuell)</em></td><td>Sprache erzwingen: <code>de</code> oder <code>en</code></td></tr> 158 </tbody> 159 </table> 160 161 <h4>Werte für <code>date</code></h4> 162 <table class="lcs-table lcs-table--compact"> 163 <thead> 164 <tr><th>Wert</th><th>Ausgabe (Beispiel)</th></tr> 165 </thead> 166 <tbody> 167 <tr><td><code>short</code></td><td>14.02.2026</td></tr> 168 <tr><td><code>medium</code></td><td>Sa, 14.02.2026</td></tr> 169 <tr><td><code>long</code></td><td>Samstag, 14.02.2026</td></tr> 170 <tr><td><code>full</code></td><td>Samstag, 14. Februar 2026</td></tr> 171 </tbody> 172 </table> 173 174 <h4>Werte für <code>time</code></h4> 175 <p><strong>Wichtig:</strong> Ohne <code>time</code>-Parameter wird <strong>keine Uhrzeit</strong> ausgegeben – nur das Datum!</p> 176 <table class="lcs-table lcs-table--compact"> 177 <thead> 178 <tr><th>Wert</th><th>Ausgabe (Beispiel)</th></tr> 179 </thead> 180 <tbody> 181 <tr><td><em>(nicht gesetzt)</em></td><td><em>keine Zeit</em></td></tr> 182 <tr><td><code>raw</code></td><td>18:00</td></tr> 183 <tr><td><code>prefix</code></td><td>von 18:00 Uhr</td></tr> 184 <tr><td><code>range</code></td><td>18:00 bis 20:00</td></tr> 185 <tr><td><code>range_long</code></td><td>von 18:00 bis 20:00 Uhr</td></tr> 186 <tr><td><code>auto</code></td><td>von 18:00 bis 20:00 Uhr <em>(intelligent)</em></td></tr> 187 </tbody> 188 </table> 189 190 <h4>Trennzeichen (Separatoren) – So funktioniert's</h4> 191 <p>Die Ausgabe besteht aus bis zu drei Teilen: <strong>Datum</strong>, <strong>Zeit</strong> und <strong>Eventtext</strong>. Mit den Trennzeichen bestimmst du, was zwischen diesen Teilen steht.</p> 192 193 <div class="lcs-callout"> 194 <p><strong>Aufbau der Ausgabe:</strong></p> 195 <p style="font-family: monospace; background: #f5f5f5; padding: 10px; border-radius: 4px;"> 196 [DATUM] <span style="color: #c00;">sep1</span> [ZEIT] <span style="color: #c00;">sep2</span> [TEXT] 197 </p> 198 <ul style="margin-top: 10px;"> 199 <li><code>sep1</code> = Was steht zwischen Datum und Zeit?</li> 200 <li><code>sep2</code> = Was steht zwischen Zeit und Text? (oder zwischen Datum und Text, wenn keine Zeit)</li> 201 <li><code>sep3</code> = Was steht zwischen den Einträgen in einer Liste?</li> 202 </ul> 203 </div> 204 205 <p><strong>Typische Werte:</strong></p> 206 <ul> 207 <li><code>" "</code> (Leerzeichen) – alles in einer Zeile</li> 208 <li><code>"<br>"</code> – Zeilenumbruch (neuer Absatz)</li> 209 <li><code>" | "</code> – senkrechter Strich als Trenner</li> 210 <li><code>" – "</code> – Gedankenstrich als Trenner</li> 211 </ul> 212 213 <h4>Beispiele mit Trennzeichen</h4> 214 215 <p><strong>Alles in einer Zeile (Standard):</strong></p> 216 <pre><code>[luzid_cs slug="event" date="long" time="auto" text="true"]</code></pre> 217 <p>→ Samstag, 14.02.2026 von 18:00 bis 20:00 Uhr Valentins-Dinner</p> 218 219 <p><strong>Nur Datum und Text, zweizeilig:</strong></p> 220 <pre><code>[luzid_cs slug="event" date="long" text="true" sep2="<br>"]</code></pre> 221 <p>→ Samstag, 14.02.2026<br> Valentins-Dinner</p> 222 223 <p><strong>Datum, Zeit und Text jeweils in eigener Zeile (dreizeilig):</strong></p> 224 <pre><code>[luzid_cs slug="event" date="long" time="auto" text="true" sep1="<br>" sep2="<br>"]</code></pre> 225 <p>→ Samstag, 14.02.2026<br> von 18:00 bis 20:00 Uhr<br> Valentins-Dinner</p> 226 227 <p><strong>Mit Trennstrich:</strong></p> 228 <pre><code>[luzid_cs slug="event" date="long" time="raw" text="true" sep1=" | " sep2=" – "]</code></pre> 229 <p>→ Samstag, 14.02.2026 | 18:00 – Valentins-Dinner</p> 230 231 <p><strong>Liste mit Leerzeile zwischen Einträgen:</strong></p> 232 <pre><code>[luzid_cs slug="event" date="long" text="true" sep2="<br>" sep3="<br><br>" list="true" count="5"]</code></pre> 233 234 <h4>CSS-Klassen für eigenes Styling</h4> 118 235 <ul> 119 236 <li><code>.luzid-cs</code> – Wrapper (zusätzlich <code>.luzid-cs--single</code> / <code>.luzid-cs--list</code>)</li> 120 237 <li><code>.luzid-cs-item</code> – ein Event (bei Listen-Ausgabe)</li> 121 <li><code>.luzid-cs-opt</code> – Datums-/Zeit-Teil (opt)</li> 122 <li><code>.luzid-cs-sep</code> – Separator</li> 123 <li><code>.luzid-cs-text</code> – optionaler Eventtext</li> 124 </ul> 125 126 <p><em>Hinweis:</em> <code>opt="list"</code> bleibt aus Kompatibilitätsgründen erhalten und entspricht <code>opt="long" list="true"</code>.</p> 238 <li><code>.luzid-cs-date</code> – Datums-Teil</li> 239 <li><code>.luzid-cs-time</code> – Zeit-Teil</li> 240 <li><code>.luzid-cs-text</code> – Eventtext</li> 241 <li><code>.luzid-cs-sep</code> – Trennzeichen (zusätzlich <code>.luzid-cs-sep1</code> / <code>.luzid-cs-sep2</code>)</li> 242 </ul> 243 </div> 244 245 <div class="lcs-howto-section"> 246 <h3>Shortcode: Event-Tabelle</h3> 247 <p>Mit <code>[luzid_cs_eventtable]</code> gibst du eine Tabelle aller kommenden Events aus – aus allen Schedulern, die „In Event-Tabelle einbeziehen" aktiviert haben.</p> 248 249 <p><strong>Basis-Syntax:</strong> <code>[luzid_cs_eventtable]</code></p> 250 251 <h4>Parameter-Übersicht</h4> 252 <table class="lcs-table lcs-table--compact"> 253 <thead> 254 <tr><th>Parameter</th><th>Standard</th><th>Beschreibung</th></tr> 255 </thead> 256 <tbody> 257 <tr><td><code>cols</code></td><td><code>date_medium,time_auto,text</code></td><td>Spalten (komma-getrennt)</td></tr> 258 <tr><td><code>count</code></td><td><code>30</code></td><td>Max. Anzahl Events</td></tr> 259 <tr><td><code>headers</code></td><td><em>(automatisch)</em></td><td>Eigene Header (komma-getrennt)</td></tr> 260 <tr><td><code>noheaders</code></td><td><code>false</code></td><td>Header ausblenden</td></tr> 261 <tr><td><code>class</code></td><td><em>(leer)</em></td><td>Zusätzliche CSS-Klasse</td></tr> 262 <tr><td><code>empty</code></td><td>Keine Termine</td><td>Text wenn keine Events</td></tr> 263 <tr><td><code>order</code></td><td><code>asc</code></td><td>Sortierung: <code>asc</code> oder <code>desc</code></td></tr> 264 <tr><td><code>lang</code></td><td><em>(aktuell)</em></td><td>Sprache erzwingen</td></tr> 265 </tbody> 266 </table> 267 268 <h4>Verfügbare Spaltentypen für <code>cols</code></h4> 269 <table class="lcs-table lcs-table--compact"> 270 <thead> 271 <tr><th>Spalte</th><th>Ausgabe (Beispiel)</th></tr> 272 </thead> 273 <tbody> 274 <tr><td><code>date_short</code></td><td>14.02.2026</td></tr> 275 <tr><td><code>date_medium</code></td><td>Sa, 14.02.2026</td></tr> 276 <tr><td><code>date_long</code></td><td>Samstag, 14.02.2026</td></tr> 277 <tr><td><code>date_full</code></td><td>Samstag, 14. Februar 2026</td></tr> 278 <tr><td><code>weekday_short</code></td><td>Sa</td></tr> 279 <tr><td><code>weekday_long</code></td><td>Samstag</td></tr> 280 <tr><td><code>time_raw</code></td><td>18:00</td></tr> 281 <tr><td><code>time_auto</code></td><td>von 18:00 bis 20:00 Uhr</td></tr> 282 <tr><td><code>time_prefix</code></td><td>von 18:00 Uhr</td></tr> 283 <tr><td><code>time_range</code></td><td>18:00 bis 20:00</td></tr> 284 <tr><td><code>time_range_long</code></td><td>von 18:00 bis 20:00 Uhr</td></tr> 285 <tr><td><code>text</code></td><td>Eventtext</td></tr> 286 <tr><td><code>scheduler</code></td><td>Scheduler-Name</td></tr> 287 </tbody> 288 </table> 289 290 <div class="lcs-callout"> 291 <p><strong>Beispiele:</strong></p> 292 <p><code>[luzid_cs_eventtable]</code> → Standard-Tabelle</p> 293 <p><code>[luzid_cs_eventtable cols="weekday_short,date_short,time_range,text" count="10"]</code></p> 294 <p><code>[luzid_cs_eventtable cols="date_long,time_auto,text,scheduler" headers="Datum,Uhrzeit,Event,Kategorie"]</code></p> 295 </div> 296 297 <p><strong>CSS-Klassen für eigenes Styling:</strong></p> 298 <ul> 299 <li><code>.luzid-cs-eventtable</code> – Tabelle</li> 300 <li><code>.luzid-cs-eventtable__head</code> – Tabellenkopf</li> 301 <li><code>.luzid-cs-eventtable__body</code> – Tabellenkörper</li> 302 <li><code>.luzid-cs-eventtable__row</code> – Zeile (+ <code>--header</code>, <code>--odd</code>, <code>--even</code>)</li> 303 <li><code>.luzid-cs-eventtable__cell</code> – Zelle (+ <code>--date_short</code>, <code>--time_auto</code>, etc.)</li> 304 </ul> 127 305 </div> 128 306 </div> -
luzid-content-scheduler/trunk/howto-en.php
r3456038 r3476291 5 5 <div class="luzid-howto"> 6 6 <div class="lcs-howto-section"> 7 <h3>What does it do?</h3>8 <p> <strong>Luzid Content Scheduler</strong> controls the visibility of frontend content blocks (e.g. banners, alerts, divs) based on <strong>dates</strong>, <strong>recurring rules</strong> and <strong>exceptions</strong> — without writingcode.</p>9 <div class="lcs-callout"> 10 <p><strong>R ule of thumb:</strong> The block is visible if <strong>any rule matches</strong> (OR logic). <strong>Exceptions</strong> override everything and hide the content.</p>11 </div> 12 </div> 13 14 <div class="lcs-howto-section"> 15 <h3>Quick start in 3 steps</h3>7 <h3>What's this about?</h3> 8 <p>The <strong>Luzid Content Scheduler</strong> controls the visibility of frontend content blocks (e.g., banners, alerts, divs) based on <strong>dates</strong>, <strong>recurring rules</strong>, and <strong>exceptions</strong> – without writing any code.</p> 9 <div class="lcs-callout"> 10 <p><strong>Remember:</strong> The block is visible when <strong>at least one rule matches</strong> (OR logic). <strong>Exceptions</strong> override everything and prevent display.</p> 11 </div> 12 </div> 13 14 <div class="lcs-howto-section"> 15 <h3>Quick Start in 3 Steps</h3> 16 16 <ol> 17 <li><strong>Create a scheduler</strong> ( tab “Setup”) and save it. This generates the <em>slug</em>.</li>18 <li>Add the CSS class <code>.luzid-cs-<slug></code> to your frontend block (see “Using the CSS class in page builders”).</li>19 <li>Define rules (Dates / Recurring /Exceptions). Optionally enable Event Offset.</li>17 <li><strong>Create a scheduler</strong> (Setup tab) and save. This generates the <em>slug</em>.</li> 18 <li>Add the CSS class <code>.luzid-cs-<slug></code> to your frontend block (see "CSS Class in Page Builders").</li> 19 <li>Define rules (Dates/Recurring/Exceptions). Optionally enable Event Offset.</li> 20 20 </ol> 21 21 </div> 22 22 23 23 <div class="lcs-howto-section"> 24 <h3>Tab “Setup”</h3> 25 <p>Enter a <strong>name</strong> for the scheduler. When saving, the CSS class/slug is generated automatically. If you rename it later, the slug changes as well.</p> 26 <div class="lcs-callout"> 27 <p><strong>CSS class:</strong> Add this to the frontend block you want to control.</p> 28 <p><strong>Shortcode:</strong> Prints the next event dynamically, e.g. <code>[luzid_cs slug="maintenance" opt="long"]</code></p> 29 </div> 30 </div> 31 32 <div class="lcs-howto-section"> 33 <h3>Tab “Dates”</h3> 34 <p>Dates define <strong>single days</strong> or <strong>ranges</strong> in which content can be shown.</p> 35 <ul> 36 <li><strong>From</strong> set, <strong>To</strong> empty → applies to that single day.</li> 37 <li><strong>From</strong> and <strong>To</strong> set → treated as a date range.</li> 38 <li><strong>Prefix</strong> + <strong>time</strong> control the moment: <em>from</em>, <em>until</em>, <em>at</em>.</li> 39 </ul> 40 </div> 41 42 <div class="lcs-howto-section"> 43 <h3>Tab “Recurring”</h3> 44 <p>Recurring rules are patterns like “every Thursday” or “every 1st Thursday of the month”. Multiple rules are combined with <strong>OR</strong>.</p> 24 <h3>Setup Tab</h3> 25 <p>Here you set the <strong>name</strong> of the scheduler. When saving, the stable CSS class/slug is automatically created. If you change the name later, the slug changes too.</p> 26 <div class="lcs-callout"> 27 <p><strong>CSS Class:</strong> Add this to the frontend block you want to control.</p> 28 <p><strong>Shortcode:</strong> Outputs the next event date dynamically, e.g., <code>[luzid_cs slug="maintenance" date="long"]</code></p> 29 <p><strong>Event Table:</strong> Enable "Include in Event Table" to show events from this scheduler in the <code>[luzid_cs_eventtable]</code> shortcode.</p> 30 </div> 31 </div> 32 33 <div class="lcs-howto-section"> 34 <h3>Dates Tab</h3> 35 <p>Dates define <strong>single days</strong> when content should be displayed.</p> 36 <ul> 37 <li><strong>Date</strong>: The day of the event.</li> 38 <li><strong>Prefix 1</strong> + <strong>Time from</strong>: Start time with optional prefix (from, until, at).</li> 39 <li><strong>Prefix 2</strong> + <strong>Time to</strong>: End time with optional prefix (to, –).</li> 40 <li><strong>Event text</strong>: Optional text that can be output in the shortcode.</li> 41 </ul> 42 </div> 43 44 <div class="lcs-howto-section"> 45 <h3>Recurring Tab</h3> 46 <p>Recurring rules are patterns like "every Thursday", "first Thursday of the month", or "yearly on February 14". Multiple rules are combined with <strong>OR</strong> logic.</p> 45 47 <div class="lcs-callout"> 46 48 <p><strong>Example:</strong> You want content visible <em>every Monday</em> and also <em>on the 1st of each month</em> → create two rules. As soon as one rule matches, the block is visible.</p> 47 49 </div> 48 <p>Each rule can have its own <strong>rule period</strong> (from/to) which restricts only that rule.</p> 49 </div> 50 51 <div class="lcs-howto-section"> 52 <h3>Tab “Exceptions”</h3> 53 <p>Exceptions override all other rules. No content is shown within the defined exception ranges.</p> 54 <ul> 55 <li>For a single day, fill only <strong>from</strong> and leave <strong>to</strong> empty.</li> 56 <li>For a range, fill both fields.</li> 50 <p><strong>Types:</strong></p> 51 <ul> 52 <li><strong>Weekly</strong>: Every specific weekday.</li> 53 <li><strong>Monthly</strong>: On a specific day of the month (e.g., every 15th).</li> 54 <li><strong>Weekday in month</strong>: E.g., "1st Thursday" or "last Friday" of the month.</li> 55 <li><strong>Yearly</strong>: On a specific date each year (e.g., February 14).</li> 56 </ul> 57 <p>Each rule can have its own <strong>validity period</strong> (Valid from/to) that restricts only that rule.</p> 58 </div> 59 60 <div class="lcs-howto-section"> 61 <h3>Exceptions Tab</h3> 62 <p>Exceptions override all other rules. On defined exceptions, <strong>no content</strong> is displayed.</p> 63 <ul> 64 <li>For single days, fill only the <strong>from</strong> field and leave the <strong>to</strong> field empty.</li> 65 <li>For date ranges, fill both fields.</li> 57 66 </ul> 58 67 <div class="lcs-callout lcs-callout--warn"> 59 <p><strong>Important:</strong> If “now” is inside an exception range, the block stays hidden — even if dates or recurring rules match.</p> 60 </div> 61 </div> 62 63 <div class="lcs-howto-section"> 64 <h3>Tab “Event Offset”</h3> 65 <p>Event offset extends the visibility window <strong>before</strong> and/or <strong>after</strong> the calculated event. It does <strong>not</strong> change the shortcode text output — only visibility.</p> 66 <ul> 67 <li><strong>Days before</strong> + <strong>start time</strong>: when the block becomes visible before the event.</li> 68 <li><strong>Days after</strong> + <strong>end time</strong>: when visibility ends (for ranges, based on the last day).</li> 69 </ul> 70 </div> 71 72 <div class="lcs-howto-section"> 73 <h3>Tab “Preview”</h3> 74 <p>The preview generates upcoming calculated events (including show-from time and shortcode text) — useful for testing your rules.</p> 75 </div> 76 77 <div class="lcs-howto-section"> 78 <h3>Using the CSS class in WordPress & page builders</h3> 79 <div class="lcs-callout"> 80 <p><strong>Gutenberg:</strong> Select block → “Advanced” → “Additional CSS class(es)” → enter <code>luzid-cs-<slug></code> (without the dot).</p> 81 <p><strong>Elementor:</strong> Select widget → “Advanced” → “CSS Classes” → enter <code>luzid-cs-<slug></code>.</p> 82 <p><strong>Divi/other builders:</strong> Use the “CSS Class” / “Custom CSS class” field with the same value.</p> 83 </div> 84 <p>Note: In builders you enter the class <strong>without</strong> the leading dot. The dot <code>.</code> is only used in CSS notation.</p> 85 </div> 86 87 <div class="lcs-howto-section"> 88 <h3>Shortcode output</h3> 89 <p>Use the shortcode to print the next event – or a list of upcoming events – in the frontend.</p> 90 91 <p><strong>Formats (<code>opt</code>):</strong></p> 92 <ul> 93 <li><code>opt="short"</code> → <em>01.01.2026</em></li> 94 <li><code>opt="date"</code> → <em>Thursday, 01.01.2026</em></li> 95 <li><code>opt="time"</code> → <em>from 16:00</em></li> 96 <li><code>opt="long"</code> → <em>Thursday, 01.01.2026 from 16:00</em></li> 97 </ul> 98 99 <p><strong>Parameters:</strong></p> 100 <ul> 101 <li><code>list="true|false"</code> (default: <code>false</code>) → outputs <code>count</code> events as a list (one per line, separated by <code><br></code>).</li> 102 <li><code>count="n"</code> (n > 0, default: 10, max: 200) → number of events for <code>list="true"</code>.</li> 103 <li><code>text="true|false"</code> (default: <code>false</code>) → appends the optional <strong>event text</strong> (from single dates / recurring rules).</li> 104 <li><code>sep="…"</code> (default: <code>-</code>) → separator between <code><opt></code> and event text. Tip: line break with <code>sep="<br>"</code>.</li> 105 <li><code>timeoffset="…"</code> (minutes, can be negative) → shifts the <em>printed output</em> only (not the schedule logic).</li> 106 <li><code>lang="de|en"</code> → force output language (empty = current UI).</li> 107 </ul> 108 109 <div class="lcs-callout"> 110 <p><strong>Example:</strong> <code>[luzid_cs slug="maintenance" opt="long"]</code></p> 111 <p><strong>Example (list + text):</strong> <code>[luzid_cs slug="krimidinner" opt="short" list="true" count="5" text="true" sep=" | "]</code></p> 112 <p><strong>Example (text on new line):</strong> <code>[luzid_cs slug="krimidinner" opt="date" text="true" sep="<br>"]</code></p> 113 </div> 114 115 <p><strong>CSS hooks for your own styling (external CSS):</strong><br> 116 Output is wrapped in spans so you can style date/separator/text individually:</p> 117 <ul> 118 <li><code>.luzid-cs</code> – wrapper (plus <code>.luzid-cs--single</code> / <code>.luzid-cs--list</code>)</li> 119 <li><code>.luzid-cs-item</code> – one event (for list output)</li> 120 <li><code>.luzid-cs-opt</code> – date/time part (opt)</li> 121 <li><code>.luzid-cs-sep</code> – separator</li> 122 <li><code>.luzid-cs-text</code> – optional event text</li> 123 </ul> 124 125 <p><em>Note:</em> <code>opt="list"</code> is kept for backwards compatibility and equals <code>opt="long" list="true"</code>.</p> 68 <p><strong>Important:</strong> If "now" falls within an exception, the block stays invisible – even if dates or recurring rules match.</p> 69 </div> 70 </div> 71 72 <div class="lcs-howto-section"> 73 <h3>Event Classes Tab</h3> 74 <p>Event classes allow you to define <strong>multiple independent visibility windows</strong> for a scheduler. Each class generates its own CSS class with individual offset settings.</p> 75 76 <div class="lcs-callout"> 77 <p><strong>Use case example:</strong> You have an event on April 15th. With event classes you can:</p> 78 <ul style="margin: 10px 0;"> 79 <li>Show a <strong>popup</strong> 4 days before</li> 80 <li>Display the <strong>event content</strong> (with images) 2 weeks before</li> 81 <li>Activate a <strong>menu item</strong> 11 days before</li> 82 </ul> 83 <p>Each of these content blocks has its own CSS class and becomes visible independently.</p> 84 </div> 85 86 <h4>Standard Class</h4> 87 <p>The first class is always called "Standard" and cannot be deleted or renamed. It generates the main CSS class:</p> 88 <p><code>.luzid-cs-<schedulername></code></p> 89 90 <h4>Additional Classes</h4> 91 <p>Click "+ Add class" to create more classes. Each additional class gets its own name and generates an extended CSS class:</p> 92 <p><code>.luzid-cs-<schedulername>-<classname></code></p> 93 94 <h4>Offset Settings per Class</h4> 95 <ul> 96 <li><strong>Days before</strong>: How many days before the event should the content become visible?</li> 97 <li><strong>Start time</strong>: At what time does visibility begin? (empty = 00:00)</li> 98 <li><strong>Days after</strong>: How many days after the event should the content remain visible?</li> 99 <li><strong>End time</strong>: Until what time does visibility last? (empty = event time)</li> 100 </ul> 101 102 <h4>Preview</h4> 103 <p>Below each class you'll see a live preview of the visibility window based on the next calculated event.</p> 104 105 <div class="lcs-callout"> 106 <p><strong>Example configuration:</strong></p> 107 <table class="lcs-table lcs-table--compact"> 108 <thead> 109 <tr><th>Class</th><th>CSS Class</th><th>Days before</th><th>Usage</th></tr> 110 </thead> 111 <tbody> 112 <tr><td>Standard</td><td><code>.luzid-cs-murder-mystery</code></td><td>14</td><td>Main content with images</td></tr> 113 <tr><td>popup</td><td><code>.luzid-cs-murder-mystery-popup</code></td><td>4</td><td>Announcement popup</td></tr> 114 <tr><td>menu</td><td><code>.luzid-cs-murder-mystery-menu</code></td><td>11</td><td>Menu entry</td></tr> 115 </tbody> 116 </table> 117 </div> 118 </div> 119 120 <div class="lcs-howto-section"> 121 <h3>Preview Tab</h3> 122 <p>The preview shows you the next calculated events (including display time and shortcode text) – useful for testing your rules.</p> 123 </div> 124 125 <div class="lcs-howto-section"> 126 <h3>CSS Class in WordPress & Page Builders</h3> 127 <div class="lcs-callout"> 128 <p><strong>Gutenberg:</strong> Select block → "Advanced" → "Additional CSS class(es)" → enter <code>luzid-cs-<slug></code> (without dot).</p> 129 <p><strong>Elementor:</strong> Select widget → "Advanced" → "CSS Classes" → enter <code>luzid-cs-<slug></code>.</p> 130 <p><strong>Divi/other builders:</strong> Enter the same value in the "CSS Class" or "Custom CSS Class" field.</p> 131 </div> 132 <p>Important: The class is entered <strong>without the leading dot</strong> in the builder. The dot <code>.</code> is only used in CSS notation.</p> 133 </div> 134 135 <div class="lcs-howto-section"> 136 <h3>Shortcode: Single Event</h3> 137 <p>Use the <code>[luzid_cs]</code> shortcode to output the next event or a list of upcoming events in the frontend.</p> 138 139 <p><strong>Basic syntax:</strong> <code>[luzid_cs slug="your-slug"]</code></p> 140 141 <h4>Parameter Overview</h4> 142 <table class="lcs-table lcs-table--compact"> 143 <thead> 144 <tr><th>Parameter</th><th>Default</th><th>Description</th></tr> 145 </thead> 146 <tbody> 147 <tr><td><code>slug</code></td><td><em>(required)</em></td><td>Scheduler slug</td></tr> 148 <tr><td><code>date</code></td><td><em>(leer)</em></td><td>Date format: <code>short</code>, <code>medium</code>, <code>long</code>, <code>full</code></td></tr> 149 <tr><td><code>time</code></td><td><em>(empty)</em></td><td>Time format: <code>auto</code>, <code>raw</code>, <code>prefix</code>, <code>range</code>, <code>range_long</code></td></tr> 150 <tr><td><code>list</code></td><td><code>false</code></td><td>Output as list: <code>true</code> or <code>false</code></td></tr> 151 <tr><td><code>count</code></td><td><code>10</code></td><td>Number of events when <code>list="true"</code> (max. 200)</td></tr> 152 <tr><td><code>text</code></td><td><code>false</code></td><td>Append event text: <code>true</code> or <code>false</code></td></tr> 153 <tr><td><code>sep1</code></td><td><code> </code> (space)</td><td>Separator between date and time</td></tr> 154 <tr><td><code>sep2</code></td><td><code> </code> (space)</td><td>Separator between time and text</td></tr> 155 <tr><td><code>sep3</code></td><td><em>(empty)</em></td><td>Separator between list items</td></tr> 156 <tr><td><code>timeoffset</code></td><td><code>0</code></td><td>Time offset in minutes (can be negative)</td></tr> 157 <tr><td><code>lang</code></td><td><em>(current)</em></td><td>Force language: <code>de</code> or <code>en</code></td></tr> 158 </tbody> 159 </table> 160 161 <h4>Values for <code>date</code></h4> 162 <table class="lcs-table lcs-table--compact"> 163 <thead> 164 <tr><th>Value</th><th>Output (Example)</th></tr> 165 </thead> 166 <tbody> 167 <tr><td><code>short</code></td><td>14.02.2026</td></tr> 168 <tr><td><code>medium</code></td><td>Sat, 14.02.2026</td></tr> 169 <tr><td><code>long</code></td><td>Saturday, 14.02.2026</td></tr> 170 <tr><td><code>full</code></td><td>Saturday, 14 February 2026</td></tr> 171 </tbody> 172 </table> 173 174 <h4>Values for <code>time</code></h4> 175 <p><strong>Important:</strong> Without the <code>time</code> parameter, <strong>no time</strong> is displayed – only the date!</p> 176 <table class="lcs-table lcs-table--compact"> 177 <thead> 178 <tr><th>Value</th><th>Output (Example)</th></tr> 179 </thead> 180 <tbody> 181 <tr><td><em>(not set)</em></td><td><em>no time</em></td></tr> 182 <tr><td><code>raw</code></td><td>18:00</td></tr> 183 <tr><td><code>prefix</code></td><td>from 18:00</td></tr> 184 <tr><td><code>range</code></td><td>18:00 to 20:00</td></tr> 185 <tr><td><code>range_long</code></td><td>from 18:00 to 20:00</td></tr> 186 <tr><td><code>auto</code></td><td>from 18:00 to 20:00 <em>(intelligent)</em></td></tr> 187 </tbody> 188 </table> 189 190 <h4>Separators – How They Work</h4> 191 <p>The output consists of up to three parts: <strong>Date</strong>, <strong>Time</strong>, and <strong>Event text</strong>. Separators let you control what appears between these parts.</p> 192 193 <div class="lcs-callout"> 194 <p><strong>Output structure:</strong></p> 195 <p style="font-family: monospace; background: #f5f5f5; padding: 10px; border-radius: 4px;"> 196 [DATE] <span style="color: #c00;">sep1</span> [TIME] <span style="color: #c00;">sep2</span> [TEXT] 197 </p> 198 <ul style="margin-top: 10px;"> 199 <li><code>sep1</code> = What goes between date and time?</li> 200 <li><code>sep2</code> = What goes between time and text? (or between date and text if no time)</li> 201 <li><code>sep3</code> = What goes between entries in a list?</li> 202 </ul> 203 </div> 204 205 <p><strong>Common values:</strong></p> 206 <ul> 207 <li><code>" "</code> (space) – everything on one line</li> 208 <li><code>"<br>"</code> – line break (new line)</li> 209 <li><code>" | "</code> – pipe character as separator</li> 210 <li><code>" – "</code> – dash as separator</li> 211 </ul> 212 213 <h4>Examples with Separators</h4> 214 215 <p><strong>Everything on one line (default):</strong></p> 216 <pre><code>[luzid_cs slug="event" date="long" time="auto" text="true"]</code></pre> 217 <p>→ Saturday, 14.02.2026 from 18:00 to 20:00 Valentine's Dinner</p> 218 219 <p><strong>Date and text only, two lines:</strong></p> 220 <pre><code>[luzid_cs slug="event" date="long" text="true" sep2="<br>"]</code></pre> 221 <p>→ Saturday, 14.02.2026<br> Valentine's Dinner</p> 222 223 <p><strong>Date, time, and text each on separate lines (three lines):</strong></p> 224 <pre><code>[luzid_cs slug="event" date="long" time="auto" text="true" sep1="<br>" sep2="<br>"]</code></pre> 225 <p>→ Saturday, 14.02.2026<br> from 18:00 to 20:00<br> Valentine's Dinner</p> 226 227 <p><strong>With separators:</strong></p> 228 <pre><code>[luzid_cs slug="event" date="long" time="raw" text="true" sep1=" | " sep2=" – "]</code></pre> 229 <p>→ Saturday, 14.02.2026 | 18:00 – Valentine's Dinner</p> 230 231 <p><strong>List with blank line between entries:</strong></p> 232 <pre><code>[luzid_cs slug="event" date="long" text="true" sep2="<br>" sep3="<br><br>" list="true" count="5"]</code></pre> 233 234 <h4>CSS Classes for Custom Styling</h4> 235 <ul> 236 <li><code>.luzid-cs</code> – Wrapper (also <code>.luzid-cs--single</code> / <code>.luzid-cs--list</code>)</li> 237 <li><code>.luzid-cs-item</code> – One event (in list output)</li> 238 <li><code>.luzid-cs-date</code> – Date part</li> 239 <li><code>.luzid-cs-time</code> – Time part</li> 240 <li><code>.luzid-cs-text</code> – Event text</li> 241 <li><code>.luzid-cs-sep</code> – Separator (also <code>.luzid-cs-sep1</code> / <code>.luzid-cs-sep2</code>)</li> 242 </ul> 243 </div> 244 245 <div class="lcs-howto-section"> 246 <h3>Shortcode: Event Table</h3> 247 <p>Use <code>[luzid_cs_eventtable]</code> to output a table of all upcoming events – from all schedulers that have "Include in Event Table" enabled.</p> 248 249 <p><strong>Basic syntax:</strong> <code>[luzid_cs_eventtable]</code></p> 250 251 <h4>Parameter Overview</h4> 252 <table class="lcs-table lcs-table--compact"> 253 <thead> 254 <tr><th>Parameter</th><th>Default</th><th>Description</th></tr> 255 </thead> 256 <tbody> 257 <tr><td><code>cols</code></td><td><code>date_medium,time_auto,text</code></td><td>Columns (comma-separated)</td></tr> 258 <tr><td><code>count</code></td><td><code>30</code></td><td>Max. number of events</td></tr> 259 <tr><td><code>headers</code></td><td><em>(automatic)</em></td><td>Custom headers (comma-separated)</td></tr> 260 <tr><td><code>noheaders</code></td><td><code>false</code></td><td>Hide headers</td></tr> 261 <tr><td><code>class</code></td><td><em>(empty)</em></td><td>Additional CSS class</td></tr> 262 <tr><td><code>empty</code></td><td>No events</td><td>Text when no events</td></tr> 263 <tr><td><code>order</code></td><td><code>asc</code></td><td>Sort order: <code>asc</code> or <code>desc</code></td></tr> 264 <tr><td><code>lang</code></td><td><em>(current)</em></td><td>Force language</td></tr> 265 </tbody> 266 </table> 267 268 <h4>Available Column Types for <code>cols</code></h4> 269 <table class="lcs-table lcs-table--compact"> 270 <thead> 271 <tr><th>Column</th><th>Output (Example)</th></tr> 272 </thead> 273 <tbody> 274 <tr><td><code>date_short</code></td><td>14.02.2026</td></tr> 275 <tr><td><code>date_medium</code></td><td>Sat, 14.02.2026</td></tr> 276 <tr><td><code>date_long</code></td><td>Saturday, 14.02.2026</td></tr> 277 <tr><td><code>date_full</code></td><td>Saturday, 14 February 2026</td></tr> 278 <tr><td><code>weekday_short</code></td><td>Sat</td></tr> 279 <tr><td><code>weekday_long</code></td><td>Saturday</td></tr> 280 <tr><td><code>time_raw</code></td><td>18:00</td></tr> 281 <tr><td><code>time_auto</code></td><td>from 18:00 to 20:00</td></tr> 282 <tr><td><code>time_prefix</code></td><td>from 18:00</td></tr> 283 <tr><td><code>time_range</code></td><td>18:00 to 20:00</td></tr> 284 <tr><td><code>time_range_long</code></td><td>from 18:00 to 20:00</td></tr> 285 <tr><td><code>text</code></td><td>Event text</td></tr> 286 <tr><td><code>scheduler</code></td><td>Scheduler name</td></tr> 287 </tbody> 288 </table> 289 290 <div class="lcs-callout"> 291 <p><strong>Examples:</strong></p> 292 <p><code>[luzid_cs_eventtable]</code> → Default table</p> 293 <p><code>[luzid_cs_eventtable cols="weekday_short,date_short,time_range,text" count="10"]</code></p> 294 <p><code>[luzid_cs_eventtable cols="date_long,time_auto,text,scheduler" headers="Date,Time,Event,Category"]</code></p> 295 </div> 296 297 <p><strong>CSS Classes for Custom Styling:</strong></p> 298 <ul> 299 <li><code>.luzid-cs-eventtable</code> – Table</li> 300 <li><code>.luzid-cs-eventtable__head</code> – Table head</li> 301 <li><code>.luzid-cs-eventtable__body</code> – Table body</li> 302 <li><code>.luzid-cs-eventtable__row</code> – Row (+ <code>--header</code>, <code>--odd</code>, <code>--even</code>)</li> 303 <li><code>.luzid-cs-eventtable__cell</code> – Cell (+ <code>--date_short</code>, <code>--time_auto</code>, etc.)</li> 304 </ul> 126 305 </div> 127 306 </div> -
luzid-content-scheduler/trunk/luzid-content-scheduler.php
r3456038 r3476291 4 4 * Plugin URI: 5 5 * Description: Schedule the visibility of frontend content blocks (banners, alerts, divs) and output the next event via shortcode. 6 * Version: 1. 2.26 * Version: 1.4.2 7 7 * Author: Luzid Media 8 8 * Author URI: https://luzid-media.com … … 65 65 class Luzid_Content_Scheduler { 66 66 67 const VERSION = '1. 2.2';67 const VERSION = '1.4.2'; 68 68 const MENU_SLUG = 'luzid-content-scheduler'; 69 69 const OPT_ENTRIES = 'luzid_cs_entries'; … … 290 290 291 291 add_shortcode( 'luzid_cs', [ $this, 'shortcode_next_event' ] ); 292 add_shortcode( 'luzid_cs_eventtable', [ $this, 'shortcode_eventtable' ] ); 293 292 294 293 295 add_filter( 'body_class', [ $this, 'body_class_active_entries' ] ); … … 307 309 return []; 308 310 } 309 // Deep sanitize: we sanitize individual fields when accessing them,310 // but we need the raw structure for array fields like luzid_cs_single[].311 // Using map_deep with sanitize_text_field for basic protection.312 311 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce is verified in handle_actions() before data processing. 313 312 return map_deep( wp_unslash( $_POST ), 'sanitize_text_field' ); … … 477 476 'exceptions' => [], 478 477 'event_offset' => [ 'enabled' => 0, 'days_before' => 0, 'start_time' => '00:01', 'days_after' => 0, 'end_time' => '' ], 478 'include_in_eventtable' => 0, 479 479 ]; 480 480 … … 520 520 $type = sanitize_text_field( (string) $type_raw ); 521 521 if ( $type === '' ) { continue; } 522 $allowed_types = [ 'weekly', 'day_of_month', 'nth_weekday_month' ];522 $allowed_types = [ 'weekly', 'day_of_month', 'nth_weekday_month', 'yearly' ]; 523 523 if ( ! in_array( $type, $allowed_types, true ) ) { continue; } 524 524 $time = $this->sanitize_time( $rr['time'] ?? '' ); … … 536 536 $offset_enabled = ! empty( $post['luzid_cs_offset_enabled'] ) ? 1 : 0; 537 537 538 $days_before = isset( $post['luzid_cs_offset_days_before'] ) ? (int) $post['luzid_cs_offset_days_before'] : 0; 538 // Backward-Compat: altes Feld luzid_cs_offset_days -> days_before 539 $days_before = isset( $post['luzid_cs_offset_days_before'] ) ? (int) wp_unslash( (string) $post['luzid_cs_offset_days_before'] ) : ( isset( $post['luzid_cs_offset_days'] ) ? (int) wp_unslash( (string) $post['luzid_cs_offset_days'] ) : 0 ); 539 540 $days_before = max( 0, $days_before ); 540 541 … … 555 556 'end_time' => $end_time, 556 557 ]; 558 559 // Include in Event Table 560 $entry['include_in_eventtable'] = ! empty( $post['luzid_cs_include_in_eventtable'] ) ? 1 : 0; 557 561 558 562 return [ 'entry' => $entry, 'errors' => $errors ]; … … 660 664 private function sanitize_prefix( $s ) { 661 665 $s = sanitize_text_field( (string) $s ); 662 $allowed = [ '', 'ab', 'bis', 'um' ]; 666 // Präfix 1: keins | ab | bis | um | von | Ab | Bis | Um | Von (+ EN variants) 667 $allowed = [ '', 'ab', 'bis', 'um', 'von', 'Ab', 'Bis', 'Um', 'Von', 'from', 'until', 'at', 'From', 'Until', 'At' ]; 663 668 return in_array( $s, $allowed, true ) ? $s : ''; 664 669 } 665 670 671 private function sanitize_prefix2( $s ) { 672 $s = sanitize_text_field( (string) $s ); 673 // Präfix 2: keins | bis | Bis | dash (+ EN variants) 674 $allowed = [ '', 'bis', 'Bis', 'dash', 'to', 'To' ]; 675 return in_array( $s, $allowed, true ) ? $s : ''; 676 } 677 666 678 /** 667 * Termine: Von/Bis (Bis optional). Für einzelne Tage nur "von" ausfüllen. 668 * Backward-Compat: alte Struktur {date,prefix,time} wird als {from,to,prefix,time} interpretiert. 679 * Termine: Einzelne Tage mit optionaler Zeit und Zeitspanne. 669 680 */ 670 681 … … 679 690 // Migration: "date" -> "from" 680 691 $from = $this->sanitize_date( $r['from'] ?? ( $r['date'] ?? '' ) ); 681 $to = $this->sanitize_date( $r['to'] ?? '' );682 692 683 693 if ( $from === '' ) { 684 694 continue; 685 695 } 686 if ( $to !== '' && $to < $from ) {687 $tmp = $from;688 $from = $to;689 $to = $tmp;690 }691 696 692 697 $prefix = $this->sanitize_prefix( $r['prefix'] ?? '' ); 693 698 $time = $this->sanitize_time( $r['time'] ?? '' ); 699 $prefix2 = $this->sanitize_prefix2( $r['prefix2'] ?? '' ); 700 $time_to = $this->sanitize_time( $r['time_to'] ?? '' ); 694 701 $text = $this->sanitize_event_text( $r['text'] ?? '' ); 695 702 696 703 $out[] = [ 697 'from' => $from, 698 'to' => $to, 699 'prefix' => $prefix, 700 'time' => $time, 701 'text' => $text, 704 'from' => $from, 705 'prefix' => $prefix, 706 'time' => $time, 707 'prefix2' => $prefix2, 708 'time_to' => $time_to, 709 'text' => $text, 702 710 ]; 703 711 } … … 736 744 if ( ! is_array( $r ) ) continue; 737 745 $type = sanitize_text_field( (string) ( $r['type'] ?? '' ) ); 738 $allowed = [ 'weekly', 'day_of_month', 'nth_weekday_month' ];746 $allowed = [ 'weekly', 'day_of_month', 'nth_weekday_month', 'yearly' ]; 739 747 if ( ! in_array( $type, $allowed, true ) ) continue; 740 748 … … 742 750 $time = $this->sanitize_time( $r['time'] ?? '' ); 743 751 if ( $time === '' ) { continue; } 752 $prefix2 = $this->sanitize_prefix2( $r['prefix2'] ?? '' ); 753 $time_to = $this->sanitize_time( $r['time_to'] ?? '' ); 744 754 $text = $this->sanitize_event_text( $r['text'] ?? '' ); 745 755 … … 752 762 'prefix' => $prefix, 753 763 'time' => $time, 764 'prefix2' => $prefix2, 765 'time_to' => $time_to, 754 766 'text' => $text, 755 767 'from' => $from, … … 784 796 $rule['nth'] = $nth; 785 797 $rule['weekday'] = $weekday; 798 } 799 800 if ( $type === 'yearly' ) { 801 $yearly_day = isset( $r['yearly_day'] ) ? (int) $r['yearly_day'] : 0; 802 $yearly_month = isset( $r['yearly_month'] ) ? (int) $r['yearly_month'] : 0; 803 if ( $yearly_day < 1 || $yearly_day > 31 ) continue; 804 if ( $yearly_month < 1 || $yearly_month > 12 ) continue; 805 $rule['yearly_day'] = $yearly_day; 806 $rule['yearly_month'] = $yearly_month; 786 807 } 787 808 … … 969 990 $is_sel = ( ! $is_new && $selected && $selected['id'] === $e['id'] ); 970 991 $slug = (string) ( $e['slug'] ?? '' ); 971 $sc = $slug ? '[luzid_cs slug="' . esc_attr( $slug ) . '" opt="long"]' : '';992 $sc = $slug ? '[luzid_cs slug="' . esc_attr( $slug ) . '" date="long"]' : ''; 972 993 973 994 $active = $this->is_entry_active_now( $e, $now_ts ); … … 1033 1054 $slug = (string) ( $entry['slug'] ?? '' ); 1034 1055 $css_class = $slug ? 'luzid-cs-' . $slug : 'luzid-cs-…'; 1035 $sc_example = $slug ? '[luzid_cs slug="' . esc_attr( $slug ) . '" opt="long"]' : '[luzid_cs slug="…" opt="long"]'; 1056 $sc_example = $slug ? '[luzid_cs slug="' . esc_attr( $slug ) . '" date="long"]' : '[luzid_cs slug="…" date="long"]'; 1057 $include_in_table = ! empty( $entry['include_in_eventtable'] ); 1036 1058 1037 1059 echo '<div class="lcs-tab-content" data-panel="setup">'; … … 1059 1081 echo '<button type="button" class="lcs-copy-btn lcs-button-secondary lcs-btn-icon" data-lcs-copy="' . esc_attr( $sc_example ) . '"><span class="dashicons dashicons-admin-page" aria-hidden="true"></span></button>'; 1060 1082 echo '</div>'; 1061 echo '<span class="lcs-description">' . esc_html( $this->is_en ? 'Prints the next event dynamically (opt: short|date|time|long).' : 'Gibt den nächsten Event dynamisch aus (opt: short|date|time|long).' ) . '</span>'; 1083 echo '<span class="lcs-description">' . esc_html( $this->is_en ? 'Prints the next event dynamically (date: short|medium|long|full, time: auto|raw|prefix|range).' : 'Gibt den nächsten Event dynamisch aus (date: short|medium|long|full, time: auto|raw|prefix|range).' ) . '</span>'; 1084 echo '</td></tr>'; 1085 1086 // Include in Event Table 1087 echo '<tr><th>' . esc_html( $this->is_en ? 'Event Table' : 'Event-Tabelle' ) . '</th><td>'; 1088 echo '<label class="lcs-checkbox-label"><input type="checkbox" name="luzid_cs_include_in_eventtable" value="1" ' . checked( $include_in_table, true, false ) . '>'; 1089 echo ' ' . esc_html( $this->is_en ? 'Include in Event Table output' : 'In Event-Tabelle einbeziehen' ) . '</label>'; 1090 echo '<span class="lcs-description">' . esc_html( $this->is_en ? 'If checked, events from this scheduler will be included when using [luzid_cs_eventtable].' : 'Wenn aktiviert, werden Events dieses Schedulers bei [luzid_cs_eventtable] einbezogen.' ) . '</span>'; 1062 1091 echo '</td></tr>'; 1063 1092 … … 1077 1106 'exceptions' => [], 1078 1107 'event_offset' => [ 'enabled' => 0, 'days_before' => 0, 'start_time' => '00:01', 'days_after' => 0, 'end_time' => '' ], 1108 'include_in_eventtable' => 0, 1079 1109 ]; 1080 1110 } … … 1096 1126 echo '<div class="lcs-tab-content" data-panel="termine">'; 1097 1127 $this->render_panel_title( $this->panel_title_text( 'termine', $entry, $is_new ) ); 1098 echo '<p class="lcs-paragraph lcs-text">' . esc_html( $this->is_en ? 'Dates define ranges in which content can be shown. For single days fill only the from field and leave the to field empty.' : 'Termine definieren Zeiträume, an denen Content angezeigt werden kann. Für einzelne Tage nur das von Feld ausfüllen und das bis Feld freilassen.' ) . '</p>'; 1099 1100 echo '<div data-lcs-repeater="single">'; 1101 echo '<table class="lcs-table lcs-table--compact">'; 1102 echo '<thead><tr>'; 1103 echo '<th>' . esc_html( $this->is_en ? 'From' : 'Von' ) . '</th><th>' . esc_html( $this->is_en ? 'To' : 'Bis' ) . '</th><th>' . esc_html( $this->is_en ? 'Prefix' : 'Präfix' ) . '</th><th>' . esc_html( $this->is_en ? 'Time (optional)' : 'Zeit (optional)' ) . '</th><th>' . esc_html( $this->is_en ? 'Event text (optional)' : 'Eventtext (optional)' ) . '</th><th>' . esc_html( $this->is_en ? 'Action' : 'Aktion' ) . '</th>'; 1104 echo '</tr></thead>'; 1105 echo '<tbody data-lcs-rows>'; 1128 echo '<p class="lcs-paragraph lcs-text">' . esc_html( $this->is_en ? 'Define single dates when content should be visible.' : 'Definiere einzelne Termine, an denen Content sichtbar sein soll.' ) . '</p>'; 1129 1130 echo '<div data-lcs-repeater="single" class="lcs-cards-repeater">'; 1106 1131 1107 1132 if ( empty( $rows ) ) { 1108 $rows = [ [ 'from' => '', ' to' => '', 'prefix' => '', 'time' => '', 'text' => '' ] ];1133 $rows = [ [ 'from' => '', 'prefix' => '', 'time' => '', 'prefix2' => '', 'time_to' => '', 'text' => '' ] ]; 1109 1134 } 1110 1135 1111 1136 foreach ( $rows as $i => $r ) { 1112 1137 $from_raw = (string) ( $r['from'] ?? ( $r['date'] ?? '' ) ); 1113 $to_raw = (string) ( $r['to'] ?? '' );1114 1138 $prefix = (string) ( $r['prefix'] ?? '' ); 1115 1139 $time_raw = (string) ( $r['time'] ?? '' ); 1140 $prefix2 = (string) ( $r['prefix2'] ?? '' ); 1141 $time_to_raw = (string) ( $r['time_to'] ?? '' ); 1116 1142 $text_raw = (string) ( $r['text'] ?? '' ); 1117 echo '<tr data-lcs-row>'; 1118 echo '<td><input class="lcs-input" type="date" lang="' . esc_attr( $this->is_en ? 'en-GB' : 'de-DE' ) . '" name="luzid_cs_single[' . (int) $i . '][from]" value="' . esc_attr( $from_raw ) . '"></td>'; 1119 echo '<td><input class="lcs-input" type="date" lang="' . esc_attr( $this->is_en ? 'en-GB' : 'de-DE' ) . '" name="luzid_cs_single[' . (int) $i . '][to]" value="' . esc_attr( $to_raw ) . '"></td>'; 1120 echo '<td>' . $this->prefix_select( 'luzid_cs_single[' . (int) $i . '][prefix]', $prefix ) . '</td>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 1121 echo '<td><input class="lcs-input" type="time" name="luzid_cs_single[' . (int) $i . '][time]" value="' . esc_attr( $time_raw ) . '"></td>'; 1122 echo '<td><input class="lcs-input" type="text" name="luzid_cs_single[' . (int) $i . '][text]" value="' . esc_attr( $text_raw ) . '" placeholder="' . esc_attr( $this->is_en ? 'Optional' : 'Optional' ) . '"></td>'; 1123 echo '<td><div class="lcs-row-actions">'; 1124 echo '<a class="lcs-copy-btn lcs-button-secondary lcs-btn-icon" href="#" data-lcs-remove title="' . esc_attr( $this->is_en ? 'Remove row' : 'Zeile entfernen' ) . '"><span class="dashicons dashicons-trash"></span></a>'; 1125 echo '</div></td>'; 1126 echo '</tr>'; 1127 } 1128 1129 echo '</tbody>'; 1130 echo '</table>'; 1143 1144 echo '<div class="lcs-card" data-lcs-row>'; 1145 echo '<div class="lcs-card__row">'; 1146 echo '<div class="lcs-card__field"><label>' . esc_html( $this->is_en ? 'Date' : 'Datum' ) . '</label><input class="lcs-input" type="date" name="luzid_cs_single[' . (int) $i . '][from]" value="' . esc_attr( $from_raw ) . '"></div>'; 1147 echo '<div class="lcs-card__field"><label>' . esc_html( $this->is_en ? 'Prefix 1' : 'Präfix 1' ) . '</label>' . $this->prefix_select( 'luzid_cs_single[' . (int) $i . '][prefix]', $prefix ) . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 1148 echo '<div class="lcs-card__field"><label>' . esc_html( $this->is_en ? 'Time from' : 'Zeit von' ) . '</label><input class="lcs-input" type="time" name="luzid_cs_single[' . (int) $i . '][time]" value="' . esc_attr( $time_raw ) . '"></div>'; 1149 echo '<div class="lcs-card__field"><label>' . esc_html( $this->is_en ? 'Prefix 2' : 'Präfix 2' ) . '</label>' . $this->prefix2_select( 'luzid_cs_single[' . (int) $i . '][prefix2]', $prefix2 ) . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 1150 echo '<div class="lcs-card__field"><label>' . esc_html( $this->is_en ? 'Time to' : 'Zeit bis' ) . '</label><input class="lcs-input" type="time" name="luzid_cs_single[' . (int) $i . '][time_to]" value="' . esc_attr( $time_to_raw ) . '"></div>'; 1151 echo '</div>'; 1152 echo '<div class="lcs-card__row">'; 1153 echo '<div class="lcs-card__field lcs-card__field--wide"><label>' . esc_html( $this->is_en ? 'Event text' : 'Eventtext' ) . '</label><input class="lcs-input" type="text" name="luzid_cs_single[' . (int) $i . '][text]" value="' . esc_attr( $text_raw ) . '" placeholder="' . esc_attr( $this->is_en ? 'Optional' : 'Optional' ) . '"></div>'; 1154 echo '<div class="lcs-card__actions"><a class="lcs-button-secondary lcs-btn-icon" href="#" data-lcs-remove title="' . esc_attr( $this->is_en ? 'Remove' : 'Entfernen' ) . '"><span class="dashicons dashicons-trash"></span></a></div>'; 1155 echo '</div>'; 1156 echo '</div>'; 1157 } 1131 1158 1132 1159 echo '<template data-lcs-template>'; 1133 echo '<tr data-lcs-row>'; 1134 echo '<td><input class="lcs-input" type="date" lang="' . esc_attr( $this->is_en ? 'en-GB' : 'de-DE' ) . '" name="luzid_cs_single[__i__][from]" value=""></td>'; 1135 echo '<td><input class="lcs-input" type="date" lang="' . esc_attr( $this->is_en ? 'en-GB' : 'de-DE' ) . '" name="luzid_cs_single[__i__][to]" value=""></td>'; 1136 echo '<td>' . $this->prefix_select( 'luzid_cs_single[__i__][prefix]', '' ) . '</td>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 1137 echo '<td><input class="lcs-input" type="time" name="luzid_cs_single[__i__][time]" value=""></td>'; 1138 echo '<td><input class="lcs-input" type="text" name="luzid_cs_single[__i__][text]" value="" placeholder="' . esc_attr( $this->is_en ? 'Optional' : 'Optional' ) . '"></td>'; 1139 echo '<td><div class="lcs-row-actions">'; 1140 echo '<a class="lcs-copy-btn lcs-button-secondary lcs-btn-icon" href="#" data-lcs-remove title="' . esc_attr( $this->is_en ? 'Remove row' : 'Zeile entfernen' ) . '"><span class="dashicons dashicons-trash"></span></a>'; 1141 echo '</div></td>'; 1142 echo '</tr>'; 1160 echo '<div class="lcs-card" data-lcs-row>'; 1161 echo '<div class="lcs-card__row">'; 1162 echo '<div class="lcs-card__field"><label>' . esc_html( $this->is_en ? 'Date' : 'Datum' ) . '</label><input class="lcs-input" type="date" name="luzid_cs_single[__i__][from]" value=""></div>'; 1163 echo '<div class="lcs-card__field"><label>' . esc_html( $this->is_en ? 'Prefix 1' : 'Präfix 1' ) . '</label>' . $this->prefix_select( 'luzid_cs_single[__i__][prefix]', '' ) . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 1164 echo '<div class="lcs-card__field"><label>' . esc_html( $this->is_en ? 'Time from' : 'Zeit von' ) . '</label><input class="lcs-input" type="time" name="luzid_cs_single[__i__][time]" value=""></div>'; 1165 echo '<div class="lcs-card__field"><label>' . esc_html( $this->is_en ? 'Prefix 2' : 'Präfix 2' ) . '</label>' . $this->prefix2_select( 'luzid_cs_single[__i__][prefix2]', '' ) . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 1166 echo '<div class="lcs-card__field"><label>' . esc_html( $this->is_en ? 'Time to' : 'Zeit bis' ) . '</label><input class="lcs-input" type="time" name="luzid_cs_single[__i__][time_to]" value=""></div>'; 1167 echo '</div>'; 1168 echo '<div class="lcs-card__row">'; 1169 echo '<div class="lcs-card__field lcs-card__field--wide"><label>' . esc_html( $this->is_en ? 'Event text' : 'Eventtext' ) . '</label><input class="lcs-input" type="text" name="luzid_cs_single[__i__][text]" value="" placeholder="' . esc_attr( $this->is_en ? 'Optional' : 'Optional' ) . '"></div>'; 1170 echo '<div class="lcs-card__actions"><a class="lcs-button-secondary lcs-btn-icon" href="#" data-lcs-remove title="' . esc_attr( $this->is_en ? 'Remove' : 'Entfernen' ) . '"><span class="dashicons dashicons-trash"></span></a></div>'; 1171 echo '</div>'; 1172 echo '</div>'; 1143 1173 echo '</template>'; 1144 1174 … … 1158 1188 echo '<p class="lcs-paragraph lcs-text">' . esc_html( $this->is_en ? 'Multiple recurring rules (OR logic). Each rule has its own period (from/to) that restricts only that rule.' : 'Mehrere Wiederholungsregeln (ODER-Logik). Jede Regel hat einen eigenen Zeitraum (von/bis), der nur diese Regel einschränkt.' ) . '</p>'; 1159 1189 1160 echo '<div data-lcs-repeater="recurring">'; 1161 echo '<table class="lcs-table lcs-table--compact">'; 1162 echo '<thead><tr>'; 1163 echo '<th>' . esc_html( $this->is_en ? 'Type' : 'Typ' ) . '</th><th>' . esc_html( $this->is_en ? 'Details' : 'Details' ) . '</th><th>' . esc_html( $this->is_en ? 'Rule period' : 'Regel-Zeitraum' ) . '</th><th>' . esc_html( $this->is_en ? 'Action' : 'Aktion' ) . '</th>'; 1164 echo '</tr></thead>'; 1165 echo '<tbody data-lcs-rows>'; 1190 echo '<div data-lcs-repeater="recurring" class="lcs-cards-repeater">'; 1166 1191 1167 1192 if ( empty( $rows ) ) { 1168 $rows = [ [ 'type' => '', 'weekdays' => [], 'prefix' => '', 'time' => '', ' text' => '', 'from' => '', 'to' => '' ] ];1193 $rows = [ [ 'type' => '', 'weekdays' => [], 'prefix' => '', 'time' => '', 'prefix2' => '', 'time_to' => '', 'text' => '', 'from' => '', 'to' => '' ] ]; 1169 1194 } 1170 1195 1171 1196 foreach ( $rows as $i => $r ) { 1172 echo $this->render_recurring_row( $i, $r ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 1173 } 1174 1175 echo '</tbody>'; 1176 echo '</table>'; 1197 echo $this->render_recurring_card( $i, $r ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 1198 } 1177 1199 1178 1200 echo '<template data-lcs-template>'; 1179 echo $this->render_recurring_ row( '__i__', [ 'type' => '', 'weekdays' => [], 'prefix' => '', 'time' => '', 'text' => '', 'from' => '', 'to' => '' ], true ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped1201 echo $this->render_recurring_card( '__i__', [ 'type' => '', 'weekdays' => [], 'prefix' => '', 'time' => '', 'prefix2' => '', 'time_to' => '', 'text' => '', 'from' => '', 'to' => '' ], true ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 1180 1202 echo '</template>'; 1181 1203 … … 1188 1210 } 1189 1211 1190 private function render_recurring_ row( $i, $r, $is_template = false ) {1212 private function render_recurring_card( $i, $r, $is_template = false ) { 1191 1213 $idx = $i; 1192 1214 $name = function( $field ) use ( $idx ) { return 'luzid_cs_recurring[' . $idx . '][' . $field . ']'; }; … … 1195 1217 $prefix = (string) ( $r['prefix'] ?? '' ); 1196 1218 $time = (string) ( $r['time'] ?? '' ); 1219 $prefix2 = (string) ( $r['prefix2'] ?? '' ); 1220 $time_to = (string) ( $r['time_to'] ?? '' ); 1197 1221 $text = (string) ( $r['text'] ?? '' ); 1198 1222 $from = (string) ( $r['from'] ?? '' ); 1199 1223 $to = (string) ( $r['to'] ?? '' ); 1200 1224 1201 $html = '';1202 1225 $is_invalid = ( ! $is_template && $type !== '' && $time === '' ); 1203 $tr_class = $is_invalid ? ' class="lcs-row--invalid"' : ''; 1204 $html .= '<tr data-lcs-row' . $tr_class . '>'; 1205 1206 // Type 1207 $html .= '<td>'; 1226 $card_class = $is_invalid ? 'lcs-card lcs-card--invalid' : 'lcs-card'; 1227 1228 $html = '<div class="' . esc_attr( $card_class ) . '" data-lcs-row>'; 1229 1230 // Warning for incomplete rules 1231 if ( $is_invalid ) { 1232 $html .= '<div class="lcs-rec-inline-warning">'; 1233 $html .= '<span class="dashicons dashicons-warning"></span>'; 1234 $html .= '<strong>' . esc_html( $this->is_en ? 'Incomplete: missing time – this rule will be ignored.' : 'Unvollständig: Uhrzeit fehlt – diese Regel wird ignoriert.' ) . '</strong>'; 1235 $html .= '</div>'; 1236 } 1237 1238 // Row 1: Type + Details + Prefix 1 + Time from + Prefix 2 + Time to 1239 $html .= '<div class="lcs-card__row">'; 1240 1241 // Type select 1242 $html .= '<div class="lcs-card__field">'; 1243 $html .= '<label>' . esc_html( $this->is_en ? 'Type' : 'Typ' ) . '</label>'; 1208 1244 $html .= '<select class="lcs-select lcs-rec-type" name="' . esc_attr( $name('type') ) . '">'; 1209 $html .= '<option value="">' . esc_html( $this->is_en ? '— Choose type —' : '— Typ wählen —' ) . '</option>';1245 $html .= '<option value="">' . esc_html( $this->is_en ? '— Choose —' : '— Wählen —' ) . '</option>'; 1210 1246 $html .= '<option value="weekly" ' . selected( $type, 'weekly', false ) . '>' . esc_html( $this->is_en ? 'Weekly' : 'Wöchentlich' ) . '</option>'; 1211 1247 $html .= '<option value="day_of_month" ' . selected( $type, 'day_of_month', false ) . '>' . esc_html( $this->is_en ? 'Monthly' : 'Monatlich' ) . '</option>'; 1212 1248 $html .= '<option value="nth_weekday_month" ' . selected( $type, 'nth_weekday_month', false ) . '>' . esc_html( $this->is_en ? 'Weekday in month' : 'Wochentag im Monat' ) . '</option>'; 1249 $html .= '<option value="yearly" ' . selected( $type, 'yearly', false ) . '>' . esc_html( $this->is_en ? 'Yearly' : 'Jährlich' ) . '</option>'; 1213 1250 $html .= '</select>'; 1214 $html .= '</td>'; 1215 1216 // Details 1217 $html .= '<td class="lcs-rec-details">'; 1218 if ( $is_invalid ) { 1219 $html .= '<div class="lcs-rec-inline-warning" style="margin-bottom:8px; padding:8px 10px; background:#fff8e5; border:1px solid #dba617; border-radius:4px; color:#6b5a00; font-size:13px;">'; 1220 $html .= '<span class="dashicons dashicons-warning" style="margin-right:6px; vertical-align:middle;"></span>'; 1221 $html .= '<strong>' . esc_html( $this->is_en ? 'Incomplete: missing time – this rule will be ignored.' : 'Unvollständig: Uhrzeit fehlt – diese Regel wird ignoriert.' ) . '</strong>'; 1222 $html .= '</div>'; 1223 } 1224 1225 1226 // Weekly details (Dropdown, single) 1251 $html .= '</div>'; 1252 1253 // Details (dynamic based on type) 1227 1254 $weekday_single = (int) ( $r['weekday'] ?? 0 ); 1228 1255 if ( $weekday_single < 1 || $weekday_single > 7 ) { … … 1232 1259 } 1233 1260 1234 $weekly_style = ( $type === 'weekly' ) ? 'display: inline-flex;' : 'display: none;'; 1235 $dom_style = ( $type === 'day_of_month' ) ? 'display: inline-flex;' : 'display: none;'; 1236 $nth_style = ( $type === 'nth_weekday_month' ) ? 'display: inline-flex;' : 'display: none;'; 1237 1238 $html .= '<span class="lcs-inline-inputs" data-details="weekly" style="' . esc_attr( $weekly_style ) . '">'; 1239 $html .= '<span class="lcs-help-inline">' . esc_html( $this->is_en ? 'Every ' : 'Jeden ' ) . '</span> '; 1261 $weekly_style = ( $type === 'weekly' ) ? '' : 'display: none;'; 1262 $dom_style = ( $type === 'day_of_month' ) ? '' : 'display: none;'; 1263 $nth_style = ( $type === 'nth_weekday_month' ) ? '' : 'display: none;'; 1264 $yearly_style = ( $type === 'yearly' ) ? '' : 'display: none;'; 1265 1266 // Weekly: Weekday 1267 $html .= '<div class="lcs-card__field" data-details="weekly" style="' . esc_attr( $weekly_style ) . '">'; 1268 $html .= '<label>' . esc_html( $this->is_en ? 'Weekday' : 'Wochentag' ) . '</label>'; 1240 1269 $html .= $this->weekday_select( $name('weekday'), $weekday_single ); 1241 $html .= '</ span>';1242 1243 // Day-of-month details (Dropdown)1270 $html .= '</div>'; 1271 1272 // Monthly: Day of month 1244 1273 $dom = (int) ( $r['day'] ?? 1 ); 1245 1274 if ( $dom < 1 || $dom > 31 ) { $dom = 1; } 1246 $html .= '< span class="lcs-inline-inputs" data-details="day_of_month" style="' . esc_attr( $dom_style ) . '">';1247 $html .= '< span class="lcs-help-inline">' . esc_html( $this->is_en ? 'Every ' : 'Jeden ' ) . '</span>';1275 $html .= '<div class="lcs-card__field" data-details="day_of_month" style="' . esc_attr( $dom_style ) . '">'; 1276 $html .= '<label>' . esc_html( $this->is_en ? 'Day' : 'Tag' ) . '</label>'; 1248 1277 $html .= $this->day_of_month_select( $name('day'), $dom ); 1249 $html .= '<span class="lcs-help-inline"> </span>'; 1250 $html .= '</span>'; 1251 1252 // Nth weekday details 1278 $html .= '</div>'; 1279 1280 // Nth weekday: Nth + Weekday 1253 1281 $nth = (int) ( $r['nth'] ?? 1 ); 1254 1282 $weekday = (int) ( $r['weekday'] ?? 4 ); 1255 $html .= '< span class="lcs-inline-inputs" data-details="nth_weekday_month" style="' . esc_attr( $nth_style ) . '">';1256 $html .= '< span class="lcs-help-inline">' . esc_html( $this->is_en ? 'Every' : 'Jeden' ) . '</span>';1283 $html .= '<div class="lcs-card__field" data-details="nth_weekday_month" style="' . esc_attr( $nth_style ) . '">'; 1284 $html .= '<label>' . esc_html( $this->is_en ? 'Which' : 'Welcher' ) . '</label>'; 1257 1285 $html .= '<select class="lcs-select" name="' . esc_attr( $name('nth') ) . '">'; 1258 1286 $nth_labels = [ 1 => '1.', 2 => '2.', 3 => '3.', 4 => '4.', 5 => '5.', -1 => ( $this->is_en ? 'last' : 'letzter' ) ]; … … 1261 1289 } 1262 1290 $html .= '</select>'; 1263 1264 $html .= '<span class="lcs-help-inline"> </span> '; 1291 $html .= '</div>'; 1292 1293 $html .= '<div class="lcs-card__field" data-details="nth_weekday_month" style="' . esc_attr( $nth_style ) . '">'; 1294 $html .= '<label>' . esc_html( $this->is_en ? 'Weekday' : 'Wochentag' ) . '</label>'; 1265 1295 $html .= $this->weekday_select( $name('weekday'), $weekday ); 1266 $html .= '</span>'; 1267 1268 // Common: prefix + time (single fields, avoids duplicate POST values) 1269 $html .= '<span class="lcs-inline-inputs lcs-rec-common" style="' . ( $type !== '' ? 'display: inline-flex;' : 'display: none;' ) . '">'; 1270 $html .= '<span class="lcs-sep"> </span>'; 1296 $html .= '</div>'; 1297 1298 // Yearly: Day + Month 1299 $yearly_day = (int) ( $r['yearly_day'] ?? 1 ); 1300 $yearly_month = (int) ( $r['yearly_month'] ?? 1 ); 1301 if ( $yearly_day < 1 || $yearly_day > 31 ) { $yearly_day = 1; } 1302 if ( $yearly_month < 1 || $yearly_month > 12 ) { $yearly_month = 1; } 1303 1304 $html .= '<div class="lcs-card__field" data-details="yearly" style="' . esc_attr( $yearly_style ) . '">'; 1305 $html .= '<label>' . esc_html( $this->is_en ? 'Day' : 'Tag' ) . '</label>'; 1306 $html .= $this->day_of_month_select( $name('yearly_day'), $yearly_day ); 1307 $html .= '</div>'; 1308 1309 $html .= '<div class="lcs-card__field" data-details="yearly" style="' . esc_attr( $yearly_style ) . '">'; 1310 $html .= '<label>' . esc_html( $this->is_en ? 'Month' : 'Monat' ) . '</label>'; 1311 $html .= '<select class="lcs-select" name="' . esc_attr( $name('yearly_month') ) . '">'; 1312 $months = $this->is_en 1313 ? [ 1 => 'January', 2 => 'February', 3 => 'March', 4 => 'April', 5 => 'May', 6 => 'June', 7 => 'July', 8 => 'August', 9 => 'September', 10 => 'October', 11 => 'November', 12 => 'December' ] 1314 : [ 1 => 'Januar', 2 => 'Februar', 3 => 'März', 4 => 'April', 5 => 'Mai', 6 => 'Juni', 7 => 'Juli', 8 => 'August', 9 => 'September', 10 => 'Oktober', 11 => 'November', 12 => 'Dezember' ]; 1315 foreach ( $months as $m => $label ) { 1316 $html .= '<option value="' . esc_attr( (string) $m ) . '" ' . selected( $yearly_month, $m, false ) . '>' . esc_html( $label ) . '</option>'; 1317 } 1318 $html .= '</select>'; 1319 $html .= '</div>'; 1320 1321 // Time fields in Row 1 (visible when type is selected) 1322 $html .= '<div class="lcs-card__field lcs-rec-common" data-rec-time style="' . ( $type !== '' ? '' : 'display: none;' ) . '">'; 1323 $html .= '<label>' . esc_html( $this->is_en ? 'Prefix 1' : 'Präfix 1' ) . '</label>'; 1271 1324 $html .= $this->prefix_select( $name('prefix'), $prefix ); 1272 $html .= '<span class="lcs-help-inline"> </span> '; 1325 $html .= '</div>'; 1326 1327 $html .= '<div class="lcs-card__field lcs-rec-common" data-rec-time style="' . ( $type !== '' ? '' : 'display: none;' ) . '">'; 1328 $html .= '<label>' . esc_html( $this->is_en ? 'Time from' : 'Zeit von' ) . '</label>'; 1273 1329 $html .= '<input class="lcs-input" type="time" name="' . esc_attr( $name('time') ) . '" value="' . esc_attr( $time ) . '">'; 1274 1275 // Optional event text for this recurring rule (shown in shortcode output when text="true") 1276 $html .= '<input class="lcs-input" type="text" name="' . esc_attr( $name('text') ) . '" value="' . esc_attr( $text ) . '" placeholder="' . esc_attr( $this->is_en ? 'Event text (optional)' : 'Eventtext (optional)' ) . '">'; 1277 $html .= '</span>'; 1278 1279 $html .= '</td>'; 1280 1281 // Period 1282 $html .= '<td>'; 1283 $html .= '<span class="lcs-inline-inputs">'; 1284 $html .= '<span class="lcs-help-inline">' . esc_html( $this->is_en ? 'from' : 'von' ) . '</span> <input class="lcs-input" type="date" lang="' . esc_attr( $this->is_en ? 'en-GB' : 'de-DE' ) . '" name="' . esc_attr( $name('from') ) . '" value="' . esc_attr( $from ) . '">'; 1285 $html .= '<span class="lcs-help-inline">' . esc_html( $this->is_en ? 'to' : 'bis' ) . '</span> <input class="lcs-input" type="date" lang="' . esc_attr( $this->is_en ? 'en-GB' : 'de-DE' ) . '" name="' . esc_attr( $name('to') ) . '" value="' . esc_attr( $to ) . '">'; 1286 $html .= '</span>'; 1287 $html .= '</td>'; 1288 1289 // Action 1290 $html .= '<td><div class="lcs-row-actions">'; 1291 $html .= '<a class="lcs-copy-btn lcs-button-secondary lcs-btn-icon" href="#" data-lcs-remove title="' . esc_attr( $this->is_en ? 'Remove row' : 'Zeile entfernen' ) . '"><span class="dashicons dashicons-trash"></span></a>'; 1292 $html .= '</div></td>'; 1293 1294 $html .= '</tr>'; 1330 $html .= '</div>'; 1331 1332 $html .= '<div class="lcs-card__field lcs-rec-common" data-rec-time style="' . ( $type !== '' ? '' : 'display: none;' ) . '">'; 1333 $html .= '<label>' . esc_html( $this->is_en ? 'Prefix 2' : 'Präfix 2' ) . '</label>'; 1334 $html .= $this->prefix2_select( $name('prefix2'), $prefix2 ); 1335 $html .= '</div>'; 1336 1337 $html .= '<div class="lcs-card__field lcs-rec-common" data-rec-time style="' . ( $type !== '' ? '' : 'display: none;' ) . '">'; 1338 $html .= '<label>' . esc_html( $this->is_en ? 'Time to' : 'Zeit bis' ) . '</label>'; 1339 $html .= '<input class="lcs-input" type="time" name="' . esc_attr( $name('time_to') ) . '" value="' . esc_attr( $time_to ) . '">'; 1340 $html .= '</div>'; 1341 1342 $html .= '</div>'; // End Row 1 1343 1344 // Row 2: Event text, Valid from, Valid to, Action 1345 $html .= '<div class="lcs-card__row lcs-rec-common" style="' . ( $type !== '' ? '' : 'display: none;' ) . '">'; 1346 1347 $html .= '<div class="lcs-card__field lcs-card__field--wide">'; 1348 $html .= '<label>' . esc_html( $this->is_en ? 'Event text' : 'Eventtext' ) . '</label>'; 1349 $html .= '<input class="lcs-input" type="text" name="' . esc_attr( $name('text') ) . '" value="' . esc_attr( $text ) . '" placeholder="' . esc_attr( $this->is_en ? 'Optional' : 'Optional' ) . '">'; 1350 $html .= '</div>'; 1351 1352 $html .= '<div class="lcs-card__field">'; 1353 $html .= '<label>' . esc_html( $this->is_en ? 'Valid from' : 'Gültig von' ) . '</label>'; 1354 $html .= '<input class="lcs-input" type="date" name="' . esc_attr( $name('from') ) . '" value="' . esc_attr( $from ) . '">'; 1355 $html .= '</div>'; 1356 1357 $html .= '<div class="lcs-card__field">'; 1358 $html .= '<label>' . esc_html( $this->is_en ? 'Valid to' : 'Gültig bis' ) . '</label>'; 1359 $html .= '<input class="lcs-input" type="date" name="' . esc_attr( $name('to') ) . '" value="' . esc_attr( $to ) . '">'; 1360 $html .= '</div>'; 1361 1362 $html .= '<div class="lcs-card__actions">'; 1363 $html .= '<a class="lcs-button-secondary lcs-btn-icon" href="#" data-lcs-remove title="' . esc_attr( $this->is_en ? 'Remove' : 'Entfernen' ) . '"><span class="dashicons dashicons-trash"></span></a>'; 1364 $html .= '</div>'; 1365 1366 $html .= '</div>'; // End Row 2 1367 1368 $html .= '</div>'; // End Card 1295 1369 1296 1370 return $html; … … 1348 1422 } 1349 1423 private function render_panel_event_offset( $entry, $is_new ) { 1350 $offset = isset( $entry['event_offset'] ) && is_array( $entry['event_offset'] ) ? $entry['event_offset'] : [ 'enabled' => 0, 'days_before' => 0, 'start_time' => '00:01', 'days_after' => 0, 'end_time' => '' ]; 1351 $enabled = ! empty( $offset['enabled'] ); 1352 $days_before = (int) ( $offset['days_before'] ?? ( $offset['days'] ?? 0 ) ); // backward compat 1353 $days_before = max( 0, $days_before ); 1354 $start_time = (string) ( $offset['start_time'] ?? '00:01' ); 1355 $days_after = (int) ( $offset['days_after'] ?? 0 ); 1356 $days_after = max( 0, $days_after ); 1357 $end_time = (string) ( $offset['end_time'] ?? '' ); 1424 $event_classes = isset( $entry['event_classes'] ) && is_array( $entry['event_classes'] ) ? $entry['event_classes'] : []; 1425 $slug = (string) ( $entry['slug'] ?? '' ); 1426 1427 if ( empty( $event_classes ) ) { 1428 $event_classes = [[ 'name' => 'standard', 'days_before' => 0, 'start_time' => '', 'days_after' => 0, 'end_time' => '' ]]; 1429 } 1358 1430 1359 1431 $next = $this->compute_next_event( $entry, time() ); … … 1361 1433 echo '<div class="lcs-tab-content" data-panel="event-offset">'; 1362 1434 $this->render_panel_title( $this->panel_title_text( 'event-offset', $entry, $is_new ) ); 1363 echo '<p class="lcs-paragraph lcs-text">' . ( $this->is_en ? 'Show content X days <strong>before</strong> the calculated event – starting at a fixed start time. The offset affects only the visibility window, not the text output of the shortcode.' : 'Zeige Content X Tage <strong>vor</strong> dem berechneten Event – ab einer festen Startzeit. Der Offset beeinflusst nur das Sichtbarkeitsfenster, nicht die Text-Ausgabe im Shortcode.' ) . '</p>'; 1364 1365 echo '<table class="lcs-form-table">'; 1366 1367 echo '<tr><th>' . esc_html( $this->is_en ? 'Enable event offset' : 'Event-Offset aktivieren' ) . '</th><td>'; 1368 echo '<div class="lcs-checkbox-wrapper"><input type="checkbox" name="luzid_cs_offset_enabled" value="1" ' . checked( $enabled, true, false ) . '><span>aktiv</span></div>'; 1369 echo '</td></tr>'; 1370 1371 echo '<tr><th>' . esc_html( $this->is_en ? 'Days before / start time' : 'Tage vorher / Startzeit' ) . '</th><td>'; 1372 echo '<div class="lcs-inline-inputs">'; 1373 echo '<input type="number" min="0" max="365" name="luzid_cs_offset_days_before" value="' . esc_attr( (string) $days_before ) . '" style="max-width:120px;">'; 1374 echo '<input type="time" name="luzid_cs_offset_start_time" value="' . esc_attr( $start_time ) . '" style="max-width:160px;">'; 1435 1436 $desc_de = 'Definiere über Klassen die Sichtbarkeit von Content | Popups | Alerts | Menüeinträgen. Bestimme das Zeitfenster der Sichtbarkeit vor und nach dem Event über Offsets. Die Klassen beeinflussen nur das Sichtbarkeitsfenster, nicht die Text-Ausgabe im Shortcode.'; 1437 $desc_en = 'Define classes to control visibility of Content | Popups | Alerts | Menu items. Set the visibility time window before and after the event using offsets. Classes only affect the visibility window, not the text output of the shortcode.'; 1438 echo '<p class="lcs-paragraph lcs-text">' . esc_html( $this->is_en ? $desc_en : $desc_de ) . '</p>'; 1439 1440 echo '<div class="lcs-repeater" data-lcs-repeater="event_classes">'; 1441 1442 foreach ( $event_classes as $i => $cls ) { 1443 echo $this->render_event_class_card( $i, $cls, $slug, $next, false ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 1444 } 1445 1446 echo '<template data-lcs-template>'; 1447 echo $this->render_event_class_card( '__i__', [ 'name' => '', 'days_before' => 0, 'start_time' => '', 'days_after' => 0, 'end_time' => '' ], $slug, $next, true ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 1448 echo '</template>'; 1449 1450 echo '<div class="lcs-actions">'; 1451 echo '<button type="button" class="lcs-button-small lcs-btn-icon" data-lcs-add><span class="dashicons dashicons-plus-alt2" aria-hidden="true"></span><span>' . esc_html( $this->is_en ? 'Add class' : 'Klasse hinzufügen' ) . '</span></button>'; 1375 1452 echo '</div>'; 1376 echo '<div class="lcs-description">' . ( $this->is_en ? 'Start of visibility: <strong>(event date minus days before)</strong> from start time.' : 'Start der Sichtbarkeit: <strong>(Event-Datum minus Tage vorher)</strong> ab Startzeit.' ) . '</div>'; 1377 echo '</td></tr>'; 1378 1379 echo '<tr><th>' . esc_html( $this->is_en ? 'Days after / end time' : 'Tage nachher / Endzeit' ) . '</th><td>'; 1380 echo '<div class="lcs-inline-inputs">'; 1381 echo '<input type="number" min="0" max="365" name="luzid_cs_offset_days_after" value="' . esc_attr( (string) $days_after ) . '" style="max-width:120px;">'; 1382 echo '<input type="time" name="luzid_cs_offset_end_time" value="' . esc_attr( $end_time ) . '" style="max-width:160px;" placeholder="">'; 1453 1383 1454 echo '</div>'; 1384 echo '<div class="lcs-description">' . ( $this->is_en ? 'End of visibility: <strong>(event date plus days after)</strong> at end time. If empty, visibility ends at the event time (for event ranges: the last day).' : 'Ende der Sichtbarkeit: <strong>(Event-Datum plus Tage nachher)</strong> zur Endzeit. Wenn Endzeit leer ist, gilt der Event-Zeitpunkt (bei Zeiträumen: der letzte Tag).' ) . '</div>'; 1385 echo '</td></tr>'; 1386 1387 echo '<tr><th>' . esc_html( $this->is_en ? 'Next event preview' : 'Vorschau nächster Event' ) . '</th><td>'; 1388 if ( $next && ! empty( $next['ts'] ) ) { 1389 echo '<strong>' . esc_html( $this->format_event_datetime( $next['ts'], $next['prefix'], $next['time'], 'long', $next ) ) . '</strong>'; 1390 1391 $use_offset = $enabled && ( $days_before > 0 || $days_after > 0 || $end_time !== '' ); 1392 if ( $use_offset ) { 1393 $window = $this->compute_offset_window( $entry, $next ); 1394 if ( $window ) { 1395 $tz = wp_timezone(); 1396 echo '<div class="lcs-description">' . esc_html( $this->is_en ? 'Visible from:' : 'Sichtbar von:' ) . ' ' . esc_html( $this->wp_date_lang( 'd.m.Y H:i', (int) $window['start'], $tz ) ) . ' – ' . esc_html( $this->is_en ? 'until:' : 'bis:' ) . ' ' . esc_html( $this->wp_date_lang( 'd.m.Y H:i', (int) $window['end'], $tz ) ) . '</div>'; 1397 } 1398 } 1455 1456 echo '<div class="lcs-callout" style="margin-top: 20px;">'; 1457 echo '<p><strong>' . esc_html( $this->is_en ? 'Start of visibility:' : 'Start der Sichtbarkeit:' ) . '</strong> '; 1458 echo esc_html( $this->is_en ? '(Event date minus days before) from start time. If start time is empty, visibility starts at 00:00.' : '(Event-Datum minus Tage vorher) ab Startzeit. Wenn Startzeit leer ist, beginnt die Sichtbarkeit um 00:00 Uhr.' ) . '</p>'; 1459 echo '<p><strong>' . esc_html( $this->is_en ? 'End of visibility:' : 'Ende der Sichtbarkeit:' ) . '</strong> '; 1460 echo esc_html( $this->is_en ? '(Event date plus days after) at end time. If end time is empty, visibility ends at the event time (for date ranges: the last day).' : '(Event-Datum plus Tage nachher) zur Endzeit. Wenn Endzeit leer ist, gilt der Event-Zeitpunkt (bei Zeiträumen: der letzte Tag).' ) . '</p>'; 1461 echo '</div>'; 1462 1463 echo '</div>'; 1464 } 1465 1466 private function render_event_class_card( $i, $cls, $scheduler_slug, $next_event, $is_template = false ) { 1467 $idx = $i; 1468 $name = function( $field ) use ( $idx ) { return 'luzid_cs_event_classes[' . $idx . '][' . $field . ']'; }; 1469 1470 $class_name = (string) ( $cls['name'] ?? '' ); 1471 $days_before = (int) ( $cls['days_before'] ?? 0 ); 1472 $start_time = (string) ( $cls['start_time'] ?? '' ); 1473 $days_after = (int) ( $cls['days_after'] ?? 0 ); 1474 $end_time = (string) ( $cls['end_time'] ?? '' ); 1475 1476 $is_standard = ( $class_name === 'standard' || ( $i === 0 && ! $is_template ) ); 1477 $css_class = $is_standard ? 'luzid-cs-' . $scheduler_slug : 'luzid-cs-' . $scheduler_slug . '-' . $class_name; 1478 1479 $html = '<div class="lcs-card" data-lcs-row>'; 1480 $html .= '<div class="lcs-card__row">'; 1481 1482 // Name field 1483 $html .= '<div class="lcs-card__field lcs-card__field--grow">'; 1484 $html .= '<label>' . esc_html( $this->is_en ? 'Name' : 'Name' ) . '</label>'; 1485 if ( $is_standard && ! $is_template ) { 1486 $html .= '<input type="text" class="lcs-input" value="Standard" disabled style="background: #f0f0f0;">'; 1487 $html .= '<input type="hidden" name="' . esc_attr( $name('name') ) . '" value="standard">'; 1488 $html .= '<input type="hidden" name="' . esc_attr( $name('is_standard') ) . '" value="1">'; 1399 1489 } else { 1400 echo '<span class="lcs-description">' . esc_html( $this->tr( 'no_date_available' ) ) . '</span>'; 1401 } 1402 echo '</td></tr>'; 1403 1404 echo '</table>'; 1405 1406 echo '</div>'; 1490 $html .= '<input type="text" class="lcs-input lcs-event-class-name" name="' . esc_attr( $name('name') ) . '" value="' . esc_attr( $class_name ) . '" placeholder="' . esc_attr( $this->is_en ? 'e.g. popup, banner, menu' : 'z.B. popup, banner, menu' ) . '">'; 1491 } 1492 $html .= '</div>'; 1493 1494 // CSS Class preview 1495 $html .= '<div class="lcs-card__field lcs-card__field--grow">'; 1496 $html .= '<label>' . esc_html( $this->is_en ? 'CSS Class' : 'CSS-Klasse' ) . '</label>'; 1497 $html .= '<code class="lcs-event-class-preview" style="display: inline-block; padding: 6px 10px; background: #f5f5f5; border-radius: 4px;">.' . esc_html( $css_class ) . '</code>'; 1498 $html .= '</div>'; 1499 1500 // Action buttons: Copy + Delete 1501 $html .= '<div class="lcs-card__field lcs-card__field--actions">'; 1502 $html .= '<div class="lcs-row-actions">'; 1503 // Copy button (always visible) 1504 $html .= '<button type="button" class="lcs-button-secondary lcs-copy-btn lcs-btn-icon" data-lcs-copy="' . esc_attr( $css_class ) . '" title="' . esc_attr( $this->is_en ? 'Copy CSS class' : 'CSS-Klasse kopieren' ) . '"><span class="dashicons dashicons-admin-page"></span></button>'; 1505 // Delete button (not for standard, unless template) 1506 if ( ! $is_standard || $is_template ) { 1507 $html .= '<a class="lcs-button-secondary lcs-btn-icon" href="#" data-lcs-remove title="' . esc_attr( $this->is_en ? 'Remove' : 'Entfernen' ) . '"><span class="dashicons dashicons-trash"></span></a>'; 1508 } 1509 $html .= '</div>'; 1510 $html .= '</div>'; 1511 1512 $html .= '</div>'; 1513 1514 // Row 2: Days before + Start time + Days after + End time 1515 $html .= '<div class="lcs-card__row">'; 1516 1517 $html .= '<div class="lcs-card__field">'; 1518 $html .= '<label>' . esc_html( $this->is_en ? 'Days before' : 'Tage vorher' ) . '</label>'; 1519 $html .= '<input type="number" class="lcs-input" name="' . esc_attr( $name('days_before') ) . '" value="' . esc_attr( (string) $days_before ) . '" min="0" max="365" style="max-width: 80px;">'; 1520 $html .= '</div>'; 1521 1522 $html .= '<div class="lcs-card__field">'; 1523 $html .= '<label>' . esc_html( $this->is_en ? 'Start time' : 'Startzeit' ) . '</label>'; 1524 $html .= '<input type="time" class="lcs-input" name="' . esc_attr( $name('start_time') ) . '" value="' . esc_attr( $start_time ) . '" style="max-width: 120px;">'; 1525 $html .= '</div>'; 1526 1527 $html .= '<div class="lcs-card__field">'; 1528 $html .= '<label>' . esc_html( $this->is_en ? 'Days after' : 'Tage nachher' ) . '</label>'; 1529 $html .= '<input type="number" class="lcs-input" name="' . esc_attr( $name('days_after') ) . '" value="' . esc_attr( (string) $days_after ) . '" min="0" max="365" style="max-width: 80px;">'; 1530 $html .= '</div>'; 1531 1532 $html .= '<div class="lcs-card__field">'; 1533 $html .= '<label>' . esc_html( $this->is_en ? 'End time' : 'Endzeit' ) . '</label>'; 1534 $html .= '<input type="time" class="lcs-input" name="' . esc_attr( $name('end_time') ) . '" value="' . esc_attr( $end_time ) . '" style="max-width: 120px;">'; 1535 $html .= '</div>'; 1536 1537 $html .= '</div>'; 1538 1539 // Row 3: Preview 1540 if ( $next_event && ! empty( $next_event['ts'] ) && ! $is_template ) { 1541 $window = $this->compute_class_offset_window( $cls, $next_event ); 1542 if ( $window ) { 1543 $tz = wp_timezone(); 1544 $html .= '<div class="lcs-card__row lcs-card__row--preview">'; 1545 $html .= '<div class="lcs-card__field lcs-card__field--full">'; 1546 $html .= '<span class="lcs-preview-label">' . esc_html( $this->is_en ? 'Preview:' : 'Vorschau:' ) . '</span> '; 1547 $html .= '<span class="lcs-preview-text">'; 1548 $html .= esc_html( $this->is_en ? 'Visible from' : 'Sichtbar von' ) . ' '; 1549 $html .= '<strong>' . esc_html( $this->wp_date_lang( 'd.m.Y H:i', (int) $window['start'], $tz ) ) . '</strong>'; 1550 $html .= ' ' . esc_html( $this->is_en ? 'until' : 'bis' ) . ' '; 1551 $html .= '<strong>' . esc_html( $this->wp_date_lang( 'd.m.Y H:i', (int) $window['end'], $tz ) ) . '</strong>'; 1552 $html .= '</span>'; 1553 $html .= '</div>'; 1554 $html .= '</div>'; 1555 } 1556 } 1557 1558 $html .= '</div>'; 1559 1560 return $html; 1561 } 1562 1563 private function compute_class_offset_window( $cls, $next_event ) { 1564 if ( empty( $next_event['ts'] ) ) return null; 1565 1566 $days_before = max( 0, (int) ( $cls['days_before'] ?? 0 ) ); 1567 $start_time = (string) ( $cls['start_time'] ?? '' ); 1568 if ( $start_time !== '' && ! preg_match( '/^\d{2}:\d{2}$/', $start_time ) ) $start_time = ''; 1569 1570 $days_after = max( 0, (int) ( $cls['days_after'] ?? 0 ) ); 1571 $end_time = (string) ( $cls['end_time'] ?? '' ); 1572 if ( $end_time !== '' && ! preg_match( '/^\d{2}:\d{2}$/', $end_time ) ) { $end_time = ''; } 1573 1574 $tz = wp_timezone(); 1575 $event_dt = (new DateTime('@' . (int) $next_event['ts']))->setTimezone( $tz ); 1576 1577 if ( $days_before > 0 ) { 1578 $start_dt = clone $event_dt; 1579 $start_dt->modify( '-' . $days_before . ' days' ); 1580 if ( $start_time !== '' ) { 1581 $start_dt->setTime( (int) substr( $start_time, 0, 2 ), (int) substr( $start_time, 3, 2 ), 0 ); 1582 } else { 1583 $start_dt->setTime( 0, 0, 0 ); 1584 } 1585 $start_ts = $start_dt->getTimestamp(); 1586 } else { 1587 if ( $start_time !== '' ) { 1588 $start_dt = clone $event_dt; 1589 $start_dt->setTime( (int) substr( $start_time, 0, 2 ), (int) substr( $start_time, 3, 2 ), 0 ); 1590 $start_ts = $start_dt->getTimestamp(); 1591 } else { 1592 $start_ts = (int) $next_event['ts']; 1593 } 1594 } 1595 1596 $base_end_ts = ! empty( $next_event['end_ts'] ) ? (int) $next_event['end_ts'] : (int) $next_event['ts']; 1597 $end_ts = $base_end_ts; 1598 1599 if ( $days_after > 0 ) { 1600 $end_ts = $base_end_ts + ( $days_after * DAY_IN_SECONDS ); 1601 } 1602 1603 if ( $end_time !== '' ) { 1604 $base_end_dt = (new DateTime('@' . (int) $base_end_ts ))->setTimezone( $tz ); 1605 if ( $days_after > 0 ) { 1606 $base_end_dt->modify( '+' . $days_after . ' days' ); 1607 } 1608 $base_end_dt->setTime( (int) substr( $end_time, 0, 2 ), (int) substr( $end_time, 3, 2 ), 0 ); 1609 $end_ts = $base_end_dt->getTimestamp(); 1610 } 1611 1612 return [ 'start' => (int) $start_ts, 'end' => (int) $end_ts ]; 1407 1613 } 1408 1614 … … 1430 1636 echo '<div style="margin-top:15px;">'; 1431 1637 echo '<table class="lcs-table lcs-table--compact">'; 1432 echo '<thead><tr><th>' . esc_html( $this->is_en ? 'Day or period' : 'Tag oder Zeitraum' ) . '</th><th>' . esc_html( $this->is_en ? 'Show from' : 'Einblenden am' ) . '</th><th>' . esc_html( $this->is_en ? 'Text (long)' : 'Text (long)' ) . '</th><th>' . esc_html( $this->is_en ? ' Source' : 'Quelle' ) . '</th></tr></thead>';1638 echo '<thead><tr><th>' . esc_html( $this->is_en ? 'Day or period' : 'Tag oder Zeitraum' ) . '</th><th>' . esc_html( $this->is_en ? 'Show from' : 'Einblenden am' ) . '</th><th>' . esc_html( $this->is_en ? 'Text (long)' : 'Text (long)' ) . '</th><th>' . esc_html( $this->is_en ? 'Event text (opt)' : 'Eventtext (opt)' ) . '</th></tr></thead>'; 1433 1639 echo '<tbody>'; 1434 1640 … … 1492 1698 $long_txt = $this->format_event_datetime( $ts, $prefix, $time, 'long', $ev ); 1493 1699 1494 $src = (string) ( $ev['source'] ?? ( $ev['type'] ?? '' ) ); 1495 $rule_type = (string) ( $ev['rule_type'] ?? '' ); 1496 if ( $src === 'recurring' ) { 1497 $src_txt = $this->is_en ? 'Recurring' : 'Wiederholung'; 1498 if ( $rule_type !== '' ) { 1499 $src_txt .= ' (' . $rule_type . ')'; 1500 } 1501 } else { 1502 $src_txt = $this->is_en ? 'Single' : 'Termin'; 1503 } 1700 // Eventtext (optional) - aus dem Event-Datensatz 1701 $event_text = (string) ( $ev['text'] ?? '' ); 1504 1702 1505 1703 … … 1509 1707 echo '<td>' . esc_html( $show_txt ) . '</td>'; 1510 1708 echo '<td>' . esc_html( $long_txt ) . '</td>'; 1511 echo '<td>' . esc_html( $ src_txt ) . '</td>';1709 echo '<td>' . esc_html( $event_text ) . '</td>'; 1512 1710 echo '</tr>'; 1513 1711 } … … 1539 1737 private function prefix_select( $name, $current ) { 1540 1738 $current = (string) $current; 1739 // Präfix 1: keins | ab | bis | um | von | Ab | Bis | Um | Von 1541 1740 $opts = [ 1542 '' => '—', 1543 'ab' => ( $this->is_en ? 'from' : 'ab' ), 1544 'bis' => ( $this->is_en ? 'until' : 'bis' ), 1545 'um' => ( $this->is_en ? 'at' : 'um' ), 1741 '' => ( $this->is_en ? 'none' : 'keins' ), 1742 'ab' => 'ab', 1743 'bis' => 'bis', 1744 'um' => 'um', 1745 'von' => 'von', 1746 'Ab' => 'Ab', 1747 'Bis' => 'Bis', 1748 'Um' => 'Um', 1749 'Von' => 'Von', 1546 1750 ]; 1751 if ( $this->is_en ) { 1752 $opts = [ 1753 '' => 'none', 1754 'from' => 'from', 1755 'until' => 'until', 1756 'at' => 'at', 1757 'From' => 'From', 1758 'Until' => 'Until', 1759 'At' => 'At', 1760 ]; 1761 } 1762 $html = '<select class="lcs-select" name="' . esc_attr( $name ) . '">'; 1763 foreach ( $opts as $val => $label ) { 1764 $html .= '<option value="' . esc_attr( $val ) . '" ' . selected( $current, $val, false ) . '>' . esc_html( $label ) . '</option>'; 1765 } 1766 $html .= '</select>'; 1767 return $html; 1768 } 1769 1770 private function prefix2_select( $name, $current ) { 1771 $current = (string) $current; 1772 // Präfix 2: keins | bis | Bis | – (Bindestrich) 1773 $opts = [ 1774 '' => ( $this->is_en ? 'none' : 'keins' ), 1775 'bis' => 'bis', 1776 'Bis' => 'Bis', 1777 'dash' => '– (' . ( $this->is_en ? 'dash' : 'Strich' ) . ')', 1778 ]; 1779 if ( $this->is_en ) { 1780 $opts = [ 1781 '' => 'none', 1782 'to' => 'to', 1783 'To' => 'To', 1784 'dash' => '– (dash)', 1785 ]; 1786 } 1547 1787 $html = '<select class="lcs-select" name="' . esc_attr( $name ) . '">'; 1548 1788 foreach ( $opts as $val => $label ) { … … 1638 1878 1639 1879 $cls_new = 'luzid-cs-' . $slug; 1880 $cls_old = 'luzid-cs-' . $slug; // legacy support (pre-1.2.2) 1640 1881 $is_active = $this->is_entry_active_now( $e, $now ); 1641 1882 1642 1883 // Set element visibility directly (independent of body_class()). 1643 $css .= '.' . $cls_new . ' {display:' . ( $is_active ? 'block' : 'none' ) . '!important;}' . "\n";1884 $css .= '.' . $cls_new . ',.' . $cls_old . '{display:' . ( $is_active ? 'block' : 'none' ) . '!important;}' . "\n"; 1644 1885 1645 1886 // Provide a body class hint for themes/custom CSS (optional). … … 1668 1909 if ( $this->is_entry_active_now( $e, $now ) ) { 1669 1910 $classes[] = 'luzid-cs-active-' . $slug; 1911 $classes[] = 'luzid-cs-active-' . $slug; // legacy support 1670 1912 } 1671 1913 } … … 1965 2207 $prefix = isset( $r['prefix'] ) ? (string) $r['prefix'] : ''; 1966 2208 $time = isset( $r['time'] ) ? (string) $r['time'] : ''; 2209 $prefix2 = isset( $r['prefix2'] ) ? (string) $r['prefix2'] : ''; 2210 $time_to = isset( $r['time_to'] ) ? (string) $r['time_to'] : ''; 1967 2211 $text = isset( $r['text'] ) ? (string) $r['text'] : ''; 1968 2212 … … 1976 2220 1977 2221 $cand = [ 1978 'ts' => (int) $ts, 1979 'type' => 'single', 1980 'source' => 'single', 1981 'prefix' => $prefix, 1982 'time' => $time, 1983 'text' => $text, 1984 'from' => $from, 2222 'ts' => (int) $ts, 2223 'type' => 'single', 2224 'source' => 'single', 2225 'prefix' => $prefix, 2226 'time' => $time, 2227 'prefix2' => $prefix2, 2228 'time_to' => $time_to, 2229 'text' => $text, 2230 'from' => $from, 1985 2231 ]; 1986 2232 … … 2025 2271 $time = isset( $row['time'] ) ? (string) $row['time'] : ''; 2026 2272 $prefix = isset( $row['prefix'] ) ? (string) $row['prefix'] : ''; 2273 $prefix2 = isset( $row['prefix2'] ) ? (string) $row['prefix2'] : ''; 2274 $time_to = isset( $row['time_to'] ) ? (string) $row['time_to'] : ''; 2027 2275 $text = isset( $row['text'] ) ? (string) $row['text'] : ''; 2028 2276 … … 2030 2278 if ( $ts !== null ) { 2031 2279 $cand = [ 2032 'ts' => (int) $ts, 2033 'type' => 'single', 2034 'source' => 'single', 2035 'prefix' => $prefix, 2036 'time' => $time, 2037 'text' => $text, 2280 'ts' => (int) $ts, 2281 'type' => 'single', 2282 'source' => 'single', 2283 'prefix' => $prefix, 2284 'time' => $time, 2285 'prefix2' => $prefix2, 2286 'time_to' => $time_to, 2287 'text' => $text, 2038 2288 ]; 2039 2289 … … 2218 2468 if ( $ts === null ) continue; 2219 2469 if ( $ts < $after_ts ) continue; 2220 return [ 'ts' => $ts, 'type' => 'recurring', 'source' => 'recurring', 'rule_type' => $type, 'prefix' => $prefix, 'time' => $time, 'text' => $text ];2470 return [ 'ts' => $ts, 'type' => 'recurring', 'source' => 'recurring', 'rule_type' => $type, 'prefix' => $prefix, 'time' => $time, 'text' => $text, 'prefix2' => (string) ( $rule['prefix2'] ?? '' ), 'time_to' => (string) ( $rule['time_to'] ?? '' ) ]; 2221 2471 } 2222 2472 return null; … … 2238 2488 if ( $ts === null ) continue; 2239 2489 if ( $ts < $after_ts ) continue; 2240 return [ 'ts' => $ts, 'type' => 'recurring', 'source' => 'recurring', 'rule_type' => $type, 'prefix' => $prefix, 'time' => $time, 'text' => $text ];2490 return [ 'ts' => $ts, 'type' => 'recurring', 'source' => 'recurring', 'rule_type' => $type, 'prefix' => $prefix, 'time' => $time, 'text' => $text, 'prefix2' => (string) ( $rule['prefix2'] ?? '' ), 'time_to' => (string) ( $rule['time_to'] ?? '' ) ]; 2241 2491 } 2242 2492 return null; … … 2258 2508 if ( $ts === null ) continue; 2259 2509 if ( $ts < $after_ts ) continue; 2260 return [ 'ts' => $ts, 'type' => 'recurring', 'source' => 'recurring', 'rule_type' => $type, 'prefix' => $prefix, 'time' => $time, 'text' => $text ]; 2510 return [ 'ts' => $ts, 'type' => 'recurring', 'source' => 'recurring', 'rule_type' => $type, 'prefix' => $prefix, 'time' => $time, 'text' => $text, 'prefix2' => (string) ( $rule['prefix2'] ?? '' ), 'time_to' => (string) ( $rule['time_to'] ?? '' ) ]; 2511 } 2512 return null; 2513 } 2514 2515 if ( $type === 'yearly' ) { 2516 $yearly_day = (int) ( $rule['yearly_day'] ?? 0 ); 2517 $yearly_month = (int) ( $rule['yearly_month'] ?? 0 ); 2518 if ( $yearly_day < 1 || $yearly_day > 31 ) return null; 2519 if ( $yearly_month < 1 || $yearly_month > 12 ) return null; 2520 2521 $start_year = (int) $after->format('Y'); 2522 for ( $y = 0; $y < 10; $y++ ) { 2523 $year = $start_year + $y; 2524 $month_str = str_pad( (string) $yearly_month, 2, '0', STR_PAD_LEFT ); 2525 $day_str = str_pad( (string) $yearly_day, 2, '0', STR_PAD_LEFT ); 2526 $date = $year . '-' . $month_str . '-' . $day_str; 2527 2528 // Validate date (e.g., Feb 30 doesn't exist) 2529 if ( ! checkdate( $yearly_month, $yearly_day, $year ) ) continue; 2530 if ( ! $this->rule_period_allows( $rule, $date ) ) continue; 2531 $ts = $this->ts_from_date_time( $date, $time ); 2532 if ( $ts === null ) continue; 2533 if ( $ts < $after_ts ) continue; 2534 return [ 'ts' => $ts, 'type' => 'recurring', 'source' => 'recurring', 'rule_type' => $type, 'prefix' => $prefix, 'time' => $time, 'text' => $text, 'prefix2' => (string) ( $rule['prefix2'] ?? '' ), 'time_to' => (string) ( $rule['time_to'] ?? '' ) ]; 2261 2535 } 2262 2536 return null; … … 2315 2589 $atts = shortcode_atts( 2316 2590 [ 2317 'slug' => '', 2318 'opt' => 'long', 2319 'count' => 10, 2591 'slug' => '', 2592 'date' => '', // leer=kein Datum | short|medium|long|full 2593 'time' => '', // leer=keine Zeit | auto|raw|prefix|range|range_long 2594 'count' => 10, 2320 2595 'timeoffset' => 0, 2321 'lang' => '', 2322 'list' => 'false', 2323 'text' => 'false', 2324 'sep' => '-', 2596 'lang' => '', 2597 'list' => 'false', 2598 'text' => 'false', 2599 'sep1' => ' ', // zwischen Datum und Zeit 2600 'sep2' => ' ', // zwischen Zeit und Text (oder Datum und Text wenn keine Zeit) 2601 'sep3' => '', // zwischen Listenelementen (nur bei list="true") 2325 2602 ], 2326 2603 $atts, … … 2329 2606 2330 2607 $slug = sanitize_title( (string) $atts['slug'] ); 2331 $opt = sanitize_text_field( (string) $atts['opt'] ); 2608 $date_format = sanitize_text_field( (string) $atts['date'] ); 2609 $time_format = sanitize_text_field( (string) $atts['time'] ); 2332 2610 $count = (int) $atts['count']; 2333 2611 $timeoffset = (int) $atts['timeoffset']; 2334 2612 $lang = strtolower( sanitize_text_field( (string) $atts['lang'] ) ); 2335 2613 2336 $list = strtolower( sanitize_text_field( (string) $atts['list'] ) ); 2337 $with_text = strtolower( sanitize_text_field( (string) $atts['text'] ) ); 2338 $sep = (string) $atts['sep']; 2339 // Separator should preserve intentional leading/trailing spaces (e.g. " | "). 2340 // So we only strip tags/control chars but do NOT trim. 2341 $sep = html_entity_decode( $sep, ENT_QUOTES, 'UTF-8' ); 2342 2343 // Allow line breaks for separator if user passes <br> (or a newline char). 2344 $sep_is_br = ( preg_match( '/<\s*br\s*\/?\s*>/i', $sep ) === 1 ) || ( strpos( $sep, "\n" ) !== false ) || ( strpos( $sep, "\r" ) !== false ); 2345 if ( $sep_is_br ) { 2346 $sep = '<br />'; 2347 } else { 2348 // Strip tags/control chars, but do NOT trim. 2349 $sep = wp_strip_all_tags( $sep, true ); 2350 $sep = str_replace( ["\n", "\r", "\t"], ' ', $sep ); 2351 if ( $sep === '' ) { $sep = '-'; } 2352 } 2353 2354 // WordPress shortcode parsing trims leading/trailing spaces inside quoted attributes. 2355 // To still support sep=" | " we pad separators with single spaces by default, 2356 // unless the separator already has edge whitespace or uses explicitly. 2357 $sep_has_edge_ws = ( preg_match( '/^\s|\s$/u', $sep ) === 1 ); 2358 $sep_has_nbsp = ( strpos( $sep, ' ' ) !== false ); 2359 if ( ! $sep_is_br && ! $sep_has_edge_ws && ! $sep_has_nbsp ) { 2360 $sep = ' ' . $sep . ' '; 2361 } 2362 2363 2364 // Build safe HTML for separator (allow only <br> as HTML) 2365 $sep_html = esc_html( $sep ); 2366 if ( preg_match( '/<\s*br\s*\/?\s*>/i', $sep ) || strpos( $sep, "\n" ) !== false || strpos( $sep, "\r" ) !== false ) { 2367 $sep_html = '<br />'; 2368 } 2369 $is_list = in_array( $list, [ '1', 'true', 'yes', 'on' ], true ); 2370 $show_text = in_array( $with_text, [ '1', 'true', 'yes', 'on' ], true ); 2371 2372 // Backward-compat: opt="list" behaves like list="true" + opt="long" 2373 if ( $opt === 'list' ) { 2374 $opt = 'long'; 2375 $is_list = true; 2376 } 2377 2378 2379 // Optional language override for shortcode output (de|en). Empty = current. 2614 // If neither date nor time is set, default to date="long" 2615 if ( $date_format === '' && $time_format === '' ) { 2616 $date_format = 'long'; 2617 } 2618 2619 $is_list = in_array( strtolower( (string) $atts['list'] ), [ '1', 'true', 'yes', 'on' ], true ); 2620 $show_text = in_array( strtolower( (string) $atts['text'] ), [ '1', 'true', 'yes', 'on' ], true ); 2621 2622 // Process separators 2623 $sep1_html = $this->process_separator( (string) $atts['sep1'] ); 2624 $sep2_html = $this->process_separator( (string) $atts['sep2'] ); 2625 $sep3_html = $this->process_separator( (string) $atts['sep3'], '' ); 2626 2627 // Optional language override for shortcode output (de|en) 2380 2628 $old_lang = $this->lang; 2381 2629 $old_is_en = $this->is_en; … … 2393 2641 $now = time(); 2394 2642 2395 // List output: next N events in the selected format, separated by <br />2643 // List output: next N events 2396 2644 if ( $is_list ) { 2397 2645 $count = max( 1, min( 200, $count ) ); … … 2403 2651 } 2404 2652 2405 $ lines = [];2653 $items = []; 2406 2654 foreach ( $events as $ev ) { 2407 2655 if ( empty( $ev['ts'] ) ) { … … 2409 2657 } 2410 2658 $ts = (int) $ev['ts'] + ( $timeoffset * 60 ); 2411 2412 $opt_html = esc_html( $this->format_event_datetime( $ts, $ev['prefix'] ?? '', $ev['time'] ?? '', $opt, $ev ) ); 2413 $item_html = '<div class="luzid-cs-item"><span class="luzid-cs-opt">' . $opt_html . '</span>'; 2414 2415 if ( $show_text && ! empty( $ev['text'] ) ) { 2416 $item_html .= '<span class="luzid-cs-sep">' . $sep_html . '</span><span class="luzid-cs-text">' . esc_html( (string) $ev['text'] ) . '</span>'; 2417 } 2418 2419 $item_html .= '</div>'; 2420 $lines[] = $item_html; 2421 } 2422 $this->lang = $old_lang; 2659 $item_html = $this->build_event_output( $ts, $ev, $date_format, $time_format, $show_text, $sep1_html, $sep2_html ); 2660 $items[] = '<div class="luzid-cs-item">' . $item_html . '</div>'; 2661 } 2662 2663 $this->lang = $old_lang; 2423 2664 $this->is_en = $old_is_en; 2424 return '<div class="luzid-cs luzid-cs--list">' . implode( "\n", $lines ) . '</div>'; 2425 } 2426 2427 $next = $this->compute_next_event( $entry, $now ); 2665 2666 // Join items with sep3 2667 $glue = $sep3_html !== '' ? $sep3_html : "\n"; 2668 return '<div class="luzid-cs luzid-cs--list">' . implode( $glue, $items ) . '</div>'; 2669 } 2670 2671 // Single event output 2672 $next = $this->compute_next_event( $entry, $now ); 2428 2673 if ( ! $next || empty( $next['ts'] ) ) { 2429 2674 $this->lang = $old_lang; … … 2433 2678 2434 2679 $ts = (int) $next['ts'] + ( $timeoffset * 60 ); 2435 $opt_html = esc_html( $this->format_event_datetime( $ts, $next['prefix'] ?? '', $next['time'] ?? '', $opt, $next ) ); 2436 $out = '<span class="luzid-cs luzid-cs--single"><span class="luzid-cs-opt">' . $opt_html . '</span>'; 2437 2438 if ( $show_text && ! empty( $next['text'] ) ) { 2439 $out .= '<span class="luzid-cs-sep">' . $sep_html . '</span><span class="luzid-cs-text">' . esc_html( (string) $next['text'] ) . '</span>'; 2440 } 2441 2442 $out .= '</span>'; 2680 $out = '<span class="luzid-cs luzid-cs--single">' . $this->build_event_output( $ts, $next, $date_format, $time_format, $show_text, $sep1_html, $sep2_html ) . '</span>'; 2443 2681 2444 2682 $this->lang = $old_lang; 2445 2683 $this->is_en = $old_is_en; 2446 2684 return $out; 2685 } 2686 2687 /** 2688 * Process a separator value - handle <br>, spaces, etc. 2689 * User gets exactly what they input - no auto-padding. 2690 */ 2691 private function process_separator( $sep, $default = ' ' ) { 2692 $sep = html_entity_decode( $sep, ENT_QUOTES, 'UTF-8' ); 2693 2694 // Check for <br> variants 2695 if ( preg_match( '/<\s*br\s*\/?\s*>/i', $sep ) || strpos( $sep, "\n" ) !== false || strpos( $sep, "\r" ) !== false ) { 2696 return '<br />'; 2697 } 2698 2699 // Strip tags but preserve content 2700 $sep = wp_strip_all_tags( $sep, true ); 2701 $sep = str_replace( ["\n", "\r", "\t"], ' ', $sep ); 2702 2703 // WordPress trims spaces from shortcode attributes. 2704 // If the result is empty, use the default (typically a space). 2705 if ( $sep === '' ) { 2706 return esc_html( $default ); 2707 } 2708 2709 return esc_html( $sep ); 2710 } 2711 2712 /** 2713 * Build the HTML output for a single event. 2714 */ 2715 private function build_event_output( $ts, $ev, $date_format, $time_format, $show_text, $sep1_html, $sep2_html ) { 2716 $tz = wp_timezone(); 2717 2718 // Date part (only if date_format is set) 2719 $date_out = ''; 2720 if ( $date_format !== '' ) { 2721 $date_out = $this->format_date_part( $ts, $date_format, $tz ); 2722 } 2723 2724 // Time part (only if time_format is set) 2725 $time_out = ''; 2726 if ( $time_format !== '' ) { 2727 $time_out = $this->format_time_part( $ts, $ev, $time_format, $tz ); 2728 } 2729 2730 // Text part 2731 $text_out = ''; 2732 if ( $show_text && ! empty( $ev['text'] ) ) { 2733 $text_out = (string) $ev['text']; 2734 } 2735 2736 // Build output - only include parts that are set 2737 $parts = []; 2738 2739 // Date (if present) 2740 if ( $date_out !== '' ) { 2741 $parts[] = '<span class="luzid-cs-date">' . esc_html( $date_out ) . '</span>'; 2742 } 2743 2744 // Time (if present) 2745 if ( $time_out !== '' ) { 2746 // Only add sep1 if we have both date and time 2747 if ( $date_out !== '' ) { 2748 $parts[] = '<span class="luzid-cs-sep luzid-cs-sep1">' . $sep1_html . '</span>'; 2749 } 2750 $parts[] = '<span class="luzid-cs-time">' . esc_html( $time_out ) . '</span>'; 2751 } 2752 2753 // Text (if present) 2754 if ( $text_out !== '' ) { 2755 // Only add sep2 if we have something before it 2756 if ( ! empty( $parts ) ) { 2757 $parts[] = '<span class="luzid-cs-sep luzid-cs-sep2">' . $sep2_html . '</span>'; 2758 } 2759 $parts[] = '<span class="luzid-cs-text">' . esc_html( $text_out ) . '</span>'; 2760 } 2761 2762 return implode( '', $parts ); 2763 } 2764 2765 /** 2766 * Format just the date part. 2767 */ 2768 private function format_date_part( $ts, $date_format, $tz ) { 2769 switch ( $date_format ) { 2770 case 'short': 2771 return $this->wp_date_lang( 'd.m.Y', $ts, $tz ); 2772 case 'medium': 2773 return $this->wp_date_lang( 'D, d.m.Y', $ts, $tz ); 2774 case 'long': 2775 return $this->wp_date_lang( 'l, d.m.Y', $ts, $tz ); 2776 case 'full': 2777 if ( $this->is_en ) { 2778 return $this->wp_date_lang( 'l, d F Y', $ts, $tz ); 2779 } else { 2780 return $this->wp_date_lang( 'l, d. F Y', $ts, $tz ); 2781 } 2782 default: 2783 return $this->wp_date_lang( 'l, d.m.Y', $ts, $tz ); 2784 } 2785 } 2786 2787 /** 2788 * Format just the time part. 2789 */ 2790 private function format_time_part( $ts, $ev, $time_format, $tz ) { 2791 $prefix = (string) ( $ev['prefix'] ?? '' ); 2792 $prefix2 = (string) ( $ev['prefix2'] ?? '' ); 2793 $time_hm = (string) ( $ev['time'] ?? '' ); 2794 $time_to_hm = (string) ( $ev['time_to'] ?? '' ); 2795 2796 $has_time = ( $time_hm !== '' && preg_match( '/^\d{2}:\d{2}$/', $time_hm ) ); 2797 $has_time_to = ( $time_to_hm !== '' && preg_match( '/^\d{2}:\d{2}$/', $time_to_hm ) ); 2798 2799 if ( ! $has_time ) { 2800 return ''; 2801 } 2802 2803 $time_from = $this->wp_date_lang( 'H:i', $ts, $tz ); 2804 2805 switch ( $time_format ) { 2806 case 'raw': 2807 return $time_from; 2808 2809 case 'prefix': 2810 $p = $this->format_prefix_output( $prefix ); 2811 $out = trim( $p . ' ' . $time_from ); 2812 if ( ! $this->is_en ) { 2813 $out .= ' Uhr'; 2814 } 2815 return $out; 2816 2817 case 'range': 2818 if ( $has_time_to ) { 2819 $p2 = $this->format_prefix2_output( $prefix2 ); 2820 return $time_from . ' ' . $p2 . ' ' . $time_to_hm; 2821 } 2822 return $time_from; 2823 2824 case 'range_long': 2825 $p = $this->format_prefix_output( $prefix ); 2826 if ( $has_time_to ) { 2827 $p2 = $this->format_prefix2_output( $prefix2 ); 2828 $out = trim( $p . ' ' . $time_from . ' ' . $p2 . ' ' . $time_to_hm ); 2829 } else { 2830 $out = trim( $p . ' ' . $time_from ); 2831 } 2832 if ( ! $this->is_en ) { 2833 $out .= ' Uhr'; 2834 } 2835 return $out; 2836 2837 case 'auto': 2838 default: 2839 $p = $this->format_prefix_output( $prefix ); 2840 if ( $has_time_to ) { 2841 $p2 = $this->format_prefix2_output( $prefix2 ); 2842 $out = trim( $p . ' ' . $time_from . ' ' . $p2 . ' ' . $time_to_hm ); 2843 } else { 2844 $out = trim( $p . ' ' . $time_from ); 2845 } 2846 if ( ! $this->is_en ) { 2847 $out .= ' Uhr'; 2848 } 2849 return $out; 2850 } 2851 } 2852 2853 /** 2854 * Format event with new parameter structure. 2855 * 2856 * @param int $ts Timestamp 2857 * @param array $event Event data (prefix, prefix2, time, time_to) 2858 * @param string $date_format short|medium|long|full 2859 * @param string $time_format auto|raw|prefix|range|range_long 2860 * @param string $sep_html Separator between date and time 2861 * @return string Formatted output 2862 */ 2863 private function format_event_new( $ts, $event, $date_format, $time_format, $sep_html ) { 2864 $tz = wp_timezone(); 2865 2866 $prefix = (string) ( $event['prefix'] ?? '' ); 2867 $prefix2 = (string) ( $event['prefix2'] ?? '' ); 2868 $time_hm = (string) ( $event['time'] ?? '' ); 2869 $time_to_hm = (string) ( $event['time_to'] ?? '' ); 2870 2871 // Date formatting 2872 $date_out = ''; 2873 switch ( $date_format ) { 2874 case 'short': 2875 $date_out = $this->wp_date_lang( 'd.m.Y', $ts, $tz ); 2876 break; 2877 case 'medium': 2878 $date_out = $this->wp_date_lang( 'D, d.m.Y', $ts, $tz ); 2879 break; 2880 case 'long': 2881 $date_out = $this->wp_date_lang( 'l, d.m.Y', $ts, $tz ); 2882 break; 2883 case 'full': 2884 if ( $this->is_en ) { 2885 $date_out = $this->wp_date_lang( 'l, d F Y', $ts, $tz ); 2886 } else { 2887 $date_out = $this->wp_date_lang( 'l, d. F Y', $ts, $tz ); 2888 } 2889 break; 2890 default: 2891 $date_out = $this->wp_date_lang( 'l, d.m.Y', $ts, $tz ); 2892 } 2893 2894 // Time formatting 2895 $time_out = ''; 2896 $has_time = ( $time_hm !== '' && preg_match( '/^\d{2}:\d{2}$/', $time_hm ) ); 2897 $has_time_to = ( $time_to_hm !== '' && preg_match( '/^\d{2}:\d{2}$/', $time_to_hm ) ); 2898 2899 if ( $has_time && $time_format !== '' ) { 2900 $time_from = $this->wp_date_lang( 'H:i', $ts, $tz ); 2901 $time_to = ''; 2902 if ( $has_time_to ) { 2903 $time_to = $time_to_hm; 2904 } 2905 2906 switch ( $time_format ) { 2907 case 'raw': 2908 $time_out = $time_from; 2909 break; 2910 2911 case 'prefix': 2912 $p = $this->format_prefix_output( $prefix ); 2913 $time_out = trim( $p . ' ' . $time_from ); 2914 if ( ! $this->is_en ) { 2915 $time_out .= ' Uhr'; 2916 } 2917 break; 2918 2919 case 'range': 2920 if ( $has_time_to ) { 2921 $p2 = $this->format_prefix2_output( $prefix2 ); 2922 $time_out = $time_from . ' ' . $p2 . ' ' . $time_to; 2923 } else { 2924 $time_out = $time_from; 2925 } 2926 break; 2927 2928 case 'range_long': 2929 $p = $this->format_prefix_output( $prefix ); 2930 if ( $has_time_to ) { 2931 $p2 = $this->format_prefix2_output( $prefix2 ); 2932 $time_out = trim( $p . ' ' . $time_from . ' ' . $p2 . ' ' . $time_to ); 2933 } else { 2934 $time_out = trim( $p . ' ' . $time_from ); 2935 } 2936 if ( ! $this->is_en ) { 2937 $time_out .= ' Uhr'; 2938 } 2939 break; 2940 2941 case 'auto': 2942 default: 2943 $p = $this->format_prefix_output( $prefix ); 2944 if ( $has_time_to ) { 2945 $p2 = $this->format_prefix2_output( $prefix2 ); 2946 $time_out = trim( $p . ' ' . $time_from . ' ' . $p2 . ' ' . $time_to ); 2947 } else { 2948 $time_out = trim( $p . ' ' . $time_from ); 2949 } 2950 if ( ! $this->is_en ) { 2951 $time_out .= ' Uhr'; 2952 } 2953 break; 2954 } 2955 } 2956 2957 // Combine date and time 2958 if ( $time_out !== '' ) { 2959 return $date_out . $sep_html . $time_out; 2960 } 2961 return $date_out; 2962 } 2963 2964 /** 2965 * Format prefix1 for output (handles case as stored). 2966 */ 2967 private function format_prefix_output( $prefix ) { 2968 if ( $prefix === '' ) { 2969 return ''; 2970 } 2971 // The prefix is stored exactly as the user chose (ab, Ab, von, Von, etc.) 2972 return $prefix; 2973 } 2974 2975 /** 2976 * Format prefix2 for output (handles dash conversion). 2977 */ 2978 private function format_prefix2_output( $prefix2 ) { 2979 if ( $prefix2 === '' ) { 2980 return ''; 2981 } 2982 if ( $prefix2 === 'dash' ) { 2983 return '–'; // Em dash 2984 } 2985 // The prefix is stored exactly as the user chose (bis, Bis, to, To, etc.) 2986 return $prefix2; 2447 2987 } 2448 2988 … … 2502 3042 } 2503 3043 3044 /** 3045 * Shortcode: [luzid_cs_eventtable] 3046 * 3047 * Outputs a table of upcoming events from all schedulers with include_in_eventtable=1. 3048 * 3049 * Parameters: 3050 * - cols: Comma-separated column types (default: date_medium,time_auto,text) 3051 * - count: Max number of events (default: 30) 3052 * - headers: Comma-separated custom headers (default: auto-generated) 3053 * - noheaders: Hide table headers (default: false) 3054 * - class: Additional CSS class for the table 3055 * - empty: Text when no events (default: "Keine Termine" / "No events") 3056 * - order: Sort order asc|desc (default: asc) 3057 * - lang: Language override de|en 3058 */ 3059 public function shortcode_eventtable( $atts ) { 3060 $atts = shortcode_atts( 3061 [ 3062 'cols' => 'date_medium,time_auto,text', 3063 'count' => 30, 3064 'headers' => '', 3065 'noheaders' => 'false', 3066 'class' => '', 3067 'empty' => '', 3068 'order' => 'asc', 3069 'lang' => '', 3070 ], 3071 $atts, 3072 'luzid_cs_eventtable' 3073 ); 3074 3075 $cols = array_map( 'trim', explode( ',', (string) $atts['cols'] ) ); 3076 $cols = array_filter( $cols ); 3077 if ( empty( $cols ) ) { 3078 $cols = [ 'date_medium', 'time_auto', 'text' ]; 3079 } 3080 3081 $count = max( 1, min( 200, (int) $atts['count'] ) ); 3082 $custom_headers = array_map( 'trim', explode( ',', (string) $atts['headers'] ) ); 3083 $noheaders = in_array( strtolower( (string) $atts['noheaders'] ), [ '1', 'true', 'yes', 'on' ], true ); 3084 $custom_class = sanitize_html_class( (string) $atts['class'] ); 3085 $empty_text = sanitize_text_field( (string) $atts['empty'] ); 3086 $order = strtolower( (string) $atts['order'] ) === 'desc' ? 'desc' : 'asc'; 3087 $lang = strtolower( sanitize_text_field( (string) $atts['lang'] ) ); 3088 3089 // Language override 3090 $old_lang = $this->lang; 3091 $old_is_en = $this->is_en; 3092 if ( $lang === 'en' ) { 3093 $this->lang = 'en'; 3094 $this->is_en = true; 3095 } elseif ( $lang === 'de' ) { 3096 $this->lang = 'de'; 3097 $this->is_en = false; 3098 } 3099 3100 // Default empty text 3101 if ( $empty_text === '' ) { 3102 $empty_text = $this->is_en ? 'No events' : 'Keine Termine'; 3103 } 3104 3105 // Collect events from all schedulers with include_in_eventtable=1 3106 $entries = $this->get_entries(); 3107 $all_events = []; 3108 $now = time(); 3109 3110 foreach ( $entries as $entry ) { 3111 if ( empty( $entry['include_in_eventtable'] ) ) { 3112 continue; 3113 } 3114 3115 $events = $this->compute_next_events( $entry, $now, $count ); 3116 if ( ! is_array( $events ) ) { 3117 continue; 3118 } 3119 3120 foreach ( $events as $ev ) { 3121 if ( empty( $ev['ts'] ) ) { 3122 continue; 3123 } 3124 $ev['_scheduler_slug'] = (string) ( $entry['slug'] ?? '' ); 3125 $ev['_scheduler_name'] = (string) ( $entry['name'] ?? '' ); 3126 $all_events[] = $ev; 3127 } 3128 } 3129 3130 // Sort by timestamp 3131 usort( $all_events, function( $a, $b ) use ( $order ) { 3132 $cmp = (int) $a['ts'] - (int) $b['ts']; 3133 return $order === 'desc' ? -$cmp : $cmp; 3134 } ); 3135 3136 // Limit to count 3137 $all_events = array_slice( $all_events, 0, $count ); 3138 3139 // No events? 3140 if ( empty( $all_events ) ) { 3141 $this->lang = $old_lang; 3142 $this->is_en = $old_is_en; 3143 return '<p class="luzid-cs-eventtable-empty">' . esc_html( $empty_text ) . '</p>'; 3144 } 3145 3146 // Build table 3147 $table_class = 'luzid-cs-eventtable'; 3148 if ( $custom_class !== '' ) { 3149 $table_class .= ' ' . $custom_class; 3150 } 3151 3152 $html = '<table class="' . esc_attr( $table_class ) . '">'; 3153 3154 // Headers 3155 if ( ! $noheaders ) { 3156 $html .= '<thead class="luzid-cs-eventtable__head"><tr class="luzid-cs-eventtable__row luzid-cs-eventtable__row--header">'; 3157 foreach ( $cols as $i => $col ) { 3158 $header = isset( $custom_headers[ $i ] ) && $custom_headers[ $i ] !== '' 3159 ? $custom_headers[ $i ] 3160 : $this->eventtable_default_header( $col ); 3161 $html .= '<th class="luzid-cs-eventtable__cell luzid-cs-eventtable__cell--' . esc_attr( $col ) . '">' . esc_html( $header ) . '</th>'; 3162 } 3163 $html .= '</tr></thead>'; 3164 } 3165 3166 // Body 3167 $html .= '<tbody class="luzid-cs-eventtable__body">'; 3168 $row_num = 0; 3169 foreach ( $all_events as $ev ) { 3170 $row_num++; 3171 $row_class = 'luzid-cs-eventtable__row luzid-cs-eventtable__row--' . ( $row_num % 2 === 0 ? 'even' : 'odd' ); 3172 $slug = esc_attr( $ev['_scheduler_slug'] ?? '' ); 3173 3174 $html .= '<tr class="' . esc_attr( $row_class ) . '" data-scheduler="' . $slug . '">'; 3175 foreach ( $cols as $col ) { 3176 $cell_value = $this->eventtable_cell_value( $ev, $col ); 3177 $html .= '<td class="luzid-cs-eventtable__cell luzid-cs-eventtable__cell--' . esc_attr( $col ) . '">' . esc_html( $cell_value ) . '</td>'; 3178 } 3179 $html .= '</tr>'; 3180 } 3181 $html .= '</tbody>'; 3182 3183 $html .= '</table>'; 3184 3185 $this->lang = $old_lang; 3186 $this->is_en = $old_is_en; 3187 return $html; 2504 3188 } 2505 3189 3190 /** 3191 * Get default header text for a column type. 3192 */ 3193 private function eventtable_default_header( $col ) { 3194 $headers_de = [ 3195 'date_short' => 'Datum', 3196 'date_medium' => 'Datum', 3197 'date_long' => 'Datum', 3198 'date_full' => 'Datum', 3199 'weekday_short' => 'Tag', 3200 'weekday_long' => 'Wochentag', 3201 'time_raw' => 'Uhrzeit', 3202 'time_auto' => 'Uhrzeit', 3203 'time_prefix' => 'Uhrzeit', 3204 'time_range' => 'Uhrzeit', 3205 'time_range_long' => 'Uhrzeit', 3206 'text' => 'Event', 3207 'scheduler' => 'Scheduler', 3208 ]; 3209 $headers_en = [ 3210 'date_short' => 'Date', 3211 'date_medium' => 'Date', 3212 'date_long' => 'Date', 3213 'date_full' => 'Date', 3214 'weekday_short' => 'Day', 3215 'weekday_long' => 'Weekday', 3216 'time_raw' => 'Time', 3217 'time_auto' => 'Time', 3218 'time_prefix' => 'Time', 3219 'time_range' => 'Time', 3220 'time_range_long' => 'Time', 3221 'text' => 'Event', 3222 'scheduler' => 'Scheduler', 3223 ]; 3224 3225 $headers = $this->is_en ? $headers_en : $headers_de; 3226 return $headers[ $col ] ?? $col; 3227 } 3228 3229 /** 3230 * Get cell value for a column type. 3231 */ 3232 private function eventtable_cell_value( $ev, $col ) { 3233 $ts = (int) ( $ev['ts'] ?? 0 ); 3234 $tz = wp_timezone(); 3235 3236 $prefix = (string) ( $ev['prefix'] ?? '' ); 3237 $prefix2 = (string) ( $ev['prefix2'] ?? '' ); 3238 $time_hm = (string) ( $ev['time'] ?? '' ); 3239 $time_to_hm = (string) ( $ev['time_to'] ?? '' ); 3240 $text = (string) ( $ev['text'] ?? '' ); 3241 3242 $has_time = ( $time_hm !== '' && preg_match( '/^\d{2}:\d{2}$/', $time_hm ) ); 3243 $has_time_to = ( $time_to_hm !== '' && preg_match( '/^\d{2}:\d{2}$/', $time_to_hm ) ); 3244 3245 switch ( $col ) { 3246 // Date columns 3247 case 'date_short': 3248 return $this->wp_date_lang( 'd.m.Y', $ts, $tz ); 3249 case 'date_medium': 3250 return $this->wp_date_lang( 'D, d.m.Y', $ts, $tz ); 3251 case 'date_long': 3252 return $this->wp_date_lang( 'l, d.m.Y', $ts, $tz ); 3253 case 'date_full': 3254 return $this->is_en 3255 ? $this->wp_date_lang( 'l, d F Y', $ts, $tz ) 3256 : $this->wp_date_lang( 'l, d. F Y', $ts, $tz ); 3257 3258 // Weekday columns 3259 case 'weekday_short': 3260 return $this->wp_date_lang( 'D', $ts, $tz ); 3261 case 'weekday_long': 3262 return $this->wp_date_lang( 'l', $ts, $tz ); 3263 3264 // Time columns 3265 case 'time_raw': 3266 return $has_time ? $this->wp_date_lang( 'H:i', $ts, $tz ) : ''; 3267 3268 case 'time_auto': 3269 if ( ! $has_time ) return ''; 3270 $time_from = $this->wp_date_lang( 'H:i', $ts, $tz ); 3271 $p = $this->format_prefix_output( $prefix ); 3272 if ( $has_time_to ) { 3273 $p2 = $this->format_prefix2_output( $prefix2 ); 3274 $out = trim( $p . ' ' . $time_from . ' ' . $p2 . ' ' . $time_to_hm ); 3275 } else { 3276 $out = trim( $p . ' ' . $time_from ); 3277 } 3278 if ( ! $this->is_en ) { 3279 $out .= ' Uhr'; 3280 } 3281 return $out; 3282 3283 case 'time_prefix': 3284 if ( ! $has_time ) return ''; 3285 $time_from = $this->wp_date_lang( 'H:i', $ts, $tz ); 3286 $p = $this->format_prefix_output( $prefix ); 3287 $out = trim( $p . ' ' . $time_from ); 3288 if ( ! $this->is_en ) { 3289 $out .= ' Uhr'; 3290 } 3291 return $out; 3292 3293 case 'time_range': 3294 if ( ! $has_time ) return ''; 3295 $time_from = $this->wp_date_lang( 'H:i', $ts, $tz ); 3296 if ( $has_time_to ) { 3297 $p2 = $this->format_prefix2_output( $prefix2 ); 3298 return $time_from . ' ' . $p2 . ' ' . $time_to_hm; 3299 } 3300 return $time_from; 3301 3302 case 'time_range_long': 3303 if ( ! $has_time ) return ''; 3304 $time_from = $this->wp_date_lang( 'H:i', $ts, $tz ); 3305 $p = $this->format_prefix_output( $prefix ); 3306 if ( $has_time_to ) { 3307 $p2 = $this->format_prefix2_output( $prefix2 ); 3308 $out = trim( $p . ' ' . $time_from . ' ' . $p2 . ' ' . $time_to_hm ); 3309 } else { 3310 $out = trim( $p . ' ' . $time_from ); 3311 } 3312 if ( ! $this->is_en ) { 3313 $out .= ' Uhr'; 3314 } 3315 return $out; 3316 3317 // Content columns 3318 case 'text': 3319 return $text; 3320 3321 case 'scheduler': 3322 return (string) ( $ev['_scheduler_name'] ?? '' ); 3323 3324 default: 3325 return ''; 3326 } 3327 } 3328 3329 } 3330 2506 3331 new Luzid_Content_Scheduler(); -
luzid-content-scheduler/trunk/readme.txt
r3456038 r3476291 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.4 7 Stable tag: 1. 2.27 Stable tag: 1.4.2 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 17 17 This gives you two complementary ways to use a scheduler: 18 18 19 1 )**Show / hide a block** (banner, alert, section, popup wrapper, …)19 1. **Show / hide a block** (banner, alert, section, popup wrapper, …) 20 20 Add the generated CSS class `luzid-cs-<slug>` to the element. The plugin adds body classes and a tiny frontend helper so the block is only displayed while the schedule is active. 21 21 22 2 )**Display dates in the frontend**22 2. **Display dates in the frontend** 23 23 Use the shortcode to print the next event (or a list of upcoming events) anywhere in your content. 24 24 25 Typical use cases: 26 27 * **Visibility scheduling**: show a maintenance banner only during a defined window, or show a seasonal notice with exceptions. 28 * **Event announcements**: print "Next event: …" on a page, including weekday + time. 29 * **Lists of upcoming dates**: output the next N dates of a program/club/course using `list="true"` + `count="N"`. 30 31 Core concepts: 25 = Typical Use Cases = 26 27 * **Maintenance banners**: Show a "We're updating our systems" notice only during scheduled maintenance windows. 28 * **Seasonal content**: Display holiday greetings, special offers, or seasonal menus during specific date ranges. 29 * **Event announcements**: Print "Next event: Saturday, March 15th at 7 PM" dynamically on any page. 30 * **Recurring schedules**: Show "Happy Hour specials" every Friday from 5 PM to 8 PM. 31 * **Multi-stage visibility**: Use Event Classes to show a popup 4 days before an event, the full content 2 weeks before, and a menu item 11 days before — all independently controlled. 32 * **Event calendars**: Generate a table of all upcoming events across multiple schedulers. 33 34 = Core Concepts = 32 35 33 36 * A scheduler becomes **active** when **at least one** rule matches (single dates/ranges *or* recurring rules). 34 37 * **Exceptions override everything**: if "now" is inside an exception range, the block is hidden even if other rules match. 35 * **Event offset** can shift the *visibility window* relative to the next computed event (e.g. show the block 4 days before the event, starting at a chosen time). 36 * The shortcode reads the same event logic and can output: 37 * `opt="long"` (default): weekday + date + time (e.g. "Mittwoch, 25.03.2026 ab 09:00 Uhr") 38 * `opt="short"`: date only (e.g. "25.03.2026") 39 * `opt="date"`: weekday + date (no time) 40 * `opt="time"`: time only 41 * `list="true"` + `count="N"`: outputs the next N events (one per line, separated by `<br />`) 42 * `text="true"` optionally appends the event text (from the new "Event text (optional)" fields) 43 * `sep="…"` sets the separator between opt output and event text (use `sep="<br>"` for a line break) 44 * `timeoffset` (minutes, can be negative) shifts the **printed time** without changing the schedule itself (useful for timezone-like adjustments or "doors open 30 min earlier"). 45 * Optional `lang="de|en"` forces the output language for the shortcode (empty = current UI language). 38 * **Event Classes** (new in 1.4) allow multiple independent visibility windows with different offsets — each generating its own CSS class. 39 * The shortcode reads the same event logic and outputs formatted dates with full control over separators and formatting. 46 40 47 41 == Features == 48 42 49 * **Setup**: Name → automatic slug + CSS class. 50 * **Single dates / date ranges ("Termine")** 51 * From/To fields 52 * Optional time + prefix `ab` / `bis` / `um` 53 * **Recurring rules ("Wiederholungen")** 54 * Weekly (weekday) 55 * Monthly (day of month) 56 * Weekday in month (e.g. 2nd Thursday) 57 * Each recurring rule can have its own valid-from/to range 58 * **Exceptions ("Ausnahmen")** 59 * Date or date range 60 * Overrides all other rules 61 * **Event offset** 62 * Show the block X days before the computed event (from a fixed start time) 63 * Hide the block X days after the computed event (to a fixed end time) 64 * **Preview tab**: generate a table of upcoming events including show time and shortcode output. 65 * **Shortcode** 66 * Base output: `[luzid_cs slug="my-scheduler" opt="short|date|time|long"]` 67 * List output: add `list="true"` + `count="N"` (N > 0) to output the next N events (one per line, separated by `<br>`). 68 * Optional event text: add `text="true"` to append the event's **optional Event text**. Use `sep=" | "` to define the separator between date/time and text. 69 * Optional `timeoffset` (minutes; can be negative) to shift the output time. 70 * **Languages**: German + English UI (flag switcher in the header). 43 = Scheduling = 44 45 * **Single dates & date ranges**: Define specific days or periods when content should be visible. 46 * **Recurring rules**: Weekly (every Monday), Monthly (every 15th), Weekday in month (2nd Thursday), Yearly (February 14th). 47 * **Exceptions**: Override all rules — content stays hidden during exception periods. 48 * **Event Classes**: Create multiple CSS classes per scheduler, each with independent visibility offsets. 49 50 = Shortcode Output = 51 52 * **Flexible date/time formatting**: Choose from short, medium, long, or full date formats. 53 * **Separators**: Control exactly what appears between date, time, and text parts. 54 * **Lists**: Output multiple upcoming events with customizable separators. 55 * **Event Table**: Generate a sortable table of all upcoming events from multiple schedulers. 56 * **Time offset**: Shift the displayed time (e.g., "doors open 30 minutes before"). 57 58 = Administration = 59 60 * **Card-based UI**: Clean, modern interface for managing rules. 61 * **Live preview**: See upcoming events and visibility windows instantly. 62 * **Bilingual**: Full German and English support with one-click language switching. 71 63 72 64 == Installation == 73 65 74 1. Upload the plugin folder to `/wp-content/plugins/` or install itvia the WordPress Plugins screen.66 1. Upload the plugin folder to `/wp-content/plugins/` or install via the WordPress Plugins screen. 75 67 2. Activate **Luzid Content Scheduler**. 76 68 3. Go to **WP Admin → Luzid Content Scheduler**. … … 80 72 == Usage == 81 73 82 = Add the CSS class to your block = 83 84 * **Gutenberg**: select block → "Advanced" → "Additional CSS class(es)" → `luzid-cs-<slug>` 85 * **Elementor**: widget → "Advanced" → "CSS Classes" → `luzid-cs-<slug>` 86 * **Divi/others**: module settings → "CSS Class" → `luzid-cs-<slug>` 87 88 = Shortcode output = 89 90 Examples: 91 92 * Long (default): `[luzid_cs slug="wartung" opt="long"]` 93 * Date only: `[luzid_cs slug="wartung" opt="date"]` 94 * Time only: `[luzid_cs slug="wartung" opt="time"]` 95 * List (next 7): `[luzid_cs slug="wartung" opt="short" list="true" count="7"]` 96 * List + event text: `[luzid_cs slug="krimidinner" opt="short" list="true" count="5" text="true" sep=" | "]` 97 * Event text on new line: `[luzid_cs slug="krimidinner" opt="date" text="true" sep="<br>"]` 98 99 CSS hooks (no default styling – add your own CSS if you want): 100 101 * `.luzid-cs` (wrapper, plus `.luzid-cs--single` / `.luzid-cs--list`) 102 * `.luzid-cs-item` (one event in list output) 103 * `.luzid-cs-opt` (date/time part) 104 * `.luzid-cs-sep` (separator) 105 * `.luzid-cs-text` (optional event text) 74 = Adding CSS Classes to Blocks = 75 76 * **Gutenberg**: Select block → "Advanced" → "Additional CSS class(es)" → `luzid-cs-<slug>` 77 * **Elementor**: Widget → "Advanced" → "CSS Classes" → `luzid-cs-<slug>` 78 * **Divi/others**: Module settings → "CSS Class" → `luzid-cs-<slug>` 79 80 = Basic Shortcode = 81 82 `[luzid_cs slug="your-scheduler"]` 83 84 Outputs: The next event date in the default format. 85 86 = Shortcode Parameters = 87 88 | Parameter | Default | Description | 89 |-----------|---------|-------------| 90 | `slug` | (required) | Scheduler slug | 91 | `date` | (empty) | Date format: `short`, `medium`, `long`, `full` | 92 | `time` | (empty) | Time format: `auto`, `raw`, `prefix`, `range`, `range_long` | 93 | `list` | `false` | Output as list: `true` or `false` | 94 | `count` | `10` | Number of events for lists (max 200) | 95 | `text` | `false` | Include event text: `true` or `false` | 96 | `sep1` | (space) | Separator between date and time | 97 | `sep2` | (space) | Separator between time and text | 98 | `sep3` | (empty) | Separator between list items | 99 | `timeoffset` | `0` | Time offset in minutes (can be negative) | 100 | `lang` | (current) | Force language: `de` or `en` | 101 102 = Date Formats = 103 104 | Value | Output Example | 105 |-------|----------------| 106 | `short` | 14.02.2026 | 107 | `medium` | Sat, 14.02.2026 | 108 | `long` | Saturday, 14.02.2026 | 109 | `full` | Saturday, 14 February 2026 | 110 111 = Time Formats = 112 113 | Value | Output Example | 114 |-------|----------------| 115 | (empty) | No time output | 116 | `raw` | 18:00 | 117 | `prefix` | from 18:00 | 118 | `range` | 18:00 to 20:00 | 119 | `range_long` | from 18:00 to 20:00 | 120 | `auto` | Intelligent format based on data | 121 122 = Shortcode Examples = 123 124 **Simple date output:** 125 `[luzid_cs slug="maintenance" date="long"]` 126 → Saturday, 15.03.2026 127 128 **Date with time:** 129 `[luzid_cs slug="event" date="long" time="auto"]` 130 → Saturday, 15.03.2026 from 18:00 to 20:00 131 132 **Only time output:** 133 `[luzid_cs slug="event" time="raw"]` 134 → 18:00 135 136 **Multi-line output:** 137 `[luzid_cs slug="event" date="long" time="auto" text="true" sep1="<br>" sep2="<br>"]` 138 → Saturday, 15.03.2026 139 from 18:00 to 20:00 140 Valentine's Dinner 141 142 **List of next 5 events:** 143 `[luzid_cs slug="concert" date="long" text="true" list="true" count="5" sep2="<br>" sep3="<br><br>"]` 144 145 **With custom separators:** 146 `[luzid_cs slug="event" date="long" time="raw" text="true" sep1=" | " sep2=" – "]` 147 → Saturday, 15.03.2026 | 18:00 – Valentine's Dinner 148 149 = Event Table Shortcode = 150 151 `[luzid_cs_eventtable]` 152 153 Outputs a table of all upcoming events from schedulers with "Include in Event Table" enabled. 154 155 | Parameter | Default | Description | 156 |-----------|---------|-------------| 157 | `cols` | `date_medium,time_auto,text` | Columns (comma-separated) | 158 | `count` | `30` | Maximum number of events | 159 | `headers` | (automatic) | Custom column headers | 160 | `noheaders` | `false` | Hide table headers | 161 | `class` | (empty) | Additional CSS class | 162 | `empty` | "No events" | Text when no events found | 163 | `order` | `asc` | Sort order: `asc` or `desc` | 164 165 **Available columns:** 166 `date_short`, `date_medium`, `date_long`, `date_full`, `weekday_short`, `weekday_long`, `time_raw`, `time_auto`, `time_prefix`, `time_range`, `time_range_long`, `text`, `scheduler` 167 168 **Example with custom columns:** 169 `[luzid_cs_eventtable cols="weekday_short,date_short,time_range,text" count="10" headers="Day,Date,Time,Event"]` 170 171 = Event Classes (New in 1.4) = 172 173 Event Classes allow you to create multiple independent visibility windows for a single scheduler. Each class has its own CSS class and offset settings. 174 175 **Example setup:** 176 177 | Class | CSS Class | Days Before | Use Case | 178 |-------|-----------|-------------|----------| 179 | Standard | `.luzid-cs-dinner` | 14 | Main content | 180 | popup | `.luzid-cs-dinner-popup` | 4 | Announcement popup | 181 | menu | `.luzid-cs-dinner-menu` | 11 | Navigation menu item | 182 183 This allows you to control when different elements appear, all based on the same event schedule. 184 185 = CSS Hooks = 186 187 **Shortcode output:** 188 * `.luzid-cs` — Wrapper (plus `.luzid-cs--single` / `.luzid-cs--list`) 189 * `.luzid-cs-item` — Single event in list 190 * `.luzid-cs-date` — Date part 191 * `.luzid-cs-time` — Time part 192 * `.luzid-cs-text` — Event text 193 * `.luzid-cs-sep` / `.luzid-cs-sep1` / `.luzid-cs-sep2` — Separators 194 195 **Event table:** 196 * `.luzid-cs-eventtable` — Table wrapper 197 * `.luzid-cs-eventtable__head` / `__body` / `__row` / `__cell` 198 199 **Body classes (for conditional CSS):** 200 * `.luzid-cs-active-<slug>` — Added when scheduler is active 201 * `.luzid-cs-active-<slug>-<classname>` — Added when specific event class is active 106 202 107 203 == Frequently Asked Questions == … … 115 211 Exceptions affect **everything** (frontend visibility, next event calculation, and preview). If "now" is inside an exception range, the block stays hidden. 116 212 213 = What's the difference between Event Offset (old) and Event Classes (new)? = 214 215 Event Classes replaced the old Event Offset feature in version 1.4. The key difference: you can now create **multiple classes** with independent offsets, allowing different content to appear at different times before/after an event. 216 217 = Can I use multiple Event Classes for one block? = 218 219 Each Event Class generates a separate CSS class. You can only apply one class per block, but you can have multiple blocks, each with a different Event Class. 220 117 221 = Does the plugin require a specific theme? = 118 222 119 223 No. It works with any theme/page builder that lets you set a custom CSS class on a block/element. 120 224 225 = Can I output only the time without the date? = 226 227 Yes! Use `[luzid_cs slug="your-slug" time="raw"]` to output only the time. Leave out the `date` parameter. 228 229 = How do I create a line break between date and text? = 230 231 Use `sep2="<br>"`: `[luzid_cs slug="event" date="long" text="true" sep2="<br>"]` 232 121 233 == Screenshots == 122 234 123 1. Setup tab: create a scheduler and copy the CSS class / shortcode. 124 2. Add single dates or date ranges. 125 3. Recurring rules with weekday/month options. 126 4. Exceptions override all other rules. 127 5. Preview of upcoming events. 235 1. Setup tab: Create a scheduler and copy the CSS class / shortcode. 236 2. Dates tab: Add single dates or date ranges with times and event text. 237 3. Recurring tab: Define weekly, monthly, or yearly patterns. 238 4. Exceptions tab: Block specific dates from showing content. 239 5. Event Classes tab: Create multiple CSS classes with independent visibility windows. 240 6. Preview tab: See all upcoming events at a glance. 128 241 129 242 == License == … … 134 247 135 248 Assets: 136 137 * Luzid Logo and Flag icons are SVG assets shipped with the plugin (assets/img/luzid-media-logo-plugins.svg | assets/img/ger.svg | assets/img/uk.svg). 249 * Luzid Logo and Flag icons are SVG assets shipped with the plugin (assets/img/). 138 250 139 251 == Changelog == 140 252 141 = 1.2.2 = 142 * Initial WordPress.org release. 143 * Shortcode: `[luzid_cs slug="..." opt="long"]` 144 * CSS class: `luzid-cs-<slug>` 145 * Body class: `luzid-cs-active-<slug>` 146 * Full WordPress coding standards compliance. 253 = 1.4.2 = 254 * Fixed: Version number display in admin header. 255 * Improved: Event Classes UI with copy button for CSS classes and consistent icon styling. 256 257 = 1.4.1 = 258 * Fixed: WordPress Plugin Check escaping warnings for Event Classes panel. 259 260 = 1.4.0 = 261 * **New: Event Classes** — Replace single Event Offset with unlimited named classes, each with independent visibility offsets. 262 * New: Each Event Class generates its own CSS class (`luzid-cs-<slug>-<classname>`). 263 * New: Live preview of visibility windows per class. 264 * New: Standard class (non-deletable) maintains backward compatibility. 265 * Changed: Tab renamed from "Event-Offset" to "Event-Klassen". 266 * Improved: Card-based UI consistent with other tabs. 267 * Migration: Existing Event Offset settings are automatically converted to the new Event Classes structure. 268 269 = 1.3.12 = 270 * Fixed: Shortcode now outputs exactly what is specified — no hidden defaults. 271 * Fixed: `date` parameter defaults to empty (not "long"). Only when neither `date` nor `time` is set, `date="long"` is used as fallback. 272 * Fixed: Using only `time="raw"` now correctly outputs only the time, not date + time. 273 274 = 1.3.11 = 275 * Fixed: Auto-padding removed from separators. ` ` now outputs exactly one non-breaking space. 276 277 = 1.3.10 = 278 * Fixed: Single space separator handling — WordPress trims `sep1=" "`, now uses default fallback. 279 280 = 1.3.9 = 281 * **New: Separator system refactored** — `sep` replaced by `sep1`, `sep2`, `sep3` for precise control. 282 * New: `sep1` controls separator between date and time. 283 * New: `sep2` controls separator between time and text (or date and text if no time). 284 * New: `sep3` controls separator between list items. 285 * Changed: `time` parameter now defaults to empty (no time output unless explicitly requested). 286 * Breaking: Old `sep` parameter removed. Use `sep1`/`sep2`/`sep3` instead. 287 288 = 1.3.8 = 289 * Fixed: Preview table now shows actual saved event text per date. 290 291 = 1.3.7 = 292 * Fixed: Yearly recurrence calculation for events spanning year boundaries. 293 294 = 1.3.6 = 295 * New: `[luzid_cs_eventtable]` shortcode for tabular event output. 296 * New: "Include in Event Table" checkbox per scheduler. 297 * New: Customizable columns, headers, and sorting for event tables. 298 299 = 1.3.5 = 300 * Improved: Card-based layout unified across all tabs. 301 302 = 1.3.4 = 303 * New: Dual-prefix system for Termine tab (CSS class prefix options). 304 305 = 1.3.0 = 306 * New: "Time to" field for time ranges (e.g., 18:00 – 20:00) in single dates and recurring rules. 307 * New: Yearly recurring rule type (e.g., every February 14th at 16:00). 308 * New: Card-based layout for Termine and Wiederholungen tabs. 309 * Changed: Shortcode renamed from `[lz-cs]` to `[luzid_cs]` for WordPress.org compliance. 310 * Changed: CSS classes renamed from `lz-cs-*` to `luzid-cs-*`. 311 312 = 1.2.9 = 313 * Changed: List wrapper from `<span>` to `<div>` for better HTML structure. 314 315 = 1.2.7 = 316 * Fix: Prevent "phantom" events from incomplete recurring rules. 317 * New: Preview table shows event source (Single vs Recurring). 318 319 = 1.2.6 = 320 * New: Shortcode output wrapped in styled span classes. 321 * Improved: `sep` parameter can output `<br>` for line breaks. 322 323 = 1.2.5 = 324 * Fix: Separator spacing preserved correctly. 325 * Improvement: Single dates sorted chronologically. 326 * New: Optional event text for recurring rules. 327 328 = 1.2.3 = 329 * Added: Optional event text field for single dates. 330 * Changed: Shortcode supports `list`, `count`, `text` and `sep` parameters. 331 332 = 1.2.0 = 333 * Initial release.
Note: See TracChangeset
for help on using the changeset viewer.