Interaktives Tutorial-System mit 20 Schritten, Spotlight, Sprechblasen und virtuellen Maus-Demos
Neues Tutorial-System fuer gefuehrten Rundgang durch den Monitor: - tutorial.js: Tutorial-Engine mit Spotlight-Abdunkelung, Bubble-Navigation, virtuellem Cursor fuer Drag/Resize-Demos, Keyboard-Support (Escape/Pfeiltasten) - 20 Steps: Welcome, Sidebar, Lagen, Kacheln, Layout, Theme, Export, Chat, etc. - Automatisches Ueberspringen von Steps wenn keine Lage geoeffnet - Modal-Handling fuer Neue-Lage und Quellenverwaltung Steps - Chat-Integration: Tutorial-Hinweis beim ersten Oeffnen, Keywords (rundgang/tutorial/tour/fuehrung) - localStorage-Persistenz (osint_tutorial_seen) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
743
src/static/js/tutorial.js
Normale Datei
743
src/static/js/tutorial.js
Normale Datei
@@ -0,0 +1,743 @@
|
||||
/**
|
||||
* AegisSight Tutorial - Interaktiver Rundgang durch den Monitor.
|
||||
*/
|
||||
const Tutorial = {
|
||||
_steps: [],
|
||||
_currentStep: -1,
|
||||
_isActive: false,
|
||||
_cleanupFns: [],
|
||||
_resizeTimer: null,
|
||||
_keyHandler: null,
|
||||
_resizeHandler: null,
|
||||
_demoRunning: false,
|
||||
|
||||
// DOM-Referenzen
|
||||
_els: {
|
||||
overlay: null,
|
||||
spotlight: null,
|
||||
bubble: null,
|
||||
cursor: null,
|
||||
},
|
||||
|
||||
init() {
|
||||
this._defineSteps();
|
||||
this._els.overlay = document.getElementById('tutorial-overlay');
|
||||
this._els.spotlight = document.getElementById('tutorial-spotlight');
|
||||
this._els.bubble = document.getElementById('tutorial-bubble');
|
||||
this._els.cursor = document.getElementById('tutorial-cursor');
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Step-Definitionen
|
||||
// -----------------------------------------------------------------------
|
||||
_defineSteps() {
|
||||
this._steps = [
|
||||
// 0 - Welcome
|
||||
{
|
||||
id: 'welcome',
|
||||
target: '#main-content',
|
||||
title: 'Willkommen im AegisSight Monitor',
|
||||
text: 'Dieser kurze Rundgang zeigt Ihnen die wichtigsten Funktionen des Monitors. Sie können jederzeit mit Escape abbrechen oder mit den Pfeiltasten navigieren.',
|
||||
position: 'center',
|
||||
},
|
||||
// 1 - Sidebar
|
||||
{
|
||||
id: 'sidebar',
|
||||
target: '.sidebar',
|
||||
title: 'Seitenleiste: Ihre Lagen',
|
||||
text: 'Hier finden Sie alle Ihre Lagen, unterteilt in Live-Monitoring, Recherchen und das Archiv. Klicken Sie auf eine Lage, um sie zu öffnen.',
|
||||
position: 'right',
|
||||
},
|
||||
// 2 - Neue Lage Button
|
||||
{
|
||||
id: 'new-incident-btn',
|
||||
target: '#new-incident-btn',
|
||||
title: 'Neue Lage anlegen',
|
||||
text: 'Mit diesem Button erstellen Sie eine neue Lage. Sie können zwischen Live-Monitoring und Recherche wählen.',
|
||||
position: 'right',
|
||||
onEnter: function() {
|
||||
setTimeout(function() {
|
||||
var btn = document.getElementById('new-incident-btn');
|
||||
if (btn) btn.click();
|
||||
}, 1500);
|
||||
},
|
||||
},
|
||||
// 3 - Neue Lage Modal
|
||||
{
|
||||
id: 'new-incident-modal',
|
||||
target: '#modal-new .modal',
|
||||
title: 'Lage konfigurieren',
|
||||
text: 'Hier geben Sie den Titel, die Beschreibung und den Typ der Lage ein. Bei Live-Monitoring-Lagen können Sie zusätzlich Suchbegriffe und Quellen festlegen.',
|
||||
position: 'right',
|
||||
requireModal: '#modal-new',
|
||||
onEnter: function() {
|
||||
var overlay = document.getElementById('modal-new');
|
||||
if (overlay && !overlay.classList.contains('active')) {
|
||||
overlay.classList.add('active');
|
||||
}
|
||||
if (overlay) overlay.style.zIndex = '9002';
|
||||
},
|
||||
onExit: function() {
|
||||
var overlay = document.getElementById('modal-new');
|
||||
if (overlay) {
|
||||
overlay.classList.remove('active');
|
||||
overlay.style.zIndex = '';
|
||||
}
|
||||
},
|
||||
},
|
||||
// 4 - Sidebar Filter
|
||||
{
|
||||
id: 'sidebar-filters',
|
||||
target: '.sidebar-filter',
|
||||
title: 'Lagen filtern',
|
||||
text: 'Wechseln Sie zwischen "Alle" Lagen und "Eigene", um nur Ihre selbst erstellten Lagen anzuzeigen.',
|
||||
position: 'right',
|
||||
},
|
||||
// 5 - Lagebild
|
||||
{
|
||||
id: 'lagebild',
|
||||
target: '[gs-id="lagebild"]',
|
||||
title: 'Lagebild',
|
||||
text: 'Das Lagebild fasst die aktuelle Situation zusammen. Quellenverweise sind als Nummern markiert und führen direkt zu den Originalartikeln. Über das Pfeil-Symbol öffnen Sie die Vollansicht.',
|
||||
position: 'top',
|
||||
requireIncident: true,
|
||||
},
|
||||
// 6 - Faktencheck
|
||||
{
|
||||
id: 'faktencheck',
|
||||
target: '[gs-id="faktencheck"]',
|
||||
title: 'Faktencheck',
|
||||
text: 'Hier werden Behauptungen automatisch geprüft und in Kategorien eingeteilt: Bestätigt, Widersprüchlich, Unbelegt und Falsch. Jeder Eintrag zeigt die Quellen der Bewertung.',
|
||||
position: 'top',
|
||||
requireIncident: true,
|
||||
},
|
||||
// 7 - Quellen
|
||||
{
|
||||
id: 'quellen',
|
||||
target: '[gs-id="quellen"]',
|
||||
title: 'Quellenübersicht',
|
||||
text: 'Alle verwendeten Quellen auf einen Blick, gruppiert nach Kategorie. Klicken Sie auf eine Gruppe, um die einzelnen Quellen aufzuklappen.',
|
||||
position: 'top',
|
||||
requireIncident: true,
|
||||
},
|
||||
// 8 - Timeline
|
||||
{
|
||||
id: 'timeline',
|
||||
target: '[gs-id="timeline"]',
|
||||
title: 'Ereignis-Timeline',
|
||||
text: 'Die Timeline zeigt alle Ereignisse chronologisch. Nutzen Sie die Filter, um nach Zeitraum oder Kategorie einzugrenzen.',
|
||||
position: 'top',
|
||||
requireIncident: true,
|
||||
},
|
||||
// 9 - Karte
|
||||
{
|
||||
id: 'karte',
|
||||
target: '[gs-id="karte"]',
|
||||
title: 'Geografische Verteilung',
|
||||
text: 'Die Karte zeigt automatisch erkannte Orte aus den Quellen. Klicken Sie auf Marker für Details oder nutzen Sie das Vollbild-Symbol für eine größere Ansicht.',
|
||||
position: 'top',
|
||||
requireIncident: true,
|
||||
},
|
||||
// 10 - Layout Toolbar
|
||||
{
|
||||
id: 'layout-toolbar',
|
||||
target: '#layout-toolbar',
|
||||
title: 'Layout-Steuerung',
|
||||
text: 'Mit diesen Schaltern blenden Sie einzelne Kacheln ein oder aus. So passen Sie das Dashboard an Ihre Bedürfnisse an.',
|
||||
position: 'bottom',
|
||||
requireIncident: true,
|
||||
},
|
||||
// 11 - Drag Demo
|
||||
{
|
||||
id: 'drag-demo',
|
||||
target: '[gs-id="lagebild"] .card-header',
|
||||
title: 'Kacheln verschieben',
|
||||
text: 'Sie können jede Kachel per Drag-and-Drop an eine andere Position verschieben. Greifen Sie dazu die Kopfzeile der Kachel und ziehen Sie sie an die gewünschte Stelle.',
|
||||
position: 'top',
|
||||
requireIncident: true,
|
||||
disableNav: true,
|
||||
onEnter: function() {
|
||||
Tutorial._simulateDrag();
|
||||
},
|
||||
},
|
||||
// 12 - Resize Demo
|
||||
{
|
||||
id: 'resize-demo',
|
||||
target: '[gs-id="faktencheck"]',
|
||||
title: 'Kacheln anpassen',
|
||||
text: 'Ziehen Sie am rechten unteren Rand einer Kachel, um ihre Größe anzupassen. So können Sie wichtigen Inhalten mehr Platz einräumen.',
|
||||
position: 'top',
|
||||
requireIncident: true,
|
||||
disableNav: true,
|
||||
onEnter: function() {
|
||||
Tutorial._simulateResize();
|
||||
},
|
||||
},
|
||||
// 13 - Theme
|
||||
{
|
||||
id: 'theme',
|
||||
target: '#theme-toggle',
|
||||
title: 'Design umschalten',
|
||||
text: 'Wechseln Sie zwischen dem dunklen und hellen Design. Ihre Auswahl wird gespeichert.',
|
||||
position: 'bottom',
|
||||
},
|
||||
// 14 - Refresh
|
||||
{
|
||||
id: 'refresh',
|
||||
target: '#refresh-btn',
|
||||
title: 'Aktualisieren',
|
||||
text: 'Starten Sie eine manuelle Aktualisierung der Lage. Bei Live-Monitoring-Lagen erfolgt die Aktualisierung zusätzlich automatisch in regelmaessigen Abstaenden.',
|
||||
position: 'bottom',
|
||||
requireIncident: true,
|
||||
},
|
||||
// 15 - Quellen-Button
|
||||
{
|
||||
id: 'sources-btn',
|
||||
target: '.sidebar-sources-link button:first-child',
|
||||
title: 'Quellenverwaltung',
|
||||
text: 'Hier öffnen Sie die Quellenverwaltung, um Quellen hinzuzufügen, zu bearbeiten oder zu deaktivieren.',
|
||||
position: 'right',
|
||||
onEnter: function() {
|
||||
setTimeout(function() {
|
||||
if (typeof App !== 'undefined' && App.openSourceManagement) {
|
||||
App.openSourceManagement();
|
||||
}
|
||||
}, 1500);
|
||||
},
|
||||
},
|
||||
// 16 - Quellen Modal
|
||||
{
|
||||
id: 'sources-modal',
|
||||
target: '#modal-sources .modal',
|
||||
title: 'Quellen verwalten',
|
||||
text: 'In der Quellenverwaltung sehen Sie alle konfigurierten Quellen. Sie können neue Quellen hinzufuegen, bestehende bearbeiten oder einzelne Quellen für bestimmte Lagen ausschließen.',
|
||||
position: 'right',
|
||||
requireModal: '#modal-sources',
|
||||
onEnter: function() {
|
||||
var overlay = document.getElementById('modal-sources');
|
||||
if (overlay && !overlay.classList.contains('active')) {
|
||||
if (typeof App !== 'undefined' && App.openSourceManagement) {
|
||||
App.openSourceManagement();
|
||||
}
|
||||
}
|
||||
if (overlay) overlay.style.zIndex = '9002';
|
||||
},
|
||||
onExit: function() {
|
||||
var overlay = document.getElementById('modal-sources');
|
||||
if (overlay) {
|
||||
overlay.classList.remove('active');
|
||||
overlay.style.zIndex = '';
|
||||
}
|
||||
},
|
||||
},
|
||||
// 17 - Export
|
||||
{
|
||||
id: 'export',
|
||||
target: '#export-dropdown',
|
||||
title: 'Exportieren',
|
||||
text: 'Exportieren Sie den Lagebericht als Markdown, JSON oder drucken Sie ihn als PDF. Waehlen Sie zwischen dem kompakten Lagebericht und dem Vollexport mit allen Details.',
|
||||
position: 'bottom',
|
||||
requireIncident: true,
|
||||
},
|
||||
// 18 - Chat
|
||||
{
|
||||
id: 'chat',
|
||||
target: '#chat-toggle-btn',
|
||||
title: 'Chat-Assistent',
|
||||
text: 'Der Chat-Assistent hilft Ihnen bei Fragen zur Bedienung des Monitors. Stellen Sie einfach Ihre Frage und erhalten Sie sofort eine Antwort.',
|
||||
position: 'left',
|
||||
},
|
||||
// 19 - Ende
|
||||
{
|
||||
id: 'end',
|
||||
target: null,
|
||||
title: 'Rundgang abgeschlossen',
|
||||
text: 'Sie kennen jetzt die wichtigsten Funktionen des AegisSight Monitors. Bei weiteren Fragen steht Ihnen der Chat-Assistent oder unser Support unter support@aegis-sight.de zur Verfügung.',
|
||||
position: 'center',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Lifecycle
|
||||
// -----------------------------------------------------------------------
|
||||
start() {
|
||||
if (this._isActive) return;
|
||||
this._isActive = true;
|
||||
this._currentStep = -1;
|
||||
|
||||
// Chat schliessen falls offen
|
||||
if (typeof Chat !== 'undefined' && Chat._isOpen) Chat.close();
|
||||
|
||||
// Overlay einblenden
|
||||
this._els.overlay.classList.add('active');
|
||||
|
||||
// Keyboard
|
||||
this._keyHandler = this._onKey.bind(this);
|
||||
document.addEventListener('keydown', this._keyHandler);
|
||||
|
||||
// Resize
|
||||
this._resizeHandler = this._onResize.bind(this);
|
||||
window.addEventListener('resize', this._resizeHandler);
|
||||
|
||||
this.next();
|
||||
},
|
||||
|
||||
stop() {
|
||||
if (!this._isActive) return;
|
||||
|
||||
// Aktuellen Step aufraumen
|
||||
if (this._currentStep >= 0) this._exitStep(this._currentStep);
|
||||
|
||||
this._isActive = false;
|
||||
this._currentStep = -1;
|
||||
this._demoRunning = false;
|
||||
|
||||
// Overlay ausblenden
|
||||
this._els.overlay.classList.remove('active');
|
||||
this._els.spotlight.style.opacity = '0';
|
||||
this._els.bubble.classList.remove('visible');
|
||||
this._hideCursor();
|
||||
|
||||
// Events entfernen
|
||||
if (this._keyHandler) {
|
||||
document.removeEventListener('keydown', this._keyHandler);
|
||||
this._keyHandler = null;
|
||||
}
|
||||
if (this._resizeHandler) {
|
||||
window.removeEventListener('resize', this._resizeHandler);
|
||||
this._resizeHandler = null;
|
||||
}
|
||||
|
||||
// Cleanup-Callbacks
|
||||
this._cleanupFns.forEach(function(fn) { try { fn(); } catch(e) {} });
|
||||
this._cleanupFns = [];
|
||||
|
||||
this._markSeen();
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Navigation
|
||||
// -----------------------------------------------------------------------
|
||||
next() {
|
||||
if (!this._isActive || this._demoRunning) return;
|
||||
var nextIdx = this._currentStep + 1;
|
||||
|
||||
// Skip Steps deren Voraussetzungen nicht erfuellt sind
|
||||
while (nextIdx < this._steps.length && this._shouldSkip(nextIdx)) {
|
||||
nextIdx++;
|
||||
}
|
||||
|
||||
if (nextIdx >= this._steps.length) {
|
||||
this.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
this.goToStep(nextIdx);
|
||||
},
|
||||
|
||||
prev() {
|
||||
if (!this._isActive || this._demoRunning) return;
|
||||
var prevIdx = this._currentStep - 1;
|
||||
|
||||
while (prevIdx >= 0 && this._shouldSkip(prevIdx)) {
|
||||
prevIdx--;
|
||||
}
|
||||
|
||||
if (prevIdx < 0) return;
|
||||
this.goToStep(prevIdx);
|
||||
},
|
||||
|
||||
goToStep(index) {
|
||||
if (index < 0 || index >= this._steps.length) return;
|
||||
|
||||
if (this._currentStep >= 0) this._exitStep(this._currentStep);
|
||||
this._currentStep = index;
|
||||
this._enterStep(index);
|
||||
},
|
||||
|
||||
_shouldSkip(index) {
|
||||
var step = this._steps[index];
|
||||
if (!step) return true;
|
||||
|
||||
// Steps die eine offene Lage brauchen
|
||||
if (step.requireIncident) {
|
||||
var view = document.getElementById('incident-view');
|
||||
if (!view || view.style.display === 'none' || !view.offsetParent) return true;
|
||||
}
|
||||
|
||||
// Target muss existieren (ausser center-Steps)
|
||||
if (step.target && step.position !== 'center') {
|
||||
var el = document.querySelector(step.target);
|
||||
if (!el) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Step Enter/Exit
|
||||
// -----------------------------------------------------------------------
|
||||
_enterStep(i) {
|
||||
var step = this._steps[i];
|
||||
var self = this;
|
||||
|
||||
// Spotlight positionieren
|
||||
if (step.target && step.position !== 'center') {
|
||||
this._spotlightElement(step.target);
|
||||
} else {
|
||||
// Kein Spotlight / zentrierte Bubble
|
||||
this._els.spotlight.style.opacity = '0';
|
||||
}
|
||||
|
||||
// Bubble konfigurieren und anzeigen
|
||||
this._showBubble(step, i);
|
||||
|
||||
// onEnter-Callback
|
||||
if (step.onEnter) {
|
||||
step.onEnter();
|
||||
}
|
||||
},
|
||||
|
||||
_exitStep(i) {
|
||||
var step = this._steps[i];
|
||||
|
||||
// onExit-Callback
|
||||
if (step.onExit) {
|
||||
step.onExit();
|
||||
}
|
||||
|
||||
this._hideCursor();
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Spotlight
|
||||
// -----------------------------------------------------------------------
|
||||
_spotlightElement(selector) {
|
||||
var el = document.querySelector(selector);
|
||||
if (!el) {
|
||||
this._els.spotlight.style.opacity = '0';
|
||||
return;
|
||||
}
|
||||
|
||||
var rect = el.getBoundingClientRect();
|
||||
var pad = 8;
|
||||
var s = this._els.spotlight.style;
|
||||
|
||||
s.top = (rect.top - pad) + 'px';
|
||||
s.left = (rect.left - pad) + 'px';
|
||||
s.width = (rect.width + pad * 2) + 'px';
|
||||
s.height = (rect.height + pad * 2) + 'px';
|
||||
s.borderRadius = '8px';
|
||||
s.opacity = '1';
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Bubble
|
||||
// -----------------------------------------------------------------------
|
||||
_showBubble(step, index) {
|
||||
var bubble = this._els.bubble;
|
||||
var total = this._steps.length;
|
||||
|
||||
// Inhalt
|
||||
var counterHtml = '<div class="tutorial-bubble-counter">Schritt ' + (index + 1) + ' von ' + total + '</div>';
|
||||
var titleHtml = '<div class="tutorial-bubble-title">' + step.title + '</div>';
|
||||
var textHtml = '<div class="tutorial-bubble-text">' + step.text + '</div>';
|
||||
|
||||
// Fortschrittspunkte
|
||||
var dotsHtml = '<div class="tutorial-bubble-dots">';
|
||||
for (var d = 0; d < total; d++) {
|
||||
dotsHtml += '<span class="tutorial-dot' + (d === index ? ' active' : '') + (d < index ? ' done' : '') + '"></span>';
|
||||
}
|
||||
dotsHtml += '</div>';
|
||||
|
||||
// Navigation
|
||||
var navHtml = '<div class="tutorial-bubble-nav">';
|
||||
if (index > 0 && !step.disableNav) {
|
||||
navHtml += '<button class="tutorial-btn tutorial-btn-back" onclick="Tutorial.prev()">Zurück</button>';
|
||||
} else {
|
||||
navHtml += '<span></span>';
|
||||
}
|
||||
if (index < total - 1 && !step.disableNav) {
|
||||
navHtml += '<button class="tutorial-btn tutorial-btn-next" onclick="Tutorial.next()">Weiter</button>';
|
||||
} else if (index === total - 1) {
|
||||
navHtml += '<button class="tutorial-btn tutorial-btn-next" onclick="Tutorial.stop()">Fertig</button>';
|
||||
} else {
|
||||
navHtml += '<span></span>';
|
||||
}
|
||||
navHtml += '</div>';
|
||||
|
||||
// Close-Button
|
||||
var closeHtml = '<button class="tutorial-bubble-close" onclick="Tutorial.stop()" title="Rundgang beenden">×</button>';
|
||||
|
||||
bubble.innerHTML = closeHtml + counterHtml + titleHtml + textHtml + dotsHtml + navHtml;
|
||||
|
||||
// Positionierung
|
||||
this._positionBubble(step);
|
||||
|
||||
// Sichtbar machen
|
||||
bubble.classList.add('visible');
|
||||
},
|
||||
|
||||
_positionBubble(step) {
|
||||
var bubble = this._els.bubble;
|
||||
var bw = 340;
|
||||
|
||||
// Zentriert (Welcome / End)
|
||||
if (step.position === 'center' || !step.target) {
|
||||
bubble.className = 'tutorial-bubble visible tutorial-pos-center';
|
||||
bubble.style.top = '50%';
|
||||
bubble.style.left = '50%';
|
||||
bubble.style.transform = 'translate(-50%, -50%)';
|
||||
return;
|
||||
}
|
||||
|
||||
var el = document.querySelector(step.target);
|
||||
if (!el) {
|
||||
bubble.className = 'tutorial-bubble visible tutorial-pos-center';
|
||||
bubble.style.top = '50%';
|
||||
bubble.style.left = '50%';
|
||||
bubble.style.transform = 'translate(-50%, -50%)';
|
||||
return;
|
||||
}
|
||||
|
||||
var rect = el.getBoundingClientRect();
|
||||
var vw = window.innerWidth;
|
||||
var vh = window.innerHeight;
|
||||
var gap = 16;
|
||||
|
||||
// Reset transform
|
||||
bubble.style.transform = '';
|
||||
|
||||
// Automatische Positionswahl falls nicht genug Platz
|
||||
var pos = step.position || 'bottom';
|
||||
var bubbleHeight = 250; // Schaetzung
|
||||
|
||||
if (pos === 'bottom' && (rect.bottom + gap + bubbleHeight > vh)) pos = 'top';
|
||||
if (pos === 'top' && (rect.top - gap - bubbleHeight < 0)) pos = 'bottom';
|
||||
if (pos === 'right' && (rect.right + gap + bw > vw)) pos = 'left';
|
||||
if (pos === 'left' && (rect.left - gap - bw < 0)) pos = 'right';
|
||||
|
||||
bubble.className = 'tutorial-bubble visible tutorial-pos-' + pos;
|
||||
|
||||
switch (pos) {
|
||||
case 'bottom':
|
||||
bubble.style.top = (rect.bottom + gap) + 'px';
|
||||
bubble.style.left = Math.max(8, Math.min(rect.left + rect.width / 2 - bw / 2, vw - bw - 8)) + 'px';
|
||||
break;
|
||||
case 'top':
|
||||
bubble.style.top = (rect.top - gap) + 'px';
|
||||
bubble.style.left = Math.max(8, Math.min(rect.left + rect.width / 2 - bw / 2, vw - bw - 8)) + 'px';
|
||||
bubble.style.transform = 'translateY(-100%)';
|
||||
break;
|
||||
case 'right':
|
||||
bubble.style.top = Math.max(8, rect.top + rect.height / 2 - bubbleHeight / 2) + 'px';
|
||||
bubble.style.left = (rect.right + gap) + 'px';
|
||||
break;
|
||||
case 'left':
|
||||
bubble.style.top = Math.max(8, rect.top + rect.height / 2 - bubbleHeight / 2) + 'px';
|
||||
bubble.style.left = (rect.left - gap - bw) + 'px';
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Cursor (virtuelle Maus)
|
||||
// -----------------------------------------------------------------------
|
||||
_showCursor(x, y, type) {
|
||||
var c = this._els.cursor;
|
||||
c.className = 'tutorial-cursor';
|
||||
if (type) c.classList.add('tutorial-cursor-' + type);
|
||||
c.style.left = x + 'px';
|
||||
c.style.top = y + 'px';
|
||||
c.classList.add('visible');
|
||||
},
|
||||
|
||||
_hideCursor() {
|
||||
this._els.cursor.classList.remove('visible');
|
||||
},
|
||||
|
||||
_animateCursor(fromX, fromY, toX, toY, ms) {
|
||||
var self = this;
|
||||
return new Promise(function(resolve) {
|
||||
var c = self._els.cursor;
|
||||
var start = null;
|
||||
|
||||
function easeInOutCubic(t) {
|
||||
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
||||
}
|
||||
|
||||
function frame(timestamp) {
|
||||
if (!self._isActive) { resolve(); return; }
|
||||
if (!start) start = timestamp;
|
||||
var elapsed = timestamp - start;
|
||||
var progress = Math.min(elapsed / ms, 1);
|
||||
var eased = easeInOutCubic(progress);
|
||||
|
||||
var cx = fromX + (toX - fromX) * eased;
|
||||
var cy = fromY + (toY - fromY) * eased;
|
||||
c.style.left = cx + 'px';
|
||||
c.style.top = cy + 'px';
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(frame);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(frame);
|
||||
});
|
||||
},
|
||||
|
||||
_wait(ms) {
|
||||
var self = this;
|
||||
return new Promise(function(resolve) {
|
||||
setTimeout(function() { resolve(); }, ms);
|
||||
});
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Drag-Demo (Step 11)
|
||||
// -----------------------------------------------------------------------
|
||||
async _simulateDrag() {
|
||||
this._demoRunning = true;
|
||||
var el = document.querySelector('[gs-id="lagebild"] .card-header');
|
||||
if (!el) { this._demoRunning = false; this.next(); return; }
|
||||
|
||||
var rect = el.getBoundingClientRect();
|
||||
var startX = rect.left + rect.width / 2;
|
||||
var startY = rect.top + rect.height / 2;
|
||||
var endX = startX + 200;
|
||||
var endY = startY;
|
||||
|
||||
// 1. Cursor erscheint
|
||||
this._showCursor(startX, startY, 'default');
|
||||
await this._wait(500);
|
||||
|
||||
// 2. Grabbing-State
|
||||
this._els.cursor.classList.remove('tutorial-cursor-default');
|
||||
this._els.cursor.classList.add('tutorial-cursor-grabbing');
|
||||
await this._wait(300);
|
||||
|
||||
// 3. Nach rechts gleiten
|
||||
await this._animateCursor(startX, startY, endX, endY, 1500);
|
||||
await this._wait(200);
|
||||
|
||||
// 4. Release + zurück
|
||||
this._els.cursor.classList.remove('tutorial-cursor-grabbing');
|
||||
this._els.cursor.classList.add('tutorial-cursor-default');
|
||||
await this._animateCursor(endX, endY, startX, startY, 800);
|
||||
await this._wait(300);
|
||||
|
||||
// 5. Cursor verschwindet
|
||||
this._hideCursor();
|
||||
this._demoRunning = false;
|
||||
this._enableNavAfterDemo();
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Resize-Demo (Step 12)
|
||||
// -----------------------------------------------------------------------
|
||||
async _simulateResize() {
|
||||
this._demoRunning = true;
|
||||
var el = document.querySelector('[gs-id="faktencheck"]');
|
||||
if (!el) { this._demoRunning = false; this.next(); return; }
|
||||
|
||||
var rect = el.getBoundingClientRect();
|
||||
var startX = rect.right - 4;
|
||||
var startY = rect.bottom - 4;
|
||||
var endX = startX + 100;
|
||||
var endY = startY + 60;
|
||||
|
||||
// 1. Cursor an unterer rechter Ecke
|
||||
this._showCursor(startX, startY, 'resize');
|
||||
await this._wait(500);
|
||||
|
||||
// 2. Diagonal ziehen
|
||||
await this._animateCursor(startX, startY, endX, endY, 1200);
|
||||
await this._wait(200);
|
||||
|
||||
// 3. Zurück
|
||||
await this._animateCursor(endX, endY, startX, startY, 800);
|
||||
await this._wait(300);
|
||||
|
||||
// 4. Cursor verschwindet
|
||||
this._hideCursor();
|
||||
this._demoRunning = false;
|
||||
this._enableNavAfterDemo();
|
||||
},
|
||||
|
||||
_enableNavAfterDemo() {
|
||||
// Nav-Buttons nachträglich einblenden
|
||||
var bubble = this._els.bubble;
|
||||
var nav = bubble.querySelector('.tutorial-bubble-nav');
|
||||
if (!nav) return;
|
||||
var step = this._steps[this._currentStep];
|
||||
var index = this._currentStep;
|
||||
var total = this._steps.length;
|
||||
|
||||
var navHtml = '';
|
||||
if (index > 0) {
|
||||
navHtml += '<button class="tutorial-btn tutorial-btn-back" onclick="Tutorial.prev()">Zurück</button>';
|
||||
} else {
|
||||
navHtml += '<span></span>';
|
||||
}
|
||||
if (index < total - 1) {
|
||||
navHtml += '<button class="tutorial-btn tutorial-btn-next" onclick="Tutorial.next()">Weiter</button>';
|
||||
} else {
|
||||
navHtml += '<button class="tutorial-btn tutorial-btn-next" onclick="Tutorial.stop()">Fertig</button>';
|
||||
}
|
||||
nav.innerHTML = navHtml;
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Keyboard
|
||||
// -----------------------------------------------------------------------
|
||||
_onKey(e) {
|
||||
if (!this._isActive) return;
|
||||
switch (e.key) {
|
||||
case 'Escape':
|
||||
e.preventDefault();
|
||||
this.stop();
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
case 'Enter':
|
||||
e.preventDefault();
|
||||
this.next();
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
e.preventDefault();
|
||||
this.prev();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Resize
|
||||
// -----------------------------------------------------------------------
|
||||
_onResize() {
|
||||
if (!this._isActive) return;
|
||||
clearTimeout(this._resizeTimer);
|
||||
var self = this;
|
||||
this._resizeTimer = setTimeout(function() {
|
||||
if (!self._isActive || self._currentStep < 0) return;
|
||||
var step = self._steps[self._currentStep];
|
||||
if (step.target && step.position !== 'center') {
|
||||
self._spotlightElement(step.target);
|
||||
}
|
||||
self._positionBubble(step);
|
||||
}, 150);
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Persistenz
|
||||
// -----------------------------------------------------------------------
|
||||
_markSeen() {
|
||||
try { localStorage.setItem('osint_tutorial_seen', '1'); } catch(e) {}
|
||||
},
|
||||
|
||||
_hasSeen() {
|
||||
try { return localStorage.getItem('osint_tutorial_seen') === '1'; } catch(e) { return false; }
|
||||
},
|
||||
};
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren