fix: reset editor selection before note swaps
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
31
src/hooks/editorTiptapSelection.ts
Normal file
31
src/hooks/editorTiptapSelection.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user