var _ = require("lodash");
var Emitter = require("./emitter");
var validate = require("../utils/validate");
var browser = require("../utils/browser");
/**
* Mixin for objects that need to bind to events from a source object,
* and wish to handle events with member functions.
*
* @mixin
*/
var binder = {
/**
* Binds events from the source object to a handler method on 'this' object.
* The handler method is automatically bound to 'this', and stored on 'this' using
* a prefixed method name, based on the original method name.
*
* @instance
* @param {!*} source object which emits an event, and can be subscribed to.
* @param {!Object} eventToMethodMap hash of event name to method name of this.
* @return {undefined}
*/
bindEvents: function bindEvents(source, eventToMethodMap) {
validate(source, "source", { isRequired: true });
validate(eventToMethodMap, "eventToMethodMap", { isPlainObject: true });
_.each(eventToMethodMap, function(methodName, eventName) {
if (!_.isFunction(this[methodName])) {
throw new Error("binder#bindEvents: event target object does not have a method named " + methodName);
}
var eventNames = this.getEventNames(eventName);
_.each(eventNames, function(eventName) {
var onMethodName = this.getOnMethodName(source);
if (source instanceof Emitter) {
// Assuming the on method supports a context arg, so we don't need to _.bind the method
source[onMethodName](eventName, this[methodName], this);
} else {
// Not assuming the on method supports a context arg, so we need to _.bind the method
var boundMethodName = this.getBoundMethodName(methodName);
if (!_.isFunction(this[boundMethodName])) {
this[boundMethodName] = _.bind(this[methodName], this);
}
// IE8 prefixes the event name with "on"
if (onMethodName === "attachEvent") {
eventName = "on" + eventName;
}
source[onMethodName](eventName, this[boundMethodName]);
}
}, this);
}, this);
},
/**
* Unbinds from events from a given source object, which were previously bound using bindEvents.
*
* @instance
* @param {!*} source source object that emits events
* @param {!Object} eventToMethodMap hash of event names to method names of 'this'
* @return {undefined}
*/
unbindEvents: function unbindEvents(source, eventToMethodMap) {
validate(source, "source", { isRequired: true });
validate(eventToMethodMap, "eventToMethodMap", { isPlainObject: true });
_.each(eventToMethodMap, function(methodName, eventName) {
var eventNames = this.getEventNames(eventName);
_.each(eventNames, function(eventName) {
var offMethodName = this.getOffMethodName(source, eventName);
if (source instanceof Emitter) {
// Assuming the off method supports a context arg, so we don't need to _.bind the method
source[offMethodName](eventName, this[methodName], this);
} else {
// Not assuming the off method supports a context arg, get the _.bind copy of the method
var boundMethodName = this.getBoundMethodName(methodName);
if (!this[boundMethodName]) {
return;
}
// IE8 prefixes event names with "on" (e.g. onmouseup)
if (offMethodName === "detachEvent") {
eventName = "on" + eventName;
}
source[offMethodName](eventName, this[boundMethodName]);
}
}, this);
}, this);
},
/**
* Tries to find a method on the source object which can be used to bind events.
*
* @instance
* @param {!*} source object that emits events
* @return {String} first method name that could be used to bind events.
*/
getOnMethodName: function getOnMethodName(source) {
var name = _.find(["on", "addListener", "addEventListener", "attachEvent"], function(name) {
// "attachEvent" is not a "function" in IE8 (WTF)
return _.isFunction(source[name]) || (browser.isIE8 && source[name]);
});
if (!name) {
throw new Error("binder#getOnMethodName: event source object does not have an event binding method");
}
return name;
},
/**
* Tries to find a method on the source object which can be used to unbind events.
*
* @instance
* @param {!*} source object which emits events
* @return {String} first method name that could be used to unbind events.
*/
getOffMethodName: function getOffMethodName(source) {
var name = _.find(["off", "removeListener", "removeEventListener", "detachEvent"], function(name) {
// "detachEvent" is not a "function" in IE8 (WTF)
return _.isFunction(source[name]) || (browser.isIE8 && source[name]);
});
if (!name) {
throw new Error("binder#getOffMethodName: event source object does not have an event unbinding method");
}
return name;
},
/**
* Splits an event name by " " into possibly multiple event names
*
* @instance
* @param {String} eventName - single or space-delimited event name(s)
* @return {String[]} - array of event names
*/
getEventNames: function getEventNames(eventName) {
eventName = eventName || "";
return _(eventName.split(" "))
.map(function(name) {
return name.trim();
})
.filter(function(name) {
return name.length > 0;
})
.value();
},
/**
* Creates a method name to use for binding a member event handler function to
* the target instance. E.g. if your target has a method named "onItemChanged", this method
* will return a new function name like "_bound_onItemChanged" which can be used as a new
* member name to store the bound member function.
*
* @param {String} methodName method name to prefix
* @return {String} prefixed method name
*/
getBoundMethodName: function getBoundMethodName(methodName) {
return "_bound_" + methodName;
}
};
module.exports = binder;