RobinsAIWorld commited on
Commit
3024053
·
verified ·
1 Parent(s): 58d7ea5

🐳 10/02 - 14:47 - I should be able to star ta new json file by clcking anywhere on the left, and if it's an emptey file, will immediately pre-fille the opening { and open a key's editing textbox for

Browse files
Files changed (2) hide show
  1. index.html +12 -11
  2. script.js +137 -33
index.html CHANGED
@@ -22,15 +22,15 @@
22
  }
23
  </script>
24
  </head>
25
- <body class="bg-slate-900 min-h-screen p-1">
26
- <div class="w-full">
27
- <header class="mb-2 text-center py-1">
28
  <h1 class="text-2xl font-bold text-slate-100">Visual JSON Editor</h1>
29
  <p class="text-slate-400 text-sm">The way JSON should <i class="italic font-semibold">ALWAYS</i> been editored!</p>
30
  </header>
31
 
32
  <!-- Menu Bar -->
33
- <div class="menu-bar flex flex-wrap py-1 px-2">
34
  <div class="dropdown">
35
  <div class="menu-item px-2 py-1">File</div>
36
  <div class="dropdown-content">
@@ -66,7 +66,7 @@
66
  </div>
67
  </div>
68
 
69
- <div class="bg-slate-800 rounded-lg overflow-hidden mb-2 border border-slate-700">
70
  <div class="p-2 bg-slate-700 border-b border-slate-600 flex flex-wrap items-center justify-between gap-2">
71
  <div class="flex flex-wrap gap-1">
72
  <button id="newBtn2" class="btn bg-primary hover:bg-secondary text-white px-3 py-1.5 rounded flex items-center gap-1 text-sm">
@@ -99,12 +99,12 @@
99
  <div class="flex flex-col lg:flex-row gap-0.5">
100
  <!-- Left Panel - Visual JSON Editor -->
101
  <div class="w-full lg:w-1/2">
102
- <div class="bg-slate-700 h-full flex flex-col">
103
  <div class="flex justify-between items-center px-2 py-1 border-b border-slate-600">
104
  <h2 class="text-sm font-semibold text-slate-200">Visual Editor</h2>
105
- <span class="text-xs text-slate-400">Click to edit • Tab/Shift+Tab nav • Shift+Enter new</span>
106
  </div>
107
- <div class="relative json-editor border-none overflow-auto flex-1" style="height: calc(100vh - 180px);">
108
  <!-- Left line numbers -->
109
  <div id="lineNumbersLeft" class="absolute left-0 top-0 bottom-0 w-8 bg-slate-800 text-slate-500 text-xs font-mono text-right pr-2 select-none overflow-hidden z-10 pt-1 border-r border-slate-700"></div>
110
  <!-- Right line numbers -->
@@ -119,7 +119,7 @@
119
 
120
  <!-- Right Panel - JSON Code Editor -->
121
  <div class="w-full lg:w-1/2">
122
- <div class="bg-gray-50 h-full flex flex-col">
123
  <div class="flex justify-between items-center px-2 py-1 border-b border-gray-300 bg-gray-100">
124
  <h2 class="text-sm font-semibold text-gray-700">Code Editor</h2>
125
  <div class="flex gap-1 items-center">
@@ -129,7 +129,7 @@
129
  </button>
130
  </div>
131
  </div>
132
- <textarea id="jsonOutput" class="w-full flex-1 font-mono text-sm p-1 border-none bg-white focus:outline-none resize-none" style="height: calc(100vh - 180px);"></textarea>
133
  <div class="px-2 py-1 border-t border-gray-300 flex flex-wrap gap-1 justify-between items-center bg-gray-100">
134
  <div class="flex gap-1">
135
  <button id="copyBtn" class="btn bg-gray-200 hover:bg-gray-300 px-2 py-1 rounded text-xs">
@@ -147,9 +147,10 @@
147
  </div>
148
  </div>
149
  </div>
 
150
  </div>
151
 
152
- <div id="notification" class="notification fixed bottom-4 right-4 bg-white shadow-lg rounded-lg p-4 border-l-4 border-green-500 max-w-md">
153
  <div class="flex items-start">
154
  <i class="fas fa-check-circle text-green-500 text-xl mt-0.5 mr-3"></i>
