190 lines
4.8 KiB
TypeScript
190 lines
4.8 KiB
TypeScript
import React, {
|
|
memo, useCallback, useEffect, useMemo, useRef, useState,
|
|
} from '../../lib/teact/teact';
|
|
|
|
import type { FC } from '../../lib/teact/teact';
|
|
|
|
import buildClassName from '../../util/buildClassName';
|
|
|
|
import styles from './Draggable.module.scss';
|
|
import useLang from '../../hooks/useLang';
|
|
import buildStyle from '../../util/buildStyle';
|
|
|
|
type TPoint = {
|
|
x: number;
|
|
y: number;
|
|
};
|
|
|
|
type DraggableState = {
|
|
isDragging: boolean;
|
|
origin: TPoint;
|
|
translation: TPoint;
|
|
width?: number;
|
|
height?: number;
|
|
};
|
|
|
|
type OwnProps = {
|
|
children: React.ReactNode;
|
|
onDrag: (translation: TPoint, id: number) => void;
|
|
onDragEnd: NoneToVoidFunction;
|
|
id: number;
|
|
style?: string;
|
|
knobStyle?: string;
|
|
isDisabled?: boolean;
|
|
};
|
|
|
|
const ZERO_POINT: TPoint = { x: 0, y: 0 };
|
|
|
|
const Draggable: FC<OwnProps> = ({
|
|
children,
|
|
id,
|
|
onDrag,
|
|
onDragEnd,
|
|
style: externalStyle,
|
|
knobStyle,
|
|
isDisabled,
|
|
}) => {
|
|
const lang = useLang();
|
|
// eslint-disable-next-line no-null/no-null
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
|
|
const [state, setState] = useState<DraggableState>({
|
|
isDragging: false,
|
|
origin: ZERO_POINT,
|
|
translation: ZERO_POINT,
|
|
});
|
|
|
|
const handleMouseDown = (e: React.MouseEvent | React.TouchEvent) => {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
const { x, y } = getClientCoordinate(e);
|
|
|
|
setState({
|
|
...state,
|
|
isDragging: true,
|
|
origin: { x, y },
|
|
width: ref.current?.offsetWidth,
|
|
height: ref.current?.offsetHeight,
|
|
});
|
|
};
|
|
|
|
const handleMouseMove = useCallback((e: MouseEvent | TouchEvent) => {
|
|
const { x, y } = getClientCoordinate(e);
|
|
|
|
const translation = {
|
|
x: x - state.origin.x,
|
|
y: y - state.origin.y,
|
|
};
|
|
|
|
setState((current) => ({
|
|
...current,
|
|
translation,
|
|
}));
|
|
|
|
onDrag(translation, id);
|
|
}, [id, onDrag, state.origin.x, state.origin.y]);
|
|
|
|
const handleMouseUp = useCallback(() => {
|
|
requestAnimationFrame(() => {
|
|
setState((current) => ({
|
|
...current,
|
|
isDragging: false,
|
|
width: undefined,
|
|
height: undefined,
|
|
}));
|
|
|
|
onDragEnd();
|
|
});
|
|
}, [onDragEnd]);
|
|
|
|
useEffect(() => {
|
|
if (state.isDragging && isDisabled) {
|
|
setState((current) => ({
|
|
...current,
|
|
isDragging: false,
|
|
width: undefined,
|
|
height: undefined,
|
|
}));
|
|
}
|
|
}, [isDisabled, state.isDragging]);
|
|
|
|
useEffect(() => {
|
|
if (state.isDragging) {
|
|
window.addEventListener('touchmove', handleMouseMove);
|
|
window.addEventListener('mousemove', handleMouseMove);
|
|
window.addEventListener('touchend', handleMouseUp);
|
|
window.addEventListener('touchcancel', handleMouseUp);
|
|
window.addEventListener('mouseup', handleMouseUp);
|
|
} else {
|
|
window.removeEventListener('touchmove', handleMouseMove);
|
|
window.removeEventListener('mousemove', handleMouseMove);
|
|
window.removeEventListener('touchend', handleMouseUp);
|
|
window.removeEventListener('touchcancel', handleMouseUp);
|
|
window.removeEventListener('mouseup', handleMouseUp);
|
|
|
|
setState((current) => ({
|
|
...current,
|
|
translation: ZERO_POINT,
|
|
}));
|
|
}
|
|
|
|
return () => {
|
|
if (state.isDragging) {
|
|
window.removeEventListener('touchmove', handleMouseMove);
|
|
window.removeEventListener('mousemove', handleMouseMove);
|
|
window.removeEventListener('touchend', handleMouseUp);
|
|
window.removeEventListener('touchcancel', handleMouseUp);
|
|
window.removeEventListener('mouseup', handleMouseUp);
|
|
}
|
|
};
|
|
}, [handleMouseMove, handleMouseUp, state.isDragging]);
|
|
|
|
const fullClassName = buildClassName(styles.container, state.isDragging && styles.isDragging);
|
|
|
|
const cssStyles = useMemo(() => {
|
|
return buildStyle(
|
|
`transform: translate(${state.translation.x}px, ${state.translation.y}px)`,
|
|
state.width ? `width: ${state.width}px` : undefined,
|
|
state.height ? `height: ${state.height}px` : undefined,
|
|
externalStyle,
|
|
);
|
|
}, [externalStyle, state.height, state.translation.x, state.translation.y, state.width]);
|
|
|
|
return (
|
|
<div style={cssStyles} className={fullClassName} ref={ref}>
|
|
{children}
|
|
|
|
{!isDisabled && (
|
|
<div
|
|
aria-label={lang('i18n_dragToSort')}
|
|
tabIndex={0}
|
|
role="button"
|
|
className={buildClassName(styles.knob, 'draggable-knob')}
|
|
onMouseDown={handleMouseDown}
|
|
onTouchStart={handleMouseDown}
|
|
style={knobStyle}
|
|
>
|
|
<i className="icon-sort" aria-hidden />
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default memo(Draggable);
|
|
|
|
function getClientCoordinate(e: MouseEvent | TouchEvent | React.MouseEvent | React.TouchEvent) {
|
|
let x;
|
|
let y;
|
|
|
|
if ('touches' in e) {
|
|
x = e.touches[0].clientX;
|
|
y = e.touches[0].clientY;
|
|
} else {
|
|
x = e.clientX;
|
|
y = e.clientY;
|
|
}
|
|
|
|
return { x, y };
|
|
}
|