React component / div를 드래그 가능하게 만드는 권장 방법


100

드래그 가능한 (즉, 마우스로 재배치 할 수있는) React 구성 요소를 만들고 싶습니다. 이는 반드시 전역 상태 및 분산 된 이벤트 핸들러를 포함하는 것으로 보입니다. JS 파일의 전역 변수를 사용하여 더러운 방식으로 할 수 있으며 멋진 클로저 인터페이스로 래핑 할 수도 있지만 React와 더 잘 맞 물리는 방법이 있는지 알고 싶습니다.

또한 이전에 원시 JavaScript에서이 작업을 수행 한 적이 없기 때문에 특히 React와 관련된 모든 코너 케이스를 처리했는지 확인하기 위해 전문가가 어떻게 수행하는지보고 싶습니다.

감사.


사실, 나는 최소한 코드처럼 산문 설명에 만족하거나 심지어 "당신은 잘하고있다"는 말에 만족할 것입니다. 하지만 여기에 지금까지 내 작업의 JSFiddle이 있습니다. jsfiddle.net/Z2JtM
Andrew Fleenor 2014 년

현재 살펴볼 반응 코드의 예가 거의 없다는 점을 감안할 때 이것이 유효한 질문이라는 데 동의합니다
Jared Forsyth

1
내 사용 사례에 대한 간단한 HTML5 솔루션을 찾았습니다 -youtu.be/z2nHLfiiKBA . 누군가를 도울 수 있습니다 !!
Prem

이 시도. 래핑 된 요소를 드래그 할 수 있도록 바꾸는 간단한 HOC입니다. npmjs.com/package/just-drag
Shan

답변:


113

이 글을 블로그 게시물로 바꿔야 할 것 같지만 여기에 꽤 확실한 예가 있습니다.

댓글은 내용을 잘 설명해야하지만 질문이 있으면 알려주세요.

그리고 여기에 연주 할 바이올린이 있습니다 : 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

부모는 "움직이지 않는 위치"만 소유하면되므로 드래그 가능 개체는 "드래깅 위치"를 소유하게되지만 마우스를 올려서 this.props.onChange(x, y)최종 결정을 부모에게 연기합니다. 부모가 드래그 가능 항목이 끝나는 위치가 마음에 들지 않으면 상태를 업데이트하지 않고 드래그 가능 항목을 드래그하기 전에 초기 위치로 "스냅"합니다.

Mixin 또는 구성 요소?

@ssorallen은 "draggable"이 그 자체로 사물보다 속성이 더 많기 때문에 믹스 인으로 더 잘 작용할 수 있다고 지적했습니다. 믹스 인에 대한 나의 경험은 제한적이므로 복잡한 상황에서 어떻게 도움이되거나 방해가 될 수 있는지 보지 못했습니다. 이것이 최선의 선택 일 수 있습니다.


4
좋은 예입니다. 이것은 Mixin"Draggable"이 실제로 개체가 아니라 개체의 능력이기 때문에 전체 클래스보다 더 적절 해 보입니다 .
Ross Allen

2
나는 그것으로 조금 주위를 연주, 그것은 아무것도 할 부모 나던 외부 드래그처럼 보이지만 이상한 일 모든 종류의 또 다른에 드래그 구성 요소에 반응 할 때의 일
Gorkem Yurtseven

11
다음을 수행하여 jquery 종속성을 제거 할 수 있습니다. var computedStyle = window.getComputedStyle(this.getDOMNode()); pos = { top: parseInt(computedStyle.top), left: parseInt(computedStyle.left) }; react와 함께 jquery를 사용 하는 경우 아마도 뭔가 잘못하고있을 것입니다.) jquery 플러그인이 필요한 경우 일반적으로 순수한 반응으로 다시 작성하는 것이 더 쉽고 코드가 적다는 것을 알았습니다.
Matt Crinklaw-Vogt

7
그냥 더 방탄 솔루션을 사용하는 것입니다 말할 @ MattCrinklaw - 보그에 의해 위의 주석에 후속 싶어 this.getDOMNode().getBoundingClientRect()getComputedStyle 출력 할 수있는 등 유효한 CSS 속성 - autoA의 발생합니다 코드 위의 경우에를 NaN. MDN 기사 참조 : developer.mozilla.org/en-US/docs/Web/API/Element/…
Andru

2
그리고 re this.getDOMNode(), 그것은 더 이상 사용되지 않습니다. 참조를 사용하여 dom 노드를 가져옵니다. facebook.github.io/react/docs/...
크리스 Sattinger

65

저는 React -dnd , 유연한 HTML5 드래그 앤 드롭 믹스 인 인 React를 완전한 DOM 제어로 구현했습니다.

