Tutorial: Karte im Vollbild mit Markern, echte Drag/Resize-Animationen
Karte (Step 17): - Oeffnet jetzt die Karten-Vollbild-Ansicht mit eigenem Leaflet-Map - Zoomt auf Hamburg (Zoom 13) mit 3 farbcodierten Markern - Cursor faehrt zu Markern, oeffnet Popups (Burchardkai, Innenstadt) - Legende erklaert Kategorien (Hauptereignisort/Erwaehnt/Kontext) - Funktionen Orte einlesen + Vollbild werden in der Bubble erklaert - Map wird beim Step-Exit sauber aus dem Fullscreen entfernt Drag-Demo (Step 18): - Kachel bewegt sich jetzt visuell per CSS transform mit dem Cursor - 150px nach rechts, dann zurueck - echte Verschiebe-Animation - Kachel erhaelt erhoehten z-index waehrend der Animation Resize-Demo (Step 19): - Kachel aendert visuell Breite/Hoehe mit dem Cursor - 80px breiter + 50px hoeher, dann zurueck - Echte Groessenaenderung sichtbar statt nur Cursor-Bewegung Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -17,7 +17,7 @@
|
||||
<link rel="stylesheet" href="/static/vendor/MarkerCluster.css">
|
||||
<link rel="stylesheet" href="/static/vendor/MarkerCluster.Default.css">
|
||||
<link rel="stylesheet" href="/static/css/network.css?v=20260316a">
|
||||
<link rel="stylesheet" href="/static/css/style.css?v=20260316f">
|
||||
<link rel="stylesheet" href="/static/css/style.css?v=20260316g">
|
||||
</head>
|
||||
<body>
|
||||
<a href="#main-content" class="skip-link">Zum Hauptinhalt springen</a>
|
||||
@@ -764,7 +764,7 @@
|
||||
<script src="/static/js/api_network.js?v=20260316a"></script>
|
||||
<script src="/static/js/network-graph.js?v=20260316a"></script>
|
||||
<script src="/static/js/app_network.js?v=20260316a"></script>
|
||||
<script src="/static/js/tutorial.js?v=20260316h"></script>
|
||||
<script src="/static/js/tutorial.js?v=20260316i"></script>
|
||||
<script src="/static/js/chat.js?v=20260316f"></script>
|
||||
<script>document.addEventListener("DOMContentLoaded",function(){Chat.init();Tutorial.init()});</script>
|
||||
|
||||
|
||||
@@ -188,12 +188,11 @@ const Tutorial = {
|
||||
timeline.innerHTML = tlHtml;
|
||||
}
|
||||
|
||||
// Karte: Leaflet-Map mit Demo-Markern initialisieren
|
||||
// Karte: Stats setzen, Map wird erst im Vollbild-Step initialisiert
|
||||
var mapEmpty = document.getElementById('map-empty');
|
||||
if (mapEmpty) mapEmpty.style.display = 'none';
|
||||
var mapStats = document.getElementById('map-stats');
|
||||
if (mapStats) mapStats.textContent = '3 Orte / 9 Artikel';
|
||||
this._initDemoMap();
|
||||
|
||||
// Meta
|
||||
var metaUpdated = document.getElementById('meta-updated');
|
||||
@@ -841,30 +840,26 @@ const Tutorial = {
|
||||
Tutorial._clearSubHighlights();
|
||||
},
|
||||
},
|
||||
// 17 - Karte
|
||||
// 17 - Karte (Vollbild)
|
||||
{
|
||||
id: 'karte',
|
||||
target: '[gs-id="karte"]',
|
||||
target: '#map-fullscreen-overlay',
|
||||
title: 'Geografische Verteilung',
|
||||
text: 'Die Karte zeigt per Geoparsing automatisch erkannte Orte aus den Quellen.<br><br>'
|
||||
+ '<strong style="color:#EF4444;">●</strong> <strong>Hauptereignisort</strong> - Zentraler Ort des Geschehens<br>'
|
||||
+ '<strong style="color:#F59E0B;">●</strong> <strong>Erwähnt</strong> - In Artikeln genannte Orte<br>'
|
||||
+ '<strong style="color:#F59E0B;">●</strong> <strong>Erw\u00e4hnt</strong> - In Artikeln genannte Orte<br>'
|
||||
+ '<strong style="color:#3B82F6;">●</strong> <strong>Kontext</strong> - Relevante Umgebung<br><br>'
|
||||
+ 'Klicken Sie auf Marker für Details und verknüpfte Artikel. '
|
||||
+ 'Bei vielen Markern werden nahe Orte zu Clustern gruppiert.',
|
||||
position: 'top',
|
||||
+ 'Klicken Sie auf Marker f\u00fcr Details und verkn\u00fcpfte Artikel. '
|
||||
+ 'Bei vielen Markern werden nahe Orte zu Clustern gruppiert.<br><br>'
|
||||
+ '<strong>Orte einlesen</strong> startet das Geoparsing manuell neu. '
|
||||
+ '<strong>Vollbild</strong> zeigt die Karte in dieser gro\u00dfen Ansicht.',
|
||||
position: 'left',
|
||||
disableNav: true,
|
||||
onEnter: function() {
|
||||
// Demo-Map Popup öffnen + invalidateSize
|
||||
if (Tutorial._demoMap) {
|
||||
Tutorial._demoMap.invalidateSize();
|
||||
setTimeout(function() {
|
||||
if (Tutorial._demoMap) Tutorial._demoMap.setView([53.545, 9.98], 13);
|
||||
}, 300);
|
||||
}
|
||||
Tutorial._simulateMapDemo();
|
||||
Tutorial._openDemoMapFullscreen();
|
||||
},
|
||||
onExit: function() {
|
||||
Tutorial._closeDemoMapFullscreen();
|
||||
Tutorial._clearSubHighlights();
|
||||
},
|
||||
},
|
||||
@@ -1346,43 +1341,153 @@ const Tutorial = {
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Karten-Demo (Step 17): Marker klicken, Geoparse + Vollbild highlighten
|
||||
// Karten-Demo: Vollbild mit Markern und Cursor-Demo
|
||||
// -----------------------------------------------------------------------
|
||||
_openDemoMapFullscreen() {
|
||||
var overlay = document.getElementById('map-fullscreen-overlay');
|
||||
var fsContainer = document.getElementById('map-fullscreen-container');
|
||||
var fsStats = document.getElementById('map-fullscreen-stats');
|
||||
if (!overlay || !fsContainer) return;
|
||||
|
||||
// Overlay anzeigen (\u00fcber Tutorial-Z-Index)
|
||||
overlay.classList.add('active');
|
||||
overlay.style.zIndex = '9002';
|
||||
|
||||
if (fsStats) fsStats.textContent = '3 Orte / 9 Artikel';
|
||||
|
||||
// Alte Demo-Map entfernen falls vorhanden
|
||||
this._destroyDemoMap();
|
||||
|
||||
// Neue Map im Fullscreen-Container erstellen
|
||||
fsContainer.innerHTML = '';
|
||||
var mapDiv = document.createElement('div');
|
||||
mapDiv.id = 'tutorial-fs-map';
|
||||
mapDiv.style.cssText = 'width:100%;height:100%;';
|
||||
fsContainer.appendChild(mapDiv);
|
||||
|
||||
var isDark = !document.documentElement.getAttribute('data-theme') || document.documentElement.getAttribute('data-theme') !== 'light';
|
||||
this._demoMap = L.map(mapDiv, {
|
||||
zoomControl: true,
|
||||
attributionControl: true,
|
||||
}).setView([53.545, 9.98], 13);
|
||||
|
||||
var tileUrl = isDark
|
||||
? 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
|
||||
: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png';
|
||||
L.tileLayer(tileUrl, {
|
||||
attribution: '\u00a9 OpenStreetMap, \u00a9 CARTO',
|
||||
maxZoom: 19,
|
||||
}).addTo(this._demoMap);
|
||||
|
||||
// Marker
|
||||
var locations = [
|
||||
{ lat: 53.5325, lon: 9.9275, name: 'Burchardkai Terminal', articles: 6, cat: 'primary' },
|
||||
{ lat: 53.5460, lon: 9.9690, name: 'Hamburg Innenstadt', articles: 2, cat: 'secondary' },
|
||||
{ lat: 53.5380, lon: 9.9400, name: 'Elbe / Hafengebiet', articles: 1, cat: 'tertiary' },
|
||||
];
|
||||
var catColors = { primary: '#EF4444', secondary: '#F59E0B', tertiary: '#3B82F6' };
|
||||
var catLabels = { primary: 'Hauptereignisort', secondary: 'Erw\u00e4hnt', tertiary: 'Kontext' };
|
||||
var self = this;
|
||||
this._demoMapMarkers = [];
|
||||
|
||||
locations.forEach(function(loc) {
|
||||
var color = catColors[loc.cat];
|
||||
var icon = L.divIcon({
|
||||
className: 'tutorial-map-marker',
|
||||
html: '<div style="width:16px;height:16px;border-radius:50%;background:' + color
|
||||
+ ';border:2px solid #fff;box-shadow:0 2px 8px rgba(0,0,0,0.5);"></div>',
|
||||
iconSize: [16, 16],
|
||||
iconAnchor: [8, 8],
|
||||
});
|
||||
var marker = L.marker([loc.lat, loc.lon], { icon: icon });
|
||||
var popupHtml = '<div class="map-popup">'
|
||||
+ '<div class="map-popup-title">' + loc.name + '</div>'
|
||||
+ '<div style="font-size:11px;margin:4px 0;"><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:' + color + ';margin-right:4px;vertical-align:middle;"></span>' + catLabels[loc.cat] + '</div>'
|
||||
+ '<div class="map-popup-count">' + loc.articles + ' Artikel</div>'
|
||||
+ '</div>';
|
||||
marker.bindPopup(popupHtml, { maxWidth: 250, className: 'map-popup-container' });
|
||||
marker.addTo(self._demoMap);
|
||||
self._demoMapMarkers.push(marker);
|
||||
});
|
||||
|
||||
// Legende
|
||||
var legend = L.control({ position: 'bottomright' });
|
||||
legend.onAdd = function() {
|
||||
var div = L.DomUtil.create('div', 'map-legend-ctrl');
|
||||
L.DomEvent.disableClickPropagation(div);
|
||||
var html = '<strong style="display:block;margin-bottom:6px;">Legende</strong>';
|
||||
['primary', 'secondary', 'tertiary'].forEach(function(cat) {
|
||||
html += '<div class="map-legend-item" style="display:flex;align-items:center;gap:6px;margin:3px 0;">'
|
||||
+ '<span style="width:10px;height:10px;border-radius:50%;background:' + catColors[cat] + ';flex-shrink:0;"></span>'
|
||||
+ '<span>' + catLabels[cat] + '</span></div>';
|
||||
});
|
||||
div.innerHTML = html;
|
||||
return div;
|
||||
};
|
||||
legend.addTo(this._demoMap);
|
||||
this._demoMapLegend = legend;
|
||||
|
||||
// Resize + Start Demo
|
||||
var map = this._demoMap;
|
||||
setTimeout(function() {
|
||||
if (map) map.invalidateSize();
|
||||
setTimeout(function() {
|
||||
if (map) map.setView([53.545, 9.98], 13);
|
||||
self._simulateMapDemo();
|
||||
}, 300);
|
||||
}, 200);
|
||||
},
|
||||
|
||||
_closeDemoMapFullscreen() {
|
||||
var overlay = document.getElementById('map-fullscreen-overlay');
|
||||
if (overlay) {
|
||||
overlay.classList.remove('active');
|
||||
overlay.style.zIndex = '';
|
||||
}
|
||||
var fsContainer = document.getElementById('map-fullscreen-container');
|
||||
if (fsContainer) fsContainer.innerHTML = '';
|
||||
this._destroyDemoMap();
|
||||
},
|
||||
|
||||
async _simulateMapDemo() {
|
||||
this._demoRunning = true;
|
||||
|
||||
// 1. Auf Marker zeigen und klicken
|
||||
await this._wait(500);
|
||||
|
||||
// 1. Cursor zu Hauptmarker und klicken
|
||||
if (this._demoMapMarkers.length > 0 && this._demoMap) {
|
||||
var mainMarker = this._demoMapMarkers[0];
|
||||
var latLng = mainMarker.getLatLng();
|
||||
var point = this._demoMap.latLngToContainerPoint(latLng);
|
||||
var mapContainer = document.getElementById('map-container');
|
||||
if (mapContainer) {
|
||||
var mapRect = mapContainer.getBoundingClientRect();
|
||||
var mapEl = document.getElementById('tutorial-fs-map');
|
||||
if (mapEl) {
|
||||
var mapRect = mapEl.getBoundingClientRect();
|
||||
var markerX = mapRect.left + point.x;
|
||||
var markerY = mapRect.top + point.y;
|
||||
|
||||
this._showCursor(markerX - 40, markerY - 40, 'default');
|
||||
await this._wait(400);
|
||||
await this._animateCursor(markerX - 40, markerY - 40, markerX, markerY, 600);
|
||||
this._showCursor(markerX - 60, markerY - 60, 'default');
|
||||
await this._wait(300);
|
||||
await this._animateCursor(markerX - 60, markerY - 60, markerX, markerY, 700);
|
||||
await this._wait(200);
|
||||
mainMarker.openPopup();
|
||||
await this._wait(2000);
|
||||
await this._wait(2500);
|
||||
mainMarker.closePopup();
|
||||
|
||||
// 2. Zum zweiten Marker
|
||||
var secondMarker = this._demoMapMarkers[1];
|
||||
if (secondMarker) {
|
||||
var p2 = this._demoMap.latLngToContainerPoint(secondMarker.getLatLng());
|
||||
var m2x = mapRect.left + p2.x;
|
||||
var m2y = mapRect.top + p2.y;
|
||||
await this._animateCursor(markerX, markerY, m2x, m2y, 600);
|
||||
await this._wait(200);
|
||||
secondMarker.openPopup();
|
||||
await this._wait(2000);
|
||||
secondMarker.closePopup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Geoparse-Button highlighten
|
||||
this._hideCursor();
|
||||
this._highlightSub('#geoparse-btn');
|
||||
await this._wait(2000);
|
||||
this._clearSubHighlights();
|
||||
|
||||
// 3. Vollbild-Button highlighten
|
||||
this._highlightSub('#map-expand-btn');
|
||||
await this._wait(2000);
|
||||
this._clearSubHighlights();
|
||||
|
||||
this._hideCursor();
|
||||
this._demoRunning = false;
|
||||
this._enableNavAfterDemo();
|
||||
@@ -1642,6 +1747,162 @@ const Tutorial = {
|
||||
this._enableNavAfterDemo();
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Drag-Demo: Kachel visuell verschieben
|
||||
// -----------------------------------------------------------------------
|
||||
async _simulateDrag() {
|
||||
this._demoRunning = true;
|
||||
var tile = document.querySelector('[gs-id="lagebild"]');
|
||||
var header = tile ? tile.querySelector('.card-header') : null;
|
||||
if (!header || !tile) { this._demoRunning = false; this._enableNavAfterDemo(); return; }
|
||||
|
||||
var rect = header.getBoundingClientRect();
|
||||
var startX = rect.left + rect.width / 2;
|
||||
var startY = rect.top + rect.height / 2;
|
||||
var moveX = 150;
|
||||
|
||||
// Cursor erscheint
|
||||
this._showCursor(startX - 50, startY - 30, 'default');
|
||||
await this._wait(300);
|
||||
await this._animateCursor(startX - 50, startY - 30, startX, startY, 500);
|
||||
await this._wait(300);
|
||||
|
||||
// Grabbing
|
||||
this._els.cursor.classList.remove('tutorial-cursor-default');
|
||||
this._els.cursor.classList.add('tutorial-cursor-grabbing');
|
||||
await this._wait(200);
|
||||
|
||||
// Kachel visuell verschieben (CSS transform)
|
||||
tile.style.transition = 'none';
|
||||
tile.style.zIndex = '9002';
|
||||
var self = this;
|
||||
var start = null;
|
||||
await new Promise(function(resolve) {
|
||||
function frame(ts) {
|
||||
if (!self._isActive) { resolve(); return; }
|
||||
if (!start) start = ts;
|
||||
var progress = Math.min((ts - start) / 1200, 1);
|
||||
var eased = progress < 0.5 ? 4*progress*progress*progress : 1 - Math.pow(-2*progress+2,3)/2;
|
||||
var dx = moveX * eased;
|
||||
tile.style.transform = 'translateX(' + dx + 'px)';
|
||||
self._els.cursor.style.left = (startX + dx) + 'px';
|
||||
if (progress < 1) requestAnimationFrame(frame); else resolve();
|
||||
}
|
||||
requestAnimationFrame(frame);
|
||||
});
|
||||
await this._wait(300);
|
||||
|
||||
// Zurück
|
||||
this._els.cursor.classList.remove('tutorial-cursor-grabbing');
|
||||
this._els.cursor.classList.add('tutorial-cursor-default');
|
||||
start = null;
|
||||
await new Promise(function(resolve) {
|
||||
function frame(ts) {
|
||||
if (!self._isActive) { resolve(); return; }
|
||||
if (!start) start = ts;
|
||||
var progress = Math.min((ts - start) / 800, 1);
|
||||
var eased = progress < 0.5 ? 4*progress*progress*progress : 1 - Math.pow(-2*progress+2,3)/2;
|
||||
var dx = moveX * (1 - eased);
|
||||
tile.style.transform = 'translateX(' + dx + 'px)';
|
||||
self._els.cursor.style.left = (startX + dx) + 'px';
|
||||
if (progress < 1) requestAnimationFrame(frame); else resolve();
|
||||
}
|
||||
requestAnimationFrame(frame);
|
||||
});
|
||||
|
||||
// Aufräumen
|
||||
tile.style.transform = '';
|
||||
tile.style.transition = '';
|
||||
tile.style.zIndex = '';
|
||||
this._hideCursor();
|
||||
await this._wait(200);
|
||||
|
||||
this._demoRunning = false;
|
||||
this._enableNavAfterDemo();
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Resize-Demo: Kachel visuell vergrößern
|
||||
// -----------------------------------------------------------------------
|
||||
async _simulateResize() {
|
||||
this._demoRunning = true;
|
||||
var tile = document.querySelector('[gs-id="faktencheck"]');
|
||||
if (!tile) { this._demoRunning = false; this._enableNavAfterDemo(); return; }
|
||||
|
||||
var rect = tile.getBoundingClientRect();
|
||||
var startX = rect.right - 6;
|
||||
var startY = rect.bottom - 6;
|
||||
var expandX = 80;
|
||||
var expandY = 50;
|
||||
|
||||
// Cursor am Rand
|
||||
this._showCursor(startX - 40, startY - 40, 'default');
|
||||
await this._wait(300);
|
||||
await this._animateCursor(startX - 40, startY - 40, startX, startY, 400);
|
||||
await this._wait(200);
|
||||
|
||||
// Resize-Cursor
|
||||
this._els.cursor.classList.remove('tutorial-cursor-default');
|
||||
this._els.cursor.classList.add('tutorial-cursor-resize');
|
||||
await this._wait(200);
|
||||
|
||||
// Kachel vergrößern (visuell)
|
||||
tile.style.transition = 'none';
|
||||
tile.style.zIndex = '9002';
|
||||
var origW = tile.offsetWidth;
|
||||
var origH = tile.offsetHeight;
|
||||
var self = this;
|
||||
var start = null;
|
||||
|
||||
await new Promise(function(resolve) {
|
||||
function frame(ts) {
|
||||
if (!self._isActive) { resolve(); return; }
|
||||
if (!start) start = ts;
|
||||
var progress = Math.min((ts - start) / 1000, 1);
|
||||
var eased = progress < 0.5 ? 4*progress*progress*progress : 1 - Math.pow(-2*progress+2,3)/2;
|
||||
var dx = expandX * eased;
|
||||
var dy = expandY * eased;
|
||||
tile.style.width = (origW + dx) + 'px';
|
||||
tile.style.height = (origH + dy) + 'px';
|
||||
self._els.cursor.style.left = (startX + dx) + 'px';
|
||||
self._els.cursor.style.top = (startY + dy) + 'px';
|
||||
if (progress < 1) requestAnimationFrame(frame); else resolve();
|
||||
}
|
||||
requestAnimationFrame(frame);
|
||||
});
|
||||
await this._wait(500);
|
||||
|
||||
// Zurück
|
||||
start = null;
|
||||
await new Promise(function(resolve) {
|
||||
function frame(ts) {
|
||||
if (!self._isActive) { resolve(); return; }
|
||||
if (!start) start = ts;
|
||||
var progress = Math.min((ts - start) / 700, 1);
|
||||
var eased = progress < 0.5 ? 4*progress*progress*progress : 1 - Math.pow(-2*progress+2,3)/2;
|
||||
var dx = expandX * (1 - eased);
|
||||
var dy = expandY * (1 - eased);
|
||||
tile.style.width = (origW + dx) + 'px';
|
||||
tile.style.height = (origH + dy) + 'px';
|
||||
self._els.cursor.style.left = (startX + dx) + 'px';
|
||||
self._els.cursor.style.top = (startY + dy) + 'px';
|
||||
if (progress < 1) requestAnimationFrame(frame); else resolve();
|
||||
}
|
||||
requestAnimationFrame(frame);
|
||||
});
|
||||
|
||||
// Aufräumen
|
||||
tile.style.width = '';
|
||||
tile.style.height = '';
|
||||
tile.style.transition = '';
|
||||
tile.style.zIndex = '';
|
||||
this._hideCursor();
|
||||
await this._wait(200);
|
||||
|
||||
this._demoRunning = false;
|
||||
this._enableNavAfterDemo();
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Keyboard
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren