fix: zoom pan helper
This commit is contained in:
@@ -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'
|
||||
@@ -0,0 +1,31 @@
|
||||
import useKeyPress from './useKeyPress'
|
||||
import { isNode, getConnectedEdges } from '~/utils/graph'
|
||||
import { Elements, KeyCode, ElementId, FlowElement } from '~/types'
|
||||
import { Store } from '~/context'
|
||||
|
||||
interface HookParams {
|
||||
deleteKeyCode: KeyCode
|
||||
multiSelectionKeyCode: KeyCode
|
||||
onElementsRemove?: (elements: Elements) => void
|
||||
}
|
||||
|
||||
export default ({ deleteKeyCode, multiSelectionKeyCode, onElementsRemove = () => {} }: HookParams): void => {
|
||||
const store = inject(Store)!
|
||||
|
||||
useKeyPress(deleteKeyCode, (keyPressed) => {
|
||||
if (keyPressed && store.selectedElements) {
|
||||
const selectedNodes = store.selectedElements.filter(isNode)
|
||||
const connectedEdges = getConnectedEdges(selectedNodes, store.edges)
|
||||
const elementsToRemove = [...store.selectedElements, ...connectedEdges].reduce(
|
||||
(res, item) => res.set(item.id, item),
|
||||
new Map<ElementId, FlowElement>(),
|
||||
)
|
||||
|
||||
onElementsRemove(Array.from(elementsToRemove.values()))
|
||||
store.unsetNodesSelection()
|
||||
store.resetSelectedElements()
|
||||
}
|
||||
})
|
||||
|
||||
useKeyPress(multiSelectionKeyCode, (keyPressed) => (store.multiSelectionActive = keyPressed))
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// global hooks
|
||||
import { Connection, Edge, Elements, FlowHooks, FlowTransform, Node, OnConnectStartParams, OnLoadParams } from '~/types'
|
||||
|
||||
export default (): FlowHooks & { bind: (emit: (event: string, ...args: any[]) => void) => FlowHooks } => {
|
||||
const eventHooks: FlowHooks = {
|
||||
elementClick: createEventHook<{ event: MouseEvent; element: Node | Edge }>(),
|
||||
elementsRemove: createEventHook<Elements>(),
|
||||
nodeDoubleClick: createEventHook<{ event: MouseEvent; node: Node }>(),
|
||||
nodeClick: createEventHook<{ event: MouseEvent; node: Node }>(),
|
||||
nodeMouseEnter: createEventHook<{ event: MouseEvent; node: Node }>(),
|
||||
nodeMouseMove: createEventHook<{ event: MouseEvent; node: Node }>(),
|
||||
nodeMouseLeave: createEventHook<{ event: MouseEvent; node: Node }>(),
|
||||
nodeContextMenu: createEventHook<{ event: MouseEvent; node: Node }>(),
|
||||
nodeDragStart: createEventHook<{ event: MouseEvent; node: Node }>(),
|
||||
nodeDrag: createEventHook<{ event: MouseEvent; node: Node }>(),
|
||||
nodeDragStop: createEventHook<{ event: MouseEvent; node: Node }>(),
|
||||
connect: createEventHook<Connection>(),
|
||||
connectStart: createEventHook<{
|
||||
event: MouseEvent
|
||||
params: OnConnectStartParams
|
||||
}>(),
|
||||
connectStop: createEventHook<MouseEvent>(),
|
||||
connectEnd: createEventHook<MouseEvent>(),
|
||||
load: createEventHook<OnLoadParams>(),
|
||||
move: createEventHook<FlowTransform | undefined>(),
|
||||
moveStart: createEventHook<FlowTransform | undefined>(),
|
||||
moveEnd: createEventHook<FlowTransform | undefined>(),
|
||||
selectionChange: createEventHook<Elements | null>(),
|
||||
selectionDragStart: createEventHook<{ event: MouseEvent; nodes: Node[] }>(),
|
||||
selectionDrag: createEventHook<{ event: MouseEvent; nodes: Node[] }>(),
|
||||
selectionDragStop: createEventHook<{ event: MouseEvent; nodes: Node[] }>(),
|
||||
selectionContextMenu: createEventHook<{ event: MouseEvent; nodes: Node[] }>(),
|
||||
paneScroll: createEventHook<WheelEvent | undefined>(),
|
||||
paneClick: createEventHook<MouseEvent>(),
|
||||
paneContextMenu: createEventHook<MouseEvent>(),
|
||||
edgeUpdate: createEventHook<{ edge: Edge; connection: Connection }>(),
|
||||
edgeContextMenu: createEventHook<{ event: MouseEvent; edge: Edge }>(),
|
||||
edgeMouseEnter: createEventHook<{ event: MouseEvent; edge: Edge }>(),
|
||||
edgeMouseMove: createEventHook<{ event: MouseEvent; edge: Edge }>(),
|
||||
edgeMouseLeave: createEventHook<{ event: MouseEvent; edge: Edge }>(),
|
||||
edgeDoubleClick: createEventHook<{ event: MouseEvent; edge: Edge }>(),
|
||||
edgeClick: createEventHook<{ event: MouseEvent; edge: Edge }>(),
|
||||
edgeUpdateStart: createEventHook<{ event: MouseEvent; edge: Edge }>(),
|
||||
edgeUpdateEnd: createEventHook<{ event: MouseEvent; edge: Edge }>(),
|
||||
}
|
||||
|
||||
const bind = (emit: (event: string, args: any) => void) => {
|
||||
for (const [key, value] of Object.entries(eventHooks)) {
|
||||
value.on((data: any) => {
|
||||
emit(key, data)
|
||||
})
|
||||
}
|
||||
|
||||
return eventHooks
|
||||
}
|
||||
|
||||
return {
|
||||
...eventHooks,
|
||||
bind,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Ref } from 'vue'
|
||||
import { onKeyDown, onKeyPressed, onKeyUp } from '@vueuse/core'
|
||||
import { KeyCode } from '~/types'
|
||||
|
||||
export default (keyCode: KeyCode, onChange?: (keyPressed: boolean) => void): Ref<boolean> => {
|
||||
const isPressed = controlledRef<boolean>(false, {
|
||||
onBeforeChange(val, oldVal) {
|
||||
if (val === oldVal) return false
|
||||
},
|
||||
onChanged() {
|
||||
if (onChange && typeof onChange === 'function') onChange(isPressed.value)
|
||||
},
|
||||
})
|
||||
|
||||
onKeyPressed(
|
||||
(e) => e.key === keyCode || e.keyCode === keyCode,
|
||||
(e) => {
|
||||
e.preventDefault()
|
||||
isPressed.value = true
|
||||
},
|
||||
)
|
||||
|
||||
onKeyDown(
|
||||
(e) => e.key === keyCode || e.keyCode === keyCode,
|
||||
(e) => {
|
||||
e.preventDefault()
|
||||
isPressed.value = true
|
||||
},
|
||||
)
|
||||
|
||||
onKeyUp(
|
||||
(event) => event.key === keyCode || event.keyCode === keyCode,
|
||||
(e) => {
|
||||
e.preventDefault()
|
||||
isPressed.value = false
|
||||
},
|
||||
)
|
||||
|
||||
useEventListener(window, 'blur', () => {
|
||||
isPressed.value = false
|
||||
})
|
||||
|
||||
if (onChange && typeof onChange === 'function') onChange(isPressed.value)
|
||||
|
||||
return isPressed
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Ref } from 'vue'
|
||||
import { getDimensions } from '~/utils'
|
||||
import { Dimensions } from '~/types'
|
||||
|
||||
export default function (el: Ref<HTMLDivElement>) {
|
||||
const dimensions = ref<Dimensions>({ width: 0, height: 0 })
|
||||
const updateDimensions = () => {
|
||||
const unrefEl = unrefElement(el)
|
||||
if (!unrefEl) return
|
||||
|
||||
const size = getDimensions(unrefEl as HTMLDivElement)
|
||||
if (size.height === 0 || size.width === 0)
|
||||
console.log('The revue Flow parent container needs a width and a height to render the graph.')
|
||||
else dimensions.value = size
|
||||
}
|
||||
|
||||
useEventListener(window, 'resize', updateDimensions)
|
||||
useResizeObserver(el, () => updateDimensions())
|
||||
|
||||
until(el).toBeTruthy().then(updateDimensions)
|
||||
|
||||
return dimensions
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ElementId, RevueFlowStore, UpdateNodeInternals } from '../types'
|
||||
|
||||
function useUpdateNodeInternals(): UpdateNodeInternals {
|
||||
const store = inject<RevueFlowStore>('store')!
|
||||
|
||||
return (id: ElementId) => {
|
||||
const nodeElement: HTMLDivElement | null = document.querySelector(`.revue-flow__node[data-id="${id}"]`)
|
||||
|
||||
if (nodeElement) {
|
||||
store.updateNodeDimensions([{ id, nodeElement, forceUpdate: true }])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default useUpdateNodeInternals
|
||||
+13
-33
@@ -2,20 +2,10 @@ import { D3ZoomEvent, zoom, zoomIdentity, ZoomTransform } from 'd3-zoom'
|
||||
import { pointer, select } from 'd3-selection'
|
||||
import { Ref } from 'vue'
|
||||
import { get } from '@vueuse/core'
|
||||
import {
|
||||
D3Selection,
|
||||
D3Zoom,
|
||||
FlowTransform,
|
||||
InitD3ZoomPayload,
|
||||
KeyCode,
|
||||
PanOnScrollMode,
|
||||
Transform,
|
||||
TranslateExtent,
|
||||
} from '~/types'
|
||||
import { D3Selection, D3Zoom, FlowTransform, KeyCode, PanOnScrollMode, Transform } from '~/types'
|
||||
import { clamp } from '~/utils'
|
||||
import useKeyPress from '~/hooks/useKeyPress'
|
||||
import { Hooks } from '~/context/symbols'
|
||||
import { onLoadGetElements, onLoadProject, onLoadToObject } from '~/utils/graph'
|
||||
import useKeyPress from '~/composables/useKeyPress'
|
||||
import { Hooks, Store } from '~/context'
|
||||
|
||||
const viewChanged = (prevTransform: FlowTransform, eventTransform: ZoomTransform): boolean =>
|
||||
prevTransform.x !== eventTransform.x || prevTransform.y !== eventTransform.y || prevTransform.zoom !== eventTransform.k
|
||||
@@ -30,11 +20,8 @@ interface UseZoomOptions {
|
||||
selectionKeyCode?: KeyCode
|
||||
zoomActivationKeyCode?: KeyCode
|
||||
paneMoveable?: boolean
|
||||
minZoom?: number
|
||||
maxZoom?: number
|
||||
defaultZoom?: number
|
||||
defaultPosition?: [number, number]
|
||||
translateExtent?: TranslateExtent
|
||||
zoomOnScroll?: boolean
|
||||
zoomOnPinch?: boolean
|
||||
panOnScroll?: boolean
|
||||
@@ -49,22 +36,12 @@ interface UseZoom {
|
||||
d3Selection: Ref<D3Selection>
|
||||
}
|
||||
|
||||
export default function (
|
||||
el: Ref<HTMLDivElement>,
|
||||
options: UseZoomOptions,
|
||||
init: (initD3ZoomPayload: InitD3ZoomPayload) => void = () => {},
|
||||
): UseZoom {
|
||||
export default function (el: Ref<HTMLDivElement>, options: UseZoomOptions): UseZoom {
|
||||
const {
|
||||
selectionKeyCode = 'Shift',
|
||||
zoomActivationKeyCode = 'Meta',
|
||||
minZoom = 0.5,
|
||||
maxZoom = 2,
|
||||
defaultZoom = 1,
|
||||
defaultPosition = [0, 0],
|
||||
translateExtent = [
|
||||
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
||||
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
|
||||
],
|
||||
zoomOnScroll = true,
|
||||
zoomOnPinch = true,
|
||||
zoomOnDoubleClick = true,
|
||||
@@ -74,13 +51,17 @@ export default function (
|
||||
paneMoveable = true,
|
||||
} = options
|
||||
const hooks = inject(Hooks)!
|
||||
const store = inject(Store)!
|
||||
const prevTransform = ref<FlowTransform>({ x: 0, y: 0, zoom: 0 })
|
||||
|
||||
const clampedX = clamp(defaultPosition[0], translateExtent[0][0], translateExtent[1][0])
|
||||
const clampedY = clamp(defaultPosition[1], translateExtent[0][1], translateExtent[1][1])
|
||||
const clampedZoom = clamp(defaultZoom, minZoom, maxZoom)
|
||||
const clampedX = clamp(defaultPosition[0], store.translateExtent[0][0], store.translateExtent[1][0])
|
||||
const clampedY = clamp(defaultPosition[1], store.translateExtent[0][1], store.translateExtent[1][1])
|
||||
const clampedZoom = clamp(defaultZoom, store.minZoom, store.maxZoom)
|
||||
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()
|
||||
|
||||
until(el)
|
||||
@@ -93,8 +74,7 @@ export default function (
|
||||
|
||||
const updatedTransform = zoomIdentity.translate(clampedX, clampedY).scale(clampedZoom)
|
||||
d3z.transform(d3s, updatedTransform)
|
||||
init({ d3Zoom: d3z, d3ZoomHandler, d3Selection: d3s })
|
||||
|
||||
store.initD3Zoom({ d3Zoom: d3z, d3Selection: d3s, d3ZoomHandler })
|
||||
const applyZoomHandlers = () => {
|
||||
d3z.on('start', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||
if (viewChanged(prevTransform.value, event.transform)) {
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user