From 12295427622edf16a5692cacae6b195a7d1091dc Mon Sep 17 00:00:00 2001 From: lin onetwo Date: Sun, 16 Mar 2025 00:29:44 +0800 Subject: [PATCH] feat: add removeEventListener , and allow register multiple listeners Instead of remove old one when add new one. --- core/modules/widgets/widget.js | 56 +++-- .../test/tiddlers/tests/test-widget-event.js | 198 ++++++++++++++++++ 2 files changed, 239 insertions(+), 15 deletions(-) create mode 100644 editions/test/tiddlers/tests/test-widget-event.js diff --git a/core/modules/widgets/widget.js b/core/modules/widgets/widget.js index 14e90ba2d..03bcd068a 100755 --- a/core/modules/widgets/widget.js +++ b/core/modules/widgets/widget.js @@ -628,36 +628,62 @@ Widget.prototype.addEventListeners = function(listeners) { }; /* -Add an event listener +Add an event listener. Listener could return a boolean indicating whether +to further propagation or not. */ Widget.prototype.addEventListener = function(type,handler) { var self = this; - if(typeof handler === "string") { // The handler is a method name on this widget - this.eventListeners[type] = function(event) { - return self[handler].call(self,event); - }; - } else { // The handler is a function - this.eventListeners[type] = function(event) { - return handler.call(self,event); - }; + var listenerWrapper; + if(typeof handler === "string") { + // keep the original function for comparing when remove. + listenerWrapper = { original: handler, listener: function(event) { return self[handler].call(self,event); } }; + } else { + listenerWrapper = { original: handler, listener: function(event) { return handler.call(self,event); } }; } + this.eventListeners[type] = this.eventListeners[type] || []; + this.eventListeners[type].push(listenerWrapper); }; /* -Dispatch an event to a widget. If the widget doesn't handle the event then it is also dispatched to the parent widget +Remove an event listener +*/ +Widget.prototype.removeEventListener = function(type,handler) { + if(!this.eventListeners[type]) return; + var self = this; + $tw.utils.each(this.eventListeners[type].slice(), function(listener) { + if(listener.original === handler) { + var index = self.eventListeners[type].indexOf(listener); + if(index !== -1) { + self.eventListeners[type].splice(index,1); + } + } + }); +}; + +/* +Dispatch an event to a widget. If the widget doesn't handle the event then it is also dispatched to the parent widget. + +An event listener can return a boolean "propagate" value, indicating whether to stop propagation. By default it is false (stop propagation). */ Widget.prototype.dispatchEvent = function(event) { event.widget = event.widget || this; // Dispatch the event if this widget handles it - var listener = this.eventListeners[event.type]; - if(listener) { - // Don't propagate the event if the listener returned false - if(!listener(event)) { + var listeners = this.eventListeners[event.type]; + if(listeners) { + // Don't propagate the event if any of the listeners returned false + var self = this; + var shouldPropagate = true; + $tw.utils.each(listeners, function(listener) { + if(!listener.listener(event)) { + shouldPropagate = false; + } + }); + if (!shouldPropagate) { return false; } } // Dispatch the event to the parent widget - if(this.parentWidget) { + if (this.parentWidget) { return this.parentWidget.dispatchEvent(event); } return true; diff --git a/editions/test/tiddlers/tests/test-widget-event.js b/editions/test/tiddlers/tests/test-widget-event.js new file mode 100644 index 000000000..123679757 --- /dev/null +++ b/editions/test/tiddlers/tests/test-widget-event.js @@ -0,0 +1,198 @@ +/*\ +title: test-widget-event.js +type: application/javascript +tags: [[$:/tags/test-spec]] +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +describe("Widget Event Listeners", function() { + var widget = require("$:/core/modules/widgets/widget.js"); + + function createWidgetNode(parseTreeNode,wiki,parentWidget) { + return new widget.widget(parseTreeNode,{ + wiki: wiki, + document: $tw.fakeDocument, + parentWidget: parentWidget + }); + } + + it("should call all added event listeners on dispatchEvent", function() { + var calls = []; + var wiki = new $tw.Wiki(); + var widget = createWidgetNode({type:"widget", text:"text"}, wiki); + + // Add a function listener. + widget.addEventListener("testEvent", function(e) { + calls.push("funcListener"); + return true; + }); + // Setup a method on widget for string listener. + widget.testHandler = function(e) { + calls.push("methodListener"); + return true; + }; + widget.addEventListener("testEvent", "testHandler"); + + var event = {type:"testEvent"}; + var result = widget.dispatchEvent(event); + expect(result).toBe(true); + expect(calls.length).toBe(2); + expect(calls).toContain("funcListener"); + expect(calls).toContain("methodListener"); + }); + + it("should remove an event listener correctly", function() { + var calls = []; + var wiki = new $tw.Wiki(); + var widget = createWidgetNode({type:"widget", text:"text"}, wiki); + + function listener(e) { + calls.push("listener"); + return true; + } + // Add listener twice: once as function and then add another distinct listener. + widget.addEventListener("removeTest", listener); + widget.addEventListener("removeTest", function(e) { + calls.push("secondListener"); + return true; + }); + // Remove the function listener. + widget.removeEventListener("removeTest", listener); + + var event = {type:"removeTest"}; + var result = widget.dispatchEvent(event); + expect(result).toBe(true); + expect(calls.length).toBe(1); + expect(calls).toContain("secondListener"); + expect(calls).not.toContain("listener"); + }); + + it("stop further propagation by returns false won't block other listeners on the same level.", function() { + var calls = []; + var wiki = new $tw.Wiki(); + var widget = createWidgetNode({type:"widget", text:"text"}, wiki); + + widget.addEventListener("stopEvent", function(e) { + calls.push("first"); + // stops further propagation, but still dispatch event for second listener. + return false; + }); + widget.addEventListener("stopEvent", function(e) { + calls.push("second"); + return true; + }); + var event = {type:"stopEvent"}; + var result = widget.dispatchEvent(event); + expect(result).toBe(false); + expect(calls.length).toBe(2); + expect(calls).toContain("first"); + expect(calls).toContain("second"); + }); + + it("should dispatch event to parent widget if not handled on child", function() { + var parentCalls = []; + var wiki = new $tw.Wiki(); + var parentWidget = createWidgetNode({type:"widget", text:"text"}, wiki); + parentWidget.addEventListener("parentEvent", function(e) { + parentCalls.push("parentListener"); + return true; + }); + // Create a child with parentWidget assigned. + var childWidget = createWidgetNode({type:"widget", text:"text"}, wiki, parentWidget); + // No listener on child; so dispatch should bubble up. + var event = {type:"parentEvent"}; + var result = childWidget.dispatchEvent(event); + expect(result).toBe(true); + expect(parentCalls.length).toBe(1); + expect(parentCalls).toContain("parentListener"); + }); + + it("should not dispatch event to parent if child's listener stops propagation", function() { + var parentCalls = []; + var wiki = new $tw.Wiki(); + var parentWidget = createWidgetNode({type:"widget", text:"text"}, wiki); + parentWidget.addEventListener("bubbleTest", function(e) { + parentCalls.push("parentListener"); + return true; + }); + var childWidget = createWidgetNode({type:"widget", text:"text"}, wiki, parentWidget); + childWidget.addEventListener("bubbleTest", function(e) { + return false; // Stop event propagation + }); + var event = {type:"bubbleTest"}; + var result = childWidget.dispatchEvent(event); + expect(result).toBe(false); + expect(parentCalls.length).toBe(0); + }); + + it("should call multiple listeners in proper order across child and parent", function() { + var calls = []; + var wiki = new $tw.Wiki(); + var parentWidget = createWidgetNode({type:"widget", text:"text"}, wiki); + parentWidget.addEventListener("chainEvent", function(e) { + calls.push("parentListener"); + return true; + }); + var childWidget = createWidgetNode({type:"widget", text:"text"}, wiki, parentWidget); + childWidget.addEventListener("chainEvent", function(e) { + calls.push("childListener"); + return true; + }); + var event = {type:"chainEvent"}; + var result = childWidget.dispatchEvent(event); + expect(result).toBe(true); + expect(calls.length).toBe(2); + // First call from child widget and then parent's listener. + expect(calls[0]).toBe("childListener"); + expect(calls[1]).toBe("parentListener"); + }); + + // Additional tests for multiple event types + it("should handle events of different types separately", function() { + var callsA = []; + var callsB = []; + var wiki = new $tw.Wiki(); + var widget = createWidgetNode({type:"widget", text:"text"}, wiki); + widget.addEventListener("eventA", function(e) { + callsA.push("A1"); + return true; + }); + widget.addEventListener("eventB", function(e) { + callsB.push("B1"); + return true; + }); + widget.dispatchEvent({type:"eventA"}); + widget.dispatchEvent({type:"eventB"}); + expect(callsA).toContain("A1"); + expect(callsB).toContain("B1"); + }); + + // Test using $tw.utils.each in removeEventListener internally (behavior verified via dispatch) + it("should remove listeners using $tw.utils.each without affecting other listeners", function() { + var calls = []; + var wiki = new $tw.Wiki(); + var widget = createWidgetNode({type:"widget", text:"text"}, wiki); + function listener1(e) { + calls.push("listener1"); + return true; + } + function listener2(e) { + calls.push("listener2"); + return true; + } + widget.addEventListener("testRemove", listener1); + widget.addEventListener("testRemove", listener2); + widget.removeEventListener("testRemove", listener1); + widget.dispatchEvent({type:"testRemove"}); + expect(calls.length).toBe(1); + expect(calls).toContain("listener2"); + expect(calls).not.toContain("listener1"); + }); + +}); + +})();