Spaces:
Running
Running
🐳 10/02 - 13:47 - I think it's better if i save the whole chat and exit and reload; it'll lose all the changes but we've lost all our progerss anad moving backwards breaking it more now
Browse files- index.html +201 -418
index.html
CHANGED
|
@@ -6,6 +6,7 @@
|
|
| 6 |
<title>Visual JSON Editor</title>
|
| 7 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
|
|
| 9 |
<script>
|
| 10 |
tailwind.config = {
|
| 11 |
theme: {
|
|
@@ -20,13 +21,12 @@
|
|
| 20 |
}
|
| 21 |
}
|
| 22 |
</script>
|
| 23 |
-
<link rel="stylesheet" href="style.css">
|
| 24 |
</head>
|
| 25 |
<body class="bg-slate-900 min-h-screen p-4 md:p-8">
|
| 26 |
-
<div class="max-w-
|
| 27 |
<header class="mb-8 text-center">
|
| 28 |
<h1 class="text-3xl md:text-4xl font-bold text-slate-100 mb-2">Visual JSON Editor</h1>
|
| 29 |
-
<p class="text-slate-300">Edit JSON with
|
| 30 |
</header>
|
| 31 |
|
| 32 |
<!-- Menu Bar -->
|
|
@@ -57,13 +57,6 @@
|
|
| 57 |
<a href="#" id="validateBtn"><i class="fas fa-check-circle mr-2"></i>Validate</a>
|
| 58 |
</div>
|
| 59 |
</div>
|
| 60 |
-
<div class="dropdown">
|
| 61 |
-
<div class="menu-item">View</div>
|
| 62 |
-
<div class="dropdown-content">
|
| 63 |
-
<a href="#" id="viewModeBtn"><i class="fas fa-eye mr-2"></i>View Mode</a>
|
| 64 |
-
<a href="#" id="editModeBtn"><i class="fas fa-edit mr-2"></i>Edit Mode</a>
|
| 65 |
-
</div>
|
| 66 |
-
</div>
|
| 67 |
<div class="dropdown">
|
| 68 |
<div class="menu-item">Help</div>
|
| 69 |
<div class="dropdown-content">
|
|
@@ -103,24 +96,6 @@
|
|
| 103 |
</div>
|
| 104 |
</div>
|
| 105 |
|
| 106 |
-
<div class="p-4 bg-gray-50 border-b flex flex-wrap gap-2">
|
| 107 |
-
<div class="flex items-center gap-2 bg-white px-3 py-1 rounded-lg shadow-sm">
|
| 108 |
-
<span class="text-gray-600">Mode:</span>
|
| 109 |
-
<div class="flex gap-1">
|
| 110 |
-
<button id="viewModeBtn" class="toolbar-btn px-3 py-1 rounded-md">View</button>
|
| 111 |
-
<button id="editModeBtn" class="toolbar-btn px-3 py-1 rounded-md active">Edit</button>
|
| 112 |
-
</div>
|
| 113 |
-
</div>
|
| 114 |
-
|
| 115 |
-
<div class="flex items-center gap-2 bg-white px-3 py-1 rounded-lg shadow-sm">
|
| 116 |
-
<span class="text-gray-600">Indent:</span>
|
| 117 |
-
<div class="flex gap-1">
|
| 118 |
-
<button id="indentBtn" class="toolbar-btn px-3 py-1 rounded-md">2</button>
|
| 119 |
-
<button id="indentBtn4" class="toolbar-btn px-3 py-1 rounded-md">4</button>
|
| 120 |
-
</div>
|
| 121 |
-
</div>
|
| 122 |
-
</div>
|
| 123 |
-
|
| 124 |
<div class="p-4 flex flex-col lg:flex-row gap-4">
|
| 125 |
<!-- Left Panel - Visual JSON Editor -->
|
| 126 |
<div class="w-full lg:w-1/2">
|
|
@@ -231,8 +206,6 @@
|
|
| 231 |
// DOM elements
|
| 232 |
const jsonEditor = document.getElementById('jsonEditor');
|
| 233 |
const jsonOutput = document.getElementById('jsonOutput');
|
| 234 |
-
const openBtn = document.getElementById('openBtn');
|
| 235 |
-
const openBtn2 = document.getElementById('openBtn2');
|
| 236 |
const notification = document.getElementById('notification');
|
| 237 |
const copyBtn = document.getElementById('copyBtn');
|
| 238 |
const downloadBtn = document.getElementById('downloadBtn');
|
|
@@ -249,10 +222,9 @@
|
|
| 249 |
|
| 250 |
// Current state
|
| 251 |
let jsonData = {};
|
| 252 |
-
let selectedElement = null;
|
| 253 |
let history = [];
|
| 254 |
let historyIndex = -1;
|
| 255 |
-
const MAX_HISTORY = 50;
|
| 256 |
|
| 257 |
// Track all editable fields for TAB navigation
|
| 258 |
let editableFields = [];
|
|
@@ -267,7 +239,6 @@
|
|
| 267 |
// Initialize with sample data
|
| 268 |
loadJSON(sampleJSON);
|
| 269 |
|
| 270 |
-
// Event listeners
|
| 271 |
// Create hidden file input for opening files
|
| 272 |
const fileInput = document.createElement('input');
|
| 273 |
fileInput.type = 'file';
|
|
@@ -297,8 +268,8 @@
|
|
| 297 |
}
|
| 298 |
});
|
| 299 |
|
| 300 |
-
openBtn.addEventListener('click', openFile);
|
| 301 |
-
openBtn2.addEventListener('click', openFile);
|
| 302 |
|
| 303 |
copyBtn.addEventListener('click', () => {
|
| 304 |
jsonOutput.select();
|
|
@@ -321,7 +292,7 @@
|
|
| 321 |
|
| 322 |
// Load JSON data into the editor
|
| 323 |
function loadJSON(data) {
|
| 324 |
-
jsonData = JSON.parse(JSON.stringify(data));
|
| 325 |
renderEditor();
|
| 326 |
updateOutput();
|
| 327 |
saveToHistory();
|
|
@@ -330,12 +301,12 @@
|
|
| 330 |
// Render the JSON editor
|
| 331 |
function renderEditor() {
|
| 332 |
jsonEditor.innerHTML = '';
|
| 333 |
-
editableFields = [];
|
| 334 |
currentFieldIndex = -1;
|
| 335 |
renderElement(jsonEditor, jsonData, 0, 'root');
|
| 336 |
}
|
| 337 |
|
| 338 |
-
// Render a single JSON element
|
| 339 |
function renderElement(container, data, depth, key, parentKey = 'root') {
|
| 340 |
const layerClass = `lcars-layer-${Math.min(depth, 7)}`;
|
| 341 |
const layerColor = layerColors[Math.min(depth, 7)];
|
|
@@ -357,12 +328,6 @@
|
|
| 357 |
layerIndicator.style.boxShadow = `2px 0 8px ${layerColor}`;
|
| 358 |
wrapper.appendChild(layerIndicator);
|
| 359 |
|
| 360 |
-
// LCARS horizontal connector
|
| 361 |
-
const connector = document.createElement('div');
|
| 362 |
-
connector.className = 'lcars-connector';
|
| 363 |
-
connector.style.background = layerColor;
|
| 364 |
-
wrapper.appendChild(connector);
|
| 365 |
-
|
| 366 |
const content = document.createElement('div');
|
| 367 |
content.className = 'json-item-content flex items-start py-1';
|
| 368 |
content.style.marginLeft = `${depth * 24 + 12}px`;
|
|
@@ -373,81 +338,22 @@
|
|
| 373 |
keySpan.className = 'json-key';
|
| 374 |
keySpan.textContent = `"${key}"`;
|
| 375 |
keySpan.style.color = '#93c5fd';
|
| 376 |
-
keySpan.dataset.key = key;
|
| 377 |
-
keySpan.dataset.parent = parentKey;
|
| 378 |
-
keySpan.dataset.type = 'key';
|
| 379 |
-
keySpan.style.cursor = 'pointer';
|
| 380 |
-
|
| 381 |
-
// Track for tab navigation
|
| 382 |
-
editableFields.push({ element: keySpan, key, parentKey, type: 'key' });
|
| 383 |
-
|
| 384 |
-
// Single-click to edit
|
| 385 |
-
keySpan.addEventListener('click', (e) => {
|
| 386 |
-
e.stopPropagation();
|
| 387 |
-
makeEditable(keySpan, key, parentKey, 'key');
|
| 388 |
-
});
|
| 389 |
-
|
| 390 |
content.appendChild(keySpan);
|
| 391 |
|
| 392 |
const colon = document.createElement('span');
|
| 393 |
colon.textContent = ': ';
|
| 394 |
colon.className = 'text-slate-500';
|
| 395 |
content.appendChild(colon);
|
| 396 |
-
}
|
| 397 |
-
|
| 398 |
-
// Value or children
|
| 399 |
-
} else {
|
| 400 |
-
// Primitive value (string, number, boolean, null)
|
| 401 |
-
const valueSpan = document.createElement('span');
|
| 402 |
-
valueSpan.className = 'json-value';
|
| 403 |
-
|
| 404 |
-
if (key !== 'root') {
|
| 405 |
-
valueSpan.dataset.key = key;
|
| 406 |
-
valueSpan.dataset.parent = parentKey;
|
| 407 |
-
valueSpan.dataset.type = 'value';
|
| 408 |
-
valueSpan.style.color = '#6ee7b7';
|
| 409 |
-
|
| 410 |
-
// Format the value display
|
| 411 |
-
if (typeof data === 'string') {
|
| 412 |
-
valueSpan.textContent = `"${data}"`;
|
| 413 |
-
} else if (data === null) {
|
| 414 |
-
valueSpan.textContent = 'null';
|
| 415 |
-
valueSpan.style.color = '#f87171';
|
| 416 |
-
} else if (typeof data === 'boolean') {
|
| 417 |
-
valueSpan.textContent = data.toString();
|
| 418 |
-
valueSpan.style.color = data ? '#22c55e' : '#ef4444';
|
| 419 |
-
} else if (typeof data === 'number') {
|
| 420 |
-
valueSpan.textContent = data.toString();
|
| 421 |
-
valueSpan.style.color = '#fbbf24';
|
| 422 |
-
}
|
| 423 |
|
| 424 |
-
//
|
| 425 |
-
|
| 426 |
-
|
| 427 |
e.stopPropagation();
|
| 428 |
-
makeEditable(
|
| 429 |
});
|
| 430 |
-
} else {
|
| 431 |
-
if (typeof data === 'string') {
|
| 432 |
-
valueSpan.textContent = `"${data}"`;
|
| 433 |
-
} else if (data === null) {
|
| 434 |
-
valueSpan.textContent = 'null';
|
| 435 |
-
valueSpan.style.color = '#f87171';
|
| 436 |
-
} else if (typeof data === 'boolean') {
|
| 437 |
-
valueSpan.textContent = data.toString();
|
| 438 |
-
valueSpan.style.color = data ? '#22c55e' : '#ef4444';
|
| 439 |
-
} else if (typeof data === 'number') {
|
| 440 |
-
valueSpan.textContent = data.toString();
|
| 441 |
-
valueSpan.style.color = '#fbbf24';
|
| 442 |
-
}
|
| 443 |
}
|
| 444 |
|
| 445 |
-
|
| 446 |
-
wrapper.appendChild(content);
|
| 447 |
-
container.appendChild(wrapper);
|
| 448 |
-
}
|
| 449 |
-
|
| 450 |
-
if (typeof data === 'object' && data !== null) {
|
| 451 |
if (Array.isArray(data)) {
|
| 452 |
// Array
|
| 453 |
const bracket = document.createElement('span');
|
|
@@ -523,116 +429,207 @@
|
|
| 523 |
closingWrapper.appendChild(closingContent);
|
| 524 |
container.appendChild(closingWrapper);
|
| 525 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 526 |
}
|
| 527 |
}
|
| 528 |
|
| 529 |
-
// Make
|
| 530 |
-
function makeEditable(span,
|
| 531 |
-
const currentValue = type === '
|
| 532 |
-
|
| 533 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 534 |
|
| 535 |
const input = document.createElement('input');
|
| 536 |
input.type = 'text';
|
| 537 |
-
input.
|
| 538 |
-
input.
|
| 539 |
|
| 540 |
-
//
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
editableFields[fieldIndex] = fieldData;
|
| 545 |
-
currentFieldIndex = fieldIndex;
|
| 546 |
-
} else {
|
| 547 |
-
editableFields.push(fieldData);
|
| 548 |
-
currentFieldIndex = editableFields.length - 1;
|
| 549 |
-
}
|
| 550 |
-
|
| 551 |
-
// Handle keyboard navigation
|
| 552 |
-
input.addEventListener('keydown', (e) => {
|
| 553 |
-
if (e.key === 'Tab') {
|
| 554 |
-
e.preventDefault();
|
| 555 |
-
navigateToField(e.shiftKey ? -1 : 1);
|
| 556 |
-
} else if (e.key === 'Enter') {
|
| 557 |
-
e.preventDefault();
|
| 558 |
-
navigateToField(1);
|
| 559 |
-
} else if (e.shiftKey && e.key === 'Enter') {
|
| 560 |
-
e.preventDefault();
|
| 561 |
-
// Insert new field
|
| 562 |
-
input.blur();
|
| 563 |
-
insertNewField({ key, parentKey });
|
| 564 |
-
}
|
| 565 |
-
});
|
| 566 |
|
| 567 |
-
//
|
| 568 |
-
|
| 569 |
const newValue = input.value.trim();
|
|
|
|
| 570 |
|
| 571 |
if (type === 'key') {
|
| 572 |
-
//
|
| 573 |
-
if (newValue
|
| 574 |
-
|
| 575 |
-
const value = jsonData[key];
|
| 576 |
-
delete jsonData[key];
|
| 577 |
-
jsonData[newValue] = value;
|
| 578 |
-
} else {
|
| 579 |
-
const parent = findElementByKey(jsonData, parentKey);
|
| 580 |
-
if (parent && typeof parent === 'object') {
|
| 581 |
-
const value = parent[key];
|
| 582 |
-
delete parent[key];
|
| 583 |
-
parent[newValue] = value;
|
| 584 |
-
}
|
| 585 |
-
}
|
| 586 |
}
|
| 587 |
} else {
|
| 588 |
-
// Update value
|
| 589 |
const parsedValue = parseValue(newValue);
|
| 590 |
-
if (
|
| 591 |
-
|
| 592 |
-
} else {
|
| 593 |
-
const parent = findElementByKey(jsonData, parentKey);
|
| 594 |
-
if (parent && typeof parent === 'object') {
|
| 595 |
-
parent[key] = parsedValue;
|
| 596 |
-
}
|
| 597 |
}
|
| 598 |
}
|
| 599 |
-
|
| 600 |
-
renderEditor();
|
| 601 |
-
updateOutput();
|
| 602 |
-
saveToHistory();
|
| 603 |
-
showSavedIndicator();
|
| 604 |
-
});
|
| 605 |
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 610 |
}
|
| 611 |
|
| 612 |
-
// Navigate to next/previous field
|
| 613 |
-
function
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 628 |
}
|
| 629 |
}
|
| 630 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 631 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 632 |
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 636 |
|
| 637 |
// Insert a new field after the current one
|
| 638 |
function insertNewField(currentFieldData) {
|
|
@@ -643,7 +640,6 @@
|
|
| 643 |
const index = keys.indexOf(key);
|
| 644 |
const newKey = 'newField';
|
| 645 |
|
| 646 |
-
// Create new object with inserted field
|
| 647 |
const newData = {};
|
| 648 |
keys.forEach((k, i) => {
|
| 649 |
newData[k] = jsonData[k];
|
|
@@ -683,29 +679,6 @@
|
|
| 683 |
updateOutput();
|
| 684 |
saveToHistory();
|
| 685 |
showSavedIndicator();
|
| 686 |
-
|
| 687 |
-
setTimeout(() => {
|
| 688 |
-
const newField = editableFields.find(f => f.key === 'newField' && f.type === 'key');
|
| 689 |
-
if (newField) {
|
| 690 |
-
newField.element.click();
|
| 691 |
-
}
|
| 692 |
-
}, 50);
|
| 693 |
-
}
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
// Get nested value
|
| 700 |
-
function getNestedValue(obj, parentKey, key) {
|
| 701 |
-
if (parentKey === 'root') {
|
| 702 |
-
return obj[key];
|
| 703 |
-
}
|
| 704 |
-
const parent = findElementByKey(obj, parentKey);
|
| 705 |
-
if (parent && typeof parent === 'object') {
|
| 706 |
-
return parent[key];
|
| 707 |
-
}
|
| 708 |
-
return undefined;
|
| 709 |
}
|
| 710 |
|
| 711 |
// Parse a value from string to appropriate type
|
|
@@ -741,42 +714,15 @@
|
|
| 741 |
return null;
|
| 742 |
}
|
| 743 |
|
| 744 |
-
// Update JSON output
|
| 745 |
function updateOutput() {
|
| 746 |
if (isApplyingChanges) return;
|
| 747 |
try {
|
| 748 |
const jsonString = JSON.stringify(jsonData, null, 2);
|
| 749 |
-
|
| 750 |
-
// Add color spans to brackets
|
| 751 |
-
let coloredJson = jsonString;
|
| 752 |
-
const bracketStack = [];
|
| 753 |
-
let result = '';
|
| 754 |
-
|
| 755 |
-
for (let i = 0; i < coloredJson.length; i++) {
|
| 756 |
-
const char = coloredJson[i];
|
| 757 |
-
if (char === '{' || char === '[') {
|
| 758 |
-
const depth = bracketStack.length;
|
| 759 |
-
bracketStack.push(char);
|
| 760 |
-
const layerClass = `bracket-layer-${Math.min(depth, 7)}`;
|
| 761 |
-
result += `<span class="${layerClass}">${char}</span>`;
|
| 762 |
-
} else if (char === '}' || char === ']') {
|
| 763 |
-
bracketStack.pop();
|
| 764 |
-
const depth = bracketStack.length;
|
| 765 |
-
const layerClass = `bracket-layer-${Math.min(depth, 7)}`;
|
| 766 |
-
result += `<span class="${layerClass}">${char}</span>`;
|
| 767 |
-
} else {
|
| 768 |
-
result += char;
|
| 769 |
-
}
|
| 770 |
-
}
|
| 771 |
-
|
| 772 |
-
// For textarea, we can't use HTML, so just use plain text
|
| 773 |
jsonOutput.value = jsonString;
|
| 774 |
-
jsonOutput.classList.remove('error-highlight');
|
| 775 |
jsonOutput.style.borderColor = '#10b981';
|
| 776 |
validationStatus.innerHTML = '<i class="fas fa-check-circle text-green-500"></i><span class="text-green-600">Valid JSON</span>';
|
| 777 |
} catch (e) {
|
| 778 |
-
jsonOutput.value = 'Invalid JSON structure';
|
| 779 |
-
jsonOutput.classList.add('error-highlight');
|
| 780 |
jsonOutput.style.borderColor = '#ef4444';
|
| 781 |
validationStatus.innerHTML = '<i class="fas fa-exclamation-circle text-red-500"></i><span class="text-red-600">Invalid JSON</span>';
|
| 782 |
}
|
|
@@ -803,18 +749,12 @@
|
|
| 803 |
}
|
| 804 |
}
|
| 805 |
|
| 806 |
-
// Real-time validation
|
| 807 |
jsonOutput.addEventListener('input', () => {
|
| 808 |
try {
|
| 809 |
-
|
| 810 |
jsonOutput.style.borderColor = '#10b981';
|
| 811 |
validationStatus.innerHTML = '<i class="fas fa-check-circle text-green-500"></i><span class="text-green-600">Valid JSON</span>';
|
| 812 |
-
|
| 813 |
-
// Real-time sync: Update visual editor when valid JSON is typed
|
| 814 |
-
if (!isApplyingChanges) {
|
| 815 |
-
jsonData = parsed;
|
| 816 |
-
renderEditor();
|
| 817 |
-
}
|
| 818 |
} catch (e) {
|
| 819 |
jsonOutput.style.borderColor = '#ef4444';
|
| 820 |
validationStatus.innerHTML = '<i class="fas fa-exclamation-circle text-red-500"></i><span class="text-red-600">Invalid JSON</span>';
|
|
@@ -856,18 +796,15 @@
|
|
| 856 |
|
| 857 |
// Save to history for undo/redo
|
| 858 |
function saveToHistory() {
|
| 859 |
-
// Remove future history if we're not at the end
|
| 860 |
if (historyIndex < history.length - 1) {
|
| 861 |
history = history.slice(0, historyIndex + 1);
|
| 862 |
}
|
| 863 |
|
| 864 |
-
// Limit history size
|
| 865 |
if (history.length >= MAX_HISTORY) {
|
| 866 |
history.shift();
|
| 867 |
historyIndex--;
|
| 868 |
}
|
| 869 |
|
| 870 |
-
// Don't save if it's the same as current
|
| 871 |
if (history.length > 0 && JSON.stringify(history[historyIndex]) === JSON.stringify(jsonData)) {
|
| 872 |
return;
|
| 873 |
}
|
|
@@ -902,48 +839,7 @@
|
|
| 902 |
document.getElementById('undoBtn').addEventListener('click', undo);
|
| 903 |
document.getElementById('redoBtn').addEventListener('click', redo);
|
| 904 |
|
| 905 |
-
//
|
| 906 |
-
document.getElementById('copyBtnMenu').addEventListener('click', () => {
|
| 907 |
-
if (selectedElement) {
|
| 908 |
-
const range = document.createRange();
|
| 909 |
-
range.selectNode(selectedElement);
|
| 910 |
-
window.getSelection().removeAllRanges();
|
| 911 |
-
window.getSelection().addRange(range);
|
| 912 |
-
document.execCommand('copy');
|
| 913 |
-
window.getSelection().removeAllRanges();
|
| 914 |
-
showNotification('Copied to clipboard');
|
| 915 |
-
}
|
| 916 |
-
});
|
| 917 |
-
|
| 918 |
-
document.getElementById('cutBtnMenu').addEventListener('click', () => {
|
| 919 |
-
if (selectedElement) {
|
| 920 |
-
const range = document.createRange();
|
| 921 |
-
range.selectNode(selectedElement);
|
| 922 |
-
window.getSelection().removeAllRanges();
|
| 923 |
-
window.getSelection().addRange(range);
|
| 924 |
-
document.execCommand('cut');
|
| 925 |
-
window.getSelection().removeAllRanges();
|
| 926 |
-
showNotification('Cut to clipboard');
|
| 927 |
-
}
|
| 928 |
-
});
|
| 929 |
-
|
| 930 |
-
document.getElementById('pasteBtnMenu').addEventListener('click', () => {
|
| 931 |
-
navigator.clipboard.readText().then(text => {
|
| 932 |
-
// Try to parse as JSON and load if valid
|
| 933 |
-
try {
|
| 934 |
-
const data = JSON.parse(text);
|
| 935 |
-
loadJSON(data);
|
| 936 |
-
showNotification('JSON pasted successfully');
|
| 937 |
-
} catch (e) {
|
| 938 |
-
// If not valid JSON, just show notification
|
| 939 |
-
showNotification('Clipboard content is not valid JSON');
|
| 940 |
-
}
|
| 941 |
-
}).catch(err => {
|
| 942 |
-
showNotification('Failed to read clipboard contents');
|
| 943 |
-
});
|
| 944 |
-
});
|
| 945 |
-
|
| 946 |
-
// Enable pasting in JSON output pane
|
| 947 |
jsonOutput.addEventListener('paste', (e) => {
|
| 948 |
e.preventDefault();
|
| 949 |
const pasteHandler = (text) => {
|
|
@@ -952,54 +848,32 @@
|
|
| 952 |
loadJSON(data);
|
| 953 |
showNotification('JSON pasted and loaded successfully');
|
| 954 |
} catch (parseError) {
|
| 955 |
-
// Paste as plain text at cursor position
|
| 956 |
const start = jsonOutput.selectionStart;
|
| 957 |
const end = jsonOutput.selectionEnd;
|
| 958 |
const currentValue = jsonOutput.value;
|
| 959 |
jsonOutput.value = currentValue.substring(0, start) + text + currentValue.substring(end);
|
| 960 |
-
showNotification('Pasted as plain text (use Apply button to load)');
|
| 961 |
}
|
| 962 |
};
|
| 963 |
|
| 964 |
navigator.clipboard.readText().then(text => {
|
| 965 |
pasteHandler(text);
|
| 966 |
}).catch(err => {
|
| 967 |
-
// Fallback for older browsers
|
| 968 |
const text = (e.originalEvent || e).clipboardData.getData('text/plain');
|
| 969 |
pasteHandler(text);
|
| 970 |
});
|
| 971 |
});
|
| 972 |
|
| 973 |
-
// Enable pasting in JSON editor pane
|
| 974 |
-
jsonEditor.addEventListener('paste', (e) => {
|
| 975 |
-
e.preventDefault();
|
| 976 |
-
navigator.clipboard.readText().then(text => {
|
| 977 |
-
try {
|
| 978 |
-
const data = JSON.parse(text);
|
| 979 |
-
loadJSON(data);
|
| 980 |
-
showNotification('JSON pasted successfully');
|
| 981 |
-
} catch (parseError) {
|
| 982 |
-
showNotification('Clipboard content is not valid JSON');
|
| 983 |
-
}
|
| 984 |
-
}).catch(err => {
|
| 985 |
-
// Fallback to clipboardData for older browsers
|
| 986 |
-
const text = (e.originalEvent || e).clipboardData.getData('text/plain');
|
| 987 |
-
try {
|
| 988 |
-
const data = JSON.parse(text);
|
| 989 |
-
loadJSON(data);
|
| 990 |
-
showNotification('JSON pasted successfully');
|
| 991 |
-
} catch (parseError) {
|
| 992 |
-
showNotification('Clipboard content is not valid JSON');
|
| 993 |
-
}
|
| 994 |
-
});
|
| 995 |
-
});
|
| 996 |
-
|
| 997 |
// Format JSON
|
| 998 |
document.getElementById('formatBtn').addEventListener('click', () => {
|
| 999 |
updateOutput();
|
| 1000 |
showNotification('JSON formatted');
|
| 1001 |
});
|
| 1002 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1003 |
// Validate JSON
|
| 1004 |
document.getElementById('validateBtn').addEventListener('click', () => {
|
| 1005 |
try {
|
|
@@ -1010,100 +884,9 @@
|
|
| 1010 |
}
|
| 1011 |
});
|
| 1012 |
|
| 1013 |
-
// Duplicate event listeners for toolbar buttons (buttons with "2" suffix)
|
| 1014 |
-
document.getElementById('formatBtn2').addEventListener('click', () => {
|
| 1015 |
-
updateOutput();
|
| 1016 |
-
showNotification('JSON formatted');
|
| 1017 |
-
});
|
| 1018 |
-
|
| 1019 |
document.getElementById('validateBtn2').addEventListener('click', () => {
|
| 1020 |
try {
|
| 1021 |
JSON.parse(jsonOutput.value);
|
| 1022 |
showNotification('JSON is valid!');
|
| 1023 |
} catch (e) {
|
| 1024 |
-
showNotification('Invalid JSON: ' + e.message);
|
| 1025 |
-
}
|
| 1026 |
-
});
|
| 1027 |
-
|
| 1028 |
-
document.getElementById('undoBtn2').addEventListener('click', undo);
|
| 1029 |
-
document.getElementById('redoBtn2').addEventListener('click', redo);
|
| 1030 |
-
|
| 1031 |
-
// New button
|
| 1032 |
-
const newBtn = document.getElementById('newBtn');
|
| 1033 |
-
const newBtn2 = document.getElementById('newBtn2');
|
| 1034 |
-
function newJSON() {
|
| 1035 |
-
jsonData = {};
|
| 1036 |
-
renderEditor();
|
| 1037 |
-
updateOutput();
|
| 1038 |
-
saveToHistory();
|
| 1039 |
-
showNotification('New JSON created');
|
| 1040 |
-
}
|
| 1041 |
-
newBtn.addEventListener('click', newJSON);
|
| 1042 |
-
newBtn2.addEventListener('click', newJSON);
|
| 1043 |
-
|
| 1044 |
-
// Save button
|
| 1045 |
-
document.getElementById('saveBtn').addEventListener('click', () => {
|
| 1046 |
-
downloadBtn.click();
|
| 1047 |
-
});
|
| 1048 |
-
document.getElementById('saveBtn2').addEventListener('click', () => {
|
| 1049 |
-
downloadBtn.click();
|
| 1050 |
-
});
|
| 1051 |
-
|
| 1052 |
-
// Preferences button
|
| 1053 |
-
document.getElementById('preferencesBtn').addEventListener('click', () => {
|
| 1054 |
-
showNotification('Preferences coming soon!');
|
| 1055 |
-
});
|
| 1056 |
-
|
| 1057 |
-
// Instructions button
|
| 1058 |
-
document.getElementById('instructionsBtn').addEventListener('click', () => {
|
| 1059 |
-
showNotification('Visual: Single-click to edit • Tab/Shift+Tab to navigate • Shift+Enter for new field • Auto-save enabled');
|
| 1060 |
-
});
|
| 1061 |
-
|
| 1062 |
-
// Sample JSON button
|
| 1063 |
-
document.getElementById('sampleBtn').addEventListener('click', () => {
|
| 1064 |
-
loadJSON(sampleJSON);
|
| 1065 |
-
showNotification('Sample JSON loaded');
|
| 1066 |
-
});
|
| 1067 |
-
|
| 1068 |
-
// View/Edit mode buttons
|
| 1069 |
-
let isEditMode = true;
|
| 1070 |
-
const viewModeBtns = [document.getElementById('viewModeBtn')];
|
| 1071 |
-
const editModeBtns = [document.getElementById('editModeBtn')];
|
| 1072 |
-
|
| 1073 |
-
viewModeBtns.forEach(btn => {
|
| 1074 |
-
btn.addEventListener('click', () => {
|
| 1075 |
-
isEditMode = false;
|
| 1076 |
-
viewModeBtns.forEach(b => b.classList.add('active'));
|
| 1077 |
-
editModeBtns.forEach(b => b.classList.remove('active'));
|
| 1078 |
-
showNotification('View mode enabled');
|
| 1079 |
-
});
|
| 1080 |
-
});
|
| 1081 |
-
|
| 1082 |
-
editModeBtns.forEach(btn => {
|
| 1083 |
-
btn.addEventListener('click', () => {
|
| 1084 |
-
isEditMode = true;
|
| 1085 |
-
editModeBtns.forEach(b => b.classList.add('active'));
|
| 1086 |
-
viewModeBtns.forEach(b => b.classList.remove('active'));
|
| 1087 |
-
showNotification('Edit mode enabled');
|
| 1088 |
-
});
|
| 1089 |
-
});
|
| 1090 |
-
|
| 1091 |
-
// Indent buttons
|
| 1092 |
-
const indentBtn = document.getElementById('indentBtn');
|
| 1093 |
-
const indentBtn4 = document.getElementById('indentBtn4');
|
| 1094 |
-
|
| 1095 |
-
indentBtn.addEventListener('click', () => {
|
| 1096 |
-
indentBtn.classList.add('active');
|
| 1097 |
-
indentBtn4.classList.remove('active');
|
| 1098 |
-
updateOutput();
|
| 1099 |
-
});
|
| 1100 |
-
|
| 1101 |
-
indentBtn4.addEventListener('click', () => {
|
| 1102 |
-
indentBtn4.classList.add('active');
|
| 1103 |
-
indentBtn.classList.remove('active');
|
| 1104 |
-
updateOutput();
|
| 1105 |
-
});
|
| 1106 |
-
});
|
| 1107 |
-
</script>
|
| 1108 |
-
</body>
|
| 1109 |
-
</html>
|
|
|
|
| 6 |
<title>Visual JSON Editor</title>
|
| 7 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
| 9 |
+
<link rel="stylesheet" href="style.css">
|
| 10 |
<script>
|
| 11 |
tailwind.config = {
|
| 12 |
theme: {
|
|
|
|
| 21 |
}
|
| 22 |
}
|
| 23 |
</script>
|
|
|
|
| 24 |
</head>
|
| 25 |
<body class="bg-slate-900 min-h-screen p-4 md:p-8">
|
| 26 |
+
<div class="max-w-7xl mx-auto">
|
| 27 |
<header class="mb-8 text-center">
|
| 28 |
<h1 class="text-3xl md:text-4xl font-bold text-slate-100 mb-2">Visual JSON Editor</h1>
|
| 29 |
+
<p class="text-slate-300">Edit JSON with intuitive visual editing and keyboard navigation</p>
|
| 30 |
</header>
|
| 31 |
|
| 32 |
<!-- Menu Bar -->
|
|
|
|
| 57 |
<a href="#" id="validateBtn"><i class="fas fa-check-circle mr-2"></i>Validate</a>
|
| 58 |
</div>
|
| 59 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
<div class="dropdown">
|
| 61 |
<div class="menu-item">Help</div>
|
| 62 |
<div class="dropdown-content">
|
|
|
|
| 96 |
</div>
|
| 97 |
</div>
|
| 98 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
<div class="p-4 flex flex-col lg:flex-row gap-4">
|
| 100 |
<!-- Left Panel - Visual JSON Editor -->
|
| 101 |
<div class="w-full lg:w-1/2">
|
|
|
|
| 206 |
// DOM elements
|
| 207 |
const jsonEditor = document.getElementById('jsonEditor');
|
| 208 |
const jsonOutput = document.getElementById('jsonOutput');
|
|
|
|
|
|
|
| 209 |
const notification = document.getElementById('notification');
|
| 210 |
const copyBtn = document.getElementById('copyBtn');
|
| 211 |
const downloadBtn = document.getElementById('downloadBtn');
|
|
|
|
| 222 |
|
| 223 |
// Current state
|
| 224 |
let jsonData = {};
|
|
|
|
| 225 |
let history = [];
|
| 226 |
let historyIndex = -1;
|
| 227 |
+
const MAX_HISTORY = 50;
|
| 228 |
|
| 229 |
// Track all editable fields for TAB navigation
|
| 230 |
let editableFields = [];
|
|
|
|
| 239 |
// Initialize with sample data
|
| 240 |
loadJSON(sampleJSON);
|
| 241 |
|
|
|
|
| 242 |
// Create hidden file input for opening files
|
| 243 |
const fileInput = document.createElement('input');
|
| 244 |
fileInput.type = 'file';
|
|
|
|
| 268 |
}
|
| 269 |
});
|
| 270 |
|
| 271 |
+
document.getElementById('openBtn').addEventListener('click', openFile);
|
| 272 |
+
document.getElementById('openBtn2').addEventListener('click', openFile);
|
| 273 |
|
| 274 |
copyBtn.addEventListener('click', () => {
|
| 275 |
jsonOutput.select();
|
|
|
|
| 292 |
|
| 293 |
// Load JSON data into the editor
|
| 294 |
function loadJSON(data) {
|
| 295 |
+
jsonData = JSON.parse(JSON.stringify(data));
|
| 296 |
renderEditor();
|
| 297 |
updateOutput();
|
| 298 |
saveToHistory();
|
|
|
|
| 301 |
// Render the JSON editor
|
| 302 |
function renderEditor() {
|
| 303 |
jsonEditor.innerHTML = '';
|
| 304 |
+
editableFields = [];
|
| 305 |
currentFieldIndex = -1;
|
| 306 |
renderElement(jsonEditor, jsonData, 0, 'root');
|
| 307 |
}
|
| 308 |
|
| 309 |
+
// Render a single JSON element (COMPLETE VERSION)
|
| 310 |
function renderElement(container, data, depth, key, parentKey = 'root') {
|
| 311 |
const layerClass = `lcars-layer-${Math.min(depth, 7)}`;
|
| 312 |
const layerColor = layerColors[Math.min(depth, 7)];
|
|
|
|
| 328 |
layerIndicator.style.boxShadow = `2px 0 8px ${layerColor}`;
|
| 329 |
wrapper.appendChild(layerIndicator);
|
| 330 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
const content = document.createElement('div');
|
| 332 |
content.className = 'json-item-content flex items-start py-1';
|
| 333 |
content.style.marginLeft = `${depth * 24 + 12}px`;
|
|
|
|
| 338 |
keySpan.className = 'json-key';
|
| 339 |
keySpan.textContent = `"${key}"`;
|
| 340 |
keySpan.style.color = '#93c5fd';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
content.appendChild(keySpan);
|
| 342 |
|
| 343 |
const colon = document.createElement('span');
|
| 344 |
colon.textContent = ': ';
|
| 345 |
colon.className = 'text-slate-500';
|
| 346 |
content.appendChild(colon);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 347 |
|
| 348 |
+
// Make key editable
|
| 349 |
+
keySpan.style.cursor = 'pointer';
|
| 350 |
+
keySpan.addEventListener('click', (e) => {
|
| 351 |
e.stopPropagation();
|
| 352 |
+
makeEditable(keySpan, 'key', key, parentKey);
|
| 353 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 354 |
}
|
| 355 |
|
| 356 |
+
// Value or children
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
if (Array.isArray(data)) {
|
| 358 |
// Array
|
| 359 |
const bracket = document.createElement('span');
|
|
|
|
| 429 |
closingWrapper.appendChild(closingContent);
|
| 430 |
container.appendChild(closingWrapper);
|
| 431 |
}
|
| 432 |
+
} else {
|
| 433 |
+
// Primitive value (string, number, boolean, null)
|
| 434 |
+
const wrapper = document.createElement('div');
|
| 435 |
+
wrapper.className = `json-item relative ${layerClass}`;
|
| 436 |
+
wrapper.dataset.key = key;
|
| 437 |
+
wrapper.dataset.parent = parentKey;
|
| 438 |
+
wrapper.dataset.depth = depth;
|
| 439 |
+
wrapper.dataset.layer = Math.min(depth, 7);
|
| 440 |
+
|
| 441 |
+
// Layer indicator line
|
| 442 |
+
const layerIndicator = document.createElement('div');
|
| 443 |
+
layerIndicator.className = 'layer-indicator';
|
| 444 |
+
layerIndicator.style.left = `${depth * 24}px`;
|
| 445 |
+
layerIndicator.style.background = layerColor;
|
| 446 |
+
layerIndicator.style.boxShadow = `2px 0 8px ${layerColor}`;
|
| 447 |
+
wrapper.appendChild(layerIndicator);
|
| 448 |
+
|
| 449 |
+
const content = document.createElement('div');
|
| 450 |
+
content.className = 'json-item-content flex items-start py-1';
|
| 451 |
+
content.style.marginLeft = `${depth * 24 + 12}px`;
|
| 452 |
+
|
| 453 |
+
// Key
|
| 454 |
+
if (key !== 'root') {
|
| 455 |
+
const keySpan = document.createElement('span');
|
| 456 |
+
keySpan.className = 'json-key';
|
| 457 |
+
keySpan.textContent = `"${key}"`;
|
| 458 |
+
keySpan.style.color = '#93c5fd';
|
| 459 |
+
content.appendChild(keySpan);
|
| 460 |
+
|
| 461 |
+
const colon = document.createElement('span');
|
| 462 |
+
colon.textContent = ': ';
|
| 463 |
+
colon.className = 'text-slate-500';
|
| 464 |
+
content.appendChild(colon);
|
| 465 |
+
|
| 466 |
+
// Make key editable
|
| 467 |
+
keySpan.style.cursor = 'pointer';
|
| 468 |
+
keySpan.addEventListener('click', (e) => {
|
| 469 |
+
e.stopPropagation();
|
| 470 |
+
makeEditable(keySpan, 'key', key, parentKey);
|
| 471 |
+
});
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
// Value
|
| 475 |
+
const valueSpan = document.createElement('span');
|
| 476 |
+
valueSpan.className = 'json-value';
|
| 477 |
+
if (typeof data === 'string') {
|
| 478 |
+
valueSpan.textContent = `"${data}"`;
|
| 479 |
+
valueSpan.style.color = '#6ee7b7';
|
| 480 |
+
} else if (typeof data === 'number') {
|
| 481 |
+
valueSpan.textContent = data;
|
| 482 |
+
valueSpan.style.color = '#f472b6';
|
| 483 |
+
} else if (typeof data === 'boolean') {
|
| 484 |
+
valueSpan.textContent = data;
|
| 485 |
+
valueSpan.style.color = '#fbbf24';
|
| 486 |
+
} else if (data === null) {
|
| 487 |
+
valueSpan.textContent = 'null';
|
| 488 |
+
valueSpan.style.color = '#94a3b8';
|
| 489 |
+
}
|
| 490 |
+
content.appendChild(valueSpan);
|
| 491 |
+
|
| 492 |
+
// Make value editable
|
| 493 |
+
valueSpan.style.cursor = 'pointer';
|
| 494 |
+
valueSpan.addEventListener('click', (e) => {
|
| 495 |
+
e.stopPropagation();
|
| 496 |
+
makeEditable(valueSpan, 'value', key, parentKey);
|
| 497 |
+
});
|
| 498 |
+
|
| 499 |
+
wrapper.appendChild(content);
|
| 500 |
+
container.appendChild(wrapper);
|
| 501 |
}
|
| 502 |
}
|
| 503 |
|
| 504 |
+
// Make an element editable
|
| 505 |
+
function makeEditable(span, type, key, parentKey) {
|
| 506 |
+
const currentValue = type === 'key' ? key : getValue(parentKey, key);
|
| 507 |
+
let displayValue = type === 'key' ? currentValue : JSON.stringify(currentValue);
|
| 508 |
+
|
| 509 |
+
// Remove quotes from display for editing
|
| 510 |
+
if (displayValue.startsWith('"') && displayValue.endsWith('"')) {
|
| 511 |
+
displayValue = displayValue.slice(1, -1);
|
| 512 |
+
}
|
| 513 |
|
| 514 |
const input = document.createElement('input');
|
| 515 |
input.type = 'text';
|
| 516 |
+
input.value = displayValue;
|
| 517 |
+
input.className = `editable-field ${type}-input`;
|
| 518 |
|
| 519 |
+
// Replace span with input
|
| 520 |
+
span.parentNode.replaceChild(input, span);
|
| 521 |
+
input.focus();
|
| 522 |
+
input.select();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 523 |
|
| 524 |
+
// Save on blur or Enter
|
| 525 |
+
const saveEdit = () => {
|
| 526 |
const newValue = input.value.trim();
|
| 527 |
+
input.parentNode.replaceChild(span, span);
|
| 528 |
|
| 529 |
if (type === 'key') {
|
| 530 |
+
// Rename the key
|
| 531 |
+
if (newValue !== key) {
|
| 532 |
+
renameKey(parentKey, key, newValue);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 533 |
}
|
| 534 |
} else {
|
| 535 |
+
// Update the value
|
| 536 |
const parsedValue = parseValue(newValue);
|
| 537 |
+
if (JSON.stringify(parsedValue) !== JSON.stringify(currentValue)) {
|
| 538 |
+
updateValue(parentKey, key, parsedValue);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 539 |
}
|
| 540 |
}
|
| 541 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 542 |
|
| 543 |
+
input.addEventListener('blur', saveEdit);
|
| 544 |
+
input.addEventListener('keydown', (e) => {
|
| 545 |
+
if (e.key === 'Enter') {
|
| 546 |
+
e.preventDefault();
|
| 547 |
+
input.blur();
|
| 548 |
+
} else if (e.key === 'Tab') {
|
| 549 |
+
e.preventDefault();
|
| 550 |
+
const direction = e.shiftKey ? -1 : 1;
|
| 551 |
+
navigateFields(direction);
|
| 552 |
+
} else if (e.key === 'Shift+Enter' || (e.shiftKey && e.key === 'Enter')) {
|
| 553 |
+
e.preventDefault();
|
| 554 |
+
saveEdit();
|
| 555 |
+
insertNewField({ key, parentKey });
|
| 556 |
+
}
|
| 557 |
+
});
|
| 558 |
}
|
| 559 |
|
| 560 |
+
// Navigate to next/previous editable field
|
| 561 |
+
function navigateFields(direction) {
|
| 562 |
+
// Find all editable spans
|
| 563 |
+
const keys = jsonEditor.querySelectorAll('.json-key');
|
| 564 |
+
const values = jsonEditor.querySelectorAll('.json-value');
|
| 565 |
+
const allFields = [];
|
| 566 |
+
|
| 567 |
+
keys.forEach(k => allFields.push({ element: k, type: 'key' }));
|
| 568 |
+
values.forEach(v => allFields.push({ element: v, type: 'value' }));
|
| 569 |
+
|
| 570 |
+
let currentIndex = -1;
|
| 571 |
+
for (let i = 0; i < allFields.length; i++) {
|
| 572 |
+
if (document.activeElement === allFields[i].element ||
|
| 573 |
+
document.activeElement.parentNode === allFields[i].element.parentNode) {
|
| 574 |
+
currentIndex = i;
|
| 575 |
+
break;
|
| 576 |
+
}
|
| 577 |
+
}
|
| 578 |
+
|
| 579 |
+
let nextIndex = currentIndex + direction;
|
| 580 |
+
if (nextIndex < 0) nextIndex = allFields.length - 1;
|
| 581 |
+
if (nextIndex >= allFields.length) nextIndex = 0;
|
| 582 |
+
|
| 583 |
+
if (allFields[nextIndex]) {
|
| 584 |
+
allFields[nextIndex].element.click();
|
| 585 |
}
|
| 586 |
}
|
| 587 |
|
| 588 |
+
// Get nested value
|
| 589 |
+
function getValue(parentKey, key) {
|
| 590 |
+
if (parentKey === 'root') {
|
| 591 |
+
return jsonData[key];
|
| 592 |
+
}
|
| 593 |
+
const parent = findElementByKey(jsonData, parentKey);
|
| 594 |
+
if (parent && typeof parent === 'object') {
|
| 595 |
+
return parent[key];
|
| 596 |
+
}
|
| 597 |
+
return undefined;
|
| 598 |
+
}
|
| 599 |
|
| 600 |
+
// Update a value
|
| 601 |
+
function updateValue(parentKey, key, newValue) {
|
| 602 |
+
if (parentKey === 'root') {
|
| 603 |
+
jsonData[key] = newValue;
|
| 604 |
+
} else {
|
| 605 |
+
const parent = findElementByKey(jsonData, parentKey);
|
| 606 |
+
if (parent && typeof parent === 'object') {
|
| 607 |
+
parent[key] = newValue;
|
| 608 |
+
}
|
| 609 |
+
}
|
| 610 |
+
renderEditor();
|
| 611 |
+
updateOutput();
|
| 612 |
+
saveToHistory();
|
| 613 |
+
showSavedIndicator();
|
| 614 |
+
}
|
| 615 |
|
| 616 |
+
// Rename a key
|
| 617 |
+
function renameKey(parentKey, oldKey, newKey) {
|
| 618 |
+
if (parentKey === 'root') {
|
| 619 |
+
jsonData[newKey] = jsonData[oldKey];
|
| 620 |
+
delete jsonData[oldKey];
|
| 621 |
+
} else {
|
| 622 |
+
const parent = findElementByKey(jsonData, parentKey);
|
| 623 |
+
if (parent && typeof parent === 'object' && !Array.isArray(parent)) {
|
| 624 |
+
parent[newKey] = parent[oldKey];
|
| 625 |
+
delete parent[oldKey];
|
| 626 |
+
}
|
| 627 |
+
}
|
| 628 |
+
renderEditor();
|
| 629 |
+
updateOutput();
|
| 630 |
+
saveToHistory();
|
| 631 |
+
showSavedIndicator();
|
| 632 |
+
}
|
| 633 |
|
| 634 |
// Insert a new field after the current one
|
| 635 |
function insertNewField(currentFieldData) {
|
|
|
|
| 640 |
const index = keys.indexOf(key);
|
| 641 |
const newKey = 'newField';
|
| 642 |
|
|
|
|
| 643 |
const newData = {};
|
| 644 |
keys.forEach((k, i) => {
|
| 645 |
newData[k] = jsonData[k];
|
|
|
|
| 679 |
updateOutput();
|
| 680 |
saveToHistory();
|
| 681 |
showSavedIndicator();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 682 |
}
|
| 683 |
|
| 684 |
// Parse a value from string to appropriate type
|
|
|
|
| 714 |
return null;
|
| 715 |
}
|
| 716 |
|
| 717 |
+
// Update JSON output
|
| 718 |
function updateOutput() {
|
| 719 |
if (isApplyingChanges) return;
|
| 720 |
try {
|
| 721 |
const jsonString = JSON.stringify(jsonData, null, 2);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 722 |
jsonOutput.value = jsonString;
|
|
|
|
| 723 |
jsonOutput.style.borderColor = '#10b981';
|
| 724 |
validationStatus.innerHTML = '<i class="fas fa-check-circle text-green-500"></i><span class="text-green-600">Valid JSON</span>';
|
| 725 |
} catch (e) {
|
|
|
|
|
|
|
| 726 |
jsonOutput.style.borderColor = '#ef4444';
|
| 727 |
validationStatus.innerHTML = '<i class="fas fa-exclamation-circle text-red-500"></i><span class="text-red-600">Invalid JSON</span>';
|
| 728 |
}
|
|
|
|
| 749 |
}
|
| 750 |
}
|
| 751 |
|
| 752 |
+
// Real-time validation of code editor
|
| 753 |
jsonOutput.addEventListener('input', () => {
|
| 754 |
try {
|
| 755 |
+
JSON.parse(jsonOutput.value);
|
| 756 |
jsonOutput.style.borderColor = '#10b981';
|
| 757 |
validationStatus.innerHTML = '<i class="fas fa-check-circle text-green-500"></i><span class="text-green-600">Valid JSON</span>';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 758 |
} catch (e) {
|
| 759 |
jsonOutput.style.borderColor = '#ef4444';
|
| 760 |
validationStatus.innerHTML = '<i class="fas fa-exclamation-circle text-red-500"></i><span class="text-red-600">Invalid JSON</span>';
|
|
|
|
| 796 |
|
| 797 |
// Save to history for undo/redo
|
| 798 |
function saveToHistory() {
|
|
|
|
| 799 |
if (historyIndex < history.length - 1) {
|
| 800 |
history = history.slice(0, historyIndex + 1);
|
| 801 |
}
|
| 802 |
|
|
|
|
| 803 |
if (history.length >= MAX_HISTORY) {
|
| 804 |
history.shift();
|
| 805 |
historyIndex--;
|
| 806 |
}
|
| 807 |
|
|
|
|
| 808 |
if (history.length > 0 && JSON.stringify(history[historyIndex]) === JSON.stringify(jsonData)) {
|
| 809 |
return;
|
| 810 |
}
|
|
|
|
| 839 |
document.getElementById('undoBtn').addEventListener('click', undo);
|
| 840 |
document.getElementById('redoBtn').addEventListener('click', redo);
|
| 841 |
|
| 842 |
+
// Paste in JSON output pane
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 843 |
jsonOutput.addEventListener('paste', (e) => {
|
| 844 |
e.preventDefault();
|
| 845 |
const pasteHandler = (text) => {
|
|
|
|
| 848 |
loadJSON(data);
|
| 849 |
showNotification('JSON pasted and loaded successfully');
|
| 850 |
} catch (parseError) {
|
|
|
|
| 851 |
const start = jsonOutput.selectionStart;
|
| 852 |
const end = jsonOutput.selectionEnd;
|
| 853 |
const currentValue = jsonOutput.value;
|
| 854 |
jsonOutput.value = currentValue.substring(0, start) + text + currentValue.substring(end);
|
|
|
|
| 855 |
}
|
| 856 |
};
|
| 857 |
|
| 858 |
navigator.clipboard.readText().then(text => {
|
| 859 |
pasteHandler(text);
|
| 860 |
}).catch(err => {
|
|
|
|
| 861 |
const text = (e.originalEvent || e).clipboardData.getData('text/plain');
|
| 862 |
pasteHandler(text);
|
| 863 |
});
|
| 864 |
});
|
| 865 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 866 |
// Format JSON
|
| 867 |
document.getElementById('formatBtn').addEventListener('click', () => {
|
| 868 |
updateOutput();
|
| 869 |
showNotification('JSON formatted');
|
| 870 |
});
|
| 871 |
|
| 872 |
+
document.getElementById('formatBtn2').addEventListener('click', () => {
|
| 873 |
+
updateOutput();
|
| 874 |
+
showNotification('JSON formatted');
|
| 875 |
+
});
|
| 876 |
+
|
| 877 |
// Validate JSON
|
| 878 |
document.getElementById('validateBtn').addEventListener('click', () => {
|
| 879 |
try {
|
|
|
|
| 884 |
}
|
| 885 |
});
|
| 886 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 887 |
document.getElementById('validateBtn2').addEventListener('click', () => {
|
| 888 |
try {
|
| 889 |
JSON.parse(jsonOutput.value);
|
| 890 |
showNotification('JSON is valid!');
|
| 891 |
} catch (e) {
|
| 892 |
+
showNotification('Invalid JSON: ' + e.message);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|