EditorDragHandle
用法
EditorDragHandle 组件使用 @tiptap/extension-drag-handle-vue-3 软件包,为重排编辑器区块提供拖放功能。
它继承了 Button 组件,因此您可以传入任何属性,例如 color、variant、size 等。
<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>
Icon
使用 icon 属性来自定义拖拽句柄图标。
<template>
<UEditor v-slot="{ editor }">
<UEditorDragHandle :editor="editor" icon="i-lucide-move" />
</UEditor>
</template>
选项
使用 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/editor 的 mapEditorItems 工具函数,自动将处理程序类型(如 duplicate、delete、moveUp 等)映射到对应的编辑器命令,并进行适当的状态管理。带有建议菜单
使用默认插槽在拖拽句柄旁边添加一个 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.drag | any |
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 中间件的选项。
|
pluginKey | string | PluginKey<any> | |
nestedOptions | NormalizedNestedOptions
| |
onElementDragStart | (e: DragEvent): void | |
onElementDragEnd | (e: DragEvent): void | |
getReferencedVirtualElement | (): VirtualElement | null | |
nested | boolean | NestedOptions为嵌套内容(列表项、块引用等)启用拖拽句柄。 启用后,拖拽句柄将出现在嵌套区块上,而不仅仅是顶层区块。基于规则的评分系统会根据光标位置和配置的规则确定目标节点。
| |
autofocus | false | true | "true" | "false" | |
disabled | boolean | |
name | string | |
type | 'button' | "reset" | "submit" | "button"当不是链接时,按钮的类型。 |
label | string | |
activeColor | "错误" | "中性" | "主要" | "次要" | "成功" | "信息" | "警告" | |
activeVariant | "ghost" | "solid" | "outline" | "soft" | "subtle" | "link" | |
尺寸 | 'sm' | "sm" | "xs" | "md" | "lg" | "xl" |
正方形 | boolean以四边等距内边距渲染按钮。 | |
block | boolean渲染全宽按钮。 | |
loadingAuto | boolean根据 | |
avatar | AvatarProps在左侧显示头像。
| |
前置 | boolean当为 | |
leadingIcon | any在左侧显示图标。 | |
尾部 | boolean当为 | |
trailingIcon | any在右侧显示图标。 | |
loading | boolean当为 | |
loadingIcon | appConfig.ui.icons.loading | any当 |
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; }] |
主题
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'
}
}
}
})
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'
}
}
}
})
]
})