/
EventPublisher.js
278 lines (253 loc) · 8.91 KB
/
EventPublisher.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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
/**
* EventPublisher
*
* Released under Academic Free Licence 3.0.
* @author Garrett Smith
*
* @class
* <code>APE.EventPublisher</code> can be used for native browser events or custom events.
*
* <p> For native browser events, use <code>APE.EventPublisher</code>
* steals the event handler off native elements and creates a callStack.
* that fires in its place.
* </p>
* <p>
* There are two ways to create custom events.
* </p>
* <ol>
* <li>Create a function on the object that fires the "event", then call that function
* when the event fires (this happens automatically with native events).
* </li>
* <li>
* Instantiate an <code>EventPublisher</code> using the constructor, then call <code>fire</code>
* when the callbacks should be run.
* </li>
* </ol>
* <p>
* An <code>EventPublisher</code> itself publishes <code>beforeFire</code> and <code>afterFire</code>.
* This makes it possible to add AOP before advice to the callStack.
* </p><p>
* adding before-before advice is possible, but will impair performance.
* Instead, add multiple beforeAdvice with:
* <code>publisher.addBefore(fp, thisArg).add(fp2, thisArg);</code>
* </p><p>
* There are no <code>beforeEach</code> and <code>afterEach</code> methods; to create advice
* for each callback would require modification
* to the registry (see comments below). I have not yet found a real need for this.
* </p>
*/
(function(){
var APE = self.APE,
/** Map of [APE.EventPublisher], keyed by type. */
Registry = {},
isMaybeLeak/*@cc_on=(@_jscript_version<5.7)@*/;
/**
* @constructor
* @description creates an <code>EventPublisher</code> with methods <code>add()</code>,
* <code>fire</code>, etc.
* @memberof APE
*/
function EventPublisher(src, type) {
this.src = src;
this._callStack = [];
this.type = type;
}
APE.EventPublisher = APE.createMixin(EventPublisher, {
addCallback : add,
removeCallback : remove,
get : get,
add : add,
remove : remove,
fire : fire,
cleanUp : cleanUp,
prototype : {
/**
* @param {Function} fp the callback function that gets called when src[sEvent] is called.
* @param {Object} thisArg the context that the function executes in.
* @return {EventPublisher} this.
*/
addCallback : function(fp, thisArg) {
this._callStack.push([fp, thisArg||this.src]);
return this;
},
/** Adds beforeAdvice to the callStack. This fires before the callstack.
* @param {Function} fp the callback function that gets called when src[sEvent] is called.
* function's returnValue proceed false stops the callstack and returns false to the original call.
* @param {Object} thisArg the context that the function executes in.
* @return {EventPublisher} this.
*/
addBefore : function(f, thisArg) {
return add(this, "beforeFire", f, thisArg||this.src);
},
/** Adds afterAdvice to the callStack. This fires after the callstack.
* @param {Function} fp the callback function that gets called when src[sEvent] is called.
* function's returnValue of false returns false to the original call.
* @param {Object} thisArg the context that the function executes in.
* @return {EventPublisher} this.
*/
addAfter : function(f, thisArg) {
return add(this, "afterFire", f, thisArg||this.src);
},
/**
* @param {String} "beforeFire", "afterFire" conveneince.
* @return {EventPublisher} this;
*/
getEvent : function(type) {
return get(this, type);
},
/** Removes fp from callstack.
* @param {Function} fp the callback function to remove.
* @param {Object} [thisArg] the context that the function executes in.
* @return {EventPublisher} this.
*/
removeCallback : function(fp, thisArg) {
var cs = this._callStack, i, call;
thisArg = thisArg || this.src;
for(i = 0; i < cs.length; i++) {
call = cs[i];
if(call[0] === fp && call[1] === thisArg) {
cs.splice(i, 1);
}
}
return this;
},
/** Removes fp from callstack's beforeFire.
* @param {Function} fp the callback function to remove.
* @param {Object} [thisArg] the context that the function executes in.
* @return {EventPublisher} this.
*/
removeBefore : function(fp, thisArg) {
return get(this, "beforeFire").removeCallback(fp, thisArg||this.src);
},
/** Removes fp from callstack's afterFire.
* @param {Function} fp the callback function to .
* @param {Object} [thisArg] the context that the function executes in.
* @return {EventPublisher} this.
*/
removeAfter : function(fp, thisArg) {
return get(this, "afterFire").removeCallback(fp, thisArg||this.src);
},
/** Fires the event. */
fire : function(payload) {
return fire(this)(payload);
},
/** helpful debugging info */
toString : function() {
return"EventPublisher src=" + this.src + ", type=" + this.type +
", length="+this._callStack.length+"}";
}
}
});
/**
* @static
* @memberOf {APE.EventPublisher}
* called onunload, automatically onunload.
* This is only called for if jscript version <= 5.6 is detected
* supported. IE has memory leak problems; other browsers have fast forward/back,
* but that won't work if there's an onunload handler.
*/
function cleanUp() {
var type, publisherList, publisher, i, len;
for(type in Registry) {
publisherList = Registry[type];
for(i = 0, len = publisherList.length; i < len; i++) {
publisher = publisherList[i];
publisher.src[publisher.type] = null;
}
}
Registry = {};
}
/**
* @static
* @param {Object} src the object which calls the function
* @param {String} sEvent the function that gets called.
* @param {Function} fp the callback function that gets called when src[sEvent] is called.
* @param {Object} thisArg the context that the function executes in.
*/
function add(src, sEvent, fp, thisArg) {
return get(src, sEvent).addCallback(fp, thisArg);
}
function remove(src, sEvent, fp, thisArg) {
return get(src, sEvent).removeCallback(fp, thisArg);
}
/**
* @static
* @private
* @memberOf {APE.EventPublisher}
* @return {boolean} false if any one of callStack's methods return false.
*/
function fire(publisher) {
// return function w/identifier doesn't work in Safari 2.
return fireEvent;
function fireEvent(e) {
var preventDefault = false,
i,
cs = publisher._callStack.slice(), csi;
// beforeFire can affect return value.
if(typeof publisher.beforeFire == "function") {
try {
if(publisher.beforeFire(e) == false)
preventDefault = true;
} catch(ex){deferError(ex);}
}
for(i = 0; i < cs.length; i++) {
csi = cs[i];
// If an error occurs, continue the event fire,
// but still throw the error.
try {
// TODO: beforeEach to prevent or advise each call.
if(csi[0].call(csi[1], e) == false)
preventDefault = true; // continue main callstack and return false afterwards.
// TODO: afterEach
}
catch(ex) {
deferError(ex);
}
}
// afterFire can prevent default.
if(typeof publisher.afterFire == "function") {
if(publisher.afterFire(e) == false)
preventDefault = true;
}
return !preventDefault;
}
}
/** Throws the error in a setTimeout 1ms.
* Deferred errors are useful for Event Notification systems,
* Animation, and testing.
* @param {Error} error that occurred.
*/
function deferError(error) {
self.setTimeout(function(){throw error;},1);
}
/**
* @static
* @param {Object} src the object which calls the function
* @param {String} sEvent the function that gets called.
* @memberOf {APE.EventPublisher}
* Looks for an APE.EventPublisher in the Registry.
* If none found, creates and adds one to the Registry.
*/
function get(src, sEvent) {
var publisherList = Registry[sEvent] || (Registry[sEvent] = []),
i, len,
publisher;
for(i = 0, len = publisherList.length; i < len; i++) {
publisher = publisherList[i];
if(publisher.src === src) {
return publisher;
}
}
// not found.
publisher = new EventPublisher(src, sEvent);
// Steal.
if(src[sEvent]) {
publisher.addCallback(src[sEvent], src);
}
src[sEvent] = fire(publisher);
publisherList[len] = publisher;
return publisher;
}
if(isMaybeLeak)
get( window, "onunload" ).addAfter( cleanUp, EventPublisher );
})();