ドラッグ可能(つまり、マウスで再配置可能)Reactコンポーネントを作成します。これには、グローバル状態と散在するイベントハンドラーが必ず含まれているようです。私はJSファイルにグローバル変数を使用してダーティな方法でそれを行うことができ、おそらくニースのクロージャーインターフェイスでラップすることさえできますが、Reactとより良い方法があるかどうか知りたいです。
また、未加工のJavaScriptでこれを行ったことがないので、特にReactに関連するすべてのコーナーケースが処理されるように、専門家がどのように行うかを確認したいと思います。
ありがとう。
おそらくこれをブログ投稿に変える必要がありますが、これはかなり堅実な例です。
コメントは物事をかなりよく説明するはずですが、質問があれば教えてください。
そして、ここで遊ぶフィドルがあります: 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))
}
})
「誰がどの州を所有すべきか」は、最初から答えるべき重要な質問です。 「ドラッグ可能な」コンポーネントの場合、いくつかの異なるシナリオを見ることができました。
親は、ドラッグ可能の現在の位置を所有する必要があります。この場合、draggableはまだ「am I dragging」状態を所有しますが、mousemoveイベントが発生するたびにthis.props.onChange(x, y)
を呼び出します。
親は「移動しない位置」を所有するだけでよいので、ドラッグ可能なオブジェクトは「ドラッグ位置」を所有しますが、onmouseupはthis.props.onChange(x, y)
を呼び出し、最終決定を親に委ねます。親がドラッグ可能オブジェクトの終了位置が気に入らない場合、その状態は更新されず、ドラッグ可能オブジェクトはドラッグする前の初期位置に「スナップバック」します。
@ssorallenは、「ドラッグ可能」はそれ自体が物よりも属性であるため、ミックスインとしてより良い役割を果たす可能性があると指摘しました。ミックスインの私の経験は限られているため、複雑な状況でどのように役立つか、または邪魔になるのか見ていません。これが最良のオプションです。
実装したreact-dnd、完全なReactの柔軟なHTML5ドラッグアンドドロップミックスインDOMコントロール。
既存のドラッグアンドドロップライブラリは私のユースケースに適合しなかったので、自分で作成しました。 Stampsy.comで約1年間実行しているコードに似ていますが、ReactとFluxを利用するように書き直されています。
私が持っていた主な要件:
これらがおなじみの場合は、読み進めてください。
最初に、ドラッグできるデータのタイプを宣言します。
これらは、ドラッグソースとドロップターゲットの「互換性」を確認するために使用されます。
// ItemTypes.js
module.exports = {
BLOCK: 'block',
IMAGE: '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)} />
);
}
);
configureDragDrop
を指定することにより、DragDropMixin
にこのコンポーネントのドラッグドロップ動作を伝えます。ドラッグ可能なコンポーネントとドロップ可能なコンポーネントの両方が同じミックスインを使用します。
configureDragDrop
内で、コンポーネントがサポートするカスタムregisterType
のそれぞれに対してItemTypes
を呼び出す必要があります。たとえば、アプリには複数の画像表現があり、それぞれがItemTypes.IMAGE
に対してdragSource
を提供します。
dragSource
は、ドラッグソースの動作を指定するオブジェクトです。 beginDrag
を実装して、ドラッグしているデータを表すアイテムと、オプションでドラッグUIを調整するいくつかのオプションを返す必要があります。オプションで、canDrag
を実装してドラッグを禁止したり、endDrag(didDrop)
を使用してドロップが発生した(または発生しなかった)ときにロジックを実行したりできます。また、共有mixinにdragSource
を生成させることにより、コンポーネント間でこのロジックを共有できます。
最後に、render
のいくつかの(1つ以上の)要素で{...this.dragSourceFor(itemType)}
を使用して、ドラッグハンドラーをアタッチする必要があります。つまり、1つの要素に複数の「ドラッグハンドル」を含めることができ、それらは異なるアイテムタイプに対応することさえあります。 ( JSX Spread Attributes 構文に慣れていない場合は、チェックしてください)。
ImageBlock
をIMAGE
sのドロップターゲットにしたいとしましょう。 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>
);
}
);
ユーザーが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>
);
}
);
すべてをカバーしたわけではありませんが、このAPIをさらにいくつかの方法で使用することは可能です。
getDragState(type)
およびgetDropState(type)
を使用して、ドラッグがアクティブかどうかを確認し、それを使用してCSSクラスまたは属性を切り替えます。dragPreview
をImage
に指定して、イメージをドラッグプレースホルダーとして使用します(ImagePreloaderMixin
を使用してロードします)。ImageBlocks
を再注文可能にしたいとします。必要なのは、ItemTypes.BLOCK
に対してdropTarget
およびdragSource
を実装することだけです。dropTargetFor(...types)
では、一度に複数のタイプを指定できるため、1つのドロップゾーンでさまざまなタイプをキャッチできます。最新のドキュメントとインストール手順については、Githubのreact-dndリポジトリに進んでください。
Jared Forsythの答えは恐ろしく間違っていて時代遅れです。 stopPropagation
の使用法 、 propsから状態を初期化する 、jQueryの使用法、状態のネストされたオブジェクトなどのアンチパターンのセット全体に続き、いくつかの奇妙なdragging
状態フィールド。書き換えられた場合、ソリューションは次のようになりますが、それでもすべてのマウス移動ティックで仮想DOM調整が強制され、あまりパフォーマンスが高くありません。
UPD。私の答えは恐ろしく間違っていて時代遅れでした。現在、コードは、ネイティブイベントハンドラーとスタイルの更新を使用することにより、Reactコンポーネントのライフサイクルの問題を軽減し、リフローを引き起こさないtransform
を使用し、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'),
);
そして少しの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;
}
react-draggableも使いやすいです。 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')
);
そして、私の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>
あなたは彼らのスタイルを必要としますが、それは短いか、期待したような振る舞いが得られません。他の可能な選択肢のいくつかよりも動作が好きですが、 react-resizable-and-movable と呼ばれるものもあります。私はサイズ変更をドラッグ可能にしようとしていますが、今のところ喜びはありません。
Polkovnikov.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;
番目のシナリオを追加したい
移動位置は保存されません。マウスの動きと考えてください-あなたのカーソルはReactコンポーネントではありませんか?
あなたがすることは、「ドラッグ可能」のような小道具をコンポーネントに追加することと、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操作はエレガントなものです(私はこれを言うとは思わなかった)