your programing

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

lovepro 2020. 10. 4. 12:54
반응형

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


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

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

감사.


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

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

그리고 여기에 연주 할 바이올린이 있습니다 : http://jsfiddle.net/Af9Jt/2/

var Draggable = React.createClass({
  getDefaultProps: function () {
    return {
      // allow the initial position to be passed in as a prop
      initialPos: {x: 0, y: 0}
    }
  },
  getInitialState: function () {
    return {
      pos: this.props.initialPos,
      dragging: false,
      rel: null // position relative to the cursor
    }
  },
  // we could get away with not having this (and just having the listeners on
  // our div), but then the experience would be possibly be janky. If there's
  // anything w/ a higher z-index that gets in the way, then you're toast,
  // etc.
  componentDidUpdate: function (props, state) {
    if (this.state.dragging && !state.dragging) {
      document.addEventListener('mousemove', this.onMouseMove)
      document.addEventListener('mouseup', this.onMouseUp)
    } else if (!this.state.dragging && state.dragging) {
      document.removeEventListener('mousemove', this.onMouseMove)
      document.removeEventListener('mouseup', this.onMouseUp)
    }
  },

  // calculate relative position to the mouse and set dragging=true
  onMouseDown: function (e) {
    // only left mouse button
    if (e.button !== 0) return
    var pos = $(this.getDOMNode()).offset()
    this.setState({
      dragging: true,
      rel: {
        x: e.pageX - pos.left,
        y: e.pageY - pos.top
      }
    })
    e.stopPropagation()
    e.preventDefault()
  },
  onMouseUp: function (e) {
    this.setState({dragging: false})
    e.stopPropagation()
    e.preventDefault()
  },
  onMouseMove: function (e) {
    if (!this.state.dragging) return
    this.setState({
      pos: {
        x: e.pageX - this.state.rel.x,
        y: e.pageY - this.state.rel.y
      }
    })
    e.stopPropagation()
    e.preventDefault()
  },
  render: function () {
    // transferPropsTo will merge style & other props passed into our
    // component to also be on the child DIV.
    return this.transferPropsTo(React.DOM.div({
      onMouseDown: this.onMouseDown,
      style: {
        left: this.state.pos.x + 'px',
        top: this.state.pos.y + 'px'
      }
    }, this.props.children))
  }
})

국가 소유권 등에 대한 생각

"누가 어떤 상태를 소유해야하는지"는 처음부터 대답해야 할 중요한 질문입니다. "드래그 가능한"구성 요소의 경우 몇 가지 다른 시나리오를 볼 수 있습니다.

시나리오 1

부모는 드래그 가능한 현재 위치를 소유해야합니다. 이 경우 draggable은 여전히 ​​"am I dragging"상태를 소유하지만 this.props.onChange(x, y)mousemove 이벤트가 발생할 때마다 호출 합니다.

시나리오 2

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

Mixin 또는 구성 요소?

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


저는 React -dnd , React를위한 유연한 HTML5 드래그 앤 드롭 믹스 인, 완전한 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 저장소로 이동 하세요.


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의 예.


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


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;

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

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

All you do, is to add a prop like "draggable" to your component and a stream of the dragging events that will manipulate the 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
    };
},

In this case, a DOM manipulation is an elegant thing (I never thought I'd say this)

참고URL : https://stackoverflow.com/questions/20926551/recommended-way-of-making-react-component-div-draggable

반응형