/**
 * Script for Content Template edit page
 *
 * Creates a function object WPViews.CTEditScreen and it's instance WPViews.ct_edit_screen.
 *
 * Requires: jquery, underscore, knockout3, views-utils-script, icl_editor-script, icl_media-manager-js, quicktags
 * and wp-pointer. Also wp_enqueue_media() call is necessary.
 *
 * @since 1.9
 */

// WPV_Toolset, WPV_Toolset.CodeMirror_instance and WPViews are expected global variables.
var WPV_Toolset = WPV_Toolset || {};

if (typeof WPV_Toolset.CodeMirror_instance === "undefined") {
    WPV_Toolset.CodeMirror_instance = [];
}

var WPViews = WPViews || {};

var ajaxurl = ajaxurl || '';


/**
 * CT edit page object.
 *
 * After the document is loaded, it will be instantiated into WPViews.ct_edit_screen. It encapsulates everything
 * that happens on the edit page.
 *
 * Knockout is used heavily. All interaction between knockout and the page is encapsulated inside a ViewModel (self.vm).
 *
 * It contains following sections:
 * - Constants
 * - ViewModel
 * - Helper functions, knockout modifications, etc.
 * - Interaction with the server
 * - Tooltips, formatting instructions and pointers
 * - Codemirror stuff
 * - Action bar
 * - Init
 *
 * Lot of code here is rather generic. If a new Edit page, based on this one, is going to be created in the future,
 * I recommend to extract as much as possible common code into a generic script that would be shared between those two.
 *
 * @param $ The jQuery object.
 *
 * @since 1.9.0
 * @since 2.3.0 Added a keymap shortcut for editing Viws shortcodes in Codemirror editors.
 */
