diff --git a/packages/interaction/src/InteractionEvent.js b/packages/interaction/src/InteractionEvent.js index 59f602d..68d6945 100644 --- a/packages/interaction/src/InteractionEvent.js +++ b/packages/interaction/src/InteractionEvent.js @@ -9,13 +9,34 @@ constructor() { /** - * Whether this event will continue propagating in the tree + * Whether this event will continue propagating in the tree. + * + * Remaining events for the {@link stopsPropagatingAt} object + * will still be dispatched. * * @member {boolean} */ this.stopped = false; /** + * At which object this event stops propagating. + * + * @private + * @member {PIXI.DisplayObject} + */ + this.stopsPropagatingAt = null; + + /** + * Whether we already reached the element we want to + * stop propagating at. This is important for delayed events, + * where we start over deeper in the tree again. + * + * @private + * @member {boolean} + */ + this.stopPropagationHint = false; + + /** * The object which caused this event to be dispatched. * For listener callback see {@link PIXI.interaction.InteractionEvent.currentTarget}. * @@ -52,6 +73,8 @@ stopPropagation() { this.stopped = true; + this.stopPropagationHint = true; + this.stopsPropagatingAt = this.currentTarget; } /** @@ -60,6 +83,8 @@ reset() { this.stopped = false; + this.stopsPropagatingAt = null; + this.stopPropagationHint = false; this.currentTarget = null; this.target = null; } diff --git a/packages/interaction/src/InteractionEvent.js b/packages/interaction/src/InteractionEvent.js index 59f602d..68d6945 100644 --- a/packages/interaction/src/InteractionEvent.js +++ b/packages/interaction/src/InteractionEvent.js @@ -9,13 +9,34 @@ constructor() { /** - * Whether this event will continue propagating in the tree + * Whether this event will continue propagating in the tree. + * + * Remaining events for the {@link stopsPropagatingAt} object + * will still be dispatched. * * @member {boolean} */ this.stopped = false; /** + * At which object this event stops propagating. + * + * @private + * @member {PIXI.DisplayObject} + */ + this.stopsPropagatingAt = null; + + /** + * Whether we already reached the element we want to + * stop propagating at. This is important for delayed events, + * where we start over deeper in the tree again. + * + * @private + * @member {boolean} + */ + this.stopPropagationHint = false; + + /** * The object which caused this event to be dispatched. * For listener callback see {@link PIXI.interaction.InteractionEvent.currentTarget}. * @@ -52,6 +73,8 @@ stopPropagation() { this.stopped = true; + this.stopPropagationHint = true; + this.stopsPropagatingAt = this.currentTarget; } /** @@ -60,6 +83,8 @@ reset() { this.stopped = false; + this.stopsPropagatingAt = null; + this.stopPropagationHint = false; this.currentTarget = null; this.target = null; } diff --git a/packages/interaction/src/InteractionManager.js b/packages/interaction/src/InteractionManager.js index e54445d..3b3081b 100644 --- a/packages/interaction/src/InteractionManager.js +++ b/packages/interaction/src/InteractionManager.js @@ -939,7 +939,9 @@ */ dispatchEvent(displayObject, eventString, eventData) { - if (!eventData.stopped) + // Even if the event was stopped, at least dispatch any remaining events + // for the same display object. + if (!eventData.stopPropagationHint || displayObject === eventData.stopsPropagatingAt) { eventData.currentTarget = displayObject; eventData.type = eventString; @@ -1158,15 +1160,25 @@ if (delayedEvents.length && !skipDelayed) { + // Reset the propagation hint, because we start deeper in the tree again. + interactionEvent.stopPropagationHint = false; + const delayedLen = delayedEvents.length; this.delayedEvents = []; for (let i = 0; i < delayedLen; i++) { - const delayed = delayedEvents[i]; + const { displayObject, eventString, eventData } = delayedEvents[i]; - this.dispatchEvent(delayed.displayObject, delayed.eventString, delayed.eventData); + // When we reach the object we wanted to stop propagating at, + // set the propagation hint. + if (eventData.stopsPropagatingAt === displayObject) + { + eventData.stopPropagationHint = true; + } + + this.dispatchEvent(displayObject, eventString, eventData); } } diff --git a/packages/interaction/src/InteractionEvent.js b/packages/interaction/src/InteractionEvent.js index 59f602d..68d6945 100644 --- a/packages/interaction/src/InteractionEvent.js +++ b/packages/interaction/src/InteractionEvent.js @@ -9,13 +9,34 @@ constructor() { /** - * Whether this event will continue propagating in the tree + * Whether this event will continue propagating in the tree. + * + * Remaining events for the {@link stopsPropagatingAt} object + * will still be dispatched. * * @member {boolean} */ this.stopped = false; /** + * At which object this event stops propagating. + * + * @private + * @member {PIXI.DisplayObject} + */ + this.stopsPropagatingAt = null; + + /** + * Whether we already reached the element we want to + * stop propagating at. This is important for delayed events, + * where we start over deeper in the tree again. + * + * @private + * @member {boolean} + */ + this.stopPropagationHint = false; + + /** * The object which caused this event to be dispatched. * For listener callback see {@link PIXI.interaction.InteractionEvent.currentTarget}. * @@ -52,6 +73,8 @@ stopPropagation() { this.stopped = true; + this.stopPropagationHint = true; + this.stopsPropagatingAt = this.currentTarget; } /** @@ -60,6 +83,8 @@ reset() { this.stopped = false; + this.stopsPropagatingAt = null; + this.stopPropagationHint = false; this.currentTarget = null; this.target = null; } diff --git a/packages/interaction/src/InteractionManager.js b/packages/interaction/src/InteractionManager.js index e54445d..3b3081b 100644 --- a/packages/interaction/src/InteractionManager.js +++ b/packages/interaction/src/InteractionManager.js @@ -939,7 +939,9 @@ */ dispatchEvent(displayObject, eventString, eventData) { - if (!eventData.stopped) + // Even if the event was stopped, at least dispatch any remaining events + // for the same display object. + if (!eventData.stopPropagationHint || displayObject === eventData.stopsPropagatingAt) { eventData.currentTarget = displayObject; eventData.type = eventString; @@ -1158,15 +1160,25 @@ if (delayedEvents.length && !skipDelayed) { + // Reset the propagation hint, because we start deeper in the tree again. + interactionEvent.stopPropagationHint = false; + const delayedLen = delayedEvents.length; this.delayedEvents = []; for (let i = 0; i < delayedLen; i++) { - const delayed = delayedEvents[i]; + const { displayObject, eventString, eventData } = delayedEvents[i]; - this.dispatchEvent(delayed.displayObject, delayed.eventString, delayed.eventData); + // When we reach the object we wanted to stop propagating at, + // set the propagation hint. + if (eventData.stopsPropagatingAt === displayObject) + { + eventData.stopPropagationHint = true; + } + + this.dispatchEvent(displayObject, eventString, eventData); } } diff --git a/packages/interaction/test/InteractionManager.js b/packages/interaction/test/InteractionManager.js index 8b3250a..5de94a2 100644 --- a/packages/interaction/test/InteractionManager.js +++ b/packages/interaction/test/InteractionManager.js @@ -184,6 +184,113 @@ }); }); + describe('event propagation', function () + { + it('should stop event propagation', function () + { + const stage = new Container(); + const parent = new Container(); + const graphics = new Graphics(); + + const pointer = this.pointer = new MockPointer(stage); + + const mouseDownChild = sinon.spy((evt) => evt.stopPropagation()); + const mouseDownParent = sinon.spy(); + + stage.addChild(parent); + parent.addChild(graphics); + + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + parent.interactive = true; + + graphics.on('mousedown', mouseDownChild); + + parent.on('mousedown', mouseDownParent); + + pointer.mousedown(10, 10); + + expect(mouseDownChild).to.have.been.called; + expect(mouseDownParent).to.not.have.been.called; + }); + + it('should not stop events on the same object from happening', function () + { + const stage = new Container(); + const parent = new Container(); + const graphics = new Graphics(); + + const pointer = this.pointer = new MockPointer(stage); + + // Neither of these should stop the other from firing + const mouseMoveChild = sinon.spy((evt) => evt.stopPropagation()); + const mouseOverChild = sinon.spy((evt) => evt.stopPropagation()); + + const mouseMoveParent = sinon.spy(); + const mouseOverParent = sinon.spy(); + + stage.addChild(parent); + parent.addChild(graphics); + + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + parent.interactive = true; + + graphics.on('mousemove', mouseMoveChild); + graphics.on('mouseover', mouseOverChild); + + parent.on('mousemove', mouseMoveParent); + parent.on('mouseover', mouseOverParent); + + pointer.mousemove(10, 10); + + expect(mouseOverChild).to.have.been.called; + expect(mouseMoveChild).to.have.been.called; + + expect(mouseOverParent).to.not.have.been.called; + expect(mouseMoveParent).to.not.have.been.called; + }); + + it('should not stop events on children of an object from happening', function () + { + const stage = new Container(); + const parent = new Container(); + const graphics = new Graphics(); + + const pointer = this.pointer = new MockPointer(stage); + + const mouseMoveChild = sinon.spy(); + const mouseMoveParent = sinon.spy((evt) => evt.stopPropagation()); + + const mouseOverChild = sinon.spy(); + const mouseOverParent = sinon.spy(); + + stage.addChild(parent); + parent.addChild(graphics); + + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + parent.interactive = true; + + graphics.on('mousemove', mouseMoveChild); + graphics.on('mouseover', mouseOverChild); + + parent.on('mousemove', mouseMoveParent); + parent.on('mouseover', mouseOverParent); + + pointer.mousemove(10, 10); + + expect(mouseMoveChild).to.have.been.called; + expect(mouseOverChild).to.have.been.called; + + expect(mouseMoveParent).to.have.been.called; + expect(mouseOverParent).to.have.been.called; + }); + }); + describe('touch vs pointer', function () { it('should call touchstart and pointerdown when touch event and pointer supported', function ()