<!doctype html>

<html lang="en">

<head>

<meta charset="utf-8" />

<meta name="viewport" content="width=device-width, initial-scale=1" />

<title>JSON Editor – Upload • Edit • Download</title>

<style>

:root{

--bg:#0b0f14; --panel:#121922; --muted:#1a2430; --text:#e6edf3; --sub:#9fb0c3;

--accent:#4aa3ff; --accent-2:#22c55e; --warn:#f59e0b; --err:#ef4444; --border:#243241;

}

*{box-sizing:border-box}

body{margin:0; font:14px/1.4 system-ui,Segoe UI,Roboto,Ubuntu,Arial,sans-serif; background:var(--bg); color:var(--text);}

header{display:flex; gap:.75rem; align-items:center; padding:12px 14px; border-bottom:1px solid var(--border); background:linear-gradient(180deg,var(--panel),#0f1520);}

header h1{font-size:16px; margin:0; font-weight:600; letter-spacing:.2px}

header .spacer{flex:1}

button,input[type=file],.btn{border:1px solid var(--border); background:var(--muted); color:var(--text); border-radius:10px; padding:8px 12px; cursor:pointer}

button:hover,.btn:hover{background:#223046}

button.primary{background:var(--accent); color:#06121f; border-color:#2d7cd6; font-weight:600}

button.success{background:var(--accent-2); color:#04120a; border-color:#18a24c; font-weight:600}

button.warn{background:var(--warn); color:#1d1402; border-color:#b7791f; font-weight:600}

button.danger{background:var(--err); color:#1c0303; border-color:#b91c1c; font-weight:600}

.wrap{display:grid; grid-template-rows:auto 1fr; height:calc(100dvh - 0px);}

.toolbar{display:flex; flex-wrap:wrap; gap:.5rem; padding:10px 14px; background:#0f1622; border-bottom:1px solid var(--border)}

.toolbar .group{display:flex; gap:.5rem; align-items:center}

.filelabel{padding:8px 12px; border:1px dashed var(--border); border-radius:10px; background:var(--panel)}

.tabs{display:flex; gap:8px; padding:8px 14px; border-bottom:1px solid var(--border); background:#0e1520}

.tab{padding:8px 12px; border-radius:8px; cursor:pointer; color:var(--sub); border:1px solid transparent}

.tab.active{color:var(--text); border-color:var(--border); background:var(--muted)}

.panes{height:100%;}

.pane{display:none; height:calc(100dvh - 180px);} /* adjusted for header+toolbars */

.pane.active{display:block}

.row{display:grid; grid-template-columns: 1fr 1fr; gap:12px; height:100%}

.col{height:100%; overflow:auto; background:var(--panel); border-top:1px solid var(--border)}

.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)}

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}

.dropzone{border:2px dashed var(--border); margin:12px; padding:20px; border-radius:12px; text-align:center; color:var(--sub)}

.dropzone.drag{border-color:var(--accent); color:var(--accent)}



/* Tree editor */

.tree{padding:8px 10px}

.node{display:flex; align-items:center; gap:8px; padding:4px 6px; border-left:2px solid transparent}

.node:hover{background:#0e1520; border-left-color:#233347}

.indent{width:16px}

.key{width:220px; max-width:35vw}

.val{flex:1}

.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}

.val input.invalid{border-color:var(--err); outline:2px solid rgba(239,68,68,.25)}

.pill{font-size:11px; padding:2px 6px; border-radius:999px; border:1px solid var(--border); background:#0b111a; color:var(--sub)}

.controls{display:flex; gap:6px}

.controls button{padding:4px 8px; font-size:12px; border-radius:8px}

.help{color:var(--sub); padding:8px 12px; font-size:12px}

.status{padding:6px 10px; font-size:12px}

.status.good{color:var(--accent-2)}

.status.bad{color:var(--err)}

.search{margin-left:auto; display:flex; gap:6px; align-items:center}

.search input{padding:6px 8px; border-radius:8px; border:1px solid var(--border); background:#0b111a; color:var(--text)}

</style>

</head>

<body>

<header>

<h1>JSON Editor</h1>

<div class="spacer"></div>

<button id="downloadBtn" class="primary" title="Download edited JSON (Ctrl/Cmd + S)">Download</button>

</header>



<div class="wrap">

<div class="toolbar">

<div class="group">

<label class="filelabel" for="file">Choose JSON…</label>

<input id="file" type="file" accept="application/json,.json,.txt" hidden>

<button id="pasteBtn">Paste</button>

<button id="sampleBtn">Load Sample</button>

<button id="formatBtn">Format</button>

<button id="minifyBtn">Minify</button>

<button id="clearBtn">Clear</button>

</div>

<div class="group search">

<input id="searchBox" placeholder="Find key or value… (Enter)" />

<span id="matchCount" class="pill">0 matches</span>

</div>

</div>



<div class="tabs">

<div class="tab active" data-tab="tree">Tree Editor</div>

<div class="tab" data-tab="raw">Raw JSON</div>

</div>



<div class="panes">

<div class="pane active" id="pane-tree">

<div class="row" style="grid-template-columns:1fr;">

<div class="col">

<h3>Tree • <span id="status" class="status">No document</span></h3>

<div id="drop" class="dropzone">Drop a .json file here</div>

<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>

<div id="tree" class="tree"></div>

</div>

</div>

</div>



<div class="pane" id="pane-raw">

<div class="row" style="grid-template-columns:1fr;">

<div class="col">

<h3>Raw JSON <span id="rawStatus" class="status"></span></h3>

<textarea id="raw" spellcheck="false" placeholder="Paste or type JSON here…"></textarea>

</div>

</div>

</div>

</div>

</div>



<script>

(function(){

'use strict';



// ---- State ----

let data = null; // current JSON object/array

let filename = 'edited.json';

let lastSearch = '';



// ---- DOM ----

const $ = sel => document.querySelector(sel);

const $$ = sel => Array.from(document.querySelectorAll(sel));

const fileInput = $('#file');

const downloadBtn = $('#downloadBtn');

const pasteBtn = $('#pasteBtn');

const formatBtn = $('#formatBtn');

const minifyBtn = $('#minifyBtn');

const clearBtn = $('#clearBtn');

const sampleBtn = $('#sampleBtn');

const statusEl = $('#status');

const rawStatusEl = $('#rawStatus');

const rawTA = $('#raw');

const drop = $('#drop');

const treeEl = $('#tree');

const searchBox = $('#searchBox');

const matchCount = $('#matchCount');



// ---- Tabs ----

$$('.tab').forEach(tab => tab.addEventListener('click', () => {

$$('.tab').forEach(t=>t.classList.remove('active'));

tab.classList.add('active');

const target = tab.dataset.tab;

$$('.pane').forEach(p=>p.classList.remove('active'));

$('#pane-'+target).classList.add('active');

if (target === 'raw') syncRawFromData();

if (target === 'tree') syncDataFromRaw(false);

}));



// ---- File handling ----

fileInput.addEventListener('change', async (e) => {

if (!e.target.files.length) return;

const f = e.target.files[0];

filename = f.name.replace(/\.[^.]+$/, '') + '.json';

const text = await f.text();

loadJSONText(text);

});



// Drag & Drop

;['dragenter','dragover'].forEach(ev=>drop.addEventListener(ev, e=>{e.preventDefault(); drop.classList.add('drag');}))

;['dragleave','drop'].forEach(ev=>drop.addEventListener(ev, e=>{e.preventDefault(); drop.classList.remove('drag');}))

drop.addEventListener('drop', async (e)=>{

const f = e.dataTransfer.files?.[0];

if (!f) return;

filename = f.name.replace(/\.[^.]+$/, '') + '.json';

const text = await f.text();

loadJSONText(text);

});



function loadJSONText(text){

try{

const parsed = JSON.parse(text);

data = parsed;

status(`Loaded ${typeof data === 'object' && data ? (Array.isArray(data)?'Array':'Object') : typeof data}`);

renderTree();

syncRawFromData();

}catch(err){

alert('Invalid JSON: '+ err.message);

}

}



// ---- Toolbar actions ----

pasteBtn.addEventListener('click', async ()=>{

try{

const text = await navigator.clipboard.readText();

if (!text) return alert('Clipboard is empty.');

loadJSONText(text);

}catch{ alert('Clipboard access denied. Paste into Raw JSON tab instead.'); }

});



sampleBtn.addEventListener('click', ()=>{

const sample = {

name:"Sample Document",

version:1,

tags:["example","json","editor"],

features:{

upload:true,

edit:true,

download:true,

nested:{ ok:true, count:3 }

},

items:[ {id:1, title:"First", active:true}, {id:2, title:"Second", active:false} ]

};

data = sample;

filename = 'sample.json';

renderTree();

syncRawFromData();

status('Sample loaded');

});



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);} });

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);} });

clearBtn.addEventListener('click', ()=>{ data = null; filename='edited.json'; treeEl.innerHTML=''; rawTA.value=''; status('No document'); rawStatus(''); matchCount.textContent='0 matches'; });



// Download

downloadBtn.addEventListener('click', ()=>{

if ($('#pane-raw').classList.contains('active')){

if (!syncDataFromRaw(true)) return; // invalid

}

const blob = new Blob([JSON.stringify(data, null, 2)], {type:'application/json'});

const a = document.createElement('a');

a.href = URL.createObjectURL(blob);

a.download = filename || 'edited.json';

a.click();

setTimeout(()=>URL.revokeObjectURL(a.href), 1500);

});



// Save shortcut

window.addEventListener('keydown', (e)=>{

if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase()==='s'){

e.preventDefault();

downloadBtn.click();

}

});



// ---- Search ----

searchBox.addEventListener('keydown', (e)=>{

if (e.key==='Enter') doSearch(searchBox.value.trim());

});



function doSearch(q){

lastSearch = q;

let matches = 0;

$$('.node').forEach(n=>{

n.style.outline = 'none';

const hay = (n.dataset.key+' '+n.dataset.type+' '+n.dataset.value).toLowerCase();

if (q && hay.includes(q.toLowerCase())){ matches++; n.style.outline='2px solid var(--accent)'; }

});

matchCount.textContent = `${matches} match${matches===1?'':'es'}`;

}



// ---- Raw JSON <-> data sync ----

function syncRawFromData(){

if (data==null){ rawTA.value=''; rawStatus(''); return; }

rawTA.value = JSON.stringify(data, null, 2);

rawStatus('Synced from tree ✓', true);

}

function syncDataFromRaw(showErrors){

if (!rawTA.value.trim()) return true;

try{ data = JSON.parse(rawTA.value); renderTree(); rawStatus('Applied ✓', true); return true; }

catch(e){ if (showErrors) rawStatus('Invalid JSON: '+e.message, false); return false; }

}

function rawStatus(msg, good){ rawStatusEl.textContent = msg||''; rawStatusEl.className = 'status ' + (good? 'good':'bad'); }

function status(msg){ statusEl.textContent = msg; statusEl.className = 'status ' + (/✓|Loaded|Sample/.test(msg)?'good':''); }



// ---- Tree editor rendering ----

function renderTree(){

treeEl.innerHTML='';

if (data==null){ return; }



// root row header

const rootControls = document.createElement('div');

rootControls.className='node';

rootControls.dataset.key='(root)';

rootControls.dataset.type=Array.isArray(data)?'array':'object';

rootControls.dataset.value='';



// Root type switcher

const typeSel = typeSelect(Array.isArray(data)?'array':'object');

typeSel.addEventListener('change', ()=>{

const t = typeSel.value;

data = (t==='array')? [] : {};

renderTree();

syncRawFromData();

});



const keyPill = pill('(root)');

const controls = btnRow([

['+ key', ()=> addChild([], 'object')],

['+ item', ()=> addChild([], 'array')],

['Clear', ()=>{ data = Array.isArray(data)?[]:{}; renderTree(); syncRawFromData(); }]

]);



rootControls.append(

span('indent'),

divEl('key', keyPill),

divEl('val', typeSel),

divEl('controls', controls)

);

treeEl.appendChild(rootControls);



// body

buildNodes(data, []);

status(`Editing ${Array.isArray(data)?'Array':'Object'} ✓`);

if (lastSearch) doSearch(lastSearch);

}



function buildNodes(value, path){

if (Array.isArray(value)){

value.forEach((v, i)=> renderNode(String(i), v, path.concat(i), 'array'));

} else if (isObject(value)){

Object.keys(value).forEach(k=> renderNode(k, value[k], path.concat(k), 'object'));

}

}



function renderNode(key, value, path, parentType){

const node = document.createElement('div');

node.className='node';

node.dataset.key=key;

node.dataset.type=typeOf(value);

node.dataset.value=safeStr(value);



// indent based on depth

const depth = path.length; // root is []

const indent = document.createElement('div');

indent.className='indent';

indent.style.marginLeft = (depth*16)+ 'px';



// key editor (objects) or index label (arrays)

let keyEl;

if (parentType==='object'){

keyEl = document.createElement('input');

keyEl.value = key;

keyEl.title = 'Double‑click to rename key';

keyEl.readOnly = true;

keyEl.addEventListener('dblclick', ()=> keyEl.readOnly=false);

keyEl.addEventListener('blur', ()=> keyEl.readOnly=true);

keyEl.addEventListener('change', ()=>{

if (!keyEl.value) { keyEl.value = key; return; }

renameKey(path.slice(0,-1), key, keyEl.value);

});

}else{

keyEl = pill('['+key+']');

}



// type selector and value editor

const tSel = typeSelect(typeOf(value));

const valInput = valueInput(value);



tSel.addEventListener('change', ()=>{

const newType = tSel.value;

const converted = convertValue(valInput.value, newType);

setByPath(path, converted);

renderTree();

syncRawFromData();

});



valInput.addEventListener('change', ()=>{

const t = tSel.value;

if (!validateValue(valInput.value, t)){

valInput.classList.add('invalid');

return;

}

valInput.classList.remove('invalid');

const v = parseValue(valInput.value, t);

setByPath(path, v);

syncRawFromData();

node.dataset.value = safeStr(v);

if (lastSearch) doSearch(lastSearch);

});



// controls

const controls = btnRow([

['+', ()=> addChild(path, tSel.value)],

['−', ()=> deletePath(path)],

['⋮', ()=> navigator.clipboard.writeText(JSON.stringify(getByPath(path)))],

]);



node.append(

indent,

divEl('key', keyEl),

divEl('val', compose(tSel, valInput)),

divEl('controls', controls)

);



treeEl.appendChild(node);



// recurse for containers

if (Array.isArray(value) || isObject(value)) buildNodes(value, path);

}



// ---- Helpers: create common UI bits ----

function typeSelect(current){

const sel = document.createElement('select');

['string','number','boolean','null','object','array'].forEach(t=>{

const o = document.createElement('option'); o.value=t; o.textContent=t; sel.appendChild(o);

});

sel.value = current;

return sel;

}

function valueInput(value){

const i = document.createElement('input');

if (isObject(value) || Array.isArray(value)){

i.value = JSON.stringify(value);

i.title = 'Edit as JSON fragment';

} else if (value === null){

i.value = 'null';

} else {

i.value = String(value);

}

return i;

}

function btnRow(items){

const wrap = document.createElement('div'); wrap.className='controls';

items.forEach(([label, fn])=>{ const b=document.createElement('button'); b.textContent=label; b.addEventListener('click', fn); wrap.appendChild(b); });

return wrap;

}

function pill(text){ const s=document.createElement('span'); s.className='pill'; s.textContent=text; return s; }

function span(t){ const s=document.createElement('span'); s.textContent=t; return s; }

function divEl(cls, child){ const d=document.createElement('div'); d.className=cls; if (child) d.appendChild(child); return d; }

function compose(a,b){ const w=document.createElement('div'); w.style.display='flex'; w.style.gap='6px'; w.append(a,b); return w; }



// ---- Data ops ----

function isObject(o){ return o && typeof o==='object' && !Array.isArray(o); }

function typeOf(v){ return v===null? 'null' : Array.isArray(v)? 'array' : typeof v; }

function safeStr(v){ try{return JSON.stringify(v);}catch{return String(v);} }



function getByPath(path){

return path.reduce((acc, key)=> acc?.[key], data);

}

function setByPath(path, value){

if (path.length===0){ data = value; renderTree(); return; }

const parent = getByPath(path.slice(0,-1));

const key = path[path.length-1];

parent[key] = value;

}

function deletePath(path){

const parent = getByPath(path.slice(0,-1));

const key = path[path.length-1];

if (Array.isArray(parent)) parent.splice(Number(key),1); else delete parent[key];

renderTree();

syncRawFromData();

}

function renameKey(parentPath, oldKey, newKey){

const parent = getByPath(parentPath);

if (!isObject(parent)) return;

if (newKey in parent){ alert('Key already exists at this level.'); renderTree(); return; }

parent[newKey] = parent[oldKey];

delete parent[oldKey];

renderTree();

syncRawFromData();

}

function addChild(path, typeAtPath){

// If this node is a primitive, convert it into a container first

let target = getByPath(path);

const atRoot = path.length===0;

if (typeAtPath==='object' && !isObject(target)) setByPath(path, {}), target = getByPath(path);

if (typeAtPath==='array' && !Array.isArray(target)) setByPath(path, []), target = getByPath(path);



if (Array.isArray(target)){

target.push(null);

} else if (isObject(target)){

let k = 'key'; let i=1; while(Object.prototype.hasOwnProperty.call(target, k)) k = 'key_'+(i++);

target[k] = null;

} else if (atRoot){

data = (typeAtPath==='array')? [] : {};

}

renderTree();

syncRawFromData();

}



// ---- Parsing / validation ----

function validateValue(text, type){

try{ parseValue(text, type); return true; }catch{ return false; }

}

function parseValue(text, type){

switch(type){

case 'string': return text;

case 'number': {

const n = Number(text); if (!Number.isFinite(n)) throw new Error('Not a number'); return n;

}

case 'boolean': {

if (/^(true|false)$/i.test(text)) return /^true$/i.test(text); throw new Error('Use true or false');

}

case 'null': return null;

case 'object': {

const v = JSON.parse(text||'{}'); if (!isObject(v)) throw new Error('Not an object'); return v;

}

case 'array': {

const v = JSON.parse(text||'[]'); if (!Array.isArray(v)) throw new Error('Not an array'); return v;

}

default: return text;

}

}

function convertValue(text, toType){

// best‑effort conversion from current text view

return parseValue(text, toType);

}



// Initialize empty doc

data = {};

renderTree();

syncRawFromData();

})();

</script>

</body>

</html>