@stonecrop/node-editor 0.2.23 → 0.2.25

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.
@@ -1,35 +1,33 @@
1
1
  <template>
2
- <div class="node-editor-wrapper" :class="containerClass" @mouseover="hover = true" @mouseleave="hover = false">
2
+ <div class="node-editor-wrapper" :class="nodeContainerClass" @mouseover="hover = true" @mouseleave="hover = false">
3
3
  <div class="chart-controls">
4
4
  <div class="chart-controls-left">
5
5
  <div><b>Selected Node:</b> {{ activeElementKey ? activeElementKey : 'none' }}</div>
6
6
  </div>
7
7
  <div class="chart-controls-right">
8
8
  <div>
9
- <button class="button-default" @click="addNode()">Add Node</button>
9
+ <button class="button-default" @click="addNode">Add Node</button>
10
10
  </div>
11
11
  <div>
12
- <button class="button-default" @click="fitView()">Center</button>
12
+ <button class="button-default" @click="fitView">Center</button>
13
13
  </div>
14
14
  <div v-if="activeElementIndex > -1">
15
- <button class="button-default" @click="shiftInput()">Shift Input Position</button>
15
+ <button class="button-default" @click="shiftInput">Shift Input Position</button>
16
16
  </div>
17
17
  <div v-if="activeElementIndex > -1">
18
- <button class="button-default" @click="shiftOutput()">Shift Output Position</button>
18
+ <button class="button-default" @click="shiftOutput">Shift Output Position</button>
19
19
  </div>
20
20
  </div>
21
21
  </div>
22
22
 
23
23
  <VueFlow
24
- @wheel.prevent="onWheel($event)"
24
+ v-if="vueFlowElements && vueFlowElements.length"
25
25
  class="nowheel"
26
26
  :prevent-scrolling="true"
27
27
  :zoom-on-scroll="false"
28
28
  :fit-view-on-init="true"
29
- v-if="vueFlowElements && vueFlowElements.length"
30
29
  v-model="vueFlowElements"
31
- @connect="onConnect($event)"
32
- @edge-double-click="onEdgeDoubleClick($event)">
30
+ @wheel.prevent="onWheel">
33
31
  <template #node-editable="props">
34
32
  <EditableNode v-bind="props" @change="labelChanged($event, props.id)" />
35
33
  </template>
@@ -39,79 +37,62 @@
39
37
  </VueFlow>
40
38
  </div>
41
39
  </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
40
 
50
- // Props
41
+ <script setup lang="ts">
42
+ import { type VueFlowStore, Position, VueFlow, useVueFlow, Node } from '@vue-flow/core'
43
+ import { type HTMLAttributes, ref, computed, defineEmits, onBeforeUnmount, onMounted } from 'vue'
44
+
45
+ import EditableEdge from '@/components/EditableEdge.vue'
46
+ import EditableNode from '@/components/EditableNode.vue'
47
+ import type { FlowElements } from '@/types'
51
48
 
52
49
  const props = defineProps<{
53
- modelValue: any
54
- nodeContainerClass: string
50
+ modelValue: FlowElements
51
+ nodeContainerClass?: HTMLAttributes['class']
55
52
  }>()
56
-
57
- // Emits
58
-
59
53
  const emit = defineEmits(['update:modelValue'])
60
54
 
61
- // Data
62
-
63
- const containerClass = ref('')
64
- const vueFlowInstance = ref({})
65
55
  const hover = ref(false)
66
- const labelEditor = ref({
67
- x: 0,
68
- y: 0,
69
- })
56
+ const vueFlowElements = ref<FlowElements>([])
57
+ const vueFlowInstance = ref<Partial<VueFlowStore>>()
70
58
 
71
59
  const activeElementKey = ref('')
72
-
73
- const vueFlowElements = ref([])
74
-
75
- // Computed variables
76
-
77
60
  const activeElementIndex = computed(() => {
78
- for (let j = 0; j < vueFlowElements.value.length; j++) {
79
- if (vueFlowElements.value[j].id == activeElementKey.value) return j
80
- }
61
+ vueFlowElements.value.forEach((element, index) => {
62
+ if (element.id === activeElementKey.value) {
63
+ return index
64
+ }
65
+ })
66
+
81
67
  return -1
82
68
  })
