File: src/core/event_emitter.js
window.multigraph.util.namespace("window.multigraph.core", function (ns) {
"use strict";
/**
* @module multigraph
* @submodule core
*/
ns.EventEmitter = new window.jermaine.Model(function () {
/**
* EventEmitter is a Jermaine model that supports basic event emitting /
* handling for Jermaine objects.
*
* Events are represented as plain old JavaScript objects with at least
* the following two properties:
*
* **type**
*
* > a string giving the type of the event; this can be any
* arbitrary string. The event type is not restricted to be
* from some predetermined list; applications are free to
* use whatever strings they want for their event types.
*
* **target**
*
* > a reference to the object that emitted the event
*
* Event objects may also contain arbitrary other properties that are specific to
* a particular event type.
*
* Any Jermaine model can declare itself to be an event emitter by saying
* "this.isA(EventEmitter)" in its model declaration.
*
* This adds three methods to the model:
*
* **addListener(eventType, listenerFunction)**
*
* > Registers listenerFunction as a listener for events of type
* eventType (a string). listenerFunction should be a function
* that accepts a single argument which will be a reference to an
* event object as described above. When the object emits the
* event, the listener function will be invoked in the context
* where its "this" keyword refers to the object that emitted the
* event (the event target). If listenerFunction is already
* registered as a listener for eventType, this function does
* nothing --- each listener function can be registered only once.
*
* **removeListener(eventType, listenerFunction)**
*
* > Removes the given listenerFunction from the list of listeners
* for this object for events of type eventType.
*
* **emit(event)**
*
* > Causes the object to emit the given event. The argument can be
* either a string, in which case it is assumed to be an event type
* and is converted to an event object with the given 'type'
* property, or an event object with a 'type' property and any
* other desired properties. The emit() method automatically adds
* a 'target' property to the event object, whose value is a
* reference to the object emitting the event.
*
* In most cases the emit() method is only called from within the
* implementation of an EventEmitter object, and code external to the
* object's model will use its addListener() and removeListener() methods
* to process events that the object emits. All three of these methods
* are public methods, though, so it's also possible for code outside of
* an object's implementation to cause it to emit an event, or for the
* object's own code to listen for and process its own events.
*
* Two special types of events are always present for every EventEmitter
* object: the "listenerAdded" and "listenerRemoved" events. These
* events make it possible to monitor the addition or removal of event
* listeners. The "listenerAdded" event is emitted whenever a new
* listener function is added, and the "listenerRemoved" event is emitted
* whenever a listener is removed. Each of these events contain the
* following properties:
*
* **targetType**
*
* > the event type associated with the listener
* being added or removed
*
* **listener**
*
* > the listener function being added or removed
*
* @class EventEmitter
* @for EventEmitter
* @constructor
* @example
* var Person = new window.jermaine.Model(function() {
* this.isA(EventEmitter);
* this.hasA("name").which.isA("string");
* this.respondsTo("say", function(something) {
* console.log(this.name() + ' says ' + something);
* this.emit({type : "say", message : something});
* });
* });
* var person = new Person().name("Mark");
*
* var sayListener = function(event) {
* console.log(event.target.name() + ' said ' + event.message);
* };
*
* person.say('Hello');
* person.addListener("say", sayListener);
* person.say('Alright');
* person.removeListener("say", sayListener);
* person.say('Goodbye');
*
*
* OUTPUT:
*
* Mark says Hello
* Mark says Alright
* Mark said Alright
* Mark said Goodbye
*/
// listeners is a plain old JS object whose keys are events
// types (strings); the value associated with each key is the
// list of registered listener functions for that event type.
this.hasA("listeners").which.defaultsTo( function() {
// Use a function that returns an empty object as the
// default value, so we get a new listeners object
// created for each EventEmitter instance.
return {};
});
/**
* Adds a listener function for events of a specific type
* emitted by this object.
*
* @method addListener
* @param {string} eventType the type of event
* @param {function} listener a listener function
* @return {boolean} a value indicating whether the listener
* was actually added (a listener is not added if it
* is already registered for the eventType)
*/
this.respondsTo("addListener", function (eventType, listener) {
var listeners = this.listeners(),
i;
if (listeners[eventType] === undefined) {
listeners[eventType] = [];
}
for (i=0; i<listeners[eventType].length; ++i) {
if (listeners[eventType][i] === listener) {
return false;
}
}
listeners[eventType].push(listener);
this.emit({ type : "listenerAdded",
targetType : eventType,
listener : listener});
return true;
});
/**
* Removes a listener function for events of a specific type
* emitted by this object.
*
* @method removeListener
* @param {string} eventType the type of event
* @param {function} listener the listener function to remove
* @return {boolean} a value indicating whether the listener
* was actually removed.
*/
this.respondsTo("removeListener", function (eventType, listener) {
var listeners = this.listeners(),
i;
if (listeners[eventType] !== undefined) {
for (i=0; i<listeners[eventType].length; ++i) {
if (listeners[eventType][i] === listener) {
listeners[eventType][i] = null;
this.emit({ type : "listenerRemoved",
targetType : eventType,
listener : listener});
return true;
}
}
}
return false;
});
/**
* Call this objects listeners for a specific event. If the "event"
* argument is a string, it is converted to an Object having
* that string as the value of its "type" attribute; otherwise
* the "event" argument should be an event Object having a
* "type" attribute and any other attributes approriate for
* that event type. In either case, all (if there are any) of
* the current listeners on this object for events of the
* given type will be invoked, being passed an event object.
*
* @method emit
* @param {Object|string} event either a string representing an event type, or an event
* object with a 'type' attribute.
* @return (nothing)
*/
this.respondsTo("emit", function (event) {
var listeners,
i,
nulls = [];
if (typeof(event) === "string") {
event = { type : event };
}
if (!event.target) {
event.target = this;
}
if (!event.type) {
throw new Error("Event object missing 'type' property");
}
listeners = this.listeners()[event.type];
if (!listeners) {
// no listeners registered for this event type
return;
}
// call all the listeners for this event type, except for
// nulls, which we keep track of
for (i = 0; i < listeners.length; i++) {
if (listeners[i] !== null) {
listeners[i].call(this, event);
} else {
nulls.push(i);
}
}
// remove any nulls from the listeners list; work from the end
// of the list backwards so that removing an item doesn't change
// the index of other items to be removed
if (nulls.length > 0) {
for (i=nulls.length-1; i>=0; --i) {
listeners.splice(nulls[i],1);
}
}
});
});
});