Skip to content

Commit

Permalink
⚡ Fix typebot drag and drop lag
Browse files Browse the repository at this point in the history
Closes #1376
  • Loading branch information
baptisteArno committed Mar 26, 2024
1 parent c552fa7 commit 798e94a
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 52 deletions.
4 changes: 2 additions & 2 deletions apps/builder/src/features/folders/components/BackButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ export const BackButton = ({ id }: { id: string | null }) => {
href={id ? `/typebots/folders/${id}` : '/typebots'}
leftIcon={<ChevronLeftIcon />}
variant={'outline'}
colorScheme={isTypebotOver ? 'blue' : 'gray'}
borderWidth={isTypebotOver ? '3px' : '1px'}
colorScheme={isTypebotOver || draggedTypebot ? 'blue' : 'gray'}
borderWidth={isTypebotOver ? '2px' : '1px'}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useRouter } from 'next/router'
import { stringify } from 'qs'
import React from 'react'
import { useTranslate } from '@tolgee/react'
import { useTypebotDnd } from '../TypebotDndProvider'

export const CreateBotButton = ({
folderId,
Expand All @@ -12,6 +13,7 @@ export const CreateBotButton = ({
}: { folderId?: string; isFirstBot: boolean } & ButtonProps) => {
const { t } = useTranslate()
const router = useRouter()
const { draggedTypebot } = useTypebotDnd()

const handleClick = () =>
router.push(
Expand All @@ -28,6 +30,7 @@ export const CreateBotButton = ({
paddingX={6}
whiteSpace={'normal'}
colorScheme="blue"
opacity={draggedTypebot ? 0.3 : 1}
{...props}
>
<VStack spacing="6">
Expand Down
28 changes: 18 additions & 10 deletions apps/builder/src/features/folders/components/FolderButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,24 @@ import { FolderIcon, MoreVerticalIcon } from '@/components/icons'
import { ConfirmModal } from '@/components/ConfirmModal'
import { useTypebotDnd } from '../TypebotDndProvider'
import { useRouter } from 'next/router'
import React, { useMemo } from 'react'
import React, { memo, useMemo } from 'react'
import { useToast } from '@/hooks/useToast'
import { T, useTranslate } from '@tolgee/react'
import { trpc } from '@/lib/trpc'

export const FolderButton = ({
folder,
index,
onFolderDeleted,
onFolderRenamed,
}: {
type Props = {
folder: DashboardFolder
index: number
onFolderDeleted: () => void
onFolderRenamed: () => void
}) => {
}

const FolderButton = ({
folder,
index,
onFolderDeleted,
onFolderRenamed,
}: Props) => {
const { t } = useTranslate()
const router = useRouter()
const { draggedTypebot, setMouseOverFolderId, mouseOverFolderId } =
Expand Down Expand Up @@ -88,8 +90,9 @@ export const FolderButton = ({
pos="relative"
cursor="pointer"
variant="outline"
colorScheme={isTypebotOver ? 'blue' : 'gray'}
borderWidth={isTypebotOver ? '3px' : '1px'}
colorScheme={isTypebotOver || draggedTypebot ? 'blue' : 'gray'}
borderWidth={isTypebotOver ? '2px' : '1px'}
transition={'border-width 0.1s ease'}
justifyContent="center"
onClick={handleClick}
onMouseEnter={handleMouseEnter}
Expand Down Expand Up @@ -186,3 +189,8 @@ export const ButtonSkeleton = () => (
</VStack>
</Button>
)

export default memo(
FolderButton,
(prev, next) => prev.folder.id === next.folder.id && prev.index === next.index
)
75 changes: 44 additions & 31 deletions apps/builder/src/features/folders/components/FolderContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,22 @@ import {
Wrap,
} from '@chakra-ui/react'
import { useTypebotDnd } from '../TypebotDndProvider'
import React, { useState } from 'react'
import React, { useEffect, useState } from 'react'
import { BackButton } from './BackButton'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { useToast } from '@/hooks/useToast'
import { CreateBotButton } from './CreateBotButton'
import { CreateFolderButton } from './CreateFolderButton'
import { ButtonSkeleton, FolderButton } from './FolderButton'
import { TypebotButton } from './TypebotButton'
import FolderButton, { ButtonSkeleton } from './FolderButton'
import TypebotButton from './TypebotButton'
import { TypebotCardOverlay } from './TypebotButtonOverlay'
import { useTypebots } from '@/features/dashboard/hooks/useTypebots'
import { TypebotInDashboard } from '@/features/dashboard/types'
import { trpc } from '@/lib/trpc'
import { NodePosition } from '@/features/graph/providers/GraphDndProvider'

type Props = { folder: DashboardFolder | null }

const dragDistanceTolerance = 20

export const FolderContent = ({ folder }: Props) => {
const { workspace, currentRole } = useWorkspace()
const [isCreatingFolder, setIsCreatingFolder] = useState(false)
Expand All @@ -36,14 +35,11 @@ export const FolderContent = ({ folder }: Props) => {
mouseOverFolderId,
setMouseOverFolderId,
} = useTypebotDnd()
const [mouseDownPosition, setMouseDownPosition] = useState({ x: 0, y: 0 })
const [draggablePosition, setDraggablePosition] = useState({ x: 0, y: 0 })
const [relativeDraggablePosition, setRelativeDraggablePosition] = useState({
const [mousePositionInElement, setMousePositionInElement] = useState({
x: 0,
y: 0,
})
const [typebotDragCandidate, setTypebotDragCandidate] =
useState<TypebotInDashboard>()

const { showToast } = useToast()

Expand Down Expand Up @@ -121,40 +117,55 @@ export const FolderContent = ({ folder }: Props) => {
const handleMouseUp = async () => {
if (mouseOverFolderId !== undefined && draggedTypebot)
await moveTypebotToFolder(draggedTypebot.id, mouseOverFolderId ?? 'root')
setTypebotDragCandidate(undefined)
setMouseOverFolderId(undefined)
setDraggedTypebot(undefined)
}
useEventListener('mouseup', handleMouseUp)

const handleMouseDown =
(typebot: TypebotInDashboard) => (e: React.MouseEvent) => {
const element = e.currentTarget as HTMLDivElement
const rect = element.getBoundingClientRect()
setDraggablePosition({ x: rect.left, y: rect.top })
const x = e.clientX - rect.left
const y = e.clientY - rect.top
setRelativeDraggablePosition({ x, y })
setMouseDownPosition({ x: e.screenX, y: e.screenY })
setTypebotDragCandidate(typebot)
const handleTypebotDrag =
(typebot: TypebotInDashboard) =>
({ absolute, relative }: NodePosition) => {
if (draggedTypebot) return
setMousePositionInElement(relative)
setDraggablePosition({
x: absolute.x - relative.x,
y: absolute.y - relative.y,
})
setDraggedTypebot(typebot)
}

const handleMouseMove = (e: MouseEvent) => {
if (!typebotDragCandidate) return
const { clientX, clientY, screenX, screenY } = e
if (
Math.abs(mouseDownPosition.x - screenX) > dragDistanceTolerance ||
Math.abs(mouseDownPosition.y - screenY) > dragDistanceTolerance
)
setDraggedTypebot(typebotDragCandidate)
if (!draggedTypebot) return
const { clientX, clientY } = e
setDraggablePosition({
...draggablePosition,
x: clientX - relativeDraggablePosition.x,
y: clientY - relativeDraggablePosition.y,
x: clientX - mousePositionInElement.x,
y: clientY - mousePositionInElement.y,
})
}
useEventListener('mousemove', handleMouseMove)

useEffect(() => {
if (!draggablePosition || !draggedTypebot) return
const { innerHeight } = window
const scrollSpeed = 10
const scrollMargin = 50
const clientY = draggablePosition.y + mousePositionInElement.y
const scrollY =
clientY < scrollMargin
? -scrollSpeed
: clientY > innerHeight - scrollMargin
? scrollSpeed
: 0
window.scrollBy(0, scrollY)
const interval = setInterval(() => {
window.scrollBy(0, scrollY)
}, 5)

return () => {
clearInterval(interval)
}
}, [draggablePosition, draggedTypebot, mousePositionInElement])

return (
<Flex w="full" flex="1" justify="center">
<Stack w="1000px" spacing={6} pt="4">
Expand Down Expand Up @@ -196,8 +207,9 @@ export const FolderContent = ({ folder }: Props) => {
<TypebotButton
key={typebot.id.toString()}
typebot={typebot}
draggedTypebot={draggedTypebot}
onTypebotUpdated={refetchTypebots}
onMouseDown={handleMouseDown(typebot)}
onDrag={handleTypebotDrag(typebot)}
/>
))}
</Wrap>
Expand All @@ -214,6 +226,7 @@ export const FolderContent = ({ folder }: Props) => {
style={{
transform: `translate(${draggablePosition.x}px, ${draggablePosition.y}px) rotate(-2deg)`,
}}
transformOrigin="0 0 0"
/>
</Portal>
)}
Expand Down
35 changes: 27 additions & 8 deletions apps/builder/src/features/folders/components/TypebotButton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react'
import React, { memo } from 'react'
import {
Alert,
AlertIcon,
Expand All @@ -16,7 +16,6 @@ import {
import { useRouter } from 'next/router'
import { ConfirmModal } from '@/components/ConfirmModal'
import { GripIcon } from '@/components/icons'
import { useTypebotDnd } from '../TypebotDndProvider'
import { useDebounce } from 'use-debounce'
import { useToast } from '@/hooks/useToast'
import { MoreButton } from './MoreButton'
Expand All @@ -26,29 +25,41 @@ import { TypebotInDashboard } from '@/features/dashboard/types'
import { isMobile } from '@/helpers/isMobile'
import { trpc, trpcVanilla } from '@/lib/trpc'
import { duplicateName } from '@/features/typebot/helpers/duplicateName'
import {
NodePosition,
useDragDistance,
} from '@/features/graph/providers/GraphDndProvider'

type Props = {
typebot: TypebotInDashboard
isReadOnly?: boolean
draggedTypebot: TypebotInDashboard | undefined
onTypebotUpdated: () => void
onMouseDown?: (e: React.MouseEvent<HTMLButtonElement>) => void
onDrag: (position: NodePosition) => void
}

export const TypebotButton = ({
const TypebotButton = ({
typebot,
isReadOnly = false,
draggedTypebot,
onTypebotUpdated,
onMouseDown,
onDrag,
}: Props) => {
const { t } = useTranslate()
const router = useRouter()
const { draggedTypebot } = useTypebotDnd()
const [draggedTypebotDebounced] = useDebounce(draggedTypebot, 200)
const {
isOpen: isDeleteOpen,
onOpen: onDeleteOpen,
onClose: onDeleteClose,
} = useDisclosure()
const buttonRef = React.useRef<HTMLDivElement>(null)

useDragDistance({
ref: buttonRef,
onDrag,
deps: [],
})

const { showToast } = useToast()

Expand Down Expand Up @@ -125,6 +136,7 @@ export const TypebotButton = ({

return (
<Button
ref={buttonRef}
as={WrapItem}
onClick={handleTypebotClick}
display="flex"
Expand All @@ -134,8 +146,7 @@ export const TypebotButton = ({
h="270px"
rounded="lg"
whiteSpace="normal"
opacity={draggedTypebot?.id === typebot.id ? 0.2 : 1}
onMouseDown={onMouseDown}
opacity={draggedTypebot ? 0.3 : 1}
cursor="pointer"
>
{typebot.publishedTypebotId && (
Expand Down Expand Up @@ -223,3 +234,11 @@ export const TypebotButton = ({
</Button>
)
}

export default memo(
TypebotButton,
(prev, next) =>
prev.draggedTypebot?.id === next.draggedTypebot?.id &&
prev.typebot.id === next.typebot.id &&
prev.isReadOnly === next.isReadOnly
)
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export const useDragDistance = ({
ref: React.MutableRefObject<HTMLDivElement | null>
onDrag: (position: { absolute: Coordinates; relative: Coordinates }) => void
distanceTolerance?: number
isDisabled: boolean
isDisabled?: boolean
deps?: unknown[]
}) => {
const mouseDownPosition = useRef<{
Expand Down

0 comments on commit 798e94a

Please sign in to comment.