Skip to main content

✏️ Editierbare TV-Übersicht

Diese Anleitung beschreibt eine Frontend-Lösung, um ausgewählte Template Variablen (TVs) von Ressourcen in einer Tabelle anzuzeigen und direkt bearbeiten zu können.
Die Bearbeitung ist nur für eingeloggte, berechtigte User möglich.

Das Layout wird vollständig durch Chunks gesteuert und kann für Bootswatch/Bootstrap-5 flexibel angepasst werden.

Die verwendeten TVs, IDs sowie der verwendete Context bzw. die verwendeten Contexts (z.B. web,mobile,staging) müssen ggf. angepasst werden.

  • tvStartseite (Radio Options, gespeicherter Wert: ja oder nein)
  • tvStartdatum (Text)
  • tvEnddatum (Text)

✏️ Snippet erstellen: tvFrontpageManager

tvFrontpageManager.jpg

Dieses Snippet verarbeitet Formularabschickungen und speichert die übergebenen TV-Werte.

Name des Snippets: tvFrontpageManager

<?php
/**
 * Snippet: tvFrontpageManager
 * Verarbeitet Formularabschickungen und speichert die TV-Werte.
 * V 2025-12-04
 *
 * Aufruf-Beispiel:
 * [[!tvFrontpageManager?
 *    &allowed_groups=`Redaktion,Administrator`
 *    &contexts=`web,mobile,staging`
 *    &tv_startseite=`tvStartseite`
 *    &tv_startdatum=`tvStartdatum`
 *    &tv_enddatum=`tvEnddatum`
 * ]]
 */

$allowedGroups   = isset($scriptProperties['allowed_groups'])
    ? array_map('trim', explode(',', $scriptProperties['allowed_groups']))
    : [];

// Contexts für Cache-Refresh (Standard: web)
$contextsOption = $scriptProperties['contexts'] ?? '';
if ($contextsOption !== '') {
    $contexts = array_filter(array_map('trim', explode(',', $contextsOption)));
} else {
    $contexts = ['web'];
}

$tv1 = $scriptProperties['tv_startseite'] ?? '';
$tv2 = $scriptProperties['tv_startdatum'] ?? '';
$tv3 = $scriptProperties['tv_enddatum'] ?? '';

// Regex wie im TV-Validator (Frontend-Validierung)
// siehe FullCalendar
// erlaubt ist z.B.: 2025-11-28 10:00 || 2025-11-30 22:00:00
$tvDatePattern = '#^\s*\d{4}-\d{2}-\d{2}(?:\s+[0-2]\d:[0-5]\d(?:\:[0-5]\d)?)?(?:\s*\|\|\s*\d{4}-\d{2}-\d{2}(?:\s+[0-5]\d(?:\:[0-5]\d)?)?)*\s*$#u';

// Zugriff nur für eingeloggte User im Manager oder Web-Kontext
if (
    !$modx->user
    || (
        !$modx->user->isAuthenticated('mgr')
        && !$modx->user->isAuthenticated('web')
    )
) {
    return '';
}

// Gruppenprüfung (optional über &allowed_groups)
$hasAccess = false;
if (!empty($allowedGroups)) {
    foreach ($allowedGroups as $group) {
        if ($modx->user->isMember($group)) {
            $hasAccess = true;
            break;
        }
    }
} else {
    // Wenn keine Gruppen angegeben sind, alle eingeloggten User zulassen
    $hasAccess = true;
}

if (!$hasAccess) {
    return '';
}

