Skip to content

Conversation

@SonyLeo
Copy link
Contributor

@SonyLeo SonyLeo commented Mar 25, 2025

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?

  1. 当前画布不支持多选节点拖拽
  2. 当前画布节点拖拽时,大纲树插件 固定状态下 面板异常关闭

Issue Number: N/A

What is the new behavior?

  1. 画布支持多选节点拖拽,如下图所示

操作步骤:
1.1 点击 Ctrl + 鼠标左键进行节点多选;
1.2 节点多选结束后,使用鼠标左键点击多选节点进行拖拽;
1.3 拖拽到指定位置、松开鼠标左键、拖拽结束;

多节点拖拽

  1. 画布节点拖拽时,大纲树插件 固定状态下 面板不会关闭

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Introduced a visual indicator on the canvas that provides real-time feedback during multi-drag and multi-selection operations.
    • Enhanced drag-and-drop functionality with improved handling of multi-drag actions, including multi-node selection and repositioning.
    • Added a new component specifically for displaying the state of multi-dragging operations.
    • Enabled multi-selection drag priority over single-node drag with seamless event handling.
  • Style

    • Updated visual cues, including animations and dynamic styling, to clearly signal multi-drag and multi-select states, improving overall interaction clarity.
    • Introduced new styles for selected items and dragging states to enhance user interface feedback.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 25, 2025

Walkthrough

The changes introduce multi-drag functionality to the canvas interface by adding a new component, CanvasMultiDragIndicator, and enhancing existing components to manage multi-drag events. The event handling logic in the canvas container has been refined to prioritize multi-dragging over single drag, with new reactive states tracking mouse and drag status. A new composable manages the multi-drag lifecycle, including position calculation, drop validation, and UI feedback. Type safety improvements enforce valid drag position values. Visual feedback for multi-selection and multi-drag states is enhanced with new props, CSS styles, and animations.

Changes

File(s) Change Summary
packages/.../CanvasContainer.vue Integrated multi-drag functionality with event handling prioritizing multi-drag; added reactive states and methods (isMultiDragging, multiDragState, getMultiDragPositionText, isMouseDown); included CanvasMultiDragIndicator component; updated setCurrentNode logic to handle multi-drag start and right-click menu.
packages/.../components/CanvasAction.vue Added isMultiDragging prop; enhanced class bindings and CSS styles to reflect multi-select and multi-drag visual states with pulsing animations.
packages/.../components/CanvasMultiDragIndicator.vue Added new component showing multi-drag status, drop indicators, and stacked previews of dragged nodes with dynamic styling and animations.
packages/.../composables/useMultiDrag.ts Created new composable managing multi-node drag lifecycle (start, move, end), position calculations, drop validation, drag state, and UI feedback.
packages/.../composables/useMultiSelect.ts Added reactive isMouseDown state to track mouse button status; modified multi-selection toggle logic to depend on mouse state; exposed isMouseDown externally.
packages/.../container.ts Improved type safety by exporting PositionType alias, freezing POSITION constant with explicit types, exporting initialDragState and isAncestor function; updated insertNode position parameter to use PositionType; removed unused imports and obsolete calls.

Poem

Oh, what a joyous day in our codegarden so bright,
I’m a little bunny hopping through lines with delight.
Multi-drag indicators and smart reactive cues,
Bring magic to our canvas with each clever use.
Bugs hop away as our functions align,
With carrot-like precision, our code begins to shine!
Hoppy coding to all—may our changes ever be fine! 🐇✨


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 enhancement New feature or request label Mar 25, 2025
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

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

1-431: 💡 Verification agent

🧩 Analysis chain

Verify if tests have been added for multi-drag functionality.

The PR description notes that tests have not been added for the changes. Consider adding tests to ensure the multi-drag functionality works as expected in various scenarios.


🏁 Script executed:

#!/bin/bash
# Look for test files that might test the multi-drag functionality
echo "Searching for potential test files..."
fd -t f -e spec.js -e spec.ts -e test.js -e test.ts 'drag|select|multi|canvas' packages/canvas/

Length of output: 182


Action Required: Add Tests for Multi-Drag Functionality

Our search in the packages/canvas/ directory did not return any test files related to drag, select, or multi functionalities. Since no tests were found verifying the multi-drag behavior, please add appropriate tests to cover various multi-drag scenarios and ensure the robustness of this functionality.

🧹 Nitpick comments (4)
packages/canvas/container/src/components/CanvasMultiDragIndicator.vue (2)

4-7: Consider adding an ARIA announcement for accessibility.
When displaying “正在拖拽 {{ multiStateLength }} 个组件” (line 5), you could provide an aria-live region or similar mechanism to announce changes to screen readers.


17-34: Consider optimizing preview rendering for large selections.
Displaying multiple preview items can grow overhead if users select numerous nodes. You might consider a virtualization approach or a limit on displayed previews if performance becomes an issue.

packages/canvas/container/src/composables/useMultiSelect.ts (1)

25-26: Evaluate the UX of removing selection only when the mouse is not pressed.
This approach can prevent unintended removal during drag, but verify it doesn’t introduce confusion if users expect the item to be deselected immediately.

packages/canvas/container/src/composables/useMultiDrag.ts (1)

526-683: Refactor the insertion of multiple nodes to handle edge concurrency.
Though the approach of removing nodes first and then re-inserting them in bulk is sensible, consider concurrent modifications if other operations might occur within the same timeframe. Locking or a synchronized approach might be beneficial in real-time multi-user scenarios.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between cb59aa0 and 5c0a841.

📒 Files selected for processing (6)
  • packages/canvas/container/src/CanvasContainer.vue (11 hunks)
  • packages/canvas/container/src/components/CanvasAction.vue (3 hunks)
  • packages/canvas/container/src/components/CanvasMultiDragIndicator.vue (1 hunks)
  • packages/canvas/container/src/composables/useMultiDrag.ts (1 hunks)
  • packages/canvas/container/src/composables/useMultiSelect.ts (3 hunks)
  • packages/canvas/container/src/container.ts (3 hunks)
🧰 Additional context used
🧬 Code Definitions (1)
packages/canvas/container/src/composables/useMultiDrag.ts (1)
packages/canvas/container/src/container.ts (15)
  • PositionType (29-29)
  • querySelectById (352-362)
  • POSITION (31-38)
  • lineState (121-123)
  • allowInsert (464-483)
  • isAncestor (485-500)
  • dragState (84-86)
  • initialDragState (40-48)
  • getElement (215-237)
  • getDocument (65-65)
  • getSchema (81-81)
  • removeNode (319-324)
  • insertNode (810-840)
  • getController (63-63)
  • syncNodeScroll (657-660)
🔇 Additional comments (26)
packages/canvas/container/src/components/CanvasMultiDragIndicator.vue (2)

9-15: Verify that lineState.forbidden covers all edge cases.
If the user drags over a forbidden area, double-check whether the UI accurately reflects this state in all scenarios (e.g., partial overlaps).


63-265: Great use of scoped styles and transitions!
The styling seems well-structured with relevant animation keyframes. This is readable and consistent with the codebase's theme usage.

packages/canvas/container/src/composables/useMultiSelect.ts (1)

8-9: Confirm that isMouseDown is appropriately toggled in all mouse event handlers.
Ensure isMouseDown.value is set to true on mousedown and reset on mouseup so the multi-selection logic accurately reflects user interactions.

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

