update(examples): move examples into src dir
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
<script lang="ts" setup>
|
||||
import { VueFlow, MiniMap, Controls, Background, isNode, useVueFlow, Elements } from '@braks/vue-flow'
|
||||
import ResizableNode from './ResizableNode.vue'
|
||||
|
||||
const elements = ref<Elements>([
|
||||
{ id: '1', type: 'input', label: 'Node 1', position: { x: 250, y: 5 } },
|
||||
{
|
||||
id: '2',
|
||||
type: 'resize',
|
||||
label: 'Node 2',
|
||||
position: { x: 100, y: 100 },
|
||||
},
|
||||
{ id: '3', label: 'Node 3', position: { x: 400, y: 100 } },
|
||||
{ id: '4', label: 'Node 4', position: { x: 400, y: 200 } },
|
||||
{ id: 'e1-2', source: '1', target: '2', animated: true },
|
||||
{ id: 'e1-3', source: '1', target: '3' },
|
||||
])
|
||||
const { onPaneReady, onNodeDragStop, onConnect, instance, addEdges, store } = useVueFlow()
|
||||
onPaneReady(({ fitView }) => {
|
||||
fitView({ padding: 0.1 })
|
||||
})
|
||||
onNodeDragStop((e) => console.log('drag stop', e))
|
||||
onConnect((params) => addEdges([params]))
|
||||
|
||||
const updatePos = () =>
|
||||
elements.value.forEach((el) => {
|
||||
if (isNode(el)) {
|
||||
el.position = {
|
||||
x: Math.random() * 400,
|
||||
y: Math.random() * 400,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const logToObject = () => console.log(instance.value?.toObject())
|
||||
const resetTransform = () => instance.value?.setTransform({ x: 0, y: 0, zoom: 1 })
|
||||
const toggleclass = () => elements.value.forEach((el) => (el.class = el.class === 'light' ? 'dark' : 'light'))
|
||||
</script>
|
||||
<template>
|
||||
<VueFlow v-model="elements" class="vue-flow-basic-example" :default-zoom="1.5" :min-zoom="0.2" :max-zoom="4">
|
||||
<template #node-resize="props">
|
||||
<ResizableNode v-bind="props" />
|
||||
</template>
|
||||
<Background :height="33" bg-color="pink">
|
||||
<text fill="#000000" font-size="22" font-family="ARIAL" x="120" y="110"> LEVEL 1 </text>
|
||||
</Background>
|
||||
<Background :height="33" bg-color="purple">
|
||||
<text fill="white" font-size="22" font-family="ARIAL" x="120" y="110"> LEVEL 2 </text>
|
||||
</Background>
|
||||
<Background :height="33" bg-color="crimson">
|
||||
<text fill="white" font-size="22" font-family="ARIAL" x="120" y="110"> LEVEL 3 </text>
|
||||
</Background>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
<div style="position: absolute; right: 10px; top: 10px; z-index: 4">
|
||||
<button style="margin-right: 5px" @click="resetTransform">reset transform</button>
|
||||
<button style="margin-right: 5px" @click="updatePos">change pos</button>
|
||||
<button style="margin-right: 5px" @click="toggleclass">toggle class</button>
|
||||
<button @click="logToObject">toObject</button>
|
||||
</div>
|
||||
</VueFlow>
|
||||
</template>
|
||||
@@ -0,0 +1,74 @@
|
||||
<script lang="ts">
|
||||
import { VueFlow, Background, MiniMap, Controls, Elements, FlowEvents, FlowInstance, isNode, addEdge } from '@braks/vue-flow'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BasicOptionsAPI',
|
||||
components: { VueFlow, Background, MiniMap, Controls },
|
||||
data() {
|
||||
return {
|
||||
instance: null as FlowInstance | null,
|
||||
elements: [
|
||||
{ id: '1', type: 'input', label: 'Node 1', position: { x: 250, y: 5 } },
|
||||
{ id: '2', label: 'Node 2', position: { x: 100, y: 100 } },
|
||||
{ id: '3', label: 'Node 3', position: { x: 400, y: 100 } },
|
||||
{ id: '4', label: 'Node 4', position: { x: 400, y: 200 } },
|
||||
{ id: 'e1-2', source: '1', target: '2', animated: true },
|
||||
{ id: 'e1-3', source: '1', target: '3' },
|
||||
] as Elements,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
logToObject() {
|
||||
console.log(this.instance?.toObject())
|
||||
},
|
||||
resetTransform() {
|
||||
//
|
||||
},
|
||||
toggleclass() {
|
||||
this.elements.forEach((el) => (el.class = el.class === 'light' ? 'dark' : 'light'))
|
||||
},
|
||||
updatePos() {
|
||||
this.elements.forEach((el) => {
|
||||
if (isNode(el)) {
|
||||
el.position = {
|
||||
x: Math.random() * 400,
|
||||
y: Math.random() * 400,
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
onNodeDragStop(e: FlowEvents['nodeDragStop']) {
|
||||
console.log('drag stop', e)
|
||||
},
|
||||
onPaneReady({ fitView }: FlowEvents['paneReady']) {
|
||||
fitView({ padding: 0.1 })
|
||||
},
|
||||
onConnect(params: FlowEvents['connect']) {
|
||||
addEdge(params, this.elements)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<VueFlow
|
||||
v-model="elements"
|
||||
class="vue-flow-basic-example"
|
||||
:default-zoom="1.5"
|
||||
:min-zoom="0.2"
|
||||
:max-zoom="4"
|
||||
:zoom-on-scroll="false"
|
||||
@connect="onConnect"
|
||||
@pane-ready="onPaneReady"
|
||||
@node-drag-stop="onNodeDragStop"
|
||||
>
|
||||
<Background />
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
<div style="position: absolute; right: 10px; top: 10px; z-index: 4">
|
||||
<button style="margin-right: 5px" @click="resetTransform">reset transform</button>
|
||||
<button style="margin-right: 5px" @click="updatePos">change pos</button>
|
||||
<button style="margin-right: 5px" @click="toggleclass">toggle class</button>
|
||||
<button @click="logToObject">toObject</button>
|
||||
</div>
|
||||
</VueFlow>
|
||||
</template>
|
||||
@@ -0,0 +1,64 @@
|
||||
<script lang="ts" setup>
|
||||
import { Position, Handle, ValidConnectionFunc } from '@braks/vue-flow'
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
selected?: boolean
|
||||
connectable?: boolean
|
||||
label?: string
|
||||
isValidSourcePos?: ValidConnectionFunc
|
||||
isValidTargetPos?: ValidConnectionFunc
|
||||
parentNode?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
connectable: false,
|
||||
sourcePosition: 'bottom' as Position,
|
||||
targetPosition: 'top' as Position,
|
||||
})
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ResizableNode',
|
||||
inheritAttrs: false,
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Handle
|
||||
type="target"
|
||||
:position="props.targetPosition"
|
||||
:is-connectable="props.connectable"
|
||||
:is-valid-connection="props.isValidTargetPos"
|
||||
/>
|
||||
<div class="resize-node">
|
||||
<div v-html="props.label"></div>
|
||||
<div ref="el" class="resizer nodrag" />
|
||||
</div>
|
||||
<Handle
|
||||
type="source"
|
||||
:position="props.sourcePosition"
|
||||
:is-connectable="props.connectable"
|
||||
:is-valid-connection="props.isValidSourcePos"
|
||||
/>
|
||||
</template>
|
||||
<style>
|
||||
.resize-node {
|
||||
padding: 1rem;
|
||||
border: 1px solid gray;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.resizer {
|
||||
resize: both;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
outline: none;
|
||||
white-space: pre;
|
||||
overflow-wrap: normal;
|
||||
overflow: hidden;
|
||||
background: aliceblue;
|
||||
border: 1px solid gray;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
interface CustomConnectionLineProps {
|
||||
sourceX: number
|
||||
sourceY: number
|
||||
targetX: number
|
||||
targetY: number
|
||||
}
|
||||
const props = defineProps<CustomConnectionLineProps>()
|
||||
</script>
|
||||
<template>
|
||||
<g>
|
||||
<path
|
||||
class="animated"
|
||||
fill="none"
|
||||
stroke="#222"
|
||||
:stroke-width="1.5"
|
||||
:d="`M${props.sourceX},${props.sourceY} C ${props.sourceX} ${props.targetY} ${props.sourceX} ${props.targetY} ${props.targetX},${props.targetY}`"
|
||||
/>
|
||||
<circle :cx="props.targetX" :cy="props.targetY" fill="#fff" :r="3" stroke="#222" :stroke-width="1.5" />
|
||||
</g>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
import ConnectionLine from './ConnectionLine.vue'
|
||||
import { VueFlow, Background, BackgroundVariant, Elements } from '@braks/vue-flow'
|
||||
|
||||
const elements = ref<Elements>([
|
||||
{
|
||||
id: '1',
|
||||
type: 'input',
|
||||
label: 'Node 1',
|
||||
position: { x: 250, y: 5 },
|
||||
},
|
||||
])
|
||||
</script>
|
||||
<template>
|
||||
<VueFlow v-model="elements">
|
||||
<template #connection-line="props">
|
||||
<ConnectionLine v-bind="props" />
|
||||
</template>
|
||||
<Background :variant="BackgroundVariant.Lines" />
|
||||
</VueFlow>
|
||||
</template>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script lang="ts" setup>
|
||||
import { CSSProperties } from 'vue'
|
||||
import { Handle, Position, Connection, Edge, NodeProps } from '@braks/vue-flow'
|
||||
|
||||
interface ColorSelectorNodeProps extends NodeProps {
|
||||
data: {
|
||||
color: string
|
||||
onChange: (event: any) => void
|
||||
}
|
||||
}
|
||||
const props = defineProps<ColorSelectorNodeProps>()
|
||||
|
||||
const targetHandleStyle: CSSProperties = { background: '#555' }
|
||||
const sourceHandleStyleA: CSSProperties = { ...targetHandleStyle, top: '10px' }
|
||||
const sourceHandleStyleB: CSSProperties = { ...targetHandleStyle, bottom: '10px', top: 'auto' }
|
||||
|
||||
const onConnect = (params: Connection | Edge) => console.log('handle onConnect', params)
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Handle type="target" :position="Position.Left" :style="targetHandleStyle" :on-connect="onConnect" />
|
||||
<div>
|
||||
Custom Color Picker Node: <strong>{{ data.color }}</strong>
|
||||
</div>
|
||||
<input class="nodrag" type="color" :value="data.color" @input="props.data.onChange" />
|
||||
<Handle id="a" type="source" :position="Position.Right" :style="sourceHandleStyleA" />
|
||||
<Handle id="b" type="source" :position="Position.Right" :style="sourceHandleStyleB" />
|
||||
</template>
|
||||
@@ -0,0 +1,85 @@
|
||||
<script lang="ts" setup>
|
||||
import ColorSelectorNode from './ColorSelectorNode.vue'
|
||||
import { ConnectionMode, Controls, Elements, isEdge, MiniMap, Node, Position, SnapGrid, useVueFlow, VueFlow } from '@braks/vue-flow'
|
||||
|
||||
const elements = ref<Elements>([])
|
||||
const bgColor = ref('#1A192B')
|
||||
const connectionLineStyle = { stroke: '#fff' }
|
||||
const snapGrid: SnapGrid = [16, 16]
|
||||
|
||||
const nodeStroke = (n: Node): string => {
|
||||
if (n.type === 'input') return '#0041d0'
|
||||
if (n.type === 'selectorNode') return bgColor.value
|
||||
if (n.type === 'output') return '#ff0072'
|
||||
return '#eee'
|
||||
}
|
||||
const nodeColor = (n: Node): string => {
|
||||
if (n.type === 'selectorNode') return bgColor.value
|
||||
return '#fff'
|
||||
}
|
||||
|
||||
const onChange = (event: Event) => {
|
||||
elements.value.forEach((e) => {
|
||||
if (isEdge(e) || e.id !== '2') return e
|
||||
bgColor.value = (event.target as HTMLInputElement).value
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
elements.value = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'input',
|
||||
label: 'An input node',
|
||||
position: { x: 0, y: 50 },
|
||||
sourcePosition: Position.Right,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'selectorNode',
|
||||
data: { onChange, color: bgColor },
|
||||
style: { border: '1px solid #777', padding: '10px' },
|
||||
position: { x: 250, y: 50 },
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'output',
|
||||
label: 'Output A',
|
||||
position: { x: 650, y: 25 },
|
||||
targetPosition: Position.Left,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
type: 'output',
|
||||
label: 'Output B',
|
||||
position: { x: 650, y: 100 },
|
||||
targetPosition: Position.Left,
|
||||
},
|
||||
|
||||
{ id: 'e1-2', source: '1', target: '2', animated: true, style: { stroke: '#fff' } },
|
||||
{ id: 'e2a-3', source: '2', sourceHandle: 'a', target: '3', animated: true, style: { stroke: '#fff' } },
|
||||
{ id: 'e2b-4', source: '2', sourceHandle: 'b', target: '4', animated: true, style: { stroke: '#fff' } },
|
||||
]
|
||||
})
|
||||
|
||||
const { onPaneReady } = useVueFlow({
|
||||
connectionMode: ConnectionMode.Loose,
|
||||
connectionLineStyle,
|
||||
snapToGrid: true,
|
||||
snapGrid,
|
||||
defaultZoom: 1.5,
|
||||
})
|
||||
onPaneReady((i) => {
|
||||
i.fitView()
|
||||
console.log('flow loaded:', i)
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<VueFlow v-model="elements" :style="{ backgroundColor: bgColor }">
|
||||
<template #node-selectorNode="props">
|
||||
<ColorSelectorNode v-bind="props" />
|
||||
</template>
|
||||
<MiniMap :node-stroke-color="nodeStroke" :node-color="nodeColor" />
|
||||
<Controls />
|
||||
</VueFlow>
|
||||
</template>
|
||||
@@ -0,0 +1,49 @@
|
||||
<script lang="ts" setup>
|
||||
import Sidebar from './Sidebar.vue'
|
||||
import { VueFlow, FlowInstance, Node, useVueFlow } from '@braks/vue-flow'
|
||||
|
||||
let id = 0
|
||||
const getId = () => `dndnode_${id++}`
|
||||
|
||||
const { instance, onConnect, nodes, edges, addEdges, addNodes } = useVueFlow({
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'input',
|
||||
label: 'input node',
|
||||
position: { x: 250, y: 5 },
|
||||
},
|
||||
],
|
||||
})
|
||||
const onDragOver = (event: DragEvent) => {
|
||||
event.preventDefault()
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = 'move'
|
||||
}
|
||||
}
|
||||
|
||||
onConnect((params) => addEdges([params]))
|
||||
|
||||
const onDrop = (event: DragEvent) => {
|
||||
if (instance.value) {
|
||||
const type = event.dataTransfer?.getData('application/vueflow')
|
||||
const position = instance.value.project({ x: event.clientX, y: event.clientY - 40 })
|
||||
const newNode = {
|
||||
id: getId(),
|
||||
type,
|
||||
position,
|
||||
label: `${type} node`,
|
||||
} as Node
|
||||
addNodes([newNode])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="dndflow" @drop="onDrop">
|
||||
<VueFlow @dragover="onDragOver" />
|
||||
<Sidebar />
|
||||
</div>
|
||||
</template>
|
||||
<style>
|
||||
@import 'dnd.css';
|
||||
</style>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts" setup>
|
||||
const onDragStart = (event: DragEvent, nodeType: string) => {
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.setData('application/vueflow', nodeType)
|
||||
event.dataTransfer.effectAllowed = 'move'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<aside>
|
||||
<div class="description">You can drag these nodes to the pane on the left.</div>
|
||||
<div class="vue-flow__node-input" :draggable="true" @dragstart="(event: DragEvent) => onDragStart(event, 'input')">
|
||||
Input Node
|
||||
</div>
|
||||
<div class="vue-flow__node-default" :draggable="true" @dragstart="(event: DragEvent) => onDragStart(event, 'default')">
|
||||
Default Node
|
||||
</div>
|
||||
<div class="vue-flow__node-output" :draggable="true" @dragstart="(event: DragEvent) => onDragStart(event, 'output')">
|
||||
Output Node
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
@@ -0,0 +1,37 @@
|
||||
.dndflow {
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dndflow aside {
|
||||
border-right: 1px solid #eee;
|
||||
padding: 15px 10px;
|
||||
font-size: 12px;
|
||||
background: #fcfcfc;
|
||||
}
|
||||
|
||||
.dndflow aside > * {
|
||||
margin-bottom: 10px;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.dndflow aside .description {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.dndflow .vue-flow-wrapper {
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.dndflow {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.dndflow aside {
|
||||
width: 20%;
|
||||
max-width: 180px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts" setup>
|
||||
import { getElements } from './utils'
|
||||
import { VueFlow, MiniMap, Controls, FlowInstance } from '@braks/vue-flow'
|
||||
|
||||
const onLoad = (flowInstance: FlowInstance) => {
|
||||
flowInstance.fitView()
|
||||
console.log(flowInstance.getElements())
|
||||
}
|
||||
|
||||
const elements = getElements()
|
||||
</script>
|
||||
<template>
|
||||
<VueFlow v-model="elements" :min-zoom="0.2" @pane-ready="onLoad">
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
</VueFlow>
|
||||
</template>
|
||||
@@ -0,0 +1,106 @@
|
||||
import { Elements, Position } from '@braks/vue-flow/src'
|
||||
|
||||
const nodeWidth = 80
|
||||
const nodeGapWidth = nodeWidth * 2
|
||||
const nodeStyle = { width: `${nodeWidth}px`, fontSize: '11px', color: 'white' }
|
||||
|
||||
const sourceTargetPositions = [
|
||||
{ source: Position.Bottom, target: Position.Top },
|
||||
{ source: Position.Right, target: Position.Left },
|
||||
]
|
||||
const nodeColors = [
|
||||
['#1e9e99', '#4cb3ac', '#6ec9c0', '#8ddfd4'],
|
||||
['#0f4c75', '#1b5d8b', '#276fa1', '#3282b8'],
|
||||
]
|
||||
const edgeTypes = ['default', 'step', 'smoothstep', 'straight']
|
||||
const offsets = [
|
||||
{
|
||||
x: 0,
|
||||
y: -nodeGapWidth,
|
||||
},
|
||||
{
|
||||
x: nodeGapWidth,
|
||||
y: -nodeGapWidth,
|
||||
},
|
||||
{
|
||||
x: nodeGapWidth,
|
||||
y: 0,
|
||||
},
|
||||
{
|
||||
x: nodeGapWidth,
|
||||
y: nodeGapWidth,
|
||||
},
|
||||
{
|
||||
x: 0,
|
||||
y: nodeGapWidth,
|
||||
},
|
||||
{
|
||||
x: -nodeGapWidth,
|
||||
y: nodeGapWidth,
|
||||
},
|
||||
{
|
||||
x: -nodeGapWidth,
|
||||
y: 0,
|
||||
},
|
||||
{
|
||||
x: -nodeGapWidth,
|
||||
y: -nodeGapWidth,
|
||||
},
|
||||
]
|
||||
|
||||
let id = 0
|
||||
const getNodeId = () => (id++).toString()
|
||||
|
||||
export function getElements(): Elements {
|
||||
const initialElements = []
|
||||
|
||||
for (let sourceTargetIndex = 0; sourceTargetIndex < sourceTargetPositions.length; sourceTargetIndex++) {
|
||||
const currSourceTargetPos = sourceTargetPositions[sourceTargetIndex]
|
||||
|
||||
for (let edgeTypeIndex = 0; edgeTypeIndex < edgeTypes.length; edgeTypeIndex++) {
|
||||
const currEdgeType = edgeTypes[edgeTypeIndex]
|
||||
|
||||
for (let offsetIndex = 0; offsetIndex < offsets.length; offsetIndex++) {
|
||||
const currOffset = offsets[offsetIndex]
|
||||
|
||||
const style = { ...nodeStyle, background: nodeColors[sourceTargetIndex][edgeTypeIndex] }
|
||||
const sourcePosition = {
|
||||
x: offsetIndex * nodeWidth * 4,
|
||||
y: edgeTypeIndex * 300 + sourceTargetIndex * edgeTypes.length * 300,
|
||||
}
|
||||
const sourceId = getNodeId()
|
||||
const sourceLabel = `Source ${sourceId}`
|
||||
const sourceNode = {
|
||||
id: sourceId,
|
||||
style,
|
||||
label: sourceLabel,
|
||||
position: sourcePosition,
|
||||
sourcePosition: currSourceTargetPos.source,
|
||||
targetPosition: currSourceTargetPos.target,
|
||||
}
|
||||
|
||||
const targetId = getNodeId()
|
||||
const targetLabel = `Target ${targetId}`
|
||||
const targetPosition = {
|
||||
x: sourcePosition.x + currOffset.x,
|
||||
y: sourcePosition.y + currOffset.y,
|
||||
}
|
||||
const targetNode = {
|
||||
id: targetId,
|
||||
style,
|
||||
label: targetLabel,
|
||||
position: targetPosition,
|
||||
sourcePosition: currSourceTargetPos.source,
|
||||
targetPosition: currSourceTargetPos.target,
|
||||
}
|
||||
|
||||
initialElements.push(sourceNode)
|
||||
initialElements.push(targetNode)
|
||||
|
||||
initialElements.push({ id: `${sourceId}-${targetId}`, source: sourceId, target: targetId, type: currEdgeType })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return initialElements
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<script lang="ts" setup>
|
||||
import { getBezierPath, getMarkerId, Position, EdgeProps } from '@braks/vue-flow'
|
||||
|
||||
interface CustomEdgeProps<T = { text: string }> extends EdgeProps<T> {
|
||||
source: string
|
||||
target: string
|
||||
sourceHandleId?: string
|
||||
targetHandleId?: string
|
||||
id: string
|
||||
sourceX: number
|
||||
sourceY: number
|
||||
targetX: number
|
||||
targetY: number
|
||||
sourcePosition: Position
|
||||
targetPosition: Position
|
||||
markerEnd?: string
|
||||
data?: T
|
||||
}
|
||||
|
||||
const props = defineProps<CustomEdgeProps>()
|
||||
const edgePath = computed(() =>
|
||||
getBezierPath({
|
||||
sourceX: props.sourceX,
|
||||
sourceY: props.sourceY,
|
||||
sourcePosition: props.sourcePosition,
|
||||
targetX: props.targetX,
|
||||
targetY: props.targetY,
|
||||
targetPosition: props.targetPosition,
|
||||
}),
|
||||
)
|
||||
|
||||
const markerEnd = computed(() => getMarkerId(props.markerEnd))
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<path :id="props.id" class="vue-flow__edge-path" :d="edgePath" :marker-end="markerEnd" />
|
||||
<text>
|
||||
<textPath :href="`#${props.id}`" :style="{ fontSize: '12px' }" startOffset="50%" text-anchor="middle">
|
||||
{{ props.data?.text }}
|
||||
</textPath>
|
||||
</text>
|
||||
</template>
|
||||
@@ -0,0 +1,63 @@
|
||||
<script lang="ts" setup>
|
||||
import { getEdgeCenter, getBezierPath, getMarkerId, Position, EdgeProps, EdgeText } from '@braks/vue-flow'
|
||||
|
||||
interface CustomEdgeProps extends EdgeProps {
|
||||
source: string
|
||||
target: string
|
||||
sourceHandleId?: string
|
||||
targetHandleId?: string
|
||||
id: string
|
||||
sourceX: number
|
||||
sourceY: number
|
||||
targetX: number
|
||||
targetY: number
|
||||
sourcePosition: Position
|
||||
targetPosition: Position
|
||||
markerEndId?: string
|
||||
data?: {
|
||||
text: string
|
||||
}
|
||||
}
|
||||
|
||||
const props = defineProps<CustomEdgeProps>()
|
||||
|
||||
const edgePath = computed(() =>
|
||||
getBezierPath({
|
||||
sourceX: props.sourceX,
|
||||
sourceY: props.sourceY,
|
||||
sourcePosition: props.sourcePosition,
|
||||
targetX: props.targetX,
|
||||
targetY: props.targetY,
|
||||
targetPosition: props.targetPosition,
|
||||
}),
|
||||
)
|
||||
const markerEnd = computed(() => getMarkerId(props.markerEnd))
|
||||
const center = computed(() =>
|
||||
getEdgeCenter({
|
||||
sourceX: props.sourceX,
|
||||
sourceY: props.sourceY,
|
||||
targetX: props.targetX,
|
||||
targetY: props.targetY,
|
||||
}),
|
||||
)
|
||||
const onClick = () => console.log(props.data)
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<path :id="props.id" class="vue-flow__edge-path" :d="edgePath" :marker-end="markerEnd" />
|
||||
<EdgeText
|
||||
:x="center[0]"
|
||||
:y="center[1]"
|
||||
:label="props.data?.text"
|
||||
:label-style="{ fill: 'white' }"
|
||||
:label-show-bg="true"
|
||||
:label-bg-style="{ fill: 'red' }"
|
||||
:label-bg-padding="[2, 4]"
|
||||
:label-bg-border-radius="2"
|
||||
@click="onClick"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
interface CustomLabelProps {
|
||||
label: string
|
||||
}
|
||||
|
||||
const props = defineProps<CustomLabelProps>()
|
||||
</script>
|
||||
<template>
|
||||
<tspan dy="10" x="0">{{ props.label }}</tspan>
|
||||
</template>
|
||||
@@ -0,0 +1,91 @@
|
||||
<script lang="ts" setup>
|
||||
import CustomEdge from './CustomEdge.vue'
|
||||
import CustomEdge2 from './CustomEdge2.vue'
|
||||
import CustomLabel from './CustomLabel.vue'
|
||||
import { VueFlow, MiniMap, Controls, Background, MarkerType, useVueFlow, Edge, Node } from '@braks/vue-flow'
|
||||
|
||||
const initialNodes: Node[] = [
|
||||
{ id: '1', type: 'input', label: 'Input 1', position: { x: 250, y: 0 } },
|
||||
{ id: '2', label: 'Node 2', position: { x: 150, y: 100 } },
|
||||
{ id: '2a', label: 'Node 2a', position: { x: 0, y: 180 } },
|
||||
{ id: '3', label: 'Node 3', position: { x: 250, y: 200 } },
|
||||
{ id: '4', label: 'Node 4', position: { x: 400, y: 300 } },
|
||||
{ id: '3a', label: 'Node 3a', position: { x: 150, y: 300 } },
|
||||
{ id: '5', label: 'Node 5', position: { x: 250, y: 400 } },
|
||||
{ id: '6', type: 'output', label: 'Output 6', position: { x: 50, y: 550 } },
|
||||
{ id: '7', type: 'output', label: 'Output 7', position: { x: 250, y: 550 } },
|
||||
{ id: '8', type: 'output', label: 'Output 8', position: { x: 525, y: 600 } },
|
||||
{ id: '9', type: 'output', label: 'Output 9', position: { x: 675, y: 500 } },
|
||||
]
|
||||
|
||||
const initialEdges: Edge[] = [
|
||||
{ id: 'e1-2', source: '1', target: '2', label: 'bezier edge (default)', class: 'normal-edge' },
|
||||
{ id: 'e2-2a', source: '2', target: '2a', type: 'smoothstep', label: 'smoothstep edge' },
|
||||
{ id: 'e2-3', source: '2', target: '3', type: 'step', label: 'step edge' },
|
||||
{ id: 'e3-4', source: '3', target: '4', type: 'straight', label: 'straight edge' },
|
||||
{ id: 'e3-3a', source: '3', target: '3a', type: 'straight', label: 'label only edge', style: { stroke: 'none' } },
|
||||
{ id: 'e3-5', source: '4', target: '5', animated: true, label: 'animated styled edge', style: { stroke: 'red' } },
|
||||
{
|
||||
id: 'e5-6',
|
||||
source: '5',
|
||||
target: '6',
|
||||
label: {
|
||||
component: markRaw(CustomLabel),
|
||||
props: {
|
||||
label: 'custom label text',
|
||||
},
|
||||
},
|
||||
labelStyle: { fill: 'red', fontWeight: 700 },
|
||||
markerEnd: {
|
||||
type: MarkerType.Arrow,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'e5-7',
|
||||
source: '5',
|
||||
target: '7',
|
||||
label: 'label with styled bg',
|
||||
labelBgPadding: [8, 4],
|
||||
labelBgBorderRadius: 4,
|
||||
labelBgStyle: { fill: '#FFCC00', color: '#fff', fillOpacity: 0.7 },
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'e5-8',
|
||||
source: '5',
|
||||
target: '8',
|
||||
type: 'custom',
|
||||
data: { text: 'custom edge' },
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'e5-9',
|
||||
source: '5',
|
||||
target: '9',
|
||||
type: 'custom2',
|
||||
data: { text: 'custom edge 2' },
|
||||
},
|
||||
]
|
||||
|
||||
useVueFlow({
|
||||
nodes: initialNodes,
|
||||
edges: initialEdges,
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<VueFlow :fit-view-on-init="true" :snap-to-grid="true">
|
||||
<template #edge-custom="props">
|
||||
<CustomEdge v-bind="props" />
|
||||
</template>
|
||||
<template #edge-custom2="props">
|
||||
<CustomEdge2 v-bind="props" />
|
||||
</template>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
<Background />
|
||||
</VueFlow>
|
||||
</template>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script lang="ts" setup>
|
||||
import { VueFlow, MiniMap, Controls, Background, BackgroundVariant, Node, useVueFlow } from '@braks/vue-flow'
|
||||
|
||||
const { nodes, addNodes, edges, addEdges, onConnect, onPaneReady, onNodeDragStop, dimensions } = useVueFlow()
|
||||
|
||||
onConnect((params) => addEdges([params]))
|
||||
onPaneReady((flowInstance) => console.log('flow loaded:', flowInstance))
|
||||
onNodeDragStop((node) => console.log('drag stop', node))
|
||||
|
||||
const addRandomNode = () => {
|
||||
const nodeId = (nodes.value.length + 1).toString()
|
||||
const newNode: Node = {
|
||||
id: nodeId,
|
||||
label: `Node: ${nodeId}`,
|
||||
position: { x: Math.random() * dimensions.value.width, y: Math.random() * dimensions.value.height },
|
||||
}
|
||||
addNodes([newNode])
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<VueFlow>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
<Background :variant="BackgroundVariant.Lines" />
|
||||
|
||||
<button type="button" :style="{ position: 'absolute', left: '10px', top: '10px', zIndex: 4 }" @click="addRandomNode">
|
||||
add node
|
||||
</button>
|
||||
</VueFlow>
|
||||
</template>
|
||||
@@ -0,0 +1,38 @@
|
||||
<script lang="ts" setup>
|
||||
import { VueFlow, MiniMap, Controls, useVueFlow } from '@braks/vue-flow'
|
||||
|
||||
const isHidden = ref(false)
|
||||
|
||||
const { nodes, edges } = useVueFlow({
|
||||
nodes: [
|
||||
{ id: '1', type: 'input', label: 'Node 1', position: { x: 250, y: 5 } },
|
||||
{ id: '2', label: 'Node 2', position: { x: 100, y: 100 } },
|
||||
{ id: '3', label: 'Node 3', position: { x: 400, y: 100 } },
|
||||
{ id: '4', label: 'Node 4', position: { x: 400, y: 200 } },
|
||||
],
|
||||
edges: [
|
||||
{ id: 'e1-2', source: '1', target: '2' },
|
||||
{ id: 'e1-3', source: '1', target: '3' },
|
||||
{ id: 'e3-4', source: '3', target: '4' },
|
||||
],
|
||||
})
|
||||
watchEffect(() => {
|
||||
nodes.value.forEach((n) => (n.hidden = isHidden.value))
|
||||
edges.value.forEach((e) => (e.hidden = isHidden.value))
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<VueFlow>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
|
||||
<div :style="{ position: 'absolute', left: '10px', top: '10px', zIndex: 4 }">
|
||||
<div>
|
||||
<label for="ishidden">
|
||||
isHidden
|
||||
<input id="ishidden" v-model="isHidden" type="checkbox" class="vue-flow__ishidden" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</VueFlow>
|
||||
</template>
|
||||
@@ -0,0 +1,122 @@
|
||||
<script lang="ts" setup>
|
||||
import { VueFlow, MiniMap, Controls, useVueFlow } from '@braks/vue-flow'
|
||||
|
||||
const {
|
||||
nodesDraggable,
|
||||
nodesConnectable,
|
||||
elementsSelectable,
|
||||
zoomOnScroll,
|
||||
zoomOnDoubleClick,
|
||||
zoomOnPinch,
|
||||
panOnScroll,
|
||||
panOnScrollMode,
|
||||
panOnDrag,
|
||||
onConnect,
|
||||
onNodeDragStart,
|
||||
onNodeDragStop,
|
||||
onPaneClick,
|
||||
onPaneScroll,
|
||||
onPaneContextMenu,
|
||||
onMoveEnd,
|
||||
addEdges,
|
||||
} = useVueFlow({
|
||||
modelValue: [
|
||||
{ id: '1', type: 'input', label: 'Node 1', position: { x: 250, y: 5 } },
|
||||
{ id: '2', label: 'Node 2', position: { x: 100, y: 100 } },
|
||||
{ id: '3', label: 'Node 3', position: { x: 400, y: 100 } },
|
||||
{ id: '4', label: 'Node 4', position: { x: 400, y: 200 } },
|
||||
{ id: 'e1-2', source: '1', target: '2', animated: true },
|
||||
{ id: 'e1-3', source: '1', target: '3' },
|
||||
],
|
||||
})
|
||||
|
||||
const captureZoomClick = ref(false)
|
||||
const captureZoomScroll = ref(false)
|
||||
|
||||
onConnect((params) => addEdges([params]))
|
||||
onNodeDragStart((e) => console.log('drag start', e))
|
||||
onNodeDragStop((e) => console.log('drag stop', e))
|
||||
onPaneClick((event) => captureZoomClick.value && console.log('pane click', event))
|
||||
onPaneScroll((event) => captureZoomScroll.value && console.log('pane scroll', event))
|
||||
onPaneContextMenu((event) => captureZoomClick && console.log('pane ctx menu', event))
|
||||
onMoveEnd((flowTransform) => console.log('move end', flowTransform))
|
||||
</script>
|
||||
<template>
|
||||
<VueFlow>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
|
||||
<div :style="{ position: 'absolute', left: 10, top: 10, zIndex: 4 }">
|
||||
<div>
|
||||
<label for="draggable">
|
||||
nodesDraggable
|
||||
<input id="draggable" v-model="nodesDraggable" type="checkbox" class="vue-flow__draggable" />
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="connectable">
|
||||
nodesConnectable
|
||||
<input id="connectable" v-model="nodesConnectable" type="checkbox" class="vue-flow__connectable" />
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="selectable">
|
||||
elementsSelectable
|
||||
<input id="selectable" v-model="elementsSelectable" type="checkbox" class="vue-flow__selectable" />
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="zoomonscroll">
|
||||
zoomOnScroll
|
||||
<input id="zoomonscroll" v-model="zoomOnScroll" type="checkbox" class="vue-flow__zoomonscroll" />
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="zoomonpinch">
|
||||
zoomOnPinch
|
||||
<input id="zoomonpinch" v-model="zoomOnPinch" type="checkbox" class="vue-flow__zoomonpinch" />
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="panonscroll">
|
||||
panOnScroll
|
||||
<input id="panonscroll" v-model="panOnScroll" type="checkbox" class="vue-flow__panonscroll" />
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
panOnScrollMode
|
||||
<select id="panonscrollmode" v-model="panOnScrollMode" class="vue-flow__panonscrollmode">
|
||||
<option value="free">free</option>
|
||||
<option value="horizontal">horizontal</option>
|
||||
<option value="vertical">vertical</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="zoomondbl">
|
||||
zoomOnDoubleClick
|
||||
<input id="zoomondbl" v-model="zoomOnDoubleClick" type="checkbox" class="vue-flow__zoomondbl" />
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="panemoveable">
|
||||
paneMovable
|
||||
<input id="panemoveable" v-model="panOnDrag" type="checkbox" class="vue-flow__panemoveable" />
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="capturezoompaneclick">
|
||||
capture onPaneClick
|
||||
<input id="capturezoompaneclick" v-model="captureZoomClick" type="checkbox" class="vue-flow__capturezoompaneclick" />
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="capturezoompanescroll">
|
||||
capture onPaneScroll
|
||||
<input id="capturezoompanescroll" v-model="captureZoomScroll" type="checkbox" class="vue-flow__capturezoompanescroll" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</VueFlow>
|
||||
</template>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script lang="ts" setup>
|
||||
import dagre from 'dagre'
|
||||
import initialElements from './initial-elements'
|
||||
import { VueFlow, Controls, ConnectionMode, Elements, isNode, CoordinateExtent, Position } from '@braks/vue-flow'
|
||||
|
||||
const dagreGraph = new dagre.graphlib.Graph()
|
||||
dagreGraph.setDefaultEdgeLabel(() => ({}))
|
||||
|
||||
const nodeExtent: CoordinateExtent = [
|
||||
[0, -100],
|
||||
[1000, 500],
|
||||
]
|
||||
|
||||
const elements = ref<Elements>(initialElements)
|
||||
|
||||
const onLayout = (direction: string) => {
|
||||
const isHorizontal = direction === 'LR'
|
||||
dagreGraph.setGraph({ rankdir: direction })
|
||||
|
||||
elements.value.forEach((el) => {
|
||||
if (isNode(el)) {
|
||||
dagreGraph.setNode(el.id, { width: 150, height: 50 })
|
||||
} else {
|
||||
dagreGraph.setEdge(el.source, el.target)
|
||||
}
|
||||
})
|
||||
|
||||
dagre.layout(dagreGraph)
|
||||
|
||||
elements.value.forEach((el) => {
|
||||
if (isNode(el)) {
|
||||
const nodeWithPosition = dagreGraph.node(el.id)
|
||||
el.targetPosition = isHorizontal ? Position.Left : Position.Top
|
||||
el.sourcePosition = isHorizontal ? Position.Right : Position.Bottom
|
||||
el.position = { x: nodeWithPosition.x, y: nodeWithPosition.y }
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="layoutflow">
|
||||
<VueFlow v-model="elements" :node-extent="nodeExtent" :connection-mode="ConnectionMode.Loose" @pane-ready="onLayout('TB')">
|
||||
<Controls />
|
||||
</VueFlow>
|
||||
<div class="controls">
|
||||
<button :style="{ marginRight: 10 }" @click="onLayout('TB')">vertical layout</button>
|
||||
<button @click="onLayout('LR')">horizontal layout</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style>
|
||||
@import 'layouting.css';
|
||||
</style>
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Elements, XYPosition } from '@braks/vue-flow'
|
||||
|
||||
const position: XYPosition = { x: 0, y: 0 }
|
||||
|
||||
const elements: Elements = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'input',
|
||||
label: 'input',
|
||||
position,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
label: 'node 2',
|
||||
position,
|
||||
},
|
||||
{
|
||||
id: '2a',
|
||||
label: 'node 2a',
|
||||
position,
|
||||
},
|
||||
{
|
||||
id: '2b',
|
||||
label: 'node 2b',
|
||||
position,
|
||||
},
|
||||
{
|
||||
id: '2c',
|
||||
label: 'node 2c',
|
||||
position,
|
||||
},
|
||||
{
|
||||
id: '2d',
|
||||
label: 'node 2d',
|
||||
position,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
label: 'node 3',
|
||||
position,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
label: 'node 4',
|
||||
position,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
label: 'node 5',
|
||||
position,
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
type: 'output',
|
||||
label: 'output',
|
||||
position,
|
||||
},
|
||||
{ id: '7', type: 'output', label: 'output', position: { x: 400, y: 450 } },
|
||||
{ id: 'e12', source: '1', target: '2', type: 'smoothstep', animated: true },
|
||||
{ id: 'e13', source: '1', target: '3', type: 'smoothstep', animated: true },
|
||||
{ id: 'e22a', source: '2', target: '2a', type: 'smoothstep', animated: true },
|
||||
{ id: 'e22b', source: '2', target: '2b', type: 'smoothstep', animated: true },
|
||||
{ id: 'e22c', source: '2', target: '2c', type: 'smoothstep', animated: true },
|
||||
{ id: 'e2c2d', source: '2c', target: '2d', type: 'smoothstep', animated: true },
|
||||
|
||||
{ id: 'e45', source: '4', target: '5', type: 'smoothstep', animated: true },
|
||||
{ id: 'e56', source: '5', target: '6', type: 'smoothstep', animated: true },
|
||||
{ id: 'e57', source: '5', target: '7', type: 'smoothstep', animated: true },
|
||||
]
|
||||
|
||||
export default elements
|
||||
@@ -0,0 +1,15 @@
|
||||
.layoutflow {
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.layoutflow .controls {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.controls button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<script lang="ts" setup>
|
||||
import { VueFlow, Background, Elements } from '@braks/vue-flow'
|
||||
const initialElements: Elements = [
|
||||
{ id: '1', type: 'input', label: 'Node 1', position: { x: 250, y: 5 }, class: 'light' },
|
||||
{ id: '2', label: 'Node 2', position: { x: 100, y: 100 }, class: 'light' },
|
||||
{ id: '3', label: 'Node 3', position: { x: 400, y: 100 }, class: 'light' },
|
||||
{ id: '4', label: 'Node 4', position: { x: 400, y: 200 }, class: 'light' },
|
||||
{ id: 'e1-2', source: '1', target: '2', animated: true },
|
||||
{ id: 'e1-3', source: '1', target: '3' },
|
||||
]
|
||||
|
||||
const elements = ref<Elements>(initialElements)
|
||||
</script>
|
||||
<template>
|
||||
<VueFlow v-model="elements" :fit-view-on-init="true">
|
||||
<Background />
|
||||
</VueFlow>
|
||||
</template>
|
||||
@@ -0,0 +1,12 @@
|
||||
<script lang="ts" setup>
|
||||
import Flow from './Flow.vue'
|
||||
</script>
|
||||
<template>
|
||||
<div class="vue-flow__example-multiflows">
|
||||
<Flow />
|
||||
<Flow />
|
||||
</div>
|
||||
</template>
|
||||
<style>
|
||||
@import 'multiflows.css';
|
||||
</style>
|
||||
@@ -0,0 +1,13 @@
|
||||
.vue-flow__example-multiflows {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.vue-flow__example-multiflows .vue-flow {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.vue-flow__example-multiflows .vue-flow:first-child {
|
||||
border-right: 2px solid #333;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<script lang="ts" setup>
|
||||
import { Handle, Position, getNodesInside, useVueFlow } from '@braks/vue-flow'
|
||||
import type { NodeProps } from '@braks/vue-flow'
|
||||
|
||||
const props = defineProps<NodeProps>()
|
||||
const { onNodeDragStop, getNodes, transform } = useVueFlow()
|
||||
onNodeDragStop(({ node }) => {
|
||||
const nodes = getNodesInside(
|
||||
getNodes.value,
|
||||
{
|
||||
...props.dimensions,
|
||||
x: props.computedPosition.x,
|
||||
y: props.computedPosition.y,
|
||||
},
|
||||
transform.value,
|
||||
)
|
||||
if (nodes.some((n) => n.id === node.id && n.id !== props.id)) {
|
||||
node.label = `In ${props.id}`
|
||||
node.data = {
|
||||
group: props.id,
|
||||
}
|
||||
} else if (node.data?.group === props.id) {
|
||||
node.data.group = undefined
|
||||
node.label = node.id
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div class="vue-flow__group-node">
|
||||
<Handle type="target" :position="Position.Top" />
|
||||
<strong>Group {{ label }}</strong>
|
||||
<Handle type="source" :position="Position.Bottom" />
|
||||
</div>
|
||||
</template>
|
||||
<style>
|
||||
.vue-flow__group-node {
|
||||
padding: 15px;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
border: solid 1px black;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,96 @@
|
||||
<script lang="ts" setup>
|
||||
import { ConnectionMode, useVueFlow, VueFlow, MiniMap, Background, Controls } from '@braks/vue-flow'
|
||||
|
||||
const { onConnect, nodes, edges, addEdges, addNodes } = useVueFlow({
|
||||
fitViewOnInit: true,
|
||||
connectionMode: ConnectionMode.Loose,
|
||||
nodes: [
|
||||
{ id: '1', type: 'input', label: 'Node 1', position: { x: 250, y: 5 }, class: 'light' },
|
||||
{
|
||||
id: '2',
|
||||
label: 'Node 2',
|
||||
position: { x: 100, y: 100 },
|
||||
class: 'light',
|
||||
style: { backgroundColor: 'rgba(255, 0, 0, 0.8)', width: '200px', height: '200px' },
|
||||
},
|
||||
{
|
||||
id: '2a',
|
||||
label: 'Node 2a',
|
||||
position: { x: 10, y: 50 },
|
||||
parentNode: '2',
|
||||
},
|
||||
{ id: '3', label: 'Node 3', position: { x: 320, y: 100 }, class: 'light' },
|
||||
{
|
||||
id: '4',
|
||||
label: 'Node 4',
|
||||
position: { x: 320, y: 200 },
|
||||
class: 'light',
|
||||
style: { backgroundColor: 'rgba(255, 0, 0, 0.7)', width: '300px', height: '300px' },
|
||||
},
|
||||
{
|
||||
id: '4a',
|
||||
label: 'Node 4a',
|
||||
position: { x: 15, y: 65 },
|
||||
class: 'light',
|
||||
extent: 'parent',
|
||||
parentNode: '4',
|
||||
},
|
||||
{
|
||||
id: '4b',
|
||||
label: 'Node 4b',
|
||||
position: { x: 15, y: 120 },
|
||||
class: 'light',
|
||||
style: { backgroundColor: 'rgba(255, 0, 255, 0.7)', height: '150px', width: '270px' },
|
||||
parentNode: '4',
|
||||
},
|
||||
{
|
||||
id: '4b1',
|
||||
label: 'Node 4b1',
|
||||
position: { x: 20, y: 40 },
|
||||
class: 'light',
|
||||
parentNode: '4b',
|
||||
},
|
||||
{
|
||||
id: '4b2',
|
||||
label: 'Node 4b2',
|
||||
position: { x: 100, y: 100 },
|
||||
class: 'light',
|
||||
parentNode: '4b',
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{ id: 'e1-2', source: '1', target: '2', animated: true },
|
||||
{ id: 'e1-3', source: '1', target: '3' },
|
||||
{ id: 'e2a-4a', source: '2a', target: '4a' },
|
||||
{ id: 'e3-4', source: '3', target: '4' },
|
||||
{ id: 'e3-4b', source: '3', target: '4b' },
|
||||
{ id: 'e4a-4b1', source: '4a', target: '4b1' },
|
||||
{ id: 'e4a-4b2', source: '4a', target: '4b2' },
|
||||
{ id: 'e4b1-4b2', source: '4b1', target: '4b2' },
|
||||
],
|
||||
})
|
||||
|
||||
onConnect((params) => addEdges([params]))
|
||||
|
||||
onMounted(() => {
|
||||
// add nodes to parent
|
||||
addNodes([
|
||||
{
|
||||
id: '999',
|
||||
type: 'input',
|
||||
label: 'Added after mount',
|
||||
position: { x: 20, y: 100 },
|
||||
class: 'light',
|
||||
expandParent: true,
|
||||
parentNode: '2',
|
||||
},
|
||||
])
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<VueFlow>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
<Background />
|
||||
</VueFlow>
|
||||
</template>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script lang="ts" setup>
|
||||
import { VueFlow, addEdge, Connection, Edge, Elements, isEdge, FlowInstance, Position } from '@braks/vue-flow'
|
||||
|
||||
const initialElements: Elements = [
|
||||
{
|
||||
id: '1',
|
||||
sourcePosition: Position.Right,
|
||||
type: 'input',
|
||||
label: 'Input',
|
||||
position: { x: 0, y: 80 },
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'output',
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Left,
|
||||
label: 'A Node',
|
||||
position: { x: 250, y: 0 },
|
||||
},
|
||||
{ id: 'e1-2', source: '1', type: 'smoothstep', target: '2', animated: true },
|
||||
]
|
||||
|
||||
const elements = ref<Elements>(initialElements)
|
||||
|
||||
const onConnect = (params: Connection | Edge) => (elements.value = addEdge(params, elements.value))
|
||||
|
||||
const onLoad = (flowInstance: FlowInstance) => flowInstance.fitView()
|
||||
|
||||
const changeType = () => {
|
||||
elements.value.forEach((el) => {
|
||||
if (isEdge(el) || el.type === 'input') return
|
||||
el.type = el.type === 'default' ? 'output' : 'default'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<VueFlow v-model="elements" @connect="onConnect" @pane-ready="onLoad">
|
||||
<button :style="{ position: 'absolute', right: 10, top: 30, zIndex: 4 }" @click="changeType">change type</button>
|
||||
</VueFlow>
|
||||
</template>
|
||||
@@ -0,0 +1,148 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
VueFlow,
|
||||
addEdge,
|
||||
MiniMap,
|
||||
Controls,
|
||||
Background,
|
||||
Node,
|
||||
Elements,
|
||||
FlowInstance,
|
||||
FlowTransform,
|
||||
SnapGrid,
|
||||
Connection,
|
||||
Edge,
|
||||
FlowEvents,
|
||||
MarkerType,
|
||||
} from '@braks/vue-flow'
|
||||
|
||||
const onNodeDragStart = (e: FlowEvents['nodeDragStart']) => console.log('drag start', e)
|
||||
const onNodeDrag = (e: FlowEvents['nodeDrag']) => console.log('drag', e)
|
||||
const onNodeDragStop = (e: FlowEvents['nodeDragStop']) => console.log('drag stop', e)
|
||||
const onNodeDoubleClick = (e: FlowEvents['nodeDoubleClick']) => console.log('node double click', e)
|
||||
const onPaneClick = (e: FlowEvents['paneClick']) => console.log('pane click', e)
|
||||
const onPaneScroll = (e: FlowEvents['paneScroll']) => console.log('pane scroll', e)
|
||||
const onPaneContextMenu = (e: FlowEvents['paneContextMenu']) => console.log('pane context menu', e)
|
||||
const onSelectionDrag = (e: FlowEvents['selectionDrag']) => console.log('selection drag', e)
|
||||
const onSelectionDragStart = (e: FlowEvents['selectionDragStart']) => console.log('selection drag start', e)
|
||||
const onSelectionDragStop = (e: FlowEvents['selectionDragStop']) => console.log('selection drag stop', e)
|
||||
const onSelectionContextMenu = (e: FlowEvents['selectionContextMenu']) => console.log('selection context menu', e)
|
||||
const onLoad = (flowInstance: FlowInstance) => {
|
||||
console.log('flow loaded:', flowInstance)
|
||||
flowInstance.fitView()
|
||||
}
|
||||
|
||||
const onMoveEnd = (transform?: FlowTransform) => console.log('zoom/move end', transform)
|
||||
const onEdgeContextMenu = (e: FlowEvents['edgeContextMenu']) => console.log('edge context menu', e)
|
||||
const onEdgeMouseEnter = (e: FlowEvents['edgeMouseEnter']) => console.log('edge mouse enter', e)
|
||||
const onEdgeMouseMove = (e: FlowEvents['edgeMouseMove']) => console.log('edge mouse move', e)
|
||||
const onEdgeMouseLeave = (e: FlowEvents['edgeMouseLeave']) => console.log('edge mouse leave', e)
|
||||
const onEdgeDoubleClick = (e: FlowEvents['edgeDoubleClick']) => console.log('edge double click', e)
|
||||
|
||||
const initialElements: Elements = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'input',
|
||||
label: 'Welcome to <strong>Vue VueFlow!</strong>',
|
||||
position: { x: 250, y: 0 },
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
label: 'This is a <strong>default node</strong>',
|
||||
position: { x: 100, y: 100 },
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
label: 'This one has a <strong>custom style</strong>',
|
||||
position: { x: 400, y: 100 },
|
||||
style: { background: '#D6D5E6', color: '#333', border: '1px solid #222138', width: 180 },
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
position: { x: 250, y: 200 },
|
||||
label: `You can find the docs on
|
||||
<a href="https://github.com/bcakmakoglu/vue-flow" target="_blank" rel="noopener noreferrer">
|
||||
Github
|
||||
</a>`,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
label: 'Or check out the other <strong>examples</strong>',
|
||||
position: { x: 250, y: 325 },
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
type: 'output',
|
||||
label: 'An <strong>output node</strong>',
|
||||
position: { x: 100, y: 480 },
|
||||
},
|
||||
{ id: '7', type: 'output', label: 'Another output node', position: { x: 400, y: 450 } },
|
||||
{ id: 'e1-2', source: '1', target: '2', label: 'this is an edge label' },
|
||||
{ id: 'e1-3', source: '1', target: '3' },
|
||||
{ id: 'e3-4', source: '3', target: '4', animated: true, label: 'animated edge' },
|
||||
{ id: 'e4-5', source: '4', target: '5', markerEnd: MarkerType.Arrow, label: 'edge with arrow head' },
|
||||
{ id: 'e5-6', source: '5', target: '6', type: 'smoothstep', label: 'smooth step edge' },
|
||||
{
|
||||
id: 'e5-7',
|
||||
source: '5',
|
||||
target: '7',
|
||||
type: 'step',
|
||||
style: { stroke: '#f6ab6c' },
|
||||
label: 'a step edge',
|
||||
animated: true,
|
||||
labelStyle: { fill: '#f6ab6c', fontWeight: 700 },
|
||||
},
|
||||
]
|
||||
|
||||
const snapGrid: SnapGrid = [16, 16]
|
||||
|
||||
const nodeStrokeColor = (n: Node): string => {
|
||||
if (n.style?.background) return n.style.background as string
|
||||
if (n.type === 'input') return '#0041d0'
|
||||
if (n.type === 'output') return '#ff0072'
|
||||
if (n.type === 'default') return '#1a192b'
|
||||
|
||||
return '#eee'
|
||||
}
|
||||
|
||||
const nodeColor = (n: Node): string => {
|
||||
if (n.style?.background) return n.style.background as string
|
||||
|
||||
return '#fff'
|
||||
}
|
||||
|
||||
const elements = ref<Elements>(initialElements)
|
||||
const onConnect = (params: Connection | Edge) => (elements.value = addEdge(params, elements.value))
|
||||
</script>
|
||||
<template>
|
||||
<VueFlow
|
||||
v-model="elements"
|
||||
:connection-line-style="{ stroke: '#ddd' }"
|
||||
:snap-to-grid="true"
|
||||
:snap-grid="snapGrid"
|
||||
@connect="onConnect"
|
||||
@pane-ready="onLoad"
|
||||
@pane-click="onPaneClick"
|
||||
@pane-scroll="onPaneScroll"
|
||||
@pane-contex-menu="onPaneContextMenu"
|
||||
@node-drag-start="onNodeDragStart"
|
||||
@node-drag="onNodeDrag"
|
||||
@node-drag-stop="onNodeDragStop"
|
||||
@node-double-click="onNodeDoubleClick"
|
||||
@selection-drag-start="onSelectionDragStart"
|
||||
@selection-drag="onSelectionDrag"
|
||||
@selection-drag-stop="onSelectionDragStop"
|
||||
@selection-context-menu="onSelectionContextMenu"
|
||||
@move-end="onMoveEnd"
|
||||
@edge-update="onEdgeMouseMove"
|
||||
@edge-context-menu="onEdgeContextMenu"
|
||||
@edge-mouse-enter="onEdgeMouseEnter"
|
||||
@edge-mouse-move="onEdgeMouseMove"
|
||||
@edge-mouse-leave="onEdgeMouseLeave"
|
||||
@edge-double-click="onEdgeDoubleClick"
|
||||
>
|
||||
<MiniMap :node-stroke-color="nodeStrokeColor" :node-color="nodeColor" :node-border-radius="2" />
|
||||
<Controls />
|
||||
<Background color="#aaa" :gap="20" />
|
||||
</VueFlow>
|
||||
</template>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts" setup>
|
||||
import Sidebar from './Sidebar.vue'
|
||||
import { VueFlow, Controls, FlowInstance, Elements, ConnectionMode, useVueFlow } from '@braks/vue-flow'
|
||||
|
||||
const onLoad = (flowInstance: FlowInstance) => console.log('flow loaded:', flowInstance)
|
||||
|
||||
const initialElements: Elements = [
|
||||
{ id: '1', type: 'input', label: 'Node 1', position: { x: 250, y: 5 } },
|
||||
{ id: '2', label: 'Node 2', position: { x: 100, y: 100 } },
|
||||
{ id: '3', label: 'Node 3', position: { x: 400, y: 100 } },
|
||||
{ id: '4', label: 'Node 4', position: { x: 400, y: 200 } },
|
||||
{ id: 'e1-2', source: '1', target: '2', animated: true },
|
||||
{ id: 'e1-3', source: '1', target: '3' },
|
||||
]
|
||||
|
||||
useVueFlow()
|
||||
const elements = ref<Elements>(initialElements)
|
||||
</script>
|
||||
<template>
|
||||
<div class="providerflow">
|
||||
<Sidebar />
|
||||
<div class="vue-flow-wrapper">
|
||||
<VueFlow v-model="elements" :connection-mode="ConnectionMode.Loose" @pane-ready="onLoad">
|
||||
<Controls />
|
||||
</VueFlow>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style>
|
||||
@import 'provider.css';
|
||||
</style>
|
||||
@@ -0,0 +1,28 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVueFlow } from '@braks/vue-flow'
|
||||
const { nodesSelectionActive, addSelectedNodes, getNodes, transform } = useVueFlow()
|
||||
|
||||
const selectAll = () => {
|
||||
addSelectedNodes(getNodes.value)
|
||||
nodesSelectionActive.value = true
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<aside>
|
||||
<div class="description">
|
||||
This is an example of how you can access the internal state outside of the Vue VueFlow component.
|
||||
</div>
|
||||
<div class="title">Zoom & pan transform</div>
|
||||
<div class="transform">
|
||||
{{ [transform[0].toFixed(2), transform[1].toFixed(2), transform[2].toFixed(2)] }}
|
||||
</div>
|
||||
<div class="title">Nodes</div>
|
||||
<div v-for="node of getNodes" :key="node.id">
|
||||
Node {{ node.id }} - x: {{ node.position.x.toFixed(2) }}, y: {{ node.position.y.toFixed(2) }}
|
||||
</div>
|
||||
|
||||
<div class="selectall">
|
||||
<button @click="selectAll">select all nodes</button>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
@@ -0,0 +1,45 @@
|
||||
.providerflow {
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.providerflow aside {
|
||||
border-right: 1px solid #eee;
|
||||
padding: 15px 10px;
|
||||
font-size: 12px;
|
||||
background: #fcfcfc;
|
||||
}
|
||||
|
||||
.providerflow aside .description {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.providerflow aside .title {
|
||||
font-weight: 700;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.providerflow aside .transform {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.providerflow .vue-flow-wrapper {
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.providerflow .selectall {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.providerflow {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.providerflow aside {
|
||||
width: 20%;
|
||||
max-width: 250px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<script lang="ts" setup>
|
||||
import { templateRef } from '@vueuse/core'
|
||||
import RGBNode from './RGBNode.vue'
|
||||
import RGBOutputNode from './RGBOutputNode.vue'
|
||||
import { Elements, FlowInstance, VueFlow } from '@braks/vue-flow'
|
||||
|
||||
type Colors = {
|
||||
red: number
|
||||
green: number
|
||||
blue: number
|
||||
}
|
||||
const elements = ref<Elements>([
|
||||
{ id: '1', type: 'rgb', data: { color: 'r' }, position: { x: -25, y: 50 } },
|
||||
{ id: '2', type: 'rgb', data: { color: 'g' }, position: { x: 50, y: -100 } },
|
||||
{ id: '3', type: 'rgb', data: { color: 'b' }, position: { x: 0, y: 200 } },
|
||||
{ id: '4', type: 'rgb-output', data: { label: 'RGB' }, position: { x: 400, y: 50 } },
|
||||
{ id: 'e1-4', data: { color: 'red' }, source: '1', target: '4', animated: true },
|
||||
{ id: 'e2-4', data: { color: 'green' }, source: '2', target: '4', animated: true },
|
||||
{ id: 'e3-4', data: { color: 'blue' }, source: '3', target: '4', animated: true },
|
||||
])
|
||||
|
||||
const el = templateRef<HTMLDivElement>('page', null)
|
||||
|
||||
const onLoad = (flowInstance: FlowInstance) => {
|
||||
flowInstance.fitView({ padding: 1 })
|
||||
}
|
||||
const color = ref<Colors>({
|
||||
red: 100,
|
||||
green: 150,
|
||||
blue: 100,
|
||||
})
|
||||
const onChange = ({ color: c, val }: { color: keyof Colors; val: number }) => (color.value[c] = Number(val))
|
||||
</script>
|
||||
<template>
|
||||
<div ref="page" class="demo-flow">
|
||||
<VueFlow v-model="elements" @pane-ready="onLoad">
|
||||
<template #node-rgb="props">
|
||||
<RGBNode v-bind="props" :amount="color" @change="onChange" />
|
||||
</template>
|
||||
<template #node-rgb-output="props">
|
||||
<RGBOutputNode v-bind="props" :rgb="`rgb(${color.red}, ${color.green}, ${color.blue})`" />
|
||||
</template>
|
||||
</VueFlow>
|
||||
</div>
|
||||
</template>
|
||||
<style>
|
||||
.demo-flow {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 80vh;
|
||||
width: 100%;
|
||||
border-radius: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,74 @@
|
||||
<script lang="ts" setup>
|
||||
import { CSSProperties } from 'vue'
|
||||
import { Handle, NodeProps, Position } from '@braks/vue-flow'
|
||||
|
||||
interface RGBNodeProps extends NodeProps {
|
||||
data: {
|
||||
color: 'r' | 'g' | 'b'
|
||||
}
|
||||
amount: {
|
||||
red: number
|
||||
green: number
|
||||
blue: number
|
||||
}
|
||||
}
|
||||
|
||||
const props = defineProps<RGBNodeProps>()
|
||||
const emit = defineEmits(['change'])
|
||||
let color = 'red'
|
||||
switch (props.data.color) {
|
||||
case 'r':
|
||||
color = 'red'
|
||||
break
|
||||
case 'g':
|
||||
color = 'green'
|
||||
break
|
||||
case 'b':
|
||||
color = 'blue'
|
||||
break
|
||||
}
|
||||
|
||||
const colorVal = computed({
|
||||
get: () => props.amount[color as 'red' | 'green' | 'blue'],
|
||||
set: (val) => {
|
||||
emit('change', { color, val })
|
||||
},
|
||||
})
|
||||
|
||||
const style = { '--color': color } as CSSProperties
|
||||
</script>
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<div class="text-md" :style="{ color }">{{ `${color} Amount`.toUpperCase() }}</div>
|
||||
<input v-model="colorVal" class="slider nodrag" :style="style" type="range" min="0" max="255" />
|
||||
<Handle type="source" :position="Position.Right" :style="{ backgroundColor: color }" />
|
||||
</div>
|
||||
</template>
|
||||
<style>
|
||||
.wrapper {
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
border: 2px solid black;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.slider {
|
||||
--color: red;
|
||||
margin-top: 12px;
|
||||
background: gainsboro;
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
outline: none;
|
||||
border-radius: 999px;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
|
||||
&::-moz-range-thumb,
|
||||
&::-webkit-slider-thumb {
|
||||
@apply w-[15px] h-[15px] cursor-pointer border-1 border-solid border-white rounded-full;
|
||||
-webkit-appearance: none;
|
||||
background: var(--color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts" setup>
|
||||
import { Handle, NodeProps, Position } from '@braks/vue-flow'
|
||||
|
||||
interface RBGOutputNodeProps extends NodeProps {
|
||||
rgb: string
|
||||
}
|
||||
|
||||
const props = defineProps<RBGOutputNodeProps>()
|
||||
</script>
|
||||
<template>
|
||||
<div :style="{ backgroundColor: props.rgb }" class="rgb-output-node">
|
||||
<div class="text-md uppercase">{{ props.rgb }}</div>
|
||||
<Handle type="target" :position="Position.Left" />
|
||||
</div>
|
||||
</template>
|
||||
<style>
|
||||
.rgb-output-node {
|
||||
padding: 9px;
|
||||
border-radius: 25px;
|
||||
text-align: left;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,47 @@
|
||||
<script lang="ts" setup>
|
||||
import { useZoomPanHelper, FlowExportObject, Node, useVueFlow } from '@braks/vue-flow'
|
||||
|
||||
const flowKey = 'example-flow'
|
||||
const state = useStorage<FlowExportObject>(flowKey, {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
position: [NaN, NaN],
|
||||
zoom: 1,
|
||||
})
|
||||
|
||||
const getNodeId = () => `randomnode_${+new Date()}`
|
||||
|
||||
const { setTransform } = useZoomPanHelper()
|
||||
const { nodes, edges, addNodes, setNodes, setEdges, instance, dimensions } = useVueFlow()
|
||||
|
||||
const onSave = () => {
|
||||
state.value = instance.value?.toObject()
|
||||
}
|
||||
|
||||
const onRestore = () => {
|
||||
const flow: FlowExportObject | null = state.value
|
||||
|
||||
if (flow) {
|
||||
const [x = 0, y = 0] = flow.position
|
||||
setNodes(state.value.nodes)
|
||||
setEdges(state.value.edges)
|
||||
setTransform({ x, y, zoom: flow.zoom || 0 })
|
||||
}
|
||||
}
|
||||
|
||||
const onAdd = () => {
|
||||
const newNode = {
|
||||
id: `random_node-${getNodeId()}`,
|
||||
label: 'Added node',
|
||||
position: { x: Math.random() * dimensions.value.width, y: Math.random() * dimensions.value.height },
|
||||
} as Node
|
||||
addNodes([newNode])
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="save__controls">
|
||||
<button @click="onSave">save</button>
|
||||
<button @click="onRestore">restore</button>
|
||||
<button @click="onAdd">add node</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts" setup>
|
||||
import Controls from './Controls.vue'
|
||||
import { VueFlow, Elements } from '@braks/vue-flow'
|
||||
|
||||
const initialElements: Elements = [
|
||||
{ id: '1', label: 'Node 1', position: { x: 100, y: 100 } },
|
||||
{ id: '2', label: 'Node 2', position: { x: 100, y: 200 } },
|
||||
{ id: 'e1-2', source: '1', target: '2' },
|
||||
]
|
||||
|
||||
const elements = ref(initialElements)
|
||||
</script>
|
||||
<template>
|
||||
<VueFlow v-model="elements">
|
||||
<Controls />
|
||||
</VueFlow>
|
||||
</template>
|
||||
<style>
|
||||
@import 'save.css';
|
||||
</style>
|
||||
@@ -0,0 +1,11 @@
|
||||
.save__controls {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
z-index: 4;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.save__controls button {
|
||||
margin-left: 5px;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<script lang="ts" setup>
|
||||
import { getElements } from './utils'
|
||||
import { VueFlow, FlowInstance } from '@braks/vue-flow'
|
||||
|
||||
const instance = ref<FlowInstance>()
|
||||
const onLoad = (flowInstance: FlowInstance) => {
|
||||
flowInstance.fitView()
|
||||
instance.value = flowInstance
|
||||
console.log(flowInstance.getNodes())
|
||||
}
|
||||
|
||||
const { nodes, edges } = getElements(10, 10)
|
||||
</script>
|
||||
<template>
|
||||
<VueFlow :nodes="nodes" :edges="edges" @pane-ready="onLoad"> </VueFlow>
|
||||
</template>
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Edge, Node } from '@braks/vue-flow'
|
||||
|
||||
export function getElements(xElements = 10, yElements = 10) {
|
||||
const initialNodes: Node[] = []
|
||||
const initialEdges: Edge[] = []
|
||||
let nodeId = 1
|
||||
let recentNodeId = null
|
||||
|
||||
for (let y = 0; y < yElements; y++) {
|
||||
for (let x = 0; x < xElements; x++) {
|
||||
const position = { x: x * 100, y: y * 50 }
|
||||
const node = {
|
||||
id: nodeId.toString(),
|
||||
style: { width: 50, fontSize: 11 },
|
||||
label: `Node ${nodeId}`,
|
||||
position,
|
||||
}
|
||||
initialNodes.push(node)
|
||||
|
||||
if (recentNodeId && nodeId <= xElements * yElements) {
|
||||
initialEdges.push({ id: `${x}-${y}`, source: recentNodeId.toString(), target: nodeId.toString() })
|
||||
}
|
||||
|
||||
recentNodeId = nodeId
|
||||
nodeId++
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
nodes: initialNodes,
|
||||
edges: initialEdges,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<script lang="ts" setup>
|
||||
import { VueFlow, Elements } from '@braks/vue-flow'
|
||||
|
||||
const elementsA: Elements = [
|
||||
{ id: '1a', type: 'input', label: 'Node 1', position: { x: 250, y: 5 }, class: 'light' },
|
||||
{ id: '2a', label: 'Node 2', position: { x: 100, y: 100 }, class: 'light' },
|
||||
{ id: '3a', label: 'Node 3', position: { x: 400, y: 100 }, class: 'light' },
|
||||
{ id: '4a', label: 'Node 4', position: { x: 400, y: 200 }, class: 'light' },
|
||||
{ id: 'e1-2', source: '1a', target: '2a' },
|
||||
{ id: 'e1-3', source: '1a', target: '3a' },
|
||||
]
|
||||
|
||||
const elementsB: Elements = [
|
||||
{ id: 'inputb', type: 'input', label: 'Input', position: { x: 300, y: 5 }, class: 'light' },
|
||||
{ id: '1b', label: 'Node 1', position: { x: 0, y: 100 }, class: 'light' },
|
||||
{ id: '2b', label: 'Node 2', position: { x: 200, y: 100 }, class: 'light' },
|
||||
{ id: '3b', label: 'Node 3', position: { x: 400, y: 100 }, class: 'light' },
|
||||
{ id: '4b', label: 'Node 4', position: { x: 600, y: 100 }, class: 'light' },
|
||||
|
||||
{ id: 'e1b', source: 'inputb', target: '1b' },
|
||||
{ id: 'e2b', source: 'inputb', target: '2b' },
|
||||
{ id: 'e3b', source: 'inputb', target: '3b' },
|
||||
{ id: 'e4b', source: 'inputb', target: '4b' },
|
||||
]
|
||||
|
||||
const elements = ref(elementsA)
|
||||
</script>
|
||||
<template>
|
||||
<VueFlow v-model="elements">
|
||||
<div :style="{ position: 'absolute', right: 10, top: 10, zIndex: 4 }">
|
||||
<button style="margin-right: 5px" @click="() => (elements = elementsA)">flow a</button>
|
||||
<button @click="() => (elements = elementsB)">flow b</button>
|
||||
</div>
|
||||
</VueFlow>
|
||||
</template>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts" setup>
|
||||
import { CSSProperties } from 'vue'
|
||||
import { Handle, Position } from '@braks/vue-flow'
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const nodeStyles: CSSProperties = { padding: '10px 15px', border: '1px solid #ddd' }
|
||||
</script>
|
||||
<template>
|
||||
<div :key="props.id" :style="nodeStyles">
|
||||
<div>node {{ props.id }}</div>
|
||||
<Handle id="left" type="source" :position="Position.Left" />
|
||||
<Handle id="right" type="source" :position="Position.Right" />
|
||||
<Handle id="top" type="source" :position="Position.Top" />
|
||||
<Handle id="bottom" type="source" :position="Position.Bottom" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,186 @@
|
||||
<script lang="ts" setup>
|
||||
import CustomNode from './CustomNode.vue'
|
||||
import { VueFlow, useZoomPanHelper, Elements, Node, ConnectionLineType, ConnectionMode, MarkerType } from '@braks/vue-flow'
|
||||
|
||||
const initialElements: Elements = [
|
||||
{
|
||||
id: '00',
|
||||
type: 'custom',
|
||||
position: { x: 300, y: 250 },
|
||||
},
|
||||
{
|
||||
id: '01',
|
||||
type: 'custom',
|
||||
position: { x: 100, y: 50 },
|
||||
},
|
||||
{
|
||||
id: '02',
|
||||
type: 'custom',
|
||||
position: { x: 500, y: 50 },
|
||||
},
|
||||
{
|
||||
id: '03',
|
||||
type: 'custom',
|
||||
position: { x: 500, y: 500 },
|
||||
},
|
||||
{
|
||||
id: '04',
|
||||
type: 'custom',
|
||||
position: { x: 100, y: 500 },
|
||||
},
|
||||
{
|
||||
id: '10',
|
||||
type: 'custom',
|
||||
position: { x: 300, y: 5 },
|
||||
},
|
||||
{
|
||||
id: '20',
|
||||
type: 'custom',
|
||||
position: { x: 600, y: 250 },
|
||||
},
|
||||
{
|
||||
id: '30',
|
||||
type: 'custom',
|
||||
position: { x: 300, y: 600 },
|
||||
},
|
||||
{
|
||||
id: '40',
|
||||
type: 'custom',
|
||||
position: { x: 5, y: 250 },
|
||||
},
|
||||
{
|
||||
id: 'e0-1a',
|
||||
source: '00',
|
||||
target: '01',
|
||||
sourceHandle: 'left',
|
||||
targetHandle: 'bottom',
|
||||
type: 'step',
|
||||
markerEnd: MarkerType.Arrow,
|
||||
},
|
||||
{
|
||||
id: 'e0-1b',
|
||||
source: '00',
|
||||
target: '01',
|
||||
sourceHandle: 'top',
|
||||
targetHandle: 'right',
|
||||
type: 'step',
|
||||
markerEnd: MarkerType.Arrow,
|
||||
},
|
||||
{
|
||||
id: 'e0-2a',
|
||||
source: '00',
|
||||
target: '02',
|
||||
sourceHandle: 'top',
|
||||
targetHandle: 'left',
|
||||
type: 'step',
|
||||
markerEnd: MarkerType.Arrow,
|
||||
},
|
||||
{
|
||||
id: 'e0-2b',
|
||||
source: '00',
|
||||
target: '02',
|
||||
sourceHandle: 'right',
|
||||
targetHandle: 'bottom',
|
||||
type: 'step',
|
||||
markerEnd: MarkerType.Arrow,
|
||||
},
|
||||
{
|
||||
id: 'e0-3a',
|
||||
source: '00',
|
||||
target: '03',
|
||||
sourceHandle: 'right',
|
||||
targetHandle: 'top',
|
||||
type: 'step',
|
||||
markerEnd: MarkerType.Arrow,
|
||||
},
|
||||
{
|
||||
id: 'e0-3b',
|
||||
source: '00',
|
||||
target: '03',
|
||||
sourceHandle: 'bottom',
|
||||
targetHandle: 'left',
|
||||
type: 'step',
|
||||
markerEnd: MarkerType.Arrow,
|
||||
},
|
||||
{
|
||||
id: 'e0-4a',
|
||||
source: '00',
|
||||
target: '04',
|
||||
sourceHandle: 'bottom',
|
||||
targetHandle: 'right',
|
||||
type: 'step',
|
||||
markerEnd: MarkerType.Arrow,
|
||||
},
|
||||
{
|
||||
id: 'e0-4b',
|
||||
source: '00',
|
||||
target: '04',
|
||||
sourceHandle: 'left',
|
||||
targetHandle: 'top',
|
||||
type: 'step',
|
||||
markerEnd: MarkerType.Arrow,
|
||||
},
|
||||
{
|
||||
id: 'e0-10',
|
||||
source: '00',
|
||||
target: '10',
|
||||
sourceHandle: 'top',
|
||||
targetHandle: 'bottom',
|
||||
type: 'step',
|
||||
markerEnd: MarkerType.Arrow,
|
||||
},
|
||||
{
|
||||
id: 'e0-20',
|
||||
source: '00',
|
||||
target: '20',
|
||||
sourceHandle: 'right',
|
||||
targetHandle: 'left',
|
||||
type: 'step',
|
||||
markerEnd: MarkerType.Arrow,
|
||||
},
|
||||
{
|
||||
id: 'e0-30',
|
||||
source: '00',
|
||||
target: '30',
|
||||
sourceHandle: 'bottom',
|
||||
targetHandle: 'top',
|
||||
type: 'step',
|
||||
markerEnd: MarkerType.Arrow,
|
||||
},
|
||||
{
|
||||
id: 'e0-40',
|
||||
source: '00',
|
||||
target: '40',
|
||||
sourceHandle: 'left',
|
||||
targetHandle: 'right',
|
||||
type: 'step',
|
||||
markerEnd: MarkerType.Arrow,
|
||||
},
|
||||
]
|
||||
|
||||
let id = 4
|
||||
const getId = () => `${id++}`
|
||||
|
||||
const elements = ref(initialElements)
|
||||
const { project } = useZoomPanHelper()
|
||||
|
||||
const onPaneClick = (evt: MouseEvent) =>
|
||||
(elements.value = elements.value.concat({
|
||||
id: getId(),
|
||||
position: project({ x: evt.clientX, y: evt.clientY - 40 }),
|
||||
type: 'custom',
|
||||
} as Node))
|
||||
</script>
|
||||
<template>
|
||||
<VueFlow
|
||||
v-model="elements"
|
||||
:connection-line-type="ConnectionLineType.SmoothStep"
|
||||
:connection-mode="ConnectionMode.Loose"
|
||||
@pane-click="onPaneClick"
|
||||
@pane-ready="({ fitView }) => fitView()"
|
||||
>
|
||||
<template #node-custom="props">
|
||||
<CustomNode v-bind="props" />
|
||||
</template>
|
||||
</VueFlow>
|
||||
</template>
|
||||
@@ -0,0 +1,58 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
VueFlow,
|
||||
Controls,
|
||||
updateEdge,
|
||||
addEdge,
|
||||
Elements,
|
||||
FlowInstance,
|
||||
Connection,
|
||||
Edge,
|
||||
FlowEvents,
|
||||
ConnectionMode,
|
||||
} from '@braks/vue-flow'
|
||||
|
||||
const initialElements: Elements = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'input',
|
||||
label: 'Node <strong>A</strong>',
|
||||
position: { x: 250, y: 0 },
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
label: 'Node <strong>B</strong>',
|
||||
position: { x: 100, y: 100 },
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
label: 'Node <strong>C</strong>',
|
||||
position: { x: 400, y: 100 },
|
||||
style: { background: '#D6D5E6', color: '#333', border: '1px solid #222138', width: 180 },
|
||||
},
|
||||
{ id: 'e1-2', source: '1', target: '2', label: 'Updateable edge', updatable: true },
|
||||
]
|
||||
|
||||
const elements = ref(initialElements)
|
||||
const onLoad = (flowInstance: FlowInstance) => flowInstance.fitView()
|
||||
const onEdgeUpdateStart = (edge: Edge) => console.log('start update', edge)
|
||||
const onEdgeUpdateEnd = (edge: Edge) => console.log('end update', edge)
|
||||
const onEdgeUpdate = ({ edge, connection }: FlowEvents['edgeUpdate']) => {
|
||||
elements.value = updateEdge(edge, connection, elements.value)
|
||||
}
|
||||
const onConnect = (params: Connection | Edge) => (elements.value = addEdge(params, elements.value))
|
||||
</script>
|
||||
<template>
|
||||
<VueFlow
|
||||
v-model="elements"
|
||||
:snap-to-grid="true"
|
||||
:connection-mode="ConnectionMode.Loose"
|
||||
@pane-ready="onLoad"
|
||||
@edge-update="onEdgeUpdate"
|
||||
@connect="onConnect"
|
||||
@edge-update-start="onEdgeUpdateStart"
|
||||
@edge-update-end="onEdgeUpdateEnd"
|
||||
>
|
||||
<Controls />
|
||||
</VueFlow>
|
||||
</template>
|
||||
@@ -0,0 +1,48 @@
|
||||
<script lang="ts" setup>
|
||||
import { VueFlow, Elements } from '@braks/vue-flow'
|
||||
|
||||
const initialElements: Elements = [
|
||||
{ id: '1', label: '-', position: { x: 100, y: 100 } },
|
||||
{ id: '2', label: 'Node 2', position: { x: 100, y: 200 } },
|
||||
{ id: 'e1-2', source: '1', target: '2' },
|
||||
]
|
||||
|
||||
const elements = ref<Elements>(initialElements)
|
||||
const opts = ref({
|
||||
bg: '#eee',
|
||||
name: 'Node 1',
|
||||
hidden: false,
|
||||
})
|
||||
|
||||
const updateNode = () => {
|
||||
elements.value.forEach((el) => {
|
||||
if (el.id === '1') {
|
||||
// it's important that you create a new object here in order to notify react flow about the change
|
||||
el.label = opts.value.name
|
||||
el.style = { backgroundColor: opts.value.bg }
|
||||
el.hidden = opts.value.hidden
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(updateNode)
|
||||
</script>
|
||||
<template>
|
||||
<VueFlow v-model="elements" :default-zoom="1.5" :min-zoom="0.2" :max-zoom="4">
|
||||
<div class="updatenode__controls">
|
||||
<label>label:</label>
|
||||
<input v-model="opts.name" @input="updateNode" />
|
||||
|
||||
<label class="updatenode__bglabel">background:</label>
|
||||
<input v-model="opts.bg" type="color" @input="updateNode" />
|
||||
|
||||
<div class="updatenode__checkboxwrapper">
|
||||
<label>hidden:</label>
|
||||
<input v-model="opts.hidden" type="checkbox" @change="updateNode" />
|
||||
</div>
|
||||
</div>
|
||||
</VueFlow>
|
||||
</template>
|
||||
<style>
|
||||
@import 'updatenode.css';
|
||||
</style>
|
||||
@@ -0,0 +1,21 @@
|
||||
.updatenode__controls {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
z-index: 4;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.updatenode__controls label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.updatenode__bglabel {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.updatenode__checkboxwrapper {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts" setup>
|
||||
import { Position, Handle, ValidConnectionFunc } from '@braks/vue-flow'
|
||||
|
||||
interface CustomInputProps {
|
||||
isValidTargetPos: ValidConnectionFunc
|
||||
}
|
||||
const props = defineProps<CustomInputProps>()
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div>Only connectable with B</div>
|
||||
<Handle type="source" :position="Position.Right" :is-valid-connection="props.isValidTargetPos" />
|
||||
</template>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
import { Position, Handle, NodeProps, ValidConnectionFunc } from '@braks/vue-flow'
|
||||
|
||||
interface CustomNodeProps extends NodeProps {
|
||||
id: string
|
||||
isValidSourcePos: ValidConnectionFunc
|
||||
}
|
||||
|
||||
const props = defineProps<CustomNodeProps>()
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Handle type="target" :position="Position.Left" :is-valid-connection="props.isValidSourcePos" />
|
||||
<div>{{ props.id }}</div>
|
||||
</template>
|
||||
@@ -0,0 +1,49 @@
|
||||
<script lang="ts" setup>
|
||||
import CustomInput from './CustomInput.vue'
|
||||
import CustomNode from './CustomNode.vue'
|
||||
import { VueFlow, Connection, OnConnectStartParams, FlowInstance, useVueFlow } from '@braks/vue-flow'
|
||||
|
||||
const { nodes, edges, addEdges } = useVueFlow({
|
||||
nodes: [
|
||||
{ id: '0', type: 'custominput', position: { x: 0, y: 150 }, isValidTargetPos: (connection) => connection.target === 'B' },
|
||||
{
|
||||
id: 'A',
|
||||
type: 'customnode',
|
||||
position: { x: 250, y: 0 },
|
||||
isValidSourcePos: () => false,
|
||||
},
|
||||
{ id: 'B', type: 'customnode', position: { x: 250, y: 150 }, isValidSourcePos: (connection) => connection.target === 'B' },
|
||||
{ id: 'C', type: 'customnode', position: { x: 250, y: 300 }, isValidSourcePos: (connection) => connection.target === 'B' },
|
||||
],
|
||||
})
|
||||
const onLoad = (flowInstance: FlowInstance) => flowInstance.fitView()
|
||||
const onConnectStart = ({ nodeId, handleType }: OnConnectStartParams) => console.log('on connect start', { nodeId, handleType })
|
||||
const onConnectStop = (event: MouseEvent) => console.log('on connect stop', event)
|
||||
const onConnectEnd = (event: MouseEvent) => console.log('on connect end', event)
|
||||
|
||||
const onConnect = (params: Connection) => {
|
||||
console.log('on connect', params)
|
||||
addEdges([params])
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<VueFlow
|
||||
:select-nodes-on-drag="false"
|
||||
class="validationflow"
|
||||
@connect="onConnect"
|
||||
@pane-ready="onLoad"
|
||||
@connect-start="onConnectStart"
|
||||
@connect-stop="onConnectStop"
|
||||
@connect-end="onConnectEnd"
|
||||
>
|
||||
<template #node-custominput="props">
|
||||
<CustomInput v-bind="props" />
|
||||
</template>
|
||||
<template #node-customnode="props">
|
||||
<CustomNode v-bind="props" />
|
||||
</template>
|
||||
</VueFlow>
|
||||
</template>
|
||||
<style>
|
||||
@import 'validation.css';
|
||||
</style>
|
||||
@@ -0,0 +1,31 @@
|
||||
.validationflow .vue-flow__node {
|
||||
width: 150px;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
color: #555;
|
||||
border: 1px solid #ddd;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.validationflow .vue-flow__node-customnode {
|
||||
background: #e6e6e9;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.vue-flow__node-custominput .vue-flow__handle {
|
||||
background: #e6e6e9;
|
||||
}
|
||||
|
||||
.validationflow .vue-flow__node-custominput {
|
||||
background: #fff;
|
||||
|
||||
}
|
||||
|
||||
.validationflow .vue-flow__handle-connecting {
|
||||
background: #ff6060;
|
||||
}
|
||||
|
||||
.validationflow .vue-flow__handle-valid {
|
||||
background: #55dd99;
|
||||
}
|
||||
Reference in New Issue
Block a user