Javascript 使React组件/分区可拖动的推荐方法
我想制作一个可拖动(即,可通过鼠标重新定位)的React组件,它似乎必然涉及全局状态和分散的事件处理程序。我可以用肮脏的方式,在我的JS文件中使用一个全局变量,甚至可以将它包装在一个漂亮的闭包界面中,但我想知道是否有一种方法可以更好地与网格交互 此外,由于我以前从未在原始JavaScript中这样做过,我想看看专家是如何做到这一点的,以确保我已经处理了所有的关键案例,尤其是与React相关的案例Javascript 使React组件/分区可拖动的推荐方法,javascript,reactjs,Javascript,Reactjs,我想制作一个可拖动(即,可通过鼠标重新定位)的React组件,它似乎必然涉及全局状态和分散的事件处理程序。我可以用肮脏的方式,在我的JS文件中使用一个全局变量,甚至可以将它包装在一个漂亮的闭包界面中,但我想知道是否有一种方法可以更好地与网格交互 此外,由于我以前从未在原始JavaScript中这样做过,我想看看专家是如何做到这一点的,以确保我已经处理了所有的关键案例,尤其是与React相关的案例 谢谢。我可能应该把这篇文章变成一篇博客文章,但这里有一个很好的例子 这些评论应该能很好地解释事情,但
谢谢。我可能应该把这篇文章变成一篇博客文章,但这里有一个很好的例子 这些评论应该能很好地解释事情,但是如果你有问题,请告诉我 这是一把小提琴: 关于国家所有制等问题的思考。 “谁应该拥有什么样的国家”从一开始就是一个需要回答的重要问题。在“可拖动”组件的情况下,我可以看到一些不同的场景 情景1 父对象应拥有可拖动对象的当前位置。在这种情况下,draggable仍将拥有“我正在拖动”状态,但每当mousemove事件发生时,它将调用
this.props.onChange(x,y)
情景2
父对象只需要拥有“非移动位置”,因此可拖动对象将拥有其“拖动位置”,但在MouseUp上,它将调用this.props.onChange(x,y)
,并将最终决定推迟给父对象。如果父级不喜欢拖动表的结束位置,它将不更新其状态,并且拖动表将在拖动前“快速恢复”到其初始位置
混合物还是成分?
@ssorallen指出,由于“draggable”与其说是事物本身,不如说是一个属性,因此它可能更适合作为一个混音。我对mixin的经验是有限的,所以我还没有看到它们在复杂的情况下如何帮助或阻碍。这可能是最好的选择。我想添加一个第三种方案 移动位置不会以任何方式保存。可以将其视为鼠标移动-您的光标不是React组件,对吗 您所要做的就是向组件添加一个类似于“draggable”的道具,以及一个将操纵dom的拖动事件流
setXandY: function(event) {
// DOM Manipulation of x and y on your node
},
componentDidMount: function() {
if(this.props.draggable) {
var node = this.getDOMNode();
dragStream(node).onValue(this.setXandY); //baconjs stream
};
},
在这种情况下,DOM操作是一件优雅的事情(我从来没有想过我会这么说)我实现了,一种灵活的HTML5拖放混合,用于使用完整的DOM控件进行反应
现有的拖放库不适合我的用例,所以我写了自己的。它类似于我们在Stampsy.com上运行了大约一年的代码,但被重写以利用React和Flux
我的主要要求是:
- 发出自己的零DOM或CSS,将其留给消费组件李>
- 对消费组件施加尽可能少的结构李>
- 使用HTML5拖放作为主要后端,但可以在将来添加不同的后端李>
- 像最初的HTML5API一样,强调拖动数据,而不仅仅是“可拖动的视图”李>
- 从消费代码中隐藏HTML5API的怪癖李>
- 对于不同类型的数据,不同的组件可能是“拖放源”或“拖放目标”李>
- 允许一个组件包含多个拖放源,并在需要时拖放目标李>
- 使拖放目标在拖动或悬停兼容数据时易于更改其外观李>
- 可以轻松地使用图像来拖动缩略图,而不是元素屏幕截图,从而避免浏览器的怪癖
// ItemTypes.js
module.exports = {
BLOCK: 'block',
IMAGE: '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)} />
);
}
);
在一个组件中拖动源+拖放目标
假设我们现在希望用户能够从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>
);
}
);
var{DragDropMixin}=require('react-dnd'),
ItemTypes=require('./ItemTypes');
var ImageBlock=React.createClass({
混合物:[DragDropMixin],
配置DragDrop(注册表类型){
registerType(ItemTypes.IMAGE{
//添加仅在ImageBlock具有图像时可用的拖动源:
dragSource:{
坎德拉格(){
return!!this.props.image;
},
开始{
返回{
项目:this.props.image
};
}
}
dropTarget:{
acceptDrop(图像){
DocumentActionCreators.setImage(this.props.blockId,image);
}
}
});
},
render(){
返回(
{/*将{…this.dragSourceFor}处理程序添加到嵌套节点*/}
{this.props.image&&
}
);
}
);
还有什么可能?
我并没有涵盖所有内容,但可以通过以下几种方式使用此API:
- 使用
和getDragState(type)
了解拖动是否处于活动状态,并使用它切换CSS类或属性getDropState(type)
- 指定
为dragPreview
以使用图像作为拖动占位符(使用Image
加载图像)imagepredermixin
- 比如说,我们想使
可重新排序。我们只需要它们为ImageBlocks
实现项目类型.BLOCK
和dropTarget
dragSource
- 假设我们添加其他类型的块,我们可以通过将其放置在mixin中重用它们的重新排序逻辑
允许一次指定多个类型,因此一个放置区域可以捕获多个不同的类型dropTargetFor(…类型)
- 当您需要更细粒度的控制时
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> ); } );
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> ); } );
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') );
<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>
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'), );
.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; }
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;
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> ); }