RobinsAIWorld commited on
Commit
527614f
·
verified ·
1 Parent(s): 559934a

🐳 10/02 - 14:33 - 1. '...ALWAYS been editored!' I invented the word 'editored' in the subtitle--not 'edited'2. Scrolling down scrlls the line numbers correctly up, but then introduces numberless line

Browse files
Files changed (3) hide show
  1. index.html +21 -1
  2. script.js +156 -3
  3. style.css +39 -0
index.html CHANGED
@@ -26,7 +26,7 @@
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">The way JSON should <i class="italic font-semibold">ALWAYS</i> been edited!</p>
30
  </header>
31
 
32
  <!-- Menu Bar -->
@@ -190,6 +190,26 @@
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>
 
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">The way JSON should <i class="italic font-semibold">ALWAYS</i> been editored!</p>
30
  </header>
31
 
32
  <!-- Menu Bar -->
 
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
+
194
+ <!-- Context menu -->
195
+ <div id="contextMenu" class="context-menu">
196
+ <div class="context-menu-item" id="ctxInsertAfter">
197
+ <i class="fas fa-plus"></i> Insert Line After
198
+ </div>
199
+ <div class="context-menu-item" id="ctxInsertBefore">
200
+ <i class="fas fa-arrow-up"></i> Insert Line Before
201
+ </div>
202
+ <div class="context-menu-divider"></div>
203
+ <div class="context-menu-item" id="ctxCopyKey">
204
+ <i class="fas fa-copy"></i> Copy Key
205
+ </div>
206
+ <div class="context-menu-item" id="ctxCopyValue">
207
+ <i class="fas fa-copy"></i> Copy Value
208
+ </div>
209
+ <div class="context-menu-item" id="ctxDelete">
210
+ <i class="fas fa-trash"></i> Delete
211
+ </div>
212
+ </div>
213
  </div>
214
 
215
  <script src="script.js"></script>
script.js CHANGED
@@ -272,22 +272,31 @@ document.addEventListener('DOMContentLoaded', function() {
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)
@@ -541,16 +550,37 @@ document.addEventListener('DOMContentLoaded', function() {
541
  displayValue = displayValue.slice(1, -1);
542
  }
543
 
 
 
 
 
544
  const input = document.createElement('input');
545
  input.type = 'text';
546
  input.value = displayValue !== undefined ? displayValue : '';
547
  input.className = `editable-field ${type}-input`;
548
 
 
 
 
 
549
  // Replace span with input
550
  span.parentNode.replaceChild(input, span);
551
  input.focus();
552
  input.select();
553
 
 
 
 
 
 
 
 
 
 
 
 
 
 
554
  // Save on blur or Enter
555
  const saveEdit = () => {
556
  const newValue = input.value.trim();
@@ -883,6 +913,129 @@ document.addEventListener('DOMContentLoaded', function() {
883
  });
884
  });
885
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
886
  // Show notification (success by default)
887
  function showNotification(message, isError = false) {
888
  const notificationContent = notification.querySelector('p');
 
272
 
273
  // Update line numbers on both sides
274
  function updateLineNumbers() {
275
+ // Get all json-item elements (including closing brackets)
276
  const items = jsonEditor.querySelectorAll('.json-item');
277
  const totalLines = items.length;
278
 
279
  lineNumbersLeft.innerHTML = '';
280
  lineNumbersRight.innerHTML = '';
281
 
282
+ // Create a document fragment for better performance
283
+ const fragmentLeft = document.createDocumentFragment();
284
+ const fragmentRight = document.createDocumentFragment();
285
+
286
  for (let i = 0; i < totalLines; i++) {
287
  const lineNum = document.createElement('div');
288
+ lineNum.className = 'h-5 leading-5 flex items-center justify-end pr-2';
289
  lineNum.textContent = (i + 1).toString();
290
 
291
  const lineNumRight = lineNum.cloneNode(true);
292
+ lineNumRight.className = 'h-5 leading-5 flex items-center justify-start pl-2';
293
 
294
+ fragmentLeft.appendChild(lineNum);
295
+ fragmentRight.appendChild(lineNumRight);
296
  }
297
+
298
+ lineNumbersLeft.appendChild(fragmentLeft);
299
+ lineNumbersRight.appendChild(fragmentRight);
300
  }
301
 
302
  // Render a single JSON element (COMPLETE VERSION)
 
550
  displayValue = displayValue.slice(1, -1);
551
  }
552
 
553
+ // Store original span text content to calculate width
554
+ const originalText = span.textContent;
555
+ const originalWidth = span.offsetWidth;
556
+
557
  const input = document.createElement('input');
558
  input.type = 'text';
559
  input.value = displayValue !== undefined ? displayValue : '';
560
  input.className = `editable-field ${type}-input`;
561
 
562
+ // Calculate appropriate width - minimum 50px or original width + padding
563
+ const calculatedWidth = Math.max(50, originalWidth + 20);
564
+ input.style.width = `${calculatedWidth}px`;
565
+
566
  // Replace span with input
567
  span.parentNode.replaceChild(input, span);
568
  input.focus();
569
  input.select();
570
 
571
+ // Adjust width dynamically as user types
572
+ input.addEventListener('input', function() {
573
+ const tempSpan = document.createElement('span');
574
+ tempSpan.style.font = getComputedStyle(input).font;
575
+ tempSpan.style.visibility = 'hidden';
576
+ tempSpan.style.position = 'absolute';
577
+ tempSpan.textContent = input.value;
578
+ document.body.appendChild(tempSpan);
579
+ const newWidth = Math.max(50, tempSpan.offsetWidth + 30);
580
+ input.style.width = `${newWidth}px`;
581
+ document.body.removeChild(tempSpan);
582
+ });
583
+
584
  // Save on blur or Enter
585
  const saveEdit = () => {
586
  const newValue = input.value.trim();
 
913
  });
