タグ D&D 時の表示位置修正 (#268) #400

マージ済み
みてるぞ が 8 個のコミットを feature/268 から main へマージ 2026-07-08 00:01:08 +09:00
2個のファイルの変更67行の追加95行の削除
コミット 33f9c6602f の変更だけを表示してゐます - すべてのコミットを表示
+6 -4
ファイルの表示
@@ -58,15 +58,14 @@ const DraggableDroppableTagRow: FC<Props> = ({ tag, nestLevel, pathKey, parentTa
data: { kind: 'tag',
tagId: tag.id,
parentTagId,
nestLevel,
dndId } })
nestLevel } })
const { setNodeRef: setDropRef, isOver: over } = useDroppable ({
id: dndId,
data: { kind: 'tag', tagId: tag.id } })
const style: CSSProperties = { transform: CSS.Translate.toString (transform),
visibility: dragging ? 'hidden' : 'visible' }
visibility: dragging && !(sp) ? 'hidden' : 'visible' }
return (
<div
@@ -79,12 +78,16 @@ const DraggableDroppableTagRow: FC<Props> = ({ tag, nestLevel, pathKey, parentTa
return
const dx = e.clientX - p.x
const dy = e.clientY - p.y
if (dx * dx + dy * dy >= 9)
armEatNextClick ()
}}
onPointerUpCapture={() => {
downPosRef.current = null
}}
onPointerCancelCapture={() => {
downPosRef.current = null
}}
onClickCapture={e => {
if (suppressClickRef.current)
{
@@ -100,7 +103,6 @@ const DraggableDroppableTagRow: FC<Props> = ({ tag, nestLevel, pathKey, parentTa
>
<motion.div
ref={setDragRef}
data-tag-dnd-id={dndId}
style={style}
className={cn (
'inline-flex min-w-0 max-w-full items-baseline overflow-hidden',
+49 -79
ファイルの表示
@@ -1,8 +1,8 @@
import { DndContext,
DragOverlay,
MeasuringStrategy,
MouseSensor,
TouchSensor,
type Modifier,
pointerWithin,
rectIntersection,
useDroppable,
@@ -11,7 +11,7 @@ import { DndContext,
import { restrictToWindowEdges } from '@dnd-kit/modifiers'
import { useQueryClient } from '@tanstack/react-query'
import { motion } from 'framer-motion'
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import DraggableDroppableTagRow from '@/components/DraggableDroppableTagRow'
import PrefetchLink from '@/components/PrefetchLink'
@@ -28,13 +28,21 @@ import { postsKeys, tagsKeys } from '@/lib/queryKeys'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { dateString, originalCreatedAtString } from '@/lib/utils'
import type { DragEndEvent } from '@dnd-kit/core'
import type { CollisionDetection, DragEndEvent } from '@dnd-kit/core'
import type { FC, MutableRefObject, ReactNode } from 'react'
import type { Category, Post, TagWithSections } from '@/types'
type TagByCategory = { [key in Category]: TagWithSections[] }
const tagCollisionDetection: CollisionDetection = args => {
const byPointer = pointerWithin (args)
return byPointer.length > 0 ? byPointer : rectIntersection (args)
}
const alwaysMeasureDroppables = {
droppable: { strategy: MeasuringStrategy.Always } } as const
const renderTagTree = (
tag: TagWithSections,
@@ -180,6 +188,24 @@ const DropSlot = ({ cat }: { cat: Category }) => {
</li>)
}
const EmptyCategoryDropSection = (
{ cat, children }: { cat: Category
children: ReactNode }) => {
const { setNodeRef, isOver: over } = useDroppable ({
id: `slot:${ cat }`,
data: { kind: 'slot', cat } })
return (
<div ref={setNodeRef} className="my-3">
{children}
<ul>
<li className="h-1">
{over && <div className="h-0.5 w-full rounded bg-sky-400"/>}
</li>
</ul>
</div>)
}
type Props = {
className?: string
@@ -188,12 +214,7 @@ type Props = {
type ActiveTagDrag = {
tagId: number
nestLevel: number
dndId: string }
type DragOverlayOffset = {
x: number
y: number }
nestLevel: number }
const TagDetailSidebar: FC<Props> = ({ className, post, sp }) => {
@@ -228,59 +249,16 @@ const TagDetailSidebar: FC<Props> = ({ className, post, sp }) => {
const [dragging, setDragging] = useState (false)
const [saving, setSaving] = useState (false)
const [tags, setTags] = useState (baseTags)
const [dragOverlayOffset, setDragOverlayOffset] =
useState<DragOverlayOffset> ({ x: 0, y: 0 })
const flatTagsByCategory = useMemo<TagByCategory> (
() => buildFlatTagByCategory (tags),
[tags],
)
const suppressClickRef = useRef (false)
const dragStartRectRef = useRef<DOMRect | null> (null)
const offsetFrameRef = useRef<number | null> (null)
const sensors = useSensors (
useSensor (MouseSensor, { activationConstraint: { distance: 6 } }),
useSensor (TouchSensor, { activationConstraint: { delay: 250, tolerance: 8 } }))
const dragOverlayModifiers = useMemo<Modifier[]> (
() => (
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<HTMLElement> (
`[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<void> => {
setTags (buildTagByCategory (await apiGet<Post> (`/posts/${ post.id }`)))
@@ -383,28 +361,17 @@ const TagDetailSidebar: FC<Props> = ({ className, post, sp }) => {
<TagSearch/>
<DndContext
sensors={sensors}
collisionDetection={args => {
const byPointer = pointerWithin (args)
return byPointer.length > 0 ? byPointer : rectIntersection (args)
}}
collisionDetection={sp ? tagCollisionDetection : undefined}
measuring={sp ? alwaysMeasureDroppables : undefined}
onDragStart={e => {
if (e.active.data.current?.kind === 'tag')
{
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 || !(dndId)
tagId == null || nestLevel == null
? null
: { tagId, nestLevel, dndId })
: { tagId, nestLevel })
}
setDragging (true)
suppressClickRef.current = true
@@ -418,16 +385,12 @@ const TagDetailSidebar: FC<Props> = ({ 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 = ''
}}
@@ -441,8 +404,7 @@ const TagDetailSidebar: FC<Props> = ({ className, post, sp }) => {
if (!(categoryTags.length > 0 || dragging))
return null
return (
<div className="my-3" key={cat}>
const sectionTitle = (
<SubsectionTitle>
<motion.div
layoutId={animationMode === 'off'
@@ -451,8 +413,17 @@ const TagDetailSidebar: FC<Props> = ({ className, post, sp }) => {
transition={{ layout: layoutTransition }}>
{CATEGORY_NAMES[cat]}
</motion.div>
</SubsectionTitle>
</SubsectionTitle>)
if (!(categoryTags.length > 0))
return (
<EmptyCategoryDropSection cat={cat} key={cat}>
{sectionTitle}
</EmptyCategoryDropSection>)
return (
<div className="my-3" key={cat}>
{sectionTitle}
<ul>
{(tagRelationDisplay === 'grouped'
? (tags[cat] ?? []).flatMap (tag =>
@@ -520,16 +491,15 @@ const TagDetailSidebar: FC<Props> = ({ className, post, sp }) => {
</ul>
</motion.div>)}
<DragOverlay adjustScale={false} modifiers={dragOverlayModifiers}>
{!(sp) && (
<DragOverlay adjustScale={false}>
<div className="pointer-events-none">
{activeTagDrag != null && (() => {
const tag = findTag (tags, activeTagDrag.tagId)
return (
tag
&& <TagLink tag={tag} nestLevel={activeTagDrag.nestLevel}/>)
return tag && <TagLink tag={tag} nestLevel={activeTagDrag.nestLevel}/>
}) ()}
</div>
</DragOverlay>
</DragOverlay>)}
</DndContext>
</SidebarComponent>)
}