業務アプリのファイルアップロード画面の横には、誰も作っていないショートカットがあります。ユーザーは手元の Excel を開き、必要な範囲を選び、あなたのページで Ctrl+V を押す ― アップロードも、列マッピング画面も、待ち時間もありません。プロダクトの中でいちばん速い取り込み経路であり、そしてたいていの Web アプリでは、それがテキスト入力欄1つにタブ区切りの長い1行として落ちて終わります。
この記事は、そのときクリップボードに実際に何が載っているのか、そのうちどれだけが ReoGrid Web まで届くのか、そして「すべて正しく動いているのに見た目が違う」2つの値についての話です。
ペイロードは1つではなく2つ
表計算ソフトのコピーは、同じ選択範囲を2通りの形式で書き出します。
text/plain は最大公約数、つまり TSV です。セルの区切りはタブ、行の区切りは改行、値にタブ・改行・引用符が含まれる場合は引用符で囲む。どの表計算ソフトも読め、どのテキストエディタでも見え、そして値しか運びません。
text/html がリッチな側です。中身は本物の <table> で、各 <td> にセルの文字列と、フォント・太さ・色・配置といった見た目が inline CSS で付いています。Word やメール本文に貼ったときに書式が保たれるのはこれのおかげで、Excel・Google スプレッドシート・LibreOffice が共通で使う、事実上のリッチクリップボード標準です。
ReoGrid Web はコピー時に両方を書き出し、貼り付け時は HTML を優先して読み、HTML を持たない相手からは TSV に落とします。
Ctrl+V と、出てこない権限ダイアログ
ブラウザでクリップボードを読む方法は2つあり、両者は等価ではありません。
navigator.clipboard.read() はいつでも呼べる代わりに、Chrome ではユーザーに権限ダイアログを出してから中身を渡します。いっぽう paste イベントは、event.clipboardData にデータが最初から付いた状態で飛んできます ― ユーザーがたった今そのキーを押したので、確認する相手がいないからです。
そのため ReoGrid Web は Ctrl/Cmd+V を意図的に preventDefault() しません。キー入力はブラウザに素通りし、グリッドのコンテナで native な paste イベントが発火し、ハンドラはそこからペイロードを読みます。
// 貼り付けのたびにグリッド内部で起きていること。
const html = event.clipboardData?.getData('text/html');
if (html) { /* <table> をパースする */ }
else { /* text/plain(TSV)にフォールバック */ }
UI 側への影響はひとつ。ツールバーの「貼り付け」ボタンは Ctrl+V と同じ機能ではありません。読み取り元の paste イベントが無いので Clipboard API を通るしかなく、権限ダイアログが出ることがあります。
const clipboard = grid.keyboardController.clipboardService;
document.querySelector('#paste')!.addEventListener('click', () => {
clipboard.pasteFromClipboardApi(); // navigator.clipboard.read() ― 確認が出ることがある
});
// コピーとカットは「書く」側なので、この問題は起きない。
document.querySelector('#copy')!.addEventListener('click', () => clipboard.copy());
document.querySelector('#cut')!.addEventListener('click', () => clipboard.cut());
ボタンが必要なら置いて構いませんが、確実に通るのはキーボード経路のほうです。
外部アプリからの貼り付けで残るもの
届いた HTML に ReoGrid の目印が無ければ、パーサはそれを外部由来として扱い、各セルから2つを取り出します。文字列と、inline に書かれたスタイルです。読むのは font-family / font-size / font-weight(700 以上で太字)/ font-style / text-decoration: underline / color / background-color / text-align / vertical-align。
以下は、表計算ソフトがクリップボードに載せる形のペイロードを、Ctrl+V と同じ経路で A1 に貼り付けたものです。
import { createReogrid } from '@reogrid/lite';
const grid = createReogrid({ workspace: '#grid' });
const ws = grid.worksheet;
const html = `<table>
<tr>
<td style="font-weight:700;background:#dbeafe;text-align:center">受注番号</td>
<td style="font-weight:700;background:#dbeafe;text-align:center">日付</td>
<td style="font-weight:700;background:#dbeafe;text-align:center">商品名</td>
<td style="font-weight:700;background:#dbeafe;text-align:center">数量</td>
<td style="font-weight:700;background:#dbeafe;text-align:center">単価</td>
</tr>
<tr><td>SO-1042</td><td>2026/09/01</td><td>ノートPCスタンド</td><td>3</td><td>¥4,800</td></tr>
<tr><td>SO-1043</td><td>2026/09/02</td><td>ワイヤレスマウス</td><td>8</td><td>¥2,600</td></tr>
<tr><td>SO-1044</td><td>2026/09/03</td>
<td style="color:#b91c1c">ライセンス(1年)</td><td>1</td><td>¥18,000</td></tr>
</table>`;
const dt = new DataTransfer();
dt.setData('text/html', html);
dt.setData('text/plain', '受注番号\t日付\t商品名\t数量\t単価\n…');
ws.selection.moveTo('A1'); // 貼り付け先はアクティブセル
grid.keyboardController.clipboardService.pasteFromEvent(
new ClipboardEvent('paste', { clipboardData: dt }),
);
なお、この最後のコードはそのまま貼り付けのテストの書き方でもあります。DataTransfer を組み立てて pasteFromEvent に渡し、セルを検証する ― 実クリップボードを触らずに再現できます。
ただしクリップボードは見た目のチャネルです。そこから2つのことが導かれます。
CSS クラス側に書かれたスタイルは渡ってきません(パーサが読むのは td.style であって <style> ブロックではありません)。そして各 <td> の文字列は、コピー元が表示していた内容であって、保持していた値ではありません。後者が、上の2列の話につながります。
46266 として届く日付
2026/09/01 はクリップボード上では文字列です。ReoGrid Web は日付の形をした入力を認識してシリアル値に変換し、日付として読める書式を当てます ― ユーザーがセルに直接打ち込んだときに起きるのはこの動作です。
ところが貼り付けでは、値の書き込みに続いて「貼り付け元セルの状態」が適用されます。書式を持たないセルが届くと、貼り付け先の書式は消去されます。自動で当たったはずの日付書式もその巻き添えになり、シリアル値がそのまま 46266 と表示されます。
値そのものは正しく、日付として並べ替えも比較も DATEDIF も通ります。欠けているのは書式だけです。
¥4,800 はちょうど裏返しです。コピー元が保持していたのは 4800 で、描画していたのが ¥4,800。クリップボードが運ぶのは描画のほうです。Number('¥4,800') は NaN なので、セルは文字列を抱えます ― 左寄せで、SUM からは見えません。
貼り付いた範囲を直す
どちらも、貼り付いた範囲を1回なめれば片付きます。そしてその範囲は必ず分かります ― 貼り付け後は、貼り付いた範囲がそのまま選択されているからです。
function normalizePastedBlock(ws) {
const b = ws.selection.bounds!; // いま貼り付いたブロックそのもの
const firstDataRow = b.topRow + 1; // 1行目は貼り付いた見出し行
// B列はこの表では日付列 ― 書式を与え直す
ws.range(firstDataRow, 1, b.bottomRow, 1).setFormat('yyyy/mm/dd');
// E列は文字列で届いている: 表示用の装飾を落として、数値だけ残す
for (let r = firstDataRow; r <= b.bottomRow; r++) {
const raw = ws.getCellInput(r, 4) ?? '';
const n = Number(raw.replace(/[^0-9.-]/g, ''));
if (raw && Number.isFinite(n)) ws.setCellInput(r, 4, String(n));
}
ws.range(firstDataRow, 4, b.bottomRow, 4).setFormat('¥#,##0');
}
コンテナ自身の paste イベントに繋いでおけば、貼り付けのたびに、グリッドの処理が終わったあとで走ります。
document.querySelector('#grid')!.addEventListener('paste', () => {
// グリッドのハンドラ(より内側の要素)が先に走る。ここに来た時点で
// セルは書き込み済みで、選択範囲は貼り付いたブロックになっている。
normalizePastedBlock(ws);
});
手軽さよりファイルの中身が大事な場面 ― 型も罫線も結合も生きた数式も必要な場面 ― は、そもそもクリップボードの仕事ではありません。.xlsx をそのまま読み込むほうが無損失で、しかもブラウザから一歩も出ません。
逆向きのコピー
範囲を選んで Ctrl/Cmd+C し、Excel に貼れば、見出しの書式も色も結合も一緒に届きます。Excel が text/html 側を読むからです。ターミナルやテキストエディタに貼れば TSV が出てきます。
TSV に何が入るかは正確に書いておく価値があります ― 各セルの表示テキストです。日付セルは 46266 ではなく 2026/09/01、数式セルは計算結果(=… のソースは決して出ません。結果が空なら空欄)。これは Excel 自身の挙動でもあり、グリッドからのコピーがどこに貼っても筋の通った結果になる理由です。
裏返せば、数式はクリップボード経由では Excel に渡りません。相手先で生きた数式が必要なら、シートごと書き出します ― saveAsXlsx() は Excel が再計算できる本物の SUMIFS や XLOOKUP を書きます。詳しくは ブラウザでの xlsx 読み書き に書きました。
Excel の挙動をひとつ意図的に真似ている箇所もあります。Ctrl+クリックで作った複数選択のうち、矩形が揃っていないものをコピーしようとすると拒否され、Excel と同じ文言 ―「この操作は複数の選択範囲に対しては機能しません」にあたる英文 ― がアクティブセルに出ます。
グリッド同士
ReoGrid Web のインスタンス同士 ― 2つのタブ、同じアプリの2つのペイン ― では、コピーがもう1層を運びます。目に見える HTML と並んで、各セルに data-rg-* 属性が付き、そこに生の状態が入ります。入力文字列(5280 ではなく =D2*1.1)、数値書式、セル型、リッチテキストの run。パーサはこの目印を見つけると、見た目を読む代わりに生の層を復元します。
したがってグリッド間でコピーした数式は数式のままです。日付は書式を保ち、ドロップダウンのセルはドロップダウンのまま届きます。
ひとつだけやらないことがあります ― 相対参照は書き換えられません。=D2*E2 を1行下にコピーしても =D2*E2 のままで、Excel のような参照のずらしは起きません。数式を列に流し込むのはフィルハンドルの仕事で、こちらは相対参照をきちんとずらします。貼り付け時の参照変換は設計上の割り切りではなく、未実装の項目です。
残りの角
ユーザーに見つけられる前に知っておく価値のあるもの。
- 結合は再現されません。 横方向に結合されたセルは先頭セルに値が入り、残りは空セルで埋まります。縦方向(
rowspan)は1セルとして読まれるため、その下の行がずれることがあります。構造は残りますが、結合そのものは残りません。 - 罫線は渡りません。 塗りとフォントは渡ります。罫線は予定であって実装ではありません。
- 入力規則は貼り付けにも効きます。
stop種別のルールに反する値はセル単位で静かに弾かれ、元の値が残ります。取り込み経路としては正しい挙動であり、貼り付けを信用するより列に規則を張る理由でもあります。 - 保護が優先されます。 保護シート上のロックされたセルが貼り付け先に1つでも含まれていれば、貼り付け全体が拒否され、代わりに保護セル編集イベントが飛びます。中途半端に書き込まれることはありません。
- Undo は1回です。 貼り付けは1つの undo 単位、カット&ペーストは1つのグループなので、Ctrl+Z で貼り付け先と元の範囲が同時に戻ります。ただしカットと貼り付けの間でシートを切り替えると「移動」は忘れられ、貼り付けはコピーとして振る舞い、元の範囲はそのまま残ります。
- 形式を選択して貼り付けはまだありません。 値のみ・書式のみ・行列入れ替え、そしてコピー範囲を囲む点線アニメーションも未実装です。
まとめ
Ctrl+V は「本来作るべきだった取り込み機能」の代用品ではありません。数百行までなら、それが取り込み機能そのものです。人がブラウザのタブに貼り付けようとするのは、省ける手続きの量がそれだけ大きいからです。
それを成立させるのは、貼り付け先がセルを理解していること ― 見出しの塗りを保ち、数値なら右に寄せ、貼り付いた範囲を選択範囲として渡し、1回の undo で全部戻せること。そこまで揃って初めて、Ctrl+V は機能になります。
API の一覧はクリップボードのドキュメントに、貼り付けが通るアクションモデルは Undo / Redo、それを止める仕組みはセル保護にあります。貼り付けでは足りなくなったら、xlsx をそのまま読む経路がすぐ隣にあります ― 同じグリッド、同じページ、アップロードなしのままで。