fix: zoom pan helper

This commit is contained in:
Braks
2021-10-20 22:39:54 +02:00
parent a895853b64
commit c3fb3cd10a
38 changed files with 214 additions and 277 deletions
+4 -4
View File
@@ -60,13 +60,13 @@ const toggleClassnames = () => {
<Flow <Flow
class="revue-flow-basic-example" class="revue-flow-basic-example"
:elements="elements" :elements="elements"
:on-elements-remove="onElementsRemove"
:on-connect="onConnect"
:on-node-drag-stop="onNodeDragStop"
:on-node-click="onElementClick"
:default-zoom="1.5" :default-zoom="1.5"
:min-zoom="0.2" :min-zoom="0.2"
:max-zoom="4" :max-zoom="4"
@elements-remove="onElementsRemove"
@connect="onConnect"
@node-drag-stop="onNodeDragStop"
@node-click="onElementClick"
@elementClick="onElementClick" @elementClick="onElementClick"
@load="onLoad" @load="onLoad"
> >
@@ -15,7 +15,7 @@ const onElementsRemove = (elementsToRemove: Elements) =>
const onConnect = (params: Connection | Edge) => (elements.value = addEdge(params, elements.value as Elements)) const onConnect = (params: Connection | Edge) => (elements.value = addEdge(params, elements.value as Elements))
</script> </script>
<template> <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" /> <Background :variant="BackgroundVariant.Lines" />
</Flow> </Flow>
</template> </template>
+5 -5
View File
@@ -114,17 +114,17 @@ const onConnect = (params: Connection | Edge) =>
<template> <template>
<Flow <Flow
:elements="elements" :elements="elements"
:on-element-click="onElementClick"
:on-elements-remove="onElementsRemove"
:on-connect="onConnect"
:on-node-drag-stop="onNodeDragStop"
:style="`background: ${bgColor}`" :style="`background: ${bgColor}`"
:on-load="onLoad"
:node-types="nodeTypes" :node-types="nodeTypes"
:connection-line-style="connectionLineStyle" :connection-line-style="connectionLineStyle"
:snap-to-grid="true" :snap-to-grid="true"
:snap-grid="snapGrid" :snap-grid="snapGrid"
:default-zoom="1.5" :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" /> <MiniMap :node-stroke-color="nodeStroke" :node-color="nodeColor" />
<Controls /> <Controls />
+3 -13
View File
@@ -1,15 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { RevueFlowHooks } from '~/hooks/RevueFlowHooks' import { getEdgeCenter, getBezierPath, getMarkerEnd, ArrowHeadType, EdgeProps, ElementId, Position, Hooks, Store } from '~/index'
import {
getEdgeCenter,
getBezierPath,
getMarkerEnd,
ArrowHeadType,
EdgeProps,
ElementId,
Position,
RevueFlowStore,
} from '~/index'
interface CustomEdgeProps<T = any> extends EdgeProps<T> { interface CustomEdgeProps<T = any> extends EdgeProps<T> {
id: ElementId id: ElementId
@@ -25,8 +15,8 @@ interface CustomEdgeProps<T = any> extends EdgeProps<T> {
} }
const props = defineProps<CustomEdgeProps>() const props = defineProps<CustomEdgeProps>()
const store = inject<RevueFlowStore>('store')! const store = inject(Store)!
const hooks = inject<RevueFlowHooks>('hooks')! const hooks = inject(Hooks)!
const onEdgeClick = (evt: Event, id: string) => { const onEdgeClick = (evt: Event, id: string) => {
const edge = store.edges.find((edge) => edge.id === id) const edge = store.edges.find((edge) => edge.id === id)
if (edge) { if (edge) {
-20
View File
@@ -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>
-4
View File
@@ -45,10 +45,6 @@ export const routes: RouterOptions['routes'] = [
path: '/interaction', path: '/interaction',
component: () => import('./Interaction/InteractionExample.vue'), component: () => import('./Interaction/InteractionExample.vue'),
}, },
{
path: '/super-flow',
component: () => import('./Superflow/Superflow.vue'),
},
] ]
export const router = createRouter({ export const router = createRouter({
@@ -1,6 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { HTMLAttributes } from 'vue' import { HTMLAttributes } from 'vue'
import { BackgroundVariant, RevueFlowStore } from '~/types' import { BackgroundVariant } from '~/types'
import { Store } from '~/context'
export interface BackgroundProps extends HTMLAttributes { export interface BackgroundProps extends HTMLAttributes {
variant?: BackgroundVariant variant?: BackgroundVariant
@@ -20,14 +21,23 @@ const defaultColors: Record<BackgroundVariant, string> = {
[BackgroundVariant.Lines]: '#eee', [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. // 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 bgClasses = ['revue-flow__background']
const scaledGap = computed(() => props.gap && props.gap * store.transform[2]) const background = computed(() => {
const xOffset = computed(() => scaledGap.value && store.transform[0] % scaledGap.value) const scaledGap = props.gap && props.gap * store.transform[2]
const yOffset = computed(() => scaledGap.value && store.transform[1] % scaledGap.value) const xOffset = scaledGap && store.transform[0] % scaledGap
const size = computed(() => props.size || 0.4 * store.transform[2]) 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 isLines = props.variant === BackgroundVariant.Lines
const bgColor = props.color ? props.color : defaultColors[props.variant || BackgroundVariant.Dots] 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%', 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"> <template v-if="isLines">
<path <path
:stroke="bgColor" :stroke="bgColor"
:stroke-width="props.size" :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>
<template v-else> <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> </template>
</pattern> </pattern>
<rect x="0" y="0" width="100%" height="100%" :fill="`url(#${patternId})`" /> <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 Fitview from '@/assets/icons/fitview.svg'
import Lock from '@/assets/icons/lock.svg' import Lock from '@/assets/icons/lock.svg'
import Unlock from '@/assets/icons/unlock.svg' import Unlock from '@/assets/icons/unlock.svg'
import { FitViewParams, RevueFlowStore, ZoomPanHelperFunctions } from '~/types' import { FitViewParams } from '~/types'
import useZoomPanHelper from '~/hooks/useZoomPanHelper' import useZoomPanHelper from '~/composables/useZoomPanHelper'
import { Store } from '~/context'
export interface ControlProps extends HTMLAttributes { export interface ControlProps extends HTMLAttributes {
showZoom?: boolean showZoom?: boolean
@@ -26,25 +27,24 @@ const props = withDefaults(defineProps<ControlProps>(), {
showInteractive: true, showInteractive: true,
}) })
const store = inject<RevueFlowStore>('store')! const store = inject(Store)!
// const { onReady } = useZoomPanHelper() const { zoomIn, zoomOut, fitView } = useZoomPanHelper()
const isInteractive = store.nodesDraggable && store.nodesConnectable && store.elementsSelectable const isInteractive = store.nodesDraggable && store.nodesConnectable && store.elementsSelectable
const mapClasses = ['revue-flow__controls'] const mapClasses = ['revue-flow__controls']
/* const onZoomInHandler = () => {
* const onZoomInHandler = () => { zoomIn?.()
zoomHelper.value?.zoomIn?.()
props.onZoomIn?.() props.onZoomIn?.()
} }
const onZoomOutHandler = () => { const onZoomOutHandler = () => {
zoomHelper.value?.zoomOut?.() zoomOut?.()
props.onZoomOut?.() props.onZoomOut?.()
} }
const onFitViewHandler = () => { const onFitViewHandler = () => {
zoomHelper.value?.fitView?.(props.fitViewParams) fitView?.(props.fitViewParams)
props.onFitView?.() props.onFitView?.()
} }
@@ -52,7 +52,6 @@ const onInteractiveChangeHandler = () => {
store.setInteractive?.(!isInteractive) store.setInteractive?.(!isInteractive)
props.onInteractiveChange?.(!isInteractive) props.onInteractiveChange?.(!isInteractive)
} }
*/
</script> </script>
<template> <template>
<div :class="mapClasses"> <div :class="mapClasses">
@@ -2,7 +2,8 @@
import { HTMLAttributes } from 'vue' import { HTMLAttributes } from 'vue'
import MiniMapNode from './MiniMapNode.vue' import MiniMapNode from './MiniMapNode.vue'
import { getBoundsofRects, getRectOfNodes } from '~/utils/graph' import { getBoundsofRects, getRectOfNodes } from '~/utils/graph'
import { Node, RevueFlowStore } from '~/types' import { Node } from '~/types'
import { Store } from '~/context'
type StringFunc = (node: Node) => string type StringFunc = (node: Node) => string
@@ -30,7 +31,7 @@ declare const window: any
const defaultWidth = 200 const defaultWidth = 200
const defaultHeight = 150 const defaultHeight = 150
const store = inject<RevueFlowStore>('store')! const store = inject(Store)!
const elementWidth = attrs.style?.width ?? defaultWidth const elementWidth = attrs.style?.width ?? defaultWidth
const elementHeight = attrs.style?.height ?? defaultHeight const elementHeight = attrs.style?.height ?? defaultHeight
@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, CSSProperties } from 'vue' import { CSSProperties } from 'vue'
interface MiniMapNodeProps { interface MiniMapNodeProps {
x?: number x?: number
@@ -8,11 +8,13 @@ interface MiniMapNodeProps {
height?: number height?: number
borderRadius?: number borderRadius?: number
color?: string color?: string
shapeRendering?: string shapeRendering?: 'auto' | 'optimizeSpeed' | 'crispEdges' | 'geometricPrecision' | 'inherit'
strokeColor?: string strokeColor?: string
strokeWidth?: number strokeWidth?: number
} }
const props = defineProps<MiniMapNodeProps>() const props = withDefaults(defineProps<MiniMapNodeProps>(), {
shapeRendering: 'geometricPrecision',
})
const attrs = useAttrs() const attrs = useAttrs()
const styles = (attrs.style ?? {}) as CSSProperties const styles = (attrs.style ?? {}) as CSSProperties
@@ -2,7 +2,7 @@
import { CSSProperties } from 'vue' import { CSSProperties } from 'vue'
import { ConnectionLineType, CustomConnectionLine, HandleElement, Node, Position } from '~/types' import { ConnectionLineType, CustomConnectionLine, HandleElement, Node, Position } from '~/types'
import { getBezierPath, getSmoothStepPath } from '~/components/Edges/utils' import { getBezierPath, getSmoothStepPath } from '~/components/Edges/utils'
import { Hooks, Store } from '~/context/symbols' import { Hooks, Store } from '~/context'
interface ConnectionLineProps { interface ConnectionLineProps {
sourceNode: Node sourceNode: Node
+1 -1
View File
@@ -4,7 +4,7 @@ import { getEdgePositions, getHandle, getSourceTargetNodes, isEdgeVisible } from
import { isEdge } from '~/utils/graph' import { isEdge } from '~/utils/graph'
import { ConnectionMode, Dimensions, Edge, EdgeType, Elements, Position, Transform } from '~/types' import { ConnectionMode, Dimensions, Edge, EdgeType, Elements, Position, Transform } from '~/types'
import { onMouseDown } from '~/components/Handle/utils' import { onMouseDown } from '~/components/Handle/utils'
import { Hooks, Store } from '~/context/symbols' import { Hooks, Store } from '~/context'
interface EdgeProps { interface EdgeProps {
type: EdgeType type: EdgeType
+1 -1
View File
@@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ElementId, Position } from '~/types' import { ElementId, Position } from '~/types'
import { onMouseDown, ValidConnectionFunc } from '~/components/Handle/utils' import { onMouseDown, ValidConnectionFunc } from '~/components/Handle/utils'
import { Hooks, Store } from '~/context/symbols' import { Hooks, Store } from '~/context'
interface HandleProps { interface HandleProps {
id?: string id?: string
+7 -8
View File
@@ -1,6 +1,5 @@
import { getHostForElement } from '~/utils' import { getHostForElement } from '~/utils'
import { ElementId, ConnectionMode, Connection, HandleType, RevueFlowStore } from '~/types' import { ElementId, ConnectionMode, Connection, HandleType, FlowStore, FlowHooks } from '~/types'
import { RevueFlowHooks } from '~/hooks/RevueFlowHooks'
export type ValidConnectionFunc = (connection: Connection) => boolean export type ValidConnectionFunc = (connection: Connection) => boolean
@@ -71,13 +70,13 @@ function resetRecentHandle(hoveredHandle: Element): void {
export function onMouseDown( export function onMouseDown(
event: MouseEvent, event: MouseEvent,
store: RevueFlowStore, store: FlowStore,
hooks: { hooks: {
connectStart: RevueFlowHooks['connectStart'] connectStart: FlowHooks['connectStart']
connectStop: RevueFlowHooks['connectStop'] connectStop: FlowHooks['connectStop']
connectEnd: RevueFlowHooks['connectEnd'] connectEnd: FlowHooks['connectEnd']
connect: RevueFlowHooks['connect'] connect: FlowHooks['connect']
edgeUpdateEnd: RevueFlowHooks['edgeUpdateEnd'] edgeUpdateEnd: FlowHooks['edgeUpdateEnd']
}, },
handleId: ElementId, handleId: ElementId,
nodeId: ElementId, nodeId: ElementId,
+1 -1
View File
@@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { DraggableEventListener } from '@braks/revue-draggable' import { DraggableEventListener } from '@braks/revue-draggable'
import { Node, NodeDimensionUpdate, NodeType } from '~/types' import { Node, NodeDimensionUpdate, NodeType } from '~/types'
import { Hooks, Store } from '~/context/symbols' import { Hooks, Store } from '~/context'
interface NodeProps { interface NodeProps {
node: Node node: Node
@@ -2,7 +2,7 @@
import { DraggableEventListener } from '@braks/revue-draggable' import { DraggableEventListener } from '@braks/revue-draggable'
import { Node } from '~/types' import { Node } from '~/types'
import { isNode } from '~/utils/graph' import { isNode } from '~/utils/graph'
import { Hooks, Store } from '~/context/symbols' import { Hooks, Store } from '~/context'
interface NodesSelectionProps { interface NodesSelectionProps {
onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void
@@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import SelectionRect from './SelectionRect.vue' import SelectionRect from './SelectionRect.vue'
import { getMousePosition } from '~/components/UserSelection/utils' import { getMousePosition } from '~/components/UserSelection/utils'
import { Store } from '~/context/symbols' import { Store } from '~/context'
const store = inject(Store)! const store = inject(Store)!
const el = templateRef('user-selection', null) const el = templateRef('user-selection', null)
+7
View 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 useKeyPress from './useKeyPress'
import { isNode, getConnectedEdges } from '~/utils/graph' import { isNode, getConnectedEdges } from '~/utils/graph'
import { Elements, KeyCode, ElementId, FlowElement } from '~/types' import { Elements, KeyCode, ElementId, FlowElement } from '~/types'
import { Store } from '~/context/symbols' import { Store } from '~/context'
interface HookParams { interface HookParams {
deleteKeyCode: KeyCode deleteKeyCode: KeyCode
@@ -1,7 +1,7 @@
// global hooks // global hooks
import { Connection, Edge, Elements, FlowHooks, FlowTransform, Node, OnConnectStartParams, OnLoadParams } from '~/types' 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 = { const eventHooks: FlowHooks = {
elementClick: createEventHook<{ event: MouseEvent; element: Node | Edge }>(), elementClick: createEventHook<{ event: MouseEvent; element: Node | Edge }>(),
elementsRemove: createEventHook<Elements>(), elementsRemove: createEventHook<Elements>(),
+13 -33
View File
@@ -2,20 +2,10 @@ import { D3ZoomEvent, zoom, zoomIdentity, ZoomTransform } from 'd3-zoom'
import { pointer, select } from 'd3-selection' import { pointer, select } from 'd3-selection'
import { Ref } from 'vue' import { Ref } from 'vue'
import { get } from '@vueuse/core' import { get } from '@vueuse/core'
import { import { D3Selection, D3Zoom, FlowTransform, KeyCode, PanOnScrollMode, Transform } from '~/types'
D3Selection,
D3Zoom,
FlowTransform,
InitD3ZoomPayload,
KeyCode,
PanOnScrollMode,
Transform,
TranslateExtent,
} from '~/types'
import { clamp } from '~/utils' import { clamp } from '~/utils'
import useKeyPress from '~/hooks/useKeyPress' import useKeyPress from '~/composables/useKeyPress'
import { Hooks } from '~/context/symbols' import { Hooks, Store } from '~/context'
import { onLoadGetElements, onLoadProject, onLoadToObject } from '~/utils/graph'
const viewChanged = (prevTransform: FlowTransform, eventTransform: ZoomTransform): boolean => const viewChanged = (prevTransform: FlowTransform, eventTransform: ZoomTransform): boolean =>
prevTransform.x !== eventTransform.x || prevTransform.y !== eventTransform.y || prevTransform.zoom !== eventTransform.k prevTransform.x !== eventTransform.x || prevTransform.y !== eventTransform.y || prevTransform.zoom !== eventTransform.k
@@ -30,11 +20,8 @@ interface UseZoomOptions {
selectionKeyCode?: KeyCode selectionKeyCode?: KeyCode
zoomActivationKeyCode?: KeyCode zoomActivationKeyCode?: KeyCode
paneMoveable?: boolean paneMoveable?: boolean
minZoom?: number
maxZoom?: number
defaultZoom?: number defaultZoom?: number
defaultPosition?: [number, number] defaultPosition?: [number, number]
translateExtent?: TranslateExtent
zoomOnScroll?: boolean zoomOnScroll?: boolean
zoomOnPinch?: boolean zoomOnPinch?: boolean
panOnScroll?: boolean panOnScroll?: boolean
@@ -49,22 +36,12 @@ interface UseZoom {
d3Selection: Ref<D3Selection> d3Selection: Ref<D3Selection>
} }
export default function ( export default function (el: Ref<HTMLDivElement>, options: UseZoomOptions): UseZoom {
el: Ref<HTMLDivElement>,
options: UseZoomOptions,
init: (initD3ZoomPayload: InitD3ZoomPayload) => void = () => {},
): UseZoom {
const { const {
selectionKeyCode = 'Shift', selectionKeyCode = 'Shift',
zoomActivationKeyCode = 'Meta', zoomActivationKeyCode = 'Meta',
minZoom = 0.5,
maxZoom = 2,
defaultZoom = 1, defaultZoom = 1,
defaultPosition = [0, 0], defaultPosition = [0, 0],
translateExtent = [
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
],
zoomOnScroll = true, zoomOnScroll = true,
zoomOnPinch = true, zoomOnPinch = true,
zoomOnDoubleClick = true, zoomOnDoubleClick = true,
@@ -74,13 +51,17 @@ export default function (
paneMoveable = true, paneMoveable = true,
} = options } = options
const hooks = inject(Hooks)! const hooks = inject(Hooks)!
const store = inject(Store)!
const prevTransform = ref<FlowTransform>({ x: 0, y: 0, zoom: 0 }) const prevTransform = ref<FlowTransform>({ x: 0, y: 0, zoom: 0 })
const clampedX = clamp(defaultPosition[0], translateExtent[0][0], translateExtent[1][0]) const clampedX = clamp(defaultPosition[0], store.translateExtent[0][0], store.translateExtent[1][0])
const clampedY = clamp(defaultPosition[1], translateExtent[0][1], translateExtent[1][1]) const clampedY = clamp(defaultPosition[1], store.translateExtent[0][1], store.translateExtent[1][1])
const clampedZoom = clamp(defaultZoom, minZoom, maxZoom) const clampedZoom = clamp(defaultZoom, store.minZoom, store.maxZoom)
const transform = ref<Transform>([clampedX, clampedY, clampedZoom]) 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() const d3Selection = ref()
until(el) until(el)
@@ -93,8 +74,7 @@ export default function (
const updatedTransform = zoomIdentity.translate(clampedX, clampedY).scale(clampedZoom) const updatedTransform = zoomIdentity.translate(clampedX, clampedY).scale(clampedZoom)
d3z.transform(d3s, updatedTransform) d3z.transform(d3s, updatedTransform)
init({ d3Zoom: d3z, d3ZoomHandler, d3Selection: d3s }) store.initD3Zoom({ d3Zoom: d3z, d3Selection: d3s, d3ZoomHandler })
const applyZoomHandlers = () => { const applyZoomHandlers = () => {
d3z.on('start', (event: D3ZoomEvent<HTMLDivElement, any>) => { d3z.on('start', (event: D3ZoomEvent<HTMLDivElement, any>) => {
if (viewChanged(prevTransform.value, event.transform)) { if (viewChanged(prevTransform.value, event.transform)) {
+62
View 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)
},
}
}
+1 -1
View File
@@ -5,7 +5,7 @@ import MarkerDefinitions from './MarkerDefinitions.vue'
import Edge from '~/components/Edges/Edge.vue' import Edge from '~/components/Edges/Edge.vue'
import ConnectionLine from '~/components/ConnectionLine/ConnectionLine.vue' import ConnectionLine from '~/components/ConnectionLine/ConnectionLine.vue'
import { ConnectionLineType, ConnectionMode, CustomConnectionLine, Dimensions, EdgeType, Transform } from '~/types' import { ConnectionLineType, ConnectionMode, CustomConnectionLine, Dimensions, EdgeType, Transform } from '~/types'
import { Store } from '~/context/symbols' import { Store } from '~/context'
interface EdgeRendererProps { interface EdgeRendererProps {
edgeTypes: Record<string, EdgeType> edgeTypes: Record<string, EdgeType>
+2 -2
View File
@@ -24,8 +24,8 @@ import { DefaultNode, InputNode, OutputNode } from '~/components/Nodes'
import { BezierEdge, SmoothStepEdge, StepEdge, StraightEdge } from '~/components/Edges' import { BezierEdge, SmoothStepEdge, StepEdge, StraightEdge } from '~/components/Edges'
import { createEdgeTypes } from '~/container/EdgeRenderer/utils' import { createEdgeTypes } from '~/container/EdgeRenderer/utils'
import { createNodeTypes } from '~/container/NodeRenderer/utils' import { createNodeTypes } from '~/container/NodeRenderer/utils'
import { useHooks } from '~/hooks/RevueFlowHooks' import { useHooks } from '~/composables'
import { Hooks, Store } from '~/context/symbols' import { Hooks, Store } from '~/context'
export interface FlowProps { export interface FlowProps {
elements: Elements elements: Elements
+1 -1
View File
@@ -2,7 +2,7 @@
import Node from '~/components/Nodes/Node.vue' import Node from '~/components/Nodes/Node.vue'
import { NodeType, Node as TNode, Transform, Dimensions } from '~/types' import { NodeType, Node as TNode, Transform, Dimensions } from '~/types'
import { getNodesInside } from '~/utils/graph' import { getNodesInside } from '~/utils/graph'
import { Store } from '~/context/symbols' import { Store } from '~/context'
interface NodeRendererProps { interface NodeRendererProps {
nodeTypes: Record<string, NodeType> nodeTypes: Record<string, NodeType>
@@ -1,10 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import useKeyPress from '~/hooks/useKeyPress' import useKeyPress from '~/composables/useKeyPress'
import { KeyCode } from '~/types' import { KeyCode } from '~/types'
import useGlobalKeyHandler from '~/hooks/useGlobalKeyHandler' import useGlobalKeyHandler from '~/composables/useGlobalKeyHandler'
import NodesSelection from '~/components/NodesSelection/NodesSelection.vue' import NodesSelection from '~/components/NodesSelection/NodesSelection.vue'
import UserSelection from '~/components/UserSelection/UserSelection.vue' import UserSelection from '~/components/UserSelection/UserSelection.vue'
import { Hooks, Store } from '~/context/symbols' import { Hooks, Store } from '~/context'
interface SelectionPaneProps { interface SelectionPaneProps {
selectionKeyCode?: KeyCode selectionKeyCode?: KeyCode
+17 -18
View File
@@ -1,9 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { KeyCode, PanOnScrollMode } from '~/types' import { KeyCode, PanOnScrollMode } from '~/types'
import useZoom from '~/composables/useZoom' import { useZoom, useResizeHandler, useZoomPanHelper } from '~/composables'
import useResizeHandler from '~/hooks/useResizeHandler' import { Hooks, Store } from '~/context'
import useZoomPanHelper from '~/hooks/useZoomPanHelper' import { onLoadGetElements, onLoadProject, onLoadToObject } from '~/utils/graph'
import { Store } from '~/context/symbols'
interface ZoomPaneProps { interface ZoomPaneProps {
selectionKeyCode?: KeyCode selectionKeyCode?: KeyCode
@@ -33,25 +32,25 @@ const props = withDefaults(defineProps<ZoomPaneProps>(), {
paneMoveable: true, paneMoveable: true,
}) })
const store = inject(Store)! const store = inject(Store)!
const zoomPaneEl = templateRef<HTMLDivElement>('zoom-pane', null) const hooks = inject(Hooks)!
const { transform, d3Selection, d3Zoom } = useZoom(zoomPaneEl, props, (initD3ZoomPayload) => store.initD3Zoom(initD3ZoomPayload))
const zoomPaneEl = templateRef<HTMLDivElement>('zoom-pane', null)
const { transform } = useZoom(zoomPaneEl, props)
watch(transform, (val) => (store.transform = val), { flush: 'post' }) watch(transform, (val) => (store.transform = val), { flush: 'post' })
const dimensions = useResizeHandler(zoomPaneEl) const dimensions = useResizeHandler(zoomPaneEl)
watch(dimensions, (val) => (store.dimensions = val), { flush: 'post' }) watch(dimensions, (val) => (store.dimensions = val), { flush: 'post' })
const getZoomPanHelper = reactify(useZoomPanHelper) const { zoomIn, zoomOut, zoomTo, transform: setTransform, fitView } = useZoomPanHelper()
const zoomPanHelper = getZoomPanHelper( hooks.load.trigger({
d3Zoom, fitView: (params = { padding: 0.1 }) => fitView(params),
d3Selection, zoomIn,
store.nodes, zoomOut,
transform, zoomTo,
dimensions, setTransform,
store.minZoom, project: onLoadProject(store),
store.maxZoom, getElements: onLoadGetElements(store),
store.snapToGrid, toObject: onLoadToObject(store),
store.snapGrid, })
)
</script> </script>
<template> <template>
<div ref="zoom-pane" class="revue-flow__renderer revue-flow__zoompane"> <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 Store: InjectionKey<FlowStore> = Symbol('store')
export const Hooks: InjectionKey<FlowHooks> = Symbol('hooks') export const Hooks: InjectionKey<FlowHooks> = Symbol('hooks')
export const NodeIdContextKey: InjectionKey<string> = Symbol('NodeIdContext')
-90
View File
@@ -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)
+3 -2
View File
@@ -15,8 +15,9 @@ export {
getTransformForBounds, getTransformForBounds,
getRectOfNodes, getRectOfNodes,
} from './utils/graph' } from './utils/graph'
export { default as useZoomPanHelper } from './hooks/useZoomPanHelper' export { default as useZoomPanHelper } from './composables/useZoomPanHelper'
export { default as useUpdateNodeInternals } from './hooks/useUpdateNodeInternals' export { default as useUpdateNodeInternals } from './composables/useUpdateNodeInternals'
export * from './additional-components' export * from './additional-components'
export * from './types' export * from './types'
export * from './context'
+2 -2
View File
@@ -1,6 +1,6 @@
import { RevueFlowState, ConnectionMode } from '../types' import { ConnectionMode, FlowState } from '~/types'
export const initialState: RevueFlowState = { export const initialState: FlowState = {
dimensions: { dimensions: {
width: 0, width: 0,
height: 0, height: 0,
+1 -1
View File
@@ -13,7 +13,7 @@ export enum PanOnScrollMode {
Horizontal = 'horizontal', Horizontal = 'horizontal',
} }
export interface ZoomPanHelperFunctions { export interface UseZoomPanHelper {
zoomIn: () => void zoomIn: () => void
zoomOut: () => void zoomOut: () => void
zoomTo: (zoomLevel: number) => void zoomTo: (zoomLevel: number) => void
+29 -6
View File
@@ -10,9 +10,32 @@ import {
Connection, Connection,
FlowExportObject, FlowExportObject,
NodeExtent, NodeExtent,
RevueFlowStore, Dimensions,
} from '../types' FlowStore,
import { clampPosition, clamp } from './index' } 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 => export const isEdge = (element: Node | Connection | Edge): element is Edge =>
'id' in element && 'source' in element && 'target' in element 'id' in element && 'source' in element && 'target' in element
@@ -136,7 +159,7 @@ export const pointToRendererPoint = (
} }
export const onLoadProject = export const onLoadProject =
(currentStore: RevueFlowStore) => (currentStore: FlowStore) =>
(position: XYPosition): XYPosition => (position: XYPosition): XYPosition =>
pointToRendererPoint(position, currentStore.transform, currentStore.snapToGrid, currentStore.snapGrid) pointToRendererPoint(position, currentStore.transform, currentStore.snapToGrid, currentStore.snapGrid)
@@ -256,10 +279,10 @@ const parseElements = (nodes: Node[], edges: Edge[]): Elements => [
...edges.map((e) => ({ ...e })), ...edges.map((e) => ({ ...e })),
] ]
export const onLoadGetElements = (currentStore: RevueFlowStore) => (): Elements => export const onLoadGetElements = (currentStore: FlowStore) => (): Elements =>
parseElements(currentStore.nodes || [], currentStore.edges || []) parseElements(currentStore.nodes || [], currentStore.edges || [])
export const onLoadToObject = (currentStore: RevueFlowStore) => (): FlowExportObject => ({ export const onLoadToObject = (currentStore: FlowStore) => (): FlowExportObject => ({
elements: parseElements(currentStore.nodes || [], currentStore.edges || []), elements: parseElements(currentStore.nodes || [], currentStore.edges || []),
position: [currentStore.transform[0], currentStore.transform[1]], position: [currentStore.transform[0], currentStore.transform[1]],
zoom: currentStore.transform[2], zoom: currentStore.transform[2],
+1 -24
View File
@@ -1,24 +1 @@
import { Dimensions, XYPosition, NodeExtent } from '../types' export * from './graph'
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
}
-7
View File
@@ -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')