power-link 2.0.5 → 2.1.0

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 CHANGED
@@ -21,6 +21,8 @@ visit [online demo](https://tem-man.github.io/power-link)
21
21
  - 📦 **Zero Dependencies** - Pure JavaScript, no framework required
22
22
  - 🎭 **Multiple Connection Points** - Support for left and right connection dots
23
23
  - 🔌 **Event Callbacks** - Listen to connection and disconnection events
24
+ - 🖱️ **Context Menu Hooks** - `onNodeContextMenu` and `onCanvasContextMenu` for custom right-click menus
25
+ - 📐 **Coordinate Conversion** - `clientToCanvas()` for converting viewport coordinates to canvas space
24
26
 
25
27
  ## 📦 Installation
26
28
 
@@ -145,6 +147,8 @@ connector.registerNode("node2", node2, {
145
147
  | `onNodeMove` | Function | `() => {}` | Callback when a node is dragged (receives `{ id, x, y }`) |
146
148
  | `onNodeSelect` | Function | `() => {}` | Callback when a node is selected/deselected (receives node info or `null`) |
147
149
  | `onNodeDelete` | Function | `() => {}` | Callback when a node is deleted (receives `{ id, info }`) |
150
+ | `onNodeContextMenu` | Function | - | Callback when right-clicking a node (receives `{ id, info, clientX, clientY }`). Library auto prevents default menu. |
151
+ | `onCanvasContextMenu` | Function | - | Callback when right-clicking empty canvas (receives `{ clientX, clientY, canvasX, canvasY }`). `canvasX/Y` are pre-converted coordinates. |
148
152
 
149
153
  ### Methods
150
154
 
@@ -338,6 +342,37 @@ const viewState = connector.getViewState();
338
342
  console.log(viewState); // { scale: 1, translateX: 0, translateY: 0 }
339
343
  ```
340
344
 
345
+ #### `clientToCanvas(clientX, clientY)`
346
+
347
+ Convert viewport (client) coordinates to canvas (contentWrapper) coordinates. Automatically accounts for current zoom and pan.
348
+
349
+ Useful for: pasting nodes at cursor position, positioning tooltips, handling external drag-and-drop, etc.
350
+
351
+ **Parameters:**
352
+
353
+ - `clientX` (Number): X coordinate in viewport (e.g. `MouseEvent.clientX`)
354
+ - `clientY` (Number): Y coordinate in viewport (e.g. `MouseEvent.clientY`)
355
+
356
+ **Returns:** `{ x, y }` - Canvas coordinates
357
+
358
+ **Example:**
359
+
360
+ ```javascript
361
+ // Paste node at mouse cursor position
362
+ canvas.addEventListener('click', (e) => {
363
+ const pos = connector.clientToCanvas(e.clientX, e.clientY);
364
+ addNodeAt(pos.x, pos.y);
365
+ });
366
+
367
+ // Or with Ctrl+V paste
368
+ document.addEventListener('keydown', (e) => {
369
+ if (e.ctrlKey && e.key === 'v') {
370
+ const pos = connector.clientToCanvas(lastMouseX, lastMouseY);
371
+ pasteNode(pos.x, pos.y);
372
+ }
373
+ });
374
+ ```
375
+
341
376
  #### `setZoom(scale)`
342
377
 
343
378
  Set the zoom level (centered on canvas).
@@ -914,6 +949,20 @@ const connector = new Connector({
914
949
  // Remove node from your state management
915
950
  // In Vue: nodes.value = nodes.value.filter(n => n.id !== id);
916
951
  // In React: setNodes(nodes.filter(n => n.id !== id));
952
+ },
953
+
954
+ // Right-click on a node - show custom context menu
955
+ onNodeContextMenu: ({ id, info, clientX, clientY }) => {
956
+ console.log("Node right-clicked:", id, "at", clientX, clientY);
957
+ // Show your custom menu at (clientX, clientY)
958
+ // e.g. showContextMenu(clientX, clientY, { id, info });
959
+ },
960
+
961
+ // Right-click on empty canvas - e.g. paste menu
962
+ onCanvasContextMenu: ({ clientX, clientY, canvasX, canvasY }) => {
963
+ console.log("Canvas right-clicked at", clientX, clientY, "→ canvas", canvasX, canvasY);
964
+ // canvasX/Y are pre-converted for paste position, tooltip placement, etc.
965
+ // e.g. showPasteMenu(clientX, clientY, { x: canvasX, y: canvasY });
917
966
  }
918
967
  });
919
968
  ```
package/dist/index.d.mts CHANGED
@@ -91,6 +91,28 @@ interface NodeDeleteInfo {
91
91
  id: string;
92
92
  info?: Record<string, unknown>;
93
93
  }
94
+ /** 节点右键菜单信息 */
95
+ interface NodeContextMenuInfo {
96
+ /** 被右键的节点 id */
97
+ id: string;
98
+ /** 节点附加信息 */
99
+ info?: Record<string, unknown>;
100
+ /** 鼠标在视口中的 X 坐标 */
101
+ clientX: number;
102
+ /** 鼠标在视口中的 Y 坐标 */
103
+ clientY: number;
104
+ }
105
+ /** 画布空白区域右键菜单信息 */
106
+ interface CanvasContextMenuInfo {
107
+ /** 鼠标在视口中的 X 坐标 */
108
+ clientX: number;
109
+ /** 鼠标在视口中的 Y 坐标 */
110
+ clientY: number;
111
+ /** 鼠标对应的画布(contentWrapper)X 坐标(已换算缩放/平移) */
112
+ canvasX: number;
113
+ /** 鼠标对应的画布(contentWrapper)Y 坐标(已换算缩放/平移) */
114
+ canvasY: number;
115
+ }
94
116
  /** 构造函数选项 */
95
117
  interface ConnectorOptions extends Partial<ConnectorConfig> {
96
118
  container: HTMLElement;
@@ -102,6 +124,16 @@ interface ConnectorOptions extends Partial<ConnectorConfig> {
102
124
  onNodeSelect?: (info: NodeSelectInfo | null) => void;
103
125
  /** 节点被删除时触发 */
104
126
  onNodeDelete?: (info: NodeDeleteInfo) => void;
127
+ /**
128
+ * 右键单击某个节点时触发。
129
+ * 库会自动 preventDefault + stopPropagation,用户只需响应回调渲染自定义菜单。
130
+ */
131
+ onNodeContextMenu?: (info: NodeContextMenuInfo) => void;
132
+ /**
133
+ * 右键单击画布空白区域时触发(节点上的右键不会冒泡到此)。
134
+ * info 中同时提供视口坐标(clientX/Y)和已换算的画布坐标(canvasX/Y)。
135
+ */
136
+ onCanvasContextMenu?: (info: CanvasContextMenuInfo) => void;
105
137
  config?: Partial<ConnectorConfig>;
106
138
  }
107
139
  /** 节点注册选项 */
@@ -259,6 +291,26 @@ declare class Connector {
259
291
  * @param nodeFactory 可选,原生 JS 场景下的节点元素工厂
260
292
  */
261
293
  import(data: ExportData, nodeFactory?: NodeFactory): Promise<void>;
294
+ /**
295
+ * 将视口(client)坐标转换为画布(contentWrapper)坐标
296
+ *
297
+ * 已自动计算当前缩放比例与平移量,可在任何需要把屏幕位置映射到画布的场景中使用:
298
+ * 粘贴节点、从外部拖入元素、定位自定义 tooltip 等。
299
+ *
300
+ * @param clientX - 鼠标/触点在视口中的 X 坐标(如 MouseEvent.clientX)
301
+ * @param clientY - 鼠标/触点在视口中的 Y 坐标(如 MouseEvent.clientY)
302
+ * @returns 对应的画布坐标 `{ x, y }`
303
+ *
304
+ * @example
305
+ * canvas.addEventListener('click', (e) => {
306
+ * const pos = connector.clientToCanvas(e.clientX, e.clientY);
307
+ * console.log('Canvas position:', pos.x, pos.y);
308
+ * });
309
+ */
310
+ clientToCanvas(clientX: number, clientY: number): {
311
+ x: number;
312
+ y: number;
313
+ };
262
314
  /**
263
315
  * 设置缩放比例(以画布中心为基准)
264
316
  */
@@ -299,4 +351,4 @@ declare class Connector {
299
351
  * @description 导出 Connector 类及相关类型定义
300
352
  */
301
353
 
302
- export { type Connection, type ConnectionInfo, Connector, type ConnectorConfig, type ConnectorNode, type ConnectorOptions, type Dot, type DotPosition, type ExportConnectionData, type ExportData, type ExportNodeData, type NodeDeleteInfo, type NodeFactory, type NodeMoveInfo, type NodeSelectInfo, type Point, type RegisterNodeOptions, type SilentOptions, type ViewState, Connector as default };
354
+ export { type CanvasContextMenuInfo, type Connection, type ConnectionInfo, Connector, type ConnectorConfig, type ConnectorNode, type ConnectorOptions, type Dot, type DotPosition, type ExportConnectionData, type ExportData, type ExportNodeData, type NodeContextMenuInfo, type NodeDeleteInfo, type NodeFactory, type NodeMoveInfo, type NodeSelectInfo, type Point, type RegisterNodeOptions, type SilentOptions, type ViewState, Connector as default };
package/dist/index.d.ts CHANGED
@@ -91,6 +91,28 @@ interface NodeDeleteInfo {
91
91
  id: string;
92
92
  info?: Record<string, unknown>;
93
93
  }
94
+ /** 节点右键菜单信息 */
95
+ interface NodeContextMenuInfo {
96
+ /** 被右键的节点 id */
97
+ id: string;
98
+ /** 节点附加信息 */
99
+ info?: Record<string, unknown>;
100
+ /** 鼠标在视口中的 X 坐标 */
101
+ clientX: number;
102
+ /** 鼠标在视口中的 Y 坐标 */
103
+ clientY: number;
104
+ }
105
+ /** 画布空白区域右键菜单信息 */
106
+ interface CanvasContextMenuInfo {
107
+ /** 鼠标在视口中的 X 坐标 */
108
+ clientX: number;
109
+ /** 鼠标在视口中的 Y 坐标 */
110
+ clientY: number;
111
+ /** 鼠标对应的画布(contentWrapper)X 坐标(已换算缩放/平移) */
112
+ canvasX: number;
113
+ /** 鼠标对应的画布(contentWrapper)Y 坐标(已换算缩放/平移) */
114
+ canvasY: number;
115
+ }
94
116
  /** 构造函数选项 */
95
117
  interface ConnectorOptions extends Partial<ConnectorConfig> {
96
118
  container: HTMLElement;
@@ -102,6 +124,16 @@ interface ConnectorOptions extends Partial<ConnectorConfig> {
102
124
  onNodeSelect?: (info: NodeSelectInfo | null) => void;
103
125
  /** 节点被删除时触发 */
104
126
  onNodeDelete?: (info: NodeDeleteInfo) => void;
127
+ /**
128
+ * 右键单击某个节点时触发。
129
+ * 库会自动 preventDefault + stopPropagation,用户只需响应回调渲染自定义菜单。
130
+ */
131
+ onNodeContextMenu?: (info: NodeContextMenuInfo) => void;
132
+ /**
133
+ * 右键单击画布空白区域时触发(节点上的右键不会冒泡到此)。
134
+ * info 中同时提供视口坐标(clientX/Y)和已换算的画布坐标(canvasX/Y)。
135
+ */
136
+ onCanvasContextMenu?: (info: CanvasContextMenuInfo) => void;
105
137
  config?: Partial<ConnectorConfig>;
106
138
  }
107
139
  /** 节点注册选项 */
@@ -259,6 +291,26 @@ declare class Connector {
259
291
  * @param nodeFactory 可选,原生 JS 场景下的节点元素工厂
260
292
  */
261
293
  import(data: ExportData, nodeFactory?: NodeFactory): Promise<void>;
294
+ /**
295
+ * 将视口(client)坐标转换为画布(contentWrapper)坐标
296
+ *
297
+ * 已自动计算当前缩放比例与平移量,可在任何需要把屏幕位置映射到画布的场景中使用:
298
+ * 粘贴节点、从外部拖入元素、定位自定义 tooltip 等。
299
+ *
300
+ * @param clientX - 鼠标/触点在视口中的 X 坐标(如 MouseEvent.clientX)
301
+ * @param clientY - 鼠标/触点在视口中的 Y 坐标(如 MouseEvent.clientY)
302
+ * @returns 对应的画布坐标 `{ x, y }`
303
+ *
304
+ * @example
305
+ * canvas.addEventListener('click', (e) => {
306
+ * const pos = connector.clientToCanvas(e.clientX, e.clientY);
307
+ * console.log('Canvas position:', pos.x, pos.y);
308
+ * });
309
+ */
310
+ clientToCanvas(clientX: number, clientY: number): {
311
+ x: number;
312
+ y: number;
313
+ };
262
314
  /**
263
315
  * 设置缩放比例(以画布中心为基准)
264
316
  */
@@ -299,4 +351,4 @@ declare class Connector {
299
351
  * @description 导出 Connector 类及相关类型定义
300
352
  */
301
353
 
302
- export { type Connection, type ConnectionInfo, Connector, type ConnectorConfig, type ConnectorNode, type ConnectorOptions, type Dot, type DotPosition, type ExportConnectionData, type ExportData, type ExportNodeData, type NodeDeleteInfo, type NodeFactory, type NodeMoveInfo, type NodeSelectInfo, type Point, type RegisterNodeOptions, type SilentOptions, type ViewState, Connector as default };
354
+ export { type CanvasContextMenuInfo, type Connection, type ConnectionInfo, Connector, type ConnectorConfig, type ConnectorNode, type ConnectorOptions, type Dot, type DotPosition, type ExportConnectionData, type ExportData, type ExportNodeData, type NodeContextMenuInfo, type NodeDeleteInfo, type NodeFactory, type NodeMoveInfo, type NodeSelectInfo, type Point, type RegisterNodeOptions, type SilentOptions, type ViewState, Connector as default };
@@ -388,6 +388,23 @@ var PowerLink = (function (exports) {
388
388
  if (target.closest && target.closest(".connector-delete-btn")) return;
389
389
  this.deselectNode();
390
390
  }, "handleContainerMouseDown");
391
+ /**
392
+ * 画布空白区域右键:节点的 contextmenu 会 stopPropagation,
393
+ * 因此只有点击空白处时此处才会触发
394
+ */
395
+ this.handleContainerContextMenu = /* @__PURE__ */ __name((e) => {
396
+ if (!this.ctx.onCanvasContextMenu) return;
397
+ e.preventDefault();
398
+ const rect = this.ctx.container.getBoundingClientRect();
399
+ const { scale, translateX, translateY } = this.ctx.viewState;
400
+ const info = {
401
+ clientX: e.clientX,
402
+ clientY: e.clientY,
403
+ canvasX: (e.clientX - rect.left - translateX) / scale,
404
+ canvasY: (e.clientY - rect.top - translateY) / scale
405
+ };
406
+ this.ctx.onCanvasContextMenu(info);
407
+ }, "handleContainerContextMenu");
391
408
  this.ctx = ctx;
392
409
  this.positionHelper = positionHelper;
393
410
  }
@@ -406,6 +423,7 @@ var PowerLink = (function (exports) {
406
423
  init() {
407
424
  document.addEventListener("keydown", this.handleKeyDown);
408
425
  this.ctx.container.addEventListener("mousedown", this.handleContainerMouseDown);
426
+ this.ctx.container.addEventListener("contextmenu", this.handleContainerContextMenu);
409
427
  }
410
428
  // ==================== 节点注册 ====================
411
429
  /**
@@ -465,6 +483,7 @@ var PowerLink = (function (exports) {
465
483
  } else {
466
484
  this.bindNodeClickEvents(node);
467
485
  }
486
+ this.bindNodeContextMenuEvents(node);
468
487
  return node;
469
488
  }
470
489
  // ==================== 触点 ====================
@@ -519,6 +538,24 @@ var PowerLink = (function (exports) {
519
538
  document.addEventListener("mouseup", this.handleMouseUp);
520
539
  });
521
540
  }
541
+ /**
542
+ * 绑定节点右键菜单事件
543
+ * 仅在用户提供 onNodeContextMenu 回调时拦截事件,否则不干预浏览器默认行为
544
+ */
545
+ bindNodeContextMenuEvents(node) {
546
+ node.element.addEventListener("contextmenu", (e) => {
547
+ if (!this.ctx.onNodeContextMenu) return;
548
+ e.preventDefault();
549
+ e.stopPropagation();
550
+ const info = {
551
+ id: node.id,
552
+ info: node.info,
553
+ clientX: e.clientX,
554
+ clientY: e.clientY
555
+ };
556
+ this.ctx.onNodeContextMenu(info);
557
+ });
558
+ }
522
559
  /**
523
560
  * 绑定节点拖拽事件(同时包含点击选中逻辑)
524
561
  */
@@ -685,6 +722,7 @@ var PowerLink = (function (exports) {
685
722
  destroy() {
686
723
  document.removeEventListener("keydown", this.handleKeyDown);
687
724
  this.ctx.container.removeEventListener("mousedown", this.handleContainerMouseDown);
725
+ this.ctx.container.removeEventListener("contextmenu", this.handleContainerContextMenu);
688
726
  this.ctx.nodes.forEach((node) => {
689
727
  if (node.dots) {
690
728
  Object.values(node.dots).forEach((dot) => {
@@ -1206,7 +1244,9 @@ var PowerLink = (function (exports) {
1206
1244
  onNodeSelect: options.onNodeSelect || (() => {
1207
1245
  }),
1208
1246
  onNodeDelete: options.onNodeDelete || (() => {
1209
- })
1247
+ }),
1248
+ onNodeContextMenu: options.onNodeContextMenu,
1249
+ onCanvasContextMenu: options.onCanvasContextMenu
1210
1250
  };
1211
1251
  this.positionHelper = new PositionHelper(this.ctx);
1212
1252
  this.viewManager = new ViewManager(this.ctx, this.positionHelper);
@@ -1447,6 +1487,31 @@ var PowerLink = (function (exports) {
1447
1487
  this.setViewState(data.viewState);
1448
1488
  }
1449
1489
  }
1490
+ // ==================== 坐标工具 API ====================
1491
+ /**
1492
+ * 将视口(client)坐标转换为画布(contentWrapper)坐标
1493
+ *
1494
+ * 已自动计算当前缩放比例与平移量,可在任何需要把屏幕位置映射到画布的场景中使用:
1495
+ * 粘贴节点、从外部拖入元素、定位自定义 tooltip 等。
1496
+ *
1497
+ * @param clientX - 鼠标/触点在视口中的 X 坐标(如 MouseEvent.clientX)
1498
+ * @param clientY - 鼠标/触点在视口中的 Y 坐标(如 MouseEvent.clientY)
1499
+ * @returns 对应的画布坐标 `{ x, y }`
1500
+ *
1501
+ * @example
1502
+ * canvas.addEventListener('click', (e) => {
1503
+ * const pos = connector.clientToCanvas(e.clientX, e.clientY);
1504
+ * console.log('Canvas position:', pos.x, pos.y);
1505
+ * });
1506
+ */
1507
+ clientToCanvas(clientX, clientY) {
1508
+ const rect = this.ctx.container.getBoundingClientRect();
1509
+ const { scale, translateX, translateY } = this.ctx.viewState;
1510
+ return {
1511
+ x: (clientX - rect.left - translateX) / scale,
1512
+ y: (clientY - rect.top - translateY) / scale
1513
+ };
1514
+ }
1450
1515
  // ==================== 视图 API ====================
1451
1516
  /**
1452
1517
  * 设置缩放比例(以画布中心为基准)
package/dist/index.js CHANGED
@@ -389,6 +389,23 @@ var _NodeManager = class _NodeManager {
389
389
  if (target.closest && target.closest(".connector-delete-btn")) return;
390
390
  this.deselectNode();
391
391
  }, "handleContainerMouseDown");
392
+ /**
393
+ * 画布空白区域右键:节点的 contextmenu 会 stopPropagation,
394
+ * 因此只有点击空白处时此处才会触发
395
+ */
396
+ this.handleContainerContextMenu = /* @__PURE__ */ __name((e) => {
397
+ if (!this.ctx.onCanvasContextMenu) return;
398
+ e.preventDefault();
399
+ const rect = this.ctx.container.getBoundingClientRect();
400
+ const { scale, translateX, translateY } = this.ctx.viewState;
401
+ const info = {
402
+ clientX: e.clientX,
403
+ clientY: e.clientY,
404
+ canvasX: (e.clientX - rect.left - translateX) / scale,
405
+ canvasY: (e.clientY - rect.top - translateY) / scale
406
+ };
407
+ this.ctx.onCanvasContextMenu(info);
408
+ }, "handleContainerContextMenu");
392
409
  this.ctx = ctx;
393
410
  this.positionHelper = positionHelper;
394
411
  }
@@ -407,6 +424,7 @@ var _NodeManager = class _NodeManager {
407
424
  init() {
408
425
  document.addEventListener("keydown", this.handleKeyDown);
409
426
  this.ctx.container.addEventListener("mousedown", this.handleContainerMouseDown);
427
+ this.ctx.container.addEventListener("contextmenu", this.handleContainerContextMenu);
410
428
  }
411
429
  // ==================== 节点注册 ====================
412
430
  /**
@@ -466,6 +484,7 @@ var _NodeManager = class _NodeManager {
466
484
  } else {
467
485
  this.bindNodeClickEvents(node);
468
486
  }
487
+ this.bindNodeContextMenuEvents(node);
469
488
  return node;
470
489
  }
471
490
  // ==================== 触点 ====================
@@ -520,6 +539,24 @@ var _NodeManager = class _NodeManager {
520
539
  document.addEventListener("mouseup", this.handleMouseUp);
521
540
  });
522
541
  }
542
+ /**
543
+ * 绑定节点右键菜单事件
544
+ * 仅在用户提供 onNodeContextMenu 回调时拦截事件,否则不干预浏览器默认行为
545
+ */
546
+ bindNodeContextMenuEvents(node) {
547
+ node.element.addEventListener("contextmenu", (e) => {
548
+ if (!this.ctx.onNodeContextMenu) return;
549
+ e.preventDefault();
550
+ e.stopPropagation();
551
+ const info = {
552
+ id: node.id,
553
+ info: node.info,
554
+ clientX: e.clientX,
555
+ clientY: e.clientY
556
+ };
557
+ this.ctx.onNodeContextMenu(info);
558
+ });
559
+ }
523
560
  /**
524
561
  * 绑定节点拖拽事件(同时包含点击选中逻辑)
525
562
  */
@@ -686,6 +723,7 @@ var _NodeManager = class _NodeManager {
686
723
  destroy() {
687
724
  document.removeEventListener("keydown", this.handleKeyDown);
688
725
  this.ctx.container.removeEventListener("mousedown", this.handleContainerMouseDown);
726
+ this.ctx.container.removeEventListener("contextmenu", this.handleContainerContextMenu);
689
727
  this.ctx.nodes.forEach((node) => {
690
728
  if (node.dots) {
691
729
  Object.values(node.dots).forEach((dot) => {
@@ -1207,7 +1245,9 @@ var _Connector = class _Connector {
1207
1245
  onNodeSelect: options.onNodeSelect || (() => {
1208
1246
  }),
1209
1247
  onNodeDelete: options.onNodeDelete || (() => {
1210
- })
1248
+ }),
1249
+ onNodeContextMenu: options.onNodeContextMenu,
1250
+ onCanvasContextMenu: options.onCanvasContextMenu
1211
1251
  };
1212
1252
  this.positionHelper = new PositionHelper(this.ctx);
1213
1253
  this.viewManager = new ViewManager(this.ctx, this.positionHelper);
@@ -1448,6 +1488,31 @@ var _Connector = class _Connector {
1448
1488
  this.setViewState(data.viewState);
1449
1489
  }
1450
1490
  }
1491
+ // ==================== 坐标工具 API ====================
1492
+ /**
1493
+ * 将视口(client)坐标转换为画布(contentWrapper)坐标
1494
+ *
1495
+ * 已自动计算当前缩放比例与平移量,可在任何需要把屏幕位置映射到画布的场景中使用:
1496
+ * 粘贴节点、从外部拖入元素、定位自定义 tooltip 等。
1497
+ *
1498
+ * @param clientX - 鼠标/触点在视口中的 X 坐标(如 MouseEvent.clientX)
1499
+ * @param clientY - 鼠标/触点在视口中的 Y 坐标(如 MouseEvent.clientY)
1500
+ * @returns 对应的画布坐标 `{ x, y }`
1501
+ *
1502
+ * @example
1503
+ * canvas.addEventListener('click', (e) => {
1504
+ * const pos = connector.clientToCanvas(e.clientX, e.clientY);
1505
+ * console.log('Canvas position:', pos.x, pos.y);
1506
+ * });
1507
+ */
1508
+ clientToCanvas(clientX, clientY) {
1509
+ const rect = this.ctx.container.getBoundingClientRect();
1510
+ const { scale, translateX, translateY } = this.ctx.viewState;
1511
+ return {
1512
+ x: (clientX - rect.left - translateX) / scale,
1513
+ y: (clientY - rect.top - translateY) / scale
1514
+ };
1515
+ }
1451
1516
  // ==================== 视图 API ====================
1452
1517
  /**
1453
1518
  * 设置缩放比例(以画布中心为基准)
package/dist/index.mjs CHANGED
@@ -385,6 +385,23 @@ var _NodeManager = class _NodeManager {
385
385
  if (target.closest && target.closest(".connector-delete-btn")) return;
386
386
  this.deselectNode();
387
387
  }, "handleContainerMouseDown");
388
+ /**
389
+ * 画布空白区域右键:节点的 contextmenu 会 stopPropagation,
390
+ * 因此只有点击空白处时此处才会触发
391
+ */
392
+ this.handleContainerContextMenu = /* @__PURE__ */ __name((e) => {
393
+ if (!this.ctx.onCanvasContextMenu) return;
394
+ e.preventDefault();
395
+ const rect = this.ctx.container.getBoundingClientRect();
396
+ const { scale, translateX, translateY } = this.ctx.viewState;
397
+ const info = {
398
+ clientX: e.clientX,
399
+ clientY: e.clientY,
400
+ canvasX: (e.clientX - rect.left - translateX) / scale,
401
+ canvasY: (e.clientY - rect.top - translateY) / scale
402
+ };
403
+ this.ctx.onCanvasContextMenu(info);
404
+ }, "handleContainerContextMenu");
388
405
  this.ctx = ctx;
389
406
  this.positionHelper = positionHelper;
390
407
  }
@@ -403,6 +420,7 @@ var _NodeManager = class _NodeManager {
403
420
  init() {
404
421
  document.addEventListener("keydown", this.handleKeyDown);
405
422
  this.ctx.container.addEventListener("mousedown", this.handleContainerMouseDown);
423
+ this.ctx.container.addEventListener("contextmenu", this.handleContainerContextMenu);
406
424
  }
407
425
  // ==================== 节点注册 ====================
408
426
  /**
@@ -462,6 +480,7 @@ var _NodeManager = class _NodeManager {
462
480
  } else {
463
481
  this.bindNodeClickEvents(node);
464
482
  }
483
+ this.bindNodeContextMenuEvents(node);
465
484
  return node;
466
485
  }
467
486
  // ==================== 触点 ====================
@@ -516,6 +535,24 @@ var _NodeManager = class _NodeManager {
516
535
  document.addEventListener("mouseup", this.handleMouseUp);
517
536
  });
518
537
  }
538
+ /**
539
+ * 绑定节点右键菜单事件
540
+ * 仅在用户提供 onNodeContextMenu 回调时拦截事件,否则不干预浏览器默认行为
541
+ */
542
+ bindNodeContextMenuEvents(node) {
543
+ node.element.addEventListener("contextmenu", (e) => {
544
+ if (!this.ctx.onNodeContextMenu) return;
545
+ e.preventDefault();
546
+ e.stopPropagation();
547
+ const info = {
548
+ id: node.id,
549
+ info: node.info,
550
+ clientX: e.clientX,
551
+ clientY: e.clientY
552
+ };
553
+ this.ctx.onNodeContextMenu(info);
554
+ });
555
+ }
519
556
  /**
520
557
  * 绑定节点拖拽事件(同时包含点击选中逻辑)
521
558
  */
@@ -682,6 +719,7 @@ var _NodeManager = class _NodeManager {
682
719
  destroy() {
683
720
  document.removeEventListener("keydown", this.handleKeyDown);
684
721
  this.ctx.container.removeEventListener("mousedown", this.handleContainerMouseDown);
722
+ this.ctx.container.removeEventListener("contextmenu", this.handleContainerContextMenu);
685
723
  this.ctx.nodes.forEach((node) => {
686
724
  if (node.dots) {
687
725
  Object.values(node.dots).forEach((dot) => {
@@ -1203,7 +1241,9 @@ var _Connector = class _Connector {
1203
1241
  onNodeSelect: options.onNodeSelect || (() => {
1204
1242
  }),
1205
1243
  onNodeDelete: options.onNodeDelete || (() => {
1206
- })
1244
+ }),
1245
+ onNodeContextMenu: options.onNodeContextMenu,
1246
+ onCanvasContextMenu: options.onCanvasContextMenu
1207
1247
  };
1208
1248
  this.positionHelper = new PositionHelper(this.ctx);
1209
1249
  this.viewManager = new ViewManager(this.ctx, this.positionHelper);
@@ -1444,6 +1484,31 @@ var _Connector = class _Connector {
1444
1484
  this.setViewState(data.viewState);
1445
1485
  }
1446
1486
  }
1487
+ // ==================== 坐标工具 API ====================
1488
+ /**
1489
+ * 将视口(client)坐标转换为画布(contentWrapper)坐标
1490
+ *
1491
+ * 已自动计算当前缩放比例与平移量,可在任何需要把屏幕位置映射到画布的场景中使用:
1492
+ * 粘贴节点、从外部拖入元素、定位自定义 tooltip 等。
1493
+ *
1494
+ * @param clientX - 鼠标/触点在视口中的 X 坐标(如 MouseEvent.clientX)
1495
+ * @param clientY - 鼠标/触点在视口中的 Y 坐标(如 MouseEvent.clientY)
1496
+ * @returns 对应的画布坐标 `{ x, y }`
1497
+ *
1498
+ * @example
1499
+ * canvas.addEventListener('click', (e) => {
1500
+ * const pos = connector.clientToCanvas(e.clientX, e.clientY);
1501
+ * console.log('Canvas position:', pos.x, pos.y);
1502
+ * });
1503
+ */
1504
+ clientToCanvas(clientX, clientY) {
1505
+ const rect = this.ctx.container.getBoundingClientRect();
1506
+ const { scale, translateX, translateY } = this.ctx.viewState;
1507
+ return {
1508
+ x: (clientX - rect.left - translateX) / scale,
1509
+ y: (clientY - rect.top - translateY) / scale
1510
+ };
1511
+ }
1447
1512
  // ==================== 视图 API ====================
1448
1513
  /**
1449
1514
  * 设置缩放比例(以画布中心为基准)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "power-link",
3
- "version": "2.0.5",
3
+ "version": "2.1.0",
4
4
  "description": "A pure TypeScript visual node connector for creating draggable connections between nodes. Framework-agnostic and easy to use",
5
5
  "author": {
6
6
  "name": "Lin Ji Man",