4-4: Dynamic class bindings are well-defined.
Conditionally applying 'multi-select' and 'dragging' is straightforward and should maintain clarity for multi-drag states.


192-195: Ensure isMultiDragging prop is updated correctly.
Confirm that isMultiDragging aligns with the composable’s isMultiDragging() for consistent UI feedback.


723-749: Styling for .multi-select.dragging is solid.
The lowered opacity, dashed border, and pulsing animation effectively communicate multi-drag. This is a good UX improvement.

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

99-130: Multi-drag initialization looks correct.
Storing initial mouse positions and offset references for each selected node is clear and avoids unnecessary overhead.


251-390: Check correctness of body-drop edge cases.
The logic handles dropping onto “body” or empty space by scanning body’s children. Please confirm it behaves correctly for nested structures or for pages without children.

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

29-39: Good addition of type safety for position constants.

The introduction of the PositionType type alias and the refactoring of POSITION constant with Object.freeze() and proper type assertions enhances type safety and improves code maintainability.


40-40: Exporting initialDragState is appropriate.

Making the initialDragState exportable allows it to be reused by other components, particularly the new multi-drag functionality.


485-500: Good decision to export isAncestor function.

Exporting the isAncestor function makes it available for use in the multi-drag functionality. This function checks parent-child relationships between nodes, which is essential for validating drag operations.


810-810: Enhanced type safety for insertNode function.

Adding the PositionType type annotation to the position parameter improves type checking and helps prevent invalid position values from being passed to the function.

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

11-11: Good addition of isMultiDragging prop to CanvasAction.

Adding the isMultiDragging prop to the CanvasAction component allows it to adjust its behavior based on whether multiple nodes are being dragged.


16-22: Well-structured multi-drag indicator component.

The new canvas-multi-drag-indicator component is properly configured with all necessary props to display the state of multi-drag operations, enhancing the user experience.


73-75: Appropriate imports for new functionality.

Adding imports for CanvasMultiDragIndicator component and useMultiDrag composable correctly sets up the dependencies for the multi-drag feature.


106-106: Registered new component in components list.

Properly registering the CanvasMultiDragIndicator component in the components list ensures it's available for use in the template.

Also applies to: 107-107


127-127: Good extraction of isMouseDown state.

Extracting the isMouseDown state from the useMultiSelect composable enables better tracking of mouse state, which is essential for multi-drag operations.


130-131: Well-structured use of useMultiDrag composable.

The destructuring assignment from the useMultiDrag composable extracts all necessary functions and state for implementing the multi-drag functionality.


145-172: Improved event handling logic in setCurrentNode.

The modifications to setCurrentNode correctly prioritize handling of right-click menu and multi-drag start over single-node dragging, improving user experience.


174-180: Clear separation of single-node dragging logic.

The single-node dragging logic is now properly separated and only executed when multi-drag is not activated, ensuring the correct behavior in both scenarios.


250-251: Mouse state tracking added to mousedown event.

Setting isMouseDown.value = true in the mousedown event handler correctly tracks the mouse state, which is essential for multi-drag operations.


265-274: Prioritized multi-drag in mousemove event.

The mousemove event handler now correctly prioritizes multi-drag moving over normal dragging, ensuring a smooth user experience when dragging multiple nodes.


276-289: Enhanced mouseup event handling.

The mouseup event handler now updates the isMouseDown state and prioritizes multi-drag ending over normal mouseup, ensuring proper cleanup of drag state.


295-298: Prioritized multi-drag in dragover event.

The dragover event handler now correctly prioritizes multi-drag moving, ensuring a consistent experience across different drag initiation methods.


304-307: Prioritized multi-drag in drop event.

The drop event handler now correctly prioritizes multi-drag ending, ensuring proper cleanup of drag state regardless of how the drag was initiated.


396-398: Exposed necessary functions and state for the template.

Exporting isMultiDragging, multiDragState, and getMultiDragPositionText makes them available for use in the template, particularly for the new multi-drag indicator component.

@SonyLeo SonyLeo force-pushed the feat/multi-node-drag branch from 5c0a841 to 96d3040 Compare March 25, 2025 01:35
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

🧹 Nitpick comments (6)
packages/canvas/container/src/components/CanvasMultiDragIndicator.vue (3)

4-7: Consider externalizing status text for i18n flexibility
Currently, the text "正在拖拽 {{ multiStateLength }} 个组件" is hardcoded in Chinese, which may limit internationalization options if the application needs multi-language support.


38-60: Check prop defaults and usage
All props (e.g., lineState, multiDragState, getMultiDragPositionText) default to empty objects or strings. Verify that these defaults align with expected real usage, ensuring no references produce undefined property errors at runtime.


63-265: Recommend auditing style complexity
The advanced styling and animations are thorough and visually appealing. However, consider centralizing shared constants (e.g., color or spacing) or using theme variables to keep the styles maintainable and consistent across the codebase.

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

192-195: Confirm new isMultiDragging prop is correctly set
The prop isMultiDragging defaults to false. Validate that upstream logic properly toggles this prop to reflect the real multi-drag status throughout the drag lifecycle.


752-774: Check animation performance
The pulse-border and pulse-bg keyframes run continuously. For large-scale usage (e.g., dozens of selected items), consider potential performance effects and use debouncing or conditional animations.

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

40-48: Confirm initialDragState aligns with multi-drag logic
The new draging, data, and position fields may need to account for multi-drag scenarios if multiple items are being dragged. Validate that higher-level logic does not overlook these cases.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 5c0a841 and 96d3040.

📒 Files selected for processing (6)
  • packages/canvas/container/src/CanvasContainer.vue (11 hunks)
  • packages/canvas/container/src/components/CanvasAction.vue (3 hunks)
  • packages/canvas/container/src/components/CanvasMultiDragIndicator.vue (1 hunks)
  • packages/canvas/container/src/composables/useMultiDrag.ts (1 hunks)
  • packages/canvas/container/src/composables/useMultiSelect.ts (3 hunks)
  • packages/canvas/container/src/container.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/canvas/container/src/composables/useMultiSelect.ts
  • packages/canvas/container/src/composables/useMultiDrag.ts
  • packages/canvas/container/src/CanvasContainer.vue
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: push-check
🔇 Additional comments (7)
packages/canvas/container/src/components/CanvasMultiDragIndicator.vue (2)

17-31: Validate dragged node data in multiDragState
When rendering multiple previews, each dragged node is assumed to have id, componentName, and optional props. Ensure upstream code consistently sets these properties to prevent potential errors when a node is missing expected fields.


9-15:

Details

✅ Verification successful

Ensure line state object is always valid
The conditional check lineState.height && lineState.width effectively prevents rendering an empty drop indicator. However, to avoid potential runtime errors, confirm lineState is not null or undefined in all possible usage scenarios.

Run a quick check across the codebase to confirm lineState is never assigned null or undefined by searching for its assignments:


🏁 Script executed:

#!/bin/bash
rg -A 3 "lineState\s*="

Length of output: 973


Review Verification: lineState Validation

I verified the assignments across the codebase and confirmed that lineState is always created as a reactive object (in packages/canvas/container/src/container.ts) and consistently passed to components (e.g., in CanvasContainer.vue). There’s no evidence of it ever being set to null or undefined. The current conditional (lineState.height && lineState.width) is therefore sufficient to guard against rendering an empty drop indicator.

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

