かな変換入力アニメーション


テレビドラマのお芝居撮影でメールを入力しているようなユースケースを考えた際のプログラムについて考えてみたいと思います。

正確にはこのスレッドにあるplaceCaretAtEnd関数に加筆して、平仮名をonkeydownで順に入力しつつ、入力文字列の末尾から走査し、予め用意しておいたキーバリューを仮名と漢字(表記)にしたJSON配列がマッチしたら上述の関数内で文字選択することであたかも変換しているかのように見せる技法に利用したというお話です。

// https://stackoverflow.com/questions/4233265/contenteditable-set-caret-at-the-end-of-the-text-cross-browser/4238971#4238971
const placeCaretAtEnd = (el, startOffset) => {
  el.focus()
  if (typeof window.getSelection !== undefined && typeof document.createRange !== undefined) {
    const range = document.createRange()
    if(startOffset === null) {
      range.selectNodeContents(el)
      range.collapse(false)
    } else {
      range.setStart(el.firstChild, startOffset)
      range.setEnd(el.firstChild, el.textContent.length)
    }
    const sel = window.getSelection()
    sel.removeAllRanges()
    sel.addRange(range)
  } else if (typeof document.body.createTextRange !== undefined) {
    const textRange = document.body.createTextRange()
    textRange.moveToElementText(el)
    textRange.collapse(false)
    textRange.select()
  }
}

引数1番目のelは対象要素、startOffsetは選択する時の最初の文字番目数(nullは選択無し)となります。前者がcontenteditabletrueDIV、あるいはINPUTなどになるかと思いますが、後者の場合実際にはかな変換関数と条件を利用しています。

const convertKana = () => {
  let startOffset = null
  Object.keys(data[msgId]).map((x) => {
    const string = inputTextEs[msgId].textContent
    if (string.lastIndexOf(x) > -1 && data[msgId][x]) {
      inputTextEs[msgId].textContent = string.replace(x, data[msgId][x])
      startOffset = string.lastIndexOf(x)
    }
  })
  placeCaretAtEnd(inputTextEs[msgId], startOffset)
}

textContentinnerHTMLにする必要がある場合がありますが、値が空の場合は変換せずそのまま入力(放置)することから分かるように、順番は逆になりますが最後に肝心のデータはこの様に作っておきました。

const data = [
{'わたしじしん': '私自身、', 'ななしさんとの': '名無しさんとの', 'たいだんで': '対談で', 'あらためて': '改めて', 'かがくの': '科学の', 'このさき': 'この先', 'について': '',},
]
let msgs = []
data.map((x) => {
  msgs.push(Object.keys(x).join(''))
})
let msgId = 0
const inputTextEs = document.querySelectorAll('.inputText')

キータイプということで、この様に仕込みます。キーコード8は削除キーなので条件下にしました。

document.onkeydown = (e) => {
  if(e.keyCode !== 8) {
    const letter = queries[msgId].shift()
    if(letter !== undefined) {
      inputTextEs[msgId].textContent = inputTextEs[msgId].textContent + letter
      convertKana()
    } else {
      placeCaretAtEnd(inputTextEs[msgId], null)
    }
    return false
  }
}

突然queries変数が出てきたのですが、上記でshift関数を使いたいがために次の処理を挟んでいます。

let queries = []
msgs.map((x, i) => {
  queries[i] = x.split('')
})

静的でも構わないのでHTMLを用意しておきます。INPUTTEXTAREAといったフォーム要素でも問題ありません(がスタイル設定が面倒なのでいつもやりません)。

<p contenteditable="true" class="inputText">

キータイプ毎にdataのオブジェクトキーを繋げた平仮名を入力しつつ選択し変換する処理を続けます。サンプルと全コードは気が向いたら加えます。

技術的には余談に近いですが、実際の撮影でキーボードが無線だった場合は接続しないで、マウスクリック3秒後に自動繰り返し処理によるアニメーションとなることがあります。またスマホの入力挙動にも使えますのでハイブリッド側アプリ用途に便利です。