值槽

值槽可让您在槽内存储除 Tiptap 内容之外的任何 JSON 值。将这些值显示为表单字段(例如文本输入和复选框),或构建您自己的自定义输入。

插入值槽

创建槽时,通过 mode: 'value' 创建值槽:

editor.commands.insertSlot({
  mode: 'value',
  kind: 'inline',
  id: 'priority',
  value: null,
  config: {
    label: 'Priority',
    placeholder: 'Choose a priority',
    input: {
      type: 'select',
      options: [
        { value: 'normal', label: 'Normal' },
        { value: 'urgent', label: 'Urgent' },
      ],
    },
  },
})

使用 kind: 'block' 将控件放入其自己的块中。如果没有 mode: 'value',插入会创建一个普通的内容槽。

值可以是任意有限 JSON:null、字符串、布尔值、数字、数组或对象。使用验证执行应用程序的约束。

内置输入

插槽扩展附带一组内置输入类型。您还可以创建自己的自定义输入。

设置 config.input.type 以选择控件。对于文本输入,省略 input。

类型控件存储值选项类型 (config.input.options)
text原生文本输入string未使用
textarea多行文本输入string未使用
number原生数字输入string,包括 ''未使用
date原生日期输入string,例如 '2026-10-14'未使用
checkbox原生复选框boolean未使用
select下拉选择string 选项值{ value: string; label: string }[]
radio单选按钮列表string 选项值{ value: string; label: string }[]

SlotInput.options 是可选的。只有下拉选择和单选按钮控件会使用它;它们存储所选选项的 value,而不是显示标签。

对于单选按钮组,将 config.input 设置为:

{
  type: 'radio',
  options: [
    { value: 'standard', label: 'Standard' },
    { value: 'express', label: 'Express' },
  ],
}

每个槽位都是一个独立的单选按钮组。对于多选,请使用自定义 NodeView,如演示的芯片选择器。

数字输入将值存储为字符串,因为浏览器可能会针对不完整的数字返回 ''。

读取和更新值

import { getSlot } from '@tiptap-pro/extension-slot'

editor.commands.setSlotValue({ id: 'priority', value: 'urgent' })
// Clearing a slot sets its value to null.
editor.commands.clearSlot({ id: 'priority' })

// Update multiple slot values in an atomic batch.
editor.commands.setSlotValues({
  values: [
    { id: 'priority', value: 'urgent' },
    { id: 'name', value: 'Alex' },
  ],
})

const slot = getSlot({ snapshot: editor.storage.slot.snapshot, id: 'priority' })
if (slot?.mode === 'value') console.log(slot.value)

插槽与协作兼容。启用协作后,槽的更新会与协作文档的其他用户实时同步。

混合自定义和默认 NodeViews

按照完整教程构建自定义切换按钮,包括其 UI:

在 addNodeView() 内选择渲染器并保留内置控件作为后备:

import { Slot, InlineValueSlot, createDefaultSlotValueView } from '@tiptap-pro/extension-slot'

const CustomValueSlot = InlineValueSlot.extend({
  addNodeView() {
    return (props) => {
      if (props.node.attrs.config?.input?.type === 'rating') {
        return createRatingView(props)
      }
      return createDefaultSlotValueView(props)
    }
  },
})

Slot.configure({ inlineValueSlot: CustomValueSlot })

createRatingView 是应用程序代码。该演示使用原生按钮来实现它。对块视图使用 BlockValueSlot 和 blockValueSlot 选项。保留原始节点名称和叶架构。请勿提供 contentDOM 或 NodeViewContent。

在自定义视图内,createSlotValueController({ editor, getPos }) 提供:

  • getValue() 和 setValue(value) 来读取和写入答案。
  • getState() 用于 value、config、editable 和 available 的稳定快照。
  • subscribe(listener) 在文档和权限更改后更新 UI。
  • focus() 当其输入接收焦点时选择插槽。
  • destroy() 用于在删除普通视图时释放订阅。

自定义视图应:

  • 当 editable 为 false 时禁用控件。
  • 在 setValue 返回 false(拒绝写入)后渲染当前状态。
  • 将输入事件保留在控件内并处理文本输入的 IME 组合。
  • 删除视图时释放订阅。

如果更改配置选择不同的渲染器,请从 NodeView 的 update 方法返回 false,以便 Tiptap 再次运行 addNodeView 的工厂。当输入类型更改时,默认视图会执行此操作。

