Skip to content

refactor: optimize Vue canvas interaction logic#1326

Merged
hexqi merged 5 commits into
opentiny:release/2.5.0-alpha.6from
chilingling:refactor/getRectByVNode
Apr 24, 2025
Merged

refactor: optimize Vue canvas interaction logic#1326
hexqi merged 5 commits into
opentiny:release/2.5.0-alpha.6from
chilingling:refactor/getRectByVNode

Conversation

@chilingling
Copy link
Copy Markdown
Member

@chilingling chilingling commented Apr 24, 2025

重构: 优化 Vue 画布交互逻辑,提取元素定位和交互方法English | 简体中文

PR

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?

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

…tioning and interaction methods

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

Summary by CodeRabbit

  • New Features

    • Added a new Date Picker component from Element Plus, now available for use and as a snippet.
    • Introduced new configuration options for interaction modes.
  • Improvements

    • Enhanced node selection and hover behavior for a more intuitive canvas editing experience.
    • Added visual overlays for node hover and insertion lines.
    • Improved event handling and selection logic, supporting both single and multi-selection modes.
  • Refactor

    • Replaced previous multi-selection and hover state management with a more robust, composable-driven system.
    • Updated internal logic to improve consistency and maintainability across canvas and toolbar interactions.
  • Bug Fixes

    • Streamlined selection clearing after saving and node deletion.
    • Improved reliability of selection rectangle positioning and event handling.
  • Chores

    • Updated documentation and internal configuration for better maintainability.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 24, 2025

Walkthrough

This update introduces a comprehensive refactor of the canvas container's selection and hover logic, transitioning from a custom multi-selection state management system to a modular, composable-driven approach using new useSelectNode and useHoverNode hooks. Several new modules and Vue components are added to encapsulate hover, selection, and insertion line UI, while the underlying event handling and state updates are centralized and made mode-aware (supporting both Vue and HTML modes). The changes propagate throughout the canvas, keyboard, toolbar, and tree view logic, ensuring consistent selection and hover behavior. Additionally, a new Element Plus Date Picker component is added to the mock bundle for demonstration.

Changes

File(s) Change Summary
designer-demo/engine.config.js Added dslMode and selectMode properties (both set to 'vue') to the exported config object.
designer-demo/public/mock/bundle.json Added a new Element Plus Date Picker (ElDatePicker) component definition, configuration, events, slots, and snippet entry; no removals or modifications to existing components.
packages/canvas/container/index.ts Replaced useMultiSelect import/export with useSelectNode from the new interactions module.
packages/canvas/container/src/CanvasContainer.vue Refactored to use composable-driven selection (useSelectNode) and hover (useHoverNode) states; updated event handlers; replaced multi-select logic; registered new UI components (CanvasHover, CanvasInsertLine).
packages/canvas/container/src/components/CanvasAction.vue Simplified to use selectState.rect for rectangle data; removed hover/inactive hover/line overlays and related props; switched to selectNodeById.
packages/canvas/container/src/components/CanvasDivider.vue
packages/canvas/container/src/components/CanvasResizeBorder.vue
Updated to use nested rect property from selectState for layout calculations; no logic changes.
packages/canvas/container/src/components/CanvasHover.vue New component: Renders hover overlays for active/inactive nodes; supports selection via label click; uses dynamic styling.
packages/canvas/container/src/components/CanvasInsertLine.vue New component: Renders insertion line overlays and interactive slot selection UI based on lineState and hoverState.
packages/canvas/container/src/components/CanvasRouterJumper.vue
packages/canvas/container/src/components/CanvasViewerSwitcher.vue
Removed inactiveHoverState prop and related logic; now rely solely on hoverState for determining overlay visibility and position.
packages/canvas/container/src/composables/useMultiSelect.ts Deleted: Removed the legacy multi-selection composable and its interface.
packages/canvas/container/src/container.ts Refactored to remove direct multi-selection and hover state; now delegates to useSelectNode/useHoverNode for all selection/hover logic; updated API exports; improved utility functions.
packages/canvas/container/src/interactions/common.ts New module: Provides interfaces and shared utilities for hover/selection state, DOM traversal, and simulated events for both Vue and HTML modes.
packages/canvas/container/src/interactions/html-interactions.ts New module: Implements default hover/selection logic for HTML-based canvas nodes using data attributes and DOM events.
packages/canvas/container/src/interactions/index.ts New module: Dynamically selects and exports the correct hover/selection composables based on engine config (selectMode).
packages/canvas/container/src/interactions/vue-interactions.ts New module: Implements Vue-specific hover/selection logic, including Vue instance traversal and bounding box calculation; supports multi-selection and event emission.
packages/canvas/container/src/interactions/vue-rect.ts New module: Calculates bounding rectangles for Vue component instances, including fragments and text nodes.
packages/canvas/container/src/keyboard.ts Refactored keyboard handlers to use useSelectNode/useHoverNode instead of the legacy multi-select composable; updated clipboard and delete logic.
packages/canvas/render/src/render.ts Removed assignment of onMouseover event handler in design mode; no other changes.
packages/canvas/render/src/runner.ts Added a Vue plugin to attach Vue instance references to DOM nodes; updated app creation to use this plugin, enabling DOM-to-component mapping.
packages/plugins/tree/src/Main.vue Switched from useMultiSelect to useSelectNode for selected node IDs; removed explicit re-selection after drop.
packages/toolbars/save/src/js/index.ts Replaced selectNode(null) with clearSelect?.() to clear selection after save.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CanvasContainer
    participant useHoverNode
    participant useSelectNode
    participant CanvasState
    participant CanvasHover/InsertLine

    User->>CanvasContainer: Mouse event (mousedown/mouseover/contextmenu)
    CanvasContainer->>useHoverNode: updateHoverNode(event)
    useHoverNode->>CanvasContainer: Updates curHoverState
    CanvasContainer->>useSelectNode: updateSelectedNode(event, type, isMultiple)
    useSelectNode->>CanvasState: Update selection state
    useSelectNode->>CanvasContainer: Updates selectState
    CanvasContainer->>CanvasHover/InsertLine: Passes updated hover/selection state
    CanvasHover/InsertLine->>User: Renders overlays and UI
