Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): add VueFlowProvider #1482

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
5 changes: 5 additions & 0 deletions .changeset/breezy-chefs-add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@vue-flow/core": minor
---

Allow passing event target to `useKeyPress`
5 changes: 5 additions & 0 deletions .changeset/clever-cougars-guess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@vue-flow/core": minor
---

Use pointer events to capture interactions on `Pane` cmp and prevent selections from being cancelled when moving outside of the `Pane` while holding selection key
5 changes: 5 additions & 0 deletions .changeset/giant-mayflies-study.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@vue-flow/core": patch
---

Calculate correct handle position in handle lookup
5 changes: 5 additions & 0 deletions .changeset/weak-timers-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@vue-flow/core": minor
---

Make `useKeyPress` public and export it
6 changes: 1 addition & 5 deletions packages/core/src/components/ConnectionLine/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,7 @@ const ConnectionLine = defineComponent({

const fromHandle = (startHandleId ? handleBounds.find((d) => d.id === startHandleId) : handleBounds[0]) ?? null
const fromPosition = fromHandle?.position || Position.Top
const { x: fromX, y: fromY } = getHandlePosition(
fromPosition,
{ ...fromNode.value.dimensions, ...fromNode.value.computedPosition },
fromHandle,
)
const { x: fromX, y: fromY } = getHandlePosition(fromNode.value, fromHandle, fromPosition)

let toHandle: HandleElement | null = null
if (toNode.value && connectionEndHandle.value?.handleId) {
Expand Down
17 changes: 6 additions & 11 deletions packages/core/src/components/Edges/EdgeWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import {
ErrorCode,
VueFlowError,
elementSelectionKeys,
getEdgePositions,
getHandle,
getHandlePosition,
getMarkerId,
} from '../../utils'
import EdgeAnchor from './EdgeAnchor'
Expand Down Expand Up @@ -160,19 +160,14 @@ const EdgeWrapper = defineComponent({

const targetHandle = getHandle(targetNodeHandles, edge.value.targetHandle)

const sourcePosition = sourceHandle ? sourceHandle.position : Position.Bottom
const sourcePosition = sourceHandle?.position || Position.Bottom

const targetPosition = targetHandle ? targetHandle.position : Position.Top
const targetPosition = targetHandle?.position || Position.Top

const { sourceX, sourceY, targetY, targetX } = getEdgePositions(
sourceNode,
sourceHandle,
sourcePosition,
targetNode,
targetHandle,
targetPosition,
)
const { x: sourceX, y: sourceY } = getHandlePosition(sourceNode, sourceHandle, sourcePosition)
const { x: targetX, y: targetY } = getHandlePosition(targetNode, targetHandle, targetPosition)

// todo: let's avoid writing these here (in v2 we want to remove all of these self-managed refs)
edge.value.sourceX = sourceX
edge.value.sourceY = sourceY
edge.value.targetX = targetX
Expand Down
55 changes: 29 additions & 26 deletions packages/core/src/components/Handle/Handle.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<script lang="ts" setup>
import { until } from '@vueuse/core'
import { computed, onUnmounted, ref, toRef } from 'vue'
import { computed, onMounted, onUnmounted, ref, toRef } from 'vue'
import type { HandleProps } from '../../types'
import { Position } from '../../types'
import { useHandle, useNode, useVueFlow } from '../../composables'
Expand Down Expand Up @@ -97,38 +96,42 @@ const isConnectable = computed(() => {

// todo: remove this and have users handle this themselves using `updateNodeInternals`
// set up handle bounds if they don't exist yet and the node has been initialized (i.e. the handle was added after the node has already been mounted)
until(() => !!node.dimensions.width && !!node.dimensions.height)
.toBe(true, { flush: 'post' })
.then(() => {
const existingBounds = node.handleBounds[type.value]?.find((b) => b.id === handleId)
onMounted(() => {
// if the node isn't initialized yet, we can't set up the handle bounds
// the handle bounds will be automatically set up when the node is initialized (`updateNodeDimensions`)
if (!node.dimensions.width || !node.dimensions.height) {
return
}

if (!vueFlowRef.value || existingBounds) {
return
}
const existingBounds = node.handleBounds[type.value]?.find((b) => b.id === handleId)

const viewportNode = vueFlowRef.value.querySelector('.vue-flow__transformationpane')
if (!vueFlowRef.value || existingBounds) {
return
}

if (!nodeEl.value || !handle.value || !viewportNode || !handleId) {
return
}
const viewportNode = vueFlowRef.value.querySelector('.vue-flow__transformationpane')

const nodeBounds = nodeEl.value.getBoundingClientRect()
if (!nodeEl.value || !handle.value || !viewportNode || !handleId) {
return
}

const nodeBounds = nodeEl.value.getBoundingClientRect()

const handleBounds = handle.value.getBoundingClientRect()
const handleBounds = handle.value.getBoundingClientRect()

const style = window.getComputedStyle(viewportNode)
const { m22: zoom } = new window.DOMMatrixReadOnly(style.transform)
const style = window.getComputedStyle(viewportNode)
const { m22: zoom } = new window.DOMMatrixReadOnly(style.transform)

const nextBounds = {
id: handleId,
position,
x: (handleBounds.left - nodeBounds.left) / zoom,
y: (handleBounds.top - nodeBounds.top) / zoom,
...getDimensions(handle.value),
}
const nextBounds = {
id: handleId,
position,
x: (handleBounds.left - nodeBounds.left) / zoom,
y: (handleBounds.top - nodeBounds.top) / zoom,
...getDimensions(handle.value),
}

node.handleBounds[type.value] = [...(node.handleBounds[type.value] ?? []), nextBounds]
})
node.handleBounds[type.value] = [...(node.handleBounds[type.value] ?? []), nextBounds]
})

onUnmounted(() => {
// clean up node internals
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<script lang="ts" setup>
import { inject } from 'vue'
import { useVueFlow } from '../../composables'
import { VueFlow } from '../../context'
import type { VueFlowProviderProps } from '../../types'

const props = defineProps<VueFlowProviderProps>()

const hasInjection = inject(VueFlow, null)

if (!hasInjection) {
// createVueFlow() or setupVueFlow()
useVueFlow({
nodes: props.initialNodes,
edges: props.initialEdges,
fitViewOnInit: props.fitViewOnInit,
})
}
</script>

<template>
<slot />
</template>
38 changes: 14 additions & 24 deletions packages/core/src/composables/useKeyPress.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { MaybeRefOrGetter } from 'vue'
import { ref, toValue, watch } from 'vue'
import { onMounted, ref, toValue, watch } from 'vue'
import type { KeyFilter, KeyPredicate } from '@vueuse/core'
import { onKeyStroke, useEventListener } from '@vueuse/core'
import { useWindow } from './useWindow'

export interface UseKeyPressOptions {
actInsideInputWithModifier?: MaybeRefOrGetter<boolean>
target?: MaybeRefOrGetter<EventTarget>
}

export function isInputDOMNode(event: KeyboardEvent): boolean {
Expand Down Expand Up @@ -68,22 +69,19 @@ function useKeyOrCode(code: string, keysToWatch: string | string[]) {
return keysToWatch.includes(code) ? 'code' : 'key'
}

const window = useWindow()

/**
* Reactive key press state
* Composable that returns a boolean value if a key is pressed
*
* todo: make this public?
* @internal
* @param keyFilter - Can be a boolean, a string or an array of strings. If it's a boolean, it will always return that value. If it's a string, it will return true if the key is pressed. If it's an array of strings, it will return true if any of the keys are pressed, or a combination is pressed (e.g. ['ctrl+a', 'ctrl+b'])
* @param onChange - Callback function that will be called when the key state changes
* @public
* @param keyFilter - Can be a boolean, a string, an array of strings or a function that returns a boolean. If it's a boolean, it will act as if the key is always pressed. If it's a string, it will return true if a key matching that string is pressed. If it's an array of strings, it will return true if any of the strings match a key being pressed, or a combination (e.g. ['ctrl+a', 'ctrl+b'])
* @param options - Options object
*/
export function useKeyPress(
keyFilter: MaybeRefOrGetter<KeyFilter | null>,
onChange?: (keyPressed: boolean) => void,
options: UseKeyPressOptions = { actInsideInputWithModifier: true },
options: UseKeyPressOptions = { actInsideInputWithModifier: true, target: window },
) {
const window = useWindow()

const isPressed = ref(toValue(keyFilter) === true)

let modifierPressed = false
Expand All @@ -92,12 +90,6 @@ export function useKeyPress(

let currentFilter = createKeyFilterFn(toValue(keyFilter))

watch(isPressed, (isKeyPressed, wasPressed) => {
if (isKeyPressed !== wasPressed) {
onChange?.(isKeyPressed)
}
})

watch(
() => toValue(keyFilter),
(nextKeyFilter, previousKeyFilter) => {
Expand All @@ -113,10 +105,8 @@ export function useKeyPress(
},
)

useEventListener(window, 'blur', () => {
if (toValue(keyFilter) !== true) {
isPressed.value = false
}
onMounted(() => {
useEventListener(window, ['blur', 'contextmenu'], reset)
})

onKeyStroke(
Expand All @@ -134,7 +124,7 @@ export function useKeyPress(

isPressed.value = true
},
{ eventName: 'keydown' },
{ eventName: 'keydown', target: options.target },
)

onKeyStroke(
Expand All @@ -150,11 +140,9 @@ export function useKeyPress(
reset()
}
},
{ eventName: 'keyup' },
{ eventName: 'keyup', target: options.target },
)

return isPressed

function reset() {
modifierPressed = false

Expand Down Expand Up @@ -184,4 +172,6 @@ export function useKeyPress(

return keyFilter
}

return isPressed
}
2 changes: 1 addition & 1 deletion packages/core/src/composables/useVueFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
*
* @public
* @returns a vue flow store instance
* @param idOrOpts - id of the store instance or options to create a new store instance
* @param idOrOpts - id of the store instance or options to pass to the store instance (options are deprecated!)

Check warning on line 22 in packages/core/src/composables/useVueFlow.ts

View workflow job for this annotation

GitHub Actions / build-and-test (ubuntu-latest, 20)

Expected @param names to be "id". Got "idOrOpts"
*/
export function useVueFlow(id?: string): VueFlowStore
export function useVueFlow(options?: FlowOptions): VueFlowStore
Expand Down
14 changes: 7 additions & 7 deletions packages/core/src/composables/useWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ type UseWindow = Window & typeof globalThis & { chrome?: any }
export function useWindow(): UseWindow {
if (typeof window !== 'undefined') {
return window as UseWindow
} else {
return {
chrome: false,
addEventListener(..._: Parameters<Window['addEventListener']>) {
// do nothing
},
} as UseWindow
}

return {
chrome: false,
addEventListener(..._: Parameters<Window['addEventListener']>) {
// do nothing
},
} as UseWindow
}
Loading
Loading