4-4: Validate dynamic class binding
Ensuring [ 'multi-select': multiStateLength > 1 ] and { dragging: isMultiDragging } logic matches the multi-drag state is crucial for clear visual feedback. Double-check that multiStateLength correctly updates from your multi-drag feature to avoid mismatch with the actual dragging state.


723-750: Ensure consistent multi-select styling
The .multi-select class shift to a dashed border and partial opacity is visually clear. Verify that each multi-selected element remains selectable or visible when multiple items overlap, preventing user confusion during multi-drag.

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

29-38: Enhanced type safety for position management
Defining PositionType and freezing POSITION helps avoid accidental modification of position constants. This is a good practice for clarity and reliability.


485-485: Exporting isAncestor
Exporting this function broadens its applicability. Ensure that only valid consumers call it, as misuse may cause unexpected recursion or misinterpretation if the hierarchy is not properly set up.


810-810: Overlay definition of insertNode
The position: PositionType = POSITION.IN signature enforces consistent use of POSITION. Great approach to strongly-type insertion positions and reduce accidental usage of invalid strings.

Copy link
Collaborator

@hexqi hexqi left a comment

Choose a reason for hiding this comment

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

似乎还是单个节点的拖拽
选中文本3和4拖拽:
image
拖拽后:
image

@hexqi hexqi added this to the v2.5.0 milestone Apr 16, 2025
@SonyLeo SonyLeo force-pushed the feat/multi-node-drag branch from 96d3040 to fff1549 Compare April 17, 2025 12:45
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

🧹 Nitpick comments (1)
packages/canvas/container/src/components/CanvasMultiDragIndicator.vue (1)

3-7: Consider internationalization for user-facing text.

The component uses hardcoded Chinese text which may cause issues for non-Chinese users. Consider using an i18n system for better localization support.

-      <span>正在拖拽 {{ multiStateLength }} 个组件</span>
+      <span>{{ i18n.t('canvas.dragging_components', { count: multiStateLength }) }}</span>
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 96d3040 and fff1549.

📒 Files selected for processing (6)
  • packages/canvas/container/src/CanvasContainer.vue (11 hunks)
  • packages/canvas/container/src/components/CanvasAction.vue (3 hunks)
  • packages/canvas/container/src/components/CanvasMultiDragIndicator.vue (1 hunks)
  • packages/canvas/container/src/composables/useMultiDrag.ts (1 hunks)
  • packages/canvas/container/src/composables/useMultiSelect.ts (3 hunks)
  • packages/canvas/container/src/container.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/canvas/container/src/composables/useMultiSelect.ts
  • packages/canvas/container/src/components/CanvasAction.vue
  • packages/canvas/container/src/container.ts
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: push-check
🔇 Additional comments (31)
packages/canvas/container/src/components/CanvasMultiDragIndicator.vue (6)

1-35: Well-structured template with clear section organization.

The template is organized into three logical sections:

  1. Multi-drag status indicator (lines 4-7)
  2. Drop position indicator (lines 9-15)
  3. Node preview container (lines 17-34)

This organization makes the component's purpose clear and maintains good separation of concerns.


38-61: Props structure is clean and well-typed.

The component has a clear API with well-defined props and default values. Each prop has an appropriate type definition which enhances type safety.


63-96: Good use of CSS variables for theming.

The component leverages CSS variables like --te-canvas-container-bg-color-checked for theming, which allows for consistent styling across the application and easier customization.


97-172: Well-implemented drop indicators with animation effects.

The drop indicator styles provide clear visual feedback with different positions (top, left, bottom, right, in) and states (normal, forbidden). The pulse animations enhance the user experience by drawing attention to the active drop zone.


173-228: Excellent preview implementation with stacked items.

The multi-drag preview provides a stacked view of dragged items with:

  1. Sequential numbering
  2. Component name display
  3. Optional ID display
  4. Z-index management for proper stacking
  5. Hover effects for better interaction

This gives users clear feedback about what they're dragging.


230-264: Well-defined animations for visual feedback.

The keyframe animations (preview-pulse, pulse-drop-line, pulse-drop-border) provide subtle but effective visual cues during the drag operation, making the interaction more intuitive.

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

11-11: Good integration of multi-drag state with CanvasAction component.

The isMultiDragging() prop is correctly passed to the CanvasAction component, allowing it to adapt its appearance based on the multi-drag state.


16-22: Well-implemented multi-drag indicator integration.

The new CanvasMultiDragIndicator component is properly integrated with all necessary props:

  • lineState for positioning
  • multiDragState for node information
  • multiStateLength for count display
  • isMultiDragging for conditional rendering
  • getMultiDragPositionText for position information

This ensures the indicator has all the data it needs to provide appropriate visual feedback.


73-75: Good component and composable imports.

The necessary imports are added for the new multi-drag functionality: the CanvasMultiDragIndicator component and the useMultiDrag composable.


105-106: Component registration is correctly updated.

The new CanvasMultiDragIndicator component is properly registered in the components object.


127-131: Clean composable integration with destructuring.

The code uses destructuring to extract both the multiSelectedStates and isMouseDown from useMultiSelect, as well as all necessary methods and state from useMultiDrag. This makes the code more readable and maintainable.


146-152: Improved event handling with clear priority.

The event handling logic has been restructured to have a clear order of operations:

  1. Early return if no element
  2. Handle right-click menu first
  3. Then handle drag operations

This approach prevents conflicts between different interactions.


169-172: Multi-drag operation takes precedence over single-node drag.

The code correctly prioritizes multi-drag handling before single-node drag operations, which ensures consistent behavior when multiple nodes are selected.


173-180: Single-node drag behavior is preserved with proper condition.

The single-node drag operation is only triggered when:

  1. The left mouse button is pressed
  2. The target is not the body element
  3. Only one node is selected

This ensures compatibility with the existing drag behavior while adding multi-drag functionality.


250-252: Mouse down state tracking added for multi-select behavior.

The isMouseDown state is now tracked in the mousedown event, which is important for coordinating multi-select and multi-drag operations.


265-274: Mouse move handler prioritizes multi-drag operations.

The mousemove event handler first attempts to handle multi-drag movement with moveMultiDrag(), falling back to the standard dragMove() if the multi-drag handler returns false. This ensures proper event handling precedence.


277-288: Clean mouse up event handling with state management.

The mouseup event handler:

  1. Updates the isMouseDown state
  2. Attempts to handle multi-drag end first
  3. Falls back to standard drag end handling if necessary

This approach maintains a clear separation of concerns and proper event handling precedence.


295-298: Consistent drag prioritization in dragover event.

The dragover event handler follows the same pattern as mousemove, prioritizing multi-drag operations before standard drag operations.


304-307: Consistent drop prioritization in drop event.

The drop event handler follows the same pattern as mouseup, prioritizing multi-drag end before standard drag end handling.


374-398: Exports include all necessary multi-drag state and methods.

The setup function returns all the necessary multi-drag related state and methods:

  • isMouseDown for tracking mouse state
  • isMultiDragging for conditional rendering
  • multiDragState for node information
  • getMultiDragPositionText for position description

This makes them available to the template and child components.

packages/canvas/container/src/composables/useMultiDrag.ts (11)

24-65: Well-defined TypeScript interfaces for type safety.

The code defines clear interfaces for:

  • Position (x, y coordinates)
  • Offset (drag offset data)
  • NodeSchema (component data structure)
  • MultiDragState (drag operation state)
  • SelectState (selection state)

