Quelltext des aktuellen Standes
<?php
/**
* Versionsvergleich
* Modernes Full-Width Design mit Hero Header
*/
// Sichere Defaults
$topic = $topic ?? ['id' => 0, 'name' => 'Unbekannt'];
$topic_ancestors = $topic_ancestors ?? [];
$page = $page ?? ['id' => 0, 'title' => 'Unbekannt'];
$version1 = $version1 ?? ['id' => 0, 'version_number' => 0, 'content_html' => '', 'created_at' => '', 'created_by_name' => 'Unbekannt'];
$version2 = $version2 ?? ['id' => 0, 'version_number' => 0, 'content_html' => '', 'created_at' => '', 'created_by_name' => 'Unbekannt'];
$this->layout('layouts/main', [
'page_title' => 'Versionsvergleich: ' . $page['title'],
'breadcrumbs' => [],
'full_width' => true
]);
?>
<div class="content-header">
<div>
<h1><?= $this->e($page['title']) ?> <small style="font-size:0.75rem;color:var(--gray-500);">v<?= $version1['version_number'] ?> vs. v<?= $version2['version_number'] ?></small></h1>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/"><?= $this->te('Home') ?></a></li>
<li class="breadcrumb-item"><a href="/topics"><?= $this->te('Themengebiete') ?></a></li>
<?php foreach ($topic_ancestors as $ancestor): ?>
<li class="breadcrumb-item"><a href="/topics/<?= $ancestor['id'] ?>"><?= $this->e($ancestor['name']) ?></a></li>
<?php endforeach; ?>
<li class="breadcrumb-item"><a href="/topics/<?= $topic['id'] ?>"><?= $this->e($topic['name']) ?></a></li>
<li class="breadcrumb-item"><a href="/pages/<?= $page['id'] ?>"><?= $this->e($page['title']) ?></a></li>
<li class="breadcrumb-item"><a href="/pages/<?= $page['id'] ?>/versions"><?= $this->te('Versionen') ?></a></li>
<li class="breadcrumb-item active"><?= $this->te('Vergleich') ?></li>
</ol>
</div>
<div class="content-header-actions">
<a href="/pages/<?= $page['id'] ?>/versions" class="btn btn-default btn-sm"><span aria-hidden="true">📚</span> <?= $this->te('Alle Versionen') ?></a>
<a href="/pages/<?= $page['id'] ?>" class="btn btn-default btn-sm"><span aria-hidden="true">📄</span> <?= $this->te('Aktuelle Seite') ?></a>
</div>
</div>
<div class="compare-content">
<!-- Version Headers -->
<div class="compare-headers">
<div class="version-header version-old">
<div class="version-badge-row">
<span class="status-pill status-pill--neutral">Version <?= $version1['version_number'] ?></span>
</div>
<div class="version-meta">
<span><span aria-hidden="true">📅</span> <?= date('d.m.Y H:i', strtotime($version1['created_at'])) ?></span>
<span><span aria-hidden="true">👤</span> <?= $this->e($version1['created_by_name'] ?? 'Unbekannt') ?></span>
</div>
<?php if (!empty($version1['change_reason'])): ?>
<div class="version-reason"><?= $this->e($version1['change_reason']) ?></div>
<?php endif; ?>
<a href="/pages/<?= $page['id'] ?>/versions/<?= $version1['id'] ?>" class="version-link">
<?= $this->te('Version anzeigen →') ?>
</a>
</div>
<div class="version-header version-new">
<div class="version-badge-row">
<span class="status-pill status-pill--success">Version <?= $version2['version_number'] ?></span>
</div>
<div class="version-meta">
<span><span aria-hidden="true">📅</span> <?= date('d.m.Y H:i', strtotime($version2['created_at'])) ?></span>
<span><span aria-hidden="true">👤</span> <?= $this->e($version2['created_by_name'] ?? 'Unbekannt') ?></span>
</div>
<?php if (!empty($version2['change_reason'])): ?>
<div class="version-reason"><?= $this->e($version2['change_reason']) ?></div>
<?php endif; ?>
<a href="/pages/<?= $page['id'] ?>/versions/<?= $version2['id'] ?>" class="version-link">
<?= $this->te('Version anzeigen →') ?>
</a>
</div>
</div>
<!-- Side by Side Comparison -->
<div class="compare-columns">
<div class="compare-column column-old">
<div class="column-header">
<span class="column-label"><?= $this->te('Aelter') ?></span>
<span class="column-version">v<?= $version1['version_number'] ?></span>
</div>
<div class="column-content wysiwyg-content">
<?= $this->fixRelativeUrls($version1['content_html']) ?>
</div>
</div>
<div class="compare-column column-new">
<div class="column-header">
<span class="column-label"><?= $this->te('Neuer') ?></span>
<span class="column-version">v<?= $version2['version_number'] ?></span>
</div>
<div class="column-content wysiwyg-content">
<?= $this->fixRelativeUrls($version2['content_html']) ?>
</div>
</div>
</div>
<!-- Diff View -->
<div class="diff-section">
<div class="diff-header">
<h2><span aria-hidden="true">🔄</span> <?= $this->te('Änderungen') ?></h2>
<div class="diff-legend">
<span class="legend-item"><span class="legend-color legend-added"></span> <?= $this->te('Hinzugefügt') ?></span>
<span class="legend-item"><span class="legend-color legend-removed"></span> <?= $this->te('Entfernt') ?></span>
</div>
</div>
<div class="diff-content" id="diffOutput">
<p class="diff-note"><?= $this->te('Diff wird berechnet...') ?></p>
</div>
</div>
<!-- Navigation -->
<div class="compare-nav">
<a href="/pages/<?= $page['id'] ?>/versions" class="btn btn-default">
<span aria-hidden="true">←</span> <?= $this->te('Zurueck zur Versionshistorie') ?>
</a>
</div>
</div>
<style>
/* Main Content */
.compare-content {
padding: 2rem;
background: var(--slate-50);
}
/* Version Headers */
.compare-headers {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
margin-bottom: 1.5rem;
}
.version-header {
background: var(--color-bg);
border-radius: 0.75rem;
padding: 1.25rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.version-header.version-old {
border-left: 4px solid var(--red-500);
}
.version-header.version-new {
border-left: 4px solid var(--green-500);
}
.version-badge-row {
margin-bottom: 0.75rem;
}
.version-meta {
display: flex;
gap: 1.5rem;
font-size: 0.85rem;
color: var(--gray-500);
margin-bottom: 0.5rem;
}
.version-reason {
font-size: 0.9rem;
color: var(--gray-600);
font-style: italic;
margin-bottom: 0.75rem;
}
.version-link {
font-size: 0.85rem;
color: var(--indigo-500);
text-decoration: none;
}
.version-link:hover {
text-decoration: underline;
}
/* Compare Columns */
.compare-columns {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
margin-bottom: 1.5rem;
}
.compare-column {
background: var(--color-bg);
border-radius: 0.75rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.column-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1.25rem;
border-bottom: 1px solid var(--gray-200);
}
.column-old .column-header {
background: var(--red-50);
}
.column-new .column-header {
background: var(--green-50);
}
.column-label {
font-weight: 600;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.025em;
}
.column-old .column-label {
color: var(--red-600);
}
.column-new .column-label {
color: var(--green-600);
}
.column-version {
font-weight: 600;
font-size: 0.85rem;
color: var(--gray-500);
}
.column-content {
padding: 1.5rem;
max-height: 600px;
overflow-y: auto;
}
.column-content.wysiwyg-content {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif !important;
font-size: 0.95rem;
line-height: 1.7;
color: var(--gray-800);
}
.wysiwyg-content :not(code):not(pre):not(kbd):not(samp) {
font-family: inherit !important;
}
.wysiwyg-content table { width: 100% !important; max-width: 100% !important; table-layout: fixed; }
.wysiwyg-content th, .wysiwyg-content td { word-wrap: break-word; overflow-wrap: break-word; }
.wysiwyg-content h1 { font-size: 1.5rem; margin: 1.5rem 0 0.75rem; }
.wysiwyg-content h2 { font-size: 1.25rem; margin: 1.25rem 0 0.5rem; border-bottom: 2px solid var(--gray-200); padding-bottom: 0.5rem; }
.wysiwyg-content h3 { font-size: 1.1rem; margin: 1rem 0 0.5rem; }
.wysiwyg-content p { margin: 0 0 0.75rem; }
.wysiwyg-content ul, .wysiwyg-content ol { margin: 0 0 0.75rem; padding-left: 1.25rem; }
.wysiwyg-content li { margin-bottom: 0.2rem; }
.wysiwyg-content table { width: 100%; border-collapse: collapse; margin: 0.75rem 0; font-size: 0.85rem; }
.wysiwyg-content th, .wysiwyg-content td { border: 1px solid var(--gray-200); padding: 0.4rem 0.6rem; text-align: left; }
.wysiwyg-content th { background: var(--gray-50); font-weight: 600; }
.wysiwyg-content pre { background: var(--gray-800); color: var(--gray-50); padding: 0.75rem; border-radius: 0.375rem; overflow-x: auto; font-size: 0.8rem; }
.wysiwyg-content code { background: var(--gray-100); padding: 0.1rem 0.3rem; border-radius: 0.2rem; font-size: 0.85em; }
.wysiwyg-content pre code { background: transparent; padding: 0; }
.wysiwyg-content blockquote { border-left: 3px solid var(--blue-600); margin: 0.75rem 0; padding: 0.4rem 0.75rem; background: var(--blue-50); }
.wysiwyg-content img { max-width: 100%; height: auto; border-radius: 0.375rem; display: block; }
/* Navigation */
.compare-nav {
display: flex;
justify-content: center;
padding-top: 1rem;
}
/* Diff Section */
.diff-section {
background: var(--color-bg);
border-radius: 0.75rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
overflow: hidden;
margin-bottom: 1.5rem;
}
.diff-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1.25rem;
background: var(--gray-50);
border-bottom: 1px solid var(--gray-200);
}
.diff-header h2 {
margin: 0;
font-size: 0.95rem;
font-weight: 600;
color: var(--gray-700);
}
.diff-legend {
display: flex;
gap: 1rem;
font-size: 0.8rem;
color: var(--gray-500);
}
.legend-item {
display: flex;
align-items: center;
gap: 0.375rem;
}
.legend-color {
width: 14px;
height: 14px;
border-radius: 0.25rem;
}
.legend-added { background: var(--green-100); border: 1px solid var(--green-300); }
.legend-removed { background: var(--red-100); border: 1px solid var(--red-300); }
.diff-content {
padding: 1.5rem;
font-size: 0.95rem;
line-height: 1.8;
color: var(--gray-800);
max-height: 600px;
overflow-y: auto;
white-space: pre-wrap;
word-wrap: break-word;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.diff-added {
background: var(--green-100);
color: var(--green-800);
padding: 0.05rem 0.2rem;
border-radius: 0.15rem;
text-decoration: none;
}
.diff-removed {
background: var(--red-100);
color: var(--red-800);
padding: 0.05rem 0.2rem;
border-radius: 0.15rem;
text-decoration: line-through;
}
/* Statusmeldung im Diff-Bereich (Berechnung, keine Unterschiede, nur Formatierung) */
.diff-note {
color: var(--gray-400);
text-align: center;
margin: 0;
}
/* Responsive */
@media (max-width: 900px) {
.compare-headers,
.compare-columns {
grid-template-columns: 1fr;
}
.version-header.version-new {
order: -1;
}
.compare-column.column-new {
order: -1;
}
}
@media (max-width: 768px) {
.version-meta {
flex-direction: column;
gap: 0.25rem;
}
.column-content {
padding: 1rem;
max-height: 400px;
}
.diff-content {
padding: 1rem;
font-size: 0.85rem;
max-height: 400px;
}
.diff-header {
flex-direction: column;
gap: 0.5rem;
align-items: flex-start;
}
}
@media (max-width: 480px) {
.version-header {
padding: 1rem;
}
.column-content {
padding: 0.75rem;
}
.diff-content {
padding: 0.75rem;
font-size: 0.8rem;
}
}
</style>
<script>
// Simple word-level diff
(function() {
var oldEl = document.querySelector('.column-old .column-content');
var newEl = document.querySelector('.column-new .column-content');
var output = document.getElementById('diffOutput');
if (!oldEl || !newEl || !output) return;
var oldText = oldEl.innerText.replace(/\r\n/g, '\n');
var newText = newEl.innerText.replace(/\r\n/g, '\n');
var oldWords = oldText.split(/(\s+)/);
var newWords = newText.split(/(\s+)/);
// LCS-based diff
var m = oldWords.length;
var n = newWords.length;
// For very large texts, use a simpler line-based approach
if (m * n > 500000) {
var oldLines = oldText.split('\n');
var newLines = newText.split('\n');
var lineResult = lineDiff(oldLines, newLines);
renderDiff(lineResult.html, lineResult.changed);
return;
}
// Build LCS table
var dp = [];
for (var i = 0; i <= m; i++) {
dp[i] = new Uint16Array(n + 1);
}
for (var i = 1; i <= m; i++) {
for (var j = 1; j <= n; j++) {
if (oldWords[i-1] === newWords[j-1]) {
dp[i][j] = dp[i-1][j-1] + 1;
} else {
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
}
}
}
// Backtrack to build diff
var result = [];
var i = m, j = n;
while (i > 0 || j > 0) {
if (i > 0 && j > 0 && oldWords[i-1] === newWords[j-1]) {
result.unshift({type: 'same', text: oldWords[i-1]});
i--; j--;
} else if (j > 0 && (i === 0 || dp[i][j-1] >= dp[i-1][j])) {
result.unshift({type: 'added', text: newWords[j-1]});
j--;
} else {
result.unshift({type: 'removed', text: oldWords[i-1]});
i--;
}
}
// Render diff
var html = '';
var changed = false;
for (var k = 0; k < result.length; k++) {
var r = result[k];
var escaped = escapeHtml(r.text);
if (r.type === 'same') {
html += escaped;
} else if (r.type === 'added') {
html += '<span class="diff-added">' + escaped + '</span>';
changed = true;
} else {
html += '<span class="diff-removed">' + escaped + '</span>';
changed = true;
}
}
renderDiff(html, changed);
/**
* Zeigt den Diff an. Ohne Textunterschiede wird zusaetzlich das gerenderte
* HTML beider Versionen verglichen, damit reine Formatierungs-, Tabellen-
* oder Bildaenderungen nicht als "keine Unterschiede" erscheinen.
*/
function renderDiff(diffHtml, hasTextChanges) {
if (hasTextChanges) {
output.innerHTML = diffHtml;
return;
}
if (normalizeHtml(oldEl) !== normalizeHtml(newEl)) {
output.innerHTML = '<p class="diff-note">Keine Textunterschiede – es wurden nur Formatierung, Tabellen oder Bilder geändert.</p>';
return;
}
output.innerHTML = '<p class="diff-note">Keine Unterschiede gefunden.</p>';
}
// Whitespace zusammenfassen, damit reine Einrueckungen im Quelltext nicht als Aenderung zaehlen
function normalizeHtml(el) {
return el.innerHTML.replace(/\s+/g, ' ').trim();
}
function lineDiff(oldLines, newLines) {
var html = '';
var lineChanged = false;
var ml = oldLines.length, nl = newLines.length;
var d = [];
for (var i = 0; i <= ml; i++) { d[i] = new Uint16Array(nl + 1); }
for (var i = 1; i <= ml; i++) {
for (var j = 1; j <= nl; j++) {
d[i][j] = oldLines[i-1] === newLines[j-1] ? d[i-1][j-1]+1 : Math.max(d[i-1][j], d[i][j-1]);
}
}
var res = [];
var i = ml, j = nl;
while (i > 0 || j > 0) {
if (i > 0 && j > 0 && oldLines[i-1] === newLines[j-1]) {
res.unshift({type:'same',text:oldLines[i-1]}); i--; j--;
} else if (j > 0 && (i === 0 || d[i][j-1] >= d[i-1][j])) {
res.unshift({type:'added',text:newLines[j-1]}); j--;
} else {
res.unshift({type:'removed',text:oldLines[i-1]}); i--;
}
}
for (var k = 0; k < res.length; k++) {
var r = res[k];
var t = escapeHtml(r.text);
if (r.type === 'same') {
html += t + '\n';
} else if (r.type === 'added') {
html += '<span class="diff-added">' + t + '</span>\n';
lineChanged = true;
} else {
html += '<span class="diff-removed">' + t + '</span>\n';
lineChanged = true;
}
}
return { html: html, changed: lineChanged };
}
function escapeHtml(str) {
return str.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
}
})();
</script>