Example: user-menu-dynamic
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 UserMenuDynamic from './user-menu-dynamic'
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-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-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-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
          <UserMenuDynamic />
        </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...',
    }),
    defineMention(),
  )
}
export type EditorExtension = ReturnType<typeof defineExtension>import {
  useEffect,
  useState,
} from 'react'
import { users as allUsers } from './user-data'
/**
 * Simulate a user searching with some delay.
 */
export function useUserQuery(query: string, enabled: boolean) {
  const [users, setUsers] = useState<{ name: string; id: number }[]>([])
  const [loading, setLoading] = useState(true)
  useEffect(() => {
    if (!enabled) {
      setUsers([])
      return
    }
    setLoading(true)
    const searchQuery = query.toLowerCase()
    const id = setTimeout(async () => {
      await waitForTestBlocking()
      setLoading(false)
      setUsers(
        allUsers
          .filter((user) => user.name.toLowerCase().includes(searchQuery))
          .slice(0, 10),
      )
    }, 500)
    return () => {
      clearTimeout(id)
    }
  }, [enabled, query])
  return { loading, users }
}
/**
 * Use a global variable to simulate a network request delay.
 */
async function waitForTestBlocking() {
  return await new Promise((resolve) => {
    const id = setInterval(() => {
      const hasTestBlocking = !!window._PROSEKIT_TEST_BLOCKING
      if (!hasTestBlocking) {
        clearInterval(id)
        resolve(true)
      }
    }, 100)
  })
}
declare global {
  interface Window {
    _PROSEKIT_TEST_BLOCKING: boolean | undefined
  }
}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 { useState } from 'react'
import type { EditorExtension } from './extension'
import { useUserQuery } from './use-user-query'
export default function UserMenuDynamic() {
  const editor = useEditor<EditorExtension>()
  const [query, setQuery] = useState('')
  const [open, setOpen] = useState(false)
  const handleUserInsert = (id: number, username: string) => {
    editor.commands.insertMention({
      id: id.toString(),
      value: '@' + username,
      kind: 'user',
    })
    editor.commands.insertText({ text: ' ' })
  }
  const { loading, users } = useUserQuery(query, open)
  return (
    <AutocompletePopover
      regex={/@\w*$/}
      onQueryChange={setQuery}
      onOpenChange={setOpen}
      className="relative block max-h-100 min-w-60 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 filter={null}>
        <AutocompleteEmpty className="relative flex items-center justify-between min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden data-[focused]:bg-gray-100 dark:data-[focused]:bg-gray-800">
          {loading ? 'Loading...' : 'No results'}
        </AutocompleteEmpty>
        {users.map((user) => (
          <AutocompleteItem
            key={user.id}
            className="relative flex items-center justify-between min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden data-[focused]:bg-gray-100 dark:data-[focused]:bg-gray-800"
            onSelect={() => handleUserInsert(user.id, user.name)}
          >
            <span className={loading ? 'opacity-50' : undefined}>
              {user.name}
            </span>
          </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 UserMenuDynamic from './user-menu-dynamic.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-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-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-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500" />
        <UserMenuDynamic />
      </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...',
    }),
    defineMention(),
  )
}
export type EditorExtension = ReturnType<typeof defineExtension>import {
  ref,
  watchEffect,
  type Ref,
} from 'vue'
import { users as allUsers } from './user-data'
/**
 * Simulate a user searching with some delay.
 */
export function useUserQuery(query: Ref<string>, enabled: Ref<boolean>) {
  const users = ref<{ id: number; name: string }[]>([])
  const loading = ref(true)
  watchEffect((onInvalidate) => {
    if (!enabled.value) {
      users.value = []
      return
    }
    loading.value = true
    const searchQuery = query.value.toLowerCase()
    const timeoutId = setTimeout(async () => {
      await waitForTestBlocking()
      loading.value = false
      users.value = allUsers
        .filter((user) => user.name.toLowerCase().includes(searchQuery))
        .slice(0, 10)
    }, 500)
    onInvalidate(() => {
      clearTimeout(timeoutId)
    })
  })
  return { loading, users }
}
/**
 * Use a global variable to simulate a network request delay.
 */
async function waitForTestBlocking() {
  return await new Promise((resolve) => {
    const id = setInterval(() => {
      const hasTestBlocking = !!window._PROSEKIT_TEST_BLOCKING
      if (!hasTestBlocking) {
        clearInterval(id)
        resolve(true)
      }
    }, 100)
  })
}
declare global {
  interface Window {
    _PROSEKIT_TEST_BLOCKING: boolean | undefined
  }
}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 { ref } from 'vue'
import type { EditorExtension } from './extension'
import { useUserQuery } from './use-user-query'
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: ' ' })
}
const query = ref('')
const open = ref(false)
const handleQueryChange = (value: string) => (query.value = value)
const handleOpenChange = (value: boolean) => (open.value = value)
const { users, loading } = useUserQuery(query, open)
</script>
<template>
  <AutocompletePopover
    :regex="/@\w*$/"
    class="relative block max-h-100 min-w-60 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"
    @query-change="handleQueryChange"
    @open-change="handleOpenChange"
  >
    <AutocompleteList :filter="null">
      <AutocompleteEmpty class="relative flex items-center justify-between min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden data-[focused]:bg-gray-100 dark:data-[focused]:bg-gray-800">
        {{ loading ? 'Loading...' : 'No results' }}
      </AutocompleteEmpty>
      <AutocompleteItem
        v-for="user in users"
        :key="user.id"
        class="relative flex items-center justify-between min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden data-[focused]:bg-gray-100 dark:data-[focused]:bg-gray-800"
        @select="() => handleUserInsert(user.id, user.name)"
      >
        <span :class="loading && 'opacity-50'">
          {{ user.name }}
        </span>
      </AutocompleteItem>
    </AutocompleteList>
  </AutocompletePopover>
</template>