使用普通 JavaScript 创建自定义输入

构建一个在开和关之间切换的按钮并存储布尔值。原生按钮提供 Enter 和 Space 键盘支持。

1. 创建 UI

创建 toggle-slot.js。使用值控制器进行读取、受保护的写入和权限更改:

import {
  BlockValueSlot,
  createDefaultSlotValueView,
  createSlotValueController,
} from '@tiptap-pro/extension-slot'

function createToggleView({ editor, getPos, node }) {
  const slot = createSlotValueController({ editor, getPos })
  const dom = document.createElement('div')
  dom.contentEditable = 'false'
  const button = document.createElement('button')
  button.type = 'button'
  dom.append(button)

  function render() {
    const { value, editable, available } = slot.getState()
    const on = value === true
    button.textContent = `${slot.getState().config?.label ?? 'Notifications'}: ${on ? 'On' : 'Off'}`
    button.setAttribute('aria-pressed', String(on))
    button.disabled = !available || !editable
  }

  button.addEventListener('focus', () => slot.focus())
  button.addEventListener('click', () => {
    slot.setValue(slot.getValue() !== true)
    render() // Also restore the current state if the write was refused.
  })
  const unsubscribe = slot.subscribe(render)
  render()

  return {
    dom,
    update(nextNode) {
      return nextNode.type === node.type && nextNode.attrs.config?.input?.type === 'toggle'
    },
    stopEvent: () => true,
    ignoreMutation: () => true,
    destroy() {
      unsubscribe()
      slot.destroy()
    },
  }
}

subscribe 在值或权限更改时刷新按钮。它不会立即调用 render,因此请您自己调用一次。 focus()选择槽位;浏览器将焦点保持在按钮上。

2. 注册自定义渲染器

将其添加到同一文件中。处理 toggle 输入并保留内置输入的默认渲染器:

export const ToggleSlot = BlockValueSlot.extend({
  addNodeView() {
    return (props) =>
      props.node.attrs.config?.input?.type === 'toggle'
        ? createToggleView(props)
        : createDefaultSlotValueView(props)
  },
})

当输入类型更改时,视图的 update 返回 false,以便 Tiptap 可以选择另一个渲染器。保留叶架构:不添加 contentDOM。

3. 配置编辑器并插入插槽

import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import { Slot } from '@tiptap-pro/extension-slot'
import { ToggleSlot } from './toggle-slot.js'

const editor = new Editor({
  element: document.querySelector('#editor'),
  extensions: [StarterKit, Slot.configure({ blockValueSlot: ToggleSlot })],
  content: '<p>Notification preferences</p>',
})

editor.commands.insertSlot({
  mode: 'value',
  kind: 'block',
  id: 'notifications',
  value: false,
  config: { label: 'Notifications', input: { type: 'toggle' } },
})

将编辑器安装到 <div id="editor"></div> 中。单击该按钮或将其聚焦并按空格键:attrs.value 在 true 和 false 之间切换。撤消可恢复之前的答案。使用内容保护,锁定的插槽会自动禁用该按钮。

toggle 是应用程序的输入类型,而不是内置的。存储的值仍然是普通的 JSON,导出的 HTML 渲染静态答案。

使用 React 创建自定义输入

使用 React 组件构建与 JavaScript 教程 相同的布尔切换。该按钮支持 Enter 和 Space,无需自定义键盘处理程序。

使用 @tiptap/react 中的 ReactNodeViewRenderer 来渲染组件。 slot 包本身没有 React 或 Vue 依赖。

1. 创建组件

创建 ToggleSlot.jsx。将控制器与 useSyncExternalStore 连接,以便文档和权限更改刷新 UI:

