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>