Skip to content

Commit edfe9b1

Browse files
committed
feat: Tagging-Widget mit forCalTaggingHelper
- Neuer Feldtyp tagging in entries.yml (standardmäßig aktiv) - forCalTaggingHelper: eigenständige Hilfsklasse (forCal\Utils) - Eigener Suggest-API-Endpunkt rex_api_forcal_tagging_suggest - Eigenständige Assets forcal-tagging.js / forcal-tagging.css - DB-Spalte tags in forcal_entries (install + update) - Version 6.4.0
1 parent 0cdcc12 commit edfe9b1

12 files changed

Lines changed: 1207 additions & 1 deletion

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,24 @@
1+
## 6.4.0 - 2026-03-13
2+
3+
### Added
4+
5+
- **Tagging-Widget für Custom Fields**: Neuer Feldtyp `tagging` in YAML-Definitionen ermöglicht farbige Schlagwörter direkt im Termin-/Kategorie-Formular.
6+
- **`forCalTaggingHelper`** (`lib/forcal/Utils/forCalTaggingHelper.php`): Eigenständige PHP-Klasse (`forCal\Utils` Namespace) mit folgenden Methoden:
7+
- `decode(string $raw): array` – JSON → `list<array{text, color}>`
8+
- `encode(array $tags): string` – Tags → JSON-String
9+
- `getTexts(array $tags): array` – Nur Texte
10+
- `toHtml(array $tags, string $emptyText = ''): string` – Farbige Chip-Spans
11+
- `chipHtml(string $text, string $color): string` – Einzelner Chip
12+
- `fromRaw(string $raw, string $emptyText = ''): string` – Kurzform decode + render
13+
- `collectFromTable(string $table, string $field): array` – Alle eindeutigen Tags aus DB (alphabetisch)
14+
- `collectTextsFromTable(string $table, string $field): array` – Nur Texte aus DB
15+
- `sqlHasTag(string $field, string $tagText): string` – MySQL `JSON_SEARCH`-WHERE-Fragment
16+
- `filterByTag(array $rows, string $field, string $tagText): array` – PHP-seitiger Filter
17+
- `renderWidget(string $fieldId, string $fieldName, string $value, array $options): string` – Vollständiges Widget-HTML für `rex_form`
18+
- **Eigener Suggest-API-Endpunkt** (`rex_api_forcal_tagging_suggest`): Tabellen-Whitelist (`forcal_entries`, `forcal_categories`, `forcal_venues`), erweiterbar via Extension Point `FORCAL_TAGGING_ALLOWED_TABLES`.
19+
- **Eigenständige Assets** (`forcal-tagging.js`, `forcal-tagging.css`): Werden automatisch geladen, ohne Konflikte mit anderen Addons.
20+
- **Custom Color Picker mit WCAG-Kontrastprüfung** (Ratio ≥ 3,0:1 für weiße Schrift).
21+
122
## 6.3.0 - 2026-02-18
223

324
### Added

README.md

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,110 @@ Orte-bezogene Felder werden ausgeblendet, wenn die Orte-Funktionalität deaktivi
426426
### JavaScript-Validierung für Pflichtfelder
427427
Validierung von Pflichtfeldern wie Terminname und Kategorie direkt im Browser mit benutzerfreundlichen Fehlermeldungen.
428428

