document.addEventListener('DOMContentLoaded', function() { // Sample JSON data const sampleJSON = { "name": "John Doe", "age": 30, "isStudent": false, "address": { "street": "123 Main St", "city": "Anytown", "zipcode": "12345" }, "hobbies": [ "reading", "swimming", "coding" ], "contact": { "email": "john@example.com", "phone": "555-1234" } }; // DOM elements const jsonEditor = document.getElementById('jsonEditor'); const jsonOutput = document.getElementById('jsonOutput'); const notification = document.getElementById('notification'); const copyBtn = document.getElementById('copyBtn'); const downloadBtn = document.getElementById('downloadBtn'); const applyCodeBtn = document.getElementById('applyCodeBtn'); const validationStatus = document.getElementById('validationStatus'); const lineNumbersLeft = document.getElementById('lineNumbersLeft'); const lineNumbersRight = document.getElementById('lineNumbersRight'); const coordLine = document.getElementById('coordLine'); const coordChar = document.getElementById('coordChar'); const coordDepth = document.getElementById('coordDepth'); // Create saved indicator const savedIndicator = document.createElement('div'); savedIndicator.className = 'saved-indicator'; savedIndicator.innerHTML = 'Saved'; document.body.appendChild(savedIndicator); let isApplyingChanges = false; let leftEditorLastModified = 0; // Current state let jsonData = {}; let history = []; let historyIndex = -1; const MAX_HISTORY = 50; // Track editor states to prevent data loss let leftEditorModified = false; let rightEditorModified = false; let rightEditorLastModified = 0; let autoApplyTimeout = null; // Track all editable fields for TAB navigation let editableFields = []; let currentFieldIndex = -1; // Line number tracking let currentLine = 0; let lineElements = []; // Drag and drop state let draggedElement = null; let draggedData = null; let dropTarget = null; let dropPosition = null; // 'above', 'below', or 'inside' // Layer colors for visual distinction const layerColors = [ '#ef4444', '#f97316', '#eab308', '#22c55e', '#06b6d4', '#3b82f6', '#8b5cf6', '#ec4899' ]; // Initialize with sample data loadJSON(sampleJSON); // Set initial history state saveToHistory(); // Create hidden file input for opening files const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = 'application/json,.json'; fileInput.style.display = 'none'; document.body.appendChild(fileInput); // ==================== NEW JSON FUNCTION ==================== function newJSON() { const newEmpty = { "newField": "value" }; loadJSON(newEmpty); showNotification('New JSON created', false); } document.getElementById('newBtn').addEventListener('click', newJSON); document.getElementById('newBtn2').addEventListener('click', newJSON); // ==================== OPEN FILE FUNCTION ==================== function openFile() { fileInput.click(); } fileInput.addEventListener('change', (event) => { const file = event.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = (e) => { try { const data = JSON.parse(e.target.result); loadJSON(data); showNotification('File loaded successfully!', false); } catch (error) { const errorMessage = error.message || 'Invalid JSON format'; showNotification('Error loading file: ' + errorMessage, true); } }; reader.readAsText(file); } fileInput.value = ''; }); document.getElementById('openBtn').addEventListener('click', openFile); document.getElementById('openBtn2').addEventListener('click', openFile); // ==================== SAVE FILE FUNCTION ==================== function saveFile() { const blob = new Blob([jsonOutput.value], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'data.json'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showNotification('JSON file saved!', false); } document.getElementById('saveBtn').addEventListener('click', saveFile); document.getElementById('saveBtn2').addEventListener('click', saveFile); // ==================== COPY FUNCTION ==================== copyBtn.addEventListener('click', () => { jsonOutput.select(); document.execCommand('copy'); showNotification('JSON copied to clipboard!', false); }); document.getElementById('copyBtnMenu').addEventListener('click', () => { jsonOutput.select(); document.execCommand('copy'); showNotification('JSON copied to clipboard!', false); }); // ==================== DOWNLOAD FUNCTION ==================== downloadBtn.addEventListener('click', () => { saveFile(); }); // ==================== UNDO/REDO FUNCTIONS ==================== function undo() { if (historyIndex > 0) { historyIndex--; jsonData = JSON.parse(JSON.stringify(history[historyIndex])); renderEditor(); updateOutput(); showNotification('Undo successful', false); } } function redo() { if (historyIndex < history.length - 1) { historyIndex++; jsonData = JSON.parse(JSON.stringify(history[historyIndex])); renderEditor(); updateOutput(); showNotification('Redo successful', false); } } // Menu undo/redo document.getElementById('undoBtn').addEventListener('click', undo); document.getElementById('redoBtn').addEventListener('click', redo); // Toolbar undo/redo document.getElementById('undoBtn2').addEventListener('click', undo); document.getElementById('redoBtn2').addEventListener('click', redo); // ==================== FORMAT FUNCTION ==================== function formatJSON() { updateOutput(); showNotification('JSON formatted', false); } document.getElementById('formatBtn').addEventListener('click', formatJSON); document.getElementById('formatBtn2').addEventListener('click', formatJSON); // ==================== VALIDATE FUNCTION ==================== function validateJSON() { try { JSON.parse(jsonOutput.value); showNotification('JSON is valid!', false); } catch (e) { showNotification('Invalid JSON: ' + e.message, true); } } document.getElementById('validateBtn').addEventListener('click', validateJSON); document.getElementById('validateBtn2').addEventListener('click', validateJSON); // ==================== SAMPLE JSON FUNCTION ==================== document.getElementById('sampleBtn').addEventListener('click', () => { loadJSON(sampleJSON); showNotification('Sample JSON loaded', false); }); // ==================== INSTRUCTIONS FUNCTION ==================== document.getElementById('instructionsBtn').addEventListener('click', () => { showNotification('Click on any key or value to edit. Use Tab to navigate, Shift+Enter to add new fields.', false); }); // ==================== CUT/PASTE/DEFAULT FUNCTIONS ==================== document.getElementById('cutBtnMenu').addEventListener('click', () => { jsonOutput.select(); document.execCommand('copy'); document.execCommand('delete'); showNotification('Cut to clipboard', false); }); document.getElementById('pasteBtnMenu').addEventListener('click', () => { navigator.clipboard.readText().then(text => { try { const data = JSON.parse(text); loadJSON(data); showNotification('JSON pasted and loaded', false); } catch (e) { jsonOutput.value += text; showNotification('Text pasted', false); } }).catch(() => { showNotification('Unable to access clipboard', true); }); }); document.getElementById('preferencesBtn').addEventListener('click', () => { showNotification('Preferences coming soon!', false); }); // ==================== CORE EDITOR FUNCTIONS ==================== // Load JSON data into the editor function loadJSON(data) { try { if (data === null || data === undefined) { jsonData = {}; } else if (typeof data !== 'object') { jsonData = { value: data }; } else { jsonData = JSON.parse(JSON.stringify(data)); } renderEditor(); updateOutput(); } catch (e) { console.error('Error loading JSON:', e); jsonData = {}; renderEditor(); updateOutput(); } } // Render the JSON editor function renderEditor() { jsonEditor.innerHTML = ''; lineNumbersLeft.innerHTML = ''; lineNumbersRight.innerHTML = ''; editableFields = []; currentFieldIndex = -1; currentLine = 0; lineElements = []; renderElement(jsonEditor, jsonData, 0, 'root'); updateLineNumbers(); } // Update line numbers on both sides function updateLineNumbers() { // Get all json-item elements (including closing brackets) const items = jsonEditor.querySelectorAll('.json-item'); const totalLines = items.length; lineNumbersLeft.innerHTML = ''; lineNumbersRight.innerHTML = ''; // Create a document fragment for better performance const fragmentLeft = document.createDocumentFragment(); const fragmentRight = document.createDocumentFragment(); for (let i = 0; i < totalLines; i++) { const lineNum = document.createElement('div'); lineNum.className = 'h-5 leading-5 flex items-center justify-end pr-2'; lineNum.textContent = (i + 1).toString(); const lineNumRight = lineNum.cloneNode(true); lineNumRight.className = 'h-5 leading-5 flex items-center justify-start pl-2'; fragmentLeft.appendChild(lineNum); fragmentRight.appendChild(lineNumRight); } lineNumbersLeft.appendChild(fragmentLeft); lineNumbersRight.appendChild(fragmentRight); } // Render a single JSON element function renderElement(container, data, depth, key, parentKey = 'root') { const layerClass = `lcars-layer-${Math.min(depth, 7)}`; const layerColor = layerColors[Math.min(depth, 7)]; if (typeof data === 'object' && data !== null) { // Object or Array - render opening bracket and children const wrapper = document.createElement('div'); wrapper.className = `json-item relative ${layerClass}`; wrapper.dataset.key = key; wrapper.dataset.parent = parentKey; wrapper.dataset.depth = depth; wrapper.dataset.layer = Math.min(depth, 7); wrapper.dataset.type = Array.isArray(data) ? 'array' : 'object'; wrapper.dataset.line = currentLine; lineElements.push(wrapper); currentLine++; // Make primitive items draggable (not opening/closing brackets) if (key !== 'root' && key !== 'closing') { wrapper.classList.add('draggable'); wrapper.draggable = true; // Add drag handle const dragHandle = document.createElement('div'); dragHandle.className = 'drag-handle'; dragHandle.innerHTML = ''; dragHandle.style.left = `${depth * 24 + 2}px`; wrapper.appendChild(dragHandle); // Add drag event listeners addDragListeners(wrapper, key, parentKey, depth); } // Layer indicator line (LCARS style) const layerIndicator = document.createElement('div'); layerIndicator.className = 'layer-indicator'; layerIndicator.style.left = `${depth * 24}px`; layerIndicator.style.background = layerColor; layerIndicator.style.boxShadow = `2px 0 8px ${layerColor}`; wrapper.appendChild(layerIndicator); const content = document.createElement('div'); content.className = 'json-item-content flex items-start py-1'; content.style.marginLeft = `${depth * 24 + 12}px`; // Key (for objects, not arrays) if (!Array.isArray(data) && key !== 'root') { const keySpan = document.createElement('span'); keySpan.className = 'json-key'; keySpan.textContent = `"${key}"`; keySpan.style.color = '#93c5fd'; content.appendChild(keySpan); const colon = document.createElement('span'); colon.textContent = ': '; colon.className = 'text-slate-500'; content.appendChild(colon); // Make key editable keySpan.style.cursor = 'pointer'; keySpan.addEventListener('click', (e) => { e.stopPropagation(); makeEditable(keySpan, 'key', key, parentKey); }); } // Value or children if (Array.isArray(data)) { // Array const bracket = document.createElement('span'); bracket.className = `json-bracket bracket-layer-${Math.min(depth, 7)}`; bracket.textContent = '['; content.appendChild(bracket); wrapper.appendChild(content); container.appendChild(wrapper); // Render array items data.forEach((item, index) => { renderElement(container, item, depth + 1, index, key); }); // Closing bracket const closingWrapper = document.createElement('div'); closingWrapper.className = `json-item relative ${layerClass}`; closingWrapper.dataset.key = 'closing'; closingWrapper.dataset.parent = key; closingWrapper.dataset.depth = depth; closingWrapper.dataset.layer = Math.min(depth, 7); const closingLayerIndicator = document.createElement('div'); closingLayerIndicator.className = 'layer-indicator'; closingLayerIndicator.style.left = `${depth * 24}px`; closingWrapper.appendChild(closingLayerIndicator); const closingContent = document.createElement('div'); closingContent.className = 'json-item-content flex items-start py-1'; closingContent.style.marginLeft = `${depth * 24 + 12}px`; const closingBracket = document.createElement('span'); closingBracket.className = `json-bracket bracket-layer-${Math.min(depth, 7)}`; closingBracket.textContent = ']'; closingContent.appendChild(closingBracket); closingWrapper.appendChild(closingContent); container.appendChild(closingWrapper); } else { // Object const bracket = document.createElement('span'); bracket.className = `json-bracket bracket-layer-${Math.min(depth, 7)}`; bracket.textContent = '{'; content.appendChild(bracket); wrapper.appendChild(content); container.appendChild(wrapper); // Render object properties Object.keys(data).forEach(propKey => { renderElement(container, data[propKey], depth + 1, propKey, key); }); // Closing bracket const closingWrapper = document.createElement('div'); closingWrapper.className = `json-item relative ${layerClass}`; closingWrapper.dataset.key = 'closing'; closingWrapper.dataset.parent = key; closingWrapper.dataset.depth = depth; closingWrapper.dataset.layer = Math.min(depth, 7); const closingLayerIndicator = document.createElement('div'); closingLayerIndicator.className = 'layer-indicator'; closingLayerIndicator.style.left = `${depth * 24}px`; closingWrapper.appendChild(closingLayerIndicator); const closingContent = document.createElement('div'); closingContent.className = 'json-item-content flex items-start py-1'; closingContent.style.marginLeft = `${depth * 24 + 12}px`; const closingBracket = document.createElement('span'); closingBracket.className = `json-bracket bracket-layer-${Math.min(depth, 7)}`; closingBracket.textContent = '}'; closingContent.appendChild(closingBracket); closingWrapper.appendChild(closingContent); container.appendChild(closingWrapper); } } else { // Primitive value (string, number, boolean, null) const wrapper = document.createElement('div'); wrapper.className = `json-item relative ${layerClass}`; wrapper.dataset.key = key; wrapper.dataset.parent = parentKey; wrapper.dataset.depth = depth; wrapper.dataset.layer = Math.min(depth, 7); wrapper.dataset.type = 'primitive'; wrapper.dataset.line = currentLine; lineElements.push(wrapper); currentLine++; // Make draggable if (key !== 'root') { wrapper.classList.add('draggable'); wrapper.draggable = true; // Add drag handle const dragHandle = document.createElement('div'); dragHandle.className = 'drag-handle'; dragHandle.innerHTML = ''; dragHandle.style.left = `${depth * 24 + 2}px`; wrapper.appendChild(dragHandle); // Add drag event listeners addDragListeners(wrapper, key, parentKey, depth); } // Layer indicator line const layerIndicator = document.createElement('div'); layerIndicator.className = 'layer-indicator'; layerIndicator.style.left = `${depth * 24}px`; layerIndicator.style.background = layerColor; layerIndicator.style.boxShadow = `2px 0 8px ${layerColor}`; wrapper.appendChild(layerIndicator); const content = document.createElement('div'); content.className = 'json-item-content flex items-start py-1'; content.style.marginLeft = `${depth * 24 + 12}px`; // Key if (key !== 'root') { const keySpan = document.createElement('span'); keySpan.className = 'json-key'; keySpan.textContent = `"${key}"`; keySpan.style.color = '#93c5fd'; content.appendChild(keySpan); const colon = document.createElement('span'); colon.textContent = ': '; colon.className = 'text-slate-500'; content.appendChild(colon); // Make key editable keySpan.style.cursor = 'pointer'; keySpan.addEventListener('click', (e) => { e.stopPropagation(); makeEditable(keySpan, 'key', key, parentKey); }); } // Value const valueSpan = document.createElement('span'); valueSpan.className = 'json-value'; if (typeof data === 'string') { valueSpan.textContent = `"${data}"`; valueSpan.style.color = '#6ee7b7'; } else if (typeof data === 'number') { valueSpan.textContent = data; valueSpan.style.color = '#f472b6'; } else if (typeof data === 'boolean') { valueSpan.textContent = data; valueSpan.style.color = '#fbbf24'; } else if (data === null) { valueSpan.textContent = 'null'; valueSpan.style.color = '#94a3b8'; } content.appendChild(valueSpan); // Make value editable valueSpan.style.cursor = 'pointer'; valueSpan.addEventListener('click', (e) => { e.stopPropagation(); makeEditable(valueSpan, 'value', key, parentKey); }); wrapper.appendChild(content); container.appendChild(wrapper); } } // Make an element editable function makeEditable(span, type, key, parentKey) { let currentValue = type === 'key' ? key : getValue(parentKey, key); // Handle undefined/null values gracefully - allow editing even if undefined if (currentValue === undefined || currentValue === null) { currentValue = ''; } let displayValue = type === 'key' ? currentValue : JSON.stringify(currentValue); // Remove quotes from display for editing if (displayValue && displayValue.startsWith('"') && displayValue.endsWith('"')) { displayValue = displayValue.slice(1, -1); } // Store original span text content to calculate width const originalText = span.textContent; const originalWidth = span.offsetWidth; // Create a wrapper div to hold both the hint and input const wrapper = document.createElement('div'); wrapper.style.position = 'relative'; wrapper.style.display = 'inline-block'; // Create the faint hint text const hint = document.createElement('span'); hint.textContent = originalText; hint.className = 'editable-hint'; hint.style.position = 'absolute'; hint.style.left = '0'; hint.style.top = '0'; hint.style.pointerEvents = 'none'; hint.style.opacity = '0.25'; hint.style.whiteSpace = 'pre'; const input = document.createElement('input'); input.type = 'text'; input.value = displayValue !== undefined ? displayValue : ''; input.className = `editable-field ${type}-input`; input.style.position = 'relative'; input.style.backgroundColor = 'transparent'; // Calculate appropriate width - minimum 50px or original width + padding const calculatedWidth = Math.max(50, originalWidth + 20); input.style.width = `${calculatedWidth}px`; hint.style.width = `${calculatedWidth}px`; // Build wrapper: hint behind, input on top wrapper.appendChild(hint); wrapper.appendChild(input); // Replace span with wrapper span.parentNode.replaceChild(wrapper, span); input.focus(); input.select(); // Adjust width dynamically as user types input.addEventListener('input', function() { const tempSpan = document.createElement('span'); tempSpan.style.font = getComputedStyle(input).font; tempSpan.style.visibility = 'hidden'; tempSpan.style.position = 'absolute'; tempSpan.textContent = input.value; document.body.appendChild(tempSpan); const newWidth = Math.max(50, tempSpan.offsetWidth + 30); input.style.width = `${newWidth}px`; document.body.removeChild(tempSpan); }); // Save on blur or Enter const saveEdit = () => { const newValue = input.value.trim(); const wrapper = input.parentNode; // Check if this is a newly inserted field that's still empty const isNewField = key === 'newField' || (key && key.toString().startsWith('newField')); const isEmptyValue = (type === 'value' && newValue === '') || (type === 'key' && newValue === ''); // If it's a new field and still empty, auto-delete it if (isNewField && isEmptyValue) { removeFromParent(parentKey, key); renderEditor(); updateOutput(); return; } if (type === 'key') { // Rename the key if (newValue !== key && newValue !== '') { renameKey(parentKey, key, newValue); } else { // Just restore the span without saving wrapper.parentNode.replaceChild(span, wrapper); } } else { // Update the value const parsedValue = parseValue(newValue); if (JSON.stringify(parsedValue) !== JSON.stringify(currentValue)) { updateValue(parentKey, key, parsedValue); } else { // Just restore the span without saving wrapper.parentNode.replaceChild(span, wrapper); } } }; input.addEventListener('blur', saveEdit); input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); // Save current edit saveEdit(); // Move to next field navigateFields(1, true); // true = auto-add comma/newline } else if (e.key === 'Tab') { e.preventDefault(); // Check for Ctrl+Tab or Ctrl+Shift+Tab for indentation if (e.ctrlKey) { // Change indentation depth const direction = e.shiftKey ? -1 : 1; changeIndentation(key, parentKey, direction); } else { // Normal tab navigation const direction = e.shiftKey ? -1 : 1; navigateFields(direction, false); } } else if (e.shiftKey && e.key === 'Enter') { e.preventDefault(); saveEdit(); insertNewField({ key, parentKey }); } }); // Track that we're editing so we don't auto-delete this field const wrapper = input.parentNode; wrapper.dataset.isEditing = 'true'; } // Navigate to next/previous editable field function navigateFields(direction, autoAddField = false) { // Find all editable spans const keys = jsonEditor.querySelectorAll('.json-key'); const values = jsonEditor.querySelectorAll('.json-value'); const allFields = []; keys.forEach(k => allFields.push({ element: k, type: 'key' })); values.forEach(v => allFields.push({ element: v, type: 'value' })); let currentIndex = -1; for (let i = 0; i < allFields.length; i++) { if (document.activeElement === allFields[i].element || document.activeElement.parentNode === allFields[i].element.parentNode) { currentIndex = i; break; } } let nextIndex = currentIndex + direction; // If we're on the last field and going forward, add a new field first if (autoAddField && nextIndex >= allFields.length) { // Find current field's info if (currentIndex >= 0) { const currentField = allFields[currentIndex]; const parentElement = currentField.element.closest('.json-item'); if (parentElement) { insertNewField({ key: parentElement.dataset.key, parentKey: parentElement.dataset.parent }); // Re-render and navigate to the new field setTimeout(() => navigateFields(direction, false), 10); return; } } } if (nextIndex < 0) nextIndex = allFields.length - 1; if (nextIndex >= allFields.length) nextIndex = 0; if (allFields[nextIndex]) { allFields[nextIndex].element.click(); } } // Change indentation depth of a field - simplified version function changeIndentation(key, parentKey, direction) { const currentValue = getValue(parentKey, key); if (currentValue === undefined) return; showNotification('Indentation changes via keyboard are limited. Use drag and drop for complex restructuring.', false); } // Get nested value function getValue(parentKey, key) { if (parentKey === 'root' || parentKey === undefined) { return jsonData !== undefined ? jsonData[key] : undefined; } const parent = findElementByKey(jsonData, parentKey); if (parent && typeof parent === 'object') { return parent[key]; } return undefined; } // Update a value function updateValue(parentKey, key, newValue) { if (parentKey === 'root' || parentKey === undefined) { if (jsonData !== undefined && jsonData[key] !== undefined) { jsonData[key] = newValue; } } else { const parent = findElementByKey(jsonData, parentKey); if (parent && typeof parent === 'object' && parent[key] !== undefined) { parent[key] = newValue; } } renderEditor(); updateOutput(); saveToHistory(); showSavedIndicator(); } // Rename a key function renameKey(parentKey, oldKey, newKey) { if (parentKey === 'root' || parentKey === undefined) { if (jsonData[oldKey] !== undefined) { jsonData[newKey] = jsonData[oldKey]; delete jsonData[oldKey]; } } else { const parent = findElementByKey(jsonData, parentKey); if (parent && typeof parent === 'object' && !Array.isArray(parent) && parent[oldKey] !== undefined) { parent[newKey] = parent[oldKey]; delete parent[oldKey]; } } renderEditor(); updateOutput(); saveToHistory(); showSavedIndicator(); } // Insert a new field after the current one function insertNewField(currentFieldData) { const { key, parentKey } = currentFieldData; let fieldCounter = 1; let newKey = 'newField'; // Generate unique key name while (getValue(parentKey, newKey) !== undefined) { newKey = `newField${fieldCounter++}`; } // Mark this as a new field for tracking newKey = newKey + '_new'; const parent = (parentKey === 'root' || parentKey === undefined) ? jsonData : findElementByKey(jsonData, parentKey); if (!parent) { // If parent doesn't exist, initialize jsonData as empty object if (typeof jsonData !== 'object' || jsonData === null) { jsonData = {}; } jsonData[newKey] = ''; } else if (typeof parent === 'object' && parent !== null) { if (Array.isArray(parent)) { parent.push(''); } else { const keys = Object.keys(parent); const index = keys.indexOf(key); if (index === -1 || index === keys.length - 1 || key === null || key === undefined) { // Append at the end parent[newKey] = ''; } else { // Insert after current - need to reconstruct const newData = {}; keys.forEach((k, i) => { newData[k] = parent[k]; if (i === index) { newData[newKey] = ''; } }); Object.keys(parent).forEach(k => delete parent[k]); Object.assign(parent, newData); } } } renderEditor(); updateOutput(); saveToHistory(); showSavedIndicator(); // Auto-focus the new field after rendering setTimeout(() => { const newItems = jsonEditor.querySelectorAll('.json-item'); newItems.forEach(item => { if (item.dataset.key === newKey) { const keySpan = item.querySelector('.json-key'); if (keySpan) keySpan.click(); } }); }, 10); } // Parse a value from string to appropriate type function parseValue(value) { if (value === 'true') return true; if (value === 'false') return false; if (value === 'null') return null; if (!isNaN(value) && value.trim() !== '' && !isNaN(Number(value))) { return Number(value); } return value; } // Show saved indicator function showSavedIndicator() { savedIndicator.classList.add('show'); setTimeout(() => { savedIndicator.classList.remove('show'); }, 1500); } // Find an element by key in nested structure function findElementByKey(obj, key) { if (obj === null || obj === undefined) return null; if (obj[key] !== undefined) return obj; for (let prop in obj) { if (typeof obj[prop] === 'object' && obj[prop] !== null) { const result = findElementByKey(obj[prop], key); if (result) return result; } } return null; } // Update JSON output (left → right sync) function updateOutput() { if (isApplyingChanges) return; leftEditorModified = true; leftEditorLastModified = Date.now(); try { const jsonString = JSON.stringify(jsonData, null, 2); jsonOutput.value = jsonString; jsonOutput.style.borderColor = '#10b981'; validationStatus.innerHTML = 'Valid JSON'; rightEditorModified = false; } catch (e) { jsonOutput.style.borderColor = '#ef4444'; validationStatus.innerHTML = 'Invalid JSON'; } } // Sync line numbers scroll with editor jsonEditor.parentElement.addEventListener('scroll', function() { lineNumbersLeft.scrollTop = this.scrollTop; lineNumbersRight.scrollTop = this.scrollTop; }); // Apply changes from code editor to visual editor function applyCodeChanges(showMsg = true) { if (!jsonOutput.value.trim()) return false; try { const newData = JSON.parse(jsonOutput.value); isApplyingChanges = true; jsonData = JSON.parse(JSON.stringify(newData)); renderEditor(); updateOutput(); saveToHistory(); isApplyingChanges = false; rightEditorModified = false; if (showMsg) { showNotification('Changes applied successfully!', false); } return true; } catch (e) { jsonOutput.style.borderColor = '#ef4444'; validationStatus.innerHTML = 'Invalid JSON'; const errorMessage = e.message || 'Unknown JSON syntax error'; if (showMsg) { showNotification('Invalid JSON: ' + errorMessage, true); } return false; } } // Real-time validation and debounced auto-apply jsonOutput.addEventListener('input', () => { rightEditorModified = true; rightEditorLastModified = Date.now(); try { JSON.parse(jsonOutput.value); jsonOutput.style.borderColor = '#10b981'; validationStatus.innerHTML = 'Valid JSON'; // Clear previous timeout if (autoApplyTimeout) { clearTimeout(autoApplyTimeout); } // Set debounced auto-apply (1.5 seconds after user stops typing) autoApplyTimeout = setTimeout(() => { // Only auto-apply if left editor hasn't been modified recently const timeSinceLeftModified = Date.now() - (leftEditorLastModified || 0); if (!leftEditorModified || timeSinceLeftModified > 2000) { if (applyCodeChanges(false)) { showSavedIndicator(); } } }, 1500); } catch (e) { jsonOutput.style.borderColor = '#ef4444'; const errorMessage = e.message || 'Invalid JSON syntax'; validationStatus.innerHTML = 'Invalid JSON: ' + errorMessage + ''; } }); // Apply button click applyCodeBtn.addEventListener('click', () => applyCodeChanges(true)); // Auto-apply on Ctrl+Enter or Cmd+Enter jsonOutput.addEventListener('keydown', (e) => { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); applyCodeChanges(true); } }); // Paste in JSON output pane jsonOutput.addEventListener('paste', (e) => { e.preventDefault(); const pasteHandler = (text) => { try { const data = JSON.parse(text); loadJSON(data); showNotification('JSON pasted and loaded successfully', false); } catch (parseError) { const start = jsonOutput.selectionStart; const end = jsonOutput.selectionEnd; const currentValue = jsonOutput.value; jsonOutput.value = currentValue.substring(0, start) + text + currentValue.substring(end); } }; navigator.clipboard.readText().then(text => { pasteHandler(text); }).catch(err => { const text = (e.originalEvent || e).clipboardData.getData('text/plain'); pasteHandler(text); }); }); // ==================== CONTEXT MENU ==================== const contextMenu = document.getElementById('contextMenu'); let contextMenuTarget = null; let contextMenuKey = null; let contextMenuParentKey = null; // Show context menu on right-click jsonEditor.addEventListener('contextmenu', (e) => { e.preventDefault(); // Find the closest json-item const item = e.target.closest('.json-item'); if (!item) return; // Don't show on closing brackets if (item.dataset.key === 'closing') return; contextMenuTarget = item; contextMenuKey = item.dataset.key; contextMenuParentKey = item.dataset.parent; // Position the menu const x = Math.min(e.clientX, window.innerWidth - 200); const y = Math.min(e.clientY, window.innerHeight - 200); contextMenu.style.left = `${x}px`; contextMenu.style.top = `${y}px`; contextMenu.style.display = 'block'; }); // Hide context menu on click elsewhere document.addEventListener('click', (e) => { if (!contextMenu.contains(e.target)) { contextMenu.style.display = 'none'; } }); // Context menu actions document.getElementById('ctxInsertAfter').addEventListener('click', () => { insertNewField({ key: contextMenuKey, parentKey: contextMenuParentKey }); contextMenu.style.display = 'none'; }); document.getElementById('ctxInsertBefore').addEventListener('click', () => { const parent = (contextMenuParentKey === 'root' || contextMenuParentKey === undefined) ? jsonData : findElementByKey(jsonData, contextMenuParentKey); if (parent && typeof parent === 'object') { let newKey = 'newField'; let counter = 1; while (parent[newKey] !== undefined) { newKey = `newField${counter++}`; } // Mark as new field newKey = newKey + '_new'; if (Array.isArray(parent)) { const index = parseInt(contextMenuKey); if (!isNaN(index) && index >= 0) { parent.splice(index, 0, ''); } } else { const keys = Object.keys(parent); const index = keys.indexOf(contextMenuKey); if (index !== -1) { const newData = {}; keys.forEach((k, i) => { if (i === index) { newData[newKey] = ''; } newData[k] = parent[k]; }); Object.keys(parent).forEach(k => delete parent[k]); Object.assign(parent, newData); } } renderEditor(); updateOutput(); saveToHistory(); showSavedIndicator(); } contextMenu.style.display = 'none'; }); document.getElementById('ctxCopyKey').addEventListener('click', async () => { try { await navigator.clipboard.writeText(contextMenuKey); showNotification('Key copied to clipboard!', false); } catch (err) { showNotification('Failed to copy key', true); } contextMenu.style.display = 'none'; }); document.getElementById('ctxCopyValue').addEventListener('click', async () => { const value = getValue(contextMenuParentKey, contextMenuKey); try { await navigator.clipboard.writeText(JSON.stringify(value, null, 2)); showNotification('Value copied to clipboard!', false); } catch (err) { showNotification('Failed to copy value', true); } contextMenu.style.display = 'none'; }); document.getElementById('ctxDelete').addEventListener('click', () => { // No confirmation - just delete removeFromParent(contextMenuParentKey, contextMenuKey); renderEditor(); updateOutput(); saveToHistory(); showSavedIndicator(); contextMenu.style.display = 'none'; }); // Click on empty space between items to insert line or start new JSON jsonEditor.addEventListener('click', (e) => { // Only if clicking directly on json-editor (not on an item) if (e.target === jsonEditor) { // Check if editor is effectively empty const isEmpty = jsonEditor.innerHTML.trim() === '' || jsonEditor.querySelectorAll('.json-item').length === 0 || (jsonEditor.querySelectorAll('.json-item').length === 1 && jsonEditor.querySelector('.json-item')?.dataset.key === 'closing'); if (isEmpty || Object.keys(jsonData).length === 0) { // Initialize new JSON structure with empty object jsonData = {}; // Create a new field and focus it immediately insertNewField({ key: null, parentKey: 'root' }); } else { // Insert at root level insertNewField({ key: null, parentKey: 'root' }); } } }); // Show notification (success by default) function showNotification(message, isError = false) { const notificationContent = notification.querySelector('p'); const notificationTitle = notification.querySelector('h4'); const notificationIcon = notification.querySelector('i'); notificationContent.textContent = message; if (isError) { notificationTitle.textContent = 'Error!'; notificationIcon.className = 'fas fa-times-circle text-red-500 text-xl mt-0.5 mr-3'; notification.classList.remove('border-green-500'); notification.classList.add('border-red-500'); } else { notificationTitle.textContent = 'Success!'; notificationIcon.className = 'fas fa-check-circle text-green-500 text-xl mt-0.5 mr-3'; notification.classList.remove('border-red-500'); notification.classList.add('border-green-500'); } notification.classList.add('show'); setTimeout(() => { notification.classList.remove('show'); }, 4000); } // Save to history for undo/redo function saveToHistory() { if (historyIndex < history.length - 1) { history = history.slice(0, historyIndex + 1); } if (history.length >= MAX_HISTORY) { history.shift(); historyIndex--; } // Only save if different from current state if (history.length > 0 && historyIndex >= 0 && JSON.stringify(history[historyIndex]) === JSON.stringify(jsonData)) { return; } history.push(JSON.parse(JSON.stringify(jsonData))); historyIndex = history.length - 1; } // ==================== DRAG AND DROP FUNCTIONS ==================== // Add drag event listeners to an element function addDragListeners(element, key, parentKey, depth) { element.addEventListener('dragstart', (e) => { draggedElement = element; draggedData = { key: key, parentKey: parentKey, depth: depth, element: element }; element.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', JSON.stringify({ key, parentKey })); }); element.addEventListener('dragend', (e) => { element.classList.remove('dragging'); clearDropIndicators(); draggedElement = null; draggedData = null; dropTarget = null; dropPosition = null; }); element.addEventListener('dragover', (e) => { e.preventDefault(); if (draggedElement === element) return; const rect = element.getBoundingClientRect(); const y = e.clientY - rect.top; const height = rect.height; // Determine drop position based on mouse position if (y < height * 0.25) { dropPosition = 'above'; } else if (y > height * 0.75) { dropPosition = 'below'; } else { dropPosition = 'inside'; } dropTarget = element; updateDropIndicators(); }); element.addEventListener('dragleave', (e) => { element.classList.remove('drag-over', 'drag-over-above', 'drag-over-below', 'drag-over-inside'); }); element.addEventListener('drop', (e) => { e.preventDefault(); if (draggedElement === element) return; handleDrop(draggedData, dropTarget, dropPosition); }); } // Update visual drop indicators function updateDropIndicators() { clearDropIndicators(); if (!dropTarget) return; if (dropPosition === 'above') { dropTarget.classList.add('drag-over-above'); } else if (dropPosition === 'below') { dropTarget.classList.add('drag-over-below'); } else if (dropPosition === 'inside') { dropTarget.classList.add('drag-over-inside'); } } // Clear all drop indicators function clearDropIndicators() { document.querySelectorAll('.drag-over, .drag-over-above, .drag-over-below, .drag-over-inside').forEach(el => { el.classList.remove('drag-over', 'drag-over-above', 'drag-over-below', 'drag-over-inside'); }); } // Handle the drop operation function handleDrop(draggedData, dropTarget, dropPosition) { const { key: draggedKey, parentKey: draggedParentKey, depth: draggedDepth } = draggedData; const dropKey = dropTarget.dataset.key; const dropParentKey = dropTarget.dataset.parent; const dropDepth = parseInt(dropTarget.dataset.depth); const dropType = dropTarget.dataset.type; // Don't allow dropping on self if (draggedKey === dropKey && draggedParentKey === dropParentKey) { return; } // Don't allow dropping on closing brackets if (dropKey === 'closing') { return; } // Get the value being moved BEFORE removing const draggedParent = (draggedParentKey === 'root' || draggedParentKey === undefined) ? null : findElementByKey(jsonData, draggedParentKey); let movedValue = null; if (draggedParent === null) { if (Array.isArray(jsonData)) { const index = parseInt(draggedKey); if (!isNaN(index) && index >= 0 && index < jsonData.length) { movedValue = jsonData[index]; } } else { movedValue = jsonData[draggedKey]; } } else if (draggedParent && typeof draggedParent === 'object') { if (Array.isArray(draggedParent)) { const index = parseInt(draggedKey); if (!isNaN(index) && index >= 0 && index < draggedParent.length) { movedValue = draggedParent[index]; } } else { movedValue = draggedParent[draggedKey]; } } if (movedValue === null || movedValue === undefined) { showNotification('Could not move item - value not found', true); return; } // Determine new parent and position let newParentKey = dropParentKey; let insertPosition = 'after'; let insertKey = dropKey; if (dropPosition === 'above') { insertPosition = 'before'; newParentKey = dropParentKey; insertKey = dropKey; } else if (dropPosition === 'below') { insertPosition = 'after'; newParentKey = dropParentKey; insertKey = dropKey; } else if (dropPosition === 'inside') { newParentKey = dropKey; insertPosition = 'append'; insertKey = null; // For primitives, we can't insert inside - change to below if (dropType === 'primitive') { newParentKey = dropParentKey; insertPosition = 'after'; insertKey = dropKey; } } // Get the new parent object let newParent = null; if (newParentKey === 'root' || newParentKey === undefined) { newParent = jsonData; } else { newParent = findElementByKey(jsonData, newParentKey); } if (!newParent) { showNotification('Cannot move here - parent not found', true); return; } // If parent is a primitive, we can't insert inside - move to parent's parent if (typeof newParent !== 'object' || newParent === null) { showNotification('Cannot move here - invalid parent type', true); return; } // Remove from original location if (draggedParent === null) { if (Array.isArray(jsonData)) { const index = parseInt(draggedKey); if (!isNaN(index) && index >= 0 && index < jsonData.length) { jsonData.splice(index, 1); } } else { delete jsonData[draggedKey]; } } else { if (draggedParent && Array.isArray(draggedParent)) { const index = parseInt(draggedKey); if (!isNaN(index) && index >= 0 && index < draggedParent.length) { draggedParent.splice(index, 1); } } else if (draggedParent) { delete draggedParent[draggedKey]; } } // Insert at new location if (Array.isArray(newParent)) { if (insertPosition === 'append') { newParent.push(movedValue); } else if (insertPosition === 'before' || insertPosition === 'after') { const index = parseInt(insertKey); if (!isNaN(index) && index >= 0) { const insertIndex = insertPosition === 'after' ? index + 1 : index; newParent.splice(insertIndex, 0, movedValue); } else { newParent.push(movedValue); } } } else { // Object - need a key for the new value let newKey = 'newField'; let counter = 1; while (newParent[newKey] !== undefined) { newKey = `newField${counter++}`; } if (insertPosition === 'append') { newParent[newKey] = movedValue; } else if (insertPosition === 'before' || insertPosition === 'after') { const keys = Object.keys(newParent); const index = keys.indexOf(insertKey); if (index === -1) { // Key not found, just append newParent[newKey] = movedValue; } else { // Rebuild object with new item in correct position const newObj = {}; keys.forEach((k, i) => { if (insertPosition === 'before' && i === index) { newObj[newKey] = movedValue; } newObj[k] = newParent[k]; if (insertPosition === 'after' && i === index) { newObj[newKey] = movedValue; } }); // Clear old object and assign new one Object.keys(newParent).forEach(k => delete newParent[k]); Object.assign(newParent, newObj); } } } // Re-render and sync renderEditor(); updateOutput(); saveToHistory(); showSavedIndicator(); showNotification('Item moved successfully!', false); } // Remove an element from its parent function removeFromParent(parentKey, key) { if (parentKey === 'root' || parentKey === undefined) { delete jsonData[key]; } else { const parent = findElementByKey(jsonData, parentKey); if (parent && typeof parent === 'object') { if (Array.isArray(parent)) { const index = parseInt(key); parent.splice(index, 1); } else { delete parent[key]; } } } } });