ReoGrid ReoGrid Web

Excel からの貼り付けで、実際にクリップボードを渡ってくるもの

· unvell team
Excel からの貼り付けで、実際にクリップボードを渡ってくるもの

業務アプリのファイルアップロード画面の横には、誰も作っていないショートカットがあります。ユーザーは手元の 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>&yen;4,800</td></tr>
  <tr><td>SO-1043</td><td>2026/09/02</td><td>ワイヤレスマウス</td><td>8</td><td>&yen;2,600</td></tr>
  <tr><td>SO-1044</td><td>2026/09/03</td>
      <td style="color:#b91c1c">ライセンス(1年)</td><td>1</td><td>&yen;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 }),
);
実行結果
貼り付けた直後。見出し行は太字・塗り・中央揃えのまま届き、赤い商品名も色を保っています。数量が右寄せなのは、グリッドが数値だと判断したから。要注意なのは B列と E列です。
貼り付けた直後。見出し行は太字・塗り・中央揃えのまま届き、赤い商品名も色を保っています。数量が右寄せなのは、グリッドが数値だと判断したから。要注意なのは B列と E列です。

なお、この最後のコードはそのまま貼り付けのテストの書き方でもあります。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 が再計算できる本物の SUMIFSXLOOKUP を書きます。詳しくは ブラウザでの 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 をそのまま読む経路がすぐ隣にあります ― 同じグリッド、同じページ、アップロードなしのままで。

ReoGrid Web を試してみる

React/Vue 向けの Canvas ベース Excel 互換スプレッドシートコンポーネント。 Lite は無料 — npm install 一発で始められます。

関連記事

JavaScriptスプレッドシートの VLOOKUP・XLOOKUP・SUMIFS ― 単価も集計も自動で追随する受注明細表

商品コードから単価を引き、区分ごとに集計する受注明細表をブラウザで組み立てる。VLOOKUP と XLOOKUP の違い、マスタに無いコードの扱い(if_not_found・IFNA・IFERROR)、SUMIF / SUMIFS / COUNTIFS の criteria 記法と引数順の落とし穴、そして ReoGrid Web に「無い」2つの参照関数まで。

JavaScriptスプレッドシートでピボットテーブル ― 数行のAPIで、ライブ更新されるクロス集計を

数百行の生データを、データの変更に追随して再計算されるクロス集計表に。createPivot 一発で行・列・集計を宣言し、ハンドルの update / refresh / remove でコードから操作。地域×商品の売上を使った完全な実例と、フィルター・総計・Lite 対 Pro の境界線まで。

JavaScriptスプレッドシートに「並べ替えとフィルター」を ― Excel 風オートフィルターを、クリックでもコードでも

ブラウザのデータ表に、Excel 風の列の並べ替えとヘッダードロップダウンのフィルターを追加。createAutoFilter 一発でユーザーにドロップダウンを、getColumnValues / setColumnFilter で同じフィルターをコードから操作。表示行数のライブ表示付きの実例と、並べ替え対フィルター・Lite 対 Pro の勘どころまで。

ニュースレター

開発の最新情報をお届けします

新しいリリース・機能追加・お知らせをいち早く受け取るには、
メーリングリストにご登録ください。