Skip to content

Commit

Permalink
feat: 🎸 add useScratch() sensor hook
Browse files Browse the repository at this point in the history
  • Loading branch information
streamich committed Jan 12, 2020
1 parent e232bcc commit 58db2f9
Show file tree
Hide file tree
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
Expand Up @@ -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",
Expand Down
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
Expand Up @@ -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"
Expand Down

0 comments on commit 58db2f9

Please sign in to comment.