Empfohlene Methode, um React Component / Div ziehbar zu machen

93

Ich möchte eine ziehbare (dh mit der Maus repositionierbare) React-Komponente erstellen, an der anscheinend globale Status- und Streuereignishandler beteiligt sind. Ich kann es auf schmutzige Weise tun, mit einer globalen Variablen in meiner JS-Datei, und könnte es wahrscheinlich sogar in eine schöne Abschlussoberfläche einbinden, aber ich möchte wissen, ob es einen Weg gibt, der besser mit React zusammenpasst.

Da ich dies noch nie zuvor in rohem JavaScript getan habe, würde ich gerne sehen, wie ein Experte dies tut, um sicherzustellen, dass alle Eckfälle behandelt werden, insbesondere in Bezug auf React.

Vielen Dank.

Andrew Fleenor
quelle
Eigentlich wäre ich mit einer Prosaerklärung mindestens genauso zufrieden wie mit Code oder einfach nur "Du machst es gut". Aber hier ist eine JSFiddle meiner bisherigen Arbeit: jsfiddle.net/Z2JtM
Andrew Fleenor
Ich bin damit einverstanden, dass dies eine gültige Frage ist, da es derzeit nur sehr wenige Beispiele für Reaktionscode gibt
Jared Forsyth
1
Ich habe eine einfache HTML5-Lösung für meinen Anwendungsfall gefunden - youtu.be/z2nHLfiiKBA . Könnte jemandem helfen !!
Prem
Versuche dies. Es ist ein einfaches HOC, um verpackte Elemente ziehbar
Shan

Antworten:

110

Ich sollte dies wahrscheinlich in einen Blog-Beitrag verwandeln, aber hier ist ein ziemlich solides Beispiel.

Die Kommentare sollten die Dinge ziemlich gut erklären, aber lassen Sie mich wissen, wenn Sie Fragen haben.

Und hier ist die Geige zum Spielen: 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))
  }
})

Gedanken zum Staatseigentum usw.

"Wem sollte welcher Staat gehören" ist von Anfang an eine wichtige Frage. Im Fall einer "ziehbaren" Komponente konnte ich einige verschiedene Szenarien sehen.

Szenario 1

Der Elternteil sollte die aktuelle Position des Draggables besitzen. In diesem Fall würde der Draggable weiterhin den Status " this.props.onChange(x, y)Ziehe ich" besitzen, würde jedoch immer dann aufrufen, wenn ein Mausbewegungsereignis auftritt.

Szenario 2

Der Elternteil muss nur die "nicht bewegte Position" besitzen, und so würde der Draggable seine "Ziehposition" besitzen, aber beim Hochfahren würde er this.props.onChange(x, y)die endgültige Entscheidung aufrufen und dem Elternteil aufschieben. Wenn dem übergeordneten Element nicht gefällt, wo das Draggable gelandet ist, wird sein Status einfach nicht aktualisiert, und das Draggable wird vor dem Ziehen an seiner ursprünglichen Position "zurückschnappen".

Mixin oder Komponente?

@ssorallen wies darauf hin, dass "draggable" eher ein Attribut als eine Sache an sich ist und daher besser als Mixin dienen könnte. Meine Erfahrung mit Mixins ist begrenzt, daher habe ich nicht gesehen, wie sie in komplizierten Situationen helfen oder im Weg stehen könnten. Dies könnte die beste Option sein.

Jared Forsyth
quelle
4
Tolles Beispiel. Dies scheint Mixinals eine vollständige Klasse angemessener zu sein, da "Draggable" eigentlich kein Objekt ist, sondern eine Fähigkeit eines Objekts.
Ross Allen
2
Ich habe ein bisschen damit herumgespielt, es scheint, als würde das Ziehen außerhalb des Elternteils nichts bewirken, aber alle möglichen seltsamen Dinge passieren, wenn es in eine andere Reaktionskomponente gezogen wird
Gorkem Yurtseven,
11
Sie können die JQuery-Abhängigkeit entfernen, indem Sie Folgendes tun: var computedStyle = window.getComputedStyle(this.getDOMNode()); pos = { top: parseInt(computedStyle.top), left: parseInt(computedStyle.left) }; Wenn Sie JQuery mit React verwenden, machen Sie wahrscheinlich etwas falsch;) Wenn Sie ein JQuery-Plugin benötigen, ist es normalerweise einfacher und weniger Code, es in Pure Reag neu zu schreiben.
Matt Crinklaw-Vogt
7
Ich wollte nur den obigen Kommentar von @ MattCrinklaw-Vogt weiterverfolgen, um zu sagen, dass eine kugelsichere Lösung verwendet werden soll this.getDOMNode().getBoundingClientRect()- getComputedStyle kann jede gültige CSS-Eigenschaft ausgeben, einschließlich, autoin welchem ​​Fall der obige Code zu einem führt NaN. Siehe den MDN-Artikel: developer.mozilla.org/en-US/docs/Web/API/Element/…
Andru
2
Und um nachzufolgen this.getDOMNode(), ist das veraltet. Verwenden Sie eine Referenz, um den Dom-Knoten abzurufen. facebook.github.io/react/docs/…
Chris Sattinger
63