WPViews.CTEditScreen = function( $ ) {

    var self = this;

	/**
	 * Keymap shortcut definition for editing Views shortcodes in Codemirror editors.
	 * @since 2.3.0
	 */
	self.editor_keymap = {
		// Ctrl + Alt + Space = Try to edit the current Views shortcode under the cursor
		"Ctrl-Alt-Space": function( cm ) {
					var textarea_id = cm.getTextArea().id;
					if (
						textarea_id !== undefined
						&& _.has( WPV_Toolset.CodeMirror_instance, textarea_id )
					) {
						Toolset.hooks.doAction( 'wpv-action-wpv-shortcodes-gui-maybe-edit-shortcode', textarea_id );
					}
				}
	};


    self.html = $('html');


    /**
     * If set to true, console logging will be activated on CT edit page.
     * @type boolean
     */
    self.debug = false;


    /**
     * Log function that works only in debug mode (controlled by self.debug).
     * @since 1.10
     */
    self.log = function() {
        if(self.debug) {
            console.log.apply(console, arguments);
        }
    };



    // ----------------------------------------------------------------------------
    // Constants
    // ----------------------------------------------------------------------------


    // Selectors for jQuery (mostly message containers)
    self.titleSectionMessageContainer = '.js-wpv-title-section .js-wpv-message-container';

    self.settingsSectionMessageContainer = '.js-wpv-settings-section .js-wpv-message-container';

    self.usageSectionMessageContainer = '.js-wpv-usage-section .js-wpv-message-container';
    self.usageOtherAssignments = '.js-wpv-usage-section .js-wpv-usage-other-assignments';

    self.contentSectionMessageContainer = '.js-wpv-content-section .js-wpv-message-container';


    // ----------------------------------------------------------------------------
    // ViewModel
    // ----------------------------------------------------------------------------


    /**
     * The ViewModel, which will be instantiated into self.vm. It handles loading and displaying
     * data (various properties of the Content Template) via Knockout bindings.
     *
     * There are few things to note about CT properties in the ViewModel. First of all, for each property (propertyName)
     * there have to be two Knockout observables: vm.propertyNameAccepted and vm.propertyNameOriginal. "Accepted" is
     * for values that *can* be stored into database (there can be a Knockout binding to them or to yet another observable
     * or pureComputed that does some kind of input validation, but that's not a requirement), and "Original", that will
     * mirror the current state of CT property as it *is* stored in the database. Difference between those two observables
     * are used to determine if a property needs updating, etc. See the "Model updating" part for more information.
     *
     * @param ct_data The Model = Content Template data as constructed in wpv_ct_editor_page(). It contains required
     *     CT properties and additional data added by individual sections.
     * @param section_data Additional data from different sections, if the standard way cannot be used.
     *
     * @since 1.9
     */
    self.ViewModel = function(ct_data, section_data) {


        /**
         * vm ~ ViewModel
         */
        var vm = this;


        /**
         * Content template ID
         * @type int
         */
        vm.id = ct_data.id;


        /**
         * If true, debug mode will be activated (console logging of things happening inside vm)
         * @type bool
         */
        vm.debug = self.debug;


        /**
         * Print a log message if debug mode is active.
         *
         * In debug mode, it has the same behaviour as console.log(). Otherwise it does nothing.
         */
        vm.log = function() {
            if(vm.debug) {
                console.log.apply(console, arguments);
            }
        };


        // ------------------------------------------------------------------------
        // Mapping between Model's and ViewModel's property names
        // ------------------------------------------------------------------------

        /* To achieve greater flexibility, property names in the ViewModel can differ from those in Model.
         * Model property names are dictated by properties in WPV_Content_Template PHP class and there can be
         * other needs (or just conventions) for naming in ViewModel.
         *
         * Model property names should be mentioned only once, so they can be easily changed in the future.
         *
         * In order to access Model's property, convert property name by vm.getModelPropertyName() or just
         * use vm.getPropertyFromModel().
         *
         * From now on, by 'property' or 'property name' is meant it's ViewModel version, unless specified otherwise.
         */

        /**
         * Mapping between ViewModel and Model property names.
         *
         * ViewModel propery names are properties and Model property names are their values.
         *
         * If a name pair isn't present here, both values are the same. You should never need to access this object
         * directly, allways use vm.getModelPropertyName or vm.getViewModelPropertyName instead.
         *
         * @since 1.9
         */
        vm.propertyNameViewmodelToModelMap = {
            description: 'description_raw',
            outputMode: 'output_mode',
            assignedSinglePostTypes: 'assigned_single_post_types',
            assignedPostArchives: 'assigned_post_archives',
            assignedTaxonomyArchives: 'assigned_taxonomy_archives',
            postContent: 'content',
            templateCss: 'template_extra_css',
            templateJs: 'template_extra_js'
        };


        /**
         * Reverse mapping to vm.propertyNameViewmodelToModelMap.
         *
         * @since 1.9
         */
        vm.propertyNameModelToViewmodelMap = _.invert(vm.propertyNameViewmodelToModelMap);


        /**
         * Get a Model property name from a ViewModel property name.
         *
         * @param viewModelPropertyName string
         * @returns string
         *
         * @since 1.9
         */
        vm.getModelPropertyName = function(viewModelPropertyName) {
            if(vm.propertyNameViewmodelToModelMap.hasOwnProperty(viewModelPropertyName)) {
                return vm.propertyNameViewmodelToModelMap[ viewModelPropertyName ];
            } else {
                return viewModelPropertyName;
            }
        };


        /**
         * Get a ViewModel property name from a Model property name.
         *
         * @param modelPropertyName string
         * @returns string
         *
         * @since 1.9
         */
        vm.getViewModelPropertyName = function(modelPropertyName) {
            if(vm.propertyNameModelToViewmodelMap.hasOwnProperty(modelPropertyName)) {
                return vm.propertyNameModelToViewmodelMap[ modelPropertyName ];
            } else {
                return modelPropertyName;
            }
        };


        /**
         * Get a property value from Model.
         *
         * @param propertyName ViewModel property name.
         * @returns {*} Property value (undefined if property doesn't exist). Refer to WPV_Content_Template for
         *     particular property description.
         *
         * @since 1.9
         */
        vm.getPropertyFromModel = function(propertyName) {
            return ct_data[vm.getModelPropertyName(propertyName)];
        };


        // ------------------------------------------------------------------------
        // Model updating (generic functionality)
        // ------------------------------------------------------------------------

        /* In order to update a property manually (e.g. on button click), just call
         * vm.updateProperties() and pass array of property names.
         *
         * In order to add property to the automatically updated ones, just push the property name
         * to vm.propertiesToUpdate observableArray.
         *
         * After the update process is finished, vm.lastUpdateResults will be updated and for successfully updated
         * properties, their "Original" versions will be equal with "Accepted".
         */


        /**
         * observableArray of properties that are being updated right now.
         *
         * This is used to determine whether a particular section is being updated (spinner visibility etc.).
         *
         * @since 1.9
         */
        vm.updatingProperties = ko.observableArray();


        /**
         * Update specified CT properties.
         *
         * Manual property updating should be done through this method.
         * It updates vm.updatingProperties instantly and initiates the updating, which happens in a debounced method.
         *
         * @param propertyNames Array of property names that should be updated.
         *
         * @since 1.9
         */
        vm.updateProperties = function(propertyNames) {
            vm.log('vm.updateProperties BEGIN:', propertyNames);
            vm.updatingProperties.pushAll(propertyNames);
            vm.updatePropertiesDebounced();
            vm.log('vm.updateProperties END:', propertyNames);
        };


        /**
         * An observable object with the results of last update.
         *
         * It will allways have following properties:
         * - propertiesUpdated: Array of names of all properties that have been updated (disregarding update result).
         * - succeeded: Array of successful update results.
         * - failed: Array of unsuccessful update results.
         *
         * An update result is an object with those properties:
         * - name: Property name.
         * - success: boolean indicating successful update.
         * - message: Optional. If present, this is an error message that should be displayed instead of a generic one
         *     (it most probably comes from a WPV_RuntimeExceptionWithMessage thrown by WPV_Content_Template).
         *
         * @since 1.9
         */
        vm.lastUpdateResults = ko.observable({
            propertiesUpdated: [],
            succeeded: [],
            failed: []
        });


        /**
         * The actual, debounced, property updating. Prepare data for the AJAX call, execute it and process the result.
         * See comment at the top of this section for more information.
         *
         * @param propertyNames array
         *
         * @since 1.9
         */
        vm.updatePropertiesDebounced = _.debounce(function() {
            var propertyNames = _.toArray(vm.updatingProperties());
            vm.log('vm.updatePropertiesDebounced BEGIN:', propertyNames);

            // Prepare data (property model names and values) for the AJAX call.
            var ct_data = _.map(propertyNames, function(propertyName) {
                return {
                    name: vm.getModelPropertyName(propertyName),
                    value: vm[ propertyName + 'Accepted' ]()
                };
			});

			self.highlight_action_bar('saving');

            // Callback after asynchronous AJAX action.
            var callback = function(ajaxResult) {

                var isCallSuccessful = ( ajaxResult !== false );
                var failedUpdates = [];
                var successfulUpdates = [];

                // If call is successful at all, populate failedUpdates and successfulUpdates with update results.
                // Otherwise failedUpdates will be populated with all property names.
                if (isCallSuccessful) {

                    // Transform model property names into viewmodel property names.
                    var updateResults = _.map(ajaxResult, function (updateResult) {
                        updateResult.name = vm.getViewModelPropertyName(updateResult.name);
                        return updateResult;
                    });

                    // Sort update results into successful and failed ones.
                    successfulUpdates = _.filter(updateResults, function (propertyUpdateResult) {
                        return (true == propertyUpdateResult.success);
                    });

                    failedUpdates = _.difference(updateResults, successfulUpdates);

                } else {
                    // Unsuccessful call, all has failed.
                    vm.log('vm.updateProperties: AJAX call unsuccessful, failed updates:', propertyNames);
                    failedUpdates = _.map(propertyNames, function (propertyName) {
                        return {
                            name: propertyName,
                            success: false
                        }
                    });
                }

                // For successfully updated properties, update their "Original" versions.
                _.each(successfulUpdates, function (updateResult) {
                    var originalValue = vm[updateResult.name + 'Original'];

                    // We will update with the value that was actually saved to database. So even
                    // if the 'Accepted' version of the VM property has changed in the meantime,
                    // we're on the safe side (the 'Original' version will keep the right data
                    // and changed property will be further indicated correctly)
                    var acceptedValue = _.findWhere(ct_data, {name: vm.getModelPropertyName(updateResult.name)}).value;
                    vm.log('vm.updatePropertiesDebounced.callback: updating property ' + updateResult.name + ' original (', originalValue(), ') to accepted (', acceptedValue, ')' );

                    // Arrays need to be handled differently - we have to clone them instead passing them by reference.
                    if (_.isArray(acceptedValue)) {
                        originalValue(_.toArray(acceptedValue));
                    } else {
                        originalValue(acceptedValue);
                        // For the case of an escaped accepted value that is equal to the original value, we need to trigger
                        // the "valueHasMutated" event for the subscribers to be notified as it won't be triggered by itself.
                        // This is particularly useful when the escaped CT title is the same as its original value.
                        if ( originalValue() === acceptedValue ) {
                            originalValue.valueHasMutated();
                        }
                    }
                });

                // Tell that those properties are no longer being updated.
                vm.updatingProperties.removeAll(propertyNames);

                // Update the last update results.
                vm.lastUpdateResults({
                    propertiesUpdated: propertyNames,
                    succeeded: successfulUpdates,
                    failed: failedUpdates
                });

                vm.log('vm.updatePropertiesDebounced END:', propertyNames);

            };

            // Execute the AJAX call asynchronously
            self.updateProperties(ct_data, callback);

        }, 500);


        /**
         * observableArray of names of properties that should be automatically updated.
         *
         * @since 1.9
         */
        vm.propertiesToUpdate = ko.observableArray();


        /**
         * When propertiesToUpdate array is modified, trigger the update and empty this array.
         *
         * @since 1.9
         */
        vm.propertiesToUpdate.subscribe(_.debounce(function(newPropertiesToUpdate) {
            if(typeof(newPropertiesToUpdate) != 'undefined' && newPropertiesToUpdate.length > 0) {
                var propertiesToUpdate = _.toArray(newPropertiesToUpdate);
                vm.updateProperties(propertiesToUpdate);
                vm.propertiesToUpdate.removeAll(propertiesToUpdate);
            }
        }, 100));


        // ------------------------------------------------------------------------
        // Handling property changes and update results
        // ------------------------------------------------------------------------


        /**
         * Array of names of properties that need updating.
         *
         * This is being used to compute Update button visibility or to trigger automatic update.
         *
         * @since 1.9
         */
        vm.changedProperties = ko.observableArray();


        /**
         * Generic handler for detecting property change (and the need to update) with custom comparator.
         *
         * Compares "Accepted" and "Original" versions of given property via provided comparator. Based on that, it
         * either adds or removes the property name from vm.changedProperties.
         *
         * @param propertyName Name of the property to check.
         * @param isEqual Comparator function accepting two parameters and returning a boolean.
         *
         * @since 1.9
         */
        vm.propertyChangeByComparator = function(propertyName, isEqual) {

            // Something has happened with the property - an user input! So we will remove any error messages.
            vm.log('vm.propertyChange: deleting error messages for changed property:', propertyName);
            self.deletePreviousMessages(undefined, [ propertyName ]);

            var acceptedValue = vm[ propertyName + 'Accepted' ]();
            var originalValue = vm[ propertyName + 'Original' ]();
            var forcePropertyChanged = vm.hasOwnProperty( propertyName + 'ForcePropertyChanged' ) ? vm[ propertyName + 'ForcePropertyChanged' ]() : false;

            if( ! isEqual( acceptedValue, originalValue ) || forcePropertyChanged ) {
                // If we forced a property change event, we need to reset the flag.
                if ( forcePropertyChanged ) {
                    vm[ propertyName + 'ForcePropertyChanged' ]( false );
                }
                // Values are different, add property name to vm.changedProperties() if it's not already there.
                if (!_.contains(vm.changedProperties(), propertyName)) {
                    vm.log("vm.propertyChange: property " + propertyName + " has changed (for the first time)", acceptedValue);
                    vm.changedProperties.push(propertyName);
                } else {
                    vm.log("vm.propertyChange: property " + propertyName + " is still changed", acceptedValue);
                }
            } else {
                vm.log("vm.propertyChange: property " + propertyName + " is now unchanged", acceptedValue);
                vm.changedProperties.remove(propertyName);
            }

        };


        /**
         * Generic handler for detecting property change (and the need to update).
         *
         * It uses the equality operator to compare "Accepted" and "Original" values.
         * See vm.propertyChangeByComparator() for more information.
         *
         * @param propertyName Name of the property to check.
         *
         * @since 1.9
         */
        vm.propertyChange = _.partial(vm.propertyChangeByComparator, _, function(acceptedValue, originalValue) {
            return (acceptedValue == originalValue);
        });


        /**
         * Process update results after vm.lastUpdateResults is updated.
         *
         * This method is meant to process update results for a particular section: Determine if anything relevant
         * to that section has happened, and if so, show an appropriate message in the right message container.
         *
         * If there are some failed relevant updates, an error message is displayed, and if any of those updates has
         * a custom message, this message overrides the default one.
         *
         * @param propertiesToCheck Array of relevant property names.
         * @param unsavedMessage Generic message to show when any of the relevant updates have failed.
         * @param savedMessage Generic message to show when all of the relevant updates have succeeded.
         * @param messageContainer jQuery selector for the message container.
         * @param updateResults The lastUpdateResults object.
         *
         * @since 1.9
         */
        vm.processUpdateResults = function(propertiesToCheck, unsavedMessage, savedMessage, messageContainer, updateResults) {

            var relevantUpdates = _.intersection(updateResults.propertiesUpdated, propertiesToCheck);
            var haveRelevantUpdates = (relevantUpdates.length > 0);

            if(haveRelevantUpdates) {

                // Filter out relevant failed updates
                var failedUpdates = _.filter(updateResults.failed, function(updateResult) {
                    return _.contains(propertiesToCheck, updateResult.name);
                });
                var haveFailedUpdates = (failedUpdates.length > 0);

                if(haveFailedUpdates) {

                    // Try to find a custom error message.
                    var updateWithErrorMessage = _.find(failedUpdates, function(updateResult) {
                        return updateResult.hasOwnProperty('message');
                    });

                    // Use a custom error message if there is any, or a generic one.
                    var errorMessage = (updateWithErrorMessage != undefined) ? updateWithErrorMessage.message : unsavedMessage;

                    self.showErrorMessage(messageContainer, propertiesToCheck, errorMessage);

                }
            }

        };


        /**
         * Determine whether a section (meaning any property in that section) is being updated right now.
         *
         * @param propertiesToCheck Array of property names in the section.
         * @returns {boolean}
         *
         * @since 1.9
         */
        vm.isSectionUpdating = function(propertiesToCheck) {
            return (_.intersection(vm.updatingProperties(), propertiesToCheck).length > 0);
        };


        /**
         * Determine whether a section (meaning any property in that section) has unsaved changes and needs to update.
         *
         * Needed update will be indicated if there are any unsaved changes and the section is not updating at the moment.
         *
         * @param propertiesToCheck Array of property names in the section
         * @param isSectionUpdatingCheck Method to check whether the section is being updated right now.
         * @returns {boolean}
         *
         * @since 1.9
         */
        vm.isSectionUpdateNeeded = function(propertiesToCheck, isSectionUpdatingCheck) {
            var changedSectionProperties = _.intersection(vm.changedProperties(), propertiesToCheck);
            return ( !isSectionUpdatingCheck() && changedSectionProperties.length > 0 );
        };



        // ------------------------------------------------------------------------
        // Properties for Title and Description section
        // ------------------------------------------------------------------------

        // Properties belonging to the Title and Description section.
        vm.titleSectionProperties = ['title', 'slug', 'description'];

        // title
        vm.titleOriginal = ko.observable(ct_data.title);
        vm.titleOriginal.subscribe(_.partial(vm.propertyChange, 'title' ));

        vm.titleAccepted = ko.observable(ct_data.title);
        vm.titleAccepted.subscribe(_.partial(vm.propertyChange, 'title' ));

        vm.titleLastInput = ko.observable(null);

        vm.title = ko.pureComputed({
            read: function() {
                if(null === vm.titleLastInput()) {
                    return vm.titleAccepted();
                } else {
                    return vm.titleLastInput();
                }
            },
            write: function( newTitle ) {
                vm.titleLastInput( newTitle );
	            vm.titleAccepted( newTitle );
            }
        });


        // slug
        vm.slugOriginal = ko.observable(ct_data.slug);
        vm.slugOriginal.subscribe(_.partial(vm.propertyChange,'slug'));

        vm.slugAccepted = ko.observable(ct_data.slug);
        vm.slugAccepted.subscribe(_.partial(vm.propertyChange,'slug'));


        //noinspection JSUnresolvedVariable
        /**
         * Description property.
         *
         * @since 1.9
         */
        vm.descriptionOriginal = ko.observable(ct_data.description_raw);
        vm.descriptionOriginal.subscribe(_.partial(vm.propertyChange,'description'));

        //noinspection JSUnresolvedVariable
        vm.descriptionAccepted = ko.observable(ct_data.description_raw);
        vm.descriptionAccepted.subscribe(_.partial(vm.propertyChange,'description'));


        /**
         * Determines the visibility of description field.
         *
         * It is displayed either when there is a description or manually.
         *
         * @since 1.9
         */
        vm.isDescriptionVisible = ko.observable(vm.descriptionAccepted().length > 0);


        /**
         * Determines the visibility of "Add description" button.
         *
         * @since 1.9
         */
        vm.showAddDescriptionButton = ko.pureComputed(function() {
            return (0 == vm.descriptionAccepted().length) && !vm.isDescriptionVisible();
        });


        /**
         * Show the description field (and hide the "Add description" button as a consequence).
         *
         * @since 1.9
         */
        vm.showDescriptionField = function() {
            vm.isDescriptionVisible(true);
        };


        /**
         * This will be true when any of the properties from Title section are being updated right now.
         *
         * @since 1.9
         */
        vm.isTitleSectionUpdating = ko.pureComputed(_.partial(vm.isSectionUpdating, vm.titleSectionProperties));


        /**
         * This will be true when any of the properties from the Title section have unsaved changes
         * or when unsecaped title value differs from the accepted one (even if they will be equal after escaping).
         *
         * @since 1.9
         */
        vm.isTitleSectionUpdateNeeded = ko.pureComputed(function() {
            var isUnescapedTitleChanged = ((vm.titleLastInput() != null) && (vm.titleLastInput() != vm.titleAccepted()));
            var isSectionUpdateNeeded = vm.isSectionUpdateNeeded(vm.titleSectionProperties, vm.isTitleSectionUpdating);
            return (isUnescapedTitleChanged || isSectionUpdateNeeded);
        });


        /**
         * Manually update all properties from the Title section.
         *
         * @since 1.9
         * @deprecated 2.7
         */
        vm.titleSectionUpdate = function() {
            vm.log('vm.titleSectionUpdate');
            vm.updateProperties(vm.titleSectionProperties);
        };


        /**
         * Custom Title section method for processing update results after vm.lastUpdateResults is updated.
         *
         * See vm.ProcessUpdateResults. The difference here is that one particular case is handled differently:
         * When both 'title' and 'slug' properties have failed with a specific error code indicating their
         * value is used elsewhere, we will show a different message for both properties at once, so the user
         * can fix both problems and saves a mouse click.
         *
         * @param updateResults The lastUpdateResults object.
         *
         * @since 1.10
         */
        vm.processTitleSectionUpdateResults = function(updateResults) {

            var relevantUpdates = _.intersection(updateResults.propertiesUpdated, vm.titleSectionProperties);
            var haveRelevantUpdates = (relevantUpdates.length > 0);

            if(haveRelevantUpdates) {

                // Filter out relevant failed updates
                var failedUpdates = _.filter(updateResults.failed, function(updateResult) {
                    return _.contains(vm.titleSectionProperties, updateResult.name);
                });
                var haveFailedUpdates = (failedUpdates.length > 0);

                if(haveFailedUpdates) {

                    // Use a custom error message if there is any, or a generic one.
                    //noinspection JSUnresolvedVariable
                    var errorMessage = self.l10n.title_section.unsaved;

                    // Determine whether both title and slug have failed because they're used elsewhere
                    var titleAndSlugUsedUpdates = _.filter(failedUpdates, function(updateResult) {
                        //noinspection JSUnresolvedVariable
                        return (_.contains(['title', 'slug'], updateResult.name)
                            && (updateResult.code == self.l10n.title_section.value_already_used_exception_code));
                    });
                    var areTitleAndSlugUsedElsewhere = (titleAndSlugUsedUpdates.length == 2);

                    if(areTitleAndSlugUsedElsewhere) {
                        // Use custom error message for title+slug already used.
                        //noinspection JSUnresolvedVariable
                        errorMessage = self.l10n.title_section.title_and_slug_used;
                    } else {
                        // Try to find a custom error message.
                        var updateWithErrorMessage = _.find(failedUpdates, function (updateResult) {
                            return updateResult.hasOwnProperty('message');
                        });
                        if (updateWithErrorMessage != undefined) {
                            errorMessage = updateWithErrorMessage.message;
                        }
                    }

                    self.showErrorMessage(self.titleSectionMessageContainer, vm.titleSectionProperties, errorMessage);

                }
            }

        };


        /**
         * Process update results for the Title section.
         *
         * See vm.processUpdateResults() for more information.
         *
         * @since 1.9
         */
        vm.lastUpdateResults.subscribe(vm.processTitleSectionUpdateResults);


        // ------------------------------------------------------------------------
        // Properties for Content Template Settings section
        // ------------------------------------------------------------------------

        // Properties belonging to the Content Template Settings section.
        vm.settingsSectionProperties = ['outputMode'];

        // outputMode
        vm.outputModeOriginal = ko.observable(vm.getPropertyFromModel('outputMode'));
        vm.outputModeOriginal.subscribe(_.partial(vm.propertyChange, 'outputMode' ));

        vm.outputModeAccepted = ko.observable(vm.outputModeOriginal());
        vm.outputModeAccepted.subscribe(_.partial(vm.propertyChange, 'outputMode' ));

        vm.outputModeAccepted.subscribe(function() {
            vm.propertiesToUpdate.push('outputMode');
        });


        vm.isSettingsSectionUpdating = ko.pureComputed(_.partial(vm.isSectionUpdating, vm.settingsSectionProperties));


        //noinspection JSUnresolvedVariable
        vm.lastUpdateResults.subscribe(_.partial(
            vm.processUpdateResults,
            vm.settingsSectionProperties,
            self.l10n.settings_section.unsaved,
            self.l10n.settings_section.saved,
            self.settingsSectionMessageContainer));


        // ------------------------------------------------------------------------
        // Properties for Usage section
        // ------------------------------------------------------------------------


        vm.usageSectionProperties = ['assignedSinglePostTypes', 'assignedPostArchives', 'assignedTaxonomyArchives'];

        // These properies are arrays with post type or taxonomy archive names. Since we're working
        // with arrays here, notice the usage _.toArray() - it is neccessary to clone the array instead
        // of just passing a reference.

        // assignedSinglePostTypes
        vm.assignedSinglePostTypesOriginal = ko.observableArray(_.toArray(vm.getPropertyFromModel('assignedSinglePostTypes')));
        vm.assignedSinglePostTypesOriginal.subscribe(_.partial(vm.propertyChangeByComparator, 'assignedSinglePostTypes', _.isEqual));

        vm.assignedSinglePostTypesAccepted = ko.observableArray(_.toArray(vm.getPropertyFromModel('assignedSinglePostTypes')));
        vm.assignedSinglePostTypesAccepted.subscribe(_.partial(vm.propertyChangeByComparator, 'assignedSinglePostTypes', _.isEqual));


        // assignedPostArchives
        vm.assignedPostArchivesOriginal = ko.observableArray(_.toArray(vm.getPropertyFromModel('assignedPostArchives')));
        vm.assignedPostArchivesOriginal.subscribe(_.partial(vm.propertyChangeByComparator, 'assignedPostArchives', _.isEqual));

        vm.assignedPostArchivesAccepted = ko.observableArray(_.toArray(vm.getPropertyFromModel('assignedPostArchives')));
        vm.assignedPostArchivesAccepted.subscribe(_.partial(vm.propertyChangeByComparator, 'assignedPostArchives', _.isEqual));


        // assignedTaxonomyArchives
        vm.assignedTaxonomyArchivesOriginal = ko.observableArray(_.toArray(vm.getPropertyFromModel('assignedTaxonomyArchives')));
        vm.assignedTaxonomyArchivesOriginal.subscribe(_.partial(vm.propertyChangeByComparator, 'assignedTaxonomyArchives', _.isEqual));

        vm.assignedTaxonomyArchivesAccepted = ko.observableArray(_.toArray(vm.getPropertyFromModel('assignedTaxonomyArchives')));
        vm.assignedTaxonomyArchivesAccepted.subscribe(_.partial(vm.propertyChangeByComparator, 'assignedTaxonomyArchives', _.isEqual));




        /**
         * Information about post types that have different CT assigned.
         *
         * For each assignment type, this contains a property with an observable array of
         * post type or taxonomy names that have a different CT assigned. These observable
         * arrays are used to calculate the visibility of asterisks (see below) and they get
         * updated when user assigns this CT instead.
         *
         * @type {{single_posts: observableArray, cpt_archives: observableArray, taxonomy_archives: observableArray}}
         * @since 1.9
         */
        vm.usageData = {
            single_posts: ko.observableArray(),
            cpt_archives: ko.observableArray(),
            taxonomy_archives: ko.observableArray()
        };


        // Fill vm.usageData if there is the required information.
        if(_.has(section_data, 'usage') && (section_data.usage != undefined)) {
            if(_.has(section_data.usage, 'single_posts' ) ) {
                //noinspection JSCheckFunctionSignatures
                vm.usageData.single_posts.pushAll(section_data.usage.single_posts);
            }
            if(_.has(section_data.usage, 'cpt_archives' ) ) {
                //noinspection JSCheckFunctionSignatures
                vm.usageData.cpt_archives.pushAll(section_data.usage.cpt_archives);
            }
            if(_.has(section_data.usage, 'taxonomy_archives' ) ) {
                //noinspection JSCheckFunctionSignatures
                vm.usageData.taxonomy_archives.pushAll(section_data.usage.taxonomy_archives);
            }
        }



        // dissident post binding

        //noinspection JSUnresolvedVariable
        /**
         * Array with objects that describe dissident posts for each post type.
         *
         * Dissident posts are those, who have different (or none) Content Template assigned, than is
         * default for their post type.
         *
         * This array contains objects with following attributes:
         * - postType: Post type slug.
         * - posts: An array of post IDs that are dissident..
         * - labelSingular: Singular display name (e.g. Page) of the post type.
         * - labelPlural: Plural display name (e.g. Pages) of the post type.
         *
         * CT edit page offers to bind dissident posts (to assign this CT to them), when this CT is assigned
         * to that type (this option becomes available after saving changes to database).
         *
         * @since 1.9
         */
        vm.postTypesWithDissidentPosts = ko.observableArray(_.map(ct_data.dissident_posts, function(value, key) {
            //noinspection JSUnresolvedVariable
            return {
                postType: key,
                posts: value,
                labelSingular: ct_data.usage_post_type_labels[key].singular,
                labelPlural: ct_data.usage_post_type_labels[key].plural
            };
        }));


        /**
         * Array of post type names whose dissident posts are being bound (from the displaying of confirmation
         * dialog until the action is finished).
         *
         * This is being used to determine spinner visibility, button availability, etc.
         *
         * @since 1.9
         */
        vm.postTypesWithDissidentPostsBeingBound = ko.observableArray();


        /**
         * Action when user click on the "Bind posts" button.
         *
         * It updates the "post types being bound" (see above) and displays a confirmation dialog.
         * If user confirms the action, it continues with vm.finishBindingDissidentPosts,
         * otherwise with vm.cancelBindingDissidentPosts.
         *
         * @param postType Post type slug whose dissident posts should be bound.
         *
         * @since 1.9
         */
        vm.bindDissidentPosts = function(postType) {
            var postTypeInfo = _.findWhere(vm.postTypesWithDissidentPosts(), {postType: postType});
            vm.log('vm.bindDissidentPosts', postTypeInfo);
            vm.postTypesWithDissidentPostsBeingBound.push(postType);
            //noinspection JSUnresolvedVariable
            self.showBindDissidentPostsDialog(postTypeInfo, ct_data.usage_bind_dialog_template);
        };


        /**
         * After confirmation by user, do the actual post binding.
         *
         * If the binding is successful, also update the vm.postTypesWithDissidentPosts array.
         *
         * @param postTypeInfo {{postType: string, posts: Array}}
         * @since 1.9
         */
        vm.finishBindingDissidentPosts = function(postTypeInfo) {
            self.bindPosts(postTypeInfo.posts, function(success, result) {
                if(success) {
                    vm.postTypesWithDissidentPosts.remove(postTypeInfo);
                }
                vm.postTypesWithDissidentPostsBeingBound.remove(postTypeInfo.postType);
                vm.log('vm.finishBindingDissidentPosts', success, result);
            });
        };


        /**
         * Action after user cancelled dissident post binding.
         *
         * Only remove the post type from "post types being bound" array.
         *
         * @param postTypeInfo {{postType: string, posts: Array}}
         * @since 1.9
         */
        vm.cancelBindingDissidentPosts = function(postTypeInfo) {
            vm.log('vm.cancelBindingDissidentPosts', postTypeInfo.postType);
            vm.postTypesWithDissidentPostsBeingBound.remove(postTypeInfo.postType);
        };


        /**
         * Determine if a "Bind posts" button should be visible for given post type.
         *
         * 1. the post type must have dissident posts
         * 2. CT must be curently assigned to this post type (stored in database)
         * 3. CT must be assigned to this post type also on the page (current unsaved value)
         *
         * @param postType Post type name.
         * @returns {boolean}
         * @since 1.9
         */
        vm.isBindButtonVisible = function(postType) {
            var dissidentPostsExist = (typeof(_.findWhere(vm.postTypesWithDissidentPosts(), {postType: postType})) != 'undefined');
            var postTypeAssigned = _.contains(vm.assignedSinglePostTypesOriginal(), postType);
            var postTypeSelected = _.contains(vm.assignedSinglePostTypesAccepted(), postType);
            var isVisible =  dissidentPostsExist && postTypeSelected && postTypeAssigned;
            vm.log('vm.isBindButtonVisible(' + postType + ')', isVisible);
            return isVisible;
        };


        /**
         * Determine if a "Bind posts" button should be enabled for given post type.
         *
         * 1. it must be visible
         * 2. given post type must not be in a process of binding
         *
         * @param postType Post type name.
         * @returns {boolean}
         * @since 1.9
         */
        vm.isBindButtonEnabled = function(postType) {
            var isEnabled = vm.isBindButtonVisible(postType) && !_.contains(vm.postTypesWithDissidentPostsBeingBound(), postType);
            vm.log('vm.isBindButtonEnabled(' + postType + ')', isEnabled);
            return isEnabled;
        };

		/**
		 * Returns the number of Dissident posts the given post type.
		 *
		 * @param postType
		 * @returns {number}
		 */
		vm.dissidentPostsCountForPostType = function( postType ) {
			var dissidentPostsForPostType = _.find( vm.postTypesWithDissidentPosts(), function( postTypeWithDissidentPosts ) {
				return postTypeWithDissidentPosts.postType === postType;
			});
			return ! _.isUndefined( dissidentPostsForPostType ) ? dissidentPostsForPostType.posts.length : 0;
		};

        vm.isUsageSectionUpdating = ko.pureComputed(_.partial(vm.isSectionUpdating, vm.usageSectionProperties));

        vm.isUsageSectionUpdateNeeded = ko.pureComputed(_.partial(vm.isSectionUpdateNeeded, vm.usageSectionProperties, vm.isUsageSectionUpdating));

        vm.usageSectionUpdate = function() {
            vm.log('vm.usageSectionUpdate');
            vm.updateProperties(vm.usageSectionProperties);
        };

        //noinspection JSUnresolvedVariable
        vm.lastUpdateResults.subscribe(_.partial(
            vm.processUpdateResults,
            vm.usageSectionProperties,
            self.l10n.usage_section.unsaved,
            self.l10n.usage_section.saved,
            self.usageSectionMessageContainer));

        /**
		 * Updates the ViewModel for the Usage section to display the "Bind posts" button with the proper number of posts.
         *
		 * @param postTypesWithDissidentPosts
		 * @param updateResults
		 */
		vm.notifyBindDissidentPosts = function( postTypesWithDissidentPosts, updateResults ) {
			 _.each( updateResults.succeeded, function( property ) {
				if (
					_.has( property, 'dissidentPosts' ) &&
					_.isObject( property.dissidentPosts )
				) {
					if ( Object.keys( property.dissidentPosts ).length < postTypesWithDissidentPosts().length ) {
						// This case updates the ViewModel when post type is removed from the observable array.
						var postTypesToRemove = [];
						_.each(
							postTypesWithDissidentPosts(),
							function( postTypeObject ) {
								if ( ! _.has( property.dissidentPosts, postTypeObject.postType ) ) {
									postTypesToRemove.push( postTypeObject );
								}
							}
						);

						_.each(
							postTypesToRemove,
							function( postTypeObject ) {
								postTypesWithDissidentPosts.remove( postTypeObject );
							}
						);
					} else {
						// This case updates the ViewModel when post type is added to the observable array.
						_.each(
							property.dissidentPosts,
							function( dissidentPosts, postType ) {
								var postTypeObject = _.find(
									postTypesWithDissidentPosts(),
									function ( postTypeObj ) {
										return postTypeObj.postType === postType;
									}
								);

								if ( _.isUndefined( postTypeObject ) ) {
									var newPostTypeObject = {
										postType: postType,
										posts: dissidentPosts,
										labelSingular: ct_data.usage_post_type_labels[ postType ].singular,
										labelPlural: ct_data.usage_post_type_labels[ postType ].plural
									};
									postTypesWithDissidentPosts.push( newPostTypeObject );
								}
							}
						);
					}
				}
			});
		};

	    vm.lastUpdateResults.subscribe(
	    	_.partial(
	    		vm.notifyBindDissidentPosts,
			    vm.postTypesWithDissidentPosts
		    )
	    );


        /**
         * Determines if an asterisk, indicating another CT is already assigned, should be displayed.
         *
         * @param assignmentType Index into custom section data with information about other CTs being assigned.
         * @param vmArrayToCompare {Array} ViewModel property name with currently selected items
         *     for given assignmentType.
         * @param value Actual value for which this asterisk would be displayed.
         * @returns {boolean} True if the asterisk should be visible.
         * @since 1.9
         */
        vm.isAsteriskVisible = function(assignmentType, vmArrayToCompare, value) {
            var otherAssignmentExists = _.contains(vm.usageData[ assignmentType ](), value);
            var valueSelected = _.contains(vm[ vmArrayToCompare ](), value);
            return (otherAssignmentExists && valueSelected);
        };


        /**
         * Determines if an explanation for an asterisk should be displayed for given assignment type.
         *
         * True if at least one asterisk was displayed.
         *
         * @param vmArrayToCompare {observableArray} ViewModel property with currently selected items
         *     for given assignmentType.
         * @param assignmentType Index into custom section data with information about other CTs being assigned.
         * @returns {boolean} True if the asterisk explanation should be visible.
         * @since 1.9
         */
        vm.isAsteriskExplanationVisible = function(vmArrayToCompare, assignmentType) {
            return _.intersection(vm[ vmArrayToCompare ](), vm.usageData[ assignmentType ]()).length > 0;
        };


        /**
         * Update information about other CTs being assigned to post types or taxonomies.
         *
         * Updates the appropriate observableArray in vm.usageData.
         *
         * @param {string} assignmentType One of the three assignment types.
         * @param {Array} vmArrayToCompare Array of post type or taxonomy names that where this
         *     CT has been assigned.
         * @since 1.9
         */
        vm.updateOtherAssignments = function(assignmentType, vmArrayToCompare) {
            vm.usageData[assignmentType].removeAll(vmArrayToCompare);
        };


        // Subscribe to update other assignments after changes have been saved to database.
        vm.assignedSinglePostTypesOriginal.subscribe(_.partial(vm.updateOtherAssignments, 'single_posts'));
        vm.assignedPostArchivesOriginal.subscribe(_.partial(vm.updateOtherAssignments, 'cpt_archives'));
        vm.assignedTaxonomyArchivesOriginal.subscribe(_.partial(vm.updateOtherAssignments, 'taxonomy_archives'));


        // ------------------------------------------------------------------------
        // Properties for Content section
        // ------------------------------------------------------------------------


        vm.contentSectionProperties = ['postContent', 'templateCss', 'templateJs'];


        // postContent
        vm.postContentOriginal = ko.observable(vm.getPropertyFromModel('postContent'));
        vm.postContentOriginal.subscribe(_.partial(vm.propertyChange,'postContent'));

        vm.postContentAccepted = ko.observable(vm.getPropertyFromModel('postContent'));
        vm.postContentAccepted.subscribe(_.partial(vm.propertyChange,'postContent'));


        // templateCss
        vm.templateCssOriginal = ko.observable(vm.getPropertyFromModel('templateCss'));
        vm.templateCssOriginal.subscribe(_.partial(vm.propertyChange,'templateCss'));

        vm.templateCssAccepted = ko.observable(vm.getPropertyFromModel('templateCss'));
        vm.templateCssAccepted.subscribe(_.partial(vm.propertyChange,'templateCss'));


        /**
         * Determines whether CSS editor is expanded (visible).
         *
         * Used for determining other elements' visibility.
         *
         * @since 1.9
         */
        vm.isCssEditorExpanded = ko.observable(false);


        /**
         * Determines whether the "Pin" icon should be visible for a CSS editor.
         *
         * It is displayed when editor is not displaying but there is some CSS code.
         *
         * @since 1.9
         */
        vm.isCssPinVisible = ko.pureComputed(function() {
            return ( !vm.isCssEditorExpanded() && vm.templateCssAccepted().trim() != '' );
        });


        /**
         * Action to show or hide CSS editor.
         *
         * @since 1.9
         */
        vm.toggleCssEditor = function() {
            vm.isCssEditorExpanded(!vm.isCssEditorExpanded());
        };


        // templateJs
        vm.templateJsOriginal = ko.observable(vm.getPropertyFromModel('templateJs'));
        vm.templateJsOriginal.subscribe(_.partial(vm.propertyChange,'templateJs'));

        vm.templateJsAccepted = ko.observable(vm.getPropertyFromModel('templateJs'));
        vm.templateJsAccepted.subscribe(_.partial(vm.propertyChange,'templateJs'));


        vm.isJsEditorExpanded = ko.observable(false);


        vm.isJsPinVisible = ko.pureComputed(function() {
            return ( !vm.isJsEditorExpanded() && vm.templateJsAccepted().trim() != '' );
        });


        vm.toggleJsEditor = function() {
            vm.isJsEditorExpanded(!vm.isJsEditorExpanded());
        };


        vm.isContentSectionUpdating = ko.pureComputed(_.partial(vm.isSectionUpdating, vm.contentSectionProperties));

        vm.isContentSectionUpdateNeeded = ko.pureComputed(_.partial(vm.isSectionUpdateNeeded, vm.contentSectionProperties, vm.isContentSectionUpdating));

        vm.contentSectionUpdate = function() {
            vm.log('vm.contentSectionUpdate');
            vm.updateProperties(vm.contentSectionProperties);
        };

        //noinspection JSUnresolvedVariable
        vm.lastUpdateResults.subscribe(_.partial(
            vm.processUpdateResults,
            vm.contentSectionProperties,
            self.l10n.content_section.unsaved,
            self.l10n.content_section.saved,
            self.contentSectionMessageContainer));


        // ------------------------------------------------------------------------
        // "Save the Content Template"
        // ------------------------------------------------------------------------


        /**
         * "Summary" of existing sections.
         *
         * @type {{title: {properties: Array, messageContainer: string, isUpdateNeeded: *}, settings: {properties: Array, messageContainer: string, isUpdateNeeded: Function}, usage: {properties: Array, messageContainer: Array, isUpdateNeeded: *}, content: {properties: Array, messageContainer: string, isUpdateNeeded: *}}}
         * @since 1.10
         */
        vm.sections = {
            title: {
                properties: vm.titleSectionProperties,
                messageContainer: vm.titleSectionMessageContainer,
				isUpdateNeeded: vm.isTitleSectionUpdateNeeded,
				isUpdating: vm.isTitleSectionUpdating
            },
            settings: {
                properties: vm.settingsSectionProperties,
                messageContainer: vm.settingsSectionMessageContainer,
				isUpdateNeeded: function() { return false },
				isUpdating: vm.isSettingsSectionUpdating
            },
            usage: {
                properties: vm.usageSectionProperties,
                messageContainer: vm.usageSectionProperties,
				isUpdateNeeded: vm.isUsageSectionUpdateNeeded,
				isUpdating: vm.isUsageSectionUpdating
            },
            content: {
                properties: vm.contentSectionProperties,
                messageContainer: vm.contentSectionMessageContainer,
				isUpdateNeeded: vm.isContentSectionUpdateNeeded,
				isUpdating: vm.isContentSectionUpdating
            }
        };


        /**
         * Determine if there are any unsaved properties.
         *
         * Depends on the per-section checking functions.
         *
         * @since 1.10
         */
        vm.isAnyUpdateNeeded = ko.pureComputed(function() {
            return _.any(vm.sections, function(section) { return section.isUpdateNeeded(); });
		});

        /**
         * Determine if there are any saving properties.
         *
         * Depends on the per-section checking functions.
         *
         * @since 2.7.3
         */
        vm.isAnySectionUpdating = ko.pureComputed(function() {
            return _.any(vm.sections, function(section) { return section.isUpdating(); });
        });


        /**
         * Determine "Save all changes at once" button visibility.
         *
         * It will be visible if any property has been changed and isn't being updated yet.
         *
         * @since 1.9
         */
        vm.isSaveAllButtonEnabled = ko.pureComputed(function() {
            return vm.isAnyUpdateNeeded();
        });


        /**
         * Manually save all changed properties at once.
         *
         * @since 1.9
         */
        vm.saveAllProperties = function() {
            vm.updateProperties(_.toArray(vm.changedProperties()));
        };


        /**
         * Process update results.
         *
         * Show a generic error/success message on the action bar.
         *
         * @since 1.9
         */
        vm.lastUpdateResults.subscribe(function(updateResults) {
            var haveFailedUpdates = (updateResults.failed.length > 0);
            var messageContainerSelector = self.action_bar_message_container.selector;
            if(haveFailedUpdates) {
                // This message is about all the properties that have failed (and it will be removed if any of those
                // gets an input from user)
                var failedPropertyNames = _.pluck(updateResults.failed, 'name');
                //noinspection JSUnresolvedVariable
                self.showErrorMessage(messageContainerSelector, failedPropertyNames, self.l10n.editor.unsaved);
                self.highlight_action_bar('failure');
            } else {
                self.showSuccessMessage(messageContainerSelector, self.l10n.editor.saved);
                self.highlight_action_bar('success');
                jQuery( document ).trigger('ct_saved');
            }
        });


        // ------------------------------------------------------------------------
        // Trash button bindings
        // ------------------------------------------------------------------------


        /**
         * Initiate trashing of this CT.
         * @since 1.10
         */
        vm.trashAction = function() {
            self.trashAction();
        };


        /**
         * Indicates that the CT is in the process of trashing.
         * @since 1.10
         */
        vm.isTrashing = ko.observable(false);


        // ------------------------------------------------------------------------
        // Initialize the ViewModel
        // ------------------------------------------------------------------------


        // Now the magic happens)
        ko.applyBindings(vm);

    };


    // ----------------------------------------------------------------------------
    // Helper functions, knockout modifications, etc.
    // ----------------------------------------------------------------------------


    /**
     * Collection of previous messages that keep being displayed.
     *
     * Each message should be removed when
     * - a new one is being displayed in the same place.
     * - related property gets user input (note that it's not the same as if it was changed in the usual sense)
     *
     * Contains object with three properties:
     * - selector: CSS selector of the message container.
     * - propertyNames: Array of property names this message is about.
     * - messageObject: the wpvToolsetMessage object used to hide the message.
     *
     * @type {Array<{selector: {string}, propertyNames: [], messageObject: {}}>}
     * @since 1.9
     */
    self.previousMessages = [];


    /**
     * Delete previously displayed messages based on their selector and/or related property names.
     *
     * @param {string|undefined} selector All messages with this selector will be removed. Can be undefined, in which case
     *     this check will be skipped.
     * @param {[]|undefined} propertyNames Messages who are related to at least one of these properties will be removed.
     *     Can be undefined, in which case this check will be skipped.
     *
     * @since 1.10
     */
    self.deletePreviousMessages = function(selector, propertyNames) {

        var messagesToDelete = [];

        // Collect messages that will be deleted.
        if(typeof(selector) != 'undefined') {
            messagesToDelete = _.where(self.previousMessages, {selector: selector});
        }

        if(typeof(propertyNames) != 'undefined') {
            var messagesByPropertyNames = _.filter(self.previousMessages, function(msg) {
                return (_.intersection(msg.propertyNames, propertyNames).length > 0);
            });
            messagesToDelete = messagesToDelete.concat(messagesByPropertyNames);
        }

        // Delete collected messages.
        _.each(messagesToDelete, function(messageToDelete) {
            if(_.has(messageToDelete.messageObject, 'wpvMessageRemove')) {
                messageToDelete.messageObject.wpvMessageRemove();
                self.previousMessages = _.without(self.previousMessages, messageToDelete);
            }
        });

    };


    /**
     * Display a standard Toolset message.
     *
     * Also deletes previously displayed messages with the same selector.
     *
     * @param type {string} 'success' or 'error'
     * @param stay {boolean} Determines whether the message will fade out or stays displayed.
     * @param fadeOut {int} How long should the fadeOut message last when removing the message.
     * @param selector {string} CSS selector for the message container.
     * @param text {string} Text of the message.
     *
     * @return {*} The message object.
     *
     * @since 1.9
     */
    self.showMessage = function(type, stay, fadeOut, selector, text) {

        self.deletePreviousMessages(selector, undefined);

        return $(selector).wpvToolsetMessage({
            text: text,
            type: type,
            inline: false,
            stay: stay,
            fadeOut: fadeOut
        });
    };


    /**
     * Display a success message.
     *
     * @param selector {string} CSS selector for the message container.
     * @param text {string} Text of the message.
     *
     * @since 1.9
     */
    self.showSuccessMessage = _.partial(self.showMessage, 'success', false, 2000);


    /**
     * Display an error message.
     *
     * Store the message object in self.previousMessages so that it will be removed at the right time.
     *
     * @param selector {string} CSS selector for the message container.
     * @param propertyNames {[]} Names of properties that this message relates to.
     * @param text {string} Text of the message.
     *
     * @since 1.9
     */
    self.showErrorMessage = function(selector, propertyNames, text) {
        var message = {
            selector: selector,
            propertyNames: propertyNames,
            messageObject: self.showMessage('error', true, 0, selector, text)
        };
        // Note that we first have to create the message and then we can push to
        // self.previousMessages, because this array is being overwritten in
        // self.showMessage().
        self.previousMessages.push(message);
    };


    /**
     * Extension of the ko.observableArray() for pushing of array of items at once.
     *
     * See https://github.com/knockout/knockout/pull/845.
     *
     * @param valuesToPush {Array} Array of values to push into the observableArray
     * @returns {ko.observableArray.fn}
     *
     * @since 1.9
     */
    ko.observableArray.fn.pushAll = function(valuesToPush) {
        var items = this;
        for (var i = 0, j = valuesToPush.length; i < j; i++) {
            items.push(valuesToPush[i]);
        }
        //noinspection JSValidateTypes
        return items;
    };


    /**
     * Creates a custom Knockout binding that makes elements shown/hidden via custom method applyEfect
     *
     * @param applyEffect {Function} Function that takes the element and value and applies it in some custom way.
     * @returns {{init: Function, update: Function}} Knockout binding handler.
     *
     * @since 1.9
     */
    self.knockoutEffectBinding = function(applyEffect) {
        return {
            init: function(element, valueAccessor) {
                // Initially set the element to be instantly visible/hidden depending on the value
                var value = valueAccessor();
                // Use "unwrapObservable" so we can handle values that may or may not be observable
                $(element).toggle(ko.unwrap(value));
            },
            update: function(element, valueAccessor) {
                // Whenever the value subsequently changes, slowly fade the element in or out
                var value = valueAccessor();
                applyEffect(element, ko.unwrap(value));
            }
        }
    };


    /**
     * Custom knockout binding for hiding and displaying CodeMirror editors.
     *
     * The target element needs to contain a data-target-editor with an editor slug that's present in self.editors.
     * It slides down/up and it refreshes the CodeMirror instance after the first slideDown.
     *
     * @type {{init: Function, update: Function}}
     *
     * @since 1.9
     */
    ko.bindingHandlers.editorVisible = self.knockoutEffectBinding(function(element, show) {
        var target = $(element);
        if(true == show) {
            target.slideDown(200);

            var editor_slug = target.data('target-editor');
            var editor_info = _.findWhere(self.editors, {slug: editor_slug});
            if( !editor_info.hasOwnProperty('was_refreshed') || !editor_info.was_refreshed ) {
                self.vm.log('ko.bindingHandlers.editorVisible: refreshing', editor_slug);
                self['codemirror_' + editor_slug].refresh();
                editor_info.was_refreshed = true;
            }

        } else {
            target.slideUp(200);
        }
    });


    /**
     * Custom knockout binding for changing element visibility with animating it's width.
     *
     * @type {{init: Function, update: Function}}
     *
     * @since 1.9
     */
    ko.bindingHandlers.widthToggleVisible = self.knockoutEffectBinding(function(element, show) {
        var target = $(element);
        if(true == show) {
            target.animate({width:'show'}, 200);
        } else {
            target.animate({width:'hide'}, 200);
        }
    });


    /**
     * Custom knockout binding for changing element visibility with sliding up/down.
     *
     * @type {{init: Function, update: Function}}
     *
     * @since 1.9
     */
    ko.bindingHandlers.slideToggleVisible = self.knockoutEffectBinding(function(element, show) {
        var target = $(element);
        if(true == show) {
            target.slideDown(200);
        } else {
            target.slideUp(200);
        }
    });


    /**
     * Custom knockout binding for changing element visibility with fading.
     *
     * @type {{init: Function, update: Function}}
     *
     * @since 1.9
     */
    ko.bindingHandlers.fadeVisibility = {
        init: function(element, valueAccessor) {
            // Initially set the element to be instantly visible/hidden depending on the value
            var show = ko.unwrap(valueAccessor());
            var visibility = show ? 'visible' : 'hidden';
            $(element).css('visibility', visibility);
        },
        update: function(element, valueAccessor) {
            // Whenever the value subsequently changes, slowly fade the element in or out
            var show = ko.unwrap(valueAccessor());
            var isDisplayed = ($(element).css('display') != 'none');

            if(show && !isDisplayed) {
                $(element).css('visibility', 'visible').hide().fadeIn('slow');
            } else if(!show && isDisplayed) {
                $(element).css('visibility', 'hide').show().fadeOut('slow');
            }
        }
    };


    /**
     * Custom knockout binding for changing spinner visibility by adding or removing the "is-active" class.
     *
     * @type {{init: Function, update: Function}}
     *
     * @since 1.9
     */
    ko.bindingHandlers.spinnerActive = {
        update: function(element, valueAccessor) {
            var value = ko.utils.unwrapObservable(valueAccessor());
            if(true == value) {
                $(element).addClass('is-active');
            } else {
                $(element).removeClass('is-active');
            }
        }
    };


    // ----------------------------------------------------------------------------
    // Interaction with the server
    // ----------------------------------------------------------------------------

    /* All AJAX calls are defined here. Currently we have only two:
     * - wpv_ct_update_properties for updating a set of properties
     * - wpv_ct_bind_posts for binding posts to this Content Template
     */

    /**
     * Nonce for updating CT properties.
     * @type string
     */
    self.update_nonce = null;


    /**
     * Nonce for bulk trashing CTs.
     * @type {string}
     */
    self.trash_nonce = null;



    /**
     * Ensure that response is always an object with the success property.
     *
     * If it's not, return a dummy object indicating a failure.
     *
     * @param response {*} Response from the AJAX call.
     * @returns {{success: boolean}} Sanitized response.
     *
     * @since 1.9
     */
    self.parseResponse = function(response) {
        if( typeof(response.success) === 'undefined' ) {
            self.log("self.parseResponse: no success", response);
            return { success: false };
        } else {
            return response;
        }
    };


    /**
     * Update Content Template properties on the server.
     *
     * Execute a wpv_ct_update_properties AJAX call.
     *
     * @param ct_data Array of objects holding subset of CT data ("name" with property Model name and "value").
     * @param {function} callback Function that will be called after AJAX action finished. It should
     *     accept one argument: Array of update results or false if the AJAX call has failed entirely.
     *
     * @since 1.9
     */
    self.updateProperties = function(ct_data, callback) {

        var data = {
            action: self.l10n.editor.ajax.action.update_content_template_properties,
            id: self.ct_data.id,
            wpnonce: self.l10n.editor.ajax.nonce.update_content_template_properties,
            properties: ct_data
        };

        var ret = false;

        self.vm.log('self.updateProperties', data);

        $.ajax({
            async: true,
            type: 'POST',
            url: ajaxurl,
            data: data,
            success: function( originalResponse ) {
                var response = self.parseResponse(originalResponse);
                if(response.success) {

                    // We will be returning update results.
                    ret = response.data.results;
                    self.vm.log('wpv_ct_update_properties response:', JSON.stringify(originalResponse));

                    callback(ret);

                } else {
                    console.log('Error:', originalResponse);
                    ret = false;

                    callback(ret);
                }
            },
            error: function( ajaxContext ) {
                console.log('Error:', ajaxContext.responseText);
                ret = false;

                callback(ret);
            }
        });
    };


    /**
     * Bind posts to the current Content Template.
     *
     * @param postsToBind Array of post IDs to bind.
     * @param callback Function taking two arguments, success and response data.
     *
     * @since 1.9
     */
    self.bindPosts = function(postsToBind, callback) {

        //noinspection JSUnresolvedVariable
        var data = {
            action: 'wpv_ct_bind_posts',
            id: self.ct_data.id,
            wpnonce: self.ct_data.usage_bind_nonce,
            posts_to_bind: postsToBind
        };

        $.ajax({
            type: 'POST',
            url: ajaxurl,
            data: data,
            success: function( originalResponse ) {
                var response = self.parseResponse(originalResponse);
                if(!response.success) {
                    console.log('Error:', originalResponse);
                }
                self.vm.log('wpv_ct_bind_posts response:', JSON.stringify(originalResponse));

                callback(response.success, response.data);
            },
            error: function( ajaxContext ) {
                console.log('Error:', ajaxContext.responseText);

                callback(false, ajaxContext.responseText);
            }
        });
    };


    /**
     * Instance of the Trash dialog object.
     * @type {null|WPViews.CTDialogs.TrashContentTemplatesDialog}
     * @since 1.10
     */
    self.trashDialog = null;


    /**
     * Initialize and setup the Trash dialog object and execute the trashing action.
     *
     * Note that no dialog might actually display, depending on what assignment this CT has.
     * After successful trashing redirects to CT listing page.
     *
     * @since 1.10
     */
    self.trashAction = function() {

        self.vm.isTrashing(true);

        // Callbacks
        var afterTrashing = function() {
            self.log('self.trashAction succeeded');

            // CTs have been trashed. Redirect to CT listing page and show a message.
            //noinspection JSUnresolvedVariable
            var uri = new URI(self.ct_data.listing_page_url);
            window.location.href = uri.addQuery('trashed', '1').addQuery('affected', self.vm.id).toString();
        };

        var onCancel = function() {
            self.log('self.trashAction cancelled');
            self.vm.isTrashing(false);
        };

        // Initialize the dialog object if it wasn't done already
        if(null == self.trashDialog) {
            self.trashDialog = new WPViews.ct_dialogs.TrashContentTemplatesDialog(self.trash_nonce, afterTrashing, onCancel, onCancel);
        }

        self.trashDialog.trashContentTemplates([ self.vm.id ]);
    };


    // ----------------------------------------------------------------------------
    // Tooltips, formatting instructions and pointers
    // ----------------------------------------------------------------------------


    /**
     * Show or hide "Formatting help" section.
     *
     * Expects the "toggle" element to contain a data-target attribute with class name of the element that
     * should actually be toggled. Also changes the caret icon (up/down).
     *
     * @param toggle jQuery wrapper of the toggle element.
     *
     * @since 1.9
     */
    self.show_hide_formatting_help = function( toggle ) {
        $( '.' + toggle.data( 'target' ) ).slideToggle( 400, function() {
            toggle.find( '.js-wpv-toggle-toggler-icon i' ).toggleClass( 'icon-caret-down icon-caret-up fa-caret-down fa-caret-up' );
        });
    };


    /**
     * Show or hide formatting instructions for an editor.
     *
     * @since 1.9
     */
    $( document ).on( 'click', '.js-wpv-editor-instructions-toggle', function() {
        var toggle = $( this );
        self.show_hide_formatting_help( toggle );
    });


    /**
     * Show a pointer when js-wpv-show-pointer is clicked.
     *
     * The target (clicked element) must contain following data attributes:
     * - section: Slug of the section
     * - pointer-slug: Slug of the pointer
     *
     * Those two values will be used to access pointer data in the l10n object.
     *
     * self.l10n.section.pointer_slug must contain two attributes:
     * - title: A string with pointer title
     * - paragraphs: An array of strings for pointer body, each of them will be wrapped in a p tag.
     *
     * @since 1.9
     */
    $( document ).on( 'click', '.js-wpv-show-pointer', function() {
        var target = $(this);

        $('.wp-pointer').fadeOut(100);

        var pointer_data = self.l10n[ target.data('section') ][ target.data('pointer-slug') ];

        var title_html = '<h3>' + pointer_data.title + '</h3>';

        //noinspection JSUnresolvedVariable
        var content_html = _.reduce(pointer_data.paragraphs, function(memo, paragraph) {
            return memo + '<p>' + paragraph + '</p>';
        }, '');

        //noinspection JSUnresolvedVariable
        var close_button_html = '<button class="button button-primary-toolset alignright js-wpv-close-this">' + self.l10n.editor.pointer_close + '</button>';

        target.pointer({
            pointerClass: 'wp-toolset-pointer wp-toolset-views-pointer',
            content: title_html + content_html,
            position: {
                // Pass this through pointer_data if different values are required in the future.
                edge: 'left',
                align: 'right',
                offset: '-5 0'
            },
            buttons: function( event, t ) {
                var button_close = $(close_button_html);
                button_close.bind( 'click.pointer', function( e ) {
                    e.preventDefault();
                    t.element.pointer('close');
                });
                return button_close;
            }
        }).pointer( 'open' );
    });


    // ----------------------------------------------------------------------------
    // Codemirror stuff
    // ----------------------------------------------------------------------------

    /* Code for initializing and maintaining CodeMirror editors on the page. Each editor
     * should be described in the self.editors array.
     *
     * self.init_editors() should be called at the beginning of the initialization and
     * self.fill_editors() after knockout bindings are applied.
     */


    /**
     * Wrapper function for updating the Content section.
     *
     * It needs to be implemented this way because we use this function before self.vm is initialized (but never
     * call it too early).
     *
     * @since 1.12
     */
    self.updateContentSectionEditors = function() {
        self.vm.contentSectionUpdate();
    };


    /**
     * Array with description of CM editors on the page.
     *
     * Each editor description should have these attributes:
     * - slug: CM editor slug. It will be used to store the CM instance in self.codemirror_{$slug} and
     *   if quicktags are allowed, it's instance in self.codemirror_{$slug}_quicktags.
     * - selector: id attribute of the underlying textarea.
     * - allow_quicktags: boolean, if true, quicktags will be added to the editor (basic HTML ones, hardcoded below).
     * - propertyToUpdate: Full name of the VM property (incl. the 'Accepted' suffix) that should be kept in sync
     *   with editor content.
     * - propertyName: Name of the VM property (without the suffix).
     * - manualUpdateHandler: Function that will be called when a manual update is triggered by CodeMirror (currently,
     *   that means user pressing Ctrl+S or Cmd+S). It gets propertyName as first parameter.
     * - mode: Editor mode (for syntax highlighting). Allowed values depend on CM.
     *
     * @since 1.9
     */
    self.editors = [
        {
            slug: 'content',
            selector: 'wpv_content',
            allow_quicktags: true,
            propertyToUpdate: 'postContentAccepted',
            propertyName: 'postContent',
            manualUpdateHandler: self.updateContentSectionEditors,
            mode: undefined
        },
        {
            slug: 'css',
            selector: 'wpv_template_extra_css',
            allow_quicktags: false,
            propertyToUpdate: 'templateCssAccepted',
            propertyName: 'templateCss',
            manualUpdateHandler: self.updateContentSectionEditors,
            mode: 'css'
        },
        {
            slug: 'js',
            selector: 'wpv_template_extra_js',
            allow_quicktags: false,
            propertyToUpdate: 'templateJsAccepted',
            propertyName: 'templateJs',
            manualUpdateHandler: self.updateContentSectionEditors,
            mode: 'javascript'
        }
    ];


    /**
     * Initialize CodeMirror editors.
     *
     * Initialize CM editors based on their definition in self.editors. Optionally add quicktags. Store the CM
     * instance in self.codemirror_{$slug} and also in WPV_Toolset.CodeMirror_instance[$selector].
     *
     * This should be called at the beginning of the initialization.
     *
     * @since 1.9.0
	 * @since 2.3.0 Added a keymap shortcut to edit Views shortcodes on Codemirror editors tha allow Quicktags,
	 *     hence leaving our CSS and JS editors.
     */
    self.init_editors = function() {
        _.each(self.editors, function(editor_info) {
            var editor_slug = 'codemirror_' + editor_info.slug;

            // create the CodeMirror instance
            self[editor_slug] = icl_editor.codemirror(editor_info.selector, true, editor_info.mode);

            // save it also in WPV_Toolset.CodeMirror_instance
            WPV_Toolset.CodeMirror_instance[editor_info.selector] = self[editor_slug];

            // Optionally add quicktags
            if(editor_info.allow_quicktags) {
                var quicktags_slug = editor_slug + '_quicktags';
                self[quicktags_slug] = quicktags( { id: editor_info.selector, buttons: 'strong,em,link,block,del,ins,img,ul,ol,li,code,close' } );
                WPV_Toolset.add_qt_editor_buttons( self[quicktags_slug],self[editor_slug] );
				WPV_Toolset.CodeMirror_instance[ editor_info.selector ].addKeyMap( self.editor_keymap );
                Toolset.hooks.doAction( 'toolset_text_editor_CodeMirror_init', editor_info.selector );
            }
        });

        self.codemirror_apply_autoresize_options();

        self.init_codemirror_autosave();
    };


    /**
     * Apply autoresize options to the CodeMirror editors.
     *
     * @since 1.10
     */
    self.codemirror_apply_autoresize_options = function() {
        //noinspection JSUnresolvedVariable
        if (self.l10n.content_section.codemirror_autoresize == 'true'
            || self.l10n.content_section.codemirror_autoresize == '1')
        {
            $( '.CodeMirror' ).css( 'height', 'auto' );
            $( '.CodeMirror-scroll' ).css( {'overflow-y':'hidden', 'overflow-x':'auto', 'min-height':'15em'} );
        }
    };


    /**
     * Setup CodeMirror editors to run an update handler on Ctrl+S or Cmd+S keypress.
     *
     * @since 1.12
     */
    self.init_codemirror_autosave = function() {
        CodeMirror.commands.save = function(cm) {

            // Prevent Firefox trigger Save Dialog
            var keypress_handler = function (cm, event) {
                if (event.which == 115 && (event.ctrlKey || event.metaKey) || (event.which == 19)) {
                    event.preventDefault();
                    return false;
                }
                return true;
            };
            CodeMirror.off(cm.getWrapperElement(), 'keypress', keypress_handler);
            cm.on('keypress', keypress_handler);

            var editor_selector = cm.getTextArea().id;
            var editor_description = _.findWhere(self.editors, {selector: editor_selector});
            if( 'undefined' != typeof(editor_description) ) {
                editor_description.manualUpdateHandler(editor_description.propertyName);
            }

        };
    };



    /**
     * Populate CodeMirror editors with content and bind it to ViewModel properties.
     *
     * More precisely, we will initialize the editor with the current property value and then, on editor content change,
     * we will be updating the underlying textarea, which is already bound with the ViewModel property via Knockout.
     *
     * @since 1.9
     */
    self.fill_editors = function() {
        _.each(self.editors, function(editor_info) {
            var editor_slug = 'codemirror_' + editor_info.slug;

            // Set current property's value into the editor and refresh it
            self[editor_slug].setValue(self.vm[editor_info.propertyToUpdate]());
            self[editor_slug].refresh();

            // When editor value changes, update the underlying textarea.
            self[editor_slug].on('change', function(cm) {
                var value = cm.getValue();
                $('#' + editor_info.selector).val(value);
                self.vm.log(
                    'codemirror change(' + editor_slug + '): updating vm[' + editor_info.propertyToUpdate + '] from "'
                    + self.vm[ editor_info.propertyToUpdate ]() + '" to "' + value + '".'
                );
                self.vm[editor_info.propertyToUpdate](value);
            });
        });
    };


    // ----------------------------------------------------------------------------
    // Action bar
    // ----------------------------------------------------------------------------


    /**
     * Initialize the Action bar with the "Save all changes at once" button.
     *
     * Note: This is taken from Views and WPA edit pages and IMHO should exist only once
     * in some common JS file.
     *
     * @since 1.9
     */
    self.init_action_bar = function() {

        self.action_bar = $( '#js-wpv-general-actions-bar' );
        self.action_bar_message_container	= $( '#js-wpv-general-actions-bar .js-wpv-message-container' );

        var adminBarWidth = $( '.wrap.toolset-views' ).width(),
        	adminBarHeight = self.action_bar.height(),
        	adminBarTopOffset = 0,
			adjustControls = function() {
				if ( $( window ).scrollTop() > 5 ) {
					$( '#save-form-actions' ).fadeOut( 'fast' );
					$( '#describe-actions' ).fadeOut( 'fast' );
				}
				else {
					$( '#save-form-actions' ).fadeIn( 'fast' );
					$( '#describe-actions' ).fadeIn( 'fast' );
				}
			};

		if ( $( '#wpadminbar' ).length !== 0 ) {
			adminBarTopOffset = $('#wpadminbar').height();
		}

		self.action_bar.css({
			'position': 'fixed',
			'top':adminBarTopOffset,
			'width':adminBarWidth
		});

		$( 'div#wpbody-content' ).css({
			'padding-top':( adminBarHeight + 20 )
		});

		$( window ).on( 'scroll', adjustControls );

		$( window ).on( 'resize', function() {
			var adminBarWidth = $( '.wrap.toolset-views' ).width();
			self.action_bar.width( adminBarWidth );
		});

		$( document ).on( 'click', '#title-alt', function( e ) {
			e.preventDefault();
			$( this ).hide();
			$( '#title' ).show();
		});

		$( document ).on( 'click', '#description-alt', function( e ) {
			e.preventDefault();
			$( this ).hide();
			$( '.js-wpv-description' ).show();
			var updatedAdminBarHeight = self.action_bar.height();
			$( 'div#wpbody-content' ).css({
				'padding-top':( updatedAdminBarHeight + 20 )
			});
		});

		adjustControls();

    };


    /**
     * Show an effect of geen highlighting the bottom of action bar for one second.
     *
     * This is meant to indicate a successful update.
     *
     * @since 1.9
     */
    self.highlight_action_bar = function( mode ) {
		self.action_bar.removeClass( 'wpv-action-saving wpv-action-failure wpv-action-success' );

		switch ( mode ) {
			case 'saving':
				self.action_bar.addClass( 'wpv-action-saving' );
				break;
			case 'failure':
				self.action_bar.addClass( 'wpv-action-failure' );
				setTimeout(function() { self.action_bar.removeClass( 'wpv-action-failure' ); }, 1000);
				break;
			default:
				self.action_bar.addClass( 'wpv-action-success' );
				setTimeout(function() { self.action_bar.removeClass( 'wpv-action-success' ); }, 1000);
				break;
		}
    };


    // ----------------------------------------------------------------------------
    // Dialogs
    // ----------------------------------------------------------------------------

    /* Code for custom dialogs. Currently there is only one - Colorbox - dialog for confirmation of binding
     * dissident posts.
     *
     * This should be done better and possibly with jQuery dialogs instead. The main problem is retrieving
     * "return value" (what button has the user pressed) and performing an action based on it.
     */

    /**
     * Indicates whether a default action (cancel) should be executed when dialog closes.
     *
     * This will be set to false if user choses a non-default action (clicks on the confirm button).
     *
     * @type {boolean}
     * @since 1.9
     */
    self.bindDissidentPostsDialogExecuteOnClosed = true;


    /**
     * Display a Colorbox dialog asking if user wants to bind dissident posts of a particular type.
     *
     * Default action is to call self.vm.cancelBindingDissidentPosts().
     *
     * @param postTypeInfo {{postType: string, posts: Array}} Post type information with IDs of dissident
     *     posts of that type.
     * @param htmlTemplate {string} Dialog template.
     *
     * @since 1.9
	 *
	 * @todo Deprecate the Colorbox dependency and use jQuery UI dialogs instead.
     */
    self.showBindDissidentPostsDialog = function(postTypeInfo, htmlTemplate) {

        self.bindDissidentPostsDialogExecuteOnClosed = true;

        $.colorbox({
			transition: 'fade',
			opacity: 0.3,
			speed: 150,
			fadeOut : 0,
			closeButton: false,
			trapFocus: false,
            html: htmlTemplate,
            onComplete: function() {
                // Populate the template
                $('.js-wpv-ct-bind-dialog-post-count').text(postTypeInfo.posts.length);

                var labelToUse = (postTypeInfo.posts.length == 1) ? 'labelSingular' : 'labelPlural';
                $('.js-wpv-ct-bind-dialog-post-type').text(postTypeInfo[labelToUse]);

                var updateButton = $('.js-wpv-bind-dissident-posts-dialog .js-wpv-dialog-update-button');
                updateButton.data('post-type-info', postTypeInfo);
            },
            onClosed: function() {
                if(self.bindDissidentPostsDialogExecuteOnClosed) {
                    self.vm.cancelBindingDissidentPosts(postTypeInfo);
                }
            }
        });
    };

	// ----------------------------------------------------------------------------
    // Colorbox compatibility - to deprecate
    // ----------------------------------------------------------------------------

	// Add .colorbox-active to the body element when colorbox is active
    $(document).on('cbox_complete', function() {

        if ( $('#colorbox .no-scrollbar').length === 0 ) {
            $('body').addClass('disable-scrollbar');
        }

        // trigger .button-primary to click when ENTER key is pressed and colorbox popup is opened
        $(document).on('keypress.colorbox', function(e) {
            var keycode = parseInt((e.keyCode ? e.keyCode : e.which),10);
            if ( keycode === 13 ) { // 13 is for ENTER key
                $('#cboxContent .wpv-dialog-footer .button-primary').click(); // trigger click event on the currently opened popup
            }
        });

    });

    $(document).on('cbox_cleanup', function() {
         $('body').removeClass('disable-scrollbar');
    });

    // Bind close event to .js-dialog-close classes
	// @todo remove when colorbox is also removed, that classname will never be used anymore
    $(document).on('click', '.js-dialog-close', function(e) {
        e.preventDefault();
        $.colorbox.close();
        return false;
    });


    /**
     * On "bind dissident posts" dialog, handle the update button action.
     *
     * Run self.vm.finishBindingDissidentPosts and close the dialog, disabling the default action.
     *
     * @since 1.9
     */
    $(document).on('click', '.js-wpv-bind-dissident-posts-dialog .js-wpv-dialog-update-button', function() {

        self.bindDissidentPostsDialogExecuteOnClosed = false;

        var updateButton = $(this);
        var postTypeInfo = updateButton.data('post-type-info');
        self.vm.finishBindingDissidentPosts(postTypeInfo);

        $.colorbox.close();

    });


	/**
	 * Toggle the usage section visibility
	 *
	 * @since 2.6.0
	 */
	$(document).on('click', '.js-wpv-ct-usage-toggle', function() {
		$(this).closest('.js-wpv-ct-usage-container-summary').remove();
		$('.js-wpv-ct-usage-container').fadeIn('fast');
	});



    // ----------------------------------------------------------------------------
    // Toolset compatibility
    // ----------------------------------------------------------------------------

    /**
     * Interoperation with other Toolset plugins.
     *
     * @since 1.9
	 * @since 2.4.0 Removed the CRED buttons initialization as CRED itself manages that
     */
    self.toolset_compatibility = function() {

    };


    // ----------------------------------------------------------------------------
    // Confirm unsaved changes before leaving the page
    // ----------------------------------------------------------------------------


    /**
     * Setup displaying confirmation message before user leaves the page with unsaved changes.
     *
     * When such situation occurs, additionally, all sections with unsaved changes will get an error message
     * informing about pending changes.
     *
     * @since 1.10
     */
    self.set_confirm_unload = function() {

        // skip if the method is not available yet
        // @todo should be safe to remove this in Views 1.10
        if(typeof(WPV_Toolset.Utils.setConfirmUnload) == 'undefined') {
            return;
        }

        var show_error_messages_for_unsaved_sections = function() {
            _.each(self.vm.sections, function(section) {
               if(section.isUpdateNeeded()) {
                   //noinspection JSUnresolvedVariable
                   // The variable "section.messageContainer" is always an array with selectors, so instead of calling
                   // "showErrorMessage" with an array of selectors, we need to call it with each selector separately.
                   // @todo Verify that this functionality is actually used cause the chosen message is never shown. A JS alert appears instead when trying to leave from a page with unsaved changes.
                   $.each( section.messageContainer, function( index, value ) {
                       self.showErrorMessage( value, section.properties, self.l10n.editor.pending_changes );
                   });
               }
            });
        };

        //noinspection JSUnresolvedVariable
        WPV_Toolset.Utils.setConfirmUnload(self.vm.isAnyUpdateNeeded, show_error_messages_for_unsaved_sections, self.l10n.editor.confirm_unload);
    };

    /**
     *  wpv_filter_wpv_shortcodes_gui_exclude_content_template_callback
     *
     *  Callback function for 'wpv-filter-wpv-shortcodes-gui-*-exclude-content-template' filter
     *  Returns the ID of currently editing content template, as an array.
     *  So it is not presented as a self-pointing CT for views short codes (i.e. wpv-post-body)
     */
    self.wpv_filter_wpv_shortcodes_gui_exclude_content_template_callback = function( excluded_cts ) {
        excluded_cts.push( self.ct_data.id );
        return excluded_cts;
    };


    // ----------------------------------------------------------------------------
    // Init
    // ----------------------------------------------------------------------------

    /**
     * Check if current used editor is the default editor or any third party editor
     * like Visual Composer or Beaver Builder.
     *
     * @since 2.2
     */
    self.no_third_party_editor = function() {
		wpv_ct_editor_l10n.user_editor = wpv_ct_editor_l10n.user_editor || 'basic';
		return ( wpv_ct_editor_l10n.user_editor == 'basic' );
    }

    /**
     * Initialize the edit page script.
     *
     * @since 1.9
     */
    self.init = function() {

        // Get the localization data
        //noinspection JSUnresolvedVariable
        self.l10n = wpv_ct_editor_l10n;

        if( self.no_third_party_editor() )
            self.init_editors();

        // Read additional data for specific sections.
        // Note: this is not passed via ct_data, because the data is gathered only when sections
        // are being rendered.
        var section_data = {
            usage: $(self.usageOtherAssignments).data('value')
        };

        // Read the Content Template data passed as a l10n variable and initialize
        // ViewModel with them. This will also apply knockout bindings.
        //noinspection JSUnresolvedVariable
        self.ct_data = wpv_ct_editor_ct_data;
        self.vm = new self.ViewModel(self.ct_data, section_data);

        // Add 'wpv-filter-wpv-shortcodes-gui-wpv_post_body-exclude-content-template' filter
        Toolset.hooks.addFilter( 'wpv-filter-wpv-shortcodes-gui-wpv_post_body-exclude-content-template', self.wpv_filter_wpv_shortcodes_gui_exclude_content_template_callback );

        self.update_nonce = self.ct_data.update_nonce;
        self.trash_nonce = self.ct_data.trash_nonce;

        // Show the sections once Knockout bindings have been applied.
        $('.wpv-settings-section').each(function(){
            $(this).removeClass('hidden');
        });

        // Fill CodeMirror editors with data (has to be performed after Knockout bindings
        // have been applied and the sections displayed)
        if( self.no_third_party_editor() )
            self.fill_editors();

        self.init_action_bar();

        // Adjust admin menu link
        var menu_link = $( '.wp-has-current-submenu li.current a' );
        menu_link.attr( 'href', menu_link.attr( 'href' ) + '&ct_id=' + self.ct_data.id );

        self.toolset_compatibility();

        self.set_confirm_unload();

    };

    // Call the init method and return the instance.
    _.delay(self.init, 0);

    return self;

};

// Start doing everything when the page is loaded.
jQuery( document ).ready( function( $ ) {
    WPViews.ct_edit_screen = new WPViews.CTEditScreen( $ );
});
