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 = Operationsexport 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.utilsexport 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.initialshared.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/objectconst 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.Operationsfunction 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;
}
效果截图如下