Author: hlship
Date: Tue Jul 19 01:32:26 2011
New Revision: 1148123
URL: http://svn.apache.org/viewvc?rev=1148123&view=rev
Log:
TAP5-999: Pass the actual element on which a topic is published to listener
functions
Modified:
tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/t5-pubsub.js
tapestry/tapestry5/trunk/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/JavaScriptTests.tml
tapestry/tapestry5/trunk/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/js-testing.js
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=1148123&r1=1148122&r2=1148123&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
Tue Jul 19 01:32:26 2011
@@ -1,226 +1,252 @@
-T5.define("pubsub", function() {
+T5.define("pubsub", function()
+{
- var arrays = T5.arrays;
- var first = arrays.first;
- var without = arrays.without;
- var filter = arrays.filter;
- var remove = arrays.remove;
- var map = arrays.map;
- var each = arrays.each;
-
- // 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 = [];
-
- // Necessary since T5.dom depends on T5.pubsub
- function $(element) {
- return T5.$(element);
- }
-
- 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));
- }
-
- /**
- * 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. If a string, then T5.$() is used to locate
the
- * DOM element with the matching client id.
- * @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 subscribe(topic, element, listenerfn) {
-
- var subscriber = {
- topic : topic,
- element : $(element),
- listenerfn : listenerfn
- };
-
- subscribers.push(subscriber);
- purgePublisherCache(subscriber.topic);
-
- // To prevent memory leaks via closure:
-
- topic = null;
- element = null;
- listenerfn = null;
-
- // Return a function to unsubscribe
- return function() {
- subscribers = without(subscriber, subscribers);
- purgePublisherCache(subscriber.topic);
- }
- }
-
- /**
- * Creates a publish function for the indicated topic name and DOM
element.
- *
- * <p>
- * 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.
- *
- * <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). Passed through T5.$(),
the
- * result must not be null.
- * @return publisher function used to publish a message
- */
- function createPublisher(topic, element) {
-
- element = $(element);
-
- if (element == null) {
- throw "Element may not be null when creating a
publisher.";
- }
-
- var existing = first(function(publisher) {
- return publisher.topic === topic && publisher.element
=== element;
- }, publishers);
-
- if (existing) {
- return existing.publisherfn;
- }
-
- var publisher = {
- topic : topic,
- element : element,
- publisherfn : function(message) {
-
- if (publisher.listeners == undefined) {
- publisher.listeners =
findListeners(publisher.topic,
- publisher.element);
- }
-
- // TODO: pass a second object to each function
to allow for
- // control over message propagation, supply
listener with access
- // to source element.
-
- return map(function(listenerfn) {
- return listenerfn(message);
- }, publisher.listeners);
- }
- };
-
- publishers.push(publisher);
-
- // 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.
-
- // Mark the element to indicate it requires cleanup once
removed from
- // the DOM.
-
- element.t5pubsub = true;
-
- // Don't want to hold a reference via closure:
-
- topic = null;
- element = null;
-
- return publisher.publisherfn;
- }
-
- /**
- * Creates a publisher and immediately publishes the message, return the
- * array of results.
- */
- function publish(topic, element, message) {
- return createPublisher(topic, element)(message);
- }
-
- /**
- * Invoked whenever an element is about to be removed from the DOM to
remove
- * any publishers or subscribers for the element.
- */
- 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;
- }
-
- return match;
- });
- }
-
- return {
- createPublisher : createPublisher,
- subscribe : subscribe,
- publish : publish,
- cleanupRemovedElement : cleanup
- };
+ var arrays = T5.arrays;
+ var first = arrays.first;
+ var without = arrays.without;
+ var filter = arrays.filter;
+ var remove = arrays.remove;
+ var map = arrays.map;
+ var each = arrays.each;
+
+ // 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 = [];
+
+ // Necessary since T5.dom depends on T5.pubsub
+ function $(element)
+ {
+ return T5.$(element);
+ }
+
+ 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));
+ }
+
+ /**
+ * 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. If a string, then T5.$() is used to locate the
+ * DOM element with the matching client id.
+ * @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.
+ *
+ * The listener function is passed a message object as the
first parameter; this is provided
+ * on each call to the topic's publish function. The second
parameter is the element
+ * defined by the publisher (this is useful when subscribed to
all elements). For truly global
+ * events, the document object is used as the element.
+ * @return a function of no arguments used to unsubscribe the listener
+ */
+ function subscribe(topic, element, listenerfn)
+ {
+
+ var subscriber = {
+ topic : topic,
+ element : $(element),
+ listenerfn : listenerfn
+ };
+
+ subscribers.push(subscriber);
+ purgePublisherCache(subscriber.topic);
+
+ // To prevent memory leaks via closure:
+
+ topic = null;
+ element = null;
+ listenerfn = null;
+
+ // Return a function to unsubscribe
+ return function()
+ {
+ subscribers = without(subscriber, subscribers);
+ purgePublisherCache(subscriber.topic);
+ }
+ }
+
+ /**
+ * Creates a publish function for the indicated topic name and DOM
element. For global
+ * events, the convention is to use the document object.
+ *
+ * <p>
+ * 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.
+ *
+ * <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). Passed through T5.$(), the
+ * result must not be null. The element will be passed to
listener function as
+ * the second parameter.
+ * @return publisher function used to publish a message
+ */
+ function createPublisher(topic, element)
+ {
+
+ element = $(element);
+
+ if (element == null)
+ {
+ throw "Element may not be null when creating a publisher.";
+ }
+
+ var existing = first(function(publisher)
+ {
+ return publisher.topic === topic && publisher.element === element;
+ }, publishers);
+
+ if (existing)
+ {
+ return existing.publisherfn;
+ }
+
+ var publisher = {
+ topic : topic,
+ element : element,
+ publisherfn : function(message)
+ {
+
+ if (publisher.listeners == undefined)
+ {
+ publisher.listeners = findListeners(publisher.topic,
+ publisher.element);
+ }
+
+ return map(function(listenerfn)
+ {
+ return listenerfn(message, publisher.element);
+ }, publisher.listeners);
+ }
+ };
+
+ publishers.push(publisher);
+
+ // 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.
+
+ // Mark the element to indicate it requires cleanup once removed from
+ // the DOM.
+
+ element.t5pubsub = true;
+
+ // Don't want to hold a reference via closure:
+
+ topic = null;
+ element = null;
+
+ return publisher.publisherfn;
+ }
+
+ /**
+ * Creates a publisher and immediately publishes the message, return the
+ * array of results.
+ */
+ function publish(topic, element, message)
+ {
+ return createPublisher(topic, element)(message);
+ }
+
+ /**
+ * Invoked whenever an element is about to be removed from the DOM to
remove
+ * any publishers or subscribers for the element.
+ */
+ 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;
+ }
+
+ return match;
+ });
+ }
+
+ return {
+ createPublisher : createPublisher,
+ subscribe : subscribe,
+ publish : publish,
+ cleanupRemovedElement : cleanup
+ };
});
/**
- * Create aliases on T5 directly: pub -> pubsub.publish and sub ->
- * pubsub.subscribe.
+ * Create aliases on T5 directly: pub -> T5.pubsub.publish and sub ->
+ * T5.pubsub.subscribe.
*/
T5.extend(T5, {
- pub : T5.pubsub.publish,
- sub : T5.pubsub.subscribe
+ pub : T5.pubsub.publish,
+ sub : T5.pubsub.subscribe
});
\ No newline at end of file
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=1148123&r1=1148122&r2=1148123&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
Tue Jul 19 01:32:26 2011
@@ -1,18 +1,18 @@
<html t:type="Border"
xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
- <h1>JavaScript Tests</h1>
+<h1>JavaScript Tests</h1>
- <p>
+<p>
This page executes a number of JavaScript tests.
- </p>
+</p>
- <div id="tests">
+<div id="tests">
- <h2>Arrays</h2>
+<h2>Arrays</h2>
- <div>
- <p> Checks for T5.arrays.isEmpty()
- </p>
+<div>
+ <p> Checks for T5.arrays.isEmpty()
+ </p>
<pre>
assertEqual(isEmpty(null), true)
assertEqual(isEmpty(undefined), true)
@@ -20,10 +20,10 @@
assertEqual(isEmpty([1]), false)
</pre>
- </div>
- <div>
- <p> Checks for T5.arrays.isNonEmpty()
- </p>
+</div>
+<div>
+ <p> Checks for T5.arrays.isNonEmpty()
+ </p>
<pre>
assertEqual(isNonEmpty(null), false)
assertEqual(isNonEmpty(undefined), false)
@@ -31,59 +31,59 @@
false)
assertEqual(isNonEmpty([1]), true)
</pre>
- </div>
- <div>
- <p> Map() on null returns an empty array
- </p>
+</div>
+<div>
+ <p> Map() on null returns an empty array
+ </p>
<pre>
assertEqual(map(null, null), [])
</pre>
- </div>
+</div>
- <div>
- <p> Map() on undefined returns an empty array
+<div>
+ <p> Map() on undefined returns an empty array
</p>
<pre>
assertEqual(map(null, undefined), [])
</pre>
- </div>
+</div>
- <div>
- <p>Map() on empty array returns an empty array</p>
+<div>
+ <p>Map() on empty array returns an empty array</p>
<pre>
assertEqual(map(null, []), [])
</pre>
- </div>
+</div>
- <div>
- <p> The normal use of map().
+<div>
+ <p> The normal use of map().
</p>
<pre>
assertEqual(map(function(e) { return e + 100; }, [1, 2, 3]), [101,
102, 103])
</pre>
- </div>
+</div>
- <div>
- <p> Optional second parameter to map() is an index number. </p>
+<div>
+ <p> Optional second parameter to map() is an index number. </p>
<pre>
assertEqual(map(function(e, index) { return (e + 100) + "@" + index;
}, [1, 2, 3]),
["101@0", "102@1",
"103@2"])
</pre>
- </div>
+</div>
- <div>
- <p> The initial value is returned, as is, when no reducing occurs due to
empty/null/undefined list.
+<div>
+ <p> The initial value is returned, as is, when no reducing occurs due to
empty/null/undefined list.
</p>
<pre>
var initial = { };
assertSame(reduce(function() { }, initial, []), initial);
</pre>
- </div>
+</div>
- <div>
- <p> Show that the index of each element is passed to the reducer
function.</p>
+<div>
+ <p> Show that the index of each element is passed to the reducer
function.</p>
<pre>
assertEqual(reduce(function(o, value, index) {
o[value] = index; return o;
@@ -92,44 +92,44 @@
{ fred: 0, barney: 1, wilma: 2 })
</pre>
- </div>
+</div>
- <div>
- <p> Removing a value that does not exist in the array returns the array
unchanged.
+<div>
+ <p> Removing a value that does not exist in the array returns the array
unchanged.
</p>
<pre>
var initial = [1, 2, 3]
assertSame(without(9, initial), initial)
</pre>
- </div>
+</div>
- <div>
- <p> Multiple matches should all be removed.</p>
+<div>
+ <p> Multiple matches should all be removed.</p>
<pre>
assertEqual(without(2, [1, 2, 2, 3, 4, 2, 5]), [1, 3, 4, 5])
</pre>
- </div>
+</div>
- <div>
- <p> The array should not be changed even if values are removed from the
result.
-</p>
+<div>
+ <p> The array should not be changed even if values are removed from the
result.
+ </p>
<pre>
var initial = [1, 2, 3]
without(2, initial)
assertEqual(initial, [1, 2, 3])
</pre>
- </div>
+</div>
- <div>
- <p>mapcat() on empty/null/undefined returns empty list.
+<div>
+ <p>mapcat() on empty/null/undefined returns empty list.
</p>
<pre>
assertEqual(mapcat(null,null), [])</pre>
- </div>
+</div>
- <div>
- <p> Basic execution of mapcat().
- </p>
+<div>
+ <p> Basic execution of mapcat().
+ </p>
<pre>
assertEqual(mapcat(function (value) {
var out = []
@@ -138,19 +138,19 @@
}, [1, 3, 5]),
[1, 3, 3, 3, 5, 5, 5, 5, 5])
</pre>
- </div>
+</div>
- <div>
- <p>filter() a list.</p>
+<div>
+ <p>filter() a list.</p>
<pre>
assertEqual(filter(function(value) {
return value % 2 === 0;
}, [1, 2, 3, 4, 5, 6, 7, 8, 9]), [2, 4, 6, 8])
</pre>
- </div>
+</div>
- <div>
- <p>
+<div>
+ <p>
Check that filter passes an optional index argument.
</p>
<pre>
@@ -160,10 +160,10 @@
8, 9]),
[1, 2, 4, 6, 7, 8])
</pre>
- </div>
+</div>
- <div>
- <p>remove() elements from a list.</p>
+<div>
+ <p>remove() elements from a list.</p>
<pre>
assertEqual(remove(function(value, index) {
return value % 2 !== 0 && index % 3 !== 0;
@@ -172,56 +172,56 @@
5, 6, 7, 8, 9]),
[1, 2, 4, 6, 7, 8])
</pre>
- </div>
+</div>
- <div>
- <p>first() on empty list returns null.</p>
+<div>
+ <p>first() on empty list returns null.</p>
<pre>
assertEqual(first(null, null), null)
</pre>
- </div>
+</div>
- <div>
- <p>first() match</p>
+<div>
+ <p>first() match</p>
<pre>
assertEqual(first(function(value) { return value > 3; }, [1, 2, 3,
4, 5]), 4)
</pre>
- </div>
+</div>
- <div>
- <p>first() passes the index as the second value.</p>
+<div>
+ <p>first() passes the index as the second value.</p>
<pre>
assertEqual(first(function(value, index) { return index > 2 }, [1,
2, 3, 4, 5]), 4)
</pre>
- <div>
+ <div>
<p>extractProperty() returns a function to extract a named
property.</p>
<pre>
var fn = T5.arrays.extractProperty('name')
assertEqual(fn({name: 'Tapestry', id: 5}), 'Tapestry')
</pre>
- </div>
+ </div>
- <div>
+ <div>
<p>
- Extract a property from an array.
-</p>
+ Extract a property from an array.
+ </p>
<pre>
assertEqual(extract('id', [ { id: 7, name: "Fred" }, { id: 22, name:
"Barney" }]), [7, 22])
</pre>
- </div>
-
</div>
- <h2>Publish / Subscribe</h2>
+</div>
+<h2>Publish / Subscribe</h2>
- <div>
- <p> Ensure that the message object passed to pub() is delivered to the
listener.</p>
+
+<div>
+ <p> Ensure that the message object passed to pub() is delivered to the
listener.</p>
<pre>
var message = { }
var rcvd;
@@ -232,10 +232,21 @@
assertSame(rcvd, message)
</pre>
- </div>
+</div>
- <div>
- <p>Check that the function returned from sub() can be used to cancel
message reciept.</p>
+<div>
+ <p> Ensure that the second object to a listener is the source object.</p>
+ <pre>
+ var unsub = sub("foo", null, function(message, element) {
assertSame(element, document); })
+
+ pub("foo", document, null)
+
+ unsub()
+ </pre>
+</div>
+
+<div>
+ <p>Check that the function returned from sub() can be used to cancel
message receipt.</p>
<pre>
var count = 0;
@@ -257,11 +268,11 @@
pub("foo", document);
assertEqual(count, 2)
</pre>
- </div>
+</div>
- <div>
- <p> Ensure that messages are delivered to specific listeners before
general listeners.
- </p>
+<div>
+ <p> Ensure that messages are delivered to specific listeners before
general listeners.
+ </p>
<pre>
var pubs = []
@@ -283,10 +294,10 @@
"general1",
"general2"])
</pre>
- </div>
+</div>
- <div>
- <p>Removing an element also removes publishers and subscribers for the
element.</p>
+<div>
+ <p>Removing an element also removes publishers and subscribers for the
element.</p>
<pre>
var pubs = []
@@ -317,10 +328,10 @@
assertEqual(pubs, ["general"])
</pre>
- </div>
+</div>
- <div>
- <p>Publish/subscribe via client id rather than element.</p>
+<div>
+ <p>Publish/subscribe via client id rather than element.</p>
<pre>
var element = new Element("span",{ id: "zaphod" }).update("Published
Message Source Zaphod")
@@ -341,12 +352,12 @@
T5.dom.remove(element)
</pre>
- </div>
+</div>
- </div>
+</div>
- <script>
-<![CDATA[
+<script>
+ <![CDATA[
isEmpty = T5.arrays.isEmpty
isNonEmpty = T5.arrays.isNonEmpty
map = T5.arrays.map
@@ -367,6 +378,6 @@
JST.runTestSuite("tests");
]]>
- </script>
+</script>
</html>
Modified:
tapestry/tapestry5/trunk/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/js-testing.js
URL:
http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/js-testing.js?rev=1148123&r1=1148122&r2=1148123&view=diff
==============================================================================
---
tapestry/tapestry5/trunk/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/js-testing.js
(original)
+++
tapestry/tapestry5/trunk/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/js-testing.js
Tue Jul 19 01:32:26 2011
@@ -106,6 +106,9 @@ var JST = (function() {
test.addClassName("pass");
} catch (e) {
+
+ Tapestry.error(e)
+
failCount++;
if (e !== $fail) {