749 lines
24 KiB
JavaScript
749 lines
24 KiB
JavaScript
/**
|
|
* Mindmap.js - Interaktive Mind-Map Implementierung
|
|
* - Cytoscape.js für Graph-Rendering
|
|
* - Fetch API für REST-Zugriffe
|
|
* - Socket.IO für Echtzeit-Synchronisation
|
|
*/
|
|
|
|
(async () => {
|
|
/* 1. Initialisierung und Grundkonfiguration */
|
|
const cy = cytoscape({
|
|
container: document.getElementById('cy'),
|
|
style: [
|
|
{
|
|
selector: 'node',
|
|
style: {
|
|
'label': 'data(name)',
|
|
'text-valign': 'center',
|
|
'color': '#fff',
|
|
'background-color': 'data(color)',
|
|
'width': 45,
|
|
'height': 45,
|
|
'font-size': 11,
|
|
'text-outline-width': 1,
|
|
'text-outline-color': '#000',
|
|
'text-outline-opacity': 0.5,
|
|
'text-wrap': 'wrap',
|
|
'text-max-width': 80
|
|
}
|
|
},
|
|
{
|
|
selector: 'node[icon]',
|
|
style: {
|
|
'background-image': function(ele) {
|
|
return `static/img/icons/${ele.data('icon')}.svg`;
|
|
},
|
|
'background-width': '60%',
|
|
'background-height': '60%',
|
|
'background-position-x': '50%',
|
|
'background-position-y': '40%',
|
|
'text-margin-y': 10
|
|
}
|
|
},
|
|
{
|
|
selector: 'edge',
|
|
style: {
|
|
'width': 2,
|
|
'line-color': '#888',
|
|
'target-arrow-shape': 'triangle',
|
|
'curve-style': 'bezier',
|
|
'target-arrow-color': '#888'
|
|
}
|
|
},
|
|
{
|
|
selector: ':selected',
|
|
style: {
|
|
'border-width': 3,
|
|
'border-color': '#f8f32b'
|
|
}
|
|
}
|
|
],
|
|
layout: {
|
|
name: 'breadthfirst',
|
|
directed: true,
|
|
padding: 30,
|
|
spacingFactor: 1.2
|
|
}
|
|
});
|
|
|
|
/* 2. Hilfs-Funktionen für API-Zugriffe */
|
|
const get = async endpoint => {
|
|
try {
|
|
const response = await fetch(endpoint);
|
|
if (!response.ok) {
|
|
console.error(`API-Fehler: ${endpoint} antwortet mit Status ${response.status}`);
|
|
return []; // Leeres Array zurückgeben bei Fehlern
|
|
}
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error(`Fehler beim Abrufen von ${endpoint}:`, error);
|
|
return []; // Leeres Array zurückgeben bei Netzwerkfehlern
|
|
}
|
|
};
|
|
|
|
const post = async (endpoint, body) => {
|
|
try {
|
|
const response = await fetch(endpoint, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body)
|
|
});
|
|
if (!response.ok) {
|
|
console.error(`API-Fehler: ${endpoint} antwortet mit Status ${response.status}`);
|
|
return {}; // Leeres Objekt zurückgeben bei Fehlern
|
|
}
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error(`Fehler beim POST zu ${endpoint}:`, error);
|
|
return {}; // Leeres Objekt zurückgeben bei Netzwerkfehlern
|
|
}
|
|
};
|
|
|
|
const del = async endpoint => {
|
|
try {
|
|
const response = await fetch(endpoint, { method: 'DELETE' });
|
|
if (!response.ok) {
|
|
console.error(`API-Fehler: ${endpoint} antwortet mit Status ${response.status}`);
|
|
return {}; // Leeres Objekt zurückgeben bei Fehlern
|
|
}
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error(`Fehler beim DELETE zu ${endpoint}:`, error);
|
|
return {}; // Leeres Objekt zurückgeben bei Netzwerkfehlern
|
|
}
|
|
};
|
|
|
|
/* 3. Kategorien laden für Style-Informationen */
|
|
let categories = await get('/api/categories');
|
|
|
|
/* 4. Daten laden und Rendering */
|
|
const loadMindmap = async () => {
|
|
try {
|
|
// Nodes und Beziehungen parallel laden
|
|
const [nodes, relationships] = await Promise.all([
|
|
get('/api/mind_map_nodes'),
|
|
get('/api/node_relationships')
|
|
]);
|
|
|
|
// Graph leeren (für Reload-Fälle)
|
|
cy.elements().remove();
|
|
|
|
// Überprüfen, ob nodes ein Array ist, wenn nicht, setze es auf ein leeres Array
|
|
const nodesArray = Array.isArray(nodes) ? nodes : [];
|
|
|
|
// Knoten zum Graph hinzufügen
|
|
cy.add(
|
|
nodesArray.map(node => {
|
|
// Kategorie-Informationen für Styling abrufen
|
|
const category = categories.find(c => c.id === node.category_id) || {};
|
|
|
|
return {
|
|
data: {
|
|
id: node.id.toString(),
|
|
name: node.name,
|
|
description: node.description,
|
|
color: node.color_code || category.color_code || '#6b7280',
|
|
icon: node.icon || category.icon,
|
|
category_id: node.category_id
|
|
},
|
|
position: node.x && node.y ? { x: node.x, y: node.y } : undefined
|
|
};
|
|
})
|
|
);
|
|
|
|
// Überprüfen, ob relationships ein Array ist, wenn nicht, setze es auf ein leeres Array
|
|
const relationshipsArray = Array.isArray(relationships) ? relationships : [];
|
|
|
|
// Kanten zum Graph hinzufügen
|
|
cy.add(
|
|
relationshipsArray.map(rel => ({
|
|
data: {
|
|
id: `${rel.parent_id}_${rel.child_id}`,
|
|
source: rel.parent_id.toString(),
|
|
target: rel.child_id.toString()
|
|
}
|
|
}))
|
|
);
|
|
|
|
// Wenn keine Knoten geladen wurden, Fallback-Knoten erstellen
|
|
if (nodesArray.length === 0) {
|
|
// Mindestens einen Standardknoten hinzufügen
|
|
cy.add({
|
|
data: {
|
|
id: 'fallback-1',
|
|
name: 'Mindmap',
|
|
description: 'Erstellen Sie hier Ihre eigene Mindmap',
|
|
color: '#3b82f6',
|
|
icon: 'help-circle'
|
|
},
|
|
position: { x: 300, y: 200 }
|
|
});
|
|
|
|
// Erfolgsmeldung anzeigen
|
|
console.log('Mindmap erfolgreich initialisiert mit Fallback-Knoten');
|
|
|
|
// Info-Meldung für Benutzer anzeigen
|
|
const infoBox = document.createElement('div');
|
|
infoBox.classList.add('info-message');
|
|
infoBox.style.position = 'absolute';
|
|
infoBox.style.top = '50%';
|
|
infoBox.style.left = '50%';
|
|
infoBox.style.transform = 'translate(-50%, -50%)';
|
|
infoBox.style.padding = '15px 20px';
|
|
infoBox.style.backgroundColor = 'rgba(59, 130, 246, 0.9)';
|
|
infoBox.style.color = 'white';
|
|
infoBox.style.borderRadius = '8px';
|
|
infoBox.style.zIndex = '5';
|
|
infoBox.style.maxWidth = '80%';
|
|
infoBox.style.textAlign = 'center';
|
|
infoBox.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
|
|
infoBox.innerHTML = 'Mindmap erfolgreich initialisiert.<br>Verwenden Sie die Werkzeugleiste, um Knoten hinzuzufügen.';
|
|
|
|
document.getElementById('cy').appendChild(infoBox);
|
|
|
|
// Meldung nach 5 Sekunden ausblenden
|
|
setTimeout(() => {
|
|
infoBox.style.opacity = '0';
|
|
infoBox.style.transition = 'opacity 0.5s ease';
|
|
setTimeout(() => {
|
|
if (infoBox.parentNode) {
|
|
infoBox.parentNode.removeChild(infoBox);
|
|
}
|
|
}, 500);
|
|
}, 5000);
|
|
}
|
|
|
|
// Layout anwenden wenn keine Positionsdaten vorhanden
|
|
const nodesWithoutPosition = cy.nodes().filter(node =>
|
|
!node.position() || (node.position().x === 0 && node.position().y === 0)
|
|
);
|
|
|
|
if (nodesWithoutPosition.length > 0) {
|
|
cy.layout({
|
|
name: 'breadthfirst',
|
|
directed: true,
|
|
padding: 30,
|
|
spacingFactor: 1.2
|
|
}).run();
|
|
}
|
|
|
|
// Tooltip-Funktionalität
|
|
cy.nodes().unbind('mouseover').bind('mouseover', (event) => {
|
|
const node = event.target;
|
|
const description = node.data('description');
|
|
|
|
if (description) {
|
|
const tooltip = document.getElementById('node-tooltip') ||
|
|
document.createElement('div');
|
|
|
|
if (!tooltip.id) {
|
|
tooltip.id = 'node-tooltip';
|
|
tooltip.style.position = 'absolute';
|
|
tooltip.style.backgroundColor = '#333';
|
|
tooltip.style.color = '#fff';
|
|
tooltip.style.padding = '8px';
|
|
tooltip.style.borderRadius = '4px';
|
|
tooltip.style.maxWidth = '250px';
|
|
tooltip.style.zIndex = 10;
|
|
tooltip.style.pointerEvents = 'none';
|
|
tooltip.style.transition = 'opacity 0.2s';
|
|
tooltip.style.boxShadow = '0 2px 10px rgba(0,0,0,0.3)';
|
|
document.body.appendChild(tooltip);
|
|
}
|
|
|
|
const renderedPosition = node.renderedPosition();
|
|
const containerRect = cy.container().getBoundingClientRect();
|
|
|
|
tooltip.innerHTML = description;
|
|
tooltip.style.left = (containerRect.left + renderedPosition.x + 25) + 'px';
|
|
tooltip.style.top = (containerRect.top + renderedPosition.y - 15) + 'px';
|
|
tooltip.style.opacity = '1';
|
|
}
|
|
});
|
|
|
|
cy.nodes().unbind('mouseout').bind('mouseout', () => {
|
|
const tooltip = document.getElementById('node-tooltip');
|
|
if (tooltip) {
|
|
tooltip.style.opacity = '0';
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Fehler beim Laden der Mindmap:', error);
|
|
alert('Die Mindmap konnte nicht geladen werden. Bitte prüfen Sie die Konsole für Details.');
|
|
}
|
|
};
|
|
|
|
// Initial laden
|
|
await loadMindmap();
|
|
|
|
/* 5. Socket.IO für Echtzeit-Synchronisation */
|
|
const socket = io();
|
|
|
|
socket.on('node_added', async (node) => {
|
|
// Kategorie-Informationen für Styling abrufen
|
|
const category = categories.find(c => c.id === node.category_id) || {};
|
|
|
|
cy.add({
|
|
data: {
|
|
id: node.id.toString(),
|
|
name: node.name,
|
|
description: node.description,
|
|
color: node.color_code || category.color_code || '#6b7280',
|
|
icon: node.icon || category.icon,
|
|
category_id: node.category_id
|
|
}
|
|
});
|
|
|
|
// Layout neu anwenden, wenn nötig
|
|
if (!node.x || !node.y) {
|
|
cy.layout({ name: 'breadthfirst', directed: true, padding: 30 }).run();
|
|
}
|
|
});
|
|
|
|
socket.on('node_updated', (node) => {
|
|
const cyNode = cy.$id(node.id.toString());
|
|
if (cyNode.length > 0) {
|
|
// Kategorie-Informationen für Styling abrufen
|
|
const category = categories.find(c => c.id === node.category_id) || {};
|
|
|
|
cyNode.data({
|
|
name: node.name,
|
|
description: node.description,
|
|
color: node.color_code || category.color_code || '#6b7280',
|
|
icon: node.icon || category.icon,
|
|
category_id: node.category_id
|
|
});
|
|
|
|
if (node.x && node.y) {
|
|
cyNode.position({ x: node.x, y: node.y });
|
|
}
|
|
}
|
|
});
|
|
|
|
socket.on('node_deleted', (nodeId) => {
|
|
const cyNode = cy.$id(nodeId.toString());
|
|
if (cyNode.length > 0) {
|
|
cy.remove(cyNode);
|
|
}
|
|
});
|
|
|
|
socket.on('relationship_added', (rel) => {
|
|
cy.add({
|
|
data: {
|
|
id: `${rel.parent_id}_${rel.child_id}`,
|
|
source: rel.parent_id.toString(),
|
|
target: rel.child_id.toString()
|
|
}
|
|
});
|
|
});
|
|
|
|
socket.on('relationship_deleted', (rel) => {
|
|
const edgeId = `${rel.parent_id}_${rel.child_id}`;
|
|
const cyEdge = cy.$id(edgeId);
|
|
if (cyEdge.length > 0) {
|
|
cy.remove(cyEdge);
|
|
}
|
|
});
|
|
|
|
socket.on('category_updated', async () => {
|
|
// Kategorien neu laden
|
|
categories = await get('/api/categories');
|
|
// Nodes aktualisieren, die diese Kategorie verwenden
|
|
cy.nodes().forEach(node => {
|
|
const categoryId = node.data('category_id');
|
|
if (categoryId) {
|
|
const category = categories.find(c => c.id === categoryId);
|
|
if (category) {
|
|
node.data('color', node.data('color_code') || category.color_code);
|
|
node.data('icon', node.data('icon') || category.icon);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
/* 6. UI-Interaktionen */
|
|
// Knoten hinzufügen
|
|
const btnAddNode = document.getElementById('addNode');
|
|
if (btnAddNode) {
|
|
btnAddNode.addEventListener('click', async () => {
|
|
const name = prompt('Knotenname eingeben:');
|
|
if (!name) return;
|
|
|
|
const description = prompt('Beschreibung (optional):');
|
|
|
|
// Kategorie auswählen
|
|
let categoryId = null;
|
|
if (categories.length > 0) {
|
|
const categoryOptions = categories.map((c, i) => `${i}: ${c.name}`).join('\n');
|
|
const categoryChoice = prompt(
|
|
`Kategorie auswählen (Nummer eingeben):\n${categoryOptions}`,
|
|
'0'
|
|
);
|
|
|
|
if (categoryChoice !== null) {
|
|
const index = parseInt(categoryChoice, 10);
|
|
if (!isNaN(index) && index >= 0 && index < categories.length) {
|
|
categoryId = categories[index].id;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Knoten erstellen
|
|
await post('/api/mind_map_node', {
|
|
name,
|
|
description,
|
|
category_id: categoryId
|
|
});
|
|
// Darstellung wird durch Socket.IO Event übernommen
|
|
});
|
|
}
|
|
|
|
// Verbindung hinzufügen
|
|
const btnAddEdge = document.getElementById('addEdge');
|
|
if (btnAddEdge) {
|
|
btnAddEdge.addEventListener('click', async () => {
|
|
const sel = cy.$('node:selected');
|
|
if (sel.length !== 2) {
|
|
alert('Bitte genau zwei Knoten auswählen (Parent → Child)');
|
|
return;
|
|
}
|
|
|
|
const [parent, child] = sel.map(node => node.id());
|
|
await post('/api/node_relationship', {
|
|
parent_id: parent,
|
|
child_id: child
|
|
});
|
|
// Darstellung wird durch Socket.IO Event übernommen
|
|
});
|
|
}
|
|
|
|
// Knoten bearbeiten
|
|
const btnEditNode = document.getElementById('editNode');
|
|
if (btnEditNode) {
|
|
btnEditNode.addEventListener('click', async () => {
|
|
const sel = cy.$('node:selected');
|
|
if (sel.length !== 1) {
|
|
alert('Bitte genau einen Knoten auswählen');
|
|
return;
|
|
}
|
|
|
|
const node = sel[0];
|
|
const nodeData = node.data();
|
|
|
|
const name = prompt('Knotenname:', nodeData.name);
|
|
if (!name) return;
|
|
|
|
const description = prompt('Beschreibung:', nodeData.description || '');
|
|
|
|
// Kategorie auswählen
|
|
let categoryId = nodeData.category_id;
|
|
if (categories.length > 0) {
|
|
const categoryOptions = categories.map((c, i) => `${i}: ${c.name}`).join('\n');
|
|
const categoryChoice = prompt(
|
|
`Kategorie auswählen (Nummer eingeben):\n${categoryOptions}`,
|
|
categories.findIndex(c => c.id === categoryId).toString()
|
|
);
|
|
|
|
if (categoryChoice !== null) {
|
|
const index = parseInt(categoryChoice, 10);
|
|
if (!isNaN(index) && index >= 0 && index < categories.length) {
|
|
categoryId = categories[index].id;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Knoten aktualisieren
|
|
await post(`/api/mind_map_node/${nodeData.id}`, {
|
|
name,
|
|
description,
|
|
category_id: categoryId
|
|
});
|
|
// Darstellung wird durch Socket.IO Event übernommen
|
|
});
|
|
}
|
|
|
|
// Knoten löschen
|
|
const btnDeleteNode = document.getElementById('deleteNode');
|
|
if (btnDeleteNode) {
|
|
btnDeleteNode.addEventListener('click', async () => {
|
|
const sel = cy.$('node:selected');
|
|
if (sel.length !== 1) {
|
|
alert('Bitte genau einen Knoten auswählen');
|
|
return;
|
|
}
|
|
|
|
if (confirm('Sind Sie sicher, dass Sie diesen Knoten löschen möchten?')) {
|
|
const nodeId = sel[0].id();
|
|
await del(`/api/mind_map_node/${nodeId}`);
|
|
// Darstellung wird durch Socket.IO Event übernommen
|
|
}
|
|
});
|
|
}
|
|
|
|
// Verbindung löschen
|
|
const btnDeleteEdge = document.getElementById('deleteEdge');
|
|
if (btnDeleteEdge) {
|
|
btnDeleteEdge.addEventListener('click', async () => {
|
|
const sel = cy.$('edge:selected');
|
|
if (sel.length !== 1) {
|
|
alert('Bitte genau eine Verbindung auswählen');
|
|
return;
|
|
}
|
|
|
|
if (confirm('Sind Sie sicher, dass Sie diese Verbindung löschen möchten?')) {
|
|
const edge = sel[0];
|
|
const parentId = edge.source().id();
|
|
const childId = edge.target().id();
|
|
|
|
await del(`/api/node_relationship/${parentId}/${childId}`);
|
|
// Darstellung wird durch Socket.IO Event übernommen
|
|
}
|
|
});
|
|
}
|
|
|
|
// Layout aktualisieren
|
|
const btnReLayout = document.getElementById('reLayout');
|
|
if (btnReLayout) {
|
|
btnReLayout.addEventListener('click', () => {
|
|
cy.layout({
|
|
name: 'breadthfirst',
|
|
directed: true,
|
|
padding: 30,
|
|
spacingFactor: 1.2
|
|
}).run();
|
|
});
|
|
}
|
|
|
|
/* 7. Position speichern bei Drag & Drop */
|
|
cy.on('dragfree', 'node', async (e) => {
|
|
const node = e.target;
|
|
const position = node.position();
|
|
|
|
await post(`/api/mind_map_node/${node.id()}/position`, {
|
|
x: Math.round(position.x),
|
|
y: Math.round(position.y)
|
|
});
|
|
|
|
// Andere Benutzer erhalten die Position über den node_updated Event
|
|
});
|
|
|
|
/* 8. Kontextmenü (optional) */
|
|
const setupContextMenu = () => {
|
|
cy.on('cxttap', 'node', function(e) {
|
|
const node = e.target;
|
|
const nodeData = node.data();
|
|
|
|
// Position des Kontextmenüs berechnen
|
|
const renderedPosition = node.renderedPosition();
|
|
const containerRect = cy.container().getBoundingClientRect();
|
|
const menuX = containerRect.left + renderedPosition.x;
|
|
const menuY = containerRect.top + renderedPosition.y;
|
|
|
|
// Kontextmenü erstellen oder aktualisieren
|
|
let contextMenu = document.getElementById('context-menu');
|
|
if (!contextMenu) {
|
|
contextMenu = document.createElement('div');
|
|
contextMenu.id = 'context-menu';
|
|
contextMenu.style.position = 'absolute';
|
|
contextMenu.style.backgroundColor = '#fff';
|
|
contextMenu.style.border = '1px solid #ccc';
|
|
contextMenu.style.borderRadius = '4px';
|
|
contextMenu.style.padding = '5px 0';
|
|
contextMenu.style.boxShadow = '0 2px 10px rgba(0,0,0,0.2)';
|
|
contextMenu.style.zIndex = 1000;
|
|
document.body.appendChild(contextMenu);
|
|
}
|
|
|
|
// Menüinhalte
|
|
contextMenu.innerHTML = `
|
|
<div class="menu-item" data-action="edit">Knoten bearbeiten</div>
|
|
<div class="menu-item" data-action="connect">Verbindung erstellen</div>
|
|
<div class="menu-item" data-action="delete">Knoten löschen</div>
|
|
`;
|
|
|
|
// Styling für Menüpunkte
|
|
const menuItems = contextMenu.querySelectorAll('.menu-item');
|
|
menuItems.forEach(item => {
|
|
item.style.padding = '8px 20px';
|
|
item.style.cursor = 'pointer';
|
|
item.style.fontSize = '14px';
|
|
|
|
item.addEventListener('mouseover', function() {
|
|
this.style.backgroundColor = '#f0f0f0';
|
|
});
|
|
|
|
item.addEventListener('mouseout', function() {
|
|
this.style.backgroundColor = 'transparent';
|
|
});
|
|
|
|
// Event-Handler
|
|
item.addEventListener('click', async function() {
|
|
const action = this.getAttribute('data-action');
|
|
|
|
switch(action) {
|
|
case 'edit':
|
|
// Knoten bearbeiten (gleiche Logik wie beim Edit-Button)
|
|
const name = prompt('Knotenname:', nodeData.name);
|
|
if (name) {
|
|
const description = prompt('Beschreibung:', nodeData.description || '');
|
|
await post(`/api/mind_map_node/${nodeData.id}`, { name, description });
|
|
}
|
|
break;
|
|
|
|
case 'connect':
|
|
// Modus zum Verbinden aktivieren
|
|
cy.nodes().unselect();
|
|
node.select();
|
|
alert('Wählen Sie nun einen zweiten Knoten aus, um eine Verbindung zu erstellen');
|
|
break;
|
|
|
|
case 'delete':
|
|
if (confirm('Sind Sie sicher, dass Sie diesen Knoten löschen möchten?')) {
|
|
await del(`/api/mind_map_node/${nodeData.id}`);
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Menü schließen
|
|
contextMenu.style.display = 'none';
|
|
});
|
|
});
|
|
|
|
// Menü positionieren und anzeigen
|
|
contextMenu.style.left = menuX + 'px';
|
|
contextMenu.style.top = menuY + 'px';
|
|
contextMenu.style.display = 'block';
|
|
|
|
// Event-Listener zum Schließen des Menüs
|
|
const closeMenu = function() {
|
|
if (contextMenu) {
|
|
contextMenu.style.display = 'none';
|
|
}
|
|
document.removeEventListener('click', closeMenu);
|
|
};
|
|
|
|
// Verzögerung, um den aktuellen Click nicht zu erfassen
|
|
setTimeout(() => {
|
|
document.addEventListener('click', closeMenu);
|
|
}, 0);
|
|
|
|
e.preventDefault();
|
|
});
|
|
};
|
|
|
|
// Kontextmenü aktivieren (optional)
|
|
// setupContextMenu();
|
|
|
|
/* 9. Export-Funktion (optional) */
|
|
const btnExport = document.getElementById('exportMindmap');
|
|
if (btnExport) {
|
|
btnExport.addEventListener('click', () => {
|
|
const elements = cy.json().elements;
|
|
const exportData = {
|
|
nodes: elements.nodes.map(n => ({
|
|
id: n.data.id,
|
|
name: n.data.name,
|
|
description: n.data.description,
|
|
category_id: n.data.category_id,
|
|
x: Math.round(n.position?.x || 0),
|
|
y: Math.round(n.position?.y || 0)
|
|
})),
|
|
relationships: elements.edges.map(e => ({
|
|
parent_id: e.data.source,
|
|
child_id: e.data.target
|
|
}))
|
|
};
|
|
|
|
const blob = new Blob([JSON.stringify(exportData, null, 2)], {type: 'application/json'});
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'mindmap_export.json';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
});
|
|
}
|
|
|
|
/* 10. Filter-Funktion nach Kategorien (optional) */
|
|
const setupCategoryFilters = () => {
|
|
const filterContainer = document.getElementById('category-filters');
|
|
if (!filterContainer || !categories.length) return;
|
|
|
|
filterContainer.innerHTML = '';
|
|
|
|
// "Alle anzeigen" Option
|
|
const allBtn = document.createElement('button');
|
|
allBtn.innerText = 'Alle Kategorien';
|
|
allBtn.className = 'category-filter active';
|
|
allBtn.onclick = () => {
|
|
document.querySelectorAll('.category-filter').forEach(btn => btn.classList.remove('active'));
|
|
allBtn.classList.add('active');
|
|
cy.nodes().removeClass('filtered').show();
|
|
cy.edges().show();
|
|
};
|
|
filterContainer.appendChild(allBtn);
|
|
|
|
// Filter-Button pro Kategorie
|
|
categories.forEach(category => {
|
|
const btn = document.createElement('button');
|
|
btn.innerText = category.name;
|
|
btn.className = 'category-filter';
|
|
btn.style.backgroundColor = category.color_code;
|
|
btn.style.color = '#fff';
|
|
btn.onclick = () => {
|
|
document.querySelectorAll('.category-filter').forEach(btn => btn.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
|
|
const matchingNodes = cy.nodes().filter(node => node.data('category_id') === category.id);
|
|
cy.nodes().addClass('filtered').hide();
|
|
matchingNodes.removeClass('filtered').show();
|
|
|
|
// Verbindungen zu/von diesen Knoten anzeigen
|
|
cy.edges().hide();
|
|
matchingNodes.connectedEdges().show();
|
|
};
|
|
filterContainer.appendChild(btn);
|
|
});
|
|
};
|
|
|
|
// Filter-Funktionalität aktivieren (optional)
|
|
// setupCategoryFilters();
|
|
|
|
/* 11. Suchfunktion (optional) */
|
|
const searchInput = document.getElementById('search-mindmap');
|
|
if (searchInput) {
|
|
searchInput.addEventListener('input', (e) => {
|
|
const searchTerm = e.target.value.toLowerCase();
|
|
|
|
if (!searchTerm) {
|
|
cy.nodes().removeClass('search-hidden').show();
|
|
cy.edges().show();
|
|
return;
|
|
}
|
|
|
|
cy.nodes().forEach(node => {
|
|
const name = node.data('name').toLowerCase();
|
|
const description = (node.data('description') || '').toLowerCase();
|
|
|
|
if (name.includes(searchTerm) || description.includes(searchTerm)) {
|
|
node.removeClass('search-hidden').show();
|
|
node.connectedEdges().show();
|
|
} else {
|
|
node.addClass('search-hidden').hide();
|
|
// Kanten nur verstecken, wenn beide verbundenen Knoten versteckt sind
|
|
node.connectedEdges().forEach(edge => {
|
|
const otherNode = edge.source().id() === node.id() ? edge.target() : edge.source();
|
|
if (otherNode.hasClass('search-hidden')) {
|
|
edge.hide();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
console.log('Mindmap erfolgreich initialisiert');
|
|
})();
|