Spaces:
Running
Running
🐳 10/02 - 13:23 - Also, could you make the little highlights that appear on the active line, be colour-coded for each indent level, and alwayds show, with 'star trek: tng LCARS-type' lines joining sa
Browse files- index.html +383 -216
- style.css +101 -0
index.html
CHANGED
|
@@ -25,73 +25,52 @@
|
|
| 25 |
min-height: 500px;
|
| 26 |
max-height: 70vh;
|
| 27 |
overflow-y: auto;
|
| 28 |
-
background-color: #
|
| 29 |
-
background-image:
|
|
|
|
|
|
|
| 30 |
background-size: 20px 20px;
|
| 31 |
padding: 20px;
|
| 32 |
border-radius: 8px;
|
|
|
|
| 33 |
}
|
| 34 |
.json-item {
|
| 35 |
transition: all 0.2s ease;
|
| 36 |
-
margin:
|
| 37 |
border-radius: 6px;
|
| 38 |
-
background-color:
|
| 39 |
-
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
| 40 |
position: relative;
|
| 41 |
-
display:
|
|
|
|
| 42 |
}
|
| 43 |
.json-item-content {
|
| 44 |
-
padding:
|
| 45 |
margin-left: 0;
|
| 46 |
-
display: inline-
|
|
|
|
| 47 |
min-width: 200px;
|
| 48 |
-
|
| 49 |
}
|
| 50 |
.json-item:hover .json-item-content {
|
| 51 |
-
background-color:
|
| 52 |
-
border-left: 3px solid #60a5fa;
|
| 53 |
}
|
| 54 |
-
.json-item.
|
| 55 |
-
background-color:
|
| 56 |
-
border-left: 3px solid #60a5fa;
|
| 57 |
-
box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.3);
|
| 58 |
}
|
| 59 |
.json-item.editing .json-item-content {
|
| 60 |
-
background-color:
|
| 61 |
-
border-left: 3px solid #fbbf24;
|
| 62 |
-
box-shadow: 0 0 0 3px rgba(251, 191, 36, 0.5);
|
| 63 |
-
}
|
| 64 |
-
.json-item:hover {
|
| 65 |
-
background-color: #475569;
|
| 66 |
-
border-left: 3px solid #60a5fa;
|
| 67 |
-
}
|
| 68 |
-
.json-item.selected {
|
| 69 |
-
background-color: #475569;
|
| 70 |
-
border-left: 3px solid #60a5fa;
|
| 71 |
-
box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.3);
|
| 72 |
-
}
|
| 73 |
-
.json-item.editing {
|
| 74 |
-
background-color: #64748b;
|
| 75 |
-
border-left: 3px solid #fbbf24;
|
| 76 |
-
box-shadow: 0 0 0 3px rgba(251, 191, 36, 0.5);
|
| 77 |
-
}
|
| 78 |
-
.json-item.dragging {
|
| 79 |
-
opacity: 0.5;
|
| 80 |
-
background-color: #475569;
|
| 81 |
-
}
|
| 82 |
-
.json-item.drag-over {
|
| 83 |
-
border-top: 2px dashed #60a5fa;
|
| 84 |
}
|
| 85 |
.json-key {
|
| 86 |
-
font-weight:
|
| 87 |
color: #93c5fd;
|
| 88 |
-
margin-right:
|
| 89 |
}
|
| 90 |
.json-value {
|
| 91 |
color: #6ee7b7;
|
| 92 |
}
|
| 93 |
.json-bracket {
|
| 94 |
-
|
|
|
|
| 95 |
}
|
| 96 |
.btn {
|
| 97 |
transition: all 0.2s ease;
|
|
@@ -104,22 +83,7 @@
|
|
| 104 |
left: 0;
|
| 105 |
top: 0;
|
| 106 |
bottom: 0;
|
| 107 |
-
width:
|
| 108 |
-
background-color: #475569;
|
| 109 |
-
}
|
| 110 |
-
.cursor-pointer {
|
| 111 |
-
cursor: pointer;
|
| 112 |
-
}
|
| 113 |
-
.editable:focus {
|
| 114 |
-
outline: 2px solid #60a5fa;
|
| 115 |
-
border-radius: 4px;
|
| 116 |
-
}
|
| 117 |
-
.editable {
|
| 118 |
-
min-width: 20px;
|
| 119 |
-
display: inline-block;
|
| 120 |
-
background-color: rgba(96, 165, 250, 0.1);
|
| 121 |
-
padding: 2px 4px;
|
| 122 |
-
border-radius: 4px;
|
| 123 |
}
|
| 124 |
.toolbar-btn {
|
| 125 |
transition: all 0.2s;
|
|
@@ -139,7 +103,7 @@
|
|
| 139 |
transform: translateX(0);
|
| 140 |
}
|
| 141 |
.error-highlight {
|
| 142 |
-
background-color:
|
| 143 |
border-left: 3px solid #ef4444;
|
| 144 |
}
|
| 145 |
.menu-bar {
|
|
@@ -227,6 +191,20 @@
|
|
| 227 |
border-radius: 4px;
|
| 228 |
padding: 2px 4px;
|
| 229 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
</style>
|
| 231 |
</head>
|
| 232 |
<body class="bg-slate-900 min-h-screen p-4 md:p-8">
|
|
@@ -334,7 +312,7 @@
|
|
| 334 |
<div class="bg-slate-700 rounded-lg p-4 h-full">
|
| 335 |
<div class="flex justify-between items-center mb-3">
|
| 336 |
<h2 class="text-lg font-semibold text-slate-200">Visual Editor</h2>
|
| 337 |
-
<span class="text-xs text-slate-400 bg-slate-800 px-2 py-1 rounded">
|
| 338 |
</div>
|
| 339 |
<div id="jsonEditor" class="json-editor border rounded-lg p-4 font-mono min-h-[500px] max-h-[600px] overflow-auto">
|
| 340 |
<!-- JSON content will be rendered here -->
|
|
@@ -347,7 +325,8 @@
|
|
| 347 |
<div class="bg-gray-50 rounded-lg p-4 h-full">
|
| 348 |
<div class="flex justify-between items-center mb-3">
|
| 349 |
<h2 class="text-lg font-semibold text-gray-700">Code Editor</h2>
|
| 350 |
-
<div class="flex gap-2">
|
|
|
|
| 351 |
<button id="applyCodeBtn" class="btn bg-green-500 hover:bg-green-600 text-white px-3 py-1 rounded-lg text-sm">
|
| 352 |
<i class="fas fa-check mr-1"></i> Apply
|
| 353 |
</button>
|
|
@@ -385,17 +364,17 @@
|
|
| 385 |
</div>
|
| 386 |
<div class="bg-green-50 p-4 rounded-lg border border-green-200">
|
| 387 |
<div class="text-green-500 text-2xl mb-2">
|
| 388 |
-
<i class="fas fa-
|
| 389 |
</div>
|
| 390 |
-
<h3 class="font-bold text-lg mb-2">
|
| 391 |
-
<p class="text-gray-700">
|
| 392 |
</div>
|
| 393 |
<div class="bg-purple-50 p-4 rounded-lg border border-purple-200">
|
| 394 |
<div class="text-purple-500 text-2xl mb-2">
|
| 395 |
-
<i class="fas fa-
|
| 396 |
</div>
|
| 397 |
-
<h3 class="font-bold text-lg mb-2">
|
| 398 |
-
<p class="text-gray-700">
|
| 399 |
</div>
|
| 400 |
</div>
|
| 401 |
</div>
|
|
@@ -444,6 +423,13 @@
|
|
| 444 |
const downloadBtn = document.getElementById('downloadBtn');
|
| 445 |
const applyCodeBtn = document.getElementById('applyCodeBtn');
|
| 446 |
const validationStatus = document.getElementById('validationStatus');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 447 |
let isApplyingChanges = false;
|
| 448 |
|
| 449 |
// Current state
|
|
@@ -451,6 +437,17 @@
|
|
| 451 |
let selectedElement = null;
|
| 452 |
let history = [];
|
| 453 |
let historyIndex = -1;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 454 |
|
| 455 |
// Initialize with sample data
|
| 456 |
loadJSON(sampleJSON);
|
|
@@ -518,36 +515,81 @@
|
|
| 518 |
// Render the JSON editor
|
| 519 |
function renderEditor() {
|
| 520 |
jsonEditor.innerHTML = '';
|
|
|
|
|
|
|
| 521 |
renderElement(jsonEditor, jsonData, 0, 'root');
|
| 522 |
}
|
| 523 |
|
| 524 |
// Render a single JSON element
|
| 525 |
function renderElement(container, data, depth, key = null, parentKey = null) {
|
|
|
|
| 526 |
const wrapper = document.createElement('div');
|
| 527 |
-
wrapper.className =
|
| 528 |
wrapper.dataset.key = key;
|
| 529 |
wrapper.dataset.parent = parentKey;
|
| 530 |
wrapper.dataset.depth = depth;
|
|
|
|
| 531 |
|
| 532 |
-
// Add
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 539 |
|
| 540 |
// Create element content
|
| 541 |
const content = document.createElement('div');
|
| 542 |
content.className = 'json-item-content flex items-start py-1';
|
| 543 |
-
content.style.marginLeft = `${depth *
|
| 544 |
|
| 545 |
-
// Key
|
| 546 |
if (key !== null && key !== 'root') {
|
| 547 |
-
const
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 551 |
}
|
| 552 |
|
| 553 |
// Value or children
|
|
@@ -555,7 +597,7 @@
|
|
| 555 |
if (Array.isArray(data)) {
|
| 556 |
// Array
|
| 557 |
const bracket = document.createElement('span');
|
| 558 |
-
bracket.className =
|
| 559 |
bracket.textContent = '[';
|
| 560 |
content.appendChild(bracket);
|
| 561 |
|
|
@@ -569,23 +611,22 @@
|
|
| 569 |
|
| 570 |
// Closing bracket
|
| 571 |
const closingWrapper = document.createElement('div');
|
| 572 |
-
closingWrapper.className =
|
| 573 |
closingWrapper.dataset.key = 'closing';
|
| 574 |
closingWrapper.dataset.parent = key;
|
| 575 |
closingWrapper.dataset.depth = depth;
|
|
|
|
| 576 |
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
closingWrapper.appendChild(indentLine);
|
| 582 |
-
}
|
| 583 |
|
| 584 |
const closingContent = document.createElement('div');
|
| 585 |
closingContent.className = 'json-item-content flex items-start py-1';
|
| 586 |
-
closingContent.style.marginLeft = `${depth *
|
| 587 |
const closingBracket = document.createElement('span');
|
| 588 |
-
closingBracket.className =
|
| 589 |
closingBracket.textContent = ']';
|
| 590 |
closingContent.appendChild(closingBracket);
|
| 591 |
closingWrapper.appendChild(closingContent);
|
|
@@ -593,7 +634,7 @@
|
|
| 593 |
} else {
|
| 594 |
// Object
|
| 595 |
const bracket = document.createElement('span');
|
| 596 |
-
bracket.className =
|
| 597 |
bracket.textContent = '{';
|
| 598 |
content.appendChild(bracket);
|
| 599 |
|
|
@@ -607,177 +648,290 @@
|
|
| 607 |
|
| 608 |
// Closing bracket
|
| 609 |
const closingWrapper = document.createElement('div');
|
| 610 |
-
closingWrapper.className =
|
| 611 |
closingWrapper.dataset.key = 'closing';
|
| 612 |
closingWrapper.dataset.parent = key;
|
| 613 |
closingWrapper.dataset.depth = depth;
|
|
|
|
| 614 |
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
closingWrapper.appendChild(indentLine);
|
| 620 |
-
}
|
| 621 |
|
| 622 |
const closingContent = document.createElement('div');
|
| 623 |
closingContent.className = 'json-item-content flex items-start py-1';
|
| 624 |
-
closingContent.style.marginLeft = `${depth *
|
| 625 |
const closingBracket = document.createElement('span');
|
| 626 |
-
closingBracket.className =
|
| 627 |
closingBracket.textContent = '}';
|
| 628 |
closingContent.appendChild(closingBracket);
|
| 629 |
closingWrapper.appendChild(closingContent);
|
| 630 |
container.appendChild(closingWrapper);
|
| 631 |
}
|
| 632 |
} else {
|
| 633 |
-
// Primitive value
|
| 634 |
-
const
|
| 635 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 636 |
|
| 637 |
if (typeof data === 'string') {
|
| 638 |
-
|
| 639 |
} else if (typeof data === 'boolean') {
|
| 640 |
-
|
|
|
|
|
|
|
| 641 |
} else {
|
| 642 |
-
|
| 643 |
}
|
| 644 |
|
| 645 |
-
content.appendChild(
|
| 646 |
wrapper.appendChild(content);
|
| 647 |
container.appendChild(wrapper);
|
| 648 |
-
}
|
| 649 |
-
|
| 650 |
-
// Add event listeners for editing
|
| 651 |
-
if (wrapper.dataset.key !== 'closing') {
|
| 652 |
-
wrapper.addEventListener('dblclick', () => editElement(wrapper));
|
| 653 |
-
wrapper.addEventListener('click', () => selectElement(wrapper));
|
| 654 |
-
}
|
| 655 |
-
}
|
| 656 |
-
|
| 657 |
-
// Edit an element
|
| 658 |
-
function editElement(element) {
|
| 659 |
-
if (element.dataset.key === 'closing') return;
|
| 660 |
-
|
| 661 |
-
const keyElement = element.querySelector('.json-key');
|
| 662 |
-
const valueElement = element.querySelector('.json-value');
|
| 663 |
-
|
| 664 |
-
if (keyElement) {
|
| 665 |
-
const keyText = keyElement.textContent.replace(/"/g, '').replace(':', '').trim();
|
| 666 |
-
const input = document.createElement('input');
|
| 667 |
-
input.type = 'text';
|
| 668 |
-
input.className = 'editable bg-yellow-100 px-1';
|
| 669 |
-
input.value = keyText;
|
| 670 |
-
keyElement.replaceWith(input);
|
| 671 |
-
input.focus();
|
| 672 |
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
|
|
|
|
|
|
| 678 |
});
|
| 679 |
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
});
|
| 685 |
-
}
|
| 686 |
-
|
| 687 |
-
if (valueElement) {
|
| 688 |
-
const valueText = valueElement.textContent.replace(/"/g, '');
|
| 689 |
-
const input = document.createElement('input');
|
| 690 |
-
input.type = 'text';
|
| 691 |
-
input.className = 'editable bg-yellow-100 px-1';
|
| 692 |
-
input.value = valueText;
|
| 693 |
-
valueElement.replaceWith(input);
|
| 694 |
-
input.focus();
|
| 695 |
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
input.replaceWith(valueElement);
|
| 700 |
-
valueElement.textContent = formatValue(newValue);
|
| 701 |
});
|
| 702 |
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
});
|
|
|
|
|
|
|
| 708 |
}
|
| 709 |
}
|
| 710 |
|
| 711 |
-
//
|
| 712 |
-
function
|
| 713 |
-
|
| 714 |
-
if (
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
}
|
| 719 |
|
| 720 |
-
//
|
| 721 |
-
function
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 726 |
}
|
| 727 |
|
| 728 |
-
//
|
| 729 |
-
function
|
| 730 |
-
|
| 731 |
-
|
| 732 |
|
| 733 |
-
if (parentKey === 'root') {
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 740 |
}
|
|
|
|
| 741 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 742 |
jsonData = newData;
|
| 743 |
} else {
|
| 744 |
-
//
|
| 745 |
-
const parent = findElementByKey(jsonData, parentKey);
|
| 746 |
-
if (parent && typeof parent === 'object') {
|
| 747 |
-
const
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 753 |
}
|
|
|
|
| 754 |
});
|
| 755 |
-
Object.keys(parent).forEach(key => delete parent[key]);
|
| 756 |
-
Object.assign(parent, newData);
|
| 757 |
}
|
| 758 |
}
|
| 759 |
|
| 760 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 761 |
updateOutput();
|
| 762 |
saveToHistory();
|
|
|
|
| 763 |
}
|
| 764 |
|
| 765 |
-
// Update a value
|
| 766 |
-
function updateValue(
|
| 767 |
-
const
|
| 768 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 769 |
|
| 770 |
if (parentKey === 'root') {
|
| 771 |
-
jsonData[key] =
|
| 772 |
} else {
|
| 773 |
const parent = findElementByKey(jsonData, parentKey);
|
| 774 |
if (parent && typeof parent === 'object') {
|
| 775 |
-
parent[key] =
|
| 776 |
}
|
| 777 |
}
|
| 778 |
|
| 779 |
updateOutput();
|
| 780 |
saveToHistory();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 781 |
}
|
| 782 |
|
| 783 |
// Find an element by key in nested structure
|
|
@@ -794,23 +948,14 @@
|
|
| 794 |
return null;
|
| 795 |
}
|
| 796 |
|
| 797 |
-
//
|
| 798 |
-
function selectElement(element) {
|
| 799 |
-
if (selectedElement) {
|
| 800 |
-
selectedElement.classList.remove('selected');
|
| 801 |
-
}
|
| 802 |
-
|
| 803 |
-
element.classList.add('selected');
|
| 804 |
-
selectedElement = element;
|
| 805 |
-
}
|
| 806 |
-
|
| 807 |
-
// Update JSON output
|
| 808 |
function updateOutput() {
|
| 809 |
if (isApplyingChanges) return;
|
| 810 |
try {
|
| 811 |
-
|
|
|
|
| 812 |
jsonOutput.classList.remove('error-highlight');
|
| 813 |
-
jsonOutput.style.borderColor = '#
|
| 814 |
validationStatus.innerHTML = '<i class="fas fa-check-circle text-green-500"></i><span class="text-green-600">Valid JSON</span>';
|
| 815 |
} catch (e) {
|
| 816 |
jsonOutput.value = 'Invalid JSON structure';
|
|
@@ -860,6 +1005,17 @@
|
|
| 860 |
}
|
| 861 |
});
|
| 862 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 863 |
// Show notification
|
| 864 |
function showNotification(message) {
|
| 865 |
const notificationContent = notification.querySelector('p');
|
|
@@ -878,6 +1034,17 @@
|
|
| 878 |
history = history.slice(0, historyIndex + 1);
|
| 879 |
}
|
| 880 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 881 |
history.push(JSON.parse(JSON.stringify(jsonData)));
|
| 882 |
historyIndex = history.length - 1;
|
| 883 |
}
|
|
@@ -1062,7 +1229,7 @@
|
|
| 1062 |
|
| 1063 |
// Instructions button
|
| 1064 |
document.getElementById('instructionsBtn').addEventListener('click', () => {
|
| 1065 |
-
showNotification('Visual:
|
| 1066 |
});
|
| 1067 |
|
| 1068 |
// Sample JSON button
|
|
|
|
| 25 |
min-height: 500px;
|
| 26 |
max-height: 70vh;
|
| 27 |
overflow-y: auto;
|
| 28 |
+
background-color: #0f172a;
|
| 29 |
+
background-image:
|
| 30 |
+
linear-gradient(rgba(30, 41, 59, 0.5) 1px, transparent 1px),
|
| 31 |
+
linear-gradient(90deg, rgba(30, 41, 59, 0.5) 1px, transparent 1px);
|
| 32 |
background-size: 20px 20px;
|
| 33 |
padding: 20px;
|
| 34 |
border-radius: 8px;
|
| 35 |
+
position: relative;
|
| 36 |
}
|
| 37 |
.json-item {
|
| 38 |
transition: all 0.2s ease;
|
| 39 |
+
margin: 4px 0;
|
| 40 |
border-radius: 6px;
|
| 41 |
+
background-color: rgba(30, 41, 59, 0.6);
|
|
|
|
| 42 |
position: relative;
|
| 43 |
+
display: flex;
|
| 44 |
+
align-items: center;
|
| 45 |
}
|
| 46 |
.json-item-content {
|
| 47 |
+
padding: 6px 12px;
|
| 48 |
margin-left: 0;
|
| 49 |
+
display: inline-flex;
|
| 50 |
+
align-items: center;
|
| 51 |
min-width: 200px;
|
| 52 |
+
gap: 8px;
|
| 53 |
}
|
| 54 |
.json-item:hover .json-item-content {
|
| 55 |
+
background-color: rgba(51, 65, 85, 0.5);
|
|
|
|
| 56 |
}
|
| 57 |
+
.json-item.active .json-item-content {
|
| 58 |
+
background-color: rgba(51, 65, 85, 0.7);
|
|
|
|
|
|
|
| 59 |
}
|
| 60 |
.json-item.editing .json-item-content {
|
| 61 |
+
background-color: rgba(100, 116, 139, 0.5);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
}
|
| 63 |
.json-key {
|
| 64 |
+
font-weight: 500;
|
| 65 |
color: #93c5fd;
|
| 66 |
+
margin-right: 4px;
|
| 67 |
}
|
| 68 |
.json-value {
|
| 69 |
color: #6ee7b7;
|
| 70 |
}
|
| 71 |
.json-bracket {
|
| 72 |
+
font-weight: bold;
|
| 73 |
+
font-family: monospace;
|
| 74 |
}
|
| 75 |
.btn {
|
| 76 |
transition: all 0.2s ease;
|
|
|
|
| 83 |
left: 0;
|
| 84 |
top: 0;
|
| 85 |
bottom: 0;
|
| 86 |
+
width: 2px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
}
|
| 88 |
.toolbar-btn {
|
| 89 |
transition: all 0.2s;
|
|
|
|
| 103 |
transform: translateX(0);
|
| 104 |
}
|
| 105 |
.error-highlight {
|
| 106 |
+
background-color: rgba(239, 68, 68, 0.1);
|
| 107 |
border-left: 3px solid #ef4444;
|
| 108 |
}
|
| 109 |
.menu-bar {
|
|
|
|
| 191 |
border-radius: 4px;
|
| 192 |
padding: 2px 4px;
|
| 193 |
}
|
| 194 |
+
|
| 195 |
+
/* Code Editor bracket coloring */
|
| 196 |
+
#jsonOutput {
|
| 197 |
+
line-height: 1.6;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
.bracket-layer-0 { color: #ef4444; }
|
| 201 |
+
.bracket-layer-1 { color: #f97316; }
|
| 202 |
+
.bracket-layer-2 { color: #eab308; }
|
| 203 |
+
.bracket-layer-3 { color: #22c55e; }
|
| 204 |
+
.bracket-layer-4 { color: #06b6d4; }
|
| 205 |
+
.bracket-layer-5 { color: #3b82f6; }
|
| 206 |
+
.bracket-layer-6 { color: #8b5cf6; }
|
| 207 |
+
.bracket-layer-7 { color: #ec4899; }
|
| 208 |
</style>
|
| 209 |
</head>
|
| 210 |
<body class="bg-slate-900 min-h-screen p-4 md:p-8">
|
|
|
|
| 312 |
<div class="bg-slate-700 rounded-lg p-4 h-full">
|
| 313 |
<div class="flex justify-between items-center mb-3">
|
| 314 |
<h2 class="text-lg font-semibold text-slate-200">Visual Editor</h2>
|
| 315 |
+
<span class="text-xs text-slate-400 bg-slate-800 px-2 py-1 rounded">Single-click to edit • Tab to navigate • Shift+Enter for new field</span>
|
| 316 |
</div>
|
| 317 |
<div id="jsonEditor" class="json-editor border rounded-lg p-4 font-mono min-h-[500px] max-h-[600px] overflow-auto">
|
| 318 |
<!-- JSON content will be rendered here -->
|
|
|
|
| 325 |
<div class="bg-gray-50 rounded-lg p-4 h-full">
|
| 326 |
<div class="flex justify-between items-center mb-3">
|
| 327 |
<h2 class="text-lg font-semibold text-gray-700">Code Editor</h2>
|
| 328 |
+
<div class="flex gap-2 items-center">
|
| 329 |
+
<span id="autoSaveStatus" class="text-xs text-green-600 hidden"><i class="fas fa-check-circle mr-1"></i>Auto-saved</span>
|
| 330 |
<button id="applyCodeBtn" class="btn bg-green-500 hover:bg-green-600 text-white px-3 py-1 rounded-lg text-sm">
|
| 331 |
<i class="fas fa-check mr-1"></i> Apply
|
| 332 |
</button>
|
|
|
|
| 364 |
</div>
|
| 365 |
<div class="bg-green-50 p-4 rounded-lg border border-green-200">
|
| 366 |
<div class="text-green-500 text-2xl mb-2">
|
| 367 |
+
<i class="fas fa-keyboard"></i>
|
| 368 |
</div>
|
| 369 |
+
<h3 class="font-bold text-lg mb-2">Keyboard Navigation</h3>
|
| 370 |
+
<p class="text-gray-700">Use Tab/Shift+Tab to navigate fields, Enter to move forward, Shift+Enter to insert new fields. Everything auto-saves!</p>
|
| 371 |
</div>
|
| 372 |
<div class="bg-purple-50 p-4 rounded-lg border border-purple-200">
|
| 373 |
<div class="text-purple-500 text-2xl mb-2">
|
| 374 |
+
<i class="fas fa-undo"></i>
|
| 375 |
</div>
|
| 376 |
+
<h3 class="font-bold text-lg mb-2">50-Step Undo History</h3>
|
| 377 |
+
<p class="text-gray-700">Made a mistake? No problem. With 50 levels of undo history, you can always go back. Auto-save protects your work.</p>
|
| 378 |
</div>
|
| 379 |
</div>
|
| 380 |
</div>
|
|
|
|
| 423 |
const downloadBtn = document.getElementById('downloadBtn');
|
| 424 |
const applyCodeBtn = document.getElementById('applyCodeBtn');
|
| 425 |
const validationStatus = document.getElementById('validationStatus');
|
| 426 |
+
|
| 427 |
+
// Create saved indicator
|
| 428 |
+
const savedIndicator = document.createElement('div');
|
| 429 |
+
savedIndicator.className = 'saved-indicator';
|
| 430 |
+
savedIndicator.innerHTML = '<i class="fas fa-check-circle mr-2"></i>Saved';
|
| 431 |
+
document.body.appendChild(savedIndicator);
|
| 432 |
+
|
| 433 |
let isApplyingChanges = false;
|
| 434 |
|
| 435 |
// Current state
|
|
|
|
| 437 |
let selectedElement = null;
|
| 438 |
let history = [];
|
| 439 |
let historyIndex = -1;
|
| 440 |
+
const MAX_HISTORY = 50; // Increased undo history
|
| 441 |
+
|
| 442 |
+
// Track all editable fields for TAB navigation
|
| 443 |
+
let editableFields = [];
|
| 444 |
+
let currentFieldIndex = -1;
|
| 445 |
+
|
| 446 |
+
// Layer colors for visual distinction
|
| 447 |
+
const layerColors = [
|
| 448 |
+
'#ef4444', '#f97316', '#eab308', '#22c55e',
|
| 449 |
+
'#06b6d4', '#3b82f6', '#8b5cf6', '#ec4899'
|
| 450 |
+
];
|
| 451 |
|
| 452 |
// Initialize with sample data
|
| 453 |
loadJSON(sampleJSON);
|
|
|
|
| 515 |
// Render the JSON editor
|
| 516 |
function renderEditor() {
|
| 517 |
jsonEditor.innerHTML = '';
|
| 518 |
+
editableFields = []; // Clear tracked fields
|
| 519 |
+
currentFieldIndex = -1;
|
| 520 |
renderElement(jsonEditor, jsonData, 0, 'root');
|
| 521 |
}
|
| 522 |
|
| 523 |
// Render a single JSON element
|
| 524 |
function renderElement(container, data, depth, key = null, parentKey = null) {
|
| 525 |
+
const layerClass = `lcars-layer-${Math.min(depth, 7)}`;
|
| 526 |
const wrapper = document.createElement('div');
|
| 527 |
+
wrapper.className = `json-item relative ${layerClass}`;
|
| 528 |
wrapper.dataset.key = key;
|
| 529 |
wrapper.dataset.parent = parentKey;
|
| 530 |
wrapper.dataset.depth = depth;
|
| 531 |
+
wrapper.dataset.layer = Math.min(depth, 7);
|
| 532 |
|
| 533 |
+
// Add LCARS-style layer indicator
|
| 534 |
+
const layerIndicator = document.createElement('div');
|
| 535 |
+
layerIndicator.className = 'layer-indicator';
|
| 536 |
+
layerIndicator.style.left = `${depth * 24}px`;
|
| 537 |
+
wrapper.appendChild(layerIndicator);
|
| 538 |
+
|
| 539 |
+
// Add LCARS connector line
|
| 540 |
+
const connector = document.createElement('div');
|
| 541 |
+
connector.className = 'lcars-connector';
|
| 542 |
+
connector.style.left = `${depth * 24 + 4}px`;
|
| 543 |
+
wrapper.appendChild(connector);
|
| 544 |
|
| 545 |
// Create element content
|
| 546 |
const content = document.createElement('div');
|
| 547 |
content.className = 'json-item-content flex items-start py-1';
|
| 548 |
+
content.style.marginLeft = `${depth * 24 + 12}px`;
|
| 549 |
|
| 550 |
+
// Key - make it editable with single click
|
| 551 |
if (key !== null && key !== 'root') {
|
| 552 |
+
const keyInput = document.createElement('input');
|
| 553 |
+
keyInput.type = 'text';
|
| 554 |
+
keyInput.className = 'editable-field key-input';
|
| 555 |
+
keyInput.value = key;
|
| 556 |
+
keyInput.dataset.fieldType = 'key';
|
| 557 |
+
keyInput.dataset.parentKey = parentKey;
|
| 558 |
+
keyInput.dataset.depth = depth;
|
| 559 |
+
content.appendChild(keyInput);
|
| 560 |
+
|
| 561 |
+
const colon = document.createElement('span');
|
| 562 |
+
colon.className = 'json-bracket';
|
| 563 |
+
colon.textContent = ':';
|
| 564 |
+
content.appendChild(colon);
|
| 565 |
+
|
| 566 |
+
// Track editable field
|
| 567 |
+
editableFields.push({
|
| 568 |
+
element: keyInput,
|
| 569 |
+
type: 'key',
|
| 570 |
+
key: key,
|
| 571 |
+
parentKey: parentKey,
|
| 572 |
+
depth: depth
|
| 573 |
+
});
|
| 574 |
+
|
| 575 |
+
// Single-click to edit
|
| 576 |
+
keyInput.addEventListener('click', (e) => {
|
| 577 |
+
e.stopPropagation();
|
| 578 |
+
activateField(keyInput);
|
| 579 |
+
});
|
| 580 |
+
|
| 581 |
+
keyInput.addEventListener('focus', () => {
|
| 582 |
+
wrapper.classList.add('active');
|
| 583 |
+
wrapper.classList.add('editing');
|
| 584 |
+
});
|
| 585 |
+
|
| 586 |
+
keyInput.addEventListener('blur', () => {
|
| 587 |
+
wrapper.classList.remove('active');
|
| 588 |
+
wrapper.classList.remove('editing');
|
| 589 |
+
updateKey(keyInput);
|
| 590 |
+
});
|
| 591 |
+
|
| 592 |
+
keyInput.addEventListener('keydown', handleKeyNavigation);
|
| 593 |
}
|
| 594 |
|
| 595 |
// Value or children
|
|
|
|
| 597 |
if (Array.isArray(data)) {
|
| 598 |
// Array
|
| 599 |
const bracket = document.createElement('span');
|
| 600 |
+
bracket.className = `json-bracket bracket-layer-${Math.min(depth, 7)}`;
|
| 601 |
bracket.textContent = '[';
|
| 602 |
content.appendChild(bracket);
|
| 603 |
|
|
|
|
| 611 |
|
| 612 |
// Closing bracket
|
| 613 |
const closingWrapper = document.createElement('div');
|
| 614 |
+
closingWrapper.className = `json-item relative ${layerClass}`;
|
| 615 |
closingWrapper.dataset.key = 'closing';
|
| 616 |
closingWrapper.dataset.parent = key;
|
| 617 |
closingWrapper.dataset.depth = depth;
|
| 618 |
+
closingWrapper.dataset.layer = Math.min(depth, 7);
|
| 619 |
|
| 620 |
+
const closingLayerIndicator = document.createElement('div');
|
| 621 |
+
closingLayerIndicator.className = 'layer-indicator';
|
| 622 |
+
closingLayerIndicator.style.left = `${depth * 24}px`;
|
| 623 |
+
closingWrapper.appendChild(closingLayerIndicator);
|
|
|
|
|
|
|
| 624 |
|
| 625 |
const closingContent = document.createElement('div');
|
| 626 |
closingContent.className = 'json-item-content flex items-start py-1';
|
| 627 |
+
closingContent.style.marginLeft = `${depth * 24 + 12}px`;
|
| 628 |
const closingBracket = document.createElement('span');
|
| 629 |
+
closingBracket.className = `json-bracket bracket-layer-${Math.min(depth, 7)}`;
|
| 630 |
closingBracket.textContent = ']';
|
| 631 |
closingContent.appendChild(closingBracket);
|
| 632 |
closingWrapper.appendChild(closingContent);
|
|
|
|
| 634 |
} else {
|
| 635 |
// Object
|
| 636 |
const bracket = document.createElement('span');
|
| 637 |
+
bracket.className = `json-bracket bracket-layer-${Math.min(depth, 7)}`;
|
| 638 |
bracket.textContent = '{';
|
| 639 |
content.appendChild(bracket);
|
| 640 |
|
|
|
|
| 648 |
|
| 649 |
// Closing bracket
|
| 650 |
const closingWrapper = document.createElement('div');
|
| 651 |
+
closingWrapper.className = `json-item relative ${layerClass}`;
|
| 652 |
closingWrapper.dataset.key = 'closing';
|
| 653 |
closingWrapper.dataset.parent = key;
|
| 654 |
closingWrapper.dataset.depth = depth;
|
| 655 |
+
closingWrapper.dataset.layer = Math.min(depth, 7);
|
| 656 |
|
| 657 |
+
const closingLayerIndicator = document.createElement('div');
|
| 658 |
+
closingLayerIndicator.className = 'layer-indicator';
|
| 659 |
+
closingLayerIndicator.style.left = `${depth * 24}px`;
|
| 660 |
+
closingWrapper.appendChild(closingLayerIndicator);
|
|
|
|
|
|
|
| 661 |
|
| 662 |
const closingContent = document.createElement('div');
|
| 663 |
closingContent.className = 'json-item-content flex items-start py-1';
|
| 664 |
+
closingContent.style.marginLeft = `${depth * 24 + 12}px`;
|
| 665 |
const closingBracket = document.createElement('span');
|
| 666 |
+
closingBracket.className = `json-bracket bracket-layer-${Math.min(depth, 7)}`;
|
| 667 |
closingBracket.textContent = '}';
|
| 668 |
closingContent.appendChild(closingBracket);
|
| 669 |
closingWrapper.appendChild(closingContent);
|
| 670 |
container.appendChild(closingWrapper);
|
| 671 |
}
|
| 672 |
} else {
|
| 673 |
+
// Primitive value - make it editable
|
| 674 |
+
const valueInput = document.createElement('input');
|
| 675 |
+
valueInput.type = 'text';
|
| 676 |
+
valueInput.className = 'editable-field value-input';
|
| 677 |
+
valueInput.dataset.fieldType = 'value';
|
| 678 |
+
valueInput.dataset.key = key;
|
| 679 |
+
valueInput.dataset.parentKey = parentKey;
|
| 680 |
+
valueInput.dataset.depth = depth;
|
| 681 |
|
| 682 |
if (typeof data === 'string') {
|
| 683 |
+
valueInput.value = data;
|
| 684 |
} else if (typeof data === 'boolean') {
|
| 685 |
+
valueInput.value = data.toString();
|
| 686 |
+
} else if (data === null) {
|
| 687 |
+
valueInput.value = 'null';
|
| 688 |
} else {
|
| 689 |
+
valueInput.value = data.toString();
|
| 690 |
}
|
| 691 |
|
| 692 |
+
content.appendChild(valueInput);
|
| 693 |
wrapper.appendChild(content);
|
| 694 |
container.appendChild(wrapper);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 695 |
|
| 696 |
+
// Track editable field
|
| 697 |
+
editableFields.push({
|
| 698 |
+
element: valueInput,
|
| 699 |
+
type: 'value',
|
| 700 |
+
key: key,
|
| 701 |
+
parentKey: parentKey,
|
| 702 |
+
depth: depth
|
| 703 |
});
|
| 704 |
|
| 705 |
+
// Single-click to edit
|
| 706 |
+
valueInput.addEventListener('click', (e) => {
|
| 707 |
+
e.stopPropagation();
|
| 708 |
+
activateField(valueInput);
|
| 709 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 710 |
|
| 711 |
+
valueInput.addEventListener('focus', () => {
|
| 712 |
+
wrapper.classList.add('active');
|
| 713 |
+
wrapper.classList.add('editing');
|
|
|
|
|
|
|
| 714 |
});
|
| 715 |
|
| 716 |
+
valueInput.addEventListener('blur', () => {
|
| 717 |
+
wrapper.classList.remove('active');
|
| 718 |
+
wrapper.classList.remove('editing');
|
| 719 |
+
updateValue(valueInput);
|
| 720 |
});
|
| 721 |
+
|
| 722 |
+
valueInput.addEventListener('keydown', handleKeyNavigation);
|
| 723 |
}
|
| 724 |
}
|
| 725 |
|
| 726 |
+
// Activate a field and set it as current
|
| 727 |
+
function activateField(input) {
|
| 728 |
+
currentFieldIndex = editableFields.findIndex(f => f.element === input);
|
| 729 |
+
if (currentFieldIndex !== -1) {
|
| 730 |
+
editableFields.forEach(f => f.element.parentElement.classList.remove('active'));
|
| 731 |
+
input.parentElement.classList.add('active');
|
| 732 |
+
}
|
| 733 |
}
|
| 734 |
|
| 735 |
+
// Handle keyboard navigation
|
| 736 |
+
function handleKeyNavigation(e) {
|
| 737 |
+
const input = e.target;
|
| 738 |
+
const fieldData = editableFields.find(f => f.element === input);
|
| 739 |
+
|
| 740 |
+
if (e.key === 'Tab') {
|
| 741 |
+
e.preventDefault();
|
| 742 |
+
if (e.shiftKey) {
|
| 743 |
+
navigateToField(-1);
|
| 744 |
+
} else {
|
| 745 |
+
navigateToField(1);
|
| 746 |
+
}
|
| 747 |
+
} else if (e.key === 'Enter' && e.shiftKey) {
|
| 748 |
+
e.preventDefault();
|
| 749 |
+
// Insert new field after current one
|
| 750 |
+
insertNewField(fieldData);
|
| 751 |
+
} else if (e.key === 'Enter') {
|
| 752 |
+
e.preventDefault();
|
| 753 |
+
// Navigate to next field
|
| 754 |
+
navigateToField(1);
|
| 755 |
+
}
|
| 756 |
+
}
|
| 757 |
+
|
| 758 |
+
// Navigate to next/previous field
|
| 759 |
+
function navigateToField(direction) {
|
| 760 |
+
const newIndex = currentFieldIndex + direction;
|
| 761 |
+
if (newIndex >= 0 && newIndex < editableFields.length) {
|
| 762 |
+
const nextField = editableFields[newIndex];
|
| 763 |
+
nextField.element.focus();
|
| 764 |
+
nextField.element.select();
|
| 765 |
+
currentFieldIndex = newIndex;
|
| 766 |
+
}
|
| 767 |
}
|
| 768 |
|
| 769 |
+
// Insert a new field after the current one
|
| 770 |
+
function insertNewField(currentFieldData) {
|
| 771 |
+
let newData;
|
| 772 |
+
let insertAfterKey;
|
| 773 |
|
| 774 |
+
if (currentFieldData.parentKey === 'root') {
|
| 775 |
+
newData = {};
|
| 776 |
+
insertAfterKey = currentFieldData.key;
|
| 777 |
+
|
| 778 |
+
// Get all keys and find insertion point
|
| 779 |
+
const keys = Object.keys(jsonData);
|
| 780 |
+
const insertIndex = keys.indexOf(insertAfterKey) + 1;
|
| 781 |
+
|
| 782 |
+
// Create new object with inserted field
|
| 783 |
+
let index = 0;
|
| 784 |
+
keys.forEach(key => {
|
| 785 |
+
newData[key] = jsonData[key];
|
| 786 |
+
if (key === insertAfterKey) {
|
| 787 |
+
newData['newField'] = '';
|
| 788 |
}
|
| 789 |
+
index++;
|
| 790 |
});
|
| 791 |
+
|
| 792 |
+
// If insertAfterKey was the last key, append at end
|
| 793 |
+
if (insertIndex >= keys.length) {
|
| 794 |
+
newData['newField'] = '';
|
| 795 |
+
}
|
| 796 |
+
|
| 797 |
jsonData = newData;
|
| 798 |
} else {
|
| 799 |
+
// Handle nested objects
|
| 800 |
+
const parent = findElementByKey(jsonData, currentFieldData.parentKey);
|
| 801 |
+
if (parent && typeof parent === 'object' && !Array.isArray(parent)) {
|
| 802 |
+
const keys = Object.keys(parent);
|
| 803 |
+
insertAfterKey = currentFieldData.key;
|
| 804 |
+
|
| 805 |
+
let index = 0;
|
| 806 |
+
keys.forEach(key => {
|
| 807 |
+
if (key === insertAfterKey && index < keys.length) {
|
| 808 |
+
// Insert after this key
|
| 809 |
+
const tempData = {};
|
| 810 |
+
let inserted = false;
|
| 811 |
+
keys.forEach(k => {
|
| 812 |
+
tempData[k] = parent[k];
|
| 813 |
+
if (!inserted && k === insertAfterKey) {
|
| 814 |
+
tempData['newField'] = '';
|
| 815 |
+
inserted = true;
|
| 816 |
+
}
|
| 817 |
+
});
|
| 818 |
+
// Clear parent
|
| 819 |
+
Object.keys(parent).forEach(k => delete parent[k]);
|
| 820 |
+
Object.assign(parent, tempData);
|
| 821 |
}
|
| 822 |
+
index++;
|
| 823 |
});
|
|
|
|
|
|
|
| 824 |
}
|
| 825 |
}
|
| 826 |
|
| 827 |
+
// Re-render and focus on new field
|
| 828 |
+
renderEditor();
|
| 829 |
+
updateOutput();
|
| 830 |
+
saveToHistory();
|
| 831 |
+
|
| 832 |
+
// Find and focus the new field
|
| 833 |
+
setTimeout(() => {
|
| 834 |
+
const newField = editableFields.find(f => f.key === 'newField');
|
| 835 |
+
if (newField) {
|
| 836 |
+
newField.element.focus();
|
| 837 |
+
newField.element.select();
|
| 838 |
+
}
|
| 839 |
+
}, 100);
|
| 840 |
+
|
| 841 |
+
showSavedIndicator();
|
| 842 |
+
}
|
| 843 |
+
|
| 844 |
+
// Update a key from input
|
| 845 |
+
function updateKey(input) {
|
| 846 |
+
const newKey = input.value.trim();
|
| 847 |
+
const oldKey = input.dataset.originalKey || input.value;
|
| 848 |
+
const parentKey = input.dataset.parentKey;
|
| 849 |
+
|
| 850 |
+
// Only save if value actually changed
|
| 851 |
+
if (newKey === oldKey) return;
|
| 852 |
+
|
| 853 |
+
input.dataset.originalKey = newKey;
|
| 854 |
+
|
| 855 |
+
if (parentKey === 'root') {
|
| 856 |
+
// Root level
|
| 857 |
+
const value = jsonData[oldKey];
|
| 858 |
+
delete jsonData[oldKey];
|
| 859 |
+
jsonData[newKey] = value;
|
| 860 |
+
} else {
|
| 861 |
+
// Nested level
|
| 862 |
+
const parent = findElementByKey(jsonData, parentKey);
|
| 863 |
+
if (parent && typeof parent === 'object' && !Array.isArray(parent)) {
|
| 864 |
+
const value = parent[oldKey];
|
| 865 |
+
delete parent[oldKey];
|
| 866 |
+
parent[newKey] = value;
|
| 867 |
+
}
|
| 868 |
+
}
|
| 869 |
+
|
| 870 |
+
// Update tracking
|
| 871 |
+
const fieldData = editableFields.find(f => f.element === input);
|
| 872 |
+
if (fieldData) {
|
| 873 |
+
fieldData.key = newKey;
|
| 874 |
+
}
|
| 875 |
+
|
| 876 |
updateOutput();
|
| 877 |
saveToHistory();
|
| 878 |
+
showSavedIndicator();
|
| 879 |
}
|
| 880 |
|
| 881 |
+
// Update a value from input
|
| 882 |
+
function updateValue(input) {
|
| 883 |
+
const rawValue = input.value.trim();
|
| 884 |
+
const parsedValue = parseValue(rawValue);
|
| 885 |
+
const key = input.dataset.key;
|
| 886 |
+
const parentKey = input.dataset.parentKey;
|
| 887 |
+
|
| 888 |
+
// Only save if value actually changed
|
| 889 |
+
const currentValue = getNestedValue(jsonData, parentKey, key);
|
| 890 |
+
if (JSON.stringify(currentValue) === JSON.stringify(parsedValue)) return;
|
| 891 |
|
| 892 |
if (parentKey === 'root') {
|
| 893 |
+
jsonData[key] = parsedValue;
|
| 894 |
} else {
|
| 895 |
const parent = findElementByKey(jsonData, parentKey);
|
| 896 |
if (parent && typeof parent === 'object') {
|
| 897 |
+
parent[key] = parsedValue;
|
| 898 |
}
|
| 899 |
}
|
| 900 |
|
| 901 |
updateOutput();
|
| 902 |
saveToHistory();
|
| 903 |
+
showSavedIndicator();
|
| 904 |
+
}
|
| 905 |
+
|
| 906 |
+
// Get nested value
|
| 907 |
+
function getNestedValue(obj, parentKey, key) {
|
| 908 |
+
if (parentKey === 'root') {
|
| 909 |
+
return obj[key];
|
| 910 |
+
}
|
| 911 |
+
const parent = findElementByKey(obj, parentKey);
|
| 912 |
+
if (parent && typeof parent === 'object') {
|
| 913 |
+
return parent[key];
|
| 914 |
+
}
|
| 915 |
+
return undefined;
|
| 916 |
+
}
|
| 917 |
+
|
| 918 |
+
// Parse a value from string to appropriate type
|
| 919 |
+
function parseValue(value) {
|
| 920 |
+
if (value === 'true') return true;
|
| 921 |
+
if (value === 'false') return false;
|
| 922 |
+
if (value === 'null') return null;
|
| 923 |
+
if (!isNaN(value) && value.trim() !== '' && !isNaN(Number(value))) {
|
| 924 |
+
return Number(value);
|
| 925 |
+
}
|
| 926 |
+
return value;
|
| 927 |
+
}
|
| 928 |
+
|
| 929 |
+
// Show saved indicator
|
| 930 |
+
function showSavedIndicator() {
|
| 931 |
+
savedIndicator.classList.add('show');
|
| 932 |
+
setTimeout(() => {
|
| 933 |
+
savedIndicator.classList.remove('show');
|
| 934 |
+
}, 1500);
|
| 935 |
}
|
| 936 |
|
| 937 |
// Find an element by key in nested structure
|
|
|
|
| 948 |
return null;
|
| 949 |
}
|
| 950 |
|
| 951 |
+
// Update JSON output with colored brackets
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 952 |
function updateOutput() {
|
| 953 |
if (isApplyingChanges) return;
|
| 954 |
try {
|
| 955 |
+
const jsonString = JSON.stringify(jsonData, null, 2);
|
| 956 |
+
jsonOutput.value = jsonString;
|
| 957 |
jsonOutput.classList.remove('error-highlight');
|
| 958 |
+
jsonOutput.style.borderColor = '#10b981';
|
| 959 |
validationStatus.innerHTML = '<i class="fas fa-check-circle text-green-500"></i><span class="text-green-600">Valid JSON</span>';
|
| 960 |
} catch (e) {
|
| 961 |
jsonOutput.value = 'Invalid JSON structure';
|
|
|
|
| 1005 |
}
|
| 1006 |
});
|
| 1007 |
|
| 1008 |
+
// Auto-apply on blur (when clicking away)
|
| 1009 |
+
jsonOutput.addEventListener('blur', () => {
|
| 1010 |
+
const autoSaveStatus = document.getElementById('autoSaveStatus');
|
| 1011 |
+
if (applyCodeChanges()) {
|
| 1012 |
+
autoSaveStatus.classList.remove('hidden');
|
| 1013 |
+
setTimeout(() => {
|
| 1014 |
+
autoSaveStatus.classList.add('hidden');
|
| 1015 |
+
}, 2000);
|
| 1016 |
+
}
|
| 1017 |
+
}, true);
|
| 1018 |
+
|
| 1019 |
// Show notification
|
| 1020 |
function showNotification(message) {
|
| 1021 |
const notificationContent = notification.querySelector('p');
|
|
|
|
| 1034 |
history = history.slice(0, historyIndex + 1);
|
| 1035 |
}
|
| 1036 |
|
| 1037 |
+
// Limit history size
|
| 1038 |
+
if (history.length >= MAX_HISTORY) {
|
| 1039 |
+
history.shift();
|
| 1040 |
+
historyIndex--;
|
| 1041 |
+
}
|
| 1042 |
+
|
| 1043 |
+
// Don't save if it's the same as current
|
| 1044 |
+
if (history.length > 0 && JSON.stringify(history[historyIndex]) === JSON.stringify(jsonData)) {
|
| 1045 |
+
return;
|
| 1046 |
+
}
|
| 1047 |
+
|
| 1048 |
history.push(JSON.parse(JSON.stringify(jsonData)));
|
| 1049 |
historyIndex = history.length - 1;
|
| 1050 |
}
|
|
|
|
| 1229 |
|
| 1230 |
// Instructions button
|
| 1231 |
document.getElementById('instructionsBtn').addEventListener('click', () => {
|
| 1232 |
+
showNotification('Visual: Single-click to edit • Tab/Shift+Tab to navigate • Shift+Enter for new field • Auto-save enabled');
|
| 1233 |
});
|
| 1234 |
|
| 1235 |
// Sample JSON button
|
style.css
CHANGED
|
@@ -26,3 +26,104 @@ p {
|
|
| 26 |
.card p:last-child {
|
| 27 |
margin-bottom: 0;
|
| 28 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
.card p:last-child {
|
| 27 |
margin-bottom: 0;
|
| 28 |
}
|
| 29 |
+
|
| 30 |
+
/* LCARS-style layer colors */
|
| 31 |
+
.lcars-layer-0 { --layer-color: #ef4444; }
|
| 32 |
+
.lcars-layer-1 { --layer-color: #f97316; }
|
| 33 |
+
.lcars-layer-2 { --layer-color: #eab308; }
|
| 34 |
+
.lcars-layer-3 { --layer-color: #22c55e; }
|
| 35 |
+
.lcars-layer-4 { --layer-color: #06b6d4; }
|
| 36 |
+
.lcars-layer-5 { --layer-color: #3b82f6; }
|
| 37 |
+
.lcars-layer-6 { --layer-color: #8b5cf6; }
|
| 38 |
+
.lcars-layer-7 { --layer-color: #ec4899; }
|
| 39 |
+
|
| 40 |
+
/* Layer indicator line */
|
| 41 |
+
.layer-indicator {
|
| 42 |
+
position: absolute;
|
| 43 |
+
left: 0;
|
| 44 |
+
top: 0;
|
| 45 |
+
bottom: 0;
|
| 46 |
+
width: 4px;
|
| 47 |
+
background: var(--layer-color);
|
| 48 |
+
border-radius: 0 4px 4px 0;
|
| 49 |
+
box-shadow: 2px 0 8px var(--layer-color);
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
/* LCARS horizontal connector */
|
| 53 |
+
.lcars-connector {
|
| 54 |
+
position: absolute;
|
| 55 |
+
left: 4px;
|
| 56 |
+
top: 50%;
|
| 57 |
+
height: 2px;
|
| 58 |
+
width: calc(100% - 4px);
|
| 59 |
+
background: var(--layer-color);
|
| 60 |
+
opacity: 0.4;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
/* Active row - bold text instead of background */
|
| 64 |
+
.json-item.active {
|
| 65 |
+
background-color: transparent;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.json-item.active .json-key,
|
| 69 |
+
.json-item.active .json-value {
|
| 70 |
+
font-weight: bold;
|
| 71 |
+
text-shadow: 0 0 10px var(--layer-color);
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
/* Saved indicator */
|
| 75 |
+
.saved-indicator {
|
| 76 |
+
position: fixed;
|
| 77 |
+
bottom: 20px;
|
| 78 |
+
right: 20px;
|
| 79 |
+
background: linear-gradient(135deg, #10b981, #059669);
|
| 80 |
+
color: white;
|
| 81 |
+
padding: 12px 24px;
|
| 82 |
+
border-radius: 8px;
|
| 83 |
+
font-weight: bold;
|
| 84 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
| 85 |
+
transform: translateY(100px);
|
| 86 |
+
transition: transform 0.3s ease;
|
| 87 |
+
z-index: 1000;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
.saved-indicator.show {
|
| 91 |
+
transform: translateY(0);
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
/* Editable fields styling */
|
| 95 |
+
.editable-field {
|
| 96 |
+
border: none;
|
| 97 |
+
background: transparent;
|
| 98 |
+
padding: 2px 4px;
|
| 99 |
+
border-radius: 4px;
|
| 100 |
+
font-family: inherit;
|
| 101 |
+
font-size: inherit;
|
| 102 |
+
width: auto;
|
| 103 |
+
min-width: 30px;
|
| 104 |
+
transition: all 0.2s ease;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
.editable-field:focus {
|
| 108 |
+
outline: none;
|
| 109 |
+
background: rgba(255,255,255,0.15);
|
| 110 |
+
box-shadow: inset 0 0 0 2px rgba(255,255,255,0.3);
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
.editable-field.key-input {
|
| 114 |
+
color: #93c5fd;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
.editable-field.value-input {
|
| 118 |
+
color: #6ee7b7;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
/* New field indicator */
|
| 122 |
+
.new-field-highlight {
|
| 123 |
+
animation: pulse-green 1s ease-in-out infinite;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
@keyframes pulse-green {
|
| 127 |
+
0%, 100% { background-color: rgba(34, 197, 94, 0.2); }
|
| 128 |
+
50% { background-color: rgba(34, 197, 94, 0.4); }
|
| 129 |
+
}
|