draggable component in react not working as expected - javascript

I am trying to make a draggable button using react.
The button drags over the page in a proper manner but when I drop it. Its top and left values become negative(not even reset to their original top:0,left:0) i.e. the component goes out of the page.
code sand box link : code
main draggable.js component:
import React, { Component } from 'react';
import { Button } from '#material-ui/core';
class DraggableButton extends Component {
constructor() {
super();
this.state = {
dragging: false,
diffX: 0,
diffY: 0,
style: {
top: 0,
left: 0
}
}
}
handleMouseDown = (event) => {
console.log("element caught");
this.setState({
diffX: event.clientX - event.currentTarget.getBoundingClientRect().left,
diffY: event.clientY - event.currentTarget.getBoundingClientRect().top,
dragging: true
})
}
handleMouseMove = (event) => {
if (this.state.dragging) {
console.log("dragging");
let left = event.clientX - this.state.diffX;
let top = event.clientY - this.state.diffY;
this.setState({
style: {
left,
top
}
}, console.log("style ", this.state.style))
}
}
handleMouseUp = () => {
console.log('element released');
console.log('left value ', this.state.style.left);
console.log('top value ', this.state.style.top);
this.setState({
dragging: false,
})
}
render() {
return (
<Button
variant="contained" color="primary"
style={{ position: "absolute", ...this.state.style }}
draggable={true}
onDragStart={this.handleMouseDown}
onDrag={this.handleMouseMove}
onDragEnd={this.handleMouseUp}
// onMouseDown={this.handleMouseDown}
// onMouseMove={this.handleMouseMove}
// onMouseUp={this.handleMouseUp}
>
draggable button
</Button>
);
}
}
export default DraggableButton;
console screenshot :
As is visible in the image above at the time of dragging top: 193 left : 309 and as we dropped the element it turned to left: -109 top: -13.
why is this happening how can we fix it ?

In your handleMouseMove you need to check if event.clientX is a positive integer and then change the state, or else it will reduce the diffX value and it will be nevative. (On drag release this becomes 0)
let left = event.clientX - this.state.diffX;
handleMouseMove = (event) => {
if (this.state.dragging) {
let left = event.clientX - this.state.diffX;
let top = event.clientY - this.state.diffY;
if (event.clientX !== 0)
this.setState({
style: {
left,
top
}
});
}
};

Related

Drag to scroll outside of element

I've made a scrollable container but there's a problem with how it behaves when the mouse pointer leaves the element box while pressed. When it happens scrolling goes in the opposite direction from what I want in Firefox and stops in Edge. Scrolling in Firefox seems to be the native behaviour of the browser.
Is it possible to make it work the same way it does when the cursor is over the element?
Codepen
HTML
<div id="app"></div>
JS
import React from "https://cdn.skypack.dev/react"
import ReactDOM from "https://cdn.skypack.dev/react-dom"
const HelloWorld = () => <h1>Hello, World!</h1>
const Scrollable = props => {
const ref = React.useRef()
const [scroll, setScroll] = React.useState({ isScrolling: false, clientX: 0 })
const onMouseDown = e => {
setScroll({ ...scroll, isScrolling: true, clientX: e.clientX })
}
const onMouseUp = () => {
setScroll({ ...scroll, isScrolling: false })
}
const onMouseMove = e => {
const { clientX } = scroll
if (scroll.isScrolling) {
const dx = e.clientX - clientX
const scrollLeft = ref.current.scrollLeft - dx
if (scrollLeft < 0 || scrollLeft > ref.current.scrollLeftMax) return
ref.current.scrollLeft = scrollLeft
setScroll({ ...scroll, clientX: e.clientX })
}
}
return(
<div className="scrollable" ref={ref}
style={{'overflow-x': 'auto', width: '200px', 'margin-left': '100px' }}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
onMouseMove={onMouseMove}
>
{props.children}
</div>
)
}
ReactDOM.render(<Scrollable><div style={{ height: '100px', width: '1000px', background: 'red' }}></div></Scrollable>, document.getElementById('app'))

How Can I convert React.createclass to Class Component?

