Matt Toschlog
Matt Toschlog

Reputation: 159

Getting a mousemove event from a component that doesn't want me to have it

I'm working on a React project and I'm using react-player to play a video. I want to get mousemove events while the video is playing, but react-player appears to be capturing the events and not propagating them. What can I do to get the events?

First I tried adding a handler to a div:

<div onMouseMove={() => console.log("mousemove")}>
 <ReactPlayer url={props.url} />
</div>

Then I tried using addEventListener:

document.getElementById("my-div").addEventListener("mousemove", () => console.log("mousemove"), true)

<div id="my-div">
  <ReactPlayer url={props.url} />
</div>

I was hoping that the addEventListener would work if I set the useCapture code to true, but it didn't help.

Upvotes: 2

Views: 2087

Answers (2)

Oleksandr Tyshchenko
Oleksandr Tyshchenko

Reputation: 471

Strictly speaking there is no way to provide event propagation the way you want it.

The reason is that ReactPlayer is rendered in a separate <iframe...> element that means that you are dealing with two independent DOM trees: one is in your main window context and another is in iframe context. Yet DOM events are propagated only within single DOM tree and are never being propagated outside of it.

In your specific case that means that mouse events that are happening over the player’s surface are completely processed by the code executed within iframe context and your main window will never even know about those events.

The discussed above solution with placing a kind of transparent div that would overlay the whole player’s surface won’t work either. In this case the “overlay div” would capture the mouse events indeed and these events would be properly propagated but only within the main window DOM tree. Yet DOM tree that is created within iframe would never know about these events. As you said you are aware of this already. This situation is simply opposite to the one described in the previous paragraph.

If you would have full control over the code that is run within iframe you would be able with a bit of efforts arrange events dispatching from main window to iframe using Window.postMessage() and [possibly] achieve desired results but ReactPlayer is a black box for you and this is not a working solution either.

So you have to either reject your idea of mouse event capturing or if you badly need to know that mouse pointer is moving over the players surface you have to invent other solution that is not based on [not existing] mouse event propagation between different DOM trees.

I’ve drafted a little POC component named SmartPlayer that implements one possible solution.

The basic idea is that the player is overlayed by other component named SmartOverlay that is intended to capture mouse events. This SmartOverlay is actually a grid of smaller “tiles” each of which has its own onMouseMove event handler. The trick is that once the tile’s onMouseMove event handler is fired it actually removes the tile from the DOM creating a “hole” in the overlay. Through that “hole” mouse events become “visible” to the player’s iframe. I know it sounds weird but I hope that you can get the whole picture from the code. You can actually see the “tiles” and the moving “hole” if you set “Show overlay” selector on.

Note that mouse move counter isn’t being changed while you move mouse within the “hole”. You can reduce this “granularity” by making tileHeight and tileWidth smaller but the price to be paid is a lower performance.

Strictly speaking it’s a kind of hack and I would think twice before using it in production. Though if you really need to catch the mouse event over the ReactPlayer it’s possibly the simpler solution you can get. There is some performance overhead in this solution but I tried to keep it acceptably low.

Good luck :)

P.S. I’ve had troubles making this code run as a code snippet. Hope to fix it somehow. Meanwhile I included the whole code directly in this answer.

To test the solution you may create a React application with create-react-app and then completely replace the App.js content with the code below.

I also I put the running version here: http://react-player.2358.com.ua/

import React, { Component } from 'react'
import ReactPlayer from 'react-player'

class SmartOverlay extends Component {

