Vue.js×TypeScriptで「ワンタップでテキストをコピーする」ボタンを作ったらハマりポイントがたくさんありました。
生jsやjQueryでの解決策はたくさん見つかりましたが、Vue.js×TSは見つからなかったのでメモです。
やりたいこと
フォームに文字入力した時に、飾り文字を追加した文章を出力して、ワンタップでコピーできるようにする。
完成したコード
<template lang="pug">
.CopyText
button(@click.prevent="copyTexts")
span.copy-message クリップボードにコピー
.formatted-text
span.recipe
span#copy-text {{formattedTitle}}<br>
</template>
<script lang="ts">
import { Recipe } from "../../components/molecules/RecipeTitle.vue";
import Vue, { PropType } from "vue";
export default Vue.extend({
props: {
recipe: {
type: Object as PropType<Recipe>,
default: {}
}
},
computed: {
//inputで入力した内容ではなく、ここでフォーマットしたテキストがコピー対象
formattedTitle(): string {
return this.recipe.title ? `【${this.recipe.title}】` : "";
}
},
methods: {
//iOSの判定
isIOS() {
const agent = window.navigator.userAgent;
return agent.indexOf("iPhone") != -1 || agent.indexOf("iPad") != -1;
},
//コピー
copyTexts(): void {
if (this.isIOS()) {
//iOSの場合
const doc: HTMLInputElement = document.getElementById(
"copy-text"
) as HTMLInputElement;
const selected = window.getSelection();
const range = document.createRange();
range.selectNodeContents(doc);
selected!.removeAllRanges();
selected!.addRange(range);
document.execCommand("copy");
} else {
//それ以外
const formattedText = `${this.formattedTitle}`;
navigator.clipboard.writeText(formattedText);
}
}
}
});
</script>
参考:Javascriptによるコピー機能(クロスブラウザ対応)
ハマった部分の解説
iOSでのコピー
jsでコピーをしようと思ったらnavigator.clipboardを使用するのが一般的かと思います。
ユーザーエージェントなどの情報を扱うNavigatorインターフェイスにclipboardプロパティを追加して、writeText()メソッドを呼び出すことで、テキストがコピーできます。
navigator.clipboard.writeText(text);
しかし、このnavigator.clipboardはiOSの10以降、textareaなど一部のタグからしかコピーできないなど仕様が変わっています。
今回はinputに入力した文字ではなく、フォーマットをかけたテキストをコピーするため、まさにこの条件に引っかかり、iOSのsafariとchromeで動作しませんでした。
参考: Copy to clipboard using Javascript in iOS
そのため、iOSとそれ以外でコピーの処理を変える必要があります。
iOSかどうかの判定
isIOS() {
const agent = window.navigator.userAgent;
return agent.indexOf("iPhone") != -1 || agent.indexOf("iPad") != -1 || agent.indexOf("iPod") != -1;
}
navigator.userAgentを使います。
今回はブラウザではなくiOSかどうかだけ判定するので、上記のようにしてみました。
iOS用のコピー
iOSのコピーは、コピーしたい文章を選択→コピーの実行という流れで行います。
const doc = document.getElementById("copy-text");
const selected = window.getSelection();
const range = document.createRange();
range.selectNodeContents(doc);
selected.removeAllRanges();
selected.addRange(range);
document.execCommand("copy");
ユーザーはワンタップするだけですが、内部の動作はマウスなどで文章選択→コピーをするのと同じです。
2行目のwindow.getSelectionはselectionオブジェクトを取得するものです。
selectionオブジェクトは、ユーザーが選択した範囲のDOMに関する情報を持つことができます。
3行目のcreateRangeはdocument中のテキストやノードに関する情報を持つrangeオブジェクトを作成します。
rangeオブジェクトを作成しただけでは何も情報を持っていないため、4行目のrange.selectNodeContents(doc)で、最初に取得した要素を渡します。
5行目は2行目に取得したselectionオブジェクトが現在持っているrangeに関する情報をあらかじめ削除する処理です。文章がすでに選択されてselectionオブジェクトに情報が設定されている場合、この後の処理が無視されるので先に削除してしまいます。
これにより、6行目でselectionオブジェクトに作成したrangeオブジェクトを追加することができます。
最後のdocument.execCommand()はhtmlのdocumentオブジェクトを操作するコマンドを実行します。copyは選択範囲をクリップボードにコピーするコマンドです。
これでiOSでもテキストコピーができるようになりました!
参考: memo: テキスト全選択の JavaScript コードが動かなくなったので修正した
TSで"Argument of type 'HTMLElement | null' is not assignable to parameter of type 'Node'"
上記のコードはType Scriptを使うと以下の部分でエラーを吐きます。
//Argument of type 'HTMLElement | null' is not assignable to parameter of type 'Node'
const doc = document.getElementById("copy-text");
document.getElementById() はHTMLElement型もしくはnullを返しますが、nullを返す可能性があるとTSがエラーを出すようです。
そのため、返り値がHTMLElement型であることを明示的に示します。
const doc: HTMLInputElement = document.getElementById("copy-text") as HTMLInputElement;
TSでObject is possibly 'null'エラー
rangeの削除、追加部分でも型エラーが出ます。
selected.removeAllRanges(); //Object is possibly 'null'
selected.addRange(range); //Object is possibly 'null'
これはselectedの部分がnullの可能性があることで出るエラーです。
そこで、!をつけて、selectedがnullでもundefinedでもないことを推論させます。ただ、この方法はESlintで"Forbidden non-null assertion"の警告が出ます。