ユースケース
- Excelから社内ブログやWikiに表をコピペしたい
実装
とりあえず paste
イベントをうけて処理するのでそのようにする。今回はCodeMirrorで制御されているtextarea
なので、CodeMirrorを使ってないない場合は適宜読み替えてください。
ClipboardData
pasteで発行されるClipBoardEventに clipboardData: DataTransfer
プロパティがあります。
spec: https://www.w3.org/TR/clipboard-apis/
// Web IDL
dictionary ClipboardEventInit : EventInit {
DataTransfer? clipboardData = null;
};
こいつが items: DataTransferItemList
をもっています。
// Web IDL
interface DataTransfer {
attribute DOMString dropEffect;
attribute DOMString effectAllowed;
[SameObject] readonly attribute DataTransferItemList items;
void setDragImage(Element image, long x, long y);
/* old interface */
[SameObject] readonly attribute DOMString[] types;
DOMString getData(DOMString format);
void setData(DOMString format, DOMString data);
void clearData(optional DOMString format);
[SameObject] readonly attribute FileList files;
}
なぜクリップボードの中身がリスト状の items
になっているかというと、コピーしたものの表現が単一ではないからですね。
たとえば、ブラウザからのコピペはスタイルが適用されているので、 text/plain
なプレインテキストと text/html
なHTMLテキスト両方を含みます。画像の場合、参照URLがtext/plain
で、画像のバイナリデータがimage/*
で来ます。ただし、詳細はブラウザごとの違いも多く、たとえばLibreOfficeのスプレッドシートからのペーストは、Chromeだと画像化したtext/png
なitemが含まれますが、IEやFirefoxではテキストデータのみ存在します。
// Web IDL
interface DataTransferItemList {
readonly attribute unsigned long length;
getter DataTransferItem (unsigned long index);
}
さて、このitems
の要素は DataTransferItem
で、クリップボードの中身の実体です。kind
は文字列かファイルかという情報で、type
がMIME typeです。ただし、Firefoxなどitems
の存在しないブラウザもあります。まだ仕様が標準化されていないからでしょう。
// Web IDL
interface DataTransferItem {
readonly attribute DOMString kind; // "string" or "file"
readonly attribute DOMString type; // "text/plain", "image/png", ...
void getAsString(FunctionStringCallback? _callback);
File? getAsFile();
};
callback FunctionStringCallback = void (DOMString data);
ここで、ExcelのシートからChromeにペーストすると、 items
の中身は text/plain
, text/html
, image/png
が入っており、最後の画像データは該当部分のスクショになっています。Excelの要素をGitHub issuesのtextarea
にExcelからコピペすると、画像が貼り付けられるのはそのためです。
画像のコピーの場合、items
は text/plain
, image/png
なので、text/html
の有無が画像コピーとの違いとなります。
なので、「画像のコピーは画像としてアップロードして貼り付けて、Excelからのコピペは<table>...</table>
に変換して貼り付ける」とするならば、以下のロジックでよさそうです。
text/plain
と text/html
のデータが存在して、 text/html
のなかに table
要素が含まれるとき、その table
要素をペーストする
以上を加味すると、ペーストイベントをハンドルするイベントリスナは以下のようになるでしょう。
var editor = CodeMirror.fromTextArea(...);
editor.on('paste', function(_, e) {
const clipboardData = e.clipboardData;
const plainTextItem = clipboardData.getData('text/plain');
const rtfItem = clipboardData.getData('text/rtf');
const htmlItem = clipboardData.getData('text/html');
const imageItem = (function (items) {
if (!items) {
return null;
}
for (let i = 0; i < items.length; i++) {
if (items[i].type.startsWith('image/')) {
return items[i];
}
}
return null;
})(clipboardData.items);
console.log(clipboardData.types);
console.log([plainTextItem, rtfItem, htmlItem, imageItem]);
if (htmlItem && plainTextItem) {
const html = new DOMParser().parseFromString(htmlItem, 'text/html');
const tables = html.getElementsByTagName('table');
if (tables.length) {
const content = html.getElementsByTagName('body')[0].innerHTML;
editor.replaceSelection(content);
e.preventDefault();
e.stopPropagation();
} else {
TODO
}
} else if (imageItem) {
const blob = imageItem.getAsFile();
const matched = blob.type.match(/^image\/(\w+)$/);
if (matched) {
const ext = matched[1];
const filename = 'clipboard.' + ext;
const formData = new FormData();
formData.append('file', blob, filename);
upload(formData);
e.preventDefault();
e.stopPropagation();
}
}
});
というわけで実装してみたものの、だいぶ複雑だしブラウザによる挙動の違いもあるのでちょっと使いづらい部分があります。
TSVやCSVをコードスニペットとして貼り付けるとtableとして表示する、というほうがいいかもしれないですね。