LoginSignup
4
0

More than 5 years have passed since last update.

CodeMirrorでEmoji AutoCompleteする

Last updated at Posted at 2017-09-07

CodeMirrorでEmoji AutoCompleteをするに書いたけどQiitaにも転載。


":"に続けて打ち込むと自動で補完してくれるようになります。

demo

ちゃんとブラウザ上で動いている様子です。

EmojiリストはGitHub APIから取得しています。

LocksというLocalStorageを使ったスライドエディタを作っているときにできた副産物です。

必要なパッケージ

  • CodeMirror
    • show-hint addonのコードを編集するのでソースコードが必要
  • Axios

パッケージの読み込み方法は各自におまかせします(scriptタグでもビルドツールでも構いません)。

やり方

// sample data
let emojiList = ['apple:', 'abc:', 'axz:', 'bee:', 'beam:', 'bleach:']

let emojiComplete = function(cm) {
  CodeMirror.showHint(cm, function() {
    let cur = cm.getCursor(), token = cm.getTokenAt(cur)
    let start = token.start, end = cur.ch, word = token.string.slice(0, end - start)
    let ch = cur.ch, line = cur.line
    let currentWord = token.string
    while (ch-- > -1) {
      let t = cm.getTokenAt({ch, line}).string
      if (t === ':') {
        let filteredList = emojiList.filter((item) => {
          return item.indexOf(currentWord) == 0 ? true : false
        })
        if (filteredList.length >= 1) {
          return {
            list: filteredList,
            from: CodeMirror.Pos(line, ch),
            to: CodeMirror.Pos(line, end)
          }
        }
      }
      currentWord = t + currentWord
    }
  }, { completeSingle: false })
}

cm.on('change', emojiComplete)

ひとまずemoji画像のないシンプルな例です。cmはCodeMirrorインスタンスです。

CodeMirrorのchangeイベントにshowHintを仕込んで実現しています。

showHintの中身が肝です。

最初の4行はshowHintを書く際の決まり文句のようなものでカーソル位置や対象の単語情報を変数に記録しています。

while文内では、単語を一文字ずつ遡っていって":"が見つかったら補完候補を絞り込んで、補完候補が存在する場合は表示するようにしています。

showHintの戻り値は、補完のリストと補完する位置を指定します。位置は今の位置から単語終了位置までになります。

これで自動的に":"のあとに文字を入れると補完候補が表示されます。

showHint関数に指定している、completeSingle: falseは必須です。細かい理由はわかりませんがこれがないと無限ループするようです。

emojiListが末尾に":"が付いて不格好なのは、補完完了時に末尾の":"まで補完されるようにしているからですがこれは回避できます、これは以下の内容で説明します。

Emojiのリスト取得

GitHub APIにEmoji一覧を取得するAPIがあるので利用します。

Emojis | GitHub Developer Guide

先程の例のemojiListをapiで取得したものに置き換えれば完全なリストになります。

let res = await axios.get('https://api.github.com/emojis')
if (res.status !== 200) {
  console.log('Error...')
  return
}

let emojiList = []
for (let key in res.data) {
  emojiList.push(`${key}:`)
}

Emoji画像の表示

show-hint addonの補完候補には文字列しか表示できません。

実際にCodeMirrorのaddon/hint/show-hint.jsを見てみると、文字列リスト or {text: str, displayText: str}のリストを指定する仕様になっています。

画像を表示させるようにshow-hint.jsを書き換えます。

      if (cur.render) cur.render(elt, data, cur);
-     else elt.appendChild(document.createTextNode(cur.displayText || getText(cur)));
+     else elt.innerHTML = cur.displayText
      elt.hintId = i;

219行目あたりです。

displayTextで指定する補完候補表示をinnerHTMLで表示できるようにしました。

{text: str, displayText: str}のリストでの補完候補の指定は、textは実際に補完する際に使われる文字列でdisplayTextが補完候補を表示するときに使われる文字列になるので、emojiListを以下のようにします。

let res = await axios.get('https://api.github.com/emojis')
if (res.status !== 200) {
  console.log('Error...')
  return
}

let emojiList = []
for (let key in res.data) {
  emojiList.push({text: `${key}:`, displayText: `<img width="15" height="15" src="${res.data[key]}" alt="icon" async></img> ${key}`})
}

displayTextをGitHub APIで取得した画像を小さくしたもの + Emoji名の形にします。

これで冒頭のGifキャプチャーのように表示されるようになります。

かなりの量の画像を取得するのでimgタグにはasyncオプションをつけています。

完全なコード

書き換えたshow-hint.jsを使用する必要があるので注意。
render function を利用すれば show-hint.js の書き換えは不要。
参考: https://github.com/codemirror/CodeMirror/blob/5.33.0/addon/hint/show-hint.js#L212

let cm = CodeMirror(/* Dom Element */, {
  // Options
})

//
// Emoji Complete
//
let res = await axios.get('https://api.github.com/emojis')
if (res.status !== 200) {
  console.log('Error...')
  return
}

let emojiList = []
for (let key in res.data) {
  emojiList.push({
    text: `${key}:`,
    render: (element) => {
      element.innerHTML = `<img width="15" height="15" src="${res.data[key]}" alt="icon" async></img> ${key}`})
    }
  })
}

let emojiComplete = function(cm) {
  CodeMirror.showHint(cm, function() {
    let cur = cm.getCursor(), token = cm.getTokenAt(cur)
    let start = token.start, end = cur.ch, word = token.string.slice(0, end - start)
    let ch = cur.ch, line = cur.line
    let currentWord = token.string
    while (ch-- > -1) {
      let t = cm.getTokenAt({ch, line}).string
      if (t === ':') {
        let filteredList = emojiList.filter((item) => {
          return item.text.indexOf(currentWord) == 0 ? true : false
        })
        if (filteredList.length >= 1) {
          return {
            list: filteredList,
            from: CodeMirror.Pos(line, ch),
            to: CodeMirror.Pos(line, end)
          }
        }
      }
      currentWord = t + currentWord
    }
  }, { completeSingle: false })
}

cm.on('change', emojiComplete)

参考記事

4
0
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
0