Upload files to "/"

This commit is contained in:
2025-11-04 17:12:06 +00:00
commit 6f3265e748

774
jsonedit.html Normal file
View 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>&lt;!doctype
html&gt;</p>
<p>&lt;html lang=&quot;en&quot;&gt;</p>
<p>&lt;head&gt;</p>
<p> &lt;meta charset=&quot;utf-8&quot; /&gt;</p>
<p> &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width,
initial-scale=1&quot; /&gt;</p>
<p> &lt;title&gt;JSON Editor Upload • Edit •
Download&lt;/title&gt;</p>
<p> &lt;style&gt;</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,
&quot;Liberation Mono&quot;, &quot;Courier New&quot;, 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> &lt;/style&gt;</p>
<p>&lt;/head&gt;</p>
<p>&lt;body&gt;</p>
<p> &lt;header&gt;</p>
<p> &lt;h1&gt;JSON Editor&lt;/h1&gt;</p>
<p> &lt;div class=&quot;spacer&quot;&gt;&lt;/div&gt;</p>
<p> &lt;button id=&quot;downloadBtn&quot; class=&quot;primary&quot;
title=&quot;Download edited JSON (Ctrl/Cmd + S)&quot;&gt;Download&lt;/button&gt;</p>
<p> &lt;/header&gt;</p>
<p><br/>
<br/>
</p>
<p> &lt;div class=&quot;wrap&quot;&gt;</p>
<p> &lt;div class=&quot;toolbar&quot;&gt;</p>
<p> &lt;div class=&quot;group&quot;&gt;</p>
<p> &lt;label class=&quot;filelabel&quot; for=&quot;file&quot;&gt;Choose
JSON…&lt;/label&gt;</p>
<p> &lt;input id=&quot;file&quot; type=&quot;file&quot;
accept=&quot;application/json,.json,.txt&quot; hidden&gt;</p>
<p> &lt;button id=&quot;pasteBtn&quot;&gt;Paste&lt;/button&gt;</p>
<p> &lt;button id=&quot;sampleBtn&quot;&gt;Load
Sample&lt;/button&gt;</p>
<p> &lt;button id=&quot;formatBtn&quot;&gt;Format&lt;/button&gt;</p>
<p> &lt;button id=&quot;minifyBtn&quot;&gt;Minify&lt;/button&gt;</p>
<p> &lt;button id=&quot;clearBtn&quot;&gt;Clear&lt;/button&gt;</p>
<p> &lt;/div&gt;</p>
<p> &lt;div class=&quot;group search&quot;&gt;</p>
<p> &lt;input id=&quot;searchBox&quot; placeholder=&quot;Find
key or value… (Enter)&quot; /&gt;</p>
<p> &lt;span id=&quot;matchCount&quot; class=&quot;pill&quot;&gt;0
matches&lt;/span&gt;</p>
<p> &lt;/div&gt;</p>
<p> &lt;/div&gt;</p>
<p><br/>
<br/>
</p>
<p> &lt;div class=&quot;tabs&quot;&gt;</p>
<p> &lt;div class=&quot;tab active&quot; data-tab=&quot;tree&quot;&gt;Tree
Editor&lt;/div&gt;</p>
<p> &lt;div class=&quot;tab&quot; data-tab=&quot;raw&quot;&gt;Raw
JSON&lt;/div&gt;</p>
<p> &lt;/div&gt;</p>
<p><br/>
<br/>
</p>
<p> &lt;div class=&quot;panes&quot;&gt;</p>
<p> &lt;div class=&quot;pane active&quot; id=&quot;pane-tree&quot;&gt;</p>
<p> &lt;div class=&quot;row&quot;
style=&quot;grid-template-columns:1fr;&quot;&gt;</p>
<p> &lt;div class=&quot;col&quot;&gt;</p>
<p> &lt;h3&gt;Tree • &lt;span id=&quot;status&quot;
class=&quot;status&quot;&gt;No document&lt;/span&gt;&lt;/h3&gt;</p>
<p> &lt;div id=&quot;drop&quot; class=&quot;dropzone&quot;&gt;Drop
a .json file here&lt;/div&gt;</p>
<p> &lt;div class=&quot;help&quot;&gt;Tips: Double&#8209;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.&lt;/div&gt;</p>
<p> &lt;div id=&quot;tree&quot; class=&quot;tree&quot;&gt;&lt;/div&gt;</p>
<p> &lt;/div&gt;</p>
<p> &lt;/div&gt;</p>
<p> &lt;/div&gt;</p>
<p><br/>
<br/>
</p>
<p> &lt;div class=&quot;pane&quot; id=&quot;pane-raw&quot;&gt;</p>
<p> &lt;div class=&quot;row&quot;
style=&quot;grid-template-columns:1fr;&quot;&gt;</p>
<p> &lt;div class=&quot;col&quot;&gt;</p>
<p> &lt;h3&gt;Raw JSON &lt;span id=&quot;rawStatus&quot;
class=&quot;status&quot;&gt;&lt;/span&gt;&lt;/h3&gt;</p>
<p> &lt;textarea id=&quot;raw&quot; spellcheck=&quot;false&quot;
placeholder=&quot;Paste or type JSON here…&quot;&gt;&lt;/textarea&gt;</p>
<p> &lt;/div&gt;</p>
<p> &lt;/div&gt;</p>
<p> &lt;/div&gt;</p>
<p> &lt;/div&gt;</p>
<p> &lt;/div&gt;</p>
<p><br/>
<br/>
</p>
<p>&lt;script&gt;</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 =&gt; document.querySelector(sel);</p>
<p> const $$ = sel =&gt; 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 =&gt; tab.addEventListener('click', () =&gt;
{</p>
<p> $$('.tab').forEach(t=&gt;t.classList.remove('active'));</p>
<p> tab.classList.add('active');</p>
<p> const target = tab.dataset.tab;</p>
<p> $$('.pane').forEach(p=&gt;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) =&gt; {</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 &amp; Drop</p>
<p> ;['dragenter','dragover'].forEach(ev=&gt;drop.addEventListener(ev,
e=&gt;{e.preventDefault(); drop.classList.add('drag');}))</p>
<p> ;['dragleave','drop'].forEach(ev=&gt;drop.addEventListener(ev,
e=&gt;{e.preventDefault(); drop.classList.remove('drag');}))</p>
<p> drop.addEventListener('drop', async (e)=&gt;{</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' &amp;&amp; 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 ()=&gt;{</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', ()=&gt;{</p>
<p> const sample = {</p>
<p> name:&quot;Sample Document&quot;,</p>
<p> version:1,</p>
<p> tags:[&quot;example&quot;,&quot;json&quot;,&quot;editor&quot;],</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:&quot;First&quot;, active:true}, {id:2,
title:&quot;Second&quot;, 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', ()=&gt;{ 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', ()=&gt;{ 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', ()=&gt;{ 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', ()=&gt;{</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(()=&gt;URL.revokeObjectURL(a.href), 1500);</p>
<p> });</p>
<p><br/>
<br/>
</p>
<p> // Save shortcut</p>
<p> window.addEventListener('keydown', (e)=&gt;{</p>
<p> if ((e.ctrlKey || e.metaKey) &amp;&amp;
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)=&gt;{</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=&gt;{</p>
<p> n.style.outline = 'none';</p>
<p> const hay = (n.dataset.key+' '+n.dataset.type+'
'+n.dataset.value).toLowerCase();</p>
<p> if (q &amp;&amp; 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 &lt;-&gt; 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', ()=&gt;{</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', ()=&gt; addChild([], 'object')],</p>
<p> ['+ item', ()=&gt; addChild([], 'array')],</p>
<p> ['Clear', ()=&gt;{ 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)=&gt; renderNode(String(i), v,
path.concat(i), 'array'));</p>
<p> } else if (isObject(value)){</p>
<p> Object.keys(value).forEach(k=&gt; 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&#8209;click to rename key';</p>
<p> keyEl.readOnly = true;</p>
<p> keyEl.addEventListener('dblclick', ()=&gt;
keyEl.readOnly=false);</p>
<p> keyEl.addEventListener('blur', ()=&gt; keyEl.readOnly=true);</p>
<p> keyEl.addEventListener('change', ()=&gt;{</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', ()=&gt;{</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', ()=&gt;{</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> ['+', ()=&gt; addChild(path, tSel.value)],</p>
<p> ['', ()=&gt; deletePath(path)],</p>
<p> ['⋮', ()=&gt;
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=&gt;{</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])=&gt;{ 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 &amp;&amp; typeof o==='object' &amp;&amp;
!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)=&gt; 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' &amp;&amp; !isObject(target))
setByPath(path, {}), target = getByPath(path);</p>
<p> if (typeAtPath==='array' &amp;&amp; !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&#8209;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>&lt;/script&gt;</p>
<p>&lt;/body&gt;</p>
<p>&lt;/html&gt;</p>
</body>
</html>