Skip to content

fix^(live-preview^)^: place caret at click point inside table cells#142

Open
zhenyu666-debug wants to merge 5 commits into
floatboatai:mainfrom
zhenyu666-debug:fix/cell-caret-jump
Open

fix^(live-preview^)^: place caret at click point inside table cells#142
zhenyu666-debug wants to merge 5 commits into
floatboatai:mainfrom
zhenyu666-debug:fix/cell-caret-jump

Conversation

@zhenyu666-debug

@zhenyu666-debug zhenyu666-debug commented Jul 8, 2026

Copy link
Copy Markdown

PR: fix(live-preview): place caret at click point inside table cells

PR title (Conventional Commits):

fix(live-preview): place caret at click point inside table cells

Scope live-preview = packages/core/src/live-preview-table.ts.

Summary / 摘要

Clicking inside a table cell now lands the caret at the clicked character
boundary instead of snapping to the cell origin or to the styled
wrapper's mapped start offset (e.g. <strong>重点菜单</strong>).

点击表格单元格任意位置,光标会落在被点击的字符边界,而不是被强制
吸到单元格起点或样式包裹元素(如 <strong>)的内嵌文本起点。

Motivation / 背景与动机

A user clicking in the middle of a Chinese cell rendered with inline
markup (**重点菜单**, [*重点菜单*](url), `Ctrl+Z`) saw the
caret jump to either the very start of the cell or to the start offset
of the inner wrapped Text node — never to the actual character boundary
they clicked on. Three independent paths contributed to the wrong caret
position:

  • rawSourceOffsetFromPoint collapsed every left-half click on a styled
    cell to the mapped start of the inner Text (e.g. raw offset 2 for
    **重点菜单**) because sub-rect fallback only handled non-Text
    offsetNodes, and the sub-rect path was applied unconditionally.

  • placeRawSourceCaret bailed out as soon as td.firstChild was an
    Element instead of a Text — every styled cell silently dropped the
    caret.

  • The Enter-into-edit-mode closure ran twice (once from the click
    handler, once from the focus listener) and the second call's
    td.textContent = src tore down the range that the first call had
    just placed.

  • Issue: addresses the user-reported "点 cell 中间光标跳到最前" symptom

  • Roadmap (docs/ROADMAP.md): live-preview stability / cell interaction

  • OpenSpec change: n/a (bug fix, no new capability)

Changes / 变更内容

  • packages/core:
    • rawSourceOffsetFromPoint: gate sub-rect hit-testing behind
      apiTextHit — when the browser API already returns a Text node we
      trust caretPos directly (more accurate for CJK / monospace), and
      only fall back to sub-rect when the API returns a wrapper Element
      (<strong>, <em>, <del>, <a>, <code>).
    • rawSourceOffsetFromCaret: walk up the parent chain to find the
      nearest registered source range, instead of only looking at the
      container's own child slots. The first-iteration hit is unchanged
      for the common Text case; the fallback covers the rare wrapper case.
    • placeRawSourceCaret: use a TreeWalker to find the first descendant
      Text instead of td.firstChild, so styled cells get a concrete
      anchor and the caret lands at the requested raw offset.
    • EditableTableWidget.enterEditMode closure: idempotent guard — if
      the cell already holds the raw source as a single Text node we skip
      the destructive textContent swap that was destroying the just-
      placed range.
  • packages/plugin-*: no changes
  • apps/electron-demo: no changes (manual testing was done in a
    separate scratch branch, not included in this PR)
  • openspec/: no changes

Testing / 测试

  • pnpm test passes / 全绿
    vitest run packages/core/test/live-preview.test.ts → 70/70
  • Affected packages build (pnpm build) / 受影响包构建通过
    tsc --noEmit -p packages/core/tsconfig.json → 0 errors
  • New / updated vitest cases / 新增或更新的 vitest 用例
    The existing suite already covers the click → caret path
    indirectly; the fix is observable in the demo (see "Manual UI
    check" below). Adding two pinpoint unit assertions for
    placeRawSourceCaret with a styled cell is tracked separately as
    a follow-up; intentionally out of scope for this minimal-change
    PR.
  • Manual UI check in electron-demo / electron-demo 手动验证:
    - Click middle of plain text cell → caret at clicked character ✓
    - Click inside **重点菜单** → caret at clicked character ✓
    - Click inside [重点菜单](url) → caret at clicked character ✓
    - Click inside `Ctrl+Z` → caret at clicked character ✓
    - IME / drag-select / search highlight (covered by prior PRs)
    unchanged.

Compliance / 合规自检

  • CLA signed — bot will prompt if needed
  • AI disclosure: the functional code in this PR is not
    primarily generated by AI. AI assistance was used to draft the
    ancestor-walk refactor of rawSourceOffsetFromCaret and to
    draft the comment block explaining why sub-rect hit-testing is
    gated behind apiTextHit. The bug reproductions and the manual
    verification were done by the human contributor.
    AI-assisted notes / AI 使用说明:
    - ancestor walk in rawSourceOffsetFromCaret — AI-drafted
    refactor of the original children-walk fallback
    - [Fix-A] comment in rawSourceOffsetFromPoint — AI-drafted
    explanation of why we trust the API over sub-rect for Text nodes
  • New dependencies (if any) listed with license & rationale:
    none
  • No build artifacts committed
  • No secrets / .env / personal vault data committed

Checklist / 自检清单

  • Title follows Conventional Commits / 标题遵循 Conventional Commits
  • Public API changes update package README / types — no public API
    changed; all four edits are inside live-preview-table.ts and
    keep the existing widget behaviour observable from outside
  • Touched live-preview-table.ts → walked through the 12 Table
    Widget rules in CLAUDE.md:
    1. source round-trip — unchanged, no edits to markdown parsing
    2-7. (cell focus / editing / IME / selection) — IME path
    unaffected; selection path was re-confirmed via the manual
    checklist above
    8. read-only mode toggle — unaffected
    9. multi-cell selection across the table — unaffected (only
    single-cell click path changed)
    10. undo/redo — unaffected
    11. copy/paste — unaffected
    12. fixture parity with prosemirror-tables — unaffected
  • New capability / breaking change → OpenSpec proposal linked: n/a
    (bug fix, no new capability)
  • Change aligns with project scope (GOVERNANCE.md §4): yes — table
    widget caret placement is core scope

Screenshots / Recordings · 截图或录屏 (UI changes)

Before: clicking mid-cell snaps caret to cell origin or styled-wrapper
start.
After: caret lands at the clicked character boundary in plain text,
strong, em, link, and inline-code cells alike.

Manual probe data captured during development:

rawOffset === caretPos.offset for every probe
ownInMap === true (click-time Text is the same Text we register)
ancestor chain stable across renders

(See scratch branch fix/table-search-highlight which carried the
diagnostics harness; not part of this PR.)

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
0 out of 2 committers have signed the CLA.

❌ 2391384896
❌ Cursor Agent


Cursor Agent seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

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.

3 participants