WYSIWYGなMarkdownエディターを目指してContentEditableおよびexecCommandと真っ向勝負してみる(part 2)

この記事は**part2**です。
[part1](https://qiita.com/TomOse/items/9d71ad99b880028b4d53)をまだご覧になっていない方はそちらを先にお読みいただくと話の流れがわかりやすいかと思います。

# 前回残された修正点

前回の実装で残された修正点は以下のようになっています。

1. 挿入された要素にキャレットを合わせることができない
2. `blockquote`は`Enter`を押しても抜け出すことができない

今回はこれらを修正していきます。

# ① 挿入した要素にキャレットを合わせられない問題について

前回の実装では`execCommand`でHTML要素を変更した後に、`innerText`を空にしていました。
この際、キャレットが置き換えた要素の直前の要素に(なぜか)移動してしまうのが原因でこのような不具合が起こっています。
そして、置き換えた要素にはキャレットを合わせることはできません。

![画面収録-2019-09-03-23.15.33.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315273/1968565f-ac28-de05-cb6d-4cd26f7d3aa1.gif)

この問題を解決するために、要素を置き換えた後に`innerText`を空にするのではなく、スペースを入れることで対処してみました。
すなわち、

``` javascript
// 前回のコード
window.getSelection().getRangeAt(0).endContainer.parentNode.innerText = ''

// 修正
window.getSelection().getRangeAt(0).endContainer.parentNode.innerText = '\xA0'
```

![画面収録-2019-09-03-23.26.06.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315273/f2232c86-db60-629d-7ff7-c98c6a2664fc.gif)


どうにかキャレットを合わせることができました。
ですが、これによって新たな問題が出てきました。

上のGif画像の最後にちょっと映ってますが、改行した後に再び`H1`が挿入されています。
この問題は前回の二番目の問題である、無限`blockquote`と同じようなものと捉え次の章で対処します。

また、ブラウザの標準機能として、リストを抜け出す際には空の行で`Enter`を押す必要があります。

``` リストを抜け出す例
1. aaa
2. ｜  ←キャレット位置

↓ Enter押下

1. aaa
|
```

ですが、先ほどの実装では行の末尾にスペースが入るため、行が空とはみなされず改行しても延々とリストのままです。
つまり、リストや順序付きリストを挿入する際にはスペースを挿入する必要はなさそうです。
前回の実装でマークダウン記法の場合分けをしましたが、リストや順序付きリストの時には前回同様`innerText `を空にするようにします。


# ② 無限`blockquote`について

例によって詳しい原因は不明ですが、前回の実装では`blockquote`の中で`Enter`を押しても抜け出すことができませんでした。
原因がわからないため、改行で新たに挿入された`blockquote`を`execCommand`で無理やり`div`に変換する実装をしました。
マークダウン記法を置き換えたのと逆のやり方です。

``` javascript
document.execCommand('formatblock', false, 'div')
```

なお、`Shift+Enter`は同じ要素内での改行なので、`Shift`なしの`Enter`のみを捕捉して上記のコマンドを実行する必要があります。
また、リストや順序付きリストはブラウザの標準機能で望ましい動きをするので、`div`に置き換える処理を適応しない必要があります。

`Shift`の捕捉はキーイベントの`.shiftKey`プロパティで入手できます。
リストの判定は、キャレットのある要素名を取得することでやりました。

上記をまとめると以下の処理になります。

``` javascript 
if (!event.shiftKey && event.keyCode === 13) {
    if (!focusingOnOrderElement()) {
       document.execCommand('formatblock', false, 'div')
    }
} 

const focusingOnOrderElement = () => {
    const element_name = window.getSelection().getRangeAt(0).endContainer.parentNode.nodeName
    return (element_name === 'LI' || element_name === 'UL' || element_name === 'OL')
}
```

# 完成形

今回の改良点をまとめると以下のようになります。

``` javascript

const element = document.getElementById('markdown')
    element.focus()

    element.addEventListener('keyup', (event) => {
      const currentLine = window.getSelection().getRangeAt(0).endContainer.data || ''

      if (!event.shiftKey && event.keyCode === 13) {
         if (!focusingOnOrderElement()) {
          document.execCommand('formatblock', false, 'div')
        }
      }else{
        if (currentLine.match(/^#{1}\xA0$/)) { // 見出し
          document.execCommand('formatblock', false, 'h1')
          clearCurrentLine()
        } else if (currentLine.match(/^#{2}\xA0$/)){
          document.execCommand('formatblock', false, 'h2')
          clearCurrentLine()
        } else if (currentLine.match(/^#{3}\xA0$/)) {
          document.execCommand('formatblock', false, 'h3')
          clearCurrentLine()
        } else if (currentLine.match(/^#{4}\xA0$/)) {
          document.execCommand('formatblock', false, 'h4')
          clearCurrentLine()
        } else if (currentLine.match(/^#{5}\xA0$/)) {
          document.execCommand('formatblock', false, 'h5')
          clearCurrentLine()
        } else if (currentLine.match(/^#{6}\xA0$/)) {
          document.execCommand('formatblock', false, 'h6')
          clearCurrentLine()
        } else if (currentLine.match(/^>\xA0$/)) { // 引用
          document.execCommand('formatblock', false, 'blockquote')
          clearCurrentLine()
        } else if (currentLine.match(/^\d+\.\xA0$/)) { // 順序付きリスト
          document.execCommand('insertOrderedList')
          clearCurrentLine('')
        } else if (currentLine.match(/^[\-+*]\xA0+$/)) { // リスト
          document.execCommand('insertUnorderedList')
          clearCurrentLine('')
        }
      }
    })

    const clearCurrentLine = (clearCharacter = '\xA0') => {
      window.getSelection().getRangeAt(0).endContainer.parentNode.innerText = clearCharacter
    }

    const focusingOnOrderElement = () => {
      const element_name = window.getSelection().getRangeAt(0).endContainer.parentNode.nodeName
      return (element_name === 'LI' || element_name === 'UL' || element_name === 'OL')
    }
```

デモは[こちら](https://tom-ose.github.io/qiita/mde/mde-part2.html)です。

# 次回に向けて

今回の実装でChrome上でもなんとか動くようになりました(SafariとChromeのみで試しています)
次回以降は**強調**や*イタリック*などの文頭ではないマークダウン記法の判定をできればと思います。

乱文でしたが、最後までお読みいただきありがとうございましたm(_ _)m
改善点などございましたら、ご教授願います。