mirror of
https://github.com/streamich/react-use.git
synced 2026-01-18 14:06:52 +00:00
Also reduce max line width to 100. And remove `lint:types` step for commit sequence, it bothers when committing incomplete (wip) changes.
183 lines
5.3 KiB
TypeScript
183 lines
5.3 KiB
TypeScript
import { cloneElement, FC, useEffect, useRef, useState } from 'react';
|
|
import { render } from 'react-universal-interface';
|
|
import useLatest from './useLatest';
|
|
import { noop, off, on } from './misc/util';
|
|
|
|
export interface ScratchSensorParams {
|
|
disabled?: boolean;
|
|
onScratch?: (state: ScratchSensorState) => void;
|
|
onScratchStart?: (state: ScratchSensorState) => void;
|
|
onScratchEnd?: (state: ScratchSensorState) => void;
|
|
}
|
|
|
|
export interface ScratchSensorState {
|
|
isScratching: boolean;
|
|
start?: number;
|
|
end?: number;
|
|
x?: number;
|
|
y?: number;
|
|
dx?: number;
|
|
dy?: number;
|
|
docX?: number;
|
|
docY?: number;
|
|
posX?: number;
|
|
posY?: number;
|
|
elH?: number;
|
|
elW?: number;
|
|
elX?: number;
|
|
elY?: number;
|
|
}
|
|
|
|
const useScratch = (
|
|
params: ScratchSensorParams = {}
|
|
): [(el: HTMLElement | null) => void, ScratchSensorState] => {
|
|
const { disabled } = params;
|
|
const paramsRef = useLatest(params);
|
|
const [state, setState] = useState<ScratchSensorState>({ isScratching: false });
|
|
const refState = useRef<ScratchSensorState>(state);
|
|
const refScratching = useRef<boolean>(false);
|
|
const refAnimationFrame = useRef<any>(null);
|
|
const [el, setEl] = useState<HTMLElement | null>(null);
|
|
useEffect(() => {
|
|
if (disabled) return;
|
|
if (!el) return;
|
|
|
|
const onMoveEvent = (docX, docY) => {
|
|
cancelAnimationFrame(refAnimationFrame.current);
|
|
refAnimationFrame.current = requestAnimationFrame(() => {
|
|
const { left, top } = el.getBoundingClientRect();
|
|
const elX = left + window.scrollX;
|
|
const elY = top + window.scrollY;
|
|
const x = docX - elX;
|
|
const y = docY - elY;
|
|
setState((oldState) => {
|
|
const newState = {
|
|
...oldState,
|
|
dx: x - (oldState.x || 0),
|
|
dy: y - (oldState.y || 0),
|
|
end: Date.now(),
|
|
isScratching: true,
|
|
};
|
|
refState.current = newState;
|
|
(paramsRef.current.onScratch || noop)(newState);
|
|
return newState;
|
|
});
|
|
});
|
|
};
|
|
|
|
const onMouseMove = (event) => {
|
|
onMoveEvent(event.pageX, event.pageY);
|
|
};
|
|
|
|
const onTouchMove = (event) => {
|
|
onMoveEvent(event.changedTouches[0].pageX, event.changedTouches[0].pageY);
|
|
};
|
|
|
|
let onMouseUp;
|
|
let onTouchEnd;
|
|
|
|
const stopScratching = () => {
|
|
if (!refScratching.current) return;
|
|
refScratching.current = false;
|
|
refState.current = { ...refState.current, isScratching: false };
|
|
(paramsRef.current.onScratchEnd || noop)(refState.current);
|
|
setState({ isScratching: false });
|
|
off(window, 'mousemove', onMouseMove);
|
|
off(window, 'touchmove', onTouchMove);
|
|
off(window, 'mouseup', onMouseUp);
|
|
off(window, 'touchend', onTouchEnd);
|
|
};
|
|
|
|
onMouseUp = stopScratching;
|
|
onTouchEnd = stopScratching;
|
|
|
|
const startScratching = (docX, docY) => {
|
|
if (!refScratching.current) return;
|
|
const { left, top } = el.getBoundingClientRect();
|
|
const elX = left + window.scrollX;
|
|
const elY = top + window.scrollY;
|
|
const x = docX - elX;
|
|
const y = docY - elY;
|
|
const time = Date.now();
|
|
const newState = {
|
|
isScratching: true,
|
|
start: time,
|
|
end: time,
|
|
docX,
|
|
docY,
|
|
x,
|
|
y,
|
|
dx: 0,
|
|
dy: 0,
|
|
elH: el.offsetHeight,
|
|
elW: el.offsetWidth,
|
|
elX,
|
|
elY,
|
|
};
|
|
refState.current = newState;
|
|
(paramsRef.current.onScratchStart || noop)(newState);
|
|
setState(newState);
|
|
on(window, 'mousemove', onMouseMove);
|
|
on(window, 'touchmove', onTouchMove);
|
|
on(window, 'mouseup', onMouseUp);
|
|
on(window, 'touchend', onTouchEnd);
|
|
};
|
|
|
|
const onMouseDown = (event) => {
|
|
refScratching.current = true;
|
|
startScratching(event.pageX, event.pageY);
|
|
};
|
|
|
|
const onTouchStart = (event) => {
|
|
refScratching.current = true;
|
|
startScratching(event.changedTouches[0].pageX, event.changedTouches[0].pageY);
|
|
};
|
|
|
|
on(el, 'mousedown', onMouseDown);
|
|
on(el, 'touchstart', onTouchStart);
|
|
|
|
return () => {
|
|
off(el, 'mousedown', onMouseDown);
|
|
off(el, 'touchstart', onTouchStart);
|
|
off(window, 'mousemove', onMouseMove);
|
|
off(window, 'touchmove', onTouchMove);
|
|
off(window, 'mouseup', onMouseUp);
|
|
off(window, 'touchend', onTouchEnd);
|
|
|
|
if (refAnimationFrame.current) cancelAnimationFrame(refAnimationFrame.current);
|
|
refAnimationFrame.current = null;
|
|
|
|
refScratching.current = false;
|
|
refState.current = { isScratching: false };
|
|
setState(refState.current);
|
|
};
|
|
}, [el, disabled, paramsRef]);
|
|
|
|
return [setEl, state];
|
|
};
|
|
|
|
export interface ScratchSensorProps extends ScratchSensorParams {
|
|
children: (
|
|
state: ScratchSensorState,
|
|
ref: (el: HTMLElement | null) => void
|
|
) => React.ReactElement<any>;
|
|
}
|
|
|
|
export const ScratchSensor: FC<ScratchSensorProps> = (props) => {
|
|
const { children, ...params } = props;
|
|
const [ref, state] = useScratch(params);
|
|
const element = render(props, state);
|
|
return cloneElement(element, {
|
|
...element.props,
|
|
ref: (el) => {
|
|
if (element.props.ref) {
|
|
if (typeof element.props.ref === 'object') element.props.ref.current = el;
|
|
if (typeof element.props.ref === 'function') element.props.ref(el);
|
|
}
|
|
ref(el);
|
|
},
|
|
});
|
|
};
|
|
|
|
export default useScratch;
|