How to create nested draggable component in react app? - javascript

Initially app will have only one thing that is a button AddParent on the top.
AddParent button will add a parent Draggable to this component i.e every time the user clicks the AddParent button, a parent draggable should envelope the draggable component. Keep in mind that each level of component should in itself be draggable inside its parents’ window i.e. each of the title bars can be used to move around the respective component inside its parents’ blank box.
Something like this (after 3 clicks of AddParent, there are 3 parents for the initial child) ->
(You don’t need to actually color the blank areas, it is just for ease of explanation...they can be white)
result after 3 clicks - image
Goal is to achive this
I have tried a lot but not able to come with expected solution. This is working but it is dragging its parent component too and top-most component is not draggble to bottom side.
import React, { useState } from 'react';
import Draggable from 'react-draggable';
function ParentDraggable(props) {
const [x, setX] = useState(0);
const [y, setY] = useState(0);
const handleMouseDown = (e) => {
let startX = e.clientX;
let startY = e.clientY;
const handleMouseMove = (e) => {
setX(x + e.clientX - startX);
setY(y + e.clientY - startY);
startX = e.clientX;
startY = e.clientY;
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', () => {
document.removeEventListener('mousemove', handleMouseMove);
});
};
return (
<Draggable bounds='parent' handle='.handle'>
<div
style={{
position: 'relative',
top: y,
left: x,
border: '1px solid black',
width: `${props.w}px`,
height: `${props.w}px`,
}}
>
<div
style={{
height: 20,
backgroundColor: 'gray',
}}
onMouseDown={handleMouseDown}
className='handle'
onClick={e=>e.preventDefault()}
>
Title Bar
</div>
{props.children}
</div>
</Draggable>
);
}
function App() {
const [parents, setParents] = useState([]);
const [w, setW] = useState(200)
const handleAddParent = () => {
setParents([ <ParentDraggable w = {w}>
{parents}
</ParentDraggable>]);
setW(prev => prev + 200)
};
return (
<div>
<button onClick={handleAddParent}>AddParent</button>
{parents}
</div>
);
}
export default App;
If you want to check code in codesandbox
Please help me to achieve my goal

Related

React request animation frame for event listener mousemove

I'm not sure how to implement requestAnimationFrame in this example as I always create an exponential infinite loop when trying it. I want the mousemove animation to be smooth that's why I want to use requestAnimationFrame but where should I set it. In my case I can't work with useEffect.
In the Codesandbox, if you move your Mouse over the red background it should activate it the mousemove event.
Codesandbox:
https://codesandbox.io/s/late-dream-97bfhp?file=/src/RequestAnimationFrameTest.tsx:0-1307
Code:
import React, { useState, useRef } from "react";
const RequestAnimationFrameTest = () => {
const [cursorPos, setCursorPos] = useState({ x: 0, y: 0 });
const [cursorVisible, setCursorVisible] = useState(false);
const requestRef = useRef();
const handleMouseMove = (e) => {
console.log("eventListener");
const x = e.pageX;
const y = e.pageY;
setCursorPos({ x: x, y: y });
// requestRef.current = requestAnimationFrame(handleMouseMove);
};
const handleMouseMoveRef = useRef(handleMouseMove);
const handleMouseEnter = () => {
document.body.style.cursor = "none";
addEventListener("mousemove", handleMouseMoveRef.current);
setCursorVisible(true);
};
const handleMouseLeave = () => {
document.body.style.cursor = "default";
removeEventListener("mousemove", handleMouseMoveRef.current);
// cancelAnimationFrame(requestRef.current);
setCursorVisible(false);
};
return (
<div className="container">
{cursorVisible && (
<div
style={{ top: cursorPos.y + "px", left: cursorPos.x + "px" }}
className="cursor"
>
Mouse
</div>
)}
<div
className="item"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
></div>
</div>
);
};

How to stop at the nearest row using react-spring + decay?

