Zalecany sposób na przeciągnięcie komponentu / div Reacta

101

Chcę utworzyć komponent React z możliwością przeciągania (czyli repozycjonowania za pomocą myszy), który wydaje się koniecznie obejmować obsługę stanu globalnego i rozproszonych zdarzeń. Mogę to zrobić na brudno, z globalną zmienną w moim pliku JS i prawdopodobnie mógłbym nawet owinąć ją ładnym interfejsem zamknięcia, ale chcę wiedzieć, czy istnieje sposób, który lepiej współgra z React.

Ponadto, ponieważ nigdy wcześniej nie robiłem tego w surowym języku JavaScript, chciałbym zobaczyć, jak robi to ekspert, aby upewnić się, że załatwiłem wszystkie przypadki narożne, zwłaszcza te związane z Reactem.

Dzięki.

Andrew Fleenor
źródło
Właściwie byłbym przynajmniej tak samo zadowolony z wyjaśnienia prozy, jak z kodu, a nawet po prostu „robisz to dobrze”. Ale oto JSFiddle mojej dotychczasowej pracy: jsfiddle.net/Z2JtM
Andrew Fleenor
Zgadzam się, że jest to poprawne pytanie, biorąc pod uwagę, że obecnie jest bardzo niewiele przykładów kodu reakcji, którym można się przyjrzeć
Jared Forsyth
1
Znalazłem proste rozwiązanie HTML5 dla mojego przypadku użycia - youtu.be/z2nHLfiiKBA . Może komuś pomóc !!
Prem
Spróbuj tego. To prosta HOC, aby zamienić elementy owinięte w przeciąganie npmjs.com/package/just-drag
Shan,

Odpowiedzi:

114

Powinienem prawdopodobnie zamienić to w post na blogu, ale oto całkiem solidny przykład.

Komentarze powinny dość dobrze wyjaśniać, ale daj mi znać, jeśli masz pytania.

A oto skrzypce do zabawy: 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))
  }
})

Myśli o własności państwowej itp.

„Kto powinien posiadać jaki stan” to ważne pytanie od samego początku. W przypadku komponentu „przeciągalnego” mogłem zobaczyć kilka różnych scenariuszy.

Scenariusz 1

rodzic powinien być właścicielem bieżącej pozycji elementu przeciąganego. W tym przypadku element do przeciągania nadal będzie posiadał stan „czy przeciągam”, ale będzie wywoływał, this.props.onChange(x, y)gdy wystąpi zdarzenie przesunięcia myszy.

Scenariusz 2

rodzic musi posiadać tylko „nieruchomą pozycję”, więc element do przeciągania będzie posiadał swoją „pozycję przeciągania”, ale po włączeniu myszy wywoła this.props.onChange(x, y)i odroczy ostateczną decyzję do rodzica. Jeśli rodzicowi nie podoba się miejsce, w którym zakończył się element przeciągany, po prostu nie zaktualizowałby swojego stanu, a element do przeciągania „wskoczyłby z powrotem” do swojej początkowej pozycji przed przeciągnięciem.

Mieszanka czy składnik?

@ssorallen zwrócił uwagę, że skoro „draggable” jest bardziej atrybutem niż rzeczą samą w sobie, może lepiej służyć jako mieszanka. Moje doświadczenie z mieszankami jest ograniczone, więc nie widziałem, jak mogą pomóc lub przeszkodzić w skomplikowanych sytuacjach. To może być najlepsza opcja.

