bomindflow / index.html
tukangkustom's picture
Add 3 files
ca06a8e verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MindMapper Pro</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
@keyframes buttonClick {
0% { transform: scale(1); }
50% { transform: scale(0.95); }
100% { transform: scale(1); }
}
@keyframes buttonHover {
from { transform: translateY(0); }
to { transform: translateY(-2px); }
}
@keyframes buttonActive {
0% { transform: scale(1); }
50% { transform: scale(0.98); }
100% { transform: scale(1); }
}
@keyframes ripple {
0% { transform: scale(0); opacity: 1; }
100% { transform: scale(4); opacity: 0; }
}
.node-animation {
animation: fadeIn 0.3s ease-out forwards;
}
.pulse-animation {
animation: pulse 1.5s infinite;
}
.button-click {
animation: buttonClick 0.3s ease;
}
.wireframe {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.9);
z-index: 1000;
display: none;
overflow-y: auto;
padding: 20px;
}
.connector {
position: absolute;
background-color: #94a3b8;
z-index: -1;
}
.mindmap-container {
transform-origin: center center;
transition: transform 0.3s ease;
}
.node-content {
min-width: 120px;
min-height: 40px;
}
.color-palette {
display: none;
position: absolute;
z-index: 10;
background: white;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
padding: 8px;
}
.color-option {
width: 24px;
height: 24px;
border-radius: 50%;
margin: 4px;
cursor: pointer;
border: 2px solid transparent;
transition: transform 0.2s;
}
.color-option:hover {
transform: scale(1.1);
}
.color-option.selected {
border-color: #000;
}
.tooltip {
position: absolute;
background-color: #333;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
z-index: 100;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s;
}
.button-container:hover .tooltip {
opacity: 1;
}
.tutorial-highlight {
animation: pulse 2s infinite;
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.5);
border-radius: 8px;
}
.node-text {
cursor: text;
}
.node-text:focus {
outline: none;
background-color: rgba(255, 255, 255, 0.2);
border-radius: 4px;
padding: 2px 4px;
}
/* Enhanced button styles */
.btn {
position: relative;
overflow: hidden;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
animation: buttonHover 0.3s ease-out forwards;
}
.btn:active {
transform: scale(0.98);
animation: buttonActive 0.2s ease-out forwards;
}
.btn-primary {
background-color: #4f46e5;
color: white;
}
.btn-primary:hover {
background-color: #4338ca;
}
.btn-secondary {
background-color: white;
color: #4f46e5;
border: 1px solid #e5e7eb;
}
.btn-secondary:hover {
background-color: #f9fafb;
}
.btn-danger {
background-color: #ef4444;
color: white;
}
.btn-danger:hover {
background-color: #dc2626;
}
.btn-warning {
background-color: #f59e0b;
color: white;
}
.btn-warning:hover {
background-color: #d97706;
}
.btn-success {
background-color: #10b981;
color: white;
}
.btn-success:hover {
background-color: #059669;
}
.btn-info {
background-color: #3b82f6;
color: white;
}
.btn-info:hover {
background-color: #2563eb;
}
.ripple {
position: absolute;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.4);
transform: scale(0);
animation: ripple 0.6s linear;
pointer-events: none;
}
/* Link creation mode */
.link-mode {
background-color: rgba(79, 70, 229, 0.2);
border: 2px dashed #4f46e5;
}
.node-linkable {
cursor: crosshair;
box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.5);
}
.custom-link {
stroke: #4f46e5;
stroke-width: 2;
stroke-dasharray: 5, 5;
}
</style>
</head>
<body class="bg-gray-50 font-sans">
<!-- Wireframe Overlay -->
<div id="wireframe" class="wireframe">
<div class="container mx-auto">
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-bold text-gray-800">MindMapper Wireframe</h2>
<button id="close-wireframe" class="btn btn-danger px-4 py-2 rounded-lg transition flex items-center">
<i class="fas fa-times mr-2"></i> Close Wireframe
</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div class="bg-white p-6 rounded-lg shadow-md">
<h3 class="text-xl font-semibold mb-4 text-gray-700">Main Interface</h3>
<div class="space-y-4">
<div class="h-8 bg-blue-100 rounded"></div>
<div class="h-8 bg-blue-100 rounded w-3/4"></div>
<div class="flex space-x-4">
<div class="h-8 bg-blue-200 rounded w-1/4"></div>
<div class="h-8 bg-blue-200 rounded w-1/4"></div>
<div class="h-8 bg-blue-200 rounded w-1/4"></div>
</div>
<div class="h-64 bg-gray-100 rounded-lg flex items-center justify-center">
<div class="text-center">
<div class="h-6 bg-blue-300 rounded-full w-6 mx-auto mb-2"></div>
<div class="h-4 bg-blue-300 rounded w-24 mx-auto"></div>
</div>
</div>
</div>
</div>
<div class="bg-white p-6 rounded-lg shadow-md">
<h3 class="text-xl font-semibold mb-4 text-gray-700">Node Structure</h3>
<div class="flex justify-center">
<div class="relative">
<div class="h-8 bg-green-300 rounded-full w-8 mx-auto mb-2"></div>
<div class="h-4 bg-green-300 rounded w-20 mx-auto mb-6"></div>
<div class="flex justify-center space-x-8">
<div class="text-center">
<div class="h-6 bg-green-200 rounded-full w-6 mx-auto mb-2"></div>
<div class="h-3 bg-green-200 rounded w-16 mx-auto"></div>
</div>
<div class="text-center">
<div class="h-6 bg-green-200 rounded-full w-6 mx-auto mb-2"></div>
<div class="h-3 bg-green-200 rounded w-16 mx-auto"></div>
</div>
</div>
</div>
</div>
</div>
<div class="bg-white p-6 rounded-lg shadow-md">
<h3 class="text-xl font-semibold mb-4 text-gray-700">Toolbar</h3>
<div class="flex flex-wrap gap-2">
<div class="h-8 w-8 bg-purple-200 rounded"></div>
<div class="h-8 w-8 bg-purple-200 rounded"></div>
<div class="h-8 w-8 bg-purple-200 rounded"></div>
<div class="h-8 w-8 bg-purple-200 rounded"></div>
<div class="h-8 w-8 bg-purple-200 rounded"></div>
<div class="h-8 w-16 bg-purple-300 rounded"></div>
</div>
</div>
<div class="bg-white p-6 rounded-lg shadow-md">
<h3 class="text-xl font-semibold mb-4 text-gray-700">Color Palette</h3>
<div class="flex flex-wrap gap-2">
<div class="h-8 w-8 bg-red-400 rounded-full"></div>
<div class="h-8 w-8 bg-blue-400 rounded-full"></div>
<div class="h-8 w-8 bg-green-400 rounded-full"></div>
<div class="h-8 w-8 bg-yellow-400 rounded-full"></div>
<div class="h-8 w-8 bg-purple-400 rounded-full"></div>
<div class="h-8 w-8 bg-pink-400 rounded-full"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Main App -->
<div class="min-h-screen flex flex-col">
<!-- Header -->
<header class="bg-indigo-600 text-white shadow-lg">
<div class="container mx-auto px-4 py-3 flex justify-between items-center">
<div class="flex items-center space-x-2">
<i class="fas fa-brain text-2xl"></i>
<h1 class="text-2xl font-bold">MindMapper Pro</h1>
</div>
<div class="flex items-center space-x-4">
<button id="tutorial-btn" class="btn btn-warning px-4 py-2 rounded-lg transition flex items-center">
<i class="fas fa-question-circle mr-2"></i> Tutorial
</button>
<button id="show-wireframe" class="btn btn-secondary px-4 py-2 rounded-lg transition flex items-center">
<i class="fas fa-project-diagram mr-2"></i> Wireframe
</button>
<button id="export-btn" class="btn btn-primary px-4 py-2 rounded-lg transition flex items-center">
<i class="fas fa-file-export mr-2"></i> Export
</button>
</div>
</div>
</header>
<!-- Toolbar -->
<div class="bg-gray-100 border-b border-gray-200 py-2 px-4">
<div class="container mx-auto flex flex-wrap items-center gap-3">
<div class="button-container relative">
<button id="add-child-btn" class="btn btn-success px-3 py-1 rounded-lg transition flex items-center">
<i class="fas fa-plus-circle mr-2"></i> Add Child
</button>
<div class="tooltip">Add a child node to selected node (Enter)</div>
</div>
<div class="button-container relative">
<button id="add-sibling-btn" class="btn btn-info px-3 py-1 rounded-lg transition flex items-center">
<i class="fas fa-plus-square mr-2"></i> Add Sibling
</button>
<div class="tooltip">Add a sibling node to selected node (Shift+Enter)</div>
</div>
<div class="button-container relative">
<button id="delete-node-btn" class="btn btn-danger px-3 py-1 rounded-lg transition flex items-center">
<i class="fas fa-trash-alt mr-2"></i> Delete
</button>
<div class="tooltip">Delete selected node (Delete)</div>
</div>
<div class="button-container relative">
<button id="edit-text-btn" class="btn btn-secondary px-3 py-1 rounded-lg transition flex items-center">
<i class="fas fa-edit mr-2"></i> Edit Text
</button>
<div class="tooltip">Edit node text (Double Click)</div>
</div>
<div class="button-container relative">
<div class="relative">
<button id="color-btn" class="btn btn-secondary px-3 py-1 rounded-lg transition flex items-center">
<i class="fas fa-palette mr-2"></i> Color
</button>
<div class="tooltip">Change node color</div>
<div id="color-palette" class="color-palette grid grid-cols-5 gap-1">
<div class="color-option bg-red-400" data-color="bg-red-400"></div>
<div class="color-option bg-blue-400" data-color="bg-blue-400"></div>
<div class="color-option bg-green-400" data-color="bg-green-400"></div>
<div class="color-option bg-yellow-400" data-color="bg-yellow-400"></div>
<div class="color-option bg-purple-400" data-color="bg-purple-400"></div>
<div class="color-option bg-pink-400" data-color="bg-pink-400"></div>
<div class="color-option bg-indigo-400" data-color="bg-indigo-400"></div>
<div class="color-option bg-teal-400" data-color="bg-teal-400"></div>
<div class="color-option bg-orange-400" data-color="bg-orange-400"></div>
<div class="color-option bg-gray-400" data-color="bg-gray-400"></div>
</div>
</div>
</div>
<div class="button-container relative">
<button id="zoom-in-btn" class="btn btn-secondary px-3 py-1 rounded-lg transition flex items-center">
<i class="fas fa-search-plus mr-2"></i> Zoom In
</button>
<div class="tooltip">Zoom in (Ctrl + +)</div>
</div>
<div class="button-container relative">
<button id="zoom-out-btn" class="btn btn-secondary px-3 py-1 rounded-lg transition flex items-center">
<i class="fas fa-search-minus mr-2"></i> Zoom Out
</button>
<div class="tooltip">Zoom out (Ctrl + -)</div>
</div>
<div class="button-container relative">
<button id="reset-zoom-btn" class="btn btn-secondary px-3 py-1 rounded-lg transition flex items-center">
<i class="fas fa-expand mr-2"></i> Reset Zoom
</button>
<div class="tooltip">Reset zoom (Ctrl + 0)</div>
</div>
<div class="button-container relative">
<button id="toggle-layout-btn" class="btn btn-secondary px-3 py-1 rounded-lg transition flex items-center">
<i class="fas fa-sitemap mr-2"></i> Toggle Layout
</button>
<div class="tooltip">Switch between radial and horizontal layout (L)</div>
</div>
<div class="button-container relative">
<button id="animate-btn" class="btn btn-primary px-3 py-1 rounded-lg transition flex items-center">
<i class="fas fa-play-circle mr-2"></i> Animate
</button>
<div class="tooltip">Play visualization animation (A)</div>
</div>
<div class="button-container relative">
<button id="create-link-btn" class="btn btn-success px-3 py-1 rounded-lg transition flex items-center">
<i class="fas fa-link mr-2"></i> Create Link
</button>
<div class="tooltip">Create a custom link between nodes</div>
</div>
<div class="button-container relative">
<button id="undo-btn" class="btn btn-secondary px-3 py-1 rounded-lg transition flex items-center">
<i class="fas fa-undo mr-2"></i> Undo
</button>
<div class="tooltip">Undo last action (Ctrl+Z)</div>
</div>
<div class="button-container relative">
<button id="redo-btn" class="btn btn-secondary px-3 py-1 rounded-lg transition flex items-center">
<i class="fas fa-redo mr-2"></i> Redo
</button>
<div class="tooltip">Redo last action (Ctrl+Y)</div>
</div>
<div class="button-container relative">
<button id="center-view-btn" class="btn btn-secondary px-3 py-1 rounded-lg transition flex items-center">
<i class="fas fa-crosshairs mr-2"></i> Center View
</button>
<div class="tooltip">Center view on selected node (C)</div>
</div>
</div>
</div>
<!-- Main Content -->
<main class="flex-grow overflow-hidden relative">
<div id="mindmap-container" class="mindmap-container w-full h-full absolute">
<!-- Mind map will be rendered here -->
</div>
<!-- Temporary link line during creation -->
<svg id="temp-link" class="absolute top-0 left-0 w-full h-full pointer-events-none" style="z-index: 10; display: none;">
<line id="temp-line" class="custom-link" />
</svg>
<!-- Tutorial Highlights -->
<div id="tutorial-highlights" class="hidden">
<div id="tutorial-add-child" class="absolute tutorial-highlight"></div>
<div id="tutorial-add-sibling" class="absolute tutorial-highlight"></div>
<div id="tutorial-edit-text" class="absolute tutorial-highlight"></div>
<div id="tutorial-zoom" class="absolute tutorial-highlight"></div>
</div>
</main>
<!-- Status Bar -->
<footer class="bg-gray-800 text-gray-300 text-sm py-1 px-4">
<div class="container mx-auto flex justify-between items-center">
<div>
<span id="node-count">Nodes: 1</span>
<span class="mx-2">|</span>
<span id="zoom-level">Zoom: 100%</span>
<span class="mx-2">|</span>
<span id="layout-type">Layout: Radial</span>
<span class="mx-2">|</span>
<span id="link-mode-status" class="hidden text-indigo-300">Link Mode: Select source node</span>
</div>
<div>
<span id="status-message">Ready</span>
<span class="mx-2">|</span>
<span id="shortcut-hint">Tip: Press H for help</span>
</div>
</div>
</footer>
</div>
<!-- Export Modal -->
<div id="export-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-lg p-6 w-full max-w-md">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold">Export Mind Map</h3>
<button id="close-export-modal" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Format</label>
<select id="export-format" class="w-full border border-gray-300 rounded-lg px-3 py-2">
<option value="png">PNG Image</option>
<option value="json">JSON Data</option>
<option value="svg">SVG Vector</option>
<option value="pdf">PDF Document</option>
</select>
</div>
<div id="export-png-options">
<label class="block text-sm font-medium text-gray-700 mb-1">Quality</label>
<input type="range" id="export-quality" min="1" max="10" value="8" class="w-full">
<div class="flex justify-between text-xs text-gray-500">
<span>Low</span>
<span>High</span>
</div>
</div>
<div id="export-pdf-options" class="hidden">
<label class="block text-sm font-medium text-gray-700 mb-1">Page Size</label>
<select id="pdf-page-size" class="w-full border border-gray-300 rounded-lg px-3 py-2">
<option value="A4">A4</option>
<option value="Letter">Letter</option>
<option value="A3">A3</option>
</select>
</div>
<div class="flex justify-end space-x-3 pt-4">
<button id="cancel-export" class="btn btn-secondary px-4 py-2 rounded-lg">
Cancel
</button>
<button id="confirm-export" class="btn btn-primary px-4 py-2 rounded-lg">
Export
</button>
</div>
</div>
</div>
</div>
<!-- Help Modal -->
<div id="help-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-lg p-6 w-full max-w-2xl max-h-[80vh] overflow-y-auto">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold">MindMapper Help & Shortcuts</h3>
<button id="close-help-modal" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
<div class="space-y-4">
<div>
<h4 class="font-semibold text-lg mb-2">Basic Controls</h4>
<ul class="space-y-2">
<li><span class="font-mono bg-gray-100 px-2 py-1 rounded">Click</span> - Select a node</li>
<li><span class="font-mono bg-gray-100 px-2 py-1 rounded">Double Click</span> - Edit node text</li>
<li><span class="font-mono bg-gray-100 px-2 py-1 rounded">Drag</span> - Move the canvas</li>
</ul>
</div>
<div>
<h4 class="font-semibold text-lg mb-2">Node Operations</h4>
<ul class="space-y-2">
<li><span class="font-mono bg-gray-100 px-2 py-1 rounded">Enter</span> - Add child node</li>
<li><span class="font-mono bg-gray-100 px-2 py-1 rounded">Shift+Enter</span> - Add sibling node</li>
<li><span class="font-mono bg-gray-100 px-2 py-1 rounded">Delete</span> - Delete selected node</li>
<li><span class="font-mono bg-gray-100 px-2 py-1 rounded">Tab</span> - Focus on first child</li>
<li><span class="font-mono bg-gray-100 px-2 py-1 rounded">Shift+Tab</span> - Focus on parent</li>
<li><span class="font-mono bg-gray-100 px-2 py-1 rounded">L</span> - Create custom link between nodes</li>
</ul>
</div>
<div>
<h4 class="font-semibold text-lg mb-2">View Controls</h4>
<ul class="space-y-2">
<li><span class="font-mono bg-gray-100 px-2 py-1 rounded">Ctrl + +</span> - Zoom in</li>
<li><span class="font-mono bg-gray-100 px-2 py-1 rounded">Ctrl + -</span> - Zoom out</li>
<li><span class="font-mono bg-gray-100 px-2 py-1 rounded">Ctrl + 0</span> - Reset zoom</li>
<li><span class="font-mono bg-gray-100 px-2 py-1 rounded">L</span> - Toggle layout</li>
<li><span class="font-mono bg-gray-100 px-2 py-1 rounded">C</span> - Center view</li>
<li><span class="font-mono bg-gray-100 px-2 py-1 rounded">A</span> - Animate</li>
</ul>
</div>
<div>
<h4 class="font-semibold text-lg mb-2">Other Shortcuts</h4>
<ul class="space-y-2">
<li><span class="font-mono bg-gray-100 px-2 py-1 rounded">Ctrl+Z</span> - Undo</li>
<li><span class="font-mono bg-gray-100 px-2 py-1 rounded">Ctrl+Y</span> - Redo</li>
<li><span class="font-mono bg-gray-100 px-2 py-1 rounded">H</span> - Show help</li>
<li><span class="font-mono bg-gray-100 px-2 py-1 rounded">E</span> - Export</li>
</ul>
</div>
<div class="pt-4 border-t">
<button id="start-tutorial" class="btn btn-warning px-4 py-2 rounded-lg">
<i class="fas fa-play mr-2"></i> Start Interactive Tutorial
</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// App state
const state = {
nodes: [],
selectedNode: null,
nextId: 1,
zoomLevel: 1,
layout: 'radial', // 'radial' or 'horizontal'
connectors: [],
customLinks: [],
history: [],
historyIndex: -1,
isDragging: false,
dragStartX: 0,
dragStartY: 0,
translateX: 0,
translateY: 0,
tutorialStep: 0,
tutorialActive: false,
linkMode: false,
linkSourceNode: null
};
// DOM elements
const mindmapContainer = document.getElementById('mindmap-container');
const wireframe = document.getElementById('wireframe');
const showWireframeBtn = document.getElementById('show-wireframe');
const closeWireframeBtn = document.getElementById('close-wireframe');
const addChildBtn = document.getElementById('add-child-btn');
const addSiblingBtn = document.getElementById('add-sibling-btn');
const deleteNodeBtn = document.getElementById('delete-node-btn');
const editTextBtn = document.getElementById('edit-text-btn');
const colorBtn = document.getElementById('color-btn');
const colorPalette = document.getElementById('color-palette');
const zoomInBtn = document.getElementById('zoom-in-btn');
const zoomOutBtn = document.getElementById('zoom-out-btn');
const resetZoomBtn = document.getElementById('reset-zoom-btn');
const toggleLayoutBtn = document.getElementById('toggle-layout-btn');
const animateBtn = document.getElementById('animate-btn');
const createLinkBtn = document.getElementById('create-link-btn');
const exportBtn = document.getElementById('export-btn');
const exportModal = document.getElementById('export-modal');
const closeExportModal = document.getElementById('close-export-modal');
const cancelExport = document.getElementById('cancel-export');
const confirmExport = document.getElementById('confirm-export');
const nodeCountSpan = document.getElementById('node-count');
const zoomLevelSpan = document.getElementById('zoom-level');
const statusMessageSpan = document.getElementById('status-message');
const layoutTypeSpan = document.getElementById('layout-type');
const linkModeStatus = document.getElementById('link-mode-status');
const shortcutHintSpan = document.getElementById('shortcut-hint');
const tutorialBtn = document.getElementById('tutorial-btn');
const helpModal = document.getElementById('help-modal');
const closeHelpModal = document.getElementById('close-help-modal');
const startTutorialBtn = document.getElementById('start-tutorial');
const tutorialHighlights = document.getElementById('tutorial-highlights');
const exportFormatSelect = document.getElementById('export-format');
const undoBtn = document.getElementById('undo-btn');
const redoBtn = document.getElementById('redo-btn');
const centerViewBtn = document.getElementById('center-view-btn');
const tempLink = document.getElementById('temp-link');
const tempLine = document.getElementById('temp-line');
// Initialize the mind map with a root node
createNode('Main Idea', null, true);
saveState();
// Add ripple effect to buttons
function createRipple(event) {
const button = event.currentTarget;
const circle = document.createElement("span");
const rect = button.getBoundingClientRect();
const diameter = Math.max(rect.width, rect.height);
const radius = diameter / 2;
circle.style.width = circle.style.height = `${diameter}px`;
circle.style.left = `${event.clientX - rect.left - radius}px`;
circle.style.top = `${event.clientY - rect.top - radius}px`;
circle.classList.add("ripple");
const ripple = button.getElementsByClassName("ripple")[0];
if (ripple) {
ripple.remove();
}
button.appendChild(circle);
}
const buttons = document.querySelectorAll('.btn');
buttons.forEach(button => {
button.addEventListener('click', createRipple);
});
// Node creation function
function createNode(text, parentId, isRoot = false) {
const nodeId = state.nextId++;
const node = {
id: nodeId,
text: text,
parentId: parentId,
children: [],
color: isRoot ? 'bg-indigo-400' : 'bg-gray-200',
x: 0,
y: 0,
collapsed: false
};
state.nodes.push(node);
// If this isn't the root node, add it to its parent's children
if (parentId !== null) {
const parent = state.nodes.find(n => n.id === parentId);
if (parent) {
parent.children.push(nodeId);
}
}
// If this is the root node, select it
if (isRoot) {
selectNode(node);
}
renderMindMap();
updateNodeCount();
return node;
}
// Render the entire mind map
function renderMindMap() {
// Clear the container
mindmapContainer.innerHTML = '';
state.connectors = [];
// Create SVG for custom links
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute('class', 'absolute top-0 left-0 w-full h-full pointer-events-none');
svg.setAttribute('style', 'z-index: 0;');
mindmapContainer.appendChild(svg);
// Find root node(s)
const rootNodes = state.nodes.filter(node => node.parentId === null);
// Position and render nodes
rootNodes.forEach(rootNode => {
if (state.layout === 'radial') {
positionNodesRadially(rootNode, mindmapContainer.clientWidth / 2 + state.translateX, mindmapContainer.clientHeight / 2 + state.translateY, 0);
} else {
positionNodesHorizontally(rootNode, mindmapContainer.clientWidth / 2 + state.translateX, 100 + state.translateY, 200);
}
});
// Render custom links
state.customLinks.forEach(link => {
const fromNode = state.nodes.find(n => n.id === link.from);
const toNode = state.nodes.find(n => n.id === link.to);
if (fromNode && toNode) {
const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
line.setAttribute('x1', fromNode.x);
line.setAttribute('y1', fromNode.y);
line.setAttribute('x2', toNode.x);
line.setAttribute('y2', toNode.y);
line.setAttribute('class', 'custom-link');
line.setAttribute('stroke', '#4f46e5');
line.setAttribute('stroke-width', '2');
line.setAttribute('stroke-dasharray', '5,5');
svg.appendChild(line);
}
});
// Render connectors first (so they appear behind nodes)
renderConnectors();
// Then render nodes
state.nodes.forEach(node => {
if (!node.collapsed || node === state.selectedNode) {
renderNode(node);
}
});
// Update layout type display
layoutTypeSpan.textContent = `Layout: ${state.layout.charAt(0).toUpperCase() + state.layout.slice(1)}`;
// Center view if tutorial is active
if (state.tutorialActive) {
centerView();
}
}
// Position nodes in a radial layout
function positionNodesRadially(node, centerX, centerY, level, angle = 0, angleRange = Math.PI * 2) {
const radius = 150 + (level * 120);
const childCount = node.children.length;
if (level === 0) {
// Root node position
node.x = centerX;
node.y = centerY;
} else {
// Position child nodes in an arc
const angleStep = childCount > 1 ? angleRange / (childCount - 1) : 0;
const startAngle = angle - (angleRange / 2);
node.children.forEach((childId, index) => {
const childNode = state.nodes.find(n => n.id === childId);
if (childNode) {
const childAngle = startAngle + (index * angleStep);
childNode.x = centerX + Math.cos(childAngle) * radius;
childNode.y = centerY + Math.sin(childAngle) * radius;
// Store connector information
state.connectors.push({
fromX: node.x,
fromY: node.y,
toX: childNode.x,
toY: childNode.y,
color: node.color
});
// Position grandchildren recursively
if (!childNode.collapsed) {
positionNodesRadially(childNode, childNode.x, childNode.y, level + 1, childAngle, angleRange / 2);
}
}
});
}
}
// Position nodes in a horizontal layout
function positionNodesHorizontally(node, x, y, levelWidth) {
node.x = x;
node.y = y;
if (node.children.length > 0 && !node.collapsed) {
const childCount = node.children.length;
const totalWidth = (childCount - 1) * levelWidth;
const startX = x - totalWidth / 2;
node.children.forEach((childId, index) => {
const childNode = state.nodes.find(n => n.id === childId);
if (childNode) {
const childX = startX + (index * levelWidth);
const childY = y + 100;
childNode.x = childX;
childNode.y = childY;
// Store connector information
state.connectors.push({
fromX: node.x,
fromY: node.y,
toX: childNode.x,
toY: childNode.y,
color: node.color
});
// Position grandchildren recursively
positionNodesHorizontally(childNode, childX, childY, levelWidth * 0.7);
}
});
}
}
// Render a single node
function renderNode(node) {
const nodeElement = document.createElement('div');
nodeElement.className = `absolute node ${node.color} text-white rounded-lg shadow-md transition-all duration-200 hover:shadow-lg ${node === state.selectedNode ? 'ring-2 ring-yellow-400' : ''} ${state.linkMode ? 'node-linkable' : ''}`;
nodeElement.style.left = `${node.x}px`;
nodeElement.style.top = `${node.y}px`;
nodeElement.style.transform = `translate(-50%, -50%)`;
nodeElement.dataset.nodeId = node.id;
// Add animation class if this is a new node
if (node.id === state.nextId - 1) {
nodeElement.classList.add('node-animation');
}
// Node content
nodeElement.innerHTML = `
<div class="node-content p-3 flex flex-col items-center">
<div class="font-semibold text-center mb-1 node-text" contenteditable="false">${node.text}</div>
${node.children.length > 0 ? `
<button class="toggle-children text-xs bg-black bg-opacity-20 px-2 py-1 rounded-full mt-1" data-node-id="${node.id}">
${node.collapsed ? '<i class="fas fa-plus mr-1"></i> Expand' : '<i class="fas fa-minus mr-1"></i> Collapse'}
</button>
` : ''}
</div>
`;
// Add event listeners
nodeElement.addEventListener('click', (e) => {
if (!e.target.classList.contains('toggle-children') && !e.target.classList.contains('node-text')) {
if (state.linkMode) {
handleLinkModeClick(node);
} else {
selectNode(node);
}
}
});
// Double click to edit text
const textElement = nodeElement.querySelector('.node-text');
textElement.addEventListener('dblclick', () => {
editNodeText(textElement, node);
});
const toggleBtn = nodeElement.querySelector('.toggle-children');
if (toggleBtn) {
toggleBtn.addEventListener('click', (e) => {
e.stopPropagation();
toggleChildren(node);
});
}
mindmapContainer.appendChild(nodeElement);
}
// Handle node selection in link mode
function handleLinkModeClick(node) {
if (!state.linkSourceNode) {
// First node selected (source)
state.linkSourceNode = node;
linkModeStatus.textContent = `Link Mode: Select target node (ESC to cancel)`;
// Show temporary line
tempLink.style.display = 'block';
tempLine.setAttribute('x1', node.x);
tempLine.setAttribute('y1', node.y);
tempLine.setAttribute('x2', node.x);
tempLine.setAttribute('y2', node.y);
// Update line on mouse move
const updateTempLine = (e) => {
const rect = mindmapContainer.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
tempLine.setAttribute('x2', x);
tempLine.setAttribute('y2', y);
};
document.addEventListener('mousemove', updateTempLine);
// Cancel link mode on ESC
const cancelLinkMode = (e) => {
if (e.key === 'Escape') {
state.linkMode = false;
state.linkSourceNode = null;
linkModeStatus.classList.add('hidden');
tempLink.style.display = 'none';
document.removeEventListener('mousemove', updateTempLine);
document.removeEventListener('keydown', cancelLinkMode);
renderMindMap();
updateStatus('Link creation canceled');
}
};
document.addEventListener('keydown', cancelLinkMode);
} else {
// Second node selected (target)
if (node.id === state.linkSourceNode.id) {
updateStatus('Cannot link a node to itself', 'error');
return;
}
// Check if link already exists
const linkExists = state.customLinks.some(link =>
(link.from === state.linkSourceNode.id && link.to === node.id) ||
(link.from === node.id && link.to === state.linkSourceNode.id)
);
if (linkExists) {
updateStatus('Link already exists between these nodes', 'error');
return;
}
// Create the link
state.customLinks.push({
from: state.linkSourceNode.id,
to: node.id
});
// Exit link mode
state.linkMode = false;
state.linkSourceNode = null;
linkModeStatus.classList.add('hidden');
tempLink.style.display = 'none';
saveState();
renderMindMap();
updateStatus(`Created link between nodes`);
}
}
// Edit node text
function editNodeText(element, node) {
element.setAttribute('contenteditable', 'true');
element.focus();
const range = document.createRange();
range.selectNodeContents(element);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
function saveText() {
element.setAttribute('contenteditable', 'false');
const newText = element.textContent.trim();
if (newText !== node.text) {
node.text = newText;
saveState();
updateStatus(`Updated node text to "${node.text}"`);
}
element.removeEventListener('blur', saveText);
element.removeEventListener('keydown', handleTextEditKeydown);
}
function handleTextEditKeydown(e) {
if (e.key === 'Enter') {
e.preventDefault();
saveText();
} else if (e.key === 'Escape') {
element.textContent = node.text;
saveText();
}
}
element.addEventListener('blur', saveText);
element.addEventListener('keydown', handleTextEditKeydown);
}
// Render connectors between nodes
function renderConnectors() {
state.connectors.forEach(connector => {
const line = document.createElement('div');
line.className = 'connector';
// Calculate line position and dimensions
const x1 = connector.fromX;
const y1 = connector.fromY;
const x2 = connector.toX;
const y2 = connector.toY;
const length = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
const angle = Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI;
line.style.width = `${length}px`;
line.style.height = '2px';
line.style.left = `${x1}px`;
line.style.top = `${y1}px`;
line.style.transform = `translate(-50%, -50%) rotate(${angle}deg)`;
line.style.transformOrigin = '0 0';
line.style.backgroundColor = connector.color.replace('bg-', '').replace('-400', '');
mindmapContainer.appendChild(line);
});
}
// Select a node
function selectNode(node) {
state.selectedNode = node;
renderMindMap();
updateStatus(`Selected: ${node.text}`);
// If tutorial is active, advance to next step
if (state.tutorialActive) {
advanceTutorial();
}
}
// Add a child node to the selected node
function addChildNode() {
if (state.selectedNode) {
const newNode = createNode('New Child', state.selectedNode.id);
newNode.color = state.selectedNode.color;
saveState();
renderMindMap();
updateStatus(`Added child node to "${state.selectedNode.text}"`);
// If tutorial is active, advance to next step
if (state.tutorialActive) {
advanceTutorial();
}
} else {
updateStatus('Please select a node first', 'error');
}
}
// Add a sibling node to the selected node
function addSiblingNode() {
if (state.selectedNode && state.selectedNode.parentId !== null) {
const newNode = createNode('New Sibling', state.selectedNode.parentId);
newNode.color = state.selectedNode.color;
saveState();
renderMindMap();
updateStatus(`Added sibling node to "${state.selectedNode.text}"`);
// If tutorial is active, advance to next step
if (state.tutorialActive) {
advanceTutorial();
}
} else if (state.selectedNode) {
updateStatus('Cannot add sibling to root node', 'error');
} else {
updateStatus('Please select a node first', 'error');
}
}
// Edit selected node text
function editSelectedNodeText() {
if (state.selectedNode) {
const nodeElement = document.querySelector(`.node[data-node-id="${state.selectedNode.id}"]`);
if (nodeElement) {
const textElement = nodeElement.querySelector('.node-text');
editNodeText(textElement, state.selectedNode);
}
} else {
updateStatus('Please select a node first', 'error');
}
}
// Delete the selected node
function deleteSelectedNode() {
if (state.selectedNode) {
if (state.selectedNode.parentId === null && state.nodes.length > 1) {
updateStatus('Cannot delete the only root node', 'error');
return;
}
const parent = state.nodes.find(n => n.id === state.selectedNode.parentId);
if (parent) {
parent.children = parent.children.filter(id => id !== state.selectedNode.id);
}
// Recursively delete children
const deleteChildren = (nodeId) => {
const node = state.nodes.find(n => n.id === nodeId);
if (node) {
node.children.forEach(deleteChildren);
state.nodes = state.nodes.filter(n => n.id !== nodeId);
}
};
deleteChildren(state.selectedNode.id);
// Also remove any custom links involving this node
state.customLinks = state.customLinks.filter(link =>
link.from !== state.selectedNode.id && link.to !== state.selectedNode.id
);
// Select parent node if available, otherwise select first root node
if (parent) {
selectNode(parent);
} else {
const rootNodes = state.nodes.filter(n => n.parentId === null);
if (rootNodes.length > 0) {
selectNode(rootNodes[0]);
}
}
saveState();
renderMindMap();
updateNodeCount();
updateStatus(`Deleted node "${state.selectedNode.text}"`);
// If tutorial is active, advance to next step
if (state.tutorialActive) {
advanceTutorial();
}
} else {
updateStatus('Please select a node first', 'error');
}
}
// Toggle children visibility
function toggleChildren(node) {
node.collapsed = !node.collapsed;
saveState();
renderMindMap();
updateStatus(`${node.collapsed ? 'Collapsed' : 'Expanded'} children of "${node.text}"`);
}
// Toggle link creation mode
function toggleLinkMode() {
state.linkMode = !state.linkMode;
state.linkSourceNode = null;
if (state.linkMode) {
linkModeStatus.classList.remove('hidden');
linkModeStatus.textContent = 'Link Mode: Select source node (ESC to cancel)';
updateStatus('Link mode: Select first node to connect');
} else {
linkModeStatus.classList.add('hidden');
tempLink.style.display = 'none';
updateStatus('Link mode canceled');
}
renderMindMap();
}
// Zoom functions
function zoomIn() {
state.zoomLevel = Math.min(state.zoomLevel + 0.1, 2);
applyZoom();
}
function zoomOut() {
state.zoomLevel = Math.max(state.zoomLevel - 0.1, 0.5);
applyZoom();
}
function resetZoom() {
state.zoomLevel = 1;
applyZoom();
}
function applyZoom() {
mindmapContainer.style.transform = `scale(${state.zoomLevel}) translate(${state.translateX}px, ${state.translateY}px)`;
zoomLevelSpan.textContent = `Zoom: ${Math.round(state.zoomLevel * 100)}%`;
}
// Toggle layout between radial and horizontal
function toggleLayout() {
state.layout = state.layout === 'radial' ? 'horizontal' : 'radial';
saveState();
renderMindMap();
updateStatus(`Switched to ${state.layout} layout`);
}
// Animate the mind map
function animateMindMap() {
const nodes = mindmapContainer.querySelectorAll('.node');
nodes.forEach(node => {
node.classList.add('pulse-animation');
});
setTimeout(() => {
nodes.forEach(node => {
node.classList.remove('pulse-animation');
});
}, 3000);
updateStatus('Playing animation');
}
// Color palette functions
function toggleColorPalette(e) {
e.stopPropagation();
if (state.selectedNode) {
const rect = colorBtn.getBoundingClientRect();
colorPalette.style.display = 'grid';
colorPalette.style.top = `${rect.bottom + 5}px`;
colorPalette.style.left = `${rect.left}px`;
// Mark current color as selected
const colorOptions = colorPalette.querySelectorAll('.color-option');
colorOptions.forEach(option => {
option.classList.toggle('selected', option.dataset.color === state.selectedNode.color);
});
} else {
updateStatus('Please select a node first', 'error');
}
}
function closeColorPalette(e) {
if (!colorPalette.contains(e.target) && e.target !== colorBtn) {
colorPalette.style.display = 'none';
} else if (e.target.classList.contains('color-option')) {
if (state.selectedNode) {
state.selectedNode.color = e.target.dataset.color;
saveState();
renderMindMap();
updateStatus(`Changed node color`);
}
colorPalette.style.display = 'none';
}
}
// Export functions
function showExportModal() {
exportModal.classList.remove('hidden');
}
function hideExportModal() {
exportModal.classList.add('hidden');
}
function exportMindMap() {
const format = document.getElementById('export-format').value;
if (format === 'png') {
// In a real app, you would use html2canvas or similar library
updateStatus('Exporting as PNG... (simulated)');
setTimeout(() => {
updateStatus('Exported as PNG');
hideExportModal();
}, 1500);
} else if (format === 'json') {
const data = {
nodes: state.nodes,
layout: state.layout,
customLinks: state.customLinks
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'mindmap.json';
a.click();
URL.revokeObjectURL(url);
updateStatus('Exported as JSON');
hideExportModal();
} else if (format === 'svg') {
updateStatus('Exporting as SVG... (simulated)');
setTimeout(() => {
updateStatus('Exported as SVG');
hideExportModal();
}, 1500);
} else if (format === 'pdf') {
updateStatus('Exporting as PDF... (simulated)');
setTimeout(() => {
updateStatus('Exported as PDF');
hideExportModal();
}, 1500);
}
}
// Help modal functions
function showHelpModal() {
helpModal.classList.remove('hidden');
}
function hideHelpModal() {
helpModal.classList.add('hidden');
}
// Tutorial functions
function startTutorial() {
state.tutorialActive = true;
state.tutorialStep = 0;
tutorialHighlights.classList.remove('hidden');
hideHelpModal();
advanceTutorial();
}
function advanceTutorial() {
const steps = [
{
message: "Welcome to MindMapper! Let's start by selecting the root node.",
highlight: null,
action: () => {
const rootNode = state.nodes.find(n => n.parentId === null);
if (rootNode) selectNode(rootNode);
}
},
{
message: "Great! Now try adding a child node by clicking the 'Add Child' button or pressing Enter.",
highlight: 'tutorial-add-child',
action: () => {
const highlight = document.getElementById('tutorial-add-child');
const btnRect = addChildBtn.getBoundingClientRect();
highlight.style.width = `${btnRect.width}px`;
highlight.style.height = `${btnRect.height}px`;
highlight.style.top = `${btnRect.top}px`;
highlight.style.left = `${btnRect.left}px`;
}
},
{
message: "Excellent! Now select the new node and try adding a sibling with the 'Add Sibling' button or Shift+Enter.",
highlight: 'tutorial-add-sibling',
action: () => {
const highlight = document.getElementById('tutorial-add-sibling');
const btnRect = addSiblingBtn.getBoundingClientRect();
highlight.style.width = `${btnRect.width}px`;
highlight.style.height = `${btnRect.height}px`;
highlight.style.top = `${btnRect.top}px`;
highlight.style.left = `${btnRect.left}px`;
}
},
{
message: "Well done! Now double-click a node or use the 'Edit Text' button to change its text.",
highlight: 'tutorial-edit-text',
action: () => {
const highlight = document.getElementById('tutorial-edit-text');
const btnRect = editTextBtn.getBoundingClientRect();
highlight.style.width = `${btnRect.width}px`;
highlight.style.height = `${btnRect.height}px`;
highlight.style.top = `${btnRect.top}px`;
highlight.style.left = `${btnRect.left}px`;
}
},
{
message: "Almost done! Try zooming in/out with the buttons or Ctrl + +/-.",
highlight: 'tutorial-zoom',
action: () => {
const highlight = document.getElementById('tutorial-zoom');
const btnRect = zoomInBtn.getBoundingClientRect();
highlight.style.width = `${btnRect.width * 3 + 24}px`; // Cover all zoom buttons
highlight.style.height = `${btnRect.height}px`;
highlight.style.top = `${btnRect.top}px`;
highlight.style.left = `${btnRect.left}px`;
}
},
{
message: "Congratulations! You've completed the tutorial. Explore other features on your own!",
highlight: null,
action: () => {
state.tutorialActive = false;
tutorialHighlights.classList.add('hidden');
}
}
];
if (state.tutorialStep < steps.length) {
const step = steps[state.tutorialStep];
updateStatus(step.message);
if (step.highlight) {
document.querySelectorAll('[id^="tutorial-"]').forEach(el => {
el.style.display = 'none';
});
document.getElementById(step.highlight).style.display = 'block';
step.action();
} else {
document.querySelectorAll('[id^="tutorial-"]').forEach(el => {
el.style.display = 'none';
});
step.action();
}
state.tutorialStep++;
}
}
// Undo/redo functionality
function saveState() {
// Remove any states after current index (if we're not at the end)
state.history = state.history.slice(0, state.historyIndex + 1);
// Save current state
const stateCopy = {
nodes: JSON.parse(JSON.stringify(state.nodes)),
selectedNodeId: state.selectedNode ? state.selectedNode.id : null,
customLinks: JSON.parse(JSON.stringify(state.customLinks)),
layout: state.layout,
zoomLevel: state.zoomLevel,
translateX: state.translateX,
translateY: state.translateY
};
state.history.push(stateCopy);
state.historyIndex = state.history.length - 1;
// Update undo/redo button states
updateUndoRedoButtons();
}
function undo() {
if (state.historyIndex > 0) {
state.historyIndex--;
restoreState();
}
}
function redo() {
if (state.historyIndex < state.history.length - 1) {
state.historyIndex++;
restoreState();
}
}
function restoreState() {
const savedState = state.history[state.historyIndex];
state.nodes = JSON.parse(JSON.stringify(savedState.nodes));
state.customLinks = JSON.parse(JSON.stringify(savedState.customLinks));
state.layout = savedState.layout;
state.zoomLevel = savedState.zoomLevel;
state.translateX = savedState.translateX;
state.translateY = savedState.translateY;
if (savedState.selectedNodeId) {
const node = state.nodes.find(n => n.id === savedState.selectedNodeId);
if (node) {
state.selectedNode = node;
}
}
// Exit any special modes
state.linkMode = false;
state.linkSourceNode = null;
linkModeStatus.classList.add('hidden');
tempLink.style.display = 'none';
renderMindMap();
updateNodeCount();
updateStatus('State restored');
updateUndoRedoButtons();
}
function updateUndoRedoButtons() {
undoBtn.disabled = state.historyIndex <= 0;
redoBtn.disabled = state.historyIndex >= state.history.length - 1;
}
// Panning functionality
function startDrag(e) {
if (e.target === mindmapContainer) {
state.isDragging = true;
state.dragStartX = e.clientX - state.translateX;
state.dragStartY = e.clientY - state.translateY;
mindmapContainer.style.cursor = 'grabbing';
}
}
function drag(e) {
if (state.isDragging) {
state.translateX = e.clientX - state.dragStartX;
state.translateY = e.clientY - state.dragStartY;
applyZoom();
}
}
function endDrag() {
state.isDragging = false;
mindmapContainer.style.cursor = '';
}
// Center view on selected node
function centerView() {
if (state.selectedNode) {
const containerWidth = mindmapContainer.clientWidth;
const containerHeight = mindmapContainer.clientHeight;
state.translateX = containerWidth / 2 - state.selectedNode.x;
state.translateY = containerHeight / 2 - state.selectedNode.y;
applyZoom();
updateStatus(`Centered view on "${state.selectedNode.text}"`);
}
}
// Keyboard shortcuts
function handleKeyboardShortcuts(e) {
// Don't handle shortcuts when editing text
if (document.activeElement.hasAttribute('contenteditable')) {
return;
}
// Check for Ctrl key combinations
if (e.ctrlKey) {
switch (e.key.toLowerCase()) {
case 'z':
e.preventDefault();
undo();
break;
case 'y':
e.preventDefault();
redo();
break;
case '+':
case '=':
e.preventDefault();
zoomIn();
break;
case '-':
e.preventDefault();
zoomOut();
break;
case '0':
e.preventDefault();
resetZoom();
break;
}
return;
}
// Single key shortcuts
switch (e.key) {
case 'Enter':
e.preventDefault();
if (e.shiftKey) {
addSiblingNode();
} else {
addChildNode();
}
break;
case 'Delete':
e.preventDefault();
deleteSelectedNode();
break;
case 'l':
e.preventDefault();
if (state.linkMode) {
toggleLinkMode();
} else {
toggleLayout();
}
break;
case 'a':
e.preventDefault();
animateMindMap();
break;
case 'h':
e.preventDefault();
showHelpModal();
break;
case 'e':
e.preventDefault();
showExportModal();
break;
case 'c':
e.preventDefault();
centerView();
break;
case 'Tab':
e.preventDefault();
if (state.selectedNode) {
if (e.shiftKey) {
// Focus on parent
if (state.selectedNode.parentId) {
const parent = state.nodes.find(n => n.id === state.selectedNode.parentId);
if (parent) selectNode(parent);
}
} else {
// Focus on first child
if (state.selectedNode.children.length > 0) {
const firstChild = state.nodes.find(n => n.id === state.selectedNode.children[0]);
if (firstChild) selectNode(firstChild);
}
}
}
break;
case 'Escape':
if (state.linkMode) {
e.preventDefault();
toggleLinkMode();
}
break;
}
}
// UI update functions
function updateNodeCount() {
nodeCountSpan.textContent = `Nodes: ${state.nodes.length}`;
}
function updateStatus(message, type = 'info') {
statusMessageSpan.textContent = message;
statusMessageSpan.className = '';
statusMessageSpan.classList.add(type === 'error' ? 'text-red-500' : 'text-gray-600');
}
// Wireframe functions
function showWireframe() {
wireframe.style.display = 'block';
updateStatus('Showing wireframe');
}
function hideWireframe() {
wireframe.style.display = 'none';
updateStatus('Ready');
}
// Event listeners
showWireframeBtn.addEventListener('click', showWireframe);
closeWireframeBtn.addEventListener('click', hideWireframe);
addChildBtn.addEventListener('click', addChildNode);
addSiblingBtn.addEventListener('click', addSiblingNode);
deleteNodeBtn.addEventListener('click', deleteSelectedNode);
editTextBtn.addEventListener('click', editSelectedNodeText);
colorBtn.addEventListener('click', toggleColorPalette);
zoomInBtn.addEventListener('click', zoomIn);
zoomOutBtn.addEventListener('click', zoomOut);
resetZoomBtn.addEventListener('click', resetZoom);
toggleLayoutBtn.addEventListener('click', toggleLayout);
createLinkBtn.addEventListener('click', toggleLinkMode);
animateBtn.addEventListener('click', animateMindMap);
exportBtn.addEventListener('click', showExportModal);
closeExportModal.addEventListener('click', hideExportModal);
cancelExport.addEventListener('click', hideExportModal);
confirmExport.addEventListener('click', exportMindMap);
document.addEventListener('click', closeColorPalette);
tutorialBtn.addEventListener('click', showHelpModal);
helpModal.addEventListener('click', (e) => e.stopPropagation());
closeHelpModal.addEventListener('click', hideHelpModal);
startTutorialBtn.addEventListener('click', startTutorial);
undoBtn.addEventListener('click', undo);
redoBtn.addEventListener('click', redo);
centerViewBtn.addEventListener('click', centerView);
// Export format change
exportFormatSelect.addEventListener('change', function() {
document.getElementById('export-png-options').classList.toggle('hidden', this.value !== 'png');
document.getElementById('export-pdf-options').classList.toggle('hidden', this.value !== 'pdf');
});
// Panning functionality
mindmapContainer.addEventListener('mousedown', startDrag);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', endDrag);
// Keyboard shortcuts
document.addEventListener('keydown', handleKeyboardShortcuts);
});
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=tukangkustom/bomindflow" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>