155
  <div>
 
22
  }
23
  </script>
24
  </head>
25
+ <body class="bg-slate-900 h-screen overflow-hidden p-0">
26
+ <div class="w-full h-screen flex flex-col">
27
+ <header class="flex-shrink-0 mb-1 text-center py-1">
28
  <h1 class="text-2xl font-bold text-slate-100">Visual JSON Editor</h1>
29
  <p class="text-slate-400 text-sm">The way JSON should <i class="italic font-semibold">ALWAYS</i> been editored!</p>
30
  </header>
31
 
32
  <!-- Menu Bar -->
33
+ <div class="menu-bar flex flex-wrap py-1 px-2 flex-shrink-0">
34
  <div class="dropdown">
35
  <div class="menu-item px-2 py-1">File</div>
36
  <div class="dropdown-content">
 
66
  </div>
67
  </div>
68
 
69
+ <div class="bg-slate-800 rounded-lg overflow-hidden mb-1 border border-slate-700 flex-1 flex flex-col overflow-hidden">
70
  <div class="p-2 bg-slate-700 border-b border-slate-600 flex flex-wrap items-center justify-between gap-2">
71
  <div class="flex flex-wrap gap-1">
72
  <button id="newBtn2" class="btn bg-primary hover:bg-secondary text-white px-3 py-1.5 rounded flex items-center gap-1 text-sm">
 
99
  <div class="flex flex-col lg:flex-row gap-0.5">
100
  <!-- Left Panel - Visual JSON Editor -->
101
  <div class="w-full lg:w-1/2">
102
+ <div class="bg-slate-700 h-full flex flex-col" style="height: calc(100vh - 120px);">
103
  <div class="flex justify-between items-center px-2 py-1 border-b border-slate-600">
104
  <h2 class="text-sm font-semibold text-slate-200">Visual Editor</h2>
105
+ <span class="text-xs text-slate-400">Click to edit • Tab nav • Ctrl+Tab indent</span>
106
  </div>
107
+ <div class="relative json-editor border-none overflow-auto flex-1" style="min-height: 100%;">
108
  <!-- Left line numbers -->
109
  <div id="lineNumbersLeft" class="absolute left-0 top-0 bottom-0 w-8 bg-slate-800 text-slate-500 text-xs font-mono text-right pr-2 select-none overflow-hidden z-10 pt-1 border-r border-slate-700"></div>
110
  <!-- Right line numbers -->
 
119
 
120
  <!-- Right Panel - JSON Code Editor -->
121
  <div class="w-full lg:w-1/2">
122
+ <div class="bg-gray-50 h-full flex flex-col" style="height: calc(100vh - 120px);">
123
  <div class="flex justify-between items-center px-2 py-1 border-b border-gray-300 bg-gray-100">
124
  <h2 class="text-sm font-semibold text-gray-700">Code Editor</h2>
125
  <div class="flex gap-1 items-center">
 
129
  </button>
130
  </div>
131
  </div>
132
+ <textarea id="jsonOutput" class="w-full flex-1 font-mono text-sm p-1 border-none bg-white focus:outline-none resize-none" style="min-height: 100%;"></textarea>
133
  <div class="px-2 py-1 border-t border-gray-300 flex flex-wrap gap-1 justify-between items-center bg-gray-100">
134
  <div class="flex gap-1">
135
  <button id="copyBtn" class="btn bg-gray-200 hover:bg-gray-300 px-2 py-1 rounded text-xs">
 
147
  </div>
148
  </div>
149
  </div>
150
+ >
151
  </div>
152
 
153
+ <div id="notification" class="notification fixed bottom-4 right-4 bg-white shadow-lg rounded-lg p-4 border-l-4 border-green-500 max-w-md z-50">
154
  <div class="flex items-start">
155
  <i class="fas fa-check-circle text-green-500 text-xl mt-0.5 mr-3"></i>
156
  <div>
