1 /** 2 * @fileoverview 3 * EventPublisher 4 * 5 * Released under Academic Free Licence 3.0. 6 * @author Garrett Smith 7 * @class 8 * <code>APE.EventPublisher</code> can be used for native browser events or custom events. 9 * 10 * <p> For native browser events, use <code>APE.EventPublisher</code> 11 * steals the event handler off native elements and creates a callStack. 12 * that fires in its place. 13 * </p> 14 * <p> 15 * There are two ways to create custom events. 16 * </p> 17 * <ol> 18 * <li>Create a function on the object that fires the "event", then call that function 19 * when the event fires (this happens automatically with native events). 20 * </li> 21 * <li> 22 * Instantiate an <code>EventPublisher</code> using the constructor, then call <code>fire</code> 23 * when the callbacks should be run. 24 * </li> 25 * </ol> 26 * <p> 27 * An <code>EventPublisher</code> itself publishes <code>beforeFire</code> and <code>afterFire</code>. 28 * This makes it possible to add AOP before advice to the callStack. 29 * </p><p> 30 * adding before-before advice is possible, but will impair performance. 31 * Instead, add multiple beforeAdvice with: 32 * <code>publisher.addBefore(fp, thisArg).add(fp2, thisArg);</code> 33 * </p><p> 34 * There are no <code>beforeEach</code> and <code>afterEach</code> methods; to create advice 35 * for each callback would require modification 36 * to the registry (see comments below). I have not yet found a real need for this. 37 * </p> 38 */ 39 /** 40 * @constructor 41 * @description creates an <code>EventPublisher</code> with methods <code>add()</code>, 42 * <code>fire</code>, et c. 43 */ 44 APE.EventPublisher = function(src, type) { 45 this.src = src; 46 // Really could use a List of bound methods here. 47 this._callStack = []; 48 this.type = type; 49 }; 50 51 APE.EventPublisher.prototype = { 52 53 /** 54 * @param {Function} fp the callback function that gets called when src[sEvent] is called. 55 * @param {Object} thisArg the context that the function executes in. 56 * @return {EventPublisher} this; 57 */ 58 add : function(fp, thisArg) { 59 this._callStack.push([fp, thisArg||this.src]); 60 return this; 61 }, 62 /** Adds beforeAdvice to the callStack. This fires before the callstack. 63 * @param {Function:boolean} fp the callback function that gets called when src[sEvent] is called. 64 * function's returnValue proceed false stops the callstack and returns false to the original call. 65 * @param {Object} thisArg the context that the function executes in. 66 * @return {EventPublisher} this; 67 */ 68 addBefore : function(f, thisArg) { 69 return APE.EventPublisher.add(this, "beforeFire", f, thisArg); 70 }, 71 72 /** Adds afterAdvice to the callStack. This fires after the callstack. 73 * @param {Function:boolean} fp the callback function that gets called when src[sEvent] is called. 74 * function's returnValue of false returns false to the original call. 75 * @param {Object} thisArg the context that the function executes in. 76 * @return {EventPublisher} this; 77 */ 78 addAfter : function(f, thisArg) { 79 return APE.EventPublisher.add(this, "afterFire", f, thisArg); 80 }, 81 82 /** 83 * @param {String} "beforeFire", "afterFire" conveneince. 84 * @return {EventPublisher} this; 85 */ 86 getEvent : function(type) { 87 return APE.EventPublisher.get(this, type); 88 }, 89 90 /** Removes fp from callstack. 91 * @param {Function:boolean} fp the callback function to remove. 92 * @param {Object} [thisArg] the context that the function executes in. 93 * @return {Function} the function that was passed in, or null if not found; 94 */ 95 remove : function(fp, thisArg) { 96 var cs = this._callStack, i = 0, len, call; 97 if(!thisArg) thisArg = this.src; 98 for(len = cs.length; i < len; i++) { 99 call = cs[i]; 100 if(call[0] === fp && call[1] === thisArg) { 101 return cs.splice(i, 1); 102 } 103 } 104 return null; 105 }, 106 107 /** Removes fp from callstack's beforeFire. 108 * @param {Function:boolean} fp the callback function to remove. 109 * @param {Object} [thisArg] the context that the function executes in. 110 * @return {Function} the function that was passed in, or null if not found (uses remove()); 111 */ 112 removeBefore : function(fp, thisArg) { 113 return this.getEvent("beforeFire").remove(fp, thisArg); 114 }, 115 116 117 /** Removes fp from callstack's afterFire. 118 * @param {Function:boolean} fp the callback function to remove. 119 * @param {Object} [thisArg] the context that the function executes in. 120 * @return {Function} the function that was passed in, or null if not found (uses remove()); 121 */ 122 removeAfter : function(fp, thisArg) { 123 return this.getEvent("afterFire").remove(fp, thisArg); 124 }, 125 126 /** Fires the event. */ 127 fire : function(payload) { 128 return APE.EventPublisher.fire(this)(payload); 129 }, 130 131 /** helpful debugging info */ 132 toString : function() { 133 return "APE.EventPublisher: {src=" + this.src + ", type=" + this.type + 134 ", length="+this._callStack.length+"}"; 135 } 136 }; 137 138 /** 139 * @static 140 * @param {Object} src the object which calls the function 141 * @param {String} sEvent the function that gets called. 142 * @param {Function} fp the callback function that gets called when src[sEvent] is called. 143 * @param {Object} thisArg the context that the function executes in. 144 */ 145 APE.EventPublisher.add = function(src, sEvent, fp, thisArg) { 146 return APE.EventPublisher.get(src, sEvent).add(fp, thisArg); 147 }; 148 149 /** 150 * @static 151 * @private 152 * @memberOf {APE.EventPublisher} 153 * @return {boolean} false if any one of callStack's methods return false. 154 */ 155 APE.EventPublisher.fire = function(publisher) { 156 // This closure sucks. We should have partial/bind in ES. 157 // If we did, this could more reasonably be a prototype method. 158 159 // return function w/identifier doesn't work in Safari 2. 160 return fireEvent; 161 function fireEvent(e) { 162 var preventDefault = false, 163 i = 0, len, 164 cs = publisher._callStack, csi; 165 166 // beforeFire can affect return value. 167 if(typeof publisher.beforeFire == "function") { 168 try { 169 if(publisher.beforeFire(e) == false) 170 preventDefault = true; 171 } catch(ex){APE.deferError(ex);} 172 } 173 174 for(len = cs.length; i < len; i++) { 175 csi = cs[i]; 176 // If an error occurs, continue the event fire, 177 // but still throw the error. 178 try { 179 // TODO: beforeEach to prevent or advise each call. 180 if(csi[0].call(csi[1], e) == false) 181 preventDefault = true; // continue main callstack and return false afterwards. 182 // TODO: afterEach 183 } 184 catch(ex) { 185 APE.deferError(ex); 186 } 187 } 188 // afterFire can prevent default. 189 if(typeof publisher.afterFire == "function") { 190 if(publisher.afterFire(e) == false) 191 preventDefault = true; 192 } 193 return !preventDefault; 194 } 195 }; 196 197 /** 198 * @static 199 * @param {Object} src the object which calls the function 200 * @param {String} sEvent the function that gets called. 201 * @memberOf {APE.EventPublisher} 202 * Looks for an APE.EventPublisher in the Registry. 203 * If none found, creates and adds one to the Registry. 204 */ 205 APE.EventPublisher.get = function(src, sEvent) { 206 207 var publisherList = this.Registry.hasOwnProperty(sEvent) && this.Registry[sEvent] || 208 (this.Registry[sEvent] = []), 209 i = 0, len = publisherList.length, 210 publisher; 211 212 for(; i < len; i++) 213 if(publisherList[i].src === src) 214 return publisherList[i]; 215 216 // not found. 217 publisher = new APE.EventPublisher(src, sEvent); 218 // Steal. 219 if(src[sEvent]) 220 publisher.add(src[sEvent], src); 221 src[sEvent] = this.fire(publisher); 222 publisherList[publisherList.length] = publisher; 223 return publisher; 224 }; 225 226 /** 227 * Map of [APE.EventPublisher], keyed by type. 228 * @private 229 * @static 230 * @memberOf {APE.EventPublisher} 231 */ 232 APE.EventPublisher.Registry = {}; 233 234 /** 235 * @static 236 * @memberOf {APE.EventPublisher} 237 * called onunload, automatically onunload. 238 * This is only called for if window.CollectGarbage is 239 * supported. IE has memory leak problems; other browsers have fast forward/back, 240 * but that won't work if there's an onunload handler. 241 */ 242 APE.EventPublisher.cleanUp = function() { 243 var type, publisherList, publisher, i, len; 244 for(type in this.Registry) { 245 publisherList = this.Registry[type]; 246 for(i = 0, len = publisherList.length; i < len; i++) { 247 publisher = publisherList[i]; 248 publisher.src[publisher.type] = null; 249 } 250 } 251 }; 252 if(window.CollectGarbage) 253 APE.EventPublisher.get( window, "onunload" ).addAfter( APE.EventPublisher.cleanUp, APE.EventPublisher );