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 = ({ config, pixels, index, onConfigChange, onConfigFinish, }) => { const { ledCount } = useLedCount(); const startXRef = useRef(0); const currentXRef = useRef(0); const configRef = useRef(); const [availableConfig, setAvailableConfig] = useState(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) => ( )); }, [pixels, availableConfig]); const [placeholders, placeholderRefs]: [ReactNode[], RefObject[]] = useMemo( () => new Array(ledCount) .fill(undefined) .map((_, i) => { const ref = createRef(); const n = ( ); return [n, ref] as [ReactNode, RefObject]; }) .reduce( ([nList, refList], [n, ref]) => [ [...nList, n], [...refList, ref], ], [[], []] as [ReactNode[], RefObject[]], ), [ledCount], ); // start and moving const handleMouseDown: MouseEventHandler = 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 ( {placeholders} {ledItems}
); };