优化超大md文档编辑器
parent
2bb7db0d56
commit
e42a5525f6
|
|
@ -18,7 +18,7 @@ function buildAnchorItems(items, searchKeyword, renderTitle) {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
function TocContent({ items = [], getContainer, searchKeyword = '', renderTitle, onItemClick }) {
|
function TocContent({ items = [], getContainer, searchKeyword = '', renderTitle, onItemClick, onNavigate }) {
|
||||||
const anchorItems = buildAnchorItems(items, searchKeyword, renderTitle)
|
const anchorItems = buildAnchorItems(items, searchKeyword, renderTitle)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -30,6 +30,12 @@ function TocContent({ items = [], getContainer, searchKeyword = '', renderTitle,
|
||||||
getContainer={getContainer}
|
getContainer={getContainer}
|
||||||
items={anchorItems}
|
items={anchorItems}
|
||||||
onClick={(e, link) => {
|
onClick={(e, link) => {
|
||||||
|
// antd Anchor 的 link 为 { href, title } 对象
|
||||||
|
// 虚拟滚动场景下目标标题可能不在 DOM 中,由 onNavigate 接管滚动
|
||||||
|
if (onNavigate) {
|
||||||
|
e.preventDefault()
|
||||||
|
onNavigate(link?.href)
|
||||||
|
}
|
||||||
window.setTimeout(() => onItemClick?.(link), 120)
|
window.setTimeout(() => onItemClick?.(link), 120)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -47,6 +53,7 @@ export function TocDrawer({
|
||||||
getContainer,
|
getContainer,
|
||||||
searchKeyword = '',
|
searchKeyword = '',
|
||||||
renderTitle,
|
renderTitle,
|
||||||
|
onNavigate,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
|
|
@ -63,6 +70,7 @@ export function TocDrawer({
|
||||||
searchKeyword={searchKeyword}
|
searchKeyword={searchKeyword}
|
||||||
renderTitle={renderTitle}
|
renderTitle={renderTitle}
|
||||||
onItemClick={onClose}
|
onItemClick={onClose}
|
||||||
|
onNavigate={onNavigate}
|
||||||
/>
|
/>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
)
|
)
|
||||||
|
|
@ -73,6 +81,7 @@ export default function FloatingToc({
|
||||||
getContainer,
|
getContainer,
|
||||||
searchKeyword = '',
|
searchKeyword = '',
|
||||||
renderTitle,
|
renderTitle,
|
||||||
|
onNavigate,
|
||||||
className = '',
|
className = '',
|
||||||
}) {
|
}) {
|
||||||
const [dismissed, setDismissed] = useState(false)
|
const [dismissed, setDismissed] = useState(false)
|
||||||
|
|
@ -100,6 +109,7 @@ export default function FloatingToc({
|
||||||
searchKeyword={searchKeyword}
|
searchKeyword={searchKeyword}
|
||||||
renderTitle={renderTitle}
|
renderTitle={renderTitle}
|
||||||
onItemClick={() => setDismissed(true)}
|
onItemClick={() => setDismissed(true)}
|
||||||
|
onNavigate={onNavigate}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
/* 外框:与 ByteMD 编辑器一致的边框容器 */
|
||||||
|
.large-markdown-editor {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: var(--bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部提示栏:复用 ByteMD 工具栏的视觉规范 */
|
||||||
|
.large-markdown-editor-header {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 16px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
background-color: var(--toolbar-bg);
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.large-markdown-editor-header-icon {
|
||||||
|
color: var(--link-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 编辑区域 */
|
||||||
|
.large-markdown-editor-body {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.large-markdown-editor-body .CodeMirror {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左右内边距对称 */
|
||||||
|
.large-markdown-editor-body .CodeMirror-lines {
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.large-markdown-editor-body .CodeMirror pre.CodeMirror-line,
|
||||||
|
.large-markdown-editor-body .CodeMirror pre.CodeMirror-line-like {
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.large-markdown-editor-body .CodeMirror-cursor {
|
||||||
|
border-left-color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.large-markdown-editor-body .CodeMirror-placeholder {
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.large-markdown-editor-body .CodeMirror-selected {
|
||||||
|
background: rgba(22, 119, 255, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色模式下覆盖 cm-s-default 的浅色配色,提升对比度 */
|
||||||
|
body.dark .large-markdown-editor-body .CodeMirror-selected {
|
||||||
|
background: rgba(22, 119, 255, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .large-markdown-editor-body .cm-s-default .cm-header {
|
||||||
|
color: #4ea1ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .large-markdown-editor-body .cm-s-default .cm-quote {
|
||||||
|
color: #6cc070;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .large-markdown-editor-body .cm-s-default .cm-link {
|
||||||
|
color: #58a6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .large-markdown-editor-body .cm-s-default .cm-url {
|
||||||
|
color: #d2545b;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .large-markdown-editor-body .cm-s-default .cm-variable-2 {
|
||||||
|
color: #79b8ff;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
import { InfoCircleOutlined } from '@ant-design/icons'
|
||||||
|
import factory from 'codemirror-ssr'
|
||||||
|
import usePlaceholder from 'codemirror-ssr/addon/display/placeholder.js'
|
||||||
|
import useContinuelist from 'codemirror-ssr/addon/edit/continuelist.js'
|
||||||
|
import useOverlay from 'codemirror-ssr/addon/mode/overlay.js'
|
||||||
|
import useGfm from 'codemirror-ssr/mode/gfm/gfm.js'
|
||||||
|
import useMarkdown from 'codemirror-ssr/mode/markdown/markdown.js'
|
||||||
|
import useXml from 'codemirror-ssr/mode/xml/xml.js'
|
||||||
|
import 'codemirror-ssr/lib/codemirror.css'
|
||||||
|
import { LARGE_MARKDOWN_NOTICE } from './LargeMarkdownViewer'
|
||||||
|
import './LargeMarkdownEditor.css'
|
||||||
|
|
||||||
|
// 复用 ByteMD 底层的 codemirror-ssr,为超大文档提供带 Markdown 源码着色的纯文本编辑器。
|
||||||
|
// 不引入新依赖,CodeMirror 5 的视口渲染让超大文档编辑保持流畅。
|
||||||
|
function createCodeMirror() {
|
||||||
|
const codemirror = factory()
|
||||||
|
usePlaceholder(codemirror)
|
||||||
|
useOverlay(codemirror)
|
||||||
|
useXml(codemirror)
|
||||||
|
useMarkdown(codemirror)
|
||||||
|
useGfm(codemirror)
|
||||||
|
useContinuelist(codemirror)
|
||||||
|
return codemirror
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LargeMarkdownEditor({ value = '', onChange, placeholder = '' }) {
|
||||||
|
const containerRef = useRef(null)
|
||||||
|
const editorRef = useRef(null)
|
||||||
|
const onChangeRef = useRef(onChange)
|
||||||
|
|
||||||
|
// 保持 onChange 最新,但不因其变化而重建编辑器
|
||||||
|
useEffect(() => {
|
||||||
|
onChangeRef.current = onChange
|
||||||
|
}, [onChange])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current) return
|
||||||
|
|
||||||
|
const codemirror = createCodeMirror()
|
||||||
|
const editor = codemirror(containerRef.current, {
|
||||||
|
value,
|
||||||
|
mode: 'gfm',
|
||||||
|
lineWrapping: true,
|
||||||
|
lineNumbers: false,
|
||||||
|
tabSize: 2,
|
||||||
|
indentUnit: 2,
|
||||||
|
placeholder,
|
||||||
|
extraKeys: {
|
||||||
|
Enter: 'newlineAndIndentContinueMarkdownList',
|
||||||
|
Tab: 'indentMore',
|
||||||
|
'Shift-Tab': 'indentLess',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
editor.on('change', () => {
|
||||||
|
onChangeRef.current?.(editor.getValue())
|
||||||
|
})
|
||||||
|
|
||||||
|
editorRef.current = editor
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// 卸载时清理 CodeMirror 实例的 DOM
|
||||||
|
const wrapper = editor.getWrapperElement()
|
||||||
|
wrapper?.parentNode?.removeChild(wrapper)
|
||||||
|
editorRef.current = null
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// 外部 value 变化(切换文件、重置)时同步,避免覆盖正在输入的光标位置
|
||||||
|
useEffect(() => {
|
||||||
|
const editor = editorRef.current
|
||||||
|
if (!editor) return
|
||||||
|
if (value !== editor.getValue()) {
|
||||||
|
const cursor = editor.getCursor()
|
||||||
|
editor.setValue(value)
|
||||||
|
editor.setCursor(cursor)
|
||||||
|
}
|
||||||
|
}, [value])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="large-markdown-editor">
|
||||||
|
<div className="large-markdown-editor-header">
|
||||||
|
<InfoCircleOutlined className="large-markdown-editor-header-icon" />
|
||||||
|
<span>{LARGE_MARKDOWN_NOTICE}</span>
|
||||||
|
</div>
|
||||||
|
<div className="large-markdown-editor-body" ref={containerRef} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -39,15 +39,35 @@
|
||||||
padding: 0 0 1px;
|
padding: 0 0 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 首块下移,避免被固定浮动的提示窗遮挡 */
|
||||||
|
.markdown-block[data-index='0'] {
|
||||||
|
padding-top: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 虚拟滚动条更纤细,减少视觉割裂 */
|
||||||
|
.markdown-virtual-list::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-virtual-list::-webkit-scrollbar-thumb {
|
||||||
|
background: color-mix(in srgb, var(--text-color-secondary) 28%, transparent);
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 3px solid transparent;
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-virtual-list::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: color-mix(in srgb, var(--text-color-secondary) 44%, transparent);
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.large-markdown-notice,
|
|
||||||
.markdown-block {
|
.markdown-block {
|
||||||
width: calc(100% - 32px);
|
width: calc(100% - 32px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.large-markdown-notice,
|
|
||||||
.markdown-block {
|
.markdown-block {
|
||||||
width: calc(100% - 24px);
|
width: calc(100% - 24px);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,14 @@ import ReactMarkdown from 'react-markdown'
|
||||||
import remarkGfm from 'remark-gfm'
|
import remarkGfm from 'remark-gfm'
|
||||||
import rehypeRaw from 'rehype-raw'
|
import rehypeRaw from 'rehype-raw'
|
||||||
import rehypeSlug from 'rehype-slug'
|
import rehypeSlug from 'rehype-slug'
|
||||||
|
import rehypeHighlight from 'rehype-highlight'
|
||||||
|
import GithubSlugger from 'github-slugger'
|
||||||
|
import FloatingToc from '@/components/FloatingToc/FloatingToc'
|
||||||
import './LargeMarkdownViewer.css'
|
import './LargeMarkdownViewer.css'
|
||||||
|
|
||||||
export const LARGE_MARKDOWN_THRESHOLD = 250000
|
export const LARGE_MARKDOWN_THRESHOLD = 250000
|
||||||
export const LARGE_MARKDOWN_NOTICE = '此文档内容过长,将采用精简模式显示'
|
export const LARGE_MARKDOWN_NOTICE = '此文档内容过长,将采用精简模式显示'
|
||||||
|
const MAX_TOC_ITEMS = 500
|
||||||
|
|
||||||
export function isLargeMarkdownContent(content = '') {
|
export function isLargeMarkdownContent(content = '') {
|
||||||
return content.length > LARGE_MARKDOWN_THRESHOLD
|
return content.length > LARGE_MARKDOWN_THRESHOLD
|
||||||
|
|
@ -27,10 +31,14 @@ export function MarkdownSizeNotice({ className = '' }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function splitMarkdownIntoBlocks(content) {
|
// 将 markdown 切分为块,并在切分的同时提取标题用于目录导航。
|
||||||
if (!content) return []
|
// 每个块以标题或空行为边界,目录项记录其所在块索引,便于虚拟滚动定位。
|
||||||
|
function buildBlocksAndToc(content) {
|
||||||
|
if (!content) return { blocks: [], tocItems: [] }
|
||||||
|
|
||||||
const blocks = []
|
const blocks = []
|
||||||
|
const tocItems = []
|
||||||
|
const slugger = new GithubSlugger()
|
||||||
const lines = content.split('\n')
|
const lines = content.split('\n')
|
||||||
let current = []
|
let current = []
|
||||||
let inFence = false
|
let inFence = false
|
||||||
|
|
@ -44,10 +52,20 @@ function splitMarkdownIntoBlocks(content) {
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const isFenceLine = /^\s*(```|~~~)/.test(line)
|
const isFenceLine = /^\s*(```|~~~)/.test(line)
|
||||||
const isHeading = /^(#{1,6})\s+/.test(line)
|
const headingMatch = inFence ? null : line.match(/^(#{1,6})\s+(.+)$/)
|
||||||
|
|
||||||
if (!inFence && isHeading) {
|
if (headingMatch) {
|
||||||
flush()
|
flush()
|
||||||
|
if (tocItems.length < MAX_TOC_ITEMS) {
|
||||||
|
const key = slugger.slug(headingMatch[2].trim())
|
||||||
|
tocItems.push({
|
||||||
|
key: `#${key}`,
|
||||||
|
href: `#${key}`,
|
||||||
|
title: headingMatch[2].trim(),
|
||||||
|
level: headingMatch[1].length,
|
||||||
|
blockIndex: blocks.length, // 该标题即将进入的块的索引
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
current.push(line)
|
current.push(line)
|
||||||
|
|
@ -62,23 +80,48 @@ function splitMarkdownIntoBlocks(content) {
|
||||||
}
|
}
|
||||||
|
|
||||||
flush()
|
flush()
|
||||||
return blocks
|
return { blocks, tocItems }
|
||||||
}
|
}
|
||||||
|
|
||||||
const LargeMarkdownViewer = forwardRef(function LargeMarkdownViewer({ content, components, onClick }, ref) {
|
const LargeMarkdownViewer = forwardRef(function LargeMarkdownViewer(
|
||||||
|
{ content, components, onClick, searchKeyword = '', renderTitle },
|
||||||
|
ref
|
||||||
|
) {
|
||||||
const virtuosoRef = useRef(null)
|
const virtuosoRef = useRef(null)
|
||||||
const markdownBlocks = useMemo(() => splitMarkdownIntoBlocks(content), [content])
|
const { blocks: markdownBlocks, tocItems } = useMemo(
|
||||||
|
() => buildBlocksAndToc(content),
|
||||||
|
[content]
|
||||||
|
)
|
||||||
|
|
||||||
|
const hrefToIndex = useMemo(() => {
|
||||||
|
const map = new Map()
|
||||||
|
for (const item of tocItems) {
|
||||||
|
if (!map.has(item.href)) {
|
||||||
|
map.set(item.href, item.blockIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}, [tocItems])
|
||||||
|
|
||||||
|
const scrollToIndex = (index) => {
|
||||||
|
virtuosoRef.current?.scrollToIndex({
|
||||||
|
index,
|
||||||
|
align: 'start',
|
||||||
|
behavior: 'smooth',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
scrollToTop: () => {
|
scrollToTop: () => scrollToIndex(0),
|
||||||
virtuosoRef.current?.scrollToIndex({
|
|
||||||
index: 0,
|
|
||||||
align: 'start',
|
|
||||||
behavior: 'smooth',
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}), [])
|
}), [])
|
||||||
|
|
||||||
|
const handleTocNavigate = (link) => {
|
||||||
|
const index = hrefToIndex.get(link)
|
||||||
|
if (typeof index === 'number') {
|
||||||
|
scrollToIndex(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="large-markdown-viewer">
|
<div className="large-markdown-viewer">
|
||||||
<MarkdownSizeNotice />
|
<MarkdownSizeNotice />
|
||||||
|
|
@ -91,7 +134,7 @@ const LargeMarkdownViewer = forwardRef(function LargeMarkdownViewer({ content, c
|
||||||
<div className="markdown-block" data-index={index}>
|
<div className="markdown-block" data-index={index}>
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
remarkPlugins={[remarkGfm]}
|
remarkPlugins={[remarkGfm]}
|
||||||
rehypePlugins={[rehypeRaw, rehypeSlug]}
|
rehypePlugins={[rehypeRaw, rehypeSlug, rehypeHighlight]}
|
||||||
components={components}
|
components={components}
|
||||||
>
|
>
|
||||||
{block}
|
{block}
|
||||||
|
|
@ -100,6 +143,12 @@ const LargeMarkdownViewer = forwardRef(function LargeMarkdownViewer({ content, c
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<FloatingToc
|
||||||
|
items={tocItems}
|
||||||
|
searchKeyword={searchKeyword}
|
||||||
|
renderTitle={renderTitle}
|
||||||
|
onNavigate={handleTocNavigate}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
|
import { useLocation } from 'react-router-dom'
|
||||||
import { Layout } from 'antd'
|
import { Layout } from 'antd'
|
||||||
import AppSider from './AppSider'
|
import AppSider from './AppSider'
|
||||||
import AppHeader from './AppHeader'
|
import AppHeader from './AppHeader'
|
||||||
|
|
@ -6,9 +7,19 @@ import './MainLayout.css'
|
||||||
|
|
||||||
const { Content } = Layout
|
const { Content } = Layout
|
||||||
|
|
||||||
|
// 进入项目文档页/编辑页时自动折叠全局侧边栏,给文档内容腾出空间
|
||||||
|
const COLLAPSE_PATTERNS = [/^\/projects\/[^/]+\/docs/, /^\/projects\/[^/]+\/editor/]
|
||||||
|
|
||||||
function MainLayout({ children }) {
|
function MainLayout({ children }) {
|
||||||
|
const location = useLocation()
|
||||||
const [collapsed, setCollapsed] = useState(false)
|
const [collapsed, setCollapsed] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (COLLAPSE_PATTERNS.some((pattern) => pattern.test(location.pathname))) {
|
||||||
|
setCollapsed(true)
|
||||||
|
}
|
||||||
|
}, [location.pathname])
|
||||||
|
|
||||||
const toggleCollapsed = () => {
|
const toggleCollapsed = () => {
|
||||||
setCollapsed(!collapsed)
|
setCollapsed(!collapsed)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -236,15 +236,12 @@
|
||||||
|
|
||||||
.bytemd-wrapper.large-file-editor {
|
.bytemd-wrapper.large-file-editor {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 0;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.large-file-editor-notice {
|
/* Fix for bytemd-react wrapper div(仅普通编辑器,超大着色编辑器不受此约束) */
|
||||||
top: 12px;
|
.bytemd-wrapper:not(.large-file-editor)>div {
|
||||||
}
|
|
||||||
|
|
||||||
/* Fix for bytemd-react wrapper div */
|
|
||||||
.bytemd-wrapper>div {
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -253,36 +250,6 @@
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bytemd-wrapper.large-file-editor>.large-markdown-notice {
|
|
||||||
flex: none !important;
|
|
||||||
display: block !important;
|
|
||||||
width: min(920px, calc(100% - 48px)) !important;
|
|
||||||
height: auto !important;
|
|
||||||
min-height: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.large-markdown-textarea {
|
|
||||||
flex: 1;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 0;
|
|
||||||
resize: none;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 2px;
|
|
||||||
padding: 64px 16px 16px;
|
|
||||||
background: var(--bg-color);
|
|
||||||
color: var(--text-color);
|
|
||||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.8;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.large-markdown-textarea:focus {
|
|
||||||
border-color: #1677ff;
|
|
||||||
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-editor {
|
.empty-editor {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,8 @@ import {
|
||||||
} from '@/api/file'
|
} from '@/api/file'
|
||||||
import Toast from '@/components/Toast/Toast'
|
import Toast from '@/components/Toast/Toast'
|
||||||
import ModeSwitch from '@/components/ModeSwitch/ModeSwitch'
|
import ModeSwitch from '@/components/ModeSwitch/ModeSwitch'
|
||||||
import { MarkdownSizeNotice, isLargeMarkdownContent } from '@/components/LargeMarkdownViewer/LargeMarkdownViewer'
|
import { isLargeMarkdownContent } from '@/components/LargeMarkdownViewer/LargeMarkdownViewer'
|
||||||
|
import LargeMarkdownEditor from '@/components/LargeMarkdownViewer/LargeMarkdownEditor'
|
||||||
import './DocumentEditor.css'
|
import './DocumentEditor.css'
|
||||||
|
|
||||||
const { Sider, Content } = Layout
|
const { Sider, Content } = Layout
|
||||||
|
|
@ -1192,15 +1193,10 @@ function DocumentEditor() {
|
||||||
onDragOver={(e) => e.preventDefault()}
|
onDragOver={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
{isLargeMarkdown ? (
|
{isLargeMarkdown ? (
|
||||||
<>
|
<LargeMarkdownEditor
|
||||||
<MarkdownSizeNotice className="large-file-editor-notice" />
|
value={fileContent}
|
||||||
<textarea
|
onChange={setFileContent}
|
||||||
className="large-markdown-textarea"
|
/>
|
||||||
value={fileContent}
|
|
||||||
onChange={(e) => setFileContent(e.target.value)}
|
|
||||||
spellCheck={false}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<Editor
|
<Editor
|
||||||
key={selectedFile}
|
key={selectedFile}
|
||||||
|
|
|
||||||
|
|
@ -1160,6 +1160,8 @@ function DocumentPage() {
|
||||||
ref={largeMarkdownRef}
|
ref={largeMarkdownRef}
|
||||||
content={markdownContent}
|
content={markdownContent}
|
||||||
components={markdownComponents}
|
components={markdownComponents}
|
||||||
|
searchKeyword={searchKeyword}
|
||||||
|
renderTitle={(item, keyword) => <HighlightText text={item.title} keyword={keyword} />}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="markdown-body">
|
<div className="markdown-body">
|
||||||
|
|
|
||||||
|
|
@ -607,6 +607,8 @@ function ProjectSharePage() {
|
||||||
ref={largeMarkdownRef}
|
ref={largeMarkdownRef}
|
||||||
content={markdownContent}
|
content={markdownContent}
|
||||||
components={markdownComponents}
|
components={markdownComponents}
|
||||||
|
searchKeyword={searchKeyword}
|
||||||
|
renderTitle={(item, keyword) => <HighlightText text={item.title} keyword={keyword} />}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (e.defaultPrevented) return
|
if (e.defaultPrevented) return
|
||||||
const target = e.target.closest('a')
|
const target = e.target.closest('a')
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue