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:
@@ -4943,3 +4943,247 @@ a.map-popup-article:hover {
|
|||||||
position: relative;
|
position: relative;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
Tutorial System
|
||||||
|
================================================================ */
|
||||||
|
|
||||||
|
/* Overlay (Hintergrund-Abdunkelung) */
|
||||||
|
.tutorial-overlay {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 9000;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.tutorial-overlay.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Spotlight */
|
||||||
|
.tutorial-spotlight {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 9001;
|
||||||
|
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.65);
|
||||||
|
border: 2px solid var(--accent);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
transition: top 0.4s ease, left 0.4s ease, width 0.4s ease, height 0.4s ease, opacity 0.3s ease;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Target-Element klickbar machen */
|
||||||
|
.tutorial-overlay.active ~ * [data-tutorial-target] {
|
||||||
|
position: relative;
|
||||||
|
z-index: 9002;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bubble (Sprechblase) */
|
||||||
|
.tutorial-bubble {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 9003;
|
||||||
|
width: 340px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-lg), 0 0 20px rgba(150, 121, 26, 0.15);
|
||||||
|
padding: var(--sp-xl);
|
||||||
|
pointer-events: auto;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease, top 0.4s ease, left 0.4s ease, transform 0.4s ease;
|
||||||
|
font-family: var(--font-body);
|
||||||
|
}
|
||||||
|
.tutorial-bubble.visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bubble-Pfeil */
|
||||||
|
.tutorial-bubble::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-pos-bottom::before {
|
||||||
|
top: -7px;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -6px;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.tutorial-pos-top::before {
|
||||||
|
bottom: -7px;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -6px;
|
||||||
|
border-left: none;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
.tutorial-pos-right::before {
|
||||||
|
left: -7px;
|
||||||
|
top: 30px;
|
||||||
|
border-top: none;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
.tutorial-pos-left::before {
|
||||||
|
right: -7px;
|
||||||
|
top: 30px;
|
||||||
|
border-bottom: none;
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
.tutorial-pos-center::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bubble-Inhalt */
|
||||||
|
.tutorial-bubble-counter {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: var(--sp-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-bubble-title {
|
||||||
|
font-family: var(--font-title);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: var(--sp-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-bubble-text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: var(--sp-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Close-Button */
|
||||||
|
.tutorial-bubble-close {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--sp-md);
|
||||||
|
right: var(--sp-md);
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
transition: color 0.15s, background 0.15s;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.tutorial-bubble-close:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fortschrittspunkte */
|
||||||
|
.tutorial-bubble-dots {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: var(--sp-lg);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.tutorial-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--border);
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.tutorial-dot.active {
|
||||||
|
background: var(--accent);
|
||||||
|
width: 18px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.tutorial-dot.done {
|
||||||
|
background: var(--accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nav-Buttons */
|
||||||
|
.tutorial-bubble-nav {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-btn {
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: var(--sp-md) var(--sp-xl);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s, color 0.15s;
|
||||||
|
font-family: var(--font-body);
|
||||||
|
}
|
||||||
|
.tutorial-btn-back {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
.tutorial-btn-back:hover {
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
.tutorial-btn-next {
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.tutorial-btn-next:hover {
|
||||||
|
background: var(--accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Virtueller Cursor */
|
||||||
|
.tutorial-cursor {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 9500;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
.tutorial-cursor.visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.tutorial-cursor-default {
|
||||||
|
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M5 3l14 8-6 2 4 8-3 1-4-8-5 4z' fill='%23fff' stroke='%23000' stroke-width='1'/%3E%3C/svg%3E") no-repeat center/contain;
|
||||||
|
}
|
||||||
|
.tutorial-cursor-grabbing {
|
||||||
|
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M8 10V8a1 1 0 112 0v2h1V7a1 1 0 112 0v3h1V8a1 1 0 112 0v2h.5a1.5 1.5 0 011.5 1.5V16a5 5 0 01-5 5h-2a5 5 0 01-5-5v-3.5A1.5 1.5 0 017.5 11H8z' fill='%23fff' stroke='%23000' stroke-width='0.8'/%3E%3C/svg%3E") no-repeat center/contain;
|
||||||
|
}
|
||||||
|
.tutorial-cursor-resize {
|
||||||
|
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M22 22H20V20H22V22ZM22 18H18V22H16V16H22V18ZM18 18V14H22V12H16V18H18ZM14 22H12V16H18V14H10V22H14Z' fill='%23fff' stroke='%23000' stroke-width='0.3'/%3E%3C/svg%3E") no-repeat center/contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chat Tutorial-Hinweis */
|
||||||
|
.chat-tutorial-hint {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: var(--sp-lg);
|
||||||
|
margin: var(--sp-md) var(--sp-md) 0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.chat-tutorial-hint:hover {
|
||||||
|
background: var(--tint-accent-subtle);
|
||||||
|
}
|
||||||
|
.chat-tutorial-hint strong {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<link rel="stylesheet" href="/static/vendor/MarkerCluster.css">
|
<link rel="stylesheet" href="/static/vendor/MarkerCluster.css">
|
||||||
<link rel="stylesheet" href="/static/vendor/MarkerCluster.Default.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/network.css?v=20260316a">
|
||||||
<link rel="stylesheet" href="/static/css/style.css?v=20260316b">
|
<link rel="stylesheet" href="/static/css/style.css?v=20260316c">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<a href="#main-content" class="skip-link">Zum Hauptinhalt springen</a>
|
<a href="#main-content" class="skip-link">Zum Hauptinhalt springen</a>
|
||||||
@@ -742,6 +742,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Tutorial -->
|
||||||
|
<div class="tutorial-overlay" id="tutorial-overlay">
|
||||||
|
<div class="tutorial-spotlight" id="tutorial-spotlight"></div>
|
||||||
|
</div>
|
||||||
|
<div class="tutorial-bubble" id="tutorial-bubble"></div>
|
||||||
|
<div class="tutorial-cursor" id="tutorial-cursor"></div>
|
||||||
|
|
||||||
<!-- Toast Container -->
|
<!-- Toast Container -->
|
||||||
<div class="toast-container" id="toast-container" aria-live="polite" aria-atomic="true"></div>
|
<div class="toast-container" id="toast-container" aria-live="polite" aria-atomic="true"></div>
|
||||||
|
|
||||||
@@ -757,8 +764,9 @@
|
|||||||
<script src="/static/js/api_network.js?v=20260316a"></script>
|
<script src="/static/js/api_network.js?v=20260316a"></script>
|
||||||
<script src="/static/js/network-graph.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/app_network.js?v=20260316a"></script>
|
||||||
<script src="/static/js/chat.js?v=20260315b"></script>
|
<script src="/static/js/tutorial.js?v=20260316a"></script>
|
||||||
<script>document.addEventListener("DOMContentLoaded",function(){Chat.init()});</script>
|
<script src="/static/js/chat.js?v=20260316c"></script>
|
||||||
|
<script>document.addEventListener("DOMContentLoaded",function(){Chat.init();Tutorial.init()});</script>
|
||||||
|
|
||||||
<!-- Map Fullscreen Overlay -->
|
<!-- Map Fullscreen Overlay -->
|
||||||
<div class="map-fullscreen-overlay" id="map-fullscreen-overlay">
|
<div class="map-fullscreen-overlay" id="map-fullscreen-overlay">
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const Chat = {
|
|||||||
this.send();
|
this.send();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Enter sendet, Shift+Enter fuer Zeilenumbruch
|
// Enter sendet, Shift+Enter für Zeilenumbruch
|
||||||
input.addEventListener('keydown', (e) => {
|
input.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -63,7 +63,11 @@ const Chat = {
|
|||||||
|
|
||||||
if (!this._hasGreeted) {
|
if (!this._hasGreeted) {
|
||||||
this._hasGreeted = true;
|
this._hasGreeted = true;
|
||||||
this.addMessage('assistant', 'Hallo! Ich bin der AegisSight Assistent und helfe dir bei der Bedienung des Monitors.\n\nFrag mich zum Beispiel:\n\n"Wie erstelle ich eine neue Lage?"\n"Was bedeuten die Faktencheck-Status?"\n"Wie funktioniert der automatische Refresh?"\n"Wie nutze ich die Kartenansicht?"\n"Wie exportiere ich einen Lagebericht?"\n\nFuer alle weiteren Anliegen erreichst du den Support unter support@aegis-sight.de.');
|
this.addMessage('assistant', 'Hallo! Ich bin der AegisSight Assistent und helfe dir bei der Bedienung des Monitors.\n\nFrag mich zum Beispiel:\n\n"Wie erstelle ich eine neue Lage?"\n"Was bedeuten die Faktencheck-Status?"\n"Wie funktioniert der automatische Refresh?"\n"Wie nutze ich die Kartenansicht?"\n"Wie exportiere ich einen Lagebericht?"\n\nFür alle weiteren Anliegen erreichst du den Support unter support@aegis-sight.de.');
|
||||||
|
// Tutorial-Hinweis beim ersten Oeffnen
|
||||||
|
if (typeof Tutorial !== "undefined" && !Tutorial._hasSeen()) {
|
||||||
|
this._showTutorialHint();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Focus auf Input
|
// Focus auf Input
|
||||||
@@ -129,6 +133,16 @@ const Chat = {
|
|||||||
this._showTyping();
|
this._showTyping();
|
||||||
this._isLoading = true;
|
this._isLoading = true;
|
||||||
|
|
||||||
|
// Tutorial-Keywords abfangen
|
||||||
|
var lowerText = text.toLowerCase();
|
||||||
|
if (lowerText === 'rundgang' || lowerText === 'tutorial' || lowerText === 'tour' || lowerText === 'f\u00fchrung') {
|
||||||
|
this._hideTyping();
|
||||||
|
this._isLoading = false;
|
||||||
|
this.close();
|
||||||
|
if (typeof Tutorial !== 'undefined') Tutorial.start();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = {
|
const body = {
|
||||||
message: text,
|
message: text,
|
||||||
@@ -274,4 +288,17 @@ const Chat = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_showTutorialHint() {
|
||||||
|
var container = document.getElementById('chat-messages');
|
||||||
|
if (!container) return;
|
||||||
|
var hint = document.createElement('div');
|
||||||
|
hint.className = 'chat-tutorial-hint';
|
||||||
|
hint.innerHTML = '<strong>Tipp:</strong> Kennen Sie schon den interaktiven Rundgang? Er zeigt Ihnen Schritt für Schritt alle Funktionen des Monitors. Klicken Sie hier, um den Rundgang zu starten.';
|
||||||
|
hint.addEventListener('click', function() {
|
||||||
|
Chat.close();
|
||||||
|
if (typeof Tutorial !== 'undefined') Tutorial.start();
|
||||||
|
});
|
||||||
|
container.appendChild(hint);
|
||||||
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
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