docs: replace codesandbox embed with repl (#122)

* docs: Animations

* docs: add confetti gun

* docs: update basic example on home page

* docs(deps): Add stackblitz sdk to deps

* chore: update yarn.lock

* docs: use vue repl

* docs: remove stackblitz sdk

* docs: basic repl example

* docs: use repl for examples/index.md (basic example)

* docs: pass ext to repl

* docs: add copy plugin

* docs: use import map instead of dynamic imports

* docs: use repl for custom node example

* docs: hide repl errors

* docs: use repl for custom connectionline example

* docs: use repl for edges example

* docs: exclude repl from ssr

* docs: use repl for nested example

* docs: use repl for stress example

* docs: rename customNode to custom-node

* docs: use repl for update-edge example

* docs: use repl for update-node example

* docs: use repl for validation example

* docs: use repl for save-restore example

* docs: scale down minimap in repl examples

* docs: use repl for dnd example

* docs: use repl for empty example

* docs: use repl for hidden example

* docs: use repl for interaction example

* docs: use repl for multi example

* docs: add pinia example with stackblitz

* docs: update basic example

* docs: update examples

* docs: remove transition from intro

* docs: update features

* update: README.md

* docs: scope css
This commit is contained in:
Braks
2022-05-26 19:28:25 +02:00
parent 6494b65b3e
commit 98a893108c
95 changed files with 2342 additions and 321 deletions
+107
View File
@@ -0,0 +1,107 @@
<script lang="ts" setup>
import { Repl, ReplStore } from '@vue/repl'
import pkg from '../package.json'
import '@vue/repl/style.css'
import { exampleImports } from './examples'
const props = defineProps<{ example: keyof typeof exampleImports; mainFile?: string; dependencies?: Record<string, string> }>()
let css = `@import 'https://cdn.jsdelivr.net/npm/@braks/vue-flow@${pkg.dependencies['@braks/vue-flow']}/dist/style.css';
@import 'https://cdn.jsdelivr.net/npm/@braks/vue-flow@${pkg.dependencies['@braks/vue-flow']}/dist/theme-default.css';
html,
body,
#app {
margin: 0;
height: 100%;
}
#app {
text-transform: uppercase;
font-family: 'JetBrains Mono', monospace;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
.vue-flow__minimap {
transform: scale(50%)
transform-origin: bottom right;
}`
const store = new ReplStore({
showOutput: true,
outputMode: 'preview',
})
watchEffect(
() => {
document.documentElement.style.setProperty('--vh', `${window.innerHeight}px`)
},
{ flush: 'post' },
)
const files: any = {}
const imports = exampleImports[props.example]
for (const example of Object.keys(imports)) {
if (example.includes('css')) {
css += `${imports[example as keyof typeof imports]}`
} else {
files[example] = imports[example as keyof typeof imports]
}
}
await store.setFiles(
{
...files,
'main.css': css,
},
props.mainFile ?? 'App.vue',
)
// pre-set import map
store.setImportMap({
imports: {
'@braks/vue-flow': `${location.origin}/vue-flow.es.js`,
},
})
store.setVueVersion('3.2.25')
const sfcOptions = {
script: {
reactivityTransform: true,
},
}
</script>
<template>
<Repl
:clear-console="true"
:auto-resize="true"
:store="store"
:show-compile-output="false"
:show-import-map="false"
:sfc-options="sfcOptions"
@keydown.ctrl.s.prevent
@keydown.meta.s.prevent
/>
</template>
<style>
.file-selector {
@apply scrollbar scrollbar-thin scrollbar-thumb-rounded scrollbar-thumb-green-500 scrollbar-track-black;
}
.vue-repl {
@apply rounded-lg border-1 border-solid dark:border-gray-200/10 border-gray-200;
height: calc(var(--vh) - var(--navbar-height));
}
.msg.err {
display: none;
}
</style>
+84
View File
@@ -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, instance, addEdges } = 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(instance.value?.toObject())
/**
* Resets the current viewpane transformation (zoom & pan)
*/
const resetTransform = () => instance.value?.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>
+3
View File
@@ -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' },
]
+30
View File
@@ -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,82 @@
<script setup>
import { Background, ConnectionMode, MiniMap, Position, VueFlow } from '@braks/vue-flow'
import { h, onMounted, ref } from 'vue'
import ColorSelectorNode from './CustomNode.vue'
import { presets } from './presets.js'
const elements = ref([])
const bgColor = ref(presets.ayame)
const bgName = ref('AYAME')
const connectionLineStyle = { stroke: '#fff' }
const snapGrid = [16, 16]
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) => {
bgColor.value = color.value
bgName.value = color.name
}
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: 175 },
targetPosition: Position.Left,
},
{ id: 'e1a-2', source: '1', sourceHandle: 'a', target: '2', animated: true, style: { stroke: '#fff' } },
{ id: 'e1b-3', source: '1', sourceHandle: 'b', target: '3', animated: true, style: { stroke: '#fff' } },
]
})
</script>
<template>
<VueFlow
v-model="elements"
:style="{ backgroundColor: bgColor }"
:connection-mode="ConnectionMode.Loose"
:connection-line-style="connectionLineStyle"
:snap-to-grid="true"
:snap-grid="snapGrid"
:default-zoom="1.5"
:fit-view-on-init="true"
>
<template #node-custom="props">
<ColorSelectorNode v-bind="props" @change="onChange" />
</template>
<Background :size="0.7" pattern-color="black" />
<MiniMap :node-stroke-color="nodeStroke" :node-color="nodeColor" />
</VueFlow>
</template>
@@ -0,0 +1,64 @@
<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'])
const targetHandleStyle = { background: '#555' }
const sourceHandleStyleA = { ...targetHandleStyle, top: '10px' }
const sourceHandleStyleB = { ...targetHandleStyle, bottom: '10px', top: 'auto' }
const onConnect = (params) => console.log('handle onConnect', params)
const onSelect = (color) => {
emit('change', color)
}
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)
})
</script>
<script>
export default {
inheritAttrs: false,
}
</script>
<template>
<div v-if="selectedColor">
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 }"
style="padding: 3px; width: 25px; height: 25px"
type="button"
@click="onSelect(color)"
></button>
</template>
</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 = {
byakuroku: '#A8D8B9',
mizu: '#81C7D4',
asagi: '#33A6B8',
ukon: '#EFBB24',
mushikuri: '#D9CD90',
hiwa: '#BEC23F',
ichigo: '#B5495B',
kurenai: '#CB1B45',
syojyohi: '#E83015',
konjyo: '#113285',
fuji: '#8B81C3',
ayame: '#6F3381',
gofun: '#FFFFFB',
torinoko: '#DAC9A6',
kurotsurubami: '#0B1013',
sumi: '#1C1C1C',
ohni: '#F05E1C',
kokikuchinashi: '#FB9966',
beniukon: '#E98B2A',
sakura: '#FEDFE1',
toki: '#EEA9A9',
}
@@ -0,0 +1,12 @@
.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;
}
+47
View File
@@ -0,0 +1,47 @@
<script setup>
import { VueFlow, useVueFlow } from '@braks/vue-flow'
import Sidebar from './Sidebar.vue'
let id = 0
const getId = () => `dndnode_${id++}`
const { instance, onConnect, nodes, edges, addEdges, addNodes, viewport } = 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) => {
if (instance.value) {
const type = event.dataTransfer?.getData('application/vueflow')
const position = instance.value.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>
+19
View File
@@ -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>
+3
View File
@@ -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'
+51
View File
@@ -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;
}
}
+74
View File
@@ -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: '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 } },
{ 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: () => h(CustomEdgeLabel, { label: 'custom label text' }),
labelStyle: { fill: 'red', fontWeight: 700 },
markerEnd: 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: MarkerType.ArrowClosed,
},
{
id: 'e5-8',
source: '5',
target: '8',
type: 'custom',
data: { text: 'custom edge' },
markerEnd: MarkerType.ArrowClosed,
},
{
id: 'e5-9',
source: '5',
target: '9',
type: 'custom2',
data: { text: 'custom edge 2' },
},
])
</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 animated-text-gradient" @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: 'red' }"
: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>
+5
View File
@@ -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'
+34
View File
@@ -0,0 +1,34 @@
.edgebutton {
color: whitesmoke;
border-radius: 999px;
cursor: pointer;
}
.edgebutton:hover {
box-shadow: 0 0 0 2px pink, 0 0 0 4px #f05f75;
}
.animated-text-gradient {
background: linear-gradient(122deg, #6f3381, #81c7d4, #fedfe1, #fffffb);
background-size: 800% 800%;
-webkit-animation: textgradient 4s ease infinite;
-moz-animation: textgradient 4s ease infinite;
animation: textgradient 4s ease infinite;
}
@-webkit-keyframes textgradient {
0%{background-position:0% 22%}
50%{background-position:100% 79%}
100%{background-position:0% 22%}
}
@-moz-keyframes textgradient {
0%{background-position:0% 22%}
50%{background-position:100% 79%}
100%{background-position:0% 22%}
}
@keyframes textgradient {
0%{background-position:0% 22%}
50%{background-position:100% 79%}
100%{background-position:0% 22%}
}
+31
View File
@@ -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>
+1
View File
@@ -0,0 +1 @@
export { default as EmptyApp } from './App.vue?raw'
+41
View File
@@ -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>
+1
View File
@@ -0,0 +1 @@
export { default as HiddenApp } from './App.vue?raw'
+87
View File
@@ -0,0 +1,87 @@
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'
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,
},
}
@@ -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;
}
+10
View File
@@ -0,0 +1,10 @@
<script setup>
import Flow from './Flow.vue'
</script>
<template>
<div class="multiflows">
<Flow />
<Flow />
</div>
</template>
+36
View File
@@ -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>
+3
View File
@@ -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'
+18
View File
@@ -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;
}
+98
View File
@@ -0,0 +1,98 @@
<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,
nodes: [
{ id: '1', type: 'input', label: 'node', position: { x: 250, y: 0 }, class: 'light' },
{
id: '2',
label: 'parent node',
position: { x: 100, y: 100 },
class: 'light',
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: '3', label: 'node', position: { x: 350, y: 100 }, class: 'light' },
{
id: '4',
label: 'parent node',
position: { x: 320, y: 300 },
class: 'light',
style: { backgroundColor: 'rgba(16, 185, 129, 0.5)', width: '300px', height: '300px' },
},
{
id: '4a',
label: 'child node',
position: { x: 15, y: 65 },
class: 'light',
extent: 'parent',
parentNode: '4',
},
{
id: '4b',
label: 'nested parent node',
position: { x: 15, y: 120 },
class: 'light',
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 },
class: 'light',
parentNode: '4b',
},
{
id: '4b2',
label: 'nested child node',
position: { x: 100, y: 100 },
class: 'light',
parentNode: '4b',
},
],
edges: [
{ id: 'e1-2', source: '1', target: '2' },
{ 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: 'Drag me to extend area!',
position: { x: 20, y: 100 },
class: 'light',
expandParent: true,
parentNode: '2',
},
])
})
</script>
<template>
<VueFlow>
<MiniMap />
<Controls />
<Background />
</VueFlow>
</template>
+1
View File
@@ -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-jvtrur', {
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, instance, dimensions } = useVueFlow()
const onSave = () => {
localStorage.setItem(flowKey, JSON.stringify(instance.value?.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)
instance.value.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;
}
+38
View File
@@ -0,0 +1,38 @@
<script setup>
import { VueFlow, isNode, useVueFlow } from '@braks/vue-flow'
import { ref } from 'vue'
import { getElements } from './utils.js'
const { nodes, edges } = getElements(10, 10)
const elements = ref([...nodes, ...edges])
const { onPaneReady } = useVueFlow()
onPaneReady((i) => {
i.fitView({
padding: 0.2,
})
console.log(i.getElements())
})
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() * window.innerWidth,
y: Math.random() * window.innerHeight,
}
}
})
</script>
<template>
<VueFlow v-model="elements">
<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>
+3
View File
@@ -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;
}
+32
View File
@@ -0,0 +1,32 @@
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` },
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() })
}
recentNodeId = nodeId
nodeId++
}
}
return {
nodes: initialNodes,
edges: initialEdges,
}
}
@@ -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;
}
+24 -11
View File
@@ -28,18 +28,31 @@ watch([breakpoints.sm, breakpoints.md, breakpoints.lg, breakpoints.xl, breakpoin
<div
class="flex flex-col divide-y divide-gray-500 md:divide-y-0 gap-12 md:gap-24 lg:gap-36 max-w-9/12 md:max-w-11/12 lg:max-w-9/12 m-auto py-12 md:py-24 text-center md:text-left"
>
<div class="flex flex-col md:flex-row gap-12 md:gap-24">
<Basic @pane="onLoad" />
</div>
<div class="flex flex-col-reverse md:flex-row flex-unwrap gap-12 md:gap-24">
<RGB @pane="onLoad" />
</div>
<div class="flex flex-col md:flex-row flex-unwrap gap-12 md:gap-24">
<Nested @pane="onLoad" />
</div>
<div class="flex flex-col-reverse md:flex-row flex-unwrap gap-12 md:gap-24">
<Additional @pane="onLoad" />
<div ref="basic">
<XyzTransition :appear-visible="true" xyz="fade left ease-out-back">
<div class="flex flex-col md:flex-row gap-12 md:gap-24">
<Basic @pane="onLoad" />
</div>
</XyzTransition>
</div>
<XyzTransition :appear-visible="true" xyz="fade down ease-out-back">
<div class="flex flex-col-reverse md:flex-row flex-unwrap gap-12 md:gap-24">
<RGB @pane="onLoad" />
</div>
</XyzTransition>
<XyzTransition :appear-visible="true" xyz="fade down ease-out-back">
<div class="flex flex-col md:flex-row flex-unwrap gap-12 md:gap-24">
<Nested @pane="onLoad" />
</div>
</XyzTransition>
<XyzTransition :appear-visible="true" xyz="fade down ease-out-back">
<div class="flex flex-col-reverse md:flex-row flex-unwrap gap-12 md:gap-24">
<Additional @pane="onLoad" />
</div>
</XyzTransition>
</div>
</div>
</template>
+4 -2
View File
@@ -18,9 +18,11 @@ onMounted(() => {
magnetic: false,
dotColor: '#10b981',
radius: 8,
focusableElementsOffsetX: 2,
focusableElementsOffsetY: 2,
focusableElementsOffsetX: 5,
focusableElementsOffsetY: 4,
mode: 'bouncy',
focusableElements:
'[data-blobity], a:not([data-no-blobity]), button:not([data-no-blobity]), [data-blobity-tooltip], .back-to-top, .intro',
})
onBeforeUnmount(() => {
+29 -12
View File
@@ -1,5 +1,6 @@
<script lang="ts" setup>
import { GraphEdge, useVueFlow, VueFlow, Background, Controls, ClassFunc } from '@braks/vue-flow'
import { Background, ClassFunc, ConnectionLineType, Controls, GraphEdge, useVueFlow, VueFlow } from "@braks/vue-flow";
import Cross from "~icons/mdi/window-close";
const getClass: ClassFunc = (el) => {
const classes = ['font-semibold', '!border-2', 'transition-colors', 'duration-300', 'ease-in-out']
@@ -13,26 +14,31 @@ const getClass: ClassFunc = (el) => {
const emit = defineEmits(['pane'])
const { onPaneReady } = useVueFlow({
const { onPaneReady, onConnect, addEdges, viewport } = useVueFlow({
connectionLineType: ConnectionLineType.SmoothStep,
connectionLineStyle: {
strokeDasharray: 5,
animation: 'dashdraw 0.5s linear infinite',
},
modelValue: [
{
id: '1',
type: 'input',
label: 'input',
label: 'Start',
position: { x: 250, y: 5 },
class: getClass,
},
{
id: '2',
label: 'default',
label: 'Waypoint',
position: { x: 100, y: 100 },
class: getClass,
},
{ id: '3', label: 'default', position: { x: 400, y: 100 }, class: getClass },
{ id: '3', label: 'Waypoint', position: { x: 400, y: 100 }, class: getClass },
{
id: '4',
type: 'output',
label: 'output',
label: 'End',
position: { x: 250, y: 225 },
class: getClass,
},
@@ -93,14 +99,21 @@ const { onPaneReady } = useVueFlow({
})
onPaneReady((i) => emit('pane', i))
onConnect((param) => {
addEdges([{
...param,
type: 'smoothstep',
animated: true,
}])
})
</script>
<template>
<div class="md:max-w-1/3 flex flex-col justify-center">
<div class="flex flex-col gap-2 items-center md:items-start">
<h1>Batteries-included</h1>
<div class="flex flex-col items-center md:items-start">
<h1>Interactive Graphs</h1>
<p>
Vue Flow comes with several built-in features including zooming, panning, zoom & pan controls, single & multi-selections, element dragging,
node and edge components and event handlers.
Vue Flow comes with built-in features like zoom & pan and dedicated controls, single & multi-selections, draggable elements,
customizable nodes and edges and a bunch of event handlers.
</p>
<router-link class="button max-w-max" to="/guide/"> Documentation </router-link>
</div>
@@ -109,8 +122,12 @@ onPaneReady((i) => emit('pane', i))
class="w-full h-[300px] md:min-h-[400px] shadow-xl rounded-xl font-mono uppercase border-1 border-secondary overflow-hidden"
>
<VueFlow class="basic">
<Controls class="md:!left-auto md:!right-[10px]" />
<Background pattern-color="#aaa" :gap="16" />
<Controls class="md:(!left-auto !right-[10px])" />
<Background pattern-color="#aaa" :gap="60">
<template #pattern>
<Cross :style="{ fontSize: `${8 * viewport.zoom || 1}px` }" class="text-[#10b981] opacity-50" />
</template>
</Background>
</VueFlow>
</div>
</template>
+94 -57
View File
@@ -1,11 +1,17 @@
<script lang="ts" setup>
import { Handle, Position, VueFlow, useVueFlow } from '@braks/vue-flow'
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
import BoxNode from '../nodes/Box.vue'
import confetti from 'canvas-confetti'
import colors from 'windicss/colors'
import { cheer, fireworks } from './confetti'
import Heart from '~icons/mdi/heart'
const breakpoints = useBreakpoints(breakpointsTailwind)
const dark = useDark({
selector: 'html',
})
const initialEdges = [
{
id: 'eintro-examples',
@@ -35,7 +41,7 @@ const initialEdges = [
style: { strokeWidth: 2, stroke: '#0ea5e9' },
},
]
const { dimensions, instance, onPaneReady, getNodes, getNode, getEdge, updateEdge, edges, setEdges } = useVueFlow({
const { dimensions, instance, onNodeClick, onPaneReady, getNodes, getNode, getEdge, updateEdge, edges, setEdges } = useVueFlow({
nodes: [
{ id: 'intro', type: 'box', position: { x: 0, y: 0 } },
{ id: 'examples', type: 'box', position: { x: -50, y: 400 } },
@@ -47,10 +53,54 @@ const { dimensions, instance, onPaneReady, getNodes, getNode, getEdge, updateEdg
nodesDraggable: false,
panOnDrag: false,
zoomOnScroll: false,
zoomOnDoubleClick: false,
zoomOnPinch: false,
})
const clickInterval = ref()
const clicks = ref(0)
const disabled = ref(false)
const confettiColors = Object.values(colors).flatMap((color) => {
if (typeof color === 'string') return color
else return Object.values(color).flatMap((c) => c)
})
onNodeClick(async ({ node }) => {
if (node.id === 'intro') {
if (disabled.value) return
if (clickInterval.value) {
clearInterval(clickInterval.value)
}
clickInterval.value = setInterval(() => {
clearInterval(clickInterval.value)
clicks.value = 0
}, 1000)
clicks.value++
if (clicks.value < 10) {
confetti({
startVelocity: 25,
spread: 360,
angle: 270,
origin: { y: 0.1 },
colors: confettiColors,
})
} else if (clicks.value < 20) {
await cheer(['#10b981', dark.value ? '#ffffff' : '#000000'])
} else if (clicks.value === 20) {
disabled.value = true
await fireworks(confettiColors)
disabled.value = false
} else {
clicks.value = 0
}
}
})
const init = ref(false)
const edgeId = ref('eintro-documentation')
const el = templateRef<HTMLDivElement>('el', null)
onPaneReady(({ fitView }) => {
@@ -83,40 +133,34 @@ onPaneReady(({ fitView }) => {
}
})
nextTick(() => {
setEdges(() => {
return [
{
id: 'eintro-examples',
type: 'smoothstep',
sourceHandle: 'a',
source: 'intro',
target: 'examples',
animated: true,
style: { strokeWidth: 2, stroke: '#8b5cf6' },
},
{
id: 'eexamples-documentation',
type: 'smoothstep',
source: 'examples',
target: 'documentation',
animated: true,
style: { strokeWidth: 2, stroke: '#f97316' },
},
{
id: 'edocumentation-acknowledgement',
type: 'smoothstep',
source: 'documentation',
target: 'acknowledgement',
animated: true,
style: { strokeWidth: 2, stroke: '#0ea5e9' },
},
]
})
fitView({
duration: 1500,
})
setEdges(() => {
return [
{
id: 'eintro-examples',
type: 'smoothstep',
sourceHandle: 'a',
source: 'intro',
target: 'examples',
animated: true,
style: { strokeWidth: 2, stroke: '#8b5cf6' },
},
{
id: 'eexamples-documentation',
type: 'smoothstep',
source: 'examples',
target: 'documentation',
animated: true,
style: { strokeWidth: 2, stroke: '#f97316' },
},
{
id: 'edocumentation-acknowledgement',
type: 'smoothstep',
source: 'documentation',
target: 'acknowledgement',
animated: true,
style: { strokeWidth: 2, stroke: '#0ea5e9' },
},
]
})
} else {
getNodes.value.forEach((node) => {
@@ -143,24 +187,16 @@ onPaneReady(({ fitView }) => {
}
})
nextTick(() => {
setEdges(initialEdges)
fitView({
duration: 1500,
})
})
setEdges(initialEdges)
}
setTimeout(
() => {
if (!init.value) init.value = true
else instance.value?.fitView()
},
init.value ? 5 : 1505,
)
nextTick(() => {
fitView()
if (!init.value) init.value = true
})
}
useResizeObserver(el, setNodes)
useResizeObserver(el, useDebounceFn(setNodes, 5))
})
const scrollTo = () => {
@@ -175,34 +211,35 @@ const scrollTo = () => {
<VueFlow ref="el" class="dark:bg-black bg-white transition-colors duration-200 ease-in-out">
<template #node-box="props">
<template v-if="props.id === 'intro'">
<div class="max-w-[500px]">
<BoxNode class="intro">
<div class="box max-w-[500px]">
<div class="intro px-4 py-2 shadow-lg rounded-md border-2 border-solid border-black">
<div class="font-mono flex flex-col gap-4 p-4 items-center text-center">
<h1 class="text-2xl lg:text-4xl">Vue Flow</h1>
<h2 class="text-lg lg:text-xl font-normal !border-0">
The customizable Vue 3 component bringing interactivity to flowcharts and graphs.
</h2>
</div>
</BoxNode>
<Handle id="a" type="source" :position="Position.Bottom" />
</div>
</div>
</template>
<template v-else-if="props.id === 'documentation'">
<div class="flex">
<router-link class="link group bg-orange-500" to="/guide/"> Read The Documentation </router-link>
<router-link class="link group bg-[#f15a16]" to="/guide/"> Read The Documentation </router-link>
</div>
<Handle type="target" :position="Position.Top" />
<Handle class="block md:hidden" type="source" :position="Position.Bottom" />
</template>
<template v-else-if="props.id === 'examples'">
<div class="flex">
<router-link class="link group bg-purple-500" to="/examples/"> Check The Examples </router-link>
<router-link class="link group bg-[#ef467e]" to="/examples/"> Check The Examples </router-link>
</div>
<Handle type="target" :position="Position.Top" />
<Handle class="block md:hidden" type="source" :position="Position.Bottom" />
</template>
<template v-else-if="props.id === 'acknowledgement'">
<div class="flex" @click="scrollTo">
<div class="link group bg-sky-500"><Heart class="text-red-500" /> Acknowledgement</div>
<button class="link group bg-sky-500"><Heart class="text-red-500" /> Acknowledgement</button>
</div>
<Handle type="target" :position="Position.Top" />
</template>
+1 -1
View File
@@ -72,7 +72,7 @@ onPaneReady((i) => emit('pane', i))
class="w-full h-[300px] md:min-h-[400px] shadow-xl rounded-xl font-mono uppercase border-1 border-green-500 overflow-hidden"
>
<VueFlow class="nested">
<Controls class="md:!left-auto md:!right-[10px]" />
<Controls class="md:(!left-auto !right-[10px])" />
<Background pattern-color="#aaa" class="!bg-gray-800" :gap="18" />
</VueFlow>
</div>
+69
View File
@@ -0,0 +1,69 @@
import confetti from 'canvas-confetti'
export const fireworks = (colors?: string[]) => {
return new Promise((resolve) => {
const duration = 15 * 1000
const animationEnd = Date.now() + duration
const defaults = { startVelocity: 60, spread: 360, ticks: 20, zIndex: 0 }
function randomInRange(min: number, max: number) {
return Math.random() * (max - min) + min
}
const interval: any = setInterval(function () {
const timeLeft = animationEnd - Date.now()
if (timeLeft <= 0) {
clearInterval(interval)
return resolve(true)
}
const particleCount = 50 * (timeLeft / duration)
// since particles fall down, start a bit higher than random
confetti(
Object.assign({}, defaults, {
particleCount,
origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 },
colors,
}),
)
confetti(
Object.assign({}, defaults, {
particleCount,
origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 },
colors,
}),
)
}, 250)
})
}
export const cheer = (colors: string[]) => {
return new Promise((resolve) => {
const end = Date.now() + 2 * 1000
function frame() {
confetti({
particleCount: 2,
angle: 60,
spread: 75,
origin: { x: 0 },
colors,
})
confetti({
particleCount: 2,
angle: 120,
spread: 75,
origin: { x: 1 },
colors,
})
if (Date.now() < end) {
requestAnimationFrame(frame)
} else {
resolve(true)
}
}
frame()
})
}
-12
View File
@@ -1,12 +0,0 @@
<script lang="ts" setup>
import { Handle, Position } from '@braks/vue-flow'
const emit = defineEmits(['next'])
</script>
<template>
<div class="px-4 py-2 shadow-lg rounded-md border-2 border-solid border-black">
<slot />
<Handle id="a" type="source" :position="Position.Bottom" />
</div>
</template>