Skip to content
全部文档

富文本编辑器

简介

富文本编辑器可让你编辑和预览 HTML 内容,以及上传图片。它使用 TipTap 作为底层编辑器。

php
use Filament\Forms\Components\RichEditor;

RichEditor::make('content')
富文本编辑器富文本编辑器

配置 Livewire 的最大嵌套深度

富文本编辑器以嵌套数据形式将其 TipTap 文档与 Livewire 同步。Livewire 默认将嵌套属性路径限制为 10 层,这对列表和表格等结构可能不够。若遇到 Livewire\Exceptions\MaxNestingDepthExceededException,且应用尚无 config/livewire.php 文件,请发布 Livewire 的配置文件:

bash
php artisan livewire:publish --config

该命令会覆盖已有的 config/livewire.php 文件,因此若已发布过配置请跳过。

然后,提高 config/livewire.php 中现有的 max_nesting_depth 设置。例如,深度 32 可为深度嵌套的富文本内容留出空间:

php
'payload' => [
    // ...
    'max_nesting_depth' => 32,
],

请仅更改现有 payload 数组中的 max_nesting_depth 值,以保留 Livewire 其他与版本相关的 payload 设置。

以 JSON 存储内容

默认情况下,富文本编辑器以 HTML 存储内容。若希望改为以 JSON 存储,可以使用 json() 方法:

php
use Filament\Forms\Components\RichEditor;

RichEditor::make('content')
    ->json()

该 JSON 采用 TipTap 格式,是内容的结构化表示。

若使用 Eloquent 保存 JSON 内容,请务必在模型属性上添加 array 转换(cast)

php
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'content' => 'array',
        ];
    }

    // ...
}

自定义工具栏按钮

你可以使用 toolbarButtons() 方法设置编辑器的工具栏按钮。此处显示的是默认选项:

php
use Filament\Forms\Components\RichEditor;

RichEditor::make('content')
    ->toolbarButtons([
        ['bold', 'italic', 'underline', 'strike', 'subscript', 'superscript', 'link'],
        ['h2', 'h3'],
        ['alignStart', 'alignCenter', 'alignEnd'],
        ['blockquote', 'codeBlock', 'bulletList', 'orderedList'],
        ['table', 'attachFiles'], // The `customBlocks` and `mergeTags` tools are also added here if those features are used.
        ['undo', 'redo'],
    ])

主数组中的每个嵌套数组代表工具栏中的一组按钮。

带自定义工具栏按钮的富文本编辑器带自定义工具栏按钮的富文本编辑器

工具栏中还可使用的其他工具包括:

  • h1 - 对文本应用「h1」标签。
  • h4 - 对文本应用「h4」标签。
  • h5 - 对文本应用「h5」标签。
  • h6 - 对文本应用「h6」标签。
  • alignJustify - 两端对齐文本。
  • clearFormatting - 清除所选文本的所有格式。
  • details - 插入 <details> 标签,允许用户在内容中创建可折叠区块。
  • grid - 在编辑器中插入网格布局,允许用户创建响应式内容列。
  • gridDelete - 删除当前网格布局。
  • highlight - <mark> 标签高亮所选文本。
  • horizontalRule - 插入水平分隔线。
  • lead - 在文本周围应用 lead 类,通常用于文章首段。
  • paragraph - 将当前块设为段落,并移除任何标题格式。
  • small - 对文本应用 <small> 标签,通常用于小号字体或免责声明。
  • code - 将所选文本格式化为行内代码。
  • textColor - 更改所选文本的文本颜色
  • table - 在编辑器中创建默认 3 列 2 行的表格,第一行配置为表头行。
  • tableAddColumnBefore - 在当前列之前添加新列。
  • tableAddColumnAfter - 在当前列之后添加新列。
  • tableDeleteColumn - 删除当前列。
  • tableAddRowBefore - 在当前行上方添加新行。
  • tableAddRowAfter - 在当前行下方添加新行。
  • tableDeleteRow - 删除当前行。
  • tableMergeCells - 将所选单元格合并为一个。
  • tableSplitCell - 将所选单元格拆分为多个单元格。
  • tableToggleHeaderRow - 切换表格的表头行。
  • tableToggleHeaderCell - 切换表格的表头单元格。
  • tableDelete - 删除表格。

TIP

除了允许静态值外,toolbarButtons() 方法也接受函数以动态计算。你可以将各种工具作为参数注入到该函数中。

自定义浮动工具栏

若工具栏过于拥挤,可以使用浮动工具栏:仅当用户位于特定节点类型内时,在光标下方的工具栏中显示某些工具。这样可保持主工具栏简洁,同时在需要时仍能访问额外工具。

你可以使用 floatingToolbars() 方法自定义光标位于特定节点内时出现的浮动工具栏。

在下面的示例中,当光标位于段落节点内时会出现浮动工具栏,显示粗体、斜体等按钮。当光标在标题节点中时,显示与标题相关的按钮;在表格内时,显示表格专用控件。

php
use Filament\Forms\Components\RichEditor;

RichEditor::make('content')
    ->floatingToolbars([
        'paragraph' => [
            'bold', 'italic', 'underline', 'strike', 'subscript', 'superscript',
        ],
        'heading' => [
            'h1', 'h2', 'h3',
        ],
        'table' => [
            'tableAddColumnBefore', 'tableAddColumnAfter', 'tableDeleteColumn',
            'tableAddRowBefore', 'tableAddRowAfter', 'tableDeleteRow',
            'tableMergeCells', 'tableSplitCell',
            'tableToggleHeaderRow', 'tableToggleHeaderCell',
            'tableDelete',
        ],
    ])
所选文本下方带浮动工具栏的富文本编辑器所选文本下方带浮动工具栏的富文本编辑器

将工具栏按钮分组到下拉菜单

你可以使用 ToolbarButtonGroup 将相关工具栏按钮分组到下拉菜单。第一个参数是用于下拉菜单提示与无障碍访问的标签,第二个参数是要包含在下拉菜单中的按钮名称数组:

php
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\RichEditor\ToolbarButtonGroup;

RichEditor::make('content')
    ->toolbarButtons([
        ['bold', 'italic', 'underline', 'strike'],
        [ToolbarButtonGroup::make('Paragraph', ['paragraph', 'h1', 'h2', 'h3'])],
        [ToolbarButtonGroup::make('Alignment', ['alignStart', 'alignCenter', 'alignEnd', 'alignJustify'])],
        ['blockquote', 'codeBlock', 'bulletList', 'orderedList'],
        ['undo', 'redo'],
    ])