// Formularverarbeitung
if (
    $_SERVER['REQUEST_METHOD'] === 'POST'
    && !empty($_POST['tvmanager_action'])
    && $_POST['tvmanager_action'] === 'save_tvs'
) {
    if (empty($_POST['resources']) || !is_array($_POST['resources'])) {
        return '';
    }

    // Ressourcen-IDs aus dem Formular einsammeln
    $resources = [];
    foreach ($_POST['resources'] as $id) {
        $id = (int)$id;
        if ($id > 0) {
            $resources[] = $id;
        }
    }

    if (empty($resources)) {
        return '';
    }

    // Arrays für Datumsfehler (optional für die Meldung)
    $invalidStartDates = [];
    $invalidEndDates   = [];

    foreach ($resources as $resId) {
        /** @var modResource $resource */
        $resource = $modx->getObject('modResource', $resId);
        if (!$resource) {
            continue;
        }

        // tvStartseite: Radio Options ja||nein
        // Formular-Name: tvStartseite[ID]
        if ($tv1 !== '') {
            $valueStartseite = (
                isset($_POST['tvStartseite'][$resId])
                && $_POST['tvStartseite'][$resId] !== ''
            ) ? 'ja' : 'nein';

            $resource->setTVValue($tv1, $valueStartseite);
        }

        // tvStartdatum: Text, mit Regex-Validierung
        // Formular-Name: tvStartdatum[ID]
        if ($tv2 !== '') {
            $valueStartdatum = isset($_POST['tvStartdatum'][$resId])
                ? trim($_POST['tvStartdatum'][$resId])
                : '';

            // leere Werte sind erlaubt, sonst Regex prüfen
            if ($valueStartdatum === '' || preg_match($tvDatePattern, $valueStartdatum)) {
                $resource->setTVValue($tv2, $valueStartdatum);
            } else {
                $invalidStartDates[] = $resId;
            }
        }

        // tvEnddatum: Text, mit Regex-Validierung
        // Formular-Name: tvEnddatum[ID]
        if ($tv3 !== '') {
            $valueEnddatum = isset($_POST['tvEnddatum'][$resId])
                ? trim($_POST['tvEnddatum'][$resId])
                : '';

            // leere Werte sind erlaubt, sonst Regex prüfen
            if ($valueEnddatum === '' || preg_match($tvDatePattern, $valueEnddatum)) {
                $resource->setTVValue($tv3, $valueEnddatum);
            } else {
                $invalidEndDates[] = $resId;
            }
        }
    }

    // Cache aktualisieren, damit Änderungen direkt sichtbar werden
    $modx->cacheManager->refresh([
        'resource' => [
            'contexts' => $contexts,
        ],
    ]);

    // Meldung für das Frontend
    $message = 'Die TV-Werte wurden gespeichert.';
    if (!empty($invalidStartDates) || !empty($invalidEndDates)) {
        $message .= ' Hinweis: Einige Datumsangaben waren ungültig und wurden nicht übernommen.';
    }

    $modx->setPlaceholder('tvmanager_message', $message);
}

return '';

🧩 Chunk erstellen: tvAdminOuter_bs5

Dieser Chunk enthält das Formular und die Bootstrap-5-Struktur (ideal für Bootswatch-Themes).

<div class="container my-4">
    [[+tvmanager_message:notempty=`
    <div class="alert alert-success" role="alert">
        [[+tvmanager_message]]
    </div>
    `]]

    <div class="card shadow-sm">
        <div class="card-header">
            TV-Übersicht (auf Startseite anzeigen & Datum Kalender)
        </div>

        <div class="card-body">
            <form method="post">
                <input type="hidden" name="tvmanager_action" value="save_tvs">

                <!-- Filter + Speichern oben -->
                <div class="d-flex flex-wrap justify-content-between align-items-center mb-2">
                    <div class="col-sm-6 col-md-4 p-0">
                        <input type="text"
                               id="tvFilterInput"
                               class="form-control form-control-sm"
                               placeholder="Tabelle filtern …">
                    </div>

                    <button type="submit" class="btn btn-primary btn-sm ms-2 mt-2 mt-sm-0">
                        Speichern
                    </button>
                </div>

                <div class="table-responsive">
                    <table id="tvTable" class="table table-striped table-sm align-middle">
                        <thead class="table-light">
                            <tr>
                                <th scope="col">ID</th>
                                <th scope="col">Titel</th>
                                <th scope="col">Startseite</th>
                                <th scope="col">Startdatum</th>
                                <th scope="col">Enddatum</th>
                            </tr>
                        </thead>

                        <tbody>
                            [[+output]]
                        </tbody>
                    </table>
                </div>

                <!-- Speichern unten -->
                <button type="submit" class="btn btn-primary mt-2">
                    Speichern
                </button>
            </form>
        </div>
    </div>
</div>

