Spaces:
Running
Running
🐳 10/02 - 14:18 - 1. Can you also please add subtle but clear (ie. use the same colour, different shade of it) line numbers on the left hand side AND far right hand side, and X,Y character coordinate
Browse files- index.html +14 -2
- script.js +147 -9
- style.css +31 -0
index.html
CHANGED
|
@@ -104,8 +104,15 @@
|
|
| 104 |
<h2 class="text-lg font-semibold text-slate-200">Visual Editor</h2>
|
| 105 |
<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>
|
| 106 |
</div>
|
| 107 |
-
<div
|
| 108 |
-
<!--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
</div>
|
| 110 |
</div>
|
| 111 |
</div>
|
|
@@ -178,6 +185,11 @@
|
|
| 178 |
</div>
|
| 179 |
</div>
|
| 180 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
</div>
|
| 182 |
|
| 183 |
<script src="script.js"></script>
|
|
|
|
| 104 |
<h2 class="text-lg font-semibold text-slate-200">Visual Editor</h2>
|
| 105 |
<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>
|
| 106 |
</div>
|
| 107 |
+
<div class="relative json-editor border rounded-lg min-h-[600px] max-h-[700px] overflow-auto">
|
| 108 |
+
<!-- Left line numbers -->
|
| 109 |
+
<div id="lineNumbersLeft" class="absolute left-0 top-0 bottom-0 w-10 bg-slate-800 text-slate-500 text-xs font-mono text-right pr-2 select-none overflow-hidden z-10 pt-2"></div>
|
| 110 |
+
<!-- Right line numbers -->
|
| 111 |
+
<div id="lineNumbersRight" class="absolute right-0 top-0 bottom-0 w-10 bg-slate-800 text-slate-500 text-xs font-mono text-left pl-2 select-none overflow-hidden z-10 pt-2"></div>
|
| 112 |
+
<!-- JSON content -->
|
| 113 |
+
<div id="jsonEditor" class="pl-10 pr-10 py-2 font-mono">
|
| 114 |
+
<!-- JSON content will be rendered here -->
|
| 115 |
+
</div>
|
| 116 |
</div>
|
| 117 |
</div>
|
| 118 |
</div>
|
|
|
|
| 185 |
</div>
|
| 186 |
</div>
|
| 187 |
</div>
|
| 188 |
+
|
| 189 |
+
<!-- Coordinates display - fixed position -->
|
| 190 |
+
<div id="coordinates" class="fixed top-20 right-4 bg-slate-800 text-slate-300 px-3 py-2 rounded-lg text-xs font-mono shadow-lg z-50 opacity-80 hover:opacity-100 transition-opacity">
|
| 191 |
+
<span id="coordLine">Line: 0</span> | <span id="coordChar">Char: 0</span> | <span id="coordDepth">Depth: 0</span>
|
| 192 |
+
</div>
|
| 193 |
</div>
|
| 194 |
|
| 195 |
<script src="script.js"></script>
|
script.js
CHANGED
|
@@ -28,6 +28,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
| 28 |
const downloadBtn = document.getElementById('downloadBtn');
|
| 29 |
const applyCodeBtn = document.getElementById('applyCodeBtn');
|
| 30 |
const validationStatus = document.getElementById('validationStatus');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
// Create saved indicator
|
| 33 |
const savedIndicator = document.createElement('div');
|
|
@@ -54,6 +59,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
| 54 |
let editableFields = [];
|
| 55 |
let currentFieldIndex = -1;
|
| 56 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
// Drag and drop state
|
| 58 |
let draggedElement = null;
|
| 59 |
let draggedData = null;
|
|
@@ -251,9 +260,34 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
| 251 |
// Render the JSON editor
|
| 252 |
function renderEditor() {
|
| 253 |
jsonEditor.innerHTML = '';
|
|
|
|
|
|
|
| 254 |
editableFields = [];
|
| 255 |
currentFieldIndex = -1;
|
|
|
|
|
|
|
| 256 |
renderElement(jsonEditor, jsonData, 0, 'root');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
}
|
| 258 |
|
| 259 |
// Render a single JSON element (COMPLETE VERSION)
|
|
@@ -270,6 +304,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
| 270 |
wrapper.dataset.depth = depth;
|
| 271 |
wrapper.dataset.layer = Math.min(depth, 7);
|
| 272 |
wrapper.dataset.type = Array.isArray(data) ? 'array' : 'object';
|
|
|
|
|
|
|
|
|
|
| 273 |
|
| 274 |
// Make primitive items draggable (not opening/closing brackets)
|
| 275 |
if (key !== 'root' && key !== 'closing') {
|
|
@@ -405,6 +442,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
| 405 |
wrapper.dataset.depth = depth;
|
| 406 |
wrapper.dataset.layer = Math.min(depth, 7);
|
| 407 |
wrapper.dataset.type = 'primitive';
|
|
|
|
|
|
|
|
|
|
| 408 |
|
| 409 |
// Make draggable
|
| 410 |
if (key !== 'root') {
|
|
@@ -741,6 +781,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
| 741 |
}
|
| 742 |
}
|
| 743 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 744 |
// Apply changes from code editor to visual editor
|
| 745 |
function applyCodeChanges(showMsg = true) {
|
| 746 |
if (!jsonOutput.value.trim()) return false;
|
|
@@ -968,17 +1014,36 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
| 968 |
|
| 969 |
// Handle the drop operation
|
| 970 |
function handleDrop(draggedData, dropTarget, dropPosition) {
|
| 971 |
-
const { key: draggedKey, parentKey: draggedParentKey } = draggedData;
|
| 972 |
const dropKey = dropTarget.dataset.key;
|
| 973 |
const dropParentKey = dropTarget.dataset.parent;
|
| 974 |
const dropDepth = parseInt(dropTarget.dataset.depth);
|
| 975 |
const dropType = dropTarget.dataset.type;
|
| 976 |
|
| 977 |
-
//
|
| 978 |
-
|
|
|
|
|
|
|
| 979 |
|
| 980 |
-
//
|
| 981 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 982 |
|
| 983 |
// Determine new parent and position
|
| 984 |
let newParentKey = dropParentKey;
|
|
@@ -986,17 +1051,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
| 986 |
let insertKey = dropKey;
|
| 987 |
|
| 988 |
if (dropPosition === 'above') {
|
| 989 |
-
// Insert before the drop target
|
| 990 |
insertPosition = 'before';
|
| 991 |
newParentKey = dropParentKey;
|
| 992 |
insertKey = dropKey;
|
| 993 |
} else if (dropPosition === 'below') {
|
| 994 |
-
// Insert after the drop target
|
| 995 |
insertPosition = 'after';
|
| 996 |
newParentKey = dropParentKey;
|
| 997 |
insertKey = dropKey;
|
| 998 |
} else if (dropPosition === 'inside') {
|
| 999 |
-
// Insert inside the drop target (as a child)
|
| 1000 |
newParentKey = dropKey;
|
| 1001 |
insertPosition = 'append';
|
| 1002 |
insertKey = null;
|
|
@@ -1009,8 +1071,84 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
| 1009 |
}
|
| 1010 |
}
|
| 1011 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1012 |
// Insert at new location
|
| 1013 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1014 |
|
| 1015 |
// Re-render and sync
|
| 1016 |
renderEditor();
|
|
|
|
| 28 |
const downloadBtn = document.getElementById('downloadBtn');
|
| 29 |
const applyCodeBtn = document.getElementById('applyCodeBtn');
|
| 30 |
const validationStatus = document.getElementById('validationStatus');
|
| 31 |
+
const lineNumbersLeft = document.getElementById('lineNumbersLeft');
|
| 32 |
+
const lineNumbersRight = document.getElementById('lineNumbersRight');
|
| 33 |
+
const coordLine = document.getElementById('coordLine');
|
| 34 |
+
const coordChar = document.getElementById('coordChar');
|
| 35 |
+
const coordDepth = document.getElementById('coordDepth');
|
| 36 |
|
| 37 |
// Create saved indicator
|
| 38 |
const savedIndicator = document.createElement('div');
|
|
|
|
| 59 |
let editableFields = [];
|
| 60 |
let currentFieldIndex = -1;
|
| 61 |
|
| 62 |
+
// Line number tracking
|
| 63 |
+
let currentLine = 0;
|
| 64 |
+
let lineElements = [];
|
| 65 |
+
|
| 66 |
// Drag and drop state
|
| 67 |
let draggedElement = null;
|
| 68 |
let draggedData = null;
|
|
|
|
| 260 |
// Render the JSON editor
|
| 261 |
function renderEditor() {
|
| 262 |
jsonEditor.innerHTML = '';
|
| 263 |
+
lineNumbersLeft.innerHTML = '';
|
| 264 |
+
lineNumbersRight.innerHTML = '';
|
| 265 |
editableFields = [];
|
| 266 |
currentFieldIndex = -1;
|
| 267 |
+
currentLine = 0;
|
| 268 |
+
lineElements = [];
|
| 269 |
renderElement(jsonEditor, jsonData, 0, 'root');
|
| 270 |
+
updateLineNumbers();
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
// Update line numbers on both sides
|
| 274 |
+
function updateLineNumbers() {
|
| 275 |
+
const items = jsonEditor.querySelectorAll('.json-item');
|
| 276 |
+
const totalLines = items.length;
|
| 277 |
+
|
| 278 |
+
lineNumbersLeft.innerHTML = '';
|
| 279 |
+
lineNumbersRight.innerHTML = '';
|
| 280 |
+
|
| 281 |
+
for (let i = 0; i < totalLines; i++) {
|
| 282 |
+
const lineNum = document.createElement('div');
|
| 283 |
+
lineNum.className = 'h-5 leading-5';
|
| 284 |
+
lineNum.textContent = (i + 1).toString();
|
| 285 |
+
|
| 286 |
+
const lineNumRight = lineNum.cloneNode(true);
|
| 287 |
+
|
| 288 |
+
lineNumbersLeft.appendChild(lineNum);
|
| 289 |
+
lineNumbersRight.appendChild(lineNumRight);
|
| 290 |
+
}
|
| 291 |
}
|
| 292 |
|
| 293 |
// Render a single JSON element (COMPLETE VERSION)
|
|
|
|
| 304 |
wrapper.dataset.depth = depth;
|
| 305 |
wrapper.dataset.layer = Math.min(depth, 7);
|
| 306 |
wrapper.dataset.type = Array.isArray(data) ? 'array' : 'object';
|
| 307 |
+
wrapper.dataset.line = currentLine;
|
| 308 |
+
lineElements.push(wrapper);
|
| 309 |
+
currentLine++;
|
| 310 |
|
| 311 |
// Make primitive items draggable (not opening/closing brackets)
|
| 312 |
if (key !== 'root' && key !== 'closing') {
|
|
|
|
| 442 |
wrapper.dataset.depth = depth;
|
| 443 |
wrapper.dataset.layer = Math.min(depth, 7);
|
| 444 |
wrapper.dataset.type = 'primitive';
|
| 445 |
+
wrapper.dataset.line = currentLine;
|
| 446 |
+
lineElements.push(wrapper);
|
| 447 |
+
currentLine++;
|
| 448 |
|
| 449 |
// Make draggable
|
| 450 |
if (key !== 'root') {
|
|
|
|
| 781 |
}
|
| 782 |
}
|
| 783 |
|
| 784 |
+
// Sync line numbers scroll with editor
|
| 785 |
+
jsonEditor.parentElement.addEventListener('scroll', function() {
|
| 786 |
+
lineNumbersLeft.scrollTop = this.scrollTop;
|
| 787 |
+
lineNumbersRight.scrollTop = this.scrollTop;
|
| 788 |
+
});
|
| 789 |
+
|
| 790 |
// Apply changes from code editor to visual editor
|
| 791 |
function applyCodeChanges(showMsg = true) {
|
| 792 |
if (!jsonOutput.value.trim()) return false;
|
|
|
|
| 1014 |
|
| 1015 |
// Handle the drop operation
|
| 1016 |
function handleDrop(draggedData, dropTarget, dropPosition) {
|
| 1017 |
+
const { key: draggedKey, parentKey: draggedParentKey, depth: draggedDepth } = draggedData;
|
| 1018 |
const dropKey = dropTarget.dataset.key;
|
| 1019 |
const dropParentKey = dropTarget.dataset.parent;
|
| 1020 |
const dropDepth = parseInt(dropTarget.dataset.depth);
|
| 1021 |
const dropType = dropTarget.dataset.type;
|
| 1022 |
|
| 1023 |
+
// Don't allow dropping on self
|
| 1024 |
+
if (draggedKey === dropKey && draggedParentKey === dropParentKey) {
|
| 1025 |
+
return;
|
| 1026 |
+
}
|
| 1027 |
|
| 1028 |
+
// Get the value being moved BEFORE removing
|
| 1029 |
+
const draggedParent = findElementByKey(jsonData, draggedParentKey);
|
| 1030 |
+
let movedValue = null;
|
| 1031 |
+
|
| 1032 |
+
if (draggedParent && typeof draggedParent === 'object') {
|
| 1033 |
+
if (Array.isArray(draggedParent)) {
|
| 1034 |
+
const index = parseInt(draggedKey);
|
| 1035 |
+
if (!isNaN(index) && index >= 0 && index < draggedParent.length) {
|
| 1036 |
+
movedValue = draggedParent[index];
|
| 1037 |
+
}
|
| 1038 |
+
} else {
|
| 1039 |
+
movedValue = draggedParent[draggedKey];
|
| 1040 |
+
}
|
| 1041 |
+
}
|
| 1042 |
+
|
| 1043 |
+
if (movedValue === null) {
|
| 1044 |
+
showNotification('Could not move item - value not found', true);
|
| 1045 |
+
return;
|
| 1046 |
+
}
|
| 1047 |
|
| 1048 |
// Determine new parent and position
|
| 1049 |
let newParentKey = dropParentKey;
|
|
|
|
| 1051 |
let insertKey = dropKey;
|
| 1052 |
|
| 1053 |
if (dropPosition === 'above') {
|
|
|
|
| 1054 |
insertPosition = 'before';
|
| 1055 |
newParentKey = dropParentKey;
|
| 1056 |
insertKey = dropKey;
|
| 1057 |
} else if (dropPosition === 'below') {
|
|
|
|
| 1058 |
insertPosition = 'after';
|
| 1059 |
newParentKey = dropParentKey;
|
| 1060 |
insertKey = dropKey;
|
| 1061 |
} else if (dropPosition === 'inside') {
|
|
|
|
| 1062 |
newParentKey = dropKey;
|
| 1063 |
insertPosition = 'append';
|
| 1064 |
insertKey = null;
|
|
|
|
| 1071 |
}
|
| 1072 |
}
|
| 1073 |
|
| 1074 |
+
// Get the new parent object
|
| 1075 |
+
const newParent = findElementByKey(jsonData, newParentKey);
|
| 1076 |
+
|
| 1077 |
+
if (!newParent || typeof newParent !== 'object') {
|
| 1078 |
+
showNotification('Cannot move here - invalid parent', true);
|
| 1079 |
+
return;
|
| 1080 |
+
}
|
| 1081 |
+
|
| 1082 |
+
// Remove from original location
|
| 1083 |
+
if (draggedParent === 'root') {
|
| 1084 |
+
if (Array.isArray(jsonData)) {
|
| 1085 |
+
const index = parseInt(draggedKey);
|
| 1086 |
+
if (!isNaN(index) && index >= 0 && index < jsonData.length) {
|
| 1087 |
+
jsonData.splice(index, 1);
|
| 1088 |
+
}
|
| 1089 |
+
} else {
|
| 1090 |
+
delete jsonData[draggedKey];
|
| 1091 |
+
}
|
| 1092 |
+
} else {
|
| 1093 |
+
if (draggedParent && Array.isArray(draggedParent)) {
|
| 1094 |
+
const index = parseInt(draggedKey);
|
| 1095 |
+
if (!isNaN(index) && index >= 0 && index < draggedParent.length) {
|
| 1096 |
+
draggedParent.splice(index, 1);
|
| 1097 |
+
}
|
| 1098 |
+
} else if (draggedParent) {
|
| 1099 |
+
delete draggedParent[draggedKey];
|
| 1100 |
+
}
|
| 1101 |
+
}
|
| 1102 |
+
|
| 1103 |
// Insert at new location
|
| 1104 |
+
if (Array.isArray(newParent)) {
|
| 1105 |
+
if (insertPosition === 'append') {
|
| 1106 |
+
newParent.push(movedValue);
|
| 1107 |
+
} else if (insertPosition === 'before' || insertPosition === 'after') {
|
| 1108 |
+
const index = parseInt(insertKey);
|
| 1109 |
+
if (!isNaN(index) && index >= 0) {
|
| 1110 |
+
const insertIndex = insertPosition === 'after' ? index + 1 : index;
|
| 1111 |
+
newParent.splice(insertIndex, 0, movedValue);
|
| 1112 |
+
} else {
|
| 1113 |
+
newParent.push(movedValue);
|
| 1114 |
+
}
|
| 1115 |
+
}
|
| 1116 |
+
} else {
|
| 1117 |
+
// Object - need a key for the new value
|
| 1118 |
+
let newKey = 'newField';
|
| 1119 |
+
let counter = 1;
|
| 1120 |
+
while (newParent[newKey] !== undefined) {
|
| 1121 |
+
newKey = `newField${counter++}`;
|
| 1122 |
+
}
|
| 1123 |
+
|
| 1124 |
+
if (insertPosition === 'append') {
|
| 1125 |
+
newParent[newKey] = movedValue;
|
| 1126 |
+
} else if (insertPosition === 'before' || insertPosition === 'after') {
|
| 1127 |
+
const keys = Object.keys(newParent);
|
| 1128 |
+
const index = keys.indexOf(insertKey);
|
| 1129 |
+
|
| 1130 |
+
if (index === -1) {
|
| 1131 |
+
// Key not found, just append
|
| 1132 |
+
newParent[newKey] = movedValue;
|
| 1133 |
+
} else {
|
| 1134 |
+
// Rebuild object with new item in correct position
|
| 1135 |
+
const newObj = {};
|
| 1136 |
+
keys.forEach((k, i) => {
|
| 1137 |
+
if (insertPosition === 'before' && i === index) {
|
| 1138 |
+
newObj[newKey] = movedValue;
|
| 1139 |
+
}
|
| 1140 |
+
newObj[k] = newParent[k];
|
| 1141 |
+
if (insertPosition === 'after' && i === index) {
|
| 1142 |
+
newObj[newKey] = movedValue;
|
| 1143 |
+
}
|
| 1144 |
+
});
|
| 1145 |
+
|
| 1146 |
+
// Clear old object and assign new one
|
| 1147 |
+
Object.keys(newParent).forEach(k => delete newParent[k]);
|
| 1148 |
+
Object.assign(newParent, newObj);
|
| 1149 |
+
}
|
| 1150 |
+
}
|
| 1151 |
+
}
|
| 1152 |
|
| 1153 |
// Re-render and sync
|
| 1154 |
renderEditor();
|
style.css
CHANGED
|
@@ -394,4 +394,35 @@ p {
|
|
| 394 |
.drag-handle:active {
|
| 395 |
cursor: grabbing;
|
| 396 |
color: rgba(255, 255, 255, 0.6);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 397 |
}
|
|
|
|
| 394 |
.drag-handle:active {
|
| 395 |
cursor: grabbing;
|
| 396 |
color: rgba(255, 255, 255, 0.6);
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
/* Line numbers */
|
| 400 |
+
#lineNumbersLeft,
|
| 401 |
+
#lineNumbersRight {
|
| 402 |
+
border-top: 4px solid #334155;
|
| 403 |
+
border-bottom: 4px solid #334155;
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
#lineNumbersLeft {
|
| 407 |
+
border-right: 1px solid #475569;
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
#lineNumbersRight {
|
| 411 |
+
border-left: 1px solid #475569;
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
/* Coordinates display */
|
| 415 |
+
#coordinates {
|
| 416 |
+
border: 1px solid #475569;
|
| 417 |
+
min-width: 150px;
|
| 418 |
+
text-align: center;
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
#coordinates span {
|
| 422 |
+
color: #94a3b8;
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
#coordinates span::before {
|
| 426 |
+
color: #60a5fa;
|
| 427 |
+
margin-right: 2px;
|
| 428 |
}
|