jsonic-v2 / script.js
RobinsAIWorld's picture
🐳 10/02 - 15:00 - I didn't dcancel
22a5c1c verified
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];
}
}
}
}
});