Jared Forsyth
źródło
4
Świetny przykład. Wydaje się to bardziej odpowiednie jako a Mixinniż pełna klasa, ponieważ „Draggable” w rzeczywistości nie jest obiektem, jest to zdolność obiektu.
Ross Allen,
2
Trochę się z tym bawiłem, wydaje się, że wyciąganie poza jego rodzica nic nie robi, ale różne dziwne rzeczy dzieją się, gdy jest wciągany do innego składnika reakcji
Gorkem Yurtseven
11
Możesz usunąć zależność od jquery, wykonując następujące czynności: var computedStyle = window.getComputedStyle(this.getDOMNode()); pos = { top: parseInt(computedStyle.top), left: parseInt(computedStyle.left) }; Jeśli używasz jquery z reakcją, prawdopodobnie robisz coś źle;) Jeśli potrzebujesz jakiejś wtyczki jquery, stwierdzam, że zwykle łatwiej i mniej kodu jest przepisanie go w czystej reakcji.
Matt Crinklaw-Vogt
7
Chciałem tylko nawiązać do powyższego komentarza @ MattCrinklaw-Vogt, aby powiedzieć, że należy użyć bardziej kuloodpornego rozwiązania this.getDOMNode().getBoundingClientRect()- getComputedStyle może wyprowadzić dowolną prawidłową właściwość CSS, autow tym w takim przypadku powyższy kod spowoduje utworzenie pliku NaN. Zobacz artykuł w MDN: developer.mozilla.org/en-US/docs/Web/API/Element/ ...
Andru
2
I aby kontynuować this.getDOMNode(), to zostało wycofane. Użyj ref, aby uzyskać węzeł dom. facebook.github.io/react/docs/…
Chris Sattinger
65

Zaimplementowałem react-dnd , elastyczną mieszankę HTML5 typu przeciągnij i upuść dla Reacta z pełną kontrolą DOM.

Istniejące biblioteki typu „przeciągnij i upuść” nie pasowały do ​​mojego przypadku użycia, więc napisałem własną. Jest podobny do kodu, który używamy od około roku na Stampsy.com, ale został przepisany, aby wykorzystać React i Flux.

Kluczowe wymagania jakie miałem:

  • Emituj zero własnego DOM lub CSS, pozostawiając to konsumującym komponentom;
  • Nakładaj jak najmniejszą strukturę na zużywane komponenty;
  • Użyj funkcji przeciągnij i upuść HTML5 jako podstawowego zaplecza, ale umożliwisz dodawanie różnych backendów w przyszłości;
  • Podobnie jak oryginalny interfejs API HTML5, kładź nacisk na przeciąganie danych, a nie tylko na „przeciągalne widoki”;
  • Ukryj dziwactwa interfejsu API HTML5 przed konsumującym kodem;
  • Różne komponenty mogą być „źródłami przeciągania” lub „celami upuszczania” dla różnych rodzajów danych;
  • Pozwól, aby jeden komponent zawierał kilka źródeł przeciągania i upuszczania celów w razie potrzeby;
  • Ułatw celom upuszczania zmianę ich wyglądu, jeśli zgodne dane są przeciągane lub najeżdżane;
  • Ułatw korzystanie z obrazów do przeciągania miniatur zamiast zrzutów ekranu elementów, omijając dziwactwa przeglądarki.

Jeśli to brzmi znajomo, czytaj dalej.

Stosowanie

Proste źródło przeciągania

Najpierw zadeklaruj typy danych, które można przeciągać.

Służą one do sprawdzania „zgodności” źródeł przeciągania i celów upuszczania:

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

(Jeśli nie masz wielu typów danych, ta biblioteka może nie być dla Ciebie).

Następnie stwórzmy bardzo prosty komponent do przeciągania, który po przeciągnięciu reprezentuje 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)} />
    );
  }
);

Podając specyfikację configureDragDrop, mówimy o DragDropMixinzachowaniu tego komponentu przez przeciąganie i upuszczanie. Komponenty, które można przeciągać i upuszczać, używają tego samego miksu.

Wewnątrz configureDragDropmusimy zadzwonić registerTypedo każdego z naszych niestandardowych ItemTypesobsługiwanych przez ten składnik. Na przykład, nie może być kilka reprezentacje obrazów w aplikacji, a każda będzie dostarczenie dragSourcedo ItemTypes.IMAGE.

