/*global define, require, window, $, console */
/*jslint nomen: true, debug: true, bitwise: true, todo: true */
/**
* @module defaultView
*/
define([
"backbone",
"handlebars",
"models/defaultModel",
"framework",
"utils",
"underscore",
"assets/titleHelper",
"assets/ifCondHelper",
"assets/localizationHelper",
"assets/menuHelper",
"css!styles/style.css"
],
function (Backbone, Handlebars, defaultModel, $, utils, _, titleTpl) {
"use strict";
/**
* @name module:defaultView
* @description Default widget view class
* @extends Backbone.View
* @class Backbone.View
* @requires Backbone
* @requires module:defaultModel
* @see module:defaultModel
* @requires framework
* @requires Handlebars
* @requires Requirejs text plugin
* @constructor
* @returns {Function} Backbone.View constructor
*/
return Backbone.View.extend({
/**
* @name module:defaultView#initialize
* @description Triggers model creation and self property definition
* @function
*/
initialize: function () {
this.self = this;
this.modelInit({
host: "http://" + this.host
});
this.setStatus = _.wrap(this.setStatus, function (setStatus, status) {
// Indicator depends on view.status
/* istanbul ignore else */
if (this.setIndicator) {
this.setIndicator(status);
}
return setStatus(status);
});
return this;
},
/**
* @name module:defaultView#ModelConstructor
* @description Default model constructor
* @see module:defaultModel
* @memberOf module:defaultView
* @function
*/
ModelConstructor: defaultModel,
/**
* @name module:defaultView#modelInit
* @description Creates related model instance
* @param ext {Object} Class extension object
* @param [Model] {Function} Model class constructor. Equals this.ModelConstructor if undefined
* @param [name] {String} Inner model property name. "model" if undefined
* @param [attr] {Object} Attributes
* @function
*/
modelInit: function (ext, Model, name, attr) {
var model;
Model = Model || this.ModelConstructor;
/* istanbul ignore else */
if (Model) {
name = name || "model";
model = this[name];
if (model) {
// Status 0: rendered but model data is still not fetched
this.setStatus(1);
// Unbinding previous handlers
model.off("change");
}
// new Model instance
model = this[name] = new (Model.extend(ext))(typeof attr === "object" ? attr : {});
// Sets renderData as default model change event handler
model.on("change", this.renderData, this);
}
return model;
},
/**
* @name module:defaultView#renderData
* @description renderData "placeHolder"
* @function
*/
renderData: function () {
return true;
},
/**
* @name module:defaultView#render
* @description Render template and append to DOM
* .render() also calls this.update() after.
* @function
* @returns view {object} current view instance
*/
render: function () {
// If is not rendered yet
if (this.status < 1) {
// Appending template to DOM
this.$el.append(Handlebars.compile(this.template || "")({
id: this.id
}));
// Reset element link from container
this.setElement(this.$el.find("#" + this.id));
this.setStatus(1);
// There's no way back if uncommented
delete this.template;
this.renderTitle();
this.afterRender();
this.afterRender = undefined;
this.setMode();
}
this.update();
return this;
},
/**
* @name module:defaultView#renderTitle
* @description Renders widget title
* Actually title can be placed in template, but in this case it couldn't be changed after
* without full widget re-render
* @param [title] New widget title
* @function
*/
renderTitle: function (title) {
var CLASSNAME = ".enw__title";
if (title !== undefined) {
this.title = title;
}
$(CLASSNAME).html(titleTpl(this.title));
return this.title;
},
/**
* @name module:defaultView#afterRender
* @description Generally is used once for event bindings and similar things,
* so this method might be removed right after the call
* @function
*/
afterRender: function () {
},
/**
* @name module:defaultView#setProgressIndicator
* @description Widget background ajax-loader.gif toggle
* @param [status] {Number} Widget status code or this.status if undefined
* @function
* @returns {number} View status code
*/
setIndicator: function (status) {
var CLASSNAME = "enw__widget--complete",
state = (status || this.status) === 2;
this.$el.toggleClass(CLASSNAME, state);
return state;
},
/**
* @name module:defaultView#update
* @description Model data updater
* @function
*/
update: function () {
this.fetch();
},
/**
* @name module:defaultView#fetch
* @description The simplest model fetch
* @function
*/
fetch: function (url) {
var data = utils.getFromCache(url);
/* istanbul ignore else */
if (this.model) {
// Silent to prevent double onChange firing
this.model.clear({silent: true});
if (url) {
this.model.instanceUrl = url;
}
// If cached data is available, just reset the model state
if (data) {
this.model.set(data);
// If not, make a new one request
} else {
this.setStatus(1);
this.model.fetch({
success: $.proxy(this.successCb, this),
error: $.proxy(this.errorCb, this)
});
}
}
},
/**
* @name module:defaultView#successCb
* @description success model fetch callback
* @function
*/
successCb: function (model, response, options) {
/**
* @see module:app#setStatus
*/
this.setStatus(2);
if (response.error || (response.collection && !response.collection.length)) {
console.warn("elasticnode.ru/widgets/#errorEmptyData");
this.showMsg("notFound");
} else {
// Pure memoization
// NB: global cache scope goes here if scope isn't specified
/**
* @see module:utils#setToCache
*/
utils.setToCache(model.instanceUrl, response);
this.switchModeDelayed();
return true;
}
return false;
},
/**
* @name module:defaultView#errorCb
* @description error model fetch callback
* @function
*/
errorCb: function (model, response, options) {
// elasticnode.ru is covered behind Varnish
// Mostly you can get this error when widget overflows throttling limit
this.showMsg("connectionError");
console.warn("elasticnode.ru/widgets/#errorConnectionFailure");
this.setStatus(2);
return true;
},
/**
* @name module:defaultView#renderMenu
* @description Renders static link list (widget mode switcher)
* @param list {Array} Menu elements
* @function
* @returns {boolean} Is there a menu in view.$el
*/
renderMenu: function (list, selector) {
var SELECTOR = selector || ".enw__menu",
TPLNAME = "menuTpl",
$menu = this.$el.find(SELECTOR),
// .renderMenu() method is common at least for 5 widget types,
// so template should be cached globally
template = utils.getFromCache(TPLNAME) ||
utils.setToCache(TPLNAME, Handlebars.compile(Handlebars.partials.menu));
// Insert rendered menu
$menu.html(template(list));
// Setting active class for the first item
this.selectMenuItem($menu.children().eq(0));
return $menu.size() > 0;
},
/**
* @name module:defaultView#selectMenuItem
* @description Menu link state indicator
* @function
*/
selectMenuItem: function (target, $menu) {
var CLASSNAME = "enw__menu__link--active",
$target = target instanceof Object ?
$(target) :
$('*[data-href="' + target + '"]');
switch (!!CLASSNAME) {
case !!$target.size():
$target
.addClass(CLASSNAME)
.siblings()
.removeClass(CLASSNAME);
return true;
case $menu instanceof $.fn.constructor:
$menu
.children()
.removeClass(CLASSNAME);
return true;
default:
return false;
}
},
/**
* @name module:defaultView#setMode
* @description Updates widget .mode property. It's pretty close to the validation too
* @param arg {object|number|string} Mode object or plain arguments
* @function
* @returns {object|boolean} Mode object or false
*/
setMode: function (arg) {
// Defaults and current settings have the same keys
var defaults = this.mode || {},
keys = _.keys(defaults),
// New state may be set in array or object notation
argsToObj = function () {
var mode = {},
args = arguments;
_.each(keys, function (value, i) {
mode[value] = args[i];
});
return mode;
},
mode = _.pick.apply(
this,
[_.defaults(
typeof arg === "object" ? arg : argsToObj.apply(this, arguments),
defaults
)].concat(keys)
);
if (!_.isEqual(this.mode, mode)) {
this.mode = mode;
return mode;
}
// If mode state wasn't changed return false to prevent useless actions
return false;
},
/**
* @name module:defaultView#switchModeDelayed
* @description Schedules widget mode change
* @function
* @param arg {*} Any argument
* @returns {boolean} Is delayed set supported
*/
switchModeDelayed: function () {
if (this.delayedSetModeArgs && this.delayedSetModeArgs.length && this.switchMode) {
this.switchMode.apply(this, this.delayedSetModeArgs);
this.delayedSetModeArgs = undefined;
return true;
}
return false;
},
/**
* @name module:defaultView#showMsg
* @description In-view message display. For errors, alerts, etc.
* @param [msg] {String} Message or message code. If !msg == true the box will be hidden
* @function
* @returns {string|*} Message text
*/
showMsg: function (msg) {
var MSGBOX_SELECTOR = ".enw__msgbox",
CONTENT_SELECTOR = ".enw__content",
HIDDEN_CLASSNAME = "enw--hidden",
$msgbox = this.$el.find(MSGBOX_SELECTOR),
$content = this.$el.find(CONTENT_SELECTOR),
codes = {
"notFound": "notFoundMsg".toLocaleString(),
"connectionError": "connectionErrorMsg".toLocaleString()
};
// If code exists
msg = codes[msg] || msg;
$content.toggleClass(HIDDEN_CLASSNAME, !!msg);
$msgbox.toggleClass(HIDDEN_CLASSNAME, !msg).html(msg);
return msg;
},
/**
* @name module:defaultView#showMessage
* @description showMsg alias
* @function
* @see module:defaultView#showMsg
* @returns {string|*} Message text
*/
showMessage: function () {
return this.showMsg.apply(this, arguments);
},
/**
* @name module:defaultView#initRelatedView
* @description Provides internal view initialization and stores object as parent view property
* @function
* @returns {object|undefined} Related view instance
*/
initRelatedView: function (hash, View, selector) {
if (!this[hash] && View && hash && selector) {
var element = this.$el.find(selector).get(0);
/* istanbul ignore else */
if (element) {
this[hash] = new (View.extend({widget: this}))({
el: element
});
}
}
return this[hash];
},
/**
* @name module:defaultView#switchRelatedView
* @description Switches widget inner views
* @param $target {Object} View.$el to activate
* @function
* @returns {object|boolean} View.$el or false
*/
switchRelatedView: function ($target, className) {
var CLASSNAME = className || "enw--displaynone";
if ($target instanceof $.fn.constructor) {
return $target
.removeClass(CLASSNAME)
.siblings()
.addClass(CLASSNAME);
}
return false;
}
});
});