[object Object]
Bearbeiten
Die EditorDragHandle-Komponente bietet Drag-and-Drop-Funktionen zum Umordnen von Editorblöcken mit dem Paket @tiptap/extension-drag-handle-vue-3.
Es erweitert die Komponente Button, so dass Sie jede Eigenschaft wie color, variant, size usw. übergeben können.
<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><script setup lang="ts">
import { ref } from 'vue'
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 (englisch)
Verwenden Sie die icon-Prop, um das Drag Handle-Symbol anzupassen.
<template>
<UEditor v-slot="{ editor }">
<UEditorDragHandle :editor="editor" icon="i-lucide-move" />
</UEditor>
</template>
Options (englisch)
Verwenden Sie die options-Prop, um das Positionierungsverhalten mit Floating UI-Optionen anzupassen.
<template>
<UEditor v-slot="{ editor }">
<UEditorDragHandle
:editor="editor"
:options="{
placement: 'left'
}"
/>
</UEditor>
</template>
Examples (Beispiele)
Mit dem Dropdown-Menü
Verwenden Sie den Standardsteckplatz, um ein DropdownMenu mit Aktionen auf Blockebene wie Duplizieren, Löschen, Aufwärts-/Abwärtsbewegen oder Transformieren von Blöcken in verschiedene Typen hinzuzufügen.
Hören Sie sich das @node-change-Ereignis an, um den derzeit schwebenden Knoten und seine Position zu verfolgen, und verwenden Sie dann editor.chain().setMeta('lockDragHandle', open).run(), um die Handle-Position zu sperren, während das Menü geöffnet ist.
<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>
<script setup lang="ts">
import { ref } from 'vue'
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>
mapEditorItems von @nuxt/ui/utils/editor verwendet, um Handlertypen (wie duplicate, delete, moveUp usw.) automatisch den entsprechenden Editorbefehlen mit korrekter Zustandsverwaltung zuzuordnen.Mit Vorschlagsmenu
Verwenden Sie den Standardsteckplatz, um einen Button neben dem Ziehpunkt hinzuzufügen, um den EditorSuggestionMenu zu öffnen.
Rufen Sie die Slot-Funktion onClick auf, um die aktuelle Knotenposition zu ermitteln, und verwenden Sie dann handlers.suggestion?.execute(editor, { pos: node?.pos }).run(), um neue Blöcke an dieser Position einzufügen.
<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' }"
>
<UEditorSuggestionMenu :editor="editor" :items="suggestionItems" />
<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>
</UEditor>
</template>
<script setup lang="ts">
import { ref } from 'vue'
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' }"
>
<UEditorSuggestionMenu :editor="editor" :items="suggestionItems" />
<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>
</UEditor>
</template>
API (Englisch)
Props (englisch)
| Prop | Default | Type |
|---|---|---|
as | 'button' | anyThe element or component this component should render as when not a link. |
editor | Editor | |
icon | appConfig.ui.icons.drag | any |
color | 'neutral' | "error" | "neutral" | "primary" | "secondary" | "success" | "info" | "warning" |
variant | 'ghost' | "ghost" | "solid" | "outline" | "soft" | "subtle" | "link" |
options | { strategy: 'absolute', placement: 'left-start' } | FloatingUIOptionsThe options for positioning the drag handle. Those are passed to Floating UI and include options for the placement, offset, flip, shift, size, autoPlacement, hide, and inline middleware. |
pluginKey | string | PluginKey<any> | |
nestedOptions | NormalizedNestedOptions | |
onElementDragStart | (e: DragEvent): void | |
onElementDragEnd | (e: DragEvent): void | |
getReferencedVirtualElement | (): VirtualElement | null | |
dragImageProperties | string[] | |
nested | boolean | NestedOptions Enable drag handles for nested content (list items, blockquotes, etc.). When enabled, the drag handle will appear for nested blocks, not just top-level blocks. A rule-based scoring system determines which node to target based on cursor position and configured rules. | |
autofocus | false | true | "true" | "false" | |
disabled | boolean | |
name | string | |
type | 'button' | "reset" | "submit" | "button"The type of the button when not a link. |
label | string | |
activeColor | "error" | "neutral" | "primary" | "secondary" | "success" | "info" | "warning" | |
activeVariant | "ghost" | "solid" | "outline" | "soft" | "subtle" | "link" | |
size | 'sm' | "sm" | "xs" | "md" | "lg" | "xl" |
square | boolean Render the button with equal padding on all sides. | |
block | boolean Render the button full width. | |
loadingAuto | boolean Set loading state automatically based on the | |
avatar | AvatarPropsDisplay an avatar on the left side. | |
leading | boolean When | |
leadingIcon | anyDisplay an icon on the left side. | |
trailing | boolean When | |
trailingIcon | anyDisplay an icon on the right side. | |
loading | boolean When | |
loadingIcon | appConfig.ui.icons.loading | anyThe icon when the |
ui | { root?: SlotClass; handle?: SlotClass; } & { base?: SlotClass; label?: SlotClass; leadingIcon?: SlotClass; leadingAvatar?: SlotClass; leadingAvatarSize?: SlotClass; trailingIcon?: SlotClass; } |
Slots Bearbeiten
| Slot | Type |
|---|---|
default | { ui: object; } |
Emits (nicht)
| Event | Type |
|---|---|
nodeChange | [{ node: JSONContent; pos: number; }] |
hover | [{ node: JSONContent; pos: number; }] |
Theme (englisch)
export default defineAppConfig({
ui: {
editorDragHandle: {
slots: {
root: 'hidden sm:flex items-center justify-center transition-[top,left] duration-200 ease-out motion-reduce:transition-none',
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-[top,left] duration-200 ease-out motion-reduce:transition-none',
handle: 'cursor-grab px-1'
}
}
}
})
]
})