This provides good type safety and makes the code more maintainable.


77-87: Clean initial state with appropriate defaults.

The initialMultiDragState object provides sensible defaults for all properties:

  • Boolean flags initialized to false
  • Collections initialized to empty
  • Object references initialized to null

This prevents undefined behavior when the state is first used.


98-130: Well-implemented drag start handler with validation.

The startMultiDrag function:

  1. Checks if multi-selection is active (>1 node selected)
  2. Verifies the clicked node is part of the selection
  3. Records initial mouse position and target node
  4. Calculates and stores offset for each node

This ensures multi-drag operations only start when appropriate.


133-159: Precise drop position calculation logic.

The calculateDropPosition function implements sophisticated logic to determine the drop position:

  1. Uses proportional thresholds based on element size (min(20, rect.height/3))
  2. Prioritizes edge detection (top, bottom, left, right)
  3. Falls back to container insertion (IN) for containers
  4. Defaults to bottom position as a last resort

This provides accurate and intuitive drop positioning.


170-248: Comprehensive validation for drop operations.

The checkAllowInsert function performs thorough validation:

  1. Special handling for body element
  2. Prevents circular references (dropping into descendants)
  3. Prevents invalid parent-child relationships
  4. Validates against component-specific insertion rules

This ensures data integrity is maintained during drag operations.


251-274: Drag threshold implementation prevents accidental drags.

The moveMultiDrag function includes a drag threshold check:

  1. Calculates distance moved from initial position
  2. Only starts dragging if threshold (5px) is exceeded
  3. Clears single-drag state when multi-drag starts

This creates a more deliberate drag interaction and prevents accidental movements.


285-390: Special handling for body and edge cases.

The code includes detailed handling for special cases:

  1. Dragging over the document body
  2. Finding the closest node when dragging between elements
  3. Handling empty body scenarios
  4. Setting appropriate forbidden states when needed

This ensures the drag operation works correctly in all contexts.


404-472: Self-drag prevention with sibling target adjustment.

The code detects and handles self-dragging scenarios:

  1. Identifies when dragging over a selected node
  2. Adjusts target to nearest sibling when appropriate
  3. Updates lineState to reflect the adjusted target
  4. Falls back to forbidden state when no valid target is found

This prevents invalid operations like dragging a node onto itself.


525-683: Batch operation for node insertion and history management.

The endMultiDrag function:

  1. Collects all valid operations into a batch
  2. Removes source nodes first to prevent conflicts
  3. Inserts nodes at their target positions
  4. Adds a single history entry for the entire operation
  5. Rebuilds the multi-selection state after insertion

This approach maintains schema integrity and provides clean undo/redo support.


686-725: Helpful position text generation for UI feedback.

The getMultiDragPositionText computed property:

  1. Provides clear descriptions for each position type
  2. Includes the target component name for context
  3. Shows appropriate messages for forbidden drops
  4. Updates reactively as the drag state changes

This enhances the user experience with informative feedback.


727-735: Clean API export with all necessary functions and state.

The composable exports a well-defined API with:

  1. State object (multiDragState)
  2. Helper for position text (getMultiDragPositionText)
  3. Lifecycle methods (startMultiDrag, moveMultiDrag, endMultiDrag)
  4. State check helper (isMultiDragging)

This provides everything needed to integrate the functionality.

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: 1

♻️ Duplicate comments (3)
packages/canvas/container/src/composables/useMultiDrag.ts (3)

169-248: 🛠️ Refactor suggestion

Refactor complex method into smaller, focused functions.

The checkAllowInsert method is too long (80 lines) and handles multiple responsibilities, making it difficult to maintain and test. Break it into smaller, focused functions for each specific check.

// Main function to check if insertion is allowed
const checkAllowInsert = (
  configure: { isContainer?: boolean } | null,
  nodes: NodeSchema[],
  targetId: string,
  position: PositionType
): boolean => {
  // 如果没有配置,不允许放置
  if (!configure) return false

  // 获取目标节点的信息
  const { parent: targetParent } = useCanvas().getNodeWithParentById(targetId) || {}
  const targetParentId = targetParent?.id

  // 如果目标是body,特殊处理
  if (targetId === 'body') {
    return checkBodyInsert(position, nodes);
  }

  // 如果目标节点的父节点是body,特殊处理
  if (targetParentId === 'body' && (position === POSITION.TOP || position === POSITION.BOTTOM)) {
    return checkBodyChildInsert(nodes);
  }

  // 检查所有节点是否都允许放置
  return checkNodesInsert(nodes, targetId, targetParentId, position, configure);
}

// Helper function to check insertion in body
const checkBodyInsert = (position: PositionType, nodes: NodeSchema[]): boolean => {
  // 对于body,允许放置到内部、上方和下方
  if (position !== POSITION.IN && position !== POSITION.TOP && position !== POSITION.BOTTOM) {
    // 强制将position设置为IN,因为body只能放置到内部、上方或下方
    lineState.position = POSITION.IN;
  }

  // 检查所有节点是否都允许放置到body内
  for (const node of nodes) {
    if (!allowInsert({ isContainer: true }, node)) {
      return false;
    }
  }
  return true;
}

// Helper function to check insertion for body's direct children
const checkBodyChildInsert = (nodes: NodeSchema[]): boolean => {
  // 检查所有节点是否都允许放置到body内
  for (const node of nodes) {
    if (!allowInsert({ isContainer: true }, node)) {
      return false;
    }
  }
  return true;
}

// Helper function to check if nodes can be inserted at target position
const checkNodesInsert = (
  nodes: NodeSchema[],
  targetId: string,
  targetParentId: string | undefined,
  position: PositionType,
  configure: { isContainer?: boolean } | null
): boolean => {
  for (const node of nodes) {
    // 如果是放置到容器内,检查节点是否是目标节点的祖先
    if (position === POSITION.IN && isAncestor(node.id, targetId)) {
      return false;
    }

    // 如果是放置到节点前后,检查节点是否是目标节点的父节点
    if (
      (position === POSITION.TOP ||
        position === POSITION.BOTTOM ||
        position === POSITION.LEFT ||
        position === POSITION.RIGHT) &&
      node.id === targetParentId
    ) {
      return false;
    }

    // 检查节点是否允许放置到目标位置
    if (position === POSITION.IN) {
      // 放置到容器内需要检查容器的配置
      if (!allowInsert(configure, node)) {
        return false;
      }
    } else {
      // 放置到节点前后需要检查父节点的配置
      const { parent: targetParent } = useCanvas().getNodeWithParentById(targetId) || {};
      const parentConfigure = targetParent ? getConfigure(targetParent.componentName) : { isContainer: true };
      if (!allowInsert(parentConfigure, node)) {
        return false;
      }
    }
  }

  return true;
}

250-523: ⚠️ Potential issue

Refactor overly complex moveMultiDrag function.

This function is extremely long (273 lines) with high cognitive complexity, handling multiple responsibilities. It should be broken down into smaller, focused functions each handling a specific aspect of the drag operation.

The function's complexity makes it hard to maintain, test, or debug. Here's a suggested approach to refactoring:

  1. Extract the body element handling into a separate function
  2. Extract the sibling node handling logic into a separate function
  3. Extract the container placement logic into a separate function
  4. Create a main coordinator function that calls these specialized functions

