Reactコンポーネント/ divをドラッグ可能にする推奨される方法


97

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

また、未加工のJavaScriptでこれを行ったことは一度もないので、特にReactに関連するすべてのコーナーケースを確実に処理できるように、エキスパートがそれを行う方法を確認したいと思います。

ありがとう。


実際、私はコードと同じくらい散文の説明、あるいは単に「あなたはそれをうまくやっている」と満足しています。しかし、これがこれまでの私の仕事のJSFiddleです。jsfiddle.net/ Z2JtM
Andrew Fleenor

私は、これは現在を見にコードを反応させるのは非常に少数の例があることを考えると、有効な問題であることに同意
ジャレドフォーサイス

1
私のユースケースのためのシンプルなHTML5ソリューションを見つけました-youtu.be/z2nHLfiiKBA。誰かを助けるかもしれない!!
プレミア

これを試して。これは、要素をラップしてドラッグ可能なnpmjs.com/package/just-dragに
Shan

回答:


111

私はおそらくこれをブログ投稿に変えるべきですが、これはかなり確かな例です。

コメントは物事をかなりよく説明するはずですが、質問がある場合はお知らせください。

そして、ここで遊ぶフィドルです: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

親はドラッグ可能なオブジェクトの現在の位置を所有する必要があります。この場合、ドラッグ可能オブジェクトは引き続き「ドラッグ中」状態を保持しますがthis.props.onChange(x, y)、mousemoveイベントが発生するたびに呼び出されます。

シナリオ2

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

ミックスインかコンポーネントか?

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


4
素晴らしい例です。Mixin「Draggable」は実際にはオブジェクトではなく、オブジェクトの能力であるため、これは完全なクラスとしてよりも適切と思われます。
ロス・アレン

2
私は少し遊んでみました。親の外にドラッグしても何も起こらないようですが、別のリアクションコンポーネントにドラッグすると、あらゆる種類の奇妙なことが起こります
Gorkem Yurtseven

11
jqueryの依存関係を削除するには、 var computedStyle = window.getComputedStyle(this.getDOMNode()); pos = { top: parseInt(computedStyle.top), left: parseInt(computedStyle.left) }; 次のようにします。reactでjqueryを使用している場合は、おそらく何か間違っているでしょう;)いくつかのjqueryプラグインが必要な場合は、通常、より簡単で、純粋なreactでコードを書き直すコードが少ないことがわかります。
Matt Crinklaw-Vogt 2014

7
@ MattCrinklaw-Vogtによる上記のコメントをフォローアップして、より防弾this.getDOMNode().getBoundingClientRect()対策のソリューションを使用することを伝えたかっただけです。getComputedStyleはauto、上記のコードを含む有効なCSSプロパティを出力できますNaN。:MDNの記事を参照してくださいdeveloper.mozilla.org/en-US/docs/Web/API/Element/...
Andru

2
そしてreをフォローアップするためにthis.getDOMNode()、それは廃止されました。refを使用してdomノードを取得します。 facebook.github.io/react/docs/...
クリスSattinger

63

完全なDOMコントロールを備えたReact用の柔軟なHTML5ドラッグアンドドロップミックスインであるreact-dndを実装しました。

既存のドラッグアンドドロップライブラリは私のユースケースに適合しなかったので、自分で作成しました。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ます。たとえば、アプリには複数の画像表現があり、それぞれにdragSourcefor が提供されItemTypes.IMAGEます。

A dragSourceは、ドラッグソースの動作を指定する単なるオブジェクトです。beginDragドラッグしているデータを表すアイテムを返すように実装し、必要に応じて、ドラッグしているUIを調整するいくつかのオプションを実装する必要があります。必要に応じcanDragて、ドラッグを禁止endDrag(didDrop)するか、ドロップが発生した(または発生していない)ときにロジックを実行するように実装できます。また、コンポーネントdragSource用に共有ミックスインを生成させることで、コンポーネント間でこのロジックを共有できます。

最後に、ドラッグハンドラをアタッチするには{...this.dragSourceFor(itemType)}、の一部(1つ以上)の要素を使用する必要がありrenderます。これは、1つの要素に複数の「ドラッグハンドル」を含めることができ、それらが異なる項目タイプに対応している場合があることを意味します。(JSXスプレッド属性の構文に慣れていない場合は、チェックしてください)。

単純なドロップターゲット

sのImageBlockドロップターゲットになりたいとしましょうIMAGE。それは我々が与える必要があることを除いて、ほとんど同じだ実装を: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クラスまたは属性を切り替えるために使用します。
  • ドラッグプレースホルダーとして画像を使用dragPreviewするようImageに指定します(画像ImagePreloaderMixinをロードするために使用)。
  • たとえば、再ImageBlocks注文可能にしたいとします。私たちはそれらを実装する必要があるdropTargetdragSourceのためにItemTypes.BLOCK
  • 他の種類のブロックを追加するとします。並べ替えロジックをミックスインに配置して再利用できます。
  • dropTargetFor(...types) 一度に複数のタイプを指定できるため、1つのドロップゾーンで多くの異なるタイプをキャッチできます。
  • より細かい制御が必要な場合、ほとんどのメソッドには最後のパラメーターとしてそれらを引き起こしたドラッグイベントが渡されます。

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


5
ドラッグアンドドロップとマウスドラッグには、マウスを使用する以外に何が共通するのですか?あなたの答えは質問とはまったく関係がなく、明らかに図書館の広告です。
polkovnikov.ph

