Block based WYSIWYG editor using Angular

If you haven't already seen Sir Trevor JS, then you must take a look now! It allows the user to insert blocks of different types, with a really nice interface.

There are many benefits to this approach in a responsive site, as the content types are separated using json and can be outputted in rows. I have used Sir Trevor JS along with Wagtail Streamfield, which does a similar thing.

But what if we want the same experience on an Angular project? well unfortunately the only version available is a wrapper around the main version, without proper Angular integration. I decided to write my own which could support the integration better. Here is how I did it.


First off we need to include angular.js in the page and add the ng-app to the html tag. Now we can create a controller for the editor with some default data:

    angular.module('app', [])
    .controller('editor', ['$scope', '$compile', '$templateCache', function ($scope, $compile, $templateCache) {
        'use strict';
        $scope.blocks = [
            { type: 'text', content: '<h1>Angular Sir Trevor</h1><p>AngularJS block-based editor inspired by Sir Trevor JS</p>' },
            { type: 'image', content: 'http://massimages.mobi/wp-content/uploads/forest-wallpaper-widescreen-hd-photograph-6.jpg' }
        ];
    }])

Now we have some data we need assign the controller and add the loop which will output the blocks in the html page:

<div class="editor" ng-controller="editor">
    <div block class="block {{ block.type }}" ng-repeat="block in blocks" ng-class="{ 'editing': block.edit }" ng-click="block.edit=true"></div>
</div>

Note that we also included a directive attribute called 'block'. This is going to be a dynamic directive which changes the block type based on the data. Lets add the directive and a function to the controller to handle it:

    // directive
    .directive('block', ['$window', '$compile', '$templateCache', function ($window, $compile, $templateCache) {
        'use strict';
        return {
            restrict: 'A',
            link: function (scope, element, attr, ctrl) {
                scope.updateType(element, scope);
            }
        };
    }])

    // add to the editor controller
    $scope.updateType = function (element, scope) {
        if (scope.block.type === 'text') {
            element.removeAttr('tabindex', '0');
            element.html($templateCache.get(scope.block.type));
        } else {
            element.attr('tabindex', '0');
            element.html($templateCache.get(scope.block.type) + $templateCache.get('overlay'));
        }
        $compile(element.contents())(scope);
    };

In this updateType function we are finding the template based on the block type. But we are missing templates for the block types. Lets add those to the html:

<script type="text/ng-template" id="overlay">
    <div class="overlay">
        <div class="controls">
            <button ng-click="moveUp(blocks, block)">▲</button>
            <button ng-click="remove(blocks, block)">X</button>
            <button ng-click="moveDown(blocks, block)">▼</button>
        </div>
        <p>UPLOAD FILE</p>
        <div class="area"><input upload type="file" /></div>
        <p>OR MODIFY EXISTING URL</p>
        <input paste type="text" ng-model="block.content" />
    </div>
</script>

<script type="text/ng-template" id="text">
    <div text contenteditable="true">{{ block.content }}</div>
    <div class="controls">
        <button ng-click="moveUp(blocks, block)">▲</button>
        <button ng-click="remove(blocks, block)">X</button>
        <button ng-click="moveDown(blocks, block)">▼</button>
    </div>
</script>

<script type="text/ng-template" id="image">
    <img image ng-src="{{ block.content }}" alt="" />
</script>

<script type="text/ng-template" id="video">
    <div class="wide"><iframe video content="{{ block.content }}" frameborder="0" allowfullscreen></iframe></div>
</script>

Now the blocks are showing, we need to add the functionality to the controls to move blocks around, add and remove blocks. add these functions to your controller:

    $scope.add = function (array, element) {
        array.push(element);
    };

    $scope.remove = function (array, element) {
        var index = array.indexOf(element);
        if (index === -1) {
            return false;
        }
        array.splice(index, 1);
    };

    $scope.moveUp = function (array, element) {
        var index = array.indexOf(element);
        if (index === -1) {
            return false;
        }
        if (array[index - 1]) {
            array.splice(index - 1, 2, array[index], array[index - 1]);
        } else {
            return 0;
        }
    };

    $scope.moveDown = function (array, element) {
        var index = array.indexOf(element);
        if (index === -1) {
            return false;
        }
        if (array[index + 1]) {
            array.splice(index, 2, array[index + 1], array[index]);
        } else {
            return 0;
        }
    };

You can view the working version here:
http://kmturley.github.io/angular-sir-trevor/

And get the source code at:
https://github.com/kmturley/angular-sir-trevor