Redaktion Menu
Bearbeiten
Die EditorMentionMenu-Komponente zeigt ein Menü mit Benutzervorschlägen an, wenn Sie ein Triggerzeichen (standardmäßig @) im Editor eingeben, und fügt die ausgewählte Erwähnung mit dem Paket @tiptap/extension-mention ein.
useEditorMenu composable, das auf dem Dienstprogramm Suggestion von TipTap aufbaut, um Elemente während der Eingabe zu filtern und die Tastaturnavigation zu unterstützen (Pfeiltasten, zum Auswählen eingeben, zum Schließen entkommen).<script setup lang="ts">
import type { EditorMentionMenuItem } from '@nuxt/ui'
const value = ref(`# Mention Menu
Type @ to mention someone and select from the list of available users.`)
const items: EditorMentionMenuItem[] = [
{
label: 'benjamincanac',
avatar: {
src: 'https://avatars.githubusercontent.com/u/739984?v=4',
loading: 'lazy' as const
}
},
{
label: 'atinux',
avatar: {
src: 'https://avatars.githubusercontent.com/u/904724?v=4',
loading: 'lazy' as const
}
},
{
label: 'danielroe',
avatar: {
src: 'https://avatars.githubusercontent.com/u/28706372?v=4',
loading: 'lazy' as const
}
},
{
label: 'pi0',
avatar: {
src: 'https://avatars.githubusercontent.com/u/5158436?v=4',
loading: 'lazy' as const
}
}
]
// SSR-safe function to append menus to body (avoids z-index issues in docs)
const appendToBody = false ? () => document.body : undefined
</script>
<template>
<UEditor
v-slot="{ editor }"
v-model="value"
content-type="markdown"
placeholder="Type @ to mention someone..."
class="w-full min-h-21"
>
<UEditorMentionMenu :editor="editor" :items="items" :append-to="appendToBody" />
</UEditor>
</template><script setup lang="ts">
import { ref } from 'vue'
import type { EditorMentionMenuItem } from '@nuxt/ui'
const value = ref(`# Mention Menu
Type @ to mention someone and select from the list of available users.`)
const items: EditorMentionMenuItem[] = [
{
label: 'benjamincanac',
avatar: {
src: 'https://avatars.githubusercontent.com/u/739984?v=4',
loading: 'lazy' as const
}
},
{
label: 'atinux',
avatar: {
src: 'https://avatars.githubusercontent.com/u/904724?v=4',
loading: 'lazy' as const
}
},
{
label: 'danielroe',
avatar: {
src: 'https://avatars.githubusercontent.com/u/28706372?v=4',
loading: 'lazy' as const
}
},
{
label: 'pi0',
avatar: {
src: 'https://avatars.githubusercontent.com/u/5158436?v=4',
loading: 'lazy' as const
}
}
]
// SSR-safe function to append menus to body (avoids z-index issues in docs)
const appendToBody = false ? () => document.body : undefined
</script>
<template>
<UEditor
v-slot="{ editor }"
v-model="value"
content-type="markdown"
placeholder="Type @ to mention someone..."
class="w-full min-h-21"
>
<UEditorMentionMenu :editor="editor" :items="items" :append-to="appendToBody" />
</UEditor>
</template>Items Bearbeiten
Verwenden Sie die items-prop als Array von Objekten mit den folgenden Eigenschaften:
label: stringxph0222x (nicht vorhanden)avatar?: AvatarProps(englisch)icon?: string(nicht)description?: string(nicht vorhanden)disabled?: boolean(nicht vorhanden)
<script setup lang="ts">
import type { EditorMentionMenuItem } from '@nuxt/ui'
const value = ref(`Type @ to mention a user.
You can customize the items with avatars, icons, and descriptions.`)
const items: EditorMentionMenuItem[] = [{
label: 'benjamincanac',
avatar: { src: 'https://avatars.githubusercontent.com/u/739984?v=4', loading: 'lazy' as const }
}, {
label: 'HugoRCD',
avatar: { src: 'https://avatars.githubusercontent.com/u/71938701?v=4', loading: 'lazy' as const }
}, {
label: 'atinux',
avatar: { src: 'https://avatars.githubusercontent.com/u/904724?v=4', loading: 'lazy' as const }
}, {
label: 'romhml',
avatar: { src: 'https://avatars.githubusercontent.com/u/25613751?v=4', loading: 'lazy' as const }
}, {
label: 'sandros94',
avatar: { src: 'https://avatars.githubusercontent.com/u/13056429?v=4', loading: 'lazy' as const }
}, {
label: 'J-Michalek',
avatar: { src: 'https://avatars.githubusercontent.com/u/71264422?v=4', loading: 'lazy' as const }
}, {
label: 'hywax',
avatar: { src: 'https://avatars.githubusercontent.com/u/149865959?v=4', loading: 'lazy' as const }
}]
</script>
<template>
<UEditor
v-slot="{ editor }"
v-model="value"
content-type="markdown"
placeholder="Type @ to mention..."
class="w-full min-h-19"
>
<UEditorMentionMenu :editor="editor" :items="items" />
</UEditor>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { EditorMentionMenuItem } from '@nuxt/ui'
const value = ref(`Type @ to mention a user.
You can customize the items with avatars, icons, and descriptions.`)
const items: EditorMentionMenuItem[] = [{
label: 'benjamincanac',
avatar: { src: 'https://avatars.githubusercontent.com/u/739984?v=4', loading: 'lazy' as const }
}, {
label: 'HugoRCD',
avatar: { src: 'https://avatars.githubusercontent.com/u/71938701?v=4', loading: 'lazy' as const }
}, {
label: 'atinux',
avatar: { src: 'https://avatars.githubusercontent.com/u/904724?v=4', loading: 'lazy' as const }
}, {
label: 'romhml',
avatar: { src: 'https://avatars.githubusercontent.com/u/25613751?v=4', loading: 'lazy' as const }
}, {
label: 'sandros94',
avatar: { src: 'https://avatars.githubusercontent.com/u/13056429?v=4', loading: 'lazy' as const }
}, {
label: 'J-Michalek',
avatar: { src: 'https://avatars.githubusercontent.com/u/71264422?v=4', loading: 'lazy' as const }
}, {
label: 'hywax',
avatar: { src: 'https://avatars.githubusercontent.com/u/149865959?v=4', loading: 'lazy' as const }
}]
</script>
<template>
<UEditor
v-slot="{ editor }"
v-model="value"
content-type="markdown"
placeholder="Type @ to mention..."
class="w-full min-h-19"
>
<UEditorMentionMenu :editor="editor" :items="items" />
</UEditor>
</template>
items-prop übergeben, um getrennte Gruppen von Elementen zu erstellen.Char (nicht)
Verwenden Sie die prop char, um das Triggerzeichen zu ändern. Standardmäßig ist es @. Das Triggerzeichen wird auch als Präfix verwendet, wenn die eingefügte Erwähnung wiedergegeben wird (z. B. #channel anstelle von @channel).
<template>
<UEditor v-slot="{ editor }">
<UEditorMentionMenu :editor="editor" :items="channels" char="#" />
</UEditor>
</template>
EditorMentionMenu-Komponenten im selben Editor mit verschiedenen char-und plugin-key-Requisiten verwenden, um verschiedene Erwähnungstypen zu unterstützen.<template>
<UEditor v-slot="{ editor }">
<UEditorMentionMenu :editor="editor" :items="users" plugin-key="mentionMenu" />
<UEditorMentionMenu :editor="editor" :items="tags" char="#" plugin-key="tagMenu" />
</UEditor>
</template>
Vorschlag: badge{label="4.7+" class="align-text-top"}
Verwenden Sie die suggestion-Prop, um TipTaps Suggestion-Übereinstimmungsverhalten anzupassen.
Dies ist nützlich, wenn das Triggerzeichen direkt nach anderen Zeichen geöffnet werden soll, anstatt das Standard-Whitespace-Präfix zu erfordern.
<template>
<UEditor v-slot="{ editor }">
<UEditorMentionMenu
:editor="editor"
:items="items"
char="#"
:suggestion="{
allowedPrefixes: null
}"
/>
</UEditor>
</template>
Options (Deutsche Übersetzung)
Verwenden Sie die options-Prop, um das Positionierungsverhalten mit Floating UI-Optionen anzupassen.
<template>
<UEditor v-slot="{ editor }">
<UEditorMentionMenu
:editor="editor"
:items="items"
:options="{
placement: 'bottom-start',
offset: 4
}"
/>
</UEditor>
</template>
Examples (Beispiele)
With ignore filter: badge{label="4.4+" class="align-text-top"} (mit Ignorierfilter)
Sie können die ignore-filter-prop auf true setzen, um die interne Suche zu deaktivieren und Ihre eigene Suchlogik zu verwenden. Verwenden Sie v-model:search-term, um auf den aktuellen Suchbegriff zuzugreifen und Elemente aus einer API zu holen.
<script setup lang="ts">
import { refDebounced } from '@vueuse/core'
const value = ref(`# Async Mention Menu
Type @ to mention someone. Results are fetched from an API as you type.`)
const searchTerm = ref('')
const searchTermDebounced = refDebounced(searchTerm, 200)
const { data: items } = useLazyFetch('https://dummyjson.com/users/search?limit=10', {
key: 'editor-mention-users-search',
params: { q: searchTermDebounced },
transform: (data: { users: { id: number, firstName: string, lastName: string, image: string }[] }) => {
return data.users?.map(user => ({ id: user.id, label: `${user.firstName} ${user.lastName}`, avatar: { src: user.image, loading: 'lazy' as const } })) || []
},
server: false
})
// SSR-safe function to append menus to body (avoids z-index issues in docs)
const appendToBody = false ? () => document.body : undefined
</script>
<template>
<UEditor
v-slot="{ editor }"
v-model="value"
content-type="markdown"
placeholder="Type @ to mention someone..."
class="w-full min-h-21"
>
<UEditorMentionMenu
v-model:search-term="searchTerm"
:editor="editor"
:items="items"
:append-to="appendToBody"
ignore-filter
/>
</UEditor>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { refDebounced } from '@vueuse/core'
const value = ref(`# Async Mention Menu
Type @ to mention someone. Results are fetched from an API as you type.`)
const searchTerm = ref('')
const searchTermDebounced = refDebounced(searchTerm, 200)
const { data: items } = useLazyFetch('https://dummyjson.com/users/search?limit=10', {
key: 'editor-mention-users-search',
params: { q: searchTermDebounced },
transform: (data: { users: { id: number, firstName: string, lastName: string, image: string }[] }) => {
return data.users?.map(user => ({ id: user.id, label: `${user.firstName} ${user.lastName}`, avatar: { src: user.image, loading: 'lazy' as const } })) || []
},
server: false
})
// SSR-safe function to append menus to body (avoids z-index issues in docs)
const appendToBody = false ? () => document.body : undefined
</script>
<template>
<UEditor
v-slot="{ editor }"
v-model="value"
content-type="markdown"
placeholder="Type @ to mention someone..."
class="w-full min-h-21"
>
<UEditorMentionMenu
v-model:search-term="searchTerm"
:editor="editor"
:items="items"
:append-to="appendToBody"
ignore-filter
/>
</UEditor>
</template>
refDebounced verwendet, um die API-Aufrufe zu entkräften.API (englisch)
Props (nicht)
| Prop | Default | Type |
|---|---|---|
size | 'md' | "xs" | "md" | "sm" | "lg" | "xl" |
items | T[] | T[][] | |
editor | Editor | |
char | '@' | stringThe trigger character (e.g., '/', '@', ':') |
pluginKey | 'mentionMenu' | stringPlugin key to identify this menu |
filterFields | ['label'] | string[]Fields to filter items by. |
limit | 42 | numberMaximum number of items to display |
options | { strategy: 'absolute', placement: 'bottom-start', offset: 8, shift: { padding: 8 } } | FloatingUIOptionsThe options for positioning the menu. Those are passed to Floating UI and include options for the placement, offset, flip, shift, size, autoPlacement, hide, and inline middleware. |
suggestion | Omit<Partial<SuggestionOptions<any, any>>, "editor" | "char" | "pluginKey" | "items" | "command" | "render">Optional TipTap Suggestion matching options. | |
appendTo | HTMLElement | (): HTMLElementThe DOM element to append the menu to. Default is the editor's parent element. Sometimes the menu needs to be appended to a different DOM context due to accessibility, clipping, or z-index issues. | |
ignoreFilter | false | boolean Whether to ignore the default filtering.
When |
searchTerm | '' | string |
ui | { content?: SlotClass; viewport?: SlotClass; group?: SlotClass; label?: SlotClass; separator?: SlotClass; item?: SlotClass; itemLeadingIcon?: SlotClass; itemLeadingAvatar?: SlotClass; itemLeadingAvatarSize?: SlotClass; itemWrapper?: SlotClass; itemLabel?: SlotClass; itemDescription?: SlotClass; itemLabelExternalIcon?: SlotClass; } |
Theme Bearbeiten
export default defineAppConfig({
ui: {
editorMentionMenu: {
slots: {
content: 'min-w-48 max-w-60 max-h-96 bg-default shadow-lg rounded-md ring ring-default overflow-hidden data-[state=open]:animate-[scale-in_100ms_var(--ease-out)] data-[state=closed]:animate-[scale-out_100ms_var(--ease-out)] origin-(--reka-dropdown-menu-content-transform-origin) flex flex-col',
viewport: 'relative divide-y divide-default scroll-py-1 overflow-y-auto flex-1',
group: 'p-1 isolate',
label: 'w-full flex items-center font-semibold text-highlighted',
separator: '-mx-1 my-1 h-px bg-border',
item: 'group relative w-full flex items-start select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75',
itemLeadingIcon: 'shrink-0 flex items-center justify-center',
itemLeadingAvatar: 'shrink-0',
itemLeadingAvatarSize: '',
itemWrapper: 'flex-1 flex flex-col text-start min-w-0',
itemLabel: 'truncate',
itemDescription: 'truncate text-muted',
itemLabelExternalIcon: 'inline-block size-3 align-top text-dimmed'
},
variants: {
size: {
xs: {
label: 'p-1 text-[10px]/3 gap-1',
item: 'p-1 text-xs gap-1',
itemLeadingIcon: 'size-4 text-sm',
itemLeadingAvatarSize: '3xs'
},
sm: {
label: 'p-1.5 text-[10px]/3 gap-1.5',
item: 'p-1.5 text-xs gap-1.5',
itemLeadingIcon: 'size-4 text-sm',
itemLeadingAvatarSize: '3xs'
},
md: {
label: 'p-1.5 text-xs gap-1.5',
item: 'p-1.5 text-sm gap-1.5',
itemLeadingIcon: 'size-5 text-base',
itemLeadingAvatarSize: '2xs'
},
lg: {
label: 'p-2 text-xs gap-2',
item: 'p-2 text-sm gap-2',
itemLeadingIcon: 'size-5 text-base',
itemLeadingAvatarSize: '2xs'
},
xl: {
label: 'p-2 text-sm gap-2',
item: 'p-2 text-base gap-2',
itemLeadingIcon: 'size-6 text-xl',
itemLeadingAvatarSize: 'xs'
}
},
active: {
true: {
item: 'text-highlighted before:bg-elevated/75',
itemLeadingIcon: 'text-default'
},
false: {
item: [
'text-default data-highlighted:not-data-disabled:text-highlighted data-highlighted:not-data-disabled:before:bg-elevated/50',
'transition-colors before:transition-colors'
],
itemLeadingIcon: [
'text-dimmed group-data-highlighted:not-group-data-disabled:text-default',
'transition-colors'
]
}
}
},
defaultVariants: {
size: 'md'
}
}
}
})import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: [
vue(),
ui({
ui: {
editorMentionMenu: {
slots: {
content: 'min-w-48 max-w-60 max-h-96 bg-default shadow-lg rounded-md ring ring-default overflow-hidden data-[state=open]:animate-[scale-in_100ms_var(--ease-out)] data-[state=closed]:animate-[scale-out_100ms_var(--ease-out)] origin-(--reka-dropdown-menu-content-transform-origin) flex flex-col',
viewport: 'relative divide-y divide-default scroll-py-1 overflow-y-auto flex-1',
group: 'p-1 isolate',
label: 'w-full flex items-center font-semibold text-highlighted',
separator: '-mx-1 my-1 h-px bg-border',
item: 'group relative w-full flex items-start select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75',
itemLeadingIcon: 'shrink-0 flex items-center justify-center',
itemLeadingAvatar: 'shrink-0',
itemLeadingAvatarSize: '',
itemWrapper: 'flex-1 flex flex-col text-start min-w-0',
itemLabel: 'truncate',
itemDescription: 'truncate text-muted',
itemLabelExternalIcon: 'inline-block size-3 align-top text-dimmed'
},
variants: {
size: {
xs: {
label: 'p-1 text-[10px]/3 gap-1',
item: 'p-1 text-xs gap-1',
itemLeadingIcon: 'size-4 text-sm',
itemLeadingAvatarSize: '3xs'
},
sm: {
label: 'p-1.5 text-[10px]/3 gap-1.5',
item: 'p-1.5 text-xs gap-1.5',
itemLeadingIcon: 'size-4 text-sm',
itemLeadingAvatarSize: '3xs'
},
md: {
label: 'p-1.5 text-xs gap-1.5',
item: 'p-1.5 text-sm gap-1.5',
itemLeadingIcon: 'size-5 text-base',
itemLeadingAvatarSize: '2xs'
},
lg: {
label: 'p-2 text-xs gap-2',
item: 'p-2 text-sm gap-2',
itemLeadingIcon: 'size-5 text-base',
itemLeadingAvatarSize: '2xs'
},
xl: {
label: 'p-2 text-sm gap-2',
item: 'p-2 text-base gap-2',
itemLeadingIcon: 'size-6 text-xl',
itemLeadingAvatarSize: 'xs'
}
},
active: {
true: {
item: 'text-highlighted before:bg-elevated/75',
itemLeadingIcon: 'text-default'
},
false: {
item: [
'text-default data-highlighted:not-data-disabled:text-highlighted data-highlighted:not-data-disabled:before:bg-elevated/50',
'transition-colors before:transition-colors'
],
itemLeadingIcon: [
'text-dimmed group-data-highlighted:not-group-data-disabled:text-default',
'transition-colors'
]
}
}
},
defaultVariants: {
size: 'md'
}
}
}
})
]
})