update(script-setup): Refactor additional-components

* Remove jsx files

fix: ConnectionLine.vue not recalculating properly
This commit is contained in:
Braks
2021-10-19 13:58:49 +02:00
parent 560bdc203b
commit 1ff3a8dc9c
26 changed files with 359 additions and 1269 deletions

View File

@@ -0,0 +1,59 @@
<script lang="ts" setup>
import { HTMLAttributes } from 'vue'
import { BackgroundVariant, RevueFlowStore } from '~/types'
export interface BackgroundProps extends HTMLAttributes {
variant?: BackgroundVariant
gap?: number
color?: string
size?: number
}
const props = withDefaults(defineProps<BackgroundProps>(), {
variant: BackgroundVariant.Dots,
gap: 10,
size: 0.4,
})
const defaultColors: Record<BackgroundVariant, string> = {
[BackgroundVariant.Dots]: '#81818a',
[BackgroundVariant.Lines]: '#eee',
}
const store = inject<RevueFlowStore>('store')!
const transform = computed(() => store.transform)
// when there are multiple flows on a page we need to make sure that every background gets its own pattern.
const patternId = `pattern-${Math.floor(Math.random() * 100000)}`
const bgClasses = ['revue-flow__background']
const scaledGap = computed(() => props.gap && props.gap * transform.value[2])
const xOffset = computed(() => scaledGap.value && transform.value[0] % scaledGap.value)
const yOffset = computed(() => scaledGap.value && transform.value[1] % scaledGap.value)
const isLines = computed(() => props.variant === BackgroundVariant.Lines)
const bgColor = computed(() => (props.color ? props.color : defaultColors[props.variant || BackgroundVariant.Dots]))
const size = computed(() => props.size || 0.4 * transform.value[2])
</script>
<template>
<svg
:class="bgClasses"
:style="{
width: '100%',
height: '100%',
}"
>
<pattern :id="patternId" :x="xOffset" :y="yOffset" :width="scaledGap" :height="scaledGap" patternUnits="userSpaceOnUse">
<template v-if="isLines">
<path
:stroke="bgColor"
:stroke-width="props.size"
:d="`M${scaledGap / 2} 0 V${scaledGap} M0 ${scaledGap / 2} H${scaledGap}`"
/>
</template>
<template v-else>
<circle :cx="size" :cy="size" :r="size" :fill="bgColor" />
</template>
</pattern>
<rect x="0" y="0" width="100%" height="100%" :fill="`url(#${patternId})`" />
</svg>
</template>

View File

@@ -1,84 +0,0 @@
import { BackgroundVariant, RevueFlowStore } from '../../types'
import { createGridDotsPath, createGridLinesPath } from './utils'
import { computed, defineComponent, HTMLAttributes, inject, PropType } from 'vue'
export interface BackgroundProps extends HTMLAttributes {
variant?: BackgroundVariant
gap?: number
color?: string
size?: number
}
const defaultColors = {
[BackgroundVariant.Dots]: '#81818a',
[BackgroundVariant.Lines]: '#eee'
}
const Background = defineComponent({
name: 'Background',
props: {
variant: {
type: String as PropType<BackgroundProps['variant']>,
required: false,
default: BackgroundVariant.Dots
},
gap: {
type: Number as PropType<BackgroundProps['gap']>,
required: false,
default: 10
},
color: {
type: String as PropType<BackgroundProps['color']>,
required: false,
default: undefined
},
size: {
type: Number as PropType<BackgroundProps['size']>,
required: false,
default: 0.4
}
},
setup(props) {
const store = inject<RevueFlowStore>('store')!
const transform = computed(() => store.transform)
// when there are multiple flows on a page we need to make sure that every background gets its own pattern.
const patternId = `pattern-${Math.floor(Math.random() * 100000)}`
const bgClasses = ['revue-flow__background']
const scaledGap = computed(() => props.gap && props.gap * transform.value[2])
const xOffset = computed(() => scaledGap.value && transform.value[0] % scaledGap.value)
const yOffset = computed(() => scaledGap.value && transform.value[1] % scaledGap.value)
const isLines = computed(() => props.variant === BackgroundVariant.Lines)
const bgColor = computed(() => (props.color ? props.color : defaultColors[props.variant || BackgroundVariant.Dots]))
const path = computed(() =>
isLines.value
? scaledGap.value && props.size && createGridLinesPath(scaledGap.value, props.size, bgColor.value)
: createGridDotsPath(props.size || 0.4 * transform.value[2], bgColor.value)
)
return () => (
<svg
class={bgClasses}
style={{
width: '100%',
height: '100%'
}}
>
<pattern
id={patternId}
x={xOffset.value}
y={yOffset.value}
width={scaledGap.value}
height={scaledGap.value}
patternUnits="userSpaceOnUse"
>
{path.value}
</pattern>
<rect x="0" y="0" width="100%" height="100%" fill={`url(#${patternId})`} />
</svg>
)
}
})
export default Background

View File

@@ -0,0 +1,10 @@
<script lang="ts" setup>
export type ControlButtonProps = HTMLButtonElement
const props = defineProps<ControlButtonProps>()
</script>
<template>
<button :class="['revue-flow__controls-button']" v-bind="props">
<slot></slot>
</button>
</template>

View File

@@ -0,0 +1,76 @@
<script lang="ts" setup>
import { HTMLAttributes } from 'vue'
import PlusIcon from '../../../assets/icons/plus.svg'
import MinusIcon from '../../../assets/icons/minus.svg'
import Fitview from '../../../assets/icons/fitview.svg'
import Lock from '../../../assets/icons/lock.svg'
import Unlock from '../../../assets/icons/unlock.svg'
import ControlButton from './ControlButton.vue'
import { FitViewParams, RevueFlowStore, ZoomPanHelperFunctions } from '~/types'
import useZoomPanHelper from '~/hooks/useZoomPanHelper'
export interface ControlProps extends HTMLAttributes {
showZoom?: boolean
showFitView?: boolean
showInteractive?: boolean
fitViewParams?: FitViewParams
onZoomIn?: () => void
onZoomOut?: () => void
onFitView?: () => void
onInteractiveChange?: (interactiveStatus: boolean) => void
}
const props = withDefaults(defineProps<ControlProps>(), {
showZoom: true,
showFitView: true,
showInteractive: true,
})
const store = inject<RevueFlowStore>('store')!
// const { onReady } = useZoomPanHelper()
const isInteractive = store.nodesDraggable && store.nodesConnectable && store.elementsSelectable
const mapClasses = ['revue-flow__controls']
/*
* const onZoomInHandler = () => {
zoomHelper.value?.zoomIn?.()
props.onZoomIn?.()
}
const onZoomOutHandler = () => {
zoomHelper.value?.zoomOut?.()
props.onZoomOut?.()
}
const onFitViewHandler = () => {
zoomHelper.value?.fitView?.(props.fitViewParams)
props.onFitView?.()
}
const onInteractiveChangeHandler = () => {
store.setInteractive?.(!isInteractive)
props.onInteractiveChange?.(!isInteractive)
}
*/
</script>
<template>
<div :class="mapClasses">
<template v-if="props.showZoom">
<ControlButton on-click="onZoomInHandler" class="revue-flow__controls-zoomin">
<PlusIcon />
</ControlButton>
<ControlButton on-click="onZoomOutHandler" class="revue-flow__controls-zoomout">
<MinusIcon />
</ControlButton>
</template>
<ControlButton v-if="props.showFitView" class="revue-flow__controls-fitview" on-click="onFitViewHandler">
<Fitview />
</ControlButton>
<ControlButton v-if="props.showInteractive" class="revue-flow__controls-interactive" on-click="onInteractiveChangeHandler">
<Unlock v-if="isInteractive" />
<Lock v-else />
</ControlButton>
<slot></slot>
</div>
</template>

