Skip to content

Commit

Permalink
refactor: Change zoom pane into viewport
Browse files Browse the repository at this point in the history
* split zoom pane into smaller components as viewport
* rename UseZoomPanHelper Type to Viewport
  • Loading branch information
bcakmakoglu committed Apr 9, 2022
1 parent 32c0f59 commit 572456e
Show file tree
Hide file tree
Showing 10 changed files with 230 additions and 188 deletions.
4 changes: 2 additions & 2 deletions package/src/composables/useZoomPanHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, UseZoomPanHelper, D3Selection } from '~/types'
import { GraphNode, Store, Viewport, D3Selection } from '~/types'

const DEFAULT_PADDING = 0.1

const transition = (selection: D3Selection, ms = 0) => selection.transition().duration(ms)

export default (store: Store = useVueFlow().store): UseZoomPanHelper => ({
export default (store: Store = useVueFlow().store): Viewport => ({
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) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const transform = computed(() => `translate(${store.transform[0]}px,${store.tran
</script>
<script lang="ts">
export default {
name: 'TransformationPane',
name: 'Transform',
}
</script>
<template>
Expand Down
53 changes: 53 additions & 0 deletions package/src/container/Viewport/Viewport.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<script lang="ts" setup>
import { zoom } from 'd3-zoom'
import { select } from 'd3-selection'
import { useVueFlow } from '../../composables'
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 { width, height } = useElementBounding(zoomPaneEl)
useResizeObserver(zoomPaneEl, () => {
store.dimensions.width = width.value
store.dimensions.height = height.value
})
const d3Zoom = ref()
const d3Selection = ref()
const d3ZoomHandler = ref()
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')
store.setState({
d3Zoom: d3Zoom.value,
d3Selection: d3Selection.value,
d3ZoomHandler: d3ZoomHandler.value,
})
})
</script>
<script lang="ts">
export default {
name: 'Viewport',
}
</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>
<SelectionPane :key="`selection-pane-${id}`" />
</div>
</template>
152 changes: 152 additions & 0 deletions package/src/container/Viewport/Zoom.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<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) => {
console.log(event)
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>
6 changes: 3 additions & 3 deletions package/src/container/VueFlow/VueFlow.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import ZoomPane from '../ZoomPane/ZoomPane.vue'
import Viewport from '../Viewport/Viewport.vue'
import { createHooks, useHooks } from '../../store'
import { useVueFlow } from '../../composables'
import type { FlowProps } from '../../types/flow'
Expand Down Expand Up @@ -91,7 +91,7 @@ export default {
</script>
<template>
<div class="vue-flow">
<ZoomPane :key="`renderer-${id}`">
<Viewport :key="`renderer-${id}`">
<template #nodes>
<template v-for="nodeName of Object.keys(getNodeTypes)" :key="`node-${nodeName}-${id}`">
<slot :name="`node-${nodeName}`" />
Expand All @@ -106,7 +106,7 @@ export default {
<slot name="connection-line" />
</template>
<slot name="zoom-pane" />
</ZoomPane>
</Viewport>
<slot />
</div>
</template>

0 comments on commit 572456e

Please sign in to comment.