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;
}
效果截图如下