默认情况下,第一个按钮的图标用作下拉触发器,并会响应式更新以反映当前激活的按钮。点击触发器会显示分组按钮。

你可以使用 icon() 方法为下拉触发器设置固定图标。设置自定义图标后,触发器图标保持静态,不会随激活按钮变化:

php
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\RichEditor\ToolbarButtonGroup;

RichEditor::make('content')
    ->toolbarButtons([
        ['bold', 'italic', 'underline', 'strike'],
        [ToolbarButtonGroup::make('Heading', ['h1', 'h2', 'h3'])->icon('fi-o-heading')],
        [ToolbarButtonGroup::make('Alignment', ['alignStart', 'alignCenter', 'alignEnd', 'alignJustify'])],
        ['blockquote', 'codeBlock', 'bulletList', 'orderedList'],
        ['undo', 'redo'],
    ])
打开了工具栏按钮组下拉菜单的富文本编辑器打开了工具栏按钮组下拉菜单的富文本编辑器

使用带文本的下拉工具栏按钮

默认情况下,下拉工具栏按钮仅显示图标。若希望在下拉项中同时显示图标与文本标签,可以在 ToolbarButtonGroup 上使用 textualButtons() 方法:

php
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\RichEditor\ToolbarButtonGroup;

RichEditor::make('content')
    ->toolbarButtons([
        ['bold', 'italic', 'underline', 'strike', 'link'],
        [ToolbarButtonGroup::make('Paragraph', ['paragraph', 'h1', 'h2', 'h3'])->textualButtons()],
        [ToolbarButtonGroup::make('Alignment', ['alignStart', 'alignCenter', 'alignEnd', 'alignJustify'])],
        ['blockquote', 'codeBlock', 'bulletList', 'orderedList'],
        ['undo', 'redo'],
    ])
打开了带文本的工具栏按钮组下拉菜单的富文本编辑器打开了带文本的工具栏按钮组下拉菜单的富文本编辑器

在此示例中,Paragraph 下拉项会同时显示图标与文本标签(例如「Paragraph」「Heading 1」)。Alignment 下拉菜单仍仅显示图标。

设置高度

你可以通过定义 minHeight()maxHeight() 方法控制编辑器高度,它们接受任意 CSS 长度值:

php
use Filament\Forms\Components\RichEditor;

RichEditor::make('content')
    ->minHeight('12rem')
    ->maxHeight('24rem')

编辑器默认最小高度为 10rem。内容超出 maxHeight() 后,编辑器停止增高并变为可滚动。每个方法可单独使用——minHeight() 设置起始高度同时仍允许增高,maxHeight() 限制最大高度。向 minHeight() 传入 null 可使用编辑器固有的 3rem 最小高度;向 maxHeight() 传入 null 可取消上限。这些约束在编辑器禁用时同样适用。

TIP

除了允许静态值外,minHeight()maxHeight() 方法也接受函数以动态计算。你可以将各种工具作为参数注入到这些函数中。

自定义文本颜色

富文本编辑器包含用于设置行内文本样式的文本颜色工具。默认使用 Tailwind CSS 调色板。浅色模式下对文本应用 600 色阶,深色模式下使用 400 色阶。

你可以使用 textColors() 方法自定义选择器中可用的颜色:

php
use Filament\Forms\Components\RichEditor;

RichEditor::make('content')
    ->textColors([
        '#ef4444' => 'Red',
        '#10b981' => 'Green',
        '#0ea5e9' => 'Sky',
    ])
富文本编辑器文本颜色选择器模态框富文本编辑器文本颜色选择器模态框

若希望为浅色与深色模式定义不同颜色,可以使用 TextColor 对象定义颜色:

php
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\RichEditor\TextColor;

RichEditor::make('content')
    ->textColors([
        'brand' => TextColor::make('Brand', '#0ea5e9'),
        'warning' => TextColor::make('Warning', '#f59e0b', darkColor: '#fbbf24'),
    ])

若希望在现有 Tailwind 调色板上添加新颜色,可以将你的颜色合并到 TextColor::getDefaults() 数组中:

php
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\RichEditor\TextColor;

RichEditor::make('content')
    ->textColors([
        'brand' => TextColor::make('Brand', '#0ea5e9'),
        'warning' => TextColor::make('Warning', '#f59e0b', darkColor: '#fbbf24'),
        ...TextColor::getDefaults(),
    ])

使用 TextColor 对象时,数组的键会成为 <span> 标签上存储的 data-color 属性,便于在 CSS 中引用该颜色。当以颜色作为数组值时,实际颜色值(例如 HEX 字符串)会存储为 data-color 属性。

你也可以将 textColors() 传给内容渲染器富文本内容属性,使服务端渲染与编辑器配置一致。

你还可以使用 customTextColors() 方法,允许用户选择预定义列表之外的自定义颜色:

php
use Filament\Forms\Components\RichEditor;

RichEditor::make('content')
    ->textColors([
        // ...
    ])
    ->customTextColors()

无需在内容渲染器上使用 customTextColors(),它会自动渲染内容中使用的任何自定义颜色。

渲染富文本内容

若你以 JSON 而非 HTML 存储内容,或内容需要处理以注入私有图片 URL 等,则需要使用 Filament 中的 RichContentRenderer 工具输出 HTML:

php
use Filament\Forms\Components\RichEditor\RichContentRenderer;

RichContentRenderer::make($record->content)->toHtml()

toHtml() 方法返回字符串。若希望在 Blade 视图中输出 HTML 且不转义,可以不调用 toHtml() 而直接 echo RichContentRender 对象:

blade
{{ \Filament\Forms\Components\RichEditor\RichContentRenderer::make($record->content) }}

若已配置编辑器的文件附件行为以更改上传文件的磁盘或可见性,也必须将这些设置传给渲染器,以确保生成正确的 URL:

php
use Filament\Forms\Components\RichEditor\RichContentRenderer;

RichContentRenderer::make($record->content)
    ->fileAttachmentsDisk('s3')
    ->fileAttachmentsVisibility('private')
    ->toHtml()

若在富文本编辑器中使用自定义块,可以向渲染器传入自定义块数组,以确保它们正确渲染:

php
use Filament\Forms\Components\RichEditor\RichContentRenderer;

