EditorDragHandle

GitHub
一个用于在编辑器中重新排序和选择块的可拖动句柄。

用法

EditorDragHandle 组件使用 @tiptap/extension-drag-handle-vue-3 软件包,为重排编辑器区块提供拖放功能。

它必须在 Editor 组件的默认插槽内使用,以便能够访问编辑器实例。

它继承了 Button 组件,因此您可以传入任何属性,例如 colorvariantsize 等。

<script setup lang="ts">
const value = ref(`# Drag Handle

Hover over the left side of this block to see the drag handle appear and reorder blocks.`)
</script>

<template>
  <UEditor v-slot="{ editor }" v-model="value" content-type="markdown" class="w-full min-h-21">
    <UEditorDragHandle :editor="editor" />
  </UEditor>
</template>
在 TipTap 文档中了解更多关于拖拽句柄(Drag Handle)扩展的信息。

Icon

使用 icon 属性来自定义拖拽句柄图标。

<template>
  <UEditor v-slot="{ editor }">
    <UEditorDragHandle :editor="editor" icon="i-lucide-move" />
  </UEditor>
</template>
你可以在 app.config.tsui.icons.drag 键下全局自定义此图标。
你可以在 vite.config.tsui.icons.drag 键下全局自定义此图标。

选项

使用 options prop 来自定义定位行为,使用Floating UI options.

偏移量会自动计算,以便在小区块中居中显示句柄,在较高的区块中将其与顶部对齐。
<template>
  <UEditor v-slot="{ editor }">
    <UEditorDragHandle
      :editor="editor"
      :options="{
        placement: 'left'
      }"
    />
  </UEditor>
</template>

示例

带有下拉菜单

使用默认插槽添加一个 DropdownMenu(下拉菜单),以实现区块级操作,如复制、删除、上移/下移或将区块转换为不同类型。

监听 @node-change 事件来追踪当前悬停的节点及其位置,然后使用 editor.chain().setMeta('lockDragHandle', open).run() 在菜单打开时锁定句柄位置。

<script setup lang="ts">
import { upperFirst } from 'scule'
import type { DropdownMenuItem } from '@nuxt/ui'
import { mapEditorItems } from '@nuxt/ui/utils/editor'
import type { Editor, JSONContent } from '@tiptap/vue-3'

const value = ref(`Hover over the left side to see both drag handle and menu button.

Click the menu to see block actions. Try duplicating or deleting a block.`)

const selectedNode = ref<{ node: JSONContent, pos: number }>()

const items = (editor: Editor): DropdownMenuItem[][] => {
  if (!selectedNode.value?.node?.type) {
    return []
  }

  return mapEditorItems(editor, [[
    {
      type: 'label',
      label: upperFirst(selectedNode.value.node.type)
    },
    {
      label: 'Turn into',
      icon: 'i-lucide-repeat-2',
      children: [
        { kind: 'paragraph', label: 'Paragraph', icon: 'i-lucide-type' },
        { kind: 'heading', level: 1, label: 'Heading 1', icon: 'i-lucide-heading-1' },
        { kind: 'heading', level: 2, label: 'Heading 2', icon: 'i-lucide-heading-2' },
        { kind: 'heading', level: 3, label: 'Heading 3', icon: 'i-lucide-heading-3' },
        { kind: 'heading', level: 4, label: 'Heading 4', icon: 'i-lucide-heading-4' },
        { kind: 'bulletList', label: 'Bullet List', icon: 'i-lucide-list' },
        { kind: 'orderedList', label: 'Ordered List', icon: 'i-lucide-list-ordered' },
        { kind: 'blockquote', label: 'Blockquote', icon: 'i-lucide-text-quote' },
        { kind: 'codeBlock', label: 'Code Block', icon: 'i-lucide-square-code' }
      ]
    },
    {
      kind: 'clearFormatting',
      pos: selectedNode.value?.pos,
      label: 'Reset formatting',
      icon: 'i-lucide-rotate-ccw'
    }
  ], [
    {
      kind: 'duplicate',
      pos: selectedNode.value?.pos,
      label: 'Duplicate',
      icon: 'i-lucide-copy'
    },
    {
      label: 'Copy to clipboard',
      icon: 'i-lucide-clipboard',
      onSelect: async () => {
        if (!selectedNode.value) return

        const pos = selectedNode.value.pos
        const node = editor.state.doc.nodeAt(pos)
        if (node) {
          await navigator.clipboard.writeText(node.textContent)
        }
      }
    }
  ], [
    {
      kind: 'moveUp',
      pos: selectedNode.value?.pos,
      label: 'Move up',
      icon: 'i-lucide-arrow-up'
    },
    {
      kind: 'moveDown',
      pos: selectedNode.value?.pos,
      label: 'Move down',
      icon: 'i-lucide-arrow-down'
    }
  ], [
    {
      kind: 'delete',
      pos: selectedNode.value?.pos,
      label: 'Delete',
      icon: 'i-lucide-trash'
    }
  ]]) as DropdownMenuItem[][]
}
</script>

