1249 lines
45 KiB
JavaScript
1249 lines
45 KiB
JavaScript
/**
|
|
* Mindmap-Initialisierer
|
|
* Lädt und initialisiert die Mindmap-Visualisierung
|
|
*/
|
|
|
|
// Warte bis DOM geladen ist
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Prüfe, ob wir auf der Mindmap-Seite sind
|
|
const cyContainer = document.getElementById('cy');
|
|
|
|
if (!cyContainer) {
|
|
console.log('Kein Mindmap-Container gefunden, überspringe Initialisierung.');
|
|
return;
|
|
}
|
|
|
|
console.log('Initialisiere Mindmap-Visualisierung...');
|
|
|
|
// Prüfe, ob Cytoscape.js verfügbar ist
|
|
if (typeof cytoscape === 'undefined') {
|
|
loadScript('/static/js/cytoscape.min.js', initMindmap);
|
|
} else {
|
|
initMindmap();
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Lädt ein Script dynamisch
|
|
* @param {string} src - Quelldatei
|
|
* @param {Function} callback - Callback nach dem Laden
|
|
*/
|
|
function loadScript(src, callback) {
|
|
const script = document.createElement('script');
|
|
script.src = src;
|
|
script.onload = callback;
|
|
document.head.appendChild(script);
|
|
}
|
|
|
|
/**
|
|
* Initialisiert die Mindmap-Visualisierung
|
|
*/
|
|
function initMindmap() {
|
|
const cyContainer = document.getElementById('cy');
|
|
const fitBtn = document.getElementById('fit-btn');
|
|
const resetBtn = document.getElementById('reset-btn');
|
|
const toggleLabelsBtn = document.getElementById('toggle-labels-btn');
|
|
const nodeInfoPanel = document.getElementById('node-info-panel');
|
|
const nodeDescription = document.getElementById('node-description');
|
|
const connectedNodes = document.getElementById('connected-nodes');
|
|
|
|
let labelsVisible = true;
|
|
let selectedNode = null;
|
|
|
|
// Erstelle Cytoscape-Instanz
|
|
const cy = cytoscape({
|
|
container: cyContainer,
|
|
style: getDefaultStyles(),
|
|
layout: {
|
|
name: 'cose',
|
|
animate: true,
|
|
animationDuration: 800,
|
|
nodeDimensionsIncludeLabels: true,
|
|
padding: 50,
|
|
spacingFactor: 1.2,
|
|
randomize: true,
|
|
componentSpacing: 100,
|
|
nodeRepulsion: 8000,
|
|
edgeElasticity: 100,
|
|
nestingFactor: 1.2,
|
|
gravity: 80
|
|
},
|
|
wheelSensitivity: 0.3,
|
|
});
|
|
|
|
// Daten vom Server laden
|
|
loadMindmapData(cy);
|
|
|
|
// Event-Handler zuweisen
|
|
setupEventListeners(cy, fitBtn, resetBtn, toggleLabelsBtn, nodeInfoPanel, nodeDescription, connectedNodes);
|
|
}
|
|
|
|
/**
|
|
* Lädt die Mindmap-Daten vom Server
|
|
* @param {Object} cy - Cytoscape-Instanz
|
|
*/
|
|
function loadMindmapData(cy) {
|
|
fetch('/api/mindmap')
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP Fehler: ${response.status}`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
if (!data.nodes || data.nodes.length === 0) {
|
|
console.log('Keine Daten gefunden, versuche Refresh-API...');
|
|
return fetch('/api/refresh-mindmap')
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP Fehler beim Refresh: ${response.status}`);
|
|
}
|
|
return response.json();
|
|
});
|
|
}
|
|
return data;
|
|
})
|
|
.then(data => {
|
|
console.log('Mindmap-Daten geladen:', data);
|
|
|
|
// Cytoscape-Elemente vorbereiten
|
|
const elements = [];
|
|
|
|
// Prüfen, ob "Wissen"-Knoten existiert
|
|
let rootNode = data.nodes.find(node => node.name === "Wissen");
|
|
|
|
// Wenn nicht, Root-Knoten hinzufügen
|
|
if (!rootNode) {
|
|
rootNode = {
|
|
id: 'root',
|
|
name: 'Wissen',
|
|
description: 'Zentrale Wissensbasis',
|
|
color_code: '#4299E1'
|
|
};
|
|
data.nodes.unshift(rootNode);
|
|
}
|
|
|
|
// Knoten hinzufügen
|
|
data.nodes.forEach(node => {
|
|
elements.push({
|
|
group: 'nodes',
|
|
data: {
|
|
id: node.id.toString(),
|
|
name: node.name,
|
|
description: node.description || '',
|
|
color: node.color_code || '#8B5CF6',
|
|
isRoot: node.name === 'Wissen'
|
|
}
|
|
});
|
|
});
|
|
|
|
// Kanten hinzufügen, wenn vorhanden
|
|
if (data.edges && data.edges.length > 0) {
|
|
data.edges.forEach(edge => {
|
|
elements.push({
|
|
group: 'edges',
|
|
data: {
|
|
id: `${edge.source}-${edge.target}`,
|
|
source: edge.source.toString(),
|
|
target: edge.target.toString()
|
|
}
|
|
});
|
|
});
|
|
} else {
|
|
// Wenn keine Kanten definiert sind, verbinde alle Knoten mit dem Root-Knoten
|
|
const rootId = rootNode.id.toString();
|
|
|
|
data.nodes.forEach(node => {
|
|
if (node.id.toString() !== rootId) {
|
|
elements.push({
|
|
group: 'edges',
|
|
data: {
|
|
id: `${rootId}-${node.id}`,
|
|
source: rootId,
|
|
target: node.id.toString()
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Elemente zu Cytoscape hinzufügen
|
|
cy.elements().remove();
|
|
cy.add(elements);
|
|
|
|
// Layout anwenden
|
|
cy.layout({
|
|
name: 'cose',
|
|
animate: true,
|
|
animationDuration: 800,
|
|
nodeDimensionsIncludeLabels: true,
|
|
padding: 50,
|
|
spacingFactor: 1.5,
|
|
randomize: false,
|
|
fit: true
|
|
}).run();
|
|
|
|
// Nach dem Laden Event auslösen
|
|
document.dispatchEvent(new CustomEvent('mindmap-loaded'));
|
|
})
|
|
.catch(error => {
|
|
console.error('Fehler beim Laden der Mindmap-Daten:', error);
|
|
|
|
// Fallback mit Standard-Daten
|
|
const fallbackData = {
|
|
nodes: [
|
|
{ id: 1, name: 'Wissen', description: 'Zentrale Wissensbasis', color_code: '#4299E1' },
|
|
{ id: 2, name: 'Philosophie', description: 'Philosophisches Denken', color_code: '#9F7AEA' },
|
|
{ id: 3, name: 'Wissenschaft', description: 'Wissenschaftliche Erkenntnisse', color_code: '#48BB78' },
|
|
{ id: 4, name: 'Technologie', description: 'Technologische Entwicklungen', color_code: '#ED8936' },
|
|
{ id: 5, name: 'Künste', description: 'Künstlerische Ausdrucksformen', color_code: '#ED64A6' }
|
|
],
|
|
edges: [
|
|
{ source: 1, target: 2 },
|
|
{ source: 1, target: 3 },
|
|
{ source: 1, target: 4 },
|
|
{ source: 1, target: 5 }
|
|
]
|
|
};
|
|
|
|
const fallbackElements = [];
|
|
|
|
// Knoten hinzufügen
|
|
fallbackData.nodes.forEach(node => {
|
|
fallbackElements.push({
|
|
group: 'nodes',
|
|
data: {
|
|
id: node.id.toString(),
|
|
name: node.name,
|
|
description: node.description || '',
|
|
color: node.color_code || '#8B5CF6',
|
|
isRoot: node.name === 'Wissen'
|
|
}
|
|
});
|
|
});
|
|
|
|
// Kanten hinzufügen
|
|
fallbackData.edges.forEach(edge => {
|
|
fallbackElements.push({
|
|
group: 'edges',
|
|
data: {
|
|
id: `${edge.source}-${edge.target}`,
|
|
source: edge.source.toString(),
|
|
target: edge.target.toString()
|
|
}
|
|
});
|
|
});
|
|
|
|
// Elemente zu Cytoscape hinzufügen
|
|
cy.elements().remove();
|
|
cy.add(fallbackElements);
|
|
|
|
// Layout anwenden
|
|
cy.layout({ name: 'cose', animate: true }).run();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Richtet Event-Listener für die Mindmap ein
|
|
* @param {Object} cy - Cytoscape-Instanz
|
|
* @param {HTMLElement} fitBtn - Fit-Button
|
|
* @param {HTMLElement} resetBtn - Reset-Button
|
|
* @param {HTMLElement} toggleLabelsBtn - Toggle-Labels-Button
|
|
* @param {HTMLElement} nodeInfoPanel - Node-Info-Panel
|
|
* @param {HTMLElement} nodeDescription - Node-Description
|
|
* @param {HTMLElement} connectedNodes - Connected-Nodes-Container
|
|
*/
|
|
function setupEventListeners(cy, fitBtn, resetBtn, toggleLabelsBtn, nodeInfoPanel, nodeDescription, connectedNodes) {
|
|
let labelsVisible = true;
|
|
|
|
// Fit-Button
|
|
if (fitBtn) {
|
|
fitBtn.addEventListener('click', function() {
|
|
cy.fit();
|
|
});
|
|
}
|
|
|
|
// Reset-Button
|
|
if (resetBtn) {
|
|
resetBtn.addEventListener('click', function() {
|
|
cy.layout({ name: 'cose', animate: true }).run();
|
|
});
|
|
}
|
|
|
|
// Toggle-Labels-Button
|
|
if (toggleLabelsBtn) {
|
|
toggleLabelsBtn.addEventListener('click', function() {
|
|
labelsVisible = !labelsVisible;
|
|
cy.style()
|
|
.selector('node')
|
|
.style({
|
|
'text-opacity': labelsVisible ? 1 : 0
|
|
})
|
|
.update();
|
|
});
|
|
}
|
|
|
|
// Knoten-Klick
|
|
cy.on('tap', 'node', function(evt) {
|
|
const node = evt.target;
|
|
|
|
// Zuvor ausgewählten Knoten zurücksetzen
|
|
cy.nodes().removeClass('selected');
|
|
|
|
// Neuen Knoten auswählen
|
|
node.addClass('selected');
|
|
|
|
if (nodeInfoPanel && nodeDescription && connectedNodes) {
|
|
// Info-Panel aktualisieren
|
|
nodeDescription.textContent = node.data('description') || 'Keine Beschreibung verfügbar.';
|
|
|
|
// Verbundene Knoten anzeigen
|
|
connectedNodes.innerHTML = '';
|
|
|
|
// Verbundene Knoten sammeln
|
|
const connectedNodesList = node.neighborhood('node');
|
|
|
|
if (connectedNodesList.length > 0) {
|
|
connectedNodesList.forEach(connectedNode => {
|
|
// Nicht den ausgewählten Knoten selbst anzeigen
|
|
if (connectedNode.id() !== node.id()) {
|
|
const nodeLink = document.createElement('span');
|
|
nodeLink.className = 'node-link';
|
|
nodeLink.textContent = connectedNode.data('name');
|
|
nodeLink.style.backgroundColor = connectedNode.data('color');
|
|
|
|
// Klick-Ereignis, um zu diesem Knoten zu wechseln
|
|
nodeLink.addEventListener('click', function() {
|
|
connectedNode.select();
|
|
cy.animate({
|
|
center: { eles: connectedNode },
|
|
duration: 500,
|
|
easing: 'ease-in-out-cubic'
|
|
});
|
|
});
|
|
|
|
connectedNodes.appendChild(nodeLink);
|
|
}
|
|
});
|
|
} else {
|
|
connectedNodes.innerHTML = '<em>Keine verbundenen Knoten</em>';
|
|
}
|
|
|
|
// Panel anzeigen
|
|
nodeInfoPanel.classList.add('visible');
|
|
}
|
|
});
|
|
|
|
// Hintergrund-Klick
|
|
cy.on('tap', function(evt) {
|
|
if (evt.target === cy) {
|
|
// Klick auf den Hintergrund
|
|
cy.nodes().removeClass('selected');
|
|
|
|
// Info-Panel verstecken
|
|
if (nodeInfoPanel) {
|
|
nodeInfoPanel.classList.remove('visible');
|
|
}
|
|
}
|
|
});
|
|
|
|
// Dark Mode-Änderungen
|
|
document.addEventListener('darkModeToggled', function(event) {
|
|
const isDark = event.detail.isDark;
|
|
cy.style(getDefaultStyles(isDark));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Liefert die Standard-Stile für die Mindmap
|
|
* @param {boolean} darkMode - Ob der Dark Mode aktiv ist
|
|
* @returns {Array} Array von Cytoscape-Stilen
|
|
*/
|
|
function getDefaultStyles(darkMode = document.documentElement.classList.contains('dark')) {
|
|
return [
|
|
{
|
|
selector: 'node',
|
|
style: {
|
|
'background-color': 'data(color)',
|
|
'label': 'data(name)',
|
|
'width': 40,
|
|
'height': 40,
|
|
'font-size': 12,
|
|
'text-valign': 'bottom',
|
|
'text-halign': 'center',
|
|
'text-margin-y': 8,
|
|
'color': darkMode ? '#f1f5f9' : '#334155',
|
|
'text-background-color': darkMode ? 'rgba(30, 41, 59, 0.8)' : 'rgba(241, 245, 249, 0.8)',
|
|
'text-background-opacity': 0.8,
|
|
'text-background-padding': '2px',
|
|
'text-background-shape': 'roundrectangle',
|
|
'text-wrap': 'ellipsis',
|
|
'text-max-width': '100px'
|
|
}
|
|
},
|
|
{
|
|
selector: 'node[?isRoot]',
|
|
style: {
|
|
'width': 60,
|
|
'height': 60,
|
|
'font-size': 14,
|
|
'font-weight': 'bold',
|
|
'text-background-opacity': 0.9,
|
|
'text-background-color': '#4299E1'
|
|
}
|
|
},
|
|
{
|
|
selector: 'edge',
|
|
style: {
|
|
'width': 2,
|
|
'line-color': darkMode ? 'rgba(255, 255, 255, 0.15)' : 'rgba(30, 41, 59, 0.15)',
|
|
'target-arrow-color': darkMode ? 'rgba(255, 255, 255, 0.15)' : 'rgba(30, 41, 59, 0.15)',
|
|
'curve-style': 'bezier',
|
|
'target-arrow-shape': 'triangle'
|
|
}
|
|
},
|
|
{
|
|
selector: 'node.selected',
|
|
style: {
|
|
'background-color': 'data(color)',
|
|
'border-width': 3,
|
|
'border-color': '#8b5cf6',
|
|
'width': 50,
|
|
'height': 50,
|
|
'font-size': 14,
|
|
'font-weight': 'bold',
|
|
'text-background-color': '#8b5cf6',
|
|
'text-background-opacity': 0.9
|
|
}
|
|
}
|
|
];
|
|
}
|
|
|
|
class MindMap {
|
|
constructor(selector, options = {}) {
|
|
// Standardoptionen festlegen
|
|
this.options = Object.assign({
|
|
// Standard-Basisoptionen
|
|
editable: false, // Ist die Mindmap editierbar?
|
|
isUserLoggedIn: false, // Ist der Benutzer angemeldet?
|
|
isPublicMap: true, // Handelt es sich um die öffentliche Mindmap?
|
|
initialZoom: 1, // Anfängliche Zoom-Stufe
|
|
fitViewOnInit: true, // Passt die Ansicht automatisch an
|
|
minZoom: 0.2, // Minimale Zoom-Stufe
|
|
maxZoom: 3, // Maximale Zoom-Stufe
|
|
// Farbpalette für verschiedene Elemente
|
|
colorPalette: {
|
|
node: {
|
|
default: '#8b5cf6', // Standard-Knotenfarbe
|
|
selected: '#7c3aed', // Farbe für ausgewählte Knoten
|
|
hover: '#6d28d9' // Farbe für Hover-Effekt
|
|
},
|
|
edge: {
|
|
default: '#8b5cf6', // Standard-Kantenfarbe
|
|
selected: '#7c3aed', // Farbe für ausgewählte Kanten
|
|
hover: '#6d28d9' // Farbe für Hover-Effekt
|
|
},
|
|
text: {
|
|
default: '#f3f4f6', // Standard-Textfarbe
|
|
dark: '#1f2937', // Dunkle Textfarbe (für helle Hintergründe)
|
|
light: '#f9fafb' // Helle Textfarbe (für dunkle Hintergründe)
|
|
},
|
|
background: {
|
|
dark: '#111827', // Dunkler Hintergrund
|
|
light: '#f9fafb' // Heller Hintergrund
|
|
}
|
|
},
|
|
// Anpassbare Funktionen
|
|
callbacks: {
|
|
onNodeClick: null, // Callback für Knotenklick
|
|
onEdgeClick: null, // Callback für Kantenklick
|
|
onViewportChange: null, // Callback für Änderung der Ansicht
|
|
onSelectionChange: null, // Callback für Änderung der Auswahl
|
|
onLoad: null // Callback nach dem Laden der Mindmap
|
|
}
|
|
}, options);
|
|
|
|
// Container-Element
|
|
this.container = document.querySelector(selector);
|
|
if (!this.container) {
|
|
console.error(`Container mit Selektor '${selector}' nicht gefunden`);
|
|
return;
|
|
}
|
|
|
|
// Prüfen, ob der Benutzer angemeldet ist
|
|
this.isUserLoggedIn = document.body.classList.contains('user-logged-in') || this.options.isUserLoggedIn;
|
|
|
|
// Wenn der Benutzer angemeldet ist und es sich um die öffentliche Mindmap handelt,
|
|
// wird die Editierbarkeit aktiviert
|
|
if (this.isUserLoggedIn && this.options.isPublicMap) {
|
|
this.options.editable = true;
|
|
}
|
|
|
|
// Initialisiere Cytoscape
|
|
this.initCytoscape();
|
|
|
|
// Füge Bearbeiten-Funktionalität hinzu, wenn editierbar
|
|
if (this.options.editable) {
|
|
this.initEditableMode();
|
|
}
|
|
|
|
// Lade Daten basierend auf dem Typ der Mindmap
|
|
if (this.options.isPublicMap) {
|
|
this.loadPublicMindmap();
|
|
} else if (this.options.userMindmapId) {
|
|
this.loadUserMindmap(this.options.userMindmapId);
|
|
}
|
|
|
|
// Initialisiere UI-Elemente
|
|
this.initUI();
|
|
|
|
// Event-Listener-Initialisierung
|
|
this.initEventListeners();
|
|
|
|
// Flash-Message-System
|
|
this.flashContainer = null;
|
|
this.initFlashMessages();
|
|
}
|
|
|
|
// ... existing code ...
|
|
|
|
// Initialisiert die Bearbeitungsmodi für die Mindmap
|
|
initEditableMode() {
|
|
// Toolbar für Bearbeitungsmodus hinzufügen
|
|
this.addEditToolbar();
|
|
|
|
// Kontextmenü-Funktionalität für Knoten
|
|
this.cy.on('cxttap', 'node', (evt) => {
|
|
const node = evt.target;
|
|
const position = evt.renderedPosition;
|
|
|
|
// Zeige Kontextmenü
|
|
this.showNodeContextMenu(node, position);
|
|
});
|
|
|
|
// Doppelklick-Funktion für das Hinzufügen neuer Knoten
|
|
this.cy.on('dblclick', (evt) => {
|
|
// Nur auf Hintergrund reagieren, nicht auf Knoten
|
|
if (evt.target === this.cy) {
|
|
const position = evt.position;
|
|
this.showAddNodeDialog(position);
|
|
}
|
|
});
|
|
|
|
// Kontextmenü-Schließen bei Klick außerhalb
|
|
document.addEventListener('click', (e) => {
|
|
const contextMenu = document.getElementById('context-menu');
|
|
if (contextMenu && !contextMenu.contains(e.target)) {
|
|
contextMenu.remove();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Zeigt ein Kontextmenü für einen Knoten an
|
|
showNodeContextMenu(node, position) {
|
|
// Entferne vorhandenes Kontextmenü
|
|
const existingMenu = document.getElementById('context-menu');
|
|
if (existingMenu) existingMenu.remove();
|
|
|
|
// Erstelle neues Kontextmenü
|
|
const contextMenu = document.createElement('div');
|
|
contextMenu.id = 'context-menu';
|
|
contextMenu.style.position = 'absolute';
|
|
contextMenu.style.left = `${position.x}px`;
|
|
contextMenu.style.top = `${position.y}px`;
|
|
contextMenu.style.zIndex = '1000';
|
|
|
|
// Menü-Elemente
|
|
const menuItems = [
|
|
{ label: 'Gedanken anzeigen', icon: 'fas fa-brain', action: () => this.showThoughtsForNode(node) },
|
|
{ label: 'Neuen Gedanken erstellen', icon: 'fas fa-plus-circle', action: () => this.createThoughtForNode(node) },
|
|
{ label: 'Notiz hinzufügen', icon: 'fas fa-sticky-note', action: () => this.addNoteToNode(node) },
|
|
{ label: 'Knoten bearbeiten', icon: 'fas fa-edit', action: () => this.editNode(node) },
|
|
{ label: 'Verbindung erstellen', icon: 'fas fa-link', action: () => this.startEdgeCreation(node) },
|
|
{ label: 'Aus Mindmap entfernen', icon: 'fas fa-trash-alt', action: () => this.removeNodeFromMap(node) }
|
|
];
|
|
|
|
// Menü erstellen
|
|
menuItems.forEach(item => {
|
|
const menuItem = document.createElement('div');
|
|
menuItem.className = 'menu-item';
|
|
menuItem.innerHTML = `<i class="${item.icon} mr-2"></i> ${item.label}`;
|
|
menuItem.addEventListener('click', () => {
|
|
item.action();
|
|
contextMenu.remove();
|
|
});
|
|
contextMenu.appendChild(menuItem);
|
|
});
|
|
|
|
// Anhängen an Body (außerhalb des Cytoscape-Containers)
|
|
document.body.appendChild(contextMenu);
|
|
|
|
// Stellen Sie sicher, dass das Kontextmenü vollständig sichtbar ist
|
|
const menuRect = contextMenu.getBoundingClientRect();
|
|
const windowWidth = window.innerWidth;
|
|
const windowHeight = window.innerHeight;
|
|
|
|
if (menuRect.right > windowWidth) {
|
|
contextMenu.style.left = `${position.x - menuRect.width}px`;
|
|
}
|
|
|
|
if (menuRect.bottom > windowHeight) {
|
|
contextMenu.style.top = `${position.y - menuRect.height}px`;
|
|
}
|
|
}
|
|
|
|
// Fügt die Edit-Toolbar zur Mindmap hinzu
|
|
addEditToolbar() {
|
|
const toolbar = document.querySelector('.mindmap-toolbar');
|
|
if (!toolbar) return;
|
|
|
|
// Trennlinie
|
|
const separator = document.createElement('div');
|
|
separator.className = 'border-l h-6 mx-2 opacity-20';
|
|
toolbar.appendChild(separator);
|
|
|
|
// Bearbeiten-Buttons
|
|
const editButtons = [
|
|
{ id: 'add-node-btn', icon: 'fa-plus', text: 'Knoten hinzufügen', action: () => this.showAddNodeDialog() },
|
|
{ id: 'save-layout-btn', icon: 'fa-save', text: 'Layout speichern', action: () => this.saveCurrentLayout() }
|
|
];
|
|
|
|
editButtons.forEach(btn => {
|
|
const button = document.createElement('button');
|
|
button.id = btn.id;
|
|
button.className = 'mindmap-action-btn edit-btn';
|
|
button.innerHTML = `<i class="fas ${btn.icon}"></i><span>${btn.text}</span>`;
|
|
button.addEventListener('click', btn.action);
|
|
toolbar.appendChild(button);
|
|
});
|
|
|
|
// Animation für die Edit-Buttons
|
|
const editBtns = document.querySelectorAll('.edit-btn');
|
|
editBtns.forEach(btn => {
|
|
btn.style.opacity = '0';
|
|
btn.style.transform = 'translateY(10px)';
|
|
|
|
setTimeout(() => {
|
|
btn.style.transition = 'all 0.3s ease';
|
|
btn.style.opacity = '1';
|
|
btn.style.transform = 'translateY(0)';
|
|
}, 100);
|
|
});
|
|
}
|
|
|
|
// Zeigt den Dialog zum Hinzufügen eines neuen Knotens
|
|
showAddNodeDialog(position = null) {
|
|
// Entferne bestehende Dialoge
|
|
const existingDialog = document.getElementById('add-node-dialog');
|
|
if (existingDialog) existingDialog.remove();
|
|
|
|
// Erstelle Dialog
|
|
const dialog = document.createElement('div');
|
|
dialog.id = 'add-node-dialog';
|
|
dialog.className = 'fixed inset-0 flex items-center justify-center z-50';
|
|
dialog.innerHTML = `
|
|
<div class="fixed inset-0 bg-black bg-opacity-50"></div>
|
|
<div class="bg-slate-800 rounded-lg p-6 w-full max-w-md relative z-10 border border-purple-500/20">
|
|
<h3 class="text-xl font-semibold mb-4 text-white">Neuen Knoten hinzufügen</h3>
|
|
<form id="add-node-form">
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-300 mb-1">Name</label>
|
|
<input type="text" id="node-name" class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-md text-white">
|
|
</div>
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-300 mb-1">Beschreibung</label>
|
|
<textarea id="node-description" class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-md text-white resize-none h-24"></textarea>
|
|
</div>
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-300 mb-1">Farbe</label>
|
|
<input type="color" id="node-color" class="w-full h-10 border border-slate-700 rounded-md bg-slate-900" value="#8B5CF6">
|
|
</div>
|
|
<div class="flex justify-end gap-3 mt-6">
|
|
<button type="button" id="cancel-add-node" class="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded-md transition-colors">Abbrechen</button>
|
|
<button type="submit" class="px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-md transition-colors">Hinzufügen</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
`;
|
|
|
|
// Füge Dialog zum Body hinzu
|
|
document.body.appendChild(dialog);
|
|
|
|
// Animation für den Dialog
|
|
const dialogContent = dialog.querySelector('.bg-slate-800');
|
|
dialogContent.style.opacity = '0';
|
|
dialogContent.style.transform = 'scale(0.9)';
|
|
setTimeout(() => {
|
|
dialogContent.style.transition = 'all 0.3s ease';
|
|
dialogContent.style.opacity = '1';
|
|
dialogContent.style.transform = 'scale(1)';
|
|
}, 10);
|
|
|
|
// Event-Listener für Abbrechen
|
|
document.getElementById('cancel-add-node').addEventListener('click', () => {
|
|
// Animation für das Schließen
|
|
dialogContent.style.opacity = '0';
|
|
dialogContent.style.transform = 'scale(0.9)';
|
|
setTimeout(() => dialog.remove(), 300);
|
|
});
|
|
|
|
// Event-Listener für Overlay-Klick
|
|
dialog.querySelector('.fixed.inset-0.bg-black').addEventListener('click', () => {
|
|
// Animation für das Schließen
|
|
dialogContent.style.opacity = '0';
|
|
dialogContent.style.transform = 'scale(0.9)';
|
|
setTimeout(() => dialog.remove(), 300);
|
|
});
|
|
|
|
// Event-Listener für Formular
|
|
document.getElementById('add-node-form').addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
|
|
const name = document.getElementById('node-name').value;
|
|
const description = document.getElementById('node-description').value;
|
|
const color = document.getElementById('node-color').value;
|
|
|
|
if (!name.trim()) {
|
|
this.showFlash('Bitte geben Sie einen Namen für den Knoten ein', 'error');
|
|
return;
|
|
}
|
|
|
|
// Knoten hinzufügen
|
|
this.addNodeToMindmap({
|
|
name,
|
|
description,
|
|
color_code: color,
|
|
position
|
|
});
|
|
|
|
// Dialog schließen
|
|
dialogContent.style.opacity = '0';
|
|
dialogContent.style.transform = 'scale(0.9)';
|
|
setTimeout(() => dialog.remove(), 300);
|
|
});
|
|
|
|
// Fokus auf das Name-Feld
|
|
setTimeout(() => document.getElementById('node-name').focus(), 100);
|
|
}
|
|
|
|
// Fügt einen neuen Knoten zur Mindmap hinzu
|
|
addNodeToMindmap(nodeData) {
|
|
// API-Anfrage vorbereiten
|
|
const data = {
|
|
name: nodeData.name,
|
|
description: nodeData.description || '',
|
|
color_code: nodeData.color_code || '#8b5cf6'
|
|
};
|
|
|
|
// Position hinzufügen, falls vorhanden
|
|
if (nodeData.position) {
|
|
data.x_position = nodeData.position.x;
|
|
data.y_position = nodeData.position.y;
|
|
}
|
|
|
|
// API-Anfrage zum Hinzufügen des Knotens
|
|
fetch('/api/mindmap/public/add_node', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(data)
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Netzwerkantwort war nicht ok');
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
if (data.success) {
|
|
// Hinzufügen des neuen Knotens zum Graphen
|
|
this.cy.add({
|
|
group: 'nodes',
|
|
data: {
|
|
id: `node_${data.node_id}`,
|
|
name: nodeData.name,
|
|
description: nodeData.description || '',
|
|
color: nodeData.color_code
|
|
},
|
|
position: nodeData.position || { x: 0, y: 0 }
|
|
});
|
|
|
|
// Flash-Nachricht anzeigen
|
|
this.showFlash('Knoten erfolgreich hinzugefügt', 'success');
|
|
|
|
// Ansicht anpassen
|
|
this.cy.fit();
|
|
} else {
|
|
this.showFlash('Fehler beim Hinzufügen des Knotens: ' + (data.error || 'Unbekannter Fehler'), 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Fehler beim Hinzufügen des Knotens:', error);
|
|
this.showFlash('Fehler beim Hinzufügen des Knotens', 'error');
|
|
});
|
|
}
|
|
|
|
// Entfernt einen Knoten aus der Mindmap
|
|
removeNodeFromMap(node) {
|
|
if (!confirm('Möchten Sie diesen Knoten wirklich aus der Mindmap entfernen?')) {
|
|
return;
|
|
}
|
|
|
|
const nodeId = node.id().split('_')[1]; // "node_123" => "123"
|
|
|
|
// API-Anfrage zum Entfernen des Knotens
|
|
fetch(`/api/mindmap/public/remove_node/${nodeId}`, {
|
|
method: 'DELETE'
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Netzwerkantwort war nicht ok');
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
if (data.success) {
|
|
// Knoten aus dem Graphen entfernen
|
|
this.cy.remove(node);
|
|
|
|
// Flash-Nachricht anzeigen
|
|
this.showFlash('Knoten erfolgreich entfernt', 'success');
|
|
} else {
|
|
this.showFlash('Fehler beim Entfernen des Knotens: ' + (data.error || 'Unbekannter Fehler'), 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Fehler beim Entfernen des Knotens:', error);
|
|
this.showFlash('Fehler beim Entfernen des Knotens', 'error');
|
|
});
|
|
}
|
|
|
|
// Speichert das aktuelle Layout der Mindmap
|
|
saveCurrentLayout() {
|
|
// Sammle alle Knotenpositionen
|
|
const nodePositions = [];
|
|
this.cy.nodes().forEach(node => {
|
|
const idParts = node.id().split('_');
|
|
if (idParts.length === 2 && idParts[0] === 'node') {
|
|
nodePositions.push({
|
|
node_id: parseInt(idParts[1]),
|
|
x_position: node.position('x'),
|
|
y_position: node.position('y')
|
|
});
|
|
}
|
|
});
|
|
|
|
// API-Anfrage zum Speichern des Layouts
|
|
fetch('/api/mindmap/public/update_layout', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ positions: nodePositions })
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Netzwerkantwort war nicht ok');
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
if (data.success) {
|
|
this.showFlash('Layout erfolgreich gespeichert', 'success');
|
|
} else {
|
|
this.showFlash('Fehler beim Speichern des Layouts: ' + (data.error || 'Unbekannter Fehler'), 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Fehler beim Speichern des Layouts:', error);
|
|
this.showFlash('Fehler beim Speichern des Layouts', 'error');
|
|
});
|
|
}
|
|
|
|
// Zeigt den Dialog zum Bearbeiten eines Knotens
|
|
editNode(node) {
|
|
// Entferne bestehende Dialoge
|
|
const existingDialog = document.getElementById('edit-node-dialog');
|
|
if (existingDialog) existingDialog.remove();
|
|
|
|
// Knotendaten holen
|
|
const nodeData = node.data();
|
|
|
|
// Erstelle Dialog
|
|
const dialog = document.createElement('div');
|
|
dialog.id = 'edit-node-dialog';
|
|
dialog.className = 'fixed inset-0 flex items-center justify-center z-50';
|
|
dialog.innerHTML = `
|
|
<div class="fixed inset-0 bg-black bg-opacity-50"></div>
|
|
<div class="bg-slate-800 rounded-lg p-6 w-full max-w-md relative z-10 border border-purple-500/20">
|
|
<h3 class="text-xl font-semibold mb-4 text-white">Knoten bearbeiten</h3>
|
|
<form id="edit-node-form">
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-300 mb-1">Name</label>
|
|
<input type="text" id="node-name" class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-md text-white" value="${nodeData.name}">
|
|
</div>
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-300 mb-1">Beschreibung</label>
|
|
<textarea id="node-description" class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-md text-white resize-none h-24">${nodeData.description || ''}</textarea>
|
|
</div>
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-300 mb-1">Farbe</label>
|
|
<input type="color" id="node-color" class="w-full h-10 border border-slate-700 rounded-md bg-slate-900" value="${nodeData.color || '#8B5CF6'}">
|
|
</div>
|
|
<div class="flex justify-end gap-3 mt-6">
|
|
<button type="button" id="cancel-edit-node" class="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded-md transition-colors">Abbrechen</button>
|
|
<button type="submit" class="px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-md transition-colors">Speichern</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
`;
|
|
|
|
// Füge Dialog zum Body hinzu
|
|
document.body.appendChild(dialog);
|
|
|
|
// Animation für den Dialog
|
|
const dialogContent = dialog.querySelector('.bg-slate-800');
|
|
dialogContent.style.opacity = '0';
|
|
dialogContent.style.transform = 'scale(0.9)';
|
|
setTimeout(() => {
|
|
dialogContent.style.transition = 'all 0.3s ease';
|
|
dialogContent.style.opacity = '1';
|
|
dialogContent.style.transform = 'scale(1)';
|
|
}, 10);
|
|
|
|
// Event-Listener für Abbrechen
|
|
document.getElementById('cancel-edit-node').addEventListener('click', () => {
|
|
// Animation für das Schließen
|
|
dialogContent.style.opacity = '0';
|
|
dialogContent.style.transform = 'scale(0.9)';
|
|
setTimeout(() => dialog.remove(), 300);
|
|
});
|
|
|
|
// Event-Listener für Overlay-Klick
|
|
dialog.querySelector('.fixed.inset-0.bg-black').addEventListener('click', () => {
|
|
// Animation für das Schließen
|
|
dialogContent.style.opacity = '0';
|
|
dialogContent.style.transform = 'scale(0.9)';
|
|
setTimeout(() => dialog.remove(), 300);
|
|
});
|
|
|
|
// Event-Listener für Formular
|
|
document.getElementById('edit-node-form').addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
|
|
const name = document.getElementById('node-name').value;
|
|
const description = document.getElementById('node-description').value;
|
|
const color = document.getElementById('node-color').value;
|
|
|
|
if (!name.trim()) {
|
|
this.showFlash('Bitte geben Sie einen Namen für den Knoten ein', 'error');
|
|
return;
|
|
}
|
|
|
|
// Knoten aktualisieren
|
|
this.updateNode(node, {
|
|
name,
|
|
description,
|
|
color_code: color
|
|
});
|
|
|
|
// Dialog schließen
|
|
dialogContent.style.opacity = '0';
|
|
dialogContent.style.transform = 'scale(0.9)';
|
|
setTimeout(() => dialog.remove(), 300);
|
|
});
|
|
|
|
// Fokus auf das Name-Feld
|
|
setTimeout(() => document.getElementById('node-name').focus(), 100);
|
|
}
|
|
|
|
// Aktualisiert einen Knoten in der Mindmap
|
|
updateNode(node, nodeData) {
|
|
const nodeId = node.id().split('_')[1]; // "node_123" => "123"
|
|
|
|
// API-Anfrage zum Aktualisieren des Knotens
|
|
fetch(`/api/mindmap/public/update_node/${nodeId}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
name: nodeData.name,
|
|
description: nodeData.description,
|
|
color_code: nodeData.color_code
|
|
})
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Netzwerkantwort war nicht ok');
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
if (data.success) {
|
|
// Aktualisiere Knotendaten im Graph
|
|
node.data('name', nodeData.name);
|
|
node.data('description', nodeData.description);
|
|
node.data('color', nodeData.color_code);
|
|
|
|
// Aktualisiere Darstellung
|
|
this.cy.style()
|
|
.selector(`#${node.id()}`)
|
|
.style({
|
|
'background-color': nodeData.color_code
|
|
})
|
|
.update();
|
|
|
|
// Flash-Nachricht anzeigen
|
|
this.showFlash('Knoten erfolgreich aktualisiert', 'success');
|
|
} else {
|
|
this.showFlash('Fehler beim Aktualisieren des Knotens: ' + (data.error || 'Unbekannter Fehler'), 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Fehler beim Aktualisieren des Knotens:', error);
|
|
this.showFlash('Fehler beim Aktualisieren des Knotens', 'error');
|
|
});
|
|
}
|
|
|
|
// Fügt eine Notiz zu einem Knoten hinzu
|
|
addNoteToNode(node) {
|
|
// Implementierung für das Hinzufügen von Notizen
|
|
// Ähnlich wie bei editNode, aber mit anderen Feldern
|
|
}
|
|
|
|
// Startet den Prozess zum Erstellen einer Verbindung
|
|
startEdgeCreation(node) {
|
|
// Implementierung für das Erstellen von Verbindungen
|
|
}
|
|
|
|
// Zeigt Gedanken für einen Knoten an
|
|
showThoughtsForNode(node) {
|
|
const nodeId = node.id().split('_')[1]; // "node_123" => "123"
|
|
|
|
// Wir verwenden die fetchThoughtsForNode-Methode, die wir bereits implementiert haben
|
|
this.fetchThoughtsForNode(nodeId)
|
|
.then(thoughts => {
|
|
// Nur fortfahren, wenn wir tatsächlich Gedanken haben
|
|
if (thoughts.length === 0) {
|
|
this.showFlash('Keine Gedanken für diesen Knoten gefunden', 'info');
|
|
return;
|
|
}
|
|
|
|
// Erstelle einen Gedanken-Viewer, wenn er nicht existiert
|
|
let thoughtsViewer = document.getElementById('thoughts-viewer');
|
|
if (!thoughtsViewer) {
|
|
thoughtsViewer = document.createElement('div');
|
|
thoughtsViewer.id = 'thoughts-viewer';
|
|
thoughtsViewer.className = 'fixed inset-0 flex items-center justify-center z-50';
|
|
thoughtsViewer.innerHTML = `
|
|
<div class="fixed inset-0 bg-black bg-opacity-50 thoughts-backdrop"></div>
|
|
<div class="bg-slate-800 rounded-lg w-full max-w-3xl mx-4 relative z-10 border border-purple-500/20 thoughts-content overflow-hidden">
|
|
<div class="flex justify-between items-center p-4 border-b border-gray-700">
|
|
<h3 class="text-xl font-semibold text-white">Gedanken zu: <span class="node-name"></span></h3>
|
|
<button id="close-thoughts" class="text-gray-400 hover:text-white">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
<div class="p-4 thoughts-container max-h-[70vh] overflow-y-auto"></div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(thoughtsViewer);
|
|
|
|
// Event-Listener für Schließen-Button
|
|
document.getElementById('close-thoughts').addEventListener('click', () => {
|
|
// Animation für das Schließen
|
|
const content = thoughtsViewer.querySelector('.thoughts-content');
|
|
const backdrop = thoughtsViewer.querySelector('.thoughts-backdrop');
|
|
|
|
content.style.transform = 'scale(0.9)';
|
|
content.style.opacity = '0';
|
|
backdrop.style.opacity = '0';
|
|
|
|
setTimeout(() => thoughtsViewer.remove(), 300);
|
|
});
|
|
|
|
// Event-Listener für Backdrop-Klick
|
|
thoughtsViewer.querySelector('.thoughts-backdrop').addEventListener('click', () => {
|
|
document.getElementById('close-thoughts').click();
|
|
});
|
|
}
|
|
|
|
// Aktualisiere den Titel mit dem Knotennamen
|
|
thoughtsViewer.querySelector('.node-name').textContent = node.data('name');
|
|
|
|
// Container für die Gedanken
|
|
const thoughtsContainer = thoughtsViewer.querySelector('.thoughts-container');
|
|
thoughtsContainer.innerHTML = '';
|
|
|
|
// Gedanken rendern
|
|
this.renderThoughts(thoughts, thoughtsContainer);
|
|
|
|
// Animation für das Öffnen
|
|
const content = thoughtsViewer.querySelector('.thoughts-content');
|
|
const backdrop = thoughtsViewer.querySelector('.thoughts-backdrop');
|
|
|
|
content.style.transform = 'scale(0.9)';
|
|
content.style.opacity = '0';
|
|
backdrop.style.opacity = '0';
|
|
|
|
setTimeout(() => {
|
|
content.style.transition = 'all 0.3s ease';
|
|
backdrop.style.transition = 'opacity 0.3s ease';
|
|
content.style.transform = 'scale(1)';
|
|
content.style.opacity = '1';
|
|
backdrop.style.opacity = '1';
|
|
}, 10);
|
|
});
|
|
}
|
|
|
|
// Rendert die Gedanken in der UI mit Animationen
|
|
renderThoughts(thoughts, container) {
|
|
if (!container) return;
|
|
|
|
container.innerHTML = '';
|
|
|
|
// Animation delay counter
|
|
let delay = 0;
|
|
|
|
thoughts.forEach(thought => {
|
|
const thoughtCard = document.createElement('div');
|
|
thoughtCard.className = 'thought-card mb-4 bg-slate-700/50 rounded-lg overflow-hidden border border-slate-600/50 transition-all duration-300 hover:border-purple-500/30';
|
|
thoughtCard.setAttribute('data-id', thought.id);
|
|
thoughtCard.style.opacity = '0';
|
|
thoughtCard.style.transform = 'translateY(20px)';
|
|
|
|
const cardColor = thought.color_code || this.colorPalette.default;
|
|
|
|
thoughtCard.innerHTML = `
|
|
<div class="thought-card-header p-4" style="border-left: 4px solid ${cardColor}">
|
|
<h3 class="thought-title text-lg font-semibold text-white">${thought.title}</h3>
|
|
<div class="thought-meta flex gap-3 text-sm text-gray-400 mt-1">
|
|
<span class="thought-date"><i class="far fa-calendar-alt mr-1"></i>${new Date(thought.created_at).toLocaleDateString('de-DE')}</span>
|
|
${thought.author ? `<span class="thought-author"><i class="far fa-user mr-1"></i>${thought.author.username}</span>` : ''}
|
|
</div>
|
|
</div>
|
|
<div class="thought-content p-4 text-gray-200">
|
|
<p>${thought.abstract || thought.content.substring(0, 150) + '...'}</p>
|
|
</div>
|
|
<div class="thought-footer p-4 pt-0 flex justify-between items-center">
|
|
<div class="thought-keywords flex flex-wrap gap-1">
|
|
${thought.keywords ? thought.keywords.split(',').map(kw =>
|
|
`<span class="keyword text-xs px-2 py-1 bg-purple-800/30 text-purple-200 rounded-full">${kw.trim()}</span>`).join('') : ''}
|
|
</div>
|
|
<a href="/thoughts/${thought.id}" class="thought-link text-purple-400 hover:text-purple-300 text-sm flex items-center">
|
|
Mehr lesen <i class="fas fa-arrow-right ml-1"></i>
|
|
</a>
|
|
</div>
|
|
`;
|
|
|
|
// Animation-Effekt mit Verzögerung für jede Karte
|
|
setTimeout(() => {
|
|
thoughtCard.style.transition = 'all 0.5s ease';
|
|
thoughtCard.style.opacity = '1';
|
|
thoughtCard.style.transform = 'translateY(0)';
|
|
}, delay);
|
|
delay += 100; // Jede Karte erscheint mit 100ms Verzögerung
|
|
|
|
// Event-Listener für Klick auf Gedanken
|
|
thoughtCard.addEventListener('click', (e) => {
|
|
// Verhindern, dass der Link-Klick den Kartenklick auslöst
|
|
if (e.target.tagName === 'A' || e.target.closest('a')) return;
|
|
window.location.href = `/thoughts/${thought.id}`;
|
|
});
|
|
|
|
// Hover-Animation für Karten
|
|
thoughtCard.addEventListener('mouseenter', () => {
|
|
thoughtCard.style.transform = 'translateY(-5px)';
|
|
thoughtCard.style.boxShadow = '0 10px 25px rgba(0, 0, 0, 0.2)';
|
|
});
|
|
|
|
thoughtCard.addEventListener('mouseleave', () => {
|
|
thoughtCard.style.transform = 'translateY(0)';
|
|
thoughtCard.style.boxShadow = 'none';
|
|
});
|
|
|
|
container.appendChild(thoughtCard);
|
|
});
|
|
}
|
|
|
|
// Flash-Nachrichten mit Animationen
|
|
showFlash(message, type = 'info') {
|
|
if (!this.flashContainer) {
|
|
this.flashContainer = document.createElement('div');
|
|
this.flashContainer.className = 'fixed top-4 right-4 z-50 flex flex-col gap-2';
|
|
document.body.appendChild(this.flashContainer);
|
|
}
|
|
|
|
const flash = document.createElement('div');
|
|
flash.className = `flash-message p-3 rounded-lg shadow-lg flex items-center gap-3 max-w-xs text-white`;
|
|
|
|
// Verschiedene Stile je nach Typ
|
|
let backgroundColor, icon;
|
|
switch (type) {
|
|
case 'success':
|
|
backgroundColor = 'bg-green-500';
|
|
icon = 'fa-check-circle';
|
|
break;
|
|
case 'error':
|
|
backgroundColor = 'bg-red-500';
|
|
icon = 'fa-exclamation-circle';
|
|
break;
|
|
case 'warning':
|
|
backgroundColor = 'bg-yellow-500';
|
|
icon = 'fa-exclamation-triangle';
|
|
break;
|
|
default:
|
|
backgroundColor = 'bg-blue-500';
|
|
icon = 'fa-info-circle';
|
|
}
|
|
|
|
flash.classList.add(backgroundColor);
|
|
|
|
flash.innerHTML = `
|
|
<div class="flash-icon"><i class="fas ${icon} text-lg"></i></div>
|
|
<div class="flash-text">${message}</div>
|
|
<button class="flash-close ml-auto text-white/80 hover:text-white"><i class="fas fa-times"></i></button>
|
|
`;
|
|
|
|
// Füge den Flash zum Container hinzu
|
|
this.flashContainer.appendChild(flash);
|
|
|
|
// Einblend-Animation
|
|
flash.style.opacity = '0';
|
|
flash.style.transform = 'translateX(20px)';
|
|
|
|
setTimeout(() => {
|
|
flash.style.transition = 'all 0.3s ease';
|
|
flash.style.opacity = '1';
|
|
flash.style.transform = 'translateX(0)';
|
|
}, 10);
|
|
|
|
// Schließen-Button
|
|
const closeBtn = flash.querySelector('.flash-close');
|
|
closeBtn.addEventListener('click', () => {
|
|
// Ausblend-Animation
|
|
flash.style.opacity = '0';
|
|
flash.style.transform = 'translateX(20px)';
|
|
|
|
setTimeout(() => {
|
|
flash.remove();
|
|
}, 300);
|
|
});
|
|
|
|
// Automatisches Ausblenden nach 5 Sekunden
|
|
setTimeout(() => {
|
|
if (flash.parentNode) {
|
|
flash.style.opacity = '0';
|
|
flash.style.transform = 'translateX(20px)';
|
|
|
|
setTimeout(() => {
|
|
if (flash.parentNode) flash.remove();
|
|
}, 300);
|
|
}
|
|
}, 5000);
|
|
|
|
return flash;
|
|
}
|
|
}
|