簡體   English   中英

使 React 組件/div 可拖動的推薦方法

[英]Recommended way of making React component/div draggable

我想做一個可拖動(即鼠標可重新定位)的 React 組件,這似乎必然涉及全局 state 和分散的事件處理程序。 我可以用骯臟的方式來做,在我的 JS 文件中使用一個全局變量,甚至可以將它包裝在一個漂亮的閉包接口中,但我想知道是否有一種方法可以更好地與 React 結合。

此外,由於我以前從未在 raw 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

父級應該擁有可拖動的當前位置。 在這種情況下,可拖動對象仍將擁有“我在拖動”狀態,但只要發生 mousemove 事件就會調用this.props.onChange(x, y)

方案 2

父母只需要擁有“非移動位置”,因此可拖動將擁有它的“拖動位置”,但 onmouseup 它將調用this.props.onChange(x, y)並將最終決定推遲給父母。 如果父級不喜歡可拖動對象的最終位置,它就不會更新它的狀態,並且可拖動對象會在拖動之前“彈回”到它的初始位置。

Mixin 還是組件?

@ssorallen 指出,因為“可拖動”更多的是屬性而不是事物本身,所以它可能更好地用作混合。 我對 mixins 的經驗是有限的,所以我還沒有看到它們在復雜的情況下如何提供幫助或阻礙。 這可能是最好的選擇。

我實現了react-dnd ,這是一個用於 React 的靈活的 HTML5 拖放混合器,具有完整的 DOM 控制。

現有的拖放庫不適合我的用例,所以我自己編寫了。 它類似於我們在 Stampsy.com 上運行了大約一年的代碼,但為了利用 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這個組件的拖放行為。 可拖動和可放置組件都使用相同的 mixin。

configureDragDrop中,我們需要為組件支持的每個自定義ItemTypes調用registerType 例如,您的應用程序中可能有多個圖像表示,每個都將為ItemTypes.IMAGE提供一個dragSource

dragSource只是一個指定拖動源如何工作的對象。 您必須實現beginDrag以返回表示您正在拖動的數據的項目,並且可以選擇一些調整拖動 UI 的選項。 您可以選擇實現canDrag以禁止拖動,或endDrag(didDrop)以在發生(或未發生)放置時執行某些邏輯。 您可以通過讓共享的 mixin 為它們生成dragSource來在組件之間共享此邏輯。

最后,您必須在render中的某些(一個或多個)元素上使用{...this.dragSourceFor(itemType)}以附加拖動處理程序。 這意味着您可以在一個元素中擁有多個“拖動手柄”,它們甚至可能對應於不同的項目類型。 (如果您不熟悉JSX 傳播屬性語法,請查看)。

簡單放置目標

假設我們希望ImageBlock成為IMAGE的放置目標。 這幾乎是一樣的,除了我們需要給registerType一個dropTarget實現:

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可重新排序。 我們只需要它們為ItemTypes.BLOCK實現dropTargetdragSource
  • 假設我們添加其他類型的塊。 我們可以通過將它們放在 mixin 中來重用它們的重新排序邏輯。
  • dropTargetFor(...types)允許一次指定多種類型,因此一個放置區可以捕獲許多不同的類型。
  • 當您需要更細粒度的控制時,大多數方法都會傳遞導致它們作為最后一個參數的拖動事件。

有關最新的文檔和安裝說明,請前往Github 上的 react-dnd 存儲庫

@codewithfeeling 的答案非常錯誤,並且滯后於您的頁面! 這是他的代碼的一個版本,其中修復和注釋了問題。 這應該是現在最新的基於鈎子的答案。

import React, { useRef, useState, useEffect, useCallback } from "react";

/// throttle.ts
export 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;
};