83
69
 
84
70
  const elements = computed({
85
71
  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
72
+ const _elements = props.modelValue
73
+
74
+ // Add data to each element
75
+ for (const _element of _elements) {
76
+ _element.data = {}
77
+ if (_element.type === 'input') {
78
+ _element.data.hasInput = false
79
+ _element.data.hasOutput = true
80
+ } else if (_element.type === 'output') {
81
+ _element.data.hasInput = true
82
+ _element.data.hasOutput = false
101
83
  } else {
102
- _elements[j].data.hasInput = true
103
- _elements[j].data.hasOutput = true
84
+ _element.data.hasInput = true
85
+ _element.data.hasOutput = true
104
86
  }
105
- _elements[j].class = 'vue-flow__node-default'
106
- _elements[j].type = 'editable'
87
+ _element.class = 'vue-flow__node-default'
88
+ _element.type = 'editable'
107
89
  }
108
90
 
109
- for (let j = 0; j < _elements.length; j++) {
110
- let key = _elements[j].id
111
- let el = _elements[j]
112
- _elements[j].events = {
91
+ // Add click event to each element
92
+ for (const _element of _elements) {
93
+ _element.events = {
113
94
  click: () => {
114
- activeElementKey.value = key
95
+ activeElementKey.value = _element.id
115
96
  },
116
97
  }
117
98
  }
@@ -123,20 +104,6 @@ const elements = computed({
123
104
  },
124
105
  })
125
106
 
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
107
  onMounted(() => {
141
108
  document.removeEventListener('keypress', handleKeypress)
142
109
  document.addEventListener('keypress', handleKeypress)
@@ -146,70 +113,74 @@ onBeforeUnmount(() => {
146
113
  document.removeEventListener('keypress', handleKeypress)
147
114
  })
148
115
 
149
- // Methods
116
+ const { onPaneReady } = useVueFlow()
117
+ onPaneReady(instance => {
118
+ vueFlowInstance.value = instance
119
+ })
150
120
 
151
- const shiftTerminal = currentTerminal => {
121
+ vueFlowElements.value = elements.value
122
+
123
+ // Methods
124
+ const shiftTerminal = (currentTerminal: Position) => {
152
125
  return {
153
- top: 'right',
154
- right: 'bottom',
155
- bottom: 'left',
156
- left: 'top',
126
+ [Position.Top]: Position.Right,
127
+ [Position.Right]: Position.Bottom,
128
+ [Position.Bottom]: Position.Left,
129
+ [Position.Left]: Position.Top,
157
130
  }[currentTerminal]
158
131
  }
159
132
 
160
133
  const shiftOutput = () => {
161
134
  if (activeElementIndex.value > -1) {
162
- vueFlowElements.value[activeElementIndex.value].sourcePosition = shiftTerminal(
163
- vueFlowElements.value[activeElementIndex.value].sourcePosition
164
- )
135
+ const activeNode = vueFlowElements.value[activeElementIndex.value] as Node
136
+ activeNode.sourcePosition = shiftTerminal(activeNode.sourcePosition)
165
137
  }
166
138
  }
167
139
 
168
140
  const shiftInput = () => {
169
141
  if (activeElementIndex.value > -1) {
170
- vueFlowElements.value[activeElementIndex.value].targetPosition = shiftTerminal(
171
- vueFlowElements.value[activeElementIndex.value].targetPosition
172
- )
142
+ const activeNode = vueFlowElements.value[activeElementIndex.value] as Node
143
+ activeNode.targetPosition = shiftTerminal(activeNode.targetPosition)
173
144
  }
174
145
  }
175
146
 
176
- const onWheel = $event => {
177
- window.scrollBy(0, $event.deltaY)
147
+ const onWheel = (event: WheelEvent) => {
148
+ window.scrollBy(0, event.deltaY)
178
149
  }
179
150
 
180
- const handleKeypress = e => {
181
- if (hover.value && e.ctrlKey == true) {
182
- if (e.key == '+' || e.key == '=') {
183
- vueFlowInstance.value.zoomIn()
151
+ const handleKeypress = (event: KeyboardEvent) => {
152
+ if (hover.value && event.ctrlKey == true) {
153
+ if (event.key == '+' || event.key == '=') {
154
+ void vueFlowInstance.value.zoomIn()
184
155
  }
185
- if (e.key == '-') {
186
- vueFlowInstance.value.zoomOut()
156
+ if (event.key == '-') {
157
+ void vueFlowInstance.value.zoomOut()
187
158
  }
188
159
  }
189
160
  }
190
161
 
191
- const fitView = () => {
192
- vueFlowInstance.value.fitView()
162
+ const fitView = async () => {
163
+ await vueFlowInstance.value.fitView()
193
164
  }
194
165
 
195
166
  const addNode = () => {
196
- let newNodePosition = { x: Math.random() * 200, y: Math.random() * 200 }
197
167
  let makeEdge = false
168
+ let newNodePosition = { x: Math.random() * 200, y: Math.random() * 200 }
198
169
  if (activeElementIndex.value > -1) {
199
170
  const activeNode = vueFlowElements.value[activeElementIndex.value]
200
171
  if (activeNode.data.hasOutput) {
201
- newNodePosition = { x: activeNode.position.x + 200, y: activeNode.position.y + 50 }
172
+ newNodePosition = { x: (activeNode as Node).position.x + 200, y: (activeNode as Node).position.y + 50 }
202
173
  makeEdge = true
203
174
  }
204
175
  }
205
176
 
206
- let id = vueFlowElements.value.length
207
- let nodeId = `node-${id}`
177
+ const id = vueFlowElements.value.length
178
+ const nodeId = `node-${id}`
208
179
  vueFlowElements.value.push({
209
180
  id: nodeId,
210
181
  label: 'Node ' + id,
211
- sourcePosition: 'right',
212
- targetPosition: 'left',
182
+ sourcePosition: Position.Right,
183
+ targetPosition: Position.Left,
213
184
  class: 'vue-flow__node-default',
214
185
  type: 'editable',
215
186
  data: {
@@ -243,14 +214,6 @@ const addNode = () => {
243
214
  }
244
215
  }
245
216
 
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
217
  const labelChanged = (e, id) => {
255
218
  for (let j = 0; j < vueFlowElements.value.length; j++) {
256
219
  if (vueFlowElements.value[j].id == id) {
@@ -260,6 +223,7 @@ const labelChanged = (e, id) => {
260
223
  }
261
224
  }
262
225
  </script>
226
+
263
227
  <style>
264
228
  @import '@vue-flow/core/dist/style.css';
265
229
  @import '@vue-flow/core/dist/theme-default.css';
@@ -272,9 +236,11 @@ const labelChanged = (e, id) => {
272
236
  align-items: center;
273
237
  padding-top: 0.2em;
274
238
  }
239
+
275
240
  .chart-controls-right div {
276
241
  margin-left: 5px;
277
242
  }
243
+
278
244
  .chart-controls {
279
245
  padding-left: 20px;
280
246
  padding-right: 20px;
@@ -284,26 +250,32 @@ const labelChanged = (e, id) => {
284
250
  flex-direction: row;
285
251
  justify-content: space-between;
286
252
  }
253
+
287
254
  .chart-controls div {
288
255
  margin-bottom: 5px;
289
256
  }
257
+
290
258
  .defaultContainerClass {
291
259
  height: 90vh;
292
260
  width: 100%;
293
261
  border: 1px solid #ccc;
294
262
  }
263
+
295
264
  .default-input-node.vue-flow__node-input,
296
265
  .default-output-node.vue-flow__node-output {
297
266
  border-color: #000;
298
267
  }
268
+
299
269
  .default-input-node.vue-flow__node-input .vue-flow__handle,
300
270
  .default-output-node.vue-flow__node-output .vue-flow__handle {
301
271
  background-color: #000;
302
272
  }
273
+
303
274
  .default-input-node.vue-flow__node-input.selected,
304
275
  .default-output-node.vue-flow__node-output.selected {
305
276
  box-shadow: 0 0 0 0.5px #000;
306
277
  }
278
+
307
279
  button.button-default {
308
280
  background-color: #ffffff;
309
281
  padding: 1px 12px;
@@ -316,14 +288,17 @@ button.button-default {
316
288
  button.button-default:hover {
317
289
  background-color: #f2f2f2;
318
290
  }
291
+
319
292
  .vue-flow {
320
293
  background-size: 40px 40px;
321
294
  background-image: linear-gradient(to right, #ccc 1px, transparent 1px),
322
295
  linear-gradient(to bottom, #ccc 1px, transparent 1px);
323
296
  }
297
+
324
298
  input.label-editor {
325
299
  position: absolute;
326
300
  }
301
+
327
302
  .node-editor-wrapper {
328
303
  position: relative;
329
304
  }
@@ -3,120 +3,131 @@
3
3
  <NodeEditor v-model="elements" :node-container-class="nodeContainerClass" />
4
4
  </div>
5
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
6
 
13
- // Props
7
+ <script setup lang="ts">
8
+ import { type Node, Position } from '@vue-flow/core'
9
+ import { type HTMLAttributes, computed } from 'vue'
14
10
 
15
- const props = defineProps(['layout', 'nodeContainerClass', 'modelValue'])
16
-
17
- // Emits
11
+ import NodeEditor from '@/components/NodeEditor.vue'
12
+ import type { EditorStates, FlowElement, FlowElements, Layout } from '@/types'
18
13
 
14
+ const states = defineModel<EditorStates>()
15
+ const props = defineProps<{
16
+ layout: Layout
17
+ nodeContainerClass?: HTMLAttributes['class']
18
+ }>()
19
19
  const emit = defineEmits(['update:modelValue'])
20
20
 
21
- // Computed variables
22
-
23
- const elements = computed({
21
+ const elements = computed<FlowElements>({
24
22
  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 = {
23
+ const hasInputs = {}
24
+ const stateElements: FlowElements = []
25
+ const stateHash: Record<string, FlowElement> = {}
26
+
27
+ let index = 0
28
+ for (const [key, value] of Object.entries(states.value)) {
29
+ const el: Node = {
33
30
  id: key,
34
31
  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',
32
+ position: props.layout[key]?.position || { x: 200 * index, y: 100 },
33
+ targetPosition: props.layout[key]?.targetPosition || Position.Left,
34
+ sourcePosition: props.layout[key]?.sourcePosition || Position.Right,
40
35
  }
41
- if (states[key].type && states[key].type == 'final') {
36
+
37
+ if (value.type === 'final') {
42
38
  el.type = 'output'
43
39
  el.class = 'default-output-node'
44
40
  }
41
+
45
42
  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
43
+
44
+ if (value.on) {
45
+ for (const [edgeKey, edgeValue] of Object.entries(value.on)) {
46
+ if (Array.isArray(edgeValue)) {
47
+ for (const edge of edgeValue) {
48
+ // TODO: handle typescript errors for both types of states
49
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
50
+ const edgeJson = edge.toJSON()
51
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
52
+ const target = edgeJson.target.toString()
53
+ stateElements.push({
54
+ id: `${key}-${edgeKey}`,
55
+ target: target,
56
+ source: key,
57
+ label: edgeKey,
58
+ animated: true,
59
+ })
60
+
61
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
62
+ hasInputs[target] = true
63
+ }
64
+ }
51
65
  }
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
66
  }
61
- j++
67
+
68
+ index++
62
69
  }
63
- for (let key in stateHash) {
70
+
71
+ for (const [key, value] of Object.entries(stateHash)) {
64
72
  if (!hasInputs[key]) {
65
- stateHash[key]['type'] = 'input'
66
- stateHash[key]['class'] = 'default-input-node'
73
+ value['type'] = 'input'
74
+ value['class'] = 'default-input-node'
67
75
  }
68
- stateElements.push(stateHash[key])
76
+ stateElements.push(value)
69
77
  }
78
+
70
79
  return stateElements
71
80
  },
72
81
  set: newValue => {
73
82
  // update modelValue when elements change
74
83
  onElementsChange(newValue)
75
- // emit('update:modelValue', props.modelValue)
84
+
85
+ // TODO: emit('update:modelValue', props.modelValue)
76
86
  },
77
87
  })
78
88
 
79
- // Methods
89
+ const onElementsChange = (elements: FlowElements) => {
90
+ const edges: Record<string, Record<string, any>> = {}
91
+ const idToLabel: Record<string, string> = {}
92
+ const states: EditorStates = {}
80
93
 
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') {
94
+ for (const el of elements) {
95
+ const label = el.label as string
96
+
97
+ if (el.type === 'input') {
88
98
  // it's an input node
89
- states[el.label] = {
99
+ states[label] = {
90
100
  on: {},
91
101
  }
92
- } else if (el.type == 'output') {
102
+ } else if (el.type === 'output') {
93
103
  // it's an output node
94
- states[el.label] = {
104
+ states[label] = {
95
105
  type: 'final',
96
106
  }
97
- } else if (el.source && el.target) {
107
+ } /* else if (el.source && el.target) {
98
108
  // it's an edge
99
109
  edges[el.source] = edges[el.source] || {}
100
- edges[el.source][el.label] = {
110
+ edges[el.source][label] = {
101
111
  target: el.target,
102
112
  }
103
- } else {
113
+ } */ else {
104
114
  // it's a state
105
- states[el.label] = {
115
+ states[label] = {
106
116
  on: {},
107
117
  }
108
118
  }
109
- idToLabel[el.id] = el.label
119
+
120
+ idToLabel[el.id] = label
110
121
  }
111
122
 
112
- for (let key in edges) {
123
+ for (const [edgeKey, edgeValue] of Object.entries(edges)) {
113
124
  // add edges to states
114
- let label = idToLabel[key]
115
- for (let edgeKey in edges[key]) {
116
- states[label].on[edgeKey] = edges[key][edgeKey]
125
+ const label = idToLabel[edgeKey]
126
+ for (const [key, value] of Object.entries(edgeValue)) {
127
+ states[label].on[key] = value
117
128
  }
118
129
  }
130
+
119
131
  emit('update:modelValue', states)
120
132
  }
121
133
  </script>
122
- <style scoped></style>
package/src/index.ts CHANGED
@@ -2,8 +2,14 @@ import { App } from 'vue'
2
2
 
3
3
  import NodeEditor from '@/components/NodeEditor.vue'
4
4
  import StateEditor from '@/components/StateEditor.vue'
5
+ export type { EditorStates, FlowElement, FlowElements, Layout } from '@/types'
5
6
 
6
- function install(app: App /* options */) {
7
+ /**
8
+ * Install all Node Editor components
9
+ * @param app - Vue app instance
10
+ * @public
11
+ */
12
+ function install(app: App) {
7
13
  app.component('NodeEditor', NodeEditor)
8
14
  app.component('StateEditor', StateEditor)
9
15
  }
@@ -0,0 +1,5 @@
1
+ declare module '*.vue' {
2
+ import { ComponentOptions } from 'vue'
3
+ const Component: ComponentOptions
4
+ export default Component
5
+ }
@@ -0,0 +1,24 @@
1
+ import { type Elements, type Element, type XYPosition, Position } from '@vue-flow/core'
2
+ import type { AnyStateMachine, AnyStateNodeDefinition, StatesConfig } from 'xstate'
3
+
4
+ export type EditorStates = {
5
+ [key: string]: Partial<AnyStateMachine | AnyStateNodeDefinition> | StatesConfig<any, any, any>
6
+ }
7
+
8
+ export type FlowElements = Elements<
9
+ { hasInput?: boolean; hasOutput?: boolean },
10
+ { hasInput?: boolean; hasOutput?: boolean }
11
+ >
12
+
13
+ export type FlowElement = Element<
14
+ { hasInput?: boolean; hasOutput?: boolean },
15
+ { hasInput?: boolean; hasOutput?: boolean }
16
+ >
17
+
18
+ export type Layout = {
19
+ [key: string]: {
20
+ position?: XYPosition
21
+ targetPosition?: Position
22
+ sourcePosition?: Position
23
+ }
24
+ }
@@ -1,7 +0,0 @@
1
- import { defineSetupVue3 } from '@histoire/plugin-vue'
2
- //import { BaklavaVuePlugin } from '@baklavajs/plugin-renderer-vue3'
3
-
4
- export const setupVue3 = defineSetupVue3(({ app }) => {
5
- // TODO: (typing) add typing for ADate
6
- //app.use(BaklavaVuePlugin)
7
- })