ReactでVimライク(H←L→)なInputを書いた

# こんにちは
今年入社しました @tom-ock です。
昨日terraformの記事を上げてくれた @Sadalsuud さんと同じチームで開発しております。
担当ほどはっきりしたモノは今まだないですが、最近Reactが面白いです。

自己紹介はこれくらいにして早速本題に。

# TL;DR
Reactでviっぽいinputコンポーネントを作りました(type="text"です)。
NORMALモードでHで左に、Lで右にカーソルが移動します。

<img width="530" alt="ScreenShot 8.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132987/5bdb76e6-300e-d3a1-7e64-d73383149016.jpeg">

* [Github(ok8omk/Vinput)](https://github.com/ok8omk/Vinput)
* [npm(@ok8omk/vinput)](https://www.npmjs.com/package/@ok8omk/vinput)

# 実演
**注意**

* VimiumなどはOffにしてください
* 仕様上英語入力しか受け付けません
* 入力にはFocusする必要があるためクリックしてから入力してください
* 幅も有限なので、入力しすぎたらノーマルモードでdを押してください
* スマホからは操作できません！ごめんなさい
* Safariでの不具合が現在確認されています。ご了承ください。

[:octocat: こちら(Github Pages)](https://ok8omk.github.io/Vinput/)

## 実装概説
テキストエディタっぽいカーソルをどうしても表現したかったのですが、
inputを使用して実装する方法が思い浮かばなかったので今回は全てdivで実装し、
inputはhiddenでstateで渡すことにしました。

stateはreducerで管理しています。

|state.|内容|
|:--:|:--:|
|value|入力文字列|
|cursorIndex|カーソルポジション|
|mode|モード|

今回はノーマルモードとインサートモードの切り替え、文字列の入力、ノーマルモードでのいくつか移動を実装しました。
これらの3つの属性のstate + 入力されるキーから、次のstateが決定されるためそれをreducerに書きました。

[:file_folder: useVi.ts](https://github.com/ok8omk/Vinput/blob/master/src/useVi.ts)

```ts
const normalReducer: React.Reducer<State, Action> = (state, action) => {
  switch (action.key) {
    case "d": {
      return { ...state, value: "", cursorIndex: getIndex.start(state) };
    }
    case "i": {
      return { ...state, mode: "INSERT" };
    }
    case "a": {
      return { ...state, mode: "INSERT", cursorIndex: getIndex.append(state) };
```


至る所で移動する次のstateを計算するとごちゃごちゃしたので、カーソル移動とテキスト編集をそれぞれ別のファイルに分けて関数化したものをreducerでは使用しています。

[:file_folder: getIndex.ts](https://github.com/ok8omk/Vinput/blob/master/src/getIndex.ts)

```ts
const right: IndexGetter = ({ value, mode, cursorIndex }) => {
  if (value.length === 0 || cursorIndex !== value.length - 1) {
    return cursorIndex + 1;
  } else {
    return cursorIndex;
  }
};

const left: IndexGetter = ({ value, mode, cursorIndex }) => {
  if (cursorIndex > 0) {
    return cursorIndex - 1;
  } else {
    return cursorIndex;
  }
};
```

[:file_folder: getValue.ts](https://github.com/ok8omk/Vinput/blob/master/src/getValue.ts)

```ts
const backspace: ValueGetter = ({ value, mode, cursorIndex }) => {
  return (
    value.slice(0, cursorIndex - 1) + value.slice(cursorIndex, value.length)
  );
};

const insert: ValueGetter = ({ value, mode, cursorIndex, key }) => {
  return (
    value.slice(0, cursorIndex) + key + value.slice(cursorIndex, value.length)
  );
};
```

## ここに苦労しました

### ボックス形のCaret
Caret(入力時にカーソルの位置を示す部分)を操作するCSSは現在[caret-color](https://developer.mozilla.org/ja/docs/Web/CSS/caret-color)が提供されています。
これは色の操作のみ操作できるプロパティで、viライクなボックス形のCaretは表現出来ません。
そこでハック感ありますが、inputタグは使わずにdivタグで入力フォームを表現する事にしました。

キー入力をReducerのActionとしたかったのでonKeydownを使用した結果、英数字の入力のみ行える(日本語入力できない)仕様になったのはかなり心残りです。。

![3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132987/aec6b37a-b474-60e4-cb94-b7430c281335.gif)

### Reducer

カーソル位置は前述のcursorIndexで保持し、NORMALモードとINSERTモードのカーソル移動を実現しました。

入力値、カーソル位置、モードなどの状態は
> これらの3つの属性のstate + 入力されるキーから、次のstateが決定されるためそれをreducerに書きました。

次のstateを前のstateから副作用のない関数として返せるような構造(getValue.ts, getIndex.ts)に分けています。
Vimならではのモーションとオペレーターのイメージで分けたからか、意外と見やすいReducerになった気がします。

* [:file_folder: getValue.ts](https://github.com/ok8omk/Vinput/blob/master/src/getValue.ts)
* [:file_folder: getIndex.ts](https://github.com/ok8omk/Vinput/blob/master/src/getIndex.ts)

## 機能
そこまで多くないのであえて書きません。
READMEに書いてあるので、気になる方はこちらへ！
[:octocat: Github(https://github.com/ok8omk/Vinput)](https://github.com/ok8omk/Vinput)

文字の入力をkeydownにしてしまったので日本語入力を受け付けられなかったり、
正規表現パワー足りず実装できてない部分あったり、
本当は複数行のテキストエリアを実装したかったり…
…色々やり残してるのでPR絶賛お待ちしています :bow:


# 終わりに

ブラウザとかSlackとかで入力してる時に、全部Vimみたいにカーソル移動できたらいいのに！と思う瞬間が度々あるのが発端でした。(たまにありませんか？)

jsでCLIを表現するのは不毛な実装感がありますが、Reactとreducerのいい練習になりました。

npmで公開したので興味持って頂けた方は是非使って下さい！
[npm(@ok8omk/vinput)](https://www.npmjs.com/package/@ok8omk/vinput)

課題だらけと思うのでissue大歓迎です、気長に直していくのでぜひお願いします :bow:

ReactをVimで書く記事はあっても、
ReactでVimを書く記事は見たことないなあと思って書いたので、
パイオニアであることを祈っております… :aries::gemini::scorpius:

明日は @litalico-takamasa-mizukami さんのAMP Storyです。
お楽しみに！