For example:

// Main function coordinates the drag movement
const moveMultiDrag = (event: MouseEvent): boolean => {
  if (!multiDragState.keydown || multiStateLength.value <= 1) return false

  const { clientX, clientY } = event
  const currentMousePos: Position = { x: clientX, y: clientY }

  // Handle drag initialization
  if (!handleDragInitialization(currentMousePos)) {
    return true;
  }

  // Update mouse position
  multiDragState.mouse = currentMousePos

  // Initialize drag state if needed
  if (!multiDragState.draging && multiDragState.dragStarted) {
    multiDragState.draging = true
  }

  // If not dragging yet, exit early
  if (!multiDragState.draging) {
    return true
  }

  const targetElement = getElement(event.target as HTMLElement)

  // Handle special case: body or no element
  if (!targetElement) {
    return handleBodyOrNoElement(event);
  }

  // Handle regular elements
  return handleRegularElement(event, targetElement);
}

// Helper functions would handle the specific cases

This would make the code more maintainable and easier to understand.


738-780: 🛠️ Refactor suggestion

Fix ordering issues when dragging multiple elements to containers.

The function attempts to maintain the original order of elements but has an issue with the sorting logic. Specifically, when dragging to the bottom of a container, it needs to ensure that elements maintain their vertical order.

// 处理页面底部和容器的情况
if (isBodyTarget && position === POSITION.BOTTOM) {
  // 放置到页面底部,始终保持从上到下的顺序
  const reorderedOperations = [...sortedOperations].reverse()
  reorderedOperations.forEach((op) => {
    insertNodeToTarget(op, isBodyTarget, targetId)
  })
} else if (position === POSITION.IN) {
  // 放置到容器内部,应该保持原始从上到下的顺序
- sortedOperations.forEach((op) => {
+ // 确保元素在容器内从上到下排列,不管容器的方向
+ const containerOps = position === POSITION.BOTTOM ? [...sortedOperations].reverse() : sortedOperations;
+ containerOps.forEach((op) => {
    insertNodeToTarget(op, isBodyTarget, targetId)
  })
} else {
  // 其他情况按照排序后的顺序插入
  sortedOperations.forEach((op) => {
    insertNodeToTarget(op, isBodyTarget, targetId)
  })
}
🧹 Nitpick comments (10)
packages/canvas/container/src/composables/useMultiDrag.ts (10)

89-90: Consider using a constant from a shared configuration file.

The DRAG_THRESHOLD value should ideally be managed in a centralized configuration file rather than defined locally, especially if this threshold might be used in other drag-related functionality.


98-130: The startMultiDrag function should capture selected nodes only once.

The function redundantly processes selected nodes twice - once when creating multiDragState.nodes and again when calculating offsets. This causes unnecessary iteration and DOM access.

const startMultiDrag = (event: MouseEvent, element: HTMLElement): boolean => {
  if (multiStateLength.value <= 1) return false

  // 检查点击的元素是否是已选中的节点之一
  const clickedNodeId = element?.getAttribute(NODE_UID)
  if (!clickedNodeId || !(multiSelectedStates.value as SelectState[]).some((state) => state.id === clickedNodeId)) {
    return false
  }

  const { clientX, clientY } = event
  multiDragState.keydown = true
  multiDragState.dragStarted = false
  multiDragState.draging = false
  multiDragState.initialMousePos = { x: clientX, y: clientY }
  multiDragState.targetNodeId = clickedNodeId
-  multiDragState.nodes = toRaw(multiSelectedStates.value as SelectState[]).map((state) => state.schema)
+  
+  // Process selected states once and store both nodes and offsets
+  const selectedStates = toRaw(multiSelectedStates.value as SelectState[]);
+  multiDragState.nodes = selectedStates.map((state) => state.schema)

  // 计算每个节点相对于鼠标的偏移量
-  ;(multiSelectedStates.value as SelectState[]).forEach((state) => {
+  selectedStates.forEach((state) => {
    const elem = querySelectById(state.id)
    if (elem) {
      const { x, y } = elem.getBoundingClientRect()
      multiDragState.offsets.set(state.id, {
        offsetX: clientX - x,
        offsetY: clientY - y,
        initialX: x,
        initialY: y
      })
    }
  })

  return true
}

161-167: Use built-in Vector calculation utilities if available.

The distance calculation function is a basic utility that might already exist elsewhere in the codebase. Consider checking if a similar function exists in a utility module to maintain DRY principles.


305-318: Consider caching the body rectangle.

The body's dimensions don't change frequently during drag operations, so calculating bodyRect multiple times is inefficient. Consider caching this value or extracting it to a separate utility function.


552-568: Improve the structure and return type of getTargetNodeInfo.

The function returns an object with mixed naming conventions - some properties have "target" prefix while others have "final" prefix. Additionally, the return type is not explicitly defined, which could lead to type errors.

+ interface TargetNodeInfo {
+   targetNode: NodeSchema | undefined;
+   targetParent: NodeSchema | undefined;
+   isBodyTarget: boolean;
+   finalTargetNode: NodeSchema | undefined;
+   finalTargetParent: NodeSchema | null;
+ }

- const getTargetNodeInfo = (targetId: string) => {
+ const getTargetNodeInfo = (targetId: string): TargetNodeInfo => {
  const { getNodeWithParentById, getSchema } = useCanvas()
  const { node: targetNode, parent: targetParent } = getNodeWithParentById(targetId) || {}
  const isBodyTarget = targetId === 'body'

  // 如果目标是body,使用页面schema作为目标节点
  const finalTargetNode = isBodyTarget ? getSchema() : targetNode
  const finalTargetParent = isBodyTarget ? null : targetParent

  return {
    targetNode,
    targetParent,
    isBodyTarget,
    finalTargetNode,
    finalTargetParent
  }
}

644-657: Fix potential undefined access in sort comparison.

The sort comparison function doesn't handle cases where posA or posB is undefined properly. Although there's a check that returns 0 if either is undefined, the TypeScript compiler might still flag potential undefined access.

const sortedOperations = [...operations].sort((a, b) => {
  const posA = positions.get(a.sourceId)
  const posB = positions.get(b.sourceId)

  if (!posA || !posB) return 0

-  // 先按垂直位置排序
-  if (Math.abs(posA.top - posB.top) > 5) {
-    return posA.top - posB.top
-  }
-
-  // 如果垂直位置接近,则按水平位置排序
-  return posA.left - posB.left

+  // With type safety ensured
+  // 先按垂直位置排序
+  if (Math.abs(posA.top - posB.top) > 5) {
+    return posA.top - posB.top
+  }
+
+  // 如果垂直位置接近,则按水平位置排序
+  return posA.left - posB.left
})

783-795: Use more descriptive variable names for timeouts.

The timeout duration (150ms) is a magic number. Use a named constant with a descriptive name to explain why this specific delay was chosen.

// 清理拖拽状态
const cleanupDragState = () => {
+  // Delay to ensure DOM updates are complete before cleaning up state
+  const DRAG_CLEANUP_DELAY_MS = 150;
  // 清理拖拽状态,但保留多选状态
  setTimeout(() => {
    // 只清理拖拽相关状态,不清理多选状态
    Object.assign(multiDragState, {
      ...initialMultiDragState,
      nodes: [] // 确保清空nodes数组,避免影响后续操作
    })

    // 清除单选拖动状态
    Object.assign(dragState, initialDragState)
-  }, 150)
+  }, DRAG_CLEANUP_DELAY_MS)
}

834-836: Use direct reactive values for better performance.

The isMultiDragging computed property could be more performant by directly accessing the reactive values without creating unnecessary dependencies.

// 判断是否处于多选拖拽状态
const isMultiDragging = computed(() => {
-  return multiDragState.draging && multiDragState.dragStarted && multiStateLength.value > 1
+  // Directly check the current value of multiSelectedStates to avoid unnecessary reactivity
+  return multiDragState.draging && 
+         multiDragState.dragStarted && 
+         (multiSelectedStates.value as SelectState[]).length > 1
})

839-873: Optimize the computed property with early return pattern.

The getMultiDragPositionText computed property should use early returns for clarity and to avoid unnecessary condition evaluation.

// 获取多选拖拽的位置描述
const getMultiDragPositionText: ComputedRef<string> = computed(() => {
  if (!isMultiDragging.value) return ''

  const { position, forbidden, id } = lineState
  
  // 获取目标节点的组件名称,用于更详细的提示
  let targetComponentName = ''
  if (id && id !== 'body') {
    const targetElement = querySelectById(id)
    if (targetElement) {
      targetComponentName = targetElement.getAttribute(NODE_TAG) || ''
    }
  } else if (id === 'body') {
    targetComponentName = '页面'
  }

  if (forbidden) {
    return `当前位置不允许放置 (${targetComponentName || '目标节点'})`
  }
  
+  // Early return for empty position
+  if (!position) return ''

  switch (position) {
    case POSITION.TOP:
      return `放置到 ${targetComponentName || '目标节点'} 上方`
    case POSITION.BOTTOM:
      return `放置到 ${targetComponentName || '目标节点'} 下方`
    case POSITION.LEFT:
      return `放置到 ${targetComponentName || '目标节点'} 左侧`
    case POSITION.RIGHT:
      return `放置到 ${targetComponentName || '目标节点'} 右侧`
    case POSITION.IN:
      return `放置到 ${targetComponentName || '容器'} 内部`
    default:
      return ''
  }
})

1-883: Overall: Refactor this large composable into smaller, focused modules.

This composable is extremely long (883 lines) with high complexity. It should be refactored into multiple smaller modules, each handling a specific aspect of the multi-drag functionality:

  1. A core state manager
  2. A drag initialization and position calculator
  3. A drop target validator
  4. A drag operation executor
  5. UI feedback handlers

This would significantly improve maintainability, testability, and readability.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 41434d4 and e47d0fc.

📒 Files selected for processing (1)
  • packages/canvas/container/src/composables/useMultiDrag.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
packages/canvas/container/src/composables/useMultiDrag.ts (3)
packages/canvas/container/src/container.ts (16)
  • PositionType (50-50)
  • querySelectById (374-384)
  • POSITION (41-48)
  • lineState (137-139)
  • allowInsert (490-509)
  • isAncestor (511-526)
  • getConfigure (471-482)
  • dragState (99-101)
  • initialDragState (52-61)
  • getElement (231-253)
  • getDocument (80-80)
  • getSchema (96-96)
  • insertNode (849-883)
  • syncNodeScroll (685-688)
  • removeNode (341-346)
  • getController (78-78)
packages/canvas/container/src/composables/useMultiSelect.ts (1)
  • useMultiSelect (20-82)
packages/register/src/hooks.ts (1)
  • useCanvas (77-77)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: push-check
🔇 Additional comments (1)
packages/canvas/container/src/composables/useMultiDrag.ts (1)

132-159: 🛠️ Refactor suggestion

Ensure position calculations handle edge cases consistently.

Current position calculation assumes that rect and configure parameters are always valid, but there's no error handling or fallback for invalid inputs. This could lead to unexpected behavior if the parameters are null or undefined.

const calculateDropPosition = (
  event: MouseEvent,
  rect: DOMRect,
  configure: { isContainer?: boolean } | null
): PositionType => {
+  // Validate inputs
+  if (!rect) {
+    return POSITION.BOTTOM; // Default fallback position
+  }
+  
  const { clientX: mouseX, clientY: mouseY } = event
  // 参考单选节点的实现,使用更精确的计算方式
  const yAbs = Math.min(20, rect.height / 3)
  const xAbs = Math.min(20, rect.width / 3)

  // 优先判断是否在边缘区域
  if (mouseY < rect.top + yAbs) {
    return POSITION.TOP
  } else if (mouseY > rect.bottom - yAbs) {
    return POSITION.BOTTOM
  } else if (mouseX < rect.left + xAbs) {
    return POSITION.LEFT
  } else if (mouseX > rect.right - xAbs) {
    return POSITION.RIGHT
  } else if (configure?.isContainer) {
    // 如果是容器,且鼠标在中间区域,则放置到容器内
    return POSITION.IN
  }

  // 默认放置到底部
  return POSITION.BOTTOM
}

Likely an incorrect or invalid review comment.

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/CanvasContainer.vue (1)

139-150: Consider making isMultiDragging a computed property.

The isMultiDragging variable is destructured directly from the composable. Consider converting it to a computed property to prevent unnecessary re-evaluations on each render.

🧹 Nitpick comments (2)
packages/canvas/container/src/CanvasContainer.vue (2)

264-264: Remove unused variable and eslint comment.

The isScrolling variable is declared and set in the scroll event listener, but never used in conditional logic.

-        // eslint-disable-next-line @typescript-eslint/no-unused-vars
-        let isScrolling = false

And remove the corresponding code in the scroll event listener:

        win.addEventListener('scroll', () => {
-          isScrolling = true
        })

325-356: Extract node selection logic to a separate function.

The mouseup event handler contains complex logic for handling clicks on multi-selected nodes. Consider extracting this to a named function for improved readability.

+    // Handle clicks on multi-selected nodes without dragging
+    const handleMultiSelectNodeClick = (ev) => {
+      if (multiDragState.keydown && !multiDragState.dragStarted && multiStateLength.value > 1) {
+        const element = getElement(ev.target)
+        if (element) {
+          const clickedNodeId = element?.getAttribute(NODE_UID)
+          // Only switch to single selection when clicking on a node in the multi-selection
+          if (clickedNodeId && multiSelectedStates.value.some((state) => state.id === clickedNodeId)) {
+            selectNode(clickedNodeId)
+          }
+        }
+      }
+    }

        win.addEventListener('mouseup', (ev) => {
          handleCanvasEvent(() => {
            if (ev.button === 0 && isMouseDown.value) {
              isMouseDown.value = false

              // Determine if we need to switch to single selection
-              // Only needed when clicking a multi-selected node without dragging
-              if (multiDragState.keydown && !multiDragState.dragStarted && multiStateLength.value > 1) {
-                const element = getElement(ev.target)
-                if (element) {
-                  const clickedNodeId = element?.getAttribute(NODE_UID)
-                  // Only switch to single selection when clicking on a node in the multi-selection
-                  if (clickedNodeId && multiSelectedStates.value.some((state) => state.id === clickedNodeId)) {
-                    selectNode(clickedNodeId)
-                  }
-                }
-              }
+              handleMultiSelectNodeClick(ev)
            }
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 4eee8d9 and 6bc0afd.

📒 Files selected for processing (2)
  • packages/canvas/container/src/CanvasContainer.vue (11 hunks)
  • packages/canvas/container/src/composables/useMultiDrag.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/canvas/container/src/composables/useMultiDrag.ts
🔇 Additional comments (11)
packages/canvas/container/src/CanvasContainer.vue (11)

11-11: New prop allows visual state indication for multi-dragging.

The added :isMultiDragging prop provides the canvas-action component with information about the current drag state, which is essential for proper visual feedback during multi-selection drag operations.


16-22: Multi-drag indicator component adds visual feedback.

This new component provides essential visual feedback when dragging multiple nodes, showing position indicators and potentially the count of nodes being dragged.


73-75: Necessary imports for multi-drag functionality.

The new CanvasMultiDragIndicator component and useMultiDrag composable are correctly imported to support the multi-drag feature.


105-106: Component registration is properly updated.

Both CanvasViewerSwitcher and the new CanvasMultiDragIndicator are correctly registered in the components list.


127-137: Well-structured drag type enum improves code readability.

The DRAG_TYPE enum and currentDragType state clearly define the possible drag states, making the code more maintainable and easier to understand.


161-165: Clean helper function for resetting drag states.

The clearAllDragStates function effectively centralizes the logic for resetting all drag-related states, which helps prevent state inconsistencies and code duplication.


172-208: Improved event handling prioritization.

The updated setCurrentNode function appropriately prioritizes right-click menu handling, then multi-drag operations, and finally falls back to single node operations. The explicit state transitions make the code flow clear.


298-322: Clear switch structure for drag type handling.

The mousemove event listener now has a clear structure with a switch statement that handles each drag type appropriately. The logic for determining the drag type when it hasn't been set yet is well organized.


359-372: Consistent drag event handling.

The dragover event handler uses the same switch pattern as the mousemove handler, maintaining consistency across the codebase.


375-389: Consistent drop event handling.

The drop event handler follows the same pattern as other event handlers, maintaining a consistent approach to drag operation management across different event types.


477-479: Required state variables exposed.

All necessary state variables for the multi-drag feature are correctly exposed in the component's return object.

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/CanvasContainer.vue (1)

139-150: Consider making isMultiDragging a computed property.

As mentioned in a previous review comment, converting isMultiDragging to a computed property could optimize performance by preventing unnecessary method calls during re-renders.

🧹 Nitpick comments (3)
packages/canvas/container/src/CanvasContainer.vue (3)

308-327: Simplify drag type determination logic in mousemove.

The current implementation has redundant checks to determine the drag type in the mousemove handler when none is set. The drag type should ideally be determined only once during mousedown.

 case DRAG_TYPE.NONE:
   // 如果尚未确定拖拽类型,尝试确定
   if (isMouseDown.value) {
-    if (multiDragState.keydown) {
+    // If mousedown occurred but drag type wasn't set, we can determine it based on state
+    if (multiDragState.keydown && multiStateLength.value > 0) {
       currentDragType.value = DRAG_TYPE.MULTI
       moveMultiDrag(ev)
     } else if (dragState.element) {
       currentDragType.value = DRAG_TYPE.SINGLE
       dragMove(ev, true)
+    } else {
+      // No valid drag conditions met, clear mouse state
+      isMouseDown.value = false
     }
   }
   break

339-348: Extract node selection logic to the composable.

This node selection logic (determining when to switch from multi-select to single-select) could be moved to the useMultiSelect composable for better separation of concerns.


351-359: Consider refactoring repetitive switch statements.

There are several nearly identical switch statements handling drag types in different event handlers. Consider extracting this logic into a helper function to reduce duplication.

+ // Add this helper function
+ const handleDragByType = (eventType, event) => {
+   switch (currentDragType.value) {
+     case DRAG_TYPE.MULTI:
+       if (eventType === 'move') return moveMultiDrag(event);
+       if (eventType === 'end') return endMultiDrag();
+       break;
+     case DRAG_TYPE.SINGLE:
+       if (eventType === 'move') return dragMove(event, eventType === 'mousemove');
+       if (eventType === 'end') return onMouseUp(event);
+       break;
+   }
+ }

// Then replace switch statements with calls to this function:
// e.g., handleDragByType('move', ev) for mousemove/dragover
// e.g., handleDragByType('end', ev) for mouseup/drop

Also applies to: 371-378, 386-393

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 6bc0afd and 92a4e09.

📒 Files selected for processing (3)
  • packages/canvas/container/src/CanvasContainer.vue (11 hunks)
  • packages/canvas/container/src/composables/useMultiSelect.ts (3 hunks)
  • packages/canvas/container/src/container.ts (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/canvas/container/src/composables/useMultiSelect.ts
  • packages/canvas/container/src/container.ts
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: push-check
🔇 Additional comments (8)
packages/canvas/container/src/CanvasContainer.vue (8)

11-11: Good usage of isMultiDragging prop to indicate multi-drag state.

This prop allows CanvasAction component to visually represent the multi-dragging state, which enhances the user experience by providing visual feedback during the drag operation.


16-22: Excellent addition of the multi-drag indicator component.

The new CanvasMultiDragIndicator component with all necessary props provides visual feedback during multi-drag operations, which is essential for a good user experience. This component handles showing drag counts, positions, and drag state visualizations.


73-75: The imports for multi-drag functionality are well organized.

Proper imports for the new components and composables are included, which separates concerns and follows good architectural patterns.


127-138: Well-structured enum and reactive state for drag types.

The DRAG_TYPE enum clearly defines the possible drag states with descriptive comments, and currentDragType reactive state tracks the current drag operation type. This addresses previous feedback about having a drag state variable to determine execution paths.


161-165: Good abstraction with clearAllDragStates.

Creating a centralized function to clear all drag states improves code maintainability and reduces the chance of inconsistent state cleanup across different event handlers.


184-188: Clear priority handling for multi-drag operations.

The multi-drag start attempt is correctly prioritized over single-node selection, which creates a consistent and predictable interaction model.


462-487: Well-exposed state variables for component use.

All necessary drag state variables and helper functions are properly exposed from the setup function, making them available to child components and template.


1-24: Good component integration in the template.

The template structure properly incorporates the new multi-drag indicator component and passes the necessary props to both that and the CanvasAction component, ensuring proper visual feedback during multi-drag operations.

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/CanvasContainer.vue (1)

139-150: Consider refactoring isMultiDragging to a computed property.

The isMultiDragging flag is being imported directly from the useMultiDrag composable, but it could be more efficient to define it as a computed property based on the current drag type.

-const {
-  startMultiDrag,
-  moveMultiDrag,
-  endMultiDrag,
-  isMultiDragging,
-  getMultiDragPositionText,
-  multiDragState,
-  cleanupDragState
-} = useMultiDrag()
+const {
+  startMultiDrag,
+  moveMultiDrag,
+  endMultiDrag,
+  getMultiDragPositionText,
+  multiDragState,
+  cleanupDragState
+} = useMultiDrag()
+const isMultiDragging = computed(() => currentDragType.value === DRAG_TYPE.MULTI && multiDragState.dragStarted)
🧹 Nitpick comments (3)
packages/canvas/container/src/CanvasContainer.vue (3)

304-329: Refactor mousemove event handler for better readability.

The switch-case structure is appropriate, but the nested condition in the NONE case makes the code harder to follow. Consider extracting this logic to a separate function.

win.addEventListener('mousemove', (ev) => {
  handleCanvasEvent(() => {
    // 根据当前拖拽类型执行相应操作
    switch (currentDragType.value) {
      case DRAG_TYPE.MULTI:
        moveMultiDrag(ev)
        break
      case DRAG_TYPE.SINGLE:
        dragMove(ev, true)
        break
      case DRAG_TYPE.NONE:
-       // 如果尚未确定拖拽类型,尝试确定
-       if (isMouseDown.value) {
-         if (multiDragState.keydown) {
-           currentDragType.value = DRAG_TYPE.MULTI
-           moveMultiDrag(ev)
-         } else if (dragState.element) {
-           currentDragType.value = DRAG_TYPE.SINGLE
-           dragMove(ev, true)
-         }
-       }
+       determineDragType(ev)
        break
    }
  })
})

// 新增函数,用于确定拖拽类型
const determineDragType = (ev) => {
  if (!isMouseDown.value) return
  
  if (multiDragState.keydown) {
    currentDragType.value = DRAG_TYPE.MULTI
    moveMultiDrag(ev)
  } else if (dragState.element) {
    currentDragType.value = DRAG_TYPE.SINGLE
    dragMove(ev, true)
  }
}

331-362: Add debounce to mouseup event handler.

The mouseup event handler contains complex logic for handling different scenarios. Since these events can fire rapidly in succession, consider adding debounce to prevent potential performance issues.

+import { debounce } from 'lodash-es'

// 其他代码...

+const debouncedMouseUpHandler = debounce((ev) => {
+  if (ev.button === 0 && isMouseDown.value) {
+    isMouseDown.value = false
+
+    // 判断是否需要切换到单选状态
+    // 只有当点击多选节点但没有拖动时,才需要切换到单选状态
+    if (multiDragState.keydown && !multiDragState.dragStarted && multiStateLength.value > 1) {
+      const element = getElement(ev.target)
+      if (element) {
+        const clickedNodeId = element?.getAttribute(NODE_UID)
+        // 只有点击的是多选节点中的一个时才切换到单选
+        if (clickedNodeId && multiSelectedStates.value.some((state) => state.id === clickedNodeId)) {
+          selectNode(clickedNodeId)
+        }
+      }
+    }
+  }
+
+  // 根据当前拖拽类型执行相应的结束操作
+  switch (currentDragType.value) {
+    case DRAG_TYPE.MULTI:
+      endMultiDrag()
+      break
+    case DRAG_TYPE.SINGLE:
+      onMouseUp(ev)
+      break
+  }
+
+  clearAllDragStates()
+}, 50)

win.addEventListener('mouseup', (ev) => {
  handleCanvasEvent(() => {
-    if (ev.button === 0 && isMouseDown.value) {
-      isMouseDown.value = false
-
-      // 判断是否需要切换到单选状态
-      // 只有当点击多选节点但没有拖动时,才需要切换到单选状态
-      if (multiDragState.keydown && !multiDragState.dragStarted && multiStateLength.value > 1) {
-        const element = getElement(ev.target)
-        if (element) {
-          const clickedNodeId = element?.getAttribute(NODE_UID)
-          // 只有点击的是多选节点中的一个时才切换到单选
-          if (clickedNodeId && multiSelectedStates.value.some((state) => state.id === clickedNodeId)) {
-            selectNode(clickedNodeId)
-          }
-        }
-      }
-    }
-
-    // 根据当前拖拽类型执行相应的结束操作
-    switch (currentDragType.value) {
-      case DRAG_TYPE.MULTI:
-        endMultiDrag()
-        break
-      case DRAG_TYPE.SINGLE:
-        onMouseUp(ev)
-        break
-    }
-
-    clearAllDragStates()
+    debouncedMouseUpHandler(ev)
  })
})

456-480: Add typing information to the exposed state and functions.

While the implementation exposes all the necessary state and helper functions, adding TypeScript type annotations would improve code documentation and IDE support.

return {
-  isMouseDown,
-  iframe,
-  dragState,
-  hoverState,
-  inactiveHoverState,
-  computedSelectState,
-  lineState,
-  multiSelectedStates,
-  multiStateLength,
-  removeNodeById,
-  selectSlot,
-  canvasState,
-  insertComponent,
-  insertPanel,
-  containerPanel,
-  settingModel,
-  target,
-  showSettingModel,
-  insertPosition,
-  insertContainer,
-  loading,
-  srcAttrName,
-  isMultiDragging,
-  multiDragState,
-  getMultiDragPositionText
+  isMouseDown, // Ref<boolean>
+  iframe, // Ref<HTMLIFrameElement | null>
+  dragState, // State object for single drag operations
+  hoverState, // State object for hover operations
+  inactiveHoverState, // State object for inactive hover operations
+  computedSelectState, // ComputedRef for selected state
+  lineState, // State object for line indicators
+  multiSelectedStates, // Ref<Array> of selected states
+  multiStateLength, // ComputedRef<number> for number of selected states
+  removeNodeById, // Function to remove a node
+  selectSlot, // Function to select a slot
+  canvasState, // State object for canvas
+  insertComponent, // Function to insert a component
+  insertPanel, // Ref for insert panel
+  containerPanel, // Ref for container panel
+  settingModel, // Function to open settings
+  target, // Ref for event target
+  showSettingModel, // Ref<boolean> for settings visibility
+  insertPosition, // Ref for insert position
+  insertContainer, // Ref for insert container
+  loading, // ComputedRef<boolean> for loading state
+  srcAttrName, // ComputedRef<string> for src attribute name
+  isMultiDragging, // Ref<boolean> indicating multi-drag state
+  multiDragState, // State object for multi-drag operations
+  getMultiDragPositionText // Function to get position text for multi-drag
}
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 92a4e09 and 1100764.

📒 Files selected for processing (1)
  • packages/canvas/container/src/CanvasContainer.vue (11 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: push-check
🔇 Additional comments (6)
packages/canvas/container/src/CanvasContainer.vue (6)

11-11: Good addition of visual indicators for multi-drag operations.

The new isMultiDragging prop on CanvasAction and the CanvasMultiDragIndicator component provide valuable visual feedback to users during multi-drag operations. This enhancement improves the user experience by making it clear when multiple nodes are being dragged.

Also applies to: 16-22


127-138: Good use of an enum for drag states.

Creating a DRAG_TYPE enum with clearly named states makes the code more readable and maintainable. The approach of tracking the current drag type with a reactive variable is a clean way to manage the application state.


160-165: Good practice with the clearAllDragStates function.

Centralizing the cleanup logic in a single function helps maintain consistency and prevents state leaks. This is a good approach for ensuring all drag-related states are properly reset after operations complete.


183-189: Improved event handling logic for multi-drag operations.

The code now correctly prioritizes multi-drag operations when appropriate, improving the user experience. The early return pattern when multi-drag starts prevents conflicting states from occurring.


370-375: Good implementation of type-specific dragover handling.

The code now correctly routes dragover events to the appropriate handler based on the current drag type. This ensures consistent behavior between mouse and drag events.


382-389: Good implementation of type-specific drop handling.

Similar to the dragover handling, the drop event is properly routed to the appropriate handler. The addition of clearAllDragStates ensures that drag states are always cleaned up, which prevents state inconsistencies.

@hexqi hexqi merged commit 48a124b into opentiny:develop Apr 24, 2025
1 check passed
@SonyLeo SonyLeo deleted the feat/multi-node-drag branch August 24, 2025 01:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants