feat: can drag avatar to move the comment input

This commit is contained in:
hjlarry 2025-11-18 09:53:15 +08:00
parent 3ee2508ec8
commit 8497d296b1
2 changed files with 99 additions and 4 deletions

View File

@ -1,5 +1,5 @@
import type { FC } from 'react'
import { memo, useCallback, useEffect, useState } from 'react'
import type { FC, PointerEvent as ReactPointerEvent } from 'react'
import { memo, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Avatar from '@/app/components/base/avatar'
import { useAppContext } from '@/context/app-context'
@ -10,12 +10,36 @@ type CommentInputProps = {
position: { x: number; y: number }
onSubmit: (content: string, mentionedUserIds: string[]) => void
onCancel: () => void
onPositionChange?: (position: {
pageX: number
pageY: number
elementX: number
elementY: number
}) => void
}
export const CommentInput: FC<CommentInputProps> = memo(({ position, onSubmit, onCancel }) => {
export const CommentInput: FC<CommentInputProps> = memo(({ position, onSubmit, onCancel, onPositionChange }) => {
const [content, setContent] = useState('')
const { t } = useTranslation()
const { userProfile } = useAppContext()
const dragStateRef = useRef<{
pointerId: number | null
startPointerX: number
startPointerY: number
startX: number
startY: number
active: boolean
} & {
endHandler?: (event: PointerEvent) => void
}>({
pointerId: null,
startPointerX: 0,
startPointerY: 0,
startX: 0,
startY: 0,
active: false,
endHandler: undefined,
})
useEffect(() => {
const handleGlobalKeyDown = (e: KeyboardEvent) => {
@ -37,6 +61,67 @@ export const CommentInput: FC<CommentInputProps> = memo(({ position, onSubmit, o
setContent('')
}, [onSubmit])
const handleDragPointerMove = useCallback((event: PointerEvent) => {
const state = dragStateRef.current
if (!state.active || (state.pointerId !== null && event.pointerId !== state.pointerId))
return
if (!onPositionChange)
return
event.preventDefault()
const deltaX = event.clientX - state.startPointerX
const deltaY = event.clientY - state.startPointerY
onPositionChange({
pageX: event.clientX,
pageY: event.clientY,
elementX: state.startX + deltaX,
elementY: state.startY + deltaY,
})
}, [onPositionChange])
const stopDragging = useCallback((event?: PointerEvent) => {
const state = dragStateRef.current
if (!state.active)
return
if (event && state.pointerId !== null && event.pointerId !== state.pointerId)
return
state.active = false
state.pointerId = null
window.removeEventListener('pointermove', handleDragPointerMove)
if (state.endHandler) {
window.removeEventListener('pointerup', state.endHandler)
window.removeEventListener('pointercancel', state.endHandler)
state.endHandler = undefined
}
}, [handleDragPointerMove])
const handleDragPointerDown = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
if (event.button !== 0)
return
event.stopPropagation()
event.preventDefault()
if (!onPositionChange)
return
const endHandler = (pointerEvent: PointerEvent) => {
stopDragging(pointerEvent)
}
dragStateRef.current = {
pointerId: event.pointerId,
startPointerX: event.clientX,
startPointerY: event.clientY,
startX: position.x,
startY: position.y,
active: true,
endHandler,
}
window.addEventListener('pointermove', handleDragPointerMove, { passive: false })
window.addEventListener('pointerup', endHandler)
window.addEventListener('pointercancel', endHandler)
}, [handleDragPointerMove, onPositionChange, position.x, position.y, stopDragging])
useEffect(() => () => {
stopDragging()
}, [stopDragging])
return (
<div
className="absolute z-[60] w-96"
@ -47,7 +132,10 @@ export const CommentInput: FC<CommentInputProps> = memo(({ position, onSubmit, o
data-comment-input
>
<div className="flex items-center gap-3">
<div className="relative shrink-0">
<div
className="relative shrink-0 cursor-move"
onPointerDown={handleDragPointerDown}
>
<div className="relative h-8 w-8 overflow-hidden rounded-br-full rounded-tl-full rounded-tr-full bg-primary-500">
<div className="absolute inset-[2px] overflow-hidden rounded-br-full rounded-tl-full rounded-tr-full bg-white">
<div className="flex h-full w-full items-center justify-center">

View File

@ -81,6 +81,7 @@ import {
useStore,
useWorkflowStore,
} from './store'
import type { WorkflowSliceShape } from './store/workflow/workflow-slice'
import {
CUSTOM_EDGE,
CUSTOM_NODE,
@ -210,6 +211,7 @@ export const Workflow: FC<WorkflowProps> = memo(({
const showUserComments = useStore(s => s.showUserComments)
const showUserCursors = useStore(s => s.showUserCursors)
const isCommentPreviewHovering = useStore(s => s.isCommentPreviewHovering)
const setPendingCommentState = useStore(s => s.setPendingComment)
const isCommentInputActive = Boolean(pendingComment)
const { t } = useTranslation()
@ -244,6 +246,10 @@ export const Workflow: FC<WorkflowProps> = memo(({
}
}, [])
const handlePendingCommentPositionChange = useCallback((position: NonNullable<WorkflowSliceShape['pendingComment']>) => {
setPendingCommentState(position)
}, [setPendingCommentState])
const { handleRefreshWorkflowDraft } = useWorkflowRefreshDraft()
const handleSyncWorkflowDraftWhenPageClose = useCallback(() => {
if (document.visibilityState === 'hidden')
@ -483,6 +489,7 @@ export const Workflow: FC<WorkflowProps> = memo(({
}}
onSubmit={handleCommentSubmit}
onCancel={handleCommentCancel}
onPositionChange={handlePendingCommentPositionChange}
/>
)}
{comments.map((comment, index) => {