表单
Rich editor
简介
富文本编辑器允许你编辑和预览 HTML 内容,以及上传图片。它使用 TipTap 作为底层编辑器。
use Filament\Forms\Components\RichEditor;
RichEditor::make('content')
Configuring Livewire’s maximum nesting depth
The rich editor synchronizes its TipTap document with Livewire as nested data. Livewire limits nested property paths to 10 levels by default, which may not be enough for structures such as lists and tables. If you encounter a Livewire\Exceptions\MaxNestingDepthExceededException and your application does not already have a config/livewire.php file, publish Livewire’s configuration file:
php artisan livewire:publish --config
The command overwrites an existing config/livewire.php file, so skip it if you have already published the configuration.
Then, increase the existing max_nesting_depth setting in config/livewire.php. For example, a depth of 32 allows room for deeply nested rich content:
'payload' => [
// ...
'max_nesting_depth' => 32,
],
Only change the max_nesting_depth value in the existing payload array, so that you preserve Livewire’s other version-specific payload settings.
将内容存储为 JSON
默认情况下,富文本编辑器将内容存储成 HTML,如果你想将其存储为 JSON 格式,你可以使用 json() 方法:
use Filament\Forms\Components\RichEditor;
RichEditor::make('content')
->json()
该 JSON 是以 TipTap 的格式存储,它是内容的结构化表示。
如果你使用 Eloquent 来保存 JSON 内容,你应该确保将 array cast 添加到模型属性中:
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
'content' => 'array',
];
}
// ...
}
自定义工具栏按钮
使用 toolbarButtons() 方法,你可以设置编辑器的工具栏按钮。此例中的选项为默认值:
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'],
])
主数组中的每个嵌套数组都表示工具栏中的一组按钮。
Toolbar 中可以引入的其他组件:
h1- 将 “h1” 标签应用到文本中。h4- Applies the “h4” tag to the text.h5- Applies the “h5” tag to the text.h6- Applies the “h6” tag to the text.alignJustify- 对齐文本。clearFormatting- Clears all formatting from the selected text.details- Inserts a<details>tag, which allows users to create collapsible sections in their content.grid- Inserts a grid layout into the editor, allowing users to create responsive columns of content.gridDelete- Deletes the current grid layout.highlight- 使用<mark>标签高亮显示选中的文本。horizontalRule- Inserts a horizontal rule.lead- 在文本中使用lead类,通常用于文章的第一章。paragraph- Sets the current block to a paragraph, removing any heading formatting.small- 将<small>标签应用到文本中,通常用于小字体打印或免责声明。code- Format the selected text as inline code.textColor- Changes the text color of the selected text.table- Creates a table in the editor with a default layout of 3 columns and 2 rows, with the first row configured as a header row.tableAddColumnBefore- Adds a new column before the current column.tableAddColumnAfter- Adds a new column after the current column.tableDeleteColumn- Deletes the current column.tableAddRowBefore- Adds a new row above the current row.tableAddRowAfter- Adds a new row below the current row.tableDeleteRow- Deletes the current row.tableMergeCells- Merges the selected cells into one cell.tableSplitCell- Splits the selected cell into multiple cells.tableToggleHeaderRow- Toggles the header row of the table.tableToggleHeaderCell- Toggles the header cell of the table.tableDelete- Deletes the table.
除了允许静态值之外,toolbarButtons() 方法也接收函数来计算它的值。你可以将各种 utility 作为参数注入到该函数中。
了解更多 utility 注入详情。 | Utility | 类型 | 参数 | 描述 |
|---|---|---|---|
| Field | Filament\Forms\Components\Field | $component | The current field component instance. |
| Get function | Filament\Schemas\Components\Utilities\Get | $get | A function for retrieving values from the current form data. Validation is not run. |
| Livewire | Livewire\Component | $livewire | The Livewire component instance. |
| Eloquent model FQN | ?string<Illuminate\Database\Eloquent\Model> | $model | The Eloquent model FQN for the current schema. |
| Operation | string | $operation | The current operation being performed by the schema. Usually create, edit, or view. |
| Raw state | mixed | $rawState | The current value of the field, before state casts were applied. Validation is not run. |
| Eloquent record | ?Illuminate\Database\Eloquent\Model | $record | The Eloquent record for the current schema. |
| State | mixed | $state | The current value of the field. Validation is not run. |
Customizing floating toolbars
If your toolbar is too full, you can use a floating toolbar to show certain tools in a toolbar below the cursor, only when the user is inside a specific node type. This allows you to keep the main toolbar clean while still providing access to additional tools when needed.
You can customize the floating toolbars that appear when your cursor is placed inside a specific node by using the floatingToolbars() method.
In the example below, a floating toolbar appears when the cursor is inside a paragraph node. It shows bold, italic, and similar buttons. When the cursor is in a heading node, it displays heading-related buttons, and when inside a table, it shows table-specific controls.
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',
],
])
Grouping toolbar buttons into dropdowns
You may group related toolbar buttons into a dropdown menu using ToolbarButtonGroup. The first argument is a label used for the dropdown’s tooltip and accessibility, and the second argument is an array of button names to include in the dropdown:
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'],
])
By default, the first button’s icon is used as the dropdown trigger, and it updates reactively to reflect the currently active button. Clicking on the trigger reveals the grouped buttons.
You can set a fixed icon for the dropdown trigger using the icon() method. When a custom icon is set, the trigger icon remains static and does not change based on the active button:
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'],
])
Using textual dropdown toolbar buttons
By default, dropdown toolbar buttons display icons only. If you’d like to show text labels alongside icons in the dropdown items, you can use the textualButtons() method on a ToolbarButtonGroup:
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'],
])
In this example, the Paragraph dropdown items display their icon alongside a text label (e.g., “Paragraph”, “Heading 1”). The Alignment dropdown remains icon-only.
Setting the height
You may control the editor’s height by defining the minHeight() and maxHeight() methods, which accept any CSS length value:
use Filament\Forms\Components\RichEditor;
RichEditor::make('content')
->minHeight('12rem')
->maxHeight('24rem')
The editor has a minimum height of 10rem by default. Once the content exceeds maxHeight(), the editor stops growing and becomes scrollable. Each method may be used on its own — minHeight() sets a starting height while still allowing the editor to grow, and maxHeight() caps how tall it may become. Pass null to minHeight() to use the editor’s intrinsic 3rem minimum height, or to maxHeight() to remove the cap. These constraints also apply when the editor is disabled.
As well as allowing static values, the minHeight() and maxHeight() methods also accept functions to dynamically calculate them. You can inject various utilities into the functions as parameters.
了解更多 utility 注入详情。 | Utility | 类型 | 参数 | 描述 |
|---|---|---|---|
| Field | Filament\Forms\Components\Field | $component | The current field component instance. |
| Get function | Filament\Schemas\Components\Utilities\Get | $get | A function for retrieving values from the current form data. Validation is not run. |
| Livewire | Livewire\Component | $livewire | The Livewire component instance. |
| Eloquent model FQN | ?string<Illuminate\Database\Eloquent\Model> | $model | The Eloquent model FQN for the current schema. |
| Operation | string | $operation | The current operation being performed by the schema. Usually create, edit, or view. |
| Raw state | mixed | $rawState | The current value of the field, before state casts were applied. Validation is not run. |
| Eloquent record | ?Illuminate\Database\Eloquent\Model | $record | The Eloquent record for the current schema. |
| State | mixed | $state | The current value of the field. Validation is not run. |
Customizing text colors
The rich editor includes a text color tool for styling inline text. By default, it uses the Tailwind CSS color palette. In light mode, the 600 shades are applied to text, and in dark mode, the 400 shades are used.
You can customize which colors are available in the picker using the textColors() method:
use Filament\Forms\Components\RichEditor;
RichEditor::make('content')
->textColors([
'#ef4444' => 'Red',
'#10b981' => 'Green',
'#0ea5e9' => 'Sky',
])
If you would like to define different colors for light and dark mode, you can use the a TextColor object to define the color:
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'),
])
If you would like to add new colors onto the existing Tailwind palette, you can merge your colors into the TextColor::getDefaults() array:
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(),
])
When you use a TextColor object, the key of the array becomes the stored data-color attribute on the <span> tag, allowing you to reference the color in your CSS if needed. When you use the color as the array values, the actual color value (e.g., a HEX string) is stored as the data-color attribute.
You can also pass textColors() to the content renderer and rich content attribute so that server-side rendering matches your editor configuration.
You can also allow users to pick custom colors that aren’t in the predefined list by using the customTextColors() method:
use Filament\Forms\Components\RichEditor;
RichEditor::make('content')
->textColors([
// ...
])
->customTextColors()
You do not need to use customTextColors() on the content renderer, as it will automatically render any custom colors that are used in the content.
渲染富文本内容
如果你将内容存储为 JSON而非 HTML,或者你的内容需要处理以注入私有图片 URL等,你将需要使用 Filament 中 RichContentRenderer 工具来输出 HTML:
use Filament\Forms\Components\RichEditor\RichContentRenderer;
RichContentRenderer::make($record->content)->toHtml()
toHtml() 方法返回一个字符串。如果你想要在 Blade 视图中输出 HTML 而不进行转义,你可以输出 RichContentRender 而不调用 toHtml()
{{ \Filament\Forms\Components\RichEditor\RichContentRenderer::make($record->content) }}
如果你已经配置了编辑器的文件附件行为以修改上传文件的磁盘或可见性,则还必须将这些设置传递给渲染器,以确保生成正确的 URL:
use Filament\Forms\Components\RichEditor\RichContentRenderer;
RichContentRenderer::make($record->content)
->fileAttachmentsDisk('s3')
->fileAttachmentsVisibility('private')
->toHtml()
如果你在富文本编辑器中使用了自定义 Block,你可以将自定义 Block 数组传入到渲染器,以确保其正确渲染:
use Filament\Forms\Components\RichEditor\RichContentRenderer;
RichContentRenderer::make($record->content)
->customBlocks([
HeroBlock::class => [
'categoryUrl' => $record->category->getUrl(),
],
CallToActionBlock::class,
])
->toHtml()
如果你要使用合并标签,你可以传入值数组来替换要合并标签:
use Filament\Forms\Components\RichEditor\RichContentRenderer;
RichContentRenderer::make($record->content)
->mergeTags([
'name' => $record->user->name,
'today' => now()->toFormattedDateString(),
])
->toHtml()
If you are using custom text colors, you can pass an array of colors to the renderer to ensure that the colors are rendered correctly:
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();
Styling the rendered content
The rich editor HTML uses a combination of HTML elements, CSS classes, and inline styles to style the content, depending on the features used in the editor. If you render the content in a Filament table column or infolist entry with prose(), Filament will automatically apply the necessary styles for you. If you are outputting the content in your own Blade view, you may need to add some additional styles to ensure that the content is styled correctly.
One way of styling the content is to use Tailwind CSS Typography. This plugin provides a set of pre-defined styles for common HTML elements, such as headings, paragraphs, lists, and tables. You can apply these styles to a container element using the prose class:
<div class="prose dark:prose-invert">
{!! \Filament\Forms\Components\RichEditor\RichContentRenderer::make($record->content) !!}
</div>
However, some features, such as the grid layout and text colors, require additional styles that are not included in the Tailwind CSS Typography plugin. Filament also includes its own fi-prose CSS class that adds these additional styles. Any app that loads Filament’s vendor/filament/support/resources/css/index.css CSS will have access to this class. The styling is different to the prose class, but fits with Filament’s design system better:
<div class="fi-prose">
{!! \Filament\Forms\Components\RichEditor\RichContentRenderer::make($record->content) !!}
</div>
安全
默认情况下,该编辑器输出原始 HTML,并将其发送到后端。攻击者能够拦截组件的值,并将不同的原始 HTML 字符串发送到后端。因此,从富文本编辑器输出 HTML 时,对其进行净化非常重要;否则,你的网站可能会暴露于跨站点脚本(XSS)漏洞。
当 Filament 在 TextColumn 和 TextEntry 等组件中从数据库输出原始 HTML 时,它会对其进行净化,以删除任何危险的 JavaScript。但是,如果你在自己的 Blade 视图中输出来自富文本编辑器的 HTML,这是你的责任。一种选择是使用 Filament 的 sanctizeHtml() 助手函数来执行此操作,这与我们在上述组件中用于净化 HTML 的工具相同:
{!! str($record->content)->sanitizeHtml() !!}
如果你将内容存储为 JSON而非 HTML,或者你的内容需要处理以注入私有图像 URL或类似行为,你可以使用内容渲染器以输出 HTML。这将为你自动净化 HTML,因此你无需为此担心。
NOTE
Filament’s built-in HTML sanitizer permits inline style attributes in order to support rich text formatting features such as font colors, text highlighting, and image sizing. This means that CSS properties like background: url(...) or position: fixed will not be stripped from sanitized HTML. If your content comes from untrusted users, you should consider restricting the default configuration. See the security documentation for details on how to customize the sanitizer.
上传图片到编辑器
默认情况下,上传的图片被公开保存到你的存储磁盘中,以便保存到数据库中的富文本内容可以在任何地方轻松地输出到。你可以使用配置方法,自定义图片的上传方式:
use Filament\Forms\Components\RichEditor;
RichEditor::make('content')
->fileAttachmentsDisk('s3')
->fileAttachmentsDirectory('attachments')
->fileAttachmentsVisibility('private')
除了允许静态值之外,fileAttachmentsDisk()、fileAttachmentsDirectory(), 和 fileAttachmentsVisibility() 方法也接受函数来动态计算它们的值。你可以将各种 utility 作为参数注入到函数中。
了解更多 utility 注入详情。 | Utility | 类型 | 参数 | 描述 |
|---|---|---|---|
| Field | Filament\Forms\Components\Field | $component | The current field component instance. |
| Get function | Filament\Schemas\Components\Utilities\Get | $get | A function for retrieving values from the current form data. Validation is not run. |
| Livewire | Livewire\Component | $livewire | The Livewire component instance. |
| Eloquent model FQN | ?string<Illuminate\Database\Eloquent\Model> | $model | The Eloquent model FQN for the current schema. |
| Operation | string | $operation | The current operation being performed by the schema. Usually create, edit, or view. |
| Raw state | mixed | $rawState | The current value of the field, before state casts were applied. Validation is not run. |
| Eloquent record | ?Illuminate\Database\Eloquent\Model | $record | The Eloquent record for the current schema. |
| State | mixed | $state | The current value of the field. Validation is not run. |
TIP
Filament 也支持使用 spatie/laravel-medialibrary 来存储富文本文件附件。请查阅插件文档了解更多信息。
在编辑器中使用私有图像
在编辑器中使用私有图像会增加处理流程的复杂性,因为私有图像无法通过永久 URL 直接访问。每次加载编辑器或渲染其内容时,都需要为每个镜像生成临时 URL,这些 URL 永远不会存储在数据库中。Filament 为图像标签添加了 data-id 属性,该属性包含图像在存储磁盘中的标识符,以便可以根据需要生成临时 URL。
使用私有图像渲染内容时,请确保使用 Filament 中的 RichContentRenderer 工具输出 HTML:
use Filament\Forms\Components\RichEditor\RichContentRenderer;
RichContentRenderer::make($record->content)
->fileAttachmentsDisk('s3')
->fileAttachmentsVisibility('private')
->toHtml()
Securing file attachment IDs
The data-id attribute on an image node is an identifier for a file on the configured disk. When the content is rendered, Filament generates a URL for it — a signed temporary URL if the visibility is private. Like any other Livewire form field value, the content and its data-id attributes are controlled by the client: a request can be intercepted to change a data-id to any other identifier on the same disk. If the disk also stores files belonging to other users or records, an attacker could otherwise cause the rendered content to reference (and serve a signed URL for) someone else’s file.
Filament allows this by default because legitimate features depend on it — for example, an action that inserts an image from a pre-existing library, or a “copy from another record” button. If none of your editors rely on such a flow, call preventFileAttachmentPathTampering() on the field to enable a built-in check:
use Filament\Forms\Components\RichEditor;
RichEditor::make('content')
->preventFileAttachmentPathTampering()
Filament parses the record’s original content (via $record->getOriginal() for the attribute matching the field name) and allows only the data-id values already present. Any other existing data-id causes the field to fail validation, so the record is never saved with a tampered value. Newly uploaded images always pass through.
The default file attachment provider performs no per-record scoping — any data-id that resolves to a file on the configured disk is accepted unless you enable preventFileAttachmentPathTampering() (or isolate uploads at the disk/directory level). If instead you are using the spatie/laravel-medialibrary plugin as the file attachment provider, this protection is already implicit — it looks up each data-id against the record’s own media collection via $media->has($file), so a data-id for another record’s media is rejected automatically.
NOTE
preventFileAttachmentPathTampering() needs a record on the form. Without one — for example, on a create page — every existing data-id fails validation unless the allowFilePathUsing callback approves it. New uploads are unaffected.
To apply this check to every RichEditor in your application without repeating it on each field, call configureUsing() in a service provider’s boot() method:
use Filament\Forms\Components\RichEditor;
RichEditor::configureUsing(function (RichEditor $component): void {
$component->preventFileAttachmentPathTampering();
});
Individual fields can still opt out by calling preventFileAttachmentPathTampering(false).
Allowing additional data-id values with a callback
If your application legitimately references an identifier that is not on the record — for example, a “copy from another record” action — pass the allowFilePathUsing argument to approve it. Approved identifiers bypass the validation error:
use Filament\Forms\Components\RichEditor;
RichEditor::make('content')
->preventFileAttachmentPathTampering(
allowFilePathUsing: fn (string $file): bool => str_starts_with($file, 'templates/'),
)
You can inject various utilities into the function passed to allowFilePathUsing as parameters.
了解更多 utility 注入详情。 | Utility | 类型 | 参数 | 描述 |
|---|---|---|---|
| Field | Filament\Forms\Components\Field | $component | The current field component instance. |
| File | string | $file | The submitted `data-id` value being authorized. |
| Get function | Filament\Schemas\Components\Utilities\Get | $get | A function for retrieving values from the current form data. Validation is not run. |
| Livewire | Livewire\Component | $livewire | The Livewire component instance. |
| Eloquent model FQN | ?string<Illuminate\Database\Eloquent\Model> | $model | The Eloquent model FQN for the current schema. |
| Operation | string | $operation | The current operation being performed by the schema. Usually create, edit, or view. |
| Raw state | mixed | $rawState | The current value of the field, before state casts were applied. Validation is not run. |
| Eloquent record | ?Illuminate\Database\Eloquent\Model | $record | The Eloquent record for the current schema. |
| State | mixed | $state | The current value of the field. Validation is not run. |
The validation error message can be customized via validationMessages() using the tampered key:
use Filament\Forms\Components\RichEditor;
RichEditor::make('content')
->preventFileAttachmentPathTampering()
->validationMessages([
'tampered' => 'The content references an image that is not permitted.',
])
Validating uploaded images
You may use the fileAttachmentsAcceptedFileTypes() method to control a list of accepted mime types for uploaded images. By default, image/png, image/jpeg, image/gif, and image/webp are accepted:
use Filament\Forms\Components\RichEditor;
RichEditor::make('content')
->fileAttachmentsAcceptedFileTypes(['image/png', 'image/jpeg'])
You may use the fileAttachmentsMaxSize() method to control the maximum file size for uploaded images. The size is specified in kilobytes. By default, the maximum size is 12288 KB (12 MB):
use Filament\Forms\Components\RichEditor;
RichEditor::make('content')
->fileAttachmentsMaxSize(5120) // 5 MB
Allowing users to resize images
By default, images in the editor cannot be resized by the user. You may enable image resizing using the resizableImages() method:
use Filament\Forms\Components\RichEditor;
RichEditor::make('content')
->resizableImages()
When enabled, users can resize images by clicking on them and dragging the resize handles. The aspect ratio is always preserved when resizing.
As well as allowing a static value, the resizableImages() method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.
了解更多 utility 注入详情。 | Utility | 类型 | 参数 | 描述 |
|---|---|---|---|
| Field | Filament\Forms\Components\Field | $component | The current field component instance. |
| Get function | Filament\Schemas\Components\Utilities\Get | $get | A function for retrieving values from the current form data. Validation is not run. |
| Livewire | Livewire\Component | $livewire | The Livewire component instance. |
| Eloquent model FQN | ?string<Illuminate\Database\Eloquent\Model> | $model | The Eloquent model FQN for the current schema. |
| Operation | string | $operation | The current operation being performed by the schema. Usually create, edit, or view. |
| Raw state | mixed | $rawState | The current value of the field, before state casts were applied. Validation is not run. |
| Eloquent record | ?Illuminate\Database\Eloquent\Model | $record | The Eloquent record for the current schema. |
| State | mixed | $state | The current value of the field. Validation is not run. |
使用自定义 Block
自定义 Block 是用户可以拖拽到富文本编辑器的元素。使用 customBlocks() 方法,你可以定义用户可以插入到富文本编辑器的自定义的 Block:
use Filament\Forms\Components\RichEditor;
RichEditor::make('content')
->customBlocks([
HeroBlock::class,
CallToActionBlock::class,
])
To create a custom block, you can use the following command:
php artisan make:filament-rich-content-custom-block HeroBlock
每个 Block 需要对应的类,继承自 Filament\Forms\Components\RichEditor\RichContentCustomBlock 类。getId() 方法应该为 Block 返回为一标识符,而 getLabel() 方法则返回编辑器的侧边面板中展示的标签:
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';
}
}
当用户将自定义 Block 拖拽到编辑器中时,你可以选择打开模态框以在插入该 Block 前收集用户的额外信息。为此,你可以使用 configureEditorAction() 方法配置插入 Block 时将会打开的模态框:
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'),
]);
}
}
Actiion 上的 schema() 方法可以定义将会在模态框中展示的表单字段。当用户提交表单时,表单数据将会被保存为该 Block 的“配置”。
为自定义 Block 渲染预览
一旦将 Block 插入到编辑器后,你可以使用 toPreviewHtml() 方法为其定义“预览”。该方法返回 Block 插入后展示在编辑器中的 HTML 字符串,它允许用户在保存之前查看该 Block 的外观。你可以在此方法中访问 Block 的 $config,该变量包含插入 Block 之后在模态框中提交的数据:
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() 可以访问 Block 的 $config,从而允许你在标签中显示动态信息:
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']}";
}
}
使用自定义 Block 渲染内容
当渲染富文本内容时,你可以传递自定义 Block 数组到 RichContentRender,用以确保这些 Block 可以正确渲染:
use Filament\Forms\Components\RichEditor\RichContentRenderer;
RichContentRenderer::make($record->content)
->customBlocks([
HeroBlock::class,
CallToActionBlock::class,
])
->toHtml()
每个 Block 类有一个 toHtml() 方法,它返回该 Block 要渲染的 HTML:
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() 方法接收两个参数:$cofig 包含 Block 插入时模态框中提交的配置数据,以及 $data 包含渲染 Block 所需的其他数据。它允许访问配置数据并相应地渲染 Block。数据可以在 customBlocks() 中传入:
use Filament\Forms\Components\RichEditor\RichContentRenderer;
RichContentRenderer::make($record->content)
->customBlocks([
HeroBlock::class => [
'categoryUrl' => $record->category->getUrl(),
],
CallToActionBlock::class,
])
->toHtml()
Grouping custom blocks
You can organize custom blocks into groups using string keys in the customBlocks() array. Blocks passed directly (without a string key) are ungrouped and appear first in the panel:
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,
],
])
Groups are displayed in the order they are defined in the array, with sticky headings in the side panel.
When rendering content with grouped blocks, you can pass the same grouped array structure to the RichContentRenderer. Groups are ignored during rendering — only the block classes are used:
use Filament\Forms\Components\RichEditor\RichContentRenderer;
RichContentRenderer::make($record->content)
->customBlocks([
'Marketing' => [
HeroBlock::class => [
'categoryUrl' => $record->category->getUrl(),
],
CallToActionBlock::class,
],
])
->toHtml()
默认打开自定义 Block 面板
如果你想在加载富文本编辑器时,默认打开 Block 面板,你可以使用 activePanel('customBlocks') 方法:
use Filament\Forms\Components\RichEditor;
RichEditor::make('content')
->customBlocks([
HeroBlock::class,
CallToActionBlock::class,
])
->activePanel('customBlocks')
Styling custom block previews with prose
By default, custom block previews are displayed without prose styling to make styling easier. You can enable prose styling for a block’s preview using the shouldApplyProseStylingToPreview() method. This is useful when you want the preview to display with typography styles like headings, paragraphs, and other prose elements:
use Filament\Forms\Components\RichEditor\RichContentCustomBlock;
class HeadingBlock extends RichContentCustomBlock
{
// ...
/**
* @param array<string, mixed> $config
*/
public static function shouldApplyProseStylingToPreview(array $config): bool
{
return true;
}
}
When shouldApplyProseStylingToPreview() returns true, the block’s preview will be styled with the prose typography styles defined in the rich editor, including proper margins, font sizes, and other text formatting. By default, this method returns false, so previews are displayed with minimal styling.
You can make this decision based on the block’s configuration, allowing different blocks to have different preview styling:
public static function shouldApplyProseStylingToPreview(array $config): bool
{
return ($config['useProseStyle'] ?? false) === true;
}
使用合并标签
合并标签允许用户在其富文本内容中插入“占位符”,这些占位符可以在内容渲染时被动态值替换。这对于插入当前用户姓名或当前日期等内容非常有用。
要在编辑器上注册合并标签,请使用 mergeTags() 方法:
use Filament\Forms\Components\RichEditor;
RichEditor::make('content')
->mergeTags([
'name',
'today',
])
合并标签用双花括号括起来,例如 {{ name }}。内容渲染时,这些标签将被替换为相应的值。
要将合并标签插入内容,用户可以输入 {{ 来搜索要插入的标签。或者,他们可以点击编辑器工具栏中的“合并标签”工具,这将打开一个包含所有合并标签的面板。然后,他们可以将合并标签从编辑器的侧面板拖放到内容中,或者点击插入。
使用合并标签渲染内容
渲染富文本内容时,你可以传递一个值数组来替换合并标签:
use Filament\Forms\Components\RichEditor\RichContentRenderer;
RichContentRenderer::make($record->content)
->mergeTags([
'name' => $record->user->name,
'today' => now()->toFormattedDateString(),
])
->toHtml()
如果你有多个合并标签,或者需要运行一些逻辑来确定它们的值,可以使用一个函数作为每个合并标签的值。当内容中第一次遇到合并标签时,将调用此函数,并将其结果缓存起来,以供后续同名标签使用:
use Filament\Forms\Components\RichEditor\RichContentRenderer;
RichContentRenderer::make($record->content)
->mergeTags([
'name' => fn (): string => $record->user->name,
'today' => now()->toFormattedDateString(),
])
->toHtml()
Using HTML content in merge tags
By default, merge tags render their values as plain text. However, you can render HTML content in merge tags by providing values that implement Laravel’s Htmlable interface. This is useful for inserting formatted content, links, or other HTML elements:
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()
When a merge tag value implements the Htmlable interface (such as HtmlString), the system automatically detects this and renders the HTML content without escaping it. Non-Htmlable values continue to be rendered as plain text for security.
Using custom merge tag labels
You may provide custom labels for merge tags that will be displayed in the editor’s side panel and content preview using an associative array where the keys are the merge tag names and the values are the labels:
use Filament\Forms\Components\RichEditor;
RichEditor::make('content')
->mergeTags([
'name' => 'Full name',
'today' => 'Today\'s date',
])
The labels aren’t saved in the content of the editor and are only used for display purposes.
默认打开合并标签面板
如果你希望在加载富文本编辑器时默认打开合并标签面板,可以使用 activePanel('mergeTags') 方法:
use Filament\Forms\Components\RichEditor;
RichEditor::make('content')
->mergeTags([
'name',
'today',
])
->activePanel('mergeTags')
Using mentions
Mentions allow users to insert references to other records (such as users, issues, or tags) by typing a trigger character. When the user types a trigger character like @, a dropdown appears allowing them to search and select from available options. The selected mention is inserted as a non-editable inline token.
To register mentions on an editor, use the mentions() method with one or more MentionProvider instances:
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\RichEditor\MentionProvider;
RichEditor::make('content')
->mentions([
MentionProvider::make('@')
->items([
1 => 'Jane Doe',
2 => 'John Smith',
]),
])
Each provider is configured with a trigger character (passed to make()) that activates the mention search. You can have multiple providers with different triggers:
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',
]),
])
Searching mentions from the database
For large datasets, you should fetch results dynamically using getSearchResultsUsing(). The callback receives the search term and should return an array of options with the format [id => label].
When using dynamic search results, only the mention’s id is stored in the content. To display the correct label when the content is loaded, you must also provide getLabelsUsing(). This callback receives an array of IDs and should return an array with the format [id => label]:
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()),
])
Rendering content with mentions
When rendering the rich content, you can pass the array of mention providers to the RichContentRenderer to ensure that the mentions are rendered correctly.
You can make mentions link to a URL when rendered using the url() method. The callback receives the mention’s id and label, and should return a URL string:
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
The string returned from the url() closure is rendered directly into the href attribute of an <a> tag, so if any part of the URL is built from user input you should make sure it cannot resolve to a scheme like javascript: or data: that the browser would execute. The simplest way to guarantee this is to wrap the return value in Filament’s Str::sanitizeUrl() helper, which only allows http/https and relative URLs:
use Illuminate\Support\Str;
->url(fn (string $id, string $label): ?string => Str::sanitizeUrl(
route('users.show', $id),
))
If you intentionally want to allow a javascript: URL (for example, to wire a mention to an Alpine.js handler), skip the helper and return the raw value — just make sure none of the components of that URL come from untrusted user input.
注册富文本内容属性
富文本编辑器配置中有一些元素同时适用于编辑器和渲染器。例如,如果你使用了私有图片、自定义 Block、合并标签、mentions或插件,则需要确保在两个地方使用相同的配置。为此,Filament 提供了一种注册富文本内容属性的方法,这些属性可以在编辑器和渲染器中使用。If a plugin implements HasFileAttachmentProvider, the file attachment provider is automatically resolved from the plugin, so you do not need to call fileAttachmentProvider() on the attribute or on the renderer.
要在 Eloquent 模型上注册富文本内容属性,你应该使用 InteractsWithRichContent trait 并实现 HasRichContent 接口。这样你就可以在 setUpRichContent() 方法中注册这些属性:
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 组件时,都会使用对应属性注册的配置:
use Filament\Forms\Components\RichEditor;
RichEditor::make('content')
为了轻松地从具有给定配置的模型中渲染丰富的内容 HTML,你可以调用模型上的 renderRichContent() 方法,并传递属性的名称:
{!! $record->renderRichContent('content') !!}
或者,你也可以获取 Htmlable 对象,以不转义 HTML 进行渲染。
{{ $record->getRichContentAttribute('content') }}
在表格中使用 文本列 或在信息列表中使用文本条目时,你无需手动渲染富文本内容。Filament 会自动为你完成此操作:
use Filament\Infolists\Components\TextEntry;
use Filament\Tables\Columns\TextColumn;
TextColumn::make('content')
TextEntry::make('content')
富文本编辑器扩展
你可以为富文本编辑器创建插件,它允许你将自定义 TipTap 扩展以及自定义工具栏按钮添加到编辑器和渲染器。创建一个实现 RichContentPlugin 接口的新类:
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() 方法为富文本编辑器和富文本内容渲染器注册插件:
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(),
])
Enabling or disabling toolbar buttons from a plugin
By default, when a plugin provides tools via getEditorTools(), those tools are registered but not automatically shown in the toolbar. The user needs to manually add them using toolbarButtons() or enableToolbarButtons().
If you want your plugin to automatically enable or disable toolbar buttons, you can implement the HasToolbarButtons interface alongside RichContentPlugin. This is an optional, separate interface:
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 [];
}
}
The getEnabledToolbarButtons() method returns button names to add to the toolbar. The getDisabledToolbarButtons() method returns button names to remove from the toolbar.
Plugin toolbar modifications are applied before user-level modifications. This means the user can always override the plugin’s behavior using enableToolbarButtons() or disableToolbarButtons():
RichEditor::make('content')
->plugins([
HighlightRichContentPlugin::make(),
])
->disableToolbarButtons(['highlightWithCustomColor'])
设置 TipTap JavaScript 扩展
Filament 能够异步加载 TipTap 的 JavaScript 扩展。为此,你需要创建一个包含扩展的 JavaScript 文件,并将其注册到插件的 getTipTapJsExtensions() 方法中。
例如,如果你想使用 TipTap 高亮显示扩展,请确保先安装:
npm install @tiptap/extension-highlight --save-dev
然后,新建一个 JavaScript 文件导入扩展。本例中,在 resources/js/filament/rich-content-plugin 目录中新建了一个名为 highlight.js 的文件,并添加了如下代码:
import Highlight from '@tiptap/extension-highlight'
export default Highlight.configure({
multicolor: true,
})
你可以使用 esbuild 编译该文件。可以使用 npm 按照 Esbuild:
npm install esbuild --save-dev
你必须创建一个 esbuild 脚本来编译该文件。你可以将其放在任何位置,比如 bin/build.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,请运行如下命令:
node bin/build.js
你应该在服务提供者(如 AppServiceProvider)的 boot() 方法中对其进行注册,并使用 loadedOnRequest(),这样在页面上加载富文本编辑器之前就不会下载它:
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 命令:
php artisan filament:assets
在插件对象中,其 getTipTapJsExtensions() 方法应该返回刚刚创建的 JavaScript 文件的路径。既然,它以及在 FilamentAsset 中注册了,你可以使用 getScriptSrc() 方法获取该文件的 URL:
use Filament\Support\Facades\FilamentAsset;
/**
* @return array<string>
*/
public function getTipTapJsExtensions(): array
{
return [
FilamentAsset::getScriptSrc('rich-content-plugins/highlight'),
];
}
Sharing the bundled TipTap/ProseMirror instance
When custom JavaScript extensions import from @tiptap/core or @tiptap/pm/*, each compiled extension includes its own copy of these packages. This wastes around 150-200 KB per extension and — more importantly — creates multiple ProseMirror instances on the page. Because ProseMirror relies heavily on instanceof checks (for Node, Mark, Plugin, DecorationSet, etc.), extensions that bundle their own copy of these modules can fail to interoperate with the editor’s core.
To avoid this, Filament exposes the bundled TipTap and ProseMirror modules on window.FilamentRichEditor.tiptap:
window.FilamentRichEditor.tiptap = {
core, // @tiptap/core
pmState, // @tiptap/pm/state
pmView, // @tiptap/pm/view
pmModel, // @tiptap/pm/model
}
You can reference these modules directly in your extension:
const { Node, mergeAttributes } = window.FilamentRichEditor.tiptap.core
const { Plugin, PluginKey } = window.FilamentRichEditor.tiptap.pmState
export default Node.create({
name: 'myExtension',
// ...
})
Alternatively, you can configure your build to intercept imports of @tiptap/core and @tiptap/pm/{state,view,model} and resolve them from the global at runtime. This lets you keep writing normal import statements in your extension source — other @tiptap/* packages (like @tiptap/extension-highlight) continue to be bundled as usual. The following esbuild plugin inspects each intercepted package’s real named exports at build time and rewrites the imports to read from window.FilamentRichEditor.tiptap:
npm install --save-dev @tiptap/core @tiptap/pm
// 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',
})
NOTE
window.FilamentRichEditor.tiptap is assigned when the rich editor bundle loads, which happens before getTipTapJsExtensions() URLs are fetched. If you need to use the modules in a context where the rich editor has not yet loaded, bundle your own copies instead.
The esbuild plugin above reads the named exports from your locally-installed @tiptap/core and @tiptap/pm at build time, so keep those versions roughly in sync with the version bundled by Filament — otherwise a newer named export referenced in your extension may be undefined at runtime.
Still need help? Join our Discord community or open a GitHub discussion