    constructor(props) {
        super(props);

        const {height, width, } = props;
        const tileHeight = props.tileHeight || 64;
        const tileWidth = props.tileWidth || 64;

        //  below we're creating an array of "tiles"
        //  these "tiles" together are intended to cover all the players sufrace
        this.overlayTiles = [];
        for (let top = 0; top < height; top += tileHeight) {
            for (let left = 0; left < width; left += tileWidth) {
                const elementHeight = Math.min(tileHeight, height - top);
                const elementWidth = Math.min(tileWidth, width - left);
                const tile = {top, left, elementHeight, elementWidth }

                //  for each tile its own 'mousmove' event handler is created and assigned as the tile's property
                tile.onMouseMove = () => this.onMouseMove(tile);    
                this.overlayTiles.push(tile);
            }
        }

        //  all the overlay tiles are "active" at the beginning
        this.state = {activeOverlayTiles: this.overlayTiles}
    }

    onMouseMove(currentTile) {
        //  call event handler that is passed to the SmartOvelay via its handleMouseMove property
        //  using setTimeout here isn't strictly necessary but I prefer that "external" handler was executed only after we exit current method
        setTimeout(this.props.handleMouseMove);     

        //  "remove" current tile from the activeOverlayTiles array (that will cause removing the tile's DIV element from DOM eventually)
        //  actually we are not removing the item from the array but creating a new array that contains all the tiles but the current one
        this.setState({activeOverlayTiles: this.overlayTiles.filter(item => item !== currentTile)})

        //  after current tile is removed from DOM the "hole" is left on the overlay "surface"
        //  and the player's iframe can "see" the mouse events that are happening within the "hole"
    }

    render() {
        const showOverlayTileStyle = this.props.showOverlay ? {opacity: 0.5, background: '#fff', border: "1px solid #555",  } : {}
        return (
            this.state.activeOverlayTiles.map(item => (
                <div onMouseMove = {item.onMouseMove} style = {{...showOverlayTileStyle, position: 'absolute', top: item.top, left: item.left, height: item.elementHeight, width: item.elementWidth,}}></div>
            ))
        );
    }

}

const PlayerWithoutOverlay = ({height, width, url}) => (
    <div style = {{position: 'absolute'}}>
        <ReactPlayer height = {height} width = {width} url = {url} />
    </div>
)

const SmartPlayer = ({height, width, url, showOverlay, handleMouseMove}) => (
    <>
        <PlayerWithoutOverlay height = {height} width = {width} url = {url} />
        <SmartOverlay height = {height} width = {width} showOverlay = {showOverlay} handleMouseMove = {handleMouseMove} />
    </>
)

class App extends Component {
    constructor(props) {
        super(props);

        this.state = {
            showOverlay: false,
            mouseMoves: 0
        }
    }

    toggleShowOverlay(e) {
        //  this simply shows/hide the ovelay depending on checkbox state
        this.setState(state => ({showOverlay: !state.showOverlay}))
    }

    handleMouseMove(){
        //  adds 1 to state.mouseMoves counter
        this.setState(state => ({mouseMoves: state.mouseMoves + 1}));
    }

    render() {
        const height = 420;
        const width = 640;
        return (
            <div style = {{margin: 12, position: 'relative'}}>
                <div style = {{height: height }}>
                    <SmartPlayer 
                        height = {height} 
                        width = {width} 
                        showOverlay = {this.state.showOverlay} 
                        url = "https://www.youtube.com/watch?v=A0Z7fQyTb4M" 
                        handleMouseMove = {this.handleMouseMove.bind(this)} />
                </div>
                <div style = {{marginTop: 12}}>
                    <label>Moves detected: </label>
                    <span>{`${this.state.mouseMoves}`}</span>
                    <label onClick = {this.toggleShowOverlay.bind(this)}>&nbsp; <input type = "checkbox" value="1" checked = {this.state.showOverlay} />Show overlay</label>
                </div>

            </div>
        )
    }
}

export default App;

Upvotes: 0

Ogochi
Ogochi

Reputation: 330

I couldn't find any possible way to force component to propagate events if it has internal logic preventing it.

You can try to create invisible <div> with adequate z-index attribute value to make it cover ReactPlayer component. After that you can attach listeners directly to it.

In such a way you will be able to capture all of the mouse events you want. It is not ideal way, but at least working.

Upvotes: 1

Related Questions