Loading

Poem

In the garden of code where components grow,
The rabbits have tidied selection’s flow.
No more tangled multi-select weeds,
Composables now fulfill all our needs.
Hover and pick with composure and ease,
Vue or HTML—choose what you please!
🐇✨ Selection refined, the canvas at peace.

✨ 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.

@github-actions github-actions Bot added the refactoring Refactoring label Apr 24, 2025
Copy link
Copy Markdown
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/components/CanvasAction.vue (1)

329-338: ⚠️ Potential issue

Vertical alignment uses horizontalValue instead of verticalValue.

Inside the Align.align() method, the branch handling vertical positions (top / bottom) mistakenly assigns the offset to horizontalValue.
Consequently, the computed style string never updates verticalValue, so the option / label bars can appear mis-aligned when they have to be shifted vertically.

-        if (positions.isVertical(position)) {
-          this.alignTop = position === positions.TOP
-          this.horizontalValue = value
-          return this
-        }
+        if (positions.isVertical(position)) {
+          this.alignTop = position === positions.TOP
+          this.verticalValue = value
+          return this
+        }
packages/canvas/container/src/CanvasContainer.vue (1)

121-141: 💡 Verification agent

🧩 Analysis chain

Dragging logic ignores multi-selection & can mis-start drags

handleNodeInteractions always reads selectState.value[0] after an (async) updateSelectedNode.
If the user Ctrl/⌘-clicks several nodes, only the first item is ever considered when:

  • deciding whether to start dragStart
  • calculating the offset for that drag

This makes multi-select look supported but behave unpredictably, and the TODO a few lines below confirms it.

Consider short-circuiting the drag when isMultipleSelect is true or extending dragStart so it receives an array of selected nodes.


🏁 Script executed:

#!/bin/bash
# Search for updateSelectedNode implementation to see multi-select handling
echo "---- updateSelectedNode occurrences ----"
rg "updateSelectedNode" -n .

# Search for dragStart implementation to see parameter expectations
echo "---- dragStart occurrences ----"
rg "dragStart" -n .

# Search for selectState declaration to confirm its shape and usage
echo "---- selectState occurrences ----"
rg "selectState" -n .

Length of output: 11224


Update drag logic to properly handle multi-selection in CanvasContainer.vue

The current handleNodeInteractions always uses selectState.value[0] after updateSelectedNode, so even when isMultipleSelect is true you only ever drag the first node. You’ll need to either:

  • Skip initiating a drag when multiple nodes are selected
    e.g. at the top of the handler:
    if (isMultipleSelect) return
  • Or extend dragStart to accept an array of selected nodes
    and invoke it for each entry in selectState.value.

Locations to address:

  • packages/canvas/container/src/CanvasContainer.vue
    Lines ~126–134 where you read from selectState.value[0] for node, element, and rect.
  • The call to dragStart(node, element, { offsetX, offsetY }) needs to be updated per the approach above.
♻️ Duplicate comments (1)
packages/canvas/container/src/interactions/common.ts (1)

115-125: Same issue as above for selectNodeById

The fake MouseEvent should be replaced with a typed parameter or a real event as explained in the previous comment.

🧹 Nitpick comments (12)
packages/canvas/container/src/components/CanvasAction.vue (2)

382-457: Parameter naming makes intent unclear & risks leaks of extra props.

getStyleValues receives a selectState object but only de-structures left, top, width, height.
Call-sites pass a hand-crafted object ({ left, top, … }), not the whole reactive props.selectState.rect.
Consider renaming the first argument to { left, top, width, height } (or destructure directly in the signature) to remove any ambiguity.


459-495: watchPostEffect may run more often than necessary.

watchPostEffect depends on selectState.rect and showQuickAction.value.
When the user drags-to-resize the selection rectangle, the expensive await nextTick() & DOM-measurement logic is executed on every mouse-move frame.

If jank is observed, throttling the handler or switching to an explicit watch with a deep‐false shallow-reference to rect changes would help.

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

40-56: Minor: avoid wrapping plain objects in a Vue ref when no reactivity is required.

interactionsFn never appears in a template or another reactive computation; a simple module-scoped variable would suffice and spare the tiny overhead of Vue’s proxy.

-const interactionsFn = ref<IInteractionHooksMap[keyof IInteractionHooksMap] | null>(null)
+let interactionsFn: IInteractionHooksMap[keyof IInteractionHooksMap] | null = null
packages/canvas/container/src/interactions/common.ts (2)

71-75: Remove leftover “QUESTION” comment before merging

The inline note

// QUESTION: 为什么要判断 node Type?

looks like an internal discussion rather than documentation and will confuse future readers.
Please delete it or turn it into a proper JSDoc explanation.


107-113: Avoid forging MouseEvent objects – pass the Element instead

hoverNodeById fabricates a { target: element } as unknown as MouseEvent.
The object misses many properties real MouseEvents provide and may break consumers
relying on them (e.g. modifiers, clientX, …).

