AIO 編輯器

AioEditor

參考
config 驅動的 all-in-one 富文本編輯器,傳入設定即得完整編輯器,無需組裝子元件。

安裝

pnpm dlx shadcn-vue@latest add @admin/aio-editor

安裝即取得 UI 與邏輯核心(tiptap extensions、預設 config、useAioEditor),核心置於元件內 core/ 子目錄,無需另裝其他單元。

用法

預設 config 即包含完整功能:36 個 extensions、35 顆 toolbar 控制項、7 種情境 bubble menu 與 drag handle。

<script setup lang="ts">import { ref } from 'vue'import { AdminAioEditor } from '@/components/admin/aio-editor'​const content = ref(`  <h2>AioEditor</h2>  <p>config 驅動的富文本編輯器,預設 toolbar 即包含<strong>格式</strong>、<em>樣式</em>、清單與區塊工具。</p>  <ul>    <li>項目清單</li>    <li>支援 <code>行內程式碼</code> 與 <mark>螢光標記</mark></li>  </ul>  <blockquote><p>引用區塊</p></blockquote>`)</script>​<template>  <admin-aio-editor v-model="content" /></template>

場景

自訂 toolbar 與 Custom 按鈕

config.features 以宣告陣列決定按鈕集合與順序(同時決定 extension 載入),Divider 分組;Custom 功能可帶 icon / slot / 自帶 extensions。bubble 跟隨宣告自動掛載,dragHandle 可停用。

