perf(flow): ⚡️ Smooth panning even with complexe flow
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
import { useEventListener } from '@chakra-ui/hooks'
|
import { useEventListener } from '@chakra-ui/hooks'
|
||||||
import assert from 'assert'
|
import assert from 'assert'
|
||||||
import { headerHeight } from 'components/shared/TypebotHeader/TypebotHeader'
|
|
||||||
import { useGraph, ConnectingIds } from 'contexts/GraphContext'
|
import { useGraph, ConnectingIds } from 'contexts/GraphContext'
|
||||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||||
import React, { useMemo, useState } from 'react'
|
import React, { useMemo, useState } from 'react'
|
||||||
@@ -18,7 +17,6 @@ export const DrawingEdge = () => {
|
|||||||
sourceEndpoints,
|
sourceEndpoints,
|
||||||
targetEndpoints,
|
targetEndpoints,
|
||||||
blocksCoordinates,
|
blocksCoordinates,
|
||||||
graphOffsetY,
|
|
||||||
} = useGraph()
|
} = useGraph()
|
||||||
const { createEdge } = useTypebot()
|
const { createEdge } = useTypebot()
|
||||||
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 })
|
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 })
|
||||||
@@ -32,19 +30,21 @@ export const DrawingEdge = () => {
|
|||||||
if (!connectingIds) return 0
|
if (!connectingIds) return 0
|
||||||
return getEndpointTopOffset(
|
return getEndpointTopOffset(
|
||||||
sourceEndpoints,
|
sourceEndpoints,
|
||||||
graphOffsetY,
|
graphPosition.y,
|
||||||
connectingIds.source.itemId ?? connectingIds.source.stepId
|
connectingIds.source.itemId ?? connectingIds.source.stepId
|
||||||
)
|
)
|
||||||
}, [connectingIds, sourceEndpoints, graphOffsetY])
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [connectingIds, sourceEndpoints])
|
||||||
|
|
||||||
const targetTop = useMemo(() => {
|
const targetTop = useMemo(() => {
|
||||||
if (!connectingIds) return 0
|
if (!connectingIds) return 0
|
||||||
return getEndpointTopOffset(
|
return getEndpointTopOffset(
|
||||||
targetEndpoints,
|
targetEndpoints,
|
||||||
graphOffsetY,
|
graphPosition.y,
|
||||||
connectingIds.target?.stepId
|
connectingIds.target?.stepId
|
||||||
)
|
)
|
||||||
}, [connectingIds, targetEndpoints, graphOffsetY])
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [connectingIds, targetEndpoints])
|
||||||
|
|
||||||
const path = useMemo(() => {
|
const path = useMemo(() => {
|
||||||
if (!sourceTop || !sourceBlockCoordinates) return ``
|
if (!sourceTop || !sourceBlockCoordinates) return ``
|
||||||
@@ -72,7 +72,7 @@ export const DrawingEdge = () => {
|
|||||||
const handleMouseMove = (e: MouseEvent) => {
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
setMousePosition({
|
setMousePosition({
|
||||||
x: e.clientX - graphPosition.x,
|
x: e.clientX - graphPosition.x,
|
||||||
y: e.clientY - graphPosition.y - headerHeight,
|
y: e.clientY - graphPosition.y,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
useEventListener('mousemove', handleMouseMove)
|
useEventListener('mousemove', handleMouseMove)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const DropOffEdge = ({
|
|||||||
onUnlockProPlanClick,
|
onUnlockProPlanClick,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { user } = useUser()
|
const { user } = useUser()
|
||||||
const { sourceEndpoints, blocksCoordinates, graphOffsetY } = useGraph()
|
const { sourceEndpoints, blocksCoordinates, graphPosition } = useGraph()
|
||||||
const { publishedTypebot } = useTypebot()
|
const { publishedTypebot } = useTypebot()
|
||||||
|
|
||||||
const isUserOnFreePlan = isFreePlan(user)
|
const isUserOnFreePlan = isFreePlan(user)
|
||||||
@@ -60,7 +60,7 @@ export const DropOffEdge = ({
|
|||||||
() =>
|
() =>
|
||||||
getEndpointTopOffset(
|
getEndpointTopOffset(
|
||||||
sourceEndpoints,
|
sourceEndpoints,
|
||||||
graphOffsetY,
|
graphPosition.y,
|
||||||
block?.steps[block.steps.length - 1].id
|
block?.steps[block.steps.length - 1].id
|
||||||
),
|
),
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const Edge = ({ edge }: { edge: EdgeProps }) => {
|
|||||||
sourceEndpoints,
|
sourceEndpoints,
|
||||||
targetEndpoints,
|
targetEndpoints,
|
||||||
blocksCoordinates,
|
blocksCoordinates,
|
||||||
graphOffsetY,
|
graphPosition,
|
||||||
} = useGraph()
|
} = useGraph()
|
||||||
const [isMouseOver, setIsMouseOver] = useState(false)
|
const [isMouseOver, setIsMouseOver] = useState(false)
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||||
@@ -42,7 +42,7 @@ export const Edge = ({ edge }: { edge: EdgeProps }) => {
|
|||||||
() =>
|
() =>
|
||||||
getEndpointTopOffset(
|
getEndpointTopOffset(
|
||||||
sourceEndpoints,
|
sourceEndpoints,
|
||||||
graphOffsetY,
|
graphPosition.y,
|
||||||
getSourceEndpointId(edge)
|
getSourceEndpointId(edge)
|
||||||
),
|
),
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@@ -50,16 +50,16 @@ export const Edge = ({ edge }: { edge: EdgeProps }) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const [targetTop, setTargetTop] = useState(
|
const [targetTop, setTargetTop] = useState(
|
||||||
getEndpointTopOffset(targetEndpoints, graphOffsetY, edge?.to.stepId)
|
getEndpointTopOffset(targetEndpoints, graphPosition.y, edge?.to.stepId)
|
||||||
)
|
)
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
setTargetTop(
|
setTargetTop(
|
||||||
getEndpointTopOffset(targetEndpoints, graphOffsetY, edge?.to.stepId)
|
getEndpointTopOffset(targetEndpoints, graphPosition.y, edge?.to.stepId)
|
||||||
)
|
)
|
||||||
}, [
|
}, [
|
||||||
targetBlockCoordinates?.y,
|
targetBlockCoordinates?.y,
|
||||||
targetEndpoints,
|
targetEndpoints,
|
||||||
graphOffsetY,
|
graphPosition.y,
|
||||||
edge?.to.stepId,
|
edge?.to.stepId,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import { Flex, FlexProps, useEventListener } from '@chakra-ui/react'
|
import { Flex, FlexProps, useEventListener } from '@chakra-ui/react'
|
||||||
import React, { useRef, useMemo, useEffect, useState } from 'react'
|
import React, { useRef, useMemo, useEffect, useState } from 'react'
|
||||||
import { blockWidth, useGraph } from 'contexts/GraphContext'
|
import {
|
||||||
import { BlockNode } from './Nodes/BlockNode/BlockNode'
|
blockWidth,
|
||||||
|
graphPositionDefaultValue,
|
||||||
|
useGraph,
|
||||||
|
} from 'contexts/GraphContext'
|
||||||
import { useStepDnd } from 'contexts/GraphDndContext'
|
import { useStepDnd } from 'contexts/GraphDndContext'
|
||||||
import { Edges } from './Edges'
|
|
||||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||||
import { headerHeight } from 'components/shared/TypebotHeader/TypebotHeader'
|
import { headerHeight } from 'components/shared/TypebotHeader/TypebotHeader'
|
||||||
import { Block, DraggableStepType, PublicTypebot, Typebot } from 'models'
|
import { DraggableStepType, PublicTypebot, Typebot } from 'models'
|
||||||
import { generate } from 'short-uuid'
|
import { generate } from 'short-uuid'
|
||||||
import { AnswersCount } from 'services/analytics'
|
import { AnswersCount } from 'services/analytics'
|
||||||
import { useDebounce } from 'use-debounce'
|
import { useDebounce } from 'use-debounce'
|
||||||
import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable'
|
import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable'
|
||||||
|
import GraphContent from './GraphContent'
|
||||||
|
|
||||||
declare const window: { chrome: unknown | undefined }
|
declare const window: { chrome: unknown | undefined }
|
||||||
|
|
||||||
@@ -30,19 +33,17 @@ export const Graph = ({
|
|||||||
const editorContainerRef = useRef<HTMLDivElement | null>(null)
|
const editorContainerRef = useRef<HTMLDivElement | null>(null)
|
||||||
const { createBlock } = useTypebot()
|
const { createBlock } = useTypebot()
|
||||||
const {
|
const {
|
||||||
graphPosition,
|
setGraphPosition: setGlobalGraphPosition,
|
||||||
setGraphPosition,
|
|
||||||
setOpenedStepId,
|
setOpenedStepId,
|
||||||
updateBlockCoordinates,
|
updateBlockCoordinates,
|
||||||
setGraphOffsetY,
|
|
||||||
} = useGraph()
|
} = useGraph()
|
||||||
|
const [graphPosition, setGraphPosition] = useState(graphPositionDefaultValue)
|
||||||
const [debouncedGraphPosition] = useDebounce(graphPosition, 200)
|
const [debouncedGraphPosition] = useDebounce(graphPosition, 200)
|
||||||
const transform = useMemo(
|
const transform = useMemo(
|
||||||
() =>
|
() =>
|
||||||
`translate(${graphPosition.x}px, ${graphPosition.y}px) scale(${graphPosition.scale})`,
|
`translate(${graphPosition.x}px, ${graphPosition.y}px) scale(${graphPosition.scale})`,
|
||||||
[graphPosition]
|
[graphPosition]
|
||||||
)
|
)
|
||||||
const [isMouseDown, setIsMouseDown] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
editorContainerRef.current = document.getElementById(
|
editorContainerRef.current = document.getElementById(
|
||||||
@@ -53,10 +54,12 @@ export const Graph = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!graphContainerRef.current) return
|
if (!graphContainerRef.current) return
|
||||||
setGraphOffsetY(
|
const { top, left } = graphContainerRef.current.getBoundingClientRect()
|
||||||
graphContainerRef.current.getBoundingClientRect().top +
|
setGlobalGraphPosition({
|
||||||
debouncedGraphPosition.y
|
x: left + debouncedGraphPosition.x,
|
||||||
)
|
y: top + debouncedGraphPosition.y,
|
||||||
|
scale: 1,
|
||||||
|
})
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [debouncedGraphPosition])
|
}, [debouncedGraphPosition])
|
||||||
|
|
||||||
@@ -71,7 +74,6 @@ export const Graph = ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleGlobalMouseUp = () => setIsMouseDown(false)
|
|
||||||
const handleMouseUp = (e: MouseEvent) => {
|
const handleMouseUp = (e: MouseEvent) => {
|
||||||
if (!typebot) return
|
if (!typebot) return
|
||||||
if (!draggedStep && !draggedStepType) return
|
if (!draggedStep && !draggedStepType) return
|
||||||
@@ -98,26 +100,14 @@ export const Graph = ({
|
|||||||
|
|
||||||
const handleClick = () => setOpenedStepId(undefined)
|
const handleClick = () => setOpenedStepId(undefined)
|
||||||
|
|
||||||
const handleMouseDown = () => setIsMouseDown(true)
|
|
||||||
const handleMouseMove = (event: React.MouseEvent) => {
|
|
||||||
if (!isMouseDown) return
|
|
||||||
const { movementX, movementY } = event
|
|
||||||
setGraphPosition({
|
|
||||||
x: graphPosition.x + movementX / (window.chrome ? 2 : 1),
|
|
||||||
y: graphPosition.y + movementY / (window.chrome ? 2 : 1),
|
|
||||||
scale: 1,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
useEventListener('wheel', handleMouseWheel, graphContainerRef.current)
|
useEventListener('wheel', handleMouseWheel, graphContainerRef.current)
|
||||||
useEventListener('mousedown', handleCaptureMouseDown, undefined, {
|
useEventListener('mousedown', handleCaptureMouseDown, undefined, {
|
||||||
capture: true,
|
capture: true,
|
||||||
})
|
})
|
||||||
useEventListener('mouseup', handleMouseUp, graphContainerRef.current)
|
useEventListener('mouseup', handleMouseUp, graphContainerRef.current)
|
||||||
useEventListener('mouseup', handleGlobalMouseUp)
|
|
||||||
useEventListener('click', handleClick, editorContainerRef.current)
|
useEventListener('click', handleClick, editorContainerRef.current)
|
||||||
|
|
||||||
const onDrag = (event: DraggableEvent, draggableData: DraggableData) => {
|
const onDrag = (_: DraggableEvent, draggableData: DraggableData) => {
|
||||||
const { deltaX, deltaY } = draggableData
|
const { deltaX, deltaY } = draggableData
|
||||||
setGraphPosition({
|
setGraphPosition({
|
||||||
x: graphPosition.x + deltaX,
|
x: graphPosition.x + deltaX,
|
||||||
@@ -128,13 +118,7 @@ export const Graph = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DraggableCore onDrag={onDrag}>
|
<DraggableCore onDrag={onDrag}>
|
||||||
<Flex
|
<Flex ref={graphContainerRef} position="relative" {...props}>
|
||||||
ref={graphContainerRef}
|
|
||||||
onMouseDown={handleMouseDown}
|
|
||||||
onMouseMove={handleMouseMove}
|
|
||||||
position="relative"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<Flex
|
<Flex
|
||||||
flex="1"
|
flex="1"
|
||||||
w="full"
|
w="full"
|
||||||
@@ -146,14 +130,10 @@ export const Graph = ({
|
|||||||
willChange="transform"
|
willChange="transform"
|
||||||
transformOrigin="0px 0px 0px"
|
transformOrigin="0px 0px 0px"
|
||||||
>
|
>
|
||||||
<Edges
|
<GraphContent
|
||||||
edges={typebot?.edges ?? []}
|
|
||||||
answersCounts={answersCounts}
|
answersCounts={answersCounts}
|
||||||
onUnlockProPlanClick={onUnlockProPlanClick}
|
onUnlockProPlanClick={onUnlockProPlanClick}
|
||||||
/>
|
/>
|
||||||
{typebot?.blocks.map((block, idx) => (
|
|
||||||
<BlockNode block={block as Block} blockIndex={idx} key={block.id} />
|
|
||||||
))}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</DraggableCore>
|
</DraggableCore>
|
||||||
|
|||||||
31
apps/builder/components/shared/Graph/GraphContent.tsx
Normal file
31
apps/builder/components/shared/Graph/GraphContent.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { useTypebot } from 'contexts/TypebotContext'
|
||||||
|
import { Block } from 'models'
|
||||||
|
import React from 'react'
|
||||||
|
import { AnswersCount } from 'services/analytics'
|
||||||
|
import { Edges } from './Edges'
|
||||||
|
import { BlockNode } from './Nodes/BlockNode'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
answersCounts?: AnswersCount[]
|
||||||
|
onUnlockProPlanClick?: () => void
|
||||||
|
}
|
||||||
|
const MyComponent = ({ answersCounts, onUnlockProPlanClick }: Props) => {
|
||||||
|
const { typebot } = useTypebot()
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Edges
|
||||||
|
edges={typebot?.edges ?? []}
|
||||||
|
answersCounts={answersCounts}
|
||||||
|
onUnlockProPlanClick={onUnlockProPlanClick}
|
||||||
|
/>
|
||||||
|
{typebot?.blocks.map((block, idx) => (
|
||||||
|
<BlockNode block={block as Block} blockIndex={idx} key={block.id} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Performance hack, never rerender when graph (parent) is panned
|
||||||
|
const areEqual = () => true
|
||||||
|
|
||||||
|
export default React.memo(MyComponent, areEqual)
|
||||||
@@ -73,8 +73,6 @@ const graphContext = createContext<{
|
|||||||
openedStepId?: string
|
openedStepId?: string
|
||||||
setOpenedStepId: Dispatch<SetStateAction<string | undefined>>
|
setOpenedStepId: Dispatch<SetStateAction<string | undefined>>
|
||||||
isReadOnly: boolean
|
isReadOnly: boolean
|
||||||
graphOffsetY: number
|
|
||||||
setGraphOffsetY: Dispatch<SetStateAction<number>>
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
}>({
|
}>({
|
||||||
@@ -100,8 +98,6 @@ export const GraphProvider = ({
|
|||||||
const [blocksCoordinates, setBlocksCoordinates] = useState<BlocksCoordinates>(
|
const [blocksCoordinates, setBlocksCoordinates] = useState<BlocksCoordinates>(
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
const [graphOffsetY, setGraphOffsetY] = useState(0)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setBlocksCoordinates(
|
setBlocksCoordinates(
|
||||||
blocks.reduce(
|
blocks.reduce(
|
||||||
@@ -153,8 +149,6 @@ export const GraphProvider = ({
|
|||||||
blocksCoordinates,
|
blocksCoordinates,
|
||||||
updateBlockCoordinates,
|
updateBlockCoordinates,
|
||||||
isReadOnly,
|
isReadOnly,
|
||||||
graphOffsetY,
|
|
||||||
setGraphOffsetY,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
Reference in New Issue
Block a user