refactor(flow!): Change transform to viewport
* Change array type to object type and rename transform type to viewport * Update examples & docs
This commit is contained in:
+1
-1
@@ -9,7 +9,7 @@
|
||||
"build": "vuepress build src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@braks/vue-flow": "^0.4.2-0",
|
||||
"@braks/vue-flow": "^0.4.2-3",
|
||||
"blobity": "^0.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -331,9 +331,9 @@ const edges = ref([
|
||||
|
||||
- Details:
|
||||
|
||||
Trigger fit view when transformation-pane is mounted.
|
||||
Trigger fit view when viewport is mounted.
|
||||
|
||||
## Zoom & Pan Options
|
||||
## Viewport Options
|
||||
|
||||
### zoom-activation-key-code <Badge class="text-white" style="line-height: inherit" text="optional" vertical="top" />
|
||||
|
||||
@@ -475,7 +475,7 @@ const edges = ref([
|
||||
|
||||
- Details:
|
||||
|
||||
Default zoom pane position on initial load.
|
||||
Default viewport position on initial load.
|
||||
|
||||
### translate-extent <Badge class="text-white" style="line-height: inherit" text="optional" vertical="top" />
|
||||
|
||||
@@ -492,7 +492,7 @@ const edges = ref([
|
||||
|
||||
- Details:
|
||||
|
||||
The area in which the zoom pane can be moved around.
|
||||
The area in which the viewport can be moved around.
|
||||
|
||||
## Selection Pane Options
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ title: Introduction
|
||||
# Introduction
|
||||
|
||||
Vue Flow is a library for building node-based applications. These can be simple static diagrams or complex node-based
|
||||
editors. You can implement custom nodes and edges. It also comes with components like a mini-map, zoom pane controls and
|
||||
editors. You can implement custom nodes and edges. It also comes with components like a mini-map, viewport controls and
|
||||
a background.
|
||||
|
||||
Check out the [examples](/examples/) if you want to dive directly into the code.
|
||||
|
||||
@@ -1,349 +0,0 @@
|
||||
# Utilities
|
||||
|
||||
Vue Flow comes with built-in utilities to control the zoom-pane, state and more.
|
||||
|
||||
## Graph Utilities
|
||||
|
||||
### isEdge
|
||||
|
||||
- Details:
|
||||
|
||||
Confirms if an element is an edge.
|
||||
|
||||
- Example:
|
||||
|
||||
```vue:no-line-numbers{13}
|
||||
<script setup>
|
||||
import { VueFlow, isEdge } from '@braks/vue-flow'
|
||||
|
||||
const elements = ref([
|
||||
{ id: '1', label: 'Node 1', position: { x: 250, y: 5 }, },
|
||||
{ id: '2', label: 'Node 2', position: { x: 100, y: 100 }, },
|
||||
|
||||
{ id: 'e1-2', source: '1', target: '2', class: 'light' },
|
||||
])
|
||||
|
||||
const toggleClass = () => {
|
||||
elements.value.forEach((el) => {
|
||||
if (isEdge(el)) {
|
||||
el.class = el.class === 'light' ? 'dark' : 'light'
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<VueFlow v-model="elements">
|
||||
<button style="position: absolute; top: 10px; right: 10px;" @click="toggleClass">Toggle classes</button>
|
||||
</VueFlow>
|
||||
</template>
|
||||
```
|
||||
|
||||
### isNode
|
||||
|
||||
- Details:
|
||||
|
||||
Confirms if an element is a node.
|
||||
|
||||
- Example:
|
||||
|
||||
```vue:no-line-numbers{13}
|
||||
<script setup>
|
||||
import { VueFlow, isNode } from '@braks/vue-flow'
|
||||
|
||||
const elements = ref([
|
||||
{ id: '1', label: 'Node 1', position: { x: 250, y: 5 }, class: 'light' },
|
||||
{ id: '2', label: 'Node 2', position: { x: 100, y: 100 }, class: 'light' },
|
||||
|
||||
{ id: 'e1-2', source: '1', target: '2' },
|
||||
])
|
||||
|
||||
const toggleClass = () => {
|
||||
elements.value.forEach((el) => {
|
||||
if (isNode(el)) {
|
||||
el.class = el.class === 'light' ? 'dark' : 'light'
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<VueFlow v-model="elements">
|
||||
<button style="position: absolute; top: 10px; right: 10px;" @click="toggleClass">Toggle classes</button>
|
||||
</VueFlow>
|
||||
</template>
|
||||
```
|
||||
|
||||
### addEdge
|
||||
|
||||
::: warning
|
||||
When using composition you should access `addEdges` from `useVueFlow`
|
||||
:::
|
||||
|
||||
- Details:
|
||||
|
||||
Confirms if an element is a node.
|
||||
|
||||
- Example:
|
||||
|
||||
```vue:no-line-numbers{12}
|
||||
<script setup>
|
||||
import { VueFlow, addEdge } from '@braks/vue-flow'
|
||||
|
||||
const elements = ref([
|
||||
{ id: '1', label: 'Node 1', position: { x: 250, y: 5 }, class: 'light' },
|
||||
{ id: '2', label: 'Node 2', position: { x: 100, y: 100 }, class: 'light' },
|
||||
|
||||
{ id: 'e1-2', source: '1', target: '2' },
|
||||
])
|
||||
|
||||
const onConnect = (params) => {
|
||||
addEdge(params, elements.value)
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<VueFlow v-model="elements" @connect="onConnect" />
|
||||
</template>
|
||||
```
|
||||
|
||||
### updateEdge
|
||||
|
||||
::: warning
|
||||
When using composition you should access `updateEdge` from `useVueFlow`
|
||||
:::
|
||||
|
||||
- Details:
|
||||
|
||||
Updates an edge and applies new source/target.
|
||||
|
||||
- Example:
|
||||
|
||||
```vue:no-line-numbers{12}
|
||||
<script setup>
|
||||
import { VueFlow, updateEdge } from '@braks/vue-flow'
|
||||
|
||||
const elements = ref([
|
||||
{ id: '1', label: 'Node 1', position: { x: 250, y: 5 }, class: 'light' },
|
||||
{ id: '2', label: 'Node 2', position: { x: 100, y: 100 }, class: 'light' },
|
||||
|
||||
{ id: 'e1-2', source: '1', target: '2' },
|
||||
])
|
||||
|
||||
const onEdgeUpdate = ({ edge, connection }) => {
|
||||
elements.value = updateEdge(edge, connection, elements.value)
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<VueFlow v-model="elements" @edge-update="onEdgeUpdate" />
|
||||
</template>
|
||||
```
|
||||
|
||||
### [getOutgoers](https://types.vueflow.dev/modules.html#getOutgoers)
|
||||
|
||||
- Details:
|
||||
|
||||
Returns all target elements of a node.
|
||||
|
||||
### [getIncomers](https://types.vueflow.dev/modules.html#getIncomers)
|
||||
|
||||
- Details:
|
||||
|
||||
Returns all source elements of a node.
|
||||
|
||||
### [getConnectedEdges](https://types.vueflow.dev/modules.html#getConnectedEdges)
|
||||
|
||||
- Details:
|
||||
|
||||
Returns all connected edges of a node.
|
||||
|
||||
### [getTransformForBounds](https://types.vueflow.dev/modules.html#getTransformForBounds)
|
||||
|
||||
- Details:
|
||||
|
||||
Returns a transformation for the zoom pane according to input bounds.
|
||||
|
||||
### [getRectOfNodes](https://types.vueflow.dev/modules.html#getRectOfNodes)
|
||||
|
||||
- Details:
|
||||
|
||||
Returns a rect of node elements.
|
||||
|
||||
Useful when you need to know the boundaries of a set of nodes.
|
||||
|
||||
### [getNodesInside](https://types.vueflow.dev/modules.html#getNodesInside)
|
||||
|
||||
- Details:
|
||||
|
||||
Returns node elements that are inside a specified rect.
|
||||
|
||||
### [getMarkerId](https://types.vueflow.dev/modules.html#getMarkerId)
|
||||
|
||||
- Details:
|
||||
|
||||
Returns a marker id for a marker definition.
|
||||
|
||||
## Instance
|
||||
|
||||
The Vue Flow instance provides easy access to zoom-pan-helper functions.
|
||||
It can be accessed either directly from the state using `useVueFlow` or
|
||||
you can receive the instance with a `onPaneReady` event handler.
|
||||
|
||||
|
||||
<CodeGroup>
|
||||
<CodeGroupItem title="Composition API" active>
|
||||
|
||||
```vue:no-line-numbers
|
||||
<script setup>
|
||||
import { VueFlow, useVueFlow } from '@braks/vue-flow'
|
||||
|
||||
const { onPaneReady, instance } = useVueFlow()
|
||||
|
||||
// event handler
|
||||
onPaneReady((instance) => instance.fitView())
|
||||
|
||||
onMounted(() => {
|
||||
// or directly try to access the instance
|
||||
instance.value?.fitView()
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
</CodeGroupItem>
|
||||
|
||||
|
||||
<CodeGroupItem title="Options API">
|
||||
|
||||
```vue:no-line-numbers
|
||||
<script>
|
||||
import { VueFlow } from '@braks/vue-flow'
|
||||
|
||||
export default defineComponent({
|
||||
components: { VueFlow },
|
||||
data() {
|
||||
return {
|
||||
instance: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onPaneReady(vueFlowInstance) {
|
||||
vueFlowInstance.fitView()
|
||||
this.instance = vueFlowInstance
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<VueFlow @pane-ready="onPaneReady" />
|
||||
</template>
|
||||
```
|
||||
</CodeGroupItem>
|
||||
</CodeGroup>
|
||||
|
||||
### [project](https://types.vueflow.dev/modules.html#Project)
|
||||
|
||||
- Details:
|
||||
|
||||
Transforms pixel coordinates to the internal VueFlow coordinate system.
|
||||
|
||||
This can be used when you drag nodes (from a sidebar for example) and need the internal position on the pane.
|
||||
|
||||
- Example:
|
||||
|
||||
```ts:no-line-numbers
|
||||
vueFlowInstance.project({ x: 100, y: 100 })
|
||||
```
|
||||
|
||||
### [fitView](https://types.vueflow.dev/modules.html#FitView)
|
||||
|
||||
- Details:
|
||||
|
||||
Fits the view port so that all nodes are visible.
|
||||
|
||||
Padding is 0.1 and includeHiddenNodes is false by default.
|
||||
|
||||
- Example:
|
||||
|
||||
```ts:no-line-numbers
|
||||
vueFlowInstance.fitView({ padding: 0.25, includeHiddenNodes: true })
|
||||
```
|
||||
|
||||
### [fitBounds](https://types.vueflow.dev/modules.html#FitBounds)
|
||||
|
||||
- Details:
|
||||
|
||||
Fits the view port according to the bounds' rect input.
|
||||
|
||||
- Example:
|
||||
|
||||
```ts:no-line-numbers
|
||||
vueFlowInstance.fitBounds(getRectOfNodes(nodes.value))
|
||||
```
|
||||
|
||||
### [setTransform](https://types.vueflow.dev/modules.html#SetTransform)
|
||||
|
||||
- Details:
|
||||
|
||||
Sets position and zoom of the pane.
|
||||
|
||||
- Example:
|
||||
|
||||
```ts:no-line-numbers
|
||||
vueFlowInstance.setTransform({ x: 100, y: 100, zoom: 1.5 })
|
||||
```
|
||||
|
||||
### [getTransform](https://types.vueflow.dev/modules.html#GetTransform)
|
||||
|
||||
- Details:
|
||||
|
||||
Gets position and zoom of the pane.
|
||||
|
||||
### [zoomIn](https://types.vueflow.dev/modules.html#ZoomInOut)
|
||||
|
||||
- Details:
|
||||
|
||||
Zooms in.
|
||||
|
||||
|
||||
### [zoomOut](https://types.vueflow.dev/modules.html#ZoomInOut)
|
||||
|
||||
- Details:
|
||||
|
||||
Zooms out.
|
||||
|
||||
### [zoomTo](https://types.vueflow.dev/modules.html#ZoomTo)
|
||||
|
||||
- Details:
|
||||
|
||||
Zooms to specific level.
|
||||
|
||||
### getElements
|
||||
|
||||
- Details:
|
||||
|
||||
Returns currently stored elements (nodes + edges).
|
||||
|
||||
### getNodes
|
||||
|
||||
- Details:
|
||||
|
||||
Returns currently stored nodes.
|
||||
|
||||
### getEdges
|
||||
|
||||
- Details:
|
||||
|
||||
Returns currently stored edges.
|
||||
|
||||
### toObject
|
||||
|
||||
- Details:
|
||||
|
||||
Returns elements, position and zoom of the current flow state.
|
||||
|
||||
- Example:
|
||||
|
||||
```ts:no-line-numbers
|
||||
toObject = (): {
|
||||
elements: FlowElements,
|
||||
position: [x, y],
|
||||
zoom: scale,
|
||||
}
|
||||
```
|
||||
@@ -154,7 +154,7 @@ const onEdgeUpdate = ({ edge, connection }) => {
|
||||
|
||||
- Details:
|
||||
|
||||
Returns a transformation for the zoom pane according to input bounds.
|
||||
Returns a transformation for the viewport according to input bounds.
|
||||
|
||||
### getRectOfNodes
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"serve": "vite serve"
|
||||
},
|
||||
"dependencies": {
|
||||
"@braks/vue-flow": "^0.4.2-0"
|
||||
"@braks/vue-flow": "^0.4.2-3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dagre": "^0.7.46",
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Handle, Position, getNodesInside, useVueFlow } from '@braks/vue-flow'
|
||||
import type { NodeProps } from '@braks/vue-flow'
|
||||
|
||||
const props = defineProps<NodeProps>()
|
||||
const { onNodeDragStop, getNodes, transform } = useVueFlow()
|
||||
const { onNodeDragStop, getNodes, viewport } = useVueFlow()
|
||||
onNodeDragStop(({ node }) => {
|
||||
const nodes = getNodesInside(
|
||||
getNodes.value,
|
||||
@@ -12,7 +12,7 @@ onNodeDragStop(({ node }) => {
|
||||
x: props.computedPosition.x,
|
||||
y: props.computedPosition.y,
|
||||
},
|
||||
transform.value,
|
||||
viewport.value,
|
||||
)
|
||||
if (nodes.some((n) => n.id === node.id && n.id !== props.id)) {
|
||||
node.label = `In ${props.id}`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVueFlow } from '@braks/vue-flow'
|
||||
const { nodesSelectionActive, addSelectedNodes, getNodes, transform } = useVueFlow()
|
||||
const { nodesSelectionActive, addSelectedNodes, getNodes, viewport } = useVueFlow()
|
||||
|
||||
const selectAll = () => {
|
||||
addSelectedNodes(getNodes.value)
|
||||
@@ -14,7 +14,7 @@ const selectAll = () => {
|
||||
</div>
|
||||
<div class="title">Zoom & pan transform</div>
|
||||
<div class="transform">
|
||||
{{ [transform[0].toFixed(2), transform[1].toFixed(2), transform[2].toFixed(2)] }}
|
||||
{{ [viewport.x.toFixed(2), viewport.y.toFixed(2), viewport.zoom.toFixed(2)] }}
|
||||
</div>
|
||||
<div class="title">Nodes</div>
|
||||
<div v-for="node of getNodes" :key="node.id">
|
||||
|
||||
@@ -21,10 +21,10 @@ const defaultColors: Record<BackgroundVariant, string> = {
|
||||
const { store } = useVueFlow()
|
||||
|
||||
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]
|
||||
const scaledGap = props.gap && props.gap * store.viewport.zoom
|
||||
const xOffset = scaledGap && store.viewport.x % scaledGap
|
||||
const yOffset = scaledGap && store.viewport.y % scaledGap
|
||||
const size = props.size || 0.4 * store.viewport.zoom
|
||||
|
||||
return {
|
||||
scaledGap,
|
||||
|
||||
@@ -28,16 +28,17 @@ const nodeColorFunc: MiniMapNodeFunc = props.nodeColor instanceof Function ? pro
|
||||
const nodeStrokeColorFunc: MiniMapNodeFunc =
|
||||
props.nodeStrokeColor instanceof Function ? props.nodeStrokeColor : () => props.nodeStrokeColor as string
|
||||
|
||||
const nodeClassNameFunc = props.nodeClassName instanceof Function ? props.nodeClassName : () => props.nodeClassName as MiniMapNodeFunc
|
||||
const nodeClassNameFunc =
|
||||
props.nodeClassName instanceof Function ? props.nodeClassName : () => props.nodeClassName as MiniMapNodeFunc
|
||||
|
||||
const shapeRendering: ShapeRendering = typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision'
|
||||
|
||||
const bb = computed(() => getRectOfNodes(store.getNodes))
|
||||
const viewBB = computed(() => ({
|
||||
x: -store.transform[0] / store.transform[2],
|
||||
y: -store.transform[1] / store.transform[2],
|
||||
width: store.dimensions.width / store.transform[2],
|
||||
height: store.dimensions.height / store.transform[2],
|
||||
x: -store.viewport.x / store.viewport.zoom,
|
||||
y: -store.viewport.y / store.viewport.zoom,
|
||||
width: store.dimensions.width / store.viewport.zoom,
|
||||
height: store.dimensions.height / store.viewport.zoom,
|
||||
}))
|
||||
const viewBox = computed(() => {
|
||||
const boundingRect = store.getNodes && store.getNodes.length ? getBoundsofRects(bb.value, viewBB.value) : viewBB.value
|
||||
|
||||
Vendored
+21
-1
@@ -3,14 +3,17 @@
|
||||
declare global {
|
||||
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
||||
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
|
||||
const biSyncRef: typeof import('@vueuse/core')['biSyncRef']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const computedAsync: typeof import('@vueuse/core')['computedAsync']
|
||||
const computedEager: typeof import('@vueuse/core')['computedEager']
|
||||
const computedInject: typeof import('@vueuse/core')['computedInject']
|
||||
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
|
||||
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
|
||||
const controlledRef: typeof import('@vueuse/core')['controlledRef']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const createEventHook: typeof import('@vueuse/core')['createEventHook']
|
||||
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
|
||||
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
|
||||
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
|
||||
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
|
||||
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
|
||||
@@ -31,6 +34,9 @@ declare global {
|
||||
const isDefined: typeof import('@vueuse/core')['isDefined']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const logicAnd: typeof import('@vueuse/core')['logicAnd']
|
||||
const logicNot: typeof import('@vueuse/core')['logicNot']
|
||||
const logicOr: typeof import('@vueuse/core')['logicOr']
|
||||
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
@@ -61,12 +67,17 @@ declare global {
|
||||
const reactivePick: typeof import('@vueuse/core')['reactivePick']
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
|
||||
const refDebounced: typeof import('@vueuse/core')['refDebounced']
|
||||
const refDefault: typeof import('@vueuse/core')['refDefault']
|
||||
const refThrottled: typeof import('@vueuse/core')['refThrottled']
|
||||
const refWithControl: typeof import('@vueuse/core')['refWithControl']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const syncRef: typeof import('@vueuse/core')['syncRef']
|
||||
const syncRefs: typeof import('@vueuse/core')['syncRefs']
|
||||
const templateRef: typeof import('@vueuse/core')['templateRef']
|
||||
const throttledRef: typeof import('@vueuse/core')['throttledRef']
|
||||
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
|
||||
@@ -75,6 +86,7 @@ declare global {
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
|
||||
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
|
||||
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
|
||||
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
|
||||
@@ -102,6 +114,7 @@ declare global {
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useCycleList: typeof import('@vueuse/core')['useCycleList']
|
||||
const useDark: typeof import('@vueuse/core')['useDark']
|
||||
const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
|
||||
const useDebounce: typeof import('@vueuse/core')['useDebounce']
|
||||
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
|
||||
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
|
||||
@@ -123,10 +136,12 @@ declare global {
|
||||
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
|
||||
const useFavicon: typeof import('@vueuse/core')['useFavicon']
|
||||
const useFetch: typeof import('@vueuse/core')['useFetch']
|
||||
const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
|
||||
const useFocus: typeof import('@vueuse/core')['useFocus']
|
||||
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
|
||||
const useFps: typeof import('@vueuse/core')['useFps']
|
||||
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
|
||||
const useGamepad: typeof import('@vueuse/core')['useGamepad']
|
||||
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
|
||||
const useIdle: typeof import('@vueuse/core')['useIdle']
|
||||
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
|
||||
@@ -184,6 +199,7 @@ declare global {
|
||||
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
|
||||
const useTimeout: typeof import('@vueuse/core')['useTimeout']
|
||||
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
|
||||
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
|
||||
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
|
||||
const useTitle: typeof import('@vueuse/core')['useTitle']
|
||||
const useToggle: typeof import('@vueuse/core')['useToggle']
|
||||
@@ -204,8 +220,12 @@ declare global {
|
||||
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
|
||||
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
|
||||
const watchOnce: typeof import('@vueuse/core')['watchOnce']
|
||||
const watchPausable: typeof import('@vueuse/core')['watchPausable']
|
||||
const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
|
||||
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
|
||||
const whenever: typeof import('@vueuse/core')['whenever']
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ const sourceY = props.sourceNode.computedPosition.y + sourceHandleY
|
||||
const isRightOrLeft = sourceHandle?.position === Position.Left || sourceHandle?.position === Position.Right
|
||||
const targetPosition = isRightOrLeft ? Position.Left : Position.Top
|
||||
|
||||
const targetX = computed(() => (store.connectionPosition.x - store.transform[0]) / store.transform[2])
|
||||
const targetY = computed(() => (store.connectionPosition.y - store.transform[1]) / store.transform[2])
|
||||
const targetX = computed(() => (store.connectionPosition.x - store.viewport.x) / store.viewport.zoom)
|
||||
const targetY = computed(() => (store.connectionPosition.y - store.viewport.y) / store.viewport.zoom)
|
||||
|
||||
const dAttr = computed(() => {
|
||||
let path = `M${sourceX},${sourceY} ${targetX.value},${targetY.value}`
|
||||
|
||||
@@ -117,14 +117,14 @@ const { scale, onDrag, onDragStart, onDragStop } = useDraggableCore(nodeElement,
|
||||
grid: props.snapGrid,
|
||||
cancel: `.${store.noDragClassName}`,
|
||||
enableUserSelectHack: false,
|
||||
scale: store.transform[2],
|
||||
scale: store.viewport.zoom,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
debouncedWatch(
|
||||
() => store.transform[2],
|
||||
() => store.viewport.zoom,
|
||||
() => {
|
||||
scale.value = store.transform[2]
|
||||
scale.value = store.viewport.zoom
|
||||
},
|
||||
{ debounce: 5 },
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ const innerStyle = computed(() => ({
|
||||
left: `${selectedNodesBBox.value.x}px`,
|
||||
}))
|
||||
|
||||
const transform = computed(() => `translate(${store.transform[0]}px,${store.transform[1]}px) scale(${store.transform[2]})`)
|
||||
const transform = computed(() => `translate(${store.viewport.x}px,${store.viewport.y}px) scale(${store.viewport.zoom})`)
|
||||
|
||||
watch(selectedNodesBBox, (v) => (store.selectedNodesBbox = v))
|
||||
|
||||
@@ -25,14 +25,14 @@ const onContextMenu = (event: MouseEvent) => store.hooks.selectionContextMenu.tr
|
||||
const { onDragStart, onDrag, onDragStop, scale } = useDraggableCore(el, {
|
||||
grid: store.snapToGrid ? store.snapGrid : undefined,
|
||||
enableUserSelectHack: false,
|
||||
scale: store.transform[2],
|
||||
scale: store.viewport.zoom,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
debouncedWatch(
|
||||
() => store.transform[2],
|
||||
() => store.viewport.zoom,
|
||||
() => {
|
||||
scale.value = store.transform[2]
|
||||
scale.value = store.viewport.zoom
|
||||
},
|
||||
{ debounce: 5 },
|
||||
)
|
||||
|
||||
@@ -64,7 +64,7 @@ const onMouseMove = (event: MouseEvent) => {
|
||||
height: Math.abs(mousePos.y - startY),
|
||||
}
|
||||
|
||||
const selectedNodes = getNodesInside(store.getNodes, rect.value, store.transform)
|
||||
const selectedNodes = getNodesInside(store.getNodes, rect.value, store.viewport)
|
||||
const selectedEdges = getConnectedEdges(selectedNodes, store.getEdges)
|
||||
|
||||
rect.value = nextUserSelectRect
|
||||
|
||||
@@ -2,13 +2,13 @@ import { zoomIdentity } from 'd3-zoom'
|
||||
import useVueFlow from './useVueFlow'
|
||||
import useWindow from './useWindow'
|
||||
import { getRectOfNodes, pointToRendererPoint, getTransformForBounds } from '~/utils'
|
||||
import { GraphNode, Store, Viewport, D3Selection } from '~/types'
|
||||
import { GraphNode, Store, ViewportFuncs, D3Selection } from '~/types'
|
||||
|
||||
const DEFAULT_PADDING = 0.1
|
||||
|
||||
const transition = (selection: D3Selection, ms = 0) => selection.transition().duration(ms)
|
||||
|
||||
export default (store: Store = useVueFlow().store): Viewport => ({
|
||||
export default (store: Store = useVueFlow().store): ViewportFuncs => ({
|
||||
zoomIn: (options) => store.d3Selection && store.d3Zoom?.scaleBy(transition(store.d3Selection, options?.duration), 1.2),
|
||||
zoomOut: (options) => store.d3Selection && store.d3Zoom?.scaleBy(transition(store.d3Selection, options?.duration), 1 / 1.2),
|
||||
zoomTo: (zoomLevel, options) =>
|
||||
@@ -18,9 +18,9 @@ export default (store: Store = useVueFlow().store): Viewport => ({
|
||||
store.d3Selection && store.d3Zoom?.transform(transition(store.d3Selection, options?.duration), nextTransform)
|
||||
},
|
||||
getTransform: () => ({
|
||||
x: store.transform[0],
|
||||
y: store.transform[1],
|
||||
zoom: store.transform[2],
|
||||
x: store.viewport.x,
|
||||
y: store.viewport.y,
|
||||
zoom: store.viewport.zoom,
|
||||
}),
|
||||
fitView: async (
|
||||
options = {
|
||||
@@ -44,7 +44,7 @@ export default (store: Store = useVueFlow().store): Viewport => ({
|
||||
nodes = options.includeHiddenNodes ? store.nodes : store.getNodes
|
||||
}
|
||||
const bounds = getRectOfNodes(nodes)
|
||||
const [x, y, zoom] = getTransformForBounds(
|
||||
const { x, y, zoom } = getTransformForBounds(
|
||||
bounds,
|
||||
store.dimensions.width,
|
||||
store.dimensions.height,
|
||||
@@ -66,7 +66,7 @@ export default (store: Store = useVueFlow().store): Viewport => ({
|
||||
store.d3Selection && store.d3Zoom?.transform(transition(store.d3Selection, options?.duration), transform)
|
||||
},
|
||||
fitBounds: (bounds, options = { padding: DEFAULT_PADDING }) => {
|
||||
const [x, y, zoom] = getTransformForBounds(
|
||||
const { x, y, zoom } = getTransformForBounds(
|
||||
bounds,
|
||||
store.dimensions.width,
|
||||
store.dimensions.height,
|
||||
@@ -78,5 +78,5 @@ export default (store: Store = useVueFlow().store): Viewport => ({
|
||||
|
||||
store.d3Selection && store.d3Zoom?.transform(transition(store.d3Selection, options.duration), transform)
|
||||
},
|
||||
project: (position) => pointToRendererPoint(position, store.transform, store.snapToGrid, store.snapGrid),
|
||||
project: (position) => pointToRendererPoint(position, store.viewport, store.snapToGrid, store.snapGrid),
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import NodeRenderer from '../NodeRenderer/NodeRenderer.vue'
|
||||
import EdgeRenderer from '../EdgeRenderer/EdgeRenderer.vue'
|
||||
import { useKeyPress, useVueFlow, useZoomPanHelper } from '../../composables'
|
||||
import { useVueFlow, useZoomPanHelper } from '../../composables'
|
||||
import { FlowInstance } from '../../types'
|
||||
import { onLoadGetEdges, onLoadGetElements, onLoadGetNodes, onLoadProject, onLoadToObject } from '../../utils'
|
||||
|
||||
@@ -29,7 +29,7 @@ onMounted(() => {
|
||||
store.hooks.paneReady.trigger(instance)
|
||||
})
|
||||
|
||||
const transform = computed(() => `translate(${store.transform[0]}px,${store.transform[1]}px) scale(${store.transform[2]})`)
|
||||
const transform = computed(() => `translate(${store.viewport.x}px,${store.viewport.y}px) scale(${store.viewport.zoom})`)
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
|
||||
@@ -1,33 +1,159 @@
|
||||
<script lang="ts" setup>
|
||||
import { zoom } from 'd3-zoom'
|
||||
import { select } from 'd3-selection'
|
||||
import { useVueFlow } from '../../composables'
|
||||
import { D3ZoomEvent, zoom, zoomIdentity, ZoomTransform } from 'd3-zoom'
|
||||
import { pointer, select } from 'd3-selection'
|
||||
import { FlowTransform, PanOnScrollMode } from '../../types'
|
||||
import { useKeyPress, useVueFlow } from '../../composables'
|
||||
import { clamp, clampPosition } from '../../utils'
|
||||
import SelectionPane from '../SelectionPane/SelectionPane.vue'
|
||||
import Zoom from './Zoom.vue'
|
||||
import Transform from './Transform.vue'
|
||||
|
||||
const { id, store } = useVueFlow()
|
||||
const zoomPaneEl = templateRef<HTMLDivElement>('viewport', null)
|
||||
const viewportEl = templateRef<HTMLDivElement>('viewport', null)
|
||||
|
||||
const { width, height } = useElementBounding(zoomPaneEl)
|
||||
const viewChanged = (prevTransform: FlowTransform, eventTransform: ZoomTransform): boolean =>
|
||||
(prevTransform.x !== eventTransform.x && !isNaN(eventTransform.x)) ||
|
||||
(prevTransform.y !== eventTransform.y && !isNaN(eventTransform.y)) ||
|
||||
(prevTransform.zoom !== eventTransform.k && !isNaN(eventTransform.k))
|
||||
|
||||
useResizeObserver(zoomPaneEl, () => {
|
||||
store.dimensions.width = width.value
|
||||
store.dimensions.height = height.value
|
||||
const eventToFlowTransform = (eventTransform: ZoomTransform): FlowTransform => ({
|
||||
x: eventTransform.x,
|
||||
y: eventTransform.y,
|
||||
zoom: eventTransform.k,
|
||||
})
|
||||
|
||||
const d3Zoom = ref()
|
||||
const d3Selection = ref()
|
||||
const d3ZoomHandler = ref()
|
||||
const isWrappedWithClass = (event: Event, className: string | undefined) => (event.target as Element).closest(`.${className}`)
|
||||
|
||||
const clampedZoom = clamp(store.defaultZoom, store.minZoom, store.maxZoom)
|
||||
const transform = ref({
|
||||
...clampPosition({ x: store.defaultPosition[0], y: store.defaultPosition[1] }, store.translateExtent),
|
||||
zoom: clampedZoom,
|
||||
})
|
||||
const { width, height } = useElementBounding(viewportEl)
|
||||
|
||||
watch(
|
||||
[width, height],
|
||||
([newWidth, newHeight]) => {
|
||||
store.dimensions.width = newWidth
|
||||
store.dimensions.height = newHeight
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
d3Zoom.value = zoom<HTMLDivElement, any>().scaleExtent([store.minZoom, store.maxZoom]).translateExtent(store.translateExtent)
|
||||
d3Selection.value = select(zoomPaneEl.value).call(d3Zoom.value)
|
||||
d3ZoomHandler.value = d3Selection.value.on('wheel.zoom')
|
||||
const d3Zoom = zoom<HTMLDivElement, any>().scaleExtent([store.minZoom, store.maxZoom]).translateExtent(store.translateExtent)
|
||||
const d3Selection = select(viewportEl.value).call(d3Zoom)
|
||||
const d3ZoomHandler = d3Selection.on('wheel.zoom')
|
||||
|
||||
const updatedTransform = zoomIdentity.translate(transform.value.x, transform.value.y).scale(transform.value.zoom)
|
||||
d3Zoom.transform(d3Selection, updatedTransform)
|
||||
|
||||
store.setState({
|
||||
d3Zoom: d3Zoom.value,
|
||||
d3Selection: d3Selection.value,
|
||||
d3ZoomHandler: d3ZoomHandler.value,
|
||||
d3Zoom,
|
||||
d3Selection,
|
||||
d3ZoomHandler,
|
||||
viewport: { x: updatedTransform.x, y: updatedTransform.y, zoom: updatedTransform.k },
|
||||
})
|
||||
|
||||
const selectionKeyPressed = useKeyPress(store.selectionKeyCode, (keyPress) => {
|
||||
if (keyPress) {
|
||||
d3Zoom.on('zoom', null)
|
||||
} else {
|
||||
d3Zoom.on('zoom', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||
store.setState({ viewport: { x: event.transform.x, y: event.transform.y, zoom: event.transform.k } })
|
||||
const flowTransform = eventToFlowTransform(event.transform)
|
||||
store.hooks.move.trigger({ event, flowTransform })
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const zoomKeyPressed = useKeyPress(store.zoomActivationKeyCode)
|
||||
|
||||
d3Zoom.on('start', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||
const flowTransform = eventToFlowTransform(event.transform)
|
||||
transform.value = flowTransform
|
||||
store.hooks.moveStart.trigger({ event, flowTransform })
|
||||
})
|
||||
|
||||
d3Zoom.on('end', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||
if (viewChanged(transform.value, event.transform)) {
|
||||
const flowTransform = eventToFlowTransform(event.transform)
|
||||
transform.value = flowTransform
|
||||
store.hooks.moveEnd.trigger({ event, flowTransform })
|
||||
}
|
||||
})
|
||||
|
||||
d3Selection
|
||||
?.on('wheel', (event: WheelEvent) => {
|
||||
if (store.panOnScroll && !zoomKeyPressed.value) {
|
||||
if (isWrappedWithClass(event, store.noWheelClassName)) return
|
||||
event.preventDefault()
|
||||
event.stopImmediatePropagation()
|
||||
|
||||
const currentZoom = d3Selection?.property('__zoom').k || 1
|
||||
|
||||
if (event.ctrlKey && store.zoomOnPinch) {
|
||||
const point = pointer(event)
|
||||
// taken from https://github.com/d3/d3-zoom/blob/master/src/zoom.js
|
||||
const pinchDelta = -event.deltaY * (event.deltaMode === 1 ? 0.05 : event.deltaMode ? 1 : 0.002) * 10
|
||||
const zoom = currentZoom * Math.pow(2, pinchDelta)
|
||||
if (d3Selection) d3Zoom.scaleTo(d3Selection, zoom, point)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// increase scroll speed in firefox
|
||||
// firefox: deltaMode === 1; chrome: deltaMode === 0
|
||||
const deltaNormalize = event.deltaMode === 1 ? 20 : 1
|
||||
const deltaX = store.panOnScrollMode === PanOnScrollMode.Vertical ? 0 : event.deltaX * deltaNormalize
|
||||
const deltaY = store.panOnScrollMode === PanOnScrollMode.Horizontal ? 0 : event.deltaY * deltaNormalize
|
||||
|
||||
if (d3Selection && store.panOnScrollSpeed)
|
||||
d3Zoom?.translateBy(
|
||||
d3Selection,
|
||||
-(deltaX / currentZoom) * store.panOnScrollSpeed,
|
||||
-(deltaY / currentZoom) * store.panOnScrollSpeed,
|
||||
)
|
||||
} else {
|
||||
if (
|
||||
(!store.zoomOnScroll && store.preventScrolling) ||
|
||||
!store.preventScrolling ||
|
||||
isWrappedWithClass(event, store.noWheelClassName)
|
||||
)
|
||||
return null
|
||||
event.preventDefault()
|
||||
}
|
||||
})
|
||||
.on('wheel.zoom', store.panOnScroll || typeof d3ZoomHandler === 'undefined' ? null : (d3ZoomHandler as any))
|
||||
|
||||
d3Zoom.filter((event: MouseEvent) => {
|
||||
const zoomScroll = zoomKeyPressed.value || store.zoomOnScroll
|
||||
const pinchZoom = store.zoomOnPinch && event.ctrlKey
|
||||
|
||||
// if all interactions are disabled, we prevent all zoom events
|
||||
if (!store.panOnDrag && !zoomScroll && !store.panOnScroll && !store.zoomOnDoubleClick && !store.zoomOnPinch) return false
|
||||
|
||||
// during a selection we prevent all other interactions
|
||||
if (selectionKeyPressed.value) return false
|
||||
|
||||
// if zoom on double click is disabled, we prevent the double click event
|
||||
if (!store.zoomOnDoubleClick && event.type === 'dblclick') return false
|
||||
|
||||
// if the target element is inside an element with the nowheel class, we prevent zooming
|
||||
if (isWrappedWithClass(event, store.noWheelClassName) && event.type === 'wheel') return false
|
||||
|
||||
// if the target element is inside an element with the nopan class, we prevent panning
|
||||
if (isWrappedWithClass(event, store.noPanClassName) && event.type !== 'wheel') return false
|
||||
|
||||
if (!store.zoomOnPinch && event.ctrlKey && event.type === 'wheel') return false
|
||||
|
||||
// when there is no scroll handling enabled, we prevent all wheel events
|
||||
if (!zoomScroll && !store.panOnScroll && !pinchZoom && event.type === 'wheel') return false
|
||||
|
||||
// if the pane is not movable, we prevent dragging it with mousestart or touchstart
|
||||
if (!store.panOnDrag && (event.type === 'mousedown' || event.type === 'touchstart')) return false
|
||||
|
||||
// default filter for d3-zoom
|
||||
return (!event.ctrlKey || event.type === 'wheel') && !event.button
|
||||
})
|
||||
})
|
||||
</script>
|
||||
@@ -37,17 +163,10 @@ export default {
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div ref="viewport" class="vue-flow__viewport vue-flow__container">
|
||||
<Zoom
|
||||
v-if="d3Zoom && d3Selection && d3ZoomHandler"
|
||||
:d3-zoom="d3Zoom"
|
||||
:d3-selection="d3Selection"
|
||||
:d3-zoom-handler="d3ZoomHandler"
|
||||
>
|
||||
<Transform>
|
||||
<slot />
|
||||
</Transform>
|
||||
</Zoom>
|
||||
<div ref="viewport" :key="`viewport-${id}`" class="vue-flow__viewport vue-flow__container">
|
||||
<Transform>
|
||||
<slot />
|
||||
</Transform>
|
||||
<SelectionPane :key="`selection-pane-${id}`" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { D3ZoomEvent, zoomIdentity, ZoomTransform } from 'd3-zoom'
|
||||
import { pointer } from 'd3-selection'
|
||||
import { useKeyPress, useVueFlow } from '../../composables'
|
||||
import { D3Selection, D3Zoom, D3ZoomHandler, FlowTransform, PanOnScrollMode } from '../../types'
|
||||
import { clamp, clampPosition } from '../../utils'
|
||||
|
||||
interface Props {
|
||||
d3Zoom: D3Zoom
|
||||
d3Selection: D3Selection
|
||||
d3ZoomHandler: D3ZoomHandler
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const { store } = useVueFlow()
|
||||
|
||||
const viewChanged = (prevTransform: FlowTransform, eventTransform: ZoomTransform): boolean =>
|
||||
(prevTransform.x !== eventTransform.x && !isNaN(eventTransform.x)) ||
|
||||
(prevTransform.y !== eventTransform.y && !isNaN(eventTransform.y)) ||
|
||||
(prevTransform.zoom !== eventTransform.k && !isNaN(eventTransform.k))
|
||||
|
||||
const eventToFlowTransform = (eventTransform: ZoomTransform): FlowTransform => ({
|
||||
x: eventTransform.x,
|
||||
y: eventTransform.y,
|
||||
zoom: eventTransform.k,
|
||||
})
|
||||
|
||||
const isWrappedWithClass = (event: Event, className: string | undefined) => (event.target as Element).closest(`.${className}`)
|
||||
|
||||
const clampedZoom = clamp(store.defaultZoom, store.minZoom, store.maxZoom)
|
||||
const transform = ref({
|
||||
...clampPosition({ x: store.defaultPosition[0], y: store.defaultPosition[1] }, store.translateExtent),
|
||||
zoom: clampedZoom,
|
||||
})
|
||||
|
||||
const updatedTransform = zoomIdentity.translate(transform.value.x, transform.value.y).scale(transform.value.zoom)
|
||||
props.d3Zoom.transform(props.d3Selection, updatedTransform)
|
||||
|
||||
store.transform = [updatedTransform.x, updatedTransform.y, updatedTransform.k]
|
||||
|
||||
const selectionKeyPressed = useKeyPress(store.selectionKeyCode, (keyPress) => {
|
||||
if (keyPress) {
|
||||
props.d3Zoom.on('zoom', null)
|
||||
} else {
|
||||
props.d3Zoom.on('zoom', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||
store.setState({ transform: [event.transform.x, event.transform.y, event.transform.k] })
|
||||
const flowTransform = eventToFlowTransform(event.transform)
|
||||
store.hooks.move.trigger({ event, flowTransform })
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const zoomKeyPressed = useKeyPress(store.zoomActivationKeyCode)
|
||||
|
||||
props.d3Zoom.on('start', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||
const flowTransform = eventToFlowTransform(event.transform)
|
||||
transform.value = flowTransform
|
||||
store.hooks.moveStart.trigger({ event, flowTransform })
|
||||
})
|
||||
|
||||
props.d3Zoom.on('end', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||
if (viewChanged(transform.value, event.transform)) {
|
||||
const flowTransform = eventToFlowTransform(event.transform)
|
||||
transform.value = flowTransform
|
||||
store.hooks.moveEnd.trigger({ event, flowTransform })
|
||||
}
|
||||
})
|
||||
|
||||
props.d3Selection
|
||||
?.on('wheel', (event: WheelEvent) => {
|
||||
if (store.panOnScroll && !zoomKeyPressed.value) {
|
||||
if (isWrappedWithClass(event, store.noWheelClassName)) return
|
||||
event.preventDefault()
|
||||
event.stopImmediatePropagation()
|
||||
|
||||
const currentZoom = props.d3Selection?.property('__zoom').k || 1
|
||||
|
||||
if (event.ctrlKey && store.zoomOnPinch) {
|
||||
const point = pointer(event)
|
||||
// taken from https://github.com/d3/d3-zoom/blob/master/src/zoom.js
|
||||
const pinchDelta = -event.deltaY * (event.deltaMode === 1 ? 0.05 : event.deltaMode ? 1 : 0.002) * 10
|
||||
const zoom = currentZoom * Math.pow(2, pinchDelta)
|
||||
if (props.d3Selection) props.d3Zoom.scaleTo(props.d3Selection, zoom, point)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// increase scroll speed in firefox
|
||||
// firefox: deltaMode === 1; chrome: deltaMode === 0
|
||||
const deltaNormalize = event.deltaMode === 1 ? 20 : 1
|
||||
const deltaX = store.panOnScrollMode === PanOnScrollMode.Vertical ? 0 : event.deltaX * deltaNormalize
|
||||
const deltaY = store.panOnScrollMode === PanOnScrollMode.Horizontal ? 0 : event.deltaY * deltaNormalize
|
||||
|
||||
if (props.d3Selection && store.panOnScrollSpeed)
|
||||
props.d3Zoom?.translateBy(
|
||||
props.d3Selection,
|
||||
-(deltaX / currentZoom) * store.panOnScrollSpeed,
|
||||
-(deltaY / currentZoom) * store.panOnScrollSpeed,
|
||||
)
|
||||
} else {
|
||||
if (
|
||||
(!store.zoomOnScroll && store.preventScrolling) ||
|
||||
!store.preventScrolling ||
|
||||
isWrappedWithClass(event, store.noWheelClassName)
|
||||
)
|
||||
return null
|
||||
event.preventDefault()
|
||||
}
|
||||
})
|
||||
.on('wheel.zoom', store.panOnScroll || typeof props.d3ZoomHandler === 'undefined' ? null : (props.d3ZoomHandler as any))
|
||||
|
||||
props.d3Zoom.filter((event: MouseEvent) => {
|
||||
const zoomScroll = zoomKeyPressed.value || store.zoomOnScroll
|
||||
const pinchZoom = store.zoomOnPinch && event.ctrlKey
|
||||
|
||||
// if all interactions are disabled, we prevent all zoom events
|
||||
if (!store.panOnDrag && !zoomScroll && !store.panOnScroll && !store.zoomOnDoubleClick && !store.zoomOnPinch) return false
|
||||
|
||||
// during a selection we prevent all other interactions
|
||||
if (selectionKeyPressed.value) return false
|
||||
|
||||
// if zoom on double click is disabled, we prevent the double click event
|
||||
if (!store.zoomOnDoubleClick && event.type === 'dblclick') return false
|
||||
|
||||
// if the target element is inside an element with the nowheel class, we prevent zooming
|
||||
if (isWrappedWithClass(event, store.noWheelClassName) && event.type === 'wheel') return false
|
||||
|
||||
// if the target element is inside an element with the nopan class, we prevent panning
|
||||
if (isWrappedWithClass(event, store.noPanClassName) && event.type !== 'wheel') return false
|
||||
|
||||
if (!store.zoomOnPinch && event.ctrlKey && event.type === 'wheel') return false
|
||||
|
||||
// when there is no scroll handling enabled, we prevent all wheel events
|
||||
if (!zoomScroll && !store.panOnScroll && !pinchZoom && event.type === 'wheel') return false
|
||||
|
||||
// if the pane is not movable, we prevent dragging it with mousestart or touchstart
|
||||
if (!store.panOnDrag && (event.type === 'mousedown' || event.type === 'touchstart')) return false
|
||||
|
||||
// default filter for d3-zoom
|
||||
return (!event.ctrlKey || event.type === 'wheel') && !event.button
|
||||
})
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'Zoom',
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<slot />
|
||||
</template>
|
||||
@@ -152,7 +152,7 @@ export default (state: State, getters: ComputedGetters): Actions => {
|
||||
dimensions.height &&
|
||||
(node.dimensions.width !== dimensions.width || node.dimensions.height !== dimensions.height || update.forceUpdate)
|
||||
)
|
||||
node.handleBounds = getHandleBounds(update.nodeElement, state.transform[2])
|
||||
node.handleBounds = getHandleBounds(update.nodeElement, state.viewport.zoom)
|
||||
|
||||
if (doUpdate) {
|
||||
node.dimensions = dimensions
|
||||
|
||||
@@ -35,7 +35,7 @@ export default (state: State): ComputedGetters => {
|
||||
width: state.dimensions.width,
|
||||
height: state.dimensions.height,
|
||||
},
|
||||
state.transform,
|
||||
state.viewport,
|
||||
true,
|
||||
)
|
||||
: nodes ?? []
|
||||
@@ -63,7 +63,7 @@ export default (state: State): ComputedGetters => {
|
||||
targetHeight: e.targetNode.dimensions.height,
|
||||
width: state.dimensions.width,
|
||||
height: state.dimensions.height,
|
||||
transform: state.transform,
|
||||
viewport: state.viewport,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -46,7 +46,7 @@ const defaultState = (): State => ({
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
transform: [0, 0, 1],
|
||||
viewport: { x: 0, y: 0, zoom: 1 },
|
||||
|
||||
d3Zoom: null,
|
||||
d3Selection: null,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { CSSProperties, VNode } from 'vue'
|
||||
import { GraphEdge, Edge, DefaultEdgeOptions } from './edge'
|
||||
import { GraphNode, CoordinateExtent, Node } from './node'
|
||||
import { ConnectionLineType, ConnectionMode } from './connection'
|
||||
import { KeyCode, PanOnScrollMode, Viewport } from './zoom'
|
||||
import { KeyCode, PanOnScrollMode, ViewportFuncs } from './zoom'
|
||||
import { DefaultEdgeTypes, DefaultNodeTypes, EdgeComponent, NodeComponent } from './components'
|
||||
|
||||
export type ElementData = any
|
||||
@@ -36,9 +36,6 @@ export interface Element<Data extends ElementData = ElementData> {
|
||||
}
|
||||
export type Elements<NodeData = ElementData, EdgeData = ElementData> = (Node<NodeData> | Edge<EdgeData>)[]
|
||||
|
||||
/** Transform x, y, z */
|
||||
export type Transform = [number, number, number]
|
||||
|
||||
/** Handle Positions */
|
||||
export enum Position {
|
||||
Left = 'left',
|
||||
@@ -93,7 +90,7 @@ interface Exports {
|
||||
toObject: () => FlowExportObject
|
||||
}
|
||||
|
||||
export type FlowInstance = Exports & Viewport
|
||||
export type FlowInstance = Exports & ViewportFuncs
|
||||
|
||||
export interface FlowProps<NodeData = ElementData, EdgeData = ElementData> {
|
||||
id?: string
|
||||
|
||||
@@ -1,21 +1,10 @@
|
||||
import { ComputedRef, CSSProperties, ToRefs } from 'vue'
|
||||
import {
|
||||
Dimensions,
|
||||
ElementData,
|
||||
Elements,
|
||||
FlowElements,
|
||||
FlowInstance,
|
||||
FlowOptions,
|
||||
Rect,
|
||||
SnapGrid,
|
||||
Transform,
|
||||
XYPosition,
|
||||
} from './flow'
|
||||
import { Dimensions, ElementData, Elements, FlowElements, FlowInstance, FlowOptions, Rect, SnapGrid, XYPosition } from './flow'
|
||||
import { EdgeComponent, NodeComponent, DefaultNodeTypes, DefaultEdgeTypes } from './components'
|
||||
import { Connection, ConnectionLineType, ConnectionMode } from './connection'
|
||||
import { DefaultEdgeOptions, Edge, GraphEdge } from './edge'
|
||||
import { GraphNode, CoordinateExtent, Node } from './node'
|
||||
import { D3Selection, D3Zoom, D3ZoomHandler, KeyCode, PanOnScrollMode } from './zoom'
|
||||
import { D3Selection, D3Zoom, D3ZoomHandler, KeyCode, PanOnScrollMode, Viewport } from './zoom'
|
||||
import { FlowHooks, FlowHooksOn } from './hooks'
|
||||
import { NodeChange, EdgeChange } from './changes'
|
||||
import { StartHandle, HandleType } from './handle'
|
||||
@@ -44,7 +33,7 @@ export interface State<NodeData = ElementData, EdgeData = ElementData>
|
||||
/** viewport dimensions */
|
||||
dimensions: Dimensions
|
||||
/** viewport transform x, y, z */
|
||||
transform: Transform
|
||||
viewport: Viewport
|
||||
|
||||
onlyRenderVisibleElements: boolean
|
||||
defaultPosition: [number, number]
|
||||
|
||||
@@ -5,6 +5,9 @@ export type D3Zoom = ZoomBehavior<HTMLDivElement, unknown>
|
||||
export type D3Selection = Selection<HTMLDivElement, unknown, any, any>
|
||||
export type D3ZoomHandler = (this: HTMLDivElement, event: any, d: unknown) => void
|
||||
|
||||
/** Transform x, y, z */
|
||||
export type Viewport = { x: number; y: number; zoom: number }
|
||||
|
||||
export type KeyCode = number | string
|
||||
|
||||
export enum PanOnScrollMode {
|
||||
@@ -13,7 +16,7 @@ export enum PanOnScrollMode {
|
||||
Horizontal = 'horizontal',
|
||||
}
|
||||
|
||||
export type UseZoomPanHelperOptions = {
|
||||
export type ViewportFuncsOptions = {
|
||||
duration?: number
|
||||
}
|
||||
|
||||
@@ -27,7 +30,7 @@ export type FitViewParams = {
|
||||
y?: number
|
||||
}
|
||||
nodes?: string[]
|
||||
} & UseZoomPanHelperOptions
|
||||
} & ViewportFuncsOptions
|
||||
|
||||
export type FlowTransform = {
|
||||
x: number
|
||||
@@ -35,11 +38,11 @@ export type FlowTransform = {
|
||||
zoom: number
|
||||
}
|
||||
|
||||
export type SetCenterOptions = UseZoomPanHelperOptions & {
|
||||
export type SetCenterOptions = ViewportFuncsOptions & {
|
||||
zoom?: number
|
||||
}
|
||||
|
||||
export type FitBoundsOptions = UseZoomPanHelperOptions & {
|
||||
export type FitBoundsOptions = ViewportFuncsOptions & {
|
||||
padding?: number
|
||||
}
|
||||
|
||||
@@ -47,12 +50,12 @@ export type FitView = (fitViewOptions?: FitViewParams) => void
|
||||
export type Project = (position: XYPosition) => XYPosition
|
||||
export type SetCenter = (x: number, y: number, options?: SetCenterOptions) => void
|
||||
export type FitBounds = (bounds: Rect, options?: FitBoundsOptions) => void
|
||||
export type ZoomInOut = (options?: UseZoomPanHelperOptions) => void
|
||||
export type ZoomTo = (zoomLevel: number, options?: UseZoomPanHelperOptions) => void
|
||||
export type ZoomInOut = (options?: ViewportFuncsOptions) => void
|
||||
export type ZoomTo = (zoomLevel: number, options?: ViewportFuncsOptions) => void
|
||||
export type GetTransform = () => FlowTransform
|
||||
export type SetTransform = (transform: FlowTransform, options?: UseZoomPanHelperOptions) => void
|
||||
export type SetTransform = (transform: FlowTransform, options?: ViewportFuncsOptions) => void
|
||||
|
||||
export interface Viewport {
|
||||
export interface ViewportFuncs {
|
||||
zoomIn: ZoomInOut
|
||||
zoomOut: ZoomInOut
|
||||
zoomTo: ZoomTo
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { rectToBox } from './graph'
|
||||
import { EdgePositions, GraphEdge, GraphNode, HandleElement, Position, Rect, Transform, XYPosition } from '~/types'
|
||||
import { EdgePositions, GraphEdge, GraphNode, HandleElement, Position, Rect, Viewport, XYPosition } from '~/types'
|
||||
|
||||
export const getHandlePosition = (position: Position, rect: Rect, handle?: HandleElement): XYPosition => {
|
||||
const x = (handle?.x ?? 0) + rect.x
|
||||
@@ -83,7 +83,7 @@ interface IsEdgeVisibleParams {
|
||||
targetHeight: number
|
||||
width: number
|
||||
height: number
|
||||
transform: Transform
|
||||
viewport: Viewport
|
||||
}
|
||||
|
||||
export function isEdgeVisible({
|
||||
@@ -95,7 +95,7 @@ export function isEdgeVisible({
|
||||
targetHeight,
|
||||
width,
|
||||
height,
|
||||
transform,
|
||||
viewport,
|
||||
}: IsEdgeVisibleParams): boolean {
|
||||
const edgeBox = {
|
||||
x: Math.min(sourcePos.x, targetPos.x),
|
||||
@@ -113,10 +113,10 @@ export function isEdgeVisible({
|
||||
}
|
||||
|
||||
const viewBox = rectToBox({
|
||||
x: (0 - transform[0]) / transform[2],
|
||||
y: (0 - transform[1]) / transform[2],
|
||||
width: width / transform[2],
|
||||
height: height / transform[2],
|
||||
x: (0 - viewport.x) / viewport.zoom,
|
||||
y: (0 - viewport.y) / viewport.zoom,
|
||||
width: width / viewport.zoom,
|
||||
height: height / viewport.zoom,
|
||||
})
|
||||
|
||||
const xOverlap = Math.max(0, Math.min(viewBox.x2, edgeBox.x2) - Math.max(viewBox.x, edgeBox.x))
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
Rect,
|
||||
State,
|
||||
Store,
|
||||
Transform,
|
||||
Viewport,
|
||||
XYPosition,
|
||||
XYZPosition,
|
||||
} from '~/types'
|
||||
@@ -200,7 +200,7 @@ export const updateEdge = (oldEdge: Edge, newConnection: Connection, elements: E
|
||||
|
||||
export const pointToRendererPoint = (
|
||||
{ x, y }: XYPosition,
|
||||
[tx, ty, tScale]: Transform,
|
||||
{ x: tx, y: ty, zoom: tScale }: Viewport,
|
||||
snapToGrid: boolean,
|
||||
[snapX, snapY]: [number, number],
|
||||
) => {
|
||||
@@ -220,7 +220,7 @@ export const pointToRendererPoint = (
|
||||
}
|
||||
|
||||
export const onLoadProject = (currentStore: Store) => (position: XYPosition) =>
|
||||
pointToRendererPoint(position, currentStore.transform, currentStore.snapToGrid, currentStore.snapGrid)
|
||||
pointToRendererPoint(position, currentStore.viewport, currentStore.snapToGrid, currentStore.snapGrid)
|
||||
|
||||
const getBoundsOfBoxes = (box1: Box, box2: Box): Box => ({
|
||||
x: Math.min(box1.x, box2.x),
|
||||
@@ -261,12 +261,17 @@ export const getRectOfNodes = (nodes: GraphNode[]) => {
|
||||
return boxToRect(box)
|
||||
}
|
||||
|
||||
export const graphPosToZoomedPos = ({ x, y }: XYPosition, [tx, ty, tScale]: Transform): XYPosition => ({
|
||||
export const graphPosToZoomedPos = ({ x, y }: XYPosition, { x: tx, y: ty, zoom: tScale }: Viewport): XYPosition => ({
|
||||
x: x * tScale + tx,
|
||||
y: y * tScale + ty,
|
||||
})
|
||||
|
||||
export const getNodesInside = (nodes: GraphNode[], rect: Rect, [tx, ty, tScale]: Transform = [0, 0, 1], partially = false) => {
|
||||
export const getNodesInside = (
|
||||
nodes: GraphNode[],
|
||||
rect: Rect,
|
||||
{ x: tx, y: ty, zoom: tScale }: Viewport = { x: 0, y: 0, zoom: 1 },
|
||||
partially = false,
|
||||
) => {
|
||||
const rBox = rectToBox({
|
||||
x: (rect.x - tx) / tScale,
|
||||
y: (rect.y - ty) / tScale,
|
||||
@@ -309,8 +314,8 @@ export const onLoadToObject = (store: State) => (): FlowExportObject => {
|
||||
JSON.stringify({
|
||||
nodes: store.nodes,
|
||||
edges: store.edges,
|
||||
position: [store.transform[0], store.transform[1]],
|
||||
zoom: store.transform[2],
|
||||
position: [store.viewport.x, store.viewport.y],
|
||||
zoom: store.viewport.zoom,
|
||||
} as FlowExportObject),
|
||||
)
|
||||
}
|
||||
@@ -326,7 +331,7 @@ export const getTransformForBounds = (
|
||||
x?: number
|
||||
y?: number
|
||||
} = { x: 0, y: 0 },
|
||||
): Transform => {
|
||||
): Viewport => {
|
||||
const xZoom = width / (bounds.width * (1 + padding))
|
||||
const yZoom = height / (bounds.height * (1 + padding))
|
||||
const zoom = Math.min(xZoom, yZoom)
|
||||
@@ -336,7 +341,7 @@ export const getTransformForBounds = (
|
||||
const x = width / 2 - boundsCenterX * clampedZoom + (offset.x ?? 0)
|
||||
const y = height / 2 - boundsCenterY * clampedZoom + (offset.y ?? 0)
|
||||
|
||||
return [x, y, clampedZoom]
|
||||
return { x, y, zoom: clampedZoom }
|
||||
}
|
||||
|
||||
export const getXYZPos = (parentPos: XYZPosition, computedPosition: XYZPosition): XYZPosition => {
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
"open": "cypress open-ct"
|
||||
},
|
||||
"dependencies": {
|
||||
"@braks/vue-flow": "^0.4.0-17"
|
||||
"@braks/vue-flow": "^0.4.2-3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cypress/vite-dev-server": "^2.2.1",
|
||||
|
||||
@@ -1196,18 +1196,6 @@
|
||||
"@vueuse/core" "^7.0.1"
|
||||
vue-demi latest
|
||||
|
||||
"@braks/vue-flow@^0.4.0-17":
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@braks/vue-flow/-/vue-flow-0.4.1.tgz#81ce05e6a47f7232f09ec78f93ea371105b85b43"
|
||||
integrity sha512-EOTo/zAvtnP7ZtXVJ8DSKJrQWMoajVVaFgzx5/ghVTn2KWU846S2AIaahy2L95x8gyzob0YmsJ73tkgoPgmuJg==
|
||||
dependencies:
|
||||
"@braks/revue-draggable" "^0.4.2"
|
||||
"@types/d3" "^7.1.0"
|
||||
"@vueuse/core" "^7.5.5"
|
||||
d3 "^7.1.1"
|
||||
d3-selection "^3.0.0"
|
||||
d3-zoom "^3.0.0"
|
||||
|
||||
"@cypress/mount-utils@1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@cypress/mount-utils/-/mount-utils-1.0.2.tgz#afbc4f8c350b7cd86edc5ad0db0cbe1e0181edc8"
|
||||
@@ -2352,7 +2340,7 @@
|
||||
ora "^5.4.1"
|
||||
upath "^2.0.1"
|
||||
|
||||
"@vueuse/core@^7.0.1", "@vueuse/core@^7.5.5":
|
||||
"@vueuse/core@^7.0.1":
|
||||
version "7.7.1"
|
||||
resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-7.7.1.tgz#fc284f4103de73c7fb79bc06579d8066790db511"
|
||||
integrity sha512-PRRgbATMpoeUmkCEBtUeJgOwtew8s+4UsEd+Pm7MhkjL2ihCNrSqxNVtM6NFE4uP2sWnkGcZpCjPuNSxowJ1Ow==
|
||||
@@ -3550,7 +3538,7 @@ d3-zoom@3, d3-zoom@^3.0.0:
|
||||
d3-selection "2 - 3"
|
||||
d3-transition "2 - 3"
|
||||
|
||||
d3@^7.1.1, d3@^7.4.2:
|
||||
d3@^7.4.2:
|
||||
version "7.4.2"
|
||||
resolved "https://registry.yarnpkg.com/d3/-/d3-7.4.2.tgz#c5d1001459dc9517bc774949f71b95e8735b3b4e"
|
||||
integrity sha512-7VK+QBAWtNDbP2EU/ThkXgjd0u1MsXYYgCK2ElQ4BBWh0usE75tHVVeYx47m2pqQEy4isYKAA0tAFSln0l+9EQ==
|
||||
|
||||
Reference in New Issue
Block a user