power-link 0.0.4

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/README.md ADDED
@@ -0,0 +1,523 @@
1
+ # node-link-utils
2
+
3
+ [![npm version](https://img.shields.io/npm/v/node-link-utils.svg)](https://www.npmjs.com/package/node-link-utils)
4
+ [![license](https://img.shields.io/npm/l/node-link-utils.svg)](https://github.com/your-username/node-link-utils/blob/main/LICENSE)
5
+
6
+ A pure JavaScript visual node connector for creating draggable connections between nodes. Framework-agnostic and easy to use.
7
+
8
+ ![Node Link Connector Demo](https://github.com/Tem-man/node-link-utils/blob/main/packages/images/screen-shot.png)
9
+
10
+ ### 📹 Demo Video
11
+
12
+ <video width="100%" controls>
13
+ <source src="https://github.com/Tem-man/node-link-utils/raw/main/packages/images/video.mp4" type="video/mp4">
14
+ Your browser does not support the video tag.
15
+ </video>
16
+
17
+ **Watch the demo video** to see node-link-utils in action! [Download video](https://github.com/Tem-man/node-link-utils/raw/main/packages/images/video.mp4)
18
+
19
+ ## ✨ Features
20
+
21
+ - 🎯 **Visual Node Connections** - Create beautiful bezier curve connections between nodes
22
+ - 🖱️ **Drag & Drop** - Intuitive drag-and-drop connection creation
23
+ - 🔄 **Node Dragging** - Move nodes around with automatic connection updates
24
+ - 🧲 **Smart Snapping** - Automatic connection point detection and snapping
25
+ - 🎨 **Customizable** - Fully configurable colors, sizes, and behaviors
26
+ - 🚫 **Delete Connections** - Hover over connections to show delete button
27
+ - 📦 **Zero Dependencies** - Pure JavaScript, no framework required
28
+ - 🎭 **Multiple Connection Points** - Support for left and right connection dots
29
+ - 🔌 **Event Callbacks** - Listen to connection and disconnection events
30
+
31
+ ## 📦 Installation
32
+
33
+ ```bash
34
+ npm install node-link-utils
35
+ ```
36
+
37
+ Or using yarn:
38
+
39
+ ```bash
40
+ yarn add node-link-utils
41
+ ```
42
+
43
+ Or using pnpm:
44
+
45
+ ```bash
46
+ pnpm add node-link-utils
47
+ ```
48
+
49
+ ## 🚀 Quick Start
50
+
51
+ ### Basic Usage
52
+
53
+ ```javascript
54
+ import Connector from "node-link-utils";
55
+
56
+ // 1. Get container element
57
+ const container = document.getElementById("connector-container");
58
+
59
+ // 2. Create connector instance
60
+ const connector = new Connector({
61
+ container: container,
62
+
63
+ // Optional configuration
64
+ lineColor: "#155BD4",
65
+ lineWidth: 2,
66
+ dotSize: 12,
67
+ dotColor: "#155BD4",
68
+
69
+ // Event callbacks
70
+ onConnect: (connection) => {
71
+ console.log("Connection created:", connection);
72
+ // connection: { from: 'node1', to: 'node2', fromDot: 'right', toDot: 'left' }
73
+ },
74
+
75
+ onDisconnect: (connection) => {
76
+ console.log("Connection removed:", connection);
77
+ }
78
+ });
79
+
80
+ // 3. Register nodes
81
+ const node1 = document.getElementById("node1");
82
+ const node2 = document.getElementById("node2");
83
+
84
+ connector.registerNode("node1", node1, {
85
+ dotPositions: ["right"] // Only right connection dot
86
+ });
87
+
88
+ connector.registerNode("node2", node2, {
89
+ dotPositions: ["left", "right"] // Both left and right dots
90
+ });
91
+ ```
92
+
93
+ ### HTML Structure
94
+
95
+ ```html
96
+ <div
97
+ id="connector-container"
98
+ style="position: relative; height: 600px;"
99
+ >
100
+ <div
101
+ id="node1"
102
+ style="position: absolute; left: 100px; top: 100px;"
103
+ >
104
+ Node 1
105
+ </div>
106
+ <div
107
+ id="node2"
108
+ style="position: absolute; left: 400px; top: 100px;"
109
+ >
110
+ Node 2
111
+ </div>
112
+ </div>
113
+ ```
114
+
115
+ ## 📖 API Documentation
116
+
117
+ ### Constructor Options
118
+
119
+ | Option | Type | Default | Description |
120
+ | ------------------ | ----------- | ------------ | ----------------------------------- |
121
+ | `container` | HTMLElement | **Required** | Container element for the connector |
122
+ | `lineColor` | String | `'#155BD4'` | Color of connection lines |
123
+ | `lineWidth` | Number | `2` | Width of connection lines |
124
+ | `dotSize` | Number | `12` | Size of connection dots |
125
+ | `dotColor` | String | `'#155BD4'` | Color of connection dots |
126
+ | `deleteButtonSize` | Number | `20` | Size of delete button |
127
+ | `enableNodeDrag` | Boolean | `true` | Enable node dragging |
128
+ | `enableSnap` | Boolean | `true` | Enable connection snapping |
129
+ | `snapDistance` | Number | `20` | Snap distance in pixels |
130
+ | `onConnect` | Function | `() => {}` | Callback when connection is created |
131
+ | `onDisconnect` | Function | `() => {}` | Callback when connection is removed |
132
+
133
+ ### Methods
134
+
135
+ #### `registerNode(id, element, options)`
136
+
137
+ Register a node for connection.
138
+
139
+ **Parameters:**
140
+
141
+ - `id` (String): Unique identifier for the node
142
+ - `element` (HTMLElement): DOM element of the node
143
+ - `options` (Object): Node configuration
144
+ - `dotPositions` (String | Array): Connection dot positions
145
+ - `'both'`: Both left and right dots
146
+ - `['left', 'right']`: Array format, both sides
147
+ - `['left']`: Only left dot
148
+ - `['right']`: Only right dot
149
+
150
+ **Returns:** Node object
151
+
152
+ **Example:**
153
+
154
+ ```javascript
155
+ connector.registerNode("myNode", element, {
156
+ dotPositions: ["right"]
157
+ });
158
+ ```
159
+
160
+ #### `createConnection(fromNode, toNode, fromDot, toDot)`
161
+
162
+ Programmatically create a connection between nodes.
163
+
164
+ **Parameters:**
165
+
166
+ - `fromNode` (Object): Source node object
167
+ - `toNode` (Object): Target node object
168
+ - `fromDot` (Object): Source connection dot (optional)
169
+ - `toDot` (Object): Target connection dot (optional)
170
+
171
+ **Returns:** Connection object
172
+
173
+ **Example:**
174
+
175
+ ```javascript
176
+ const node1 = connector.nodes[0];
177
+ const node2 = connector.nodes[1];
178
+ connector.createConnection(node1, node2);
179
+ ```
180
+
181
+ #### `disconnect(connectionId)`
182
+
183
+ Remove a connection.
184
+
185
+ **Parameters:**
186
+
187
+ - `connectionId` (String): Connection ID (optional, if not provided, removes all connections)
188
+
189
+ **Example:**
190
+
191
+ ```javascript
192
+ connector.disconnect(); // Remove all connections
193
+ connector.disconnect("connection-id"); // Remove specific connection
194
+ ```
195
+
196
+ #### `getConnections()`
197
+
198
+ Get all connections.
199
+
200
+ **Returns:** Array of connection information
201
+
202
+ **Example:**
203
+
204
+ ```javascript
205
+ const connections = connector.getConnections();
206
+ // [{ id: '...', from: 'node1', to: 'node2', fromDot: 'right', toDot: 'left' }]
207
+ ```
208
+
209
+ #### `getNodeConnections(nodeId)`
210
+
211
+ Get all connections for a specific node.
212
+
213
+ **Parameters:**
214
+
215
+ - `nodeId` (String): Node ID
216
+
217
+ **Returns:** Array of connection information
218
+
219
+ #### `updateNodePosition(nodeId)`
220
+
221
+ Update node position (called when node is moved).
222
+
223
+ **Parameters:**
224
+
225
+ - `nodeId` (String): Node ID
226
+
227
+ #### `destroy()`
228
+
229
+ Destroy the connector and clean up all resources.
230
+
231
+ **Example:**
232
+
233
+ ```javascript
234
+ connector.destroy();
235
+ ```
236
+
237
+ ## 🎨 Usage Examples
238
+
239
+ ### Vue 3
240
+
241
+ ```vue
242
+ <template>
243
+ <div
244
+ class="container"
245
+ ref="containerRef"
246
+ >
247
+ <div
248
+ class="node"
249
+ ref="node1Ref"
250
+ >
251
+ Node 1
252
+ </div>
253
+ <div
254
+ class="node"
255
+ ref="node2Ref"
256
+ >
257
+ Node 2
258
+ </div>
259
+ </div>
260
+ </template>
261
+
262
+ <script setup>
263
+ import { ref, onMounted, onBeforeUnmount } from "vue";
264
+ import Connector from "node-link-utils";
265
+
266
+ const containerRef = ref(null);
267
+ const node1Ref = ref(null);
268
+ const node2Ref = ref(null);
269
+
270
+ let connector = null;
271
+
272
+ onMounted(() => {
273
+ connector = new Connector({
274
+ container: containerRef.value,
275
+ onConnect: (connection) => {
276
+ console.log("Connection created:", connection);
277
+ },
278
+ onDisconnect: (connection) => {
279
+ console.log("Connection removed:", connection);
280
+ }
281
+ });
282
+
283
+ connector.registerNode("node1", node1Ref.value, {
284
+ dotPositions: ["right"]
285
+ });
286
+
287
+ connector.registerNode("node2", node2Ref.value, {
288
+ dotPositions: ["left"]
289
+ });
290
+ });
291
+
292
+ onBeforeUnmount(() => {
293
+ if (connector) {
294
+ connector.destroy();
295
+ }
296
+ });
297
+ </script>
298
+
299
+ <style scoped>
300
+ .container {
301
+ position: relative;
302
+ height: 600px;
303
+ background: #f5f5f5;
304
+ }
305
+
306
+ .node {
307
+ position: absolute;
308
+ padding: 20px;
309
+ background: white;
310
+ border-radius: 8px;
311
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
312
+ cursor: move;
313
+ }
314
+ </style>
315
+ ```
316
+
317
+ ### React
318
+
319
+ ```jsx
320
+ import { useEffect, useRef } from "react";
321
+ import Connector from "node-link-utils";
322
+
323
+ function App() {
324
+ const containerRef = useRef(null);
325
+ const node1Ref = useRef(null);
326
+ const node2Ref = useRef(null);
327
+ const connectorRef = useRef(null);
328
+
329
+ useEffect(() => {
330
+ if (!containerRef.current) return;
331
+
332
+ connectorRef.current = new Connector({
333
+ container: containerRef.current,
334
+ onConnect: (connection) => {
335
+ console.log("Connection created:", connection);
336
+ },
337
+ onDisconnect: (connection) => {
338
+ console.log("Connection removed:", connection);
339
+ }
340
+ });
341
+
342
+ connectorRef.current.registerNode("node1", node1Ref.current, {
343
+ dotPositions: ["right"]
344
+ });
345
+
346
+ connectorRef.current.registerNode("node2", node2Ref.current, {
347
+ dotPositions: ["left"]
348
+ });
349
+
350
+ return () => {
351
+ if (connectorRef.current) {
352
+ connectorRef.current.destroy();
353
+ }
354
+ };
355
+ }, []);
356
+
357
+ return (
358
+ <div
359
+ ref={containerRef}
360
+ style={{ position: "relative", height: "600px" }}
361
+ >
362
+ <div
363
+ ref={node1Ref}
364
+ style={{ position: "absolute", left: "100px", top: "100px" }}
365
+ >
366
+ Node 1
367
+ </div>
368
+ <div
369
+ ref={node2Ref}
370
+ style={{ position: "absolute", left: "400px", top: "100px" }}
371
+ >
372
+ Node 2
373
+ </div>
374
+ </div>
375
+ );
376
+ }
377
+ ```
378
+
379
+ ### Vanilla JavaScript
380
+
381
+ ```html
382
+ <!DOCTYPE html>
383
+ <html>
384
+ <head>
385
+ <style>
386
+ #container {
387
+ position: relative;
388
+ height: 600px;
389
+ background: #f5f5f5;
390
+ }
391
+ .node {
392
+ position: absolute;
393
+ padding: 20px;
394
+ background: white;
395
+ border-radius: 8px;
396
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
397
+ cursor: move;
398
+ }
399
+ </style>
400
+ </head>
401
+ <body>
402
+ <div id="container">
403
+ <div
404
+ id="node1"
405
+ class="node"
406
+ style="left: 100px; top: 100px;"
407
+ >
408
+ Node 1
409
+ </div>
410
+ <div
411
+ id="node2"
412
+ class="node"
413
+ style="left: 400px; top: 100px;"
414
+ >
415
+ Node 2
416
+ </div>
417
+ </div>
418
+
419
+ <script type="module">
420
+ import Connector from "node-link-utils";
421
+
422
+ const connector = new Connector({
423
+ container: document.getElementById("container"),
424
+ onConnect: (connection) => {
425
+ console.log("Connection created:", connection);
426
+ }
427
+ });
428
+
429
+ connector.registerNode("node1", document.getElementById("node1"), {
430
+ dotPositions: ["right"]
431
+ });
432
+
433
+ connector.registerNode("node2", document.getElementById("node2"), {
434
+ dotPositions: ["left"]
435
+ });
436
+ </script>
437
+ </body>
438
+ </html>
439
+ ```
440
+
441
+ ## 🎯 Advanced Features
442
+
443
+ ### Multiple Connection Points
444
+
445
+ ```javascript
446
+ // Node with both left and right connection points
447
+ connector.registerNode("centerNode", element, {
448
+ dotPositions: ["left", "right"]
449
+ });
450
+
451
+ // Node with only left connection point
452
+ connector.registerNode("endNode", element, {
453
+ dotPositions: ["left"]
454
+ });
455
+
456
+ // Node with only right connection point
457
+ connector.registerNode("startNode", element, {
458
+ dotPositions: ["right"]
459
+ });
460
+ ```
461
+
462
+ ### Custom Styling
463
+
464
+ ```javascript
465
+ const connector = new Connector({
466
+ container: container,
467
+ lineColor: "#FF6B6B", // Red connections
468
+ lineWidth: 3, // Thicker lines
469
+ dotSize: 16, // Larger dots
470
+ dotColor: "#4ECDC4", // Teal dots
471
+ deleteButtonSize: 24 // Larger delete button
472
+ });
473
+ ```
474
+
475
+ ### Event Handling
476
+
477
+ ```javascript
478
+ const connector = new Connector({
479
+ container: container,
480
+
481
+ onConnect: (connection) => {
482
+ console.log("New connection:", connection);
483
+ // { from: 'node1', to: 'node2', fromDot: 'right', toDot: 'left' }
484
+
485
+ // Save to database, update state, etc.
486
+ saveConnection(connection);
487
+ },
488
+
489
+ onDisconnect: (connection) => {
490
+ console.log("Connection removed:", connection);
491
+
492
+ // Update database, state, etc.
493
+ removeConnection(connection);
494
+ }
495
+ });
496
+ ```
497
+
498
+ ## 🔧 Browser Support
499
+
500
+ - Chrome (latest)
501
+ - Firefox (latest)
502
+ - Safari (latest)
503
+ - Edge (latest)
504
+
505
+ ## 📝 License
506
+
507
+ MIT License
508
+
509
+ ## 🤝 Contributing
510
+
511
+ Contributions, issues and feature requests are welcome!
512
+
513
+ ## 📮 Support
514
+
515
+ If you have any questions or need help, please open an issue on GitHub.
516
+
517
+ ## 🌟 Show Your Support
518
+
519
+ Give a ⭐️ if this project helped you!
520
+
521
+ ---
522
+
523
+ Made with ❤️ by the node-link-utils team
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ /*! node-link v0.0.4 | MIT License */
2
+ function t(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,o=Array(e);n<e;n++)o[n]=t[n];return o}function e(t,e,n){return e&&function(t,e){for(var n=0;n<e.length;n++){var o=e[n];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(t,a(o.key),o)}}(t.prototype,e),Object.defineProperty(t,"prototype",{writable:!1}),t}function n(t,e){var n="undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(!n){if(Array.isArray(t)||(n=l(t))||e){n&&(t=n);var o=0,i=function(){};return{s:i,n:function(){return o>=t.length?{done:!0}:{done:!1,value:t[o++]}},e:function(t){throw t},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var s,r=!0,a=!1;return{s:function(){n=n.call(t)},n:function(){var t=n.next();return r=t.done,t},e:function(t){a=!0,s=t},f:function(){try{r||null==n.return||n.return()}finally{if(a)throw s}}}}function o(t,e,n){return(e=a(e))in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function i(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(t);e&&(o=o.filter(function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable})),n.push.apply(n,o)}return n}function s(t){for(var e=1;e<arguments.length;e++){var n=null!=arguments[e]?arguments[e]:{};e%2?i(Object(n),!0).forEach(function(e){o(t,e,n[e])}):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(n)):i(Object(n)).forEach(function(e){Object.defineProperty(t,e,Object.getOwnPropertyDescriptor(n,e))})}return t}function r(e){return function(e){if(Array.isArray(e))return t(e)}(e)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(e)||l(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function a(t){var e=function(t,e){if("object"!=typeof t||!t)return t;var n=t[Symbol.toPrimitive];if(void 0!==n){var o=n.call(t,e);if("object"!=typeof o)return o;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(t)}(t,"string");return"symbol"==typeof e?e:e+""}function l(e,n){if(e){if("string"==typeof e)return t(e,n);var o={}.toString.call(e).slice(8,-1);return"Object"===o&&e.constructor&&(o=e.constructor.name),"Map"===o||"Set"===o?Array.from(e):"Arguments"===o||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(o)?t(e,n):void 0}}var c=function(){return e(function t(){var e=this,n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};!function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),o(this,"handleNodeDragMove",function(t){if(e.isDraggingNode&&e.draggedNode){var n=e.contentWrapper.getBoundingClientRect(),o=t.clientX-n.left-e.dragOffset.x,i=t.clientY-n.top-e.dragOffset.y;e.draggedNode.element.style.left="".concat(o,"px"),e.draggedNode.element.style.top="".concat(i,"px"),e.updateNodeConnections(e.draggedNode.id)}}),o(this,"handleNodeDragEnd",function(){e.isDraggingNode=!1,e.draggedNode=null,document.removeEventListener("mousemove",e.handleNodeDragMove),document.removeEventListener("mouseup",e.handleNodeDragEnd)}),o(this,"handleMouseMove",function(t){e.isDragging&&e.updateTempLine(t)}),o(this,"handleMouseUp",function(t){if(e.isDragging){e.isDragging=!1;var n=e.isSnapped?e.snapTarget:e.getNodeAtPosition(t.clientX,t.clientY),o=e.isSnapped?e.snapTargetDot:null;n&&n.id!==e.startNode.id&&e.createConnection(e.startNode,n,e.startDot,o),e.clearSnapHighlight(),e.snapTarget=null,e.snapTargetDot=null,e.isSnapped=!1,e.startDot=null,e.tempLine&&(e.svg.removeChild(e.tempLine),e.tempLine=null),document.removeEventListener("mousemove",e.handleMouseMove),document.removeEventListener("mouseup",e.handleMouseUp)}}),this.container=n.container,this.nodes=[],this.connections=[],this.isDragging=!1,this.isDraggingNode=!1,this.tempLine=null,this.draggedNode=null,this.dragOffset={x:0,y:0},this.onConnect=n.onConnect||function(){},this.onDisconnect=n.onDisconnect||function(){},this.onViewChange=n.onViewChange||function(){},this.config=s({lineColor:n.lineColor||"#155BD4",lineWidth:n.lineWidth||2,dotSize:n.dotSize||12,dotColor:n.dotColor||"#155BD4",deleteButtonSize:n.deleteButtonSize||20,enableNodeDrag:!1!==n.enableNodeDrag,snapDistance:n.snapDistance||20,enableSnap:!1!==n.enableSnap,enableZoom:!1!==n.enableZoom,enablePan:!1!==n.enablePan,minZoom:n.minZoom||.1,maxZoom:n.maxZoom||4,zoomStep:n.zoomStep||.1},n.config),this.snapTarget=null,this.isSnapped=!1,this.viewState={scale:1,translateX:0,translateY:0},this.isPanning=!1,this.panStart={x:0,y:0},this.spacePressed=!1,this.contentWrapper=null,this.init()},[{key:"init",value:function(){var t=this;if(!this.container)throw new Error("Container element is required");"static"===window.getComputedStyle(this.container).position&&(this.container.style.position="relative"),this.container.style.overflow="hidden",this.contentWrapper=document.createElement("div"),this.contentWrapper.className="connector-content-wrapper",this.contentWrapper.style.cssText="\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n transform-origin: 0 0;\n transition: none;\n ",Array.from(this.container.children).forEach(function(e){t.contentWrapper.appendChild(e)}),this.container.appendChild(this.contentWrapper),this.svg=document.createElementNS("http://www.w3.org/2000/svg","svg"),this.svg.style.position="absolute",this.svg.style.top="0",this.svg.style.left="0",this.svg.style.width="100%",this.svg.style.height="100%",this.svg.style.pointerEvents="none",this.svg.style.zIndex="1",this.contentWrapper.appendChild(this.svg),this.config.enableZoom&&this.bindZoomEvents(),this.config.enablePan&&this.bindPanEvents(),this.bindResizeEvents(),this.updateTransform()}},{key:"updateTransform",value:function(){if(this.contentWrapper){var t=this.viewState,e=t.scale,n=t.translateX,o=t.translateY;this.contentWrapper.style.transform="translate(".concat(n,"px, ").concat(o,"px) scale(").concat(e,")"),this.onViewChange({scale:e,translateX:n,translateY:o})}}},{key:"bindZoomEvents",value:function(){var t=this;this.handleWheel=function(e){if(e.ctrlKey||e.metaKey){e.preventDefault();var n=t.container.getBoundingClientRect(),o=e.clientX-n.left,i=e.clientY-n.top,s=e.deltaY<0?t.config.zoomStep:-t.config.zoomStep,r=Math.max(t.config.minZoom,Math.min(t.config.maxZoom,t.viewState.scale+s));t.zoomAtPoint(r,o,i)}},this.container.addEventListener("wheel",this.handleWheel,{passive:!1})}},{key:"bindPanEvents",value:function(){var t=this;this.handleKeyDown=function(e){"Space"!==e.code||t.spacePressed||t.isDragging||t.isDraggingNode||(e.preventDefault(),t.spacePressed=!0,t.container.style.cursor="grab")},this.handleKeyUp=function(e){"Space"===e.code&&(t.spacePressed=!1,t.container.style.cursor="",t.isPanning&&t.stopPan())},this.handlePanStart=function(e){!t.spacePressed||t.isDragging||t.isDraggingNode||(e.preventDefault(),e.stopPropagation(),t.isPanning=!0,t.panStart={x:e.clientX-t.viewState.translateX,y:e.clientY-t.viewState.translateY},t.container.style.cursor="grabbing")},this.handlePanMove=function(e){t.isPanning&&(e.preventDefault(),t.viewState.translateX=e.clientX-t.panStart.x,t.viewState.translateY=e.clientY-t.panStart.y,t.updateTransform())},this.handlePanEnd=function(){t.isPanning&&t.stopPan()},document.addEventListener("keydown",this.handleKeyDown),document.addEventListener("keyup",this.handleKeyUp),this.container.addEventListener("mousedown",this.handlePanStart),document.addEventListener("mousemove",this.handlePanMove),document.addEventListener("mouseup",this.handlePanEnd)}},{key:"stopPan",value:function(){this.isPanning=!1,this.container.style.cursor=this.spacePressed?"grab":""}},{key:"bindResizeEvents",value:function(){var t=this,e=null;this.handleResize=function(){e&&clearTimeout(e),e=setTimeout(function(){t.updateAllConnections()},100)},window.addEventListener("resize",this.handleResize),"undefined"!=typeof ResizeObserver&&(this.resizeObserver=new ResizeObserver(function(){e&&clearTimeout(e),e=setTimeout(function(){t.updateAllConnections()},100)}),this.resizeObserver.observe(this.container))}},{key:"zoomAtPoint",value:function(t,e,n){var o=this.viewState.scale,i=(e-this.viewState.translateX)/o,s=(n-this.viewState.translateY)/o;this.viewState.scale=t,this.viewState.translateX=e-i*t,this.viewState.translateY=n-s*t,this.updateTransform()}},{key:"setZoom",value:function(t){var e=this.container.getBoundingClientRect(),n=e.width/2,o=e.height/2;this.zoomAtPoint(t,n,o)}},{key:"getZoom",value:function(){return this.viewState.scale}},{key:"resetView",value:function(){this.viewState.scale=1,this.viewState.translateX=0,this.viewState.translateY=0,this.updateTransform()}},{key:"zoomIn",value:function(){var t=Math.min(this.config.maxZoom,this.viewState.scale+this.config.zoomStep);this.setZoom(t)}},{key:"zoomOut",value:function(){var t=Math.max(this.config.minZoom,this.viewState.scale-this.config.zoomStep);this.setZoom(t)}},{key:"getViewState",value:function(){return s({},this.viewState)}},{key:"registerNode",value:function(t,e){var n=this,o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};if(!this.nodes.find(function(e){return e.id===t})){var i=[];i="both"===o.dotPositions?["left","right"]:Array.isArray(o.dotPositions)?o.dotPositions.filter(function(t){return"left"===t||"right"===t}):[0===this.nodes.length?"right":"left"],i=r(new Set(i)).slice(0,2);var s={};i.forEach(function(t){var o=n.createDot(t);e.appendChild(o),s[t]={element:o,position:t}});var a={id:t,element:e,info:null==o?void 0:o.info,dots:s,dotPositions:i,connections:[]};return this.nodes.push(a),Object.values(s).forEach(function(t){n.bindDotEvents(a,t)}),this.config.enableNodeDrag&&this.bindNodeDragEvents(a),a}console.warn("节点 ".concat(t," 已存在"))}},{key:"createDot",value:function(t){var e=document.createElement("div");e.className="connector-dot";var n="right"===t?"right: -".concat(this.config.dotSize/2,"px;"):"left: -".concat(this.config.dotSize/2,"px;");return e.style.cssText="\n position: absolute;\n width: ".concat(this.config.dotSize,"px;\n height: ").concat(this.config.dotSize,"px;\n background-color: ").concat(this.config.dotColor,";\n border: 2px solid white;\n border-radius: 50%;\n cursor: pointer;\n ").concat(n,"\n top: 50%;\n transform: translateY(-50%);\n z-index: 10;\n transition: transform 0.2s;\n "),e.dataset.baseTransform="translateY(-50%)",e.addEventListener("mouseenter",function(){e.style.transform="translateY(-50%) scale(1.2)"}),e.addEventListener("mouseleave",function(){e.style.transform="translateY(-50%) scale(1)"}),e}},{key:"bindDotEvents",value:function(t,e){var n=this;e.element.addEventListener("mousedown",function(o){o.preventDefault(),o.stopPropagation(),n.isDragging=!0,n.startNode=t,n.startDot=e,n.tempLine=n.createLine(),n.svg.appendChild(n.tempLine),n.updateTempLine(o),document.addEventListener("mousemove",n.handleMouseMove),document.addEventListener("mouseup",n.handleMouseUp)})}},{key:"bindNodeDragEvents",value:function(t){var e=this,n=t.element;n.addEventListener("mousedown",function(o){if(!o.target.classList.contains("connector-dot")&&!e.spacePressed){o.preventDefault(),o.stopPropagation(),e.isDraggingNode=!0,e.draggedNode=t;var i=n.getBoundingClientRect(),s=e.contentWrapper.getBoundingClientRect();e.dragOffset={x:o.clientX-i.left,y:o.clientY-i.top};var r=i.left-s.left,a=i.top-s.top;n.style.left="".concat(r,"px"),n.style.top="".concat(a,"px"),n.style.right="",n.style.bottom="",document.addEventListener("mousemove",e.handleNodeDragMove),document.addEventListener("mouseup",e.handleNodeDragEnd)}})}},{key:"updateTempLine",value:function(t){if(this.tempLine&&this.startNode&&this.startDot){var e=this.getDotPosition(this.startNode.element,this.startDot.position),n=this.container.getBoundingClientRect(),o=t.clientX-n.left,i=t.clientY-n.top,s=this.viewState,r=s.scale,a={x:(o-s.translateX)/r,y:(i-s.translateY)/r};if(this.config.enableSnap){var l=this.checkSnap(a,this.startNode);l?(a=l.position,this.snapTarget=l.node,this.snapTargetDot=l.dot,this.isSnapped=!0,this.highlightSnapTarget(l.node,l.dot)):(this.isSnapped=!1,this.clearSnapHighlight(),this.snapTarget=null,this.snapTargetDot=null)}this.updateLine(this.tempLine,e,a,this.startDot.position)}}},{key:"createConnection",value:function(t,e){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null,o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null,i=arguments.length>4&&void 0!==arguments[4]?arguments[4]:{};if(!n&&t.dots&&(n=t.dots.right||t.dots.left||Object.values(t.dots)[0]),!o&&e.dots&&(o=e.dots.left||e.dots.right||Object.values(e.dots)[0]),!this.connections.find(function(i){return i.fromNode.id===t.id&&i.toNode.id===e.id&&i.fromDot.position===n.position&&i.toDot.position===o.position||i.fromNode.id===e.id&&i.toNode.id===t.id&&i.fromDot.position===o.position&&i.toDot.position===n.position})){var s=this.createLine();this.svg.appendChild(s);var r=this.createDeleteButton(),a={id:"".concat(t.id,"-").concat(e.id,"-").concat(n.position,"-").concat(o.position,"-").concat(Date.now()),fromNode:t,toNode:e,fromDot:n,toDot:o,line:s,deleteButton:r};return this.connections.push(a),t.connections.push(a),e.connections.push(a),this.updateConnection(a),this.bindConnectionEvents(a),i.silent||this.onConnect({from:t.id,fromInfo:t.info,to:e.id,toInfo:e.info,fromDot:n.position,toDot:o.position}),a}console.warn("该连接已存在")}},{key:"updateConnection",value:function(t){if(t&&t.line){var e=this.getDotPosition(t.fromNode.element,t.fromDot.position),n=this.getDotPosition(t.toNode.element,t.toDot.position);if(this.updateLine(t.line,e,n,t.fromDot.position,t.toDot.position),t.hoverPath&&this.updateLine(t.hoverPath,e,n,t.fromDot.position,t.toDot.position),t.deleteButton){var o=this.getBezierMidPoint(e,n,t.fromDot.position,t.toDot.position);t.deleteButton.style.left="".concat(o.x-this.config.deleteButtonSize/2,"px"),t.deleteButton.style.top="".concat(o.y-this.config.deleteButtonSize/2,"px")}}}},{key:"updateNodeConnections",value:function(t){var e=this;this.connections.filter(function(e){return e.fromNode.id===t||e.toNode.id===t}).forEach(function(t){return e.updateConnection(t)})}},{key:"updateAllConnections",value:function(){var t=this;this.connections.forEach(function(e){return t.updateConnection(e)})}},{key:"createLine",value:function(){var t=document.createElementNS("http://www.w3.org/2000/svg","path");return t.setAttribute("stroke",this.config.lineColor),t.setAttribute("stroke-width",this.config.lineWidth),t.setAttribute("fill","none"),t}},{key:"updateLine",value:function(t,e,n){var o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"right",i=arguments.length>4&&void 0!==arguments[4]?arguments[4]:"left",s=n.x-e.x,r=n.y-e.y,a=Math.sqrt(s*s+r*r),l=Math.min(.5*a,150),c="right"===o?e.x+l:e.x-l,d="right"===i?n.x+l:n.x-l,h="M ".concat(e.x," ").concat(e.y," C ").concat(c," ").concat(e.y,", ").concat(d," ").concat(n.y,", ").concat(n.x," ").concat(n.y);t.setAttribute("d",h)}},{key:"getBezierMidPoint",value:function(t,e){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"right",o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"left",i=e.x-t.x,s=e.y-t.y,r=Math.sqrt(i*i+s*s),a=Math.min(.5*r,150),l="right"===n?t.x+a:t.x-a,c=t.y,d="right"===o?e.x+a:e.x-a,h=e.y,u=.5,f=u*u,p=f*u;return{x:.125*t.x+.375*l+.375*d+p*e.x,y:.125*t.y+.375*c+.375*h+p*e.y}}},{key:"createDeleteButton",value:function(){var t=document.createElement("div");return t.className="connector-delete-btn",t.innerHTML="×",t.style.cssText="\n position: absolute;\n width: ".concat(this.config.deleteButtonSize,"px;\n height: ").concat(this.config.deleteButtonSize,"px;\n background-color: #ff4d4f;\n color: white;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n font-size: 16px;\n font-weight: bold;\n z-index: 100;\n opacity: 0;\n transition: opacity 0.2s, transform 0.2s;\n pointer-events: none;\n "),this.contentWrapper.appendChild(t),t.addEventListener("mouseenter",function(){t.style.transform="scale(1.2)"}),t.addEventListener("mouseleave",function(){t.style.transform="scale(1)",t.style.opacity="0",t.style.pointerEvents="none"}),t}},{key:"bindConnectionEvents",value:function(t){var e=this,n=t.line,o=t.deleteButton;n.style.pointerEvents="stroke",n.style.cursor="pointer";var i=document.createElementNS("http://www.w3.org/2000/svg","path");i.setAttribute("stroke","transparent"),i.setAttribute("stroke-width","20"),i.setAttribute("fill","none"),i.style.pointerEvents="stroke",i.style.cursor="pointer",i.setAttribute("d",n.getAttribute("d")),this.svg.insertBefore(i,n),t.hoverPath=i;var s=function(){o.style.opacity="1",o.style.pointerEvents="auto"},r=function(){setTimeout(function(){o.matches(":hover")||(o.style.opacity="0",o.style.pointerEvents="none")},100)};i.addEventListener("mouseenter",s),i.addEventListener("mouseleave",r),n.addEventListener("mouseenter",s),n.addEventListener("mouseleave",r),o.addEventListener("click",function(){e.disconnect(t.id)})}},{key:"disconnect",value:function(t){var e=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if(t){var o=this.connections.findIndex(function(e){return e.id===t});if(-1===o)return;var i=this.connections[o],s={from:i.fromNode.id,fromInfo:i.fromNode.info,to:i.toNode.id,toInfo:i.toNode.info,fromDot:i.fromDot.position,toDot:i.toDot.position};i.line&&i.line.parentNode&&this.svg.removeChild(i.line),i.hoverPath&&i.hoverPath.parentNode&&this.svg.removeChild(i.hoverPath),i.deleteButton&&i.deleteButton.parentNode&&i.deleteButton.parentNode.removeChild(i.deleteButton),i.fromNode.connections=i.fromNode.connections.filter(function(e){return e.id!==t}),i.toNode.connections=i.toNode.connections.filter(function(e){return e.id!==t}),this.connections.splice(o,1),n.silent||this.onDisconnect(s)}else{r(this.connections).forEach(function(t){return e.disconnect(t.id,n)})}}},{key:"getElementCenter",value:function(t){var e=t.getBoundingClientRect(),n=this.container.getBoundingClientRect(),o=e.left+e.width/2-n.left,i=e.top+e.height/2-n.top,s=this.viewState,r=s.scale;return{x:(o-s.translateX)/r,y:(i-s.translateY)/r}}},{key:"getDotPosition",value:function(t,e){var n=t.getBoundingClientRect(),o=this.container.getBoundingClientRect(),i="right"===e?n.right-o.left:n.left-o.left,s=n.top+n.height/2-o.top,r=this.viewState,a=r.scale;return{x:(i-r.translateX)/a,y:(s-r.translateY)/a}}},{key:"getNodeAtPosition",value:function(t,e){var o,i=n(this.nodes);try{for(i.s();!(o=i.n()).done;){var s=o.value,r=s.element.getBoundingClientRect();if(t>=r.left&&t<=r.right&&e>=r.top&&e<=r.bottom)return s}}catch(t){i.e(t)}finally{i.f()}return null}},{key:"checkSnap",value:function(t,e){var o=this;if(!this.config.enableSnap)return null;var i,s=null,r=null,a=1/0,l=n(this.nodes);try{var c=function(){var n=i.value;if(n.id===e.id)return 1;n.dots&&Object.values(n.dots).forEach(function(e){var i=o.getDotPosition(n.element,e.position),l=t.x-i.x,c=t.y-i.y,d=Math.sqrt(l*l+c*c);d<o.config.snapDistance&&d<a&&(a=d,s=n,r=e)})};for(l.s();!(i=l.n()).done;)c()}catch(t){l.e(t)}finally{l.f()}return s&&r?{node:s,dot:r,position:this.getDotPosition(s.element,r.position)}:null}},{key:"highlightSnapTarget",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;if(t&&t.element&&(this.clearSnapHighlight(),t.element.style.boxShadow="0 0 0 3px rgba(21, 91, 212, 0.3)",t.element.style.transform="scale(1.05)",t.element.dataset.snapped="true",e&&e.element)){var n=e.element.dataset.baseTransform||"translateY(-50%)";e.element.style.transform="".concat(n," scale(1.5)"),e.element.style.boxShadow="0 0 10px rgba(21, 91, 212, 0.6)",e.element.dataset.highlighted="true"}}},{key:"clearSnapHighlight",value:function(){this.nodes.forEach(function(t){"true"===t.element.dataset.snapped&&(t.element.style.boxShadow="",t.element.style.transform="",delete t.element.dataset.snapped,t.dots&&Object.values(t.dots).forEach(function(t){if("true"===t.element.dataset.highlighted){var e=t.element.dataset.baseTransform||"translateY(-50%)";t.element.style.transform=e,t.element.style.boxShadow="",delete t.element.dataset.highlighted}}))})}},{key:"updateNodePosition",value:function(t){var e=this,n=this.nodes.find(function(e){return e.id===t});n&&(n.dots&&Object.values(n.dots).forEach(function(t){t.coords=e.getDotPosition(n.element,t.position)}),this.updateNodeConnections(t))}},{key:"destroy",value:function(){this.disconnect(null,{silent:!0}),this.nodes.forEach(function(t){t.dots&&Object.values(t.dots).forEach(function(t){t.element&&t.element.parentNode&&t.element.parentNode.removeChild(t.element)})}),this.svg&&this.svg.parentNode&&this.svg.parentNode.removeChild(this.svg),document.removeEventListener("mousemove",this.handleMouseMove),document.removeEventListener("mouseup",this.handleMouseUp),document.removeEventListener("mousemove",this.handleNodeDragMove),document.removeEventListener("mouseup",this.handleNodeDragEnd),this.handleWheel&&this.container.removeEventListener("wheel",this.handleWheel),this.handleKeyDown&&document.removeEventListener("keydown",this.handleKeyDown),this.handleKeyUp&&document.removeEventListener("keyup",this.handleKeyUp),this.handlePanStart&&this.container.removeEventListener("mousedown",this.handlePanStart),this.handlePanMove&&document.removeEventListener("mousemove",this.handlePanMove),this.handlePanEnd&&document.removeEventListener("mouseup",this.handlePanEnd),this.handleResize&&window.removeEventListener("resize",this.handleResize),this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),this.nodes=[],this.connections=[]}},{key:"getConnections",value:function(){return this.connections.map(function(t){return{id:t.id,from:t.fromNode.id,to:t.toNode.id,fromDot:t.fromDot.position,toDot:t.toDot.position}})}},{key:"getNodeConnections",value:function(t){return this.connections.filter(function(e){return e.fromNode.id===t||e.toNode.id===t}).map(function(t){return{id:t.id,from:t.fromNode.id,to:t.toNode.id,fromDot:t.fromDot.position,toDot:t.toDot.position}})}}])}();export{c as Connector,c as default};
Binary file
Binary file
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "power-link",
3
+ "version": "0.0.4",
4
+ "description": "A pure JavaScript visual node connector for creating draggable connections between nodes. Framework-agnostic and easy to use",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "module": "dist/index.js",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "README.md",
16
+ "images"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "keywords": [
22
+ "connector",
23
+ "node-link",
24
+ "visual-connector",
25
+ "drag-line",
26
+ "bezier-curve"
27
+ ],
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/Tem-man/node-link-utils.git"
31
+ },
32
+ "author": {
33
+ "name": "Lin Ji Man",
34
+ "email": "Temman_lin@qq.com"
35
+ },
36
+ "license": "MIT",
37
+ "scripts": {
38
+ "build": "rollup -c utils/rollup.config.js"
39
+ },
40
+ "devDependencies": {
41
+ "@babel/core": "^7.25.2",
42
+ "@babel/preset-env": "^7.25.3",
43
+ "@rollup/plugin-babel": "^6.0.4",
44
+ "@rollup/plugin-commonjs": "^28.0.0",
45
+ "@rollup/plugin-node-resolve": "^15.3.0",
46
+ "@rollup/plugin-terser": "^0.4.4",
47
+ "rollup": "^4.23.0",
48
+ "rollup-plugin-postcss": "^4.0.2",
49
+ "rollup-plugin-typescript2": "^0.36.0",
50
+ "rollup-plugin-vue": "^6.0.0",
51
+ "typescript": "~5.9.3"
52
+ }
53
+ }