diff --git a/packages/interaction/src/InteractionManager.js b/packages/interaction/src/InteractionManager.js index f4664af..e54445d 100644 --- a/packages/interaction/src/InteractionManager.js +++ b/packages/interaction/src/InteractionManager.js @@ -253,6 +253,14 @@ this.resolution = 1; /** + * Delayed pointer events. Used to guarantee correct ordering of over/out events. + * + * @private + * @member {Array} + */ + this.delayedEvents = []; + + /** * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * @@ -946,6 +954,20 @@ } /** + * Puts a event on a queue to be dispatched later. This is used to guarantee correct + * ordering of over/out events. + * + * @param {PIXI.Container|PIXI.Sprite|PIXI.TilingSprite} displayObject - the display object in question + * @param {string} eventString - the name of the event (e.g, mousedown) + * @param {object} eventData - the event data object + * @private + */ + delayDispatchEvent(displayObject, eventString, eventData) + { + this.delayedEvents.push({ displayObject, eventString, eventData }); + } + + /** * Maps x and y coords from a DOM object and maps them correctly to the PixiJS view. The * resulting value is stored in the point. This takes into account the fact that the DOM * element could be scaled and positioned anywhere on the screen. @@ -988,9 +1010,11 @@ * interactionEvent, displayObject and hit will be passed to the function * @param {boolean} [hitTest] - this indicates if the objects inside should be hit test against the point * @param {boolean} [interactive] - Whether the displayObject is interactive + * @param {boolean} [skipDelayed] - Whether to process delayed events before returning. This is + * used to avoid processing them too early during recursive calls. * @return {boolean} returns true if the displayObject hit the point */ - processInteractive(interactionEvent, displayObject, func, hitTest, interactive) + processInteractive(interactionEvent, displayObject, func, hitTest, interactive, skipDelayed) { if (!displayObject || !displayObject.visible) { @@ -1065,7 +1089,7 @@ const child = children[i]; // time to get recursive.. if this function will return if something is hit.. - const childHit = this.processInteractive(interactionEvent, child, func, hitTest, interactiveParent); + const childHit = this.processInteractive(interactionEvent, child, func, hitTest, interactiveParent, true); if (childHit) { @@ -1130,6 +1154,22 @@ } } + const delayedEvents = this.delayedEvents; + + if (delayedEvents.length && !skipDelayed) + { + const delayedLen = delayedEvents.length; + + this.delayedEvents = []; + + for (let i = 0; i < delayedLen; i++) + { + const delayed = delayedEvents[i]; + + this.dispatchEvent(delayed.displayObject, delayed.eventString, delayed.eventData); + } + } + return hit; } @@ -1583,10 +1623,10 @@ if (!trackingData.over) { trackingData.over = true; - this.dispatchEvent(displayObject, 'pointerover', interactionEvent); + this.delayDispatchEvent(displayObject, 'pointerover', interactionEvent); if (isMouse) { - this.dispatchEvent(displayObject, 'mouseover', interactionEvent); + this.delayDispatchEvent(displayObject, 'mouseover', interactionEvent); } } diff --git a/packages/interaction/src/InteractionManager.js b/packages/interaction/src/InteractionManager.js index f4664af..e54445d 100644 --- a/packages/interaction/src/InteractionManager.js +++ b/packages/interaction/src/InteractionManager.js @@ -253,6 +253,14 @@ this.resolution = 1; /** + * Delayed pointer events. Used to guarantee correct ordering of over/out events. + * + * @private + * @member {Array} + */ + this.delayedEvents = []; + + /** * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * @@ -946,6 +954,20 @@ } /** + * Puts a event on a queue to be dispatched later. This is used to guarantee correct + * ordering of over/out events. + * + * @param {PIXI.Container|PIXI.Sprite|PIXI.TilingSprite} displayObject - the display object in question + * @param {string} eventString - the name of the event (e.g, mousedown) + * @param {object} eventData - the event data object + * @private + */ + delayDispatchEvent(displayObject, eventString, eventData) + { + this.delayedEvents.push({ displayObject, eventString, eventData }); + } + + /** * Maps x and y coords from a DOM object and maps them correctly to the PixiJS view. The * resulting value is stored in the point. This takes into account the fact that the DOM * element could be scaled and positioned anywhere on the screen. @@ -988,9 +1010,11 @@ * interactionEvent, displayObject and hit will be passed to the function * @param {boolean} [hitTest] - this indicates if the objects inside should be hit test against the point * @param {boolean} [interactive] - Whether the displayObject is interactive + * @param {boolean} [skipDelayed] - Whether to process delayed events before returning. This is + * used to avoid processing them too early during recursive calls. * @return {boolean} returns true if the displayObject hit the point */ - processInteractive(interactionEvent, displayObject, func, hitTest, interactive) + processInteractive(interactionEvent, displayObject, func, hitTest, interactive, skipDelayed) { if (!displayObject || !displayObject.visible) { @@ -1065,7 +1089,7 @@ const child = children[i]; // time to get recursive.. if this function will return if something is hit.. - const childHit = this.processInteractive(interactionEvent, child, func, hitTest, interactiveParent); + const childHit = this.processInteractive(interactionEvent, child, func, hitTest, interactiveParent, true); if (childHit) { @@ -1130,6 +1154,22 @@ } } + const delayedEvents = this.delayedEvents; + + if (delayedEvents.length && !skipDelayed) + { + const delayedLen = delayedEvents.length; + + this.delayedEvents = []; + + for (let i = 0; i < delayedLen; i++) + { + const delayed = delayedEvents[i]; + + this.dispatchEvent(delayed.displayObject, delayed.eventString, delayed.eventData); + } + } + return hit; } @@ -1583,10 +1623,10 @@ if (!trackingData.over) { trackingData.over = true; - this.dispatchEvent(displayObject, 'pointerover', interactionEvent); + this.delayDispatchEvent(displayObject, 'pointerover', interactionEvent); if (isMouse) { - this.dispatchEvent(displayObject, 'mouseover', interactionEvent); + this.delayDispatchEvent(displayObject, 'mouseover', interactionEvent); } } diff --git a/packages/interaction/test/InteractionManager.js b/packages/interaction/test/InteractionManager.js index 0671e97..8b3250a 100644 --- a/packages/interaction/test/InteractionManager.js +++ b/packages/interaction/test/InteractionManager.js @@ -138,6 +138,50 @@ expect(eventSpy).to.have.been.called; }); + + it('should always call mouseout before mouseover', function () + { + const stage = new Container(); + const graphicsA = new Graphics(); + const graphicsB = new Graphics(); + + const mouseOverSpyA = sinon.spy(); + const mouseOutSpyA = sinon.spy(); + + const mouseOverSpyB = sinon.spy(); + const mouseOutSpyB = sinon.spy(); + + const pointer = this.pointer = new MockPointer(stage); + + stage.addChild(graphicsA); + graphicsA.beginFill(0xFFFFFF); + graphicsA.drawRect(0, 0, 50, 50); + graphicsA.interactive = true; + + graphicsA.on('mouseover', mouseOverSpyA); + graphicsA.on('mouseout', mouseOutSpyA); + + stage.addChild(graphicsB); + graphicsB.x = 25; + graphicsB.beginFill(0xFFFFFF); + graphicsB.drawRect(0, 0, 50, 50); + graphicsB.interactive = true; + + graphicsB.on('mouseover', mouseOverSpyB); + graphicsB.on('mouseout', mouseOutSpyB); + + pointer.mousemove(10, 10); + + expect(mouseOverSpyA).to.have.been.called; + + pointer.mousemove(40, 10); + + expect(mouseOutSpyA).to.have.been.calledImmediatelyBefore(mouseOverSpyB); + + pointer.mousemove(10, 10); + + expect(mouseOutSpyB).to.have.been.calledImmediatelyBefore(mouseOverSpyA); + }); }); describe('touch vs pointer', function ()