I implement calendar which can be veristically dragged by a mouse. I would like to have some inertia on mouse release. I was able to achieve this functionality by using use-gesture + react-spring libs.
This is how it looks
The problem I would like to solve is how to force the inertia to stop at the nearest row? Currently it can be stopped at the middle of the row.
Here is my code. I removed some parts for simplicity
[...]
const [animatedOffsetY, animatedOffsetYProps] = useSpring(() => ({
offsetY: 0,
config: {
decay: true
frequency: 5.5,
}
}))
[...]
const bind = useDrag((state) => {
const [, currentY] = state.movement
const [, lastOffsetY] = state.lastOffset
const offsetY = lastOffsetY + currentY
if (state.down) {
setOffsetY(offsetY)
animatedOffsetYProps.set({ offsetY: offsetY })
} else {
animatedOffsetYProps.start({
offsetY: offsetY,
config: {
velocity: state.direction[1] * state.velocity[1],
}
})
}
}, {
from: [0, offsetY],
bounds: { bottom: 0, top: (totalHeight - containerHeight) * -1 },
rubberband: true
})
[...]
Update
I investigated, popmotion and framer-motion (which use popmotion under the hood) and they have modifyTarget prop which should solve my problem. I would prefer to stay with react-spring or custom solution.
For some reason using decay in the spring config is giving me lots of trouble. I've got it working just fine with a standard friction/tension based spring.
You want to animate to a position that is at the exact top of the nearest row when ending a drag. While dragging, ie. if (state.down), you want to animate to the exact coordinates. When the mouse is released, ie. else, you want to compute the desired resting position and animate to that.
I'm calling this value snapOffsetY, and you can compute it by rounding how many boxes you've scrolled:
const snapIndex = Math.round(offsetY / boxHeight);
const snapOffsetY = snapIndex * boxHeight;
Instead of calling setOffsetY on every movement, I'm using a component state to store the last offset which was snapped to. So I'm only setting it on drag release.
We can then use this lastOffsetY as part of the bounds calculation. Initially, bottom is 0 meaning that we can't drag above the top. But you need to be able to scroll back up once you've scrolled down the list. So the bounds need to change based on the last stopping position.
bounds: {
bottom: 0 - lastOffsetY,
top: -1 * (totalHeight - containerHeight) - lastOffsetY
},
My code looks like this:
import React, { useState } from "react";
import { animated, config, useSpring } from "#react-spring/web";
import { useDrag } from "react-use-gesture";
import { range } from "lodash";
import "./styles.css";
export default function SnapGrid({
boxHeight = 75,
rows = 10,
containerHeight = 300
}) {
const totalHeight = rows * boxHeight;
const [lastOffsetY, setLastOffsetY] = useState(0);
const [animation, springApi] = useSpring(
() => ({
offsetY: 0,
config: config.default
}),
[]
);
const bind = useDrag(
(state) => {
const [, currentY] = state.movement;
const offsetY = lastOffsetY + currentY;
// while animating
if (state.down) {
springApi.set({ offsetY: offsetY });
}
// when stopping
else {
const snapIndex = Math.round(offsetY / boxHeight);
const snapOffsetY = snapIndex * boxHeight;
setLastOffsetY(snapOffsetY);
springApi.start({
to: {
offsetY: snapOffsetY
},
config: {
velocity: state.velocity
}
});
}
},
{
axis: "y",
bounds: {
bottom: 0 - lastOffsetY,
top: -1 * (totalHeight - containerHeight) - lastOffsetY
},
rubberband: true
}
);
return (
<div className="container">
<animated.ul
{...bind()}
className="grid"
style={{
y: animation.offsetY,
touchAction: "none",
height: containerHeight
}}
>
{range(0, 5 * rows).map((n) => (
<li key={n} className="box" style={{ height: boxHeight }}>
{n}
</li>
))}
</animated.ul>
</div>
);
}
Code Sandbox Demo
Are you sure you need to put this logic into JS? CSS might be all you need here:
.scroller {
scroll-snap-type: y mandatory;
max-height: 16em;
overflow: scroll;
letter-spacing: 2em;
}
.row {
scroll-snap-align: start;
box-sizing: border;
padding: 1em;
}
.row:nth-child(2n) {
background: #bbb;
}
<div class = "scroller">
<div class = "row">AAAA</div>
<div class = "row">BBBB</div>
<div class = "row">CCCC</div>
<div class = "row">AAAA</div>
<div class = "row">BBBB</div>
<div class = "row">CCCC</div>
<div class = "row">AAAA</div>
<div class = "row">BBBB</div>
<div class = "row">CCCC</div>
<div class = "row">AAAA</div>
<div class = "row">BBBB</div>
<div class = "row">CCCC</div>
<div class = "row">AAAA</div>
<div class = "row">BBBB</div>
<div class = "row">CCCC</div>
<div class = "row">AAAA</div>
<div class = "row">BBBB</div>
<div class = "row">CCCC</div>
</div>

