前回も書いた通り、ブログを作りました。
このブログには画像が貼れるのですが、今まではファイルをアップロードする形でしか画像を貼ることができませんでした。
どこかでスマホで撮ってきた写真を貼る、という使い方ならそれでよいですが、ちょっとしたスクショとか、amazonの書影とかを貼るときにいちいちファイルを作るのも面倒なので、クリップボードから貼り付けられるようにしました。画面としては、以下のように、「クリップボードから貼り付け」ボタンを追加しています。
このボタンは、普通に<button>タグで作ったHTML上のボタンで、これを押すとJavaScriptの関数が普通に呼び出されます。JavaScriptの関数が普通に実行されただけでクリップボードの中身がJavaScriptに抜かれてしまうのではセキュリティ的に大問題ですので、私が使っているブラウザであるFirefoxでは、「クリップボードから貼り付け」ボタンを押すとこんなふうに「貼り付け」ボタンが出て、ユーザが明示的にこれを押さないとクリップボードの内容はコピーされません。
Edgeの場合は、こんなポップアップが出ます。「許可」を押せばペーストできます。
クリップボードから画像を貼りたければ、現状、「contenteditable属性がtrueの要素に貼り付けさせる」という方法を取ることが多いようです。たとえばこの下にはcontenteditable属性がtrueのp要素が貼ってありますが、適当な画像をコピーして、ここをクリックしてフォーカスを当ててからCtrl-Vとかでペーストすると、画像が貼れます。
ここには画像が貼れる
こうしたうえで、この要素からimg要素を取り出すことで、画像が取り出せるそうです。私は試してませんが、検索するとQiitaで以下のような記事が見つかります。
クリップボードに貼り付けた画像を取得する javascript
この方法なら、ユーザが明示的にCtrl-Vを押すわけなので、前述のような警告は出ません。contenteditableがtrueの要素にフォーカスが当たっている状態なら使い勝手もよさそうですが、うちのブログのテキスト入力エリアは前回も書いたとおりただのtextareaなので、contenteditable属性がtrueの要素は画面上にありません。それを追加して、「ここにフォーカスを当ててからペーストしてください」とするのはいまいち迂遠なので※1、今回はクリップボードAPIを使って直接クリップボードから画像を取り出すようにしました。
クリップボードから画像(バイナリデータ)を取り出すには、Clipboard APIのclipboard.read()メソッドを使います。使い方はMDNの以下のページに載っています。
Clipboard.read() - Web API | MDN
これを元にして作ったサンプルが以下になります。
まずはコピー元画像※2。別にこの画像でなくてもよいですが、右クリックしてコピーしてください。
次にこの下の「貼り付ける」ボタンを押すと、下のimg要素に画像が貼りつけられます。
これを行っているJavaScriptは以下。実物は、このページのHTMLの一番下にあります。
const destinationImage = document.querySelector("#destination"); async function pasteImage() { /* const permission = await navigator.permissions.query({ name: "clipboard-read", }); console.log("permission.state.." + permission.state); */ const clipboardContents = await navigator.clipboard.read(); for (const item of clipboardContents) { if (!item.types.includes("image/png")) { throw new Error("Clipboard contains non-image data."); } const blob = await item.getType("image/png"); destinationImage.src = URL.createObjectURL(blob); } }
4~9行目のコメントアウト部分はちょっと置いておいて、11行目でクリップボードの内容を取得しています。clipboard.read()の戻り値は(プロミス解決後は)ClipboardItemの配列とのことなので、そこからMIMEタイプがimage/pngの要素を取り出しています(16行目)。その後17行目でURL.createObjectURL()でオブジェクト化してURLを取得していますが、ここはこのページのデモ向けで、ブログでの画像貼り付けではここからファイルをアップロードしてしまうので使っていません。
コピー元の画像として上のラーメン画像を使った人は、「貼り付ける」ボタンを押したとき、Firefoxでは「貼り付け」のボタンが出ないことに気が付いたかもしれません。ブラウザ内で同一ページからコピーした画像では、このボタンは出ないようです。クリップボードにあるものがJavaScriptに勝手に取得されるのは危険ですが、同一ページのものなら問題ない、ということなのでしょう。なお、Edgeでは、同一ページのラーメン画像でもポップアップが出ました。
4~9行目部分はコメントアウトしていますが、このコメントアウトを外すと、Firefoxでは以下のエラーが出ます。
Uncaught (in promise) TypeError: 'clipboard-read' (value of 'name' member of PermissionDescriptor) is not a valid value for enumeration PermissionName.
これは、上記MDNのページに、以下のように書いてあるとおりの挙動ですね。
メモ: 現時点では、Firefoxは read() を実装していますが、 "clipboard-read" 権限を認識しないため、権限 API を使ってアクセス管理をしようとしてもうまくいきません。
Edgeだとエラーにならずにちゃんと動いて、8行目のconsole.log()が以下を出力します。
permission.state..prompt
これは、5行目のpermissions.query()で取得したPermissionStatusのstateで、 'granted'(許可)、'denied'(拒否)、'prompt'(プロンプト)のいずれか
とのことです。これがpromptだから確認のポップアップが出るのだと思います。
今回の目的は、ブログに画像を貼ることです。なので、画像を取得したら、それをサーバにアップロードしなければいけません。
このブログでは、もともとファイルを複数選んで画像をアップロードできます。クリップボードからコピペする場合、画像は1枚ですが、サーバ側の同じインタフェースをつかってアップロードしたいところです。
現在、画像アップロード時のfetch()には以下のRequestオブジェクトを渡していて、
let req = new Request(url, { body: formData, method: "POST", mode: "no-cors" });
ここでbodyに設定しているformDataは、以下のように作っています。
// <input type="file"~が変更されたときに呼ばれるコールバック function imageFileInputOnChange(event) { const files = event.target.files; const formData = new FormData(); for (let i = 0; i < files.length; i++) { formData.append("file" + i, files[i]); } …
multipart/form-dataのFormDataに、file+{連番}というnameで※3、event.target.filesで取得したファイルを順にくっつけています。
これと同等のFormDataを作ったら、こうなりました。
const file = new File([blob], "dummy.png"); formData.append("file0", file);
Fileのコンストラクタの第1引数に渡している「[blob]」ですが、これを囲む配列の[]がないと以下のエラーが出ます。
Uncaught (in promise) TypeError: File constructor: Argument 1 can't be converted to a sequence.
仕様を見るとコンストラクタの第1引数は「反復可能オブジェクト」とのことなので配列にしなければいけないのかもしれませんが、なぜこうしなければいけないのか、私はいまいちわかっていません。Stackoverflowのこのあたりの質問と回答を見て、こうすれば動くのか、と思った次第です――だから型のはっきりしない言語って嫌いなんだ。
公開日: 2024/11/03
不具合等ありましたら、掲示板にご連絡願います。
ひとつ前 | ひとつ上のページへ戻る | トップページへ戻る