429+
### Tagging-Widget für eigene Felder
430+
431+
Mit dem Feldtyp `tagging` in einer YAML-Definition können farbige Schlagwörter (Tags) direkt im Termin-Formular vergeben werden.
432+
433+
**Beispiel `custom_entries.yml`:**
434+
435+
```yaml
436+
fields:
437+
- name: 'tags'
438+
type: 'tagging'
439+
label_de: 'Schlagwörter'
440+
label_en: 'Tags'
441+
source_table: 'forcal_entries'
442+
source_field: 'tags'
443+
max_tags: 10
444+
```
445+
446+
Voraussetzung: Die Spalte muss in der Datenbank vorhanden sein, z.B.:
447+
448+
```sql
449+
ALTER TABLE rex_forcal_entries ADD tags TEXT NULL;
450+
```
451+
452+
Das Widget lädt ein farbiges Chip-Panel mit Autocomplete-Vorschlägen aus der konfigurierten Quelltabelle. Eigene Farben sind per Farbpicker wählbar – eine WCAG-Kontrastprüfung (Ratio ≥ 3,0) verhindert dabei zu helle Farben für weiße Schrift.
453+
454+
Die gespeicherten Daten haben das Format `[{"text":"php","color":"#2980b9"}, ...]`.
455+
456+
---
457+
458+
## Tags auswerten mit `forCalTaggingHelper`
459+
460+
Die Klasse `forCal\Utils\forCalTaggingHelper` (wird automatisch über den REDAXO-Autoloader geladen, keine Abhängigkeit zum `fields`-Addon) bietet alle nötigen Methoden:
461+
462+
```php
463+
use forCal\Utils\forCalTaggingHelper;
464+
```
465+
466+
### Dekodieren & Rendern
467+
468+
```php
469+
// JSON → Array
470+
$tags = forCalTaggingHelper::decode($item['tags']);
471+
// → [["text" => "php", "color" => "#2980b9"], ...]
472+
473+
// Nur Texte
474+
$texte = forCalTaggingHelper::getTexts($tags);
475+
// → ["php", "redaxo"]
476+
477+
// Als farbige HTML-Chips ausgeben
478+
echo forCalTaggingHelper::toHtml($tags);
479+
// → <span style="background:#2980b9;...">php</span> ...
480+
481+
// Direkt aus DB-Rohwert (Kurzform)
482+
echo forCalTaggingHelper::fromRaw($item['tags'], '–');
483+
484+
// Einzelnen Chip rendern
485+
echo forCalTaggingHelper::chipHtml('php', '#2980b9');
486+
487+
// Array → JSON-String (z.B. nach Manipulation)
488+
$json = forCalTaggingHelper::encode($tags);
489+
```
490+
491+
### Datenbankabfragen
492+
493+
```php
494+
// Alle eindeutigen Tags einer Tabellenspalte sammeln (alphabetisch)
495+
$alleTags = forCalTaggingHelper::collectFromTable('rex_forcal_entries', 'tags');
496+
// → [["text" => "php", "color" => "#2980b9"], ...]
497+
498+
// Nur Texte
499+
$texte = forCalTaggingHelper::collectTextsFromTable('rex_forcal_entries', 'tags');
500+
// → ["php", "redaxo", ...]
501+
502+
// SQL-WHERE-Fragment (MySQL ≥ 5.7, JSON_SEARCH)
503+
$sql = rex_sql::factory();
504+
$rows = $sql->getArray(
505+
'SELECT * FROM rex_forcal_entries WHERE '
506+
. forCalTaggingHelper::sqlHasTag('tags', 'php')
507+
);
508+
509+
// PHP-seitiger Filter (auf bereits geladene Zeilen)
510+
$gefiltert = forCalTaggingHelper::filterByTag($rows, 'tags', 'php');
511+
```
512+
513+
### In FORCalEventsFactory integrieren
514+
515+
```php
516+
use forCal\Factory\FORCalEventsFactory;
517+
use forCal\Utils\forCalTaggingHelper;
518+
519+
$factory = new FORCalEventsFactory();
520+
$events = $factory->getEventsByMonth(date('Y'), date('m'));
521+
522+
// Nur Events mit Tag "php" anzeigen
523+
$filtered = forCalTaggingHelper::filterByTag($events, 'tags', 'php');
524+
525+
foreach ($filtered as $event) {
526+
echo '<h2>' . rex_escape($event['name_1']) . '</h2>';
527+
echo forCalTaggingHelper::fromRaw($event['tags'], '–');
528+
}
529+
```
530+
531+
---
532+
429533
## Installation und Kompatibilität
430534

431535
### Automatische Tabellenanpassung

