Editor Mention Menu
安裝
pnpm dlx shadcn-vue add @admin/editor-mention-menuimport type { MentionMenuItem } from '@/registry/admin/ui/editor-mention-menu'import { AdminEditorMentionMenu } from '@/registry/admin/ui/editor-mention-menu'admin-editor-mention-menu 依賴 @admin/rich-editor + @tiptap/extension-mention。
@tiptap/extension-mention 是 Node extension,跟 suggestion / emoji 這類純 plugin 不同,它會動到 schema,而 schema 在編輯器建立時就固定、無法後補。<AdminEditorMentionMenu> mount 時會註冊 Suggestion plugin,popup 照常浮現,但 schema 沒有 mention node 時,選定後只能退回插入純文字 @John,data-id / data-label 等 attrs 不會持久化。
import Mention from '@tiptap/extension-mention'const extensions = [ Mention.configure({ HTMLAttributes: { class: 'mention' }, renderText: ({ node }) => `${node.attrs.mentionSuggestionChar ?? '@'}${node.attrs.label ?? node.attrs.id}`, }),]// <AdminRichEditor :extensions="extensions">兩個設定都是建議值,不是必填。renderText 決定序列化與複製貼上的文字輸出,未設定時沿用 tiptap 預設的 @label,設定後可改成以觸發字元開頭;class: 'mention' 讓慣用的 .mention CSS 有地方掛(本文件 demo 的 chip 樣式改以 data-type 屬性選取,不依賴它)。
用法
<script setup lang="ts">import type { JSONContent } from '@tiptap/core'import type { MentionMenuItem } from '@/components/admin/editor-mention-menu'import Mention from '@tiptap/extension-mention'import { ref } from 'vue'import { cn } from '@/lib/utils'import { baseProseClass } from '@/components/admin/editor'import { AdminEditorMentionMenu } from '@/components/admin/editor-mention-menu'import { AdminEditorToolbar, defaultEditorItems } from '@/components/admin/editor-toolbar'import { AdminRichEditor, starterKitProseClass } from '@/components/admin/rich-editor'const value = ref<JSONContent | string>('<p>輸入 @ 觸發 mention menu(例如 @john)</p>')const users: MentionMenuItem[] = [ { id: 'u1', label: 'John', avatar: 'https://i.pravatar.cc/24?u=u1' }, { id: 'u2', label: 'Jane', avatar: 'https://i.pravatar.cc/24?u=u2' }, { id: 'u3', label: 'Alex', avatar: 'https://i.pravatar.cc/24?u=u3' }, { id: 'u4', label: 'Sam', avatar: 'https://i.pravatar.cc/24?u=u4' }]// 必須自行注入配置好的 Mention extension 讓 schema 認識 mention node(完整持久化)。// `renderText` 與 `HTMLAttributes.class` 為建議配置:// - renderText 讓序列化 / 複製貼上沿用觸發字元(未設定時 Tiptap 預設輸出 `@label`)// - class: 'mention' 供慣用 .mention CSS 掛載(chip 樣式本 demo 以 data-type 屬性選取,不依賴它)const extensions = [ Mention.configure({ HTMLAttributes: { class: 'mention' }, renderText: ({ node }) => `${node.attrs.mentionSuggestionChar ?? '@'}${node.attrs.label ?? node.attrs.id}` })]// mention chip 樣式不內含於 registry,由使用者依需求自套const mentionChipClass = ` **:data-[type=mention]:inline-block **:data-[type=mention]:cursor-default **:data-[type=mention]:rounded-[4px] **:data-[type=mention]:bg-info-10 **:data-[type=mention]:px-1.5 **:data-[type=mention]:py-0.5 **:data-[type=mention]:text-sm **:data-[type=mention]:font-medium **:data-[type=mention]:text-info-70 **:data-[type=mention]:select-none`</script><template> <admin-rich-editor v-slot="{ editor, handlers }" v-model="value" :extensions="extensions" :ui="{ base: cn(baseProseClass, starterKitProseClass, mentionChipClass) }" > <admin-editor-toolbar :editor :handlers="handlers" :items="defaultEditorItems" /> <admin-editor-mention-menu :editor :items="users" /> </admin-rich-editor></template><script setup lang="ts">import Mention from '@tiptap/extension-mention'import { ref } from 'vue'import { AdminEditorMentionMenu } from '@/registry/admin/ui/editor-mention-menu'import { AdminRichEditor } from '@/registry/admin/ui/rich-editor'const value = ref('')const users = [ { id: 'u1', label: 'John' }, { id: 'u2', label: 'Jane' }]// mention node 要能持久化,務必在 :extensions 加配置好的 Mentionconst extensions = [ Mention.configure({ HTMLAttributes: { class: 'mention' }, renderText: ({ node }) => `${node.attrs.mentionSuggestionChar ?? '@'}${node.attrs.label ?? node.attrs.id}`, }),]</script><template> <admin-rich-editor v-slot="{ editor }" v-model="value" :extensions="extensions"> <admin-editor-mention-menu :editor :items="users" /> </admin-rich-editor></template>MentionMenuItem 介面
interface MentionMenuItem { id: string | number label: string avatar?: string [key: string]: unknown // 任意 metadata}選定後,mention node 的 attrs 會展開整個 item 物件(外加 mentionSuggestionChar: '@'),讓 renderText / renderHTML 讀得到任意欄位。
attrs 持久化注意
存進 DB 的 HTML 形如:
<span data-type="mention" data-id="u1" data-label="John">@John</span>item 建議只放 id、label 和很小的 metadata(部門代號、role 等);avatar URL、長描述這類欄位改用 id 在顯示時二次查詢補上,避免內容膨脹、拖慢序列化。
非同步搜尋
<script setup lang="ts">import type { JSONContent } from '@tiptap/core'import type { MentionMenuItem } from '@/components/admin/editor-mention-menu'import Mention from '@tiptap/extension-mention'import { ref, watch } from 'vue'import { cn } from '@/lib/utils'import { baseProseClass } from '@/components/admin/editor'import { AdminEditorMentionMenu } from '@/components/admin/editor-mention-menu'import { AdminEditorToolbar, defaultEditorItems } from '@/components/admin/editor-toolbar'import { AdminRichEditor, starterKitProseClass } from '@/components/admin/rich-editor'const value = ref<JSONContent | string>('<p>輸入 @ 觸發 mention menu(非同步搜尋 — 模擬 API)</p>')const searchTerm = ref('')const ALL_USERS: MentionMenuItem[] = [ { id: 'u1', label: 'John' }, { id: 'u2', label: 'Jane' }, { id: 'u3', label: 'Alex' }, { id: 'u4', label: 'Sam' }, { id: 'u5', label: 'Emily' }, { id: 'u6', label: 'Michael' }]// 初始給全部 users(模擬 API 預載清單),讓 `@` 一觸發 popup 就有東西const results = ref<MentionMenuItem[]>(ALL_USERS)// 模擬非同步 API(延遲 200ms)let fetchTimer: ReturnType<typeof setTimeout> | null = nullwatch(searchTerm, (q) => { if (fetchTimer) { clearTimeout(fetchTimer) } fetchTimer = setTimeout(() => { const ql = q.toLowerCase() results.value = ql ? ALL_USERS.filter((u) => u.label.toLowerCase().includes(ql)) : ALL_USERS }, 200)})const extensions = [Mention]// mention chip 樣式不內含於 registry,由使用者依需求自套const mentionChipClass = ` **:data-[type=mention]:inline-block **:data-[type=mention]:cursor-default **:data-[type=mention]:rounded-[4px] **:data-[type=mention]:bg-info-10 **:data-[type=mention]:px-1.5 **:data-[type=mention]:py-0.5 **:data-[type=mention]:text-sm **:data-[type=mention]:font-medium **:data-[type=mention]:text-info-70 **:data-[type=mention]:select-none`</script><template> <admin-rich-editor v-slot="{ editor, handlers }" v-model="value" :extensions="extensions" :ui="{ base: cn(baseProseClass, starterKitProseClass, mentionChipClass) }" > <admin-editor-toolbar :editor :handlers="handlers" :items="defaultEditorItems" /> <admin-editor-mention-menu v-model:search-term="searchTerm" ignore-filter :editor :items="results" /> </admin-rich-editor></template>users 來自後端 API、前端不適合全載時,用 v-model:search-term 雙向綁定搜尋字串,並加 ignore-filter 跳過內建的 fuzzy filter:
<script setup lang="ts">import Mention from '@tiptap/extension-mention'import { ref } from 'vue'const search = ref('')const { data: users } = await useFetch('/api/users', { query: { q: search }})const extensions = [ Mention.configure({ HTMLAttributes: { class: 'mention' }, renderText: ({ node }) => `${node.attrs.mentionSuggestionChar ?? '@'}${node.attrs.label ?? node.attrs.id}`, }),]</script><template> <admin-rich-editor v-slot="{ editor }" v-model="value" :extensions="extensions"> <admin-editor-mention-menu v-model:search-term="search" ignore-filter :editor :items="users ?? []" /> </admin-rich-editor></template>search 變動時 useFetch 會自動重打 API。
鍵盤導航
樣式
mention chip 樣式不內含於 registry,由使用者依需求自套。demo 以 **:data-[type=mention]:* utility class 注入編輯器容器(見「用法」demo 原始碼的 mentionChipClass 常數,色票對齊 admin design tokens),也可以改用 CSS 選取 [data-type='mention'] 自寫:
:deep([data-type='mention']) { /* 自訂樣式 */}API 參考
admin-editor-mention-menu
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
editor* | Editor | — | tiptap Editor instance(必填) |
items* | MentionMenuItem[] | MentionMenuItem[][] | — | 候選清單(可為扁平或分組陣列) |
char | string | undefined | "@" | 觸發字元,預設 `@` |
pluginKey | string | undefined | "editorMentionMenu" | PM plugin key,預設 `editorMentionMenu` |
limit | number | undefined | 10 | 顯示筆數上限,預設 10 |
filterFields | string[] | undefined | ["label"] | filter 比對欄位,預設 `['label']` |
ignoreFilter | boolean | undefined | false | true 時跳過內建 fuzzy filter(外部已 filter — 典型搭配 `v-model:searchTerm` + useFetch) |
class | ClassValue | — | — |
searchTerm | string | undefined | "" | — |
* 代表必填
Events
| 名稱 | Payload | 說明 |
|---|---|---|
update:searchTerm | string | — |
更新紀錄
- feat: 新增 tiptap editor 元件系列273a0d0