web-dev-qa-db-ja.com

React component / divをドラッグ可能にする推奨方法

ドラッグ可能(つまり、マウスで再配置可能)Reactコンポーネントを作成します。これには、グローバル状態と散在するイベントハンドラーが必ず含まれているようです。私はJSファイルにグローバル変数を使用してダーティな方法でそれを行うことができ、おそらくニースのクロージャーインターフェイスでラップすることさえできますが、Reactとより良い方法があるかどうか知りたいです。

また、未加工のJavaScriptでこれを行ったことがないので、特にReactに関連するすべてのコーナーケースが処理されるように、専門家がどのように行うかを確認したいと思います。

ありがとう。

79
Andrew Fleenor

おそらくこれをブログ投稿に変える必要がありますが、これはかなり堅実な例です。

コメントは物事をかなりよく説明するはずですが、質問があれば教えてください。

そして、ここで遊ぶフィドルがあります: 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))
  }
})

国家の所有権などに関する考え.

「誰がどの州を所有すべきか」は、最初から答えるべき重要な質問です。 「ドラッグ可能な」コンポーネントの場合、いくつかの異なるシナリオを見ることができました。

シナリオ1

親は、ドラッグ可能の現在の位置を所有する必要があります。この場合、draggableはまだ「am I dragging」状態を所有しますが、mousemoveイベントが発生するたびにthis.props.onChange(x, y)を呼び出します。

シナリオ2

親は「移動しない位置」を所有するだけでよいので、ドラッグ可能なオブジェクトは「ドラッグ位置」を所有しますが、onmouseupはthis.props.onChange(x, y)を呼び出し、最終決定を親に委ねます。親がドラッグ可能オブジェクトの終了位置が気に入らない場合、その状態は更新されず、ドラッグ可能オブジェクトはドラッグする前の初期位置に「スナップバック」します。

ミックスインまたはコンポーネント?

@ssorallenは、「ドラッグ可能」はそれ自体が物よりも属性であるため、ミックスインとしてより良い役割を果たす可能性があると指摘しました。ミックスインの私の経験は限られているため、複雑な状況でどのように役立つか、または邪魔になるのか見ていません。これが最良のオプションです。

94
Jared Forsyth

実装したreact-dnd、完全なReactの柔軟なHTML5ドラッグアンドドロップミックスインDOMコントロール。

既存のドラッグアンドドロップライブラリは私のユースケースに適合しなかったので、自分で作成しました。 Stampsy.comで約1年間実行しているコードに似ていますが、ReactとFluxを利用するように書き直されています。

私が持っていた主な要件:

  • 独自のゼロDOMまたはCSSを生成し、それを消費コンポーネントに任せます。
  • 消費コンポーネントに可能な限り小さな構造を課します。
  • HTML5のドラッグアンドドロップをプライマリバックエンドとして使用しますが、将来別のバックエンドを追加できるようにします。
  • 元のHTML5 APIと同様に、「ドラッグ可能なビュー」だけでなく、データのドラッグを強調します。
  • 消費コードからHTML5 APIの癖を隠します。
  • 異なるコンポーネントは、異なる種類のデータの「ドラッグソース」または「ドロップターゲット」です。
  • 必要に応じて、1つのコンポーネントに複数のドラッグソースとドロップターゲットを含めることができます。
  • 互換性のあるデータがドラッグまたはホバーされている場合、ドロップターゲットの外観を簡単に変更できます。
  • 要素のスクリーンショットの代わりにドラッグサムネイルに画像を簡単に使用できるようにして、ブラウザの癖を回避します。

これらがおなじみの場合は、読み進めてください。

使用法

シンプルなドラッグソース

最初に、ドラッグできるデータのタイプを宣言します。

これらは、ドラッグソースとドロップターゲットの「互換性」を確認するために使用されます。

// 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 構文に慣れていない場合は、チェックしてください)。

シンプルドロップターゲット

ImageBlockIMAGEsのドロップターゲットにしたいとしましょう。 registerTypedropTarget実装を与える必要があることを除いて、ほとんど同じです。

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>
    );
  }
);

1つのコンポーネントにソースをドラッグしてターゲットをドロップ

ユーザーが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クラスまたは属性を切り替えます。
  • dragPreviewImageに指定して、イメージをドラッグプレースホルダーとして使用します(ImagePreloaderMixinを使用してロードします)。
  • ImageBlocksを再注文可能にしたいとします。必要なのは、ItemTypes.BLOCKに対してdropTargetおよびdragSourceを実装することだけです。
  • 他の種類のブロックを追加するとします。 mixinに配置することで、並べ替えロジックを再利用できます。
  • dropTargetFor(...types)では、一度に複数のタイプを指定できるため、1つのドロップゾーンでさまざまなタイプをキャッチできます。
  • よりきめ細かい制御が必要な場合、ほとんどのメソッドには、最後のパラメーターとしてそれらを引き起こしたドラッグイベントが渡されます。

最新のドキュメントとインストール手順については、Githubのreact-dndリポジトリに進んでください。

61
Dan Abramov

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;
}

JSFiddleの例

17
polkovnikov.ph

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 と呼ばれるものもあります。私はサイズ変更をドラッグ可能にしようとしていますが、今のところ喜びはありません。

10
Joseph Larson

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;
9
anyhotcountry

番目のシナリオを追加したい

移動位置は保存されません。マウスの動きと考えてください-あなたのカーソルは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操作はエレガントなものです(私はこれを言うとは思わなかった)

2
Thomas Deutsch