Release
This commit is contained in:
211
client/islets/core/y-event-emitter/y-event-emitter.js
Normal file
211
client/islets/core/y-event-emitter/y-event-emitter.js
Normal file
@@ -0,0 +1,211 @@
|
||||
modules.define(
|
||||
'y-event-emitter',
|
||||
['inherit'],
|
||||
function (provide, inherit) {
|
||||
|
||||
var slice = Array.prototype.slice;
|
||||
|
||||
/**
|
||||
* @name YEventEmitter
|
||||
*/
|
||||
var YEventEmitter = inherit({
|
||||
/**
|
||||
* Добавляет обработчик события.
|
||||
*
|
||||
* @param {String} event
|
||||
* @param {Function} callback
|
||||
* @param {Object} [context]
|
||||
* @returns {YEventEmitter}
|
||||
*/
|
||||
on: function (event, callback, context) {
|
||||
if (typeof callback !== 'function') {
|
||||
throw new TypeError('callback must be a function');
|
||||
}
|
||||
|
||||
if (!this._events) {
|
||||
this._events = {};
|
||||
}
|
||||
|
||||
var listener = {
|
||||
callback: callback,
|
||||
context: context
|
||||
};
|
||||
|
||||
var listeners = this._events[event];
|
||||
if (listeners) {
|
||||
listeners.push(listener);
|
||||
} else {
|
||||
this._events[event] = [listener];
|
||||
this._onAddEvent(event);
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Добавляет обработчик события, который исполнится только 1 раз, затем удалится.
|
||||
*
|
||||
* @param {String} event
|
||||
* @param {Function} callback
|
||||
* @param {Object} [context]
|
||||
* @returns {YEventEmitter}
|
||||
*/
|
||||
once: function (event, callback, context) {
|
||||
if (typeof callback !== 'function') {
|
||||
throw new TypeError('callback must be a function');
|
||||
}
|
||||
|
||||
var _this = this;
|
||||
|
||||
function once() {
|
||||
_this.off(event, once, context);
|
||||
callback.apply(context, arguments);
|
||||
}
|
||||
|
||||
// Сохраняем ссылку на оригинальный колбэк. Благодаря этому можно удалить колбэк `once`,
|
||||
// используя оригинальный колбэк в методе `off()`.
|
||||
once._callback = callback;
|
||||
|
||||
this.on(event, once, context);
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Удаляет обработчик события.
|
||||
*
|
||||
* @param {String} event
|
||||
* @param {Function} callback
|
||||
* @param {Object} [context]
|
||||
* @returns {YEventEmitter}
|
||||
*/
|
||||
off: function (event, callback, context) {
|
||||
if (typeof callback !== 'function') {
|
||||
throw new TypeError('callback must be a function');
|
||||
}
|
||||
|
||||
if (!this._events) {
|
||||
return this;
|
||||
}
|
||||
|
||||
var listeners = this._events[event];
|
||||
if (!listeners) {
|
||||
return this;
|
||||
}
|
||||
|
||||
var len = listeners.length;
|
||||
for (var i = 0; i < len; i++) {
|
||||
var listener = listeners[i];
|
||||
var cb = listener.callback;
|
||||
if ((cb === callback || cb._callback === callback) && listener.context === context) {
|
||||
if (len === 1) {
|
||||
delete this._events[event];
|
||||
this._onRemoveEvent(event);
|
||||
} else {
|
||||
listeners.splice(i, 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Удаляет все обработчики всех событий или все обработчики переданного события `event`.
|
||||
*
|
||||
* @param {String} [event]
|
||||
* @returns {YEventEmitter}
|
||||
*/
|
||||
offAll: function (event) {
|
||||
if (this._events) {
|
||||
if (event) {
|
||||
if (this._events[event]) {
|
||||
delete this._events[event];
|
||||
this._onRemoveEvent(event);
|
||||
}
|
||||
} else {
|
||||
for (event in this._events) {
|
||||
if (this._events.hasOwnProperty(event)) {
|
||||
this._onRemoveEvent(event);
|
||||
}
|
||||
}
|
||||
delete this._events;
|
||||
}
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Исполняет все обработчики события `event`.
|
||||
*
|
||||
* @param {String} event
|
||||
* @param {...*} [args] Аргументы, которые будут переданы в обработчики события.
|
||||
* @returns {YEventEmitter}
|
||||
*/
|
||||
emit: function (event) {
|
||||
if (!this._events) {
|
||||
return this;
|
||||
}
|
||||
|
||||
var listeners = this._events[event];
|
||||
if (!listeners) {
|
||||
return this;
|
||||
}
|
||||
|
||||
// Копируем массив обработчиков, чтобы добавление/удаление обработчиков внутри колбэков не оказывало
|
||||
// влияния в цикле.
|
||||
var listenersCopy = listeners.slice(0);
|
||||
var len = listenersCopy.length;
|
||||
var listener;
|
||||
var i = -1;
|
||||
|
||||
switch (arguments.length) {
|
||||
// Оптимизируем наиболее частые случаи.
|
||||
case 1:
|
||||
while (++i < len) {
|
||||
listener = listenersCopy[i];
|
||||
listener.callback.call(listener.context);
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
while (++i < len) {
|
||||
listener = listenersCopy[i];
|
||||
listener.callback.call(listener.context, arguments[1]);
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
while (++i < len) {
|
||||
listener = listenersCopy[i];
|
||||
listener.callback.call(listener.context, arguments[1], arguments[2]);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
var args = slice.call(arguments, 1);
|
||||
while (++i < len) {
|
||||
listener = listenersCopy[i];
|
||||
listener.callback.apply(listener.context, args);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Вызывается когда было добавлено новое событие.
|
||||
*
|
||||
* @protected
|
||||
* @param {String} event
|
||||
*/
|
||||
_onAddEvent: function () {},
|
||||
|
||||
/**
|
||||
* Вызывается когда все обработчики события были удалены.
|
||||
*
|
||||
* @protected
|
||||
* @param {String} event
|
||||
*/
|
||||
_onRemoveEvent: function () {}
|
||||
});
|
||||
|
||||
provide(YEventEmitter);
|
||||
});
|
||||
7
client/islets/core/y-event-emitter/y-event-emitter.md
Normal file
7
client/islets/core/y-event-emitter/y-event-emitter.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# y-event-emitter: эмиттер
|
||||
|
||||
Предоставляет базовый класс для сущностей, кидающих на себе события.
|
||||
|
||||
## API класса
|
||||
|
||||
<!--JS_API-->
|
||||
403
client/islets/core/y-event-emitter/y-event-emitter.test.js
Normal file
403
client/islets/core/y-event-emitter/y-event-emitter.test.js
Normal file
@@ -0,0 +1,403 @@
|
||||
modules.define(
|
||||
'test',
|
||||
['y-event-emitter'],
|
||||
function (provide, YEventEmitter) {
|
||||
|
||||
describe('YEventEmitter', function () {
|
||||
var emitter;
|
||||
|
||||
beforeEach(function () {
|
||||
emitter = new YEventEmitter();
|
||||
});
|
||||
|
||||
function testWrongCallbacks(action) {
|
||||
var wrongCallbacks = [
|
||||
undefined,
|
||||
null,
|
||||
0,
|
||||
'',
|
||||
[],
|
||||
{},
|
||||
/\w/
|
||||
];
|
||||
|
||||
wrongCallbacks.forEach(function (wrongCallback) {
|
||||
var fn = function () {
|
||||
action(wrongCallback);
|
||||
};
|
||||
fn.should.throw(TypeError, 'callback must be a function');
|
||||
});
|
||||
}
|
||||
|
||||
describe('on()', function () {
|
||||
it('should add event listeners', function () {
|
||||
var spy1 = sinon.spy();
|
||||
var spy1_1 = sinon.spy();
|
||||
var spy2 = sinon.spy();
|
||||
|
||||
emitter
|
||||
.on('event1', spy1)
|
||||
.on('event1', spy1_1)
|
||||
.on('event2', spy2)
|
||||
.emit('event1');
|
||||
|
||||
spy1.calledOnce.should.be.true;
|
||||
spy1.firstCall.calledWithExactly().should.be.true;
|
||||
|
||||
spy1_1.calledOnce.should.be.true;
|
||||
spy2.called.should.be.false;
|
||||
|
||||
emitter.emit('event2', 2, 3, 'foo');
|
||||
spy2.calledOnce.should.be.true;
|
||||
spy2.firstCall.calledWithExactly(2, 3, 'foo').should.be.true;
|
||||
|
||||
var obj = {a: 'b'};
|
||||
emitter.emit('event1', obj);
|
||||
spy1.calledTwice.should.be.true;
|
||||
spy1.secondCall.calledWithExactly(obj).should.be.true;
|
||||
spy1_1.calledTwice.should.be.true;
|
||||
spy1_1.secondCall.calledWithExactly(obj).should.be.true;
|
||||
});
|
||||
|
||||
it('should add event listener with context', function () {
|
||||
var spy1 = sinon.spy();
|
||||
var context1 = {foo: 1};
|
||||
var spy2 = sinon.spy();
|
||||
var context2 = {bar: 2};
|
||||
|
||||
emitter.on('event', spy1, context1);
|
||||
emitter.on('event', spy2, context2);
|
||||
|
||||
emitter.emit('event');
|
||||
|
||||
spy1.firstCall.calledOn(context1).should.be.true;
|
||||
spy2.firstCall.calledOn(context2).should.be.true;
|
||||
});
|
||||
|
||||
it('should can add the same listener many times', function () {
|
||||
var spy1 = sinon.spy();
|
||||
var spy2 = sinon.spy();
|
||||
var ctx = {};
|
||||
|
||||
emitter
|
||||
.on('event', spy1)
|
||||
.on('event', spy1)
|
||||
.on('event', spy2, ctx)
|
||||
.on('event', spy2, ctx)
|
||||
.emit('event');
|
||||
|
||||
spy1.callCount.should.eq(2);
|
||||
spy2.callCount.should.eq(2);
|
||||
spy2.alwaysCalledOn(ctx).should.be.true;
|
||||
});
|
||||
|
||||
it('should throw error if callback is not a function', function () {
|
||||
testWrongCallbacks(function (callback) {
|
||||
emitter.on('event', callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('once()', function () {
|
||||
it('should add a single-shot listener', function () {
|
||||
var spy = sinon.spy();
|
||||
|
||||
emitter
|
||||
.once('event', spy)
|
||||
.emit('event')
|
||||
.emit('event')
|
||||
.emit('event');
|
||||
|
||||
spy.calledOnce.should.be.true;
|
||||
});
|
||||
|
||||
it('should add a single-shot listener with context', function () {
|
||||
var ctx1 = {};
|
||||
var spy1 = sinon.spy();
|
||||
var ctx2 = {};
|
||||
var spy2 = sinon.spy();
|
||||
|
||||
emitter
|
||||
.once('event', spy1, ctx1)
|
||||
.once('event', spy2, ctx2)
|
||||
.emit('event')
|
||||
.emit('event')
|
||||
.emit('event');
|
||||
|
||||
spy1.calledOnce.should.be.true;
|
||||
spy1.firstCall.calledOn(ctx1).should.be.true;
|
||||
spy2.calledOnce.should.be.true;
|
||||
spy2.firstCall.calledOn(ctx2).should.be.true;
|
||||
});
|
||||
|
||||
it('should throw error if callback is not a function', function () {
|
||||
testWrongCallbacks(function (callback) {
|
||||
emitter.once('event', callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('emit()', function () {
|
||||
it('should work before add any event', function () {
|
||||
emitter.emit('event', 1, 2).should.eq(emitter);
|
||||
});
|
||||
|
||||
describe('while emiting event', function () {
|
||||
it('should not call listener that was added in another listener', function () {
|
||||
var spy = sinon.spy();
|
||||
|
||||
emitter.on('event', function () {
|
||||
emitter.on('event', spy);
|
||||
});
|
||||
|
||||
emitter.emit('event');
|
||||
spy.called.should.be.false;
|
||||
|
||||
emitter.emit('event');
|
||||
spy.called.should.be.true;
|
||||
});
|
||||
|
||||
it('should call listener that was removed in another listener', function () {
|
||||
var spy = sinon.spy();
|
||||
|
||||
emitter.on('event', spy);
|
||||
emitter.on('event', function () {
|
||||
emitter.off('event', spy);
|
||||
});
|
||||
|
||||
emitter.emit('event');
|
||||
spy.calledOnce.should.be.true;
|
||||
spy.reset();
|
||||
|
||||
emitter.emit('event');
|
||||
spy.called.should.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('off()', function () {
|
||||
it('should remove listener according to event', function () {
|
||||
var spy1 = sinon.spy();
|
||||
var spy2 = sinon.spy();
|
||||
|
||||
emitter
|
||||
.on('event', spy1)
|
||||
.on('event', spy2)
|
||||
.on('event2', spy1)
|
||||
.off('event', spy1)
|
||||
.off('event2', spy2)
|
||||
.emit('event');
|
||||
|
||||
spy1.called.should.be.false;
|
||||
spy2.called.should.be.true;
|
||||
|
||||
emitter.emit('event2');
|
||||
spy1.called.should.be.true;
|
||||
});
|
||||
|
||||
it('should remove listener according to event and context', function () {
|
||||
var spy = sinon.spy();
|
||||
var ctx1 = {};
|
||||
var ctx2 = {};
|
||||
|
||||
emitter
|
||||
.on('event', spy, ctx1)
|
||||
.on('event', spy, ctx2)
|
||||
.on('event', spy)
|
||||
.off('event', spy, ctx1)
|
||||
.emit('event');
|
||||
|
||||
spy.callCount.should.eq(2);
|
||||
});
|
||||
|
||||
it('should remove once listener according to event', function () {
|
||||
var spy = sinon.spy();
|
||||
|
||||
emitter
|
||||
.once('event', spy)
|
||||
.off('event', spy)
|
||||
.emit('event')
|
||||
.emit('event')
|
||||
.emit('event');
|
||||
|
||||
spy.called.should.be.false;
|
||||
});
|
||||
|
||||
it('should remove once listener according to event and context', function () {
|
||||
var ctx1 = {};
|
||||
var ctx2 = {};
|
||||
var spy = sinon.spy();
|
||||
|
||||
emitter
|
||||
.once('event', spy, ctx1)
|
||||
.once('event', spy, ctx2)
|
||||
.off('event', spy, ctx1)
|
||||
.off('event', spy)
|
||||
.emit('event')
|
||||
.emit('event');
|
||||
|
||||
spy.calledOnce.should.be.true;
|
||||
spy.firstCall.calledOn(ctx2).should.be.true;
|
||||
});
|
||||
|
||||
it('should work before add any event', function () {
|
||||
emitter.off('event', function () {}).should.eq(emitter);
|
||||
});
|
||||
|
||||
it('should remove first listener from the list of same listeners', function () {
|
||||
var spy = sinon.spy();
|
||||
emitter
|
||||
.on('event', spy)
|
||||
.on('event', spy)
|
||||
.on('event', spy);
|
||||
|
||||
emitter.off('event', spy);
|
||||
emitter.emit('event');
|
||||
spy.callCount.should.eq(2);
|
||||
spy.reset();
|
||||
|
||||
emitter.off('event', spy);
|
||||
emitter.emit('event');
|
||||
spy.callCount.should.eq(1);
|
||||
spy.reset();
|
||||
|
||||
emitter.off('event', spy);
|
||||
emitter.emit('event');
|
||||
spy.called.should.be.false;
|
||||
});
|
||||
|
||||
it('should throw error if callback is not a function', function () {
|
||||
testWrongCallbacks(function (callback) {
|
||||
emitter.off('event', callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('offAll()', function () {
|
||||
it('should remove all listeners of all events', function () {
|
||||
var spy1 = sinon.spy();
|
||||
var spy2 = sinon.spy();
|
||||
|
||||
emitter.on('event1', spy1);
|
||||
emitter.on('event2', spy2);
|
||||
|
||||
emitter.emit('event1');
|
||||
emitter.emit('event2');
|
||||
|
||||
emitter
|
||||
.offAll()
|
||||
.emit('event1')
|
||||
.emit('event2');
|
||||
|
||||
spy1.calledOnce.should.be.true;
|
||||
spy2.calledOnce.should.be.true;
|
||||
});
|
||||
|
||||
it('should work before add any event', function () {
|
||||
emitter.offAll().should.eq(emitter);
|
||||
});
|
||||
});
|
||||
|
||||
describe('offAll(event)', function () {
|
||||
it('should remove all listeners for the specified event', function () {
|
||||
var spy1 = sinon.spy();
|
||||
var spy2 = sinon.spy();
|
||||
var spy3 = sinon.spy();
|
||||
|
||||
emitter
|
||||
.on('event1', spy1)
|
||||
.on('event1', spy2)
|
||||
.on('event2', spy1)
|
||||
.on('event2', spy3)
|
||||
.offAll('event2')
|
||||
.emit('event1')
|
||||
.emit('event2');
|
||||
|
||||
spy1.calledOnce.should.be.true;
|
||||
spy2.calledOnce.should.be.true;
|
||||
spy3.called.should.be.false;
|
||||
});
|
||||
|
||||
it('should work before add any event', function () {
|
||||
emitter.offAll('event').should.eq(emitter);
|
||||
});
|
||||
});
|
||||
|
||||
describe('_onAddEvent()', function () {
|
||||
it('should be called when new event was added', function () {
|
||||
var _onAddEvent = sinon.spy(emitter, '_onAddEvent');
|
||||
var fn1 = function () {};
|
||||
var fn2 = function () {};
|
||||
var fn3 = function () {};
|
||||
|
||||
emitter.on('event1', fn1);
|
||||
_onAddEvent.callCount.should.eq(1);
|
||||
_onAddEvent.getCall(0).calledWithExactly('event1').should.be.true;
|
||||
|
||||
emitter.on('event1', fn2);
|
||||
_onAddEvent.callCount.should.eq(1);
|
||||
|
||||
emitter.on('event2', fn1);
|
||||
_onAddEvent.callCount.should.eq(2);
|
||||
_onAddEvent.getCall(1).calledWithExactly('event2').should.be.true;
|
||||
|
||||
emitter.on('event1', fn3);
|
||||
emitter.on('event2', fn2);
|
||||
_onAddEvent.callCount.should.eq(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('_onRemoveEvent()', function () {
|
||||
var _onRemoveEvent;
|
||||
|
||||
beforeEach(function () {
|
||||
_onRemoveEvent = sinon.spy(emitter, '_onRemoveEvent');
|
||||
});
|
||||
|
||||
it('should be called when event was removed', function () {
|
||||
var fn1 = function () {};
|
||||
var fn2 = function () {};
|
||||
var fn3 = function () {};
|
||||
var fn4 = function () {};
|
||||
|
||||
emitter.on('event1', fn1);
|
||||
emitter.on('event1', fn2);
|
||||
|
||||
emitter.on('event2', fn3);
|
||||
emitter.on('event2', fn4);
|
||||
|
||||
emitter.off('event1', fn2);
|
||||
_onRemoveEvent.called.should.be.false;
|
||||
|
||||
emitter.off('event1', fn1);
|
||||
_onRemoveEvent.callCount.should.eq(1);
|
||||
_onRemoveEvent.getCall(0).calledWithExactly('event1').should.be.true;
|
||||
|
||||
emitter.offAll('event1');
|
||||
_onRemoveEvent.callCount.should.eq(1, 'should not be called for already removed event');
|
||||
|
||||
emitter.offAll('event2');
|
||||
_onRemoveEvent.callCount.should.eq(2);
|
||||
_onRemoveEvent.getCall(1).calledWithExactly('event2').should.be.true;
|
||||
});
|
||||
|
||||
describe('when remove all events using offAll()', function () {
|
||||
it('should be called for each removed event', function () {
|
||||
emitter
|
||||
.on('event1', function () {})
|
||||
.on('event2', function () {})
|
||||
.on('event2', function () {})
|
||||
.on('event3', function () {})
|
||||
.offAll();
|
||||
|
||||
_onRemoveEvent.callCount.should.eq(3);
|
||||
_onRemoveEvent.getCall(0).calledWithExactly('event1').should.be.true;
|
||||
_onRemoveEvent.getCall(1).calledWithExactly('event2').should.be.true;
|
||||
_onRemoveEvent.getCall(2).calledWithExactly('event3').should.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
provide();
|
||||
});
|
||||
Reference in New Issue
Block a user