Spaces:
Running
Running
| 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 = '<i class="fas fa-check-circle mr-2"></i>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 = '<i class="fas fa-grip-vertical"></i>'; | |
| 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 = '<i class="fas fa-grip-vertical"></i>'; | |
| 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 = '<i class="fas fa-check-circle text-green-500"></i><span class="text-green-600">Valid JSON</span>'; | |
| rightEditorModified = false; | |
| } catch (e) { | |
| jsonOutput.style.borderColor = '#ef4444'; | |
| validationStatus.innerHTML = '<i class="fas fa-exclamation-circle text-red-500"></i><span class="text-red-600">Invalid JSON</span>'; | |
| } | |
| } | |
| // 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 = '<i class="fas fa-exclamation-circle text-red-500"></i><span class="text-red-600">Invalid JSON</span>'; | |
| 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 = '<i class="fas fa-check-circle text-green-500"></i><span class="text-green-600">Valid JSON</span>'; | |
| // 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 = '<i class="fas fa-exclamation-circle text-red-500"></i><span class="text-red-600">Invalid JSON: ' + errorMessage + '</span>'; | |
| } | |
| }); | |
| // 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]; | |
| } | |
| } | |
| } | |
| } | |
| }); |