release: v0.4.0 — MetonaEditor 集成与架构重构
feat(editor): 将 Milkdown/ProseMirror 替换为 @metona-team/metona-editor v0.1.3 - 三模式视图 (edit/split/preview) 由编辑器内置工具栏切换 - searchReplace + imagePaste 预设插件 - unified/rehype 渲染管线通过 render 钩子集成 - 主题双向同步 (应用暗色模式 ↔ 编辑器主题) feat(toast): metona-toast 迁移为 @metona-team/metona-toast v2.0.1 refactor: 删除冗余组件与代码 - 移除 SourceEditor、Preview、SearchReplace、EditorToolbar、useMilkdown - 移除 StatusBar 组件及状态栏扩展架构(编辑器内置底栏替代) - 移除 useSettings、useStatusBarItem、useStatusBarItems、statusBarStore - 移除 EditMode/PreviewMode/SourceMode 图标 refactor(ui): 简化布局 - 工具栏移除模式切换按钮,新增自动保存开关 - useKeyboard 移除 Ctrl+1/2/3/B/I 快捷键 - Editor 三模式统一由 MetonaEditor 容器渲染 chore: 版本号 v0.3.13 → v0.4.0 docs: 全面更新 README/DESIGN/CONTRIBUTING/DEVSETUP
This commit is contained in:
@@ -11,11 +11,14 @@ import { useAutoExpandDir } from '../../hooks/useAutoExpandDir'
|
||||
import { useActiveHeading } from '../../hooks/useActiveHeading'
|
||||
import { OutlinePanel, parseHeadings } from '../OutlinePanel'
|
||||
import type { Heading } from '../OutlinePanel'
|
||||
import { getEditorView, useEditorStore } from '../../stores/editorStore'
|
||||
import { TextSelection } from '@milkdown/prose/state'
|
||||
import { getMetonaEditor, useEditorStore } from '../../stores/editorStore'
|
||||
|
||||
const norm = (p: string) => p.replace(/[/\\]+$/, '').replace(/\\/g, '/')
|
||||
|
||||
function escapeRegex(s: string): string {
|
||||
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
|
||||
export const Sidebar = React.memo(function Sidebar() {
|
||||
const tabs = useTabStore(s => s.tabs)
|
||||
const activeTabId = useTabStore(s => s.activeTabId)
|
||||
@@ -43,12 +46,11 @@ export const Sidebar = React.memo(function Sidebar() {
|
||||
return parseHeadings(activeTab.content)
|
||||
}, [activeTab?.content])
|
||||
|
||||
// D4: 追踪预览面板中的活跃标题
|
||||
// D4: 追踪预览面板中的活跃标题(适配 MetonaEditor 的 .me-preview)
|
||||
const previewRef = useRef<HTMLElement | null>(null)
|
||||
useEffect(() => {
|
||||
// 仅在预览模式时获取 preview DOM 节点
|
||||
if (viewMode === 'preview') {
|
||||
previewRef.current = document.getElementById('preview')
|
||||
previewRef.current = document.querySelector('.me-preview') as HTMLElement | null
|
||||
} else {
|
||||
previewRef.current = null
|
||||
}
|
||||
@@ -59,37 +61,40 @@ export const Sidebar = React.memo(function Sidebar() {
|
||||
headings
|
||||
)
|
||||
|
||||
// Navigate to heading in ProseMirror document tree
|
||||
const handleHeadingNavigate = useCallback((heading: Heading, index: number) => {
|
||||
const view = getEditorView()
|
||||
if (!view) return
|
||||
// Navigate to heading in MetonaEditor
|
||||
const handleHeadingNavigate = useCallback((heading: Heading) => {
|
||||
const editor = getMetonaEditor()
|
||||
if (!editor) return
|
||||
|
||||
try {
|
||||
let foundPos: number | null = null
|
||||
let currentIndex = 0
|
||||
// 获取当前内容,查找标题文本在源代码中的位置
|
||||
const content = editor.getValue()
|
||||
const headingPattern = new RegExp(
|
||||
`^#{1,6}\\s+${escapeRegex(heading.text)}\\s*$`,
|
||||
'm'
|
||||
)
|
||||
const match = headingPattern.exec(content)
|
||||
if (!match) return
|
||||
|
||||
view.state.doc.descendants((node, pos) => {
|
||||
if (foundPos !== null) return false
|
||||
if (node.type.name === 'heading' && node.attrs.level === heading.level) {
|
||||
if (node.textContent.trim() === heading.text) {
|
||||
if (currentIndex === index) {
|
||||
foundPos = pos
|
||||
return false
|
||||
}
|
||||
currentIndex++
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
const pos = match.index
|
||||
|
||||
if (foundPos === null) return
|
||||
// 通过 DOM 操作滚动 textarea 到对应位置
|
||||
const container = document.querySelector('.metona-editor-wrapper') as HTMLElement | null
|
||||
if (!container) return
|
||||
|
||||
const tr = view.state.tr
|
||||
.setSelection(TextSelection.create(view.state.doc, foundPos, foundPos))
|
||||
.scrollIntoView()
|
||||
view.dispatch(tr)
|
||||
view.focus()
|
||||
const textarea = container.querySelector('textarea')
|
||||
if (!textarea) return
|
||||
|
||||
// 估算滚动位置(简单方法:按行数比例)
|
||||
const linesBefore = content.substring(0, pos).split('\n').length
|
||||
const lineHeight = 24 // 估算行高
|
||||
textarea.scrollTop = linesBefore * lineHeight
|
||||
|
||||
// 设置光标位置
|
||||
textarea.focus()
|
||||
textarea.setSelectionRange(pos, pos)
|
||||
} catch {
|
||||
// Position may be invalid if content has changed
|
||||
// 导航失败,静默忽略
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user