mindflow-mapper / index.html
masoudmm's picture
looking good but the functionality is not working correct. Sticks to mouse on dragging node. Nodes not connected propperly. I want the first node to be on the center left with some padding
671ea26 verified
Raw History Blame Contribute Delete
15 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MindFlow Mapper - Visual Thinking Tool</title>
<link rel="icon" type="image/x-icon" href="https://static.photos/abstract/200x200/42">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/feather-icons"></script>
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.11.4/gsap.min.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
body {
font-family: 'Inter', sans-serif;
overflow: hidden;
touch-action: none;
}
#canvas {
background-color: #f8fafc;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.node {
position: absolute;
min-width: 120px;
background: white;
border-radius: 8px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
padding: 12px;
cursor: move;
transition: all 0.3s ease;
border: 2px solid transparent;
}
.node:hover {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
transform: translateY(-2px);
}
.node.selected {
border-color: #3b82f6;
}
.node-input {
background: transparent;
border: none;
outline: none;
font-family: inherit;
font-size: inherit;
font-weight: 500;
width: 100%;
resize: none;
}
.connector {
position: absolute;
width: 24px;
height: 24px;
border-radius: 50%;
background: #3b82f6;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 12px;
cursor: pointer;
z-index: 10;
}
.controls {
position: fixed;
bottom: 2rem;
right: 2rem;
display: flex;
gap: 0.5rem;
z-index: 100;
}
.control-btn {
background: white;
border-radius: 50%;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
cursor: pointer;
transition: all 0.2s ease;
}
.control-btn:hover {
transform: scale(1.1);
background: #3b82f6;
color: white;
}
</style>
</head>
<body class="bg-gray-50">
<div class="fixed top-0 left-0 w-full h-16 bg-white shadow-sm flex items-center px-6 z-50">
<h1 class="text-2xl font-bold text-blue-600 flex items-center gap-2">
<i data-feather="cpu"></i>
MindFlow Mapper
</h1>
<div class="ml-auto flex gap-4">
<button id="save-btn" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition flex items-center gap-2">
<i data-feather="save"></i>
Save
</button>
<button id="load-btn" class="px-4 py-2 bg-gray-200 rounded-lg hover:bg-gray-300 transition flex items-center gap-2">
<i data-feather="folder"></i>
Load
</button>
<button id="clear-btn" class="px-4 py-2 bg-red-100 text-red-600 rounded-lg hover:bg-red-200 transition flex items-center gap-2">
<i data-feather="trash-2"></i>
Clear
</button>
</div>
</div>
<canvas id="canvas"></canvas>
<div class="controls">
<button id="zoom-in" class="control-btn">
<i data-feather="zoom-in"></i>
</button>
<button id="zoom-out" class="control-btn">
<i data-feather="zoom-out"></i>
</button>
<button id="center-btn" class="control-btn">
<i data-feather="target"></i>
</button>
</div>
<script>
feather.replace();
// Canvas setup
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let scale = 1;
let offsetX = 0;
let offsetY = 0;
let isDragging = false;
let dragStartX, dragStartY;
let nodes = [];
let selectedNode = null;
let nextId = 1;
let isConnecting = false;
let connectionStartNode = null;
// Initialize canvas size
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
draw();
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
// Create initial node
function createNode(text, x, y, parentId = null) {
const node = {
id: nextId++,
text,
x,
y,
width: 120,
height: 60,
parentId,
children: []
};
nodes.push(node);
if (parentId) {
const parent = nodes.find(n => n.id === parentId);
if (parent) parent.children.push(node.id);
}
createNodeElement(node);
draw();
return node;
}
// Create DOM element for node
function createNodeElement(node) {
const nodeElement = document.createElement('div');
nodeElement.className = 'node';
nodeElement.id = `node-${node.id}`;
nodeElement.style.left = `${node.x}px`;
nodeElement.style.top = `${node.y}px`;
const input = document.createElement('textarea');
input.className = 'node-input';
input.value = node.text;
input.rows = 1;
input.addEventListener('input', (e) => {
node.text = e.target.value;
draw();
});
nodeElement.appendChild(input);
// Add connector
const connector = document.createElement('div');
connector.className = 'connector';
connector.style.bottom = '-12px';
connector.style.left = 'calc(50% - 12px)';
connector.innerHTML = '<i data-feather="plus"></i>';
connector.addEventListener('click', (e) => {
e.stopPropagation();
if (isConnecting) {
// Complete connection
if (connectionStartNode && connectionStartNode.id !== node.id) {
node.parentId = connectionStartNode.id;
connectionStartNode.children.push(node.id);
draw();
}
isConnecting = false;
connectionStartNode = null;
document.querySelectorAll('.node').forEach(n => n.classList.remove('connecting'));
} else {
// Create child node
const newNode = createNode('New Idea', node.x + 100, node.y + 100, node.id);
node.children.push(newNode.id);
draw();
}
});
nodeElement.appendChild(connector);
// Drag events
nodeElement.addEventListener('mousedown', (e) => {
if (e.target.tagName === 'TEXTAREA') return;
selectedNode = node;
document.querySelectorAll('.node').forEach(n => n.classList.remove('selected'));
nodeElement.classList.add('selected');
dragStartX = e.clientX - node.x;
dragStartY = e.clientY - node.y;
e.preventDefault();
});
document.body.appendChild(nodeElement);
feather.replace();
}
// Draw connections
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw connections
ctx.strokeStyle = '#94a3b8';
ctx.lineWidth = 2;
nodes.forEach(node => {
if (node.parentId) {
const parent = nodes.find(n => n.id === node.parentId);
if (parent) {
ctx.beginPath();
ctx.moveTo(
parent.x * scale + parent.width/2 * scale + offsetX,
parent.y * scale + parent.height * scale + offsetY
);
ctx.lineTo(
node.x * scale + node.width/2 * scale + offsetX,
node.y * scale + offsetY
);
ctx.stroke();
}
}
});
// Update node positions in DOM
nodes.forEach(node => {
const element = document.getElementById(`node-${node.id}`);
if (element) {
element.style.transform = `translate(${node.x * scale + offsetX}px, ${node.y * scale + offsetY}px) scale(${scale})`;
element.style.zIndex = node.id;
}
});
}
// Mouse events
canvas.addEventListener('mousedown', (e) => {
if (e.button === 1 || (e.button === 0 && e.ctrlKey)) { // Middle mouse or Ctrl+Left
isDragging = true;
dragStartX = e.clientX - offsetX;
dragStartY = e.clientY - offsetY;
canvas.style.cursor = 'grabbing';
}
});
canvas.addEventListener('mousemove', (e) => {
if (isDragging) {
offsetX = e.clientX - dragStartX;
offsetY = e.clientY - dragStartY;
draw();
}
if (selectedNode) {
selectedNode.x = (e.clientX - offsetX) / scale;
selectedNode.y = (e.clientY - offsetY) / scale;
const element = document.getElementById(`node-${selectedNode.id}`);
if (element) {
element.style.left = `${selectedNode.x}px`;
element.style.top = `${selectedNode.y}px`;
}
draw();
}
});
canvas.addEventListener('mouseup', () => {
isDragging = false;
selectedNode = null;
canvas.style.cursor = 'default';
});
canvas.addEventListener('wheel', (e) => {
e.preventDefault();
const delta = -e.deltaY;
const zoomIntensity = 0.1;
const zoomFactor = 1 + delta * zoomIntensity / 100;
const newScale = Math.max(0.1, Math.min(3, scale * zoomFactor));
// Keep center point stable during zoom
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
offsetX = centerX - (centerX - offsetX) * zoomFactor;
offsetY = centerY - (centerY - offsetY) * zoomFactor;
scale = newScale;
document.querySelectorAll('.node').forEach(node => {
node.style.transform = `translate(${parseInt(node.style.left) * scale + offsetX}px, ${parseInt(node.style.top) * scale + offsetY}px) scale(${scale})`;
});
draw();
});
// Double click to create new node
canvas.addEventListener('dblclick', (e) => {
const x = (e.clientX - offsetX) / scale;
const y = (e.clientY - offsetY) / scale;
if (!document.elementFromPoint(e.clientX, e.clientY).classList.contains('node')) {
createNode('New Idea', x, y);
}
});
// Button events
document.getElementById('zoom-in').addEventListener('click', () => {
scale = Math.min(3, scale * 1.1);
applyTransform();
});
document.getElementById('zoom-out').addEventListener('click', () => {
scale = Math.max(0.1, scale / 1.1);
applyTransform();
});
document.getElementById('center-btn').addEventListener('click', () => {
offsetX = (canvas.width - (canvas.width * scale)) / 2;
offsetY = (canvas.height - (canvas.height * scale)) / 2;
applyTransform();
});
document.getElementById('save-btn').addEventListener('click', () => {
const data = JSON.stringify(nodes);
localStorage.setItem('mindflow-map', data);
alert('Map saved successfully!');
});
document.getElementById('load-btn').addEventListener('click', () => {
const data = localStorage.getItem('mindflow-map');
if (data) {
nodes = JSON.parse(data);
nextId = Math.max(...nodes.map(n => n.id)) + 1;
// Clear existing nodes
document.querySelectorAll('.node').forEach(node => node.remove());
// Recreate nodes
nodes.forEach(node => createNodeElement(node));
draw();
alert('Map loaded successfully!');
} else {
alert('No saved map found!');
}
});
document.getElementById('clear-btn').addEventListener('click', () => {
if (confirm('Are you sure you want to clear the map?')) {
nodes = [];
nextId = 1;
document.querySelectorAll('.node').forEach(node => node.remove());
draw();
}
});
// Apply transform to all nodes
function applyTransform() {
nodes.forEach(node => {
const element = document.getElementById(`node-${node.id}`);
if (element) {
element.style.transform = `translate(${node.x * scale + offsetX}px, ${node.y * scale + offsetY}px) scale(${scale})`;
}
});
draw();
}
// Create initial node positioned center left with padding
createNode('Central Idea', 60, canvas.height/2 - 30);
</script>
</body>
</html>