diff --git a/packages/core/src/container/Viewport/Viewport.vue b/packages/core/src/container/Viewport/Viewport.vue index 0dbec52e..e52a9105 100644 --- a/packages/core/src/container/Viewport/Viewport.vue +++ b/packages/core/src/container/Viewport/Viewport.vue @@ -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() - -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().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) => { - 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) => { 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) => { + 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 {