기존의 드래그 앤 드롭 라이브러리는 내 사용 사례에 맞지 않아서 직접 작성했습니다. Stampsy.com에서 약 1 년 동안 실행해온 코드와 비슷하지만 React와 Flux를 활용하기 위해 다시 작성되었습니다.

내가 가진 주요 요구 사항 :

  • 자체의 DOM 또는 CSS를 내 보내지 않고 소비하는 구성 요소에 맡깁니다.
  • 소비 구성 요소에 가능한 한 적은 구조를 적용하십시오.
  • HTML5 드래그 앤 드롭을 기본 백엔드로 사용하되 나중에 다른 백엔드를 추가 할 수 있도록합니다.
  • 원본 HTML5 API와 마찬가지로 "드래그 가능한 뷰"가 아니라 데이터 드래그를 강조합니다.
  • 소비 코드에서 HTML5 API 특성을 숨 깁니다.
  • 서로 다른 구성 요소는 서로 다른 종류의 데이터에 대한 "드래그 소스"또는 "드롭 대상"일 수 있습니다.
  • 필요한 경우 하나의 구성 요소에 여러 드래그 소스 및 드롭 대상을 포함 할 수 있습니다.
  • 호환되는 데이터를 끌거나 가져 가면 놓기 대상의 모양을 쉽게 변경할 수 있습니다.
  • 요소 스크린 샷 대신 드래그 썸네일에 이미지를 사용하여 브라우저의 단점을 피하세요.

이것이 당신에게 익숙한 것처럼 들리면 계속 읽으십시오.

용법

단순 드래그 소스

먼저 드래그 할 수있는 데이터 유형을 선언합니다.

드래그 소스 및 드롭 대상의 "호환성"을 확인하는 데 사용됩니다.

// 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)}일부 (하나 이상의) 요소 를 사용해야합니다 render. 즉, 하나의 요소에 여러 "드래그 핸들"이있을 수 있으며 서로 다른 항목 유형에 해당 할 수도 있습니다. ( JSX Spread Attributes 구문에 익숙하지 않은 경우 확인하십시오).

단순 드롭 대상

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

드래그 소스 + 하나의 컴포넌트에 드롭 타겟

이제 사용자가에서 이미지를 드래그 할 수 있기를 원한다고 가정 해 보겠습니다 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재정렬 가능 하게 만들고 싶습니다 . 우리는 그들을 구현해야 dropTarget하고 dragSource위해 ItemTypes.BLOCK.
  • 다른 종류의 블록을 추가한다고 가정합니다. 믹스 인에 배치하여 재정렬 로직을 재사용 할 수 있습니다.
  • dropTargetFor(...types) 한 번에 여러 유형을 지정할 수 있으므로 하나의 드롭 영역에서 여러 유형을 포착 할 수 있습니다.
  • 보다 세밀한 제어가 필요한 경우 대부분의 메서드는 마지막 매개 변수로 발생하는 드래그 이벤트를 전달합니다.

최신 문서 및 설치 지침 은 Github의 react-dnd 저장소로 이동 하세요.


6
마우스를 사용하는 것 외에 드래그 앤 드롭과 마우스 드래그의 공통점은 무엇입니까? 귀하의 답변은 질문과 전혀 관련이 없으며 분명히 도서관 광고입니다.
polkovnikov.ph

5
29 명이 질문과 관련이 있다고 생각한 것 같습니다. React DnD를 사용하면 마우스 드래그를 너무 잘 구현할 수도 있습니다 . 무료로 내 작업을 공유하고 다음 번 에 예제방대한 문서 작업을하는 것보다 더 나은 생각을 하게 되므로 엉뚱한 댓글을 읽는 데 시간을 할애 할 필요가 없습니다.
Dan Abramov

7
네, 완벽하게 압니다. 다른 곳에 문서가 있다는 사실이 이것이 주어진 질문에 대한 답임을 의미하지는 않습니다. 동일한 결과에 대해 "Google 사용"이라고 작성할 수 있습니다. 29 개의 찬성 투표는 답변의 질 때문이 아니라 알려진 사람의 긴 게시물 때문입니다.
polkovnikov.ph

자유롭게 드래그 물건의 공식 예에 업데이트 링크는 반응-DND를 : 기본 , 고급
uryga을

@DanAbramov 게시물을 작성하기위한 귀하의 노력에 감사드립니다. 게시물이 없었다면 아마도 그렇게 빨리 react-dnd 로 향하지 않았을 것입니다 . 다른 많은 제품과 마찬가지로 처음 에는 불필요하게 복잡해 보이지만 처음 에는 전문적인 방식으로 구성합니다 (문서를 살펴보면 일반적으로 소프트웨어 엔지니어링에 대해 한두 가지를 배웁니다 ...). 유닉스가 그것을 다시 구현할 운명이라는 것을 알고 있습니다.
Nathan Chappell

23

Jared Forsyth의 대답은 끔찍하게 잘못되었고 구식입니다. 사용stopPropagation , props 에서 상태 초기화 , jQuery 사용, 상태의 중첩 된 객체 와 같은 전체 반 패턴 집합을 따르며 이상한 dragging상태 필드가 있습니다. 다시 작성되는 경우 솔루션은 다음과 같지만 여전히 모든 마우스 이동 틱에서 가상 DOM 조정을 강제하고 성능이 좋지 않습니다.

UPD. 내 대답은 끔찍하게 잘못되었고 구식이었습니다. 이제 코드는 네이티브 이벤트 핸들러 및 스타일 업데이트를 사용하여 느린 React 구성 요소 수명주기의 문제를 완화하고, transform리플 로우로 이어지지 않으므로 사용 하며 requestAnimationFrame. 이제 시도한 모든 브라우저에서 일관되게 60FPS입니다.

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 아니요, 기본값은 사악합니다. 그게 문제입니다. 소품 변경시 적절한 조치는 무엇입니까? 상태를 새 기본값으로 재설정해야합니까? 새 기본값을 이전 기본값과 비교하여 기본값이 변경된 경우에만 상태를 기본값으로 재설정해야합니까? 사용자가 상수 값만 사용하도록 제한 할 수있는 방법은 없습니다. 그것이 그것이 반 패턴 인 이유입니다. 기본값은 고차 컴포넌트 (즉, 객체가 아닌 전체 클래스에 대해)를 통해 명시 적으로 생성되어야하며 결코 소품을 통해 설정되어서는 안됩니다.
polkovnikov.ph 2011

1
저는 정중하게 동의하지 않습니다. 구성 요소 상태는 예를 들어 앱 전체와 관련이없는 구성 요소의 UI에 특정한 데이터를 저장하기에 좋은 장소입니다. 일부 인스턴스에서 잠재적으로 기본값을 소품으로 전달할 수없는 경우 마운트 후이 데이터를 검색하는 옵션이 제한되어 있으며 많은 (대부분?) 상황에서 잠재적으로 다른 someDefaultValue prop을 전달할 수있는 구성 요소 주변의 변동보다 바람직하지 않습니다. 나중 날짜. 당신이 IMO에서 제안하는대로 임, 모범 사례 또는 종류의 아무것도는 간단하지 유해 그것을 옹호하지
라이언 J

2
참으로 매우 간단하고 우아한 솔루션입니다. 제 생각이 비슷하다는 것을 알게되어 기쁩니다. 한 가지 질문이 있습니다. 성능 저하를 언급했습니다. 성능을 염두에두고 유사한 기능을 달성하기 위해 무엇을 제안 하시겠습니까?
Guillaume M

1
어쨌든 우리는 지금 갈고리가 있으며 곧 다시 한 번 답변을 업데이트해야합니다.
polkovnikov.ph

14

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 gridX 계수를 사용하는 이유 는 무엇입니까?
AlexNikonov

1
@AlexNikonov x 방향의 (맞추기) 그리드의 크기입니다. 성능을 향상 시키려면 gridX 및 gridY> 1을 사용하는 것이 좋습니다.
anyhotcountry

이것은 나를 위해 아주 잘 작동했습니다. onStart () 함수에서 변경 한 내용 : relX 및 relY 계산 e.clienX-this.props.x를 사용했습니다. 이를 통해 전체 페이지가 드래그 영역 인 것에 의존하지 않고 드래그 가능한 컴포넌트를 상위 컨테이너 내부에 배치 할 수있었습니다. 늦었다는 건 알지만 감사하다고 말하고 싶었습니다.
Geoff

11

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 이라는 것도 있습니다 . 드래그 가능한 작업으로 크기 조정을 시도하고 있지만 지금까지는 기쁨이 없습니다.


9

useState, useEffectuseRefES6 에서 이에 대한 간단한 현대적인 접근 방식이 있습니다.

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

1
이것은 여기에서 가장 최신 답변 인 것 같습니다.
codyThompson

2

번째 시나리오 를 추가하고 싶습니다

이동 위치는 저장되지 않습니다. 마우스 움직임이라고 생각하세요. 커서는 React 컴포넌트가 아닙니다.

당신이 할 일은 컴포넌트에 "draggable"과 같은 소품과 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

2

다음은 후크를 사용한 2020 년 답변입니다.

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

그런 다음 후크를 사용하는 구성 요소 :


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

1

여기에서 보는 모든 솔루션에는 더 이상 지원되지 않거나 곧 감가 상각 될 것이므로 refs를 사용하여 클래스를 업데이트했습니다 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.