And*_*nor 82 javascript reactjs
我想制作一个可拖动的(也就是说,可以通过鼠标重新定位)React组件,它似乎必然涉及全局状态和分散的事件处理程序.我可以用脏的方式,在我的JS文件中使用全局变量,甚至可以将它包装在一个漂亮的闭包界面中,但我想知道是否有一种方法可以更好地与React相结合.
此外,由于我之前从未在原始JavaScript中完成此操作,因此我想看看专家是如何做到这一点的,以确保我已经处理了所有角落案例,特别是与React相关的案例.
谢谢.
Jar*_*yth 96
我应该把它变成一篇博文,但这里有一个非常可靠的例子.
评论应该很好地解释,但如果您有疑问,请告诉我.
这里有小提琴:http://jsfiddle.net/Af9Jt/2/
var Draggable = React.createClass({
getDefaultProps: function () {
return {
// allow the initial position to be passed in as a prop
initialPos: {x: 0, y: 0}
}
},
getInitialState: function () {
return {
pos: this.props.initialPos,
dragging: false,
rel: null // position relative to the cursor
}
},
// 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: function (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: function (e) {
// only left mouse button
if (e.button !== 0) return
var pos = $(this.getDOMNode()).offset()
this.setState({
dragging: true,
rel: {
x: e.pageX - pos.left,
y: e.pageY - pos.top
}
})
e.stopPropagation()
e.preventDefault()
},
onMouseUp: function (e) {
this.setState({dragging: false})
e.stopPropagation()
e.preventDefault()
},
onMouseMove: function (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: function () {
// transferPropsTo will merge style & other props passed into our
// component to also be on the child DIV.
return this.transferPropsTo(React.DOM.div({
onMouseDown: this.onMouseDown,
style: {
left: this.state.pos.x + 'px',
top: this.state.pos.y + 'px'
}
}, this.props.children))
}
})
Run Code Online (Sandbox Code Playgroud)
"谁应该拥有什么样的国家"是一个重要的问题,从一开始就要回答.在"可拖动"组件的情况下,我可以看到一些不同的场景.
父母应该拥有可拖动的当前位置.在这种情况下,draggable仍将拥有"我在拖动"状态,但this.props.onChange(x, y)只要发生mousemove事件就会调用.
父母只需要拥有"非移动位置",因此可拖动将拥有它的"拖动位置",但是,它会调用this.props.onChange(x, y)并将最终决定推迟到父级.如果父级不喜欢可拖动的最终位置,则它不会更新它的状态,并且拖动将在拖动之前"快速"回到它的初始位置.
@ssorallen指出,因为"draggable"本身就是一个属性,而不是一个东西,它可能更适合作为mixin.我对mixin的经验是有限的,所以我还没有看到他们在复杂的情况下如何帮助或妨碍他们.这可能是最好的选择.
Dan*_*mov 61
我实现了react-dnd,一个灵活的HTML5拖放混合,用于React和完整的DOM控件.
现有的拖放库不适合我的用例,所以我写了自己的.它类似于我们在Stampsy.com上运行了大约一年的代码,但改写后利用了React和Flux.
我的主要要求:
如果这些听起来很熟悉,请继续阅读.
首先,声明可以拖动的数据类型.
这些用于检查拖动源和放置目标的"兼容性":
// ItemTypes.js
module.exports = {
BLOCK: 'block',
IMAGE: 'image'
};
Run Code Online (Sandbox Code Playgroud)
(如果您没有多种数据类型,则此库可能不适合您.)
然后,让我们创建一个非常简单的可拖动组件,当拖动时,它代表IMAGE:
var { DragDropMixin } = require('react-dnd'),
ItemTypes = require('./ItemTypes');
var Image = React.createClass({
mixins: [DragDropMixin],
configureDragDrop(registerType) {
// Specify all supported types by calling registerType(type, { dragSource?, dropTarget? })
registerType(ItemTypes.IMAGE, {
// dragSource, when specified, is { beginDrag(), canDrag()?, endDrag(didDrop)? }
dragSource: {
// beginDrag should return { item, dragOrigin?, dragPreview?, dragEffect? }
beginDrag() {
return {
item: this.props.image
};
}
}
});
},
render() {
// {...this.dragSourceFor(ItemTypes.IMAGE)} will expand into
// { draggable: true, onDragStart: (handled by mixin), onDragEnd: (handled by mixin) }.
return (
<img src={this.props.image.url}
{...this.dragSourceFor(ItemTypes.IMAGE)} />
);
}
);
Run Code Online (Sandbox Code Playgroud)
通过指定configureDragDrop,我们告诉DragDropMixin该组件的拖放行为.可拖动和可放置的组件都使用相同的mixin.
在内部configureDragDrop,我们需要调用组件支持的registerType每个自定义ItemTypes.例如,您的应用中可能会有多个图像表示形式,每个图像都会提供一个dragSourcefor ItemTypes.IMAGE.
A dragSource只是一个指定拖动源如何工作的对象.您必须实现beginDrag以返回表示您正在拖动的数据的项目,以及可选的一些调整拖动UI的选项.您可以选择实现canDrag禁止拖动,或者endDrag(didDrop)在丢弃(或尚未发生)时执行某些逻辑.您可以通过让共享mixin dragSource为它们生成来在组件之间共享此逻辑.
最后,您必须使用{...this.dragSourceFor(itemType)}某些(一个或多个)元素render来附加拖动处理程序.这意味着您可以在一个元素中包含多个"拖动句柄",它们甚至可以对应于不同的项类型.(如果您不熟悉JSX Spread Attributes语法,请查看).
假设我们希望ImageBlock成为IMAGEs 的下降目标.除了我们需要提供registerType一个dropTarget实现之外,它几乎是一样的:
var { DragDropMixin } = require('react-dnd'),
ItemTypes = require('./ItemTypes');
var ImageBlock = React.createClass({
mixins: [DragDropMixin],
configureDragDrop(registerType) {
registerType(ItemTypes.IMAGE, {
// dropTarget, when specified, is { acceptDrop(item)?, enter(item)?, over(item)?, leave(item)? }
dropTarget: {
acceptDrop(image) {
// Do something with image! for example,
DocumentActionCreators.setImage(this.props.blockId, image);
}
}
});
},
render() {
// {...this.dropTargetFor(ItemTypes.IMAGE)} will expand into
// { onDragEnter: (handled by mixin), onDragOver: (handled by mixin), onDragLeave: (handled by mixin), onDrop: (handled by mixin) }.
return (
<div {...this.dropTargetFor(ItemTypes.IMAGE)}>
{this.props.image &&
<img src={this.props.image.url} />
}
</div>
);
}
);
Run Code Online (Sandbox Code Playgroud)
假设我们现在希望用户能够将图像拖出ImageBlock.我们只需要添加适当的dragSource和一些处理程序:
var { DragDropMixin } = require('react-dnd'),
ItemTypes = require('./ItemTypes');
var ImageBlock = React.createClass({
mixins: [DragDropMixin],
configureDragDrop(registerType) {
registerType(ItemTypes.IMAGE, {
// Add a drag source that only works when ImageBlock has an image:
dragSource: {
canDrag() {
return !!this.props.image;
},
beginDrag() {
return {
item: this.props.image
};
}
}
dropTarget: {
acceptDrop(image) {
DocumentActionCreators.setImage(this.props.blockId, image);
}
}
});
},
render() {
return (
<div {...this.dropTargetFor(ItemTypes.IMAGE)}>
{/* Add {...this.dragSourceFor} handlers to a nested node */}
{this.props.image &&
<img src={this.props.image.url}
{...this.dragSourceFor(ItemTypes.IMAGE)} />
}
</div>
);
}
);
Run Code Online (Sandbox Code Playgroud)
我没有涵盖所有内容,但可以通过以下几种方式使用此API:
getDragState(type)和getDropState(type)了解拖动是否处于活动状态并使用它来切换CSS类或属性;dragPreview将Image图像用作拖动占位符(用于ImagePreloaderMixin加载它们);ImageBlocks.我们只需要他们实施dropTarget和dragSource进行ItemTypes.BLOCK.dropTargetFor(...types) 允许一次指定多个类型,因此一个放置区可以捕获许多不同的类型.有关最新文档和安装说明,请访问Github上的react-dnd repo.
raf*_*nes 20
这是一个简单的现代方法,在useState,useEffect和useRefES6 中。
import React, { useRef, useState, useEffect } from 'react'
const quickAndDirtyStyle = {
width: "200px",
height: "200px",
background: "#FF9900",
color: "#FFFFFF",
display: "flex",
justifyContent: "center",
alignItems: "center"
}
const DraggableComponent = () => {
const [pressed, setPressed] = useState(false)
const [position, setPosition] = useState({x: 0, y: 0})
const ref = useRef()
// Monitor changes to position state and update DOM
useEffect(() => {
if (ref.current) {
ref.current.style.transform = `translate(${position.x}px, ${position.y}px)`
}
}, [position])
// Update the current position if mouse is down
const onMouseMove = (event) => {
if (pressed) {
setPosition({
x: position.x + event.movementX,
y: position.y + event.movementY
})
}
}
return (
<div
ref={ ref }
style={ quickAndDirtyStyle }
onMouseMove={ onMouseMove }
onMouseDown={ () => setPressed(true) }
onMouseUp={ () => setPressed(false) }>
<p>{ pressed ? "Dragging..." : "Press to drag" }</p>
</div>
)
}
export default DraggableComponent
Run Code Online (Sandbox Code Playgroud)
pol*_*.ph 19
Jared Forsyth的回答是非常错误和过时的.它遵循一整套反模式,例如使用stopPropagation,从道具初始化状态,jQuery的使用,状态中的嵌套对象以及具有一些奇怪的dragging状态字段.如果被重写,解决方案将是以下,但它仍然强制虚拟DOM协调每次鼠标移动滴答,并不是非常高效.
UPD.我的回答是可怕的错误和过时的.现在,代码通过使用本机事件处理程序和样式更新缓解了React组件生命周期缓慢的问题,transform因为它不会导致重排,并且会限制DOM的更改requestAnimationFrame.现在,在我尝试的每个浏览器中,它始终为60 FPS.
const throttle = (f) => {
let token = null, lastArgs = null;
const invoke = () => {
f(...lastArgs);
token = null;
};
const result = (...args) => {
lastArgs = args;
if (!token) {
token = requestAnimationFrame(invoke);
}
};
result.cancel = () => token && cancelAnimationFrame(token);
return result;
};
class Draggable extends React.PureComponent {
_relX = 0;
_relY = 0;
_ref = React.createRef();
_onMouseDown = (event) => {
if (event.button !== 0) {
return;
}
const {scrollLeft, scrollTop, clientLeft, clientTop} = document.body;
// Try to avoid calling `getBoundingClientRect` if you know the size
// of the moving element from the beginning. It forces reflow and is
// the laggiest part of the code right now. Luckily it's called only
// once per click.
const {left, top} = this._ref.current.getBoundingClientRect();
this._relX = event.pageX - (left + scrollLeft - clientLeft);
this._relY = event.pageY - (top + scrollTop - clientTop);
document.addEventListener('mousemove', this._onMouseMove);
document.addEventListener('mouseup', this._onMouseUp);
event.preventDefault();
};
_onMouseUp = (event) => {
document.removeEventListener('mousemove', this._onMouseMove);
document.removeEventListener('mouseup', this._onMouseUp);
event.preventDefault();
};
_onMouseMove = (event) => {
this.props.onMove(
event.pageX - this._relX,
event.pageY - this._relY,
);
event.preventDefault();
};
_update = throttle(() => {
const {x, y} = this.props;
this._ref.current.style.transform = `translate(${x}px, ${y}px)`;
});
componentDidMount() {
this._ref.current.addEventListener('mousedown', this._onMouseDown);
this._update();
}
componentDidUpdate() {
this._update();
}
componentWillUnmount() {
this._ref.current.removeEventListener('mousedown', this._onMouseDown);
this._update.cancel();
}
render() {
return (
<div className="draggable" ref={this._ref}>
{this.props.children}
</div>
);
}
}
class Test extends React.PureComponent {
state = {
x: 100,
y: 200,
};
_move = (x, y) => this.setState({x, y});
// you can implement grid snapping logic or whatever here
/*
_move = (x, y) => this.setState({
x: ~~((x - 5) / 10) * 10 + 5,
y: ~~((y - 5) / 10) * 10 + 5,
});
*/
render() {
const {x, y} = this.state;
return (
<Draggable x={x} y={y} onMove={this._move}>
Drag me
</Draggable>
);
}
}
ReactDOM.render(
<Test />,
document.getElementById('container'),
);
Run Code Online (Sandbox Code Playgroud)
还有一点CSS
.draggable {
/* just to size it to content */
display: inline-block;
/* opaque background is important for performance */
background: white;
/* avoid selecting text while dragging */
user-select: none;
}
Run Code Online (Sandbox Code Playgroud)
Jos*_*son 10
反应可拖动也很容易使用.Github上:
https://github.com/mzabriskie/react-draggable
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import Draggable from 'react-draggable';
var App = React.createClass({
render() {
return (
<div>
<h1>Testing Draggable Windows!</h1>
<Draggable handle="strong">
<div className="box no-cursor">
<strong className="cursor">Drag Here</strong>
<div>You must click my handle to drag me</div>
</div>
</Draggable>
</div>
);
}
});
ReactDOM.render(
<App />, document.getElementById('content')
);
Run Code Online (Sandbox Code Playgroud)
我的index.html:
<html>
<head>
<title>Testing Draggable Windows</title>
<link rel="stylesheet" type="text/css" href="style.css" />
</head>
<body>
<div id="content"></div>
<script type="text/javascript" src="bundle.js" charset="utf-8"></script>
<script src="http://localhost:8080/webpack-dev-server.js"></script>
</body>
</html>
Run Code Online (Sandbox Code Playgroud)
你需要他们的风格,这很短,或者你没有达到预期的行为.我比其他一些可能的选择更喜欢这种行为,但也有一种叫做反应可重复和可移动的东西.我正试图调整大小与可拖动工作,但到目前为止没有喜悦.
我已经将polovnikov.ph解决方案更新到React 16/ES6,其增强功能包括触摸处理和捕捉到网格,这是我需要的游戏.捕捉到网格可以缓解性能问题.
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
class Draggable extends React.Component {
constructor(props) {
super(props);
this.state = {
relX: 0,
relY: 0,
x: props.x,
y: props.y
};
this.gridX = props.gridX || 1;
this.gridY = props.gridY || 1;
this.onMouseDown = this.onMouseDown.bind(this);
this.onMouseMove = this.onMouseMove.bind(this);
this.onMouseUp = this.onMouseUp.bind(this);
this.onTouchStart = this.onTouchStart.bind(this);
this.onTouchMove = this.onTouchMove.bind(this);
this.onTouchEnd = this.onTouchEnd.bind(this);
}
static propTypes = {
onMove: PropTypes.func,
onStop: PropTypes.func,
x: PropTypes.number.isRequired,
y: PropTypes.number.isRequired,
gridX: PropTypes.number,
gridY: PropTypes.number
};
onStart(e) {
const ref = ReactDOM.findDOMNode(this.handle);
const body = document.body;
const box = ref.getBoundingClientRect();
this.setState({
relX: e.pageX - (box.left + body.scrollLeft - body.clientLeft),
relY: e.pageY - (box.top + body.scrollTop - body.clientTop)
});
}
onMove(e) {
const x = Math.trunc((e.pageX - this.state.relX) / this.gridX) * this.gridX;
const y = Math.trunc((e.pageY - this.state.relY) / this.gridY) * this.gridY;
if (x !== this.state.x || y !== this.state.y) {
this.setState({
x,
y
});
this.props.onMove && this.props.onMove(this.state.x, this.state.y);
}
}
onMouseDown(e) {
if (e.button !== 0) return;
this.onStart(e);
document.addEventListener('mousemove', this.onMouseMove);
document.addEventListener('mouseup', this.onMouseUp);
e.preventDefault();
}
onMouseUp(e) {
document.removeEventListener('mousemove', this.onMouseMove);
document.removeEventListener('mouseup', this.onMouseUp);
this.props.onStop && this.props.onStop(this.state.x, this.state.y);
e.preventDefault();
}
onMouseMove(e) {
this.onMove(e);
e.preventDefault();
}
onTouchStart(e) {
this.onStart(e.touches[0]);
document.addEventListener('touchmove', this.onTouchMove, {passive: false});
document.addEventListener('touchend', this.onTouchEnd, {passive: false});
e.preventDefault();
}
onTouchMove(e) {
this.onMove(e.touches[0]);
e.preventDefault();
}
onTouchEnd(e) {
document.removeEventListener('touchmove', this.onTouchMove);
document.removeEventListener('touchend', this.onTouchEnd);
this.props.onStop && this.props.onStop(this.state.x, this.state.y);
e.preventDefault();
}
render() {
return <div
onMouseDown={this.onMouseDown}
onTouchStart={this.onTouchStart}
style={{
position: 'absolute',
left: this.state.x,
top: this.state.y,
touchAction: 'none'
}}
ref={(div) => { this.handle = div; }}
>
{this.props.children}
</div>;
}
}
export default Draggable;
Run Code Online (Sandbox Code Playgroud)
这是带有 Hook 的 2020 年答案:
function useDragging() {
const [isDragging, setIsDragging] = useState(false);
const [pos, setPos] = useState({ x: 0, y: 0 });
const ref = useRef(null);
function onMouseMove(e) {
if (!isDragging) return;
setPos({
x: e.x - ref.current.offsetWidth / 2,
y: e.y - ref.current.offsetHeight / 2,
});
e.stopPropagation();
e.preventDefault();
}
function onMouseUp(e) {
setIsDragging(false);
e.stopPropagation();
e.preventDefault();
}
function onMouseDown(e) {
if (e.button !== 0) return;
setIsDragging(true);
setPos({
x: e.x - ref.current.offsetWidth / 2,
y: e.y - ref.current.offsetHeight / 2,
});
e.stopPropagation();
e.preventDefault();
}
// When the element mounts, attach an mousedown listener
useEffect(() => {
ref.current.addEventListener("mousedown", onMouseDown);
return () => {
ref.current.removeEventListener("mousedown", onMouseDown);
};
}, [ref.current]);
// Everytime the isDragging state changes, assign or remove
// the corresponding mousemove and mouseup handlers
useEffect(() => {
if (isDragging) {
document.addEventListener("mouseup", onMouseUp);
document.addEventListener("mousemove", onMouseMove);
} else {
document.removeEventListener("mouseup", onMouseUp);
document.removeEventListener("mousemove", onMouseMove);
}
return () => {
document.removeEventListener("mouseup", onMouseUp);
document.removeEventListener("mousemove", onMouseMove);
};
}, [isDragging]);
return [ref, pos.x, pos.y, isDragging];
}
Run Code Online (Sandbox Code Playgroud)
然后是一个使用钩子的组件:
function Draggable() {
const [ref, x, y, isDragging] = useDragging();
return (
<div
ref={ref}
style={{
position: "absolute",
width: 50,
height: 50,
background: isDragging ? "blue" : "gray",
left: x,
top: y,
}}
></div>
);
}
Run Code Online (Sandbox Code Playgroud)
这是另一个简单的 React hooks 解决方案,没有任何第三方库,基于 codewithfeeling 和 Evan Conrad 的解决方案。 /sf/answers/4472124051/ /sf/answers/4316726641/
import React, { useCallback, useRef, useState } from "react";
import styled, { css } from "styled-components/macro";
const Component: React.FC = () => {
const [position, setPosition] = useState({ x: 0, y: 0 });
const elementRef = useRef<HTMLDivElement>(null);
const onMouseDown = useCallback(
(event) => {
const onMouseMove = (event: MouseEvent) => {
position.x += event.movementX;
position.y += event.movementY;
const element = elementRef.current;
if (element) {
element.style.transform = `translate(${position.x}px, ${position.y}px)`;
}
setPosition(position);
};
const onMouseUp = () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
};
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
},
[position, setPosition, elementRef]
);
return (
<Container>
<DraggableItem ref={elementRef} onMouseDown={onMouseDown}>
</DraggableItem>
</Container>
);
};
const Container = styled.div`
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
overflow: hidden;
`;
const DraggableItem = styled.div`
position: absolute;
z-index: 1;
left: 20px;
top: 20px;
width: 100px;
height: 100px;
background-color: green;
`;
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
86364 次 |
| 最近记录: |