Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

refact: getRect logic #1132

Draft
wants to merge 11 commits into
base: develop
Choose a base branch
from

Conversation

chilingling
Copy link
Member

@chilingling chilingling commented Feb 20, 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?

Issue Number: N/A

What is the new behavior?

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

Summary by CodeRabbit

  • Enhancements
    • Refined canvas interactions with smoother hover, selection, and drag-and-drop behavior.
    • Improved event responsiveness for more accurate element state updates.
    • Enhanced component mounting processes for better overall app performance.

Copy link
Contributor

coderabbitai bot commented Feb 20, 2025

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

This PR updates several canvas modules by refining how hover states and node selections are handled. In the CanvasContainer component, mouse event handling has been modified to use a newly imported hover state. A set of helper functions and reactive variables is added in the component-selection module to support Vue component detection and bounding rectangle computations. Additionally, container logic and event handling in drag-and-drop operations are updated to better integrate the new hover state functions. The Vue application initialization is also enhanced with a new mixin that attaches component instances to DOM elements.

Changes

File(s) Change Summary
packages/.../CanvasContainer.vue Updated event handling in the setup function by replacing commented logic with use of currentHoverNode and calling updateHoverNode(ev) on mouseover; streamlined hover state management.
packages/.../component-selection.js Added functions (getClosedVueElement, getElement, getTextRect, createRect, mergeRects, getFragmentRect, getElementRect) and reactive variables (currentHoverInstance, currentHoverRect, currentHoverNode) to manage Vue components and hover states; introduced updateHoverNode(e).
packages/.../container.js Modified setSelectRect and selectNode to accept an instance parameter; updated dragMove to use updateLineState instead of direct hover state setting; exported getElement from component-selection.
packages/.../render/src/render.ts Commented out the assignment of the onMouseover event in getBindProps while retaining onFocus and onClickCapture functions.
packages/.../render/src/runner.ts Introduced the GetComponentByDomNode object with an install method that attaches Vue components to the DOM via a mixin; updated the Vue app creation to use this new mixin alongside TinyI18nHost.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant CanvasContainer
  participant CompSel
  participant VueState
  User->>CanvasContainer: Mouseover event
  CanvasContainer->>CompSel: updateHoverNode(event)
  CompSel->>VueState: Update currentHoverNode, currentHoverInstance, currentHoverRect
  VueState-->>CanvasContainer: Return updated hover state
Loading
sequenceDiagram
  participant AppCreator
  participant VueApp
  participant GetComponentByDomNode
  participant TinyI18nHost
  AppCreator->>VueApp: createApp(Main)
  VueApp->>GetComponentByDomNode: Install mixin
  GetComponentByDomNode->>VueApp: Attach __vueComponent on mount
  VueApp->>TinyI18nHost: Use translation host
Loading

Possibly related PRs

Suggested labels

enhancement, refactor-main

Suggested reviewers

  • hexqi
  • rhlin

Poem

I'm a rabbit coding through the night,
Hopscotching with functions shining bright.
With hover states and mixins in tow,
My paws update the code as I go.
Celebrate these changes, leaping free—
A joyful PR made just for me! 🐰🌟


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ 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 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.

@chilingling chilingling changed the title Feat/get rect by vnode refact: getRect logic Feb 20, 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: 4

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

543-619: 🛠️ Refactor suggestion

Clean up debug logs and implement drag logic.

Several issues need attention:

  1. Remove console.log statements
  2. Implement the TODO for drag logic
  3. The hover state updates are commented out without clear replacement
-  console.log('updateLineState element', element)
-  console.log('updateLineState', element, data)

Would you like me to help implement the drag logic or create an issue to track this task?

🧹 Nitpick comments (5)
packages/canvas/container/src/component-selection.js (2)

43-136: Optimize rectangle calculation performance.

The rectangle calculation functions create new objects and perform multiple DOM operations. Consider caching results and minimizing reflows.

Consider these optimizations:

+const rectCache = new WeakMap()

 export const getElementRect = (instance) => {
+  if (rectCache.has(instance)) {
+    return rectCache.get(instance)
+  }

   let rect
   if (instance?.type?.description === 'v-fgt') {
     rect = getFragmentRect(instance)
   } else if (instance.el?.nodeType === 1) {
     rect = instance.el.getBoundingClientRect()
   } // ... rest of the conditions

+  rectCache.set(instance, rect)
   return rect
 }

+// Clear cache when needed
+export const clearRectCache = () => {
+  rectCache = new WeakMap()
+}

