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