import { useMemo, useSyncExternalStore } from 'react'
import { NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react'
import {
  BlockValueSlot,
  createDefaultSlotValueView,
  createSlotValueController,
} from '@tiptap-pro/extension-slot'

function ToggleView({ editor, getPos }) {
  const slot = useMemo(() => createSlotValueController({ editor, getPos }), [editor, getPos])
  const { value, config, editable, available } = useSyncExternalStore(
    slot.subscribe,
    slot.getState,
    slot.getState,
  )
  const on = value === true

  return (
    <NodeViewWrapper contentEditable={false}>
      <button
        type="button"
        aria-pressed={on}
        disabled={!available || !editable}
        onFocus={() => slot.focus()}
        onClick={() => slot.setValue(slot.getValue() !== true)}
      >
        {config?.label ?? 'Notifications'}: {on ? 'On' : 'Off'}
      </button>
    </NodeViewWrapper>
  )
}
  • setValue 使用与内置控件相同的受保护命令。
  • 该按钮渲染控制器状态,因此拒绝写入使其答案保持不变。
  • React 在卸载时取消订阅。当最后一个订阅者离开时,控制器会释放其编辑器监听器,包括在 React Strict 模式清理期间。

不要添加 NodeViewContent:值槽没有可编辑的子项。

2. 注册 React 渲染器

将其添加到 ToggleSlot.jsx。仅对 toggle 输入使用 React 并保留内置控件:

export const ToggleSlot = BlockValueSlot.extend({
  addNodeView() {
    const renderToggle = ReactNodeViewRenderer(ToggleView, {
      update: ({ newNode, updateProps }) => {
        if (newNode.attrs.config?.input?.type !== 'toggle') return false
        updateProps()
        return true
      },
    })

    return (props) =>
      props.node.attrs.config?.input?.type === 'toggle'
        ? renderToggle(props)
        : createDefaultSlotValueView(props)
  },
})

当输入类型更改时返回 false 让 Tiptap 选择另一个渲染器。

3. 安装编辑器并插入插槽

import { EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { Slot } from '@tiptap-pro/extension-slot'
import { ToggleSlot } from './ToggleSlot.jsx'

export function NotificationEditor() {
  const editor = useEditor({
    extensions: [StarterKit, Slot.configure({ blockValueSlot: ToggleSlot })],
    content: '<p>Notification preferences</p>',
    immediatelyRender: false,
  })

  return (
    <>
      <button
        type="button"
        disabled={!editor}
        onClick={() =>
          editor.commands.insertSlot({
            mode: 'value',
            kind: 'block',
            id: 'notifications',
            value: false,
            config: { label: 'Notifications', input: { type: 'toggle' } },
          })
        }
      >
        Add notifications toggle
      </button>
      <EditorContent editor={editor} />
    </>
  )
}

单击 添加通知切换 一次,然后使用鼠标、Enter 或 Space 激活切换。其attrs.value在true和false之间切换;撤消可恢复之前的答案。插入附加开关时使用唯一的插槽 ID。

添加内容保护 以限制编辑。该组件自动遵循控制器的 editable 状态。

使用 Vue? 使用 VueNodeViewRenderer 代替。在设置中创建控制器,将 getState() 复制到 shallowRef,更新 subscribe 中的引用,然后取消订阅并销毁 onBeforeUnmount 中的控制器。渲染没有 NodeViewContent 的 NodeViewWrapper。

验证

验证会检查答案,但不会阻止用户起草无效值。例如,需要一个名称并在提交前进行检查:

import { isSlotValidationCurrent, validateEditorSlots } from '@tiptap-pro/extension-slot'

editor.commands.insertSlot({
  mode: 'value',
  kind: 'block',
  id: 'name',
  value: null,
  config: { label: 'Your name', required: true, input: { type: 'text' } },
})

// Run in your application's submit handler.
const result = await validateEditorSlots({ editor })
if (isSlotValidationCurrent({ editor, result })) {
  if (result.valid) {
    save(editor.getJSON()) // Your application's persistence function.
  } else {
    console.log(result.issues) // Display these beside the affected inputs.
  }
}
  • 空名称或仅包含空格的名称无法通过验证,但用户仍然可以起草它。
  • required 将 null、空白字符串和空数组视为空。 false 和 0 算作答案;对必须选中的复选框使用自定义验证器。
  • 选择和单选答案必须与选项值匹配。 allowedBlocks 和 defaultContent 等内容设置不适用于值槽。

有关自定义验证器和取消的运行,请参阅提交前验证。

内容保护

使用内容保护让用户填写答案,同时保护文档的结构和槽位配置。插入插槽后,仅允许编辑 name 插槽:

import { createSlotFillingPolicy } from '@tiptap-pro/extension-slot'

// Requires ContentProtection in the editor's extensions.
editor.commands.setContentProtectionPolicy({
  policy: createSlotFillingPolicy({ editableSlotIds: ['name'] }),
})

用户可以更改插槽的值,但不能删除它或更改其配置。使用值控制器时,内置和自定义输入遵循相同的权限。

关于保护设置,请参见仅填充编辑。