tiptiap3如何实现编辑器内部嵌套多个富文本编辑器

根据文档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

相关推荐
溪饱鱼43 分钟前
主动与被动AI交互范式
前端·后端·aigc
我叫黑大帅44 分钟前
如何实现UniApp登录拦截?
前端·javascript·vue.js
写代码的皮筏艇1 小时前
Sequelize 详细指南
前端·后端
北辰alk1 小时前
解锁Vue组件通信新姿势:provide/inject深度解析
vue.js
用户600071819101 小时前
【翻译】我们如何打造v0版iOS应用
前端
编程猪猪侠1 小时前
打造高灵活度动态表单:基于 React + Ant Design 的 useDynamicForm hooks 实现思路
前端·react.js·前端框架
阿民不加班1 小时前
【React】使用browser-image-compression在上传前压缩图片、react上传图片压缩
前端·javascript·react.js
前端_yu小白1 小时前
前端实现录音,获取流分析音量大小,设置相应的动画
前端·mediarecorder·录音·浏览器安全性检查·https部署
虎子_layor1 小时前
小程序登录到底是怎么工作的?一次请求背后的三方信任链
前端·后端