RobinsAIWorld commited on
Commit
b9fe4f1
·
verified ·
1 Parent(s): 2da0329

🐳 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
Files changed (1) hide show
  1. 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-6xl 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 drag-and-drop and intuitive keyboard navigation</p>
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; // Increased undo history
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)); // Deep copy
325
  renderEditor();
326
  updateOutput();
327
  saveToHistory();
@@ -330,12 +301,12 @@
330
  // Render the JSON editor
331
  function renderEditor() {
332
  jsonEditor.innerHTML = '';
333
- editableFields = []; // Clear tracked fields
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
- // Single-click to edit
425
- valueSpan.style.cursor = 'pointer';
426
- valueSpan.addEventListener('click', (e) => {
427
  e.stopPropagation();
428
- makeEditable(valueSpan, key, parentKey, 'value');
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
- content.appendChild(valueSpan);
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 a span editable
530
- function makeEditable(span, key, parentKey, type) {
531
- const currentValue = type === 'value'
532
- ? (typeof jsonData === 'object' && parentKey === 'root' ? jsonData[key] : getNestedValue(jsonData, parentKey, key))
533
- : key;
 
 
 
 
534
 
535
  const input = document.createElement('input');
536
  input.type = 'text';
537
- input.className = `editable-field ${type === 'key' ? 'key-input' : 'value-input'}`;
538
- input.value = type === 'value' && typeof currentValue === 'string' ? currentValue : String(currentValue);
539
 
540
- // Track this field for tab navigation
541
- const fieldData = { element: input, key, parentKey, type };
542
- const fieldIndex = editableFields.findIndex(f => f.element === span);
543
- if (fieldIndex !== -1) {
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
- // Handle blur (save on exit)
568
- input.addEventListener('blur', () => {
569
  const newValue = input.value.trim();
 
570
 
571
  if (type === 'key') {
572
- // Update key
573
- if (newValue && newValue !== key) {
574
- if (parentKey === 'root') {
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 (parentKey === 'root') {
591
- jsonData[key] = parsedValue;
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
- // Replace span with input
607
- span.parentNode.replaceChild(input, span);
608
- input.focus();
609
- input.select();
 
 
 
 
 
 
 
 
 
 
 
610
  }
611
 
612
- // Navigate to next/previous field
613
- function navigateToField(direction) {
614
- const newIndex = currentFieldIndex + direction;
615
- if (newIndex >= 0 && newIndex < editableFields.length) {
616
- const field = editableFields[newIndex];
617
- currentFieldIndex = newIndex;
618
-
619
- // Click on the corresponding span to make it editable
620
- const allSpans = document.querySelectorAll('.json-key, .json-value');
621
- allSpans.forEach(span => {
622
- if ((span.dataset.key === field.key && span.dataset.type === field.type) ||
623
- (field.type === 'key' && span.classList.contains('json-key') && span.textContent.includes(field.key)) ||
624
- (field.type === 'value' && span.classList.contains('json-value'))) {
625
- span.click();
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 with colored brackets
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 and sync of code editor
807
  jsonOutput.addEventListener('input', () => {
808
  try {
809
- const parsed = JSON.parse(jsonOutput.value);
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
- // Event listeners for copy, cut, paste
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);