914
  });
915
 
916
+ // ==================== CONTEXT MENU ====================
917
+
918
+ const contextMenu = document.getElementById('contextMenu');
919
+ let contextMenuTarget = null;
920
+ let contextMenuKey = null;
921
+ let contextMenuParentKey = null;
922
+
923
+ // Show context menu on right-click
924
+ jsonEditor.addEventListener('contextmenu', (e) => {
925
+ e.preventDefault();
926
+
927
+ // Find the closest json-item
928
+ const item = e.target.closest('.json-item');
929
+ if (!item) return;
930
+
931
+ // Don't show on closing brackets
932
+ if (item.dataset.key === 'closing') return;
933
+
934
+ contextMenuTarget = item;
935
+ contextMenuKey = item.dataset.key;
936
+ contextMenuParentKey = item.dataset.parent;
937
+
938
+ // Position the menu
939
+ const x = Math.min(e.clientX, window.innerWidth - 200);
940
+ const y = Math.min(e.clientY, window.innerHeight - 200);
941
+ contextMenu.style.left = `${x}px`;
942
+ contextMenu.style.top = `${y}px`;
943
+ contextMenu.style.display = 'block';
944
+ });
945
+
946
+ // Hide context menu on click elsewhere
947
+ document.addEventListener('click', (e) => {
948
+ if (!contextMenu.contains(e.target)) {
949
+ contextMenu.style.display = 'none';
950
+ }
951
+ });
952
+
953
+ // Context menu actions
954
+ document.getElementById('ctxInsertAfter').addEventListener('click', () => {
955
+ insertNewField({ key: contextMenuKey, parentKey: contextMenuParentKey });
956
+ contextMenu.style.display = 'none';
957
+ });
958
+
959
+ document.getElementById('ctxInsertBefore').addEventListener('click', () => {
960
+ // Insert before - need to handle this specially
961
+ const parent = contextMenuParentKey === 'root' ? jsonData : findElementByKey(jsonData, contextMenuParentKey);
962
+ if (parent && typeof parent === 'object') {
963
+ let newKey = 'newField';
964
+ let counter = 1;
965
+ while (parent[newKey] !== undefined) {
966
+ newKey = `newField${counter++}`;
967
+ }
968
+
969
+ if (Array.isArray(parent)) {
970
+ const index = parseInt(contextMenuKey);
971
+ if (!isNaN(index) && index >= 0) {
972
+ parent.splice(index, 0, '');
973
+ }
974
+ } else {
975
+ const keys = Object.keys(parent);
976
+ const index = keys.indexOf(contextMenuKey);
977
+ if (index !== -1) {
978
+ const newData = {};
979
+ keys.forEach((k, i) => {
980
+ if (i === index) {
981
+ newData[newKey] = '';
982
+ }
983
+ newData[k] = parent[k];
984
+ });
985
+ Object.keys(parent).forEach(k => delete parent[k]);
986
+ Object.assign(parent, newData);
987
+ }
988
+ }
989
+
990
+ renderEditor();
991
+ updateOutput();
992
+ saveToHistory();
993
+ showSavedIndicator();
994
+ }
995
+ contextMenu.style.display = 'none';
996
+ });
997
+
998
+ document.getElementById('ctxCopyKey').addEventListener('click', async () => {
999
+ try {
1000
+ await navigator.clipboard.writeText(contextMenuKey);
1001
+ showNotification('Key copied to clipboard!', false);
1002
+ } catch (err) {
1003
+ showNotification('Failed to copy key', true);
1004
+ }
1005
+ contextMenu.style.display = 'none';
1006
+ });
1007
+
1008
+ document.getElementById('ctxCopyValue').addEventListener('click', async () => {
1009
+ const value = getValue(contextMenuParentKey, contextMenuKey);
1010
+ try {
1011
+ await navigator.clipboard.writeText(JSON.stringify(value, null, 2));
1012
+ showNotification('Value copied to clipboard!', false);
1013
+ } catch (err) {
1014
+ showNotification('Failed to copy value', true);
1015
+ }
1016
+ contextMenu.style.display = 'none';
1017
+ });
1018
+
1019
+ document.getElementById('ctxDelete').addEventListener('click', () => {
1020
+ if (confirm(`Delete "${contextMenuKey}"?`)) {
1021
+ removeFromParent(contextMenuParentKey, contextMenuKey);
1022
+ renderEditor();
1023
+ updateOutput();
1024
+ saveToHistory();
1025
+ showSavedIndicator();
1026
+ }
1027
+ contextMenu.style.display = 'none';
1028
+ });
1029
+
1030
+ // Click on empty space between items to insert line
1031
+ jsonEditor.addEventListener('click', (e) => {
1032
+ // Only if clicking directly on json-editor (not on an item)
1033
+ if (e.target === jsonEditor) {
1034
+ // Insert at root level
1035
+ insertNewField({ key: null, parentKey: 'root' });
1036
+ }
1037
+ });
1038
+
1039
  // Show notification (success by default)
