Files
JSONEditor/jsonedit.html
2025-11-04 17:12:06 +00:00

774 lines
26 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>