1T5.define("pubsub", function() { 2 3 var _ = T5._; 4 5 // Element keys: topic, element, listenerfn 6 // May be multiple elements with some topic/element pair 7 // element property may be undefined 8 var subscribers = []; 9 10 // Element keys: topic, element, publisherfn 11 var publishers = []; 12 13 // Necessary since T5.dom depends on T5.pubsub 14 function $(element) { 15 return T5.$(element); 16 } 17 18 function purgePublisherCache(topic) { 19 _.each(publishers, function(publisher) { 20 if (publisher.topic === topic) { 21 publisher.listeners = undefined; 22 } 23 }); 24 } 25 26 function findListeners(topic, element) { 27 var gross = _.select(subscribers, function(subscriber) { 28 return subscriber.topic === topic; 29 }); 30 31 var primary = _.select(gross, function(subscriber) { 32 return subscriber.element === element; 33 }); 34 35 var secondary = _.select(gross, function(subscriber) { 36 // Match where the element is null or undefined 37 return !subscriber.element; 38 }); 39 40 // Return the listenerfn property from each match. 41 return _(primary).chain().union(secondary).pluck("listenerfn").value(); 42 } 43 44 /** 45 * Subscribes a listener function to the selector. The listener function 46 * will be invoked when a message for the given topic is published. If an 47 * element is specified, then the listener will only be invoked when the 48 * subscribed element matches the published element. 49 * 50 * @param topic 51 * a topic name, which must not be blank 52 * @param element 53 * a DOM element, which may be null to subscribe to all messages 54 * for the topic. If a string, then T5.$() is used to locate the 55 * DOM element with the matching client id. 56 * @param listenerfn 57 * function invoked when a message for the topic is published. 58 * The function is invoked only if the supplied selector element 59 * is undefined OR exactly matches the source element node. The 60 * return value of the listenerfn will be accumulated in an array 61 * and returned to the publisher. 62 * 63 * The listener function is passed a message object as the first parameter; this is provided 64 * on each call to the topic's publish function. The second parameter is an object with two 65 * properties: An element property to identify the source of the message, and a cancel() function property 66 * that prevents further listeners from being invoked. 67 * @return a function of no arguments used to unsubscribe the listener 68 */ 69 function subscribe(topic, element, listenerfn) { 70 71 var subscriber = { 72 topic : topic, 73 element : $(element), 74 listenerfn : listenerfn 75 }; 76 77 subscribers.push(subscriber); 78 purgePublisherCache(subscriber.topic); 79 80 // To prevent memory leaks via closure: 81 82 topic = null; 83 element = null; 84 listenerfn = null; 85 86 // Return a function to unsubscribe 87 return function() { 88 subscribers = _.without(subscribers, subscriber); 89 purgePublisherCache(subscriber.topic); 90 } 91 } 92 93 /** 94 * Creates a publish function for the indicated topic name and DOM element. For global 95 * events, the convention is to use the document object. 96 * 97 * <p> 98 * The returned function is used to publish a message. Messages are 99 * published synchronously. The publish function will invoke listener 100 * functions for matching subscribers (subscribers to the same topic). Exact 101 * subscribers (matching the specific element) are invoked first, then 102 * general subscribers (not matching any specific element). The return value 103 * for the publish function is an array of all the return values from all 104 * invoked listener functions. 105 * 106 * <p> 107 * Listener functions are passed the message object and a second (optional) object.
108 * The second object contains two keys: The first, "element", identifies the element for which the publisher was created, i.e., 109 * the source of the message. The second, "cancel", is a function used to prevent further listeners 110 * from being invoked. 111 * 112 * <p> 113 * There is not currently a way to explicitly remove a publisher; however, 114 * when the DOM element is removed properly, all publishers and subscribers 115 * for the specific element will be removed as well. 116 * 117 * <p> 118 * Publish functions are cached, repeated calls with the same topic and 119 * element return the same publish function. 120 * 121 * @param topic 122 * used to select listeners 123 * @param element 124 * the DOM element used as the source of the published message 125 * (also used to select listeners). Passed through T5.$(), the 126 * result must not be null. The element will be passed to listener function as 127 * the second parameter. 128 * @return publisher function used to publish a message 129 */ 130 function createPublisher(topic, element) { 131 132 element = $(element); 133 134 if (element == null) { 135 throw "Element may not be null when creating a publisher."; 136 } 137 138 var existing = _.detect(publishers, function(publisher) { 139 return publisher.topic === topic && publisher.element === element; 140 }); 141 142 if (existing) { 143 return existing.publisherfn; 144 } 145 146 var publisher = { 147 topic : topic, 148 element : element, 149 publisherfn : function(message) { 150 151 if (publisher.listeners == undefined) { 152 publisher.listeners = findListeners(publisher.topic, 153 publisher.element); 154 } 155 156 var canceled = false; 157 158 var meta = { 159 element : publisher.element, 160 cancel : function() { 161 canceled = true; 162 } 163 }; 164 165 var result = []; 166 167 for (var i = 0; i < publisher.listeners.length; i++) { 168 169 var listenerfn = publisher.listeners[i]; 170 171 result.push(listenerfn(message, meta)); 172 173 if (canceled) { 174 break; 175 } 176 } 177 178 return result; 179 } 180 }; 181 182 publishers.push(publisher); 183 184 // If only there was an event or something that would inform us when the 185 // element was removed. Certainly, IE doesn't support that! Have to rely 186 // on T5.dom.remove() to inform us. 187 188 // Mark the element to indicate it requires cleanup once removed from 189 // the DOM. 190 191 element.t5pubsub = true; 192 193 // Don't want to hold a reference via closure: 194 195 topic = null; 196 element = null; 197 198 return publisher.publisherfn; 199 } 200 201 /** 202 * Creates a publisher and immediately publishes the message, return the 203 * array of results. 204 */ 205 function publish(topic, element, message) { 206 return createPublisher(topic, element)(message); 207 } 208 209 /** 210 * Invoked whenever an element is about to be removed from the DOM to remove 211 * any publishers or subscribers for the element. 212 */ 213 function cleanup(element) { 214 subscribers = _.reject(subscribers, function(subscriber) { 215 return subscriber.element === element 216 }); 217 218 // A little evil to modify the publisher object at the same time it is 219 // being removed. 220 221 publishers = _.reject(publishers, function(publisher) { 222 var match = publisher.element === element; 223 224 if (match) { 225 publisher.listeners = undefined; 226 publisher.element = undefined; 227 } 228 229 return match; 230 }); 231 } 232 233 return { 234 createPublisher : createPublisher, 235 subscribe : subscribe, 236 publish : publish, 237 cleanupRemovedElement : cleanup 238 }; 239}); 240 241/** 242 * Create aliases on T5 directly: pub -> T5.pubsub.publish and sub -> 243 * T5.pubsub.subscribe. 244 */ 245T5.extend(T5, { 246 pub : T5.pubsub.publish, 247 sub : T5.pubsub.subscribe 248});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.