✨ Enhanced mindmap visualization with professional UI
🎯 Major Features Added: - Snap to grid functionality (20x20 grid, default enabled) - Tidy Up button for instant node reorganization - Copy node values with one-click clipboard integration - HTML rendering toggle (render/raw modes for HTML content) - Accordion-style collapsible panels (Controls & Legend) - Automatic fitView on fullscreen toggle with smooth animations 🎨 UI/UX Improvements: - Professional accordion layout with exclusive panel opening - Consistent button alignment and styling across all controls - Legend moved to top-right with icon+color indicators - Vertical button stack: Controls → Legend → Tidy Up → Fullscreen - Smooth transitions and hover effects throughout - Clean, uncluttered interface with folded panels by default 🔧 Technical Enhancements: - Fixed ResizeObserver errors with proper error handling - Optimized React rendering with memo and useCallback - Debounced DOM updates to prevent infinite loops - React Flow instance management for programmatic control - Removed redundant Raw Input button for cleaner interface 🚀 Performance & Stability: - Error boundary implementation for ResizeObserver issues - Proper cleanup of event listeners and timeouts - Memoized components to prevent unnecessary re-renders - Smooth 300ms animations for all state transitions
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Search, Code, Link2, FileText, Hash, RefreshCw, GitCompare, Database, Type } from 'lucide-react';
|
||||
import { Search, Code, Link2, FileText, Hash, RefreshCw, GitCompare, Database, Type, Edit3 } from 'lucide-react';
|
||||
import ToolCard from '../components/ToolCard';
|
||||
|
||||
const Home = () => {
|
||||
@@ -7,18 +7,11 @@ const Home = () => {
|
||||
|
||||
const tools = [
|
||||
{
|
||||
icon: Code,
|
||||
title: 'JSON Encoder/Decoder',
|
||||
description: 'Format, validate, and minify JSON data with syntax highlighting',
|
||||
path: '/json',
|
||||
tags: ['JSON', 'Format', 'Validate']
|
||||
},
|
||||
{
|
||||
icon: Database,
|
||||
title: 'Serialize Encoder/Decoder',
|
||||
description: 'Encode and decode serialized data (PHP serialize format)',
|
||||
path: '/serialize',
|
||||
tags: ['PHP', 'Serialize', 'Unserialize']
|
||||
icon: Edit3,
|
||||
title: 'Object Editor',
|
||||
description: 'Visual editor for JSON and PHP serialized objects with format conversion',
|
||||
path: '/object-editor',
|
||||
tags: ['Visual', 'JSON', 'PHP', 'Objects', 'Editor']
|
||||
},
|
||||
{
|
||||
icon: Link2,
|
||||
|
||||
506
src/pages/ObjectEditor.js
Normal file
506
src/pages/ObjectEditor.js
Normal file
@@ -0,0 +1,506 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { Edit3, Upload, FileText, Download, Copy, Map } from 'lucide-react';
|
||||
import ToolLayout from '../components/ToolLayout';
|
||||
import CopyButton from '../components/CopyButton';
|
||||
import StructuredEditor from '../components/StructuredEditor';
|
||||
import MindmapView from '../components/MindmapView';
|
||||
|
||||
const ObjectEditor = () => {
|
||||
const [structuredData, setStructuredData] = useState({});
|
||||
const [showInput, setShowInput] = useState(false);
|
||||
const [inputText, setInputText] = useState('');
|
||||
const [inputFormat, setInputFormat] = useState('');
|
||||
const [inputValid, setInputValid] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [viewMode, setViewMode] = useState('visual'); // 'visual', 'mindmap'
|
||||
const [outputs, setOutputs] = useState({
|
||||
jsonPretty: '',
|
||||
jsonMinified: '',
|
||||
serialized: ''
|
||||
});
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
// PHP serialize implementation (reused from SerializeTool)
|
||||
const phpSerialize = (data) => {
|
||||
if (data === null) return 'N;';
|
||||
if (typeof data === 'boolean') return data ? 'b:1;' : 'b:0;';
|
||||
if (typeof data === 'number') {
|
||||
return Number.isInteger(data) ? `i:${data};` : `d:${data};`;
|
||||
}
|
||||
if (typeof data === 'string') {
|
||||
const escapedData = data.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||
const byteLength = new TextEncoder().encode(escapedData).length;
|
||||
return `s:${byteLength}:"${escapedData}";`;
|
||||
}
|
||||
if (Array.isArray(data)) {
|
||||
let result = `a:${data.length}:{`;
|
||||
data.forEach((item, index) => {
|
||||
result += phpSerialize(index) + phpSerialize(item);
|
||||
});
|
||||
result += '}';
|
||||
return result;
|
||||
}
|
||||
if (typeof data === 'object') {
|
||||
const keys = Object.keys(data);
|
||||
let result = `a:${keys.length}:{`;
|
||||
keys.forEach(key => {
|
||||
result += phpSerialize(key) + phpSerialize(data[key]);
|
||||
});
|
||||
result += '}';
|
||||
return result;
|
||||
}
|
||||
return 'N;';
|
||||
};
|
||||
|
||||
// PHP unserialize implementation (reused from SerializeTool)
|
||||
const phpUnserialize = (str) => {
|
||||
let index = 0;
|
||||
|
||||
const parseValue = () => {
|
||||
if (index >= str.length) {
|
||||
throw new Error('Unexpected end of string');
|
||||
}
|
||||
|
||||
const type = str[index];
|
||||
|
||||
if (type === 'N') {
|
||||
index += 2;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (str[index + 1] !== ':') {
|
||||
throw new Error(`Expected ':' after type '${type}' at position ${index + 1}`);
|
||||
}
|
||||
|
||||
index += 2;
|
||||
|
||||
switch (type) {
|
||||
case 'b':
|
||||
const boolVal = str[index] === '1';
|
||||
index += 2;
|
||||
return boolVal;
|
||||
|
||||
case 'i':
|
||||
let intStr = '';
|
||||
while (index < str.length && str[index] !== ';') {
|
||||
intStr += str[index++];
|
||||
}
|
||||
if (index >= str.length) {
|
||||
throw new Error('Unexpected end of string while parsing integer');
|
||||
}
|
||||
index++;
|
||||
return parseInt(intStr);
|
||||
|
||||
case 'd':
|
||||
let floatStr = '';
|
||||
while (index < str.length && str[index] !== ';') {
|
||||
floatStr += str[index++];
|
||||
}
|
||||
if (index >= str.length) {
|
||||
throw new Error('Unexpected end of string while parsing float');
|
||||
}
|
||||
index++;
|
||||
return parseFloat(floatStr);
|
||||
|
||||
case 's':
|
||||
let lenStr = '';
|
||||
while (index < str.length && str[index] !== ':') {
|
||||
lenStr += str[index++];
|
||||
}
|
||||
if (index >= str.length) {
|
||||
throw new Error('Unexpected end of string while parsing string length');
|
||||
}
|
||||
index++;
|
||||
|
||||
if (str[index] !== '"') {
|
||||
throw new Error(`Expected '"' at position ${index}`);
|
||||
}
|
||||
index++;
|
||||
|
||||
const byteLength = parseInt(lenStr);
|
||||
|
||||
if (isNaN(byteLength) || byteLength < 0) {
|
||||
throw new Error(`Invalid string length: ${lenStr}`);
|
||||
}
|
||||
|
||||
if (byteLength === 0) {
|
||||
if (index + 1 >= str.length || str[index] !== '"' || str[index + 1] !== ';') {
|
||||
throw new Error(`Expected '";' after empty string at position ${index}`);
|
||||
}
|
||||
index += 2;
|
||||
return '';
|
||||
}
|
||||
|
||||
const startIndex = index;
|
||||
let endQuotePos = -1;
|
||||
|
||||
for (let i = startIndex; i < str.length - 1; i++) {
|
||||
if (str[i] === '"' && str[i + 1] === ';') {
|
||||
endQuotePos = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (endQuotePos === -1) {
|
||||
throw new Error(`Could not find closing '";' for string starting at position ${startIndex}`);
|
||||
}
|
||||
|
||||
const stringVal = str.substring(startIndex, endQuotePos);
|
||||
index = endQuotePos + 2;
|
||||
|
||||
return stringVal;
|
||||
|
||||
case 'a':
|
||||
let arrayLenStr = '';
|
||||
while (index < str.length && str[index] !== ':') {
|
||||
arrayLenStr += str[index++];
|
||||
}
|
||||
if (index >= str.length) {
|
||||
throw new Error('Unexpected end of string while parsing array length');
|
||||
}
|
||||
index++;
|
||||
|
||||
if (str[index] !== '{') {
|
||||
throw new Error(`Expected '{' at position ${index}`);
|
||||
}
|
||||
index++;
|
||||
|
||||
const arrayLength = parseInt(arrayLenStr);
|
||||
if (isNaN(arrayLength) || arrayLength < 0) {
|
||||
throw new Error(`Invalid array length: ${arrayLenStr}`);
|
||||
}
|
||||
|
||||
const result = {};
|
||||
let isArray = true;
|
||||
|
||||
for (let i = 0; i < arrayLength; i++) {
|
||||
const key = parseValue();
|
||||
const value = parseValue();
|
||||
result[key] = value;
|
||||
|
||||
if (typeof key !== 'number' || key !== i) {
|
||||
isArray = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (index >= str.length || str[index] !== '}') {
|
||||
throw new Error(`Expected '}' at position ${index}`);
|
||||
}
|
||||
index++;
|
||||
|
||||
if (isArray && arrayLength > 0) {
|
||||
const arr = [];
|
||||
for (let i = 0; i < arrayLength; i++) {
|
||||
arr[i] = result[i];
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown type: '${type}' at position ${index - 2}`);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const result = parseValue();
|
||||
if (index < str.length) {
|
||||
console.warn(`Warning: Trailing data after parsing: "${str.substring(index)}"`);
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new Error(`Parse error at position ${index}: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-detect input format
|
||||
const detectInputFormat = (input) => {
|
||||
if (!input.trim()) return { format: '', valid: false, data: null };
|
||||
|
||||
// Try JSON first
|
||||
try {
|
||||
const jsonData = JSON.parse(input);
|
||||
return { format: 'JSON', valid: true, data: jsonData };
|
||||
} catch {}
|
||||
|
||||
// Try PHP serialize
|
||||
try {
|
||||
const serializedData = phpUnserialize(input);
|
||||
return { format: 'PHP Serialized', valid: true, data: serializedData };
|
||||
} catch {}
|
||||
|
||||
return { format: 'Unknown', valid: false, data: null };
|
||||
};
|
||||
|
||||
// Handle input text change
|
||||
const handleInputChange = (value) => {
|
||||
setInputText(value);
|
||||
const detection = detectInputFormat(value);
|
||||
setInputFormat(detection.format);
|
||||
setInputValid(detection.valid);
|
||||
|
||||
if (detection.valid) {
|
||||
console.log('🎯 SETTING STRUCTURED DATA:', detection.data);
|
||||
setStructuredData(detection.data);
|
||||
setError('');
|
||||
} else if (value.trim()) {
|
||||
setError('Invalid format. Please enter valid JSON or PHP serialized data.');
|
||||
} else {
|
||||
setError('');
|
||||
}
|
||||
};
|
||||
|
||||
// Handle structured data change from visual editor
|
||||
const handleStructuredDataChange = (newData) => {
|
||||
setStructuredData(newData);
|
||||
generateOutputs(newData);
|
||||
};
|
||||
|
||||
// Generate all output formats
|
||||
const generateOutputs = (data) => {
|
||||
try {
|
||||
const jsonPretty = JSON.stringify(data, null, 2);
|
||||
const jsonMinified = JSON.stringify(data);
|
||||
const serialized = phpSerialize(data);
|
||||
|
||||
setOutputs({
|
||||
jsonPretty,
|
||||
jsonMinified,
|
||||
serialized
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error generating outputs:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle file import
|
||||
const handleFileImport = (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const content = e.target.result;
|
||||
setInputText(content);
|
||||
handleInputChange(content);
|
||||
setShowInput(true);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
};
|
||||
|
||||
// Load sample data
|
||||
const loadSample = () => {
|
||||
const sample = {
|
||||
"user": {
|
||||
"name": "John Doe",
|
||||
"age": 30,
|
||||
"email": "john@example.com",
|
||||
"preferences": {
|
||||
"theme": "dark",
|
||||
"notifications": true
|
||||
},
|
||||
"tags": ["developer", "javascript", "react"]
|
||||
},
|
||||
"settings": {
|
||||
"language": "en",
|
||||
"timezone": "UTC"
|
||||
}
|
||||
};
|
||||
setStructuredData(sample);
|
||||
generateOutputs(sample);
|
||||
};
|
||||
|
||||
// Initialize outputs when component mounts or data changes
|
||||
React.useEffect(() => {
|
||||
generateOutputs(structuredData);
|
||||
}, [structuredData]);
|
||||
|
||||
return (
|
||||
<ToolLayout
|
||||
title="Object Editor"
|
||||
description="Visual editor for JSON and PHP serialized objects with format conversion"
|
||||
icon={Edit3}
|
||||
>
|
||||
{/* Input Controls */}
|
||||
<div className="flex flex-wrap gap-3 mb-6">
|
||||
<button
|
||||
onClick={() => setShowInput(!showInput)}
|
||||
className="flex items-center space-x-2 tool-button"
|
||||
>
|
||||
<FileText className="h-4 w-4" />
|
||||
<span>{showInput ? 'Hide Input' : 'Input Data'}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="flex items-center space-x-2 tool-button-secondary"
|
||||
>
|
||||
<Upload className="h-4 w-4" />
|
||||
<span>Import File</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={loadSample}
|
||||
className="flex items-center space-x-2 tool-button-secondary"
|
||||
>
|
||||
<Edit3 className="h-4 w-4" />
|
||||
<span>Load Sample</span>
|
||||
</button>
|
||||
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".json,.txt"
|
||||
onChange={handleFileImport}
|
||||
className="hidden"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* View Mode Toggle */}
|
||||
<div className="flex bg-gray-100 dark:bg-gray-800 rounded-lg p-1 mb-6 w-fit">
|
||||
<button
|
||||
onClick={() => setViewMode('visual')}
|
||||
className={`flex items-center space-x-2 px-4 py-2 rounded-md font-medium transition-colors ${
|
||||
viewMode === 'visual'
|
||||
? 'bg-white dark:bg-gray-700 text-primary-600 shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
<Edit3 className="h-4 w-4" />
|
||||
<span>Visual Editor</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('mindmap')}
|
||||
className={`flex items-center space-x-2 px-4 py-2 rounded-md font-medium transition-colors ${
|
||||
viewMode === 'mindmap'
|
||||
? 'bg-white dark:bg-gray-700 text-primary-600 shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
<Map className="h-4 w-4" />
|
||||
<span>Mindmap View</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Input Section */}
|
||||
{showInput && (
|
||||
<div className="mb-6 space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Input Data
|
||||
</label>
|
||||
{inputFormat && (
|
||||
<span className={`text-xs px-2 py-1 rounded ${
|
||||
inputValid
|
||||
? 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300'
|
||||
: 'bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-300'
|
||||
}`}>
|
||||
{inputFormat} {inputValid ? '✓' : '✗'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<textarea
|
||||
value={inputText}
|
||||
onChange={(e) => handleInputChange(e.target.value)}
|
||||
placeholder="Paste JSON or PHP serialized data here..."
|
||||
className="tool-input h-32"
|
||||
/>
|
||||
{error && (
|
||||
<p className="text-sm text-red-600 dark:text-red-400">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Editor Area */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
||||
{viewMode === 'visual' && 'Visual Editor'}
|
||||
{viewMode === 'mindmap' && 'Mindmap Visualization'}
|
||||
</h3>
|
||||
|
||||
{viewMode === 'visual' && (
|
||||
<div className="min-h-96 border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||
<StructuredEditor
|
||||
initialData={structuredData}
|
||||
onDataChange={handleStructuredDataChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{viewMode === 'mindmap' && (
|
||||
<MindmapView data={structuredData} />
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
{/* Output Actions */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{/* JSON Pretty */}
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
JSON (Pretty)
|
||||
</label>
|
||||
<div className="relative">
|
||||
<textarea
|
||||
value={outputs.jsonPretty}
|
||||
readOnly
|
||||
placeholder="JSON pretty format will appear here..."
|
||||
className="tool-input h-48 bg-gray-50 dark:bg-gray-800 text-sm font-mono"
|
||||
/>
|
||||
{outputs.jsonPretty && <CopyButton text={outputs.jsonPretty} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* JSON Minified */}
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
JSON (Minified)
|
||||
</label>
|
||||
<div className="relative">
|
||||
<textarea
|
||||
value={outputs.jsonMinified}
|
||||
readOnly
|
||||
placeholder="JSON minified format will appear here..."
|
||||
className="tool-input h-48 bg-gray-50 dark:bg-gray-800 text-sm font-mono"
|
||||
/>
|
||||
{outputs.jsonMinified && <CopyButton text={outputs.jsonMinified} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* PHP Serialized */}
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
PHP Serialized
|
||||
</label>
|
||||
<div className="relative">
|
||||
<textarea
|
||||
value={outputs.serialized}
|
||||
readOnly
|
||||
placeholder="PHP serialized format will appear here..."
|
||||
className="tool-input h-48 bg-gray-50 dark:bg-gray-800 text-sm font-mono"
|
||||
/>
|
||||
{outputs.serialized && <CopyButton text={outputs.serialized} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Usage Tips */}
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md p-4 mt-6">
|
||||
<h4 className="text-blue-800 dark:text-blue-200 font-medium mb-2">Usage Tips</h4>
|
||||
<ul className="text-blue-700 dark:text-blue-300 text-sm space-y-1">
|
||||
<li>• <strong>Visual Editor:</strong> Create and modify object structures with forms</li>
|
||||
<li>• <strong>Mindmap View:</strong> Visualize complex JSON structures as interactive diagrams</li>
|
||||
<li>• <strong>Input Data:</strong> Paste JSON/PHP serialized data with auto-detection in the input field</li>
|
||||
<li>• Import data from files or use the sample data to get started</li>
|
||||
<li>• Export your data in any format: JSON pretty, minified, or PHP serialized</li>
|
||||
<li>• Use copy buttons to quickly copy any output format</li>
|
||||
</ul>
|
||||
</div>
|
||||
</ToolLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ObjectEditor;
|
||||
Reference in New Issue
Block a user