Quelltext des aktuellen Standes
<?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">↻</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 & Drop anpassen – wird nur fü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 ? '📁' : '📂' ?></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">📄</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">📁</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">✎</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 ? '📁' : '📂' ?></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">📄</span>
<?= $pageCount ?>
</span>
<?php if ($childCount > 0): ?>
<span class="meta-item">
<span class="meta-icon" aria-hidden="true">📁</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">✎</span>
</a>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="empty-state">
<div class="empty-icon" aria-hidden="true">📂</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>