(function(root, factory) {
if (typeof define === 'function' && define.amd) {
define(['underscore', './NestedCell'], factory);
} else if (typeof exports === 'object') {
var _ = require('underscore');
var TorsoNestedCell = require('./NestedCell');
module.exports = factory(_, TorsoNestedCell);
} else {
root.Torso = root.Torso || {};
root.Torso.Behavior = factory(root._, root.Torso.NestedCell);
}
}(this, function(_, NestedCell) {
'use strict';
// Map of eventName: lifecycleMethod
var eventMap = {
'before-attached-callback': '_attached',
'before-detached-callback': '_detached',
'before-activate-callback': '_activate',
'before-deactivate-callback': '_deactivate',
'before-dispose-callback': '_dispose',
'render:before-attach-tracked-views': 'attachTrackedViews',
'render:begin': 'prerender',
'render:complete': 'postrender',
'initialize:begin': 'preinitialize',
'initialize:complete': 'postinitialize'
};
var Behavior = NestedCell.extend(/** @lends Behavior# */{
/**
* Unique name of the behavior instance w/in a view. More human readable than the cid.
* @name alias
* @type {string}
* @memberof Behavior.prototype
*/
/**
* cidPrefix of Behaviors
* @type {string}
*/
cidPrefix: 'b',
/**
* Add functions to be added to the view's public API. They will be behavior-scoped.
* @type {Object}
*/
mixin: {},
/**
* The behavior's prepare result will be combined with the view's prepare with the behavior's alias as the namespace.
* effectively: { [behaviorName]: behavior.prepare() } will be combined with the view's prepare result.
*
* @function
* @returns {Object} a prepare context suitable to being added to the view's prepare result.
*/
prepare: function() {
// do nothing, here for overrides and to properly inform jsdoc that this is a method.
},
/**
* Allows abstraction of common view logic into separate object
*
* @class Behavior
*
* @param {Object} behaviorAttributes the initial value of the behavior's attributes.
* @param {Object} behaviorOptions
* @param {external:Backbone-View} behaviorOptions.view that Behavior is attached to
* @param {string} behaviorOptions.alias the alias for the behavior in this view.
* @param {Object} [viewOptions] options passed to View's initialize
*
* @author deena.wang@vecna.com
*
* @see <a href="../annotated/modules/Behavior.html">Behavior Annotated Source</a>
*/
constructor: function(behaviorAttributes, behaviorOptions, viewOptions) {
behaviorOptions = behaviorOptions || {};
if (!behaviorOptions.view) {
throw new Error('Torso Behavior constructed without behaviorOptions.view');
}
this.view = behaviorOptions.view;
if (!behaviorOptions.alias) {
throw new Error('Torso Behavior constructed without behaviorOptions.alias');
}
this.alias = behaviorOptions.alias;
this.cid = this.cid || _.uniqueId(this.cidPrefix);
this.__bindLifecycleMethods();
NestedCell.apply(this, arguments);
this.__bindEventCallbacks();
},
/**
* This is called after the view's initialize method is called and will wrap the view's prepare()
* such that it returns the combination of the view's prepare result with the behavior's prepare result
* inside it under the behavior's alias.
* @private
*/
__augmentViewPrepare: function() {
var originalViewPrepareFn = _.bind(this.view.prepare, this.view);
var wrappedPrepareFn = _.wrap(originalViewPrepareFn, this.__viewPrepareWrapper);
this.view.prepare = _.bind(wrappedPrepareFn, this);
},
/**
* Wraps the view's prepare such that it returns the combination of the view and behavior's prepare results.
* @private
* @param {Function} viewPrepare the prepare method from the view.
* @returns {Object} the combined view and behavior prepare() results.
* {
* <behavior alias>: behavior.prepare(),
* ... // view prepare properties.
* }
*/
__viewPrepareWrapper: function(viewPrepare) {
var viewContext = viewPrepare() || {};
var behaviorContext = _.omit(this.toJSON(), 'view');
_.extend(behaviorContext, this.prepare());
viewContext[this.alias] = behaviorContext;
return viewContext;
},
/**
* Registers defined lifecycle methods to be called at appropriate time in view's lifecycle
*
* @private
*/
__bindLifecycleMethods: function() {
this.listenTo(this.view, 'initialize:complete', this.__augmentViewPrepare);
this.listenTo(this.view, 'before-dispose-callback', this.__dispose);
_.each(eventMap, function(callback, event) {
this.listenTo(this.view, event, this[callback]);
}, this);
},
/**
* Adds behavior's event handlers to view
* Behavior's event handlers fire on view events but are run in the context of the behavior
*
* @private
*/
__bindEventCallbacks: function() {
var behaviorEvents = _.result(this, 'events');
var viewEvents = this.view.events;
if (!viewEvents) {
if (!behaviorEvents) {
return;
} else {
viewEvents = {};
}
}
var namespacedEvents = this.__namespaceEvents(behaviorEvents);
var boundBehaviorEvents = this.__bindEventCallbacksToBehavior(namespacedEvents);
if (_.isFunction(viewEvents)) {
this.view.events = _.wrap(_.bind(viewEvents, this.view), function(viewEventFunction) {
return _.extend(boundBehaviorEvents, viewEventFunction());
});
} else if (_.isObject(viewEvents)) {
this.view.events = _.extend(boundBehaviorEvents, viewEvents);
}
},
/**
* Namespaces events in event hash
*
* @param {Object} eventHash to namespace
* @returns {Object} with event namespaced with '.behavior' and the cid of the behavior
* @private
*/
__namespaceEvents: function(eventHash) {
// coped from Backbone
var delegateEventSplitter = /^(\S+)\s*(.*)$/;
var namespacedEvents = {};
var behaviorId = this.cid;
_.each(eventHash, function(value, key) {
var splitEventKey = key.match(delegateEventSplitter);
var eventName = splitEventKey[1];
var selector = splitEventKey[2];
var namespacedEventName = eventName + '.behavior.' + behaviorId;
namespacedEvents[[namespacedEventName, selector].join(' ')] = value;
});
return namespacedEvents;
},
/**
* @param {Object} eventHash keys are event descriptors, values are String method names or functions
* @returns {Object} event hash with values as methods bound to view
* @private
*/
__bindEventCallbacksToBehavior: function(eventHash) {
return _.mapObject(eventHash, function(method) {
if (!_.isFunction(method)) {
method = this[method];
}
return _.bind(method, this);
}, this);
},
/**
* Removes all listeners, stops listening to events.
* After dispose is called, the behavior can be safely garbage collected.
* Called when the owning view is disposed.
* @private
*/
__dispose: function() {
this.trigger('before-dispose-callback');
this.stopListening();
this.off();
this.__isDisposed = true;
},
/**
* Method to be invoked when dispose is called. By default calling dispose will remove the
* behavior's on's and listenTo's.
* Override this method to destruct any extra
* @function
*/
_dispose: function() {
// do nothing, here for overrides and to properly inform jsdoc that this is a method.
},
/**
* @returns {boolean} true if the view was disposed
*/
isDisposed: function() {
return this.__isDisposed;
}
});
return Behavior;
}));