5
質問に関連していると29人が判断したようです。React DnDを使用すると、マウスのドラッグも適切に実装できます。私は自分の作品を無料で共有し、次回は広大なドキュメンテーションに取り組むよりもいいと思うので、下手なコメントを読むのに時間を費やす必要はありません。
Dan Abramov

7
はい、私はそれを完全に知っています。他の場所にドキュメントがあるという事実は、これが特定の質問に対する回答であることを意味しません。同じ結果を得るために「use Google」と書くこともできます。29票は、回答の質ではなく、既知の人物による長い投稿によるものです。
polkovnikov.ph 2016

react-dndで自由にドラッグできるものの公式の例へのリンクを更新しました:basicadvanced
uryga

23

Jared Forsythの答えはひどく間違っていて時代遅れです。それは、次のようなアンチパターンのセット全体次の使用stopPropagation小道具から初期化状態、のjQueryの使用、状態のネストされたオブジェクトを、いくつか奇数有するdragging状態フィールド。書き換えた場合の解決策は次のようになりますが、マウスを動かすたびに仮想DOMの調整が強制されるため、パフォーマンスはそれほど高くありません。

UPD。私の答えはひどく間違っていて時代遅れでした。今回のコードでは、ネイティブイベントハンドラーとスタイルの更新を使用transformしてReactコンポーネントのライフサイクルが遅いという問題を軽減し、リフローにつながらないために使用し、による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'),
);

そして少しの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の例。


2
このおかげで、これは明らかに最もパフォーマンスの高いソリューションではありませんが、今日のアプリケーション構築のベストプラクティスに従っています。
Spets

1
@ryanjいいえ、デフォルト値は悪です。それが問題です。小道具が変更されたときの適切なアクションは何ですか?状態を新しいデフォルトにリセットする必要がありますか?新しいデフォルト値を古いデフォルト値と比較して、デフォルトが変更された場合にのみ状態をデフォルトにリセットする必要がありますか?ユーザーが定数値のみを使用するように制限する方法はありません。それがアンチパターンである理由です。デフォルト値は、高次コンポーネント(つまり、オブジェクトではなくクラス全体)を介して明示的に作成し、propsを介して設定しないでください。
polkovnikov.ph 2016年

1
私は敬意を払いません。たとえば、コンポーネントの状態は、コンポーネントのUIに固有のデータを格納するための優れた場所です。たとえば、アプリ全体とは無関係です。一部のインスタンスではデフォルト値を小道具として渡すことができない場合、このデータをマウント後に取得するためのオプションは制限され、多くの(ほとんど?)後日。私はそれをベストプラクティスまたはそのようなものとして提唱していません。imoを提案しているほど有害ではありません
ライアンj

2
とてもシンプルでエレガントなソリューションです。私の見解が似ていたことを嬉しく思います。質問が1つあります。パフォーマンスの低下について言及していますが、パフォーマンスを考慮して同様の機能を実現するために何を提案しますか?
ギヨームM

1
とにかく、私たちは今フックを持っています、そして私はすぐにもう一度答えを更新しなければなりません。
polkovnikov.ph

13

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;

こんにちは@anyhotcountryグリッドX係数を何に使用しますか?
AlexNikonov

1
@AlexNikonov x方向の(スナップ先)グリッドのサイズです。パフォーマンスを向上させるには、gridXおよびgridY> 1にすることをお勧めします。
anyhotcountry

これは私にとっては非常にうまくいきました。onStart()関数で行った変更:relXとrelYの計算e.clienX-this.props.xを使用しました。これにより、ページ全体をドラッグ領域にするのではなく、ドラッグ可能なコンポーネントを親コンテナ内に配置できました。コメントが遅いことはわかっていますが、感謝の意を表したいと思います。
ジェフ

11

反応ドラッグも使いやすいです。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と呼ばれるものもあります。私はドラッグ可能なものでリサイズを動かそうとしていますが、今のところ喜びはありません。


8

ここでこれにシンプルでモダンなアプローチだuseStateuseEffectuseRefES6では。

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

これは、ここでの最新の回答のようです。
codyThompson

2

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

移動位置は一切保存されません。マウスの動きと考えてください。カーソルは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操作は洗練されたものです(私がこれを言うとは思いもしませんでした)。


1
setXandY想像上の要素で関数を埋めることができますか?
Thellimist '12

0

ここで見たすべてのソリューションにはサポートされなくなったものや、まもなく廃止される予定があるため、参照を使用してクラスを更新しましたReactDOM.findDOMNode。子コンポーネントまたは子グループの親になることができます:)

import React, { Component } from 'react';

class Draggable extends Component {

    constructor(props) {
        super(props);
        this.myRef = React.createRef();
        this.state = {
            counter: this.props.counter,
            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(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 = (e) => {
        if (e.button !== 0) return;
        let pos = { left: this.myRef.current.offsetLeft, top: this.myRef.current.offsetTop }
        this.setState({
            dragging: true,
            rel: {
                x: e.pageX - pos.left,
                y: e.pageY - pos.top
            }
        });
        e.stopPropagation();
        e.preventDefault();
    }

    onMouseUp = (e) => {
        this.setState({ dragging: false });
        e.stopPropagation();
        e.preventDefault();
    }

    onMouseMove = (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() {
        return (
            <span ref={this.myRef} onMouseDown={this.onMouseDown} style={{ position: 'absolute', left: this.state.pos.x + 'px', top: this.state.pos.y + 'px' }}>
                {this.props.children}
            </span>
        )
    }
}

export default Draggable;
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.