138-190: Enhance hover state management with debouncing.

The hover state updates could trigger frequent recalculations. Consider debouncing the updates.

Add debouncing to hover updates:

+import { ref } from 'vue'
+
+const debounce = (fn, wait) => {
+  let timeout
+  return (...args) => {
+    clearTimeout(timeout)
+    timeout = setTimeout(() => fn(...args), wait)
+  }
+}

 export const updateHoverNode = debounce((e) => {
   // ... existing implementation
 }, 16) // ~60fps
packages/canvas/container/src/CanvasContainer.vue (1)

214-224: Remove commented-out mousemove listener.

The mousemove event listener is commented out but should be removed since it's replaced by mouseover handling.

Remove the commented code:

-// win.addEventListener('mousemove', (ev) => {
-//   handleCanvasEvent(() => {
-//     dragMove(ev, true)
-//   })
-// })
packages/canvas/container/src/container.js (2)

215-237: Remove commented out code.

Since getElement is now imported from the component-selection module, this commented-out implementation should be removed to maintain code cleanliness.

-// export const getElement = (element) => {
-//   // 如果当前元素是body
-//   if (element === element.ownerDocument.body) {
-//     return element
-//   }
-
-//   // 如果当前元素是画布的html,返回画布的body
-//   if (element === element.ownerDocument.documentElement) {
-//     return element.ownerDocument.body
-//   }
-
-//   if (!element || element.nodeType !== 1) {
-//     return undefined
-//   }
-
-//   if (element.getAttribute(NODE_UID)) {
-//     return element
-//   } else if (element.parentElement) {
-//     return getElement(element.parentElement)
-//   }
-
-//   return undefined
-// }

342-349: Address the TODO for rootSelector + loopId handling.

The fallback mechanism using rootSelector is a good addition, but the TODO indicates that the interaction between rootSelector and loopId needs to be implemented.

Would you like me to help implement the rootSelector + loopId handling logic or create an issue to track this task?

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 8d51ad4 and df117df.

📒 Files selected for processing (5)
  • packages/canvas/container/src/CanvasContainer.vue (4 hunks)
  • packages/canvas/container/src/component-selection.js (1 hunks)
  • packages/canvas/container/src/container.js (11 hunks)
  • packages/canvas/render/src/render.ts (2 hunks)
  • packages/canvas/render/src/runner.ts (2 hunks)
🔇 Additional comments (5)
packages/canvas/render/src/runner.ts (2)

48-58: Consider potential memory leaks in the Vue mixin.

The mixin stores Vue component instances in DOM elements via __vueComponent. While this enables component lookup, it could prevent garbage collection of component instances if the DOM elements persist longer than expected.

Consider cleaning up the reference in the unmounted hook:

 const GetComponentByDomNode = {
   install: (Vue) => {
     Vue.mixin({
       mounted() {
         this.$el.__vueComponent = this?._
+      },
+      unmounted() {
+        if (this.$el) {
+          delete this.$el.__vueComponent
+        }
       }
     })
   }
 }

76-76: LGTM! The app creation sequence is correct.

The order of plugin installation is appropriate, with GetComponentByDomNode preceding TinyI18nHost.

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

101-101: LGTM! Removal of mouseover event binding is correct.

The removal of bindProps.onMouseover = stopEvent aligns with the new hover state management implemented in component-selection.js.

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

27-30: LGTM! Good modularization of element-related functionality.

Moving element-related functions to a separate module improves code organization and maintainability.


743-764: LGTM! Good enhancement to support instance-based selection.

The function has been properly updated to handle both element and instance-based selection, maintaining compatibility with existing code while adding new functionality.

Comment on lines 7 to 39
export const getClosedVueElement = (element) => {
if (!element) {
return element
}

if (element === element.ownerDocument.body) {
return element
}

if (element.__vueComponent) {
return element.__vueComponent
}

if (element.parentElement) {
return getClosedVueElement(element.parentElement)
}
}

