Author: hlship
Date: Thu Jun 30 00:38:11 2011
New Revision: 1141346

URL: http://svn.apache.org/viewvc?rev=1141346&view=rev
Log:
TAP5-999: Rewrite the pubsub module to always publish in terms of a DOM 
element, remove publisher & subscribers when DOM element is removed

Added:
    
tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/t5-dom.js
Modified:
    
tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/javascript/CoreJavaScriptStack.java
    
tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/t5-pubsub.js
    
tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/tapestry.js
    
tapestry/tapestry5/trunk/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/JavaScriptTests.tml

Modified: 
tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/javascript/CoreJavaScriptStack.java
URL: 
http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/javascript/CoreJavaScriptStack.java?rev=1141346&r1=1141345&r2=1141346&view=diff
==============================================================================
--- 
tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/javascript/CoreJavaScriptStack.java
 (original)
+++ 
tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/javascript/CoreJavaScriptStack.java
 Thu Jun 30 00:38:11 2011
@@ -64,7 +64,9 @@ public class CoreJavaScriptStack impleme
 
             "${tapestry.scriptaculous}/effects.js",
 
-            // Uses functions defined by the prior three
+            // Uses functions defined by the prior three.
+            // Order is important, there are some dependencies
+            // going on here.
 
             ROOT + "/t5-core.js",
 
@@ -74,6 +76,8 @@ public class CoreJavaScriptStack impleme
 
             ROOT + "/t5-pubsub.js",
 
+            ROOT + "/t5-dom.js",
+
             ROOT + "/tapestry.js",
 
             ROOT + "/tree.js" };

Added: 
tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/t5-dom.js
URL: 
http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/t5-dom.js?rev=1141346&view=auto
==============================================================================
--- 
tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/t5-dom.js
 (added)
+++ 
tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/t5-dom.js
 Thu Jun 30 00:38:11 2011
@@ -0,0 +1,81 @@
+/* Copyright 2011 The Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+T5.define("dom", function() {
+
+       function purgeChildren(element) {
+               var children = element.childNodes;
+
+               if (children) {
+                       var l = children.length, i, child;
+
+                       for (i = 0; i < l; i++) {
+                               var child = children[i];
+
+                               /* Just purge element nodes, not text, etc. */
+                               if (child.nodeType == 1)
+                                       purge(children[i]);
+                       }
+               }
+       }
+
+       // Adapted from http://javascript.crockford.com/memory/leak.html
+       function purge(element) {
+               var attrs = element.attributes;
+               if (attrs) {
+                       var i, name;
+                       for (i = attrs.length - 1; i >= 0; i--) {
+                               if (attrs[i]) {
+                                       name = attrs[i].name;
+                                       /* Looking for onclick, etc. */
+                                       if (typeof element[name] == 'function') 
{
+                                               element[name] = null;
+                                       }
+                               }
+                       }
+               }
+
+               // Get rid of any Prototype event handlers as well.
+               Event.stopObserving(element);
+
+               purgeChildren(element);
+
+               if (element.t5pubsub) {
+                       // TODO: Execute this deferred?
+                       T5.pubsub.cleanupRemovedElement(element);
+               }
+       }
+
+       /**
+        * Removes an element and all of its direct and indirect children. The
+        * element is first purged, to ensure that Internet Explorer doesn't 
leak
+        * memory if event handlers associated with the element (or its 
children)
+        * have references back to the element. This also removes all Prototype
+        * event handlers, and uses T5.pubsub.cleanupRemovedElement() to delete 
and
+        * publishers or subscribers for any removed elements.
+        * 
+        */
+       function remove(element) {
+               purge(element);
+
+               // Remove the element, and all children, in one go.
+               Element.remove(element);
+       }
+
+       return {
+               remove : remove,
+               purgeChildren : purgeChildren
+       };
+});
\ No newline at end of file

Modified: 
tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/t5-pubsub.js
URL: 
http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/t5-pubsub.js?rev=1141346&r1=1141345&r2=1141346&view=diff
==============================================================================
--- 
tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/t5-pubsub.js
 (original)
