OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

index.php

24 KB
<?php $this->layout('layouts/main', ['page_title' => 'Themengebiete', 'full_width' => true]); ?>

<div class="topics-index-fullwidth">
    <!-- Content Header -->
    <div class="content-header">
        <div>
            <h1><?= $this->te('Themengebiete') ?></h1>
            <ol class="breadcrumb">
                <li class="breadcrumb-item"><a href="/"><?= $this->te('Home') ?></a></li>
                <li class="breadcrumb-item active"><?= $this->te('Themengebiete') ?></li>
            </ol>
        </div>
        <div class="content-header-actions">
            <!-- View-Toggle -->
            <div class="view-toggle" id="viewToggle">
                <button class="view-btn <?= ($view_mode ?? 'cards') === 'cards' ? 'active' : '' ?>"
                        data-mode="cards" title="<?= $this->te('Kartenansicht') ?>" aria-label="<?= $this->te('Kartenansicht') ?>">
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" aria-hidden="true">
                        <rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/>
                        <rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/>
                    </svg>
                </button>
                <button class="view-btn <?= ($view_mode ?? 'cards') === 'list' ? 'active' : '' ?>"
                        data-mode="list" title="<?= $this->te('Listenansicht') ?>" aria-label="<?= $this->te('Listenansicht') ?>">
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" aria-hidden="true">
                        <line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/>
                        <line x1="8" y1="18" x2="21" y2="18"/>
                        <line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/>
                        <line x1="3" y1="18" x2="3.01" y2="18"/>
                    </svg>
                </button>
            </div>
            <!-- Sortierung zurücksetzen: sichtbar, sobald eine eigene Reihenfolge gespeichert ist -->
            <button class="btn btn-sm btn-default btn-reset-sort" id="resetSortBtn"
                    title="<?= $this->te('Eigene Sortierung zurücksetzen') ?>" aria-label="<?= $this->te('Eigene Sortierung zurücksetzen') ?>"
                    <?= !empty($has_custom_sort) ? '' : 'style="display:none;"' ?>>
                <span aria-hidden="true">&#8635;</span> <?= $this->te('Sortierung zurücksetzen') ?>
            </button>
            <?php if ($this->hasCapability('export_pdf')): ?>
                <a href="/export/handbook" class="btn btn-default" title="<?= $this->te('Mehrere Themengebiete als ein PDF ausgeben') ?>">
                    <i class="fas fa-book"></i>
                    <?= $this->te('Handbuch') ?>
                </a>
            <?php endif; ?>
            <?php if ($this->hasCapability('create_page')): ?>
                <a href="/topics/create" class="btn btn-primary">
                    <i class="fas fa-plus"></i>
                    <?= $this->te('Neues Themengebiet') ?>
                </a>
            <?php endif; ?>
        </div>
    </div>

    <?php if (!empty($topics)): ?>
        <!-- Drag-and-Drop Hinweis -->
        <div class="dnd-hint" id="dndHint">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14" aria-hidden="true">
                <circle cx="9" cy="5" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="9" cy="19" r="1"/>
                <circle cx="15" cy="5" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="15" cy="19" r="1"/>
            </svg>
            <?= $this->te('Reihenfolge per Drag &amp; Drop anpassen &ndash; wird nur f&uuml;r Sie gespeichert') ?>
        </div>

        <!-- Karten-Ansicht -->
        <div class="topics-grid topics-view-cards <?= ($view_mode ?? 'cards') === 'cards' ? '' : 'hidden' ?>"
             id="topicsCards">
            <?php foreach ($topics as $topic):
                $hasChildren = !empty($topic['children']);
                $childCount = $hasChildren ? count($topic['children']) : 0;
                $pageCount = (int)($topic['page_count'] ?? 0);
                $hasImage = !empty($topic['image_path']);
            ?>
                <article class="topic-card" data-topic-id="<?= $topic['id'] ?>" draggable="true">
                    <div class="drag-handle" title="<?= $this->te('Ziehen zum Sortieren') ?>">
                        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="12" height="12" aria-hidden="true">
                            <circle cx="9" cy="5" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="9" cy="19" r="1"/>
                            <circle cx="15" cy="5" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="15" cy="19" r="1"/>
                        </svg>
                    </div>
                    <a href="/topics/<?= $topic['id'] ?>" class="topic-card-link">
                        <div class="topic-visual">
                            <?php if ($hasImage): ?>
                                <img src="/uploads/topics/<?= $this->e($topic['image_path']) ?>"
                                     alt="<?= $this->e($topic['name']) ?>"
                                     class="topic-image">
                            <?php else: ?>
                                <div class="topic-icon-placeholder">
                                    <span class="icon" aria-hidden="true"><?= $pageCount > 0 || $hasChildren ? '&#128193;' : '&#128194;' ?></span>
                                </div>
                            <?php endif; ?>
                        </div>
                        <div class="topic-content">
                            <h2 class="topic-name"><?= $this->e($topic['name']) ?></h2>
                            <?php if (!empty($topic['description'])): ?>
                                <p class="topic-description"><?= $this->e($this->truncate($topic['description'], 120)) ?></p>
                            <?php endif; ?>
                            <div class="topic-meta">
                                <span class="meta-item">
                                    <span class="meta-icon" aria-hidden="true">&#128196;</span>
                                    <?= $this->te($pageCount === 1 ? '{anzahl} Seite' : '{anzahl} Seiten', ['anzahl' => $pageCount]) ?>
                                </span>
                                <?php if ($childCount > 0): ?>
                                    <span class="meta-item">
                                        <span class="meta-icon" aria-hidden="true">&#128193;</span>
                                        <?= $this->te($childCount === 1 ? '{anzahl} Untergebiet' : '{anzahl} Untergebiete', ['anzahl' => $childCount]) ?>
                                    </span>
                                <?php endif; ?>
                            </div>
                        </div>
                    </a>
                    <?php if ($this->hasCapability('edit_page')): ?>
                        <a href="/topics/<?= $topic['id'] ?>/edit" class="topic-edit-btn" title="<?= $this->te('Bearbeiten') ?>"
                           aria-label="<?= $this->te('{name} bearbeiten', ['name' => $topic['name']]) ?>">
                            <span aria-hidden="true">&#9998;</span>
                        </a>
                    <?php endif; ?>
                </article>
            <?php endforeach; ?>
        </div>

        <!-- Listen-Ansicht -->
        <div class="topics-view-list <?= ($view_mode ?? 'cards') === 'list' ? '' : 'hidden' ?>"
             id="topicsList">
            <?php foreach ($topics as $topic):
                $hasChildren = !empty($topic['children']);
                $childCount = $hasChildren ? count($topic['children']) : 0;
                $pageCount = (int)($topic['page_count'] ?? 0);
                $hasImage = !empty($topic['image_path']);
            ?>
                <div class="topic-list-item" data-topic-id="<?= $topic['id'] ?>" draggable="true">
                    <div class="drag-handle drag-handle-list" title="<?= $this->te('Ziehen zum Sortieren') ?>">
                        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="12" height="12" aria-hidden="true">
                            <circle cx="9" cy="5" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="9" cy="19" r="1"/>
                            <circle cx="15" cy="5" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="15" cy="19" r="1"/>
                        </svg>
                    </div>
                    <div class="topic-list-icon">
                        <?php if ($hasImage): ?>
                            <img src="/uploads/topics/<?= $this->e($topic['image_path']) ?>"
                                 alt="<?= $this->e($topic['name']) ?>" class="topic-list-thumb">
                        <?php else: ?>
                            <span class="topic-list-emoji" aria-hidden="true"><?= $pageCount > 0 || $hasChildren ? '&#128193;' : '&#128194;' ?></span>
                        <?php endif; ?>
                    </div>
                    <a href="/topics/<?= $topic['id'] ?>" class="topic-list-name">
                        <?= $this->e($topic['name']) ?>
                    </a>
                    <?php if (!empty($topic['description'])): ?>
                        <span class="topic-list-desc"><?= $this->e($this->truncate($topic['description'], 80)) ?></span>
                    <?php endif; ?>
                    <div class="topic-list-meta">
                        <span class="meta-item">
                            <span class="meta-icon" aria-hidden="true">&#128196;</span>
                            <?= $pageCount ?>
                        </span>
                        <?php if ($childCount > 0): ?>
                            <span class="meta-item">
                                <span class="meta-icon" aria-hidden="true">&#128193;</span>
                                <?= $childCount ?>
                            </span>
                        <?php endif; ?>
                    </div>
                    <?php if ($this->hasCapability('edit_page')): ?>
                        <a href="/topics/<?= $topic['id'] ?>/edit" class="topic-list-edit" title="<?= $this->te('Bearbeiten') ?>"
                           aria-label="<?= $this->te('{name} bearbeiten', ['name' => $topic['name']]) ?>">
                            <span aria-hidden="true">&#9998;</span>
                        </a>
                    <?php endif; ?>
                </div>
            <?php endforeach; ?>
        </div>

    <?php else: ?>
        <div class="empty-state">
            <div class="empty-icon" aria-hidden="true">&#128194;</div>
            <h2><?= $this->te('Keine Themengebiete vorhanden') ?></h2>
            <p><?= $this->te('Es wurden noch keine Themengebiete erstellt.') ?></p>
            <?php if ($this->hasCapability('create_page')): ?>
                <a href="/topics/create" class="btn btn-primary btn-lg">
                    <?= $this->te('Erstes Themengebiet erstellen') ?>
                </a>
            <?php endif; ?>
        </div>
    <?php endif; ?>