RichContentRenderer::make($record->content)
    ->customBlocks([
        HeroBlock::class => [
            'categoryUrl' => $record->category->getUrl(),
        ],
        CallToActionBlock::class,
    ])
    ->toHtml()

若使用合并标签,可以传入用于替换合并标签的值数组:

php
use Filament\Forms\Components\RichEditor\RichContentRenderer;

RichContentRenderer::make($record->content)
    ->mergeTags([
        'name' => $record->user->name,
        'today' => now()->toFormattedDateString(),
    ])
    ->toHtml()

若使用自定义文本颜色,可以向渲染器传入颜色数组,以确保颜色正确渲染:

php
use Filament\Forms\Components\RichEditor\RichContentRenderer;
use Filament\Forms\Components\RichEditor\TextColor;

RichContentRenderer::make($record->content)
    ->textColors([
        'brand' => TextColor::make('Brand', '#0ea5e9', darkColor: '#38bdf8'),
    ])
    ->toHtml();

为渲染内容设置样式

富文本编辑器的 HTML 根据所用功能,结合 HTML 元素、CSS 类与行内样式来呈现内容。若在 Filament 表格列或 infolist 条目中用 prose() 渲染内容,Filament 会自动应用所需样式。若在自己的 Blade 视图中输出内容,可能需要添加一些额外样式以确保正确显示。

为内容设置样式的一种方式是使用 Tailwind CSS Typography。该插件为标题、段落、列表、表格等常见 HTML 元素提供一组预定义样式。你可以使用 prose 类将这些样式应用到容器元素:

blade
<div class="prose dark:prose-invert">
    {!! \Filament\Forms\Components\RichEditor\RichContentRenderer::make($record->content) !!}
</div>

不过,网格布局和文本颜色等功能需要 Tailwind CSS Typography 插件未包含的额外样式。Filament 还提供自己的 fi-prose CSS 类来补充这些样式。任何加载 Filament 的 vendor/filament/support/resources/css/index.css 的应用都可以使用该类。其样式与 prose 类不同,但更契合 Filament 的设计系统:

blade
<div class="fi-prose">
    {!! \Filament\Forms\Components\RichEditor\RichContentRenderer::make($record->content) !!}
</div>

安全性

默认情况下,编辑器输出原始 HTML 并发送到后端。攻击者可能拦截组件的值并向后端发送不同的原始 HTML 字符串。因此,输出富文本编辑器的 HTML 时必须进行清理;否则站点可能面临跨站脚本(XSS)漏洞。

当 Filament 在 TextColumnTextEntry 等组件中从数据库输出原始 HTML 时,会清理以移除危险的 JavaScript。但若在自己的 Blade 视图中输出富文本编辑器的 HTML,则由你负责。一种做法是使用 Filament 的 sanitizeHtml() 辅助方法,这与上述组件中用于清理 HTML 的工具相同:

blade
{!! str($record->content)->sanitizeHtml() !!}

若以 JSON 而非 HTML 存储内容,或内容需要处理以注入私有图片 URL 等,可以使用内容渲染器输出 HTML。它会自动清理 HTML,因此你无需担心。

DANGER

Filament 内置的 HTML 清理器允许行内 style 属性,以支持字体颜色、文本高亮、图片尺寸等富文本格式功能。这意味着 background: url(...)position: fixed 等 CSS 属性不会从已清理的 HTML 中剥离。若内容来自不可信用户,应考虑限制默认配置。有关如何自定义清理器,请参阅安全文档

向编辑器上传图片

默认情况下,上传的图片会公开存储在你的存储磁盘上,以便数据库中的富文本内容可在任意处轻松输出。你可以使用配置方法自定义图片上传方式:

php
use Filament\Forms\Components\RichEditor;

RichEditor::make('content')
    ->fileAttachmentsDisk('s3')
    ->fileAttachmentsDirectory('attachments')
    ->fileAttachmentsVisibility('private')

TIP

除了允许静态值外,fileAttachmentsDisk()fileAttachmentsDirectory()fileAttachmentsVisibility() 方法也接受函数以动态计算。你可以将各种工具作为参数注入到该函数中。

TIP

Filament 还支持使用 spatie/laravel-medialibrary 存储富文本编辑器文件附件。更多信息请参阅我们的插件文档

在编辑器中使用私有图片

在编辑器中使用私有图片会增加一层复杂性,因为私有图片无法通过永久 URL 直接访问。每次加载编辑器或渲染其内容时,都需要为每张图片生成临时 URL,且这些 URL 不会存入数据库。相反,Filament 会在图片标签上添加 data-id 属性,其中包含存储磁盘上该图片的标识符,以便按需生成临时 URL。

使用私有图片渲染内容时,请确保使用 Filament 中的 RichContentRenderer 工具 输出 HTML:

php
use Filament\Forms\Components\RichEditor\RichContentRenderer;

RichContentRenderer::make($record->content)
    ->fileAttachmentsDisk('s3')
    ->fileAttachmentsVisibility('private')
    ->toHtml()

保护文件附件 ID

图片节点上的 data-id 属性是已配置磁盘上文件的标识符。渲染内容时,Filament 会为其生成 URL——若可见性为 private,则为签名临时 URL。与其他 Livewire 表单字段值一样,内容及其 data-id 属性由客户端控制:请求可被拦截,从而将 data-id 改为同一磁盘上的任意其他标识符。若该磁盘还存储属于其他用户或记录的文件,攻击者可能使渲染内容引用(并提供其签名 URL)他人的文件。

Filament 默认允许此行为,因为合法功能依赖它——例如从已有库插入图片的操作,或「从另一条记录复制」按钮。若你的编辑器都不依赖此类流程,可在字段上调用 preventFileAttachmentPathTampering() 以启用内置检查:

php
use Filament\Forms\Components\RichEditor;

RichEditor::make('content')
    ->preventFileAttachmentPathTampering()

Filament 会解析记录的原始内容(通过 $record->getOriginal() 获取与字段名匹配的属性),并仅允许已有的 data-id 值。任何其他已有的 data-id 都会导致字段验证失败,因此记录绝不会以被篡改的值保存。新上传的图片始终可通过。

默认文件附件提供者不做按记录范围限定——除非启用 preventFileAttachmentPathTampering()(或在磁盘/目录级别隔离上传),否则任何解析到已配置磁盘上文件的 data-id 都会被接受。若改用 spatie/laravel-medialibrary 插件 作为文件附件提供者,此保护已隐式生效——它通过 $media->has($file) 对照记录自身的媒体集合查找每个 data-id,因此另一条记录媒体的 data-id 会自动被拒绝。