assets/forcal-tagging.css

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
/* ============================================================
2+
Forcal Tagging Widget
3+
Eigenständig – keine Abhängigkeit zum fields-Addon.
4+
Kompatibel mit fields_tagging (gleiche CSS-Klassen).
5+
============================================================ */
6+
7+
/* --- Chip-Anzeige ----------------------------------------- */
8+
.fields-tagging-chips {
9+
display: flex;
10+
flex-wrap: wrap;
11+
align-items: center;
12+
gap: 6px;
13+
min-height: 34px;
14+
padding: 4px 0;
15+
}
16+
17+
.fields-tagging-chip {
18+
display: inline-flex;
19+
align-items: center;
20+
gap: 5px;
21+
padding: 3px 10px;
22+
border-radius: 12px;
23+
color: #fff;
24+
font-size: 12px;
25+
font-weight: 500;
26+
line-height: 1.4;
27+
box-shadow: 0 1px 3px rgba(0,0,0,.18);
28+
transition: opacity .15s;
29+
}
30+
31+
.fields-tagging-chip:hover {
32+
opacity: .85;
33+
}
34+
35+
.fields-tagging-chip-remove {
36+
background: none;
37+
border: none;
38+
padding: 0;
39+
margin: 0;
40+
color: rgba(255,255,255,.85);
41+
font-size: 14px;
42+
line-height: 1;
43+
cursor: pointer;
44+
transition: color .1s;
45+
}
46+
47+
.fields-tagging-chip-remove:hover {
48+
color: #fff;
49+
}
50+
51+
/* --- Panel ------------------------------------------------- */
52+
.fields-tagging-panel {
53+
margin-top: 8px;
54+
padding: 14px 16px 12px;
55+
border: 1px solid #ddd;
56+
border-radius: 6px;
57+
background: #fff;
58+
box-shadow: 0 3px 10px rgba(0,0,0,.08);
59+
}
60+
61+
body.rex-theme-dark .fields-tagging-panel {
62+
background: #2a2d35;
63+
border-color: #444;
64+
}
65+
66+
@media (prefers-color-scheme: dark) {
67+
body.rex-has-theme:not(.rex-theme-light) .fields-tagging-panel {
68+
background: #2a2d35;
69+
border-color: #444;
70+
}
71+
}
72+
73+
/* --- Farbpalette ------------------------------------------- */
74+
.fields-tagging-palette {
75+
display: flex;
76+
align-items: center;
77+
gap: 6px;
78+
margin-bottom: 10px;
79+
flex-wrap: wrap;
80+
}
81+
82+
.fields-tagging-palette-label {
83+
font-size: 12px;
84+
color: #888;
85+
margin-right: 2px;
86+
white-space: nowrap;
87+
}
88+
89+
.fields-tagging-color-btn {
90+
width: 22px;
91+
height: 22px;
92+
border-radius: 50%;
93+
border: 3px solid transparent;
94+
padding: 0;
95+
cursor: pointer;
96+
transition: transform .1s, border-color .1s;
97+
outline: none;
98+
box-shadow: 0 1px 3px rgba(0,0,0,.2);
99+
}
100+
101+
.fields-tagging-color-btn:hover {
102+
transform: scale(1.2);
103+
}
104+
105+
.fields-tagging-color-btn.active {
106+
border-color: #fff;
107+
outline: 2px solid #555;
108+
transform: scale(1.15);
109+
}
110+
111+
/* --- Eigene Farbe (Color Picker) --------------------------- */
112+
.fields-tagging-palette-sep {
113+
width: 1px;
114+
height: 20px;
115+
background: #ddd;
116+
margin: 0 4px;
117+
display: inline-block;
118+
align-self: center;
119+
}
120+
121+
body.rex-theme-dark .fields-tagging-palette-sep { background: #555; }
122+
123+
@media (prefers-color-scheme: dark) {
124+
body.rex-has-theme:not(.rex-theme-light) .fields-tagging-palette-sep { background: #555; }
125+
}
126+
127+
.fields-tagging-custom-color {
128+
width: 28px;
129+
height: 22px;
130+
padding: 1px 2px;
131+
border: 2px solid transparent;
132+
border-radius: 4px;
133+
cursor: pointer;
134+
background: none;
135+
vertical-align: middle;
136+
transition: border-color .15s, box-shadow .15s;
137+
}
138+
139+
.fields-tagging-custom-color:hover { border-color: #aaa; }
140+
141+
.fields-tagging-custom-color.fields-tagging-color-invalid {
142+
border-color: #e74c3c;
143+
box-shadow: 0 0 0 2px rgba(231,76,60,.3);
144+
}
145+
146+
.fields-tagging-contrast-hint {
147+
font-size: 11px;
148+
color: #e74c3c;
149+
align-self: center;
150+
}
151+
152+
body.rex-theme-dark .fields-tagging-contrast-hint { color: #ff7675; }
153+
154+
@media (prefers-color-scheme: dark) {
155+
body.rex-has-theme:not(.rex-theme-light) .fields-tagging-contrast-hint { color: #ff7675; }
156+
}
157+
158+
/* --- Eingabezeile ------------------------------------------ */
159+
.fields-tagging-input-group {
160+
margin-bottom: 12px;
161+
}
162+
163+
.fields-tagging-color-preview {
164+
display: inline-block;
165+
transition: background .15s;
166+
}
167+
168+
/* --- Vorschläge -------------------------------------------- */
169+
.fields-tagging-suggestions-wrap {
170+
margin-bottom: 10px;
171+
}
172+
173+
.fields-tagging-suggestions-label {
174+
font-size: 11px;
175+
color: #999;
176+
margin-bottom: 6px;
177+
text-transform: uppercase;
178+
letter-spacing: .04em;
179+
}
180+
181+
.fields-tagging-suggestions {
182+
display: flex;
183+
flex-wrap: wrap;
184+
gap: 5px;
185+
min-height: 28px;
186+
}
187+
188+
.fields-tagging-suggestion-chip {
189+
display: inline-flex;
190+
align-items: center;
191+
padding: 3px 10px;
192+
border-radius: 12px;
193+
color: #fff;
194+
font-size: 12px;
195+
cursor: pointer;
196+
opacity: .75;
197+
transition: opacity .15s, transform .1s;
198+
box-shadow: 0 1px 3px rgba(0,0,0,.15);
199+
border: none;
200+
outline: none;
201+
}
202+
203+
.fields-tagging-suggestion-chip:hover {
204+
opacity: 1;
205+
transform: scale(1.05);
206+
}
207+
208+
.fields-tagging-suggestion-chip.is-selected {
209+
opacity: 1;
210+
outline: 2px solid rgba(255,255,255,.6);
211+
outline-offset: 1px;
212+
}
213+
214+
/* --- Footer ----------------------------------------------- */
215+
.fields-tagging-panel-footer {
216+
display: flex;
217+
align-items: center;
218+
padding-top: 8px;
219+
border-top: 1px solid #eee;
220+
}
221+
222+
body.rex-theme-dark .fields-tagging-panel-footer { border-color: #444; }
223+
224+
@media (prefers-color-scheme: dark) {
225+
body.rex-has-theme:not(.rex-theme-light) .fields-tagging-panel-footer { border-color: #444; }
226+
}

0 commit comments

Comments
 (0)