Author: hlship Date: Tue Jul 19 01:32:53 2011 New Revision: 1148124 URL: http://svn.apache.org/viewvc?rev=1148124&view=rev Log: TAP5-999: Add a message topic to allow the underlying framework to remove event handlers on DOM elements
Added: tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/t5-events.js - copied, changed from r1148123, tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/t5-prototype.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-dom.js tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/t5-prototype.js 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/js-testing.js 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=1148124&r1=1148123&r2=1148124&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 Tue Jul 19 01:32:53 2011 @@ -61,7 +61,8 @@ public class CoreJavaScriptStack impleme // Uses functions defined by the prior three. // Order is important, there are some dependencies - // going on here. + // going on here. Switching over to a more managed module system + // is starting to look like a really nice idea! ROOT + "/t5-core.js", @@ -77,6 +78,8 @@ public class CoreJavaScriptStack impleme ROOT + "/t5-pubsub.js", + ROOT + "/t5-events.js", + ROOT + "/t5-dom.js", ROOT + "/t5-console.js", Modified: 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=1148124&r1=1148123&r2=1148124&view=diff ============================================================================== --- tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/t5-dom.js (original) +++ tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/t5-dom.js Tue Jul 19 01:32:53 2011 @@ -15,98 +15,107 @@ T5.define("dom", function() { - /** - * Locates an element. If element is a string, then - * document.getElementById() is used to resolve a client element id to a DOM - * element. If the id does not exist, then null will be returned. - * <p> - * If element is not a string, it is presumed to already by a DOM element, - * and is returned. - */ - function locate(element) { - if (typeof element == "string") { - return document.getElementById(element); - } - - return element; // may be null, otherwise presumed to be a DOM node - } - - /** - * Tree-walks the children of the element; for each dhild, ensure that all - * event handlers, listeners and PubSub publishers for the child are - * removed. - */ - 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. - // May generalize this to be a published message instead, for - // cross-library compatibility. - 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, - locate : locate - }; + var removeEventHandlers; + + // Necessary to lazy-instantiate femoveEventHandlers publisher function, + // due to load order of these namespaces. + function doRemoveEventHandlers(element) { + if (!removeEventHandlers) { + removeEventHandlers = T5.pubsub.createPublisher(T5.events.REMOVE_EVENT_HANDLERS, document); + } + + removeEventHandlers(element); + } + + /** + * Locates an element. If element is a string, then + * document.getElementById() is used to resolve a client element id to a DOM + * element. If the id does not exist, then null will be returned. + * <p> + * If element is not a string, it is presumed to already by a DOM element, + * and is returned. + */ + function locate(element) { + if (typeof element == "string") { + return document.getElementById(element); + } + + return element; // may be null, otherwise presumed to be a DOM node + } + + /** + * Tree-walks the children of the element; for each dhild, ensure that all + * event handlers, listeners and PubSub publishers for the child are + * removed. + */ + 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; + } + } + } + } + + purgeChildren(element); + + if (element.t5pubsub) { + // TODO: Execute this deferred? + T5.pubsub.cleanupRemovedElement(element); + } + + doRemoveEventHandlers(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, + locate : locate + }; }); /** * Create a T5.$() synonym for T5.dom.locate(). */ T5.extend(T5, { - $ : T5.dom.locate + $ : T5.dom.locate }); \ No newline at end of file Copied: tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/t5-events.js (from r1148123, tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/t5-prototype.js) URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/t5-events.js?p2=tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/t5-events.js&p1=tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/t5-prototype.js&r1=1148123&r2=1148124&rev=1148124&view=diff ============================================================================== --- tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/t5-prototype.js (original) +++ tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/t5-events.js Tue Jul 19 01:32:53 2011 @@ -14,11 +14,15 @@ */ /** - * Adapts Tapestry's SPI (Service Provider Interface) to make use of the - * Prototype JavaScript library. May also make modifications to Prototype to - * work with Tapestry. + * Defines the names of events used with the publish/subscribe framework. */ -T5.extend(T5.spi, function() { +T5.define("events", { + + /** + * Published as an element is being removed from the DOM, to allow framework-specific + * approaches to removing any event listeners for the element. This is published on the document object, + * and the message is the DOM element for which event handlers should be removed. + */ + REMOVE_EVENT_HANDLERS : "tapestry:remove-event-handlers" - return {}; }); \ No newline at end of file Modified: tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/t5-prototype.js URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/t5-prototype.js?rev=1148124&r1=1148123&r2=1148124&view=diff ============================================================================== --- tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/t5-prototype.js (original) +++ tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/t5-prototype.js Tue Jul 19 01:32:53 2011 @@ -20,5 +20,13 @@ */ T5.extend(T5.spi, function() { - return {}; -}); \ No newline at end of file + document.observe("dom:loaded", function() { + T5.sub(T5.events.REMOVE_EVENT_HANDLERS, null, function(element) { + Event.stopObserving(element); + } + ); + }); + + return {}; +}); + 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=1148124&r1=1148123&r2=1148124&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:53 2011 @@ -1,5 +1,4 @@ -T5.define("pubsub", function() -{ +T5.define("pubsub", function() { var arrays = T5.arrays; var first = arrays.first; @@ -18,36 +17,28 @@ T5.define("pubsub", function() var publishers = []; // Necessary since T5.dom depends on T5.pubsub - function $(element) - { + function $(element) { return T5.$(element); } - function purgePublisherCache(topic) - { - each(function(publisher) - { - if (publisher.topic === topic) - { + function purgePublisherCache(topic) { + each(function(publisher) { + if (publisher.topic === topic) { publisher.listeners = undefined; } }, publishers); } - function findListeners(topic, element) - { - var gross = filter(function(subscriber) - { + function findListeners(topic, element) { + var gross = filter(function(subscriber) { return subscriber.topic === topic; }, subscribers); - var primary = filter(function(subscriber) - { + var primary = filter(function(subscriber) { return subscriber.element === element; }, gross); - var secondary = filter(function(subscriber) - { + var secondary = filter(function(subscriber) { // Match where the element is null or undefined return !subscriber.element; }, gross); @@ -82,8 +73,7 @@ T5.define("pubsub", function() * 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) - { + function subscribe(topic, element, listenerfn) { var subscriber = { topic : topic, @@ -101,8 +91,7 @@ T5.define("pubsub", function() listenerfn = null; // Return a function to unsubscribe - return function() - { + return function() { subscribers = without(subscriber, subscribers); purgePublisherCache(subscriber.topic); } @@ -139,40 +128,33 @@ T5.define("pubsub", function() * the second parameter. * @return publisher function used to publish a message */ - function createPublisher(topic, element) - { + function createPublisher(topic, element) { element = $(element); - if (element == null) - { + if (element == null) { throw "Element may not be null when creating a publisher."; } - var existing = first(function(publisher) - { + var existing = first(function(publisher) { return publisher.topic === topic && publisher.element === element; }, publishers); - if (existing) - { + if (existing) { return existing.publisherfn; } var publisher = { topic : topic, element : element, - publisherfn : function(message) - { + publisherfn : function(message) { - if (publisher.listeners == undefined) - { + if (publisher.listeners == undefined) { publisher.listeners = findListeners(publisher.topic, publisher.element); } - return map(function(listenerfn) - { + return map(function(listenerfn) { return listenerfn(message, publisher.element); }, publisher.listeners); } @@ -201,8 +183,7 @@ T5.define("pubsub", function() * Creates a publisher and immediately publishes the message, return the * array of results. */ - function publish(topic, element, message) - { + function publish(topic, element, message) { return createPublisher(topic, element)(message); } @@ -210,22 +191,18 @@ T5.define("pubsub", function() * 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) - { + 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) - { + publishers = remove(function(publisher) { var match = publisher.element === element; - if (match) - { + if (match) { publisher.listeners = undefined; publisher.element = undefined; } 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=1148124&r1=1148123&r2=1148124&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:53 2011 @@ -1,158 +1,158 @@ var JST = (function() { - /* - * Original script title/version: Object.identical.js/1.11 Copyright (c) - * 2011, Chris O'Brien, prettycode.org - * http://github.com/prettycode/Object.identical.js - * - * LICENSE: Permission is hereby granted for unrestricted use, modification, - * and redistribution of this script, ONLY under the condition that this - * code comment is kept wholly complete, appearing above the script's code - * body--in all original or modified implementations of this script, except - * those that are minified. - */ + /* + * Original script title/version: Object.identical.js/1.11 Copyright (c) + * 2011, Chris O'Brien, prettycode.org + * http://github.com/prettycode/Object.identical.js + * + * LICENSE: Permission is hereby granted for unrestricted use, modification, + * and redistribution of this script, ONLY under the condition that this + * code comment is kept wholly complete, appearing above the script's code + * body--in all original or modified implementations of this script, except + * those that are minified. + */ - /* - * Requires ECMAScript 5 functions: - Array.isArray() - Object.keys() - - * Array.prototype.forEach() - JSON.stringify() - */ + /* + * Requires ECMAScript 5 functions: - Array.isArray() - Object.keys() - + * Array.prototype.forEach() - JSON.stringify() + */ - function identical(a, b, sortArrays) { + function identical(a, b, sortArrays) { - function sort(o) { + function sort(o) { - if (sortArrays === true && Array.isArray(o)) { - return o.sort(); - } else if (typeof o !== "object" || o === null) { - return o; - } + if (sortArrays === true && Array.isArray(o)) { + return o.sort(); + } else if (typeof o !== "object" || o === null) { + return o; + } - var result = {}; + var result = {}; - Object.keys(o).sort().forEach(function(key) { - result[key] = sort(o[key]); - }); + Object.keys(o).sort().forEach(function(key) { + result[key] = sort(o[key]); + }); - return result; - } + return result; + } - return JSON.stringify(sort(a)) === JSON.stringify(sort(b)); - } + return JSON.stringify(sort(a)) === JSON.stringify(sort(b)); + } - var resultElement; + var resultElement; - var $fail = {}; + var $fail = {}; - function fail(text) { - resultElement.insert({ - top : "FAIL - ", - after : "<hr>" + text - }).up("div").addClassName("fail").scrollTo(); + function fail(text) { + resultElement.insert({ + top : "FAIL - ", + after : "<hr>" + text + }).up("div").addClassName("fail").scrollTo(); - throw $fail; - } + throw $fail; + } - function toString(value) { + function toString(value) { - return Object.toJSON(value); - } + return Object.toJSON(value); + } - function failNotEqual(actual, expected) { - fail(toString(actual) + " !== " + toString(expected)); - } + function failNotEqual(actual, expected) { + fail(toString(actual) + " !== " + toString(expected)); + } - function assertSame(actual, expected) { - if (actual !== expected) { - failNotEqual(actual, expected); - } - } + function assertSame(actual, expected) { + if (actual !== expected) { + failNotEqual(actual, expected); + } + } - function assertEqual(actual, expected) { + function assertEqual(actual, expected) { - if (!identical(actual, expected)) { - failNotEqual(actual, expected); - } - } + if (!identical(actual, expected)) { + failNotEqual(actual, expected); + } + } - function doRunTests(elementId) { + function doRunTests(elementId) { - var passCount = 0; - var failCount = 0; + var passCount = 0; + var failCount = 0; - $(elementId).addClassName("js-results").select("div:odd").each( - function(e) { - e.addClassName("odd"); - }); + $(elementId).addClassName("js-results").select("div:odd").each( + function(e) { + e.addClassName("odd"); + }); - $(elementId).select("div").each( - function(test) { + $(elementId).select("div").each( + function(test) { - test.addClassName("active"); + test.addClassName("active"); - resultElement = test.down("p"); - resultNoted = false; + resultElement = test.down("p"); + resultNoted = false; - var testCode = test.down("pre").textContent; + var testCode = test.down("pre").textContent; - try { - eval(testCode); + try { + eval(testCode); - passCount++; + passCount++; - resultElement.insert({ - top : "PASS - " - }); + resultElement.insert({ + top : "PASS - " + }); - test.addClassName("pass"); + test.addClassName("pass"); - } catch (e) { + } catch (e) { - Tapestry.error(e) + Tapestry.error(e) - failCount++; + failCount++; - if (e !== $fail) { - resultElement.next().insert( - { - top : "EXCEPTION - ", - after : "<hr><div class='exception'>" - + toString(e) + "</div>" - }); + if (e !== $fail) { + resultElement.next().insert( + { + top : "EXCEPTION - ", + after : "<hr><div class='exception'>" + + toString(e) + "</div>" + }); - test.addClassName("fail").scrollTo(); - } - } + test.addClassName("fail").scrollTo(); + } + } - test.removeClassName("active"); - }); + test.removeClassName("active"); + }); - $(elementId) - .insert( - { - top : "<p class='caption #{class}'>Results: #{pass} passed / #{fail} failed</p>" - .interpolate({ - class : failCount == 0 ? "pass" - : "fail", - pass : passCount, - fail : failCount - }) - }); + $(elementId) + .insert( + { + top : "<p class='caption #{class}'>Results: #{pass} passed / #{fail} failed</p>" + .interpolate({ + class : failCount == 0 ? "pass" + : "fail", + pass : passCount, + fail : failCount + }) + }); - if (failCount == 0) { - $(elementId).down("p").scrollTo(); - } - } + if (failCount == 0) { + $(elementId).down("p").scrollTo(); + } + } - function runTestSuite(elementId) { - Tapestry.onDOMLoaded(function() { - doRunTests(elementId); - }); - } + function runTestSuite(elementId) { + Tapestry.onDOMLoaded(function() { + doRunTests(elementId); + }); + } - return { - fail : fail, - assertEqual : assertEqual, - assertSame : assertSame, - runTestSuite : runTestSuite - }; + return { + fail : fail, + assertEqual : assertEqual, + assertSame : assertSame, + runTestSuite : runTestSuite + }; })(); \ No newline at end of file