Skip to content

Commit

Permalink
feat: 🎸 add useScratch() sensor hook
Browse files Browse the repository at this point in the history
streamich committed Jan 12, 2020

Unverified

This user has not yet uploaded their public signing key.
1 parent e232bcc commit 58db2f9
Showing 3 changed files with 188 additions and 0 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -51,6 +51,7 @@
"fast-shallow-equal": "^0.1.1",
"nano-css": "^5.2.1",
"react-fast-compare": "^2.0.4",
"react-universal-interface": "^0.6.0",
"resize-observer-polyfill": "^1.5.1",
"screenfull": "^5.0.0",
"set-harmonic-interval": "^1.0.1",
180 changes: 180 additions & 0 deletions src/useScratch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import { useState, useEffect, useRef, FC, cloneElement } from 'react';
import { render } from 'react-universal-interface';

const noop = () => {};

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 = ({
disabled,
onScratch = noop,
onScratchStart = noop,
onScratchEnd = noop,
}: ScratchSensorParams = {}): [ScratchSensorState, (el: HTMLElement | null) => void] => {
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;
onScratch(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 };
onScratchEnd(refState.current);
setState({ isScratching: false });
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('touchmove', onTouchMove);
window.removeEventListener('mouseup', onMouseUp);
window.removeEventListener('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;
onScratchStart(newState);
setState(newState);
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('touchmove', onTouchMove);
window.addEventListener('mouseup', onMouseUp);
window.addEventListener('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);
};

el.addEventListener('mousedown', onMouseDown);
el.addEventListener('touchstart', onTouchStart);

return () => {
el.removeEventListener('mousedown', onMouseDown);
el.removeEventListener('touchstart', onTouchStart);
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('touchmove', onTouchMove);
window.removeEventListener('mouseup', onMouseUp);
window.removeEventListener('touchend', onTouchEnd);

if (refAnimationFrame.current) cancelAnimationFrame(refAnimationFrame.current);
refAnimationFrame.current = null;

refScratching.current = false;
refState.current = { isScratching: false };
setState(refState.current);
};
}, [el, disabled]);

return [state, setEl];
};

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 [state, ref] = 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;
7 changes: 7 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
@@ -11554,6 +11554,13 @@ react-transition-group@^2.2.1:
prop-types "^15.6.2"
react-lifecycles-compat "^3.0.4"

react-universal-interface@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/react-universal-interface/-/react-universal-interface-0.6.0.tgz#b65cbf7d71a2f3f7dd9705d8e4f06748539bd465"
integrity sha512-PzApKKWfd7gvDi1sU/D07jUqnLvFxYqvJi+GEtLvBO5tXJjKr2Sa8ETVHkMA7Jcvdwt7ttbPq7Sed1JpFdNqBQ==
dependencies:
tslib "^1.9.3"

react@16.12.0:
version "16.12.0"
resolved "https://registry.yarnpkg.com/react/-/react-16.12.0.tgz#0c0a9c6a142429e3614834d5a778e18aa78a0b83"

0 comments on commit 58db2f9

Please sign in to comment.