<script>
document.addEventListener('DOMContentLoaded', function () {
    // 1) Einfacher Filter für die Tabelle
    var input = document.getElementById('tvFilterInput');
    if (input) {
        input.addEventListener('input', function () {
            var filter = this.value.toLowerCase();
            var rows = document.querySelectorAll('#tvTable tbody tr');

            rows.forEach(function (row) {
                var text = row.textContent.toLowerCase();
                row.style.display = text.indexOf(filter) !== -1 ? '' : 'none';
            });
        });

        // 2) Enter im Filterfeld verhindern (kein Formular-Submit)
        input.addEventListener('keydown', function (e) {
            if (e.key === 'Enter' || e.keyCode === 13) {
                e.preventDefault();
            }
        });
    }

    // 3) Meldung nach 4 Sekunden automatisch ausblenden
    var alertBox = document.querySelector('.alert.alert-success');
    if (alertBox) {
        setTimeout(function () {
            alertBox.style.transition = 'opacity 0.5s';
            alertBox.style.opacity = '0';

            setTimeout(function () {
                if (alertBox && alertBox.parentNode) {
                    alertBox.parentNode.removeChild(alertBox);
                }
            }, 600);
        }, 4000);
    }
});
</script>

📄 Chunk erstellen: tvAdminRow

Dieser Chunk definiert eine Tabellenzeile für jede Ressource.

<tr>
    <td>
        [[+id]]
        <input type="hidden" name="resources[]" value="[[+id]]">
    </td>

    <td>[[+pagetitle]]</td>

    <td class="text-center">
        <input type="checkbox"
               name="tvStartseite[[+id:prepend=`[`:append=`]`]]"
               value="ja"
               [[+tvStartseite:is=`ja`:then=`checked="checked"`:else=``]]>
        <!-- Debug zum Prüfen des Rohwertes:
        <small class="text-muted d-block">[[+tvStartseite]]</small>
        -->
    </td>

    <td>
        <input type="text"
               class="form-control form-control-sm"
               name="tvStartdatum[[+id:prepend=`[`:append=`]`]]"
               value="[[+tvStartdatum]]">
    </td>

    <td>
        <input type="text"
               class="form-control form-control-sm"
               name="tvEnddatum[[+id:prepend=`[`:append=`]`]]"
               value="[[+tvEnddatum]]">
    </td>
</tr>

🌐 Einbindung in die gewünschte Frontpage-Ressource

Z.B.: TV-Übersicht (Alias: tv-overview)

Der folgende Code erzeugt die editierbare Tabellenansicht.
parents muss auf die ID des Artikel-Containers angepasst werden.

[[!tvFrontpageManager?
    &allowed_groups=`Redaktion,Administrator`
    &contexts=`web`
    &tv_startseite=`tvStartseite`
    &tv_startdatum=`tvStartdatum`
    &tv_enddatum=`tvEnddatum`
]]

[[!pdoPage?
    &element=`pdoResources`
    &parents=`4,6,7,8,24`
    &depth=`2`
    &limit=`50`
    &showUnpublished=`0`
    &showHidden=`0`    
    &includeTVs=`tvStartseite,tvStartdatum,tvEnddatum`
    &tvPrefix=``
    &includeContent=`0`
    &sortby=`id`
    &sortdir=`DESC`
    &tpl=`tvAdminRow`
    &tplWrapper=`tvAdminOuter_bs5`
]]

<div class="d-flex justify-content-center my-3">
    [[!+page.nav]]
</div>

✅ Ergebnis

  • TVs werden übersichtlich im Frontend dargestellt
  • Filtersuche pro Seite möglich
  • Werte können direkt bearbeitet und gespeichert werden
  • Bearbeitung ist ausschließlich für berechtigte, angemeldete User möglich
  • Das Design ist vollständig über BS5-Chunks anpassbar
  • Erweiterungen (weitere TVs) sind einfach möglich

🔗 Optional: Admin-Hinweisleiste mit Link zur TV-Übersicht

Erweiterte Version mit zusätzlichem Button für die TV-Übersicht (BS5 / Bootswatch-tauglich), einbaubar ins Template:

[[!+modx.user.id:is=`0`:then=``:else=`
    <div class="alert alert-secondary py-2 px-3 mb-2 small d-flex flex-wrap align-items-center gap-1">
        <span>Hallo [[!+modx.user.username]],</span>

        [[*id:isnot=`[[++site_start]]`:then=`
            <a href="https://validator.w3.org/nu/?doc=[[++site_url]][[~[[*id]]]]"
               target="_blank"
               class="alert-link">
                Seite <strong>[[~[[*id]]]]</strong> validieren
            </a>
        `:else=`
            <a href="https://validator.w3.org/nu/?doc=[[++site_url]]"
               target="_blank"
               class="alert-link">
                Startseite validieren
            </a>
        `]]

        <span class="vr d-none d-sm-inline mx-2"></span>

        <a href="[[~87]]"
           class="btn btn-outline-secondary btn-sm">
            TV-Übersicht
        </a>
    </div>
`]]