fix: zoom pan helper

This commit is contained in:
Braks
2021-10-20 22:39:54 +02:00
parent a895853b64
commit c3fb3cd10a
38 changed files with 214 additions and 277 deletions
+7
View File
@@ -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'
+31
View File
@@ -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))
}
+61
View File
@@ -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,
}
}
+46
View File
@@ -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
}
+23
View File
@@ -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
}
+15
View File
@@ -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
View File
@@ -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)) {
+62
View File
@@ -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)
},
}
}