Ich habe react-dnd implementiert , ein flexibles HTML5-Drag-and-Drop-Mixin für React mit vollständiger DOM-Steuerung.

Bestehende Drag-and-Drop-Bibliotheken passten nicht zu meinem Anwendungsfall, daher schrieb ich meine eigenen. Es ähnelt dem Code, den wir seit ungefähr einem Jahr auf Stampsy.com ausführen, wurde jedoch neu geschrieben, um die Vorteile von React und Flux zu nutzen.

Hauptanforderungen, die ich hatte:

  • Geben Sie kein eigenes DOM oder CSS aus und überlassen Sie es den verbrauchenden Komponenten.
  • Den verbrauchenden Komponenten so wenig Struktur wie möglich auferlegen;
  • Verwenden Sie HTML5-Drag & Drop als primäres Backend, aber ermöglichen Sie es, in Zukunft verschiedene Backends hinzuzufügen.
  • Betonen Sie wie bei der ursprünglichen HTML5-API das Ziehen von Daten und nicht nur das Ziehen von Ansichten.
  • Verstecken Sie HTML5-API-Macken vor dem konsumierenden Code.
  • Verschiedene Komponenten können "Drag-Quellen" oder "Drop-Ziele" für verschiedene Arten von Daten sein.
  • Erlauben Sie einer Komponente, bei Bedarf mehrere Drag-Quellen und Drop-Ziele zu enthalten.
  • Machen Sie es Drop-Zielen einfach, ihr Erscheinungsbild zu ändern, wenn kompatible Daten gezogen oder mit dem Mauszeiger darüber bewegt werden.
  • Machen Sie es sich einfach, Bilder zum Ziehen von Miniaturansichten anstelle von Element-Screenshots zu verwenden, um Browser-Macken zu umgehen.

Wenn Ihnen diese bekannt vorkommen, lesen Sie weiter.

Verwendung

Einfache Drag Source

Deklarieren Sie zunächst Datentypen, die gezogen werden können.

Diese werden verwendet, um die „Kompatibilität“ von Drag-Quellen und Drop-Zielen zu überprüfen:

// ItemTypes.js
module.exports = {
  BLOCK: 'block',
  IMAGE: 'image'
};

(Wenn Sie nicht über mehrere Datentypen verfügen, ist diese Bibliothek möglicherweise nicht für Sie geeignet.)

Dann erstellen wir eine sehr einfache ziehbare Komponente, die beim Ziehen Folgendes darstellt 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)} />
    );
  }
);

Durch Angabe erklären configureDragDropwir DragDropMixindas Drag-Drop-Verhalten dieser Komponente. Sowohl ziehbare als auch ablegbare Komponenten verwenden das gleiche Mixin.

Im Inneren configureDragDropmüssen wir registerTypejede unserer benutzerdefinierten ItemTypesKomponenten aufrufen , die von der Komponente unterstützt werden. Beispielsweise enthält Ihre App möglicherweise mehrere Darstellungen von Bildern, von denen jede ein dragSourcefür bereitstellt ItemTypes.IMAGE.

A dragSourceist nur ein Objekt, das angibt, wie die Drag-Quelle funktioniert. Sie müssen implementieren beginDrag, um ein Element zurückzugeben, das die Daten darstellt, die Sie ziehen, und optional einige Optionen, die die Benutzeroberfläche zum Ziehen anpassen. Sie können optional implementieren, canDragum das Ziehen zu verbieten oder endDrag(didDrop)eine Logik auszuführen, wenn der Abwurf aufgetreten ist (oder nicht). Und Sie können diese Logik zwischen Komponenten teilen, indem Sie ein gemeinsames Mixin dragSourcefür sie generieren lassen.

Schließlich müssen Sie {...this.dragSourceFor(itemType)}einige (ein oder mehrere) Elemente verwenden render, um Drag-Handler anzuhängen. Dies bedeutet, dass Sie mehrere „Ziehpunkte“ in einem Element haben können und diese sogar verschiedenen Elementtypen entsprechen können. (Wenn Sie mit der Syntax von JSX Spread Attributes nicht vertraut sind, lesen Sie sie durch.)

Einfaches Drop-Ziel

