Textarea高さ可変＆改行制限のやり方(Vue.js) - IME変換対応

## はじめに
あるプロジェクトで、inputタグを使って実装していた箇所に対して「改行できるようにして欲しい」と言われました。「そうだなぁ、複数行であれば、textareaタグを使うしかないな。」となるのですが、ここで問題が。改行できるようにするのは、タグを変えるだけだけど、可変で制限かけるって難しくね？


結構苦戦したので、その備忘録

## やり方
やり方は２段階になります。
まず、 「テキストエリアの高さを可変＆制限をかける」そして「改行を制限する」の二つです。では、一つずつ見てみましょう。

### 1.テキストエリアの高さを可変＆制限をかける


デフォルトのテキストエリアであれば、 高さは可変ではありません。
`rows`を指定したり、スタイルから高さを指定することで固定になります。
このようにEnterを押すことで改行していきますがスクロールが出現します。



![textarea.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252687/0ea1e10b-6827-b536-4ca5-e4da259daa12.gif)




まずはこの高さを仮に３行で制限してみましょう。

```vuejs
<template>
 <textarea
   v-model="text"
   class="textarea"
   :style="{height:`${getTextareaHeight}px`}"
 />
</template>

<script>
export default {
 data() {
  return {
    text: ''
  }
 },
 computed: {
   getTextareaHeight () { // テキストエリアの高さ制限
    let rowCount = (`${this.text}\n`).match(/\n/g).length
    if (rowCount > 3) rowCount = 3 //　ここで３行で制限をかける
    const LINEHEIGHT = 20 * 1.5 // １行あたりの高さ(font-size:20pxとline-height: 1.4を考慮)
    const PADDING_TOP_AND_BOTTOM = 5 * 2 // padding上下の5pxを考慮
    const BORDER_TOP_AND_BOTTOM = 1 * 2 // border上下の1pxを考慮
    return LINEHEIGHT * rowCount + PADDING_TOP_AND_BOTTOM + BORDER_TOP_AND_BOTTOM
   },
 }
}
</script>

<style scoped>
.textarea {
  padding: 5px;
  border: 1px solid #000;
  font-size: 20px;
  line-height: 1.5;
}
</style>
```
高さに関しては、JSで動的に指定するのでインラインスタイルになります。
テキストエリアにかかっているスタイルを考慮しなければならないので、少し面倒な記述になりますね。

![textarea2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252687/efbb9f04-eef9-7960-ff00-23996356974e.gif)

うまく、３行以上の高さにならないように可変でありながらも制限はかけることができました。


### 2.改行を制限する
次にここが、ネックとなりました。テキストエリアの改行は、Enterを押すことで行われます。
なので、enterキーを押した時に処理を発火させ、制限をかければ良いと思っていました。
`@keydown`とか`@keyup`イベントを使って、そのキーコードなり、キー名で判別すれば良いと考えます。
しかし、それはうまくいきません。

というのも、日本語入力変換時の確定Enterと改行のEnterの判別ができないからです。

`@keydown="handleKeydown"`として、`console.warn(e.key)`を仕込んでみました。
![textarea3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252687/1075aeed-ec6b-915c-ae2f-3f3bd183cc24.gif)

このように、変換確定でも`Enter`が取れてしまいます。

これの解決方法が、`composition`イベントを使うという方法です。
https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent
`compositionstart`: IMEテキスト編集開始時に発火
`compositionupdate`: IME編集中のテキストが変更された時に発火
`compostionend`: IMEテキスト編集終了時に発火

`compositionstart`と`compositionend`を使ってみます。

```vuejs
<template>
 <textarea
   v-model="text"
   class="textarea"
   @compositionstart="composing=true"
   @compositionend="composing=false"
   @keydown="handleKeydown"
   :style="{height:`${getTextareaHeight}px`}"
 />
</template>

<script>
export default {
 data() {
  return {
    text: '',
    composing: false
  }
 },
 computed: {
   //(略)
 },
 methods: {
   handleKeydown() {
     if (this.composing) return // IME対応(日本語変換中のEnterを止める)
     console.warn(e.key)
   }
 }
}
</script>

<style scoped>
.textarea {
  ...(略)
}
</style>
```


![textarea4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252687/6b03a1bb-97c1-d2db-287a-1b39bb45969f.gif)

無事に日本語変換確定時のEnterは弾いています。

これで準備は整いました。

先ほどの`console.warn(e.key)`を置き換えましょう。

```diff

methods: {
   handleKeydown() {
     if (this.composing) return // IME対応(日本語変換中のEnterを止める)
-    console.warn(e.key)
+    const rowCount = (`${this.text}\n`).match(/\n/g).length
+    if (rowCount >= 3 && e.key === 'Enter') {
     // テキストエリアからフォーカスを外すことで、Enterの連打を防ぐ
+    document.activeElement.blur()
   }
 }
```

これで、すでにテキストエリア内が３行に達している場合にEnterを押しても改行できなくなりました。

![textarea5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252687/14b37fce-8a1e-d475-2f4d-1a0effafc955.gif)

警告メッセージなど出してあげるとわかりやすいですね！ :clap:

## 最後に

これで終わりなのですが、こちらコピペなどで貼り付けると３行を越してしまいます。

やり方は色々あると思うのですが、`watch`で`text`を監視して制限処理を入れるなど方法があります。

```js
computed: {
  //(略)
},
watch: {
 text (val) {
    if (val.length >= 30) val = val.slice(0, 30)　// 30文字制限
    // pasteに対応した改行制限
    let rows = val.split(/\n/)
    this.text = rows.slice(0, 3).join('\n') // textを更新して改行を入れない
  }
},
methods: {
  //(略)
}
```

他にも、ご自分でtextareaに`wrap="off"`や`maxLength`などをかけて、制限を追加してみると面白いですね！


## 参考記事
* [How does Vue handle real enter event?](https://forum.vuejs.org/t/how-does-vue-handle-real-enter-event/2169/4)
* [JavaScript とクロスブラウザでの IME event handling (2017年)](https://tanishiking24.hatenablog.com/entry/ime-event-handling-javascript)
* [[Vue.js] IME入力時にEnterキーイベントを発火しない様にする](https://qiita.com/angeart/items/4ff19185fc8d7ef5fb79)