export const getElement = (element) => {
let closedVueEle = getClosedVueElement(element)

if (!closedVueEle) {
return
}

while(closedVueEle && !closedVueEle?.[NODE_UID]) {
closedVueEle = closedVueEle.parent
}

if (closedVueEle) {
return closedVueEle
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve error handling in component lookup functions.

The recursive component lookup functions lack proper error boundaries and type checking.

Add type checks and error handling:

 export const getClosedVueElement = (element) => {
+  if (!(element instanceof Element) && element !== null) {
+    return null
+  }
+
   if (!element) {
     return element
   }
   // ... rest of the function
 }

 export const getElement = (element) => {
+  if (!(element instanceof Element)) {
+    return null
+  }
+
   let closedVueEle = getClosedVueElement(element)
   // ... rest of the function
 }
📝 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 getClosedVueElement = (element) => {
if (!element) {
return element
}
if (element === element.ownerDocument.body) {
return element
}
if (element.__vueComponent) {
return element.__vueComponent
}
if (element.parentElement) {
return getClosedVueElement(element.parentElement)
}
}
export const getElement = (element) => {
let closedVueEle = getClosedVueElement(element)
if (!closedVueEle) {
return
}
while(closedVueEle && !closedVueEle?.[NODE_UID]) {
closedVueEle = closedVueEle.parent
}
if (closedVueEle) {
return closedVueEle
}
}
export const getClosedVueElement = (element) => {
if (!(element instanceof Element) && element !== null) {
return null
}
if (!element) {
return element
}
if (element === element.ownerDocument.body) {
return element
}
if (element.__vueComponent) {
return element.__vueComponent
}
if (element.parentElement) {
return getClosedVueElement(element.parentElement)
}
}
export const getElement = (element) => {
if (!(element instanceof Element)) {
return null
}
let closedVueEle = getClosedVueElement(element)
if (!closedVueEle) {
return
}
while (closedVueEle && !closedVueEle?.[NODE_UID]) {
closedVueEle = closedVueEle.parent
}
if (closedVueEle) {
return closedVueEle
}
}

Comment on lines 85 to 104
const setCurrentNode = async (event) => {
const { clientX, clientY } = event
const element = getElement(event.target)
// const element = getElement(event.target)
closeMenu()
let node = getCurrent().schema
// let node = getCurrent().schema
let node = currentHoverNode.value

if (element) {
const currentElement = querySelectById(getCurrent().schema?.id)
if (node) {
// const currentElement = querySelectById(getCurrent().schema?.id)
// const currentElement = element

if (!currentElement?.contains(element) || event.button === 0) {
const loopId = element.getAttribute(NODE_LOOP)
if (loopId) {
node = await selectNode(element.getAttribute(NODE_UID), `loop-id=${loopId}`)
} else {
node = await selectNode(element.getAttribute(NODE_UID))
}
}
// const loopId = element[NODE_LOOP]

if (event.button === 0 && element !== element.ownerDocument.body) {
const { x, y } = element.getBoundingClientRect()
// if (loopId) {
// node = await selectNode(element.getAttribute(NODE_UID), element, `loop-id=${loopId}`)
// } else {
// }

node = await selectNode(node.id, currentHoverInstance.value)

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Simplify node selection logic.

The node selection logic has been updated to use currentHoverNode but retains commented-out code.

Clean up the implementation:

 const setCurrentNode = async (event) => {
   const { clientX, clientY } = event
-  // const element = getElement(event.target)
   closeMenu()
-  // let node = getCurrent().schema
   let node = currentHoverNode.value

   if (node) {
-    // const currentElement = querySelectById(getCurrent().schema?.id)
-    // const currentElement = element
-    // const loopId = element[NODE_LOOP]
-    // if (loopId) {
-    //   node = await selectNode(element.getAttribute(NODE_UID), element, `loop-id=${loopId}`)
-    // } else {
-    // }
     node = await selectNode(node.id, currentHoverInstance.value)
     // ... rest of the function
   }
 }
📝 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 setCurrentNode = async (event) => {
const { clientX, clientY } = event
const element = getElement(event.target)
// const element = getElement(event.target)
closeMenu()
let node = getCurrent().schema
// let node = getCurrent().schema
let node = currentHoverNode.value
if (element) {
const currentElement = querySelectById(getCurrent().schema?.id)
if (node) {
// const currentElement = querySelectById(getCurrent().schema?.id)
// const currentElement = element
if (!currentElement?.contains(element) || event.button === 0) {
const loopId = element.getAttribute(NODE_LOOP)
if (loopId) {
node = await selectNode(element.getAttribute(NODE_UID), `loop-id=${loopId}`)
} else {
node = await selectNode(element.getAttribute(NODE_UID))
}
}
// const loopId = element[NODE_LOOP]
if (event.button === 0 && element !== element.ownerDocument.body) {
const { x, y } = element.getBoundingClientRect()
// if (loopId) {
// node = await selectNode(element.getAttribute(NODE_UID), element, `loop-id=${loopId}`)
// } else {
// }
node = await selectNode(node.id, currentHoverInstance.value)
const setCurrentNode = async (event) => {
const { clientX, clientY } = event
closeMenu()
let node = currentHoverNode.value
if (node) {
node = await selectNode(node.id, currentHoverInstance.value)
// ... rest of the function
}
}

Comment on lines 769 to 775
export const hoverNode = (id, data) => {
const element = querySelectById(id)
element && setHoverRect(element, data)
// element && updateLineState(element, data)
if (element) {
updateHoverNode({ target: element })
}
}
Copy link
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

Clean up commented code and verify hover behavior.

The hover state management has been updated to use updateHoverNode, but the old implementation is still present in comments.

-  // element && updateLineState(element, data)

Run this script to verify all hover state updates use the new system:


🏁 Script executed:

#!/bin/bash
# Description: Check for any remaining uses of old hover state management

rg "updateLineState.*element.*data" -A 2

Length of output: 708


Clean up unused commented code and verify hover behavior

  • Remove the commented-out line (// element && updateLineState(element, data)) in the hoverNode function located in packages/canvas/container/src/container.js.
  • Confirm that the new hover behavior implemented via updateHoverNode works as expected.
  • Additionally, review the updateLineState function: if it’s no longer needed elsewhere, consider removing it to avoid confusion.
📝 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 hoverNode = (id, data) => {
const element = querySelectById(id)
element && setHoverRect(element, data)
// element && updateLineState(element, data)
if (element) {
updateHoverNode({ target: element })
}
}
export const hoverNode = (id, data) => {
const element = querySelectById(id)
if (element) {
updateHoverNode({ target: element })
}
}

Comment on lines 391 to 422
const setSelectRect = (element, instance) => {
let calcElement = element

if (!element && !instance) {
calcElement = getDocument().body
}

let left = 0
let top = 0
let width = 0
let height = 0

if (calcElement) {
const { left: l, top: t, width: w, height: h } = getRect(calcElement)

left = l
top = t
width = w
height = h
}

if (instance) {
const { left: l, top: t, width: w, height: h } = getElementRect(instance)
left = l
top = t
width = w
height = h
}

const { left, height, top, width } = getRect(element)

// const { left, height, top, width } = getRect(element)
const componentName = getCurrent().schema?.componentName || ''
Copy link
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

Clean up commented code and verify instance-based calculations.

The function has been improved to handle both element and instance-based calculations, but there are two issues:

  1. Remove the commented out code on line 421
  2. Verify that instance-based calculations using getElementRect work correctly with all component types
-  // const { left, height, top, width } = getRect(element)

Run this script to verify instance-based calculations work with different component types:


🏁 Script executed:

#!/bin/bash
# Description: Find all component types that use getElementRect
# Look for patterns where components might override or customize rectangle calculations

ast-grep --pattern 'getElementRect($_)'

Length of output: 574


Action Required: Clean Up Commented Code & Confirm Instance Calculation Consistency

  • Remove the obsolete commented-out line (line 421) in packages/canvas/container/src/container.js:

    -  // const { left, height, top, width } = getRect(element)
  • Our verification confirms that using getElementRect for instance-based calculations is consistent across multiple components (see its usage in component-selection.js and container.js). No further adjustments are needed for instance handling.

📝 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 setSelectRect = (element, instance) => {
let calcElement = element
if (!element && !instance) {
calcElement = getDocument().body
}
let left = 0
let top = 0
let width = 0
let height = 0
if (calcElement) {
const { left: l, top: t, width: w, height: h } = getRect(calcElement)
left = l
top = t
width = w
height = h
}
if (instance) {
const { left: l, top: t, width: w, height: h } = getElementRect(instance)
left = l
top = t
width = w
height = h
}
const { left, height, top, width } = getRect(element)
// const { left, height, top, width } = getRect(element)
const componentName = getCurrent().schema?.componentName || ''
const setSelectRect = (element, instance) => {
let calcElement = element
if (!element && !instance) {
calcElement = getDocument().body
}
let left = 0
let top = 0
let width = 0
let height = 0
if (calcElement) {
const { left: l, top: t, width: w, height: h } = getRect(calcElement)
left = l
top = t
width = w
height = h
}
if (instance) {
const { left: l, top: t, width: w, height: h } = getElementRect(instance)
left = l
top = t
width = w
height = h
}
const componentName = getCurrent().schema?.componentName || ''
}

@chilingling chilingling reopened this Feb 20, 2025
@chilingling chilingling marked this pull request as draft February 20, 2025 02:09
@chilingling chilingling added the enhancement New feature or request label Feb 26, 2025
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.

1 participant