Is there any way to prevent mouse event propagation?

The following code:

app.mouse.on('mousedown', fn1);
app.mouse.on('mousedown', fn2);
app.mouse.on('mousedown', fn3);

Can I prevent fn2 and fn3 execute in fn1 ? Maybe something like:

function fn1(e) {
  e.stopPropagation();
  // do stuff
}

I think the most flexible way to implement this is to use prototype inheritance.

pc.MouseEvent.prototype.stopPropagation = function(e) {
    this._stopPropagation = true;
};
pc.TouchEvent.prototype.stopPropagation = function(e) {
    this._stopPropagation = true;
};

// copy from pc.events.fire
pc.events.fire2 = function(name, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8) {
    // ...
    for(var i = 0; (callbacks || this._callbackActive[name]) && (i < (callbacks || this._callbackActive[name]).length); i++) {
        var evt = (callbacks || this._callbackActive[name])[i];
        evt.callback.call(evt.scope, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8);

        if (evt.callback.once) {
            var ind = this._callbacks[name].indexOf(evt);
            if (ind !== -1) {
                if (this._callbackActive[name] === this._callbacks[name])
                    this._callbackActive[name] = this._callbackActive[name].slice();

                this._callbacks[name].splice(ind, 1);
            }
        }

        // Add the following line:
        if (arg1._stopPropagation) break;
    }

    if (! callbacks)
        this._callbackActive[name] = null;

    return this;
}

pc.app.mouse.fire = pc.events.fire2;
pc.app.touch.fire = pc.events.fire2;

So you can call e.stopPropagation() in fn1, and it will prevent fn2 and fn3 execute. :wink:

Note: The code above is not tested. Just in theory.

1 Like

Good point. :smiley:

I just test the code, it throw the following error:

Uncaught TypeError: Cannot read property 'prototype' of undefined

It seems pc.TouchEvent is not a public API. :disappointed_relieved:

I do not know the pc.TouchEvent is private API before. :scream:

But I think it should be a public API. Is there any reason to make it private ? And why pc.MouseEvent is public ? @yaustar @will

Even if something is not public API, you should be able to access the properties etc. There’s nothing in the engine that prevents/hides access.

For this particular scenario, rather than having 3 separate callbacks for mouse down, it would be best to have one callback and internally manage what should be called to the rest of the app. It’s much more controllable and manageable as it’s in one central place.

It’s public. Probably you’re using previous version of PlayCanvas?.. It was added recently.

What is private API? I’ve never heard about.

Anything with a leading ‘_’ is deemed as private API. You are free to use it but with the knowledge that it may change/modified/removed in the future. eg: https://github.com/playcanvas/engine/blob/master/src/math/curve.js#L193

@yaustar @mikefinch

It seems pc.TouchEvent is not expose to pc


    return {
        /**
        * @function
        * @name pc.getTouchTargetCoords
        * @description Similiar to {@link pc.getTargetCoords} for the MouseEvents.
        * This function takes a browser Touch object and returns the co-ordinates of the
        * touch relative to the target element.
        * @param {Touch} touch The browser Touch object
        * @returns {Object} The co-ordinates of the touch relative to the touch.target element. In the format {x, y}
        */
        getTouchTargetCoords: function (touch) {
            var totalOffsetX = 0;
            var totalOffsetY = 0;
            var canvasX = 0;
            var canvasY = 0;
            var target = touch.target;
            while (!(target instanceof HTMLElement)) {
                target = target.parentNode;
            }
            var currentElement = target;

            do {
                totalOffsetX += currentElement.offsetLeft - currentElement.scrollLeft;
                totalOffsetY += currentElement.offsetTop - currentElement.scrollTop;
                currentElement = currentElement.offsetParent;
            } while (currentElement);

            return {
                x: touch.pageX - totalOffsetX,
                y: touch.pageY - totalOffsetY
            };
        },

        TouchDevice: TouchDevice
    };

See https://github.com/playcanvas/engine/blob/master/src/input/touch.js#L155-L189

Oh, I thought it’s about private, like, for personal and organization accounts.

Yea, you have to use pc.TouchDevice in order to deal with it.

Also, pc.TouchEvent is just a wrapper for native one.

Yes, I know pc.TouchEvent is just a wrapper. However, pc.TouchDevice will pass it to event handlers.
In my implementation, I need to access pc.TouchEvent. :slightly_frowning_face:

