根据文档Adding a content editable,如果希望在编辑器中再嵌套一个富文本编辑就会使用到这个配置:
typescript
addNodeView() {
// Create a container for the node view
const dom = document.createElement('div')
// Give other elements containing text `contentEditable = false`
const label = document.createElement('span')
label.innerHTML = 'Node view'
label.contentEditable = false
// Create a container for the content
const content = document.createElement('div')
// Append all elements to the node view container
dom.append(label, content)
return {
// Pass the node view container ...
dom,
// ... and the content container:
contentDOM: content,
}
}
如果使用了框架就会是一个NodeViewWrapper包裹一个NodeViewContent,是一对一的关系,在官方的Multiple NodeViewContent · ueberdosis/tiptap · Discussion #2106也提到了这个事情
显而易见的是一个NodeViewWrapper对应一个扩展,那么当我们需要给一个扩展中添加多个富文本编辑时,唯一的方案就是扩展中嵌套扩展。就比如说官方的@tiptap/extension-table表格扩展。
在我细细评味了官方的表格扩展后,成功实现了我们自己的两栏布局:

并且具备一个正常的富文本编辑器的全部功能!
左边的我们称为组件A ,右边的我们称为组件B(其实完全可以不用拆分的,因为内部代码完全一样)
现在我们实现其中一个组件,根据文档所示:
如果是在vue中需要如下代码,组件A
typescript
import { mergeAttributes, Node } from '@tiptap/core'
import { VueNodeViewRenderer } from '@tiptap/vue-3'
import TwoEditorLayoutTemplateA from './TwoEditorLayoutTemplateA.vue'
export default Node.create({
name: "twoEditorLayoutTemplateA",
content: "inline*",
atom: false,
parseHTML() {
return [
{
tag: "twoEditorLayoutTemplateA",
},
];
},
renderHTML() {
return ["twoEditorLayoutTemplateA", {}, 0];
},
addNodeView() {
return VueNodeViewRenderer(TwoEditorLayoutTemplateA);
},
}),
typescript
<template>
<node-view-wrapper class="editor-a">
<div class="editor-a">
<node-view-content class="content is-editable" />
</div>
</node-view-wrapper>
</template>
<script setup lang="ts">
import { NodeViewWrapper, nodeViewProps, NodeViewContent } from "@tiptap/vue-3";
const props = defineProps(nodeViewProps);
</script>
<style scoped>
.editor-a {
flex: 1;
min-height: 240px;
border: 1px solid #ddd;
border-right: 1px solid #ddd;
padding: 8px;
box-sizing: border-box;
border-radius: 4px;
}
.editor-a .content {
min-height: 100%;
}
</style>
组件B为
typescript
import { mergeAttributes, Node } from '@tiptap/core'
import { VueNodeViewRenderer } from '@tiptap/vue-3'
import TwoEditorLayoutTemplateB from './TwoEditorLayoutTemplateB.vue'
export default Node.create({
name: "twoEditorLayoutTemplateB",
content: "inline*",
atom: false,
parseHTML() {
return [
{
tag: "twoEditorLayoutTemplateB",
},
];
},
renderHTML() {
return ["twoEditorLayoutTemplateB", {}, 0];
},
addNodeView() {
return VueNodeViewRenderer(TwoEditorLayoutTemplateB);
},
}),
typescript
<template>
<node-view-wrapper class="editor-b">
<div class="editor-b">
<node-view-content class="content is-editable" />
</div>
</node-view-wrapper>
</template>
<script setup lang="ts">
import { NodeViewWrapper, nodeViewProps, NodeViewContent } from "@tiptap/vue-3";
const props = defineProps(nodeViewProps);
</script>
<style scoped>
.editor-b {
flex: 1;
min-height: 240px;
border: 1px solid #ddd;
padding: 8px;
box-sizing: border-box;
border-radius: 4px;
}
.editor-b .content {
min-height: 100%;
}
</style>
显而易见的是,这个组件B基本和组件A就是一样的代码,也就样式不太一样
然后就需要把这个两个扩展注册到全局中去
typescript
<template>
<editor-content :editor="editor" />
</template>
<script>
import StarterKit from '@tiptap/starter-kit'
import { Editor, EditorContent } from '@tiptap/vue-3'
import twoEditorLayoutTemplateA from './twoEditorLayoutTemplateA.js'
import twoEditorLayoutTemplateB from './twoEditorLayoutTemplateB.js'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [StarterKit, VueComponent],
content: ``,
})
},
beforeUnmount() {
this.editor.destroy()
},
}
</script>
<style lang="scss">
/* Basic editor styles */
.tiptap {
:first-child {
margin-top: 0;
}
/* List styles */
ul,
ol {
padding: 0 1rem;
margin: 1.25rem 1rem 1.25rem 0.4rem;
li p {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
}
/* Heading styles */
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
}
h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code {
background-color: var(--purple-light);
border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
}
pre {
background: var(--black);
border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
}
blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem;
}
hr {
border: none;
border-top: 1px solid var(--gray-2);
margin: 2rem 0;
}
}
</style>
但是现在还是不够的,因为他们还是独立的,没有一个大的扩展进行统筹,我们现在再写一个大的扩展来完成这个功能
typescript
// 两栏布局扩展父节点(vue组件版本)
Node.create({
name: "twoEditorLayoutTemplate",
group: "block",
atom: false,
// content为上面两个扩展的name
content: "twoEditorLayoutTemplateA twoEditorLayoutTemplateB",
parseHTML() {
return [
{
tag: "div[data-two-editor-layout-template]",
},
];
},
renderHTML() {
return ["div", { "data-two-editor-layout-template": "", class: "two-editor-layout" }, 0];
},
addCommands() {
return {
insertTwoEditorLayoutTemplate:
() =>
({ commands }: CommandProps) => {
commands.insertContent({
type: "twoEditorLayoutTemplate",
content: [
{
type: "twoEditorLayoutTemplateA",
},
{
type: "twoEditorLayoutTemplateB",
},
],
});
return true;
},
} as Partial<RawCommands>;
},
}),
然后,我们最后在addCommands中注册的insertTwoEditorLayoutTemplate函数作为在编辑器中插入的最终函数,当调用这个函数时就可以把这最终的大的扩展给写到编辑器中去了
这就是最终的解决方案了,笔者也是翻阅了tiptap3的各种源码和实现,以及掘金上大量的文章以及官方的issues和discussions后才灵光一闪,完成了最后的实现~
推荐好文:
Tiptap 3.0 正式发布!你必须知道的 20 个重大变化 🚀🚀🚀
Tiptap 深度教程(四):终极定制 - 从零创建你的专属扩展
Multiple NodeViewContent in a Single NodeViewWrapper · ueberdosis/tiptap · Discussion #7286