Bei Steps die auf ein Modal zeigen (#modal-new, #modal-sources) wird der Spotlight-Overlay nicht angezeigt. Das Modal hat bereits einen eigenen Abdunkelungs-Hintergrund, der zusaetzliche Spotlight-Shadow verdunkelte das Formular unlesbar. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2052 Zeilen
91 KiB
JavaScript
2052 Zeilen
91 KiB
JavaScript
/**
|
|
* AegisSight Tutorial - Interaktiver Rundgang durch den Monitor.
|
|
* Erzeugt eine Demo-Lage mit Platzhalter-Inhalten, sodass der Rundgang
|
|
* auch ohne bestehende Lagen funktioniert.
|
|
*/
|
|
const Tutorial = {
|
|
_steps: [],
|
|
_currentStep: -1,
|
|
_isActive: false,
|
|
_resizeTimer: null,
|
|
_keyHandler: null,
|
|
_resizeHandler: null,
|
|
_demoRunning: false,
|
|
_stepTimers: [], // setTimeout-IDs fuer den aktuellen Step
|
|
_savedState: null, // Dashboard-Zustand vor dem Tutorial
|
|
|
|
// 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');
|
|
},
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Demo-Daten
|
|
// -----------------------------------------------------------------------
|
|
_DEMO_SIDEBAR_ITEM: '<div class="incident-item active tutorial-demo" data-id="demo">'
|
|
+ '<span class="incident-dot active" aria-hidden="true"></span>'
|
|
+ '<div style="flex:1;min-width:0;">'
|
|
+ '<div class="incident-name">Explosion in Hamburger Hafen</div>'
|
|
+ '<div class="incident-meta">14 Artikel · demo-user</div>'
|
|
+ '</div>'
|
|
+ '<span class="badge badge-auto" role="img" aria-label="Auto-Refresh aktiv">↻</span>'
|
|
+ '</div>',
|
|
|
|
_DEMO_SIDEBAR_RESEARCH: '<div class="incident-item tutorial-demo" data-id="demo-research">'
|
|
+ '<span class="incident-dot active" aria-hidden="true"></span>'
|
|
+ '<div style="flex:1;min-width:0;">'
|
|
+ '<div class="incident-name">Analyse: Cyberangriffe auf Kritische Infrastruktur 2026</div>'
|
|
+ '<div class="incident-meta">8 Artikel · demo-user</div>'
|
|
+ '</div>'
|
|
+ '</div>',
|
|
|
|
_DEMO_SUMMARY: '<p>Am 16. März 2026 kam es im Hamburger Hafen zu einer Explosion '
|
|
+ 'im Containerterminal Burchardkai '
|
|
+ '<span class="source-ref" data-index="1" title="dpa">[1]</span> '
|
|
+ '<span class="source-ref" data-index="2" title="Reuters">[2]</span>. '
|
|
+ 'Die Detonation ereignete sich gegen 06:45 Uhr in einem Gefahrgutbereich. '
|
|
+ 'Mindestens 12 Personen wurden verletzt '
|
|
+ '<span class="source-ref" data-index="3" title="NDR">[3]</span>.</p>'
|
|
+ '<p>Die Feuerwehr ist mit einem Großaufgebot vor Ort. Der Hafenbetrieb im betroffenen Terminal wurde '
|
|
+ 'vorübergehend eingestellt '
|
|
+ '<span class="source-ref" data-index="4" title="HPA">[4]</span>. '
|
|
+ 'Die Ursache ist noch unklar '
|
|
+ '<span class="source-ref" data-index="5" title="Hamburger Abendblatt">[5]</span>.</p>',
|
|
|
|
_DEMO_FACTCHECKS: [
|
|
{ status: 'confirmed', icon: '✓', claim: 'Eine Explosion ereignete sich am 16.03.2026 gegen 06:45 Uhr im Hamburger Hafen.', sources: 5 },
|
|
{ status: 'confirmed', icon: '✓', claim: 'Mindestens 12 Personen wurden bei dem Vorfall verletzt.', sources: 3 },
|
|
{ status: 'established', icon: '✓', claim: 'Die Explosion fand im Bereich des Burchardkai-Terminals statt.', sources: 4 },
|
|
{ status: 'unconfirmed', icon: '?', claim: 'Es soll sich um eine Fehlfunktion in einem Gefahrgutcontainer handeln.', sources: 1 },
|
|
{ status: 'disputed', icon: '⚠', claim: 'Die Rauchwolke soll gesundheitsgefährdende Stoffe enthalten.', sources: 2 },
|
|
{ status: 'contradicted', icon: '✗', claim: 'Der gesamte Hamburger Hafen wurde geschlossen.', sources: 2 },
|
|
],
|
|
|
|
_DEMO_SOURCES_STATS: '5 Kategorien · 14 Quellen · 14 Artikel',
|
|
|
|
_DEMO_TIMELINE: [
|
|
{ time: '06:45', title: 'Explosion im Containerterminal Burchardkai', source: 'dpa', type: 'article' },
|
|
{ time: '07:02', title: 'Hamburger Feuerwehr rückt mit Großaufgebot aus', source: 'NDR', type: 'article' },
|
|
{ time: '07:15', title: 'Polizei sperrt Hafengebiet weiträumig ab', source: 'Hamburger Abendblatt', type: 'article' },
|
|
{ time: '07:30', title: 'Erste Meldungen über Verletzte', source: 'Reuters', type: 'article' },
|
|
{ time: '08:00', title: 'Hamburg Port Authority stoppt Betrieb im Terminal', source: 'HPA', type: 'article' },
|
|
{ time: '08:22', title: 'Lagebericht: Erster Überblick zum Vorfall', source: 'AegisSight', type: 'snapshot' },
|
|
{ time: '09:10', title: 'Gefahrgut-Spezialisten am Einsatzort eingetroffen', source: 'tagesschau.de', type: 'article' },
|
|
],
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Demo-View injizieren / entfernen
|
|
// -----------------------------------------------------------------------
|
|
_injectDemoView() {
|
|
// Zustand sichern
|
|
var incidentView = document.getElementById('incident-view');
|
|
var emptyState = document.getElementById('empty-state');
|
|
this._savedState = {
|
|
incidentViewDisplay: incidentView ? incidentView.style.display : 'none',
|
|
emptyStateDisplay: emptyState ? emptyState.style.display : '',
|
|
incidentTitle: document.getElementById('incident-title') ? document.getElementById('incident-title').textContent : '',
|
|
summaryText: document.getElementById('summary-text') ? document.getElementById('summary-text').innerHTML : '',
|
|
factcheckList: document.getElementById('factcheck-list') ? document.getElementById('factcheck-list').innerHTML : '',
|
|
sourceStats: document.getElementById('source-overview-header-stats') ? document.getElementById('source-overview-header-stats').innerHTML : '',
|
|
timeline: document.getElementById('timeline') ? document.getElementById('timeline').innerHTML : '',
|
|
mapEmpty: document.getElementById('map-empty') ? document.getElementById('map-empty').style.display : '',
|
|
layoutToolbar: document.getElementById('layout-toolbar') ? document.getElementById('layout-toolbar').style.display : 'none',
|
|
typeBadge: document.getElementById('incident-type-badge') ? document.getElementById('incident-type-badge').innerHTML : '',
|
|
refreshMode: document.getElementById('meta-refresh-mode') ? document.getElementById('meta-refresh-mode').innerHTML : '',
|
|
headerStrip: document.getElementById('incident-header-strip') ? document.getElementById('incident-header-strip').style.display : '',
|
|
fcFilters: document.getElementById('fc-filters') ? document.getElementById('fc-filters').innerHTML : '',
|
|
mapStats: document.getElementById('map-stats') ? document.getElementById('map-stats').innerHTML : '',
|
|
sidebarItems: null,
|
|
};
|
|
|
|
// Sidebar: Demo-Einträge hinzufügen
|
|
var activeList = document.getElementById('active-incidents');
|
|
if (activeList) {
|
|
this._savedState.sidebarItems = activeList.innerHTML;
|
|
activeList.insertAdjacentHTML('afterbegin', this._DEMO_SIDEBAR_ITEM);
|
|
}
|
|
|
|
// Empty-State verstecken, Incident-View anzeigen
|
|
if (emptyState) emptyState.style.display = 'none';
|
|
if (incidentView) incidentView.style.display = '';
|
|
|
|
// Header
|
|
var title = document.getElementById('incident-title');
|
|
if (title) title.textContent = 'Explosion in Hamburger Hafen';
|
|
var badge = document.getElementById('incident-type-badge');
|
|
if (badge) { badge.textContent = 'Live-Monitoring'; badge.className = 'incident-type-badge type-adhoc'; }
|
|
var refreshMode = document.getElementById('meta-refresh-mode');
|
|
if (refreshMode) refreshMode.innerHTML = '<span class="auto-badge">Auto-Refresh: 15 Min</span>';
|
|
var creator = document.getElementById('incident-creator');
|
|
if (creator) creator.textContent = 'demo-user';
|
|
var desc = document.getElementById('incident-description');
|
|
if (desc) desc.textContent = 'Schwere Explosion im Hamburger Hafengebiet, Burchardkai-Terminal';
|
|
|
|
// Layout-Toolbar
|
|
var toolbar = document.getElementById('layout-toolbar');
|
|
if (toolbar) toolbar.style.display = '';
|
|
|
|
// Lagebild
|
|
var summaryText = document.getElementById('summary-text');
|
|
if (summaryText) summaryText.innerHTML = this._DEMO_SUMMARY;
|
|
|
|
// Timestamp
|
|
var ts = document.getElementById('lagebild-timestamp');
|
|
if (ts) ts.textContent = '16.03.2026, 08:22';
|
|
|
|
// Faktencheck
|
|
var fcList = document.getElementById('factcheck-list');
|
|
if (fcList) {
|
|
var fcHtml = '';
|
|
this._DEMO_FACTCHECKS.forEach(function(fc) {
|
|
fcHtml += '<div class="factcheck-item" data-fc-status="' + fc.status + '">'
|
|
+ '<div class="factcheck-icon ' + fc.status + '" aria-hidden="true">' + fc.icon + '</div>'
|
|
+ '<div style="flex:1;">'
|
|
+ '<div class="factcheck-claim">' + fc.claim + '</div>'
|
|
+ '<div style="display:flex;align-items:center;gap:6px;margin-top:2px;">'
|
|
+ '<span class="factcheck-sources">' + fc.sources + ' Quelle' + (fc.sources !== 1 ? 'n' : '') + '</span>'
|
|
+ '</div></div></div>';
|
|
});
|
|
fcList.innerHTML = fcHtml;
|
|
}
|
|
|
|
// Faktencheck-Filter
|
|
var fcFilters = document.getElementById('fc-filters');
|
|
if (fcFilters) {
|
|
fcFilters.innerHTML = '<button class="fc-dropdown-toggle" style="pointer-events:none;">Filter</button>';
|
|
}
|
|
|
|
// Quellenübersicht
|
|
var sourceStats = document.getElementById('source-overview-header-stats');
|
|
if (sourceStats) sourceStats.innerHTML = this._DEMO_SOURCES_STATS;
|
|
|
|
// Timeline
|
|
var timeline = document.getElementById('timeline');
|
|
if (timeline) {
|
|
var tlHtml = '<div class="ht-timeline">';
|
|
this._DEMO_TIMELINE.forEach(function(ev) {
|
|
var cls = ev.type === 'snapshot' ? 'ht-entry ht-snapshot' : 'ht-entry';
|
|
tlHtml += '<div class="' + cls + '">'
|
|
+ '<div class="ht-time">' + ev.time + '</div>'
|
|
+ '<div class="ht-dot"></div>'
|
|
+ '<div class="ht-content">'
|
|
+ '<div class="ht-title">' + ev.title + '</div>'
|
|
+ '<div class="ht-source">' + ev.source + '</div>'
|
|
+ '</div></div>';
|
|
});
|
|
tlHtml += '</div>';
|
|
timeline.innerHTML = tlHtml;
|
|
}
|
|
|
|
// Karte: Stats setzen, Map wird erst im Vollbild-Step initialisiert
|
|
var mapEmpty = document.getElementById('map-empty');
|
|
if (mapEmpty) mapEmpty.style.display = 'none';
|
|
var mapStats = document.getElementById('map-stats');
|
|
if (mapStats) mapStats.textContent = '3 Orte / 9 Artikel';
|
|
|
|
// Meta
|
|
var metaUpdated = document.getElementById('meta-updated');
|
|
if (metaUpdated) metaUpdated.textContent = 'Aktualisiert: 16.03.2026, 09:10';
|
|
},
|
|
|
|
_removeDemoView() {
|
|
if (!this._savedState) return;
|
|
var s = this._savedState;
|
|
|
|
// Sidebar Demo-Einträge entfernen
|
|
document.querySelectorAll('.tutorial-demo').forEach(function(el) { el.remove(); });
|
|
|
|
// Sidebar wiederherstellen
|
|
var activeList = document.getElementById('active-incidents');
|
|
if (activeList && s.sidebarItems !== null) activeList.innerHTML = s.sidebarItems;
|
|
|
|
// Views wiederherstellen
|
|
var incidentView = document.getElementById('incident-view');
|
|
if (incidentView) incidentView.style.display = s.incidentViewDisplay;
|
|
var emptyState = document.getElementById('empty-state');
|
|
if (emptyState) emptyState.style.display = s.emptyStateDisplay;
|
|
|
|
// Inhalte wiederherstellen
|
|
var title = document.getElementById('incident-title');
|
|
if (title) title.textContent = s.incidentTitle;
|
|
var summaryText = document.getElementById('summary-text');
|
|
if (summaryText) summaryText.innerHTML = s.summaryText;
|
|
var fcList = document.getElementById('factcheck-list');
|
|
if (fcList) fcList.innerHTML = s.factcheckList;
|
|
var sourceStats = document.getElementById('source-overview-header-stats');
|
|
if (sourceStats) sourceStats.innerHTML = s.sourceStats;
|
|
var timeline = document.getElementById('timeline');
|
|
if (timeline) timeline.innerHTML = s.timeline;
|
|
var mapEmpty = document.getElementById('map-empty');
|
|
if (mapEmpty) mapEmpty.style.display = s.mapEmpty;
|
|
var toolbar = document.getElementById('layout-toolbar');
|
|
if (toolbar) toolbar.style.display = s.layoutToolbar;
|
|
var badge = document.getElementById('incident-type-badge');
|
|
if (badge) badge.innerHTML = s.typeBadge;
|
|
var refreshMode = document.getElementById('meta-refresh-mode');
|
|
if (refreshMode) refreshMode.innerHTML = s.refreshMode;
|
|
var fcFilters = document.getElementById('fc-filters');
|
|
if (fcFilters) fcFilters.innerHTML = s.fcFilters;
|
|
var mapStats = document.getElementById('map-stats');
|
|
if (mapStats) mapStats.innerHTML = s.mapStats;
|
|
|
|
// Demo-Map entfernen
|
|
this._destroyDemoMap();
|
|
|
|
// Meta
|
|
var metaUpdated = document.getElementById('meta-updated');
|
|
if (metaUpdated) metaUpdated.textContent = '';
|
|
var creator = document.getElementById('incident-creator');
|
|
if (creator) creator.textContent = '';
|
|
var desc = document.getElementById('incident-description');
|
|
if (desc) desc.textContent = '';
|
|
var ts = document.getElementById('lagebild-timestamp');
|
|
if (ts) ts.textContent = '';
|
|
|
|
this._savedState = null;
|
|
},
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Demo-Karte mit Leaflet
|
|
// -----------------------------------------------------------------------
|
|
_demoMap: null,
|
|
_demoMapMarkers: [],
|
|
|
|
_initDemoMap() {
|
|
if (typeof L === 'undefined') return;
|
|
var container = document.getElementById('map-container');
|
|
if (!container) return;
|
|
|
|
// Container-Höhe sicherstellen
|
|
var gsItem = container.closest('.grid-stack-item');
|
|
if (gsItem) {
|
|
var headerEl = container.closest('.map-card');
|
|
var hdr = headerEl ? headerEl.querySelector('.card-header') : null;
|
|
var headerH = hdr ? hdr.offsetHeight : 40;
|
|
var available = gsItem.offsetHeight - headerH - 4;
|
|
container.style.height = Math.max(available, 200) + 'px';
|
|
} else if (container.offsetHeight < 50) {
|
|
container.style.height = '300px';
|
|
}
|
|
|
|
// Falls UI._map existiert, vorher sichern
|
|
if (typeof UI !== 'undefined' && UI._map) {
|
|
this._savedUIMap = true;
|
|
}
|
|
|
|
this._demoMap = L.map(container, {
|
|
zoomControl: true,
|
|
attributionControl: true,
|
|
}).setView([53.545, 9.98], 13);
|
|
|
|
// Tile-Layer (Theme-abhängig)
|
|
var isDark = !document.documentElement.getAttribute('data-theme') || document.documentElement.getAttribute('data-theme') !== 'light';
|
|
if (isDark) {
|
|
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
|
attribution: '\u00a9 OpenStreetMap, \u00a9 CARTO',
|
|
maxZoom: 19,
|
|
}).addTo(this._demoMap);
|
|
} else {
|
|
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
|
|
attribution: '\u00a9 OpenStreetMap, \u00a9 CARTO',
|
|
maxZoom: 19,
|
|
}).addTo(this._demoMap);
|
|
}
|
|
|
|
// Demo-Marker
|
|
var locations = [
|
|
{ lat: 53.5325, lon: 9.9275, name: 'Burchardkai Terminal', articles: 6, cat: 'primary' },
|
|
{ lat: 53.5460, lon: 9.9690, name: 'Hamburg Innenstadt', articles: 2, cat: 'secondary' },
|
|
{ lat: 53.5380, lon: 9.9400, name: 'Elbe / Hafengebiet', articles: 1, cat: 'tertiary' },
|
|
];
|
|
|
|
var catColors = { primary: '#EF4444', secondary: '#F59E0B', tertiary: '#3B82F6' };
|
|
var catLabels = { primary: 'Hauptereignisort', secondary: 'Erw\u00e4hnt', tertiary: 'Kontext' };
|
|
var self = this;
|
|
|
|
locations.forEach(function(loc) {
|
|
var color = catColors[loc.cat] || '#7b7b7b';
|
|
var icon = L.divIcon({
|
|
className: 'tutorial-map-marker',
|
|
html: '<div style="width:14px;height:14px;border-radius:50%;background:' + color
|
|
+ ';border:2px solid #fff;box-shadow:0 2px 6px rgba(0,0,0,0.4);"></div>',
|
|
iconSize: [14, 14],
|
|
iconAnchor: [7, 7],
|
|
});
|
|
var marker = L.marker([loc.lat, loc.lon], { icon: icon });
|
|
var popupHtml = '<div class="map-popup">'
|
|
+ '<div class="map-popup-title">' + loc.name + '</div>'
|
|
+ '<div style="font-size:11px;margin:4px 0;"><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:' + color + ';margin-right:4px;vertical-align:middle;"></span>' + catLabels[loc.cat] + '</div>'
|
|
+ '<div class="map-popup-count">' + loc.articles + ' Artikel</div>'
|
|
+ '</div>';
|
|
marker.bindPopup(popupHtml, { maxWidth: 250, className: 'map-popup-container' });
|
|
marker.addTo(self._demoMap);
|
|
self._demoMapMarkers.push(marker);
|
|
});
|
|
|
|
// Legende
|
|
var legend = L.control({ position: 'bottomright' });
|
|
legend.onAdd = function() {
|
|
var div = L.DomUtil.create('div', 'map-legend-ctrl');
|
|
L.DomEvent.disableClickPropagation(div);
|
|
var html = '<strong style="display:block;margin-bottom:6px;">Legende</strong>';
|
|
['primary', 'secondary', 'tertiary'].forEach(function(cat) {
|
|
html += '<div class="map-legend-item" style="display:flex;align-items:center;gap:6px;margin:3px 0;">'
|
|
+ '<span style="width:10px;height:10px;border-radius:50%;background:' + catColors[cat] + ';flex-shrink:0;"></span>'
|
|
+ '<span>' + catLabels[cat] + '</span></div>';
|
|
});
|
|
div.innerHTML = html;
|
|
return div;
|
|
};
|
|
legend.addTo(this._demoMap);
|
|
this._demoMapLegend = legend;
|
|
|
|
// Resize-Fix
|
|
var map = this._demoMap;
|
|
[100, 300, 800].forEach(function(delay) {
|
|
setTimeout(function() {
|
|
if (map) map.invalidateSize();
|
|
}, delay);
|
|
});
|
|
|
|
// Hauptereignisort-Popup nach kurzer Verz\u00f6gerung \u00f6ffnen
|
|
var mainMarker = this._demoMapMarkers[0];
|
|
if (mainMarker) {
|
|
setTimeout(function() {
|
|
if (map && mainMarker) mainMarker.openPopup();
|
|
}, 1500);
|
|
}
|
|
},
|
|
|
|
_destroyDemoMap() {
|
|
if (this._demoMap) {
|
|
this._demoMap.remove();
|
|
this._demoMap = null;
|
|
this._demoMapMarkers = [];
|
|
this._demoMapLegend = null;
|
|
}
|
|
},
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Highlight-Helfer: Einzelnes Sub-Element innerhalb einer Kachel markieren
|
|
// -----------------------------------------------------------------------
|
|
_highlightSub(selector) {
|
|
var el = document.querySelector(selector);
|
|
if (!el) return;
|
|
el.classList.add('tutorial-sub-highlight');
|
|
this._cleanupFns.push(function() {
|
|
el.classList.remove('tutorial-sub-highlight');
|
|
});
|
|
},
|
|
|
|
_clearSubHighlights() {
|
|
document.querySelectorAll('.tutorial-sub-highlight').forEach(function(el) {
|
|
el.classList.remove('tutorial-sub-highlight');
|
|
});
|
|
},
|
|
|
|
_cleanupFns: [],
|
|
|
|
// Timer-Helfer: Timeouts die beim Step-Wechsel automatisch gecancelt werden
|
|
_stepTimeout(fn, ms) {
|
|
var id = setTimeout(fn, ms);
|
|
this._stepTimers.push(id);
|
|
return id;
|
|
},
|
|
|
|
_clearStepTimers() {
|
|
this._stepTimers.forEach(function(id) { clearTimeout(id); });
|
|
this._stepTimers = [];
|
|
},
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Scroll-Helfer: Element in den sichtbaren Bereich scrollen
|
|
// -----------------------------------------------------------------------
|
|
_scrollToTarget(selector) {
|
|
return new Promise(function(resolve) {
|
|
var el = document.querySelector(selector);
|
|
if (!el) { resolve(); return; }
|
|
var rect = el.getBoundingClientRect();
|
|
var vh = window.innerHeight;
|
|
if (rect.top >= 0 && rect.bottom <= vh) {
|
|
resolve();
|
|
return;
|
|
}
|
|
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
setTimeout(resolve, 500);
|
|
});
|
|
},
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Step-Definitionen
|
|
// -----------------------------------------------------------------------
|
|
_defineSteps() {
|
|
this._steps = [
|
|
// 0 - Welcome
|
|
{
|
|
id: 'welcome',
|
|
target: '#main-content',
|
|
title: 'Willkommen im AegisSight Monitor',
|
|
text: 'Dieser interaktive Rundgang führt Sie durch alle Funktionen des OSINT-Monitors. '
|
|
+ 'Wir werden gemeinsam eine Demo-Lage anlegen und alle Bereiche des Dashboards erkunden.<br><br>'
|
|
+ 'Navigieren Sie mit den Pfeiltasten oder den Buttons. Mit Escape können Sie jederzeit abbrechen.',
|
|
position: 'center',
|
|
},
|
|
// 1 - Sidebar
|
|
{
|
|
id: 'sidebar',
|
|
target: '.sidebar',
|
|
title: 'Seitenleiste: Ihre Lagen',
|
|
text: 'Die Seitenleiste zeigt all Ihre Lagen in drei Bereichen:<br><br>'
|
|
+ '<strong>Live-Monitoring</strong> - Laufende Ereignisbeobachtung mit automatischer Aktualisierung<br>'
|
|
+ '<strong>Recherchen</strong> - Themenanalysen ohne Echtzeit-Updates<br>'
|
|
+ '<strong>Archiv</strong> - Abgeschlossene Lagen zur späteren Einsicht<br><br>'
|
|
+ 'Klicken Sie auf eine Lage, um sie im Hauptbereich zu öffnen.',
|
|
position: 'right',
|
|
},
|
|
// 2 - Neue Lage Button
|
|
{
|
|
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. '
|
|
+ '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');
|
|
}, 1500);
|
|
},
|
|
onExit: function() {
|
|
var overlay = document.getElementById('modal-new');
|
|
if (overlay) overlay.classList.remove('active');
|
|
},
|
|
},
|
|
// 3 - Titel und Beschreibung (Cursor-Demo)
|
|
{
|
|
id: 'form-title-desc',
|
|
target: '#modal-new .modal',
|
|
title: 'Titel und Beschreibung',
|
|
text: 'Geben Sie einen aussagekräftigen <strong>Titel</strong> ein, der das Ereignis klar beschreibt. '
|
|
+ 'Die <strong>Beschreibung</strong> liefert zusätzlichen Kontext für die Recherche.<br><br>'
|
|
+ 'Beobachten Sie die Eingabe:',
|
|
position: 'left',
|
|
disableNav: true,
|
|
onEnter: function() {
|
|
var overlay = document.getElementById('modal-new');
|
|
if (overlay && !overlay.classList.contains('active')) overlay.classList.add('active');
|
|
if (overlay) overlay.style.zIndex = '9002';
|
|
var modalBody = document.querySelector('#modal-new .modal-body');
|
|
if (modalBody) modalBody.scrollTop = 0;
|
|
var t = document.getElementById('inc-title'); if (t) t.value = '';
|
|
var d = document.getElementById('inc-description'); if (d) d.value = '';
|
|
Tutorial._simulateFormTitleDesc();
|
|
},
|
|
onExit: function() {
|
|
Tutorial._clearSubHighlights();
|
|
},
|
|
},
|
|
// 4 - Art der Lage (Cursor-Demo)
|
|
{
|
|
id: 'form-type',
|
|
target: '#modal-new .modal',
|
|
title: 'Art der Lage',
|
|
text: '<strong>Live-Monitoring</strong> beobachtet ein Ereignis in Echtzeit. Hunderte Quellen werden '
|
|
+ 'laufend durchsucht. Ideal für aktuelle Krisen und sich entwickelnde Lagen.<br><br>'
|
|
+ '<strong>Analyse/Recherche</strong> untersucht ein Thema tiefergehend ohne automatische Updates. '
|
|
+ 'Ideal für Hintergrundanalysen und Lageberichte.',
|
|
position: 'left',
|
|
disableNav: true,
|
|
onEnter: function() {
|
|
var overlay = document.getElementById('modal-new');
|
|
if (overlay && !overlay.classList.contains('active')) overlay.classList.add('active');
|
|
if (overlay) overlay.style.zIndex = '9002';
|
|
var modalBody = document.querySelector('#modal-new .modal-body');
|
|
if (modalBody) modalBody.scrollTo({ top: 0, behavior: 'smooth' });
|
|
Tutorial._stepTimeout(function() {
|
|
Tutorial._highlightSub('#inc-type');
|
|
Tutorial._simulateTypeSwitch();
|
|
}, 500);
|
|
},
|
|
onExit: function() {
|
|
var sel = document.getElementById('inc-type');
|
|
if (sel) { sel.value = 'adhoc'; try { sel.dispatchEvent(new Event('change')); } catch(e) {} }
|
|
Tutorial._clearSubHighlights();
|
|
},
|
|
},
|
|
// 5 - Quellen
|
|
{
|
|
id: 'form-sources',
|
|
target: '#modal-new .modal',
|
|
title: 'Quellen konfigurieren',
|
|
text: '<strong>Internationale Quellen</strong> bezieht englischsprachige und internationale Medien '
|
|
+ 'ein (Reuters, BBC, Al Jazeera etc.). Erhöht die Abdeckung, aber auch den Analyseumfang.<br><br>'
|
|
+ '<strong>Telegram-Kanäle</strong> liefern oft frühzeitige OSINT-Informationen, '
|
|
+ 'können aber auch unbestätigte Meldungen enthalten. Für sensible Lagen empfohlen.',
|
|
position: 'left',
|
|
disableNav: true,
|
|
onEnter: function() {
|
|
var overlay = document.getElementById('modal-new');
|
|
if (overlay && !overlay.classList.contains('active')) overlay.classList.add('active');
|
|
if (overlay) overlay.style.zIndex = '9002';
|
|
Tutorial._simulateFormSources();
|
|
},
|
|
onExit: function() {
|
|
Tutorial._clearSubHighlights();
|
|
},
|
|
},
|
|
// 6 - Sichtbarkeit
|
|
{
|
|
id: 'form-visibility',
|
|
target: '#modal-new .modal',
|
|
title: 'Sichtbarkeit',
|
|
text: '<strong>Öffentlich</strong> bedeutet, dass alle Nutzer Ihrer Organisation diese Lage sehen '
|
|
+ 'und darauf zugreifen können.<br><br>'
|
|
+ '<strong>Privat</strong> macht die Lage nur für Sie persönlich sichtbar. '
|
|
+ 'Nützlich für persönliche Recherchen oder sensible Vorgänge.',
|
|
position: 'left',
|
|
disableNav: true,
|
|
onEnter: function() {
|
|
var overlay = document.getElementById('modal-new');
|
|
if (overlay && !overlay.classList.contains('active')) overlay.classList.add('active');
|
|
if (overlay) overlay.style.zIndex = '9002';
|
|
Tutorial._simulateFormVisibility();
|
|
},
|
|
onExit: function() {
|
|
Tutorial._clearSubHighlights();
|
|
},
|
|
},
|
|
// 7 - Aktualisierung und Intervall
|
|
{
|
|
id: 'form-refresh',
|
|
target: '#modal-new .modal',
|
|
title: 'Aktualisierung',
|
|
text: '<strong>Manuell</strong>: Sie starten Aktualisierungen selbst per Button.<br>'
|
|
+ '<strong>Automatisch</strong>: Der Monitor aktualisiert im eingestellten Intervall.<br><br>'
|
|
+ '<strong>Wichtig:</strong> Kürzere Intervalle liefern aktuellere Daten, '
|
|
+ 'erhöhen aber den Creditverbrauch. Für die meisten Lagen sind 15 bis 30 Minuten ein guter Richtwert.',
|
|
position: 'left',
|
|
disableNav: true,
|
|
onEnter: function() {
|
|
var overlay = document.getElementById('modal-new');
|
|
if (overlay && !overlay.classList.contains('active')) overlay.classList.add('active');
|
|
if (overlay) overlay.style.zIndex = '9002';
|
|
Tutorial._simulateFormRefresh();
|
|
},
|
|
onExit: function() {
|
|
Tutorial._clearSubHighlights();
|
|
},
|
|
},
|
|
// 8 - Aufbewahrung
|
|
{
|
|
id: 'form-retention',
|
|
target: '#modal-new .modal',
|
|
title: 'Aufbewahrung',
|
|
text: 'Legen Sie fest, wie lange die Lage aktiv bleibt. Nach Ablauf der Frist '
|
|
+ 'wird sie automatisch ins <strong>Archiv</strong> verschoben.<br><br>'
|
|
+ 'Setzen Sie den Wert auf <strong>0</strong> für unbegrenzte Aufbewahrung. '
|
|
+ 'Standard sind 30 Tage.',
|
|
position: 'left',
|
|
disableNav: true,
|
|
onEnter: function() {
|
|
var overlay = document.getElementById('modal-new');
|
|
if (overlay && !overlay.classList.contains('active')) overlay.classList.add('active');
|
|
if (overlay) overlay.style.zIndex = '9002';
|
|
Tutorial._simulateFormRetention();
|
|
},
|
|
onExit: function() {
|
|
Tutorial._clearSubHighlights();
|
|
},
|
|
},
|
|
// 9 - E-Mail-Benachrichtigungen
|
|
{
|
|
id: 'form-notifications',
|
|
target: '#modal-new .modal',
|
|
title: 'E-Mail-Benachrichtigungen',
|
|
text: 'Lassen Sie sich per E-Mail informieren bei:<br><br>'
|
|
+ '<strong>Neues Lagebild</strong> - Wenn eine aktualisierte Zusammenfassung vorliegt<br>'
|
|
+ '<strong>Neue Artikel</strong> - Wenn neue Quellen gefunden werden<br>'
|
|
+ '<strong>Statusänderung Faktencheck</strong> - Wenn sich die Bewertung einer Behauptung ändert<br><br>'
|
|
+ 'So bleiben Sie auch ohne ständiges Einloggen auf dem Laufenden.',
|
|
position: 'left',
|
|
disableNav: true,
|
|
onEnter: function() {
|
|
var overlay = document.getElementById('modal-new');
|
|
if (overlay && !overlay.classList.contains('active')) overlay.classList.add('active');
|
|
if (overlay) overlay.style.zIndex = '9002';
|
|
Tutorial._simulateFormNotifications();
|
|
},
|
|
onExit: function() {
|
|
var overlay = document.getElementById('modal-new');
|
|
if (overlay) {
|
|
overlay.classList.remove('active');
|
|
overlay.style.zIndex = '';
|
|
}
|
|
Tutorial._clearSubHighlights();
|
|
},
|
|
},
|
|
// 5 - Sidebar Filter
|
|
{
|
|
id: 'sidebar-filters',
|
|
target: '.sidebar-filter',
|
|
title: 'Lagen filtern',
|
|
text: 'Mit diesen Filtern steuern Sie, welche Lagen angezeigt werden:<br><br>'
|
|
+ '<strong>Alle</strong> - Zeigt sämtliche Lagen Ihrer Organisation<br>'
|
|
+ '<strong>Eigene</strong> - Nur Lagen, die Sie selbst erstellt haben<br><br>'
|
|
+ 'Bei vielen Lagen hilft dies, den Überblick zu behalten.',
|
|
position: 'right',
|
|
},
|
|
// 6 - Demo-Lage einführen
|
|
{
|
|
id: 'demo-intro',
|
|
target: '.incident-item.active.tutorial-demo',
|
|
title: 'Unsere Demo-Lage',
|
|
text: 'Für diesen Rundgang haben wir eine Demo-Lage erstellt: '
|
|
+ '"Explosion in Hamburger Hafen". Sie sehen sie hier in der Seitenleiste '
|
|
+ 'als aktive Lage mit Auto-Refresh.<br><br>'
|
|
+ 'In den folgenden Schritten erkunden wir alle Bereiche, '
|
|
+ 'die nach dem Öffnen einer Lage im Hauptbereich erscheinen.',
|
|
position: 'right',
|
|
onEnter: function() {
|
|
Tutorial._injectDemoView();
|
|
},
|
|
},
|
|
// 7 - Header-Bereich
|
|
{
|
|
id: 'incident-header',
|
|
target: '#incident-header-strip',
|
|
title: 'Lage-Kopfbereich',
|
|
text: 'Der Kopfbereich zeigt alle wichtigen Informationen auf einen Blick:<br><br>'
|
|
+ '<strong>Typ-Badge</strong> - Live-Monitoring oder Recherche<br>'
|
|
+ '<strong>Auto-Refresh</strong> - Zeigt das Aktualisierungsintervall<br>'
|
|
+ '<strong>Aktionsleiste</strong> - Aktualisieren, Bearbeiten, Exportieren, Archivieren und Löschen<br><br>'
|
|
+ 'Der Zeitstempel zeigt, wann die letzte Aktualisierung stattfand.',
|
|
position: 'bottom',
|
|
},
|
|
// 8 - Refresh-Button
|
|
{
|
|
id: 'refresh',
|
|
target: '#refresh-btn',
|
|
title: 'Manuelle Aktualisierung',
|
|
text: 'Mit diesem Button starten Sie eine sofortige Aktualisierung der Lage. '
|
|
+ 'Der Monitor durchsucht dann alle konfigurierten Quellen nach neuen Meldungen, '
|
|
+ 'erstellt ein aktualisiertes Lagebild und führt einen neuen Faktencheck durch.<br><br>'
|
|
+ 'Bei Live-Monitoring-Lagen mit Auto-Refresh geschieht dies zusätzlich automatisch im eingestellten Intervall.',
|
|
position: 'bottom',
|
|
onEnter: function() {
|
|
Tutorial._highlightSub('#refresh-btn');
|
|
},
|
|
onExit: function() {
|
|
Tutorial._clearSubHighlights();
|
|
},
|
|
},
|
|
// 9 - Export
|
|
{
|
|
id: 'export',
|
|
target: '#export-dropdown',
|
|
title: 'Lagebericht exportieren',
|
|
text: 'Exportieren Sie Ihre Lage in verschiedenen Formaten:<br><br>'
|
|
+ '<strong>Lagebericht (Markdown/JSON)</strong> - Kompakte Zusammenfassung mit Faktencheck<br>'
|
|
+ '<strong>Vollexport</strong> - Alle Daten inklusive Artikel und Quellen<br>'
|
|
+ '<strong>Drucken / PDF</strong> - Druckoptimierte Ansicht für Berichte<br><br>'
|
|
+ 'Ideal, um Ergebnisse mit Kollegen zu teilen, die keinen Monitor-Zugang haben.',
|
|
position: 'bottom',
|
|
onEnter: function() {
|
|
Tutorial._highlightSub('#export-dropdown');
|
|
},
|
|
onExit: function() {
|
|
Tutorial._clearSubHighlights();
|
|
},
|
|
},
|
|
// 10 - Layout Toolbar
|
|
{
|
|
id: 'layout-toolbar',
|
|
target: '#layout-toolbar',
|
|
title: 'Layout-Steuerung',
|
|
text: 'Mit der Layout-Steuerung passen Sie Ihr Dashboard individuell an:<br><br>'
|
|
+ 'Jeder Button steht für eine Kachel: <strong>Lagebild</strong>, <strong>Faktencheck</strong>, '
|
|
+ '<strong>Quellen</strong>, <strong>Timeline</strong> und <strong>Karte</strong>.<br><br>'
|
|
+ 'Klicken Sie auf einen Button, um die entsprechende Kachel ein- oder auszublenden. '
|
|
+ 'Mit "Layout zurücksetzen" stellen Sie die Standardansicht wieder her.',
|
|
position: 'bottom',
|
|
},
|
|
// 11 - Lagebild (detailliert)
|
|
{
|
|
id: 'lagebild',
|
|
target: '[gs-id="lagebild"]',
|
|
title: 'Lagebild',
|
|
text: 'Das Lagebild ist das Herzstück jeder Lage. Es wird automatisch aus allen gesammelten '
|
|
+ 'Quellen erstellt und bei jeder Aktualisierung neu generiert.<br><br>'
|
|
+ '<strong>Quellenverweise</strong> - Die nummerierten Verweise [1], [2] etc. verlinken '
|
|
+ 'direkt zu den Originalartikeln. So können Sie jede Aussage nachprüfen.<br>'
|
|
+ '<strong>Vollansicht</strong> - Klicken Sie auf "Lagebild" in der Kopfzeile für eine '
|
|
+ 'große Darstellung mit mehr Platz zum Lesen.<br>'
|
|
+ '<strong>Zeitstempel</strong> - Zeigt, wann dieses Lagebild zuletzt generiert wurde.',
|
|
position: 'right',
|
|
onEnter: function() {
|
|
Tutorial._highlightSub('[gs-id="lagebild"] .card-title');
|
|
},
|
|
onExit: function() {
|
|
Tutorial._clearSubHighlights();
|
|
},
|
|
},
|
|
// 12 - Quellenverweise im Lagebild
|
|
{
|
|
id: 'lagebild-sources',
|
|
target: '#summary-text',
|
|
title: 'Quellenverweise im Detail',
|
|
text: 'Die farbigen Nummern im Text sind Quellenverweise. Wenn Sie mit der Maus darüberfahren, '
|
|
+ 'sehen Sie den Namen der Quelle. Ein Klick öffnet den Originalartikel.<br><br>'
|
|
+ 'So können Sie jede Behauptung im Lagebild direkt an der Originalquelle überprüfen. '
|
|
+ 'Je mehr unabhängige Quellen eine Information stützen, desto verlässlicher ist sie.',
|
|
position: 'right',
|
|
onEnter: function() {
|
|
Tutorial._highlightSub('.source-ref[data-index="1"]');
|
|
},
|
|
onExit: function() {
|
|
Tutorial._clearSubHighlights();
|
|
},
|
|
},
|
|
// 13 - Faktencheck (detailliert)
|
|
{
|
|
id: 'faktencheck',
|
|
target: '[gs-id="faktencheck"]',
|
|
title: 'Faktencheck',
|
|
text: 'Der Faktencheck prüft automatisch die wichtigsten Behauptungen anhand aller verfügbaren Quellen. '
|
|
+ 'Jeder Eintrag erhält einen Status:<br><br>'
|
|
+ '<strong style="color:var(--success);">✓ Bestätigt/Gesichert</strong> - Durch mehrere unabhängige Quellen belegt<br>'
|
|
+ '<strong style="color:var(--warning);">? Unbestätigt</strong> - Nur aus einer Quelle bekannt<br>'
|
|
+ '<strong style="color:var(--warning);">⚠ Umstritten</strong> - Quellen widersprechen sich<br>'
|
|
+ '<strong style="color:var(--error);">✗ Widerlegt</strong> - Zuverlässige Quellen widersprechen',
|
|
position: 'left',
|
|
onEnter: function() {
|
|
Tutorial._highlightSub('#factcheck-card');
|
|
},
|
|
onExit: function() {
|
|
Tutorial._clearSubHighlights();
|
|
},
|
|
},
|
|
// 14 - Faktencheck: Einzelner Eintrag
|
|
{
|
|
id: 'faktencheck-detail',
|
|
target: '.factcheck-item[data-fc-status="confirmed"]',
|
|
title: 'Faktencheck-Eintrag',
|
|
text: 'Jeder Faktencheck-Eintrag besteht aus:<br><br>'
|
|
+ '<strong>Status-Symbol</strong> - Farbcodiert für schnelle Einordnung (links)<br>'
|
|
+ '<strong>Behauptung</strong> - Die geprüfte Aussage<br>'
|
|
+ '<strong>Quellenanzahl</strong> - Wie viele Quellen diese Behauptung stützen<br><br>'
|
|
+ 'Die Filterfunktion oben rechts ermöglicht es, nach Status zu filtern, '
|
|
+ 'z.B. nur unbestätigte Meldungen anzeigen.',
|
|
position: 'left',
|
|
onEnter: function() {
|
|
var item = document.querySelector('.factcheck-item[data-fc-status="confirmed"]');
|
|
if (item) item.classList.add('tutorial-sub-highlight');
|
|
Tutorial._cleanupFns.push(function() {
|
|
if (item) item.classList.remove('tutorial-sub-highlight');
|
|
});
|
|
},
|
|
onExit: function() {
|
|
Tutorial._clearSubHighlights();
|
|
},
|
|
},
|
|
// 15 - Quellen
|
|
{
|
|
id: 'quellen',
|
|
target: '[gs-id="quellen"]',
|
|
title: 'Quellenübersicht',
|
|
text: 'Die Quellenübersicht zeigt alle für diese Lage verwendeten Quellen, gruppiert nach Kategorie:<br><br>'
|
|
+ '<strong>Nachrichtenagenturen</strong> - dpa, Reuters, AFP<br>'
|
|
+ '<strong>Öffentlich-Rechtlich</strong> - tagesschau, ZDF, NDR<br>'
|
|
+ '<strong>Qualitätszeitungen</strong> - FAZ, SZ, ZEIT<br>'
|
|
+ '<strong>Behörden</strong> - Offizielle Stellen und Pressemitteilungen<br><br>'
|
|
+ 'Klicken Sie auf die Kopfzeile, um die Gruppen aufzuklappen. '
|
|
+ 'Die "Detailansicht" zeigt alle Quellen mit einzelnen Artikeln.',
|
|
position: 'top',
|
|
onEnter: function() {
|
|
Tutorial._highlightSub('.source-overview-header-toggle');
|
|
},
|
|
onExit: function() {
|
|
Tutorial._clearSubHighlights();
|
|
},
|
|
},
|
|
// 16 - Timeline
|
|
{
|
|
id: 'timeline',
|
|
target: '[gs-id="timeline"]',
|
|
title: 'Ereignis-Timeline',
|
|
text: 'Die Timeline zeigt den chronologischen Verlauf aller Ereignisse:<br><br>'
|
|
+ '<strong>Meldungen</strong> - Einzelne Artikel und Nachrichten aus den Quellen<br>'
|
|
+ '<strong>Lageberichte</strong> - Automatisch erstellte Zusammenfassungen (hervorgehoben)<br><br>'
|
|
+ 'Nutzen Sie die Filter oben:<br>'
|
|
+ '<strong>Alle/Meldungen/Lageberichte</strong> - Typ-Filter<br>'
|
|
+ '<strong>24h/7T/Alles</strong> - Zeitraum eingrenzen<br>'
|
|
+ '<strong>Suchfeld</strong> - Freitextsuche in allen Einträgen',
|
|
position: 'top',
|
|
onEnter: function() {
|
|
Tutorial._highlightSub('.ht-controls');
|
|
},
|
|
onExit: function() {
|
|
Tutorial._clearSubHighlights();
|
|
},
|
|
},
|
|
// 17 - Karte (Vollbild)
|
|
{
|
|
id: 'karte',
|
|
target: '#map-fullscreen-overlay',
|
|
title: 'Geografische Verteilung',
|
|
text: 'Die Karte zeigt per Geoparsing automatisch erkannte Orte aus den Quellen.<br><br>'
|
|
+ '<strong style="color:#EF4444;">●</strong> <strong>Hauptereignisort</strong> - Zentraler Ort des Geschehens<br>'
|
|
+ '<strong style="color:#F59E0B;">●</strong> <strong>Erw\u00e4hnt</strong> - In Artikeln genannte Orte<br>'
|
|
+ '<strong style="color:#3B82F6;">●</strong> <strong>Kontext</strong> - Relevante Umgebung<br><br>'
|
|
+ 'Klicken Sie auf Marker f\u00fcr Details und verkn\u00fcpfte Artikel. '
|
|
+ 'Bei vielen Markern werden nahe Orte zu Clustern gruppiert.<br><br>'
|
|
+ '<strong>Orte einlesen</strong> startet das Geoparsing manuell neu. '
|
|
+ '<strong>Vollbild</strong> zeigt die Karte in dieser gro\u00dfen Ansicht.',
|
|
position: 'left',
|
|
disableNav: true,
|
|
onEnter: function() {
|
|
Tutorial._openDemoMapFullscreen();
|
|
},
|
|
onExit: function() {
|
|
Tutorial._closeDemoMapFullscreen();
|
|
Tutorial._clearSubHighlights();
|
|
},
|
|
},
|
|
// 18 - Drag Demo
|
|
{
|
|
id: 'drag-demo',
|
|
target: '[gs-id="lagebild"] .card-header',
|
|
title: 'Kacheln verschieben',
|
|
text: 'Alle Kacheln im Dashboard lassen sich frei per Drag-and-Drop verschieben. '
|
|
+ 'Greifen Sie dazu die Kopfzeile einer Kachel und ziehen Sie sie an die gewünschte Position.<br><br>'
|
|
+ 'Beobachten Sie die virtuelle Maus-Demo:',
|
|
position: 'right',
|
|
disableNav: true,
|
|
onEnter: function() {
|
|
Tutorial._simulateDrag();
|
|
},
|
|
},
|
|
// 19 - Resize Demo
|
|
{
|
|
id: 'resize-demo',
|
|
target: '[gs-id="faktencheck"]',
|
|
title: 'Kacheln in der Größe anpassen',
|
|
text: 'Ziehen Sie am rechten unteren Rand einer Kachel, um ihre Größe zu verändern. '
|
|
+ 'So können Sie wichtigen Inhalten wie dem Faktencheck mehr Platz einräumen.<br><br>'
|
|
+ 'Beobachten Sie die virtuelle Maus-Demo:',
|
|
position: 'left',
|
|
disableNav: true,
|
|
onEnter: function() {
|
|
Tutorial._simulateResize();
|
|
},
|
|
},
|
|
// 20 - Theme
|
|
{
|
|
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.',
|
|
position: 'bottom',
|
|
},
|
|
// 21 - Quellen verwalten
|
|
{
|
|
id: 'sources-btn',
|
|
target: '.sidebar-sources-link button:first-child',
|
|
title: 'Quellenverwaltung öffnen',
|
|
text: 'In der Seitenleiste ganz unten finden Sie den Zugang zur Quellenverwaltung. '
|
|
+ 'Hier können Sie:<br><br>'
|
|
+ '<strong>Neue Quellen hinzufügen</strong> - URL eingeben oder automatisch erkennen lassen<br>'
|
|
+ '<strong>Bestehende Quellen bearbeiten</strong> - Kategorie, Sprache, Notizen anpassen<br>'
|
|
+ '<strong>Quellen deaktivieren</strong> - Temporär oder dauerhaft ausschließen',
|
|
position: 'right',
|
|
onEnter: function() {
|
|
Tutorial._stepTimeout(function() {
|
|
var overlay = document.getElementById('modal-sources');
|
|
if (overlay && !overlay.classList.contains('active')) {
|
|
if (typeof App !== 'undefined' && App.openSourceManagement) {
|
|
App.openSourceManagement();
|
|
}
|
|
}
|
|
}, 1500);
|
|
},
|
|
onExit: function() {
|
|
var overlay = document.getElementById('modal-sources');
|
|
if (overlay) {
|
|
overlay.classList.remove('active');
|
|
overlay.style.zIndex = '';
|
|
}
|
|
},
|
|
},
|
|
// 22 - Quellen Modal (mit Cursor-Demo)
|
|
{
|
|
id: 'sources-modal',
|
|
target: '#modal-sources .modal',
|
|
title: 'Quellen verwalten',
|
|
text: 'Die Quellenverwaltung zeigt alle konfigurierten Nachrichtenquellen.<br><br>'
|
|
+ '<strong>Info-Symbol (i)</strong> - Zeigt Typ, Sprache und Ausrichtung der Quelle<br>'
|
|
+ '<strong>Kategorie-Badge</strong> - Farbige Kennzeichnung der Quellkategorie<br>'
|
|
+ '<strong>Ausschlie\u00dfen</strong> - Quelle f\u00fcr einzelne Lagen sperren<br>'
|
|
+ '<strong>+ Quelle</strong> - Neue Quellen per URL hinzuf\u00fcgen<br><br>'
|
|
+ 'Beobachten Sie, wie die Quellendetails angezeigt werden:',
|
|
position: 'left',
|
|
disableNav: true,
|
|
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';
|
|
// Warten bis Quellen geladen, dann Demo starten
|
|
Tutorial._stepTimeout(function() {
|
|
Tutorial._simulateSourcesDemo();
|
|
}, 1500);
|
|
},
|
|
onExit: function() {
|
|
var overlay = document.getElementById('modal-sources');
|
|
if (overlay) {
|
|
overlay.classList.remove('active');
|
|
overlay.style.zIndex = '';
|
|
}
|
|
// Tooltip entfernen falls manuell erzeugt
|
|
var tip = document.getElementById('tutorial-tooltip');
|
|
if (tip) tip.remove();
|
|
Tutorial._clearSubHighlights();
|
|
},
|
|
},
|
|
// 23 - Chat
|
|
{
|
|
id: 'chat',
|
|
target: '#chat-toggle-btn',
|
|
title: 'Chat-Assistent',
|
|
text: 'Der Chat-Assistent steht Ihnen jederzeit zur Verfügung. '
|
|
+ 'Stellen Sie Fragen zur Bedienung des Monitors und erhalten Sie sofort eine Antwort.<br><br>'
|
|
+ 'Beispiele:<br>'
|
|
+ '"Wie erstelle ich eine neue Lage?"<br>'
|
|
+ '"Was bedeuten die Faktencheck-Status?"<br>'
|
|
+ '"Wie exportiere ich einen Lagebericht?"',
|
|
position: 'left',
|
|
},
|
|
// 24 - Ende
|
|
{
|
|
id: 'end',
|
|
target: null,
|
|
title: 'Rundgang abgeschlossen',
|
|
text: 'Sie kennen jetzt alle wichtigen Funktionen des AegisSight Monitors.<br><br>'
|
|
+ 'Die Demo-Daten werden nach dem Schließen entfernt. '
|
|
+ 'Erstellen Sie Ihre erste eigene Lage über den Button "+ Neue Lage" in der Seitenleiste.<br><br>'
|
|
+ '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 schließen falls offen
|
|
if (typeof Chat !== 'undefined' && Chat._isOpen) Chat.close();
|
|
|
|
// Overlay einblenden + Klicks blockieren
|
|
this._els.overlay.classList.add('active');
|
|
document.body.classList.add('tutorial-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 aufräumen
|
|
if (this._currentStep >= 0) this._exitStep(this._currentStep);
|
|
|
|
this._isActive = false;
|
|
this._currentStep = -1;
|
|
this._demoRunning = false;
|
|
|
|
// Overlay ausblenden + Klicks freigeben
|
|
this._els.overlay.classList.remove('active');
|
|
document.body.classList.remove('tutorial-active');
|
|
this._els.spotlight.style.opacity = '0';
|
|
this._els.bubble.classList.remove('visible');
|
|
this._hideCursor();
|
|
this._clearSubHighlights();
|
|
|
|
// Step-Timers canceln
|
|
this._clearStepTimers();
|
|
|
|
// Alle Modals schliessen die das Tutorial geoeffnet haben koennte
|
|
['modal-new', 'modal-sources'].forEach(function(id) {
|
|
var overlay = document.getElementById(id);
|
|
if (overlay) {
|
|
overlay.classList.remove('active');
|
|
overlay.style.zIndex = '';
|
|
}
|
|
});
|
|
|
|
// Cleanup-Callbacks
|
|
this._cleanupFns.forEach(function(fn) { try { fn(); } catch(e) {} });
|
|
this._cleanupFns = [];
|
|
|
|
// Formular-Inputs zuruecksetzen
|
|
var titleInput = document.getElementById('inc-title');
|
|
if (titleInput) titleInput.value = '';
|
|
var descInput = document.getElementById('inc-description');
|
|
if (descInput) descInput.value = '';
|
|
var selType = document.getElementById('inc-type');
|
|
if (selType) { selType.value = 'adhoc'; try { selType.dispatchEvent(new Event('change')); } catch(e) {} }
|
|
|
|
// Demo-View entfernen
|
|
this._removeDemoView();
|
|
|
|
// Events entfernen
|
|
if (this._keyHandler) {
|
|
document.removeEventListener('keydown', this._keyHandler);
|
|
this._keyHandler = null;
|
|
}
|
|
if (this._resizeHandler) {
|
|
window.removeEventListener('resize', this._resizeHandler);
|
|
this._resizeHandler = null;
|
|
}
|
|
|
|
this._markSeen();
|
|
},
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Navigation
|
|
// -----------------------------------------------------------------------
|
|
next() {
|
|
if (!this._isActive || this._demoRunning) return;
|
|
var nextIdx = this._currentStep + 1;
|
|
if (nextIdx >= this._steps.length) {
|
|
this.stop();
|
|
return;
|
|
}
|
|
this.goToStep(nextIdx);
|
|
},
|
|
|
|
prev() {
|
|
if (!this._isActive || this._demoRunning) return;
|
|
var prevIdx = this._currentStep - 1;
|
|
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);
|
|
},
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Step Enter/Exit
|
|
// -----------------------------------------------------------------------
|
|
async _enterStep(i) {
|
|
var step = this._steps[i];
|
|
|
|
// Bei Modal-Steps: Spotlight ausblenden (Modal hat eigene Abdunkelung)
|
|
var isModalStep = step.target && (step.target.indexOf('#modal-') !== -1);
|
|
|
|
if (step.target && step.position !== 'center' && !isModalStep) {
|
|
await this._scrollToTarget(step.target);
|
|
this._spotlightElement(step.target);
|
|
} else {
|
|
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];
|
|
this._clearStepTimers();
|
|
if (step.onExit) {
|
|
step.onExit();
|
|
}
|
|
this._hideCursor();
|
|
this._clearSubHighlights();
|
|
},
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Spotlight
|
|
// -----------------------------------------------------------------------
|
|
_spotlightElement(selector) {
|
|
var el = document.querySelector(selector);
|
|
if (!el) {
|
|
this._els.spotlight.style.opacity = '0';
|
|
return;
|
|
}
|
|
|
|
var rect = el.getBoundingClientRect();
|
|
var vw = window.innerWidth;
|
|
var vh = window.innerHeight;
|
|
var pad = 8;
|
|
|
|
// Sichtbaren Bereich berechnen (auf Viewport beschränkt)
|
|
var top = Math.max(rect.top, 0) - pad;
|
|
var left = Math.max(rect.left, 0) - pad;
|
|
var bottom = Math.min(rect.bottom, vh) + pad;
|
|
var right = Math.min(rect.right, vw) + pad;
|
|
|
|
var s = this._els.spotlight.style;
|
|
s.top = top + 'px';
|
|
s.left = left + 'px';
|
|
s.width = (right - left) + 'px';
|
|
s.height = (bottom - top) + '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 = 360;
|
|
|
|
// 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%)';
|
|
bubble.style.width = bw + 'px';
|
|
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%)';
|
|
bubble.style.width = bw + 'px';
|
|
return;
|
|
}
|
|
|
|
var rect = el.getBoundingClientRect();
|
|
var vw = window.innerWidth;
|
|
var vh = window.innerHeight;
|
|
var gap = 16;
|
|
|
|
// Sichtbaren Bereich des Elements berechnen (auf Viewport beschraenkt)
|
|
var visTop = Math.max(rect.top, 0);
|
|
var visBottom = Math.min(rect.bottom, vh);
|
|
var visLeft = Math.max(rect.left, 0);
|
|
var visRight = Math.min(rect.right, vw);
|
|
var visCenterY = (visTop + visBottom) / 2;
|
|
var visCenterX = (visLeft + visRight) / 2;
|
|
|
|
// Reset transform
|
|
bubble.style.transform = '';
|
|
bubble.style.width = bw + 'px';
|
|
|
|
// Automatische Positionswahl falls nicht genug Platz
|
|
var pos = step.position || 'bottom';
|
|
var bubbleHeight = 300;
|
|
|
|
if (pos === 'bottom' && (visBottom + gap + bubbleHeight > vh)) pos = 'top';
|
|
if (pos === 'top' && (visTop - gap - bubbleHeight < 0)) pos = 'bottom';
|
|
if (pos === 'right' && (visRight + gap + bw > vw)) pos = 'left';
|
|
if (pos === 'left' && (visLeft - gap - bw < 0)) pos = 'right';
|
|
|
|
bubble.className = 'tutorial-bubble visible tutorial-pos-' + pos;
|
|
|
|
// Bubble-Top immer im sichtbaren Bereich halten
|
|
var clampTop = function(t) { return Math.max(8, Math.min(t, vh - bubbleHeight - 8)); };
|
|
|
|
switch (pos) {
|
|
case 'bottom':
|
|
bubble.style.top = Math.min(visBottom + gap, vh - bubbleHeight - 8) + 'px';
|
|
bubble.style.left = Math.max(8, Math.min(visCenterX - bw / 2, vw - bw - 8)) + 'px';
|
|
break;
|
|
case 'top':
|
|
bubble.style.top = Math.max(visTop - gap, 8) + 'px';
|
|
bubble.style.left = Math.max(8, Math.min(visCenterX - bw / 2, vw - bw - 8)) + 'px';
|
|
bubble.style.transform = 'translateY(-100%)';
|
|
break;
|
|
case 'right':
|
|
bubble.style.top = clampTop(visCenterY - bubbleHeight / 2) + 'px';
|
|
bubble.style.left = Math.min(visRight + gap, vw - bw - 8) + 'px';
|
|
break;
|
|
case 'left':
|
|
bubble.style.top = clampTop(visCenterY - bubbleHeight / 2) + 'px';
|
|
bubble.style.left = Math.max(visLeft - gap - bw, 8) + '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) {
|
|
return new Promise(function(resolve) {
|
|
setTimeout(resolve, ms);
|
|
});
|
|
},
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Karten-Demo: Vollbild mit Markern und Cursor-Demo
|
|
// -----------------------------------------------------------------------
|
|
_openDemoMapFullscreen() {
|
|
var overlay = document.getElementById('map-fullscreen-overlay');
|
|
var fsContainer = document.getElementById('map-fullscreen-container');
|
|
var fsStats = document.getElementById('map-fullscreen-stats');
|
|
if (!overlay || !fsContainer) return;
|
|
|
|
// Overlay anzeigen (\u00fcber Tutorial-Z-Index)
|
|
overlay.classList.add('active');
|
|
overlay.style.zIndex = '9002';
|
|
|
|
if (fsStats) fsStats.textContent = '3 Orte / 9 Artikel';
|
|
|
|
// Alte Demo-Map entfernen falls vorhanden
|
|
this._destroyDemoMap();
|
|
|
|
// Neue Map im Fullscreen-Container erstellen
|
|
fsContainer.innerHTML = '';
|
|
var mapDiv = document.createElement('div');
|
|
mapDiv.id = 'tutorial-fs-map';
|
|
mapDiv.style.cssText = 'width:100%;height:100%;';
|
|
fsContainer.appendChild(mapDiv);
|
|
|
|
var isDark = !document.documentElement.getAttribute('data-theme') || document.documentElement.getAttribute('data-theme') !== 'light';
|
|
this._demoMap = L.map(mapDiv, {
|
|
zoomControl: true,
|
|
attributionControl: true,
|
|
}).setView([53.545, 9.98], 13);
|
|
|
|
var tileUrl = isDark
|
|
? 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
|
|
: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png';
|
|
L.tileLayer(tileUrl, {
|
|
attribution: '\u00a9 OpenStreetMap, \u00a9 CARTO',
|
|
maxZoom: 19,
|
|
}).addTo(this._demoMap);
|
|
|
|
// Marker
|
|
var locations = [
|
|
{ lat: 53.5325, lon: 9.9275, name: 'Burchardkai Terminal', articles: 6, cat: 'primary' },
|
|
{ lat: 53.5460, lon: 9.9690, name: 'Hamburg Innenstadt', articles: 2, cat: 'secondary' },
|
|
{ lat: 53.5380, lon: 9.9400, name: 'Elbe / Hafengebiet', articles: 1, cat: 'tertiary' },
|
|
];
|
|
var catColors = { primary: '#EF4444', secondary: '#F59E0B', tertiary: '#3B82F6' };
|
|
var catLabels = { primary: 'Hauptereignisort', secondary: 'Erw\u00e4hnt', tertiary: 'Kontext' };
|
|
var self = this;
|
|
this._demoMapMarkers = [];
|
|
|
|
locations.forEach(function(loc) {
|
|
var color = catColors[loc.cat];
|
|
var icon = L.divIcon({
|
|
className: 'tutorial-map-marker',
|
|
html: '<div style="width:16px;height:16px;border-radius:50%;background:' + color
|
|
+ ';border:2px solid #fff;box-shadow:0 2px 8px rgba(0,0,0,0.5);"></div>',
|
|
iconSize: [16, 16],
|
|
iconAnchor: [8, 8],
|
|
});
|
|
var marker = L.marker([loc.lat, loc.lon], { icon: icon });
|
|
var popupHtml = '<div class="map-popup">'
|
|
+ '<div class="map-popup-title">' + loc.name + '</div>'
|
|
+ '<div style="font-size:11px;margin:4px 0;"><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:' + color + ';margin-right:4px;vertical-align:middle;"></span>' + catLabels[loc.cat] + '</div>'
|
|
+ '<div class="map-popup-count">' + loc.articles + ' Artikel</div>'
|
|
+ '</div>';
|
|
marker.bindPopup(popupHtml, { maxWidth: 250, className: 'map-popup-container' });
|
|
marker.addTo(self._demoMap);
|
|
self._demoMapMarkers.push(marker);
|
|
});
|
|
|
|
// Legende
|
|
var legend = L.control({ position: 'bottomright' });
|
|
legend.onAdd = function() {
|
|
var div = L.DomUtil.create('div', 'map-legend-ctrl');
|
|
L.DomEvent.disableClickPropagation(div);
|
|
var html = '<strong style="display:block;margin-bottom:6px;">Legende</strong>';
|
|
['primary', 'secondary', 'tertiary'].forEach(function(cat) {
|
|
html += '<div class="map-legend-item" style="display:flex;align-items:center;gap:6px;margin:3px 0;">'
|
|
+ '<span style="width:10px;height:10px;border-radius:50%;background:' + catColors[cat] + ';flex-shrink:0;"></span>'
|
|
+ '<span>' + catLabels[cat] + '</span></div>';
|
|
});
|
|
div.innerHTML = html;
|
|
return div;
|
|
};
|
|
legend.addTo(this._demoMap);
|
|
this._demoMapLegend = legend;
|
|
|
|
// Resize + Start Demo
|
|
var map = this._demoMap;
|
|
setTimeout(function() {
|
|
if (map) map.invalidateSize();
|
|
setTimeout(function() {
|
|
if (map) map.setView([53.545, 9.98], 13);
|
|
self._simulateMapDemo();
|
|
}, 300);
|
|
}, 200);
|
|
},
|
|
|
|
_closeDemoMapFullscreen() {
|
|
var overlay = document.getElementById('map-fullscreen-overlay');
|
|
if (overlay) {
|
|
overlay.classList.remove('active');
|
|
overlay.style.zIndex = '';
|
|
}
|
|
var fsContainer = document.getElementById('map-fullscreen-container');
|
|
if (fsContainer) fsContainer.innerHTML = '';
|
|
this._destroyDemoMap();
|
|
},
|
|
|
|
async _simulateMapDemo() {
|
|
this._demoRunning = true;
|
|
|
|
await this._wait(500);
|
|
|
|
// 1. Cursor zu Hauptmarker und klicken
|
|
if (this._demoMapMarkers.length > 0 && this._demoMap) {
|
|
var mainMarker = this._demoMapMarkers[0];
|
|
var latLng = mainMarker.getLatLng();
|
|
var point = this._demoMap.latLngToContainerPoint(latLng);
|
|
var mapEl = document.getElementById('tutorial-fs-map');
|
|
if (mapEl) {
|
|
var mapRect = mapEl.getBoundingClientRect();
|
|
var markerX = mapRect.left + point.x;
|
|
var markerY = mapRect.top + point.y;
|
|
|
|
this._showCursor(markerX - 60, markerY - 60, 'default');
|
|
await this._wait(300);
|
|
await this._animateCursor(markerX - 60, markerY - 60, markerX, markerY, 700);
|
|
await this._wait(200);
|
|
mainMarker.openPopup();
|
|
await this._wait(2500);
|
|
mainMarker.closePopup();
|
|
|
|
// 2. Zum zweiten Marker
|
|
var secondMarker = this._demoMapMarkers[1];
|
|
if (secondMarker) {
|
|
var p2 = this._demoMap.latLngToContainerPoint(secondMarker.getLatLng());
|
|
var m2x = mapRect.left + p2.x;
|
|
var m2y = mapRect.top + p2.y;
|
|
await this._animateCursor(markerX, markerY, m2x, m2y, 600);
|
|
await this._wait(200);
|
|
secondMarker.openPopup();
|
|
await this._wait(2000);
|
|
secondMarker.closePopup();
|
|
}
|
|
}
|
|
}
|
|
|
|
this._hideCursor();
|
|
this._demoRunning = false;
|
|
this._enableNavAfterDemo();
|
|
},
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Tipp-Simulation: Text Zeichen fuer Zeichen eingeben
|
|
// -----------------------------------------------------------------------
|
|
_simulateTyping(input, text, ms) {
|
|
var self = this;
|
|
var charDelay = ms / text.length;
|
|
return new Promise(function(resolve) {
|
|
var i = 0;
|
|
function typeNext() {
|
|
if (!self._isActive || i >= text.length) { resolve(); return; }
|
|
input.value += text[i];
|
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
i++;
|
|
setTimeout(typeNext, charDelay);
|
|
}
|
|
typeNext();
|
|
});
|
|
},
|
|
|
|
// Helfer: Cursor zu einem Element bewegen
|
|
async _cursorToElement(selector, fromX, fromY) {
|
|
var el = document.querySelector(selector);
|
|
if (!el) return { x: fromX, y: fromY };
|
|
var rect = el.getBoundingClientRect();
|
|
var tx = rect.left + Math.min(rect.width / 2, 60);
|
|
var ty = rect.top + rect.height / 2;
|
|
if (fromX !== undefined && fromY !== undefined) {
|
|
await this._animateCursor(fromX, fromY, tx, ty, 500);
|
|
} else {
|
|
this._showCursor(tx, ty, 'default');
|
|
}
|
|
await this._wait(200);
|
|
return { x: tx, y: ty };
|
|
},
|
|
|
|
// Helfer: Modal-Body zu einem Element scrollen
|
|
_scrollModalTo(selector) {
|
|
var el = document.querySelector(selector);
|
|
var modalBody = document.querySelector('#modal-new .modal-body');
|
|
if (!el || !modalBody) return;
|
|
var elTop = el.offsetTop - modalBody.offsetTop;
|
|
modalBody.scrollTo({ top: Math.max(0, elTop - 20), behavior: 'smooth' });
|
|
},
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Step 3: Titel + Beschreibung
|
|
// -----------------------------------------------------------------------
|
|
async _simulateFormTitleDesc() {
|
|
this._demoRunning = true;
|
|
var titleInput = document.getElementById('inc-title');
|
|
var descInput = document.getElementById('inc-description');
|
|
if (!titleInput) { this._demoRunning = false; this._enableNavAfterDemo(); return; }
|
|
|
|
// Cursor zum Titel
|
|
this._highlightSub('#inc-title');
|
|
var pos = await this._cursorToElement('#inc-title');
|
|
titleInput.focus();
|
|
await this._simulateTyping(titleInput, 'Explosion in Hamburger Hafen', 1200);
|
|
await this._wait(400);
|
|
this._clearSubHighlights();
|
|
|
|
// Cursor zur Beschreibung
|
|
if (descInput) {
|
|
this._highlightSub('#inc-description');
|
|
pos = await this._cursorToElement('#inc-description', pos.x, pos.y);
|
|
descInput.focus();
|
|
await this._simulateTyping(descInput, 'Schwere Explosion im Hafengebiet, Burchardkai-Terminal', 1200);
|
|
await this._wait(400);
|
|
this._clearSubHighlights();
|
|
}
|
|
|
|
this._hideCursor();
|
|
this._demoRunning = false;
|
|
this._enableNavAfterDemo();
|
|
},
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Step 4: Art der Lage (Typ-Wechsel)
|
|
// -----------------------------------------------------------------------
|
|
async _simulateTypeSwitch() {
|
|
this._demoRunning = true;
|
|
var sel = document.getElementById('inc-type');
|
|
if (!sel) { this._demoRunning = false; this._enableNavAfterDemo(); return; }
|
|
|
|
var pos = await this._cursorToElement('#inc-type');
|
|
await this._wait(300);
|
|
|
|
// Wechsel zu Recherche
|
|
sel.value = 'research';
|
|
sel.dispatchEvent(new Event('change'));
|
|
await this._wait(2000);
|
|
|
|
// Zur\u00fcck zu Live-Monitoring
|
|
sel.value = 'adhoc';
|
|
sel.dispatchEvent(new Event('change'));
|
|
await this._wait(800);
|
|
|
|
this._hideCursor();
|
|
this._demoRunning = false;
|
|
this._enableNavAfterDemo();
|
|
},
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Step 5: Quellen (International + Telegram toggles)
|
|
// -----------------------------------------------------------------------
|
|
async _simulateFormSources() {
|
|
this._demoRunning = true;
|
|
this._scrollModalTo('#inc-international');
|
|
await this._wait(400);
|
|
|
|
// International-Toggle highlighten
|
|
var intlCheckbox = document.getElementById('inc-international');
|
|
var intlLabel = intlCheckbox ? intlCheckbox.closest('.toggle-label') : null;
|
|
if (intlLabel) {
|
|
this._highlightSub('#inc-international');
|
|
var pos = await this._cursorToElement('#inc-international');
|
|
await this._wait(1500);
|
|
this._clearSubHighlights();
|
|
|
|
// Telegram-Toggle
|
|
var telegramCheckbox = document.getElementById('inc-telegram');
|
|
if (telegramCheckbox) {
|
|
this._highlightSub('#inc-telegram');
|
|
pos = await this._cursorToElement('#inc-telegram', pos.x, pos.y);
|
|
// Aktivieren
|
|
telegramCheckbox.checked = true;
|
|
await this._wait(1500);
|
|
this._clearSubHighlights();
|
|
}
|
|
}
|
|
|
|
this._hideCursor();
|
|
this._demoRunning = false;
|
|
this._enableNavAfterDemo();
|
|
},
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Step 6: Sichtbarkeit
|
|
// -----------------------------------------------------------------------
|
|
async _simulateFormVisibility() {
|
|
this._demoRunning = true;
|
|
this._scrollModalTo('#inc-visibility');
|
|
await this._wait(400);
|
|
|
|
var checkbox = document.getElementById('inc-visibility');
|
|
if (checkbox) {
|
|
this._highlightSub('#inc-visibility');
|
|
var pos = await this._cursorToElement('#inc-visibility');
|
|
await this._wait(1000);
|
|
|
|
// Umschalten auf Privat
|
|
checkbox.checked = false;
|
|
var textEl = document.getElementById('visibility-text');
|
|
if (textEl) textEl.textContent = 'Privat \u2014 nur f\u00fcr dich sichtbar';
|
|
await this._wait(1500);
|
|
|
|
// Zur\u00fcck auf \u00d6ffentlich
|
|
checkbox.checked = true;
|
|
if (textEl) textEl.textContent = '\u00d6ffentlich \u2014 f\u00fcr alle Nutzer sichtbar';
|
|
await this._wait(800);
|
|
this._clearSubHighlights();
|
|
}
|
|
|
|
this._hideCursor();
|
|
this._demoRunning = false;
|
|
this._enableNavAfterDemo();
|
|
},
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Step 7: Aktualisierung + Intervall
|
|
// -----------------------------------------------------------------------
|
|
async _simulateFormRefresh() {
|
|
this._demoRunning = true;
|
|
this._scrollModalTo('#inc-refresh-mode');
|
|
await this._wait(400);
|
|
|
|
var refreshSelect = document.getElementById('inc-refresh-mode');
|
|
if (refreshSelect) {
|
|
this._highlightSub('#inc-refresh-mode');
|
|
var pos = await this._cursorToElement('#inc-refresh-mode');
|
|
await this._wait(800);
|
|
|
|
// Auf Auto wechseln
|
|
refreshSelect.value = 'auto';
|
|
try { refreshSelect.dispatchEvent(new Event('change')); } catch(e) {}
|
|
await this._wait(1000);
|
|
this._clearSubHighlights();
|
|
|
|
// Intervall-Feld highlighten
|
|
var intervalField = document.getElementById('refresh-interval-field');
|
|
var intervalInput = document.getElementById('inc-refresh-value');
|
|
if (intervalField && intervalInput) {
|
|
this._highlightSub('#inc-refresh-value');
|
|
pos = await this._cursorToElement('#inc-refresh-value', pos.x, pos.y);
|
|
await this._wait(1500);
|
|
this._clearSubHighlights();
|
|
}
|
|
}
|
|
|
|
this._hideCursor();
|
|
this._demoRunning = false;
|
|
this._enableNavAfterDemo();
|
|
},
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Step 8: Aufbewahrung
|
|
// -----------------------------------------------------------------------
|
|
async _simulateFormRetention() {
|
|
this._demoRunning = true;
|
|
this._scrollModalTo('#inc-retention');
|
|
await this._wait(400);
|
|
|
|
var retentionInput = document.getElementById('inc-retention');
|
|
if (retentionInput) {
|
|
this._highlightSub('#inc-retention');
|
|
var pos = await this._cursorToElement('#inc-retention');
|
|
await this._wait(2000);
|
|
this._clearSubHighlights();
|
|
}
|
|
|
|
this._hideCursor();
|
|
this._demoRunning = false;
|
|
this._enableNavAfterDemo();
|
|
},
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Step 9: E-Mail-Benachrichtigungen
|
|
// -----------------------------------------------------------------------
|
|
async _simulateFormNotifications() {
|
|
this._demoRunning = true;
|
|
this._scrollModalTo('#inc-notify-summary');
|
|
await this._wait(400);
|
|
|
|
var checks = ['#inc-notify-summary', '#inc-notify-new-articles', '#inc-notify-status-change'];
|
|
var pos;
|
|
for (var i = 0; i < checks.length; i++) {
|
|
var cb = document.querySelector(checks[i]);
|
|
if (!cb) continue;
|
|
this._highlightSub(checks[i]);
|
|
if (pos) {
|
|
pos = await this._cursorToElement(checks[i], pos.x, pos.y);
|
|
} else {
|
|
pos = await this._cursorToElement(checks[i]);
|
|
}
|
|
cb.checked = true;
|
|
await this._wait(1000);
|
|
this._clearSubHighlights();
|
|
}
|
|
|
|
this._hideCursor();
|
|
this._demoRunning = false;
|
|
this._enableNavAfterDemo();
|
|
},
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Drag-Demo: Kachel visuell verschieben
|
|
// -----------------------------------------------------------------------
|
|
async _simulateDrag() {
|
|
this._demoRunning = true;
|
|
var tile = document.querySelector('[gs-id="lagebild"]');
|
|
var header = tile ? tile.querySelector('.card-header') : null;
|
|
if (!header || !tile) { this._demoRunning = false; this._enableNavAfterDemo(); return; }
|
|
|
|
var rect = header.getBoundingClientRect();
|
|
var startX = rect.left + rect.width / 2;
|
|
var startY = rect.top + rect.height / 2;
|
|
var moveX = 150;
|
|
|
|
// Cursor erscheint
|
|
this._showCursor(startX - 50, startY - 30, 'default');
|
|
await this._wait(300);
|
|
await this._animateCursor(startX - 50, startY - 30, startX, startY, 500);
|
|
await this._wait(300);
|
|
|
|
// Grabbing
|
|
this._els.cursor.classList.remove('tutorial-cursor-default');
|
|
this._els.cursor.classList.add('tutorial-cursor-grabbing');
|
|
await this._wait(200);
|
|
|
|
// Kachel visuell verschieben (CSS transform)
|
|
tile.style.transition = 'none';
|
|
tile.style.zIndex = '9002';
|
|
var self = this;
|
|
var start = null;
|
|
await new Promise(function(resolve) {
|
|
function frame(ts) {
|
|
if (!self._isActive) { resolve(); return; }
|
|
if (!start) start = ts;
|
|
var progress = Math.min((ts - start) / 1200, 1);
|
|
var eased = progress < 0.5 ? 4*progress*progress*progress : 1 - Math.pow(-2*progress+2,3)/2;
|
|
var dx = moveX * eased;
|
|
tile.style.transform = 'translateX(' + dx + 'px)';
|
|
self._els.cursor.style.left = (startX + dx) + 'px';
|
|
if (progress < 1) requestAnimationFrame(frame); else resolve();
|
|
}
|
|
requestAnimationFrame(frame);
|
|
});
|
|
await this._wait(300);
|
|
|
|
// Zurück
|
|
this._els.cursor.classList.remove('tutorial-cursor-grabbing');
|
|
this._els.cursor.classList.add('tutorial-cursor-default');
|
|
start = null;
|
|
await new Promise(function(resolve) {
|
|
function frame(ts) {
|
|
if (!self._isActive) { resolve(); return; }
|
|
if (!start) start = ts;
|
|
var progress = Math.min((ts - start) / 800, 1);
|
|
var eased = progress < 0.5 ? 4*progress*progress*progress : 1 - Math.pow(-2*progress+2,3)/2;
|
|
var dx = moveX * (1 - eased);
|
|
tile.style.transform = 'translateX(' + dx + 'px)';
|
|
self._els.cursor.style.left = (startX + dx) + 'px';
|
|
if (progress < 1) requestAnimationFrame(frame); else resolve();
|
|
}
|
|
requestAnimationFrame(frame);
|
|
});
|
|
|
|
// Aufräumen
|
|
tile.style.transform = '';
|
|
tile.style.transition = '';
|
|
tile.style.zIndex = '';
|
|
this._hideCursor();
|
|
await this._wait(200);
|
|
|
|
this._demoRunning = false;
|
|
this._enableNavAfterDemo();
|
|
},
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Resize-Demo: Kachel visuell vergrößern
|
|
// -----------------------------------------------------------------------
|
|
async _simulateResize() {
|
|
this._demoRunning = true;
|
|
var tile = document.querySelector('[gs-id="faktencheck"]');
|
|
if (!tile) { this._demoRunning = false; this._enableNavAfterDemo(); return; }
|
|
|
|
var rect = tile.getBoundingClientRect();
|
|
var startX = rect.right - 6;
|
|
var startY = rect.bottom - 6;
|
|
var expandX = 80;
|
|
var expandY = 50;
|
|
|
|
// Cursor am Rand
|
|
this._showCursor(startX - 40, startY - 40, 'default');
|
|
await this._wait(300);
|
|
await this._animateCursor(startX - 40, startY - 40, startX, startY, 400);
|
|
await this._wait(200);
|
|
|
|
// Resize-Cursor
|
|
this._els.cursor.classList.remove('tutorial-cursor-default');
|
|
this._els.cursor.classList.add('tutorial-cursor-resize');
|
|
await this._wait(200);
|
|
|
|
// Kachel vergrößern (visuell)
|
|
tile.style.transition = 'none';
|
|
tile.style.zIndex = '9002';
|
|
var origW = tile.offsetWidth;
|
|
var origH = tile.offsetHeight;
|
|
var self = this;
|
|
var start = null;
|
|
|
|
await new Promise(function(resolve) {
|
|
function frame(ts) {
|
|
if (!self._isActive) { resolve(); return; }
|
|
if (!start) start = ts;
|
|
var progress = Math.min((ts - start) / 1000, 1);
|
|
var eased = progress < 0.5 ? 4*progress*progress*progress : 1 - Math.pow(-2*progress+2,3)/2;
|
|
var dx = expandX * eased;
|
|
var dy = expandY * eased;
|
|
tile.style.width = (origW + dx) + 'px';
|
|
tile.style.height = (origH + dy) + 'px';
|
|
self._els.cursor.style.left = (startX + dx) + 'px';
|
|
self._els.cursor.style.top = (startY + dy) + 'px';
|
|
if (progress < 1) requestAnimationFrame(frame); else resolve();
|
|
}
|
|
requestAnimationFrame(frame);
|
|
});
|
|
await this._wait(500);
|
|
|
|
// Zurück
|
|
start = null;
|
|
await new Promise(function(resolve) {
|
|
function frame(ts) {
|
|
if (!self._isActive) { resolve(); return; }
|
|
if (!start) start = ts;
|
|
var progress = Math.min((ts - start) / 700, 1);
|
|
var eased = progress < 0.5 ? 4*progress*progress*progress : 1 - Math.pow(-2*progress+2,3)/2;
|
|
var dx = expandX * (1 - eased);
|
|
var dy = expandY * (1 - eased);
|
|
tile.style.width = (origW + dx) + 'px';
|
|
tile.style.height = (origH + dy) + 'px';
|
|
self._els.cursor.style.left = (startX + dx) + 'px';
|
|
self._els.cursor.style.top = (startY + dy) + 'px';
|
|
if (progress < 1) requestAnimationFrame(frame); else resolve();
|
|
}
|
|
requestAnimationFrame(frame);
|
|
});
|
|
|
|
// Aufräumen
|
|
tile.style.width = '';
|
|
tile.style.height = '';
|
|
tile.style.transition = '';
|
|
tile.style.zIndex = '';
|
|
this._hideCursor();
|
|
await this._wait(200);
|
|
|
|
this._demoRunning = false;
|
|
this._enableNavAfterDemo();
|
|
},
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Quellenverwaltung-Demo: Info-Icon hover + Tooltip zeigen
|
|
// -----------------------------------------------------------------------
|
|
async _simulateSourcesDemo() {
|
|
this._demoRunning = true;
|
|
|
|
// Warten bis Quellen geladen sind
|
|
var attempts = 0;
|
|
while (attempts < 10) {
|
|
var infoIcon = document.querySelector('#sources-list .info-icon');
|
|
if (infoIcon) break;
|
|
await this._wait(300);
|
|
attempts++;
|
|
}
|
|
|
|
var infoIcon = document.querySelector('#sources-list .info-icon');
|
|
if (!infoIcon) {
|
|
this._demoRunning = false;
|
|
this._enableNavAfterDemo();
|
|
return;
|
|
}
|
|
|
|
// 1. Cursor zum ersten Info-Icon
|
|
var rect = infoIcon.getBoundingClientRect();
|
|
var targetX = rect.left + rect.width / 2;
|
|
var targetY = rect.top + rect.height / 2;
|
|
|
|
this._showCursor(targetX - 80, targetY - 60, 'default');
|
|
await this._wait(300);
|
|
await this._animateCursor(targetX - 80, targetY - 60, targetX, targetY, 600);
|
|
await this._wait(200);
|
|
|
|
// 2. Info-Icon highlighten
|
|
this._highlightSub('#sources-list .info-icon');
|
|
|
|
// 3. Tooltip manuell anzeigen (da pointer-events blockiert sind)
|
|
var tooltipText = infoIcon.getAttribute('data-tooltip') || '';
|
|
if (tooltipText) {
|
|
var tooltip = document.createElement('div');
|
|
tooltip.id = 'tutorial-tooltip';
|
|
tooltip.style.cssText = 'position:fixed;z-index:9999;background:var(--bg-card);color:var(--text-primary);'
|
|
+ 'border:1px solid var(--accent);border-radius:var(--radius);padding:8px 12px;font-size:12px;'
|
|
+ 'line-height:1.5;max-width:250px;white-space:pre-line;box-shadow:var(--shadow-md);'
|
|
+ 'pointer-events:none;';
|
|
tooltip.textContent = tooltipText;
|
|
document.body.appendChild(tooltip);
|
|
|
|
// Tooltip unter dem Icon positionieren
|
|
tooltip.style.left = Math.max(8, rect.left - 20) + 'px';
|
|
tooltip.style.top = (rect.bottom + 8) + 'px';
|
|
}
|
|
|
|
await this._wait(3000);
|
|
|
|
// 4. Tooltip entfernen
|
|
var tip = document.getElementById('tutorial-tooltip');
|
|
if (tip) tip.remove();
|
|
this._clearSubHighlights();
|
|
|
|
// 5. Zum "+ Quelle" Button
|
|
var addBtn = document.querySelector('.sources-toolbar-actions .btn-primary');
|
|
if (addBtn) {
|
|
var addRect = addBtn.getBoundingClientRect();
|
|
await this._animateCursor(targetX, targetY, addRect.left + addRect.width / 2, addRect.top + addRect.height / 2, 500);
|
|
this._highlightSub('.sources-toolbar-actions .btn-primary');
|
|
await this._wait(2000);
|
|
this._clearSubHighlights();
|
|
}
|
|
|
|
// 6. Zum "Ausschlie\u00dfen" Button der ersten Quelle
|
|
var excludeBtn = document.querySelector('#sources-list .source-group-actions .btn-secondary');
|
|
if (excludeBtn) {
|
|
var exRect = excludeBtn.getBoundingClientRect();
|
|
var prevRect = addBtn ? addBtn.getBoundingClientRect() : rect;
|
|
await this._animateCursor(prevRect.left + prevRect.width / 2, prevRect.top + prevRect.height / 2,
|
|
exRect.left + exRect.width / 2, exRect.top + exRect.height / 2, 500);
|
|
this._highlightSub('#sources-list .source-group-actions .btn-secondary');
|
|
await this._wait(2000);
|
|
this._clearSubHighlights();
|
|
}
|
|
|
|
this._hideCursor();
|
|
this._demoRunning = false;
|
|
this._enableNavAfterDemo();
|
|
},
|
|
|
|
// -----------------------------------------------------------------------
|
|
// 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; }
|
|
},
|
|
};
|