Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions src/app/core/main/editor/markdown/bubble-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import {
ListTodo,
} from 'lucide-react'
import { useState, useCallback, useEffect, useId, useLayoutEffect, useRef } from 'react'
import { TextSelection } from '@tiptap/pm/state'
import { TextSelection, type Transaction } from '@tiptap/pm/state'
import { VIEW_STATE_RESTORE_META } from './selection-intent'
import { cn } from '@/lib/utils'
import { useTranslations } from 'next-intl'
import { toast } from '@/hooks/use-toast'
Expand Down Expand Up @@ -203,7 +204,7 @@ export function BubbleMenu({
}, [customTranslateLang, handleTranslate, t])

// 更新定位
const updatePosition = useCallback(() => {
const updatePosition = useCallback((props?: { transaction?: Transaction }) => {
const { selection } = editor.state
const { from, to } = selection

Expand All @@ -218,8 +219,11 @@ export function BubbleMenu({
}

// 应用启动或文件恢复时可能会还原一个非空选区,但这不是用户本次主动选择的文本。
// 只折叠带有恢复标记的选区;辅助功能工具(VoiceOver、写作助手)通过
// Accessibility API 创建的选区必须保留,否则它们无法继续操作。
if (!hasUserSelectionIntentRef.current) {
if (hasTextSelection(editor)) {
const isRestoredSelection = props?.transaction?.getMeta(VIEW_STATE_RESTORE_META) === true
if (isRestoredSelection && hasTextSelection(editor)) {
collapseSelection()
}
hideMenu()
Expand Down
8 changes: 8 additions & 0 deletions src/app/core/main/editor/markdown/selection-intent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Transaction meta key set by the editor when it re-applies a selection that
* was saved with the view state (app start, file reopen). The bubble menu
* collapses only selections carrying this marker; a selection created by an
* assistive tool through the accessibility API (VoiceOver, writing
* assistants) must survive so the tool can act on it.
*/
export const VIEW_STATE_RESTORE_META = 'noteGenViewStateRestore'
13 changes: 10 additions & 3 deletions src/app/core/main/editor/markdown/tiptap-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { SearchAndReplace } from '@sereneinserenade/tiptap-search-and-replace'
import { Extension, nodeInputRule, type Editor as CoreEditor, type JSONContent } from '@tiptap/core'
import { Fragment, Slice, type Node as ProseMirrorNode, type NodeType } from '@tiptap/pm/model'
import { AllSelection, EditorState, Plugin, PluginKey, TextSelection, type Selection } from '@tiptap/pm/state'
import { VIEW_STATE_RESTORE_META } from './selection-intent'
import { redoDepth, undoDepth } from '@tiptap/pm/history'
import { Decoration, DecorationSet, type EditorView } from '@tiptap/pm/view'
import { dropPoint } from '@tiptap/pm/transform'
Expand Down Expand Up @@ -3290,16 +3291,22 @@ export function TipTapEditor({
return
}

// The marker lets the bubble menu tell a restored selection from one
// made by the user or an assistive tool; only the former is collapsed.
const markViewStateRestore = ({ tr }: { tr: { setMeta: (key: string, value: unknown) => unknown } }) => {
tr.setMeta(VIEW_STATE_RESTORE_META, true)
return true
}
if (isMobile) {
editor.commands.setTextSelection({
editor.chain().setTextSelection({
from: selectionFrom,
to: selectionTo,
})
}).command(markViewStateRestore).run()
} else {
editor.chain().focus().setTextSelection({
from: selectionFrom,
to: selectionTo,
}).run()
}).command(markViewStateRestore).run()
}

requestAnimationFrame(() => {
Expand Down