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の実装が雑なので、スコープに反映する契機や方法を再考した方がいいかも
- 記事が雑なので書き直す