Source: app.js

/*global require, define, document, $, console, window */
/*jslint nomen: true, debug: true, bitwise: true, todo: true*/

/**
 * @module App
 */

define([
    "framework",
    "underscore",
    "backbone",

    "modules/mainPack"
],
    /**
     * Widget constructor class
     * @name ElascticnodeWidget
     * @requires framework {Object} jQuery|Zepto
     * @requires underscore {Object} Underscore
     * @requires backbone {Object} Backbone
     * @requires module:mainPack {Object} Loader dependent modules
     * @class Widget
     * @param config {Object} Init config object
     * @constructor
     * @return {Object} Widget constructor
     * @see module:App
     */
        function ($, _, Backbone) {
        "use strict";

        return function (config) {
            /**
             * @name module:App#config
             * @memberof module:App
             * @inner
             * @example config = {
             *   type: "exams",
             *   interval: 10,
             *   host: "school.elasticnode.ru",
             *   container: "body",
             *   title: "My title",
             * }
             * */
            var self = this,
                status = 0,
                id = 'enw' + (100000 * Math.random() | 0),
                path,
                pack,
                view = {},
                setStatus = function (val) {
                    /*
                     * -1 error
                     * 0 waiting
                     * 1 rendered
                     * 2 data fetched
                     */
                    status = view.status = val;

                    return val;
                },
                /**
                 * @name module:App#renderer
                 * @description View render function
                 * @memberof module:App
                 * @function renderer
                 * @inner
                 */
                renderer,
                /**
                 * @name module:App#flushView
                 * @description Assigned view destructor
                 * @memberof module:App
                 * @function flushView
                 * @param view {Object} Backbone.View instance
                 * @inner
                 */
                flushView = function (view) {
                    if (view && view.$el) {
                        view.undelegateEvents();
                        view.$el.unbind();

                        //Removes view from DOM
                        view.$el.remove();
                        view.remove();
                        Backbone.View.prototype.remove.call(view);
                        view = {};
                        return true;
                    }
                    return false;
                },
                /**
                 * @name module:App#loadRenderer
                 * @description Loads the view.render instance as renderer
                 * @param path {String} Module related path
                 * @memberof module:App
                 * @function loadRenderer
                 * @inner
                 */
                loadRenderer = function (path, pack) {
                    var cb = function () {
                        if (path) {
                            cb = function (viewConstructor) {
                                /* istanbul ignore else */
                                if (viewConstructor) {
                                    // Previous view annihilation
                                    flushView(view);
                                    setStatus(0);

                                    view = new (viewConstructor.extend({
                                        el: config.$container.get(0),
                                        id: id,
                                        status: status,
                                        host: config.host,
                                        title: config.title || viewConstructor.prototype.title,
                                        setStatus: setStatus,
                                        delayedSetModeArgs: [].concat(config.mode || view.delayedSetModeArgs)
                                    }))();

                                    renderer = $.proxy(view.render, view.self);
                                    renderer();
                                }
                                return renderer;
                            };
                            require([path], cb);
                            return cb;
                        }
                        return;
                    };
                    // If view module is a part of some package

                    if (pack) {
                        require([pack], cb);
                    } else {
                        cb();
                    }
                    return cb;
                },
                /**
                 * @name module:App#Loader
                 * @description Returns or creates a new one renderer instance
                 * @memberof module:App
                 * @function Loader
                 * @inner
                 */
                Loader = function () {
                    return function () {
                        return loadRenderer(path, pack);
                    };
                },
                /**
                 * @name module:App#validateConfig
                 * @description Validates config object and handles its errors
                 * @memberof module:App
                 * @function validateConfig
                 * @param [_config] {Object} Config object. If undefined actual config will be used
                 * @inner
                 * @returns {Object} Correct config or error object
                 */
                validateConfig = function (_config) {
                    _config = _.extend({}, config, _config);

                    var errCode = null,
                        allowedTypes = ["gia", "ege", "sou", "population"];


                    // At first we need to check out if widget type is allowed
                    if (!!_config.type && !_.contains(allowedTypes, _config.type)) {
                        errCode = 1;
                    }
                    // If host is undefined
                    if (!_config.host) {
                        errCode = 3;
                    }

                    // Self-update interval should be correct
                    if (_config.interval && _config.interval > 0) {
                        if (_config.interval < 5) {
                            _config.interval = 5;
                            console.warn("Too short interval. It should be at least 5m or 0 if you don't need autoupdate");
                        }
                    }
                    _config.$container = $(_config.container).eq(0);
                    // Parent existence check
                    if (!_config.$container.length) {
                        errCode = 2;
                    }

                    switch (_config.type) {
                    case "gia":
                        path = 'views/exams/giaExamsView';
                        pack = "modules/examsModule";
                        break;
                    case "ege":
                        path = 'views/exams/examsView';
                        pack = "modules/examsModule";
                        break;
                    case "sou":
                        path = 'views/sou/souView';
                        pack = "modules/souModule";
                        break;
                    case "population":
                        path = 'views/population/populationView';
                        pack = "modules/populationModule";
                        break;
                    }

                    if (errCode) {
                        setStatus(-1);
                        console.warn("Invalid config. See elasticnode.ru/widgets/#error" + errCode + " for details");
                        return {error: errCode};
                    }
                    return _config;

                },
                /**
                 * @name module:App#applyConfig
                 * @description Applies new config to current widget
                 * @memberof module:App
                 * @function applyConfig
                 * @param [config] {Object} Config object
                 * @inner
                 */
                applyConfig = function (_config, view) {
                    _config = validateConfig(_config);
                    if (!_config.hasOwnProperty("error") && !_.isEqual(_config, config)) {
                        // console.log("Config looks fine (id: " + id + ")");

                        // In case of type mismatch widget should be completely re-rendered
                        if (config.type !== _config.type) {
                            config = _config;
                            renderer = new Loader();
                        } else {
                            // if rendered
                            if (view.$el) {
                                // It title was changed
                                if (_config.title && view.title !== _config.title) {
                                    view.title = _config.title;
                                    view.renderTitle();
                                }
                                // Container was changed
                                if (config.$container.get(0) !== _config.$container.get(0)) {
                                    _config.$container.after(view.$el.detach());
                                }
                                // Todo: timeouts should be defined
                                view.interval = _config.interval;
                            }


                            // config.host influences on view.model
                            if (_config.host && config.host !== _config.host) {
                                view.host = _config.host;
                                /**
                                 * @see module:defaultView#modelInit
                                 */
                                view.modelInit();
                            }
                            config = _config;
                        }
                        // Delayed mode set
                        view.delayedSetModeArgs = [].concat(_config.mode);

                        // view.render() link
                        renderer();
                    }
                    return _config;
                };
            renderer = new Loader();
            // Public methods goes here

            /**
             * @name module:App#getStatus
             * @description Returns current widget status
             * @memberof module:App
             * @function getStatus
             */
            self.getStatus = function () {
                return status;
            };
            /**
             * @name module:App#getNode
             * @description Returns view.$el if exists or null
             * @memberof module:App
             * @function getNode
             * @returns {object|null} $container or null
             */
            self.getNode = function () {
                return view.$el || null;
            };
            /**
             * @name module:App#setConfig
             * @description Sets a new widget config
             * @memberof module:App
             * @function setConfig
             * @param config {Object} Config object
             */
            self.setConfig = function (_config) {
                return applyConfig(_config, view);
            };
            /**
             * @name module:App#setMode
             * @description Asynchronous widget mode set
             * @memberof module:App
             * @see module:defaultView#setMode
             * @function
             * @param arguments {*} Any arguments
             * @returns {boolean} True if .switchModeDelayed() method defined
             */
            self.setMode = function () {
                view.delayedSetModeArgs = arguments;
                if (view.switchModeDelayed) {
                    view.switchModeDelayed();
                    return true;
                }
                return false;
            };

            // Widget destructor
            /**
             * @name module:App#destroy
             * @description Widget annihilation
             * @memberof module:App
             * @function destroy
             * @returns {string} Message
             */
            self.destroy = function () {
                flushView();
                var key;
                //TODO clearTimeout
                for (key in this) {
                    /* istanbul ignore else */
                    if (this.hasOwnProperty(key)) {
                        delete this[key];
                    }
                }
                self.ptototype = {};
                setStatus(-1);
                // NB: destroyed object cannot be used anymore
                return "Completely destroyed view and object methods (id: " + id + ")";
            };

            // Alive!
            applyConfig(undefined, view);


            // UNIT TEST HELPERS
            self.__testonly__ = {
                applyConfig: applyConfig,
                validateConfig: validateConfig,
                setView: function (_view) { view = _view; return view; },
                getView: function () {return view; },
                getRenderer: function () {return renderer; },
                getConfig: function () {return config; },
                Loader: Loader,
                loadRenderer: loadRenderer,
                flushView: flushView,
                setStatus: setStatus
            };
        };
    });