react 实现拖动元素

demo使用create-react-app脚手架创建
删除一些文件,创建一些文件后
结构目录如下截图

在这里插入图片描述

com/index
import Movable from './move'
import { useMove } from './move.hook'
import * as Operations from './move.op'


Movable.useMove = useMove
Movable.Operations = Operations

export default Movable
com/move
import React, {forwardRef, memo} from "react"
import { noop } from "../utils/noop"
import {mouseTracker, touchTracker, moveTracker} from './move.utils';
// forwardRef 将允许组件使用ref,将dom暴露给父组件; 返回一个可以接受ref属性的组件
export const Move = forwardRef(({onBeginMove, onMove, onEndMove, ...props}, ref) =>  {
  const tracker = moveTracker(onBeginMove, onMove, onEndMove);
  const handleOnMouseDown = mouseTracker(tracker);
  return <div 
    {...props} 
    ref={ref}
    onMouseDown={handleOnMouseDown}
    className={`movable ${props.className}`}
    />
})

export default memo(Move)
com/move.utils

export const moveTracker = (onBeginMove, onMove, onEndMove) => {
  let initial = {};
  let previous = {};

  const event = e => ({
      ...e,
      cx: e.x - previous.x,
      cy: e.y - previous.y,
      dx: e.x - initial.x,
      dy: e.y - initial.y,
  });

  return {
      start: e => {
          initial = {x: e.x, y: e.y};
          previous = {...initial};
          onBeginMove(event(e));
      },
      move: e => {
          onMove(event(e));
          previous = {x: e.x, y: e.y};
      },
      end: e => {
          onEndMove(event(e));
      },
  }
};

export const mouseTracker = tracker => {

  const event = e => ({
      x: e.clientX,
      y: e.clientY,
      target: e.target,
      stopPropagation: () => e.stopPropagation(),
      preventDefault: () => e.preventDefault(),
  });

  const onMouseDown = e => {
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('mouseup', onMouseUp);
      tracker.start(event(e));
  };

  const onMouseMove = e => {
      tracker.move(event(e));
  };

  const onMouseUp = e => {
      document.removeEventListener('mousemove', onMouseMove);
      document.removeEventListener('mouseup', onMouseUp);
      tracker.end(event(e));
  };

  return onMouseDown;
};


com/move.hook
import { useRef, useCallback } from "react";

export const useMove = ops => {
  const shared = useRef({})

  const onBeginMove = useCallback(e => {
    ops.forEach(({onBeginMove}) => onBeginMove(e, shared.current));
  }, [ops])

  const onMove = useCallback(e => {
    ops.forEach(({onMove}) => onMove(e, shared.current));
  }, [ops])

  const onEndMove = useCallback(e => {
    ops.forEach(({onEndMove}) => onEndMove(e, shared.current));
  }, [ops])

  return {onBeginMove, onMove, onEndMove}
}
com/move.op
import { clamp } from "../utils/number";
import { noop } from "../utils/noop";
import { isEqual } from "../utils/object";

export const createOp = handlers => ({
  onBeginMove: noop,
  onMove: noop,
  onEndMove: noop,
  ...handlers
})


export const move = m => createOp({
  onBeginMove: (e, shared) => {
    // getBoundingClientRect返回一个 DOMRect 对象,其提供了元素的大小及其相对于视口的位置。
    const { top, left } = m.current.getBoundingClientRect()
    shared.next = {top, left}
    shared.initial = {top, left}
  },
  onMove: ({dx, dy}, shared) => {
    const {left, top} = shared.initial
    shared.next = {
      left: left + dx,
      top: top + dy
    }
  }
})

export const update = onUpdate => createOp({
  onBeginMove: _update(onUpdate),
  onMove: _update(onUpdate),
  onEndMove: _update(onUpdate),
});
const _update = onUpdate => (e, shared) => {
  if (!isEqual(shared.prev, shared.next)) {
      onUpdate(shared.next);
      shared.prev = shared.next;
  }
};
utils/number
export const clamp = (num, min, max) => {
  return Math.min(Math.max(num, min), max)
}
================================
utils/noop
export const noop = () => null;
================================
utils/object

const Types = {
  NUMBER: 'number',
  OBJECT: 'object',
  NULL: 'null',
  ARRAY: 'array',
  UNDEFINED: 'undefined',
  BOOLEAN: 'boolean',
  STRING: 'string',
  DATE: 'date',
};
const getType = v => Object.prototype.toString.call(v).slice(8, -1).toLowerCase();
const isType = (v, ...types) => types.includes(getType(v));
const isObject = v => isType(v, Types.OBJECT);
const isArray = v => isType(v, Types.ARRAY);

export const EqualityIterators = {
  SHALLOW: (a, b) => a === b,
  DEEP: (a, b, visited = []) => {
      if (visited.includes(a)) {
          return true;
      }
      if (a instanceof Object) {
          visited.push(a);
      }
      return isEqual(a, b, (a, b) => EqualityIterators.DEEP(a, b, visited))
  },
};

export const isEqual = (a, b, iterator = EqualityIterators.DEEP) => {
  if (a === b) {
      return true;
  }

  if (getType(a) !== getType(b)) {
      return false;
  }

  if (isObject(a) && Object.keys(a).length === Object.keys(b).length) {
      return Object.keys(a).every(key => iterator(a[key], b[key]));
  }

  if (isArray(a) && a.length === b.length) {
      return a.every((item, i) => iterator(a[i], b[i]))
  }

  return false;
};
App.js
import { useMemo, useRef, useState } from "react";
import Movable from "./com";

const {move, update} = Movable.Operations

function App() {
  const ref = useRef()
  const ref2 = useRef()
  const [p, setP] = useState({})
  const [p2, setP2] = useState({})
  const props = Movable.useMove(useMemo(() => [
    move(ref),
    update(setP)
  ], []))
  const props2 = Movable.useMove(useMemo(() => [
    move(ref2),
    update(setP2)
  ], []))
  return (
    <>
    <Movable {...props} ref={ref} style={p}>
          拖我
    </Movable>
    <Movable {...props2} ref={ref2} style={p2}>
          拖我2
    </Movable>
    </>
  );
}

export default App;

src/index
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

src/index.css
.movable {
  user-select: none;
  width: 100px;
  height: 100px;
  cursor: move;
  position: absolute;
  padding: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  background-color: palegreen;
} 

效果截图如下
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值