Skip to content

fix: fix cursor jumping and IME focus loss#176

Merged
SonyLeo merged 8 commits intoopentiny:developfrom
SonyLeo:fix/template-cursor-issues
Jul 31, 2025
Merged

fix: fix cursor jumping and IME focus loss#176
SonyLeo merged 8 commits intoopentiny:developfrom
SonyLeo:fix/template-cursor-issues

Conversation

@gene9831
Copy link
Copy Markdown
Collaborator

@gene9831 gene9831 commented Jul 29, 2025

修复输入块光标问题

场景:编辑块右侧无内容时
【已处理】1. 右侧输入英文时,光标位置不正确
【已处理】2. 右侧输入中文时,按下空格按键确认选择后,编辑器失焦
【已处理】3. 右侧输入中文,
中文输入法下,输入第二个字符时,光标移动至前一个编辑块,空格确认时,文字出现在前面的编辑块中;

另外修复了undo redo操作后数据未同步的问题

Summary by CodeRabbit

  • New Features

    • Improved cursor behavior and caret positioning, especially around placeholder text and templates.
    • Enhanced handling of empty template data, now recognizing cases where all nodes are empty text nodes.
  • Bug Fixes

    • Increased consistency and reliability in data management and history tracking within the template editor.
  • Refactor

    • Streamlined internal logic for placeholder management and selection handling to provide a more unified user experience.
  • Documentation

    • Updated type definitions to clarify optional item IDs and editor range properties.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jul 29, 2025

Walkthrough

The changes focus on improving the TemplateEditor component's data consistency, selection management, and cursor handling. Type definitions for user-facing text and template items now allow optional IDs. The logic for detecting empty templates is refined, and the EditorRange interface is extended with DOM element references. Several internal functions are reworked or added for robust placeholder and template handling.

Changes

Cohort / File(s) Change Summary
TemplateEditor Component Logic
packages/components/src/sender/components/TemplateEditor.vue
Refined internal data structure and selection management, improved transformation functions to preserve IDs, unified placeholder logic, reworked input and selection handlers, removed redundant functions, added new selection change listener, and updated history/model synchronization and lifecycle hooks.
User Item Type Definitions
packages/components/src/sender/index.type.ts
Updated UserTextItem and UserTemplateItem types to include an optional id property, allowing instances to optionally carry IDs while maintaining other constraints.
Template Emptiness Check
packages/components/src/sender/index.vue
Modified logic for determining if template data is empty: now considers the template empty if all nodes are empty text nodes, not just a single one. Introduced isEmptyTextNode helper for clarity.
Editor Range Interface
packages/components/src/sender/types/editor.type.ts
Extended EditorRange interface to include optional startEl and endEl properties, providing direct DOM element references for range endpoints.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant TemplateEditor
    participant Model
    participant History

    User->>TemplateEditor: Update model (external change)
    TemplateEditor->>TemplateEditor: Save selection range
    TemplateEditor->>TemplateEditor: Update internal data
    TemplateEditor->>History: Commit history
    TemplateEditor->>Model: Update model

    User->>TemplateEditor: Input event (typing, deletion)
    TemplateEditor->>TemplateEditor: Update selection range map
    TemplateEditor->>TemplateEditor: Process input (cleanup, placeholders)
    TemplateEditor->>History: Commit history
    TemplateEditor->>Model: Update model

    User->>TemplateEditor: Selection change
    TemplateEditor->>TemplateEditor: Adjust caret if on placeholder
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~35 minutes

Poem

A hop and a skip through the template’s core,
With IDs preserved and placeholders galore.
The cursor now dances, no longer stuck tight,
As rabbits refactor from morning till night.
Types gain new options, the emptiness clear—
This patch brings much order, so let’s give a cheer!
🐇✨

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.
    • Explain this complex logic.
    • 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 explain this code block.
    • @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 explain its main purpose.
    • @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.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

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 generate unit tests to generate unit tests 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.

SonyLeo
SonyLeo previously approved these changes Jul 29, 2025
Copy link
Copy Markdown

@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/components/src/sender/components/TemplateEditor.vue (1)

194-220: Good internal/external update detection logic.

The serialization comparison effectively prevents redundant processing and history commits. The selection range preservation ensures proper cursor restoration.

Regarding the TODO comment on line 200: Consider using a flag or metadata to explicitly track internal updates instead of relying on data comparison, which could be more performant and explicit.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between a0edb8a and 2b97f4a.