Nehmen wir ImageBlockan , wir wollen ein Drop-Ziel für IMAGEs sein. Es ist ziemlich ähnlich, außer dass wir registerTypeeine dropTargetImplementierung geben müssen:

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

Quelle ziehen + Ziel in einer Komponente ablegen

Angenommen, wir möchten jetzt, dass der Benutzer ein Bild herausziehen kann ImageBlock. Wir müssen nur dragSourceein paar entsprechende Handler hinzufügen :

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

Was ist sonst noch möglich?

Ich habe nicht alles behandelt, aber es ist möglich, diese API auf einige weitere Arten zu verwenden:

  • Verwenden Sie getDragState(type)und getDropState(type), um zu erfahren, ob das Ziehen aktiv ist, und um CSS-Klassen oder -Attribute umzuschalten.
  • Geben Sie dragPreviewan Image, dass Bilder als Drag-Platzhalter verwendet werden sollen ( ImagePreloaderMixinzum Laden).
  • Sagen wir, wir wollen ImageBlocksnachbestellbar machen . Wir brauchen sie nur zu implementieren dropTargetund dragSourcefür ItemTypes.BLOCK.
  • Angenommen, wir fügen andere Arten von Blöcken hinzu. Wir können ihre Neuordnungslogik wiederverwenden, indem wir sie in ein Mixin einfügen.
  • dropTargetFor(...types) Ermöglicht die gleichzeitige Angabe mehrerer Typen, sodass eine Drop-Zone viele verschiedene Typen erfassen kann.
  • Wenn Sie eine feinkörnigere Steuerung benötigen, wird den meisten Methoden ein Drag-Ereignis übergeben, das sie als letzten Parameter verursacht hat.

Aktuelle Dokumentationen und Installationsanweisungen finden Sie unter Reagieren und Repo auf Github .

Dan Abramov
quelle
4
Was haben Drag & Drop und Ziehen mit der Maus außer der Verwendung einer Maus gemeinsam? Ihre Antwort bezieht sich überhaupt nicht auf eine Frage und ist eindeutig eine Bibliothekswerbung.
polkovnikov.ph
5
Es scheint, als hätten 29 Personen festgestellt, dass es mit der Frage zusammenhängt. Mit React DnD können Sie auch das Ziehen von Mäusen zu gut implementieren . Ich denke besser, als meine Arbeit kostenlos zu teilen und beim nächsten Mal an Beispielen und umfangreichen Dokumentationen zu arbeiten, damit ich keine Zeit damit verbringen muss, snarky Kommentare zu lesen.
Dan Abramov
6
Ja, das weiß ich genau. Die Tatsache, dass Sie an anderer Stelle Dokumentation haben, bedeutet nicht, dass dies eine Antwort auf die gegebene Frage ist. Sie hätten für das gleiche Ergebnis "Google verwenden" schreiben können. 29 Upvotes sind auf einen langen Beitrag einer bekannten Person zurückzuführen, nicht auf die Qualität der Antwort.
polkovnikov.ph
aktualisierte Links zu offiziellen Beispielen von frei ziehbaren Sachen mit react-dnd: basic , advanced
uryga vor
22

Die Antwort von Jared Forsyth ist schrecklich falsch und veraltet. Es folgt eine ganze Reihe von Antimustern wie die Verwendung vonstopPropagation , das Initialisieren des Status von Requisiten , die Verwendung von jQuery, verschachtelte Objekte im Status und ein ungerades draggingStatusfeld. Wenn es neu geschrieben wird, lautet die Lösung wie folgt, erzwingt jedoch weiterhin die virtuelle DOM-Abstimmung bei jedem Mausklick und ist nicht sehr performant.

UPD. Meine Antwort war schrecklich falsch und veraltet. Jetzt transformverringert der Code Probleme des langsamen Lebenszyklus von React-Komponenten durch die Verwendung nativer Ereignishandler und Stilaktualisierungen, verwendet, da dies nicht zu Rückflüssen führt, und drosselt DOM-Änderungen requestAnimationFrame. Jetzt sind es in jedem Browser, den ich ausprobiert habe, konstant 60 FPS für mich.

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'),
);

und ein bisschen 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;
}

Beispiel für JSFiddle.

