使用 Vue 创建节点视图

如果你习惯使用 Vue,单纯用 Vanilla JavaScript 可能感觉比较复杂。好消息是:你也可以在节点视图中使用常规的 Vue 组件。只需要知道一点点相关知识,下面我们逐步讲解。

渲染 Vue 组件

在编辑器内部渲染 Vue 组件的步骤如下:

  1. 创建节点扩展
  2. 创建一个 Vue 组件
  3. 将该组件传递给提供的 VueNodeViewRenderer
  4. 使用 addNodeView() 注册
  5. 配置 Tiptap 以使用你的新节点扩展

你的节点扩展大致可以这样写:

import { Node } from '@tiptap/core'
import { VueNodeViewRenderer } from '@tiptap/vue-2'
import Component from './Component.vue'

export default Node.create({
  // 配置项 …

  addNodeView() {
    return VueNodeViewRenderer(Component)
  },
})

为了让它正常工作,少许“魔法”操作是需要的。但不用担心,我们提供了一个封装组件,帮助你轻松入门。记得将其添加到你的自定义 Vue 组件中,示范如下:

<template>
  <node-view-wrapper> Vue 组件 </node-view-wrapper>
</template>

懂了吗?让我们一起来看看实际效果。以下示例可以直接复制使用。

不过,这个组件本身不与编辑器交互。现在就来连接它。

访问节点属性

你在节点扩展中使用的 VueNodeViewRenderer 会向你的自定义 Vue 组件传递一些非常有用的 props。其中之一是 node 属性。你可以将以下代码添加到你的 Vue 组件中,以便直接访问节点:

props: {
  node: {
    type: Object,
    required: true,
  },
},

这样你就可以在 Vue 组件中访问节点的属性了。假设你在节点扩展中添加了一个 attribute,比如 count(像刚才的例子中那样),你可以这样访问它:

this.node.attrs.count

更新节点属性

你甚至可以通过传递给组件的 updateAttributes 方法,更新节点的属性。只需在你的组件中添加如下代码:

props: {
  updateAttributes: {
    type: Function,
    required: true,
  },
},

然后调用这个函数,并传入一个包含新的属性值的对象:

this.updateAttributes({
  count: this.node.attrs.count + 1,
})

是的,这一切都是响应式的,通信非常顺畅,不是挺好的吗?

添加内容可编辑区域

还有一个叫 NodeViewContent 的组件,可以帮助你为节点视图添加可编辑内容。示例如下:

<template>
  <node-view-wrapper class="dom">
    <node-view-content class="content-dom" />
  </node-view-wrapper>
</template>

<script>
  import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2'

  export default {
    components: {
      NodeViewWrapper,
      NodeViewContent,
    },
  }
</script>

不一定非要使用这些 class 属性,感觉可以删除或替换为其他类名。试试看下面的示例:

请记住,这些内容由 Tiptap 渲染。这意味着你需要在节点扩展中声明允许的内容类型,比如通过 content: 'inline*'(这是我们上面示例中的设置)。

NodeViewWrapperNodeViewContent 组件会渲染 <div>(内联节点为 <span>),但你也可以修改这个行为。例如 <node-view-content as="p"> 只要是块级标签就可以,默认会渲染为 <p>。但有个限制:标签在运行时不能更改。

在文本选择时更新 selected

默认情况下,只有当节点本身通过 NodeSelection 被选中时,selected prop 才会变成 true。这对原子节点效果很好,但当光标只是放在一个包含文本内容的节点内部时,它不会触发。如果你希望 selected 在这种情况下也能反映出来,请在 VueNodeViewRenderer 上启用 selectedOnTextSelection 选项:

return VueNodeViewRenderer(Component, { selectedOnTextSelection: true })

启用这个选项后,只要 TextSelection 完全位于节点范围内,selected 也会变成 true——例如,当光标放在节点内容中的某个位置时。只与节点部分重叠的选区不算作已选中。

所有可用 props

对于高级用例,我们会传递更多 props 给你的组件。

editor

编辑器实例。

node

当前节点。

decorations

装饰的数组。

selected

当当前节点视图存在 NodeSelection 时为 true。如果启用了 selectedOnTextSelection 选项,并且 TextSelection 完全位于节点内部时,也为 true

extension

访问节点扩展,例如用来获取配置项。

getPos()

获取当前节点在文档中的位置。

updateAttributes()

更新当前节点的属性。

deleteNode()

删除当前节点。

以下是全部可用 props 的完整列表:

<template>
  <node-view-wrapper />
</template>

<script>
  import { NodeViewWrapper } from '@tiptap/vue-2'

  export default {
    components: {
      NodeViewWrapper,
    },

    props: {
      // 编辑器实例
      editor: {
        type: Object,
      },

      // 当前节点
      node: {
        type: Object,
      },

      // 装饰数组
      decorations: {
        type: Array,
      },

      // 当前节点视图存在 `NodeSelection` 时为 `true`,或者
      // 在启用 `selectedOnTextSelection` 选项且 `TextSelection`
      // 完全位于节点内部时为 `true`
      selected: {
        type: Boolean,
      },

      // 访问节点扩展(例如获取配置)
      extension: {
        type: Object,
      },

      // 获取当前节点在文档中的位置
      getPos: {
        type: Function,
      },

      // 更新当前节点的属性
      updateAttributes: {
        type: Function,
      },

      // 删除当前节点
      deleteNode: {
        type: Function,
      },
    },
  }
</script>

如果你希望获取所有 props(支持 TypeScript),可以直接引入全部,例如:

// Vue 3
import { defineComponent } from 'src/content/editor/extensions/custom-extensions/node-views/vue.mdx'
import { nodeViewProps } from '@tiptap/vue-3'
export default defineComponent({
  props: nodeViewProps,
})

// Vue 2
import Vue from 'src/content/editor/extensions/custom-extensions/node-views/vue.mdx'
import { nodeViewProps } from '@tiptap/vue-2'
export default Vue.extend({
  props: nodeViewProps,
})

拖拽支持

若要让你的节点视图支持拖拽,只需在扩展中设置 draggable: true,并将 data-drag-handle 添加到作为拖拽手柄的 DOM 元素即可。