📒 Files selected for processing (4)
  • packages/components/src/sender/components/TemplateEditor.vue (18 hunks)
  • packages/components/src/sender/index.type.ts (1 hunks)
  • packages/components/src/sender/index.vue (1 hunks)
  • packages/components/src/sender/types/editor.type.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
packages/components/src/sender/index.vue (1)

Learnt from: gene9831
PR: #123
File: packages/components/src/bubble/message/Message.vue:22-40
Timestamp: 2025-06-25T07:04:18.791Z
Learning: In the Message component (packages/components/src/bubble/message/Message.vue), the renderer resolution is intentionally not reactive to type changes - the component is designed to work with a fixed type that doesn't change after initialization.

🧬 Code Graph Analysis (1)
packages/components/src/sender/index.type.ts (1)
packages/components/src/sender/types/editor.type.ts (2)
  • TextItem (7-9)
  • TemplateItem (11-15)
🔇 Additional comments (10)
packages/components/src/sender/index.type.ts (1)

127-129: LGTM! Good type design for optional ID handling.

The change to make id optional in user-facing types while keeping it required internally is a solid pattern. This allows the system to generate IDs when not provided by users while ensuring internal data consistency.

packages/components/src/sender/types/editor.type.ts (1)

29-36: LGTM! Useful addition for DOM element tracking.

Adding startEl and endEl to the EditorRange interface provides direct DOM element references, which improves performance and simplifies cursor positioning logic - directly supporting the PR's cursor behavior fixes.

packages/components/src/sender/index.vue (1)

278-281: LGTM! More robust template emptiness check.

The enhanced logic correctly identifies templates as empty when all nodes are zero-width space placeholders, not just when there's exactly one. This aligns well with the improved placeholder management in the TemplateEditor component.

packages/components/src/sender/components/TemplateEditor.vue (7)

2-2: LGTM! Clean setup and constants definition.

Good simplification of the zero-width character constants and appropriate lifecycle hook imports for the new selection change listener.

Also applies to: 38-42


56-72: LGTM! Proper ID handling in data transformations.

The transformations now correctly preserve existing IDs or generate new ones, ensuring consistent identity tracking between user and internal data representations. This is essential for maintaining proper references during editing operations.


76-107: LGTM! Excellent placeholder management logic.

The rewritten setOriginalData effectively prevents templates from appearing at start/end positions by adding placeholder text nodes. The cleanup of placeholders from nodes with actual content prevents accumulation. This directly addresses the cursor positioning issues mentioned in the PR objectives.


449-542: LGTM! Comprehensive input processing improvements.

The refactored processInput function properly handles:

  • Template deletion tracking
  • Empty item cleanup
  • Placeholder management to prevent templates at start/end
  • Prefix/suffix restoration

This addresses the data synchronization issues mentioned in the PR objectives for undo/redo operations.


763-784: LGTM! Proper IME composition handling.

Moving the range map updates before processing ensures the selection state is captured before DOM modifications. This directly addresses the IME focus loss issues mentioned in the PR objectives, particularly the issue where "Loss of editor focus after confirming a Chinese character selection by pressing the space key."


875-922: Excellent cursor position adjustment logic!

The handleSelectionChange listener effectively prevents the cursor from getting stuck at zero-width placeholder characters by automatically adjusting the position. This directly addresses the "cursor jumping" issues mentioned in the PR title, particularly "Incorrect cursor positioning when typing English characters on the right side of the editing block."

The implementation properly:

  • Checks for composition to avoid conflicts with IME
  • Handles both start and end placeholder positions
  • Uses the DOM element references from the enhanced EditorRange

1-14: Good architectural simplification.

The removal of the handleSentinelNodeForwardDeletion function (mentioned in the summary) and replacement with the unified placeholder management and selection change handling represents a cleaner, more maintainable approach to cursor management.

Copy link
Copy Markdown

@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 (2)
packages/components/src/sender/components/TemplateEditor.vue (2)

76-107: Improved placeholder handling with consistent approach.

The rewritten setOriginalData function provides a more systematic approach to handling placeholders, replacing browser-specific logic with consistent behavior. The logic correctly ensures templates don't appear at start/end positions.

Minor optimization suggestion:

Consider caching the regex outside the function to avoid recreating it on each call:

