Upload files to "/"
This commit is contained in:
774
jsonedit.html
Normal file
774
jsonedit.html
Normal file
@@ -0,0 +1,774 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
|
||||||
|
<title></title>
|
||||||
|
<meta name="generator" content="LibreOffice 25.8.2.2 (Linux)"/>
|
||||||
|
<meta name="created" content="00:00:00"/>
|
||||||
|
<meta name="changed" content="2025-11-04T10:10:34.197896194"/>
|
||||||
|
<meta name="" content=""/>
|
||||||
|
<style type="text/css">
|
||||||
|
@page { size: 8.5in 11in; margin-left: 0.79in; margin-right: 0.39in; margin-top: 0.39in; margin-bottom: 0.39in }
|
||||||
|
p { margin-bottom: 0.1in; line-height: 115%; background: transparent }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body lang="en-US" link="#000080" vlink="#800000" dir="ltr"><p><!doctype
|
||||||
|
html></p>
|
||||||
|
<p><html lang="en"></p>
|
||||||
|
<p><head></p>
|
||||||
|
<p> <meta charset="utf-8" /></p>
|
||||||
|
<p> <meta name="viewport" content="width=device-width,
|
||||||
|
initial-scale=1" /></p>
|
||||||
|
<p> <title>JSON Editor – Upload • Edit •
|
||||||
|
Download</title></p>
|
||||||
|
<p> <style></p>
|
||||||
|
<p> :root{</p>
|
||||||
|
<p> --bg:#0b0f14; --panel:#121922; --muted:#1a2430;
|
||||||
|
--text:#e6edf3; --sub:#9fb0c3;</p>
|
||||||
|
<p> --accent:#4aa3ff; --accent-2:#22c55e; --warn:#f59e0b;
|
||||||
|
--err:#ef4444; --border:#243241;</p>
|
||||||
|
<p> }</p>
|
||||||
|
<p> *{box-sizing:border-box}</p>
|
||||||
|
<p> body{margin:0; font:14px/1.4 system-ui,Segoe
|
||||||
|
UI,Roboto,Ubuntu,Arial,sans-serif; background:var(--bg);
|
||||||
|
color:var(--text);}
|
||||||
|
</p>
|
||||||
|
<p> header{display:flex; gap:.75rem; align-items:center;
|
||||||
|
padding:12px 14px; border-bottom:1px solid var(--border);
|
||||||
|
background:linear-gradient(180deg,var(--panel),#0f1520);}
|
||||||
|
</p>
|
||||||
|
<p> header h1{font-size:16px; margin:0; font-weight:600;
|
||||||
|
letter-spacing:.2px}</p>
|
||||||
|
<p> header .spacer{flex:1}</p>
|
||||||
|
<p> button,input[type=file],.btn{border:1px solid var(--border);
|
||||||
|
background:var(--muted); color:var(--text); border-radius:10px;
|
||||||
|
padding:8px 12px; cursor:pointer}</p>
|
||||||
|
<p> button:hover,.btn:hover{background:#223046}</p>
|
||||||
|
<p> button.primary{background:var(--accent); color:#06121f;
|
||||||
|
border-color:#2d7cd6; font-weight:600}</p>
|
||||||
|
<p> button.success{background:var(--accent-2); color:#04120a;
|
||||||
|
border-color:#18a24c; font-weight:600}</p>
|
||||||
|
<p> button.warn{background:var(--warn); color:#1d1402;
|
||||||
|
border-color:#b7791f; font-weight:600}</p>
|
||||||
|
<p> button.danger{background:var(--err); color:#1c0303;
|
||||||
|
border-color:#b91c1c; font-weight:600}</p>
|
||||||
|
<p> .wrap{display:grid; grid-template-rows:auto 1fr;
|
||||||
|
height:calc(100dvh - 0px);}
|
||||||
|
</p>
|
||||||
|
<p> .toolbar{display:flex; flex-wrap:wrap; gap:.5rem; padding:10px
|
||||||
|
14px; background:#0f1622; border-bottom:1px solid var(--border)}</p>
|
||||||
|
<p> .toolbar .group{display:flex; gap:.5rem; align-items:center}</p>
|
||||||
|
<p> .filelabel{padding:8px 12px; border:1px dashed var(--border);
|
||||||
|
border-radius:10px; background:var(--panel)}</p>
|
||||||
|
<p> .tabs{display:flex; gap:8px; padding:8px 14px;
|
||||||
|
border-bottom:1px solid var(--border); background:#0e1520}</p>
|
||||||
|
<p> .tab{padding:8px 12px; border-radius:8px; cursor:pointer;
|
||||||
|
color:var(--sub); border:1px solid transparent}</p>
|
||||||
|
<p> .tab.active{color:var(--text); border-color:var(--border);
|
||||||
|
background:var(--muted)}</p>
|
||||||
|
<p> .panes{height:100%;}</p>
|
||||||
|
<p> .pane{display:none; height:calc(100dvh - 180px);} /* adjusted
|
||||||
|
for header+toolbars */</p>
|
||||||
|
<p> .pane.active{display:block}</p>
|
||||||
|
<p> .row{display:grid; grid-template-columns: 1fr 1fr; gap:12px;
|
||||||
|
height:100%}</p>
|
||||||
|
<p> .col{height:100%; overflow:auto; background:var(--panel);
|
||||||
|
border-top:1px solid var(--border)}</p>
|
||||||
|
<p> .col h3{margin:0; padding:10px 12px; border-bottom:1px solid
|
||||||
|
var(--border); font-size:13px; letter-spacing:.25px;
|
||||||
|
color:var(--sub); position:sticky; top:0; background:var(--panel)}</p>
|
||||||
|
<p> textarea#raw{width:100%; height:100%; border:none;
|
||||||
|
outline:none; background:#0b111a; color:var(--text); padding:12px;
|
||||||
|
font:12px/1.5 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||||
|
"Liberation Mono", "Courier New", monospace}</p>
|
||||||
|
<p> .dropzone{border:2px dashed var(--border); margin:12px;
|
||||||
|
padding:20px; border-radius:12px; text-align:center;
|
||||||
|
color:var(--sub)}</p>
|
||||||
|
<p> .dropzone.drag{border-color:var(--accent);
|
||||||
|
color:var(--accent)}</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> /* Tree editor */</p>
|
||||||
|
<p> .tree{padding:8px 10px}</p>
|
||||||
|
<p> .node{display:flex; align-items:center; gap:8px; padding:4px
|
||||||
|
6px; border-left:2px solid transparent}</p>
|
||||||
|
<p> .node:hover{background:#0e1520; border-left-color:#233347}</p>
|
||||||
|
<p> .indent{width:16px}</p>
|
||||||
|
<p> .key{width:220px; max-width:35vw}</p>
|
||||||
|
<p> .val{flex:1}</p>
|
||||||
|
<p> .key input,.val input,.val select{width:100%; padding:6px 8px;
|
||||||
|
border-radius:8px; border:1px solid var(--border);
|
||||||
|
background:#0b111a; color:var(--text); font:12px
|
||||||
|
ui-monospace,Consolas,monospace}</p>
|
||||||
|
<p> .val input.invalid{border-color:var(--err); outline:2px solid
|
||||||
|
rgba(239,68,68,.25)}</p>
|
||||||
|
<p> .pill{font-size:11px; padding:2px 6px; border-radius:999px;
|
||||||
|
border:1px solid var(--border); background:#0b111a; color:var(--sub)}</p>
|
||||||
|
<p> .controls{display:flex; gap:6px}</p>
|
||||||
|
<p> .controls button{padding:4px 8px; font-size:12px;
|
||||||
|
border-radius:8px}</p>
|
||||||
|
<p> .help{color:var(--sub); padding:8px 12px; font-size:12px}</p>
|
||||||
|
<p> .status{padding:6px 10px; font-size:12px}</p>
|
||||||
|
<p> .status.good{color:var(--accent-2)}</p>
|
||||||
|
<p> .status.bad{color:var(--err)}</p>
|
||||||
|
<p> .search{margin-left:auto; display:flex; gap:6px;
|
||||||
|
align-items:center}</p>
|
||||||
|
<p> .search input{padding:6px 8px; border-radius:8px; border:1px
|
||||||
|
solid var(--border); background:#0b111a; color:var(--text)}</p>
|
||||||
|
<p> </style></p>
|
||||||
|
<p></head></p>
|
||||||
|
<p><body></p>
|
||||||
|
<p> <header></p>
|
||||||
|
<p> <h1>JSON Editor</h1></p>
|
||||||
|
<p> <div class="spacer"></div></p>
|
||||||
|
<p> <button id="downloadBtn" class="primary"
|
||||||
|
title="Download edited JSON (Ctrl/Cmd + S)">Download</button></p>
|
||||||
|
<p> </header></p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> <div class="wrap"></p>
|
||||||
|
<p> <div class="toolbar"></p>
|
||||||
|
<p> <div class="group"></p>
|
||||||
|
<p> <label class="filelabel" for="file">Choose
|
||||||
|
JSON…</label></p>
|
||||||
|
<p> <input id="file" type="file"
|
||||||
|
accept="application/json,.json,.txt" hidden></p>
|
||||||
|
<p> <button id="pasteBtn">Paste</button></p>
|
||||||
|
<p> <button id="sampleBtn">Load
|
||||||
|
Sample</button></p>
|
||||||
|
<p> <button id="formatBtn">Format</button></p>
|
||||||
|
<p> <button id="minifyBtn">Minify</button></p>
|
||||||
|
<p> <button id="clearBtn">Clear</button></p>
|
||||||
|
<p> </div></p>
|
||||||
|
<p> <div class="group search"></p>
|
||||||
|
<p> <input id="searchBox" placeholder="Find
|
||||||
|
key or value… (Enter)" /></p>
|
||||||
|
<p> <span id="matchCount" class="pill">0
|
||||||
|
matches</span></p>
|
||||||
|
<p> </div></p>
|
||||||
|
<p> </div></p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> <div class="tabs"></p>
|
||||||
|
<p> <div class="tab active" data-tab="tree">Tree
|
||||||
|
Editor</div></p>
|
||||||
|
<p> <div class="tab" data-tab="raw">Raw
|
||||||
|
JSON</div></p>
|
||||||
|
<p> </div></p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> <div class="panes"></p>
|
||||||
|
<p> <div class="pane active" id="pane-tree"></p>
|
||||||
|
<p> <div class="row"
|
||||||
|
style="grid-template-columns:1fr;"></p>
|
||||||
|
<p> <div class="col"></p>
|
||||||
|
<p> <h3>Tree • <span id="status"
|
||||||
|
class="status">No document</span></h3></p>
|
||||||
|
<p> <div id="drop" class="dropzone">Drop
|
||||||
|
a .json file here</div></p>
|
||||||
|
<p> <div class="help">Tips: Double‑click
|
||||||
|
a key to rename. Change the type dropdown to convert values. Use + to
|
||||||
|
add, − to remove. Arrays use numeric indices. Root can be Object or
|
||||||
|
Array via the type dropdown at the top.</div></p>
|
||||||
|
<p> <div id="tree" class="tree"></div></p>
|
||||||
|
<p> </div></p>
|
||||||
|
<p> </div></p>
|
||||||
|
<p> </div></p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> <div class="pane" id="pane-raw"></p>
|
||||||
|
<p> <div class="row"
|
||||||
|
style="grid-template-columns:1fr;"></p>
|
||||||
|
<p> <div class="col"></p>
|
||||||
|
<p> <h3>Raw JSON <span id="rawStatus"
|
||||||
|
class="status"></span></h3></p>
|
||||||
|
<p> <textarea id="raw" spellcheck="false"
|
||||||
|
placeholder="Paste or type JSON here…"></textarea></p>
|
||||||
|
<p> </div></p>
|
||||||
|
<p> </div></p>
|
||||||
|
<p> </div></p>
|
||||||
|
<p> </div></p>
|
||||||
|
<p> </div></p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p><script></p>
|
||||||
|
<p>(function(){</p>
|
||||||
|
<p> 'use strict';</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> // ---- State ----</p>
|
||||||
|
<p> let data = null; // current JSON object/array</p>
|
||||||
|
<p> let filename = 'edited.json';</p>
|
||||||
|
<p> let lastSearch = '';</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> // ---- DOM ----</p>
|
||||||
|
<p> const $ = sel => document.querySelector(sel);</p>
|
||||||
|
<p> const $$ = sel => Array.from(document.querySelectorAll(sel));</p>
|
||||||
|
<p> const fileInput = $('#file');</p>
|
||||||
|
<p> const downloadBtn = $('#downloadBtn');</p>
|
||||||
|
<p> const pasteBtn = $('#pasteBtn');</p>
|
||||||
|
<p> const formatBtn = $('#formatBtn');</p>
|
||||||
|
<p> const minifyBtn = $('#minifyBtn');</p>
|
||||||
|
<p> const clearBtn = $('#clearBtn');</p>
|
||||||
|
<p> const sampleBtn = $('#sampleBtn');</p>
|
||||||
|
<p> const statusEl = $('#status');</p>
|
||||||
|
<p> const rawStatusEl = $('#rawStatus');</p>
|
||||||
|
<p> const rawTA = $('#raw');</p>
|
||||||
|
<p> const drop = $('#drop');</p>
|
||||||
|
<p> const treeEl = $('#tree');</p>
|
||||||
|
<p> const searchBox = $('#searchBox');</p>
|
||||||
|
<p> const matchCount = $('#matchCount');</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> // ---- Tabs ----</p>
|
||||||
|
<p> $$('.tab').forEach(tab => tab.addEventListener('click', () =>
|
||||||
|
{</p>
|
||||||
|
<p> $$('.tab').forEach(t=>t.classList.remove('active'));</p>
|
||||||
|
<p> tab.classList.add('active');</p>
|
||||||
|
<p> const target = tab.dataset.tab;</p>
|
||||||
|
<p> $$('.pane').forEach(p=>p.classList.remove('active'));</p>
|
||||||
|
<p> $('#pane-'+target).classList.add('active');</p>
|
||||||
|
<p> if (target === 'raw') syncRawFromData();</p>
|
||||||
|
<p> if (target === 'tree') syncDataFromRaw(false);</p>
|
||||||
|
<p> }));</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> // ---- File handling ----</p>
|
||||||
|
<p> fileInput.addEventListener('change', async (e) => {</p>
|
||||||
|
<p> if (!e.target.files.length) return;</p>
|
||||||
|
<p> const f = e.target.files[0];</p>
|
||||||
|
<p> filename = f.name.replace(/\.[^.]+$/, '') + '.json';</p>
|
||||||
|
<p> const text = await f.text();</p>
|
||||||
|
<p> loadJSONText(text);</p>
|
||||||
|
<p> });</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> // Drag & Drop</p>
|
||||||
|
<p> ;['dragenter','dragover'].forEach(ev=>drop.addEventListener(ev,
|
||||||
|
e=>{e.preventDefault(); drop.classList.add('drag');}))</p>
|
||||||
|
<p> ;['dragleave','drop'].forEach(ev=>drop.addEventListener(ev,
|
||||||
|
e=>{e.preventDefault(); drop.classList.remove('drag');}))</p>
|
||||||
|
<p> drop.addEventListener('drop', async (e)=>{</p>
|
||||||
|
<p> const f = e.dataTransfer.files?.[0];</p>
|
||||||
|
<p> if (!f) return;</p>
|
||||||
|
<p> filename = f.name.replace(/\.[^.]+$/, '') + '.json';</p>
|
||||||
|
<p> const text = await f.text();</p>
|
||||||
|
<p> loadJSONText(text);</p>
|
||||||
|
<p> });</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> function loadJSONText(text){</p>
|
||||||
|
<p> try{</p>
|
||||||
|
<p> const parsed = JSON.parse(text);</p>
|
||||||
|
<p> data = parsed;</p>
|
||||||
|
<p> status(`Loaded ${typeof data === 'object' && data ?
|
||||||
|
(Array.isArray(data)?'Array':'Object') : typeof data}`);</p>
|
||||||
|
<p> renderTree();</p>
|
||||||
|
<p> syncRawFromData();</p>
|
||||||
|
<p> }catch(err){</p>
|
||||||
|
<p> alert('Invalid JSON: '+ err.message);</p>
|
||||||
|
<p> }</p>
|
||||||
|
<p> }</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> // ---- Toolbar actions ----</p>
|
||||||
|
<p> pasteBtn.addEventListener('click', async ()=>{</p>
|
||||||
|
<p> try{</p>
|
||||||
|
<p> const text = await navigator.clipboard.readText();</p>
|
||||||
|
<p> if (!text) return alert('Clipboard is empty.');</p>
|
||||||
|
<p> loadJSONText(text);</p>
|
||||||
|
<p> }catch{ alert('Clipboard access denied. Paste into Raw JSON
|
||||||
|
tab instead.'); }</p>
|
||||||
|
<p> });</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> sampleBtn.addEventListener('click', ()=>{</p>
|
||||||
|
<p> const sample = {</p>
|
||||||
|
<p> name:"Sample Document",</p>
|
||||||
|
<p> version:1,</p>
|
||||||
|
<p> tags:["example","json","editor"],</p>
|
||||||
|
<p> features:{</p>
|
||||||
|
<p> upload:true,</p>
|
||||||
|
<p> edit:true,</p>
|
||||||
|
<p> download:true,</p>
|
||||||
|
<p> nested:{ ok:true, count:3 }</p>
|
||||||
|
<p> },</p>
|
||||||
|
<p> items:[ {id:1, title:"First", active:true}, {id:2,
|
||||||
|
title:"Second", active:false} ]</p>
|
||||||
|
<p> };</p>
|
||||||
|
<p> data = sample;</p>
|
||||||
|
<p> filename = 'sample.json';</p>
|
||||||
|
<p> renderTree();</p>
|
||||||
|
<p> syncRawFromData();</p>
|
||||||
|
<p> status('Sample loaded');</p>
|
||||||
|
<p> });</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> formatBtn.addEventListener('click', ()=>{ if
|
||||||
|
(!rawTA.value.trim()) syncRawFromData(); try{ const
|
||||||
|
o=JSON.parse(rawTA.value); rawTA.value = JSON.stringify(o, null, 2);
|
||||||
|
rawStatus('Formatted ✓', true);}catch(e){ rawStatus('Invalid JSON:
|
||||||
|
'+e.message, false);} });</p>
|
||||||
|
<p> minifyBtn.addEventListener('click', ()=>{ if
|
||||||
|
(!rawTA.value.trim()) syncRawFromData(); try{ const
|
||||||
|
o=JSON.parse(rawTA.value); rawTA.value = JSON.stringify(o);
|
||||||
|
rawStatus('Minified ✓', true);}catch(e){ rawStatus('Invalid JSON:
|
||||||
|
'+e.message, false);} });</p>
|
||||||
|
<p> clearBtn.addEventListener('click', ()=>{ data = null;
|
||||||
|
filename='edited.json'; treeEl.innerHTML=''; rawTA.value='';
|
||||||
|
status('No document'); rawStatus(''); matchCount.textContent='0
|
||||||
|
matches'; });</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> // Download</p>
|
||||||
|
<p> downloadBtn.addEventListener('click', ()=>{</p>
|
||||||
|
<p> if ($('#pane-raw').classList.contains('active')){</p>
|
||||||
|
<p> if (!syncDataFromRaw(true)) return; // invalid</p>
|
||||||
|
<p> }</p>
|
||||||
|
<p> const blob = new Blob([JSON.stringify(data, null, 2)],
|
||||||
|
{type:'application/json'});</p>
|
||||||
|
<p> const a = document.createElement('a');</p>
|
||||||
|
<p> a.href = URL.createObjectURL(blob);</p>
|
||||||
|
<p> a.download = filename || 'edited.json';</p>
|
||||||
|
<p> a.click();</p>
|
||||||
|
<p> setTimeout(()=>URL.revokeObjectURL(a.href), 1500);</p>
|
||||||
|
<p> });</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> // Save shortcut</p>
|
||||||
|
<p> window.addEventListener('keydown', (e)=>{</p>
|
||||||
|
<p> if ((e.ctrlKey || e.metaKey) &&
|
||||||
|
e.key.toLowerCase()==='s'){</p>
|
||||||
|
<p> e.preventDefault();</p>
|
||||||
|
<p> downloadBtn.click();</p>
|
||||||
|
<p> }</p>
|
||||||
|
<p> });</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> // ---- Search ----</p>
|
||||||
|
<p> searchBox.addEventListener('keydown', (e)=>{</p>
|
||||||
|
<p> if (e.key==='Enter') doSearch(searchBox.value.trim());</p>
|
||||||
|
<p> });</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> function doSearch(q){</p>
|
||||||
|
<p> lastSearch = q;</p>
|
||||||
|
<p> let matches = 0;</p>
|
||||||
|
<p> $$('.node').forEach(n=>{</p>
|
||||||
|
<p> n.style.outline = 'none';</p>
|
||||||
|
<p> const hay = (n.dataset.key+' '+n.dataset.type+'
|
||||||
|
'+n.dataset.value).toLowerCase();</p>
|
||||||
|
<p> if (q && hay.includes(q.toLowerCase())){ matches++;
|
||||||
|
n.style.outline='2px solid var(--accent)'; }</p>
|
||||||
|
<p> });</p>
|
||||||
|
<p> matchCount.textContent = `${matches}
|
||||||
|
match${matches===1?'':'es'}`;</p>
|
||||||
|
<p> }</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> // ---- Raw JSON <-> data sync ----</p>
|
||||||
|
<p> function syncRawFromData(){</p>
|
||||||
|
<p> if (data==null){ rawTA.value=''; rawStatus(''); return; }</p>
|
||||||
|
<p> rawTA.value = JSON.stringify(data, null, 2);</p>
|
||||||
|
<p> rawStatus('Synced from tree ✓', true);</p>
|
||||||
|
<p> }</p>
|
||||||
|
<p> function syncDataFromRaw(showErrors){</p>
|
||||||
|
<p> if (!rawTA.value.trim()) return true;</p>
|
||||||
|
<p> try{ data = JSON.parse(rawTA.value); renderTree();
|
||||||
|
rawStatus('Applied ✓', true); return true; }</p>
|
||||||
|
<p> catch(e){ if (showErrors) rawStatus('Invalid JSON:
|
||||||
|
'+e.message, false); return false; }</p>
|
||||||
|
<p> }</p>
|
||||||
|
<p> function rawStatus(msg, good){ rawStatusEl.textContent =
|
||||||
|
msg||''; rawStatusEl.className = 'status ' + (good? 'good':'bad'); }</p>
|
||||||
|
<p> function status(msg){ statusEl.textContent = msg;
|
||||||
|
statusEl.className = 'status ' +
|
||||||
|
(/✓|Loaded|Sample/.test(msg)?'good':''); }</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> // ---- Tree editor rendering ----</p>
|
||||||
|
<p> function renderTree(){</p>
|
||||||
|
<p> treeEl.innerHTML='';</p>
|
||||||
|
<p> if (data==null){ return; }</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> // root row header</p>
|
||||||
|
<p> const rootControls = document.createElement('div');</p>
|
||||||
|
<p> rootControls.className='node';</p>
|
||||||
|
<p> rootControls.dataset.key='(root)';</p>
|
||||||
|
<p>
|
||||||
|
rootControls.dataset.type=Array.isArray(data)?'array':'object';</p>
|
||||||
|
<p> rootControls.dataset.value='';</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> // Root type switcher</p>
|
||||||
|
<p> const typeSel =
|
||||||
|
typeSelect(Array.isArray(data)?'array':'object');</p>
|
||||||
|
<p> typeSel.addEventListener('change', ()=>{</p>
|
||||||
|
<p> const t = typeSel.value;</p>
|
||||||
|
<p> data = (t==='array')? [] : {};</p>
|
||||||
|
<p> renderTree();</p>
|
||||||
|
<p> syncRawFromData();</p>
|
||||||
|
<p> });</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> const keyPill = pill('(root)');</p>
|
||||||
|
<p> const controls = btnRow([</p>
|
||||||
|
<p> ['+ key', ()=> addChild([], 'object')],</p>
|
||||||
|
<p> ['+ item', ()=> addChild([], 'array')],</p>
|
||||||
|
<p> ['Clear', ()=>{ data = Array.isArray(data)?[]:{};
|
||||||
|
renderTree(); syncRawFromData(); }]</p>
|
||||||
|
<p> ]);</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> rootControls.append(</p>
|
||||||
|
<p> span('indent'),</p>
|
||||||
|
<p> divEl('key', keyPill),</p>
|
||||||
|
<p> divEl('val', typeSel),</p>
|
||||||
|
<p> divEl('controls', controls)</p>
|
||||||
|
<p> );</p>
|
||||||
|
<p> treeEl.appendChild(rootControls);</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> // body</p>
|
||||||
|
<p> buildNodes(data, []);</p>
|
||||||
|
<p> status(`Editing ${Array.isArray(data)?'Array':'Object'} ✓`);</p>
|
||||||
|
<p> if (lastSearch) doSearch(lastSearch);</p>
|
||||||
|
<p> }</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> function buildNodes(value, path){</p>
|
||||||
|
<p> if (Array.isArray(value)){</p>
|
||||||
|
<p> value.forEach((v, i)=> renderNode(String(i), v,
|
||||||
|
path.concat(i), 'array'));</p>
|
||||||
|
<p> } else if (isObject(value)){</p>
|
||||||
|
<p> Object.keys(value).forEach(k=> renderNode(k, value[k],
|
||||||
|
path.concat(k), 'object'));</p>
|
||||||
|
<p> }</p>
|
||||||
|
<p> }</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> function renderNode(key, value, path, parentType){</p>
|
||||||
|
<p> const node = document.createElement('div');</p>
|
||||||
|
<p> node.className='node';</p>
|
||||||
|
<p> node.dataset.key=key;</p>
|
||||||
|
<p> node.dataset.type=typeOf(value);</p>
|
||||||
|
<p> node.dataset.value=safeStr(value);</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> // indent based on depth</p>
|
||||||
|
<p> const depth = path.length; // root is []</p>
|
||||||
|
<p> const indent = document.createElement('div');</p>
|
||||||
|
<p> indent.className='indent';</p>
|
||||||
|
<p> indent.style.marginLeft = (depth*16)+ 'px';</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> // key editor (objects) or index label (arrays)</p>
|
||||||
|
<p> let keyEl;</p>
|
||||||
|
<p> if (parentType==='object'){</p>
|
||||||
|
<p> keyEl = document.createElement('input');</p>
|
||||||
|
<p> keyEl.value = key;</p>
|
||||||
|
<p> keyEl.title = 'Double‑click to rename key';</p>
|
||||||
|
<p> keyEl.readOnly = true;</p>
|
||||||
|
<p> keyEl.addEventListener('dblclick', ()=>
|
||||||
|
keyEl.readOnly=false);</p>
|
||||||
|
<p> keyEl.addEventListener('blur', ()=> keyEl.readOnly=true);</p>
|
||||||
|
<p> keyEl.addEventListener('change', ()=>{</p>
|
||||||
|
<p> if (!keyEl.value) { keyEl.value = key; return; }</p>
|
||||||
|
<p> renameKey(path.slice(0,-1), key, keyEl.value);</p>
|
||||||
|
<p> });</p>
|
||||||
|
<p> }else{</p>
|
||||||
|
<p> keyEl = pill('['+key+']');</p>
|
||||||
|
<p> }</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> // type selector and value editor</p>
|
||||||
|
<p> const tSel = typeSelect(typeOf(value));</p>
|
||||||
|
<p> const valInput = valueInput(value);</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> tSel.addEventListener('change', ()=>{</p>
|
||||||
|
<p> const newType = tSel.value;</p>
|
||||||
|
<p> const converted = convertValue(valInput.value, newType);</p>
|
||||||
|
<p> setByPath(path, converted);</p>
|
||||||
|
<p> renderTree();</p>
|
||||||
|
<p> syncRawFromData();</p>
|
||||||
|
<p> });</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> valInput.addEventListener('change', ()=>{</p>
|
||||||
|
<p> const t = tSel.value;</p>
|
||||||
|
<p> if (!validateValue(valInput.value, t)){</p>
|
||||||
|
<p> valInput.classList.add('invalid');</p>
|
||||||
|
<p> return;</p>
|
||||||
|
<p> }</p>
|
||||||
|
<p> valInput.classList.remove('invalid');</p>
|
||||||
|
<p> const v = parseValue(valInput.value, t);</p>
|
||||||
|
<p> setByPath(path, v);</p>
|
||||||
|
<p> syncRawFromData();</p>
|
||||||
|
<p> node.dataset.value = safeStr(v);</p>
|
||||||
|
<p> if (lastSearch) doSearch(lastSearch);</p>
|
||||||
|
<p> });</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> // controls</p>
|
||||||
|
<p> const controls = btnRow([</p>
|
||||||
|
<p> ['+', ()=> addChild(path, tSel.value)],</p>
|
||||||
|
<p> ['−', ()=> deletePath(path)],</p>
|
||||||
|
<p> ['⋮', ()=>
|
||||||
|
navigator.clipboard.writeText(JSON.stringify(getByPath(path)))],</p>
|
||||||
|
<p> ]);</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> node.append(</p>
|
||||||
|
<p> indent,</p>
|
||||||
|
<p> divEl('key', keyEl),</p>
|
||||||
|
<p> divEl('val', compose(tSel, valInput)),</p>
|
||||||
|
<p> divEl('controls', controls)</p>
|
||||||
|
<p> );</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> treeEl.appendChild(node);</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> // recurse for containers</p>
|
||||||
|
<p> if (Array.isArray(value) || isObject(value)) buildNodes(value,
|
||||||
|
path);</p>
|
||||||
|
<p> }</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> // ---- Helpers: create common UI bits ----</p>
|
||||||
|
<p> function typeSelect(current){</p>
|
||||||
|
<p> const sel = document.createElement('select');</p>
|
||||||
|
<p>
|
||||||
|
['string','number','boolean','null','object','array'].forEach(t=>{</p>
|
||||||
|
<p> const o = document.createElement('option'); o.value=t;
|
||||||
|
o.textContent=t; sel.appendChild(o);</p>
|
||||||
|
<p> });</p>
|
||||||
|
<p> sel.value = current;</p>
|
||||||
|
<p> return sel;</p>
|
||||||
|
<p> }</p>
|
||||||
|
<p> function valueInput(value){</p>
|
||||||
|
<p> const i = document.createElement('input');</p>
|
||||||
|
<p> if (isObject(value) || Array.isArray(value)){</p>
|
||||||
|
<p> i.value = JSON.stringify(value);</p>
|
||||||
|
<p> i.title = 'Edit as JSON fragment';</p>
|
||||||
|
<p> } else if (value === null){</p>
|
||||||
|
<p> i.value = 'null';</p>
|
||||||
|
<p> } else {</p>
|
||||||
|
<p> i.value = String(value);</p>
|
||||||
|
<p> }</p>
|
||||||
|
<p> return i;</p>
|
||||||
|
<p> }</p>
|
||||||
|
<p> function btnRow(items){</p>
|
||||||
|
<p> const wrap = document.createElement('div');
|
||||||
|
wrap.className='controls';</p>
|
||||||
|
<p> items.forEach(([label, fn])=>{ const
|
||||||
|
b=document.createElement('button'); b.textContent=label;
|
||||||
|
b.addEventListener('click', fn); wrap.appendChild(b); });</p>
|
||||||
|
<p> return wrap;</p>
|
||||||
|
<p> }</p>
|
||||||
|
<p> function pill(text){ const s=document.createElement('span');
|
||||||
|
s.className='pill'; s.textContent=text; return s; }</p>
|
||||||
|
<p> function span(t){ const s=document.createElement('span');
|
||||||
|
s.textContent=t; return s; }</p>
|
||||||
|
<p> function divEl(cls, child){ const
|
||||||
|
d=document.createElement('div'); d.className=cls; if (child)
|
||||||
|
d.appendChild(child); return d; }</p>
|
||||||
|
<p> function compose(a,b){ const w=document.createElement('div');
|
||||||
|
w.style.display='flex'; w.style.gap='6px'; w.append(a,b); return w; }</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> // ---- Data ops ----</p>
|
||||||
|
<p> function isObject(o){ return o && typeof o==='object' &&
|
||||||
|
!Array.isArray(o); }</p>
|
||||||
|
<p> function typeOf(v){ return v===null? 'null' : Array.isArray(v)?
|
||||||
|
'array' : typeof v; }</p>
|
||||||
|
<p> function safeStr(v){ try{return JSON.stringify(v);}catch{return
|
||||||
|
String(v);} }</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> function getByPath(path){</p>
|
||||||
|
<p> return path.reduce((acc, key)=> acc?.[key], data);</p>
|
||||||
|
<p> }</p>
|
||||||
|
<p> function setByPath(path, value){</p>
|
||||||
|
<p> if (path.length===0){ data = value; renderTree(); return; }</p>
|
||||||
|
<p> const parent = getByPath(path.slice(0,-1));</p>
|
||||||
|
<p> const key = path[path.length-1];</p>
|
||||||
|
<p> parent[key] = value;</p>
|
||||||
|
<p> }</p>
|
||||||
|
<p> function deletePath(path){</p>
|
||||||
|
<p> const parent = getByPath(path.slice(0,-1));</p>
|
||||||
|
<p> const key = path[path.length-1];</p>
|
||||||
|
<p> if (Array.isArray(parent)) parent.splice(Number(key),1); else
|
||||||
|
delete parent[key];</p>
|
||||||
|
<p> renderTree();</p>
|
||||||
|
<p> syncRawFromData();</p>
|
||||||
|
<p> }</p>
|
||||||
|
<p> function renameKey(parentPath, oldKey, newKey){</p>
|
||||||
|
<p> const parent = getByPath(parentPath);</p>
|
||||||
|
<p> if (!isObject(parent)) return;</p>
|
||||||
|
<p> if (newKey in parent){ alert('Key already exists at this
|
||||||
|
level.'); renderTree(); return; }</p>
|
||||||
|
<p> parent[newKey] = parent[oldKey];</p>
|
||||||
|
<p> delete parent[oldKey];</p>
|
||||||
|
<p> renderTree();</p>
|
||||||
|
<p> syncRawFromData();</p>
|
||||||
|
<p> }</p>
|
||||||
|
<p> function addChild(path, typeAtPath){</p>
|
||||||
|
<p> // If this node is a primitive, convert it into a container
|
||||||
|
first</p>
|
||||||
|
<p> let target = getByPath(path);</p>
|
||||||
|
<p> const atRoot = path.length===0;</p>
|
||||||
|
<p> if (typeAtPath==='object' && !isObject(target))
|
||||||
|
setByPath(path, {}), target = getByPath(path);</p>
|
||||||
|
<p> if (typeAtPath==='array' && !Array.isArray(target))
|
||||||
|
setByPath(path, []), target = getByPath(path);</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> if (Array.isArray(target)){</p>
|
||||||
|
<p> target.push(null);</p>
|
||||||
|
<p> } else if (isObject(target)){</p>
|
||||||
|
<p> let k = 'key'; let i=1;
|
||||||
|
while(Object.prototype.hasOwnProperty.call(target, k)) k =
|
||||||
|
'key_'+(i++);</p>
|
||||||
|
<p> target[k] = null;</p>
|
||||||
|
<p> } else if (atRoot){</p>
|
||||||
|
<p> data = (typeAtPath==='array')? [] : {};</p>
|
||||||
|
<p> }</p>
|
||||||
|
<p> renderTree();</p>
|
||||||
|
<p> syncRawFromData();</p>
|
||||||
|
<p> }</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> // ---- Parsing / validation ----</p>
|
||||||
|
<p> function validateValue(text, type){</p>
|
||||||
|
<p> try{ parseValue(text, type); return true; }catch{ return
|
||||||
|
false; }</p>
|
||||||
|
<p> }</p>
|
||||||
|
<p> function parseValue(text, type){</p>
|
||||||
|
<p> switch(type){</p>
|
||||||
|
<p> case 'string': return text;</p>
|
||||||
|
<p> case 'number': {</p>
|
||||||
|
<p> const n = Number(text); if (!Number.isFinite(n)) throw new
|
||||||
|
Error('Not a number'); return n;</p>
|
||||||
|
<p> }</p>
|
||||||
|
<p> case 'boolean': {</p>
|
||||||
|
<p> if (/^(true|false)$/i.test(text)) return
|
||||||
|
/^true$/i.test(text); throw new Error('Use true or false');</p>
|
||||||
|
<p> }</p>
|
||||||
|
<p> case 'null': return null;</p>
|
||||||
|
<p> case 'object': {</p>
|
||||||
|
<p> const v = JSON.parse(text||'{}'); if (!isObject(v)) throw
|
||||||
|
new Error('Not an object'); return v;</p>
|
||||||
|
<p> }</p>
|
||||||
|
<p> case 'array': {</p>
|
||||||
|
<p> const v = JSON.parse(text||'[]'); if (!Array.isArray(v))
|
||||||
|
throw new Error('Not an array'); return v;</p>
|
||||||
|
<p> }</p>
|
||||||
|
<p> default: return text;</p>
|
||||||
|
<p> }</p>
|
||||||
|
<p> }</p>
|
||||||
|
<p> function convertValue(text, toType){</p>
|
||||||
|
<p> // best‑effort conversion from current text view</p>
|
||||||
|
<p> return parseValue(text, toType);</p>
|
||||||
|
<p> }</p>
|
||||||
|
<p><br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p> // Initialize empty doc</p>
|
||||||
|
<p> data = {};</p>
|
||||||
|
<p> renderTree();</p>
|
||||||
|
<p> syncRawFromData();</p>
|
||||||
|
<p>})();</p>
|
||||||
|
<p></script></p>
|
||||||
|
<p></body></p>
|
||||||
|
<p></html></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user