</div>

<style>
/* Full-Width Topics Layout */
.topics-index-fullwidth {
}

/* View Toggle */
.view-toggle {
    display: flex;
    background: var(--gray-100);
    border: 1px solid var(--gray-300);
    border-radius: 0.25rem;
    overflow: hidden;
}

.view-btn {
    background: none;
    border: none;
    padding: 0.4rem 0.6rem;
    cursor: pointer;
    color: var(--gray-500);
    display: flex;
    align-items: center;
    transition: all 0.15s;
}

.view-btn:hover {
    color: var(--gray-700);
    background: var(--gray-200);
}

.view-btn.active {
    background: var(--color-primary);
    color: var(--color-on-accent);
}

/* DnD Hint */
.dnd-hint {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.5rem 2.5rem;
    font-size: 0.78rem;
    color: var(--gray-500);
    background: var(--gray-50);
    border-bottom: 1px solid var(--gray-200);
}

/* hidden helper */
.hidden { display: none !important; }

/* ============ CARD VIEW ============ */
.topics-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
    gap: 1.5rem;
    padding: 2rem 1.5rem;
}

.topic-card {
    position: relative;
    background: var(--color-bg);
    border-radius: 0.25rem;
    overflow: hidden;
    box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.06);
    transition: all 0.2s ease;
    cursor: default;
}

