fix(editor): invalid caret in note-edgeless-block on focus (#14229)

### Problem
●In edgeless mode, when starting to edit, `note-block` exhibits two
types of invalid caret behavior:
(1)**Title Region Misalignment**: Clicking on the title region
incorrectly generates the caret in the first line of the note content,
rather than in the title itself.
(2)**Vanishing Caret at Line End**: When clicking in the empty space
beyond the end of a text section, the caret appears momentarily at the
line's end but disappears immediately.
●The following video demonstrates these issues:


https://github.com/user-attachments/assets/db9c2c50-709f-4d32-912c-0f01841d2024


### Solution
●**Title Click Interception**: Added a check to determine if the click
coordinates fall in the title region. If so, the caret positioning is
now handled by a dedicated logic path. Otherwise, it falls back to the
existing note-content logic as before.
●**Range Normalization**: When the generated `range.startContainer` is
not a `TextNode`, try to find a most appropriate `TextNode` and update
the `range` accordingly.

### After
●The video below shows the behavior after this fix.


https://github.com/user-attachments/assets/b2f70b64-1fc6-4049-8379-8bcf3a488a05



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Clicking a page block title no longer creates unwanted paragraphs and
reliably focuses the title.
* Paragraph creation now occurs only when needed and focus is applied
only after successful creation.
* Click coordinates are clamped to container bounds to prevent misplaced
cursors or focus.

* **Improvements**
* Caret normalization: clicks place the caret at the last meaningful
text position for consistent single-cursor behavior.

* **Tests**
  * Added end-to-end coverage for caret placement and focus transitions.
* New ratio-based click/double-click test utilities and a helper for
double-clicking note bodies.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
congzhou09
2026-03-02 18:51:23 +08:00
committed by GitHub
parent 5464d1a9ce
commit 478138493a
8 changed files with 305 additions and 5 deletions
@@ -88,11 +88,73 @@ export function getCurrentNativeRange(selection = window.getSelection()) {
return selection.getRangeAt(0);
}
// functions need to be mocked in unit-test
export const api = {
caretRangeFromPoint,
resetNativeSelection,
};
export function handleNativeRangeAtPoint(x: number, y: number) {
const range = caretRangeFromPoint(x, y);
const range = api.caretRangeFromPoint(x, y);
if (range) {
normalizeCaretRange(range);
}
const startContainer = range?.startContainer;
// click on rich text
if (startContainer instanceof Node) {
resetNativeSelection(range);
api.resetNativeSelection(range);
}
}
function lastMeaningfulTextNode(node: Node) {
const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, {
acceptNode(node) {
return node.textContent && node.textContent?.trim().length > 0
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_REJECT;
},
});
let last = null;
while (walker.nextNode()) {
last = walker.currentNode;
}
return last;
}
function normalizeCaretRange(range: Range) {
let { startContainer, startOffset } = range;
if (startContainer.nodeType === Node.TEXT_NODE) return;
// Try to find text in the element at `startOffset`
const offsetEl =
startOffset > 0
? startContainer.childNodes[startOffset - 1]
: startContainer.childNodes[0];
if (offsetEl) {
if (offsetEl.nodeType === Node.TEXT_NODE) {
range.setStart(
offsetEl,
startOffset > 0 ? (offsetEl.textContent?.length ?? 0) : 0
);
range.collapse(true);
return;
}
const text = lastMeaningfulTextNode(offsetEl);
if (text) {
range.setStart(text, text.textContent?.length ?? 0);
range.collapse(true);
return;
}
}
// Fallback, try to find text in startContainer
const text = lastMeaningfulTextNode(startContainer);
if (text) {
range.setStart(text, text.textContent?.length ?? 0);
range.collapse(true);
return;
}
}