長いタイトルになってしまっていますが、内容は以下の通りです。
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で開いてみて、
- 最初のテキストボックスを選択し、日本語を入力。
- そのまま確定せずに、次のテキストボックスをタップする。
- そうすると、最初のテキストボックスで値の変換がされるが、何故かその値が次のテキストボックスにも入力される。
という現象が発生します。値の変換を行わなければこの現象は発生しません。。。だれかこの現象の原因がわかる方いらっしゃたら教えて下さい。。。
解決策
とりあえず、この現象が起こる流れは把握できたので、以下のようにカスタムテキストボックスを実装してみました。
<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>
上記、処理の流れを大まかに説明すると、
- テキストボックスで日本語を入力すると、IME入力中となる。
- IME入力中にテキストボックスからフォーカスが外れて、かつ次のフォーカスする要素がinputだったら、次のinput要素への入力を一度だけキャンセルする。(これにより入力値がコピーされてしまう問題を対処)
- その後、次の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