Terrabyte
Terrabyte

Reputation: 342

Knockoutjs + Ckeditor Need help figuring something out

I have this code that i need to connect with ckeditor:

Basically what i'm trying to do is have it connect with a data-bind that is already there via:

textarea autocomplete="off" class="form-control" data-bind="rev_ckeditor, value: app.models.userReview.body" id="editor" maxlength="50000" name="body" cols="50" rows="10"

The textarea above has both a value data-bind and ckeditor's data-dind. But ckwditor shows up but not the value's data-bind, is there something wrong?

Code:

(function($) {
'use strict'

app.viewModels.reviews = {

    /**
     * All reviews.
     *
     * @type ko.observable(Array)
     */
    sourceItems: ko.observableArray([]),

    /**
     * Sorting type and order.
     *
     * @type ko.observable(String)
     */
    currentSort: ko.observable(),

    /**
     * Holds average score of all critic reviews.
     *
     * @type ko.observable(String)
     */
    criticAverage: ko.observable(),

    /**
     * Holds count of all critic reviews.
     *
     * @type ko.observable(String)
     */
    criticCount: ko.observable(),

    /**
     * Holds average score of all user reviews.
     *
     * @type ko.observable(String)
     */
    userAverage: ko.observable(),

    /**
     * Holds count of all user reviews.
     *
     * @type ko.observable(String)
     */
    userCount: ko.observable(),

    /**
     * Whether to who user, critic or all reviews.
     *
     * @type ko.observable(String)
     */
    currentType: ko.observable(),

    /**
     * Send request to server to create a new
     * user review.
     *
     * @return void
     */
    create: function(form) {
        var self = this;

        var params = {
            data: ko.toJSON(app.models.userReview),
            success: function(response) {
                var exists = false;

                $.each(self.sourceItems(), function(i,v) {

                    //if user has already written a review for this game we'll just replace
                    //it with this one as that's what backend is doing as well
                    if (v.type == 'user' && v.user_id == parseInt(vars.userId)) {

                        self.sourceItems()[i] = ko.toJS(app.models.userReview);
                        self.sourceItems.notifySubscribers();
                        exists = true;
                        return false;
                    }
                });

                if ( ! exists) {
                    self.sourceItems.push(ko.toJS(app.models.userReview));
                }

                $('#review-modal').modal('hide');
                app.utils.noty(response, 'success');
            },

            /**
             * Append any validation errors returned to new review form.
             *
             * @param  jq
             * @return void
             */
            error: function(jq) {
                $('.alert').remove();
                app.utils.appendError(jq);
            },
            url: form.action,
        };

        app.utils.ajax(params);
    },

    /**
     * Handle user click on review edit button.
     *
     * @param  app.models.review review
     * @return void
     */
    edit: function(review) {
        app.models.userReview.id(review.id);
        app.models.userReview.title(review.title);
        app.models.userReview.body(review.body);
        app.models.userReview.score(review.score);
        app.models.userReview.story_rev(review.story_rev);
        app.models.userReview.animation_rev(review.animation_rev);
        app.models.userReview.sound_rev(review.sound_rev);
        app.models.userReview.characters_rev(review.characters_rev);
        app.models.userReview.enjoyment_rev(review.enjoyment_rev);

        $('#review-modal').modal('show');
    },

    /**
     * Handle user click on delete button.
     *
     * @param  Object review
     * @return void
     */
    delete: function(review) {
        var self = app.viewModels.reviews;

        app.utils.ajax({
            url: vars.urls.baseUrl+'/movies/'+vars.titleId+'/reviews/'+review.id,
            type: 'DELETE',
            data: ko.toJSON(vars.token),
            success: function() {
                self.sourceItems.remove(review);
            }
        })
    }
};

/**
 * Calculate average critic/user review score as well as review counts.
 *
 * @return void
 */
 app.viewModels.reviews.calculateMeta = ko.computed(function() {
     var self     = this, score = 0,
         crCount  = 0,    crAvg  = 0,
         uCount   = 0,    uAvg   = 0;

     $.each(self.sourceItems(), function(ind, val) {
         if (val.type == 'critic') {
             crCount++;
             crAvg += parseFloat(val.score);
         } else if (val.type == 'user') {
             uCount++;
             uAvg += parseFloat(val.score);
         }
     });

     //set average to flash if there's no reviews otherwise calculate an avarage
     crCount ? self.criticAverage(crAvg / crCount) : self.criticAverage('/');
     uCount ? self.userAverage(uAvg / uCount) : self.userAverage('/');

     self.userCount(uCount);
     self.criticCount(crCount);

 }, app.viewModels.reviews, {deferEvaluation: true});

/**
 * Filters critic reviews on platform dropdown change,
 * if no reviews found fires an ajax request to query
 * review data provider.
 *
 * @return array
 */
app.viewModels.reviews.filteredReviews = ko.computed(function() {
    var self = this, filtered;

    //filter by user or critic reviews if user select either
    if (self.currentType() === 'all') {
        filtered = self.sourceItems();
    } else {
        filtered = ko.utils.arrayFilter(self.sourceItems(), function(rev) {
            return rev.type === self.currentType();
        });
    }

    //split current sort by camelCase into type and order params
    var sort = self.currentSort().match(/([A-Z]?[^A-Z]*)/g).slice(0,-1);

    if (sort.length === 2) {
        filtered.sort(app.utils.sort[sort[0]](sort[1]));
    }

    return filtered ? filtered : [];

}, app.viewModels.reviews, {deferEvaluation: true});

/**
 * New review form model.
 *
 * @type Object
 */
app.models.userReview = {
    id: ko.observable(),
    author: app.username,
    title: ko.observable(),
    source: 'Test',
    body: ko.observable(),
    story_rev: ko.observable(),
    animation_rev: ko.observable(),
    characters_rev: ko.observable(),
    sound_rev: ko.observable(),
    enjoyment_rev: ko.observable(),
    score: ko.observable(),
    type: 'user',
    _token: vars.token,
    user_id: app.user_id,
};


/**
* Renders CKeditor on textarea.
*
* @type {Object}
*/
ko.bindingHandlers.rev_ckeditor = {
        init: function (element, valueAccessor, allBindingsAccessor, context, review) {
                var $element = $(element);
                var value = ko.utils.unwrapObservable(valueAccessor());

                $element.html(value);
                var editor = CKEDITOR.replace('editor');

                /**
                * Resize CKeditor according to textarea col and rows attributes.
                *
                * @return void
                */
                jQuery.fn.cke_resize = function() {
                    return this.each(function() {
                            var $this = $(this);
                            var rows = $this.attr('rows');
                            var height = rows * 20;
                            $this.next("div.cke").find(".cke_contents").css("height", height);
                    });
                };

                CKEDITOR.on('instanceReady', function(){ $element.cke_resize(); });

                //Update body observable on ckeditor blur event
                editor.on('blur', function (e) {

                       var obs = app.models.userReview.body(review.body);

                        if (ko.isWriteableObservable(obs)) {
                                obs(e.editor.getData());
                        }
                });
        }
};

})(jQuery);

