I am trying to implement drag and drop in React and using SVG elements. The problem is mouseMove does not get triggered if the user moves the mouse too fast. It basically loses the dragging quite frequently. To solve this I think I need to handle the mouseMove in the parent but not sure how to do this with React. I tried several different approaches to no avail.
I tried addEventListener('mousemove', ...) on the parent using a ref, but the problem is that the clientX is a different coordinate system than the current component. Also, the event handler does not have access to any of the state from the component (event with arrow functions). It maintains a stale reference to any state.
I tried setting the clientX and clientY in a context on the parent and then pulling it in from the DragMe component but it is always undefined the first time around for some strange reason even though I give it a default value.
Here's the code I'm working with:
const DragMe = ({ x = 50, y = 50, r = 10 }) => {
const [dragging, setDragging] = useState(false)
const [coord, setCoord] = useState({ x, y })
const [offset, setOffset] = useState({ x: 0, y: 0 })
const [origin, setOrigin] = useState({ x: 0, y: 0 })
const xPos = coord.x + offset.x
const yPos = coord.y + offset.y
const transform = `translate(${xPos}, ${yPos})`
const fill = dragging ? 'red' : 'green'
const stroke = 'black'
const handleMouseDown = e => {
setDragging(true)
setOrigin({ x: e.clientX, y: e.clientY })
}
const handleMouseMove = e => {
if (!dragging) { return }
setOffset({
x: e.clientX - origin.x,
y: e.clientY - origin.y,
})
}
const handleMouseUp = e => {
setDragging(false)
setCoord({ x: xPos, y: yPos })
setOrigin({ x: 0, y: 0 })
setOffset({ x: 0, y: 0 })
}
return (
<svg style={{ userSelect: 'none' }}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseUp}
>
<circle transform={transform} cx="0" cy="0" r={r} fill={fill} stroke={stroke} />
</svg>
)
}
After much experimentation I was able to addEventListener to the parent canvas. I discovered that I needed to useRef in order to allow the mousemove handler to see the current state. The problem I had before was that the handleParentMouseMove handler had a stale reference to the state and never saw the startDragPos.
This is the solution I came up with. If anyone knows of a way to clean this up it would be much appreciated.
const DragMe = ({ x = 50, y = 50, r = 10, stroke = 'black' }) => {
// `mousemove` will not generate events if the user moves the mouse too fast
// because the `mousemove` only gets sent when the mouse is still over the object.
// To work around this issue, we `addEventListener` to the parent canvas.
const canvasRef = useContext(CanvasContext)
const [dragging, setDragging] = useState(false)
// Original position independent of any dragging. Updated when done dragging.
const [originalCoord, setOriginalCoord] = useState({ x, y })
// The distance the mouse has moved since `mousedown`.
const [delta, setDelta] = useState({ x: 0, y: 0 })
// Store startDragPos in a `ref` so handlers always have the latest value.
const startDragPos = useRef({ x: 0, y: 0 })
// The current object position is the original starting position + the distance
// the mouse has moved since the start of the drag.
const xPos = originalCoord.x + delta.x
const yPos = originalCoord.y + delta.y
const transform = `translate(${xPos}, ${yPos})`
// `useCallback` is needed because `removeEventListener`` requires the handler
// to be the same as `addEventListener`. Without `useCallback` React will
// create a new handler each render.
const handleParentMouseMove = useCallback(e => {
setDelta({
x: e.clientX - startDragPos.current.x,
y: e.clientY - startDragPos.current.y,
})
}, [])
const handleMouseDown = e => {
setDragging(true)
startDragPos.current = { x: e.clientX, y: e.clientY }
canvasRef.current.addEventListener('mousemove', handleParentMouseMove)
}
const handleMouseUp = e => {
setDragging(false)
setOriginalCoord({ x: xPos, y: yPos })
startDragPos.current = { x: 0, y: 0 }
setDelta({ x: 0, y: 0 })
canvasRef.current.removeEventListener('mousemove', handleParentMouseMove)
}
const fill = dragging ? 'red' : 'green'
return (
<svg style={{ userSelect: 'none' }}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
>
<circle transform={transform} cx="0" cy="0" r={r} fill={fill} stroke={stroke} />
</svg>
)
}
Just to isolate the code that works here:
First go ahead and figure out what your top-level parent is in your render() function, where you may have something like:
render() {
return (
<div ref={(parent_div) => { this.parent_div = parent_div }}}>
// other div stuff
</div>
)
}
Then, using the ref assignment as from above, go ahead and assign the event listener to it like so:
this.parent_div.addEventListener('mousemove', function (event) {
console.log(event.clientX)
}
Related
I have a state and a ref like that :
const [myValue, setMyValue] = useState();
const scrollViewRef = useRef();
I have a scrollview with multiple views in it :
<ScrollView
style={styles.optionsContainer}
horizontal={true}
ref={scrollViewRef}
>
<View>
// Some stuff
</>
<View>
// Some stuff
</>
<View>
// Some stuff
</>
</ScrollView>
When the value of myValue state changes, the position of the corresponding view changes thanks to the ref :
useEffect(() => {
if (myValue < 24) {
scrollViewRef.current.scrollTo({ x: 0, y: 0, animated: true })
}
if (myValue > 24 && myValue < 50) {
scrollViewRef.current.scrollTo({ x: 190, y: 0, animated: true });
}
if (myValue > 49 && myValue < 75) {
scrollViewRef.current.scrollTo({ x: 480, y: 0, animated: true });
}
}, [myValue]);
What I want to do is to keep that feature, but add the reverse possibility : if I scroll by hand, I want to update the value of myValue state.
How can I achieve that ?
Add scroll event listener, initialize it in useEffect, and invoke your updateState functions when scrolls.
useEffect(() => {
window.addEventListener('scroll', handleScroll);
}, [])
const handleScroll = () => {
setMyValue(window.scrollTop)
}
What you can do is add onScroll event for scrollView
const onScroll = (event) =>{
if(event.nativeEvent.contentOffset.x <190){
setMyValue(22)
}
if(event.nativeEvent.contentOffset.x < 480){
setMyValue(50)
}
}
return(<ScrollView onScroll={onScroll} >)
Hope it helps, feel free for doubts
So I wanted to use hooks for an exercise where I have a circle follow my cursor in React ( and then see other users circles using node) so I've set a randomColor() function so every page is a different color. However I have no idea how to make it not reroll the color every time I move my cursor.
function randomColor(){
return Math.floor(100000 + Math.random() * 900000)
}
function App() {
const [coord, setCoord] = useState({ x: 0, y: 0 });
const [left, setLeft] = useState({ x: 0});
const [top, setTop] = useState({ y: 0});
const handleMouseMove = (e) => {
setCoord({ x: e.screenX, y: e.screenY });
setTop({ y: e.pageY-50 });
setLeft({ x: e.pageX-10});
let circle = document.getElementById('circle');
circle.style.transform ="translate("+left.x+"px, "+top.y+"px)";
circle.style.top = top + 'px';
console.log("translate("+left.x+"px, "+top.y+"px)")
};
socket.emit("howdy",[left.x,top.y] )
return (
<div style={{ backgroundColor: "#"+randomColor()}}>
<div className="main" onMouseMove={handleMouseMove}>
<h1>
Mouse coordinates: {coord.x} {coord.y} {left.x} {top.y}
</h1>
<div id='circle'></div>
</div>
</div>
)
}
export default App;
I have a map element using an svg, and have functions to zoom and pan, however the zoom function, which uses the mouse wheel, wont stop the entire page from scrolling when the mouse is inside the map. I have tried to use the e.preventDefault call to stop this, however I keep getting the error Unable to preventDefault inside passive event listener invocation. I have been unable to find a solution to this that works. I know I am probably missing something small, but I have had no success in making it work.
Here is the code minus the stuff inside the SVG as it is too much to fit in the post.
import React, { useEffect, useRef, useState } from 'react'
export default function Map () {
const [isPanning, setPanning] = useState(false)
const [map, setMap] = useState()
const [position, setPosition] = useState({
oldX: 0,
oldY: 0,
x: 0,
y: 0,
z: 1,
})
const containerRef = useRef()
const onLoad = (e) => {
setMap({
width: 1000,
height: 1982
})
}
const onMouseDown = (e) => {
e.preventDefault()
setPanning(true)
setPosition({
...position,
oldX: e.clientX,
oldY: e.clientY
})
}
const onWheel = (e) => {
if (e.deltaY) {
const sign = Math.sign(e.deltaY) / 10
const scale = 1 - sign
const rect = containerRef.current.getBoundingClientRect()
console.log(map)
setPosition({
...position,
x: position.x * scale - (rect.width / 2 - e.clientX + rect.x) * sign,
y: position.y * scale - (1982 * rect.width / 1000 / 2 - e.clientY + rect.y) * sign,
z: position.z * scale,
})
}
}
useEffect(() => {
const mouseup = () => {
setPanning(false)
}
const mousemove = (event) => {
if (isPanning) {
setPosition({
...position,
x: position.x + event.clientX - position.oldX,
y: position.y + event.clientY - position.oldY,
oldX: event.clientX,
oldY: event.clientY
})
}
}
window.addEventListener('mouseup', mouseup)
window.addEventListener('mousemove', mousemove)
return () => {
window.removeEventListener('mouseup', mouseup)
window.removeEventListener('mousemove', mousemove)
}
})
return (
<div className='viewBox'>
<div
className="PanAndZoomImage-container"
ref={containerRef}
onMouseDown={onMouseDown}
onWheel={onWheel}
>
<svg baseProfile="tiny" fill="#7c7c7c" height="1982" stroke="#ffffff" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" version="1.2" viewBox="0 0 1000 1982" width="1000" xmlns="http://www.w3.org/2000/svg" onLoad={onLoad}>
<g className='regions' style={{
transform: `translate(${position.x}px, ${position.y}px) scale(${position.z})`,
}}>
If anyone knows a solution, any help would be greatly appreciated!
you can try
<YourComponent
onMouseEnter={(e) => {disableScroll.on()}}
onMouseLeave={(e) => {disableScroll.off()}}
/>
use lib from https://github.com/gilbarbara/disable-scroll
I'm trying to build a image cropping tool with React and konva. I'd like to change the opacity outside of the cropping rectangle to blur the rest of the image.
I have so far tried to set different opacities to the rectangle and the image but failed. I have looked up and there is no direct way to doing this
Here's the cropping function that I adapted to react with the help of this answer
import React, { useState, useEffect, useRef } from "react";
import { render } from "react-dom";
import { Stage, Layer, Rect, Image } from "react-konva";
import Konva from "konva";
const App = () => {
// Stage dims
let sW = 720,
sH = 720,
sX = 0,
sY = 0;
let src = "https://dummyimage.com/720x720/e85de8/fff&text=SO Rocks!";
let img = document.createElement("img");
useEffect(() => {
img.src = src;
function loadStatus() {
setloading(false);
}
img.addEventListener("load", loadStatus);
return () => {
img.removeEventListener("load", loadStatus);
};
}, [img, src]);
let scale = 1;
const [loading, setloading] = useState(true);
const [posStart, setposStart] = useState({});
const [posNow, setposNow] = useState({});
const [mode, setmode] = useState("");
/**
* Sets the state of posStart and posNow for tracking the coordinates of the cropping rectangle
* #param {Object} posIn - Coordinates of the pointer when MouseDown is fired
*/
function startDrag(posIn) {
setposStart({ x: posIn.x, y: posIn.y });
setposNow({ x: posIn.x, y: posIn.y });
}
/**
* Updates the state accordingly when the MouseMove event is fired
* #param {Object} posIn - Coordiantes of the current position of the pointer
*/
function updateDrag(posIn) {
setposNow({ x: posIn.x, y: posIn.y });
let posRect = reverse(posStart, posNow);
r2.current.x(posRect.x1);
r2.current.y(posRect.y1);
r2.current.width(posRect.x2 - posRect.x1);
r2.current.height(posRect.y2 - posRect.y1);
r2.current.visible(true);
}
/**
* Reverse coordinates if dragged left or up
* #param {Object} r1 - Coordinates of the starting position of cropping rectangle
* #param {Object} r2 - Coordinates of the current position of cropping rectangle
*/
function reverse(r1, r2) {
let r1x = r1.x,
r1y = r1.y,
r2x = r2.x,
r2y = r2.y,
d;
if (r1x > r2x) {
d = Math.abs(r1x - r2x);
r1x = r2x;
r2x = r1x + d;
}
if (r1y > r2y) {
d = Math.abs(r1y - r2y);
r1y = r2y;
r2y = r1y + d;
}
return { x1: r1x, y1: r1y, x2: r2x, y2: r2y }; // return the corrected rect.
}
/**
* Crops the image and saves it in jpeg format
* #param {Konva.Rect} r - Ref of the cropping rectangle
*/
function setCrop(r) {
let jpeg = new Konva.Image({
image: img,
x: sX,
y: sY
});
jpeg.cropX(r.x());
jpeg.cropY(r.y());
jpeg.cropWidth(r.width() * scale);
jpeg.cropHeight(r.height() * scale);
jpeg.width(r.width());
jpeg.height(r.height());
const url = jpeg.toDataURL({ mimeType: "image/jpeg", quality: "1.0" });
const a = document.createElement("a");
a.href = url;
a.download = "cropped.jpg";
a.click();
}
// Foreground rect to capture events
const r1 = useRef();
// Cropping rect
const r2 = useRef();
const image = useRef();
return (
<div className="container">
<Stage width={sW} height={sH}>
<Layer>
{!loading && (
<Image
ref={image}
{...{
image: img,
x: sX,
y: sY
}}
/>
)}
<Rect
ref={r1}
{...{
x: 0,
y: 0,
width: sW,
height: sH,
fill: "white",
opacity: 0
}}
onMouseDown={function (e) {
setmode("drawing");
startDrag({ x: e.evt.layerX, y: e.evt.layerY });
}}
onMouseMove={function (e) {
if (mode === "drawing") {
updateDrag({ x: e.evt.layerX, y: e.evt.layerY });
image.current.opacity(0.5);
r2.current.opacity(1);
}
}}
onMouseUp={function (e) {
setmode("");
r2.current.visible(false);
setCrop(r2.current);
image.current.opacity(1);
}}
/>
<Rect
ref={r2}
listening={false}
{...{
x: 0,
y: 0,
width: 0,
height: 0,
stroke: "white",
dash: [5, 5]
}}
/>
</Layer>
</Stage>
</div>
);
};
render(<App />, document.getElementById("root"));
Demo for the above code
It can be done using Konva.Group and its clip property. Add a new Konva.Image to the group and set its clipping positions and size to be the same as the cropping rectangle. Don't forget to set the listening prop of the group to false otherwise it will complicate things. Here's the final result
import { render } from "react-dom";
import React, { useLayoutEffect, useRef, useState } from "react";
import { Stage, Layer, Image, Rect, Group } from "react-konva";
/**
* Crops a portion of image in Konva stage and saves it in jpeg format
* #param {*} props - Takes no props
*/
function Cropper(props) {
// Stage dims
let sW = 720,
sH = 720,
sX = 0,
sY = 0;
let src = "https://dummyimage.com/720x720/e85de8/fff&text=SO Rocks!";
let img = new window.Image();
useLayoutEffect(() => {
img.src = src;
function loadStatus() {
// img.crossOrigin = "Anonymous";
setloading(false);
}
img.addEventListener("load", loadStatus);
return () => {
img.removeEventListener("load", loadStatus);
};
}, [img, src]);
let i = new Konva.Image({
x: 0,
y: 0,
width: 0,
height: 0
});
let scale = 1;
const [loading, setloading] = useState(true);
const [posStart, setposStart] = useState({});
const [posNow, setposNow] = useState({});
const [mode, setmode] = useState("");
/**
* Sets the state of posStart and posNow for tracking the coordinates of the cropping rectangle
* #param {Object} posIn - Coordinates of the pointer when MouseDown is fired
*/
function startDrag(posIn) {
setposStart({ x: posIn.x, y: posIn.y });
setposNow({ x: posIn.x, y: posIn.y });
}
/**
* Updates the state accordingly when the MouseMove event is fired
* #param {Object} posIn - Coordiantes of the current position of the pointer
*/
function updateDrag(posIn) {
setposNow({ x: posIn.x, y: posIn.y });
let posRect = reverse(posStart, posNow);
r2.current.x(posRect.x1);
r2.current.y(posRect.y1);
r2.current.width(posRect.x2 - posRect.x1);
r2.current.height(posRect.y2 - posRect.y1);
r2.current.visible(true);
grp.current.clip({
x: posRect.x1,
y: posRect.y1,
width: posRect.x2 - posRect.x1,
height: posRect.y2 - posRect.y1
});
grp.current.add(i);
i.image(img);
i.width(img.width);
i.height(img.height);
i.opacity(1);
}
/**
* Reverse coordinates if dragged left or up
* #param {Object} r1 - Coordinates of the starting position of cropping rectangle
* #param {Object} r2 - Coordinates of the current position of cropping rectangle
*/
function reverse(r1, r2) {
let r1x = r1.x,
r1y = r1.y,
r2x = r2.x,
r2y = r2.y,
d;
if (r1x > r2x) {
d = Math.abs(r1x - r2x);
r1x = r2x;
r2x = r1x + d;
}
if (r1y > r2y) {
d = Math.abs(r1y - r2y);
r1y = r2y;
r2y = r1y + d;
}
return { x1: r1x, y1: r1y, x2: r2x, y2: r2y }; // return the corrected rect.
}
/**
* Crops the image and saves it in jpeg format
* #param {Konva.Rect} r - Ref of the cropping rectangle
*/
function setCrop(r) {
let jpeg = new Konva.Image({
image: img,
x: sX,
y: sY
});
jpeg.cropX(r.x());
jpeg.cropY(r.y());
jpeg.cropWidth(r.width() * scale);
jpeg.cropHeight(r.height() * scale);
jpeg.width(r.width());
jpeg.height(r.height());
const url = jpeg.toDataURL({ mimeType: "image/jpeg", quality: "1.0" });
const a = document.createElement("a");
a.href = url;
a.download = "cropped.jpg";
a.click();
}
// Foreground rect to capture events
const r1 = useRef();
// Cropping rect
const r2 = useRef();
const image = useRef();
const grp = useRef();
return (
<div className="container">
<Stage width={sW} height={sH}>
<Layer>
{!loading && (
<Image
ref={image}
listening={false}
{...{
image: img,
x: sX,
y: sY
}}
/>
)}
<Rect
ref={r1}
{...{
x: 0,
y: 0,
width: sW,
height: sH,
fill: "white",
opacity: 0
}}
onMouseDown={function (e) {
setmode("drawing");
startDrag({ x: e.evt.layerX, y: e.evt.layerY });
image.current.opacity(0.2);
}}
onMouseMove={function (e) {
if (mode === "drawing") {
updateDrag({ x: e.evt.layerX, y: e.evt.layerY });
}
}}
onMouseUp={function (e) {
setmode("");
r2.current.visible(false);
setCrop(r2.current);
image.current.opacity(1);
grp.current.removeChildren(i);
}}
/>
<Group listening={false} ref={grp}></Group>
<Rect
ref={r2}
listening={false}
{...{
x: 0,
y: 0,
width: 0,
height: 0,
stroke: "white",
dash: [5, 10]
}}
/>
</Layer>
</Stage>
</div>
);
}
render(<Cropper />, document.getElementById("root"));
Thanks to #Vanquished Wombat for all the precious inputs. This is an adaptation of his answer here
Demo of the above code
This is quite difficult to explain so I have created this codesandbox to illustrate the problem.
I am working on a package that basically is a wrapper around mousetrap so you can add keyboard events to either the document object or a specific element.
I am testing it out with this code:
const boxes = [{ x: 0, y: 0 }, { x: 0, y: 0 }, { x: 0, y: 0 }, { x: 0, y: 0 }, { x: 0, y: 0 }].map(
(b): Box => {
return { ...b, color: `hsl(${Math.random() * 360}, 100%, 50%)` };
}
);
export const App: React.FC = () => {
const [boxState, setState] = useState(boxes);
const handleMove = (newPosition: Partial<Point>, index: number) => {
setState((state) => {
return state.map((box, i) => {
return index === i ? { ...box, ...newPosition } : { ...box };
});
});
};
return (
<div>
<h1>Click on any box and use arrow keys or WSAD</h1>
{boxState.map(({ x, y, color }, index) => (
<MovableBox key={index} color={color} index={index} x={x} y={y} onMoveRequest={handleMove} />
))}
</div>
);
};
In the code sandbox, you can move the squares down by either pressing the D button in each square of if you click a square, the div gets focus and the arrow keys should move the boxes around.
The problem is that it works fine when pressing the button but when you use the arrow key, the new state is not persisted. It always starts at it's initial value.
The Box component looks like this:
export const Box: React.FC<BoxType & BoxProps> = ({ x, y, color, index, onMoveRequest }) => {
const style: CSSProperties = {
width: '100px',
height: '100px',
backgroundColor: color,
textAlign: 'center',
lineHeight: '100px',
position: 'absolute',
top: `${y + index * 120}px`,
left: `${x + index * 120}px`
};
if (index === 0) {
console.log({ x, y });
}
const SHIFT = 10;
const handleMove = (action) => {
console.log(index);
switch (action) {
case 'MOVE_LEFT':
onMoveRequest({ x: x - SHIFT }, index);
break;
case 'MOVE_RIGHT':
onMoveRequest({ x: x + SHIFT }, index);
break;
case 'MOVE_UP':
onMoveRequest({ y: y - SHIFT }, index);
break;
case 'MOVE_DOWN':
onMoveRequest({ y: y + SHIFT }, index);
break;
default:
throw new Error('Unknown action');
}
};
return (
<Shortcuts shortcutMap={shortcutMap} mapKey="BOX" handler={handleMove} scoped>
<div style={style}>
{index + 1} ({x}, {y})
<button type="button" onClick={() => handleMove('MOVE_DOWN')}>
D
</button>
</div>
</Shortcuts>
);
};
I cannot for the life of me work out why it is different from the keyboard event.
Ok your issue:
You initialize the keyboard shortcuts in the Shortcut component on "didMount". That means it only registers the handler function when it mounts, with the reference to the initial y.
To fix this pass x & y to the Shortcuts component then take everything from componentDidMount and add it to a new method - initializeShortcuts => (props: Props) => {...}.
Run this function in componentDidMount() & componentWillReceiveProps() (or getDerivedState whatever with props & nextProps respectively and it works. Also remember to unbind the shortcuts before binding them again in the latter.
Working version - https://codesandbox.io/s/wnl6v7x69w.