Skip to content

refactor: optimize Vue canvas interaction logic #1297

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from

Conversation

chilingling
Copy link
Member

@chilingling chilingling commented Apr 9, 2025

English | 简体中文

PR 重构: 优化 Vue 画布交互逻辑,提取元素定位和交互方法

PR Checklist

Please check if your PR fulfills the following requirements:

  • The commit message follows our Commit Message Guidelines
  • Tests for the changes have been added (for bug fixes / features)
  • Docs have been added / updated (for bug fixes / features)
  • Built its own designer, fully self-validated

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • Other... Please describe:

Background and solution

What is the current behavior?

Issue Number: N/A

What is the new behavior?

【功能修改点说明】:

  1. 修复: Vue画布下 Element Plus 的 DatePicker、组件被 disabled 的场景无法选中组件的 bug。
  2. 修复:初始状态下,没有选中任何组件,hover 组件不生效的 bug。
  3. 增加:插入节点之后的选中节点的延时,确保画布已经更新,避免拖拽更换节点后,节点不选中的 bug。
  4. 增加: Element Plus DatePicker 组件。
  5. 增加:右键选中节点的功能,在组件disabled 状态时,右键能够触发事件,从而进行选中。
  6. 增加:点击 hover 节点左上角组件名进行选中的逻辑,在组件被 disabled 状态,data-uid 属性无法挂载的场景下特别有用。

【重构修改说明】

  1. 抽取节点选中、hover 相关逻辑,使得选中、hover 逻辑集中;并根据配置区分 Vue 画布和通用画布的选中hover逻辑。
  2. 增加 Vue 画布下的元素定位逻辑,使得 disabled 状态的组件以及无法挂在 data-uid 属性的组件也能被选中。
  3. 将 inactiveHover 和 hoverState 合并,通过 isInactiveNode 进行区分当前 hover 的是否是当前画布的节点。
  4. 将 hover、insertLine 的相关节点从 CanvasAction 组件中分别抽离到 CanvasHoverCanvasInsertLine 组件,避免在多选的情况下,重复渲染多个 hover 节点。

【通用画布元素定位新逻辑说明】

  1. 查询最近的带有 data-uid 属性的 DOM 节点
  2. 通过 data-uid 属性值查询得到对应的 schema 节点。
  3. 通过 getBoundingClientRect 计算DOM 节点的矩形信息。

适配:与画布技术栈无关,通用性强
缺陷:如果画布无法挂载 data-uid 属性到 DOM 节点上,那么该节点无法反查到对应的 node 节点,导致 hover 、选中等逻辑无法生效。

【Vue 画布元素定位新逻辑说明】
前置依赖:需要 vue 画布注入 __vueComponent 到当前 DOM 节点中。

  1. 通过鼠标事件获取到当前点击节点树最近的带有 __vueComponent 的节点,通过 __vueComponent 获取到 vue 实例。
  2. 通过 vue 实例获取到最近的带有 schema.id 的真正 vue 实例,通过 id 获取到对应的 node 节点。
  3. 计算真正的 vue 实例的 rect 信息,更新到 hoverState 中。

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

测试页面 schema:

