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 container: HTMLElement
|
||||
private options: EditorOptions
|
||||
// 源码模式相关状态
|
||||
private isSourceMode = false
|
||||
private sourceTextarea: HTMLTextAreaElement | null = null
|
||||
private toggleButton: HTMLButtonElement | null = null
|
||||
|
||||
constructor(container: HTMLElement, options: EditorOptions = {}) {
|
||||
this.container = container
|
||||
@ -35,6 +39,30 @@ class TiptapEditorInstance {
|
||||
el.className = 'tiptap-editor'
|
||||
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({
|
||||
element: el,
|
||||
extensions: [
|
||||
@ -114,9 +142,44 @@ class TiptapEditorInstance {
|
||||
}
|
||||
|
||||
getMarkdown(): string {
|
||||
// 源码模式下直接返回 textarea 内容,确保提交逻辑无需感知视图模式
|
||||
if (this.isSourceMode && this.sourceTextarea) {
|
||||
return this.sourceTextarea.value
|
||||
}
|
||||
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 {
|
||||
this.editor?.commands.setContent(content, { emitUpdate: false, contentType: 'markdown' })
|
||||
}
|
||||
@ -140,6 +203,10 @@ class TiptapEditorInstance {
|
||||
destroy(): void {
|
||||
this.editor?.destroy()
|
||||
this.editor = null
|
||||
// 清理源码模式相关引用(容器 innerHTML 已清空,DOM 会随之移除)
|
||||
this.sourceTextarea = null
|
||||
this.toggleButton = null
|
||||
this.isSourceMode = false
|
||||
this.container.innerHTML = ''
|
||||
}
|
||||
}
|
||||
|
||||
@ -425,3 +425,73 @@
|
||||
.dark .tiptap-editor ul[data-type="taskList"] li > label input[type="checkbox"] {
|
||||
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