IMHO, I agree with @scarlex for the flexibility of event.stopPropagation.
What’s more, if we can add priority for the event handlers, the event.stopPropagation will be great.

The problem with having priority for event handlers is that you are then having to maintain/debug the order in multiple source files rather than one.

1 Like

I prefer to not have a global ‘mouse event’ like this, which all objects subscribe to. it becomes difficult to manage as per your example. Further, it is difficult to order the input handling (for example, your UI might wish to intercept input before it gets passed onto the actual game). Sometimes object may want to obtain exclusive access to the input. eg. If a user drags inside the window, your ‘player controller’ might wish to exclusively obtain input until control has completed.

Instead I usually have a single ‘InputProvider’ that exclusively listens for input events, then delegates them to the appropriate system. Other systems register themselves with the input provider with a function similar to:

inputProvider.registerMouseHandler(InputLayer.UI, this);

Where the object implements an interface definition similar to (JSDoc definition):

/**
 * Interface for objects that handle input received from a mouse input device.
 * @interface IMouseInputHandler
 */

/**
 * Invoked when the user presses a button on the mouse device.
 * @function
 * @name IMouseInputHandler#onMouseDown
 * @param {MouseEventArgs} Description of the mouse event that caused the method to be invoked.
 * @returns {boolean} True if the event was consumed otherwise false.
 */

/**
 * Invoked when the user releases a button on the mouse device.
 * @function
 * @name IMouseInputHandler#onMouseUp
 * @param {MouseEventArgs} Description of the mouse event that caused the method to be invoked.
 * @returns {boolean} True if the event was consumed otherwise false.
 */

/**
 * Invoked when the user moves the mouse cursor.
 * @function
 * @name IMouseInputHandler#onMouseMove
 * @returns {boolean} True if the event was consumed otherwise false.
 */

The input provider then allows an object to ‘capture’ all input, when an event is received it checks the capture target and sends events there. If there’s no capture target it traverses each input layer, stopping when an object returns true.

2 Likes

Looks like you should execute specific method manually in your inputProvider ?

Yes, I’ve found using events for input response leads to uncontrolled chaos in a non-trivial application. Even if a ‘stopPropagation’ method is available, there is no control over ordering of invocation or any capacity for an object to obtain exclusive access to the input events.

As an example, the input provider would look similar to:

// This method responds to browser mouse-down event
InputProvider.prototype._onMouseDown = function(event) {
    if (this.captureTarget) {
        this.captureTarget.onMouseDown(event);
    } else {
        for (var queue of this._mouseQueue) {
            for (var handler of queue.handlers) {
                if (handler.onMouseDown(event)) {
                    return;
                }
            }
        }
    }
};

Of-course the actual behavior of the provider is entirely up-to the developer, if you only need the onMouseDown the interface can be redefined as is suitable. Though, personally, I find it rare enough that an object needs only one of the events, that I simply add empty stub-implementations of the entire interface. The number of registered objects is typically small enough that it isn’t a performance concern.

[edit] To clarify, input events are great for individual elements on a web-page. They only start to fall down when an entire application sources the events from the same place.

n!

I know what you mean.

But in framework layer, I prefer to decouple all the scripts. That means I don’t want users to specific which event handler to trigger manually when a click event trigger.

For example, I provide some useful features such as entity picking, gizmo controls, camera move(just like playcanvas editor or threejs editor) for developers. These features are based on mouse event handers, developers can enable these features or not. To simplify the usage of framework, I would do something like:

app.gizmos.create('transform');
app.gizmos.transform.attach(entity);

camera.script.create('orbitCamera');
camera.script.create('orbitCameraMouseInput');

All scripts should decouple with others.
That’s why I want to add event handler priority and event.stopPropagation.

That means I don’t want users to specific which event handler to
trigger manually when a click event trigger.

Not quite sure what you mean there sorry, there is no tight coupling using the input provider. Unless you are referring to the input provider itself, but that is no more tightly coupled than an object being tied to the ‘mousedown’ event and the mouse device.

Your ‘orbitCameraMouseInput’ (and any other scripts you’d like to respond to input) would register itself with the input provider during initialization instead of adding itself to the ‘mousedown’ event.

n!

Image you provide the following scripts for developers:

  • entityPicking.js
  • gizmoTransform.js
  • orbitCameraMouseInput.js

Developers can add them as required. If using InputProvider, they should set the event hander order manually. I think it is a bit verbose.