From 6f3265e7486bd6d38c5c4194f8cfd82d1c076564 Mon Sep 17 00:00:00 2001 From: Atlaskor Date: Tue, 4 Nov 2025 17:12:06 +0000 Subject: [PATCH] Upload files to "/" --- jsonedit.html | 774 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 774 insertions(+) create mode 100644 jsonedit.html diff --git a/jsonedit.html b/jsonedit.html new file mode 100644 index 0000000..81e210a --- /dev/null +++ b/jsonedit.html @@ -0,0 +1,774 @@ + + + + + + + + + + + +

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

+ + \ No newline at end of file