440 lines
28 KiB
JavaScript
440 lines
28 KiB
JavaScript
/**
|
||
* Mindmap.js - Modul für die Mindmap-Visualisierung mit Cytoscape.js
|
||
* Version: 1.0.0
|
||
* Datum: 01.05.2025
|
||
*/
|
||
|
||
// Stellen Sie sicher, dass das globale MindMap-Objekt existiert
|
||
if (!window.MindMap) {
|
||
window.MindMap = {};
|
||
}
|
||
|
||
/**
|
||
* Mindmap-Visualisierungsklasse
|
||
*/
|
||
class MindmapVisualization {
|
||
/**
|
||
* Konstruktor für die Mindmap-Visualisierung
|
||
* @param {string} containerId - ID des Container-Elements
|
||
* @param {Object} options - Konfigurationsoptionen
|
||
*/
|
||
constructor(containerId, options = {}) {
|
||
this.containerId = containerId;
|
||
this.container = document.getElementById(containerId);
|
||
this.cy = null;
|
||
this.data = null;
|
||
this.options = Object.assign({
|
||
defaultLayout: 'cose-bilkent',
|
||
darkMode: document.documentElement.classList.contains('dark'),
|
||
apiEndpoint: '/api/mindmap',
|
||
onNodeClick: null,
|
||
enableEditing: true,
|
||
enableExport: true
|
||
}, options);
|
||
|
||
// Event-Listener für Dark Mode-Änderungen
|
||
document.addEventListener('darkModeToggled', (event) => {
|
||
this.options.darkMode = event.detail.isDark;
|
||
if (this.cy) {
|
||
this.updateStyles();
|
||
}
|
||
});
|
||
|
||
// Toolbar für Bearbeitungswerkzeuge
|
||
this.initToolbar();
|
||
}
|
||
|
||
/**
|
||
* Initialisiert die Toolbar mit Bearbeitungswerkzeugen
|
||
*/
|
||
initToolbar() {
|
||
if (!this.options.enableEditing) return;
|
||
|
||
const toolbar = document.createElement('div');
|
||
toolbar.className = 'mindmap-toolbar';
|
||
toolbar.innerHTML = `
|
||
<button class="add-node" title="Neuen Knoten hinzufügen">
|
||
<i class="fas fa-plus"></i>
|
||
</button>
|
||
<button class="add-child" title="Unterknoten hinzufügen">
|
||
<i class="fas fa-sitemap"></i>
|
||
</button>
|
||
<button class="connect-nodes" title="Knoten verbinden">
|
||
<i class="fas fa-link"></i>
|
||
</button>
|
||
<button class="change-color" title="Farbe ändern">
|
||
<i class="fas fa-palette"></i>
|
||
</button>
|
||
<button class="delete-node" title="Knoten löschen">
|
||
<i class="fas fa-trash"></i>
|
||
</button>
|
||
${this.options.enableExport ? `
|
||
<div class="export-group">
|
||
<button class="export-mindmap" title="Exportieren">
|
||
<i class="fas fa-download"></i>
|
||
</button>
|
||
<div class="export-options">
|
||
<button data-format="png">Als Bild (.png)</button>
|
||
<button data-format="json">Als JSON</button>
|
||
<button data-format="pdf">Als PDF</button>
|
||
</div>
|
||
</div>
|
||
` : ''}
|
||
`;
|
||
|
||
this.container.parentNode.insertBefore(toolbar, this.container);
|
||
this.initToolbarEvents(toolbar);
|
||
}
|
||
|
||
/**
|
||
* Initialisiert Event-Listener für die Toolbar
|
||
*/
|
||
initToolbarEvents(toolbar) {
|
||
toolbar.querySelector('.add-node')?.addEventListener('click', () => this.addNode());
|
||
toolbar.querySelector('.add-child')?.addEventListener('click', () => this.addChildNode());
|
||
toolbar.querySelector('.connect-nodes')?.addEventListener('click', () => this.toggleConnectionMode());
|
||
toolbar.querySelector('.change-color')?.addEventListener('click', () => this.showColorPicker());
|
||
toolbar.querySelector('.delete-node')?.addEventListener('click', () => this.deleteSelectedNodes());
|
||
|
||
// Export-Funktionen
|
||
toolbar.querySelectorAll('.export-options button').forEach(btn => {
|
||
btn.addEventListener('click', () => this.exportMindmap(btn.dataset.format));
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Initialisiert die Mindmap
|
||
*/
|
||
async initialize() {
|
||
console.log("Initialisiere Mindmap...");
|
||
|
||
try {
|
||
// Versuche, Daten vom Server zu laden
|
||
this.data = await this.loadDataFromServer();
|
||
console.log("Daten vom Server geladen:", this.data);
|
||
} catch (error) {
|
||
console.warn("Fehler beim Laden der Daten vom Server:", error);
|
||
console.log("Verwende Standarddaten als Fallback");
|
||
this.data = this.getDefaultData();
|
||
}
|
||
|
||
// Cytoscape initialisieren
|
||
this.initializeCytoscape();
|
||
|
||
// Drag-and-Drop Funktionalität aktivieren
|
||
this.enableDragAndDrop();
|
||
|
||
return this;
|
||
}
|
||
|
||
/**
|
||
* Initialisiert Cytoscape.js mit erweiterten Interaktionsmöglichkeiten
|
||
*/
|
||
initializeCytoscape() {
|
||
if (!this.container) {
|
||
console.error(`Container mit ID '${this.containerId}' nicht gefunden.`);
|
||
return;
|
||
}
|
||
|
||
// Konvertiert die API-Daten ins Cytoscape-Format
|
||
const elements = this.convertToCytoscapeFormat(this.data);
|
||
|
||
// Erstellt die Cytoscape-Instanz mit erweiterten Optionen
|
||
this.cy = cytoscape({
|
||
container: this.container,
|
||
elements: elements,
|
||
style: this.getStyles(),
|
||
layout: {
|
||
name: this.options.defaultLayout,
|
||
animate: true,
|
||
animationDuration: 800,
|
||
nodeDimensionsIncludeLabels: true,
|
||
padding: 30,
|
||
spacingFactor: 1.2,
|
||
randomize: true,
|
||
componentSpacing: 100,
|
||
nodeRepulsion: 8000,
|
||
edgeElasticity: 100,
|
||
nestingFactor: 1.2,
|
||
gravity: 80
|
||
},
|
||
// Erweiterte Interaktionsoptionen
|
||
minZoom: 0.2,
|
||
maxZoom: 3,
|
||
wheelSensitivity: 0.3,
|
||
autounselectify: false,
|
||
selectionType: 'additive'
|
||
});
|
||
|
||
// Event-Listener für Interaktionen
|
||
this.initializeEventListeners();
|
||
}
|
||
|
||
/**
|
||
* Initialisiert Event-Listener für Interaktionen
|
||
*/
|
||
initializeEventListeners() {
|
||
// Knoten-Klick
|
||
this.cy.on('tap', 'node', (event) => {
|
||
const node = event.target;
|
||
this.handleNodeClick(node);
|
||
});
|
||
|
||
// Kontextmenü für Knoten
|
||
this.cy.on('cxttap', 'node', (event) => {
|
||
const node = event.target;
|
||
this.showContextMenu(node, event.renderedPosition);
|
||
});
|
||
|
||
// Drag-and-Drop Events
|
||
this.cy.on('dragfree', 'node', (event) => {
|
||
const node = event.target;
|
||
this.saveNodePosition(node);
|
||
});
|
||
|
||
// Zoom-Events für responsive Anpassungen
|
||
this.cy.on('zoom', () => {
|
||
this.adjustNodeStyling();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Fügt einen neuen Knoten hinzu
|
||
*/
|
||
addNode() {
|
||
const label = prompt('Name des neuen Knotens:');
|
||
if (!label) return;
|
||
|
||
const node = this.cy.add({
|
||
group: 'nodes',
|
||
data: {
|
||
id: 'n' + Date.now(),
|
||
name: label,
|
||
color: '#8B5CF6'
|
||
},
|
||
position: {
|
||
x: this.cy.width() / 2,
|
||
y: this.cy.height() / 2
|
||
}
|
||
});
|
||
|
||
this.saveToServer();
|
||
return node;
|
||
}
|
||
|
||
/**
|
||
* Fügt einen Unterknoten zum ausgewählten Knoten hinzu
|
||
*/
|
||
addChildNode() {
|
||
const selected = this.cy.$('node:selected');
|
||
if (selected.length !== 1) {
|
||
alert('Bitte wählen Sie genau einen Knoten aus.');
|
||
return;
|
||
}
|
||
|
||
const parent = selected[0];
|
||
const label = prompt('Name des Unterknotens:');
|
||
if (!label) return;
|
||
|
||
const child = this.cy.add({
|
||
group: 'nodes',
|
||
data: {
|
||
id: 'n' + Date.now(),
|
||
name: label,
|
||
color: parent.data('color')
|
||
},
|
||
position: {
|
||
x: parent.position('x') + 100,
|
||
y: parent.position('y') + 100
|
||
}
|
||
});
|
||
|
||
this.cy.add({
|
||
group: 'edges',
|
||
data: {
|
||
id: 'e' + Date.now(),
|
||
source: parent.id(),
|
||
target: child.id()
|
||
}
|
||
});
|
||
|
||
this.saveToServer();
|
||
}
|
||
|
||
/**
|
||
* Aktiviert/Deaktiviert den Verbindungsmodus
|
||
*/
|
||
toggleConnectionMode() {
|
||
this.connectionMode = !this.connectionMode;
|
||
this.cy.container().style.cursor = this.connectionMode ? 'crosshair' : 'default';
|
||
|
||
if (this.connectionMode) {
|
||
this.cy.once('tap', 'node', (e1) => {
|
||
const source = e1.target;
|
||
this.cy.once('tap', 'node', (e2) => {
|
||
const target = e2.target;
|
||
if (source.id() !== target.id()) {
|
||
this.cy.add({
|
||
group: 'edges',
|
||
data: {
|
||
id: 'e' + Date.now(),
|
||
source: source.id(),
|
||
target: target.id()
|
||
}
|
||
});
|
||
this.saveToServer();
|
||
}
|
||
this.connectionMode = false;
|
||
this.cy.container().style.cursor = 'default';
|
||
});
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Zeigt den Farbwähler für ausgewählte Knoten
|
||
*/
|
||
showColorPicker() {
|
||
const selected = this.cy.$('node:selected');
|
||
if (selected.length === 0) {
|
||
alert('Bitte wählen Sie mindestens einen Knoten aus.');
|
||
return;
|
||
}
|
||
|
||
const colorPicker = document.createElement('input');
|
||
colorPicker.type = 'color';
|
||
colorPicker.value = selected[0].data('color') || '#8B5CF6';
|
||
colorPicker.style.position = 'absolute';
|
||
colorPicker.style.left = '-9999px';
|
||
document.body.appendChild(colorPicker);
|
||
|
||
colorPicker.addEventListener('change', (event) => {
|
||
const color = event.target.value;
|
||
selected.forEach(node => {
|
||
node.data('color', color);
|
||
});
|
||
this.saveToServer();
|
||
document.body.removeChild(colorPicker);
|
||
});
|
||
|
||
colorPicker.click();
|
||
}
|
||
|
||
/**
|
||
* Löscht ausgewählte Knoten
|
||
*/
|
||
deleteSelectedNodes() {
|
||
const selected = this.cy.$('node:selected');
|
||
if (selected.length === 0) {
|
||
alert('Bitte wählen Sie mindestens einen Knoten aus.');
|
||
return;
|
||
}
|
||
|
||
if (confirm(`${selected.length} Knoten löschen?`)) {
|
||
this.cy.remove(selected);
|
||
this.saveToServer();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Exportiert die Mindmap im gewählten Format
|
||
*/
|
||
exportMindmap(format) {
|
||
switch (format) {
|
||
case 'png':
|
||
const png = this.cy.png({
|
||
full: true,
|
||
scale: 2,
|
||
bg: this.options.darkMode ? '#1a1a1a' : '#ffffff'
|
||
});
|
||
this.downloadFile(png, 'mindmap.png', 'image/png');
|
||
break;
|
||
|
||
case 'json':
|
||
const json = JSON.stringify(this.cy.json(), null, 2);
|
||
this.downloadFile(json, 'mindmap.json', 'application/json');
|
||
break;
|
||
|
||
case 'pdf':
|
||
// Hier könnte eine PDF-Export-Implementierung folgen
|
||
alert('PDF-Export wird in Kürze verfügbar sein.');
|
||
break;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Hilfsfunktion zum Herunterladen von Dateien
|
||
*/
|
||
downloadFile(content, filename, type) {
|
||
const blob = type.startsWith('image') ? this.dataURLtoBlob(content) : new Blob([content], { type });
|
||
const url = window.URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = filename;
|
||
a.click();
|
||
window.URL.revokeObjectURL(url);
|
||
}
|
||
|
||
/**
|
||
* Konvertiert Data URL zu Blob
|
||
*/
|
||
dataURLtoBlob(dataurl) {
|
||
const arr = dataurl.split(',');
|
||
const mime = arr[0].match(/:(.*?);/)[1];
|
||
const bstr = atob(arr[1]);
|
||
let n = bstr.length;
|
||
const u8arr = new Uint8Array(n);
|
||
while (n--) {
|
||
u8arr[n] = bstr.charCodeAt(n);
|
||
}
|
||
return new Blob([u8arr], { type: mime });
|
||
}
|
||
|
||
/**
|
||
* Speichert Änderungen auf dem Server
|
||
*/
|
||
async saveToServer() {
|
||
try {
|
||
const response = await fetch(this.options.apiEndpoint, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(this.cy.json())
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP Fehler: ${response.status}`);
|
||
}
|
||
|
||
console.log('Änderungen erfolgreich gespeichert');
|
||
} catch (error) {
|
||
console.error('Fehler beim Speichern:', error);
|
||
alert('Fehler beim Speichern der Änderungen');
|
||
}
|
||
}
|
||
}
|
||
|
||
// Globale Export
|
||
window.MindMap.Visualization = MindmapVisualization;
|
||
|
||
// Automatische Initialisierung, wenn das DOM geladen ist
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const cyContainer = document.getElementById('cy');
|
||
if (cyContainer) {
|
||
console.log("Mindmap-Container gefunden, initialisiere...");
|
||
const mindmap = new MindmapVisualization('cy', {
|
||
onNodeClick: function(nodeData) {
|
||
console.log("Knoten ausgewählt:", nodeData);
|
||
}
|
||
});
|
||
|
||
mindmap.initialize().then(() => {
|
||
console.log("Mindmap erfolgreich initialisiert");
|
||
// Speichere die Instanz global für den Zugriff von außen
|
||
window.mindmap = mindmap;
|
||
}).catch(error => {
|
||
console.error("Fehler bei der Initialisierung der Mindmap:", error);
|
||
});
|
||
}
|
||
}); |