<template>
  <UEditor
    v-slot="{ editor }"
    v-model="value"
    content-type="markdown"
    class="w-full min-h-19"
  >
    <UEditorDragHandle v-slot="{ ui }" :editor="editor" @node-change="selectedNode = $event">
      <UDropdownMenu
        v-slot="{ open }"
        :modal="false"
        :items="items(editor)"
        :content="{ side: 'left' }"
        :ui="{ content: 'w-48', label: 'text-xs' }"
        @update:open="editor.chain().setMeta('lockDragHandle', $event).run()"
      >
        <UButton
          icon="i-lucide-grip-vertical"
          color="neutral"
          variant="ghost"
          active-variant="soft"
          size="sm"
          :active="open"
          :class="ui.handle()"
        />
      </UDropdownMenu>
    </UEditorDragHandle>
  </UEditor>
</template>
此示例使用来自 @nuxt/ui/utils/editormapEditorItems 工具函数,自动将处理程序类型(如 duplicatedeletemoveUp 等)映射到对应的编辑器命令,并进行适当的状态管理。

带有建议菜单

使用默认插槽在拖拽句柄旁边添加一个 Button(按钮),以打开 EditorSuggestionMenu(编辑器建议菜单)。

调用 onClick 插槽函数获取当前节点位置,然后使用 handlers.suggestion?.execute(editor, { pos: node?.pos }).run() 在该位置插入新区块。

<script setup lang="ts">
import type { EditorSuggestionMenuItem } from '@nuxt/ui'

const value = ref(`Click the plus button to open the suggestion menu and add new blocks.

The button appears when hovering over blocks.`)

const suggestionItems: EditorSuggestionMenuItem[][] = [[{
  kind: 'heading',
  level: 1,
  label: 'Heading 1',
  icon: 'i-lucide-heading-1'
}, {
  kind: 'heading',
  level: 2,
  label: 'Heading 2',
  icon: 'i-lucide-heading-2'
}, {
  kind: 'bulletList',
  label: 'Bullet List',
  icon: 'i-lucide-list'
}, {
  kind: 'blockquote',
  label: 'Blockquote',
  icon: 'i-lucide-text-quote'
}]]
</script>

<template>
  <UEditor
    v-slot="{ editor, handlers }"
    v-model="value"
    content-type="markdown"
    class="w-full min-h-35"
    :ui="{ base: 'p-8 sm:px-16' }"
  >
    <UEditorDragHandle v-slot="{ ui, onClick }" :editor="editor">
      <UButton
        icon="i-lucide-plus"
        color="neutral"
        variant="ghost"
        size="sm"
        :class="ui.handle()"
        @click="(e) => {
          e.stopPropagation()

          const selected = onClick()
          handlers.suggestion?.execute(editor, { pos: selected?.pos }).run()
        }"
      />

      <UButton
        icon="i-lucide-grip-vertical"
        color="neutral"
        variant="ghost"
        size="sm"
        :class="ui.handle()"
      />
    </UEditorDragHandle>

    <UEditorSuggestionMenu :editor="editor" :items="suggestionItems" />
  </UEditor>