WARNING

preventFileAttachmentPathTampering() 需要表单上有记录。若没有(例如在创建页),除非 allowFilePathUsing 回调批准,否则每个已有的 data-id 都会验证失败。新上传不受影响。

若要在不逐个字段重复的情况下,对应用中每个 RichEditor 应用此检查,请在服务提供者的 boot() 方法中调用 configureUsing()

php
use Filament\Forms\Components\RichEditor;

RichEditor::configureUsing(function (RichEditor $component): void {
    $component->preventFileAttachmentPathTampering();
});

各个字段仍可通过调用 preventFileAttachmentPathTampering(false) 退出。

#### 使用回调允许额外的 `data-id` 值 {#allowing-additional-data-id-values-with-a-callback}

若应用合法地引用了记录上不存在的标识符——例如「从另一条记录复制」操作——请传入 allowFilePathUsing 参数以批准它。已批准的标识符会绕过验证错误:

php
use Filament\Forms\Components\RichEditor;

RichEditor::make('content')
    ->preventFileAttachmentPathTampering(
        allowFilePathUsing: fn (string $file): bool => str_starts_with($file, 'templates/'),
    )

TIP

你可以将各种工具作为参数注入到传给 allowFilePathUsing 的函数中。

验证错误消息可通过 validationMessages() 使用 tampered 键自定义:

php
use Filament\Forms\Components\RichEditor;

RichEditor::make('content')
    ->preventFileAttachmentPathTampering()
    ->validationMessages([
        'tampered' => 'The content references an image that is not permitted.',
    ])

验证上传的图片

你可以使用 fileAttachmentsAcceptedFileTypes() 方法控制上传图片可接受的 MIME 类型列表。默认接受 image/pngimage/jpegimage/gifimage/webp

php
use Filament\Forms\Components\RichEditor;

RichEditor::make('content')
    ->fileAttachmentsAcceptedFileTypes(['image/png', 'image/jpeg'])

你可以使用 fileAttachmentsMaxSize() 方法控制上传图片的最大文件大小。大小以千字节指定。默认最大大小为 12288 KB(12 MB):

php
use Filament\Forms\Components\RichEditor;

RichEditor::make('content')
    ->fileAttachmentsMaxSize(5120) // 5 MB

允许用户调整图片大小

默认情况下,用户无法调整编辑器中图片的大小。你可以使用 resizableImages() 方法启用图片调整大小:

php
use Filament\Forms\Components\RichEditor;

RichEditor::make('content')
    ->resizableImages()

启用后,用户可通过点击图片并拖动调整手柄来调整大小。调整大小时始终保持宽高比。

TIP

除了允许静态值外,resizableImages() 方法也接受函数以动态计算。你可以将各种工具作为参数注入到该函数中。

使用自定义块

自定义块是用户可拖放到富文本编辑器中的元素。你可以使用 customBlocks() 方法定义用户可插入富文本编辑器的自定义块:

php
use Filament\Forms\Components\RichEditor;

RichEditor::make('content')
    ->customBlocks([
        HeroBlock::class,
        CallToActionBlock::class,
    ])
打开了自定义块面板的富文本编辑器打开了自定义块面板的富文本编辑器

要创建自定义块,可以使用以下命令:

bash
php artisan make:filament-rich-content-custom-block HeroBlock

每个块需要一个继承 Filament\Forms\Components\RichEditor\RichContentCustomBlock 类的对应类。getId() 方法应返回块的唯一标识符,getLabel() 方法应返回将在编辑器侧栏中显示的标签:

php
use Filament\Forms\Components\RichEditor\RichContentCustomBlock;

class HeroBlock extends RichContentCustomBlock
{
    public static function getId(): string
    {
        return 'hero';
    }

    public static function getLabel(): string
    {
        return 'Hero section';
    }
}

当用户将自定义块拖入编辑器时,你可以选择在插入块之前打开模态框以收集额外信息。为此,可以使用 configureEditorAction() 方法配置插入块时打开的模态框

php
use Filament\Actions\Action;
use Filament\Forms\Components\RichEditor\RichContentCustomBlock;

class HeroBlock extends RichContentCustomBlock
{
    // ...

    public static function configureEditorAction(Action $action): Action
    {
        return $action
            ->modalDescription('Configure the hero section')
            ->schema([
                TextInput::make('heading')
                    ->required(),
                TextInput::make('subheading'),
            ]);
    }
}

action 上的 schema() 方法可定义将在模态框中显示的表单字段。用户提交表单后,表单数据会保存为该块的「配置」。

为自定义块渲染预览

块插入编辑器后,你可以使用 toPreviewHtml() 方法为其定义「预览」。该方法应返回一段 HTML 字符串,在块插入时显示在编辑器中,让用户在保存前看到块的外观。你可以在此方法中访问块的 $config,其中包含插入块时在模态框中提交的数据:

php
use Filament\Forms\Components\RichEditor\RichContentCustomBlock;

class HeroBlock extends RichContentCustomBlock
{
    // ...

    /**
     * @param  array<string, mixed>  $config
     */
    public static function toPreviewHtml(array $config): string
    {
        return view('filament.forms.components.rich-editor.rich-content-custom-blocks.hero.preview', [
            'heading' => $config['heading'],
            'subheading' => $config['subheading'] ?? 'Default subheading',
        ])->render();
    }
}

若希望自定义编辑器中预览上方显示的标签,可以定义 getPreviewLabel()。默认使用 getLabel() 中定义的标签,但 getPreviewLabel() 能访问块的 $config,从而在标签中显示动态信息:

php
use Filament\Forms\Components\RichEditor\RichContentCustomBlock;

class HeroBlock extends RichContentCustomBlock
{
    // ...

    /**
     * @param  array<string, mixed>  $config
     */
    public static function getPreviewLabel(array $config): string
    {
        return "Hero section: {$config['heading']}";
    }
}

使用自定义块渲染内容

渲染富文本内容时,可以将自定义块数组传给 RichContentRenderer,以确保块正确渲染:

php
use Filament\Forms\Components\RichEditor\RichContentRenderer;

RichContentRenderer::make($record->content)
    ->customBlocks([
        HeroBlock::class,
        CallToActionBlock::class,
    ])
    ->toHtml()

每个块类可以有一个 toHtml() 方法,返回应为该块渲染的 HTML:

php
use Filament\Forms\Components\RichEditor\RichContentCustomBlock;