polkovnikov.ph
quelle
2
Vielen Dank dafür, dies ist definitiv nicht die leistungsstärkste Lösung, aber sie folgt den Best Practices zum Erstellen von Anwendungen heute.
Spets
1
@ryanj Nein, Standardwerte sind böse, das ist das Problem. Was ist die richtige Aktion, wenn sich die Requisiten ändern? Sollten wir den Status auf den neuen Standard zurücksetzen? Sollten wir den neuen Standardwert mit einem alten Standardwert vergleichen, um den Status nur dann auf den Standardwert zurückzusetzen, wenn sich der Standardwert geändert hat? Es gibt keine Möglichkeit, den Benutzer darauf zu beschränken, nur einen konstanten Wert zu verwenden, und sonst nichts. Deshalb ist es ein Antimuster. Standardwerte sollten explizit über Komponenten höherer Ordnung (dh für die gesamte Klasse, nicht für ein Objekt) erstellt und niemals über Requisiten festgelegt werden.
polkovnikov.ph
1
Ich bin mit Respekt anderer Meinung - der Komponentenstatus ist ein ausgezeichneter Ort zum Speichern von Daten, die für die Benutzeroberfläche einer Komponente spezifisch sind und beispielsweise keine Relevanz für die gesamte App haben. Ohne in einigen Fällen möglicherweise Standardwerte als Requisiten übergeben zu können, sind die Optionen zum Abrufen dieser Daten nach dem Mounten begrenzt und unter vielen (den meisten?) Umständen weniger wünschenswert als die Unklarheiten um eine Komponente, an die möglicherweise eine andere someDefaultValue-Requisite bei a übergeben wird späteren Zeitpunkt. Ich befürworte es nicht als Best Practice oder ähnliches, es ist einfach nicht so schädlich, wie Sie imo
ryan j
2
Sehr einfache und elegante Lösung. Ich bin froh zu sehen, dass meine Einstellung dazu ähnlich war. Ich habe eine Frage: Sie erwähnen eine schlechte Leistung. Was würden Sie vorschlagen, um eine ähnliche Funktion im Hinblick auf die Leistung zu erzielen?
Guillaume M
1
Wie auch immer, wir haben jetzt Hooks und ich muss bald wieder eine Antwort aktualisieren.
polkovnikov.ph
13

Ich habe die polkovnikov.ph-Lösung auf React 16 / ES6 mit Verbesserungen wie Touch-Handling und Einrasten in ein Raster aktualisiert, was ich für ein Spiel benötige. Durch das Einrasten in ein Raster werden die Leistungsprobleme verringert.

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
quelle
hi @anyhotcountry wofür benutzt du den gridX Koeffizienten?
AlexNikonov
1
@AlexNikonov ist die Größe des (Fang-) Gitters in x-Richtung. Es wird empfohlen, gridX und gridY> 1 zu verwenden, um die Leistung zu verbessern.
Anyhotcountry
Das hat bei mir ganz gut funktioniert. Bei einer Änderung, die ich in der Funktion onStart () vorgenommen habe: Berechnung von relX und relY habe ich e.clienX-this.props.x verwendet. Dadurch konnte ich die ziehbare Komponente in einem übergeordneten Container platzieren, anstatt davon abhängig zu sein, dass die gesamte Seite der Ziehbereich ist. Ich weiß, dass es ein später Kommentar ist, wollte mich aber nur bedanken.
Geoff
11

React-Draggable ist ebenfalls einfach zu bedienen. 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')
);

Und meine 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>

Sie brauchen ihre Stile, was kurz ist, oder Sie bekommen nicht ganz das erwartete Verhalten. Ich mag das Verhalten mehr als einige der anderen möglichen Entscheidungen, aber es gibt auch etwas, das als reaktionsänderbar und beweglich bezeichnet wird . Ich versuche, die Größe mit Draggable zu ändern, aber bisher keine Freude.

Joseph Larson
quelle
7

Hier ist eine einfache moderne Annäherung an diese mit useState, useEffectund useRefin ES6.

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
Raffjones
quelle
2

Ich möchte ein drittes Szenario hinzufügen

Die Bewegungsposition wird in keiner Weise gespeichert. Stellen Sie sich das als Mausbewegung vor - Ihr Cursor ist keine Reaktionskomponente, oder?

Alles, was Sie tun, ist, Ihrer Komponente eine Requisite wie "ziehbar" und einen Stream der Ziehereignisse hinzuzufügen, die den Dom manipulieren.

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

In diesem Fall ist eine DOM-Manipulation eine elegante Sache (ich hätte nie gedacht, dass ich das sagen würde)

Thomas Deutsch
quelle
1
Könnten Sie die setXandYFunktion mit einer imaginären Komponente füllen ?
Thellimist
0

Ich habe die Klasse mithilfe von Refs aktualisiert, da alle Lösungen, die ich hier sehe, Dinge enthalten, die nicht mehr unterstützt werden oder bald abgeschrieben werden ReactDOM.findDOMNode. Kann Eltern einer untergeordneten Komponente oder einer Gruppe von Kindern sein :)

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;
Paul Ologeh
quelle