fix: zoom pan helper
This commit is contained in:
@@ -60,13 +60,13 @@ const toggleClassnames = () => {
|
||||
<Flow
|
||||
class="revue-flow-basic-example"
|
||||
:elements="elements"
|
||||
:on-elements-remove="onElementsRemove"
|
||||
:on-connect="onConnect"
|
||||
:on-node-drag-stop="onNodeDragStop"
|
||||
:on-node-click="onElementClick"
|
||||
:default-zoom="1.5"
|
||||
:min-zoom="0.2"
|
||||
:max-zoom="4"
|
||||
@elements-remove="onElementsRemove"
|
||||
@connect="onConnect"
|
||||
@node-drag-stop="onNodeDragStop"
|
||||
@node-click="onElementClick"
|
||||
@elementClick="onElementClick"
|
||||
@load="onLoad"
|
||||
>
|
||||
|
||||
@@ -15,7 +15,7 @@ const onElementsRemove = (elementsToRemove: Elements) =>
|
||||
const onConnect = (params: Connection | Edge) => (elements.value = addEdge(params, elements.value as Elements))
|
||||
</script>
|
||||
<template>
|
||||
<Flow :elements="elements" :custom-connection-line="ConnectionLine" @elementsRemove="onElementsRemove" @connect="onConnect">
|
||||
<Flow :elements="elements" :custom-connection-line="ConnectionLine" @elements-remove="onElementsRemove" @connect="onConnect">
|
||||
<Background :variant="BackgroundVariant.Lines" />
|
||||
</Flow>
|
||||
</template>
|
||||
|
||||
@@ -114,17 +114,17 @@ const onConnect = (params: Connection | Edge) =>
|
||||
<template>
|
||||
<Flow
|
||||
:elements="elements"
|
||||
:on-element-click="onElementClick"
|
||||
:on-elements-remove="onElementsRemove"
|
||||
:on-connect="onConnect"
|
||||
:on-node-drag-stop="onNodeDragStop"
|
||||
:style="`background: ${bgColor}`"
|
||||
:on-load="onLoad"
|
||||
:node-types="nodeTypes"
|
||||
:connection-line-style="connectionLineStyle"
|
||||
:snap-to-grid="true"
|
||||
:snap-grid="snapGrid"
|
||||
:default-zoom="1.5"
|
||||
@element-click="onElementClick"
|
||||
@elements-remove="onElementsRemove"
|
||||
@connect="onConnect"
|
||||
@node-drag-stop="onNodeDragStop"
|
||||
@load="onLoad"
|
||||
>
|
||||
<MiniMap :node-stroke-color="nodeStroke" :node-color="nodeColor" />
|
||||
<Controls />
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { RevueFlowHooks } from '~/hooks/RevueFlowHooks'
|
||||
import {
|
||||
getEdgeCenter,
|
||||
getBezierPath,
|
||||
getMarkerEnd,
|
||||
ArrowHeadType,
|
||||
EdgeProps,
|
||||
ElementId,
|
||||
Position,
|
||||
RevueFlowStore,
|
||||
} from '~/index'
|
||||
import { getEdgeCenter, getBezierPath, getMarkerEnd, ArrowHeadType, EdgeProps, ElementId, Position, Hooks, Store } from '~/index'
|
||||
|
||||
interface CustomEdgeProps<T = any> extends EdgeProps<T> {
|
||||
id: ElementId
|
||||
@@ -25,8 +15,8 @@ interface CustomEdgeProps<T = any> extends EdgeProps<T> {
|
||||
}
|
||||
|
||||
const props = defineProps<CustomEdgeProps>()
|
||||
const store = inject<RevueFlowStore>('store')!
|
||||
const hooks = inject<RevueFlowHooks>('hooks')!
|
||||
const store = inject(Store)!
|
||||
const hooks = inject(Hooks)!
|
||||
const onEdgeClick = (evt: Event, id: string) => {
|
||||
const edge = store.edges.find((edge) => edge.id === id)
|
||||
if (edge) {
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import Flowy from '~/container/Flow/Flow.vue'
|
||||
import { Elements } from '~/types'
|
||||
|
||||
const elements = ref<Elements>([
|
||||
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } },
|
||||
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } },
|
||||
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } },
|
||||
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } },
|
||||
{ id: 'e1-2', source: '1', target: '2', animated: true },
|
||||
{ id: 'e1-3', source: '1', target: '3' },
|
||||
] as Elements)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Flowy :elements="elements">
|
||||
<div>Foobar!</div>
|
||||
</Flowy>
|
||||
</template>
|
||||
@@ -45,10 +45,6 @@ export const routes: RouterOptions['routes'] = [
|
||||
path: '/interaction',
|
||||
component: () => import('./Interaction/InteractionExample.vue'),
|
||||
},
|
||||
{
|
||||
path: '/super-flow',
|
||||
component: () => import('./Superflow/Superflow.vue'),
|
||||
},
|
||||
]
|
||||
|
||||
export const router = createRouter({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { HTMLAttributes } from 'vue'
|
||||
import { BackgroundVariant, RevueFlowStore } from '~/types'
|
||||
import { BackgroundVariant } from '~/types'
|
||||
import { Store } from '~/context'
|
||||
|
||||
export interface BackgroundProps extends HTMLAttributes {
|
||||
variant?: BackgroundVariant
|
||||
@@ -20,14 +21,23 @@ const defaultColors: Record<BackgroundVariant, string> = {
|
||||
[BackgroundVariant.Lines]: '#eee',
|
||||
}
|
||||
|
||||
const store = inject<RevueFlowStore>('store')!
|
||||
const store = inject(Store)!
|
||||
// when there are multiple flows on a page we need to make sure that every background gets its own pattern.
|
||||
|
||||
const bgClasses = ['revue-flow__background']
|
||||
const scaledGap = computed(() => props.gap && props.gap * store.transform[2])
|
||||
const xOffset = computed(() => scaledGap.value && store.transform[0] % scaledGap.value)
|
||||
const yOffset = computed(() => scaledGap.value && store.transform[1] % scaledGap.value)
|
||||
const size = computed(() => props.size || 0.4 * store.transform[2])
|
||||
const background = computed(() => {
|
||||
const scaledGap = props.gap && props.gap * store.transform[2]
|
||||
const xOffset = scaledGap && store.transform[0] % scaledGap
|
||||
const yOffset = scaledGap && store.transform[1] % scaledGap
|
||||
const size = props.size || 0.4 * store.transform[2]
|
||||
|
||||
return {
|
||||
scaledGap,
|
||||
xOffset,
|
||||
yOffset,
|
||||
size,
|
||||
}
|
||||
})
|
||||
|
||||
const isLines = props.variant === BackgroundVariant.Lines
|
||||
const bgColor = props.color ? props.color : defaultColors[props.variant || BackgroundVariant.Dots]
|
||||
@@ -41,16 +51,23 @@ const patternId = `pattern-${Math.floor(Math.random() * 100000)}`
|
||||
height: '100%',
|
||||
}"
|
||||
>
|
||||
<pattern :id="patternId" :x="xOffset" :y="yOffset" :width="scaledGap" :height="scaledGap" patternUnits="userSpaceOnUse">
|
||||
<pattern
|
||||
:id="patternId"
|
||||
:x="background.xOffset"
|
||||
:y="background.yOffset"
|
||||
:width="background.scaledGap"
|
||||
:height="background.scaledGap"
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<template v-if="isLines">
|
||||
<path
|
||||
:stroke="bgColor"
|
||||
:stroke-width="props.size"
|
||||
:d="`M${scaledGap / 2} 0 V${scaledGap} M0 ${scaledGap / 2} H${scaledGap}`"
|
||||
:d="`M${background.scaledGap / 2} 0 V${background.scaledGap} M0 ${background.scaledGap / 2} H${background.scaledGap}`"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<circle :cx="size" :cy="size" :r="size" :fill="bgColor" />
|
||||
<circle :cx="background.size" :cy="background.size" :r="background.size" :fill="bgColor" />
|
||||
</template>
|
||||
</pattern>
|
||||
<rect x="0" y="0" width="100%" height="100%" :fill="`url(#${patternId})`" />
|
||||
|
||||
@@ -6,8 +6,9 @@ import MinusIcon from '@/assets/icons/minus.svg'
|
||||
import Fitview from '@/assets/icons/fitview.svg'
|
||||
import Lock from '@/assets/icons/lock.svg'
|
||||
import Unlock from '@/assets/icons/unlock.svg'
|
||||
import { FitViewParams, RevueFlowStore, ZoomPanHelperFunctions } from '~/types'
|
||||
import useZoomPanHelper from '~/hooks/useZoomPanHelper'
|
||||
import { FitViewParams } from '~/types'
|
||||
import useZoomPanHelper from '~/composables/useZoomPanHelper'
|
||||
import { Store } from '~/context'
|
||||
|
||||
export interface ControlProps extends HTMLAttributes {
|
||||
showZoom?: boolean
|
||||
@@ -26,25 +27,24 @@ const props = withDefaults(defineProps<ControlProps>(), {
|
||||
showInteractive: true,
|
||||
})
|
||||
|
||||
const store = inject<RevueFlowStore>('store')!
|
||||
// const { onReady } = useZoomPanHelper()
|
||||
const store = inject(Store)!
|
||||
const { zoomIn, zoomOut, fitView } = useZoomPanHelper()
|
||||
|
||||
const isInteractive = store.nodesDraggable && store.nodesConnectable && store.elementsSelectable
|
||||
const mapClasses = ['revue-flow__controls']
|
||||
|
||||
/*
|
||||
* const onZoomInHandler = () => {
|
||||
zoomHelper.value?.zoomIn?.()
|
||||
const onZoomInHandler = () => {
|
||||
zoomIn?.()
|
||||
props.onZoomIn?.()
|
||||
}
|
||||
|
||||
const onZoomOutHandler = () => {
|
||||
zoomHelper.value?.zoomOut?.()
|
||||
zoomOut?.()
|
||||
props.onZoomOut?.()
|
||||
}
|
||||
|
||||
const onFitViewHandler = () => {
|
||||
zoomHelper.value?.fitView?.(props.fitViewParams)
|
||||
fitView?.(props.fitViewParams)
|
||||
props.onFitView?.()
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@ const onInteractiveChangeHandler = () => {
|
||||
store.setInteractive?.(!isInteractive)
|
||||
props.onInteractiveChange?.(!isInteractive)
|
||||
}
|
||||
*/
|
||||
</script>
|
||||
<template>
|
||||
<div :class="mapClasses">
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
import { HTMLAttributes } from 'vue'
|
||||
import MiniMapNode from './MiniMapNode.vue'
|
||||
import { getBoundsofRects, getRectOfNodes } from '~/utils/graph'
|
||||
import { Node, RevueFlowStore } from '~/types'
|
||||
import { Node } from '~/types'
|
||||
import { Store } from '~/context'
|
||||
|
||||
type StringFunc = (node: Node) => string
|
||||
|
||||
@@ -30,7 +31,7 @@ declare const window: any
|
||||
const defaultWidth = 200
|
||||
const defaultHeight = 150
|
||||
|
||||
const store = inject<RevueFlowStore>('store')!
|
||||
const store = inject(Store)!
|
||||
|
||||
const elementWidth = attrs.style?.width ?? defaultWidth
|
||||
const elementHeight = attrs.style?.height ?? defaultHeight
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, CSSProperties } from 'vue'
|
||||
import { CSSProperties } from 'vue'
|
||||
|
||||
interface MiniMapNodeProps {
|
||||
x?: number
|
||||
@@ -8,11 +8,13 @@ interface MiniMapNodeProps {
|
||||
height?: number
|
||||
borderRadius?: number
|
||||
color?: string
|
||||
shapeRendering?: string
|
||||
shapeRendering?: 'auto' | 'optimizeSpeed' | 'crispEdges' | 'geometricPrecision' | 'inherit'
|
||||
strokeColor?: string
|
||||
strokeWidth?: number
|
||||
}
|
||||
const props = defineProps<MiniMapNodeProps>()
|
||||
const props = withDefaults(defineProps<MiniMapNodeProps>(), {
|
||||
shapeRendering: 'geometricPrecision',
|
||||
})
|
||||
const attrs = useAttrs()
|
||||
|
||||
const styles = (attrs.style ?? {}) as CSSProperties
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { CSSProperties } from 'vue'
|
||||
import { ConnectionLineType, CustomConnectionLine, HandleElement, Node, Position } from '~/types'
|
||||
import { getBezierPath, getSmoothStepPath } from '~/components/Edges/utils'
|
||||
import { Hooks, Store } from '~/context/symbols'
|
||||
import { Hooks, Store } from '~/context'
|
||||
|
||||
interface ConnectionLineProps {
|
||||
sourceNode: Node
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getEdgePositions, getHandle, getSourceTargetNodes, isEdgeVisible } from
|
||||
import { isEdge } from '~/utils/graph'
|
||||
import { ConnectionMode, Dimensions, Edge, EdgeType, Elements, Position, Transform } from '~/types'
|
||||
import { onMouseDown } from '~/components/Handle/utils'
|
||||
import { Hooks, Store } from '~/context/symbols'
|
||||
import { Hooks, Store } from '~/context'
|
||||
|
||||
interface EdgeProps {
|
||||
type: EdgeType
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { ElementId, Position } from '~/types'
|
||||
import { onMouseDown, ValidConnectionFunc } from '~/components/Handle/utils'
|
||||
import { Hooks, Store } from '~/context/symbols'
|
||||
import { Hooks, Store } from '~/context'
|
||||
|
||||
interface HandleProps {
|
||||
id?: string
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { getHostForElement } from '~/utils'
|
||||
import { ElementId, ConnectionMode, Connection, HandleType, RevueFlowStore } from '~/types'
|
||||
import { RevueFlowHooks } from '~/hooks/RevueFlowHooks'
|
||||
import { ElementId, ConnectionMode, Connection, HandleType, FlowStore, FlowHooks } from '~/types'
|
||||
|
||||
export type ValidConnectionFunc = (connection: Connection) => boolean
|
||||
|
||||
@@ -71,13 +70,13 @@ function resetRecentHandle(hoveredHandle: Element): void {
|
||||
|
||||
export function onMouseDown(
|
||||
event: MouseEvent,
|
||||
store: RevueFlowStore,
|
||||
store: FlowStore,
|
||||
hooks: {
|
||||
connectStart: RevueFlowHooks['connectStart']
|
||||
connectStop: RevueFlowHooks['connectStop']
|
||||
connectEnd: RevueFlowHooks['connectEnd']
|
||||
connect: RevueFlowHooks['connect']
|
||||
edgeUpdateEnd: RevueFlowHooks['edgeUpdateEnd']
|
||||
connectStart: FlowHooks['connectStart']
|
||||
connectStop: FlowHooks['connectStop']
|
||||
connectEnd: FlowHooks['connectEnd']
|
||||
connect: FlowHooks['connect']
|
||||
edgeUpdateEnd: FlowHooks['edgeUpdateEnd']
|
||||
},
|
||||
handleId: ElementId,
|
||||
nodeId: ElementId,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { DraggableEventListener } from '@braks/revue-draggable'
|
||||
import { Node, NodeDimensionUpdate, NodeType } from '~/types'
|
||||
import { Hooks, Store } from '~/context/symbols'
|
||||
import { Hooks, Store } from '~/context'
|
||||
|
||||
interface NodeProps {
|
||||
node: Node
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { DraggableEventListener } from '@braks/revue-draggable'
|
||||
import { Node } from '~/types'
|
||||
import { isNode } from '~/utils/graph'
|
||||
import { Hooks, Store } from '~/context/symbols'
|
||||
import { Hooks, Store } from '~/context'
|
||||
|
||||
interface NodesSelectionProps {
|
||||
onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import SelectionRect from './SelectionRect.vue'
|
||||
import { getMousePosition } from '~/components/UserSelection/utils'
|
||||
import { Store } from '~/context/symbols'
|
||||
import { Store } from '~/context'
|
||||
|
||||
const store = inject(Store)!
|
||||
const el = templateRef('user-selection', null)
|
||||
|
||||
7
src/composables/index.ts
Normal file
7
src/composables/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export { default as useGlobalKeyHandler } from './useGlobalKeyHandler'
|
||||
export { default as useHooks } from './useHooks'
|
||||
export { default as useKeyPress } from './useKeyPress'
|
||||
export { default as useResizeHandler } from './useResizeHandler'
|
||||
export { default as useUpdateNodeInternals } from './useUpdateNodeInternals'
|
||||
export { default as useZoom } from './useZoom'
|
||||
export { default as useZoomPanHelper } from './useZoomPanHelper'
|
||||
@@ -1,7 +1,7 @@
|
||||
import useKeyPress from './useKeyPress'
|
||||
import { isNode, getConnectedEdges } from '~/utils/graph'
|
||||
import { Elements, KeyCode, ElementId, FlowElement } from '~/types'
|
||||
import { Store } from '~/context/symbols'
|
||||
import { Store } from '~/context'
|
||||
|
||||
interface HookParams {
|
||||
deleteKeyCode: KeyCode
|
||||
@@ -1,7 +1,7 @@
|
||||
// global hooks
|
||||
import { Connection, Edge, Elements, FlowHooks, FlowTransform, Node, OnConnectStartParams, OnLoadParams } from '~/types'
|
||||
|
||||
export const useHooks = (): FlowHooks & { bind: (emit: (event: string, ...args: any[]) => void) => FlowHooks } => {
|
||||
export default (): FlowHooks & { bind: (emit: (event: string, ...args: any[]) => void) => FlowHooks } => {
|
||||
const eventHooks: FlowHooks = {
|
||||
elementClick: createEventHook<{ event: MouseEvent; element: Node | Edge }>(),
|
||||
elementsRemove: createEventHook<Elements>(),
|
||||
@@ -2,20 +2,10 @@ import { D3ZoomEvent, zoom, zoomIdentity, ZoomTransform } from 'd3-zoom'
|
||||
import { pointer, select } from 'd3-selection'
|
||||
import { Ref } from 'vue'
|
||||
import { get } from '@vueuse/core'
|
||||
import {
|
||||
D3Selection,
|
||||
D3Zoom,
|
||||
FlowTransform,
|
||||
InitD3ZoomPayload,
|
||||
KeyCode,
|
||||
PanOnScrollMode,
|
||||
Transform,
|
||||
TranslateExtent,
|
||||
} from '~/types'
|
||||
import { D3Selection, D3Zoom, FlowTransform, KeyCode, PanOnScrollMode, Transform } from '~/types'
|
||||
import { clamp } from '~/utils'
|
||||
import useKeyPress from '~/hooks/useKeyPress'
|
||||
import { Hooks } from '~/context/symbols'
|
||||
import { onLoadGetElements, onLoadProject, onLoadToObject } from '~/utils/graph'
|
||||
import useKeyPress from '~/composables/useKeyPress'
|
||||
import { Hooks, Store } from '~/context'
|
||||
|
||||
const viewChanged = (prevTransform: FlowTransform, eventTransform: ZoomTransform): boolean =>
|
||||
prevTransform.x !== eventTransform.x || prevTransform.y !== eventTransform.y || prevTransform.zoom !== eventTransform.k
|
||||
@@ -30,11 +20,8 @@ interface UseZoomOptions {
|
||||
selectionKeyCode?: KeyCode
|
||||
zoomActivationKeyCode?: KeyCode
|
||||
paneMoveable?: boolean
|
||||
minZoom?: number
|
||||
maxZoom?: number
|
||||
defaultZoom?: number
|
||||
defaultPosition?: [number, number]
|
||||
translateExtent?: TranslateExtent
|
||||
zoomOnScroll?: boolean
|
||||
zoomOnPinch?: boolean
|
||||
panOnScroll?: boolean
|
||||
@@ -49,22 +36,12 @@ interface UseZoom {
|
||||
d3Selection: Ref<D3Selection>
|
||||
}
|
||||
|
||||
export default function (
|
||||
el: Ref<HTMLDivElement>,
|
||||
options: UseZoomOptions,
|
||||
init: (initD3ZoomPayload: InitD3ZoomPayload) => void = () => {},
|
||||
): UseZoom {
|
||||
export default function (el: Ref<HTMLDivElement>, options: UseZoomOptions): UseZoom {
|
||||
const {
|
||||
selectionKeyCode = 'Shift',
|
||||
zoomActivationKeyCode = 'Meta',
|
||||
minZoom = 0.5,
|
||||
maxZoom = 2,
|
||||
defaultZoom = 1,
|
||||
defaultPosition = [0, 0],
|
||||
translateExtent = [
|
||||
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
||||
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
|
||||
],
|
||||
zoomOnScroll = true,
|
||||
zoomOnPinch = true,
|
||||
zoomOnDoubleClick = true,
|
||||
@@ -74,13 +51,17 @@ export default function (
|
||||
paneMoveable = true,
|
||||
} = options
|
||||
const hooks = inject(Hooks)!
|
||||
const store = inject(Store)!
|
||||
const prevTransform = ref<FlowTransform>({ x: 0, y: 0, zoom: 0 })
|
||||
|
||||
const clampedX = clamp(defaultPosition[0], translateExtent[0][0], translateExtent[1][0])
|
||||
const clampedY = clamp(defaultPosition[1], translateExtent[0][1], translateExtent[1][1])
|
||||
const clampedZoom = clamp(defaultZoom, minZoom, maxZoom)
|
||||
const clampedX = clamp(defaultPosition[0], store.translateExtent[0][0], store.translateExtent[1][0])
|
||||
const clampedY = clamp(defaultPosition[1], store.translateExtent[0][1], store.translateExtent[1][1])
|
||||
const clampedZoom = clamp(defaultZoom, store.minZoom, store.maxZoom)
|
||||
const transform = ref<Transform>([clampedX, clampedY, clampedZoom])
|
||||
const d3Zoom = ref(zoom<HTMLDivElement, any>().scaleExtent([minZoom, maxZoom]).translateExtent(translateExtent))
|
||||
store.transform = transform.value
|
||||
const d3Zoom = ref(
|
||||
zoom<HTMLDivElement, any>().scaleExtent([store.minZoom, store.maxZoom]).translateExtent(store.translateExtent),
|
||||
)
|
||||
const d3Selection = ref()
|
||||
|
||||
until(el)
|
||||
@@ -93,8 +74,7 @@ export default function (
|
||||
|
||||
const updatedTransform = zoomIdentity.translate(clampedX, clampedY).scale(clampedZoom)
|
||||
d3z.transform(d3s, updatedTransform)
|
||||
init({ d3Zoom: d3z, d3ZoomHandler, d3Selection: d3s })
|
||||
|
||||
store.initD3Zoom({ d3Zoom: d3z, d3Selection: d3s, d3ZoomHandler })
|
||||
const applyZoomHandlers = () => {
|
||||
d3z.on('start', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||
if (viewChanged(prevTransform.value, event.transform)) {
|
||||
|
||||
62
src/composables/useZoomPanHelper.ts
Normal file
62
src/composables/useZoomPanHelper.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { zoomIdentity } from 'd3-zoom'
|
||||
import { getRectOfNodes, pointToRendererPoint, getTransformForBounds } from '~/utils/graph'
|
||||
import { FitViewParams, FlowTransform, Rect, UseZoomPanHelper, XYPosition } from '~/types'
|
||||
import { Store } from '~/context'
|
||||
|
||||
const DEFAULT_PADDING = 0.1
|
||||
|
||||
export default function (): UseZoomPanHelper {
|
||||
const store = inject(Store)!
|
||||
return {
|
||||
zoomIn: () => store.d3Selection && store.d3Zoom?.scaleBy(store.d3Selection, 1.2),
|
||||
zoomOut: () => store.d3Selection && store.d3Zoom?.scaleBy(store.d3Selection, 1 / 1.2),
|
||||
zoomTo: (zoomLevel: number) => store.d3Selection && store.d3Zoom?.scaleTo(store.d3Selection, zoomLevel),
|
||||
transform: (transform: FlowTransform) => {
|
||||
const nextTransform = zoomIdentity.translate(transform.x, transform.y).scale(transform.zoom)
|
||||
|
||||
store.d3Selection && store.d3Zoom?.transform(store.d3Selection, nextTransform)
|
||||
},
|
||||
fitView: (options: FitViewParams = { padding: DEFAULT_PADDING, includeHiddenNodes: false }) => {
|
||||
if (!store.nodes.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const bounds = getRectOfNodes(options.includeHiddenNodes ? store.nodes : store.nodes.filter((node) => !node.isHidden))
|
||||
const [x, y, zoom] = getTransformForBounds(
|
||||
bounds,
|
||||
store.dimensions.width,
|
||||
store.dimensions.height,
|
||||
options.minZoom || store.minZoom,
|
||||
options.maxZoom || store.maxZoom,
|
||||
options.padding || DEFAULT_PADDING,
|
||||
)
|
||||
const transform = zoomIdentity.translate(x, y).scale(zoom)
|
||||
|
||||
store.d3Selection && store.d3Zoom?.transform(store.d3Selection, transform)
|
||||
},
|
||||
setCenter: (x: number, y: number, zoom?: number) => {
|
||||
const nextZoom = typeof zoom !== 'undefined' ? zoom : store.maxZoom
|
||||
const centerX = store.dimensions.width / 2 - x * nextZoom
|
||||
const centerY = store.dimensions.height / 2 - y * nextZoom
|
||||
const transform = zoomIdentity.translate(centerX, centerY).scale(nextZoom)
|
||||
|
||||
store.d3Selection && store.d3Zoom?.transform(store.d3Selection, transform)
|
||||
},
|
||||
fitBounds: (bounds: Rect, padding = DEFAULT_PADDING) => {
|
||||
const [x, y, zoom] = getTransformForBounds(
|
||||
bounds,
|
||||
store.dimensions.width,
|
||||
store.dimensions.height,
|
||||
store.minZoom,
|
||||
store.maxZoom,
|
||||
padding,
|
||||
)
|
||||
const transform = zoomIdentity.translate(x, y).scale(zoom)
|
||||
|
||||
store.d3Selection && store.d3Zoom?.transform(store.d3Selection, transform)
|
||||
},
|
||||
project: (position: XYPosition) => {
|
||||
return pointToRendererPoint(position, store.transform, store.snapToGrid, store.snapGrid)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import MarkerDefinitions from './MarkerDefinitions.vue'
|
||||
import Edge from '~/components/Edges/Edge.vue'
|
||||
import ConnectionLine from '~/components/ConnectionLine/ConnectionLine.vue'
|
||||
import { ConnectionLineType, ConnectionMode, CustomConnectionLine, Dimensions, EdgeType, Transform } from '~/types'
|
||||
import { Store } from '~/context/symbols'
|
||||
import { Store } from '~/context'
|
||||
|
||||
interface EdgeRendererProps {
|
||||
edgeTypes: Record<string, EdgeType>
|
||||
|
||||
@@ -24,8 +24,8 @@ import { DefaultNode, InputNode, OutputNode } from '~/components/Nodes'
|
||||
import { BezierEdge, SmoothStepEdge, StepEdge, StraightEdge } from '~/components/Edges'
|
||||
import { createEdgeTypes } from '~/container/EdgeRenderer/utils'
|
||||
import { createNodeTypes } from '~/container/NodeRenderer/utils'
|
||||
import { useHooks } from '~/hooks/RevueFlowHooks'
|
||||
import { Hooks, Store } from '~/context/symbols'
|
||||
import { useHooks } from '~/composables'
|
||||
import { Hooks, Store } from '~/context'
|
||||
|
||||
export interface FlowProps {
|
||||
elements: Elements
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import Node from '~/components/Nodes/Node.vue'
|
||||
import { NodeType, Node as TNode, Transform, Dimensions } from '~/types'
|
||||
import { getNodesInside } from '~/utils/graph'
|
||||
import { Store } from '~/context/symbols'
|
||||
import { Store } from '~/context'
|
||||
|
||||
interface NodeRendererProps {
|
||||
nodeTypes: Record<string, NodeType>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import useKeyPress from '~/hooks/useKeyPress'
|
||||
import useKeyPress from '~/composables/useKeyPress'
|
||||
import { KeyCode } from '~/types'
|
||||
import useGlobalKeyHandler from '~/hooks/useGlobalKeyHandler'
|
||||
import useGlobalKeyHandler from '~/composables/useGlobalKeyHandler'
|
||||
import NodesSelection from '~/components/NodesSelection/NodesSelection.vue'
|
||||
import UserSelection from '~/components/UserSelection/UserSelection.vue'
|
||||
import { Hooks, Store } from '~/context/symbols'
|
||||
import { Hooks, Store } from '~/context'
|
||||
|
||||
interface SelectionPaneProps {
|
||||
selectionKeyCode?: KeyCode
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { KeyCode, PanOnScrollMode } from '~/types'
|
||||
import useZoom from '~/composables/useZoom'
|
||||
import useResizeHandler from '~/hooks/useResizeHandler'
|
||||
import useZoomPanHelper from '~/hooks/useZoomPanHelper'
|
||||
import { Store } from '~/context/symbols'
|
||||
import { useZoom, useResizeHandler, useZoomPanHelper } from '~/composables'
|
||||
import { Hooks, Store } from '~/context'
|
||||
import { onLoadGetElements, onLoadProject, onLoadToObject } from '~/utils/graph'
|
||||
|
||||
interface ZoomPaneProps {
|
||||
selectionKeyCode?: KeyCode
|
||||
@@ -33,25 +32,25 @@ const props = withDefaults(defineProps<ZoomPaneProps>(), {
|
||||
paneMoveable: true,
|
||||
})
|
||||
const store = inject(Store)!
|
||||
const zoomPaneEl = templateRef<HTMLDivElement>('zoom-pane', null)
|
||||
const { transform, d3Selection, d3Zoom } = useZoom(zoomPaneEl, props, (initD3ZoomPayload) => store.initD3Zoom(initD3ZoomPayload))
|
||||
const hooks = inject(Hooks)!
|
||||
|
||||
const zoomPaneEl = templateRef<HTMLDivElement>('zoom-pane', null)
|
||||
const { transform } = useZoom(zoomPaneEl, props)
|
||||
watch(transform, (val) => (store.transform = val), { flush: 'post' })
|
||||
const dimensions = useResizeHandler(zoomPaneEl)
|
||||
watch(dimensions, (val) => (store.dimensions = val), { flush: 'post' })
|
||||
|
||||
const getZoomPanHelper = reactify(useZoomPanHelper)
|
||||
const zoomPanHelper = getZoomPanHelper(
|
||||
d3Zoom,
|
||||
d3Selection,
|
||||
store.nodes,
|
||||
transform,
|
||||
dimensions,
|
||||
store.minZoom,
|
||||
store.maxZoom,
|
||||
store.snapToGrid,
|
||||
store.snapGrid,
|
||||
)
|
||||
const { zoomIn, zoomOut, zoomTo, transform: setTransform, fitView } = useZoomPanHelper()
|
||||
hooks.load.trigger({
|
||||
fitView: (params = { padding: 0.1 }) => fitView(params),
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
zoomTo,
|
||||
setTransform,
|
||||
project: onLoadProject(store),
|
||||
getElements: onLoadGetElements(store),
|
||||
toObject: onLoadToObject(store),
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div ref="zoom-pane" class="revue-flow__renderer revue-flow__zoompane">
|
||||
|
||||
@@ -3,3 +3,4 @@ import { FlowHooks, FlowStore } from '~/types'
|
||||
|
||||
export const Store: InjectionKey<FlowStore> = Symbol('store')
|
||||
export const Hooks: InjectionKey<FlowHooks> = Symbol('hooks')
|
||||
export const NodeIdContextKey: InjectionKey<string> = Symbol('NodeIdContext')
|
||||
@@ -1,90 +0,0 @@
|
||||
import { zoomIdentity } from 'd3-zoom'
|
||||
import { getRectOfNodes, pointToRendererPoint, getTransformForBounds } from '~/utils/graph'
|
||||
import {
|
||||
FitViewParams,
|
||||
FlowTransform,
|
||||
ZoomPanHelperFunctions,
|
||||
Rect,
|
||||
XYPosition,
|
||||
Node,
|
||||
Transform,
|
||||
SnapGrid,
|
||||
Dimensions,
|
||||
D3Zoom,
|
||||
D3Selection,
|
||||
} from '~/types'
|
||||
|
||||
const DEFAULT_PADDING = 0.1
|
||||
|
||||
type UseZoomPanHelper = (
|
||||
d3Zoom: D3Zoom,
|
||||
d3Selection: D3Selection,
|
||||
nodes: Node[],
|
||||
transform: Transform,
|
||||
dimensions: Dimensions,
|
||||
minZoom: number,
|
||||
maxZoom: number,
|
||||
snapToGrid: boolean,
|
||||
snapGrid: SnapGrid,
|
||||
) => ZoomPanHelperFunctions
|
||||
|
||||
export default (function (
|
||||
d3Zoom,
|
||||
d3Selection,
|
||||
nodes = [],
|
||||
transform = [0, 0, 0],
|
||||
dimensions = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
minZoom = 0.5,
|
||||
maxZoom = 2,
|
||||
snapToGrid = false,
|
||||
snapGrid = [15, 15],
|
||||
): ZoomPanHelperFunctions {
|
||||
return {
|
||||
zoomIn: () => d3Zoom?.scaleBy(d3Selection, 1.2),
|
||||
zoomOut: () => d3Zoom?.scaleBy(d3Selection, 1 / 1.2),
|
||||
zoomTo: (zoomLevel: number) => d3Zoom.scaleTo(d3Selection, zoomLevel),
|
||||
transform: (transform: FlowTransform) => {
|
||||
const nextTransform = zoomIdentity.translate(transform.x, transform.y).scale(transform.zoom)
|
||||
|
||||
d3Zoom.transform(d3Selection, nextTransform)
|
||||
},
|
||||
fitView: (options: FitViewParams = { padding: DEFAULT_PADDING, includeHiddenNodes: false }) => {
|
||||
if (!nodes.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const bounds = getRectOfNodes(options.includeHiddenNodes ? nodes : nodes.filter((node) => !node.isHidden))
|
||||
const [x, y, zoom] = getTransformForBounds(
|
||||
bounds,
|
||||
dimensions.width,
|
||||
dimensions.height,
|
||||
options.minZoom || minZoom,
|
||||
options.maxZoom || maxZoom,
|
||||
options.padding || DEFAULT_PADDING,
|
||||
)
|
||||
const transform = zoomIdentity.translate(x, y).scale(zoom)
|
||||
|
||||
d3Zoom?.transform(d3Selection, transform)
|
||||
},
|
||||
setCenter: (x: number, y: number, zoom?: number) => {
|
||||
const nextZoom = typeof zoom !== 'undefined' ? zoom : maxZoom
|
||||
const centerX = dimensions.width / 2 - x * nextZoom
|
||||
const centerY = dimensions.height / 2 - y * nextZoom
|
||||
const transform = zoomIdentity.translate(centerX, centerY).scale(nextZoom)
|
||||
|
||||
d3Zoom.transform(d3Selection, transform)
|
||||
},
|
||||
fitBounds: (bounds: Rect, padding = DEFAULT_PADDING) => {
|
||||
const [x, y, zoom] = getTransformForBounds(bounds, dimensions.width, dimensions.height, minZoom, maxZoom, padding)
|
||||
const transform = zoomIdentity.translate(x, y).scale(zoom)
|
||||
|
||||
d3Zoom.transform(d3Selection, transform)
|
||||
},
|
||||
project: (position: XYPosition) => {
|
||||
return pointToRendererPoint(position, transform, snapToGrid, snapGrid)
|
||||
},
|
||||
}
|
||||
} as UseZoomPanHelper)
|
||||
@@ -15,8 +15,9 @@ export {
|
||||
getTransformForBounds,
|
||||
getRectOfNodes,
|
||||
} from './utils/graph'
|
||||
export { default as useZoomPanHelper } from './hooks/useZoomPanHelper'
|
||||
export { default as useUpdateNodeInternals } from './hooks/useUpdateNodeInternals'
|
||||
export { default as useZoomPanHelper } from './composables/useZoomPanHelper'
|
||||
export { default as useUpdateNodeInternals } from './composables/useUpdateNodeInternals'
|
||||
|
||||
export * from './additional-components'
|
||||
export * from './types'
|
||||
export * from './context'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { RevueFlowState, ConnectionMode } from '../types'
|
||||
import { ConnectionMode, FlowState } from '~/types'
|
||||
|
||||
export const initialState: RevueFlowState = {
|
||||
export const initialState: FlowState = {
|
||||
dimensions: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
|
||||
@@ -13,7 +13,7 @@ export enum PanOnScrollMode {
|
||||
Horizontal = 'horizontal',
|
||||
}
|
||||
|
||||
export interface ZoomPanHelperFunctions {
|
||||
export interface UseZoomPanHelper {
|
||||
zoomIn: () => void
|
||||
zoomOut: () => void
|
||||
zoomTo: (zoomLevel: number) => void
|
||||
|
||||
@@ -10,9 +10,32 @@ import {
|
||||
Connection,
|
||||
FlowExportObject,
|
||||
NodeExtent,
|
||||
RevueFlowStore,
|
||||
} from '../types'
|
||||
import { clampPosition, clamp } from './index'
|
||||
Dimensions,
|
||||
FlowStore,
|
||||
} from '~/types'
|
||||
|
||||
export const isInputDOMNode = (e: KeyboardEvent | MouseEvent): boolean => {
|
||||
const target = e.target as HTMLElement
|
||||
return ['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON'].includes(target.nodeName) || target.hasAttribute('contentEditable')
|
||||
}
|
||||
|
||||
export const getDimensions = (node: HTMLElement): Dimensions => ({
|
||||
width: node.offsetWidth,
|
||||
height: node.offsetHeight,
|
||||
})
|
||||
|
||||
export const clamp = (val: number, min = 0, max = 1): number => Math.min(Math.max(val, min), max)
|
||||
|
||||
export const clampPosition = (position: XYPosition, extent: NodeExtent): XYPosition => ({
|
||||
x: clamp(position.x, extent[0][0], extent[1][0]),
|
||||
y: clamp(position.y, extent[0][1], extent[1][1]),
|
||||
})
|
||||
|
||||
export const getHostForElement = (element: HTMLElement): Document => {
|
||||
const doc = element.getRootNode() as Document
|
||||
if ('getElementFromPoint' in doc) return doc
|
||||
else return window.document
|
||||
}
|
||||
|
||||
export const isEdge = (element: Node | Connection | Edge): element is Edge =>
|
||||
'id' in element && 'source' in element && 'target' in element
|
||||
@@ -136,7 +159,7 @@ export const pointToRendererPoint = (
|
||||
}
|
||||
|
||||
export const onLoadProject =
|
||||
(currentStore: RevueFlowStore) =>
|
||||
(currentStore: FlowStore) =>
|
||||
(position: XYPosition): XYPosition =>
|
||||
pointToRendererPoint(position, currentStore.transform, currentStore.snapToGrid, currentStore.snapGrid)
|
||||
|
||||
@@ -256,10 +279,10 @@ const parseElements = (nodes: Node[], edges: Edge[]): Elements => [
|
||||
...edges.map((e) => ({ ...e })),
|
||||
]
|
||||
|
||||
export const onLoadGetElements = (currentStore: RevueFlowStore) => (): Elements =>
|
||||
export const onLoadGetElements = (currentStore: FlowStore) => (): Elements =>
|
||||
parseElements(currentStore.nodes || [], currentStore.edges || [])
|
||||
|
||||
export const onLoadToObject = (currentStore: RevueFlowStore) => (): FlowExportObject => ({
|
||||
export const onLoadToObject = (currentStore: FlowStore) => (): FlowExportObject => ({
|
||||
elements: parseElements(currentStore.nodes || [], currentStore.edges || []),
|
||||
position: [currentStore.transform[0], currentStore.transform[1]],
|
||||
zoom: currentStore.transform[2],
|
||||
|
||||
@@ -1,24 +1 @@
|
||||
import { Dimensions, XYPosition, NodeExtent } from '../types'
|
||||
|
||||
export const isInputDOMNode = (e: KeyboardEvent | MouseEvent): boolean => {
|
||||
const target = e.target as HTMLElement
|
||||
return ['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON'].includes(target.nodeName) || target.hasAttribute('contentEditable')
|
||||
}
|
||||
|
||||
export const getDimensions = (node: HTMLElement): Dimensions => ({
|
||||
width: node.offsetWidth,
|
||||
height: node.offsetHeight,
|
||||
})
|
||||
|
||||
export const clamp = (val: number, min = 0, max = 1): number => Math.min(Math.max(val, min), max)
|
||||
|
||||
export const clampPosition = (position: XYPosition, extent: NodeExtent): XYPosition => ({
|
||||
x: clamp(position.x, extent[0][0], extent[1][0]),
|
||||
y: clamp(position.y, extent[0][1], extent[1][1]),
|
||||
})
|
||||
|
||||
export const getHostForElement = (element: HTMLElement): Document => {
|
||||
const doc = element.getRootNode() as Document
|
||||
if ('getElementFromPoint' in doc) return doc
|
||||
else return window.document
|
||||
}
|
||||
export * from './graph'
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { InjectionKey } from 'vue'
|
||||
import { RevueFlowStore } from '../types'
|
||||
import { RevueFlowHooks } from '../hooks/RevueFlowHooks'
|
||||
|
||||
export const StoreKey: InjectionKey<RevueFlowStore> = Symbol('store')
|
||||
export const HooksKey: InjectionKey<RevueFlowHooks> = Symbol('hooks')
|
||||
export const NodeIdContextKey: InjectionKey<string> = Symbol('NodeIdContext')
|
||||
Reference in New Issue
Block a user