GeekFactory

int128.hatenablog.com

Evernote API + AngularJS + CKEditor

Evernote APIにアクセスするWebアプリで、リッチテキストの編集をCKEditorで実現する方法を説明します。 要約するとAngularJSとCKEditorを組み合わせて使ってみたという内容です。

やってること

  • Evernoteのノートを表示したり編集したりするWebアプリを作ってる
  • クライアントサイドは AngularJS + Bootstrap を使ってる
  • サーバサイドは App Engine + Scala + Unfiltered + Evernote API Client

やりたいこと

  • ノートのコンテンツをブラウザ上で編集したい(いい感じで)
  • ノートのコンテンツは HTML に近い ENML (Evernote Markup Language) で記述されているので、人間が直接書くのは厳しい
  • ENMLにはHTML以外の独自タグも含まれている
  • 整形式でないコンテンツはEvernote APIでリジェクトされる

どうやって解決する

  • サーバから取得したENMLをブラウザのDOMツリーに直接注入して表示する
  • ブラウザのDOMツリーをシリアライズしてサーバに保存する
  • ユーザはブラウザのDOMツリーを編集すればいい
  • HTMLエディタの CKEditor を使うとDOMツリーをインライン編集できる

Chromeで確認した限りでは、HTMLタグではない要素もDOMツリーに保持してくれるようです。シリアライズしたら<en-note>などもそのまま出てきました。

サーバサイドはすでに実装済みとします。ここでは /note/xxxxx をGETすると下記のようなJSONが返却されるものとします。

{
"guid":"xxxxx",
"title":"The Note",
"content":"<en-note><div>Hello World</div></en-note>"
}

ENMLを表示する

サーバからノートを取得して表示します。

controllers.controller 'NoteEditController', ($scope, $routeParams, Note) ->
  Note.get($routeParams.guid).success (note) ->
    $scope.note = note
services.factory 'Note', ($http) ->
  get: (guid) ->
    $http.get "/note/#{guid}"

文字列をDOMツリーに注入するには ng-bind-html を使います。今回は自分自身が所有するノートを表示するので、サニタイズは行わずにそのまま表示します。

<div ng-bind-html="note.content | asHtml"></div>
app.filter 'asHtml', ($sce) ->
  (value) -> $sce.trustAsHtml value

ENMLをインライン編集する

前章で表示したコンテンツを今度は編集できるようにしてみましょう。

まずCKEditorの読み込みを追加します。

<script src="//cdn.ckeditor.com/4.4.3/standard/ckeditor.js"></script>

CKEditorを適用するにはElementに直接アクセスする必要があるため、新しいDirectiveを定義してその中で行います。

app.directive 'ngCkeditor', ($window) ->
  link: (scope, element, attrs) ->
    attrs.$set 'contenteditable', 'true'
    $window.CKEDITOR.disableAutoInline = true
    $window.CKEDITOR.inline element[0]

先ほどのdiv要素にng-ckeditor属性を追加するとCKEditorが有効になります。

<div ng-ckeditor 
     ng-bind-html="note.content | asHtml"></div>

インライン編集すげー。

ENMLを保存する

前章で編集したコンテンツを今度は保存できるようにしてみましょう。

CKEditorのフォーカスが外れた契機でDOMツリーをシリアライズしてスコープに反映するようにします。 シリアライズするとHTMLの名前空間が付加されますが、Evernote APIにリジェクトされる原因になるので除外しておきます。

app.directive 'ngXmlUpdate', ($window) ->
  serializer = new XMLSerializer()
  link: (scope, element, attrs) ->
    element.on 'blur', ->
      scope.$xml = serializer
        .serializeToString element[0].firstChild
        .replace ///xmlns=".+?"///, ''
      scope.$apply ->
        scope.$eval attrs.ngXmlUpdate
<div ng-ckeditor
     ng-bind-html="note.content | asHtml"
     ng-xml-update="note.content = $xml"></div>

<button ng-click="save(note)" type="button" class="btn btn-default">Save</button>

あとは保存ボタンが押された場合の動作を実装すればOKです。

controllers.controller 'NoteEditController', ($scope, $routeParams, Note) ->
  Note.get($routeParams.guid).success (note) ->
    $scope.note = note
  $scope.save = (note) ->
    Note.save note.guid,
      title: note.title
      content: note.content
services.factory 'Note', ($http) ->
  get: (guid) ->
    $http.get "/note/#{guid}"
  save: (guid, data) ->
    $http.post "/note/#{guid}", data

今後の課題

  • ngXmlUpdateの実装が雑なので、スコープに反映する契機や方法を再考した方がいいかも
  • 記事が雑なので書き直す