fix(editor): 新建文档自动聚焦 + 空文档占位 + 编辑面填满(docs/23)
新建/打开/恢复的标签就绪后自动把光标送进编辑器(弹窗/查找栏打开时 不抢);切回标签恢复锚点时一并聚焦。空文档首段显示「开始输入…」 占位。编辑面改为撑满窗格高度,下方空白处可点击落光标;host 显式 width:100% 防 flex 收缩窄缝。
This commit is contained in:
41
src/App.tsx
41
src/App.tsx
@@ -124,6 +124,15 @@ export default function App() {
|
||||
const editorRef = useRef<Editor | null>(null)
|
||||
const jumpRef = useRef<(text: string) => void>(() => undefined)
|
||||
const anchorApiRef = useRef<{ getAnchor: () => CursorAnchor | null; setCursorByAnchor: (a: CursorAnchor) => void } | null>(null)
|
||||
// 刚新建/刚打开的标签:其编辑器就绪后应自动获得光标(新建文档进来没有光标是缺陷)。
|
||||
const freshFocusRef = useRef<Set<string>>(new Set())
|
||||
const findOpenRef = useRef(false)
|
||||
/** 把键盘焦点送进编辑器;弹窗或查找栏打开时不抢焦点 */
|
||||
const maybeFocusEditor = (editor: Editor | null | undefined) => {
|
||||
if (!editor) return
|
||||
if (anyModalRef.current || findOpenRef.current) return
|
||||
focusEditor(editor)
|
||||
}
|
||||
const sourceApiRef = useRef<SourceEditorApi | null>(null)
|
||||
const sourceModeRef = useRef(sourceMode)
|
||||
sourceModeRef.current = sourceMode
|
||||
@@ -256,6 +265,7 @@ export default function App() {
|
||||
encoding: file.encoding, lineEnding: file.lineEnding, hadBom: file.hadBom,
|
||||
})
|
||||
const existing = stateRef.current.tabs.find((t) => samePath(t.path, path))
|
||||
if (!existing) freshFocusRef.current.add(created.id)
|
||||
dispatch({ type: 'open', tab: created })
|
||||
const stat = await statFile(path)
|
||||
if (stat.exists) baselinesRef.current.set(existing?.id ?? created.id, { mtimeMs: stat.mtimeMs, size: stat.size })
|
||||
@@ -264,7 +274,9 @@ export default function App() {
|
||||
}, [pushRecent])
|
||||
|
||||
const newDoc = useCallback(() => {
|
||||
dispatch({ type: 'open', tab: createTab({ title: '未命名', content: '' }) })
|
||||
const created = createTab({ title: '未命名', content: '' })
|
||||
freshFocusRef.current.add(created.id)
|
||||
dispatch({ type: 'open', tab: created })
|
||||
setSourceMode(false)
|
||||
}, [])
|
||||
|
||||
@@ -275,7 +287,9 @@ export default function App() {
|
||||
} else {
|
||||
const picked = await pickAndReadWebFile()
|
||||
if (picked) {
|
||||
dispatch({ type: 'open', tab: createTab({ title: picked.name.replace(/\.(md|markdown|txt)$/i, ''), content: picked.content }) })
|
||||
const created = createTab({ title: picked.name.replace(/\.(md|markdown|txt)$/i, ''), content: picked.content })
|
||||
freshFocusRef.current.add(created.id)
|
||||
dispatch({ type: 'open', tab: created })
|
||||
}
|
||||
}
|
||||
}, [tauriMode, openPath])
|
||||
@@ -447,6 +461,7 @@ export default function App() {
|
||||
encoding: file.encoding, lineEnding: file.lineEnding, hadBom: file.hadBom,
|
||||
})
|
||||
const existing = stateRef.current.tabs.find((t) => samePath(t.path, item.path))
|
||||
if (!existing) freshFocusRef.current.add(created.id)
|
||||
dispatch({ type: 'open', tab: created })
|
||||
const targetId = existing?.id ?? created.id
|
||||
dispatch({ type: 'updateContent', id: targetId, content: item.content })
|
||||
@@ -457,6 +472,7 @@ export default function App() {
|
||||
} catch { /* 文件已不存在时按未命名草稿恢复 */ }
|
||||
}
|
||||
const created = createTab({ title: item.title, content: '' })
|
||||
freshFocusRef.current.add(created.id)
|
||||
dispatch({ type: 'open', tab: created })
|
||||
dispatch({ type: 'updateContent', id: created.id, content: item.content })
|
||||
finish()
|
||||
@@ -834,9 +850,13 @@ export default function App() {
|
||||
editorRef.current = api?.editor ?? null
|
||||
jumpRef.current = api?.jumpToHeading ?? (() => undefined)
|
||||
anchorApiRef.current = api ? { getAnchor: api.getAnchor, setCursorByAnchor: api.setCursorByAnchor } : null
|
||||
if (api && pendingAnchorRef.current) {
|
||||
api.setCursorByAnchor(pendingAnchorRef.current)
|
||||
pendingAnchorRef.current = null
|
||||
if (api) {
|
||||
const hadAnchor = Boolean(pendingAnchorRef.current)
|
||||
if (hadAnchor) {
|
||||
api.setCursorByAnchor(pendingAnchorRef.current as CursorAnchor)
|
||||
pendingAnchorRef.current = null
|
||||
}
|
||||
if (hadAnchor || (activeIdForAlive && freshFocusRef.current.delete(activeIdForAlive))) maybeFocusEditor(api.editor)
|
||||
}
|
||||
}, [activeIdForAlive])
|
||||
|
||||
@@ -1071,6 +1091,7 @@ export default function App() {
|
||||
modal || appearanceOpen || exportOpen || pendingClose || conflict
|
||||
|| promptSpec || confirmSpec || quitOpen || (recoveryOffer.length > 0 && !recoveryDismissed),
|
||||
)
|
||||
findOpenRef.current = findUi.open
|
||||
|
||||
// 大纲与状态栏计数只随正文变化重算(原先每次光标移动/击键都对全文重算);
|
||||
// 大纲再经 useDeferredValue 让高频输入时延后提取,不堵击键路径。
|
||||
@@ -1298,9 +1319,13 @@ export default function App() {
|
||||
editorRef.current = api.editor
|
||||
jumpRef.current = api.jumpToHeading
|
||||
anchorApiRef.current = api.editor ? { getAnchor: api.getAnchor, setCursorByAnchor: api.setCursorByAnchor } : null
|
||||
if (api.editor && pendingAnchorRef.current) {
|
||||
api.setCursorByAnchor(pendingAnchorRef.current)
|
||||
pendingAnchorRef.current = null
|
||||
if (api.editor) {
|
||||
const hadAnchor = Boolean(pendingAnchorRef.current)
|
||||
if (hadAnchor) {
|
||||
api.setCursorByAnchor(pendingAnchorRef.current as CursorAnchor)
|
||||
pendingAnchorRef.current = null
|
||||
}
|
||||
if (hadAnchor || freshFocusRef.current.delete(t.id)) maybeFocusEditor(api.editor)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -12,7 +12,7 @@ import { clipboard } from '@milkdown/kit/plugin/clipboard'
|
||||
import { indent } from '@milkdown/kit/plugin/indent'
|
||||
import { math } from '@milkdown/plugin-math'
|
||||
import { upload, uploadConfig, type UploadOptions } from '@milkdown/kit/plugin/upload'
|
||||
import { createSyntaxRevealPlugin, createCodeBlockTabPlugin, createTableTabPlugin, createFocusDimPlugin, createPlainTextPastePlugin, createImageDisplayPlugin, selectionInTable } from './plugins'
|
||||
import { createSyntaxRevealPlugin, createCodeBlockTabPlugin, createTableTabPlugin, createFocusDimPlugin, createPlainTextPastePlugin, createImageDisplayPlugin, createEmptyPlaceholderPlugin, selectionInTable } from './plugins'
|
||||
import { createListItemViewPlugin, createCodeBlockViewPlugin } from './nodeViews'
|
||||
|
||||
export interface CursorInfo { line: number; col: number }
|
||||
@@ -90,6 +90,7 @@ export function createMarkdownEditor(root: HTMLElement, initialMarkdown: string,
|
||||
.use(createCodeBlockTabPlugin())
|
||||
.use(createTableTabPlugin())
|
||||
.use(createFocusDimPlugin())
|
||||
.use(createEmptyPlaceholderPlugin())
|
||||
.use(createListItemViewPlugin())
|
||||
.use(createCodeBlockViewPlugin())
|
||||
.use(math)
|
||||
|
||||
@@ -210,6 +210,24 @@ export function isFocusModeEnabled() {
|
||||
}
|
||||
|
||||
const focusKey = new PluginKey('new-markdown-focus')
|
||||
const emptyPlaceholderKey = new PluginKey('new-markdown-empty-placeholder')
|
||||
|
||||
/** 空文档占位:整篇只有一段空段落时给它挂占位类,CSS ::before 显示「开始输入…」,
|
||||
* 让新建文档有一个明确的输入界面(否则空页里连光标落点都看不见)。 */
|
||||
export function createEmptyPlaceholderPlugin() {
|
||||
return $prose(() => new Plugin({
|
||||
key: emptyPlaceholderKey,
|
||||
props: {
|
||||
decorations: (state) => {
|
||||
const { doc } = state
|
||||
if (doc.childCount !== 1) return DecorationSet.empty
|
||||
const first = doc.firstChild
|
||||
if (!first || first.type.name !== 'paragraph' || first.content.size !== 0) return DecorationSet.empty
|
||||
return DecorationSet.create(doc, [Decoration.node(0, first.nodeSize, { class: 'doc-placeholder' })])
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
interface FocusState {
|
||||
set: DecorationSet
|
||||
|
||||
@@ -182,7 +182,15 @@ kbd { background: var(--code-bg); border: 1px solid var(--border); border-radius
|
||||
.milkdown .ProseMirror li.task-item { list-style: none; display: flex; align-items: flex-start; gap: 2px; margin-left: -1.1em; }
|
||||
.milkdown .ProseMirror .task-checkbox { margin-top: 0.42em; width: 15px; height: 15px; accent-color: var(--accent); cursor: pointer; flex: none; }
|
||||
.milkdown .ProseMirror li[data-checked='true'] .task-content { color: var(--text-dim); }
|
||||
.editor-pane-wrap { position: relative; min-height: 100%; }
|
||||
.editor-pane-wrap { position: relative; min-height: 100%; display: flex; flex-direction: column; align-items: center; }
|
||||
/* 编辑面填满窗格高度:空文档/短文档时下方大片区域也是可点击的输入面,
|
||||
点任意处光标都会落进文档(原先只有内容实际占的那一条能点)。
|
||||
host 必须显式 width:100%:flex 列里 margin:auto 会让它按内容收缩,
|
||||
空文档时整列会缩成一条窄缝(实测踩过)。 */
|
||||
.editor-host { flex: 1; width: 100%; display: flex; flex-direction: column; }
|
||||
.editor-host .milkdown { flex: 1; display: flex; flex-direction: column; }
|
||||
.milkdown .ProseMirror { flex: 1; }
|
||||
.milkdown .ProseMirror .doc-placeholder::before { content: '开始输入…'; float: left; height: 0; pointer-events: none; color: color-mix(in srgb, var(--doc-ink, #24292f) 38%, transparent); }
|
||||
.table-toolbar { position: absolute; z-index: 30; display: flex; gap: 2px; align-items: center; background: var(--bg-editor); border: 1px solid var(--border); border-radius: 8px; padding: 3px; box-shadow: 0 6px 18px rgba(0,0,0,0.12); }
|
||||
.table-toolbar button { border: 0; background: transparent; padding: 4px 8px; border-radius: 6px; cursor: pointer; font-size: 12.5px; white-space: nowrap; }
|
||||
.table-toolbar button:hover { background: var(--accent-weak); }
|
||||
|
||||
Reference in New Issue
Block a user