class HeroBlock extends RichContentCustomBlock
{
    // ...

    /**
     * @param  array<string, mixed>  $config
     * @param  array<string, mixed>  $data
     */
    public static function toHtml(array $config, array $data): string
    {
        return view('filament.forms.components.rich-editor.rich-content-custom-blocks.hero.index', [
            'heading' => $config['heading'],
            'subheading' => $config['subheading'],
            'buttonLabel' => 'View category',
            'buttonUrl' => $data['categoryUrl'],
        ])->render();
    }
}

如上所示,toHtml() 方法接收两个参数:$config(插入块时在模态框中提交的配置数据),以及 $data(渲染块可能需要的任何额外数据)。这让你能访问配置数据并相应渲染块。数据可在 customBlocks() 方法中传入:

php
use Filament\Forms\Components\RichEditor\RichContentRenderer;

RichContentRenderer::make($record->content)
    ->customBlocks([
        HeroBlock::class => [
            'categoryUrl' => $record->category->getUrl(),
        ],
        CallToActionBlock::class,
    ])
    ->toHtml()

分组自定义块

你可以使用 customBlocks() 数组中的字符串键将自定义块组织成分组。直接传入(无字符串键)的块不分组,并首先出现在面板中:

php
use Filament\Forms\Components\RichEditor;

RichEditor::make('content')
    ->customBlocks([
        AlertBlock::class,
        DividerBlock::class,
        'Marketing' => [
            HeroBlock::class,
            CallToActionBlock::class,
            BannerBlock::class,
        ],
        'Media' => [
            ImageGalleryBlock::class,
            VideoEmbedBlock::class,
        ],
    ])
打开了分组自定义块面板的富文本编辑器打开了分组自定义块面板的富文本编辑器

分组按数组中定义的顺序显示,侧栏中带粘性标题。

使用分组块渲染内容时,可以将相同的分组数组结构传给 RichContentRenderer。渲染时会忽略分组——仅使用块类:

php
use Filament\Forms\Components\RichEditor\RichContentRenderer;

RichContentRenderer::make($record->content)
    ->customBlocks([
        'Marketing' => [
            HeroBlock::class => [
                'categoryUrl' => $record->category->getUrl(),
            ],
            CallToActionBlock::class,
        ],
    ])
    ->toHtml()

默认打开自定义块面板

若希望富文本编辑器加载时默认打开自定义块面板,可以使用 activePanel('customBlocks') 方法:

php
use Filament\Forms\Components\RichEditor;

RichEditor::make('content')
    ->customBlocks([
        HeroBlock::class,
        CallToActionBlock::class,
    ])
    ->activePanel('customBlocks')

使用 prose 为自定义块预览设置样式

默认情况下,自定义块预览不应用 prose 样式,以便于自行设置样式。你可以使用 shouldApplyProseStylingToPreview() 方法为块预览启用 prose 样式。当你希望预览以标题、段落等排版样式显示时,这很有用:

php
use Filament\Forms\Components\RichEditor\RichContentCustomBlock;

class HeadingBlock extends RichContentCustomBlock
{
    // ...

    /**
     * @param  array<string, mixed>  $config
     */
    public static function shouldApplyProseStylingToPreview(array $config): bool
    {
        return true;
    }
}

shouldApplyProseStylingToPreview() 返回 true 时,块预览将应用富文本编辑器中定义的 prose 排版样式,包括适当的边距、字号及其他文本格式。该方法默认返回 false,因此预览以最少样式显示。

你可以根据块的配置做出此决定,让不同块具有不同的预览样式:

php
public static function shouldApplyProseStylingToPreview(array $config): bool
{
    return ($config['useProseStyle'] ?? false) === true;
}

使用合并标签

合并标签允许用户在富文本内容中插入「占位符」,渲染内容时可用动态值替换。这对插入当前用户姓名或当前日期等很有用。

要在编辑器上注册合并标签,请使用 mergeTags() 方法:

php
use Filament\Forms\Components\RichEditor;

RichEditor::make('content')
    ->mergeTags([
        'name',
        'today',
    ])
带合并标签面板的富文本编辑器带合并标签面板的富文本编辑器

合并标签由双花括号包围,例如 &#123;&#123; name &#125;&#125;。渲染内容时,这些标签会被替换为对应的值。

要将合并标签插入内容,用户可以开始输入 &#123;&#123; 以搜索要插入的标签。或者,他们可以点击编辑器工具栏中的「merge tags」工具,打开包含所有合并标签的面板,然后从侧栏拖入内容或点击插入。

使用合并标签渲染内容

渲染富文本内容时,可以传入用于替换合并标签的值数组:

php
use Filament\Forms\Components\RichEditor\RichContentRenderer;

RichContentRenderer::make($record->content)
    ->mergeTags([
        'name' => $record->user->name,
        'today' => now()->toFormattedDateString(),
    ])
    ->toHtml()

若有许多合并标签,或需要运行某些逻辑来确定值,可以将函数用作每个合并标签的值。该函数会在内容中首次遇到合并标签时调用,其结果会对同名后续标签缓存:

php
use Filament\Forms\Components\RichEditor\RichContentRenderer;

RichContentRenderer::make($record->content)
    ->mergeTags([
        'name' => fn (): string => $record->user->name,
        'today' => now()->toFormattedDateString(),
    ])
    ->toHtml()

在合并标签中使用 HTML 内容

默认情况下,合并标签将其值渲染为纯文本。不过,你可以通过提供实现 Laravel Htmlable 接口的值,在合并标签中渲染 HTML 内容。这对插入格式化内容、链接或其他 HTML 元素很有用:

php
use Filament\Forms\Components\RichEditor\RichContentRenderer;
use Illuminate\Support\HtmlString;

RichContentRenderer::make($record->content)
    ->mergeTags([
        'user_name' => $record->user->name, // Plain text
        'user_profile_link' => new HtmlString('<a href="' . route('profile', $record->user) . '">View Profile</a>'),
    ])
    ->toHtml()

当合并标签值实现 Htmlable 接口(例如 HtmlString)时,系统会自动检测并在不转义的情况下渲染 HTML 内容。非 Htmlable 值出于安全考虑仍渲染为纯文本。

使用自定义合并标签标签

你可以使用关联数组为合并标签提供自定义标签(显示在编辑器侧栏和内容预览中),其中键为合并标签名,值为标签文本:

php
use Filament\Forms\Components\RichEditor;

