Angular.js, CKEditor, and REST including file uploads

Posted: 2014-04-14 12:22:00

I wanted to share the steps I followed to get this WYSIWYG editor working with Angular and to allow uploads. By the end of it you will see how to

  1. Plug CKeditor into your Angular app
  2. Setup the config options in your Angular Controller and your view
  3. Setup your REST endpoints to take the needed requests
    • Show image gallery
    • Upload image
    • Show Files to link to
    • Upload file to link to

This can work for really any backend, Laravel, Drupal, Node etc. It is mostly to share some of the undocumented settings I needed. Though ckeditor does have a good api page here ckdocs

Also what I hope for you to see how much you can really do with this. Your endpoints can easily be showing files from Dropbox, Evernote etc and offering those up in the interface for CKEditor.

Plug in the CKEditor module

I downloaded this module ng-ckeditor

for me I store it in

/app/lib/ng-ckeditor

I also placed into the folder the ckeditor download.

That download will have a plugin folder in there I downloaded 2 plugins we will use.

Imagebrowser to get an easier api to JSON and MediaEmbed which may show up in my code examples and is totally optional.

Your Angular Controller

Depending on your framework you need to inject the module and then apply some settings.

Injecting the module

Of course you need to include the js files and css file in your "index.html" file.

  1. ng-ckeditor/ng-ckeditor.css
  2. ng-ckeditor/libs/ckeditor/ckeditor.js
  3. ng-ckeditor/ng-ckeditor.js

item #2 being the actual ckeditor library.

Finally inject it into your app.js file 'ngCkeditor' so Angular makes it available to your controller.

The Controller

It is here I finally get to configure the editor

    $scope.editorOptions = {
        language: 'en',
        'skin': 'moono',
        'extraPlugins': "imagebrowser,mediaembed",
        imageBrowser_listUrl: '/api/v1/ckeditor/gallery',
        filebrowserBrowseUrl: '/api/v1/ckeditor/files',
        filebrowserImageUploadUrl: '/api/v1/ckeditor/images',
        filebrowserUploadUrl: '/api/v1/ckeditor/files',
        toolbarLocation: 'bottom',
        toolbar: 'full',
        toolbar_full: [
            { name: 'basicstyles',
                items: [ 'Bold', 'Italic', 'Strike', 'Underline' ] },
            { name: 'paragraph', items: [ 'BulletedList', 'NumberedList', 'Blockquote' ] },
            { name: 'editing', items: ['JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock' ] },
            { name: 'links', items: [ 'Link', 'Unlink', 'Anchor' ] },
            { name: 'tools', items: [ 'SpellChecker', 'Maximize' ] },
            { name: 'clipboard', items: [ 'Undo', 'Redo' ] },
            { name: 'styles', items: [ 'Format', 'FontSize', 'TextColor', 'PasteText', 'PasteFromWord', 'RemoveFormat' ] },
            { name: 'insert', items: [ 'Image', 'Table', 'SpecialChar', 'MediaEmbed' ] },'/',
        ]
    };

Can also be seen as a gist here

So of course you can do no settings but this is where the fun is. I am telling CKEditor where the endpoints are that it needs to talk to to GET and POST data.

filebrowserUploadUrl: '/api/v1/ckeditor/files',

Is telling it to POST the uploaded file to that url.

filebrowserBrowseUrl: '/api/v1/ckeditor/files',

Is telling it to GET the files available from that url.

The View

The html for this to work is simple.

All I need to do is to tell the text area field to use this 'directive'

<textarea cols="30" rows="40"
          ng-model="page.body"
          name="pageBody"
          ckeditor="editorOptions" rows="100"  required="required">
</textarea>

Here I am using the ckeditor directive to take over this textarea and pass the 'editorOptions' info here as well.

That is it!

Now your ui in Angular has the editor.

The REST endpoints

This was pretty hard to find in the documentations. Googling helped for sure but I figured it would good to put it here as well.

GET

