@stonecrop/node-editor 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/style.css ADDED
@@ -0,0 +1 @@
1
+ .editable-edge-label{background-color:#fff;position:relative;font-size:12px}.label-input-wrapper{position:absolute;top:0;left:0;right:0;bottom:0;display:flex;align-items:center;justify-content:center}.label-input{text-align:center}.vue-flow{position:relative;width:100%;height:100%;overflow:hidden;z-index:0}.vue-flow__container{position:absolute;height:100%;width:100%;left:0;top:0}.vue-flow__pane{z-index:1}.vue-flow__pane.draggable{cursor:grab}.vue-flow__pane.dragging{cursor:grabbing}.vue-flow__pane.selection{cursor:pointer}.vue-flow__transformationpane{transform-origin:0 0;z-index:2;pointer-events:none}.vue-flow__viewport{z-index:4}.vue-flow__selection{z-index:6}.vue-flow__edge-labels{position:absolute;width:100%;height:100%;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.vue-flow__nodesselection-rect:focus,.vue-flow__nodesselection-rect:focus-visible{outline:none}.vue-flow .vue-flow__edges{pointer-events:none;overflow:visible}.vue-flow__edge-path,.vue-flow__connection-path{stroke:#b1b1b7;stroke-width:1;fill:none}.vue-flow__edge{pointer-events:visibleStroke;cursor:pointer}.vue-flow__edge.animated path{stroke-dasharray:5;animation:dashdraw .5s linear infinite}.vue-flow__edge.inactive{pointer-events:none}.vue-flow__edge.selected,.vue-flow__edge:focus,.vue-flow__edge:focus-visible{outline:none}.vue-flow__edge.selected .vue-flow__edge-path,.vue-flow__edge:focus .vue-flow__edge-path,.vue-flow__edge:focus-visible .vue-flow__edge-path{stroke:#555}.vue-flow__edge-textwrapper{pointer-events:all}.vue-flow__edge-text{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.vue-flow__connection{pointer-events:none}.vue-flow__connection .animated{stroke-dasharray:5;animation:dashdraw .5s linear infinite}.vue-flow__connectionline{z-index:1001}.vue-flow__nodes{pointer-events:none;transform-origin:0 0}.vue-flow__node-default,.vue-flow__node-input,.vue-flow__node-output{border-width:1px;border-style:solid;border-color:#bbb}.vue-flow__node-default.selected,.vue-flow__node-default:focus,.vue-flow__node-default:focus-visible,.vue-flow__node-input.selected,.vue-flow__node-input:focus,.vue-flow__node-input:focus-visible,.vue-flow__node-output.selected,.vue-flow__node-output:focus,.vue-flow__node-output:focus-visible{outline:none;border:1px solid #555}.vue-flow__node{position:absolute;-webkit-user-select:none;-moz-user-select:none;user-select:none;pointer-events:all;transform-origin:0 0;box-sizing:border-box;cursor:grab}.vue-flow__node.dragging{cursor:grabbing}.vue-flow__nodesselection{z-index:3;transform-origin:left top;pointer-events:none}.vue-flow__nodesselection-rect{position:absolute;pointer-events:all;cursor:grab}.vue-flow__nodesselection-rect.dragging{cursor:grabbing}.vue-flow__handle{position:absolute;pointer-events:none;min-width:5px;min-height:5px}.vue-flow__handle.connectable{pointer-events:all;cursor:crosshair}.vue-flow__handle-bottom{top:auto;left:50%;bottom:-4px;transform:translate(-50%)}.vue-flow__handle-top{left:50%;top:-4px;transform:translate(-50%)}.vue-flow__handle-left{top:50%;left:-4px;transform:translateY(-50%)}.vue-flow__handle-right{right:-4px;top:50%;transform:translateY(-50%)}.vue-flow__edgeupdater{cursor:move;pointer-events:all}.vue-flow__panel{position:absolute;z-index:5;margin:15px}.vue-flow__panel.top{top:0}.vue-flow__panel.bottom{bottom:0}.vue-flow__panel.left{left:0}.vue-flow__panel.right{right:0}.vue-flow__panel.center{left:50%;transform:translate(-50%)}@keyframes dashdraw{0%{stroke-dashoffset:10}}:root{--vf-node-bg: #fff;--vf-node-text: #222;--vf-connection-path: #b1b1b7;--vf-handle: #555}.vue-flow__edge.updating .vue-flow__edge-path{stroke:#777}.vue-flow__edge-text{font-size:10px}.vue-flow__edge-textbg{fill:#fff}.vue-flow__connection-path{stroke:var(--vf-connection-path)}.vue-flow__node{cursor:grab}.vue-flow__node.selectable:focus,.vue-flow__node.selectable:focus-visible{outline:none}.vue-flow__node-default,.vue-flow__node-input,.vue-flow__node-output{padding:10px;border-radius:3px;width:150px;font-size:12px;text-align:center;border-width:1px;border-style:solid;color:var(--vf-node-text);background-color:var(--vf-node-bg);border-color:var(--vf-node-color)}.vue-flow__node-default.selected,.vue-flow__node-default.selected:hover,.vue-flow__node-input.selected,.vue-flow__node-input.selected:hover,.vue-flow__node-output.selected,.vue-flow__node-output.selected:hover{box-shadow:0 0 0 .5px var(--vf-box-shadow)}.vue-flow__node-default .vue-flow__handle,.vue-flow__node-input .vue-flow__handle,.vue-flow__node-output .vue-flow__handle{background:var(--vf-handle)}.vue-flow__node-default.selectable:hover,.vue-flow__node-input.selectable:hover,.vue-flow__node-output.selectable:hover{box-shadow:0 1px 4px 1px #00000014}.vue-flow__node-input{--vf-node-color: var(--vf-node-color, #0041d0);--vf-handle: var(--vf-node-color, #0041d0);--vf-box-shadow: var(--vf-node-color, #0041d0);background:var(--vf-node-bg);border-color:var(--vf-node-color, #0041d0)}.vue-flow__node-input.selected,.vue-flow__node-input:focus,.vue-flow__node-input:focus-visible{outline:none;border:1px solid var(--vf-node-color, #0041d0)}.vue-flow__node-default{--vf-handle: var(--vf-node-color, #1a192b);--vf-box-shadow: var(--vf-node-color, #1a192b);background:var(--vf-node-bg);border-color:var(--vf-node-color, #1a192b)}.vue-flow__node-default.selected,.vue-flow__node-default:focus,.vue-flow__node-default:focus-visible{outline:none;border:1px solid var(--vf-node-color, #1a192b)}.vue-flow__node-output{--vf-handle: var(--vf-node-color, #ff0072);--vf-box-shadow: var(--vf-node-color, #ff0072);background:var(--vf-node-bg);border-color:var(--vf-node-color, #ff0072)}.vue-flow__node-output.selected,.vue-flow__node-output:focus,.vue-flow__node-output:focus-visible{outline:none;border:1px solid var(--vf-node-color, #ff0072)}.vue-flow__nodesselection-rect,.vue-flow__selection{background:rgba(0,89,220,.08);border:1px dotted rgba(0,89,220,.8)}.vue-flow__nodesselection-rect:focus,.vue-flow__nodesselection-rect:focus-visible,.vue-flow__selection:focus,.vue-flow__selection:focus-visible{outline:none}.vue-flow__handle{width:6px;height:6px;background:var(--vf-handle);border:1px solid #fff;border-radius:100%}.chart-controls-left,.chart-controls-right{height:1.8em;display:flex;flex-direction:row;align-items:center;padding-top:.2em}.chart-controls-right div{margin-left:5px}.chart-controls{padding-left:20px;padding-right:20px;height:1.8em;border-bottom:1px solid #ccc;display:flex;flex-direction:row;justify-content:space-between}.chart-controls div{margin-bottom:5px}.defaultContainerClass{height:90vh;width:100%;border:1px solid #ccc}.default-input-node.vue-flow__node-input,.default-output-node.vue-flow__node-output{border-color:#000}.default-input-node.vue-flow__node-input .vue-flow__handle,.default-output-node.vue-flow__node-output .vue-flow__handle{background-color:#000}.default-input-node.vue-flow__node-input.selected,.default-output-node.vue-flow__node-output.selected{box-shadow:0 0 0 .5px #000}button.button-default{background-color:#fff;padding:1px 12px;border-radius:3px;border:1px solid #ccc;cursor:pointer;white-space:nowrap}button.button-default:hover{background-color:#f2f2f2}.vue-flow{background-size:40px 40px;background-image:linear-gradient(to right,#ccc 1px,transparent 1px),linear-gradient(to bottom,#ccc 1px,transparent 1px)}input.label-editor{position:absolute}.node-editor-wrapper{position:relative}
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@stonecrop/node-editor",
3
+ "version": "0.2.5",
4
+ "description": "Node editor UI for Stonecrop",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/node-editor.js",
10
+ "require": "./dist/node-editor.umd.cjs"
11
+ },
12
+ "./styles": "./dist/style.css"
13
+ },
14
+ "main": "index.js",
15
+ "files": [
16
+ "dist/*",
17
+ "src/**/*.vue"
18
+ ],
19
+ "dependencies": {
20
+ "@vue-flow/core": "~1.19.3",
21
+ "vue": "^3.2.47",
22
+ "vue-router": "^4",
23
+ "xstate": "~4.37.2"
24
+ },
25
+ "devDependencies": {
26
+ "@histoire/plugin-vue": "^0.16.1",
27
+ "@vitejs/plugin-vue": "^4.2.1",
28
+ "histoire": "^0.16.1",
29
+ "vite": "^4.3.5"
30
+ },
31
+ "engines": {
32
+ "node": ">=20.11.0"
33
+ },
34
+ "scripts": {
35
+ "build": "vite build",
36
+ "dev": "vite",
37
+ "preview": "vite preview",
38
+ "story:build": "histoire build",
39
+ "story:dev": "histoire dev",
40
+ "story:preview": "histoire preview"
41
+ }
42
+ }
@@ -0,0 +1,105 @@
1
+ <script lang="ts" setup>
2
+ import type { EdgeProps, Position } from '@vue-flow/core'
3
+ import { EdgeLabelRenderer, getBezierPath, useVueFlow } from '@vue-flow/core'
4
+ import type { CSSProperties } from 'vue'
5
+ import { computed, ref, nextTick } from 'vue'
6
+
7
+ interface EditableEdgeProps<T = any> extends EdgeProps<T> {
8
+ id: string
9
+ sourceX: number
10
+ sourceY: number
11
+ targetX: number
12
+ targetY: number
13
+ sourcePosition: Position
14
+ targetPosition: Position
15
+ data: T
16
+ markerEnd: string
17
+ style: CSSProperties
18
+ label: string
19
+ }
20
+
21
+ const props = defineProps<EditableEdgeProps>()
22
+
23
+ const { removeEdges } = useVueFlow()
24
+
25
+ const emit = defineEmits(['change'])
26
+
27
+ const labelInput = ref()
28
+ const newLabel = ref('')
29
+ const showInput = ref(false)
30
+ let lastClick = 0
31
+
32
+ const labelOnClick = () => {
33
+ let now = Date.now()
34
+ if (now - lastClick < 500 && !showInput.value) {
35
+ showLabelInput()
36
+ }
37
+ lastClick = now
38
+ }
39
+
40
+ const showLabelInput = async () => {
41
+ newLabel.value = props.label
42
+ showInput.value = true
43
+ await nextTick()
44
+ labelInput.value.focus()
45
+ }
46
+
47
+ const submitNewLabel = () => {
48
+ showInput.value = false
49
+ emit('change', newLabel.value)
50
+ }
51
+
52
+ const path = computed(() => getBezierPath(props))
53
+ </script>
54
+
55
+ <script lang="ts">
56
+ export default {
57
+ inheritAttrs: false,
58
+ }
59
+ </script>
60
+
61
+ <template>
62
+ <path :id="id" :style="style" class="vue-flow__edge-path" :d="path[0]" :marker-end="markerEnd" />
63
+
64
+ <EdgeLabelRenderer>
65
+ <div
66
+ :style="{
67
+ pointerEvents: 'all',
68
+ position: 'absolute',
69
+ transform: `translate(-50%, -50%) translate(${path[1]}px,${path[2]}px)`,
70
+ }"
71
+ class="nodrag nopan editable-edge-label"
72
+ @click="labelOnClick()">
73
+ <div class="vue-flow__edge-label">{{ props.label }}</div>
74
+ <div v-if="showInput" class="label-input-wrapper">
75
+ <input
76
+ ref="labelInput"
77
+ class="label-input"
78
+ v-model="newLabel"
79
+ @blur="showInput = false"
80
+ @keypress.enter="submitNewLabel" />
81
+ </div>
82
+ </div>
83
+ </EdgeLabelRenderer>
84
+ </template>
85
+
86
+ <style>
87
+ .editable-edge-label {
88
+ background-color: white;
89
+ position: relative;
90
+ font-size: 12px;
91
+ }
92
+ .label-input-wrapper {
93
+ position: absolute;
94
+ top: 0;
95
+ left: 0;
96
+ right: 0;
97
+ bottom: 0;
98
+ display: flex;
99
+ align-items: center;
100
+ justify-content: center;
101
+ }
102
+ .label-input {
103
+ text-align: center;
104
+ }
105
+ </style>
@@ -0,0 +1,83 @@
1
+ <script lang="ts" setup>
2
+ import { NodeProps, Handle, Position, NodeEventsOn } from '@vue-flow/core'
3
+ import { computed, ref, nextTick } from 'vue'
4
+
5
+ interface EditableNodeProps extends NodeProps {
6
+ id: string
7
+ label: string
8
+ sourcePosition: Position
9
+ targetPosition: Position
10
+ data: any
11
+ }
12
+
13
+ const props = defineProps<EditableNodeProps>()
14
+
15
+ const emit = defineEmits(['change'])
16
+
17
+ const positionMap = {
18
+ top: Position.Top,
19
+ right: Position.Right,
20
+ bottom: Position.Bottom,
21
+ left: Position.Left,
22
+ }
23
+
24
+ const sourcePosition = computed(() => positionMap[props.sourcePosition])
25
+ const targetPosition = computed(() => positionMap[props.targetPosition])
26
+
27
+ const labelInput = ref()
28
+ const newLabel = ref('')
29
+ const showInput = ref(false)
30
+ let lastClick = 0
31
+
32
+ const nodeOnClick = () => {
33
+ let now = Date.now()
34
+ if (now - lastClick < 500 && !showInput.value) {
35
+ showLabelInput()
36
+ }
37
+ lastClick = now
38
+ }
39
+
40
+ const showLabelInput = async () => {
41
+ newLabel.value = props.label
42
+ showInput.value = true
43
+ await nextTick()
44
+ labelInput.value.focus()
45
+ }
46
+
47
+ const submitNewLabel = () => {
48
+ showInput.value = false
49
+ emit('change', newLabel.value)
50
+ }
51
+ </script>
52
+
53
+ <template>
54
+ <div @click="nodeOnClick()">
55
+ <div>{{ props.label }}</div>
56
+ <div v-if="showInput" class="label-input-wrapper">
57
+ <input
58
+ ref="labelInput"
59
+ class="label-input"
60
+ v-model="newLabel"
61
+ @blur="showInput = false"
62
+ @keypress.enter="submitNewLabel" />
63
+ </div>
64
+ <Handle v-if="props.data.hasInput" id="a" type="target" :position="targetPosition" />
65
+ <Handle v-if="props.data.hasOutput" id="b" type="source" :position="sourcePosition" />
66
+ </div>
67
+ </template>
68
+
69
+ <style>
70
+ .label-input-wrapper {
71
+ position: absolute;
72
+ top: 0;
73
+ left: 0;
74
+ right: 0;
75
+ bottom: 0;
76
+ display: flex;
77
+ align-items: center;
78
+ justify-content: center;
79
+ }
80
+ .label-input {
81
+ text-align: center;
82
+ }
83
+ </style>
@@ -0,0 +1,330 @@
1
+ <template>
2
+ <div class="node-editor-wrapper" :class="containerClass" @mouseover="hover = true" @mouseleave="hover = false">
3
+ <div class="chart-controls">
4
+ <div class="chart-controls-left">
5
+ <div><b>Selected Node:</b> {{ activeElementKey ? activeElementKey : 'none' }}</div>
6
+ </div>
7
+ <div class="chart-controls-right">
8
+ <div>
9
+ <button class="button-default" @click="addNode()">Add Node</button>
10
+ </div>
11
+ <div>
12
+ <button class="button-default" @click="fitView()">Center</button>
13
+ </div>
14
+ <div v-if="activeElementIndex > -1">
15
+ <button class="button-default" @click="shiftInput()">Shift Input Position</button>
16
+ </div>
17
+ <div v-if="activeElementIndex > -1">
18
+ <button class="button-default" @click="shiftOutput()">Shift Output Position</button>
19
+ </div>
20
+ </div>
21
+ </div>
22
+
23
+ <VueFlow
24
+ @wheel.prevent="onWheel($event)"
25
+ class="nowheel"
26
+ :prevent-scrolling="true"
27
+ :zoom-on-scroll="false"
28
+ :fit-view-on-init="true"
29
+ v-if="vueFlowElements && vueFlowElements.length"
30
+ v-model="vueFlowElements"
31
+ @connect="onConnect($event)"
32
+ @edge-double-click="onEdgeDoubleClick($event)">
33
+ <template #node-editable="props">
34
+ <EditableNode v-bind="props" @change="labelChanged($event, props.id)" />
35
+ </template>
36
+ <template #edge-editable="props">
37
+ <EditableEdge v-bind="props" @change="labelChanged($event, props.id)" />
38
+ </template>
39
+ </VueFlow>
40
+ </div>
41
+ </template>
42
+ <script lang="ts" setup>
43
+ import { VueFlow, useVueFlow } from '@vue-flow/core'
44
+ import '@vue-flow/core/dist/style.css'
45
+ import '@vue-flow/core/dist/theme-default.css'
46
+ import EditableNode from './EditableNode.vue'
47
+ import EditableEdge from './EditableEdge.vue'
48
+ import { ref, computed, defineEmits, onBeforeUnmount, onMounted, watch, reactive } from 'vue'
49
+
50
+ // Props
51
+
52
+ const props = defineProps<{
53
+ modelValue: any
54
+ nodeContainerClass: string
55
+ }>()
56
+
57
+ // Emits
58
+
59
+ const emit = defineEmits(['update:modelValue'])
60
+
61
+ // Data
62
+
63
+ const containerClass = ref('')
64
+ const vueFlowInstance = ref({})
65
+ const hover = ref(false)
66
+ const labelEditor = ref({
67
+ x: 0,
68
+ y: 0,
69
+ })
70
+
71
+ const activeElementKey = ref('')
72
+
73
+ const vueFlowElements = ref([])
74
+
75
+ // Computed variables
76
+
77
+ const activeElementIndex = computed(() => {
78
+ for (let j = 0; j < vueFlowElements.value.length; j++) {
79
+ if (vueFlowElements.value[j].id == activeElementKey.value) return j
80
+ }
81
+ return -1
82
+ })
83
+
84
+ const elements = computed({
85
+ get: () => {
86
+ let _elements = props.modelValue
87
+ if (props.nodeContainerClass) {
88
+ containerClass.value = props.nodeContainerClass
89
+ } else {
90
+ containerClass.value = 'defaultContainerClass'
91
+ }
92
+
93
+ for (let j = 0; j < _elements.length; j++) {
94
+ _elements[j].data = {}
95
+ if (_elements[j].type == 'input') {
96
+ _elements[j].data.hasInput = false
97
+ _elements[j].data.hasOutput = true
98
+ } else if (_elements[j].type == 'output') {
99
+ _elements[j].data.hasInput = true
100
+ _elements[j].data.hasOutput = false
101
+ } else {
102
+ _elements[j].data.hasInput = true
103
+ _elements[j].data.hasOutput = true
104
+ }
105
+ _elements[j].class = 'vue-flow__node-default'
106
+ _elements[j].type = 'editable'
107
+ }
108
+
109
+ for (let j = 0; j < _elements.length; j++) {
110
+ let key = _elements[j].id
111
+ let el = _elements[j]
112
+ _elements[j].events = {
113
+ click: () => {
114
+ activeElementKey.value = key
115
+ },
116
+ }
117
+ }
118
+
119
+ return _elements
120
+ },
121
+ set: newValue => {
122
+ emit('update:modelValue', JSON.parse(JSON.stringify(newValue)))
123
+ },
124
+ })
125
+
126
+ //VueFlow
127
+
128
+ const { getNodes, onPaneReady } = useVueFlow({})
129
+
130
+ onPaneReady(i => {
131
+ vueFlowInstance.value = i
132
+ })
133
+
134
+ // Setup
135
+
136
+ vueFlowElements.value = elements.value
137
+
138
+ // Lifecycle Hooks
139
+
140
+ onMounted(() => {
141
+ document.removeEventListener('keypress', handleKeypress)
142
+ document.addEventListener('keypress', handleKeypress)
143
+ })
144
+
145
+ onBeforeUnmount(() => {
146
+ document.removeEventListener('keypress', handleKeypress)
147
+ })
148
+
149
+ // Methods
150
+
151
+ const shiftTerminal = currentTerminal => {
152
+ return {
153
+ top: 'right',
154
+ right: 'bottom',
155
+ bottom: 'left',
156
+ left: 'top',
157
+ }[currentTerminal]
158
+ }
159
+
160
+ const shiftOutput = () => {
161
+ if (activeElementIndex.value > -1) {
162
+ vueFlowElements.value[activeElementIndex.value].sourcePosition = shiftTerminal(
163
+ vueFlowElements.value[activeElementIndex.value].sourcePosition
164
+ )
165
+ }
166
+ }
167
+
168
+ const shiftInput = () => {
169
+ if (activeElementIndex.value > -1) {
170
+ vueFlowElements.value[activeElementIndex.value].targetPosition = shiftTerminal(
171
+ vueFlowElements.value[activeElementIndex.value].targetPosition
172
+ )
173
+ }
174
+ }
175
+
176
+ const onWheel = $event => {
177
+ window.scrollBy(0, $event.deltaY)
178
+ }
179
+
180
+ const handleKeypress = e => {
181
+ if (hover.value && e.ctrlKey == true) {
182
+ if (e.key == '+' || e.key == '=') {
183
+ vueFlowInstance.value.zoomIn()
184
+ }
185
+ if (e.key == '-') {
186
+ vueFlowInstance.value.zoomOut()
187
+ }
188
+ }
189
+ }
190
+
191
+ const fitView = () => {
192
+ vueFlowInstance.value.fitView()
193
+ }
194
+
195
+ const addNode = () => {
196
+ let newNodePosition = { x: Math.random() * 200, y: Math.random() * 200 }
197
+ let makeEdge = false
198
+ if (activeElementIndex.value > -1) {
199
+ const activeNode = vueFlowElements.value[activeElementIndex.value]
200
+ if (activeNode.data.hasOutput) {
201
+ newNodePosition = { x: activeNode.position.x + 200, y: activeNode.position.y + 50 }
202
+ makeEdge = true
203
+ }
204
+ }
205
+
206
+ let id = vueFlowElements.value.length
207
+ let nodeId = `node-${id}`
208
+ vueFlowElements.value.push({
209
+ id: nodeId,
210
+ label: 'Node ' + id,
211
+ sourcePosition: 'right',
212
+ targetPosition: 'left',
213
+ class: 'vue-flow__node-default',
214
+ type: 'editable',
215
+ data: {
216
+ hasInput: true,
217
+ hasOutput: true,
218
+ },
219
+ events: {
220
+ click: () => {
221
+ activeElementKey.value = nodeId
222
+ },
223
+ },
224
+ // position: { x: Math.random() * vueFlowInstance.value.dimensions.width, y: Math.random() * vueFlowInstance.value.dimensions.height }
225
+ position: newNodePosition,
226
+ })
227
+
228
+ if (makeEdge) {
229
+ let edgeId = `edge-${id + 1}`
230
+ vueFlowElements.value.push({
231
+ id: edgeId,
232
+ source: activeElementKey.value,
233
+ target: nodeId,
234
+ type: 'editable',
235
+ label: `EDGE ${id + 1}`,
236
+ animated: true,
237
+ events: {
238
+ click: () => {
239
+ activeElementKey.value = edgeId
240
+ },
241
+ },
242
+ })
243
+ }
244
+ }
245
+
246
+ const onConnect = e => {
247
+ console.log('edge connect', e)
248
+ }
249
+
250
+ const onEdgeDoubleClick = e => {
251
+ console.log('edge double click', e)
252
+ }
253
+
254
+ const labelChanged = (e, id) => {
255
+ for (let j = 0; j < vueFlowElements.value.length; j++) {
256
+ if (vueFlowElements.value[j].id == id) {
257
+ vueFlowElements.value[j].label = e
258
+ break
259
+ }
260
+ }
261
+ }
262
+ </script>
263
+ <style>
264
+ @import '@vue-flow/core/dist/style.css';
265
+ @import '@vue-flow/core/dist/theme-default.css';
266
+
267
+ .chart-controls-left,
268
+ .chart-controls-right {
269
+ height: 1.8em;
270
+ display: flex;
271
+ flex-direction: row;
272
+ align-items: center;
273
+ padding-top: 0.2em;
274
+ }
275
+ .chart-controls-right div {
276
+ margin-left: 5px;
277
+ }
278
+ .chart-controls {
279
+ padding-left: 20px;
280
+ padding-right: 20px;
281
+ height: 1.8em;
282
+ border-bottom: 1px solid #ccc;
283
+ display: flex;
284
+ flex-direction: row;
285
+ justify-content: space-between;
286
+ }
287
+ .chart-controls div {
288
+ margin-bottom: 5px;
289
+ }
290
+ .defaultContainerClass {
291
+ height: 90vh;
292
+ width: 100%;
293
+ border: 1px solid #ccc;
294
+ }
295
+ .default-input-node.vue-flow__node-input,
296
+ .default-output-node.vue-flow__node-output {
297
+ border-color: #000;
298
+ }
299
+ .default-input-node.vue-flow__node-input .vue-flow__handle,
300
+ .default-output-node.vue-flow__node-output .vue-flow__handle {
301
+ background-color: #000;
302
+ }
303
+ .default-input-node.vue-flow__node-input.selected,
304
+ .default-output-node.vue-flow__node-output.selected {
305
+ box-shadow: 0 0 0 0.5px #000;
306
+ }
307
+ button.button-default {
308
+ background-color: #ffffff;
309
+ padding: 1px 12px;
310
+ border-radius: 3px;
311
+ border: 1px solid #ccc;
312
+ cursor: pointer;
313
+ white-space: nowrap;
314
+ }
315
+
316
+ button.button-default:hover {
317
+ background-color: #f2f2f2;
318
+ }
319
+ .vue-flow {
320
+ background-size: 40px 40px;
321
+ background-image: linear-gradient(to right, #ccc 1px, transparent 1px),
322
+ linear-gradient(to bottom, #ccc 1px, transparent 1px);
323
+ }
324
+ input.label-editor {
325
+ position: absolute;
326
+ }
327
+ .node-editor-wrapper {
328
+ position: relative;
329
+ }
330
+ </style>
@@ -0,0 +1,122 @@
1
+ <template>
2
+ <div>
3
+ <NodeEditor v-model="elements" :node-container-class="nodeContainerClass" />
4
+ </div>
5
+ </template>
6
+ <script lang="ts" setup>
7
+ import { VueFlow } from '@vue-flow/core'
8
+ import '@vue-flow/core/dist/style.css'
9
+ import '@vue-flow/core/dist/theme-default.css'
10
+ import NodeEditor from './NodeEditor.vue'
11
+ import { ref, computed } from 'vue'
12
+
13
+ // Props
14
+
15
+ const props = defineProps(['layout', 'nodeContainerClass', 'modelValue'])
16
+
17
+ // Emits
18
+
19
+ const emit = defineEmits(['update:modelValue'])
20
+
21
+ // Computed variables
22
+
23
+ const elements = computed({
24
+ get: () => {
25
+ let states = props.modelValue
26
+ let stateHash = {}
27
+ let hasInputs = {}
28
+ let j = 0
29
+ let stateElements = []
30
+ for (let key in states) {
31
+ let idx = stateElements.length
32
+ let el = {
33
+ id: key,
34
+ label: key,
35
+ position: props.layout[key] && props.layout[key].position ? props.layout[key].position : { x: 200 * j, y: 100 },
36
+ targetPosition:
37
+ props.layout[key] && props.layout[key].targetPosition ? props.layout[key].targetPosition : 'left',
38
+ sourcePosition:
39
+ props.layout[key] && props.layout[key].sourcePosition ? props.layout[key].sourcePosition : 'right',
40
+ }
41
+ if (states[key].type && states[key].type == 'final') {
42
+ el.type = 'output'
43
+ el.class = 'default-output-node'
44
+ }
45
+ stateHash[key] = el
46
+ let edges = states[key].on
47
+ for (let edgeKey in states[key].on) {
48
+ let target = edges[edgeKey]
49
+ if (typeof target === 'object' && target.constructor === Object) {
50
+ target = target.target
51
+ }
52
+ stateElements.push({
53
+ id: `${key}-${edges[edgeKey]}-${edgeKey}`,
54
+ target: target,
55
+ source: key,
56
+ label: edgeKey,
57
+ animated: true,
58
+ })
59
+ hasInputs[target] = true
60
+ }
61
+ j++
62
+ }
63
+ for (let key in stateHash) {
64
+ if (!hasInputs[key]) {
65
+ stateHash[key]['type'] = 'input'
66
+ stateHash[key]['class'] = 'default-input-node'
67
+ }
68
+ stateElements.push(stateHash[key])
69
+ }
70
+ return stateElements
71
+ },
72
+ set: newValue => {
73
+ // update modelValue when elements change
74
+ onElementsChange(newValue)
75
+ // emit('update:modelValue', props.modelValue)
76
+ },
77
+ })
78
+
79
+ // Methods
80
+
81
+ const onElementsChange = elements => {
82
+ let states = {}
83
+ let edges = {}
84
+ let idToLabel = {}
85
+ for (let i = 0; i < elements.length; i++) {
86
+ let el = elements[i]
87
+ if (el.type == 'input') {
88
+ // it's an input node
89
+ states[el.label] = {
90
+ on: {},
91
+ }
92
+ } else if (el.type == 'output') {
93
+ // it's an output node
94
+ states[el.label] = {
95
+ type: 'final',
96
+ }
97
+ } else if (el.source && el.target) {
98
+ // it's an edge
99
+ edges[el.source] = edges[el.source] || {}
100
+ edges[el.source][el.label] = {
101
+ target: el.target,
102
+ }
103
+ } else {
104
+ // it's a state
105
+ states[el.label] = {
106
+ on: {},
107
+ }
108
+ }
109
+ idToLabel[el.id] = el.label
110
+ }
111
+
112
+ for (let key in edges) {
113
+ // add edges to states
114
+ let label = idToLabel[key]
115
+ for (let edgeKey in edges[key]) {
116
+ states[label].on[edgeKey] = edges[key][edgeKey]
117
+ }
118
+ }
119
+ emit('update:modelValue', states)
120
+ }
121
+ </script>
122
+ <style scoped></style>