Skip to content

Commit e821907

Browse files
author
ws-wangjg
committed
feat: pro mdx editor
1 parent ad44ae5 commit e821907

File tree

9 files changed

+436
-53
lines changed

9 files changed

+436
-53
lines changed

package-lock.json

Lines changed: 48 additions & 29 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,7 @@
335335
"@sentry/react": "^10.33.0",
336336
"@sentry/tracing": "^7.120.4",
337337
"@smakss/react-scroll-direction": "^4.2.0",
338+
"@tiptap/extension-code-block-lowlight": "^3.15.3",
338339
"@tiptap/extension-image": "^3.15.3",
339340
"@tiptap/extension-link": "^3.15.3",
340341
"@tiptap/extension-placeholder": "^3.15.3",
@@ -424,6 +425,7 @@
424425
"screenfull": "^6.0.2",
425426
"three": "^0.182.0",
426427
"turndown": "^7.2.2",
428+
"turndown-plugin-gfm": "^1.0.2",
427429
"use-debounce": "^10.1.0",
428430
"zustand": "^5.0.10"
429431
},

src/components/stateless/MdxEditor/components/EditorCore/index.jsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { memo, useEffect, useRef } from 'react'
22
import { EditorContent } from '@tiptap/react'
33
import styles from './index.module.less'
44

