User Menu
A popup that shows a list of mention suggestions after pressing @
(users) or #
(tags) etc.
import 'prosekit/basic/style.css' import 'prosekit/basic/typography.css' import { createEditor } from 'prosekit/core' import { ProseKit } from 'prosekit/react' import { useMemo } from 'react' import { defineExtension } from './extension' import TagMenu from './tag-menu' import UserMenu from './user-menu' export default function Editor() { const editor = useMemo(() => { const extension = defineExtension() return createEditor({ extension }) }, []) return ( <ProseKit editor={editor}> <div className='box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow flex flex-col bg-white dark:bg-gray-950 color-black dark:color-white'> <div className='relative w-full flex-1 box-border overflow-y-scroll'> <div ref={editor.mount} className='ProseMirror box-border min-h-full px-[max(4rem,_calc(50%-20rem))] py-8 outline-none outline-0 [&_span[data-mention="user"]]:text-blue-500 [&_span[data-mention="tag"]]:text-violet-500'></div> <UserMenu /> <TagMenu /> </div> </div> </ProseKit> ) }
import { defineBasicExtension } from 'prosekit/basic' import { union } from 'prosekit/core' import { defineMention } from 'prosekit/extensions/mention' import { definePlaceholder } from 'prosekit/extensions/placeholder' export function defineExtension() { return union( defineBasicExtension(), definePlaceholder({ placeholder: 'Type @ to metion someone or # to tag something...', }), defineMention(), ) } export type EditorExtension = ReturnType<typeof defineExtension>
export const tags = [ { id: 1, label: 'book' }, { id: 2, label: 'movie' }, { id: 3, label: 'trip' }, { id: 4, label: 'music' }, { id: 5, label: 'art' }, { id: 6, label: 'food' }, { id: 7, label: 'sport' }, { id: 8, label: 'technology' }, { id: 9, label: 'fashion' }, { id: 10, label: 'nature' }, ]
import { useEditor } from 'prosekit/react' import { AutocompleteEmpty, AutocompleteItem, AutocompleteList, AutocompletePopover, } from 'prosekit/react/autocomplete' import type { EditorExtension } from './extension' import { tags } from './tag-data' export default function TagMenu() { const editor = useEditor<EditorExtension>() const handleTagInsert = (id: number, label: string) => { editor.commands.insertMention({ id: id.toString(), value: '#' + label, kind: 'tag', }) editor.commands.insertText({ text: ' ' }) } return ( <AutocompletePopover regex={/#[\da-z]*$/i} className='relative block max-h-[25rem] min-w-[15rem] select-none overflow-auto whitespace-nowrap p-1 z-10 box-border rounded-lg border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-lg [&:not([data-state])]:hidden' > <AutocompleteList> <AutocompleteEmpty className='relative flex items-center justify-between min-w-[8rem] scroll-my-1 rounded px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-none data-[focused]:bg-gray-100 dark:data-[focused]:bg-gray-800'> No results </AutocompleteEmpty> {tags.map((tag) => ( <AutocompleteItem key={tag.id} className='relative flex items-center justify-between min-w-[8rem] scroll-my-1 rounded px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-none data-[focused]:bg-gray-100 dark:data-[focused]:bg-gray-800' onSelect={() => handleTagInsert(tag.id, tag.label)} > #{tag.label} </AutocompleteItem> ))} </AutocompleteList> </AutocompletePopover> ) }
export const users = [ { id: 1, name: 'Alex' }, { id: 2, name: 'Alice' }, { id: 3, name: 'Ben' }, { id: 4, name: 'Bob' }, { id: 5, name: 'Charlie' }, { id: 6, name: 'Cara' }, { id: 7, name: 'Derek' }, { id: 8, name: 'Diana' }, { id: 9, name: 'Ethan' }, { id: 10, name: 'Eva' }, { id: 11, name: 'Frank' }, { id: 12, name: 'Fiona' }, { id: 13, name: 'George' }, { id: 14, name: 'Gina' }, { id: 15, name: 'Harry' }, { id: 16, name: 'Hannah' }, { id: 17, name: 'Ivan' }, { id: 18, name: 'Iris' }, { id: 19, name: 'Jack' }, { id: 20, name: 'Jasmine' }, { id: 21, name: 'Kevin' }, { id: 22, name: 'Kate' }, { id: 23, name: 'Leo' }, { id: 24, name: 'Lily' }, { id: 25, name: 'Mike' }, { id: 26, name: 'Mia' }, { id: 27, name: 'Nathan' }, { id: 28, name: 'Nancy' }, { id: 29, name: 'Oscar' }, { id: 30, name: 'Olivia' }, { id: 31, name: 'Paul' }, { id: 32, name: 'Penny' }, { id: 33, name: 'Quentin' }, { id: 34, name: 'Queen' }, { id: 35, name: 'Roger' }, { id: 36, name: 'Rita' }, { id: 37, name: 'Sam' }, { id: 38, name: 'Sara' }, { id: 39, name: 'Tom' }, { id: 40, name: 'Tina' }, { id: 41, name: 'Ulysses' }, { id: 42, name: 'Una' }, { id: 43, name: 'Victor' }, { id: 44, name: 'Vera' }, { id: 45, name: 'Walter' }, { id: 46, name: 'Wendy' }, { id: 47, name: 'Xavier' }, { id: 48, name: 'Xena' }, { id: 49, name: 'Yan' }, { id: 50, name: 'Yvonne' }, { id: 51, name: 'Zack' }, { id: 52, name: 'Zara' }, ]
import { useEditor } from 'prosekit/react' import { AutocompleteEmpty, AutocompleteItem, AutocompleteList, AutocompletePopover, } from 'prosekit/react/autocomplete' import type { EditorExtension } from './extension' import { users } from './user-data' export default function UserMenu() { const editor = useEditor<EditorExtension>() const handleUserInsert = (id: number, username: string) => { editor.commands.insertMention({ id: id.toString(), value: '@' + username, kind: 'user', }) editor.commands.insertText({ text: ' ' }) } return ( <AutocompletePopover regex={/@\w*$/} className='relative block max-h-[25rem] min-w-[15rem] select-none overflow-auto whitespace-nowrap p-1 z-10 box-border rounded-lg border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-lg [&:not([data-state])]:hidden'> <AutocompleteList> <AutocompleteEmpty className='relative flex items-center justify-between min-w-[8rem] scroll-my-1 rounded px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-none data-[focused]:bg-gray-100 dark:data-[focused]:bg-gray-800'> No results </AutocompleteEmpty> {users.map((user) => ( <AutocompleteItem key={user.id} className='relative flex items-center justify-between min-w-[8rem] scroll-my-1 rounded px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-none data-[focused]:bg-gray-100 dark:data-[focused]:bg-gray-800' onSelect={() => handleUserInsert(user.id, user.name)} > {user.name} </AutocompleteItem> ))} </AutocompleteList> </AutocompletePopover> ) }
<script setup lang="ts"> import 'prosekit/basic/style.css' import 'prosekit/basic/typography.css' import { createEditor } from 'prosekit/core' import { ProseKit } from 'prosekit/vue' import { ref, watchPostEffect, } from 'vue' import { defineExtension } from './extension' import TagMenu from './tag-menu.vue' import UserMenu from './user-menu.vue' const editor = createEditor({ extension: defineExtension() }) const editorRef = ref<HTMLDivElement | null>(null) watchPostEffect((onCleanup) => { editor.mount(editorRef.value) onCleanup(() => editor.unmount()) }) </script> <template> <ProseKit :editor="editor"> <div class='box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow flex flex-col bg-white dark:bg-gray-950 color-black dark:color-white'> <div class='relative w-full flex-1 box-border overflow-y-scroll'> <div ref="editorRef" class='ProseMirror box-border min-h-full px-[max(4rem,_calc(50%-20rem))] py-8 outline-none outline-0 [&_span[data-mention="user"]]:text-blue-500 [&_span[data-mention="tag"]]:text-violet-500' /> <UserMenu /> <TagMenu /> </div> </div> </ProseKit> </template>
import { defineBasicExtension } from 'prosekit/basic' import { union } from 'prosekit/core' import { defineMention } from 'prosekit/extensions/mention' import { definePlaceholder } from 'prosekit/extensions/placeholder' export function defineExtension() { return union( defineBasicExtension(), definePlaceholder({ placeholder: 'Type @ to metion someone or # to tag something...', }), defineMention(), ) } export type EditorExtension = ReturnType<typeof defineExtension>
export const tags = [ { id: 1, label: 'book' }, { id: 2, label: 'movie' }, { id: 3, label: 'trip' }, { id: 4, label: 'music' }, { id: 5, label: 'art' }, { id: 6, label: 'food' }, { id: 7, label: 'sport' }, { id: 8, label: 'technology' }, { id: 9, label: 'fashion' }, { id: 10, label: 'nature' }, ]
<script setup lang="ts"> import { useEditor } from 'prosekit/vue' import { AutocompleteEmpty, AutocompleteItem, AutocompleteList, AutocompletePopover, } from 'prosekit/vue/autocomplete' import type { EditorExtension } from './extension' import { tags } from './tag-data' const editor = useEditor<EditorExtension>() function handleTagInsert(id: number, label: string) { editor.value.commands.insertMention({ id: id.toString(), value: '#' + label, kind: 'tag', }) editor.value.commands.insertText({ text: ' ' }) } </script> <template> <AutocompletePopover :regex="/#[\da-z]*$/i" class='relative block max-h-[25rem] min-w-[15rem] select-none overflow-auto whitespace-nowrap p-1 z-10 box-border rounded-lg border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-lg [&:not([data-state])]:hidden'> <AutocompleteList> <AutocompleteEmpty class='relative flex items-center justify-between min-w-[8rem] scroll-my-1 rounded px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-none data-[focused]:bg-gray-100 dark:data-[focused]:bg-gray-800'> No results </AutocompleteEmpty> <AutocompleteItem v-for="tag in tags" :key="tag.id" class='relative flex items-center justify-between min-w-[8rem] scroll-my-1 rounded px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-none data-[focused]:bg-gray-100 dark:data-[focused]:bg-gray-800' @select="() => handleTagInsert(tag.id, tag.label)" > {{ tag.label }} </AutocompleteItem> </AutocompleteList> </AutocompletePopover> </template>
export const users = [ { id: 1, name: 'Alex' }, { id: 2, name: 'Alice' }, { id: 3, name: 'Ben' }, { id: 4, name: 'Bob' }, { id: 5, name: 'Charlie' }, { id: 6, name: 'Cara' }, { id: 7, name: 'Derek' }, { id: 8, name: 'Diana' }, { id: 9, name: 'Ethan' }, { id: 10, name: 'Eva' }, { id: 11, name: 'Frank' }, { id: 12, name: 'Fiona' }, { id: 13, name: 'George' }, { id: 14, name: 'Gina' }, { id: 15, name: 'Harry' }, { id: 16, name: 'Hannah' }, { id: 17, name: 'Ivan' }, { id: 18, name: 'Iris' }, { id: 19, name: 'Jack' }, { id: 20, name: 'Jasmine' }, { id: 21, name: 'Kevin' }, { id: 22, name: 'Kate' }, { id: 23, name: 'Leo' }, { id: 24, name: 'Lily' }, { id: 25, name: 'Mike' }, { id: 26, name: 'Mia' }, { id: 27, name: 'Nathan' }, { id: 28, name: 'Nancy' }, { id: 29, name: 'Oscar' }, { id: 30, name: 'Olivia' }, { id: 31, name: 'Paul' }, { id: 32, name: 'Penny' }, { id: 33, name: 'Quentin' }, { id: 34, name: 'Queen' }, { id: 35, name: 'Roger' }, { id: 36, name: 'Rita' }, { id: 37, name: 'Sam' }, { id: 38, name: 'Sara' }, { id: 39, name: 'Tom' }, { id: 40, name: 'Tina' }, { id: 41, name: 'Ulysses' }, { id: 42, name: 'Una' }, { id: 43, name: 'Victor' }, { id: 44, name: 'Vera' }, { id: 45, name: 'Walter' }, { id: 46, name: 'Wendy' }, { id: 47, name: 'Xavier' }, { id: 48, name: 'Xena' }, { id: 49, name: 'Yan' }, { id: 50, name: 'Yvonne' }, { id: 51, name: 'Zack' }, { id: 52, name: 'Zara' }, ]
<script setup lang="ts"> import { useEditor } from 'prosekit/vue' import { AutocompleteEmpty, AutocompleteItem, AutocompleteList, AutocompletePopover, } from 'prosekit/vue/autocomplete' import type { EditorExtension } from './extension' import { users } from './user-data' const editor = useEditor<EditorExtension>() function handleUserInsert(id: number, username: string) { editor.value.commands.insertMention({ id: id.toString(), value: '@' + username, kind: 'user', }) editor.value.commands.insertText({ text: ' ' }) } </script> <template> <AutocompletePopover :regex="/@\w*$/" class='relative block max-h-[25rem] min-w-[15rem] select-none overflow-auto whitespace-nowrap p-1 z-10 box-border rounded-lg border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-lg [&:not([data-state])]:hidden'> <AutocompleteList> <AutocompleteEmpty class='relative flex items-center justify-between min-w-[8rem] scroll-my-1 rounded px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-none data-[focused]:bg-gray-100 dark:data-[focused]:bg-gray-800'> No results </AutocompleteEmpty> <AutocompleteItem v-for="user in users" :key="user.id" class='relative flex items-center justify-between min-w-[8rem] scroll-my-1 rounded px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-none data-[focused]:bg-gray-100 dark:data-[focused]:bg-gray-800' @select="() => handleUserInsert(user.id, user.name)" > {{ user.name }} </AutocompleteItem> </AutocompleteList> </AutocompletePopover> </template>
Copy and paste the component source files linked above into your project.