範囲選択とコピーを1クリックで完了する機能をVanilla JSで実装する
こんにちは。
開発チームのワイルド担当、まんだいです。
とある社内システムで、既定の文字列をボタン押下でクリップボードへコピーする機能を、ZeroClipboard を使って実装していました。
Zero Clipboardは、JavaScriptとSWFファイルを設置して読み込むだけでコピーボタンの作成が可能になるライブラリで、導入が非常に簡単だったと記憶しています。
でも、最近どうもうまく動いていないような気がして、ふと考えて気付いたんです。
Google ChromeってデフォルトでFlashを無効にしてた事に!
Flashの再生ができないわけじゃない
Google Chromeは、デフォルトで無効にしているだけで、何もFlashを完全無視するようになったという訳ではありません。
ちゃんとしかるべき設定をすれば、動作するようになっています。
一応、簡単に説明すると、URL欄の右側にある、縦に並んだ三点リーダーから、設定を選択し、左上のハンバーガーから「詳細設定」 → 「プライバシーとセキュリティ」へ進みます。
「コンテンツの設定」というカテゴリの中に、Flashの動作設定は押し込まれていますので、適宜変更してください。
こんなところにあるなんて、悪意さえ感じる仕打ちとも思えますが、Googleに言わせれば、セキュリティホール多すぎだろという尤もなご意見。
こんな感じでサポートはされているけれど、今後盛り上がるとは思えない状況なので、さっさと脱Flashしたいと思います。
仕様が共通化されてきたし、APIが整備されてきた最近のJavaScript
昨今のJavaScriptの流行を受けてなのか、ブラウザ側のJavaScriptの実装はすごい速さで進んでいます。
jQueryがサイトの描画速度の低下を引き起こしているなど、最近悪者扱いされることも多いjQueryですが、実際のところ、ないならないなりにどうにかできてしまうことが多くなりました。
CSSフレームワークが採用しているから仕方なく・・・というパターンで導入することの方が多いようにも思います。
今回のお題になっているクリップボードに文字列をコピーするという処理も、かつてはJavaScript単独ではできなかった処理の代表例のようなものも、今ではできてしまえるようになったんです!
そこで「Vanilla JS」の出番ですよ
ということで、前置きが長くなってしまいましたが、ここからはピュアなVanilla JSを使った、クリップボードへのコピーを試してみたいと思います。
処理の順序は
- コピーしたい文字列を選択状態にする
- コピーする
これだけです。
コピーしたい文字列を選択状態にする
コピーより文字列を選択する方が手続きが多くかかるので、この部分が分かってしまえばこっちのものです。
文字列の選択に関連するオブジェクトは、RangeオブジェクトとSelectionオブジェクトの2つになります。
Rangeオブジェクトは、ページのどの範囲が選択されているかを示す情報を保持するためのオブジェクトです。
また、Selectionオブジェクトは、Rangeオブジェクトを管理する立ち位置のオブジェクトで、1つないし複数のRangeオブジェクトを有します。
文字列を選択するのは、Rangeオブジェクトの方なので、SelectionオブジェクトからRangeオブジェクトを取り出すか、新しいRangeオブジェクトを作成するかのいずれかでRangeオブジェクトを取得します。
// Rangeオブジェクトを新規作成する var range = document.createRange(); // RangeオブジェクトをSelectionオブジェクトから取得する var selection = window.getSelection(); var range = selection.getRangeAt(0);
どちらの方法で取得しても、変数rangeの中身は同じRangeオブジェクトですので、お好きな方法でRangeオブジェクトを用意して構いません。
今回は、Selectionオブジェクトから取り出したRangeオブジェクトを使った実装を試してみます。
コピーしたい文字列が入ったタグが以下のようになっていると仮定すると
<div id="hoge">コピーしたい文字列</div>
getElementById()メソッドでDOMを取得します。
var hoge = document.getElementById('hoge');
続いて、選択範囲を指定します。
指定の仕方は2通りあって、DOM要素の中身を全てコピる場合は、RangeオブジェクトのselectNodeContents()メソッドを利用します。
range.selectNodeContents(hoge);
また、DOM要素の一部だけコピる場合は、RangeオブジェクトのsetStart()メソッドで起点を指定し、setEnd()メソッドで終点を指定します。
ここで気をつけなければいけないのが、一部だけの場合、getElementById()メソッドで取得したDOMをそのままsetStart()、setEnd()メソッドの渡すと、大抵エラーが発生しますので、DOMから文字列のNodeだけを抜き出して渡してやる必要があります。
range.setStart(hoge, 7); range.setEnd(hoge, 15); // エラーが発生 // Uncaught IndexSizeError: Failed to execute 'setStart' on 'Range': There is no child at offset 7.
このエラーにある IndexSize とは、hogeオブジェクト内のDOM配列のインデックスを見て、7つ目のNodeはないよと言っています。私たちは、文字列を渡して7つ目の文字を起点にしたつもりでしたが、認識にずれがありますね。
ですので、setStart()、setEnd()メソッドの第1引数には、hogeオブジェクトの中に含まれている、テキストノードを渡してやる必要があります。テキストノードは、firstChildプロパティから拾うのが王道のようです。
var hoge = document.getElementById('hoge'); var hogeTextNode = hoge.firstChild; range.setStart(hogeTextNode, 7); range.setEnd(hogeTextNode, 15);
選択範囲の指定ができたら、後は選択状態にするだけですが、ここにも落とし穴が用意されています。
選択状態にするには、選択範囲の指定ができているRangeオブジェクトをSelectionオブジェクトにaddRange()メソッドを使って追加するだけです。
selection.addRange(range); // エラーが発生 // Discontiguous selection is not supported.
discontiguous(連続していない)なSelectionはサポートしていませんということですが、連続していないとはどういうことでしょうか。
今回のサンプルでは、RangeオブジェクトはSelectionオブジェクトから取り出しましたが、これが上のエラーの意味するところになります。
既にSelectionオブジェクトはRangeオブジェクトを1つ持っているので、addRange()メソッドで追加すると、2つのRangeオブジェクトを持つことになり、その状態がエラーが示す、連続していないSelectionの状態です。
選択範囲として指定したいのはこちらで後から追加したRangeオブジェクトの方なので、手っ取り早いのは、Selectionオブジェクトが最初から持っているRangeオブジェクトを削除してしまった後に、Rangeオブジェクトを追加し、1つしか持っていない状態を作る方法です。
selection.removeAllRanges(); // メソッド名の通り、Rangeオブジェクトを全て削除する selection.addRange(range);
これで選択状態にすることができました。
まとめると、画面上の文字列を選択状態にするには、以下のコードになります。
// SelectionオブジェクトからRangeオブジェクトを取得する var selection = window.getSelection(); var range = selection.getRangeAt(0); var textNode = document.getElementById('hoge').firstChild; // DOM内の文字列全てを選択する場合 range.selectNodeContents(textNode); // selectNodeContents()メソッドの場合は、敢えてテキストノードを抜き出す必要もない // DOM内の一部だけ選択する場合 range.setStart(textNode, 7); range.setEnd(textNode, 15); // Selectionオブジェクトを通じて選択状態にする selection.removeAllRanges(); selection.addRange(range);
クリップボードに文字列をコピーする
選択中の文字列に対して、「Ctrl + C」をするには、以下のexecCommand()メソッドを利用します。
document.execCommand('copy');
これでクリップボードへ選択中の文字列がコピーできたので、後はお好きなところで「Ctrl + V」するだけです。
まとめ
文字列を選択する辺りでかなり紙幅を取ってしまいましたが、いかがでしょうか。
Stack Overflowなどにも、ちょこちょこ載ってはいるんですが、理解に必要な情報がまとまっていなかったのでどうしてエラーになるのか分からず、JavaScriptでのクリップボードの利用に苦手意識を持ってしまうような気がしましたので、エラーになるロジックさえ理解できれば、もう怖くないですよね。
以上です。