1040
  function showNotification(message, isError = false) {
1041
  const notificationContent = notification.querySelector('p');
style.css CHANGED
@@ -425,4 +425,43 @@ p {
425
  #coordinates span::before {
426
  color: #60a5fa;
427
  margin-right: 2px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
428
  }
 
425
  #coordinates span::before {
426
  color: #60a5fa;
427
  margin-right: 2px;
428
+ }
429
+
430
+ /* Context menu */
431
+ .context-menu {
432
+ position: fixed;
433
+ background: #334155;
434
+ border: 1px solid #475569;
435
+ border-radius: 8px;
436
+ padding: 4px 0;
437
+ min-width: 180px;
438
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
439
+ z-index: 1000;
440
+ display: none;
441
+ }
442
+
443
+ .context-menu-item {
444
+ padding: 8px 16px;
445
+ color: #cbd5e1;
446
+ cursor: pointer;
447
+ display: flex;
448
+ align-items: center;
449
+ gap: 8px;
450
+ font-size: 14px;
451
+ }
452
+
453
+ .context-menu-item:hover {
454
+ background: #475569;
455
+ }
456
+
457
+ .context-menu-item i {
458
+ width: 16px;
459
+ text-align: center;
460
+ color: #94a3b8;
461
+ }
462
+
463
+ .context-menu-divider {
464
+ height: 1px;
465
+ background: #475569;
466
+ margin: 4px 0;
467
  }