1

I’m trying to implement TinyMCE into John Papa’s HotSpa template.

I’m currently using Knockout 3.1.0, and Durandal 2.0.

For some unknown reason, my custom knockout binding init method is triggered only once on first load, but twice when view is refreshed.

By refresh, I mean using durandal router, I navigated to the same view again (but with different parameters). It still went through the whole durandal life cycle, hooks of deattached and attached are called again.

I had some debug code in there where the second call returned .editors.length = 1 which means the editor was already initialized in the first call.

Note: Inside the init method I have a method ko.utils.domNodeDisposal.addDisposeCallback and for some reason it is called after the second init call. I commented it out and tinyMCE still disappears. I looped through the editors and noticed it is deemed hidden. isHdidden = true.

<textarea id="mytinymce" class="tinymce" 
          data-bind="attr: {id: FieldId}, tinymce: FieldValue”></textarea>

The page initially loads properly with the TinyMCE control, however it fails after a screen refresh.

I navigate away from the view, and back.

ko.bindingHandlers.tinymce = {
        init: function (element, valueAccessor, allBindingsAccessor, 
                        context, arg1, arg2) {
            var options = allBindingsAccessor().tinymceOptions || {};
            var modelValue = valueAccessor();
            var value = ko.utils.unwrapObservable(valueAccessor());

            var el = $(element);
            var id = el.attr('id');

            //options.theme = "advanced";
            options.theme = "modern";

            options.menubar = false;
            options.plugins = [
                "advlist autolink autosave link image lists charmap print preview hr anchor pagebreak spellchecker",
                "searchreplace wordcount visualblocks visualchars code fullscreen insertdatetime media nonbreaking",
                "table contextmenu directionality template textcolor paste fullpage textcolor"
            ];
            //tinymce buttons
            //http://www.tinymce.com/tryit/classic.php
            options.toolbar_items_size = 'small';
            options.toolbar1 =
            "bold italic underline strikethrough | alignleft aligncenter alignright alignjustify | forecolor backcolor | hr removeformat | subscript superscript  ";

            ////handle edits made in the editor. Updates after an undo point is reached.
            options.setup = function (editor) {
                editor.on('change', function (e) {
                    if (ko.isWriteableObservable(modelValue)) {
                        var content = editor.getContent({ format: 'raw' });
                        modelValue(content);
                    }
                });

            };

            el.html(value);

            $(element).tinymce(options);

            //handle disposal (if KO removes by the template binding)
            ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
                var tinymceInstance = tinyMCE.get(id);
                if (tinymceInstance != undefined) {
                    // if instance already exist, then get rid of it... we don't want it
                    tinymceInstance.remove();
                }
            });              
        },
        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) {
                //$(el).tinymce();

                var tinymceInstance = tinyMCE.get(id);
                if (!tinymceInstance)
                    return;
                var content = tinymceInstance.getContent({ format: 'raw' });
                if (content !== value) {
                    //tinymceInstance.setContent(value);
                    valueAccessor(content);
                    //$(el).html(value);
                }
            }
        }
    };

Unfortunately I don't see any errors in the console but the editor (including the text area) are hidden and not visible.

GôTô
  • 7,974
  • 3
  • 32
  • 43
Michael Pang
  • 366
  • 1
  • 2
  • 10
  • 1
    Are they any parent elements that have "if", "foreach" or "template" bindings? They can cause child elements to re-initialised – Robert Slaney Jun 09 '14 at 22:55
  • Yes we have some code that says only compose the view template if showTinyMce = true, and we cannot get rid of this. – Michael Pang Jun 10 '14 at 05:31
  • 1
    When an init method is called, you can follow the stack trace to work out what observable has triggered the re-init. "if" bindings are usually the culprit, but "template" bindings can also trigger a DOM build. Another thing to take note is that observable's equality comparer only compares object instances or native types, it will not trawl the object graph and compare values. That has caught me before triggering unexpected refreshes. – Robert Slaney Jun 11 '14 at 03:11
  • Have you tried setting a variable in the `init` function the first time you enter it and checking if it has been set to run the code only when it hasn't? – GôTô Jun 12 '14 at 08:31
  • In the init, I've checked that if the tinyMce already active then just return. But tinyMCE is still hidden. – Michael Pang Jun 15 '14 at 12:53

2 Answers2

1

I have changed the html to:

<textarea data-bind="tinymce: FieldValue"></textarea>

i.e. removed the id attribute binding.

And now it works.

I noticed it still calls init twice but it works.

You must let TinyMCE control the ID.

Michael Pang
  • 366
  • 1
  • 2
  • 10
  • Thats what i needed to know. The field ID fight. trying to set when using Tiny did work in SPA when reloaded.. ouch. It works now ;-) Thanks Michael Pang – phil soady Jun 17 '14 at 06:23
  • I like your binding. How would you make it customizable for options and plugins? – Asle G Sep 12 '14 at 09:34
0

You say you enter the init function twice.

Have you tried a simple code to prevent running its code the second time? (I would try if you had a working jsFiddle)

Something like:

ko.bindingHandlers.tinymce = {
    init: function (element, valueAccessor, allBindingsAccessor, 
                    context, arg1, arg2) {
        if (!$(element).data("tinymce-hasinitstarted")) {
            $(element).data("tinymce-hasinitstarted", true);
            //your code here
        }
    }
}
GôTô
  • 7,974
  • 3
  • 32
  • 43
  • Although this "solution" can be used and it will probably work, this is a gambiarra, an ugly hack, and only an workaround for his problem, it doesn't actually fix the problem, it only hides it, and should not be given as a solution for his problem. – Buzinas Jun 14 '14 at 14:18
  • Well if you have a solution that is not ugly don't hesitate to post it. Without a fiddle I cannot propose a better way and I think the most important is to have one. – GôTô Jun 14 '14 at 23:48