A dragSourceto po prostu obiekt określający sposób działania źródła przeciągania. Musisz zaimplementować, beginDragaby zwrócić element, który reprezentuje przeciągane dane i opcjonalnie kilka opcji, które dostosowują przeciągany interfejs użytkownika. Opcjonalnie możesz zaimplementować, canDragaby zabronić przeciągania lub endDrag(didDrop)wykonać jakąś logikę, gdy upuszczenie wystąpiło (lub nie). Możesz dzielić tę logikę między komponentami, pozwalając na generowanie dragSourcedla nich współdzielonego miksu .

Na koniec musisz użyć {...this.dragSourceFor(itemType)}na niektórych (jednym lub więcej) elementach programu, renderaby dołączyć uchwyty przeciągania. Oznacza to, że możesz mieć kilka „uchwytów przeciągania” w jednym elemencie i mogą one nawet odpowiadać różnym typom elementów. (Jeśli nie znasz składni JSX Spread Attributes , sprawdź to).

Prosty cel upuszczenia

Powiedzmy, że chcemy ImageBlockbyć celem upuszczenia dla IMAGEs. To niemal tak samo, z tym że musimy dać registerTypedo dropTargetrealizacji:

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

Przeciągnij źródło + upuść cel w jednym komponencie

Powiedzmy, że teraz chcemy, aby użytkownik mógł wyciągnąć obraz z ImageBlock. Musimy tylko dodać dragSourcedo niego odpowiednie i kilka handlerów:

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

Co jeszcze jest możliwe?

Nie opisałem wszystkiego, ale można użyć tego API na kilka innych sposobów:

  • Użyj getDragState(type)i, getDropState(type)aby dowiedzieć się, czy przeciąganie jest aktywne, i użyj go do przełączania klas lub atrybutów CSS;
  • Określ, dragPreviewaby Imageużywać obrazów jako symboli zastępczych przeciągania (użyj, ImagePreloaderMixinaby je załadować);
  • Powiedzmy, że chcemy, aby można było je ponownie ImageBlockszamówić. Potrzebujemy ich tylko do wdrożenia dropTargeti dragSourcedla ItemTypes.BLOCK.
  • Załóżmy, że dodamy inne rodzaje bloków. Możemy ponownie wykorzystać ich logikę zmiany kolejności, umieszczając ją w miksie.
  • dropTargetFor(...types) pozwala określić kilka typów na raz, więc jedna strefa zrzutu może złapać wiele różnych typów.
  • Gdy potrzebujesz bardziej szczegółowej kontroli, większość metod jest przekazywana jako ostatni parametr jako zdarzenie przeciągania, które je spowodowało.

Aby uzyskać aktualną dokumentację i instrukcje instalacji, przejdź na stronę react-dnd repo na Github .

Dan Abramov
źródło
6
Co mają wspólnego przeciąganie i upuszczanie oraz przeciąganie myszą niż używanie myszy? Twoja odpowiedź nie jest w ogóle związana z pytaniem i najwyraźniej jest reklamą biblioteki.
polkovnikov.ph
5
Wygląda na to, że 29 osób uznało to za związane z pytaniem. React DnD pozwala również dobrze zaimplementować przeciąganie myszą . Pomyślę, że lepiej nie udostępniać mojej pracy za darmo i następnym razem pracować nad przykładami i obszerną dokumentacją, aby nie musieć tracić czasu na czytanie złośliwych komentarzy.
Dan Abramov
7
Tak, doskonale to wiem. Fakt, że masz dokumentację gdzie indziej, nie oznacza, że ​​jest to odpowiedź na zadane pytanie. Możesz napisać „użyj Google” dla tego samego wyniku. 29 głosów za pozytywnymi opiniami wynika z długiego postu znanej osoby, a nie z powodu jakości odpowiedzi.
polkovnikov.ph
zaktualizowane linki do oficjalnych przykładów rzeczy, które można swobodnie przeciągać ,
zreak
@DanAbramov Doceniam twój wysiłek, aby napisać post, bez niego prawdopodobnie nie byłbym nastawiony tak szybko na reakcję . Podobnie jak wiele produktów, wydaje się niepotrzebnie skomplikowane w pierwszym , ale umieścić razem w primely profesjonalny sposób (przeżywa dokumentacji zwykle dowiedzieć się coś więcej o inżynierii oprogramowania ...) czytałem „tych, którzy nie wiem, że unix są skazani na ponowne zaimplementowanie go ... słabo ”i czuję, że coś podobnego może dotyczyć wielu twoich kreacji (w tym tej).
Nathan Chappell
23