Upvotes: 0

Views: 468

Answers (1)

janfoeh
janfoeh

Reputation: 10328

I am not familiar with CKEditor, so I might be wrong here, but many WYSIWYG editors don't update themselves just because you change the data in the underlying <textarea>, which is what your value binding would do. I suggest removing the value binding completely, and letting the editor binding to all the work.

Right now, your binding only seems to handle writing editor changes to the observable. We also need to handle the opposite direction and write observable changes to the editor. According to the CKEditor documentation, that is done through Editor#setData:

ko.bindingHandlers.rev_ckeditor = {
    init: function (element, valueAccessor, allBindingsAccessor, context, review) {
        var $element   = $(element),
            observable = valueAccessor();

        var editor = CKEDITOR.replace('editor');

        ko.computed(function() {
          editor.setData( observable() );
        }, { disposeWhenNodeIsRemoved: element });

        jQuery.fn.cke_resize = function () {
            return this.each(function () {
                var $this = $(this);
                var rows = $this.attr('rows');
                var height = rows * 20;
                $this.next("div.cke").find(".cke_contents").css("height", height);
            });
        };

        CKEDITOR.on('instanceReady', function () {
            $element.cke_resize();
        });

        editor.on('blur', function (e) {
            if (ko.isWriteableObservable(observable)) {
                observable(e.editor.getData());
            }
        });
    }
};

Usually, one would write changes to the observable back to the editor in the bindings update handler. In this case, it is more convenient to do it in the init handler, because we already have access to the editor instance there.

If you give the editor binding access to the observable, everything should work as expected:

<textarea data-bind="rev_ckeditor: app.models.userReview.body"></textarea>

Upvotes: 1

Related Questions