EditormentoMenú
xph0000xUso
The EditorMentionMenu component displays a menu of user suggestions when typing a trigger character (defaults to @) in the editor and inserts the selected mention using the @tiptap/extension-mention package.
useEditorMenu construido sobre la utilidad Suggestion de TipTap para filtrar elementos a medida que escribe y admite la navegación con el teclado (teclas de flecha, ingresar para seleccionar, escapar para cerrar).<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>xph018XArtículos
Utilice el prop items como una matriz de objetos con las siguientes propiedades:
- x
label: stringxx{lang="ts-type"} - x
avatar?: AvatarPropsxx{lang="ts-type"} - xx
icon?: stringxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx - xx
description?: stringxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx - xxx
disabled?: booleanxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
<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 para crear grupos separados de elementos.Char (Edición española)
Utilice el prop char para cambiar el carácter de activación. Por defecto es @. El carácter de activación también se utiliza como prefijo al representar la mención insertada (por ejemplo, #channel en lugar de @channel).
<template>
<UEditor v-slot="{ editor }">
<UEditorMentionMenu :editor="editor" :items="channels" char="#" />
</UEditor>
</template>
EditorMentionMenu en el mismo editor con diferentes accesorios char y plugin-key para admitir diferentes tipos de mención.<template>
<UEditor v-slot="{ editor }">
<UEditorMentionMenu :editor="editor" :items="users" plugin-key="mentionMenu" />
<UEditorMentionMenu :editor="editor" :items="tags" char="#" plugin-key="tagMenu" />
</UEditor>
</template>
Sugerencia: badge{label="4.7+" class="align-text-top"}
Utilice el prop suggestion para personalizar el comportamiento de coincidencia de sugerencias x de TipTap.
Esto es útil cuando el carácter desencadenante debe abrirse directamente después de otros caracteres en lugar de requerir el prefijo de espacio en blanco predeterminado.
<template>
<UEditor v-slot="{ editor }">
<UEditorMentionMenu
:editor="editor"
:items="items"
char="#"
:suggestion="{
allowedPrefixes: null
}"
/>
</UEditor>
</template>
Opciones
Utilice el prop options para personalizar el comportamiento de posicionamiento utilizando las opciones de interfaz de usuario flotante ](https://floating-ui.com/docs/computeposition#options).
<template>
<UEditor v-slot="{ editor }">
<UEditorMentionMenu
:editor="editor"
:items="items"
:options="{
placement: 'bottom-start',
offset: 4
}"
/>
</UEditor>
</template>
Ejemplos
Con ignorar filtro: badge{label="4.4+" class="align-text-top"}
Puede configurar la prop ignore-filter en true para desactivar la búsqueda interna y usar su propia lógica de búsqueda. Use v-model:search-term para acceder al término de búsqueda actual y obtener elementos de una API.
<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 para desacreditar las llamadas a la API.API (Edición española)
Props (accesorios)
| 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; } |
Temas
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'
}
}
}
})
]
})