EditorDragHandle
Gebruik
De EditorDragHandle-component biedt slepen-en-neerzetten-functionaliteit voor het opnieuw ordenen van editorblokken met behulp van het @tiptap/extension-drag-handle-vue-3-pakket.
Het breidt de Button component uit, zodat u elke eigenschap zoals color, variant, size, enz. Kunt doorgeven.
<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>Icoon
Gebruik de icon prop om het pictogram van de sleepgreep aan te passen.
<template>
<UEditor v-slot="{ editor }">
<UEditorDragHandle :editor="editor" icon="i-lucide-move" />
</UEditor>
</template>
Opties
Gebruik de options prop om het positioneringsgedrag aan te passen met Floating UI options.
<template>
<UEditor v-slot="{ editor }">
<UEditorDragHandle
:editor="editor"
:options="{
placement: 'left'
}"
/>
</UEditor>
</template>
Voorbeelden
Met dropdown menu
Gebruik de standaardsleuf om een DropdownMenu toe te voegen met acties op blokniveau zoals dupliceren, verwijderen, omhoog / omlaag verplaatsen of blokken omzetten in verschillende typen.
Luister naar de @node-change-gebeurtenis om het momenteel zwevende knooppunt en zijn positie te volgen en gebruik vervolgens editor.chain().setMeta('lockDragHandle', open).run() om de handgreeppositie te vergrendelen terwijl het menu open is.
<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 hulpprogramma van @nuxt/ui/utils/editor gebruikt om handlersoorten (zoals duplicate, delete, moveUp, enz.) Automatisch toe te wijzen aan de bijbehorende editoropdrachten met het juiste statusbeheer.Met suggestie menu
Gebruik de standaardsleuf om een Button naast de sleephendel toe te voegen om de EditorSuggestionMenu te openen.
Roep de onClick-slotfunctie aan om de huidige knooppuntpositie te krijgen en gebruik vervolgens handlers.suggestion?.execute(editor, { pos: node?.pos }).run() om nieuwe blokken op die positie in te voegen.
<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
Props
| 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
| Slot | Type |
|---|---|
default | { ui: object; } |
Uitzendt
| Event | Type |
|---|---|
nodeChange | [{ node: JSONContent; pos: number; }] |
hover | [{ node: JSONContent; pos: number; }] |
Thema
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'
}
}
}
})
]
})