/// use-draggable.ts
const id = (x) => x;
// complex logic should be a hook, not a component
const useDraggable = ({ onDrag = id } = {}) => {
  // this state doesn't change often, so it's fine
  const [pressed, setPressed] = useState(false);

  // do not store position in useState! even if you useEffect on
  // it and update `transform` CSS property, React still rerenders
  // on every state change, and it LAGS
  const position = useRef({ x: 0, y: 0 });
  const ref = useRef();

  // we've moved the code into the hook, and it would be weird to
  // return `ref` and `handleMouseDown` to be set on the same element
  // why not just do the job on our own here and use a function-ref
  // to subscribe to `mousedown` too? it would go like this:
  const unsubscribe = useRef();
  const legacyRef = useCallback((elem) => {
    // in a production version of this code I'd use a
    // `useComposeRef` hook to compose function-ref and object-ref
    // into one ref, and then would return it. combining
    // hooks in this way by hand is error-prone

    // then I'd also split out the rest of this function into a
    // separate hook to be called like this:
    // const legacyRef = useDomEvent('mousedown');
    // const combinedRef = useCombinedRef(ref, legacyRef);
    // return [combinedRef, pressed];
    ref.current = elem;
    if (unsubscribe.current) {
      unsubscribe.current();
    }
    if (!elem) {
      return;
    }
    const handleMouseDown = (e) => {
      // don't forget to disable text selection during drag and drop
      // operations
      e.target.style.userSelect = "none";
      setPressed(true);
    };
    elem.addEventListener("mousedown", handleMouseDown);
    unsubscribe.current = () => {
      elem.removeEventListener("mousedown", handleMouseDown);
    };
  }, []);
  // useEffect(() => {
  //   return () => {
  //     // this shouldn't really happen if React properly calls
  //     // function-refs, but I'm not proficient enough to know
  //     // for sure, and you might get a memory leak out of it
  //     if (unsubscribe.current) {
  //       unsubscribe.current();
  //     }
  //   };
  // }, []);

  useEffect(() => {
    // why subscribe in a `useEffect`? because we want to subscribe
    // to mousemove only when pressed, otherwise it will lag even
    // when you're not dragging
    if (!pressed) {
      return;
    }

    // updating the page without any throttling is a bad idea
    // requestAnimationFrame-based throttle would probably be fine,
    // but be aware that naive implementation might make element
    // lag 1 frame behind cursor, and it will appear to be lagging
    // even at 60 FPS
    const handleMouseMove = throttle((event) => {
      // needed for TypeScript anyway
      if (!ref.current || !position.current) {
        return;
      }
      const pos = position.current;
      // it's important to save it into variable here,
      // otherwise we might capture reference to an element
      // that was long gone. not really sure what's correct
      // behavior for a case when you've been scrolling, and
      // the target element was replaced. probably some formulae
      // needed to handle that case. TODO
      const elem = ref.current;
      position.current = onDrag({
        x: pos.x + event.movementX,
        y: pos.y + event.movementY
      });
      elem.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
    });
    const handleMouseUp = (e) => {
      e.target.style.userSelect = "auto";
      setPressed(false);
    };
    // subscribe to mousemove and mouseup on document, otherwise you
    // can escape bounds of element while dragging and get stuck
    // dragging it forever
    document.addEventListener("mousemove", handleMouseMove);
    document.addEventListener("mouseup", handleMouseUp);
    return () => {
      handleMouseMove.cancel();
      document.removeEventListener("mousemove", handleMouseMove);
      document.removeEventListener("mouseup", handleMouseUp);
    };
    // if `onDrag` wasn't defined with `useCallback`, we'd have to
    // resubscribe to 2 DOM events here, not to say it would mess
    // with `throttle` and reset its internal timer
  }, [pressed, onDrag]);

  // actually it makes sense to return an array only when
  // you expect that on the caller side all of the fields
  // will be usually renamed
  return [legacyRef, pressed];

  // > seems the best of them all to me
  // this code doesn't look pretty anymore, huh?
};

/// example.ts
const quickAndDirtyStyle = {
  width: "200px",
  height: "200px",
  background: "#FF9900",
  color: "#FFFFFF",
  display: "flex",
  justifyContent: "center",
  alignItems: "center"
};

const DraggableComponent = () => {
  // handlers must be wrapped into `useCallback`. even though
  // resubscribing to `mousedown` on every tick is quite cheap
  // due to React's event system, `handleMouseDown` might be used
  // in `deps` argument of another hook, where it would really matter.
  // as you never know where return values of your hook might end up,
  // it's just generally a good idea to ALWAYS use `useCallback`

  // it's nice to have a way to at least prevent element from
  // getting dragged out of the page
  const handleDrag = useCallback(
    ({ x, y }) => ({
      x: Math.max(0, x),
      y: Math.max(0, y)
    }),
    []
  );

  const [ref, pressed] = useDraggable({
    onDrag: handleDrag
  });

  return (
    <div ref={ref} style={quickAndDirtyStyle}>
      <p>{pressed ? "Dragging..." : "Press to drag"}</p>
    </div>
  );
};

在此處查看此代碼,這是一個改進了光標定位的版本, 此處帶有約束onDrag和硬核鈎子展示

(以前這個答案是關於 pre-hook React,並告訴Jared Forsyth 的答案是非常錯誤的。現在這無關緊要,但它仍然在答案的編輯歷史中。)

這是 ES6 中使用useStateuseEffectuseRef的簡單現代方法。

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

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;

這是帶有 Hook 的 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>
  );
}

