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 );