feat(core,nodes): implement a11y in nodes
Signed-off-by: braks <78412429+bcakmakoglu@users.noreply.github.com>
This commit is contained in:
Vendored
+5
-1
@@ -22,11 +22,12 @@ declare global {
|
|||||||
const addEdgeToStore: typeof import('./utils/store')['addEdgeToStore']
|
const addEdgeToStore: typeof import('./utils/store')['addEdgeToStore']
|
||||||
const applyChanges: typeof import('./utils/changes')['applyChanges']
|
const applyChanges: typeof import('./utils/changes')['applyChanges']
|
||||||
const applyEdgeChanges: typeof import('./utils/changes')['applyEdgeChanges']
|
const applyEdgeChanges: typeof import('./utils/changes')['applyEdgeChanges']
|
||||||
const applyExtent: typeof import('./utils/drag')['applyExtent']
|
|
||||||
const applyNodeChanges: typeof import('./utils/changes')['applyNodeChanges']
|
const applyNodeChanges: typeof import('./utils/changes')['applyNodeChanges']
|
||||||
|
const arrowKeyDiffs: typeof import('./utils/a11y')['arrowKeyDiffs']
|
||||||
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
||||||
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
|
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
|
||||||
const boxToRect: typeof import('./utils/graph')['boxToRect']
|
const boxToRect: typeof import('./utils/graph')['boxToRect']
|
||||||
|
const calcNextPosition: typeof import('./utils/drag')['calcNextPosition']
|
||||||
const checkElementBelowIsValid: typeof import('./composables/useHandle')['checkElementBelowIsValid']
|
const checkElementBelowIsValid: typeof import('./composables/useHandle')['checkElementBelowIsValid']
|
||||||
const clamp: typeof import('./utils/graph')['clamp']
|
const clamp: typeof import('./utils/graph')['clamp']
|
||||||
const clampPosition: typeof import('./utils/graph')['clampPosition']
|
const clampPosition: typeof import('./utils/graph')['clampPosition']
|
||||||
@@ -72,6 +73,7 @@ declare global {
|
|||||||
const getEdgeId: typeof import('./utils/graph')['getEdgeId']
|
const getEdgeId: typeof import('./utils/graph')['getEdgeId']
|
||||||
const getEdgePositions: typeof import('./utils/edge')['getEdgePositions']
|
const getEdgePositions: typeof import('./utils/edge')['getEdgePositions']
|
||||||
const getEventHandlerParams: typeof import('./utils/drag')['getEventHandlerParams']
|
const getEventHandlerParams: typeof import('./utils/drag')['getEventHandlerParams']
|
||||||
|
const getExtent: typeof import('./utils/drag')['getExtent']
|
||||||
const getHandle: typeof import('./utils/edge')['getHandle']
|
const getHandle: typeof import('./utils/edge')['getHandle']
|
||||||
const getHandleBounds: typeof import('./utils/node')['getHandleBounds']
|
const getHandleBounds: typeof import('./utils/node')['getHandleBounds']
|
||||||
const getHandlePosition: typeof import('./utils/edge')['getHandlePosition']
|
const getHandlePosition: typeof import('./utils/edge')['getHandlePosition']
|
||||||
@@ -102,6 +104,7 @@ declare global {
|
|||||||
const isEdgeVisible: typeof import('./utils/edge')['isEdgeVisible']
|
const isEdgeVisible: typeof import('./utils/edge')['isEdgeVisible']
|
||||||
const isGraphEdge: typeof import('./utils/graph')['isGraphEdge']
|
const isGraphEdge: typeof import('./utils/graph')['isGraphEdge']
|
||||||
const isGraphNode: typeof import('./utils/graph')['isGraphNode']
|
const isGraphNode: typeof import('./utils/graph')['isGraphNode']
|
||||||
|
const isInputDOMNode: typeof import('./composables/useKeyPress')['isInputDOMNode']
|
||||||
const isNode: typeof import('./utils/graph')['isNode']
|
const isNode: typeof import('./utils/graph')['isNode']
|
||||||
const isParentSelected: typeof import('./utils/graph')['isParentSelected']
|
const isParentSelected: typeof import('./utils/graph')['isParentSelected']
|
||||||
const isProxy: typeof import('vue')['isProxy']
|
const isProxy: typeof import('vue')['isProxy']
|
||||||
@@ -322,6 +325,7 @@ declare global {
|
|||||||
const useToString: typeof import('@vueuse/core')['useToString']
|
const useToString: typeof import('@vueuse/core')['useToString']
|
||||||
const useToggle: typeof import('@vueuse/core')['useToggle']
|
const useToggle: typeof import('@vueuse/core')['useToggle']
|
||||||
const useTransition: typeof import('@vueuse/core')['useTransition']
|
const useTransition: typeof import('@vueuse/core')['useTransition']
|
||||||
|
const useUpdateNodePositions: typeof import('./composables/useUpdateNodePositions')['default']
|
||||||
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
|
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
|
||||||
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
|
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
|
||||||
const useVModel: typeof import('@vueuse/core')['useVModel']
|
const useVModel: typeof import('@vueuse/core')['useVModel']
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { isNumber } from '@vueuse/core'
|
import { isNumber } from '@vueuse/core'
|
||||||
import type { GraphNode, HandleConnectable, NodeComponent, SnapGrid, XYZPosition } from '../../types'
|
import type { GraphNode, HandleConnectable, NodeComponent, SnapGrid, XYZPosition } from '../../types'
|
||||||
|
import { ARIA_NODE_DESC_KEY } from '../../utils/a11y'
|
||||||
|
|
||||||
const { id, type, name, draggable, selectable, connectable, ...props } = defineProps<{
|
const { id, type, name, draggable, selectable, connectable, ...props } = defineProps<{
|
||||||
id: string
|
id: string
|
||||||
draggable: boolean
|
draggable: boolean
|
||||||
selectable: boolean
|
selectable: boolean
|
||||||
connectable: HandleConnectable
|
connectable: HandleConnectable
|
||||||
|
focusable: boolean
|
||||||
snapGrid?: SnapGrid
|
snapGrid?: SnapGrid
|
||||||
type: NodeComponent | Function | Object | false
|
type: NodeComponent | Function | Object | false
|
||||||
name: string
|
name: string
|
||||||
@@ -17,6 +19,7 @@ const { id, type, name, draggable, selectable, connectable, ...props } = defineP
|
|||||||
provide(NodeId, id)
|
provide(NodeId, id)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
id: vueFlowId,
|
||||||
edges,
|
edges,
|
||||||
noPanClassName,
|
noPanClassName,
|
||||||
selectNodesOnDrag,
|
selectNodesOnDrag,
|
||||||
@@ -33,8 +36,12 @@ const {
|
|||||||
nodeExtent,
|
nodeExtent,
|
||||||
onNodesInitialized,
|
onNodesInitialized,
|
||||||
elevateNodesOnSelect,
|
elevateNodesOnSelect,
|
||||||
|
disableKeyboardA11y,
|
||||||
|
ariaLiveMessage,
|
||||||
} = $(useVueFlow())
|
} = $(useVueFlow())
|
||||||
|
|
||||||
|
const updateNodePositions = useUpdateNodePositions()
|
||||||
|
|
||||||
const node = $(useVModel(props, 'node'))
|
const node = $(useVModel(props, 'node'))
|
||||||
|
|
||||||
const parentNode = $computed(() => (node.parentNode ? getNode(node.parentNode) : undefined))
|
const parentNode = $computed(() => (node.parentNode ? getNode(node.parentNode) : undefined))
|
||||||
@@ -138,33 +145,22 @@ onNodesInitialized(() => {
|
|||||||
initialized.value = true
|
initialized.value = true
|
||||||
})
|
})
|
||||||
|
|
||||||
/** Initial clamp of node position */
|
|
||||||
until(initialized)
|
until(initialized)
|
||||||
.toBe(true)
|
.toBe(true)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const extent = applyExtent(node, nodeExtent, parentNode)
|
const { computedPosition, position } = calcNextPosition(node, node.position, nodeExtent, parentNode)
|
||||||
|
|
||||||
const clampedPos = clampPosition(node.computedPosition, extent)
|
node.computedPosition = { ...node.computedPosition, ...computedPosition }
|
||||||
|
node.position = position
|
||||||
node.computedPosition = { ...node.computedPosition, ...clampedPos }
|
|
||||||
node.position = {
|
|
||||||
x: node.computedPosition.x - (parentNode?.computedPosition.x || 0),
|
|
||||||
y: node.computedPosition.y - (parentNode?.computedPosition.y || 0),
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function updateInternals() {
|
function updateInternals() {
|
||||||
if (nodeElement.value) updateNodeDimensions([{ id, nodeElement: nodeElement.value, forceUpdate: true }])
|
if (nodeElement.value) updateNodeDimensions([{ id, nodeElement: nodeElement.value, forceUpdate: true }])
|
||||||
|
|
||||||
updatePosition(
|
const { computedPosition, position } = calcNextPosition(node, node.position, nodeExtent, parentNode)
|
||||||
{
|
|
||||||
x: node.position.x,
|
node.computedPosition = { ...node.computedPosition, ...computedPosition }
|
||||||
y: node.position.y,
|
node.position = position
|
||||||
// should be using computedPosition.z but in case it's not present, fall back to selected state
|
|
||||||
z: node.computedPosition.z ? node.computedPosition.z : node.selected ? 1000 : 0,
|
|
||||||
},
|
|
||||||
parentNode ? { ...parentNode.computedPosition } : undefined,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMouseEnter(event: MouseEvent) {
|
function onMouseEnter(event: MouseEvent) {
|
||||||
@@ -200,6 +196,36 @@ function onSelectNode(event: MouseEvent) {
|
|||||||
|
|
||||||
emit.click({ event, node, connectedEdges })
|
emit.click({ event, node, connectedEdges })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (isInputDOMNode(event)) return
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
if (elementSelectionKeys.includes(event.key) && selectable) {
|
||||||
|
const unselect = event.key === 'Escape'
|
||||||
|
|
||||||
|
if (unselect) {
|
||||||
|
nodeElement.value?.blur()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNodeClick(node, multiSelectionActive, addSelectedNodes, removeSelectedElements, $$(nodesSelectionActive), unselect)
|
||||||
|
} else if (!disableKeyboardA11y && draggable && node.selected && arrowKeyDiffs[event.key]) {
|
||||||
|
$$(ariaLiveMessage).value = `Moved selected node ${event.key.replace('Arrow', '').toLowerCase()}. New position, x: ${~~node
|
||||||
|
.position.x}, y: ${~~node.position.y}`
|
||||||
|
|
||||||
|
// by default a node moves 5px on each key press, or 20px if shift is pressed
|
||||||
|
// if snap grid is enabled, we use that for the velocity.
|
||||||
|
const xVelo = props.snapGrid ? props.snapGrid[0] : 5
|
||||||
|
const yVelo = props.snapGrid ? props.snapGrid[1] : 5
|
||||||
|
const factor = event.shiftKey ? 4 : 1
|
||||||
|
|
||||||
|
updateNodePositions({
|
||||||
|
x: arrowKeyDiffs[event.key].x * xVelo * factor,
|
||||||
|
y: arrowKeyDiffs[event.key].y * yVelo * factor,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@@ -229,6 +255,10 @@ export default {
|
|||||||
pointerEvents: selectable || draggable ? 'all' : 'none',
|
pointerEvents: selectable || draggable ? 'all' : 'none',
|
||||||
...getStyle,
|
...getStyle,
|
||||||
}"
|
}"
|
||||||
|
:tabIndex="focusable ? 0 : undefined"
|
||||||
|
:role="focusable ? 'button' : undefined"
|
||||||
|
:aria-describedby="disableKeyboardA11y ? undefined : `${ARIA_NODE_DESC_KEY}-${vueFlowId}`"
|
||||||
|
:aria-label="node.ariaLabel"
|
||||||
:data-id="node.id"
|
:data-id="node.id"
|
||||||
@mouseenter="onMouseEnter"
|
@mouseenter="onMouseEnter"
|
||||||
@mousemove="onMouseMove"
|
@mousemove="onMouseMove"
|
||||||
@@ -236,6 +266,7 @@ export default {
|
|||||||
@contextmenu="onContextMenu"
|
@contextmenu="onContextMenu"
|
||||||
@click="onSelectNode"
|
@click="onSelectNode"
|
||||||
@dblclick="onDoubleClick"
|
@dblclick="onDoubleClick"
|
||||||
|
@keydown="onKeyDown"
|
||||||
>
|
>
|
||||||
<component
|
<component
|
||||||
:is="type === false ? getNodeTypes.default : type"
|
:is="type === false ? getNodeTypes.default : type"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { drag } from 'd3-drag'
|
|||||||
import { select } from 'd3-selection'
|
import { select } from 'd3-selection'
|
||||||
import type { Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
import type { MaybeRef } from '@vueuse/core'
|
import type { MaybeRef } from '@vueuse/core'
|
||||||
import type { CoordinateExtent, NodeDragEvent, NodeDragItem, SnapGrid, XYPosition } from '~/types'
|
import type { NodeDragEvent, NodeDragItem, SnapGrid, XYPosition } from '~/types'
|
||||||
|
|
||||||
export type UseDragEvent = D3DragEvent<HTMLDivElement, null, SubjectPosition>
|
export type UseDragEvent = D3DragEvent<HTMLDivElement, null, SubjectPosition>
|
||||||
|
|
||||||
@@ -98,12 +98,18 @@ function useDrag(params: UseDragParams) {
|
|||||||
nextPosition.y = snapY * Math.round(nextPosition.y / snapY)
|
nextPosition.y = snapY * Math.round(nextPosition.y / snapY)
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentExtent = applyExtent(n, nodeExtent, n.parentNode ? getNode(n.parentNode) : undefined)
|
const { computedPosition, position } = calcNextPosition(
|
||||||
|
n,
|
||||||
|
nextPosition,
|
||||||
|
nodeExtent,
|
||||||
|
n.parentNode ? getNode(n.parentNode) : undefined,
|
||||||
|
)
|
||||||
|
|
||||||
// we want to make sure that we only fire a change event when there is a changes
|
// we want to make sure that we only fire a change event when there is a changes
|
||||||
hasChange = hasChange || n.position.x !== nextPosition.x || n.position.y !== nextPosition.y
|
hasChange = hasChange || n.position.x !== nextPosition.x || n.position.y !== nextPosition.y
|
||||||
|
|
||||||
n.position = currentExtent ? clampPosition(nextPosition, currentExtent as CoordinateExtent) : nextPosition
|
n.computedPosition = { ...n.computedPosition, ...computedPosition }
|
||||||
|
n.position = position
|
||||||
|
|
||||||
return n
|
return n
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,12 +2,11 @@ import type { Ref } from 'vue'
|
|||||||
import type { KeyFilter, MaybeRef } from '@vueuse/core'
|
import type { KeyFilter, MaybeRef } from '@vueuse/core'
|
||||||
import { isBoolean, isFunction } from '@vueuse/core'
|
import { isBoolean, isFunction } from '@vueuse/core'
|
||||||
|
|
||||||
function isInputDOMNode(event: KeyboardEvent): boolean {
|
export function isInputDOMNode(event: KeyboardEvent): boolean {
|
||||||
const target = (event.composedPath?.()?.[0] || event.target) as HTMLElement
|
const target = (event.composedPath?.()?.[0] || event.target) as HTMLElement
|
||||||
|
|
||||||
// we want to be able to do a multi selection event if we are in an input field
|
// we want to be able to do a multi selection event if we are in an input field
|
||||||
if (event.ctrlKey || event.metaKey || event.shiftKey) return false
|
if (event.ctrlKey || event.metaKey || event.shiftKey) return false
|
||||||
|
|
||||||
const hasAttribute = isFunction(target.hasAttribute) ? target.hasAttribute('contenteditable') : false
|
const hasAttribute = isFunction(target.hasAttribute) ? target.hasAttribute('contenteditable') : false
|
||||||
|
|
||||||
const closest = isFunction(target.closest) ? target.closest('.nokey') : null
|
const closest = isFunction(target.closest) ? target.closest('.nokey') : null
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { NodeDragItem, XYPosition } from '~/types'
|
||||||
|
|
||||||
|
function useUpdateNodePositions() {
|
||||||
|
const { getSelectedNodes, nodeExtent, updateNodePositions, findNode } = useVueFlow()
|
||||||
|
|
||||||
|
return (positionDiff: XYPosition) => {
|
||||||
|
const nodeUpdates = getSelectedNodes.value.flatMap((n) => {
|
||||||
|
if (n.computedPosition) {
|
||||||
|
const nextPosition = { x: n.computedPosition.x + positionDiff.x, y: n.computedPosition.y + positionDiff.y }
|
||||||
|
|
||||||
|
const updatedPos = calcNextPosition(n, nextPosition, nodeExtent.value, n.parentNode ? findNode(n.parentNode) : undefined)
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: n.id,
|
||||||
|
position: updatedPos.position,
|
||||||
|
computedPosition: { ...n.computedPosition, ...updatedPos.computedPosition },
|
||||||
|
from: n.position,
|
||||||
|
distance: { x: positionDiff.x, y: positionDiff.y },
|
||||||
|
dimensions: n.dimensions,
|
||||||
|
},
|
||||||
|
] as NodeDragItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
|
||||||
|
updateNodePositions(nodeUpdates, true, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useUpdateNodePositions
|
||||||
@@ -30,9 +30,7 @@ const {
|
|||||||
} = $(useVueFlow())
|
} = $(useVueFlow())
|
||||||
|
|
||||||
const selectable = (s?: boolean) => (typeof s === 'undefined' ? elementsSelectable : s)
|
const selectable = (s?: boolean) => (typeof s === 'undefined' ? elementsSelectable : s)
|
||||||
|
|
||||||
const updatable = (u?: EdgeUpdatable) => (typeof u === 'undefined' ? edgesUpdatable : u)
|
const updatable = (u?: EdgeUpdatable) => (typeof u === 'undefined' ? edgesUpdatable : u)
|
||||||
|
|
||||||
const focusable = (f?: boolean) => (typeof f === 'undefined' ? edgesFocusable : f)
|
const focusable = (f?: boolean) => (typeof f === 'undefined' ? edgesFocusable : f)
|
||||||
|
|
||||||
const sourceNode = $(
|
const sourceNode = $(
|
||||||
|
|||||||
@@ -4,13 +4,21 @@ import type { GraphNode, HandleConnectable, NodeComponent } from '../../types'
|
|||||||
|
|
||||||
const slots = inject(Slots)
|
const slots = inject(Slots)
|
||||||
|
|
||||||
const { nodesDraggable, elementsSelectable, nodesConnectable, getNodes, getNodeTypes, updateNodeDimensions, emits } = $(
|
const {
|
||||||
useVueFlow(),
|
nodesDraggable,
|
||||||
)
|
nodesFocusable,
|
||||||
|
elementsSelectable,
|
||||||
|
nodesConnectable,
|
||||||
|
getNodes,
|
||||||
|
getNodeTypes,
|
||||||
|
updateNodeDimensions,
|
||||||
|
emits,
|
||||||
|
} = $(useVueFlow())
|
||||||
|
|
||||||
const draggable = (d?: boolean) => (typeof d === 'undefined' ? nodesDraggable : d)
|
const draggable = (d?: boolean) => (typeof d === 'undefined' ? nodesDraggable : d)
|
||||||
const selectable = (s?: boolean) => (typeof s === 'undefined' ? elementsSelectable : s)
|
const selectable = (s?: boolean) => (typeof s === 'undefined' ? elementsSelectable : s)
|
||||||
const connectable = (c?: HandleConnectable) => (typeof c === 'undefined' ? nodesConnectable : c)
|
const connectable = (c?: HandleConnectable) => (typeof c === 'undefined' ? nodesConnectable : c)
|
||||||
|
const focusable = (f?: boolean) => (typeof f === 'undefined' ? nodesFocusable : f)
|
||||||
|
|
||||||
const resizeObserver = ref<ResizeObserver>()
|
const resizeObserver = ref<ResizeObserver>()
|
||||||
|
|
||||||
@@ -91,6 +99,7 @@ export default {
|
|||||||
:draggable="draggable(node.draggable)"
|
:draggable="draggable(node.draggable)"
|
||||||
:selectable="selectable(node.selectable)"
|
:selectable="selectable(node.selectable)"
|
||||||
:connectable="connectable(node.connectable)"
|
:connectable="connectable(node.connectable)"
|
||||||
|
:focusable="focusable(node.focusable)"
|
||||||
:node="node"
|
:node="node"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ const props = withDefaults(defineProps<FlowProps>(), {
|
|||||||
elevateEdgesOnSelect: undefined,
|
elevateEdgesOnSelect: undefined,
|
||||||
elevateNodesOnSelect: undefined,
|
elevateNodesOnSelect: undefined,
|
||||||
disableKeyboardA11y: undefined,
|
disableKeyboardA11y: undefined,
|
||||||
|
edgesFocusable: undefined,
|
||||||
|
nodesFocusable: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ const defaultState = (): State => ({
|
|||||||
|
|
||||||
edgesUpdatable: false,
|
edgesUpdatable: false,
|
||||||
edgesFocusable: true,
|
edgesFocusable: true,
|
||||||
|
nodesFocusable: true,
|
||||||
nodesConnectable: true,
|
nodesConnectable: true,
|
||||||
nodesDraggable: true,
|
nodesDraggable: true,
|
||||||
elementsSelectable: true,
|
elementsSelectable: true,
|
||||||
@@ -113,6 +114,7 @@ const defaultState = (): State => ({
|
|||||||
elevateNodesOnSelect: true,
|
elevateNodesOnSelect: true,
|
||||||
|
|
||||||
disableKeyboardA11y: false,
|
disableKeyboardA11y: false,
|
||||||
|
ariaLiveMessage: '',
|
||||||
|
|
||||||
__experimentalFeatures: {
|
__experimentalFeatures: {
|
||||||
nestedFlow: false,
|
nestedFlow: false,
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import type { Dimensions, ElementData, XYPosition } from './flow'
|
import type { Dimensions, ElementData, XYPosition, XYZPosition } from './flow'
|
||||||
import type { GraphNode, Node, NodeHandleBounds } from './node'
|
import type { GraphNode, Node, NodeHandleBounds } from './node'
|
||||||
import type { GraphEdge } from './edge'
|
import type { GraphEdge } from './edge'
|
||||||
|
|
||||||
export interface NodeDragItem {
|
export interface NodeDragItem {
|
||||||
id: string
|
id: string
|
||||||
// relative node position
|
// relative node position (to parent)
|
||||||
position: XYPosition
|
position: XYPosition
|
||||||
|
// absolute node position
|
||||||
|
computedPosition: XYZPosition
|
||||||
// distance from the mouse cursor to the node when start dragging
|
// distance from the mouse cursor to the node when start dragging
|
||||||
distance: XYPosition
|
distance: XYPosition
|
||||||
dimensions: Dimensions
|
dimensions: Dimensions
|
||||||
|
|||||||
@@ -160,6 +160,8 @@ export interface FlowProps {
|
|||||||
elevateNodesOnSelect?: boolean
|
elevateNodesOnSelect?: boolean
|
||||||
|
|
||||||
disableKeyboardA11y?: boolean
|
disableKeyboardA11y?: boolean
|
||||||
|
edgesFocusable?: boolean
|
||||||
|
nodesFocusable?: boolean
|
||||||
|
|
||||||
__experimentalFeatures?: {
|
__experimentalFeatures?: {
|
||||||
nestedFlow?: boolean
|
nestedFlow?: boolean
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export interface Node<Data = ElementData, CustomEvents extends Record<string, Cu
|
|||||||
draggable?: boolean
|
draggable?: boolean
|
||||||
selectable?: boolean
|
selectable?: boolean
|
||||||
connectable?: HandleConnectable
|
connectable?: HandleConnectable
|
||||||
|
focusable?: boolean
|
||||||
dragHandle?: string
|
dragHandle?: string
|
||||||
/** move on grid */
|
/** move on grid */
|
||||||
snapGrid?: SnapGrid
|
snapGrid?: SnapGrid
|
||||||
@@ -72,6 +73,8 @@ export interface Node<Data = ElementData, CustomEvents extends Record<string, Cu
|
|||||||
data?: Data
|
data?: Data
|
||||||
/** contextual and custom events that are passed to your custom components */
|
/** contextual and custom events that are passed to your custom components */
|
||||||
events?: Partial<NodeEventsHandler<CustomEvents>>
|
events?: Partial<NodeEventsHandler<CustomEvents>>
|
||||||
|
|
||||||
|
ariaLabel?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GraphNode<Data = ElementData, CustomEvents extends Record<string, CustomEvent> = any>
|
export interface GraphNode<Data = ElementData, CustomEvents extends Record<string, CustomEvent> = any>
|
||||||
|
|||||||
@@ -87,6 +87,8 @@ export interface State extends Omit<FlowOptions, 'id' | 'modelValue'> {
|
|||||||
|
|
||||||
edgesUpdatable: EdgeUpdatable
|
edgesUpdatable: EdgeUpdatable
|
||||||
edgesFocusable: boolean
|
edgesFocusable: boolean
|
||||||
|
|
||||||
|
nodesFocusable: boolean
|
||||||
nodesDraggable: boolean
|
nodesDraggable: boolean
|
||||||
nodesConnectable: boolean
|
nodesConnectable: boolean
|
||||||
|
|
||||||
@@ -119,6 +121,8 @@ export interface State extends Omit<FlowOptions, 'id' | 'modelValue'> {
|
|||||||
|
|
||||||
disableKeyboardA11y: boolean
|
disableKeyboardA11y: boolean
|
||||||
|
|
||||||
|
ariaLiveMessage: string
|
||||||
|
|
||||||
/** current vue flow version you're using */
|
/** current vue flow version you're using */
|
||||||
readonly vueFlowVersion: string
|
readonly vueFlowVersion: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
|
import type { XYPosition } from '~/types'
|
||||||
|
|
||||||
export const ARIA_NODE_DESC_KEY = 'vue-flow__node-desc'
|
export const ARIA_NODE_DESC_KEY = 'vue-flow__node-desc'
|
||||||
export const ARIA_EDGE_DESC_KEY = 'vue-flow__edge-desc'
|
export const ARIA_EDGE_DESC_KEY = 'vue-flow__edge-desc'
|
||||||
|
|
||||||
export const elementSelectionKeys = ['Enter', ' ', 'Escape']
|
export const elementSelectionKeys = ['Enter', ' ', 'Escape']
|
||||||
|
|
||||||
|
export const arrowKeyDiffs: Record<string, XYPosition> = {
|
||||||
|
ArrowUp: { x: 0, y: -1 },
|
||||||
|
ArrowDown: { x: 0, y: 1 },
|
||||||
|
ArrowLeft: { x: -1, y: 0 },
|
||||||
|
ArrowRight: { x: 1, y: 0 },
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ export function getDragItems(
|
|||||||
.map((n) =>
|
.map((n) =>
|
||||||
markRaw({
|
markRaw({
|
||||||
id: n.id,
|
id: n.id,
|
||||||
position: n.computedPosition || { x: 0, y: 0, z: 0 },
|
position: n.position || { x: 0, y: 0 },
|
||||||
|
computedPosition: n.computedPosition || { x: 0, y: 0, z: 1 },
|
||||||
distance: {
|
distance: {
|
||||||
x: mousePos.x - n.computedPosition?.x || 0,
|
x: mousePos.x - n.computedPosition?.x || 0,
|
||||||
y: mousePos.y - n.computedPosition?.y || 0,
|
y: mousePos.y - n.computedPosition?.y || 0,
|
||||||
@@ -59,7 +60,7 @@ export function getEventHandlerParams({
|
|||||||
return [id ? extendedDragItems.find((n) => n.id === id)! : extendedDragItems[0], extendedDragItems]
|
return [id ? extendedDragItems.find((n) => n.id === id)! : extendedDragItems[0], extendedDragItems]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyExtent<T extends NodeDragItem | GraphNode>(item: T, extent?: CoordinateExtent, parent?: GraphNode) {
|
export function getExtent<T extends NodeDragItem | GraphNode>(item: T, extent?: CoordinateExtent, parent?: GraphNode) {
|
||||||
let currentExtent = item.extent || extent
|
let currentExtent = item.extent || extent
|
||||||
|
|
||||||
if (item.extent === 'parent') {
|
if (item.extent === 'parent') {
|
||||||
@@ -95,3 +96,28 @@ export function applyExtent<T extends NodeDragItem | GraphNode>(item: T, extent?
|
|||||||
|
|
||||||
return currentExtent as CoordinateExtent
|
return currentExtent as CoordinateExtent
|
||||||
}
|
}
|
||||||
|
export const calcNextPosition = (
|
||||||
|
node: GraphNode | NodeDragItem,
|
||||||
|
nextPosition: XYPosition,
|
||||||
|
nodeExtent?: CoordinateExtent,
|
||||||
|
parentNode?: GraphNode,
|
||||||
|
) => {
|
||||||
|
const extent = getExtent(node, nodeExtent, parentNode)
|
||||||
|
|
||||||
|
const clampedPos = clampPosition(nextPosition, extent)
|
||||||
|
|
||||||
|
const parentPosition = { x: 0, y: 0 }
|
||||||
|
|
||||||
|
if (parentNode) {
|
||||||
|
parentPosition.x = parentNode.computedPosition.x
|
||||||
|
parentPosition.y = parentNode.computedPosition.y
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
position: {
|
||||||
|
x: clampedPos.x - parentPosition.x,
|
||||||
|
y: clampedPos.y - parentPosition.y,
|
||||||
|
},
|
||||||
|
computedPosition: clampedPos,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export const parseNode = (node: Node, nodeExtent: CoordinateExtent, defaults?: P
|
|||||||
draggable: undefined,
|
draggable: undefined,
|
||||||
selectable: undefined,
|
selectable: undefined,
|
||||||
connectable: undefined,
|
connectable: undefined,
|
||||||
|
focusable: undefined,
|
||||||
selected: false,
|
selected: false,
|
||||||
dragging: false,
|
dragging: false,
|
||||||
resizing: false,
|
resizing: false,
|
||||||
@@ -125,6 +126,7 @@ export const parseEdge = (edge: Edge, defaults?: Partial<GraphEdge>): GraphEdge
|
|||||||
targetY: 0 || defaults?.targetY,
|
targetY: 0 || defaults?.targetY,
|
||||||
updatable: edge.updatable ?? defaults?.updatable,
|
updatable: edge.updatable ?? defaults?.updatable,
|
||||||
selectable: edge.selectable ?? defaults?.selectable,
|
selectable: edge.selectable ?? defaults?.selectable,
|
||||||
|
focusable: edge.focusable ?? defaults?.focusable,
|
||||||
data,
|
data,
|
||||||
events: markRaw(events),
|
events: markRaw(events),
|
||||||
label: (edge.label && !isString(edge.label) ? markRaw(edge.label) : edge.label) || defaults?.label,
|
label: (edge.label && !isString(edge.label) ? markRaw(edge.label) : edge.label) || defaults?.label,
|
||||||
@@ -369,6 +371,6 @@ export const getMarkerId = (marker: EdgeMarkerType | undefined, vueFlowId?: stri
|
|||||||
|
|
||||||
return `${idPrefix}${Object.keys(marker)
|
return `${idPrefix}${Object.keys(marker)
|
||||||
.sort()
|
.sort()
|
||||||
.map((key: string) => `${key}=${marker[<keyof EdgeMarkerType>key]}`)
|
.map((key) => `${key}=${marker[<keyof EdgeMarkerType>key]}`)
|
||||||
.join('&')}`
|
.join('&')}`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,12 +30,13 @@ export const handleNodeClick = (
|
|||||||
addSelectedNodes: Actions['addSelectedNodes'],
|
addSelectedNodes: Actions['addSelectedNodes'],
|
||||||
removeSelectedElements: Actions['removeSelectedElements'],
|
removeSelectedElements: Actions['removeSelectedElements'],
|
||||||
nodesSelectionActive: Ref<boolean>,
|
nodesSelectionActive: Ref<boolean>,
|
||||||
|
unselect = false,
|
||||||
) => {
|
) => {
|
||||||
nodesSelectionActive.value = false
|
nodesSelectionActive.value = false
|
||||||
|
|
||||||
if (!node.selected) {
|
if (!node.selected) {
|
||||||
addSelectedNodes([node])
|
addSelectedNodes([node])
|
||||||
} else if (node.selected && multiSelectionActive) {
|
} else if (unselect || (node.selected && multiSelectionActive)) {
|
||||||
removeSelectedElements([node])
|
removeSelectedElements([node])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user