script.js CHANGED
@@ -78,6 +78,9 @@ document.addEventListener('DOMContentLoaded', function() {
78
  // Initialize with sample data
79
  loadJSON(sampleJSON);
80
 
 
 
 
81
  // Create hidden file input for opening files
82
  const fileInput = document.createElement('input');
83
  fileInput.type = 'file';
@@ -252,9 +255,12 @@ document.addEventListener('DOMContentLoaded', function() {
252
  // Load JSON data into the editor
253
  function loadJSON(data) {
254
  jsonData = JSON.parse(JSON.stringify(data));
 
 
 
 
255
  renderEditor();
256
  updateOutput();
257
- saveToHistory();
258
  }
259
 
260
  // Render the JSON editor
@@ -645,11 +651,22 @@ document.addEventListener('DOMContentLoaded', function() {
645
  input.addEventListener('keydown', (e) => {
646
  if (e.key === 'Enter') {
647
  e.preventDefault();
648
- input.blur();
 
 
 
649
  } else if (e.key === 'Tab') {
650
  e.preventDefault();
651
- const direction = e.shiftKey ? -1 : 1;
652
- navigateFields(direction);
 
 
 
 
 
 
 
 
653
  } else if (e.shiftKey && e.key === 'Enter') {
654
  e.preventDefault();
655
  saveEdit();
@@ -663,7 +680,7 @@ document.addEventListener('DOMContentLoaded', function() {
663
  }
664
 
665
  // Navigate to next/previous editable field
666
- function navigateFields(direction) {
667
  // Find all editable spans
668
  const keys = jsonEditor.querySelectorAll('.json-key');
669
  const values = jsonEditor.querySelectorAll('.json-value');
@@ -682,6 +699,25 @@ document.addEventListener('DOMContentLoaded', function() {
682
  }
683
 
684
  let nextIndex = currentIndex + direction;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
685
  if (nextIndex < 0) nextIndex = allFields.length - 1;
686
  if (nextIndex >= allFields.length) nextIndex = 0;
687
 
@@ -690,6 +726,63 @@ document.addEventListener('DOMContentLoaded', function() {
690
  }
691
  }
692
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
693
  // Get nested value
694
  function getValue(parentKey, key) {
695
  if (parentKey === 'root') {
@@ -752,32 +845,22 @@ document.addEventListener('DOMContentLoaded', function() {
752
  // Mark this as a new field for tracking
753
  newKey = newKey + '_new';
754
 
755
- if (parentKey === 'root') {
756
- const keys = Object.keys(jsonData);
757
- const index = keys.indexOf(key);
758
-
759
- if (index === -1 || index === keys.length - 1) {
760
- // Append at the end
761
- jsonData[newKey] = '';
762
- } else {
763
- // Insert after current - need to reconstruct
764
- const newData = {};
765
- keys.forEach((k, i) => {
766
- newData[k] = jsonData[k];
767
- if (i === index) {
768
- newData[newKey] = '';
769
- }
770
- });
771
- Object.keys(jsonData).forEach(k => delete jsonData[k]);
772
- Object.assign(jsonData, newData);
773
  }
774
- } else {
775
- const parent = findElementByKey(jsonData, parentKey);
776
- if (parent && typeof parent === 'object' && !Array.isArray(parent)) {
 
 
777
  const keys = Object.keys(parent);
778
  const index = keys.indexOf(key);
779
 
780
- if (index === -1 || index === keys.length - 1) {
781
  // Append at the end
782
  parent[newKey] = '';
783
  } else {
@@ -792,9 +875,6 @@ document.addEventListener('DOMContentLoaded', function() {
792
  Object.keys(parent).forEach(k => delete parent[k]);
793
  Object.assign(parent, newData);
794
  }
795
- } else if (parent && Array.isArray(parent)) {
796
- // For arrays, push new empty string
797
- parent.push('');
798
  }
799
  }
800
 
@@ -802,6 +882,17 @@ document.addEventListener('DOMContentLoaded', function() {
802
  updateOutput();
803
  saveToHistory();
804
  showSavedIndicator();
 
 
 
 
 
 
 
 
 
 
 
805
  }
806
 
807
  // Parse a value from string to appropriate type
@@ -1076,8 +1167,19 @@ document.addEventListener('DOMContentLoaded', function() {
1076
  jsonEditor.addEventListener('click', (e) => {
1077
  // Only if clicking directly on json-editor (not on an item)
1078
  if (e.target === jsonEditor) {
1079
- // Insert at root level
1080
- insertNewField({ key: null, parentKey: 'root' });
 
 
 
 
 
 
 
 
 
 
 
1081
  }
1082
  });
1083
 
@@ -1119,7 +1221,9 @@ document.addEventListener('DOMContentLoaded', function() {
1119
  historyIndex--;
1120
  }
1121
 
1122
- if (history.length > 0 && JSON.stringify(history[historyIndex]) === JSON.stringify(jsonData)) {
 
 
1123
  return;
1124
  }
1125
 
 
78
  // Initialize with sample data
79
  loadJSON(sampleJSON);
80
 
81
+ // Set initial history state
82
+ saveToHistory();
83
+
84
  // Create hidden file input for opening files
85
  const fileInput = document.createElement('input');
86
  fileInput.type = 'file';
 
255
  // Load JSON data into the editor
256
  function loadJSON(data) {
257
  jsonData = JSON.parse(JSON.stringify(data));
258
+ // Initialize empty object if data is null/undefined
259
+ if (!jsonData || (typeof jsonData === 'object' && Object.keys(jsonData).length === 0)) {
260
+ jsonData = {};
261
+ }
262
  renderEditor();
263
  updateOutput();
 
264
  }
265
 
266
  // Render the JSON editor
 
651
  input.addEventListener('keydown', (e) => {
652
  if (e.key === 'Enter') {
653
  e.preventDefault();
654
+ // Save current edit
655
+ saveEdit();
656
+ // Move to next field
657
+ navigateFields(1, true); // true = auto-add comma/newline
658
  } else if (e.key === 'Tab') {
659
  e.preventDefault();
660
+ // Check for Ctrl+Tab or Ctrl+Shift+Tab for indentation
661
+ if (e.ctrlKey) {
662
+ // Change indentation depth
663
+ const direction = e.shiftKey ? -1 : 1;
664
+ changeIndentation(key, parentKey, direction);
665
+ } else {
666
+ // Normal tab navigation
667
+ const direction = e.shiftKey ? -1 : 1;
668
+ navigateFields(direction, false);
669
+ }
670
  } else if (e.shiftKey && e.key === 'Enter') {
671
  e.preventDefault();
672
  saveEdit();
 
680
  }
681
 
682
  // Navigate to next/previous editable field
683
+ function navigateFields(direction, autoAddField = false) {
684
  // Find all editable spans
685
  const keys = jsonEditor.querySelectorAll('.json-key');
686
  const values = jsonEditor.querySelectorAll('.json-value');
 
699
  }
700
 
701
  let nextIndex = currentIndex + direction;
702
+
703
+ // If we're on the last field and going forward, add a new field first
704
+ if (autoAddField && nextIndex >= allFields.length) {
705
+ // Find current field's info
706
+ if (currentIndex >= 0) {
707
+ const currentField = allFields[currentIndex];
708
+ const parentElement = currentField.element.closest('.json-item');
709
+ if (parentElement) {
710
+ insertNewField({
711
+ key: parentElement.dataset.key,
712
+ parentKey: parentElement.dataset.parent
713
+ });
714
+ // Re-render and navigate to the new field
715
+ setTimeout(() => navigateFields(direction, false), 10);
716
+ return;
717
+ }
718
+ }
719
+ }
720
+
721
  if (nextIndex < 0) nextIndex = allFields.length - 1;
722
  if (nextIndex >= allFields.length) nextIndex = 0;
723
 
 
726
  }
727
  }
728
 
729
+ // Change indentation depth of a field
730
+ function changeIndentation(key, parentKey, direction) {
731
+ // direction: 1 = indent deeper, -1 = un-indent
732
+
733
+ const currentValue = getValue(parentKey, key);
734
+ if (currentValue === undefined) return;
735
+
736
+ if (parentKey === 'root') {
737
+ // Can't un-indent from root
738
+ if (direction === -1) {
739
+ showNotification('Already at root level', true);
740
+ return;
741
+ }
742
+ // Need to create a wrapper object to indent
743
+ const wrapperKey = 'wrapper';
744
+ const newData = { [wrapperKey]: { [key]: currentValue } };
745
+ delete jsonData[key];
746
+ jsonData[wrapperKey] = { [key]: currentValue };
747
+ } else {
748
+ const parent = findElementByKey(jsonData, parentKey);
749
+ const grandParent = findElementByKey(jsonData, parentKey === 'root' ? 'root' :
750
+ parent?.__parentKey || 'root');
751
+
752
+ if (direction === 1) {
753
+ // Indent deeper - create wrapper in parent
754
+ if (parent && typeof parent === 'object' && !Array.isArray(parent)) {
755
+ const wrapperKey = 'wrapper';
756
+ const oldObj = { [key]: currentValue };
757
+ delete parent[key];
758
+ parent[wrapperKey] = oldObj;
759
+ }
760
+ } else {
761
+ // Un-indent - move up one level
762
+ if (parent) {
763
+ const parentOfParent = findElementByKey(jsonData, parentKey === 'root' ? 'root' :
764
+ parent?.__parentKey || 'root');
765
+
766
+ if (parentOfParent && parentOfParent !== jsonData) {
767
+ // Move to grandparent
768
+ delete parent[key];
769
+ parentOfParent[key] = currentValue;
770
+
771
+ // Clean up empty parent
772
+ if (Object.keys(parent).length === 0) {
773
+ removeFromParent(parentKey === 'root' ? 'root' : parent?.__parentKey || 'root', parentKey);
774
+ }
775
+ }
776
+ }
777
+ }
778
+ }
779
+
780
+ renderEditor();
781
+ updateOutput();
782
+ saveToHistory();
783
+ showSavedIndicator();
784
+ }
785
+
786
  // Get nested value
787
  function getValue(parentKey, key) {
788
  if (parentKey === 'root') {
 
845
  // Mark this as a new field for tracking
846
  newKey = newKey + '_new';
847
 
848
+ const parent = parentKey === 'root' ? jsonData : findElementByKey(jsonData, parentKey);
849
+
850
+ if (!parent) {
851
+ // If parent doesn't exist, initialize jsonData as empty object
852
+ if (typeof jsonData !== 'object' || jsonData === null) {
853
+ jsonData = {};
 
 
 
 
 
 
 
 
 
 
 
 
854
  }
855
+ jsonData[newKey] = '';
856
+ } else if (typeof parent === 'object' && parent !== null) {
857
+ if (Array.isArray(parent)) {
858
+ parent.push('');
859
+ } else {
860
  const keys = Object.keys(parent);
861
  const index = keys.indexOf(key);
862
 
863
+ if (index === -1 || index === keys.length - 1 || key === null) {
864
  // Append at the end
865
  parent[newKey] = '';
866
  } else {
 
875
  Object.keys(parent).forEach(k => delete parent[k]);
876
  Object.assign(parent, newData);
877
  }
 
 
 
878
  }
879
  }
880
 
 
882
  updateOutput();
883
  saveToHistory();
884
  showSavedIndicator();
885
+
886
+ // Auto-focus the new field after rendering
887
+ setTimeout(() => {
888
+ const newItems = jsonEditor.querySelectorAll('.json-item');
889
+ newItems.forEach(item => {
890
+ if (item.dataset.key === newKey) {
891
+ const keySpan = item.querySelector('.json-key');
892
+ if (keySpan) keySpan.click();
893
+ }
894
+ });
895
+ }, 10);
896
  }
897
 
898
  // Parse a value from string to appropriate type
 
1167
  jsonEditor.addEventListener('click', (e) => {
1168
  // Only if clicking directly on json-editor (not on an item)
1169
  if (e.target === jsonEditor) {
1170
+ // Check if editor is empty (only contains closing bracket)
1171
+ const isEmpty = jsonEditor.innerHTML.trim() === '' ||
1172
+ (jsonEditor.children.length === 1 &&
1173
+ jsonEditor.children[0].textContent.includes('}'));
1174
+
1175
+ if (isEmpty) {
1176
+ // Initialize new JSON structure
1177
+ jsonData = {};
1178
+ insertNewField({ key: null, parentKey: 'root' });
1179
+ } else {
1180
+ // Insert at root level
1181
+ insertNewField({ key: null, parentKey: 'root' });
1182
+ }
1183
  }
1184
  });
1185
 
 
1221
  historyIndex--;
1222
  }
1223
 
1224
+ // Fix: Check if history has content before comparing
1225
+ if (history.length > 0 && historyIndex >= 0 &&
1226
+ JSON.stringify(history[historyIndex]) === JSON.stringify(jsonData)) {
1227
  return;
1228
  }
1229