feat: ability for RichEditor plugins to provide enabled/disabled toolbar buttons (#19260)

* WIP

* chore: fix code style

* Fix tests

* fix and clean up

---------

Co-authored-by: ralphjsmit <ralphjsmit@users.noreply.github.com>
Co-authored-by: Dan Harrin <git@danharrin.com>
This commit is contained in:
Ralph J. Smit
2026-02-28 22:12:31 +00:00
committed by GitHub
co-authored by ralphjsmit Dan Harrin
parent 3f8d4e0b3d
commit 3a3db66ba3
7 changed files with 373 additions and 33 deletions
+44
View File
@@ -949,6 +949,50 @@ RichContentRenderer::make($record->content)
])
```
### 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:
```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 [];
}
}
```
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()`:
```php
RichEditor::make('content')
->plugins([
HighlightRichContentPlugin::make(),
])
->disableToolbarButtons(['highlightWithCustomColor'])
```
### Setting up a TipTap JavaScript extension
Filament is able to asynchronously load JavaScript extensions for TipTap. To do this, you need to create a JavaScript file that contains the extension, and register it in the `getTipTapJsExtensions()` method of your [plugin](#extending-the-rich-editor).
@@ -80,8 +80,12 @@ trait InteractsWithToolbarButtons
// Start with either custom buttons or default buttons
$buttons = $this->evaluate($this->toolbarButtons) ?? $this->getDefaultToolbarButtons(); /** @phpstan-ignore method.notFound */
// Apply all queued modifications in order
foreach ($this->toolbarButtonsModifications as $modification) {
// Apply all queued modifications in order.
// Extra modifications (e.g. from plugins) are applied first,
// so that user-level modifications always take precedence.
$modifications = [...$this->getExtraToolbarButtonsModifications(), ...$this->toolbarButtonsModifications];
foreach ($modifications as $modification) {
$buttons = match ($modification['type']) {
'disableAll' => [],
'disable' => $this->applyDisableToolbarButtonsModification($buttons, $modification['buttons']),
@@ -152,6 +156,14 @@ trait InteractsWithToolbarButtons
);
}
/**
* @return array<array{type: string, buttons?: array<string | array<string | array<string>>>}>
*/
protected function getExtraToolbarButtonsModifications(): array
{
return [];
}
/**
* @return array<string | array<string>>
*/
@@ -13,6 +13,7 @@ use Filament\Forms\Components\RichEditor\EditorCommand;
use Filament\Forms\Components\RichEditor\FileAttachmentProviders\Contracts\FileAttachmentProvider;
use Filament\Forms\Components\RichEditor\MentionProvider;
use Filament\Forms\Components\RichEditor\Models\Contracts\HasRichContent;
use Filament\Forms\Components\RichEditor\Plugins\Contracts\HasToolbarButtons;
use Filament\Forms\Components\RichEditor\Plugins\Contracts\RichContentPlugin;
use Filament\Forms\Components\RichEditor\RichContentAttribute;
use Filament\Forms\Components\RichEditor\RichContentCustomBlock;
@@ -732,6 +733,40 @@ class RichEditor extends Field implements Contracts\CanBeLengthConstrained
];
}
/**
* @return array<array{type: string, buttons?: array<string | array<string | array<string>>>}>
*/
protected function getExtraToolbarButtonsModifications(): array
{
$modifications = [];
foreach ($this->getPlugins() as $plugin) {
if (! ($plugin instanceof HasToolbarButtons)) {
continue;
}
$enabledButtons = $plugin->getEnabledToolbarButtons();
if (filled($enabledButtons)) {
$modifications[] = [
'type' => 'enable',
'buttons' => $enabledButtons,
];
}
$disabledButtons = $plugin->getDisabledToolbarButtons();
if (filled($disabledButtons)) {
$modifications[] = [
'type' => 'disable',
'buttons' => $disabledButtons,
];
}
}
return $modifications;
}
/**
* @return array<string | array<string>>
*/
@@ -0,0 +1,16 @@
<?php
namespace Filament\Forms\Components\RichEditor\Plugins\Contracts;
interface HasToolbarButtons
{
/**
* @return array<string | array<string | array<string>>>
*/
public function getEnabledToolbarButtons(): array;
/**
* @return array<string>
*/
public function getDisabledToolbarButtons(): array;
}
@@ -0,0 +1,69 @@
<?php
namespace Filament\Tests\Fixtures\RichEditor;
use Filament\Actions\Action;
use Filament\Forms\Components\RichEditor\Plugins\Contracts\HasToolbarButtons;
use Filament\Forms\Components\RichEditor\Plugins\Contracts\RichContentPlugin;
use Filament\Forms\Components\RichEditor\RichEditorTool;
use Tiptap\Core\Extension;
class TestRichContentPlugin implements HasToolbarButtons, RichContentPlugin
{
/**
* @param array<string | array<string | array<string>>> $enabledButtons
* @param array<string> $disabledButtons
*/
public function __construct(
protected array $enabledButtons = [],
protected array $disabledButtons = [],
) {}
/**
* @return array<Extension>
*/
public function getTipTapPhpExtensions(): array
{
return [];
}
/**
* @return array<string>
*/
public function getTipTapJsExtensions(): array
{
return [];
}
/**
* @return array<RichEditorTool>
*/
public function getEditorTools(): array
{
return [];
}
/**
* @return array<Action>
*/
public function getEditorActions(): array
{
return [];
}
/**
* @return array<string | array<string | array<string>>>
*/
public function getEnabledToolbarButtons(): array
{
return $this->enabledButtons;
}
/**
* @return array<string>
*/
public function getDisabledToolbarButtons(): array
{
return $this->disabledButtons;
}
}
@@ -0,0 +1,43 @@
<?php
namespace Filament\Tests\Fixtures\RichEditor;
use Filament\Actions\Action;
use Filament\Forms\Components\RichEditor\Plugins\Contracts\RichContentPlugin;
use Filament\Forms\Components\RichEditor\RichEditorTool;
use Tiptap\Core\Extension;
class TestRichContentPluginWithoutToolbarButtons implements RichContentPlugin
{
/**
* @return array<Extension>
*/
public function getTipTapPhpExtensions(): array
{
return [];
}
/**
* @return array<string>
*/
public function getTipTapJsExtensions(): array
{
return [];
}
/**
* @return array<RichEditorTool>
*/
public function getEditorTools(): array
{
return [];
}
/**
* @return array<Action>
*/
public function getEditorActions(): array
{
return [];
}
}
+152 -31
View File
@@ -6,6 +6,8 @@ use Filament\Forms\Components\RichEditor\RichContentRenderer;
use Filament\Schemas\Schema;
use Filament\Tests\Fixtures\Forms\RichEditor\PluginWithFileAttachmentProvider;
use Filament\Tests\Fixtures\Livewire\Livewire;
use Filament\Tests\Fixtures\RichEditor\TestRichContentPlugin;
use Filament\Tests\Fixtures\RichEditor\TestRichContentPluginWithoutToolbarButtons;
use Filament\Tests\Fixtures\Models\PostWithRichContent;
use Filament\Tests\TestCase;
use Illuminate\Validation\ValidationException;
@@ -55,11 +57,16 @@ test('can get default toolbar buttons using `getDefaultToolbarButtons()`', funct
});
test('can overwrite toolbar buttons array using `toolbarButtons()`', function (): void {
$richEditor = RichEditor::make('content')
->toolbarButtons([
['bold', 'italic'],
['undo', 'redo'],
]);
$richEditor = Schema::make(Livewire::make())
->statePath('data')
->components([
RichEditor::make('content')
->toolbarButtons([
['bold', 'italic'],
['undo', 'redo'],
]),
])
->getComponents()[0];
$buttons = $richEditor->getToolbarButtons();
@@ -71,10 +78,15 @@ test('can overwrite toolbar buttons array using `toolbarButtons()`', function ()
});
test('can overwrite toolbar buttons with closure using `toolbarButtons()`', function (): void {
$richEditor = RichEditor::make('content')
->toolbarButtons(fn () => [
['bold', 'italic'],
]);
$richEditor = Schema::make(Livewire::make())
->statePath('data')
->components([
RichEditor::make('content')
->toolbarButtons(fn () => [
['bold', 'italic'],
]),
])
->getComponents()[0];
$buttons = $richEditor->getToolbarButtons();
@@ -197,12 +209,17 @@ test('can check if custom toolbar buttons are set using `hasCustomToolbarButtons
});
test('toolbar buttons are properly grouped by `getToolbarButtons()`', function (): void {
$richEditor = RichEditor::make('content')
->toolbarButtons([
['bold', 'italic'],
'underline',
'strike',
]);
$richEditor = Schema::make(Livewire::make())
->statePath('data')
->components([
RichEditor::make('content')
->toolbarButtons([
['bold', 'italic'],
'underline',
'strike',
]),
])
->getComponents()[0];
$buttons = $richEditor->getToolbarButtons();
@@ -217,12 +234,17 @@ test('toolbar buttons are properly grouped by `getToolbarButtons()`', function (
});
test('blank button groups are filtered out by `getToolbarButtons()`', function (): void {
$richEditor = RichEditor::make('content')
->toolbarButtons([
['bold', 'italic'],
[],
['undo', 'redo'],
]);
$richEditor = Schema::make(Livewire::make())
->statePath('data')
->components([
RichEditor::make('content')
->toolbarButtons([
['bold', 'italic'],
[],
['undo', 'redo'],
]),
])
->getComponents()[0];
$buttons = $richEditor->getToolbarButtons();
@@ -276,22 +298,32 @@ test('`hasFileAttachments()` returns false when `attachFiles` button is removed
});
test('`hasFileAttachments()` returns true when `attachFiles` is in custom toolbar buttons', function (): void {
$richEditor = RichEditor::make('content')
->toolbarButtons([
['bold', 'italic'],
['attachFiles'],
]);
$richEditor = Schema::make(Livewire::make())
->statePath('data')
->components([
RichEditor::make('content')
->toolbarButtons([
['bold', 'italic'],
['attachFiles'],
]),
])
->getComponents()[0];
expect($richEditor->hasFileAttachments())->toBeTrue()
->and($richEditor->hasToolbarButton('attachFiles'))->toBeTrue();
});
test('`hasFileAttachments()` returns false with custom toolbar buttons without `attachFiles`', function (): void {
$richEditor = RichEditor::make('content')
->toolbarButtons([
['bold', 'italic'],
['undo', 'redo'],
]);
$richEditor = Schema::make(Livewire::make())
->statePath('data')
->components([
RichEditor::make('content')
->toolbarButtons([
['bold', 'italic'],
['undo', 'redo'],
]),
])
->getComponents()[0];
expect($richEditor->hasFileAttachments())->toBeFalse()
->and($richEditor->hasToolbarButton('attachFiles'))->toBeFalse();
@@ -367,6 +399,95 @@ test('`fileAttachments(true)` does not force `attachFiles` button to appear when
->and($richEditor->hasToolbarButton('attachFiles'))->toBeFalse();
});
test('plugin implementing `HasToolbarButtons` can enable toolbar buttons', function (): void {
$richEditor = Schema::make(Livewire::make())
->statePath('data')
->components([
RichEditor::make('content')
->plugins([new TestRichContentPlugin(enabledButtons: ['highlight'])]),
])
->getComponents()[0];
$flatButtons = array_merge(...$richEditor->getToolbarButtons());
expect($flatButtons)
->toContain('highlight')
->toContain('bold');
});
test('plugin implementing `HasToolbarButtons` can disable toolbar buttons', function (): void {
$richEditor = Schema::make(Livewire::make())
->statePath('data')
->components([
RichEditor::make('content')
->plugins([new TestRichContentPlugin(disabledButtons: ['bold', 'italic'])]),
])
->getComponents()[0];
$flatButtons = array_merge(...$richEditor->getToolbarButtons());
expect($flatButtons)
->not->toContain('bold')
->not->toContain('italic')
->toContain('underline')
->toContain('undo');
});
test('user `disableToolbarButtons()` overrides plugin-enabled toolbar buttons', function (): void {
$schema = Schema::make(Livewire::make())
->statePath('data')
->components([
RichEditor::make('content')
->plugins([new TestRichContentPlugin(enabledButtons: ['highlight'])]),
]);
$richEditor = $schema->getComponents()[0];
$richEditor->disableToolbarButtons(['highlight']);
$flatButtons = array_merge(...$richEditor->getToolbarButtons());
expect($flatButtons)
->not->toContain('highlight')
->toContain('bold');
});
test('user `enableToolbarButtons()` overrides plugin-disabled toolbar buttons', function (): void {
$schema = Schema::make(Livewire::make())
->statePath('data')
->components([
RichEditor::make('content')
->plugins([new TestRichContentPlugin(disabledButtons: ['bold'])]),
]);
$richEditor = $schema->getComponents()[0];
$richEditor->enableToolbarButtons(['bold']);
$flatButtons = array_merge(...$richEditor->getToolbarButtons());
expect($flatButtons)
->toContain('bold');
});
test('plugin without `HasToolbarButtons` does not affect toolbar buttons', function (): void {
$richEditor = Schema::make(Livewire::make())
->statePath('data')
->components([
RichEditor::make('content')
->plugins([new TestRichContentPluginWithoutToolbarButtons]),
])
->getComponents()[0];
$defaultRichEditor = Schema::make(Livewire::make())
->statePath('data')
->components([
RichEditor::make('content'),
])
->getComponents()[0];
expect($richEditor->getToolbarButtons())
->toEqual($defaultRichEditor->getToolbarButtons());
});
test('rich content attribute resolves file attachment provider from plugin implementing `HasFileAttachmentProvider` without calling `fileAttachmentProvider()`', function (): void {
$record = new PostWithRichContent;