Skip to content

Commit 36abad8

Browse files
committed
chore: restore mdx editor (revert deletion)
1 parent 16e7402 commit 36abad8

16 files changed

Lines changed: 1377 additions & 0 deletions

File tree

src/components/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export { default as PointerMove } from './stateless/PointerMove'
104104
export { default as RadioInput } from './stateless/RadioInput'
105105
export { ReactSignature } from './stateless/ReactSignature'
106106
export { default as ReMarkdown } from './stateless/ReMarkdown'
107+
export { default as MdxEditor } from './stateless/MdxEditor'
107108
export { default as SafeLink } from './stateless/SafeLink'
108109
export { default as SandpackBasic } from './stateless/SandpackBasic'
109110
export { default as ScratchToReveal } from './stateless/ScratchToReveal'
@@ -154,3 +155,4 @@ export { default as SliderCaptcha } from './stateless/SliderCaptcha'
154155
export { default as MoveChecker } from './stateless/MoveChecker'
155156
export { default as SafeHtml } from './stateless/SafeHtml'
156157
export { default as AnimatedIcon } from './stateless/AnimatedIcon'
158+
export { default as RichEditor } from './stateless/RichEditor'
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import React, { memo, useEffect, useRef } from 'react'
2+
import { EditorContent } from '@tiptap/react'
3+
import styles from './index.module.less'
4+
5+
function EditorCore({ editor, onStatsUpdate, onScroll, scrollRef }) {
6+
const containerRef = useRef(null)
7+
8+
useEffect(() => {
9+
if (!editor) return
10+
const updateStats = () => {
11+
const text = editor.getText()
12+
const chars = text.length
13+
const words = text
14+
.trim()
15+
.split(/\s+/)
16+
.filter((w) => w.length > 0).length
17+
const { from } = editor.state.selection
18+
const state = editor.state
19+
const textBefore = state.doc.textBetween(0, from, '\n')
20+
const lines = textBefore.split('\n')
21+
const line = lines.length
22+
const col = lines[lines.length - 1].length + 1
23+
onStatsUpdate({ cursorPos: `行 ${line}, 列 ${col}`, stats: `${chars} 字符 / ${words} 词` })
24+
}
25+
26+
updateStats()
27+
editor.on('selectionUpdate', updateStats)
28+
editor.on('update', updateStats)
29+
return () => {
30+
editor.off('selectionUpdate', updateStats)
31+
editor.off('update', updateStats)
32+
}
33+
}, [editor, onStatsUpdate])
34+
35+
useEffect(() => {
36+
const container = containerRef.current
37+
if (!container || !editor) return
38+
const handleClick = (e) => {
39+
if (e.target.classList.contains('ProseMirror')) {
40+
const contentHeight = editor.view.dom.scrollHeight
41+
const clickY = e.offsetY
42+
if (clickY > contentHeight - 50) {
43+
e.preventDefault()
44+
const estimatedLineHeight = 45
45+
const distance = clickY - contentHeight
46+
const linesToAdd = Math.floor(distance / estimatedLineHeight)
47+
let finalLines = Math.min(Math.max(linesToAdd, 1), 50)
48+
if (finalLines > 0) {
49+
const newParagraphs = Array(finalLines).fill('<p></p>').join('')
50+
editor.chain().focus('end').insertContent(newParagraphs).run()
51+
}
52+
}
53+
}
54+
}
55+
container.addEventListener('click', handleClick)
56+
return () => container.removeEventListener('click', handleClick)
57+
}, [editor])
58+
59+
return (
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+
>
68+
<EditorContent editor={editor} />
69+
</div>
70+
)
71+
}
72+
73+
export default memo(EditorCore)
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
@import '../../styles/global.less';
2+
3+
.mdxEditorCol {
4+
flex: 1;
5+
height: 100%;
6+
overflow-y: auto;
7+
padding: 40px;
8+
background: var(--mdx-bg-surface);
9+
transition: background 0.3s;
10+
}
11+
12+
.mdxEditorCol :global(.ProseMirror) {
13+
outline: none;
14+
min-height: 100%;
15+
max-width: 840px;
16+
margin: 0 auto;
17+
color: var(--mdx-text-main);
18+
line-height: 1.75;
19+
font-size: 16px;
20+
padding-bottom: 50vh;
21+
cursor: text;
22+
23+
p.is-editor-empty:first-child::before {
24+
content: attr(data-placeholder);
25+
color: var(--mdx-text-muted);
26+
pointer-events: none;
27+
float: left;
28+
height: 0;
29+
font-style: italic;
30+
}
31+
32+
h1, h2, h3, h4 {
33+
font-weight: 700;
34+
line-height: 1.3;
35+
margin-top: 1.5em;
36+
margin-bottom: 0.5em;
37+
}
38+
39+
h1 {
40+
font-size: 2.25em;
41+
border-bottom: 1px solid var(--mdx-border);
42+
padding-bottom: 0.3em;
43+
}
44+
45+
h2 { font-size: 1.75em; }
46+
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; }
69+
70+
ul[data-type="taskList"] {
71+
list-style: none;
72+
padding: 0;
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] {
80+
display: flex;
81+
align-items: center;
82+
gap: 8px;
83+
padding: 4px 0;
84+
85+
/* 情况 A: label 在左侧,label 内包含 input */
86+
> label {
87+
flex: 0 0 auto;
88+
margin: 0;
89+
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;
104+
}
105+
106+
/* 文本内容无论位于 div(常见)还是位于 label 内的子节点,都应占据剩余空间并垂直居中 */
107+
/* 文本内容:通常在 div > p,或者直接在 p,或作为 label 的子节点
108+
需要同时清除内部 p 的默认 margin(如 margin-bottom)以保证垂直居中 */
109+
> div,
110+
> label > div,
111+
> label > p,
112+
> p {
113+
flex: 1 1 auto;
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;
132+
}
133+
}
134+
}
135+
136+
pre {
137+
background: var(--mdx-code-bg);
138+
color: var(--mdx-code-text);
139+
padding: 1rem;
140+
border-radius: 8px;
141+
overflow-x: auto;
142+
margin: 1.5rem 0;
143+
font-family: monospace;
144+
font-size: 0.9em;
145+
}
146+
147+
code {
148+
background: rgba(0,0,0,0.05);
149+
padding: 0.2em 0.4em;
150+
border-radius: 4px;
151+
font-family: monospace;
152+
font-size: 0.85em;
153+
color: #ef4444;
154+
}
155+
156+
pre code {
157+
background: transparent;
158+
color: inherit;
159+
padding: 0;
160+
}
161+
162+
blockquote {
163+
border-left: 4px solid var(--mdx-primary);
164+
padding-left: 1rem;
165+
font-style: italic;
166+
color: var(--mdx-text-muted);
167+
margin: 1.5em 0;
168+
}
169+
170+
img {
171+
max-width: 100%;
172+
height: auto;
173+
border-radius: 8px;
174+
margin: 1.5rem 0;
175+
display: block;
176+
box-shadow: var(--mdx-shadow-md);
177+
178+
&.ProseMirror-selectednode {
179+
outline: 3px solid var(--mdx-primary);
180+
}
181+
}
182+
183+
a {
184+
color: var(--mdx-primary);
185+
text-decoration: underline;
186+
cursor: pointer;
187+
}
188+
189+
table {
190+
border-collapse: collapse;
191+
table-layout: fixed;
192+
width: 100%;
193+
margin: 1.5em 0;
194+
overflow: hidden;
195+
}
196+
197+
td, th {
198+
min-width: 1em;
199+
border: 1px solid var(--mdx-border);
200+
padding: 6px 10px;
201+
vertical-align: top;
202+
box-sizing: border-box;
203+
background-color: var(--mdx-bg-surface);
204+
}
205+
206+
th {
207+
font-weight: 700;
208+
text-align: left;
209+
background-color: var(--mdx-bg-body);
210+
}
211+
212+
.selectedCell:after {
213+
z-index: 2;
214+
position: absolute;
215+
content: "";
216+
left: 0; right: 0; top: 0; bottom: 0;
217+
background: rgba(200, 200, 255, 0.4);
218+
pointer-events: none;
219+
}
220+
221+
.column-resize-handle {
222+
position: absolute;
223+
right: -2px;
224+
top: 0;
225+
bottom: 0;
226+
width: 4px;
227+
z-index: 20;
228+
background-color: #adf;
229+
pointer-events: none;
230+
}
231+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React, { memo } from 'react'
2+
import styles from './index.module.less'
3+
4+
function Footer({ cursorPos, stats }) {
5+
return (
6+
<footer className={`${styles.mdxFooter} mdxFooter`}>
7+
<span>{cursorPos}</span>
8+
<span>{stats}</span>
9+
</footer>
10+
)
11+
}
12+
13+
export default memo(Footer)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
@import '../../styles/global.less';
2+
3+
.mdxFooter,
4+
:global(.mdxFooter) {
5+
height: 28px;
6+
border-top: 1px solid var(--mdx-border);
7+
background: var(--mdx-bg-surface);
8+
display: flex;
9+
align-items: center;
10+
justify-content: space-between;
11+
padding: 0 15px;
12+
font-size: 11px;
13+
color: var(--mdx-text-muted);
14+
flex-shrink: 0;
15+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import React from 'react'
2+
import { Download, Copy, Moon, Sun, PenTool } from 'lucide-react'
3+
import styles from './index.module.less'
4+
5+
export default function Header({ theme, onThemeChange, onCopy, onDownload }) {
6+
return (
7+
<header className={styles.mdxHeader}>
8+
<div className={styles.mdxBrand}>
9+
<PenTool className={styles.mdxLogoIcon} />
10+
<span>Pro MDX Editor</span>
11+
</div>
12+
<div className={styles.mdxHeaderActions}>
13+
<button className={styles.mdxIconBtn} onClick={onThemeChange} title="切换主题">
14+
{theme === 'dark' ? <Sun size={18} /> : <Moon size={18} />}
15+
</button>
16+
<button className={styles.mdxIconBtn} onClick={onDownload} title="下载文件">
17+
<Download size={18} />
18+
</button>
19+
<button className={styles.mdxCopyBtn} onClick={onCopy} title="复制 Markdown">
20+
<Copy size={18} />
21+
</button>
22+
</div>
23+
</header>
24+
)
25+
}

0 commit comments

Comments
 (0)