Spreadsheet
ox alpha · full

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 27,596 bytes · SHA-256 e2780766eb8d
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Sheet</title>
<style>
/* ============ STYLE ============ */
:root{
--cell-w: 90px;
--cell-h: 24px;
--header-bg:#f8f9fa;
--header-fg:#5f6368;
--grid-line:#e1e3e5;
--sel-color:#1a73e8;
--err-color:#d93025;
--bg:#ffffff;
}
*{box-sizing:border-box;}
body{
margin:0; font-family:'Segoe UI',Arial,sans-serif; font-size:13px;
background:var(--bg); color:#202124; overflow:hidden;
display:flex; flex-direction:column; height:100vh;
}
/* Toolbar */
#toolbar{
display:flex; gap:6px; padding:6px 10px; border-bottom:1px solid var(--grid-line);
background:var(--header-bg); align-items:center;
}
#toolbar button{
font-size:12px; padding:4px 10px; border:1px solid #dadce0; border-radius:4px;
background:#fff; cursor:pointer; color:#202124;
}
#toolbar button:hover{background:#f1f3f4;}
#toolbar .title{font-weight:600; margin-right:12px; color:#5f6368;}
/* Formula bar */
#fbar-row{
display:flex; align-items:center; border-bottom:1px solid var(--grid-line);
background:#fff; height:30px; flex:0 0 auto;
}
#cell-ref{
width:70px; text-align:center; font-weight:600; color:#5f6368;
border-right:1px solid var(--grid-line); height:100%; line-height:30px;
background:var(--header-bg); flex:0 0 auto; user-select:none;
}
#fx{padding:0 8px; color:#5f6368; font-style:italic; user-select:none;}
#fbar{
flex:1; border:none; outline:none; font-family:Consolas,Menlo,monospace;
font-size:13px; color:#202124; height:100%; padding:0 8px;
border-left:1px solid var(--grid-line);
}
/* Grid */
#grid-wrap{flex:1; overflow:auto; position:relative;}
table{border-collapse:collapse; table-layout:fixed;}
th,td{
width:var(--cell-w); min-width:var(--cell-w); max-width:var(--cell-w);
height:var(--cell-h); padding:0 5px; border-right:1px solid var(--grid-line);
border-bottom:1px solid var(--grid-line); white-space:nowrap; overflow:hidden;
text-overflow:ellipsis; background:#fff;
}
th{
background:var(--header-bg); color:var(--header-fg); font-weight:500;
text-align:center; font-size:11px; user-select:none; cursor:pointer;
}
thead th{position:sticky; top:0; z-index:3;}
tbody th{position:sticky; left:0; z-index:2;}
thead th:first-child{left:0; z-index:5;}
td{cursor:cell; font-size:13px;}
td.num{text-align:right;}
td.err{color:var(--err-color); font-weight:600;}
td.sel{
outline:2px solid var(--sel-color); outline-offset:-2px;
}
/* In-cell editor */
#cell-input{
position:absolute; display:none; border:2px solid var(--sel-color);
outline:none; font-family:Consolas,Menlo,monospace; font-size:13px;
padding:0 3px; z-index:10; background:#fff;
}
/* Context menu */
#ctxmenu{
display:none; position:absolute; z-index:100; background:#fff;
border:1px solid #dadce0; border-radius:4px; box-shadow:0 2px 8px rgba(0,0,0,.2);
min-width:160px; padding:4px 0;
}
#ctxmenu div{
padding:6px 16px; cursor:pointer; font-size:13px;
}
#ctxmenu div:hover{background:#f1f3f4;}
#ctxmenu div.danger{color:var(--err-color);}
</style>
</head>
<body>
<div id="toolbar">
<span class="title">Sheet</span>
<button id="btn-ir">+ Row</button>
<button id="btn-dr">− Row</button>
<button id="btn-ic">+ Col</button>
<button id="btn-dc">− Col</button>
</div>
<div id="fbar-row">
<div id="cell-ref">A1</div>
<div id="fx">fx</div>
<input id="fbar" autocomplete="off" spellcheck="false">
</div>
<div id="grid-wrap">
<table id="grid">
<thead id="thead"></thead>
<tbody id="tbody"></tbody>
</table>
<input id="cell-input" autocomplete="off" spellcheck="false">
</div>
<div id="ctxmenu"></div>
<script>
"use strict";
/* ============================================================
* TUNABLE CONSTANTS
* ============================================================ */
const CFG = {
ROWS: 100,
COLS: 26,
CELL_W: 90, // px (keep in sync with --cell-w)
CELL_H: 24,
STORAGE_KEY: 'sheet-app-v1',
ERR_REF: '#REF!',
ERR_DIV: '#DIV/0!',
ERR_PARSE: '#ERR!',
ERR_CYCLE: '#CYCLE!',
};
/* ============================================================
* COLUMN NAME UTILITIES
* ============================================================ */
function colName(c){ // 0-based -> letters
let s='';
c=c+1;
while(c>0){ c--; s=String.fromCharCode(65+(c%26))+s; c=Math.floor(c/26); }
return s;
}
function colIndex(name){ // letters -> 0-based
let n=0;
for(const ch of name.toUpperCase()) n=n*26+(ch.charCodeAt(0)-64);
return n-1;
}
const key=(r,c)=>r+','+c;
const inGrid=(r,c)=>r>=0&&r<CFG.ROWS&&c>=0&&c<CFG.COLS;
/* ============================================================
* TOKENIZER
* ============================================================ */
// token: {type:'num'|'ref'|'ident'|'op'|'err', v, r?, c?}
function tokenize(src){
const toks=[]; let i=0;
const isDigit=ch=>ch>='0'&&ch<='9';
const isLetter=ch=>/[A-Za-z]/.test(ch);
while(i<src.length){
const ch=src[i];
if(ch===' '||ch==='\t'){ i++; continue; }
if(isDigit(ch)||(ch==='.'&&isDigit(src[i+1]))){
let j=i; while(j<src.length&&/[0-9.]/.test(src[j]))j++;
const v=parseFloat(src.slice(i,j));
if(isNaN(v)) throw new Error('bad number');
toks.push({type:'num',v}); i=j; continue;
}
if(isLetter(ch)){
let j=i; while(j<src.length&&/[A-Za-z0-9_]/.test(src[j]))j++;
const word=src.slice(i,j);
const m=word.match(/^([A-Za-z]{1,3})([0-9]+)$/);
if(m){ toks.push({type:'ref',r:parseInt(m[2],10)-1,c:colIndex(m[1])}); }
else toks.push({type:'ident',v:word.toUpperCase()});
i=j; continue;
}
if(ch==='#'){
let j=i; while(j<src.length&&!/\s/.test(src[j]))j++;
toks.push({type:'err',v:src.slice(i,j).toUpperCase()}); i=j; continue;
}
if('+-*/(),:'.includes(ch)){ toks.push({type:'op',v:ch}); i++; continue; }
throw new Error('unexpected character '+ch);
}
return toks;
}
/* ============================================================
* PARSER (recursive descent)
*
* expr := term (('+'|'-') term)*
* term := unary (('*'|'/') unary)*
* unary := '-' unary | primary
* primary:= NUM | REF (':' REF)? | IDENT '(' args ')' | '(' expr ')'
* ============================================================ */
function parseFormula(src){
const toks=tokenize(src);
let p=0;
const peek=()=>toks[p];
const next=()=>toks[p++];
const expectOp=v=>{
const t=next();
if(!t||t.type!=='op'||t.v!==v) throw new Error('expected '+v);
};
function expr(){
let l=term();
while(peek()&&peek().type==='op'&&(peek().v==='+'||peek().v==='-')){
const op=next().v; l={t:'bin',op,l,r:term()};
}
return l;
}
function term(){
let l=unary();
while(peek()&&peek().type==='op'&&(peek().v==='*'||peek().v==='/')){
const op=next().v; l={t:'bin',op,l,r:unary()};
}
return l;
}
function unary(){
if(peek()&&peek().type==='op'&&peek().v==='-'){
next(); return {t:'neg',e:unary()};
}
return primary();
}
function primary(){
const t=next();
if(!t) throw new Error('unexpected end');
if(t.type==='num') return {t:'num',v:t.v};
if(t.type==='err') return {t:'referr'};
if(t.type==='ref'){
if(peek()&&peek().type==='op'&&peek().v===':'){
next();
const b=next();
if(!b||b.type!=='ref') throw new Error('bad range');
return {t:'rng',
a:{r:Math.min(t.r,b.r),c:Math.min(t.c,b.c)},
b:{r:Math.max(t.r,b.r),c:Math.max(t.c,b.c)}};
}
return {t:'ref',r:t.r,c:t.c};
}
if(t.type==='ident'){
expectOp('(');
const args=[];
if(!(peek()&&peek().type==='op'&&peek().v===')')){
args.push(expr());
while(peek()&&peek().type==='op'&&peek().v===','){ next(); args.push(expr()); }
}
expectOp(')');
return {t:'fn',name:t.v,args};
}
if(t.type==='op'&&t.v==='('){
const e=expr(); expectOp(')'); return e;
}
throw new Error('unexpected token');
}
const ast=expr();
if(p<toks.length) throw new Error('trailing tokens');
return ast;
}
/* ---- serialize AST back to formula text ---- */
const BIN_PREC={'+' :1,'-':1,'*':2,'/':2};
function ser(n){
switch(n.t){
case 'num': return String(n.v);
case 'ref': return colName(n.c)+(n.r+1);
case 'referr': return CFG.ERR_REF;
case 'rng': return colName(n.a.c)+(n.a.r+1)+':'+colName(n.b.c)+(n.b.r+1);
case 'neg':
return '-'+(n.e.t==='num'||n.e.t==='ref'?ser(n.e):'('+ser(n.e)+')');
case 'bin': {
const my=BIN_PREC[n.op];
const wrap=child=>(child.t==='bin'&&BIN_PREC[child.op]<my)?'('+ser(child)+')':ser(child);
let l=wrap(n.l);
let r=(n.r.t==='bin'&&BIN_PREC[n.r.op]<=my)?'('+ser(n.r)+')':ser(n.r);
return l+n.op+r;
}
case 'fn': return n.name+'('+n.args.map(ser).join(',')+')';
}
}
/* ============================================================
* SHEET MODEL (cells, dependency graph, evaluation)
* ============================================================ */
let cells=new Map(); // key -> {raw, ast|null, value}
let prec=new Map(); // key -> Set(keys this cell references)
let dep=new Map(); // key -> Set(keys that reference this cell)
function isErr(v){ return typeof v==='string'&&v.startsWith('#'); }
function getCell(k){ return cells.get(k); }
function addEdge(from,to){
if(!prec.has(from)) prec.set(from,new Set());
prec.get(from).add(to);
if(!dep.has(to)) dep.set(to,new Set());
dep.get(to).add(from);
}
function removeEdges(k){
const ps=prec.get(k);
if(ps) for(const p of ps){ const d=dep.get(p); if(d){ d.delete(k); if(!d.size)dep.delete(p);} }
prec.delete(k);
}
/* extract all referenced keys from an AST (ranges expand fully) */
function astRefs(ast,out){
if(!ast)return;
switch(ast.t){
case 'ref': out.add(key(ast.r,ast.c)); break;
case 'rng':
for(let r=ast.a.r;r<=ast.b.r;r++)
for(let c=ast.a.c;c<=ast.b.c;c++) out.add(key(r,c));
break;
case 'neg': astRefs(ast.e,out); break;
case 'bin': astRefs(ast.l,out); astRefs(ast.r,out); break;
case 'fn': ast.args.forEach(a=>astRefs(a,out)); break;
}
}
/* evaluate an AST against current cached values */
function evalNode(n){
switch(n.t){
case 'num': return n.v;
case 'referr': return CFG.ERR_REF;
case 'ref': {
if(!inGrid(n.r,n.c)) return CFG.ERR_REF;
const cell=cells.get(key(n.r,n.c));
if(!cell||cell.raw==='') return 0;
return cell.value;
}
case 'neg': {
const v=evalNode(n.e);
return typeof v==='number'?-v:v; // errors pass through
}
case 'bin': {
const l=evalNode(n.l), r=evalNode(n.r);
if(isErr(l))return l; if(isErr(r))return r;
if(typeof l!=='number'||typeof r!=='number') return CFG.ERR_PARSE;
switch(n.op){
case '+': return l+r;
case '-': return l-r;
case '*': return l*r;
case '/': return r===0?CFG.ERR_DIV:l/r;
}
}
case 'fn': return evalFn(n);
}
return CFG.ERR_PARSE;
}
function collectArgs(n){
const vals=[];
for(const a of n.args){
if(a.t==='rng'){
for(let r=a.a.r;r<=a.b.r;r++)
for(let c=a.a.c;c<=a.b.c;c++){
const cell=cells.get(key(r,c));
if(cell&&cell.raw!=='') vals.push(cell.value);
}
} else {
const v=evalNode(a);
if(!(a.t==='ref'&&!cells.get(key(a.r,a.c)))) vals.push(v);
}
}
return vals;
}
const nums=vs=>vs.filter(v=>typeof v==='number');
function evalFn(n){
const vs=collectArgs(n);
switch(n.name){
case 'COUNT': return nums(vs.filter(v=>!isErr(v))).length;
case 'SUM': {
for(const v of vs) if(isErr(v)) return v;
return nums(vs).reduce((a,b)=>a+b,0);
}
case 'AVG': case 'AVERAGE': {
for(const v of vs) if(isErr(v)) return v;
const ns=nums(vs);
if(!ns.length) return CFG.ERR_DIV;
return ns.reduce((a,b)=>a+b,0)/ns.length;
}
case 'MIN': {
for(const v of vs) if(isErr(v)) return v;
const ns=nums(vs); return ns.length?Math.min(...ns):0;
}
case 'MAX': {
for(const v of vs) if(isErr(v)) return v;
const ns=nums(vs); return ns.length?Math.max(...ns):0;
}
}
return CFG.ERR_PARSE;
}
/* compute display value of a cell from its raw content */
function computeValue(cell){
const raw=cell.raw.trim();
if(raw==='') { cell.value=''; return; }
if(raw[0]==='='){
try{
cell.ast=parseFormula(raw.slice(1));
cell.value=evalNode(cell.ast);
}catch(e){
cell.ast=null;
cell.value=CFG.ERR_PARSE;
}
} else {
cell.ast=null;
const n=Number(raw);
cell.value=(raw!==''&&!isNaN(n))?n:raw;
}
}
/* --- recalculation: only dependents of changed key, topo order --- */
function recalcFrom(startKey){
// gather transitive dependents (including startKey)
const affected=new Set([startKey]);
const stack=[startKey];
while(stack.length){
const k=stack.pop();
const ds=dep.get(k);
if(ds) for(const d of ds) if(!affected.has(d)){ affected.add(d); stack.push(d); }
}
// Kahn topological sort restricted to affected subgraph
const indeg=new Map();
for(const k of affected){
let n=0;
const ps=prec.get(k);
if(ps) for(const p of ps) if(affected.has(p)) n++;
indeg.set(k,n);
}
const q=[];
for(const [k,n] of indeg) if(n===0) q.push(k);
const done=new Set();
while(q.length){
const k=q.shift();
done.add(k);
const cell=cells.get(k);
if(cell) computeValue(cell);
const ds=dep.get(k);
if(ds) for(const d of ds) if(affected.has(d)){
indeg.set(d,indeg.get(d)-1);
if(indeg.get(d)===0) q.push(d);
}
}
// anything unprocessed is part of / downstream of a cycle
for(const k of affected){
if(done.has(k))continue;
const cell=cells.get(k);
if(cell){ cell.value=isErr(cell.value)&&cell.value===CFG.ERR_CYCLE?cell.value:CFG.ERR_CYCLE; }
renderCellKey(k);
}
for(const k of done) renderCellKey(k);
scheduleSave();
}
function fullRecalc(){
// topo over entire sheet
const indeg=new Map();
for(const k of cells.keys()){
let n=0; const ps=prec.get(k);
if(ps) for(const p of ps) if(cells.has(p)) n++;
indeg.set(k,n);
}
const q=[]; for(const [k,n] of indeg) if(n===0)q.push(k);
const done=new Set();
while(q.length){
const k=q.shift(); done.add(k);
const cell=cells.get(k); if(cell)computeValue(cell);
const ds=dep.get(k);
if(ds)for(const d of ds) if(indeg.has(d)){
indeg.set(d,indeg.get(d)-1);
if(indeg.get(d)===0)q.push(d);
}
}
for(const [k] of indeg) if(!done.has(k)){ const c=cells.get(k); if(c)c.value=CFG.ERR_CYCLE; }
renderAll();
scheduleSave();
}
/* set raw content of a cell and recalc dependents */
function setCellRaw(r,c,raw){
const k=key(r,c);
let cell=cells.get(k);
if(raw===''||raw==null){
if(!cell)return;
removeEdges(k); cells.delete(k);
recalcFrom(k);
return;
}
if(!cell){ cell={raw:'',ast:null,value:''}; cells.set(k,cell); }
removeEdges(k);
cell.raw=raw;
// parse first so we know refs even if eval fails
if(raw.trim()[0]==='='){
try{ cell.ast=parseFormula(raw.trim().slice(1)); }
catch(e){ cell.ast=null; }
} else cell.ast=null;
if(cell.ast){
const refs=new Set(); astRefs(cell.ast,refs);
for(const t of refs) addEdge(k,t);
}
recalcFrom(k);
}
/* ============================================================
* STRUCTURE OPS: insert/delete rows & columns with rewriting
* ============================================================ */
function transformAst(ast,fn){
// fn(refObj) mutates or marks deleted
let changed=false;
function walk(n){
switch(n.t){
case 'ref': {
const res=fn(n.r,n.c);
if(res===null){ n.t='referr'; delete n.r; delete n.c; changed=true; }
else { if(res.r!==n.r||res.c!==n.c)changed=true; n.r=res.r; n.c=res.c; }
break;
}
case 'rng': {
const ra=fn(n.a.r,n.a.c), rb=fn(n.b.r,n.b.c);
if(ra===null||rb===null){ n.t='referr'; delete n.a; delete n.b; changed=true; break; }
if(ra.r!==n.a.r||ra.c!==n.a.c||rb.r!==n.b.r||rb.c!==n.b.c)changed=true;
n.a=ra; n.b=rb;
if(n.a.r>n.b.r||n.a.c>n.b.c){ n.t='referr'; delete n.a; delete n.b; changed=true; }
break;
}
case 'neg': walk(n.e); break;
case 'bin': walk(n.l); walk(n.r); break;
case 'fn': n.args.forEach(walk); break;
}
}
walk(ast);
return changed;
}
function insertRow(at){
if(CFG.ROWS>=999) return;
const moved=new Map();
for(const [k,cell] of cells){
const [r,c]=k.split(',').map(Number);
moved.set(k,{r:r>=at?r+1:r,c,cell});
}
applyMove(moved);
for(const [,rec] of moved){
const cell=rec.cell;
if(cell.ast){
const ch=transformAst(cell.ast,(r,c)=>({r:r>=at?r+1:r,c}));
if(ch) cell.raw='='+ser(cell.ast);
}
}
rebuildAndRecalc();
}
function deleteRow(at){
const moved=new Map();
for(const [k,cell] of cells){
const [r,c]=k.split(',').map(Number);
if(r===at) continue;
moved.set(k,{r:r>at?r-1:r,c,cell});
}
applyMove(moved);
for(const [,rec] of moved){
const cell=rec.cell;
if(cell.ast){
const ch=transformAst(cell.ast,(r,c)=>r===at?null:(r>at?{r:r-1,c}:{r,c}));
if(ch) cell.raw='='+ser(cell.ast);
}
}
rebuildAndRecalc();
}
function insertCol(at){
const moved=new Map();
for(const [k,cell] of cells){
const [r,c]=k.split(',').map(Number);
moved.set(k,{r,c:c>=at?c+1:c,cell});
}
applyMove(moved);
for(const [,rec] of moved){
const cell=rec.cell;
if(cell.ast){
const ch=transformAst(cell.ast,(r,c)=>({r,c:c>=at?c+1:c}));
if(ch) cell.raw='='+ser(cell.ast);
}
}
rebuildAndRecalc();
}
function deleteCol(at){
const moved=new Map();
for(const [k,cell] of cells){
const [r,c]=k.split(',').map(Number);
if(c===at) continue;
moved.set(k,{r,c:c>at?c-1:c,cell});
}
applyMove(moved);
for(const [,rec] of moved){
const cell=rec.cell;
if(cell.ast){
const ch=transformAst(cell.ast,(r,c)=>c===at?null:(c>at?{r:r,c:c-1}:{r:r,c:c}));
if(ch) cell.raw='='+ser(cell.ast);
}
}
rebuildAndRecalc();
}
/* move cell records to new coordinates without recomputing */
function applyMove(moved){
const oldCells=cells;
cells=new Map();
prec=new Map(); dep=new Map();
for(const [,rec] of moved){
if(!inGrid(rec.r,rec.c)) continue;
cells.set(key(rec.r,rec.c),rec.cell);
}
// rebuild graph
for(const [k,cell] of cells){
if(cell.ast){
const refs=new Set(); astRefs(cell.ast,refs);
for(const t of refs) addEdge(k,t);
}
}
}
function rebuildAndRecalc(){
prec=new Map(); dep=new Map();
for(const [k,cell] of cells){
if(cell.ast){
const refs=new Set(); astRefs(cell.ast,refs);
for(const t of refs) addEdge(k,t);
}
}
buildTable();
fullRecalc();
}
/* ============================================================
* PERSISTENCE
* ============================================================ */
let saveTimer=null;
function scheduleSave(){
clearTimeout(saveTimer);
saveTimer=setTimeout(saveSheet,300);
}
function saveSheet(){
const o={};
for(const [k,cell] of cells) if(cell.raw!=='') o[k]=cell.raw;
try{ localStorage.setItem(CFG.STORAGE_KEY,JSON.stringify(o)); }catch(e){}
}
function loadSheet(){
let o=null;
try{ o=JSON.parse(localStorage.getItem(CFG.STORAGE_KEY)); }catch(e){}
if(!o||typeof o!=='object')return false;
for(const [k,raw] of Object.entries(o)){
const [r,c]=k.split(',').map(Number);
if(!inGrid(r,c))continue;
const cell={raw:String(raw),ast:null,value:''};
cells.set(k,cell);
}
return true;
}
/* ============================================================
* UI: GRID RENDERING & INTERACTION
* ============================================================ */
const thead=document.getElementById('thead');
const tbody=document.getElementById('tbody');
const gridWrap=document.getElementById('grid-wrap');
const cellInput=document.getElementById('cell-input');
const fbar=document.getElementById('fbar');
const cellRefBox=document.getElementById('cell-ref');
const ctxMenu=document.getElementById('ctxmenu');
let sel={r:0,c:0};
let editing=false;
let editSource=null; // 'cell' | 'fbar'
function buildTable(){
let h='<tr><th></th>';
for(let c=0;c<CFG.COLS;c++) h+=`<th data-c="${c}">${colName(c)}</th>`;
h+='</tr>';
thead.innerHTML=h;
let b='';
for(let r=0;r<CFG.ROWS;r++){
b+=`<tr><th data-r="${r}">${r+1}</th>`;
for(let c=0;c<CFG.COLS;c++) b+=`<td data-r="${r}" data-c="${c}"></td>`;
b+='</tr>';
}
tbody.innerHTML=b;
}
function fmtVal(v){
if(typeof v==='number'){
// avoid float noise like 0.30000000000000004
if(Number.isInteger(v))return String(v);
return String(parseFloat(v.toPrecision(12)));
}
return String(v);
}
function renderCellKey(k){
const [r,c]=k.split(',').map(Number);
renderCell(r,c);
}
function renderCell(r,c){
const td=tdAt(r,c);
if(!td)return;
const cell=cells.get(key(r,c));
const v=cell?cell.value:'';
td.textContent=fmtVal(v);
td.classList.toggle('num',typeof v==='number');
td.classList.toggle('err',isErr(v));
}
function renderAll(){
for(const [k] of cells) renderCellKey(k);
// clear stale displays
tbody.querySelectorAll('td.num,td.err').forEach(td=>{
const k=key(+td.dataset.r,+td.dataset.c);
if(!cells.has(k)){ td.classList.remove('num','err'); td.textContent=''; }
});
updateSel();
}
const tdAt=(r,c)=>tbody.querySelector(`td[data-r="${r}"][data-c="${c}"]`);
function updateSel(){
tbody.querySelectorAll('td.sel').forEach(td=>td.classList.remove('sel'));
const td=tdAt(sel.r,sel.c);
if(td){
td.classList.add('sel');
const rect=td.getBoundingClientRect();
cellInput.style.left=(rect.left)+'px';
cellInput.style.top=(rect.top)+'px';
cellInput.style.width=rect.width+'px';
cellInput.style.height=rect.height+'px';
}
cellRefBox.textContent=colName(sel.c)+(sel.r+1);
if(!editing){
const cell=cells.get(key(sel.r,sel.c));
fbar.value=cell?cell.raw:'';
}
}
function selectCell(r,c){
if(editing)commitEdit(null);
sel={r:Math.max(0,Math.min(CFG.ROWS-1,r)),c:Math.max(0,Math.min(CFG.COLS-1,c))};
updateSel();
const td=tdAt(sel.r,sel.c);
if(td)td.scrollIntoView({block:'nearest',inline:'nearest'});
}
/* --- editing --- */
function startEdit(initial,source){
editing=true; editSource=source;
const cell=cells.get(key(sel.r,sel.c));
const cur=cell?cell.raw:'';
const val=initial!=null?initial:cur;
fbar.value=val;
if(source==='cell'){
cellInput.style.display='block';
cellInput.value=val;
cellInput.focus();
cellInput.setSelectionRange(val.length,val.length);
} else {
fbar.focus();
}
}
function commitEdit(move){ // move: 'down'|'right'|null
if(!editing)return;
editing=false; editSource=null;
cellInput.style.display='none';
const raw=fbar.value;
setCellRaw(sel.r,sel.c,raw);
fbar.value=raw;
if(move==='down')selectCell(sel.r+1,sel.c);
else if(move==='right')selectCell(sel.r,sel.c+1);
else updateSel();
gridWrap.focus?.();
}
function cancelEdit(){
editing=false; editSource=null;
cellInput.style.display='none';
updateSel();
}
/* --- events --- */
tbody.addEventListener('mousedown',e=>{
const td=e.target.closest('td');
if(!td)return;
selectCell(+td.dataset.r,+td.dataset.c);
e.preventDefault();
});
tbody.addEventListener('dblclick',e=>{
const td=e.target.closest('td');
if(!td)return;
selectCell(+td.dataset.r,+td.dataset.c);
startEdit(null,'cell');
});
thead.addEventListener('click',e=>{
const th=e.target.closest('th[data-c]');
if(th)selectCell(0,+th.dataset.c);
});
tbody.addEventListener('click',e=>{
const th=e.target.closest('th[data-r]');
if(th)selectCell(+th.dataset.r,0);
});
document.addEventListener('keydown',e=>{
const inInput=e.target===cellInput||e.target===fbar;
if(editing){
if(e.key==='Enter'){ e.preventDefault(); commitEdit('down'); }
else if(e.key==='Tab'){ e.preventDefault(); commitEdit('right'); }
else if(e.key==='Escape'){ e.preventDefault(); cancelEdit(); }
return;
}
if(inInput)return;
switch(e.key){
case 'ArrowUp': e.preventDefault(); selectCell(sel.r-1,sel.c); break;
case 'ArrowDown': e.preventDefault(); selectCell(sel.r+1,sel.c); break;
case 'ArrowLeft': e.preventDefault(); selectCell(sel.r,sel.c-1); break;
case 'ArrowRight': e.preventDefault(); selectCell(sel.r,sel.c+1); break;
case 'Enter': e.preventDefault(); startEdit(null,'cell'); break;
case 'Delete': case 'Backspace':
e.preventDefault(); setCellRaw(sel.r,sel.c,''); updateSel(); break;
case 'F2': e.preventDefault(); startEdit(null,'cell'); break;
default:
if(e.key.length===1&&!e.ctrlKey&&!e.metaKey&&!e.altKey){
e.preventDefault(); startEdit(e.key,'cell');
}
}
});
/* formula bar <-> cell editor sync */
fbar.addEventListener('input',()=>{
if(editing&&editSource==='cell')cellInput.value=fbar.value;
else if(!editing){ startEdit(fbar.value,'fbar'); }
});
fbar.addEventListener('keydown',e=>{
if(e.key==='Enter'){ e.preventDefault(); commitEdit('down'); }
if(e.key==='Escape'){ e.preventDefault(); cancelEdit(); }
});
cellInput.addEventListener('input',()=>{ fbar.value=cellInput.value; });
cellInput.addEventListener('blur',()=>{ if(editing&&editSource==='cell')commitEdit(null); });
window.addEventListener('resize',updateSel);
/* toolbar */
document.getElementById('btn-ir').onclick=()=>insertRow(sel.r);
document.getElementById('btn-dr').onclick=()=>deleteRow(sel.r);
document.getElementById('btn-ic').onclick=()=>insertCol(sel.c);
document.getElementById('btn-dc').onclick=()=>deleteCol(sel.c);
/* header context menus */
function showMenu(x,y,items){
ctxMenu.innerHTML='';
for(const it of items){
const d=document.createElement('div');
d.textContent=it.label;
if(it.danger)d.className='danger';
d.onclick=()=>{ hideMenu(); it.action(); };
ctxMenu.appendChild(d);
}
ctxMenu.style.display='block';
ctxMenu.style.left=x+'px';
ctxMenu.style.top=y+'px';
}
function hideMenu(){ ctxMenu.style.display='none'; }
document.addEventListener('mousedown',e=>{
if(!ctxMenu.contains(e.target))hideMenu();
});
thead.addEventListener('contextmenu',e=>{
const th=e.target.closest('th[data-c]');
if(!th)return;
e.preventDefault();
const c=+th.dataset.c;
selectCell(0,c);
showMenu(e.clientX,e.clientY,[
{label:'Insert 1 column left',action:()=>insertCol(c)},
{label:'Insert 1 column right',action:()=>insertCol(c+1)},
{label:'Delete column',danger:true,action:()=>deleteCol(c)},
]);
});
tbody.addEventListener('contextmenu',e=>{
const th=e.target.closest('th[data-r]');
if(!th)return;
e.preventDefault();
const r=+th.dataset.r;
selectCell(r,0);
showMenu(e.clientX,e.clientY,[
{label:'Insert 1 row above',action:()=>insertRow(r)},
{label:'Insert 1 row below',action:()=>insertRow(r+1)},
{label:'Delete row',danger:true,action:()=>deleteRow(r)},
]);
});
/* ============================================================
* INIT
* ============================================================ */
buildTable();
loadSheet();
rebuildAndRecalc();
updateSel();
</script>
</body>
</html>
<!-- agent-meta {"model":"stealth/ox-alpha","provider":"openrouter","persona":"full","sessionId":"b211945f-94a0-441e-9b3e-b127fdce49be","tokensIn":376822,"tokensOut":14100,"tokensTotal":390922,"cost":0,"turns":15,"toolCalls":14,"failedToolCalls":0,"timestamp":"2026-08-21T17:18:43.382Z"} -->