這是另一個簡單的 React hooks 解決方案,沒有任何第三方庫,基於 codewithfeeling 和 Evan Conrad 的解決方案。 https://stackoverflow.com/a/63887486/1309218 https://stackoverflow.com/a/61667523/1309218

import React, { useCallback, useRef, useState } from "react";
import styled, { css } from "styled-components/macro";

const Component: React.FC = () => {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const elementRef = useRef<HTMLDivElement>(null);

  const onMouseDown = useCallback(
    (event) => {
      const onMouseMove = (event: MouseEvent) => {
        position.x += event.movementX;
        position.y += event.movementY;
        const element = elementRef.current;
        if (element) {
          element.style.transform = `translate(${position.x}px, ${position.y}px)`;
        }
        setPosition(position);
      };
      const onMouseUp = () => {
        document.removeEventListener("mousemove", onMouseMove);
        document.removeEventListener("mouseup", onMouseUp);
      };
      document.addEventListener("mousemove", onMouseMove);
      document.addEventListener("mouseup", onMouseUp);
    },
    [position, setPosition, elementRef]
  );

  return (
    <Container>
      <DraggableItem ref={elementRef} onMouseDown={onMouseDown}>
      </DraggableItem>
    </Container>
  );
};

const Container = styled.div`
  position: absolute;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
  overflow: hidden;
`;

const DraggableItem = styled.div`
  position: absolute;
  z-index: 1;
  left: 20px;
  top: 20px;
  width: 100px;
  height: 100px;
  background-color: green;
`;

我想添加第三個場景

移動位置不會以任何方式保存。 把它想象成一個鼠標移動——你的光標不是一個 React 組件,對吧?

您所做的就是向您的組件添加一個類似於“可拖動”的道具以及將操縱 dom 的拖動事件流。

setXandY: function(event) {
    // DOM Manipulation of x and y on your node
},

componentDidMount: function() {
    if(this.props.draggable) {
        var node = this.getDOMNode();
        dragStream(node).onValue(this.setXandY);  //baconjs stream
    };
},

在這種情況下,DOM 操作是一件優雅的事情(我從沒想過我會這么說)

我已經使用 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;

