From c6b154dbbaeb8362f040cab01e77b87deb1454ed Mon Sep 17 00:00:00 2001 From: Claude Dev Date: Mon, 23 Mar 2026 21:48:14 +0100 Subject: [PATCH] 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 --- src/static/js/tutorial.js | 212 +++++++++++++++++++++++++++++++++----- 1 file changed, 186 insertions(+), 26 deletions(-) diff --git a/src/static/js/tutorial.js b/src/static/js/tutorial.js index 8dfc032..853eaf2 100644 --- a/src/static/js/tutorial.js +++ b/src/static/js/tutorial.js @@ -554,18 +554,36 @@ const Tutorial = { id: 'new-incident-btn', target: '#new-incident-btn', 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.', position: 'right', onEnter: function() { Tutorial._stepTimeout(function() { var overlay = document.getElementById('modal-new'); 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); }, onExit: function() { var overlay = document.getElementById('modal-new'); 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) @@ -883,7 +901,7 @@ const Tutorial = { // Faktencheck: Status-Demo { id: 'faktencheck-detail', - target: '.factcheck-item[data-fc-status="confirmed"]', + target: '[gs-id="faktencheck"]', title: 'Faktencheck-Eintrag', text: 'Jeder Faktencheck-Eintrag besteht aus:

' + 'Status-Symbol - Farbcodiert f\u00fcr schnelle Einordnung
' @@ -913,9 +931,26 @@ const Tutorial = { position: 'top', disableNav: true, 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); }, 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._hideCursor(); }, @@ -1004,6 +1039,8 @@ const Tutorial = { position: 'right', disableNav: true, onEnter: function() { + // Spotlight ausblenden waehrend Drag-Demo + Tutorial._els.spotlight.style.opacity = '0'; Tutorial._runDemo(Tutorial._simulateLayoutDemo); }, onExit: function() { @@ -1013,14 +1050,26 @@ const Tutorial = { Tutorial._hideCursor(); }, }, - // 20 - Theme + // Design umschalten { id: 'theme', target: '#theme-toggle', title: 'Design umschalten', 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', + 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 { @@ -1034,6 +1083,9 @@ const Tutorial = { + 'Quellen deaktivieren - Tempor\u00e4r oder dauerhaft ausschlie\u00dfen', position: 'top', onEnter: function() { + // Button sichtbar halten per z-index + var srcLink = document.querySelector('.sidebar-sources-link'); + if (srcLink) srcLink.style.zIndex = '9003'; Tutorial._stepTimeout(function() { var overlay = document.getElementById('modal-sources'); if (overlay && !overlay.classList.contains('active')) { @@ -1044,6 +1096,8 @@ const Tutorial = { }, 1500); }, onExit: function() { + var srcLink = document.querySelector('.sidebar-sources-link'); + if (srcLink) srcLink.style.zIndex = ''; var overlay = document.getElementById('modal-sources'); if (overlay) { overlay.classList.remove('active'); @@ -1607,7 +1661,7 @@ const Tutorial = { // Start weit herausgezoomt (Europa) this._demoMap = L.map(mapDiv, { - zoomControl: true, + zoomControl: false, dragging: false, touchZoom: false, doubleClickZoom: false, scrollWheelZoom: false, boxZoom: false, keyboard: false, attributionControl: true, }).setView([51.0, 10.0], 5); @@ -1742,12 +1796,20 @@ const Tutorial = { _scrollModalTo(selector) { var el = document.querySelector(selector); 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 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 targetScroll = offset - Math.min(40, bodyRect.height * 0.2); + var targetScroll = offset - 30; 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; } var pos = await this._cursorToElement('#inc-type'); + this._els.cursor.style.zIndex = '999999'; await this._wait(300); // Simuliertes Dropdown @@ -1859,6 +1922,7 @@ const Tutorial = { await this._wait(250); if (dropdown.parentNode) dropdown.remove(); + this._els.cursor.style.zIndex = ''; this._hideCursor(); this._demoRunning = false; this._enableNavAfterDemo(); @@ -1869,7 +1933,7 @@ const Tutorial = { // ----------------------------------------------------------------------- async _simulateFormSources() { this._demoRunning = true; - this._scrollModalTo('#inc-international'); + await this._scrollModalTo('#inc-international'); await this._wait(400); // International-Toggle highlighten @@ -1903,7 +1967,7 @@ const Tutorial = { // ----------------------------------------------------------------------- async _simulateFormVisibility() { this._demoRunning = true; - this._scrollModalTo('#inc-visibility'); + await this._scrollModalTo('#inc-visibility'); await this._wait(400); var checkbox = document.getElementById('inc-visibility'); @@ -1935,7 +1999,7 @@ const Tutorial = { // ----------------------------------------------------------------------- async _simulateFormRefresh() { this._demoRunning = true; - this._scrollModalTo('#inc-refresh-mode'); + await this._scrollModalTo('#inc-refresh-mode'); await this._wait(400); var refreshSelect = document.getElementById('inc-refresh-mode'); @@ -1971,7 +2035,7 @@ const Tutorial = { // ----------------------------------------------------------------------- async _simulateFormRetention() { this._demoRunning = true; - this._scrollModalTo('#inc-retention'); + await this._scrollModalTo('#inc-retention'); await this._wait(400); var retentionInput = document.getElementById('inc-retention'); @@ -1992,7 +2056,7 @@ const Tutorial = { // ----------------------------------------------------------------------- async _simulateFormNotifications() { this._demoRunning = true; - this._scrollModalTo('#inc-notify-summary'); + await this._scrollModalTo('#inc-notify-summary'); await this._wait(400); var checks = ['#inc-notify-summary', '#inc-notify-new-articles', '#inc-notify-status-change']; @@ -2423,6 +2487,8 @@ const Tutorial = { this._enableNavAfterDemo(); }, + // ----------------------------------------------------------------------- + // 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 self = this; - // Phase 1: Drag + // Phase 1: Drag nach rechts (Lagebild <-> Faktencheck tauschen) var lbH = lbTile.querySelector('.card-header'); if (lbH) { var lbR = lbH.getBoundingClientRect(); var fcR = fcTile.getBoundingClientRect(); - 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 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; - this._showCursor(sx-40, sy-30, 'default'); + this._showCursor(sx - 40, sy - 30, 'default'); 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); this._els.cursor.classList.remove('tutorial-cursor-default'); this._els.cursor.classList.add('tutorial-cursor-grabbing'); @@ -2452,7 +2518,7 @@ const Tutorial = { lbTile.style.transition = 'none'; lbTile.style.zIndex = '9002'; - var dx = ex-sx, dy = ey-sy; + var dx = ex - sx, dy = ey - sy; var ms = null; await new Promise(function(resolve) { function f(ts) { @@ -2469,6 +2535,7 @@ const Tutorial = { }); await this._wait(400); + // GridStack-Swap lbTile.style.transform = ''; lbTile.style.transition = ''; lbTile.style.zIndex = ''; @@ -2476,25 +2543,78 @@ const Tutorial = { var ln = grid.engine.nodes.find(function(n){return n.el===lbTile;}); var fn = grid.engine.nodes.find(function(n){return n.el===fcTile;}); 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(fcTile,{x:tmpX,y:tmpY}); 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.add('tutorial-cursor-default'); await this._wait(800); } - // Phase 2: Resize + // Phase 2: Resize Lagebild stark vergroessern lbTile = document.querySelector('[gs-id="lagebild"]'); if (lbTile) { 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( - parseFloat(this._els.cursor.style.left)||hx-40, - parseFloat(this._els.cursor.style.top)||hy-30, hx, hy, 500); + parseFloat(this._els.cursor.style.left) || hx - 40, + parseFloat(this._els.cursor.style.top) || hy - 30, hx, hy, 500); await this._wait(200); this._els.cursor.classList.remove('tutorial-cursor-default'); this._els.cursor.classList.add('tutorial-cursor-resize'); @@ -2504,7 +2624,7 @@ const Tutorial = { lbTile.style.zIndex = '9002'; lbTile.style.transformOrigin = 'top left'; 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; await new Promise(function(resolve) { function f(ts) { @@ -2521,13 +2641,14 @@ const Tutorial = { }); await this._wait(600); + // GridStack-Resize lbTile.style.transform = ''; lbTile.style.transformOrigin = ''; lbTile.style.transition = ''; lbTile.style.zIndex = ''; if (grid) { - var ln2 = 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(); } + var ln3 = grid.engine.nodes.find(function(n){return n.el===lbTile;}); + if (ln3) { grid.update(lbTile,{w:12,h:Math.max(ln3.h+3,6)}); grid.compact(); } } 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 // -----------------------------------------------------------------------