+const PLACEHOLDER_REGEX = new RegExp(PLACEHOLDER, 'g')
+
 const setOriginalData = (items: (TextItem | TemplateItem)[]) => {
   // ...
-  const regex = new RegExp(PLACEHOLDER, 'g')
   if (items.length > 0) {
     if (items[0].content !== PLACEHOLDER) {
-      items[0].content = items[0].content.replace(regex, '')
+      items[0].content = items[0].content.replace(PLACEHOLDER_REGEX, '')
     }
     if (items[items.length - 1].content !== PLACEHOLDER) {
-      items[items.length - 1].content = items[items.length - 1].content.replace(regex, '')
+      items[items.length - 1].content = items[items.length - 1].content.replace(PLACEHOLDER_REGEX, '')
     }
   }
 }

194-220: Smart detection of internal vs external updates.

The enhanced watch function correctly prevents infinite loops by detecting internal updates through data comparison. The selection range preservation before processing external updates is crucial for maintaining cursor position.

Consider a more robust way to detect internal updates that doesn't rely on JSON serialization order:

// Alternative approach using a flag
const isInternalUpdate = ref(false)

// Set flag before internal updates
const updateModelValue = () => {
  isInternalUpdate.value = true
  model.value = transformInternalToUser(originalData.value)
  nextTick(() => {
    isInternalUpdate.value = false
  })
}
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 2b97f4a and c5e5912.

📒 Files selected for processing (1)
  • packages/components/src/sender/components/TemplateEditor.vue (17 hunks)
🔇 Additional comments (14)
packages/components/src/sender/components/TemplateEditor.vue (14)

2-2: LGTM! Import additions support new functionality.

The addition of onMounted and onUnmounted imports and the Block import are appropriate for the new selection change handling and component structure.

Also applies to: 14-14


38-41: Good consolidation of zero-width character constants.

Unifying the handling with ZERO_WIDTH_CHAR and creating appropriate aliases improves maintainability and reduces the chance of inconsistencies.


57-62: Proper handling of optional IDs in data transformation.

The fallback to randomId() when item.id is not provided ensures consistent ID handling between user data and internal representations.


71-71: Correct inclusion of ID in user data transformation.

Including the id property in the transformed user data maintains consistency with the updated type definitions.


433-434: Enhanced range tracking with DOM element references.

Adding startEl and endEl properties to the range transformation enables more precise DOM element tracking, which is essential for the improved selection management functionality.

Also applies to: 438-439


403-415: Proper range mapping before input processing.

Adding range mapping before processing input ensures that undo/redo operations can restore the correct cursor position, addressing the data synchronization issues mentioned in the PR objectives.


455-541: Comprehensive input processing with cleanup logic.

The enhanced processInput function properly handles:

  • Template deletion tracking
  • Empty template and text item cleanup
  • Prevention of templates at start/end positions
  • Prefix/suffix restoration
  • History commits and model synchronization

This addresses the cursor jumping issues mentioned in the PR objectives.


767-772: Consistent range mapping in composition handling.

The addition of range mapping in handleCompositionEnd ensures consistent behavior with other input handling functions, addressing IME focus loss issues.


320-321: Proper history management and model synchronization.

Adding history commits and model.value updates in insertNewTextAndSetCaretPosition ensures that undo/redo operations work correctly and the external model stays synchronized with internal state.

Also applies to: 326-327, 336-336


875-913: Essential cursor position adjustment for placeholder handling.

The handleSelectionChange function directly addresses the cursor jumping issues mentioned in the PR objectives by:

  • Detecting cursor position at placeholder text boundaries
  • Adjusting cursor position to prevent being stuck at zero-width characters
  • Properly avoiding interference with IME composition input

This is a key fix for the cursor behavior problems.


915-921: Proper event listener lifecycle management.

The onMounted and onUnmounted hooks correctly register and clean up the selectionchange event listener, following Vue.js best practices for DOM event management.


273-274: Improved comment clarity.

The updated comment better explains the fallback behavior when the first child is not a text node.


847-848: Essential model synchronization after data restoration.

Adding model.value update in restoreDataAndCaretPosition ensures the external model stays synchronized after undo/redo operations, addressing the data synchronization issues mentioned in the PR objectives.


856-867: LGTM! Template activation logic maintained.

The activateFirstField function maintains its core logic while benefiting from improved formatting and structure.

@SonyLeo SonyLeo merged commit c3820d6 into opentiny:develop Jul 31, 2025
@SonyLeo SonyLeo deleted the fix/template-cursor-issues branch August 4, 2025 08:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants