7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-09-22

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

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

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

<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. そうすると、最初のテキストボックスで値の変換がされるが、何故かその値が次のテキストボックスにも入力される。

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

解決策

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

<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要素にフォーカスを当てる。

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

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

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

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?