I have taken an example of dragging certain div in react js from here
http://jsfiddle.net/Af9Jt/2/
Now it is in createClass and I need to convert it into class Draggable extends React.Component in order to export it into another component. Here is code
APP.JS
import React from 'react';
import './App.css';
import Draggable from './Draggable.js';
function App() {
return (
<React.Fragment>
<Draggable />
</React.Fragment>
);
}
export default App;
Draggable.js
import React from 'react';
export class Draggable extends React.Component{
constructor(props) {
super(props);
this.state = {
pos: {x: 0, y: 0},
dragging: false,
rel: null
};
this.onMouseMove = this.onMouseMove.bind(this);
this.onMouseDown = this.onMouseDown.bind(this);
this.onMouseUp = this.onMouseUp.bind(this);
}
// we could get away with not having this (and just having the listeners on
// our div), but then the experience would be possibly be janky. If there's
// anything w/ a higher z-index that gets in the way, then you're toast,
// etc.
// componentDidUpdate(props, state) {
// if (this.state.dragging && !state.dragging) {
// document.addEventListener('mousemove', this.onMouseMove)
// document.addEventListener('mouseup', this.onMouseUp)
// } else if (!this.state.dragging && state.dragging) {
// document.removeEventListener('mousemove', this.onMouseMove)
// document.removeEventListener('mouseup', this.onMouseUp)
// }
// }
// calculate relative position to the mouse and set dragging=true
onMouseDown(e) {
console.log("1")
console.log(this.state);
if (e.button !== 0) return
this.setState({
dragging: true,
rel: {
x: e.pageX - e.nativeEvent.offsetX,
y: e.pageY - e.nativeEvent.offsetY
}
})
e.stopPropagation()
e.preventDefault()
}
onMouseUp(e) {
this.setState({dragging: false})
e.stopPropagation()
e.preventDefault()
}
onMouseMove(e) {
if (!this.state.dragging) return
this.setState({
pos: {
x: e.pageX - this.state.rel.x,
y: e.pageY - this.state.rel.y
}
})
e.stopPropagation()
e.preventDefault()
}
render() {
return(
<div
style={{position: "absolute", left: "175px", top: "65px", border: "2px solid rgb(170, 170, 85)", padding: "10px"}}
className="my-draggable" data-reactid=".r[2zxee]" id="messi"
onMouseDown={this.onMouseDown}
onMouseUp={this.onMouseUp}
onMouseDown={this.onMouseDown}
initialPos = {{x:0,y:0}}
>
Drag Me! See how children are passed through to the div!
</div>
)
}
}
export default Draggable;
Everything runs fine in this code the box is shown but I cannot drag the div, I couldn't figure out what issue is this. How Can I Solve this?
Here is my sample code in jsfiddle
https://jsfiddle.net/6vdurk79/3/
There were a few things I noticed when converting this into a React.Component:
You never used the this.state.pos when rendering, so even if the position changed in the variables, it wouldn't move the div. The style attribute of the <div> is just hard-coded with { left: "175px", top: "65px" }
You didn't properly get the position of the mouse in your this.onMouseDown function, which caused it to forced every movement to be at the corner.
You never bound this.onMouseMove to anything. Uncommenting the big chunk of commented out code fixed this.
The initialPos attribute you place inside the <div> does absolutely nothing. I converted that into a prop in the constructor.
Here's the updated JSFiddle link: https://jsfiddle.net/ogy4xd1c/3/
And I'll embed it here on StackOverflow in a snippet.
class Draggable extends React.Component {
constructor(props) {
super(props);
this.state = {
pos: props.initialPos || {
x: 0,
y: 0
},
dragging: false,
rel: null
}
this.onMouseMove = this.onMouseMove.bind(this);
this.onMouseDown = this.onMouseDown.bind(this);
this.onMouseUp = this.onMouseUp.bind(this);
}
// calculate relative position to the mouse and set dragging=true
onMouseDown(e) {
if (e.button !== 0) return
const de = document.documentElement;
const box = ReactDOM.findDOMNode(this).getBoundingClientRect();
const top = box.top + window.pageYOffset - de.clientTop;
const left = box.left + window.pageXOffset - de.clientLeft;
this.setState({
dragging: true,
rel: {
x: e.pageX - left,
y: e.pageY - top,
}
})
e.stopPropagation()
e.preventDefault()
}
onMouseUp(e) {
this.setState({
dragging: false
})
e.stopPropagation()
e.preventDefault()
}
onMouseMove(e) {
if (!this.state.dragging) return
this.setState({
pos: {
x: e.pageX - this.state.rel.x,
y: e.pageY - this.state.rel.y
}
})
e.stopPropagation()
e.preventDefault()
}
componentDidUpdate(props, state) {
if (this.state.dragging && !state.dragging) {
document.addEventListener('mousemove', this.onMouseMove)
document.addEventListener('mouseup', this.onMouseUp)
} else if (!this.state.dragging && state.dragging) {
document.removeEventListener('mousemove', this.onMouseMove)
document.removeEventListener('mouseup', this.onMouseUp)
}
}
render() {
return ( <div
style={{
position: "absolute",
left: this.state.pos.x,
top: this.state.pos.y,
border: "2px solid rgb(170, 170, 85)",
padding: "10px"
}}
className="my-draggable"
data-reactid=".r[2zxee]"
id="messi"
onMouseDown={this.onMouseDown}
className="my-draggable"
>
Drag Me! See how children are passed through to the div!
</div>
)
}
}
ReactDOM.render(<Draggable initialPos={{ x: 50, y: 20 }} />, document.querySelector("#root"));
.my-draggable {
cursor: pointer;
width: 200px;
height: 200px;
background-color: #cca;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
If you want to pass in children, you can do that too with this modified version: https://jsfiddle.net/hceLjz90/
class Draggable extends React.Component {
constructor(props) {
super(props);
this.state = {
pos: props.initialPos || {
x: 0,
y: 0
},
dragging: false,
rel: null
}
this.onMouseMove = this.onMouseMove.bind(this);
this.onMouseDown = this.onMouseDown.bind(this);
this.onMouseUp = this.onMouseUp.bind(this);
}
// calculate relative position to the mouse and set dragging=true
onMouseDown(e) {
if (e.button !== 0) return
const de = document.documentElement;
const box = ReactDOM.findDOMNode(this).getBoundingClientRect();
const top = box.top + window.pageYOffset - de.clientTop;
const left = box.left + window.pageXOffset - de.clientLeft;
this.setState({
dragging: true,
rel: {
x: e.pageX - left,
y: e.pageY - top,
}
})
e.stopPropagation()
e.preventDefault()
}
onMouseUp(e) {
this.setState({
dragging: false
})
e.stopPropagation()
e.preventDefault()
}
onMouseMove(e) {
if (!this.state.dragging) return
this.setState({
pos: {
x: e.pageX - this.state.rel.x,
y: e.pageY - this.state.rel.y
}
})
e.stopPropagation()
e.preventDefault()
}
componentDidUpdate(props, state) {
if (this.state.dragging && !state.dragging) {
document.addEventListener('mousemove', this.onMouseMove)
document.addEventListener('mouseup', this.onMouseUp)
} else if (!this.state.dragging && state.dragging) {
document.removeEventListener('mousemove', this.onMouseMove)
document.removeEventListener('mouseup', this.onMouseUp)
}
}
render() {
return ( <div
style={{
position: "absolute",
left: this.state.pos.x,
top: this.state.pos.y,
border: "2px solid rgb(170, 170, 85)",
padding: "10px"
}}
className="my-draggable"
data-reactid=".r[2zxee]"
id="messi"
onMouseDown={this.onMouseDown}
className="my-draggable"
>
{this.props.children}
</div>
)
}
}
ReactDOM.render(<Draggable initialPos={{ x: 50, y: 20 }}>
<h1>This is a child element</h1>
<p>This is also a child element</p>
</Draggable>, document.querySelector("#root"))
.my-draggable {
cursor: pointer;
width: 200px;
height: 200px;
background-color: #cca;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Rendering element to DOM based on dynamic sibling positions

I have a series of <StrategyComponent> elements that are rendered through a button click and are fully draggable on the page. Currently the intial positions are static and all components will be rendered to the same position through the initialPos prop. However, since the user is free to move these wherever they want, I want to render each new component that the user adds a certain distance to the right of the right most <StrategyComponent> at the time (wherever that may be). How can I access this component or what is the best way to keep track of the right most bound in which to place incoming components.
Any help with this would be great. I'm sure it can be done, but I'm just getting used to React. Thanks!
StrategyComponent.js
class StrategyComponent extends Component {
constructor(props) {
super(props);
// creates a reference to wrapper div
this.wrapper = React.createRef();
}
state = {
pos: this.props.initialPos,
dragging: false,
rel: null, // position relative to the cursor
};
componentDidUpdate = (props, state) => {
if (this.state.dragging && !state.dragging) {
document.addEventListener("mousemove", this.onMouseMove);
document.addEventListener("mouseup", this.onMouseUp);
} else if (!this.state.dragging && state.dragging) {
document.removeEventListener("mousemove", this.onMouseMove);
document.removeEventListener("mouseup", this.onMouseUp);
}
};
// calculate relative position to the mouse and set dragging=true
onMouseDown = (e) => {
// only left mouse button
if (e.button !== 0) return;
var pos = this.wrapper.current.getBoundingClientRect();
this.setState({
dragging: true,
rel: {
x: e.pageX - pos.x,
y: e.pageY - pos.y,
},
});
e.stopPropagation();
e.preventDefault();
};
onMouseUp = (e) => {
this.setState({ dragging: false });
e.stopPropagation();
e.preventDefault();
};
onMouseMove = (e) => {
if (!this.state.dragging) return;
this.setState({
pos: {
x: e.pageX - this.state.rel.x,
y: e.pageY - this.state.rel.y,
},
});
this.props.handleParentResize(this.wrapper.current.getBoundingClientRect());
e.stopPropagation();
e.preventDefault();
};
render() {
return (
<div
ref={this.wrapper}
onMouseDown={this.onMouseDown}
draggable
className="component-wrapper"
style={{ left: this.state.pos.x + "px", top: this.state.pos.y + "px" }}
>
<h3> Strategy Component {this.props.name}</h3>
</div>
);
}
}
App.js
class App extends Component {
state = {
numComponents: 1,
};
addStrategyComponent = () => {
this.setState({ numComponents: this.state.numComponents + 1 });
};
render() {
const components = [];
for (var i = 0; i < this.state.numComponents; i++) {
components.push(
<StrategyComponent key={i.toString()} name={i.toString()} initialPos={{ x: 200, y: 200 }} />
);
}
return (
<div className="App">
<button onClick={this.addStrategyComponent}>+</button>
<div className="components-wrapper">{components}</div>
</div>
);
}
}

Drag Background in React

I 'm new in React and i'm tryind to make draggable background, but no matter how hard I try, nothing happens. I found some code on jQuery, but there many advices that it's bad practice use jQuery in React.
Maybe i make something wrong.
Thanks in advance
Here's my React code
import React from "react";
import "../styles/board.css";
class Board extends React.Component {
constructor(props) {
super(props);
this.state = { mouseCliked: 0, startX: 0, startY: 0 };
}
mouseDown(e) {
this.setState({ mouseCliked: 1, startX: e.clientX, startY: e.clientY });
}
mouseUp(e) {
this.setState({ mouseCliked: 0, startX: e.clientX, startY: e.clientY });
}
mouseMove = (e) => {
let newPosY = e.clientY - this.stateY;
let newPosX = e.clientX - this.stateX;
if (this.state.mouseClicked) {
e.target.style.backgroundPositionX += newPosX;
e.target.style.backgroundPositionY += newPosY;
}
};
render() {
return (
<div
onMouseMove={this.mouseMove.bind(this)}
onMouseUp={this.mouseUp.bind(this)}
onMouseDown={this.mouseDown.bind(this)}
className="background-image"
>
</div>
);
}
}
export default Board;
CSS:
width:300px;
height: 300px;
background-size: 1000px;
background-position-x: 0;
background-position-y: 0;
background-image: url('https://images.unsplash.com/photo-1452723312111-3a7d0db0e024?crop=entropy&dpr=2&fit=crop&fm=jpg&h=750&ixjsv=2.1.0&ixlib=rb-0.3.5&q=50&w=1450.jpg');
}
I have encountered this problem and also checked jQuery - drag div css background
Finally I came up with this solution and seemed working fine.
const imageStyleInitialValue = {
backgroundImage: "",
backgroundPosition: "0 0",
backgroundSize: "0 0",
height: 0,
width: 0,
};
const [startPoint, setStartPoint] = useState({x: 0, y: 0});
const [dragging, setDragging] = useState(false);
const [imageStartPos, setImageStartPos] = useState([0, 0]);
const [imageStyles, setImageStyles] = useState<ImageStyles>(imageStyleInitialValue);
// add onMouseMove={handleDragImage} to the image component
const handleDragImage = (e) => {
if (dragging) {
const deltaX = e.clientX - startPoint.x;
const deltaY = e.clientY - startPoint.y;
setImageStyles({...imageStyles,
backgroundPosition:
`${imageStartPos[0] + deltaX} ${imageStartPos[1] + deltaY}`
})
}
};
// add onMouseDown={handleStartDragImage} to the image component
const handleStartDragImage = (e) => {
setDragging(true);
const backgroundPosArray = imageStyles.backgroundPosition.split(" ").map(value => Number(value));
setImageStartPos(backgroundPosArray);
setStartPoint({x: e.clientX, y: e.clientY});
}
// add onMouseUp={handleEndDragImage} to the top component because you want
// to set Dragging to false when the dragging ends outside of the image
const handleEndDragImage = (e) => {
setDragging(false)
};

Using initialPos from Reactjs state

I am building a React component with fixed width and resizable height. The problem is I am getting event.clientY and this.state.initialPos with unpredictable values. Here is fiddle
And here is code of React component
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import '../styles/cal-event-styles.css';
class CalEvent extends Component {
constructor(props) {
super(props);
this.state = {
isDragging: false,
height: 40,
text: this.props.text,
color: this.props.color
}
}
componentDidMount() {
ReactDOM.findDOMNode(this).addEventListener('mousemove', this.resizePanel);
ReactDOM.findDOMNode(this).addEventListener('mouseup', this.stopResize);
ReactDOM.findDOMNode(this).addEventListener('mouseleave', this.stopResize);
}
resizePanel = (event) => {
if (this.state.isDragging) {
//I tried to use event.clientY - this.state.initialPos but it doesn't work
let delta = event.clientY + this.state.height;
console.log("event.clentY " + event.clientY);
console.log("this.state.initialPos " + this.state.initialPos);
this.setState({height: delta});
}
}
stopResize = () => {
if (this.state.isDragging) {
this.setState({
isDragging: false,
});
}
// height: this.getStep(this.state.height)
}
getStep = (height) => {
return Math.floor(height / 50) * 50;
}
startResize = (event) => {
this.setState({
isDragging: true,
initialPos: event.clientX
});
}
formatText = () => {
const { text, height } = this.state;
return text.length > 10 && height <= 100 ? text.substring(0, 14) + "..." : text;
}
render(){
const {color, text, height, isDragging, initialPos } = this.state;
console.log("this.state.isDragging: " + isDragging);
if (isDragging) {
console.log("this.state.height: " + height);
console.log("this.state.initialPos: " + initialPos);
}
return(
<div className="cal-event" onMouseUp={() => this.stopResize()} style={{height: `${height}px`, background: color}}>
<div className="cal-event-tile"><p>{this.formatText()}</p></div>
<div className="resizer-height" onMouseDown={e => this.startResize(e)}></div>
</div>
)
}
}
export default CalEvent;
I think that in this line:
let delta = event.clientY + this.state.height
delta shouldn't be the sum of those values, because you resize on the Y axis and exactly the Y (in this case) should determine the new height:
I tried this in the resizePanel method and it seem to work:
this.setState({ height: event.clientY })
The problem with this approach is that, in this case, the top position of the component is almost 0, and that is why it work quite seemlessly.
If you need it to work no matter where the component is placed on the page, you have to compute delta as the difference between the top position and the event.clientY.
To do it, I would save the absolute top position of the component in the componentDidMount lifecycle method:
componentDidMount() {
const currentNode = ReactDOM.findDOMNode(this)
currentNode.addEventListener('mousemove', this.resizePanel);
currentNode.addEventListener('mouseup', this.stopResize);
currentNode.addEventListener('mouseleave', this.stopResize);
this.setState({ initialY: currentNode.getBoundingClientRect().top })
}
Then, in the event handler, the setting of the height property will be:
this.setState(prevState => ({ height: event.clientY - prevState.initialY }))

Categories