Spaces:
Running
Running
| <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> |