Get the second element under mouse that isn't a parent of the first element

I am making a card drag and drop board in HTML5 similar to Trello.
I am working on attaching the lists to list slots/containers.
When I am onMouseDown on a list, the list will move with the mouse. I need to be able to check what list container is below the mouse pointer when I onMouseUp. This will allow me to then attach the list to that container.
currently I am having difficulty in trying to get the container element under the mouse. As the list is directly under the mouse at the time I am trying to see what's under the mouse.
I cant get the parent element of the list I'm dragging as the parent will always return the original list parent, As I am dragging the list with changing the Top and Left attributes which doesn't change the parent element.
So essentially I need to see what's under the mouse, excluding the list that I am dragging. When I'm dragging over list slot 1 I need to get that element, and when dragging over list slot 2 I need to get that element.
import React, { useEffect, useState } from "react";
export default function Main() {
const [mousedown, setmousedown] = useState(0);
const [etop, setetop] = useState(0)
const [epostiontype, setepostiontype] = useState("relative")
const [eleft, seteleft] = useState(0)
///////////////////
let offsetX, offsetY;
const move = e => {
const el = e.target;
el.style.left = `${e.pageX - offsetX}px`;
el.style.top = `${e.pageY - offsetY}px`;
};
const add = e => {
const el = e.target;
offsetX = e.clientX - el.getBoundingClientRect().left;
offsetY = e.clientY - el.getBoundingClientRect().top;
el.addEventListener('mousemove', move);
};
const remove = e => {
const el = e.target;
el.removeEventListener('mousemove', move);
};
///////////////////////
const [list_states, setlist_states] = useState(
[
{
name: "This is list 1",
id: 1,
top: 0,
left: 0
}
]
);
var getParents = function (elem) {
// Set up a parent array
var parents = [];
// Push each parent element to the array
for (; elem && elem !== document; elem = elem.parentNode) {
parents.push(elem);
}
// Return our parent array
return parents;
};
function mouse_moving_in_container(props) {
// gets mouse position
var x = props.pageX, y = props.pageY
var element = document.elementFromPoint(x, y);
console.log(element)
// checks if mouse is down over a list. The mousedown var is the element id of the lsit that is being clicked. If no list is being clicked then the var is 0
if (mousedown != 0) {
var difference = props.pageY - document.getElementById("List_1").offsetTop
var mouse_top = props.pageY - 10
var mouse_left = props.pageX - 10
setepostiontype("absolute")
setetop(mouse_top)
seteleft(mouse_left)
// gets the element under the mouse
var elementMouseIsOver = document.elementFromPoint(x, y)
//returns array of all parents for the element above
var element_parents = getParents(elementMouseIsOver)
element_parents.forEach(element => {
// console.log(element.id)
if (element.id.includes("Container")){
// console.log("TThere is a <List> container under mouse. ID: ")
}
else {
//console.log("There is NO <List> container under mouse.")
}
});
}
}
return (
<div className={"testing"}>
<Container >
<Slot>
<List></List>
</Slot>
<Slot>
</Slot>
</Container>
</div>
);
function change_mouse_status(event) {
// console.log("change status",event)
setmousedown(event)
}
function List() {
return (
<div
id="List_1" className="width" style={{
height: "100px",
width: "180px",
background: "red",
position: `${epostiontype}`,
// "relative",
top: `${etop}`,
left: `${eleft}`
}}
onMouseUp={() => change_mouse_status(0)}
onMouseDown={() => change_mouse_status(1)}
>
this is a list
</div>
);
}
function Slot(props) {
return (
<div id="slot_1" style={{ display: "inline-flex", padding: "10px", margin: "10px", backgroundColor: "#e8e8ff", height: "200px", width: "200px" }}>
{props.children}
</div>
);
}
function Container(props) {
return (
<div id="Container_1" onMouseMove={mouse_moving_in_container}
name='testname'
style={{
display: "inline-flex",
backgroundColor: "#94e49d38",
height: "400px",
width: "100vw-10px"
}}
>
{props.children}
</div>
);
}
}
That is what I have so far.
Any assistant would be appreciated.
I solved the issue by setting the slots pointerevents to none when its being dragged. And set back to default when its not being dragged.
I also changed the onMouseDown and onMouseUp event handler to the body of the page to still be able to detect the event.
import React, { useEffect, useState } from "react";
export default function Main() {
const [mousedown, setmousedown] = useState("0");
const [etop, setetop] = useState(0)
const [epostiontype, setepostiontype] = useState("relative")
const [eleft, seteleft] = useState(0)
const [slot_pointer_events, setslot_pointer_events] = useState("")
const [list_states, setlist_states] = useState(
[
{
name: "This is list 1",
id: 1,
top: 0,
left: 0
}
]
);
var getParents = function (elem) {
// Set up a parent array
var parents = [];
// Push each parent element to the array
for (; elem && elem !== document; elem = elem.parentNode) {
parents.push(elem);
}
// Return our parent array
return parents;
};
document.body.onmousemove = function (props) {
var x = props.pageX, y = props.pageY
// gets mouse position
var element = document.elementFromPoint(x, y);
//console.log(element)
// checks if mouse is down over a list. The mousedown var is the element id of the lsit that is being clicked. If no list is being clicked then the var is 0
if (mousedown != 0) {
setslot_pointer_events("none")
var difference = y - document.getElementById("List_1").offsetTop
var mouse_top = y - 10
var mouse_left = x - 10
setepostiontype("absolute")
setetop(mouse_top)
seteleft(mouse_left)
// gets the element under the mouse
var elementMouseIsOver = document.elementFromPoint(x, y)
//returns array of all parents for the element above
var element_parents = getParents(elementMouseIsOver)
console.log("this: ", elementMouseIsOver)
element_parents.forEach(element => {
// console.log(element.id)
if (element.id.includes("Slot")) {
// console.log("TThere is a <Slot> container under mouse. ID: ")
}
else {
// console.log("There is NO <Slot> container under mouse.")
}
});
}
else {
setslot_pointer_events("")
}
}
var mouseDown = 0;
document.body.onmousedown = function (props) {
var x = props.pageX, y = props.pageY
var element = document.elementFromPoint(x, y);
if (element.id.includes("List")) {
setmousedown(element.id)
}
else {
setmousedown("0")
}
}
document.body.onmouseup = function () {
setmousedown("0")
}
return (
<div className={"testing"}>
<Container >
<Slot
slot_id="Slot_1"
>
<List></List>
</Slot>
<Slot
slot_id="Slot_2">
</Slot>
</Container>
</div>
);
function List() {
return (
<div
id="List_1" className="width" style={{
height: "100px",
width: "180px",
background: "red",
pointerEvents:
`${slot_pointer_events}`,
position: `${epostiontype}`,
// "relative",
top: `${etop}`,
left: `${eleft}`
}}
onMouseUp={() => change_mouse_status(0)}
onMouseDown={() => change_mouse_status(1)}
>
this is a list
</div>
);
}
function Slot(props) {
return (
<div id={props.slot_id} style={{
display: "inline-flex", padding: "10px", margin: "10px", backgroundColor: "#e8e8ff", height: "200px", width: "200px"
}}>
{props.children}
this is slot (lsit container)
</div>
);
}
function Container(props) {
return (
<div id="Container_1"
name='testname'
style={{
display: "inline-flex",
backgroundColor: "#94e49d38",
height: "400px",
width: "100vw-10px"
}}
>
{props.children}
</div>
);
}
}```

image coords changing on browser width change

Hi I am clicking on particular point on image and displaying a icon on selected area. When i change resolution that point is moving to other part of image. below is my code.How to fix the coords on resolution or browser width change. Added eventlistner and need to know on window resize any calculation have to be done?
My component is:
import React, { Component } from "react";
import { withRouter } from "react-router-dom";
import { connect } from "react-redux";
import { showCardDetails } from "../../store/Action";
let temp = [];
class cardDetails extends Component {
constructor(props) {
super(props);
}
state = {
left: "",
right: "",
coords: []
};
componentDidMount() {
const { dispatch } = this.props;
console.log(this.props.backOffice + "props");
console.log(this.props.match.params.userId);
let coords = JSON.parse(localStorage.getItem("coords"));
this.setState({ coords: coords });
window.addEventListener("resize", this.handlePress);
}
handlePress = () => {
const { coords } = this.state;
console.log(this.state);
//anycalculation here?
};
handleclick = e => {
var canvas = document.getElementById("imgCoord");
var w = document.documentElement.clientWidth;
var h = document.documentElement.clientHeight;
console.log(e.offsetLeft);
var x =
e.clientX +
document.body.scrollLeft +
document.documentElement.scrollLeft;
var y =
e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
let left = x + "px";
let top = y + "px";
let obj = {
left: left,
top: top,
width: w,
height: h
};
temp.push(obj);
this.setState({ left: left, top: top, coords: temp });
localStorage.setItem("coords", JSON.stringify(temp));
};
render() {
const { backOffice, match } = this.props;
const { left, top, coords } = this.state;
console.log(coords);
return (
<React.Fragment>
<div>
<img
id="imgCoord"
src={require("../../assets/images/imageTagging.PNG")}
onClick={$event => this.handleclick($event)}
/>
{coords &&
coords.map(item => (
<img
style={{
width: "20px",
position: "absolute",
left: item.left,
top: item.top,
backgroundColor: "white"
}}
src={require("../../assets/images/tag.png")}
id="marker"
/>
))}
</div>
</React.Fragment>
);
}
}
/**
* Redux map state
#param {} state
#param {} ownParams
*/
function mapStateToProps(state, ownParams) {
console.log(state);
return {
backOffice: state.selectedCardData
};
}
export default withRouter(connect(mapStateToProps)(cardDetails));
[EDITED]
you need to add a event listener (here is docs https://www.w3schools.com/jsref/event_onresize.asp ) in componentDidMount method, and remove this listener in componentWillUnmount method.
inside listener put a function to set new coordinates.
and also need to calculate a percentage of icon's coordinates relative to width and height of the window
handleclick = e => {
...your previous code
const yPercent = h / top;
const xPercent = w / left;
this.setState({ yPercent, xPercent });
};
handleResize = () => {
var w = document.documentElement.clientWidth;
var h = document.documentElement.clientHeight;
const newTop = (this.state.yPercent * h) + "px";
const newLeft = (this.state.xPercent * w) + "px";
this.setState({ left: newLeft; top: newTop });
}

React and lodash: applying a throttle or debounce to a function while maintaining the event

I have a relatively simply component which looks as follows (simplified)
class MouseListener extends Component {
static propTypes = {
children: PropTypes.element,
onMouseMove: PropTypes.func.isRequired
};
container = null;
onMouseMove = debounce(e => {
e.persist();
const { clientX, clientY } = e;
const { top, left, height, width } = this.container.getBoundingClientRect();
const x = (clientX - left) / width;
const y = (clientY - top) / height;
this.props.onMouseMove(x, y);
}, 50);
render() {
return (<div
ref={e => this.container = e}
onMouseMove={this.onMouseMove}
>
{this.props.children}
</div>);
}
}
However, whenever the onMouseMove is triggered, it says that the event has already passed. I expected to be able to use e.persist() to keep it in the pool for this event handler to run asynchronously.
What is the appropriate way to bind a debounced event handler in React?

Categories