woutervh-/react-pan-and-zoom-hoc

[FEATURE] Respect event.defaultPrevented

Closed this issue · 8 comments

I would like to prevent panning while the mouse is inside this red area to allow some drag and drop features:

image

To accomplish this, I added some Listeners for all events react-pan-and-zoom-hoc registers. Inside of this listeners, I try to call event.preventDefault();:

<div
    className={styles.selectedProductImageRenderer}
    style={wrapperStyle}
    onMouseDown={(event) => {
        event.preventDefault();
        event.stopPropagation();
        return false;
    }}
    onMouseUp={(event) => {
        event.preventDefault();
        event.stopPropagation();
        return false;
    }}
    onMouseMove={(event) => {
        event.preventDefault();
        event.stopPropagation();
        return false;
    }}
    onTouchStart={(event) => {
        event.preventDefault();
        event.stopPropagation();
        return false;
    }}
>
    foo
</div>

This seems to work as far as defaultPrevented is set to true when I use my event listeners (Screenshot is the output of console.log('handleMouseMove', event); ):

image

Then I thought it would be an easy task by just adding something like event.defaultPrevented in handleMouseDown, handleMouseMove and handleMouseUp like this:

         handleMouseDown = (event: MouseEvent | TouchEvent) => {
-            if (!this.panning && !this.boxZoom) {
+            if (!this.panning && !this.boxZoom && !event.defaultPrevented) {
        handleMouseMove = (event: MouseEvent | TouchEvent) => {
-            if (this.panning || this.boxZoom) {
+            if ((this.panning || this.boxZoom) && !event.defaultPrevented) {
        handleMouseUp = (event: MouseEvent | TouchEvent) => {
-            if (this.panning || this.boxZoom) {
+            if ((this.panning || this.boxZoom) && !event.defaultPrevented) {

Am I missing something? Would be nice to get an basic support of event.defaultPrevented. I think event.defaultPrevented is kinda private so I cannot access it.

Hi @blaues0cke

Thanks for the suggestion. Once I get home I will get to work on this.

Cheers

I would also love to fix this for you if you have no time. Can you confirm my POC in the issue description? It kinda works, but not "stable" and I am not sure if I followed the right/best-practive approach to fix this issue. :-)

Hi @blaues0cke
Sorry for the delay, I was on holiday and am still catching up to stuff.
A PR will be very welcome, I think your POC is spot on.

Thanks :)

@woutervh- I finally fixed the issue/found a workaround. The problem is, that component.addEventListener('mousedown', this.handleMouseDown); and component.addEventListener('touchstart', this.handleMouseDown); in componentDidMount in panAndZoomHoc.tsx is called so early that my own onMouseDown and onTouchStart are registered too late. So your handleMouseDown is called even before event.preventDefault() was called.

My workaround now is to register a own mousedown in the callback of reference. This seems to be called earlier and allows me to call event.preventDefault() early enough. This also makes the explicit check of event.defaultPrevented obsolete since this is handled by the event queue. So basically there is no need to fix this in react-pan-and-zoom-hoc but it may be sill cool to have an easier way to achieve this? May it be a workaround to wait one render-iteration before registering your own events?

This is my examples/main.js with the POC:

import React from 'react';
import ReactDOM from 'react-dom';
import panAndZoomHoc from '../lib/panAndZoomHoc';

const InteractiveDiv = panAndZoomHoc('div');

class App extends React.Component {
    state = {
        x: 0.5,
        y: 0.5,
        scale: 1
    };

+   reftest = null;
+
+   handleMouseDown = (event) => {
+       event.stopPropagation();
+   };
+
    handlePanAndZoom = (x, y, scale) => {
        this.setState({ x, y, scale });
    }

    handlePanMove = (x, y) => {
        this.setState({ x, y });
    }

    handleZoomEnd = () => console.log('Zoom has ended.');

+   setViewReference = (reference) => {
+       this.reftest = reference;
+
+       reference.addEventListener('mousedown', this.handleMouseDown, false);
+   };
+
    transformPoint({ x, y }) {
        return {
            x: 0.5 + this.state.scale * (x - this.state.x),
            y: 0.5 + this.state.scale * (y - this.state.y)
        };
    }

    render() {
        const { x, y, scale } = this.state;

        const p1 = this.transformPoint({x: 0.5, y: 0.5});

        return <InteractiveDiv
            x={x}
            y={y}
            scale={scale}
            scaleFactor={Math.sqrt(2)}
            minScale={0.5}
            maxScale={2}
            onPanAndZoom={this.handlePanAndZoom}
            ignorePanOutside
            style={{ width: 500, height: 500, boxSizing: 'border-box', border: '1px solid black', position: 'relative' }}
            onPanMove={this.handlePanMove}
            onZoomEnd={this.handleZoomEnd}
        >
            {/* Viewport */}
            <div style={{ position: 'absolute', width: 500, height: 500, boxSizing: 'border-box', border: '1px dashed blue', transform: `translate(${(x - 0.5) * 500}px, ${(y - 0.5) * 500}px) scale(${1 / scale})` }} />
            {/* Objects - original position and zoom */}
            <div style={{ position: 'absolute', width: 50, height: 50, backgroundColor: 'lightgrey', transform: `translate(250px, 250px) translate(-25px, -25px)` }} />
            <div style={{ position: 'absolute', width: 50, height: 50, backgroundColor: 'lightgrey', transform: `translate(250px, 250px) translate(25px, 25px)` }} />
            {/* Objects */}
            <div style={{ position: 'absolute', width: 50 * this.state.scale, height: 50 * this.state.scale, backgroundColor: 'black', transform: `translate(${p1.x * 500}px, ${p1.y * 500}px) translate(${-25 * scale}px, ${-25 * scale}px)` }} />
            <div style={{ position: 'absolute', width: 50 * this.state.scale, height: 50 * this.state.scale, backgroundColor: 'black', transform: `translate(${p1.x * 500}px, ${p1.y * 500}px) translate(${25 * scale}px, ${25 * scale}px)` }} />
+           <div
+               ref={this.setViewReference}
+               style={{
+                   position:        'absolute',
+                   width:           50 * this.state.scale,
+                   height:          50 * this.state.scale,
+                   backgroundColor: 'red',
+                   zIndex:          22222,
+                   transform:       `translate(${p1.x * 500}px, ${p1.y * 600}px) translate(${25 * scale}px, ${25
+                   * scale}px)`
+               }}
+           />
            {/* Axes */}
            <div style={{ position: 'absolute', width: 1, height: 500, backgroundColor: 'red', transform: 'translateX(250px)' }} />
            <div style={{ position: 'absolute', width: 500, height: 1, backgroundColor: 'red', transform: 'translateY(250px)' }} />
        </InteractiveDiv>;
    }
}

const container = document.createElement('div');
document.body.appendChild(container);

ReactDOM.render(<App />, container);

And as you can see its no longer possible to pan while clicking the red element:

Screen-Recording-2019-07-16-at-22 56 32

Also interesting:

@blaues0cke
Good detective work :) hmm yes, it makes sense that the event that gets added first is fired first.
As a workaround I can think of two things:

  • Have a prop that allows you to cancel the pan start event (onPanStartCancel={() => {...}} ?)
  • Have a method on the React element to re-register the event handlers:
panAndZoomElement.reregisterEventHandlers();

What do you think?

@woutervh- I think the second option would be the better approach since this would not require that the parent exactly knows its childs, right? (since otherwise we would have something like: onPanStartCancel={() => { /* iterate all childs + do nasty x/y checks */ }})?

In version 2.1.6 there is a reregisterEventHandlers method on the component.

Thank you very much. 🔥