For getting/displaying the gallery of images I used that Imagebrowser plugin so when the GET request was made to the endpoint '/api/v1/ckeditor/gallery' it just had to respond with a certain format.

My Images Controller method looks like this


$rel = '/assets/img/wysiwyg'; $dir = public_path() . '/assets/img/wysiwyg'; $iterator = $this->finder->in($dir)->name('*.png')->name('*.jpg'); $files = []; $count = 0; foreach($iterator as $file) { $files[$count]['thumb'] = $rel . '/' . $file->getFilename(); $files[$count]['image'] = $rel . '/' . $file->getFilename(); $files[$count]['title'] = $file->getFilename(); $count ++; } return Response::json($files);

gist is here

Since I pulled in the Imagebrowser plugin I did not have to output html.

The file viewer work though (eg the non image gallery) one I had to output html but maybe I could have found a JSON method. Here is what my endpoint returns.

    $funcNum = $_GET['CKEditorFuncNum'];
    $message = "File chosen";
    $script = "
    <script type='text/javascript' src='/assets/js/jquery.1.10.2.min.js'></script>
    <script type='text/javascript'>
        var sendLink = function(event, url) {
            event.preventDefault();
            window.opener.CKEDITOR.tools.callFunction(\"$funcNum\", url, \"$message\");
            window.close();
        };
    </script>";

    $rel = '/assets/files/wysiwyg';
    $dir = public_path() . '/assets/files/wysiwyg/';
    $iterator = $this->finder->in($dir)->name('*.pdf')->name('*.doc');
    $files = [];
    $count = 0;
    foreach($iterator as $file) {
      $f = $rel . '/' . $file->getFileName();
      $name = $file->getFileName();
      $files[$count]['name'] = "<a href='" . $f . "' onclick='sendLink(event, \"$f\")'>{$name}</a>";
      $files[$count]['choose'] = 
        "<a href='" . $f . "' onclick='sendLink(event, \"$f\")'><i class='glyphicon glyphicon-new-window'></i></a>";
      $count++;
    }

    return View::make('files.index', compact('files', 'script'));

The View::make is just a template file that goes through the array and makes a bootstrap formatted table. Nothing fancy. But the $script, though it could have been better, was the only way I could figure to pass back the chosen file to the editor.

Keep in mind this is the html/output on the popup window you get when you click "Upload File" In CKEditor.

That takes care of GET so the user will see buttons in CKEditor and browse images and files to insert as we all know.

POST / Uploading

These 2 endpoints are the same. They are POST endpoints that process the data like this

    $rel = '/assets/files/wysiwyg';
    $dir = public_path() . $rel;
    $_FILES['upload']['type'] = strtolower($_FILES['upload']['type']);
    if ($_FILES['upload']['type'] == 'application/pdf'
      || $_FILES['upload']['type'] == 'image/jpg'
      || $_FILES['upload']['type'] == 'image/gif'
      || $_FILES['upload']['type'] == 'image/jpeg'
      || $_FILES['upload']['type'] == 'application/doc')
    {
      $tmp = $_FILES['upload']['tmp_name'];
      $dest = $dir . '/' . $_FILES['upload']['name'];
      $this->filesystem->copy($tmp, $dest, $override = TRUE);

      $file = '/assets/files/wysiwyg/'.$_FILES['upload']['name'];
    }
      $funcNum = $_GET['CKEditorFuncNum'];
      $message = "File uploaded";
      $script = "<script type='text/javascript'>window.parent.CKEDITOR.tools.callFunction(\"$funcNum\", \"$file\", \"$message\");</script>";
    return $script;

gist is here

Basically I take the input of $_FILES and make sure it is what I expected and save it to the folder. The tricky part was to switch the user back to the interface, closing the parent window so they now can finish linking their file.

Final notes

This is a first draft of this working deployed system. So it may have changes to it. I will try to comment here as I learn more.

When getting info from CKEDITOR I would do a var_dump at the endpoint to see what it was sending me for data. You can always see this output in your Chrome Console under Networking.

Some links


Tags:

angular.js