/
event-listener.js
130 lines (109 loc) · 5.53 KB
/
event-listener.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
/* global RUNTIME_CHECKS, define */
(function () {
'use strict';
function moduleFactory() {
function filterList(listeningToList, eventName, eventEmitter) {
var length = listeningToList.length;
for (var i = length - 1; i >= 0; --i) {
var listeningTo = listeningToList[i];
if (!eventEmitter || listeningTo.emitter === eventEmitter) {
listeningTo.emitter.removeEventListener(eventName, listeningTo.callback);
listeningToList.splice(i, 1);
}
}
}
function setupListeners(self, eventEmitter, eventName, callback, context, useCapture) {
if (RUNTIME_CHECKS) {
if (!eventEmitter.addEventListener) {
throw '"eventEmitter" argument must be an event emitter';
}
if (typeof eventName !== 'string') {
throw '"eventName" argument must be a string';
}
}
self.__private || Object.defineProperty(self, '__private', { writable: true, value: {}, });
var listeningTo = self.__private.listeningTo || (self.__private.listeningTo = {});
var listeningToEvent = listeningTo[eventName] || (listeningTo[eventName] = []);
callback = callback.bind(context || self);
listeningToEvent.push({ callback: callback, emitter: eventEmitter });
eventEmitter.addEventListener(eventName, callback, useCapture);
}
/**
* A mixin, providing event listening capabilities to an object. This is an inversion-of-control with regards to regular event listening; the listener maintains a list of the events it is listening to. This allows the listener to remove some or all its event listeners, for instance when it is disabled or destroyed. This is an easy way to avoid leaking listeners. Caveat: don't mix eventEmitter.removeEventListener and eventListener.stopListening throughout a project, as that could result in memory leaks.
* @mixin
* @alias bff/event-listener
*/
var eventListener = {
/**
* Start listening to an event on a specified event emitting object. Both eventEmitters and eventNames arguments can be arrays. The total amount of listeners added will be the Cartesian product of the two lists.
* @instance
* @arg {Object|Array|NodeList} eventEmitters - One or more event emitters that will be listened to.
* @arg {string|Array} eventNames - One or more string identifiers for events that will be listented to.
* @arg {function} callback - The function that will be called when the event is emitted.
* @arg {any} [context] - The context with which the callback will be called (i.e. what "this" will be).
* Will default to the caller of .listenTo, if not provided.
*/
listenTo: function (eventEmitters, eventNames, callback, context, useCapture) {
if (RUNTIME_CHECKS) {
if (!eventEmitters || !(eventEmitters.addEventListener || eventEmitters instanceof Array)) {
throw '"eventEmitters" argument must be an event emitter or an array of event emitters';
}
if (typeof eventNames !== 'string' && !(eventNames instanceof Array)) {
throw '"eventNames" argument must be a string or an array of strings';
}
if (typeof callback !== 'function') {
throw '"callback" argument must be a function';
}
if (arguments.length > 4 && typeof useCapture !== 'boolean') {
throw '"useCapture" argument must be a boolean value';
}
}
// Convenience functionality that allows you to listen to all items in an Array or NodeList
// BFF Lists have this kind of functionality built it, so don't handle that case here
eventEmitters = eventEmitters instanceof Array ||
(typeof NodeList !== 'undefined' && eventEmitters instanceof NodeList) ? eventEmitters : [ eventEmitters ];
eventNames = eventNames instanceof Array ? eventNames : [ eventNames ];
for (var i = 0; i < eventEmitters.length; ++i) {
for (var j = 0; j < eventNames.length; ++j) {
setupListeners(this, eventEmitters[i], eventNames[j], callback, context, !!useCapture);
}
}
},
/**
* Stop listening to events. If no arguments are provided, the listener removes all its event listeners. Providing any or both of the optional arguments will filter the list of event listeners removed.
* @instance
* @arg {Object} [eventEmitter] - If provided, only callbacks attached to the given event emitter will be removed.
* @arg {string} [eventName] - If provided, only callbacks attached to the given event name will be removed.
*/
stopListening: function (eventEmitter, eventName) {
if (RUNTIME_CHECKS) {
if (!!eventEmitter && !eventEmitter.addEventListener) {
throw '"eventEmitter" argument must be an event emitter';
}
if (arguments.length > 1 && typeof eventName !== 'string') {
throw '"eventName" argument must be a string';
}
}
if (!this.__private || !this.__private.listeningTo) { return; } // Not listening to anything? We are done.
var eventNames = eventName ? {} : this.__private.listeningTo;
eventName && (eventNames[eventName] = true);
for (eventName in eventNames) {
var listeningToList = this.__private.listeningTo[eventName];
if (!listeningToList) { continue; }
filterList(listeningToList, eventName, eventEmitter);
listeningToList.length || (delete this.__private.listeningTo[eventName]);
}
},
};
return eventListener;
}
// Expose, based on environment
if (typeof define === 'function' && define.amd) { // AMD
define(moduleFactory);
} else if (typeof exports === 'object') { // Node, CommonJS-like
module.exports = moduleFactory();
} else { // Browser globals
var bff = window.bff = window.bff || {};
bff.eventListener = moduleFactory();
}
}());