8
6

More than 3 years have passed since last update.

Vue.js×TypeScriptでのテキストコピー(iOS対応)

Last updated at Posted at 2020-03-29

Vue.js×TypeScriptで「ワンタップでテキストをコピーする」ボタンを作ったらハマりポイントがたくさんありました。
生jsやjQueryでの解決策はたくさん見つかりましたが、Vue.js×TSは見つからなかったのでメモです。

やりたいこと

フォームに文字入力した時に、飾り文字を追加した文章を出力して、ワンタップでコピーできるようにする。

recipe formatter qiita.gif

完成したコード


    <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;

参考: Typescript で TS2322 の対処方法

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"の警告が出ます。

参考: 非nullアサーション演算子(Non-Null Assertion Operator)

8
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
6