desktop/src/configurator/components/draggable-strip.tsx
2023-01-19 23:54:10 +08:00

200 lines
5.9 KiB
TypeScript

import {
createRef,
FC,
Fragment,
MouseEventHandler,
ReactNode,
RefObject,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { css } from 'twin.macro';
import { useLedCount } from '../contents/led-count';
import { LedStripConfig } from '../models/led-strip-config';
import { PixelRgb } from '../models/pixel-rgb';
import { StyledPixel } from './styled-pixel';
import { logger } from './completed-led-strip';
interface DraggableStripProp {
config: LedStripConfig;
pixels: PixelRgb[];
index: number;
onConfigChange?: (config: LedStripConfig) => void;
onConfigFinish?: (config: LedStripConfig) => void;
}
export const DraggableStrip: FC<DraggableStripProp> = ({
config,
pixels,
index,
onConfigChange,
onConfigFinish,
}) => {
const { ledCount } = useLedCount();
const startXRef = useRef(0);
const currentXRef = useRef(0);
const configRef = useRef<LedStripConfig>();
const [availableConfig, setAvailableConfig] = useState<LedStripConfig>(config);
// const currentDiffRef = useRef(0);
const isDragRef = useRef(false);
const handleMouseMoveRef = useRef<(ev: MouseEvent) => void>();
const [boxTranslateX, setBoxTranslateX] = useState(0);
const ledItems = useMemo(() => {
const step = config.global_start_position - config.global_end_position < 0 ? 1 : -1;
return pixels.map((rgb, i) => (
<StyledPixel
key={i}
rgb={rgb}
css={css`
grid-column: ${(availableConfig.global_start_position ?? 0) + i * step + 1} /
span 1;
grid-row-start: ${index + 1};
pointer-events: none;
`}
/>
));
}, [pixels, availableConfig]);
const [placeholders, placeholderRefs]: [ReactNode[], RefObject<HTMLSpanElement>[]] =
useMemo(
() =>
new Array(ledCount)
.fill(undefined)
.map((_, i) => {
const ref = createRef<HTMLSpanElement>();
const n = (
<span
ref={ref}
key={i}
tw=" h-full w-full"
css={css`
grid-column-start: ${i + 1};
grid-row-start: ${index + 1};
`}
/>
);
return [n, ref] as [ReactNode, RefObject<HTMLSpanElement>];
})
.reduce(
([nList, refList], [n, ref]) => [
[...nList, n],
[...refList, ref],
],
[[], []] as [ReactNode[], RefObject<HTMLSpanElement>[]],
),
[ledCount],
);
// start and moving
const handleMouseDown: MouseEventHandler<HTMLDivElement> = useCallback(
(ev) => {
startXRef.current = ev.pageX;
ev.currentTarget.requestPointerLock();
isDragRef.current = true;
logger('handleMouseDown, config: %o', config);
const placeholderPositions = placeholderRefs.map((it) => {
if (!it.current) {
return [0, 0];
}
const viewportOffset = it.current.getBoundingClientRect();
return [viewportOffset.left, viewportOffset.right] as [number, number];
});
logger('placeholderPositions: %o', placeholderPositions);
// set init position
const initPos = placeholderPositions.findIndex(
([l, r]) => l <= ev.pageX && r >= ev.pageX,
);
let prevMatch = 0;
if (handleMouseMoveRef.current) {
document.body.removeEventListener('mousemove', handleMouseMoveRef.current);
}
handleMouseMoveRef.current = (ev) => {
if (!isDragRef.current) {
return;
}
currentXRef.current = ev.pageX;
setBoxTranslateX(currentXRef.current - startXRef.current);
const match = placeholderPositions.findIndex(
([l, r]) => l <= currentXRef.current && r >= currentXRef.current,
);
if (match === -1) {
return;
}
if (match === prevMatch) {
return;
}
prevMatch = match;
const diff = match - initPos;
const newValue: LedStripConfig = {
...config,
global_start_position: config.global_start_position + diff,
global_end_position: config.global_end_position + diff,
};
configRef.current = newValue;
setAvailableConfig(newValue);
logger('change config. old: %o, new: %o', config, newValue);
onConfigChange?.(newValue);
};
document.body.addEventListener('mousemove', handleMouseMoveRef.current);
},
[placeholderRefs, availableConfig, setAvailableConfig, config],
);
// move event.
useEffect(() => {
const handleMouseUp = (ev: MouseEvent) => {
if (configRef.current && isDragRef.current) {
onConfigFinish?.(configRef.current);
}
startXRef.current = 0;
isDragRef.current = false;
document.exitPointerLock();
if (handleMouseMoveRef.current) {
document.body.removeEventListener('mousemove', handleMouseMoveRef.current);
}
};
document.body.addEventListener('mouseup', handleMouseUp);
return () => {
document.body.removeEventListener('mouseup', handleMouseUp);
};
}, [onConfigFinish]);
// reset translateX when config updated.
useEffect(() => {
startXRef.current = currentXRef.current;
setAvailableConfig(config);
setBoxTranslateX(0);
logger('useEffect, config: %o', config);
}, [config]);
return (
<Fragment>
{placeholders}
{ledItems}
<div
tw="border border-gray-700 h-3 w-full rounded-full"
css={css`
grid-column: ${Math.min(
config?.global_start_position ?? 0,
config?.global_end_position ?? 0,
) + 1} / span
${Math.abs(config?.global_start_position - config?.global_end_position) + 1};
grid-row-start: ${index + 1};
cursor: ew-resize;
transform: translateX(${boxTranslateX}px);
`}
onMouseDown={handleMouseDown}
></div>
</Fragment>
);
};