{
  "componentName": "Page",
  "css": ".page-base-style {\n  padding: 24px;\n  background: #ffffff;\n}\n.block-base-style {\n  margin: 16px;\n}\n.component-base-style {\n  margin: 8px;\n}\n.text-ofpin {\n  margin: 8px;\n  font-size: 18px;\n  font-weight: 700;\n}\n.text-tkonm {\n  margin: 8px;\n  font-size: 20px;\n  font-weight: 700;\n  display: block;\n}\n.div-kqykn {\n  margin: 8px;\n  margin-top: 20px;\n}\n.div-cukqn {\n  margin: 8px;\n  margin-top: 20px;\n}\n.text-mrjfb {\n  margin: 8px;\n  font-size: 20px;\n  font-weight: 700;\n}\n.text-qovif {\n  margin: 8px;\n  font-size: 20px;\n  font-weight: 700;\n}\n.text-gojtr {\n  margin: 8px;\n  font-size: 20px;\n  font-weight: 700;\n}\n.text-nqswn {\n  margin: 8px;\n  font-size: 20px;\n  font-weight: 700;\n  display: block;\n}\n.text-flgjp {\n  margin: 8px;\n  font-size: 20px;\n  font-weight: 700;\n}\n.text-jvlui {\n  margin: 8px;\n  font-size: 20px;\n  font-weight: 700;\n  display: block;\n}\n.text-ivmur {\n  margin: 8px;\n  font-size: 20px;\n  font-weight: 700;\n  display: block;\n}\n.text-guubn {\n  margin: 8px;\n  font-size: 20px;\n  font-weight: 700;\n}\n.div-usgfl {\n  margin: 8px;\n  margin-top: 20px;\n}\n.div-rufmu {\n  margin: 8px;\n  margin-top: 20px;\n  padding-bottom: 20px;\n}\n.div-orqwq {\n  margin: 8px;\n  margin-top: 20px;\n}\n.text-ljlhh {\n  margin: 8px;\n  font-size: 20px;\n  font-weight: 700;\n}\n.text-qmwuv {\n  margin: 8px;\n  font-size: 20px;\n  font-weight: 700;\n}\n",
  "props": {
    "className": "page-base-style"
  },
  "lifeCycles": {},
  "children": [
    {
      "componentName": "div",
      "props": {
        "className": "component-base-style div-rufmu"
      },
      "children": [
        {
          "componentName": "div",
          "props": {
            "className": "component-base-style"
          },
          "children": [],
          "id": "666b9254"
        },
        {
          "componentName": "div",
          "props": {
            "className": "component-base-style"
          },
          "children": [
            {
              "componentName": "Text",
              "props": {
                "style": "display: inline-block;",
                "className": "component-base-style text-ljlhh",
                "text": "特殊组件:ElDatePicker"
              },
              "children": [],
              "id": "e3633545"
            },
            {
              "componentName": "Text",
              "props": {
                "style": "display: inline-block;",
                "className": "component-base-style",
                "text": "element-plus 的 DatePicker 是一个 Fragment 组件,实际的内容被挂在到 body 上面了,所以没法挂在 data-uid 属性,以前的方法无法选中"
              },
              "children": [],
              "id": "52564e2d"
            }
          ],
          "id": "53262513"
        },
        {
          "componentName": "ElDatePicker",
          "props": {
            "className": "component-base-style",
            "disabled": false
          },
          "children": [],
          "id": "36366565"
        }
      ],
      "id": "15325456"
    },
    {
      "componentName": "div",
      "props": {
        "className": "component-base-style div-usgfl"
      },
      "children": [
        {
          "componentName": "div",
          "props": {
            "className": "component-base-style"
          },
          "children": [
            {
              "componentName": "Text",
              "props": {
                "style": "display: inline-block;",
                "className": "component-base-style text-guubn",
                "text": "Disabled 组件被禁用场景"
              },
              "children": [],
              "id": "2c646862"
            },
            {
              "componentName": "Text",
              "props": {
                "style": "display: inline-block;",
                "className": "component-base-style",
                "text": "组件被禁用,可以通过右键,或者是点击 hover 时左上角显示的组件名称进行选中"
              },
              "children": [],
              "id": "635436c2"
            }
          ],
          "id": "24f64625"
        },
        {
          "componentName": "TinyButton",
          "props": {
            "text": "按钮文案",
            "className": "component-base-style",
            "disabled": true
          },
          "children": [],
          "id": "6155c24d"
        },
        {
          "componentName": "TinySearch",
          "props": {
            "modelValue": "",
            "placeholder": "输入关键词",
            "className": "component-base-style",
            "disabled": true
          },
          "children": [],
          "id": "35153244"
        },
        {
          "componentName": "TinyInput",
          "props": {
            "placeholder": "请输入",
            "modelValue": "",
            "className": "component-base-style",
            "disabled": true
          },
          "children": [],
          "id": "248e4d21"
        }
      ],
      "id": "36655334"
    },
    {
      "componentName": "div",
      "props": {
        "className": "component-base-style div-orqwq"
      },
      "children": [
        {
          "componentName": "Text",
          "props": {
            "className": "component-base-style text-ivmur",
            "text": "循环渲染"
          },
          "children": [],
          "id": "6a65454a"
        },
        {
          "componentName": "TinyButton",
          "props": {
            "text": {
              "type": "JSExpression",
              "value": "item.text"
            },
            "className": "component-base-style",
            "type": {
              "type": "JSExpression",
              "value": "item.type"
            }
          },
          "children": [],
          "id": "2345445d",
          "loop": {
            "type": "JSExpression",
            "value": "[\n  {\n    text: '主要按钮',\n    type: 'primary'\n  },\n  {\n    text: '次要按钮'\n  },\n  {\n    text: '成功按钮',\n    type: 'success'\n  },\n  {\n    text: '信息按钮',\n    type: 'info'\n  },\n  {\n    text: '警告按钮',\n    type: 'warning'\n  }\n]"
          },
          "loopArgs": [
            "item",
            "index"
          ]
        }
      ],
      "id": "54c56422"
    },
    {
      "componentName": "div",
      "props": {
        "className": "component-base-style"
      },
      "children": [
        {
          "componentName": "Text",
          "props": {
            "style": "display: inline-block;",
            "className": "component-base-style text-ofpin",
            "text": "布局与容器"
          },
          "children": [],
          "id": "4b633543"
        },
        {
          "componentName": "CanvasRowColContainer",
          "props": {
            "rowGap": "16px",
            "className": "component-base-style"
          },
          "children": [
            {
              "componentName": "CanvasRow",
              "props": {
                "rowGap": "16px",
                "colGap": "16px"
              },
              "children": [
                {
                  "componentName": "CanvasCol",
                  "props": {
                    "rowGap": "16px",
                    "colGap": "16px",
                    "grow": true,
                    "shrink": true,
                    "widthType": "auto"
                  },
                  "id": "32423124"
                }
              ],
              "id": "43315562"
            }
          ],
          "id": "15594525"
        },
        {
          "componentName": "div",
          "props": {
            "className": "component-base-style"
          },
          "children": [],
          "id": "2242c153"
        },
        {
          "componentName": "CanvasFlexBox",
          "props": {
            "flexDirection": "row",
            "gap": "8px",
            "padding": "8px",
            "className": "component-base-style"
          },
          "children": [],
          "id": "a6225365"
        },
        {
          "componentName": "CanvasSection",
          "props": {
            "className": "component-base-style"
          },
          "children": [],
          "id": "23336513"
        },
        {
          "componentName": "TinyLayout",
          "props": {
            "className": "component-base-style"
          },
          "children": [
            {
              "componentName": "TinyRow",
              "props": {
                "style": "padding: 10px;"
              },
              "children": [
                {
                  "componentName": "TinyCol",
                  "props": {
                    "span": 3
                  },
                  "id": "525d2664"
                },
                {
                  "componentName": "TinyCol",
                  "props": {
                    "span": 3
                  },
                  "id": "4534364b"
                },
                {
                  "componentName": "TinyCol",
                  "props": {
                    "span": 3
                  },
                  "id": "6423ca16"
                },
                {
                  "componentName": "TinyCol",
                  "props": {
                    "span": 3
                  },
                  "id": "4bd32366"
                }
              ],
              "id": "4c332165"
            },
            {
              "componentName": "TinyRow",
              "props": {
                "style": "padding: 10px;"
              },
              "children": [
                {
                  "componentName": "TinyCol",
                  "props": {
                    "span": 3
                  },
                  "id": "15233246"
                },
                {
                  "componentName": "TinyCol",
                  "props": {
                    "span": 3
                  },
                  "id": "a3f14353"
                },
                {
                  "componentName": "TinyCol",
                  "props": {
                    "span": 3
                  },
                  "id": "63536264"
                },
                {
                  "componentName": "TinyCol",
                  "props": {
                    "span": 3
                  },
                  "id": "9b51465e"
                }
              ],
              "id": "9442364b"
            }
          ],
          "id": "23466436"
        }
      ],
      "id": "22124528"
    },
    {
      "componentName": "div",
      "props": {
        "className": "component-base-style div-kqykn"
      },
      "children": [
        {
          "componentName": "Text",
          "props": {
            "className": "component-base-style text-tkonm",
            "text": "基础元素"
          },
          "children": [],
          "id": "132a464d"
        },
        {
          "componentName": "Text",
          "props": {
            "style": "display: inline-block;",
            "text": "TinyEngine 前端可视化设计器,为设计器开发者提供定制服务,在线构建出自己专属的设计器。",
            "className": "component-base-style"
          },
          "children": [],
          "id": "11314228"
        },
        {
          "componentName": "Icon",
          "props": {
            "name": "IconDel",
            "className": "component-base-style"
          },
          "children": [],
          "id": "e21a5724"
        },
        {
          "componentName": "Img",
          "props": {
            "src": "https://tinyengine-assets.obs.cn-north-4.myhuaweicloud.com/files/designer-default-icon.jpg",
            "className": "component-base-style"
          },
          "children": [],
          "id": "64b65e28"
        },
        {
          "componentName": "p",
          "children": "TinyEngine 前端可视化设计器致力于通过友好的用户交互提升业务应用的开发效率。",
          "props": {
            "className": "component-base-style"
          },
          "id": "25125564"
        },
        {
          "componentName": "hr",
          "props": {
            "className": "component-base-style"
          },
          "children": [],
          "id": "64225325"
        },
        {
          "componentName": "a",
          "children": "链接",
          "props": {
            "className": "component-base-style"
          },
          "id": "34553331"
        },
        {
          "componentName": "h1",
          "props": {
            "className": "component-base-style"
          },
          "children": "Heading",
          "id": "6355c334"
        },
        {
          "componentName": "video",
          "props": {
            "src": "img/webNova.jpg",
            "width": "200",
            "height": "100",
            "style": "border:1px solid #ccc",
            "className": "component-base-style"
          },
          "children": [],
          "id": "63266244"
        },
        {
          "componentName": "TinyButton",
          "props": {
            "text": "按钮文案",
            "className": "component-base-style"
          },
          "children": [],
          "id": "4c122462"
        },
        {
          "componentName": "div",
          "props": {
            "className": "component-base-style"
          },
          "children": [
            {
              "componentName": "TinyButton",
              "props": {
                "text": "提交",
                "type": "primary",
                "style": "margin: 0 5px 0 5px;"
              },
              "id": "43563525"
            },
            {
              "componentName": "TinyButton",
              "props": {
                "text": "重置",
                "style": "margin: 0 5px 0 5px;"
              },
              "id": "56d34256"
            },
            {
              "componentName": "TinyButton",
              "props": {
                "text": "取消"
              },
              "id": "85226153"
            },
            {
              "componentName": "TinyButtonGroup",
              "props": {
                "data": [
                  {
                    "text": "Button1",
                    "value": "1"
                  },
                  {
                    "text": "Button2",
                    "value": "2"
                  },
                  {
                    "text": "Button3",
                    "value": "3"
                  }
                ],
                "modelValue": "1",
                "className": "component-base-style"
              },
              "children": [],
              "id": "62231442"
            },
            {
              "componentName": "TinySearch",
              "props": {
                "modelValue": "",
                "placeholder": "输入关键词",
                "className": "component-base-style"
              },
              "children": [],
              "id": "13552646"
            }
          ],
          "id": "55e51933"
        }
      ],
      "id": "52868343"
    },
    {
      "componentName": "div",
      "props": {
        "className": "component-base-style div-cukqn"
      },
      "children": [
        {
          "componentName": "Text",
          "props": {
            "style": "display: inline-block;",
            "className": "component-base-style text-mrjfb",
            "text": "高级元素"
          },
          "children": [],
          "id": "53527565"
        },
        {
          "componentName": "Slot",
          "props": {
            "className": "component-base-style"
          },
          "children": [],
          "id": "2432f143",
          "show": true,
          "showEye": false
        },
        {
          "componentName": "RouterView",
          "props": {
            "className": "component-base-style"
          },
          "children": [],
          "id": "63e53416"
        },
        {
          "componentName": "RouterLink",
          "props": {
            "className": "component-base-style"
          },
          "children": [
            {
              "componentName": "Text",
              "props": {
                "text": "路由文本"
              },
              "id": "67c62462",
              "children": []
            },
            {
              "componentName": "div",
              "props": {
                "style": "text-align: center; padding: 8px 12px; box-shadow: 0 0 4px #0003;",
                "className": "component-base-style"
              },
              "children": [
                {
                  "componentName": "RouterLink",
                  "props": {
                    "to": "",
                    "style": "display: inline-flex; gap: 8px; padding: 10px 20px; color: inherit; text-decoration: none;"
                  },
                  "children": [
                    {
                      "componentName": "Icon",
                      "props": {
                        "name": "IconPublicHome",
                        "style": "margin-top: 3px;"
                      },
                      "id": "63d45626"
                    },
                    {
                      "componentName": "Text",
                      "props": {
                        "text": "首页"
                      },
                      "id": "62664355"
                    }
                  ],
                  "id": "57951e33"
                },
                {
                  "componentName": "RouterLink",
                  "props": {
                    "to": "",
                    "style": "display: inline-flex; gap: 8px; padding: 10px 20px; color: inherit; text-decoration: none;"
                  },
                  "children": [
                    {
                      "componentName": "Icon",
                      "props": {
                        "name": "IconTaskCooperation",
                        "style": "margin-top: 3px;"
                      },
                      "id": "4d442e54"
                    },
                    {
                      "componentName": "Text",
                      "props": {
                        "text": "介绍"
                      },
                      "id": "63544924"
                    }
                  ],
                  "id": "5212526a"
                },
                {
                  "componentName": "RouterLink",
                  "props": {
                    "to": "",
                    "style": "display: inline-flex; gap: 8px; padding: 10px 20px; color: inherit; text-decoration: none;"
                  },
                  "children": [
                    {
                      "componentName": "Icon",
                      "props": {
                        "name": "IconText",
                        "style": "margin-top: 3px;"
                      },
                      "id": "d21c6a33"
                    },
                    {
                      "componentName": "Text",
                      "props": {
                        "text": "文档"
                      },
                      "id": "32474d6e"
                    }
                  ],
                  "id": "2b3b6655"
                }
              ],
              "id": "43553314"
            }
          ],
          "id": "f4343566"
        },
        {
          "componentName": "div",
          "props": {
            "style": "padding: 8px 12px; border-right: 1px solid #0003;",
            "className": "component-base-style"
          },
          "children": [
            {
              "componentName": "RouterLink",
              "props": {
                "to": "",
                "style": "display: flex; gap: 8px; padding: 10px 20px; color: inherit; text-decoration: none;"
              },
              "children": [
                {
                  "componentName": "Icon",
                  "props": {
                    "name": "IconPublicHome",
                    "style": "margin-top: 3px;"
                  },
                  "id": "2644a122"
                },
                {
                  "componentName": "Text",
                  "props": {
                    "text": "首页"
                  },
                  "id": "b1c5ea66"
                }
              ],
              "id": "3552b433"
            },
            {
              "componentName": "RouterLink",
              "props": {
                "to": "",
                "style": "display: flex; gap: 8px; padding: 10px 20px; color: inherit; text-decoration: none;"
              },
              "children": [
                {
                  "componentName": "Icon",
                  "props": {
                    "name": "IconTaskCooperation",
                    "style": "margin-top: 3px;"
                  },
                  "id": "32696f11"
                },
                {
                  "componentName": "Text",
                  "props": {
                    "text": "介绍"
                  },
                  "id": "16621635"
                }
              ],
              "id": "3ab451b5"
            },
            {
              "componentName": "RouterLink",
              "props": {
                "to": "",
                "style": "display: flex; gap: 8px; padding: 10px 20px; color: inherit; text-decoration: none;"
              },
              "children": [
                {
                  "componentName": "Icon",
                  "props": {
                    "name": "IconText",
                    "style": "margin-top: 3px;"
                  },
                  "id": "53c63452"
                },
                {
                  "componentName": "Text",
                  "props": {
                    "text": "文档"
                  },
                  "id": "5213222f"
                }
              ],
              "id": "16223245"
            },
            {
              "componentName": "RouterLink",
              "props": {
                "to": "",
                "style": "display: flex; gap: 8px; padding: 10px 20px; color: inherit; text-decoration: none;"
              },
              "children": [
                {
                  "componentName": "Icon",
                  "props": {
                    "name": "IconText",
                    "style": "margin-top: 3px;"
                  },
                  "id": "43432e66"
                },
                {
                  "componentName": "Text",
                  "props": {
                    "text": "文档"
                  },
                  "id": "63275465"
                }
              ],
              "id": "e4434442"
            },
            {
              "componentName": "RouterLink",
              "props": {
                "to": "",
                "style": "display: flex; gap: 8px; padding: 10px 20px; color: inherit; text-decoration: none;"
              },
              "children": [
                {
                  "componentName": "Icon",
                  "props": {
                    "name": "IconText",
                    "style": "margin-top: 3px;"
                  },
                  "id": "63225541"
                },
                {
                  "componentName": "Text",
                  "props": {
                    "text": "文档"
                  },
                  "id": "2b3934c6"
                }
              ],
              "id": "65a1634c"
            }
          ],
          "id": "6536546f"
        },
        {
          "componentName": "Collection",
          "props": {
            "className": "component-base-style"
          },
          "children": [],
          "id": "43166746"
        }
      ],
      "id": "23624f49"
    },
    {
      "componentName": "div",
      "props": {
        "className": "component-base-style"
      },
      "children": [
        {
          "componentName": "Text",
          "props": {
            "style": "display: inline-block;",
            "className": "component-base-style text-qovif",
            "text": "表单类型"
          },
          "children": [],
          "id": "26542636"
        },
        {
          "componentName": "TinySelect",
          "props": {
            "modelValue": "",
            "placeholder": "请选择",
            "options": [
              {
                "value": "1",
                "label": "黄金糕"
              },
              {
                "value": "2",
                "label": "双皮奶"
              }
            ],
            "className": "component-base-style"
          },
          "children": [],
          "id": "23244363"
        },
        {
          "componentName": "TinySwitch",
          "props": {
            "modelValue": "",
            "className": "component-base-style"
          },
          "children": [],
          "id": "45f27546"
        },
        {
          "componentName": "TinyCheckboxGroup",
          "props": {
            "modelValue": [
              "name1",
              "name2"
            ],
            "type": "checkbox",
            "options": [
              {
                "text": "复选框1",
                "label": "name1"
              },
              {
                "text": "复选框2",
                "label": "name2"
              },
              {
                "text": "复选框3",
                "label": "name3"
              }
            ],
            "className": "component-base-style"
          },
          "children": [],
          "id": "45425331"
        },
        {
          "componentName": "TinyCheckboxGroup",
          "props": {
            "modelValue": [],
            "className": "component-base-style"
          },
          "children": [
            {
              "componentName": "TinyCheckboxButton",
              "children": [
                {
                  "componentName": "div",
                  "id": "515223e4"
                }
              ],
              "id": "21465323"
            }
          ],
          "id": "53433672"
        },
        {
          "componentName": "TinyInput",
          "props": {
            "placeholder": "请输入",
            "modelValue": "",
            "className": "component-base-style"
          },
          "children": [],
          "id": "545f4433"
        },
        {
          "componentName": "TinyRadio",
          "props": {
            "label": "1",
            "text": "单选文本",
            "className": "component-base-style"
          },
          "children": [],
          "id": "26535652"
        },
        {
          "componentName": "TinyCheckbox",
          "props": {
            "text": "复选框文案",
            "className": "component-base-style"
          },
          "children": [],
          "id": "3553f455"
        },
        {
          "componentName": "TinyDatePicker",
          "props": {
            "placeholder": "请输入",
            "modelValue": "",
            "className": "component-base-style"
          },
          "children": [],
          "id": "33be5551"
        },
        {
          "componentName": "TinyNumeric",
          "props": {
            "allow-empty": true,
            "placeholder": "请输入",
            "controlsPosition": "right",
            "step": 1,
            "className": "component-base-style"
          },
          "children": [],
          "id": "1632551b"
        },
        {
          "componentName": "TinyForm",
          "props": {
            "labelWidth": "80px",
            "labelPosition": "top",
            "className": "component-base-style"
          },
          "children": [
            {
              "componentName": "TinyFormItem",
              "props": {
                "label": "人员"
              },
              "children": [
                {
                  "componentName": "TinyInput",
                  "props": {
                    "placeholder": "请输入",
                    "modelValue": ""
                  },
                  "id": "28b54342"
                }
              ],
              "id": "42a3a256"
            },
            {
              "componentName": "TinyFormItem",
              "props": {
                "label": "密码"
              },
              "children": [
                {
                  "componentName": "TinyInput",
                  "props": {
                    "placeholder": "请输入",
                    "modelValue": "",
                    "type": "password"
                  },
                  "id": "34e56631"
                }
              ],
              "id": "6f5332f6"
            },
            {
              "componentName": "TinyFormItem",
              "props": {
                "label": ""
              },
              "children": [
                {
                  "componentName": "TinyButton",
                  "props": {
                    "text": "提交",
                    "type": "primary",
                    "style": "margin-right: 10px"
                  },
                  "id": "655435a5"
                },
                {
                  "componentName": "TinyButton",
                  "props": {
                    "text": "重置",
                    "type": "primary"
                  },
                  "id": "26553353"
                }
              ],
              "id": "1524561e"
            }
          ],
          "id": "22246525"
        }
      ],
      "id": "62246843"
    },
    {
      "componentName": "div",
      "props": {
        "className": "component-base-style"
      },
      "children": [
        {
          "componentName": "Text",
          "props": {
            "style": "display: inline-block;",
            "className": "component-base-style text-gojtr",
            "text": "表格类型"
          },
          "children": [],
          "id": "65111b51"
        },
        {
          "componentName": "TinyGrid",
          "props": {
            "editConfig": {
              "trigger": "click",
              "mode": "cell",
              "showStatus": true
            },
            "columns": [
              {
                "type": "index",
                "width": 60
              },
              {
                "type": "selection",
                "width": 60
              },
              {
                "field": "employees",
                "title": "员工数"
              },
              {
                "field": "created_date",
                "title": "创建日期"
              },
              {
                "field": "city",
                "title": "城市"
              }
            ],
            "data": [
              {
                "id": "1",
                "name": "GFD科技有限公司",
                "city": "福州",
                "employees": 800,
                "created_date": "2014-04-30 00:56:00",
                "boole": false
              },
              {
                "id": "2",
                "name": "WWW科技有限公司",
                "city": "深圳",
                "employees": 300,
                "created_date": "2016-07-08 12:36:22",
                "boole": true
              }
            ],
            "className": "component-base-style"
          },
          "children": [],
          "id": "25526464"
        },
        {
          "componentName": "TinyPager",
          "props": {
            "layout": "total, sizes, prev, pager, next",
            "total": 100,
            "pageSize": 10,
            "currentPage": 1,
            "className": "component-base-style"
          },
          "children": [],
          "id": "955945af"
        },
        {
          "componentName": "Text",
          "props": {
            "style": "display: inline-block;",
            "className": "component-base-style text-qmwuv",
            "text": "表格带插槽的选中 hover 场景"
          },
          "children": [],
          "id": "2433a464"
        },
        {
          "componentName": "TinyGrid",
          "props": {
            "editConfig": {
              "trigger": "click",
              "mode": "cell",
              "showStatus": true
            },
            "columns": [
              {
                "type": "index",
                "width": 60
              },
              {
                "type": "selection",
                "width": 60
              },
              {
                "field": "employees",
                "title": "员工数"
              },
              {
                "field": "created_date",
                "title": "创建日期"
              },
              {
                "field": "city",
                "title": "城市",
                "slots": {
                  "default": {
                    "type": "JSSlot",
                    "value": [
                      {
                        "componentName": "div",
                        "id": "68331136",
                        "children": [
                          {
                            "componentName": "Text",
                            "props": {
                              "style": "display: inline-block;",
                              "text": {
                                "type": "JSExpression",
                                "value": "row.name"
                              },
                              "className": "component-base-style"
                            },
                            "children": [],
                            "id": "36232312"
                          }
                        ]
                      }
                    ],
                    "params": [
                      "row"
                    ]
                  }
                }
              }
            ],
            "data": [
              {
                "id": "1",
                "name": "GFD科技有限公司",
                "city": "福州",
                "employees": 800,
                "created_date": "2014-04-30 00:56:00",
                "boole": false
              },
              {
                "id": "2",
                "name": "WWW科技有限公司",
                "city": "深圳",
                "employees": 300,
                "created_date": "2016-07-08 12:36:22",
                "boole": true
              }
            ],
            "className": "component-base-style"
          },
          "children": [],
          "id": "54b61743"
        }
      ],
      "id": "26632543"
    },
    {
      "componentName": "div",
      "props": {
        "className": "component-base-style"
      },
      "children": [
        {
          "componentName": "Text",
          "props": {
            "className": "component-base-style text-nqswn",
            "text": "数据展示类"
          },
          "children": [],
          "id": "436c32c3"
        },
        {
          "componentName": "TinyPopover",
          "props": {
            "width": 200,
            "title": "弹框标题",
            "trigger": "manual",
            "modelValue": true,
            "className": "component-base-style"
          },
          "children": [
            {
              "componentName": "Template",
              "props": {
                "slot": "reference"
              },
              "children": [
                {
                  "componentName": "div",
                  "props": {
                    "placeholder": "触发源"
                  },
                  "id": "54c45142"
                }
              ],
              "id": "6334e3e3"
            },
            {
              "componentName": "Template",
              "props": {
                "slot": "default"
              },
              "children": [
                {
                  "componentName": "div",
                  "props": {
                    "placeholder": "提示内容"
                  },
                  "id": "21536213"
                }
              ],
              "id": "e6834224"
            }
          ],
          "id": "44233c63"
        },
        {
          "componentName": "TinyTooltip",
          "props": {
            "content": "Top Left 提示文字",
            "placement": "top-start",
            "manual": true,
            "modelValue": true,
            "className": "component-base-style"
          },
          "children": [
            {
              "componentName": "span",
              "children": [
                {
                  "componentName": "div",
                  "props": {},
                  "id": "3552c642"
                }
              ],
              "id": "437f2356"
            },
            {
              "componentName": "Template",
              "props": {
                "slot": "content"
              },
              "children": [
                {
                  "componentName": "span",
                  "children": [
                    {
                      "componentName": "div",
                      "props": {
                        "placeholder": "提示内容"
                      },
                      "id": "22232622"
                    }
                  ],
                  "id": "14435544"
                }
              ],
              "id": "26346535"
            }
          ],
          "id": "232511b7"
        },
        {
          "componentName": "TinyTree",
          "props": {
            "data": [
              {
                "label": "一级 1",
                "children": [
                  {
                    "label": "二级 1-1",
                    "children": [
                      {
                        "label": "三级 1-1-1"
                      }
                    ]
                  }
                ]
              },
              {
                "label": "一级 2",
                "children": [
                  {
                    "label": "二级 2-1",
                    "children": [
                      {
                        "label": "三级 2-1-1"
                      }
                    ]
                  },
                  {
                    "label": "二级 2-2",
                    "children": [
                      {
                        "label": "三级 2-2-1"
                      }
                    ]
                  }
                ]
              }
            ],
            "className": "component-base-style"
          },
          "children": [],
          "id": "62128352"
        },
        {
          "componentName": "TinyPopeditor",
          "props": {
            "modelValue": "",
            "placeholder": "请选择",
            "gridOp": {
              "columns": [
                {
                  "field": "id",
                  "title": "ID",
                  "width": 40
                },
                {
                  "field": "name",
                  "title": "名称",
                  "showOverflow": "tooltip"
                },
                {
                  "field": "province",
                  "title": "省份",
                  "width": 80
                },
                {
                  "field": "city",
                  "title": "城市",
                  "width": 80
                }
              ],
              "data": [
                {
                  "id": "1",
                  "name": "GFD科技有限公司GFD科技有限公司GFD科技有限公司GFD科技有限公司GFD科技有限公司GFD科技有限公司GFD科技有限公司",
                  "city": "福州",
                  "province": "福建"
                },
                {
                  "id": "2",
                  "name": "WWW科技有限公司",
                  "city": "深圳",
                  "province": "广东"
                },
                {
                  "id": "3",
                  "name": "RFV有限责任公司",
                  "city": "中山",
                  "province": "广东"
                },
                {
                  "id": "4",
                  "name": "TGB科技有限公司",
                  "city": "龙岩",
                  "province": "福建"
                },
                {
                  "id": "5",
                  "name": "YHN科技有限公司",
                  "city": "韶关",
                  "province": "广东"
                },
                {
                  "id": "6",
                  "name": "WSX科技有限公司",
                  "city": "黄冈",
                  "province": "武汉"
                }
              ]
            },
            "className": "component-base-style"
          },
          "children": [],
          "id": "a423544a"
        },
        {
          "componentName": "TinyDialogBox",
          "props": {
            "visible": false,
            "show-close": true,
            "title": "dialogBox title",
            "className": "component-base-style"
          },
          "children": [
            {
              "componentName": "div",
              "id": "632a6586"
            }
          ],
          "id": "1b222642"
        },
        {
          "componentName": "TinyCollapse",
          "props": {
            "modelValue": "collapse1",
            "className": "component-base-style"
          },
          "children": [
            {
              "componentName": "TinyCollapseItem",
              "props": {
                "name": "collapse1",
                "title": "折叠项1"
              },
              "children": [
                {
                  "componentName": "div",
                  "id": "35252c26"
                }
              ],
              "id": "3334c336"
            },
            {
              "componentName": "TinyCollapseItem",
              "props": {
                "name": "collapse2",
                "title": "折叠项2"
              },
              "children": [
                {
                  "componentName": "div",
                  "id": "3c323c55"
                }
              ],
              "id": "32d5424c"
            },
            {
              "componentName": "TinyCollapseItem",
              "props": {
                "name": "collapse3",
                "title": "折叠项3"
              },
              "children": [
                {
                  "componentName": "div",
                  "id": "55353855"
                }
              ],
              "id": "45226235"
            }
          ],
          "id": "343143e6"
        },
        {
          "componentName": "TinyCarousel",
          "props": {
            "height": "180px",
            "className": "component-base-style"
          },
          "children": [
            {
              "componentName": "TinyCarouselItem",
              "props": {
                "title": "carousel-item-a"
              },
              "children": [
                {
                  "componentName": "div",
                  "props": {
                    "style": "margin:10px 0 0 30px"
                  },
                  "id": "231243fc"
                }
              ],
              "id": "364d24b3"
            },
            {
              "componentName": "TinyCarouselItem",
              "props": {
                "title": "carousel-item-b"
              },
              "children": [
                {
                  "componentName": "div",
                  "props": {
                    "style": "margin:10px 0 0 30px"
                  },
                  "id": "65314333"
                }
              ],
              "id": "44332a3f"
            }
          ],
          "id": "6525553b"
        }
      ],
      "id": "42464556"
    },
    {
      "componentName": "div",
      "props": {
        "className": "component-base-style"
      },
      "children": [
        {
          "componentName": "Text",
          "props": {
            "style": "display: inline-block;",
            "className": "component-base-style text-flgjp",
            "text": "导航类型"
          },
          "children": [],
          "id": "541d5253"
        },
        {
          "componentName": "TinyBreadcrumb",
          "props": {
            "options": [
              {
                "to": "{ path: '/' }",
                "label": "首页"
              },
              {
                "to": "{ path: '/breadcrumb' }",
                "label": "产品"
              },
              {
                "replace": "true",
                "label": "软件"
              }
            ],
            "className": "component-base-style"
          },
          "children": [],
          "id": "723e21a2"
        },
        {
          "componentName": "TinyTabs",
          "props": {
            "modelValue": "first",
            "className": "component-base-style"
          },
          "children": [
            {
              "componentName": "TinyTabItem",
              "props": {
                "title": "标签页1",
                "name": "first"
              },
              "children": [
                {
                  "componentName": "div",
                  "props": {
                    "style": "margin:10px 0 0 30px"
                  },
                  "id": "35564654"
                }
              ],
              "id": "33726322"
            },
            {
              "componentName": "TinyTabItem",
              "props": {
                "title": "标签页2",
                "name": "second"
              },
              "children": [
                {
                  "componentName": "div",
                  "props": {
                    "style": "margin:10px 0 0 30px"
                  },
                  "id": "914d9243"
                }
              ],
              "id": "16e43254"
            }
          ],
          "id": "22539326"
        },
        {
          "componentName": "TinyTimeLine",
          "props": {
            "active": "2",
            "data": [
              {
                "name": "已下单"
              },
              {
                "name": "运输中"
              },
              {
                "name": "已签收"
              }
            ],
            "className": "component-base-style"
          },
          "children": [],
          "id": "42376355"
        }
      ],
      "id": "35275312"
    },
    {
      "componentName": "div",
      "props": {
        "className": "component-base-style"
      },
      "children": [
        {
          "componentName": "Text",
          "props": {
            "className": "component-base-style text-jvlui",
            "text": "Element Plus组件"
          },
          "children": [],
          "id": "15b6521c"
        },
        {
          "componentName": "ElForm",
          "children": [
            {
              "componentName": "ElFormItem",
              "props": {
                "label": "账号",
                "prop": "account"
              },
              "children": [
                {
                  "componentName": "ElInput",
                  "props": {
                    "modelValue": "",
                    "placeholder": "请输入账号"
                  },
                  "id": "56463115"
                }
              ],
              "id": "35331456"
            },
            {
              "componentName": "ElFormItem",
              "props": {
                "label": "密码",
                "prop": "password"
              },
              "children": [
                {
                  "componentName": "ElInput",
                  "props": {
                    "modelValue": "",
                    "placeholder": "请输入密码",
                    "type": "password"
                  },
                  "id": "645561f2"
                }
              ],
              "id": "64543742"
            },
            {
              "componentName": "ElFormItem",
              "props": {},
              "children": [
                {
                  "componentName": "ElButton",
                  "props": {
                    "type": "primary",
                    "style": "margin-right: 10px"
                  },
                  "children": [
                    {
                      "componentName": "Text",
                      "props": {
                        "text": "提交"
                      },
                      "id": "a4547362"
                    }
                  ],
                  "id": "24663532"
                },
                {
                  "componentName": "ElButton",
                  "props": {
                    "type": "primary"
                  },
                  "children": [
                    {
                      "componentName": "Text",
                      "props": {
                        "text": "重置"
                      },
                      "id": "3c235664"
                    }
                  ],
                  "id": "1223c536"
                }
              ],
              "id": "54352515"
            }
          ],
          "props": {
            "className": "component-base-style"
          },
          "id": "66227565"
        },
        {
          "componentName": "ElButton",
          "children": [
            {
              "componentName": "Text",
              "props": {
                "text": "按钮文本"
              },
              "id": "d3542248"
            }
          ],
          "props": {
            "className": "component-base-style"
          },
          "id": "3522322c"
        },
        {
          "componentName": "ElTable",
          "props": {
            "data": [
              {
                "date": "2016-05-03",
                "name": "Tom",
                "address": "No. 189, Grove St, Los Angeles"
              },
              {
                "date": "2016-05-02",
                "name": "Tom",
                "address": "No. 189, Grove St, Los Angeles"
              },
              {
                "date": "2016-05-04",
                "name": "Tom",
                "address": "No. 189, Grove St, Los Angeles"
              },
              {
                "date": "2016-05-01",
                "name": "Tom",
                "address": "No. 189, Grove St, Los Angeles"
              }
            ],
            "columns": [
              {
                "type": "index"
              },
              {
                "label": "Date",
                "prop": "date"
              },
              {
                "label": "Name",
                "prop": "name"
              },
              {
                "label": "Address",
                "prop": "address"
              }
            ],
            "className": "component-base-style"
          },
          "children": [
            {
              "componentName": "ElTableColumn",
              "props": {
                "type": "index"
              },
              "id": "d26d2655"
            },
            {
              "componentName": "ElTableColumn",
              "props": {
                "label": "Date",
                "prop": "date"
              },
              "id": "524431a5"
            },
            {
              "componentName": "ElTableColumn",
              "props": {
                "label": "Name",
                "prop": "name"
              },
              "id": "37355322"
            },
            {
              "componentName": "ElTableColumn",
              "props": {
                "label": "Address",
                "prop": "address"
              },
              "id": "36543353"
            }
          ],
          "id": "f7256395"
        },
        {
          "componentName": "ElInput",
          "props": {
            "className": "component-base-style"
          },
          "children": [],
          "id": "3a314542"
        }
      ],
      "id": "6445193f"
    }
  ],
  "dataSource": {
    "list": []
  },
  "state": {},
  "methods": {},
  "utils": [],
  "bridge": [],
  "inputs": [],
  "outputs": [],
  "fileName": "TestSelect",
  "id": "body"
}

