1061 lines
39 KiB
HTML
1061 lines
39 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}{{ mindmap.name }} - Mindmap{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
/* Container für die Mindmap mit verbesserten Glaseffekten */
|
|
#cy {
|
|
width: 100%;
|
|
height: calc(100vh - 14rem);
|
|
background-color: rgba(15, 23, 42, 0.3);
|
|
border-radius: 1rem;
|
|
overflow: hidden;
|
|
}
|
|
|
|
body:not(.dark) #cy {
|
|
background-color: rgba(255, 255, 255, 0.3);
|
|
}
|
|
|
|
/* Info-Panel */
|
|
.node-info-panel {
|
|
position: absolute;
|
|
top: 1rem;
|
|
right: 1rem;
|
|
width: 300px;
|
|
max-height: calc(100vh - 16rem);
|
|
overflow-y: auto;
|
|
border-radius: 1rem;
|
|
opacity: 0;
|
|
transform: translateX(20px);
|
|
pointer-events: none;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.node-info-panel.visible {
|
|
opacity: 1;
|
|
transform: translateX(0);
|
|
pointer-events: all;
|
|
}
|
|
|
|
body.dark .node-info-panel {
|
|
background-color: rgba(15, 23, 42, 0.8);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
body:not(.dark) .node-info-panel {
|
|
background-color: rgba(255, 255, 255, 0.85);
|
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
backdrop-filter: blur(10px);
|
|
color: #1e293b;
|
|
}
|
|
|
|
/* Toolbar */
|
|
.mindmap-toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 0.75rem;
|
|
border-radius: 0.75rem;
|
|
margin-bottom: 1rem;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
body.dark .mindmap-toolbar {
|
|
background-color: rgba(15, 23, 42, 0.6);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
body:not(.dark) .mindmap-toolbar {
|
|
background-color: rgba(255, 255, 255, 0.8);
|
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.mindmap-action-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.5rem 1rem;
|
|
border-radius: 0.5rem;
|
|
font-weight: 500;
|
|
transition: all 0.3s ease;
|
|
cursor: pointer;
|
|
}
|
|
|
|
body.dark .mindmap-action-btn {
|
|
background-color: rgba(30, 41, 59, 0.7);
|
|
color: rgba(255, 255, 255, 0.9);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
body:not(.dark) .mindmap-action-btn {
|
|
background-color: rgba(255, 255, 255, 0.8);
|
|
color: #1e293b;
|
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
body.dark .mindmap-action-btn:hover {
|
|
background-color: rgba(51, 65, 85, 0.8);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
body:not(.dark) .mindmap-action-btn:hover {
|
|
background-color: rgba(243, 244, 246, 0.9);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
/* Node-Link Styles */
|
|
.node-link {
|
|
display: inline-block;
|
|
padding: 0.35rem 0.75rem;
|
|
margin: 0.25rem;
|
|
border-radius: 1rem;
|
|
font-size: 0.8rem;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
white-space: nowrap;
|
|
color: white;
|
|
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.node-link:hover {
|
|
transform: translateY(-2px);
|
|
filter: brightness(1.1);
|
|
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.15);
|
|
}
|
|
|
|
/* Notes */
|
|
.note-container {
|
|
position: absolute;
|
|
width: 250px;
|
|
cursor: move;
|
|
opacity: 0;
|
|
transform: scale(0.95);
|
|
transition: all 0.3s ease;
|
|
z-index: 10;
|
|
}
|
|
|
|
.note-container.visible {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
}
|
|
|
|
/* Note-Werkzeuge */
|
|
.note-tools {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.5rem;
|
|
}
|
|
|
|
.note-tool {
|
|
width: 24px;
|
|
height: 24px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
font-size: 0.7rem;
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.note-tool:hover {
|
|
opacity: 1;
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
/* Color Palette */
|
|
.color-palette {
|
|
display: flex;
|
|
gap: 0.25rem;
|
|
flex-wrap: wrap;
|
|
padding: 0.25rem;
|
|
}
|
|
|
|
.color-swatch {
|
|
width: 1rem;
|
|
height: 1rem;
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.color-swatch:hover {
|
|
transform: scale(1.2);
|
|
}
|
|
|
|
/* Notizen-Editor */
|
|
.note-content {
|
|
min-height: 100px;
|
|
padding: 0.75rem;
|
|
outline: none;
|
|
width: 100%;
|
|
font-size: 0.9rem;
|
|
resize: both;
|
|
overflow: auto;
|
|
}
|
|
|
|
/* Kontext-Menü */
|
|
#context-menu {
|
|
min-width: 180px;
|
|
border-radius: 0.5rem;
|
|
overflow: hidden;
|
|
z-index: 1000;
|
|
}
|
|
|
|
body.dark #context-menu {
|
|
background-color: rgba(15, 23, 42, 0.95);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
body:not(.dark) #context-menu {
|
|
background-color: rgba(255, 255, 255, 0.95);
|
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1);
|
|
color: #1e293b;
|
|
}
|
|
|
|
.menu-item {
|
|
padding: 0.75rem 1rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
/* Suchergebnisse Container */
|
|
.search-results-container {
|
|
position: absolute;
|
|
top: 5rem;
|
|
left: 1rem;
|
|
width: 350px;
|
|
max-height: calc(100vh - 16rem);
|
|
overflow-y: auto;
|
|
border-radius: 1rem;
|
|
opacity: 0;
|
|
transform: translateY(-10px);
|
|
pointer-events: none;
|
|
transition: all 0.3s ease;
|
|
z-index: 100;
|
|
}
|
|
|
|
.search-results-container.visible {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
pointer-events: all;
|
|
}
|
|
|
|
body.dark .search-results-container {
|
|
background-color: rgba(15, 23, 42, 0.95);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
body:not(.dark) .search-results-container {
|
|
background-color: rgba(255, 255, 255, 0.95);
|
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
|
color: #1e293b;
|
|
}
|
|
|
|
/* Suchergebnis-Item */
|
|
.search-result-item {
|
|
padding: 0.75rem;
|
|
border-bottom: 1px solid rgba(127, 127, 127, 0.2);
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.search-result-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
body.dark .search-result-item:hover {
|
|
background-color: rgba(255, 255, 255, 0.05);
|
|
}
|
|
|
|
body:not(.dark) .search-result-item:hover {
|
|
background-color: rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.search-result-title {
|
|
font-weight: 600;
|
|
margin-bottom: 0.25rem;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.search-result-color {
|
|
width: 0.75rem;
|
|
height: 0.75rem;
|
|
border-radius: 50%;
|
|
margin-right: 0.5rem;
|
|
display: inline-block;
|
|
}
|
|
|
|
.search-result-desc {
|
|
font-size: 0.85rem;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.search-result-source {
|
|
font-size: 0.75rem;
|
|
opacity: 0.6;
|
|
margin-top: 0.25rem;
|
|
}
|
|
|
|
body.dark .menu-item:hover {
|
|
background-color: rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
body:not(.dark) .menu-item:hover {
|
|
background-color: rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
/* Dialogfenster */
|
|
.dialog-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 100;
|
|
}
|
|
|
|
.dialog-content {
|
|
width: 90%;
|
|
max-width: 500px;
|
|
border-radius: 1rem;
|
|
padding: 1.5rem;
|
|
max-height: 80vh;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
body.dark .dialog-content {
|
|
background-color: rgba(15, 23, 42, 0.95);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
body:not(.dark) .dialog-content {
|
|
background-color: rgba(255, 255, 255, 0.95);
|
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
color: #1e293b;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container mx-auto max-w-7xl px-4 py-6">
|
|
<!-- Header -->
|
|
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4">
|
|
<div>
|
|
<h1 class="text-3xl font-bold gradient-text">{{ mindmap.name }}</h1>
|
|
<p class="opacity-80 mt-1">{{ mindmap.description }}</p>
|
|
</div>
|
|
|
|
<div class="flex gap-3 flex-wrap">
|
|
{% if mindmap.user_id == current_user.id %}
|
|
<a href="{{ url_for('edit_mindmap', mindmap_id=mindmap.id) }}" class="mindmap-action-btn">
|
|
<i class="fas fa-edit"></i>
|
|
<span>Bearbeiten</span>
|
|
</a>
|
|
{% endif %}
|
|
|
|
<a href="{{ url_for('profile') }}" class="mindmap-action-btn">
|
|
<i class="fas fa-arrow-left"></i>
|
|
<span>Zurück</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Toolbar -->
|
|
<div class="mindmap-toolbar mb-4">
|
|
<!-- Suchleiste -->
|
|
<div class="search-container mr-2">
|
|
<div class="relative">
|
|
<input type="text" id="mindmap-search" placeholder="Knoten suchen..."
|
|
class="px-3 py-2 pr-10 rounded-lg text-sm border dark:bg-gray-700 dark:text-white dark:border-gray-600">
|
|
<button id="search-btn" class="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-500 dark:text-gray-400">
|
|
<i class="fas fa-search"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<button id="fit-btn" class="mindmap-action-btn">
|
|
<i class="fas fa-expand"></i>
|
|
<span>Alles anzeigen</span>
|
|
</button>
|
|
<button id="reset-btn" class="mindmap-action-btn">
|
|
<i class="fas fa-redo"></i>
|
|
<span>Layout zurücksetzen</span>
|
|
</button>
|
|
<button id="toggle-labels-btn" class="mindmap-action-btn">
|
|
<i class="fas fa-tags"></i>
|
|
<span>Labels ein/aus</span>
|
|
</button>
|
|
<button id="add-note-btn" class="mindmap-action-btn">
|
|
<i class="fas fa-sticky-note"></i>
|
|
<span>Notiz hinzufügen</span>
|
|
</button>
|
|
<button id="save-layout-btn" class="mindmap-action-btn">
|
|
<i class="fas fa-save"></i>
|
|
<span>Layout speichern</span>
|
|
</button>
|
|
<button id="export-btn" class="mindmap-action-btn">
|
|
<i class="fas fa-file-export"></i>
|
|
<span>Exportieren</span>
|
|
</button>
|
|
<button id="import-btn" class="mindmap-action-btn">
|
|
<i class="fas fa-file-import"></i>
|
|
<span>Importieren</span>
|
|
</button>
|
|
<button id="toggle-public-mindmap-btn" class="mindmap-action-btn">
|
|
<i class="fas fa-globe"></i>
|
|
<span>Öffentliche Mindmap</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Mindmap Container mit Positionsindikator -->
|
|
<div class="relative rounded-xl overflow-hidden border transition-all duration-300"
|
|
x-bind:class="darkMode ? 'border-gray-700/50' : 'border-gray-300/50'">
|
|
<div id="cy" data-mindmap-id="{{ mindmap.id }}"></div>
|
|
|
|
<!-- Container für öffentliche Mindmap (zur Knotenübernahme) -->
|
|
<div id="public-mindmap-container" class="hidden mt-4 border-t border-gray-300 dark:border-gray-700 pt-4" style="height: 300px;">
|
|
<h3 class="text-lg font-semibold mb-2 text-center">Öffentliche Mindmap</h3>
|
|
<div id="public-cy" style="width: 100%; height: 250px;"></div>
|
|
</div>
|
|
|
|
<!-- Informationsanzeige für ausgewählten Knoten -->
|
|
<div id="node-info-panel" class="node-info-panel p-4">
|
|
<h3 class="text-xl font-bold gradient-text mb-2">Knotendetails</h3>
|
|
<p id="node-description" class="mb-4 opacity-80"></p>
|
|
|
|
<h4 class="font-semibold mb-2 opacity-90">Verbundene Knoten</h4>
|
|
<div id="connected-nodes" class="flex flex-wrap mb-4"></div>
|
|
|
|
<div class="flex flex-wrap gap-2 mt-4">
|
|
<button id="add-thought-btn" class="mindmap-action-btn">
|
|
<i class="fas fa-lightbulb"></i>
|
|
<span>Gedanke hinzufügen</span>
|
|
</button>
|
|
<button id="view-thoughts-btn" class="mindmap-action-btn">
|
|
<i class="fas fa-list"></i>
|
|
<span>Gedanken anzeigen</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script src="{{ url_for('static', filename='js/cytoscape.min.js') }}"></script>
|
|
<script src="{{ url_for('static', filename='js/mindmap-init.js') }}"></script>
|
|
<script nonce="{{ csp_nonce }}">
|
|
document.addEventListener('DOMContentLoaded', async function() {
|
|
const cyContainer = document.getElementById('cy');
|
|
if (!cyContainer) {
|
|
console.error("Mindmap container #cy not found!");
|
|
return;
|
|
}
|
|
const mindmapId = cyContainer.dataset.mindmapId;
|
|
const nodeInfoPanel = document.getElementById('node-info-panel');
|
|
const nodeDescription = document.getElementById('node-description');
|
|
const connectedNodesContainer = document.getElementById('connected-nodes');
|
|
const mindmapNameH1 = document.querySelector('h1.gradient-text');
|
|
const mindmapDescriptionP = document.querySelector('p.opacity-80.mt-1');
|
|
|
|
// Funktion zum Anzeigen von Benachrichtigungen (vereinfacht)
|
|
function showUINotification(message, type = 'success') {
|
|
const notificationArea = document.getElementById('notification-area-usr') || createUINotificationArea();
|
|
const notificationId = `notif-usr-${Date.now()}`;
|
|
const bgColor = type === 'success' ? 'bg-green-500' : (type === 'error' ? 'bg-red-500' : 'bg-blue-500');
|
|
|
|
const notificationElement = `
|
|
<div id="${notificationId}" class="p-3 mb-3 text-sm text-white rounded-lg ${bgColor} animate-fadeIn" role="alert">
|
|
<span>${message}</span>
|
|
</div>
|
|
`;
|
|
notificationArea.insertAdjacentHTML('beforeend', notificationElement);
|
|
|
|
setTimeout(() => {
|
|
const el = document.getElementById(notificationId);
|
|
if (el) {
|
|
el.classList.add('animate-fadeOut');
|
|
setTimeout(() => el.remove(), 500);
|
|
}
|
|
}, 3000);
|
|
}
|
|
|
|
function createUINotificationArea() {
|
|
const area = document.createElement('div');
|
|
area.id = 'notification-area-usr';
|
|
area.className = 'fixed top-20 right-5 z-[1001] w-auto max-w-xs'; // höhere z-index
|
|
document.body.appendChild(area);
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
.animate-fadeIn { animation: fadeIn 0.3s ease-out; }
|
|
.animate-fadeOut { animation: fadeOut 0.3s ease-in forwards; }
|
|
@keyframes fadeIn { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: translateX(0); } }
|
|
@keyframes fadeOut { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(20px); } }
|
|
`;
|
|
document.head.appendChild(style);
|
|
return area;
|
|
}
|
|
|
|
async function fetchMindmapData(id) {
|
|
try {
|
|
const response = await fetch(`/api/mindmaps/${id}`);
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error('Fehler beim Laden der Mindmap-Daten:', error);
|
|
showUINotification('Fehler beim Laden der Mindmap-Daten.', 'error');
|
|
cyContainer.innerHTML = '<p class="text-center text-red-500 p-10">Konnte Mindmap nicht laden.</p>';
|
|
return null;
|
|
}
|
|
}
|
|
|
|
const mindmapData = await fetchMindmapData(mindmapId);
|
|
|
|
if (mindmapData) {
|
|
if(mindmapNameH1) mindmapNameH1.textContent = mindmapData.name;
|
|
if(mindmapDescriptionP) mindmapDescriptionP.textContent = mindmapData.description || "Keine Beschreibung vorhanden.";
|
|
|
|
// Cytoscape initialisieren
|
|
const cy = cytoscape({
|
|
container: cyContainer,
|
|
elements: mindmapData.elements || [], // Verwende 'elements' aus der API-Antwort
|
|
style: [
|
|
{
|
|
selector: 'node',
|
|
style: {
|
|
'background-color': 'data(color)',
|
|
'label': 'data(label)',
|
|
'color': 'data(fontColor)',
|
|
'text-valign': 'center',
|
|
'text-halign': 'center',
|
|
'font-size': 'data(fontSize)',
|
|
'width': ele => ele.data('isCenter') ? 60 : (ele.data('size') || 40),
|
|
'height': ele => ele.data('isCenter') ? 60 : (ele.data('size') || 40),
|
|
'border-width': 2,
|
|
'border-color': '#fff',
|
|
'shape': 'ellipse',
|
|
'text-outline-color': '#555',
|
|
'text-outline-width': 1,
|
|
}
|
|
},
|
|
{
|
|
selector: 'edge',
|
|
style: {
|
|
'width': ele => ele.data('strength') ? ele.data('strength') * 1.5 : 2,
|
|
'line-color': ele => ele.data('color') || '#9dbaea',
|
|
'target-arrow-color': ele => ele.data('color') || '#9dbaea',
|
|
'target-arrow-shape': 'triangle',
|
|
'curve-style': 'bezier'
|
|
}
|
|
},
|
|
{
|
|
selector: 'node:selected',
|
|
style: {
|
|
'border-color': '#f59e42',
|
|
'border-width': 3,
|
|
}
|
|
}
|
|
],
|
|
layout: {
|
|
name: 'cose',
|
|
idealEdgeLength: 100,
|
|
nodeOverlap: 20,
|
|
refresh: 20,
|
|
fit: true,
|
|
padding: 30,
|
|
randomize: false,
|
|
componentSpacing: 100,
|
|
nodeRepulsion: 400000,
|
|
edgeElasticity: 100,
|
|
nestingFactor: 5,
|
|
gravity: 80,
|
|
numIter: 1000,
|
|
initialTemp: 200,
|
|
coolingFactor: 0.95,
|
|
minTemp: 1.0
|
|
}
|
|
});
|
|
window.cyInstance = cy; // Für globalen Zugriff falls nötig
|
|
|
|
cy.on('tap', 'node', function(evt){
|
|
const node = evt.target;
|
|
if (nodeDescription) nodeDescription.textContent = node.data('description') || 'Keine Beschreibung für diesen Knoten.';
|
|
|
|
if (connectedNodesContainer) {
|
|
connectedNodesContainer.innerHTML = '';
|
|
const connected = node.connectedEdges().otherNodes();
|
|
if (connected.length > 0) {
|
|
connected.forEach(cn => {
|
|
const link = document.createElement('span');
|
|
link.className = 'node-link';
|
|
link.textContent = cn.data('label');
|
|
link.style.backgroundColor = cn.data('color') || '#60a5fa';
|
|
link.addEventListener('click', () => {
|
|
cy.center(cn);
|
|
cn.select();
|
|
// Info Panel für den geklickten verbundenen Knoten aktualisieren
|
|
if (nodeDescription) nodeDescription.textContent = cn.data('description') || 'Keine Beschreibung für diesen Knoten.';
|
|
// Rekursiv verbundene Knoten des neu ausgewählten Knotens anzeigen (optional)
|
|
});
|
|
connectedNodesContainer.appendChild(link);
|
|
});
|
|
} else {
|
|
connectedNodesContainer.innerHTML = '<p class="opacity-70 text-sm">Keine direkten Verbindungen.</p>';
|
|
}
|
|
}
|
|
if (nodeInfoPanel) nodeInfoPanel.classList.add('visible');
|
|
});
|
|
|
|
cy.on('tap', function(evt){
|
|
if(evt.target === cy){ // Klick auf Hintergrund
|
|
if (nodeInfoPanel) nodeInfoPanel.classList.remove('visible');
|
|
}
|
|
});
|
|
|
|
// Toolbar-Buttons
|
|
document.getElementById('fit-btn')?.addEventListener('click', () => cy.fit(null, 50));
|
|
document.getElementById('reset-btn')?.addEventListener('click', () => cy.layout({name: 'cose', animate:true}).run());
|
|
|
|
let labelsVisible = true;
|
|
document.getElementById('toggle-labels-btn')?.addEventListener('click', () => {
|
|
labelsVisible = !labelsVisible;
|
|
cy.style().selector('node').style({'text-opacity': labelsVisible ? 1 : 0}).update();
|
|
});
|
|
|
|
// Notizfunktion implementieren
|
|
document.getElementById('add-note-btn').addEventListener('click', function() {
|
|
const selectedNodes = cy.$('node:selected');
|
|
if (selectedNodes.length === 0) {
|
|
showUINotification('Bitte wählen Sie einen Knoten aus, um eine Notiz hinzuzufügen.', 'info');
|
|
return;
|
|
}
|
|
|
|
const nodeId = selectedNodes[0].id();
|
|
|
|
// Notizen für den ausgewählten Knoten laden
|
|
fetch(`/api/mindmap/node/${nodeId}/notes`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
// Notiz-Dialog anzeigen
|
|
showNoteDialog(nodeId, data.notes || '', data.color_code || '#FFF59D');
|
|
} else {
|
|
showUINotification('Fehler beim Laden der Notizen: ' + data.message, 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Fehler beim Laden der Notizen:', error);
|
|
showUINotification('Fehler beim Laden der Notizen', 'error');
|
|
});
|
|
});
|
|
|
|
// Layout speichern implementieren
|
|
document.getElementById('save-layout-btn').addEventListener('click', function() {
|
|
const mindmapId = parseInt("{{ mindmap.id }}");
|
|
const nodes = [];
|
|
|
|
// Sammle die Positionen aller Knoten
|
|
cy.nodes().forEach(node => {
|
|
nodes.push({
|
|
id: node.id(),
|
|
x: node.position().x,
|
|
y: node.position().y
|
|
});
|
|
});
|
|
|
|
// Layout speichern
|
|
fetch(`/api/mindmap/${mindmapId}/layout`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': '{{ csrf_token() }}'
|
|
},
|
|
body: JSON.stringify({ nodes: nodes })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showUINotification('Layout erfolgreich gespeichert', 'success');
|
|
} else {
|
|
showUINotification('Fehler beim Speichern des Layouts: ' + data.message, 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Fehler beim Speichern des Layouts:', error);
|
|
showUINotification('Fehler beim Speichern des Layouts', 'error');
|
|
});
|
|
});
|
|
|
|
// Öffentliche Mindmap anzeigen/verbergen
|
|
document.getElementById('toggle-public-mindmap-btn').addEventListener('click', function() {
|
|
const container = document.getElementById('public-mindmap-container');
|
|
if (container.classList.contains('hidden')) {
|
|
container.classList.remove('hidden');
|
|
this.querySelector('span').textContent = 'Öffentliche Mindmap verbergen';
|
|
loadPublicMindmap();
|
|
} else {
|
|
container.classList.add('hidden');
|
|
this.querySelector('span').textContent = 'Öffentliche Mindmap';
|
|
}
|
|
});
|
|
|
|
// Funktion zum Anzeigen des Notiz-Dialogs
|
|
function showNoteDialog(nodeId, noteContent, colorCode) {
|
|
// Dialog erstellen
|
|
const overlay = document.createElement('div');
|
|
overlay.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50';
|
|
overlay.id = 'note-dialog-overlay';
|
|
|
|
overlay.innerHTML = `
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4">
|
|
<h3 class="text-xl font-bold mb-4">Notizen bearbeiten</h3>
|
|
<textarea id="note-content" class="w-full h-40 p-2 border rounded-lg mb-4 dark:bg-gray-700 dark:border-gray-600 dark:text-white">${noteContent}</textarea>
|
|
<div class="flex items-center mb-4">
|
|
<label class="mr-2">Farbe:</label>
|
|
<input type="color" id="note-color" value="${colorCode}" class="cursor-pointer">
|
|
</div>
|
|
<div class="flex justify-end gap-2">
|
|
<button id="cancel-note" class="px-4 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-lg">Abbrechen</button>
|
|
<button id="save-note" class="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg">Speichern</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(overlay);
|
|
|
|
// Event-Listener für Buttons
|
|
document.getElementById('cancel-note').addEventListener('click', function() {
|
|
document.getElementById('note-dialog-overlay').remove();
|
|
});
|
|
|
|
document.getElementById('save-note').addEventListener('click', function() {
|
|
const noteContent = document.getElementById('note-content').value;
|
|
const colorCode = document.getElementById('note-color').value;
|
|
|
|
// Notizen speichern
|
|
fetch(`/api/mindmap/node/${nodeId}/notes`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': '{{ csrf_token() }}'
|
|
},
|
|
body: JSON.stringify({
|
|
notes: noteContent,
|
|
color_code: colorCode
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showUINotification('Notizen erfolgreich gespeichert', 'success');
|
|
document.getElementById('note-dialog-overlay').remove();
|
|
|
|
// Knoten visuell markieren, der eine Notiz hat
|
|
if (noteContent.trim() !== '') {
|
|
cy.$id(nodeId).addClass('has-note');
|
|
|
|
// Stil für Knoten mit Notizen hinzufügen, falls noch nicht vorhanden
|
|
if (!cy.style().selector('node.has-note').style('border-color')) {
|
|
cy.style()
|
|
.selector('node.has-note')
|
|
.style({
|
|
'border-color': colorCode,
|
|
'border-width': 3
|
|
})
|
|
.update();
|
|
}
|
|
} else {
|
|
cy.$id(nodeId).removeClass('has-note');
|
|
}
|
|
} else {
|
|
showUINotification('Fehler beim Speichern der Notizen: ' + data.message, 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Fehler beim Speichern der Notizen:', error);
|
|
showUINotification('Fehler beim Speichern der Notizen', 'error');
|
|
});
|
|
});
|
|
}
|
|
|
|
// Funktion zum Laden der öffentlichen Mindmap
|
|
function loadPublicMindmap() {
|
|
const container = document.getElementById('public-cy');
|
|
|
|
// Lade-Anzeige
|
|
container.innerHTML = '<div class="flex justify-center items-center h-full"><p>Lade öffentliche Mindmap...</p></div>';
|
|
|
|
// Daten abrufen
|
|
fetch('/api/public-mindmap')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
container.innerHTML = '';
|
|
|
|
// Initialisiere Cytoscape für die öffentliche Mindmap
|
|
const publicCy = cytoscape({
|
|
container: container,
|
|
elements: [],
|
|
style: [
|
|
{
|
|
selector: 'node',
|
|
style: {
|
|
'background-color': 'data(color_code)',
|
|
'label': 'data(name)',
|
|
'color': '#FFFFFF',
|
|
'text-valign': 'center',
|
|
'text-halign': 'center',
|
|
'width': '30px',
|
|
'height': '30px',
|
|
'text-wrap': 'wrap',
|
|
'text-max-width': '80px',
|
|
'font-size': '10px',
|
|
'border-width': '2px',
|
|
'border-color': '#FFFFFF',
|
|
'text-outline-color': '#555',
|
|
'text-outline-width': 1
|
|
}
|
|
},
|
|
{
|
|
selector: 'edge',
|
|
style: {
|
|
'width': 1,
|
|
'line-color': '#9CA3AF',
|
|
'target-arrow-color': '#9CA3AF',
|
|
'target-arrow-shape': 'triangle',
|
|
'curve-style': 'bezier'
|
|
}
|
|
},
|
|
{
|
|
selector: 'node:selected',
|
|
style: {
|
|
'border-color': '#F59E0B',
|
|
'border-width': '3px'
|
|
}
|
|
}
|
|
],
|
|
layout: {
|
|
name: 'cose',
|
|
padding: 30
|
|
}
|
|
});
|
|
|
|
// Elemente zur öffentlichen Mindmap hinzufügen
|
|
const elements = [];
|
|
|
|
// Knoten hinzufügen
|
|
data.nodes.forEach(node => {
|
|
elements.push({
|
|
group: 'nodes',
|
|
data: {
|
|
id: 'pub_' + node.id,
|
|
originalId: node.id,
|
|
name: node.name,
|
|
description: node.description,
|
|
color_code: node.color_code
|
|
}
|
|
});
|
|
});
|
|
|
|
// Kanten hinzufügen
|
|
data.edges.forEach(edge => {
|
|
elements.push({
|
|
group: 'edges',
|
|
data: {
|
|
source: 'pub_' + edge.source,
|
|
target: 'pub_' + edge.target
|
|
}
|
|
});
|
|
});
|
|
|
|
publicCy.add(elements);
|
|
publicCy.layout({ name: 'cose' }).run();
|
|
|
|
// Event-Listener für Knoten-Klicks in der öffentlichen Mindmap
|
|
publicCy.on('tap', 'node', function(evt) {
|
|
const node = evt.target;
|
|
const nodeData = node.data();
|
|
|
|
// Dialog zum Hinzufügen des Knotens anzeigen
|
|
showAddNodeDialog(nodeData.originalId, nodeData.name, nodeData.description);
|
|
});
|
|
|
|
} else {
|
|
container.innerHTML = '<div class="flex justify-center items-center h-full"><p class="text-red-500">Fehler beim Laden der öffentlichen Mindmap</p></div>';
|
|
showUINotification('Fehler beim Laden der öffentlichen Mindmap: ' + data.message, 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Fehler beim Laden der öffentlichen Mindmap:', error);
|
|
container.innerHTML = '<div class="flex justify-center items-center h-full"><p class="text-red-500">Fehler beim Laden der öffentlichen Mindmap</p></div>';
|
|
showUINotification('Fehler beim Laden der öffentlichen Mindmap', 'error');
|
|
});
|
|
}
|
|
|
|
// Funktion zum Anzeigen des Dialogs zum Hinzufügen eines Knotens
|
|
function showAddNodeDialog(nodeId, nodeName, nodeDescription) {
|
|
// Dialog erstellen
|
|
const overlay = document.createElement('div');
|
|
overlay.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50';
|
|
overlay.id = 'add-node-dialog-overlay';
|
|
|
|
overlay.innerHTML = `
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4">
|
|
<h3 class="text-xl font-bold mb-4">Knoten zu Ihrer Mindmap hinzufügen</h3>
|
|
<div class="mb-4">
|
|
<p class="font-semibold">${nodeName}</p>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400">${nodeDescription || 'Keine Beschreibung'}</p>
|
|
</div>
|
|
<div class="mb-4">
|
|
<label class="block mb-2">Mit Knoten verbinden (optional):</label>
|
|
<select id="parent-node-select" class="w-full p-2 border rounded-lg dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
|
<option value="">Nicht verbinden</option>
|
|
</select>
|
|
</div>
|
|
<div class="flex justify-end gap-2">
|
|
<button id="cancel-add-node" class="px-4 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-lg">Abbrechen</button>
|
|
<button id="confirm-add-node" class="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg">Hinzufügen</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(overlay);
|
|
|
|
// Fülle den Parent-Node-Selector mit vorhandenen Knoten
|
|
const parentSelect = document.getElementById('parent-node-select');
|
|
cy.nodes().forEach(node => {
|
|
const option = document.createElement('option');
|
|
option.value = node.id();
|
|
option.textContent = node.data('label');
|
|
parentSelect.appendChild(option);
|
|
});
|
|
|
|
// Event-Listener für Buttons
|
|
document.getElementById('cancel-add-node').addEventListener('click', function() {
|
|
document.getElementById('add-node-dialog-overlay').remove();
|
|
});
|
|
|
|
document.getElementById('confirm-add-node').addEventListener('click', function() {
|
|
const parentNodeId = document.getElementById('parent-node-select').value;
|
|
const mindmapId = parseInt("{{ mindmap.id }}");
|
|
|
|
// Generiere Startposition basierend auf Parent oder Zufallsposition
|
|
let position = { x: 0, y: 0 };
|
|
|
|
if (parentNodeId) {
|
|
const parentNode = cy.$id(parentNodeId);
|
|
if (parentNode) {
|
|
// Positioniere in der Nähe des Elternknotens
|
|
position = {
|
|
x: parentNode.position('x') + Math.random() * 100 - 50,
|
|
y: parentNode.position('y') + Math.random() * 100 - 50
|
|
};
|
|
}
|
|
} else {
|
|
// Zufällige Position im sichtbaren Bereich
|
|
const extent = cy.extent();
|
|
position = {
|
|
x: extent.x1 + Math.random() * (extent.x2 - extent.x1),
|
|
y: extent.y1 + Math.random() * (extent.y2 - extent.y1)
|
|
};
|
|
}
|
|
|
|
// Knoten zur Mindmap hinzufügen
|
|
fetch(`/api/mindmap/${mindmapId}/add_node`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': '{{ csrf_token() }}'
|
|
},
|
|
body: JSON.stringify({
|
|
node_id: nodeId,
|
|
parent_id: parentNodeId || null,
|
|
x: position.x,
|
|
y: position.y
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showUINotification('Knoten erfolgreich hinzugefügt', 'success');
|
|
document.getElementById('add-node-dialog-overlay').remove();
|
|
|
|
// Aktualisiere die Mindmap (neu laden oder Knoten hinzufügen)
|
|
// Option 1: Seite neu laden
|
|
// window.location.reload();
|
|
|
|
// Option 2: Knoten dynamisch hinzufügen (eleganter)
|
|
fetch(`/api/mindmap/node/${nodeId}`)
|
|
.then(response => response.json())
|
|
.then(nodeData => {
|
|
// Füge den neuen Knoten zur Anzeige hinzu
|
|
cy.add({
|
|
group: 'nodes',
|
|
data: {
|
|
id: nodeId,
|
|
label: nodeData.name,
|
|
description: nodeData.description,
|
|
color: nodeData.color_code,
|
|
fontColor: '#FFFFFF'
|
|
},
|
|
position: position
|
|
});
|
|
|
|
// Füge auch die Kante hinzu, falls ein Elternknoten ausgewählt wurde
|
|
if (parentNodeId) {
|
|
cy.add({
|
|
group: 'edges',
|
|
data: {
|
|
source: parentNodeId,
|
|
target: nodeId
|
|
}
|
|
});
|
|
}
|
|
|
|
// Wähle den neuen Knoten aus
|
|
cy.$id(nodeId).select();
|
|
})
|
|
.catch(error => {
|
|
console.error('Fehler beim Laden der Knotendaten:', error);
|
|
// Fallback: Seite neu laden
|
|
window.location.reload();
|
|
});
|
|
|
|
} else {
|
|
showUINotification('Fehler beim Hinzufügen des Knotens: ' + (data.message || data.error), 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Fehler beim Hinzufügen des Knotens:', error);
|
|
showUINotification('Fehler beim Hinzufügen des Knotens', 'error');
|
|
});
|
|
});
|
|
}
|
|
|
|
} else {
|
|
// Fallback, falls mindmapData null ist
|
|
if(mindmapNameH1) mindmapNameH1.textContent = "Mindmap nicht gefunden";
|
|
cyContainer.innerHTML = '<p class="text-center text-red-500 p-10">Die angeforderte Mindmap konnte nicht geladen werden.</p>';
|
|
}
|
|
}); // End of DOMContentLoaded
|
|
</script>
|
|
{% endblock %} |