update(script-setup): Refactor additional-components
* Remove jsx files fix: ConnectionLine.vue not recalculating properly
This commit is contained in:
@@ -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>
|
||||||
@@ -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
|
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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
|
|
||||||
@@ -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>
|
||||||
@@ -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
|
|
||||||
@@ -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>
|
||||||
@@ -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
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// These components are not used by revue Flow directly
|
// These components are not used by revue Flow directly
|
||||||
// but the user can add them as children of a revue Flow component
|
// but the user can add them as children of a revue Flow component
|
||||||
|
|
||||||
export { default as MiniMap } from './MiniMap'
|
export { default as MiniMap } from './MiniMap/MiniMap.vue'
|
||||||
export { default as Controls, ControlButton } from './Controls'
|
export { default as Controls } from './Controls/Controls.vue'
|
||||||
export { default as Background } from './Background'
|
export { default as Background } from './Background/Background.vue'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { CSSProperties } from 'vue'
|
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 { RevueFlowHooks } from '~/hooks/RevueFlowHooks'
|
||||||
import { getBezierPath, getSmoothStepPath } from '~/components/Edges/utils'
|
import { getBezierPath, getSmoothStepPath } from '~/components/Edges/utils'
|
||||||
|
|
||||||
@@ -16,76 +16,67 @@ const props = withDefaults(defineProps<ConnectionLineProps>(), {
|
|||||||
connectionLineStyle: () => ({}),
|
connectionLineStyle: () => ({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const store = inject<RevueFlowStore>('store')!
|
|
||||||
const hooks = inject<RevueFlowHooks>('hooks')!
|
const hooks = inject<RevueFlowHooks>('hooks')!
|
||||||
|
const store = inject<RevueFlowStore>('store')!
|
||||||
|
|
||||||
const sourceHandle = computed(() =>
|
const sourceHandle =
|
||||||
store.connectionHandleId && store.connectionHandleType
|
store.connectionHandleId && store.connectionHandleType
|
||||||
? props.sourceNode.__rf.handleBounds[store.connectionHandleType].find((d: HandleElement) => d.id === store.connectionHandleId)
|
? props.sourceNode.__rf.handleBounds[store.connectionHandleType].find((d: HandleElement) => d.id === store.connectionHandleId)
|
||||||
: store.connectionHandleType && props.sourceNode.__rf.handleBounds[store.connectionHandleType][0],
|
: 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 sourceHandleX = computed(() =>
|
const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : props.sourceNode.__rf.height
|
||||||
sourceHandle.value ? sourceHandle.value.x + sourceHandle.value.width / 2 : (props.sourceNode.__rf.width as number) / 2,
|
const sourceX = props.sourceNode.__rf.position.x + sourceHandleX
|
||||||
)
|
const sourceY = props.sourceNode.__rf.position.y + sourceHandleY
|
||||||
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)
|
|
||||||
|
|
||||||
const targetX = computed(() => (store.connectionPosition?.x - store.transform[0]) / store.transform[2])
|
const isRightOrLeft = sourceHandle.position === Position.Left || sourceHandle.position === Position.Right
|
||||||
const targetY = computed(() => (store.connectionPosition?.y - store.transform[1]) / store.transform[2])
|
const targetPosition = isRightOrLeft ? Position.Left : Position.Top
|
||||||
|
|
||||||
const isRightOrLeft = computed(
|
const targetX = computed(() => (store.connectionPosition.x - store.transform[0]) / store.transform[2])
|
||||||
() => sourceHandle.value?.position === Position.Left || sourceHandle.value?.position === Position.Right,
|
const targetY = computed(() => (store.connectionPosition.y - store.transform[1]) / store.transform[2])
|
||||||
)
|
|
||||||
const targetPosition = computed(() => (isRightOrLeft.value ? Position.Left : Position.Top))
|
|
||||||
|
|
||||||
let dAttr = computed(() => `M${sourceX.value},${sourceY.value} ${targetX.value},${targetY.value}`)
|
const dAttr = computed(() => {
|
||||||
|
let path = `M${sourceX.value},${sourceY.value} ${targetX.value},${targetY.value}`
|
||||||
if (props.connectionLineType === ConnectionLineType.Bezier) {
|
switch (props.connectionLineType) {
|
||||||
dAttr = computed(() =>
|
case ConnectionLineType.Bezier:
|
||||||
getBezierPath({
|
path = getBezierPath({
|
||||||
sourceX: sourceX.value,
|
sourceX,
|
||||||
sourceY: sourceY.value,
|
sourceY,
|
||||||
sourcePosition: sourceHandle.value?.position,
|
sourcePosition: sourceHandle.position,
|
||||||
targetX: targetX.value,
|
targetX: targetX.value,
|
||||||
targetY: targetY.value,
|
targetY: targetY.value,
|
||||||
targetPosition: targetPosition.value,
|
targetPosition,
|
||||||
}),
|
})
|
||||||
)
|
break
|
||||||
} else if (props.connectionLineType === ConnectionLineType.Step) {
|
case ConnectionLineType.Step:
|
||||||
dAttr = computed(() =>
|
path = getSmoothStepPath({
|
||||||
getSmoothStepPath({
|
sourceX,
|
||||||
sourceX: sourceX.value,
|
sourceY,
|
||||||
sourceY: sourceY.value,
|
sourcePosition: sourceHandle.position,
|
||||||
sourcePosition: sourceHandle.value?.position,
|
targetX: targetX.value,
|
||||||
targetX: targetX.value,
|
targetY: targetY.value,
|
||||||
targetY: targetY.value,
|
targetPosition,
|
||||||
targetPosition: targetPosition.value,
|
borderRadius: 0,
|
||||||
borderRadius: 0,
|
})
|
||||||
}),
|
break
|
||||||
)
|
case ConnectionLineType.SmoothStep:
|
||||||
} else if (props.connectionLineType === ConnectionLineType.SmoothStep) {
|
path = getSmoothStepPath({
|
||||||
dAttr = computed(() =>
|
sourceX,
|
||||||
getSmoothStepPath({
|
sourceY,
|
||||||
sourceX: sourceX.value,
|
sourcePosition: sourceHandle.position,
|
||||||
sourceY: sourceY.value,
|
targetX: targetX.value,
|
||||||
sourcePosition: sourceHandle.value?.position,
|
targetY: targetY.value,
|
||||||
targetX: targetX.value,
|
targetPosition,
|
||||||
targetY: targetY.value,
|
})
|
||||||
targetPosition: targetPosition.value,
|
break
|
||||||
}),
|
}
|
||||||
)
|
return path
|
||||||
}
|
})
|
||||||
watch(dAttr, () => console.log(dAttr.value))
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<g>
|
<g class="revue-flow__connection">
|
||||||
<component
|
<component
|
||||||
:is="props.customConnectionLine"
|
:is="props.customConnectionLine"
|
||||||
v-if="props.customConnectionLine"
|
v-if="props.customConnectionLine"
|
||||||
class="revue-flow__connection"
|
|
||||||
v-bind="{
|
v-bind="{
|
||||||
sourceX: sourceX,
|
sourceX: sourceX,
|
||||||
sourceY: sourceY,
|
sourceY: sourceY,
|
||||||
|
|||||||
@@ -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>
|
|
||||||
) : (
|
|
||||||
''
|
|
||||||
)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -4,7 +4,7 @@ import { RevueFlowHooks } from '~/hooks/RevueFlowHooks'
|
|||||||
import { getEdgePositions, getHandle, getSourceTargetNodes, isEdgeVisible } from '~/container/EdgeRenderer/utils'
|
import { getEdgePositions, getHandle, getSourceTargetNodes, isEdgeVisible } from '~/container/EdgeRenderer/utils'
|
||||||
import { isEdge } from '~/utils/graph'
|
import { isEdge } from '~/utils/graph'
|
||||||
import { ConnectionMode, Dimensions, Edge, EdgeType, Elements, Position, RevueFlowStore, Transform } from '~/types'
|
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 {
|
interface EdgeProps {
|
||||||
type: EdgeType
|
type: EdgeType
|
||||||
@@ -12,7 +12,7 @@ interface EdgeProps {
|
|||||||
nodes: ReturnType<typeof getSourceTargetNodes>
|
nodes: ReturnType<typeof getSourceTargetNodes>
|
||||||
dimensions: Dimensions
|
dimensions: Dimensions
|
||||||
transform: Transform
|
transform: Transform
|
||||||
selectedElements?: Elements | null
|
selectedElements?: Elements
|
||||||
elementsSelectable?: boolean
|
elementsSelectable?: boolean
|
||||||
onlyRenderVisibleElements?: boolean
|
onlyRenderVisibleElements?: boolean
|
||||||
connectionMode?: ConnectionMode
|
connectionMode?: ConnectionMode
|
||||||
@@ -23,7 +23,6 @@ interface EdgeProps {
|
|||||||
const props = withDefaults(defineProps<EdgeProps>(), {
|
const props = withDefaults(defineProps<EdgeProps>(), {
|
||||||
elementsSelectable: true,
|
elementsSelectable: true,
|
||||||
onlyRenderVisibleElements: false,
|
onlyRenderVisibleElements: false,
|
||||||
selectedElements: () => null,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const store = inject<RevueFlowStore>('store')!
|
const store = inject<RevueFlowStore>('store')!
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ConnectionMode, ElementId, Position, RevueFlowStore } from '~/types'
|
import { ElementId, Position, RevueFlowStore } from '~/types'
|
||||||
import { onMouseDown, ValidConnectionFunc } from '~/components/Handle/handler'
|
import { onMouseDown, ValidConnectionFunc } from '~/components/Handle/utils'
|
||||||
import { RevueFlowHooks } from '~/hooks/RevueFlowHooks'
|
import { RevueFlowHooks } from '~/hooks/RevueFlowHooks'
|
||||||
|
|
||||||
interface HandleProps {
|
interface HandleProps {
|
||||||
@@ -15,7 +15,6 @@ const props = withDefaults(defineProps<HandleProps>(), {
|
|||||||
id: '',
|
id: '',
|
||||||
type: 'source',
|
type: 'source',
|
||||||
position: Position.Top,
|
position: Position.Top,
|
||||||
isValidConnection: () => true,
|
|
||||||
connectable: true,
|
connectable: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -24,7 +23,18 @@ const hooks = inject<RevueFlowHooks>('hooks')!
|
|||||||
const nodeId = inject<ElementId>('NodeIdContext')!
|
const nodeId = inject<ElementId>('NodeIdContext')!
|
||||||
|
|
||||||
const onMouseDownHandler = (event: MouseEvent) =>
|
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>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getHostForElement } from '~/utils'
|
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'
|
import { RevueFlowHooks } from '~/hooks/RevueFlowHooks'
|
||||||
|
|
||||||
export type ValidConnectionFunc = (connection: Connection) => boolean
|
export type ValidConnectionFunc = (connection: Connection) => boolean
|
||||||
@@ -82,7 +82,9 @@ export function onMouseDown(
|
|||||||
handleId: ElementId,
|
handleId: ElementId,
|
||||||
nodeId: ElementId,
|
nodeId: ElementId,
|
||||||
isTarget: boolean,
|
isTarget: boolean,
|
||||||
isValidConnection: ValidConnectionFunc = () => true,
|
isValidConnection: ValidConnectionFunc = () => {
|
||||||
|
return true
|
||||||
|
},
|
||||||
elementEdgeUpdaterType?: HandleType,
|
elementEdgeUpdaterType?: HandleType,
|
||||||
): void {
|
): void {
|
||||||
const revueFlowNode = (event.target as Element).closest('.revue-flow')
|
const revueFlowNode = (event.target as Element).closest('.revue-flow')
|
||||||
@@ -105,12 +107,6 @@ export function onMouseDown(
|
|||||||
const containerBounds = revueFlowNode.getBoundingClientRect()
|
const containerBounds = revueFlowNode.getBoundingClientRect()
|
||||||
let recentHoveredHandle: Element
|
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.x = event.clientX - containerBounds.left
|
||||||
store.connectionPosition.y = event.clientY - containerBounds.top
|
store.connectionPosition.y = event.clientY - containerBounds.top
|
||||||
|
|
||||||
@@ -122,8 +118,8 @@ export function onMouseDown(
|
|||||||
hooks.connectStart.trigger({ event, params: { nodeId, handleId, handleType } })
|
hooks.connectStart.trigger({ event, params: { nodeId, handleId, handleType } })
|
||||||
|
|
||||||
function onMouseMove(event: MouseEvent) {
|
function onMouseMove(event: MouseEvent) {
|
||||||
connectionPosition.value.x = event.clientX - containerBounds.left
|
store.connectionPosition.x = event.clientX - containerBounds.left
|
||||||
connectionPosition.value.y = event.clientY - containerBounds.top
|
store.connectionPosition.y = event.clientY - containerBounds.top
|
||||||
|
|
||||||
const { connection, elementBelow, isValid, isHoveringHandle } = checkElementBelowIsValid(
|
const { connection, elementBelow, isValid, isHoveringHandle } = checkElementBelowIsValid(
|
||||||
event,
|
event,
|
||||||
@@ -173,6 +169,7 @@ export function onMouseDown(
|
|||||||
|
|
||||||
resetRecentHandle(recentHoveredHandle)
|
resetRecentHandle(recentHoveredHandle)
|
||||||
store.setConnectionNodeId({ connectionNodeId: undefined, connectionHandleId: undefined, connectionHandleType: undefined })
|
store.setConnectionNodeId({ connectionNodeId: undefined, connectionHandleId: undefined, connectionHandleType: undefined })
|
||||||
|
store.connectionPosition = { x: NaN, y: NaN }
|
||||||
|
|
||||||
doc.removeEventListener('mousemove', onMouseMove as EventListenerOrEventListenerObject)
|
doc.removeEventListener('mousemove', onMouseMove as EventListenerOrEventListenerObject)
|
||||||
doc.removeEventListener('mouseup', onMouseUp as EventListenerOrEventListenerObject)
|
doc.removeEventListener('mouseup', onMouseUp as EventListenerOrEventListenerObject)
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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>
|
|
||||||
) : (
|
|
||||||
''
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -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'
|
||||||
@@ -17,7 +17,7 @@ export default function configureStore(
|
|||||||
setActivePinia(pinia)
|
setActivePinia(pinia)
|
||||||
|
|
||||||
return defineStore({
|
return defineStore({
|
||||||
id: `revue-flow-${Math.random()}`,
|
id: `revue-flow-${Math.floor(Math.random() * 100)}`,
|
||||||
state: () => ({
|
state: () => ({
|
||||||
...preloadedState,
|
...preloadedState,
|
||||||
}),
|
}),
|
||||||
|
|||||||
+1
-1
@@ -41,7 +41,7 @@ export const initialState: RevueFlowState = {
|
|||||||
connectionNodeId: undefined,
|
connectionNodeId: undefined,
|
||||||
connectionHandleId: undefined,
|
connectionHandleId: undefined,
|
||||||
connectionHandleType: 'source',
|
connectionHandleType: 'source',
|
||||||
connectionPosition: { x: 0, y: 0 },
|
connectionPosition: { x: NaN, y: NaN },
|
||||||
connectionMode: ConnectionMode.Strict,
|
connectionMode: ConnectionMode.Strict,
|
||||||
|
|
||||||
snapGrid: [15, 15],
|
snapGrid: [15, 15],
|
||||||
|
|||||||
+1
-1
@@ -31,7 +31,7 @@ export interface RevueFlowState {
|
|||||||
connectionNodeId?: ElementId
|
connectionNodeId?: ElementId
|
||||||
connectionHandleId?: ElementId
|
connectionHandleId?: ElementId
|
||||||
connectionHandleType?: HandleType
|
connectionHandleType?: HandleType
|
||||||
connectionPosition?: XYPosition
|
connectionPosition: XYPosition
|
||||||
connectionMode: ConnectionMode
|
connectionMode: ConnectionMode
|
||||||
|
|
||||||
snapToGrid: boolean
|
snapToGrid: boolean
|
||||||
|
|||||||
Reference in New Issue
Block a user