fix(core): allow panning when panKey is pressed
This commit is contained in:
@@ -7,7 +7,7 @@ import { toRef, useEventListener, useResizeObserver } from '@vueuse/core'
|
||||
import type { CoordinateExtent, D3ZoomHandler, FlowOptions, ViewportTransform } from '../../types'
|
||||
import { PanOnScrollMode } from '../../types'
|
||||
import { useKeyPress, useVueFlow, useWindow } from '../../composables'
|
||||
import { ErrorCode, VueFlowError, clamp, getDimensions, isMacOs } from '../../utils'
|
||||
import { ErrorCode, VueFlowError, clamp, getDimensions, isMacOs, warn } from '../../utils'
|
||||
import Pane from '../Pane/Pane.vue'
|
||||
import Transform from './Transform.vue'
|
||||
|
||||
@@ -44,10 +44,6 @@ const {
|
||||
viewportRef,
|
||||
} = useVueFlow()
|
||||
|
||||
const viewportEl = ref<HTMLDivElement>()
|
||||
|
||||
const selectionKeyPressed = ref(false)
|
||||
|
||||
const isZoomingOrPanning = ref(false)
|
||||
|
||||
const isPanScrolling = ref(false)
|
||||
@@ -66,6 +62,10 @@ let prevTransform: ViewportTransform = {
|
||||
|
||||
const panKeyPressed = useKeyPress(panActivationKeyCode)
|
||||
|
||||
const selectionKeyPressed = useKeyPress(selectionKeyCode)
|
||||
|
||||
const zoomKeyPressed = useKeyPress(zoomActivationKeyCode)
|
||||
|
||||
const shouldPanOnDrag = toRef(() => !selectionKeyPressed.value && panOnDrag.value && panKeyPressed.value)
|
||||
|
||||
const isSelecting = toRef(
|
||||
@@ -73,13 +73,20 @@ const isSelecting = toRef(
|
||||
(selectionKeyCode.value !== true && selectionKeyPressed.value) || (selectionKeyCode.value === true && !shouldPanOnDrag.value),
|
||||
)
|
||||
|
||||
useResizeObserver(viewportEl, setDimensions)
|
||||
useResizeObserver(viewportRef, setDimensions)
|
||||
|
||||
useEventListener(window, 'resize', setDimensions)
|
||||
|
||||
onMounted(() => {
|
||||
const viewportElement = viewportEl.value!
|
||||
if (!viewportRef.value) {
|
||||
warn('Viewport element is missing')
|
||||
return
|
||||
}
|
||||
|
||||
const viewportElement = viewportRef.value
|
||||
|
||||
const bbox = viewportElement.getBoundingClientRect()
|
||||
|
||||
const d3Zoom = zoom<HTMLDivElement, any>().scaleExtent([minZoom.value, maxZoom.value]).translateExtent(translateExtent.value)
|
||||
const d3Selection = select(viewportElement).call(d3Zoom)
|
||||
const d3ZoomHandler = d3Selection.on('wheel.zoom')
|
||||
@@ -100,31 +107,8 @@ onMounted(() => {
|
||||
storeD3Zoom.value = d3Zoom
|
||||
storeD3Selection.value = d3Selection
|
||||
storeD3ZoomHandler.value = d3ZoomHandler as D3ZoomHandler
|
||||
|
||||
viewport.value = { x: constrainedTransform.x, y: constrainedTransform.y, zoom: constrainedTransform.k }
|
||||
viewportRef.value = viewportElement
|
||||
|
||||
useKeyPress(selectionKeyCode, (keyPress) => {
|
||||
selectionKeyPressed.value = keyPress
|
||||
})
|
||||
|
||||
const zoomKeyPressed = useKeyPress(zoomActivationKeyCode)
|
||||
|
||||
watchEffect(() => {
|
||||
if (selectionKeyPressed.value && userSelectionActive.value && !isZoomingOrPanning.value) {
|
||||
d3Zoom.on('zoom', null)
|
||||
} else if (!selectionKeyPressed.value && !userSelectionActive.value) {
|
||||
d3Zoom.on('zoom', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||
viewport.value = { x: event.transform.x, y: event.transform.y, zoom: event.transform.k }
|
||||
|
||||
const flowTransform = eventToFlowTransform(event.transform)
|
||||
|
||||
zoomedWithRightMouseButton = isRightClickPan(panOnDrag.value, mouseButton ?? 0)
|
||||
|
||||
emits.viewportChange(flowTransform)
|
||||
emits.move({ event, flowTransform })
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
d3Zoom.on('start', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||
if (!event.sourceEvent) {
|
||||
@@ -173,8 +157,97 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
d3Zoom.filter((event: MouseEvent) => {
|
||||
const zoomScroll = zoomKeyPressed.value || zoomOnScroll.value
|
||||
const pinchZoom = zoomOnPinch.value && event.ctrlKey
|
||||
|
||||
if (
|
||||
(panOnDrag.value === true || (Array.isArray(panOnDrag.value) && panOnDrag.value.includes(1))) &&
|
||||
event.button === 1 &&
|
||||
event.type === 'mousedown' &&
|
||||
((event.target as HTMLElement)?.closest('.vue-flow__node') || (event.target as HTMLElement)?.closest('.vue-flow__edge'))
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
// if all interactions are disabled, we prevent all zoom events
|
||||
if (!panOnDrag.value && !zoomScroll && !panOnScroll.value && !zoomOnDoubleClick.value && !zoomOnPinch.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
// during a selection we prevent all other interactions
|
||||
if (userSelectionActive.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
// if zoom on double click is disabled, we prevent the double click event
|
||||
if (!zoomOnDoubleClick.value && event.type === 'dblclick') {
|
||||
return false
|
||||
}
|
||||
|
||||
// if the target element is inside an element with the nowheel class, we prevent zooming
|
||||
if (isWrappedWithClass(event, noWheelClassName.value) && event.type === 'wheel') {
|
||||
return false
|
||||
}
|
||||
|
||||
// if the target element is inside an element with the nopan class, we prevent panning
|
||||
if (
|
||||
isWrappedWithClass(event, noPanClassName.value) &&
|
||||
(event.type !== 'wheel' || (panOnScroll.value && event.type === 'wheel'))
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!zoomOnPinch.value && event.ctrlKey && event.type === 'wheel') {
|
||||
return false
|
||||
}
|
||||
|
||||
// when there is no scroll handling enabled, we prevent all wheel events
|
||||
if (!zoomScroll && !panOnScroll.value && !pinchZoom && event.type === 'wheel') {
|
||||
return false
|
||||
}
|
||||
|
||||
// if the pane is not movable, we prevent dragging it with mousestart or touchstart
|
||||
if (!panOnDrag.value && (event.type === 'mousedown' || event.type === 'touchstart')) {
|
||||
return false
|
||||
}
|
||||
|
||||
// if the pane is only movable using allowed clicks
|
||||
if (
|
||||
Array.isArray(panOnDrag.value) &&
|
||||
!panOnDrag.value.includes(event.button) &&
|
||||
(event.type === 'mousedown' || event.type === 'touchstart')
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// We only allow right clicks if pan on drag is set to right-click
|
||||
const buttonAllowed =
|
||||
(Array.isArray(panOnDrag.value) && panOnDrag.value.includes(event.button)) || !event.button || event.button <= 1
|
||||
|
||||
// default filter for d3-zoom
|
||||
return (!event.ctrlKey || event.type === 'wheel') && buttonAllowed
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (panOnScroll.value && !zoomKeyPressed.value && !userSelectionActive.value) {
|
||||
if (selectionKeyPressed.value && userSelectionActive.value && !isZoomingOrPanning.value) {
|
||||
d3Zoom.on('zoom', null)
|
||||
} else if (!selectionKeyPressed.value && !userSelectionActive.value) {
|
||||
d3Zoom.on('zoom', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||
viewport.value = { x: event.transform.x, y: event.transform.y, zoom: event.transform.k }
|
||||
|
||||
const flowTransform = eventToFlowTransform(event.transform)
|
||||
|
||||
zoomedWithRightMouseButton = isRightClickPan(panOnDrag.value, mouseButton ?? 0)
|
||||
|
||||
emits.viewportChange(flowTransform)
|
||||
emits.move({ event, flowTransform })
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (panKeyPressed.value || (panOnScroll.value && !zoomKeyPressed.value && !userSelectionActive.value)) {
|
||||
d3Selection.on(
|
||||
'wheel.zoom',
|
||||
(event: WheelEvent) => {
|
||||
@@ -262,78 +335,6 @@ onMounted(() => {
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
d3Zoom.filter((event: MouseEvent) => {
|
||||
const zoomScroll = zoomKeyPressed.value || zoomOnScroll.value
|
||||
const pinchZoom = zoomOnPinch.value && event.ctrlKey
|
||||
|
||||
if (
|
||||
(panOnDrag.value === true || (Array.isArray(panOnDrag.value) && panOnDrag.value.includes(1))) &&
|
||||
event.button === 1 &&
|
||||
event.type === 'mousedown' &&
|
||||
((event.target as HTMLElement)?.closest('.vue-flow__node') || (event.target as HTMLElement)?.closest('.vue-flow__edge'))
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
// if all interactions are disabled, we prevent all zoom events
|
||||
if (!panOnDrag.value && !zoomScroll && !panOnScroll.value && !zoomOnDoubleClick.value && !zoomOnPinch.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
// during a selection we prevent all other interactions
|
||||
if (userSelectionActive.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
// if zoom on double click is disabled, we prevent the double click event
|
||||
if (!zoomOnDoubleClick.value && event.type === 'dblclick') {
|
||||
return false
|
||||
}
|
||||
|
||||
// if the target element is inside an element with the nowheel class, we prevent zooming
|
||||
if (isWrappedWithClass(event, noWheelClassName.value) && event.type === 'wheel') {
|
||||
return false
|
||||
}
|
||||
|
||||
// if the target element is inside an element with the nopan class, we prevent panning
|
||||
if (
|
||||
isWrappedWithClass(event, noPanClassName.value) &&
|
||||
(event.type !== 'wheel' || (panOnScroll.value && event.type === 'wheel'))
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!zoomOnPinch.value && event.ctrlKey && event.type === 'wheel') {
|
||||
return false
|
||||
}
|
||||
|
||||
// when there is no scroll handling enabled, we prevent all wheel events
|
||||
if (!zoomScroll && !panOnScroll.value && !pinchZoom && event.type === 'wheel') {
|
||||
return false
|
||||
}
|
||||
|
||||
// if the pane is not movable, we prevent dragging it with mousestart or touchstart
|
||||
if (!panOnDrag.value && (event.type === 'mousedown' || event.type === 'touchstart')) {
|
||||
return false
|
||||
}
|
||||
|
||||
// if the pane is only movable using allowed clicks
|
||||
if (
|
||||
Array.isArray(panOnDrag.value) &&
|
||||
!panOnDrag.value.includes(event.button) &&
|
||||
(event.type === 'mousedown' || event.type === 'touchstart')
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// We only allow right clicks if pan on drag is set to right-click
|
||||
const buttonAllowed =
|
||||
(Array.isArray(panOnDrag.value) && panOnDrag.value.includes(event.button)) || !event.button || event.button <= 1
|
||||
|
||||
// default filter for d3-zoom
|
||||
return (!event.ctrlKey || event.type === 'wheel') && buttonAllowed
|
||||
})
|
||||
})
|
||||
|
||||
function isRightClickPan(pan: FlowOptions['panOnDrag'], usedButton: number) {
|
||||
@@ -363,11 +364,11 @@ function eventToFlowTransform(eventTransform: ZoomTransform): ViewportTransform
|
||||
}
|
||||
|
||||
function setDimensions() {
|
||||
if (!viewportEl.value) {
|
||||
if (!viewportRef.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const { width, height } = getDimensions(viewportEl.value)
|
||||
const { width, height } = getDimensions(viewportRef.value)
|
||||
|
||||
if (width === 0 || height === 0) {
|
||||
emits.error(new VueFlowError(ErrorCode.MISSING_VIEWPORT_DIMENSIONS))
|
||||
@@ -390,7 +391,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="viewportEl" :key="`viewport-${id}`" class="vue-flow__viewport vue-flow__container">
|
||||
<div ref="viewportRef" :key="`viewport-${id}`" class="vue-flow__viewport vue-flow__container">
|
||||
<Pane
|
||||
:is-selecting="isSelecting"
|
||||
:class="{ connecting: !!connectionStartHandle, dragging: paneDragging, draggable: !!panOnDrag }"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import { onMounted, onUnmounted, provide, ref, useSlots } from 'vue'
|
||||
import { onUnmounted, provide, useSlots } from 'vue'
|
||||
import type { D3ZoomEvent } from 'd3-zoom'
|
||||
import Viewport from '../Viewport/Viewport.vue'
|
||||
import A11yDescriptions from '../../components/A11y/A11yDescriptions.vue'
|
||||
@@ -145,18 +145,12 @@ const dispose = useWatchProps({ modelValue, nodes: modelNodes, edges: modelEdges
|
||||
|
||||
useHooks(emit, hooks)
|
||||
|
||||
const el = ref<HTMLDivElement>()
|
||||
|
||||
provide(Slots, useSlots())
|
||||
|
||||
onUnmounted(() => {
|
||||
dispose()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
vueFlowRef.value = el.value!
|
||||
})
|
||||
|
||||
defineExpose<VueFlowStore>({
|
||||
vueFlowRef,
|
||||
hooks,
|
||||
@@ -174,8 +168,9 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="el" class="vue-flow">
|
||||
<div ref="vueFlowRef" class="vue-flow">
|
||||
<Viewport>
|
||||
<!-- Pseudo-slots for nodes and edges so we can collect the templates using `useSlots` -->
|
||||
<template #nodes>
|
||||
<template v-for="nodeName of Object.keys(getNodeTypes)">
|
||||
<slot :name="`node-${nodeName}`" />
|
||||
@@ -192,10 +187,12 @@ export default {
|
||||
<slot name="connection-line" />
|
||||
</template>
|
||||
|
||||
<!-- This slot will be passed down to the transformation-pane and rendered inside there, meaning it's affected by zooming/panning -->
|
||||
<template #zoom-pane>
|
||||
<slot name="zoom-pane" />
|
||||
</template>
|
||||
|
||||
<!-- This slot will render outside the transformation-pane, meaning it's *not* affected by zooming and panning -->
|
||||
<slot />
|
||||
</Viewport>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user