pict-section-flow 0.0.1
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/LICENSE +21 -0
- package/example_application/css/flowexample.css +65 -0
- package/example_application/html/index.html +32 -0
- package/example_application/package.json +41 -0
- package/example_application/source/Pict-Application-FlowExample-Configuration.json +15 -0
- package/example_application/source/Pict-Application-FlowExample.js +241 -0
- package/example_application/source/providers/PictRouter-FlowExample-Configuration.json +22 -0
- package/example_application/source/views/PictView-FlowExample-About.js +184 -0
- package/example_application/source/views/PictView-FlowExample-BottomBar.js +77 -0
- package/example_application/source/views/PictView-FlowExample-Documentation.js +325 -0
- package/example_application/source/views/PictView-FlowExample-Layout.js +86 -0
- package/example_application/source/views/PictView-FlowExample-MainWorkspace.js +191 -0
- package/example_application/source/views/PictView-FlowExample-TopBar.js +95 -0
- package/package.json +22 -0
- package/source/Pict-Section-Flow.js +19 -0
- package/source/providers/PictProvider-Flow-EventHandler.js +158 -0
- package/source/providers/PictProvider-Flow-NodeTypes.js +174 -0
- package/source/services/PictService-Flow-ConnectionRenderer.js +251 -0
- package/source/services/PictService-Flow-InteractionManager.js +567 -0
- package/source/services/PictService-Flow-Layout.js +207 -0
- package/source/views/PictView-Flow-Node.js +267 -0
- package/source/views/PictView-Flow-Toolbar.js +223 -0
- package/source/views/PictView-Flow.js +1116 -0
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
const libFableServiceProviderBase = require('fable-serviceproviderbase');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Interaction states for the flow diagram
|
|
5
|
+
*/
|
|
6
|
+
const INTERACTION_STATES =
|
|
7
|
+
{
|
|
8
|
+
IDLE: 'idle',
|
|
9
|
+
DRAGGING_NODE: 'dragging-node',
|
|
10
|
+
CONNECTING: 'connecting',
|
|
11
|
+
PANNING: 'panning'
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
class PictServiceFlowInteractionManager extends libFableServiceProviderBase
|
|
15
|
+
{
|
|
16
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
17
|
+
{
|
|
18
|
+
super(pFable, pOptions, pServiceHash);
|
|
19
|
+
|
|
20
|
+
this.serviceType = 'PictServiceFlowInteractionManager';
|
|
21
|
+
|
|
22
|
+
this._FlowView = (pOptions && pOptions.FlowView) ? pOptions.FlowView : null;
|
|
23
|
+
|
|
24
|
+
this._SVGElement = null;
|
|
25
|
+
this._ViewportElement = null;
|
|
26
|
+
|
|
27
|
+
// Interaction state
|
|
28
|
+
this._State = INTERACTION_STATES.IDLE;
|
|
29
|
+
|
|
30
|
+
// Drag state
|
|
31
|
+
this._DragNodeHash = null;
|
|
32
|
+
this._DragStartX = 0;
|
|
33
|
+
this._DragStartY = 0;
|
|
34
|
+
this._DragNodeStartX = 0;
|
|
35
|
+
this._DragNodeStartY = 0;
|
|
36
|
+
|
|
37
|
+
// Pan state
|
|
38
|
+
this._PanStartX = 0;
|
|
39
|
+
this._PanStartY = 0;
|
|
40
|
+
this._PanStartPanX = 0;
|
|
41
|
+
this._PanStartPanY = 0;
|
|
42
|
+
|
|
43
|
+
// Connection drag state
|
|
44
|
+
this._ConnectSourceNodeHash = null;
|
|
45
|
+
this._ConnectSourcePortHash = null;
|
|
46
|
+
this._ConnectDragLine = null;
|
|
47
|
+
|
|
48
|
+
// Bound event handlers (for removeEventListener)
|
|
49
|
+
this._boundOnPointerDown = this._onPointerDown.bind(this);
|
|
50
|
+
this._boundOnPointerMove = this._onPointerMove.bind(this);
|
|
51
|
+
this._boundOnPointerUp = this._onPointerUp.bind(this);
|
|
52
|
+
this._boundOnWheel = this._onWheel.bind(this);
|
|
53
|
+
this._boundOnKeyDown = this._onKeyDown.bind(this);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Initialize event listeners on the SVG element
|
|
58
|
+
* @param {SVGSVGElement} pSVGElement
|
|
59
|
+
* @param {SVGGElement} pViewportElement
|
|
60
|
+
*/
|
|
61
|
+
initialize(pSVGElement, pViewportElement)
|
|
62
|
+
{
|
|
63
|
+
this._SVGElement = pSVGElement;
|
|
64
|
+
this._ViewportElement = pViewportElement;
|
|
65
|
+
|
|
66
|
+
if (!this._SVGElement) return;
|
|
67
|
+
|
|
68
|
+
// Use pointer events for unified mouse/touch handling
|
|
69
|
+
this._SVGElement.addEventListener('pointerdown', this._boundOnPointerDown);
|
|
70
|
+
this._SVGElement.addEventListener('pointermove', this._boundOnPointerMove);
|
|
71
|
+
this._SVGElement.addEventListener('pointerup', this._boundOnPointerUp);
|
|
72
|
+
this._SVGElement.addEventListener('pointerleave', this._boundOnPointerUp);
|
|
73
|
+
this._SVGElement.addEventListener('wheel', this._boundOnWheel, { passive: false });
|
|
74
|
+
|
|
75
|
+
// Keyboard events for delete
|
|
76
|
+
document.addEventListener('keydown', this._boundOnKeyDown);
|
|
77
|
+
|
|
78
|
+
// Prevent context menu on right-click
|
|
79
|
+
this._SVGElement.addEventListener('contextmenu', (pEvent) =>
|
|
80
|
+
{
|
|
81
|
+
pEvent.preventDefault();
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Remove all event listeners
|
|
87
|
+
*/
|
|
88
|
+
destroy()
|
|
89
|
+
{
|
|
90
|
+
if (this._SVGElement)
|
|
91
|
+
{
|
|
92
|
+
this._SVGElement.removeEventListener('pointerdown', this._boundOnPointerDown);
|
|
93
|
+
this._SVGElement.removeEventListener('pointermove', this._boundOnPointerMove);
|
|
94
|
+
this._SVGElement.removeEventListener('pointerup', this._boundOnPointerUp);
|
|
95
|
+
this._SVGElement.removeEventListener('pointerleave', this._boundOnPointerUp);
|
|
96
|
+
this._SVGElement.removeEventListener('wheel', this._boundOnWheel);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
document.removeEventListener('keydown', this._boundOnKeyDown);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Handle pointer down event
|
|
104
|
+
* @param {PointerEvent} pEvent
|
|
105
|
+
*/
|
|
106
|
+
_onPointerDown(pEvent)
|
|
107
|
+
{
|
|
108
|
+
if (!this._FlowView) return;
|
|
109
|
+
|
|
110
|
+
let tmpTarget = pEvent.target;
|
|
111
|
+
let tmpElementType = this._getElementType(tmpTarget);
|
|
112
|
+
|
|
113
|
+
// Capture pointer for smooth dragging
|
|
114
|
+
this._SVGElement.setPointerCapture(pEvent.pointerId);
|
|
115
|
+
|
|
116
|
+
switch (tmpElementType)
|
|
117
|
+
{
|
|
118
|
+
case 'port':
|
|
119
|
+
this._startConnection(pEvent, tmpTarget);
|
|
120
|
+
break;
|
|
121
|
+
|
|
122
|
+
case 'node':
|
|
123
|
+
case 'node-body':
|
|
124
|
+
this._startNodeDrag(pEvent, tmpTarget);
|
|
125
|
+
break;
|
|
126
|
+
|
|
127
|
+
case 'connection':
|
|
128
|
+
case 'connection-hitarea':
|
|
129
|
+
this._selectConnection(tmpTarget);
|
|
130
|
+
break;
|
|
131
|
+
|
|
132
|
+
default:
|
|
133
|
+
// Click on background - start panning or deselect
|
|
134
|
+
if (pEvent.button === 0 && this._FlowView.options.EnablePanning)
|
|
135
|
+
{
|
|
136
|
+
this._startPanning(pEvent);
|
|
137
|
+
}
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Handle pointer move event
|
|
144
|
+
* @param {PointerEvent} pEvent
|
|
145
|
+
*/
|
|
146
|
+
_onPointerMove(pEvent)
|
|
147
|
+
{
|
|
148
|
+
if (!this._FlowView) return;
|
|
149
|
+
|
|
150
|
+
switch (this._State)
|
|
151
|
+
{
|
|
152
|
+
case INTERACTION_STATES.DRAGGING_NODE:
|
|
153
|
+
this._onNodeDrag(pEvent);
|
|
154
|
+
break;
|
|
155
|
+
|
|
156
|
+
case INTERACTION_STATES.CONNECTING:
|
|
157
|
+
this._onConnectionDrag(pEvent);
|
|
158
|
+
break;
|
|
159
|
+
|
|
160
|
+
case INTERACTION_STATES.PANNING:
|
|
161
|
+
this._onPan(pEvent);
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Handle pointer up event
|
|
168
|
+
* @param {PointerEvent} pEvent
|
|
169
|
+
*/
|
|
170
|
+
_onPointerUp(pEvent)
|
|
171
|
+
{
|
|
172
|
+
if (!this._FlowView) return;
|
|
173
|
+
|
|
174
|
+
// Release pointer capture
|
|
175
|
+
if (this._SVGElement.hasPointerCapture && this._SVGElement.hasPointerCapture(pEvent.pointerId))
|
|
176
|
+
{
|
|
177
|
+
this._SVGElement.releasePointerCapture(pEvent.pointerId);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
switch (this._State)
|
|
181
|
+
{
|
|
182
|
+
case INTERACTION_STATES.DRAGGING_NODE:
|
|
183
|
+
this._endNodeDrag(pEvent);
|
|
184
|
+
break;
|
|
185
|
+
|
|
186
|
+
case INTERACTION_STATES.CONNECTING:
|
|
187
|
+
this._endConnection(pEvent);
|
|
188
|
+
break;
|
|
189
|
+
|
|
190
|
+
case INTERACTION_STATES.PANNING:
|
|
191
|
+
this._endPanning(pEvent);
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Handle mouse wheel for zoom
|
|
198
|
+
* @param {WheelEvent} pEvent
|
|
199
|
+
*/
|
|
200
|
+
_onWheel(pEvent)
|
|
201
|
+
{
|
|
202
|
+
if (!this._FlowView || !this._FlowView.options.EnableZooming) return;
|
|
203
|
+
|
|
204
|
+
pEvent.preventDefault();
|
|
205
|
+
|
|
206
|
+
let tmpDelta = pEvent.deltaY > 0 ? -this._FlowView.options.ZoomStep : this._FlowView.options.ZoomStep;
|
|
207
|
+
let tmpNewZoom = this._FlowView.viewState.Zoom + tmpDelta;
|
|
208
|
+
|
|
209
|
+
// Zoom toward mouse position
|
|
210
|
+
let tmpRect = this._SVGElement.getBoundingClientRect();
|
|
211
|
+
let tmpMouseX = pEvent.clientX - tmpRect.left;
|
|
212
|
+
let tmpMouseY = pEvent.clientY - tmpRect.top;
|
|
213
|
+
|
|
214
|
+
this._FlowView.setZoom(tmpNewZoom, tmpMouseX, tmpMouseY);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Handle keyboard events
|
|
219
|
+
* @param {KeyboardEvent} pEvent
|
|
220
|
+
*/
|
|
221
|
+
_onKeyDown(pEvent)
|
|
222
|
+
{
|
|
223
|
+
if (!this._FlowView) return;
|
|
224
|
+
|
|
225
|
+
// Only handle events when the flow is focused/visible
|
|
226
|
+
if (pEvent.key === 'Delete' || pEvent.key === 'Backspace')
|
|
227
|
+
{
|
|
228
|
+
// Don't delete if user is typing in an input
|
|
229
|
+
if (pEvent.target && (pEvent.target.tagName === 'INPUT' || pEvent.target.tagName === 'TEXTAREA' || pEvent.target.tagName === 'SELECT'))
|
|
230
|
+
{
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
this._FlowView.deleteSelected();
|
|
235
|
+
pEvent.preventDefault();
|
|
236
|
+
}
|
|
237
|
+
else if (pEvent.key === 'Escape')
|
|
238
|
+
{
|
|
239
|
+
if (this._State === INTERACTION_STATES.CONNECTING)
|
|
240
|
+
{
|
|
241
|
+
this._cancelConnection();
|
|
242
|
+
}
|
|
243
|
+
this._FlowView.deselectAll();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ---- Node Dragging ----
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Start dragging a node
|
|
251
|
+
* @param {PointerEvent} pEvent
|
|
252
|
+
* @param {Element} pTarget
|
|
253
|
+
*/
|
|
254
|
+
_startNodeDrag(pEvent, pTarget)
|
|
255
|
+
{
|
|
256
|
+
if (!this._FlowView.options.EnableNodeDragging) return;
|
|
257
|
+
|
|
258
|
+
let tmpNodeHash = this._getNodeHash(pTarget);
|
|
259
|
+
if (!tmpNodeHash) return;
|
|
260
|
+
|
|
261
|
+
// Select the node
|
|
262
|
+
this._FlowView.selectNode(tmpNodeHash);
|
|
263
|
+
|
|
264
|
+
let tmpNode = this._FlowView.getNode(tmpNodeHash);
|
|
265
|
+
if (!tmpNode) return;
|
|
266
|
+
|
|
267
|
+
this._State = INTERACTION_STATES.DRAGGING_NODE;
|
|
268
|
+
this._DragNodeHash = tmpNodeHash;
|
|
269
|
+
this._DragStartX = pEvent.clientX;
|
|
270
|
+
this._DragStartY = pEvent.clientY;
|
|
271
|
+
this._DragNodeStartX = tmpNode.X;
|
|
272
|
+
this._DragNodeStartY = tmpNode.Y;
|
|
273
|
+
|
|
274
|
+
// Add dragging class
|
|
275
|
+
this._SVGElement.classList.add('panning');
|
|
276
|
+
|
|
277
|
+
let tmpNodeGroup = this._FlowView._NodesLayer.querySelector(`[data-node-hash="${tmpNodeHash}"]`);
|
|
278
|
+
if (tmpNodeGroup)
|
|
279
|
+
{
|
|
280
|
+
tmpNodeGroup.classList.add('dragging');
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Handle node dragging
|
|
286
|
+
* @param {PointerEvent} pEvent
|
|
287
|
+
*/
|
|
288
|
+
_onNodeDrag(pEvent)
|
|
289
|
+
{
|
|
290
|
+
if (!this._DragNodeHash) return;
|
|
291
|
+
|
|
292
|
+
let tmpVS = this._FlowView.viewState;
|
|
293
|
+
let tmpDX = (pEvent.clientX - this._DragStartX) / tmpVS.Zoom;
|
|
294
|
+
let tmpDY = (pEvent.clientY - this._DragStartY) / tmpVS.Zoom;
|
|
295
|
+
|
|
296
|
+
let tmpNewX = this._DragNodeStartX + tmpDX;
|
|
297
|
+
let tmpNewY = this._DragNodeStartY + tmpDY;
|
|
298
|
+
|
|
299
|
+
this._FlowView.updateNodePosition(this._DragNodeHash, tmpNewX, tmpNewY);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* End node dragging
|
|
304
|
+
* @param {PointerEvent} pEvent
|
|
305
|
+
*/
|
|
306
|
+
_endNodeDrag(pEvent)
|
|
307
|
+
{
|
|
308
|
+
this._SVGElement.classList.remove('panning');
|
|
309
|
+
|
|
310
|
+
let tmpNodeGroup = this._FlowView._NodesLayer.querySelector(`[data-node-hash="${this._DragNodeHash}"]`);
|
|
311
|
+
if (tmpNodeGroup)
|
|
312
|
+
{
|
|
313
|
+
tmpNodeGroup.classList.remove('dragging');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Full re-render to finalize positions
|
|
317
|
+
this._FlowView.renderFlow();
|
|
318
|
+
this._FlowView.marshalFromView();
|
|
319
|
+
|
|
320
|
+
let tmpNode = this._FlowView.getNode(this._DragNodeHash);
|
|
321
|
+
if (tmpNode && this._FlowView._EventHandlerProvider)
|
|
322
|
+
{
|
|
323
|
+
this._FlowView._EventHandlerProvider.fireEvent('onNodeMoved', tmpNode);
|
|
324
|
+
this._FlowView._EventHandlerProvider.fireEvent('onFlowChanged', this._FlowView.flowData);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
this._State = INTERACTION_STATES.IDLE;
|
|
328
|
+
this._DragNodeHash = null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ---- Connection Creation ----
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Start creating a connection from a port
|
|
335
|
+
* @param {PointerEvent} pEvent
|
|
336
|
+
* @param {Element} pTarget
|
|
337
|
+
*/
|
|
338
|
+
_startConnection(pEvent, pTarget)
|
|
339
|
+
{
|
|
340
|
+
if (!this._FlowView.options.EnableConnectionCreation) return;
|
|
341
|
+
|
|
342
|
+
let tmpNodeHash = pTarget.getAttribute('data-node-hash');
|
|
343
|
+
let tmpPortHash = pTarget.getAttribute('data-port-hash');
|
|
344
|
+
let tmpPortDirection = pTarget.getAttribute('data-port-direction');
|
|
345
|
+
|
|
346
|
+
if (!tmpNodeHash || !tmpPortHash) return;
|
|
347
|
+
|
|
348
|
+
// Only allow starting connections from output ports
|
|
349
|
+
if (tmpPortDirection !== 'output')
|
|
350
|
+
{
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
this._State = INTERACTION_STATES.CONNECTING;
|
|
355
|
+
this._ConnectSourceNodeHash = tmpNodeHash;
|
|
356
|
+
this._ConnectSourcePortHash = tmpPortHash;
|
|
357
|
+
|
|
358
|
+
this._SVGElement.classList.add('connecting');
|
|
359
|
+
|
|
360
|
+
// Create drag line
|
|
361
|
+
let tmpPortPos = this._FlowView.getPortPosition(tmpNodeHash, tmpPortHash);
|
|
362
|
+
if (tmpPortPos)
|
|
363
|
+
{
|
|
364
|
+
this._ConnectDragLine = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
365
|
+
this._ConnectDragLine.setAttribute('class', 'pict-flow-drag-connection');
|
|
366
|
+
this._ConnectDragLine.setAttribute('d', `M ${tmpPortPos.x} ${tmpPortPos.y} L ${tmpPortPos.x} ${tmpPortPos.y}`);
|
|
367
|
+
|
|
368
|
+
// Add to viewport (so it transforms with pan/zoom)
|
|
369
|
+
this._FlowView._ViewportElement.appendChild(this._ConnectDragLine);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
pEvent.stopPropagation();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Handle connection drag
|
|
377
|
+
* @param {PointerEvent} pEvent
|
|
378
|
+
*/
|
|
379
|
+
_onConnectionDrag(pEvent)
|
|
380
|
+
{
|
|
381
|
+
if (!this._ConnectDragLine) return;
|
|
382
|
+
|
|
383
|
+
let tmpSourcePos = this._FlowView.getPortPosition(this._ConnectSourceNodeHash, this._ConnectSourcePortHash);
|
|
384
|
+
if (!tmpSourcePos) return;
|
|
385
|
+
|
|
386
|
+
let tmpEndCoords = this._FlowView.screenToSVGCoords(pEvent.clientX, pEvent.clientY);
|
|
387
|
+
|
|
388
|
+
// Render a bezier curve for the drag line
|
|
389
|
+
let tmpDX = Math.abs(tmpEndCoords.x - tmpSourcePos.x) * 0.5;
|
|
390
|
+
let tmpPath = `M ${tmpSourcePos.x} ${tmpSourcePos.y} C ${tmpSourcePos.x + tmpDX} ${tmpSourcePos.y}, ${tmpEndCoords.x - tmpDX} ${tmpEndCoords.y}, ${tmpEndCoords.x} ${tmpEndCoords.y}`;
|
|
391
|
+
this._ConnectDragLine.setAttribute('d', tmpPath);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* End connection creation
|
|
396
|
+
* @param {PointerEvent} pEvent
|
|
397
|
+
*/
|
|
398
|
+
_endConnection(pEvent)
|
|
399
|
+
{
|
|
400
|
+
// Remove drag line
|
|
401
|
+
if (this._ConnectDragLine && this._ConnectDragLine.parentNode)
|
|
402
|
+
{
|
|
403
|
+
this._ConnectDragLine.parentNode.removeChild(this._ConnectDragLine);
|
|
404
|
+
}
|
|
405
|
+
this._ConnectDragLine = null;
|
|
406
|
+
|
|
407
|
+
this._SVGElement.classList.remove('connecting');
|
|
408
|
+
|
|
409
|
+
// Check if we're over a valid target port
|
|
410
|
+
let tmpTarget = document.elementFromPoint(pEvent.clientX, pEvent.clientY);
|
|
411
|
+
if (tmpTarget)
|
|
412
|
+
{
|
|
413
|
+
let tmpTargetPortHash = tmpTarget.getAttribute('data-port-hash');
|
|
414
|
+
let tmpTargetNodeHash = tmpTarget.getAttribute('data-node-hash');
|
|
415
|
+
let tmpTargetPortDirection = tmpTarget.getAttribute('data-port-direction');
|
|
416
|
+
|
|
417
|
+
if (tmpTargetPortHash && tmpTargetNodeHash && tmpTargetPortDirection === 'input')
|
|
418
|
+
{
|
|
419
|
+
this._FlowView.addConnection(
|
|
420
|
+
this._ConnectSourceNodeHash,
|
|
421
|
+
this._ConnectSourcePortHash,
|
|
422
|
+
tmpTargetNodeHash,
|
|
423
|
+
tmpTargetPortHash
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
this._State = INTERACTION_STATES.IDLE;
|
|
429
|
+
this._ConnectSourceNodeHash = null;
|
|
430
|
+
this._ConnectSourcePortHash = null;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Cancel connection creation (e.g., on Escape)
|
|
435
|
+
*/
|
|
436
|
+
_cancelConnection()
|
|
437
|
+
{
|
|
438
|
+
if (this._ConnectDragLine && this._ConnectDragLine.parentNode)
|
|
439
|
+
{
|
|
440
|
+
this._ConnectDragLine.parentNode.removeChild(this._ConnectDragLine);
|
|
441
|
+
}
|
|
442
|
+
this._ConnectDragLine = null;
|
|
443
|
+
|
|
444
|
+
this._SVGElement.classList.remove('connecting');
|
|
445
|
+
|
|
446
|
+
this._State = INTERACTION_STATES.IDLE;
|
|
447
|
+
this._ConnectSourceNodeHash = null;
|
|
448
|
+
this._ConnectSourcePortHash = null;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ---- Panning ----
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Start panning the viewport
|
|
455
|
+
* @param {PointerEvent} pEvent
|
|
456
|
+
*/
|
|
457
|
+
_startPanning(pEvent)
|
|
458
|
+
{
|
|
459
|
+
// Deselect if clicking on empty space
|
|
460
|
+
this._FlowView.deselectAll();
|
|
461
|
+
|
|
462
|
+
this._State = INTERACTION_STATES.PANNING;
|
|
463
|
+
this._PanStartX = pEvent.clientX;
|
|
464
|
+
this._PanStartY = pEvent.clientY;
|
|
465
|
+
this._PanStartPanX = this._FlowView.viewState.PanX;
|
|
466
|
+
this._PanStartPanY = this._FlowView.viewState.PanY;
|
|
467
|
+
|
|
468
|
+
this._SVGElement.classList.add('panning');
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Handle panning
|
|
473
|
+
* @param {PointerEvent} pEvent
|
|
474
|
+
*/
|
|
475
|
+
_onPan(pEvent)
|
|
476
|
+
{
|
|
477
|
+
let tmpDX = pEvent.clientX - this._PanStartX;
|
|
478
|
+
let tmpDY = pEvent.clientY - this._PanStartY;
|
|
479
|
+
|
|
480
|
+
this._FlowView.viewState.PanX = this._PanStartPanX + tmpDX;
|
|
481
|
+
this._FlowView.viewState.PanY = this._PanStartPanY + tmpDY;
|
|
482
|
+
|
|
483
|
+
this._FlowView.updateViewportTransform();
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* End panning
|
|
488
|
+
* @param {PointerEvent} pEvent
|
|
489
|
+
*/
|
|
490
|
+
_endPanning(pEvent)
|
|
491
|
+
{
|
|
492
|
+
this._SVGElement.classList.remove('panning');
|
|
493
|
+
this._State = INTERACTION_STATES.IDLE;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ---- Connection Selection ----
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Select a connection
|
|
500
|
+
* @param {Element} pTarget
|
|
501
|
+
*/
|
|
502
|
+
_selectConnection(pTarget)
|
|
503
|
+
{
|
|
504
|
+
let tmpConnectionHash = pTarget.getAttribute('data-connection-hash');
|
|
505
|
+
if (tmpConnectionHash)
|
|
506
|
+
{
|
|
507
|
+
this._FlowView.selectConnection(tmpConnectionHash);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ---- Utilities ----
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Get the element type from a target element (walks up to find data attributes)
|
|
515
|
+
* @param {Element} pTarget
|
|
516
|
+
* @returns {string} The element type
|
|
517
|
+
*/
|
|
518
|
+
_getElementType(pTarget)
|
|
519
|
+
{
|
|
520
|
+
if (!pTarget) return 'background';
|
|
521
|
+
|
|
522
|
+
// Check the element itself
|
|
523
|
+
let tmpType = pTarget.getAttribute('data-element-type');
|
|
524
|
+
if (tmpType) return tmpType;
|
|
525
|
+
|
|
526
|
+
// Walk up to find the closest element with a data attribute
|
|
527
|
+
let tmpParent = pTarget.parentElement;
|
|
528
|
+
let tmpDepth = 0;
|
|
529
|
+
while (tmpParent && tmpDepth < 5)
|
|
530
|
+
{
|
|
531
|
+
tmpType = tmpParent.getAttribute('data-element-type');
|
|
532
|
+
if (tmpType) return tmpType;
|
|
533
|
+
tmpParent = tmpParent.parentElement;
|
|
534
|
+
tmpDepth++;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return 'background';
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Get the node hash from a target element (walks up parents)
|
|
542
|
+
* @param {Element} pTarget
|
|
543
|
+
* @returns {string|null}
|
|
544
|
+
*/
|
|
545
|
+
_getNodeHash(pTarget)
|
|
546
|
+
{
|
|
547
|
+
if (!pTarget) return null;
|
|
548
|
+
|
|
549
|
+
let tmpHash = pTarget.getAttribute('data-node-hash');
|
|
550
|
+
if (tmpHash) return tmpHash;
|
|
551
|
+
|
|
552
|
+
let tmpParent = pTarget.parentElement;
|
|
553
|
+
let tmpDepth = 0;
|
|
554
|
+
while (tmpParent && tmpDepth < 5)
|
|
555
|
+
{
|
|
556
|
+
tmpHash = tmpParent.getAttribute('data-node-hash');
|
|
557
|
+
if (tmpHash) return tmpHash;
|
|
558
|
+
tmpParent = tmpParent.parentElement;
|
|
559
|
+
tmpDepth++;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
module.exports = PictServiceFlowInteractionManager;
|
|
567
|
+
module.exports.INTERACTION_STATES = INTERACTION_STATES;
|