feat(editor): 新增 Markdown 源码视图切换
在 Tiptap 编辑器右上角增加 </> 悬浮按钮,支持在富文本与 Markdown 源码视图间双向切换,内容自动同步。getMarkdown() 在 源码模式下返回 textarea 内容,使 Rust 端提交逻辑无需改动。
This commit is contained in:
parent
c6d27f0ae0
commit
480aa92bb4
@ -23,6 +23,10 @@ class TiptapEditorInstance {
|
|||||||
private editor: Editor | null = null
|
private editor: Editor | null = null
|
||||||
private container: HTMLElement
|
private container: HTMLElement
|
||||||
private options: EditorOptions
|
private options: EditorOptions
|
||||||
|
// 源码模式相关状态
|
||||||
|
private isSourceMode = false
|
||||||
|
private sourceTextarea: HTMLTextAreaElement | null = null
|
||||||
|
private toggleButton: HTMLButtonElement | null = null
|
||||||
|
|
||||||
constructor(container: HTMLElement, options: EditorOptions = {}) {
|
constructor(container: HTMLElement, options: EditorOptions = {}) {
|
||||||
this.container = container
|
this.container = container
|
||||||
@ -35,6 +39,30 @@ class TiptapEditorInstance {
|
|||||||
el.className = 'tiptap-editor'
|
el.className = 'tiptap-editor'
|
||||||
this.container.appendChild(el)
|
this.container.appendChild(el)
|
||||||
|
|
||||||
|
// 源码模式切换按钮:悬浮于编辑器右上角
|
||||||
|
this.toggleButton = document.createElement('button')
|
||||||
|
this.toggleButton.className = 'tiptap-toggle-btn'
|
||||||
|
this.toggleButton.type = 'button'
|
||||||
|
this.toggleButton.title = '切换 Markdown 源码'
|
||||||
|
this.toggleButton.textContent = '</>'
|
||||||
|
this.toggleButton.addEventListener('click', () => this.toggleSource())
|
||||||
|
el.appendChild(this.toggleButton)
|
||||||
|
|
||||||
|
// 源码模式 textarea:初始隐藏,与 ProseMirror 共用同一区域
|
||||||
|
this.sourceTextarea = document.createElement('textarea')
|
||||||
|
this.sourceTextarea.className = 'tiptap-source-textarea'
|
||||||
|
this.sourceTextarea.hidden = true
|
||||||
|
this.sourceTextarea.placeholder = '在此输入 Markdown 源码...'
|
||||||
|
this.sourceTextarea.spellcheck = false
|
||||||
|
this.sourceTextarea.addEventListener('input', () => {
|
||||||
|
// 保持全局缓存与 onUpdate 回调一致
|
||||||
|
window.__tiptap_content = this.sourceTextarea!.value
|
||||||
|
if (this.options.onUpdate) {
|
||||||
|
this.options.onUpdate(this.sourceTextarea!.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
el.appendChild(this.sourceTextarea)
|
||||||
|
|
||||||
this.editor = new Editor({
|
this.editor = new Editor({
|
||||||
element: el,
|
element: el,
|
||||||
extensions: [
|
extensions: [
|
||||||
@ -114,9 +142,44 @@ class TiptapEditorInstance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getMarkdown(): string {
|
getMarkdown(): string {
|
||||||
|
// 源码模式下直接返回 textarea 内容,确保提交逻辑无需感知视图模式
|
||||||
|
if (this.isSourceMode && this.sourceTextarea) {
|
||||||
|
return this.sourceTextarea.value
|
||||||
|
}
|
||||||
return this.editor?.getMarkdown() || ''
|
return this.editor?.getMarkdown() || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在富文本模式与 Markdown 源码模式之间切换。
|
||||||
|
* 切换时同步内容:富文本 → 源码用 getMarkdown 导出;源码 → 富文本用 setMarkdown 回填。
|
||||||
|
*/
|
||||||
|
toggleSource(): void {
|
||||||
|
if (!this.editor || !this.sourceTextarea || !this.toggleButton) return
|
||||||
|
|
||||||
|
// ProseMirror 的实际 DOM 节点(.ProseMirror),用于切换显隐。
|
||||||
|
const proseMirrorDom = this.editor.view.dom
|
||||||
|
if (!this.isSourceMode) {
|
||||||
|
// 富文本 → 源码:导出当前 Markdown 到 textarea
|
||||||
|
this.sourceTextarea.value = this.editor.getMarkdown()
|
||||||
|
proseMirrorDom.style.display = 'none'
|
||||||
|
this.sourceTextarea.hidden = false
|
||||||
|
this.sourceTextarea.focus()
|
||||||
|
this.toggleButton.textContent = '✎'
|
||||||
|
this.toggleButton.title = '切换富文本'
|
||||||
|
this.isSourceMode = true
|
||||||
|
} else {
|
||||||
|
// 源码 → 富文本:把 textarea 内容回填到编辑器
|
||||||
|
const md = this.sourceTextarea.value
|
||||||
|
this.setMarkdown(md)
|
||||||
|
this.sourceTextarea.hidden = true
|
||||||
|
proseMirrorDom.style.display = ''
|
||||||
|
this.toggleButton.textContent = '</>'
|
||||||
|
this.toggleButton.title = '切换 Markdown 源码'
|
||||||
|
this.isSourceMode = false
|
||||||
|
this.editor.commands.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setMarkdown(content: string): void {
|
setMarkdown(content: string): void {
|
||||||
this.editor?.commands.setContent(content, { emitUpdate: false, contentType: 'markdown' })
|
this.editor?.commands.setContent(content, { emitUpdate: false, contentType: 'markdown' })
|
||||||
}
|
}
|
||||||
@ -140,6 +203,10 @@ class TiptapEditorInstance {
|
|||||||
destroy(): void {
|
destroy(): void {
|
||||||
this.editor?.destroy()
|
this.editor?.destroy()
|
||||||
this.editor = null
|
this.editor = null
|
||||||
|
// 清理源码模式相关引用(容器 innerHTML 已清空,DOM 会随之移除)
|
||||||
|
this.sourceTextarea = null
|
||||||
|
this.toggleButton = null
|
||||||
|
this.isSourceMode = false
|
||||||
this.container.innerHTML = ''
|
this.container.innerHTML = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -425,3 +425,73 @@
|
|||||||
.dark .tiptap-editor ul[data-type="taskList"] li > label input[type="checkbox"] {
|
.dark .tiptap-editor ul[data-type="taskList"] li > label input[type="checkbox"] {
|
||||||
accent-color: #58a6ff;
|
accent-color: #58a6ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ========== Source Toggle Button ========== */
|
||||||
|
.tiptap-toggle-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
z-index: 10;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(0, 0, 0, 0.04);
|
||||||
|
color: #6a737d;
|
||||||
|
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.55;
|
||||||
|
transition: opacity 0.15s, background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap-toggle-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background: rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .tiptap-toggle-btn {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
color: #9b9c9d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .tiptap-toggle-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== Source Textarea ========== */
|
||||||
|
.tiptap-source-textarea {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 24px 32px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #2c2c2c;
|
||||||
|
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.7;
|
||||||
|
resize: none;
|
||||||
|
outline: none;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap-source-textarea::placeholder {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .tiptap-source-textarea {
|
||||||
|
color: #dadadb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .tiptap-source-textarea::placeholder {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user