docs: use vitepress
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
<script setup>
|
||||
import { Background, Controls, MiniMap, VueFlow, isNode, useVueFlow } from '@braks/vue-flow'
|
||||
import { ref } from 'vue'
|
||||
import { initialElements } from './initial-elements.js'
|
||||
|
||||
/**
|
||||
* useVueFlow provides all event handlers and store properties
|
||||
* You can pass the composable an object that has the same properties as the VueFlow component props
|
||||
*/
|
||||
const { onPaneReady, onNodeDragStop, onConnect, addEdges, setTransform, toObject } = useVueFlow()
|
||||
|
||||
/**
|
||||
* Our elements
|
||||
*/
|
||||
const elements = ref(initialElements)
|
||||
|
||||
/**
|
||||
* This is a Vue Flow event-hook which can be listened to from anywhere you call the composable, instead of only on the main component
|
||||
*
|
||||
* onPaneReady is called when viewpane & nodes have visible dimensions
|
||||
*/
|
||||
onPaneReady(({ fitView }) => {
|
||||
fitView()
|
||||
})
|
||||
|
||||
onNodeDragStop((e) => console.log('drag stop', e))
|
||||
|
||||
/**
|
||||
* onConnect is called when a new connection is created.
|
||||
* You can add additional properties to your new edge (like a type or label) or block the creation altogether
|
||||
*/
|
||||
onConnect((params) => addEdges([params]))
|
||||
|
||||
const dark = ref(false)
|
||||
|
||||
/**
|
||||
* To update node properties you can simply use your elements v-model and mutate the elements directly
|
||||
* Changes should always be reflected on the graph reactively, without the need to overwrite the elements
|
||||
*/
|
||||
const updatePos = () =>
|
||||
elements.value.forEach((el) => {
|
||||
if (isNode(el)) {
|
||||
el.position = {
|
||||
x: Math.random() * 400,
|
||||
y: Math.random() * 400,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* toObject transforms your current graph data to an easily persist-able object
|
||||
*/
|
||||
const logToObject = () => console.log(toObject())
|
||||
|
||||
/**
|
||||
* Resets the current viewpane transformation (zoom & pan)
|
||||
*/
|
||||
const resetTransform = () => setTransform({ x: 0, y: 0, zoom: 1 })
|
||||
|
||||
const toggleClass = () => {
|
||||
dark.value = !dark.value
|
||||
elements.value.forEach((el) => (el.class = dark.value ? 'dark' : 'light'))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VueFlow v-model="elements" class="basicflow" :default-zoom="1.5" :min-zoom="0.2" :max-zoom="4">
|
||||
<Background pattern-color="#aaa" gap="8" />
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
|
||||
<div class="controls">
|
||||
<button style="background-color: #113285; color: white" @click="resetTransform">reset transform</button>
|
||||
<button style="background-color: #6f3381; color: white" @click="updatePos">update positions</button>
|
||||
<button
|
||||
:style="{ backgroundColor: dark ? '#FFFFFB' : '#1C1C1C', color: dark ? '#1C1C1C' : '#FFFFFB' }"
|
||||
@click="toggleClass"
|
||||
>
|
||||
toggle {{ dark ? 'light' : 'dark' }}
|
||||
</button>
|
||||
<button @click="logToObject">log toObject</button>
|
||||
</div>
|
||||
</VueFlow>
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as BasicApp } from './App.vue?raw'
|
||||
export { default as BasicElements } from './initial-elements.js?raw'
|
||||
export { default as BasicCSS } from './style.css'
|
||||
@@ -0,0 +1,25 @@
|
||||
import { MarkerType } from '@braks/vue-flow'
|
||||
|
||||
/**
|
||||
* You can pass elements together as a v-model value
|
||||
* or split them up into nodes and edges and pass them to the `nodes` and `edges` props of Vue Flow (or useVueFlow composable)
|
||||
*/
|
||||
export const initialElements = [
|
||||
{ id: '1', type: 'input', label: 'Node 1', position: { x: 250, y: 5 }, class: 'light' },
|
||||
{ id: '2', type: 'output', 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: 150, y: 200 }, class: 'light' },
|
||||
{ id: '5', type: 'output', label: 'Node 5', position: { x: 300, y: 300 }, class: 'light' },
|
||||
{ id: 'e1-2', source: '1', target: '2', animated: true },
|
||||
{ id: 'e1-3', label: 'edge with arrowhead', source: '1', target: '3', markerEnd: MarkerType.Arrow },
|
||||
{
|
||||
id: 'e4-5',
|
||||
type: 'step',
|
||||
label: 'step-edge',
|
||||
source: '4',
|
||||
target: '5',
|
||||
style: { stroke: 'orange' },
|
||||
labelBgStyle: { fill: 'orange' },
|
||||
},
|
||||
{ id: 'e3-4', type: 'smoothstep', label: 'smoothstep-edge', source: '3', target: '4' },
|
||||
]
|
||||
@@ -0,0 +1,30 @@
|
||||
.basicflow .vue-flow__node.dark {
|
||||
background: #1C1C1C;
|
||||
color: #FFFFFB;
|
||||
}
|
||||
|
||||
.basicflow .controls {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 10px;
|
||||
z-index: 4;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.basicflow .controls button {
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
font-weight: 500;
|
||||
-webkit-box-shadow: 0px 5px 10px 0px rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0px 5px 10px 0px rgba(0, 0, 0, 0.3);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.basicflow .controls button:hover {
|
||||
opacity: 0.8;
|
||||
transform: scale(105%);
|
||||
transition: 250ms all ease;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<script setup>
|
||||
import { Background, BackgroundVariant, VueFlow } from '@braks/vue-flow'
|
||||
import { ref } from 'vue'
|
||||
import CustomConnectionLine from './CustomConnectionLine.vue'
|
||||
|
||||
const elements = ref([
|
||||
{
|
||||
id: '1',
|
||||
type: 'input',
|
||||
label: 'Node 1',
|
||||
position: { x: 250, y: 5 },
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VueFlow v-model="elements">
|
||||
<template #connection-line="props">
|
||||
<CustomConnectionLine v-bind="props" />
|
||||
</template>
|
||||
|
||||
<Background :variant="BackgroundVariant.Lines" />
|
||||
</VueFlow>
|
||||
</template>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
sourceX: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
sourceY: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
targetX: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
targetY: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<g>
|
||||
<path
|
||||
class="animated"
|
||||
fill="none"
|
||||
stroke="#6F3381"
|
||||
:stroke-width="2.5"
|
||||
:d="`M${sourceX},${sourceY} C ${sourceX} ${targetY} ${sourceX} ${targetY} ${targetX},${targetY}`"
|
||||
/>
|
||||
<circle :cx="targetX" :cy="targetY" fill="#fff" :r="5" stroke="#6F3381" :stroke-width="1.5" />
|
||||
</g>
|
||||
</template>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as CustomConnectionLineApp } from './App.vue?raw'
|
||||
export { default as CustomConnectionLine } from './CustomConnectionLine.vue?raw'
|
||||
@@ -0,0 +1,117 @@
|
||||
<script setup>
|
||||
import { ConnectionMode, MiniMap, Position, VueFlow, useVueFlow } from '@braks/vue-flow'
|
||||
import { computed, h, onMounted, ref } from 'vue'
|
||||
import ColorSelectorNode from './CustomNode.vue'
|
||||
import { presets } from './presets.js'
|
||||
|
||||
const { getNode } = useVueFlow()
|
||||
|
||||
const outputColorNode = computed(() => getNode.value('3'))
|
||||
|
||||
const elements = ref([])
|
||||
|
||||
const gradient = ref(false)
|
||||
const bgColor = ref(presets.ayame)
|
||||
const bgName = ref('AYAME')
|
||||
|
||||
const connectionLineStyle = { stroke: '#fff' }
|
||||
|
||||
// minimap stroke color functions
|
||||
const nodeStroke = (n) => {
|
||||
if (n.type === 'input') return '#0041d0'
|
||||
if (n.type === 'custom') return presets.sumi
|
||||
if (n.type === 'output') return '#ff0072'
|
||||
return '#eee'
|
||||
}
|
||||
|
||||
const nodeColor = (n) => {
|
||||
if (n.type === 'custom') return bgColor.value
|
||||
return '#fff'
|
||||
}
|
||||
|
||||
// output labels
|
||||
const outputColorLabel = () => h('div', {}, bgColor.value)
|
||||
const outputNameLabel = () => h('div', {}, bgName.value)
|
||||
|
||||
const onChange = (color) => {
|
||||
gradient.value = false
|
||||
bgColor.value = color.value
|
||||
bgName.value = color.name
|
||||
|
||||
outputColorNode.value.hidden = false
|
||||
}
|
||||
|
||||
const onGradient = () => {
|
||||
gradient.value = true
|
||||
bgColor.value = null
|
||||
bgName.value = 'gradient'
|
||||
|
||||
outputColorNode.value.hidden = true
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
elements.value = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'custom',
|
||||
data: { color: bgColor },
|
||||
position: { x: 0, y: 50 },
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'output',
|
||||
label: outputNameLabel,
|
||||
position: { x: 350, y: 25 },
|
||||
targetPosition: Position.Left,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'output',
|
||||
label: outputColorLabel,
|
||||
position: { x: 350, y: 200 },
|
||||
targetPosition: Position.Left,
|
||||
},
|
||||
{
|
||||
id: 'e1a-2',
|
||||
source: '1',
|
||||
sourceHandle: 'a',
|
||||
target: '2',
|
||||
animated: true,
|
||||
style: () => ({
|
||||
stroke: bgColor.value,
|
||||
filter: 'invert(100%)',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'e1b-3',
|
||||
source: '1',
|
||||
sourceHandle: 'b',
|
||||
target: '3',
|
||||
animated: true,
|
||||
style: () => ({
|
||||
stroke: bgColor.value,
|
||||
filter: 'invert(100%)',
|
||||
}),
|
||||
},
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VueFlow
|
||||
v-model="elements"
|
||||
class="customnodeflow"
|
||||
:class="[gradient ? 'animated-bg-gradient' : '']"
|
||||
:style="{ backgroundColor: bgColor }"
|
||||
:connection-mode="ConnectionMode.Loose"
|
||||
:connection-line-style="connectionLineStyle"
|
||||
:default-zoom="1.5"
|
||||
:fit-view-on-init="true"
|
||||
>
|
||||
<template #node-custom="props">
|
||||
<ColorSelectorNode :data="props.data" @change="onChange" @gradient="onGradient" />
|
||||
</template>
|
||||
|
||||
<MiniMap :node-stroke-color="nodeStroke" :node-color="nodeColor" />
|
||||
</VueFlow>
|
||||
</template>
|
||||
@@ -0,0 +1,59 @@
|
||||
<script setup>
|
||||
import { Handle, Position } from '@braks/vue-flow'
|
||||
import { computed } from 'vue'
|
||||
import { presets } from './presets.js'
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['change', 'gradient'])
|
||||
|
||||
const onConnect = (params) => console.log('handle onConnect', params)
|
||||
|
||||
const onSelect = (color) => {
|
||||
emit('change', color)
|
||||
}
|
||||
|
||||
const onGradient = () => {
|
||||
emit('gradient')
|
||||
}
|
||||
|
||||
const colors = computed(() => {
|
||||
return Object.keys(presets).map((color) => {
|
||||
return {
|
||||
name: color,
|
||||
value: presets[color],
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const selectedColor = computed(() => {
|
||||
return colors.value.find((color) => color.value === props.data.color)
|
||||
})
|
||||
|
||||
const sourceHandleStyleA = computed(() => ({ backgroundColor: props.data.color, filter: 'invert(100%)', top: '10px' }))
|
||||
const sourceHandleStyleB = computed(() => ({
|
||||
backgroundColor: props.data.color,
|
||||
filter: 'invert(100%)',
|
||||
bottom: '10px',
|
||||
top: 'auto',
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>Select a color</div>
|
||||
<div
|
||||
style="display: flex; flex-direction: row; flex-wrap: wrap; justify-content: center; max-width: 90%; margin: auto; gap: 3px"
|
||||
>
|
||||
<template v-for="color of colors" :key="color.name">
|
||||
<button :title="color.name" :style="{ backgroundColor: color.value }" type="button" @click="onSelect(color)"></button>
|
||||
</template>
|
||||
<button class="animated-bg-gradient" title="gradient" type="button" @click="onGradient"></button>
|
||||
</div>
|
||||
<Handle id="a" type="source" :position="Position.Right" :style="sourceHandleStyleA" />
|
||||
<Handle id="b" type="source" :position="Position.Right" :style="sourceHandleStyleB" />
|
||||
</template>
|
||||
@@ -0,0 +1,4 @@
|
||||
export { default as CustomNodeApp } from './App.vue?raw'
|
||||
export { default as CustomNode } from './CustomNode.vue?raw'
|
||||
export { default as CustomNodeCSS } from './style.css'
|
||||
export { default as ColorPresets } from './presets.js?raw'
|
||||
@@ -0,0 +1,23 @@
|
||||
export const presets = {
|
||||
sumi: '#1C1C1C',
|
||||
gofun: '#FFFFFB',
|
||||
byakuroku: '#A8D8B9',
|
||||
mizu: '#81C7D4',
|
||||
asagi: '#33A6B8',
|
||||
ukon: '#EFBB24',
|
||||
mushikuri: '#D9CD90',
|
||||
hiwa: '#BEC23F',
|
||||
ichigo: '#B5495B',
|
||||
kurenai: '#CB1B45',
|
||||
syojyohi: '#E83015',
|
||||
konjyo: '#113285',
|
||||
fuji: '#8B81C3',
|
||||
ayame: '#6F3381',
|
||||
torinoko: '#DAC9A6',
|
||||
kurotsurubami: '#0B1013',
|
||||
ohni: '#F05E1C',
|
||||
kokikuchinashi: '#FB9966',
|
||||
beniukon: '#E98B2A',
|
||||
sakura: '#FEDFE1',
|
||||
toki: '#EEA9A9',
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
.customnodeflow .vue-flow__node-custom {
|
||||
border: 1px solid #777;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
background: whitesmoke;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.customnodeflow button {
|
||||
padding: 5px;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 25px;
|
||||
-webkit-box-shadow: 0px 5px 10px 0px rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0px 5px 10px 0px rgba(0, 0, 0, 0.3);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.customnodeflow button:hover {
|
||||
opacity: 0.9;
|
||||
transform: scale(105%);
|
||||
transition: 250ms all ease;
|
||||
}
|
||||
|
||||
.animated-bg-gradient {
|
||||
background: linear-gradient(122deg, #6f3381, #81c7d4, #fedfe1, #fffffb);
|
||||
background-size: 800% 800%;
|
||||
|
||||
-webkit-animation: gradient 4s ease infinite;
|
||||
-moz-animation: gradient 4s ease infinite;
|
||||
animation: gradient 4s ease infinite;
|
||||
}
|
||||
|
||||
@-webkit-keyframes gradient {
|
||||
0% {
|
||||
background-position: 0% 22%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 79%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 22%
|
||||
}
|
||||
}
|
||||
|
||||
@-moz-keyframes gradient {
|
||||
0% {
|
||||
background-position: 0% 22%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 79%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 22%
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes gradient {
|
||||
0% {
|
||||
background-position: 0% 22%
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 79%
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 22%
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup>
|
||||
import { VueFlow, useVueFlow } from '@braks/vue-flow'
|
||||
import Sidebar from './Sidebar.vue'
|
||||
|
||||
let id = 0
|
||||
const getId = () => `dndnode_${id++}`
|
||||
|
||||
const { onConnect, nodes, edges, addEdges, addNodes, viewport, project } = useVueFlow({
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'input',
|
||||
label: 'input node',
|
||||
position: { x: 250, y: 25 },
|
||||
},
|
||||
],
|
||||
})
|
||||
const onDragOver = (event) => {
|
||||
event.preventDefault()
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = 'move'
|
||||
}
|
||||
}
|
||||
|
||||
onConnect((params) => addEdges([params]))
|
||||
|
||||
const onDrop = (event) => {
|
||||
const type = event.dataTransfer?.getData('application/vueflow')
|
||||
const position = project({ x: event.clientX - 40, y: event.clientY - 18 })
|
||||
const newNode = {
|
||||
id: getId(),
|
||||
type,
|
||||
position,
|
||||
label: `${type} node`,
|
||||
}
|
||||
addNodes([newNode])
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dndflow" @drop="onDrop">
|
||||
<VueFlow @dragover="onDragOver" />
|
||||
<Sidebar />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script setup>
|
||||
const onDragStart = (event, nodeType) => {
|
||||
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.</div>
|
||||
<div class="nodes">
|
||||
<div class="vue-flow__node-input" :draggable="true" @dragstart="onDragStart($event, 'input')">Input Node</div>
|
||||
<div class="vue-flow__node-default" :draggable="true" @dragstart="onDragStart($event, 'default')">Default Node</div>
|
||||
<div class="vue-flow__node-output" :draggable="true" @dragstart="onDragStart($event, 'output')">Output Node</div>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as DndApp } from './App.vue?raw'
|
||||
export { default as DndSidebar } from './Sidebar.vue?raw'
|
||||
export { default as DndCSS } from './style.css'
|
||||
@@ -0,0 +1,51 @@
|
||||
.dndflow {
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dndflow aside {
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
border-right: 1px solid #eee;
|
||||
padding: 15px 10px;
|
||||
font-size: 12px;
|
||||
background: rgba(16, 185, 129, 0.75);
|
||||
-webkit-box-shadow: 0px 5px 10px 0px rgba(0,0,0,0.3);
|
||||
box-shadow: 0px 5px 10px 0px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.dndflow aside .nodes > * {
|
||||
margin-bottom: 10px;
|
||||
cursor: grab;
|
||||
font-weight: 500;
|
||||
-webkit-box-shadow: 5px 5px 10px 2px rgba(0,0,0,0.25);
|
||||
box-shadow: 5px 5px 10px 2px rgba(0,0,0,0.25);
|
||||
}
|
||||
|
||||
.dndflow aside .description {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.dndflow .vue-flow-wrapper {
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 640px) {
|
||||
.dndflow {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.dndflow aside {
|
||||
min-width: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 639px) {
|
||||
.dndflow aside .nodes {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<script setup>
|
||||
import { Background, Controls, MarkerType, MiniMap, VueFlow } from '@braks/vue-flow'
|
||||
import { h, ref } from 'vue'
|
||||
import CustomEdge from './CustomEdge.vue'
|
||||
import CustomEdge2 from './CustomEdge2.vue'
|
||||
import CustomEdgeLabel from './CustomEdgeLabel.vue'
|
||||
|
||||
const elements = ref([
|
||||
{ id: '1', type: 'input', label: 'Start', position: { x: 50, y: 0 }, style: { borderColor: '#10b981' } },
|
||||
{ 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: 175, y: 300 } },
|
||||
{ id: '5', label: 'Node 5', position: { x: 200, y: 400 } },
|
||||
{ id: '6', type: 'output', label: 'Output 6', position: { x: 0, y: 350 } },
|
||||
{ id: '7', type: 'output', label: 'Output 7', position: { x: 50, y: 600 } },
|
||||
{ id: '8', type: 'output', label: 'Output 8', position: { x: 350, y: 600 } },
|
||||
{ id: '9', type: 'output', label: 'Output 9', position: { x: 550, y: 400 } },
|
||||
{ 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: '#10b981' } },
|
||||
{
|
||||
id: 'e2a-6',
|
||||
source: '2a',
|
||||
target: '6',
|
||||
label: () => h(CustomEdgeLabel, { label: 'custom label text' }),
|
||||
labelStyle: { fill: '#10b981', fontWeight: 700 },
|
||||
markerEnd: MarkerType.Arrow,
|
||||
},
|
||||
{
|
||||
id: 'e5-7',
|
||||
source: '5',
|
||||
target: '7',
|
||||
label: 'label with bg',
|
||||
labelBgPadding: [8, 4],
|
||||
labelBgBorderRadius: 4,
|
||||
labelBgStyle: { fill: '#FFCC00', color: '#fff', fillOpacity: 0.7 },
|
||||
markerEnd: MarkerType.ArrowClosed,
|
||||
},
|
||||
{
|
||||
id: 'e5-8',
|
||||
source: '5',
|
||||
target: '8',
|
||||
type: 'custom',
|
||||
data: { text: 'custom edge' },
|
||||
markerEnd: MarkerType.ArrowClosed,
|
||||
},
|
||||
{
|
||||
id: 'e4-9',
|
||||
source: '4',
|
||||
target: '9',
|
||||
type: 'custom2',
|
||||
data: { text: 'styled custom edge label' },
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VueFlow v-model="elements" :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,100 @@
|
||||
<script setup>
|
||||
import { getBezierPath, getEdgeCenter, useVueFlow } from '@braks/vue-flow'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
sourceX: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
sourceY: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
targetX: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
targetY: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
sourcePosition: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
targetPosition: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
markerEnd: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
style: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
|
||||
const { applyEdgeChanges } = useVueFlow()
|
||||
|
||||
const foreignObjectSize = 40
|
||||
|
||||
const onClick = (evt, id) => {
|
||||
applyEdgeChanges([{ type: 'remove', id }])
|
||||
evt.stopPropagation()
|
||||
}
|
||||
|
||||
const edgePath = computed(() =>
|
||||
getBezierPath({
|
||||
sourceX: props.sourceX,
|
||||
sourceY: props.sourceY,
|
||||
sourcePosition: props.sourcePosition,
|
||||
targetX: props.targetX,
|
||||
targetY: props.targetY,
|
||||
targetPosition: props.targetPosition,
|
||||
}),
|
||||
)
|
||||
|
||||
const center = computed(() =>
|
||||
getEdgeCenter({
|
||||
sourceX: props.sourceX,
|
||||
sourceY: props.sourceY,
|
||||
targetX: props.targetX,
|
||||
targetY: props.targetY,
|
||||
}),
|
||||
)
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<path :id="id" :style="style" class="vue-flow__edge-path" :d="edgePath" :marker-end="markerEnd" />
|
||||
<foreignObject
|
||||
:width="foreignObjectSize"
|
||||
:height="foreignObjectSize"
|
||||
:x="center[0] - foreignObjectSize / 2"
|
||||
:y="center[1] - foreignObjectSize / 2"
|
||||
class="edgebutton-foreignobject"
|
||||
requiredExtensions="http://www.w3.org/1999/xhtml"
|
||||
>
|
||||
<body style="display: flex; align-items: center; justify-content: center">
|
||||
<div>
|
||||
<button ref="btn" class="edgebutton" @click="(event) => onClick(event, id)">×</button>
|
||||
</div>
|
||||
</body>
|
||||
</foreignObject>
|
||||
</template>
|
||||
@@ -0,0 +1,97 @@
|
||||
<script setup>
|
||||
import { EdgeText, getBezierPath, getEdgeCenter } from '@braks/vue-flow'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
sourceX: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
sourceY: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
targetX: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
targetY: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
sourcePosition: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
targetPosition: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
markerEnd: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
style: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
sourceHandleId: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
targetHandleId: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
|
||||
const edgePath = computed(() =>
|
||||
getBezierPath({
|
||||
sourceX: props.sourceX,
|
||||
sourceY: props.sourceY,
|
||||
sourcePosition: props.sourcePosition,
|
||||
targetX: props.targetX,
|
||||
targetY: props.targetY,
|
||||
targetPosition: props.targetPosition,
|
||||
}),
|
||||
)
|
||||
|
||||
const center = computed(() =>
|
||||
getEdgeCenter({
|
||||
sourceX: props.sourceX,
|
||||
sourceY: props.sourceY,
|
||||
targetX: props.targetX,
|
||||
targetY: props.targetY,
|
||||
}),
|
||||
)
|
||||
const onClick = () => console.log(props.data)
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<path :id="props.id" class="vue-flow__edge-path" :d="edgePath" :marker-end="props.markerEnd" />
|
||||
<EdgeText
|
||||
:x="center[0]"
|
||||
:y="center[1]"
|
||||
:label="props.data?.text"
|
||||
:label-style="{ fill: 'white' }"
|
||||
:label-show-bg="true"
|
||||
:label-bg-style="{ fill: '#10b981' }"
|
||||
:label-bg-padding="[2, 4]"
|
||||
:label-bg-border-radius="2"
|
||||
@click="onClick"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,12 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tspan dy="10" x="0">{{ props.label }}</tspan>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
export { default as EdgesApp } from './App.vue?raw'
|
||||
export { default as CustomEdge } from './CustomEdge.vue?raw'
|
||||
export { default as CustomEdge2 } from './CustomEdge2.vue?raw'
|
||||
export { default as CustomEdgeLabel } from './CustomEdgeLabel.vue?raw'
|
||||
export { default as EdgeCSS } from './style.css'
|
||||
@@ -0,0 +1,10 @@
|
||||
.edgebutton {
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.edgebutton:hover {
|
||||
transform: scale(110%);
|
||||
transition: all ease 500ms;
|
||||
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.5), 0 0 0 4px #10b981;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup>
|
||||
import { Background, BackgroundVariant, Controls, MiniMap, VueFlow, 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 = {
|
||||
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 @@
|
||||
export { default as EmptyApp } from './App.vue?raw'
|
||||
@@ -0,0 +1,41 @@
|
||||
<script setup>
|
||||
import { Controls, MiniMap, VueFlow, useVueFlow } from '@braks/vue-flow'
|
||||
import { ref, watchEffect } from 'vue'
|
||||
|
||||
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">
|
||||
hidden
|
||||
<input id="ishidden" v-model="isHidden" type="checkbox" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</VueFlow>
|
||||
</template>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as HiddenApp } from './App.vue?raw'
|
||||
@@ -0,0 +1,27 @@
|
||||
<script setup>
|
||||
import { Background, Controls, MiniMap, VueFlow, useVueFlow } from '@braks/vue-flow'
|
||||
import { ref } from 'vue'
|
||||
import { initialElements } from './initial-elements.js'
|
||||
|
||||
const { onConnect, addEdges } = useVueFlow()
|
||||
|
||||
const elements = ref(initialElements)
|
||||
|
||||
onConnect((params) => addEdges([params]))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VueFlow
|
||||
v-model="elements"
|
||||
class="basicflow"
|
||||
:default-edge-options="{ type: 'smoothstep' }"
|
||||
:default-zoom="1.5"
|
||||
:min-zoom="0.2"
|
||||
:max-zoom="4"
|
||||
:fit-view-on-init="true"
|
||||
>
|
||||
<Background pattern-color="#aaa" gap="8" />
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
</VueFlow>
|
||||
</template>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as HorizontalApp } from './App.vue?raw'
|
||||
export { default as HorizontalElements } from './initial-elements.js?raw'
|
||||
@@ -0,0 +1,38 @@
|
||||
import { MarkerType, Position } from '@braks/vue-flow'
|
||||
|
||||
export const initialElements = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'input',
|
||||
label: 'Node 1',
|
||||
position: { x: 0, y: 50 },
|
||||
sourcePosition: Position.Right,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'output',
|
||||
label: 'Node 2',
|
||||
position: { x: 250, y: 0 },
|
||||
targetPosition: Position.Left,
|
||||
},
|
||||
{ id: '3', label: 'Node 3', position: { x: 250, y: 100 }, sourcePosition: Position.Right, targetPosition: Position.Left },
|
||||
{ id: '4', label: 'Node 4', position: { x: 500, y: 150 }, sourcePosition: Position.Right, targetPosition: Position.Left },
|
||||
{
|
||||
id: '5',
|
||||
type: 'output',
|
||||
label: 'Node 5',
|
||||
position: { x: 750, y: 50 },
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Left,
|
||||
},
|
||||
{ id: 'e1-2', source: '1', target: '2', animated: true },
|
||||
{ id: 'e1-3', source: '1', target: '3', markerEnd: MarkerType.Arrow },
|
||||
{
|
||||
id: 'e4-5',
|
||||
source: '4',
|
||||
target: '5',
|
||||
style: { stroke: 'orange' },
|
||||
labelBgStyle: { fill: 'orange' },
|
||||
},
|
||||
{ id: 'e3-4', source: '3', target: '4' },
|
||||
]
|
||||
@@ -0,0 +1,100 @@
|
||||
import { BasicApp, BasicCSS, BasicElements } from './basic'
|
||||
import { ColorPresets, CustomNode, CustomNodeApp, CustomNodeCSS } from './custom-node'
|
||||
import { CustomConnectionLine, CustomConnectionLineApp } from './connectionline'
|
||||
import { CustomEdge, CustomEdge2, CustomEdgeLabel, EdgeCSS, EdgesApp } from './edges'
|
||||
import { NestedApp } from './nested'
|
||||
import { StressApp, StressCSS, StressUtils } from './stress'
|
||||
import { UpdateEdgeApp } from './update-edge'
|
||||
import { UpdateNodeApp, UpdateNodeCSS } from './update-node'
|
||||
import { ValidationApp, ValidationCSS, ValidationCustomInput, ValidationCustomNode } from './validation'
|
||||
import { SaveRestoreApp, SaveRestoreCSS, SaveRestoreControls } from './save-restore'
|
||||
import { DndApp, DndCSS, DndSidebar } from './dnd'
|
||||
import { EmptyApp } from './empty'
|
||||
import { HiddenApp } from './hidden'
|
||||
import { InteractionApp, InteractionCSS, InteractionControls } from './interaction'
|
||||
import { MultiApp, MultiCSS, MultiFlow } from './multi'
|
||||
import { HorizontalApp, HorizontalElements } from './horizontal'
|
||||
import { TeleportApp, TeleportCSS, TeleportSidebar, TeleportableNode, TeleportableUseTransition } from './teleport'
|
||||
|
||||
export const exampleImports = {
|
||||
basic: {
|
||||
'App.vue': BasicApp,
|
||||
'initial-elements.js': BasicElements,
|
||||
'style.css': BasicCSS,
|
||||
},
|
||||
customNode: {
|
||||
'App.vue': CustomNodeApp,
|
||||
'CustomNode.vue': CustomNode,
|
||||
'style.css': CustomNodeCSS,
|
||||
'presets.js': ColorPresets,
|
||||
},
|
||||
connectionline: {
|
||||
'App.vue': CustomConnectionLineApp,
|
||||
'CustomConnectionLine.vue': CustomConnectionLine,
|
||||
},
|
||||
edges: {
|
||||
'App.vue': EdgesApp,
|
||||
'CustomEdge.vue': CustomEdge,
|
||||
'CustomEdge2.vue': CustomEdge2,
|
||||
'CustomEdgeLabel.vue': CustomEdgeLabel,
|
||||
'style.css': EdgeCSS,
|
||||
},
|
||||
nested: {
|
||||
'App.vue': NestedApp,
|
||||
},
|
||||
stress: {
|
||||
'App.vue': StressApp,
|
||||
'utils.js': StressUtils,
|
||||
'style.css': StressCSS,
|
||||
},
|
||||
updateEdge: {
|
||||
'App.vue': UpdateEdgeApp,
|
||||
},
|
||||
updateNode: {
|
||||
'App.vue': UpdateNodeApp,
|
||||
'style.css': UpdateNodeCSS,
|
||||
},
|
||||
validation: {
|
||||
'App.vue': ValidationApp,
|
||||
'CustomInput.vue': ValidationCustomInput,
|
||||
'CustomNode.vue': ValidationCustomNode,
|
||||
'style.css': ValidationCSS,
|
||||
},
|
||||
saveRestore: {
|
||||
'App.vue': SaveRestoreApp,
|
||||
'Controls.vue': SaveRestoreControls,
|
||||
'style.css': SaveRestoreCSS,
|
||||
},
|
||||
dnd: {
|
||||
'App.vue': DndApp,
|
||||
'Sidebar.vue': DndSidebar,
|
||||
'style.css': DndCSS,
|
||||
},
|
||||
empty: {
|
||||
'App.vue': EmptyApp,
|
||||
},
|
||||
hidden: {
|
||||
'App.vue': HiddenApp,
|
||||
},
|
||||
interaction: {
|
||||
'App.vue': InteractionApp,
|
||||
'InteractionControls.vue': InteractionControls,
|
||||
'style.css': InteractionCSS,
|
||||
},
|
||||
multi: {
|
||||
'App.vue': MultiApp,
|
||||
'Flow.vue': MultiFlow,
|
||||
'style.css': MultiCSS,
|
||||
},
|
||||
horizontal: {
|
||||
'App.vue': HorizontalApp,
|
||||
'initial-elements.js': HorizontalElements,
|
||||
},
|
||||
teleport: {
|
||||
'App.vue': TeleportApp,
|
||||
'Sidebar.vue': TeleportSidebar,
|
||||
'TeleportableNode.vue': TeleportableNode,
|
||||
'useTransition.js': TeleportableUseTransition,
|
||||
'style.css': TeleportCSS,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<script setup>
|
||||
import { VueFlow } from '@braks/vue-flow'
|
||||
import { ref } from 'vue'
|
||||
import InteractionControls from './InteractionControls.vue'
|
||||
|
||||
const elements = ref([
|
||||
{ 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' },
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VueFlow class="interactionflow" v-model="elements" :fit-view-on-init="true">
|
||||
<InteractionControls />
|
||||
</VueFlow>
|
||||
</template>
|
||||
@@ -0,0 +1,110 @@
|
||||
<script setup>
|
||||
import { useVueFlow } from '@braks/vue-flow'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const {
|
||||
nodesDraggable,
|
||||
nodesConnectable,
|
||||
elementsSelectable,
|
||||
zoomOnScroll,
|
||||
zoomOnDoubleClick,
|
||||
zoomOnPinch,
|
||||
panOnScroll,
|
||||
panOnScrollMode,
|
||||
panOnDrag,
|
||||
onConnect,
|
||||
onNodeDragStop,
|
||||
onPaneClick,
|
||||
onPaneScroll,
|
||||
onPaneContextMenu,
|
||||
onNodeDragStart,
|
||||
onMoveEnd,
|
||||
addEdges,
|
||||
} = useVueFlow()
|
||||
|
||||
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.value && console.log('pane ctx menu', event))
|
||||
onMoveEnd((flowTransform) => console.log('move end', flowTransform))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="controls">
|
||||
<div>
|
||||
<label class="label" for="draggable">
|
||||
nodesDraggable
|
||||
<input id="draggable" v-model="nodesDraggable" type="checkbox" class="vue-flow__draggable" />
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="connectable">
|
||||
nodesConnectable
|
||||
<input id="connectable" v-model="nodesConnectable" type="checkbox" class="vue-flow__connectable" />
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="selectable">
|
||||
elementsSelectable
|
||||
<input id="selectable" v-model="elementsSelectable" type="checkbox" class="vue-flow__selectable" />
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="zoomonscroll">
|
||||
zoomOnScroll
|
||||
<input id="zoomonscroll" v-model="zoomOnScroll" type="checkbox" class="vue-flow__zoomonscroll" />
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="zoomonpinch">
|
||||
zoomOnPinch
|
||||
<input id="zoomonpinch" v-model="zoomOnPinch" type="checkbox" class="vue-flow__zoomonpinch" />
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="panonscroll">
|
||||
panOnScroll
|
||||
<input id="panonscroll" v-model="panOnScroll" type="checkbox" class="vue-flow__panonscroll" />
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="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 class="label" for="zoomondbl">
|
||||
zoomOnDoubleClick
|
||||
<input id="zoomondbl" v-model="zoomOnDoubleClick" type="checkbox" class="vue-flow__zoomondbl" />
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="panemoveable">
|
||||
paneMovable
|
||||
<input id="panemoveable" v-model="panOnDrag" type="checkbox" class="vue-flow__panemoveable" />
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="capturezoompaneclick">
|
||||
capture onPaneClick
|
||||
<input id="capturezoompaneclick" v-model="captureZoomClick" type="checkbox" class="vue-flow__capturezoompaneclick" />
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="capturezoompanescroll">
|
||||
capture onPaneScroll
|
||||
<input id="capturezoompanescroll" v-model="captureZoomScroll" type="checkbox" class="vue-flow__capturezoompanescroll" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as InteractionApp } from './App.vue?raw'
|
||||
export { default as InteractionControls } from './InteractionControls.vue?raw'
|
||||
export { default as InteractionCSS } from './style.css'
|
||||
@@ -0,0 +1,22 @@
|
||||
.interactionflow .controls {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 4;
|
||||
font-size: 11px;
|
||||
background-color: lightgray;
|
||||
border-bottom-right-radius: 10px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.interactionflow .controls .label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.interactionflow .controls .label input {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<script setup>
|
||||
import Flow from './Flow.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="multiflows">
|
||||
<Flow />
|
||||
<Flow />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup>
|
||||
import { Background, VueFlow, isNode } from '@braks/vue-flow'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const elements = ref([
|
||||
{ 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 toggleClass = () => elements.value.forEach((el) => (el.class = el.class === 'light' ? 'dark' : 'light'))
|
||||
|
||||
const updatePos = () =>
|
||||
elements.value.forEach((el) => {
|
||||
if (isNode(el)) {
|
||||
el.position = {
|
||||
x: Math.random() * 400,
|
||||
y: Math.random() * 400,
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VueFlow v-model="elements" :fit-view-on-init="true">
|
||||
<Background />
|
||||
|
||||
<div style="position: absolute; right: 10px; top: 10px; z-index: 4">
|
||||
<button style="margin-right: 5px" @click="updatePos">update positions</button>
|
||||
<button style="margin-right: 5px" @click="toggleClass">toggle class</button>
|
||||
</div>
|
||||
</VueFlow>
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as MultiApp } from './App.vue?raw'
|
||||
export { default as MultiFlow } from './Flow.vue?raw'
|
||||
export { default as MultiCSS } from './style.css'
|
||||
@@ -0,0 +1,18 @@
|
||||
.vue-flow__node.dark {
|
||||
background: #000000;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.multiflows {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.multiflows .vue-flow {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.multiflows .vue-flow:first-child {
|
||||
border-right: 2px solid #333;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<script setup>
|
||||
import { Background, ConnectionMode, Controls, MiniMap, VueFlow, useVueFlow } from '@braks/vue-flow'
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
const { onConnect, nodes, edges, addEdges, addNodes } = useVueFlow({
|
||||
fitViewOnInit: true,
|
||||
connectionMode: ConnectionMode.Loose,
|
||||
// set this to true so edges get elevated when selected, defaults to false
|
||||
elevateEdgesOnSelect: true,
|
||||
nodes: [
|
||||
{ id: '1', type: 'input', label: 'node', position: { x: 250, y: 0 } },
|
||||
{
|
||||
id: '2',
|
||||
label: 'parent node',
|
||||
position: { x: 100, y: 100 },
|
||||
style: { backgroundColor: 'rgba(16, 185, 129, 0.5)', width: '200px', height: '200px' },
|
||||
},
|
||||
{
|
||||
id: '2a',
|
||||
label: 'child node',
|
||||
position: { x: 10, y: 50 },
|
||||
parentNode: '2',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
label: 'parent node',
|
||||
position: { x: 320, y: 175 },
|
||||
style: { backgroundColor: 'rgba(16, 185, 129, 0.5)', width: '400px', height: '300px' },
|
||||
},
|
||||
{
|
||||
id: '4a',
|
||||
label: 'child node',
|
||||
position: { x: 15, y: 65 },
|
||||
extent: 'parent',
|
||||
parentNode: '4',
|
||||
},
|
||||
{
|
||||
id: '4b',
|
||||
label: 'nested parent node',
|
||||
position: { x: 15, y: 120 },
|
||||
style: { backgroundColor: 'rgba(139, 92, 246, 0.5)', height: '150px', width: '270px' },
|
||||
parentNode: '4',
|
||||
},
|
||||
{
|
||||
id: '4b1',
|
||||
label: 'nested child node',
|
||||
position: { x: 20, y: 40 },
|
||||
parentNode: '4b',
|
||||
},
|
||||
{
|
||||
id: '4b2',
|
||||
label: 'nested child node',
|
||||
position: { x: 100, y: 100 },
|
||||
parentNode: '4b',
|
||||
},
|
||||
{ id: '4c', label: 'child node', position: { x: 200, y: 65 }, parentNode: '4' },
|
||||
],
|
||||
edges: [
|
||||
{ id: 'e1-2', source: '1', target: '2' },
|
||||
{ id: 'e1-4', source: '1', target: '4' },
|
||||
{ id: 'e1-4c', source: '1', target: '4c' },
|
||||
{ id: 'e2a-4a', source: '2a', target: '4a' },
|
||||
{ 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: 'Drag me to extend area!',
|
||||
position: { x: 20, y: 100 },
|
||||
class: 'light',
|
||||
expandParent: true,
|
||||
parentNode: '2',
|
||||
},
|
||||
])
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VueFlow>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
<Background />
|
||||
</VueFlow>
|
||||
</template>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as NestedApp } from './App.vue?raw'
|
||||
@@ -0,0 +1,23 @@
|
||||
<script setup>
|
||||
import sdk from '@stackblitz/sdk'
|
||||
|
||||
const el = ref()
|
||||
|
||||
onMounted(() => {
|
||||
sdk.embedProjectId(el.value, 'vitejs-vite-wyfpsj', {
|
||||
height: 750,
|
||||
forceEmbedLayout: true,
|
||||
openFile: 'src/App.vue',
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="el" class="outline-none"></div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.application {
|
||||
@apply h-[75vh];
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script lang="ts" setup>
|
||||
import { VueFlow } from '@braks/vue-flow'
|
||||
import { ref } from 'vue'
|
||||
import Controls from './Controls.vue'
|
||||
|
||||
const elements = ref([{ id: '1', label: 'Node 1', position: { x: 100, y: 100 } }])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VueFlow v-model="elements">
|
||||
<Controls />
|
||||
</VueFlow>
|
||||
</template>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script setup>
|
||||
import { useVueFlow } from '@braks/vue-flow'
|
||||
|
||||
const flowKey = 'example-flow'
|
||||
|
||||
const { nodes, addNodes, setNodes, setEdges, dimensions, setTransform, toObject } = useVueFlow()
|
||||
|
||||
const onSave = () => {
|
||||
localStorage.setItem(flowKey, JSON.stringify(toObject()))
|
||||
}
|
||||
|
||||
const onRestore = () => {
|
||||
const flow = JSON.parse(localStorage.getItem(flowKey))
|
||||
|
||||
if (flow) {
|
||||
const [x = 0, y = 0] = flow.position
|
||||
setNodes(flow.nodes)
|
||||
setEdges(flow.edges)
|
||||
setTransform({ x, y, zoom: flow.zoom || 0 })
|
||||
}
|
||||
}
|
||||
|
||||
const onAdd = () => {
|
||||
const id = nodes.value.length + 1
|
||||
const newNode = {
|
||||
id: `random_node-${id}`,
|
||||
label: `Node ${id}`,
|
||||
position: { x: Math.random() * dimensions.value.width, y: Math.random() * dimensions.value.height },
|
||||
}
|
||||
addNodes([newNode])
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="save__controls">
|
||||
<button style="background-color: #33a6b8" @click="onSave">save</button>
|
||||
<button style="background-color: #113285" @click="onRestore">restore</button>
|
||||
<button style="background-color: #6f3381" @click="onAdd">add node</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as SaveRestoreApp } from './App.vue?raw'
|
||||
export { default as SaveRestoreControls } from './Controls.vue?raw'
|
||||
export { default as SaveRestoreCSS } from './style.css'
|
||||
@@ -0,0 +1,25 @@
|
||||
.save__controls {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 10px;
|
||||
z-index: 4;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.save__controls button {
|
||||
margin-left: 5px;
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
color: white;
|
||||
-webkit-box-shadow: 0px 5px 10px 0px rgba(0,0,0,0.3);
|
||||
box-shadow: 0px 5px 10px 0px rgba(0,0,0,0.3);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.save__controls button:hover {
|
||||
opacity: 0.8;
|
||||
transform: scale(105%);
|
||||
transition: 250ms all ease-in-out;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<script setup>
|
||||
import { VueFlow, isNode, useVueFlow } from '@braks/vue-flow'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { getElements } from './utils.js'
|
||||
|
||||
const { nodes, edges } = getElements(15, 15)
|
||||
const elements = ref([...nodes, ...edges])
|
||||
|
||||
const { onPaneReady, dimensions, onNodeClick, getEdges, fitView } = useVueFlow()
|
||||
|
||||
onPaneReady((i) => {
|
||||
i.fitView({
|
||||
padding: 0.2,
|
||||
})
|
||||
console.log(i.getElements.value)
|
||||
})
|
||||
|
||||
const toggleClass = () => elements.value.forEach((el) => (el.class = el.class === 'light' ? 'dark' : 'light'))
|
||||
|
||||
const updatePos = () => {
|
||||
elements.value.forEach((el) => {
|
||||
if (isNode(el)) {
|
||||
el.position = {
|
||||
x: Math.random() * 10 * dimensions.value.width,
|
||||
y: Math.random() * 10 * dimensions.value.height,
|
||||
}
|
||||
}
|
||||
})
|
||||
nextTick(() => {
|
||||
fitView({ duration: 1000, padding: 0.5 })
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VueFlow v-model="elements" :min-zoom="0.1">
|
||||
<div style="position: absolute; right: 10px; top: 10px; z-index: 4">
|
||||
<button style="margin-right: 5px" @click="updatePos">update positions</button>
|
||||
<button style="margin-right: 5px" @click="toggleClass">toggle class</button>
|
||||
</div>
|
||||
</VueFlow>
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as StressApp } from './App.vue?raw'
|
||||
export { default as StressUtils } from './utils.js?raw'
|
||||
export { default as StressCSS } from './style.css'
|
||||
@@ -0,0 +1,4 @@
|
||||
.vue-flow__node.dark {
|
||||
background: #000000;
|
||||
color: #ffffff;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
export function getElements(xElements = 10, yElements = 10) {
|
||||
const initialNodes = []
|
||||
const initialEdges = []
|
||||
let nodeId = 1
|
||||
let recentNodeId = null
|
||||
|
||||
for (let y = 0; y < yElements; y++) {
|
||||
for (let x = 0; x < xElements; x++) {
|
||||
const position = { x: x * 75, y: y * 75 }
|
||||
const node = {
|
||||
id: nodeId.toString(),
|
||||
style: { width: `50px`, fontSize: `11px`, zIndex: 1 },
|
||||
label: `Node ${nodeId}`,
|
||||
class: 'light',
|
||||
position,
|
||||
}
|
||||
initialNodes.push(node)
|
||||
|
||||
if (recentNodeId && nodeId <= xElements * yElements) {
|
||||
initialEdges.push({
|
||||
id: `${x}-${y}`,
|
||||
source: recentNodeId.toString(),
|
||||
target: nodeId.toString(),
|
||||
style: (edge) => {
|
||||
if (!edge.sourceNode.selected && !edge.targetNode.selected) return
|
||||
return { stroke: '#10b981', strokeWidth: 3 }
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
recentNodeId = nodeId
|
||||
nodeId++
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
nodes: initialNodes,
|
||||
edges: initialEdges,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<script setup>
|
||||
import { VueFlow } from '@braks/vue-flow'
|
||||
import { ref } from 'vue'
|
||||
import Sidebar from './Sidebar.vue'
|
||||
import TeleportableNode from './TeleportableNode.vue'
|
||||
|
||||
const elements = ref([
|
||||
{
|
||||
id: '1',
|
||||
label: 'Click to teleport',
|
||||
type: 'teleportable',
|
||||
position: { x: 125, y: 0 },
|
||||
data: {},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
label: 'Click to teleport',
|
||||
type: 'teleportable',
|
||||
position: { x: 350, y: 200 },
|
||||
data: {},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
label: 'Click to teleport',
|
||||
type: 'teleportable',
|
||||
position: { x: 0, y: 200 },
|
||||
data: {},
|
||||
},
|
||||
{
|
||||
id: 'e1-2',
|
||||
source: '1',
|
||||
target: '2',
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="teleportflow">
|
||||
<VueFlow v-model="elements" :fit-view-on-init="true">
|
||||
<template #node-teleportable="props">
|
||||
<TeleportableNode v-bind="props" />
|
||||
</template>
|
||||
</VueFlow>
|
||||
<Sidebar />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,10 @@
|
||||
<script setup>
|
||||
//
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside>
|
||||
<div class="description">Teleport destination</div>
|
||||
<div id="port" class="port"></div>
|
||||
</aside>
|
||||
</template>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script setup>
|
||||
import { Handle, Position } from '@braks/vue-flow'
|
||||
import { useTransition } from './useTransition.js'
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const { animation, transition, teleport, onClick } = useTransition(props.id)
|
||||
|
||||
const changeAnimation = () => {
|
||||
animation.value = animation.value === 'fade' ? 'shrink' : 'fade'
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<teleport :disabled="!teleport" :to="teleport">
|
||||
<transition :name="animation">
|
||||
<div v-if="!transition" class="teleportable">
|
||||
<Handle type="target" :position="Position.Top" />
|
||||
[Node {{ id }}]
|
||||
<div class="buttons">
|
||||
<div v-if="teleport !== '#port'" class="button" @click.prevent="onClick('#port')">Teleport To Sidebar</div>
|
||||
<div v-if="teleport !== null" class="button" @click.prevent="onClick(null)">Teleport To Main Graph</div>
|
||||
<div class="button" @click.prevent="changeAnimation">Animation: {{ animation }}</div>
|
||||
</div>
|
||||
<Handle type="source" :position="Position.Bottom" />
|
||||
</div>
|
||||
</transition>
|
||||
</teleport>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
export { default as TeleportApp } from './App.vue?raw'
|
||||
export { default as TeleportSidebar } from './Sidebar.vue?raw'
|
||||
export { default as TeleportableNode } from './TeleportableNode.vue?raw'
|
||||
export { default as TeleportableUseTransition } from './useTransition.js?raw'
|
||||
export { default as TeleportCSS } from './style.css'
|
||||
@@ -0,0 +1,125 @@
|
||||
.teleportflow {
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.teleportflow aside {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
border-right: 1px solid #eee;
|
||||
padding: 10px 10px;
|
||||
font-size: 12px;
|
||||
background: rgba(16, 185, 129, 0.75);
|
||||
-webkit-box-shadow: 0px 5px 10px 0px rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0px 5px 10px 0px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.teleportflow aside .port > * {
|
||||
position: relative;
|
||||
margin-bottom: 10px;
|
||||
cursor: grab;
|
||||
font-weight: 500;
|
||||
-webkit-box-shadow: 5px 5px 10px 2px rgba(0, 0, 0, 0.25);
|
||||
box-shadow: 5px 5px 10px 2px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.teleportflow aside .description {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.teleportflow .vue-flow-wrapper {
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 640px) {
|
||||
.teleportflow {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.teleportflow aside {
|
||||
min-width: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 639px) {
|
||||
.teleportflow aside .port {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.teleportable {
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid black;
|
||||
border-radius: 10px;
|
||||
color: black
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 5px;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.button {
|
||||
background-color: whitesmoke;
|
||||
cursor: pointer;
|
||||
padding: 5px 10px;
|
||||
border: 1px solid black;
|
||||
border-radius: 10px;
|
||||
color: black;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.shrink-leave-active {
|
||||
animation: shrink 0.5s;
|
||||
}
|
||||
|
||||
.shrink-enter-active {
|
||||
animation: grow 0.5s;
|
||||
}
|
||||
|
||||
@keyframes grow {
|
||||
from {
|
||||
transform: scale(0.1);
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shrink {
|
||||
from {
|
||||
transform: scale(1);
|
||||
}
|
||||
to {
|
||||
transform: scale(0.1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { getConnectedEdges, useVueFlow } from '@braks/vue-flow'
|
||||
import { nextTick, ref } from 'vue'
|
||||
|
||||
/**
|
||||
* Utility composable for specifying animations
|
||||
*
|
||||
* Animations that resize a node need to call the `updateNodeDimensions` function from store to update node handle positions
|
||||
* Otherwise edges do not connect properly
|
||||
*/
|
||||
export const useTransition = (id) => {
|
||||
const animation = ref('fade')
|
||||
const transition = ref(false)
|
||||
const teleport = ref(null)
|
||||
|
||||
const { updateNodeInternals, getNode, edges } = useVueFlow()
|
||||
|
||||
/**
|
||||
* specify a selector to teleport to
|
||||
*
|
||||
* teleported elements still behave like they're at their position before,
|
||||
* i.e. if they emit events, they will still emit them up their regular tree
|
||||
*/
|
||||
const fade = (destination, onFinish) => {
|
||||
setTimeout(() => {
|
||||
// teleport to destination or disable teleport
|
||||
teleport.value = destination
|
||||
|
||||
setTimeout(() => {
|
||||
transition.value = false
|
||||
|
||||
// if destination is null, defer hiding edges until node is teleported back
|
||||
if (!destination) {
|
||||
onFinish()
|
||||
}
|
||||
}, 500)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const shrink = (destination, onFinish) => {
|
||||
setTimeout(() => {
|
||||
// teleport to destination or disable teleport
|
||||
teleport.value = destination
|
||||
|
||||
setTimeout(() => {
|
||||
transition.value = false
|
||||
|
||||
setTimeout(() => {
|
||||
// if destination is null, defer hiding edges until node is teleported back
|
||||
if (!destination) {
|
||||
updateNodeInternals([id])
|
||||
|
||||
nextTick(() => {
|
||||
onFinish()
|
||||
})
|
||||
}
|
||||
}, 500)
|
||||
}, 500)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
/**
|
||||
* specify a selector to teleport to
|
||||
*
|
||||
* teleported elements still behave like they're at their position before,
|
||||
* i.e. if they emit events, they will still emit them up their regular tree
|
||||
*/
|
||||
const onClick = (destination) => {
|
||||
const node = getNode.value(id)
|
||||
|
||||
transition.value = true
|
||||
|
||||
// save current teleport destination to data of node
|
||||
node.data.destination = destination
|
||||
|
||||
// hide connected edges when teleporting
|
||||
const connectedEdges = getConnectedEdges([node], edges.value)
|
||||
|
||||
// if destination is not null, hide edges immediately
|
||||
// check if nodes connected to edge are teleported and hide edge if one of them is
|
||||
if (destination) {
|
||||
connectedEdges.forEach(
|
||||
(edge) => (edge.hidden = !!getNode.value(edge.source).data.destination || !!getNode.value(edge.target).data.destination),
|
||||
)
|
||||
}
|
||||
|
||||
const onFinish = () => {
|
||||
connectedEdges.forEach(
|
||||
(edge) => (edge.hidden = !!getNode.value(edge.source).data.destination || !!getNode.value(edge.target).data.destination),
|
||||
)
|
||||
}
|
||||
|
||||
switch (animation.value) {
|
||||
case 'fade':
|
||||
fade(destination, onFinish)
|
||||
break
|
||||
case 'shrink':
|
||||
shrink(destination, onFinish)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
animation,
|
||||
transition,
|
||||
teleport,
|
||||
onClick,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<script setup>
|
||||
import { ConnectionMode, Controls, VueFlow, addEdge, updateEdge } from '@braks/vue-flow'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const elements = ref([
|
||||
{
|
||||
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 onLoad = (flowInstance) => flowInstance.fitView()
|
||||
const onEdgeUpdateStart = (edge) => console.log('start update', edge)
|
||||
const onEdgeUpdateEnd = (edge) => console.log('end update', edge)
|
||||
const onEdgeUpdate = ({ edge, connection }) => {
|
||||
elements.value = updateEdge(edge, connection, elements.value)
|
||||
}
|
||||
const onConnect = (params) => (elements.value = addEdge(params, elements.value))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VueFlow
|
||||
v-model="elements"
|
||||
: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 @@
|
||||
export { default as UpdateEdgeApp } from './App.vue?raw'
|
||||
@@ -0,0 +1,48 @@
|
||||
<script setup>
|
||||
import { VueFlow, useVueFlow } from '@braks/vue-flow'
|
||||
import { reactive } from 'vue'
|
||||
|
||||
const defaultLabel = '-'
|
||||
const { onPaneReady, getNode } = useVueFlow({
|
||||
nodes: [
|
||||
{ id: '1', label: defaultLabel, position: { x: 100, y: 100 } },
|
||||
{ id: '2', label: 'Node 2', position: { x: 100, y: 200 } },
|
||||
],
|
||||
edges: [{ id: 'e1-2', source: '1', target: '2' }],
|
||||
})
|
||||
|
||||
const opts = reactive({
|
||||
bg: '#eeeeee',
|
||||
label: 'Node 1',
|
||||
hidden: false,
|
||||
})
|
||||
|
||||
const updateNode = () => {
|
||||
const node = getNode.value('1')
|
||||
node.label = opts.label.trim() !== '' ? opts.label : defaultLabel
|
||||
node.style = { backgroundColor: opts.bg }
|
||||
node.hidden = opts.hidden
|
||||
}
|
||||
|
||||
onPaneReady(({ fitView }) => {
|
||||
fitView()
|
||||
updateNode()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VueFlow>
|
||||
<div class="updatenode__controls">
|
||||
<label>label:</label>
|
||||
<input v-model="opts.label" @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>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as UpdateNodeApp } from './App.vue?raw'
|
||||
export { default as UpdateNodeCSS } from './style.css'
|
||||
@@ -0,0 +1,30 @@
|
||||
.updatenode__controls {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
z-index: 4;
|
||||
font-size: 11px;
|
||||
background-color: lightgray;
|
||||
border-radius: 10px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.updatenode__controls label {
|
||||
display: blocK;
|
||||
}
|
||||
|
||||
.updatenode__controls input {
|
||||
padding: 2px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.updatenode__bglabel {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.updatenode__checkboxwrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<script setup>
|
||||
import { VueFlow, addEdge } from '@braks/vue-flow'
|
||||
import { ref } from 'vue'
|
||||
import CustomInput from './CustomInput.vue'
|
||||
import CustomNode from './CustomNode.vue'
|
||||
|
||||
const elements = ref([
|
||||
{ id: '0', type: 'custominput', position: { x: 0, y: 150 }, isValidTargetPos: (connection) => connection.target === 'B' },
|
||||
{
|
||||
id: 'A',
|
||||
type: 'custom',
|
||||
position: { x: 250, y: 0 },
|
||||
isValidSourcePos: () => false,
|
||||
},
|
||||
{ id: 'B', type: 'custom', position: { x: 250, y: 150 }, isValidSourcePos: (connection) => connection.target === 'B' },
|
||||
{ id: 'C', type: 'custom', position: { x: 250, y: 300 }, isValidSourcePos: (connection) => connection.target === 'B' },
|
||||
])
|
||||
|
||||
const onLoad = (flowInstance) => flowInstance.fitView()
|
||||
const onConnectStart = ({ nodeId, handleType }) => console.log('on connect start', { nodeId, handleType })
|
||||
const onConnectStop = (event) => console.log('on connect stop', event)
|
||||
const onConnectEnd = (event) => console.log('on connect end', event)
|
||||
|
||||
const onConnect = (params) => {
|
||||
console.log('on connect', params)
|
||||
addEdge(params, elements.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VueFlow
|
||||
class="validationflow"
|
||||
v-model="elements"
|
||||
@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-custom="props">
|
||||
<CustomNode v-bind="props" />
|
||||
</template>
|
||||
</VueFlow>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup>
|
||||
import { Handle, Position } from '@braks/vue-flow'
|
||||
|
||||
const props = defineProps({
|
||||
isValidTargetPos: {
|
||||
type: Function,
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<script>
|
||||
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,25 @@
|
||||
<script setup>
|
||||
import { Handle, Position } from '@braks/vue-flow'
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isValidSourcePos: {
|
||||
type: Function,
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<script>
|
||||
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,4 @@
|
||||
export { default as ValidationApp } from './App.vue?raw'
|
||||
export { default as ValidationCustomInput } from './CustomInput.vue?raw'
|
||||
export { default as ValidationCustomNode } from './CustomNode.vue?raw'
|
||||
export { default as ValidationCSS } from './style.css'
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
.validationflow .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