編輯器

自訂指南

加 extension、客製 toolbar,塞進自己的按鈕、下拉選單與元件。

自訂 extension

加 extension

用 :extensions 傳入任何 tiptap extension。要注意 editor 的 schema 在建立當下就固定了、之後沒辦法再加,所以 node / mark 這類 extension 一定要一開始就放進去:

<script setup lang="ts">import CharacterCount from '@tiptap/extension-character-count'import { AdminEditorToolbar, defaultEditorItems } from '@/registry/admin/ui/editor-toolbar'import { AdminRichEditor } from '@/registry/admin/ui/rich-editor'</script>​<template>  <admin-rich-editor    v-slot="{ editor, handlers }"    v-model="value"    :extensions="[CharacterCount.configure({ limit: 500 })]"  >    <admin-editor-toolbar :editor :handlers="handlers" :items="defaultEditorItems" />  </admin-rich-editor></template>

擴充套件

admin-rich-editor 預設只帶 StarterKit,Image、TextAlign、Placeholder 這些要另外裝。自行安裝對應的官方 @tiptap/extension-* 套件,.configure({}) 設定好再傳進 :extensions:

<script setup lang="ts">import { Image } from '@tiptap/extension-image'import TextAlign from '@tiptap/extension-text-align'</script>​<template>  <admin-rich-editor    :extensions="[TextAlign.configure({ types: ['heading', 'paragraph'] }), Image.configure({})]"  /></template>

StarterKit

配置內建行為

<admin-rich-editor :starter-kit="{ heading: { levels: [1, 2] }, codeBlock: false }" />

替換內建 extension

StarterKit 內建了 Link 等 extension。想換成自己的版本,得先用 :starter-kit 把內建的那個關掉,再從 :extensions 傳入,否則兩個同名 extension 會同時生效。完整可替換清單與限制見 tiptap StarterKit 官方文件。

<admin-rich-editor :starter-kit="{ link: false }" :extensions="[MyLink]" />

工具列

AdminEditorToolbar 靠 items 這個陣列決定要畫哪些按鈕,型別是 EditorToolbarItem[][],外層陣列用來分組。每個 item 可以是:

  • { kind: '...' }:走內建或 :handlers 註冊的 handler
  • { items: [...] }:dropdown(巢狀)
  • { type: 'label' | 'separator' }:純標示
  • { onClick }:無 kind 的純自訂按鈕
  • { slot: 'name' }:改由具名 slot 渲染

有狀態的按鈕(用 :handlers)

如果按鈕的反白(active)、禁用(disabled)要跟著編輯狀態即時變化(按下會 toggle、游標移到粗體文字上時按鈕要亮起來),就得用 :handlers 註冊一個 kind。handler 裡給 canExecute、execute、isActive 三個 function,toolbar 會拿它們自動算出當下要不要反白、要不要禁用:

<script setup lang="ts">import type { EditorToolbarItem } from '@/registry/admin/ui/editor-toolbar'​const handlers = {  highlightLine: {    canExecute: (editor) => editor.can().toggleNode?.('paragraph', 'paragraph') ?? true,    execute: (editor) => editor.chain().focus().toggleMark('bold'),    isActive: (editor) => editor.isActive('bold'),  },}// satisfies 掛在自訂這組,自訂 kind 才有型別檢查:// 打錯或用了沒註冊的 kind 會在編譯期就報錯const items = [  ...defaultEditorItems,  [{ kind: 'highlightLine', label: '強調', tooltip: { text: '強調' } }] satisfies EditorToolbarItem<    typeof handlers  >[],]</script>​<template>  <admin-rich-editor v-slot="{ editor, handlers: mergedHandlers }" v-model="value" :handlers="handlers">    <admin-editor-toolbar :editor :handlers="mergedHandlers" :items="items" />  </admin-rich-editor></template>

toolbar 收的是外層 v-slot 吐出的 mergedHandlers(你的自訂 kind + StarterKit 內建 kind 已合併),這樣預設按鈕和自訂按鈕才會同時有 handler 可用。

無狀態的按鈕(用 onClick)

純觸發、不需要 active 同步的按鈕(插入內容、開外部視窗…)直接用 onClick,免註冊 kind。onClick 只收得到 MouseEvent、拿不到 editor,要操作 editor 就從 rich-editor 的 exposed ref 取:

