[Vue.js] iOSでIME入力中に他のテキストボックスにフォーカスを当てると入力していた値がコピーされてしまう問題の対処

長いタイトルになってしまっていますが、内容は以下の通りです。

## iOSのテキストボックスで発生する問題

例えば以下のように、テキストボックスの入力後、入力値をカタカナに変換する処理を書いたとして、

```vue
<template>
  <form>
    <div>
      <input
        type="text"
        v-model="value1"
      />
    </div>
    <div>{{ value1 }}</div>
    <div>
      <input
        type="text"
        v-model="value2"
      />
    </div>
    <div>{{ value2 }}</div>
  </form>
</template>

<script lang="ts">
  import {Vue, Component, Watch} from "vue-property-decorator";

  @Component
  export default class InputForiOSTest extends Vue {
    public value1: string = '';
    public value2: string = '';
    
    public toKatakana(value: string) {
      return value.replace(/[ぁ-ゔ]/g, (s) => {
        return String.fromCharCode(s.charCodeAt(0) + 0x60);
      });
    }

    @Watch("value1")
    public onValue1Change() {
      this.value1 = this.toKatakana(this.value1);
    }
    
    @Watch("value2")
    public onValue2Change() {
      this.value2 = this.toKatakana(this.value2);
    }
  }
</script>
```

これを実際にiOSのSafariで開いてみて、

1. 最初のテキストボックスを選択し、日本語を入力。
2. そのまま確定せずに、次のテキストボックスをタップする。
3. そうすると、最初のテキストボックスで値の変換がされるが、何故かその値が次のテキストボックスにも入力される。

という現象が発生します。値の変換を行わなければこの現象は発生しません。。。だれかこの現象の原因がわかる方いらっしゃたら教えて下さい。。。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136391/ff2b4fd9-278a-208f-cbba-f710d1f5fd51.gif" width="30%"/>

## 解決策

とりあえず、この現象が起こる流れは把握できたので、以下のようにカスタムテキストボックスを実装してみました。

```vue
<template>
  <input v-model="_value" />
</template>

<script lang="ts">
  import {Vue, Component, Prop, Emit} from "vue-property-decorator";

  @Component
  export default class InputForiOS extends Vue {
    
    /**
     * v-modelでプロパティの値を書き換えられるようにする設定
     */
    
    @Prop() public value!: any;
    
    @Emit() private input(value: any) { return; }
    
    private get _value(): any { return this.value; }
    private set _value(value: any) { this.input(value); }

    /**
     * 以下、iOSで未確定の入力値が他のテキストボックスにコピーされてしまう問題の対処
     */
    
    private isComposing: boolean = false; // IME入力中かどうか
    
    public mounted() {
      // イベント登録
      this.$el.addEventListener("compositionstart", this.onCompositionStart);
      this.$el.addEventListener("compositionend", this.onCompositionEnd);
      this.$el.addEventListener("blur", this.onBlur);
    }

    /**
     * IME入力開始イベント
     */
    private onCompositionStart() {
      this.isComposing = true; // IME入力中
    }

    /**
     * IME入力終了イベント
     */
    private onCompositionEnd() {
      this.isComposing = false; // IME入力確定済
    }

    /**
     * テキストボックスのフォーカスが外れたときのイベント
     * @param e イベント
     */
    private onBlur(e: Event) {
      const mouseEvent = e as MouseEvent;
      const relatedTarget = mouseEvent.relatedTarget || document.activeElement; // 次にフォーカスした要素を取得
      
      // 次のフォーカスがinput要素で、かつIME入力中ではない場合
      if (relatedTarget && relatedTarget instanceof HTMLInputElement && this.isComposing) {
        
        relatedTarget.addEventListener("beforeinput", function handler(inputEvent: Event) {
          // 一度だけ入力をキャンセル
          inputEvent.preventDefault();
          relatedTarget.removeEventListener("beforeinput", handler);
          
          // 次にフォーカスを当てたinput要素を選択する（setTimeoutで全ての処理が終了してから実行）
          setTimeout(() => relatedTarget.select(), 0);
        });
      }
    }
    
    public destroyed() {
      // イベント削除
      this.$el.removeEventListener("compositionstart", this.onCompositionStart);
      this.$el.removeEventListener("compositionend", this.onCompositionEnd);
      this.$el.removeEventListener("blur", this.onBlur);
    }
  }
</script>
```

上記、処理の流れを大まかに説明すると、

1. テキストボックスで日本語を入力すると、IME入力中となる。
2. IME入力中にテキストボックスからフォーカスが外れて、かつ次のフォーカスする要素がinputだったら、次のinput要素への入力を一度だけキャンセルする。（これにより入力値がコピーされてしまう問題を対処）
3. その後、次のinput要素にフォーカスを当てる。

という感じです。これにより、今回の現象がとりあえず解消されました。

```vue
<template>
  <form>
    <div>
      <InputForiOS
        type="text"
        v-model="value1"
      />
    </div>
    <div>{{ value1 }}</div>
    <div>
      <InputForiOS
        type="text"
        v-model="value2"
      />
    </div>
    <div>{{ value2 }}</div>
  </form>
</template>

<script lang="ts">
  import {Vue, Component, Watch} from "vue-property-decorator";
  import InputForiOS from '@/components/molecules/InputForiOS.vue';
  
  @Component({
    components: {InputForiOS}
  })
  export default class InputForiOSTest extends Vue {
    public value1: string = '';
    public value2: string = '';
    
    public toKatakana(value: string) {
      return value.replace(/[ぁ-ゔ]/g, (s) => {
        return String.fromCharCode(s.charCodeAt(0) + 0x60);
      });
    }

    @Watch("value1")
    public onValue1Change() {
      this.value1 = this.toKatakana(this.value1);
    }
    
    @Watch("value2")
    public onValue2Change() {
      this.value2 = this.toKatakana(this.value2);
    }
  }
</script>
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136391/14e9af53-011e-529a-1c4f-9f46439187ba.gif" width="30%" />

でももっといい方法がありそうだなー。。。以上！w