RichEditor::make('content')
    ->mergeTags([
        'name' => 'Full name',
        'today' => 'Today\'s date',
    ])

这些标签不会保存在编辑器内容中,仅用于显示。

默认打开合并标签面板

若希望富文本编辑器加载时默认打开合并标签面板,可以使用 activePanel('mergeTags') 方法:

php
use Filament\Forms\Components\RichEditor;

RichEditor::make('content')
    ->mergeTags([
        'name',
        'today',
    ])
    ->activePanel('mergeTags')

使用提及

提及允许用户通过输入触发字符插入对其他记录(如用户、议题或标签)的引用。当用户输入 @ 等触发字符时,会出现下拉菜单,供其搜索并从可用选项中选择。所选提及会作为不可编辑的行内 token 插入。

要在编辑器上注册提及,请使用 mentions() 方法并传入一个或多个 MentionProvider 实例:

php
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\RichEditor\MentionProvider;

RichEditor::make('content')
    ->mentions([
        MentionProvider::make('@')
            ->items([
                1 => 'Jane Doe',
                2 => 'John Smith',
            ]),
    ])
带提及建议的富文本编辑器带提及建议的富文本编辑器

每个提供者配置有一个触发字符(传给 make()),用于激活提及搜索。你可以有多个带不同触发器的提供者:

php
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\RichEditor\MentionProvider;

RichEditor::make('content')
    ->mentions([
        MentionProvider::make('@')
            ->items([
                1 => 'Jane Doe',
                2 => 'John Smith',
            ]),
        MentionProvider::make('#')
            ->items([
                'bug' => 'Bug',
                'feature' => 'Feature',
            ]),
    ])

从数据库搜索提及

对于大型数据集,应使用 getSearchResultsUsing() 动态获取结果。回调接收搜索词,并应返回格式为 [id => label] 的选项数组。

使用动态搜索结果时,内容中仅存储提及的 id。要在加载内容时显示正确标签,还必须提供 getLabelsUsing()。该回调接收 ID 数组,并应返回格式为 [id => label] 的数组:

php
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\RichEditor\MentionProvider;

RichEditor::make('content')
    ->mentions([
        MentionProvider::make('@')
            ->getSearchResultsUsing(fn (string $search): array => User::query()
                ->where('name', 'like', "%{$search}%")
                ->orderBy('name')
                ->limit(10)
                ->pluck('name', 'id')
                ->all())
            ->getLabelsUsing(fn (array $ids): array => User::query()
                ->whereIn('id', $ids)
                ->pluck('name', 'id')
                ->all()),
    ])

使用提及渲染内容

渲染富文本内容时,可以将提及提供者数组传给 RichContentRenderer,以确保提及正确渲染。

你可以使用 url() 方法使提及在渲染时链接到 URL。回调接收提及的 idlabel,并应返回 URL 字符串:

php
use Filament\Forms\Components\RichEditor\MentionProvider;
use Filament\Forms\Components\RichEditor\RichContentRenderer;

RichContentRenderer::make($record->content)
    ->mentions([
        MentionProvider::make('@')
            ->getLabelsUsing(fn (array $ids): array => User::query()
                ->whereIn('id', $ids)
                ->pluck('name', 'id')
                ->all())
            ->url(fn (string $id, string $label): string => route('users.show', $id)),
    ])
    ->toHtml()

TIP

url() 闭包返回的字符串会直接渲染到 <a> 标签的 href 属性中,因此若 URL 的任何部分由用户输入构建,应确保它不能解析为浏览器会执行的 javascript:data: 等方案。最简单的保证方式是用 Filament 的 Str::sanitizeUrl() 辅助方法包装返回值,它仅允许 http/https 和相对 URL:

php
use Illuminate\Support\Str;

->url(fn (string $id, string $label): ?string => Str::sanitizeUrl(
    route('users.show', $id),
))

若有意允许 javascript: URL(例如将提及连接到 Alpine.js 处理程序),请跳过该辅助方法并返回原始值——只需确保该 URL 的任何组成部分都不来自不可信用户输入。

注册富文本内容属性

富文本编辑器配置中有些部分同时适用于编辑器与渲染器。例如,若使用私有图片自定义块合并标签提及插件,需要确保两处使用相同配置。为此,Filament 提供了注册富文本内容属性的方式,可同时用于编辑器与渲染器。若插件实现了 HasFileAttachmentProvider,文件附件提供者会自动从插件解析,因此无需在属性或渲染器上调用 fileAttachmentProvider()

要在 Eloquent 模型上注册富文本内容属性,应使用 InteractsWithRichContent trait 并实现 HasRichContent 接口。这允许你在 setUpRichContent() 方法中注册属性:

php
use Filament\Forms\Components\RichEditor\MentionProvider;
use Filament\Forms\Components\RichEditor\Models\Concerns\InteractsWithRichContent;
use Filament\Forms\Components\RichEditor\Models\Contracts\HasRichContent;
use Illuminate\Database\Eloquent\Model;

class Post extends Model implements HasRichContent
{
    use InteractsWithRichContent;

    public function setUpRichContent(): void
    {
        $this->registerRichContent('content')
            ->fileAttachmentsDisk('s3')
            ->fileAttachmentsVisibility('private')
            ->customBlocks([
                HeroBlock::class => [
                    'categoryUrl' => fn (): string => $this->category->getUrl(),
                ],
                CallToActionBlock::class,
            ])
            ->mergeTags([
                'name' => fn (): string => $this->user->name,
                'today' => now()->toFormattedDateString(),
            ])
            ->mergeTagLabels([
                'name' => 'Full name',
                'today' => 'Today\'s date',
            ])
            ->mentions([
                MentionProvider::make('@')
                    ->items([
                        1 => 'Jane Doe',
                        2 => 'John Smith',
                    ]),
            ])
            ->textColors([
                'brand' => TextColor::make('Brand', '#0ea5e9', darkColor: '#38bdf8'),
            ])
            ->customTextColors()
            ->plugins([
                HighlightRichContentPlugin::make(),
            ]);
    }
}

每当使用 RichEditor 组件时,都会使用为对应属性注册的配置:

php
use Filament\Forms\Components\RichEditor;

RichEditor::make('content')

要使用给定配置轻松从模型渲染富文本 HTML,可以在模型上调用 renderRichContent() 方法并传入属性名:

blade
{!! $record->renderRichContent('content') !!}

