4
3

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を使用してcontenteditableの値をバインドする (改良)

Last updated at Posted at 2019-06-14

はじめに

この記事を全面的に参照していきます。
https://qiita.com/zaru/items/baacf5eb490094aaa150

任意の要素にcontenteditableをつけると入力フィールド化出来ます。
当たり前と言えば当たり前ですが、contenteditable要素に入力した際、改行するとその位置で改行され、EdgeやChromeでは要素の高さが自動的に伸びていきます。
従来のinputtextareaだと、入力内容によって高さを自動的に変えるのには少し工夫がいるのですが、この方法なら属性一発で解決できます。(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時にcontentinnerContentに値をコピーしているので、mounted後にcontentが変更された時にその値が反映されません。
かといって、contentvalueに、syncinputに変更してv-modelを使えるようにすると、参考記事冒頭にあるようにキャレットが文字列先頭に飛んでいく現象が起こります。

最初は、以下のようにvalueの値をwatchして、変更次第コピーしていました。

    @Watch('value')
    private valueChanged(n: string): void {
        // value が変更され次第 text へコピー
        this.text = this.value;
    }

これだと、結局常に1つの値(value)を見ているだけになってしまい、入力の度にレンダリングが走ってキャレットが先頭に戻ります。

解決策

問題は入力中に入力内容が変更される事なので、focusblurを使って、入力エリアがフォーカスされている時は値を変更しないようにしました。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?