はじめに
この記事を全面的に参照していきます。
https://qiita.com/zaru/items/baacf5eb490094aaa150
任意の要素にcontenteditable
をつけると入力フィールド化出来ます。
当たり前と言えば当たり前ですが、contenteditable
要素に入力した際、改行するとその位置で改行され、EdgeやChromeでは要素の高さが自動的に伸びていきます。
従来のinput
やtextarea
だと、入力内容によって高さを自動的に変えるのには少し工夫がいるのですが、この方法なら属性一発で解決できます。(EdgeとChrome以外では試していませんが…)
ただ、Vue
を用いてcontenteditable
の要素に値をバインドして、後からバインドする要素を変更するような実装に少し手間取ったので、記録として残しておきます。
TL;DR;
最終的にこんなコードのコンポーネントになりました。
<template lang="pug">
.text-input(
:contenteditable = "true"
v-text = "text"
@input = "update"
@focus = "focus"
@blur = "blur"
)
</template>
<script lang="ts">
import { Component, Vue, Prop, Emit, Watch } from 'vue-property-decorator';
@Component
export default class TextInput extends Vue {
@Prop()
public value!: string;
private text: string = '';
private focusIn: boolean = false;
@Emit()
private input(str: string): void {/* */}
@Watch('value')
private valueChanged(n: string): void {
// value が変更され次第 text へコピー
// ただし入力中は無視
if (this.focusIn) return;
this.text = this.value;
}
private update(e: Event): void {
const target: HTMLElement = e.target as HTMLElement;
this.input(target.innerText);
}
private focus(): void {
this.focusIn = true;
}
private blur(): void {
this.focusIn = false;
}
}
</script>
何が問題だったか
参考記事の内容だと、mounted
時にcontent
をinnerContent
に値をコピーしているので、mounted
後にcontent
が変更された時にその値が反映されません。
かといって、content
をvalue
に、sync
をinput
に変更してv-model
を使えるようにすると、参考記事冒頭にあるようにキャレットが文字列先頭に飛んでいく現象が起こります。
最初は、以下のようにvalue
の値をwatch
して、変更次第コピーしていました。
@Watch('value')
private valueChanged(n: string): void {
// value が変更され次第 text へコピー
this.text = this.value;
}
これだと、結局常に1つの値(value
)を見ているだけになってしまい、入力の度にレンダリングが走ってキャレットが先頭に戻ります。
解決策
問題は入力中に入力内容が変更される事なので、focus
とblur
を使って、入力エリアがフォーカスされている時は値を変更しないようにしました。
@Watch('value')
private valueChanged(n: string): void {
// value が変更され次第 text へコピー
// ただし入力中は無視
if (this.focusIn) return;
this.text = this.value;
}
おわりに
色々なケースでテストしたわけではないので、この方法ではうまくいかないケースがあるかもしれませんが、とりあえず一つの解決策として参考になれば幸いです。
おまけ(改行を復元する)
入力中に改行するとバインドされた値にも\n
が入ります。
その値をデータベース等に保存して、その後読み込んで改めてバインドすると、改行が無視されます。
どうやらcontenteditable
に入力した値は随時innerHTML
に置き換わっているようで、改行はその中で実現されているようです。
<!-- 「sample<リターン>text」と入力すると… -->
<div contenteditable="true">
sample
<div>text</div>
</div>
<!-- …となる -->
という事で、改行も復元する場合のコードがこちら。もはやデータのバインドはしておらず、innerHTML
を直で書き換えています。
<template lang="pug">
.text-input(
ref = "input"
:contenteditable = "true"
@input = "update"
@focus = "focus"
@blur = "blur"
)
</template>
<script lang="ts">
import { Component, Vue, Prop, Emit, Watch } from 'vue-property-decorator';
@Component
export default class TextInput extends Vue {
@Prop()
public value!: string;
private focusIn: boolean = false;
@Emit()
private input(str: string): void {/* */}
@Watch('value')
private valueChanged(n: string): void {
if (!this.focusIn) {
const i: HTMLElement = this.$refs.input as HTMLElement;
const elem: string[] = this.value.split('\n');
let t: string = '';
for (const e of elem) {
t += '<div>' + e + '</div>';
}
i.innerHTML = t;
}
}
private update(e: Event): void {
const target: HTMLElement = e.target as HTMLElement;
this.input(target.innerText);
}
private focus(): void {
this.focusIn = true;
}
private blur(): void {
this.focusIn = false;
}
}
</script>