fix: reset editor selection before note swaps

This commit is contained in:
lucaronin
2026-04-28 21:31:54 +02:00
parent 595c48ca59
commit 6b96b5caf6
4 changed files with 65 additions and 5 deletions

View File

@@ -68,6 +68,10 @@ export function isUntitledPath(path: FilePath): boolean {
return pathStem(path).startsWith('untitled-')
}
export function blankParagraphBlocks(): unknown[] {
return [{ type: 'paragraph', content: [], children: [] }]
}
function isParsedBlock(value: unknown): value is ParsedBlock {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}

View File

@@ -0,0 +1,31 @@
type TiptapEditorBridge = {
state?: {
doc?: { content?: { size?: unknown } }
}
commands?: {
setTextSelection?: (position: number) => unknown
}
}
function getTiptapEditorBridge(editor: unknown): TiptapEditorBridge | null {
const editorWithBridge = editor as { _tiptapEditor?: TiptapEditorBridge }
return editorWithBridge._tiptapEditor ?? null
}
function getSafeTextSelectionPosition(tiptapEditor: TiptapEditorBridge): number {
const size = tiptapEditor.state?.doc?.content?.size
if (typeof size !== 'number' || !Number.isFinite(size)) return 0
return size > 0 ? Math.min(1, size) : 0
}
export function resetTextSelectionBeforeContentSwap(editor: unknown): void {
const tiptapEditor = getTiptapEditorBridge(editor)
const setTextSelection = tiptapEditor?.commands?.setTextSelection
if (!tiptapEditor || typeof setTextSelection !== 'function') return
try {
setTextSelection(getSafeTextSelectionPosition(tiptapEditor))
} catch (err) {
console.warn('Failed to reset editor selection before content swap:', err)
}
}

View File

@@ -229,7 +229,13 @@ function makeMockEditor(docRef: { current: unknown[] }) {
blocksToMarkdownLossy: vi.fn(() => ''),
blocksToHTMLLossy: vi.fn(() => ''),
tryParseMarkdownToBlocks: vi.fn(() => blocksA),
_tiptapEditor: { commands: { setContent: vi.fn() } },
_tiptapEditor: {
state: { doc: { content: { size: 8 } } },
commands: {
setContent: vi.fn(),
setTextSelection: vi.fn(),
},
},
_docRef: docRef,
}
Object.defineProperty(editor, 'document', { get: () => docRef.current })
@@ -454,6 +460,24 @@ describe('useEditorTabSwap raw mode sync', () => {
}
})
it('resets stale TipTap selection before applying a switched note', async () => {
const tabA = makeTab('a.md', 'Note A')
const tabB = makeTab('b.md', 'Note B')
const { mockEditor, rerenderWith } = await createSwapHarness({
initialProps: { tabs: [tabA], activeTabPath: 'a.md', rawMode: false },
setupEditor: (editor) => {
editor._tiptapEditor.state.doc.content.size = 3
},
})
mockEditor._tiptapEditor.commands.setTextSelection.mockClear()
await rerenderWith({ tabs: [tabB], activeTabPath: 'b.md' })
expect(mockEditor._tiptapEditor.commands.setTextSelection).toHaveBeenCalledWith(1)
expect(mockEditor.replaceBlocks).toHaveBeenCalled()
})
it('re-parses when the active tab content changes without a path change', async () => {
const tabA = makeTab('a.md', 'Note A')
const refreshedTabA = {

View File

@@ -9,6 +9,7 @@ import { failNoteOpenTrace, finishNoteOpenTrace } from '../utils/noteOpenPerform
import { resolveImageUrls, portableImageUrls } from '../utils/vaultImages'
import { repairMalformedEditorBlocks } from './editorBlockRepair'
import {
blankParagraphBlocks,
extractEditorBody,
getH1TextFromBlocks,
isUntitledPath,
@@ -17,6 +18,7 @@ import {
slugifyPathStem,
} from './editorTabContent'
import { clearEditorDomSelection, EDITOR_CONTAINER_SELECTOR } from './editorDomSelection'
import { resetTextSelectionBeforeContentSwap } from './editorTiptapSelection'
export { extractEditorBody, getH1TextFromBlocks, replaceTitleInFrontmatter } from './editorTabContent'
interface Tab {
@@ -120,10 +122,6 @@ function extractBodyRemainderAfterEmptyH1(options: { content: string }): string
return [secondLine, ...rest].join('\n').trimStart()
}
function blankParagraphBlocks(): EditorBlocks {
return [{ type: 'paragraph', content: [], children: [] }]
}
async function parseMarkdownBlocks(
editor: ReturnType<typeof useCreateBlockNote>,
preprocessed: string,
@@ -194,6 +192,7 @@ function applyBlocksToEditor(
const safeBlocks = repairMalformedEditorBlocks(blocks) as EditorBlocks
suppressChangeRef.current = true
try {
resetTextSelectionBeforeContentSwap(editor)
const current = editor.document
if (current.length > 0 && safeBlocks.length > 0) {
editor.replaceBlocks(current, safeBlocks)
@@ -224,6 +223,7 @@ function applyBlankStateToEditor(
) {
suppressChangeRef.current = true
try {
resetTextSelectionBeforeContentSwap(editor)
editor._tiptapEditor.commands.setContent('<p></p>')
} catch (err) {
console.error('applyBlankStateToEditor failed, falling back to replaceBlocks:', err)
@@ -245,6 +245,7 @@ function applyHtmlStateToEditor(
) {
suppressChangeRef.current = true
try {
resetTextSelectionBeforeContentSwap(editor)
editor._tiptapEditor.commands.setContent(html)
} catch (err) {
console.error('applyHtmlStateToEditor failed:', err)