Two safer alternatives:

  1. Change the callback signature to accept an Element:

    hoverNodeById(id: string, cb: (el: Element) => void)
  2. Or, if you really need an event, create one properly:

    const evt = new MouseEvent('mousemove', { bubbles: true })
    Object.defineProperty(evt, 'target', { value: element })
    cb(evt)

Option 1 keeps the API simple and avoids unnecessary event creation.

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

219-238: Prefer requestAnimationFrame to throttled setTimeout for rect refresh

updateSelectedRect uses setTimeout(…, 0) which:

• Runs after all micro-tasks, not necessarily just after the next paint.
• Can queue multiple redundant executions during fast updates.

Replacing it with requestAnimationFrame ties the recalculation to the browser’s
render cycle and avoids needless work:

-const updateSelectedRect = (): void => {
-  setTimeout(() => {
+const updateSelectedRect = (): void => {
+  cancelAnimationFrame((updateSelectedRect as any)._rafId) // clear previous
+  ;(updateSelectedRect as any)._rafId = requestAnimationFrame(() => {-  }, 0)
+  })
 }

This keeps the UI responsive, especially while dragging or resizing.

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

251-258: Hot spot: mouseover fires for every nested element – throttle or switch to mousemove

Replacing the old mousemove listener with mouseover means the callback now executes once per element the pointer traverses (often dozens of times per pixel).
Even with a lightweight updateHoverNode, this will:

  • allocate more JS objects
  • trigger more DOM reads
  • consume additional CPU on large documents

A quick fix is to debounce / throttle the handler with requestAnimationFrame; longer-term you might revert to pointermove or mousemove which are naturally rate-limited.


310-312: Dead-code TODO – remove or implement

selectSlot is still wired to <canvas-action @select-slot> but its body is commented out.
Callers silently no-op, making future debugging harder. Please either:

  1. implement the required behaviour, or
  2. remove the prop / event altogether.

343-346: Expose only what the template uses

selectState and curHoverState are provided from setup() but never referenced in the <template> except as read-only props to children.
If you want to enforce encapsulation you can expose only the computed versions actually consumed (multiStateLength, computedSelectState, etc.) and keep the raw refs private.

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

321-332: Guard against undefined component names

getConfigure(undefined) is possible when querySelectById fails to locate a node.
controller.getMaterial(undefined) may throw or log noisy errors in some registries.

-export const getConfigure = (targetName: string) => {
-  const material = getController().getMaterial(targetName)
+export const getConfigure = (targetName?: string) => {
+  if (!targetName) {
+    return {}
+  }
+  const material = getController().getMaterial(targetName)

386-392: Scroll handler wipes hover but not insert-line overlay

updateRect clears hover state on scroll but leaves lineState intact.
If the user scrolls while a drag is in progress, the insertion guideline can freeze onscreen.

Either clear lineState here or inside clearHover() to avoid stale UI.


551-556: Avoid querying on undefined id

hoverNodeById(curHoverState.value?.node?.id) is invoked even when no node is hovered, resulting in a selector [data-uid="undefined"].

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

Reduces needless DOM queries.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 030ece7 and a0ce8f7.

📒 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
🧰 Additional context used
🧠 Learnings (2)
packages/canvas/container/src/components/CanvasDivider.vue (2)
Learnt from: gene9831
PR: opentiny/tiny-engine#1233
File: packages/canvas/container/src/components/CanvasDivider.vue:184-185
Timestamp: 2025-03-20T07:20:12.221Z
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.
Learnt from: gene9831
PR: opentiny/tiny-engine#1226
File: packages/canvas/container/src/components/CanvasDivider.vue:184-185
Timestamp: 2025-03-19T03:13:51.520Z
Learning: The CSS bug in packages/canvas/container/src/components/CanvasDivider.vue where verLeft already includes "px" but is being appended again in the style object will be fixed in a future update, as confirmed by gene9831.
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-02-17T12:11:22.718Z
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 (6)
packages/toolbars/save/src/js/index.ts (1)
packages/canvas/container/src/container.ts (1)
  • canvasApi (823-869)
packages/canvas/container/src/interactions/index.ts (3)
packages/register/src/common.ts (1)
  • getMergeMeta (179-181)
packages/canvas/container/src/interactions/html-interactions.ts (2)
  • useHoverNode (109-116)
  • useSelectNode (214-223)
packages/canvas/container/src/interactions/vue-interactions.ts (2)
  • useHoverNode (210-217)
  • useSelectNode (241-250)
packages/canvas/container/src/keyboard.ts (5)
packages/canvas/container/src/interactions/index.ts (2)
  • useSelectNode (50-56)
  • useHoverNode (42-48)
packages/canvas/container/src/interactions/html-interactions.ts (2)
  • useSelectNode (214-223)
  • useHoverNode (109-116)
packages/canvas/container/src/interactions/common.ts (2)
  • selectNodeById (115-126)
  • clearHover (67-69)
packages/canvas/container/src/container.ts (1)
  • removeNodeById (309-319)
packages/register/src/hooks.ts (1)
  • useCanvas (77-77)
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 (120-128)
packages/canvas/container/src/interactions/html-interactions.ts (5)
packages/canvas/container/src/interactions/common.ts (7)
  • HoverOrSelectState (9-21)
  • initialHoverState (54-65)
  • clearHover (67-69)
  • getClosedElementHasUid (71-94)
  • getWindowRect (96-105)
  • hoverNodeById (107-113)
  • selectNodeById (115-126)
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 (120-128)
  • useHoverNode (210-217)
  • useSelectNode (241-250)
packages/canvas/container/src/interactions/index.ts (2)
  • useHoverNode (42-48)
  • useSelectNode (50-56)
packages/canvas/container/src/container.ts (8)
packages/canvas/container/src/interactions/index.ts (2)
  • useHoverNode (42-48)
  • useSelectNode (50-56)
packages/canvas/container/src/interactions/html-interactions.ts (2)
  • useHoverNode (109-116)
  • useSelectNode (214-223)
packages/canvas/container/src/interactions/vue-interactions.ts (2)
  • useHoverNode (210-217)
  • useSelectNode (241-250)
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 (67-69)
  • hoverNodeById (107-113)
  • selectNodeById (115-126)
packages/canvas/render/src/material-function/configure.ts (1)
  • configure (1-1)
packages/canvas/types.ts (1)
  • Node (1-6)
🪛 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 (37)
designer-demo/engine.config.js (1)

6-8: Configuration updated to use Vue-specific interaction hooks.

The added configuration properties dslMode and selectMode set to 'vue' enable the use of Vue-specific interaction hooks for node selection and hover logic in the canvas container. This aligns with the broader refactoring to implement a modular, composable-driven approach for canvas interactions.

packages/toolbars/save/src/js/index.ts (2)

73-73: Updated to use new selection API.

Correctly destructures clearSelect from canvasApi.value instead of selectNode, aligning with the new composable-based selection management approach.


96-96: Improved selection clearing with optional chaining.

Replaces selectNode(null) with clearSelect?.(), which is more semantic and safely handles potential undefined state.

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

153-154: Updated to use new selection state structure.

Correctly destructures layout properties from selectState.rect instead of directly from selectState, aligning with the updated selection state structure.


180-180: Consistent destructuring from nested rect property.

Maintains consistency by destructuring layout properties from props.selectState.rect in the handleMouseEnter method.

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

2-2: Updated import to use the new interactions module.

Replaced the import of useMultiSelect from the composables directory with useSelectNode from the new interactions module, which provides a more flexible approach to selection management.


9-9: Updated exported API to use the new selection composable.

The exported api object now provides useSelectNode instead of useMultiSelect, representing a breaking change for consumers of this API.

Consider documenting this API change to help consumers migrate from useMultiSelect to useSelectNode. The new API is part of a broader refactoring to make selection logic more modular and framework-agnostic.

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

41-41: Simplified Vue imports

The imports have been consolidated onto a single line, which improves readability while maintaining the same functionality.


74-75: Selection state management refactor

The code now uses the new useSelectNode composable instead of the previous useMultiSelect approach, aligning with the PR's goal of optimizing canvas interaction logic. This change centralizes selection state management.


202-208: Simplified drop handler

The handleDrop method has been refactored to remove the explicit node re-selection after drop operations. This aligns with the centralized selection state management through useSelectNode and reduces redundant operations.

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

124-124: Updated property access pattern

Property access has been updated to use the nested rect object in the selection state. This change maintains consistency with the new selection state structure introduced in this refactoring.


154-155: Consistent property access pattern

Property access has been consistently updated to access geometric properties through selectState.rect while accessing componentName directly from selectState. This maintains a clear separation of concerns and aligns with the new data structure pattern.

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

48-59: Added component-to-DOM reference mechanism

A new Vue plugin has been introduced that stores component references on DOM nodes, enabling direct access to Vue component instances from DOM elements. This is a key enabler for the Vue-specific interaction logic being implemented.

The plugin correctly handles both the mounted and updated lifecycle hooks to ensure the reference is maintained throughout component updates.


79-79: Applied component reference plugin

The Vue application creation chain now includes the new GetComponentByDomNode plugin, ensuring all components in the application have DOM-to-component references.

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

46-52: Simplified hover state monitoring

The watch function has been simplified to only monitor the component name from the hover state, removing duplicate logic related to inactive hover state. This aligns with the new centralized hover state management.


53-60: Updated property access and safer attribute retrieval

The code now accesses geometric properties through hoverState.rect and uses optional chaining for attribute retrieval, improving robustness by preventing potential null reference errors.

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

124-152: Improved hover state validation with centralized logic.

The refactoring simplifies hover state handling by removing the dual hover state approach (inactiveHoverState) and implementing more explicit validation logic. The new implementation clearly specifies the conditions for a valid hover state:

  1. Component must be in the whitelist
  2. Element must be within an active page container
  3. Element must not be the active page itself

This aligns with the broader refactoring effort to centralize hover state management using composables.


147-148: Improved usage of React properties.

The destructuring of the rect object from state.usedHoverState is more efficient and readable than accessing individual properties separately.

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

14-17: Updated imports to use new interaction composables.

Replacing useMultiSelect with useHoverNode and useSelectNode aligns with the refactoring goal of creating a more modular, composable-driven approach to canvas interactions.


28-31: Updated selection API usage.

The function now uses the new selectNodeById method from the useSelectNode composable, which provides a more consistent interface for selection operations.


32-37: Consistent selection API usage.

Similar to the other handlers, this function has been updated to use the selectNodeById method from the useSelectNode composable, ensuring consistency across all selection operations.


39-44: Consistent selection API usage.

Updated to use the new selection API, maintaining the same functionality while aligning with the refactored interaction system.


46-51: Consistent selection API usage.

Updated to use the new selection API, maintaining the same functionality while aligning with the refactored interaction system.


54-68: Improved delete handler with better hover state management.

The refactored delete handler now:

  1. Iterates over all selected nodes in the collection
  2. Removes each selected node by ID
  3. Checks if the removed node was also the hovered node and clears hover state if necessary
  4. Clears selection after processing all nodes

This approach is more robust and handles edge cases better than before.


116-130: Improved clipboard cut operation.

The cut operation now properly:

  1. Retrieves the current selection state from the composable
  2. Filters selected nodes to ensure they have valid IDs
  3. Deep clones the selected nodes to prevent reference issues
  4. Removes each node by ID
  5. Clears selection after the operation

This implementation is more explicit and robust.


132-155: Updated clipboard paste operation.

The paste operation has been updated to work with the new selection state structure:

  1. Retrieves the current selection state from the composable
  2. Gets the last selected item from the array
  3. Uses the canvas API to get the node and its parent
  4. Proceeds with insertion as before

This maintains the same functionality while aligning with the new state management approach.


157-173: Updated copy event handler.

The copy operation now properly:

  1. Retrieves the current selection state from the composable
  2. Filters selected nodes to ensure they have valid IDs
  3. Deep clones the selected nodes to prevent reference issues
  4. Validates the nodes before copying them to the clipboard

This maintains the same functionality while aligning with the new state management approach.

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

1-7: Well-structured template for insertion line visualization.

The template provides a clear visual indicator for insertion positions with appropriate conditional rendering:

  1. Only shows when lineState has valid dimensions
  2. Uses dynamic classes to reflect the insertion position and forbidden status
  3. Conditionally renders the slot chooser when appropriate

This component helps visually guide users during drag and drop operations.


9-23: Clean component definition with appropriate props.

The component defines two props:

  1. lineState: For positioning and state information
  2. hoverState: For hover-related information

The empty setup function is fine since this is a purely presentational component with no internal state or methods.


25-101: Comprehensive styling with good separation of visual states.

The styles provide:

  1. Clear visual differentiation for different insertion positions (top, left, bottom, right, in)
  2. Different styling for forbidden states
  3. Interactive slot chooser with proper hover effects
  4. Dynamic positioning using v-bind for reactivity

The use of CSS variables for colors supports theming and customization.

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

1-19: Well-designed hover feedback with distinct states.

The template provides two types of hover visualizations:

  1. Active hover with interactive label and container hint
  2. Inactive hover with non-interactive label

This dual approach gives users clear visual feedback about which elements they can interact with.


21-55: Clean implementation of selection handling.

The component properly:

  1. Imports the required selection API
  2. Defines the hoverState prop
  3. Implements a handler for selecting nodes when clicking the label
  4. Supports both element-based and ID-based selection
  5. Handles multi-selection with modifier keys (Ctrl/Cmd)

This implementation aligns well with the refactored interaction system.


57-103: Comprehensive styling with good visual hierarchy.

The styles provide:

  1. Clear differentiation between active and inactive hover states
  2. Proper z-index and positioning
  3. Transition effects for smooth visual feedback
  4. Appropriate text styling and positioning for labels
  5. Dynamic positioning using v-bind for reactivity

The styling is well-organized and uses CSS variables for better maintainability.

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

30-37: Graceful fallback handles unknown modes, but selectMode lookup is re-evaluated for every import.

getInteractionFn() queries getMergeMeta('engine.config') on each first hook call.
If the user toggles selectMode at runtime (e.g. in a settings panel) the cached interactionsFn will not update, producing incorrect behaviour.

Consider:

  1. Converting interactionsFn to a shallowRef that is updated by an explicit watcher on the meta configuration, or
  2. Documenting that selectMode is read-only at boot and requires a full reload to change.
packages/canvas/container/src/interactions/vue-rect.ts (1)

48-66: Initial rect values of 0 are treated as “unset”.

The merge logic uses !a.top etc. to detect an un-initialised side, but this treats valid 0 coordinates (element flushed to viewport edge) as “unset”.
A small sentinel value (e.g. Number.POSITIVE_INFINITY / NEGATIVE_INFINITY) or an undefined initial side avoids this edge-case.

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

14414-14422: Add snippet entry for Date Picker under "Element Plus组件"

The new snippet for ElDatePicker has been correctly inserted into the Element Plus组件 children list with the appropriate name, icon, and snippetName. The minimal schema: {} is acceptable if you intend to inherit default behavior or prompt configuration at instantiation.

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

144-168: Promise returned by updateSelectedNode is ignored

selectNodeById calls updateSelectedNode but does not await the promise.
If callers depend on the selection having scrolled into view (awaiting
scrollToNode), they may run before the async work finishes.

Either:

await updateSelectedNode(evt, type, isMultipleSelect)

or document clearly that the function is fire-and-forget and callers must not rely on its completion.

Comment on lines +100 to +104
if (instance?.type?.description === 'v-fgt') {
return getFragmentRect(instance)
}

if (instance.el?.nodeType === 1) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Fragment detection is brittle across Vue versions.

instance.type.description === 'v-fgt' is an internal detail that may change.
Vue exposes the Fragment symbol (import { Fragment } from 'vue') which is more stable:

-import { Fragment } from 'vue'-if (instance?.type?.description === 'v-fgt') {
+if (instance?.type === Fragment) {
   return getFragmentRect(instance)
 }

Switching to the public API avoids breakage when upgrading Vue.

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +282 to +748
{
"id": 1,
"version": "2.4.2",
"name": {
"zh_CN": "日期选择器"
},
"component": "ElDatePicker",
"icon": "datepick",
"description": "日期选择器",
"doc_url": "",
"screenshot": "",
"tags": "",
"keywords": "",
"dev_mode": "proCode",
"npm": {
"package": "element-plus",
"exportName": "ElDatePicker",
"destructuring": true
},
"group": "表单组件",
"category": "element-plus",
"configure": {
"loop": true,
"condition": true,
"styles": true,
"isContainer": false,
"isModal": false,
"isPopper": false,
"nestingRule": {
"childWhitelist": "",
"parentWhitelist": "",
"descendantBlacklist": "",
"ancestorWhitelist": ""
},
"isNullNode": false,
"isLayout": false,
"rootSelector": "",
"shortcuts": {
"properties": ["type", "size"]
},
"contextMenu": {
"actions": ["copy", "remove", "insert", "updateAttr", "bindEevent", "createBlock"],
"disable": []
},
"invalidity": [""],
"clickCapture": true,
"framework": "Vue"
},
"schema": {
"properties": [
{
"name": "0",
"label": {
"zh_CN": "基础属性"
},
"content": [
{
"property": "modelValue",
"label": {
"text": {
"zh_CN": "绑定值"
}
},
"description": {
"zh_CN": "绑定值"
},
"required": false,
"readOnly": false,
"disabled": false,
"cols": 12,
"labelPosition": "left",
"type": "string",
"widget": {
"component": "InputConfigurator",
"props": {}
}
},
{
"property": "readonly",
"label": {
"text": {
"zh_CN": "只读"
}
},
"description": {
"zh_CN": "是否只读"
},
"required": true,
"readOnly": false,
"disabled": false,
"cols": 12,
"labelPosition": "left",
"type": "boolean",
"defaultValue": false,
"widget": {
"component": "CheckBoxConfigurator",
"props": {}
}
},
{
"property": "disabled",
"label": {
"text": {
"zh_CN": "禁用"
}
},
"description": {
"zh_CN": "是否禁用"
},
"required": true,
"readOnly": false,
"disabled": false,
"cols": 12,
"labelPosition": "left",
"type": "boolean",
"defaultValue": false,
"widget": {
"component": "CheckBoxConfigurator",
"props": {}
}
},
{
"property": "size",
"label": {
"text": {
"zh_CN": "尺寸"
}
},
"description": {
"zh_CN": "输入框尺寸"
},
"required": true,
"readOnly": false,
"disabled": false,
"cols": 12,
"labelPosition": "left",
"type": "string",
"defaultValue": "",
"widget": {
"component": "SelectConfigurator",
"props": {
"allowClear": true,
"options": [
{
"label": "large",
"value": "large"
},
{
"label": "default",
"value": "default"
},
{
"label": "small",
"value": "small"
}
]
}
}
},
{
"property": "editable",
"label": {
"text": {
"zh_CN": "是否可编辑"
}
},
"description": {
"zh_CN": "文本框是否可编辑"
},
"required": true,
"readOnly": false,
"disabled": false,
"cols": 12,
"labelPosition": "left",
"type": "boolean",
"defaultValue": true,
"widget": {
"component": "CheckBoxConfigurator",
"props": {}
},
"device": []
},
{
"property": "clearable",
"label": {
"text": {
"zh_CN": "是否可清除"
}
},
"description": {
"zh_CN": "是否显示清楚按钮"
},
"required": true,
"readOnly": false,
"disabled": false,
"cols": 12,
"labelPosition": "left",
"type": "boolean",
"defaultValue": true,
"widget": {
"component": "CheckBoxConfigurator",
"props": {}
},
"device": []
},
{
"property": "placeholder",
"label": {
"text": {
"zh_CN": "占位文本"
}
},
"description": {
"zh_CN": "非范围选择时的占位内容"
},
"required": true,
"readOnly": false,
"disabled": false,
"cols": 12,
"labelPosition": "left",
"defaultValue": "",
"type": "string",
"widget": {
"component": "InputConfigurator",
"props": {}
},
"device": []
},
{
"property": "start-placeholder",
"label": {
"text": {
"zh_CN": "起始占位文本"
}
},
"description": {
"zh_CN": "范围选择时开始日期的占位内容"
},
"required": true,
"readOnly": false,
"disabled": false,
"cols": 12,
"labelPosition": "left",
"defaultValue": "",
"type": "string",
"widget": {
"component": "InputConfigurator",
"props": {}
},
"device": []
},
{
"property": "end-placeholder",
"label": {
"text": {
"zh_CN": "结束占位文本"
}
},
"description": {
"zh_CN": "范围选择时结束日期的占位内容"
},
"required": true,
"readOnly": false,
"disabled": false,
"cols": 12,
"labelPosition": "left",
"defaultValue": "",
"type": "string",
"widget": {
"component": "InputConfigurator",
"props": {}
},
"device": []
},
{
"property": "type",
"label": {
"text": {
"zh_CN": "类型"
}
},
"description": {
"zh_CN": "显示类型"
},
"required": true,
"readOnly": false,
"disabled": false,
"cols": 12,
"labelPosition": "left",
"defaultValue": "date",
"type": "string",
"widget": {
"component": "SelectConfigurator",
"props": {
"options": [
{
"label": "year",
"value": "year"
},
{
"label": "years",
"value": "years"
},
{
"label": "month",
"value": "month"
},
{
"label": "months",
"value": "months"
},
{
"label": "date",
"value": "date"
},
{
"label": "dates",
"value": "dates"
},
{
"label": "datetime",
"value": "datetime"
},
{
"label": "week",
"value": "week"
},
{
"label": "datetimerange",
"value": "datetimerange"
},
{
"label": "daterange",
"value": "daterange"
},
{
"label": "monthrange",
"value": "monthrange"
},
{
"label": "yearrange",
"value": "yearrange"
}
]
}
},
"device": []
},
{
"property": "popper-class",
"label": {
"text": {
"zh_CN": "下拉框类名"
}
},
"description": {
"zh_CN": "DatePicker 下拉框的类名"
},
"required": true,
"readOnly": false,
"disabled": false,
"cols": 12,
"labelPosition": "left",
"defaultValue": "",
"type": "string",
"widget": {
"component": "InputConfigurator",
"props": {}
},
"device": []
}
],
"description": {
"zh_CN": ""
}
}
],
"events": {
"onUpdate:modelValue": {
"label": {
"zh_CN": "双向绑定值改变时触发"
},
"description": {
"zh_CN": "双向绑定值改变时触发"
}
},
"onChange": {
"label": {
"zh_CN": "用户确认选定的值时触发"
},
"description": {
"zh_CN": "用户确认选定的值时触发"
},
"type": "event",
"defaultValue": ""
},
"onBlur": {
"label": {
"zh_CN": "在组件 Input 失去焦点时触发"
},
"description": {
"zh_CN": "在组件 Input 失去焦点时触发"
},
"type": "event",
"defaultValue": ""
},
"onFocus": {
"label": {
"zh_CN": "在组件 Input 获得焦点时触发"
},
"description": {
"zh_CN": "在组件 Input 获得焦点时触发"
},
"type": "event",
"defaultValue": ""
},
"onCalendarChange": {
"label": {
"zh_CN": "在日历所选日期更改时触发"
},
"description": {
"zh_CN": "在日历所选日期更改时触发"
},
"type": "event",
"defaultValue": ""
},
"onPanelChange": {
"label": {
"zh_CN": "当日期面板改变时触发。"
},
"description": {
"zh_CN": "当日期面板改变时触发。"
},
"type": "event",
"defaultValue": ""
},
"onVisibleChange": {
"label": {
"zh_CN": "当 DatePicker 的下拉列表出现/消失时触发"
},
"description": {
"zh_CN": "当 DatePicker 的下拉列表出现/消失时触发"
},
"type": "event",
"defaultValue": ""
}
},
"slots": {
"default": {
"label": {
"zh_CN": "自定义单元格内容"
},
"description": {
"zh_CN": "自定义单元格内容"
}
},
"range-separator": {
"label": {
"zh_CN": "自定义范围分割符内容"
},
"description": {
"zh_CN": "自定义范围分割符内容"
}
}
}
}
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Duplicate component id detected; ensure unique identifiers.

You’ve introduced a new component entry with "id": 1, but immediately afterwards (lines 749–751) there’s another entry also using "id": 1. Component IDs must be unique across bundle.json to prevent collisions in rendering, lookup, or tooling.
Please increment the id to an unused value (e.g., the next highest integer) and re-verify that no other entries share that ID.

Run this script to spot any duplicate IDs in the file:


🏁 Script executed:

#!/usr/bin/env bash
# Detect duplicate `id` values in bundle.json
jq '[.components[].id] | group_by(.)[] | select(length > 1)' designer-demo/public/mock/bundle.json

Length of output: 192


The initial jq query failed because bundle.json doesn’t have a top-level components array. Let’s extract all "id" fields and then spot any repeats:


🏁 Script executed:

#!/usr/bin/env bash
# Extract every id value from any object and report duplicates
jq -r '.. | objects | select(has("id")) | .id' designer-demo/public/mock/bundle.json \
  | sort \
  | uniq -c \
  | awk '$1 > 1'

Length of output: 138


Fix duplicate component IDs in bundle.json

Our scan detected multiple duplicate id values in designer-demo/public/mock/bundle.json:

  • "1" appears 9 times
  • "2" appears 2 times

Component IDs must be unique to avoid rendering or lookup collisions. Please:

  • Update the new entry’s "id" to an unused integer (e.g., one greater than the current maximum).
  • Audit and resolve any other duplicates so each component has a unique id.
  • Re-run the following to confirm no duplicates remain:
    jq -r '.. | objects | select(has("id")) | .id' designer-demo/public/mock/bundle.json \
      | sort \
      | uniq -c \
      | awk '$1 > 1'

Comment on lines +167 to +172
if (selectState.value.length === 1) {
// TODO: 改成事件通知
canvasState.emit('selected', node, parent, type, node?.id)
} else {
canvasState.emit('selected', null, null, type, null)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Guard canvasState.emit – it is nullable elsewhere

Throughout this repo canvasState.emit is accessed with optional chaining (e.g. line 102).
Lines 169-172 drop the guard; if the emitter is not yet installed this becomes a
runtime error.

-    canvasState.emit('selected', node, parent, type, node?.id)
+    canvasState.emit?.('selected', node, parent, type, node?.id)
 ...
-    canvasState.emit('selected', null, null, type, null)
+    canvasState.emit?.('selected', null, null, type, null)

Please apply the same defensive style consistently.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (selectState.value.length === 1) {
// TODO: 改成事件通知
canvasState.emit('selected', node, parent, type, node?.id)
} else {
canvasState.emit('selected', null, null, type, null)
}
if (selectState.value.length === 1) {
// TODO: 改成事件通知
- canvasState.emit('selected', node, parent, type, node?.id)
+ canvasState.emit?.('selected', node, parent, type, node?.id)
} else {
- canvasState.emit('selected', null, null, type, null)
+ canvasState.emit?.('selected', null, null, type, null)
}

Comment on lines +85 to +94
const updateHoverNode = (e: MouseEvent) => {
const res = getRectAndNode(e)

if (!res || (res?.node?.id && selectState.value.some((state) => state?.node?.id === res.node.id))) {
clearHover()
return
}

curHoverState.value = res
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid writing “empty” hover state into the reactive ref

getRectAndNode returns a valid object even when no meaningful node is found (for example when the element has no UID).
Consequently updateHoverNode executes the assignment at line 93, leaving curHoverState with a rectangle {top:0,left:0,width:0,height:0} which the overlay component will still try to render – this produces a “0 × 0” artefact flicker.

-  const res = getRectAndNode(e)
-
-  if (!res || (res?.node?.id && selectState.value.some((state) => state?.node?.id === res.node.id))) {
-    clearHover()
-    return
-  }
-
-  curHoverState.value = res
+  const res = getRectAndNode(e)
+
+  // When `res.node` is null **or** the node is already selected, treat it as “no-hover”.
+  if (!res?.node || selectState.value.some((s) => s?.node?.id === res.node?.id)) {
+    clearHover()
+    return
+  }
+
+  curHoverState.value = res

This prevents the useless write and eliminates overlay flashes.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const updateHoverNode = (e: MouseEvent) => {
const res = getRectAndNode(e)
if (!res || (res?.node?.id && selectState.value.some((state) => state?.node?.id === res.node.id))) {
clearHover()
return
}
curHoverState.value = res
}
const updateHoverNode = (e: MouseEvent) => {
const res = getRectAndNode(e)
// When `res.node` is null **or** the node is already selected, treat it as “no-hover”.
if (!res?.node || selectState.value.some((s) => s?.node?.id === res.node?.id)) {
clearHover()
return
}
curHoverState.value = res
}

Comment on lines +199 to +203
if (selectState.value.length === 1) {
canvasState.emit('selected', node, parent, type, node?.id)
} else {
canvasState.emit('selected', null, null, type, null)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Null-safe emitter call

canvasState.emit might still be null (see earlier defensive usages).
Use optional chaining to prevent runtime crashes:

-  canvasState.emit('selected', node, parent, type, node?.id)
+  canvasState.emit?.('selected', node, parent, type, node?.id)
 ...
-  canvasState.emit('selected', null, null, type, null)
+  canvasState.emit?.('selected', null, null, type, null)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (selectState.value.length === 1) {
canvasState.emit('selected', node, parent, type, node?.id)
} else {
canvasState.emit('selected', null, null, type, null)
}
if (selectState.value.length === 1) {
canvasState.emit?.('selected', node, parent, type, node?.id)
} else {
canvasState.emit?.('selected', null, null, type, null)
}

Comment on lines 480 to +485
if (!element) {
const { clearHover } = useHoverNode()

return clearHover()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Stale guideline when leaving canvas – add clearLineState

If element is undefined (e.g. pointer exits the iframe), only hover is reset. The last guideline rendered by lineState remains visible.

-  if (!element) {
-    const { clearHover } = useHoverNode()
-    return clearHover()
+  if (!element) {
+    const { clearHover } = useHoverNode()
+    clearHover()
+    clearLineState()
+    return
   }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 334 to +350
export const querySelectById = (id: string) => {
let selector = `[${NODE_UID}="${id}"]`
const selector = `[${NODE_UID}="${id}"]`
const doc = getDocument()
let element = doc.querySelector(selector)
const loopId = element?.getAttribute('loop-id')
if (element && loopId) {
selector = `[${NODE_UID}="${id}"][${NODE_LOOP}="${loopId}"]`
element = doc.querySelector(selector)
const node = useCanvas().getNodeById(id)
const { rootSelector } = getConfigure(node?.componentName)

// 根据 id 无法查找到 element,尝试使用 rootSelector 查找
if (!element && rootSelector) {
// TODO: 拖入了多个相同组件的情况下,如何拿到正确的 element
const newElement = doc.querySelector(rootSelector)

if (newElement) {
element = newElement
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

rootSelector fallback may pick the wrong element

When several instances of the same component exist, doc.querySelector(rootSelector) always returns the first match.
This flakes selection/hover for later instances.

-const newElement = doc.querySelector(rootSelector)
-
-if (newElement) {
-  element = newElement
-}
+const candidates = Array.from(doc.querySelectorAll(rootSelector))
+element = candidates.find((el) => el.getAttribute(NODE_UID) === id) as Element | undefined

This keeps behaviour deterministic without changing the public API.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const querySelectById = (id: string) => {
let selector = `[${NODE_UID}="${id}"]`
const selector = `[${NODE_UID}="${id}"]`
const doc = getDocument()
let element = doc.querySelector(selector)
const loopId = element?.getAttribute('loop-id')
if (element && loopId) {
selector = `[${NODE_UID}="${id}"][${NODE_LOOP}="${loopId}"]`
element = doc.querySelector(selector)
const node = useCanvas().getNodeById(id)
const { rootSelector } = getConfigure(node?.componentName)
// 根据 id 无法查找到 element,尝试使用 rootSelector 查找
if (!element && rootSelector) {
// TODO: 拖入了多个相同组件的情况下,如何拿到正确的 element
const newElement = doc.querySelector(rootSelector)
if (newElement) {
element = newElement
}
}
export const querySelectById = (id: string) => {
const selector = `[${NODE_UID}="${id}"]`
const doc = getDocument()
let element = doc.querySelector(selector)
const node = useCanvas().getNodeById(id)
const { rootSelector } = getConfigure(node?.componentName)
// 根据 id 无法查找到 element,尝试使用 rootSelector 查找
if (!element && rootSelector) {
// TODO: 拖入了多个相同组件的情况下,如何拿到正确的 element
const candidates = Array.from(doc.querySelectorAll(rootSelector))
element = candidates.find((el) => el.getAttribute(NODE_UID) === id) as Element | undefined
}
// …rest of the function…
}

@hexqi hexqi merged commit a3b7bf8 into opentiny:release/2.5.0-alpha.6 Apr 24, 2025
5 checks passed
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.

2 participants