或者,你可以获取 Htmlable 对象以在不转义 HTML 的情况下渲染:

blade
{{ $record->getRichContentAttribute('content') }}

在表格中使用文本列或在 infolist 中使用文本条目时,无需手动渲染富文本内容。Filament 会自动完成:

php
use Filament\Infolists\Components\TextEntry;
use Filament\Tables\Columns\TextColumn;

TextColumn::make('content')

TextEntry::make('content')

扩展富文本编辑器

你可以为富文本编辑器创建插件,以便向编辑器与渲染器添加自定义 TipTap 扩展,以及自定义工具栏按钮。创建一个实现 RichContentPlugin 接口的新类:

php
use Filament\Actions\Action;
use Filament\Forms\Components\ColorPicker;
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\RichEditor\EditorCommand;
use Filament\Forms\Components\RichEditor\Plugins\Contracts\RichContentPlugin;
use Filament\Forms\Components\RichEditor\RichEditorTool;
use Filament\Support\Enums\Width;
use Filament\Support\Facades\FilamentAsset;
use Filament\Support\Icons\Heroicon;
use Tiptap\Core\Extension;
use Tiptap\Marks\Highlight;

class HighlightRichContentPlugin implements RichContentPlugin
{
    public static function make(): static
    {
        return app(static::class);
    }

    /**
     * @return array<Extension>
     */
    public function getTipTapPhpExtensions(): array
    {
        // This method should return an array of PHP TipTap extension objects.
        // See: https://github.com/ueberdosis/tiptap-php
    
        return [
            app(Highlight::class, [
                'options' => ['multicolor' => true],
            ]),
        ];
    }

    /**
     * @return array<string>
     */
    public function getTipTapJsExtensions(): array
    {
        // This method should return an array of URLs to JavaScript files containing
        // TipTap extensions that should be asynchronously loaded into the editor
        // when the plugin is used.
    
        return [
            FilamentAsset::getScriptSrc('rich-content-plugins/highlight'),
        ];
    }

    /**
     * @return array<RichEditorTool>
     */
    public function getEditorTools(): array
    {
        // This method should return an array of `RichEditorTool` objects, which can then be
        // used in the `toolbarButtons()` of the editor.
        
        // The `jsHandler()` method allows you to access the TipTap editor instance
        // through `$getEditor()`, and `chain()` any TipTap commands to it.
        // See: https://tiptap.dev/docs/editor/api/commands
        
        // The `action()` method allows you to run an action (registered in the `getEditorActions()`
        // method) when the toolbar button is clicked. This allows you to open a modal to
        // collect additional information from the user before running a command.
    
        return [
            RichEditorTool::make('highlight')
                ->jsHandler('$getEditor()?.chain().focus().toggleHighlight().run()')
                ->icon(Heroicon::CursorArrowRays),
            RichEditorTool::make('highlightWithCustomColor')
                ->action(arguments: '{ color: $getEditor().getAttributes(\'highlight\')?.[\'data-color\'] }')
                ->icon(Heroicon::CursorArrowRipple),
        ];
    }

    /**
     * @return array<Action>
     */
    public function getEditorActions(): array
    {
        // This method should return an array of `Action` objects, which can be used by the tools
        // registered in the `getEditorTools()` method. The name of the action should match
        // the name of the tool that uses it.
        
        // The `runCommands()` method allows you to run TipTap commands on the editor instance.
        // It accepts an array of `EditorCommand` objects that define the command to run,
        // as well as any arguments to pass to the command. You should also pass in the
        // `editorSelection` argument, which is the current selection in the editor
        // to apply the commands to.
    
        return [
            Action::make('highlightWithCustomColor')
                ->modalWidth(Width::Large)
                ->fillForm(fn (array $arguments): array => [
                    'color' => $arguments['color'] ?? null,
                ])
                ->schema([
                    ColorPicker::make('color'),
                ])
                ->action(function (array $arguments, array $data, RichEditor $component): void {
                    $component->runCommands(
                        [
                            EditorCommand::make(
                                'toggleHighlight',
                                arguments: [[
                                    'color' => $data['color'],
                                ]],
                            ),
                        ],
                        editorSelection: $arguments['editorSelection'],
                    );
                }),
        ];
    }
}

你可以使用 plugins() 方法将插件注册到富文本编辑器和富文本内容渲染器

php
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\RichEditor\RichContentRenderer;

RichEditor::make('content')
    ->toolbarButtons([
        ['bold', 'highlight', 'highlightWithCustomColor'],
        ['h2', 'h3'],
        ['bulletList', 'orderedList'],
    ])
    ->plugins([
        HighlightRichContentPlugin::make(),
    ])

RichContentRenderer::make($record->content)
    ->plugins([
        HighlightRichContentPlugin::make(),
    ])

从插件启用或禁用工具栏按钮

默认情况下,当插件通过 getEditorTools() 提供工具时,这些工具会被注册但不会自动显示在工具栏中。用户需要使用 toolbarButtons()enableToolbarButtons() 手动添加它们。

若希望插件自动启用或禁用工具栏按钮,可以在实现 RichContentPlugin 的同时实现 HasToolbarButtons 接口。这是一个可选的独立接口:

php
use Filament\Forms\Components\RichEditor\Plugins\Contracts\HasToolbarButtons;
use Filament\Forms\Components\RichEditor\Plugins\Contracts\RichContentPlugin;

class HighlightRichContentPlugin implements RichContentPlugin, HasToolbarButtons
{
    // ... other methods ...

    /**
     * @return array<string | array<string | array<string>>>
     */
    public function getEnabledToolbarButtons(): array
    {
        return ['highlight', 'highlightWithCustomColor'];
    }

    /**
     * @return array<string>
     */
    public function getDisabledToolbarButtons(): array
    {
        return [];
    }
}

getEnabledToolbarButtons() 方法返回要添加到工具栏的按钮名称。getDisabledToolbarButtons() 方法返回要从工具栏移除的按钮名称。

插件的工具栏修改会在用户级修改之前应用。这意味着用户始终可以使用 enableToolbarButtons()disableToolbarButtons() 覆盖插件行为:

php
RichEditor::make('content')
    ->plugins([
        HighlightRichContentPlugin::make(),
    ])
    ->disableToolbarButtons(['highlightWithCustomColor'])

设置 TipTap JavaScript 扩展

Filament 能够异步加载 TipTap 的 JavaScript 扩展。为此,你需要创建一个包含该扩展的 JavaScript 文件,并在你的插件getTipTapJsExtensions() 方法中注册它。

