98 lines
3.4 KiB
TypeScript
98 lines
3.4 KiB
TypeScript
import React, { useCallback, useRef } from 'react'
|
|
import { useTabStore } from '../../stores/tabStore'
|
|
|
|
interface SourceEditorProps {
|
|
darkMode: boolean
|
|
}
|
|
|
|
/**
|
|
* 源码编辑模式 — 使用原生 textarea 编辑原始 Markdown 文本。
|
|
* 内容实时同步到 tabStore,与 WYSIWYG 编辑器共享同一数据源。
|
|
*/
|
|
export const SourceEditor = React.memo(function SourceEditor({ darkMode }: SourceEditorProps) {
|
|
const activeTab = useTabStore(s => s.getActiveTab())
|
|
const activeTabId = useTabStore(s => s.activeTabId)
|
|
const updateTabContent = useTabStore(s => s.updateTabContent)
|
|
const setModified = useTabStore(s => s.setModified)
|
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
|
|
|
const handleChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
if (!activeTabId) return
|
|
updateTabContent(activeTabId, e.target.value)
|
|
setModified(activeTabId, true)
|
|
}, [activeTabId, updateTabContent, setModified])
|
|
|
|
const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
const isCtrl = e.ctrlKey || e.metaKey
|
|
|
|
// Tab 插入两个空格而非跳转焦点
|
|
if (e.key === 'Tab') {
|
|
e.preventDefault()
|
|
const textarea = textareaRef.current
|
|
if (!textarea) return
|
|
const start = textarea.selectionStart
|
|
const end = textarea.selectionEnd
|
|
const value = textarea.value
|
|
const newValue = value.substring(0, start) + ' ' + value.substring(end)
|
|
textarea.value = newValue
|
|
textarea.selectionStart = textarea.selectionEnd = start + 2
|
|
if (activeTabId) {
|
|
updateTabContent(activeTabId, newValue)
|
|
setModified(activeTabId, true)
|
|
}
|
|
}
|
|
|
|
// Ctrl+B: 粗体
|
|
if (isCtrl && e.key === 'b') {
|
|
e.preventDefault()
|
|
const textarea = textareaRef.current
|
|
if (!textarea || !activeTabId) return
|
|
const start = textarea.selectionStart
|
|
const end = textarea.selectionEnd
|
|
const value = textarea.value
|
|
const selected = value.substring(start, end)
|
|
const newValue = value.substring(0, start) + '**' + selected + '**' + value.substring(end)
|
|
textarea.value = newValue
|
|
textarea.selectionStart = start + 2
|
|
textarea.selectionEnd = end + 2
|
|
updateTabContent(activeTabId, newValue)
|
|
setModified(activeTabId, true)
|
|
}
|
|
|
|
// Ctrl+I: 斜体
|
|
if (isCtrl && e.key === 'i') {
|
|
e.preventDefault()
|
|
const textarea = textareaRef.current
|
|
if (!textarea || !activeTabId) return
|
|
const start = textarea.selectionStart
|
|
const end = textarea.selectionEnd
|
|
const value = textarea.value
|
|
const selected = value.substring(start, end)
|
|
const newValue = value.substring(0, start) + '*' + selected + '*' + value.substring(end)
|
|
textarea.value = newValue
|
|
textarea.selectionStart = start + 1
|
|
textarea.selectionEnd = end + 1
|
|
updateTabContent(activeTabId, newValue)
|
|
setModified(activeTabId, true)
|
|
}
|
|
}, [activeTabId, updateTabContent, setModified])
|
|
|
|
return (
|
|
<div className={`source-editor-wrapper${darkMode ? ' source-editor-dark' : ''}`}>
|
|
<textarea
|
|
ref={textareaRef}
|
|
className="source-editor-textarea"
|
|
value={activeTab?.content ?? ''}
|
|
onChange={handleChange}
|
|
onKeyDown={handleKeyDown}
|
|
spellCheck={false}
|
|
wrap="off"
|
|
aria-label="Markdown 源码编辑器"
|
|
placeholder="在此输入 Markdown 内容..."
|
|
/>
|
|
</div>
|
|
)
|
|
})
|
|
|
|
SourceEditor.displayName = 'SourceEditor'
|