.topic-card:hover {
    box-shadow: 0 10px 25px rgba(0,0,0,0.1), 0 4px 10px rgba(0,0,0,0.05);

}

.topic-card.dragging {
    opacity: 0.45;
    transform: scale(0.97);
    box-shadow: 0 20px 40px rgba(0,0,0,0.2);
}

.topic-card.drag-over {
    outline: 2px dashed var(--color-primary);
    outline-offset: 3px;
}

.topic-card-link {
    display: block;
    text-decoration: none !important;
    color: inherit;
}

.topic-visual {
    height: 140px;
    overflow: hidden;
    background: linear-gradient(135deg, var(--blue-50) 0%, var(--blue-50) 100%);
}

.topic-image {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.topic-icon-placeholder {
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
}

.topic-icon-placeholder .icon {
    font-size: 3.5rem;
    opacity: 0.6;
}

.topic-content {
    padding: 1.25rem;
}

.topic-name {
    margin: 0 0 0.5rem 0;
    font-size: 1.15rem;
    font-weight: 600;
    color: var(--gray-900);
    line-height: 1.3;
}

.topic-card:hover .topic-name { color: var(--color-primary); }

.topic-description {
    margin: 0 0 0.75rem 0;
    font-size: 0.875rem;
    color: var(--gray-500);
    line-height: 1.5;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.topic-meta {
    display: flex;
    gap: 1rem;
    padding-top: 0.75rem;
    border-top: 1px solid var(--gray-100);
}

.meta-item {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    font-size: 0.8rem;
    color: var(--gray-400);
}

.meta-icon { font-size: 0.9rem; }

/* Drag Handle (Card) */
.drag-handle {
    position: absolute;
    top: 0.6rem;
    left: 0.6rem;
    width: 26px;
    height: 26px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(255,255,255,0.85);
    border-radius: 0.3rem;
    color: var(--gray-400);
    cursor: grab;
    opacity: 0;
    transition: opacity 0.15s;
    z-index: 2;
    box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}

.drag-handle:active { cursor: grabbing; }
.topic-card:hover .drag-handle { opacity: 1; }
.topic-card.dragging .drag-handle { opacity: 1; }

/* Edit Button */
.topic-edit-btn {
    position: absolute;
    top: 0.75rem;
    right: 0.75rem;
    width: 32px;
    height: 32px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(255,255,255,0.9);
    border-radius: 0.375rem;
    color: var(--gray-500);
    text-decoration: none !important;
    font-size: 0.9rem;
    opacity: 0;
    transition: all 0.15s;
    box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}

.topic-card:hover .topic-edit-btn { opacity: 1; }
.topic-edit-btn:hover { background: var(--color-bg); color: var(--color-primary); }

/* ============ LIST VIEW ============ */
.topics-view-list {
    padding: 1.5rem 2rem;
    display: flex;
    flex-direction: column;
    gap: 0.375rem;
}

.topic-list-item {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.75rem 1rem;
    background: var(--color-bg);
    border-radius: 0.25rem;
    box-shadow: 0 1px 2px rgba(0,0,0,0.05);
    border: 1px solid var(--gray-100);
    transition: all 0.15s;
    cursor: default;
}

.topic-list-item:hover {
    border-color: var(--blue-100);
    box-shadow: 0 2px 6px rgba(0,0,0,0.07);
}

.topic-list-item.dragging {
    opacity: 0.45;
    box-shadow: 0 8px 20px rgba(0,0,0,0.15);
}

.topic-list-item.drag-over {
    outline: 2px dashed var(--color-primary);
    outline-offset: 2px;
}

.drag-handle-list {
    position: static;
    opacity: 0.35;
    background: none;
    box-shadow: none;
    cursor: grab;
    flex-shrink: 0;
}

.drag-handle-list:active { cursor: grabbing; }
.topic-list-item:hover .drag-handle-list { opacity: 0.7; }

.topic-list-icon {
    flex-shrink: 0;
    width: 36px;
    height: 36px;
    border-radius: 0.5rem;
    overflow: hidden;
    background: var(--blue-50);
    display: flex;
    align-items: center;
    justify-content: center;
}

.topic-list-thumb {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.topic-list-emoji { font-size: 1.3rem; }

.topic-list-name {
    font-weight: 600;
    color: var(--gray-900);
    text-decoration: none;
    font-size: 0.925rem;
    flex-shrink: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 220px;
}

.topic-list-name:hover { color: var(--color-primary); text-decoration: none; }

.topic-list-desc {
    font-size: 0.8rem;
    color: var(--gray-400);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    flex: 1;
    min-width: 0;
}

.topic-list-meta {
    display: flex;
    gap: 0.5rem;
    flex-shrink: 0;
    margin-left: auto;
}

.topic-list-edit {
    flex-shrink: 0;
    width: 28px;
    height: 28px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--gray-100);
    border-radius: 0.375rem;
    color: var(--gray-500);
    text-decoration: none;
    font-size: 0.85rem;
    opacity: 0;
    transition: all 0.15s;
}

.topic-list-item:hover .topic-list-edit { opacity: 1; }
.topic-list-edit:hover { background: var(--blue-100); color: var(--color-primary); text-decoration: none; }


.empty-icon {
    font-size: 5rem;
    margin-bottom: 1.5rem;
    opacity: 0.4;
}
/* Responsive */
@media (max-width: 768px) {
    .topics-grid {
        grid-template-columns: 1fr;
        padding: 1rem;
        gap: 1rem;
    }

    .topic-visual { height: 120px; }
    .topic-edit-btn { opacity: 1; }
    .drag-handle { opacity: 1; }

    .topics-view-list { padding: 1rem; }
    .topic-list-desc { display: none; }
    .drag-handle-list { opacity: 0.5; }
    .topic-list-edit { opacity: 1; }

    .dnd-hint { padding: 0.5rem 1rem; }
}

@media (max-width: 480px) {
    .topics-grid { padding: 0.75rem; gap: 0.75rem; }
    .topic-visual { height: 100px; }
    .topic-content { padding: 0.75rem; }
    .topic-name { font-size: 1rem; }
}
</style>

<script>
(function () {
    'use strict';

    var csrf = document.querySelector('meta[name="csrf-token"]')
              ? document.querySelector('meta[name="csrf-token"]').content
              : '';

    // -------- View Toggle --------
    var viewBtns = document.querySelectorAll('.view-btn');
    var cardsEl  = document.getElementById('topicsCards');
    var listEl   = document.getElementById('topicsList');

    viewBtns.forEach(function (btn) {
        btn.addEventListener('click', function () {
            var mode = btn.dataset.mode;
            viewBtns.forEach(function (b) { b.classList.remove('active'); });
            btn.classList.add('active');

            if (mode === 'cards') {
                cardsEl && cardsEl.classList.remove('hidden');
                listEl  && listEl.classList.add('hidden');
            } else {
                cardsEl && cardsEl.classList.add('hidden');
                listEl  && listEl.classList.remove('hidden');
            }

            // Persist
            fetch('/topics/set-view-mode', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                    'X-Requested-With': 'XMLHttpRequest',
                    'X-CSRF-Token': csrf
                },
                body: 'mode=' + encodeURIComponent(mode)
            });
        });
    });

    // -------- Drag & Drop --------
    var dragSrc = null;
    var hasDragged = false;
    var resetBtn = document.getElementById('resetSortBtn');

    function getDraggables() {
        // Aktive Ansicht
        var container = (listEl && !listEl.classList.contains('hidden')) ? listEl : cardsEl;
        if (!container) return [];
        return Array.from(container.querySelectorAll('[data-topic-id][draggable="true"]'));
    }

    function bindDrag(el) {
        el.addEventListener('dragstart', function (e) {
            dragSrc = el;
            el.classList.add('dragging');
            e.dataTransfer.effectAllowed = 'move';
            e.dataTransfer.setData('text/plain', el.dataset.topicId);
        });

        el.addEventListener('dragend', function () {
            el.classList.remove('dragging');
            document.querySelectorAll('.drag-over').forEach(function (x) { x.classList.remove('drag-over'); });
        });

        el.addEventListener('dragover', function (e) {
            e.preventDefault();
            e.dataTransfer.dropEffect = 'move';
            if (el !== dragSrc) el.classList.add('drag-over');
        });

        el.addEventListener('dragleave', function () {
            el.classList.remove('drag-over');
        });

        el.addEventListener('drop', function (e) {
            e.preventDefault();
            el.classList.remove('drag-over');
            if (!dragSrc || dragSrc === el) return;

            // Beide Container synchron halten
            moveInContainer(cardsEl, dragSrc.dataset.topicId, el.dataset.topicId);
            moveInContainer(listEl,  dragSrc.dataset.topicId, el.dataset.topicId);

            hasDragged = true;
            resetBtn && (resetBtn.style.display = '');
            saveOrder();
        });
    }

    /**
     * Verschiebt das gezogene Element an die Position des Ziels und schiebt die
     * dazwischenliegenden Elemente auf – Listen-Umsortierung, kein Tausch.
     * Richtung: liegt das Ziel vor dem gezogenen Element, wird davor eingefügt,
     * sonst dahinter, damit die Karte dort landet, wo sie fallen gelassen wurde.
     */
    function moveInContainer(container, srcId, tgtId) {
        if (!container) return;
        var src = container.querySelector('[data-topic-id="' + srcId + '"]');
        var tgt = container.querySelector('[data-topic-id="' + tgtId + '"]');
        if (!src || !tgt || src === tgt) return;

        var targetIsBefore =
            (src.compareDocumentPosition(tgt) & Node.DOCUMENT_POSITION_PRECEDING) !== 0;

        tgt.parentNode.insertBefore(src, targetIsBefore ? tgt : tgt.nextSibling);
    }

    function getOrderedIds() {
        var container = cardsEl || listEl;
        if (!container) return [];
        return Array.from(container.querySelectorAll('[data-topic-id]'))
            .map(function (el) { return parseInt(el.dataset.topicId, 10); });
    }

    function saveOrder() {
        var ids = getOrderedIds();
        fetch('/topics/save-sort-order', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-Requested-With': 'XMLHttpRequest',
                'X-CSRF-Token': csrf
            },
            body: JSON.stringify({ids: ids})
        });
    }

    // Bind drag to all items
    if (cardsEl) {
        cardsEl.querySelectorAll('[data-topic-id]').forEach(bindDrag);
    }
    if (listEl) {
        listEl.querySelectorAll('[data-topic-id]').forEach(bindDrag);
    }

    // Reset Button
    if (resetBtn) {
        resetBtn.addEventListener('click', function () {
            fetch('/topics/reset-sort-order', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                    'X-Requested-With': 'XMLHttpRequest',
                    'X-CSRF-Token': csrf
                },
                body: ''
            }).then(function () {
                window.location.reload();
            });
        });
    }

    // Die Startsichtbarkeit des Reset-Buttons liefert der Server ($has_custom_sort);
    // nach einem Drag blendet ihn der Drop-Handler zusätzlich ein.
})();
</script>