Tutorial Patch 2: Pfeile, Cursor-Z-Index, Modal-Scroll, Karteninteraktion, Layout-Demo, Theme-Toggle

- Schritt 3: Bubble repositioniert sich auf Modal nach Oeffnung
- Schritt 5: Cursor-Z-Index ueber Dropdown (999999)
- Schritt 7ff: Modal scrollt automatisch zu Feldern (async scrollModalTo)
- Schritt 20: Goldener Rahmen um gesamte Faktencheck-Kachel
- Schritt 21: Timeline-Kachel wird temporaer vergroessert
- Schritt 23: Alle Karteninteraktionen deaktiviert (kein Zoom/Click)
- Schritt 25: Drag nach rechts + zurueck, dann Resize vom Original
- Schritt 26: Theme-Toggle-Simulation (hell/dunkel/zurueck)
- Schritt 27: Button bleibt sichtbar nach Quellenverwaltung-Oeffnung
- Spotlight ausgeblendet waehrend Layout-Demo
Dieser Commit ist enthalten in:
Claude Dev
2026-03-23 21:48:14 +01:00
Ursprung 584183951f
Commit c6b154dbba

Datei anzeigen

@@ -554,18 +554,36 @@ const Tutorial = {
id: 'new-incident-btn', id: 'new-incident-btn',
target: '#new-incident-btn', target: '#new-incident-btn',
title: 'Neue Lage anlegen', title: 'Neue Lage anlegen',
text: 'Mit diesem Button öffnen Sie das Formular zur Erstellung einer neuen Lage. ' text: 'Mit diesem Button \u00f6ffnen Sie das Formular zur Erstellung einer neuen Lage. '
+ 'Wir gehen jetzt gemeinsam alle Felder durch.', + 'Wir gehen jetzt gemeinsam alle Felder durch.',
position: 'right', position: 'right',
onEnter: function() { onEnter: function() {
Tutorial._stepTimeout(function() { Tutorial._stepTimeout(function() {
var overlay = document.getElementById('modal-new'); var overlay = document.getElementById('modal-new');
if (overlay && !overlay.classList.contains('active')) overlay.classList.add('active'); if (overlay && !overlay.classList.contains('active')) overlay.classList.add('active');
// Bubble auf Modal umpositionieren nach Oeffnung
Tutorial._stepTimeout(function() {
var modal = document.querySelector('#modal-new .modal');
if (modal) {
var step = Tutorial._steps[Tutorial._currentStep];
step._origTarget = step.target;
step.target = '#modal-new .modal';
step.position = 'left';
Tutorial._positionBubble(step);
}
}, 400);
}, 1500); }, 1500);
}, },
onExit: function() { onExit: function() {
var overlay = document.getElementById('modal-new'); var overlay = document.getElementById('modal-new');
if (overlay) overlay.classList.remove('active'); if (overlay) overlay.classList.remove('active');
// Target zuruecksetzen
var step = Tutorial._steps[2];
if (step && step._origTarget) {
step.target = step._origTarget;
step.position = 'right';
delete step._origTarget;
}
}, },
}, },
// 3 - Titel und Beschreibung (Cursor-Demo) // 3 - Titel und Beschreibung (Cursor-Demo)
@@ -883,7 +901,7 @@ const Tutorial = {
// Faktencheck: Status-Demo // Faktencheck: Status-Demo
{ {
id: 'faktencheck-detail', id: 'faktencheck-detail',
target: '.factcheck-item[data-fc-status="confirmed"]', target: '[gs-id="faktencheck"]',
title: 'Faktencheck-Eintrag', title: 'Faktencheck-Eintrag',
text: 'Jeder Faktencheck-Eintrag besteht aus:<br><br>' text: 'Jeder Faktencheck-Eintrag besteht aus:<br><br>'
+ '<strong>Status-Symbol</strong> - Farbcodiert f\u00fcr schnelle Einordnung<br>' + '<strong>Status-Symbol</strong> - Farbcodiert f\u00fcr schnelle Einordnung<br>'
@@ -913,9 +931,26 @@ const Tutorial = {
position: 'top', position: 'top',
disableNav: true, disableNav: true,
onEnter: function() { onEnter: function() {
// Timeline-Kachel vergroessern damit Detail-Panel sichtbar
var tile = document.querySelector('[gs-id="timeline"]');
if (tile && typeof LayoutManager !== 'undefined' && LayoutManager._grid) {
var node = LayoutManager._grid.engine.nodes.find(function(n) { return n.el === tile; });
if (node) {
Tutorial._savedTimelineH = node.h;
LayoutManager._grid.update(tile, { h: Math.max(node.h, 4) });
}
}
Tutorial._runDemo(Tutorial._simulateTimeline); Tutorial._runDemo(Tutorial._simulateTimeline);
}, },
onExit: function() { onExit: function() {
// Kachel-Hoehe wiederherstellen
if (Tutorial._savedTimelineH) {
var tile = document.querySelector('[gs-id="timeline"]');
if (tile && typeof LayoutManager !== 'undefined' && LayoutManager._grid) {
LayoutManager._grid.update(tile, { h: Tutorial._savedTimelineH });
}
delete Tutorial._savedTimelineH;
}
Tutorial._clearSubHighlights(); Tutorial._clearSubHighlights();
Tutorial._hideCursor(); Tutorial._hideCursor();
}, },
@@ -1004,6 +1039,8 @@ const Tutorial = {
position: 'right', position: 'right',
disableNav: true, disableNav: true,
onEnter: function() { onEnter: function() {
// Spotlight ausblenden waehrend Drag-Demo
Tutorial._els.spotlight.style.opacity = '0';
Tutorial._runDemo(Tutorial._simulateLayoutDemo); Tutorial._runDemo(Tutorial._simulateLayoutDemo);
}, },
onExit: function() { onExit: function() {
@@ -1013,14 +1050,26 @@ const Tutorial = {
Tutorial._hideCursor(); Tutorial._hideCursor();
}, },
}, },
// 20 - Theme // Design umschalten
{ {
id: 'theme', id: 'theme',
target: '#theme-toggle', target: '#theme-toggle',
title: 'Design umschalten', title: 'Design umschalten',
text: 'Wechseln Sie zwischen dem dunklen und hellen Design. ' text: 'Wechseln Sie zwischen dem dunklen und hellen Design. '
+ 'Ihre Auswahl wird automatisch gespeichert und beim nächsten Besuch beibehalten.', + 'Ihre Auswahl wird automatisch gespeichert und beim n\u00e4chsten Besuch beibehalten.',
position: 'bottom', position: 'bottom',
disableNav: true,
onEnter: function() {
Tutorial._runDemo(Tutorial._simulateThemeToggle);
},
onExit: function() {
Tutorial._hideCursor();
// Sicherstellen dass Dark-Theme aktiv ist
if (document.documentElement.getAttribute('data-theme') === 'light') {
var btn = document.getElementById('theme-toggle');
if (btn) btn.click();
}
},
}, },
// Quellen verwalten // Quellen verwalten
{ {
@@ -1034,6 +1083,9 @@ const Tutorial = {
+ '<strong>Quellen deaktivieren</strong> - Tempor\u00e4r oder dauerhaft ausschlie\u00dfen', + '<strong>Quellen deaktivieren</strong> - Tempor\u00e4r oder dauerhaft ausschlie\u00dfen',
position: 'top', position: 'top',
onEnter: function() { onEnter: function() {
// Button sichtbar halten per z-index
var srcLink = document.querySelector('.sidebar-sources-link');
if (srcLink) srcLink.style.zIndex = '9003';
Tutorial._stepTimeout(function() { Tutorial._stepTimeout(function() {
var overlay = document.getElementById('modal-sources'); var overlay = document.getElementById('modal-sources');
if (overlay && !overlay.classList.contains('active')) { if (overlay && !overlay.classList.contains('active')) {
@@ -1044,6 +1096,8 @@ const Tutorial = {
}, 1500); }, 1500);
}, },
onExit: function() { onExit: function() {
var srcLink = document.querySelector('.sidebar-sources-link');
if (srcLink) srcLink.style.zIndex = '';
var overlay = document.getElementById('modal-sources'); var overlay = document.getElementById('modal-sources');
if (overlay) { if (overlay) {
overlay.classList.remove('active'); overlay.classList.remove('active');
@@ -1607,7 +1661,7 @@ const Tutorial = {
// Start weit herausgezoomt (Europa) // Start weit herausgezoomt (Europa)
this._demoMap = L.map(mapDiv, { this._demoMap = L.map(mapDiv, {
zoomControl: true, zoomControl: false, dragging: false, touchZoom: false, doubleClickZoom: false, scrollWheelZoom: false, boxZoom: false, keyboard: false,
attributionControl: true, attributionControl: true,
}).setView([51.0, 10.0], 5); }).setView([51.0, 10.0], 5);
@@ -1742,12 +1796,20 @@ const Tutorial = {
_scrollModalTo(selector) { _scrollModalTo(selector) {
var el = document.querySelector(selector); var el = document.querySelector(selector);
var modalBody = document.querySelector('#modal-new .modal-body'); var modalBody = document.querySelector('#modal-new .modal-body');
if (!el || !modalBody) return; if (!el || !modalBody) return Promise.resolve();
// Element-Position relativ zum sichtbaren Modal-Bereich
var elRect = el.getBoundingClientRect(); var elRect = el.getBoundingClientRect();
var bodyRect = modalBody.getBoundingClientRect(); var bodyRect = modalBody.getBoundingClientRect();
// Pruefen ob Element bereits sichtbar
if (elRect.top >= bodyRect.top && elRect.bottom <= bodyRect.bottom) {
return Promise.resolve();
}
// Scroll-Ziel: Element mit Abstand oben im sichtbaren Bereich
var offset = elRect.top - bodyRect.top + modalBody.scrollTop; var offset = elRect.top - bodyRect.top + modalBody.scrollTop;
var targetScroll = offset - Math.min(40, bodyRect.height * 0.2); var targetScroll = offset - 30;
modalBody.scrollTo({ top: Math.max(0, targetScroll), behavior: 'smooth' }); modalBody.scrollTo({ top: Math.max(0, targetScroll), behavior: 'smooth' });
// Warten bis Scroll abgeschlossen
return new Promise(function(resolve) { setTimeout(resolve, 500); });
}, },
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
@@ -1795,6 +1857,7 @@ const Tutorial = {
if (!sel) { this._demoRunning = false; this._enableNavAfterDemo(); return; } if (!sel) { this._demoRunning = false; this._enableNavAfterDemo(); return; }
var pos = await this._cursorToElement('#inc-type'); var pos = await this._cursorToElement('#inc-type');
this._els.cursor.style.zIndex = '999999';
await this._wait(300); await this._wait(300);
// Simuliertes Dropdown // Simuliertes Dropdown
@@ -1859,6 +1922,7 @@ const Tutorial = {
await this._wait(250); await this._wait(250);
if (dropdown.parentNode) dropdown.remove(); if (dropdown.parentNode) dropdown.remove();
this._els.cursor.style.zIndex = '';
this._hideCursor(); this._hideCursor();
this._demoRunning = false; this._demoRunning = false;
this._enableNavAfterDemo(); this._enableNavAfterDemo();
@@ -1869,7 +1933,7 @@ const Tutorial = {
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
async _simulateFormSources() { async _simulateFormSources() {
this._demoRunning = true; this._demoRunning = true;
this._scrollModalTo('#inc-international'); await this._scrollModalTo('#inc-international');
await this._wait(400); await this._wait(400);
// International-Toggle highlighten // International-Toggle highlighten
@@ -1903,7 +1967,7 @@ const Tutorial = {
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
async _simulateFormVisibility() { async _simulateFormVisibility() {
this._demoRunning = true; this._demoRunning = true;
this._scrollModalTo('#inc-visibility'); await this._scrollModalTo('#inc-visibility');
await this._wait(400); await this._wait(400);
var checkbox = document.getElementById('inc-visibility'); var checkbox = document.getElementById('inc-visibility');
@@ -1935,7 +1999,7 @@ const Tutorial = {
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
async _simulateFormRefresh() { async _simulateFormRefresh() {
this._demoRunning = true; this._demoRunning = true;
this._scrollModalTo('#inc-refresh-mode'); await this._scrollModalTo('#inc-refresh-mode');
await this._wait(400); await this._wait(400);
var refreshSelect = document.getElementById('inc-refresh-mode'); var refreshSelect = document.getElementById('inc-refresh-mode');
@@ -1971,7 +2035,7 @@ const Tutorial = {
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
async _simulateFormRetention() { async _simulateFormRetention() {
this._demoRunning = true; this._demoRunning = true;
this._scrollModalTo('#inc-retention'); await this._scrollModalTo('#inc-retention');
await this._wait(400); await this._wait(400);
var retentionInput = document.getElementById('inc-retention'); var retentionInput = document.getElementById('inc-retention');
@@ -1992,7 +2056,7 @@ const Tutorial = {
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
async _simulateFormNotifications() { async _simulateFormNotifications() {
this._demoRunning = true; this._demoRunning = true;
this._scrollModalTo('#inc-notify-summary'); await this._scrollModalTo('#inc-notify-summary');
await this._wait(400); await this._wait(400);
var checks = ['#inc-notify-summary', '#inc-notify-new-articles', '#inc-notify-status-change']; var checks = ['#inc-notify-summary', '#inc-notify-new-articles', '#inc-notify-status-change'];
@@ -2423,6 +2487,8 @@ const Tutorial = {
this._enableNavAfterDemo(); this._enableNavAfterDemo();
}, },
// -----------------------------------------------------------------------
// Layout Demo (Drag + Resize kombiniert)
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// Layout Demo (Drag + Resize kombiniert) // Layout Demo (Drag + Resize kombiniert)
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
@@ -2434,17 +2500,17 @@ const Tutorial = {
var grid = typeof LayoutManager !== 'undefined' ? LayoutManager._grid : null; var grid = typeof LayoutManager !== 'undefined' ? LayoutManager._grid : null;
var self = this; var self = this;
// Phase 1: Drag // Phase 1: Drag nach rechts (Lagebild <-> Faktencheck tauschen)
var lbH = lbTile.querySelector('.card-header'); var lbH = lbTile.querySelector('.card-header');
if (lbH) { if (lbH) {
var lbR = lbH.getBoundingClientRect(); var lbR = lbH.getBoundingClientRect();
var fcR = fcTile.getBoundingClientRect(); var fcR = fcTile.getBoundingClientRect();
var sx = lbR.left+lbR.width/2, sy = lbR.top+lbR.height/2; var sx = lbR.left + lbR.width/2, sy = lbR.top + lbR.height/2;
var ex = fcR.left+fcR.width/2, ey = fcR.top+lbR.height/2; var ex = fcR.left + fcR.width/2, ey = fcR.top + lbR.height/2;
this._showCursor(sx-40, sy-30, 'default'); this._showCursor(sx - 40, sy - 30, 'default');
await this._wait(300); await this._wait(300);
await this._animateCursor(sx-40, sy-30, sx, sy, 400); await this._animateCursor(sx - 40, sy - 30, sx, sy, 400);
await this._wait(200); await this._wait(200);
this._els.cursor.classList.remove('tutorial-cursor-default'); this._els.cursor.classList.remove('tutorial-cursor-default');
this._els.cursor.classList.add('tutorial-cursor-grabbing'); this._els.cursor.classList.add('tutorial-cursor-grabbing');
@@ -2452,7 +2518,7 @@ const Tutorial = {
lbTile.style.transition = 'none'; lbTile.style.transition = 'none';
lbTile.style.zIndex = '9002'; lbTile.style.zIndex = '9002';
var dx = ex-sx, dy = ey-sy; var dx = ex - sx, dy = ey - sy;
var ms = null; var ms = null;
await new Promise(function(resolve) { await new Promise(function(resolve) {
function f(ts) { function f(ts) {
@@ -2469,6 +2535,7 @@ const Tutorial = {
}); });
await this._wait(400); await this._wait(400);
// GridStack-Swap
lbTile.style.transform = ''; lbTile.style.transform = '';
lbTile.style.transition = ''; lbTile.style.transition = '';
lbTile.style.zIndex = ''; lbTile.style.zIndex = '';
@@ -2476,25 +2543,78 @@ const Tutorial = {
var ln = grid.engine.nodes.find(function(n){return n.el===lbTile;}); var ln = grid.engine.nodes.find(function(n){return n.el===lbTile;});
var fn = grid.engine.nodes.find(function(n){return n.el===fcTile;}); var fn = grid.engine.nodes.find(function(n){return n.el===fcTile;});
if (ln && fn) { if (ln && fn) {
var tmpX=ln.x, tmpY=ln.y; var tmpX=ln.x, tmpY=ln.y, tmpW=ln.w, tmpH=ln.h;
grid.update(lbTile,{x:fn.x,y:fn.y}); grid.update(lbTile,{x:fn.x,y:fn.y});
grid.update(fcTile,{x:tmpX,y:tmpY}); grid.update(fcTile,{x:tmpX,y:tmpY});
grid.compact(); grid.compact();
} }
} }
await this._wait(600);
// Phase 1b: Zurueck tauschen
lbTile = document.querySelector('[gs-id="lagebild"]');
fcTile = document.querySelector('[gs-id="faktencheck"]');
if (lbTile && fcTile) {
lbH = lbTile.querySelector('.card-header');
if (lbH) {
lbR = lbH.getBoundingClientRect();
fcR = fcTile.getBoundingClientRect();
sx = lbR.left + lbR.width/2; sy = lbR.top + lbR.height/2;
ex = fcR.left + fcR.width/2; ey = fcR.top + lbR.height/2;
dx = ex - sx; dy = ey - sy;
await this._animateCursor(
parseFloat(this._els.cursor.style.left),
parseFloat(this._els.cursor.style.top), sx, sy, 400);
await this._wait(200);
lbTile.style.transition = 'none';
lbTile.style.zIndex = '9002';
ms = null;
await new Promise(function(resolve) {
function f(ts) {
if (!self._isActive) { resolve(); return; }
if (!ms) ms = ts;
var p = Math.min((ts-ms)/1000,1);
var e2 = p<0.5?4*p*p*p:1-Math.pow(-2*p+2,3)/2;
lbTile.style.transform = 'translate('+(dx*e2)+'px,'+(dy*e2)+'px)';
self._els.cursor.style.left = (sx+dx*e2)+'px';
self._els.cursor.style.top = (sy+dy*e2)+'px';
if (p<1) requestAnimationFrame(f); else resolve();
}
requestAnimationFrame(f);
});
await this._wait(300);
lbTile.style.transform = '';
lbTile.style.transition = '';
lbTile.style.zIndex = '';
if (grid) {
var ln2 = grid.engine.nodes.find(function(n){return n.el===lbTile;});
var fn2 = grid.engine.nodes.find(function(n){return n.el===fcTile;});
if (ln2 && fn2) {
var tx2=ln2.x, ty2=ln2.y;
grid.update(lbTile,{x:fn2.x,y:fn2.y});
grid.update(fcTile,{x:tx2,y:ty2});
grid.compact();
}
}
}
}
this._els.cursor.classList.remove('tutorial-cursor-grabbing'); this._els.cursor.classList.remove('tutorial-cursor-grabbing');
this._els.cursor.classList.add('tutorial-cursor-default'); this._els.cursor.classList.add('tutorial-cursor-default');
await this._wait(800); await this._wait(800);
} }
// Phase 2: Resize // Phase 2: Resize Lagebild stark vergroessern
lbTile = document.querySelector('[gs-id="lagebild"]'); lbTile = document.querySelector('[gs-id="lagebild"]');
if (lbTile) { if (lbTile) {
var tr = lbTile.getBoundingClientRect(); var tr = lbTile.getBoundingClientRect();
var hx = tr.right-6, hy = tr.bottom-6; var hx = tr.right - 6, hy = tr.bottom - 6;
await this._animateCursor( await this._animateCursor(
parseFloat(this._els.cursor.style.left)||hx-40, parseFloat(this._els.cursor.style.left) || hx - 40,
parseFloat(this._els.cursor.style.top)||hy-30, hx, hy, 500); parseFloat(this._els.cursor.style.top) || hy - 30, hx, hy, 500);
await this._wait(200); await this._wait(200);
this._els.cursor.classList.remove('tutorial-cursor-default'); this._els.cursor.classList.remove('tutorial-cursor-default');
this._els.cursor.classList.add('tutorial-cursor-resize'); this._els.cursor.classList.add('tutorial-cursor-resize');
@@ -2504,7 +2624,7 @@ const Tutorial = {
lbTile.style.zIndex = '9002'; lbTile.style.zIndex = '9002';
lbTile.style.transformOrigin = 'top left'; lbTile.style.transformOrigin = 'top left';
var ow = tr.width, oh = tr.height; var ow = tr.width, oh = tr.height;
var epx = ow*0.6, epy = oh*0.5; var epx = ow * 0.6, epy = oh * 0.5;
var rs = null; var rs = null;
await new Promise(function(resolve) { await new Promise(function(resolve) {
function f(ts) { function f(ts) {
@@ -2521,13 +2641,14 @@ const Tutorial = {
}); });
await this._wait(600); await this._wait(600);
// GridStack-Resize
lbTile.style.transform = ''; lbTile.style.transform = '';
lbTile.style.transformOrigin = ''; lbTile.style.transformOrigin = '';
lbTile.style.transition = ''; lbTile.style.transition = '';
lbTile.style.zIndex = ''; lbTile.style.zIndex = '';
if (grid) { if (grid) {
var ln2 = grid.engine.nodes.find(function(n){return n.el===lbTile;}); var ln3 = grid.engine.nodes.find(function(n){return n.el===lbTile;});
if (ln2) { grid.update(lbTile,{w:12,h:Math.max(ln2.h+3,6)}); grid.compact(); } if (ln3) { grid.update(lbTile,{w:12,h:Math.max(ln3.h+3,6)}); grid.compact(); }
} }
await this._wait(600); await this._wait(600);
} }
@@ -2538,6 +2659,45 @@ const Tutorial = {
}, },
// -----------------------------------------------------------------------
// Theme-Toggle Demo
// -----------------------------------------------------------------------
async _simulateThemeToggle() {
this._demoRunning = true;
await this._wait(400);
var btn = document.getElementById('theme-toggle');
if (!btn) { this._demoRunning = false; this._enableNavAfterDemo(); return; }
var rect = btn.getBoundingClientRect();
var tx = rect.left + rect.width / 2;
var ty = rect.top + rect.height / 2;
this._showCursor(tx - 40, ty - 30, 'default');
await this._wait(300);
await this._animateCursor(tx - 40, ty - 30, tx, ty, 400);
await this._wait(300);
// Klick - zum hellen Design wechseln
btn.click();
await this._wait(2500);
// Zurueck zum dunklen Design
rect = btn.getBoundingClientRect();
tx = rect.left + rect.width / 2;
ty = rect.top + rect.height / 2;
await this._animateCursor(
parseFloat(this._els.cursor.style.left),
parseFloat(this._els.cursor.style.top), tx, ty, 300);
await this._wait(300);
btn.click();
await this._wait(800);
this._hideCursor();
this._demoRunning = false;
this._enableNavAfterDemo();
},
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// Navigation nach Demo-Ende freigeben // Navigation nach Demo-Ende freigeben
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------