</template>

API

属性

属性默认值类型
as'button'any

此组件在不是链接时应呈现的元素或组件。

editor编辑器
图标appConfig.ui.icons.dragany
color'neutral'"错误" | "中性" | "主要" | "次要" | "成功" | "信息" | "警告"
variant'ghost'"ghost" | "solid" | "outline" | "soft" | "subtle" | "link"
options{ strategy: 'absolute', placement: 'left-start' }FloatingUIOptions

拖拽句柄的定位选项。这些选项将传递给 Floating UI,包括 placement、offset、flip、shift、size、autoPlacement、hide 和 inline 中间件的选项。

pluginKeystring | PluginKey<any>
nestedOptionsNormalizedNestedOptions
onElementDragStart(e: DragEvent): void
onElementDragEnd(e: DragEvent): void
getReferencedVirtualElement(): VirtualElement | null
nestedboolean | NestedOptions

为嵌套内容(列表项、块引用等)启用拖拽句柄。

启用后,拖拽句柄将出现在嵌套区块上,而不仅仅是顶层区块。基于规则的评分系统会根据光标位置和配置的规则确定目标节点。

autofocusfalse | true | "true" | "false"
disabledboolean
namestring
type'button'"reset" | "submit" | "button"

当不是链接时,按钮的类型。

labelstring
activeColor"错误" | "中性" | "主要" | "次要" | "成功" | "信息" | "警告"
activeVariant"ghost" | "solid" | "outline" | "soft" | "subtle" | "link"
尺寸'sm'"sm" | "xs" | "md" | "lg" | "xl"
正方形boolean

以四边等距内边距渲染按钮。

blockboolean

渲染全宽按钮。

loadingAutoboolean

根据 @click promise 状态自动设置加载状态

avatarAvatarProps

在左侧显示头像。

前置boolean

当为 true 时,图标将显示在左侧。

leadingIconany

在左侧显示图标。

尾部boolean

当为 true 时,图标将显示在右侧。

trailingIconany

在右侧显示图标。

loadingboolean

当为 true 时,将显示加载图标。

loadingIconappConfig.ui.icons.loadingany

loading prop 为 true 时显示的图标。

ui{ root?: ClassNameValue; handle?: ClassNameValue; } & { base?: ClassNameValue; label?: ClassNameValue; leadingIcon?: ClassNameValue; leadingAvatar?: ClassNameValue; leadingAvatarSize?: ClassNameValue; trailingIcon?: ClassNameValue; }

插槽

插槽类型
default{ ui: object; }

事件

事件类型
nodeChange[{ node: JSONContent; pos: number; }]
hover[{ node: JSONContent; pos: number; }]

主题

app.config.ts
export default defineAppConfig({
  ui: {
    editorDragHandle: {
      slots: {
        root: 'hidden sm:flex items-center justify-center transition-all duration-200 ease-out',
        handle: 'cursor-grab px-1'
      }
    }
  }
})
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      ui: {
        editorDragHandle: {
          slots: {
            root: 'hidden sm:flex items-center justify-center transition-all duration-200 ease-out',
            handle: 'cursor-grab px-1'
          }
        }
      }
    })
  ]
})

更新日志

v4.5.0
  • c9704— feat(Theme): 新组件 (#4387)
  • ed601— feat(EditorDragHandle): 代理 nested / nestedOptions 属性并触发 hover 事件 (#5960)
v4.3.0
  • 1b850— fix(EditorDragHandle): 添加缺失的 UButton 导入
  • 38765— feat(Editor): 新组件 (#5407)