hover-layer.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. /**
  2. * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
  3. * SPDX-License-Identifier: MIT
  4. */
  5. /* eslint-disable complexity */
  6. import { inject, injectable } from 'inversify';
  7. import { type IPoint } from '@flowgram.ai/utils';
  8. import { SelectorBoxConfigEntity } from '@flowgram.ai/renderer';
  9. import {
  10. WorkflowDocument,
  11. WorkflowDragService,
  12. WorkflowHoverService,
  13. WorkflowLineEntity,
  14. WorkflowLinesManager,
  15. WorkflowNodeEntity,
  16. WorkflowSelectService,
  17. } from '@flowgram.ai/free-layout-core';
  18. import { WorkflowPortEntity } from '@flowgram.ai/free-layout-core';
  19. import { FlowNodeBaseType, FlowNodeTransformData } from '@flowgram.ai/document';
  20. import {
  21. EditorState,
  22. EditorStateConfigEntity,
  23. Layer,
  24. PlaygroundConfigEntity,
  25. observeEntities,
  26. observeEntity,
  27. observeEntityDatas,
  28. type LayerOptions,
  29. } from '@flowgram.ai/core';
  30. import { getSelectionBounds } from './selection-utils';
  31. const PORT_BG_CLASS_NAME = 'workflow-port-bg';
  32. export interface HoverLayerOptions extends LayerOptions {
  33. canHovered?: (e: MouseEvent, service: WorkflowHoverService) => boolean;
  34. }
  35. // eslint-disable-next-line @typescript-eslint/no-namespace
  36. export namespace HoverLayerOptions {
  37. export const DEFAULT: HoverLayerOptions = {
  38. canHovered: () => true,
  39. };
  40. }
  41. const LINE_CLASS_NAME = '.gedit-flow-activity-line';
  42. const NODE_CLASS_NAME = '.gedit-flow-activity-node';
  43. @injectable()
  44. export class HoverLayer extends Layer<HoverLayerOptions> {
  45. static type = 'HoverLayer';
  46. @inject(WorkflowDocument) document: WorkflowDocument;
  47. @inject(WorkflowSelectService) selectionService: WorkflowSelectService;
  48. @inject(WorkflowDragService) dragService: WorkflowDragService;
  49. @inject(WorkflowHoverService) hoverService: WorkflowHoverService;
  50. @inject(WorkflowLinesManager)
  51. linesManager: WorkflowLinesManager;
  52. @observeEntity(EditorStateConfigEntity)
  53. protected editorStateConfig: EditorStateConfigEntity;
  54. @observeEntity(SelectorBoxConfigEntity)
  55. protected selectorBoxConfigEntity: SelectorBoxConfigEntity;
  56. @inject(PlaygroundConfigEntity) configEntity: PlaygroundConfigEntity;
  57. /**
  58. * 监听节点 transform
  59. */
  60. @observeEntityDatas(WorkflowNodeEntity, FlowNodeTransformData)
  61. protected readonly nodeTransforms: FlowNodeTransformData[];
  62. /**
  63. * 按选中排序
  64. * @private
  65. */
  66. protected nodeTransformsWithSort: FlowNodeTransformData[] = [];
  67. autorun(): void {
  68. const { activatedNode } = this.selectionService;
  69. this.nodeTransformsWithSort = this.nodeTransforms
  70. .filter((n) => n.entity.id !== 'root' && n.entity.flowNodeType !== FlowNodeBaseType.GROUP)
  71. .reverse() // 后创建的排在前面
  72. .sort((n1) => (n1.entity === activatedNode ? -1 : 0));
  73. }
  74. /**
  75. * 监听线条
  76. */
  77. @observeEntities(WorkflowLineEntity)
  78. protected readonly lines: WorkflowLineEntity[];
  79. /**
  80. * 是否正在调整线条
  81. * @protected
  82. */
  83. get isDrawing(): boolean {
  84. return this.linesManager.isDrawing;
  85. }
  86. onReady(): void {
  87. this.options = {
  88. ...HoverLayerOptions.DEFAULT,
  89. ...this.options,
  90. };
  91. this.toDispose.pushAll([
  92. // 监听主动触发的 hover 事件
  93. this.hoverService.onUpdateHoverPosition((hoverPosition) => {
  94. const { position, target } = hoverPosition;
  95. const canvasPosition = this.config.getPosFromMouseEvent({
  96. clientX: position.x,
  97. clientY: position.y,
  98. });
  99. this.updateHoveredState(canvasPosition, target);
  100. }),
  101. // 监听画布鼠标移动事件
  102. this.listenPlaygroundEvent('mousemove', (e: MouseEvent) => {
  103. this.hoverService.hoveredPos = this.config.getPosFromMouseEvent(e);
  104. if (!this.isEnabled()) {
  105. return;
  106. }
  107. if (!this.options.canHovered!(e, this.hoverService)) {
  108. return;
  109. }
  110. const mousePos = this.config.getPosFromMouseEvent(e);
  111. // 更新 hover 状态
  112. this.updateHoveredState(mousePos, e?.target as HTMLElement);
  113. }),
  114. this.selectionService.onSelectionChanged(() => this.autorun()),
  115. // 控制触控
  116. this.listenPlaygroundEvent('touchstart', (e: MouseEvent): boolean | undefined => {
  117. if (!this.isEnabled() || this.isDrawing) {
  118. return undefined;
  119. }
  120. return this.handleDragLine(e);
  121. }),
  122. // 控制选中逻辑
  123. this.listenPlaygroundEvent('mousedown', (e: MouseEvent): boolean | undefined => {
  124. if (!this.isEnabled() || this.isDrawing) {
  125. return undefined;
  126. }
  127. const { hoveredNode } = this.hoverService;
  128. const lineDrag = this.handleDragLine(e);
  129. if (lineDrag) {
  130. return true;
  131. }
  132. const mousePos = this.config.getPosFromMouseEvent(e);
  133. const selectionBounds = getSelectionBounds(
  134. this.selectionService.selection,
  135. // 这里只考虑多选模式,单选模式已经下沉到 use-node-render 中
  136. true
  137. );
  138. if (selectionBounds.width > 0 && selectionBounds.contains(mousePos.x, mousePos.y)) {
  139. /**
  140. * 拖拽选择框
  141. */
  142. this.dragService.startDragSelectedNodes(e)?.then((dragSuccess) => {
  143. if (!dragSuccess) {
  144. // 拖拽没有成功触发了点击
  145. if (hoveredNode && hoveredNode instanceof WorkflowNodeEntity) {
  146. // 追加选择
  147. if (e.shiftKey) {
  148. this.selectionService.toggleSelect(hoveredNode);
  149. } else {
  150. this.selectionService.selectNode(hoveredNode);
  151. }
  152. } else {
  153. this.selectionService.clear();
  154. }
  155. }
  156. });
  157. // 这里会组织触发 selector box
  158. return true;
  159. } else {
  160. if (!hoveredNode) {
  161. this.selectionService.clear();
  162. }
  163. }
  164. return undefined;
  165. }),
  166. ]);
  167. }
  168. /**
  169. * 更新 hoverd
  170. * @param mousePos
  171. */
  172. updateHoveredState(mousePos: IPoint, target?: HTMLElement): void {
  173. const { hoverService } = this;
  174. const nodeTransforms = this.nodeTransformsWithSort;
  175. const outputPortHovered = this.linesManager.getPortFromMousePos(mousePos, 'output');
  176. const inputPortHovered = this.linesManager.getPortFromMousePos(mousePos, 'input');
  177. // 在两个端口叠加情况,优先使用 outputPort
  178. const portHovered = outputPortHovered || inputPortHovered;
  179. const lineDomNodes = this.playgroundNode.querySelectorAll(LINE_CLASS_NAME);
  180. const checkTargetFromLine = [...lineDomNodes].some((lineDom) =>
  181. lineDom.contains(target as HTMLElement)
  182. );
  183. if (portHovered) {
  184. if (this.document.options.twoWayConnection) {
  185. hoverService.updateHoveredKey(portHovered.id);
  186. } else {
  187. // 默认 只有 output 点位可以 hover
  188. if (portHovered.portType === 'output') {
  189. hoverService.updateHoveredKey(portHovered.id);
  190. } else if (checkTargetFromLine || target?.className?.includes?.(PORT_BG_CLASS_NAME)) {
  191. // 输入点采用获取最接近的线条
  192. const lineHovered = this.linesManager.getCloseInLineFromMousePos(mousePos);
  193. if (lineHovered) {
  194. this.updateHoveredKey(lineHovered.id);
  195. }
  196. }
  197. }
  198. return;
  199. }
  200. // Drawing 情况,不能选中节点和线条
  201. if (this.isDrawing) {
  202. return;
  203. }
  204. const nodeHovered = nodeTransforms.find((trans: FlowNodeTransformData) =>
  205. trans.bounds.contains(mousePos.x, mousePos.y)
  206. )?.entity as WorkflowNodeEntity;
  207. // 判断当前鼠标位置所在元素是否在节点内部
  208. const nodeDomNodes = this.playgroundNode.querySelectorAll(NODE_CLASS_NAME);
  209. const checkTargetFromNode = [...nodeDomNodes].some((nodeDom) =>
  210. nodeDom.contains(target as HTMLElement)
  211. );
  212. if (nodeHovered || checkTargetFromNode) {
  213. if (nodeHovered?.id) {
  214. this.updateHoveredKey(nodeHovered.id);
  215. }
  216. }
  217. // 获取最接近的线条
  218. // 线条会相交需要获取最接近点位的线条,不能删除的线条不能被选中
  219. const lineHovered = checkTargetFromLine
  220. ? this.linesManager.getCloseInLineFromMousePos(mousePos)
  221. : undefined;
  222. if (nodeHovered && lineHovered) {
  223. const nodeStackIndex = nodeHovered.renderData.stackIndex;
  224. const lineStackIndex = lineHovered.stackIndex;
  225. if (nodeStackIndex > lineStackIndex) {
  226. return this.updateHoveredKey(nodeHovered.id);
  227. } else {
  228. return this.updateHoveredKey(lineHovered.id);
  229. }
  230. }
  231. // 判断节点是否 hover
  232. if (nodeHovered) {
  233. return this.updateHoveredKey(nodeHovered.id);
  234. }
  235. // 判断线条是否 hover
  236. if (lineHovered) {
  237. return this.updateHoveredKey(lineHovered.id);
  238. }
  239. // 上述逻辑都未命中 则清空 hovered
  240. hoverService.clearHovered();
  241. const currentState = this.editorStateConfig.getCurrentState();
  242. const isMouseFriendly = currentState === EditorState.STATE_MOUSE_FRIENDLY_SELECT;
  243. // 鼠标优先,并且不是按住 shift 键,更新为小手
  244. if (isMouseFriendly && !this.editorStateConfig.isPressingShift) {
  245. this.configEntity.updateCursor('grab');
  246. }
  247. }
  248. updateHoveredKey(key: string): void {
  249. // 鼠标优先交互模式,如果是 hover,需要将鼠标的小手去掉,还原鼠标原有样式
  250. this.configEntity.updateCursor('default');
  251. this.hoverService.updateHoveredKey(key);
  252. }
  253. /**
  254. * 判断是否能够 hover
  255. * @returns 是否能 hover
  256. */
  257. isEnabled(): boolean {
  258. const currentState = this.editorStateConfig.getCurrentState();
  259. // 选择框情况禁止 hover
  260. return (
  261. // 鼠标友好模式下,也需要支持 hover 效果,不然线条选择不到
  262. // Coze 中没有使用该插件,需要在 workflow/render 包相应位置改动
  263. (currentState === EditorState.STATE_SELECT ||
  264. currentState === EditorState.STATE_MOUSE_FRIENDLY_SELECT) &&
  265. !this.selectorBoxConfigEntity.isStart &&
  266. !this.dragService.isDragging
  267. );
  268. }
  269. private handleDragLine(e: MouseEvent): boolean | undefined {
  270. const { someHovered } = this.hoverService;
  271. // 重置线条
  272. if (someHovered && someHovered instanceof WorkflowLineEntity) {
  273. this.dragService.resetLine(someHovered, e);
  274. return true;
  275. }
  276. if (
  277. someHovered &&
  278. someHovered instanceof WorkflowPortEntity &&
  279. !someHovered.disabled &&
  280. e.button !== 1
  281. ) {
  282. e.stopPropagation();
  283. e.preventDefault();
  284. this.dragService.startDrawingLine(someHovered, e);
  285. return true;
  286. }
  287. }
  288. }