例如,若要使用 TipTap highlight 扩展,请先确保已安装:

bash
npm install @tiptap/extension-highlight --save-dev

然后,创建一个导入该扩展的 JavaScript 文件。在此示例中,在 resources/js/filament/rich-content-plugins 目录下创建名为 highlight.js 的文件,并添加以下代码:

javascript
import Highlight from '@tiptap/extension-highlight'

export default Highlight.configure({
    multicolor: true,
})

编译此文件的一种方式是使用 esbuild。你可以使用 npm 安装它:

bash
npm install esbuild --save-dev

你必须创建一个 esbuild 脚本来编译该文件。可以放在任意位置,例如 bin/build.js

js
import * as esbuild from 'esbuild'

async function compile(options) {
    const context = await esbuild.context(options)

    await context.rebuild()
    await context.dispose()
}

compile({
    define: {
        'process.env.NODE_ENV': `'production'`,
    },
    bundle: true,
    mainFields: ['module', 'main'],
    platform: 'neutral',
    sourcemap: false,
    sourcesContent: false,
    treeShaking: true,
    target: ['es2020'],
    minify: true,
    entryPoints: ['./resources/js/filament/rich-content-plugins/highlight.js'],
    outfile: './resources/js/dist/filament/rich-content-plugins/highlight.js',
})

如脚本底部所示,我们将名为 resources/js/filament/rich-content-plugins/highlight.js 的文件编译为 resources/js/dist/filament/rich-content-plugins/highlight.js。你可以根据需要更改这些路径,也可以编译任意数量的文件。

要运行脚本并将此文件编译到 resources/js/dist/filament/rich-content-plugins/highlight.js,请运行以下命令:

bash
node bin/build.js

你应在服务提供者(如 AppServiceProvider)的 boot() 方法中注册它,并使用 loadedOnRequest(),以便在页面加载富文本编辑器之前不会下载该文件:

php
use Filament\Support\Assets\Js;
use Filament\Support\Facades\FilamentAsset;

FilamentAsset::register([
    Js::make('rich-content-plugins/highlight', __DIR__ . '/../../resources/js/dist/filament/rich-content-plugins/highlight.js')->loadedOnRequest(),
]);

要将此新 JavaScript 文件发布到应用的 /public 目录以便提供服务,可以使用 filament:assets 命令:

bash
php artisan filament:assets

插件对象中,getTipTapJsExtensions() 方法应返回你刚创建的 JavaScript 文件路径。既然已用 FilamentAsset 注册,你可以使用 getScriptSrc() 方法获取该文件的 URL:

php
use Filament\Support\Facades\FilamentAsset;

/**
 * @return array<string>
 */
public function getTipTapJsExtensions(): array
{
    return [
        FilamentAsset::getScriptSrc('rich-content-plugins/highlight'),
    ];
}

共享捆绑的 TipTap/ProseMirror 实例

当自定义 JavaScript 扩展从 @tiptap/core@tiptap/pm/* 导入时,每个编译后的扩展都会包含这些包的一份副本。这会使每个扩展浪费约 150–200 KB,更重要的是会在页面上创建多个 ProseMirror 实例。由于 ProseMirror 严重依赖 instanceof 检查(针对 NodeMarkPluginDecorationSet 等),捆绑自身模块副本的扩展可能无法与编辑器核心互操作。

为避免此问题,Filament 在 window.FilamentRichEditor.tiptap 上公开捆绑的 TipTap 和 ProseMirror 模块:

js
window.FilamentRichEditor.tiptap = {
    core,     // @tiptap/core
    pmState,  // @tiptap/pm/state
    pmView,   // @tiptap/pm/view
    pmModel,  // @tiptap/pm/model
}

你可以在扩展中直接引用这些模块:

javascript
const { Node, mergeAttributes } = window.FilamentRichEditor.tiptap.core
const { Plugin, PluginKey } = window.FilamentRichEditor.tiptap.pmState

export default Node.create({
    name: 'myExtension',
    // ...
})

或者,你可以配置构建以拦截对 @tiptap/core@tiptap/pm/{state,view,model} 的导入,并在运行时从全局解析它们。这样你仍可在扩展源码中编写普通的 import 语句——其他 @tiptap/* 包(如 @tiptap/extension-highlight)仍按常规捆绑。以下 esbuild 插件在构建时检查每个被拦截包的真实具名导出,并将导入重写为从 window.FilamentRichEditor.tiptap 读取:

bash
npm install --save-dev @tiptap/core @tiptap/pm
js
// bin/build.js
import * as esbuild from 'esbuild'

const tiptapSharedPlugin = {
    name: 'tiptap-shared',
    setup(build) {
        const keys = {
            '@tiptap/core': 'core',
            '@tiptap/pm/state': 'pmState',
            '@tiptap/pm/view': 'pmView',
            '@tiptap/pm/model': 'pmModel',
        }

        build.onResolve({ filter: /^@tiptap\/(core|pm\/(state|view|model))$/ }, (args) => ({
            path: args.path,
            namespace: 'tiptap-shared',
        }))

        build.onLoad({ filter: /.*/, namespace: 'tiptap-shared' }, async (args) => {
            const realModule = await import(args.path)
            const namedExports = Object.keys(realModule).filter(
                (key) => key !== '__esModule' && key !== 'default',
            )

            const key = keys[args.path]
            let code = `const __module = window.FilamentRichEditor.tiptap.${key};\n`

            if (namedExports.length) {
                code += `export const { ${namedExports.join(', ')} } = __module;\n`
            }

            code += `export default __module?.default ?? __module;\n`

            return { contents: code, loader: 'js' }
        })
    },
}

esbuild.build({
    // ...
    plugins: [tiptapSharedPlugin],
    entryPoints: ['./resources/js/filament/rich-content-plugins/my-extension.js'],
    outfile: './resources/js/dist/filament/rich-content-plugins/my-extension.js',
})

INFO

window.FilamentRichEditor.tiptap 在富文本编辑器包加载时赋值,这发生在获取 getTipTapJsExtensions() URL 之前。若需要在富文本编辑器尚未加载的上下文中使用这些模块,请改为捆绑你自己的副本。

上述 esbuild 插件在构建时从本地安装的 @tiptap/core@tiptap/pm 读取具名导出,因此请使这些版本与 Filament 捆绑的版本大致同步——否则扩展中引用的较新具名导出在运行时可能为 undefined