[vislib/Dispatch] move away from d3.dispatch

This commit is contained in:
Spencer Alger 2015-05-07 20:10:01 -07:00
parent c41c401c02
commit baab9cad8d
6 changed files with 279 additions and 25 deletions

View file

@ -3,6 +3,7 @@ define(function (require) {
var _ = require('lodash');
var $ = require('jquery');
var Tooltip = Private(require('components/vislib/components/tooltip/tooltip'));
var SimpleEmitter = require('utils/SimpleEmitter');
/**
* Handles event responses
@ -12,18 +13,15 @@ define(function (require) {
* @param handler {Object} Reference to Handler Class Object
*/
_(Dispatch).inherits(SimpleEmitter);
function Dispatch(handler) {
var stockEvents = ['brush', 'click', 'hover', 'mouseup', 'mousedown', 'mouseover', 'mouseout'];
var customEvents = _.deepGet(handler, 'vis.eventTypes.enabled');
var eventTypes = customEvents ? stockEvents.concat(customEvents) : stockEvents;
if (!(this instanceof Dispatch)) {
return new Dispatch(handler);
}
Dispatch.Super.call(this);
this.handler = handler;
this.dispatch = d3.dispatch.apply(this, eventTypes);
this._listeners = {};
}
/**
@ -96,7 +94,6 @@ define(function (require) {
};
};
/**
*
* @method addHoverEvent
@ -104,7 +101,7 @@ define(function (require) {
*/
Dispatch.prototype.addHoverEvent = function () {
var self = this;
var isClickable = (this.dispatch.on('click'));
var isClickable = this.listenerCount('click') > 0;
var addEvent = this.addEvent;
var $el = this.handler.el;
@ -117,7 +114,7 @@ define(function (require) {
}
self.highlightLegend.call(this, $el);
self.dispatch.hover.call(this, self.eventResponse(d, i));
self.emit('hover', self.eventResponse(d, i));
}
return addEvent('mouseover', hover);
@ -153,7 +150,7 @@ define(function (require) {
function click(d, i) {
d3.event.stopPropagation();
self.dispatch.click.call(this, self.eventResponse(d, i));
self.emit('click', self.eventResponse(d, i));
}
return addEvent('click', click);
@ -177,7 +174,7 @@ define(function (require) {
* @returns {Boolean}
*/
Dispatch.prototype.isBrushable = function () {
return this.allowBrushing() && (typeof this.dispatch.on('brush') === 'function');
return this.allowBrushing() && this.listenerCount('brush') > 0;
};
/**
@ -265,8 +262,8 @@ define(function (require) {
* @returns {*} Returns a D3 brush function and a SVG with a brush group attached
*/
Dispatch.prototype.createBrush = function (xScale, svg) {
var dispatch = this.dispatch;
var attr = this.handler._attr;
var self = this;
var attr = self.handler._attr;
var height = attr.height;
var margin = attr.margin;
@ -286,7 +283,7 @@ define(function (require) {
});
var range = isTimeSeries ? brush.extent() : selected;
return dispatch.brush({
return self.emit('brush', {
range: range,
config: attr,
e: d3.event,
@ -295,7 +292,7 @@ define(function (require) {
});
// if `addBrushing` is true, add brush canvas
if (dispatch.on('brush')) {
if (self.listenerCount('brush')) {
svg.insert('g', 'g')
.attr('class', 'brush')
.call(brush)

View file

@ -100,11 +100,11 @@ define(function (require) {
* clean up event handlers every time it destroys the chart
* rebind them every time it creates the charts
*/
if (chart.events.dispatch) {
if (chart.events) {
enabledEvents = self.vis.eventTypes.enabled;
// Copy dispatch.on methods to chart object
d3.rebind(chart, chart.events.dispatch, 'on');
d3.rebind(chart, chart.events, 'on');
// Bind events to chart(s)
if (enabledEvents.length) {

View file

@ -84,7 +84,7 @@ define(function (require) {
var drawOptions = {draw: {}};
_.each(['polyline', 'polygon', 'circle', 'marker', 'rectangle'], function (drawShape) {
if (!self.events.dispatch[drawShape]) {
if (!self.events.listenerCount(drawShape)) {
drawOptions.draw[drawShape] = false;
} else {
drawOptions.draw[drawShape] = {
@ -133,12 +133,12 @@ define(function (require) {
map.on('draw:created', function (e) {
var drawType = e.layerType;
if (!self.events.dispatch[drawType]) return;
if (!self.events.listenerCount(drawType)) return;
// TODO: Different drawTypes need differ info. Need a switch on the object creation
var bounds = e.layer.getBounds();
self.events.dispatch[drawType]({
self.events.emit(drawType, {
e: e,
data: self.chartData,
bounds: {

View file

@ -0,0 +1,101 @@
define(function (require) {
var _ = require('lodash');
/**
* Simple event emitter class used in the vislib. Calls
* handlers synchronously and implements a chainable api
*
* @class
*/
function SimpleEmitter() {
this._listeners = {};
}
/**
* Add an event handler
*
* @param {string} event
* @param {function} handler
* @return {SimpleEmitter} - this, for chaining
*/
SimpleEmitter.prototype.on = function (event, handler) {
var handlers = this._listeners[event];
if (!handlers) handlers = this._listeners[event] = [];
if (!_.contains(handlers, handler)) {
handlers.push(handler);
}
return this;
};
/**
* Remove an event handler
*
* @param {string} event
* @param {function} [handler] - optional handler to remove, if no handler is
* passed then all are removed
* @return {SimpleEmitter} - this, for chaining
*/
SimpleEmitter.prototype.off = function (event, handler) {
if (!this._listeners[event]) {
return this;
}
// remove a specific handler
if (handler) _.pull(this._listeners[event], handler);
// or remove all listeners
else this._listeners[event] = null;
return this;
};
/**
* Emit an event and all arguments to all listeners for an event name
*
* @param {string} event
* @param {*} [arg...] - any number of arguments that will be applied to each handler
* @return {SimpleEmitter} - this, for chaining
*/
SimpleEmitter.prototype.emit = function (event, arg) {
if (!this._listeners[event]) return this;
var args = _.rest(arguments);
var handlers = this._listeners[event].slice(0);
var i = -1;
while (++i < handlers.length) {
handlers[i].apply(this, args);
}
return this;
};
/**
* Get the count of handlers for a specific event
*
* @param {string} [event] - optional event name to filter by
* @return {number}
*/
SimpleEmitter.prototype.listenerCount = function (event) {
if (event) {
return _.size(this._listeners[event]);
}
return _.reduce(this._listeners, function (count, handlers) {
return count + _.size(handlers);
}, 0);
};
/**
* Remove all event listeners bound to this emitter.
*
* @return {SimpleEmitter} - this, for chaining
*/
SimpleEmitter.prototype.removeAllListeners = function () {
this._listeners = {};
return this;
};
return SimpleEmitter;
});

View file

@ -0,0 +1,156 @@
define(function (require) {
describe('SimpleEmitter class', function () {
var SimpleEmitter = require('utils/SimpleEmitter');
var sinon = require('test_utils/auto_release_sinon');
var emitter;
beforeEach(function () {
emitter = new SimpleEmitter();
});
it('constructs an event emitter', function () {
expect(emitter).to.have.property('on');
expect(emitter).to.have.property('off');
expect(emitter).to.have.property('emit');
expect(emitter).to.have.property('listenerCount');
expect(emitter).to.have.property('removeAllListeners');
});
describe('#listenerCount', function () {
it('counts all event listeners without any arg', function () {
expect(emitter.listenerCount()).to.be(0);
emitter.on('a', function () {});
expect(emitter.listenerCount()).to.be(1);
emitter.on('b', function () {});
expect(emitter.listenerCount()).to.be(2);
});
it('limits to the event that is passed in', function () {
expect(emitter.listenerCount()).to.be(0);
emitter.on('a', function () {});
expect(emitter.listenerCount('a')).to.be(1);
emitter.on('a', function () {});
expect(emitter.listenerCount('a')).to.be(2);
emitter.on('b', function () {});
expect(emitter.listenerCount('a')).to.be(2);
expect(emitter.listenerCount('b')).to.be(1);
expect(emitter.listenerCount()).to.be(3);
});
});
describe('#on', function () {
it('registers a handler', function () {
var handler = sinon.stub();
emitter.on('a', handler);
expect(emitter.listenerCount('a')).to.be(1);
expect(handler.callCount).to.be(0);
emitter.emit('a');
expect(handler.callCount).to.be(1);
});
it('allows multiple event handlers for the same event', function () {
emitter.on('a', function () {});
emitter.on('a', function () {});
expect(emitter.listenerCount('a')).to.be(2);
});
it('only registers a specific listener once', function () {
var handler = function () {};
emitter.on('a', handler);
expect(emitter.listenerCount()).to.be(1);
emitter.on('a', handler);
expect(emitter.listenerCount()).to.be(1);
});
});
describe('#off', function () {
it('removes a listener if it was registered', function () {
var handler = sinon.stub();
expect(emitter.listenerCount()).to.be(0);
emitter.on('a', handler);
expect(emitter.listenerCount('a')).to.be(1);
emitter.off('a', handler);
expect(emitter.listenerCount('a')).to.be(0);
});
it('clears all listeners if no handler is passed', function () {
emitter.on('a', function () {});
emitter.on('a', function () {});
expect(emitter.listenerCount()).to.be(2);
emitter.off('a');
expect(emitter.listenerCount()).to.be(0);
});
it('does not mind if the listener is not registered', function () {
emitter.off('a', function () {});
});
it('does not mind if the event has no listeners', function () {
emitter.off('a');
});
});
describe('#emit', function () {
it('calls the handlers in the order they were defined', function () {
var i = 0;
var incr = function () { return ++i; };
var one = sinon.spy(incr);
var two = sinon.spy(incr);
var three = sinon.spy(incr);
var four = sinon.spy(incr);
emitter
.on('a', one)
.on('a', two)
.on('a', three)
.on('a', four)
.emit('a');
expect(one).to.have.property('callCount', 1);
expect(one.returned(1)).to.be.ok();
expect(two).to.have.property('callCount', 1);
expect(two.returned(2)).to.be.ok();
expect(three).to.have.property('callCount', 1);
expect(three.returned(3)).to.be.ok();
expect(four).to.have.property('callCount', 1);
expect(four.returned(4)).to.be.ok();
});
it('always emits the handlers that were initially registered', function () {
var destructive = sinon.spy(function () {
emitter.removeAllListeners();
expect(emitter.listenerCount()).to.be(0);
});
var stub = sinon.stub();
emitter.on('run', destructive).on('run', stub).emit('run');
expect(destructive).to.have.property('callCount', 1);
expect(stub).to.have.property('callCount', 1);
});
it('applies all arguments except the first', function () {
emitter
.on('a', function (a, b, c) {
expect(a).to.be('foo');
expect(b).to.be('bar');
expect(c).to.be('baz');
})
.emit('a', 'foo', 'bar', 'baz');
});
it('uses the SimpleEmitter as the this context', function () {
emitter
.on('a', function () {
expect(this).to.be(emitter);
})
.emit('a');
});
});
});
});

View file

@ -57,7 +57,7 @@ define(function (require) {
it('should attach a hover event', function () {
vis.handler.charts.forEach(function (chart) {
expect(_.isFunction(chart.events.dispatch.hover)).to.be(true);
expect(chart.events.listenerCount('hover')).to.be.above(0);
});
});
});
@ -73,7 +73,7 @@ define(function (require) {
it('should attach a click event', function () {
vis.handler.charts.forEach(function (chart) {
expect(_.isFunction(chart.events.dispatch.click)).to.be(true);
expect(chart.events.listenerCount('click')).to.be.above(0);
});
});
});
@ -89,7 +89,7 @@ define(function (require) {
it('should attach a brush event', function () {
vis.handler.charts.forEach(function (chart) {
expect(_.isFunction(chart.events.dispatch.brush)).to.be(true);
expect(chart.events.listenerCount('brush')).to.be.above(0);
});
});
});
@ -106,7 +106,7 @@ define(function (require) {
});
describe('Custom event handlers', function () {
it('should attach whatever gets passed on vis.on() to dispatch', function (done) {
it('should attach whatever gets passed on vis.on() to chart.events', function (done) {
var vis;
var chart;
module('AreaChartFactory');
@ -116,7 +116,7 @@ define(function (require) {
vis.render(data);
vis.handler.charts.forEach(function (chart) {
expect(chart.events.dispatch.someEvent).to.be.a(Function);
expect(chart.events.listenerCount('someEvent')).to.be.above(0);
});
destroyVis(vis);