ユースケース
- 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
要素をペーストする
以上を加味すると、ペーストイベントをハンドルするイベントリスナは以下のようになるでしょう。
// JavaScript var editor = CodeMirror.fromTextArea(...); editor.on('paste', function(_, e) { // see https://html.spec.whatwg.org/multipage/interaction.html 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) { // rich contents, such as Excel and Power Point const html = new DOMParser().parseFromString(htmlItem, 'text/html'); const tables = html.getElementsByTagName('table'); if (tables.length) { // includes <table> tags const content = html.getElementsByTagName('body')[0].innerHTML; editor.replaceSelection(content); e.preventDefault(); e.stopPropagation(); } else { // TODO: ask users what to paste (HTML or plain text) // fallback to default } } else if (imageItem) { const blob = imageItem.getAsFile(); const matched = blob.type.match(/^image\/(\w+)$/); if (matched) { const ext = matched[1]; // e.g. "png", "jpeg", or "gif" const filename = 'clipboard.' + ext; const formData = new FormData(); formData.append('file', blob, filename); upload(formData); e.preventDefault(); e.stopPropagation(); } } });
というわけで実装してみたものの、だいぶ複雑だしブラウザによる挙動の違いもあるのでちょっと使いづらい部分があります。
TSVやCSVをコードスニペットとして貼り付けるとtableとして表示する、というほうがいいかもしれないですね。