reactjs draggable and resizeable component - javascript

for the past few hours I have been trying to search for a way to make a react component draggable and resizable. I have found a way to make it draggable with react drag and drop, but I can't find a simple way to make it resizeable :/
Does anyone have any experience on how to make a component draggable and resizable?
Any help or pointers are appreciated!

Using react-grid-layout to solve this problem. Specifically for a scheduling widget where users can scale time blocks and drag and drop them along a horizontal timeline.
react-grid-layout provides a grid with draggable and resizable widgets plus responsive layout, optional auto-packing, among other features.
var ReactGridLayout = require('react-grid-layout');
// React component render function:
render: function() {
return (
<ReactGridLayout className="layout" cols={12} rowHeight={30}>
<div key={1} _grid={{x: 0, y: 0, w: 1, h: 2}}>1</div>
<div key={2} _grid={{x: 1, y: 0, w: 1, h: 2}}>2</div>
<div key={3} _grid={{x: 2, y: 0, w: 1, h: 2}}>3</div>
</ReactGridLayout>
)
}
The child nodes are draggable and resizable. The layout defined in each child "_grid" prop can alternatively be defined directly on the parent "layout" prop:
// React component render function:
render: function() {
// layout is an array of objects, see the demo
var layout = getOrGenerateLayout();
return (
<ReactGridLayout className="layout" layout={layout} cols={12} rowHeight={30}>
<div key={1}>1</div>
<div key={2}>2</div>
<div key={3}>3</div>
</ReactGridLayout>
)
}
Callback functions can be passed to the components as props. Hooking into these should allow you to define any custom behavior:
// Calls when drag starts.
onDragStart: React.PropTypes.func,
// Calls on each drag movement.
onDrag: React.PropTypes.func,
// Calls when drag is complete.
onDragStop: React.PropTypes.func,
// Calls when resize starts.
onResizeStart: React.PropTypes.func,
// Calls when resize movement happens.
onResize: React.PropTypes.func,
// Calls when resize is complete.
onResizeStop: React.PropTypes.func
Code sample from docs:
https://github.com/STRML/react-grid-layout
Demo here:
https://strml.github.io/react-grid-layout/examples/0-showcase.html

I've been using react-rnd and am really happy with it: https://github.com/bokuweb/react-rnd

https://github.com/STRML/react-resizable
This answer is only for resizable component. You can find other answer which has both functionalities.
'use strict';
var React = require('react/addons');
typeof window !== "undefined" && (window.React = React); // for devtools
typeof window !== "undefined" && (window.Perf = React.addons.Perf); // for devtools
var _ = require('lodash');
var ResizableBox = require('../lib/ResizableBox.jsx');
var Resizable = require('../lib/Resizable.jsx');
require('style!css!../css/styles.css');
var TestLayout = module.exports = React.createClass({
displayName: 'TestLayout',
getInitialState() {
return {width: 200, height: 200};
},
onClick() {
this.setState({width: 200, height: 200})
},
onResize(event, {element, size}) {
this.setState({width: size.width, height: size.height});
},
render() {
return (
<div>
<button onClick={this.onClick} style={{'marginBottom': '10px'}}>Reset first element's width/height</button>
<Resizable className="box" height={this.state.height} width={this.state.width} onResize={this.onResize}>
<div className="box" style={{width: this.state.width + 'px', height: this.state.height + 'px'}}>
<span className="text">{"Raw use of <Resizable> element. 200x200, no constraints."}</span>
</div>
</Resizable>
<ResizableBox className="box" width={200} height={200}>
<span className="text">{"<ResizableBox>, same as above."}</span>
</ResizableBox>
<ResizableBox className="box" width={200} height={200} draggableOpts={{grid: [25, 25]}}>
<span className="text">Resizable box that snaps to even intervals of 25px.</span>
</ResizableBox>
<ResizableBox className="box" width={200} height={200} minConstraints={[150, 150]} maxConstraints={[500, 300]}>
<span className="text">Resizable box, starting at 200x200. Min size is 150x150, max is 500x300.</span>
</ResizableBox>
<ResizableBox className="box box3" width={200} height={200} minConstraints={[150, 150]} maxConstraints={[500, 300]}>
<span className="text">Resizable box with a handle that only appears on hover.</span>
</ResizableBox>
<ResizableBox className="box" width={200} height={200} lockAspectRatio={true}>
<span className="text">Resizable square with a locked aspect ratio.</span>
</ResizableBox>
<ResizableBox className="box" width={200} height={120} lockAspectRatio={true}>
<span className="text">Resizable rectangle with a locked aspect ratio.</span>
</ResizableBox>
</div>
);
}
});