+++ 
tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/t5-pubsub.js
 Thu Jun 30 00:38:11 2011
@@ -1,162 +1,205 @@
-/* Copyright 2011 The Apache Software Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
 T5.define("pubsub", function() {
 
        var arrays = T5.arrays;
-
-       var map = arrays.map;
-       var mapcat = arrays.mapcat;
+       var first = arrays.first;
        var without = arrays.without;
+       var filter = arrays.filter;
+       var remove = arrays.remove;
+       var map = arrays.map;
+       var each = arrays.each;
 
-       var subscribersVersion = 0;
-
-       var subscribers = {};
-       var publishers = {};
+       // Element keys: topic, element, listenerfn
+       // May be multiple elements with some topic/element pair
+       // element property may be undefined
+       var subscribers = [];
+
+       // Element keys: topic, element, publisherfn
+       var publishers = [];
+
+       function purgePublisherCache(topic) {
+               each(function(publisher) {
+                       if (publisher.topic === topic) {
+                               publisher.listeners = undefined;
+                       }
+               }, publishers);
+       }
+
+       function findListeners(topic, element) {
+               var gross = filter(function(subscriber) {
+                       return subscriber.topic === topic;
+               }, subscribers);
+
+               var primary = filter(function(subscriber) {
+                       return subscriber.element === element;
+               }, gross);
+
+               var secondary = filter(function(subscriber) {
+                       // Match where the element is null or undefined
+                       return !subscriber.element;
+               }, gross);
+
+               // Return the listenerfn property from each match.
+               return map(arrays.extractProperty("listenerfn"), primary
+                               .concat(secondary));
+       }
 
        /**
-        * Expands a selector into a array of strings representing the 
containers of
-        * the selector (by stripping off successive terms, breaking at 
slashes).
+        * Subscribes a listener function to the selector. The listener function
+        * will be invoked when a message for the given topic is published. If 
an
+        * element is specified, then the listener will only be invoked when the
+        * subscribed element matches the published element.
+        * 
+        * @param topic
+        *            a topic name, which must not be blank
+        * @param element
+        *            a DOM element, which may be null to subscribe to all 
messages
+        *            for the topic
+        * @param listenerfn
+        *            function invoked when a message for the topic is 
published.
+        *            The function is invoked only if the supplied selector 
element
+        *            is undefined OR exactly matches the source element node. 
The
+        *            return value of the listenerfn will be accumulated in an 
array
+        *            and returned to the publisher.
+        * @return a function of no arguments used to unsubscribe the listener
         */