<script setup lang="ts">import { IconSeparator } from '@tabler/icons-vue'import { useTemplateRef } from 'vue'import { AdminEditorToolbar, defaultEditorItems } from '@/registry/admin/ui/editor-toolbar'import { AdminRichEditor } from '@/registry/admin/ui/rich-editor'​const richEditor = useTemplateRef<InstanceType<typeof AdminRichEditor>>('richEditor')const items = [  ...defaultEditorItems,  [{    label: '插入分隔',    icon: IconSeparator,    onClick: () => richEditor.value?.editor?.commands.setHorizontalRule(),  }],]</script>​<template>  <admin-rich-editor ref="richEditor" v-slot="{ editor, handlers }" v-model="value">    <admin-editor-toolbar :editor :handlers="handlers" :items="items" />  </admin-rich-editor></template>
handler vs onClick 怎麼選
  • 要按鈕反白 / disabled 跟著編輯狀態變 → :handlers 註冊 kind(onClick 沒有狀態同步)。
  • 只是按一下做件事、長相固定 → onClick 即可。
  • item 同時有 kind 與 onClick 時,kind 優先、onClick 忽略。

完全自訂渲染(用 slot)

toolbar 開了兩種 slot,不用改 toolbar 內部即可塞 button / dropdown / 元件:

  • 通用 #item:攔下每一顆 item,全部自己畫
  • 具名 slot:在 item 上寫 slot: 'xxx',對應的 <template #xxx> 就會用你的元件取代那顆標準按鈕

兩種 slot 拿到的 props 都是 { item, index, isActive, isDisabled, onClick }(對齊 Nuxt UI)。這裡的 item 是原始資料,狀態要自己用 isActive(item)、isDisabled(item) 算,點擊呼叫 onClick(e, item)。props 裡沒有 editor,需要時從外層 <AdminRichEditor v-slot="{ editor }"> 拿。

<template>  <admin-rich-editor v-slot="{ editor, handlers }" v-model="value">    <admin-editor-toolbar :editor :handlers="handlers" :items="items">      <!-- slot props 無 editor;從外層 v-slot 的 editor 取 -->      <template #myPicker>        <my-custom-button :editor="editor" />      </template>    </admin-editor-toolbar>  </admin-rich-editor></template>
slot 內容取不到 provide 的 context

slot 內容定義在 consumer 作用域,取不到 <AdminRichEditor> provide 的 context。需要 editor 時用外層 <AdminRichEditor v-slot="{ editor }"> 的 editor,不要靠 inject。

範例:接一排對齊按鈕

文字對齊要三步:裝上官方 TextAlign extension、用 :handlers 註冊左/中/右三個 kind、再把對應按鈕加進 toolbar。按鈕的反白由 handler 的 isActive 自動同步:

<script setup lang="ts">import type { Editor, JSONContent } from '@tiptap/core'import type { EditorToolbarItem } from '@/components/admin/editor-toolbar'import { IconAlignCenter, IconAlignLeft, IconAlignRight } from '@tabler/icons-vue'import TextAlign from '@tiptap/extension-text-align'import { ref } from 'vue'import { cn } from '@/lib/utils'import { baseProseClass } from '@/components/admin/editor'import { AdminEditorToolbar, defaultEditorItems } from '@/components/admin/editor-toolbar'import { AdminRichEditor, starterKitProseClass } from '@/components/admin/rich-editor'​const value = ref<JSONContent | string>(  '<h2 style="text-align: center">置中標題</h2><p style="text-align: right">這段靠右對齊。</p><p>這段預設靠左。</p>')​// 1. 裝上官方 TextAlign extensionconst extensions = [TextAlign.configure({ types: ['heading', 'paragraph'] })]​// 2. 每個對齊方向註冊一個 kind:handler 提供 canExecute / execute / isActivefunction alignHandler(align: 'left' | 'center' | 'right') {  return {    canExecute: (editor: Editor) => editor.can().setTextAlign(align),    execute: (editor: Editor) => editor.chain().focus().setTextAlign(align),    isActive: (editor: Editor) => editor.isActive({ textAlign: align })  }}const handlers = {  alignLeft: alignHandler('left'),  alignCenter: alignHandler('center'),  alignRight: alignHandler('right')}​// 3. 用 kind 掛上按鈕,toolbar 會據 handler 自動算 active / disabled// satisfies 掛在自訂這組:打錯或用了沒註冊的 kind 會在編譯期就報錯const items = [  ...defaultEditorItems,  [    { kind: 'alignLeft', icon: IconAlignLeft, tooltip: { text: '靠左' } },    { kind: 'alignCenter', icon: IconAlignCenter, tooltip: { text: '置中' } },    { kind: 'alignRight', icon: IconAlignRight, tooltip: { text: '靠右' } }  ] satisfies EditorToolbarItem<typeof handlers>[]]</script>​<template>  <admin-rich-editor    v-slot="{ editor, handlers: mergedHandlers }"    v-model="value"    :extensions="extensions"    :handlers="handlers"    :ui="{ base: cn(baseProseClass, starterKitProseClass) }"  >    <admin-editor-toolbar :editor :handlers="mergedHandlers" :items="items" />  </admin-rich-editor></template>

不想自己接的話,文字顏色、螢光筆、對齊這類功能本系統都有封裝好的擴充元件可直接用,見 Text Color、Highlight、Text Align。