Related

Popover does not show up in React

I am trying to make a webapplication with Treeviz dependency. The goal is to place a popover button to each node of the tree and if user clicks to the button he/she can see the description of the node,and after it should be editable. I tried in many ways but for me popover does not work in React.
There is an example for what I would like to do. You can see I have to insert React component to HTML therefor I am using renderToString. All you have to look is the renderNode property of the tree. I am referencing to React component in renderNode like: ${tooltip} ${popover}.
import React from "react";
import { TreevizReact } from "treeviz-react";
import { renderToString } from "react-dom/server";
const data_1 = [
{
id: 1,
text_1: "Chaos",
description: "Void",
father: null,
color: "#FF5722"
},
{
id: 2,
text_1: "Tartarus",
description: "Abyss",
father: 1,
color: "#FFC107"
},
{ id: 3, text_1: "Gaia", description: "Earth", father: 1, color: "#8BC34A" },
{ id: 4, text_1: "Eros", description: "Desire", father: 1, color: "#00BCD4" }
];
export const App = () => {
return (
<div style={{ marginLeft: 10 }}>
<div style={{ display: "flex" }}>
<TreevizReact
data={data_1}
relationnalField={"father"}
nodeWidth={120}
nodeHeight={80}
areaHeight={500}
areaWidth={1000}
mainAxisNodeSpacing={2}
secondaryAxisNodeSpacing={2}
linkShape={"quadraticBeziers"}
renderNode={(data) => {
const nodeData = data.data;
const settings = data.settings;
let result = "";
const tooltip = renderToString(
<strong
data-toggle="tooltip"
data-placement="top"
title={nodeData.description}
>
{nodeData.text_1}
</strong>
);
const popover = renderToString(
<button
type="button"
className="btn btn-secondary"
data-container="body"
data-toggle="popover"
data-placement="top"
data-content={nodeData.description}
>
Popover on top
</button>
);
if (data.depth !== 2) {
result = `<div className="box"
style='cursor:pointer;height:${settings.nodeHeight}px; width:${settings.nodeWidth}px;display:flex;flex-direction:column;justify-content:center;align-items:center;background-color:${nodeData.color};border-radius:5px;'>
<div>
${tooltip}
${popover}
</div></div>`;
} else {
result = `<div className="box" style='cursor:pointer;height:${
settings.nodeHeight
}px; width:${
settings.nodeHeight
}px;display:flex;flex-direction:column;justify-content:center;align-items:center;background-color:${
nodeData.color
};border-radius:${settings.nodeHeight / 2}px;'><div><strong>
${nodeData.text_1}
</strong></div></div>`;
}
return result;
}}
duration={600}
isHorizontal
linkWidth={(node) => 10}
/>
</div>
</div>
);
};
export default App;
Tooltip is working but popover does not show up.
You can try it: https://codesandbox.io/s/zealous-orla-4bq5f?file=/src/App.js
Also tried
const popover = renderToString(
<Popup
trigger={<button> Trigger</button>}
position="right center"
>
<form onSubmit={saveHandler}>
<ContentEditable
html={text.current}
onChange={handleChange}
/>
<button type="submit">Save</button>
<button>Cancel</button>
</form>
</Popup>
const popoverContent = (
<Popover id="popover-basic">
<Popover.Header as="h3">Popover right</Popover.Header>
<Popover.Body>
And here's some <strong>amazing</strong> content. It's very
engaging. right?
</Popover.Body>
</Popover>
);
const popover = renderToString(
<OverlayTrigger
trigger="click"
placement="right"
overlay={popoverContent}
>
<Button variant="success">Click me to see</Button>
</OverlayTrigger>
);
None of them worked for me.
Probably your approach doesn't work because the dom elements in the tree are created dynamically, and bootstrap doesn't set them up.
A more react-ish way to do it would be using react-bootstrap lib and managing every UI aspect in states. To implement the tooltip, the Overlay component actually as a prop called target that allows you to change over what element the tooltip is shown.
<Overlay target={tooltipTarget} show={showTooltip} placement="right">
{(props) => (
<Tooltip id="overlay-example" {...props}>
{tooltipNode?.data.text_1}
</Tooltip>
)}
</Overlay>
Then you only need to manage all these states in the onNodeMouseEnter and onNodeMouseLeave handlers in the TreevizReact component.
onNodeMouseEnter={(_event, node) => {
const t = document.querySelector(`#node-${node.id}`);
setTooltipTarget(t);
setShowTooltip(true);
setTooltipNode(node);
}}
onNodeMouseLeave={(_event, node) => {
setTooltipTarget(null);
setShowTooltip(false);
setTooltipNode(null);
}}
The popup follows the same logic with another set of states.
<div ref={ref}>
<Overlay
show={!!selectedNode.current}
target={target}
placement="bottom"
container={ref.current}
containerPadding={20}
>
<Popover id="popover-contained">
{/* hack to avoid weird blinking (due mutable state) */}
{!!selectedNode.current && (
<>
some form based on node data
{JSON.stringify(selectedNode.current?.data)}
</>
)}
</Popover>
</Overlay>
</div>
and in onNodeClick handler
onNodeClick={(_event, node) => {
const t = document.querySelector(`#node-${node.id}`);
// unselect if already selected
if (selectedNode.current?.id === node.id) {
selectedNode.current = null;
setTarget(null);
} else {
selectedNode.current = node;
setTarget(t);
}
}}
You might notice this time I used a mutable variable via a ref (selectedNode.current), for some reason using a state caused some issues, maybe due to D3 and react having different rendering cycles, reason why you'll probably encounter some other glitches in this implementation.
https://codesandbox.io/s/quizzical-buck-slofh?file=/src/App.js

React ContextMenu and Draggable

I am trying to make a Draggable component with custom context menu in ReactJS, using
react-draggable and react-contextmenu with no success.
I`ve had succeeded to make a button draggable but when I add the context menu it disables the draggable and only the contextmenu works.
I would love to combine them together somehow, thanks!
import React, { useState } from 'react';
import Draggable from 'react-draggable';
import {ContextMenu, ContextMenuTrigger} from 'react-contextmenu';
export default function DraggableButton() {
const [deltaPosition, setDeltaPosition] = useState({ x: 0, y: 0 });
const handleDrag = (e, ui) => {
const { x, y } = deltaPosition;
setDeltaPosition({
x: x + ui.deltaX,
y: y + ui.deltaY,
});
};
return (
<div>
<Draggable
scale={1}
onDrag={handleDrag}
bounds="parent"
defaultPosition={{ x: 40, y: 40 }}
allowAnyClick={false}
>
<ContextMenuTrigger id='trigger' >
<div className="w-24">
<button className="btn btn-primary">
click me
</button>
</div>
</ContextMenuTrigger>
</Draggable>
<ContextMenu id='trigger'>
<h1>test</h1>
</ContextMenu>
</div>
);}
Try wrapping Draggable around ContextMenuTrigger:
<ContextMenuTrigger id='trigger'>
<Draggable
scale={1}
onDrag={handleDrag}
bounds="parent"
defaultPosition={{ x: 40, y: 40 }}
allowAnyClick={false}>
<div className="w-24">
<button className="btn btn-primary">click me</button>
</div>
</Draggable>
</ContextMenuTrigger>
Note: the context menu opens in a different location with this.
Leaving this here as it may help others.

Framer motion animate when element is in-view (When you scroll to element)

Is there any built-in way to make the animation start when the element is in-view (for example, when we scroll to the element)?
Framer Motion has mount animations section which says:
When a component mounts, it'll automatically animate to the values in animate if they're different from those defined in style or initial
So I couldn't really find a straight forward way to make the animation start when it comes into view.
However, I reached the only option I see for now is using Animation Controls which means I'll have to implement a listener on the scroll manually and trigger the control.start(), any easier ways are much appreciated.
framer-motion has built-in support for this use case since version 5.3.
Here's a CodeSandbox demonstrating the pattern: https://codesandbox.io/s/framer-motion-animate-in-view-5-3-94j13
Relevant code:
function FadeInWhenVisible({ children }) {
return (
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ duration: 0.3 }}
variants={{
visible: { opacity: 1, scale: 1 },
hidden: { opacity: 0, scale: 0 }
}}
>
{children}
</motion.div>
);
}
Usage:
<FadeInWhenVisible>
<Box />
</FadeInWhenVisible>
Previous versions:
You can currently use the imperative animation controls to achieve this effect. Intersection observers are useful to detect if an element is currently visible.
Here's a CodeSandbox demonstrating the pattern: https://codesandbox.io/s/framer-motion-animate-in-view-gqcc8.
Relevant code:
function FadeInWhenVisible({ children }) {
const controls = useAnimation();
const [ref, inView] = useInView();
useEffect(() => {
if (inView) {
controls.start("visible");
}
}, [controls, inView]);
return (
<motion.div
ref={ref}
animate={controls}
initial="hidden"
transition={{ duration: 0.3 }}
variants={{
visible: { opacity: 1, scale: 1 },
hidden: { opacity: 0, scale: 0 }
}}
>
{children}
</motion.div>
);
}

What is the correct way to set the new coordinates of a view after translation animation in react-native?

I am trying to run a few simple animations using react-native-animatable library. (But I believe the question should be generic to any react animations so adding other tags as well.)
The problem is, in the first time, the image animates just as expected. But when aimed to start second animation animation with the gesture, the image translation starts from its original coordinates.
A search yielt, in Android development (which is obviously not my case) there seems a method, setFillAfter which sets the coordinate after the animation.
My question is, how to set the location (left / top values for example) to the final translated point so that consecutive animation starts from the point the previous translation left.
The expo snack for below code block is here.
import * as React from 'react';
import { Image, StyleSheet, ImageBackground } from 'react-native';
import * as Animatable from 'react-native-animatable';
import { PanGestureHandler, State } from 'react-native-gesture-handler';
import testImg from './test.png';
import backImg from './back.png';
export default class App extends React.Component {
onTestMove(event) {
this.testAnimRef.transitionTo({
translateX: event.nativeEvent.translationX,
translateY: event.nativeEvent.translationY,
}, 0);
}
render() {
return (
<ImageBackground source={backImg} style={{ flex: 1 }} >
<PanGestureHandler
key={`test`}
onGestureEvent={(e) => { this.onTestMove(e) }}
onHandlerStateChange={e => { }}
>
<Animatable.View style={styles._animatable_view}
ref={((ref) => { this.testAnimRef = ref }).bind(this)}
useNativeDriver={true}
>
<Image source={testImg} style={styles._image} />
</Animatable.View>
</PanGestureHandler>
</ImageBackground>
);
}
}
const styles = StyleSheet.create({
_image: {
width: 50,
height: 25,
resizeMode: 'contain',
backgroundColor: 'black',
borderColor: 'gainsboro',
borderWidth: 2,
},
_animatable_view: {
position: "absolute",
top: 200,
left: 100,
},
});
I had the same problem trying to move around some cards in a view, and upon further dragging, they would reset to their origin.
My theory is/was that while the translated view would have its x / y coordinates translated, this would not apply to the parent of that view, and so the animated event passed from that component would initially have the original coordinates (nuke me if I'm wrong here)
So my solution was to keep an initial offset value in state, and maintain this every time the user releases the dragged motion
_onHandleGesture: any
constructor(props: OwnProps) {
super(props)
this.state = {
animationValue: new Animated.ValueXY({ x: 0, y: 0 }),
initialOffset: { x: 0, y: 0 },
}
this._onHandleGesture = (e: PanGestureHandlerGestureEvent) => {
this.state.animationValue.setValue({
x: e.nativeEvent.translationX + this.state.initialOffset.x, <- add initial offset to coordinates passed
y: e.nativeEvent.translationY + this.state.initialOffset.y,
})
}
}
_acceptCard = (cardValue: number) => {
const { targetLocation, onAccept } = this.props
const { x, y } = targetLocation
onAccept(cardValue)
Animated.spring(this.state.animationValue, {
// Some animation here
}).start(() => {
this.setState({ initialOffset: targetLocation }) // <- callback to set state value for next animation start
})
}
and the render method
<PanGestureHandler
onHandlerStateChange={this.onPanHandlerStateChange}
onGestureEvent={this._onHandleGesture}
failOffsetX={[-xThreshold, xThreshold]}
>
<Animated.View
style={{
position: "absolute",
left: 0,
top: 0,
transform: [{ translateX: this.state.animationValue.x }, { translateY: this.state.animationValue.y }],
}}
>
<CardTile size={size} content={content} layout={layout} backgroundImage={backgroundImage} shadow={shadow} />
</Animated.View>
</PanGestureHandler>
This example is based on the react-native-gesture-handler library, but the concept should apply to other solutions.
Dont know if this way is advisable, though it is functional.
Hope this helps!

React add Component as a variable

I have a simple problem to solve. I've created a react app using npx create-react-app. I made a Map component, which I add to my view with this:
class App extends Component {
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Welcome to React</h1>
</header>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
<p>
<Map />
</p>
</div>
);
}
}
And the Map component appears. Now I want to have a button which has an onClick method which calls Map.addImage. First of all it cannot be static (addImage uses Map members, which have to be initialized in constructor). The problem is I know I would have to do var map = new Map(). And then <button type="button" onClick={map.addImage} /> but how can I make my map appear? I cannot go for:
<p>
<map />
</p>
So the question is how can I make my map (after var map = new Map() in render() method, above the return) appear on the screen?
#Edit
Map implementation:
export default class Map extends React.Component {
constructor(props) {
super(props);
this.stage = null;
this.layer = null;
}
componentDidMount() {
//const tween = null;
this.stage = new Konva.Stage({
container: this.containerRef,
width: 1024,
height: 600
});
this.layer = new Konva.Layer();
//const dragLayer = new Konva.Layer();
this.stage.add(this.layer);
/*stage.on("dragstart", function (evt) {
const shape = evt.target;
// moving to another layer will improve dragging performance
shape.moveTo(dragLayer);
stage.draw();
if (tween) {
tween.pause();
}
shape.setAttrs({
shadowOffset: {
x: 15,
y: 15
},
scale: {
x: shape.getAttr("startScale") * 1.2,
y: shape.getAttr("startScale") * 1.2
}
});
});
stage.on("dragend", function (evt) {
const shape = evt.target;
shape.moveTo(layer);
stage.draw();
shape.to({
duration: 0.5,
easing: Konva.Easings.ElasticEaseOut,
scaleX: shape.getAttr("startScale"),
scaleY: shape.getAttr("startScale"),
shadowOffsetX: 5,
shadowOffsetY: 5
});
});*/
}
render() {
return (
<div
className="container"
ref={ref => {
console.log(ref);
this.containerRef = ref;
}}
/>
);
}
addImage() {
var imageObj = new Image();
imageObj.src = './box.png';
imageObj.misc = { stage: this.stage, layer: this.layer };
console.log(this.stage)
imageObj.onload = function () {
var image = new Konva.Image({
x: Math.random() * this.misc.stage.getWidth(),
y: Math.random() * this.misc.stage.getHeight(),
width: 100,
height: 100,
image: imageObj,
draggable: true
});
this.misc.layer.add(image);
this.misc.layer.draw();
};
}
}
As Oblosys said, you generally don't want to try to do this in React. If you must, go with the ref route. It's there for a reason.
But I would suggest considering lifting the images up to your App state and passing them down as props. It's hard to say what this would look like as you have not provided an implementation for the Map component, but it would look something like this.
class App extends Component {
constructor() {
super();
this.state = { images: [] };
this.addImage = this.addImage.bind(this);
}
addImage() {
const newImage = 5; // I obviously have no idea what this actually looks like
this.setState(({ images }) => ({ images: [...images, newImage] }));
}
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Welcome to React</h1>
</header>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
<p>
<Map images={this.state.images} />
</p>
<button onClick={this.addImage}>Add an image</button>
</div>
);
}
}
This won't work well in all cases (for example, if you're using a third party tool in Map), but this is more The React Way.

Categories