knockoutJS binding handler for CKEditor 4 inline

I was checking out CKEditor 4’s inline editing functionality and thought it would be nice to create a knockoutjs binding handler. It’s awesome as it is a full WYSIWYG experience! Here’s the bindling handler snippet:

(function (ko, CKEDITOR) {
    CKEDITOR.disableAutoInline = true;
    ko.bindingHandlers.inlineCkeditor = {
        counter: 0,
        prefix: '__cked_',
        init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
            if (!element.id) {
                element.id = ko.bindingHandlers.inlineCkeditor.prefix + (++ko.bindingHandlers.inlineCkeditor.counter);
            }
            var options = allBindingsAccessor().ckeditorOptions || {};
            var ckUpdate = allBindingsAccessor().ckUpdate || function () { };

            // Override the normal CKEditor save plugin

            CKEDITOR.plugins.registered['save'] =
            {
                init: function (editor) {
                    editor.addCommand('save',
            {
                modes: { wysiwyg: 1, source: 1 },
                exec: function (editor) {
                    var ckValue = editor.getData();
                    if (editor.checkDirty()) {
                        var self = valueAccessor();
                         if (ko.isWriteableObservable(self) && (ko.utils.unwrapObservable(self) !==ckValue)) {
                            valueAccessor()(ckValue);
                        }

                        editor.resetDirty();
                    }
                    ckUpdate.call(ckValue);
                    ckValue = null;
                }
            }
            );
                    editor.ui.addButton('Save', { label: 'Save', command: 'save', toolbar: 'document' });
                }
            };

            options.on = {
                instanceReady: function (e) {

                },
                blur: function (e) {
                     var ckValue = e.editor.getData();
                    if (e.editor.checkDirty()) {

                        var self = valueAccessor();
                        if (ko.isWriteableObservable(self) && (ko.utils.unwrapObservable(self) !==ckValue)) {
                            self(ckValue);
                        }

                        e.editor.resetDirty();
                    }
                    ckUpdate.call(ckValue);
                    ckValue = null;
                }
            };
            options.floatSpaceDockedOffsetY = 0;
            options.extraPlugins = 'sourcedialog';
            options.removePlugins = 'sourcearea';

            var editor = CKEDITOR.inline(element, options);

            //handle destroying
            ko.utils.domNodeDisposal.addDisposeCallback(element, function () {

                    var existingEditor = CKEDITOR.instances && CKEDITOR.instances[element.id];
                    if (existingEditor) {
                        existingEditor.destroy(true);
                    }

            });

        },
        update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
            //handle programmatic updates to the observable
            var value = ko.utils.unwrapObservable(valueAccessor()),
                existingEditor = CKEDITOR.instances && CKEDITOR.instances[element.id];

            if (existingEditor) {
                if (value !== existingEditor.getData()) {
                    existingEditor.setData(value, function () {
                        this.checkDirty(); // true
                    });

                }
            }

        }

    };

})(ko, CKEDITOR);

The view and its ViewModel

    <h1>
        CKEditor And knockoutJS</h1>
    <h2>
        Raw Data</h2>
    <pre data-bind="text: body"></pre>
    <h2>
        Editor</h2>
    <div data-bind="inlineCkeditor: body, ckUpdate: updateContent" contenteditable="true">
    </div>
    <script src="Scripts/knockout-2.3.0.js"></script>
    <script src="content/ckeditor/ckeditor.js" type="text/javascript"></script>
    <script src="Scripts/ko-inline-ckeditor.js" type="text/javascript"></script>
    <script type="text/javascript">
        "use strict";
        var my = my || {};
        my.vm = (function () {
            var body = ko.observable('<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>'),
            updateContent = function () {
                var newData = this;
                // call ajax
                console.log(newData);
            };
            return {
                body: body,
                updateContent: updateContent
            }
        })();

        ko.applyBindings(my.vm);
    </script>

Notes

  • You have to download the sourcedialog CkEditor plugin to make the source editing work as well.
  • Take note of the sequence of the script placement
Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer’s view in any way.
Tagged with: ,
Posted in knockoutJS
  • chucks

    Thank you for this. It has saved me a bunch of time! I have a couple of modifications you may be interested in. The following changes will keep the viewmodel up-to-date with every change but will not cause the ckUpdate function to be called for every change. ckUpdate will be called only on blur. This way the vewmodel is always current.

    1) Load the undo plug in: options.extraPlugins = ‘sourcedialog,undo';
    2) The undo plug in gives you the ‘change’ event. so change ‘blur:’ to ‘change:’
    3) In the change callback, do not call ckUpdate.call(ckValue);
    4) Add a blur callback with the following:
    blur: function (e) {
    ckUpdate.call(bindingContext.$data);
    }
    Note that I’m calling the ckUpdate function with bindingContext.$data so that the this pointer within the ckUpdate function is set to the correct object.

    • Prasanna

      Hi,
      Thanks for the changes. Can you please post the updated code.

  • chucks

    Another small change:

    change the following two lines from this

    var options = allBindingsAccessor().ckeditorOptions || {};
    var ckUpdate = allBindingsAccessor().ckUpdate || function () { };

    to this:

    var options = allBindingsAccessor.get(‘ckeditorOptions’) || {};
    var ckUpdate = allBindingsAccessor.get(‘ckUpdate’) || function () { };

    Otherwise Knockout will call all the computed functions for each allBindingsAccessor() call. For example, I have the following in my data-binding:

    data-bind=’inlineCkeditor: text, ckUpdate: UpdateDatabase, attr: { id: makeID(), contenteditable: “true”}

    Knockout called makeID() for each allBindingsAccessor() call. However, changing to use allBindingsAccessor.get() eliminated the problem.

  • ericpanorel

    Good Stuff!