Odpowiedź Jareda Forsytha jest strasznie błędna i nieaktualna. Podąża za całym zestawem antywzorów, takich jak użyciestopPropagation , inicjalizacja stanu z właściwości, użycie jQuery, zagnieżdżone obiekty w stanie i ma dziwne draggingpole stanu. Jeśli zostanie przepisane, rozwiązanie będzie następujące, ale nadal wymusza uzgadnianie wirtualnego DOM przy każdym ruchu myszy i nie jest zbyt wydajne.

UPD. Moja odpowiedź była strasznie błędna i nieaktualna. Teraz kod łagodzi problemy związane z powolnym cyklem życia komponentu React, używając natywnych procedur obsługi zdarzeń i aktualizacji stylów, używa, transformponieważ nie prowadzi do ponownego przepływu i ogranicza zmiany DOM requestAnimationFrame. Teraz to dla mnie konsekwentnie 60 FPS w każdej przeglądarce, którą wypróbowałem.

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

i trochę 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;
}

Przykład na JSFiddle.

polkovnikov.ph
źródło
2
Dzięki temu zdecydowanie nie jest to najbardziej wydajne rozwiązanie, ale jest zgodne z najlepszymi praktykami dzisiejszego budowania aplikacji.
Spets
1
@ryanj Nie, wartości domyślne są złe, to jest problem. Jakie jest właściwe działanie, gdy rekwizyty się zmieniają? Czy powinniśmy zresetować stan do nowego domyślnego? Czy powinniśmy porównać nową wartość domyślną ze starą wartością domyślną, aby przywrócić stan domyślny tylko wtedy, gdy wartość domyślna uległa zmianie? Nie ma sposobu, aby ograniczyć użytkownika do używania tylko stałej wartości i nic więcej. Dlatego jest to anty-wzór. Wartości domyślne powinny być tworzone jawnie za pomocą komponentów wysokiego rzędu (tj. Dla całej klasy, a nie dla obiektu) i nigdy nie powinny być ustawiane za pomocą właściwości.
polkovnikov.ph
1
Z całym szacunkiem nie zgadzam się - stan komponentu to doskonałe miejsce do przechowywania danych specyficznych dla interfejsu użytkownika komponentu, który na przykład nie ma związku z całą aplikacją. Nie będąc w stanie potencjalnie przekazać wartości domyślnych jako właściwości w niektórych przypadkach, opcje pobierania tych danych po zamontowaniu są ograniczone iw wielu (większości?) Okolicznościach mniej pożądane niż kaprysy związane z komponentem, który może być przekazywany do innej właściwości o wartości domyślnej w późniejszy termin. Nie zalecam tego jako najlepszej praktyki ani niczego w tym rodzaju, po prostu nie jest tak szkodliwy, jak sugerujesz imo
ryan j
2
Naprawdę bardzo proste i eleganckie rozwiązanie. Cieszę się, że moje podejście do tego było podobne. Mam jedno pytanie: wspominasz o słabej wydajności, co byś zaproponował, aby osiągnąć podobną funkcję z myślą o wydajności?
Guillaume M
1
W każdym razie mamy teraz haczyki i muszę wkrótce ponownie zaktualizować odpowiedź.
polkovnikov.ph
14

Zaktualizowałem rozwiązanie polkovnikov.ph do React 16 / ES6 z ulepszeniami, takimi jak obsługa dotykowa i przyciąganie do siatki, czego potrzebuję w grze. Przyciąganie do siatki łagodzi problemy z wydajnością.

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
źródło
cześć @anyhotcountry, do czego używasz współczynnika gridX ?
AlexNikonov
1
@AlexNikonov to rozmiar (przyciągania) siatki w kierunku x. Aby poprawić wydajność, zaleca się ustawienie gridX i gridY> 1.
anyhotcountry
U mnie to działało całkiem nieźle. Po zmianie dokonanej w funkcji onStart (): obliczając relX i relY użyłem e.clienX-this.props.x. Pozwoliło mi to umieścić składnik, który można przeciągać, wewnątrz kontenera nadrzędnego, zamiast polegać na całej stronie będącej obszarem przeciągania. Wiem, że to późny komentarz, ale chciałem tylko podziękować.
Geoff
11