5-
function EditorCore({ editor, onStatsUpdate }) {
5+
function EditorCore({ editor, onStatsUpdate, onScroll, scrollRef }) {
66
const containerRef = useRef(null)
77

88
useEffect(() => {
@@ -57,7 +57,14 @@ function EditorCore({ editor, onStatsUpdate }) {
5757
}, [editor])
5858

5959
return (
60-
<div className={`${styles.mdxEditorCol} mdxEditorCol`} ref={containerRef}>
60+
<div
61+
className={`${styles.mdxEditorCol} mdxEditorCol`}
62+
ref={(el) => {
63+
containerRef.current = el
64+
if (scrollRef) scrollRef.current = el
65+
}}
66+
onScroll={onScroll}
67+
>
6168
<EditorContent editor={editor} />
6269
</div>
6370
)

src/components/stateless/MdxEditor/components/EditorCore/index.module.less

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,27 +44,91 @@
4444

4545
h2 { font-size: 1.75em; }
4646

47-
ul, ol { padding-left: 1.5em; margin-bottom: 1em; }
48-
li { margin: 0.25em 0; }
47+
ul, ol {
48+
padding-left: 1.5em;
49+
margin-bottom: 1em;
50+
}
51+
52+
ul {
53+
list-style-type: disc;
54+
}
55+
56+
ol {
57+
list-style-type: decimal;
58+
}
59+
60+
li {
61+
margin: 0.25em 0;
62+
display: list-item;
63+
}
64+
65+
ul ul { list-style-type: circle; }
66+
ul ul ul { list-style-type: square; }
67+
ol ol { list-style-type: lower-alpha; }
68+
ol ol ol { list-style-type: lower-roman; }
4969

5070
ul[data-type="taskList"] {
5171
list-style: none;
5272
padding: 0;
53-
54-
li {
73+
/* 支持多种 Tiptap 输出结构:
74+
1. li > label > input + div
75+
2. li > input + div
76+
3. li > label (仅包裹 input)
77+
*/
78+
/* 支持多种 Tiptap 输出:有的版本会生成 li[data-type="taskItem"], 有的生成 li[data-checked] */
79+
li[data-type="taskItem"], li[data-checked] {
5580
display: flex;
5681
align-items: center;
5782
gap: 8px;
83+
padding: 4px 0;
5884

85+
/* 情况 A: label 在左侧,label 内包含 input */
5986
> label {
6087
flex: 0 0 auto;
6188
margin: 0;
6289
cursor: pointer;
90+
display: inline-flex;
91+
align-items: center;
92+
justify-content: center;
93+
width: 20px;
94+
height: 20px;
95+
}
96+
97+
/* 情况 B: input 与内容为直接子节点(input + div) */
98+
> input[type="checkbox"] {
99+
flex: 0 0 auto;
100+
margin: 0;
101+
width: 18px;
102+
height: 18px;
103+
cursor: pointer;
63104
}
64105

65-
> div {
106+
/* 文本内容无论位于 div(常见)还是位于 label 内的子节点,都应占据剩余空间并垂直居中 */
107+
/* 文本内容:通常在 div > p,或者直接在 p,或作为 label 的子节点
108+
需要同时清除内部 p 的默认 margin(如 margin-bottom)以保证垂直居中 */
109+
> div,
110+
> label > div,
111+
> label > p,
112+
> p {
66113
flex: 1 1 auto;
67-
line-height: 1.75;
114+
display: flex;
115+
align-items: center;
116+
line-height: 1.5;
117+
margin: 0;
118+
}
119+
120+
/* 额外确保 div 内的 p 元素没有默认 margin */
121+
> div p,
122+
> label > div p {
123+
margin: 0;
124+
}
125+
126+
/* 当 input 在 label 内时,确保 input 大小 */
127+
label > input[type="checkbox"] {
128+
width: 18px;
129+
height: 18px;
130+
margin: 0;
131+
cursor: pointer;
68132
}
69133
}
70134
}

src/components/stateless/MdxEditor/components/SourceView/index.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import React, { memo } from 'react'
22
import styles from './index.module.less'
33

4-
function SourceView({ markdown, onChange }) {
4+
function SourceView({ markdown, onChange, onScroll, scrollRef }) {
55
return (
6-
<div className={`${styles.mdxSourceCol} mdxSourceCol`}>
6+
<div className={`${styles.mdxSourceCol} mdxSourceCol`} ref={scrollRef} onScroll={onScroll}>
77
<textarea
88
className={styles.mdxSourceTextarea}
99
value={markdown}

src/components/stateless/MdxEditor/components/Toolbar/index.jsx

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useMemo, useRef } from 'react'
1+
import React, { useMemo, useRef, useState } from 'react'
22
import {
33
Undo2,
44
Redo2,
@@ -22,26 +22,44 @@ import {
2222
Image,
2323
Link2,
2424
Table,
25+
X,
2526
} from 'lucide-react'
2627
import styles from './index.module.less'
2728

2829
// editorKey 参数用于触发重新渲染,确保工具栏高亮状态更新
2930
function Toolbar({ editor, viewMode, onChangeViewMode, editorKey }) {
3031
const fileInputRef = useRef(null)
3132
const iconSize = 18
33+
const [showLinkModal, setShowLinkModal] = useState(false)
34+
const [linkUrl, setLinkUrl] = useState('https://')
35+
const linkInputRef = useRef(null)
3236

3337
// editorKey 变化时强制重新计算
3438
void editorKey
3539

40+
const handleLinkSubmit = () => {
41+
if (linkUrl && linkUrl !== 'https://') {
42+
editor?.chain().focus().setLink({ href: linkUrl }).run()
43+
}
44+
setShowLinkModal(false)
45+
setLinkUrl('https://')
46+
}
47+
48+
const handleLinkCancel = () => {
49+
setShowLinkModal(false)
50+
setLinkUrl('https://')
51+
editor?.chain().focus().run()
52+
}
53+
3654
const handleAction = (cmd, opt) => {
3755
if (!editor) return
3856
if (cmd === 'image') {
3957
fileInputRef.current?.click()
4058
return
4159
}
4260
if (cmd === 'link') {
43-
const url = prompt('输入链接地址:', 'https://')
44-
if (url) editor.chain().focus().setLink({ href: url }).run()
61+
setShowLinkModal(true)
62+
setTimeout(() => linkInputRef.current?.select(), 100)
4563
return
4664
}
4765
if (cmd === 'table') {
@@ -191,6 +209,43 @@ function Toolbar({ editor, viewMode, onChangeViewMode, editorKey }) {
191209
</button>
192210
</div>
193211
</div>
212+
213+
{/* 链接输入弹窗 */}
214+
{showLinkModal && (
215+
<div className={styles.linkModalOverlay} onClick={handleLinkCancel}>
216+
<div className={styles.linkModal} onClick={(e) => e.stopPropagation()}>
217+
<div className={styles.linkModalHeader}>
218+
<span>插入链接</span>
219+
<button type="button" className={styles.linkModalClose} onClick={handleLinkCancel}>
220+
<X size={16} />
221+
</button>
222+
</div>
223+
<div className={styles.linkModalBody}>
224+
<input
225+
ref={linkInputRef}
226+
type="url"
227+
className={styles.linkInput}
228+
value={linkUrl}
229+
onChange={(e) => setLinkUrl(e.target.value)}
230+
onKeyDown={(e) => {
231+
if (e.key === 'Enter') handleLinkSubmit()
232+
if (e.key === 'Escape') handleLinkCancel()
233+
}}
234+
placeholder="https://example.com"
235+
autoFocus
236+
/>
237+
</div>
238+
<div className={styles.linkModalFooter}>
239+
<button type="button" className={styles.linkBtnCancel} onClick={handleLinkCancel}>
240+
取消
241+
</button>
242+
<button type="button" className={styles.linkBtnConfirm} onClick={handleLinkSubmit}>
243+
确定
244+
</button>
245+
</div>
246+
</div>
247+
</div>
248+
)}
194249
</div>
195250
)
196251
}

0 commit comments

Comments
 (0)