View File

@@ -1,154 +0,0 @@
import { defineComponent, HTMLAttributes, inject, onMounted, PropType, ref } from 'vue'
import useZoomPanHelper from '../../hooks/useZoomPanHelper'
import { FitViewParams, RevueFlowStore, ZoomPanHelperFunctions } from '../../types'
import PlusIcon from '../../../assets/icons/plus.svg'
import MinusIcon from '../../../assets/icons/minus.svg'
import Fitview from '../../../assets/icons/fitview.svg'
import Lock from '../../../assets/icons/lock.svg'
import Unlock from '../../../assets/icons/unlock.svg'
export interface ControlProps extends HTMLAttributes {
showZoom?: boolean
showFitView?: boolean
showInteractive?: boolean
fitViewParams?: FitViewParams
onZoomIn?: () => void
onZoomOut?: () => void
onFitView?: () => void
onInteractiveChange?: (interactiveStatus: boolean) => void
}
export type ControlButtonProps = HTMLButtonElement
export const ControlButton = defineComponent({
props: {
disabled: {
type: Boolean as PropType<ControlButtonProps['disabled']>,
required: false,
default: undefined
},
onClick: {
type: Function as PropType<() => any>,
required: false,
default: undefined
}
},
setup(props, { slots }) {
return () => (
<button class={['revue-flow__controls-button']} {...props}>
{slots.default ? slots.default() : ''}
</button>
)
}
})
const Controls = defineComponent({
name: 'Controls',
props: {
showZoom: {
type: Boolean as PropType<ControlProps['showZoom']>,
required: false,
default: true
},
showFitView: {
type: Boolean as PropType<ControlProps['showFitView']>,
required: false,
default: true
},
showInteractive: {
type: Boolean as PropType<ControlProps['showInteractive']>,
required: false,
default: true
},
fitViewParams: {
type: Object as PropType<ControlProps['fitViewParams']>,
required: false,
default: undefined
},
onZoomIn: {
type: Function() as PropType<ControlProps['onZoomIn']>,
required: false,
default: undefined
},
onZoomOut: {
type: Function() as PropType<ControlProps['onZoomOut']>,
required: false,
default: undefined
},
onFitView: {
type: Function() as PropType<ControlProps['onFitView']>,
required: false,
default: undefined
},
onInteractiveChange: {
type: Function() as PropType<ControlProps['onInteractiveChange']>,
required: false,
default: undefined
}
},
setup(props, { slots }) {
const store = inject<RevueFlowStore>('store')!
const isVisible = ref<boolean>(false)
const zoomHelper = ref<ZoomPanHelperFunctions>()
const { onReady } = useZoomPanHelper()
onReady((helper) => {
zoomHelper.value = helper
})
const isInteractive = store.nodesDraggable && store.nodesConnectable && store.elementsSelectable
const mapClasses = ['revue-flow__controls']
const onZoomInHandler = () => {
zoomHelper.value?.zoomIn?.()
props.onZoomIn?.()
}
const onZoomOutHandler = () => {
zoomHelper.value?.zoomOut?.()
props.onZoomOut?.()
}
const onFitViewHandler = () => {
zoomHelper.value?.fitView?.(props.fitViewParams)
props.onFitView?.()
}
const onInteractiveChangeHandler = () => {
store.setInteractive?.(!isInteractive)
props.onInteractiveChange?.(!isInteractive)
}
onMounted(() => {
isVisible.value = true
})
return () => (
<div class={mapClasses}>
{props.showZoom && (
<>
<ControlButton onClick={onZoomInHandler} class="revue-flow__controls-zoomin">
<PlusIcon />
</ControlButton>
<ControlButton onClick={onZoomOutHandler} class="revue-flow__controls-zoomout">
<MinusIcon />
</ControlButton>
</>
)}
{props.showFitView && (
<ControlButton class="revue-flow__controls-fitview" onClick={onFitViewHandler}>
<Fitview />
</ControlButton>
)}
{props.showInteractive && (
<ControlButton class="revue-flow__controls-interactive" onClick={onInteractiveChangeHandler}>
{isInteractive ? <Unlock /> : <Lock />}
</ControlButton>
)}
{slots.default ? slots.default() : ''}
</div>
)
}
})
export default Controls

View File

