Skip to content
Michael Papworth edited this page Jan 22, 2014 · 1 revision

Demonstration of original version

http://jsfiddle.net/scottmessinger/yJ5GV/2/

Updated original version: http://jsfiddle.net/MikeEast/yJ5GV/167/

Code

(function($) {
var instances_by_id = {} // needed for referencing instances during updates.
  , init_queue = $.Deferred() // jQuery deferred object used for creating TinyMCE instances synchronously
  , init_queue_next = init_queue;
init_queue.resolve();
ko.bindingHandlers.tinymce = {
    init: function (element, valueAccessor, allBindingsAccessor, context) {
        var init_arguments = arguments;
        var options = allBindingsAccessor().tinymceOptions || {};
        var modelValue = valueAccessor();
        var value = ko.utils.unwrapObservable(valueAccessor());
        var el = $(element);

        options.setup = function (ed) {
            ed.onChange.add(function (editor, l) { //handle edits made in the editor. Updates after an undo point is reached.
                if (ko.isWriteableObservable(modelValue)) {
                    modelValue(l.content);
                }
            });

            //This is required if you want the HTML Edit Source button to work correctly
            ed.onBeforeSetContent.add(function (editor, l) { 
                if (ko.isWriteableObservable(modelValue)) {
                    modelValue(l.content);
                }
            });

           ed.onPaste.add(function (ed, evt) { // The paste event for the mouse paste fix.
                var doc = ed.getDoc();

                if (ko.isWriteableObservable(modelValue)) {
                    setTimeout(function () { modelValue(ed.getContent({ format: 'raw' })); }, 10);
                }

            });

            ed.onInit.add(function (ed, evt) { // Make sure observable is updated when leaving editor.
                var doc = ed.getDoc();
                tinymce.dom.Event.add(doc, 'blur', function (e) {
                    if (ko.isWriteableObservable(modelValue)) {
                        modelValue(ed.getContent({ format: 'raw' }));
                    }
                });
            });

        };

        //handle destroying an editor (based on what jQuery plugin does)
        ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
            $(element).parent().find("span.mceEditor,div.mceEditor").each(function (i, node) {
                var tid = node.id.replace( /_parent$/, '' );
                var ed = tinyMCE.get(tid);
                if (ed) {
                    ed.remove();
                    // remove referenced instance if possible.
                    if(instances_by_id[tid]){
                      delete instances_by_id[tid];
                    }
                }
            });
        });

        // TinyMCE attaches to the element by DOM id, so we need to make one for the element if it doesn't have one already.
        if (!element.id) {
            element.id = tinyMCE.DOM.uniqueId();
        }

        // create each tinyMCE instance synchronously. This addresses an issue when working with foreach bindings
        init_queue_next = init_queue_next.pipe(function () {
            var defer = $.Deferred();
            var init_options = $.extend({}, options, {
                mode: 'none',
                init_instance_callback: function(instance) {
                    instances_by_id[element.id] = instance;
                    ko.bindingHandlers.tinymce.update.apply(undefined,init_arguments);
                    defer.resolve(element.id);
                    if (options.hasOwnProperty("init_instance_callback")) {
                        options.init_instance_callback(instance);
                    }
                }
            });
            setTimeout(function() {
                tinyMCE.init(init_options);
                setTimeout(function () {
                    tinyMCE.execCommand("mceAddControl", true, element.id);
                }, 0);
            },0);
            return defer.promise();
        });
        el.val(value);
    },
    update: function (element, valueAccessor, allBindingsAccessor, context) {
        var el = $(element);
        var value = ko.utils.unwrapObservable(valueAccessor());
        var id = el.attr('id');

        //handle programmatic updates to the observable
        // also makes sure it doesn't update it if it's the same.
        // otherwise, it will reload the instance, causing the cursor to jump.
        if (id !== undefined && id !== '' && instances_by_id.hasOwnProperty(id)) {
            var content = instances_by_id[id].getContent({ format: 'raw' });
            if (content !== value) {
                el.val(value);
            }
        }
    }
};
}(jQuery));

Note: to get this to work I had to change the setTimeout intervals to 10 for both setTimeout calls in init_queue_next. Hope this helps somebody else. (Kevin Jones)

Note #2: This didn't work with TinyMCE version 4+. I got it working with 3.5.8. (Mikael Östberg).

Initial version by Ryan Niemeyer. Updated by Scott Messinger, Frederik Raabye, Thomas Hallock and Drew Freyling.

Version for TinyMCE 4 & TinyMCE jQuery Plugin

Source: https://github.com/michaelpapworth/tinymce-knockout-binding

Release: https://www.nuget.org/packages/tinymce-knockout-binding

Version for TinyMCE 4 (needs polishing)

Code

(function ($) {
    var instances_by_id = {}; // needed for referencing instances during updates.
    var init_queue = $.Deferred(); // jQuery deferred object used for creating TinyMCE instances synchronously
    init_queue.resolve();

    ko.bindingHandlers.tinymce = {
        init: function (element, valueAccessor, allBindingsAccessor, context) {

            var options = allBindingsAccessor().tinymceOptions || {};
            var modelValue = valueAccessor();
            var value = ko.utils.unwrapObservable(valueAccessor());
            var $element = $(element);

            options.setup = function (ed) {
                ed.on('change', function (e) {
                    if (ko.isWriteableObservable(modelValue)) {
                        var current = modelValue();
                        if(current !== this.getContent()) {
                            modelValue(this.getContent());
                        }
                    }
                });
                ed.on('keyup', function (e) {
                    if (ko.isWriteableObservable(modelValue)) {
                        var current = modelValue();
                        var editorValue = this.getContent({ format: 'raw' });
                        if(current !== editorValue) {
                            modelValue(editorValue);
                        }
                    }
                });
                ed.on('beforeSetContent', function (e, l) {
                    if (ko.isWriteableObservable(modelValue)) {
                        if (typeof (e.content) != 'undefined') {
                           var current = modelValue();
                           if(current !== e.content) {
                               modelValue(e.content);
                           }
                        }
                    }
                });
            };

            //handle destroying an editor 
            ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
                $(element).parent().find("span.mceEditor,div.mceEditor").each(function (i, node) {
                    var tid = node.id.replace(/_parent$/, ''),
                        ed = tinymce.get(tid);
                    if (ed) {
                        ed.remove();
                        // remove referenced instance if possible.
                        if (instances_by_id[tid]) {
                            delete instances_by_id[tid];
                        }
                    }
                });
            });

            setTimeout(function () {
                if (!element.id) {
                    element.id = tinymce.DOM.uniqueId();
                }
                tinyMCE.init(options);
                tinymce.execCommand("mceAddEditor", true, element.id);
            }, 0);
            $element.html(value);

        },
        update: function (element, valueAccessor, allBindingsAccessor, context) {
            var $element = $(element),
                value = ko.utils.unwrapObservable(valueAccessor()),
                id = $element.attr('id');

            //handle programmatic updates to the observable
            // also makes sure it doesn't update it if it's the same. 
            // otherwise, it will reload the instance, causing the cursor to jump.
            if (id !== undefined) {
                var tinymceInstance = tinyMCE.get(id);
                if (!tinymceInstance)
                    return;
                var content = tinymceInstance.getContent({ format: 'raw' });
                if (content !== value) {
                    $element.html(value);
                    //this should be more proper but ctr+c, ctr+v is broken, above need fixing
                    //tinymceInstance.setContent(value,{ format: 'raw' })
                }
            }
        }
    };
}(jQuery));