gojiteji's picture
Update index.html
15a2fee verified
Raw
History Blame Contribute Delete
78.6 kB
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>fableCalc v1.0</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<style>
:root{--green:#1D6F42;--green2:#2E8B57;--bd:#d0d7de;--grid:#e3e8ee;--hd:#f6f8fa;--hd2:#eaf4ee;--sel:rgba(29,111,66,.12);--text:#24292f}
*{box-sizing:border-box;margin:0;padding:0}
html{height:100%;background:#f1f5f3}
body{font-family:"Segoe UI","Yu Gothic UI",system-ui,sans-serif;font-size:13px;height:100vh;display:flex;flex-direction:column;overflow:hidden;background:#fff;color:var(--text)}
button,input,select,textarea{font:inherit}
button{color:inherit}
#titlebar{background:linear-gradient(180deg,#217a49 0%,var(--green) 100%);color:#fff;padding:6px 12px;display:flex;align-items:center;gap:10px;min-height:34px}
#titlebar .name{font-weight:600;font-size:14px}
#titlebar .file{font-size:12px;opacity:.85}
#qat{display:flex;gap:2px}
#qat button{background:none;border:1px solid transparent;cursor:pointer;padding:3px 6px;border-radius:4px}
#qat button:hover{background:rgba(255,255,255,.2)}
#qat svg{stroke:#fff}
#tabstrip{display:flex;background:var(--green);padding:0 8px;gap:2px}
#tabstrip .rtab{color:#fff;font-size:12.5px;padding:6px 16px;cursor:pointer;border-radius:4px 4px 0 0}
#tabstrip .rtab.on{background:#fff;color:var(--green);font-weight:600}
#tabstrip .rtab:hover:not(.on){background:rgba(255,255,255,.15)}
#ribbon{display:flex;padding:6px 8px 3px;border-bottom:1px solid var(--bd);background:#fff;min-height:80px;box-shadow:0 1px 2px rgba(27,31,36,.05)}
.rgrp{display:flex;flex-direction:column;padding:0 10px;border-right:1px solid #e3e3e3}
.rgrp .tools{display:flex;gap:2px;align-items:flex-start;flex:1}
.rgrp .cap{font-size:10.5px;color:#777;text-align:center;padding:2px 0 1px}
.rbtn{display:flex;flex-direction:column;align-items:center;gap:2px;border:1px solid transparent;background:none;padding:4px 7px;border-radius:3px;cursor:pointer;font-size:11px;color:#333;min-width:44px}
.rbtn:hover{border-color:#c8c8c8;background:#f3f9f5}
.rbtn.big svg{width:26px;height:26px}
.rbtn svg{width:18px;height:18px;stroke:#333;fill:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}
.sbtn{border:1px solid transparent;background:none;padding:3px 6px;border-radius:3px;cursor:pointer;display:flex;align-items:center;gap:3px;font-size:12px}
.sbtn:hover{border-color:#c8c8c8;background:#f3f9f5}
.sbtn svg{width:16px;height:16px;stroke:#333;fill:none;stroke-width:1.5;stroke-linecap:round}
.scol{display:flex;flex-direction:column;gap:1px;justify-content:center}
.fbtn{font-family:Georgia,serif;font-size:14px;width:26px;height:24px;border:1px solid transparent;background:none;border-radius:3px;cursor:pointer}
.fbtn:hover{border-color:#c8c8c8;background:#f3f9f5}
.colorbtn{position:relative;width:28px;height:24px;border:1px solid transparent;border-radius:3px;background:none;cursor:pointer;font-family:Georgia,serif;font-size:13px;padding:0;display:inline-flex;align-items:center;justify-content:center;line-height:1}
.colorbtn:hover{border-color:#c8c8c8;background:#f3f9f5}
.colorbtn .bar{position:absolute;left:5px;right:5px;bottom:3px;height:4px;border:1px solid #999}
.colorbtn input{display:none}
#colorPalette{position:fixed;z-index:100;display:none;background:#fff;border:1px solid var(--bd);border-radius:6px;box-shadow:0 8px 24px rgba(27,31,36,.18);padding:8px;width:204px}
#colorPalette .ptitle{font-size:11px;color:#666;margin:0 0 6px}
#colorPalette .pgrid{display:grid;grid-template-columns:repeat(8,20px);gap:4px}
#colorPalette button.sw{width:20px;height:20px;border:1px solid #9ca3af;border-radius:3px;background:var(--c);cursor:pointer}
#colorPalette button.sw:hover{outline:2px solid var(--green);outline-offset:1px}
#colorPalette .pactions{display:flex;gap:6px;margin-top:8px}
#colorPalette .pactions button{font-size:11px;border:1px solid var(--bd);background:#fff;border-radius:4px;padding:3px 6px;cursor:pointer}
#fxbar{display:flex;align-items:center;gap:6px;padding:6px 10px;border-bottom:1px solid var(--bd);background:#fbfcfd}
#cellname{width:70px;border:1px solid var(--bd);border-radius:4px;padding:4px 6px;font-family:Consolas,monospace;font-size:12px;text-align:center;background:#fff}
#fx{flex:1;border:1px solid var(--bd);border-radius:4px;padding:4px 8px;font-family:Consolas,monospace;font-size:13px;background:#fff;min-height:27px}
#cellname:focus,#fx:focus,#sidepanel select:focus,#sidepanel textarea:focus{outline:none;border-color:var(--green);box-shadow:0 0 0 2px rgba(29,111,66,.15)}
#formulaHelp{position:absolute;left:198px;top:0;transform:translateY(100%);z-index:40;display:none;min-width:260px;max-width:520px;background:#fff;border:1px solid var(--bd);box-shadow:0 6px 18px rgba(27,31,36,.16);border-radius:6px;padding:6px 8px;font-size:12px;color:#4b5563}
#formulaHelp .fname{font-weight:700;color:#1f2328}
#formulaHelp .args{font-family:Consolas,monospace;color:#57606a;margin-top:2px}
#formulaHelp .suggest{display:inline-block;margin:3px 4px 0 0;padding:2px 6px;border-radius:4px;background:#eef6f0;color:var(--green);font-weight:600}
#main{flex:1;display:flex;overflow:hidden}
#gridwrap{flex:1;overflow:auto;position:relative;background:#fff}
#grid{border-collapse:separate;border-spacing:0;table-layout:fixed;user-select:none;-webkit-user-select:none;background:#fff}
th{background:linear-gradient(#f9fafb,var(--hd));border:1px solid var(--bd);border-width:0 1px 1px 0;font-weight:400;color:#4b5563;font-size:var(--head-font,12px);position:sticky;z-index:3;user-select:none}
thead th{top:0;height:var(--head-height,24px);position:sticky}
tbody th{left:0;width:42px;text-align:center;position:sticky;z-index:2}
thead th:first-child{left:0;z-index:5}
thead th[data-c],tbody th[data-r]{cursor:pointer}
thead th[data-c]:hover,tbody th[data-r]:hover{background:var(--hd2);color:var(--green)}
thead th.hsel,tbody th.hsel{background:#dcefe5;color:var(--green);font-weight:600}
td{border:1px solid var(--grid);border-width:0 1px 1px 0;padding:0 var(--cell-pad-x,8px);width:auto;max-width:none;height:auto;line-height:var(--cell-line,20px);overflow:hidden;white-space:nowrap;cursor:cell;background:#fff;color:#1f2328;font-size:var(--grid-font,13px)}
td.sel{background:transparent;border-color:#d8e3dc}
td.formula-ref{background:rgba(55,138,221,.055)}
td.formula-ref.ref2{background:rgba(239,159,39,.065)}
td.formula-ref.ref3{background:rgba(127,119,221,.06)}
td.formula-ref.ref4{background:rgba(216,90,48,.055)}
td.cur{box-shadow:inset 0 0 0 1px rgba(29,111,66,.22)}
#selbox{position:absolute;border:2px solid var(--green);pointer-events:none;z-index:4;display:none;box-sizing:border-box;box-shadow:0 0 0 1px rgba(29,111,66,.18)}
#refboxes{position:absolute;inset:0;pointer-events:none;z-index:3}
.refbox{position:absolute;box-sizing:border-box;border:2px solid #378ADD;background:rgba(55,138,221,.035)}
.refbox.ref2{border-color:#EF9F27;background:rgba(239,159,39,.04)}
.refbox.ref3{border-color:#7F77DD;background:rgba(127,119,221,.04)}
.refbox.ref4{border-color:#D85A30;background:rgba(216,90,48,.035)}
.refbox::after{content:"";position:absolute;width:6px;height:6px;right:-4px;bottom:-4px;background:currentColor;border:1px solid #fff}
#grid{user-select:none;-webkit-user-select:none}
td input{user-select:text;-webkit-user-select:text}
#fillh{position:absolute;width:7px;height:7px;background:var(--green);border:1px solid #fff;right:-5px;bottom:-5px;cursor:crosshair;pointer-events:auto}
#ctx{position:fixed;background:#fff;border:1px solid #bbb;box-shadow:2px 3px 8px rgba(0,0,0,.2);z-index:99;display:none;min-width:180px;padding:3px 0;font-size:13px}
#ctx div{padding:5px 14px;cursor:pointer}
#ctx div:hover{background:var(--sel)}
#ctx hr{border:none;border-top:1px solid #e0e0e0;margin:3px 0}
thead th{top:0}
.cres{position:absolute;right:-3px;top:0;width:6px;height:100%;cursor:col-resize;z-index:5}
.rres{position:absolute;left:0;bottom:-3px;height:6px;width:100%;cursor:row-resize;z-index:5}
tbody th{position:sticky;left:0}
td input{width:100%;height:100%;border:none;outline:none;font:inherit;background:#fff;padding:0 1px;line-height:var(--cell-line,20px)}
#sidepanel{width:0;border-left:1px solid var(--bd);overflow:auto;transition:width .15s;background:#fafafa}
#sidepanel.open{width:380px;padding:12px}
#sheetbar{display:flex;align-items:center;gap:2px;border-top:1px solid var(--bd);background:var(--hd);padding:2px 8px}
#sheetbar .tab{padding:4px 14px;font-size:12px;cursor:pointer;border:1px solid transparent;border-bottom:none;border-radius:4px 4px 0 0}
#sheetbar .tab.on{background:#fff;border-color:var(--bd);font-weight:600;color:var(--green)}
#status{margin-left:auto;font-size:12px;color:#555;display:flex;gap:16px}
#zoomctl{display:flex;align-items:center;gap:6px;color:#555}
#zoomctl button{width:22px;height:22px;border:1px solid var(--bd);border-radius:4px;background:#fff;cursor:pointer;line-height:18px}
#zoomctl button:hover{border-color:var(--green);color:var(--green)}
#zoomctl input{width:96px;accent-color:var(--green)}
#zoompct{min-width:42px;text-align:right;font-variant-numeric:tabular-nums}
.mini{font-size:11px;color:#777}
#sidepanel h3{font-size:13px;margin-bottom:8px;color:var(--green)}
#sidepanel textarea{width:100%;height:160px;font-family:Consolas,monospace;font-size:12px;border:1px solid var(--bd);border-radius:6px;padding:8px;background:#fff}
#sidepanel select,#sidepanel button{font-size:12px;padding:5px 9px;margin:2px 0;border:1px solid var(--bd);border-radius:5px;background:#fff;cursor:pointer}
#sidepanel button.go{background:var(--green);color:#fff;border-color:var(--green)}
.pvt td,.pvt th{position:static;font-size:12px;padding:3px 8px}
</style>
</head>
<body>
<div id="titlebar">
<div id="qat">
<button data-cmd="undo" title="元に戻す (Ctrl+Z)"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke-width="2"><path d="M9 14 4 9l5-5"/><path d="M4 9h10a6 6 0 0 1 0 12h-3"/></svg></button>
<button data-cmd="redo" title="やり直す (Ctrl+Y)"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke-width="2"><path d="m15 14 5-5-5-5"/><path d="M20 9H10a6 6 0 0 0 0 12h3"/></svg></button>
</div>
<span class="name">fableCalc</span><span class="file" id="fname">Book1</span>
<span style="flex:1"></span><span class="mini" style="color:#cde">v1.4 — Linux native (browser runtime)</span>
</div>
<div id="tabstrip">
<span class="rtab" data-tab="file" style="background:rgba(255,255,255,.12)">ファイル</span>
<span class="rtab on" data-tab="home">ホーム</span>
<span class="rtab" data-tab="insert">挿入</span>
</div>
<div id="ribbon">
<div id="rb-home" style="display:flex">
<div class="rgrp">
<div class="tools">
<button class="rbtn big" data-cmd="paste" title="貼り付け (Ctrl+V)"><svg viewBox="0 0 24 24"><rect x="5" y="4" width="14" height="17" rx="1"/><rect x="9" y="2" width="6" height="4" rx="1" fill="#fff"/><path d="M9 11h6M9 14h6M9 17h4"/></svg>貼り付け</button>
<div class="scol">
<button class="sbtn" data-cmd="cut" title="切り取り (Ctrl+X)"><svg viewBox="0 0 24 24"><circle cx="6" cy="18" r="2.5"/><circle cx="18" cy="18" r="2.5"/><path d="M7.8 16 18 4M16.2 16 6 4"/></svg>切り取り</button>
<button class="sbtn" data-cmd="copy" title="コピー (Ctrl+C)"><svg viewBox="0 0 24 24"><rect x="8" y="8" width="12" height="13" rx="1"/><path d="M5 16H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h11a1 1 0 0 1 1 1v1"/></svg>コピー</button>
</div>
</div>
<div class="cap">クリップボード</div>
</div>
<div class="rgrp">
<div class="tools" style="align-items:center">
<select id="fsize" title="フォントサイズ" style="height:24px;font-size:12px;border:1px solid #c8c8c8;border-radius:3px"><option>10</option><option selected>13</option><option>16</option><option>20</option><option>26</option></select>
<button class="fbtn" data-cmd="bold" title="太字 (Ctrl+B)" style="font-weight:700">B</button>
<button class="fbtn" data-cmd="italic" title="斜体 (Ctrl+I)" style="font-style:italic">I</button>
<button class="fbtn" data-cmd="border" title="罫線"><svg viewBox="0 0 24 24" width="16" height="16" style="stroke:#333;fill:none;stroke-width:1.5"><rect x="4" y="4" width="16" height="16"/><path d="M12 4v16M4 12h16"/></svg></button>
<span class="colorbtn" title="塗りつぶしの色"><svg viewBox="0 0 24 24" width="17" height="17" style="position:absolute;left:5px;top:0;stroke:#333;fill:none;stroke-width:1.5"><path d="m7 11 6-6 6 6-6 6a4.2 4.2 0 0 1-6-6z"/><path d="M13 5 11 3"/><path d="M20 16s1.5 2 1.5 3.2a1.5 1.5 0 0 1-3 0C18.5 18 20 16 20 16z" fill="#333"/></svg><span class="bar" id="bgbar" style="background:#ffff00"></span><input type="color" id="bgcol" value="#ffff00"></span>
<span class="colorbtn" title="フォントの色">A<span class="bar" id="fgbar" style="background:#c00000"></span><input type="color" id="fgcol" value="#c00000"></span>
</div>
<div class="cap">フォント</div>
</div>
<div class="rgrp">
<div class="tools" style="align-items:center">
<button class="fbtn" data-cmd="alleft" title="左揃え"><svg viewBox="0 0 24 24" width="15" height="15" style="stroke:#333;fill:none;stroke-width:1.8"><path d="M4 6h16M4 10h10M4 14h16M4 18h10"/></svg></button>
<button class="fbtn" data-cmd="alcenter" title="中央揃え"><svg viewBox="0 0 24 24" width="15" height="15" style="stroke:#333;fill:none;stroke-width:1.8"><path d="M4 6h16M7 10h10M4 14h16M7 18h10"/></svg></button>
<button class="fbtn" data-cmd="alright" title="右揃え"><svg viewBox="0 0 24 24" width="15" height="15" style="stroke:#333;fill:none;stroke-width:1.8"><path d="M4 6h16M10 10h10M4 14h16M10 18h10"/></svg></button>
</div>
<div class="cap">配置</div>
</div>
<div class="rgrp">
<div class="tools" style="align-items:center">
<button class="fbtn" data-cmd="comma" title="桁区切りスタイル" style="width:30px">,</button>
<button class="fbtn" data-cmd="percent" title="パーセント スタイル" style="width:30px;font-family:inherit">%</button>
<button class="fbtn" data-cmd="decdec" title="小数点以下の表示桁数を減らす" style="width:34px;font-size:11px;font-family:inherit">.0</button>
<button class="fbtn" data-cmd="decinc" title="小数点以下の表示桁数を増やす" style="width:34px;font-size:11px;font-family:inherit">.00</button>
<button class="fbtn" data-cmd="clearfmt" title="書式のクリア" style="width:34px;font-size:11px;font-family:inherit">消去</button>
</div>
<div class="cap">数値</div>
</div>
<div class="rgrp" style="border-right:none">
<div class="tools">
<button class="rbtn" data-cmd="autosum" title="オートSUM"><svg viewBox="0 0 24 24"><path d="M18 5H7l6 7-6 7h11"/></svg>合計</button>
<button class="rbtn" data-cmd="sortd" title="降順で並べ替え"><svg viewBox="0 0 24 24"><path d="M4 6h9M4 12h6M4 18h3"/><path d="M18 6v12m0 0 3-3m-3 3-3-3"/></svg>並べ替え</button>
</div>
<div class="cap">編集</div>
</div>
</div>
<div id="rb-insert" style="display:none">
<div class="rgrp">
<div class="tools">
<button class="rbtn big" data-cmd="chart" title="グラフの挿入"><svg viewBox="0 0 24 24"><path d="M4 20V4"/><path d="M4 20h16"/><rect x="7" y="12" width="3" height="8"/><rect x="12" y="7" width="3" height="13"/><rect x="17" y="10" width="3" height="10"/></svg>グラフ</button>
<button class="rbtn big" data-cmd="pivot" title="ピボットテーブル"><svg viewBox="0 0 24 24"><rect x="4" y="4" width="16" height="16"/><path d="M4 9h16M9 4v16"/></svg>ピボット</button>
</div>
<div class="cap">テーブル・グラフ</div>
</div>
<div class="rgrp" style="border-right:none">
<div class="tools">
<button class="rbtn big" data-cmd="macro" title="マクロ"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.9 4.9l2.1 2.1M17 17l2.1 2.1M19.1 4.9 17 7M7 17l-2.1 2.1"/></svg>マクロ</button>
</div>
<div class="cap">コード</div>
</div>
</div>
<div id="rb-file" style="display:none">
<div class="rgrp" style="border-right:none">
<div class="tools">
<button class="rbtn big" data-cmd="open" title="開く"><svg viewBox="0 0 24 24"><path d="M3 7V5a1 1 0 0 1 1-1h5l2 2h9a1 1 0 0 1 1 1v2"/><path d="M3 7h18l-2 12H5L3 7z"/></svg>開く</button>
<button class="rbtn big" data-cmd="savex" title="xlsx形式で保存 (Ctrl+S)"><svg viewBox="0 0 24 24"><path d="M5 3h12l4 4v14H5V3z"/><rect x="8" y="3" width="7" height="5"/><rect x="8" y="13" width="8" height="8"/></svg>xlsx保存</button>
<button class="rbtn big" data-cmd="savec" title="CSV形式で保存"><svg viewBox="0 0 24 24"><path d="M6 2h9l5 5v15H6V2z"/><path d="M15 2v5h5"/><path d="M9 13h6M9 17h6"/></svg>CSV保存</button>
</div>
<div class="cap">ファイル</div>
</div>
</div>
<input type="file" id="filein" accept=".xlsx,.csv" style="display:none">
</div>
<div id="fxbar" style="position:relative">
<input id="cellname" value="A1" readonly><span class="mini">fx</span>
<input id="fx" placeholder="=VLOOKUP(A2,D:F,2,FALSE)" spellcheck="false">
<div id="formulaHelp"></div>
</div>
<div id="main">
<div id="gridwrap"><table id="grid"><colgroup id="cols"></colgroup><thead></thead><tbody></tbody></table><div id="refboxes"></div><div id="selbox"><div id="fillh"></div></div></div>
<div id="ctx"></div>
<div id="sidepanel"></div>
</div>
<div id="sheetbar">
<div id="tabs"></div>
<button id="addsheet" style="border:none;background:none;cursor:pointer;font-size:14px"></button>
<div id="status"><span id="aggr"></span><span id="msg">準備完了</span><span id="zoomctl"><button id="zminus" title="縮小"></button><input id="zoomrange" type="range" min="25" max="200" step="5" value="100" title="ズーム"><button id="zplus" title="拡大"></button><span id="zoompct">100%</span></span></div>
</div>
<div id="colorPalette"></div>
<script>
"use strict";
const COLS=52, ROWS=512;
const colL=i=>{let s="",n=i+1;while(n>0){const r=(n-1)%26;s=String.fromCharCode(65+r)+s;n=Math.floor((n-1)/26);}return s;};
const colN=s=>String(s).toUpperCase().split("").reduce((n,ch)=>n*26+ch.charCodeAt(0)-64,0)-1;
const A=(c,r)=>colL(c)+r;
const parseA=a=>{const m=a.match(/^\$?([A-Z]+)\$?(\d+)$/);return m?[colN(m[1]),+m[2]]:null;};
// ---- workbook state ----
let wb={sheets:{Sheet1:{cells:{},fmt:{}}},order:["Sheet1"],active:"Sheet1"};
const sh=()=>wb.sheets[wb.active];
let cur=[0,1], anchor=[0,1], selEnd=[0,1]; // [col,row]
let undoStack=[],redoStack=[];
let zoom=1;
let formulaRefs=[];
const snap=()=>JSON.stringify(sh());
function pushUndo(){undoStack.push(snap());if(undoStack.length>100)undoStack.shift();redoStack=[];}
function undo(){if(!undoStack.length)return;redoStack.push(snap());wb.sheets[wb.active]=JSON.parse(undoStack.pop());renderAll();msg("元に戻しました");}
function redo(){if(!redoStack.length)return;undoStack.push(snap());wb.sheets[wb.active]=JSON.parse(redoStack.pop());renderAll();msg("やり直しました");}
const msg=t=>document.getElementById("msg").textContent=t;
function selRect(){const[c1,r1]=anchor,[c2,r2]=selEnd;return[Math.min(c1,c2),Math.min(r1,r2),Math.max(c1,c2),Math.max(r1,r2)];}
function inSel(c,r){const[a,b,x,y]=selRect();return c>=a&&c<=x&&r>=b&&r<=y;}
function refClass(c,r){
for(let i=0;i<formulaRefs.length;i++){
const q=formulaRefs[i];
if(c>=q.a&&c<=q.x&&r>=q.b&&r<=q.y)return " formula-ref ref"+((i%4)+1);
}
return "";
}
function updateHeaders(){
const[a,b,x,y]=selRect();
thead.querySelectorAll("th[data-c]").forEach(th=>{
const c=+th.dataset.c;
th.classList.toggle("hsel",c>=a&&c<=x);
});
tbody.querySelectorAll("th[data-r]").forEach(th=>{
const r=+th.dataset.r;
th.classList.toggle("hsel",r>=b&&r<=y);
});
}
// ---- rendering ----
const tbody=document.querySelector("#grid tbody"), thead=document.querySelector("#grid thead"), colg=document.getElementById("cols");
function colw(){sh().colw=sh().colw||{};return sh().colw;}
function rowh(){sh().rowh=sh().rowh||{};return sh().rowh;}
function z(v){return Math.max(1,Math.round(v*zoom));}
function buildGrid(){
let cg='<col id="rowheadcol" style="width:'+z(42)+'px">';
for(let c=0;c<COLS;c++)cg+='<col id="col'+c+'" style="width:'+z(colw()[c]||88)+'px">';
colg.innerHTML=cg;
let h="<tr><th></th>";
for(let c=0;c<COLS;c++)h+='<th data-c="'+c+'">'+colL(c)+'<span class="cres" data-c="'+c+'"></span></th>';
thead.innerHTML=h+"</tr>";
let b="";
for(let r=1;r<=ROWS;r++){
b+='<tr id="row'+r+'" style="height:'+z(rowh()[r]||22)+'px"><th data-r="'+r+'">'+r+'<span class="rres" data-r="'+r+'"></span></th>';
for(let c=0;c<COLS;c++)b+='<td id="c'+c+'_'+r+'"></td>';b+="</tr>";
}
tbody.innerHTML=b;
}
function applySizes(){
const rowHead=document.getElementById("rowheadcol");if(rowHead)rowHead.style.width=z(42)+"px";
document.documentElement.style.setProperty("--grid-font",Math.max(6,Math.round(13*zoom))+"px");
document.documentElement.style.setProperty("--head-font",Math.max(6,Math.round(12*zoom))+"px");
document.documentElement.style.setProperty("--cell-pad-x",Math.max(1,Math.round(8*zoom))+"px");
document.documentElement.style.setProperty("--cell-line",Math.max(6,Math.round(20*zoom))+"px");
document.documentElement.style.setProperty("--head-height",Math.max(8,Math.round(24*zoom))+"px");
for(let c=0;c<COLS;c++){const el=document.getElementById("col"+c);if(el)el.style.width=z(colw()[c]||88)+"px";}
for(let r=1;r<=ROWS;r++){const el=document.getElementById("row"+r);if(el)el.style.height=z(rowh()[r]||22)+"px";}
}
// ---- column/row resize ----
let resizing=null;
document.getElementById("grid").addEventListener("mousedown",e=>{
const cr=e.target.closest(".cres"), rr=e.target.closest(".rres");
if(cr){resizing={type:"c",i:+cr.dataset.c,start:e.clientX,base:colw()[+cr.dataset.c]||88};e.preventDefault();e.stopPropagation();}
else if(rr){resizing={type:"r",i:+rr.dataset.r,start:e.clientY,base:rowh()[+rr.dataset.r]||22};e.preventDefault();e.stopPropagation();}
},true);
document.addEventListener("mousemove",e=>{
if(!resizing)return;
if(resizing.type==="c"){colw()[resizing.i]=Math.max(28,resizing.base+e.clientX-resizing.start);}
else{rowh()[resizing.i]=Math.max(14,resizing.base+e.clientY-resizing.start);}
applySizes();positionSelbox();
});
document.addEventListener("mouseup",()=>{if(resizing){resizing=null;msg("サイズを変更しました");}});
function fmtNum(v,f){
if(typeof v!=="number")return v;
const dec=f&&f.dec!==undefined?f.dec:undefined;
let n=f&&f.percent?v*100:v;
let s;
if(dec!==undefined){
s=(f&&f.comma)?n.toLocaleString(undefined,{minimumFractionDigits:dec,maximumFractionDigits:dec}):n.toFixed(dec);
}else{
s=(f&&f.comma)?n.toLocaleString(undefined,{maximumFractionDigits:2}):String(Math.round(n*10000)/10000);
}
if(f&&f.percent)s+="%";
return s;
}
function paintCell(c,r){
const td=document.getElementById("c"+c+"_"+r);if(!td)return;
const a=A(c,r), f=sh().fmt[a]||{}, raw=sh().cells[a];
let v="";
if(raw!==undefined&&raw!==""){CALC.depth=0;v=String(raw)[0]==="="?CALC.evalCell(a):coerce(raw);}
td.textContent=fmtNum(v,f);
td.style.fontWeight=f.b?"700":"";td.style.fontStyle=f.i?"italic":"";
td.style.color=f.fg||"";td.style.backgroundColor=f.bg||"";
td.style.border=f.bd?"1px solid #666":"";
td.style.fontSize=f.fs?Math.max(6,Math.round(f.fs*zoom))+"px":"";
td.style.textAlign=f.al||((typeof v==="number")?"right":"left");
td.className=(inSel(c,r)?"sel":"")+(c===cur[0]&&r===cur[1]?" cur":"")+refClass(c,r);
}
function positionSelbox(){
const box=document.getElementById("selbox");
const[a,b,x,y]=selRect();
const t1=document.getElementById("c"+a+"_"+b),t2=document.getElementById("c"+x+"_"+y);
if(!t1||!t2){box.style.display="none";return;}
box.style.display="block";
box.style.left=(t1.offsetLeft-1)+"px";
box.style.top=(t1.offsetTop-1)+"px";
box.style.width=(t2.offsetLeft+t2.offsetWidth-t1.offsetLeft+1)+"px";
box.style.height=(t2.offsetTop+t2.offsetHeight-t1.offsetTop+1)+"px";
}
function positionRefBoxes(){
const wrap=document.getElementById("refboxes");
if(!wrap)return;
if(!formulaRefs.length){wrap.innerHTML="";return;}
wrap.innerHTML=formulaRefs.map((q,i)=>{
const t1=document.getElementById("c"+q.a+"_"+q.b),t2=document.getElementById("c"+q.x+"_"+q.y);
if(!t1||!t2)return "";
const left=t1.offsetLeft-1, top=t1.offsetTop-1;
const width=t2.offsetLeft+t2.offsetWidth-t1.offsetLeft+1;
const height=t2.offsetTop+t2.offsetHeight-t1.offsetTop+1;
return '<div class="refbox ref'+((i%4)+1)+'" style="left:'+left+'px;top:'+top+'px;width:'+width+'px;height:'+height+'px;color:'+["#378ADD","#EF9F27","#7F77DD","#D85A30"][i%4]+'"></div>';
}).join("");
}
let paintQueued=false;
function renderAll(){
if(paintQueued)return;paintQueued=true;
requestAnimationFrame(()=>{
paintQueued=false;
for(let r=1;r<=ROWS;r++)for(let c=0;c<COLS;c++)paintCell(c,r);
document.getElementById("cellname").value=A(...cur);
const fx=document.getElementById("fx");
if(document.activeElement!==fx)fx.value=sh().cells[A(...cur)]||"";
applySizes();positionSelbox();positionRefBoxes();updateHeaders();renderTabs();aggregate();
});
}
document.getElementById("gridwrap").addEventListener("scroll",()=>{positionSelbox();positionRefBoxes();});
function aggregate(){
const[a,b,x,y]=selRect();let n=0,s=0;
for(let c=a;c<=x;c++)for(let r=b;r<=y;r++){CALC.depth=0;const v=CALC.val(A(c,r));if(typeof v==="number"&&v!==0||sh().cells[A(c,r)]!==undefined&&typeof v==="number"){n++;s+=v;}}
document.getElementById("aggr").textContent=n>1?("合計:"+(Math.round(s*100)/100).toLocaleString()+" 平均:"+(Math.round(s/n*100)/100).toLocaleString()+" 件数:"+n):"";
}
function coerce(raw){const n=parseFloat(raw);return(!isNaN(n)&&String(raw).trim()!==""&&/^-?[\d.,]+%?$/.test(String(raw).trim().replace(/,/g,"")))?parseFloat(String(raw).replace(/,/g,"")):raw;}
function finishPreview(repaint){
if(repaint)renderAll();else positionRefBoxes();
}
const FORMULA_SIG={
SUM:"SUM(number1, [number2], ...)", AVERAGE:"AVERAGE(number1, [number2], ...)", COUNT:"COUNT(value1, [value2], ...)",
COUNTA:"COUNTA(value1, [value2], ...)", MIN:"MIN(number1, ...)", MAX:"MAX(number1, ...)", MEDIAN:"MEDIAN(number1, ...)",
IF:"IF(logical_test, value_if_true, value_if_false)", IFERROR:"IFERROR(value, value_if_error)", AND:"AND(logical1, ...)", OR:"OR(logical1, ...)",
ROUND:"ROUND(number, num_digits)", ROUNDUP:"ROUNDUP(number, num_digits)", ROUNDDOWN:"ROUNDDOWN(number, num_digits)",
COUNTIF:"COUNTIF(range, criteria)", SUMIF:"SUMIF(range, criteria, [sum_range])", AVERAGEIF:"AVERAGEIF(range, criteria, [average_range])",
SUMIFS:"SUMIFS(sum_range, criteria_range1, criteria1, ...)", COUNTIFS:"COUNTIFS(criteria_range1, criteria1, ...)",
VLOOKUP:"VLOOKUP(lookup_value, table_array, col_index_num, [range_lookup])", HLOOKUP:"HLOOKUP(lookup_value, table_array, row_index_num, [range_lookup])",
XLOOKUP:"XLOOKUP(lookup_value, lookup_array, return_array, [if_not_found])", INDEX:"INDEX(array, row_num, [column_num])", MATCH:"MATCH(lookup_value, lookup_array, [match_type])",
LEFT:"LEFT(text, [num_chars])", RIGHT:"RIGHT(text, [num_chars])", MID:"MID(text, start_num, num_chars)", LEN:"LEN(text)", TEXTJOIN:"TEXTJOIN(delimiter, ignore_empty, text1, ...)",
FIND:"FIND(find_text, within_text, [start_num])", SEARCH:"SEARCH(find_text, within_text, [start_num])", VALUE:"VALUE(text)", EXACT:"EXACT(text1, text2)",
PRODUCT:"PRODUCT(number1, [number2], ...)", RAND:"RAND()", RANDBETWEEN:"RANDBETWEEN(bottom, top)", FLOOR:"FLOOR(number, significance)", CEILING:"CEILING(number, significance)",
TODAY:"TODAY()", NOW:"NOW()", DATE:"DATE(year, month, day)", EDATE:"EDATE(start_date, months)", EOMONTH:"EOMONTH(start_date, months)", DAYS:"DAYS(end_date, start_date)", WEEKDAY:"WEEKDAY(serial_number)"
};
function updateFormulaHelp(text){
const box=document.getElementById("formulaHelp");
if(!box)return;
text=String(text||"");
if(!text.startsWith("=")){box.style.display="none";return;}
const before=text.slice(1);
const active=[...before.matchAll(/([A-Z][A-Z0-9]*)\(/gi)].pop();
const tail=before.match(/(?:^|[^A-Z0-9_])([A-Z][A-Z0-9]*)$/i);
let html="";
if(active){
const name=active[1].toUpperCase();
html='<div><span class="fname">'+name+'</span></div><div class="args">'+(FORMULA_SIG[name]||"未対応または未知の関数")+'</div>';
}else if(tail){
const q=tail[1].toUpperCase();
const names=Object.keys(FORMULA_SIG).filter(n=>n.startsWith(q)).slice(0,8);
if(names.length)html='<div>関数候補</div>'+names.map(n=>'<span class="suggest">'+n+'</span>').join("");
}
box.innerHTML=html;
box.style.display=html?"block":"none";
}
function previewFormula(text,repaint=true){
formulaRefs=[];
updateFormulaHelp(text);
if(!String(text).startsWith("=")){
msg(text===""?"入力プレビュー: 空白":"入力プレビュー: "+String(coerce(text)));
finishPreview(repaint);
return;
}
const seen=new Set();
const addRef=(a,b)=>{
const p1=parseA(a.replace(/\$/g,"")),p2=parseA((b||a).replace(/\$/g,""));
if(!p1||!p2)return;
const q={a:Math.min(p1[0],p2[0]),b:Math.min(p1[1],p2[1]),x:Math.max(p1[0],p2[0]),y:Math.max(p1[1],p2[1])};
if(q.a>=COLS||q.x<0||q.b>ROWS||q.y<1)return;
q.a=Math.max(0,q.a);q.x=Math.min(COLS-1,q.x);q.b=Math.max(1,q.b);q.y=Math.min(ROWS,q.y);
const key=[q.a,q.b,q.x,q.y].join(":");
if(!seen.has(key)){seen.add(key);formulaRefs.push(q);}
};
const addPartialRef=token=>{
token=String(token||"").replace(/\$/g,"").toUpperCase();
if(!token)return;
const toCol=part=>{
const m=String(part||"").match(/^([A-Z]+)(\d*)$/);
if(!m)return null;
const c=colN(m[1]);
if(c<0||c>=COLS)return null;
return {c,r:m[2]?+m[2]:null};
};
const parts=token.split(":");
const p1=toCol(parts[0]),p2=toCol(parts[1]||parts[0]);
if(!p1)return;
if(parts.length>1&&!p2){
formulaRefs.push({a:p1.c,b:p1.r||1,x:p1.c,y:p1.r||ROWS});
return;
}
const q={
a:Math.min(p1.c,p2.c),
b:Math.min(p1.r||1,p2.r||ROWS),
x:Math.max(p1.c,p2.c),
y:Math.max(p1.r||ROWS,p2.r||ROWS)
};
const key=[q.a,q.b,q.x,q.y].join(":");
if(!seen.has(key)){seen.add(key);formulaRefs.push(q);}
};
const body=String(text).slice(1);
body.replace(/(\$?[A-Z]+\$?\d+):(\$?[A-Z]+\$?\d+)/g,(_,a,b)=>{addRef(a,b);return _;});
body.replace(/(^|[^:A-Z0-9_$])(\$?[A-Z]+\$?\d+)(?!\s*:)/g,(_,pre,a)=>{addRef(a);return _;});
const tail=body.match(/(?:^|[\(,+\-*/^<>=])\s*(\$?[A-Z]+(?:\$?\d*)?(?::\$?[A-Z]*(?:\$?\d*)?)?)$/i);
if(tail)addPartialRef(tail[1]);
CALC.depth=0;
const old=sh().cells[A(...cur)];
sh().cells[A(...cur)]=text;
const v=CALC.evalCell(A(...cur));
if(old===undefined)delete sh().cells[A(...cur)];else sh().cells[A(...cur)]=old;
const refs=formulaRefs.length?(" / 参照:"+formulaRefs.map(q=>A(q.a,q.b)+(q.a!==q.x||q.b!==q.y?":"+A(q.x,q.y):"")).join(", ")):"";
msg("プレビュー: "+String(v)+refs);
finishPreview(repaint);
}
function clearFormulaPreview(repaint=true){
const had=formulaRefs.length;
formulaRefs=[];
updateFormulaHelp("");
if(had)finishPreview(repaint);
msg("準備完了");
}
// ---- selection (drag / shift) ----
let dragging=false;
let headerDragging=null;
let formulaPick=null;
function activeFormulaInput(){
const ae=document.activeElement;
if(ae&&ae.id==="fx"&&String(ae.value).startsWith("="))return ae;
const inp=editTd&&editTd.querySelector("input");
if(inp&&document.activeElement===inp&&String(inp.value).startsWith("="))return inp;
return null;
}
function refTextFromRect(a,b,x,y){
return A(a,b)+(a!==x||b!==y?":"+A(x,y):"");
}
function replaceFormulaToken(input,ref){
const pos=input.selectionStart??input.value.length;
const before=input.value.slice(0,pos), after=input.value.slice(pos);
const m=before.match(/([\(,+\-*/^<>=]\s*)(\$?[A-Z]{0,3}\$?\d*(?::\$?[A-Z]{0,3}\$?\d*)?)$/i);
let next, caret;
if(m&&m[2]!==undefined){
next=before.slice(0,before.length-m[2].length)+ref+after;
caret=before.length-m[2].length+ref.length;
}else{
next=before+ref+after;
caret=before.length+ref.length;
}
input.value=next;
input.focus();
input.setSelectionRange(caret,caret);
previewFormula(input.value,input.id==="fx");
}
function startFormulaPick(td,e){
const input=activeFormulaInput();if(!input)return false;
const[,c,r]=td.id.match(/c(\d+)_(\d+)/).map(Number);
formulaPick={input,start:[c,r],end:[c,r]};
formulaRefs=[{a:c,b:r,x:c,y:r}];
replaceFormulaToken(input,A(c,r));
positionRefBoxes();
e.preventDefault();e.stopPropagation();
return true;
}
function updateFormulaPick(td){
if(!formulaPick)return;
const[,c,r]=td.id.match(/c(\d+)_(\d+)/).map(Number);
formulaPick.end=[c,r];
const a=Math.min(formulaPick.start[0],c),b=Math.min(formulaPick.start[1],r),x=Math.max(formulaPick.start[0],c),y=Math.max(formulaPick.start[1],r);
formulaRefs=[{a,b,x,y}];
replaceFormulaToken(formulaPick.input,refTextFromRect(a,b,x,y));
positionRefBoxes();
}
thead.addEventListener("mousedown",e=>{
const th=e.target.closest("th[data-c]");if(!th||e.target.closest(".cres"))return;
const c=+th.dataset.c;
if(e.shiftKey){selEnd=[c,ROWS];}
else{cur=[c,1];anchor=[c,1];selEnd=[c,ROWS];}
headerDragging="col";commitEdit();renderAll();e.preventDefault();
});
thead.addEventListener("mouseover",e=>{
if(headerDragging!=="col")return;
const th=e.target.closest("th[data-c]");if(!th)return;
selEnd=[+th.dataset.c,ROWS];renderAll();
});
tbody.addEventListener("mousedown",e=>{
const th=e.target.closest("th[data-r]");if(!th||e.target.closest(".rres"))return;
const r=+th.dataset.r;
if(e.shiftKey){selEnd=[COLS-1,r];}
else{cur=[0,r];anchor=[0,r];selEnd=[COLS-1,r];}
headerDragging="row";commitEdit();renderAll();e.preventDefault();
});
tbody.addEventListener("mouseover",e=>{
if(headerDragging!=="row")return;
const th=e.target.closest("th[data-r]");if(!th)return;
selEnd=[COLS-1,+th.dataset.r];renderAll();
});
tbody.addEventListener("mousedown",e=>{
const td=e.target.closest("td");if(!td)return;
if(startFormulaPick(td,e))return;
const[,c,r]=td.id.match(/c(\d+)_(\d+)/).map(Number);
if(e.shiftKey){selEnd=[c,r];}else{cur=[c,r];anchor=[c,r];selEnd=[c,r];}
dragging=true;commitEdit();renderAll();
});
tbody.addEventListener("mouseover",e=>{
if(formulaPick){const td=e.target.closest("td");if(td)updateFormulaPick(td);return;}
if(!dragging)return;const td=e.target.closest("td");if(!td)return;
const[,c,r]=td.id.match(/c(\d+)_(\d+)/).map(Number);selEnd=[c,r];renderAll();
});
document.addEventListener("mousemove",e=>{
if(!dragging&&!formulaPick&&!filling)return;
const td=document.elementFromPoint(e.clientX,e.clientY)?.closest?.("td");
if(!td||!td.id||!td.id.startsWith("c"))return;
const[,c,r]=td.id.match(/c(\d+)_(\d+)/).map(Number);
if(formulaPick){updateFormulaPick(td);return;}
if(filling){
filling.end=[c,r];
const[a,b,x,y]=filling.rect;
anchor=[Math.min(a,c),Math.min(b,r)];selEnd=[Math.max(x,c),Math.max(y,r)];renderAll();
return;
}
selEnd=[c,r];renderAll();
});
document.addEventListener("mouseup",()=>dragging=false);
document.addEventListener("mouseup",()=>headerDragging=null);
document.addEventListener("mouseup",()=>formulaPick=null);
tbody.addEventListener("dblclick",e=>{const td=e.target.closest("td");if(td)startEdit();});
// ---- autofill (fill handle) ----
let filling=null;
document.getElementById("fillh").addEventListener("mousedown",e=>{
e.preventDefault();e.stopPropagation();
filling={rect:selRect(),end:null};
});
tbody.addEventListener("mouseover",e=>{
if(!filling)return;const td=e.target.closest("td");if(!td)return;
const[,c,r]=td.id.match(/c(\d+)_(\d+)/).map(Number);
filling.end=[c,r];
const[a,b,x,y]=filling.rect;
anchor=[Math.min(a,c),Math.min(b,r)];selEnd=[Math.max(x,c),Math.max(y,r)];renderAll();
});
document.addEventListener("mouseup",()=>{
if(!filling)return;
const[a,b,x,y]=filling.rect,end=filling.end;filling=null;
if(!end)return;
const[ec,er]=end;
if(er>=b&&er<=y&&ec>=a&&ec<=x)return;
pushUndo();
const down=er>y, up=er<b, right=ec>x, left=ec<a;
if(down){
for(let c=a;c<=x;c++){
const src=[];for(let r=b;r<=y;r++)src.push(sh().cells[A(c,r)]);
const nsrc=src.map(v=>coerce(v??""));
const linear=nsrc.length>=2&&nsrc.every(v=>typeof v==="number");
const step=linear?nsrc[1]-nsrc[0]:1;
const singleNum=nsrc.length===1&&typeof nsrc[0]==="number";
for(let r=y+1;r<=er;r++){
const i=(r-b)%src.length, off=r-(b+i);
let v=src[i];
if(v===undefined)continue;
if(String(v)[0]==="=")v=shiftRefs(v,0,off);
else if(linear)v=String(nsrc[0]+step*(r-b));
else if(singleNum)v=String(nsrc[0]+(r-y));
sh().cells[A(c,r)]=v;
}
}
}else if(right){
for(let r=b;r<=y;r++){
for(let c=x+1;c<=ec;c++){
const i=(c-a)%(x-a+1), off=c-(a+i);
let v=sh().cells[A(a+i,r)];
if(v===undefined)continue;
if(String(v)[0]==="=")v=shiftRefs(v,off,0);
sh().cells[A(c,r)]=v;
}
}
}else if(up){
for(let c=a;c<=x;c++){
const src=[];for(let r=b;r<=y;r++)src.push(sh().cells[A(c,r)]);
for(let r=b-1;r>=er;r--){
const i=((b-1-r)%src.length), off=r-(b+i);
let v=src[i];if(v===undefined)continue;
if(String(v)[0]==="=")v=shiftRefs(v,0,off);
sh().cells[A(c,r)]=v;
}
}
}else if(left){
for(let r=b;r<=y;r++){
const src=[];for(let c=a;c<=x;c++)src.push(sh().cells[A(c,r)]);
for(let c=a-1;c>=ec;c--){
const i=((a-1-c)%src.length), off=c-(a+i);
let v=src[i];if(v===undefined)continue;
if(String(v)[0]==="=")v=shiftRefs(v,off,0);
sh().cells[A(c,r)]=v;
}
}
}
renderAll();msg("オートフィルしました");
});
// ---- context menu ----
const ctx=document.getElementById("ctx");
function showCtx(x,y){
ctx.innerHTML=`
<div data-m="cut">切り取り</div><div data-m="copy">コピー</div><div data-m="paste">貼り付け</div><hr>
<div data-m="insrow">行の挿入</div><div data-m="delrow">行の削除</div>
<div data-m="inscol">列の挿入</div><div data-m="delcol">列の削除</div><hr>
<div data-m="clear">数式と値のクリア</div><div data-m="clearf">書式のクリア</div>`;
ctx.style.display="block";
ctx.style.left=Math.min(x,innerWidth-200)+"px";ctx.style.top=Math.min(y,innerHeight-260)+"px";
}
document.addEventListener("click",()=>ctx.style.display="none");
tbody.addEventListener("contextmenu",e=>{
const td=e.target.closest("td");if(!td)return;
e.preventDefault();
const[,c,r]=td.id.match(/c(\d+)_(\d+)/).map(Number);
if(!inSel(c,r)){cur=[c,r];anchor=[c,r];selEnd=[c,r];renderAll();}
showCtx(e.clientX,e.clientY);
});
thead.addEventListener("contextmenu",e=>{
const th=e.target.closest("th[data-c]");if(!th)return;
e.preventDefault();
const c=+th.dataset.c;
cur=[c,1];anchor=[c,1];selEnd=[c,ROWS];renderAll();
showCtx(e.clientX,e.clientY);
});
tbody.addEventListener("contextmenu",e=>{
const th=e.target.closest("th[data-r]");if(!th)return;
e.preventDefault();
const r=+th.dataset.r;
cur=[0,r];anchor=[0,r];selEnd=[COLS-1,r];renderAll();
showCtx(e.clientX,e.clientY);
});
ctx.addEventListener("click",e=>{
const m=e.target.dataset.m;if(!m)return;
if(m==="cut"){copySel();delSel();}
else if(m==="copy")copySel();
else if(m==="paste")pasteClip();
else if(m==="clear")delSel();
else if(m==="clearf"){pushUndo();eachSel(a=>delete sh().fmt[a]);renderAll();}
else if(m==="insrow")insRow(cur[1]);
else if(m==="delrow")delRow(cur[1]);
else if(m==="inscol")insCol(cur[0]);
else if(m==="delcol")delCol(cur[0]);
});
// ---- row/col insert/delete with reference shift ----
function remapSheet(mapAddr,mapRef){
const s=sh(),nc={},nf={};
Object.entries(s.cells).forEach(([a,v])=>{
const na=mapAddr(a);if(na===null)return;
if(String(v)[0]==="=")v="="+String(v).slice(1).replace(/(\$?)([A-Z]+)(\$?)(\d+)/g,(mm,d1,col,d2,row)=>{
const nr=mapRef(colN(col),+row);return nr===null?"#REF!":d1+colL(nr[0])+d2+nr[1];
});
nc[na]=v;
});
Object.entries(s.fmt).forEach(([a,v])=>{const na=mapAddr(a);if(na!==null)nf[na]=v;});
s.cells=nc;s.fmt=nf;
}
function insRow(at){pushUndo();
remapSheet(a=>{const p=parseA(a);return p[1]>=at?A(p[0],p[1]+1):a;},(c,r)=>r>=at?[c,r+1]:[c,r]);
const rh=rowh();for(let r=ROWS;r>at;r--){if(rh[r-1])rh[r]=rh[r-1];else delete rh[r];}delete rh[at];
renderAll();msg(at+"行目に挿入しました");}
function delRow(at){pushUndo();
remapSheet(a=>{const p=parseA(a);return p[1]===at?null:p[1]>at?A(p[0],p[1]-1):a;},(c,r)=>r===at?null:r>at?[c,r-1]:[c,r]);
const rh=rowh();for(let r=at;r<ROWS;r++){if(rh[r+1])rh[r]=rh[r+1];else delete rh[r];}
renderAll();msg(at+"行目を削除しました");}
function insCol(at){pushUndo();
remapSheet(a=>{const p=parseA(a);return p[0]>=at?A(p[0]+1,p[1]):a;},(c,r)=>c>=at?[c+1,r]:[c,r]);
const cw=colw();for(let c=COLS-1;c>at;c--){if(cw[c-1]!==undefined)cw[c]=cw[c-1];else delete cw[c];}delete cw[at];
renderAll();msg(colL(at)+"列に挿入しました");}
function delCol(at){pushUndo();
remapSheet(a=>{const p=parseA(a);return p[0]===at?null:p[0]>at?A(p[0]-1,p[1]):a;},(c,r)=>c===at?null:c>at?[c-1,r]:[c,r]);
const cw=colw();for(let c=at;c<COLS-1;c++){if(cw[c+1]!==undefined)cw[c]=cw[c+1];else delete cw[c];}
renderAll();msg(colL(at)+"列を削除しました");}
// ---- editing ----
let editTd=null, editAddr=null;
function startEdit(initial){
const[c,r]=cur, td=document.getElementById("c"+c+"_"+r);
const inp=document.createElement("input");
inp.value=initial!==undefined?initial:(sh().cells[A(c,r)]||"");
td.textContent="";td.appendChild(inp);inp.focus();
if(initial!==undefined)inp.setSelectionRange(inp.value.length,inp.value.length);
editTd=td;editAddr=A(c,r);
inp.addEventListener("keydown",e=>{
if(e.key==="Enter"){e.preventDefault();commitEdit();move(0,1);}
else if(e.key==="Tab"){e.preventDefault();commitEdit();move(1,0);}
else if(e.key==="Escape"){editTd=null;editAddr=null;clearFormulaPreview(false);renderAll();}
e.stopPropagation();
});
inp.addEventListener("input",e=>previewFormula(e.target.value,false));
previewFormula(inp.value,false);
}
function commitEdit(){
if(!editTd)return;const inp=editTd.querySelector("input");if(!inp)return;
pushUndo();sh().cells[editAddr]=inp.value;if(inp.value==="")delete sh().cells[editAddr];
editTd=null;editAddr=null;formulaRefs=[];renderAll();
}
function move(dc,dr){
cur=[Math.max(0,Math.min(COLS-1,cur[0]+dc)),Math.max(1,Math.min(ROWS,cur[1]+dr))];
anchor=[...cur];selEnd=[...cur];renderAll();scrollToCur();
}
function scrollToCur(){const td=document.getElementById("c"+cur[0]+"_"+cur[1]);if(td)td.scrollIntoView({block:"nearest",inline:"nearest"});}
// ---- keyboard ----
document.addEventListener("keydown",e=>{
const k=e.key;
const key=k.toLowerCase();
if((e.ctrlKey||e.metaKey)&&!e.altKey){
if(key==="z"){
e.preventDefault();
if(editTd)commitEdit();
e.shiftKey?redo():undo();
return;
}
if(key==="y"){
e.preventDefault();
if(editTd)commitEdit();
redo();
return;
}
}
if(editTd||["INPUT","TEXTAREA","SELECT"].includes(document.activeElement.tagName)){
if(document.activeElement.id==="fx"&&e.key==="Enter"){
pushUndo();const v=document.activeElement.value;
sh().cells[A(...cur)]=v;if(v==="")delete sh().cells[A(...cur)];
move(0,1);document.getElementById("gridwrap").focus();
}
return;
}
if(e.ctrlKey||e.metaKey){
if(key==="c"){e.preventDefault();copySel();}
else if(key==="v"){e.preventDefault();pasteClip();}
else if(key==="x"){e.preventDefault();copySel();delSel();}
else if(key==="a"){e.preventDefault();selectUsedRange();}
else if(key==="s"){e.preventDefault();saveXlsx();}
else if(key==="b"){e.preventDefault();toggleFmt("b");}
else if(key==="i"){e.preventDefault();toggleFmt("i");}
else if(k==="Home"){e.preventDefault();cur=[0,1];anchor=[...cur];selEnd=[...cur];renderAll();scrollToCur();}
else if(k==="ArrowDown"){e.preventDefault();jump(0,1);}
else if(k==="ArrowUp"){e.preventDefault();jump(0,-1);}
else if(k==="ArrowRight"){e.preventDefault();jump(1,0);}
else if(k==="ArrowLeft"){e.preventDefault();jump(-1,0);}
return;
}
if(k==="Home"){e.preventDefault();cur=[0,cur[1]];anchor=[...cur];selEnd=[...cur];renderAll();scrollToCur();return;}
if(k.startsWith("Arrow")){
e.preventDefault();
const d={ArrowDown:[0,1],ArrowUp:[0,-1],ArrowRight:[1,0],ArrowLeft:[-1,0]}[k];
if(e.shiftKey){selEnd=[Math.max(0,Math.min(COLS-1,selEnd[0]+d[0])),Math.max(1,Math.min(ROWS,selEnd[1]+d[1]))];renderAll();}
else move(...d);
}
else if(k==="Enter"){e.preventDefault();move(0,1);}
else if(k==="Tab"){e.preventDefault();move(1,0);}
else if(k==="Delete"||k==="Backspace"){e.preventDefault();delSel();}
else if(k==="F2"){e.preventDefault();startEdit();}
else if(k.length===1){startEdit(k);e.preventDefault?e.preventDefault():0;}
});
function jump(dc,dr){
let[c,r]=cur;
const has=(c,r)=>sh().cells[A(c,r)]!==undefined;
const step=()=>{c+=dc;r+=dr;};
if(has(c,r)&&has(c+dc,r+dr)){while(c+dc>=0&&c+dc<COLS&&r+dr>=1&&r+dr<=ROWS&&has(c+dc,r+dr))step();}
else{while(c+dc>=0&&c+dc<COLS&&r+dr>=1&&r+dr<=ROWS&&!has(c+dc,r+dr))step();if(has(c,r)===false&&c+dc>=0&&c+dc<COLS&&r+dr>=1&&r+dr<=ROWS)step();}
cur=[Math.max(0,Math.min(COLS-1,c)),Math.max(1,Math.min(ROWS,r))];anchor=[...cur];selEnd=[...cur];renderAll();scrollToCur();
}
function delSel(){pushUndo();const[a,b,x,y]=selRect();for(let c=a;c<=x;c++)for(let r=b;r<=y;r++)delete sh().cells[A(c,r)];renderAll();}
function selectUsedRange(){
let maxC=0,maxR=1,found=false;
Object.keys(sh().cells).forEach(a=>{const p=parseA(a);if(p){found=true;maxC=Math.max(maxC,p[0]);maxR=Math.max(maxR,p[1]);}});
anchor=[0,1];selEnd=found?[maxC,maxR]:[COLS-1,ROWS];cur=[0,1];renderAll();
msg("全選択 A1:"+A(selEnd[0],selEnd[1]));
}
// ---- clipboard ----
let clip=null;
function parseClipboardText(text){
text=String(text||"").replace(/\r\n/g,"\n").replace(/\r/g,"\n");
const rows=[], row=[];let cur="",q=false;
for(let i=0;i<text.length;i++){
const ch=text[i], next=text[i+1];
if(q){
if(ch==='"'&&next==='"'){cur+='"';i++;}
else if(ch==='"')q=false;
else cur+=ch;
}else if(ch==='"')q=true;
else if(ch==="\t"||ch===","){row.push(cur);cur="";}
else if(ch==="\n"){row.push(cur);rows.push(row.splice(0));cur="";}
else cur+=ch;
}
if(cur!==""||row.length){row.push(cur);rows.push(row);}
return rows;
}
function pasteMatrix(rows){
if(!rows||!rows.length)return false;
pushUndo();
const[c0,r0]=cur;
rows.forEach((row,dr)=>row.forEach((v,dc)=>{
const c=c0+dc,r=r0+dr;if(c>=COLS||r>ROWS)return;
if(v==="")delete sh().cells[A(c,r)];else sh().cells[A(c,r)]=String(v);
}));
anchor=[c0,r0];selEnd=[Math.min(COLS-1,c0+Math.max(...rows.map(r=>r.length),1)-1),Math.min(ROWS,r0+rows.length-1)];
renderAll();msg("貼り付けました ("+rows.length+"×"+Math.max(...rows.map(r=>r.length))+")");
return true;
}
function copySel(){
const[a,b,x,y]=selRect();clip={w:x-a+1,h:y-b+1,data:[]};let txt=[];
for(let r=b;r<=y;r++){let row=[],trow=[];
for(let c=a;c<=x;c++){row.push(sh().cells[A(c,r)]??"");CALC.depth=0;trow.push(String(CALC.val(A(c,r))??""));}
clip.data.push(row);txt.push(trow.join("\t"));}
navigator.clipboard&&navigator.clipboard.writeText(txt.join("\n")).catch(()=>{});
msg("コピーしました ("+clip.w+"×"+clip.h+")");
}
function pasteClip(){
if(!clip){
if(navigator.clipboard&&navigator.clipboard.readText){
navigator.clipboard.readText().then(t=>pasteMatrix(parseClipboardText(t))).catch(()=>msg("貼り付ける内容がありません"));
}
return;
}
pushUndo();
const[c0,r0]=cur;
for(let dr=0;dr<clip.h;dr++)for(let dc=0;dc<clip.w;dc++){
const c=c0+dc,r=r0+dr;if(c>=COLS||r>ROWS)continue;
let v=clip.data[dr][dc];
if(String(v)[0]==="=")v=shiftRefs(v,dc,dr);
if(v==="")delete sh().cells[A(c,r)];else sh().cells[A(c,r)]=v;
}
renderAll();msg("貼り付けました");
}
function shiftRefs(f,dc,dr){
return f.replace(/(\$?)([A-Z]+)(\$?)(\d+)/g,(m,d1,col,d2,row)=>{
let c=colN(col),r=+row;
if(!d1)c+=dc;if(!d2)r+=dr;
if(c<0||c>=COLS||r<1||r>ROWS)return"#REF!";
return d1+colL(c)+d2+r;
});
}
// ---- format ----
function eachSel(fn){const[a,b,x,y]=selRect();for(let c=a;c<=x;c++)for(let r=b;r<=y;r++)fn(A(c,r));}
function toggleFmt(key){pushUndo();const first=sh().fmt[A(...cur)]||{};const on=!first[key];eachSel(a=>{sh().fmt[a]=sh().fmt[a]||{};sh().fmt[a][key]=on;});renderAll();}
function setFmt(key,val){pushUndo();eachSel(a=>{sh().fmt[a]=sh().fmt[a]||{};sh().fmt[a][key]=val;});renderAll();}
function adjustDecimals(delta){
pushUndo();
const first=sh().fmt[A(...cur)]||{};
const base=first.dec!==undefined?first.dec:(first.percent?0:2);
const next=Math.max(0,Math.min(8,base+delta));
eachSel(a=>{sh().fmt[a]=sh().fmt[a]||{};sh().fmt[a].dec=next;});
renderAll();msg("小数点以下 "+next+" 桁");
}
// ---- sheets ----
function renderTabs(){
const t=document.getElementById("tabs");t.innerHTML="";
wb.order.forEach(n=>{
const d=document.createElement("span");d.className="tab"+(n===wb.active?" on":"");d.textContent=n;
d.onclick=()=>{wb.active=n;renderAll();};
d.ondblclick=()=>{const nn=prompt("シート名",n);if(nn&&!wb.sheets[nn]){wb.sheets[nn]=wb.sheets[n];delete wb.sheets[n];wb.order[wb.order.indexOf(n)]=nn;if(wb.active===n)wb.active=nn;renderAll();}};
t.appendChild(d);
});
}
document.getElementById("addsheet").onclick=()=>{
let i=wb.order.length+1;while(wb.sheets["Sheet"+i])i++;
wb.sheets["Sheet"+i]={cells:{},fmt:{}};wb.order.push("Sheet"+i);wb.active="Sheet"+i;renderAll();
};
</script>
<script>
"use strict";
// ==== formula engine ====
const CALC={depth:0};
CALC.val=function(addr){
const raw=sh().cells[addr];
if(raw===undefined||raw==="")return 0;
if(String(raw)[0]==="="){
if(CALC.depth>64)return"#CIRC!";
CALC.depth++;const r=CALC.evalStr(String(raw).slice(1));CALC.depth--;return r;
}
return coerce(raw);
};
CALC.evalCell=a=>CALC.evalStr(String(sh().cells[a]).slice(1));
CALC.range=function(a,b){
const p1=parseA(a),p2=parseA(b);if(!p1||!p2)return[];
const out=[];
for(let c=Math.min(p1[0],p2[0]);c<=Math.max(p1[0],p2[0]);c++)
for(let r=Math.min(p1[1],p2[1]);r<=Math.max(p1[1],p2[1]);r++)
out.push(CALC.val(A(c,r)));
return out;
};
CALC.rangeCells=function(a,b){
const p1=parseA(a),p2=parseA(b);if(!p1||!p2)return[];
const rows=[];
for(let r=Math.min(p1[1],p2[1]);r<=Math.max(p1[1],p2[1]);r++){
const row=[];for(let c=Math.min(p1[0],p2[0]);c<=Math.max(p1[0],p2[0]);c++)row.push(CALC.val(A(c,r)));
rows.push(row);
}
return rows;
};
const nums=v=>v.filter(x=>typeof x==="number");
function crit(v,c){
c=String(c);
const m=c.match(/^(<=|>=|<>|<|>|=)?(.*)$/);
const op=m[1]||"=",t=m[2];const tn=parseFloat(t);
const cmpV=isNaN(tn)?String(v):Number(v), cmpT=isNaN(tn)?t:tn;
switch(op){case"=":return cmpV==cmpT;case"<>":return cmpV!=cmpT;case"<":return cmpV<cmpT;case">":return cmpV>cmpT;case"<=":return cmpV<=cmpT;case">=":return cmpV>=cmpT;}
}
const FN={
SUM:a=>nums(a.flat(Infinity)).reduce((x,y)=>x+y,0),
AVERAGE:a=>{const n=nums(a.flat(Infinity));return n.length?FN.SUM([n])/n.length:0;},
COUNT:a=>nums(a.flat(Infinity)).length,
COUNTA:a=>a.flat(Infinity).filter(x=>x!==""&&x!==0||typeof x==="number").length,
MIN:a=>{const n=nums(a.flat(Infinity));return n.length?Math.min(...n):0;},
MAX:a=>{const n=nums(a.flat(Infinity));return n.length?Math.max(...n):0;},
MEDIAN:a=>{const n=nums(a.flat(Infinity)).sort((x,y)=>x-y);return n.length?(n.length%2?n[(n.length-1)/2]:(n[n.length/2-1]+n[n.length/2])/2):0;},
STDEV:a=>{const n=nums(a.flat(Infinity));if(n.length<2)return 0;const m=FN.SUM([n])/n.length;return Math.sqrt(n.reduce((s,x)=>s+(x-m)**2,0)/(n.length-1));},
IF:(a)=>a[0]?a[1]:a[2],
IFERROR:(a)=>String(a[0]).startsWith("#")?a[1]:a[0],
AND:a=>a.flat(Infinity).every(Boolean),
OR:a=>a.flat(Infinity).some(Boolean),
NOT:a=>!a[0],
ROUND:a=>{const p=10**(a[1]||0);return Math.round(a[0]*p)/p;},
ROUNDUP:a=>{const p=10**(a[1]||0);return Math.ceil(a[0]*p)/p;},
ROUNDDOWN:a=>{const p=10**(a[1]||0);return Math.floor(a[0]*p)/p;},
ABS:a=>Math.abs(a[0]), INT:a=>Math.floor(a[0]), MOD:a=>a[0]%a[1], POWER:a=>a[0]**a[1], SQRT:a=>Math.sqrt(a[0]),
PRODUCT:a=>nums(a.flat(Infinity)).reduce((x,y)=>x*y,1),
RAND:()=>Math.random(), RANDBETWEEN:a=>Math.floor(Math.random()*(a[1]-a[0]+1))+a[0],
FLOOR:a=>Math.floor(a[0]/(a[1]||1))*(a[1]||1), CEILING:a=>Math.ceil(a[0]/(a[1]||1))*(a[1]||1),
CONCATENATE:a=>a.flat(Infinity).join(""), CONCAT:a=>a.flat(Infinity).join(""),
LEFT:a=>String(a[0]).slice(0,a[1]??1), RIGHT:a=>String(a[0]).slice(-(a[1]??1)),
MID:a=>String(a[0]).substr(a[1]-1,a[2]), LEN:a=>String(a[0]).length,
UPPER:a=>String(a[0]).toUpperCase(), LOWER:a=>String(a[0]).toLowerCase(), TRIM:a=>String(a[0]).trim(),
SUBSTITUTE:a=>String(a[0]).split(String(a[1])).join(String(a[2])),
FIND:a=>{const i=String(a[1]).indexOf(String(a[0]),(a[2]||1)-1);return i<0?"#VALUE!":i+1;},
SEARCH:a=>{const i=String(a[1]).toLowerCase().indexOf(String(a[0]).toLowerCase(),(a[2]||1)-1);return i<0?"#VALUE!":i+1;},
VALUE:a=>{const n=parseFloat(String(a[0]).replace(/,/g,""));return isNaN(n)?"#VALUE!":n;},
EXACT:a=>String(a[0])===String(a[1]),
TEXT:a=>typeof a[0]==="number"?a[0].toLocaleString():String(a[0]),
TODAY:()=>new Date().toLocaleDateString(), NOW:()=>new Date().toLocaleString(),
DATE:a=>new Date(a[0],a[1]-1,a[2]).toLocaleDateString(),
EDATE:a=>{const d=new Date(a[0]);d.setMonth(d.getMonth()+Number(a[1]||0));return d.toLocaleDateString();},
EOMONTH:a=>{const d=new Date(a[0]);return new Date(d.getFullYear(),d.getMonth()+Number(a[1]||0)+1,0).toLocaleDateString();},
DAYS:a=>Math.round((new Date(a[0])-new Date(a[1]))/86400000),
WEEKDAY:a=>new Date(a[0]).getDay()+1,
YEAR:a=>new Date(a[0]).getFullYear(), MONTH:a=>new Date(a[0]).getMonth()+1, DAY:a=>new Date(a[0]).getDate(),
COUNTIF:a=>a[0].flat(Infinity).filter(v=>crit(v,a[1])).length,
SUMIF:a=>{
const rng=a[0].flat(Infinity), c=a[1], srng=a[2]?a[2].flat(Infinity):rng;
let s=0;rng.forEach((v,i)=>{if(crit(v,c)&&typeof srng[i]==="number")s+=srng[i];});return s;
},
AVERAGEIF:a=>{
const rng=a[0].flat(Infinity), c=a[1], srng=a[2]?a[2].flat(Infinity):rng;
let s=0,n=0;rng.forEach((v,i)=>{if(crit(v,c)&&typeof srng[i]==="number"){s+=srng[i];n++;}});return n?s/n:0;
},
VLOOKUP:a=>{
const[key,tbl,col,exact]=a;
for(const row of tbl){if(exact===false||exact===0?row[0]==key:row[0]==key)return row[col-1]??"#N/A";}
if(exact!==false&&exact!==0){let best="#N/A";for(const row of tbl){if(row[0]<=key)best=row[col-1];}return best;}
return"#N/A";
},
HLOOKUP:a=>{
const[key,tbl,rowi]=a;
for(let c=0;c<tbl[0].length;c++)if(tbl[0][c]==key)return tbl[rowi-1]?.[c]??"#N/A";
return"#N/A";
},
INDEX:a=>{const[tbl,r,c]=a;return Array.isArray(tbl[0])?(tbl[r-1]?.[(c||1)-1]??"#REF!"):(tbl[r-1]??"#REF!");},
MATCH:a=>{const[key,arr]=a;const f=arr.flat(Infinity);const i=f.findIndex(v=>v==key);return i<0?"#N/A":i+1;},
XLOOKUP:a=>{
const[key,lk,rt,ifnf]=a;const l=lk.flat(Infinity),r=rt.flat(Infinity);
const i=l.findIndex(v=>v==key);return i<0?(ifnf??"#N/A"):r[i];
},
SUMIFS:a=>{
const sr=a[0].flat(Infinity);let s=0;
for(let i=0;i<sr.length;i++){
let ok=true;
for(let j=1;j<a.length;j+=2){if(!crit(a[j].flat(Infinity)[i],a[j+1])){ok=false;break;}}
if(ok&&typeof sr[i]==="number")s+=sr[i];
}
return s;
},
COUNTIFS:a=>{
const n=a[0].flat(Infinity).length;let cnt=0;
for(let i=0;i<n;i++){
let ok=true;
for(let j=0;j<a.length;j+=2){if(!crit(a[j].flat(Infinity)[i],a[j+1])){ok=false;break;}}
if(ok)cnt++;
}
return cnt;
},
RANK:a=>{const v=a[0],arr=nums(a[1].flat(Infinity)).sort((x,y)=>a[2]?x-y:y-x);const i=arr.indexOf(v);return i<0?"#N/A":i+1;},
LARGE:a=>{const n=nums(a[0].flat(Infinity)).sort((x,y)=>y-x);return n[a[1]-1]??"#NUM!";},
SMALL:a=>{const n=nums(a[0].flat(Infinity)).sort((x,y)=>x-y);return n[a[1]-1]??"#NUM!";},
PROPER:a=>String(a[0]).replace(/\w+/g,w=>w[0].toUpperCase()+w.slice(1).toLowerCase()),
REPT:a=>String(a[0]).repeat(Math.max(0,a[1])),
TEXTJOIN:a=>{const[d,ig,...rest]=a;return rest.flat(Infinity).filter(v=>!ig||v!==""&&v!==0||typeof v==="number"&&v!==0||String(v)!=="").join(String(d));}
};
CALC.evalStr=function(f){
try{
let i=0;const s=f;
function ws(){while(i<s.length&&s[i]===" ")i++;}
function parseExpr(){return parseCmp();}
function parseCmp(){
let l=parseAdd();ws();
while(i<s.length){
let op=null;
if(s.startsWith("<=",i)||s.startsWith(">=",i)||s.startsWith("<>",i)){op=s.substr(i,2);i+=2;}
else if("<>=".includes(s[i])){op=s[i];i++;}
else break;
const r=parseAdd();
l=op==="="?l==r:op==="<>"?l!=r:op==="<"?l<r:op===">"?l>r:op==="<="?l<=r:l>=r;
ws();
}
return l;
}
function parseAdd(){
let l=parseMul();ws();
while(i<s.length&&(s[i]==="+"||s[i]==="-")){const op=s[i++];const r=parseMul();l=op==="+"?num(l)+num(r):num(l)-num(r);ws();}
return l;
}
function parseMul(){
let l=parsePow();ws();
while(i<s.length&&(s[i]==="*"||s[i]==="/")){const op=s[i++];const r=parsePow();l=op==="*"?num(l)*num(r):num(l)/num(r);ws();}
return l;
}
function parsePow(){
let l=parseUnary();ws();
while(i<s.length&&s[i]==="^"){i++;const r=parseUnary();l=num(l)**num(r);ws();}
return l;
}
function parseUnary(){ws();if(s[i]==="-"){i++;return -num(parseUnary());}if(s[i]==="+"){i++;return parseUnary();}return parseAtom();}
function num(v){if(typeof v==="boolean")return v?1:0;if(typeof v==="string"){const n=parseFloat(v);return isNaN(n)?0:n;}return v;}
function parseAtom(){
ws();
if(s[i]==="("){i++;const v=parseExpr();ws();if(s[i]===")")i++;return v;}
if(s[i]==='"'){let j=i+1,out="";while(j<s.length&&s[j]!=='"'){out+=s[j++];}i=j+1;return out;}
const rest=s.slice(i);
let m=rest.match(/^(TRUE|FALSE)(?![A-Z0-9(])/i);
if(m){i+=m[0].length;return m[0].toUpperCase()==="TRUE";}
m=rest.match(/^([A-Z]+)\(/i);
if(m&&FN[m[1].toUpperCase()]){
const name=m[1].toUpperCase();i+=m[0].length;
const args=[];ws();
if(s[i]===")"){i++;}
else{
while(true){args.push(parseArg());ws();if(s[i]===","){i++;continue;}if(s[i]===")"){i++;break;}break;}
}
return FN[name](args);
}
m=rest.match(/^\$?[A-Z]+\$?\d+:\$?[A-Z]+\$?\d+/);
if(m){i+=m[0].length;const[a,b]=m[0].replace(/\$/g,"").split(":");return CALC.range(a,b);}
m=rest.match(/^\$?([A-Z]+):\$?([A-Z]+)/);
if(m){i+=m[0].length;return CALC.range(m[1]+"1",m[2]+ROWS);}
m=rest.match(/^\$?[A-Z]+\$?\d+/);
if(m){i+=m[0].length;return CALC.val(m[0].replace(/\$/g,""));}
m=rest.match(/^\d+\.?\d*/);
if(m){i+=m[0].length;return parseFloat(m[0]);}
i++;return"#NAME?";
}
function parseArg(){
ws();
const rest=s.slice(i);
const m=rest.match(/^\$?[A-Z]+\$?\d+:\$?[A-Z]+\$?\d+/);
if(m&&!rest.slice(m[0].length).match(/^\s*[+\-*/^]/)){
const peek=rest.slice(m[0].length).trimStart();
if(peek[0]===","||peek[0]===")"){
i+=m[0].length;const[a,b]=m[0].replace(/\$/g,"").split(":");
return CALC.rangeCells(a,b);
}
}
return parseExpr();
}
const r=parseExpr();
if(typeof r==="number"&&isFinite(r))return Math.round(r*1e8)/1e8;
if(typeof r==="number")return"#DIV/0!";
return r;
}catch(e){return"#ERROR";}
};
</script>
<script>
"use strict";
// ==== side panel modules ====
const panel=document.getElementById("sidepanel");
function openPanel(html){panel.classList.add("open");panel.innerHTML=html;}
function closePanel(){panel.classList.remove("open");panel.innerHTML="";}
let chartObj=null;
function uiChart(){
const[a,b,x,y]=selRect();
openPanel(`<h3>グラフの挿入</h3>
<p class="mini">選択範囲 ${A(a,b)}:${A(x,y)}(1列目=ラベル、2列目以降=系列)</p>
<div style="margin:8px 0">種類
<select id="ctype"><option value="bar">縦棒</option><option value="line">折れ線</option><option value="pie">円</option><option value="scatter">散布図</option></select>
<button class="go" id="cgo">挿入</button> <button onclick="closePanel()">閉じる</button></div>
<div style="position:relative;height:260px;background:#fff;border:1px solid var(--bd);border-radius:6px;padding:6px"><canvas id="cvs"></canvas></div>`);
document.getElementById("cgo").onclick=()=>{
const t=document.getElementById("ctype").value;
const labels=[],sers=[];
for(let c=a+1;c<=x;c++)sers.push({label:String(CALC.val(A(c,b))),data:[]});
for(let r=b+1;r<=y;r++){
labels.push(String(CALC.val(A(a,r))));
for(let c=a+1;c<=x;c++){CALC.depth=0;sers[c-a-1].data.push(CALC.val(A(c,r)));}
}
const pal=["#1D9E75","#378ADD","#EF9F27","#D85A30","#7F77DD","#D4537E","#639922","#E24B4A"];
if(chartObj)chartObj.destroy();
chartObj=new Chart(document.getElementById("cvs"),{
type:t==="scatter"?"scatter":t,
data:{labels,datasets:sers.map((s,i)=>({...s,
data:t==="scatter"?s.data.map((v,j)=>({x:j+1,y:v})):s.data,
backgroundColor:t==="pie"?pal:pal[i%pal.length],borderColor:pal[i%pal.length],fill:false}))},
options:{responsive:true,maintainAspectRatio:false}
});
msg("グラフを挿入しました");
};
document.getElementById("cgo").click();
}
function uiPivot(){
const[a,b,x,y]=selRect();
let heads=[];for(let c=a;c<=x;c++)heads.push({c,name:String(CALC.val(A(c,b)))});
const opt=heads.map(h=>`<option value="${h.c}">${h.name}</option>`).join("");
openPanel(`<h3>ピボットテーブル</h3>
<p class="mini">データ範囲 ${A(a,b)}:${A(x,y)}(1行目=見出し)</p>
<div>行: <select id="prow">${opt}</select></div>
<div>値: <select id="pval">${opt}</select>
集計: <select id="pagg"><option value="sum">合計</option><option value="count">件数</option><option value="avg">平均</option><option value="max">最大</option><option value="min">最小</option></select></div>
<div style="margin:6px 0"><button class="go" id="pgo">生成</button> <button onclick="closePanel()">閉じる</button></div>
<div id="pout"></div>`);
document.getElementById("pgo").onclick=()=>{
const rc=+document.getElementById("prow").value, vc=+document.getElementById("pval").value, ag=document.getElementById("pagg").value;
const groups={};
for(let r=b+1;r<=y;r++){
CALC.depth=0;const k=String(CALC.val(A(rc,r)));CALC.depth=0;const v=CALC.val(A(vc,r));
if(!groups[k])groups[k]=[];if(typeof v==="number")groups[k].push(v);
}
const agg={sum:v=>v.reduce((a,b)=>a+b,0),count:v=>v.length,avg:v=>v.length?v.reduce((a,b)=>a+b,0)/v.length:0,max:v=>v.length?Math.max(...v):0,min:v=>v.length?Math.min(...v):0}[ag];
let rows=Object.entries(groups).map(([k,v])=>[k,Math.round(agg(v)*100)/100]);
rows.sort((p,q)=>q[1]-p[1]);
const total=Math.round(rows.reduce((s,r)=>s+r[1],0)*100)/100;
let h='<table class="pvt" style="width:100%;border:1px solid var(--bd);margin-top:6px"><tr><th style="text-align:left;background:var(--hd)">'+heads.find(x=>x.c===rc).name+'</th><th style="text-align:right;background:var(--hd)">'+document.getElementById("pagg").selectedOptions[0].text+' / '+heads.find(x=>x.c===vc).name+'</th></tr>';
rows.forEach(r=>h+='<tr><td>'+r[0]+'</td><td style="text-align:right">'+r[1].toLocaleString()+'</td></tr>');
h+='<tr style="font-weight:600;border-top:2px solid #666"><td>総計</td><td style="text-align:right">'+total.toLocaleString()+'</td></tr></table>';
h+='<div style="margin-top:6px"><button id="pins">新シートへ書き出し</button></div>';
document.getElementById("pout").innerHTML=h;
document.getElementById("pins").onclick=()=>{
let i=wb.order.length+1;while(wb.sheets["Pivot"+i])i++;
const name="Pivot"+i;wb.sheets[name]={cells:{},fmt:{}};wb.order.push(name);
const cells=wb.sheets[name].cells;
cells["A1"]=heads.find(x=>x.c===rc).name;cells["B1"]="集計";
rows.forEach((r,idx)=>{cells["A"+(idx+2)]=r[0];cells["B"+(idx+2)]=String(r[1]);});
cells["A"+(rows.length+2)]="総計";cells["B"+(rows.length+2)]="=SUM(B2:B"+(rows.length+1)+")";
wb.active=name;renderAll();msg("ピボットを"+name+"に書き出しました");
};
};
document.getElementById("pgo").click();
}
function uiMacro(){
openPanel(`<h3>マクロ(VBA風API)</h3>
<p class="mini">Range("A1")/Range("A1:C5") .value .values .set(v) .clear() .bold() .fill(色) / Cells(r,c) / Selection / MsgBox(t) / InputBox(t) / ActiveSheet.name</p>
<textarea id="mcode">// 例: 純増2000超に★、書式も設定
for (let r = 2; r <= 7; r++) {
if (Range("D"+r).value > 2000) {
Range("G"+r).set("★好調");
Range("G"+r).fill("#FFF2CC");
} else {
Range("G"+r).clear();
}
}
Range("G1").set("判定"); Range("G1").bold();
MsgBox("完了: " + ActiveSheet.name + " / 選択=" + Selection.address);</textarea>
<div><button class="go" id="mrun">実行 (F5)</button> <button onclick="closePanel()">閉じる</button></div>
<div id="mout" class="mini" style="margin-top:6px;white-space:pre-wrap"></div>`);
const run=()=>{
const out=document.getElementById("mout");out.textContent="";
const mkRange=spec=>{
spec=String(spec).toUpperCase();
let [p1,p2]=spec.includes(":")?spec.split(":").map(parseA):[parseA(spec),parseA(spec)];
if(!p1||!p2)throw new Error("不正な参照: "+spec);
const each=fn=>{for(let c=Math.min(p1[0],p2[0]);c<=Math.max(p1[0],p2[0]);c++)for(let r=Math.min(p1[1],p2[1]);r<=Math.max(p1[1],p2[1]);r++)fn(A(c,r));};
return{
get address(){return A(p1[0],p1[1])+(spec.includes(":")?":"+A(p2[0],p2[1]):"");},
get value(){CALC.depth=0;return CALC.val(A(p1[0],p1[1]));},
get values(){return CALC.rangeCells(A(p1[0],p1[1]),A(p2[0],p2[1]));},
get formula(){return sh().cells[A(p1[0],p1[1])]||"";},
set:v=>each(a=>{sh().cells[a]=String(v);if(String(v)==="")delete sh().cells[a];}),
clear:()=>each(a=>{delete sh().cells[a];delete sh().fmt[a];}),
bold:on=>each(a=>{sh().fmt[a]=sh().fmt[a]||{};sh().fmt[a].b=on!==false;}),
fill:col=>each(a=>{sh().fmt[a]=sh().fmt[a]||{};sh().fmt[a].bg=col;})
};
};
const api={
Range:mkRange,
Cells:(r,c)=>mkRange(A(c-1,r)),
get Selection(){const[a,b,x,y]=selRect();return mkRange(A(a,b)+":"+A(x,y));},
MsgBox:t=>{out.textContent+="MsgBox: "+t+"\n";},
InputBox:t=>prompt(t)||"",
ActiveSheet:{get name(){return wb.active;}}
};
try{
pushUndo();
Function("Range","Cells","Selection","MsgBox","InputBox","ActiveSheet",document.getElementById("mcode").value)(api.Range,api.Cells,api.Selection,api.MsgBox,api.InputBox,api.ActiveSheet);
renderAll();msg("マクロ実行完了");
}catch(e){out.textContent+="エラー: "+e.message;msg("マクロエラー");}
};
document.getElementById("mrun").onclick=run;
document.getElementById("mcode").addEventListener("keydown",e=>{if(e.key==="F5"){e.preventDefault();run();}e.stopPropagation();});
}
// ==== file I/O ====
function saveXlsx(){
const wbo=XLSX.utils.book_new();
wb.order.forEach(name=>{
const cells=wb.sheets[name].cells,aoa=[];
Object.keys(cells).forEach(a=>{
const p=parseA(a);if(!p)return;
while(aoa.length<p[1])aoa.push([]);
const old=wb.active;wb.active=name;CALC.depth=0;
const raw=cells[a];
aoa[p[1]-1][p[0]]=String(raw)[0]==="="?CALC.evalCell(a):coerce(raw);
wb.active=old;
});
XLSX.utils.book_append_sheet(wbo,XLSX.utils.aoa_to_sheet(aoa),name);
});
XLSX.writeFile(wbo,(document.getElementById("fname").textContent||"Book1")+".xlsx");
msg("xlsxを保存しました(数式は計算値として保存)");
}
function saveCsv(){
const cells=sh().cells;let maxR=0,maxC=0;
Object.keys(cells).forEach(a=>{const p=parseA(a);if(p){maxC=Math.max(maxC,p[0]);maxR=Math.max(maxR,p[1]);}});
let lines=[];
for(let r=1;r<=maxR;r++){const row=[];for(let c=0;c<=maxC;c++){CALC.depth=0;let v=cells[A(c,r)]===undefined?"":CALC.val(A(c,r));v=String(v);if(/[",\n]/.test(v))v='"'+v.replace(/"/g,'""')+'"';row.push(v);}lines.push(row.join(","));}
const blob=new Blob(["\uFEFF"+lines.join("\n")],{type:"text/csv"});
const aEl=document.createElement("a");aEl.href=URL.createObjectURL(blob);aEl.download=wb.active+".csv";aEl.click();
msg("CSVを保存しました");
}
function openFile(file){
const rd=new FileReader();
rd.onload=e=>{
pushUndo();
if(file.name.endsWith(".csv")){
const txt=e.target.result.replace(/^\uFEFF/,"");
const cells={};
txt.split(/\r?\n/).forEach((line,r)=>{
let c=0,cur="",inQ=false,ci=0;
const putc=()=>{if(cur!=="")cells[A(ci,r+1)]=cur;cur="";ci++;};
for(const ch of line){
if(inQ){if(ch==='"')inQ=false;else cur+=ch;}
else if(ch==='"')inQ=true;
else if(ch===",")putc();
else cur+=ch;
}
putc();
});
sh().cells=cells;sh().fmt={};
}else{
const wbi=XLSX.read(e.target.result,{type:"array"});
wb={sheets:{},order:[],active:wbi.SheetNames[0]};
wbi.SheetNames.forEach(n=>{
const ws=wbi.Sheets[n],cells={};
Object.keys(ws).forEach(a=>{
if(a[0]==="!")return;
const cell=ws[a];
cells[a]=cell.f?"="+cell.f:String(cell.v);
});
wb.sheets[n]={cells,fmt:{}};wb.order.push(n);
});
alert("注意: 条件付き書式・図形・VBAマクロは読み込み対象外です(非対応)。上書き保存するとこれらは失われるため、保存は別名で行われます。");
}
document.getElementById("fname").textContent=file.name.replace(/\.(xlsx|csv)$/,"")+"(復元)";
renderAll();msg(file.name+" を開きました");
};
file.name.endsWith(".csv")?rd.readAsText(file):rd.readAsArrayBuffer(file);
}
// ==== ribbon wiring ====
document.getElementById("tabstrip").addEventListener("click",e=>{
const t=e.target.closest(".rtab");if(!t)return;
document.querySelectorAll(".rtab").forEach(x=>x.classList.remove("on"));t.classList.add("on");
["home","insert","file"].forEach(n=>document.getElementById("rb-"+n).style.display=n===t.dataset.tab?"flex":"none");
});
function autoSum(){
const[c,r]=cur;let r0=r-1;
while(r0>=1){CALC.depth=0;const v=CALC.val(A(c,r0));if(typeof v!=="number"||sh().cells[A(c,r0)]===undefined)break;r0--;}
r0++;
if(r0>=r){msg("上に数値データがありません");return;}
pushUndo();sh().cells[A(c,r)]="=SUM("+A(c,r0)+":"+A(c,r-1)+")";renderAll();msg("オートSUM: =SUM("+A(c,r0)+":"+A(c,r-1)+")");
}
function sortSel(){
const[a,b,x,y]=selRect();if(y<=b){msg("複数行を選択してください");return;}
pushUndo();
const rows=[];
for(let r=b;r<=y;r++){const row=[];for(let c=a;c<=x;c++)row.push(sh().cells[A(c,r)]);rows.push(row);}
const keyC=cur[0]-a>=0&&cur[0]<=x?cur[0]-a:0;
rows.sort((p,q)=>{const pv=coerce(p[keyC]??""),qv=coerce(q[keyC]??"");return(typeof qv==="number"?qv:0)-(typeof pv==="number"?pv:0);});
rows.forEach((row,i)=>row.forEach((v,j)=>{const addr=A(a+j,b+i);if(v===undefined)delete sh().cells[addr];else sh().cells[addr]=v;}));
renderAll();msg("降順で並べ替えました(キー列: "+colL(a+keyC)+")");
}
document.body.addEventListener("click",e=>{
if(e.target.closest("#colorPalette"))return;
const colorButton=e.target.closest(".colorbtn");
if(colorButton){
e.stopPropagation();
showColorPalette(colorButton,colorButton.querySelector("#bgcol")?"bg":"fg");
return;
}
document.getElementById("colorPalette").style.display="none";
const b=e.target.closest("[data-cmd]");if(!b)return;
const cmd=b.dataset.cmd;
if(cmd==="bold")toggleFmt("b");
else if(cmd==="italic")toggleFmt("i");
else if(cmd==="border")setFmt("bd",true);
else if(cmd==="comma")toggleFmt("comma");
else if(cmd==="percent")toggleFmt("percent");
else if(cmd==="decinc")adjustDecimals(1);
else if(cmd==="decdec")adjustDecimals(-1);
else if(cmd==="clearfmt"){pushUndo();eachSel(a=>delete sh().fmt[a]);renderAll();}
else if(cmd==="undo")undo();
else if(cmd==="redo")redo();
else if(cmd==="copy")copySel();
else if(cmd==="cut"){copySel();delSel();}
else if(cmd==="paste")pasteClip();
else if(cmd==="alleft")setFmt("al","left");
else if(cmd==="alcenter")setFmt("al","center");
else if(cmd==="alright")setFmt("al","right");
else if(cmd==="autosum")autoSum();
else if(cmd==="sortd")sortSel();
else if(cmd==="chart")uiChart();
else if(cmd==="pivot")uiPivot();
else if(cmd==="macro")uiMacro();
else if(cmd==="open")document.getElementById("filein").click();
else if(cmd==="savex")saveXlsx();
else if(cmd==="savec")saveCsv();
});
const PALETTE=["#000000","#404040","#808080","#BFBFBF","#FFFFFF","#C00000","#FF0000","#FFC000","#FFFF00","#92D050","#00B050","#00B0F0","#0070C0","#002060","#7030A0","#E2F0D9","#DDEBF7","#FFF2CC","#FCE4D6","#F4CCCC","#D9EAD3","#CFE2F3","#D9D2E9","#EADCF8"];
function showColorPalette(anchor,type){
const pop=document.getElementById("colorPalette");
pop.dataset.type=type;
pop.innerHTML='<div class="ptitle">'+(type==="bg"?"塗りつぶしの色":"フォントの色")+'</div><div class="pgrid">'+
PALETTE.map(c=>'<button class="sw" style="--c:'+c+'" data-color="'+c+'" title="'+c+'"></button>').join("")+
'</div><div class="pactions"><button data-clear="1">色なし</button><button data-close="1">閉じる</button></div>';
const r=anchor.getBoundingClientRect();
pop.style.left=Math.min(r.left,innerWidth-220)+"px";
pop.style.top=Math.min(r.bottom+6,innerHeight-210)+"px";
pop.style.display="block";
}
document.getElementById("colorPalette").addEventListener("click",e=>{
e.stopPropagation();
const pop=e.currentTarget,type=pop.dataset.type;
const sw=e.target.closest("[data-color]");
if(sw){
const c=sw.dataset.color;
if(type==="bg"){document.getElementById("bgbar").style.background=c;setFmt("bg",c);}
else{document.getElementById("fgbar").style.background=c;setFmt("fg",c);}
pop.style.display="none";
return;
}
if(e.target.closest("[data-clear]")){
pushUndo();
eachSel(a=>{sh().fmt[a]=sh().fmt[a]||{};delete sh().fmt[a][type==="bg"?"bg":"fg"];});
if(type==="bg")document.getElementById("bgbar").style.background="transparent";
else document.getElementById("fgbar").style.background="transparent";
renderAll();pop.style.display="none";return;
}
if(e.target.closest("[data-close]"))pop.style.display="none";
});
document.getElementById("fsize").addEventListener("change",e=>setFmt("fs",+e.target.value));
document.getElementById("filein").addEventListener("change",e=>{if(e.target.files[0])openFile(e.target.files[0]);e.target.value="";});
function setZoom(pct){
const next=Math.max(25,Math.min(200,Math.round(pct/5)*5));
zoom=next/100;
document.getElementById("zoomrange").value=String(next);
document.getElementById("zoompct").textContent=next+"%";
applySizes();renderAll();msg("ズーム: "+next+"%");
}
document.getElementById("zoomrange").addEventListener("input",e=>setZoom(+e.target.value));
document.getElementById("zminus").addEventListener("click",()=>setZoom(zoom*100-5));
document.getElementById("zplus").addEventListener("click",()=>setZoom(zoom*100+5));
document.getElementById("gridwrap").addEventListener("wheel",e=>{
if(!(e.ctrlKey||e.metaKey))return;
e.preventDefault();
setZoom(zoom*100+(e.deltaY<0?5:-5));
},{passive:false});
document.getElementById("fx").addEventListener("input",e=>previewFormula(e.target.value));
document.getElementById("fx").addEventListener("focus",e=>previewFormula(e.target.value));
document.getElementById("fx").addEventListener("blur",clearFormulaPreview);
document.addEventListener("paste",e=>{
if(editTd||["INPUT","TEXTAREA"].includes(document.activeElement.tagName))return;
const text=e.clipboardData&&e.clipboardData.getData("text/plain");
if(!text)return;
e.preventDefault();
pasteMatrix(parseClipboardText(text));
});
document.getElementById("cellname").addEventListener("click",e=>{e.target.removeAttribute("readonly");e.target.select();});
document.getElementById("cellname").addEventListener("keydown",e=>{
if(e.key==="Enter"){const p=parseA(e.target.value.toUpperCase());if(p&&p[0]<COLS&&p[1]<=ROWS){cur=[p[0],p[1]];anchor=[...cur];selEnd=[...cur];renderAll();scrollToCur();}e.target.setAttribute("readonly","");document.getElementById("gridwrap").focus();}
e.stopPropagation();
});
// ==== seed data ====
(function(){
const seed=[
["月","新規MRR","解約","純増","商談数","受注数","判定","","単価表","プラン","単価"],
["1月",1200,200,"=B2-C2",24,6,"","","","Lite",5000],
["2月",1500,180,"=B3-C3",28,8,"","","","Std",15000],
["3月",2100,250,"=B4-C4",35,11,"","","","Pro",40000],
["4月",1800,300,"=B5-C5",30,9],
["5月",2400,220,"=B6-C6",38,13],
["6月",2900,260,"=B7-C7",42,15],
["合計","=SUM(B2:B7)","=SUM(C2:C7)","=SUM(D2:D7)","=SUM(E2:E7)","=SUM(F2:F7)"],
["受注率","=F8/E8","平均純増","=AVERAGE(D2:D7)","最高月","=INDEX(A2:A7,MATCH(MAX(D2:D7),D2:D7,0))"],
["Pro単価","=VLOOKUP(\"Pro\",J2:K4,2,FALSE)","好調月数","=COUNTIF(D2:D7,\">2000\")"]
];
const cells=sh().cells;
seed.forEach((row,r)=>row.forEach((v,c)=>{if(v!==""&&v!==undefined)cells[A(c,r+1)]=String(v);}));
["A1","B1","C1","D1","E1","F1","G1","J1","K1"].forEach(a=>sh().fmt[a]={b:true,bg:"#E1F5EE"});
["B8","C8","D8","E8","F8"].forEach(a=>sh().fmt[a]={b:true,comma:true});
})();
buildGrid();renderAll();
</script>
</body>
</html>