-       function expandSelector(selector) {
-               var result = [];
-               var current = selector;
-
-               while (true) {
-                       result.push(current);
-                       var slashx = current.lastIndexOf('/');
+       function subscribe(topic, element, listenerfn) {
 
-                       if (slashx < 0)
-                               break;
+               var subscriber = {
+                       topic : topic,
+                       element : element,
+                       listenerfn : listenerfn
+               };
 
-                       current = current.substring(0, slashx);
-               }
+               subscribers.push(subscriber);
+               purgePublisherCache(subscriber.topic);
 
-               return result;
-       }
+               // To prevent memory leaks via closure:
 
-       function doPublish(listeners, message) {
+               topic = null;
+               element = null;
+               listenerfn = null;
 
-               return map(function(fn) {
-                       return fn(message);
-               }, listeners);
+               // Return a function to unsubscribe
+               return function() {
+                       subscribers = without(subscriber, subscribers);
+                       purgePublisherCache(subscriber.topic);
+               }
        }
 
        /**
-        * Creates a publisher for a selector. The selector is a string 
consisting
-        * of individual terms separated by slashes. A publisher sends a 
message to
-        * listener functions. Publishers are cached internally.
+        * Creates a publish function for the indicated topic name and DOM 
element.
         * 
         * <p>
-        * The returned publisher function is used to publish a message. It 
takes a
-        * single argument, the message object. The message object is passed to 
all
-        * listener functions matching the selector. The return value from the
-        * publisher function is all the return values from the listener 
functions.
+        * The returned function is used to publish a message. Messages are
+        * published synchronously. The publish function will invoke listener
+        * functions for matching subscribers (subscribers to the same topic). 
Exact
+        * subscribers (matching the specific element) are invoked first, then
+        * general subscribers (not matching any specific element). The return 
value
+        * for the publish function is an array of all the return values from 
all
+        * invoked listener functions.
         * 
-        * @return publisher function
+        * <p>
+        * There is not currently a way to explicitly remove a publisher; 
however,
+        * when the DOM element is removed properly, all publishers and 
subscribers
+        * for the specific element will be removed as well.
+        * 
+        * <p>
+        * Publish functions are cached, repeated calls with the same topic and
+        * element return the same publish function.
+        * 
+        * @param topic
+        *            used to select listeners
+        * @param element
+        *            the DOM element used as the source of the published 
message
+        *            (also used to select listeners)
+        * @return publisher function used to publish a message
         */
-       function createPublisher(selector) {
+       function createPublisher(topic, element) {
 
-               var publisher = publishers[selector];
+               var existing = first(function(publisher) {
+                       return publisher.topic === topic && publisher.element 
=== element;
+               }, publishers);
 
-               if (publisher === undefined) {
-                       var selectors = expandSelector(selector);
+               if (existing) {
+                       return existing.publisherfn;
+               }
 
-                       var listeners = null;
+               var publisher = {
+                       topic : topic,
+                       element : element,
+                       publisherfn : function(message) {
+
+                               if (publisher.listeners == undefined) {
+                                       publisher.listeners = 
findListeners(publisher.topic,
+                                                       publisher.element);
+                               }
 
-                       var subscribersVersionSnapshot = -1;
+                               // TODO: pass a second object to each function 
to allow for
+                               // control over message propagation, supply 
listener with access
+                               // to source element.
 
-                       var publisher = function(message) {
+                               return map(function(listenerfn) {
+                                       return listenerfn(message);
+                               }, publisher.listeners);
+                       }
+               };
 
-                               // Recalculate the listeners whenever the 
subscribers map
-                               // has changed.
+               publishers.push(publisher);
 
-                               if (subscribersVersionSnapshot !== 
subscribersVersion) {
-                                       listeners = mapcat(function(selector) {
-                                               return subscribers[selector] || 
[];
-                                       }, selectors);
+               // If only there was an event or something that would inform us 
when the
+               // element was removed. Certainly, IE doesn't support that! 
Have to rely
+               // on T5.dom.remove() to inform us.
 
-                                       subscribersVersionSnapshot = 
subscribersVersion;
-                               }
+               // Mark the element to indicate it requires cleanup once 
removed from
+               // the DOM.
 
-                               return doPublish(listeners, message);
-                       };
+               element.t5pubsub = true;
 
-                       publishers[selector] = publisher;
-               }
+               // Don't want to hold a reference via closure:
+
+               topic = null;
+               element = null;
 
-               return publisher;
+               return publisher.publisherfn;
        }
 
        /**
-        * Creates a publisher for the selector (or uses a previously cached
-        * publisher) and publishes the message, returning the combined results 
of
-        * all the listener functions.
+        * Creates a publisher and immediately publishes the message, return the
+        * array of results.
         */
-       function publish(selector, message) {
-
-               return createPublisher(selector)(message);
-       }
-
-       function unsubscribe(selector, listenerfn) {
-               var listeners = subscribers[selector];
-
-               var editted = without(listenerfn, listeners);
-
-               if (editted !== listeners) {
-                       subscribers[selector] = editted;
-
-                       subscribersVersion++;
-               }
+       function publish(topic, element, message) {
+               return createPublisher(topic, element)(message);
        }
 
        /**
-        * Subscribes a listener function to a selector. The selector is a 
string
-        * consisting of individual terms separated by slashes. A publisher will
-        * send a message object to a selector; matching listener functions are
-        * invoked, and are passed the message object.
-        * <p>
-        * The return value is a function, of no parameters, used to 
unsubscribe the
-        * listener function.
-        * 
+        * Invoked whenever an element is about to be removed from the DOM to 
remove
+        * any publishers or subscribers for the element.
         */
-       function subscribe(selector, listenerfn) {
-
-               var listeners = subscribers[selector];
-
-               if (listeners === undefined) {
-                       listeners = [];
-                       subscribers[selector] = listeners;
-               }
-
-               listeners.push(listenerfn);
+       function cleanup(element) {
+               subscribers = remove(function(subscriber) {
+                       return subscriber.element === element
+               }, subscribers);
+
+               // A little evil to modify the publisher object at the same 
time it is
+               // being removed.
+
+               publishers = remove(function(publisher) {
+                       var match = publisher.element === element;
+
+                       if (match) {
+                               publisher.listeners = undefined;
+                               publisher.element = undefined;
+                       }
 
-               // Indicate that subscribers has changed, so publishers need to
-               // recalculate their listeners.
-
-               subscribersVersion++;
-
-               return function() {
-                       unsubscribe(selector, listenerfn);
-               }
+                       return match;
+               });
        }
 
        return {
-               create : createPublisher,
+               createPublisher : createPublisher,
+               subscribe : subscribe,
                publish : publish,
-               subscribe : subscribe
+               cleanupRemovedElement : cleanup
        };
 });
 

Modified: 
tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/tapestry.js
URL: 
http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/tapestry.js?rev=1141346&r1=1141345&r2=1141346&view=diff
==============================================================================
--- 
tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/tapestry.js
 (original)
+++ 
tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/tapestry.js
 Thu Jun 30 00:38:11 2011
@@ -375,7 +375,7 @@ var Tapestry = {
                Tapestry.error(Tapestry.Messages.communicationFailed + 
exception);
 
                Tapestry.debug(Tapestry.Messages.ajaxFailure + exception, 
response);
-               
+
                throw exception;
        },
 
@@ -616,7 +616,7 @@ var Tapestry = {
                        before : replaceHTML
                });
 
-               Tapestry.remove(element);
+               T5.dom.remove(element);
        },
 
        /**
@@ -626,60 +626,12 @@ var Tapestry = {
         * have references back to the element.
         * 
         * @since 5.2.0
+        * @deprecated Since 5.3, use T5.dom.remove() instead
         */