詳細闡述埃文康拉德的回答( https://stackoverflow.com/a/63887486/1531141 )我來到了這種打字稿方法:

import { RefObject, useEffect, useRef, useState } from "react";

export enum DraggingState {
    undefined = -1,
    starts = 0, 
    moves = 1,
    finished = 2
}

export default function useDragging() {
    const [state, setState] = useState(DraggingState.undefined);        
    const [point, setPoint] = useState({x: 0, y: 0});                   // point of cursor in relation to the element's parent
    const [elementOffset, setElementOffset] = useState({x: 0, y: 0});   // offset of element in relation to it's parent
    const [touchOffset, setTouchOffset] = useState({x: 0, y: 0});       // offset of mouse down point in relation to the element
    const ref = useRef() as RefObject<HTMLDivElement>;

// shows active state of dragging
const isDragging = () => {
    return (state === DraggingState.starts) || (state === DraggingState.moves);
}

function onMouseDown(e: MouseEvent) {
    const parentElement = ref.current?.offsetParent as HTMLElement;
    if (e.button !== 0 || !ref.current || !parentElement) return;
    
    // First entry to the flow. 
    // We save touchOffset value as parentElement's offset 
    // to calculate element's offset on the move. 
    setPoint({
        x: e.x - parentElement.offsetLeft,
        y: e.y - parentElement.offsetTop
    });
    setElementOffset({
        x: ref.current.offsetLeft,
        y: ref.current.offsetTop
    });
    setTouchOffset({
        x: e.x - parentElement.offsetLeft - ref.current.offsetLeft,
        y: e.y - parentElement.offsetTop - ref.current.offsetTop
    });

    setState(DraggingState.starts);
}

function onMouseMove(e: MouseEvent) {
    const parentElement = ref.current?.offsetParent as HTMLElement;
    if (!isDragging() || !ref.current || !parentElement) return;
    setState(DraggingState.moves);
    
    setPoint({
        x: e.x - parentElement.offsetLeft,
        y: e.y - parentElement.offsetTop
    });
    setElementOffset({
        x: e.x - touchOffset.x - parentElement.offsetLeft,
        y: e.y - touchOffset.y - parentElement.offsetTop
    });
}

function onMouseUp(e: MouseEvent) {
    // ends up the flow by setting the state 
    setState(DraggingState.finished);
}


function onClick(e: MouseEvent) {
    // that's a fix for touch pads that transfer touches to click, 
    // e.g "Tap to click" on macos. When enabled, on tap mouseDown is fired,
    // but mouseUp isn't. In this case we invoke mouseUp manually, to trigger 
    // finishing state; 
    setState(DraggingState.finished);
}

// When the element mounts, attach an mousedown listener
useEffect(() => {
    const element = ref.current;
    element?.addEventListener("mousedown", onMouseDown);
    
    return () => {
        element?.removeEventListener("mousedown", onMouseDown);
    };
}, [ref.current]);

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

return {
    ref: ref,
    state: state,
    point: point,
    elementOffset: elementOffset,
    touchOffset: touchOffset
   }
}

還添加了 onClick 處理程序,就像在觸摸板上啟用了點擊選項一樣 onClick 和 mouseDown 在同一時刻發生,但 mouseUp 永遠不會被觸發來關閉手勢。

此外,此掛鈎返回三對坐標 - 元素到其父級的偏移量,元素內的抓取點和元素父級內的點。 詳情見代碼內注釋;

像這樣使用:

const dragging = useDragging();
const ref = dragging.ref;

const style: CSSProperties = {
    marginLeft: dragging.elementOffset.x,
    marginTop: dragging.elementOffset.y,
    border: "1px dashed red"
}

return (<div ref={ref} style={style}>
           {dragging.state === DraggingState.moves ? "is dragging" : "not dragging"}
        </div>)

這是一個使用反應功能的可拖動 div 示例(已測試)

function Draggable() {
  const startX = 300;
  const startY = 200;
  const [pos, setPos] = useState({ left: startX , top: startY });
  const [isDragging, setDragging] = useState(false);
  const isDraggingRef = React.useRef(isDragging);
  const setDraggingState = (data) => {
    isDraggingRef.current = data;
    setDragging(data);
  };

  function onMouseDown(e) {
    setDraggingState(true);
    e.stopPropagation();
    e.preventDefault();
  }

  function onMouseMove(e) {
    if (isDraggingRef.current) {
      const rect = e.target.parentNode.getBoundingClientRect();
      let newLeft = e.pageX - rect.left - 20;
      let newTop = e.pageY - rect.top - 20;

      if (
        newLeft > 0 &&
        newTop > 0 &&
        newLeft < rect.width &&
        newTop < rect.height
      ) {
        setPos({
          left: newLeft,
          top: newTop,
        });
      } else setDraggingState(false);
    }
    e.stopPropagation();
    e.preventDefault();
  }

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

  useEffect(() => {
    document.addEventListener("mousemove", onMouseMove);
    document.addEventListener("mouseup", onMouseUp);
  }, []);

  useEffect(() => {
      console.log(pos)
  }, [pos]);
  return <div style={pos} className="draggableDiv" onMouseDown={onMouseDown}></div>;
}

已經有很多答案了,但我也會拋出我的。 這個答案的優點如下:

  • 現代基於鈎子的解決方案
  • 使用打字稿
  • 動態添加/刪除事件以增加性能優勢
  • 合理的封裝; 即 Draggable 的位置是相對於其直接父級的
  • 在 Draggable 組件中計算對父級的引用。 它不需要傳入。
  • 簡單、直觀的 C​​SS
  • 可拖動位置清晰明確地夾在父級尺寸

 import { CSSProperties, useEffect, useRef, useState, MouseEvent as r_MouseEvent, MutableRefObject, } from 'react'; interface PositionType { x: number, y: number, } interface MinMaxType { min: number, max: number, } interface Props { text: string, position: PositionType isDragging?: boolean, style?: CSSProperties, } const clamp = (num: number, min: number, max: number): number => Math.min(max, Math.max(min, num)); const Draggable = ({ text, position, style = {}, }: Props) => { const [pos, setPos] = useState<PositionType>(); const draggableRef = useRef<HTMLDivElement>(); const [parent, setParent] = useState<HTMLElement | null>(); const [xBounds, setXBounds] = useState<MinMaxType>({ min: 0, max: 0 }); const [yBounds, setYBounds] = useState<MinMaxType>({ min: 0, max: 0 }); useEffect(() => { const parentElement: HTMLDivElement = draggableRef?.current?.parentElement as HTMLDivElement; const parentWidth: number = parentElement?.offsetWidth as number; const parentHeight: number = parentElement?.offsetHeight as number; const parentLeft: number = parentElement?.offsetLeft as number; const parentTop: number = parentElement?.offsetTop as number; const draggableWidth: number = draggableRef?.current?.offsetWidth as number; const draggableHeight: number = draggableRef?.current?.offsetHeight as number; setParent(parentElement); setPos({ x: parentLeft + position.x, y: parentTop + position.y }); setXBounds({ min: parentLeft, max: parentWidth + parentLeft - draggableWidth, }); setYBounds({ min: parentTop, max: parentHeight + parentTop - draggableHeight, }); }, [draggableRef, setParent, setPos, setXBounds, setYBounds, position]); const mouseDownHandler = (e: r_MouseEvent) => { if (e.button !== 0) return // only left mouse button parent?.addEventListener('mousemove', mouseMoveHandler); parent?.addEventListener('mouseup', mouseUpHandler); parent?.addEventListener('mouseleave', mouseUpHandler); e.stopPropagation(); e.preventDefault(); }; const mouseMoveHandler = (e: MouseEvent) => { setPos({ x: clamp(e.pageX, xBounds?.min, xBounds?.max), y: clamp(e.pageY, yBounds?.min, yBounds?.max), }); e.stopPropagation(); e.preventDefault(); }; const mouseUpHandler = (e: MouseEvent) => { parent?.removeEventListener('mousemove', mouseMoveHandler); parent?.removeEventListener('mouseup', mouseUpHandler); e.stopPropagation(); e.preventDefault(); }; const positionStyle = pos && { left: `${pos.x}px`, top: `${pos.y}px` }; const draggableStyle = { ..._styles.draggable, ...positionStyle, ...style } as CSSProperties; return ( <div ref = { draggableRef as MutableRefObject <HTMLDivElement> } style = { draggableStyle } onMouseDown = {mouseDownHandler}> { text } </div> ); } const _styles = { draggable: { position: 'absolute', padding: '2px', border: '1px solid black', borderRadius: '5px', }, }; export default Draggable;

這是使元素可拖動的最簡單的組件。 在此組件中插入任何您想要使其可拖動的元素,它就會起作用。

 import { useEffect, useState } from "react"; const DragAndDrop = ({ children }) => { const [isDragging, setIsDragging] = useState(false); const [xTranslate, setXTranslate] = useState(0); const [yTranslate, setYTranslate] = useState(0); const [initialMousePosition, setInitialMousePosition] = useState({}); const onMouseDown = ({ clientX, clientY }) => { setInitialMousePosition({ x: clientX, y: clientY }); setIsDragging(true); }; useEffect(() => { const onMouseMove = (e) => { setXTranslate(xTranslate + e.clientX - initialMousePosition.x); setYTranslate(yTranslate + e.clientY - initialMousePosition.y); }; if (isDragging) { window.addEventListener("mousemove", onMouseMove); } return () => window.removeEventListener("mousemove", onMouseMove); }, [isDragging, initialMousePosition]); useEffect(() => { const onMouseUp = () => setIsDragging(false); window.addEventListener("mouseup", onMouseUp); return () => window.removeEventListener("mouseup", onMouseUp); }, []); return ( <div style={{ transform: `translate(${xTranslate}px,${yTranslate}px)` }} onMouseDown={onMouseDown} > {" "} {children} </div> ); }; export default DragAndDrop;

例如像這樣:

 //In your main file: <DragAndDrop> <div>I am draggable</div> </DragAndDrop>

不要使用 React Component 和 useEffect Hook 來實現拖拽容器的功能

這是基於 React 類的組件 ES6 版本 -->

 import React from "react"; import $ from 'jquery'; import { useRef } from "react"; class Temp_Class extends React.Component{ constructor(props){ super(props); this.state = { pos: {x:0, y:0}, dragging: false, rel: null }; this.onMouseDown = this.onMouseDown.bind(this); this.onMouseMove = this.onMouseMove.bind(this); this.onMouseUp = this.onMouseUp.bind(this); } componentDidUpdate(props, state){ // console.log("Dragging State is ",this.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) } } onMouseDown(e){ console.log("Mouse Down") if (e.button !== 0) return var pos = document.getElementById("contianer").getBoundingClientRect(); // console.log(pos) this.setState({ dragging: true, rel: { x: e.pageX - pos.left, y: e.pageY - pos.top } }) e.stopPropagation() e.preventDefault() } onMouseUp(e) { console.log("Mouse Up") this.setState({dragging: false}) e.stopPropagation() e.preventDefault() } onMouseMove(e) { console.log("Mouse Move") 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() console.log("Current State is ", this.state) } render(){ return (<div id="contianer" style = {{ position: 'absolute', left: this.state.pos.x + 'px', top: this.state.pos.y + 'px', cursor: 'pointer', width: '200px', height: '200px', backgroundColor: '#cca', }} onMouseDown = {this.onMouseDown}> Lovepreet Singh </div>); } } export default Temp_Class;

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM