Skip to content

Instantly share code, notes, and snippets.

@kevinchampion
Created June 20, 2014 01:51

Revisions

  1. kevinchampion created this gist Jun 20, 2014.
    251 changes: 251 additions & 0 deletions ng-ckeditor.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,251 @@
    (function(angular, factory) {
    if (typeof define === 'function' && define.amd) {
    define(['angular', 'ckeditor'], function(angular) {
    return factory(angular);
    });
    } else {
    return factory(angular);
    }
    }(angular || null, function(angular) {
    var app = angular.module('ngCkeditor', []);
    var $defer, loaded = false;

    app.run(['$q', '$timeout', function($q, $timeout) {
    $defer = $q.defer();

    if (angular.isUndefined(CKEDITOR)) {
    throw new Error('CKEDITOR not found');
    }
    CKEDITOR.disableAutoInline = true;
    function checkLoaded() {
    if (CKEDITOR.status == 'loaded') {
    loaded = true;
    $defer.resolve();
    } else {
    checkLoaded();
    }
    }
    CKEDITOR.on('loaded', checkLoaded);
    $timeout(checkLoaded, 100);
    }])

    app.directive('ckeditor', ['$timeout', '$q', function ($timeout, $q) {
    'use strict';

    return {
    restrict: 'AC',
    require: ['ngModel', '^?form'],
    scope: false,
    link: function (scope, element, attrs, ctrls) {

    var ngModel = ctrls[0];
    var form = ctrls[1] || null;
    var EMPTY_HTML = '<p></p>',
    isTextarea = element[0].tagName.toLowerCase() == 'textarea',
    data = [],
    isReady = false;

    if (!isTextarea) {

    element.attr('contenteditable', true);

    }

    var onLoad = function () {
    var options = {
    toolbar: 'full',
    toolbar_full: [
    { name: 'styles', items: [ 'FontSize', 'Bold', 'Italic', 'Underline', 'TextColor', '-', 'JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock', '-', 'NumberedList', 'BulletedList', '-', 'Link', 'Unlink', 'Image', 'HorizontalRule', '-', 'Sourcedialog' ] }
    ],
    disableNativeSpellChecker: false,
    width: '100%',
    resize_enabled: false,
    extraPlugins: 'sourcedialog',
    skin: 'diobox',
    sharedSpaces: { top: 'cke_container' },
    removeDialogTabs: 'link:advanced;link:target;image:advanced',
    // The location of an external file browser, that should be launched when "Browse Server" button is pressed.
    filebrowserBrowseUrl: "/ckeditor/attachment_files",
    // The location of an external file browser, that should be launched when "Browse Server" button is pressed in the Flash dialog.
    filebrowserFlashBrowseUrl: "/ckeditor/attachment_files",
    // The location of a script that handles file uploads in the Flash dialog.
    filebrowserFlashUploadUrl: "/ckeditor/attachment_files",
    // The location of an external file browser, that should be launched when "Browse Server" button is pressed in the Link tab of Image dialog.
    filebrowserImageBrowseLinkUrl: "/ckeditor/pictures",
    // The location of an external file browser, that should be launched when "Browse Server" button is pressed in the Image dialog.
    filebrowserImageBrowseUrl: "/ckeditor/pictures",
    // The location of a script that handles file uploads in the Image dialog.
    filebrowserImageUploadUrl: "/ckeditor/pictures",
    // The location of a script that handles file uploads.
    filebrowserUploadUrl: "/ckeditor/attachment_files",
    // Rails CSRF token
    filebrowserParams: function() {
    var csrf_token, csrf_param, meta,
    metas = document.getElementsByTagName('meta'),
    params = new Object();

    for ( var i = 0 ; i < metas.length ; i++ ){
    meta = metas[i];

    switch(meta.name) {
    case "csrf-token":
    csrf_token = meta.content;
    break;
    case "csrf-param":
    csrf_param = meta.content;
    break;
    default:
    continue;
    }
    }

    if (csrf_param !== undefined && csrf_token !== undefined) {
    params[csrf_param] = csrf_token;
    }

    return params;
    },
    addQueryString: function( url, params ) {
    var queryString = [];

    if ( !params ) {
    return url;
    } else {
    for ( var i in params )
    queryString.push( i + "=" + encodeURIComponent( params[ i ] ) );
    }

    return url + ( ( url.indexOf( "?" ) != -1 ) ? "&" : "?" ) + queryString.join( "&" );
    }
    };
    options = angular.extend(options, scope[attrs.ckeditor]);

    var instance = (isTextarea) ? CKEDITOR.replace(element[0], options) : CKEDITOR.inline(element[0], options),
    configLoaderDef = $q.defer();



    element.bind('$destroy', function () {

    instance.destroy(
    false //If the instance is replacing a DOM element, this parameter indicates whether or not to update the element with the instance contents.
    );

    });

    var setModelData = function(setPristine) {

    var data = instance.getData();
    if (data == '') {
    data = null;
    }
    $timeout(function () { // for key up event
    (setPristine !== true || data != ngModel.$viewValue) && ngModel.$setViewValue(data);
    (setPristine === true && form) && form.$setPristine();
    }, 0);

    }, onUpdateModelData = function(setPristine) {

    if (!data.length) { return; }
    var item = data.pop() || EMPTY_HTML;
    isReady = false;
    instance.setData(item, function () {
    setModelData(setPristine);
    isReady = true;
    });

    }, showContainer = function() {

    $('#cke_container').show();

    }, hideContainer = function() {

    $('#cke_container').hide();

    }

    //instance.on('pasteState', setModelData);
    instance.on('change', setModelData);
    instance.on('blur', function() {
    hideContainer();
    setModelData();
    });
    instance.on('focus', showContainer);
    //instance.on('key', setModelData); // for source view

    instance.on('instanceReady', function() {

    instance.setReadOnly(false);
    scope.$broadcast("ckeditor.ready");
    scope.$apply(function() {
    onUpdateModelData(true);
    });

    instance.document.on("keyup", setModelData);

    hideContainer();

    });

    instance.on('customConfigLoaded', function() {

    configLoaderDef.resolve();

    });

    // Integrate Rails CSRF token into file upload dialogs (link, image, attachment and flash)
    instance.on( 'dialogDefinition', function( ev ){
    // Take the dialog name and its definition from the event data.
    var dialogName = ev.data.name;
    var dialogDefinition = ev.data.definition;
    var content, upload;

    if (CKEDITOR.tools.indexOf(['link', 'image', 'attachment', 'flash'], dialogName) > -1) {
    content = (dialogDefinition.getContents('Upload') || dialogDefinition.getContents('upload'));
    upload = (content == null ? null : content.get('upload'));

    if (upload && upload.filebrowser && upload.filebrowser['params'] === undefined) {
    upload.filebrowser['params'] = config.filebrowserParams();
    upload.action = config.addQueryString(upload.action, upload.filebrowser['params']);
    }
    }
    });

    ngModel.$render = function() {

    data.push(ngModel.$viewValue);
    if (isReady) {
    onUpdateModelData();
    }

    };
    };

    if (CKEDITOR.status == 'loaded') {

    loaded = true;

    }

    if (loaded) {

    onLoad();

    } else {

    $defer.promise.then(onLoad);

    }

    // TODO: Real placeholder.
    // TODO: Compile diobox skin.
    // TODO: Deal with sanitization.
    // TODO: Deal with content actually saving to the model.
    // TODO: Better image plugin with upload ability.

    }
    };
    }]);

    return app;
    }));