-       remove : function(element) {
-               Tapestry.purge(element);
-
-               Element.remove(element);
-       },
-
-       /**
-        * Purges the element of any event handlers (necessary in IE to ensure 
that
-        * memory leaks do not occur, and harmless in other browsers). The 
element
-        * is purged, then any children of the element are purged.
-        */
-       purge : function(element) {
-
-               /* Adapted from 
http://javascript.crockford.com/memory/leak.html */
-               var attrs = element.attributes;
-               if (attrs) {
-                       var i, name;
-                       for (i = attrs.length - 1; i >= 0; i--) {
-                               if (attrs[i]) {
-                                       name = attrs[i].name;
-                                       /* Looking for onclick, etc. */
-                                       if (typeof element[name] == 'function') 
{
-                                               element[name] = null;
-                                       }
-                               }
-                       }
-               }
-
-               /* Get rid of any Prototype event handlers as well. */
-               Event.stopObserving(element);
-
-               Tapestry.purgeChildren(element);
-       },
+       remove : T5.dom.remove,
 
-       /**
-        * Invokes purge() on all the children of the element.
-        */
-       purgeChildren : function(element) {
-
-               var children = element.childNodes;
-
-               if (children) {
-                       var l = children.length, i, child;
-
-                       for (i = 0; i < l; i++) {
-                               var child = children[i];
-
-                               /* Just purge element nodes, not text, etc. */
-                               if (child.nodeType == 1)
-                                       Tapestry.purge(children[i]);
-                       }
-               }
-       }
+       /** @deprecated Since 5.3, use T5.dom.purgeChildren instead */
+       purgeChildren : T5.dom.purgeChildren
 };
 
 Element.addMethods({
@@ -1259,9 +1211,10 @@ T5
 
                                
element.observe(Tapestry.CHANGE_VISIBILITY_EVENT, function(
                                                event) {
-                    //since events propogate up, you have you call 
event.stop() here to prevent hiding
-                    //container formFragments.
-                    event.stop();
+                                       // since events propogate up, you have 
you call event.stop()
+                                       // here to prevent hiding
+                                       // container formFragments.
+                                       event.stop();
 
                                        var makeVisible = event.memo.visible;
 
@@ -1271,14 +1224,15 @@ T5
                                        runAnimation(makeVisible);
                                });
 
-                               element.observe(Tapestry.HIDE_AND_REMOVE_EVENT, 
function(event) {
-                    event.stop();
-                                       var effect = runAnimation(false);
-
-                                       effect.options.afterFinish = function() 
{
-                                               Tapestry.remove(element);
-                                       };
-                               });
+                               element.observe(Tapestry.HIDE_AND_REMOVE_EVENT,
+                                               function(event) {
+                                                       event.stop();
+                                                       var effect = 
runAnimation(false);
+
+                                                       
effect.options.afterFinish = function() {
+                                                               
Tapestry.remove(element);
+                                                       };
+                                               });
 
                                if (!spec.alwaysSubmit) {
                                        
form.observe(Tapestry.FORM_PREPARE_FOR_SUBMIT_EVENT,

Modified: 
tapestry/tapestry5/trunk/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/JavaScriptTests.tml
URL: 
http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/JavaScriptTests.tml?rev=1141346&r1=1141345&r2=1141346&view=diff
==============================================================================
--- 
tapestry/tapestry5/trunk/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/JavaScriptTests.tml
 (original)
+++ 
tapestry/tapestry5/trunk/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/JavaScriptTests.tml
 Thu Jun 30 00:38:11 2011
@@ -216,9 +216,9 @@
         var message = { }
         var rcvd;
 
-        sub("foo", function(m) { rcvd = m; })
+        sub("foo", document, function(m) { rcvd = m; })
 
-        pub("foo", message)
+        pub("foo", document, message)
 
         assertSame(rcvd, message)        
         </pre>
@@ -229,58 +229,53 @@
       <pre>
         var count = 0;
 
-        pub("foo"); assertEqual(count, 0)
+        pub("foo", document); assertEqual(count, 0)
 
-        var unsub = sub("foo", function() { count++; } )
+        var unsub = sub("foo", document, function() {
+        count++; } )
 
-        pub("foo");
+        pub("foo", document);
         assertEqual(count, 1)
 
-        pub("foo"); assertEqual(count, 2)
+        pub("foo", document); assertEqual(count, 2)
 
-        pub("bar"); assertEqual(count, 2)
+        pub("bar",
+        document); assertEqual(count, 2)
 
         unsub()
 
-        pub("foo");
+        pub("foo", document);
         assertEqual(count, 2)
           </pre>
     </div>
 
     <div>
-      <p> Check that messages are published to less specific selectors after 
more specific.
-</p>
+      <p> Ensure that messages are delivered to specific listeners before 
general listeners. 
+      </p>
       <pre>
         var pubs = []
 
-        sub("foo/bar", function() { pubs.push("foo/bar"); })
-        sub("foo", function() {
-        pubs.push("foo");
-        })
+        sub("foo", null, function() { pubs.push("general1") })
 
-        pub("foo/bar")
+        sub("foo", document, function() {
+        pubs.push("specific1") })
 
-        assertEqual(pubs, ["foo/bar", "foo"])                
-        </pre>
-    </div>
+        sub("foo", null, function() { pubs.push("general2") })
 
-    <div>
-      <p>Check that messages to less specific selectors do not invoke 
listeners of more specific selectors.   </p>
-      <pre>
-        var pubs = []
-
-        sub("foo/bar", function() { pubs.push("foo/bar"); })
-
-        sub("foo", function() {
-        pubs.push("foo");
+        sub("foo", document, function()
+        {
+        pubs.push("specific2")
         })
 
-        pub("foo")
+        pub("foo", document, {})
 
-        assertEqual(pubs, ["foo"])                        
+        assertEqual(pubs, ["specific1", "specific2",
+        "general1",
+        "general2"])                
         </pre>
     </div>
 
+
   </div>
 
   <script>


Reply via email to