tamuraです。
ある問題をちょっとだけ解決したので載せます。


Ghostの問題点

現在、Ghostでブログをやっていますが、

http://devlog.forkwell.com/2014/09/01/setup-ghost-on-digitalocean/

にもあるように、IME変換中の文字が分かりにくいという問題があります。 0.6で直す!とのことですが、今現在は0.5.8でして本格的に対応されるのはもう少し先になりそうです。


とりあえず直せないか?

エディタ部分は CodeMirror を使っているので、 CodeMirror の実装がどうなっているかを見に行きます。 その前に Issue を確認します。ありました。 (この他にもIME関連で「下線が出ない」系のIssueが結構あがっていました。とりあえず落ち着け状態)

No underline while composing double-byte characters with IME. #1513

中を見ていくとプルリクエストもあるようです。

Adding markers while composing text #1382

pull request

ただ、まだいろいろなブラウザでの挙動がテストし切れていないということで、取り込まれていないようです。

とりあえず直す

プルリクエストがあるので、手元のGhostに取り込んでみます。

Ghostの再インストール

前回はビルド済みのzipファイルを落としてきましたが、GitHubからコードを落としてきます。

git clone https://github.com/TryGhost/Ghost.git
cd Ghost
npm install

パッチの適用

行数が少ないので手でコピペして対応しました。 まずはJS側。

vi bower_components/codemirror/lib/codemirror.js

function fastPoll(cm)の中に書いていきます。

  function fastPoll(cm) {
    var missed = false;
    cm.display.pollingFast = true;
    function p() {
      var changed = readInput(cm);
      if (!changed && !missed) {missed = true; cm.display.poll.set(60, p);}
      else {cm.display.pollingFast = false; slowPoll(cm);}
    }
    if (cm.state.composing) p();
    cm.display.poll.set(20, p);
  }

続いてfunction readInput(cm)の中です。これは最後のreturnの直前に入れればいいのでわかりやすいです。

    cm.curOp.updateInput = updateInput;
    if (text.length > 1000 || text.indexOf(\"\
\") > -1) input.value = cm.display.prevInput = \"\";
    else cm.display.prevInput = text;
    if (withOp) endOperation(cm);
    cm.state.pasteIncoming = false;

    if (cm.state.composing) {
      if (!cm.state.lastComposeMark) {
        cm.state.lastComposeMark = cm.doc.markText(cm.state.composeStart, {line: cm.state.composeStart.line, ch: cm.state.composeStart.ch + cm.state.lastCompositionEventData.length}, {className: 'CodeMirror-composing'});
      } else {
        cm.state.lastComposeMark.clear();
        cm.state.lastComposeMark = cm.doc.markText(cm.state.composeStart, {line: cm.state.composeStart.line, ch: cm.state.composeStart.ch + cm.state.lastCompositionEventData.length}, {className: 'CodeMirror-composing'});
      }
    }
    return true;
  }

続いてfunction registerEventHandlers(cm)の中です。イベントハンドラを登録しているところに追加していきます。

    on(d.input, \"input\", bind(fastPoll, cm));
    on(d.input, \"keydown\", operation(cm, onKeyDown));
    on(d.input, \"keypress\", operation(cm, onKeyPress));
    on(d.input, \"focus\", bind(onFocus, cm));
    on(d.input, \"blur\", bind(onBlur, cm));
    on(d.input, \"compositionstart\", function(e) { onCompositionEvent(cm, e); });
    on(d.input, \"compositionupdate\", function(e) { onCompositionEvent(cm, e); });
    on(d.input, \"compositionend\", function(e) { onCompositionEvent(cm, e); });

最後にfunction onBlur(cm)の後ろあたりに新しい関数を追加します。

  function onBlur(cm) {
    if (cm.state.focused) {
      signal(cm, \"blur\", cm);
      cm.state.focused = false;
      cm.display.wrapper.className = cm.display.wrapper.className.replace(\" CodeMirror-focused\", \"\");
    }
    clearInterval(cm.display.blinker);
    setTimeout(function() {if (!cm.state.focused) cm.doc.sel.shift = false;}, 150);
  }
  function onCompositionEvent(cm, e) {
    switch(e.type) {
    case \"compositionstart\":
      cm.state.composing = true;
      cm.state.composeStart = cm.getCursor();
      break;
    case \"compositionupdate\":
      cm.state.lastCompositionEventData = e.data;
      fastPoll(cm);
      break;
    case \"compositionend\":
      cm.state.composing = false;
      if (cm.state.lastComposeMark) {
        cm.state.lastComposeMark.clear();
        cm.state.lastComposeMark = null;
      }
      cm.state.composeStart = null;
      break;
    }
  }

これでJS側の修正は完了です。

続いてCSS側。 CSSはSassを修正します。
適当なところに .CodeMirror-composing を入れておきます。

vi core/client/assets/sass/vendor/codemirror.scss
.CodeMirror-composing {
  border-bottom: 3px solid black;
}

動作確認

grunt init
npm start

EDIT画面でIME入力中に下線が出ることを確認します。
ちゃんと未決定の部分に下線が出ます。 

技術

DOM Level3 Events

Composition Eventを使ってIME入力の開始、終了を検知しています。
http://www.w3.org/TR/DOM-Level-3-Events/#events-compositionevents

各イベントが発火した時点でこのような処理を行っています。

  • compositionstart検出(IME入力の開始)
    • composingステータスをtrueに
    • 入力開始位置を現在のカーソル位置から取得


  • compositionupdate検出(IME入力中・変換中)
    • 入力中の文字を取得


  • compositionend検出(IME入力の完了)
    • composingステータスをfalseに
    • マーカーをオフに

あとはcomposingステータスの状態に応じて下線を引くCSSを有効にしていたりします。 なので 「わたしのなまえはなかのです」 を変換させて途中途中で確定させようとすると分からなくなります。