Summary by CodeRabbit

  • New Features

    • Added a new "Date Picker" component to the component library, supporting a wide range of configuration options and events.
    • Introduced enhanced selection and hover interaction capabilities for canvas nodes, including new visual components for hover and insertion lines.
    • Enabled both single and multiple node selection with improved visual feedback and keyboard/clipboard support.
    • Added configuration options for interaction modes.
  • Refactor

    • Replaced previous multi-selection logic with a unified selection state management system.
    • Centralized hover and selection state management using new composable hooks.
    • Simplified and improved event handling and state updates across the canvas and related components.
    • Streamlined hover state handling by removing inactive hover states and focusing on active hover states.
    • Updated internal rectangle calculations to use nested properties for better clarity.
    • Introduced a Vue plugin to link DOM elements with Vue component instances for improved interaction.
  • Bug Fixes

    • Improved reliability and consistency of node selection, hover, and deletion behaviors.
  • Chores

    • Updated internal configuration and code structure to support new interaction modes and maintainability.

Sorry, something went wrong.

Copy link
Contributor

coderabbitai bot commented Apr 9, 2025

Walkthrough

This update introduces a comprehensive refactor of the canvas container's node selection and hover logic, centralizing state management through new composable hooks (useHoverNode and useSelectNode) with support for both Vue and HTML interaction modes. Multi-selection state management is removed and replaced with a unified selection state. Several new components are added for improved hover and insertion line visualization. The event handling and keyboard logic are updated to use the new composables. The bundle configuration is extended with a new date picker component, and the engine configuration is updated for Vue mode. Supporting utilities for Vue instance rectangle calculation and DOM-to-component mapping are introduced, and the overall API surface is streamlined for clarity and modularity.