@@ -0,0 +1,93 @@
<script lang="ts" setup>
import { HTMLAttributes } from 'vue'
import MiniMapNodeDepr from './MiniMapNodeDepr'
import { getRectOfNodes, getBoundsofRects } from '~/utils/graph'
import { Node, Rect, RevueFlowStore } from '~/types'
type StringFunc = (node: Node) => string
export interface MiniMapProps extends HTMLAttributes {
nodeColor?: string | StringFunc
nodeStrokeColor?: string | StringFunc
nodeClassName?: string | StringFunc
nodeBorderRadius?: number
nodeStrokeWidth?: number
maskColor?: string
}
const props = withDefaults(defineProps<MiniMapProps>(), {
nodeStrokeColor: '#555',
nodeColor: '#fff',
nodeClassName: '',
nodeBorderRadius: 5,
nodeStrokeWidth: 2,
maskColor: 'rgb(240, 242, 243, 0.7)',
})
const attrs: any = useAttrs()
declare const window: any
const defaultWidth = 200
const defaultHeight = 150
const store = inject<RevueFlowStore>('store')!
const transform = computed(() => store.transform)
const elementWidth = computed(() => (attrs.style?.width ?? defaultWidth)! as number)
const elementHeight = computed(() => (attrs.style?.height ?? defaultHeight)! as number)
const nodeColorFunc = computed(
() => (props.nodeColor instanceof Function ? props.nodeColor : () => props.nodeColor) as StringFunc,
)
const nodeStrokeColorFunc = computed(
() => (props.nodeStrokeColor instanceof Function ? props.nodeStrokeColor : () => props.nodeStrokeColor) as StringFunc,
)
const nodeClassNameFunc = computed(
() => (props.nodeClassName instanceof Function ? props.nodeClassName : () => props.nodeClassName) as StringFunc,
)
const hasNodes = computed(() => store.nodes && store.nodes.length)
const bb = computed(() => getRectOfNodes(store.nodes))
const viewBB = computed<Rect>(() => ({
x: -transform.value[0] / transform.value[2],
y: -transform.value[1] / transform.value[2],
width: store.width / transform.value[2],
height: store.height / transform.value[2],
}))
const boundingRect = computed(() => (hasNodes.value ? getBoundsofRects(bb.value, viewBB.value) : viewBB.value))
const scaledWidth = computed(() => boundingRect.value.width / elementWidth.value)
const scaledHeight = computed(() => boundingRect.value.height / elementHeight.value)
const viewScale = computed(() => Math.max(scaledWidth.value, scaledHeight.value))
const viewWidth = computed(() => viewScale.value * elementWidth.value)
const viewHeight = computed(() => viewScale.value * elementHeight.value)
const offset = computed(() => 5 * viewScale.value)
const x = computed(() => boundingRect.value.x - (viewWidth.value - boundingRect.value.width) / 2 - offset.value)
const y = computed(() => boundingRect.value.y - (viewHeight.value - boundingRect.value.height) / 2 - offset.value)
const width = computed(() => viewWidth.value + offset.value * 2)
const height = computed(() => viewHeight.value + offset.value * 2)
const shapeRendering = typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision'
</script>
<template>
<svg :width="elementWidth" :height="elementHeight" :viewBox="`${x} ${y} ${width} ${height}`" class="revue-flow__minimap">
<template v-for="(node, i) of store.nodes" :key="`mini-map-node-${i}`">
<MiniMapNodeDepr
v-if="!node.isHidden"
:x="node.__rf.position.x"
:y="node.__rf.position.y"
:width="node.__rf.width || 0"
:height="node.__rf.height || 0"
:style="node.style"
:class="nodeClassNameFunc(node)"
:color="nodeColorFunc(node)"
:border-radius="props.nodeBorderRadius"
:stroke-color="nodeStrokeColorFunc(node)"
:stroke-width="props.nodeStrokeWidth"
:shape-rendering="shapeRendering"
/>
</template>
<path
class="revue-flow__minimap-mask"
:d="`M${x - offset},${y - offset}h${width + offset * 2}v${height + offset * 2}h${-width - offset * 2}z
M${viewBB.x},${viewBB.y}h${viewBB.width}v${viewBB.height}h${-viewBB.width}z`"
:fill="props.maskColor"
fill-rule="evenodd"
/>
</svg>
</template>

View File

@@ -1,86 +0,0 @@
import { computed, defineComponent, PropType } from 'vue'
interface MiniMapNodeProps {
x: number
y: number
width: number
height: number
borderRadius: number
color: string
shapeRendering: string
strokeColor: string
strokeWidth: number
}
const MiniMapNode = defineComponent({
name: 'MiniMapNode',
props: {
x: {
type: Number as PropType<MiniMapNodeProps['x']>,
required: false,
default: undefined
},
y: {
type: Number as PropType<MiniMapNodeProps['y']>,
required: false,
default: undefined
},
width: {
type: Number as PropType<MiniMapNodeProps['width']>,
required: false,
default: undefined
},
height: {
type: Number as PropType<MiniMapNodeProps['height']>,
required: false,
default: undefined
},
borderRadius: {
type: Number as PropType<MiniMapNodeProps['borderRadius']>,
required: false,
default: undefined
},
color: {
type: String as PropType<MiniMapNodeProps['color']>,
required: false,
default: undefined
},
shapeRendering: {
type: String as PropType<MiniMapNodeProps['shapeRendering']>,
required: false,
default: undefined
},
strokeColor: {
type: String as PropType<MiniMapNodeProps['strokeColor']>,
required: false,
default: undefined
},
strokeWidth: {
type: Number as PropType<MiniMapNodeProps['strokeWidth']>,
required: false,
default: undefined
}
},
setup(props, { attrs }: { attrs: Record<string, any> }) {
const styles = attrs.style || {}
const fill = computed(() => (props.color || styles.value.background || styles.value.backgroundColor) as string)
return () => (
<rect
class="revue-flow__minimap-node"
x={props.x}
y={props.y}
rx={props.borderRadius}
ry={props.borderRadius}
width={props.width}
height={props.height}
fill={fill.value}
stroke={props.strokeColor}
stroke-width={props.strokeWidth}
shape-rendering={props.shapeRendering}
/>
)
}
})
export default MiniMapNode

View File

@@ -0,0 +1,35 @@
<script lang="ts" setup>
import { computed, CSSProperties } from 'vue'
interface MiniMapNodeProps {
x?: number
y?: number
width?: number
height?: number
borderRadius?: number
color?: string
shapeRendering?: string
strokeColor?: string
strokeWidth?: number
}
const props = defineProps<MiniMapNodeProps>()
const attrs = useAttrs()
const styles = (attrs.style ?? {}) as CSSProperties
const fill = computed(() => (props.color || styles.background || styles.backgroundColor) as string)
</script>
<template>
<rect
class="revue-flow__minimap-node"
:x="props.x"
:y="props.y"
:rx="props.borderRadius"
:ry="props.borderRadius"
:width="props.width"
:height="props.height"
:fill="fill"
:stroke="props.strokeColor"
:stroke-width="props.strokeWidth"
:shape-rendering="props.shapeRendering"
/>
</template>

View File

@@ -1,130 +0,0 @@
import { getRectOfNodes, getBoundsofRects } from '../../utils/graph'
import { Node, Rect, RevueFlowStore } from '../../types'
import MiniMapNode from './MiniMapNode'
import { computed, defineComponent, HTMLAttributes, inject, PropType } from 'vue'
type StringFunc = (node: Node) => string
export interface MiniMapProps extends HTMLAttributes {
nodeColor?: string | StringFunc
nodeStrokeColor?: string | StringFunc
nodeClassName?: string | StringFunc
nodeBorderRadius?: number
nodeStrokeWidth?: number
maskColor?: string
}
declare const window: any
const defaultWidth = 200
const defaultHeight = 150
const MiniMap = defineComponent({
name: 'MiniMap',
props: {
nodeStrokeColor: {
type: [String, Function] as PropType<MiniMapProps['nodeStrokeColor']>,
required: false,
default: '#555'
},
nodeColor: {
type: [String, Function] as PropType<MiniMapProps['nodeColor']>,
required: false,
default: '#fff'
},
nodeClassName: {
type: [String, Function] as PropType<MiniMapProps['nodeClassName']>,
required: false,
default: ''
},
nodeBorderRadius: {
type: Number as PropType<MiniMapProps['nodeBorderRadius']>,
required: false,
default: 5
},
nodeStrokeWidth: {
type: Number as PropType<MiniMapProps['nodeStrokeWidth']>,
required: false,
default: 2
},
maskColor: {
type: String as PropType<MiniMapProps['maskColor']>,
required: false,
default: 'rgb(240, 242, 243, 0.7)'
}
},
setup(props, { attrs }: { attrs: Record<string, any> }) {
const store = inject<RevueFlowStore>('store')!
const transform = computed(() => store.transform)
const elementWidth = computed(() => (attrs.style?.width || defaultWidth)! as number)
const elementHeight = computed(() => (attrs.style?.height || defaultHeight)! as number)
const nodeColorFunc = computed(
() => (props.nodeColor instanceof Function ? props.nodeColor : () => props.nodeColor) as StringFunc
)
const nodeStrokeColorFunc = computed(
() => (props.nodeStrokeColor instanceof Function ? props.nodeStrokeColor : () => props.nodeStrokeColor) as StringFunc
)
const nodeClassNameFunc = computed(
() => (props.nodeClassName instanceof Function ? props.nodeClassName : () => props.nodeClassName) as StringFunc
)
const hasNodes = computed(() => store.nodes && store.nodes.length)
const bb = computed(() => getRectOfNodes(store.nodes))
const viewBB = computed<Rect>(() => ({
x: -transform.value[0] / transform.value[2],
y: -transform.value[1] / transform.value[2],
width: store.width / transform.value[2],
height: store.height / transform.value[2]
}))
const boundingRect = computed(() => (hasNodes.value ? getBoundsofRects(bb.value, viewBB.value) : viewBB.value))
const scaledWidth = computed(() => boundingRect.value.width / elementWidth.value)
const scaledHeight = computed(() => boundingRect.value.height / elementHeight.value)
const viewScale = computed(() => Math.max(scaledWidth.value, scaledHeight.value))
const viewWidth = computed(() => viewScale.value * elementWidth.value)
const viewHeight = computed(() => viewScale.value * elementHeight.value)
const offset = computed(() => 5 * viewScale.value)
const x = computed(() => boundingRect.value.x - (viewWidth.value - boundingRect.value.width) / 2 - offset.value)
const y = computed(() => boundingRect.value.y - (viewHeight.value - boundingRect.value.height) / 2 - offset.value)
const width = computed(() => viewWidth.value + offset.value * 2)
const height = computed(() => viewHeight.value + offset.value * 2)
const shapeRendering = typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision'
return () => (
<svg
width={elementWidth.value}
height={elementHeight.value}
viewBox={`${x.value} ${y.value} ${width.value} ${height.value}`}
class="revue-flow__minimap"
>
{store.nodes
.filter((node) => !node.isHidden)
.map((node) => (
<MiniMapNode
key={node.id}
x={node.__rf.position.x}
y={node.__rf.position.y}
width={node.__rf.width || 0}
height={node.__rf.height || 0}
style={node.style}
class={nodeClassNameFunc.value(node)}
color={nodeColorFunc.value(node)}
borderRadius={props.nodeBorderRadius}
strokeColor={nodeStrokeColorFunc.value(node)}
strokeWidth={props.nodeStrokeWidth}
shapeRendering={shapeRendering}
/>
))}
<path
class="revue-flow__minimap-mask"
d={`M${x.value - offset.value},${y.value - offset.value}h${width.value + offset.value * 2}v${
height.value + offset.value * 2
}h${-width.value - offset.value * 2}z
M${viewBB.value.x},${viewBB.value.y}h${viewBB.value.width}v${viewBB.value.height}h${-viewBB.value.width}z`}
fill={props.maskColor}
fill-rule="evenodd"
/>
</svg>
)
}
})
export default MiniMap

View File

@@ -1,6 +1,6 @@
// These components are not used by revue Flow directly
// but the user can add them as children of a revue Flow component
export { default as MiniMap } from './MiniMap'
export { default as Controls, ControlButton } from './Controls'
export { default as Background } from './Background'
export { default as MiniMap } from './MiniMap/MiniMap.vue'
export { default as Controls } from './Controls/Controls.vue'
export { default as Background } from './Background/Background.vue'

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup>
import { CSSProperties } from 'vue'
import { ConnectionLineType, CustomConnectionLine, HandleElement, Position, RevueFlowStore, Node } from '~/types'
import { ConnectionLineType, CustomConnectionLine, HandleElement, Node, Position, RevueFlowStore } from '~/types'
import { RevueFlowHooks } from '~/hooks/RevueFlowHooks'
import { getBezierPath, getSmoothStepPath } from '~/components/Edges/utils'
@@ -16,76 +16,67 @@ const props = withDefaults(defineProps<ConnectionLineProps>(), {
connectionLineStyle: () => ({}),
})
const store = inject<RevueFlowStore>('store')!
const hooks = inject<RevueFlowHooks>('hooks')!
const store = inject<RevueFlowStore>('store')!
const sourceHandle = computed(() =>
const sourceHandle =
store.connectionHandleId && store.connectionHandleType
? props.sourceNode.__rf.handleBounds[store.connectionHandleType].find((d: HandleElement) => d.id === store.connectionHandleId)
: store.connectionHandleType && props.sourceNode.__rf.handleBounds[store.connectionHandleType][0],
)
const sourceHandleX = computed(() =>
sourceHandle.value ? sourceHandle.value.x + sourceHandle.value.width / 2 : (props.sourceNode.__rf.width as number) / 2,
)
const sourceHandleY = computed(() =>
sourceHandle.value ? sourceHandle.value.y + sourceHandle.value.height / 2 : props.sourceNode.__rf.height,
)
const sourceX = computed(() => props.sourceNode.__rf.position.x + sourceHandleX.value)
const sourceY = computed(() => props.sourceNode.__rf.position.y + sourceHandleY.value)
: store.connectionHandleType && props.sourceNode.__rf.handleBounds[store.connectionHandleType][0]
const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : (props.sourceNode.__rf.width as number) / 2
const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : props.sourceNode.__rf.height
const sourceX = props.sourceNode.__rf.position.x + sourceHandleX
const sourceY = props.sourceNode.__rf.position.y + sourceHandleY
const targetX = computed(() => (store.connectionPosition?.x - store.transform[0]) / store.transform[2])
const targetY = computed(() => (store.connectionPosition?.y - store.transform[1]) / store.transform[2])
const isRightOrLeft = sourceHandle.position === Position.Left || sourceHandle.position === Position.Right
const targetPosition = isRightOrLeft ? Position.Left : Position.Top
const isRightOrLeft = computed(
() => sourceHandle.value?.position === Position.Left || sourceHandle.value?.position === Position.Right,
)
const targetPosition = computed(() => (isRightOrLeft.value ? Position.Left : Position.Top))
const targetX = computed(() => (store.connectionPosition.x - store.transform[0]) / store.transform[2])
const targetY = computed(() => (store.connectionPosition.y - store.transform[1]) / store.transform[2])
let dAttr = computed(() => `M${sourceX.value},${sourceY.value} ${targetX.value},${targetY.value}`)
if (props.connectionLineType === ConnectionLineType.Bezier) {
dAttr = computed(() =>
getBezierPath({
sourceX: sourceX.value,
sourceY: sourceY.value,
sourcePosition: sourceHandle.value?.position,
targetX: targetX.value,
targetY: targetY.value,
targetPosition: targetPosition.value,
}),
)
} else if (props.connectionLineType === ConnectionLineType.Step) {
dAttr = computed(() =>
getSmoothStepPath({
sourceX: sourceX.value,
sourceY: sourceY.value,
sourcePosition: sourceHandle.value?.position,
targetX: targetX.value,
targetY: targetY.value,
targetPosition: targetPosition.value,
borderRadius: 0,
}),
)
} else if (props.connectionLineType === ConnectionLineType.SmoothStep) {
dAttr = computed(() =>
getSmoothStepPath({
sourceX: sourceX.value,
sourceY: sourceY.value,
sourcePosition: sourceHandle.value?.position,
targetX: targetX.value,
targetY: targetY.value,
targetPosition: targetPosition.value,
}),
)
}
watch(dAttr, () => console.log(dAttr.value))
const dAttr = computed(() => {
let path = `M${sourceX.value},${sourceY.value} ${targetX.value},${targetY.value}`
switch (props.connectionLineType) {
case ConnectionLineType.Bezier:
path = getBezierPath({
sourceX,
sourceY,
sourcePosition: sourceHandle.position,
targetX: targetX.value,
targetY: targetY.value,
targetPosition,
})
break
case ConnectionLineType.Step:
path = getSmoothStepPath({
sourceX,
sourceY,
sourcePosition: sourceHandle.position,
targetX: targetX.value,
targetY: targetY.value,
targetPosition,
borderRadius: 0,
})
break
case ConnectionLineType.SmoothStep:
path = getSmoothStepPath({
sourceX,
sourceY,
sourcePosition: sourceHandle.position,
targetX: targetX.value,
targetY: targetY.value,
targetPosition,
})
break
}
return path
})
</script>
<template>
<g>
<g class="revue-flow__connection">
<component
:is="props.customConnectionLine"
v-if="props.customConnectionLine"
class="revue-flow__connection"
v-bind="{
sourceX: sourceX,
sourceY: sourceY,

View File

@@ -1,125 +0,0 @@
import { ref, defineComponent, CSSProperties, PropType, computed, inject, h } from 'vue'
import { getBezierPath } from '../Edges/BezierEdgeDepr'
import { getSmoothStepPath } from '../Edges/SmoothStepEdgeDepr'
import { Node, HandleElement, Position, ConnectionLineType, ConnectionLineComponent, RevueFlowStore } from '../../types'
interface ConnectionLineProps {
connectionLineType: ConnectionLineType
connectionLineStyle?: CSSProperties
customConnectionLine?: ConnectionLineComponent
}
export default defineComponent({
props: {
connectionLineStyle: {
type: Object as PropType<ConnectionLineProps['connectionLineStyle']>,
required: false,
default: () => {},
},
connectionLineType: {
type: String as PropType<ConnectionLineProps['connectionLineType']>,
required: false,
default: ConnectionLineType.Bezier,
},
customConnectionLine: {
type: Object as PropType<ConnectionLineProps['customConnectionLine']>,
required: false,
default: undefined,
},
},
setup(props) {
const store = inject<RevueFlowStore>('store')!
const sourceNode = ref<Node | null>(store.nodes.find((n) => n.id === store.connectionNodeId) || null)
const nodesConnectable = computed(() => store.nodesConnectable)
const sourceHandle = computed(() =>
store.connectionHandleId && store.connectionHandleType
? sourceNode.value?.__rf.handleBounds[store.connectionHandleType].find(
(d: HandleElement) => d.id === store.connectionHandleId,
)
: store.connectionHandleType && sourceNode.value?.__rf.handleBounds[store.connectionHandleType][0],
)
const sourceHandleX = computed(() =>
sourceHandle.value ? sourceHandle.value.x + sourceHandle.value.width / 2 : (sourceNode.value?.__rf.width as number) / 2,
)
const sourceHandleY = computed(() =>
sourceHandle.value ? sourceHandle.value.y + sourceHandle.value.height / 2 : sourceNode.value?.__rf.height,
)
const sourceX = computed(() => sourceNode.value?.__rf.position.x + sourceHandleX.value)
const sourceY = computed(() => sourceNode.value?.__rf.position.y + sourceHandleY.value)
const targetX = computed(() => (store.connectionPosition.x - store.transform[0]) / store.transform[2])
const targetY = computed(() => (store.connectionPosition.y - store.transform[1]) / store.transform[2])
const isRightOrLeft = computed(
() => sourceHandle.value?.position === Position.Left || sourceHandle.value?.position === Position.Right,
)
const targetPosition = computed(() => (isRightOrLeft.value ? Position.Left : Position.Top))
let dAttr = computed(() => `M${sourceX.value},${sourceY.value} ${targetX.value},${targetY.value}`)
if (props.connectionLineType === ConnectionLineType.Bezier) {
dAttr = computed(() =>
getBezierPath({
sourceX: sourceX.value,
sourceY: sourceY.value,
sourcePosition: sourceHandle.value?.position,
targetX: targetX.value,
targetY: targetY.value,
targetPosition: targetPosition.value,
}),
)
} else if (props.connectionLineType === ConnectionLineType.Step) {
dAttr = computed(() =>
getSmoothStepPath({
sourceX: sourceX.value,
sourceY: sourceY.value,
sourcePosition: sourceHandle.value?.position,
targetX: targetX.value,
targetY: targetY.value,
targetPosition: targetPosition.value,
borderRadius: 0,
}),
)
} else if (props.connectionLineType === ConnectionLineType.SmoothStep) {
dAttr = computed(() =>
getSmoothStepPath({
sourceX: sourceX.value,
sourceY: sourceY.value,
sourcePosition: sourceHandle.value?.position,
targetX: targetX.value,
targetY: targetY.value,
targetPosition: targetPosition.value,
}),
)
}
if (props.customConnectionLine) {
return () => (
<g className="revue-flow__connection">
{props.customConnectionLine &&
h(props.customConnectionLine, {
sourceX: sourceX.value,
sourceY: sourceY.value,
sourcePosition: sourceHandle.value?.position,
targetX: targetX.value,
targetY: targetY.value,
targetPosition: targetPosition.value,
connectionLineType: props.connectionLineType,
connectionLineStyle: props.connectionLineStyle,
})}
</g>
)
}
return () =>
nodesConnectable.value && sourceNode.value ? (
<g className="revue-flow__connection">
<path d={dAttr.value} className="revue-flow__connection-path" style={props.connectionLineStyle} />
</g>
) : (
''
)
},
})

View File

@@ -4,7 +4,7 @@ import { RevueFlowHooks } from '~/hooks/RevueFlowHooks'
import { getEdgePositions, getHandle, getSourceTargetNodes, isEdgeVisible } from '~/container/EdgeRenderer/utils'
import { isEdge } from '~/utils/graph'
import { ConnectionMode, Dimensions, Edge, EdgeType, Elements, Position, RevueFlowStore, Transform } from '~/types'
import { onMouseDown } from '~/components/Handle/handler'
import { onMouseDown } from '~/components/Handle/utils'
interface EdgeProps {
type: EdgeType
@@ -12,7 +12,7 @@ interface EdgeProps {
nodes: ReturnType<typeof getSourceTargetNodes>
dimensions: Dimensions
transform: Transform
selectedElements?: Elements | null
selectedElements?: Elements
elementsSelectable?: boolean
onlyRenderVisibleElements?: boolean
connectionMode?: ConnectionMode
@@ -23,7 +23,6 @@ interface EdgeProps {
const props = withDefaults(defineProps<EdgeProps>(), {
elementsSelectable: true,
onlyRenderVisibleElements: false,
selectedElements: () => null,
})
const store = inject<RevueFlowStore>('store')!

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup>
import { ConnectionMode, ElementId, Position, RevueFlowStore } from '~/types'
import { onMouseDown, ValidConnectionFunc } from '~/components/Handle/handler'
import { ElementId, Position, RevueFlowStore } from '~/types'
import { onMouseDown, ValidConnectionFunc } from '~/components/Handle/utils'
import { RevueFlowHooks } from '~/hooks/RevueFlowHooks'
interface HandleProps {
@@ -15,7 +15,6 @@ const props = withDefaults(defineProps<HandleProps>(), {
id: '',
type: 'source',
position: Position.Top,
isValidConnection: () => true,
connectable: true,
})
@@ -24,7 +23,18 @@ const hooks = inject<RevueFlowHooks>('hooks')!
const nodeId = inject<ElementId>('NodeIdContext')!
const onMouseDownHandler = (event: MouseEvent) =>
onMouseDown(event, store, hooks, props.id, nodeId, props.type === 'target', props.isValidConnection)
onMouseDown(
event,
store,
hooks,
props.id,
nodeId,
props.type === 'target',
props.isValidConnection ??
function () {
return true
},
)
</script>
<template>
<div

View File

@@ -1,65 +0,0 @@
import { ElementId, Position, RevueFlowStore } from '../../types'
import { onMouseDown, ValidConnectionFunc } from './handler'
import { defineComponent, inject, PropType } from 'vue'
import { RevueFlowHooks } from '../../hooks/RevueFlowHooks'
const alwaysValid = () => true
export default defineComponent({
props: {
type: {
type: String,
required: false,
default: 'source'
},
position: {
type: String as PropType<Position>,
required: false,
default: Position.Top
},
isValidConnection: {
type: Function as PropType<ValidConnectionFunc>,
required: false,
default: alwaysValid
},
isConnectable: {
type: Boolean,
required: false,
default: true
},
id: {
type: String,
required: false,
default: undefined
}
},
setup(props, { slots }) {
const store = inject<RevueFlowStore>('store')!
const hooks = inject<RevueFlowHooks>('hooks')!
const nodeId = inject<ElementId>('NodeIdContext')!
const onMouseDownHandler = (event: MouseEvent) =>
onMouseDown(event, store, hooks, props.id as string, nodeId, props.type === 'target', props.isValidConnection)
return () => (
<div
data-handleid={props.id}
data-nodeid={nodeId}
data-handlepos={props.position}
class={[
'revue-flow__handle',
`revue-flow__handle-${props.position}`,
'nodrag',
{
source: props.type !== 'target',
target: props.type === 'target',
connectable: props.isConnectable
}
]}
onMousedown={onMouseDownHandler}
>
{slots.default ? slots.default() : ''}
</div>
)
}
})

View File

@@ -1,5 +1,5 @@
import { getHostForElement } from '~/utils'
import { ElementId, ConnectionMode, Connection, HandleType, XYPosition, RevueFlowStore } from '~/types'
import { ElementId, ConnectionMode, Connection, HandleType, RevueFlowStore } from '~/types'
import { RevueFlowHooks } from '~/hooks/RevueFlowHooks'
export type ValidConnectionFunc = (connection: Connection) => boolean
@@ -82,7 +82,9 @@ export function onMouseDown(
handleId: ElementId,
nodeId: ElementId,
isTarget: boolean,
isValidConnection: ValidConnectionFunc = () => true,
isValidConnection: ValidConnectionFunc = () => {
return true
},
elementEdgeUpdaterType?: HandleType,
): void {
const revueFlowNode = (event.target as Element).closest('.revue-flow')
@@ -105,12 +107,6 @@ export function onMouseDown(
const containerBounds = revueFlowNode.getBoundingClientRect()
let recentHoveredHandle: Element
const connectionPosition = ref<XYPosition>({
x: event.clientX - containerBounds.left,
y: event.clientY - containerBounds.top,
})
if (!store.connectionPosition) store.connectionPosition = { x: 0, y: 0 }
store.connectionPosition.x = event.clientX - containerBounds.left
store.connectionPosition.y = event.clientY - containerBounds.top
@@ -122,8 +118,8 @@ export function onMouseDown(
hooks.connectStart.trigger({ event, params: { nodeId, handleId, handleType } })
function onMouseMove(event: MouseEvent) {
connectionPosition.value.x = event.clientX - containerBounds.left
connectionPosition.value.y = event.clientY - containerBounds.top
store.connectionPosition.x = event.clientX - containerBounds.left
store.connectionPosition.y = event.clientY - containerBounds.top
const { connection, elementBelow, isValid, isHoveringHandle } = checkElementBelowIsValid(
event,
@@ -173,6 +169,7 @@ export function onMouseDown(
resetRecentHandle(recentHoveredHandle)
store.setConnectionNodeId({ connectionNodeId: undefined, connectionHandleId: undefined, connectionHandleType: undefined })
store.connectionPosition = { x: NaN, y: NaN }
doc.removeEventListener('mousemove', onMouseMove as EventListenerOrEventListenerObject)
doc.removeEventListener('mouseup', onMouseUp as EventListenerOrEventListenerObject)

View File

@@ -1,40 +0,0 @@
import { defineComponent, PropType } from 'vue'
import Handle from '../../components/Handle'
import { NodeProps, NodeType, Position } from '../../types'
export default defineComponent({
name: 'DefaultNode',
components: { Handle },
inheritAttrs: false,
props: {
data: {
type: Object as PropType<NodeProps['data']>,
required: false,
default: undefined as any
},
isConnectable: {
type: Boolean as PropType<NodeProps['isConnectable']>,
required: false,
default: undefined
},
targetPosition: {
type: String as PropType<NodeProps['targetPosition']>,
required: false,
default: Position.Top
},
sourcePosition: {
type: String as PropType<NodeProps['sourcePosition']>,
required: false,
default: Position.Bottom
}
},
setup(props) {
return () => (
<>
<Handle type="target" position={props.targetPosition} isConnectable={props.isConnectable} />
{props.data?.label}
<Handle type="source" position={props.sourcePosition} isConnectable={props.isConnectable} />
</>
)
}
}) as NodeType

View File

@@ -1,34 +0,0 @@
import Handle from '../../components/Handle'
import { NodeProps, NodeType, Position } from '../../types'
import { defineComponent, PropType } from 'vue'
export default defineComponent({
name: 'InputNode',
components: { Handle },
inheritAttrs: false,
props: {
data: {
type: Object as PropType<NodeProps['data']>,
required: false,
default: undefined as any
},
isConnectable: {
type: Boolean as PropType<NodeProps['isConnectable']>,
required: false,
default: false
},
sourcePosition: {
type: String as PropType<NodeProps['sourcePosition']>,
required: false,
default: Position.Bottom
}
},
setup(props) {
return () => (
<>
{props.data?.label}
<Handle type="source" position={props.sourcePosition} isConnectable={props.isConnectable} />
</>
)
}
}) as NodeType

View File

@@ -1,236 +0,0 @@
import { computed, defineComponent, h, inject, onMounted, PropType, provide } from 'vue'
import { templateRef, useResizeObserver } from '@vueuse/core'
import { DraggableCore, DraggableEventListener } from '@braks/revue-draggable'
import { Node, NodeDimensionUpdate, NodeType, RevueFlowStore } from '../../types'
import { RevueFlowHooks } from '../../hooks/RevueFlowHooks'
interface NodeProps {
nodeTypes: Record<string, NodeType>
snapToGrid: boolean
snapGrid: [number, number]
selectNodesOnDrag: boolean
}
export default defineComponent({
components: { DraggableCore },
props: {
node: {
type: Object as PropType<Node>,
required: true
},
type: {
type: Object,
required: true
},
snapToGrid: {
type: Boolean as PropType<NodeProps['snapToGrid']>,
required: false,
default: undefined
},
snapGrid: {
type: Array as unknown as PropType<NodeProps['snapGrid']>,
required: false,
default: undefined
},
selectNodesOnDrag: {
type: Boolean as PropType<NodeProps['selectNodesOnDrag']>,
required: false,
default: true
}
},
setup(props) {
const store = inject<RevueFlowStore>('store')!
const hooks = inject<RevueFlowHooks>('hooks')!
provide('NodeIdContext', props.node.id)
const nodeElement = templateRef<HTMLDivElement>('nodeElement', null)
const selected = computed(() => store.selectedElements?.some(({ id }) => id === props.node.id) || false)
const isDraggable = computed(
() => props.node.draggable || (store.nodesDraggable && typeof props.node.draggable === 'undefined')
)
const isSelectable = computed(
() => props.node.selectable || (store.elementsSelectable && typeof props.node.selectable === 'undefined')
)
const isConnectable = computed(
() => props.node.connectable || (store.nodesConnectable && typeof props.node.connectable === 'undefined')
)
const node = () =>
({
id: props.node.id,
type: props.node.type,
position: { x: props.node.__rf.position.x, y: props.node.__rf.position.y },
data: props.node.data
} as Node)
const onMouseEnterHandler = () => {
if (props.node.__rf.isDragging) {
return
}
return (event: MouseEvent) => hooks.nodeMouseEnter.trigger({ event, node: node() })
}
const onMouseMoveHandler = () => {
if (props.node.__rf.isDragging) {
return
}
return (event: MouseEvent) => hooks.nodeMouseMove.trigger({ event, node: node() })
}
const onMouseLeaveHandler = () => {
if (props.node.__rf.isDragging) {
return
}
return (event: MouseEvent) => hooks.nodeMouseLeave.trigger({ event, node: node() })
}
const onContextMenuHandler = () => {
return (event: MouseEvent) => hooks.nodeContextMenu.trigger({ event, node: node() })
}
const onSelectNodeHandler = (event: MouseEvent) => {
if (!isDraggable.value) {
const n = node()
if (isSelectable.value) {
store.unsetNodesSelection()
if (!selected.value) {
store.addSelectedElements([n])
}
}
hooks.nodeClick.trigger({ event, node: n })
}
}
const onDragStart: DraggableEventListener = ({ event }) => {
const n = node()
hooks.nodeDragStart.trigger({ event, node: n })
if (props.selectNodesOnDrag && isSelectable.value) {
store.unsetNodesSelection()
if (!selected.value) {
store.addSelectedElements([n])
}
} else if (!props.selectNodesOnDrag && !selected.value && isSelectable.value) {
store.unsetNodesSelection()
store.addSelectedElements([])
}
}
const onDrag: DraggableEventListener = ({ event, data }) => {
const n = node()
n.position.x += data.deltaX
n.position.y += data.deltaY
hooks.nodeDrag.trigger({ event, node: n })
store?.updateNodePosDiff({
id: props.node.id as string,
diff: {
x: data.deltaX,
y: data.deltaY
},
isDragging: true
})
}
const onDragStop: DraggableEventListener = ({ event }) => {
const n = node()
// onDragStop also gets called when user just clicks on a node.
// Because of that we set dragging to true inside the onDrag handler and handle the click here
if (!props.node.__rf.isDragging) {
if (isSelectable.value && !props.selectNodesOnDrag && !selected.value) {
store.addSelectedElements([n])
}
hooks.nodeClick.trigger({ event, node: n })
return
}
store.updateNodePosDiff({
id: n.id || '',
isDragging: false
})
hooks.nodeDragStop.trigger({ event, node: n })
}
useResizeObserver(nodeElement, (entries) => {
const updates: NodeDimensionUpdate[] = entries.map((entry) => ({
id: entry.target.getAttribute('data-id') || '',
nodeElement: entry.target as HTMLDivElement
}))
store.updateNodeDimensions(updates)
})
onMounted(() => {
store.updateNodeDimensions([
{
id: props.node.id || '',
nodeElement: nodeElement.value,
forceUpdate: true
}
])
})
return () =>
props.node.isHidden ? (
''
) : (
<DraggableCore
cancel=".nodrag"
scale={store.transform[2]}
disabled={!isDraggable.value}
grid={props.snapGrid}
enableUserSelectHack={false}
onStart={onDragStart}
onMove={onDrag}
onStop={onDragStop}
>
<div
ref="nodeElement"
class={[
'revue-flow__node',
`revue-flow__node-${props.node.type}`,
{
selected: selected.value,
selectable: isSelectable.value
}
]}
style={{
zIndex: selected.value ? 10 : 3,
transform: `translate(${props.node.__rf.position.x}px,${props.node.__rf.position.y}px)`,
pointerEvents: isSelectable.value || isDraggable.value ? 'all' : 'none',
opacity: props.node.__rf.width !== null && props.node.__rf.height !== null ? 1 : 0,
...props.node.style
}}
data-id={props.node.id}
onMouseenter={onMouseEnterHandler}
onMousemove={onMouseMoveHandler}
onMouseleave={onMouseLeaveHandler}
onContextmenu={onContextMenuHandler}
onClick={onSelectNodeHandler}
>
{h(props.type, {
data: props.node.data,
type: props.node.type,
xPos: props.node.__rf.position.x,
yPos: props.node.__rf.position.y,
selected: selected.value,
isConnectable: isConnectable.value,
sourcePosition: props.node.sourcePosition,
targetPosition: props.node.targetPosition,
isDragging: props.node.__rf.isDragging
})}
</div>
</DraggableCore>
)
}
})

View File

@@ -1,34 +0,0 @@
import Handle from '../../components/Handle'
import { NodeProps, NodeType, Position } from '../../types'
import { defineComponent, PropType } from 'vue'
export default defineComponent({
name: 'OutputNode',
components: { Handle },
inheritAttrs: false,
props: {
data: {
type: Object as PropType<NodeProps['data']>,
required: false,
default: undefined as any
},
isConnectable: {
type: Boolean as PropType<NodeProps['isConnectable']>,
required: false,
default: false
},
targetPosition: {
type: String as PropType<NodeProps['targetPosition']>,
required: false,
default: Position.Top
}
},
setup(props) {
return () => (
<>
{props.data?.label}
<Handle type="source" position={props.targetPosition} isConnectable={props.isConnectable} />
</>
)
}
}) as NodeType

View File

@@ -1,103 +0,0 @@
// @ts-nocheck
/**
* The nodes selection rectangle gets displayed when a user
* made a selectio with on or several nodes
*/
import { computed, defineComponent, inject } from 'vue'
import { Draggable, DraggableEventListener } from '@braks/revue-draggable'
import { isNode } from '../../utils/graph'
import { Node, RevueFlowStore } from '../../types'
import { RevueFlowHooks } from '../../hooks/RevueFlowHooks'
export interface NodesSelectionProps {
onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void
onSelectionDrag?: (event: MouseEvent, nodes: Node[]) => void
onSelectionDragStop?: (event: MouseEvent, nodes: Node[]) => void
onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void
}
const NodesSelection = defineComponent({
setup() {
const store = inject<RevueFlowStore>('store')!
const hooks = inject<RevueFlowHooks>('hooks')!
const grid = computed(() => (store.snapToGrid ? store.snapGrid : [1, 1])! as [number, number])
const transform = computed(() => store.transform)
const selectedNodes = computed(() =>
store.selectedElements
? store.selectedElements.filter(isNode).map((selectedNode) => {
const matchingNode = store.nodes.find((node) => node.id === selectedNode.id)
return {
...matchingNode,
position: matchingNode?.__rf.position
} as Node
})
: []
)
const style = computed(() => ({
transform: `translate(${store.transform[0]}px,${store.transform[1]}px) scale(${store.transform[2]})`
}))
const innerStyle = computed(() => ({
width: `${store.selectedNodesBbox.width}px`,
height: `${store.selectedNodesBbox.height}px`,
top: `${store.selectedNodesBbox.y}px`,
left: `${store.selectedNodesBbox.x}px`
}))
const onStart: DraggableEventListener = ({ event }) => {
hooks.selectionDragStart.trigger({ event, nodes: selectedNodes.value })
}
const onDrag: DraggableEventListener = ({ event, data }) => {
hooks.selectionDrag.trigger({ event, nodes: selectedNodes.value })
store.updateNodePosDiff({
diff: {
x: data.deltaX,
y: data.deltaY
},
isDragging: true
})
}
const onStop: DraggableEventListener = ({ event }) => {
store.updateNodePosDiff({
isDragging: false
})
hooks.selectionDragStop.trigger({ event, nodes: selectedNodes.value })
}
const onContextMenu = (event: MouseEvent) => {
const selectedNodes: Node[] = store.selectedElements
? store.selectedElements.filter(isNode).map((selectedNode) => store.nodes.find((node) => node.id === selectedNode.id))
: []
hooks.selectionContextMenu.trigger({ event, nodes: selectedNodes })
}
return () => {
return !store.selectedElements || store.selectionActive ? (
''
) : (
<div class="revue-flow__nodesselection" style={style.value}>
<Draggable
onStart={onStart}
onMove={onDrag}
onStop={onStop}
scale={transform.value[2]}
grid={grid.value}
enableUserSelectHack={false}
>
<div class="revue-flow__nodesselection-rect" onContextmenu={onContextMenu} style={innerStyle.value} />
</Draggable>
</div>
)
}
}
})
export default NodesSelection

View File

@@ -1,96 +0,0 @@
/**
* The user selection rectangle gets displayed when a user drags the mouse while pressing shift
*/
import { RevueFlowStore, XYPosition } from '../../types'
import { computed, defineComponent, inject } from 'vue'
import { templateRef, useEventListener } from '@vueuse/core'
function getMousePosition(event: MouseEvent): XYPosition | void {
const revueFlowNode = (event.target as Element).closest('.revue-flow')
if (!revueFlowNode) {
return
}
const containerBounds = revueFlowNode.getBoundingClientRect()
return {
x: event.clientX - containerBounds.left,
y: event.clientY - containerBounds.top
}
}
const SelectionRect = (props: { width: number; height: number; x: number; y: number }) => {
return (
<div
class="revue-flow__selection"
style={{
width: `${props.width}px`,
height: `${props.height}px`,
transform: `translate(${props.x}px, ${props.y}px)`
}}
/>
)
}
export default defineComponent({
components: { SelectionRect },
setup() {
const store = inject<RevueFlowStore>('store')!
const el = templateRef('user-selection', null)
const shouldRender = computed(() => store.selectionActive || store.elementsSelectable)
const onMouseDown = (event: MouseEvent) => {
const mousePos = getMousePosition(event)
if (!mousePos) {
return
}
store.setUserSelection(mousePos)
}
const onMouseMove = (event: MouseEvent) => {
if (!store.selectionActive) {
return
}
const mousePos = getMousePosition(event)
if (!mousePos) {
return
}
store.updateUserSelection(mousePos)
}
const onMouseUp = () => {
store.unsetUserSelection()
}
const onMouseLeave = () => {
store.unsetUserSelection()
store.unsetNodesSelection()
}
useEventListener(el, 'mousedown', onMouseDown)
useEventListener(el, 'mousemove', onMouseMove)
useEventListener(el, 'click', onMouseUp)
useEventListener(el, 'mouseup', onMouseUp)
useEventListener(el, 'mouseleave', onMouseLeave)
return () =>
shouldRender.value ? (
<div class="revue-flow__selectionpane" ref="user-selection">
{store?.userSelectionRect.draw ? (
<SelectionRect
width={store.userSelectionRect.width}
height={store.userSelectionRect.height}
x={store.userSelectionRect.x}
y={store.userSelectionRect.y}
/>
) : (
''
)}
</div>
) : (
''
)
}
})

7
src/container/index.ts Normal file
View File

@@ -0,0 +1,7 @@
export { default as Flow } from './Flow.vue'
export { default as EdgeRenderer } from './EdgeRenderer.vue'
export { default as NodeRenderer } from './NodeRenderer.vue'
export { default as Marker } from './Marker.vue'
export { default as MarkerDefinitions } from './MarkerDefinitions.vue'
export { default as ZoomPane } from './ZoomPane.vue'
export { default as SelectionPane } from './SelectionPane.vue'

View File

@@ -17,7 +17,7 @@ export default function configureStore(
setActivePinia(pinia)
return defineStore({
id: `revue-flow-${Math.random()}`,
id: `revue-flow-${Math.floor(Math.random() * 100)}`,
state: () => ({
...preloadedState,
}),

View File

@@ -41,7 +41,7 @@ export const initialState: RevueFlowState = {
connectionNodeId: undefined,
connectionHandleId: undefined,
connectionHandleType: 'source',
connectionPosition: { x: 0, y: 0 },
connectionPosition: { x: NaN, y: NaN },
connectionMode: ConnectionMode.Strict,
snapGrid: [15, 15],

View File

@@ -31,7 +31,7 @@ export interface RevueFlowState {
connectionNodeId?: ElementId
connectionHandleId?: ElementId
connectionHandleType?: HandleType
connectionPosition?: XYPosition
connectionPosition: XYPosition
connectionMode: ConnectionMode
snapToGrid: boolean