<script setup lang="ts">import type { AioEditorConfig } from '@/components/admin/aio-editor'import { IconSparkles } from '@tabler/icons-vue'import { ref } from 'vue'import { AdminAioEditor, AioEditorFeature } from '@/components/admin/aio-editor'​const content = ref('<p>精簡 toolbar:只放常用按鈕,並示範 Custom 按鈕。</p>')​const config: AioEditorConfig = {  features: [    AioEditorFeature.FontBold,    AioEditorFeature.FontItalic,    AioEditorFeature.Highlight,    AioEditorFeature.Divider,    AioEditorFeature.BulletList,    AioEditorFeature.Blockquote,    AioEditorFeature.Divider,    {      kind: AioEditorFeature.Custom,      label: '插入問候',      icon: IconSparkles,      run: (editor) => editor.chain().focus().insertContent('👋 你好!').run()    }  ],  // bubble 跟隨 features 宣告自動掛載(此處僅 BulletList 會帶 List bubble);drag handle 另關  dragHandle: false}</script>​<template>  <admin-aio-editor v-model="content" :config="config" /></template>

表格

Table 功能載入 TableKit(Table / TableRow / TableCell / TableHeader)。游標進入表格時依位置掛載 Table / TableColumn / TableRow 三種 bubble,欄寬可拖曳框線調整。

<script setup lang="ts">import type { AioEditorConfig } from '@/components/admin/aio-editor'import { ref } from 'vue'import { AdminAioEditor, AioEditorFeature } from '@/components/admin/aio-editor'​const content = ref(`  <p>將游標移入表格即出現欄 / 列控制 bubble:可增刪欄列、合併與拆分儲存格。</p>  <table>    <tbody>      <tr>        <th>項目</th>        <th>說明</th>      </tr>      <tr>        <td>欄寬</td>        <td>拖曳框線調整(resizable)</td>      </tr>      <tr>        <td>控制項</td>        <td>Table / TableColumn / TableRow 三種 bubble 跟隨游標位置</td>      </tr>    </tbody>  </table>`)​const config: AioEditorConfig = {  features: [    AioEditorFeature.Undo,    AioEditorFeature.Redo,    AioEditorFeature.Divider,    AioEditorFeature.FontBold,    AioEditorFeature.Divider,    AioEditorFeature.Table  ]}</script>​<template>  <admin-aio-editor v-model="content" :config="config" /></template>

圖片與 requestUrl

Image 功能之插入來源走 config.toolbarOptions.requestUrl(Link / Video 共用同一入口,以 kind 區分);未提供時 fallback 為 window.prompt。實務上在此接上傳對話框或素材庫,回傳 null 即取消。點選圖片出現 Image bubble,可切換對齊與寬度。

<script setup lang="ts">import type { AioEditorConfig } from '@/components/admin/aio-editor'import { ref } from 'vue'import { AdminAioEditor, AioEditorFeature } from '@/components/admin/aio-editor'​const content = ref(`  <p>點選圖片即出現 Image bubble:可切換對齊、調整寬度或移除。</p>  <img src="/demo/sample.gif" alt="範例圖片" />`)​// requestUrl 為 Link / Image / Video 之 URL 輸入來源;未提供時 fallback 為 window.prompt。// 實務上這裡接上傳對話框或素材庫,回傳 null 代表取消。const config: AioEditorConfig = {  features: [    AioEditorFeature.Undo,    AioEditorFeature.Redo,    AioEditorFeature.Divider,    AioEditorFeature.AlignLeft,    AioEditorFeature.AlignCenter,    AioEditorFeature.Divider,    AioEditorFeature.Image  ],  toolbarOptions: {    requestUrl: ({ kind, previous }) => {      if (kind !== 'image') {        return previous ?? null      }      return '/demo/sample.gif'    }  }}</script>​<template>  <admin-aio-editor v-model="content" :config="config" /></template>

前台渲染輸出 HTML

編輯器內容樣式已拆為共通 prose 樣式 adminAioEditorProseClass:前台(或任何唯讀情境)渲染編輯器產出的 HTML 時,把它套在容器上即可對齊編輯時外觀,不需掛編輯器。

<script setup lang="ts">import { adminAioEditorProseClass } from '@/components/admin/aio-editor'import { cn } from '@/lib/utils'</script>​<template>  <div :class="cn(adminAioEditorProseClass)" v-html="htmlFromEditor"></div></template>
<script setup lang="ts">import { ref } from 'vue'import { cn } from '@/lib/utils'import { AdminAioEditor, adminAioEditorProseClass } from '@/components/admin/aio-editor'​const content = ref(`  <h3>發布預覽</h3>  <p>左側編輯,右側即為<strong>前台渲染</strong>結果——同一份 prose 樣式。</p>  <ul>    <li>清單與 <code>行內程式碼</code></li>  </ul>  <blockquote><p>引用區塊</p></blockquote>`)</script>​<template>  <div    :class="`      grid w-full gap-4      lg:grid-cols-2    `"  >    <admin-aio-editor v-model="content" />    <div      :class="cn(`rounded-[6px] border border-dashed border-text-30 p-3`, adminAioEditorProseClass)"      v-html="content"    ></div>  </div></template>

Mention(opt-in)

Mention / HashTag 不在預設集合——需經 config.include 啟用,並以 mentionSuggestion 的 runtime hooks 提供選單渲染實作(選單 UI 由 consumer 全權決定)。

<script setup lang="ts">import type { SuggestionProps } from '@tiptap/suggestion'import type { AioEditorConfig } from '@/components/admin/aio-editor'import { ref } from 'vue'import { cn } from '@/lib/utils'import {  AdminAioEditor,  AioEditorCoreExtension,  AioEditorFeature,  mentionSuggestion} from '@/components/admin/aio-editor'​const content = ref('<p>輸入 @ 觸發 mention 選單。</p>')​const MEMBERS = ['王小明', '陳美麗', '林大同', '張雅婷']​// mention 選單為 consumer 自行實作:以 runtime hooks 掛一個最小清單 UIfunction renderMenu(mountEl: HTMLElement, props: SuggestionProps) {  mountEl.innerHTML = ''  const items = MEMBERS.filter((name) => name.includes(props.query))  for (const name of items) {    const button = document.createElement('button')    button.type = 'button'    button.textContent = name    button.className = 'aio-mention-option'    button.addEventListener('mousedown', (event) => {      event.preventDefault()      props.command({ id: name, label: name })    })    mountEl.appendChild(button)  }  return mountEl}​const config: AioEditorConfig = {  include: [AioEditorCoreExtension.Mention],  extensionsConfig: {    Mention: {      suggestion: {        ...mentionSuggestion({          onRender: (props, mountEl) => renderMenu(mountEl!, props as SuggestionProps),          onUpdate: (props, componentEl) => {            if (componentEl) {              renderMenu(componentEl, props as SuggestionProps)            }          }        }),        items: ({ query }) => MEMBERS.filter((name) => name.includes(query))      }    }  },  features: [AioEditorFeature.FontBold, AioEditorFeature.FontItalic],  dragHandle: false}</script>​<template>  <admin-aio-editor    v-model="content"    :class="      cn(`        [&_.aio-mention-option]:block [&_.aio-mention-option]:w-full        [&_.aio-mention-option]:rounded-[4px] [&_.aio-mention-option]:px-3        [&_.aio-mention-option]:py-1 [&_.aio-mention-option]:text-left        [&_.aio-mention-option]:hover:bg-bg-70        [&_.editor-mention]:text-primary-40        [&_.editor-suggestion]:z-10 [&_.editor-suggestion]:rounded-[6px]        [&_.editor-suggestion]:border [&_.editor-suggestion]:border-text-30        [&_.editor-suggestion]:bg-bg-90 [&_.editor-suggestion]:p-1 [&_.editor-suggestion]:shadow-md      `)    "    :config="config"  /></template>

API 參考

aio-editor

Props

名稱型別預設值說明
configAioEditorConfig | undefined—編輯器設定:features / extensions / 選項覆寫,未指定欄位取預設值
modelValuestring | JSONContent | undefined—內容(v-model):依 contentType 解讀;未指定 contentType 時 string 視為 html、物件視為 json
contentType"json" | "html" | undefined—內容型別(顯式優先於自動推斷)
editableboolean | undefinedtrue是否可編輯
idstring | undefined—元件 id:多編輯器頁面之辨識,寫入 hooks context 與 editor.storage.aio.id;未提供自動產生
autofocusFocusPosition | undefinedfalse掛載後自動聚焦位置(true = 'start';false 不聚焦)
enableInputRulesboolean | ((names: string[]) => boolean | string[]) | undefinedtrue是否啟用輸入規則;函式形接收最終 extension 名單,回傳 boolean 或要啟用的名稱陣列
enablePasteRulesboolean | ((names: string[]) => boolean | string[]) | undefinedtrue是否啟用貼上規則;函式形同 enableInputRules
toolbarboolean | undefinedtrue是否顯示工具列(false 隱藏,extensions 照常載入)
toolbarTargetstring | HTMLElement | undefined—工具列外掛目標:CSS selector 或 HTMLElement(Teleport),未指定渲染於編輯器頂部
ui{ base?: string | undefined; content?: string | undefined; toolbar?: string | undefined; bubble?: string | undefined; } | undefined—各部位 class 覆寫
classClassValue—root 之 class

Events

名稱Payload說明
blurany—
focusany—
keydownany—
update:modelValueany—
createany—
updateany—
selectionUpdateany—
destroyany—
contentErrorany—

Slots

名稱Bindings說明
default{ editor: Editor | undefined; editorId: string; }預設 slot:置於內容區之後,暴露 editor 實例供進階操作
footer{ editor: Editor | undefined; editorId: string; }底部列:預設渲染字數統計(設定 CharacterCount limit 時),提供此 slot 可完全自訂

Expose

名稱簽名說明
editorEditor | undefinedtiptap Editor 實例(mounted 前為 undefined)

config 之完整型別(AioEditorConfig、AioEditorExtensionsConfig、enums 與各 default*Config)由 @admin/aio-editor 匯出,各 extension 的完整選項說明見 Extensions 總覽。

注意事項

  • URL 輸入來源:Link / Image / Video 按鈕預設以 window.prompt 取得 URL;提供 config.toolbarOptions.requestUrl(ctx 帶 editor / editorId / target)即可換成自訂 UI。連結編輯已內建於 LinkTips bubble 的 inline 輸入框。
  • 不允許的連結網址:網址被 Link extension 的 isAllowedUri 拒絕(如 javascript:)時不會套用,LinkTips 輸入框還原為原網址;提供 config.toolbarOptions.onInvalidLink(ctx 帶 url,LinkTips 觸發時 target 為輸入框)即可顯示自訂提示。選取範圍本身不能加連結(程式碼區塊、行內程式碼)時同樣不會套用,但不屬網址問題,不會呼叫 onInvalidLink。
  • 色盤:TextColor / Highlight 內建 @admin/color-picker 色票(toolbarOptions.textColorSwatches / highlightSwatches 自訂調色盤);要自由取色提供 requestColor。value: '' 為清除項;完全透明之色票(transparent、#0000、rgba(…, 0) 等)於浮層中會加上外框以便辨識。
  • 工具列版面:toolbar: false 隱藏(extensions 照載);toolbarTarget 可把工具列 Teleport 到任意容器(多編輯器共用一條、頂部固定列、浮動容器)。
  • 事件:create / update / selectionUpdate / focus / blur / destroy / contentError / keydown 透傳,payload 統一帶 { editor, editorId }——多編輯器頁面以 editorId(或 id prop)辨識來源。
  • 圖片 / 影片上傳:貼上與拖放檔案會觸發 extensionsConfig.Image.pasteAndDropHandle / extensionsConfig.Video.pasteAndDropHandle,上傳邏輯由 consumer 實作後回呼 afterImageUpload / afterVideoUpload。未實作時僅於 console 提示。
  • Mention / HashTag:無按鈕功能,經 config.include 啟用並提供 suggestion onRender 實作。UndoRedo / Code 同理——未宣告按鈕時要保留能力就 include。
  • v-model 型別:綁 string 視為 HTML、綁物件視為 JSON(tiptap JSONContent)。不掛編輯器的轉換用 editorJson2Html / editorHtml2Json(瀏覽器環境限定)。
  • 外部寫入不觸發事件:父層透過 v-model 寫入內容、或切換 editable 時,不會 emit update:modelValue 與 update——父層拿到的值維持原樣,不會被換成編輯器正規化後的 HTML,表單載入後也不會被判為已修改。只有使用者編輯(含透過 editor 實例執行之 command)才會觸發;字數統計則一律隨內容更新。
  • exclude 一律生效:exclude 對任何 extension(含 schema 必備之 Document / Text / Paragraph)都會生效、不自動保留。排除後以 extraExtensions 或 Custom extensions 提供同名 extension 即視為替換、不警告;未替換且有依賴者受影響時 console 警告;schema 必備項於最終清單缺席時給最強警告(編輯器將無法初始化)。toolbar 按鈕依 extension 啟用狀態自動隱藏。

更新紀錄

  • fix: 透明色票之其他寫法也套用清除項外框4831781
  • fix: 選取範圍無法加連結時不再誤觸 onInvalidLink92db11a
  • refactor: 工具列 Link 改以 can() 判定網址是否允許5e30a51
  • fix: 清除色票外框與勾勾僅套用於透明色票f79356c
  • fix: 外部載入內容後字數統計未更新d7a90ba
  • fix: 父層設回先前的 v-model 值時未同步編輯器內容fe06783
  • fix: 修正清除色票在顏色浮層中看不見51d18fe
  • fix: exclude 搭配同名替換時不再印出警告e25a859
  • feat: 新增 toolbarOptions.onInvalidLink 處理不允許的連結網址6aeeeb9
  • fix: 父層同步 v-model 或 editable 時不再回寫 update:modelValue1f67b83
  • fix: 修正 active 按鈕懸停時底色反而變淺174feba
  • fix: 移除工具列 active 按鈕會隨圓角彎曲的底線48d0f73
  • fix: 修正文字顏色與螢光標記按鈕懸停時沒有 tooltip92ed8b3
  • feat: 新增 config 驅動的 all-in-one 編輯器系列bc3086e