From 2d6af0aa5b67fe674e434aa52a982ce091b22218 Mon Sep 17 00:00:00 2001 From: miteruzo Date: Tue, 7 Jul 2026 21:01:57 +0900 Subject: [PATCH] #268 --- .../components/DraggableDroppableTagRow.tsx | 4 +- frontend/src/components/TagDetailSidebar.tsx | 72 +++++++++++++++++-- 2 files changed, 70 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/DraggableDroppableTagRow.tsx b/frontend/src/components/DraggableDroppableTagRow.tsx index 0e24398..f1ecf20 100644 --- a/frontend/src/components/DraggableDroppableTagRow.tsx +++ b/frontend/src/components/DraggableDroppableTagRow.tsx @@ -58,7 +58,8 @@ const DraggableDroppableTagRow: FC = ({ tag, nestLevel, pathKey, parentTa data: { kind: 'tag', tagId: tag.id, parentTagId, - nestLevel } }) + nestLevel, + dndId } }) const { setNodeRef: setDropRef, isOver: over } = useDroppable ({ id: dndId, @@ -99,6 +100,7 @@ const DraggableDroppableTagRow: FC = ({ tag, nestLevel, pathKey, parentTa > = ({ className, post, sp }) => { @@ -222,16 +228,59 @@ const TagDetailSidebar: FC = ({ className, post, sp }) => { const [dragging, setDragging] = useState (false) const [saving, setSaving] = useState (false) const [tags, setTags] = useState (baseTags) + const [dragOverlayOffset, setDragOverlayOffset] = + useState ({ x: 0, y: 0 }) const flatTagsByCategory = useMemo ( () => buildFlatTagByCategory (tags), [tags], ) const suppressClickRef = useRef (false) + const dragStartRectRef = useRef (null) + const offsetFrameRef = useRef (null) const sensors = useSensors ( useSensor (MouseSensor, { activationConstraint: { distance: 6 } }), useSensor (TouchSensor, { activationConstraint: { delay: 250, tolerance: 8 } })) + const dragOverlayModifiers = useMemo ( + () => ( + sp + ? [({ transform }) => ({ + ...transform, + x: transform.x + dragOverlayOffset.x, + y: transform.y + dragOverlayOffset.y })] + : []), + [dragOverlayOffset.x, dragOverlayOffset.y, sp], + ) + + useLayoutEffect (() => { + if (!(sp && dragging && activeTagDrag && dragStartRectRef.current)) + { + setDragOverlayOffset ({ x: 0, y: 0 }) + return + } + + offsetFrameRef.current = requestAnimationFrame (() => { + const node = document.querySelector ( + `[data-tag-dnd-id="${ activeTagDrag.dndId }"]`, + ) + + if (!(node && dragStartRectRef.current)) + return + + const currentRect = node.getBoundingClientRect () + const initialRect = dragStartRectRef.current + + setDragOverlayOffset ({ + x: currentRect.left - initialRect.left, + y: currentRect.top - initialRect.top }) + }) + + return () => { + if (offsetFrameRef.current != null) + cancelAnimationFrame (offsetFrameRef.current) + } + }, [activeTagDrag, dragging, sp]) const reloadTags = async (): Promise => { setTags (buildTagByCategory (await apiGet (`/posts/${ post.id }`))) @@ -343,10 +392,19 @@ const TagDetailSidebar: FC = ({ className, post, sp }) => { { const tagId = e.active.data.current?.tagId const nestLevel = e.active.data.current?.nestLevel + const dndId = e.active.data.current?.dndId + dragStartRectRef.current = + e.active.rect.current.initial + ? new DOMRect ( + e.active.rect.current.initial.left, + e.active.rect.current.initial.top, + e.active.rect.current.initial.width, + e.active.rect.current.initial.height) + : null setActiveTagDrag ( - tagId == null || nestLevel == null + tagId == null || nestLevel == null || !(dndId) ? null - : { tagId, nestLevel }) + : { tagId, nestLevel, dndId }) } setDragging (true) suppressClickRef.current = true @@ -360,12 +418,16 @@ const TagDetailSidebar: FC = ({ className, post, sp }) => { onDragCancel={() => { setActiveTagDrag (null) setDragging (false) + setDragOverlayOffset ({ x: 0, y: 0 }) + dragStartRectRef.current = null document.body.style.userSelect = '' suppressClickRef.current = false }} onDragEnd={async e => { setActiveTagDrag (null) setDragging (false) + setDragOverlayOffset ({ x: 0, y: 0 }) + dragStartRectRef.current = null await onDragEnd (e) document.body.style.userSelect = '' }} @@ -458,7 +520,7 @@ const TagDetailSidebar: FC = ({ className, post, sp }) => { )} - +
{activeTagDrag != null && (() => { const tag = findTag (tags, activeTagDrag.tagId)