Reag-draggable jest również łatwy w użyciu. 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')
);

I mój 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>

Potrzebujesz ich stylów, które są krótkie lub nie uzyskasz oczekiwanego zachowania. Zachowanie podoba mi się bardziej niż niektóre inne możliwe wybory, ale jest też coś, co nazywa się możliwością zmiany rozmiaru i przeniesienia . Próbuję zmienić rozmiar, pracując z draggable, ale jak dotąd nie ma radości.

Joseph Larson
źródło
10

Oto prosty nowoczesne podejście do tego z useState, useEffecti useRefw 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
kod z uczuciem
źródło
1
Wydaje się, że jest to najbardziej aktualna odpowiedź.
codyThompson
3

Oto odpowiedź na rok 2020 z hakiem:

function useDragging() {
  const [isDragging, setIsDragging] = useState(false);
  const [pos, setPos] = useState({ x: 0, y: 0 });
  const ref = useRef(null);

  function onMouseMove(e) {
    if (!isDragging) return;
    setPos({
      x: e.x - ref.current.offsetWidth / 2,
      y: e.y - ref.current.offsetHeight / 2,
    });
    e.stopPropagation();
    e.preventDefault();
  }

  function onMouseUp(e) {
    setIsDragging(false);
    e.stopPropagation();
    e.preventDefault();
  }

  function onMouseDown(e) {
    if (e.button !== 0) return;
    setIsDragging(true);

    setPos({
      x: e.x - ref.current.offsetWidth / 2,
      y: e.y - ref.current.offsetHeight / 2,
    });

    e.stopPropagation();
    e.preventDefault();
  }

  // When the element mounts, attach an mousedown listener
  useEffect(() => {
    ref.current.addEventListener("mousedown", onMouseDown);

    return () => {
      ref.current.removeEventListener("mousedown", onMouseDown);
    };
  }, [ref.current]);

  // Everytime the isDragging state changes, assign or remove
  // the corresponding mousemove and mouseup handlers
  useEffect(() => {
    if (isDragging) {
      document.addEventListener("mouseup", onMouseUp);
      document.addEventListener("mousemove", onMouseMove);
    } else {
      document.removeEventListener("mouseup", onMouseUp);
      document.removeEventListener("mousemove", onMouseMove);
    }
    return () => {
      document.removeEventListener("mouseup", onMouseUp);
      document.removeEventListener("mousemove", onMouseMove);
    };
  }, [isDragging]);

  return [ref, pos.x, pos.y, isDragging];
}

Następnie komponent korzystający z haka:


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>
  );
}
Evan Conrad
źródło
2

Chciałbym dodać trzeci scenariusz

Ruchoma pozycja nie jest w żaden sposób zapisywana. Pomyśl o tym jak o ruchu myszy - Twój kursor nie jest elementem React, prawda?

Wszystko, co musisz zrobić, to dodać do komponentu właściwość typu „draggable” i strumień zdarzeń związanych z przeciąganiem, które będą manipulować domeną.

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

W tym przypadku manipulacja DOM jest elegancką rzeczą (nigdy nie myślałem, że to powiem)

Thomas Deutsch
źródło
1
czy mógłbyś wypełnić setXandYfunkcję wyimaginowanym komponentem?
Thellimist
1

Zaktualizowałem klasę za pomocą referencji, ponieważ wszystkie rozwiązania, które tu widzę, mają rzeczy, które nie są już obsługiwane lub wkrótce zostaną zdeprecjonowane, takie jak ReactDOM.findDOMNode. Może być rodzicem elementu podrzędnego lub grupy dzieci :)

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
źródło