Changes

File(s) Change Summary
designer-demo/engine.config.js Added dslMode and selectMode properties set to 'vue' in the exported config object. Minor formatting adjustment.
designer-demo/public/mock/bundle.json Added a new date picker component (ElDatePicker) with full configuration and schema. Included in the "Element Plus组件" snippet group.
packages/canvas/container/index.ts Replaced useMultiSelect with useSelectNode in the exported API.
packages/canvas/container/src/CanvasContainer.vue Refactored to use new composables for hover and selection state. Replaced multi-selection logic. Updated event handling. Added CanvasHover and CanvasInsertLine components.
packages/canvas/container/src/components/CanvasAction.vue Updated to use selectState.rect for rectangle data. Removed hover and line state props and related logic. Switched to useSelectNode for selection.
packages/canvas/container/src/components/CanvasDivider.vue
packages/canvas/container/src/components/CanvasResizeBorder.vue
Changed access to rectangle properties to use selectState.rect instead of root selectState.
packages/canvas/container/src/components/CanvasHover.vue New: Added CanvasHover component for hover rectangle visualization and selection.
packages/canvas/container/src/components/CanvasInsertLine.vue New: Added CanvasInsertLine component for visualizing insertion lines and slot selection.
packages/canvas/container/src/components/CanvasRouterJumper.vue
packages/canvas/container/src/components/CanvasViewerSwitcher.vue
Removed inactiveHoverState prop and logic. Simplified hover state handling.
packages/canvas/container/src/composables/useMultiSelect.ts Deleted: Removed multi-selection composable and related interface.
packages/canvas/container/src/container.ts Refactored to use new composables for hover and selection. Removed direct state management. Updated API to wrap new composable methods.
packages/canvas/container/src/interactions/common.ts New: Added shared interfaces/utilities for hover/selection state and DOM traversal.
packages/canvas/container/src/interactions/index.ts New: Added dynamic interaction hook selection based on engine config (vue or html).
packages/canvas/container/src/interactions/vue-interactions.ts New: Added Vue-based hover and selection logic using Vue instance traversal.
packages/canvas/container/src/interactions/html-interactions.ts New: Added HTML-based hover and selection logic using DOM attributes.
packages/canvas/container/src/interactions/vue-rect.ts New: Added utilities for calculating Vue instance bounding rectangles.
packages/canvas/container/src/keyboard.ts Refactored keyboard and clipboard logic to use new composables for selection and hover. Removed multi-selection logic.
packages/canvas/render/src/render.ts Removed onMouseover event handler assignment in design mode from getBindProps.
packages/canvas/render/src/runner.ts Added a Vue plugin to link DOM nodes to Vue component instances. Applied the plugin in app creation.
packages/plugins/tree/src/Main.vue Switched to new selection composable. Updated selected IDs logic. Removed call to selectNode after drop.
packages/toolbars/save/src/js/index.ts Replaced selectNode(null) with clearSelect?.() after saving.

Sequence Diagram(s)

Loading
sequenceDiagram
    participant User
    participant CanvasContainer
    participant useHoverNode
    participant useSelectNode
    participant CanvasHover
    participant CanvasInsertLine

    User->>CanvasContainer: Mouse event (mousedown/mouseover/contextmenu)
    CanvasContainer->>useSelectNode: updateSelectedNode (for selection)
    CanvasContainer->>useHoverNode: updateHoverNode (for hover)
    CanvasContainer->>CanvasHover: Render hover rectangle (with curHoverState)
    CanvasContainer->>CanvasInsertLine: Render insert line (with lineState)
    useSelectNode-->>CanvasContainer: Update selectState
    useHoverNode-->>CanvasContainer: Update curHoverState
    CanvasContainer-->>User: Visual feedback (selection, hover, insert line)
Loading
sequenceDiagram
    participant Keyboard
    participant useSelectNode
    participant useHoverNode
    participant CanvasContainer

    Keyboard->>useSelectNode: selectNodeById / clearSelect (arrow/delete)
    Keyboard->>useHoverNode: clearHover (on delete)
    useSelectNode-->>CanvasContainer: Update selectState
    useHoverNode-->>CanvasContainer: Update curHoverState
    CanvasContainer-->>Keyboard: Update UI

Poem

🐇✨
In the garden of canvas, where nodes gently gleam,
Selection and hover now flow as a team.
No more tangled states, just composables bright,
With Vue or with HTML, all works just right.
New rectangles and lines, a hover that glows—
This bunny hops forward, where clarity grows!
🌱🎨

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Sorry, something went wrong.

@github-actions github-actions bot added the refactoring Refactoring label Apr 9, 2025
@chilingling chilingling marked this pull request as ready for review April 10, 2025 09:57
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (27)
designer-demo/public/mock/bundle.json (1)

322-324: Typo in Context Menu Actions
Within the "configure" object, the "contextMenu" has an action "bindEevent". This appears to be a typographical error and likely should be "bindEvent".
Apply the following diff suggestion:

-              "actions": ["copy", "remove", "insert", "updateAttr", "bindEevent", "createBlock"],
+              "actions": ["copy", "remove", "insert", "updateAttr", "bindEvent", "createBlock"],
packages/canvas/render/src/runner.ts (1)

48-59: Well-structured component instance referencing system.

The GetComponentByDomNode plugin provides a way to access Vue component instances from DOM nodes, which is vital for the refactored selection mechanism. The mixin approach ensures all Vue components will have their instance accessible via the DOM element's __vueComponent property.

Consider adding JSDoc comments to document this plugin's purpose. Also, since __vueComponent is a non-standard property, you might want to add a comment explaining this convention:

+/**
+ * Plugin that allows retrieving the Vue component instance from a DOM node.
+ * This is used by the canvas interaction system to access component data when selecting elements.
+ */
const GetComponentByDomNode = {
  install: (Vue) => {
    Vue.mixin({
      mounted() {
+       // Store a reference to the component instance on its root DOM element
        this.$el.__vueComponent = this?._
      },
      updated() {
        this.$el.__vueComponent = this?._
      }
    })
  }
}
packages/canvas/container/src/components/CanvasHover.vue (2)

21-54: Clean implementation of hover node selection.

The script section properly uses the new useSelectNode hooks and implements a clear selection function that either updates the selected node with the provided element or selects by ID.

Consider adding prop validation to ensure the required properties of the hoverState object are present:

props: {
  hoverState: {
    type: Object,
-   default: () => ({})
+   default: () => ({}),
+   validator: (value) => {
+     return value.rect !== undefined && value.componentName !== undefined;
+   }
  }
},

56-101: Well-structured hover styling with dynamic binding.

The CSS effectively uses dynamic v-bind values to position the hover rectangle based on the state. The separate styling for active and inactive hovers provides good visual feedback to users.

Add appropriate keyboard accessibility to the clickable corner mark:

.corner-mark-left {
  // 为了支持点击选中
  pointer-events: auto;
+  cursor: pointer;
+  &:focus {
+    outline: 2px solid var(--te-canvas-container-border-color-checked);
+  }
}

Also, consider adding tabindex and role attributes in the template:

-<div class="corner-mark-left" @click="handleSelectHoverNode">
+<div class="corner-mark-left" @click="handleSelectHoverNode" @keydown.enter="handleSelectHoverNode" tabindex="0" role="button">
packages/canvas/container/src/interactions/index.ts (2)

12-37: Well-designed interaction hook selection system.

The implementation cleanly maps different interaction hook sets based on DSL mode, providing a flexible, extensible architecture for switching between Vue-specific and default interactions.

Add JSDoc comments to improve code documentation:

+/**
+ * Map of interaction hooks for different DSL modes
+ * Each mode provides hooks for hover and selection functionality
+ */
const interactionHooksMap = {
  vue: {
    useHoverNode: useVueHoverNode,
    useSelectNode: useVueSelectNode
  },
  default: {
    useHoverNode: useDefaultHoverNode,
    useSelectNode: useDefaultSelectNode
  }
}

+/**
+ * Returns the appropriate interaction hooks based on the configured DSL mode
+ * Defaults to standard hooks if no valid mode is found
+ * @returns Object containing useHoverNode and useSelectNode hooks
+ */
const getInteractionFn = () => {
  const dslMode = getMergeMeta('engine.config')?.dslMode?.toLowerCase?.() as keyof typeof interactionHooksMap

  if (interactionHooksMap[dslMode]) {
    return interactionHooksMap[dslMode]
  }

  return interactionHooksMap.default
}

38-54: Effective implementation of interaction hook providers.

The exported functions useHoverNode and useSelectNode efficiently manage the initialization of interaction functions and memoize the result to avoid redundant calculations.

Consider adding type safety for return values:

+type InteractionHooks = typeof interactionHooksMap[keyof typeof interactionHooksMap];
-const interactionsFn = ref<typeof interactionHooksMap[keyof typeof interactionHooksMap] | null>(null)
+const interactionsFn = ref<InteractionHooks | null>(null)

+/**
+ * Hook to access hover node functionality
+ * Initializes interaction functions if not already done
+ * @returns Object with hover node state and methods
+ */
export const useHoverNode = () => {
  if (!interactionsFn.value) {
    interactionsFn.value = getInteractionFn()
  }

  return interactionsFn.value.useHoverNode()
}

+/**
+ * Hook to access node selection functionality
+ * Initializes interaction functions if not already done
+ * @returns Object with selection state and methods
+ */
export const useSelectNode = () => {
  if (!interactionsFn.value) {
    interactionsFn.value = getInteractionFn()
  }

  return interactionsFn.value.useSelectNode()
}
packages/canvas/container/src/components/CanvasInsertLine.vue (2)

9-23: Consider removing empty setup function and optimizing props

The setup() function is empty and could be removed entirely as it doesn't add any value.

-  setup() {}

94-97: Use CSS variables for consistent styling

There are hardcoded color values for the hover state of slot selectors. Consider using CSS variables for better consistency with the theme system.

-        background: #40a9ff;
-        color: #fff;
+        background: var(--te-canvas-container-text-color-checked);
+        color: var(--te-common-text-dark-inverse);
packages/canvas/container/src/components/CanvasAction.vue (1)

382-399: Consider simplifying the document null check

After calling getDocument(), there's still a check if doc exists, which seems redundant since you just obtained it. This might be leftover from previous code structure.

  const { left, top, width, height } = selectState
  const doc = getDocument()
  const { width: canvasWidth, height: canvasHeight } = canvasSize
  // 标签宽度和工具操作条宽度之和加上间距
  const fullRectWidth = labelWidth + optionWidth + OPTION_SPACE

  // 是否 将label 标签放置到底部,判断 top 距离
  const isLabelAtBottom = top < LABEL_HEIGHT
  const labelAlign = new Align({
    alignLeft: true,
    horizontalValue: 0,
    alignTop: !isLabelAtBottom,
    verticalValue: -LABEL_HEIGHT
  })

-  if (!doc) {
-    return {}
-  }
packages/canvas/container/src/interactions/default-interactions.ts (2)

102-109: Replace event-based communication with proper event system

There's a TODO comment about changing to event notification, but the implementation is currently using a side-effect approach.

Consider implementing a proper event system instead of temporarily using the 'remove' event to trigger schema updates. This would make the code more maintainable and easier to understand.

-  // TODO: 改成事件通知
-  // 临时借用 remove 事触发 currentSchema 更新
-  canvasState?.emit?.('remove')
+  // Use a dedicated event for schema updates
+  canvasState?.emit?.('schemaUpdated')

223-258: Consider refactoring the timeout pattern in updateSelectedRect

The function uses setTimeout with a 0ms delay, which is typically used to defer execution to the next event loop cycle.

Consider using nextTick from Vue instead of setTimeout(fn, 0) for consistency with other async operations in the codebase.

const updateSelectedRect = () => {
-  setTimeout(() => {
+  nextTick(() => {
    if (!selectState.value.length) {
      return
    }

    selectState.value = selectState.value.map((stateItem) => {
      // ... existing code
    })
-  }, 0)
+  })
}
packages/canvas/container/src/keyboard.ts (1)

184-193: Commented code should be removed

Line 185 has a commented reference to a function that's not being used.

Remove the commented line to keep the code clean:

const selectNodeById = async (id: string, type: string) => {
-  // commonSelectNodeById(updateSelectedNode, id, type)
  const element = querySelectById(id)
  const { node, parent } = useCanvas().getNodeWithParentById(id) || {}
packages/canvas/container/src/interactions/vue-rect.ts (1)

79-80: ESLint disable comment could be avoided with code restructuring

There's an ESLint disable comment for the no-use-before-define rule.

Consider restructuring the code to define functions in order of dependency, avoiding the need for the ESLint disable comment:

-export const getFragmentRect = (instance: VueInstanceInternal): Rect => {
+export const getElementRectByInstance = (instance: VueInstanceInternal): Rect | undefined => {
+  if (instance?.type?.description === 'v-fgt') {
+    return getFragmentRect(instance)
+  }
+
+  if (instance.el?.nodeType === 1) {
+    return instance.el.getBoundingClientRect()
+  }
+
+  if (instance.component) {
+    return getElementRectByInstance(instance.component)
+  }
+
+  if (instance.subTree) {
+    return getElementRectByInstance(instance.subTree)
+  }
+
+  return undefined
+}
+
+export const getFragmentRect = (instance: VueInstanceInternal): Rect => {
   // ...function body...
 }
-
-export const getElementRectByInstance = (instance: VueInstanceInternal): Rect | undefined => {
-  // ...function body...
-}
packages/canvas/container/src/interactions/common.ts (2)

71-94: Clarify purpose of nodeType check

There's a question comment about the nodeType check that could be clarified.

Add a more descriptive comment explaining why the nodeType check is necessary:

-  // QUESTION: 为什么要判断 node Type?
+  // Only process Element nodes (nodeType === 1), skip text nodes (nodeType === 3) and other node types
   if (!element || element.nodeType !== 1) {
     return undefined
   }

107-125: Consider improving the MouseEvent creation approach

Both hoverNodeById and selectNodeById functions create fake MouseEvents by type casting, which works but is not ideal.

Consider creating a more explicit method for simulating interactions rather than using type casting to create fake events:

+// Create a simulated mouse event targeting the specified element
+const createSimulatedMouseEvent = (target: Element): MouseEvent => {
+  return {
+    target,
+    preventDefault: () => {},
+    stopPropagation: () => {}
+  } as unknown as MouseEvent
+}
+
 export const hoverNodeById = (id: string, updateHoverNode: (e: MouseEvent) => void) => {
   const element = querySelectById(id)

   if (element) {
-    updateHoverNode({ target: element } as unknown as MouseEvent)
+    updateHoverNode(createSimulatedMouseEvent(element))
   }
 }

 export const selectNodeById = async (
   updateSelectedNode: (e: MouseEvent, type: string) => void,
   id: string,
   type: string
 ) => {
   const element = querySelectById(id)

   if (element) {
-    updateSelectedNode({ target: element } as unknown as MouseEvent, type)
+    updateSelectedNode(createSimulatedMouseEvent(element), type)
   }
 }
packages/canvas/container/src/CanvasContainer.vue (5)

110-112: Destructuring new composables
Extracting selectState, updateSelectedNode, defaultSelectState, and similarly for hover is a well-structured approach. Double-check concurrency with each reactive call to ensure no flickering in complex scenarios.


202-203: Broadcasting 'canvas-mousedown' event
Emitting an event externally can be powerful for cross-component communication. Confirm no duplication or conflict with other mousedown handlers in the environment.


217-226: Managing scroll events with time-based checks
Using a scrollTimeout to debounce scroll logic is typically effective. Just ensure it doesn't cause performance issues on large canvases. Consider a requestAnimationFrame approach for smoother UI updates if needed.


309-311: Commented-out hover slot logic
The _slotName function is present but references commented-out hover code. If truly obsolete, consider removing it entirely or reintroducing it if planned for future multi-slot features.


344-346: Exposing reactive references
srcAttrName, selectState, and curHoverState are published from setup(). Verify that only the needed references are exposed to the template or parent, to avoid unintentional override or misuse.

packages/canvas/container/src/interactions/vue-interactions.ts (5)

24-37: Imports and references
The relevant references from 'vue', '@opentiny/tiny-engine-meta-register', '../../../common', etc. are properly consolidated. Keep an eye on ensuring unused imports don’t accumulate as the codebase evolves.


67-134: Fetching rect and node data
getRectAndNode does a thorough job retrieving the ID from nested Vue instances. The while loop ensures we find relevant attributes or a fallback. Verify performance on large component trees, and consider short-circuiting if no match is found within reasonable depth.


137-142: Hover logic and optional chaining
The condition checks res?.node?.id and whether it appears in selectState. The static analysis hint suggests strengthening usage of optional chaining. You might rewrite for clarity, e.g.:

-if (!res || (res?.node?.id && selectState.value.some((state) => state?.node?.id === res.node.id))) {
+if (!res || (res?.node?.id && selectState.value.some((state) => state?.node?.id === res?.node?.id))) {

Nonetheless, functionality appears correct. Confirm that it gracefully handles missing properties.

🧰 Tools
🪛 Biome (1.9.4)

[error] 137-138: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


224-226: Selecting node by ID
Bridging to a common function (commonSelectNodeById) is consistent. Confirm that raising an error or fallback occurs if the node is absent or the ID is invalid, preventing silent failures.


237-248: Delayed rect update
updateSelectedRect uses a setTimeout to refresh dimensions, presumably allowing layout to settle. This is often effective but can be fragile if layout changes are significant. Consider using nextTick for more Vue-centric reactivity.

packages/canvas/container/src/container.ts (2)

165-167: Clearing hover on dragStart
dragStart destructures clearHover from useHoverNode and immediately calls it. This helps give a clean drag experience but double-check abrupt hover clearing doesn’t hamper user feedback in certain edge cases.


658-669: Deprecated selectors
Functions selectNode and hoverNode now delegate to useSelectNode / useHoverNode. This helps backward compatibility. Consider logging a warning to encourage migration for anyone still calling these.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fc16170 and 736f995.

📒 Files selected for processing (23)
  • designer-demo/engine.config.js (1 hunks)
  • designer-demo/public/mock/bundle.json (2 hunks)
  • packages/canvas/container/index.ts (1 hunks)
  • packages/canvas/container/src/CanvasContainer.vue (9 hunks)
  • packages/canvas/container/src/components/CanvasAction.vue (6 hunks)
  • packages/canvas/container/src/components/CanvasDivider.vue (2 hunks)
  • packages/canvas/container/src/components/CanvasHover.vue (1 hunks)
  • packages/canvas/container/src/components/CanvasInsertLine.vue (1 hunks)
  • packages/canvas/container/src/components/CanvasResizeBorder.vue (2 hunks)
  • packages/canvas/container/src/components/CanvasRouterJumper.vue (1 hunks)
  • packages/canvas/container/src/components/CanvasViewerSwitcher.vue (1 hunks)
  • packages/canvas/container/src/composables/useMultiSelect.ts (0 hunks)
  • packages/canvas/container/src/container.ts (15 hunks)
  • packages/canvas/container/src/interactions/common.ts (1 hunks)
  • packages/canvas/container/src/interactions/default-interactions.ts (1 hunks)
  • packages/canvas/container/src/interactions/index.ts (1 hunks)
  • packages/canvas/container/src/interactions/vue-interactions.ts (1 hunks)
  • packages/canvas/container/src/interactions/vue-rect.ts (1 hunks)
  • packages/canvas/container/src/keyboard.ts (5 hunks)
  • packages/canvas/render/src/render.ts (0 hunks)
  • packages/canvas/render/src/runner.ts (2 hunks)
  • packages/plugins/tree/src/Main.vue (3 hunks)
  • packages/toolbars/save/src/js/index.ts (2 hunks)
💤 Files with no reviewable changes (2)
  • packages/canvas/render/src/render.ts
  • packages/canvas/container/src/composables/useMultiSelect.ts
🧰 Additional context used
🧠 Learnings (2)
packages/canvas/container/src/components/CanvasDivider.vue (1)
Learnt from: gene9831
PR: opentiny/tiny-engine#1233
File: packages/canvas/container/src/components/CanvasDivider.vue:184-185
Timestamp: 2025-04-10T06:10:03.602Z
Learning: In CanvasDivider.vue, even though state.verLeft and state.horizontalTop already include 'px' suffix, the CSS properties in state.dividerStyle still need to append 'px' again according to gene9831, suggesting that these state variables might be processed differently than expected when used in style binding.
packages/canvas/container/src/components/CanvasViewerSwitcher.vue (1)
Learnt from: gene9831
PR: opentiny/tiny-engine#1117
File: packages/canvas/container/src/components/CanvasViewerSwitcher.vue:96-117
Timestamp: 2025-04-10T06:10:03.602Z
Learning: In CanvasViewerSwitcher.vue, `state.usedHoverState.element` is guaranteed to have a value when `handleClick` is called, making additional error handling unnecessary.
🧬 Code Graph Analysis (8)
packages/toolbars/save/src/js/index.ts (1)
packages/canvas/container/src/container.ts (1)
  • canvasApi (824-870)
packages/canvas/container/src/interactions/vue-rect.ts (2)
packages/canvas/types.ts (1)
  • Node (1-6)
packages/canvas/container/src/interactions/common.ts (1)
  • VueInstanceInternal (21-43)
packages/canvas/container/src/keyboard.ts (5)
packages/canvas/container/src/interactions/default-interactions.ts (2)
  • useSelectNode (260-269)
  • useHoverNode (115-122)
packages/canvas/container/src/interactions/index.ts (2)
  • useSelectNode (48-54)
  • useHoverNode (40-46)
packages/canvas/container/src/interactions/vue-interactions.ts (2)
  • useSelectNode (250-259)
  • useHoverNode (228-235)
packages/canvas/container/src/interactions/common.ts (2)
  • selectNodeById (115-125)
  • clearHover (64-69)
packages/canvas/container/src/container.ts (1)
  • removeNodeById (309-319)
packages/canvas/container/src/interactions/default-interactions.ts (1)
packages/canvas/container/src/interactions/common.ts (7)
  • HoverOrSelectState (6-18)
  • initialHoverState (51-62)
  • clearHover (64-69)
  • getClosedElementHasUid (71-94)
  • getWindowRect (96-105)
  • hoverNodeById (107-113)
  • selectNodeById (115-125)
packages/canvas/container/src/interactions/index.ts (3)
packages/register/src/common.ts (1)
  • getMergeMeta (179-181)
packages/canvas/container/src/interactions/default-interactions.ts (2)
  • useHoverNode (115-122)
  • useSelectNode (260-269)
packages/canvas/container/src/interactions/vue-interactions.ts (2)
  • useHoverNode (228-235)
  • useSelectNode (250-259)
packages/canvas/container/src/interactions/vue-interactions.ts (1)
packages/canvas/container/src/interactions/common.ts (8)
  • HTMLElementWithVue (46-49)
  • VueInstanceInternal (21-43)
  • HoverOrSelectState (6-18)
  • initialHoverState (51-62)
  • clearHover (64-69)
  • getWindowRect (96-105)
  • hoverNodeById (107-113)
  • selectNodeById (115-125)
packages/canvas/container/src/interactions/common.ts (2)
packages/canvas/container/src/container.ts (2)
  • getWindow (72-72)
  • querySelectById (334-352)
packages/canvas/container/src/interactions/vue-interactions.ts (1)
  • updateHoverNode (136-144)
packages/canvas/container/src/container.ts (3)
packages/canvas/container/src/interactions/default-interactions.ts (2)
  • useHoverNode (115-122)
  • useSelectNode (260-269)
packages/canvas/container/src/interactions/index.ts (2)
  • useHoverNode (40-46)
  • useSelectNode (48-54)
packages/canvas/container/src/interactions/vue-interactions.ts (2)
  • useHoverNode (228-235)
  • useSelectNode (250-259)
🪛 Biome (1.9.4)
packages/canvas/container/src/interactions/vue-interactions.ts

[error] 137-138: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (55)
designer-demo/engine.config.js (1)

6-7: Addition of Vue DSL mode configuration looks good

The new dslMode: 'vue' property has been added to the configuration object, which aligns with the PR objectives to optimize Vue canvas interaction logic. This configuration will support the new selection and hover functionality implemented across the application.

Note that according to the PR objectives, this introduces a breaking change that may impact existing applications, particularly in how nodes are selected and interacted with in the Vue canvas environment.

designer-demo/public/mock/bundle.json (2)

282-330: New Component Addition: 日期选择器 Configuration
A new JSON object defining the "日期选择器" (DatePicker) component has been added with a detailed configuration including metadata (id, version, name, icon, description), npm package info, and an extensive "configure" and "schema" structure.
Suggestion: Verify that the "id": 1 value does not conflict with other component IDs within the mock data.


14391-14399: New Child Entry in Element Plus组件
A new child entry for "日期选择器" has been inserted into the "children" array under the "Element Plus组件" category. The child configuration is minimal, offering properties such as the component name, icon, and snippetName.
Suggestion: Confirm that the minimal "schema": {} provided here is sufficient for downstream usage or if additional configuration details are needed in the long term.

packages/canvas/container/src/components/CanvasDivider.vue (2)

153-154: Improved structure for selectState access

The code now correctly destructures position-related properties (width, height, left, top) from selectState.rect and assigns componentName separately from selectState.componentName. This change aligns with the restructured state management approach in the PR, making the component more maintainable.


180-180: Consistent rect property access pattern

The change to access position properties via props.selectState.rect is now consistent with the pattern used in other parts of the file and across the codebase, contributing to improved code consistency.

packages/canvas/container/index.ts (1)

2-2:

❓ Verification inconclusive

API refactoring from multi-selection to node selection

The module now exports useSelectNode instead of useMultiSelect, which is a significant change in the selection paradigm. This aligns with the PR objectives to centralize selection logic and distinguish between Vue canvas and general canvas behaviors.

Also applies to: 9-9


🏁 Script executed:

#!/bin/bash
# Check where useSelectNode is imported and used to understand the impact
rg --type ts --type vue "useSelectNode" -A 3 -B 3

Length of output: 77


Manual Verification Needed: Confirm useSelectNode Integration Across File Types

  • The import in packages/canvas/container/index.ts has been updated to use useSelectNode instead of useMultiSelect, matching the PR's objectives for centralizing node selection.
  • The original automated check using rg --type ts --type vue "useSelectNode" -A 3 -B 3 failed due to an unrecognized file type "vue". This may hide relevant occurrence checks in Vue files.
  • Action Items:
    • Re-run the search without the --type vue flag for TypeScript files:
      rg --type ts "useSelectNode" -A 3 -B 3
    • For Vue files, add the file type explicitly:
      rg --type-add 'vue:*.vue' "useSelectNode" -A 3 -B 3
    • Manually verify that all references to the old useMultiSelect have been updated and that the new node selection logic is consistently applied.
packages/toolbars/save/src/js/index.ts (1)

73-73: Updated canvas selection clearing mechanism

The code now properly destructures clearSelect from canvasApi.value and uses the optional chaining operator when calling it. This change aligns with the new selection API where clearSelect() is preferred over the previous selectNode(null) approach.

This implementation is more robust as it follows the new selection management pattern introduced in this PR.

Also applies to: 96-96

packages/canvas/container/src/components/CanvasResizeBorder.vue (2)

124-124: Structured access to position properties

Updated to access rectangle properties from props.selectState.rect instead of directly from props.selectState, which aligns with the new state structure. This change is part of the broader refactoring effort to centralize selection logic.


154-155: Improved state destructuring pattern

The code now follows a more structured approach by separately destructuring position properties from selectState.rect and assigning componentName directly from selectState. This change improves readability and maintainability by clearly distinguishing between position data and component metadata.

packages/canvas/render/src/runner.ts (1)

79-79: Plugin chain updated to include GetComponentByDomNode.

The application setup correctly includes the new plugin in the chain.

packages/canvas/container/src/components/CanvasHover.vue (1)

1-19: Good component structure for hover visualization.

The template provides distinct visual treatments for active vs. inactive hover states, with the component name displayed in the top-left corner. The active state allows selection via clicking the component name.

packages/canvas/container/src/components/CanvasRouterJumper.vue (1)

45-62:

❓ Verification inconclusive

Simplified hover state watching logic.

The watch function has been streamlined to only monitor the component name from the hover state, making the code more focused and easier to understand.

Ensure that removing the watch on inactiveHoverState doesn't affect any edge cases where inactive router links still need to be processed. Run this verification:


🏁 Script executed:

#!/bin/bash
# Check if there are any usages of inactiveHoverState with router components
rg "inactiveHoverState.*RouterLink" --type ts --type vue

Length of output: 86


Below is the updated review comment after additional verification:

Simplified hover state watching logic – Verify Edge Case Handling for Inactive Router Links

The logic for watching only the component name appears solid. However, please note that our automated search (re-run using file globs for both TypeScript and Vue files) found no references to inactiveHoverState used in combination with RouterLink. Nonetheless, please manually verify that removing the watch on inactiveHoverState doesn’t inadvertently bypass any edge cases where inactive router links still require processing.

  • Confirm that no other parts of the codebase rely on inactiveHoverState to trigger router link updates.
  • Double-check any conditional flows that might be affected by this change.
packages/canvas/container/src/components/CanvasViewerSwitcher.vue (1)

124-148: Improved hover state management with better null checking

The refactored hover state logic is more straightforward and robust. By removing the inactiveHoverState prop and implementing proper null checking with an early return when the element doesn't exist, the code is now more maintainable and less error-prone.

The validation logic has been improved while maintaining the guarantee that state.usedHoverState.element will have a value when handleClick is called, which aligns with established patterns.

packages/canvas/container/src/components/CanvasInsertLine.vue (1)

1-7: Good implementation of the insertion line component

This new component effectively extracts the insertion line rendering logic into a dedicated component, which aligns with the PR objective to optimize rendering by extracting hover and insertion logic into separate components.

The conditional rendering and class bindings are well implemented to handle various states and positions.

packages/canvas/container/src/components/CanvasAction.vue (2)

3-10: Improved structure using rect object for dimensions

The refactored code now consistently accesses positioning properties through the selectState.rect object instead of directly from selectState. This structural change improves code organization and maintainability.


116-118: Well-executed transition to hook-based selection API

The code properly adopts the new hook-based approach by:

  1. Importing useSelectNode instead of direct selectNode function
  2. Using getDocument() to access the document context
  3. Calling selectNodeById through the hook

This aligns with the PR's goal to centralize selection logic and distinguish between Vue canvas and general canvas behaviors.

Also applies to: 205-207

packages/plugins/tree/src/Main.vue (2)

74-75: Updated to use the new node selection mechanism

The code has been successfully refactored to use useSelectNode instead of useMultiSelect, which aligns with the PR's goal of centralizing selection logic. The computed property selectedIds has been properly updated to map over the new data structure.


202-208:

❓ Verification inconclusive

Note: Node selection after drop has been removed

The selectNode call after insertion has been removed from the handleDrop method. This aligns with the PR objective that mentions "introducing a delay in selecting nodes after insertion to ensure the canvas updates correctly." This behavior change prevents selection issues after dragging nodes.

Verify that there's another mechanism for selecting the node after insertion or that this is an intentional UX change by running:


🏁 Script executed:

#!/bin/bash
# Check if there's any code that selects nodes after insertion elsewhere
rg -A 3 "insertNode\(" | rg "selectNode" 

Length of output: 40


Action Required: Confirm Node Selection Behavior

The explicit selectNode call has been removed in the handleDrop method (packages/plugins/tree/src/Main.vue, lines 202-208) to introduce a delay in node selection, as outlined by the PR objective. However, our initial search did not reveal an alternate mechanism invoking selectNode after node insertion.

  • Removed Code: The selectNode call immediately after insertNode has been removed.
  • Verification Needed: Please verify that node selection is now being handled as intended (e.g., via a delayed mechanism elsewhere) or that this behavior is an intentional UX change to avoid selection issues on drop.
packages/canvas/container/src/interactions/default-interactions.ts (4)

1-20: Well-documented purpose and limitations

The file header provides a clear explanation of the default node hover and selection logic for HTML canvases, including its steps and limitations. This is excellent documentation that helps developers understand the purpose and constraints of the implementation.


32-37: Good initialization pattern

The state initialization properly creates a new object with spread operator to avoid reference issues. This ensures that the hover state is initialized with all necessary properties and a clean rectangle object.


91-100: Conditional hover state update

The updateHoverNode logic correctly checks if a node is already selected before updating the hover state, which prevents hovering over selected elements. This implements one of the PR objectives - fixing the bug where "hovering over components was ineffective when no components were initially selected."


124-180: Complex selection logic handles both single and multiple selection

The updateSelectedNode function correctly implements the PR objective of handling element selection, including:

  1. Support for multi-selection with ctrl/meta key
  2. Special handling for inactive nodes
  3. Proper scrolling to the selected node
  4. Emitting appropriate events based on selection state

This effectively addresses the issue where "Element Plus DatePicker and disabled components could not be selected".

packages/canvas/container/src/keyboard.ts (4)

14-17: Good refactoring to use the new interaction hooks

The import changes correctly reflect the move to using the new selection and hover hooks, which aligns with the PR objective of centralizing selection and hover logic.


28-51: Consistent refactoring of keyboard handlers

All keyboard navigation handlers (handlerLeft, handlerRight, handlerUp, handlerDown) have been consistently updated to use the new selectNodeById function from useSelectNode. This provides a unified approach to selection across the application.


54-68: Improved delete handler with hover state cleanup

The refactored handlerDelete function now properly clears both selection and hover states, and checks if the deleted node is currently being hovered over. This prevents UI artifacts when deleting nodes.


116-130: Effective clipboard cut handler

The clipboard cut handler has been properly updated to use the new selection state API, maintaining the same functionality while integrating with the refactored selection system.

packages/canvas/container/src/interactions/vue-rect.ts (4)

1-6: Well-documented source of inspiration

The file header properly acknowledges the inspiration from Vue.js DevTools, including the repo link and specific file location. This is good practice for attributing external influences.


31-46: Clean implementation of rectangle creation

The createRect function uses computed properties (getters) for width and height, which is a clean approach that ensures these values are always calculated from the current dimensions.


68-97: Comprehensive fragment rectangle calculation

The getFragmentRect function correctly handles different Vue component structures and edge cases, calculating a combined rectangle that encompasses all child elements. This supports the PR objective to "enhance element positioning logic for Vue canvases."


99-117: Thorough recursive implementation for element rect calculation

The getElementRectByInstance function implements a comprehensive recursive approach to handle different Vue instance types, which is essential for accurately determining element positions in a Vue component tree. This supports the PR objective of "allowing selection of disabled components and those that cannot mount the data-uid attribute."

packages/canvas/container/src/interactions/common.ts (2)

6-18: Well-structured interface for hover and selection states

The HoverOrSelectState interface clearly defines the structure needed for tracking hover and selection states, including rectangle dimensions, node reference, and other necessary properties.


21-49: Comprehensive Vue instance internal structure definition

The VueInstanceInternal and HTMLElementWithVue interfaces provide detailed typing for Vue component structures, which is essential for the rectangle calculation functions and traversing the component tree.

packages/canvas/container/src/CanvasContainer.vue (12)

2-2: Refactor to use single-state iteration
Replacing multiSelectedStates with selectState looks consistent with the new logic. Ensure state.id is guaranteed to be unique across potential multiple selections in future expansions.


12-15: New hover and line insertion components
Introducing <canvas-hover>, <canvas-insert-line>, <canvas-router-jumper>, and <canvas-viewer-switcher> to handle different aspects of interaction is a solid move. Confirm that each component performs lightweight work to avoid potential performance overhead with many children.


64-65: Additional imports for hover and insert line
Glad to see these new components are being cleanly imported. Ensure they're only registered here if they're not reused in other modules.


79-79: Switch to composable-based interactions
Using useHoverNode and useSelectNode consolidates hover/selection logic into composables. This should improve maintainability.


89-91: Registering CanvasViewerSwitcher, CanvasHover, and CanvasInsertLine
These additions keep the component registry consistent with newly introduced functionalities. Good job ensuring all references are declared in the components field.


114-118: Fallback to default selection state
These checks correctly handle the single node scenario versus a fallback. This keeps UI elements consistent, preventing potential null references if no node is selected.


121-140: Async node interaction handler
The new handleNodeInteractions function correctly updates the selected node and differentiates left/right clicks. However, consider handling the middle-click event (event.button === 1) or ignoring it explicitly if not supported. Also confirm that errors during async selection (e.g., if updateSelectedNode fails) are gracefully handled.


163-167: Clearing hover on drag start
Storing drag offset data followed by an immediate hover clear is logical—this helps avoid stale hover states during a fresh drag operation. Keep an eye out for potential user confusion if a slight delay is needed before forcibly clearing hover.


196-199: Resetting insert states on mousedown
Nullifying insertPosition and insertContainer prevents lingering insertion panels. Ensure no race conditions with handleNodeInteractions if the user quickly toggles multiple insertion actions.


204-213: Contextmenu event listener
This approach selectively handles right-click logic in the iframe. Skipping the menu when the target is the document element (line 206) allows precise control over the canvas background. Carefully confirm that it aligns with user expectations for a blank-area contextmenu.


251-258: Mouseover handling and line-state reset
Clearing the drag line when hovering simplifies the logic. Verify no conflicts occur if a user quickly re-engages a drag mid-hover.


293-293: Dragenter event
Clearing line state on dragenter helps maintain consistent UI. Confirm that external drags (e.g., from outside the canvas) also behave correctly, especially if partial data is dropped.

packages/canvas/container/src/interactions/vue-interactions.ts (7)

1-11: License header
License and attribution blocks are properly included. No issues here.


13-22: Documentation for Vue canvas hover logic
Clearly outlining the rationale behind referencing __vueComponent is helpful. The explanation of how it resolves issues with components that can’t carry a data-uid attribute ensures future readers understand the approach.


38-56: Recursive search for nearest Vue instance
getClosedVueElement effectively climbs the DOM to find the closest __vueComponent. Watch out for potential cyclical references in unusual conditions. Otherwise, this approach is straightforward.


58-61: Global hover state
Initializing curHoverState as a global ref is convenient. Confirm that splitting this among multiple canvases (if needed) is handled properly, so states don’t interfere.


146-154: Clear selection side effects
clearSelect() resets both local state and canvasState.current/parent via 'remove' event. Ensure downstream listeners are aware that 'remove' also signals a selection clear.


160-222: Updating selected node
updateSelectedNode properly supports multi-selection with Ctrl/Meta keys. Good approach to skip a “non-active” node if isInactiveNode is set. Validate that multi-selection edge cases (e.g., selecting the same node repeatedly) do not cause inconsistent states.


250-259: Composables for selection
useSelectNode centralizes selection logic, making it reusable. Returning defaultSelectState from initialHoverState is a clever reuse. Keep an eye on potential confusion if “hover” usage diverges from “select” usage in the future.

packages/canvas/container/src/container.ts (4)

14-14: Refactored import
Using consolidated imports from '../../common' for NODE_UID and NODE_TAG is consistent with other changes.


20-20: Hover and select composables
Switching to useHoverNode and useSelectNode from older multi-select patterns. This aligns the container’s logic with the new approach.


316-316: Selection clearing on node removal
After removing a node, calling clearSelect ensures no stale references remain. Perfect for avoiding residual highlights or errors.


832-847: canvasApi abstraction
Exposing new wrappers like clearSelect, selectNodeById, and hoverNodeById is consistent with the official hooks. Perfectly keeps a single entry point for external systems.

@hexqi hexqi added this to the v2.5.0 milestone Apr 16, 2025
@hexqi hexqi requested review from gene9831 and SonyLeo April 22, 2025 03:48

Verified

This commit was signed with the committer’s verified signature.
…tioning and interaction methods

重构: 优化 Vue 画布交互逻辑,提取元素定位和交互方法

Verified

This commit was signed with the committer’s verified signature.

Verified

This commit was signed with the committer’s verified signature.

Verified

This commit was signed with the committer’s verified signature.
@chilingling chilingling force-pushed the refactor/getRectByVNode branch from bc010f1 to 59b9695 Compare April 23, 2025 06:38
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🔭 Outside diff range comments (2)
packages/canvas/container/src/CanvasContainer.vue (2)

121-141: ⚠️ Potential issue

Drag logic only works for the first selected node – multi‑select is ignored

handleNodeInteractions always dereferences selectState.value[0], even when the user Ctrl/⌘‑clicks to build a multi‑selection (isMultipleSelect === true).
This will:

  • Use the wrong rectangle when the primary click occurred on a node that is not the first in the array.
  • Prevent intuitive drag actions for multi‑selection (TODO left in place).

Consider:

- const node = selectState.value[0]?.node
+ const primaryState = selectState.value.find(({ element }) => element === event.target) 
+ const node = primaryState?.node

and bail out early if no suitable state is found.
Alternatively, disable drag until a dedicated multi‑drag implementation lands to avoid broken UX.


314-320: ⚠️ Potential issue

Several iframe‑scoped listeners are never removed – potential memory leak

onUnmounted only detaches the hot‑key and message listeners. The handlers added in canvasReady
(mousedown, contextmenu, scroll, dragover, drop, mouseover, etc.) stay bound to the
iframe’s contentWindow, surviving hot reloads and keeping large reactive graphs alive.

Expose an offCanvasReady() helper mirroring canvasReady() and invoke it inside onUnmounted
after verifying iframe.value.
Failing to do so will gradually degrade performance during long editing sessions.

♻️ Duplicate comments (1)
packages/canvas/container/src/interactions/vue-interactions.ts (1)

58-61: structuredClone again needs a polyfill

Same portability concern as raised for common.ts. Factor out the fallback util or import from a
shared helper module to avoid duplication.

🧹 Nitpick comments (11)
packages/canvas/container/src/components/CanvasHover.vue (3)

6-6: Avoid creating a new function on every render

Writing an inline arrow function inside the template means Vue has to create a brand‑new callback on every render, which slightly hurts performance and breaks template caching. You can simplify the markup and avoid the extra allocation:

-    <div class="corner-mark-left" @click="(e) => handleSelectHoverNode(e)">
+    <div class="corner-mark-left" @click="handleSelectHoverNode">

31-48: Call the composable once, not inside the event handler

useSelectNode() is a composable; calling it inside the click handler violates the “call composables at the top of setup” guideline and re‑evaluates dependency injection each time. Cache its return value once and reuse it:

   setup(props) {
-    const handleSelectHoverNode = (e) => {
-      const node = props.hoverState.node
-      const element = props.hoverState.element
-
-      if (!node) {
-        return
-      }
-
-      const { selectNodeById, updateSelectedNode } = useSelectNode()
+    const { selectNodeById, updateSelectedNode } = useSelectNode()
+
+    const handleSelectHoverNode = (e) => {
+      const node = props.hoverState.node
+      const element = props.hoverState.element
+
+      if (!node) {
+        return
+      }

This keeps composable usage compliant and removes redundant work at runtime.


9-17: Hard‑coded Chinese label breaks i18n

拖放元素到容器内 is baked into the template, preventing localisation. Consider pulling the text from an i18n resource or providing a prop so consumers can translate it.

packages/canvas/container/src/keyboard.ts (1)

28-37: Cache selection helpers to avoid repeated composable look‑ups

useSelectNode() is invoked inside every arrow‑key handler. Although the composable is idempotent, the repeated calls are unnecessary. Capture its return value once at module scope:

const { selectNodeById } = useSelectNode()

function handlerLeft({ parent }) {
  selectNodeById(parent?.id, '', false)
}

This trim downs function overhead for every key press.

packages/canvas/container/src/CanvasContainer.vue (2)

251-259: High‑frequency mouseover handler can flood the render pipeline

Replacing the old mousemove with mouseover still fires on every DOM edge crossing and quickly saturates the event loop on dense component trees.
A small throttle (≈ 16 ms) keeps the UI responsive without sacrificing precision:

- win.addEventListener('mouseover', (ev) => {
-   ...
- })
+ const handleHover = throttle((ev) => {
+   updateHoverNode(ev)
+   lineState.position = ''
+   lineState.width = 0
+ }, 16)
+ win.addEventListener('mouseover', handleHover)

(You already depend on lodash-es elsewhere – throttle is available.)


112-118: multiStateLength is a computed ref but the prop still reads “multi”

With the new single‑source selectState, multiStateLength is simply
selectState.value.length. The name suggests multi‑selection only; if that is the intention keep
it, otherwise consider renaming to selectedCount for clarity.

packages/canvas/container/src/interactions/vue-interactions.ts (2)

118-126: Optional chaining can shorten the guard clause

Static analysis already hinted at this; applying it makes the intent clearer:

-if (!res || (res?.node?.id && selectState.value.some((state) => state?.node?.id === res.node.id))) {
+if (!res || res.node?.id && selectState.value.some((s) => s.node?.id === res.node.id)) {

No behavioural change, just readability.


217-237: Relying on naked setTimeout for rect refresh is fragile

setTimeout(..., 0) runs after every micro‑task; rapid successive DOM
mutations will queue many redundant jobs.
Consider using nextTick (already imported in container.ts) or a debounced job via
requestAnimationFrame.

-const updateSelectedRect = (): void => {
-  setTimeout(() => {
+const updateSelectedRect = (): void => {
+  queueMicrotask(() => {
     ...
-  }, 0)
+  })
 }
packages/canvas/container/src/container.ts (3)

480-483: Avoid instantiating hooks in the hot drag‑move loop

updateLineState is executed on every dragMove. Each time it calls useHoverNode() which, while memoised internally, still incurs function overhead and obscures intent.

Extract the hook result once at module scope (or cache it in the outer closure) and reuse it:

-const updateLineState = (element?: Element, data?: Node | null) => {
-  if (!element) {
-    const { clearHover } = useHoverNode()
+const { clearHover } = useHoverNode()
+
+const updateLineState = (element?: Element, data?: Node | null) => {
+  if (!element) {

A small optimisation that keeps the hot‑path lean.


551-556: Guard against undefined IDs when syncing hover state

hoverNodeById is called unconditionally with curHoverState.value?.node?.id.
When no node is currently hovered, id is undefined, resulting in a redundant DOM query ([data-uid="undefined"]). Add a simple guard:

-  const { hoverNodeById, curHoverState } = useHoverNode()
-  hoverNodeById(curHoverState.value?.node?.id)
+  const { hoverNodeById, curHoverState } = useHoverNode()
+  const hoverId = curHoverState.value?.node?.id
+  if (hoverId) {
+    hoverNodeById(hoverId)
+  }

This prevents unnecessary work and yields clearer intent.


831-846: Memoise hook wrappers inside canvasApi

canvasApi.clearSelect, selectNodeById, and hoverNodeById create a new wrapper on every call, internally re‑entering useSelectNode / useHoverNode. Although the hooks themselves singleton‑cache their impl, wrapping once at definition time is simpler and marginally faster:

-  clearSelect: () => {
-    const { clearSelect } = useSelectNode()
-    return clearSelect()
-  },
+  clearSelect: useSelectNode().clearSelect,

-  selectNodeById: (id: string, type: string, isMultipleSelect = false) => {
-    const { selectNodeById } = useSelectNode()
-    return selectNodeById(id, type, isMultipleSelect)
-  },
+  selectNodeById: useSelectNode().selectNodeById,

-  hoverNodeById: (id: string) => {
-    const { hoverNodeById } = useHoverNode()
-    return hoverNodeById(id)
-  },
+  hoverNodeById: useHoverNode().hoverNodeById,

Makes the API thinner and avoids per‑call indirection.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 736f995 and 59b9695.

📒 Files selected for processing (23)
  • designer-demo/engine.config.js (1 hunks)
  • designer-demo/public/mock/bundle.json (2 hunks)
  • packages/canvas/container/index.ts (1 hunks)
  • packages/canvas/container/src/CanvasContainer.vue (9 hunks)
  • packages/canvas/container/src/components/CanvasAction.vue (6 hunks)
  • packages/canvas/container/src/components/CanvasDivider.vue (2 hunks)
  • packages/canvas/container/src/components/CanvasHover.vue (1 hunks)
  • packages/canvas/container/src/components/CanvasInsertLine.vue (1 hunks)
  • packages/canvas/container/src/components/CanvasResizeBorder.vue (2 hunks)
  • packages/canvas/container/src/components/CanvasRouterJumper.vue (1 hunks)
  • packages/canvas/container/src/components/CanvasViewerSwitcher.vue (1 hunks)
  • packages/canvas/container/src/composables/useMultiSelect.ts (0 hunks)
  • packages/canvas/container/src/container.ts (15 hunks)
  • packages/canvas/container/src/interactions/common.ts (1 hunks)
  • packages/canvas/container/src/interactions/html-interactions.ts (1 hunks)
  • packages/canvas/container/src/interactions/index.ts (1 hunks)
  • packages/canvas/container/src/interactions/vue-interactions.ts (1 hunks)
  • packages/canvas/container/src/interactions/vue-rect.ts (1 hunks)
  • packages/canvas/container/src/keyboard.ts (5 hunks)
  • packages/canvas/render/src/render.ts (0 hunks)
  • packages/canvas/render/src/runner.ts (2 hunks)
  • packages/plugins/tree/src/Main.vue (3 hunks)
  • packages/toolbars/save/src/js/index.ts (2 hunks)
💤 Files with no reviewable changes (2)
  • packages/canvas/render/src/render.ts
  • packages/canvas/container/src/composables/useMultiSelect.ts
✅ Files skipped from review due to trivial changes (2)
  • packages/canvas/container/src/interactions/index.ts
  • packages/canvas/container/src/interactions/vue-rect.ts
🚧 Files skipped from review as they are similar to previous changes (12)
  • packages/canvas/container/src/components/CanvasResizeBorder.vue
  • designer-demo/engine.config.js
  • packages/toolbars/save/src/js/index.ts
  • packages/canvas/render/src/runner.ts
  • packages/canvas/container/src/components/CanvasDivider.vue
  • packages/canvas/container/src/components/CanvasViewerSwitcher.vue
  • designer-demo/public/mock/bundle.json
  • packages/canvas/container/src/components/CanvasRouterJumper.vue
  • packages/plugins/tree/src/Main.vue
  • packages/canvas/container/index.ts
  • packages/canvas/container/src/components/CanvasInsertLine.vue
  • packages/canvas/container/src/components/CanvasAction.vue
🧰 Additional context used
🧬 Code Graph Analysis (4)
packages/canvas/container/src/interactions/html-interactions.ts (5)
packages/canvas/container/src/interactions/common.ts (7)
  • HoverOrSelectState (6-18)
  • initialHoverState (51-62)
  • clearHover (64-66)
  • getClosedElementHasUid (68-91)
  • getWindowRect (93-102)
  • hoverNodeById (104-110)
  • selectNodeById (112-123)
packages/register/src/hooks.ts (1)
  • useCanvas (77-77)
packages/canvas/container/src/container.ts (5)
  • getConfigure (321-332)
  • canvasState (53-64)
  • getDocument (70-70)
  • scrollToNode (359-384)
  • querySelectById (334-352)
packages/canvas/container/src/interactions/vue-interactions.ts (3)
  • updateHoverNode (118-126)
  • useHoverNode (208-215)
  • useSelectNode (239-248)
packages/canvas/container/src/interactions/index.ts (2)
  • useHoverNode (42-48)
  • useSelectNode (50-56)
packages/canvas/container/src/interactions/common.ts (2)
packages/canvas/container/src/container.ts (2)
  • getWindow (72-72)
  • querySelectById (334-352)
packages/canvas/container/src/interactions/vue-interactions.ts (1)
  • updateHoverNode (118-126)
packages/canvas/container/src/container.ts (7)
packages/canvas/container/src/interactions/index.ts (2)
  • useHoverNode (42-48)
  • useSelectNode (50-56)
packages/canvas/container/src/interactions/html-interactions.ts (2)
  • useHoverNode (107-114)
  • useSelectNode (212-221)
packages/canvas/render/src/render.ts (1)
  • getController (321-321)
packages/register/src/hooks.ts (1)
  • useCanvas (77-77)
packages/canvas/container/src/interactions/common.ts (3)
  • clearHover (64-66)
  • hoverNodeById (104-110)
  • selectNodeById (112-123)
packages/canvas/render/src/material-function/configure.ts (1)
  • configure (1-1)
packages/canvas/types.ts (1)
  • Node (1-6)
packages/canvas/container/src/interactions/vue-interactions.ts (7)
packages/canvas/container/src/interactions/common.ts (8)
  • HTMLElementWithVue (46-49)
  • VueInstanceInternal (21-43)
  • HoverOrSelectState (6-18)
  • initialHoverState (51-62)
  • clearHover (64-66)
  • getWindowRect (93-102)
  • hoverNodeById (104-110)
  • selectNodeById (112-123)
packages/canvas/container/src/interactions/vue-rect.ts (1)
  • getElementRectByInstance (99-117)
packages/register/src/hooks.ts (1)
  • useCanvas (77-77)
packages/canvas/render/src/material-function/configure.ts (1)
  • configure (1-1)
packages/canvas/container/src/container.ts (5)
  • getConfigure (321-332)
  • canvasState (53-64)
  • getDocument (70-70)
  • scrollToNode (359-384)
  • querySelectById (334-352)
packages/canvas/container/src/interactions/index.ts (2)
  • useHoverNode (42-48)
  • useSelectNode (50-56)
packages/canvas/container/src/interactions/html-interactions.ts (2)
  • useHoverNode (107-114)
  • useSelectNode (212-221)
🪛 Biome (1.9.4)
packages/canvas/container/src/interactions/vue-interactions.ts

[error] 116-119: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: push-check
🔇 Additional comments (4)
packages/canvas/container/src/interactions/html-interactions.ts (1)

35-41: structuredClone might not be available in all build targets

structuredClone is still missing in Node ≤ 16 and some browsers behind enterprise evergreen.
If your tool‑chain transpiles only TS, you may need a ponyfill (e.g., import 'core-js/actual/structured-clone') or fall back to JSON.parse(JSON.stringify(obj)) for maximum compatibility.

packages/canvas/container/src/container.ts (3)

321-333: Guard against undefined component names & missing materials in getConfigure

getConfigure can be invoked with an undefined targetName (see the new call site in querySelectById).
When that happens, controller.getMaterial(undefined) will run, and the subsequent optional‑chaining returns {} – this is fine, but the intent is unclear and it silently hides a potential misuse. Adding an explicit early‑return improves readability and prevents accidental calls with falsy names.

 export const getConfigure = (targetName: string) => {
-  const material = getController().getMaterial(targetName)
+  if (!targetName) {
+    // Gracefully degrade instead of passing an invalid name down the line
+    return {}
+  }
+
+  const material = getController().getMaterial(targetName)

[ suggest_essential_refactor ]


335-350: rootSelector fallback may select the wrong element when multiple instances exist

The TODO comment highlights the risk: doc.querySelector(rootSelector) picks the first match, which is unreliable once the same component is rendered several times.
Consider:

  1. querySelectorAll(rootSelector) to fetch all matches.
  2. Narrow the result set (e.g., by traversing from the hovered target toward ancestors, or by comparing against runtime‐kept IDs).
  3. Fall back only when the original lookup failed and only one unambiguous match exists.

Failing to disambiguate may cause hover/selection to jump to a sibling instance.
If you need help devising a deterministic strategy, let me know—happy to sketch one.
[ suggest_optional_refactor ]


386-392:

✅ Verification successful

updateRect signature changed – ensure all call sites are updated

updateRect no longer accepts an id parameter. Please double‑check for stale invocations elsewhere in the codebase that still pass an argument; TypeScript will error during build, but a quick grep avoids surprises.


🏁 Script executed:

#!/bin/bash
# Find external call sites that still pass parameters to updateRect
rg -n "updateRect\\(" | grep -v "container.ts"

Length of output: 1047


🏁 Script executed:

#!/bin/bash
# Find call sites of updateRect with any arguments (non-empty parentheses)
rg -n 'updateRect\(\s*\S' | grep -v "container.ts"

Length of output: 1052


No stale updateRect arguments detected—change is safe

I ran a repo‑wide search for any updateRect(...) calls passing parameters and found none. All usages invoke updateRect() with empty parentheses, so the updated signature is already reflected everywhere.

Verified

This commit was signed with the committer’s verified signature.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
packages/canvas/container/src/interactions/vue-interactions.ts (1)

64-118: ⚠️ Potential issue

Unbounded while loop may hang on malformed component trees.

getRectAndNode climbs the Vue-instance chain until it finds an instance with NODE_UID/NODE_TAG === 'RouterView'. If the chain is cyclic or extremely deep (e.g. large recursive components) this could freeze the UI.

Add a depth guard to prevent potential infinite loops:

 let closedVueEle: VueInstanceInternal | undefined = instance
+let depth = 0
+const MAX_DEPTH = 100 // Reasonable limit to prevent infinite loops

-while (closedVueEle && !(closedVueEle.attrs?.[NODE_UID] || closedVueEle.attrs?.[NODE_TAG] === 'RouterView')) {
+while (closedVueEle && 
+       !(closedVueEle.attrs?.[NODE_UID] || closedVueEle.attrs?.[NODE_TAG] === 'RouterView') && 
+       depth < MAX_DEPTH) {
   closedVueEle = closedVueEle.parent
+  depth++
 }

+if (depth >= MAX_DEPTH) {
+  console.warn('Maximum depth reached when searching for component with NODE_UID or RouterView tag')
+  res.rect = { ...windowRect }
+  return res
+}
🧹 Nitpick comments (3)
packages/canvas/container/src/CanvasContainer.vue (2)

126-126: TODO comment needs to be addressed.

This TODO indicates there's still work needed to support multi-selection dragging logic, which aligns with the PR objectives to improve Vue canvas interaction.

Would you like help implementing the multi-selection drag functionality?


309-312: Redundant method needs cleanup.

The selectSlot method has been stripped of its functionality (commented out code) but is still exported and called. Since it no longer does anything meaningful, consider removing it or documenting why it's kept as a no-op.

-// TODO: 需要确认下该事件还是否需要
-const selectSlot = (_slotName) => {
-  // hoverState.slot = slotName
-}
+// If this method is truly needed as a placeholder for future implementation:
+const selectSlot = (slotName) => {
+  // Method intentionally left empty until slot selection requirements are confirmed
+  console.debug('selectSlot called with:', slotName);
+}
+
+// Or if it's not needed at all, consider removing it and its references
packages/canvas/container/src/interactions/vue-interactions.ts (1)

120-128: Use optional chaining for safer null handling.

The condition could be simplified and made safer by using optional chaining.

-if (!res || (res?.node?.id && selectState.value.some((state) => state?.node?.id === res.node.id))) {
+if (!res || (res?.node?.id && selectState.value.some((state) => state?.node?.id === res?.node?.id))) {
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 59b9695 and a0ce8f7.

📒 Files selected for processing (4)
  • packages/canvas/container/src/CanvasContainer.vue (9 hunks)
  • packages/canvas/container/src/interactions/common.ts (1 hunks)
  • packages/canvas/container/src/interactions/html-interactions.ts (1 hunks)
  • packages/canvas/container/src/interactions/vue-interactions.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/canvas/container/src/interactions/html-interactions.ts
  • packages/canvas/container/src/interactions/common.ts
🧰 Additional context used
🪛 Biome (1.9.4)
packages/canvas/container/src/interactions/vue-interactions.ts

[error] 118-121: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: push-check
🔇 Additional comments (17)
packages/canvas/container/src/CanvasContainer.vue (10)

2-2: Good fix for the key issue in v-for!

This properly implements the recommendation from the previous review by using state.node?.id || state.componentName instead of state.id, ensuring unique and stable keys for Vue's rendering optimization.


12-15: Well-structured component separation for improved maintainability.

Moving hover, insertion line, router jumper, and viewer switcher functionality into separate components follows good component design principles and separation of concerns.


79-79: Good architectural improvement with composable hooks.

Using specialized composable hooks (useHoverNode and useSelectNode) centralizes state management and interaction logic, making the code more maintainable and testable.


110-112: Improved state management with composable hooks.

The refactoring replaces direct state management with composable hooks, providing better encapsulation and reusability of selection and hover logic.


121-141: Good consolidation of node interaction logic.

handleNodeInteractions centralizes previously scattered interaction logic into a single, well-organized method. This improves code readability and maintainability.


197-201: Improved event handling with the consolidated interaction method.

The mousedown event handler now properly clears insertion states before delegating to the centralized handleNodeInteractions method, making the flow clearer.


205-214: Right-click selection now works correctly for disabled components.

This implementation now allows right-click selection of nodes even when components are disabled, addressing one of the PR objectives.


218-227: Improved scroll handling with debounce.

Adding a timeout to manage the scrolling state prevents potential race conditions and improves user experience during scroll operations.


251-259: Mouse event optimization from mousemove to mouseover.

Switching from mousemove to mouseover is more efficient as it reduces the number of event triggers while still providing the needed hover functionality.


344-345: Proper state exposure in the component.

The new state variables are correctly exposed in the return object, making them available to the template and child components.

packages/canvas/container/src/interactions/vue-interactions.ts (7)

59-62: Good state initialization using Vue's reactivity system.

The reactive state setup properly leverages Vue's ref API for managing hover and selection states, making them reactive throughout the component tree.


39-57: Well-implemented DOM traversal to find Vue components.

The getClosedVueElement function efficiently traverses the DOM tree to find Vue component instances, enabling proper interaction with Vue components in the canvas.


144-204: Well-implemented selection logic with multi-select support.

The updateSelectedNode function correctly handles both single and multiple selection modes, properly manages inactive nodes, and emits appropriate events. This addresses the PR objective of refactoring selection logic.


219-239: Smart rectangle update with setTimeout to handle DOM changes.

Using setTimeout to update rectangle coordinates after DOM changes is a good practice as it ensures the DOM has been fully updated before measuring. The code also handles cases where elements might have been removed.


210-217: Clean composable API for hover state management.

The useHoverNode composable provides a well-designed API with clear methods for managing hover state, following Vue's composition API best practices.


241-250: Clean composable API for selection state management.

The useSelectNode composable provides a well-designed API with clear methods for managing selection state, following Vue's composition API best practices.


152-167: Good handling of inactive nodes in selection logic.

The code correctly differentiates between regular nodes and inactive nodes (those not in the current schema), providing appropriate fallback behavior in each case.

@hexqi hexqi removed this from the v2.5.0 milestone Apr 25, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
refactoring Refactoring
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants